Documentation
¶
Overview ¶
Package licensr is the official Go SDK for the Licensr license API: validation, seat/domain activation, offline EdDSA token verification, hardware IDs and token caching.
client := licensr.NewClient(licensr.Config{APIKey: "pk_live_...", PluginSlug: "my-plugin"})
result, err := client.Validate(ctx, licensr.ValidateRequest{LicenseKey: key})
var apiErr *licensr.APIError
switch {
case errors.As(err, &apiErr): // The API said no — branch on apiErr.Code.
case err != nil: // *licensr.NetworkError: no response at all.
case result.Valid: // Unlock the product.
}
Index ¶
- Constants
- Variables
- func ClearJWKSCache()
- func ComputeHardwareID(salt string) (string, error)
- func FetchJWKS(ctx context.Context, jwksURL string, opts *VerifyOptions) (json.RawMessage, error)
- type APIError
- type ActivateRequest
- type ActivateResponse
- type ActivationInfo
- type ActivationListItem
- type ActivationType
- type ActivationsRequest
- type ActivationsResponse
- type CachedOfflineToken
- type CheckoutRequest
- type CheckoutResponse
- type Client
- func (c *Client) Activate(ctx context.Context, req ActivateRequest) (*ActivateResponse, error)
- func (c *Client) Activations(ctx context.Context, req ActivationsRequest) (*ActivationsResponse, error)
- func (c *Client) Checkout(ctx context.Context, req CheckoutRequest) (*CheckoutResponse, error)
- func (c *Client) Deactivate(ctx context.Context, req DeactivateRequest) (*DeactivateResponse, error)
- func (c *Client) LastValidated() *ValidateResponse
- func (c *Client) Token(ctx context.Context, req ValidateRequest) (*TokenResponse, error)
- func (c *Client) Validate(ctx context.Context, req ValidateRequest) (*ValidateResponse, error)
- type Config
- type DeactivateRequest
- type DeactivateResponse
- type Entitlement
- type FileTokenStore
- type Hooks
- type LicenseDetails
- type LicenseStatus
- type MemoryTokenStore
- type NetworkError
- type OfflineTokenClaims
- type RetryOptions
- type TokenDetails
- type TokenResponse
- type TokenStore
- type TokenVerificationError
- type ValidateRequest
- type ValidateResponse
- type VerifyOptions
Constants ¶
const ( CodeMissingPluginAPIKey = "missing_plugin_api_key" CodeInvalidPluginAPIKey = "invalid_plugin_api_key" CodeOriginNotAllowed = "origin_not_allowed" CodeOriginNotActivated = "origin_not_activated" CodePluginMismatch = "plugin_mismatch" CodeLicenseInactive = "license_inactive" CodeLicenseNotValid = "license_not_valid" CodeLicenseNotFound = "license_not_found" CodeActivationNotFound = "activation_not_found" CodeActivationModeMismatch = "activation_mode_mismatch" CodeActivationCapExceeded = "activation_cap_exceeded" CodeJWKSPathDeprecated = "jwks_path_deprecated" CodeRateLimitExceeded = "rate_limit_exceeded" CodeInsufficientScope = "insufficient_scope" // Client-side codes for error bodies with no detail.error. CodeValidationError = "validation_error" CodeUnknownError = "unknown_error" )
Every documented detail.error code from the license API — see contract/conformance.yaml. Branch on APIError.Code, not on its message.
const DeviceIDHeader = "X-Licensr-Device-Id"
DeviceIDHeader carries Config.DeviceID. SDKs already compute a stable per-machine HWID for activation — send it here for free so the server buckets rate limits per installation rather than per API key (see backend/services/rate_limit_key.py).
Variables ¶
var DefaultRetry = RetryOptions{MaxRetries: 2, BaseDelay: 300 * time.Millisecond, MaxDelay: 5 * time.Second}
DefaultRetry is used when Config.Retry is nil.
ErrHardwareIDUnavailable is wrapped by ComputeHardwareID's error when the platform identifier couldn't be read.
Functions ¶
func ClearJWKSCache ¶
func ClearJWKSCache()
ClearJWKSCache drops cached JWKS lookups — call after rotating a plugin's signing key in tests, or to force a fresh fetch.
func ComputeHardwareID ¶
ComputeHardwareID reads a platform-specific hardware identifier — IOPlatformUUID on macOS, MachineGuid + system volume serial on Windows, /etc/machine-id on Linux — and returns sha256(salt + rawID) as a lowercase hex string, so the actual platform identifier never leaves the process and two different salts (e.g. two different plugins) never produce a correlatable identifier for the same machine. The result is identical to the C++ SDK's compute_hardware_id on the same machine.
salt should be stable for your plugin (e.g. the plugin slug, or a constant baked into your build) — changing it changes every user's computed identifier, which for ActivateRequest.Identifier means their existing seat activation would appear to be a different machine.
Returns an error wrapping ErrHardwareIDUnavailable if the platform identifier couldn't be read (rare: restricted permissions, an unusual container/sandbox, or an unsupported platform). Fall back to a self-generated, persisted UUID in that case — this never talks to the network, so there's nothing to retry.
func FetchJWKS ¶
func FetchJWKS(ctx context.Context, jwksURL string, opts *VerifyOptions) (json.RawMessage, error)
FetchJWKS downloads the plugin's public key set from jwksURL (the TokenResponse's Details.JWKSURL) and caches it for VerifyOfflineToken. Save the result with the token (CachedOfflineToken.JWKS) and pass it back as VerifyOptions.JWKS to verify on a later launch with no network.
Types ¶
type APIError ¶
type APIError struct {
Status int
Code string
Message string
// Set on 429 responses when the server sends Retry-After; zero otherwise.
RetryAfter time.Duration
}
APIError means the API responded with a non-2xx status and a parsed detail.error body.
Code is one of the documented error codes (the Code* constants, e.g. CodeLicenseInactive, CodeOriginNotAllowed, CodeRateLimitExceeded). Branch on Code, not on Message — the message is for humans and may change without notice.
type ActivateRequest ¶
type ActivateRequest struct {
LicenseKey string
ActivationType ActivationType
// Stable per-install identifier: HWID for ActivationSeat (see ComputeHardwareID), hostname for ActivationDomain.
Identifier string
// Cosmetic, shown in admin/customer portals (e.g. "My MacBook Pro"). Empty sends null.
Label string
}
type ActivateResponse ¶
type ActivateResponse struct {
// Save this: you need it to deactivate later.
ActivationID string `json:"activation_id"`
FeatureFlags map[string]any `json:"feature_flags"`
Activation ActivationInfo `json:"activation"`
}
type ActivationInfo ¶
type ActivationInfo struct {
Type ActivationType `json:"type"`
Identifier string `json:"identifier"`
}
ActivationInfo is what was activated (an echo of your request). Informational only.
type ActivationListItem ¶
type ActivationType ¶
type ActivationType string
ActivationType is how a license is bound: one seat per machine, or one domain per website.
const ( ActivationSeat ActivationType = "seat" ActivationDomain ActivationType = "domain" )
type ActivationsRequest ¶
type ActivationsRequest struct {
LicenseKey string
}
type ActivationsResponse ¶
type ActivationsResponse struct {
LicenseID string `json:"license_id"`
Activations []ActivationListItem `json:"activations"`
}
type CachedOfflineToken ¶
type CachedOfflineToken struct {
Token string `json:"token"`
// The key set from FetchJWKS, so the token verifies with no network.
JWKS json.RawMessage `json:"jwks"`
// When this entry was cached.
CachedAt time.Time `json:"cached_at"`
}
CachedOfflineToken is the last-known-good offline token for one license. It holds no claims on purpose: anything on disk can be edited, so read the claims by verifying Token again (VerifyOptions.JWKS = JWKS).
type CheckoutRequest ¶
type CheckoutRequest struct {
PlanID string
CustomerEmail string
// Empty sends null (the plan's default redirect).
SuccessURL string
// Empty sends null (the plan's default redirect).
CancelURL string
// Test keys (pk_test_...) only. Puts the buyer on a new Stripe test clock
// you can fast-forward to try renewals. False leaves it out of the request.
TestClock bool
}
type CheckoutResponse ¶
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client is a thin client for the Licensr public license API (/v1/license/*, /v1/billing/checkout). It is safe for concurrent use.
client := licensr.NewClient(licensr.Config{APIKey: "pk_live_...", PluginSlug: "my-plugin"})
result, err := client.Validate(ctx, licensr.ValidateRequest{LicenseKey: key})
if err == nil && result.Valid { ... }
func (*Client) Activate ¶
func (c *Client) Activate(ctx context.Context, req ActivateRequest) (*ActivateResponse, error)
Activate activates a seat (per-machine HWID) or domain for a license.
func (*Client) Activations ¶
func (c *Client) Activations(ctx context.Context, req ActivationsRequest) (*ActivationsResponse, error)
Activations lists every current activation for a license (seats/domains in use).
func (*Client) Checkout ¶
func (c *Client) Checkout(ctx context.Context, req CheckoutRequest) (*CheckoutResponse, error)
Checkout starts a hosted checkout session for a plan; redirect the user to the returned URL.
func (*Client) Deactivate ¶
func (c *Client) Deactivate(ctx context.Context, req DeactivateRequest) (*DeactivateResponse, error)
Deactivate releases a previously created activation, freeing its seat/domain slot.
func (*Client) LastValidated ¶
func (c *Client) LastValidated() *ValidateResponse
LastValidated returns the most recent successful Validate result from this client, or nil if Validate hasn't succeeded yet. It never blocks or touches the network, so it is safe to call from any goroutine on a hot path. The result is shared: treat it as read-only.
func (*Client) Token ¶
func (c *Client) Token(ctx context.Context, req ValidateRequest) (*TokenResponse, error)
Token runs the same check as Validate, and on success mints a short-lived EdDSA-signed offline token. Verify it with VerifyOfflineToken — no network call needed after the first successful fetch of the plugin's JWKS.
func (*Client) Validate ¶
func (c *Client) Validate(ctx context.Context, req ValidateRequest) (*ValidateResponse, error)
Validate checks whether a license is valid for this plugin, without minting an offline token.
type Config ¶
type Config struct {
// Plugin API key (pk_live_... / pk_test_...).
// Admin → Catalog → your product → API keys. Issue a Client key to
// embed in a shipped app (it cannot open checkout).
APIKey string
// Product slug from Admin → Catalog (the same value on the product row).
// Sent as plugin_slug on every license call.
PluginSlug string
// Defaults to https://api.licensr.app. Override for self-hosted/staging.
BaseURL string
// Explicitly set the Origin header, for the domain-origin guard (see
// docs/sdks). Desktop apps and services should instead set the
// product's client type to "native" in the admin UI and leave this empty.
Origin string
// Stable per-install identifier, e.g. the same HWID used for Activate
// (see ComputeHardwareID). Buckets rate limits per installation instead
// of per key.
DeviceID string
// Defaults to a new http.Client. Override for proxies, custom TLS or testing.
HTTPClient *http.Client
// Per-attempt timeout. Zero means no timeout beyond the caller's context.
Timeout time.Duration
// Nil means DefaultRetry. A zero BaseDelay or MaxDelay keeps its default;
// set MaxRetries to 0 to disable retries.
Retry *RetryOptions
// Optional lifecycle callbacks.
Hooks Hooks
}
Config configures a Client. Only APIKey and PluginSlug are required.
type DeactivateRequest ¶
type DeactivateResponse ¶
type DeactivateResponse struct {
Deactivated bool `json:"deactivated"`
}
type Entitlement ¶
type Entitlement string
Entitlement is the level of access a license grants.
const ( EntitlementFull Entitlement = "full" EntitlementLimited Entitlement = "limited" EntitlementNone Entitlement = "none" )
type FileTokenStore ¶
type FileTokenStore struct {
// contains filtered or unexported fields
}
FileTokenStore is a TokenStore with one JSON file per license key, under a per-plugin directory in the platform's app-data location (the same one the C++ SDK uses; the .json extension keeps it apart from C++'s .token files):
macOS: ~/Library/Application Support/Licensr/<pluginSlug>/ Windows: %APPDATA%\Licensr\<pluginSlug>\ Linux: $XDG_DATA_HOME/licensr/<pluginSlug>/ (falls back to ~/.local/share/licensr/<pluginSlug>/)
func NewFileTokenStore ¶
func NewFileTokenStore(pluginSlug, baseDir string) *FileTokenStore
NewFileTokenStore stores tokens under baseDir/pluginSlug, or under the platform default above when baseDir is empty. The directory is created on the first Set.
func (*FileTokenStore) Clear ¶
func (s *FileTokenStore) Clear(licenseKey string) error
func (*FileTokenStore) Dir ¶
func (s *FileTokenStore) Dir() string
Dir is the resolved directory tokens are written under.
func (*FileTokenStore) Get ¶
func (s *FileTokenStore) Get(licenseKey string) (*CachedOfflineToken, error)
func (*FileTokenStore) Set ¶
func (s *FileTokenStore) Set(licenseKey string, entry CachedOfflineToken) error
type Hooks ¶
type Hooks struct {
OnValidated func(*ValidateResponse)
OnActivated func(*ActivateResponse)
OnDeactivated func(*DeactivateResponse)
OnTokenIssued func(*TokenResponse)
// Fired on every retried request, before the retry delay.
OnRetry func(method string, attempt int, delay time.Duration)
// Fired whenever a client method fails, right before the error is returned.
OnError func(method string, err error)
}
Hooks are optional lifecycle callbacks. Each runs synchronously on the goroutine that made the call; leave unset if you don't need it.
type LicenseDetails ¶
type LicenseDetails struct {
PlanID *string `json:"plan_id"`
ActivationMode *ActivationType `json:"activation_mode"`
// Devices or users allowed. nil means unlimited.
MaxSeats *int `json:"max_seats"`
// Websites allowed. nil means unlimited.
MaxDomains *int `json:"max_domains"`
SeatsInUse *int `json:"seats_in_use"`
// When the license expires. nil means it never expires.
ExpiresAt *string `json:"expires_at"`
}
LicenseDetails is information about the license. Nothing here decides access; safe to ignore.
type LicenseStatus ¶
type LicenseStatus string
LicenseStatus says why a license is or isn't valid.
const ( StatusActive LicenseStatus = "active" StatusInactive LicenseStatus = "inactive" StatusExpired LicenseStatus = "expired" )
type MemoryTokenStore ¶
type MemoryTokenStore struct {
// contains filtered or unexported fields
}
MemoryTokenStore is a TokenStore that does not persist across process restarts. Safe for concurrent use.
func NewMemoryTokenStore ¶
func NewMemoryTokenStore() *MemoryTokenStore
func (*MemoryTokenStore) Clear ¶
func (s *MemoryTokenStore) Clear(licenseKey string) error
func (*MemoryTokenStore) Get ¶
func (s *MemoryTokenStore) Get(licenseKey string) (*CachedOfflineToken, error)
func (*MemoryTokenStore) Set ¶
func (s *MemoryTokenStore) Set(licenseKey string, entry CachedOfflineToken) error
type NetworkError ¶
NetworkError means the request never got a response — network failure, timeout, or a cancelled context. Unwrap returns the underlying cause.
func (*NetworkError) Error ¶
func (e *NetworkError) Error() string
func (*NetworkError) Unwrap ¶
func (e *NetworkError) Unwrap() error
type OfflineTokenClaims ¶
type OfflineTokenClaims struct {
Sub string `json:"sub"`
Iss string `json:"iss"`
Iat int64 `json:"iat"`
Nbf int64 `json:"nbf"`
Exp int64 `json:"exp"`
PluginSlug string `json:"plugin_slug"`
PlanID *string `json:"plan_id"`
Status LicenseStatus `json:"status"`
Valid bool `json:"valid"`
Entitlement Entitlement `json:"entitlement"`
Fallback bool `json:"fallback"`
ActivationMode *ActivationType `json:"activation_mode"`
MaxSeats *int `json:"max_seats"`
MaxDomains *int `json:"max_domains"`
LicenseExpiresAt *int64 `json:"license_expires_at"`
FeatureFlags map[string]any `json:"feature_flags"`
}
OfflineTokenClaims are the claims embedded in the offline token. Flat, as in any JWT; carries the same facts as ValidateResponse.
func VerifyOfflineToken ¶
func VerifyOfflineToken(ctx context.Context, token, jwksURL string, opts *VerifyOptions) (*OfflineTokenClaims, error)
VerifyOfflineToken verifies an offline license token (from Client.Token) with no network call beyond fetching (and caching) the plugin's public JWKS — or none at all when VerifyOptions.JWKS is set. Returns a *TokenVerificationError on a bad signature, wrong algorithm, another product's token, an expired/not-yet-valid token, or a failed JWKS download (errors.Unwrap gives the cause).
Tokens carry no revocation signal — this only proves the token was genuinely issued by Licensr and hasn't expired, not that the license is still active right now. Re-validate online (Validate/Token) before the token's exp.
type RetryOptions ¶
type RetryOptions struct {
// Extra attempts after the first. Default 2.
MaxRetries int
// Base delay for exponential backoff with full jitter. Default 300ms.
BaseDelay time.Duration
// Ceiling for the backoff delay. Default 5s.
MaxDelay time.Duration
}
RetryOptions controls retries on network errors, 429 and 5xx responses.
type TokenDetails ¶
type TokenDetails struct {
Algorithm string `json:"algorithm"`
Kid string `json:"kid"`
IssuedAt string `json:"issued_at"`
ExpiresAt string `json:"expires_at"`
JWKSURL string `json:"jwks_url"`
}
TokenDetails says how to verify the offline token and when to refresh it.
type TokenResponse ¶
type TokenResponse struct {
// EdDSA-signed JWT — verify with VerifyOfflineToken (pass Details.JWKSURL).
Token string `json:"token"`
Details TokenDetails `json:"details"`
}
type TokenStore ¶
type TokenStore interface {
// Get returns nil, nil when nothing is cached for licenseKey.
Get(licenseKey string) (*CachedOfflineToken, error)
Set(licenseKey string, entry CachedOfflineToken) error
Clear(licenseKey string) error
}
TokenStore persists the last-known-good offline token per license, so an app can boot fully offline (no network at all) before its first successful Token call in a given session. Bring your own implementation for hosts with their own storage (an OS keychain, an app's config file) — Licensr only needs something implementing this interface.
type TokenVerificationError ¶
TokenVerificationError means an offline token failed EdDSA verification, was malformed, was for another product, or has expired — or that its JWKS couldn't be downloaded. Unwrap returns the underlying cause, so errors.Is(err, context.DeadlineExceeded) tells "offline" from "forged".
func (*TokenVerificationError) Error ¶
func (e *TokenVerificationError) Error() string
func (*TokenVerificationError) Unwrap ¶
func (e *TokenVerificationError) Unwrap() error
type ValidateRequest ¶
type ValidateRequest struct {
LicenseKey string
}
type ValidateResponse ¶
type ValidateResponse struct {
// The answer you gate on: true unlocks your product, false keeps it locked.
Valid bool `json:"valid"`
// Why: pick the message to show the customer. Do not gate access on it.
Status LicenseStatus `json:"status"`
// Optional. EntitlementLimited means expired but a fallback plan keeps reduced access.
Entitlement Entitlement `json:"entitlement"`
// Optional. Custom per-plan settings you defined.
FeatureFlags map[string]any `json:"feature_flags"`
// Plan, limits and expiry. Informational only.
License LicenseDetails `json:"license"`
}
type VerifyOptions ¶
type VerifyOptions struct {
// Reject tokens minted for any other product. Every product in a store
// is signed with the store's one key, so without this a token for one
// of your products also verifies for another — set it to your PluginSlug.
PluginSlug string
// Verify against this key set (from FetchJWKS, saved alongside the
// token) instead of downloading it, so verification needs no network.
JWKS json.RawMessage
// Used to download the JWKS. Defaults to http.DefaultClient.
HTTPClient *http.Client
// Accept exp/nbf this far past/before now (clock skew tolerance). Zero means 60s.
ClockTolerance time.Duration
}
VerifyOptions tunes VerifyOfflineToken. A nil *VerifyOptions uses the defaults.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
examples
|
|
|
cli
command
Command cli is a minimal end-to-end Licensr integration for a Go CLI or service: boot from a cached offline token when there is one, otherwise validate online, activate this machine's seat, and cache a fresh token.
|
Command cli is a minimal end-to-end Licensr integration for a Go CLI or service: boot from a cached offline token when there is one, otherwise validate online, activate this machine's seat, and cache a fresh token. |