@hookflo/tern 4.1.0 → 4.2.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.
Files changed (37) hide show
  1. package/README.md +256 -438
  2. package/dist/adapters/cloudflare.d.ts +3 -0
  3. package/dist/adapters/cloudflare.js +25 -1
  4. package/dist/adapters/express.d.ts +3 -0
  5. package/dist/adapters/express.js +16 -0
  6. package/dist/adapters/nextjs.d.ts +3 -0
  7. package/dist/adapters/nextjs.js +25 -1
  8. package/dist/alerts.d.ts +7 -0
  9. package/dist/alerts.js +21 -0
  10. package/dist/index.d.ts +4 -0
  11. package/dist/index.js +20 -1
  12. package/dist/notifications/channels/discord/build-payload.d.ts +17 -0
  13. package/dist/notifications/channels/discord/build-payload.js +53 -0
  14. package/dist/notifications/channels/discord/index.d.ts +1 -0
  15. package/dist/notifications/channels/discord/index.js +5 -0
  16. package/dist/notifications/channels/slack/build-payload.d.ts +5 -0
  17. package/dist/notifications/channels/slack/build-payload.js +82 -0
  18. package/dist/notifications/channels/slack/index.d.ts +1 -0
  19. package/dist/notifications/channels/slack/index.js +5 -0
  20. package/dist/notifications/constants.d.ts +8 -0
  21. package/dist/notifications/constants.js +15 -0
  22. package/dist/notifications/dispatch.d.ts +8 -0
  23. package/dist/notifications/dispatch.js +13 -0
  24. package/dist/notifications/index.d.ts +10 -0
  25. package/dist/notifications/index.js +27 -0
  26. package/dist/notifications/send-alert.d.ts +2 -0
  27. package/dist/notifications/send-alert.js +69 -0
  28. package/dist/notifications/types.d.ts +48 -0
  29. package/dist/notifications/types.js +2 -0
  30. package/dist/notifications/utils.d.ts +4 -0
  31. package/dist/notifications/utils.js +80 -0
  32. package/dist/upstash/controls.d.ts +2 -6
  33. package/dist/upstash/controls.js +63 -0
  34. package/dist/upstash/index.d.ts +1 -1
  35. package/dist/upstash/queue.js +5 -1
  36. package/dist/upstash/types.d.ts +18 -0
  37. package/package.json +1 -1
package/README.md CHANGED
@@ -1,322 +1,108 @@
1
- # Tern - Algorithm Agnostic Webhook Verification Framework
1
+ # Tern Webhook Verification for Every Platform
2
2
 
