#!/usr/bin/env node /* * route-privacy.test.mjs — the Worker routing boundary, gated (05-05 T-05-16; * re-pinned by 06-03, then re-pinned AGAIN by 06-04 per the gate re-pin law). * ------------------------------------------------------------------------------ * PHASE-5 SHAPE (historical): D-05 banned any /api/route outright — the * routing stack lived web-side and the Worker grew no route proxy. * * 06-03's re-pin: the frozen guidance envelope's geometry IS polyline6, so * the polyline codec entered the Worker and RP2 became an explicit allowlist. * RP1 stood unchanged. * * 06-04's re-pin (THIS FILE — D-01 made the Worker the router, so the * negative-space ban is superseded by a positive-space law): * * RP1 claim-bound routing — the Worker MAY route, but only under the * session identity: bot/src/routing/worker-router.ts must derive * identity from the Bearer CLAIM (getClaimByToken — T-06-01), must * NEVER forward to an external routing host (no fetch, no proxy — * the routing computation is local A* over the R2 graph), and every * file referencing the /api/route arm must be wired to that * claim-deriving module. An unauthenticated or proxy-shaped routing * arm fails here at test time. * RP2 workspace allowlist — every @araharah/* specifier in bot/src must * be a SUBPATH-WITH-EXTENSION import of the two allowed workspaces: * @araharah/search/* and @araharah/routing/*. Any OTHER workspace * package, or a bare package specifier (no subpath), fails. (GE5 * still owns the KV-namespace count; this gate owns imports.) * * The scans cover EVERY TypeScript file under bot/src and grow with the * tree automatically. */ import { strict as assert } from "node:assert"; import { readdirSync, readFileSync, statSync } from "node:fs"; import { fileURLToPath } from "node:url"; import path from "node:path"; import { test } from "./harness.mjs"; const SRC = path.join(fileURLToPath(new URL(import.meta.url)), "..", "..", "src"); const ROUTER = path.join(SRC, "routing", "worker-router.ts"); function listFiles(dir, exts, acc = []) { for (const entry of readdirSync(dir)) { const full = path.join(dir, entry); if (statSync(full).isDirectory()) listFiles(full, exts, acc); else if (exts.some((e) => entry.endsWith(e))) acc.push(full); } return acc; } function stripComments(src) { return src .replace(/\/\*[\s\S]*?\*\//g, " ") // block comments .replace(/(^|[^:\\])\/\/[^\n]*/g, "$1"); // line comments (not URLs inside strings — benign over-stripping, gate is prefer-fail reading) } function codeText(file) { return stripComments(readFileSync(file, "utf8")); } /* ————— RP1 — the routing arm is claim-bound and never a proxy (T-06-01) ————— */ test("RP1a the router derives identity from the Bearer CLAIM and never forwards (worker-router.ts, T-06-01)", () => { assert.ok( existsIn(ROUTER), "bot/src/routing/worker-router.ts must exist — 06-04's engine-A arm is the one lawful routing surface", ); const code = codeText(ROUTER); assert.ok( code.includes("getClaimByToken"), "worker-router.ts must resolve the Bearer token through getClaimByToken — an unauthenticated routing arm fails RP1 (T-06-01)", ); assert.ok( !/\bfetch\s*\(/.test(code), "worker-router.ts must NEVER fetch — the routing computation is local A* over the R2 graph; a forwarding/proxy-shaped router fails RP1", ); }); test("RP1b every /api/route reference is wired to the claim-deriving router module", () => { const files = listFiles(SRC, [".ts"]); assert.ok(files.length >= 8, `scanned a real tree (found ${files.length} files)`); for (const file of files) { const code = codeText(file); if (!code.includes("/api/route")) continue; const wired = code.includes("worker-router") || code.includes("getClaimByToken"); assert.ok( wired, `${path.relative(process.cwd(), file)} references "/api/route" but neither delegates to ` + `worker-router.ts nor derives the claim itself — a detached/unauthenticated route arm fails RP1`, ); } }); test("RP1c no external routing hostnames anywhere in bot/src (the routing oracle is OUR Worker)", () => { const banned = /(overpass|project-osrm|osrm|valhalla|graphhopper|openrouteservice|routing\.openstreetmap)/i; const files = listFiles(SRC, [".ts"]); for (const file of files) { const code = codeText(file); const hit = code.match(banned); assert.ok( hit === null, `${path.relative(process.cwd(), file)} references routing-host "${hit?.[0]}" — the Worker routes locally (D-01); external routing hosts are maintainer-time build tools, never runtime dependencies`, ); } }); /* ————— RP2 — the workspace import allowlist (re-pinned 06-04: search + routing) ————— */ test("RP2 workspace allowlist — @araharah/* imports in bot/src are search/routing SUBPATHS only (D-01 + 06-04)", () => { const ALLOWED_PREFIXES = ["@araharah/search/", "@araharah/routing/"]; const files = listFiles(SRC, [".ts"]); for (const file of files) { const code = codeText(file); const specifiers = [...code.matchAll(/["'](@araharah\/[^"']*)["']/g)].map((m) => m[1]); for (const spec of specifiers) { const allowed = ALLOWED_PREFIXES.some((prefix) => spec.startsWith(prefix)) && !ALLOWED_PREFIXES.some((prefix) => spec === prefix.slice(0, -1)); // a bare prefix with no subpath fails too assert.ok( allowed, `${path.relative(process.cwd(), file)} imports "${spec}" — bot may import ONLY @araharah/search/* and ` + `@araharah/routing/* as subpath-with-extension specifiers (the subpath law); any other workspace ` + `package (or a bare specifier) fails RP2`, ); } } }); function existsIn(file) { try { statSync(file); return true; } catch { return false; } }