From 554b2e2e1714467a987d901c4362fd4e78713f3c Mon Sep 17 00:00:00 2001 From: Padreug Date: Fri, 19 Jun 2026 00:59:29 +0200 Subject: [PATCH] docs(pairing): correct duration_hours TTL docstring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit duration_hours stamps Token.expiresAt, but nsecbunkerd reads expiresAt only in validateToken at connect/redeem time — the sign-time ACL never checks it (materialised SigningConditions carry no expiry; the policy join filters revokedAt only). So TTL bounds only the un-redeemed connect window, not an established binding; revoke_key_user is the real post-bind cutoff. Same ACL-ordering class as the revoke finding (#22). Tracked at aiolabs/nsecbunkerd#24. Co-Authored-By: Claude Opus 4.8 --- pairing.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pairing.py b/pairing.py index 8629de9..ed5684d 100644 --- a/pairing.py +++ b/pairing.py @@ -165,10 +165,15 @@ async def pair_spire( """Mint a bunker-held key + scoped connect token for `machine` and return the seed URL the spire redeems at first boot. - `duration_hours` (optional, aiolabs/lnbits#54 item 2) sets a TTL on the - spire's connect token — the bunker stamps `expiresAt` and rejects the - token once it lapses, forcing a re-pair. None = non-expiring (the only - invalidation path is then revoke, `revoke_spire`). + `duration_hours` (optional, aiolabs/lnbits#54 item 2) stamps `expiresAt` + on the spire's connect token. NOTE: this bounds ONLY the window in which + an *un-redeemed* token can first connect — nsecbunkerd reads `expiresAt` + solely in `validateToken` at redeem time. Once the spire has connected + and its per-KeyUser grants are materialized, an expired token keeps + signing (the sign-time ACL never checks `expiresAt`; same ACL-ordering + subtlety as the revoke finding, #22). The real post-bind cutoff is + `revoke_spire` (`revoke_key_user`), not TTL. Post-bind TTL enforcement is + tracked at aiolabs/nsecbunkerd#24. None = non-expiring connect window. `admin_client` must already be connected (the caller owns the `async with NsecBunkerAdminClient.from_settings()` context) — keeps