hono-webhooks 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kristoffer Hansen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,587 @@
1
+ # hono-webhooks
2
+
3
+ Type-safe webhook endpoints for [Hono](https://hono.dev). Define each webhook
4
+ once with its schemas, get typed `params`, `query`, `headers` and `body` in the
5
+ handler, and mount them all as one Hono app. You can use any
6
+ [Standard Schema](https://standardschema.dev) library: Zod, Valibot, ArkType
7
+ and others.
8
+
9
+ ```ts
10
+ import { Hono } from 'hono'
11
+ import { createWebhooks, defineWebhook } from 'hono-webhooks'
12
+ import { z } from 'zod'
13
+
14
+ const orderPaid = defineWebhook(
15
+ {
16
+ method: 'POST',
17
+ path: '/shop/orders/:orderId/paid',
18
+ body: z.object({ amount: z.number(), currency: z.string() })
19
+ },
20
+ async ({ params, body }) => {
21
+ await markPaid(params.orderId, body.amount, body.currency)
22
+ return { received: true }
23
+ }
24
+ )
25
+
26
+ const app = new Hono()
27
+ app.route('/webhooks', createWebhooks([orderPaid]))
28
+ ```
29
+
30
+ - **Typed from the definition.** `params.orderId` is a `string` because it is
31
+ in the path, `body` has the schema's output type, and the handler's return
32
+ value is checked against the `response` schema.
33
+ - **Any schema library.** Zod, Valibot, ArkType, Effect Schema, or anything
34
+ else that implements Standard Schema. Async schemas work too.
35
+ - **Raw body included.** `rawBody` is always passed next to the parsed body,
36
+ so you can verify signatures (Stripe, GitHub, Shopify, ...) without extra
37
+ setup.
38
+ - **Plain Hono.** `createWebhooks` returns a Hono app. Handlers get the Hono
39
+ context `c`, Hono middleware works, `HTTPException` works, and it runs on
40
+ every runtime Hono supports.
41
+ - **One hook for observability.** `onEvent` fires once per request with the
42
+ route, status, duration, validated input, response and error. Use it for
43
+ logs, metrics, traces or an audit table.
44
+ - **A CLI** to list webhooks, print their JSON Schemas and send test requests.
45
+
46
+ ## Contents
47
+
48
+ - [Install](#install)
49
+ - [Defining webhooks](#defining-webhooks)
50
+ - [Handler input](#handler-input)
51
+ - [Return values](#return-values)
52
+ - [Body parsing](#body-parsing)
53
+ - [Mounting](#mounting)
54
+ - [Typed env and middleware](#typed-env-and-middleware)
55
+ - [Errors](#errors)
56
+ - [Observability with `onEvent`](#observability-with-onevent)
57
+ - [Recipes](#recipes)
58
+ - [Testing](#testing)
59
+ - [CLI](#cli)
60
+ - [API reference](#api-reference)
61
+ - [Migrating from `createHttpRoute`](#migrating-from-createhttproute)
62
+ - [Releasing](#releasing)
63
+
64
+ ## Install
65
+
66
+ ```bash
67
+ bun add hono-webhooks hono
68
+ # plus the schema library you use
69
+ bun add zod
70
+ ```
71
+
72
+ `hono` (4.6 or newer) is a peer dependency. The package is ESM only. Bun
73
+ loads the TypeScript source directly; Node and bundlers load the built files
74
+ from `dist`.
75
+
76
+ ## Defining webhooks
77
+
78
+ `defineWebhook(config, handler)` returns a webhook definition. It does not
79
+ register anything; pass the definitions to `createWebhooks`.
80
+
81
+ ```ts
82
+ import { defineWebhook } from 'hono-webhooks'
83
+ import * as v from 'valibot'
84
+
85
+ export const updateItem = defineWebhook(
86
+ {
87
+ method: 'PUT',
88
+ path: '/items/:id',
89
+ description: 'Update an item from the ERP',
90
+ params: v.object({ id: v.pipe(v.string(), v.transform(Number)) }),
91
+ query: v.object({ dryRun: v.optional(v.picklist(['true', 'false'])) }),
92
+ headers: v.object({ 'x-api-key': v.string() }),
93
+ body: v.object({ name: v.string(), price: v.number() }),
94
+ response: v.object({ id: v.number(), updated: v.boolean() })
95
+ },
96
+ async ({ params, query, headers, body }) => {
97
+ // params.id: number, body.price: number, headers['x-api-key']: string
98
+ return { id: params.id, updated: query.dryRun !== 'true' }
99
+ }
100
+ )
101
+ ```
102
+
103
+ | Option | Type | Description |
104
+ | ------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
105
+ | `method` | `'GET' \| 'POST' \| 'PUT' \| 'PATCH' \| 'DELETE' \| 'OPTIONS'` | HTTP method. `HEAD` is answered by the `GET` route automatically. |
106
+ | `path` | `string` | A [Hono path](https://hono.dev/docs/api/routing): `/a/:id`, `/a/:id?`, `/a/:id{[0-9]+}`, `/a/*`. |
107
+ | `params` | Standard Schema | Validates path params. Without it, `params` is typed from `path`. |
108
+ | `query` | Standard Schema | Validates the query string as `Record<string, string>`. |
109
+ | `headers` | Standard Schema | Validates headers as `Record<string, string>` with **lower-case** names. |
110
+ | `body` | Standard Schema | Validates the parsed body (see [Body parsing](#body-parsing)). |
111
+ | `response` | Standard Schema | Types the handler's return value and checks it at runtime (see [Return values](#return-values)). |
112
+ | `bodyType` | `'auto' \| 'json' \| 'form' \| 'text'` | How to parse the body. Default `'auto'`. |
113
+ | `description` | `string` | Shown by `hono-webhooks list`. |
114
+
115
+ Schemas can transform: the handler receives the schema's **output**, so
116
+ `z.coerce.number()` on a param gives you a `number`.
117
+
118
+ ### Params from the path
119
+
120
+ Without a `params` schema, the path gives the types:
121
+
122
+ ```ts
123
+ defineWebhook({ method: 'GET', path: '/repos/:owner/:repo/:ref?' }, ({ params }) => {
124
+ params.owner // string
125
+ params.ref // string | undefined
126
+ params.nope // type error
127
+ })
128
+ ```
129
+
130
+ ## Handler input
131
+
132
+ The handler receives one object:
133
+
134
+ | Field | Type | Description |
135
+ | --------- | ------------------------------------------------------ | ------------------------------------------------------------------- |
136
+ | `c` | `Context<E>` | The Hono context: `c.env`, `c.var`, `c.header()`, `c.req.raw`, ... |
137
+ | `params` | schema output, or typed from `path` | Path params, URL-decoded. |
138
+ | `query` | schema output, or `Record<string, string>` | Query string. Repeated keys keep the first value. |
139
+ | `headers` | schema output, or `Record<string, string>` | Request headers, lower-case names. |
140
+ | `body` | schema output, or `unknown` | Parsed body. `undefined` when the body is empty. |
141
+ | `rawBody` | `string` | The body exactly as received. `''` when empty. |
142
+
143
+ ## Return values
144
+
145
+ | Handler returns | Response |
146
+ | ----------------- | -------------------------------------------------------------------- |
147
+ | a `Response` | Sent as is. Use `c.json(data, 201)`, `c.redirect()`, `new Response()` |
148
+ | `undefined` | `204 No Content` |
149
+ | a `string` | `200`, `text/plain` |
150
+ | anything else | `200`, JSON |
151
+
152
+ With a `response` schema, the return type is checked at compile time. At
153
+ runtime the value is also validated. If it fails, the response is **still
154
+ sent** so the caller isn't affected, and the failure is reported on
155
+ [`onEvent`](#observability-with-onevent) as `event.validation` with
156
+ `target: 'response'`.
157
+
158
+ ## Body parsing
159
+
160
+ The body is read once as text (`rawBody`), then parsed according to
161
+ `bodyType`:
162
+
163
+ | `bodyType` | Parsed as |
164
+ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
165
+ | `auto` (default) | Depends on `Content-Type`: `application/json` and any `+json` type → JSON; `application/x-www-form-urlencoded` → `Record<string, string>`; anything else → `string` |
166
+ | `json` | JSON, whatever the `Content-Type` says. Use it for providers that send JSON without the right header. |
167
+ | `form` | URL-encoded form (for example Twilio) |
168
+ | `text` | The raw string |
169
+
170
+ An empty body is `undefined` (the schema decides whether that's allowed).
171
+ Malformed JSON is a `400` with `issues: [{ message: 'Malformed JSON' }]`.
172
+
173
+ ## Mounting
174
+
175
+ `createWebhooks(webhooks, options?)` returns a Hono app. Mount it under a
176
+ prefix with `app.route()`:
177
+
178
+ ```ts
179
+ const app = new Hono()
180
+ app.route('/webhooks', createWebhooks([stripe, github, health], options))
181
+ ```
182
+
183
+ Below that prefix:
184
+
185
+ - An unknown path answers `404 { error: 'Route not found' }`.
186
+ - A known path with a method that isn't defined answers
187
+ `405 { error: 'Method DELETE not allowed' }` with an `Allow` header.
188
+ - A static path wins over a dynamic one that also matches (`/users/me` beats
189
+ `/users/:id`) no matter which order they were passed in.
190
+ - Defining the same method and path twice throws when `createWebhooks` runs.
191
+
192
+ Because the webhooks app answers 404 for anything under its prefix, mount it
193
+ under its own prefix (`/webhooks`), not at `/`.
194
+
195
+ The definitions are available as `webhooksApp.webhooks`, which is handy for
196
+ generating docs.
197
+
198
+ ## Typed env and middleware
199
+
200
+ `defineWebhook` and `createWebhooks` type `c` as `Context<any>`. To type
201
+ `c.env` (Cloudflare bindings) and `c.var` (values set by middleware), make a
202
+ factory once and use it everywhere:
203
+
204
+ ```ts
205
+ // src/webhooks/factory.ts
206
+ import { createWebhookFactory } from 'hono-webhooks'
207
+
208
+ export type AppEnv = {
209
+ Bindings: { STRIPE_WEBHOOK_SECRET: string; DB: D1Database }
210
+ Variables: { requestId: string }
211
+ }
212
+
213
+ export const { defineWebhook, createWebhooks } = createWebhookFactory<AppEnv>()
214
+ ```
215
+
216
+ ```ts
217
+ defineWebhook({ method: 'POST', path: '/ping' }, ({ c }) => {
218
+ c.env.DB // D1Database
219
+ c.var.requestId // string
220
+ })
221
+ ```
222
+
223
+ Middleware goes on the parent app, before `app.route()`, so it runs before
224
+ the webhooks (Hono runs handlers in the order they were registered):
225
+
226
+ ```ts
227
+ const app = new Hono<AppEnv>()
228
+ app.use('/webhooks/*', requestId())
229
+ app.use('/webhooks/admin/*', bearerAuth({ token }))
230
+ app.route('/webhooks', createWebhooks(webhooks))
231
+ ```
232
+
233
+ ## Errors
234
+
235
+ Anything thrown while handling a request becomes a response:
236
+
237
+ | Thrown | Default response |
238
+ | ---------------------------------------------------- | ------------------------------------------------- |
239
+ | `WebhookValidationError` (a request schema failed) | `400 { error: 'Invalid body', issues: [...] }` |
240
+ | Hono's `HTTPException` | Its status and `{ error: message }`, or its `res` |
241
+ | anything else | `500 { error: 'Internal Server Error' }`, and the error is logged with `console.error` |
242
+
243
+ `issues` is the Standard Schema issue list: `{ message, path? }[]`.
244
+
245
+ Throw an `HTTPException` from a handler to answer with another status:
246
+
247
+ ```ts
248
+ import { HTTPException } from 'hono/http-exception'
249
+
250
+ defineWebhook({ method: 'POST', path: '/orders/:id' }, async ({ params }) => {
251
+ const order = await findOrder(params.id)
252
+ if (!order) throw new HTTPException(404, { message: 'Unknown order' })
253
+ // ...
254
+ })
255
+ ```
256
+
257
+ `onError` changes the response for any error, including 404 and 405. Return a
258
+ `Response`, or nothing to fall back to the default. `console.error` is only
259
+ called when you don't pass `onError`.
260
+
261
+ ```ts
262
+ import { WebhookValidationError } from 'hono-webhooks'
263
+
264
+ createWebhooks(webhooks, {
265
+ onError(error, c) {
266
+ if (error instanceof WebhookValidationError) {
267
+ return c.json({ type: 'validation', target: error.target, issues: error.issues }, 422)
268
+ }
269
+ logger.error(error)
270
+ // return nothing: default response
271
+ }
272
+ })
273
+ ```
274
+
275
+ ## Observability with `onEvent`
276
+
277
+ `onEvent` is called once per request after the response is built, for
278
+ successes, validation failures, handler errors, 404s and 405s.
279
+
280
+ ```ts
281
+ createWebhooks(webhooks, {
282
+ onEvent(event) {
283
+ logger.info('webhook', {
284
+ route: event.route, // '/orders/:id', undefined for a 404
285
+ method: event.method,
286
+ path: event.path, // '/webhooks/orders/42'
287
+ status: event.status,
288
+ durationMs: event.durationMs
289
+ })
290
+ }
291
+ })
292
+ ```
293
+
294
+ | Field | Description |
295
+ | ------------------------------------------ | ---------------------------------------------------------------------------------------------------- |
296
+ | `c` | The Hono context, e.g. `c.req.header('user-agent')` |
297
+ | `route` | Matched path pattern, `undefined` for a 404 |
298
+ | `method`, `path` | Request method and path |
299
+ | `status`, `durationMs` | Response status and time spent in the webhooks app |
300
+ | `params`, `query`, `headers`, `body`, `rawBody` | Each is set once it passed validation, so a request that fails on `body` still reports `params` |
301
+ | `response` | What the handler returned, unless it returned a `Response` |
302
+ | `error` | Whatever was thrown |
303
+ | `validation` | `{ target, issues }` when a request part or the response failed its schema |
304
+
305
+ `onEvent` isn't awaited, so a slow database write doesn't delay the response.
306
+ When it returns a promise, the promise is passed to
307
+ `c.executionCtx.waitUntil()` on runtimes that have one (Cloudflare Workers,
308
+ Vercel Edge), and a rejection is logged instead of crashing the request.
309
+
310
+ ## Recipes
311
+
312
+ ### Stripe signature verification
313
+
314
+ `rawBody` is the exact payload Stripe signed.
315
+
316
+ ```ts
317
+ import Stripe from 'stripe'
318
+ import { HTTPException } from 'hono/http-exception'
319
+
320
+ const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
321
+
322
+ export const stripeEvents = defineWebhook(
323
+ {
324
+ method: 'POST',
325
+ path: '/stripe',
326
+ headers: z.object({ 'stripe-signature': z.string() })
327
+ },
328
+ async ({ headers, rawBody }) => {
329
+ let event: Stripe.Event
330
+ try {
331
+ event = await stripe.webhooks.constructEventAsync(
332
+ rawBody,
333
+ headers['stripe-signature'],
334
+ process.env.STRIPE_WEBHOOK_SECRET!
335
+ )
336
+ } catch {
337
+ throw new HTTPException(400, { message: 'Invalid signature' })
338
+ }
339
+ if (event.type === 'checkout.session.completed') {
340
+ await fulfil(event.data.object)
341
+ }
342
+ return { received: true }
343
+ }
344
+ )
345
+ ```
346
+
347
+ ### GitHub signature verification (Web Crypto, any runtime)
348
+
349
+ ```ts
350
+ async function hmacSha256Hex(secret: string, payload: string) {
351
+ const key = await crypto.subtle.importKey(
352
+ 'raw',
353
+ new TextEncoder().encode(secret),
354
+ { name: 'HMAC', hash: 'SHA-256' },
355
+ false,
356
+ ['sign']
357
+ )
358
+ const signature = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(payload))
359
+ return [...new Uint8Array(signature)].map((b) => b.toString(16).padStart(2, '0')).join('')
360
+ }
361
+
362
+ export const githubPush = defineWebhook(
363
+ {
364
+ method: 'POST',
365
+ path: '/github',
366
+ headers: z.object({
367
+ 'x-github-event': z.string(),
368
+ 'x-hub-signature-256': z.string()
369
+ })
370
+ },
371
+ async ({ c, headers, rawBody, body }) => {
372
+ const expected = `sha256=${await hmacSha256Hex(c.env.GITHUB_WEBHOOK_SECRET, rawBody)}`
373
+ // Use a constant-time compare in production, e.g. crypto.timingSafeEqual on Node.
374
+ if (expected !== headers['x-hub-signature-256']) {
375
+ throw new HTTPException(401, { message: 'Invalid signature' })
376
+ }
377
+ // body is the parsed JSON
378
+ }
379
+ )
380
+ ```
381
+
382
+ ### Twilio form posts
383
+
384
+ ```ts
385
+ defineWebhook(
386
+ {
387
+ method: 'POST',
388
+ path: '/twilio/sms',
389
+ body: z.object({ From: z.string(), Body: z.string() })
390
+ },
391
+ ({ c, body }) =>
392
+ c.body(`<Response><Message>Got "${body.Body}"</Message></Response>`, 200, {
393
+ 'content-type': 'text/xml'
394
+ })
395
+ )
396
+ ```
397
+
398
+ ### Storing every call
399
+
400
+ ```ts
401
+ createWebhooks(webhooks, {
402
+ onEvent: (e) =>
403
+ db.insert(webhookCalls).values({
404
+ route: e.route ?? null,
405
+ path: e.path,
406
+ method: e.method,
407
+ statusCode: e.status,
408
+ durationMs: Math.round(e.durationMs),
409
+ body: e.body ?? null,
410
+ responseBody: e.response ?? null,
411
+ errorMessage: e.error instanceof Error ? e.error.message : null,
412
+ validationError: e.validation ?? null,
413
+ ipAddress: e.c.req.header('x-forwarded-for')?.split(',')[0]?.trim() ?? null,
414
+ userAgent: e.c.req.header('user-agent') ?? null
415
+ })
416
+ })
417
+ ```
418
+
419
+ ### OpenTelemetry metrics
420
+
421
+ ```ts
422
+ import { metrics } from '@opentelemetry/api'
423
+
424
+ const duration = metrics.getMeter('webhooks').createHistogram('webhook.duration', { unit: 'ms' })
425
+
426
+ createWebhooks(webhooks, {
427
+ onEvent: (e) =>
428
+ duration.record(e.durationMs, {
429
+ 'http.route': e.route ?? 'unmatched',
430
+ 'http.request.method': e.method,
431
+ 'http.response.status_code': e.status
432
+ })
433
+ })
434
+ ```
435
+
436
+ For traces, add a tracing middleware (for example
437
+ [`@hono/otel`](https://github.com/honojs/middleware/tree/main/packages/otel))
438
+ on the parent app before `app.route('/webhooks', ...)`.
439
+
440
+ ### Skipping noise
441
+
442
+ `onEvent` fires for everything, so filter in the hook:
443
+
444
+ ```ts
445
+ onEvent(e) {
446
+ if (e.route === '/health') return
447
+ log(e)
448
+ }
449
+ ```
450
+
451
+ ### Organising webhooks in a project
452
+
453
+ Keep one file per webhook and a registry that lists them:
454
+
455
+ ```ts
456
+ // src/webhooks/index.ts
457
+ import { createWebhooks } from './factory'
458
+ import { health } from './health'
459
+ import { stripeEvents } from './stripe'
460
+
461
+ export const webhooks = createWebhooks([health, stripeEvents], { onEvent, onError })
462
+ ```
463
+
464
+ ```ts
465
+ // src/app.ts
466
+ app.route('/webhooks', webhooks)
467
+ ```
468
+
469
+ Hono's `showRoutes(app)` from `hono/dev` prints every route at startup.
470
+
471
+ ## Testing
472
+
473
+ The webhooks app is a Hono app, so `request()` runs the full pipeline:
474
+ routing, validation, handler and response.
475
+
476
+ ```ts
477
+ import { expect, test } from 'bun:test'
478
+ import { webhooks } from './webhooks'
479
+
480
+ test('rejects an order without a total', async () => {
481
+ const res = await webhooks.request('/orders/42', {
482
+ method: 'POST',
483
+ headers: { 'content-type': 'application/json' },
484
+ body: JSON.stringify({})
485
+ })
486
+ expect(res.status).toBe(400)
487
+ })
488
+ ```
489
+
490
+ Pass bindings as the third argument: `webhooks.request(path, init, env)`.
491
+
492
+ ## CLI
493
+
494
+ ```text
495
+ hono-webhooks <module> <command> [options]
496
+
497
+ Commands:
498
+ list List the webhooks
499
+ schema <path> Print the JSON Schemas of the webhooks at <path>
500
+ test <method> <path> Send a request through the webhooks app
501
+
502
+ Options for test:
503
+ -b, --body <text> Request body (JSON sets content-type: application/json)
504
+ -q, --query <params> Query string, e.g. "a=1&b=2"
505
+ -H, --header <k: v> Request header, repeatable
506
+ ```
507
+
508
+ `<module>` is a file that exports the `createWebhooks()` result, as default or
509
+ as any named export.
510
+
511
+ ```bash
512
+ bunx hono-webhooks ./src/webhooks/index.ts list
513
+ bunx hono-webhooks ./src/webhooks/index.ts schema /orders/:id
514
+ bunx hono-webhooks ./src/webhooks/index.ts test POST /orders/42 -b '{"total":10}' -H 'x-api-key: dev'
515
+ ```
516
+
517
+ - `test` sends a real request through the webhooks app (without the parent
518
+ app's middleware) and exits with code 1 on a 4xx or 5xx response.
519
+ - `schema` prints JSON Schema for libraries that implement
520
+ [Standard JSON Schema](https://standardschema.dev) (Zod 4.2+ does).
521
+ - The CLI imports your TypeScript file. That works under Bun, and under Node
522
+ 22.18+ which strips types natively. If the module needs setup first (loading
523
+ secrets, for example), preload it: `bun --preload ./src/secrets.ts x hono-webhooks ...`.
524
+
525
+ ## API reference
526
+
527
+ ```ts
528
+ import {
529
+ defineWebhook, // (config, handler) => Webhook
530
+ createWebhooks, // (webhooks, options?) => WebhooksApp (a Hono app + .webhooks)
531
+ createWebhookFactory, // <E extends Env>() => { defineWebhook, createWebhooks }
532
+ WebhookValidationError // extends HTTPException; .target, .issues
533
+ } from 'hono-webhooks'
534
+
535
+ import type {
536
+ Webhook,
537
+ WebhookConfig,
538
+ WebhookInput,
539
+ WebhookResult,
540
+ WebhookEvent,
541
+ WebhooksOptions,
542
+ WebhooksApp,
543
+ WebhookMethod,
544
+ BodyType,
545
+ ValidationTarget,
546
+ PathParams,
547
+ StandardSchemaV1
548
+ } from 'hono-webhooks'
549
+ ```
550
+
551
+ Use `Webhook` to type a list: `const all: Webhook<AppEnv>[] = [a, b]`.
552
+
553
+ ## Migrating from `createHttpRoute`
554
+
555
+ For code that used the in-app `lib/webhook` this package came from:
556
+
557
+ | Before | After |
558
+ | ---------------------------------------------- | ----------------------------------------------------------------------------- |
559
+ | `createHttpRoute(handlerData, handler)` | `defineWebhook(config, handler)` |
560
+ | `reqBodySchema`, `reqQuerySchema`, ... | `body`, `query`, `headers`, `params`, `response` |
561
+ | `z.string()` body switches to text parsing | Parsing follows `Content-Type`; set `bodyType: 'text'` to force it |
562
+ | `z.any()` body skips the content-type check | No content-type check anymore; leave `body` out and read `body` / `rawBody` |
563
+ | `rawBody: true` | Always available as `rawBody`; the body is also parsed |
564
+ | `request` in the handler | `c.req.raw` |
565
+ | `useHonoWebhooks(app)` | `app.route('/webhooks', createWebhooks(webhooks, { onEvent }))` |
566
+ | Built-in OTel span, logger, `recordWebhookCall` | `onEvent` (see [Recipes](#recipes)) and tracing middleware |
567
+ | `HEAD` / custom methods in `method` | `GET` routes answer `HEAD` automatically |
568
+ | Response schema mismatch recorded as 500 | Recorded as `event.validation` with the real status |
569
+ | Malformed JSON without a schema → `body: null` | `400 Invalid body`. Use `bodyType: 'text'` to accept anything |
570
+
571
+ ## Releasing
572
+
573
+ Releases are automated with [Release Please](https://github.com/googleapis/release-please)
574
+ and npm trusted publishing:
575
+
576
+ 1. PRs are squash-merged with a [conventional](https://www.conventionalcommits.org)
577
+ title. `fix:` releases a patch, `feat:` a minor, `feat!:` or a
578
+ `BREAKING CHANGE:` footer a major. `chore:`, `ci:`, `docs:`, `refactor:`
579
+ and `test:` don't release.
580
+ 2. Each push to `main` opens or updates a release PR with the next version and
581
+ the changelog.
582
+ 3. Merging the release PR tags the version, creates the GitHub release and
583
+ publishes to npm with provenance.
584
+
585
+ ## License
586
+
587
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};