fix(access): pass plain objects over IPC for session Complete

At Complete the store handed the session's withdraw/pay step to
window.electronAPI straight out of the loadedBoltCard ref — a Vue
reactive proxy — and Electron's structured clone refused it:
'[ATM] Bolt Card withdraw failed: Error: An object could not be cloned.'
(sintra, 2026-09-21 06:51). The customer had to re-tap, which works
because the direct-tap path passes a plain string. Copy the steps field
by field into plain objects before they cross the bridge.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-09-22 14:24:13 +02:00
commit 42e3657fe1

View file

@ -306,6 +306,22 @@ export const useAtmStore = defineStore('atm', () => {
// screen (raw lnurlw, spent by this call) or the session opened at entry // screen (raw lnurlw, spent by this call) or the session opened at entry
// (hit-keyed steps, no p/c). // (hit-keyed steps, no p/c).
type BoltCardSource = { lnurlw: string } | { session: CardSession } type BoltCardSource = { lnurlw: string } | { session: CardSession }
// Electron IPC structured-clones its arguments and rejects Vue reactive
// proxies with "An object could not be cloned". `loadedBoltCard` is a ref,
// so anything reached through it is a proxy — copy the steps field by field
// into plain objects before they cross the bridge.
const plainWithdrawStep = (w: NonNullable<CardSession['withdraw']>) => ({
callback: w.callback,
k1: w.k1,
minWithdrawable: w.minWithdrawable,
maxWithdrawable: w.maxWithdrawable,
})
const plainPayStep = (p: CardSession['pay']) => ({
callback: p.callback,
minSendable: p.minSendable,
maxSendable: p.maxSendable,
metadata: p.metadata,
})
// Access-control gate config (ADR-003). Defaults disabled → the machine's // Access-control gate config (ADR-003). Defaults disabled → the machine's
// `locked` state bypasses straight to `idle` (behaviour identical to no gate). // `locked` state bypasses straight to `idle` (behaviour identical to no gate).
// Populated from RuntimeConfig.accessControl in initializeForProduction. // Populated from RuntimeConfig.accessControl in initializeForProduction.
@ -673,7 +689,7 @@ export const useAtmStore = defineStore('atm', () => {
'session' in source 'session' in source
? source.session.withdraw ? source.session.withdraw
? await api.withdrawWithSession({ ? await api.withdrawWithSession({
withdraw: source.session.withdraw, withdraw: plainWithdrawStep(source.session.withdraw),
bolt11: invoice, bolt11: invoice,
amountMsat, amountMsat,
}) })
@ -713,7 +729,7 @@ export const useAtmStore = defineStore('atm', () => {
const api = window.electronAPI! const api = window.electronAPI!
const res = const res =
'session' in source 'session' in source
? await api.resolveSessionInvoice({ pay: source.session.pay, amountMsat }) ? await api.resolveSessionInvoice({ pay: plainPayStep(source.session.pay), amountMsat })
: await api.resolveCardInvoice({ lnurlw: source.lnurlw, amountMsat }) : await api.resolveCardInvoice({ lnurlw: source.lnurlw, amountMsat })
if (!res.ok || !res.bolt11) { if (!res.ok || !res.bolt11) {
boltCardProcessing.value = false boltCardProcessing.value = false