import crypto from "node:crypto"; import { createServer } from "node:http"; import express from "express"; import { WebSocket, WebSocketServer } from "ws"; const DEFAULT_WS_PATH = "/api/ai-workspace/hub"; const MAX_EVENTS_PER_AGENT = 5000; const MAX_REQUEST_META_PER_AGENT = 1000; const HEARTBEAT_INTERVAL_MS = 30000; const AGENT_STALE_MS = 75000; const ASSISTANT_RELAY_POLL_TIMEOUT_MS = Number(process.env.AI_WORKSPACE_ASSISTANT_RELAY_POLL_TIMEOUT_MS || 25000); const ASSISTANT_RELAY_ACTION_TIMEOUT_MS = Number(process.env.AI_WORKSPACE_ASSISTANT_RELAY_ACTION_TIMEOUT_MS || 45000); const ASSISTANT_RELAY_MAX_PENDING = Number(process.env.AI_WORKSPACE_ASSISTANT_RELAY_MAX_PENDING || 200); const ASSISTANT_RELAY_MAX_BATCH = Number(process.env.AI_WORKSPACE_ASSISTANT_RELAY_MAX_BATCH || 10); const config = readConfig(); const app = express(); const httpServer = createServer(app); const wss = new WebSocketServer({ noServer: true }); const agentsByCode = new Map(); const assistantRelaysById = new Map(); let eventSeq = 0; app.disable("x-powered-by"); app.use(express.json({ limit: "1mb" })); app.get("/healthz", (_req, res) => { res.json({ ok: true, service: "nodedc-ai-workspace-hub", wsPath: config.wsPath, agentsOnline: Array.from(agentsByCode.values()).filter(isAgentOnline).length, assistantRelays: assistantRelaysById.size, internalApiConfigured: config.internalAccessTokens.length > 0, }); }); app.get("/api/ai-workspace/hub/v1/agents/:pairingCode", requireInternalApi, (req, res) => { const agent = agentsByCode.get(cleanPairingCode(req.params.pairingCode)); res.json({ ok: true, agent: publicAgent(agent) }); }); app.get("/api/ai-workspace/hub/v1/agents/:pairingCode/events", requireInternalApi, (req, res) => { const payload = readAgentEvents(req.params.pairingCode, { since: req.query.since, limit: req.query.limit, }); res.json({ ok: true, ...payload }); }); app.post("/api/ai-workspace/hub/v1/agents/:pairingCode/request", requireInternalApi, asyncRoute(async (req, res) => { const command = cleanString(req.body?.command, 100); const timeoutMs = sanitizeTimeoutMs(req.body?.timeoutMs, 30000); const payload = await requestAgent(req.params.pairingCode, command, req.body?.payload || {}, timeoutMs, { quiet: Boolean(req.body?.quiet), }); res.json({ ok: true, payload }); })); app.post("/api/ai-workspace/hub/v1/agents/:pairingCode/dispatch", requireInternalApi, (req, res) => { const command = cleanString(req.body?.command, 100); const timeoutMs = sanitizeTimeoutMs(req.body?.timeoutMs, 30000); const dispatch = dispatchAgent(req.params.pairingCode, command, req.body?.payload || {}, timeoutMs, { quiet: Boolean(req.body?.quiet), }); res.json({ ok: true, requestId: dispatch.requestId }); }); app.post("/api/ai-workspace/assistant/v1/actions", requireInternalApi, asyncRoute(proxyAssistantActions)); app.post("/api/ai-workspace/setup-codes/redeem", asyncRoute(proxySetupCodeRedeem)); app.post("/api/ai-workspace/hub/v1/assistant-relays/:relayId/actions", requireInternalApi, asyncRoute(callAssistantRelayAction)); app.post("/api/ai-workspace/hub/v1/assistant-relays/:relayId/poll", requireInternalApi, asyncRoute(pollAssistantRelay)); app.post("/api/ai-workspace/hub/v1/assistant-relays/:relayId/results/:callId", requireInternalApi, asyncRoute(completeAssistantRelayCall)); app.use("/api/ai-workspace/hub/v1/ndc-agent-mcp/:pairingCode", asyncRoute(proxyNdcAgentMcp)); app.use((error, _req, res, _next) => { const status = Number(error?.status || 500); res.status(status >= 400 && status < 600 ? status : 500).json({ ok: false, error: String(error?.message || error || "internal_error"), }); }); httpServer.on("upgrade", (req, socket, head) => { let pathname = ""; try { pathname = new URL(req.url || "/", "http://localhost").pathname; } catch {} if (pathname !== config.wsPath) return; wss.handleUpgrade(req, socket, head, (ws) => { wss.emit("connection", ws, req); }); }); wss.on("connection", attachAgent); const heartbeat = setInterval(() => { for (const agent of Array.from(agentsByCode.values())) { if (!isSocketOpen(agent) || isAgentStale(agent)) { removeAgent(agent, "bridge_agent_stale"); continue; } try { agent.awaitingPong = true; agent.lastPingAt = new Date().toISOString(); agent.socket.ping(); } catch { removeAgent(agent, "bridge_agent_ping_failed"); } } }, HEARTBEAT_INTERVAL_MS); httpServer.listen(config.port, "0.0.0.0", () => { console.log(`NODE.DC AI Workspace Hub listening on http://0.0.0.0:${config.port}`); console.log(`NODE.DC AI Workspace Hub ws path: ${config.wsPath}`); }); process.on("SIGTERM", shutdown); process.on("SIGINT", shutdown); function attachAgent(socket, req) { const url = new URL(req.url || config.wsPath, "http://localhost"); const pairingCode = cleanPairingCode(url.searchParams.get("pairingCode")); if (!pairingCode) { socket.close(1008, "pairing_code_required"); return; } const previous = agentsByCode.get(pairingCode); if (previous) { removeAgent(previous, "agent_replaced"); } const now = new Date().toISOString(); const agent = { socket, pairingCode, machineName: cleanString(url.searchParams.get("machineName"), 120), host: "", agentVersion: "", protocolVersion: "", capabilities: [], runtimeStatus: "", runtimeError: "", runtimeCheckedAt: "", connectedAt: now, lastSeenAt: now, lastPingAt: "", awaitingPong: false, closed: false, pending: new Map(), requestMeta: previous?.requestMeta || new Map(), events: previous?.events || [], }; agentsByCode.set(pairingCode, agent); pushAgentEvent(agent, "", { kind: "connected", message: "Bridge agent connected." }); socket.on("pong", () => { agent.lastSeenAt = new Date().toISOString(); agent.awaitingPong = false; }); socket.on("message", (raw) => handleAgentMessage(agent, raw)); socket.on("close", () => removeAgent(agent, "bridge_agent_offline")); socket.on("error", () => {}); sendJson(socket, { type: "ready", pairingCode, connectedAt: now, }); } function handleAgentMessage(agent, raw) { agent.lastSeenAt = new Date().toISOString(); agent.awaitingPong = false; let message = null; try { message = JSON.parse(String(raw)); } catch { return; } if (message?.type === "hello") { agent.machineName = cleanString(message.machineName || agent.machineName, 120); agent.host = cleanString(message.host, 120); agent.agentVersion = cleanString(message.agentVersion, 80); agent.protocolVersion = cleanString(message.protocolVersion, 120); agent.capabilities = cleanStringList(message.capabilities, 80, 80); agent.runtimeStatus = cleanString(message.runtimeStatus, 40); agent.runtimeError = cleanString(message.runtimeError, 1000); agent.runtimeCheckedAt = cleanString(message.runtimeCheckedAt, 80); pushAgentEvent(agent, "", { kind: "hello", message: "Bridge agent hello received.", cwd: cleanString(message.cwd, 500), agentVersion: agent.agentVersion, protocolVersion: agent.protocolVersion, }); return; } if (message?.type === "event") { const pending = message?.requestId ? agent.pending.get(String(message.requestId || "")) : null; if (pending?.quiet) return; pushAgentEvent(agent, message.requestId, message.event || message.payload || {}); return; } if (message?.type !== "response") return; const requestId = String(message.requestId || ""); const pending = agent.pending.get(requestId); if (!pending) return; agent.pending.delete(requestId); clearTimeout(pending.timeout); if (!pending.quiet) { pushAgentEvent(agent, requestId, { kind: "response_received", message: `Bridge response received after ${elapsedLabel(pending.startedAt)}.`, }); } if (message.ok === false) { const error = new Error(String(message.error || "bridge_agent_failed")); error.status = Number(message.status || 502); if (!pending.quiet) pushAgentEvent(agent, requestId, { kind: "error", message: error.message }); pending.reject(error); return; } if (!pending.quiet) pushAgentEvent(agent, requestId, { kind: "done", message: "Bridge request completed." }); pending.resolve(message.payload || { ok: true }); } function requestAgent(pairingCodeRaw, command, payload = {}, timeoutMs = 30000, options = {}) { return startAgentRequest(pairingCodeRaw, command, payload, timeoutMs, options).promise; } function dispatchAgent(pairingCodeRaw, command, payload = {}, timeoutMs = 30000, options = {}) { const { requestId, promise } = startAgentRequest(pairingCodeRaw, command, payload, timeoutMs, options); promise.catch(() => {}); return { requestId }; } async function proxyNdcAgentMcp(req, res) { if (!config.engineInternalUrl || !config.engineInternalAccessToken) { res.status(503).json({ ok: false, error: "engine_proxy_not_configured" }); return; } const pairingCode = cleanPairingCode(req.params.pairingCode); const agent = agentsByCode.get(pairingCode); if (!agent || !isAgentOnline(agent)) { res.status(503).json({ ok: false, error: "bridge_agent_offline" }); return; } const marker = `/api/ai-workspace/hub/v1/ndc-agent-mcp/${encodeURIComponent(req.params.pairingCode)}`; const suffix = String(req.originalUrl || "").startsWith(marker) ? String(req.originalUrl || "").slice(marker.length) : String(req.url || ""); const targetUrl = `${config.engineInternalUrl.replace(/\/+$/, "")}/api/ndc-agent-mcp${suffix || ""}`; const method = String(req.method || "GET").toUpperCase(); const hasBody = !["GET", "HEAD"].includes(method); const upstream = await fetch(targetUrl, { method, redirect: "manual", headers: { Accept: "application/json", Authorization: `Bearer ${config.engineInternalAccessToken}`, ...(hasBody ? { "Content-Type": "application/json" } : {}), }, ...(hasBody ? { body: JSON.stringify(req.body || {}) } : {}), }); const contentType = upstream.headers.get("content-type") || "application/json; charset=utf-8"; const text = await upstream.text(); res.status(upstream.status); res.setHeader("content-type", contentType); res.send(text); } async function proxyAssistantActions(req, res) { if (!config.assistantInternalUrl || !config.assistantInternalAccessToken) { res.status(503).json({ ok: false, error: "assistant_action_proxy_not_configured" }); return; } const targetUrl = `${config.assistantInternalUrl.replace(/\/+$/, "")}/api/ai-workspace/assistant/v1/actions`; const upstream = await fetch(targetUrl, { method: "POST", redirect: "manual", headers: { Accept: "application/json", "Content-Type": "application/json", Authorization: `Bearer ${config.assistantInternalAccessToken}`, ...forwardOwnerHeaders(req), }, body: JSON.stringify(req.body || {}), }); const contentType = upstream.headers.get("content-type") || "application/json; charset=utf-8"; const text = await upstream.text(); res.status(upstream.status); res.setHeader("content-type", contentType); res.send(text); } async function proxySetupCodeRedeem(req, res) { if (!config.assistantInternalUrl || !config.assistantInternalAccessToken) { res.status(503).json({ ok: false, error: "assistant_setup_proxy_not_configured" }); return; } const targetUrl = `${config.assistantInternalUrl.replace(/\/+$/, "")}/api/ai-workspace/assistant/v1/setup-codes/redeem`; const upstream = await fetch(targetUrl, { method: "POST", redirect: "manual", headers: { Accept: "application/json", "Content-Type": "application/json", Authorization: `Bearer ${config.assistantInternalAccessToken}`, }, body: JSON.stringify(req.body || {}), }); const contentType = upstream.headers.get("content-type") || "application/json; charset=utf-8"; const text = await upstream.text(); res.status(upstream.status); res.setHeader("content-type", contentType); res.send(text); } async function callAssistantRelayAction(req, res) { const relayId = cleanRelayId(req.params.relayId); if (!relayId) { res.status(400).json({ ok: false, error: "assistant_relay_id_required" }); return; } const relay = getAssistantRelay(relayId); pruneAssistantRelay(relay); if (relay.calls.size >= ASSISTANT_RELAY_MAX_PENDING) { res.status(429).json({ ok: false, error: "assistant_relay_backpressure" }); return; } const callId = crypto.randomUUID(); const timeoutMs = sanitizeTimeoutMs(req.body?.timeoutMs || req.query?.timeoutMs || ASSISTANT_RELAY_ACTION_TIMEOUT_MS, ASSISTANT_RELAY_ACTION_TIMEOUT_MS); const createdAt = new Date().toISOString(); const call = { callId, relayId, createdAt, payload: req.body || {}, headers: forwardOwnerHeaders(req), }; const result = await new Promise((resolve, reject) => { const timeout = setTimeout(() => { relay.calls.delete(callId); relay.pending = relay.pending.filter((item) => item.callId !== callId); const error = new Error("assistant_relay_timeout"); error.status = 504; reject(error); }, timeoutMs); timeout.unref?.(); relay.calls.set(callId, { resolve, reject, timeout, createdAt: Date.now() }); relay.pending.push(call); notifyAssistantRelayWaiters(relay); }); res.status(result.status).json(result.body); } async function pollAssistantRelay(req, res) { const relayId = cleanRelayId(req.params.relayId); if (!relayId) { res.status(400).json({ ok: false, error: "assistant_relay_id_required" }); return; } const relay = getAssistantRelay(relayId); relay.lastSeenAt = new Date().toISOString(); const limit = sanitizeInteger(req.body?.limit, ASSISTANT_RELAY_MAX_BATCH, 1, ASSISTANT_RELAY_MAX_BATCH); const timeoutMs = sanitizeTimeoutMs(req.body?.timeoutMs || req.query?.timeoutMs || ASSISTANT_RELAY_POLL_TIMEOUT_MS, ASSISTANT_RELAY_POLL_TIMEOUT_MS); const calls = await waitForAssistantRelayCalls(relay, limit, timeoutMs); res.json({ ok: true, relayId, calls }); } async function completeAssistantRelayCall(req, res) { const relayId = cleanRelayId(req.params.relayId); const callId = cleanString(req.params.callId, 120); const relay = assistantRelaysById.get(relayId); const pending = relay?.calls.get(callId); if (!relay || !pending) { res.status(404).json({ ok: false, error: "assistant_relay_call_not_found" }); return; } relay.calls.delete(callId); clearTimeout(pending.timeout); pending.resolve({ status: sanitizeHttpStatus(req.body?.status), body: req.body?.body === undefined ? { ok: true } : req.body.body, }); res.json({ ok: true, relayId, callId }); } function getAssistantRelay(relayId) { const cleanId = cleanRelayId(relayId); let relay = assistantRelaysById.get(cleanId); if (!relay) { relay = { relayId: cleanId, pending: [], waiters: [], calls: new Map(), createdAt: new Date().toISOString(), lastSeenAt: "", }; assistantRelaysById.set(cleanId, relay); } return relay; } function waitForAssistantRelayCalls(relay, limit, timeoutMs) { const ready = takeAssistantRelayCalls(relay, limit); if (ready.length) return Promise.resolve(ready); return new Promise((resolve) => { const timeout = setTimeout(() => { relay.waiters = relay.waiters.filter((waiter) => waiter.resolve !== resolve); resolve([]); }, timeoutMs); timeout.unref?.(); relay.waiters.push({ resolve, timeout, limit }); }); } function notifyAssistantRelayWaiters(relay) { while (relay.waiters.length && relay.pending.length) { const waiter = relay.waiters.shift(); clearTimeout(waiter.timeout); waiter.resolve(takeAssistantRelayCalls(relay, waiter.limit)); } } function takeAssistantRelayCalls(relay, limit) { const calls = []; while (relay.pending.length && calls.length < limit) { const call = relay.pending.shift(); if (relay.calls.has(call.callId)) calls.push(call); } return calls; } function pruneAssistantRelay(relay) { relay.pending = relay.pending.filter((call) => relay.calls.has(call.callId)); } function cleanRelayId(value) { return String(value || "").trim().replace(/[^A-Za-z0-9_.:-]/g, "").slice(0, 120); } function sanitizeHttpStatus(value) { const status = Number(value || 200); return Number.isInteger(status) && status >= 100 && status <= 599 ? status : 200; } function forwardOwnerHeaders(req) { const headers = {}; for (const name of [ "x-nodedc-user-id", "x-nodedc-user-email", "x-nodedc-user-role", "x-nodedc-user-groups", ]) { const value = req.headers[name]; if (typeof value === "string" && value.trim()) headers[name] = value.trim(); } return headers; } function startAgentRequest(pairingCodeRaw, command, payload = {}, timeoutMs = 30000, options = {}) { const pairingCode = cleanPairingCode(pairingCodeRaw); const agent = agentsByCode.get(pairingCode); if (!agent || !isAgentOnline(agent)) { const error = new Error("bridge_agent_offline"); error.status = 503; throw error; } const requestId = crypto.randomUUID(); const quiet = Boolean(options?.quiet); agent.requestMeta.set(requestId, requestMetaFromPayload(command, payload)); trimRequestMeta(agent); const promise = new Promise((resolve, reject) => { const timeout = setTimeout(() => { agent.pending.delete(requestId); const error = new Error("bridge_timeout"); error.status = 504; if (!quiet) pushAgentEvent(agent, requestId, { kind: "timeout", command, message: "Bridge request timed out." }); reject(error); }, timeoutMs); const startedAt = Date.now(); agent.pending.set(requestId, { resolve, reject, timeout, startedAt, quiet }); if (!quiet) pushAgentEvent(agent, requestId, { kind: "engine_send", command, message: `Engine sent request: ${command}` }); sendJson(agent.socket, { type: "request", requestId, command: String(command || ""), payload, quiet, }); }); return { requestId, promise }; } function readAgentEvents(pairingCodeRaw, options = {}) { const pairingCode = cleanPairingCode(pairingCodeRaw); const agent = agentsByCode.get(pairingCode); const since = Number(options.since || 0); const limit = Math.max(0, Math.min(2000, Number(options.limit || 100))); const events = agent?.events || []; const latestEventId = events.length ? events[events.length - 1].id : eventSeq; return { events: events.filter((event) => event.id > since).slice(-limit), latestEventId, online: Boolean(agent && isAgentOnline(agent)), }; } function requestMetaFromPayload(command, payload = {}) { const context = payload?.context && typeof payload.context === "object" ? payload.context : {}; const publicUserMessage = payload?.displayMessage || payload?.publicUserMessage || payload?.message; return { command: cleanString(command, 80), threadId: cleanString(payload?.threadId, 160), threadTitle: cleanString(payload?.threadTitle, 240), userMessage: cleanString(publicUserMessage, 12000), remoteControlMode: cleanString(context.remoteControlMode, 40), }; } function pushAgentEvent(agent, requestId, rawEvent = {}) { if (!agent) return null; const now = new Date().toISOString(); const cleanRequestId = String(requestId || rawEvent.requestId || ""); const meta = cleanRequestId ? agent.requestMeta?.get(cleanRequestId) : null; const event = { id: ++eventSeq, pairingCode: agent.pairingCode, requestId: cleanRequestId, threadId: rawEvent.threadId == null ? cleanString(meta?.threadId, 160) : cleanString(rawEvent.threadId, 160), sessionId: rawEvent.sessionId == null ? "" : cleanString(rawEvent.sessionId, 160), threadTitle: rawEvent.threadTitle == null ? cleanString(meta?.threadTitle, 240) : cleanString(rawEvent.threadTitle, 240), userMessage: rawEvent.userMessage == null ? cleanString(meta?.userMessage, 12000) : cleanString(rawEvent.userMessage, 12000), remoteControlMode: rawEvent.remoteControlMode == null ? cleanString(meta?.remoteControlMode, 40) : cleanString(rawEvent.remoteControlMode, 40), kind: cleanString(rawEvent.kind || rawEvent.type || "event", 40), stream: rawEvent.stream ? cleanString(rawEvent.stream, 40) : "", text: rawEvent.text == null ? "" : cleanString(rawEvent.text, 12000), message: rawEvent.message == null ? "" : cleanString(rawEvent.message, 12000), command: rawEvent.command == null ? "" : cleanString(rawEvent.command, 1000), cwd: rawEvent.cwd == null ? "" : cleanString(rawEvent.cwd, 500), currentFile: rawEvent.currentFile == null ? "" : cleanString(rawEvent.currentFile, 1000), eventsFile: rawEvent.eventsFile == null ? "" : cleanString(rawEvent.eventsFile, 1000), at: rawEvent.at ? cleanString(rawEvent.at, 80) : now, }; agent.events.push(event); if (agent.events.length > MAX_EVENTS_PER_AGENT) { agent.events.splice(0, agent.events.length - MAX_EVENTS_PER_AGENT); } return event; } function publicAgent(agent) { if (!agent) return null; return { online: isAgentOnline(agent), pairingCode: agent.pairingCode, machineName: agent.machineName, host: agent.host, agentVersion: agent.agentVersion, protocolVersion: agent.protocolVersion, capabilities: agent.capabilities, runtimeStatus: agent.runtimeStatus, runtimeError: agent.runtimeError, runtimeCheckedAt: agent.runtimeCheckedAt, connectedAt: agent.connectedAt, lastSeenAt: agent.lastSeenAt, }; } function isSocketOpen(agent) { return Boolean(agent?.socket && agent.socket.readyState === WebSocket.OPEN); } function isAgentStale(agent) { const lastSeenMs = Date.parse(agent?.lastSeenAt || agent?.connectedAt || ""); if (!Number.isFinite(lastSeenMs)) return true; return Date.now() - lastSeenMs > AGENT_STALE_MS; } function isAgentOnline(agent) { return isSocketOpen(agent) && !isAgentStale(agent); } function removeAgent(agent, reason = "bridge_agent_offline") { if (!agent || agent.closed) return; agent.closed = true; const error = Object.assign(new Error(reason), { status: reason === "agent_replaced" ? 409 : 503 }); pushAgentEvent(agent, "", { kind: "disconnected", message: reason === "agent_replaced" ? "Bridge agent connection replaced." : reason === "bridge_agent_stale" ? "Bridge agent heartbeat timed out." : "Bridge agent disconnected.", }); if (agentsByCode.get(agent.pairingCode) === agent) agentsByCode.delete(agent.pairingCode); settleAll(agent, error); try { if (isSocketOpen(agent)) agent.socket.terminate(); } catch {} } function settleAll(agent, error) { for (const pending of agent.pending.values()) { clearTimeout(pending.timeout); pending.reject(error); } agent.pending.clear(); } function trimRequestMeta(agent) { if (!agent?.requestMeta || agent.requestMeta.size <= MAX_REQUEST_META_PER_AGENT) return; const excess = agent.requestMeta.size - MAX_REQUEST_META_PER_AGENT; Array.from(agent.requestMeta.keys()).slice(0, excess).forEach((key) => agent.requestMeta.delete(key)); } function sendJson(socket, payload) { socket.send(JSON.stringify(payload)); } function cleanPairingCode(value) { return String(value || "") .trim() .toUpperCase() .replace(/[^A-Z0-9]/g, "") .slice(0, 32); } function cleanString(value, maxLength) { return String(value || "").trim().slice(0, maxLength); } function cleanStringList(value, maxItems = 80, maxLength = 120) { const source = Array.isArray(value) ? value : String(value || "").split(/[\n,;]+/); const out = []; for (const item of source) { const text = cleanString(item, maxLength); if (!text || out.includes(text)) continue; out.push(text); if (out.length >= maxItems) break; } return out; } function sanitizeTimeoutMs(value, fallback) { const numeric = Number(value || fallback); if (!Number.isFinite(numeric)) return fallback; return Math.max(1000, Math.min(12 * 60 * 60 * 1000, numeric)); } function sanitizeInteger(value, fallback, min, max) { const numeric = Number(value || fallback); if (!Number.isFinite(numeric)) return fallback; return Math.min(Math.max(Math.trunc(numeric), min), max); } function elapsedLabel(startedAt) { const elapsed = Math.max(0, Date.now() - Number(startedAt || Date.now())); if (elapsed < 1000) return `${elapsed}ms`; return `${(elapsed / 1000).toFixed(1)}s`; } function asyncRoute(handler) { return (req, res, next) => Promise.resolve(handler(req, res, next)).catch(next); } function requireInternalApi(req, res, next) { if (!config.internalAccessTokens.length) { res.status(503).json({ ok: false, error: "ai_workspace_hub_token_not_configured" }); return; } const header = String(req.headers.authorization || ""); const token = header.startsWith("Bearer ") ? header.slice(7) : ""; if (!token || !config.internalAccessTokens.some((candidate) => safeEqual(token, candidate))) { res.status(401).json({ ok: false, error: "ai_workspace_hub_unauthorized" }); return; } next(); } function safeEqual(left, right) { const leftBuffer = Buffer.from(String(left || "")); const rightBuffer = Buffer.from(String(right || "")); return leftBuffer.length === rightBuffer.length && crypto.timingSafeEqual(leftBuffer, rightBuffer); } function readConfig() { const internalAccessTokens = Array.from(new Set([ process.env.AI_WORKSPACE_HUB_TOKEN, process.env.NODEDC_INTERNAL_ACCESS_TOKEN, ].map((value) => cleanString(value, 1000)).filter(Boolean))); return { port: Number(process.env.PORT || 18081), wsPath: process.env.AI_WORKSPACE_HUB_WS_PATH || DEFAULT_WS_PATH, internalAccessTokens, engineInternalUrl: cleanString(process.env.NODEDC_ENGINE_INTERNAL_URL, 1000).replace(/\/+$/, ""), engineInternalAccessToken: cleanString(process.env.NODEDC_INTERNAL_ACCESS_TOKEN, 1000), assistantInternalUrl: cleanString( process.env.NODEDC_AI_WORKSPACE_ASSISTANT_URL || process.env.NDC_AI_WORKSPACE_ASSISTANT_INTERNAL_URL || process.env.AI_WORKSPACE_ASSISTANT_INTERNAL_URL || "http://ai-workspace-assistant:18082", 1000, ).replace(/\/+$/, ""), assistantInternalAccessToken: cleanString(process.env.NODEDC_INTERNAL_ACCESS_TOKEN, 1000), }; } function shutdown() { clearInterval(heartbeat); wss.close(); httpServer.close(() => { process.exit(0); }); setTimeout(() => process.exit(1), 5000).unref(); }