Fair-code licensing: offline Ed25519 verification + seat enforcement + check-in (v0.4 #24) #44

Merged
Cordy merged 7 commits from feat/license into main 2026-07-26 17:11:16 +00:00
Owner

Implements the enforcement model recorded on #24, with the production public key (M1) embedded.

Key format: CAIRN1.<b64url(payload)>.<b64url(sig)> — payload {id, org, seats, issuedAt, expiresAt, keyId}, Ed25519-verified offline against the embedded public key (private key never enters this repo; license.Sign exists for the server and tests only).

Seat accounting (no DB): distinct authenticated usernames, persisted to license.statePath JSON. Enforcement wraps the auth provider: known users and admins always pass; a new user is admitted only under the cap (50 free tier, or the license's seat count when active). Existing users are never locked out; reads are never blocked.

Check-in: monthly against license.checkinUrl, only when >50 seats are in use (≤50 never phones home, key installed or not). Connectivity failure → daily retry, 7-day grace (surfaced in status), then the license stops counting until a check-in succeeds. Server 403 → revoked immediately. Payload is just {licenseId, seatsUsed} — no telemetry.

API: GET/PUT /api/v1/license (admin-gated) — status (org, cap, seats used, expiry, last check-in, grace deadline) and key installation (persists across restarts via the state file, so the config secret needn't change). Config block license {key, checkinUrl, statePath} + CAIRN_LICENSE_KEY.

Tests: parse/tamper/wrong-key, embedded-key parse, free-tier cap + admin bypass, license raises cap, expired-install rejection, grace-window expiry drops cap (existing over-cap user stays admitted), revocation, state persistence across restart.

UI (Settings → License, #43) follows in the next PR.

Implements the enforcement model recorded on #24, with the production public key (M1) embedded. **Key format:** `CAIRN1.<b64url(payload)>.<b64url(sig)>` — payload `{id, org, seats, issuedAt, expiresAt, keyId}`, Ed25519-verified offline against the embedded public key (private key never enters this repo; `license.Sign` exists for the server and tests only). **Seat accounting (no DB):** distinct authenticated usernames, persisted to `license.statePath` JSON. Enforcement wraps the auth provider: known users and admins always pass; a *new* user is admitted only under the cap (`50` free tier, or the license's seat count when active). **Existing users are never locked out; reads are never blocked.** **Check-in:** monthly against `license.checkinUrl`, **only when >50 seats are in use** (≤50 never phones home, key installed or not). Connectivity failure → daily retry, 7-day grace (surfaced in status), then the license stops counting until a check-in succeeds. Server 403 → revoked immediately. Payload is just `{licenseId, seatsUsed}` — no telemetry. **API:** `GET/PUT /api/v1/license` (admin-gated) — status (org, cap, seats used, expiry, last check-in, grace deadline) and key installation (persists across restarts via the state file, so the config secret needn't change). Config block `license {key, checkinUrl, statePath}` + `CAIRN_LICENSE_KEY`. Tests: parse/tamper/wrong-key, embedded-key parse, free-tier cap + admin bypass, license raises cap, expired-install rejection, grace-window expiry drops cap (existing over-cap user stays admitted), revocation, state persistence across restart. UI (Settings → License, #43) follows in the next PR.
license: fix TestFreeTierCap existing-user probe (usera0 vs fillName scheme)
All checks were successful
ci / test-and-build (pull_request) Successful in 18s
7df946e0f2
Cordy merged commit a1a1a97d6f into main 2026-07-26 17:11:16 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: Cordy/Cairn#44
No description provided.