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}5051525354555657export function tryBase64UrlToBytes(s: string): Uint8Array | null {58 try {59 return base64UrlToBytes(s);60 } catch {61 return null;62 }63}646566676869707172export async function sha256Hex(bytes: Uint8Array): Promise<string> {73 74 75 76 const owned = new Uint8Array(bytes.byteLength);77 owned.set(bytes);78 const hash = await crypto.subtle.digest("SHA-256", owned);79 const view = new Uint8Array(hash);80 let hex = "";81 for (let i = 0; i < view.length; i++) {82 hex += view[i]!.toString(16).padStart(2, "0");83 }84 return hex.toUpperCase();85}8687888990919293949596979899export async function buildChunkEnvelope(100 chunkBytes: Uint8Array,101 index: number,102 total: number,103 isParity: boolean,104 verifyCode: string,105 stripe: number = 0,106 stripeCount: number = 1,107): Promise<ChunkEnvelope> {108 const hash = (await sha256Hex(chunkBytes)).slice(0, 32);109 return {110 v: 3,111 c: verifyCode,112 s: stripe,113 S: stripeCount,114 i: index,115 t: total,116 p: isParity,117 d: bytesToBase64Url(chunkBytes),118 h: hash,119 };120}121122123124125126127export async function verifyChunkIntegrity(envelope: ChunkEnvelope): Promise<boolean> {128 if (envelope.h.length !== 32) return false;129 const bytes = base64UrlToBytes(envelope.d);130 const fullHash = await sha256Hex(bytes);131 const expected = fullHash.slice(0, envelope.h.length);132 return expected === envelope.h;133}134135136137138139140141142const VERIFY_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";143144145const VERIFY_CODE_PATTERN = /^PX-[A-Z0-9]{4}-[A-Z0-9]{4}$/;146147148149150151152153154155156export async function getOrCreateCodexVerifyCode(): Promise<string> {157 const existing = await readStoredVerifyCode();158 if (existing && VERIFY_CODE_PATTERN.test(existing)) return existing;159160 const fresh = generateVerifyCode();161 await writeStoredVerifyCode(fresh);162 return fresh;163}164165function generateVerifyCode(): string {166 const bytes = new Uint8Array(8);167 crypto.getRandomValues(bytes);168 const chars: string[] = [];169 for (let i = 0; i < 8; i++) {170 171 172 chars.push(VERIFY_CODE_ALPHABET[bytes[i]! % VERIFY_CODE_ALPHABET.length]!);173 }174 return `PX-${chars.slice(0, 4).join("")}-${chars.slice(4, 8).join("")}`;175}176177async function readStoredVerifyCode(): Promise<string | null> {178 if (typeof chrome === "undefined" || !chrome.storage?.local) return null;179 const result = await chrome.storage.local.get(STORAGE_KEY_VERIFY_CODE);180 const value = (result as Record<string, unknown>)[STORAGE_KEY_VERIFY_CODE];181 return typeof value === "string" ? value : null;182}183184async function writeStoredVerifyCode(code: string): Promise<void> {185 if (typeof chrome === "undefined" || !chrome.storage?.local) {186 throw new CodexError(187 "PDF_GENERATION_FAILED",188 "chrome.storage.local is unavailable; cannot persist Codex verify code",189 );190 }191 await chrome.storage.local.set({ [STORAGE_KEY_VERIFY_CODE]: code });192}193194195export async function clearVerifyCode(): Promise<void> {196 if (typeof chrome === "undefined" || !chrome.storage?.local) return;197 await chrome.storage.local.remove(STORAGE_KEY_VERIFY_CODE);198}