const http = require("http"); const fs = require("fs"); const net = require("net"); const PORT = Number(process.env.PUBLIC_PORT || 7861); const TARGET_PORT = Number(process.env.N8N_PORT || 5678); const TARGET_HOST = "127.0.0.1"; const SYNC_STATUS_FILE = "/tmp/hugging8n-sync-status.json"; const CLOUDFLARE_KEEPALIVE_STATUS_FILE = "/tmp/hugging8n-cloudflare-keepalive-status.json"; const startTime = Date.now(); function parseRequestUrl(url) { try { return new URL(url, "http://localhost"); } catch { return new URL("http://localhost/"); } } function getStatus() { try { if (fs.existsSync(SYNC_STATUS_FILE)) { return JSON.parse(fs.readFileSync(SYNC_STATUS_FILE, "utf8")); } } catch {} return { status: "unknown", message: "Initial startup...", timestamp: new Date().toISOString(), }; } function getKeepaliveStatus() { try { if (fs.existsSync(CLOUDFLARE_KEEPALIVE_STATUS_FILE)) { return JSON.parse( fs.readFileSync(CLOUDFLARE_KEEPALIVE_STATUS_FILE, "utf8"), ); } } catch {} return null; } function probeN8nHealth(timeoutMs = 1500) { return new Promise((resolve) => { const request = http.get( { hostname: TARGET_HOST, port: TARGET_PORT, path: "/healthz", timeout: timeoutMs, }, (response) => { response.resume(); resolve(response.statusCode >= 200 && response.statusCode < 400); }, ); request.on("timeout", () => { request.destroy(); resolve(false); }); request.on("error", () => resolve(false)); }); } function escapeHtml(value) { return String(value) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function toneBadge(label, tone = "neutral") { return `${escapeHtml(label)}`; } function renderTile({ title, value, detail = "", tone = "neutral", meta = "", }) { return `
${escapeHtml(title)}
${value}
${detail ? `
${detail}
` : ""} ${meta ? `
${meta}
` : ""}
`; } function renderDashboard(data) { const syncStatus = String(data.sync?.status || "unknown"); const syncTone = ["success", "restored", "synced", "configured"].includes( syncStatus, ) ? "ok" : syncStatus === "disabled" ? "warn" : "neutral"; const backupDetail = data.sync?.message ? escapeHtml(data.sync.message) : "No status yet"; const keepaliveConfigured = data.keepalive?.configured === true; const keepaliveStatus = String( data.keepalive?.status || (process.env.CLOUDFLARE_WORKERS_TOKEN ? "pending" : "not configured"), ); const keepAliveTone = keepaliveConfigured ? "ok" : process.env.CLOUDFLARE_WORKERS_TOKEN ? "warn" : "neutral"; const keepAliveDetail = keepaliveConfigured ? `Pinging ${escapeHtml(data.keepalive.targetUrl || "/health")}` : process.env.CLOUDFLARE_WORKERS_TOKEN ? "Worker pending or failed" : "Not configured"; const tiles = [ renderTile({ title: "n8n Core", value: toneBadge( data.n8nReady ? "Online" : "Offline", data.n8nReady ? "ok" : "off", ), detail: `Internal Port ${TARGET_PORT}`, tone: data.n8nReady ? "ok" : "off", }), renderTile({ title: "Runtime", value: escapeHtml(data.uptimeHuman), detail: `Public Port ${PORT}`, tone: "neutral", }), renderTile({ title: "Backup", value: toneBadge(syncStatus.toUpperCase(), syncTone), detail: backupDetail, tone: syncTone, meta: data.sync?.timestamp ? `` : "", }), renderTile({ title: "Keep Awake", value: toneBadge( keepaliveConfigured ? "CF Cron" : keepaliveStatus.toUpperCase(), keepAliveTone, ), detail: keepAliveDetail, tone: keepAliveTone, }), ].join(""); return ` Hugging8n

Hugging8n

Workflow Automation Space
Open n8n Editor ->
${tiles}
`; } const server = http.createServer(async (req, res) => { const url = parseRequestUrl(req.url); const pathname = url.pathname; // 1. Dashboard Routes if (pathname === "/health") { const n8nReady = await probeN8nHealth(); res.writeHead(n8nReady ? 200 : 503, { "Content-Type": "application/json" }); return res.end( JSON.stringify({ status: n8nReady ? "ok" : "degraded", n8nReady, ...getStatus(), keepalive: getKeepaliveStatus(), }), ); } if (pathname === "/status") { const uptime = Math.floor((Date.now() - startTime) / 1000); const n8nReady = await probeN8nHealth(); res.writeHead(200, { "Content-Type": "application/json" }); return res.end( JSON.stringify({ uptime: `${Math.floor(uptime / 3600)}h ${Math.floor((uptime % 3600) / 60)}m`, n8nReady, sync: getStatus(), keepalive: getKeepaliveStatus(), }), ); } if (pathname === "/" || pathname === "/dashboard") { const uptime = Math.floor((Date.now() - startTime) / 1000); const n8nReady = await probeN8nHealth(); res.writeHead(200, { "Content-Type": "text/html" }); return res.end( renderDashboard({ uptimeHuman: `${Math.floor(uptime / 3600)}h ${Math.floor((uptime % 3600) / 60)}m`, n8nReady, sync: getStatus(), keepalive: getKeepaliveStatus(), }), ); } // 2. n8n Proxy Logic const proxyHeaders = { ...req.headers, host: `127.0.0.1:${TARGET_PORT}`, "x-forwarded-for": req.socket.remoteAddress, "x-forwarded-host": req.headers.host, "x-forwarded-proto": "https", }; const proxyReq = http.request( { hostname: TARGET_HOST, port: TARGET_PORT, path: pathname + url.search, method: req.method, headers: proxyHeaders, }, (proxyRes) => { res.writeHead(proxyRes.statusCode, proxyRes.headers); proxyRes.pipe(res); proxyRes.on("error", (err) => { console.error("proxyRes error:", err); res.end(); }); }, ); req.on("error", (err) => { console.error("req error:", err); proxyReq.destroy(); }); res.on("error", (err) => { console.error("res error:", err); proxyReq.destroy(); }); proxyReq.on("error", (err) => { console.error("proxyReq error:", err); if (!res.headersSent) { res.writeHead(503, { "Content-Type": "application/json" }); res.end( JSON.stringify({ status: "starting", message: "n8n is initializing... or connection failed", }), ); } else { res.end(); } }); req.pipe(proxyReq); }); server.on("upgrade", (req, socket, head) => { const url = parseRequestUrl(req.url); const proxyPath = url.pathname; const proxySocket = net.connect(TARGET_PORT, TARGET_HOST, () => { proxySocket.write( `${req.method} ${proxyPath}${url.search} HTTP/${req.httpVersion}\r\n`, ); for (let i = 0; i < req.rawHeaders.length; i += 2) { proxySocket.write(`${req.rawHeaders[i]}: ${req.rawHeaders[i + 1]}\r\n`); } proxySocket.write("\r\n"); if (head && head.length) proxySocket.write(head); proxySocket.pipe(socket).pipe(proxySocket); }); proxySocket.on("error", () => socket.destroy()); }); server.timeout = 0; server.keepAliveTimeout = 65000; server.listen(PORT, "0.0.0.0", () => console.log(`Namespace Proxy on ${PORT} -> n8n on ${TARGET_PORT}`), );