import fs from "node:fs"; import type { IncomingMessage, ServerResponse } from "node:http"; import path from "node:path"; import { fileURLToPath } from "node:url"; const _UI_PREFIX = "/ui/"; const ROOT_PREFIX = "/"; function resolveControlUiRoot(): string | null { const here = path.dirname(fileURLToPath(import.meta.url)); const candidates = [ // Running from dist: dist/gateway/control-ui.js -> dist/control-ui path.resolve(here, "../control-ui"), // Running from source: src/gateway/control-ui.ts -> dist/control-ui path.resolve(here, "../../dist/control-ui"), // Fallback to cwd (dev) path.resolve(process.cwd(), "dist", "control-ui"), ]; for (const dir of candidates) { if (fs.existsSync(path.join(dir, "index.html"))) return dir; } return null; } function contentTypeForExt(ext: string): string { switch (ext) { case ".html": return "text/html; charset=utf-8"; case ".js": return "application/javascript; charset=utf-8"; case ".css": return "text/css; charset=utf-8"; case ".json": case ".map": return "application/json; charset=utf-8"; case ".svg": return "image/svg+xml"; case ".png": return "image/png"; case ".jpg": case ".jpeg": return "image/jpeg"; case ".ico": return "image/x-icon"; case ".txt": return "text/plain; charset=utf-8"; default: return "application/octet-stream"; } } function respondNotFound(res: ServerResponse) { res.statusCode = 404; res.setHeader("Content-Type", "text/plain; charset=utf-8"); res.end("Not Found"); } function serveFile(res: ServerResponse, filePath: string) { const ext = path.extname(filePath).toLowerCase(); res.setHeader("Content-Type", contentTypeForExt(ext)); // Static UI should never be cached aggressively while iterating; allow the // browser to revalidate. res.setHeader("Cache-Control", "no-cache"); res.end(fs.readFileSync(filePath)); } function isSafeRelativePath(relPath: string) { if (!relPath) return false; const normalized = path.posix.normalize(relPath); if (normalized.startsWith("../") || normalized === "..") return false; if (normalized.includes("\0")) return false; return true; } export function handleControlUiHttpRequest( req: IncomingMessage, res: ServerResponse, ): boolean { const urlRaw = req.url; if (!urlRaw) return false; if (req.method !== "GET" && req.method !== "HEAD") { res.statusCode = 405; res.setHeader("Content-Type", "text/plain; charset=utf-8"); res.end("Method Not Allowed"); return true; } const url = new URL(urlRaw, "http://localhost"); if (url.pathname === "/ui" || url.pathname.startsWith("/ui/")) { respondNotFound(res); return true; } const root = resolveControlUiRoot(); if (!root) { res.statusCode = 503; res.setHeader("Content-Type", "text/plain; charset=utf-8"); res.end( "Control UI assets not found. Build them with `pnpm ui:build` (or run `pnpm ui:dev` during development).", ); return true; } const rel = (() => { if (url.pathname === ROOT_PREFIX) return ""; if (url.pathname.startsWith("/assets/")) return url.pathname.slice(1); return url.pathname.slice(1); })(); const requested = rel && !rel.endsWith("/") ? rel : `${rel}index.html`; const fileRel = requested || "index.html"; if (!isSafeRelativePath(fileRel)) { respondNotFound(res); return true; } const filePath = path.join(root, fileRel); if (!filePath.startsWith(root)) { respondNotFound(res); return true; } if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) { serveFile(res, filePath); return true; } // SPA fallback (client-side router): serve index.html for unknown paths. const indexPath = path.join(root, "index.html"); if (fs.existsSync(indexPath)) { serveFile(res, indexPath); return true; } respondNotFound(res); return true; }