import { spawn } from "node:child_process"; import { createHash, timingSafeEqual } from "node:crypto"; import http from "node:http"; import { fileURLToPath } from "node:url"; import path from "node:path"; import { BUILD_TIMEOUT_MS, MAX_BUILD_FRAME_BYTES, readBuilderToken, validateBuildCommand, } from "./release-builder-security.mjs"; const roots = [ "/srv/core/repository", "/srv/core/profile-worktrees", "/srv/core/release-worktrees", "/workspace/core2026", ]; const caches = [...roots, "/srv/builder-cache"]; const appendTail = (current, chunk) => `${current}${String(chunk)}`.slice(-64 * 1024); const digest = (value) => createHash("sha256").update(value).digest(); const terminate = (child, signal) => { if (!child.pid) return; try { process.kill(-child.pid, signal); } catch { try { child.kill(signal); } catch { /* Already exited. */ } } }; const readJson = (request, timeoutMs) => new Promise((resolve, reject) => { const body = Buffer.alloc(256 * 1024); let size = 0; const finish = (error, value) => { clearTimeout(timer); request .off("data", data) .off("end", end) .off("error", fail) .off("aborted", fail); if (error) reject(error); else resolve(value); }; const fail = () => finish( Object.assign(new Error("Build request interrupted."), { status: 400 }), ); const data = (chunk) => { if (size + chunk.length > body.length) { request.pause(); finish( Object.assign(new Error("Build request is too large."), { status: 413, }), ); return; } chunk.copy(body, size); size += chunk.length; }; const end = () => { try { finish( null, JSON.parse( new TextDecoder("utf-8", { fatal: true }).decode( body.subarray(0, size), ), ), ); } catch { finish( Object.assign(new Error("Invalid build JSON."), { status: 400 }), ); } }; const timer = setTimeout(() => { request.pause(); finish( Object.assign(new Error("Build request timed out."), { status: 408 }), ); }, timeoutMs); timer.unref(); request .on("data", data) .once("end", end) .once("error", fail) .once("aborted", fail); }); export const createReleaseBuilderServer = ({ token, allowedRoots = roots, cacheRoots = caches, spawnImpl = spawn, maxJobs = 3, jobTimeoutMs = BUILD_TIMEOUT_MS, bodyTimeoutMs = 5000, terminateGraceMs = 5000, baseEnv = process.env, }) => { if (!/^[A-Za-z0-9_-]{43,128}$/.test(token ?? "")) throw new Error("A valid builder token is required."); const tokenDigest = digest(token); // Repository scripts are trusted release inputs; unrelated process secrets are excluded. const childBaseEnv = Object.fromEntries( Object.entries(baseEnv).filter( ([name, value]) => typeof value === "string" && [ "PATH", "HOME", "LANG", "TZ", "CI", "NODE_OPTIONS", "PROFILE_FRONTEND_BUILD_NODE_OPTIONS", "RAYON_NUM_THREADS", "RELEASE_TURBO_CONCURRENCY", "TURBO_CACHE_DIR", "XDG_DATA_HOME", ].includes(name), ), ); let jobs = 0; let queue = Promise.resolve(); const controllers = new Set(); const children = new Set(); const runCommand = (command, emit, signal) => new Promise((resolve) => { if (signal.aborted) { resolve({ ok: false, exitCode: null, output: "", aborted: true }); return; } emit({ event: { type: "COMMAND_START", command } }); if (signal.aborted) { resolve({ ok: false, exitCode: null, output: "", aborted: true }); return; } const child = spawnImpl(command.command, command.args, { cwd: command.cwd, env: { ...childBaseEnv, ...command.env }, stdio: ["ignore", "pipe", "pipe"], detached: true, }); children.add(child); let output = ""; let failed = false; let killTimer; const abort = () => { terminate(child, "SIGTERM"); killTimer = setTimeout( () => terminate(child, "SIGKILL"), terminateGraceMs, ); killTimer.unref(); }; signal.addEventListener("abort", abort, { once: true }); if (signal.aborted) abort(); for (const stream of ["stdout", "stderr"]) child[stream].on("data", (chunk) => { if (signal.aborted) return; output = appendTail(output, chunk); const text = String(chunk); for (let offset = 0; offset < text.length; offset += 2000) emit({ event: { type: "OUTPUT", stream, message: text.slice(offset, offset + 2000), }, }); }); child.once("error", () => { failed = true; output = appendTail(output, "Release build process failed."); }); child.once("close", (exitCode) => { children.delete(child); signal.removeEventListener("abort", abort); clearTimeout(killTimer); emit({ event: { type: "COMMAND_END", command, exitCode: failed ? null : exitCode, }, }); resolve({ ok: !signal.aborted && !failed && exitCode === 0, exitCode: failed ? null : exitCode, output, ...(signal.aborted ? { aborted: true } : {}), }); }); }); const server = http.createServer(async (request, response) => { request.on("error", () => {}); const reject = (status) => { response.writeHead(status, { "content-type": "text/plain", connection: "close", ...(status === 429 ? { "retry-after": "1" } : {}), }); response.end(`Release builder request rejected (${status}).`); }; if (request.method === "GET" && request.url === "/healthz") { response.writeHead(200, { "content-type": "text/plain" }).end("ok"); return; } if (request.method !== "POST" || request.url !== "/v1/builds") { reject(404); return; } const presented = request.headers["x-release-builder-token"]; if ( typeof presented !== "string" || !/^[A-Za-z0-9_-]{43,128}$/.test(presented) || !timingSafeEqual(digest(presented), tokenDigest) ) { reject(401); return; } if (jobs >= maxJobs) { reject(429); return; } jobs += 1; const controller = new AbortController(); controllers.add(controller); const deadline = setTimeout(() => controller.abort(), jobTimeoutMs); deadline.unref(); response.once("close", () => { if (!response.writableEnded) controller.abort(); }); try { const payload = await readJson(request, bodyTimeoutMs); if ( !payload || !Array.isArray(payload.commands) || payload.commands.length < 1 || payload.commands.length > 12 ) throw new Error("Invalid command list."); const commands = payload.commands.map((raw) => validateBuildCommand(raw, allowedRoots, cacheRoots), ); response.writeHead(200, { "content-type": "application/x-ndjson", "cache-control": "no-store", "x-content-type-options": "nosniff", }); response.flushHeaders(); let streamedBytes = 0; const emit = (message) => { if ( response.destroyed || response.writableEnded || controller.signal.aborted ) return; const line = `${JSON.stringify(message)}\n`; streamedBytes += Buffer.byteLength(line); if ( streamedBytes > 64 * 1024 * 1024 || Buffer.byteLength(line) > MAX_BUILD_FRAME_BYTES || response.writableLength + Buffer.byteLength(line) > 1024 * 1024 ) { controller.abort(); response.destroy(); return; } response.write(line); }; const execute = async () => { let output = ""; for (const command of commands) { const result = await runCommand(command, emit, controller.signal); output = appendTail(output, result.output); if (!result.ok) return { ...result, output }; } return { ok: true, exitCode: 0, output }; }; const execution = queue.then(execute); queue = execution.then( () => undefined, () => undefined, ); const result = await execution; if (controller.signal.aborted && !response.destroyed) response.end(`${JSON.stringify({ result })}\n`); else { emit({ result }); response.end(); } } catch (error) { controller.abort(); if (!response.headersSent) reject(error?.status ?? 400); else response.destroy(); } finally { clearTimeout(deadline); controllers.delete(controller); jobs -= 1; } }); server.maxConnections = 64; server.maxRequestsPerSocket = 20; server.headersTimeout = 10000; server.requestTimeout = 30000; server.keepAliveTimeout = 5000; server.on("close", () => { for (const controller of controllers) controller.abort(); for (const child of children) terminate(child, "SIGKILL"); }); return server; }; if ( process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url) ) { const server = createReleaseBuilderServer({ token: readBuilderToken(process.env.RELEASE_BUILDER_TOKEN_FILE), }); const port = Number(process.env.RELEASE_BUILDER_PORT ?? 15100); server.listen(port, "0.0.0.0", () => console.log(`[release-builder] listening on ${port}`), ); const shutdown = () => { server.closeAllConnections(); server.close(() => process.exit(0)); }; process.once("SIGINT", shutdown); process.once("SIGTERM", shutdown); }