3
- A robust, algorithm-agnostic webhook verification framework that supports multiple platforms with accurate signature verification and payload retrieval.
4
- The same framework that secures webhook verification at [Hookflo](https://hookflo.com).
3
+ **When Stripe, Shopify, Clerk or any other platform sends a webhook to your server, how do you know it's real and not a forged request?** Tern checks the signature for you one simplified TypeScript SDK, any provider, no boilerplate.
5
4
 
6
- Star this repo to support the project and help others discover it!
5
+ [![npm version](https://img.shields.io/npm/v/@hookflo/tern)](https://www.npmjs.com/package/@hookflo/tern)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-Strict-blue)](https://www.typescriptlang.org/)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
8
+ [![Zero Dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)](package.json)
9
+
10
+ Stop writing webhook verification from scratch. **Tern** handles signature verification for Stripe, GitHub, Clerk, Shopify, and 15+ more platforms — with one consistent API.
7
11
 
8
- 💬 Join the discussion & contribute in our Discord: [Hookflo Community](https://discord.com/invite/SNmCjU97nr)
12
+ > Need reliable delivery too? Tern supports inbound webhook delivery via Upstash QStash — automatic retries, DLQ management, replay controls, and Slack/Discord alerting. Bring your own Upstash account (BYOK).
9
13
 
10
14
  ```bash
11
15
  npm install @hookflo/tern
12
16
  ```
13
17
 
14
- [![npm version](https://img.shields.io/npm/v/@hookflo/tern)](https://www.npmjs.com/package/@hookflo/tern)
15
- [![TypeScript](https://img.shields.io/badge/TypeScript-Strict-blue)](https://www.typescriptlang.org/)
16
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
17
-
18
- Tern is a zero-dependency TypeScript framework for robust webhook verification across multiple platforms and algorithms.
18
+ > The same framework powering webhook verification at [Hookflo](https://hookflo.com).
19
19
 
20
- **Runtime requirements:** Node.js 18+ (or any runtime with Web Crypto + Fetch APIs, such as Deno and Cloudflare Workers).
20
+ Star this repo to help others discover it · 💬 [Join our Discord](https://discord.com/invite/SNmCjU97nr)
21
21
 
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" />
22
+ <img width="1200" height="630" alt="Tern Webhook Verification Framework" src="https://tern.hookflo.com/og-image.webp" style="border-radius: 10px; margin-top: 16px;" />
23
23
 
24
- ## Features
24
+ **Navigation**
25
25
 
26
- - **Algorithm Agnostic**: Decouples platform logic from signature verification verify based on cryptographic algorithm, not hardcoded platform rules.
27
- Supports HMAC-SHA256, HMAC-SHA1, HMAC-SHA512, and custom algorithms
26
+ [The Problem](#the-problem) · [Quick Start](#quick-start) · [Framework Integrations](#framework-integrations) · [Supported Platforms](#supported-platforms) · [Key Features](#key-features) · [Reliable Delivery & Alerting](#reliable-delivery--alerting) · [Custom Config](#custom-platform-configuration) · [API Reference](#api-reference) · [Troubleshooting](#troubleshooting) · [Contributing](#contributing) · [Support](#support)
28
27
 
29
- - **Platform Specific**: Accurate implementations for **Stripe, GitHub, Clerk**, and other platforms
30
- - **Flexible Configuration**: Custom signature configurations for any webhook format
31
- - **Type Safe**: Full TypeScript support with comprehensive type definitions
32
- - **Framework Agnostic**: Works with Express.js, Next.js, Cloudflare Workers, and more
33
- - **Body-Parser Safe Adapters**: Read raw request bodies correctly to avoid signature mismatch issues
34
- - **Multi-Provider Verification**: Verify and auto-detect across multiple providers with one API
35
- - **Payload Normalization**: Opt-in normalized event shape to reduce provider lock-in
36
- - **Category-aware Migration**: Normalize within provider categories (payment/auth/infrastructure) for safe platform switching
37
- - **Strong Typed Normalized Schemas**: Category types like `PaymentWebhookNormalized` and `AuthWebhookNormalized` for safe migrations
38
- - **Foundational Error Taxonomy**: Stable `errorCode` values (`INVALID_SIGNATURE`, `MISSING_SIGNATURE`, etc.)
28
+ ## The Problem
39
29
 
40
- ## Why Tern?
30
+ Every webhook provider has a different signature format. You end up writing — and maintaining — the same verification boilerplate over and over:
41
31
 
42
- Most webhook verifiers are tightly coupled to specific platforms or hardcoded logic. Tern introduces a flexible, scalable, algorithm-first approach that:
43
-
44
- - Works across all major platforms
45
- - Supports custom signing logic
46
- - Keeps your code clean and modular
47
- - Avoids unnecessary dependencies
48
- - Is written in strict, modern TypeScript
32
+ ```typescript
33
+ // ❌ Without Tern — different logic for every provider
34
+ const stripeSignature = req.headers['stripe-signature'];
35
+ const parts = stripeSignature.split(',');
36
+ // ... 30 more lines just for Stripe
49
37
 
50
- ## Installation
38
+ const githubSignature = req.headers['x-hub-signature-256'];
39
+ // ... completely different 20 lines for GitHub
40
+ ```
51
41
 
52
- ```bash
53
- npm install @hookflo/tern
42
+ ```typescript
43
+ // With Tern — one API for everything
44
+ const result = await WebhookVerificationService.verify(request, {
45
+ platform: 'stripe',
46
+ secret: process.env.STRIPE_WEBHOOK_SECRET!,
47
+ });
54
48
  ```
55
49
 
56
50
  ## Quick Start
57
51
 
58
- ### Basic Usage
52
+ ### Verify a single platform
59
53
 
60
54
  ```typescript
61
55
  import { WebhookVerificationService } from '@hookflo/tern';
62
56
 
63
57
  const result = await WebhookVerificationService.verify(request, {
64
58
  platform: 'stripe',
65
- secret: 'whsec_your_stripe_webhook_secret',
59
+ secret: process.env.STRIPE_WEBHOOK_SECRET!,
66
60
  toleranceInSeconds: 300,
67
61
  });
68
62
 
69
63
  if (result.isValid) {
70
- console.log('Webhook verified!', result.eventId, result.payload);
64
+ console.log('Verified!', result.eventId, result.payload);
71
65
  } else {
72
- console.log('Verification failed:', result.error);
66
+ console.log('Failed:', result.error, result.errorCode);
73
67
  }
74
68
  ```
75
69
 
76
- ### Universal Verification (auto-detect platform)
70
+ ### Auto-detect platform
77
71
 
78
72
  ```typescript
79
- import { WebhookVerificationService } from '@hookflo/tern';
80
-
81
73
  const result = await WebhookVerificationService.verifyAny(request, {
82
74
  stripe: process.env.STRIPE_WEBHOOK_SECRET,
83
75
  github: process.env.GITHUB_WEBHOOK_SECRET,
84
76
  clerk: process.env.CLERK_WEBHOOK_SECRET,
85
77
  });
86
78
 
87
- if (result.isValid) {
88
- console.log(`Verified ${result.platform} webhook`);
89
- }
79
+ console.log(`Verified ${result.platform} webhook`);
90
80
  ```
91
81
 
92
- ### Category-aware Payload Normalization
82
+ ### Core SDK (runtime-agnostic)
93
83
 
94
- ### Strongly-Typed Normalized Payloads
84
+ Use Tern without framework adapters in any runtime that supports the Web `Request` API.
95
85
 
96
86
  ```typescript
97
- import {
98
- WebhookVerificationService,
99
- PaymentWebhookNormalized,
100
- } from '@hookflo/tern';
87
+ import { WebhookVerificationService } from '@hookflo/tern';
101
88
 
102
- const result = await WebhookVerificationService.verifyWithPlatformConfig<PaymentWebhookNormalized>(
89
+ const verified = await WebhookVerificationService.verifyWithPlatformConfig(
103
90
  request,
104
- 'stripe',
105
- process.env.STRIPE_WEBHOOK_SECRET!,
91
+ 'workos',
92
+ process.env.WORKOS_WEBHOOK_SECRET!,
106
93
  300,
107
- { enabled: true, category: 'payment' },
108
94
  );
109
95
 
110
- if (result.isValid && result.payload?.event === 'payment.succeeded') {
111
- // result.payload is strongly typed
112
- console.log(result.payload.amount, result.payload.customer_id);
96
+ if (!verified.isValid) {
97
+ return new Response(JSON.stringify({ error: verified.error }), { status: 400 });
113
98
  }
114
- ```
115
-
116
- ```typescript
117
- import { WebhookVerificationService, getPlatformsByCategory } from '@hookflo/tern';
118
-
119
- // Discover migration-compatible providers in the same category
120
- const paymentPlatforms = getPlatformsByCategory('payment');
121
- // ['stripe', 'polar', ...]
122
99
 
123
- const result = await WebhookVerificationService.verifyWithPlatformConfig(
124
- request,
125
- 'stripe',
126
- process.env.STRIPE_WEBHOOK_SECRET!,
127
- 300,
128
- {
129
- enabled: true,
130
- category: 'payment',
131
- includeRaw: true,
132
- },
133
- );
134
-
135
- console.log(result.payload);
136
- // {
137
- // event: 'payment.succeeded',
138
- // amount: 5000,
139
- // currency: 'USD',
140
- // customer_id: 'cus_123',
141
- // transaction_id: 'pi_123',
142
- // provider: 'stripe',
143
- // category: 'payment',
144
- // _raw: {...}
145
- // }
100
+ // verified.payload + verified.metadata available here
146
101
  ```
147
102
 
148
- ### Platform-Specific Configurations
149
-
150
- ```typescript
151
- import { WebhookVerificationService } from '@hookflo/tern';
152
-
153
- // Stripe webhook
154
- const stripeConfig = {
155
- platform: 'stripe',
156
- secret: 'whsec_your_stripe_webhook_secret',
157
- toleranceInSeconds: 300,
158
- };
159
-
160
- // GitHub webhook
161
- const githubConfig = {
162
- platform: 'github',
163
- secret: 'your_github_webhook_secret',
164
- toleranceInSeconds: 300,
165
- };
166
-
167
- // Clerk webhook
168
- const clerkConfig = {
169
- platform: 'clerk',
170
- secret: 'whsec_your_clerk_webhook_secret',
171
- toleranceInSeconds: 300,
172
- };
103
+ ## Framework Integrations
173
104
 
174
- const result = await WebhookVerificationService.verify(request, stripeConfig);
175
- ```
176
-
177
- ## Supported Platforms
178
-
179
- ### Stripe OK Tested
180
- - **Signature Format**: `t={timestamp},v1={signature}`
181
- - **Algorithm**: HMAC-SHA256
182
- - **Payload Format**: `{timestamp}.{body}`
183
-
184
- ### GitHub
185
- - **Signature Format**: `sha256={signature}`
186
- - **Algorithm**: HMAC-SHA256
187
- - **Payload Format**: Raw body
188
-
189
- ### Clerk
190
- - **Signature Format**: `v1,{signature}` (space-separated)
191
- - **Algorithm**: HMAC-SHA256 with base64 encoding
192
- - **Payload Format**: `{id}.{timestamp}.{body}`
193
-
194
- ### Other Platforms
195
- - **Dodo Payments**: HMAC-SHA256 OK Tested
196
- - **Paddle**: HMAC-SHA256 OK Tested
197
- - **Razorpay**: HMAC-SHA256 Pending
198
- - **Lemon Squeezy**: HMAC-SHA256 OK Tested
199
- - **WorkOS**: HMAC-SHA256 (`workos-signature`, `t/v1`) OK Tested
200
- - **WooCommerce**: HMAC-SHA256 (base64 signature) Pending
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=`)
206
- - **fal.ai**: ED25519 (`x-fal-webhook-signature`)
207
- - **Shopify**: HMAC-SHA256 (base64 signature) OK Tested
208
- - **Vercel**: HMAC-SHA256 Pending
209
- - **Polar**: HMAC-SHA256 OK Tested
210
- - **GitLab**: Token-based authentication OK Tested
211
-
212
- ## Custom Platform Configuration
213
-
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.
215
-
216
- ### Example: Standard HMAC-SHA256 Webhook
217
-
218
- ```typescript
219
- import { WebhookVerificationService } from '@hookflo/tern';
220
-
221
- const acmeConfig = {
222
- platform: 'acmepay',
223
- secret: 'acme_secret',
224
- signatureConfig: {
225
- algorithm: 'hmac-sha256',
226
- headerName: 'x-acme-signature',
227
- headerFormat: 'raw',
228
- // Optional: only include when provider sends timestamp in a separate header
229
- timestampHeader: 'x-acme-timestamp',
230
- timestampFormat: 'unix',
231
- payloadFormat: 'timestamped', // signs as {timestamp}.{body}
232
- }
233
- };
234
-
235
- const result = await WebhookVerificationService.verify(request, acmeConfig);
236
- ```
237
-
238
- ### Example: Svix/Standard Webhooks (Clerk, Dodo Payments, etc.)
239
-
240
- ```typescript
241
- const svixConfig = {
242
- platform: 'my-svix-platform',
243
- secret: 'whsec_abc123...',
244
- signatureConfig: {
245
- algorithm: 'hmac-sha256',
246
- headerName: 'webhook-signature',
247
- headerFormat: 'raw',
248
- timestampHeader: 'webhook-timestamp',
249
- timestampFormat: 'unix',
250
- payloadFormat: 'custom',
251
- customConfig: {
252
- payloadFormat: '{id}.{timestamp}.{body}',
253
- idHeader: 'webhook-id',
254
- // encoding: 'base64' // only if the provider uses base64, otherwise omit
255
- }
256
- }
257
- };
258
-
259
- const result = await WebhookVerificationService.verify(request, svixConfig);
260
- ```
261
-
262
- You can configure any combination of algorithm, header, payload, and encoding. See the `SignatureConfig` type for all options.
263
-
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)
267
- - **Stripe**
268
- - **GitHub**
269
- - **Clerk**
270
- - **Dodo Payments**
271
- - **GitLab**
272
- - **WorkOS**
273
- - **Lemon Squeezy**
274
- - **Paddle**
275
- - **Shopify**
276
- - **Polar**
277
- - **ReplicateAI**
278
-
279
- Other listed platforms are supported but may have lighter coverage depending on release cycle.
280
-
281
-
282
- ## Custom Configurations
283
-
284
- ### Custom HMAC-SHA256
285
-
286
- ```typescript
287
- const customConfig = {
288
- platform: 'custom',
289
- secret: 'your_custom_secret',
290
- signatureConfig: {
291
- algorithm: 'hmac-sha256',
292
- headerName: 'x-custom-signature',
293
- headerFormat: 'prefixed',
294
- prefix: 'sha256=',
295
- payloadFormat: 'raw',
296
- },
297
- };
298
- ```
299
-
300
- ### Custom Timestamped Payload
301
-
302
- ```typescript
303
- const timestampedConfig = {
304
- platform: 'custom',
305
- secret: 'your_custom_secret',
306
- signatureConfig: {
307
- algorithm: 'hmac-sha256',
308
- headerName: 'x-webhook-signature',
309
- headerFormat: 'raw',
310
- timestampHeader: 'x-webhook-timestamp',
311
- timestampFormat: 'unix',
312
- payloadFormat: 'timestamped',
313
- },
314
- };
315
- ```
316
-
317
- ## Framework Integration
318
-
319
- ### Express.js middleware (body-parser safe)
105
+ ### Express.js
320
106
 
321
107
  ```typescript
322
108
  import express from 'express';
@@ -326,14 +112,14 @@ const app = express();
326
112
 
327
113
  app.post(
328
114
  '/webhooks/stripe',
115
+ express.raw({ type: '*/*' }),
329
116
  createWebhookMiddleware({
330
117
  platform: 'stripe',
331
118
  secret: process.env.STRIPE_WEBHOOK_SECRET!,
332
- normalize: true,
333
119
  }),
334
120
  (req, res) => {
335
- const event = (req as any).webhook.payload;
336
- res.json({ received: true, event: event.event });
121
+ const event = (req as any).webhook?.payload;
122
+ res.json({ received: true, event });
337
123
  },
338
124
  );
339
125
  ```
@@ -346,7 +132,7 @@ import { createWebhookHandler } from '@hookflo/tern/nextjs';
346
132
  export const POST = createWebhookHandler({
347
133
  platform: 'github',
348
134
  secret: process.env.GITHUB_WEBHOOK_SECRET!,
349
- handler: async (payload) => ({ received: true, event: payload.event ?? payload.type }),
135
+ handler: async (payload, metadata) => ({ received: true, delivery: metadata.delivery }),
350
136
  });
351
137
  ```
352
138
 
@@ -355,250 +141,282 @@ export const POST = createWebhookHandler({
355
141
  ```typescript
356
142
  import { createWebhookHandler } from '@hookflo/tern/cloudflare';
357
143
 
358
- const handleStripe = createWebhookHandler({
144
+ export const onRequestPost = createWebhookHandler({
359
145
  platform: 'stripe',
360
146
  secretEnv: 'STRIPE_WEBHOOK_SECRET',
361
- handler: async (payload) => ({ received: true, event: payload.event ?? payload.type }),
147
+ handler: async (payload) => ({ received: true, payload }),
362
148
  });
363
-
364
- export default {
365
- async fetch(request: Request, env: Record<string, string>) {
366
- if (new URL(request.url).pathname === '/webhooks/stripe') {
367
- return handleStripe(request, env);
368
- }
369
- return new Response('Not Found', { status: 404 });
370
- },
371
- };
372
149
  ```
373
150
 
151
+ > All built-in platforms work across Express, Next.js, and Cloudflare adapters. You only change `platform` and `secret` per route.
374
152
 
375
- ### Are new platforms available in framework middlewares automatically?
376
-
377
- Yes. All built-in platforms are available in:
378
- - `createWebhookMiddleware` (`@hookflo/tern/express`)
379
- - `createWebhookHandler` (`@hookflo/tern/nextjs`)
380
- - `createWebhookHandler` (`@hookflo/tern/cloudflare`)
381
-
382
- You only change `platform` and `secret` per route.
153
+ ## Supported Platforms
383
154
 
384
- ### Platform route examples (Express / Next.js / Cloudflare)
155
+ | Platform | Algorithm | Status |
156
+ |---|---|---|
157
+ | **Stripe** | HMAC-SHA256 | ✅ Tested |
158
+ | **GitHub** | HMAC-SHA256 | ✅ Tested |
159
+ | **Clerk** | HMAC-SHA256 (base64) | ✅ Tested |
160
+ | **Shopify** | HMAC-SHA256 (base64) | ✅ Tested |
161
+ | **Dodo Payments** | HMAC-SHA256 | ✅ Tested |
162
+ | **Paddle** | HMAC-SHA256 | ✅ Tested |
163
+ | **Lemon Squeezy** | HMAC-SHA256 | ✅ Tested |
164
+ | **Polar** | HMAC-SHA256 | ✅ Tested |
165
+ | **WorkOS** | HMAC-SHA256 | ✅ Tested |
166
+ | **ReplicateAI** | HMAC-SHA256 | ✅ Tested |
167
+ | **GitLab** | Token-based | ✅ Tested |
168
+ | **fal.ai** | ED25519 | ✅ Tested |
169
+ | **Sentry** | HMAC-SHA256 | ✅ Tested |
170
+ | **Grafana** | HMAC-SHA256 | ✅ Tested |
171
+ | **Doppler** | HMAC-SHA256 | ✅ Tested |
172
+ | **Sanity** | HMAC-SHA256 | ✅ Tested |
173
+ | **Razorpay** | HMAC-SHA256 | 🔄 Pending |
174
+ | **Vercel** | HMAC-SHA256 | 🔄 Pending |
175
+
176
+ > Don't see your platform? [Use custom config](#custom-platform-configuration) or [open an issue](https://github.com/Hookflo/tern/issues).
177
+
178
+ ### Platform signature notes
179
+
180
+ - **Standard Webhooks style** platforms (Clerk, Dodo Payments, Polar, ReplicateAI) commonly use a secret that starts with `whsec_...`.
181
+ - **ReplicateAI**: copy the webhook signing secret from your Replicate webhook settings and pass it directly as `secret`.
182
+ - **fal.ai**: supports JWKS key resolution out of the box — use `secret: ''` for auto key resolution, or pass a PEM public key explicitly.
183
+
184
+ ### Note on fal.ai
185
+
186
+ fal.ai uses **ED25519** signing. Pass an **empty string** as the webhook secret — the public key is resolved automatically via JWKS from fal's infrastructure.
385
187
 
386
188
  ```typescript
387
- // Express (Razorpay)
388
- app.post('/webhooks/razorpay', createWebhookMiddleware({
389
- platform: 'razorpay',
390
- secret: process.env.RAZORPAY_WEBHOOK_SECRET!,
391
- }), (req, res) => res.json({ ok: true }));
392
-
393
- // Next.js (WorkOS)
394
- export const POST = createWebhookHandler({
395
- platform: 'workos',
396
- secret: process.env.WORKOS_WEBHOOK_SECRET!,
397
- handler: async (payload) => ({ received: true, type: payload.type }),
398
- });
399
-
400
- // Cloudflare (Lemon Squeezy)
401
- const handleLemonSqueezy = createWebhookHandler({
402
- platform: 'lemonsqueezy',
403
- secretEnv: 'LEMON_SQUEEZY_WEBHOOK_SECRET',
404
- handler: async () => ({ received: true }),
405
- });
406
- ```
407
-
408
- ### fal.ai production usage
409
-
410
- fal.ai uses **ED25519** (`x-fal-webhook-signature`) and signs:
411
- `{request-id}.{user-id}.{timestamp}.{sha256(body)}`.
412
-
413
- Use one of these strategies:
414
- 1. **Public key as `secret`** (recommended for framework adapters).
415
- 2. **JWKS auto-resolution** via the built-in fal.ai config (`x-fal-webhook-key-id` + fal JWKS URL).
189
+ import { createWebhookHandler } from '@hookflo/tern/nextjs';
416
190
 
417
- ```typescript
418
- // Next.js with explicit public key PEM as secret
419
191
  export const POST = createWebhookHandler({
420
192
  platform: 'falai',
421
- secret: process.env.FAL_PUBLIC_KEY_PEM!,
193
+ secret: '', // fal.ai resolves the public key automatically
422
194
  handler: async (payload, metadata) => ({ received: true, requestId: metadata.requestId }),
423
195
  });
424
196
  ```
425
197
 
426
- ## API Reference
427
-
428
- ### WebhookVerificationService
198
+ ## Key Features
429
199
 
430
- #### `verify(request: Request, config: WebhookConfig): Promise<WebhookVerificationResult>`
200
+ - **Queue + Retry Support** — optional Upstash QStash-based reliable inbound webhook delivery with automatic retries and deduplication
201
+ - **DLQ + Replay Controls** — list failed events, replay DLQ messages, and trigger replay-aware alerts
202
+ - **Alerting** — built-in Slack + Discord alerts through adapters and controls
203
+ - **Auto Platform Detection** — detect and verify across multiple providers via `verifyAny` with diagnostics on failure
204
+ - **Algorithm Agnostic** — HMAC-SHA256, HMAC-SHA1, HMAC-SHA512, ED25519, and custom algorithms
205
+ - **Zero Dependencies** — no bloat, no supply chain risk
206
+ - **Framework Agnostic** — works with Express, Next.js, Cloudflare Workers, Deno, and any runtime with Web Crypto
207
+ - **Body-Parser Safe** — reads raw bodies correctly to prevent signature mismatch
208
+ - **Strong TypeScript** — strict types, full inference, comprehensive type definitions
209
+ - **Stable Error Codes** — `INVALID_SIGNATURE`, `MISSING_SIGNATURE`, `TIMESTAMP_EXPIRED`, and more
431
210
 
432
- Verifies a webhook using the provided configuration.
211
+ ## Reliable Delivery & Alerting
433
212
 
434
- #### `verifyWithPlatformConfig(request: Request, platform: WebhookPlatform, secret: string, toleranceInSeconds?: number, normalize?: boolean | NormalizeOptions): Promise<WebhookVerificationResult>`
213
+ Tern supports both immediate and queue-based webhook processing. Queue mode is **optional and opt-in** — bring your own Upstash account (BYOK).
435
214
 
436
- Simplified verification using platform-specific configurations with optional payload normalization.
215
+ ### Non-queue mode (default)
437
216
 
438
- #### `verifyAny(request: Request, secrets: Record<string, string>, toleranceInSeconds?: number, normalize?: boolean | NormalizeOptions): Promise<WebhookVerificationResult>`
439
-
440
- Auto-detects platform from headers and verifies against one or more provider secrets.
217
+ ```typescript
218
+ import { createWebhookHandler } from '@hookflo/tern/nextjs';
441
219
 
442
- #### `verifyTokenAuth(request: Request, webhookId: string, webhookToken: string): Promise<WebhookVerificationResult>`
220
+ export const POST = createWebhookHandler({
221
+ platform: 'stripe',
222
+ secret: process.env.STRIPE_WEBHOOK_SECRET!,
223
+ handler: async (payload) => {
224
+ return { ok: true };
225
+ },
226
+ });
227
+ ```
443
228
 
444
- Verifies token-based webhooks.
229
+ ### Queue mode (opt-in)
445
230
 
446
- > `verifyTokenBased(...)` remains available as a backward-compatible alias and still works for existing integrations.
231
+ ```typescript
232
+ import { createWebhookHandler } from '@hookflo/tern/nextjs';
447
233
 
448
- #### `getPlatformsByCategory(category: 'payment' | 'auth' | 'ecommerce' | 'infrastructure'): WebhookPlatform[]`
234
+ export const POST = createWebhookHandler({
235
+ platform: 'stripe',
236
+ secret: process.env.STRIPE_WEBHOOK_SECRET!,
237
+ queue: true,
238
+ handler: async (payload, metadata) => {
239
+ return { processed: true, eventId: metadata.id };
240
+ },
241
+ });
242
+ ```
449
243
 
450
- Returns built-in providers that normalize into a shared schema for the given migration category.
244
+ ### Upstash Queue Setup
451
245
 
452
- ### Types
246
+ 1. Create a QStash project at [console.upstash.com/qstash](https://console.upstash.com/qstash)
247
+ 2. Copy your keys: `QSTASH_TOKEN`, `QSTASH_CURRENT_SIGNING_KEY`, `QSTASH_NEXT_SIGNING_KEY`
248
+ 3. Add them to your environment and set `queue: true`
249
+ 4. Enable queue with `queue: true` (or explicit queue config).
453
250
 
454
- #### `WebhookVerificationResult`
251
+ Direct queue config option:
455
252
 
456
253
  ```typescript
457
- interface WebhookVerificationResult {
458
- isValid: boolean;
459
- error?: string;
460
- errorCode?: WebhookErrorCode;
461
- platform: WebhookPlatform;
462
- payload?: any;
463
- eventId?: string; // canonical ID, e.g. 'stripe:evt_123'
464
- metadata?: {
465
- timestamp?: string;
466
- id?: string | null; // raw provider ID (legacy)
467
- [key: string]: any;
468
- };
254
+ queue: {
255
+ token: process.env.QSTASH_TOKEN!,
256
+ signingKey: process.env.QSTASH_CURRENT_SIGNING_KEY!,
257
+ nextSigningKey: process.env.QSTASH_NEXT_SIGNING_KEY!,
258
+ retries: 5,
469
259
  }
470
260
  ```
471
261
 
472
- #### `WebhookConfig`
262
+ ### Simple alerting
473
263
 
474
264
  ```typescript
475
- interface WebhookConfig {
476
- platform: WebhookPlatform;
477
- secret: string;
478
- toleranceInSeconds?: number;
479
- signatureConfig?: SignatureConfig;
480
- normalize?: boolean | NormalizeOptions;
481
- }
482
- ```
483
-
484
- ## Testing
485
-
486
- ### Run All Tests
487
-
488
- ```bash
489
- npm test
490
- ```
491
-
492
- ### Platform-Specific Testing
493
-
494
- ```bash
495
- # Test a specific platform
496
- npm run test:platform stripe
265
+ import { createWebhookHandler } from '@hookflo/tern/nextjs';
497
266
 
498
- # Test all platforms
499
- npm run test:all
267
+ export const POST = createWebhookHandler({
268
+ platform: 'stripe',
269
+ secret: process.env.STRIPE_WEBHOOK_SECRET!,
270
+ alerts: {
271
+ slack: { webhookUrl: process.env.SLACK_WEBHOOK_URL! },
272
+ discord: { webhookUrl: process.env.DISCORD_WEBHOOK_URL! },
273
+ },
274
+ handler: async () => ({ ok: true }),
275
+ });
500
276
  ```
501
277
 
502
- ### Documentation and Analysis
278
+ ### DLQ-aware alerting and replay
503
279
 
504
- ```bash
505
- # Fetch platform documentation
506
- npm run docs:fetch
280
+ ```typescript
281
+ import { createTernControls } from '@hookflo/tern/upstash';
507
282
 
508
- # Generate diffs between versions
509
- npm run docs:diff
283
+ const controls = createTernControls({
284
+ token: process.env.QSTASH_TOKEN!,
285
+ notifications: {
286
+ slackWebhookUrl: process.env.SLACK_WEBHOOK_URL,
287
+ discordWebhookUrl: process.env.DISCORD_WEBHOOK_URL,
288
+ },
289
+ });
510
290
 
511
- # Analyze changes and generate reports
512
- npm run docs:analyze
291
+ const dlqMessages = await controls.dlq();
292
+ if (dlqMessages.length > 0) {
293
+ await controls.alert({
294
+ dlq: true,
295
+ dlqId: dlqMessages[0].dlqId,
296
+ severity: 'warning',
297
+ message: 'Replay attempted for failed event',
298
+ });
299
+ }
513
300
  ```
514
301
 
515
- ## Examples
516
-
517
- See the [examples.ts](./src/examples.ts) file for comprehensive usage examples.
518
-
519
- ## Contributing
520
-
521
- We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for detailed information on how to:
302
+ ## Custom Platform Configuration
522
303
 
523
- - Set up your development environment
524
- - Add new platforms
525
- - Write tests
526
- - Submit pull requests
527
- - Follow our code style guidelines
304
+ Not built-in? Configure any webhook provider without waiting for a library update.
528
305
 
529
- ### Quick Start for Contributors
306
+ ```typescript
307
+ const result = await WebhookVerificationService.verify(request, {
308
+ platform: 'acmepay',
309
+ secret: 'acme_secret',
310
+ signatureConfig: {
311
+ algorithm: 'hmac-sha256',
312
+ headerName: 'x-acme-signature',
313
+ headerFormat: 'raw',
314
+ timestampHeader: 'x-acme-timestamp',
315
+ timestampFormat: 'unix',
316
+ payloadFormat: 'timestamped',
317
+ },
318
+ });
319
+ ```
530
320
 
531
- 1. Fork the repository
532
- 2. Clone your fork: `git clone https://github.com/your-username/tern.git`
533
- 3. Create a feature branch: `git checkout -b feature/your-feature-name`
534
- 4. Make your changes
535
- 5. Run tests: `npm test`
536
- 6. Submit a pull request
321
+ ### Svix / Standard Webhooks format (Clerk, Dodo Payments, ReplicateAI, etc.)
537
322
 
538
- ### Adding a New Platform
323
+ ```typescript
324
+ const svixConfig = {
325
+ platform: 'my-svix-platform',
326
+ secret: 'whsec_abc123...',
327
+ signatureConfig: {
328
+ algorithm: 'hmac-sha256',
329
+ headerName: 'webhook-signature',
330
+ headerFormat: 'raw',
331
+ timestampHeader: 'webhook-timestamp',
332
+ timestampFormat: 'unix',
333
+ payloadFormat: 'custom',
334
+ customConfig: {
335
+ payloadFormat: '{id}.{timestamp}.{body}',
336
+ idHeader: 'webhook-id',
337
+ },
338
+ },
339
+ };
340
+ ```
539
341
 
540
- See our [Platform Development Guide](CONTRIBUTING.md#adding-new-platforms) for step-by-step instructions on adding support for new webhook platforms.
342
+ See the [SignatureConfig type](https://tern.hookflo.com) for all options.
541
343
 
542
- ## Code of Conduct
344
+ ## API Reference
543
345
 
544
- This project adheres to our [Code of Conduct](CODE_OF_CONDUCT.md). Please read it before contributing.
346
+ ### `WebhookVerificationService`
545
347
 
546
- ## 📄 License
348
+ | Method | Description |
349
+ |---|---|
350
+ | `verify(request, config)` | Verify with full config object |
351
+ | `verifyWithPlatformConfig(request, platform, secret, tolerance?)` | Shorthand for built-in platforms |
352
+ | `verifyAny(request, secrets, tolerance?)` | Auto-detect platform and verify |
353
+ | `verifyTokenAuth(request, webhookId, webhookToken)` | Token-based verification |
354
+ | `verifyTokenBased(request, webhookId, webhookToken)` | Alias for `verifyTokenAuth` |
355
+ | `handleWithQueue(request, options)` | Core SDK helper for queue receive/process |
547
356
 
548
- MIT License - see [LICENSE](./LICENSE) for details.
357
+ ### `@hookflo/tern/upstash`
549
358
 
550
- ## 🔗 Links
359
+ | Export | Description |
360
+ |---|---|
361
+ | `createTernControls(config)` | Read DLQ/events, replay, and send alerts |
362
+ | `handleQueuedRequest(request, options)` | Route request between receive/process modes |
363
+ | `handleReceive(request, platform, secret, queueConfig, tolerance)` | Verify webhook and enqueue to QStash |
364
+ | `handleProcess(request, handler, queueConfig)` | Verify QStash signature and process payload |
365
+ | `resolveQueueConfig(queue)` | Resolve `queue: true` from env or explicit object |
551
366
 
552
- - [Documentation](./USAGE.md)
553
- - [Framework Summary](./FRAMEWORK_SUMMARY.md)
554
- - [Architecture Guide](./ARCHITECTURE.md)
555
- - [Issues](https://github.com/Hookflo/tern/issues)
367
+ ### `WebhookVerificationResult`
556
368
 
369
+ ```typescript
370
+ interface WebhookVerificationResult {
371
+ isValid: boolean;
372
+ error?: string;
373
+ errorCode?: string;
374
+ platform: WebhookPlatform;
375
+ payload?: any;
376
+ eventId?: string;
377
+ metadata?: {
378
+ timestamp?: string;
379
+ id?: string | null;
380
+ [key: string]: any;
381
+ };
382
+ }
383
+ ```
557
384
 
558
385
  ## Troubleshooting
559
386
 
560
- ### `Module not found: Can't resolve "@hookflo/tern/nextjs"`
561
-
562
- If this happens in a Next.js project, it usually means one of these:
563
-
564
- 1. You installed an older published package version that does not include subpath exports yet.
565
- 2. Lockfile still points to an old tarball/version.
566
- 3. `node_modules` cache is stale after upgrading.
567
-
568
- Fix steps:
387
+ **`Module not found: Can't resolve "@hookflo/tern/nextjs"`**
569
388
 
570
389
  ```bash
571
- # in your Next.js app
572
390
  npm i @hookflo/tern@latest
573
391
  rm -rf node_modules package-lock.json .next
574
392
  npm i
575
393
  ```
576
394
 
577
- Then verify resolution:
395
+ **Signature verification failing?**
396
+
397
+ Make sure you're passing the **raw** request body — not a parsed JSON object. Tern's framework adapters handle this automatically. If you're using the core service directly, ensure body parsers aren't consuming the stream before Tern does.
398
+
399
+ ## Contributing
400
+
401
+ Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for how to add platforms, write tests, and submit PRs.
578
402
 
579
403
  ```bash
580
- node -e "console.log(require.resolve('@hookflo/tern/nextjs'))"
404
+ git clone https://github.com/Hookflo/tern.git
405
+ cd tern
406
+ npm install
407
+ npm test
581
408
  ```
582
409
 
583
- If you are testing this repo locally before publish:
410
+ ## Support
584
411
 
585
- ```bash
586
- # inside /workspace/tern
587
- npm run build
588
- npm pack
412
+ Have a question, running into an issue, or want to request a platform? We're happy to help.
589
413
 
590
- # inside your other project
591
- npm i /path/to/hookflo-tern-<version>.tgz
592
- ```
414
+ Join the conversation on [Discord](https://discord.com/invite/SNmCjU97nr) or [open an issue](https://github.com/Hookflo/tern/issues) on GitHub — all questions, bug reports, and platform requests are welcome.
593
415
 
594
- Minimal Next.js App Router usage:
416
+ ## Links
595
417
 
596
- ```ts
597
- import { createWebhookHandler } from '@hookflo/tern/nextjs';
418
+ [Detailed Usage & Docs](https://tern.hookflo.com) · [npm Package](https://www.npmjs.com/package/@hookflo/tern) · [Discord Community](https://discord.com/invite/SNmCjU97nr) · [Issues](https://github.com/Hookflo/tern/issues)
598
419
 
599
- export const POST = createWebhookHandler({
600
- platform: 'stripe',
601
- secret: process.env.STRIPE_WEBHOOK_SECRET!,
602
- handler: async (payload) => ({ received: true, event: payload.event ?? payload.type }),
603
- });
604
- ```
420
+ ## License
421
+
422
+ MIT © [Hookflo](https://hookflo.com)