feat(pairing): slim the spire seed + carry lnbits_npub (bitspire-#70) #37

Merged
padreug merged 7 commits from bitspire-70-seed-lnbits-npub into main 2026-07-02 17:45:02 +00:00

7 commits

Author SHA1 Message Date
2b90590104 fix(pairing): retry all bunker admin RPCs past transient timeouts (#38)
Some checks failed
ci.yml / fix(pairing): retry all bunker admin RPCs past transient timeouts (#38) (pull_request) Failing after 0s
The get_key_tokens retry only covered an empty token list; a transient
NsecBunkerTimeoutError on any admin RPC still failed pairing with a 502 (seen
live on aio-demo: create_new_key and get_key_tokens both 15s-timed-out, then a
manual retry succeeded). Generalise to `_bunker_retry`, wrapping every admin
call in the pair_spire chain (create_new_key, ensure_policy, create_new_token,
get_key_tokens): a NsecBunkerTimeoutError (and, for get_key_tokens, an empty
list) is transient → retry with backoff; a NsecBunkerRpcError rejection or
misconfig is terminal → fail fast. create_new_key is replace-by-name and
ensure_policy reconciles idempotently, so retrying on timeout is safe.

Also: _validate_relay now rejects a host-less ws:// (mint was laxer than the
consumer's parseSpireSeed), and stale "nostrclient" comments in the pair UI +
a test are corrected to "transport relay".

229 tests pass (incl. timeout-retry + fail-fast-rejection coverage).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 15:30:55 +02:00
0bb9939822 fix(pairing): default relay to the transport's nostrrelay, not nostrclient proxy
Some checks failed
ci.yml / fix(pairing): default relay to the transport's nostrrelay, not nostrclient proxy (pull_request) Failing after 0s
The nostrclient endpoint is a subscription MULTIPLEXER, not a full relay: its
router forwards a client's EVENT upstream but never returns an OK ack (see
nostrclient/router.py). A transport client that awaits OK on publish therefore
times out ("publish timed out"), so kind-21000 RPCs never complete — verified
on the Sintra: connect succeeded but list_wallets hung, and switching to the
nostrrelay endpoint made the whole flow work (wallet, balance, availability).

default_relay_endpoint now derives from settings.nostr_transport_relays — the
relay the transport actually listens on: use it as-is when already
machine-reachable, or re-home its path on lnbits_baseurl when it's a co-located
loopback relay (the bundled nostrrelay). Validation/localhost-reject and the
pair-dialog pre-fill/hint carry over; wording updated to "transport relay".

227 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 00:43:54 +02:00
765b07737b fix(pairing): retry get_key_tokens past a transient empty result
Some checks failed
ci.yml / fix(pairing): retry get_key_tokens past a transient empty result (pull_request) Failing after 0s
pair_spire failed with "bunker returned no tokens after create_new_token" when
the nsecbunkerd was briefly slow — get_key_tokens listed nothing in the few ms
after create_new_token before the write landed. Observed live: a nip44_decrypt
timeout, then a 502 on pair, then the identical call 25s later succeeding.

Retry get_key_tokens up to 5x with a 0.4s backoff before giving up, so pairing
survives a sluggish bunker instead of flaking. Tests cover the transient-empty
recovery and the exhausted-attempts failure.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 00:29:08 +02:00
4e36b98534 feat(pairing-ui): pre-fill the pair dialog relay with the default endpoint
Some checks failed
ci.yml / feat(pairing-ui): pre-fill the pair dialog relay with the default endpoint (pull_request) Failing after 0s
The backend now defaults an omitted relay to the nostrclient proxy endpoint,
but the pair dialog still showed an empty, client-side-required field — so the
operator saw "no default". Wire it through:

- GET /api/v1/dca/default-relay returns default_relay_endpoint() (the derived
  ws(s)://<host>/nostrclient/api/v1/relay).
- openPairDialog pre-fills the relay textarea with it; the operator can override
  or clear it (blank → same server-side default). Drops the client-side
  "at least one relay is required" guard.
- Hint updated to explain the default.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 00:16:46 +02:00
c3791ed6c8 feat(pairing): default seed relay to the nostrclient endpoint + validate relays
Some checks failed
ci.yml / feat(pairing): default seed relay to the nostrclient endpoint + validate relays (pull_request) Failing after 0s
Two robustness fixes for on-machine pairing (bitspire-#70), after a QR scan
silently corrupted a seed's relay (ws://→As://) and crash-looped a machine on
an unreachable relay:

- Default relays: when the operator omits `relays`, derive the seed's relay from
  THIS lnbits' own nostrclient proxy endpoint — `<ws(s)>://<host>/nostrclient/api/v1/relay`,
  built from `lnbits_baseurl` (default_relay_endpoint). Operators configure
  upstream relays once in the nostrclient extension (public_ws must be on) and
  every seed points at one stable, operator-independent URL. `relays` is now
  optional on PairMachineData + the /pair endpoint.
- Validate every relay (+ bunker_relay) is a `ws://`/`wss://` URL AND reject
  loopback hosts (localhost/127.0.0.1/::1/0.0.0.0) — a seed is redeemed by a
  REMOTE machine, so a localhost relay is exactly the unreachable case. Catches
  both the ws://→As:// corruption and the localhost /pair gotcha at mint time.

Consumer side (bitspire): parseSpireSeed rejects non-ws relays, and the wizard
gained a "test relay" reachability button before committing.

223 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 00:00:13 +02:00
9dc4d09973 feat(pairing): slim the spire seed + carry lnbits_npub (bitspire-#70)
Some checks failed
ci.yml / feat(pairing): slim the spire seed + carry lnbits_npub (bitspire-#70) (pull_request) Failing after 0s
Mint the new-shape seed the bitspire consumer now expects: the pubkey rides
once as spire_npub (consumer derives the hex + reconstructs bunker_url from
bunker_secret + bunker_relay|relays[0]), and lnbits_npub is embedded so a paired
machine reaches this lnbits' nostr-transport with nothing else provisioned.

- build_seed_url emits {spire_npub, lnbits_npub, bunker_secret, relays} and
  bunker_relay only when it differs from relays[0] (omitted in the common case).
  Drops spire_pubkey + the full bunker_url from the payload.
- pair_spire reads settings.nostr_transport_public_key, hex_to_npub's it, and
  raises PairingError when it's empty (transport not running → can't mint a
  self-sufficient seed). bunker_url is still returned in PairResult for operator
  display / audit; only the seed stops embedding it.

Consumer side: bitspire packages/nostr-client/src/seed.ts + the #70 machine
wiring. Kept as v: 1 (redefined in place; no shipped seed to preserve).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 21:16:10 +02:00
b93e3fb698 test(pair-endpoint): update stale pair_spire double + mock get_super_config
The pairing endpoint tests' fake_pair predated the bunker_relay parameter
(views_api passes bunker_relay=data.bunker_relay), so it raised TypeError; and
api_pair_machine later grew a get_super_config() read for the post-pair fee
publish that the doubles never mocked, hitting a DB with no spirekeeper.super_config
table. Both were pre-existing failures on main (masked one behind the other).

Add bunker_relay to the fake_pair signature and mock get_super_config → None
(the happy path doesn't exercise fee publishing). Suite green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 21:16:10 +02:00