UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

231 lines (230 loc) • 8.67 kB
import { t as createDeferredCore } from "./deferred-D0La5CRk.mjs"; import { c as isRecord } from "./record-coerce-TIsquoVR.mjs"; import { s as registerSecretValueForRedaction } from "./secret-redaction-registry-kd5IVWi3.mjs"; import { t as createSubsystemLogger } from "./subsystem-4NzR43uz.mjs"; import { n as WebSocketServer, r as createWebSocketStream } from "./websocket-CGHaToS5.mjs"; import { t as isWorkerDesktopArdPassword } from "./worker-desktop-descriptor-DZyj9bOd.mjs"; import { r as hasExactOwnKeys } from "./protocol-record-CQCXx-YV.mjs"; import { r as NODE_PORTAL_ATTACH_PATH, t as NODE_DESKTOP_ATTACH_PATH } from "./node-desktop-stream-BZM2AiRA.mjs"; import { t as createOneTimeTicketStore } from "./one-time-ticket-store-hVfYCIYG.mjs"; import { n as rawDataToString, t as rawDataByteLength } from "./ws-BdD3UP1C.mjs"; import { t as startWebSocketKeepalive } from "./websocket-keepalive-DYmRD_O3.mjs"; import { t as rejectWebSocketUpgrade } from "./websocket-upgrade-reject-D2Ac4nen.mjs"; //#region src/gateway/desktop/node-stream-broker.ts const DEFAULT_TICKET_TTL_MS = 6e4; const MAX_ATTACH_FRAME_BYTES = 65536; const streamLog = createSubsystemLogger("gateway/node-stream"); function parseStreamMetadata(data, isBinary, kind) { const length = rawDataByteLength(data); if (!isBinary || length === 0 || length > MAX_ATTACH_FRAME_BYTES) throw new Error(`invalid node ${kind} attach metadata`); let value; try { value = JSON.parse(rawDataToString(data)); } catch { throw new Error(`invalid node ${kind} attach metadata`); } if (kind === "portal") { if (!isRecord(value) || value.ok !== true || Object.keys(value).length !== 1) throw new Error("invalid node portal attach metadata"); return; } if (!isRecord(value) || value.auth !== "vnc-password" && value.auth !== "ard-account" || !hasExactOwnKeys(value, ["auth"], ["vncPassword"]) || value.vncPassword !== void 0 && typeof value.vncPassword !== "string" || value.auth === "ard-account" && value.vncPassword !== void 0 && !isWorkerDesktopArdPassword(value.vncPassword)) throw new Error("invalid node desktop attach metadata"); const { vncPassword } = value; if (vncPassword) registerSecretValueForRedaction(vncPassword); return { auth: value.auth, ...vncPassword ? { vncPassword } : {} }; } function readAttachedStream(ws, kind, onStreamError) { return new Promise((resolve, reject) => { const cleanup = () => { ws.off("message", onMessage); ws.off("close", onClose); }; const onMessage = (data, isBinary) => { cleanup(); try { const metadata = parseStreamMetadata(data, isBinary, kind); const stream = createWebSocketStream(ws, { allowHalfOpen: false }); stream.on("error", onStreamError); resolve({ metadata, stream }); } catch (error) { reject(error instanceof Error ? error : new Error(String(error))); } }; const onClose = () => { cleanup(); reject(/* @__PURE__ */ new Error(`node ${kind} stream closed before attach`)); }; const onError = (error) => { cleanup(); onStreamError(error); reject(error); }; ws.once("message", onMessage); ws.once("close", onClose); ws.on("error", onError); }); } /** Owns one-time node stream tickets and pairs authenticated desktop or portal duplexes. */ function createNodeDesktopStreamBroker(deps = {}) { const ttlMs = deps.ttlMs ?? DEFAULT_TICKET_TTL_MS; const now = deps.now ?? Date.now; const pending = /* @__PURE__ */ new Map(); const tickets = createOneTimeTicketStore({ ttlMs, now, onExpire: (entry, ticket) => rejectTicket(ticket, /* @__PURE__ */ new Error(`node ${entry.kind} stream ticket expired`)) }); const wss = new WebSocketServer({ noServer: true, maxPayload: MAX_ATTACH_FRAME_BYTES }); const remove = (ticket) => { const entry = pending.get(ticket); if (!entry) return; pending.delete(ticket); tickets.delete(ticket); return entry; }; const rejectTicket = (ticket, error) => { const entry = remove(ticket); if (!entry) return; entry.reject(error); entry.stopKeepalive?.(); entry.closeTrigger ??= "attach-rejected"; entry.ws?.close(1008, `node ${entry.kind} attach rejected`); entry.socket?.destroy(); }; const resolveTicket = (ticket, stream, metadata) => { const entry = remove(ticket); if (!entry) { stream.destroy(); return; } entry.resolve(stream, metadata); }; function mintStream(binding, kind, attach) { const nowMs = now(); const attached = createDeferredCore(); attached.promise.catch(() => void 0); const entry = { kind, binding, expiresAtMs: nowMs + ttlMs, resolve(stream, metadata) { try { attached.resolve(attach(stream, metadata)); } catch (error) { stream.destroy(); attached.reject(error instanceof Error ? error : new Error(String(error))); } }, reject: attached.reject, redeemed: false }; const { token: ticket, expiresAtMs } = tickets.mint(entry, { nowMs }); pending.set(ticket, entry); return { ticket, attachPath: `${kind === "desktop" ? NODE_DESKTOP_ATTACH_PATH : NODE_PORTAL_ATTACH_PATH}?ticket=${ticket}`, expiresAtMs, attached: attached.promise, cancel() { rejectTicket(ticket, /* @__PURE__ */ new Error(`node ${kind} stream ticket cancelled`)); } }; } function mint(binding) { return mintStream(binding, "desktop", (stream, metadata) => { if (!metadata) throw new Error("invalid node desktop attach metadata"); return { ...metadata, stream }; }); } function mintPortal(binding) { return mintStream(binding, "portal", (stream) => ({ stream })); } const bindingIsCurrent = async (registry, binding) => { const hasCurrentConnection = () => registry.getForPairingGeneration(binding.nodeId, binding.pairingGeneration)?.connId === binding.connId; return hasCurrentConnection() && await registry.isConnectionCurrentPairingState(binding.connId) && hasCurrentConnection(); }; async function handleUpgrade(req, socket, head, registry) { const resource = new URL(req.url ?? "/", "http://127.0.0.1"); const kind = resource.pathname === "/node-desktop/attach" ? "desktop" : resource.pathname === "/node-portal/attach" ? "portal" : void 0; if (!kind) return false; const ticket = (resource.searchParams.get("ticket") ?? "").trim(); const entry = pending.get(ticket); if (!entry || entry.kind !== kind || entry.redeemed || entry.expiresAtMs <= now()) { rejectWebSocketUpgrade(socket, { status: 401 }); if (entry && entry.kind === kind && !entry.redeemed) rejectTicket(ticket, /* @__PURE__ */ new Error(`node ${kind} stream ticket expired`)); return true; } entry.redeemed = true; entry.socket = socket; const onSocketError = (error) => rejectTicket(ticket, error); const onSocketClose = () => rejectTicket(ticket, /* @__PURE__ */ new Error(`node ${kind} attach closed during authorization`)); socket.once("error", onSocketError); socket.once("end", onSocketClose); socket.once("close", onSocketClose); let current; try { current = await bindingIsCurrent(registry, entry.binding); } catch { current = false; } if (pending.get(ticket) !== entry) return true; socket.off("error", onSocketError); socket.off("end", onSocketClose); socket.off("close", onSocketClose); if (!current) { rejectWebSocketUpgrade(socket, { status: 401 }); rejectTicket(ticket, /* @__PURE__ */ new Error(`node ${kind} stream ticket binding is stale`)); return true; } try { wss.handleUpgrade(req, socket, head, (ws) => { entry.socket = void 0; entry.ws = ws; ws.once("close", (closeCode) => { streamLog.info("node stream closed", { streamKind: kind, nodeId: entry.binding.nodeId, connId: entry.binding.connId, trigger: entry.closeTrigger ?? "websocket-close", closeCode }); }); ws.once("error", () => { entry.closeTrigger ??= "websocket-error"; }); entry.stopKeepalive = startWebSocketKeepalive(ws); const attached = readAttachedStream(ws, kind, (error) => rejectTicket(ticket, error)); (async () => { try { const resolved = await attached; if (!await bindingIsCurrent(registry, entry.binding)) throw new Error(`node ${kind} stream ticket binding is stale`); resolveTicket(ticket, resolved.stream, resolved.metadata); } catch (error) { rejectTicket(ticket, error instanceof Error ? error : new Error(String(error))); } })(); }); } catch (error) { rejectTicket(ticket, error instanceof Error ? error : new Error(String(error))); } return true; } return { mint, mintPortal, handleUpgrade }; } //#endregion export { createNodeDesktopStreamBroker };