feat(pairing): seed-URL pairing — operator-side producer (S0 / #9) #21
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/seed-url-pairing"
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?
Operator-side producer for seed-URL pairing (S0 / #9), model A1: the spire's signing key lives inside the operator's nsecbunkerd, never on the spire's disk. The spire self-signs all its events as that bunker-held key; lnbits' path-B roster maps the npub → operator wallet.
Design note (a contradiction resolved)
While building this I found and corrected a contradiction in the plan: #9's original pseudocode had the ATM signing kind-21000 as the operator via the bunker, which conflicts with the shipped path-B roster (routes by the kind-21000 sender = the spire's own npub). The corrected, shipped-aligned model: the spire self-signs with its own (bunker-held) key; the bunker holds the key so no nsec sits on the device. See the #9 correction comment (2026-06-16). Also confirmed
aiolabs/lnbits#18's scoped-token API is already shipped — this work is unblocked.What's here (3 commits)
dca_machines+=bunker_spire_key_name,paired_at(idempotent, m009 pattern).pairing.py) —pair_spire()orchestrates the nsecbunkerd admin chain (create_new_key(spire-<id>)→ scopedspirekeeper-spirepolicy →create_new_token→get_key_tokens) and builds abunker://+ base64urlspire-seed:v1:URL{spire_npub, spire_pubkey, bunker_url, relays}. spirekeeper does steps 1–4; the spire does the NIP-46 connect/bind at first boot (bitspire#52). A local_ensure_spire_policyavoids lnbits' policy-name-blind_ensure_policycache.POST /machines/{id}/pair) — ownership guard, bunker-error mapping (503 / 502), post-mint collision guard on the minted hex, persist viaset_machine_pairing. Re-pair supported.Tests
203 green (12 new: 9 pairing-service with a fake bunker; 3 endpoint wiring). npub↔hex through lnbits' real helpers. New code is black/ruff-clean — pre-existing lint debt in
crud.py/views_api.pyleft untouched (avoided black version-drift whole-file churn).Reviewer note — lifecycle change
Under A1 the bunker mints the spire identity, so
/pairoverwritesmachine_npubwith the bunker-generated key (re-running the no-collision guard). "Add machine" no longer needs an operator-supplied npub — it's assigned at pair time.Naming
New code uses
spire(the bitSpire machine; "ATM" is the legacy Lamassu term); bunker keys arespire-<machine_id>. Cross-repoatmterms (bitspire.env, lnbits roster) left for a later sweep; the Nostr wire is unaffected.Follow-ups (not in this PR)
bootstrapPublishedAton a new seed (folds in the cassette-bootstrap-gate fix, #56).🤖 Generated with Claude Code
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>Wires the pairing service into the operator API. api_pair_machine: - _machine_owned_by ownership guard (404 on miss) - opens NsecBunkerAdminClient.from_settings() and runs pair_spire - maps bunker failures: not-configured -> 503, PairingError/NsecBunkerError -> 502 (nothing persisted on failure) - runs _assert_no_pubkey_collision on the bunker-minted hex, then set_machine_pairing persists machine_npub (= minted spire identity, so path-B roster routes it), bunker_spire_key_name, paired_at. Re-pair supported; revoke/expiry gated on aiolabs/lnbits#54. Adds Create... PairMachineData {relays} body, set_machine_pairing CRUD, and 3 endpoint wiring tests (persist+collision, empty-relays 400, failure 502). 203 tests green. Pre-existing black/ruff debt in crud/views_api left untouched (version-drift churn avoided); new code is lint-clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>