Help us shape the future of Go! Take the 2026 Go Developer Survey

licensr

package module
v0.2.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 28, 2026 License: MIT Imports: 24 Imported by: 0

README

licensr-go

Official Go SDK for the Licensr license API: validation, seat/domain activation, offline EdDSA token verification, and hosted checkout — plus a hardware ID and a token store that saves to disk, for desktop tools that must unlock offline. Go 1.22+, no third-party dependencies (offline verification uses the standard library's crypto/ed25519; only the conformance test uses gopkg.in/yaml.v3).

See the License calls guide for the underlying API's concepts (activation modes, entitlement/fallback, rate limits, the embedded-key doctrine) and the Go SDK guide for the full walkthrough — this README covers the Go-specific surface.

Install

go get github.com/tekunodev/licensr-go@latest

Quickstart

import licensr "github.com/tekunodev/licensr-go"

client := licensr.NewClient(licensr.Config{
	APIKey:     "pk_live_...", // Client key from Admin → Catalog → product → API keys
	PluginSlug: "my-plugin",   // product slug on that same row
})

result, err := client.Validate(ctx, licensr.ValidateRequest{LicenseKey: userEnteredKey})
switch {
case err != nil:
	// Licensr could not answer (offline, rate limited, server error): keep the previous state.
case result.Valid:
	unlockFullFeatures()
case result.Entitlement == licensr.EntitlementLimited:
	unlockDegradedMode() // perpetual-fallback: expired, but the plan grants a limited tier
}

The result keeps what you act on at the top level: Valid, Status (why, for your message), Entitlement and FeatureFlags. Plan and limit details are grouped under result.License. They are for display only; never use them to decide whether to unlock.

Desktop tools and services never send an Origin header: set the product's client type to Native in the admin UI.

API

Every method takes a context.Context, blocks until it has an answer, and returns an error on failure — see Errors. A *Client is safe for concurrent use.

Method Wraps Notes
Validate(ctx, ValidateRequest{...}) POST /v1/license/validate Always read Valid/Entitlement; never branch on HTTP status — an unknown key returns 200 {valid: false}, not 404.
Token(ctx, ValidateRequest{...}) POST /v1/license/token Same check as Validate, plus a short-lived signed offline token.
Activate(ctx, ActivateRequest{...}) POST /v1/license/activate ActivationSeat (per-machine) or ActivationDomain, fixed by the product's mode. An empty Label sends null.
Deactivate(ctx, DeactivateRequest{...}) POST /v1/license/deactivate Frees a seat/domain slot.
Activations(ctx, ActivationsRequest{...}) GET /v1/license/activations Lists every current activation.
Checkout(ctx, CheckoutRequest{...}) POST /v1/billing/checkout Returns CheckoutURL. Requires a key with the checkout scope.
LastValidated() (no request) The last successful Validate result, or nil. Never blocks — safe on a hot path.

With a test key (pk_test_…), set TestClock: true on CheckoutRequest to put the buyer on a Stripe test clock you can fast-forward to try renewals; the response returns its id as TestClockID.

Config also takes BaseURL, DeviceID (buckets rate limits per installation — pass the hardware ID), Origin, Timeout (per attempt), Retry (network errors / 429 / 5xx, exponential backoff + full jitter, honors Retry-After; default 2 retries, and delays you leave at zero keep their 300ms/5s defaults), HTTPClient, and Hooks (OnValidated, OnActivated, OnDeactivated, OnTokenIssued, OnRetry, OnError).

Offline verification and token storage
token, err := client.Token(ctx, licensr.ValidateRequest{LicenseKey: licenseKey})
jwks, err := licensr.FetchJWKS(ctx, token.Details.JWKSURL, nil)
opts := &licensr.VerifyOptions{PluginSlug: "my-plugin", JWKS: jwks}
claims, err := licensr.VerifyOfflineToken(ctx, token.Token, "", opts)

store := licensr.NewFileTokenStore("my-plugin", "") // "" = the platform's app-data folder
err = store.Set(licenseKey, licensr.CachedOfflineToken{Token: token.Token, JWKS: jwks, CachedAt: time.Now()})
// Next launch, no network: VerifyOfflineToken(ctx, cached.Token, "", &licensr.VerifyOptions{PluginSlug: "my-plugin", JWKS: cached.JWKS})
  • Always set VerifyOptions.PluginSlug. Every product in a store is signed with the same key, so without it one product's token also verifies for another.
  • Without VerifyOptions.JWKS, the key set is downloaded from the URL (10s timeout), cached for the process lifetime, and downloaded again on an unknown kid; ClearJWKSCache forces a fresh download.
  • Offline tokens carry no revocation signal — re-validate online before claims.Exp.
  • CachedOfflineToken deliberately holds no claims (a file can be edited); re-verify to read them. NewFileTokenStore uses the same folders as the C++ SDK's FileTokenStore (with a .json extension, so the two never collide); NewMemoryTokenStore doesn't survive a restart; implement TokenStore for your own storage.
Hardware IDs

ComputeHardwareID(salt) returns sha256(salt + machine ID) as lowercase hex — IOPlatformUUID on macOS, MachineGuid + system volume serial on Windows, /etc/machine-id on Linux. It matches the C++ SDK's compute_hardware_id byte for byte. On failure (a locked-down sandbox, an unsupported OS) it returns an error wrapping ErrHardwareIDUnavailable; fall back to a persisted random UUID.

Errors
Error When
*APIError Non-2xx response. Status, Code (the Code* constants), Message, RetryAfter. Branch on Code.
*NetworkError No response at all (network failure, timeout, cancelled context, retries exhausted). Unwraps to the cause.
*TokenVerificationError The token was rejected (signature, algorithm, product, expiry), or the key set couldn't be downloaded. Unwraps to the cause.

examples/cli puts it all together: unlock from a saved token when there is one, otherwise validate, activate this computer, and save a fresh token.

Development

go test -race ./...
go vet ./...
gofmt -l .
go run honnef.co/go/tools/cmd/staticcheck@2025.1.1 ./...
Conformance suite

conformance_test.go runs every case in contract/conformance.yaml against a real, running backend — the same contract backend/tests/test_conformance_contract.py and the other SDKs' runners enforce. It's skipped by default (no live backend in a normal go test run); to run it locally:

# 1. Start Postgres + migrate (from repo root)
docker compose up -d postgres
cd backend && APP_ENV=dev poetry run alembic upgrade head

# 2. Start the backend (BILLING_PROVIDER=mock avoids needing real Stripe creds)
APP_ENV=dev BILLING_PROVIDER=mock poetry run uvicorn main:app --port 8080

# 3. Seed fixtures for every case (in another shell)
poetry run python scripts/seed_conformance_fixtures.py --out /tmp/conformance_fixtures.json

# 4. Run the conformance suite (from sdks/go)
cd ../sdks/go
CONFORMANCE_BASE_URL=http://localhost:8080 \
CONFORMANCE_FIXTURES_PATH=/tmp/conformance_fixtures.json \
go test -run 'TestConformance$' -v ./...

The rate_limit_exceeded case additionally requires CONFORMANCE_RUN_SLOW=1 (it fires 61 requests to trip the bucket) — a plain local run skips just that one case. See .github/workflows/test-sdk-go.yaml for how CI wires up the same four steps as one job.

Releasing

There is no version file: tag go/vX.Y.Z on main and push the tag (mirrors to github.com/tekunodev/licensr-go as vX.Y.Z, which is what go get resolves). Never re-tag a published Go version — the Go checksum database pins it forever. Step-by-step: docs/SDK_PROGRAM.md §2 or sdks/RELEASING.md.

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.
}

