项目作者: keygen-sh

项目描述 :
Keygen SDK for Go. Integrate license activation and automatic updates for Go binaries.
高级语言: Go
项目地址: git://github.com/keygen-sh/keygen-go.git
创建时间: 2021-10-11T13:25:01Z
项目社区:https://github.com/keygen-sh/keygen-go

开源协议:Other

下载


Keygen Go SDK

godoc reference
CI

Package keygen allows Go programs to
license and remotely update themselves using the keygen.sh service.

Installing

  1. go get github.com/keygen-sh/keygen-go/v3

Config

keygen.Account

Account is your Keygen account ID used globally in the SDK. All requests will be made
to this account. This should be hard-coded into your app.

  1. keygen.Account = "1fddcec8-8dd3-4d8d-9b16-215cac0f9b52"

keygen.Product

Product is your Keygen product ID used globally in the SDK. All license validations and
upgrade requests will be scoped to this product. This should be hard-coded into your app.

  1. keygen.Product = "1f086ec9-a943-46ea-9da4-e62c2180c2f4"

keygen.LicenseKey

LicenseKey is a license key belonging to the end-user (licensee). This will be used for license
validations, activations, deactivations and upgrade requests. You will need to prompt the
end-user for this value.

You will need to set the license policy’s authentication strategy to LICENSE or MIXED.

Setting LicenseKey will take precedence over Token.

  1. keygen.LicenseKey = "C1B6DE-39A6E3-DE1529-8559A0-4AF593-V3"

keygen.Token

Token is an activation token belonging to the licensee. This will be used for license validations,
activations, deactivations and upgrade requests. You will need to prompt the end-user for this.

You will need to set the license policy’s authentication strategy to TOKEN or MIXED.

  1. keygen.Token = "activ-d66e044ddd7dcc4169ca9492888435d3v3"

keygen.PublicKey

PublicKey is your Keygen account’s hex-encoded Ed25519 public key, used for verifying signed license keys
and API response signatures. When set, API response signatures will automatically be verified. You may
leave it blank to skip verifying response signatures. This should be hard-coded into your app.

  1. keygen.PublicKey = "e8601e48b69383ba520245fd07971e983d06d22c4257cfd82304601479cee788"

keygen.Logger

Logger is a leveled logger implementation used for printing debug, informational, warning, and
error messages. The default log level is LogLevelError. You may provide your own logger which
implements LeveledLogger.

  1. keygen.Logger = &CustomLogger{Level: keygen.LogLevelDebug}

Usage

The following top-level functions are available. We recommend starting here.

keygen.Validate(ctx, fingerprints …string)

To validate a license, configure keygen.Account and keygen.Product with your Keygen account
details. Then prompt the end-user for their license key or token and set keygen.LicenseKey
or keygen.Token, respectively.

The Validate method accepts zero or more fingerprints, which can be used to scope a
license validation to a particular device fingerprint and its hardware components.
The first fingerprint should be a machine fingerprint, and the rest are optional
component fingerprints.

It will return a License object as well as any validation errors that occur. The License
object can be used to perform additional actions, such as license.Activate(fingerprint).

  1. license, err := keygen.Validate(context.Background(), fingerprint)
  2. switch {
  3. case err == keygen.ErrLicenseNotActivated:
  4. panic("license is not activated!")
  5. case err == keygen.ErrLicenseExpired:
  6. panic("license is expired!")
  7. case err != nil:
  8. panic("license is invalid!")
  9. }
  10. fmt.Println("License is valid!")

keygen.Upgrade(ctx, options keygen.UpgradeOptions)

Check for an upgrade. When an upgrade is available, a Release will be returned which will
allow the update to be installed, replacing the currently running binary. When an upgrade
is not available, an ErrUpgradeNotAvailable error will be returned indicating the current
version is up-to-date.

When a PublicKey is provided, and the release has a Signature, the signature will be
cryptographically verified using Ed25519ph before installing. The PublicKey MUST be a
personal Ed25519ph public key. It MUST NOT be your Keygen account’s public key (method
will panic if public keys match).

