53fdf206c18d
scaffold codex types, RS WASM loader, public asset, deps; bump to v2.42.0
- time
- 2026-05-15 17:17 UTC · 5 months ago
- machine
- koh2
- parent
- 096884e49b99
↓ peach-browser-53fdf206.tar.gzatlas at this save
files changed7 files
manifest.firefox.json+1 −1manifest.json+1 −1package-lock.json+139 −7package.json+7 −1public/codex/reed_solomon_erasure_bg.wasmbinarysrc/popup/codex/rs-loader.ts+69 −0src/shared/codex-types.ts+180 −0manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach",4- "version": "2.41.4",4+ "version": "2.42.0",55 "description": "Zero-knowledge password manager",66 "browser_specific_settings": {77 "gecko": {manifest.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach",4- "version": "2.41.4",4+ "version": "2.42.0",55 "description": "Zero-knowledge password manager",66 "action": {77 "default_popup": "src/popup/index.html",package-lock.json+139 −7
@@ -1,24 +1,29 @@11 {22 "name": "peach",3- "version": "2.35.1",3+ "version": "2.41.3",44 "lockfileVersion": 3,55 "requires": true,66 "packages": {77 "": {88 "name": "peach",9- "version": "2.35.1",9+ "version": "2.41.3",1010 "dependencies": {1111 "@bokuweb/zstd-wasm": "^0.0.27",12+ "@digitaldefiance/reed-solomon-erasure.wasm": "^1.0.2",1213 "@stablelib/blake2s": "^2.0.1",1314 "@stablelib/chacha20poly1305": "^2.0.1",1415 "@stablelib/hkdf": "^2.0.1",1516 "@stablelib/hmac": "^2.0.1",1617 "@stablelib/sha256": "^2.0.1",1718 "@stablelib/x25519": "^2.0.1",18- "@types/qrcode": "^1.5.6",19+ "@zxing/library": "^0.23.0",20+ "bwip-js": "^4.10.1",1921 "hash-wasm": "^4.12.0",22+ "jsqr": "^1.4.0",2023 "otpauth": "^9.5.0",24+ "pako": "^2.1.0",2125 "papaparse": "^5.5.3",26+ "pdf-lib": "^1.17.1",2227 "qrcode": "^1.5.4",2328 "tldts": "^7.0.28",2429 "zxcvbn": "^4.4.2"@@ -28,7 +33,9 @@2833 "@playwright/test": "^1.59.1",2934 "@types/chrome": "^0.1.40",3035 "@types/node": "^22.0.0",36+ "@types/pako": "^2.0.4",3137 "@types/papaparse": "^5.5.2",38+ "@types/qrcode": "^1.5.6",3239 "@types/zxcvbn": "^4.4.5",3340 "@vitest/ui": "^4.1.4",3441 "jsdom": "^29.0.2",@@ -620,6 +627,15 @@620627 }621628 }622629 },630+ "node_modules/@digitaldefiance/reed-solomon-erasure.wasm": {631+ "version": "1.0.2",632+ "resolved": "https://registry.npmjs.org/@digitaldefiance/reed-solomon-erasure.wasm/-/reed-solomon-erasure.wasm-1.0.2.tgz",633+ "integrity": "sha512-Qltu+Vzj/x07rryH8wQo+5uMTtIxFEYi88phWgX+V5qVHdTcjMSSHXr6Ay24dPvsc6Q7NsdYnr28vQNkspxkfA==",634+ "license": "MIT",635+ "engines": {636+ "node": ">=18.0.0"637+ }638+ },623639 "node_modules/@emnapi/core": {624640 "version": "1.9.2",625641 "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",@@ -999,6 +1015,36 @@9991015 "url": "https://github.com/sponsors/Boshen"10001016 }10011017 },1018+ "node_modules/@pdf-lib/standard-fonts": {1019+ "version": "1.0.0",1020+ "resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz",1021+ "integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==",1022+ "license": "MIT",1023+ "dependencies": {1024+ "pako": "^1.0.6"1025+ }1026+ },1027+ "node_modules/@pdf-lib/standard-fonts/node_modules/pako": {1028+ "version": "1.0.11",1029+ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",1030+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",1031+ "license": "(MIT AND Zlib)"1032+ },1033+ "node_modules/@pdf-lib/upng": {1034+ "version": "1.0.1",1035+ "resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz",1036+ "integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==",1037+ "license": "MIT",1038+ "dependencies": {1039+ "pako": "^1.0.10"1040+ }1041+ },1042+ "node_modules/@pdf-lib/upng/node_modules/pako": {1043+ "version": "1.0.11",1044+ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",1045+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",1046+ "license": "(MIT AND Zlib)"1047+ },10021048 "node_modules/@playwright/test": {10031049 "version": "1.59.1",10041050 "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz",@@ -1632,11 +1678,19 @@16321678 "version": "22.19.17",16331679 "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz",16341680 "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==",1681+ "dev": true,16351682 "license": "MIT",16361683 "dependencies": {16371684 "undici-types": "~6.21.0"16381685 }16391686 },1687+ "node_modules/@types/pako": {1688+ "version": "2.0.4",1689+ "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz",1690+ "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==",1691+ "dev": true,1692+ "license": "MIT"1693+ },16401694 "node_modules/@types/papaparse": {16411695 "version": "5.5.2",16421696 "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.5.2.tgz",@@ -1651,6 +1705,7 @@16511705 "version": "1.5.6",16521706 "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz",16531707 "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==",1708+ "dev": true,16541709 "license": "MIT",16551710 "dependencies": {16561711 "@types/node": "*"@@ -1839,6 +1894,28 @@18391894 "dev": true,18401895 "license": "BSD-3-Clause"18411896 },1897+ "node_modules/@zxing/library": {1898+ "version": "0.23.0",1899+ "resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.23.0.tgz",1900+ "integrity": "sha512-6fkkoFwP8CHxl6ugnPsj74PLJgX2iRv5zczGAyt5OBzQgxFhuhF0NCEc4t4OvSr8xAv2MRLlI0Iu9ZGDZQ2urA==",1901+ "license": "Apache-2.0",1902+ "dependencies": {1903+ "ts-custom-error": "^3.3.1"1904+ },1905+ "engines": {1906+ "node": ">= 24.0.0"1907+ },1908+ "optionalDependencies": {1909+ "@zxing/text-encoding": "~0.9.0"1910+ }1911+ },1912+ "node_modules/@zxing/text-encoding": {1913+ "version": "0.9.0",1914+ "resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz",1915+ "integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==",1916+ "license": "(Unlicense OR Apache-2.0)",1917+ "optional": true1918+ },18421919 "node_modules/acorn": {18431920 "version": "8.16.0",18441921 "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",@@ -2515,6 +2592,15 @@25152592 "url": "https://github.com/sponsors/sindresorhus"25162593 }25172594 },2595+ "node_modules/bwip-js": {2596+ "version": "4.10.1",2597+ "resolved": "https://registry.npmjs.org/bwip-js/-/bwip-js-4.10.1.tgz",2598+ "integrity": "sha512-I/cEPiXsu7dRCp78PpVY4gdIXmbH752n8dMC+DStM77XPkrzeathdYrjnZ/i/vZPIxXTUWc+JxgJ/MvbodqPLA==",2599+ "license": "MIT",2600+ "bin": {2601+ "bwip-js": "bin/bwip-js.js"2602+ }2603+ },25182604 "node_modules/callsites": {25192605 "version": "3.1.0",25202606 "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",@@ -4570,6 +4656,12 @@45704656 "graceful-fs": "^4.1.6"45714657 }45724658 },4659+ "node_modules/jsqr": {4660+ "version": "1.4.0",4661+ "resolved": "https://registry.npmjs.org/jsqr/-/jsqr-1.4.0.tgz",4662+ "integrity": "sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A==",4663+ "license": "Apache-2.0"4664+ },45734665 "node_modules/jszip": {45744666 "version": "3.10.1",45754667 "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",@@ -4583,6 +4675,13 @@45834675 "setimmediate": "^1.0.5"45844676 }45854677 },4678+ "node_modules/jszip/node_modules/pako": {4679+ "version": "1.0.11",4680+ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",4681+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",4682+ "dev": true,4683+ "license": "(MIT AND Zlib)"4684+ },45864685 "node_modules/keyv": {45874686 "version": "4.5.4",45884687 "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",@@ -5352,10 +5451,9 @@53525451 }53535452 },53545453 "node_modules/pako": {5355- "version": "1.0.11",5356- "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",5357- "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",5358- "dev": true,5454+ "version": "2.1.0",5455+ "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",5456+ "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",53595457 "license": "(MIT AND Zlib)"53605458 },53615459 "node_modules/papaparse": {@@ -5486,6 +5584,30 @@54865584 "dev": true,54875585 "license": "MIT"54885586 },5587+ "node_modules/pdf-lib": {5588+ "version": "1.17.1",5589+ "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz",5590+ "integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==",5591+ "license": "MIT",5592+ "dependencies": {5593+ "@pdf-lib/standard-fonts": "^1.0.0",5594+ "@pdf-lib/upng": "^1.0.1",5595+ "pako": "^1.0.11",5596+ "tslib": "^1.11.1"5597+ }5598+ },5599+ "node_modules/pdf-lib/node_modules/pako": {5600+ "version": "1.0.11",5601+ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",5602+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",5603+ "license": "(MIT AND Zlib)"5604+ },5605+ "node_modules/pdf-lib/node_modules/tslib": {5606+ "version": "1.14.1",5607+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",5608+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",5609+ "license": "0BSD"5610+ },54895611 "node_modules/pend": {54905612 "version": "1.2.0",54915613 "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",@@ -6595,6 +6717,15 @@65956717 "node": ">=20"65966718 }65976719 },6720+ "node_modules/ts-custom-error": {6721+ "version": "3.3.1",6722+ "resolved": "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.3.1.tgz",6723+ "integrity": "sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A==",6724+ "license": "MIT",6725+ "engines": {6726+ "node": ">=14.0.0"6727+ }6728+ },65986729 "node_modules/tslib": {65996730 "version": "2.8.1",66006731 "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",@@ -6663,6 +6794,7 @@66636794 "version": "6.21.0",66646795 "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",66656796 "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",6797+ "dev": true,66666798 "license": "MIT"66676799 },66686800 "node_modules/universalify": {package.json+7 −1
@@ -1,6 +1,6 @@11 {22 "name": "peach",3- "version": "2.41.3",3+ "version": "2.42.0",44 "description": "Zero-knowledge password manager",55 "type": "module",66 "scripts": {@@ -19,16 +19,21 @@1919 },2020 "dependencies": {2121 "@bokuweb/zstd-wasm": "^0.0.27",22+ "@digitaldefiance/reed-solomon-erasure.wasm": "^1.0.2",2223 "@stablelib/blake2s": "^2.0.1",2324 "@stablelib/chacha20poly1305": "^2.0.1",2425 "@stablelib/hkdf": "^2.0.1",2526 "@stablelib/hmac": "^2.0.1",2627 "@stablelib/sha256": "^2.0.1",2728 "@stablelib/x25519": "^2.0.1",29+ "@zxing/library": "^0.23.0",30+ "bwip-js": "^4.10.1",2831 "hash-wasm": "^4.12.0",2932 "jsqr": "^1.4.0",3033 "otpauth": "^9.5.0",34+ "pako": "^2.1.0",3135 "papaparse": "^5.5.3",36+ "pdf-lib": "^1.17.1",3237 "qrcode": "^1.5.4",3338 "tldts": "^7.0.28",3439 "zxcvbn": "^4.4.2"@@ -38,6 +43,7 @@3843 "@playwright/test": "^1.59.1",3944 "@types/chrome": "^0.1.40",4045 "@types/node": "^22.0.0",46+ "@types/pako": "^2.0.4",4147 "@types/papaparse": "^5.5.2",4248 "@types/qrcode": "^1.5.6",4349 "@types/zxcvbn": "^4.4.5",src/popup/codex/rs-loader.ts+69 −0
@@ -0,0 +1,69 @@1+/**2+ * rs-loader.ts — load the Reed-Solomon erasure WASM module.3+ *4+ * The same code runs in two contexts:5+ * - Extension popup (browser): fetch the WASM via `chrome.runtime.getURL`6+ * so the asset path resolves against the extension package. The WASM7+ * file is shipped from `public/codex/reed_solomon_erasure_bg.wasm`.8+ * - Vitest (Node + jsdom): read the WASM bytes from `node_modules`9+ * synchronously. We avoid the library's `fromCurrentDirectory*` helpers10+ * because they probe `__dirname`, which is unreliable when the test11+ * file is bundled by Vite.12+ *13+ * The instance is cached — WASM instantiation is non-trivial work and the14+ * library is safe to reuse across encode / reconstruct calls.15+ */16+17+import { ReedSolomonErasure } from "@digitaldefiance/reed-solomon-erasure.wasm";18+import { CodexError } from "../../shared/codex-types";19+20+const WASM_RELATIVE_PATH = "codex/reed_solomon_erasure_bg.wasm";21+22+let cached: Promise<ReedSolomonErasure> | null = null;23+24+export function getReedSolomon(): Promise<ReedSolomonErasure> {25+ if (!cached) cached = loadReedSolomon();26+ return cached;27+}28+29+async function loadReedSolomon(): Promise<ReedSolomonErasure> {30+ try {31+ if (isExtensionPopup()) {32+ const url = chrome.runtime.getURL(WASM_RELATIVE_PATH);33+ const response = await fetch(url);34+ if (!response.ok) {35+ throw new Error(`fetch ${url} → ${response.status}`);36+ }37+ const bytes = new Uint8Array(await response.arrayBuffer());38+ return ReedSolomonErasure.fromBytes(bytes);39+ }40+41+ // Node / vitest path. Dynamic import so the bundler does not try to42+ // resolve `node:fs` for the browser build.43+ const fs = await import("node:fs");44+ const path = await import("node:path");45+ const wasmPath = path.resolve(46+ "node_modules/@digitaldefiance/reed-solomon-erasure.wasm/dist/reed_solomon_erasure_bg.wasm",47+ );48+ const bytes = fs.readFileSync(wasmPath);49+ return ReedSolomonErasure.fromBytes(bytes);50+ } catch (err) {51+ throw new CodexError(52+ "RS_LIBRARY_FAILED",53+ `Failed to load Reed-Solomon WASM module: ${err instanceof Error ? err.message : String(err)}`,54+ );55+ }56+}57+58+function isExtensionPopup(): boolean {59+ return (60+ typeof chrome !== "undefined" &&61+ typeof chrome.runtime !== "undefined" &&62+ typeof chrome.runtime.getURL === "function"63+ );64+}65+66+/** Reset the cache. Test-only — never called from production code. */67+export function _resetReedSolomonCacheForTest(): void {68+ cached = null;69+}src/shared/codex-types.ts+180 −0
@@ -0,0 +1,180 @@1+/**2+ * codex-types.ts — shared shapes for the Peach Codex.3+ *4+ * The Codex is a printed A4 PDF that encodes the user's encrypted vault as5+ * Aztec barcodes plus optional recovery words. See /Users/june/Downloads/peach_codex_v2.md6+ * for the full design doc.7+ *8+ * These types are the contract between four execution boundaries:9+ * - service worker (`src/background/codex-handlers.ts`)10+ * - popup library (`src/popup/codex/*`)11+ * - popup views (`src/popup/views/CodexGenerateView.tsx`, `CodexRestoreView.tsx`)12+ * - tests (`src/popup/codex/*.test.ts`)13+ *14+ * Bump `version` (and `ChunkEnvelope.v`) only on breaking changes to the15+ * on-paper format. A v2 Codex must always be decodable by future Peach16+ * versions.17+ */18+19+import type { VaultEntry } from "./types";20+21+/** Payload that the Codex encodes. Compressed, chunked, erasure-coded, then printed. */22+export interface CodexPayload {23+ /** Format version. Increment on breaking changes. */24+ version: 2;25+ /** 12-char alphanumeric: `PX-XXXX-XXXX`. Identifies a vault across Codex prints. */26+ verifyCode: string;27+ /** Base64url-encoded JSON `{ a: slotA, b: slotB|null }` — both encrypted vault slots. */28+ vaultBlob: string;29+ /** Vault last-modified unix timestamp (ms). Used to compare against on-device vault on restore. */30+ timestamp: number;31+ /** Visible entry count for user confirmation at restore time. */32+ entryCount: number;33+ /** First 16 chars of SHA-256(vaultBlob), uppercase hex. Verifies reconstruction integrity. */34+ vaultFingerprint: string;35+ /** When this Codex PDF was generated. Printed in the footer. */36+ generatedAt: number;37+ /** True if the last page includes the 12 recovery words. */38+ includesRecoveryWords: boolean;39+}40+41+/**42+ * One Aztec barcode's contents. Each envelope is a JSON-encoded chunk of the43+ * compressed payload plus a per-chunk integrity hash.44+ *45+ * Field names are intentionally single letters to keep the encoded JSON46+ * compact — every byte saved shrinks the printed page count.47+ */48+export interface ChunkEnvelope {49+ /** Envelope format version. Increment on breaking changes. */50+ v: 2;51+ /** First 12 chars of `verifyCode` ("PX-XXXX-XXXX"). Lets the scanner reject foreign Codexes. */52+ c: string;53+ /** Chunk index, 0-based. */54+ i: number;55+ /** Total chunk count (data + parity). */56+ t: number;57+ /** True if this is a Reed-Solomon parity shard. */58+ p: boolean;59+ /** Chunk data, base64url-encoded. */60+ d: string;61+ /** First 8 chars of SHA-256(rawChunkBytes), uppercase hex. Catches per-chunk corruption. */62+ h: string;63+}64+65+/** Bookkeeping returned alongside the generated PDF. */66+export interface CodexManifest {67+ verifyCode: string;68+ totalChunks: number;69+ dataChunks: number;70+ parityChunks: number;71+ pageCount: number;72+ entryCount: number;73+ vaultFingerprint: string;74+ generatedAt: number;75+ includesRecoveryWords: boolean;76+}77+78+/** Dependencies the generator needs from the service worker. */79+export interface CodexDeps {80+ getSlotA: () => Promise<string | null>;81+ getSlotB: () => Promise<string | null>;82+ getLastModified: () => Promise<string | null>;83+ getAllEntries: () => Promise<VaultEntry[]>;84+ getRecoveryWords: () => Promise<string[] | null>;85+ getVerifyCode: () => Promise<string>;86+ isVaultUnlocked: () => boolean;87+}88+89+export interface CodexOptions {90+ includeRecoveryWords: boolean;91+}92+93+/** Payload sent from the popup to the SW to perform the actual vault restoration. */94+export interface CodexRestorePayload {95+ vaultBlob: string;96+ timestamp: number;97+ verifyCode: string;98+ vaultFingerprint: string;99+ entryCount: number;100+ masterPassphrase: string;101+}102+103+/** Vault data the SW exposes to the popup so it can build a CodexPayload. */104+export interface CodexVaultSnapshot {105+ slotA: string | null;106+ slotB: string | null;107+ lastModified: string | null;108+ entryCount: number;109+ recoveryWords: string[] | null;110+ verifyCode: string;111+ vaultUnlocked: boolean;112+}113+114+/**115+ * Every Codex error is loud and named. The `code` field is a stable identifier116+ * for log analysis; the human-readable message lives in `.message`.117+ */118+export class CodexError extends Error {119+ constructor(120+ public code: CodexErrorCode,121+ message: string,122+ ) {123+ super(message);124+ this.name = "CodexError";125+ }126+}127+128+export type CodexErrorCode =129+ | "VAULT_LOCKED"130+ | "VAULT_EMPTY"131+ | "RECOVERY_WORDS_MISSING"132+ | "RECOVERY_WORDS_INVALID"133+ | "COMPRESSION_ROUNDTRIP_FAILED"134+ | "ERASURE_CODING_VERIFICATION_FAILED"135+ | "ERASURE_CODING_CONTENT_MISMATCH"136+ | "AZTEC_GENERATION_FAILED"137+ | "AZTEC_ROUNDTRIP_FAILED"138+ | "AZTEC_CONTENT_MISMATCH"139+ | "INSUFFICIENT_CHUNKS"140+ | "RECONSTRUCTION_SIMULATION_FAILED"141+ | "RECONSTRUCTION_VERIFY_CODE_MISMATCH"142+ | "RECONSTRUCTION_FINGERPRINT_MISMATCH"143+ | "RECONSTRUCTION_COUNT_MISMATCH"144+ | "PDF_GENERATION_FAILED"145+ | "RS_LIBRARY_FAILED"146+ | "PRO_REQUIRED"147+ | "WRONG_PASSPHRASE"148+ | "INVALID_VAULT_BLOB"149+ | "RESTORE_FAILED";150+151+// ── Constants — shared across generation and restoration ─────────────────────152+153+/**154+ * Bytes of binary data per chunk. Each chunk becomes one Aztec code; this size155+ * keeps the printed code at ~28mm (CODE_SIZE in PDF rendering) while staying156+ * within Aztec's 1914-byte ECC-25% capacity for compact codes.157+ *158+ * Conservative: leaves headroom for the JSON envelope overhead (~120 bytes).159+ */160+export const CHUNK_SIZE = 1024;161+162+/**163+ * Number of parity shards. With 2 parity shards, the Codex survives the loss164+ * of any 2 chunks (torn corner, coffee stain over one code, etc.) without165+ * data loss. Raising this beyond 2 grows the page count meaningfully.166+ */167+export const PARITY_SHARDS = 2;168+169+/** Aztec error-correction percentage. 25% balances barcode density vs. resilience. */170+export const AZTEC_ECC_PERCENT = 25;171+172+/** PDF layout: 6 columns × 4 rows = 24 codes per A4 page. */173+export const CODES_PER_ROW = 6;174+export const CODES_PER_COL = 4;175+export const CODES_PER_PAGE = CODES_PER_ROW * CODES_PER_COL;176+177+/** Stable storage keys for Codex bookkeeping. */178+export const STORAGE_KEY_VERIFY_CODE = "peach_codex_verify_code";179+export const STORAGE_KEY_LAST_GENERATED = "peach_codex_last_generated";180+export const STORAGE_KEY_LAST_RESTORED = "peach_last_restored_at";