cultmesh-browser: fetchVerifiedContent and fetchVerifiedChunk release only verified bytes

The reader judges received bytes: each chunk streams into its slot of a private
buffer, is cut at the first byte past its size, and is hashed; the whole body is
hashed before the buffer is returned. Length and coding headers are never read.
URLs equal the C# BuildChunkUri; concurrency 1..32, maxBytes, AbortSignal, typed
errors. Hashing, hex, keys and the loopback rule stay in cultnet-ts.

Builds and passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-19 15:28:21 +02:00
commit 11fd0aa4ff
3 changed files with 498 additions and 0 deletions

View file

@ -0,0 +1,143 @@
// Verified reads of CultMesh CDN content over HTTP(S). This module is the only place bytes are released to a caller, and it
// releases them after judging what it received: each chunk against its hash, then the whole body against the manifest's.
// The response's declared length and coding are never consulted (Fetch decodes codings; the bytes received are the evidence).
// Hashing, hex rules, record keys, the loopback rule and manifest validation belong to cultnet-ts and are only called here.
import { EMPTY_SHA256, chunkRecordKey, parseSha256Hex, sha256Hex, validateManifest, type CultMeshCdnArtifactManifest } from "cultnet-ts/content";
import { isLoopbackEndpoint } from "cultnet-ts/authority";
export type CultMeshContentErrorCode =
| "manifest-invalid" | "too-large" | "insecure-endpoint" | "webcrypto-unavailable" | "network"
| "http-status" | "length-mismatch" | "chunk-hash-mismatch" | "content-hash-mismatch";
const MESSAGES: Record<CultMeshContentErrorCode, string> = {
"manifest-invalid": "The CDN manifest or chunk description is invalid.",
"too-large": "The content is larger than maxBytes.",
"insecure-endpoint": "Content endpoints must be https, or http on loopback.",
"webcrypto-unavailable": "WebCrypto SHA-256 is unavailable.",
"network": "The content request failed.",
"http-status": "The content server did not answer 200.",
"length-mismatch": "The content body did not match the chunk size.",
"chunk-hash-mismatch": "A content chunk did not match its hash.",
"content-hash-mismatch": "The content did not match the manifest content hash.",
};
/** A refusal to release bytes. The message is fixed per code and never carries the endpoint or any body byte. */
export class CultMeshContentError extends Error {
constructor(readonly code: CultMeshContentErrorCode, readonly chunkHash?: string, readonly status?: number, cause?: unknown) {
super(MESSAGES[code], cause === undefined ? undefined : { cause });
this.name = "CultMeshContentError";
}
}
export interface CultMeshContentFetchOptions {
readonly signal?: AbortSignal;
/** Largest whole body, checked before any request. Default 256 MiB. */
readonly maxBytes?: number;
/** Chunk requests in flight at once, an integer 1..32. Default 4. */
readonly maxConcurrentChunkRequests?: number;
readonly fetch?: typeof globalThis.fetch;
}
interface Resolved { readonly doFetch: typeof globalThis.fetch; readonly caller?: AbortSignal; readonly maxBytes: number; readonly concurrency: number }
interface Job { readonly url: string; readonly hash: string; readonly slot: Uint8Array }
const fail = (code: CultMeshContentErrorCode, chunkHash?: string, status?: number, cause?: unknown) => new CultMeshContentError(code, chunkHash, status, cause);
const isByteCount = (value: number): boolean => Number.isSafeInteger(value) && value >= 0;
// Uri.EscapeDataString: encodeURIComponent, plus the five characters it leaves bare.
const escapeData = (value: string): string => encodeURIComponent(value).replace(/[!'()*]/g, c => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
/** `CultMeshHttpsContentTransportConnector.BuildChunkUri`: an existing query (even an empty one) is kept and joined with `&`. */
function chunkUrl(endpoint: string, hash: string, recordKey: string): string {
const url = new URL(endpoint);
const kept = url.href.split("#")[0]!.includes("?") ? `${url.search.slice(1)}&` : "";
url.search = `${kept}chunkHash=${escapeData(hash)}&recordKey=${escapeData(recordKey)}`;
return url.href;
}
/** Options, caller abort and endpoint scheme: everything that can be refused without reading the manifest. */
function resolve(endpoint: string, options: CultMeshContentFetchOptions): Resolved {
const { signal, maxBytes = 268435456, maxConcurrentChunkRequests: concurrency = 4, fetch } = options;
if (!Number.isInteger(concurrency) || concurrency < 1 || concurrency > 32) throw new RangeError("maxConcurrentChunkRequests must be an integer from 1 to 32.");
if (!isByteCount(maxBytes)) throw new RangeError("maxBytes must be a non-negative safe integer.");
if (signal?.aborted) throw signal.reason;
const secure = (() => { try { const { protocol } = new URL(endpoint); return protocol === "https:" || (protocol === "http:" && isLoopbackEndpoint(endpoint)); } catch { return false; } })();
if (!secure) throw fail("insecure-endpoint");
return { doFetch: fetch ?? ((input, init) => globalThis.fetch(input, init)), caller: signal, maxBytes, concurrency };
}
const requireWebCrypto = (): void => { if (!globalThis.crypto?.subtle) throw fail("webcrypto-unavailable"); };
/** One chunk: GET, status 200, the body streamed into its slot and cut at the first byte past the slot, then the chunk hash. */
async function readChunk(r: Resolved, signal: AbortSignal, { url, hash, slot }: Job): Promise<void> {
if (slot.length === 0) { if (hash !== EMPTY_SHA256) throw fail("chunk-hash-mismatch", hash); return; }
r.caller?.throwIfAborted();
const network = (cause: unknown) => r.caller?.aborted ? r.caller.reason : fail("network", hash, undefined, cause);
const response = await r.doFetch(url, { method: "GET", credentials: "omit", signal }).catch(cause => { throw network(cause); });
if (response.status !== 200) { void response.body?.cancel().catch(() => {}); throw fail("http-status", hash, response.status); }
const reader = response.body?.getReader();
let filled = 0;
try {
for (let part = await reader?.read(); part && !part.done; part = await reader!.read()) {
if (filled + part.value.length > slot.length) { void reader!.cancel().catch(() => {}); throw fail("length-mismatch", hash); }
slot.set(part.value, filled);
filled += part.value.length;
}
} catch (error) { throw error instanceof CultMeshContentError ? error : network(error); }
if (filled < slot.length) throw fail("length-mismatch", hash);
if (await sha256Hex(slot) !== hash) throw fail("chunk-hash-mismatch", hash);
}
/** A sliding window of chunk reads. The first failure aborts every request still in flight and is the one thrown. */
async function readChunks(r: Resolved, jobs: readonly Job[]): Promise<void> {
const internal = new AbortController();
const relay = () => internal.abort(r.caller!.reason);
r.caller?.addEventListener("abort", relay, { once: true });
let failure: { error: unknown } | undefined;
let next = 0;
const worker = async () => {
for (let job = jobs[next++]; job && !failure; job = jobs[next++]) {
try { await readChunk(r, internal.signal, job); } catch (error) { failure ??= { error }; internal.abort(error); }
}
};
try { await Promise.all(Array.from({ length: Math.min(r.concurrency, jobs.length) }, worker)); }
finally { r.caller?.removeEventListener("abort", relay); }
if (failure) throw failure.error;
}
/** The whole body of a manifest's artifact, returned only after every chunk and the whole-body hash verified. */
export async function fetchVerifiedContent(endpoint: string, manifest: CultMeshCdnArtifactManifest, options: CultMeshContentFetchOptions = {}): Promise<Uint8Array> {
const r = resolve(endpoint, options);
let contentHash: string;
let planned: { hash: string; url: string; offset: number; size: number }[];
try {
validateManifest(manifest);
contentHash = parseSha256Hex(manifest.contentHash, "contentHash");
const refs = [...manifest.chunks].sort((a, b) => a.offset - b.offset);
if (!isByteCount(manifest.sizeBytes) || !refs.every(ref => isByteCount(ref.offset) && isByteCount(ref.sizeBytes))) throw new Error("Sizes must be safe integers.");
if (refs.length > 1 && refs.some(ref => ref.sizeBytes === 0)) throw new Error("A zero-size chunk must be the only chunk.");
planned = refs.map(ref => { const hash = parseSha256Hex(ref.chunkHash, "chunkHash"); return { hash, url: chunkUrl(endpoint, hash, ref.recordKey ?? ""), offset: ref.offset, size: ref.sizeBytes }; });
} catch (cause) { throw fail("manifest-invalid", undefined, undefined, cause); }
if (manifest.sizeBytes > r.maxBytes) throw fail("too-large");
requireWebCrypto();
const buffer = new Uint8Array(manifest.sizeBytes);
await readChunks(r, planned.map(({ hash, url, offset, size }) => ({ url, hash, slot: buffer.subarray(offset, offset + size) })));
if (await sha256Hex(buffer) !== contentHash) throw fail("content-hash-mismatch");
return buffer;
}
/** One chunk by hash and size, over the same primitive; the record key is the one a packed manifest carries. */
export async function fetchVerifiedChunk(endpoint: string, chunkHash: string, sizeBytes: number, options: CultMeshContentFetchOptions = {}): Promise<Uint8Array> {
const r = resolve(endpoint, options);
let hash: string;
try {
hash = parseSha256Hex(chunkHash, "chunkHash");
if (!isByteCount(sizeBytes)) throw new Error("Size must be a safe integer.");
} catch (cause) { throw fail("manifest-invalid", undefined, undefined, cause); }
if (sizeBytes > r.maxBytes) throw fail("too-large");
requireWebCrypto();
const slot = new Uint8Array(sizeBytes);
await readChunks(r, [{ url: chunkUrl(endpoint, hash, chunkRecordKey(hash)), hash, slot }]);
return slot;
}

View file

@ -32,6 +32,7 @@ import {
type CultMeshAuthorityTrustPolicy,
type CultMeshP256PublicKey,
} from "cultnet-ts/authority";
export * from "./content.js";
export type CultMeshBrowserIdentity = CultMeshAuthorityIdentity;
export type CultMeshBrowserRoute = CultMeshAuthorityRouteView;

View file

@ -0,0 +1,354 @@
// The verified content reader, rule by rule. Chunks and manifests come from cultnet-ts's own packContent, so the reader is
// judged against the shapes the provider will serve; the URL expectations are the .NET `BuildChunkUri` AbsoluteUri strings
// probed for the map (F5, F19), not a spelling of this module's own builder.
import test from "node:test";
import assert from "node:assert/strict";
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
import type { AddressInfo } from "node:net";
import { gzipSync } from "node:zlib";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { build } from "esbuild";
import { EMPTY_SHA256, packContent, sha256Hex, type CultMeshCdnArtifactManifest } from "cultnet-ts/content";
import { CultMeshContentError, fetchVerifiedChunk, fetchVerifiedContent, type CultMeshContentErrorCode } from "../src/content.js";
const ENDPOINT = "https://cdn.example/content";
const body = (length: number): Uint8Array => Uint8Array.from({ length }, (_, index) => (index * 7 + 3) & 255);
const pack = (length: number, chunkSizeBytes: number) => packContent("art", body(length), { chunkSizeBytes, createdAtUtc: "2026-01-01T00:00:00.0000000+00:00" });
const streamOf = (bytes: Uint8Array, onCancel?: () => void) =>
new ReadableStream<Uint8Array>({ start(controller) { controller.enqueue(bytes); controller.close(); }, cancel: onCancel });
const withChunk = (manifest: CultMeshCdnArtifactManifest, index: number, change: object): CultMeshCdnArtifactManifest =>
({ ...manifest, chunks: manifest.chunks.map((chunk, i) => i === index ? { ...chunk, ...change } : chunk) });
const codeOf = (code: CultMeshContentErrorCode, extra: object = {}) => (error: unknown) =>
error instanceof CultMeshContentError && error.code === code && Object.entries(extra).every(([key, value]) => (error as unknown as Record<string, unknown>)[key] === value);
interface Call { url: string; init: RequestInit }
type Handler = (call: Call, index: number) => Response | Promise<Response>;
/** A fetch double: records every call, counts requests in flight (the handler's lifetime) and their peak. */
function double(handler: Handler) {
const calls: Call[] = [];
const state = { inflight: 0, peak: 0 };
const fetch = (async (input: string | URL | Request, init?: RequestInit) => {
const call = { url: String(input), init: init ?? {} };
const index = calls.push(call) - 1;
state.peak = Math.max(state.peak, ++state.inflight);
try { return await handler(call, index); } finally { state.inflight--; }
}) as typeof globalThis.fetch;
return { fetch, calls, state };
}
/** Serves each requested chunk by the `chunkHash` query parameter, after `delayMs`. */
function chunkServer(chunks: { chunkHash: string; payload: Uint8Array }[], delayMs = 0) {
const byHash = new Map(chunks.map(chunk => [chunk.chunkHash, chunk.payload]));
return double(async call => {
if (delayMs) await new Promise(resolve => setTimeout(resolve, delayMs));
const payload = byHash.get(new URL(call.url).searchParams.get("chunkHash")!);
return payload ? new Response(payload as BodyInit) : new Response(null, { status: 404 });
});
}
/** A response that never settles until the request's signal aborts, as a real fetch rejects with the signal's reason. */
const hangUntilAborted = (call: Call) => new Promise<Response>((_, reject) => {
const signal = call.init.signal!;
if (signal.aborted) reject(signal.reason); else signal.addEventListener("abort", () => reject(signal.reason), { once: true });
});
test("request URLs equal the .NET BuildChunkUri AbsoluteUri for every probed endpoint shape", async () => {
const { manifest, chunks } = await pack(1, 4);
const hex = manifest.chunks[0]!.chunkHash;
const cases: [string, string, string][] = [
["https://ygg.example/content", "mesh:cdn:chunk:" + hex, `https://ygg.example/content?chunkHash=${hex}&recordKey=mesh%3Acdn%3Achunk%3A${hex}`],
["https://ygg.example/content?tenant=sp", "k", `https://ygg.example/content?tenant=sp&chunkHash=${hex}&recordKey=k`],
["https://h", "k", `https://h/?chunkHash=${hex}&recordKey=k`],
["https://h/", "k", `https://h/?chunkHash=${hex}&recordKey=k`],
["https://h:443/x", "k", `https://h/x?chunkHash=${hex}&recordKey=k`],
["https://H.Example/X/", "k", `https://h.example/X/?chunkHash=${hex}&recordKey=k`],
["https://h/x#frag", "k", `https://h/x?chunkHash=${hex}&recordKey=k#frag`],
["https://h/x?a=%20b+c", "k", `https://h/x?a=%20b+c&chunkHash=${hex}&recordKey=k`],
["https://h/x?", "k", `https://h/x?&chunkHash=${hex}&recordKey=k`],
["https://[::1]:8443/c", "k", `https://[::1]:8443/c?chunkHash=${hex}&recordKey=k`],
["https://h/x", "", `https://h/x?chunkHash=${hex}&recordKey=`],
["https://h/x", "k with space/ü&=?#", `https://h/x?chunkHash=${hex}&recordKey=k%20with%20space%2F%C3%BC%26%3D%3F%23`],
["https://h/x", "!'()*~-._ +é𐐀", `https://h/x?chunkHash=${hex}&recordKey=%21%27%28%29%2A~-._%20%2B%C3%A9%F0%90%90%80`],
];
for (const [endpoint, recordKey, expected] of cases) {
const server = chunkServer(chunks);
await fetchVerifiedContent(endpoint, withChunk(manifest, 0, { recordKey }), { fetch: server.fetch });
assert.equal(server.calls[0]!.url, expected, endpoint);
}
});
test("hash spellings are normalized into the URL: a prefixed upper-case manifest hash is requested as bare lower-case hex", async () => {
const { manifest, chunks } = await pack(5, 5);
const hex = manifest.chunks[0]!.chunkHash;
const server = chunkServer(chunks);
const spelled = { ...withChunk(manifest, 0, { chunkHash: `SHA256:${hex.toUpperCase()}` }), contentHash: hex.toUpperCase() };
assert.deepEqual(await fetchVerifiedContent(ENDPOINT, spelled, { fetch: server.fetch }), body(5));
assert.equal(new URL(server.calls[0]!.url).searchParams.get("chunkHash"), hex);
});
test("a 5.5-chunk manifest returns the input bytes with one credential-free GET per chunk, whatever order the refs are listed in", async () => {
const { manifest, chunks } = await pack(55, 10);
assert.equal(manifest.chunks.length, 6);
for (const refs of [manifest.chunks, [...manifest.chunks].reverse()]) {
const server = chunkServer(chunks);
assert.deepEqual(await fetchVerifiedContent(ENDPOINT, { ...manifest, chunks: refs }, { fetch: server.fetch }), body(55));
assert.equal(server.calls.length, 6);
assert.deepEqual(new Set(server.calls.map(call => new URL(call.url).searchParams.get("chunkHash"))), new Set(manifest.chunks.map(chunk => chunk.chunkHash)));
for (const call of server.calls) { assert.equal(call.init.method, "GET"); assert.equal(call.init.credentials, "omit"); }
}
});
test("with 10 chunks, maxConcurrentChunkRequests 3 peaks at exactly 3 in flight and the default at 4; bad bounds reject RangeError with no fetch", async () => {
const { manifest, chunks } = await pack(100, 10);
for (const [limit, peak] of [[3, 3], [undefined, 4], [1, 1], [32, 10]] as const) {
const server = chunkServer(chunks, 5);
assert.deepEqual(await fetchVerifiedContent(ENDPOINT, manifest, { fetch: server.fetch, maxConcurrentChunkRequests: limit }), body(100));
assert.equal(server.state.peak, peak, `limit ${limit}`);
}
for (const maxConcurrentChunkRequests of [0, 33, 1.5, Number.NaN]) {
const server = chunkServer(chunks);
await assert.rejects(fetchVerifiedContent(ENDPOINT, manifest, { fetch: server.fetch, maxConcurrentChunkRequests }), RangeError);
await assert.rejects(fetchVerifiedChunk(ENDPOINT, manifest.chunks[0]!.chunkHash, 10, { fetch: server.fetch, maxConcurrentChunkRequests }), RangeError);
assert.equal(server.calls.length, 0);
}
for (const maxBytes of [-1, 1.5, Number.NaN]) await assert.rejects(fetchVerifiedContent(ENDPOINT, manifest, { maxBytes }), RangeError);
});
test("an empty artifact returns an empty array with no fetch, but still answers to its content hash", async () => {
const { manifest } = await pack(0, 4);
const server = double(() => { throw new Error("no request expected"); });
const result = await fetchVerifiedContent(ENDPOINT, manifest, { fetch: server.fetch });
assert.equal(result.length, 0);
await assert.rejects(fetchVerifiedContent(ENDPOINT, { ...manifest, contentHash: "ab".repeat(32) }, { fetch: server.fetch }), codeOf("content-hash-mismatch"));
await assert.rejects(fetchVerifiedContent(ENDPOINT, withChunk(manifest, 0, { chunkHash: "ab".repeat(32) }), { fetch: server.fetch }), codeOf("chunk-hash-mismatch", { chunkHash: "ab".repeat(32) }));
assert.equal(server.calls.length, 0);
});
test("a zero-size chunk beside others is manifest-invalid with no fetch", async () => {
const { manifest } = await pack(8, 4);
const [first, second] = manifest.chunks as [typeof manifest.chunks[number], typeof manifest.chunks[number]];
const flood = { ...manifest, chunks: [first, { ...first, chunkHash: EMPTY_SHA256, offset: 4, sizeBytes: 0 }, second] };
const server = double(() => { throw new Error("no request expected"); });
await assert.rejects(fetchVerifiedContent(ENDPOINT, flood, { fetch: server.fetch }), codeOf("manifest-invalid"));
assert.equal(server.calls.length, 0);
});
test("the byte cap is checked before any request: one over maxBytes and one over the 256 MiB default are too-large, the cap itself is not", async () => {
const { manifest, chunks } = await pack(10, 5);
const server = chunkServer(chunks);
await assert.rejects(fetchVerifiedContent(ENDPOINT, manifest, { fetch: server.fetch, maxBytes: 9 }), codeOf("too-large"));
await assert.rejects(fetchVerifiedChunk(ENDPOINT, manifest.chunks[0]!.chunkHash, 5, { fetch: server.fetch, maxBytes: 4 }), codeOf("too-large"));
assert.equal(server.calls.length, 0);
assert.deepEqual(await fetchVerifiedContent(ENDPOINT, manifest, { fetch: server.fetch, maxBytes: 10 }), body(10));
const huge = (size: number) => ({ ...manifest, sizeBytes: size, chunks: [{ ...manifest.chunks[0]!, offset: 0, sizeBytes: size }] });
const refuses = double(() => { throw new TypeError("offline"); });
await assert.rejects(fetchVerifiedContent(ENDPOINT, huge(268435457), { fetch: refuses.fetch }), codeOf("too-large"));
assert.equal(refuses.calls.length, 0);
await assert.rejects(fetchVerifiedContent(ENDPOINT, huge(268435456), { fetch: refuses.fetch }), codeOf("network"));
});
test("https is required, http only on loopback; ftp and unparseable endpoints are insecure-endpoint with no fetch", async () => {
const { manifest, chunks } = await pack(5, 5);
for (const endpoint of ["http://example.com/c", "ftp://127.0.0.1/c", "not a url", "", "//cdn.example/c"]) {
const server = chunkServer(chunks);
await assert.rejects(fetchVerifiedContent(endpoint, manifest, { fetch: server.fetch }), codeOf("insecure-endpoint"), endpoint);
await assert.rejects(fetchVerifiedChunk(endpoint, manifest.chunks[0]!.chunkHash, 5, { fetch: server.fetch }), codeOf("insecure-endpoint"), endpoint);
assert.equal(server.calls.length, 0);
}
for (const endpoint of ["http://127.0.0.1:8080/c", "http://localhost/c", "http://[::1]:9/c", "https://cdn.example/c"]) {
const server = chunkServer(chunks);
assert.deepEqual(await fetchVerifiedContent(endpoint, manifest, { fetch: server.fetch }), body(5), endpoint);
}
});
test("non-hex hashes and a gapped manifest are manifest-invalid with no fetch, in both exports", async () => {
const { manifest, chunks } = await pack(10, 5);
const server = chunkServer(chunks);
const bad = [
withChunk(manifest, 1, { chunkHash: "g".repeat(64) }),
{ ...manifest, contentHash: "abc" },
withChunk(manifest, 1, { offset: 6 }),
{ ...manifest, sizeBytes: 11 },
{ ...manifest, sizeBytes: 10.5 },
];
for (const candidate of bad) await assert.rejects(fetchVerifiedContent(ENDPOINT, candidate, { fetch: server.fetch }), codeOf("manifest-invalid"));
for (const [hash, size] of [["xyz", 5], [manifest.chunks[0]!.chunkHash, -1], [manifest.chunks[0]!.chunkHash, 1.5], [manifest.chunks[0]!.chunkHash, Number.NaN]] as const) {
await assert.rejects(fetchVerifiedChunk(ENDPOINT, hash, size, { fetch: server.fetch }), codeOf("manifest-invalid"));
}
assert.equal(server.calls.length, 0);
});
test("statuses 404, 500, 206 and 304 are http-status carrying the status, and the body is cancelled", async () => {
const { manifest } = await pack(5, 5);
for (const status of [404, 500, 206, 304]) {
let cancelled = false;
const server = double(() => new Response(status === 304 ? null : streamOf(body(5), () => { cancelled = true; }), { status }));
await assert.rejects(fetchVerifiedContent(ENDPOINT, manifest, { fetch: server.fetch }), codeOf("http-status", { status, chunkHash: manifest.chunks[0]!.chunkHash }));
assert.equal(cancelled, status !== 304, String(status));
}
});
test("wrong bytes of the right length are chunk-hash-mismatch naming the chunk; the other in-flight requests are aborted and nothing is returned", async () => {
const { manifest, chunks } = await pack(30, 10);
const signals: AbortSignal[] = [];
const server = double((call, index) => {
signals.push(call.init.signal!);
return index === 0 ? new Response(new Uint8Array(10).fill(9)) : hangUntilAborted(call);
});
await assert.rejects(fetchVerifiedContent(ENDPOINT, manifest, { fetch: server.fetch, maxConcurrentChunkRequests: 3 }),
codeOf("chunk-hash-mismatch", { chunkHash: chunks[0]!.chunkHash }));
assert.equal(signals.length, 3);
assert.deepEqual(signals.map(signal => signal.aborted), [true, true, true]);
});
test("chunks that each verify under a wrong contentHash are content-hash-mismatch", async () => {
const { manifest, chunks } = await pack(20, 10);
const server = chunkServer(chunks);
await assert.rejects(fetchVerifiedContent(ENDPOINT, { ...manifest, contentHash: await sha256Hex(body(21)) }, { fetch: server.fetch }), codeOf("content-hash-mismatch"));
});
test("an endless body is cancelled at the first byte past the chunk, a short body is refused at its end", async () => {
const { manifest } = await pack(10, 10);
let pulled = 0;
let cancelled = false;
const endless = double(() => new Response(new ReadableStream<Uint8Array>({
pull(controller) { pulled++; controller.enqueue(Uint8Array.of(1)); }, cancel() { cancelled = true; },
}, { highWaterMark: 0 })));
await assert.rejects(fetchVerifiedContent(ENDPOINT, manifest, { fetch: endless.fetch }), codeOf("length-mismatch"));
assert.ok(pulled <= 11, `pulled ${pulled}`);
assert.ok(cancelled);
for (const length of [0, 9]) {
const short = double(() => new Response(body(10).subarray(0, length) as BodyInit));
await assert.rejects(fetchVerifiedContent(ENDPOINT, manifest, { fetch: short.fetch }), codeOf("length-mismatch"));
}
const bodyless = double(() => new Response(null));
await assert.rejects(fetchVerifiedContent(ENDPOINT, manifest, { fetch: bodyless.fetch }), codeOf("length-mismatch"));
});
test("a fetch TypeError and a mid-body stream error are network, with the cause kept", async () => {
const { manifest } = await pack(10, 10);
const down = new TypeError("fetch failed");
await assert.rejects(fetchVerifiedContent(ENDPOINT, manifest, { fetch: double(() => { throw down; }).fetch }), (error: unknown) => codeOf("network")(error) && (error as Error).cause === down);
const boom = new Error("boom");
const broken = double(() => new Response(new ReadableStream<Uint8Array>({ start(controller) { controller.enqueue(body(4)); controller.error(boom); } })));
await assert.rejects(fetchVerifiedContent(ENDPOINT, manifest, { fetch: broken.fetch }), (error: unknown) => codeOf("network")(error) && (error as Error).cause === boom);
});
test("a pre-aborted signal rejects with its reason and no fetch; an abort mid-flight rejects with the same reason and starts no further fetch", async () => {
const { manifest } = await pack(100, 10);
const reason = new Error("caller gave up");
const server = double(hangUntilAborted);
await assert.rejects(fetchVerifiedContent(ENDPOINT, manifest, { fetch: server.fetch, signal: AbortSignal.abort(reason) }), (error: unknown) => error === reason);
await assert.rejects(fetchVerifiedChunk(ENDPOINT, manifest.chunks[0]!.chunkHash, 10, { fetch: server.fetch, signal: AbortSignal.abort(reason) }), (error: unknown) => error === reason);
assert.equal(server.calls.length, 0);
const controller = new AbortController();
const pending = fetchVerifiedContent(ENDPOINT, manifest, { fetch: server.fetch, signal: controller.signal, maxConcurrentChunkRequests: 2 });
while (server.calls.length < 2) await new Promise(resolve => setTimeout(resolve, 1));
controller.abort(reason);
await assert.rejects(pending, (error: unknown) => error === reason);
await new Promise(resolve => setTimeout(resolve, 10));
assert.equal(server.calls.length, 2);
});
test("with crypto.subtle shadowed, both exports are webcrypto-unavailable before any fetch", async () => {
const { manifest, chunks } = await pack(5, 5);
const server = chunkServer(chunks);
const original = Object.getOwnPropertyDescriptor(globalThis, "crypto")!;
Object.defineProperty(globalThis, "crypto", { value: {}, configurable: true });
try {
await assert.rejects(fetchVerifiedContent(ENDPOINT, manifest, { fetch: server.fetch }), codeOf("webcrypto-unavailable"));
await assert.rejects(fetchVerifiedChunk(ENDPOINT, manifest.chunks[0]!.chunkHash, 5, { fetch: server.fetch }), codeOf("webcrypto-unavailable"));
} finally { Object.defineProperty(globalThis, "crypto", original); }
assert.equal(server.calls.length, 0);
});
test("fetchVerifiedChunk sends the packed record key, verifies, sends nothing for size 0, and refuses wrong bytes", async () => {
const { manifest, chunks } = await pack(10, 10);
const hex = manifest.chunks[0]!.chunkHash;
const server = chunkServer(chunks);
assert.deepEqual(await fetchVerifiedChunk(ENDPOINT, hex.toUpperCase(), 10, { fetch: server.fetch }), body(10));
assert.equal(server.calls[0]!.url, `${ENDPOINT}?chunkHash=${hex}&recordKey=mesh%3Acdn%3Achunk%3A${hex}`);
assert.equal(server.calls[0]!.init.credentials, "omit");
const none = double(() => { throw new Error("no request expected"); });
assert.equal((await fetchVerifiedChunk(ENDPOINT, EMPTY_SHA256, 0, { fetch: none.fetch })).length, 0);
await assert.rejects(fetchVerifiedChunk(ENDPOINT, hex, 0, { fetch: none.fetch }), codeOf("chunk-hash-mismatch", { chunkHash: hex }));
assert.equal(none.calls.length, 0);
await assert.rejects(fetchVerifiedChunk(ENDPOINT, hex, 10, { fetch: double(() => new Response(new Uint8Array(10))).fetch }), codeOf("chunk-hash-mismatch", { chunkHash: hex }));
});
test("no error message contains the endpoint, its secret query, or a body byte", async () => {
const secret = "https://cdn.example/c?token=CANARYSECRET";
const { manifest } = await pack(10, 10);
const canary = new TextEncoder().encode("BODYCANARY");
const doubles: [CultMeshContentErrorCode, Handler | undefined][] = [
["http-status", () => new Response(canary, { status: 500 })],
["chunk-hash-mismatch", () => new Response(canary)],
["length-mismatch", () => new Response(new Uint8Array(40).fill(66))],
["network", () => { throw new TypeError(`failed ${secret}`); }],
["insecure-endpoint", undefined],
];
for (const [code, handler] of doubles) {
const server = double(handler ?? (() => new Response()));
const error = await fetchVerifiedContent(code === "insecure-endpoint" ? "http://example.com/c?token=CANARYSECRET" : secret, manifest, { fetch: server.fetch }).then(() => undefined, reason => reason);
assert.ok(codeOf(code)(error), code);
for (const text of [(error as Error).message, String(error)]) { assert.doesNotMatch(text, /CANARY|cdn\.example|token/); }
}
});
interface Stub { endpoint: string; close(): Promise<void> }
async function stub(handler: (request: IncomingMessage, response: ServerResponse) => void): Promise<Stub> {
const server = createServer(handler);
await new Promise<void>(resolve => server.listen(0, "127.0.0.1", resolve));
return {
endpoint: `http://127.0.0.1:${(server.address() as AddressInfo).port}/content`,
close: () => new Promise<void>(resolve => { server.close(() => resolve()); server.closeAllConnections(); }),
};
}
test("stub server through the real global fetch: gzip verifies, a short body is network, an endless body is cut, 404 is http-status", async () => {
const { manifest, chunks } = await pack(1000, 1000);
const payload = chunks[0]!.payload;
const modes = new Map<string, (request: IncomingMessage, response: ServerResponse) => void>();
const server = await stub((request, response) => modes.get(new URL(request.url!, "http://x").searchParams.get("tenant")!)!(request, response));
const via = (tenant: string) => fetchVerifiedContent(`${server.endpoint}?tenant=${tenant}`, manifest);
try {
modes.set("gzip", (_, response) => {
const encoded = gzipSync(payload);
response.writeHead(200, { "content-encoding": "gzip", "content-length": encoded.length, "access-control-allow-origin": "*" });
response.end(encoded);
});
assert.deepEqual(await via("gzip"), payload);
modes.set("short", (_, response) => {
response.writeHead(200, { "content-length": 1000 });
response.write(payload.subarray(0, 400));
setTimeout(() => response.socket!.destroy(), 20);
});
await assert.rejects(via("short"), codeOf("network"));
let sawClose!: () => void;
const closed = new Promise<void>(resolve => { sawClose = resolve; });
modes.set("endless", (_, response) => {
response.writeHead(200);
const timer = setInterval(() => response.write(new Uint8Array(1000)), 1);
response.on("close", () => { clearInterval(timer); sawClose(); });
});
await assert.rejects(via("endless"), codeOf("length-mismatch"));
await closed;
modes.set("missing", (_, response) => { response.writeHead(404, { "access-control-allow-origin": "*" }); response.end("nope"); });
await assert.rejects(via("missing"), codeOf("http-status", { status: 404 }));
} finally { await server.close(); }
});
test("esbuild bundles dist/content.js with platform 'browser', and a node:crypto import fails the same bundle", async () => {
const entry = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "dist", "content.js");
const options = { bundle: true, platform: "browser", format: "esm", write: false, logLevel: "silent" } as const;
const result = await build({ ...options, entryPoints: [entry] });
assert.ok(result.outputFiles[0]!.text.includes("CultMeshContentError"));
await assert.rejects(build({ ...options, stdin: { contents: `import "node:crypto"; import ${JSON.stringify(entry)};`, resolveDir: dirname(entry) } }));
});