#!/usr/bin/env node /** * Optional DNS fallback for Cursor browser → internal stage UI. * * Canonical access is direct HTTPS (valid public DNS + TLS): * https://stage.calentiq.com/ (client) * https://admin.stage.calentiq.com/ * * Use this proxy ONLY when Cursor/Chromium cannot resolve the stage hostname * (e.g. *.local mDNS quirks) — not because of certificates. * * Serves http://ui.stage.eventhub.test:8787 → https://ui.stage.eventhub.local * with Host/SNI rewrite for Traefik. * * Usage: * node stage-browser-alias-proxy.mjs * open http://ui.stage.eventhub.test:8787/login * * Requires hosts: 127.0.0.1 ui.stage.eventhub.test * * Docs: EventHubSpec/STAGE-BROWSER.md */ import http from 'node:http'; import https from 'node:https'; const LISTEN_HOST = process.env.ALIAS_LISTEN_HOST || '127.0.0.1'; const LISTEN_PORT = Number(process.env.ALIAS_LISTEN_PORT || 8787); const UPSTREAM_IP = process.env.STAGE_IP || '195.208.119.190'; const UPSTREAM_HOST = process.env.STAGE_UI_HOST || 'ui.stage.eventhub.local'; const ALIAS_HOST = process.env.ALIAS_HOST || 'ui.stage.eventhub.test'; const agent = new https.Agent({ rejectUnauthorized: false, servername: UPSTREAM_HOST, keepAlive: true, }); function rewriteLocation(value) { if (!value) return value; return value .replaceAll(`https://${UPSTREAM_HOST}`, `http://${ALIAS_HOST}:${LISTEN_PORT}`) .replaceAll(`http://${UPSTREAM_HOST}`, `http://${ALIAS_HOST}:${LISTEN_PORT}`); } function scrubHeaders(headers) { const out = { ...headers, host: UPSTREAM_HOST }; delete out['content-length']; // Avoid compression surprises while piping delete out['accept-encoding']; return out; } function proxyHttp(req, res) { const opts = { hostname: UPSTREAM_IP, port: 443, path: req.url, method: req.method, headers: scrubHeaders(req.headers), agent, }; const up = https.request(opts, (upRes) => { const headers = { ...upRes.headers }; if (headers.location) headers.location = rewriteLocation(headers.location); res.writeHead(upRes.statusCode || 502, headers); upRes.pipe(res); }); up.on('error', (err) => { if (!res.headersSent) res.writeHead(502, { 'content-type': 'text/plain' }); res.end(`alias-proxy upstream error: ${err.message}`); }); req.pipe(up); } function proxyUpgrade(req, socket, head) { const opts = { hostname: UPSTREAM_IP, port: 443, path: req.url, method: 'GET', headers: scrubHeaders(req.headers), agent, }; const up = https.request(opts); up.on('upgrade', (upRes, upSocket, upHead) => { const lines = [`HTTP/1.1 ${upRes.statusCode} Switching Protocols`]; for (const [k, v] of Object.entries(upRes.headers)) { if (Array.isArray(v)) v.forEach((x) => lines.push(`${k}: ${x}`)); else lines.push(`${k}: ${v}`); } lines.push('', ''); socket.write(lines.join('\r\n')); if (upHead?.length) socket.write(upHead); upSocket.pipe(socket); socket.pipe(upSocket); }); up.on('error', () => socket.destroy()); up.end(head); } const server = http.createServer(proxyHttp); server.on('upgrade', proxyUpgrade); server.listen(LISTEN_PORT, LISTEN_HOST, () => { console.log( `stage browser alias: http://${ALIAS_HOST}:${LISTEN_PORT} → https://${UPSTREAM_HOST} (${UPSTREAM_IP})` ); });