Skip to main content

Offline License Verification

Offline License Verification — ɳSelf documentation.

ɳSelf plugin licensing is online-first with a bounded offline path, not offline-first: a bundle plugin install validates live against ping.nself.org by default, and only falls back to a time-limited local cache if that call cannot complete. This page documents the real grace ladder, fail-open variable, and offline commands, cited against the CLI source so it cannot drift out of sync again.

Default behavior: bounded offline grace

nself plugin install checks bundle entitlement live (BundleEntitled, cli/internal/license/checker.go:59-118). If the server answers, that answer is authoritative: a 401/403 or a valid: false response fails closed immediately, no cache involved. Only when the call cannot complete at all, a transport error, DNS failure, or timeout, does the CLI fall back to your local, signed license cache, and only on a bounded ladder (bundleEntitledFromGrace, cli/internal/license/checker.go:168-196):

Cache age since last successful validation Result
Under 72 hours Proceeds silently
72 hours to 7 days Proceeds, with a warning printed to stderr
Over 7 days Fails closed — the install is refused until validation succeeds

A revoked license is refused at any cache age; the ladder never overrides revocation. These are the same numbers as the nSelf Bundle License §4 and the Licensing page — one source (cli/internal/license/grace.go), three places it’s read from.

Fail-open for CI and air-gapped installs

Set NSELF_LICENSE_FAIL_OPEN=1 to replace the bounded ladder above with an unbounded check, for CI runners and air-gapped installs that are offline by design. It only changes the same transport-failure branch: instead of the 72h/7d ladder, the CLI checks the cache’s tier against what you are trying to install with no age ceiling at all (bundleEntitledFromCache, cli/internal/license/checker.go:124-151). It is not a global switch — a server that answers 401/403 or valid: false still fails closed regardless of this variable, and a revoked license is still refused at any cache age:

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.

Removing the age ceiling matters because the cache is a bare credential: it carries only the license key, with no per-machine identifier, so a copy of it works on any host it is placed on. The 72h/7d ceiling is what limits how long a copied cache stays useful; this variable removes that limit entirely.

Variable Default Purpose
NSELF_LICENSE_FAIL_OPEN unset (bounded 72h/7d grace) CI/air-gap only. Never set on a production install.
LICENSE_CACHE_PATH ~/.cache/nself/license.json Override the license cache file location.

Air-gapped installs: export / import

For a host that never reaches the internet, move a signed cache file over instead of setting NSELF_LICENSE_FAIL_OPEN:

# On a machine that can reach ping.nself.org:
nself license refresh
nself license export > cache.json

# Copy cache.json to the disconnected host, then:
nself license import cache.json

nself license import verifies the Ed25519 signature on the file before writing it to the local cache and refuses an unsigned or tampered file (cli/internal/license/validate.go:177-195). NSELF_LICENSE_SKIP_VERIFY=1 (plus NSELF_LICENSE_SKIP_VERIFY_FORCE=1 and --force) can force import to accept an unsigned file. Development and testing only — it defeats the signature check that makes the offline cache trustworthy, so never set it on a production installation.

Manual refresh

nself license refresh

Pulls a fresh signed entitlement from ping.nself.org/license/validate and replaces the local cache. Run this after extended offline periods, after key rotation, or before a planned offline window.

Simulating offline behavior

LICENSE_ALLOW_SIMULATION=true nself license simulate-offline <days>
nself license simulate-offline --clear

Exercises the ladder by backdating your local cache so the CLI behaves as though it had been offline for that many days. It is gated behind LICENSE_ALLOW_SIMULATION=true and returns ErrSimulationNotAllowed without it (cli/internal/license/simulate.go:36-38).

This writes to the cache file — it is not a dry run. SimulateOffline backdates FetchedAt and calls WriteCache (simulate.go:52-55), so run nself license simulate-offline --clear afterwards to restore the real timestamp. Never run it against a production install.

Useful days, given the 72h/7d ladder above: 1 stays silent, 5 triggers the warning, 10 puts the cache past the ceiling.

Troubleshooting

Cache corruption or a signature that no longer verifies. Remove the cache file at LICENSE_CACHE_PATH (default ~/.cache/nself/license.json) and run nself license refresh. The cache is fully regenerable from a live validation.

Stuck failing closed with no network. Use the export/import workflow above from a machine that does have network access, or set NSELF_LICENSE_FAIL_OPEN=1 if this is a CI runner or air-gapped install where that is the accepted trade-off — never on a production host.