/** * earth-now.co * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai * File: infra/proxy.mjs * Purpose: Zero-dependency reverse proxy (nginx.conf equivalent) — routes /api,/v1,/sse,/badge to the API, the rest to web; SSE-safe (no buffering) */ import http from "node:http"; const PORT = Number(process.env.PROXY_PORT ?? 8080); const API = { host: "127.0.0.1", port: Number(process.env.API_PORT ?? 4000) }; const WEB = { host: "127.0.0.1", port: Number(process.env.WEB_PORT ?? 4100) }; const API_PREFIXES = ["/api", "/v1", "/sse", "/badge"]; function targetFor(url) { return API_PREFIXES.some((p) => url === p || url.startsWith(`${p}/`) || url.startsWith(`${p}?`)) ? API : WEB; } const server = http.createServer((req, res) => { const target = targetFor(req.url ?? "/"); const upstream = http.request( { host: target.host, port: target.port, path: req.url, method: req.method, headers: { ...req.headers, host: req.headers.host ?? "www.earth-now.co" }, }, (up) => { res.writeHead(up.statusCode ?? 502, up.headers); // pipe() flushes chunks as they arrive — SSE streams pass through unbuffered. up.pipe(res); }, ); upstream.on("error", () => { if (!res.headersSent) res.writeHead(502, { "content-type": "application/json" }); res.end(JSON.stringify({ error: "upstream unavailable" })); }); req.pipe(upstream); // If the client disconnects (closed SSE tab), tear down the upstream leg too. res.on("close", () => upstream.destroy()); }); // Long-lived SSE connections: disable the default 5-minute socket timeout. server.requestTimeout = 0; server.headersTimeout = 60_000; server.listen(PORT, "127.0.0.1", () => { console.log(`earth-now proxy on 127.0.0.1:${PORT} → api :${API.port}, web :${WEB.port}`); });