// Single public entrypoint for HF Spaces: dashboard + reverse proxy to OpenClaw + JupyterLab.
const http = require("http");
const https = require("https");
const fs = require("fs");
const net = require("net");
function isTrue(value) {
return /^(true|1|yes|on)$/i.test(String(value || "").trim());
}
function normalizeBase(value, fallback) {
const raw = String(value || fallback || "").trim() || fallback;
if (!raw) return fallback;
const base = raw.startsWith("/") ? raw : `/${raw}`;
return base.replace(/\/+$/, "") || fallback;
}
const PORT = Number.parseInt(process.env.PORT || "7861", 10);
const GATEWAY_PORT = Number.parseInt(process.env.GATEWAY_PORT || "7860", 10);
const GATEWAY_HOST = "127.0.0.1";
const JUPYTER_PORT = Number.parseInt(process.env.JUPYTER_PORT || "8888", 10);
const JUPYTER_HOST = "127.0.0.1";
const JUPYTER_BASE = normalizeBase(process.env.JUPYTER_BASE, "/terminal");
const GATEWAY_TOKEN = (process.env.GATEWAY_TOKEN || "").trim();
const DEV_MODE_ENABLED = isTrue(process.env.DEV_MODE);
// Default true. Only false when DEV_MODE=false or HUGGINGCLAW_JUPYTER_ENABLED=false is explicitly set.
const JUPYTER_ENABLED =
!/^(false|0|no|off)$/i.test(String(process.env.DEV_MODE || "").trim()) &&
!/^(false|0|no|off)$/i.test(String(process.env.HUGGINGCLAW_JUPYTER_ENABLED || "").trim());
const startTime = Date.now();
const LLM_MODEL = process.env.LLM_MODEL || "Not Set";
const TELEGRAM_ENABLED = !!process.env.TELEGRAM_BOT_TOKEN;
const WHATSAPP_ENABLED = isTrue(process.env.WHATSAPP_ENABLED);
const WHATSAPP_STATUS_FILE = "/tmp/huggingclaw-wa-status.json";
const HF_BACKUP_ENABLED = !!process.env.HF_TOKEN;
const SYNC_INTERVAL = (process.env.SYNC_INTERVAL || "180").trim() || "180";
const BACKUP_DATASET_NAME = (process.env.BACKUP_DATASET_NAME || process.env.BACKUP_DATASET || "huggingclaw-backup").trim() || "huggingclaw-backup";
const DEVDATA_DATASET_NAME = (process.env.DEVDATA_DATASET_NAME || "huggingclaw-devdata").trim() || "huggingclaw-devdata";
const DEVDATA_SYNC_INTERVAL = (process.env.DEVDATA_SYNC_INTERVAL || "180").trim() || "180";
const DEVDATA_SEPARATE_DATASET = DEVDATA_DATASET_NAME !== BACKUP_DATASET_NAME;
const DEVDATA_ENABLED = JUPYTER_ENABLED && HF_BACKUP_ENABLED && DEVDATA_SEPARATE_DATASET && !/^(off|false|0|no)$/i.test((process.env.DEVDATA || "on").trim());
const APP_BASE = normalizeBase(process.env.APP_BASE, "/app");
const SYNC_STATUS_FILE = "/tmp/sync-status.json";
// ── Private Space redirect support ──
// HF automatically sets SPACE_ID as "username/spacename" in every Space container.
const SPACE_ID = (process.env.SPACE_ID || "").trim();
function deriveHfSpaceUrl() {
if (SPACE_ID) return `https://huggingface.co/spaces/${SPACE_ID}`;
const host = (process.env.SPACE_HOST || "").replace(/\.hf\.space$/i, "");
const author = (process.env.SPACE_AUTHOR_NAME || "").trim().toLowerCase();
if (author && host.toLowerCase().startsWith(author + "-")) {
const spaceName = host.slice(author.length + 1);
return `https://huggingface.co/spaces/${process.env.SPACE_AUTHOR_NAME}/${spaceName}`;
}
return "";
}
const HF_SPACE_URL = deriveHfSpaceUrl();
// Auto-detect space privacy via HF API at startup.
// Caches result so every request doesn't hit the API.
let SPACE_IS_PRIVATE = false;
async function detectSpacePrivacy() {
if (!SPACE_ID) return;
try {
const token = (process.env.HF_TOKEN || "").trim();
const reqOptions = {
hostname: "huggingface.co",
path: `/api/spaces/${SPACE_ID}`,
method: "GET",
headers: Object.assign(
{ "User-Agent": "HuggingClaw/health-server" },
token ? { Authorization: `Bearer ${token}` } : {}
),
};
await new Promise((resolve) => {
const r = https.request(reqOptions, (res) => {
let body = "";
res.on("data", (chunk) => { body += chunk; });
res.on("end", () => {
try {
if (res.statusCode === 200) {
const data = JSON.parse(body);
SPACE_IS_PRIVATE = data.private === true;
} else if (res.statusCode === 404 && !token) {
// 404 with no token usually means private space
SPACE_IS_PRIVATE = true;
}
} catch {}
resolve();
});
});
r.on("error", resolve);
r.setTimeout(5000, () => { r.destroy(); resolve(); });
r.end();
});
console.log(`[health-server] Space privacy detected: ${SPACE_IS_PRIVATE ? "private" : "public"}`);
} catch {
// Network error — default to false (safe)
}
}
detectSpacePrivacy();
const CLOUDFLARE_KEEPALIVE_STATUS_FILE =
"/tmp/huggingclaw-cloudflare-keepalive-status.json";
function parseRequestUrl(url) {
try { return new URL(url, "http://localhost"); }
catch { return new URL("http://localhost/"); }
}
function getSyncStatus() {
try {
if (fs.existsSync(SYNC_STATUS_FILE))
return JSON.parse(fs.readFileSync(SYNC_STATUS_FILE, "utf8"));
} catch {}
if (HF_BACKUP_ENABLED)
return { status: "configured", message: `Backup enabled. Waiting for sync window (${SYNC_INTERVAL}s).` };
return { status: "unknown", message: "No sync data yet" };
}
function readGuardianStatus() {
if (!WHATSAPP_ENABLED) return { configured: false, connected: false, pairing: false };
try {
if (fs.existsSync(WHATSAPP_STATUS_FILE)) {
const p = JSON.parse(fs.readFileSync(WHATSAPP_STATUS_FILE, "utf8"));
return { configured: p.configured !== false, connected: p.connected === true, pairing: p.pairing === true };
}
} catch {}
return { configured: true, connected: false, pairing: false };
}
function getKeepaliveStatus() {
try {
if (fs.existsSync(CLOUDFLARE_KEEPALIVE_STATUS_FILE))
return JSON.parse(fs.readFileSync(CLOUDFLARE_KEEPALIVE_STATUS_FILE, "utf8"));
} catch {}
return null;
}
function probePort(host, port, path, timeoutMs = 1500) {
return new Promise((resolve) => {
const req = http.get({ hostname: host, port, path, timeout: timeoutMs }, (res) => {
res.resume();
resolve(res.statusCode >= 200 && res.statusCode < 400);
});
req.on("timeout", () => { req.destroy(); resolve(false); });
req.on("error", () => resolve(false));
});
}
function formatUptime(ms) {
const t = Math.floor(ms / 1000);
const d = Math.floor(t / 86400), h = Math.floor((t % 86400) / 3600), m = Math.floor((t % 3600) / 60);
if (d) return `${d}d ${h}h ${m}m`;
if (h) return `${h}h ${m}m`;
return `${m}m`;
}
function escapeHtml(v) {
return String(v).replace(/&/g,"&").replace(//g,">").replace(/"/g,""");
}
function badge(label, tone = "neutral") {
return `${escapeHtml(label)}`;
}
function tile({ title, value, detail = "", tone = "neutral", meta = "" }) {
return `${escapeHtml(LLM_MODEL)}`, detail: "Primary LLM configured", tone: "neutral" }),
tile({ title: "Runtime", value: escapeHtml(data.uptimeHuman), detail: `Public port ${PORT}`, tone: "neutral" }),
tile({ title: "Telegram", value: badge(TELEGRAM_ENABLED ? "Enabled" : "Disabled", TELEGRAM_ENABLED ? "ok" : "neutral"), detail: TELEGRAM_ENABLED ? "Bot channel active" : "Not configured", tone: TELEGRAM_ENABLED ? "ok" : "neutral" }),
];
tiles.push(
tile({ title: "Backup", value: badge(syncStatus.toUpperCase(), syncTone), detail: escapeHtml(data.sync?.message || "No status yet"), tone: syncTone, meta: data.sync?.timestamp ? `` : "" }),
tile({ title: "Keep Awake", value: badge(kaConf ? "CF Cron" : kaStatus.toUpperCase(), kaTone), detail: kaConf ? `Pinging ${escapeHtml(data.keepalive?.targetUrl || "/health")}` : process.env.CLOUDFLARE_WORKERS_TOKEN ? "Worker pending or failed" : "Not configured", tone: kaTone }),
);
if (JUPYTER_ENABLED) {
tiles.push(tile({ title: "Terminal", value: badge(data.jupyterReady ? "Online" : "Starting…", data.jupyterReady ? "ok" : "warn"), detail: `JupyterLab at ${JUPYTER_BASE}/`, tone: data.jupyterReady ? "ok" : "warn" }));
tiles.push(tile({
title: "DevData",
value: badge(DEVDATA_ENABLED ? "Enabled" : "Disabled", DEVDATA_ENABLED ? "ok" : "neutral"),
detail: DEVDATA_ENABLED ? `Separate dataset ${escapeHtml(DEVDATA_DATASET_NAME)}` : DEVDATA_SEPARATE_DATASET ? "Separate Jupyter dataset backup inactive" : "DevData dataset must be separate from main backup dataset",
tone: DEVDATA_ENABLED ? "ok" : "neutral",
meta: `Sync interval ${escapeHtml(DEVDATA_SYNC_INTERVAL)}s`,
}));
}
const tilesHtml = tiles.join("");
return `
This HuggingFace Space is private. You need to be logged in to huggingface.co to access it.
Redirecting you now…
${escapeHtml(exc.message)}`;
}
}
// ── Generic proxy ──
function proxiedPath(url, { stripPrefix = "" } = {}) {
if (!stripPrefix) return url.pathname + url.search;
if (url.pathname === stripPrefix) return "/" + url.search;
if (url.pathname.startsWith(stripPrefix + "/")) {
return url.pathname.slice(stripPrefix.length) + url.search;
}
return url.pathname + url.search;
}
function rewriteProxyHeaders(headers, { publicPrefix = "", targetHost = "", targetPort = "" } = {}) {
const next = { ...headers };
// Keep browser redirects inside the public HF Space path. Backends may emit
// root-relative redirects ("/login") or absolute redirects pointing at their
// internal listener ("http://127.0.0.1:8888/..."). Both break from a browser
// if we do not normalize them back to the public mount path.
if (publicPrefix && typeof next.location === "string") {
try {
const internalOrigins = new Set([
"http://huggingclaw.local",
`http://${targetHost}:${targetPort}`,
`http://localhost:${targetPort}`,
`http://127.0.0.1:${targetPort}`,
]);
const location = new URL(next.location, "http://huggingclaw.local");
if (internalOrigins.has(location.origin)) {
let path = location.pathname;
if (path !== publicPrefix && !path.startsWith(publicPrefix + "/")) {
path = publicPrefix + (path.startsWith("/") ? path : `/${path}`);
}
next.location = path + location.search + location.hash;
}
} catch {}
}
return next;
}
function sendServiceUnavailable(res) {
if (!res.headersSent) {
res.writeHead(503, { "Content-Type": "application/json" });
res.end(JSON.stringify({ status: "starting", message: "Service is initializing… please wait." }));
} else {
res.end();
}
}
function proxyHTTP(req, res, targetHost, targetPort, options = {}) {
const url = parseRequestUrl(req.url);
const headers = {
...req.headers,
host: `${targetHost}:${targetPort}`,
"x-forwarded-for": req.socket.remoteAddress,
"x-forwarded-host": req.headers.host,
"x-forwarded-proto": "https",
"x-forwarded-prefix": options.publicPrefix || "",
};
const canReplayRequest = req.method === "GET" || req.method === "HEAD";
const proxyOnce = (path, retryOn404) => {
const pr = http.request({ hostname: targetHost, port: targetPort, path, method: req.method, headers }, (pres) => {
if (canReplayRequest && retryOn404 && pres.statusCode === 404 && options.stripPrefix) {
pres.resume();
return proxyOnce(proxiedPath(url, { stripPrefix: options.stripPrefix }), false);
}
res.writeHead(pres.statusCode, rewriteProxyHeaders(pres.headers, { ...options, targetHost, targetPort }));
pres.pipe(res);
pres.on("error", () => res.end());
});
req.on("error", () => pr.destroy());
res.on("error", () => pr.destroy());
pr.on("error", () => sendServiceUnavailable(res));
req.pipe(pr);
};
// First try the public path as-is because OpenClaw and JupyterLab are both
// configured with base paths. If a backend still returns 404, retry with the
// mount prefix stripped; that covers images built before the base-path config
// took effect and avoids the common HF Spaces "404 at /app or /terminal" trap.
proxyOnce(url.pathname + url.search, !!options.retryWithoutPrefixOn404);
}
// ── HTTP server ──
const server = http.createServer(async (req, res) => {
const { pathname } = parseRequestUrl(req.url);
if (pathname === "/health") {
const gatewayReady = await probePort(GATEWAY_HOST, GATEWAY_PORT, "/health");
res.writeHead(gatewayReady ? 200 : 503, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ status: gatewayReady ? "ok" : "degraded", gatewayReady, uptime: formatUptime(Date.now() - startTime), sync: getSyncStatus(), keepalive: getKeepaliveStatus() }));
}
if (pathname === "/status") {
const [gatewayReady, jupyterReady] = await Promise.all([
probePort(GATEWAY_HOST, GATEWAY_PORT, "/health"),
JUPYTER_ENABLED ? probePort(JUPYTER_HOST, JUPYTER_PORT, `${JUPYTER_BASE}/login`) : Promise.resolve(false),
]);
res.writeHead(200, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ model: LLM_MODEL, uptime: formatUptime(Date.now() - startTime), gatewayReady, jupyterReady, sync: getSyncStatus(), whatsapp: readGuardianStatus(), keepalive: getKeepaliveStatus() }));
}
// Private space redirect — send users to the authenticated HF Spaces page.
// Works for both direct .hf.space links AND programmatic shares.
if (pathname === "/hf-redirect" || pathname === "/hf-redirect/") {
if (HF_SPACE_URL) {
res.writeHead(302, { Location: HF_SPACE_URL, "Cache-Control": "no-store" });
return res.end();
}
res.writeHead(404, { "Content-Type": "text/plain" });
return res.end("SPACE_ID not configured.");
}
// ── Private Space Guard (server-side) ──
// Triggers automatically when SPACE_IS_PRIVATE=true (detected via HF API at startup).
// Only intercepts browser navigation (Accept: text/html) — API calls, assets,
// and WebSocket upgrades pass through untouched.
// /health and /status are always exempt so uptime monitors keep working.
const isHtmlRequest = (req.headers.accept || "").includes("text/html");
const isDirectHfSpaceRequest = SPACE_IS_PRIVATE &&
HF_SPACE_URL &&
isHtmlRequest &&
typeof req.headers.host === "string" &&
req.headers.host.endsWith(".hf.space");
if (pathname === "/env-builder" || pathname === "/env-builder/") {
if (isDirectHfSpaceRequest) {
res.writeHead(200, { "Content-Type": "text/html" });
return res.end(renderPrivateRedirect(HF_SPACE_URL));
}
res.writeHead(200, { "Content-Type": "text/html" });
return res.end(renderEnvBuilder());
}
if (pathname === "/env-builder.js") {
try {
const js = fs.readFileSync(require("path").join(__dirname, "env-builder.js"), "utf8");
res.writeHead(200, { "Content-Type": "application/javascript" });
return res.end(js);
} catch (exc) {
res.writeHead(404, { "Content-Type": "text/plain" });
return res.end(`env-builder.js not found: ${exc.message}`);
}
}
if (pathname === "/" || pathname === "/dashboard") {
if (isDirectHfSpaceRequest) {
res.writeHead(200, { "Content-Type": "text/html" });
return res.end(renderPrivateRedirect(HF_SPACE_URL));
}
const [gatewayReady, jupyterReady] = await Promise.all([
probePort(GATEWAY_HOST, GATEWAY_PORT, "/health"),
JUPYTER_ENABLED ? probePort(JUPYTER_HOST, JUPYTER_PORT, `${JUPYTER_BASE}/login`) : Promise.resolve(false),
]);
res.writeHead(200, { "Content-Type": "text/html" });
return res.end(renderDashboard({ uptimeHuman: formatUptime(Date.now() - startTime), gatewayReady, jupyterReady, sync: getSyncStatus(), whatsapp: readGuardianStatus(), keepalive: getKeepaliveStatus() }));
}
// JupyterLab terminal
if (pathname === JUPYTER_BASE || pathname.startsWith(JUPYTER_BASE + "/")) {
if (!JUPYTER_ENABLED) {
res.writeHead(404, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ status: "disabled", message: "JupyterLab terminal is disabled. Remove DEV_MODE=false to re-enable." }));
}
if (isDirectHfSpaceRequest) {
res.writeHead(200, { "Content-Type": "text/html" });
return res.end(renderPrivateRedirect(HF_SPACE_URL));
}
return proxyHTTP(req, res, JUPYTER_HOST, JUPYTER_PORT, {
publicPrefix: JUPYTER_BASE,
// Jupyter is started with --ServerApp.base_url=/terminal/, so keep the
// /terminal prefix when proxying. Stripping it breaks static/theme URLs.
stripPrefix: "",
retryWithoutPrefixOn404: false,
});
}
// OpenClaw Control UI mounted under /app. Retry without the mount prefix on
// 404 so deployments keep working across OpenClaw basePath behavior changes.
if (pathname === APP_BASE || pathname.startsWith(APP_BASE + "/")) {
if (isDirectHfSpaceRequest) {
res.writeHead(200, { "Content-Type": "text/html" });
return res.end(renderPrivateRedirect(HF_SPACE_URL));
}
return proxyHTTP(req, res, GATEWAY_HOST, GATEWAY_PORT, {
publicPrefix: APP_BASE,
stripPrefix: APP_BASE,
retryWithoutPrefixOn404: true,
});
}
// Favicon — serve a minimal inline SVG so browsers don't proxy to the gateway
if (pathname === "/favicon.ico" || pathname === "/favicon.svg") {
const svg = '';
res.writeHead(200, { "Content-Type": "image/svg+xml", "Cache-Control": "public, max-age=86400" });
return res.end(svg);
}
// OpenClaw gateway API/static fallback (everything else)
if (isDirectHfSpaceRequest) {
res.writeHead(200, { "Content-Type": "text/html" });
return res.end(renderPrivateRedirect(HF_SPACE_URL));
}
proxyHTTP(req, res, GATEWAY_HOST, GATEWAY_PORT);
});
// ── WebSocket upgrade (JupyterLab kernels + terminals need this) ──
server.on("upgrade", (req, socket, head) => {
const { pathname, search } = parseRequestUrl(req.url);
const isJupyter = JUPYTER_ENABLED && (pathname === JUPYTER_BASE || pathname.startsWith(JUPYTER_BASE + "/"));
const isApp = pathname === APP_BASE || pathname.startsWith(APP_BASE + "/");
const [targetHost, targetPort] = isJupyter ? [JUPYTER_HOST, JUPYTER_PORT] : [GATEWAY_HOST, GATEWAY_PORT];
const publicPrefix = isJupyter ? JUPYTER_BASE : isApp ? APP_BASE : "";
const targetPath = pathname + search;
const ps = net.connect(targetPort, targetHost, () => {
ps.write(`${req.method} ${targetPath} HTTP/${req.httpVersion}\r\n`);
ps.write(`Host: ${targetHost}:${targetPort}\r\n`);
ps.write(`X-Forwarded-For: ${req.socket.remoteAddress || ""}\r\n`);
ps.write(`X-Forwarded-Host: ${req.headers.host || ""}\r\n`);
ps.write("X-Forwarded-Proto: https\r\n");
if (publicPrefix) ps.write(`X-Forwarded-Prefix: ${publicPrefix}\r\n`);
for (let i = 0; i < req.rawHeaders.length; i += 2) {
const header = req.rawHeaders[i];
const lower = header.toLowerCase();
if (["host", "x-forwarded-for", "x-forwarded-host", "x-forwarded-proto", "x-forwarded-prefix"].includes(lower)) continue;
ps.write(`${header}: ${req.rawHeaders[i + 1]}\r\n`);
}
ps.write("\r\n");
if (head && head.length) ps.write(head);
ps.pipe(socket).pipe(ps);
});
ps.on("error", () => socket.destroy());
ps.on("close", () => socket.destroy());
socket.on("error", () => ps.destroy());
socket.on("close", () => ps.destroy());
});
server.timeout = 0;
server.keepAliveTimeout = 65000;
server.on("error", (err) => console.error(`[health-server] Server error:`, err));
server.listen(PORT, "0.0.0.0", () =>
console.log(`🦞 HuggingClaw :${PORT} → Gateway :${GATEWAY_PORT}${JUPYTER_ENABLED ? ` | Terminal :${JUPYTER_PORT} at ${JUPYTER_BASE}/` : " | Terminal disabled"}`),
);