Skip to content

docs(license): correct offline grace ladder to match code (7d ceiling, read-only not fail-closed) - #416

Merged
acamarata merged 3 commits into
mainfrom
docs/license-offline-ladder-truth
Sep 13, 2026
Merged

acamarata merged 3 commits into
mainfrom
docs/license-offline-ladder-truth

Conversation

@acamarata

Copy link
Copy Markdown
Collaborator

The defect

P6-E12-W4-S4-T2 unified FailOpenHardTTL from a separate 14-day value to
GraceHardThreshold (7 days). The code comment records the change:

Alias of GraceHardThreshold (grace.go). Previously a separate 14-day value;
unified to the same 7-day ceiling as the rest of the offline ladder
(P6-E12-W4-S4-T2)

The docs never followed. license-verification.md still describes the pre-change
ladder, so we currently tell paying customers they have 14 days offline when the
CLI cuts them to read-only at 7.
That is a support incident waiting to happen, not
a cosmetic drift.

What was wrong

The page was wrong in three independent ways:

Claim in docs Actual code
Silent until 7 days Silent until 72 hours (GraceSoftThreshold)
Warning 7→14 days Warning 72h→7 days
Over 14 days: "fail-closed, command refuses to run" Over 7 days: CanProceed: true, WriteAllowed: falseread-only, still runs

It additionally documented configuration that does not exist anywhere in the
codebase
NSELF_LICENSE_OFFLINE_MAX_DAYS, NSELF_LICENSE_CACHE_PATH and
NSELF_LICENSE_OFFLINE all return zero grep hits — along with an air-gapped flow
(~/.nself/license/offline.key, NSELF_LICENSE_OFFLINE=1) built on two of them.
The real cache override is LICENSE_CACHE_PATH, defaulting to
~/.cache/nself/license.json, and the real air-gap mechanism is
nself license export / nself license import, which already exist.

What this PR does

  • Rewrites the page against internal/license/grace.go, with the real states
    (valid / grace_soft / grace_hard) and the actual user-facing strings.
  • Separates the two clocks that were conflated: cache age (the offline ladder)
    versus server-reported expiry, which has its own independent 30-day
    PostExpiryGraceWindow. Refreshing the cache does not clear an expiry lapse, and
    the old page gave no way to tell those apart.
  • Records why the 7-day ceiling does not widen alongside the soft threshold —
    validation sends only the license key with no per-machine identifier, so the cache
    is a bare copyable credential and every extra day multiplies that exposure. This
    reasoning lived only in a code comment.
  • Replaces the fabricated env vars and air-gap steps with the export/import flow.
  • Documents simulate-offline as the way to exercise each band.

It also fixes that command's own --help, which listed 7 # Trigger warning banner
and 14 # Trigger hard stop. Under the current thresholds 7 days is past the
warning band and 14 is deep into read-only, so both examples demonstrated the wrong
state. Now 1 / 5 / 10, which land in silent / warning / read-only.

Not in scope

No threshold is changed. This is documentation catching up to code. The 72h and
7d constants are deliberate and well-argued in grace.go; if anything here reads as
a case for widening the ceiling, that is a separate decision with a security
tradeoff attached.

Verification

  • go build ./cmd/... — success
  • go vet ./cmd/commands/ — no issues
  • Every documented env var, path, subcommand and threshold traced to its
    definition; the three fabricated vars confirmed absent by grep across *.go.

Follow-up (separate PRs)

The same stale ladder was copied into two other repos and is being corrected
alongside this: web/org/src/content/docs/licensing/offline.mdx (which carries a
sourceWiki: pointer at this very file) and plugins-pro/.github/docs/licensing/
(entitlements.md, ping-api.md, both still claiming 14 days).

P6-E12-W4-S4-T2 unified FailOpenHardTTL from a separate 14-day value to
GraceHardThreshold (7 days), but the offline-mode docs and the
simulate-offline help text were never updated. They still describe a
ladder the CLI has not implemented since that change.

The wiki page was wrong in three separate ways, not one:

  - warning starts at 72h, not 7 days (GraceSoftThreshold)
  - the ceiling is 7 days, not 14 (GraceHardThreshold)
  - past the ceiling the CLI goes READ-ONLY (CanProceed true,
    WriteAllowed false), it does not refuse to run

It also documented three environment variables that do not exist in the
codebase (NSELF_LICENSE_OFFLINE_MAX_DAYS, NSELF_LICENSE_CACHE_PATH,
NSELF_LICENSE_OFFLINE) and an air-gap flow built on two of them. The real
cache override is LICENSE_CACHE_PATH at ~/.cache/nself/license.json, and
the real air-gap path is license export/import.

Rewrites the page against the code, separates the two clocks that were
conflated (cache age vs server-reported expiry, the latter carrying its
own 30-day PostExpiryGraceWindow), and records why the 7-day ceiling
deliberately does not widen. Also corrects simulate-offline's examples,
which used 7 and 14 as the warning and hard-stop days when those now land
in the silent and read-only bands respectively.

No threshold is changed; this is documentation catching up to code.
…only

bundleEntitledFromGrace requires WriteAllowed, so a bundle install past
the 7-day ceiling is refused rather than degraded. Without this the wiki
page and web's licensing/offline page read as contradicting each other on
the same threshold.
The rewrite asserted no env var widens the grace window. That is wrong:
NSELF_LICENSE_FAIL_OPEN=1 takes the unbounded bundleEntitledFromCache
branch (checker.go:84-87), removing the age ceiling entirely for CI and
air-gap installs. Documents it with the production warning, and states
precisely that it switches branch rather than retuning the thresholds.
@acamarata
acamarata merged commit 2a3ec04 into main Sep 13, 2026
31 checks passed
@acamarata
acamarata deleted the docs/license-offline-ladder-truth branch September 13, 2026 08:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant