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}7273747576777879808182838485export async function buildChunkEnvelope(86 chunkBytes: Uint8Array,87 index: number,88 total: number,89 isParity: boolean,90 verifyCode: string,91 stripe: number = 0,92 stripeCount: number = 1,93): Promise<ChunkEnvelope> {94 const hash = (await sha256Hex(chunkBytes)).slice(0, 32);95 return {96 v: 3,97 c: verifyCode,98 s: stripe,99 S: stripeCount,100 i: index,101 t: total,102 p: isParity,103 d: bytesToBase64Url(chunkBytes),104 h: hash,105 };106}107108109110111112113export async function verifyChunkIntegrity(envelope: ChunkEnvelope): Promise<boolean> {114 115 116 117 118 if (envelope.h.length !== 16 && envelope.h.length !== 32) return false;119 const bytes = base64UrlToBytes(envelope.d);120 const fullHash = await sha256Hex(bytes);121 const expected = fullHash.slice(0, envelope.h.length);122 return expected === envelope.h;123}124125126127128129130131132const VERIFY_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";133134135const VERIFY_CODE_PATTERN = /^PX-[A-Z0-9]{4}-[A-Z0-9]{4}$/;136137138139140141142143144145146export async function getOrCreateCodexVerifyCode(): Promise<string> {147 const existing = await readStoredVerifyCode();148 if (existing && VERIFY_CODE_PATTERN.test(existing)) return existing;149150 const fresh = generateVerifyCode();151 await writeStoredVerifyCode(fresh);152 return fresh;153}154155function generateVerifyCode(): string {156 const bytes = new Uint8Array(8);157 crypto.getRandomValues(bytes);158 const chars: string[] = [];159 for (let i = 0; i < 8; i++) {160 161 162 chars.push(VERIFY_CODE_ALPHABET[bytes[i]! % VERIFY_CODE_ALPHABET.length]!);163 }164 return `PX-${chars.slice(0, 4).join("")}-${chars.slice(4, 8).join("")}`;165}166167async function readStoredVerifyCode(): Promise<string | null> {168 if (typeof chrome === "undefined" || !chrome.storage?.local) return null;169 const result = await chrome.storage.local.get(STORAGE_KEY_VERIFY_CODE);170 const value = (result as Record<string, unknown>)[STORAGE_KEY_VERIFY_CODE];171 return typeof value === "string" ? value : null;172}173174async function writeStoredVerifyCode(code: string): Promise<void> {175 if (typeof chrome === "undefined" || !chrome.storage?.local) {176 throw new CodexError(177 "PDF_GENERATION_FAILED",178 "chrome.storage.local is unavailable; cannot persist Codex verify code",179 );180 }181 await chrome.storage.local.set({ [STORAGE_KEY_VERIFY_CODE]: code });182}183184185export async function clearVerifyCode(): Promise<void> {186 if (typeof chrome === "undefined" || !chrome.storage?.local) return;187 await chrome.storage.local.remove(STORAGE_KEY_VERIFY_CODE);188}