codex-types.ts
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 */1819import type { VaultEntry } from "./types";2021/** Payload that the Codex encodes. Compressed, chunked, erasure-coded, then printed. */22export 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}4041/**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 */48export 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}6465/** Bookkeeping returned alongside the generated PDF. */66export 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}7778/** Dependencies the generator needs from the service worker. */79export 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}8889export interface CodexOptions {90 includeRecoveryWords: boolean;91}9293/** Payload sent from the popup to the SW to perform the actual vault restoration. */94export interface CodexRestorePayload {95 vaultBlob: string;96 timestamp: number;97 verifyCode: string;98 vaultFingerprint: string;99 entryCount: number;100 masterPassphrase: string;101}102103/** Vault data the SW exposes to the popup so it can build a CodexPayload. */104export 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}113114/**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 */118export class CodexError extends Error {119 constructor(120 public code: CodexErrorCode,121 message: string,122 ) {123 super(message);124 this.name = "CodexError";125 }126}127128export 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";150151// ── Constants — shared across generation and restoration ─────────────────────152153/**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 */160export const CHUNK_SIZE = 1024;161162/**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 */167export const PARITY_SHARDS = 2;168169/** Aztec error-correction percentage. 25% balances barcode density vs. resilience. */170export const AZTEC_ECC_PERCENT = 25;171172/** PDF layout: 6 columns × 4 rows = 24 codes per A4 page. */173export const CODES_PER_ROW = 6;174export const CODES_PER_COL = 4;175export const CODES_PER_PAGE = CODES_PER_ROW * CODES_PER_COL;176177/** Stable storage keys for Codex bookkeeping. */178export const STORAGE_KEY_VERIFY_CODE = "peach_codex_verify_code";179export const STORAGE_KEY_LAST_GENERATED = "peach_codex_last_generated";180export const STORAGE_KEY_LAST_RESTORED = "peach_last_restored_at";