mirror of https://github.com/openclaw/openclaw.git
fix(zalo): scope webhook replay dedupe per target (#58196)
This commit is contained in:
parent
57fccca2dc
commit
4d038bb242
|
|
@ -130,6 +130,7 @@ Docs: https://docs.openclaw.ai
|
|||
- Diffs/config: preserve schema-shaped plugin config parsing from `diffsPluginConfigSchema.safeParse()`, so direct callers keep `defaults` and `security` sections instead of receiving flattened tool defaults. (#57904) Thanks @gumadeiras.
|
||||
- Diffs: fall back to plain text when `lang` hints are invalid during diff render and viewer hydration, so bad or stale language values no longer break the diff viewer. (#57902) Thanks @gumadeiras.
|
||||
- Doctor/plugins: skip false Matrix legacy-helper warnings when no migration plans exist, and keep bundled `enabledByDefault` plugins in the gateway startup set. (#57931) Thanks @dinakars777.
|
||||
- Zalo/webhooks: scope replay dedupe to the authenticated target so one configured account can no longer cause same-id inbound events for another target to be dropped. Thanks @smaeljaish771 and @vincentkoc.
|
||||
- Matrix/CLI send: start one-off Matrix send clients before outbound delivery so `openclaw message send --channel matrix` restores E2EE in encrypted rooms instead of sending plain events. (#57936) Thanks @gumadeiras.
|
||||
- xAI/Responses: normalize image-bearing tool results for xAI responses payloads, including OpenResponses-style `input_image.source` parts, so image tool replays no longer 422 on the follow-up turn. (#58017) Thanks @neeravmakwana.
|
||||
- Cron/isolated sessions: carry the full live-session provider, model, and auth-profile selection across retry restarts so cron jobs with model overrides no longer fail or loop on mid-run model-switch requests. (#57972) Thanks @issaba1.
|
||||
|
|
|
|||
|
|
@ -238,6 +238,62 @@ describe("handleZaloWebhookRequest", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("keeps replay dedupe isolated per authenticated target", async () => {
|
||||
const sinkA = vi.fn();
|
||||
const sinkB = vi.fn();
|
||||
const unregisterA = registerTarget({
|
||||
path: "/hook-replay-scope",
|
||||
secret: "secret-a",
|
||||
statusSink: sinkA,
|
||||
});
|
||||
const unregisterB = registerTarget({
|
||||
path: "/hook-replay-scope",
|
||||
secret: "secret-b",
|
||||
statusSink: sinkB,
|
||||
account: {
|
||||
...DEFAULT_ACCOUNT,
|
||||
accountId: "work",
|
||||
},
|
||||
});
|
||||
const payload = createTextUpdate({
|
||||
messageId: "msg-replay-scope-1",
|
||||
userId: "123",
|
||||
userName: "",
|
||||
chatId: "123",
|
||||
text: "hello",
|
||||
});
|
||||
|
||||
try {
|
||||
await withServer(webhookRequestHandler, async (baseUrl) => {
|
||||
const first = await fetch(`${baseUrl}/hook-replay-scope`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"x-bot-api-secret-token": "secret-a",
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const second = await fetch(`${baseUrl}/hook-replay-scope`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"x-bot-api-secret-token": "secret-b",
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
expect(first.status).toBe(200);
|
||||
expect(second.status).toBe(200);
|
||||
});
|
||||
|
||||
expect(sinkA).toHaveBeenCalledTimes(1);
|
||||
expect(sinkB).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
unregisterA();
|
||||
unregisterB();
|
||||
}
|
||||
});
|
||||
|
||||
it("downloads inbound image media from webhook photo_url and preserves display_name", async () => {
|
||||
const {
|
||||
core,
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ const webhookAnomalyTracker = createWebhookAnomalyTracker({
|
|||
|
||||
export function clearZaloWebhookSecurityStateForTest(): void {
|
||||
webhookRateLimiter.clear();
|
||||
recentWebhookEvents.clear();
|
||||
webhookAnomalyTracker.clear();
|
||||
}
|
||||
|
||||
|
|
@ -87,12 +88,12 @@ function timingSafeEquals(left: string, right: string): boolean {
|
|||
return timingSafeEqual(leftBuffer, rightBuffer);
|
||||
}
|
||||
|
||||
function isReplayEvent(update: ZaloUpdate, nowMs: number): boolean {
|
||||
function isReplayEvent(target: ZaloWebhookTarget, update: ZaloUpdate, nowMs: number): boolean {
|
||||
const messageId = update.message?.message_id;
|
||||
if (!messageId) {
|
||||
return false;
|
||||
}
|
||||
const key = `${update.event_name}:${messageId}`;
|
||||
const key = `${target.path}:${target.account.accountId}:${update.event_name}:${messageId}`;
|
||||
return recentWebhookEvents.check(key, nowMs);
|
||||
}
|
||||
|
||||
|
|
@ -222,7 +223,7 @@ export async function handleZaloWebhookRequest(
|
|||
return true;
|
||||
}
|
||||
|
||||
if (isReplayEvent(update, nowMs)) {
|
||||
if (isReplayEvent(target, update, nowMs)) {
|
||||
res.statusCode = 200;
|
||||
res.end("ok");
|
||||
return true;
|
||||
|
|
|
|||
Loading…
Reference in New Issue