Skip to content

fix(license): unify offline grace ladder to one canonical source - #404

Merged
acamarata merged 3 commits into
mainfrom
fix/unify-license-grace-ladder
Sep 11, 2026
Merged

acamarata merged 3 commits into
mainfrom
fix/unify-license-grace-ladder

Conversation

@acamarata

Copy link
Copy Markdown
Collaborator

Summary

  • Unifies the CLI's three drifted descriptions of "license server unreachable, cache present" into one source: internal/license/grace.go's GraceSoftThreshold (72h, silent) and GraceHardThreshold (7d, fail closed).
  • validator.go's previously-separate FailOpenSoftTTL/FailOpenHardTTL (72h/14d) become aliases of the grace.go constants instead of a second declaration.
  • internal/plugin/license.go's cacheTTL/offlineGraceTTL (a separate HMAC-signed cache) now alias the same constants instead of hand-typing 24h/7d.
  • sdk/go/license/license.go's DefaultGracePeriod keeps its own literal (separate Go module, cannot import cli/internal) but its comment no longer cites the nonexistent F07-PRICING-TIERS.md.
  • Removed LicenseConfig.GraceDays (declared, never read) and the false "Configurable via LICENSE_GRACE_DAYS env var" claim in grace.go.
  • Kept Validate/ValidateFull (no non-test callers, but ~35 direct unit tests covering branches not exercised elsewhere) rather than deleting them, repointing them to the unified constants instead.
  • Updated test boundaries for the new 72h soft threshold and added an explicit just-under-72h silent case.

Companion web docs fix (bundle license §4, licensing.mdx, offline.mdx) lands separately on the existing PR #226 branch in nself/web.

Test plan

  • gofmt -l . empty
  • make vet clean
  • go test ./internal/license/... ./internal/plugin/... — 836 passing
  • internal/license coverage 91.1% (baseline 91.2%, no regression)
  • internal/plugin coverage 50.9% (unchanged from baseline)

The CLI shipped three descriptions of "license server unreachable, cache
present": grace.go's GraceSoftThreshold/GraceHardThreshold (24h/7d, the
only one with live callers), validator.go's FailOpenSoftTTL/FailOpenHardTTL
(72h/14d, dead code with no non-test callers), and a third ladder published
in the web docs. They had drifted apart.

grace.go is now the single owner of the offline ladder: soft=72h (silent,
covers a full weekend outage on our side without alarming a paying
customer), hard=7d (fail closed - checker.go sends only the license key
with no machine identifier, so the cache is a copyable bare credential and
widening the ceiling multiplies that exposure).

- validator.go's FailOpenSoftTTL/FailOpenHardTTL become aliases of
  grace.go's constants instead of a second declaration. Validate/
  ValidateFull are unused in production (no non-test callers in cmd/ or
  sdk/) but carry ~35 direct unit tests across four files exercising
  branches (signature verification, revocation-during-fail-open, atomic
  cache write) not otherwise covered elsewhere; kept and repointed rather
  than deleted, per the "keep and read the unified constants" option.
- internal/plugin/license.go's cacheTTL/offlineGraceTTL (a separate
  HMAC-signed cache file) now alias the same two constants instead of
  hand-typing their own 24h/7d.
- sdk/go/license/license.go's DefaultGracePeriod keeps its own literal
  (sdk/go is a separate Go module and cannot import cli/internal/license)
  but its comment now says so instead of citing a nonexistent
  F07-PRICING-TIERS.md.
- Removed LicenseConfig.GraceDays (internal/config): declared with an
  env tag but never read anywhere, and grace.go's comment claiming the
  ladder was "Configurable via LICENSE_GRACE_DAYS env var" was false.

Test boundaries updated for the new 72h soft threshold (several tests used
24-48h ages to exercise the old soft-grace window) and added an explicit
just-under-72h silent case. No behavior change outside the ladder itself:
gofmt clean, vet clean, 836 tests passing, license package coverage 91.1%
(unchanged from baseline), plugin package coverage unchanged at 50.9%.
The doc-sync gate flagged this branch for changing env vars without touching
F09-ENV-VAR-INVENTORY.md or .github/wiki/Config-Env-Vars.md. The gate is right:
this branch removes the GraceDays config field.

Config-Env-Vars.md had no licensing section at all, so rather than a token edit
this adds one covering the variables the code actually reads, each verified
against its env tag or os.Getenv call site.

Records explicitly that the offline grace window is NOT settable by env, and
that LICENSE_GRACE_DAYS never had any effect despite a code comment advertising
it. Someone who set that variable is entitled to know it did nothing.

Also documents NSELF_LICENSE_FAIL_OPEN and NSELF_LICENSE_SKIP_VERIFY as escape
hatches with their real limits: neither overrides a server that answers, and
neither overrides revocation.
…e env vars

The parity gate failed with '.github/surface-parity.{md,json} is stale (run
make parity)'. Two things on this branch move that surface: the GraceDays
config field is removed, and the new Licensing section in
.github/wiki/Config-Env-Vars.md documents NSELF_LICENSE_KEY, which the tool had
been counting as undocumented.

Regenerated with make parity. The only change is nself init flipping from
'undocumented: NSELF_LICENSE_KEY' to 'documented', and the undocumented env var
total dropping from 17 to 16.
@acamarata
acamarata merged commit 2296259 into main Sep 11, 2026
32 checks passed
@acamarata
acamarata deleted the fix/unify-license-grace-ladder branch September 11, 2026 14:43
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