feat(machine): on-machine QR-pairing wizard #68

Merged
padreug merged 6 commits from qr-pairing-wizard into dev 2026-06-24 22:40:16 +00:00

6 commits

Author SHA1 Message Date
fd4f69826d fix(machine): decode QR at intrinsic frame + tuned capture resolution
The camera pairing source decoded frames at the <video> element's CSS box
size (qr's readFrame default) rather than the intrinsic frame, and let the
stream stay at the panel-bound ~720p that frontalCamera negotiates from the
screen size. On the 1280x800 kiosk with a fixed-focus 5MP scan camera that
left far too few pixels-per-module for a dense spire-seed QR, so a centered,
in-square code never decoded.

Decode the intrinsic frame (readFrame fullSize=true) and pin a deliberate
1280x960 capture via applyConstraints. lamassu-machine caps QR scanning at
640x480 for decode speed (megapixels only slow the per-frame decode); our
seed QR is denser than a lightning invoice, so 1280x960 balances
pixels-per-module against latency and keeps auto-exposure from blowing out a
frame-filling phone screen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 23:53:16 +02:00
d32ddc806a build(nix): bump pnpmDeps hash for the qr dependency
Adding `qr` (and dropping `jsqr`) changed pnpm-lock.yaml, invalidating the
fixed-output hash for the vendored pnpm store. Without this the NixOS build
of the ATM app fails at the FOD before activation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 23:21:10 +02:00
b9340f7754 docs(machine): document the on-machine QR-pairing wizard
Note the wizard flow next to VITE_SPIRE_SEED + a dedicated Pairing section
(aiolabs/bitspire#52): unpaired → scan seed off camera → persist + relaunch →
normal boot pairs. Records the qr-over-jsqr choice rationale by reference.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 23:08:24 +02:00
aca6aebcb6 feat(machine): render QR-pairing wizard for unpaired machines
Wires the capture + ingest pieces into a screen (aiolabs/bitspire#52). When
the machine boots `unpaired` (fresh, or binding revoked/expired) and runs
under Electron, App.vue renders `PairingWizard` in place of the static
"Pairing Required" card.

The wizard probes available sources, shows the camera viewfinder, and on a
valid scan persists + relaunches. A stray/non-seed QR is rejected with a hint
and scanning resumes. NFC (when present) appears as an alternate source
button. Browser dev (no Electron bridge) still falls back to the static card.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 23:07:43 +02:00
d22157b40c feat(machine): pairing-source abstraction + QR/NFC capture + seed ingest
The capture half of the QR-pairing wizard (aiolabs/bitspire#52), behind a
`PairingSource` seam so the wizard UI stays agnostic to how the seed arrives:

- `QrPairingSource` — camera capture + decode via `qr` (paulmillr). Chosen
  over the dormant, unmaintained `jsqr`: `qr` is zero-dependency, auditable,
  dual MIT/Apache, actively maintained, and authored by the same person as the
  `@noble`/`@scure` crypto our nostr stack already trusts. Its `qr/dom.js`
  helper wraps getUserMedia + the per-frame decode loop.
- `NfcPairingSource` — Web NFC scaffold; `isAvailable()` is false on the
  Sintra's Linux Electron, so it's inert until real NFC hardware lands (the
  user flagged NFC as a plausible future pairing method).
- `ingestScannedSeed` — validates the scan parses as a spire-seed (rejecting a
  stray QR), persists it, and relaunches. Covered by unit tests
  (invalid-seed / no-bridge / persist-failed / happy path).
- `availablePairingSources()` probes each source and returns the runnable ones
  in preference order (camera first).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 23:07:31 +02:00
9935807f8c 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>
2026-06-23 23:07:18 +02:00