This commit is contained in:
oliver committed 2026-11-04 13:32:37 -03:00
commit 5245c0a856
42 files changed
+11202

No files matched your search

+138
View File
@@ -0,0 +1,138 @@
// Optional reverse proxy + static host for the built PWA.
//
// HERMES_TARGET=https://hermes.example.com PORT=8787 node server/proxy.mjs
//
// Why use it? A browser PWA served from a different origin than Hermes needs
// the Hermes server to send CORS headers *and* allow credentialed cookies.
// Serving the app from this proxy puts the app and the API on the same origin,
// so the `hermes_session` cookie just works — and SSE streams pass through
// unbuffered. It uses only Node built-ins (no dependencies).
//
import { createServer } from "node:http";
import { request as httpsRequest } from "node:https";
import { request as httpRequest } from "node:http";
import { readFile, stat } from "node:fs/promises";
import { extname, join, normalize, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = resolve(fileURLToPath(import.meta.url), "..");
const DIST = resolve(__dirname, "../dist");
const PORT = Number(process.env.PORT || 8787);
const TARGET = process.env.HERMES_TARGET;
// Optional: inject the API key server-side so it never reaches the browser.
const API_KEY = process.env.HERMES_API_KEY;
if (!TARGET) {
console.error("Set HERMES_TARGET to your Hermes base URL, e.g.");
console.error(" HERMES_TARGET=https://hermes.example.com node server/proxy.mjs");
process.exit(1);
}
const target = new URL(TARGET);
const isHttps = target.protocol === "https:";
const doRequest = isHttps ? httpsRequest : httpRequest;
const MIME = {
".html": "text/html; charset=utf-8",
".js": "text/javascript; charset=utf-8",
".css": "text/css; charset=utf-8",
".json": "application/json; charset=utf-8",
".svg": "image/svg+xml",
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".webp": "image/webp",
".ico": "image/x-icon",
".webmanifest": "application/manifest+json",
".woff2": "font/woff2",
".map": "application/json",
};
function proxy(req, res) {
const headers = { ...req.headers };
headers.host = target.host;
if (API_KEY && !headers.authorization) {
headers.authorization = `Bearer ${API_KEY}`;
}
// Let the upstream decide; we forward the original origin for CSRF checks.
const options = {
protocol: target.protocol,
hostname: target.hostname,
port: target.port || (isHttps ? 443 : 80),
method: req.method,
path: req.url,
headers,
};
const upstream = doRequest(options, (upRes) => {
// Strip hop-by-hop headers.
const outHeaders = { ...upRes.headers };
delete outHeaders["transfer-encoding"];
delete outHeaders["connection"];
// Ensure SSE is not buffered by any intermediary.
if (String(outHeaders["content-type"] || "").includes("text/event-stream")) {
outHeaders["x-accel-buffering"] = "no";
outHeaders["cache-control"] = "no-cache";
}
res.writeHead(upRes.statusCode || 502, outHeaders);
upRes.pipe(res);
});
upstream.on("error", (err) => {
res.writeHead(502, { "content-type": "application/json" });
res.end(JSON.stringify({ error: `Upstream error: ${err.message}` }));
});
req.pipe(upstream);
}
async function serveStatic(req, res) {
let pathname = decodeURIComponent((req.url || "/").split("?")[0]);
if (pathname === "/") pathname = "/index.html";
const filePath = normalize(join(DIST, pathname));
if (!filePath.startsWith(DIST)) {
res.writeHead(403);
res.end("Forbidden");
return;
}
try {
const info = await stat(filePath);
if (info.isDirectory()) throw new Error("dir");
const data = await readFile(filePath);
const type = MIME[extname(filePath)] || "application/octet-stream";
const immutable = pathname.startsWith("/assets/");
res.writeHead(200, {
"content-type": type,
"cache-control": immutable ? "public, max-age=31536000, immutable" : "no-cache",
});
res.end(data);
} catch {
// SPA fallback.
try {
const html = await readFile(join(DIST, "index.html"));
res.writeHead(200, { "content-type": MIME[".html"], "cache-control": "no-cache" });
res.end(html);
} catch {
res.writeHead(404, { "content-type": "text/plain" });
res.end("Not found. Did you run `npm run build` first?");
}
}
}
const server = createServer((req, res) => {
const path = req.url || "";
if (path.startsWith("/api/") || path.startsWith("/v1/") || path.startsWith("/health")) {
proxy(req, res);
} else {
void serveStatic(req, res);
}
});
server.listen(PORT, () => {
console.log(`Hermes PWA proxy listening on http://localhost:${PORT}`);
console.log(` serving: ${DIST}`);
console.log(` proxying: /api/*, /v1/* -> ${target.origin}`);
if (API_KEY) console.log(" injecting: Authorization: Bearer <HERMES_API_KEY>");
});