feat(#11): live-policy auth + 6 companion admin RPCs + Token.revokedAt #13
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "issue-11-live-policy-auth"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Closes #11. Three commits implementing the design ratified by webapp in issue #11 comment 1473:
chore(schema): add Token.revokedAt for surgical token revocationfeat(acl): live-policy auth in checkIfPubkeyAllowedfeat(admin): companion RPCs for live policy + token revocationWhat changes
Sign-time authorization shifts from materialized
SigningConditionsnapshots (frozen at token-bind time, can't be modified once issued) to a layered live lookup:KeyUser; missing →undefinedKeyUser.revokedAtset →false(binary user revoke beats everything)SigningConditionoverride layer (per-user grants/denies):method='*',allowed=false) →false(method, kind)row → returnrow.allowedKeyUser → Token → Policy → PolicyRulewithToken.revokedAt IS NULLand a matching rule →trueundefined(caller may still prompt admin)Six new admin RPCs operate on the policy/condition graph, each following the canonical
create_new_policy.tspattern (single JSON-stringified param,kind-24134response, one prisma mutation, gated by existing admin-npub allowlist):add_policy_rule{ policyId, rule: {method, kind?, maxUsageCount?} }remove_policy_rule{ ruleId }update_policy{ policyId, patch: {name?, expiresAt?} }add_signing_condition{ keyUserId, condition: {method, kind?, allowed} }remove_signing_condition{ conditionId }revoke_token{ tokenId }Token.revokedAt = now()— surgical revoke without nuking the KeyUserThe
'all'kind literal is honored as a wildcard inadd_policy_rulefor parity with the existingallowScopeToSigningConditionQueryconvention.Backwards compatibility
applyToken's SigningCondition fan-out is left intact. Existing bound users keep working because step 3 (override layer) finds their snapshot rows. The downstream cleanup — trimming the fan-out once step 4 is the established happy path — is tracked separately as #12.Dogfood — 8/8 PASS
Webapp ran the 9-case regression from comment 1473 §3 against this branch on bohm regtest (see coord log 2026-05-30T12:05Z). All 8 functional cases pass (case 9 —
maxUsageCount— is out of scope per the original spec):add_signing_condition allowed=false) denies even when policy allowsadd_signing_condition allowed=true) authorizes kind the policy doesn'tadd_policy_ruleimmediately enables sign_event for previously-bound users (headline feature)remove_policy_ruleimmediately revokes — live denial via step 4revoke_usercontinues to deny everything regardless of conditions/policy (step 2)revoke_tokendenies under that token, leaves other tokens for same KeyUser workingThe container entrypoint runs
prisma migrate deploycleanly on startup, applies the20260530112308_add_token_revoked_atmigration, and serves kind-24134 admin RPCs + kind-24133 NIP-46 sessions normally.Caveats worth flagging for reviewers
undefinedis a silent NIP-46 drop, not an error event. Only explicitfalse(step 2 or 3 deny) sends back["error", "Not authorized"].undefined(step 5 "no rule matched") produces no response at all; clients see a timeout. This is howNDKNip46Backendinterprets a falsy permit result — not a bug, but a thing test harnesses must be aware of.invalid Schnorr signatureduring dogfood on a freshly-provisioned account's first kind-31923 publish. Did not reproduce across 3 immediate retries. Bunker authorized and signed cleanly; lnbits-side strict verify rejected the returned event. Same class as the NDK first-call hiccup observed elsewhere. Not auth-related, not blocking.applyTokenfan-out interaction with new RPCs. Step 3 (override) short-circuits before step 4 (live policy), so any test exercising step 4 must avoid step-3 contamination. The dogfood harness uses an empty-rules policy + post-bindadd_policy_ruleto keep the live-policy path clean. This redundancy is what #12 will trim once step 4 is the established happy path. Cross-linked above.Test plan
NSECBUNKER_SRC=/home/padreug/dev/nsecbunkerd/dev docker compose -f docker-compose.dev.yml up -d --build nsecbunkerfrom~/dev/local/docker/regtest, then run the Python harness at/tmp/smoke11_cases.py(recoverable from coord log §"Harness shape")Token.revokedAtpresentcc #11 (the ratified design), #12 (the pre-filed follow-up).
🤖 Generated with Claude Code
Shifts sign-time authorization from materialized SigningCondition snapshots (frozen at token-bind time) to a layered model: 1. fetch KeyUser; missing → undefined 2. KeyUser.revokedAt set → false 3. SigningCondition override layer (explicit reject, per-(method, kind) grant/deny) → return matching row's allowed value 4. live policy join: KeyUser → Token (revokedAt IS NULL) → Policy → PolicyRule. Match on method + kind (exact / 'all' / NULL defensive) → true 5. else → undefined (caller may still prompt admin) Backwards-compatible. The existing applyToken fan-out of SigningCondition rows continues to populate step 3 for legacy auth, so already-bound users keep working with no behavior change. New users will still go through that fan-out until the follow-up trim (#12). The key win: PolicyRule mutations via the upcoming companion RPCs (add_policy_rule / remove_policy_rule / etc.) propagate live to every KeyUser bound to that policy, rather than requiring per-user backfill RPCs that don't exist. Algorithm ratified by webapp in #11 (comment). refs: #11