feat(machine): seed + bunker-binding IPC bridge
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>
This commit is contained in:
parent
40239aa075
commit
209e4c3e20
4 changed files with 75 additions and 8 deletions
|
|
@ -26,14 +26,19 @@ import {
|
||||||
getLastKnownConfigCreatedAt,
|
getLastKnownConfigCreatedAt,
|
||||||
getBootstrapPublishedAt,
|
getBootstrapPublishedAt,
|
||||||
markBootstrapPublished,
|
markBootstrapPublished,
|
||||||
|
resetBootstrapGate,
|
||||||
applyOperatorCassettesConfig,
|
applyOperatorCassettesConfig,
|
||||||
getFeeConfig,
|
getFeeConfig,
|
||||||
getLastKnownFeeConfigCreatedAt,
|
getLastKnownFeeConfigCreatedAt,
|
||||||
applyFeeConfig,
|
applyFeeConfig,
|
||||||
|
getBunkerBinding,
|
||||||
|
saveBunkerBinding,
|
||||||
|
clearBunkerBinding,
|
||||||
type OperatorCassettesPayload,
|
type OperatorCassettesPayload,
|
||||||
type FeeConfigPayload,
|
type FeeConfigPayload,
|
||||||
type FeeConfigRow,
|
type FeeConfigRow,
|
||||||
type ApplyResult,
|
type ApplyResult,
|
||||||
|
type StoredBunkerBinding,
|
||||||
} from './state-store.js'
|
} from './state-store.js'
|
||||||
import { initializeHal, type HalInstance } from './hal-service.js'
|
import { initializeHal, type HalInstance } from './hal-service.js'
|
||||||
|
|
||||||
|
|
@ -323,14 +328,31 @@ let secretsConsumed = false
|
||||||
ipcMain.handle('get-atm-secrets', () => {
|
ipcMain.handle('get-atm-secrets', () => {
|
||||||
if (secretsConsumed) {
|
if (secretsConsumed) {
|
||||||
console.warn('[Electron] SECURITY: get-atm-secrets called after secrets already consumed')
|
console.warn('[Electron] SECURITY: get-atm-secrets called after secrets already consumed')
|
||||||
return { atmPrivateKey: '' }
|
return { spireSeed: '', bunkerBinding: null }
|
||||||
}
|
}
|
||||||
secretsConsumed = true
|
secretsConsumed = true
|
||||||
|
// The spire pairing seed (one-shot connect token inside) + the persisted
|
||||||
|
// bunker binding (transport key). The renderer resolves these into a
|
||||||
|
// BunkerSigner; see services/signer-resolver.ts (aiolabs/bitspire#52).
|
||||||
return {
|
return {
|
||||||
atmPrivateKey: process.env.VITE_ATM_PRIVATE_KEY || '',
|
spireSeed: process.env.VITE_SPIRE_SEED || '',
|
||||||
|
bunkerBinding: getBunkerBinding(),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Bunker binding persistence — the renderer writes the binding after a
|
||||||
|
// successful pairing (connectNewSeed), and resets the bootstrap gate so the
|
||||||
|
// new operator receives the spire's hello-event (aiolabs/bitspire#52 / #56).
|
||||||
|
ipcMain.handle('state:save-bunker-binding', (_event, binding: StoredBunkerBinding): void => {
|
||||||
|
saveBunkerBinding(binding)
|
||||||
|
})
|
||||||
|
ipcMain.handle('state:clear-bunker-binding', (): void => {
|
||||||
|
clearBunkerBinding()
|
||||||
|
})
|
||||||
|
ipcMain.handle('state:reset-bootstrap-gate', (): void => {
|
||||||
|
resetBootstrapGate()
|
||||||
|
})
|
||||||
|
|
||||||
// State persistence IPC handlers
|
// State persistence IPC handlers
|
||||||
ipcMain.handle('state:load-cassettes', () => loadCassettes())
|
ipcMain.handle('state:load-cassettes', () => loadCassettes())
|
||||||
ipcMain.handle('state:set-cassettes', (_event, cassettes) => setCassettes(cassettes))
|
ipcMain.handle('state:set-cassettes', (_event, cassettes) => setCassettes(cassettes))
|
||||||
|
|
|
||||||
|
|
@ -42,13 +42,25 @@ export interface BrandingConfig {
|
||||||
logoDarkDataUrl: string | null
|
logoDarkDataUrl: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persisted NIP-46 bunker binding (mirror of state-store's StoredBunkerBinding).
|
||||||
|
*/
|
||||||
|
export interface BunkerBindingRecord {
|
||||||
|
clientSecretHex: string
|
||||||
|
spirePubkey: string
|
||||||
|
bunkerUrl: string
|
||||||
|
seedFingerprint: string
|
||||||
|
pairedAt: number
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ATM secrets — returned once by getAtmSecrets(), then empty on subsequent calls.
|
* ATM secrets — returned once by getAtmSecrets(), then empty on subsequent calls.
|
||||||
|
* The spire pairing seed (carries the one-shot connect token) plus the persisted
|
||||||
|
* bunker binding; the renderer resolves these into a signer.
|
||||||
*/
|
*/
|
||||||
export interface AtmSecrets {
|
export interface AtmSecrets {
|
||||||
atmPrivateKey: string
|
spireSeed: string
|
||||||
/** Legacy LP admin token — retained until 3d removes the LP backend. */
|
bunkerBinding: BunkerBindingRecord | null
|
||||||
adminToken?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expose protected methods to renderer
|
// Expose protected methods to renderer
|
||||||
|
|
@ -100,6 +112,13 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
ipcRenderer.invoke('state:get-bootstrap-published-at'),
|
ipcRenderer.invoke('state:get-bootstrap-published-at'),
|
||||||
markBootstrapPublished: (unixTimestamp: number): Promise<void> =>
|
markBootstrapPublished: (unixTimestamp: number): Promise<void> =>
|
||||||
ipcRenderer.invoke('state:mark-bootstrap-published', unixTimestamp),
|
ipcRenderer.invoke('state:mark-bootstrap-published', unixTimestamp),
|
||||||
|
|
||||||
|
// Bunker binding persistence (aiolabs/bitspire#52)
|
||||||
|
saveBunkerBinding: (binding: BunkerBindingRecord): Promise<void> =>
|
||||||
|
ipcRenderer.invoke('state:save-bunker-binding', binding),
|
||||||
|
clearBunkerBinding: (): Promise<void> => ipcRenderer.invoke('state:clear-bunker-binding'),
|
||||||
|
resetBootstrapGate: (): Promise<void> => ipcRenderer.invoke('state:reset-bootstrap-gate'),
|
||||||
|
|
||||||
applyOperatorCassettesConfig: (
|
applyOperatorCassettesConfig: (
|
||||||
payload: {
|
payload: {
|
||||||
positions: Record<string, { denomination: number; count: number }>
|
positions: Record<string, { denomination: number; count: number }>
|
||||||
|
|
@ -212,6 +231,9 @@ declare global {
|
||||||
getLastKnownConfigCreatedAt: () => Promise<number>
|
getLastKnownConfigCreatedAt: () => Promise<number>
|
||||||
getBootstrapPublishedAt: () => Promise<number | null>
|
getBootstrapPublishedAt: () => Promise<number | null>
|
||||||
markBootstrapPublished: (unixTimestamp: number) => Promise<void>
|
markBootstrapPublished: (unixTimestamp: number) => Promise<void>
|
||||||
|
saveBunkerBinding: (binding: BunkerBindingRecord) => Promise<void>
|
||||||
|
clearBunkerBinding: () => Promise<void>
|
||||||
|
resetBootstrapGate: () => Promise<void>
|
||||||
applyOperatorCassettesConfig: (
|
applyOperatorCassettesConfig: (
|
||||||
payload: { positions: Record<string, { denomination: number; count: number }> },
|
payload: { positions: Record<string, { denomination: number; count: number }> },
|
||||||
eventCreatedAt: number
|
eventCreatedAt: number
|
||||||
|
|
|
||||||
|
|
@ -483,6 +483,16 @@ export function clearBunkerBinding(): void {
|
||||||
db.prepare('DELETE FROM bunker_binding WHERE id = 1').run()
|
db.prepare('DELETE FROM bunker_binding WHERE id = 1').run()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the bootstrap-publish gate so the ATM re-publishes its
|
||||||
|
* `bitspire-cassettes-state` hello-event. Called on a re-pair (new seed) so
|
||||||
|
* the new operator receives the spire's current state (aiolabs/bitspire#56).
|
||||||
|
*/
|
||||||
|
export function resetBootstrapGate(): void {
|
||||||
|
if (!db) throw new Error('Database not initialized')
|
||||||
|
db.prepare('UPDATE meta SET value = ? WHERE key = ?').run('', 'bootstrapPublishedAt')
|
||||||
|
}
|
||||||
|
|
||||||
export type OperatorCassettesPayload = {
|
export type OperatorCassettesPayload = {
|
||||||
positions: Record<string, { denomination: number; count: number }>
|
positions: Record<string, { denomination: number; count: number }>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
19
apps/machine/src/types/electron.d.ts
vendored
19
apps/machine/src/types/electron.d.ts
vendored
|
|
@ -39,10 +39,20 @@ export interface BrandingConfig {
|
||||||
logoDarkDataUrl: string | null
|
logoDarkDataUrl: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Persisted NIP-46 bunker binding (mirror of state-store's StoredBunkerBinding). */
|
||||||
|
export interface BunkerBindingRecord {
|
||||||
|
clientSecretHex: string
|
||||||
|
spirePubkey: string
|
||||||
|
bunkerUrl: string
|
||||||
|
seedFingerprint: string
|
||||||
|
pairedAt: number
|
||||||
|
}
|
||||||
|
|
||||||
export interface AtmSecrets {
|
export interface AtmSecrets {
|
||||||
atmPrivateKey: string
|
/** Spire pairing seed URL (`spire-seed:v1:…`); carries the one-shot connect token. */
|
||||||
/** Legacy LP admin token — retained until 3d removes the LP backend. */
|
spireSeed: string
|
||||||
adminToken?: string
|
/** Persisted bunker binding, or null when the ATM is unpaired. */
|
||||||
|
bunkerBinding: BunkerBindingRecord | null
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|
@ -85,6 +95,9 @@ declare global {
|
||||||
getLastKnownConfigCreatedAt: () => Promise<number>
|
getLastKnownConfigCreatedAt: () => Promise<number>
|
||||||
getBootstrapPublishedAt: () => Promise<number | null>
|
getBootstrapPublishedAt: () => Promise<number | null>
|
||||||
markBootstrapPublished: (unixTimestamp: number) => Promise<void>
|
markBootstrapPublished: (unixTimestamp: number) => Promise<void>
|
||||||
|
saveBunkerBinding: (binding: BunkerBindingRecord) => Promise<void>
|
||||||
|
clearBunkerBinding: () => Promise<void>
|
||||||
|
resetBootstrapGate: () => Promise<void>
|
||||||
applyOperatorCassettesConfig: (
|
applyOperatorCassettesConfig: (
|
||||||
payload: { positions: Record<string, { denomination: number; count: number }> },
|
payload: { positions: Record<string, { denomination: number; count: number }> },
|
||||||
eventCreatedAt: number
|
eventCreatedAt: number
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue