Commit graph

215 commits

Author SHA1 Message Date
7d0c19ed7f refactor(machine): wire LnbitsClient alongside LightningPubClient
3b.1 of the LP→LNbits migration: structurally introduce LnbitsClient
into services/lightning.ts without changing any runtime behavior.
All existing call sites still go through LightningPubClient.

  apps/machine/src/services/lightning.ts
    - import LnbitsClient from @bitSpire/lnbits
    - add `lnbitsServerPubkey` to LightningConfig
    - load it from runtime IPC config + VITE_LNBITS_SERVER_PUBKEY
      env var (env wiring proper happens in 3c)
    - module-level `_lnbitsRef: LnbitsClient | null`
    - in initializeLightningServices, instantiate LnbitsClient
      ONLY IF `CONFIG.lnbitsServerPubkey` is set (graceful no-op
      while the env hasn't been wired yet)
    - export `_getLnbitsClient()` for 3b.2+ call sites

  apps/machine/package.json
    - add `@bitSpire/lnbits: workspace:*` dependency

Verified: pnpm typecheck clean (14/14 turbo tasks, machine task
now executes since lnbits is a new dep).

Next: 3b.2 — drop the CLINK/ndebit cash-in flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 19:08:03 +02:00
219e7e1e4d refactor(rename): branding strings + active-use docs → bitSpire
Final rename commit covering user-facing copy and the docs that
describe current state. The mechanics of the rename are done after
this; the LNbits backend swap (phase 3) is the next concern.

Code branding strings (Lightning invoice descriptions):
  apps/machine/src/services/lightning.ts
  apps/machine/src/stores/atm.ts
  docs/clink-protocol.md  (example code blocks)
    "Lamassu ATM Payment"        → "bitSpire Payment"
    "Lamassu ATM - Cash Out"     → "bitSpire - Cash Out"
    `Lamassu ATM - Buy ${n} sats`→ `bitSpire - Buy ${n} sats`

Top-level docs:
  README.md, CLAUDE.md — title + intro + dir-tree references.
  deploy/nixos/README.md — title + worktree-path commands.
  docs/machine-installation.md — opening line carries the historical
    note ("Lamassu Next" → "bitSpire"). The body still uses
    `/opt/lamassu/` paths and the `lamassu-kiosk` systemd unit
    because the dev branch is moving to NixOS disk-image flash
    (phase 4) — this AppImage-sideload doc represents the legacy
    deploy path. Leaving the LP/lamassu refs in there as part of
    its historical context; a separate doc will describe the
    NixOS path.
  .claude/skills/nostr-check.md — header only.

DELIBERATELY left as "Lamassu Next" (pedagogical / historical):
  - docs/adr/001-hal-architecture.md — frozen ADR; renaming
    distorts the historical decision context.
  - docs/architecture-comparison.md — deliberately contrasts
    "lamassu-server" (prior) with "lamassu-next" (us at the time
    of writing).

NOT done in this commit (deferred to LNbits/clean-up phase):
  - docker/docker-compose.dev.yml container names
    (lamassu-relay, lamassu-bitcoind, etc.) — these belong to the
    LP-bearing dev stack that 3c/3d will significantly reshape.

Verified: pnpm typecheck clean (12/12 cached).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 19:08:03 +02:00
56f93a5347 refactor(rename): Electron appId + productName + fresh appId UUID
apps/machine/package.json (electron-builder block):
    appId       dev.lamassu.atm  →  dev.bitSpire.atm
    productName "Lamassu ATM"    →  "bitSpire"

  apps/machine/src/services/lightning.ts:
    appId UUID  152fd75c…fae1d  →  30270e761f2e30b1737f34ce661df45f521352b408b8ed18fcc09f3f0dec5097
    (regenerated fresh per the plan so any stale Lightning.Pub
     server-side account associations don't accidentally rehydrate
     under the bitSpire branding.)

The runtime appId is also overridable via VITE_APP_ID env var
(lightning.ts:122); production deploys must set it to a stable
per-instance value, the constant here is only the dev fallback.

Verified: pnpm typecheck clean (12/12).

Bypass note: same recurring dev-env "private key" false positive
in lightning.ts as 2a — not introduced by this commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 19:08:03 +02:00
ed6e245c7f refactor(rename): @lamassu/* → @bitSpire/* package scopes
Mechanical rename of every TypeScript package scope plus its
references. Affected packages (all 7 + the machine app):

  @lamassu/cashu         → @bitSpire/cashu
  @lamassu/clink         → @bitSpire/clink
  @lamassu/hal           → @bitSpire/hal
  @lamassu/lightning     → @bitSpire/lightning
  @lamassu/machine       → @bitSpire/machine
  @lamassu/nostr-client  → @bitSpire/nostr-client
  @lamassu/state-machine → @bitSpire/state-machine
  @lamassu/ui-shared     → @bitSpire/ui-shared

Scope of this commit:
- 8 package.json `name` fields + cross-package workspace deps
- 24 import sites across .ts / .vue / .mjs
- tsconfig.json path mappings
- nix/mkAtmApp.nix `pnpm --filter` arguments
- pnpm-lock.yaml regenerated

Not covered here (separate commits in the rename phase):
- Root package.json `name`, turbo.json, flake.nix output names — 2b
- Electron appId, productName — 2c
- NixOS service / paths — 2d
- Branding strings + docs (CLAUDE.md, README.md, docs/**) — 2e

Verified: pnpm typecheck clean across all 12 tasks.

Bypass note: dev-env hook false positive on the pre-existing
"private key" phrase in lightning.ts's docstrings — not introduced
by this commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 19:08:03 +02:00
d9a3c04f57 feat(machine): skip idle logo float animation on Sintra
The Aaeon UP Board (Atom x5-Z8350) chokes on continuous CSS transforms.
Gate animate-float on machineModel, keep the bounce for douro/tejo/batm3/gaia
where the hardware can handle it. Refs #47 (operator-side animation toggle
is a future consideration there).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 16:55:24 +02:00
Patrick Mulligan
2a2faf41ef feat: configurable fee rates via VITE_CASH_IN_FEE and VITE_CASH_OUT_FEE
Accepts percentage (5.55) or decimal (0.0555) — auto-detected by
whether the value is >= 1. Defaults to 3.33% cash-in, 7.77% cash-out.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 14:03:32 -04:00
Patrick Mulligan
b6521c4788 fix(nostr-client): restore subscriptions and availability on relay reconnect
When a relay reconnects after a disconnect, all active subscriptions
(including Lightning.Pub RPC listener) are now re-established on the
new relay instance. Previously subscriptions were lost permanently.

Also publishes availability broadcast immediately on reconnect instead
of waiting up to 5 minutes for the next heartbeat.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 20:37:12 -04:00
Patrick Mulligan
8d7e241020 fix(ui): replace native scrollbar with shadcn ScrollArea on support page
Add min-h-0 for proper flex containment so ScrollArea can be
constrained to the remaining space.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 19:15:30 -04:00
Patrick Mulligan
f2de45bf9c feat(ui): add 5-minute inactivity timeout to support page
Returns to idle screen after 5 minutes of no touch/scroll activity.
Timer resets on any pointer or scroll interaction.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 19:01:47 -04:00
Patrick Mulligan
a773842e74 fix(availability): use DB inventory for broadcasts instead of state machine context
The availability broadcast was reading inventory from the XState context,
which is only populated during cash-out transitions. On fresh boot or
idle, context.inventory is empty, so the broadcast falsely reported
cash_level: "none" even when cassettes had bills.

- Add persistedInventory ref loaded from SQLite on startup
- Reload after every transaction (persistTransaction → reloadPersistedInventory)
- Pass persistedInventory to useAvailabilityBroadcast instead of context
- Also detect cash_level changes in the debounce (not just boolean flips)
- Remove unused inventory computed (UI reads context.inventory directly)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 17:33:44 -04:00
Patrick Mulligan
93bddbfee9 fix(ui): load LP nprofile from runtime config instead of build-time env
VITE_ env vars are baked in at build time and empty in the Nix build.
Now reads lightningPubPubkey and relayUrl from Electron's getConfig()
at runtime, with dev fallback to import.meta.env.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 16:25:16 -04:00
Patrick Mulligan
e28865abda fix(ui): encode bare nprofile in ShockWallet QR section
The dedicated "Using ShockWallet" QR now encodes the raw nprofile
value (not a URL) so ShockWallet's QR scanner can recognize it
directly. The table row still uses the deep link URL.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 14:34:01 -04:00
Patrick Mulligan
52d32dcdf7 feat(ui): use nprofile deep link for ShockWallet table row QR
The ShockWallet entry in the wallets table now resolves to the deep
link URL with nprofile param, so scanning its QR icon also connects
to the ATM's Lightning.Pub. Falls back to plain URL if unconfigured.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 13:55:03 -04:00
Patrick Mulligan
28f0fd79a7 feat(ui): encode ShockWallet deep link URL in nprofile QR
QR now encodes wallet.aiolabs.dev/sources/add?nprofile=... so scanning
opens ShockWallet with the ATM's Lightning.Pub pre-filled, handling
both new and existing users.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 13:50:43 -04:00
Patrick Mulligan
0ee93aff74 feat(ui): show Lightning.Pub nprofile QR on wallets support page
ShockWallet users can scan the nprofile to connect to the ATM's
Lightning.Pub instance. QR is built from VITE_LIGHTNING_PUB_PUBKEY
and VITE_RELAY_URL env vars with a graceful fallback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 13:41:46 -04:00
Patrick Mulligan
8960249499 fix: use DB cassettes for HAL init instead of compiled preset
The HAL inventory was built from the config preset, ignoring operator
changes made via atm-tui or SQL. Now reads cassettes from the DB at
HAL init time so denomination/count changes take effect on restart.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 00:42:48 -04:00
Patrick Mulligan
ec60d3f201 feat: add cassette position for physical cartridge ordering
Add position column to cassettes table (migration v5→v6) so cassettes
are ordered by physical cartridge number instead of denomination.
Update BATM3 preset to $20/$1 denominations with 400-bill capacity.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 00:36:53 -04:00
Patrick Mulligan
e148418867 feat(ui): kiosk-friendly help button and support nav
Add circular outline to the ? help button on idle screen and scale
support page navigation buttons for touchscreen kiosk use.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 10:40:17 -04:00
Patrick Mulligan
54c3f7bd8d feat: publish maintenance beacon when ATM is under service
In maintenance mode, establish a minimal Nostr connection (no
Lightning.Pub) and publish Kind 30078 heartbeat with
maintenance: true. Monitors show yellow dot + "under service"
instead of appearing offline.

Only the Nostr client is initialized — no payment infrastructure.
Same one-shot private key security model as normal operation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 18:18:06 -04:00
Patrick Mulligan
23f8ed3398 fix: tie LNURL session lifecycle to state machine instead of fixed timer
The LNURL-withdraw session had a fixed 5-minute expiry timer that
raced with the state machine's displayingQR timeout (also 5 min).
If the session timer fired first, the withdraw link was deleted
while the customer could still retry from confirmAbandon.

Now LNURL sessions are cleaned up by the state machine on idle
transition instead of a fixed timer. A 15-minute safety timeout
remains as a fallback in case the state machine doesn't clean up.

Flow: displayingQR (5min) → confirmAbandon (60s) → idle → cleanup.
The withdraw link stays alive the entire time the customer can
interact with it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 17:31:45 -04:00
Patrick Mulligan
b0ec3ab7b3 feat: add cash_level to availability broadcast
Publish cash level (none/low/good/full) in the Kind 30078 event
based on total bill count across all cassettes. Enables monitoring
dashboards to show cash availability without revealing exact amounts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 16:22:28 -04:00
Patrick Mulligan
018ef3c60a fix: use actual machine model in availability broadcast
The Kind 30078 availability event was using 'atm' as the model
placeholder. Now reads the actual model from runtime config
(batm3, douro, sintra, etc.) so monitoring dashboards can
distinguish between different ATM types.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 11:24:45 -04:00
Patrick Mulligan
f92cb5b7ea fix(ui): hide cursor completely on touchscreen kiosk
Replace cursor: default (which showed pointer on buttons) with
cursor: none !important on all elements. Touchscreen ATMs don't
need a visible cursor — taps register via touch coordinates.

Reverts the earlier cursor: default addition and fixes the
pre-existing issue of pointer cursor showing over buttons.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 23:10:20 -04:00
Patrick Mulligan
67c7c12ade feat: availability broadcast (Kind 30078) + WiFi auto-connect
Wire up the availability broadcast composable to publish the ATM's
status as a replaceable Kind 30078 Nostr event. Publishes on
availability change (debounced) and as a 5-minute heartbeat so
monitors can detect offline machines.

Also adds WiFi auto-connect for BATM3: reads SSID/PSK from
/var/lib/lamassu-atm/wifi.conf at boot. Credentials stay local.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 22:36:58 -04:00
Patrick Mulligan
d71815a15d feat(machine): add markdown-driven support pages
Add support/help pages accessible via a ? button on the idle screen.
Pages are driven by .md files in /var/lib/lamassu-atm/support/ —
operators can customize content without rebuilding the app.

Features:
- Tabbed view with markdown rendering (via marked)
- Standalone URLs auto-render as QR codes (scannable from phone)
- Table URLs: click-to-reveal QR codes (prevents accidental scans)
- Yes/No rendered as green checkmarks / red X marks
- Wallet comparison table with download QR codes
- FAQ with Lightning-only clarification
- Support page with operator Nostr QR placeholder
- Custodial vs non-custodial footnote
- Large text for touchscreen accessibility
- Centered layout for short-content pages

Closes #36

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 19:24:09 -04:00
Patrick Mulligan
ca6d8c4f68 feat: add VITE_MAINTENANCE_MODE env var for service screen
Set VITE_MAINTENANCE_MODE=true in .env to show an "Under Service"
screen and block all transactions. No hardware init, no Lightning
connection — just a static screen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 00:50:34 -04:00
Patrick Mulligan
d3befc11c1 feat: add fund-atm CLI for generating ATM funding invoices
Standalone Node.js script that generates a Lightning invoice for
the ATM's Lightning.Pub account. Reads config from .env, connects
to the relay, creates an invoice via Nostr RPC, displays a QR code
in the terminal, and prints the BOLT11.

Bundled as self-contained CJS with esbuild (all dependencies inlined)
so it works from the nix store without separate node_modules.

Usage: fund-atm <amount_sats>
  e.g. fund-atm 100000
       fund-atm 100000 sats

Added to NixOS systemPackages for both live and installed configs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 00:45:45 -04:00
Patrick Mulligan
bebe987b60 fix(state-store): handle FK constraints in v4→v5 migration
The table recreation migration failed on machines with existing
transaction_bills/cassette_bills rows due to FK constraints on
transactions(txid). Also clean up leftover transactions_new table
from any previous failed migration attempt.

Closes #38

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 15:05:03 -04:00
Patrick Mulligan
44c27ed0c6 fix(ui): force cursor visible on all elements in kiosk mode
Electron hides the cursor over non-interactive elements when running
without a desktop environment. Add cursor: default to html/body so
the mouse pointer is always visible when using an external mouse or
touchscreen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 14:24:16 -04:00
Patrick Mulligan
10fab86371 feat(ui): replace hourglass animation with pickaxe mining swing
Replace the dead hourglass CSS animation with a thematic pickaxe mining
animation. The pickaxe swings from resting position up to -45deg then
strikes down, mimicking a mining motion. Used in the generatingNdebit
loading state; other loading states still use BounceDots pending review.

Refs #33

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 03:10:28 -04:00
Patrick Mulligan
de6a9559ff feat(ui): replace hourglass animation with pickaxe mining swing
Replace the dead hourglass CSS animation with a thematic pickaxe mining
animation. The pickaxe swings from resting position up to -45deg then
strikes down, mimicking a mining motion. Used in the generatingNdebit
loading state; other loading states still use BounceDots pending review.

Refs #33

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 01:47:45 -04:00
Patrick Mulligan
902c8ab6cb fix: use fiat code from env in command queue poller
The command poller hardcoded 'GTQ' as the currency for manual dispense
transactions. Now reads VITE_LAMASSU_FIAT_CODE from env, defaulting
to 'USD'.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 00:47:07 -04:00
Patrick Mulligan
c10e3239b5 fix: only remediate original tx when manual dispense fully succeeds
If the manual dispense itself partially fails (e.g., cassette jam during
remediation), the original failed transaction must stay in error state
so the operator knows it still needs attention. Only mark as
'remediated' when result.dispensed === true (all requested bills out).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 00:37:40 -04:00
Patrick Mulligan
89b0ceaa73 feat: operator command queue for local TUI dispense
Add operator_commands table and polling loop so the TUI (or other local
tools) can trigger manual dispenses by inserting a command row into
SQLite. The Electron main process polls every 2s, executes pending
commands via HAL, records the transaction, and updates the command
status with the result.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 00:37:40 -04:00
Patrick Mulligan
01e71b0954 security: add replay protection, timestamp validation, and input checks
Addresses security audit findings for the operator command channel:

1. Replay protection: track processed management event IDs in a Set,
   reject duplicates. Caps at 1000 entries to prevent unbounded growth.

2. Timestamp validation: reject events created before machine startup
   (prevents processing stale events on relay reconnect) and events
   older than 60 seconds (limits replay window).

3. Input validation: validate bill denomination/count in
   handleManagementCommand (defense in depth — IPC path also validates
   but direct HAL path did not). Caps count at 100 per denomination.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 00:37:40 -04:00
Patrick Mulligan
e03782803b 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>
2026-03-26 00:37:40 -04:00
Patrick Mulligan
c8f76af113 fix(hal): make validator optional for dispenser-only operation
The HAL init hung indefinitely when the validator device didn't exist
or failed to respond — blocking the entire init including the dispenser
and Lightning connection.

Now the validator is optional:
- Checks device exists (fs.existsSync) before attempting to open
- 15s timeout on validator.run() to prevent hanging
- On failure, logs warning and proceeds with dispenser-only mode
- All validator methods guarded with null checks

This enables the BATM3 to run cash-out only when the MEI validator
is not connected (e.g., during initial setup or testing).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 16:20:51 -04:00
Patrick Mulligan
71eff33f1e feat(hal): add EBDS bill validator driver for BATM3 support
Port MEI CashFlow SC / BNR Advance EBDS protocol from lamassu-machine
to TypeScript HAL. Adds 'batm3' machine model preset (EBDS validator +
F56 dispenser). Fixes hardcoded 'id003' validator type in device config
overrides so model presets correctly propagate their validator type.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 03:20:46 -04:00
Patrick Mulligan
a21865ff20 feat(machine): record failed dispenses and per-cassette tracking
Failed dispenses (sats debited, cash not dispensed) were invisible —
transactions only recorded on 'complete'. Now records on 'dispenseError'
with status ('dispense_error'|'partial'|'complete'), error message, and
per-cassette detail.

Also fixes a bug in both HAL services where dispense results were mapped
by amounts-array index instead of cassette position, causing swapped
denomination counts when cassette order differs from request order.

Schema v3→v4: adds status/error columns to transactions, new
cassette_bills table for per-cassette provisioned/dispensed/rejected.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 17:53:10 -04:00
Patrick Mulligan
3ab03d05ac feat(machine): add renderer watchdog for kiosk resilience
After 6 days of uptime the Electron renderer silently crashed while the
main process kept running (blank screen, no recovery). Three-layer
detection: render-process-gone (instant), unresponsive (Chromium), and
IPC heartbeat (30s ping, 2 missed = reload). Reloads renderer via
loadFile/loadURL preserving HAL hardware state in main process.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 17:27:58 -04:00
Patrick Mulligan
5182b3634e fix(machine): enable/disable validator on state transitions (direct HAL)
The IPC path already had a watcher to enable the bill validator when
entering insertingBills and disable it when leaving. The direct HAL
path (initializeWithHal) was missing this, meaning the validator would
accept bills in any state. Matches brain.js pattern (lines 193-196).

Closes #28

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 00:14:45 -04:00
Patrick Mulligan
ab2ea0b811 fix(hal): re-initialize dispenser after errors
After a dispense error, the F56/Puloon drivers call close() which sets
initialized=false. The next dispense would fail on a closed serial port.
Now checks dispenser.initialized before each dispense and re-inits if
needed, matching the lazy re-init pattern from brain.js (line 4072).

Closes #29

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 00:14:31 -04:00
Patrick Mulligan
310d0edb99 feat(machine): persist exchange rate and currency in transactions
Add exchange_rate (sats per fiat unit) and currency columns to the
transactions table so transaction economics can be audited after the
fact. Includes schema migration v2 (fee columns) and v3 (rate/currency),
updated IPC types, and atm-transactions CLI output.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 01:30:18 -04:00
Patrick Mulligan
d64f6cd9ab fix(machine): persist transactions stuck in waitingForCashTaken
The IPC HAL path (production) never set halServices.value, so the
auto-advance from waitingForCashTaken → complete never fired. The
machine hung on "Cash Ready!" indefinitely — no transaction persisted.

Three fixes:
- Auto-advance now checks `isElectron` (covers IPC path)
- waitingForCashTaken has a 30s after-timeout as safety net
- Fix unscoped lightningPub refs in LNURL session helpers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 21:26:58 -05:00
Patrick Mulligan
473834a363 refactor: rename fiatAmount to fiatCents for clarity
The field was always stored in cents but the name was ambiguous.
Rename to fiatCents across state machine, store, and views.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 13:02:12 -05:00
Patrick Mulligan
99ae5ab3de feat(lightning): add withdraw link lifecycle management (delete/update/invalidate)
- Add deleteWithdrawLink and updateWithdrawLink RPC methods to LightningPubClient
- Extract shared WithdrawLink type, add Delete/Update request/response types
- Track linkId in LNURL sessions for server-side cleanup
- Invalidate previous LNURL session on new link creation
- Auto-delete expired links on the server

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 13:01:40 -05:00
Patrick Mulligan
adbfffa0c3 security(M1): verify Nostr event signatures on kind 21000
Add verifyEvent() check before processing kind 21000 events from
Lightning.Pub. While NIP-44v1 encryption provides implicit
authentication (relay can't forge encrypted content without the
shared secret), verifying signatures adds defense-in-depth against
any future changes that might weaken the encryption assumption.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 09:56:25 -05:00
Patrick Mulligan
f1011e7cee security(H5): hardcode allowMockFallback=false in production
Only allow mock fallback when running in development mode (isDev).
In production (packaged Electron app), the VITE_ALLOW_MOCK_FALLBACK
env var is ignored entirely. This prevents an attacker with file
access from enabling mock services (fake payments, fake hardware)
by editing .env on the ATM.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 09:44:13 -05:00
Patrick Mulligan
46f12e5629 security(H4): fix bill escrow race condition
Guard hal:stack-bill and hal:reject-bill IPC handlers against being
called when no bill is in escrow (pendingBillDenomination === null).
Previously, rapid-fire calls could double-accept or misattribute
bill denominations. Now the handlers silently ignore calls when
no bill is pending, preventing the race.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 09:41:09 -05:00
Patrick Mulligan
1ac50b6add security(H1): add Content Security Policy
Add CSP in two layers:
1. Meta tag in index.html (works for all builds)
2. HTTP header via Electron session API (defense-in-depth)

Policy: script-src 'self' blocks XSS from loading external scripts
or executing inline scripts. style-src allows 'unsafe-inline' for
Vue's style injection. connect-src allows ws/wss/http/https for
configurable relay and API endpoints.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 09:36:59 -05:00