refactor(gateway): harden plugin http route contracts

This commit is contained in:
Peter Steinberger
2026-03-02 16:47:51 +00:00
parent 33e76db12a
commit 7a7eee920a
23 changed files with 642 additions and 270 deletions

View File

@@ -4,8 +4,7 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk";
import {
isRequestBodyLimitError,
readRequestBodyWithLimit,
registerPluginHttpRoute,
registerWebhookTarget,
registerWebhookTargetWithPluginRoute,
rejectNonPostWebhookRequest,
requestBodyErrorToText,
resolveSingleWebhookTarget,
@@ -236,23 +235,25 @@ function removeDebouncer(target: WebhookTarget): void {
}
export function registerBlueBubblesWebhookTarget(target: WebhookTarget): () => void {
const registered = registerWebhookTarget(webhookTargets, target, {
onFirstPathTarget: ({ path }) =>
registerPluginHttpRoute({
path,
pluginId: "bluebubbles",
source: "bluebubbles-webhook",
accountId: target.account.accountId,
log: target.runtime.log,
handler: async (req, res) => {
const handled = await handleBlueBubblesWebhookRequest(req, res);
if (!handled && !res.headersSent) {
res.statusCode = 404;
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end("Not Found");
}
},
}),
const registered = registerWebhookTargetWithPluginRoute({
targetsByPath: webhookTargets,
target,
route: {
auth: "plugin",
match: "exact",
pluginId: "bluebubbles",
source: "bluebubbles-webhook",
accountId: target.account.accountId,
log: target.runtime.log,
handler: async (req, res) => {
const handled = await handleBlueBubblesWebhookRequest(req, res);
if (!handled && !res.headersSent) {
res.statusCode = 404;
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end("Not Found");
}
},
},
});
return () => {
registered.unregister();
@@ -530,20 +531,10 @@ export async function monitorBlueBubblesProvider(
path,
statusSink,
});
const unregisterRoute = registerPluginHttpRoute({
path,
auth: "plugin",
match: "exact",
pluginId: "bluebubbles",
accountId: account.accountId,
log: (message) => logVerbose(core, runtime, message),
handler: handleBlueBubblesWebhookRequest,
});
return await new Promise((resolve) => {
const stop = () => {
unregister();
unregisterRoute();
resolve();
};

View File

@@ -5,9 +5,7 @@ import {
createScopedPairingAccess,
createReplyPrefixOptions,
readJsonBodyWithLimit,
registerPluginHttpRoute,
registerWebhookTarget,
registerPluginHttpRoute,
registerWebhookTargetWithPluginRoute,
rejectNonPostWebhookRequest,
isDangerousNameMatchingEnabled,
resolveAllowlistProviderRuntimeGroupPolicy,
@@ -102,23 +100,25 @@ function warnDeprecatedUsersEmailEntries(
}
export function registerGoogleChatWebhookTarget(target: WebhookTarget): () => void {
return registerWebhookTarget(webhookTargets, target, {
onFirstPathTarget: ({ path }) =>
registerPluginHttpRoute({
path,
pluginId: "googlechat",
source: "googlechat-webhook",
accountId: target.account.accountId,
log: target.runtime.log,
handler: async (req, res) => {
const handled = await handleGoogleChatWebhookRequest(req, res);
if (!handled && !res.headersSent) {
res.statusCode = 404;
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end("Not Found");
}
},
}),
return registerWebhookTargetWithPluginRoute({
targetsByPath: webhookTargets,
target,
route: {
auth: "plugin",
match: "exact",
pluginId: "googlechat",
source: "googlechat-webhook",
accountId: target.account.accountId,
log: target.runtime.log,
handler: async (req, res) => {
const handled = await handleGoogleChatWebhookRequest(req, res);
if (!handled && !res.headersSent) {
res.statusCode = 404;
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end("Not Found");
}
},
},
}).unregister;
}
@@ -981,19 +981,9 @@ export function monitorGoogleChatProvider(options: GoogleChatMonitorOptions): ()
statusSink: options.statusSink,
mediaMaxMb,
});
const unregisterRoute = registerPluginHttpRoute({
path: webhookPath,
auth: "plugin",
match: "exact",
pluginId: "googlechat",
accountId: options.account.accountId,
log: (message) => logVerbose(core, options.runtime, message),
handler: handleGoogleChatWebhookRequest,
});
return () => {
unregisterTarget();
unregisterRoute();
};
}

View File

@@ -33,7 +33,6 @@ function createApi(params: {
logger: { info() {}, warn() {}, error() {} },
registerTool() {},
registerHook() {},
registerHttpHandler() {},
registerHttpRoute() {},
registerChannel() {},
registerGatewayMethod() {},

View File

@@ -295,6 +295,8 @@ export function createSynologyChatPlugin() {
const unregister = registerPluginHttpRoute({
path: account.webhookPath,
auth: "plugin",
replaceExisting: true,
pluginId: CHANNEL_ID,
accountId: account.accountId,
log: (msg: string) => log?.info?.(msg),

View File

@@ -3,7 +3,6 @@ import type { MarkdownTableMode, OpenClawConfig, OutboundReplyPayload } from "op
import {
createScopedPairingAccess,
createReplyPrefixOptions,
registerPluginHttpRoute,
resolveDirectDmAuthorizationOutcome,
resolveSenderCommandAuthorizationWithRuntime,
resolveOutboundMediaUrls,
@@ -77,22 +76,22 @@ function logVerbose(core: ZaloCoreRuntime, runtime: ZaloRuntimeEnv, message: str
export function registerZaloWebhookTarget(target: ZaloWebhookTarget): () => void {
return registerZaloWebhookTargetInternal(target, {
onFirstPathTarget: ({ path }) =>
registerPluginHttpRoute({
path,
pluginId: "zalo",
source: "zalo-webhook",
accountId: target.account.accountId,
log: target.runtime.log,
handler: async (req, res) => {
const handled = await handleZaloWebhookRequest(req, res);
if (!handled && !res.headersSent) {
res.statusCode = 404;
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end("Not Found");
}
},
}),
route: {
auth: "plugin",
match: "exact",
pluginId: "zalo",
source: "zalo-webhook",
accountId: target.account.accountId,
log: target.runtime.log,
handler: async (req, res) => {
const handled = await handleZaloWebhookRequest(req, res);
if (!handled && !res.headersSent) {
res.statusCode = 404;
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end("Not Found");
}
},
},
});
}
@@ -653,17 +652,7 @@ export async function monitorZaloProvider(options: ZaloMonitorOptions): Promise<
mediaMaxMb: effectiveMediaMaxMb,
fetcher,
});
const unregisterRoute = registerPluginHttpRoute({
path,
auth: "plugin",
match: "exact",
pluginId: "zalo",
accountId: account.accountId,
log: (message) => logVerbose(core, runtime, message),
handler: handleZaloWebhookRequest,
});
stopHandlers.push(unregister);
stopHandlers.push(unregisterRoute);
abortSignal.addEventListener(
"abort",
() => {

View File

@@ -7,7 +7,9 @@ import {
createWebhookAnomalyTracker,
readJsonWebhookBodyOrReject,
applyBasicWebhookRequestGuards,
registerWebhookTargetWithPluginRoute,
type RegisterWebhookTargetOptions,
type RegisterWebhookPluginRouteOptions,
registerWebhookTarget,
resolveSingleWebhookTarget,
resolveWebhookTargets,
@@ -109,11 +111,21 @@ function recordWebhookStatus(
export function registerZaloWebhookTarget(
target: ZaloWebhookTarget,
opts?: Pick<
opts?: {
route?: RegisterWebhookPluginRouteOptions;
} & Pick<
RegisterWebhookTargetOptions<ZaloWebhookTarget>,
"onFirstPathTarget" | "onLastPathTargetRemoved"
>,
): () => void {
if (opts?.route) {
return registerWebhookTargetWithPluginRoute({
targetsByPath: webhookTargets,
target,
route: opts.route,
onLastPathTargetRemoved: opts.onLastPathTargetRemoved,
}).unregister;
}
return registerWebhookTarget(webhookTargets, target, opts).unregister;
}