feat(pairing): optional token TTL + revoke endpoint (#9/#12, #22) #23

Merged
padreug merged 2 commits from feat/pairing-revoke-ttl into main 2026-06-18 17:09:02 +00:00
Owner

Stacked on #21 (base = feat/seed-url-pairing). Retarget to main once #21 merges. The follow-up scoped out of #21 per discussion: adds the two additive pieces unblocked by aiolabs/lnbits#55.

(b) Optional token TTL

PairMachineData.duration_hours (validated > 0, optional) threads through pair_spirecreate_new_token(..., duration_hours=…) (lnbits#55 item 2). The bunker stamps expiresAt and rejects the token once it lapses, forcing a re-pair. None (default) = non-expiring; revoke is then the only invalidation path.

(c) Revoke endpoint — POST /machines/{id}/revoke

revoke_spireadmin_client.revoke_key_user(spire-<id>)set_machine_unpaired (clears paired_at, keeps machine_npub + bunker_spire_key_name for audit / re-pair). Returns RevokeResult{revoked_count} (>= 1 = access cut, 0 = nothing was bound).

Why revoke_key_user, not token revoke (the load-bearing detail, spirekeeper#22): lnbits eager-binds (redeems) the connect token at provision time (lnbits#32), so nsecbunkerd has already materialised the token's policy into standing per-KeyUser SigningCondition grants. Its sign-time ACL checks those grants before the Token.revokedAt filter — so revoking the token is a silent no-op and the spire keeps signing. Only KeyUser.revokedAt (via revoke_user/revoke_key_user) actually cuts it off. Verified live against nsecbunkerd@dev. A unit test asserts revoke routes to revoke_key_user and never token-revoke.

Tests

7 new (210 green): duration threading + default-None; revoke_spirerevoke_key_user (+ never token-revoke) + bunker-error mapping; endpoint wiring (revoke happy / zero-bound / 502). New code black + ruff clean.

Notes

  • Same lnbits-version constraint as #21: requires lnbits ≥ the #55 merge (b5fba561) for revoke_key_user + create_new_token(duration_hours=…).
  • Deferred from here (matches #21's remaining): Fleet "Pair/Revoke Spire" UI, and aiolabs/bitspire#52 (spire-side consumer).

🤖 Generated with Claude Code

**Stacked on #21** (base = `feat/seed-url-pairing`). Retarget to `main` once #21 merges. The follow-up scoped out of #21 per discussion: adds the two additive pieces unblocked by `aiolabs/lnbits#55`. ## (b) Optional token TTL `PairMachineData.duration_hours` (validated `> 0`, optional) threads through `pair_spire` → `create_new_token(..., duration_hours=…)` (lnbits#55 item 2). The bunker stamps `expiresAt` and rejects the token once it lapses, forcing a re-pair. `None` (default) = non-expiring; revoke is then the only invalidation path. ## (c) Revoke endpoint — `POST /machines/{id}/revoke` `revoke_spire` → `admin_client.revoke_key_user(spire-<id>)` → `set_machine_unpaired` (clears `paired_at`, keeps `machine_npub` + `bunker_spire_key_name` for audit / re-pair). Returns `RevokeResult{revoked_count}` (`>= 1` = access cut, `0` = nothing was bound). **Why `revoke_key_user`, not token revoke (the load-bearing detail, `spirekeeper#22`):** lnbits eager-binds (redeems) the connect token at provision time (`lnbits#32`), so nsecbunkerd has already materialised the token's policy into standing per-`KeyUser` `SigningCondition` grants. Its sign-time ACL checks those grants **before** the `Token.revokedAt` filter — so revoking the *token* is a silent no-op and the spire keeps signing. Only `KeyUser.revokedAt` (via `revoke_user`/`revoke_key_user`) actually cuts it off. Verified live against `nsecbunkerd@dev`. A unit test asserts revoke routes to `revoke_key_user` and **never** token-revoke. ## Tests 7 new (210 green): duration threading + default-None; `revoke_spire` → `revoke_key_user` (+ never token-revoke) + bunker-error mapping; endpoint wiring (revoke happy / zero-bound / 502). New code black + ruff clean. ## Notes - Same lnbits-version constraint as #21: requires lnbits ≥ the #55 merge (`b5fba561`) for `revoke_key_user` + `create_new_token(duration_hours=…)`. - Deferred from here (matches #21's remaining): Fleet "Pair/Revoke Spire" UI, and `aiolabs/bitspire#52` (spire-side consumer). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(pairing): optional token TTL + revoke endpoint (#9/#12, #22)
Some checks failed
ci.yml / feat(pairing): optional token TTL + revoke endpoint (#9/#12, #22) (pull_request) Failing after 0s
a5efdf22a1
Builds on the seed-URL pairing in #21 (stacked).

(b) TTL — PairMachineData.duration_hours (validated > 0) threads through
    pair_spire -> create_new_token (lnbits#55). None = non-expiring.

(c) Revoke — POST /machines/{id}/revoke -> revoke_spire ->
    admin_client.revoke_key_user(spire-<id>). Per spirekeeper#22, revoke
    MUST go through KeyUser.revokedAt (revoke_key_user), NOT token revoke:
    lnbits eager-binds (redeems) the connect token at provision, so
    nsecbunkerd has materialised the policy into per-KeyUser grants its
    ACL checks BEFORE the Token.revokedAt filter -> token revoke is a
    silent no-op. Returns RevokeResult{revoked_count}: >=1 = cut, 0 =
    never bound. set_machine_unpaired clears paired_at (keeps npub +
    bunker_spire_key_name for audit / re-pair).

7 new tests (duration threading + default-None; revoke routes to
revoke_key_user and never token-revoke + error mapping; endpoint wiring
revoke happy/zero/502). 210 green; new code black/ruff-clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
padreug changed target branch from feat/seed-url-pairing to main 2026-06-18 17:03:35 +00:00
Merge branch 'main' into feat/pairing-revoke-ttl
Some checks failed
ci.yml / Merge branch 'main' into feat/pairing-revoke-ttl (pull_request) Failing after 0s
4db5c3de4e
padreug deleted branch feat/pairing-revoke-ttl 2026-06-18 17:09:02 +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
aiolabs/spirekeeper!23
No description provided.