feat(machine): persist scanned spire-seed + signal unpaired state for wizard
Foundation for the on-machine QR-pairing wizard (aiolabs/bitspire#52). An unpaired ATM can now have a seed planted at runtime rather than only via provisioning: - electron IPC `state:save-spire-seed` writes VITE_SPIRE_SEED into the runtime .env (0600), and `app:relaunch` restarts the kiosk so the normal boot path (signer-resolver → connectNewSeed) does the actual bunker pairing. We deliberately do NOT pair in-renderer — persist + relaunch reuses the single, hardware-tested pairing path. - signer-resolver throws a typed `NoPairingError` (distinct `.name`, survives the bundle boundary) when there's no seed and no binding, instead of a generic Error. - init-error maps NoPairingError → `unpaired`, so the renderer can route a fresh machine to the interactive wizard (next commit) rather than a dead-end fault screen. Revoked/TTL bindings already map there too — re-pair is the same scan-a-fresh-seed flow. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
14d62e4c34
commit
9935807f8c
5 changed files with 72 additions and 5 deletions
|
|
@ -3,14 +3,17 @@
|
|||
* (see App.vue's MAINTENANCE_SCREENS).
|
||||
*
|
||||
* Bunker failures (aiolabs/bitspire#52) get dedicated screens:
|
||||
* - `NoPairingError` (fresh machine, never paired) → `unpaired` — render the
|
||||
* interactive QR-pairing wizard so the operator can scan a spire-seed.
|
||||
* - `BunkerRejectedError` (revoked / TTL-expired / off-policy binding) →
|
||||
* `unpaired` — the operator must re-pair the machine.
|
||||
* `unpaired` too — re-pairing is the same scan-a-fresh-seed flow.
|
||||
* - `BunkerTimeoutError` (signer/relay unreachable) → `signer-unreachable`,
|
||||
* a transient condition.
|
||||
* Everything else surfaces its raw message (or the caller's fallback).
|
||||
*/
|
||||
export function classifyInitError(error: unknown, fallback = 'Initialization failed'): string {
|
||||
const name = (error as { name?: string } | null)?.name
|
||||
if (name === 'NoPairingError') return 'unpaired'
|
||||
if (name === 'BunkerRejectedError') return 'unpaired'
|
||||
if (name === 'BunkerTimeoutError') return 'signer-unreachable'
|
||||
return error instanceof Error ? error.message : fallback
|
||||
|
|
|
|||
|
|
@ -31,6 +31,20 @@ import type { BunkerBindingRecord } from '@/types/electron'
|
|||
|
||||
const isElectron = typeof window !== 'undefined' && window.electronAPI !== undefined
|
||||
|
||||
/**
|
||||
* Thrown in strict mode when the machine has no seed and no binding — it is
|
||||
* genuinely unpaired, not misconfigured. The renderer catches this to show the
|
||||
* QR-pairing wizard (camera scan of a spire-seed) rather than a fault screen.
|
||||
* Distinct `.name` so it survives the bundle boundary (instanceof is fragile
|
||||
* across the electron/renderer split). See services/init-error.ts.
|
||||
*/
|
||||
export class NoPairingError extends Error {
|
||||
override readonly name = 'NoPairingError'
|
||||
constructor() {
|
||||
super('[Signer] Machine is unpaired — no spire seed and no bunker binding.')
|
||||
}
|
||||
}
|
||||
|
||||
export interface ResolveSignerOptions {
|
||||
/** Allow an ephemeral LocalSigner when no seed/binding exists (dev only). */
|
||||
allowEphemeral: boolean
|
||||
|
|
@ -109,8 +123,5 @@ export async function resolveSigner(opts: ResolveSignerOptions): Promise<Signer>
|
|||
return new LocalSigner(generateIdentity())
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'[Signer] No spire seed and no bunker binding — cannot resolve a signing identity (strict mode). ' +
|
||||
'Set VITE_SPIRE_SEED or pair the ATM.'
|
||||
)
|
||||
throw new NoPairingError()
|
||||
}
|
||||
|
|
|
|||
2
apps/machine/src/types/electron.d.ts
vendored
2
apps/machine/src/types/electron.d.ts
vendored
|
|
@ -98,6 +98,8 @@ declare global {
|
|||
saveBunkerBinding: (binding: BunkerBindingRecord) => Promise<void>
|
||||
clearBunkerBinding: () => Promise<void>
|
||||
resetBootstrapGate: () => Promise<void>
|
||||
saveSpireSeed: (seed: string) => Promise<void>
|
||||
relaunchApp: () => Promise<void>
|
||||
applyOperatorCassettesConfig: (
|
||||
payload: { positions: Record<string, { denomination: number; count: number }> },
|
||||
eventCreatedAt: number
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue