feat: operator command channel via Nostr (manual dispense)

Add a Nostr-native operator command channel using Kind 21003 (CLINK
Manage) events. Operators listed in OPERATOR_PUBKEYS can send encrypted
commands to the machine.

Phase 1 implements manual dispense: operator sends a dispense command,
machine verifies sender, checks it's idle, performs a direct HAL
dispense (bypassing state machine), and records the transaction.

When ref_txid is provided, the referenced failed transaction is updated
to status 'remediated', closing the loop on dispense errors.

Changes:
- CLINK types: add 'machine' resource, MachineDispenseRequest type
- CLINK client: support operator pubkey list (string | string[])
- Runtime config: VITE_OPERATOR_PUBKEYS env var
- Schema v4→v5: manual_dispense type, remediated_by column
- Lightning services: wire onManagement callback
- ATM store: handleManagementCommand with idle check + remediation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-03-23 00:36:55 -04:00
commit e03782803b
11 changed files with 274 additions and 24 deletions

View file

@ -17,6 +17,7 @@ import {
setCassettes,
getInventory,
recordTransaction,
remediateTransaction,
emptyCashbox,
getCashbox,
} from './state-store.js'
@ -180,6 +181,9 @@ ipcMain.handle('get-config', () => {
// SECURITY: In production (packaged app), mock fallback is always disabled.
// Only allow it in development mode, and only when explicitly opted in via env.
allowMockFallback: isDev && process.env.VITE_ALLOW_MOCK_FALLBACK === 'true',
// Operator identity (comma-separated hex pubkeys)
operatorPubkeys: process.env.VITE_OPERATOR_PUBKEYS || '',
}
})
@ -213,6 +217,9 @@ ipcMain.handle('state:get-inventory', () => getInventory())
ipcMain.handle('state:get-cashbox', () => getCashbox())
ipcMain.handle('state:record-transaction', (_event, tx) => recordTransaction(tx))
ipcMain.handle('state:empty-cashbox', () => emptyCashbox())
ipcMain.handle('state:remediate-transaction', (_event, txid: string, remediatedByTxid: string) =>
remediateTransaction(txid, remediatedByTxid)
)
// =============================================================================
// HAL Hardware IPC Handlers