const http = require("http"); const https = require("https"); 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 startTime = Date.now(); const UPTIMEROBOT_SETUP_ENABLED = String(process.env.UPTIMEROBOT_SETUP_ENABLED || "true").toLowerCase() === "true"; const UPTIMEROBOT_RATE_WINDOW_MS = 60 * 1000; const UPTIMEROBOT_RATE_MAX = Number( process.env.UPTIMEROBOT_RATE_LIMIT_PER_MINUTE || 5, ); const uptimerobotRateMap = new Map(); 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 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 getRequesterIp(req) { return ( req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.socket.remoteAddress || "unknown" ); } function isRateLimited(req) { const now = Date.now(); const ip = getRequesterIp(req); const bucket = uptimerobotRateMap.get(ip) || []; const recent = bucket.filter((ts) => now - ts < UPTIMEROBOT_RATE_WINDOW_MS); recent.push(now); uptimerobotRateMap.set(ip, recent); return recent.length > UPTIMEROBOT_RATE_MAX; } function isAllowedUptimeSetupOrigin(req) { const host = String(req.headers.host || "").toLowerCase(); const origin = String(req.headers.origin || "").toLowerCase(); const referer = String(req.headers.referer || "").toLowerCase(); if (!host) return false; if (origin && !origin.includes(host)) return false; if (referer && !referer.includes(host)) return false; return true; } function isValidUptimeApiKey(key) { return /^[A-Za-z0-9_-]{20,128}$/.test(String(key || "")); } function renderDashboard(data) { const { status } = data.sync; const getBadge = (status) => { let cls = "status-offline"; if ( status === "success" || status === "configured" || status === "restored" || status === "synced" ) cls = "status-online"; if (status === "syncing" || status === "restoring") cls = "status-syncing"; return `
${cls === "status-online" ? '
' : ""}${String(status).toUpperCase()}
`; }; const keepAwakeHtml = data.isPrivate ? `
This Space is private. External monitors cannot reliably access private HF health URLs, so keep-awake setup is only available on public Spaces.
` : `
One-time setup for public Spaces. Paste your UptimeRobot Main API key to create the monitor.
`; return ` Hugging8n Dashboard

🔗 Hugging8n

Workflow Automation Space

Uptime
${data.uptimeHuman}
n8n Port
${TARGET_PORT}
Sync Status
${getBadge(data.sync.status)}
Last Activity: ${data.sync.timestamp.split(".")[0]}Z
${data.sync.message}
Open n8n Editor
Keep Space Awake ${keepAwakeHtml}
`; } async function resolveSpaceIsPrivate(req) { const host = (req.headers.host || "").split(":")[0]; if (!host.endsWith(".hf.space")) return false; const params = new URLSearchParams(req.url.split("?")[1] || ""); const token = params.get("__sign"); if (!token) return false; try { const payload = JSON.parse( Buffer.from(token.split(".")[1], "base64").toString(), ); const sub = payload.sub || ""; const match_sub = sub.match(/^\/spaces\/([^/]+)\/([^/]+)$/); if (!match_sub) return false; return new Promise((resolve) => { https .get( `https://huggingface.co/api/spaces/${match_sub[1]}/${match_sub[2]}`, { headers: { "User-Agent": "Hugging8n" } }, (res) => { resolve( res.statusCode === 401 || res.statusCode === 403 || res.statusCode === 404, ); }, ) .on("error", () => resolve(false)); }); } catch { return false; } } function readRequestBody(req) { return new Promise((resolve, reject) => { let body = ""; req.on("data", (chunk) => { body += chunk; if (body.length > 1024 * 64) { reject(new Error("Request too large")); req.destroy(); } }); req.on("end", () => resolve(body)); req.on("error", reject); }); } function postUptimeRobot(path, form) { const body = new URLSearchParams(form).toString(); return new Promise((resolve, reject) => { const request = https.request( { hostname: "api.uptimerobot.com", port: 443, method: "POST", path, headers: { "Content-Type": "application/x-www-form-urlencoded", "Content-Length": Buffer.byteLength(body), }, }, (response) => { let raw = ""; response.setEncoding("utf8"); response.on("data", (chunk) => { raw += chunk; }); response.on("end", () => { try { resolve(JSON.parse(raw)); } catch { reject(new Error("Unexpected response from UptimeRobot")); } }); }, ); request.on("error", reject); request.write(body); request.end(); }); } async function createUptimeRobotMonitor(apiKey, host) { const cleanHost = String(host || "") .replace(/^https?:\/\//, "") .replace(/\/.*$/, ""); if (!cleanHost.endsWith(".hf.space")) { throw new Error("Uptime setup is only supported on .hf.space hosts."); } if (!cleanHost) throw new Error("Missing Space host."); const monitorUrl = `https://${cleanHost}/health`; const existing = await postUptimeRobot("/v2/getMonitors", { api_key: apiKey, format: "json", logs: "0", response_times: "0", response_times_limit: "1", }); const existingMonitor = Array.isArray(existing.monitors) ? existing.monitors.find((m) => m.url === monitorUrl) : null; if (existingMonitor) { return { created: false, message: `Monitor already exists for ${monitorUrl}`, }; } const created = await postUptimeRobot("/v2/newMonitor", { api_key: apiKey, format: "json", type: "1", friendly_name: `Hugging8n ${cleanHost}`, url: monitorUrl, interval: "300", }); if (created.stat !== "ok") { throw new Error(created?.error?.message || "Failed to create monitor."); } return { created: true, message: `Monitor created for ${monitorUrl}` }; } 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(), }), ); } if (pathname === "/status") { const uptime = Math.floor((Date.now() - startTime) / 1000); const n8nReady = await probeN8nHealth(); return res.end( JSON.stringify({ uptime: `${Math.floor(uptime / 3600)}h ${Math.floor((uptime % 3600) / 60)}m`, n8nReady, sync: getStatus(), }), ); } if (pathname === "/uptimerobot/setup" && req.method === "POST") { void (async () => { try { if (!UPTIMEROBOT_SETUP_ENABLED) { res.writeHead(403, { "Content-Type": "application/json" }); return res.end( JSON.stringify({ message: "Uptime setup is disabled." }), ); } if (isRateLimited(req)) { res.writeHead(429, { "Content-Type": "application/json" }); return res.end(JSON.stringify({ message: "Too many requests." })); } if (!isAllowedUptimeSetupOrigin(req)) { res.writeHead(403, { "Content-Type": "application/json" }); return res.end( JSON.stringify({ message: "Invalid request origin." }), ); } const body = await readRequestBody(req); const { apiKey } = JSON.parse(body || "{}"); if (!isValidUptimeApiKey(apiKey)) { res.writeHead(400, { "Content-Type": "application/json" }); return res.end( JSON.stringify({ message: "A valid API key is required." }), ); } const result = await createUptimeRobotMonitor(apiKey, req.headers.host); res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify(result)); } catch (e) { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ message: e.message || "Invalid request." })); } })(); return; } if (pathname === "/" || pathname === "/dashboard") { const uptime = Math.floor((Date.now() - startTime) / 1000); const isPrivate = await resolveSpaceIsPrivate(req); res.writeHead(200, { "Content-Type": "text/html" }); return res.end( renderDashboard({ uptimeHuman: `${Math.floor(uptime / 3600)}h ${Math.floor((uptime % 3600) / 60)}m`, sync: getStatus(), isPrivate, }), ); } // 2. n8n Proxy Logic // Any path that isn't a dashboard route gets proxied to n8n. 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()); }); // Disable overall timeout for SSE, but keep keep-alive healthy server.timeout = 0; server.keepAliveTimeout = 65000; server.listen(PORT, "0.0.0.0", () => console.log(`Namespace Proxy on ${PORT} -> n8n on ${TARGET_PORT}`), );