import * as fs from "node:fs"; import * as path from "node:path"; export interface FactoryDeployOptions { command: string; token?: string; url?: string; yes?: boolean; gateReceipt?: string; out?: string; now?: Date; fetcher?: (url: string, init: { method: string; headers: Record; body: string }) => Promise<{ status: number; json(): Promise; text(): Promise }>; } export interface FactoryDeployReceipt { created_at: string; url: string; command: string; deploy_attempted: boolean; deploy_allowed: boolean; status: "blocked" | "posted" | "failed"; http_status?: number; response?: unknown; reason?: string; } const DEFAULT_URL = "http://77.42.112.29:8099/deploy"; const SAFE_COMMANDS = [ /^echo [A-Za-z0-9 _.,:;@/+\-=]+$/, /^cat \/tmp\/[A-Za-z0-9._/-]+$/, /^docker ps(?: --format '[A-Za-z0-9 {}_.:;@/+\-=|]+')?$/, /^docker logs --tail \d{1,4} [A-Za-z0-9_.-]+$/, /^docker inspect [A-Za-z0-9_.-]+$/, /^curl -fsS -m \d{1,2} http:\/\/127\.0\.0\.1:8099\/(?:health|factory-status)$/, ]; export async function deployToFactory(opts: FactoryDeployOptions): Promise { const url = opts.url ?? DEFAULT_URL; const blocked = blockReason(opts, url); if (blocked) return writeMaybe(opts.out, receipt(opts, url, "blocked", false, false, blocked)); try { const res = await (opts.fetcher ?? fetchJson)(url, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${opts.token}` }, body: JSON.stringify({ command: opts.command }), }); const body = await readBody(res); const posted = res.status >= 200 && res.status < 300; return writeMaybe(opts.out, { ...receipt(opts, url, posted ? "posted" : "failed", true, posted, posted ? undefined : `HTTP ${res.status}`), http_status: res.status, response: body, }); } catch (error) { const reason = error instanceof Error ? error.message : String(error); return writeMaybe(opts.out, receipt(opts, url, "failed", false, false, reason)); } } export function isAllowedFactoryCommand(command: string): boolean { return SAFE_COMMANDS.some((pattern) => pattern.test(command.trim())); } export function isAllowedDeployUrl(url: string): boolean { return url === DEFAULT_URL; } export function hasPassedGateReceipt(file: string): boolean { if (!fs.existsSync(file)) return false; return fs.readFileSync(file, "utf-8") .split(/\r?\n/) .filter(Boolean) .some((line) => { try { const json = JSON.parse(line) as { status?: unknown; task?: unknown; repo?: unknown; commands?: unknown; createdAt?: unknown }; return json.status === "passed" && typeof json.task === "string" && typeof json.repo === "string" && Array.isArray(json.commands) && json.commands.some((cmd) => typeof cmd === "string" && cmd.includes("fable5 verify")) && typeof json.createdAt === "string" && Number.isFinite(Date.parse(json.createdAt)); } catch { return false; } }); } export function writeFactoryDeployReceipt(file: string, value: FactoryDeployReceipt): FactoryDeployReceipt { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`); return value; } function blockReason(opts: FactoryDeployOptions, url: string): string | undefined { if (!opts.yes) return "missing --yes human approval"; if (!isAllowedDeployUrl(url)) return "deploy url must be the guarded 8099 git-proxy route"; if (!opts.gateReceipt) return "missing --gate-receipt verifier evidence"; if (!hasPassedGateReceipt(opts.gateReceipt)) return "gate receipt missing passed verifier status"; if (!opts.token) return "missing deploy token"; if (!isAllowedFactoryCommand(opts.command)) return "command is outside the guarded deploy allowlist"; return undefined; } function receipt(opts: FactoryDeployOptions, url: string, status: FactoryDeployReceipt["status"], attempted: boolean, allowed: boolean, reason?: string): FactoryDeployReceipt { return { created_at: (opts.now ?? new Date()).toISOString(), url, command: opts.command, deploy_attempted: attempted, deploy_allowed: allowed, status, reason }; } function writeMaybe(file: string | undefined, value: FactoryDeployReceipt): FactoryDeployReceipt { return file ? writeFactoryDeployReceipt(file, value) : value; } async function fetchJson(url: string, init: { method: string; headers: Record; body: string }) { return fetch(url, init); } async function readBody(res: { json(): Promise; text(): Promise }): Promise { try { return await res.json(); } catch { return await res.text(); } }