fix(acl): enforce token grant lifecycle live at sign time (#24, #25) #27

Merged
padreug merged 6 commits from issue-25-live-grant-lifecycle into dev 2026-06-19 16:05:19 +00:00

6 commits

Author SHA1 Message Date
7dcf97a296 refactor(acl)(#27 review): remove dead reject-all sentinel
Some checks failed
Docker image / build-and-push-image (push) Has been cancelled
PR #27 review finding #3: step 3a queried SigningCondition method='*'
and the docstring attributed it to rejectAllRequestsFromKey — but that
function writes method=null (never '*') and has zero callers, so the
'reject all' branch could never match. Subject-level reject is already
KeyUser.revokedAt (step 2, via the revoke_user admin command).

Drop the dead step-3a branch and the orphaned rejectAllRequestsFromKey
so the code matches reality. Per-(method,kind) denies (step 3, written
by add_signing_condition) are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 16:02:13 +02:00
e2cf10a66d test(acl)(#25): extract pure grantIsLive/liveWhere + unit tests
Some checks failed
Docker image / build-and-push-image (push) Has been cancelled
Move the lifecycle predicate into lib/acl/lifecycle.ts (re-exported from
the ACL module) so it can be unit-tested without a database. Adds Node
built-in test-runner coverage for the boundary conditions that define
the fix: past expiry -> dead, expiry == now -> dead (exclusive), revoke
beats a future expiry, and liveWhere kept in lockstep with grantIsLive.

Runner is node:test via ts-node (no new dependency; pnpm add is blocked
by the nix-built node_modules hoist pattern). 'npm test' -> 7 passing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 15:16:37 +02:00
85e781dfa9 fix(acl)(#24,#25): enforce token expiry+revoke live at sign time
The bug (#24): applyToken photocopied a token's policy rules into
SigningCondition rows at redeem; checkIfPubkeyAllowed matched those rows
(step 3) and short-circuited before the live Token join (step 4), so an
expired or revoked token kept signing forever — the copy carried no
lifecycle. Same cause re-shipped by upstream Signet (see docs survey).

Option D fix:
- grantIsLive(grant, now): the single 'valid right now?' predicate
  (revokedAt null AND not past expiresAt), used identically at redeem
  (Backend.validateToken) and sign (checkIfPubkeyAllowed). Redeem and
  sign can no longer disagree.
- Backend.applyToken records ONLY the KeyUser<-Token binding; it no
  longer materializes SigningCondition rows. Token policy is evaluated
  live every request.
- checkIfPubkeyAllowed step 4 filters tokens through liveWhere(now)
  (revoke + expiry) and grants connect off a live bound token; the
  manual-override layer (step 3) now honors SigningCondition
  expiresAt/revokedAt too (denials beat grants).

Closes the materialization-drift family: a new lifecycle rule is one
more predicate, never a forgotten photocopy. Token-revoke sibling
(spirekeeper#22) falls out of the same seam. Usage caps deferred (no
durable signing log exists yet to count) — follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 15:11:23 +02:00
6397c7988d feat(schema)(#25): Request.keyUserId + SigningCondition lifecycle for live grant eval
Additive, non-breaking schema prep for the Option D live-evaluation ACL:

- Request gains keyUserId (FK) + @@index([keyUserId, method]) so token
  usage caps can be derived live by COUNTing allowed Requests, replacing
  the never-enforced mutable PolicyRule.currentUsageCount (derive-don't-count,
  per lnbits/nostr_bunker prior art).
- SigningCondition gains createdAt/expiresAt/revokedAt so the manual-override
  layer carries its own lifecycle and runs through the same grantIsLive(now)
  predicate as token grants (D1: two typed sources, one shared rule).

No behavior change yet; the ACL hot path and applyToken de-materialization
follow in subsequent commits.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 15:00:33 +02:00
a707d203a1 docs(#25): source-verified ACL prior-art survey + keep-our-fork decision
Surveys Signet, Amber, FROSTR, promenade, NDK/rust-nostr/nak against
actual source; records the decision to keep our fork and treat Signet
as a parts donor (NIP-46 wire boundary keeps the signer substitutable).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 14:54:18 +02:00
8326a16ea9 docs(#25): add lnbits/nostr_bunker comparison (prior art)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 14:54:18 +02:00