You can read more about generating a personal keypair and about code signing here.

  1. opts := keygen.UpgradeOptions{CurrentVersion: "1.0.0", Channel: "stable", PublicKey: "5ec69b78d4b5d4b624699cef5faf3347dc4b06bb807ed4a2c6740129f1db7159"}
  2. ctx := context.Background()
  3. // Check for an upgrade
  4. release, err := keygen.Upgrade(ctx, opts)
  5. switch {
  6. case err == keygen.ErrUpgradeNotAvailable:
  7. fmt.Println("No upgrade available, already at the latest version!")
  8. return
  9. case err != nil:
  10. fmt.Println("Upgrade check failed!")
  11. return
  12. }
  13. // Install the upgrade
  14. if err := release.Install(ctx); err != nil {
  15. panic("upgrade install failed!")
  16. }
  17. fmt.Println("Upgrade complete! Please restart.")

To quickly generate a keypair, use Keygen’s CLI:

  1. keygen genkey

Examples

Below are various implementation examples, covering common licensing scenarios and use cases.

License Activation

Validate the license for a particular device fingerprint, and activate when needed. We’re
using machineid for fingerprinting, which
is cross-platform, using the operating system’s native GUID.

  1. package main
  2. import (
  3. "context"
  4. "github.com/keygen-sh/keygen-go/v3"
  5. "github.com/keygen-sh/machineid"
  6. )
  7. func main() {
  8. keygen.Account = "YOUR_KEYGEN_ACCOUNT_ID"
  9. keygen.Product = "YOUR_KEYGEN_PRODUCT_ID"
  10. keygen.LicenseKey = "A_KEYGEN_LICENSE_KEY"
  11. fingerprint, err := machineid.ProtectedID(keygen.Product)
  12. if err != nil {
  13. panic(err)
  14. }
  15. ctx := context.Background()
  16. // Validate the license for the current fingerprint
  17. license, err := keygen.Validate(ctx, fingerprint)
  18. switch {
  19. case err == keygen.ErrLicenseNotActivated:
  20. // Activate the current fingerprint
  21. machine, err := license.Activate(ctx, fingerprint)
  22. switch {
  23. case err == keygen.ErrMachineLimitExceeded:
  24. panic("machine limit has been exceeded!")
  25. case err != nil:
  26. panic("machine activation failed!")
  27. }
  28. case err == keygen.ErrLicenseExpired:
  29. panic("license is expired!")
  30. case err != nil:
  31. panic("license is invalid!")
  32. }
  33. fmt.Println("License is activated!")
  34. }

Automatic Upgrades

Check for an upgrade and automatically replace the current binary with the newest version.

  1. package main
  2. import (
  3. "context"
  4. "github.com/keygen-sh/keygen-go/v3"
  5. )
  6. // The current version of the program
  7. const CurrentVersion = "1.0.0"
  8. func main() {
  9. keygen.PublicKey = "YOUR_KEYGEN_PUBLIC_KEY"
  10. keygen.Account = "YOUR_KEYGEN_ACCOUNT_ID"
  11. keygen.Product = "YOUR_KEYGEN_PRODUCT_ID"
  12. keygen.LicenseKey = "A_KEYGEN_LICENSE_KEY"
  13. fmt.Printf("Current version: %s\n", CurrentVersion)
  14. fmt.Println("Checking for upgrades...")
  15. ctx := context.Background()
  16. opts := keygen.UpgradeOptions{CurrentVersion: CurrentVersion, Channel: "stable", PublicKey: "YOUR_COMPANY_PUBLIC_KEY"}
  17. // Check for upgrade
  18. release, err := keygen.Upgrade(ctx, opts)
  19. switch {
  20. case err == keygen.ErrUpgradeNotAvailable:
  21. fmt.Println("No upgrade available, already at the latest version!")
  22. return
  23. case err != nil:
  24. fmt.Println("Upgrade check failed!")
  25. return
  26. }
  27. fmt.Printf("Upgrade available! Newest version: %s\n", release.Version)
  28. fmt.Println("Installing upgrade...")
  29. // Download the upgrade and install it
  30. err = release.Install(ctx)
  31. if err != nil {
  32. panic("upgrade install failed!")
  33. }
  34. fmt.Printf("Upgrade complete! Installed version: %s\n", release.Version)
  35. fmt.Println("Restart to finish installation...")
  36. }

