CultLib/packages/cultmesh-browser/test/interop/content-https-provider.ts
Libby 286a9af389
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Failing after 5m49s
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Successful in 36s
npm: scope cultnet-ts, cultmesh-ts and cultmesh-browser under @gamecult
Unscoped npm names are unclaimed and open to dependency confusion; cultcache-ts was
already @gamecult/cultcache-ts. package.json names, intra-repo dependencies, package-lock,
import specifiers (src, tests, samples, docs/start, first-call docs test), the workspace
list in run-typescript-workspaces, the verify-eve module map and package READMEs now use
the scoped names. Directory names, workspace paths in CI and release tag prefixes are
unchanged. Historical cut docs and past CHANGELOG entries keep the old names.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 16:25:10 +00:00

51 lines
3.3 KiB
TypeScript

// The TypeScript content provider in its own process, for content-https.test.ts. Argv: <store dir> <corrupt store dir> <key pem> <cert pem> <escape record key>.
// Protocol, stdout JSON lines only: ready, one line per request ({phase, mount, url}), one per server fault ({phase, mount, fault}).
// Stdin: "phase <name>" sets the phase and is answered {ack}; stdin ending closes the server and exits, so no orchestrator crash orphans it.
import { readFileSync } from "node:fs";
import { createServer } from "node:https";
import type { AddressInfo } from "node:net";
import { createInterface } from "node:readline";
import { chunkRecordKey } from "@gamecult/cultnet-ts/content";
import { createContentChunkHandler, createDirectoryChunkStore, toNodeListener, type ContentChunkAnswer, type ContentChunkRequest } from "@gamecult/cultmesh-ts/content";
const [storeDir, corruptDir, keyPem, certPem, escapeKey] = process.argv.slice(2) as [string, string, string, string, string];
const say = (line: object): void => { process.stdout.write(`${JSON.stringify(line)}\n`); };
let phase = "start";
const handlerFor = (mount: string, dir: string) =>
createContentChunkHandler(createDirectoryChunkStore(dir), { onServerFault: fault => say({ phase, mount, fault: fault.kind }) });
const honest = handlerFor("honest", storeDir);
// In flight, after the provider verified the bytes: a 200 with a body comes back with byte 0 flipped and the headers unchanged.
const tampered = async (request: ContentChunkRequest): Promise<ContentChunkAnswer> => {
const answer = await honest(request);
if (answer.status !== 200 || !answer.body?.length) return answer;
const body = answer.body.slice();
body[0]! ^= 1;
return { ...answer, body };
};
// The one non-canonical record key the fixtures carry (escapeKey) is answered as its canonical key, so the real handler serves the chunk
// and the request line above, logged before this runs, is what each consumer actually sent. Every other recordKey reaches the handler untouched.
const honestEscape = async (request: ContentChunkRequest): Promise<ContentChunkAnswer> => {
const url = new URL(request.url, "http://provider.invalid");
if (url.searchParams.get("recordKey") !== escapeKey) return honest(request);
url.searchParams.set("recordKey", chunkRecordKey(url.searchParams.get("chunkHash")));
return honest({ ...request, url: `${url.pathname}${url.search}` });
};
const mounts = { honest: toNodeListener(honestEscape), tampered: toNodeListener(tampered), corrupt: toNodeListener(handlerFor("corrupt", corruptDir)) };
const server = createServer({ key: readFileSync(keyPem), cert: readFileSync(certPem) }, (req, res) => {
const mount = /^\/(honest|tampered|corrupt)\//.exec(req.url ?? "")?.[1] as keyof typeof mounts | undefined;
if (!mount) { res.writeHead(404).end(); return; }
say({ phase, mount, url: req.url });
mounts[mount](req, res);
});
await new Promise<void>(resolve => server.listen(0, "127.0.0.1", resolve));
const port = (server.address() as AddressInfo).port;
say({ status: "ready", endpoints: Object.fromEntries(Object.keys(mounts).map(mount => [mount, `https://127.0.0.1:${port}/${mount}/content`])) });
createInterface({ input: process.stdin }).on("line", line => {
const name = /^phase (.+)$/.exec(line)?.[1];
if (name) { phase = name; say({ ack: name }); }
}).on("close", () => { server.closeAllConnections(); server.close(() => process.exit(0)); });