import { ServerResponse, createServer, type IncomingMessage } from "node:http"; import { createReadStream } from "node:fs"; import { stat } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import type { ServerBuild } from "react-router"; import { loadRequiredWebConfig } from "@toolctrl/config"; import { createChildLogger, defaultLogger } from "@toolctrl/logger"; import { app as apiApp } from "../apps/api/src/app"; import { getHostRoutingRuntimeDecision } from "./host-routing-runtime"; process.env.NODE_ENV = process.env.NODE_ENV ?? "production"; const RUNTIME_DIRECTORY = path.dirname(fileURLToPath(import.meta.url)); const REPO_ROOT_DIRECTORY = RUNTIME_DIRECTORY.endsWith(path.join("build", "server-runtime")) ? path.resolve(RUNTIME_DIRECTORY, "..", "..") : path.resolve(RUNTIME_DIRECTORY, ".."); const WEB_BUILD_DIRECTORY = process.env.WEB_BUILD_DIRECTORY ? path.resolve(process.env.WEB_BUILD_DIRECTORY) : path.join(REPO_ROOT_DIRECTORY, "apps", "web", "build"); const BUILD_PATH = path.join(WEB_BUILD_DIRECTORY, "server", "index.js"); const CLIENT_BUILD_DIRECTORY = path.join(WEB_BUILD_DIRECTORY, "client"); const ALLOWED_STATIC_ASSET_EXTENSIONS = new Set([ ".avif", ".css", ".eot", ".gif", ".ico", ".jpeg", ".jpg", ".js", ".json", ".mjs", ".png", ".svg", ".ttf", ".txt", ".webmanifest", ".webp", ".woff", ".woff2", ".xml", ]); const webConfig = loadRequiredWebConfig(); const PORT = webConfig.listen.port; const HOST = webConfig.listen.host; const serverLogger = createChildLogger(defaultLogger, { component: "server" }); const [{ createRequestHandler }, { createReadableStreamFromReadable, writeReadableStreamToWritable }] = await Promise.all([ import("react-router"), import("@react-router/node"), ]); const build = (await import(pathToFileURL(BUILD_PATH).href)) as ServerBuild; const handleReactRouterRequest = createRequestHandler(build, process.env.NODE_ENV); type NodeRequestInit = RequestInit & { duplex?: "half" }; const server = createServer(async (req, res) => { try { if (await serveStaticAsset(req, res)) { return; } const request = createWebRequest(req, res); const routingDecision = getHostRoutingRuntimeDecision(request); if (routingDecision.type === "redirect") { await sendWebResponse( res, new Response(null, { status: routingDecision.status, headers: { Location: routingDecision.location }, }) ); return; } if (routingDecision.type === "reject") { await sendWebResponse(res, new Response("Not Found", { status: routingDecision.status })); return; } if (routingDecision.target === "web" && isWebOperationalRoute(request)) { await sendWebResponse(res, await handleReactRouterRequest(request)); return; } const response = routingDecision.target === "api" ? await apiApp.fetch(routingDecision.request) : await handleReactRouterRequest(routingDecision.request); await sendWebResponse(res, response); } catch (error) { serverLogger.error({ error }, "server request failed"); if (!res.headersSent) { res.statusCode = 500; res.setHeader("Content-Type", "text/plain; charset=utf-8"); } res.end("Internal Server Error"); } }); server.listen(PORT, HOST, () => { const address = HOST ?? getLocalAddress() ?? "localhost"; serverLogger.info({ host: address, port: PORT }, "server listening"); }); for (const signal of ["SIGTERM", "SIGINT"] as const) { process.once(signal, () => { server.close((error) => { if (error) { serverLogger.error({ error }, "server shutdown failed"); process.exitCode = 1; } }); }); } async function serveStaticAsset(req: IncomingMessage, res: ServerResponse) { if (req.method !== "GET" && req.method !== "HEAD") { return false; } const requestUrl = new URL(req.url ?? "/", "http://internal"); const filePath = getClientFilePath(requestUrl.pathname); if (!filePath) { return false; } const fileStat = await stat(filePath).catch(() => null); if (!fileStat?.isFile()) { return false; } const headers = getStaticHeaders(filePath, requestUrl.pathname); res.writeHead(200, headers); if (req.method === "HEAD") { res.end(); return true; } createReadStream(filePath) .on("error", (error) => { serverLogger.error({ error }, "static asset stream failed"); if (!res.headersSent) { res.writeHead(500); } res.end(); }) .pipe(res); return true; } function getClientFilePath(pathname: string) { let decodedPathname: string; try { decodedPathname = decodeURIComponent(pathname); } catch { return null; } const relativePath = decodedPathname.replace(/^\/+/, ""); if (!relativePath) { return null; } if (!ALLOWED_STATIC_ASSET_EXTENSIONS.has(path.extname(relativePath).toLowerCase())) { return null; } const filePath = path.resolve(CLIENT_BUILD_DIRECTORY, relativePath); if (filePath !== CLIENT_BUILD_DIRECTORY && !filePath.startsWith(`${CLIENT_BUILD_DIRECTORY}${path.sep}`)) { return null; } return filePath; } function createWebRequest(req: IncomingMessage, res: ServerResponse) { const protocol = req.headers["x-forwarded-proto"]?.toString().split(",")[0]?.trim() || "http"; const host = req.headers.host || "localhost"; const url = new URL(req.url ?? "/", `${protocol}://${host}`); const controller = new AbortController(); res.on("close", () => { controller.abort(); }); const init: NodeRequestInit = { method: req.method, headers: createWebHeaders(req.headers), signal: controller.signal, }; if (req.method !== "GET" && req.method !== "HEAD") { init.body = createReadableStreamFromReadable(req); init.duplex = "half"; } return new Request(url, init); } function isWebOperationalRoute(request: Request) { const { pathname } = new URL(request.url); return pathname === "/healthz" || pathname === "/readyz" || pathname === "/healthcheck"; } function createWebHeaders(headers: IncomingMessage["headers"]) { const webHeaders = new Headers(); for (const [key, value] of Object.entries(headers)) { if (Array.isArray(value)) { for (const item of value) { webHeaders.append(key, item); } } else if (value !== undefined) { webHeaders.set(key, value); } } return webHeaders; } async function sendWebResponse(res: ServerResponse, response: Response) { res.statusCode = response.status; res.statusMessage = response.statusText; for (const [key, value] of response.headers) { res.appendHeader(key, value); } appendWebSecurityHeaders(res); if (!response.body) { res.end(); return; } await writeReadableStreamToWritable(response.body, res); } function appendWebSecurityHeaders(headers: Headers): Headers; function appendWebSecurityHeaders(headers: Record): Record; function appendWebSecurityHeaders(headers: ServerResponse): ServerResponse; function appendWebSecurityHeaders(headers: Headers | Record | ServerResponse) { const securityHeaders: Record = { "Content-Security-Policy": "base-uri 'self'; object-src 'none'; frame-ancestors 'self'", "X-Content-Type-Options": "nosniff", "Referrer-Policy": "strict-origin-when-cross-origin", "X-Frame-Options": "SAMEORIGIN", "Permissions-Policy": "camera=(), microphone=(), geolocation=(), payment=()", "Cross-Origin-Opener-Policy": "same-origin", "Cross-Origin-Resource-Policy": "same-origin", "Cross-Origin-Embedder-Policy": "unsafe-none", }; if (webConfig.routing.protocol === "https") { securityHeaders["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"; } for (const [key, value] of Object.entries(securityHeaders)) { if (key === "Content-Security-Policy" && hasHeader(headers, key)) { continue; } if (headers instanceof Headers) { headers.set(key, value); } else if (headers instanceof ServerResponse) { headers.setHeader(key, value); } else { headers[key] = value; } } return headers; } function hasHeader(headers: Headers | Record | ServerResponse, key: string) { if (headers instanceof Headers) { return headers.has(key); } if (headers instanceof ServerResponse) { return headers.hasHeader(key); } const normalizedKey = key.toLowerCase(); return Object.keys(headers).some((candidate) => candidate.toLowerCase() === normalizedKey); } function getStaticHeaders(filePath: string, pathname: string) { const headers: Record = { "Content-Type": getContentType(filePath), }; // Cache-Control strategy aligned with docs/ops/asset-cdn.md: // * /assets/* → Vite-hashed, safe to cache forever (1 year, immutable) // * /fonts/* → static brand assets, 1 day // * /images/* → static brand assets, 1 day // Anything else (HTML / loader responses / favicons) keeps the default // (no Cache-Control here — the loader / entry.server is in charge). if (pathname === "/assets" || pathname.startsWith("/assets/")) { headers["Cache-Control"] = "public, max-age=31536000, immutable"; } else if (pathname === "/fonts" || pathname.startsWith("/fonts/") || pathname === "/images" || pathname.startsWith("/images/")) { headers["Cache-Control"] = "public, max-age=86400"; } return appendWebSecurityHeaders(headers); } function getContentType(filePath: string) { switch (path.extname(filePath).toLowerCase()) { case ".css": return "text/css; charset=utf-8"; case ".js": case ".mjs": return "text/javascript; charset=utf-8"; case ".json": case ".webmanifest": 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 ".gif": return "image/gif"; case ".ico": return "image/x-icon"; case ".txt": return "text/plain; charset=utf-8"; case ".html": return "text/html; charset=utf-8"; case ".woff": return "font/woff"; case ".woff2": return "font/woff2"; case ".ttf": return "font/ttf"; default: return "application/octet-stream"; } } function getLocalAddress() { return Object.values(os.networkInterfaces()) .flat() .find((entry) => entry && String(entry.family).includes("4") && !entry.internal)?.address; }