openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
195 lines (194 loc) • 8.76 kB
JavaScript
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 };