@hookflo/tern 2.2.10 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -17,6 +17,8 @@ npm install @hookflo/tern
17
17
 
18
18
  Tern is a zero-dependency TypeScript framework for robust webhook verification across multiple platforms and algorithms.
19
19
 
20
+ **Runtime requirements:** Node.js 18+ (or any runtime with Web Crypto + Fetch APIs, such as Deno and Cloudflare Workers).
21
+
20
22
  <img width="1396" height="470" style="border-radius: 10px" alt="tern bird nature" src="https://github.com/user-attachments/assets/5f0da3e6-1aba-4f88-a9d7-9d8698845c39" />
21
23
 
22
24
  ## Features
@@ -24,7 +26,7 @@ Tern is a zero-dependency TypeScript framework for robust webhook verification a
24
26
  - **Algorithm Agnostic**: Decouples platform logic from signature verification — verify based on cryptographic algorithm, not hardcoded platform rules.
25
27
  Supports HMAC-SHA256, HMAC-SHA1, HMAC-SHA512, and custom algorithms
26
28
 
27
- - **Platform Specific**: Accurate implementations for **Stripe, GitHub, Supabase, Clerk**, and other platforms
29
+ - **Platform Specific**: Accurate implementations for **Stripe, GitHub, Clerk**, and other platforms
28
30
  - **Flexible Configuration**: Custom signature configurations for any webhook format
29
31
  - **Type Safe**: Full TypeScript support with comprehensive type definitions
30
32
  - **Framework Agnostic**: Works with Express.js, Next.js, Cloudflare Workers, and more
@@ -56,20 +58,16 @@ npm install @hookflo/tern
56
58
  ### Basic Usage
57
59
 
58
60
  ```typescript
59
- import { WebhookVerificationService, platformManager } from '@hookflo/tern';
60
-
61
- // Method 1: Using the service (recommended)
62
- const result = await WebhookVerificationService.verifyWithPlatformConfig(
63
- request,
64
- 'stripe',
65
- 'whsec_your_stripe_webhook_secret'
66
- );
61
+ import { WebhookVerificationService } from '@hookflo/tern';
67
62
 
68
- // Method 2: Using platform manager (for platform-specific operations)
69
- const stripeResult = await platformManager.verify(request, 'stripe', 'whsec_your_secret');
63
+ const result = await WebhookVerificationService.verify(request, {
64
+ platform: 'stripe',
65
+ secret: 'whsec_your_stripe_webhook_secret',
66
+ toleranceInSeconds: 300,
67
+ });
70
68
 
71
69
  if (result.isValid) {
72
- console.log('Webhook verified!', result.payload);
70
+ console.log('Webhook verified!', result.eventId, result.payload);
73
71
  } else {
74
72
  console.log('Verification failed:', result.error);
75
73
  }
@@ -147,21 +145,6 @@ console.log(result.payload);
147
145
  // }
148
146
  ```
149
147
 
150
- ### Platform-Specific Usage
151
-
152
- ```typescript
153
- import { platformManager } from '@hookflo/tern';
154
-
155
- // Run tests for a specific platform
156
- const testsPassed = await platformManager.runPlatformTests('stripe');
157
-
158
- // Get platform configuration
159
- const config = platformManager.getConfig('stripe');
160
-
161
- // Get platform documentation
162
- const docs = platformManager.getDocumentation('stripe');
163
- ```
164
-
165
148
  ### Platform-Specific Configurations
166
149
 
