From 786789f5170cd1e8c3523498afa499bb14cd01f8 Mon Sep 17 00:00:00 2001 From: Padreug Date: Wed, 1 Jul 2026 13:16:16 +0200 Subject: [PATCH] fix(machine): resume from binding when a stored spire seed won't parse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit resolveSigner parses the stored VITE_SPIRE_SEED on every boot before it checks the binding, so a machine whose .env still holds a legacy-shape seed would throw on the new parser (bitspire-#70) and surface "ATM Unavailable" on the next auto-pull — even though it has a perfectly good, server-persistent binding to resume from. Guard the parse: an unparseable stored seed with a binding present falls back to resuming the binding (authoritative); with no binding it still fails closed, since the seed is then the only pairing input. Also dedupes the three resume-from-binding call sites behind a small local. Co-Authored-By: Claude Opus 4.8 --- apps/machine/src/services/signer-resolver.ts | 42 ++++++++++++++------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/apps/machine/src/services/signer-resolver.ts b/apps/machine/src/services/signer-resolver.ts index 44aa4b5..65f1647 100644 --- a/apps/machine/src/services/signer-resolver.ts +++ b/apps/machine/src/services/signer-resolver.ts @@ -26,6 +26,7 @@ import { parseSpireSeed, seedFingerprint, type Signer, + type SpireSeed, } from '@bitSpire/nostr-client' import type { BunkerBindingRecord } from '@/types/electron' @@ -67,17 +68,38 @@ async function loadPairingState(): Promise { export async function resolveSigner(opts: ResolveSignerOptions): Promise { const { spireSeed, binding } = await loadPairingState() + const resume = (b: BunkerBindingRecord): Promise => + resumeFromBinding({ + clientSecretHex: b.clientSecretHex, + spirePubkey: b.spirePubkey, + bunkerUrl: b.bunkerUrl, + }) + if (spireSeed) { - const seed = parseSpireSeed(spireSeed) - const fingerprint = seedFingerprint(spireSeed) + let seed: SpireSeed + let fingerprint: string + try { + seed = parseSpireSeed(spireSeed) + fingerprint = seedFingerprint(spireSeed) + } catch (err) { + // A stored seed we can't parse — e.g. a legacy-shape seed left in .env + // after the seed format changed (bitspire-#70). If we already hold a + // binding it's authoritative (server-persistent), so resume from it + // rather than bricking a paired machine on the next boot. With no + // binding the seed is our only pairing input, so fail closed. + if (binding) { + console.warn( + '[Signer] Stored spire seed is unparseable; resuming from existing binding:', + (err as Error).message, + ) + return resume(binding) + } + throw err + } if (binding && binding.seedFingerprint === fingerprint) { console.log('[Signer] Resuming bunker session for spire', seed.spirePubkey) - return resumeFromBinding({ - clientSecretHex: binding.clientSecretHex, - spirePubkey: binding.spirePubkey, - bunkerUrl: binding.bunkerUrl, - }) + return resume(binding) } // First pair or re-pair: redeem the one-shot connect secret. @@ -105,11 +127,7 @@ export async function resolveSigner(opts: ResolveSignerOptions): Promise // No seed in this boot but a binding survives → resume. if (binding) { console.log('[Signer] Resuming bunker session from stored binding (no seed this boot)') - return resumeFromBinding({ - clientSecretHex: binding.clientSecretHex, - spirePubkey: binding.spirePubkey, - bunkerUrl: binding.bunkerUrl, - }) + return resume(binding) } if (opts.allowEphemeral) {