ai-chatbot/scripts/ai-cad-zoo-ws-dev-proxy.mjs

122 lines
3.7 KiB
JavaScript

#!/usr/bin/env node
import { createServer, request as httpRequest } from "node:http";
import path from "node:path";
import process from "node:process";
import { fileURLToPath } from "node:url";
import dotenv from "dotenv";
import WebSocket, { WebSocketServer } from "ws";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
dotenv.config({ path: path.join(root, ".env.local") });
const host = "127.0.0.1";
const port = Number(process.env.HICAD_ZOO_WS_DEV_PROXY_PORT || 3010);
const appPort = Number(process.env.HICAD_APP_DEV_PORT || 3003);
const apiBaseUrl = String(
process.env.KITTYCAD_BASE_URL ||
process.env.ZOO_BASE_URL ||
"https://api.zoo.dev"
).replace(/\/+$/, "");
const apiKey = String(
process.env.KITTYCAD_API_TOKEN ||
process.env.KITTYCAD_API_KEY ||
process.env.ZOO_API_TOKEN ||
""
)
.replace(/\s+/g, "")
.trim();
if (!apiKey) {
throw new Error("Missing KITTYCAD_API_TOKEN in .env.local");
}
const allowedPaths = new Map([
["/api/ai-cad/zoo-ws/ml/copilot", "/ws/ml/copilot"],
["/api/ai-cad/zoo-ws/modeling/commands", "/ws/modeling/commands"],
]);
const browserSockets = new WebSocketServer({ noServer: true });
const server = createServer((request, response) => {
const upstreamRequest = httpRequest(
{
headers: request.headers,
host,
method: request.method,
path: request.url,
port: appPort,
},
(upstreamResponse) => {
response.writeHead(
upstreamResponse.statusCode || 502,
upstreamResponse.headers
);
upstreamResponse.pipe(response);
}
);
upstreamRequest.on("error", (error) => {
if (!response.headersSent) {
response.writeHead(502, {
"Content-Type": "text/plain; charset=utf-8",
});
}
response.end(`HiCAD development proxy failed: ${error.message}`);
});
request.pipe(upstreamRequest);
});
server.on("upgrade", (request, socket, head) => {
const incomingUrl = new URL(request.url || "/", `http://${host}:${port}`);
const upstreamPath = allowedPaths.get(incomingUrl.pathname);
if (!upstreamPath) {
socket.write("HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n");
socket.destroy();
return;
}
browserSockets.handleUpgrade(request, socket, head, (browserSocket) => {
const base = new URL(apiBaseUrl);
base.protocol = base.protocol === "http:" ? "ws:" : "wss:";
base.pathname = upstreamPath;
base.search = incomingUrl.search;
const upstreamSocket = new WebSocket(base, {
headers: {
Authorization: `Bearer ${apiKey}`,
Origin: "https://app.zoo.dev",
},
});
const closeBoth = (code = 1011, reason = "Zoo connection closed") => {
if (browserSocket.readyState < WebSocket.CLOSING) {
browserSocket.close(code, reason);
}
if (upstreamSocket.readyState < WebSocket.CLOSING) {
upstreamSocket.close(code, reason);
}
};
upstreamSocket.on("open", () => {
browserSocket.on("message", (data, isBinary) => {
if (upstreamSocket.readyState === WebSocket.OPEN) {
upstreamSocket.send(data, { binary: isBinary });
}
});
upstreamSocket.on("message", (data, isBinary) => {
if (browserSocket.readyState === WebSocket.OPEN) {
browserSocket.send(data, { binary: isBinary });
}
});
});
browserSocket.on("close", () => closeBoth(1000, "Browser disconnected"));
browserSocket.on("error", () => closeBoth());
upstreamSocket.on("close", () => closeBoth(1000, "Zoo disconnected"));
upstreamSocket.on("error", (error) => {
console.error("[HiCAD Zoo WS proxy]", error.message);
closeBoth();
});
});
});
server.listen(port, host, () => {
console.log(`HiCAD development proxy: http://${host}:${port}`);
});