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.
Do not use the Read-only API key or a Monitor-specific API key.
One-time setup. Your key is only used to create the monitor for this Space.
`;
return `
Hugging8n Dashboard
🔗 Hugging8n
Workflow Automation Space
Uptime
${data.uptimeHuman}
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}`),
);