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>
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>
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>
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>
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>