fix(machine): resume from binding when a stored spire seed won't parse

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 <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-07-01 13:16:16 +02:00 • committed by padreug
commit 786789f517

View file

@ -26,6 +26,7 @@ import {
parseSpireSeed, parseSpireSeed,
seedFingerprint, seedFingerprint,
type Signer, type Signer,
type SpireSeed,
} from '@bitSpire/nostr-client' } from '@bitSpire/nostr-client'
import type { BunkerBindingRecord } from '@/types/electron' import type { BunkerBindingRecord } from '@/types/electron'
@ -67,17 +68,38 @@ async function loadPairingState(): Promise<PairingState> {
export async function resolveSigner(opts: ResolveSignerOptions): Promise<Signer> { export async function resolveSigner(opts: ResolveSignerOptions): Promise<Signer> {
const { spireSeed, binding } = await loadPairingState() const { spireSeed, binding } = await loadPairingState()
const resume = (b: BunkerBindingRecord): Promise<Signer> =>
resumeFromBinding({
clientSecretHex: b.clientSecretHex,
spirePubkey: b.spirePubkey,
bunkerUrl: b.bunkerUrl,
})
if (spireSeed) { if (spireSeed) {
const seed = parseSpireSeed(spireSeed) let seed: SpireSeed
const fingerprint = seedFingerprint(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) { if (binding && binding.seedFingerprint === fingerprint) {
console.log('[Signer] Resuming bunker session for spire', seed.spirePubkey) console.log('[Signer] Resuming bunker session for spire', seed.spirePubkey)
return resumeFromBinding({ return resume(binding)
clientSecretHex: binding.clientSecretHex,
spirePubkey: binding.spirePubkey,
bunkerUrl: binding.bunkerUrl,
})
} }
// First pair or re-pair: redeem the one-shot connect secret. // First pair or re-pair: redeem the one-shot connect secret.
@ -105,11 +127,7 @@ export async function resolveSigner(opts: ResolveSignerOptions): Promise<Signer>
// No seed in this boot but a binding survives → resume. // No seed in this boot but a binding survives → resume.
if (binding) { if (binding) {
console.log('[Signer] Resuming bunker session from stored binding (no seed this boot)') console.log('[Signer] Resuming bunker session from stored binding (no seed this boot)')
return resumeFromBinding({ return resume(binding)
clientSecretHex: binding.clientSecretHex,
spirePubkey: binding.spirePubkey,
bunkerUrl: binding.bunkerUrl,
})
} }
if (opts.allowEphemeral) { if (opts.allowEphemeral) {