/** * Production server for @executor-js/local. * * Serves the Vite-built SPA + Effect API + MCP server. * * Run directly: bun run apps/local/src/serve.ts * Or import: import { startServer } from "@executor-js/local/serve" */ import { timingSafeEqual } from "node:crypto";import { resolve, join } from "node:path";import { readdirSync } from "node:fs";import { getServerHandlers } from "./server/main"; // ---------------------------------------------------------------------------// Host allowlist// --------------------------------------------------------------------------- const DEFAULT_ALLOWED_HOSTS = ["localhost", "127.0.0.1", "[::1]", "::1"];const LOOPBACK_BIND_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]", "::1"]); const normalizeCredential = (value: string | undefined): string | null => { const normalized = value?.trim(); return normalized && normalized.length > 0 ? normalized : null;}; const safeEqual = (actual: string, expected: string): boolean => { const actualBytes = Buffer.from(actual); const expectedBytes = Buffer.from(expected); return actualBytes.length === expectedBytes.length && timingSafeEqual(actualBytes, expectedBytes);}; const isLoopbackBindHost = (hostname: string): boolean => LOOPBACK_BIND_HOSTS.has(hostname.trim().toLowerCase()); const makeIsAllowedHost = (allowed: ReadonlySet<string>) => (request: Request): boolean => { const host = request.headers.get("host"); if (!host) return true; const hostname = host.replace(/:\d+$/, ""); return allowed.has(hostname); }; const hasBearerToken = (request: Request, token: string): boolean => { const authorization = request.headers.get("authorization"); const bearer = authorization?.match(/^Bearer\s+(.+)$/i)?.[1]?.trim(); return ( (bearer !== undefined && safeEqual(bearer, token)) || safeEqual(request.headers.get("x-executor-token") ?? "", token) );}; const hasBasicPassword = (request: Request, password: string): boolean => { const authorization = request.headers.get("authorization"); const encoded = authorization?.match(/^Basic\s+(.+)$/i)?.[1]?.trim(); if (!encoded) return false; let decoded: string; // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: Basic auth decoding accepts untrusted header bytes try { decoded = Buffer.from(encoded, "base64").toString("utf8"); } catch { return false; } const separator = decoded.indexOf(":"); const actualPassword = separator >= 0 ? decoded.slice(separator + 1) : decoded; return safeEqual(actualPassword, password);}; const makeIsAuthorized = (auth: { token: string | null; password: string | null }) => (request: Request): boolean => (auth.token !== null && hasBearerToken(request, auth.token)) || (auth.password !== null && hasBasicPassword(request, auth.password)); // ---------------------------------------------------------------------------// Static files// --------------------------------------------------------------------------- type StaticHandler = () => Response | Promise<Response>; const hasFileExtension = (pathname: string): boolean => { const lastSegment = pathname.split("/").at(-1) ?? ""; return lastSegment.includes(".");}; function collectStaticRoutes(dir: string, prefix = ""): Record<string, StaticHandler> { const routes: Record<string, StaticHandler> = {}; // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: filesystem route discovery is best-effort for optional built assets try { for (const entry of readdirSync(dir, { withFileTypes: true })) { const fullPath = join(dir, entry.name); const routePath = `${prefix}/${entry.name}`; if (entry.isDirectory()) { Object.assign(routes, collectStaticRoutes(fullPath, routePath)); } else { const file = Bun.file(fullPath); routes[routePath] = () => new Response(file, { headers: { "content-type": file.type || "application/octet-stream" }, }); } } } catch {} return routes;} /** * Convert an embedded web UI map (path → bunfs path) into Bun.serve routes. * The embedded map is generated by the CLI build step using `with { type: "file" }`. */function embeddedToStaticRoutes(embedded: Record<string, string>): Record<string, StaticHandler> { const routes: Record<string, StaticHandler> = {}; for (const [key, bunfsPath] of Object.entries(embedded)) { const file = Bun.file(bunfsPath); routes[`/${key}`] = () => new Response(file, { headers: { "content-type": file.type || "application/octet-stream" }, }); } return routes;} // ---------------------------------------------------------------------------// Server// --------------------------------------------------------------------------- export interface StartServerOptions { port?: number; /** Directory containing built client assets (for dev/disk serving). */ clientDir?: string; /** Embedded web UI map from compiled binary (path → bunfs path). Overrides clientDir. */ embeddedWebUI?: Record<string, string> | null; /** Bind address. Defaults to 127.0.0.1. Use 0.0.0.0 to listen on all interfaces. */ hostname?: string; /** Extra hostnames permitted in the Host header, on top of localhost/127.0.0.1. */ allowedHosts?: ReadonlyArray<string>; /** Bearer token required for requests. Required for non-loopback bind addresses. */ authToken?: string; /** Basic auth password required for requests. Required for non-loopback bind addresses. */ authPassword?: string; /** Test hook for supplying API/MCP handlers without loading the local server graph. */ handlers?: ServerHandlers;} export interface ServerInstance { port: number; stop: () => Promise<void>;} type ServerHandlers = Awaited<ReturnType<typeof getServerHandlers>>; export async function startServer(opts: StartServerOptions = {}): Promise<ServerInstance> { const port = opts.port ?? parseInt(process.env.PORT ?? "4788", 10); const hostname = opts.hostname ?? "127.0.0.1"; const auth = { token: normalizeCredential(opts.authToken), password: normalizeCredential(opts.authPassword), }; const isNetworkBind = !isLoopbackBindHost(hostname); const requiresAuth = auth.token !== null || auth.password !== null; if (isNetworkBind && !requiresAuth) { // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: startServer is a Promise API and rejects invalid bind options throw new Error("Refusing to listen on a non-loopback host without an auth token or password."); } const isAuthorized = makeIsAuthorized(auth); const allowedHostSet = new Set<string>([...DEFAULT_ALLOWED_HOSTS, ...(opts.allowedHosts ?? [])]); const isAllowedHost = makeIsAllowedHost(allowedHostSet); const clientDir = opts.clientDir ?? resolve(import.meta.dirname, "../dist"); const handlers = opts.handlers ?? (await getServerHandlers()); // Build static routes from either embedded assets or disk let staticRoutes: Record<string, StaticHandler>; let serveIndex: StaticHandler; if (opts.embeddedWebUI) { staticRoutes = embeddedToStaticRoutes(opts.embeddedWebUI); const indexFile = Bun.file(opts.embeddedWebUI["index.html"] ?? join(clientDir, "index.html")); serveIndex = () => new Response(indexFile, { headers: { "content-type": "text/html" } }); } else { staticRoutes = collectStaticRoutes(clientDir); const indexFile = Bun.file(join(clientDir, "index.html")); serveIndex = () => new Response(indexFile, { headers: { "content-type": "text/html" } }); } const server = Bun.serve({ port, hostname, // Disable Bun's default 10s idle timeout. MCP elicitation and pause/resume // can idle longer during human approval; `0` disables the socket timeout. idleTimeout: 0, routes: { ...staticRoutes }, async fetch(req) { if (!isAllowedHost(req)) { return new Response("Forbidden", { status: 403 }); } if (requiresAuth && !isAuthorized(req)) { return new Response("Unauthorized", { status: 401, headers: { "www-authenticate": 'Bearer realm="executor", Basic realm="executor"' }, }); } const url = new URL(req.url); if (url.pathname.startsWith("/mcp")) { return handlers.mcp.handleRequest(req); } if (url.pathname.startsWith("/api/") || url.pathname === "/api") { url.pathname = url.pathname.slice("/api".length) || "/"; return handlers.api.handler(new Request(url, req)); } // If a path looks like a static asset (has a file extension), do not // fall back to SPA HTML. Returning index.html here causes browser module // MIME errors when hashed chunks are stale/missing. if (hasFileExtension(url.pathname)) { return new Response("Not Found", { status: 404 }); } // SPA fallback return serveIndex(); }, error(error) { console.error("Server error:", error); return new Response("Internal Server Error", { status: 500 }); }, }); return { port: server.port!, async stop() { server.stop(true); await handlers.mcp.close(); await handlers.api.dispose(); }, };} if (import.meta.main) { const server = await startServer(); console.log(`Executor listening on http://localhost:${server.port}`);}
/**
* Production server for @executor-js/local.
*
* Serves the Vite-built SPA + Effect API + MCP server.
*
* Run directly: bun run apps/local/src/serve.ts
* Or import: import { startServer } from "@executor-js/local/serve"
*/
import { timingSafeEqual } from "node:crypto";
import { resolve, join } from "node:path";
import { readdirSync } from "node:fs";
import { getServerHandlers } from "./server/main";
// ---------------------------------------------------------------------------
// Host allowlist
// ---------------------------------------------------------------------------
const DEFAULT_ALLOWED_HOSTS = ["localhost", "127.0.0.1", "[::1]", "::1"];
const LOOPBACK_BIND_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
const normalizeCredential = (value: string | undefined): string | null => {
const normalized = value?.trim();
return normalized && normalized.length > 0 ? normalized : null;
};
const safeEqual = (actual: string, expected: string): boolean => {
const actualBytes = Buffer.from(actual);
const expectedBytes = Buffer.from(expected);
return actualBytes.length === expectedBytes.length && timingSafeEqual(actualBytes, expectedBytes);
};
const isLoopbackBindHost = (hostname: string): boolean =>
LOOPBACK_BIND_HOSTS.has(hostname.trim().toLowerCase());
const makeIsAllowedHost =
(allowed: ReadonlySet<string>) =>
(request: Request): boolean => {
const host = request.headers.get("host");
if (!host) return true;
const hostname = host.replace(/:\d+$/, "");
return allowed.has(hostname);
};
const hasBearerToken = (request: Request, token: string): boolean => {
const authorization = request.headers.get("authorization");
const bearer = authorization?.match(/^Bearer\s+(.+)$/i)?.[1]?.trim();
return (
(bearer !== undefined && safeEqual(bearer, token)) ||
safeEqual(request.headers.get("x-executor-token") ?? "", token)
);
};
const hasBasicPassword = (request: Request, password: string): boolean => {
const authorization = request.headers.get("authorization");
const encoded = authorization?.match(/^Basic\s+(.+)$/i)?.[1]?.trim();
if (!encoded) return false;
let decoded: string;
// oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: Basic auth decoding accepts untrusted header bytes
try {
decoded = Buffer.from(encoded, "base64").toString("utf8");
} catch {
return false;
}
const separator = decoded.indexOf(":");
const actualPassword = separator >= 0 ? decoded.slice(separator + 1) : decoded;
return safeEqual(actualPassword, password);
};
const makeIsAuthorized =
(auth: { token: string | null; password: string | null }) =>
(request: Request): boolean =>
(auth.token !== null && hasBearerToken(request, auth.token)) ||
(auth.password !== null && hasBasicPassword(request, auth.password));
// ---------------------------------------------------------------------------
// Static files
// ---------------------------------------------------------------------------
type StaticHandler = () => Response | Promise<Response>;
const hasFileExtension = (pathname: string): boolean => {
const lastSegment = pathname.split("/").at(-1) ?? "";
return lastSegment.includes(".");
};
function collectStaticRoutes(dir: string, prefix = ""): Record<string, StaticHandler> {
const routes: Record<string, StaticHandler> = {};
// oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: filesystem route discovery is best-effort for optional built assets
try {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const fullPath = join(dir, entry.name);
const routePath = `${prefix}/${entry.name}`;
if (entry.isDirectory()) {
Object.assign(routes, collectStaticRoutes(fullPath, routePath));
} else {
const file = Bun.file(fullPath);
routes[routePath] = () =>
new Response(file, {
headers: { "content-type": file.type || "application/octet-stream" },
});
}
}
} catch {}
return routes;
}
/**
* Convert an embedded web UI map (path → bunfs path) into Bun.serve routes.
* The embedded map is generated by the CLI build step using `with { type: "file" }`.
*/
function embeddedToStaticRoutes(embedded: Record<string, string>): Record<string, StaticHandler> {
const routes: Record<string, StaticHandler> = {};
for (const [key, bunfsPath] of Object.entries(embedded)) {
const file = Bun.file(bunfsPath);
routes[`/${key}`] = () =>
new Response(file, {
headers: { "content-type": file.type || "application/octet-stream" },
});
}
return routes;
}
// ---------------------------------------------------------------------------
// Server
// ---------------------------------------------------------------------------
export interface StartServerOptions {
port?: number;
/** Directory containing built client assets (for dev/disk serving). */
clientDir?: string;
/** Embedded web UI map from compiled binary (path → bunfs path). Overrides clientDir. */
embeddedWebUI?: Record<string, string> | null;
/** Bind address. Defaults to 127.0.0.1. Use 0.0.0.0 to listen on all interfaces. */
hostname?: string;
/** Extra hostnames permitted in the Host header, on top of localhost/127.0.0.1. */
allowedHosts?: ReadonlyArray<string>;
/** Bearer token required for requests. Required for non-loopback bind addresses. */
authToken?: string;
/** Basic auth password required for requests. Required for non-loopback bind addresses. */
authPassword?: string;
/** Test hook for supplying API/MCP handlers without loading the local server graph. */
handlers?: ServerHandlers;
}
export interface ServerInstance {
port: number;
stop: () => Promise<void>;
}
type ServerHandlers = Awaited<ReturnType<typeof getServerHandlers>>;
export async function startServer(opts: StartServerOptions = {}): Promise<ServerInstance> {
const port = opts.port ?? parseInt(process.env.PORT ?? "4788", 10);
const hostname = opts.hostname ?? "127.0.0.1";
const auth = {
token: normalizeCredential(opts.authToken),
password: normalizeCredential(opts.authPassword),
};
const isNetworkBind = !isLoopbackBindHost(hostname);
const requiresAuth = auth.token !== null || auth.password !== null;
if (isNetworkBind && !requiresAuth) {
// oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: startServer is a Promise API and rejects invalid bind options
throw new Error("Refusing to listen on a non-loopback host without an auth token or password.");
}
const isAuthorized = makeIsAuthorized(auth);
const allowedHostSet = new Set<string>([...DEFAULT_ALLOWED_HOSTS, ...(opts.allowedHosts ?? [])]);
const isAllowedHost = makeIsAllowedHost(allowedHostSet);
const clientDir = opts.clientDir ?? resolve(import.meta.dirname, "../dist");
const handlers = opts.handlers ?? (await getServerHandlers());
// Build static routes from either embedded assets or disk
let staticRoutes: Record<string, StaticHandler>;
let serveIndex: StaticHandler;
if (opts.embeddedWebUI) {
staticRoutes = embeddedToStaticRoutes(opts.embeddedWebUI);
const indexFile = Bun.file(opts.embeddedWebUI["index.html"] ?? join(clientDir, "index.html"));
serveIndex = () => new Response(indexFile, { headers: { "content-type": "text/html" } });
} else {
staticRoutes = collectStaticRoutes(clientDir);
const indexFile = Bun.file(join(clientDir, "index.html"));
serveIndex = () => new Response(indexFile, { headers: { "content-type": "text/html" } });
}
const server = Bun.serve({
port,
hostname,
// Disable Bun's default 10s idle timeout. MCP elicitation and pause/resume
// can idle longer during human approval; `0` disables the socket timeout.
idleTimeout: 0,
routes: { ...staticRoutes },
async fetch(req) {
if (!isAllowedHost(req)) {
return new Response("Forbidden", { status: 403 });
}
if (requiresAuth && !isAuthorized(req)) {
return new Response("Unauthorized", {
status: 401,
headers: { "www-authenticate": 'Bearer realm="executor", Basic realm="executor"' },
});
}
const url = new URL(req.url);
if (url.pathname.startsWith("/mcp")) {
return handlers.mcp.handleRequest(req);
}
if (url.pathname.startsWith("/api/") || url.pathname === "/api") {
url.pathname = url.pathname.slice("/api".length) || "/";
return handlers.api.handler(new Request(url, req));
}
// If a path looks like a static asset (has a file extension), do not
// fall back to SPA HTML. Returning index.html here causes browser module
// MIME errors when hashed chunks are stale/missing.
if (hasFileExtension(url.pathname)) {
return new Response("Not Found", { status: 404 });
}
// SPA fallback
return serveIndex();
},
error(error) {
console.error("Server error:", error);
return new Response("Internal Server Error", { status: 500 });
},
});
return {
port: server.port!,
async stop() {
server.stop(true);
await handlers.mcp.close();
await handlers.api.dispose();
},
};
}
if (import.meta.main) {
const server = await startServer();
console.log(`Executor listening on http://localhost:${server.port}`);
}