import { createServer, request as httpRequest } from "node:http"; import { DEV_ENDPOINT, ENVIRONMENT_DEV } from "../protocol/index.mjs"; const DEFAULT_PROXY_TIMEOUT_MS = 150000; export function parsePort(value, fallback) { const parsed = Number.parseInt(value ?? "", 10); if (Number.isInteger(parsed) && parsed > 0 && parsed < 65536) { return parsed; } return fallback; } export function resolveHostPort({ listenEnv, hostEnv, portEnv, fallbackPort }) { const listen = process.env[listenEnv] ?? ""; const [listenHost, listenPort] = listen.includes(":") ? listen.split(":") : ["", listen]; const host = process.env[hostEnv] || listenHost || "0.0.0.0"; const port = parsePort(process.env[portEnv] || listenPort, fallbackPort); return { host, port }; } export function healthPayload({ serviceId, role, details = {} }) { const commitId = process.env.HWLAB_COMMIT_ID || process.env.HWLAB_GIT_SHA || "unknown"; const imageReference = process.env.HWLAB_IMAGE || `ghcr.io/pikastech/${serviceId}:${commitId.slice(0, 7) || "unknown"}`; const imageTag = process.env.HWLAB_IMAGE_TAG || commitId.slice(0, 7) || "unknown"; return { serviceId, environment: ENVIRONMENT_DEV, status: "ok", service: { id: serviceId, role, healthPath: "/health", livePath: "/health/live" }, commit: { id: commitId, source: process.env.HWLAB_BUILD_SOURCE || "runtime-env" }, image: { reference: imageReference, tag: imageTag, digest: process.env.HWLAB_IMAGE_DIGEST || "unknown" }, endpoint: process.env.HWLAB_PUBLIC_ENDPOINT || DEV_ENDPOINT, observedAt: new Date().toISOString(), details }; } export function sendJson(response, statusCode, body) { const payload = JSON.stringify(body, null, 2); response.writeHead(statusCode, { "content-type": "application/json; charset=utf-8", "content-length": Buffer.byteLength(payload) }); response.end(payload); } export function createServiceServer({ serviceId, role, routes = new Map(), details = () => ({}) }) { return createServer(async (request, response) => { try { const url = new URL(request.url || "/", `http://${serviceId}.local`); if (request.method === "GET" && (url.pathname === "/health" || url.pathname === "/health/live")) { sendJson(response, 200, healthPayload({ serviceId, role, details: details() })); return; } if (request.method === "GET" && url.pathname === "/help") { sendJson(response, 200, { serviceId, commands: ["GET /health", "GET /health/live", ...routes.keys()] }); return; } const handler = routes.get(`${request.method ?? "GET"} ${url.pathname}`); if (!handler) { sendJson(response, 404, { error: "not_found", serviceId, path: url.pathname }); return; } sendJson(response, 200, await handler({ request, url })); } catch (error) { sendJson(response, 500, { error: "internal_error", serviceId, message: error instanceof Error ? error.message : String(error) }); } }); } export function listen(server, { serviceId, host, port }) { server.listen(port, host, () => { process.stdout.write(`${JSON.stringify({ serviceId, status: "listening", host, port })}\n`); }); for (const signal of ["SIGINT", "SIGTERM"]) { process.on(signal, () => { server.close(() => { process.exit(0); }); }); } } export function proxyHttpRequest({ request, response, upstream, timeoutMs = DEFAULT_PROXY_TIMEOUT_MS }) { const target = new URL(request.url || "/", upstream); const timeout = setTimeout(() => { proxy.destroy(new Error(`upstream timed out after ${timeoutMs}ms`)); }, timeoutMs); const proxy = httpRequest( target, { method: request.method, headers: { ...request.headers, host: target.host } }, (upstreamResponse) => { response.writeHead(upstreamResponse.statusCode ?? 502, upstreamResponse.headers); upstreamResponse.on("end", () => clearTimeout(timeout)); upstreamResponse.on("close", () => clearTimeout(timeout)); upstreamResponse.pipe(response); } ); let settled = false; proxy.on("error", (error) => { clearTimeout(timeout); if (settled || response.headersSent) { response.destroy(error); return; } settled = true; sendJson(response, 502, { error: "upstream_unavailable", upstream, message: error.message }); }); request.pipe(proxy); }