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 async function sha256Hex(bytes: Uint8Array): Promise<string> {58 59 60 61 const owned = new Uint8Array(bytes.byteLength);62 owned.set(bytes);63 const hash = await crypto.subtle.digest("SHA-256", owned);64 const view = new Uint8Array(hash);65 let hex = "";66 for (let i = 0; i < view.length; i++) {67 hex += view[i]!.toString(16).padStart(2, "0");68 }69 return hex.toUpperCase();70}71727374757677787980export async function buildChunkEnvelope(81 chunkBytes: Uint8Array,82 index: number,83 total: number,84 isParity: boolean,85 verifyCode: string,86): Promise<ChunkEnvelope> {87 const hash = (await sha256Hex(chunkBytes)).slice(0, 16);88 return {89 v: 2,90 c: verifyCode,91 i: index,92 t: total,93 p: isParity,94 d: bytesToBase64Url(chunkBytes),95 h: hash,96 };97}9899100101102103104export async function verifyChunkIntegrity(envelope: ChunkEnvelope): Promise<boolean> {105 const bytes = base64UrlToBytes(envelope.d);106 const expected = (await sha256Hex(bytes)).slice(0, 16);107 return expected === envelope.h;108}109110111112113114115116117const VERIFY_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";118119120const VERIFY_CODE_PATTERN = /^PX-[A-Z0-9]{4}-[A-Z0-9]{4}$/;121122123124125126127128129130131export async function getOrCreateCodexVerifyCode(): Promise<string> {132 const existing = await readStoredVerifyCode();133 if (existing && VERIFY_CODE_PATTERN.test(existing)) return existing;134135 const fresh = generateVerifyCode();136 await writeStoredVerifyCode(fresh);137 return fresh;138}139140function generateVerifyCode(): string {141 const bytes = new Uint8Array(8);142 crypto.getRandomValues(bytes);143 const chars: string[] = [];144 for (let i = 0; i < 8; i++) {145 146 147 chars.push(VERIFY_CODE_ALPHABET[bytes[i]! % VERIFY_CODE_ALPHABET.length]!);148 }149 return `PX-${chars.slice(0, 4).join("")}-${chars.slice(4, 8).join("")}`;150}151152async function readStoredVerifyCode(): Promise<string | null> {153 if (typeof chrome === "undefined" || !chrome.storage?.local) return null;154 const result = await chrome.storage.local.get(STORAGE_KEY_VERIFY_CODE);155 const value = (result as Record<string, unknown>)[STORAGE_KEY_VERIFY_CODE];156 return typeof value === "string" ? value : null;157}158159async function writeStoredVerifyCode(code: string): Promise<void> {160 if (typeof chrome === "undefined" || !chrome.storage?.local) {161 throw new CodexError(162 "PDF_GENERATION_FAILED",163 "chrome.storage.local is unavailable; cannot persist Codex verify code",164 );165 }166 await chrome.storage.local.set({ [STORAGE_KEY_VERIFY_CODE]: code });167}168169170export async function clearVerifyCode(): Promise<void> {171 if (typeof chrome === "undefined" || !chrome.storage?.local) return;172 await chrome.storage.local.remove(STORAGE_KEY_VERIFY_CODE);173}