openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
199 lines (198 loc) • 7.6 kB
JavaScript
import { i as registerSecretValueForRedaction } from "./secret-redaction-registry-BOLC6DkF.js";
import { n as NODE_DESKTOP_STREAM_COMMAND } from "./node-desktop-stream-BZM2AiRA.js";
import { l as resolveNodeCommandAllowlist, o as isNodeCommandAllowed } from "./node-command-policy-H4FpS0aA.js";
import { t as DesktopCredentialsRequiredError } from "./host-source-errors-46uOYNUn.js";
import { r as mintDesktopObserverToken } from "./observe-bridge-DyRZ6Fs5.js";
//#region src/gateway/desktop/node-source.ts
function invocationError(result) {
const message = result.error?.message?.trim();
return new Error(message || "node desktop stream closed before attachment");
}
async function stopActiveStream(active) {
if (active.stopped) return;
retireActiveStream(active);
await active.invocation?.catch(() => void 0);
}
function retireActiveStream(active) {
if (active.stopped) return;
active.stopped = true;
clearTimeout(active.unclaimedTimer);
active.ticket?.cancel();
active.controller.abort();
if (!active.reservationTransferred) active.reservation?.release();
active.stream?.destroy();
}
/** Combines node command policy, ticket redemption, and desktop session ownership. */
function createNodeDesktopService(params) {
const ownerEpochs = /* @__PURE__ */ new Map();
const sessions = /* @__PURE__ */ new Map();
const commandAllowed = (node) => isNodeCommandAllowed({
command: NODE_DESKTOP_STREAM_COMMAND,
declaredCommands: node.commands,
allowlist: resolveNodeCommandAllowlist(params.getConfig(), node)
}).ok;
const stopNode = async (nodeId) => {
const session = sessions.get(nodeId);
if (session) await params.desktopRegistry.stop(`node:${nodeId}`, session.ownerEpoch);
};
const ensureSession = async (request) => {
const sourceKey = `node:${request.nodeId}`;
const current = sessions.get(request.nodeId);
if (current?.connId === request.connId && current.pairingGeneration === request.pairingGeneration) {
await params.desktopRegistry.activate({
sourceKey,
ownerEpoch: current.ownerEpoch
});
return current;
}
const ownerEpoch = (ownerEpochs.get(request.nodeId) ?? 0) + 1;
ownerEpochs.set(request.nodeId, ownerEpoch);
const session = {
connId: request.connId,
pairingGeneration: request.pairingGeneration,
ownerEpoch,
active: /* @__PURE__ */ new Set()
};
sessions.set(request.nodeId, session);
try {
await params.desktopRegistry.activate({
sourceKey,
ownerEpoch,
teardown: async () => {
if (sessions.get(request.nodeId) === session) sessions.delete(request.nodeId);
await Promise.all([...session.active].map(stopActiveStream));
session.active.clear();
}
});
return session;
} catch (error) {
if (sessions.get(request.nodeId) === session) sessions.delete(request.nodeId);
throw error;
}
};
return {
stopNode,
async reconcileRuntimePolicy() {
await Promise.all([...sessions].map(async ([nodeId, session]) => {
const node = params.nodeRegistry.get(nodeId);
if (!node || node.connId !== session.connId || node.pairingGeneration !== session.pairingGeneration || !commandAllowed(node)) await stopNode(nodeId);
}));
},
async observe(request) {
const node = params.nodeRegistry.get(request.nodeId);
if (!node?.pairingGeneration) throw new Error("node desktop is unavailable; reconnect and approve the node capability");
const pairingGeneration = node.pairingGeneration;
const isAuthorized = () => params.nodeRegistry.get(request.nodeId) === node && node.pairingGeneration === pairingGeneration && commandAllowed(node);
const assertAuthorized = () => {
if (!isAuthorized()) throw new Error("node desktop is not enabled; explicitly allow and approve desktop.stream for this node");
};
assertAuthorized();
const sourceKey = `node:${request.nodeId}`;
const session = await ensureSession({
nodeId: request.nodeId,
connId: node.connId,
pairingGeneration
});
assertAuthorized();
const active = {
controller: new AbortController(),
reservation: params.desktopRegistry.reserveObserver(sourceKey, session.ownerEpoch),
reservationTransferred: false,
stopped: false
};
if (!active.reservation) throw new Error("node desktop observer limit reached");
session.active.add(active);
try {
active.ticket = params.streamBroker.mint({
nodeId: request.nodeId,
connId: node.connId,
pairingGeneration
});
active.invocation = params.nodeRegistry.invoke({
nodeId: request.nodeId,
expectedConnId: node.connId,
expectedPairingGeneration: pairingGeneration,
command: NODE_DESKTOP_STREAM_COMMAND,
params: {
ticket: active.ticket.ticket,
attachPath: active.ticket.attachPath
},
timeoutMs: 0,
onProgress: () => {},
signal: active.controller.signal,
isDispatchAuthorized: () => !active.stopped && sessions.get(request.nodeId) === session && isAuthorized()
});
const invocationFinished = active.invocation.then((result) => {
throw invocationError(result);
});
invocationFinished.catch(() => void 0);
const attached = await Promise.race([active.ticket.attached, invocationFinished]);
if (active.stopped || sessions.get(request.nodeId) !== session) {
attached.stream.destroy();
throw new Error("node desktop session was superseded before attachment");
}
active.stream = attached.stream;
assertAuthorized();
let preauth;
if (attached.auth === "vnc-password") {
const password = attached.vncPassword ?? request.credentials?.password;
if (!password) throw new DesktopCredentialsRequiredError("vnc-password", "VNC password is required to observe this node");
registerSecretValueForRedaction(password);
preauth = {
auth: attached.auth,
credentials: { password }
};
} else {
const username = request.credentials?.username?.trim() ?? "";
const password = request.credentials?.password ?? "";
if (!username || !password) throw new DesktopCredentialsRequiredError("ard-account", "macOS account credentials are required to observe this node");
registerSecretValueForRedaction(password);
preauth = {
auth: attached.auth,
credentials: {
username,
password
}
};
}
const attachment = params.desktopRegistry.publishStream({
sourceKey,
ownerEpoch: session.ownerEpoch,
stream: attached.stream,
reservation: active.reservation
});
if (!attachment) throw new Error("node desktop session was superseded before publication");
active.reservationTransferred = true;
const minted = mintDesktopObserverToken({
sourceKey,
ownerEpoch: session.ownerEpoch,
control: request.control,
attachment,
preauth
});
active.unclaimedTimer = setTimeout(() => {
if (params.desktopRegistry.hasPendingStream(sourceKey, attachment)) stopActiveStream(active).then(() => session.active.delete(active));
}, Math.max(0, minted.expiresAtMs - Date.now()));
active.unclaimedTimer.unref?.();
active.invocation.finally(() => {
retireActiveStream(active);
session.active.delete(active);
}).catch(() => void 0);
return {
transport: "rfb",
wsPath: `/desktop/observe?token=${minted.token}`,
expiresAtMs: minted.expiresAtMs,
control: request.control,
auth: attached.auth,
preauthenticated: true
};
} catch (error) {
await stopActiveStream(active);
session.active.delete(active);
throw error;
}
}
};
}
//#endregion
export { createNodeDesktopService };