/** * KHAELOR * File: website/server.mjs * Description: Zero-dependency static server for www.khaelor.sh — serves public/, the install tarball, and /health. * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai * * Run: node server.mjs (PORT env var, default 8790; binds 0.0.0.0) * Serves: * / → public/index.html * /docs/... → public/docs/... * /install.sh → public/install.sh (no-cache) * /khaelor.tgz → artifacts/khaelor.tgz (no-cache; 503 if absent) * /health → {"ok":true,"version":...} */ import { createServer } from "node:http"; import { createReadStream, existsSync, statSync } from "node:fs"; import { extname, join, normalize, resolve, sep } from "node:path"; import { fileURLToPath } from "node:url"; const VERSION = "1.0.0"; const PORT = Number.parseInt(process.env.PORT ?? "8790", 10); const HOST = "0.0.0.0"; const ROOT = fileURLToPath(new URL(".", import.meta.url)); const PUBLIC_DIR = resolve(ROOT, "public"); const TARBALL_PATH = resolve(ROOT, "artifacts", "khaelor.tgz"); const MIME = { ".html": "text/html; charset=utf-8", ".css": "text/css; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".mjs": "text/javascript; charset=utf-8", ".json": "application/json; charset=utf-8", ".sh": "text/plain; charset=utf-8", ".txt": "text/plain; charset=utf-8", ".md": "text/plain; charset=utf-8", ".svg": "image/svg+xml", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".ico": "image/x-icon", ".webp": "image/webp", ".woff": "font/woff", ".woff2": "font/woff2", ".tgz": "application/gzip", ".gz": "application/gzip", ".xml": "application/xml; charset=utf-8", }; /** Assets get a day of caching; the installer and tarball must always be fresh. */ function cacheHeaderFor(urlPath) { if (urlPath === "/install.sh" || urlPath === "/khaelor.tgz" || urlPath === "/health") { return "no-cache, no-store, must-revalidate"; } if (urlPath.endsWith(".html") || urlPath === "/" || extname(urlPath) === "") { return "no-cache"; // revalidate pages so doc updates show up immediately } return "public, max-age=86400"; // css/js/images } function log(req, status, extra = "") { const now = new Date().toISOString(); const ip = req.headers["x-forwarded-for"]?.split(",")[0]?.trim() ?? req.socket.remoteAddress ?? "-"; console.log(`${now} ${ip} ${req.method} ${req.url} ${status}${extra ? " " + extra : ""}`); } function sendText(req, res, status, body, headers = {}) { res.writeHead(status, { "Content-Type": "text/plain; charset=utf-8", "Content-Length": Buffer.byteLength(body), "Cache-Control": "no-cache, no-store, must-revalidate", ...headers, }); res.end(req.method === "HEAD" ? undefined : body); log(req, status); } function sendFile(req, res, filePath, urlPath) { let st; try { st = statSync(filePath); } catch { return sendText(req, res, 404, "404 — not found\n"); } if (!st.isFile()) return sendText(req, res, 404, "404 — not found\n"); const type = MIME[extname(filePath).toLowerCase()] ?? "application/octet-stream"; res.writeHead(200, { "Content-Type": type, "Content-Length": st.size, "Cache-Control": cacheHeaderFor(urlPath), "X-Content-Type-Options": "nosniff", }); if (req.method === "HEAD") { res.end(); log(req, 200); return; } const stream = createReadStream(filePath); stream.on("error", () => { if (!res.headersSent) sendText(req, res, 500, "500 — read error\n"); else res.destroy(); }); stream.pipe(res); log(req, 200, `${st.size}b`); } const server = createServer((req, res) => { if (req.method !== "GET" && req.method !== "HEAD") { return sendText(req, res, 405, "405 — method not allowed\n", { Allow: "GET, HEAD" }); } let urlPath; try { urlPath = decodeURIComponent(new URL(req.url, "http://localhost").pathname); } catch { return sendText(req, res, 400, "400 — bad request\n"); } // ── health ────────────────────────────────────────────────────────── if (urlPath === "/health") { const body = JSON.stringify({ ok: true, version: VERSION }); res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Content-Length": Buffer.byteLength(body), "Cache-Control": "no-cache, no-store, must-revalidate", }); res.end(req.method === "HEAD" ? undefined : body); return log(req, 200); } // ── install tarball (dropped into artifacts/ at deploy time) ─────── if (urlPath === "/khaelor.tgz") { if (!existsSync(TARBALL_PATH)) { return sendText( req, res, 503, "The khaelor package tarball is not available on this server yet.\n" + "Try again shortly, or contact contact@spboucher.ai.\n", { "Retry-After": "3600" }, ); } return sendFile(req, res, TARBALL_PATH, urlPath); } // ── static files from public/ ─────────────────────────────────────── if (urlPath === "/") urlPath = "/index.html"; // directory-style URLs: /docs → /docs/index.html would 404; leave as-is. const resolved = resolve(PUBLIC_DIR, "." + normalize("/" + urlPath)); if (resolved !== PUBLIC_DIR && !resolved.startsWith(PUBLIC_DIR + sep)) { return sendText(req, res, 403, "403 — forbidden\n"); } let target = resolved; if (existsSync(target) && statSync(target).isDirectory()) { target = join(target, "index.html"); } if (!existsSync(target) && extname(target) === "" && existsSync(target + ".html")) { target = target + ".html"; // /docs/usage → /docs/usage.html } return sendFile(req, res, target, urlPath); }); server.listen(PORT, HOST, () => { console.log( `${new Date().toISOString()} khaelor-website v${VERSION} listening on http://${HOST}:${PORT} (public: ${PUBLIC_DIR})`, ); }); for (const sig of ["SIGINT", "SIGTERM"]) { process.on(sig, () => { console.log(`${new Date().toISOString()} ${sig} received — shutting down`); server.close(() => process.exit(0)); setTimeout(() => process.exit(0), 2000).unref(); }); }