// Peach Codex — restoration view. // // Two entry paths, both feeding the same ChunkBuffer: // • Camera scan — Aztec codes via @zxing/library // • PDF import — pdf.js + grid scan of each rendered page // // Once ChunkBuffer.isReady(): // 1. Prompt for the master passphrase. // 2. Reconstruct + integrity-verify locally (popup) via reconstructAndVerify. // 3. Hand the payload to the SW (RESTORE_FROM_CODEX) for actual decrypt // and replace. // // Restoration is intentionally NOT Pro-gated — a printed Codex must remain // restorable forever, regardless of license state. import { createSignal, onCleanup, onMount, Show } from "solid-js"; import { sendMessage } from "../../shared/bridge"; import { ChunkBuffer, createScanGate, parseRawScan, reconstructAndVerify, } from "../codex/restore"; import { startScanner } from "../codex/scanner"; import type { ScannerHandle } from "../codex/scanner"; import type { CodexPayload } from "../../shared/codex-types"; interface CodexRestoreViewProps { onBack: () => void; onRestoreSuccess: () => void; } type Phase = | "choose" | "scanning" | "pdf-import" | "pdf-shortfall" | "ready" | "restoring" | "success" | "error"; export function CodexRestoreView(props: CodexRestoreViewProps) { // The buffer is intentionally not stored in a signal — its identity is // stable across the lifetime of the view and we read it through the // `size()`/`isReady()` methods. We mirror its observable state into // signals so Solid can re-render. const buffer = new ChunkBuffer(); let videoEl: HTMLVideoElement | undefined; let scanner: ScannerHandle | null = null; let pdfFileInput: HTMLInputElement | undefined; const [phase, setPhase] = createSignal("choose"); const [scanned, setScanned] = createSignal(0); const [dataShards, setDataShards] = createSignal(null); const [verifyCode, setVerifyCode] = createSignal(null); const [hint, setHint] = createSignal( "point your camera at the first code on page 1", ); const [cameraError, setCameraError] = createSignal(null); const [scanWarning, setScanWarning] = createSignal(null); const [pdfProgress, setPdfProgress] = createSignal<{ page: number; total: number; } | null>(null); const [pdfError, setPdfError] = createSignal(null); const [pdfShortfall, setPdfShortfall] = createSignal<{ found: number; missing: number; ready: number; total: number; } | null>(null); // Hold the reconstructed payload between the integrity check and the SW // call so the user can re-enter the passphrase on a wrong-password failure // without rescanning every code. const [pendingPayload, setPendingPayload] = createSignal( null, ); const [passphrase, setPassphrase] = createSignal(""); const [restoreError, setRestoreError] = createSignal(null); const [restoredCount, setRestoredCount] = createSignal(null); function refreshFromBuffer() { setScanned(buffer.size()); const ctx = buffer.getContext(); if (ctx) { setDataShards(ctx.dataShards); setVerifyCode(ctx.verifyCode); } if ( buffer.isReady() && (phase() === "scanning" || phase() === "pdf-import" || phase() === "pdf-shortfall") ) { setPhase("ready"); setHint("scan complete. enter your master password to restore."); setPdfShortfall(null); // Stop the camera the instant we have enough codes — no reason to keep // the green light on while the user types their passphrase. if (scanner) { scanner.stop(); scanner = null; } } else if (phase() === "scanning") { const ds = ctx?.dataShards; if (ds) { const remaining = Math.max(ds - buffer.size(), 0); setHint( remaining > 0 ? `${remaining} more ${remaining === 1 ? "code" : "codes"} needed` : "almost there…", ); } } } function explainScanReason(reason: string): string { switch (reason) { case "foreign_codex": return "that code belongs to a different Codex."; case "bad_integrity": return "a code's checksum failed — try again."; case "duplicate": return ""; // not surfaced — duplicates are normal as the user pans case "parse_error": return "could not read that code."; default: return ""; } } async function handleScan(raw: string) { const parsed = parseRawScan(raw); if ("error" in parsed) { // parseRawScan failures are usually a non-Peach Aztec code or a corrupt // frame; quietly ignore so the scanner keeps running. return; } const result = await buffer.add(parsed); refreshFromBuffer(); if (!result.accepted && result.reason) { const msg = explainScanReason(result.reason); if (msg) { setScanWarning(msg); // Auto-clear so the warning doesn't linger after the user moves on. setTimeout(() => setScanWarning(null), 2500); } } } function handleScannerError(err: Error) { setCameraError( err.name === "NotAllowedError" ? "camera access was denied. allow camera permission and reopen this screen." : err.message || "camera failed to start.", ); } async function startCameraScan() { setPhase("scanning"); setCameraError(null); setPdfShortfall(null); // Solid hasn't rendered the