Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,39 @@ func main() {
}
```

## Showing The Expiry Date

`ActivateKey` and `FloatingCheckout` return `ExpirationDate`: when the license
expires, as an ISO 8601 string, or `nil` if it never expires. On
`FloatingCheckoutResponse` this is the license expiry; `ExpiresAt` is the
floating seat lease.

```go
res, err := client.ActivateKey(keymint.ActivateKeyParams{
ProductID: productId,
LicenseKey: licenseKey,
HostID: keymint.String(hostId),
})
if err != nil {
// handle error
}

if res.ExpirationDate == nil {
fmt.Println("License never expires")
} else if exp, err := time.Parse(time.RFC3339, *res.ExpirationDate); err == nil {
daysLeft := int(math.Ceil(time.Until(exp).Hours() / 24))
if daysLeft < 0 {
daysLeft = 0
}
fmt.Printf("Expires %s (%d days left)\n", exp.Format("2006-01-02"), daysLeft)
}
```

Store the last `ExpirationDate` your app received. An expired license fails
activation with the same generic error as an invalid key, so the stored date
lets your app tell the user the license expired on that date rather than
calling the key invalid.

## Machine Identity
Keymint provides utilities to uniquely identify machines for node-locking:

Expand Down
8 changes: 8 additions & 0 deletions Types.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ type ActivateKeyResponse struct {
Version map[string]interface{} `json:"version,omitempty"`
// AllowedHosts is an optional list of authorized machine IDs.
AllowedHosts []string `json:"allowedHosts,omitempty"`
// ExpirationDate is when the license expires, as an ISO 8601 string
// (e.g. "2027-03-01T00:00:00.000Z"). It is nil when the license never
// expires. An expired license fails activation with the generic
// invalid-key error, so store the last value received.
ExpirationDate *string `json:"expirationDate,omitempty"`
}

// DeactivateKeyParams represents parameters for the deactivateKey API endpoint.
Expand Down Expand Up @@ -515,6 +520,9 @@ type FloatingCheckoutResponse struct {
LicenseeName *string `json:"licenseeName,omitempty"`
// LicenseeEmail is the optional email of the customer licensee.
LicenseeEmail *string `json:"licenseeEmail,omitempty"`
// ExpirationDate is when the license expires, as an ISO 8601 string, or
// nil when it never expires. It is not the seat lease; see ExpiresAt.
ExpirationDate *string `json:"expirationDate,omitempty"`
}

// FloatingHeartbeatParams represents parameters for the floating license heartbeat API endpoint.
Expand Down
64 changes: 64 additions & 0 deletions contract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,67 @@ func TestApiErrorsExposeNestedMessage(t *testing.T) {
t.Fatalf("status not captured: %#v", apiErr.Status)
}
}

// expirationCases covers the three shapes the API can send: a date, an
// explicit null (never expires), and an older server that omits the field.
var expirationCases = []struct {
name string
body string
want *string
}{
{"date", `"expirationDate":"2027-03-01T00:00:00.000Z",`, String("2027-03-01T00:00:00.000Z")},
{"null", `"expirationDate":null,`, nil},
{"absent", ``, nil},
}

func checkExpiration(t *testing.T, got, want *string) {
t.Helper()
switch {
case want == nil && got != nil:
t.Fatalf("expected nil ExpirationDate, got %q", *got)
case want != nil && got == nil:
t.Fatalf("expected ExpirationDate %q, got nil", *want)
case want != nil && *got != *want:
t.Fatalf("expected ExpirationDate %q, got %q", *want, *got)
}
}

func TestActivateKeyDeserializesExpirationDate(t *testing.T) {
for _, tc := range expirationCases {
t.Run(tc.name, func(t *testing.T) {
c, done := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"code":0,"message":"License valid",` + tc.body + `"metadata":{"hostId":"h1"}}`))
})
defer done()

res, err := c.ActivateKey(ActivateKeyParams{ProductID: "p", LicenseKey: "k", HostID: String("h1")})
if err != nil {
t.Fatalf("ActivateKey failed: %v", err)
}
checkExpiration(t, res.ExpirationDate, tc.want)
})
}
}

func TestFloatingCheckoutDeserializesExpirationDate(t *testing.T) {
for _, tc := range expirationCases {
t.Run(tc.name, func(t *testing.T) {
c, done := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"code":0,"message":"ok","sessionId":"s","sessionSecret":"sec","nextNonce":"n",` +
`"expiresAt":"2026-10-26T12:00:00.000Z","heartbeatInterval":60,` + tc.body + `"currentSessions":1,"maxSessions":5}`))
})
defer done()

res, err := c.FloatingCheckout(FloatingCheckoutParams{ProductID: "p", LicenseKey: "k", HostID: "h"})
if err != nil {
t.Fatalf("FloatingCheckout failed: %v", err)
}
if res.ExpiresAt != "2026-10-26T12:00:00.000Z" {
t.Fatalf("seat lease ExpiresAt changed: %q", res.ExpiresAt)
}
checkExpiration(t, res.ExpirationDate, tc.want)
})
}
}
Loading