@hookflo/tern 4.2.9-beta → 4.3.0-beta.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
@@ -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)
7
9
 
8
- 💬 Join the discussion & contribute in our Discord: [Hookflo Community](https://discord.com/invite/SNmCjU97nr)
10
+ Stop writing webhook verification from scratch. **Tern** handles signature verification for Stripe, GitHub, Clerk, Shopify, and 15+ more platforms — with one consistent API.
11
+
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.
19
-
20
- **Runtime requirements:** Node.js 18+ (or any runtime with Web Crypto + Fetch APIs, such as Deno and Cloudflare Workers).
18
+ > The same framework powering webhook verification at [Hookflo](https://hookflo.com).
21
19
 
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" />
20
+ Star this repo to help others discover it · 💬 [Join our Discord](https://discord.com/invite/SNmCjU97nr)
23
21
 
24
- ## Features
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;" />
25
23
 
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
24
+ **Navigation**
28
25
 
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.)
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)
39
27
 
40
- ## Why Tern?
28
+ ## The Problem
41
29
 
42
- Most webhook verifiers are tightly coupled to specific platforms or hardcoded logic. Tern introduces a flexible, scalable, algorithm-first approach that:
30
+ Every webhook provider has a different signature format. You end up writing and maintaining the same verification boilerplate over and over:
43
31
 
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
-
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
- // }
146
- ```
147
-
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
- };
173
-
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
99
 
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
- };
100
+ // verified.payload + verified.metadata available here
315
101
  ```
316
102
 
317
- ## Framework Integration
103
+ ## Framework Integrations
318
104
 
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,171 +141,146 @@ 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
 
374
-
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.
383
-
384
- ### Platform route examples (Express / Next.js / Cloudflare)
151
+ ### Hono (Edge Runtimes)
385
152
 
386
153
  ```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 }));
154
+ import { Hono } from 'hono';
155
+ import { createWebhookHandler } from '@hookflo/tern/hono';
392
156
 
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
- });
157
+ const app = new Hono();
399
158
 
400
- // Cloudflare (Lemon Squeezy)
401
- const handleLemonSqueezy = createWebhookHandler({
402
- platform: 'lemonsqueezy',
403
- secretEnv: 'LEMON_SQUEEZY_WEBHOOK_SECRET',
404
- handler: async () => ({ received: true }),
405
- });
159
+ app.post('/webhooks/stripe', createWebhookHandler({
160
+ platform: 'stripe',
161
+ secret: process.env.STRIPE_WEBHOOK_SECRET!,
162
+ handler: async (payload, metadata, c) => c.json({
163
+ received: true,
164
+ eventId: metadata.id,
165
+ payload,
166
+ }),
167
+ }));
406
168
  ```
407
169
 
408
- ### fal.ai production usage
170
+ > All built-in platforms work across Express, Next.js, Cloudflare, and Hono adapters. You only change `platform` and `secret` per route.
409
171
 
410
- fal.ai uses **ED25519** (`x-fal-webhook-signature`) and signs:
411
- `{request-id}.{user-id}.{timestamp}.{sha256(body)}`.
172
+ ## Supported Platforms
412
173
 
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).
174
+ | Platform | Algorithm | Status |
175
+ |---|---|---|
176
+ | **Stripe** | HMAC-SHA256 | Tested |
177
+ | **GitHub** | HMAC-SHA256 | ✅ Tested |
178
+ | **Clerk** | HMAC-SHA256 (base64) | ✅ Tested |
179
+ | **Shopify** | HMAC-SHA256 (base64) | ✅ Tested |
180
+ | **Dodo Payments** | HMAC-SHA256 | ✅ Tested |
181
+ | **Paddle** | HMAC-SHA256 | ✅ Tested |
182
+ | **Lemon Squeezy** | HMAC-SHA256 | ✅ Tested |
183
+ | **Polar** | HMAC-SHA256 | ✅ Tested |
184
+ | **WorkOS** | HMAC-SHA256 | ✅ Tested |
185
+ | **ReplicateAI** | HMAC-SHA256 | ✅ Tested |
186
+ | **GitLab** | Token-based | ✅ Tested |
187
+ | **fal.ai** | ED25519 | ✅ Tested |
188
+ | **Sentry** | HMAC-SHA256 | ✅ Tested |
189
+ | **Grafana** | HMAC-SHA256 | ✅ Tested |
190
+ | **Doppler** | HMAC-SHA256 | ✅ Tested |
191
+ | **Sanity** | HMAC-SHA256 | ✅ Tested |
192
+ | **Razorpay** | HMAC-SHA256 | 🔄 Pending |
193
+ | **Vercel** | HMAC-SHA256 | 🔄 Pending |
194
+
195
+ > Don't see your platform? [Use custom config](#custom-platform-configuration) or [open an issue](https://github.com/Hookflo/tern/issues).
196
+
197
+ ### Platform signature notes
198
+
199
+ - **Standard Webhooks style** platforms (Clerk, Dodo Payments, Polar, ReplicateAI) commonly use a secret that starts with `whsec_...`.
200
+ - **ReplicateAI**: copy the webhook signing secret from your Replicate webhook settings and pass it directly as `secret`.
201
+ - **fal.ai**: supports JWKS key resolution out of the box — use `secret: ''` for auto key resolution, or pass a PEM public key explicitly.
202
+
203
+ ### Note on fal.ai
204
+
205
+ 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.
416
206
 
