feat(acl): per-rule windowed usage caps enforced live at sign time (#28) #34
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "issue-28-usage-caps"
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?
Closes #28. Completes the lifecycle family from #25 — usage caps were the third sibling (after expiry #24 and revoke), written-but-never-enforced.
Model
PolicyRule.windowSecondsadded; the never-enforced mutablecurrentUsageCountdropped. A cap =(maxUsageCount, windowSeconds): at most N signings of this(method, kind)per rolling window.windowSecondsNULL = lifetime;maxUsageCountNULL = uncapped.SigningLog— new durable, append-only record of allowed signings; the source of truth caps count against (derive-don't-count — no counter to drift, thelnbits/nostr_bunkerpattern).Enforcement
checkIfPubkeyAllowedstep 4: among a live token's matching rules, every capped rule must have remaining budget in its window (COUNT(SigningLog) < maxUsageCount), counted live. Stacked caps all bind — a20/hrrule and a200/dayrule on the same policy are enforced together.recordSigning()writes aSigningLogrow from the permit callback when a consequential request (sign_event/encrypt/decrypt) is allowed.Retuning is live (your question)
New
update_policy_ruleadmin RPC patchesmaxUsageCount/windowSeconds/method/kindin place:Switching
200/day → 20/hris that one call — and because the ACL reads the rule live every request (the #27 Option D payoff), it takes effect on the next sign for every client on that policy, no re-pairing, no migration. Tightening is immediate.get_policiesnow returns each rule'sid+window_secondsso an operator can target it.Tests — 22 integration + 7 unit, all green
9 new cap cases: under/at-limit, signings outside the window excluded, uncapped, lifetime (null window) all-time count, kind-specific counting, both stacked-cap directions (hourly binds vs daily binds), and the
recordSigning → count → denyloop.Verification
nix develop -c npm run test:all→ 7 + 22 passing.tsc --noEmit→ only the 3 pre-existing unrelated errors.npm run build→ clean.20260620194331_usage_caps_signing_log(dropscurrentUsageCount, addswindowSeconds+SigningLog+ index).Deferred
SigningLogretention/pruning — the log grows unbounded; a prune reaper (older than the longest active window) is a follow-up I'll file.🤖 Generated with Claude Code
59295318f8c76bbf2791