UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

195 lines (194 loc) 8.76 kB
import { l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js"; import { S as requireActivePluginHttpRouteRegistry } from "./runtime-BL4wZfTq.js"; import { n as normalizePluginHttpPath, t as findPluginHttpRouteRegistrationConflicts } from "./http-route-overlap-Ck15uOqk.js"; import { AsyncLocalStorage } from "node:async_hooks"; //#region src/plugins/http-registry.ts const pluginHttpRouteRegistryScope = new AsyncLocalStorage(); const routeOwners = /* @__PURE__ */ new WeakMap(); const leasedRoutes = /* @__PURE__ */ new WeakMap(); const noopUnregister = () => {}; function removeOwnedRoute(owner, successor) { const index = owner.routes.indexOf(owner.entry); if (index >= 0) owner.routes.splice(index, 1); for (const handoff of owner.handoffs) { handoff.delete(owner); if (successor) { handoff.add(successor); successor.handoffs.add(handoff); } } owner.handoffs.clear(); routeOwners.delete(owner.entry); } function retireUnheldRoute(owner) { if (owner.holders.size > 0) return; const index = owner.routes.indexOf(owner.entry); if (owner.handoffs.size === 0 || index < 0) removeOwnedRoute(owner); else if (!owner.entry.handoff) { const previous = owner.entry; owner.entry = { ...previous, handoff: true, handleUpgrade: void 0, handler: (_req, res) => { res.statusCode = 503; res.setHeader("Retry-After", "1"); res.setHeader("Content-Type", "text/plain; charset=utf-8"); res.end("plugin route is restarting; retry"); return true; } }; owner.routes.splice(index, 1, owner.entry); routeOwners.delete(previous); routeOwners.set(owner.entry, owner); } } /** Keep retired ingress retryable until a successor claims it or every replacement ends. */ function createPluginHttpRouteHandoff() { const routes = /* @__PURE__ */ new Set(); return { park(lease) { for (const { owner } of leasedRoutes.get(lease) ?? []) if (owner.routes.includes(owner.entry)) { routes.add(owner); owner.handoffs.add(routes); } }, release() { for (const owner of routes) { owner.handoffs.delete(routes); retireUnheldRoute(owner); } routes.clear(); } }; } function hasSameRouteOwner(left, right) { return left.auth === right.auth && normalizeOptionalString(left.pluginId) === normalizeOptionalString(right.pluginId) && normalizeOptionalString(left.source) === normalizeOptionalString(right.source); } function adoptPluginHttpRouteHandoffs(previous, next) { if (previous === next) return; const transfers = previous.httpRoutes.flatMap((entry) => { const owner = routeOwners.get(entry); if (!owner || !entry.handoff) return []; const conflicts = findPluginHttpRouteRegistrationConflicts(next.httpRoutes, entry); if (conflicts.authOverlap || conflicts.canonicalMatches.some((route) => !hasSameRouteOwner(route, entry))) throw new Error(`plugin reload cannot replace HTTP route ownership at ${entry.path}`); return [{ owner, replacement: conflicts.canonicalMatches[0] }]; }); for (const { owner, replacement } of transfers) if (replacement) removeOwnedRoute(owner, routeOwners.get(replacement)); else { previous.httpRoutes.splice(previous.httpRoutes.indexOf(owner.entry), 1); next.httpRoutes.push(owner.entry); owner.routes = next.httpRoutes; } } function retainPluginHttpRoute(params) { const owner = routeOwners.get(params.entry); if (!owner) return noopUnregister; const retention = { owner }; const leaseReleases = []; const release = () => { if (!owner.holders.delete(release)) return; for (const lease of params.leases) leasedRoutes.get(lease)?.delete(retention); for (const releaseLease of leaseReleases.splice(0)) releaseLease(); retireUnheldRoute(owner); }; owner.holders.add(release); for (const lease of params.leases) { let retentions = leasedRoutes.get(lease); if (!retentions) { retentions = /* @__PURE__ */ new Set(); leasedRoutes.set(lease, retentions); } retentions.add(retention); leaseReleases.push(lease.retain(release)); } return release; } function withPluginHttpRouteRegistry(registry, run, lease) { const inherited = pluginHttpRouteRegistryScope.getStore()?.leases ?? []; const leases = lease && !inherited.includes(lease) ? [...inherited, lease] : inherited; return pluginHttpRouteRegistryScope.run({ registry, leases }, run); } function registerPluginHttpRoute(params) { const scope = pluginHttpRouteRegistryScope.getStore(); const registry = params.registry ?? scope?.registry ?? requireActivePluginHttpRouteRegistry(); const suffix = params.accountId ? ` for account "${params.accountId}"` : ""; const rejectRegistration = (message) => { params.log?.(message); if (params.throwOnFailure) throw new Error(message); return noopUnregister; }; if (scope?.leases.some((lease) => !lease.isActive())) return rejectRegistration("plugin runtime HTTP route lease is no longer active"); const routes = registry.httpRoutes ?? []; registry.httpRoutes = routes; const normalizedPath = normalizePluginHttpPath(params.path, params.fallbackPath); if (!normalizedPath) return rejectRegistration(`plugin: webhook path missing${suffix}`); const routeMatch = params.match ?? "exact"; const candidate = { path: normalizedPath, match: routeMatch, auth: params.auth }; const { authOverlap, canonicalMatches } = findPluginHttpRouteRegistrationConflicts(routes, candidate); if (authOverlap) return rejectRegistration(`plugin: route overlap denied at ${normalizedPath} (${routeMatch}, ${params.auth})${suffix}; overlaps ${authOverlap.path} (${authOverlap.match}, ${authOverlap.auth}) owned by ${authOverlap.pluginId ?? "unknown-plugin"} (${authOverlap.source ?? "unknown-source"})`); const entry = { path: normalizedPath, handler: params.handler, auth: params.auth, match: routeMatch, ...params.gatewayRuntimeScopeSurface ? { gatewayRuntimeScopeSurface: params.gatewayRuntimeScopeSurface } : {}, pluginId: params.pluginId, source: params.source }; const successor = { entry, routes, holders: /* @__PURE__ */ new Set(), handoffs: /* @__PURE__ */ new Set() }; const existingIndex = canonicalMatches[0] ? routes.indexOf(canonicalMatches[0]) : -1; if (existingIndex >= 0) { const existing = routes[existingIndex]; if (!existing) return rejectRegistration(`plugin: route conflict at ${normalizedPath} (${routeMatch})${suffix}`); const requestedOwner = normalizeOptionalString(params.pluginId); const requestedSource = normalizeOptionalString(params.source); const mismatchedOwner = canonicalMatches.find((route) => !hasSameRouteOwner(route, params)); const replaceExisting = params.replaceExisting || !mismatchedOwner && canonicalMatches.every((route) => route.handoff); if (!replaceExisting && params.reuseExistingSameOwner) { if (requestedOwner !== void 0 && requestedSource !== void 0 && !mismatchedOwner) { params.log?.(`plugin: reusing existing webhook path ${normalizedPath} (${routeMatch}) (${requestedOwner}/${requestedSource})`); return retainPluginHttpRoute({ entry: existing, leases: scope?.leases ?? [] }); } const conflictingOwner = mismatchedOwner ?? existing; return rejectRegistration(`plugin: route reuse denied for ${normalizedPath} (${routeMatch})${suffix}; owned by ${conflictingOwner.pluginId ?? "unknown-plugin"} (${conflictingOwner.source ?? "unknown-source"})`); } if (!replaceExisting) return rejectRegistration(`plugin: route conflict at ${normalizedPath} (${routeMatch})${suffix}; owned by ${existing.pluginId ?? "unknown-plugin"} (${existing.source ?? "unknown-source"})`); const incompatibleReplacement = canonicalMatches.find((route) => normalizeOptionalString(route.pluginId) !== requestedOwner || requestedOwner !== void 0 && normalizeOptionalString(route.source) !== requestedSource); if (incompatibleReplacement) return rejectRegistration(`plugin: route replacement denied for ${normalizedPath} (${routeMatch})${suffix}; owned by ${incompatibleReplacement.pluginId ?? "unknown-plugin"} (${incompatibleReplacement.source ?? "unknown-source"})`); const pluginHint = params.pluginId ? ` (${params.pluginId})` : ""; params.log?.(`plugin: replacing stale webhook path ${normalizedPath} (${routeMatch})${suffix}${pluginHint}`); for (const route of canonicalMatches.toReversed()) { const owner = routeOwners.get(route); if (owner) removeOwnedRoute(owner, successor); const index = routes.indexOf(route); if (index >= 0) routes.splice(index, 1); } } routeOwners.set(entry, successor); routes.push(entry); return retainPluginHttpRoute({ entry, leases: scope?.leases ?? [] }); } //#endregion export { withPluginHttpRouteRegistry as i, createPluginHttpRouteHandoff as n, registerPluginHttpRoute as r, adoptPluginHttpRouteHandoffs as t };