Phase C: resolve signer from spire seed / bunker binding at bootstrap (#52) #60

Merged
padreug merged 5 commits from phase-c-bunker-bootstrap into dev 2026-06-21 10:35:56 +00:00
Owner

Phase C of the NIP-46 bunker migration (#52) — the bootstrap cutover. The ATM stops loading a local signing nsec and instead resolves its signer from the spire pairing (seed → bunker, or resume from the persisted binding). Behavioural change, so it's a PR against dev rather than direct commits (the Sintra dev unit auto-pulls dev).

Phases A + B (the Signer seam, the v1 retirement, the BunkerSigner/seed parser, state.db v11) already merged to dev; this PR is only the bootstrap wiring on top. ~306/-121 across 13 files.

Suggested review order (smallest blast radius → largest)

  1. packages/nostr-client/src/bunker-signer.ts (8 lines) — just a docstring correction (TTL is now a post-bind deauth cause per nsecbunkerd#27). Skim.
  2. apps/machine/electron/state-store.ts (+10) — one new resetBootstrapGate() helper. The bunker_binding table + accessors already landed in Phase B.
  3. apps/machine/electron/{main,preload}.ts + src/types/electron.d.ts — the IPC bridge: get-atm-secrets now returns { spireSeed, bunkerBinding } instead of the raw nsec; three new handlers (save/clear binding, reset gate). Mechanical; the three declarations must stay in lockstep.
  4. apps/machine/src/services/signer-resolver.ts (NEW, 116 lines) — the heart of the PR. The pair/resume/dev-fallback decision. This is where to spend your attention. Key things to sanity-check:
    • pairing only happens when seedFingerprint(seed) !== binding.seedFingerprint (so a stable seed resumes, doesn't re-redeem);
    • on (re)pair we persist the binding and resetBootstrapGate() (folds in #56);
    • strict/prod throws when there's no seed and no binding; dev falls back to an ephemeral/LocalSigner.
  5. apps/machine/src/services/lightning.ts (-56/+, net simpler) — drops all atmPrivateKey plumbing, calls resolveSigner. The Phase-A seam means the swap touches exactly one point. Verify the strict-mode block and that CONFIG no longer carries a secret.
  6. packages/clink/src/client.ts (~91 changed) — CLINK migrated from MachineIdentity to the async Signer (sign/nip44). Largest single-file diff but mechanical, same pattern as Phase A. Migrated fully (not stubbed) because CLINK is returning soon for ndebit/k1. Live path is kind-21003 management; offer/debit are dormant-but-bunker-ready.
  7. apps/machine/src/App.vue (+13) — maintenance beacon resolves the same way (best-effort).
  8. fund-atm.ts + .env.example + CLAUDE.md + package.json — fund-atm resumes from the binding (better-sqlite3 external in the bundle); docs for VITE_SPIRE_SEED. fund-atm is slated for deprecation.

What CI / local checks cover

  • pnpm typecheck 12/12, pnpm test 104 passing, full electron prod build (vite + electron tsc + fund-atm bundle) clean.
  • Not covered: a live bunker round-trip. connectNewSeed/resumeFromBinding are unit-tested against a fake inner client only; the real nsecbunkerd handshake is Phase F. So this PR is safe to read/merge-to-dev as the staging step, but the live pairing gets vetted on Sintra before anything reaches main.

Open coordination (not blocking this PR)

  • spirekeeper TTL-policy question (immortal ATM tokens vs. exposing expiresAt in the seed) — posted on #52, awaiting their call.

Do NOT use the MCP merge endpoint — merge via the Forgejo UI after review (per our convention).

🤖 Generated with Claude Code

Phase C of the NIP-46 bunker migration (#52) — the bootstrap cutover. The ATM stops loading a local signing nsec and instead resolves its signer from the spire pairing (seed → bunker, or resume from the persisted binding). **Behavioural change**, so it's a PR against `dev` rather than direct commits (the Sintra dev unit auto-pulls `dev`). Phases A + B (the `Signer` seam, the v1 retirement, the `BunkerSigner`/seed parser, state.db v11) already merged to `dev`; this PR is only the bootstrap wiring on top. ~306/-121 across 13 files. ### Suggested review order (smallest blast radius → largest) 1. **`packages/nostr-client/src/bunker-signer.ts`** (8 lines) — just a docstring correction (TTL is now a post-bind deauth cause per nsecbunkerd#27). Skim. 2. **`apps/machine/electron/state-store.ts`** (+10) — one new `resetBootstrapGate()` helper. The `bunker_binding` table + accessors already landed in Phase B. 3. **`apps/machine/electron/{main,preload}.ts` + `src/types/electron.d.ts`** — the IPC bridge: `get-atm-secrets` now returns `{ spireSeed, bunkerBinding }` instead of the raw nsec; three new handlers (save/clear binding, reset gate). Mechanical; the three declarations must stay in lockstep. 4. **`apps/machine/src/services/signer-resolver.ts`** (NEW, 116 lines) — **the heart of the PR.** The pair/resume/dev-fallback decision. This is where to spend your attention. Key things to sanity-check: - pairing only happens when `seedFingerprint(seed) !== binding.seedFingerprint` (so a stable seed resumes, doesn't re-redeem); - on (re)pair we persist the binding *and* `resetBootstrapGate()` (folds in #56); - strict/prod throws when there's no seed and no binding; dev falls back to an ephemeral/LocalSigner. 5. **`apps/machine/src/services/lightning.ts`** (-56/+, net simpler) — drops all `atmPrivateKey` plumbing, calls `resolveSigner`. The Phase-A seam means the swap touches exactly one point. Verify the strict-mode block and that `CONFIG` no longer carries a secret. 6. **`packages/clink/src/client.ts`** (~91 changed) — CLINK migrated from `MachineIdentity` to the async `Signer` (sign/nip44). Largest single-file diff but mechanical, same pattern as Phase A. Migrated fully (not stubbed) because CLINK is returning soon for ndebit/k1. Live path is kind-21003 management; offer/debit are dormant-but-bunker-ready. 7. **`apps/machine/src/App.vue`** (+13) — maintenance beacon resolves the same way (best-effort). 8. **`fund-atm.ts` + `.env.example` + `CLAUDE.md` + `package.json`** — fund-atm resumes from the binding (`better-sqlite3` external in the bundle); docs for `VITE_SPIRE_SEED`. fund-atm is slated for deprecation. ### What CI / local checks cover - `pnpm typecheck` 12/12, `pnpm test` 104 passing, full electron prod build (vite + electron tsc + fund-atm bundle) clean. - **Not covered:** a live bunker round-trip. `connectNewSeed`/`resumeFromBinding` are unit-tested against a fake inner client only; the real nsecbunkerd handshake is Phase F. So this PR is safe to read/merge-to-`dev` as the staging step, but the live pairing gets vetted on Sintra before anything reaches `main`. ### Open coordination (not blocking this PR) - spirekeeper TTL-policy question (immortal ATM tokens vs. exposing `expiresAt` in the seed) — posted on #52, awaiting their call. Do NOT use the MCP merge endpoint — merge via the Forgejo UI after review (per our convention). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Swap CLINKClient's MachineIdentity for the Signer abstraction: sign_event /
nip44 now go through the signer (async), so the spire identity can live in a
NIP-46 bunker. The kind-21003 management path (operator-driven manual
dispense, the one live CLINK path on dev) decrypts as the spire via the
bunker; the dormant offer/debit paths are migrated too so they're
bunker-ready when CLINK is re-implemented for the upcoming ndebit/k1 spec
(shocknet/CLINK#7, #8).

Part of Phase C, aiolabs/bitspire#52.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
get-atm-secrets now returns { spireSeed, bunkerBinding } instead of the raw
nsec (one-shot semantics kept). Adds IPC handlers + preload bindings for
saveBunkerBinding / clearBunkerBinding / resetBootstrapGate so the renderer
can persist a pairing and re-arm the cassette-state hello on re-pair (#56).
resetBootstrapGate added to state-store. Types mirrored in electron.d.ts.

Part of Phase C, aiolabs/bitspire#52.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New signer-resolver.ts turns the ATM's pairing state into a Signer:
 - seed present, fingerprint differs from stored binding → pair: generate a
   transport key, redeem the one-shot connect secret, persist the binding,
   reset the bootstrap gate (re-publish hello to the new operator, #56);
 - seed matches binding, or binding-only → resume (no re-redeem);
 - neither → ephemeral LocalSigner (dev) or throw (strict/prod).

lightning.ts drops the atmPrivateKey plumbing and calls resolveSigner; the
Phase-A Signer seam means nothing downstream changes. App.vue's maintenance
beacon resolves the same way (best-effort, skips if unpaired).

Part of Phase C, aiolabs/bitspire#52.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fund-atm resolves its signer by resuming the bunker binding from state.db
(the connect token is already spent by the main app, so it can't re-pair);
falls back to a dev nsec via VITE_ATM_PRIVATE_KEY. better-sqlite3 marked
external in the esbuild bundle. .env.example + CLAUDE.md document
VITE_SPIRE_SEED as the prod identity, VITE_ATM_PRIVATE_KEY as dev-only.

(fund-atm is slated for deprecation in favour of the operator funding the
wallet directly via the LNbits UI — kept working for now.)

Part of Phase C, aiolabs/bitspire#52.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
nsecbunkerd#27 enforces token lifecycle at sign time (Option D): an expired
token (`expiresAt`) now stops signing post-bind, not just at connect —
reversing the earlier #24 "TTL is connect-window-only" note. A lapsed TTL
now surfaces as the same BunkerRejectedError as a revoke, so the Phase D
re-pair handling covers both. Docstring corrected to say so.

refs nsecbunkerd#27/#24/#25, aiolabs/bitspire#52

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
padreug deleted branch phase-c-bunker-bootstrap 2026-06-21 10:35:57 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/bitspire!60
No description provided.