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 129 130 131 132 if (envelope.h.length !== 16 && envelope.h.length !== 32) return false;133 const bytes = base64UrlToBytes(envelope.d);134 const fullHash = await sha256Hex(bytes);135 const expected = fullHash.slice(0, envelope.h.length);136 return expected === envelope.h;137}138139140141142143144145146const VERIFY_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";147148149const VERIFY_CODE_PATTERN = /^PX-[A-Z0-9]{4}-[A-Z0-9]{4}$/;150151152153154155156157158159160export async function getOrCreateCodexVerifyCode(): Promise<string> {161 const existing = await readStoredVerifyCode();162 if (existing && VERIFY_CODE_PATTERN.test(existing)) return existing;163164 const fresh = generateVerifyCode();165 await writeStoredVerifyCode(fresh);166 return fresh;167}168169function generateVerifyCode(): string {170 const bytes = new Uint8Array(8);171 crypto.getRandomValues(bytes);172 const chars: string[] = [];173 for (let i = 0; i < 8; i++) {174 175 176 chars.push(VERIFY_CODE_ALPHABET[bytes[i]! % VERIFY_CODE_ALPHABET.length]!);177 }178 return `PX-${chars.slice(0, 4).join("")}-${chars.slice(4, 8).join("")}`;179}180181async function readStoredVerifyCode(): Promise<string | null> {182 if (typeof chrome === "undefined" || !chrome.storage?.local) return null;183 const result = await chrome.storage.local.get(STORAGE_KEY_VERIFY_CODE);184 const value = (result as Record<string, unknown>)[STORAGE_KEY_VERIFY_CODE];185 return typeof value === "string" ? value : null;186}187188async function writeStoredVerifyCode(code: string): Promise<void> {189 if (typeof chrome === "undefined" || !chrome.storage?.local) {190 throw new CodexError(191 "PDF_GENERATION_FAILED",192 "chrome.storage.local is unavailable; cannot persist Codex verify code",193 );194 }195 await chrome.storage.local.set({ [STORAGE_KEY_VERIFY_CODE]: code });196}197198199export async function clearVerifyCode(): Promise<void> {200 if (typeof chrome === "undefined" || !chrome.storage?.local) return;201 await chrome.storage.local.remove(STORAGE_KEY_VERIFY_CODE);202}