Monitor Machine Heartbeats

Monitor a machine’s heartbeat, and automatically deactivate machines in case of a crash
or an unresponsive node. We recommend using a random UUID fingerprint for activating
nodes in cloud-based scenarios, since nodes may share underlying hardware.

  1. package main
  2. import (
  3. "context"
  4. "github.com/google/uuid"
  5. "github.com/keygen-sh/keygen-go/v3"
  6. )
  7. func main() {
  8. keygen.Account = "YOUR_KEYGEN_ACCOUNT_ID"
  9. keygen.Product = "YOUR_KEYGEN_PRODUCT_ID"
  10. keygen.LicenseKey = "A_KEYGEN_LICENSE_KEY"
  11. // The current device's fingerprint (could be e.g. MAC, mobo ID, GUID, etc.)
  12. fingerprint := uuid.New().String()
  13. ctx := context.Background()
  14. // Keep our example process alive
  15. done := make(chan bool, 1)
  16. // Validate the license for the current fingerprint
  17. license, err := keygen.Validate(ctx, fingerprint)
  18. switch {
  19. case err == keygen.ErrLicenseNotActivated:
  20. // Activate the current fingerprint
  21. machine, err := license.Activate(ctx, fingerprint)
  22. if err != nil {
  23. fmt.Println("machine activation failed!")
  24. panic(err)
  25. }
  26. // Handle SIGINT and gracefully deactivate the machine
  27. sigs := make(chan os.Signal, 1)
  28. signal.Notify(sigs, os.Interrupt)
  29. go func() {
  30. for sig := range sigs {
  31. fmt.Printf("Caught %v, deactivating machine and gracefully exiting...\n", sig)
  32. if err := machine.Deactivate(ctx); err != nil {
  33. panic(err)
  34. }
  35. fmt.Println("Machine was deactivated!")
  36. fmt.Println("Exiting...")
  37. done <- true
  38. }
  39. }()
  40. // Start a heartbeat monitor for the current machine
  41. if err := machine.Monitor(ctx); err != nil {
  42. fmt.Println("Machine heartbeat monitor failed to start!")
  43. panic(err)
  44. }
  45. fmt.Println("Machine is activated and monitored!")
  46. case err != nil:
  47. fmt.Println("License is invalid!")
  48. panic(err)
  49. }
  50. fmt.Println("License is valid!")
  51. <-done
  52. }

Offline License Files

Cryptographically verify and decrypt an encrypted license file. This is useful for checking if a license
file is genuine in offline or air-gapped environments. Returns the license file’s dataset and any
errors that occurred during verification and decryption, e.g. ErrLicenseFileNotGenuine.

When decrypting a license file, you MUST provide the license’s key as the decryption key.

When initializing a LicenseFile, Certificate is required.

Requires that keygen.PublicKey is set.

  1. package main
  2. import "github.com/keygen-sh/keygen-go/v3"
  3. func main() {
  4. keygen.PublicKey = "YOUR_KEYGEN_PUBLIC_KEY"
  5. // Read the license file
  6. cert, err := ioutil.ReadFile("/etc/example/license.lic")
  7. if err != nil {
  8. panic("license file is missing")
  9. }
  10. // Verify the license file's signature
  11. lic := &keygen.LicenseFile{Certificate: string(cert)}
  12. err = lic.Verify()
  13. switch {
  14. case err == keygen.ErrLicenseFileNotGenuine:
  15. panic("license file is not genuine!")
  16. case err != nil:
  17. panic(err)
  18. }
  19. // Use the license key to decrypt the license file
  20. dataset, err := lic.Decrypt("A_KEYGEN_LICENSE_KEY")
  21. switch {
  22. case err == keygen.ErrSystemClockUnsynced:
  23. panic("system clock tampering detected!")
  24. case err == keygen.ErrLicenseFileExpired:
  25. panic("license file is expired!")
  26. case err != nil:
  27. panic(err)
  28. }
  29. fmt.Println("License file is genuine!")
  30. fmt.Printf("Decrypted dataset: %v\n", dataset)
  31. }

