Spaces:
Running
Running
Download apps/api/src/integrations/webhook-idempotency.service.ts from Benaah/MicProtocol: direct link, hf CLI and curl.
- Browser
- Download file 3.05 kB
-
https://huggingface.co/spaces/Benaah/MicProtocol/resolve/main/apps/api/src/integrations/webhook-idempotency.service.ts
- Command line
-
hf download hf://spaces/Benaah/MicProtocol/apps/api/src/integrations/webhook-idempotency.service.ts
-
curl -L -o webhook-idempotency.service.ts https://huggingface.co/spaces/Benaah/MicProtocol/resolve/main/apps/api/src/integrations/webhook-idempotency.service.ts
3.05 kB
| import { Injectable, Logger } from "@nestjs/common"; | |
| import { ConfigService } from "@nestjs/config"; | |
| import { createClient } from "redis"; | |
| import type { RedisClientType } from "redis"; | |
| import * as crypto from "crypto"; | |
| () | |
| export class WebhookIdempotencyService { | |
| private readonly logger = new Logger(WebhookIdempotencyService.name); | |
| private client: RedisClientType | null = null; | |
| private readonly ttlSeconds = 86400; // 24 hours | |
| constructor(private readonly configService: ConfigService) {} | |
| private async getClient(): Promise<RedisClientType> { | |
| if (this.client?.isOpen) return this.client; | |
| const redisUrl = this.configService.get<string>( | |
| "REDIS_URL", | |
| "redis://:redis_secret@localhost:6379" | |
| ); | |
| this.client = createClient({ url: redisUrl }); | |
| this.client.on("error", (err) => this.logger.error("Redis error", err)); | |
| await this.client.connect(); | |
| return this.client; | |
| } | |
| /** | |
| * Check if webhook event was already processed. | |
| * Returns true if this is a duplicate (already processed). | |
| * Uses SETNX for atomic check-and-set. | |
| */ | |
| async checkAndMark(eventKey: string): Promise<boolean> { | |
| const client = await this.getClient(); | |
| const key = `webhook:idempotency:${eventKey}`; | |
| // SET key value NX EX ttl - only sets if not exists | |
| const result = await client.set(key, "1", { | |
| NX: true, | |
| EX: this.ttlSeconds, | |
| }); | |
| // result is "OK" if set (first time), null if already exists (duplicate) | |
| return result === null; | |
| } | |
| /** | |
| * Generate a unique event key from provider and webhook payload | |
| */ | |
| generateEventKey(provider: string, body: any): string { | |
| const eventId = this.generateEventId(provider, body); | |
| return `${provider}:${eventId}`; | |
| } | |
| /** | |
| * Generate a unique event ID from webhook payload | |
| */ | |
| generateEventId(provider: string, body: any): string { | |
| switch (provider) { | |
| case "calcom": | |
| // Cal.com uses booking UID or event ID | |
| return body?.payload?.uid || body?.uid || body?.bookingId || | |
| body?.payload?.id || cryptoRandomId(); | |
| case "chatwoot": | |
| // Chatwoot uses conversation ID + message ID | |
| return `${body?.conversation?.id || "unknown"}:${body?.id || body?.message?.id || cryptoRandomId()}`; | |
| case "formbricks": | |
| // Formbricks uses response ID | |
| return body?.responseId || body?.data?.id || body?.id || cryptoRandomId(); | |
| case "listmonk": | |
| // Listmonk uses subscriber ID + event type | |
| const event = body?.event || "unknown"; | |
| const subId = body?.data?.subscriber?.id || body?.subscriber_id || "unknown"; | |
| return `${event}:${subId}`; | |
| case "documenso": | |
| return body?.documentId || body?.data?.id || cryptoRandomId(); | |
| case "livekit": | |
| return `${body?.event || "unknown"}:${body?.room?.name || body?.roomName || cryptoRandomId()}`; | |
| default: | |
| return cryptoRandomId(); | |
| } | |
| } | |
| } | |
| function cryptoRandomId(): string { | |
| return crypto.randomBytes(16).toString("hex"); | |
| } |