417
207
  ```typescript
418
- // Next.js with explicit public key PEM as secret
208
+ import { createWebhookHandler } from '@hookflo/tern/nextjs';
209
+
419
210
  export const POST = createWebhookHandler({
420
211
  platform: 'falai',
421
- secret: process.env.FAL_PUBLIC_KEY_PEM!,
212
+ secret: '', // fal.ai resolves the public key automatically
422
213
  handler: async (payload, metadata) => ({ received: true, requestId: metadata.requestId }),
423
214
  });
424
215
  ```
425
216
 
426
- ## API Reference
427
-
428
- ### WebhookVerificationService
429
-
430
- #### `verify(request: Request, config: WebhookConfig): Promise<WebhookVerificationResult>`
431
-
432
- Verifies a webhook using the provided configuration.
433
-
434
- #### `verifyWithPlatformConfig(request: Request, platform: WebhookPlatform, secret: string, toleranceInSeconds?: number, normalize?: boolean | NormalizeOptions): Promise<WebhookVerificationResult>`
435
-
436
- Simplified verification using platform-specific configurations with optional payload normalization.
437
-
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
+ ## Key Features
441
218
 
442
- #### `verifyTokenAuth(request: Request, webhookId: string, webhookToken: string): Promise<WebhookVerificationResult>`
219
+ - **Queue + Retry Support** optional Upstash QStash-based reliable inbound webhook delivery with automatic retries and deduplication
220
+ - **DLQ + Replay Controls** — list failed events, replay DLQ messages, and trigger replay-aware alerts
221
+ - **Alerting** — built-in Slack + Discord alerts through adapters and controls
222
+ - **Auto Platform Detection** — detect and verify across multiple providers via `verifyAny` with diagnostics on failure
223
+ - **Algorithm Agnostic** — HMAC-SHA256, HMAC-SHA1, HMAC-SHA512, ED25519, and custom algorithms
224
+ - **Zero Dependencies** — no bloat, no supply chain risk
225
+ - **Framework Agnostic** — works with Express, Next.js, Cloudflare Workers, Hono, Deno, Bun, and any runtime with Web Crypto
226
+ - **Body-Parser Safe** — reads raw bodies correctly to prevent signature mismatch
227
+ - **Strong TypeScript** — strict types, full inference, comprehensive type definitions
228
+ - **Stable Error Codes** — `INVALID_SIGNATURE`, `MISSING_SIGNATURE`, `TIMESTAMP_EXPIRED`, and more
443
229
 
444
- Verifies token-based webhooks.
230
+ ## Reliable Delivery & Alerting
445
231
 
446
- > `verifyTokenBased(...)` remains available as a backward-compatible alias and still works for existing integrations.
232
+ Tern supports both immediate and queue-based webhook processing. Queue mode is **optional and opt-in** bring your own Upstash account (BYOK).
447
233
 
448
- #### `getPlatformsByCategory(category: 'payment' | 'auth' | 'ecommerce' | 'infrastructure'): WebhookPlatform[]`
449
-
450
- Returns built-in providers that normalize into a shared schema for the given migration category.
451
-
452
- ### Types
453
-
454
- #### `WebhookVerificationResult`
234
+ ### Non-queue mode (default)
455
235
 
