openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
181 lines (180 loc) • 6.55 kB
JavaScript
import { t as safeEqualSecret } from "./secret-equal-DRsL8lKD.js";
import { f as resolveClientIp } from "./net-DTe7AQiu.js";
import "./security-runtime-CQm7DD1u.js";
import { t as createClaimableDedupe } from "./persistent-dedupe-tc3aSkBu.js";
import { r as applyBasicWebhookRequestGuards, s as readJsonWebhookBodyOrReject } from "./webhook-request-guards-gS2khsvD.js";
import { a as createFixedWindowRateLimiter, o as createWebhookAnomalyTracker, r as WEBHOOK_RATE_LIMIT_DEFAULTS, t as WEBHOOK_ANOMALY_COUNTER_DEFAULTS } from "./webhook-ingress-Dgd3Jtks.js";
import { l as withResolvedWebhookRequestPipeline, n as registerWebhookTargetWithPluginRoute, s as resolveWebhookTargetWithAuthOrRejectSync, t as registerWebhookTarget } from "./webhook-targets-Beqkt_FW.js";
import "./runtime-api-B-NWjRfH.js";
//#region extensions/zalo/src/monitor.webhook.ts
const ZALO_WEBHOOK_REPLAY_WINDOW_MS = 5 * 6e4;
const webhookTargets = /* @__PURE__ */ new Map();
const webhookRateLimiter = createFixedWindowRateLimiter({
windowMs: WEBHOOK_RATE_LIMIT_DEFAULTS.windowMs,
maxRequests: WEBHOOK_RATE_LIMIT_DEFAULTS.maxRequests,
maxTrackedKeys: WEBHOOK_RATE_LIMIT_DEFAULTS.maxTrackedKeys
});
const recentWebhookEvents = createClaimableDedupe({
ttlMs: ZALO_WEBHOOK_REPLAY_WINDOW_MS,
memoryMaxSize: 5e3
});
const webhookAnomalyTracker = createWebhookAnomalyTracker({
maxTrackedKeys: WEBHOOK_ANOMALY_COUNTER_DEFAULTS.maxTrackedKeys,
ttlMs: WEBHOOK_ANOMALY_COUNTER_DEFAULTS.ttlMs,
logEvery: WEBHOOK_ANOMALY_COUNTER_DEFAULTS.logEvery
});
function clearZaloWebhookSecurityStateForTest() {
webhookRateLimiter.clear();
recentWebhookEvents.clearMemory();
webhookAnomalyTracker.clear();
}
function getZaloWebhookRateLimitStateSizeForTest() {
return webhookRateLimiter.size();
}
function getZaloWebhookStatusCounterSizeForTest() {
return webhookAnomalyTracker.size();
}
function timingSafeEquals(left, right) {
return safeEqualSecret(left, right);
}
function buildReplayEventCacheKey(target, update) {
const messageId = update.message?.message_id;
if (!messageId) return null;
const chatId = update.message?.chat?.id ?? "";
const senderId = update.message?.from?.id ?? "";
return JSON.stringify([
target.path,
target.account.accountId,
update.event_name,
chatId,
senderId,
messageId
]);
}
var ZaloRetryableWebhookError = class extends Error {
constructor(message, options) {
super(message, options);
this.name = "ZaloRetryableWebhookError";
}
};
async function processZaloReplayGuardedUpdate(params) {
const replayEventKey = buildReplayEventCacheKey(params.target, params.update);
if (replayEventKey) {
if ((await recentWebhookEvents.claim(replayEventKey, { now: params.nowMs })).kind !== "claimed") return "duplicate";
}
params.target.statusSink?.({ lastInboundAt: Date.now() });
try {
await params.processUpdate({
update: params.update,
target: params.target
});
if (replayEventKey) await recentWebhookEvents.commit(replayEventKey);
return "processed";
} catch (error) {
if (replayEventKey) if (error instanceof ZaloRetryableWebhookError) recentWebhookEvents.release(replayEventKey, { error });
else await recentWebhookEvents.commit(replayEventKey);
throw error;
}
}
function recordWebhookStatus(runtime, path, statusCode) {
webhookAnomalyTracker.record({
key: `${path}:${statusCode}`,
statusCode,
log: runtime?.log,
message: (count) => `[zalo] webhook anomaly path=${path} status=${statusCode} count=${String(count)}`
});
}
function headerValue(value) {
return Array.isArray(value) ? value[0] : value;
}
function registerZaloWebhookTarget(target, opts) {
if (opts?.route) return registerWebhookTargetWithPluginRoute({
targetsByPath: webhookTargets,
target,
route: opts.route,
onLastPathTargetRemoved: opts.onLastPathTargetRemoved
}).unregister;
return registerWebhookTarget(webhookTargets, target, opts).unregister;
}
async function handleZaloWebhookRequest(req, res, processUpdate) {
return await withResolvedWebhookRequestPipeline({
req,
res,
targetsByPath: webhookTargets,
allowMethods: ["POST"],
handle: async ({ targets, path }) => {
const trustedProxies = targets[0]?.config.gateway?.trustedProxies;
const allowRealIpFallback = targets[0]?.config.gateway?.allowRealIpFallback === true;
const rateLimitKey = `${path}:${resolveClientIp({
remoteAddr: req.socket.remoteAddress,
forwardedFor: headerValue(req.headers["x-forwarded-for"]),
realIp: headerValue(req.headers["x-real-ip"]),
trustedProxies,
allowRealIpFallback
}) ?? req.socket.remoteAddress ?? "unknown"}`;
const nowMs = Date.now();
if (!applyBasicWebhookRequestGuards({
req,
res,
rateLimiter: webhookRateLimiter,
rateLimitKey,
nowMs
})) {
recordWebhookStatus(targets[0]?.runtime, path, res.statusCode);
return true;
}
const headerToken = String(req.headers["x-bot-api-secret-token"] ?? "");
const target = resolveWebhookTargetWithAuthOrRejectSync({
targets,
res,
isMatch: (entry) => timingSafeEquals(entry.secret, headerToken)
});
if (!target) {
recordWebhookStatus(targets[0]?.runtime, path, res.statusCode);
return true;
}
if (!applyBasicWebhookRequestGuards({
req,
res,
requireJsonContentType: true
})) {
recordWebhookStatus(target.runtime, path, res.statusCode);
return true;
}
const body = await readJsonWebhookBodyOrReject({
req,
res,
maxBytes: 1024 * 1024,
timeoutMs: 3e4,
emptyObjectOnEmpty: false,
invalidJsonMessage: "Bad Request"
});
if (!body.ok) {
recordWebhookStatus(target.runtime, path, res.statusCode);
return true;
}
const raw = body.value;
const record = raw && typeof raw === "object" ? raw : null;
const update = record && record.ok === true && record.result ? record.result : record ?? void 0;
if (!update?.event_name) {
res.statusCode = 400;
res.end("Bad Request");
recordWebhookStatus(target.runtime, path, res.statusCode);
return true;
}
processZaloReplayGuardedUpdate({
target,
update,
processUpdate,
nowMs
}).catch((err) => {
target.runtime.error?.(`[${target.account.accountId}] Zalo webhook failed: ${String(err)}`);
});
res.statusCode = 200;
res.end("ok");
return true;
}
});
}
//#endregion
export { ZaloRetryableWebhookError, clearZaloWebhookSecurityStateForTest, getZaloWebhookRateLimitStateSizeForTest, getZaloWebhookStatusCounterSizeForTest, handleZaloWebhookRequest, processZaloReplayGuardedUpdate, registerZaloWebhookTarget };