feat(desktop): Tauri v2 kiosk shell — maximized window, prod right-click block, auto-update + code-signing

Add apps/desktop, a thin Tauri v2 shell wrapping the SAME @parking/web SPA so
the desktop and browser UIs never drift: dev loads the Vite dev server (HMR),
prod bundles the web app's dist/. No business logic in the shell (device/auth/
ledger stay in @parking/server); deny-by-default capabilities.

apps/web (single UI source of truth):
- lib/origin.ts: centralize the backend origin (API_BASE/apiUrl/wsUrl from
  VITE_API_BASE); no-op in the browser, lets the desktop build target Fastify.
- lib/kiosk.ts: block the right-click context menu in PROD only (dev keeps it +
  devtools).
- lib/desktop-updater.ts: prompt-on-update auto-update (no-op in browser/offline)
  → downloadAndInstall + relaunch; i18n update.* keys (sq+en).
- .env.production: VITE_API_BASE wired to the Fastify origin for the bundle.

Desktop:
- window starts maximized (not fullscreen — operator keeps OS access).
- auto-update via tauri-plugin-updater + -process; self-hosted endpoint is a
  PLACEHOLDER to fill in. Updater keypair: pubkey embedded in tauri.conf.json;
  private key + password kept OUTSIDE the repo (~/.parking-updater-keys) and as
  TAURI_SIGNING_* build secrets.
- Turbo build is a no-op; the real signed bundle is `pnpm --filter
  @parking/desktop bundle` (verified → .deb/.rpm/.AppImage + .sig signatures).

Verified: cargo check clean; turbo run build lint 14/14 green; i18n parity holds;
no key/sig/bundle artifacts in the repo.

Wiki (security + desktop analysis recorded alongside):
- new concepts/tpm.md (TPM 2.0: how it works, sealed-LUKS auto-unlock + non-
  extractable signing key, limits — live-root, bus-sniff — TPM-vs-ATECC608 by
  platform).
- new decisions/desktop-shell-tauri.md (Tauri v2 over Electron; best-case Ubuntu
  26.04 LTS, worst-case Windows+WSL → kiosk browser; full as-built).
- pull-the-disk attack trace on append-only-event-chain; ATECC608 not-in-a-PC
  caveat; cross-links from disk-os-hardening / threat-model.
