import fs from "node:fs"; import type { IncomingMessage, ServerResponse } from "node:http"; import path from "node:path"; import { fileURLToPath } from "node:url"; const ROOT_PREFIX = "/"; export type ControlUiRequestOptions = { basePath?: string; }; export function normalizeControlUiBasePath(basePath?: string): string { if (!basePath) return ""; let normalized = basePath.trim(); if (!normalized) return ""; if (!normalized.startsWith("/")) normalized = `/${normalized}`; if (normalized === "/") return ""; if (normalized.endsWith("/")) normalized = normalized.slice(0, -1); return normalized; } function resolveControlUiRoot(): string | null { const here = path.dirname(fileURLToPath(import.meta.url)); const execDir = (() => { try { return path.dirname(fs.realpathSync(process.execPath)); } catch { return null; } })(); const candidates = [ // Packaged relay: Resources/Relay/control-ui execDir ? path.resolve(execDir, "control-ui") : null, // 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"), ].filter((dir): dir is string => Boolean(dir)); 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 injectControlUiBasePath(html: string, basePath: string): string { const script = ``; if (html.includes("__CLAWDBOT_CONTROL_UI_BASE_PATH__")) return html; const headClose = html.indexOf(""); if (headClose !== -1) { return `${html.slice(0, headClose)}${script}${html.slice(headClose)}`; } return `${script}${html}`; } function serveIndexHtml( res: ServerResponse, indexPath: string, basePath: string, ) { res.setHeader("Content-Type", "text/html; charset=utf-8"); res.setHeader("Cache-Control", "no-cache"); const raw = fs.readFileSync(indexPath, "utf8"); res.end(injectControlUiBasePath(raw, basePath)); } 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, opts?: ControlUiRequestOptions, ): 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"); const basePath = normalizeControlUiBasePath(opts?.basePath); const pathname = url.pathname; if (!basePath) { if (pathname === "/ui" || pathname.startsWith("/ui/")) { respondNotFound(res); return true; } } if (basePath) { if (pathname === basePath) { res.statusCode = 302; res.setHeader("Location", `${basePath}/${url.search}`); res.end(); return true; } if (!pathname.startsWith(`${basePath}/`)) return false; } 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 `bun run ui:build` (or run `bun run ui:dev` during development).", ); return true; } const uiPath = basePath && pathname.startsWith(`${basePath}/`) ? pathname.slice(basePath.length) : pathname; const rel = (() => { if (uiPath === ROOT_PREFIX) return ""; const assetsIndex = uiPath.indexOf("/assets/"); if (assetsIndex >= 0) return uiPath.slice(assetsIndex + 1); return uiPath.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()) { if (path.basename(filePath) === "index.html") { serveIndexHtml(res, filePath, basePath); return true; } 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)) { serveIndexHtml(res, indexPath, basePath); return true; } respondNotFound(res); return true; }