import { createServer } from "node:http"; import { existsSync, readFileSync } from "node:fs"; import { isAbsolute, relative, resolve } from "node:path"; import { getMimeType } from "@hyperframes/core/studio-api"; export interface StaticProjectServer { url: string; port: number; close: () => Promise; } export async function serveStaticProjectHtml( projectDir: string, html: string, bindErrorMessage = "Failed to bind local HTTP server", ): Promise { // fallow-ignore-next-line complexity const server = createServer((req, res) => { const url = req.url ?? "/"; if (url === "/" || url === "/index.html") { res.writeHead(200, { "Content-Type": "text/html" }); res.end(html); return; } const filePath = resolve(projectDir, decodeURIComponent(url).replace(/^\//, "")); const rel = relative(projectDir, filePath); if (rel.startsWith("..") || isAbsolute(rel)) { res.writeHead(403); res.end(); return; } if (existsSync(filePath)) { res.writeHead(200, { "Content-Type": getMimeType(filePath) }); res.end(readFileSync(filePath)); return; } res.writeHead(404); res.end(); }); const port = await new Promise((resolvePort, rejectPort) => { server.on("error", rejectPort); // Bind loopback only (SECURITY F-001): a bare listen(0) binds 0.0.0.0/::, // which an IDE's port auto-forward surfaces as a transient "preview". The // snapshot browser is co-located (url below is already 127.0.0.1). server.listen(0, "127.0.0.1", () => { const addr = server.address(); const resolvedPort = typeof addr === "object" && addr ? addr.port : 0; if (!resolvedPort) rejectPort(new Error(bindErrorMessage)); else resolvePort(resolvedPort); }); }); return { url: `http://127.0.0.1:${port}/`, port, close: () => new Promise((resolveClose) => { server.close(() => resolveClose()); }), }; }