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 <noreply@anthropic.com>
This commit is contained in:
@@ -831,8 +831,27 @@ const plugin = definePlugin({
|
|||||||
async onWebhook(input: PluginWebhookInput): Promise<void> {
|
async onWebhook(input: PluginWebhookInput): Promise<void> {
|
||||||
if (!pluginCtx) return; // not yet initialized
|
if (!pluginCtx) return; // not yet initialized
|
||||||
|
|
||||||
// TODO: add idempotency guard using input.requestId to prevent duplicate
|
// Idempotency guard: skip duplicate deliveries within 5 minutes
|
||||||
// comments on rapid retries (store requestId in plugin state with ~60s TTL)
|
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;
|
const { endpointKey, parsedBody } = input;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user