/** * codex-types.ts — shared shapes for the Peach Codex. * * The Codex is a printed A4 PDF that encodes the user's encrypted vault as * Aztec barcodes. Recovery words are never included in the PDF — they live * on paper separately by design. See peach_codex_v2.md + peach_codex_addendum.md * for the full design doc. * * These types are the contract between four execution boundaries: * - service worker (`src/background/codex-handlers.ts`) * - popup library (`src/popup/codex/*`) * - popup views (`src/popup/views/CodexGenerateView.tsx`, `CodexRestoreView.tsx`) * - tests (`src/popup/codex/*.test.ts`) * * Bump `version` (and `ChunkEnvelope.v`) only on breaking changes to the * on-paper format. A v2 Codex must always be decodable by future Peach * versions. */ import type { VaultEntry } from "./types"; /** Payload that the Codex encodes. Compressed, chunked, erasure-coded, then printed. */ export interface CodexPayload { /** Format version. Increment on breaking changes. */ version: 2; /** 12-char alphanumeric: `PX-XXXX-XXXX`. Identifies a vault across Codex prints. */ verifyCode: string; /** Base64url-encoded JSON `{ a: slotA, b: slotB|null }` — both encrypted vault slots. */ vaultBlob: string; /** Vault last-modified unix timestamp (ms). Used to compare against on-device vault on restore. */ timestamp: number; /** Visible entry count for user confirmation at restore time. */ entryCount: number; /** First 32 chars (128 bits) of SHA-256(vaultBlob), uppercase hex. Verifies reconstruction integrity. AUD-004: was 16 chars; legacy Codexes still verify. */ vaultFingerprint: string; /** When this Codex PDF was generated. Printed in the footer. */ generatedAt: number; } /** * One Aztec barcode's contents. Each envelope is a JSON-encoded chunk of the * compressed payload plus a per-chunk integrity hash. * * Field names are intentionally single letters to keep the encoded JSON * compact — every byte saved shrinks the printed page count. */ export interface ChunkEnvelope { /** Envelope format version. Increment on breaking changes. */ v: 2; /** First 12 chars of `verifyCode` ("PX-XXXX-XXXX"). Lets the scanner reject foreign Codexes. */ c: string; /** Chunk index, 0-based. */ i: number; /** Total chunk count (data + parity). */ t: number; /** True if this is a Reed-Solomon parity shard. */ p: boolean; /** Chunk data, base64url-encoded. */ d: string; /** First 32 chars (128 bits) of SHA-256(rawChunkBytes), hex. Catches per-chunk corruption with ≤2^-128 collision odds. AUD-004: was 16 chars; legacy Codexes still verify. */ h: string; } /** Bookkeeping returned alongside the generated PDF. */ export interface CodexManifest { verifyCode: string; totalChunks: number; dataChunks: number; parityChunks: number; pageCount: number; entryCount: number; vaultFingerprint: string; generatedAt: number; } /** Dependencies the generator needs from the service worker. */ export interface CodexDeps { getSlotA: () => Promise; getSlotB: () => Promise; getLastModified: () => Promise; getAllEntries: () => Promise; getVerifyCode: () => Promise; isVaultUnlocked: () => boolean; } /** * Progress reporter for the generator. Called at named milestones; for the * Aztec step `current`/`total` advance per code so the UI can render a bar. */ export type CodexProgress = ( step: "compress" | "erasure" | "aztec" | "verify" | "pdf", current?: number, total?: number, ) => void; /** Payload sent from the popup to the SW to perform the actual vault restoration. */ export interface CodexRestorePayload { vaultBlob: string; timestamp: number; verifyCode: string; vaultFingerprint: string; entryCount: number; masterPassphrase: string; } /** Vault data the SW exposes to the popup so it can build a CodexPayload. */ export interface CodexVaultSnapshot { slotA: string | null; slotB: string | null; lastModified: string | null; entryCount: number; recoveryWords: string[] | null; verifyCode: string; vaultUnlocked: boolean; } /** * Every Codex error is loud and named. The `code` field is a stable identifier * for log analysis; the human-readable message lives in `.message`. */ export class CodexError extends Error { constructor( public code: CodexErrorCode, message: string, ) { super(message); this.name = "CodexError"; } } export type CodexErrorCode = | "VAULT_LOCKED" | "VAULT_EMPTY" | "COMPRESSION_ROUNDTRIP_FAILED" | "ERASURE_CODING_VERIFICATION_FAILED" | "ERASURE_CODING_CONTENT_MISMATCH" | "AZTEC_GENERATION_FAILED" | "AZTEC_ROUNDTRIP_FAILED" | "AZTEC_CONTENT_MISMATCH" | "INSUFFICIENT_CHUNKS" | "RECONSTRUCTION_SIMULATION_FAILED" | "RECONSTRUCTION_VERIFY_CODE_MISMATCH" | "RECONSTRUCTION_FINGERPRINT_MISMATCH" | "RECONSTRUCTION_COUNT_MISMATCH" | "PDF_GENERATION_FAILED" | "RS_LIBRARY_FAILED" | "PRO_REQUIRED" | "WRONG_PASSPHRASE" | "INVALID_VAULT_BLOB" | "RESTORE_FAILED"; // ── Constants — shared across generation and restoration ───────────────────── /** * Bytes of binary data per chunk. Each chunk becomes one Aztec code. * * 3200 bytes base64url-encodes to ~4267 chars; combined with the JSON envelope * overhead (~100 chars) the printed text is ~4367 chars — well under the * bwip-js limit of 4999 chars at eclevel=25. DO NOT increase without re-running * a bwip-js capacity test for the densest envelope. * * Sized to drive a typical vault down to ~6 A4 pages instead of ~24. Old * Codexes printed with a smaller chunk size still restore — the erasure * decoder derives shard size from the scanned chunks, not from this constant. */ export const CHUNK_SIZE = 3200; /** * Number of parity shards. With 2 parity shards, the Codex survives the loss * of any 2 chunks (torn corner, coffee stain over one code, etc.) without * data loss. Raising this beyond 2 grows the page count meaningfully. */ export const PARITY_SHARDS = 2; /** Aztec error-correction percentage. 25% balances barcode density vs. resilience. */ export const AZTEC_ECC_PERCENT = 25; /** PDF layout: 6 columns × 6 rows = 36 codes per A4 page. */ export const CODES_PER_ROW = 6; export const CODES_PER_COL = 6; export const CODES_PER_PAGE = CODES_PER_ROW * CODES_PER_COL; /** Stable storage keys for Codex bookkeeping. */ export const STORAGE_KEY_VERIFY_CODE = "peach_codex_verify_code"; export const STORAGE_KEY_LAST_GENERATED = "peach_codex_last_generated"; export const STORAGE_KEY_LAST_RESTORED = "peach_last_restored_at";