See https://docs.licensr.app/sdks/go.

Index

Constants

View Source
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.

View Source
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

View Source
var DefaultRetry = RetryOptions{MaxRetries: 2, BaseDelay: 300 * time.Millisecond, MaxDelay: 5 * time.Second}

DefaultRetry is used when Config.Retry is nil.

View Source
var ErrHardwareIDUnavailable = errors.New("licensr: hardware ID unavailable")

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

func ComputeHardwareID(salt string) (string, error)

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.

func (*APIError) Error

func (e *APIError) Error() string

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 ActivationListItem struct {
	ActivationID   string         `json:"activation_id"`
	ActivationType ActivationType `json:"activation_type"`
	Identifier     string         `json:"identifier"`
	Label          *string        `json:"label"`
	FirstSeenAt    string         `json:"first_seen_at"`
	LastSeenAt     string         `json:"last_seen_at"`
}

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 CheckoutResponse struct {
	CheckoutURL string `json:"checkout_url"`
	// The Stripe test clock the buyer is on, when TestClock asked for one.
	TestClockID *string `json:"test_clock_id"`
}

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 NewClient

func NewClient(cfg Config) *Client

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 DeactivateRequest struct {
	LicenseKey   string
	ActivationID string
}

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

type NetworkError struct {
	Message string
	Cause   error
}

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

type TokenVerificationError struct {
	Message string
	Cause   error
}

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.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL