import { createCloudApiServer } from "./server.ts"; import { createHwpodNodeWsRegistry } from "./hwpod-node-ws-registry.ts"; export async function createCloudApiBunServer(options: any = {}) { const env = options.env ?? process.env; const hwpodNodeWsRegistry = options.hwpodNodeWsRegistry || createHwpodNodeWsRegistry({ ...options, env }); const innerServer = createCloudApiServer({ ...options, env, hwpodNodeWsRegistry }); await new Promise((resolve) => innerServer.listen(0, "127.0.0.1", resolve)); const innerAddress = innerServer.address(); const innerPort = typeof innerAddress === "object" && innerAddress ? innerAddress.port : 0; if (!innerPort) throw new Error("cloud-api inner HTTP server did not expose a port"); const host = options.host ?? "0.0.0.0"; const port = options.port ?? 0; const idleTimeout = parsePositiveInteger(options.idleTimeout ?? env.HWLAB_CLOUD_API_IDLE_TIMEOUT_SECONDS, 120); const server = Bun.serve({ hostname: host, port, idleTimeout, async fetch(request, bunServer) { const url = new URL(request.url); if (url.pathname === "/v1/hwpod-node/ws") { const expectedToken = String(env.HWLAB_HWPOD_NODE_WS_TOKEN ?? "").trim(); const token = String(url.searchParams.get("token") ?? request.headers.get("x-hwpod-node-token") ?? ""); if (expectedToken && token !== expectedToken) return json({ ok: false, error: { code: "hwpod_node_ws_token_invalid" } }, 401); const upgraded = bunServer.upgrade(request, { data: { hwpodNodeWsRegistry } }); return upgraded ? undefined : json({ ok: false, status: "upgrade_required", route: "/v1/hwpod-node/ws" }, 426); } return proxyHttpToInnerServer(request, innerPort); }, websocket: { open(socket: any) { socket.data.hwpodNodeWsRegistry.openBunSocket(socket); }, message(socket: any, message: string | Buffer) { socket.data.hwpodNodeWsRegistry.handleBunMessage(socket, message); }, close(socket: any) { socket.data.hwpodNodeWsRegistry.closeBunSocket(socket); } } }); return { server, innerServer, hwpodNodeWsRegistry, port: server.port, url: `http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${server.port}`, stop() { server.stop(true); innerServer.close(); } }; } async function proxyHttpToInnerServer(request: Request, innerPort: number) { const sourceUrl = new URL(request.url); const targetUrl = new URL(sourceUrl.pathname + sourceUrl.search, `http://127.0.0.1:${innerPort}`); const method = request.method.toUpperCase(); return fetch(targetUrl, { method, headers: request.headers, body: method === "GET" || method === "HEAD" ? undefined : request.body, redirect: "manual" }); } function parsePositiveInteger(value: unknown, fallback: number) { const parsed = Number(value); if (!Number.isFinite(parsed) || parsed <= 0) return fallback; return Math.floor(parsed); } function json(body: any, status = 200) { return new Response(`${JSON.stringify(body)}\n`, { status, headers: { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" } }); }