import test from "node:test"; import assert from "node:assert/strict"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { spawn } from "node:child_process"; import { request } from "node:http"; import { createReleaseBuilderServer } from "../runtime/release-builder.mjs"; import { validateBuildCommand, readBuilderToken, } from "../runtime/release-builder-security.mjs"; const token = "t".repeat(64); const fixture = async (options = {}) => { const root = await fs.mkdtemp( path.join(os.tmpdir(), "builder-http-security-"), ); const bin = path.join(root, "bin"); await fs.mkdir(bin); await fs.writeFile( path.join(bin, "pnpm"), `#!${process.execPath}\nconst mode=process.env.VITE_FIXTURE_MODE;\nif(mode==='hold'||mode==='ignore'){if(mode==='ignore')process.on('SIGTERM',()=>{});console.log('ready');setInterval(()=>{},1000)}else if(mode==='flood'){process.stdout.write('x'.repeat(20*1024*1024));}else{console.log(JSON.stringify({args:process.argv.slice(2),secretInChild:Boolean(process.env.RELEASE_BUILDER_TOKEN_FILE||process.env.DATABASE_URL||process.env.UNRELATED_SECRET),publicValue:process.env.VITE_FIXTURE_VALUE}));}\n`, { mode: 0o755 }, ); const children = []; const server = createReleaseBuilderServer({ token, allowedRoots: [root], cacheRoots: [root], baseEnv: { ...process.env, PATH: `${bin}:${process.env.PATH}`, RELEASE_BUILDER_TOKEN_FILE: "/private/token", DATABASE_URL: "synthetic-private", UNRELATED_SECRET: "synthetic-private", }, spawnImpl: (...args) => { const child = spawn(...args); children.push(child); return child; }, ...options, }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); const url = `http://127.0.0.1:${server.address().port}/v1/builds`; const command = (env = {}) => ({ command: "pnpm", args: ["install", "--frozen-lockfile"], cwd: root, env, }); const post = (commands = [command()], presented = token, signal) => fetch(url, { method: "POST", headers: { "content-type": "application/json", "x-release-builder-token": presented, }, body: JSON.stringify({ commands }), signal, }); const close = async () => { server.closeAllConnections(); await new Promise((resolve) => server.close(resolve)); await fs.rm(root, { recursive: true, force: true }); }; return { root, url, command, post, children, close }; }; const alive = (pid) => { try { process.kill(pid, 0); return true; } catch { return false; } }; const waitFor = async (predicate) => { const until = Date.now() + 5000; while (!predicate()) { if (Date.now() >= until) throw new Error("Fixture state deadline exceeded."); await new Promise((resolve) => setTimeout(resolve, 20)); } }; test("rejects missing, placeholder and oversized token files without exposing values", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "builder-secret-test-")); try { assert.throws( () => readBuilderToken(path.join(root, "missing")), /A valid RELEASE_BUILDER_TOKEN_FILE/, ); const filename = path.join(root, "token"); for (const value of ["replace-with-token", "x".repeat(257)]) { await fs.writeFile(filename, value); assert.throws( () => readBuilderToken(filename), /A valid RELEASE_BUILDER_TOKEN_FILE/, ); } await fs.writeFile(filename, token); assert.equal(readBuilderToken(filename), token); } finally { await fs.rm(root, { recursive: true, force: true }); } }); test("rejects unauthenticated HTTP before parsing a command and executes authenticated work without inherited secrets", async () => { const f = await fixture(); try { for (const presented of ["", "x".repeat(64)]) { const response = await f.post([f.command()], presented); assert.equal(response.status, 401); assert.equal(f.children.length, 0); } const response = await f.post([ f.command({ VITE_FIXTURE_VALUE: "public", PATH: "/attacker/bin" }), ]); assert.equal(response.status, 200); const body = await response.text(); assert.match(body, /secretInChild\\\":false/); assert.match(body, /publicValue\\\":\\\"public/); assert.ok(!body.includes(token)); assert.ok(!body.includes("/attacker/bin")); assert.equal(JSON.parse(body.trim().split("\n").at(-1)).result.ok, true); } finally { await f.close(); } }); test("denies extra pnpm flags, arbitrary packages/tasks, unsafe Node controls and escaping symlinks", async () => { const f = await fixture(); const outside = await fs.mkdtemp(path.join(os.tmpdir(), "builder-outside-")); try { await fs.symlink(outside, path.join(f.root, "escape")); const invalid = [ { ...f.command(), args: ["install", "--frozen-lockfile", "--dir=/tmp"] }, { ...f.command(), args: ["exec", "turbo", "run", "arbitrary", "--filter=@sammo-ts/infra"], }, { ...f.command(), args: ["--filter", "attacker-package", "build"] }, { ...f.command(), env: { NODE_OPTIONS: "--require=/tmp/attack.cjs" } }, { ...f.command(), env: { TURBO_CACHE_DIR: path.join(f.root, "escape", "cache") }, }, { ...f.command(), cwd: path.join(f.root, "escape") }, { ...f.command(), command: "node", args: ["-e", "process.exit(0)"] }, ]; for (const command of invalid) { assert.equal((await f.post([command])).status, 400); assert.equal(f.children.length, 0); } const valid = { ...f.command(), args: [ "exec", "turbo", "run", "build:release", "--filter=@sammo-ts/game-frontend", `--cache-dir=${f.root}/cache`, "--concurrency=1", "--ui=stream", "--output-logs=new-only", ], env: { NODE_OPTIONS: "--max-old-space-size=3072", RAYON_NUM_THREADS: "2", }, }; assert.deepEqual( validateBuildCommand(valid, [f.root], [f.root]).args, valid.args, ); } finally { await f.close(); await fs.rm(outside, { recursive: true, force: true }); } }); test("caps actual UTF-8 request bytes before child execution", async () => { const f = await fixture(); try { const response = await fetch(f.url, { method: "POST", headers: { "x-release-builder-token": token }, body: JSON.stringify({ commands: [f.command()], extra: "한".repeat(100_000), }), }); assert.equal(response.status, 413); assert.equal(f.children.length, 0); } finally { await f.close(); } }); test("caps active plus queued requests and reclaims capacity after client disconnects", async () => { const f = await fixture(); const controllers = Array.from({ length: 3 }, () => new AbortController()); try { const responses = []; for (const controller of controllers) responses.push( await f.post( [f.command({ VITE_FIXTURE_MODE: "hold" })], token, controller.signal, ), ); const denied = await f.post(); assert.equal(denied.status, 429); assert.equal(denied.headers.get("retry-after"), "1"); await waitFor(() => f.children.length > 0); for (const controller of controllers) controller.abort(); await Promise.all( responses.map((response) => response.text().catch(() => "")), ); await waitFor(() => f.children.every((child) => !alive(child.pid))); let recovered; for (let i = 0; i < 100; i++) { recovered = await f.post(); if (recovered.status !== 429) break; await recovered.text(); await new Promise((resolve) => setTimeout(resolve, 20)); } assert.equal(recovered.status, 200); assert.equal( JSON.parse((await recovered.text()).trim().split("\n").at(-1)).result.ok, true, ); } finally { for (const controller of controllers) controller.abort(); await f.close(); } }); test("job deadline kills a subprocess that ignores SIGTERM and returns a failed result", async () => { const f = await fixture({ jobTimeoutMs: 1000, terminateGraceMs: 50 }); try { const response = await f.post([f.command({ VITE_FIXTURE_MODE: "ignore" })]); const body = await response.text(); assert.match(body, /ready/); const result = JSON.parse(body.trim().split("\n").at(-1)).result; assert.equal(result.ok, false); assert.equal(result.aborted, true); assert.equal(f.children.length, 1); assert.equal(alive(f.children[0].pid), false); } finally { await f.close(); } }); test("slow response consumer cannot retain a flooding build subprocess", async () => { const f = await fixture({ terminateGraceMs: 50 }); try { const response = await f.post([f.command({ VITE_FIXTURE_MODE: "flood" })]); await waitFor(() => f.children.length === 1 && !alive(f.children[0].pid)); await response.body.cancel().catch(() => {}); const next = await f.post(); assert.equal(next.status, 200); assert.equal( JSON.parse((await next.text()).trim().split("\n").at(-1)).result.ok, true, ); } finally { await f.close(); } }); test("a stalled authenticated body is rejected within the body deadline", async () => { const f = await fixture({ bodyTimeoutMs: 100 }); try { const status = await new Promise((resolve, reject) => { const req = request( f.url, { method: "POST", headers: { "x-release-builder-token": token, "content-length": "100", }, }, (res) => { res.resume(); res.once("end", () => resolve(res.statusCode)); }, ); req.once("error", reject); req.flushHeaders(); }); assert.equal(status, 408); assert.equal(f.children.length, 0); } finally { await f.close(); } });