123456789101112131415161718192021222324import {25 CodexError,26 deriveShardCounts,27 type ChunkEnvelope,28 type CodexPayload,29} from "../../shared/codex-types";30import { reconstructPayloadFromChunks } from "./generate";31import {32 base64UrlToBytes,33 sha256Hex,34 verifyChunkIntegrity,35} from "./helpers";3637export interface ScannedChunk {38 envelope: ChunkEnvelope;39 bytes: Uint8Array;40}4142export type ChunkAddReason =43 | "duplicate"44 | "foreign_codex"45 | "bad_integrity"46 | "parse_error";4748export interface ChunkAddResult {49 accepted: boolean;50 reason?: ChunkAddReason;51}5253export interface ChunkBufferContext {54 verifyCode: string;55 total: number;56 dataShards: number;57}5859export class ChunkBuffer {60 private readonly chunks = new Map<number, ScannedChunk>();61 private ctx: ChunkBufferContext | null = null;6263 646566676869707172 async add(envelope: ChunkEnvelope): Promise<ChunkAddResult> {73 74 75 76 if (envelope.v !== 2) {77 return { accepted: false, reason: "parse_error" };78 }7980 if (!(await verifyChunkIntegrity(envelope))) {81 return { accepted: false, reason: "bad_integrity" };82 }8384 if (this.ctx === null) {85 86 87 88 89 90 91 92 let dataShards: number;93 try {94 dataShards = deriveShardCounts(envelope.t).dataShards;95 } catch {96 return { accepted: false, reason: "parse_error" };97 }98 this.ctx = {99 verifyCode: envelope.c,100 total: envelope.t,101 dataShards,102 };103 } else if (envelope.c !== this.ctx.verifyCode) {104 return { accepted: false, reason: "foreign_codex" };105 }106107 if (this.chunks.has(envelope.i)) {108 return { accepted: false, reason: "duplicate" };109 }110111 this.chunks.set(envelope.i, {112 envelope,113 bytes: base64UrlToBytes(envelope.d),114 });115 return { accepted: true };116 }117118 size(): number {119 return this.chunks.size;120 }121122 getContext(): ChunkBufferContext | null {123 return this.ctx;124 }125126 isReady(): boolean {127 return this.ctx !== null && this.chunks.size >= this.ctx.dataShards;128 }129130 entries(): ScannedChunk[] {131 return Array.from(this.chunks.values());132 }133134 135 toChunkMap(): Map<number, Uint8Array> {136 const out = new Map<number, Uint8Array>();137 for (const [i, scanned] of this.chunks) {138 out.set(i, scanned.bytes);139 }140 return out;141 }142}143144export type ParseRawScanResult =145 | ChunkEnvelope146 | { error: "parse_error" | "wrong_version" };147148149150151152153export function parseRawScan(raw: string): ParseRawScanResult {154 let parsed: unknown;155 try {156 parsed = JSON.parse(raw);157 } catch {158 return { error: "parse_error" };159 }160161 if (typeof parsed !== "object" || parsed === null) {162 return { error: "parse_error" };163 }164165 const obj = parsed as Record<string, unknown>;166167 168 169 if (obj.v !== 2) {170 171 172 if ("v" in obj) return { error: "wrong_version" };173 return { error: "parse_error" };174 }175176 const looksLikeEnvelope =177 typeof obj.c === "string" &&178 typeof obj.i === "number" &&179 typeof obj.t === "number" &&180 typeof obj.p === "boolean" &&181 typeof obj.d === "string" &&182 typeof obj.h === "string";183184 if (!looksLikeEnvelope) {185 return { error: "parse_error" };186 }187188 return obj as unknown as ChunkEnvelope;189}190191192193194195196export async function reconstructAndVerify(197 buffer: ChunkBuffer,198): Promise<CodexPayload> {199 const ctx = buffer.getContext();200 if (ctx === null) {201 throw new CodexError(202 "INSUFFICIENT_CHUNKS",203 "No chunks have been scanned yet.",204 );205 }206207 const payload = await reconstructPayloadFromChunks(208 buffer.toChunkMap(),209 ctx.dataShards,210 ctx.total,211 );212213 214 215 216 217 218 219 220 221 222 223 const fingerprintLen = payload.vaultFingerprint.length;224 const fingerprint = (await sha256Hex(new TextEncoder().encode(payload.vaultBlob)))225 .slice(0, fingerprintLen)226 .toUpperCase();227 if (fingerprint !== payload.vaultFingerprint) {228 throw new CodexError(229 "RECONSTRUCTION_FINGERPRINT_MISMATCH",230 "Reconstructed vault fingerprint does not match the printed Codex. " +231 "Try scanning the Codex again from the beginning.",232 );233 }234235 return payload;236}