Offline License Keys

Cryptographically verify and decode a signed license key. This is useful for checking if a license
key is genuine in offline or air-gapped environments. Returns the key’s decoded dataset and any
errors that occurred during cryptographic verification, e.g. ErrLicenseKeyNotGenuine.

When initializing a License, Scheme and Key are required.

Requires that keygen.PublicKey is set.

  1. package main
  2. import "github.com/keygen-sh/keygen-go/v3"
  3. func main() {
  4. keygen.PublicKey = "YOUR_KEYGEN_PUBLIC_KEY"
  5. // Verify the license key's signature and decode embedded dataset
  6. license := &keygen.License{Scheme: keygen.SchemeCodeEd25519, Key: "A_SIGNED_KEYGEN_LICENSE_KEY"}
  7. dataset, err := license.Verify()
  8. switch {
  9. case err == keygen.ErrLicenseKeyNotGenuine:
  10. panic("license key is not genuine!")
  11. case err != nil:
  12. panic(err)
  13. }
  14. fmt.Println("License is genuine!")
  15. fmt.Printf("Decoded dataset: %s\n", dataset)
  16. }

Verify Webhooks

When listening for webhook events from Keygen, you can verify requests came from
Keygen’s servers by using keygen.VerifyWebhook. This protects your webhook
endpoint from event forgery and replay attacks.

Requires that keygen.PublicKey is set.

  1. package main
  2. import (
  3. "log"
  4. "net/http"
  5. "github.com/keygen-sh/keygen-go/v3"
  6. )
  7. func main() {
  8. keygen.PublicKey = "YOUR_KEYGEN_PUBLIC_KEY"
  9. http.HandleFunc("/webhooks", func(w http.ResponseWriter, r *http.Request) {
  10. if err := keygen.VerifyWebhook(r); err != nil {
  11. w.WriteHeader(http.StatusBadRequest)
  12. return
  13. }
  14. w.WriteHeader(http.StatusNoContent)
  15. })
  16. log.Fatal(http.ListenAndServe(":8081", nil))
  17. }

Error Handling

Our SDK tries to return meaningful errors which can be handled in your integration. Below
are a handful of error recipes that can be used for the more common errors.

Invalid License Key

When authenticating with a license key, you may receive a LicenseKeyError when the license
key does not exist. You can handle this accordingly.

  1. package main
  2. import (
  3. "context"
  4. "github.com/keygen-sh/keygen-go/v3"
  5. )
  6. func getLicense(ctx context.Context) (*keygen.License, error) {
  7. keygen.LicenseKey = promptForLicenseKey()
  8. license, err := keygen.Validate(ctx)
  9. if err != nil {
  10. if _, ok := err.(*keygen.LicenseKeyError); ok {
  11. fmt.Println("License key does not exist!")
  12. return getLicense(ctx)
  13. }
  14. return nil, err
  15. }
  16. return license, nil
  17. }
  18. func main() {
  19. keygen.Account = "..."
  20. keygen.Product = "..."
  21. ctx := context.Background()
  22. license, err := getLicense(ctx)
  23. if err != nil {
  24. panic(err)
  25. }
  26. fmt.Printf("License: %v\n", license)
  27. }

Invalid License Token

When authenticating with a license token, you may receive a LicenseTokenError when the license
token does not exist or has expired. You can handle this accordingly.

  1. package main
  2. import "github.com/keygen-sh/keygen-go/v3"
  3. func getLicense(ctx context.Context) (*keygen.License, error) {
  4. keygen.Token = promptForLicenseToken()
  5. license, err := keygen.Validate(ctx)
  6. if err != nil {
  7. if _, ok := err.(*keygen.LicenseTokenError); ok {
  8. fmt.Println("License token does not exist!")
  9. return getLicense(ctx)
  10. }
  11. return nil, err
  12. }
  13. return license, nil
  14. }
  15. func main() {
  16. keygen.Account = "..."
  17. keygen.Product = "..."
  18. ctx := context.Background()
  19. license, err := getLicense(ctx)
  20. if err != nil {
  21. panic(err)
  22. }
  23. fmt.Printf("License: %v\n", license)
  24. }

