From 73078946f1df7b5c224562ed728c972dfd068165 Mon Sep 17 00:00:00 2001 From: Chaim Marcus Date: Sun, 17 May 2026 10:52:12 +0000 Subject: [PATCH] feat: add idempotency guard to onWebhook Prevent duplicate comments on rapid Paperclip retries: - Check plugin instance state for requestId before processing - Skip if same requestId was seen within 5 minutes - Store requestId with ISO timestamp after first successful delivery Co-Authored-By: Claude Sonnet 4.6 --- src/worker.ts | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/worker.ts b/src/worker.ts index 5ac952b..f2ddbf1 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -831,8 +831,27 @@ const plugin = definePlugin({ async onWebhook(input: PluginWebhookInput): Promise { if (!pluginCtx) return; // not yet initialized - // TODO: add idempotency guard using input.requestId to prevent duplicate - // comments on rapid retries (store requestId in plugin state with ~60s TTL) + // Idempotency guard: skip duplicate deliveries within 5 minutes + if (input.requestId) { + const idempKey = `webhook-idem-${input.requestId}`; + const seenAt = await pluginCtx.state.get({ + scopeKind: "instance", + stateKey: idempKey, + }); + if (seenAt && typeof seenAt === "string") { + const ageMs = Date.now() - new Date(seenAt).getTime(); + if (ageMs < 5 * 60 * 1000) { + pluginCtx.logger.info( + `onWebhook: skipping duplicate requestId ${input.requestId} (age ${Math.round(ageMs / 1000)}s)`, + ); + return; + } + } + await pluginCtx.state.set( + { scopeKind: "instance", stateKey: idempKey }, + new Date().toISOString(), + ); + } const { endpointKey, parsedBody } = input;