456
236
  ```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
- };
469
- }
470
- ```
471
-
472
- #### `WebhookConfig`
237
+ import { createWebhookHandler } from '@hookflo/tern/nextjs';
473
238
 
474
- ```typescript
475
- interface WebhookConfig {
476
- platform: WebhookPlatform;
477
- secret: string;
478
- toleranceInSeconds?: number;
479
- signatureConfig?: SignatureConfig;
480
- normalize?: boolean | NormalizeOptions;
481
- }
239
+ export const POST = createWebhookHandler({
240
+ platform: 'stripe',
241
+ secret: process.env.STRIPE_WEBHOOK_SECRET!,
242
+ handler: async (payload) => {
243
+ return { ok: true };
244
+ },
245
+ });
482
246
  ```
483
247
 
248
+ ### Queue mode (opt-in)
484
249
 
485
- ## Alerting (Slack + Discord)
486
-
487
- For the simplest DX, configure webhooks once in `createTernControls` and call `controls.alert(...)`.
488
-
489
- ```ts
490
- import { createTernControls } from '@hookflo/tern/upstash';
250
+ ```typescript
251
+ import { createWebhookHandler } from '@hookflo/tern/nextjs';
491
252
 
492
- const controls = createTernControls({
493
- token: process.env.QSTASH_TOKEN!,
494
- notifications: {
495
- slackWebhookUrl: process.env.SLACK_WEBHOOK_URL,
496
- discordWebhookUrl: process.env.DISCORD_WEBHOOK_URL,
253
+ export const POST = createWebhookHandler({
254
+ platform: 'stripe',
255
+ secret: process.env.STRIPE_WEBHOOK_SECRET!,
256
+ queue: true,
257
+ handler: async (payload, metadata) => {
258
+ return { processed: true, eventId: metadata.id };
497
259
  },
498
260
  });
499
-
500
- // Non-DLQ event alert with defaults
501
- await controls.alert();
502
-
503
- // DLQ alert + replay flow
504
- await controls.alert({
505
- dlq: true,
506
- dlqId: 'dlq_xxx',
507
- });
508
261
  ```
509
262
 
510
- ### Behavior
263
+ ### Upstash Queue Setup
511
264
 
512
- - `controls.alert()` with no params sends a normal (non-DLQ) alert with internal defaults.
513
- - `controls.alert({ dlq: true, dlqId })` sends a DLQ alert and attempts replay internally via `controls.replay(dlqId)`.
514
- - `eventId` is auto-filled from `dlqId` for DLQ alerts.
515
- - Optional overrides like `title`, `message`, `metadata`, `source`, or `branding` are used directly in Slack/Discord payloads.
265
+ 1. Create a QStash project at [console.upstash.com/qstash](https://console.upstash.com/qstash)
266
+ 2. Copy your keys: `QSTASH_TOKEN`, `QSTASH_CURRENT_SIGNING_KEY`, `QSTASH_NEXT_SIGNING_KEY`
267
+ 3. Add them to your environment and set `queue: true`
268
+ 4. Enable queue with `queue: true` (or explicit queue config).
516
269
 
517
- ### Adapter-level alerts (works with and without queue)
270
+ Direct queue config option:
271
+
272
+ ```typescript
273
+ queue: {
274
+ token: process.env.QSTASH_TOKEN!,
275
+ signingKey: process.env.QSTASH_CURRENT_SIGNING_KEY!,
276
+ nextSigningKey: process.env.QSTASH_NEXT_SIGNING_KEY!,
277
+ retries: 5,
278
+ }
279
+ ```
518
280
 
519
- If you are not using Upstash controls, you can enable alerting directly in adapters by providing `alerts`.
520
- This works in **both queue and non-queue** modes.
281
+ ### Simple alerting
521
282
 
522
- ```ts
283
+ ```typescript
523
284
  import { createWebhookHandler } from '@hookflo/tern/nextjs';
524
285
 
525
286
  export const POST = createWebhookHandler({
@@ -529,142 +290,152 @@ export const POST = createWebhookHandler({
529
290
  slack: { webhookUrl: process.env.SLACK_WEBHOOK_URL! },
530
291
  discord: { webhookUrl: process.env.DISCORD_WEBHOOK_URL! },
531
292
  },
532
- alert: {
533
- title: 'Alert Recieved',
534
- message: 'Alert received in handler',
535
- },
536
- handler: async (payload, metadata) => {
537
- return { ok: true };
538
- },
293
+ handler: async () => ({ ok: true }),
539
294
  });
540
295
  ```
541
296
 
542
- - In non-queue mode, alerts include `source` (platform) and canonical `eventId` from verification.
543
- - In queue mode, normal alerts are sent on successful enqueue (DLQ alerting remains Upstash-controls based).
544
- - Adapter-level alert calls do **not** auto-inject metadata; pass explicit alert fields via `alert` for predictable payloads.
545
-
546
- ### Core SDK queue + alerts
297
+ ### DLQ-aware alerting and replay
547
298
 
548
- `WebhookVerificationService.handleWithQueue(...)` also supports alerting through the same `alerts` + `alert` options, so core SDK users get the same behavior as framework adapters.
549
-
550
- ## Testing
299
+ ```typescript
300
+ import { createTernControls } from '@hookflo/tern/upstash';
551
301
 
552
- ### Run All Tests
302
+ const controls = createTernControls({
303
+ token: process.env.QSTASH_TOKEN!,
304
+ notifications: {
305
+ slackWebhookUrl: process.env.SLACK_WEBHOOK_URL,
306
+ discordWebhookUrl: process.env.DISCORD_WEBHOOK_URL,
307
+ },
308
+ });
553
309
 
554
- ```bash
555
- npm test
310
+ const dlqMessages = await controls.dlq();
311
+ if (dlqMessages.length > 0) {
312
+ await controls.alert({
313
+ dlq: true,
314
+ dlqId: dlqMessages[0].dlqId,
315
+ severity: 'warning',
316
+ message: 'Replay attempted for failed event',
317
+ });
318
+ }
556
319
  ```
557
320
 
558
- ### Platform-Specific Testing
321
+ ## Custom Platform Configuration
559
322
 
560
- ```bash
561
- # Test a specific platform
562
- npm run test:platform stripe
323
+ Not built-in? Configure any webhook provider without waiting for a library update.
563
324
 
564
- # Test all platforms
565
- npm run test:all
325
+ ```typescript
326
+ const result = await WebhookVerificationService.verify(request, {
327
+ platform: 'acmepay',
328
+ secret: 'acme_secret',
329
+ signatureConfig: {
330
+ algorithm: 'hmac-sha256',
331
+ headerName: 'x-acme-signature',
332
+ headerFormat: 'raw',
333
+ timestampHeader: 'x-acme-timestamp',
334
+ timestampFormat: 'unix',
335
+ payloadFormat: 'timestamped',
336
+ },
337
+ });
566
338
  ```
567
339
 
568
- ### Documentation and Analysis
569
-
570
- ```bash
571
- # Fetch platform documentation
572
- npm run docs:fetch
573
-
574
- # Generate diffs between versions
575
- npm run docs:diff
340
+ ### Svix / Standard Webhooks format (Clerk, Dodo Payments, ReplicateAI, etc.)
576
341
 
577
- # Analyze changes and generate reports
578
- npm run docs:analyze
342
+ ```typescript
343
+ const svixConfig = {
344
+ platform: 'my-svix-platform',
345
+ secret: 'whsec_abc123...',
346
+ signatureConfig: {
347
+ algorithm: 'hmac-sha256',
348
+ headerName: 'webhook-signature',
349
+ headerFormat: 'raw',
350
+ timestampHeader: 'webhook-timestamp',
351
+ timestampFormat: 'unix',
352
+ payloadFormat: 'custom',
353
+ customConfig: {
354
+ payloadFormat: '{id}.{timestamp}.{body}',
355
+ idHeader: 'webhook-id',
356
+ },
357
+ },
358
+ };
579
359
  ```
580
360
 
581
- ## Examples
582
-
583
- See the [examples.ts](./src/examples.ts) file for comprehensive usage examples.
584
-
585
- ## Contributing
586
-
587
- We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for detailed information on how to:
588
-
589
- - Set up your development environment
590
- - Add new platforms
591
- - Write tests
592
- - Submit pull requests
593
- - Follow our code style guidelines
361
+ See the [SignatureConfig type](https://tern.hookflo.com) for all options.
594
362
 
595
- ### Quick Start for Contributors
596
-
597
- 1. Fork the repository
598
- 2. Clone your fork: `git clone https://github.com/your-username/tern.git`
599
- 3. Create a feature branch: `git checkout -b feature/your-feature-name`
600
- 4. Make your changes
601
- 5. Run tests: `npm test`
602
- 6. Submit a pull request
603
-
604
- ### Adding a New Platform
605
-
606
- See our [Platform Development Guide](CONTRIBUTING.md#adding-new-platforms) for step-by-step instructions on adding support for new webhook platforms.
607
-
608
- ## Code of Conduct
363
+ ## API Reference
609
364
 
610
- This project adheres to our [Code of Conduct](CODE_OF_CONDUCT.md). Please read it before contributing.
365
+ ### `WebhookVerificationService`
611
366
 
612
- ## 📄 License
367
+ | Method | Description |
368
+ |---|---|
369
+ | `verify(request, config)` | Verify with full config object |
370
+ | `verifyWithPlatformConfig(request, platform, secret, tolerance?)` | Shorthand for built-in platforms |
371
+ | `verifyAny(request, secrets, tolerance?)` | Auto-detect platform and verify |
372
+ | `verifyTokenAuth(request, webhookId, webhookToken)` | Token-based verification |
373
+ | `verifyTokenBased(request, webhookId, webhookToken)` | Alias for `verifyTokenAuth` |
374
+ | `handleWithQueue(request, options)` | Core SDK helper for queue receive/process |
613
375
 
614
- MIT License - see [LICENSE](./LICENSE) for details.
376
+ ### `@hookflo/tern/upstash`
615
377
 
616
- ## 🔗 Links
378
+ | Export | Description |
379
+ |---|---|
380
+ | `createTernControls(config)` | Read DLQ/events, replay, and send alerts |
381
+ | `handleQueuedRequest(request, options)` | Route request between receive/process modes |
382
+ | `handleReceive(request, platform, secret, queueConfig, tolerance)` | Verify webhook and enqueue to QStash |
383
+ | `handleProcess(request, handler, queueConfig)` | Verify QStash signature and process payload |
384
+ | `resolveQueueConfig(queue)` | Resolve `queue: true` from env or explicit object |
617
385
 
618
- - [Documentation](./USAGE.md)
619
- - [Framework Summary](./FRAMEWORK_SUMMARY.md)
620
- - [Architecture Guide](./ARCHITECTURE.md)
621
- - [Issues](https://github.com/Hookflo/tern/issues)
386
+ ### `WebhookVerificationResult`
622
387
 
388
+ ```typescript
389
+ interface WebhookVerificationResult {
390
+ isValid: boolean;
391
+ error?: string;
392
+ errorCode?: string;
393
+ platform: WebhookPlatform;
394
+ payload?: any;
395
+ eventId?: string;
396
+ metadata?: {
397
+ timestamp?: string;
398
+ id?: string | null;
399
+ [key: string]: any;
400
+ };
401
+ }
402
+ ```
623
403
 
624
404
  ## Troubleshooting
625
405
 
626
- ### `Module not found: Can't resolve "@hookflo/tern/nextjs"`
627
-
628
- If this happens in a Next.js project, it usually means one of these:
629
-
630
- 1. You installed an older published package version that does not include subpath exports yet.
631
- 2. Lockfile still points to an old tarball/version.
632
- 3. `node_modules` cache is stale after upgrading.
633
-
634
- Fix steps:
406
+ **`Module not found: Can't resolve "@hookflo/tern/nextjs"`**
635
407
 
636
408
  ```bash
637
- # in your Next.js app
638
409
  npm i @hookflo/tern@latest
639
410
  rm -rf node_modules package-lock.json .next
640
411
  npm i
641
412
  ```
642
413
 
643
- Then verify resolution:
414
+ **Signature verification failing?**
415
+
416
+ 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.
417
+
418
+ ## Contributing
419
+
420
+ Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for how to add platforms, write tests, and submit PRs.
644
421
 
645
422
  ```bash
646
- node -e "console.log(require.resolve('@hookflo/tern/nextjs'))"
423
+ git clone https://github.com/Hookflo/tern.git
424
+ cd tern
425
+ npm install
426
+ npm test
647
427
  ```
648
428
 
649
- If you are testing this repo locally before publish:
429
+ ## Support
650
430
 
651
- ```bash
652
- # inside /workspace/tern
653
- npm run build
654
- npm pack
431
+ Have a question, running into an issue, or want to request a platform? We're happy to help.
655
432
 
656
- # inside your other project
657
- npm i /path/to/hookflo-tern-<version>.tgz
658
- ```
433
+ 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.
659
434
 
660
- Minimal Next.js App Router usage:
435
+ ## Links
661
436
 
662
- ```ts
663
- import { createWebhookHandler } from '@hookflo/tern/nextjs';
437
+ [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)
664
438
 
665
- export const POST = createWebhookHandler({
666
- platform: 'stripe',
667
- secret: process.env.STRIPE_WEBHOOK_SECRET!,
668
- handler: async (payload) => ({ received: true, event: payload.event ?? payload.type }),
669
- });
670
- ```
439
+ ## License
440
+
441
+ MIT © [Hookflo](https://hookflo.com)
@@ -0,0 +1,21 @@
1
+ import { WebhookPlatform, NormalizeOptions } from '../types';
2
+ import { QueueOption } from '../upstash/types';
3
+ import type { AlertConfig, SendAlertOptions } from '../notifications/types';
4
+ export interface HonoContextLike {
5
+ req: {
6
+ raw: Request;
7
+ };
8
+ json: (payload: unknown, status?: number) => Response;
9
+ }
10
+ export interface HonoWebhookHandlerOptions<TContext extends HonoContextLike = HonoContextLike, TPayload = any, TMetadata extends Record<string, unknown> = Record<string, unknown>, TResponse = unknown> {
11
+ platform: WebhookPlatform;
12
+ secret: string;
13
+ toleranceInSeconds?: number;
14
+ normalize?: boolean | NormalizeOptions;
15
+ queue?: QueueOption;
16
+ alerts?: AlertConfig;
17
+ alert?: Omit<SendAlertOptions, 'dlq' | 'dlqId' | 'source' | 'eventId'>;
18
+ onError?: (error: Error) => void;
19
+ handler: (payload: TPayload, metadata: TMetadata, c: TContext) => Promise<TResponse> | TResponse;
20
+ }
21
+ export declare function createWebhookHandler<TContext extends HonoContextLike = HonoContextLike, TPayload = any, TMetadata extends Record<string, unknown> = Record<string, unknown>, TResponse = unknown>(options: HonoWebhookHandlerOptions<TContext, TPayload, TMetadata, TResponse>): (c: TContext) => Promise<Response>;
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createWebhookHandler = createWebhookHandler;
4
+ const index_1 = require("../index");
5
+ const queue_1 = require("../upstash/queue");
6
+ const dispatch_1 = require("../notifications/dispatch");
7
+ function createWebhookHandler(options) {
8
+ return async (c) => {
9
+ try {
10
+ const request = c.req.raw;
11
+ if (options.queue) {
12
+ const queueConfig = (0, queue_1.resolveQueueConfig)(options.queue);
13
+ const response = await (0, queue_1.handleQueuedRequest)(request, {
14
+ platform: options.platform,
15
+ secret: options.secret,
16
+ queueConfig,
17
+ handler: (payload, metadata) => options.handler(payload, metadata, c),
18
+ toleranceInSeconds: options.toleranceInSeconds ?? 300,
19
+ });
20
+ if (response.ok) {
21
+ let eventId;
22
+ try {
23
+ const body = await response.clone().json();
24
+ eventId = typeof body.eventId === 'string' ? body.eventId : undefined;
25
+ }
26
+ catch {
27
+ eventId = undefined;
28
+ }
29
+ await (0, dispatch_1.dispatchWebhookAlert)({
30
+ alerts: options.alerts,
31
+ source: options.platform,
32
+ eventId,
33
+ alert: options.alert,
34
+ });
35
+ }
36
+ return response;
37
+ }
38
+ const result = await index_1.WebhookVerificationService.verifyWithPlatformConfig(request, options.platform, options.secret, options.toleranceInSeconds, options.normalize);
39
+ if (!result.isValid) {
40
+ return c.json({
41
+ error: result.error,
42
+ errorCode: result.errorCode,
43
+ platform: result.platform,
44
+ metadata: result.metadata,
45
+ }, 400);
46
+ }
47
+ await (0, dispatch_1.dispatchWebhookAlert)({
48
+ alerts: options.alerts,
49
+ source: options.platform,
50
+ eventId: result.eventId,
51
+ alert: options.alert,
52
+ });
53
+ const data = await options.handler(result.payload, (result.metadata || {}), c);
54
+ if (data instanceof Response) {
55
+ return data;
56
+ }
57
+ return c.json(data);
58
+ }
59
+ catch (error) {
60
+ if (options.onError) {
61
+ options.onError(error);
62
+ }
63
+ else {
64
+ console.error('[tern/hono]', error);
65
+ }
66
+ return c.json({ error: error.message }, 500);
67
+ }
68
+ };
69
+ }
@@ -2,3 +2,4 @@ export { createWebhookMiddleware, ExpressWebhookMiddlewareOptions, ExpressLikeRe
2
2
  export { createWebhookHandler as createNextjsWebhookHandler, NextWebhookHandlerOptions, } from './nextjs';
3
3
  export { createWebhookHandler as createCloudflareWebhookHandler, CloudflareWebhookHandlerOptions, } from './cloudflare';
4
4
  export { toWebRequest, extractRawBody } from './shared';
5
+ export { createWebhookHandler as createHonoWebhookHandler, HonoWebhookHandlerOptions, HonoContextLike, } from './hono';
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.extractRawBody = exports.toWebRequest = exports.createCloudflareWebhookHandler = exports.createNextjsWebhookHandler = exports.createWebhookMiddleware = void 0;
3
+ exports.createHonoWebhookHandler = exports.extractRawBody = exports.toWebRequest = exports.createCloudflareWebhookHandler = exports.createNextjsWebhookHandler = exports.createWebhookMiddleware = void 0;
4
4
  var express_1 = require("./express");
5
5
  Object.defineProperty(exports, "createWebhookMiddleware", { enumerable: true, get: function () { return express_1.createWebhookMiddleware; } });
6
6
  var nextjs_1 = require("./nextjs");
@@ -10,3 +10,5 @@ Object.defineProperty(exports, "createCloudflareWebhookHandler", { enumerable: t
10
10
  var shared_1 = require("./shared");
11
11
  Object.defineProperty(exports, "toWebRequest", { enumerable: true, get: function () { return shared_1.toWebRequest; } });
12
12
  Object.defineProperty(exports, "extractRawBody", { enumerable: true, get: function () { return shared_1.extractRawBody; } });
13
+ var hono_1 = require("./hono");
14
+ Object.defineProperty(exports, "createHonoWebhookHandler", { enumerable: true, get: function () { return hono_1.createWebhookHandler; } });
package/dist/hono.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { createWebhookHandler } from './adapters/hono';
2
+ export type { HonoContextLike, HonoWebhookHandlerOptions, } from './adapters/hono';
package/dist/hono.js ADDED
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createWebhookHandler = void 0;
4
+ var hono_1 = require("./adapters/hono");
5
+ Object.defineProperty(exports, "createWebhookHandler", { enumerable: true, get: function () { return hono_1.createWebhookHandler; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hookflo/tern",
3
- "version": "4.2.9-beta",
3
+ "version": "4.3.0-beta.0",
4
4
  "description": "A robust, scalable webhook verification framework supporting multiple platforms and signature algorithms",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -32,7 +32,9 @@
32
32
  "typescript",
33
33
  "node",
34
34
  "express",
35
- "nextjs"
35
+ "nextjs",
36
+ "cloudflare",
37
+ "hono"
36
38
  ],
37
39
  "author": "Prateek Jain",
38
40
  "license": "MIT",
@@ -102,6 +104,12 @@
102
104
  "require": "./dist/upstash/index.js",
103
105
  "import": "./dist/upstash/index.js",
104
106
  "default": "./dist/upstash/index.js"
107
+ },
108
+ "./hono": {
109
+ "types": "./dist/hono.d.ts",
110
+ "require": "./dist/hono.js",
111
+ "import": "./dist/hono.js",
112
+ "default": "./dist/hono.js"
105
113
  }
106
114
  },
107
115
  "typesVersions": {
@@ -120,6 +128,9 @@
120
128
  ],
121
129
  "upstash": [
122
130
  "dist/upstash/index.d.ts"
131
+ ],
132
+ "hono": [
133
+ "dist/hono.d.ts"
123
134
  ]
124
135
  }
125
136
  },