Rate Limiting

When your integration makes too many requests too quickly, the IP address may be rate limited.
You can handle this via the RateLimitError error. For example, you could use this error to
determine how long to wait before retrying a request.

  1. package main
  2. import "github.com/keygen-sh/keygen-go/v3"
  3. func validate(ctx context.Context) (*keygen.License, error) {
  4. license, err := keygen.Validate(ctx)
  5. if err != nil {
  6. if e, ok := err.(*keygen.RateLimitError); ok {
  7. // Sleep until our rate limit window is passed
  8. time.Sleep(time.Duration(e.RetryAfter) * time.Second)
  9. // Retry validate
  10. return validate(ctx)
  11. }
  12. return nil, err
  13. }
  14. return license, nil
  15. }
  16. func main() {
  17. keygen.Account = "YOUR_KEYGEN_ACCOUNT_ID"
  18. keygen.Product = "YOUR_KEYGEN_PRODUCT_ID"
  19. keygen.LicenseKey = "A_KEYGEN_LICENSE_KEY"
  20. ctx := context.Background()
  21. license, err := validate(ctx)
  22. if err != nil {
  23. panic(err)
  24. }
  25. fmt.Printf("License: %v\n", license)
  26. }

You may want to add a limit to the number of retry attempts.

Automatic retries

When your integration has less-than-stellar network connectivity, or you simply want to
ensure that failed requests are retried, you can utilize a package such as retryablehttp
to implement automatic retries.

  1. package main
  2. import (
  3. "context"
  4. "github.com/hashicorp/go-retryablehttp"
  5. "github.com/keygen-sh/keygen-go/v3"
  6. )
  7. func main() {
  8. c := retryablehttp.NewClient()
  9. // Configure with a jitter backoff and max attempts
  10. c.Backoff = retryablehttp.LinearJitterBackoff
  11. c.RetryMax = 5
  12. keygen.HTTPClient = c.StandardClient()
  13. keygen.Account = "YOUR_KEYGEN_ACCOUNT_ID"
  14. keygen.Product = "YOUR_KEYGEN_PRODUCT_ID"
  15. keygen.LicenseKey = "A_KEYGEN_LICENSE_KEY"
  16. ctx := context.Background()
  17. // Use SDK as you would normally
  18. keygen.Validate(ctx)
  19. }

Testing

When implementing a testing strategy for your licensing integration, we recommend that you
fully mock our APIs. This is especially important for CI/CD environments, to prevent
unneeded load on our servers. Mocking our APIs will also allow you to more easily
stay within your account’s daily request limits.

