Documentation
¶
Overview ¶
Package license — banner.go provides warning banner text for grace period states. Used by CLI header output and nself-admin UI.
Package license — cache.go implements the local license cache with Ed25519 signature verification and grace-period-aware freshness checks.
Cache location: ~/.cache/nself/license.json (0600) Fields: key_hash, tier, plugins_allowed, fetched_at, expires_at, signature
Package license — checker.go: bundle-level entitlement check.
BundleEntitled calls ping_api /license/validate?bundle=<name> with the operator's license key. This is a DISTINCT call from per-plugin validation: a user who has individual plugin entitlements does NOT automatically get bundle-level access (bundles are a separate SKU on the ping_api side).
S2.T03 CR-C security requirement: bundle validation MUST use the bundle query parameter, NOT individual plugin checks. An operator with a la-carte plugin licenses must NOT gain bundle install access for free.
Package license — errors.go defines typed license errors with friendly user-facing messages. Each error carries a stable code (for programmatic matching), a human message (printed verbatim by the CLI), and an optional wrapped underlying error.
Use IsLicenseError to extract a *Error from any error chain.
Package license — grace.go implements the license grace period state machine and degradation mode enforcement.
States: valid -> grace_soft -> grace_hard -> expired -> revoked Grace periods:
- <24h offline: proceed silently (valid)
- 24h-7d offline: WARNING banner (grace_soft)
- >7d offline: read-only degraded mode (grace_hard)
- Expired license: refuse to start (expired)
- Revoked license: refuse to start (revoked)
Package license — multi-key support for product-based licensing.
Keys are collected from environment variables:
- NSELF_PLUGIN_LICENSE_KEY (legacy single key)
- NSELF_LICENSE_KEY_1 through NSELF_LICENSE_KEY_10
And from the stored key file at ~/.nself/license/key.
Package license provides CLI-level license key management operations: setting, reading, clearing, and displaying license keys.
Key storage: ~/.config/nself/license.json (chmod 0600) Legacy storage (v1): ~/.nself/license.json Env override: NSELF_PLUGIN_LICENSE_KEY
Package license — owner.go implements owner-key separation.
S133-T01: Owner license stored at ~/.nself/owner.license (separate from
regular license.json / license/key). --owner flag required for owner-key plugin installs.
S133-T02: Refuse owner key from being set when NSELF_ENV=dev. Print error
message and return without saving.
Package license — revocation.go implements the CLI-side revocation list consumer (D3-T08).
The CLI fetches `GET /license/revocation-list` from ping.nself.org hourly and stores the signed payload at `~/.config/nself/revocation-cache.json`. Every license validation pass consults the local cache to decide whether a presented JWT (jti / user_id / kid) has been revoked.
FAIL-OPEN policy (per PPI § Vendor Stack — license validation fail-mode):
- Cache up to 7 days stale: still authoritative; refresh attempts run in the background but failures don't block.
- Cache > 7 days stale + remote fetch fails: continue treating items as NOT revoked, log a prominent warning. License-server unreachable must never lock paying users out.
Wire format mirrors web/backend/services/ping_api/src/routes/license/ revocation-list.ts exactly. Canonical JSON (sorted keys at every level, arrays in declared order) is the bytes the server signs and we verify.
Package license — revoke.go implements local license revocation and plugin dormancy (S133-T03 / T04).
Revoking a license:
- Marks the local cache with revoked=true and wiped_at timestamp.
- Removes the stored key from disk.
- Plugins transition to DORMANT on next build (not uninstalled).
Restoring a license (S133-T04):
- Validates a new key before writing.
- Clears the revoked marker from the cache.
- Signals dormant plugins to re-activate on next build.
Package license — simulate.go provides offline simulation mode for testing grace period behavior without actually blocking network access.
Simulation writes an override to the license cache that makes the system believe it has been offline for N days. Guarded by LICENSE_ALLOW_SIMULATION env var (must be "true" explicitly; defaults to false in production).
Package license — ttl.go implements tier-differentiated cache TTL, pre-expiry warning thresholds, and signed+compressed cache helpers.
TTL policy (S132-T03):
Free tier → instant (no caching, revalidates on every command) Pro tier → 7 days ɳSelf+/Max → 30 days Owner → 30 days
Package license — validate.go implements the full license validation flow with ping.nself.org and grace state machine integration.
Flow: try remote validation -> update cache -> fall back to cached result with grace state evaluation on network failure.
Package license — validator.go implements the FAIL-OPEN license validation policy per memory/decisions.md (D3-T10, P97).
Policy summary:
- Cache valid + within TTL → Valid
- Cache valid + remote 200 (verified) → Valid
- Cache valid + remote unreachable + age ≤ 7d → Valid (FAIL-OPEN, silent)
- Cache valid + remote unreachable + age 7-14d → Valid (FAIL-OPEN, warning)
- Cache valid + remote unreachable + age > 14d → FailClosed
- Cache signature invalid OR tampered → FailClosed (NEVER fail-open)
- Cache absent + remote unreachable → FailClosed
- Remote 200 with revoked → Revoked (overrides cache)
- Remote auth failure (401/403) → FailClosed (NOT FAIL-OPEN)
- Remote transport / 5xx error → FAIL-OPEN if cache permits
Network-failure classification distinguishes transport errors from auth failures: only transport-class failures qualify for FAIL-OPEN. Auth failures indicate explicit policy decisions from the server and must fail-closed.
Index ¶
- Constants
- Variables
- func AddKey(key string) error
- func BannerAtExpiry() string
- func BannerAtWarning(result GraceCheckResult) string
- func BundleEntitled(ctx context.Context, key, bundleName string) (bool, error)
- func CachePath() (string, error)
- func CacheTTLExpiry(tier string, fetchedAt time.Time) time.Time
- func CacheTTLForTier(tier string) time.Duration
- func ClearKey() error
- func ClearOwnerKey() error
- func ClearRevokedMarker() error
- func ClearSimulation() error
- func CollectLicenseKey() string
- func CollectLicenseKeys() []string
- func DeleteCache() error
- func DetectTierFromKey(key string) string
- func ExportCache() ([]byte, error)
- func GetAllStoredKeys() []string
- func GetEmbeddedPubKeyHex() string
- func GetKey() (string, error)
- func GetOwnerKey() (string, error)
- func HashKey(key string) string
- func ImportCache(data []byte) error
- func IsOwnerKey(key string) bool
- func IsRecordRevoked(rec LicenseRecord) bool
- func IsRevoked() bool
- func IsZeroPubKey() bool
- func MaskKey(key string) string
- func MigrateLicenseFromV1(home string) error
- func PingURL() string
- func PreExpiryWarning(entry *CacheEntry) (message string, isPostExpiry bool)
- func RemoveKey(query string) (int, error)
- func ResetFailOpenWarning()
- func RestoreWithKey(newKey string) error
- func RevocationCachePath() (string, error)
- func RevokeLicense() error
- func SetKey(key string) error
- func SetKeyReplaceAll(key string) (int, error)
- func SetOwnerKey(key string) error
- func SetRevocationHTTPClient(c *http.Client)
- func ShowKey() (masked string, tier string, err error)
- func StartRevocationRefresher(ctx context.Context) (stop func())
- func TailStream(ctx context.Context, opts TailOptions) error
- func ValidateKeyFormat(key string) error
- func VerifyRevocationSignature(list *RevocationList) bool
- func WriteCache(entry *CacheEntry) error
- func WriteRevocationCache(c *RevocationCache) error
- type CacheEntry
- type Clock
- type Error
- type GraceCheckResult
- type GraceState
- type HTTPDoer
- type LicenseEvent
- type LicenseRecord
- type ProductPrefix
- type PublicKeyEntry
- type RevocationCache
- type RevocationEntry
- type RevocationList
- type RevokedMarker
- type TailOptions
- type ValidateResponse
- type ValidationResult
- type ValidatorOptions
- type ValidatorResult
- type ValidatorStatus
Constants ¶
const ( CodeNotFound = "LICENSE_NOT_FOUND" CodeExpired = "LICENSE_EXPIRED" CodeRevoked = "LICENSE_REVOKED" CodeInvalidSignature = "LICENSE_INVALID_SIGNATURE" CodeFailClosed = "LICENSE_FAIL_CLOSED" CodeInsufficientTier = "LICENSE_INSUFFICIENT_TIER" CodeSlotExhausted = "LICENSE_SLOT_EXHAUSTED" )
Error codes. Stable identifiers safe for scripts to match against.
const ( // TTLFree is the cache TTL for the free tier (no caching). TTLFree = 0 // TTLPro is the cache TTL for Pro/standard paid tiers. TTLPro = 7 * 24 * time.Hour // TTLPlus is the cache TTL for ɳSelf+, Max, Enterprise, and Owner tiers. TTLPlus = 30 * 24 * time.Hour )
Cache TTL durations by tier group.
const ( // PreExpiryWarnStart is when the "N day(s) to expire" warning starts. // Must be shorter than the shortest paid TTL (Pro = 7 days) so that a // freshly-fetched Pro cache does not immediately trigger the warning. // A 2-day window gives enough runway without false positives. PreExpiryWarnStart = 2 * 24 * time.Hour // PreExpiryWarnEnd is when the warning stops (cache has expired). PreExpiryWarnEnd = 0 // PostExpiryGraceWindow is the period where the post-expiry grace message // is shown before hard enforcement begins. PostExpiryGraceWindow = 24 * time.Hour )
Pre-expiry warning thresholds (S132-T01 / T02).
const DefaultCheckInterval = 6 * time.Hour
DefaultCheckInterval is the default time between license checks (6 hours).
const DefaultPingURL = "https://ping.nself.org"
DefaultPingURL is the production license validation endpoint.
const FailOpenHardTTL = 14 * 24 * time.Hour
FailOpenHardTTL is the absolute fail-open ceiling. Beyond this, fail-closed. Default 14 days.
const FailOpenSoftTTL = 7 * 24 * time.Hour
FailOpenSoftTTL is the silent-fail-open window. ≤ this value, no warning. Configurable for tests via the validator's clock; default 7 days.
const GraceHardThreshold = 7 * 24 * time.Hour
GraceHardThreshold is when hard degradation begins (7 days). Configurable via LICENSE_GRACE_DAYS env var.
const GraceSoftThreshold = 24 * time.Hour
GraceSoftThreshold is when the soft grace warning starts (24 hours).
const LicenseDirV1 = ".nself"
LicenseDirV1 is the v1 license directory name under $HOME.
const LicenseDirV2 = ".config/nself"
LicenseDirV2 is the v2 license directory name under $HOME.
const LicenseFile = "license.json"
LicenseFile is the license JSON filename used in both v1 and v2.
const RevocationFailOpenStaleness = 7 * 24 * time.Hour
RevocationFailOpenStaleness is the cutoff beyond which a stale cache + a failed remote fetch logs a prominent warning. Within the window, stale caches are silently authoritative.
const RevocationRefreshInterval = 1 * time.Hour
RevocationRefreshInterval is the recommended hourly refresh cadence.
const SimulationGuardEnv = "LICENSE_ALLOW_SIMULATION"
SimulationGuardEnv is the env var that must be set to "true" to allow simulation mode. This prevents accidental use in production.
Variables ¶
var ErrLicenseFailClosed = &Error{ Code: CodeFailClosed, Message: "Cannot verify license offline (cache too stale). Reconnect to the internet, then run: nself license refresh", UserAction: "Reconnect to the internet and run: nself license refresh", }
ErrLicenseFailClosed is returned when offline validation cannot proceed because the cached entry is older than the fail-closed TTL.
var ErrLicenseInvalidSignature = &Error{ Code: CodeInvalidSignature, Message: "License signature invalid, likely tampered. Re-fetch with: nself license refresh", UserAction: "Run: nself license refresh", }
ErrLicenseInvalidSignature is returned when the cached license signature does not validate against the public keys (likely tampered).
var ErrLicenseNotFound = &Error{ Code: CodeNotFound, Message: "No license found. Set one with: nself license set <key>", UserAction: "Run: nself license set <key>", }
ErrLicenseNotFound is returned when no license key is configured.
var ErrRevocationSignatureInvalid = fmt.Errorf("revocation list signature invalid")
ErrRevocationSignatureInvalid signals that a fetched payload failed signature verification. Callers should treat this as a refresh failure and keep the existing cache untouched.
var ErrSimulationNotAllowed = fmt.Errorf( "simulation mode is disabled — set %s=true to enable (never in production)", SimulationGuardEnv, )
ErrSimulationNotAllowed is returned when simulation is attempted without the guard env var being set.
Functions ¶
func AddKey ¶ added in v1.0.4
AddKey stores an additional license key. If no keys exist yet, it writes to the primary key file. If a key already exists, it stores as a numbered key file (key.2, key.3, etc.) in ~/.nself/license/.
func BannerAtExpiry ¶ added in v1.0.6
func BannerAtExpiry() string
BannerAtExpiry returns the hard-stop banner text when grace period is exhausted (day 14+).
func BannerAtWarning ¶ added in v1.0.6
func BannerAtWarning(result GraceCheckResult) string
BannerAtWarning returns the yellow-banner text shown when the license server has been unreachable for 7+ days (grace_soft state). Returns empty string if no banner is needed.
func BundleEntitled ¶ added in v1.1.1
BundleEntitled reports whether the operator's license key grants access to the named bundle. It calls ping_api /license/validate?bundle=<bundleName> with the key. Returns false on any error (network, auth, invalid key) so the installer can surface a precise message rather than silently proceeding.
FAIL-OPEN exception: when NSELF_LICENSE_FAIL_OPEN=1 is set (CI / air-gap environments) and the network is unreachable, falls back to the local cache to check tier coverage. Production deployments MUST NOT set this var.
func CachePath ¶ added in v1.0.6
CachePath returns the full path to the license cache file. It respects LICENSE_CACHE_PATH if set.
func CacheTTLExpiry ¶ added in v1.0.12
CacheTTLExpiry returns the time.Time when a cache entry written at fetchedAt for the given tier should expire.
func CacheTTLForTier ¶ added in v1.0.12
CacheTTLForTier returns the appropriate cache TTL for a given tier string. Tier matching is case-insensitive.
func ClearKey ¶
func ClearKey() error
ClearKey removes both the stored license key file and the validation cache file from ~/.nself/license/.
func ClearOwnerKey ¶ added in v1.0.12
func ClearOwnerKey() error
ClearOwnerKey removes the owner license file.
func ClearRevokedMarker ¶ added in v1.0.12
func ClearRevokedMarker() error
ClearRevokedMarker removes the revoked marker file (called on successful restore).
func ClearSimulation ¶ added in v1.0.6
func ClearSimulation() error
ClearSimulation restores the cache to current time (as if just validated). Also guarded by LICENSE_ALLOW_SIMULATION.
func CollectLicenseKey ¶ added in v1.1.1
func CollectLicenseKey() string
CollectLicenseKey returns the first available license key from env vars or key files. This is a convenience helper for callers that need the raw key for BundleEntitled without going through the full manager flow.
func CollectLicenseKeys ¶ added in v1.0.4
func CollectLicenseKeys() []string
CollectLicenseKeys reads all configured license keys from environment variables and the stored key file. It deduplicates and returns all non-empty keys. The order is: NSELF_PLUGIN_LICENSE_KEY, then NSELF_LICENSE_KEY_1 through _10, then the stored key file.
func DeleteCache ¶ added in v1.0.6
func DeleteCache() error
DeleteCache removes the license cache file.
func DetectTierFromKey ¶ added in v1.0.12
DetectTierFromKey returns the human-readable tier name for a key based on its prefix (e.g. "nself_owner_" → "Owner"). Returns "Unknown" when the prefix is not recognised. Public wrapper for the unexported detectTier in manager.go.
func ExportCache ¶ added in v1.0.6
ExportCache reads the current cache and returns it as JSON bytes suitable for air-gap transfer.
func GetAllStoredKeys ¶ added in v1.0.4
func GetAllStoredKeys() []string
GetAllStoredKeys returns all keys stored in key files (not env vars).
func GetEmbeddedPubKeyHex ¶ added in v1.0.13
func GetEmbeddedPubKeyHex() string
GetEmbeddedPubKeyHex returns the hex-encoded Ed25519 public key that was injected at build time via goreleaser ldflags (NSELF_LICENSE_PUBKEY_HEX). Returns an empty string in dev builds without ldflags. D3-T01: used by `nself license pubkey` and pubkey-refresh flow (D3-T10).
func GetKey ¶
GetKey returns the current license key. It checks the NSELF_PLUGIN_LICENSE_KEY environment variable first, then falls back to reading ~/.nself/license/key. Returns ("", nil) when no key is configured.
func GetOwnerKey ¶ added in v1.0.12
GetOwnerKey reads the stored owner license key. Returns ("", nil) if no owner key is configured.
func ImportCache ¶ added in v1.0.6
ImportCache reads a previously exported cache file and writes it to the local cache location. It verifies the Ed25519 signature before accepting.
func IsOwnerKey ¶ added in v1.0.12
IsOwnerKey returns true if the given key uses the nself_owner_ prefix.
func IsRecordRevoked ¶ added in v1.0.13
func IsRecordRevoked(rec LicenseRecord) bool
IsRecordRevoked reports whether the supplied license record is on the cached revocation list. Implements the FAIL-OPEN policy described at the top of this file.
Returns false (not revoked) when:
- the cache file is absent (cold start);
- the cache is older than RevocationFailOpenStaleness (7 days) AND the most recent refresh attempt failed;
- the cached list does not contain a matching identifier.
A prominent warning is printed to stderr (once per process) when the cache is fail-open-stale.
Disambiguation: this is distinct from the legacy IsRevoked() in revoke.go, which reports whether the local "license.revoked" marker file is present (the marker is an out-of-band signal written by `nself license revoke` for plugin dormancy).
func IsRevoked ¶ added in v1.0.12
func IsRevoked() bool
IsRevoked returns true if a revoked marker file is present on disk.
func IsZeroPubKey ¶ added in v1.0.12
func IsZeroPubKey() bool
IsZeroPubKey reports whether the build was made without an ldflags-injected signing key. Returns true when licensePubKeyHex is empty OR consists entirely of '0' characters (e.g., a placeholder 64-char zero string). goreleaser injects a real non-zero Ed25519 pubkey hex; dev builds leave it empty.
Exception: when LICENSE_PUBLIC_KEY_OVERRIDE is set to a valid non-zero Ed25519 public key hex, IsZeroPubKey returns false so that tests can exercise the production signature-verification code path without goreleaser ldflags.
func MaskKey ¶ added in v1.0.4
MaskKey returns a masked version of the key showing the first 10 and last 4 characters. Delegates to the existing maskKey function.
func MigrateLicenseFromV1 ¶
MigrateLicenseFromV1 copies ~/.nself/license.json to ~/.config/nself/license.json when upgrading from v1. The function is a no-op when:
- v2 already exists (NEVER overwrite existing v2 license)
- v1 does not exist (nothing to migrate)
On success the v1 file is preserved (non-destructive). File is written with mode 0600 matching the key file convention.
func PreExpiryWarning ¶ added in v1.0.12
func PreExpiryWarning(entry *CacheEntry) (message string, isPostExpiry bool)
PreExpiryWarning returns a non-empty message string when the cache age is within the PreExpiryWarnStart window but has not yet expired. Returns ("", false) when no warning is needed.
S132-T01: pre-expiry warning — "License cache expires in N day(s). Connect to revalidate." S132-T02: post-expiry grace — "Cache expired. Run nself license revalidate after connecting."
func RemoveKey ¶ added in v1.0.4
RemoveKey removes a key by prefix match or product name. Returns the number of keys removed.
func ResetFailOpenWarning ¶ added in v1.0.13
func ResetFailOpenWarning()
ResetFailOpenWarning clears the once-per-process warning latch. Test-only.
func RestoreWithKey ¶ added in v1.0.12
RestoreWithKey validates the new key, clears the revoked marker, and stores the new key so dormant plugins re-activate on next build.
The key must pass format validation before it is written. Remote validation is handled by the caller (license restore command) so that we can surface a clear error to the user before touching disk.
func RevocationCachePath ¶ added in v1.0.13
RevocationCachePath returns the on-disk cache location, honouring LICENSE_REVOCATION_CACHE_PATH for tests and unusual deployments.
func RevokeLicense ¶ added in v1.0.12
func RevokeLicense() error
RevokeLicense marks the license as revoked:
- Writes a revoked marker to disk.
- Deletes the local license cache.
- Clears all stored license keys.
Plugins go DORMANT (not uninstalled) on the next `nself build`.
func SetKey ¶
SetKey validates the key format, creates the license directory if needed, and writes the key to ~/.nself/license/key with mode 0600.
func SetKeyReplaceAll ¶ added in v1.0.4
SetKeyReplaceAll replaces all stored keys with a single new key. Returns the number of keys that were replaced.
func SetOwnerKey ¶ added in v1.0.12
SetOwnerKey saves an owner license key to ~/.nself/owner.license (0600).
Enforces S133-T02: refuses to save if NSELF_ENV=dev to prevent accidental owner-key commits via tracked env files.
func SetRevocationHTTPClient ¶ added in v1.0.13
SetRevocationHTTPClient overrides the HTTP client used for refreshes. Test-only.
func ShowKey ¶
ShowKey returns a masked version of the current key and its detected tier. The masked key shows the first 10 and last 4 characters with the middle replaced by asterisks. If no key is set, all return values are empty strings with a nil error.
func StartRevocationRefresher ¶ added in v1.0.13
StartRevocationRefresher launches a background goroutine that calls RefreshRevocationList every RevocationRefreshInterval until ctx is done. Refresh failures are logged but do not stop the loop — the FAIL-OPEN policy is enforced at lookup time, not here.
Returns a stop function that the caller can defer to halt the loop before ctx is cancelled.
func TailStream ¶ added in v1.0.11
func TailStream(ctx context.Context, opts TailOptions) error
TailStream streams ping_api /license/validate events until ctx is canceled.
Color coding:
- green → allow
- red → deny
- yellow → rate_limit
Ctrl-C (context cancel) exits cleanly with no goroutine leak. OAuth tokens and full key values are never emitted — only KeyPrefix. Connection loss reconnects with exponential backoff.
func ValidateKeyFormat ¶ added in v1.0.4
ValidateKeyFormat checks that a key has a recognized product prefix and meets the minimum length requirement. Returns errs.ErrInvalidLicenseKey on failure.
func VerifyRevocationSignature ¶ added in v1.0.13
func VerifyRevocationSignature(list *RevocationList) bool
VerifyRevocationSignature checks the Ed25519 signature on `list` against the bundled license public keys. Returns true on a valid signature.
func WriteCache ¶ added in v1.0.6
func WriteCache(entry *CacheEntry) error
WriteCache writes the cache entry to disk with 0600 permissions using an atomic tmpfile + rename pattern so partial writes never corrupt an existing cache. (D3-T10: prevent torn writes that would otherwise force fail-closed.)
func WriteRevocationCache ¶ added in v1.0.13
func WriteRevocationCache(c *RevocationCache) error
WriteRevocationCache persists the cache with 0600 permissions.
Types ¶
type CacheEntry ¶ added in v1.0.6
type CacheEntry struct {
KeyHash string `json:"key_hash"`
Tier string `json:"tier"`
PluginsAllowed []string `json:"plugins_allowed"`
FetchedAt int64 `json:"fetched_at"`
ExpiresAt int64 `json:"expires_at"`
Signature string `json:"signature"`
SignatureKeyID int `json:"signature_key_id"`
}
CacheEntry represents a cached license validation response with Ed25519 signature from the server.
func ReadCache ¶ added in v1.0.6
func ReadCache() (*CacheEntry, error)
ReadCache reads and parses the license cache file. Returns nil, nil if the cache file does not exist.
func (*CacheEntry) CacheAge ¶ added in v1.0.6
func (c *CacheEntry) CacheAge() time.Duration
CacheAge returns how long ago the cache was fetched.
func (*CacheEntry) VerifySignature ¶ added in v1.0.6
func (c *CacheEntry) VerifySignature() bool
VerifySignature verifies the cache entry's Ed25519 signature against the bundled public keys. It accepts the current key (keyID N) and the previous key (keyID N-1) to support rotation windows.
type Error ¶ added in v1.0.13
Error is the typed license error returned by the validation flow. Message is printed to the user verbatim. UserAction is a short next-step suggestion (already embedded in Message for the predefined errors below; kept separate so callers may format their own UI).
func IsLicenseError ¶ added in v1.0.13
IsLicenseError reports whether err is or wraps a *Error and returns it.
func NewExpiredError ¶ added in v1.0.13
NewExpiredError builds a license-expired error. expiredOn should be a formatted date string such as "2026-01-15".
func NewInsufficientTierError ¶ added in v1.0.13
NewInsufficientTierError builds an insufficient-tier error for a plugin install attempt. plugin is the plugin name, required is the required tier, current is the user's current tier.
func NewRevokedError ¶ added in v1.0.13
NewRevokedError builds a license-revoked error with a human reason.
func NewSlotExhaustedError ¶ added in v1.0.13
NewSlotExhaustedError builds a slot-exhausted error. used and limit are the current and maximum activation counts.
type GraceCheckResult ¶ added in v1.0.6
type GraceCheckResult struct {
State GraceState
CacheAge time.Duration
ExpiresAt time.Time
Tier string
Message string
CanProceed bool
WriteAllowed bool
}
GraceCheckResult contains the outcome of a grace period check.
func DetermineGraceState ¶ added in v1.0.6
func DetermineGraceState(entry *CacheEntry) GraceCheckResult
DetermineGraceState evaluates the current grace state from a cache entry. If entry is nil, returns GraceExpired (no cache means no validation).
func SimulateOffline ¶ added in v1.0.6
func SimulateOffline(days int) (*GraceCheckResult, error)
SimulateOffline modifies the license cache to appear as if the system has been offline for the given number of days. This is used for testing grace period transitions without iptables manipulation.
The function: 1. Checks the LICENSE_ALLOW_SIMULATION guard 2. Reads the current cache 3. Backdates FetchedAt by the specified number of days 4. Writes the modified cache back
func (GraceCheckResult) IsWriteAllowed ¶ added in v1.0.6
func (r GraceCheckResult) IsWriteAllowed() bool
IsWriteAllowed checks if write operations on paid plugins are permitted given the current grace state.
func (GraceCheckResult) NeedsBanner ¶ added in v1.0.6
func (r GraceCheckResult) NeedsBanner() bool
NeedsBanner returns true if a warning banner should be displayed.
type GraceState ¶ added in v1.0.6
type GraceState string
GraceState represents the license grace period state.
const ( // GraceValid means the license is validated and current. GraceValid GraceState = "valid" // GraceSoft means the cache is 24h-7d old; show warning banner. GraceSoft GraceState = "grace_soft" // GraceHard means the cache is >7d old; paid plugin writes are refused. GraceHard GraceState = "grace_hard" // GraceExpired means the license has expired. GraceExpired GraceState = "expired" // GraceRevoked means the license was explicitly revoked. GraceRevoked GraceState = "revoked" )
type LicenseEvent ¶ added in v1.0.11
type LicenseEvent struct {
// Timestamp of the event.
Timestamp time.Time `json:"ts"`
// KeyPrefix is the first 12 chars of the license key (never full key).
KeyPrefix string `json:"key_prefix"`
// Plugin is the plugin being validated.
Plugin string `json:"plugin"`
// Result is "allow", "deny", or "rate_limit".
Result string `json:"result"`
// IP is the requester IP.
IP string `json:"ip,omitempty"`
}
LicenseEvent is a single validation event from ping_api.
type LicenseRecord ¶ added in v1.0.13
LicenseRecord is the minimal shape needed to evaluate revocation. Each field is optional: zero values are treated as "no match attempted".
type ProductPrefix ¶ added in v1.0.4
ProductPrefix maps key prefixes to product display names.
func DetectProduct ¶ added in v1.0.4
func DetectProduct(key string) *ProductPrefix
DetectProduct returns the product info for a key based on its prefix. Returns nil if the prefix is not recognized.
type PublicKeyEntry ¶ added in v1.0.6
PublicKeyEntry holds a versioned Ed25519 public key.
func GetPublicKeys ¶ added in v1.0.6
func GetPublicKeys() []PublicKeyEntry
GetPublicKeys returns the active public keys, respecting the LICENSE_PUBLIC_KEY_OVERRIDE environment variable for testing.
type RevocationCache ¶ added in v1.0.13
type RevocationCache struct {
List RevocationList `json:"list"`
FetchedAt int64 `json:"fetched_at"` // unix seconds
ETag string `json:"etag,omitempty"` // server-emitted strong ETag (sha256 hex)
}
RevocationCache is the on-disk cache wrapper around RevocationList. FetchedAt is set by the CLI whenever a successful refresh completes. ETag is captured from the most recent 200 response so subsequent refreshes can send `If-None-Match` and let the server short-circuit with 304. Older caches (pre-D3-T08a) without an ETag field decode with ETag="" and simply skip the conditional-GET header — fully backward compatible.
func ReadRevocationCache ¶ added in v1.0.13
func ReadRevocationCache() (*RevocationCache, error)
ReadRevocationCache loads the cache from disk. Returns (nil, nil) when the file is absent (cold start).
func RefreshRevocationList ¶ added in v1.0.13
func RefreshRevocationList(ctx context.Context) (*RevocationCache, error)
RefreshRevocationList fetches the signed list from ping.nself.org, verifies the signature, and persists it on disk.
Behaviour:
- Network / decode error → returns the wrapped error; does NOT touch disk.
- Bad signature → returns ErrRevocationSignatureInvalid; does NOT persist.
- Success → writes cache and returns the new RevocationCache.
type RevocationEntry ¶ added in v1.0.13
type RevocationEntry struct {
Type string `json:"type"` // "jti" | "user_id" | "kid"
ID string `json:"id"` // identifier value
Reason string `json:"reason"` // human-readable
RevokedAt string `json:"revoked_at"` // ISO8601
}
RevocationEntry is one revoked identifier. Field order matters for the canonical JSON encoder; struct tags pin the wire names.
type RevocationList ¶ added in v1.0.13
type RevocationList struct {
IssuedAt string `json:"issued_at"`
ValidUntil string `json:"valid_until"`
Kid string `json:"kid"`
Revoked []RevocationEntry `json:"revoked"`
Signature string `json:"signature"`
}
RevocationList is the full signed payload from /license/revocation-list.
type RevokedMarker ¶ added in v1.0.12
type RevokedMarker struct {
// WipedAt is the Unix timestamp when the license was revoked.
WipedAt int64 `json:"wiped_at"`
// Reason is an optional human-readable reason.
Reason string `json:"reason,omitempty"`
}
RevokedMarker is written to disk when a license is revoked.
type TailOptions ¶ added in v1.0.11
type TailOptions struct {
// Filters is a list of field=value pairs (e.g. "key=nself_pro_xxx", "result=denied").
Filters map[string]string
// PingURL is the ping_api base URL (defaults to NSELF_PING_URL or https://ping.nself.org).
PingURL string
// Stdout for output.
Stdout io.Writer
// HTTPClient allows injection in tests.
HTTPClient *http.Client
}
TailOptions holds parsed flags for license tail.
type ValidateResponse ¶ added in v1.0.6
type ValidateResponse struct {
Valid bool `json:"valid"`
Reason string `json:"reason,omitempty"`
Tier string `json:"tier"`
Plugins []string `json:"plugins"`
ExpiresAt string `json:"expires_at,omitempty"`
Signature string `json:"signature,omitempty"`
KeyID int `json:"key_id,omitempty"`
}
ValidateResponse is the JSON body returned by /license/validate on HTTP 200.
type ValidationResult ¶ added in v1.0.6
type ValidationResult struct {
Valid bool
Tier string
Plugins []string
ExpiresAt time.Time
GraceState GraceState
Message string
FromCache bool
WriteAllowed bool
}
ValidationResult holds the outcome of a full license validation.
func RefreshCache ¶ added in v1.0.6
func RefreshCache(ctx context.Context, key string) (*ValidationResult, error)
RefreshCache forces a remote validation and updates the cache. Returns the validation result or an error if the remote call fails.
func ValidateFull ¶ added in v1.0.6
func ValidateFull(ctx context.Context, key string) (*ValidationResult, error)
ValidateFull performs the complete license validation flow: 1. Try remote validation against ping.nself.org 2. On success: update cache, return valid 3. On network failure: fall back to cache with grace state evaluation 4. On invalid response: mark cache as invalid, return error
type ValidatorOptions ¶ added in v1.0.13
type ValidatorOptions struct {
// Clock is used for all time comparisons. nil → wall clock.
Clock Clock
// HTTPClient is used for the remote validation call. nil → default 30s client.
HTTPClient HTTPDoer
// PingURL overrides the configured ping endpoint. Empty → PingURL().
PingURL string
// SkipSignatureVerify allows skipping signature verification for tests.
// Production callers MUST leave this false.
SkipSignatureVerify bool
// WarnOnce is a hook called at most once per process when emitting a
// FAIL-OPEN warning. nil → default: write to os.Stderr once.
WarnOnce func(msg string)
}
ValidatorOptions configures Validate.
type ValidatorResult ¶ added in v1.0.13
type ValidatorResult struct {
Status ValidatorStatus
CanProceed bool
FromCache bool
Tier string
Plugins []string
CacheAge time.Duration
Reason string
WarnMessage string // non-empty when caller should emit a stderr warning
}
ValidatorResult is the FAIL-OPEN-aware validation outcome.
func Validate ¶ added in v1.0.13
func Validate(ctx context.Context, key string, opts *ValidatorOptions) (*ValidatorResult, error)
Validate executes the FAIL-OPEN license validation flow.
Steps:
- Try remote validation (when HTTPClient + key supplied). Distinguish: - 200 valid: update cache, return Valid - 200 invalid/revoked: return Revoked - 401/403: return FailClosed (auth failure, NOT fail-open) - 5xx / transport err: fall through to cache-only path (fail-open eligible)
- Cache-only path: - cache absent: FailClosed - signature invalid: FailClosed (never fail-open on tamper) - cache age ≤ soft TTL: FailOpen (silent — no warning) - cache age ≤ hard TTL: FailOpen (warning) - cache age > hard TTL: FailClosed
`key` may be empty to validate from cache only (no remote call).
type ValidatorStatus ¶ added in v1.0.13
type ValidatorStatus string
ValidatorStatus represents the validator's terminal status decision.
const ( // StatusValid means the license is current and the operation may proceed. StatusValid ValidatorStatus = "valid" // StatusExpired means the license itself has passed its expires_at. StatusExpired ValidatorStatus = "expired" // StatusRevoked means the server explicitly revoked the license. StatusRevoked ValidatorStatus = "revoked" // StatusFailOpen means cache permits proceeding even though the remote was // unreachable. CanProceed is true; a stderr warning may be emitted. StatusFailOpen ValidatorStatus = "fail_open" // StatusFailClosed means the operation must NOT proceed. Bad signature, // stale cache beyond hard TTL, missing cache, or auth failure. StatusFailClosed ValidatorStatus = "fail_closed" )