bimr-studio/proxy/node.mjs
2026-09-21 15:33:30 +02:00

79 lines
2.7 KiB
JavaScript

// SPDX-FileCopyrightText: 2026 Milovann Yanatchkov
// SPDX-License-Identifier: MIT
// proxy/node.mjs — the Node entry point for the Studio forge proxy.
//
// Bridges Node's http request/response to the Web Fetch handler in
// `proxy.mjs`, and starts a server when run directly:
//
// FORGE_ALLOWED_HOSTS=artefacts.bimr.net,bimr.net node proxy/node.mjs
//
// It is also imported by the Vite dev server to mount the same handler, so
// development and production share one implementation.
import http from "node:http";
import { Readable } from "node:stream";
import { pathToFileURL } from "node:url";
import { handleFetch } from "./proxy.mjs";
/** Build a Web Request from a Node incoming message. */
export function toWebRequest(req) {
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
const headers = new Headers();
for (const [name, value] of Object.entries(req.headers)) {
if (value === undefined) continue;
if (Array.isArray(value)) for (const entry of value) headers.append(name, entry);
else headers.set(name, value);
}
const method = req.method ?? "GET";
const init = { method, headers };
if (method !== "GET" && method !== "HEAD") {
init.body = Readable.toWeb(req);
init.duplex = "half";
}
return new Request(url, init);
}
/** Write a Web Response to a Node server response. */
export async function sendWebResponse(res, response) {
res.statusCode = response.status;
if (response.statusText) res.statusMessage = response.statusText;
response.headers.forEach((value, name) => res.setHeader(name, value));
if (response.body) Readable.fromWeb(response.body).pipe(res);
else res.end();
}
/** Handle one Node request/response pair through the proxy handler. */
export async function handleNode(req, res) {
try {
const response = await handleFetch(toWebRequest(req));
await sendWebResponse(res, response);
} catch (err) {
if (!res.headersSent) res.statusCode = 502;
res.end(`proxy error: ${err?.message ?? err}`);
}
}
export function startServer({
port = Number(process.env.PORT ?? 8787),
host = process.env.HOST ?? "127.0.0.1",
} = {}) {
const server = http.createServer((req, res) => {
void handleNode(req, res);
});
server.listen(port, host, () => {
console.log(`studio forge proxy: http://${host}:${port}/forge-proxy/`);
if (!process.env.FORGE_ALLOWED_HOSTS && process.env.FORGE_ALLOW_ANY !== "1") {
console.warn(
"studio forge proxy: FORGE_ALLOWED_HOSTS is empty — all targets are denied. " +
"Set it, or FORGE_ALLOW_ANY=1 for development.",
);
}
});
return server;
}
const invokedDirectly =
process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href;
if (invokedDirectly) startServer();