167
150
  ```typescript
@@ -213,20 +196,22 @@ const result = await WebhookVerificationService.verify(request, stripeConfig);
213
196
  - **Paddle**: HMAC-SHA256 OK Tested
214
197
  - **Razorpay**: HMAC-SHA256 Pending
215
198
  - **Lemon Squeezy**: HMAC-SHA256 OK Tested
216
- - **Auth0**: HMAC-SHA256 Pending
217
199
  - **WorkOS**: HMAC-SHA256 (`workos-signature`, `t/v1`) OK Tested
218
200
  - **WooCommerce**: HMAC-SHA256 (base64 signature) Pending
219
201
  - **ReplicateAI**: HMAC-SHA256 (Standard Webhooks style) OK Tested
202
+ - **Sentry**: HMAC-SHA256 (`sentry-hook-signature`) with JSON-stringified payload + issue-alert fallback
203
+ - **Grafana (v12+)**: HMAC-SHA256 (`x-grafana-alerting-signature`) with optional timestamped payload
204
+ - **Doppler**: HMAC-SHA256 (`x-doppler-signature`, `sha256=` prefix)
205
+ - **Sanity**: Stripe-compatible HMAC-SHA256 (`sanity-webhook-signature`, `t=/v1=`)
220
206
  - **fal.ai**: ED25519 (`x-fal-webhook-signature`)
221
207
  - **Shopify**: HMAC-SHA256 (base64 signature) OK Tested
222
208
  - **Vercel**: HMAC-SHA256 Pending
223
209
  - **Polar**: HMAC-SHA256 OK Tested
224
- - **Supabase**: Token-based authentication Pending
225
210
  - **GitLab**: Token-based authentication OK Tested
226
211
 
227
212
  ## Custom Platform Configuration
228
213
 
229
- This framework is fully configuration-driven. You can verify webhooks from any provider—even if it is not built-in—by supplying a custom configuration object. This allows you to support new or proprietary platforms instantly, without waiting for a library update.
214
+ This framework is fully configuration-driven. `timestampHeader` is optional and only needed for providers that send timestamp separately from the signature. You can verify webhooks from any provider—even if it is not built-in—by supplying a custom configuration object. This allows you to support new or proprietary platforms instantly, without waiting for a library update.
230
215
 
231
216
  ### Example: Standard HMAC-SHA256 Webhook
232
217
 
@@ -240,6 +225,7 @@ const acmeConfig = {
240
225
  algorithm: 'hmac-sha256',
241
226
  headerName: 'x-acme-signature',
242
227
  headerFormat: 'raw',
228
+ // Optional: only include when provider sends timestamp in a separate header
243
229
  timestampHeader: 'x-acme-timestamp',
244
230
  timestampFormat: 'unix',
245
231
  payloadFormat: 'timestamped', // signs as {timestamp}.{body}
@@ -275,14 +261,22 @@ const result = await WebhookVerificationService.verify(request, svixConfig);
275
261
 
276
262
  You can configure any combination of algorithm, header, payload, and encoding. See the `SignatureConfig` type for all options.
277
263
 
278
- ## Webhook Verification OK Tested Platforms
264
+ For `platform: 'custom'`, default config remains compatible with token-style providers through `signatureConfig.customConfig` (`type: 'token-based'`, `idHeader: 'x-webhook-id'`), and you can override it per provider.
265
+
266
+ ## Verified Platforms (continuously tested)
279
267
  - **Stripe**
280
- - **Supabase**
281
- - **Github**
268
+ - **GitHub**
282
269
  - **Clerk**
283
270
  - **Dodo Payments**
271
+ - **GitLab**
272
+ - **WorkOS**
273
+ - **Lemon Squeezy**
274
+ - **Paddle**
275
+ - **Shopify**
276
+ - **Polar**
277
+ - **ReplicateAI**
284
278
 
285
- - **Other Platforms** : Yet to verify....
279
+ Other listed platforms are supported but may have lighter coverage depending on release cycle.
286
280
 
287
281
 
288
282
  ## Custom Configurations
@@ -445,9 +439,11 @@ Simplified verification using platform-specific configurations with optional pay
445
439
 
446
440
  Auto-detects platform from headers and verifies against one or more provider secrets.
447
441
 
448
- #### `verifyTokenBased(request: Request, webhookId: string, webhookToken: string): Promise<WebhookVerificationResult>`
442
+ #### `verifyTokenAuth(request: Request, webhookId: string, webhookToken: string): Promise<WebhookVerificationResult>`
443
+
444
+ Verifies token-based webhooks.
449
445
 
450
- Verifies token-based webhooks (like Supabase).
446
+ > `verifyTokenBased(...)` remains available as a backward-compatible alias and still works for existing integrations.
451
447
 
452
448
  #### `getPlatformsByCategory(category: 'payment' | 'auth' | 'ecommerce' | 'infrastructure'): WebhookPlatform[]`
453
449
 
@@ -464,9 +460,10 @@ interface WebhookVerificationResult {
464
460
  errorCode?: WebhookErrorCode;
465
461
  platform: WebhookPlatform;
466
462
  payload?: any;
463
+ eventId?: string; // canonical ID, e.g. 'stripe:evt_123'
467
464
  metadata?: {
468
465
  timestamp?: string;
469
- id?: string | null;
466
+ id?: string | null; // raw provider ID (legacy)
470
467
  [key: string]: any;
471
468
  };
472
469
  }
@@ -1,11 +1,11 @@
1
1
  import { WebhookPlatform, NormalizeOptions } from '../types';
2
- export interface CloudflareWebhookHandlerOptions<TEnv = Record<string, unknown>, TResponse = unknown> {
2
+ export interface CloudflareWebhookHandlerOptions<TEnv = Record<string, unknown>, TPayload = any, TMetadata extends Record<string, unknown> = Record<string, unknown>, TResponse = unknown> {
3
3
  platform: WebhookPlatform;
4
4
  secret?: string;
5
5
  secretEnv?: string;
6
6
  toleranceInSeconds?: number;
7
7
  normalize?: boolean | NormalizeOptions;
8
8
  onError?: (error: Error) => void;
9
- handler: (payload: any, env: TEnv, metadata: Record<string, any>) => Promise<TResponse> | TResponse;
9
+ handler: (payload: TPayload, env: TEnv, metadata: TMetadata) => Promise<TResponse> | TResponse;
10
10
  }
11
- export declare function createWebhookHandler<TEnv = Record<string, unknown>, TResponse = unknown>(options: CloudflareWebhookHandlerOptions<TEnv, TResponse>): (request: Request, env: TEnv) => Promise<Response>;
11
+ export declare function createWebhookHandler<TEnv = Record<string, unknown>, TPayload = any, TMetadata extends Record<string, unknown> = Record<string, unknown>, TResponse = unknown>(options: CloudflareWebhookHandlerOptions<TEnv, TPayload, TMetadata, TResponse>): (request: Request, env: TEnv) => Promise<Response>;
@@ -14,7 +14,7 @@ function createWebhookHandler(options) {
14
14
  if (!result.isValid) {
15
15
  return Response.json({ error: result.error, errorCode: result.errorCode, platform: result.platform, metadata: result.metadata }, { status: 400 });
16
16
  }
17
- const data = await options.handler(result.payload, env, result.metadata || {});
17
+ const data = await options.handler(result.payload, env, (result.metadata || {}));
18
18
  return Response.json(data);
19
19
  }
20
20
  catch (error) {
@@ -1,5 +1,5 @@
1
- import { WebhookPlatform, WebhookVerificationResult, NormalizeOptions } from "../types";
2
- import { MinimalNodeRequest } from "./shared";
1
+ import { WebhookPlatform, WebhookVerificationResult, NormalizeOptions } from '../types';
2
+ import { MinimalNodeRequest } from './shared';
3
3
  export interface ExpressLikeResponse {
4
4
  status: (code: number) => ExpressLikeResponse;
5
5
  json: (payload: unknown) => unknown;
@@ -1,10 +1,10 @@
1
1
  import { WebhookPlatform, NormalizeOptions } from '../types';
2
- export interface NextWebhookHandlerOptions<TResponse = unknown> {
2
+ export interface NextWebhookHandlerOptions<TPayload = any, TMetadata extends Record<string, unknown> = Record<string, unknown>, TResponse = unknown> {
3
3
  platform: WebhookPlatform;
4
4
  secret: string;
5
5
  toleranceInSeconds?: number;
6
6
  normalize?: boolean | NormalizeOptions;
7
7
  onError?: (error: Error) => void;
8
- handler: (payload: any, metadata: Record<string, any>) => Promise<TResponse> | TResponse;
8
+ handler: (payload: TPayload, metadata: TMetadata) => Promise<TResponse> | TResponse;
9
9
  }
10
- export declare function createWebhookHandler<TResponse = unknown>(options: NextWebhookHandlerOptions<TResponse>): (request: Request) => Promise<Response>;
10
+ export declare function createWebhookHandler<TPayload = any, TMetadata extends Record<string, unknown> = Record<string, unknown>, TResponse = unknown>(options: NextWebhookHandlerOptions<TPayload, TMetadata, TResponse>): (request: Request) => Promise<Response>;
@@ -9,7 +9,7 @@ function createWebhookHandler(options) {
9
9
  if (!result.isValid) {
10
10
  return Response.json({ error: result.error, errorCode: result.errorCode, platform: result.platform, metadata: result.metadata }, { status: 400 });
11
11
  }
12
- const data = await options.handler(result.payload, result.metadata || {});
12
+ const data = await options.handler(result.payload, (result.metadata || {}));
13
13
  return Response.json(data);
14
14
  }
15
15
  catch (error) {
@@ -6,7 +6,7 @@ exports.toWebRequest = toWebRequest;
6
6
  function getHeaderValue(headers, name) {
7
7
  const value = headers[name.toLowerCase()] ?? headers[name];
8
8
  if (Array.isArray(value)) {
9
- return value.join(",");
9
+ return value.join(',');
10
10
  }
11
11
  return value;
12
12
  }
@@ -15,11 +15,11 @@ async function readIncomingMessageBodyAsBuffer(request) {
15
15
  return new Uint8Array(0);
16
16
  const chunks = [];
17
17
  return new Promise((resolve, reject) => {
18
- request.on?.("data", (chunk) => {
18
+ request.on?.('data', (chunk) => {
19
19
  if (chunk instanceof Uint8Array) {
20
20
  chunks.push(chunk);
21
21
  }
22
- else if (typeof chunk === "string") {
22
+ else if (typeof chunk === 'string') {
23
23
  chunks.push(new TextEncoder().encode(chunk));
24
24
  }
25
25
  else if (chunk != null) {
@@ -31,7 +31,7 @@ async function readIncomingMessageBodyAsBuffer(request) {
31
31
  }
32
32
  }
33
33
  });
34
- request.on?.("end", () => {
34
+ request.on?.('end', () => {
35
35
  const totalLength = chunks.reduce((sum, c) => sum + c.byteLength, 0);
36
36
  const result = new Uint8Array(totalLength);
37
37
  let offset = 0;
@@ -41,22 +41,22 @@ async function readIncomingMessageBodyAsBuffer(request) {
41
41
  }
42
42
  resolve(result);
43
43
  });
44
- request.on?.("error", (err) => reject(err));
44
+ request.on?.('error', (err) => reject(err));
45
45
  });
46
46
  }
47
47
  async function extractRawBody(request) {
48
- const body = request.body;
48
+ const { body } = request;
49
49
  if (body instanceof Uint8Array) {
50
50
  return body;
51
51
  }
52
- if (typeof body === "string") {
52
+ if (typeof body === 'string') {
53
53
  return new TextEncoder().encode(body);
54
54
  }
55
- if (body !== null && body !== undefined && typeof body === "object") {
56
- console.warn("[Tern] Warning: request body is already parsed as JSON. " +
57
- "Signature verification may fail. " +
58
- 'Add express.raw({ type: "*/*" }) before Tern on webhook routes, ' +
59
- "or register webhook routes before app.use(express.json()).");
55
+ if (body !== null && body !== undefined && typeof body === 'object') {
56
+ console.warn('[Tern] Warning: request body is already parsed as JSON. '
57
+ + 'Signature verification may fail. '
58
+ + 'Add express.raw({ type: "*/*" }) before Tern on webhook routes, '
59
+ + 'or register webhook routes before app.use(express.json()).');
60
60
  return new TextEncoder().encode(JSON.stringify(body));
61
61
  }
62
62
  // nothing ran — read raw stream directly
@@ -73,7 +73,7 @@ function toHeadersInit(headers) {
73
73
  continue;
74
74
  }
75
75
  if (Array.isArray(value)) {
76
- normalized.set(key, value.join(","));
76
+ normalized.set(key, value.join(','));
77
77
  continue;
78
78
  }
79
79
  normalized.set(key, value);
@@ -81,17 +81,17 @@ function toHeadersInit(headers) {
81
81
  return normalized;
82
82
  }
83
83
  async function toWebRequest(request) {
84
- const protocol = request.protocol || "https";
85
- const host = request.get?.("host") ||
86
- getHeaderValue(request.headers, "host") ||
87
- "localhost";
88
- const path = request.originalUrl || request.url || "/";
84
+ const protocol = request.protocol || 'https';
85
+ const host = request.get?.('host')
86
+ || getHeaderValue(request.headers, 'host')
87
+ || 'localhost';
88
+ const path = request.originalUrl || request.url || '/';
89
89
  const rawBody = await extractRawBody(request);
90
90
  const init = {
91
- method: request.method || "POST",
91
+ method: request.method || 'POST',
92
92
  headers: toHeadersInit(request.headers),
93
93
  body: toArrayBuffer(rawBody),
94
- duplex: "half",
94
+ duplex: 'half',
95
95
  };
96
96
  return new Request(`${protocol}://${host}${path}`, init);
