codex-types.ts

8.4 KB · raw

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. Recovery words are never included in the PDF — they live6 * on paper separately by design. See peach_codex_v2.md + peach_codex_addendum.md7 * for the full design doc.8 *9 * These types are the contract between four execution boundaries:10 *  - service worker (`src/background/codex-handlers.ts`)11 *  - popup library (`src/popup/codex/*`)12 *  - popup views (`src/popup/views/CodexGenerateView.tsx`, `CodexRestoreView.tsx`)13 *  - tests (`src/popup/codex/*.test.ts`)14 *15 * Bump `version` (and `ChunkEnvelope.v`) only on breaking changes to the16 * on-paper format. A v2 Codex must always be decodable by future Peach17 * versions.18 */1920import type { VaultEntry } from "./types";2122/** Payload that the Codex encodes. Compressed, chunked, erasure-coded, then printed. */23export interface CodexPayload {24  /** Format version. Increment on breaking changes. */25  version: 2;26  /** 12-char alphanumeric: `PX-XXXX-XXXX`. Identifies a vault across Codex prints. */27  verifyCode: string;28  /** Base64url-encoded JSON `{ a: slotA, b: slotB|null }` — both encrypted vault slots. */29  vaultBlob: string;30  /** Vault last-modified unix timestamp (ms). Used to compare against on-device vault on restore. */31  timestamp: number;32  /** Visible entry count for user confirmation at restore time. */33  entryCount: number;34  /** First 32 chars (128 bits) of SHA-256(vaultBlob), uppercase hex. Verifies reconstruction integrity. AUD-004: was 16 chars; legacy Codexes still verify. */35  vaultFingerprint: string;36  /** When this Codex PDF was generated. Printed in the footer. */37  generatedAt: number;38}3940/**41 * One Aztec barcode's contents. Each envelope is a JSON-encoded chunk of the42 * compressed payload plus a per-chunk integrity hash.43 *44 * Field names are intentionally single letters to keep the encoded JSON45 * compact — every byte saved shrinks the printed page count.46 */47export interface ChunkEnvelope {48  /** Envelope format version. Increment on breaking changes. */49  v: 2;50  /** First 12 chars of `verifyCode` ("PX-XXXX-XXXX"). Lets the scanner reject foreign Codexes. */51  c: string;52  /** Chunk index, 0-based. */53  i: number;54  /** Total chunk count (data + parity). */55  t: number;56  /** True if this is a Reed-Solomon parity shard. */57  p: boolean;58  /** Chunk data, base64url-encoded. */59  d: string;60  /** 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. */61  h: string;62}6364/** Bookkeeping returned alongside the generated PDF. */65export interface CodexManifest {66  verifyCode: string;67  totalChunks: number;68  dataChunks: number;69  parityChunks: number;70  pageCount: number;71  entryCount: number;72  vaultFingerprint: string;73  generatedAt: number;74}7576/** Dependencies the generator needs from the service worker. */77export interface CodexDeps {78  getSlotA: () => Promise<string | null>;79  getSlotB: () => Promise<string | null>;80  getLastModified: () => Promise<string | null>;81  getAllEntries: () => Promise<VaultEntry[]>;82  getVerifyCode: () => Promise<string>;83  isVaultUnlocked: () => boolean;84}8586/**87 * Progress reporter for the generator. Called at named milestones; for the88 * Aztec step `current`/`total` advance per code so the UI can render a bar.89 */90export type CodexProgress = (91  step: "compress" | "erasure" | "aztec" | "verify" | "pdf",92  current?: number,93  total?: number,94) => void;9596/** Payload sent from the popup to the SW to perform the actual vault restoration. */97export interface CodexRestorePayload {98  vaultBlob: string;99  timestamp: number;100  verifyCode: string;101  vaultFingerprint: string;102  entryCount: number;103  masterPassphrase: string;104}105106/** Vault data the SW exposes to the popup so it can build a CodexPayload. */107export interface CodexVaultSnapshot {108  slotA: string | null;109  slotB: string | null;110  lastModified: string | null;111  entryCount: number;112  recoveryWords: string[] | null;113  verifyCode: string;114  vaultUnlocked: boolean;115}116117/**118 * Every Codex error is loud and named. The `code` field is a stable identifier119 * for log analysis; the human-readable message lives in `.message`.120 */121export class CodexError extends Error {122  constructor(123    public code: CodexErrorCode,124    message: string,125  ) {126    super(message);127    this.name = "CodexError";128  }129}130131export type CodexErrorCode =132  | "VAULT_LOCKED"133  | "VAULT_EMPTY"134  | "COMPRESSION_ROUNDTRIP_FAILED"135  | "ERASURE_CODING_VERIFICATION_FAILED"136  | "ERASURE_CODING_CONTENT_MISMATCH"137  | "AZTEC_GENERATION_FAILED"138  | "AZTEC_ROUNDTRIP_FAILED"139  | "AZTEC_CONTENT_MISMATCH"140  | "INSUFFICIENT_CHUNKS"141  | "RECONSTRUCTION_SIMULATION_FAILED"142  | "RECONSTRUCTION_VERIFY_CODE_MISMATCH"143  | "RECONSTRUCTION_FINGERPRINT_MISMATCH"144  | "RECONSTRUCTION_COUNT_MISMATCH"145  | "PDF_GENERATION_FAILED"146  | "RS_LIBRARY_FAILED"147  | "PRO_REQUIRED"148  | "WRONG_PASSPHRASE"149  | "INVALID_VAULT_BLOB"150  | "RESTORE_FAILED";151152// ── Constants — shared across generation and restoration ─────────────────────153154/**155 * Bytes of binary data per chunk. Each chunk becomes one Aztec code.156 *157 * The envelope JSON is: base64url(chunkBytes) + ~100 chars of metadata.158 * base64url expands by 4/3: 800 bytes → 1068 chars → ~1168-char envelope.159 * The empirical bwip-js/ZXing ceiling at eclevel=25 is ~1386 chars; staying160 * ~200 chars under gives reliable headroom across cheap printers, camera161 * angles, and ZXing decode variance. The original 1024-byte value produced162 * ~1468-char envelopes — over the ceiling, causing AZTEC_ROUNDTRIP_FAILED.163 *164 * Old Codexes printed with a different chunk size still restore — the165 * erasure decoder derives shard size from the scanned chunks, not from166 * this constant.167 */168export const CHUNK_SIZE = 800;169170/** Floor on parity shards: a Codex must always survive losing at least 2 codes. */171export const CODEX_MIN_PARITY_SHARDS = 2;172173/** Ceiling on parity shards: caps printing overhead for very large vaults. */174export const CODEX_MAX_PARITY_SHARDS = 20;175176/** Target ratio of parity to data shards (5%). */177export const CODEX_PARITY_RATIO = 0.05;178179/**180 * Number of parity shards for a Codex with `dataShards` data shards. Scales181 * with vault size — small vaults pin to the minimum, huge vaults cap at the182 * maximum. Survives the loss of up to the returned count of codes anywhere183 * in the printed document. The function is monotonic non-decreasing in184 * `dataShards`, which `deriveShardCounts` exploits to invert (total → data, parity).185 *186 * Examples:187 *   13 → 2   (minimum floor)188 *   56 → 3   (5% bracket)189 *   213 → 11190 *   660 → 20 (maximum cap)191 */192export function computeParityShards(dataShards: number): number {193  return Math.min(194    CODEX_MAX_PARITY_SHARDS,195    Math.max(196      CODEX_MIN_PARITY_SHARDS,197      Math.ceil(dataShards * CODEX_PARITY_RATIO),198    ),199  );200}201202/**203 * Recover (dataShards, parityShards) from a total chunk count printed on a204 * Codex. Restoration sees only `ChunkEnvelope.t` from scanned codes — there205 * is no manifest to consult — so we invert `computeParityShards` by trying206 * each candidate parity count in [MIN, MAX] and picking the one that round207 * trips. Monotonicity of `computeParityShards` guarantees a unique solution.208 */209export function deriveShardCounts(totalChunks: number): {210  dataShards: number;211  parityShards: number;212} {213  for (214    let parityShards = CODEX_MIN_PARITY_SHARDS;215    parityShards <= CODEX_MAX_PARITY_SHARDS;216    parityShards++217  ) {218    const dataShards = totalChunks - parityShards;219    if (dataShards < 1) continue;220    if (computeParityShards(dataShards) === parityShards) {221      return { dataShards, parityShards };222    }223  }224  throw new CodexError(225    "ERASURE_CODING_VERIFICATION_FAILED",226    `Cannot derive shard layout for totalChunks=${totalChunks}`,227  );228}229230/** Aztec error-correction percentage. 25% balances barcode density vs. resilience. */231export const AZTEC_ECC_PERCENT = 25;232233/** PDF layout: 6 columns × 6 rows = 36 codes per A4 page. */234export const CODES_PER_ROW = 6;235export const CODES_PER_COL = 6;236export const CODES_PER_PAGE = CODES_PER_ROW * CODES_PER_COL;237238/** Stable storage keys for Codex bookkeeping. */239export const STORAGE_KEY_VERIFY_CODE = "peach_codex_verify_code";240export const STORAGE_KEY_LAST_GENERATED = "peach_codex_last_generated";241export const STORAGE_KEY_LAST_RESTORED = "peach_last_restored_at";