- open-questions #11 (appliance WebKitGTK), #12 (TPM hardening impl), #13
  (startup verifyChain self-check); index/overview/log/standing-decisions.

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
2026-06-21 12:21:49 +02:00
parent ae736a9e3e
commit d0536da3d7
52 changed files with 5792 additions and 22 deletions
+56
View File
@@ -1173,3 +1173,59 @@ reserve checkbox (SiteSettings); booth pay modal shows an "OUT-OF-WINDOW" charge
Verified on a copy of the live DB: qty 2 = 2× price; night-plan 19:30 entry → 30min/15,000 ALL owed,
stamped + paid → gate clears, chain verifies; reserve toggle holds a qty-2 sub's 2 spots. Build+lint
12/12; 80 shared tests. Updated [[subscription]], [[capacity-occupancy]], [[tariff]].
## [2026-06-21] query | Desktop shell: Tauri v2 vs. Electron
Compared Tauri v2 and Electron for shipping the operator UI as a desktop app (mobile deferred).
Decision (with user): **Tauri v2** — small footprint, no bundled Chromium to patch, deny-by-default
native surface fitting the booth-operator threat model; MIT/Apache. Shell stays thin (device/auth/
ledger/pricing remain in Fastify, per user). Filed [[desktop-shell-tauri]]; cross-linked from
[[standing-decisions]], [[overview]], [[index]]. Open dependency: appliance WebKitGTK version
([[open-questions]] #11) — flips to Electron if ancient/unavailable.
## [2026-06-21] query | Desktop shell — target OS (best/worst case)
User specified deployment span: best = Ubuntu 26.04 LTS desktop, worst = Windows 11 + WSL + Docker.
Refined [[desktop-shell-tauri]] + [[open-questions]] #11: Ubuntu 26.04 LTS ships a current
distro-maintained WebKitGTK → effectively closes the WebView risk; Tauri unconditional there. The
Windows+WSL case is NOT an "Electron instead" fallback — it conflicts with the standing Linux-
appliance platform decision and can't host a GUI shell in headless WSL/Docker; fallback is a kiosk
browser at the local Fastify-served SPA (Electron only if a standalone Windows installer is
mandated). Thin-shell architecture makes that fallback cheap.
## [2026-06-21] query | TPM 2.0 hardening — analysis + pull-the-disk attack trace
How a TPM works (non-extractable keys + PCR sealing) and its limits, recorded after tracing the
"pull the SSD, tamper parking.sqlite offline, reboot" attack against event-log.ts/signer.ts.
Findings: verifyChain() catches every blind tamper (bad sig / index gap / prevHash / unknown keyId)
but (a) nothing runs it at boot, and (b) the software HMAC key lives in .env on the same disk →
attacker can re-sign undetectably. Only a secure-element key (TPM on a PC, ATECC608 on embedded)
makes it tamper-PROOF; TPM-sealed LUKS additionally blocks off-host mount. TPM verdict: recommended
not required on the Ubuntu appliance (sealed-LUKS auto-unlock + non-extractable signing key); does
NOT defend a rooted live host or the operator; bus-sniff/PCR-brittleness caveats → prefer fTPM + PIN,
keep recovery passphrase + re-seal runbook; complements not replaces reconciliation. Also corrected:
ATECC608 is NOT in a PC (external I²C part) → on a PC appliance the TPM is the host secure-element,
ATECC608 reserved for the ESP32 controller. New page [[tpm]]; cross-linked [[disk-os-hardening]],
[[threat-model]], [[atecc608]], [[append-only-event-chain]]; open-questions #12 (TPM impl, to build),
#13 (startup verifyChain self-check, to build); index + counts updated.
## [2026-06-21] build | apps/desktop — Tauri v2 kiosk shell scaffolded
Built the thin Tauri v2 shell per [[desktop-shell-tauri]]: new apps/desktop package wrapping the
SAME apps/web SPA (dev → localhost:5173 with HMR; prod → bundled web dist/), so desktop and browser
UIs can't drift (user requirement). Rust core holds no business logic; capabilities core:default
only (deny-by-default). One apps/web change: centralized the backend origin into lib/origin.ts
(API_BASE/apiUrl/wsUrl from VITE_API_BASE) — no-op in the browser, lets the Tauri build target the
Fastify origin. Turbo build is a no-op; real bundle = `pnpm --filter @parking/desktop bundle`.
VERIFIED: cargo check + full tauri build → working .deb/.rpm/.AppImage; turbo run build lint 14/14
green; prereqs present (Rust 1.93, WebKitGTK 4.1, libsoup-3, WSLg). Filled the As-built section of
[[desktop-shell-tauri]]. Deferred: kiosk lockdown, auto-update, signing, Windows kiosk-browser path.
## [2026-06-21] build | apps/desktop — window/right-click, auto-update, code-signing, env wiring
Per user choices on the Tauri shell: window starts MAXIMIZED (not fullscreen — operator keeps OS
access); right-click context menu blocked in PROD only (lib/kiosk.ts, dev keeps devtools).
VITE_API_BASE wired via apps/web/.env.production (committed non-secret, allow-listed in .gitignore;
auto-loaded by vite build → desktop bundle targets Fastify, no manual export). Auto-update built:
tauri-plugin-updater + -process, prompt-on-update flow (lib/desktop-updater.ts, no-op in browser/
offline) → downloadAndInstall + relaunch; endpoint is a self-hosted PLACEHOLDER to fill in. Updater
keypair generated: pubkey embedded in tauri.conf.json; private key + password kept OUTSIDE the repo
(~/.parking-updater-keys, 0600) + as TAURI_SIGNING_* build secrets. VERIFIED: signed bundle →
.deb/.rpm/.AppImage + .sig updater signatures; turbo run build lint 14/14 green; no key material in
the repo. Updated As-built in [[desktop-shell-tauri]]. Deferred: real update URL, OS installer
signing, Windows kiosk-browser fallback.