feat(pairing): seed-URL pairing — operator-side producer (S0 / #9) #21

Merged
padreug merged 4 commits from feat/seed-url-pairing into main 2026-06-18 17:03:34 +00:00

4 commits

Author SHA1 Message Date
9c5f07c72e refactor(pairing): use lnbits' public ensure_policy, drop fork duplicate (#9)
Some checks failed
ci.yml / refactor(pairing): use lnbits' public ensure_policy, drop fork duplicate (#9) (pull_request) Failing after 0s
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>
2026-06-18 18:31:21 +02:00
761f078053 feat(pairing): POST /machines/{id}/pair endpoint (#9)
Some checks failed
ci.yml / feat(pairing): POST /machines/{id}/pair endpoint (#9) (pull_request) Failing after 0s
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>
2026-06-16 23:39:18 +02:00
a77f5bcb5c feat(pairing): bunker pairing service — mint per-spire key + seed URL (#9)
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>
2026-06-16 23:17:48 +02:00
bb473f5385 feat(pairing): m010 schema — bunker pairing columns on dca_machines
Schema checkpoint for seed-URL pairing (S0 / #9; spire-side bitspire#52),
model A1 — the spire's signing key lives in the operator's nsecbunkerd,
not on the spire's disk.

dca_machines gains:
  - bunker_spire_key_name — the spire's key name in the bunker
    (spire-<machine_id>); used to re-issue connect tokens on re-pair.
  - paired_at — last successful pair; NULL = never paired.

Both nullable, idempotent column-probe add (m009 pattern). Machine model
gains the matching optional fields. Validated on the regtest dev db
(columns present, migrations clean); 191 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 22:47:42 +02:00