97
97
  }
@@ -6,79 +6,78 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const crypto_1 = __importDefault(require("crypto"));
7
7
  const shared_1 = require("./shared");
8
8
  const __1 = __importDefault(require(".."));
9
- const secret = "whsec_test_secret";
10
- const payload = JSON.stringify({ type: "payment_intent.succeeded", data: { amount: 2000 } }) +
11
- "\n";
9
+ const secret = 'whsec_test_secret';
10
+ const payload = `${JSON.stringify({ type: 'payment_intent.succeeded', data: { amount: 2000 } })}\n`;
12
11
  function buildStripeSignature(body, secret) {
13
12
  const timestamp = Math.floor(Date.now() / 1000);
14
13
  const signed = `${timestamp}.${body}`;
15
14
  const hmac = crypto_1.default
16
- .createHmac("sha256", secret.replace("whsec_", ""))
15
+ .createHmac('sha256', secret.replace('whsec_', ''))
17
16
  .update(signed)
18
- .digest("hex");
17
+ .digest('hex');
19
18
  return `t=${timestamp},v1=${hmac}`;
20
19
  }
21
20
  // ── Test 1: raw stream — no middleware ran ───────────────────
22
21
  async function testRawStream() {
23
22
  const sig = buildStripeSignature(payload, secret); // ✓ fixed: was missing secret arg
24
- const chunks = [Buffer.from(payload, "utf8")];
23
+ const chunks = [Buffer.from(payload, 'utf8')];
25
24
  const req = {
26
- method: "POST",
25
+ method: 'POST',
27
26
  headers: {
28
- "stripe-signature": sig,
29
- "content-type": "application/json",
27
+ 'stripe-signature': sig,
28
+ 'content-type': 'application/json',
30
29
  },
31
30
  body: undefined,
32
31
  on: (event, cb) => {
33
- if (event === "data") {
32
+ if (event === 'data') {
34
33
  for (const chunk of chunks)
35
34
  cb(chunk);
36
35
  }
37
- if (event === "end") {
36
+ if (event === 'end') {
38
37
  cb();
39
38
  }
40
39
  },
41
40
  };
42
41
  const webReq = await (0, shared_1.toWebRequest)(req);
43
- const result = await __1.default.verifyWithPlatformConfig(webReq, "stripe", secret);
44
- console.log("Test 1 Raw stream (no middleware):", result.isValid ? "✓ PASS" : `✗ FAIL — ${result.error}`);
42
+ const result = await __1.default.verifyWithPlatformConfig(webReq, 'stripe', secret);
43
+ console.log('Test 1 Raw stream (no middleware):', result.isValid ? '✓ PASS' : `✗ FAIL — ${result.error}`);
45
44
  }
46
45
  // ── Test 2: Buffer body — express.raw() ran ──────────────────
47
46
  async function testBufferBody() {
48
47
  const sig = buildStripeSignature(payload, secret);
49
48
  const req = {
50
- method: "POST",
49
+ method: 'POST',
51
50
  headers: {
52
- "content-type": "application/json",
53
- "stripe-signature": sig,
51
+ 'content-type': 'application/json',
52
+ 'stripe-signature': sig,
54
53
  },
55
- body: Buffer.from(payload, "utf8"),
54
+ body: Buffer.from(payload, 'utf8'),
56
55
  };
57
56
  const webReq = await (0, shared_1.toWebRequest)(req);
58
- const result = await __1.default.verifyWithPlatformConfig(webReq, "stripe", secret);
59
- console.log("Test 2 Buffer body (express.raw):", result.isValid ? "✓ PASS" : `✗ FAIL — ${result.error}`);
57
+ const result = await __1.default.verifyWithPlatformConfig(webReq, 'stripe', secret);
58
+ console.log('Test 2 Buffer body (express.raw):', result.isValid ? '✓ PASS' : `✗ FAIL — ${result.error}`);
60
59
  }
61
60
  // ── Test 3: parsed object — express.json() ran ───────────────
62
61
  async function testParsedObject() {
63
62
  // signature computed against raw payload including \n
64
63
  const sig = buildStripeSignature(payload, secret);
65
64
  const req = {
66
- method: "POST",
65
+ method: 'POST',
67
66
  headers: {
68
- "content-type": "application/json",
69
- "stripe-signature": sig,
67
+ 'content-type': 'application/json',
68
+ 'stripe-signature': sig,
70
69
  },
71
70
  body: JSON.parse(payload), // \n gone, bytes lost
72
71
  };
73
72
  const webReq = await (0, shared_1.toWebRequest)(req);
74
- const result = await __1.default.verifyWithPlatformConfig(webReq, "stripe", secret);
73
+ const result = await __1.default.verifyWithPlatformConfig(webReq, 'stripe', secret);
75
74
  // should FAIL — \n was in original signature but lost after JSON.parse
76
- console.log("Test 3 Parsed object (express.json):", !result.isValid ? "✓ FAILS AS EXPECTED" : "✗ SHOULD HAVE FAILED");
75
+ console.log('Test 3 Parsed object (express.json):', !result.isValid ? '✓ FAILS AS EXPECTED' : '✗ SHOULD HAVE FAILED');
77
76
  }
78
77
  async function run() {
79
78
  await testRawStream(); // no middleware → should PASS
80
79
  await testBufferBody(); // express.raw() → should PASS
81
80
  await testParsedObject(); // express.json() → should FAIL as expected
82
- console.log("\nDone. Test 3 failing is correct behavior.");
81
+ console.log('\nDone. Test 3 failing is correct behavior.');
83
82
  }
84
83
  run();
package/dist/examples.js CHANGED
@@ -113,7 +113,7 @@ async function exampleSimplifiedPlatform() {
113
113
  console.error('Error:', error);
114
114
  }
115
115
  }
116
- // Example 4: Token-based authentication (Supabase style)
116
+ // Example 4: Token-based authentication helper
117
117
  async function exampleTokenBased() {
118
118
  console.log('\n🔧 Example 4: Token-Based Authentication\n');
119
119
  const request = new Request('https://your-app.com/webhook', {
@@ -126,7 +126,7 @@ async function exampleTokenBased() {
126
126
  body: JSON.stringify({ event: 'database.insert' }),
127
127
  });
128
128
  try {
129
- const result = await index_1.WebhookVerificationService.verifyTokenBased(request, 'webhook_123', 'token_456');
129
+ const result = await index_1.WebhookVerificationService.verifyTokenAuth(request, 'webhook_123', 'token_456');
130
130
  if (result.isValid) {
131
131
  console.log('✅ Token-based webhook verified!');
132
132
  console.log('Webhook ID:', result.metadata?.id);
package/dist/index.d.ts CHANGED
@@ -6,10 +6,13 @@ export declare class WebhookVerificationService {
6
6
  private static getLegacyVerifier;
7
7
  static verifyWithPlatformConfig<TPayload = unknown>(request: Request, platform: WebhookPlatform, secret: string, toleranceInSeconds?: number, normalize?: boolean | NormalizeOptions): Promise<WebhookVerificationResult<TPayload>>;
8
8
  static verifyAny<TPayload = unknown>(request: Request, secrets: MultiPlatformSecrets, toleranceInSeconds?: number, normalize?: boolean | NormalizeOptions): Promise<WebhookVerificationResult<TPayload>>;
9
+ private static resolveCanonicalEventId;
10
+ private static resolveRawEventId;
9
11
  static detectPlatform(request: Request): WebhookPlatform;
10
12
  static getPlatformsUsingAlgorithm(algorithm: string): WebhookPlatform[];
11
13
  static platformUsesAlgorithm(platform: WebhookPlatform, algorithm: string): boolean;
12
14
  static validateSignatureConfig(config: SignatureConfig): boolean;
15
+ static verifyTokenAuth<TPayload = unknown>(request: Request, webhookId: string, webhookToken: string): Promise<WebhookVerificationResult<TPayload>>;
13
16
  static verifyTokenBased<TPayload = unknown>(request: Request, webhookId: string, webhookToken: string): Promise<WebhookVerificationResult<TPayload>>;
14
17
  }
15
18
  export * from './types';
package/dist/index.js CHANGED
@@ -26,6 +26,7 @@ class WebhookVerificationService {
26
26
  // Ensure the platform is set correctly in the result
27
27
  if (result.isValid) {
28
28
  result.platform = config.platform;
29
+ result.eventId = this.resolveCanonicalEventId(config.platform, result.metadata);
29
30
  if (config.normalize) {
30
31
  result.payload = (0, simple_1.normalizePayload)(config.platform, result.payload, config.normalize);
31
32
  }
@@ -109,6 +110,17 @@ class WebhookVerificationService {
109
110
  },
110
111
  };
111
112
  }
113
+ static resolveCanonicalEventId(platform, metadata) {
114
+ const rawId = this.resolveRawEventId(platform, metadata);
115
+ return `${platform}:${rawId}`;
116
+ }
117
+ static resolveRawEventId(platform, metadata) {
118
+ const candidate = metadata?.id || metadata?.delivery || metadata?.requestId || metadata?.timestamp;
119
+ if (candidate !== undefined && candidate !== null && `${candidate}`.trim().length > 0) {
120
+ return `${candidate}`.trim();
121
+ }
122
+ return `generated-${Date.now().toString(36)}-${platform}`;
123
+ }
112
124
  static detectPlatform(request) {
113
125
  const headers = request.headers;
114
126
  if (headers.has('stripe-signature'))
@@ -137,18 +149,22 @@ class WebhookVerificationService {
137
149
  return 'razorpay';
138
150
  if (headers.has('x-signature'))
139
151
  return 'lemonsqueezy';
140
- if (headers.has('x-auth0-signature'))
141
- return 'auth0';
142
152
  if (headers.has('x-wc-webhook-signature'))
143
153
  return 'woocommerce';
144
154
  if (headers.has('x-fal-signature') || headers.has('x-fal-webhook-signature'))
145
155
  return 'falai';
156
+ if (headers.has('sentry-hook-signature'))
157
+ return 'sentry';
158
+ if (headers.has('x-grafana-alerting-signature'))
159
+ return 'grafana';
160
+ if (headers.has('x-doppler-signature'))
161
+ return 'doppler';
162
+ if (headers.has('sanity-webhook-signature'))
163
+ return 'sanity';
146
164
  if (headers.has('x-shopify-hmac-sha256'))
147
165
  return 'shopify';
148
166
  if (headers.has('x-vercel-signature'))
149
167
  return 'vercel';
150
- if (headers.has('x-webhook-token') && headers.has('x-webhook-id'))
151
- return 'supabase';
152
168
  return 'unknown';
153
169
  }
154
170
  // Helper method to get all platforms using a specific algorithm
@@ -163,8 +179,8 @@ class WebhookVerificationService {
163
179
  static validateSignatureConfig(config) {
164
180
  return (0, algorithms_2.validateSignatureConfig)(config);
165
181
  }
166
- // Simple token-based verification for platforms like Supabase
167
- static async verifyTokenBased(request, webhookId, webhookToken) {
182
+ // Simple token-based verification helper for token-auth providers
183
+ static async verifyTokenAuth(request, webhookId, webhookToken) {
168
184
  try {
169
185
  const idHeader = request.headers.get('x-webhook-id');
170
186
  const tokenHeader = request.headers.get('x-webhook-token');
@@ -198,6 +214,7 @@ class WebhookVerificationService {
198
214
  isValid: true,
199
215
  platform: 'custom',
200
216
  payload: payload,
217
+ eventId: `custom:${idHeader}`,
201
218
  metadata: {
202
219
  id: idHeader,
203
220
  algorithm: 'token-based',
@@ -213,6 +230,9 @@ class WebhookVerificationService {
213
230
  };
214
231
  }
215
232
  }
233
+ static async verifyTokenBased(request, webhookId, webhookToken) {
234
+ return this.verifyTokenAuth(request, webhookId, webhookToken);
235
+ }
216
236
  }
217
237
  exports.WebhookVerificationService = WebhookVerificationService;
218
238
  __exportStar(require("./types"), exports);
@@ -6,8 +6,6 @@ const providers = [
6
6
  { id: 'razorpay', name: 'Razorpay', category: 'payment' },
7
7
  { id: 'paypal', name: 'PayPal', category: 'payment' },
8
8
  { id: 'clerk', name: 'Clerk', category: 'auth' },
9
- { id: 'auth0', name: 'Auth0', category: 'auth' },
10
- { id: 'supabase', name: 'Supabase', category: 'auth' },
11
9
  { id: 'shopify', name: 'Shopify', category: 'ecommerce' },
12
10
  { id: 'woocommerce', name: 'WooCommerce', category: 'ecommerce' },
13
11
  ];
@@ -57,18 +57,6 @@ const platformNormalizers = {
57
57
  occurred_at: new Date().toISOString(),
58
58
  }),
59
59
  },
60
- supabase: {
61
- platform: 'supabase',
62
- category: 'auth',
63
- normalize: (payload) => ({
64
- category: 'auth',
65
- event: readPath(payload, 'type') || readPath(payload, 'event') || 'auth.unknown',
66
- user_id: readPath(payload, 'record.id') || readPath(payload, 'id'),
67
- email: readPath(payload, 'record.email') || readPath(payload, 'email'),
68
- metadata: {},
69
- occurred_at: new Date().toISOString(),
70
- }),
71
- },
72
60
  vercel: {
73
61
  platform: 'vercel',
74
62
  category: 'infrastructure',
@@ -1,4 +1,4 @@
1
- import { PlatformAlgorithmConfig, WebhookPlatform, SignatureConfig } from "../types";
1
+ import { PlatformAlgorithmConfig, WebhookPlatform, SignatureConfig } from '../types';
2
2
  export declare const platformAlgorithmConfigs: Record<WebhookPlatform, PlatformAlgorithmConfig>;
3
3
  export declare function getPlatformAlgorithmConfig(platform: WebhookPlatform): PlatformAlgorithmConfig;
4
4
  export declare function platformUsesAlgorithm(platform: WebhookPlatform, algorithm: string): boolean;