UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

254 lines (253 loc) • 11.7 kB
import { D as withPluginCache, a as createPluginCache } from "./plugin-cache-BABafcXD.mjs"; import { r as loadPluginManifest } from "./manifest-BXjFCp0F.mjs"; import { r as getPluginRegistryForContext } from "./gateway-request-scope-BSmCVVpP.mjs"; import { o as getRuntimeConfigSnapshot } from "./runtime-snapshot-DYptB0Cf.mjs"; import { i as readPluginControlUiAssets } from "./control-ui-assets-2Tfx8Ftx.mjs"; import { a as capturePluginLifecycleAuthority } from "./registry-lifecycle-CZIcWb1R.mjs"; import "./runtime-NsVZTERq.mjs"; import "./operator-scopes-DsrPoRc4.mjs"; import { r as authorizeOperatorScopesForRequiredScope } from "./method-scopes-BrMPjfvW.mjs"; import { o as CUSTOM_PLUGIN_UI_DISABLED_MESSAGE, s as isControlUiPluginAllowed } from "./control-ui-plugin-tabs-Cbijnt6c.mjs"; import { c as sendGatewayAuthFailure, d as sendMethodNotAllowed, x as respondNotFound } from "./http-common-BJjsxTZ6.mjs"; import { n as controlUiPluginAssetRoot, t as controlUiPluginAssetPrefix } from "./control-ui-plugin-assets-contract-BEmf5f0r.mjs"; import { p as authorizeControlUiPluginCookieRequest, t as authorizeControlUiReadRequestOrReply } from "./http-auth-utils-DUIIp4wk.mjs"; import { t as resolveSharedGatewaySessionGeneration } from "./ws-shared-generation-BeRsGF9T.mjs"; import { createHash } from "node:crypto"; //#region src/gateway/control-ui-plugin-assets.ts const MAX_CONTROL_UI_PLUGINS = 64; const MAX_CONTROL_UI_CATALOG_BYTES = 67108864; const MAX_CONTROL_UI_CATALOG_REVISIONS = 256; const browserPluginStates = /* @__PURE__ */ new WeakMap(); let pendingCatalogOperation; function getActiveBrowserRegistry() { const registry = getPluginRegistryForContext(); return registry && capturePluginLifecycleAuthority(registry)?.() ? registry : null; } function pluginState(registry, record) { let state = browserPluginStates.get(record); if (!state?.isCurrent()) { const isCurrent = capturePluginLifecycleAuthority(registry, record); if (!isCurrent?.()) throw new Error("plugin is no longer active"); state = { isCurrent, revisions: /* @__PURE__ */ new Map(), activations: /* @__PURE__ */ new WeakMap(), initialized: false }; browserPluginStates.set(record, state); } return state; } function browserOwners(registry) { return registry.plugins.filter((record) => record.enabled && record.status === "loaded" && record.controlUi).toSorted((left, right) => left.id.localeCompare(right.id)); } async function snapshotBrowserBuild(registry, record, declaration = record.controlUi, uiCapabilities) { const authority = capturePluginLifecycleAuthority(registry, record); const isCurrent = () => authority?.() === true && isControlUiPluginAllowed(record); if (!declaration || !record.rootDir || !isCurrent()) throw new Error("plugin is no longer active"); const { entryName, styles, assets, bytes } = await readPluginControlUiAssets(record.rootDir, declaration); const digest = createHash("sha256").update(JSON.stringify({ declaration, uiCapabilities })); for (const [name, asset] of [...assets].toSorted(([left], [right]) => left.localeCompare(right))) digest.update(`${name}\0${asset.body.length}\0`).update(asset.body); const revision = digest.digest("hex"); const basePath = getRuntimeConfigSnapshot()?.gateway?.controlUi?.basePath; const prefix = `${controlUiPluginAssetPrefix(record.id, basePath)}${revision}/`; const assetUrl = (name) => `${prefix}${name.split("/").map(encodeURIComponent).join("/")}`; if (!isCurrent()) throw new Error("plugin was replaced while its browser assets loaded"); return { isCurrent, assets, bytes, module: { pluginId: record.id, name: record.name, revision, entryUrl: assetUrl(entryName), styles: styles.map(assetUrl), ...uiCapabilities !== void 0 ? { uiCapabilities } : {} } }; } async function refreshBrowserCatalog(registry, isCurrent, pluginId, reloadManifest = false) { if (!isCurrent()) throw new Error("plugin registry is no longer active"); const owners = browserOwners(registry); if (owners.length > MAX_CONTROL_UI_PLUGINS) throw new Error(`Native Control UI supports at most ${MAX_CONTROL_UI_PLUGINS} active plugins`); if (pluginId && !owners.some((record) => record.id === pluginId)) throw new Error("No active Control UI entrypoint for this plugin"); for (const record of owners) { if (!isControlUiPluginAllowed(record)) { browserPluginStates.delete(record); continue; } const state = pluginState(registry, record); if (state.initialized && (!reloadManifest || pluginId && record.id !== pluginId)) continue; try { let declaration = record.controlUi; let uiCapabilities = record.uiCapabilities; if (reloadManifest) { const loaded = withPluginCache(createPluginCache(), () => loadPluginManifest(record.rootDir, true, record.rootDir)); if (!loaded.ok || loaded.manifest.id !== record.id || !loaded.manifest.controlUi) throw new Error("active plugin browser declaration is missing or invalid"); declaration = loaded.manifest.controlUi; uiCapabilities = loaded.manifest.uiCapabilities; } const build = await snapshotBrowserBuild(registry, record, declaration, uiCapabilities); if (!isCurrent()) throw new Error("plugin registry was replaced while its browser assets loaded"); const revisionKey = build.module.revision; if (!state.revisions.has(revisionKey)) { let bytes = build.bytes; let revisionCount = 0; for (const owner of owners) { const retained = browserPluginStates.get(owner); if (!isControlUiPluginAllowed(owner) || !retained?.isCurrent()) continue; revisionCount += retained.revisions.size; for (const retainedBuild of retained.revisions.values()) bytes += retainedBuild.bytes; } if (bytes > MAX_CONTROL_UI_CATALOG_BYTES || revisionCount >= MAX_CONTROL_UI_CATALOG_REVISIONS) { state.diagnostic = { pluginId: record.id, message: "Control UI revision cache is full. Reload this plugin's backend or restart the Gateway before loading another browser build." }; continue; } } state.revisions.set(revisionKey, build); state.build = build; delete state.diagnostic; } catch { if (!isCurrent()) throw new Error("plugin registry was replaced while its browser assets loaded"); state.diagnostic = { pluginId: record.id, message: "Control UI assets could not be loaded. Build the plugin and reload its UI." }; } finally { if (isCurrent()) state.initialized = true; } } } function projectBrowserCatalog(registry) { const active = registry ? browserOwners(registry) : []; const plugins = active.flatMap((record) => { const build = browserPluginStates.get(record)?.build; return build?.isCurrent() ? [build.module] : []; }); const diagnostics = active.flatMap((record) => { if (!isControlUiPluginAllowed(record)) return [{ pluginId: record.id, code: "custom-plugin-ui-disabled", message: CUSTOM_PLUGIN_UI_DISABLED_MESSAGE }]; const diagnostic = browserPluginStates.get(record)?.diagnostic; return diagnostic ? [diagnostic] : []; }); return { revision: createHash("sha256").update(JSON.stringify({ plugins, diagnostics })).digest("hex"), plugins, diagnostics }; } async function loadControlUiPluginCatalog(pluginId, reloadManifest = false) { const registry = getPluginRegistryForContext(); if (!registry) { if (pluginId) throw new Error("No active Control UI entrypoint for this plugin"); return projectBrowserCatalog(null); } const isCurrent = capturePluginLifecycleAuthority(registry); if (!isCurrent?.()) throw new Error("plugin registry is no longer active"); const operation = (pendingCatalogOperation ?? Promise.resolve()).then(async () => { await refreshBrowserCatalog(registry, isCurrent, pluginId, reloadManifest); if (!isCurrent()) throw new Error("plugin registry is no longer active"); return projectBrowserCatalog(registry); }); const pending = operation.then(() => void 0, () => void 0); pendingCatalogOperation = pending; try { return await operation; } finally { if (pendingCatalogOperation === pending) pendingCatalogOperation = void 0; } } /** Explicit UI-only refresh; never imports or replaces backend plugin code. */ function reloadControlUiPluginCatalog(pluginId) { return loadControlUiPluginCatalog(pluginId, true); } function listControlUiPluginCatalog() { return loadControlUiPluginCatalog(); } /** A receipt is an observation by one live browser connection, never activation authority. */ function reportControlUiPluginActivation(client, report) { const owner = getActiveBrowserRegistry()?.plugins.find((record) => record.id === report.pluginId); const state = owner && browserPluginStates.get(owner); const build = state?.build; if (!state || !build?.isCurrent() || build.module.revision !== report.revision) return false; state.activations.set(client, { ...report }); return true; } function listControlUiPluginActivations(client, pluginId) { const registry = getActiveBrowserRegistry(); return (registry ? browserOwners(registry) : []).flatMap((record) => { const state = browserPluginStates.get(record); const report = state?.activations.get(client); return report && (!pluginId || pluginId === record.id) && state?.build?.isCurrent() && state.build.module.revision === report.revision ? [report] : []; }); } /** Serves only snapshot bytes after scoped plugin-cookie or explicit read authentication. */ async function handleControlUiPluginAssetRequest(req, res, params) { const pathname = new URL(req.url ?? "/", "http://localhost").pathname; const assetRoot = controlUiPluginAssetRoot(params.basePath); if (!pathname.startsWith(assetRoot)) return false; if (req.method !== "GET" && req.method !== "HEAD") { sendMethodNotAllowed(res, "GET, HEAD"); return true; } let segments; try { segments = pathname.slice(assetRoot.length).split("/").map(decodeURIComponent); } catch { respondNotFound(res); return true; } const [pluginId, revision = "", ...fileParts] = segments; if (!pluginId || !/^[a-f0-9]{64}$/u.test(revision) || fileParts.length === 0 || fileParts.some((part) => !part || part === "." || part === ".." || /[\\/\0]/u.test(part))) { respondNotFound(res); return true; } const cookieAuth = authorizeControlUiPluginCookieRequest(req, { requestPath: pathname, authGeneration: resolveSharedGatewaySessionGeneration(params.auth, params.trustedProxies), res }); if (res.writableEnded || res.destroyed) return true; if (cookieAuth) { if (!cookieAuth.requestAuth.controlUiPluginGrants?.find((candidate) => candidate.pluginId === pluginId && authorizeOperatorScopesForRequiredScope("operator.read", candidate.scopes).allowed)) { sendGatewayAuthFailure(res, { ok: false, reason: "unauthorized" }); return true; } } else if (!await authorizeControlUiReadRequestOrReply({ req, res, ...params })) return true; const owner = getActiveBrowserRegistry()?.plugins.find((record) => record.id === pluginId); const build = owner && browserPluginStates.get(owner)?.revisions.get(revision); const asset = build && build.module.revision === revision && build.isCurrent() ? build.assets.get(fileParts.join("/")) : void 0; if (!asset) { respondNotFound(res); return true; } res.statusCode = 200; res.setHeader("Content-Type", asset.contentType); res.setHeader("Content-Length", asset.body.length); res.setHeader("Cache-Control", "private, no-cache"); res.setHeader("X-Content-Type-Options", "nosniff"); res.end(req.method === "HEAD" ? void 0 : asset.body); return true; } //#endregion export { reportControlUiPluginActivation as a, reloadControlUiPluginCatalog as i, listControlUiPluginActivations as n, listControlUiPluginCatalog as r, handleControlUiPluginAssetRequest as t };