mirror of
https://github.com/ReCloudStudio/WebHooker.git
synced 2026-09-23 00:21:28 +00:00
feat(reliability): idempotency store, custom webhook replay protection, correlation ids
Add LICENSE (MIT), an IdempotencyStore abstraction with a KV implementation and provider-scoped delivery keys, optional replay protection for custom webhooks (X-WebHooker-Timestamp + X-WebHooker-Nonce), and per-request correlation ids in webhook responses and logs.
This commit is contained in:
parent
92ba2203a5
commit
737dd7af98
15 changed files with 378 additions and 26 deletions
7
server/lib/lib/correlation.ts
Normal file
7
server/lib/lib/correlation.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
/** A short, unique correlation id for tracing one webhook request. */
|
||||
export function newCorrelationId(): string {
|
||||
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
||||
}
|
||||
39
server/lib/lib/idempotency.ts
Normal file
39
server/lib/lib/idempotency.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
/**
|
||||
* Delivery idempotency: a small, reusable abstraction over "has this key been
|
||||
* seen / can I claim it once" so webhook dedup, nonce replay protection and
|
||||
* future delivery retry all share one semantics. `claim` is best-effort atomic
|
||||
* on KV (get-then-put): under a concurrent double-send both callers may see
|
||||
* "unclaimed", but that matches the existing dedup behavior and is acceptable
|
||||
* because dispatch itself is idempotent per (provider, group, delivery).
|
||||
*/
|
||||
export interface IdempotencyStore {
|
||||
has(key: string): Promise<boolean>;
|
||||
claim(key: string, ttlSeconds: number): Promise<boolean>;
|
||||
}
|
||||
|
||||
export function kvIdempotencyStore(kv: KVNamespace): IdempotencyStore {
|
||||
return {
|
||||
async has(key): Promise<boolean> {
|
||||
const value = await kv.get(key);
|
||||
return value !== null && value !== undefined;
|
||||
},
|
||||
async claim(key, ttlSeconds): Promise<boolean> {
|
||||
const value = await kv.get(key);
|
||||
if (value !== null && value !== undefined) return false;
|
||||
await kv.put(key, "1", { expirationTtl: ttlSeconds });
|
||||
return true;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Canonical dedup key for a webhook delivery: provider + tenant + delivery id.
|
||||
* The legacy global endpoint has no tenant, so `groupId` is "global".
|
||||
*/
|
||||
export function deliveryKey(
|
||||
provider: string,
|
||||
groupId: string | undefined,
|
||||
deliveryId: string,
|
||||
): string {
|
||||
return `delivery:${provider}:${groupId ?? "global"}:${deliveryId}`;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue