123456789101112131415import {16 CodexError,17 STORAGE_KEY_VERIFY_CODE,18 type ChunkEnvelope,19} from "../../shared/codex-types";202122232425262728export function bytesToBase64Url(bytes: Uint8Array): string {29 let binary = "";30 for (let i = 0; i < bytes.length; i++) {31 binary += String.fromCharCode(bytes[i]!);32 }33 const b64 = (typeof btoa !== "undefined" ? btoa(binary) : Buffer.from(binary, "binary").toString("base64"));34 return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");35}363738export function base64UrlToBytes(s: string): Uint8Array {39 const b64 = s.replace(/-/g, "+").replace(/_/g, "/");40 41 const padded = b64 + "=".repeat((4 - (b64.length % 4)) % 4);42 const binary =43 typeof atob !== "undefined"44 ? atob(padded)45 : Buffer.from(padded, "base64").toString("binary");46 const out = new Uint8Array(binary.length);47 for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);48 return out;49}505152535455565758export async function sha256Hex(bytes: Uint8Array): Promise<string> {59 60 61 62 const owned = new Uint8Array(bytes.byteLength);63 owned.set(bytes);64 const hash = await crypto.subtle.digest("SHA-256", owned);65 const view = new Uint8Array(hash);66 let hex = "";67 for (let i = 0; i < view.length; i++) {68 hex += view[i]!.toString(16).padStart(2, "0");69 }70 return hex.toUpperCase();71}72737475767778798081export async function buildChunkEnvelope(82 chunkBytes: Uint8Array,83 index: number,84 total: number,85 isParity: boolean,86 verifyCode: string,87): Promise<ChunkEnvelope> {88 const hash = (await sha256Hex(chunkBytes)).slice(0, 32);89 return {90 v: 2,91 c: verifyCode,92 i: index,93 t: total,94 p: isParity,95 d: bytesToBase64Url(chunkBytes),96 h: hash,97 };98}99100101102103104105export async function verifyChunkIntegrity(envelope: ChunkEnvelope): Promise<boolean> {106 107 108 109 110 if (envelope.h.length !== 16 && envelope.h.length !== 32) return false;111 const bytes = base64UrlToBytes(envelope.d);112 const fullHash = await sha256Hex(bytes);113 const expected = fullHash.slice(0, envelope.h.length);114 return expected === envelope.h;115}116117118119120121122123124const VERIFY_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";125126127const VERIFY_CODE_PATTERN = /^PX-[A-Z0-9]{4}-[A-Z0-9]{4}$/;128129130131132133134135136137138export async function getOrCreateCodexVerifyCode(): Promise<string> {139 const existing = await readStoredVerifyCode();140 if (existing && VERIFY_CODE_PATTERN.test(existing)) return existing;141142 const fresh = generateVerifyCode();143 await writeStoredVerifyCode(fresh);144 return fresh;145}146147function generateVerifyCode(): string {148 const bytes = new Uint8Array(8);149 crypto.getRandomValues(bytes);150 const chars: string[] = [];151 for (let i = 0; i < 8; i++) {152 153 154 chars.push(VERIFY_CODE_ALPHABET[bytes[i]! % VERIFY_CODE_ALPHABET.length]!);155 }156 return `PX-${chars.slice(0, 4).join("")}-${chars.slice(4, 8).join("")}`;157}158159async function readStoredVerifyCode(): Promise<string | null> {160 if (typeof chrome === "undefined" || !chrome.storage?.local) return null;161 const result = await chrome.storage.local.get(STORAGE_KEY_VERIFY_CODE);162 const value = (result as Record<string, unknown>)[STORAGE_KEY_VERIFY_CODE];163 return typeof value === "string" ? value : null;164}165166async function writeStoredVerifyCode(code: string): Promise<void> {167 if (typeof chrome === "undefined" || !chrome.storage?.local) {168 throw new CodexError(169 "PDF_GENERATION_FAILED",170 "chrome.storage.local is unavailable; cannot persist Codex verify code",171 );172 }173 await chrome.storage.local.set({ [STORAGE_KEY_VERIFY_CODE]: code });174}175176177export async function clearVerifyCode(): Promise<void> {178 if (typeof chrome === "undefined" || !chrome.storage?.local) return;179 await chrome.storage.local.remove(STORAGE_KEY_VERIFY_CODE);180}