The seed minted via the Pair UI baked an unreachable bunker relay into
bunker_url. The UI form has no bunker_relay field, so pair_spire fell back to
its default `settings.lnbits_nsec_bunker_url` — which on a deployed instance is
the INTERNAL relay lnbits uses to reach the co-located bunker (e.g.
ws://127.0.0.1:5000/nostrrelay/demo). The remote ATM can't reach localhost, so
connectNewSeed hangs -> BunkerTimeoutError "Signer Unreachable". (Flagged by
bitspire on the demo; the localhost-relay /pair gotcha the coord thread called out.)
Default bunker_relay to the spire's own public event relay (relays[0]) instead:
the bunker lives on the same operator nostrrelay the spire publishes its events
to, so that URL is machine-reachable. An explicit `bunker_relay` still overrides
for split-relay deploys. An empty override now falls back to the same default
rather than raising.
Regression test: with no (or empty) bunker_relay, bunker_url embeds relays[0]
and contains no 127.0.0.1.
NOTE: relays[0] is a pragmatic default; whether the seed should carry multiple
relays / be sourced from the operator's nostrclient relay is a follow-up.
nsecbunkerd#27 (deployed 2026-06-19) reverses the #24 finding: the
sign-time ACL now evaluates token lifecycle live on every request
(checkIfPubkeyAllowed step 4 joins through a liveWhere filter;
applyToken stopped photocopying grants into SigningConditions). So:
- duration_hours / token expiresAt now bounds an ESTABLISHED binding —
an expired token stops signing post-bind, not just at connect. The
prior docstring (connect-window-only, pointing at the now-closed
nsecbunkerd#24) is corrected.
- Token-revoke is no longer a post-redeem no-op (closes the #22
mechanism bunker-side). revoke_spire keeps using revoke_key_user
because that's the subject-level ban cutting the whole binding, not
just one token's grant — rationale updated, behavior unchanged.
Doc/comment only; 20 pairing tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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 <noreply@anthropic.com>
bitspire#52 consumer review (2026-06-18) enumerated the kinds the spire
signs as its OWN identity and found NIP-42 relay AUTH (kind 22242) missing
from SPIRE_POLICY_RULES — a silent bunker reject the moment a relay
challenges with AUTH. It must be bunker-signed (AUTH proves control of
spire_pubkey, which only the bunker holds; can't use the local client_nsec).
Adds 22242. Records the confirmed set in the policy comment: live = 21000 +
30078 + 22242; CLINK 21001-21003 dormant but kept; nip04 unused (v1 path is
dead code). New test locks the required-kinds contract so 22242 can't
silently regress.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Adopts aiolabs/lnbits#55 (merged b5fba561): pair_spire now calls the
public ensure_policy(client, name='spirekeeper-spire', rules=...,
methods_no_kind=...) instead of spirekeeper's cache-free
_ensure_spire_policy copy. #55 re-keyed _POLICY_ID_CACHE on
(admin_pubkey, policy_name), so the shared helper no longer returns the
wrong (lnbits-default) id for a non-default policy name — the exact
reason the duplicate existed. Net -45 LOC, one less fork-divergent
reimplementation to keep in sync.
Requires lnbits >= the #55 merge (ensure_policy importable) — already
true on dev/demo.
Tests: FakeBunker gains admin_pubkey; an autouse fixture clears lnbits'
_POLICY_ID_CACHE between tests (the shared helper caches, unlike the old
local one). 203 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Operator-side producer for seed-URL pairing (S0/#9, model A1). pair_spire()
orchestrates the nsecbunkerd admin chain via lnbits' NsecBunkerAdminClient:
create_new_key(spire-<id>) -> _ensure_spire_policy -> create_new_token ->
get_key_tokens -> package the <npub>#secret token into a bunker:// URL +
base64url seed URL {spire_npub, spire_pubkey, bunker_url, relays}.
The spire later self-signs all its events as that bunker-held key; lnbits'
path-B roster maps the npub to the operator wallet — no nsec on the spire.
spirekeeper does steps 1-4 only; the NIP-46 connect/bind happens spire-side
(bitspire#52) with the spire's own client keypair.
Scoped policy 'spirekeeper-spire': sign_event 21000/21001-3/30078 + nip44
(kind-less via add_policy_rule). Local _ensure_spire_policy (no cache)
avoids lnbits' admin-pubkey-keyed _ensure_policy cache (policy-name-blind).
9 unit tests with a fake bunker (orchestration, policy reconcile, seed/
bunker:// wire shape, error paths); npub<->hex via lnbits' real helpers.
200 tests green.
Known gaps (lnbits NsecBunkerAdminClient): no token-expiry param, no revoke
RPC — re-pair works; 'revoke spire access' deferred to a bunker follow-up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>