SPB Git

spb/khaelor Public

KHAELOR — a terminal-native autonomous engineering agent powered by Anthropic.

TypeScript 82.9% HTML 14.9% CSS 1.1% JavaScript 0.7%
6.3 KB · 187 lines javascript
Raw Blame History
1/**2 * KHAELOR3 * File: website/server.mjs4 * Description: Zero-dependency static server for www.khaelor.sh — serves public/, the install tarball, and /health.5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 *9 * Run:   node server.mjs            (PORT env var, default 8790; binds 0.0.0.0)10 * Serves:11 *   /                → public/index.html12 *   /docs/...        → public/docs/...13 *   /install.sh      → public/install.sh          (no-cache)14 *   /khaelor.tgz     → artifacts/khaelor.tgz       (no-cache; 503 if absent)15 *   /health          → {"ok":true,"version":...}16 */1718import { createServer } from "node:http";19import { createReadStream, existsSync, statSync } from "node:fs";20import { extname, join, normalize, resolve, sep } from "node:path";21import { fileURLToPath } from "node:url";2223const VERSION = "1.0.0";24const PORT = Number.parseInt(process.env.PORT ?? "8790", 10);25const HOST = "0.0.0.0";2627const ROOT = fileURLToPath(new URL(".", import.meta.url));28const PUBLIC_DIR = resolve(ROOT, "public");29const TARBALL_PATH = resolve(ROOT, "artifacts", "khaelor.tgz");3031const MIME = {32  ".html": "text/html; charset=utf-8",33  ".css": "text/css; charset=utf-8",34  ".js": "text/javascript; charset=utf-8",35  ".mjs": "text/javascript; charset=utf-8",36  ".json": "application/json; charset=utf-8",37  ".sh": "text/plain; charset=utf-8",38  ".txt": "text/plain; charset=utf-8",39  ".md": "text/plain; charset=utf-8",40  ".svg": "image/svg+xml",41  ".png": "image/png",42  ".jpg": "image/jpeg",43  ".jpeg": "image/jpeg",44  ".gif": "image/gif",45  ".ico": "image/x-icon",46  ".webp": "image/webp",47  ".woff": "font/woff",48  ".woff2": "font/woff2",49  ".tgz": "application/gzip",50  ".gz": "application/gzip",51  ".xml": "application/xml; charset=utf-8",52};5354/** Assets get a day of caching; the installer and tarball must always be fresh. */55function cacheHeaderFor(urlPath) {56  if (urlPath === "/install.sh" || urlPath === "/khaelor.tgz" || urlPath === "/health") {57    return "no-cache, no-store, must-revalidate";58  }59  if (urlPath.endsWith(".html") || urlPath === "/" || extname(urlPath) === "") {60    return "no-cache"; // revalidate pages so doc updates show up immediately61  }62  return "public, max-age=86400"; // css/js/images63}6465function log(req, status, extra = "") {66  const now = new Date().toISOString();67  const ip =68    req.headers["x-forwarded-for"]?.split(",")[0]?.trim() ??69    req.socket.remoteAddress ??70    "-";71  console.log(`${now} ${ip} ${req.method} ${req.url} ${status}${extra ? " " + extra : ""}`);72}7374function sendText(req, res, status, body, headers = {}) {75  res.writeHead(status, {76    "Content-Type": "text/plain; charset=utf-8",77    "Content-Length": Buffer.byteLength(body),78    "Cache-Control": "no-cache, no-store, must-revalidate",79    ...headers,80  });81  res.end(req.method === "HEAD" ? undefined : body);82  log(req, status);83}8485function sendFile(req, res, filePath, urlPath) {86  let st;87  try {88    st = statSync(filePath);89  } catch {90    return sendText(req, res, 404, "404 — not found\n");91  }92  if (!st.isFile()) return sendText(req, res, 404, "404 — not found\n");9394  const type = MIME[extname(filePath).toLowerCase()] ?? "application/octet-stream";95  res.writeHead(200, {96    "Content-Type": type,97    "Content-Length": st.size,98    "Cache-Control": cacheHeaderFor(urlPath),99    "X-Content-Type-Options": "nosniff",100  });101  if (req.method === "HEAD") {102    res.end();103    log(req, 200);104    return;105  }106  const stream = createReadStream(filePath);107  stream.on("error", () => {108    if (!res.headersSent) sendText(req, res, 500, "500 — read error\n");109    else res.destroy();110  });111  stream.pipe(res);112  log(req, 200, `${st.size}b`);113}114115const server = createServer((req, res) => {116  if (req.method !== "GET" && req.method !== "HEAD") {117    return sendText(req, res, 405, "405 — method not allowed\n", { Allow: "GET, HEAD" });118  }119120  let urlPath;121  try {122    urlPath = decodeURIComponent(new URL(req.url, "http://localhost").pathname);123  } catch {124    return sendText(req, res, 400, "400 — bad request\n");125  }126127  // ── health ──────────────────────────────────────────────────────────128  if (urlPath === "/health") {129    const body = JSON.stringify({ ok: true, version: VERSION });130    res.writeHead(200, {131      "Content-Type": "application/json; charset=utf-8",132      "Content-Length": Buffer.byteLength(body),133      "Cache-Control": "no-cache, no-store, must-revalidate",134    });135    res.end(req.method === "HEAD" ? undefined : body);136    return log(req, 200);137  }138139  // ── install tarball (dropped into artifacts/ at deploy time) ───────140  if (urlPath === "/khaelor.tgz") {141    if (!existsSync(TARBALL_PATH)) {142      return sendText(143        req,144        res,145        503,146        "The khaelor package tarball is not available on this server yet.\n" +147          "Try again shortly, or contact contact@spboucher.ai.\n",148        { "Retry-After": "3600" },149      );150    }151    return sendFile(req, res, TARBALL_PATH, urlPath);152  }153154  // ── static files from public/ ───────────────────────────────────────155  if (urlPath === "/") urlPath = "/index.html";156  // directory-style URLs: /docs → /docs/index.html would 404; leave as-is.157158  const resolved = resolve(PUBLIC_DIR, "." + normalize("/" + urlPath));159  if (resolved !== PUBLIC_DIR && !resolved.startsWith(PUBLIC_DIR + sep)) {160    return sendText(req, res, 403, "403 — forbidden\n");161  }162163  let target = resolved;164  if (existsSync(target) && statSync(target).isDirectory()) {165    target = join(target, "index.html");166  }167  if (!existsSync(target) && extname(target) === "" && existsSync(target + ".html")) {168    target = target + ".html"; // /docs/usage → /docs/usage.html169  }170171  return sendFile(req, res, target, urlPath);172});173174server.listen(PORT, HOST, () => {175  console.log(176    `${new Date().toISOString()} khaelor-website v${VERSION} listening on http://${HOST}:${PORT} (public: ${PUBLIC_DIR})`,177  );178});179180for (const sig of ["SIGINT", "SIGTERM"]) {181  process.on(sig, () => {182    console.log(`${new Date().toISOString()} ${sig} received — shutting down`);183    server.close(() => process.exit(0));184    setTimeout(() => process.exit(0), 2000).unref();185  });186}187