To do so in Go, you can utilize gock or httptest.

  1. package main
  2. import (
  3. "context"
  4. "testing"
  5. "github.com/keygen-sh/keygen-go/v3"
  6. "gopkg.in/h2non/gock.v1"
  7. )
  8. func init() {
  9. keygen.PublicKey = "e8601e48b69383ba520245fd07971e983d06d22c4257cfd82304601479cee788"
  10. keygen.Account = "1fddcec8-8dd3-4d8d-9b16-215cac0f9b52"
  11. keygen.Product = "1f086ec9-a943-46ea-9da4-e62c2180c2f4"
  12. keygen.LicenseKey = "C1B6DE-39A6E3-DE1529-8559A0-4AF593-V3"
  13. }
  14. func TestExample(t *testing.T) {
  15. ctx := context.Background()
  16. defer gock.Off()
  17. // Intercept Keygen's HTTP client
  18. gock.InterceptClient(keygen.HTTPClient)
  19. defer gock.RestoreClient(keygen.HTTPClient)
  20. // Mock endpoints
  21. gock.New("https://api.keygen.sh").
  22. Get(`/v1/accounts/([^\/]+)/me`).
  23. Reply(200).
  24. SetHeader("Keygen-Signature", `keyid="1fddcec8-8dd3-4d8d-9b16-215cac0f9b52", algorithm="ed25519", signature="IiyYX1ah2HFzbcCx+3sv+KJpOppFdMRuZ7NWlnwZMKAf5khj9c4TO4z6fr62BqNXlyROOTxZinX8UpXHJHVyAw==", headers="(request-target) host date digest"`).
  25. SetHeader("Digest", "sha-256=d4uZ26hjiUNqopuSkYcYwg2aBuNtr4D1/9iDhlvf0H8=").
  26. SetHeader("Date", "Wed, 15 Jun 2022 18:52:14 GMT").
  27. BodyString(`{"data":{"id":"218810ed-2ac8-4c26-a725-a6da67500561","type":"licenses","attributes":{"name":"Demo License","key":"C1B6DE-39A6E3-DE1529-8559A0-4AF593-V3","expiry":null,"status":"ACTIVE","uses":0,"suspended":false,"scheme":null,"encrypted":false,"strict":false,"floating":false,"concurrent":false,"protected":true,"maxMachines":1,"maxProcesses":null,"maxCores":null,"maxUses":null,"requireHeartbeat":false,"requireCheckIn":false,"lastValidated":"2022-06-15T18:52:12.068Z","lastCheckIn":null,"nextCheckIn":null,"metadata":{"email":"user@example.com"},"created":"2020-09-14T21:18:08.990Z","updated":"2022-06-15T18:52:12.073Z"},"relationships":{"account":{"links":{"related":"/v1/accounts/1fddcec8-8dd3-4d8d-9b16-215cac0f9b52"},"data":{"type":"accounts","id":"1fddcec8-8dd3-4d8d-9b16-215cac0f9b52"}},"product":{"links":{"related":"/v1/accounts/1fddcec8-8dd3-4d8d-9b16-215cac0f9b52/licenses/218810ed-2ac8-4c26-a725-a6da67500561/product"},"data":{"type":"products","id":"ef6e0993-70d6-42c4-a0e8-846cb2e3fa54"}},"policy":{"links":{"related":"/v1/accounts/1fddcec8-8dd3-4d8d-9b16-215cac0f9b52/licenses/218810ed-2ac8-4c26-a725-a6da67500561/policy"},"data":{"type":"policies","id":"629307fb-331d-430b-978a-44d45d9de133"}},"group":{"links":{"related":"/v1/accounts/1fddcec8-8dd3-4d8d-9b16-215cac0f9b52/licenses/218810ed-2ac8-4c26-a725-a6da67500561/group"},"data":null},"user":{"links":{"related":"/v1/accounts/1fddcec8-8dd3-4d8d-9b16-215cac0f9b52/licenses/218810ed-2ac8-4c26-a725-a6da67500561/user"},"data":null},"machines":{"links":{"related":"/v1/accounts/1fddcec8-8dd3-4d8d-9b16-215cac0f9b52/licenses/218810ed-2ac8-4c26-a725-a6da67500561/machines"},"meta":{"cores":0,"count":1}},"tokens":{"links":{"related":"/v1/accounts/1fddcec8-8dd3-4d8d-9b16-215cac0f9b52/licenses/218810ed-2ac8-4c26-a725-a6da67500561/tokens"}},"entitlements":{"links":{"related":"/v1/accounts/1fddcec8-8dd3-4d8d-9b16-215cac0f9b52/licenses/218810ed-2ac8-4c26-a725-a6da67500561/entitlements"}}},"links":{"self":"/v1/accounts/1fddcec8-8dd3-4d8d-9b16-215cac0f9b52/licenses/218810ed-2ac8-4c26-a725-a6da67500561"}}}`)
  28. gock.New("https://api.keygen.sh").
  29. Post(`/v1/accounts/([^\/]+)/licenses/([^\/]+)/actions/validate`).
  30. Reply(200).
  31. SetHeader("Keygen-Signature", `keyid="1fddcec8-8dd3-4d8d-9b16-215cac0f9b52", algorithm="ed25519", signature="18+5Q4749BKuUz9/f35UrdP5g3Pyt32pPN3J8e5BqSlRbqiXnz0HwtqbP5sbvGkq1yixelwgV6bcJ0WUtpDSBw==", headers="(request-target) host date digest"`).
  32. SetHeader("Digest", "sha-256=c1y1CVVLG0mvt0MP1SJy/bOiNjCytxMOuHUhlCXXVVk=").
  33. SetHeader("Date", "Thu, 09 Feb 2023 21:20:13 GMT").
  34. BodyString(`{"data":{"id":"218810ed-2ac8-4c26-a725-a6da67500561","type":"licenses","attributes":{"name":"Demo License","key":"C1B6DE-39A6E3-DE1529-8559A0-4AF593-V3","expiry":null,"status":"ACTIVE","uses":0,"suspended":false,"scheme":null,"encrypted":false,"strict":false,"floating":false,"protected":true,"maxMachines":1,"maxProcesses":null,"maxCores":null,"maxUses":null,"requireHeartbeat":false,"requireCheckIn":false,"lastValidated":"2023-02-09T21:20:13.679Z","lastCheckIn":null,"nextCheckIn":null,"lastCheckOut":null,"metadata":{"email":"user@example.com"},"created":"2020-09-14T21:18:08.990Z","updated":"2023-02-09T21:20:13.691Z"},"relationships":{"account":{"links":{"related":"/v1/accounts/1fddcec8-8dd3-4d8d-9b16-215cac0f9b52"},"data":{"type":"accounts","id":"1fddcec8-8dd3-4d8d-9b16-215cac0f9b52"}},"product":{"links":{"related":"/v1/accounts/1fddcec8-8dd3-4d8d-9b16-215cac0f9b52/licenses/218810ed-2ac8-4c26-a725-a6da67500561/product"},"data":{"type":"products","id":"ef6e0993-70d6-42c4-a0e8-846cb2e3fa54"}},"policy":{"links":{"related":"/v1/accounts/1fddcec8-8dd3-4d8d-9b16-215cac0f9b52/licenses/218810ed-2ac8-4c26-a725-a6da67500561/policy"},"data":{"type":"policies","id":"629307fb-331d-430b-978a-44d45d9de133"}},"group":{"links":{"related":"/v1/accounts/1fddcec8-8dd3-4d8d-9b16-215cac0f9b52/licenses/218810ed-2ac8-4c26-a725-a6da67500561/group"},"data":null},"user":{"links":{"related":"/v1/accounts/1fddcec8-8dd3-4d8d-9b16-215cac0f9b52/licenses/218810ed-2ac8-4c26-a725-a6da67500561/user"},"data":null},"machines":{"links":{"related":"/v1/accounts/1fddcec8-8dd3-4d8d-9b16-215cac0f9b52/licenses/218810ed-2ac8-4c26-a725-a6da67500561/machines"},"meta":{"cores":0,"count":1}},"tokens":{"links":{"related":"/v1/accounts/1fddcec8-8dd3-4d8d-9b16-215cac0f9b52/licenses/218810ed-2ac8-4c26-a725-a6da67500561/tokens"}},"entitlements":{"links":{"related":"/v1/accounts/1fddcec8-8dd3-4d8d-9b16-215cac0f9b52/licenses/218810ed-2ac8-4c26-a725-a6da67500561/entitlements"}}},"links":{"self":"/v1/accounts/1fddcec8-8dd3-4d8d-9b16-215cac0f9b52/licenses/218810ed-2ac8-4c26-a725-a6da67500561"}},"meta":{"ts":"2023-02-09T21:20:13.696Z","valid":true,"detail":"is valid","code":"VALID"}}`)
  35. // Allow old response signatures
  36. keygen.MaxClockDrift = -1
  37. // Use SDK as you would normally
  38. _, err := keygen.Validate(ctx)
  39. if err != nil {
  40. t.Fatalf("Should not fail mock validation: err=%v", err)
  41. }
  42. }