license

package
v1.1.2 Latest Latest
Warning

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

Go to latest
Published: May 15, 2026 License: MIT Imports: 19 Imported by: 0

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

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

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

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

View Source
const DefaultCheckInterval = 6 * time.Hour

DefaultCheckInterval is the default time between license checks (6 hours).

View Source
const DefaultPingURL = "https://ping.nself.org"

DefaultPingURL is the production license validation endpoint.

View Source
const FailOpenHardTTL = 14 * 24 * time.Hour

FailOpenHardTTL is the absolute fail-open ceiling. Beyond this, fail-closed. Default 14 days.

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

View Source
const GraceHardThreshold = 7 * 24 * time.Hour

GraceHardThreshold is when hard degradation begins (7 days). Configurable via LICENSE_GRACE_DAYS env var.

View Source
const GraceSoftThreshold = 24 * time.Hour

GraceSoftThreshold is when the soft grace warning starts (24 hours).

View Source
const LicenseDirV1 = ".nself"

LicenseDirV1 is the v1 license directory name under $HOME.

View Source
const LicenseDirV2 = ".config/nself"

LicenseDirV2 is the v2 license directory name under $HOME.

View Source
const LicenseFile = "license.json"

LicenseFile is the license JSON filename used in both v1 and v2.

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

View Source
const RevocationRefreshInterval = 1 * time.Hour

RevocationRefreshInterval is the recommended hourly refresh cadence.

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

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

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

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

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

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

func AddKey(key string) error

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

func BundleEntitled(ctx context.Context, key, bundleName string) (bool, error)

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

func CachePath() (string, error)

CachePath returns the full path to the license cache file. It respects LICENSE_CACHE_PATH if set.

func CacheTTLExpiry added in v1.0.12

func CacheTTLExpiry(tier string, fetchedAt time.Time) time.Time

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

func CacheTTLForTier(tier string) time.Duration

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

func DetectTierFromKey(key string) string

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

func ExportCache() ([]byte, error)

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

func GetKey() (string, error)

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

func GetOwnerKey() (string, error)

GetOwnerKey reads the stored owner license key. Returns ("", nil) if no owner key is configured.

func HashKey added in v1.0.6

func HashKey(key string) string

HashKey returns the SHA-256 hex digest of a license key.

func ImportCache added in v1.0.6

func ImportCache(data []byte) error

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

func IsOwnerKey(key string) bool

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

func MaskKey(key string) string

MaskKey returns a masked version of the key showing the first 10 and last 4 characters. Delegates to the existing maskKey function.

func MigrateLicenseFromV1

func MigrateLicenseFromV1(home string) error

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 PingURL added in v1.0.6

func PingURL() string

PingURL returns the configured ping API URL.

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

func RemoveKey(query string) (int, error)

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

func RestoreWithKey(newKey string) error

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

func RevocationCachePath() (string, error)

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:

  1. Writes a revoked marker to disk.
  2. Deletes the local license cache.
  3. Clears all stored license keys.

Plugins go DORMANT (not uninstalled) on the next `nself build`.

func SetKey

func SetKey(key string) error

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

func SetKeyReplaceAll(key string) (int, error)

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

func SetOwnerKey(key string) error

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

func SetRevocationHTTPClient(c *http.Client)

SetRevocationHTTPClient overrides the HTTP client used for refreshes. Test-only.

func ShowKey

func ShowKey() (masked string, tier string, err error)

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

func StartRevocationRefresher(ctx context.Context) (stop func())

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

func ValidateKeyFormat(key string) error

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 Clock added in v1.0.13

type Clock interface {
	Now() time.Time
}

Clock abstracts time.Now for testability.

type Error added in v1.0.13

type Error struct {
	Code       string
	Message    string
	UserAction string
	Wrapped    error
}

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

func IsLicenseError(err error) (*Error, bool)

IsLicenseError reports whether err is or wraps a *Error and returns it.

func NewExpiredError added in v1.0.13

func NewExpiredError(expiredOn string) *Error

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

func NewInsufficientTierError(plugin, required, current string) *Error

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

func NewRevokedError(reason string) *Error

NewRevokedError builds a license-revoked error with a human reason.

func NewSlotExhaustedError added in v1.0.13

func NewSlotExhaustedError(used, limit int) *Error

NewSlotExhaustedError builds a slot-exhausted error. used and limit are the current and maximum activation counts.

func (*Error) Error added in v1.0.13

func (e *Error) Error() string

Error returns the user-facing message.

func (*Error) Unwrap added in v1.0.13

func (e *Error) Unwrap() error

Unwrap returns the wrapped underlying error, if any.

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 HTTPDoer added in v1.0.13

type HTTPDoer interface {
	Do(req *http.Request) (*http.Response, error)
}

HTTPDoer abstracts http.Client.Do for testability.

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

type LicenseRecord struct {
	JTI    string
	UserID string
	Kid    string
}

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

type ProductPrefix struct {
	Prefix      string
	Product     string
	DisplayName string
}

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

type PublicKeyEntry struct {
	ID  int
	Key ed25519.PublicKey
}

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:

  1. 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)
  2. 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"
)

Jump to

Keyboard shortcuts

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