import { readFileSync, realpathSync, statSync } from "node:fs"; import path from "node:path"; export const MAX_BUILD_FRAME_BYTES = 512 * 1024; export const BUILD_TIMEOUT_MS = 30 * 60 * 1000; const packages = new Set( [ "common", "infra", "logic", "game-engine", "game-api", "game-frontend", "gateway-api", "gateway-frontend", "release-controller", ].map((name) => `@sammo-ts/${name}`), ); const within = (candidate, root) => { const relative = path.relative(root, candidate); return ( relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)) ); }; export const readBuilderToken = (filename) => { try { if ( !filename || !statSync(filename).isFile() || statSync(filename).size > 256 ) throw new Error(); const token = readFileSync(filename, "utf8").trim(); if ( token.startsWith("replace-with-") || !/^[A-Za-z0-9_-]{43,128}$/.test(token) ) throw new Error(); return token; } catch { throw new Error("A valid RELEASE_BUILDER_TOKEN_FILE is required."); } }; const statExists = (filename) => { try { statSync(filename); return true; } catch { return false; } }; const safeCachePath = (value, roots) => { if (!path.isAbsolute(value) || value.length > 2048) throw new Error("Invalid build cache path."); const resolved = path.resolve(value); if (!roots.some((root) => within(resolved, root))) throw new Error("Build cache is outside shared storage."); let existing = resolved; while (!statExists(existing)) existing = path.dirname(existing); if (!roots.some((root) => within(realpathSync(existing), root))) throw new Error("Build cache symlink leaves shared storage."); return resolved; }; const safeEnv = (env, roots) => { if ( !env || typeof env !== "object" || Array.isArray(env) || Object.keys(env).length > 32 ) throw new Error("Invalid build environment."); const result = {}; for (const [name, value] of Object.entries(env)) { if ( typeof value !== "string" || value.length > 8192 || value.includes("\0") ) throw new Error("Invalid build environment."); if ( /^VITE_[A-Z0-9_]+$/.test(name) || name === "TZ" || (name === "CI" && ["true", "false", "1", "0"].includes(value)) ) result[name] = value; else if ( ["NODE_OPTIONS", "PROFILE_FRONTEND_BUILD_NODE_OPTIONS"].includes(name) ) { const heap = /^--max-old-space-size=(\d+)$/.exec(value); if (!heap || Number(heap[1]) < 512 || Number(heap[1]) > 3584) throw new Error("Invalid build heap control."); result[name] = value; } else if ( ["RAYON_NUM_THREADS", "RELEASE_TURBO_CONCURRENCY"].includes(name) ) { if (!/^[1-4]$/.test(value)) throw new Error("Invalid build concurrency control."); result[name] = value; } else if (name === "TURBO_CACHE_DIR") result[name] = safeCachePath(value, roots); else if (name !== "PATH") throw new Error("Unsupported build environment variable."); } return result; }; export const validateBuildCommand = (raw, allowedRoots, cacheRoots) => { if ( !raw || typeof raw !== "object" || Array.isArray(raw) || !Array.isArray(raw.args) || raw.args.length > 40 || typeof raw.cwd !== "string" || raw.cwd.length > 2048 || !path.isAbsolute(raw.cwd) || raw.args.some( (arg) => typeof arg !== "string" || arg.length > 2048 || arg.includes("\0"), ) ) throw new Error("Invalid build command."); const cwd = realpathSync(raw.cwd); if (!allowedRoots.some((root) => within(cwd, root))) throw new Error("Build cwd is outside shared storage."); const args = raw.args; const install = args.length === 2 && args[0] === "install" && args[1] === "--frozen-lockfile"; const packageTask = args.length === 3 && args[0] === "--filter" && packages.has(args[1]) && (args[2] === "build" || (args[1] === "@sammo-ts/infra" && args[2] === "prisma:generate")); let turbo = false; if ( args[0] === "exec" && args[1] === "turbo" && args[2] === "run" && ["build", "build:release"].includes(args[3]) ) { const flags = args.slice(4); const seen = new Set(); let filters = 0; turbo = flags.every((flag) => { if (flag.startsWith("--filter=")) { filters += 1; return packages.has(flag.slice(9)); } const name = flag.split("=")[0]; if (seen.has(name)) return false; seen.add(name); if (flag.startsWith("--cache-dir=")) { safeCachePath(flag.slice(12), cacheRoots); return true; } return ( /^--concurrency=[1-4]$/.test(flag) || flag === "--ui=stream" || flag === "--output-logs=new-only" ); }) && filters > 0; } const materialize = args.length === 2 && args[0] === "tools/build-scripts/materialize-profile-frontend.mjs" && /^(?:che|kwe|pwe|twe|nya|pya|hwe):[A-Za-z0-9_.-]{1,64}$/.test(args[1]); if (!( (raw.command === "pnpm" && (install || packageTask || turbo)) || (raw.command === "node" && materialize) )) throw new Error("Unsupported release build command."); return { command: raw.command, args, cwd, env: safeEnv(raw.env ?? {}, cacheRoots), }; }; export async function* readBuilderMessages(response) { if (!response.ok || !response.body) { await response.body?.cancel().catch(() => undefined); throw new Error(`Release builder returned HTTP ${response.status}.`); } const reader = response.body.getReader(); const decoder = new TextDecoder("utf-8", { fatal: true }); let pending = ""; let bytes = 0; try { while (true) { const chunk = await reader.read(); if (chunk.done) break; bytes += chunk.value.byteLength; if (bytes > 64 * 1024 * 1024) throw new Error("Release builder stream is too large."); pending += decoder.decode(chunk.value, { stream: true }); let newline; while ((newline = pending.indexOf("\n")) >= 0) { const line = pending.slice(0, newline); pending = pending.slice(newline + 1); if (Buffer.byteLength(line) > MAX_BUILD_FRAME_BYTES) throw new Error("Release builder frame is too large."); if (line.trim()) yield JSON.parse(line); } if (Buffer.byteLength(pending) > MAX_BUILD_FRAME_BYTES) throw new Error("Release builder frame is too large."); } pending += decoder.decode(); if (Buffer.byteLength(pending) > MAX_BUILD_FRAME_BYTES) throw new Error("Release builder frame is too large."); if (pending.trim()) yield JSON.parse(pending); } finally { await reader.cancel().catch(() => undefined); reader.releaseLock(); } }