(options: ServeOptions)
| 53 | type ServerState = "serving" | "regenerating" | "done"; |
| 54 | |
| 55 | export async function serve(options: ServeOptions): Promise<void> { |
| 56 | const { html, port = 0, hostname = "127.0.0.1", timeout = 600 } = options; |
| 57 | |
| 58 | // Validate HTML file exists |
| 59 | if (!fs.existsSync(html)) { |
| 60 | console.error(`SERVE_ERROR: HTML file not found: ${html}`); |
| 61 | process.exit(1); |
| 62 | } |
| 63 | |
| 64 | // Security: anchor all file reads to the initial HTML's directory. |
| 65 | // Prevents /api/reload from reading arbitrary files via path traversal. |
| 66 | const allowedDir = fs.realpathSync(path.dirname(path.resolve(html))); |
| 67 | |
| 68 | let htmlContent = fs.readFileSync(html, "utf-8"); |
| 69 | let state: ServerState = "serving"; |
| 70 | let timeoutTimer: ReturnType<typeof setTimeout> | null = null; |
| 71 | |
| 72 | const server = Bun.serve({ |
| 73 | port, |
| 74 | hostname, |
| 75 | fetch(req) { |
| 76 | const url = new URL(req.url); |
| 77 | |
| 78 | // Serve the comparison board HTML. The board JS uses relative paths |
| 79 | // (./api/feedback, ./api/progress) and a location.protocol |
| 80 | // feature-detect, so no per-request injection is needed. |
| 81 | if ( |
| 82 | req.method === "GET" && |
| 83 | (url.pathname === "/" || url.pathname === "/index.html") |
| 84 | ) { |
| 85 | return new Response(htmlContent, { |
| 86 | headers: { "Content-Type": "text/html; charset=utf-8" }, |
| 87 | }); |
| 88 | } |
| 89 | |
| 90 | // Progress polling endpoint (used by board during regeneration) |
| 91 | if (req.method === "GET" && url.pathname === "/api/progress") { |
| 92 | return Response.json({ status: state }); |
| 93 | } |
| 94 | |
| 95 | // Feedback submission from the board |
| 96 | if (req.method === "POST" && url.pathname === "/api/feedback") { |
| 97 | return handleFeedback(req); |
| 98 | } |
| 99 | |
| 100 | // Reload endpoint (used by the agent to swap in new board HTML) |
| 101 | if (req.method === "POST" && url.pathname === "/api/reload") { |
| 102 | return handleReload(req); |
| 103 | } |
| 104 | |
| 105 | return new Response("Not found", { status: 404 }); |
| 106 | }, |
| 107 | }); |
| 108 | |
| 109 | const actualPort = server.port; |
| 110 | const boardUrl = `http://127.0.0.1:${actualPort}`; |
| 111 | |
| 112 | console.error(`SERVE_STARTED: port=${actualPort} html=${html}`); |
no test coverage detected