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
Open n8n Editor ->
`;
}
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}`),
);