openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
237 lines (236 loc) • 10.6 kB
JavaScript
import { D as resolveExpiresAtMsFromDurationMs, d as asPositiveSafeInteger, g as isFutureDateTimestampMs, o as asDateTimestampMs } from "./number-coercion-CLj0HTDM.js";
import { t as safeEqualSecret } from "./secret-equal-DRsL8lKD.js";
import "./version-CwNT1gaY.js";
import { n as invalidateGatewayPolicyClient } from "./ws-policy-close-CZEENNh9.js";
import { isDeepStrictEqual } from "node:util";
import { randomBytes } from "node:crypto";
//#region src/gateway/plugin-node-capability.ts
/** Path marker used to scope plugin-hosted node URLs with one-time capabilities. */
const PLUGIN_NODE_CAPABILITY_PATH_PREFIX = "/__openclaw__/cap";
const PLUGIN_NODE_CAPABILITY_QUERY_PARAM = "oc_cap";
/** Default lifetime for plugin-node capability tokens. */
const DEFAULT_PLUGIN_NODE_CAPABILITY_TTL_MS = 6e5;
/** Index surfaces by normalized surface id, keeping the strictest TTL per surface. */
function indexPluginNodeCapabilitySurfaces(surfaces) {
const indexed = {};
for (const entry of surfaces) {
const surface = normalizeSurface(entry.surface);
if (!surface) continue;
const existing = indexed[surface];
const next = {
...entry,
surface
};
if (!existing || resolvePluginNodeCapabilityTtlMs(next) < resolvePluginNodeCapabilityTtlMs(existing)) indexed[surface] = next;
}
return indexed;
}
/** Reconnect changed nodes so the handshake owns newly scoped URLs and capabilities. */
function reconcileClientPluginNodeCapabilities(client, surfaces, close) {
if (client.connect.role !== "node" || (client.connect.maxProtocol < 4 ? isDeepStrictEqual(client.pluginNodeCapabilitySurfaces ?? {}, surfaces) : [...client.connect.caps ?? [], ...Object.keys(client.pluginSurfaceUrls ?? {})].every((surface) => isDeepStrictEqual(client.pluginNodeCapabilitySurfaces?.[surface], surfaces[surface])))) return true;
invalidateGatewayPolicyClient(client, {
reason: "plugin-node-capabilities-changed",
code: 1012,
message: "node capabilities changed",
close
});
return false;
}
function normalizeCapability(raw) {
const trimmed = raw?.trim();
return trimmed ? trimmed : void 0;
}
function normalizeSurface(raw) {
const trimmed = raw?.trim();
return trimmed ? trimmed : void 0;
}
function resolvePluginNodeCapabilityStorageKey(surface) {
const normalizedSurface = normalizeSurface(surface.surface);
if (!normalizedSurface) return;
const scopeKey = surface.scopeKey?.trim();
return scopeKey ? `${normalizedSurface}\0${scopeKey}` : normalizedSurface;
}
/** Resolve a positive TTL for a plugin-node capability surface. */
function resolvePluginNodeCapabilityTtlMs(surface) {
return asPositiveSafeInteger(surface.ttlMs) ?? 6e5;
}
/** Resolve the expiration timestamp for a capability minted against a surface. */
function resolvePluginNodeCapabilityExpiresAtMs(surface, nowMs = Date.now()) {
return resolveExpiresAtMsFromDurationMs(resolvePluginNodeCapabilityTtlMs(surface), { nowMs });
}
/** Mint an opaque capability token for plugin-node surface access. */
function mintPluginNodeCapabilityToken() {
return randomBytes(18).toString("base64url");
}
/** Append a capability path segment to a plugin host URL. */
function buildPluginNodeCapabilityScopedHostUrl(baseUrl, capability) {
const normalizedCapability = normalizeCapability(capability);
if (!normalizedCapability) return;
try {
const url = new URL(baseUrl);
url.pathname = `${url.pathname.replace(/\/+$/, "")}${`${PLUGIN_NODE_CAPABILITY_PATH_PREFIX}/${encodeURIComponent(normalizedCapability)}`}`;
url.search = "";
url.hash = "";
return url.toString().replace(/\/$/, "");
} catch {
return;
}
}
/** Replace the capability segment in an already scoped host URL. */
function replacePluginNodeCapabilityInScopedHostUrl(scopedUrl, capability) {
const normalizedCapability = normalizeCapability(capability);
if (!normalizedCapability) return;
try {
const url = new URL(scopedUrl);
const prefix = `${PLUGIN_NODE_CAPABILITY_PATH_PREFIX}/`;
const markerStart = url.pathname.indexOf(prefix);
if (markerStart < 0) return buildPluginNodeCapabilityScopedHostUrl(scopedUrl, normalizedCapability);
const capabilityStart = markerStart + prefix.length;
const nextSlashIndex = url.pathname.indexOf("/", capabilityStart);
const capabilityEnd = nextSlashIndex >= 0 ? nextSlashIndex : url.pathname.length;
if (capabilityEnd <= capabilityStart) return;
url.pathname = url.pathname.slice(0, capabilityStart) + encodeURIComponent(normalizedCapability) + url.pathname.slice(capabilityEnd);
url.search = "";
url.hash = "";
return url.toString().replace(/\/$/, "");
} catch {
return;
}
}
function pluginNodeCapabilityFromScopedHostUrl(rawUrl) {
try {
const pathname = new URL(rawUrl).pathname;
const prefix = `${PLUGIN_NODE_CAPABILITY_PATH_PREFIX}/`;
const markerStart = pathname.indexOf(prefix);
if (markerStart < 0) return;
const capabilityStart = markerStart + prefix.length;
const nextSlashIndex = pathname.indexOf("/", capabilityStart);
const capabilityEnd = nextSlashIndex >= 0 ? nextSlashIndex : pathname.length;
if (capabilityEnd <= capabilityStart) return;
return normalizeCapability(decodeURIComponent(pathname.slice(capabilityStart, capabilityEnd)));
} catch {
return;
}
}
/** Detect conflicting scoped capabilities while allowing transport host rewriting. */
function pluginNodeCapabilityScopedHostUrlsConflict(first, second) {
const firstCapability = pluginNodeCapabilityFromScopedHostUrl(first);
const secondCapability = pluginNodeCapabilityFromScopedHostUrl(second);
return Boolean(firstCapability && secondCapability && !safeEqualSecret(firstCapability, secondCapability));
}
/** Check whether a client's current scoped surface URL still has live authorization. */
function hasAuthorizedClientPluginNodeCapabilityUrl(params) {
const storageKey = resolvePluginNodeCapabilityStorageKey(params.surface);
const capability = pluginNodeCapabilityFromScopedHostUrl(params.url);
if (!storageKey || !capability) return false;
const entry = params.client.pluginNodeCapabilities?.[storageKey];
const nowMs = params.nowMs ?? Date.now();
return Boolean(entry && isFutureDateTimestampMs(entry.expiresAtMs, { nowMs }) && safeEqualSecret(entry.capability, capability));
}
/** Parse and rewrite scoped capability URLs into canonical paths plus query tokens. */
function normalizePluginNodeCapabilityScopedUrl(rawUrl) {
let url;
try {
url = new URL(rawUrl, "http://localhost");
} catch {
return {
pathname: "/",
scopedPath: false,
malformedScopedPath: true
};
}
const prefix = `${PLUGIN_NODE_CAPABILITY_PATH_PREFIX}/`;
let scopedPath = false;
let malformedScopedPath = false;
let capabilityFromPath;
let rewrittenUrl;
if (url.pathname.startsWith(prefix)) {
scopedPath = true;
const remainder = url.pathname.slice(prefix.length);
const slashIndex = remainder.indexOf("/");
if (slashIndex <= 0) malformedScopedPath = true;
else {
const encodedCapability = remainder.slice(0, slashIndex);
const canonicalPath = remainder.slice(slashIndex) || "/";
let decoded;
try {
decoded = decodeURIComponent(encodedCapability);
} catch {
malformedScopedPath = true;
}
capabilityFromPath = normalizeCapability(decoded);
if (!capabilityFromPath || !canonicalPath.startsWith("/")) malformedScopedPath = true;
else {
url.pathname = canonicalPath;
url.searchParams.set(PLUGIN_NODE_CAPABILITY_QUERY_PARAM, capabilityFromPath);
rewrittenUrl = `${url.pathname}${url.search}`;
}
}
}
const capability = capabilityFromPath ?? normalizeCapability(url.searchParams.get(PLUGIN_NODE_CAPABILITY_QUERY_PARAM));
return {
pathname: url.pathname,
capability,
rewrittenUrl,
scopedPath,
malformedScopedPath
};
}
/** Store a minted capability on a client under the surface/scope storage key. */
function setClientPluginNodeCapability(params) {
const surface = normalizeSurface(params.surface.surface);
const storageKey = resolvePluginNodeCapabilityStorageKey(params.surface);
const expiresAtMs = asDateTimestampMs(params.expiresAtMs);
if (!surface || !storageKey || expiresAtMs === void 0) return;
params.client.pluginNodeCapabilities ??= {};
params.client.pluginNodeCapabilities[storageKey] = {
capability: params.capability,
expiresAtMs
};
}
function refreshClientPluginNodeCapability(params) {
const surface = normalizeSurface(params.surface.surface);
if (!surface) return;
const existingUrl = params.client.pluginSurfaceUrls?.[surface];
if (!existingUrl) return;
const capabilitySurface = params.client.pluginNodeCapabilitySurfaces?.[surface] ?? params.surface;
const capability = mintPluginNodeCapabilityToken();
const expiresAtMs = resolvePluginNodeCapabilityExpiresAtMs(capabilitySurface, params.nowMs ?? Date.now());
if (expiresAtMs === void 0) return;
const scopedUrl = replacePluginNodeCapabilityInScopedHostUrl(existingUrl, capability);
if (!scopedUrl) return;
params.client.pluginSurfaceUrls ??= {};
params.client.pluginSurfaceUrls[surface] = scopedUrl;
setClientPluginNodeCapability({
client: params.client,
surface: capabilitySurface,
capability,
expiresAtMs
});
return {
surface,
capability,
expiresAtMs,
scopedUrl
};
}
function hasAuthorizedPluginNodeCapability(params) {
const surface = normalizeSurface(params.surface.surface);
const storageKey = resolvePluginNodeCapabilityStorageKey(params.surface);
if (!surface || !storageKey) return false;
const nowMs = params.nowMs ?? Date.now();
const nextExpiresAtMs = resolvePluginNodeCapabilityExpiresAtMs(params.surface, nowMs);
if (nextExpiresAtMs === void 0) return false;
for (const client of params.clients) {
if (client.invalidated) continue;
const entry = client.pluginNodeCapabilities?.[storageKey];
if (!entry || !isFutureDateTimestampMs(entry.expiresAtMs, { nowMs })) continue;
if (safeEqualSecret(entry.capability, params.capability)) {
entry.expiresAtMs = nextExpiresAtMs;
return true;
}
}
return false;
}
//#endregion
export { hasAuthorizedPluginNodeCapability as a, normalizePluginNodeCapabilityScopedUrl as c, refreshClientPluginNodeCapability as d, resolvePluginNodeCapabilityExpiresAtMs as f, hasAuthorizedClientPluginNodeCapabilityUrl as i, pluginNodeCapabilityScopedHostUrlsConflict as l, setClientPluginNodeCapability as m, PLUGIN_NODE_CAPABILITY_PATH_PREFIX as n, indexPluginNodeCapabilitySurfaces as o, resolvePluginNodeCapabilityTtlMs as p, buildPluginNodeCapabilityScopedHostUrl as r, mintPluginNodeCapabilityToken as s, DEFAULT_PLUGIN_NODE_CAPABILITY_TTL_MS as t, reconcileClientPluginNodeCapabilities as u };