@zap-studio/webhooks 0.4.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,10 +1,30 @@
1
- ## @zap-studio/webhooks@0.4.0
1
+ # Changelog
2
2
 
3
- ### Rework the package on Web API `Request`/`Response` (breaking)
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [1.0.0]
8
+
9
+ ### Changed
10
+
11
+ `WebhookRouter`'s stateless private static methods (`runBeforeHooks`, `runAfterHooks`, `createHandlerEntry`, `parseRequestBody`, `validatePayload`, `executeHandler`) are now module-level functions in `router.ts`. Internal-only change; the public API (`WebhookRouter`, `createWebhookRouter`, `.register()`, `.handle()`) is unaffected.
12
+
13
+ HMAC signature verification decodes the incoming header's hex signature to bytes and compares it against the computed digest byte-for-byte, instead of hex-encoding the digest and comparing hex text. Behavior is unchanged for valid requests; this only affects internals (fewer bytes compared, and the header's hex is no longer case-normalized as text since decoding handles case natively).
14
+
15
+ `constantTimeEquals` moved from `utils.ts` into `verify.ts` (its only consumer) and now compares `Uint8Array`s (bytes) instead of strings. Still exported from `@zap-studio/webhooks` and `@zap-studio/webhooks/verify`.
16
+
17
+ ### Removed
18
+
19
+ Removed the `./utils` subpath export.
20
+
21
+ ## [0.4.0]
22
+
23
+ ### Changed
4
24
 
5
25
  The custom `NormalizedRequest`/`NormalizedResponse` contract is gone. `router.handle` now takes a standard Web API `Request` and returns a standard `Response`, so the router plugs directly into fetch-native runtimes (Bun, Deno, Cloudflare Workers, Next.js route handlers, Hono) with no adapter layer.
6
26
 
7
- Breaking changes:
27
+ **Breaking changes:**
8
28
 
9
29
  - `handle(req: NormalizedRequest): Promise<NormalizedResponse>` → `handle(request: Request): Promise<Response>`.
10
30
  - Handlers receive `{ request, rawBody, path, payload }` (a `WebhookContext` plus the validated `payload`) and return a `Response` or `undefined` (default `200` `"ok"`). The `ack` helper is removed — use `Response.json(body, init)`.
@@ -14,73 +34,68 @@ Breaking changes:
14
34
 
15
35
  Behavior kept: hook execution order, prefix semantics (default `/webhooks/`), exact-match routing, HMAC verification, and the `404`/`400`/`500` error body shapes. Unknown routes now return `404` without reading the request body.
16
36
 
17
- ## @zap-studio/webhooks@0.3.0
37
+ ## [0.3.0]
18
38
 
19
- ### Migrate to ultracite lint/format; make the adapter contract generic
39
+ ### Changed
20
40
 
21
41
  `Adapter` and `BaseAdapter` are now generic over the framework request/response types (`Adapter<TReq, TRes>`, `BaseAdapter<TReq, TRes>`), replacing the previous per-method generics. The mapping members (`toNormalizedRequest`, `toFrameworkResponse`, `handleWebhook`) are now arrow properties, so custom adapters must override them with property syntax rather than method syntax.
22
42
 
23
- Also: `register()` now returns `this`, error hooks always receive a real `Error` instance, and `rawBody` is typed as `Uint8Array`.
24
-
25
- # @zap-studio/webhooks
43
+ Also: `register()` now returns `this`, error hooks always receive a real `Error` instance, and `rawBody` is typed as `Uint8Array`. Internal formatting and lint cleanup migrated to ultracite.
26
44
 
27
- ## 0.2.2
45
+ ## [0.2.2]
28
46
 
29
- ### Dependencies
47
+ ### Changed
30
48
 
31
49
  - Updated dependency `@zap-studio/validation` to `0.3.4`.
32
50
 
33
- ## 0.2.1
34
-
35
- ### Fixed
36
-
37
- - 3a950dc: Preserve registered hook assignment types while keeping the schema-first router API unchanged.
51
+ ## [0.2.1]
38
52
 
39
53
  ### Changed
40
54
 
41
55
  - 5fa58b1: Reduced webhook router complexity by consolidating hook normalization and handler entry creation.
42
56
  - 7004e9f: Allow explicit `undefined` in option handling, then follow with d707800 to remove redundant `| undefined` unions from public types.
43
57
  - 9f31f87: Switched the package build to ESNext-aligned output and updated package tooling and publish metadata.
58
+ - Updated dependency `@zap-studio/validation` to `0.3.3`.
44
59
 
45
- ### Dependencies
60
+ ### Fixed
46
61
 
47
- - Updated dependency `@zap-studio/validation` to `0.3.3`.
62
+ - 3a950dc: Preserve registered hook assignment types while keeping the schema-first router API unchanged.
48
63
 
49
- ## 0.2.0
64
+ ## [0.2.0]
50
65
 
51
- ### Minor Changes
66
+ ### Changed
52
67
 
53
68
  - c686862: Switch `createHmacVerifier` to Web Crypto and standardize the verifier around string secrets.
54
69
 
55
70
  This change removes the Node `crypto` dependency from the verifier path, keeps `req.rawBody` as `Uint8Array`, simplifies `createHmacVerifier` to take a string secret, and adds public `VerificationError` in `@zap-studio/webhooks/errors` for verifier setup and signature failures.
56
71
 
57
- ## 0.1.4
72
+ ## [0.1.4]
58
73
 
59
- ### Patch Changes
74
+ ### Changed
60
75
 
61
76
  - e26293e: Updated dependencies.
62
77
  - @zap-studio/validation@0.3.2
63
78
 
64
- ## 0.1.3
79
+ ## [0.1.3]
65
80
 
66
- ### Patch Changes
81
+ ### Changed
67
82
 
68
83
  - 5ea3d3b: Updated dependencies.
69
84
  - @zap-studio/validation@0.3.1
70
85
 
71
- ## 0.1.2
86
+ ## [0.1.2]
72
87
 
73
- ### Patch Changes
88
+ ### Fixed
74
89
 
75
90
  - c209a27: Fix payload schema validation internals to use the current async `standardValidate` options API (`{ throwOnError: false }`), restoring typecheck compatibility after the validation helper signature update.
76
91
 
77
- ## 0.1.1
92
+ ## [0.1.1]
78
93
 
79
- ### Dependencies
94
+ ### Changed
80
95
 
81
96
  - f75b984: Updated dependency `@zap-studio/validation` to `0.3.0`.
82
97
 
83
- ## 0.1.0
98
+ ## [0.1.0]
84
99
 
85
100
  ### Added
86
101
 
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 alexandretrotel
3
+ Copyright (c) 2026 Alexandre Trotel
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -2,66 +2,50 @@
2
2
 
3
3
  Schema-first, type-safe webhook routing built on the standard Web API [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) primitives, with runtime-agnostic signature verification support.
4
4
 
5
- Works with any validation library that implements [Standard Schema](https://github.com/standard-schema/standard-schema), including Zod, Valibot, and ArkType.
5
+ Full documentation: [zapstudio.dev/webhooks](https://www.zapstudio.dev/webhooks)
6
6
 
7
- ## Why this package exists
8
-
9
- Webhook handlers usually repeat the same plumbing:
10
-
11
- - verify request authenticity
12
- - parse and validate payloads
13
- - route by event path
14
- - normalize success/error responses
15
-
16
- `@zap-studio/webhooks` isolates that plumbing so your handler code stays focused on business logic.
17
-
18
- Schemas are the source of truth, and payload types are inferred from them.
19
-
20
- ## Install
7
+ ## Installation
21
8
 
22
9
  ```bash
23
10
  npm install @zap-studio/webhooks
24
11
  ```
25
12
 
26
- ## Quickstart
13
+ You also need a schema library that implements [Standard Schema](https://github.com/standard-schema/standard-schema), such as Zod, Valibot, or ArkType.
14
+
15
+ ## Features
16
+
17
+ - **Web API native** — `handle(request: Request)` returns a `Response`, so the router plugs directly into Bun, Deno, Cloudflare Workers, Next.js route handlers, Hono, and any other fetch-compatible runtime.
18
+ - **Type-safe routing** — handler payload types are inferred from the route schema.
19
+ - **Standard Schema validation** — bring Zod, Valibot, ArkType, or any compatible library.
20
+ - **Signature verification** — built-in HMAC verifier with constant-time comparison, or plug in your own `verify` function.
21
+ - **Lifecycle hooks** — global `before`, `after`, and `onError` hooks for cross-cutting behavior.
22
+ - **Runtime-agnostic** — uses the Web Crypto API, not Node-specific APIs.
23
+ - **Tree-shakeable** — validation and hook-running internals are standalone functions; unused exports are dropped by any modern bundler.
24
+
25
+ ## Quick Start
27
26
 
28
27
  ```ts
29
28
  import { createWebhookRouter } from "@zap-studio/webhooks";
30
29
  import { z } from "zod";
31
30
 
32
- const router = createWebhookRouter({
33
- prefix: "/webhooks", // default
34
- });
31
+ const router = createWebhookRouter({ prefix: "/webhooks" });
35
32
 
36
33
  router.register("/payments/succeeded", {
37
- schema: z.object({
38
- id: z.string(),
39
- amount: z.number().positive(),
40
- currency: z.string().length(3),
41
- }),
34
+ schema: z.object({ id: z.string(), amount: z.number().positive() }),
42
35
  handler: ({ payload }) => {
43
36
  // payload is inferred from schema
44
- return Response.json(`processed ${payload.id}`);
37
+ return Response.json({ processed: payload.id });
45
38
  },
46
39
  });
47
40
 
48
- // Any fetch-compatible runtime: Bun, Deno, Cloudflare Workers, ...
49
41
  export default {
50
42
  fetch: (request: Request) => router.handle(request),
51
43
  };
52
44
  ```
53
45
 
54
- `router.handle` takes a standard `Request` and returns a standard `Response`, so the router plugs directly into any fetch-native runtime — no adapter layer needed.
55
-
56
- Handlers can return a `Response`, or `undefined` to let the router reply with its default `200` acknowledgement.
57
-
58
- ### Paths and the prefix
59
-
60
- Routes are registered with a leading slash (`"/payments/succeeded"`) and matched relative to the router's `prefix` (default `"/webhooks"`, no trailing slash) — so the example above answers on `/webhooks/payments/succeeded`. Paths are normalized internally: missing leading slashes are added, trailing slashes stripped, and duplicate slashes collapsed, on both registered routes and incoming request URLs. Set `prefix: ""` (or `"/"`) to mount routes at the root.
61
-
62
- ## Runtime integration
46
+ ## Web API Native
63
47
 
64
- Because `handle(request)` speaks fetch, integration is one line in most environments:
48
+ `handle(request: Request)` returns a `Response`, so the router plugs directly into any fetch-compatible runtime.
65
49
 
66
50
  ```ts
67
51
  // Bun / Deno / Cloudflare Workers
@@ -74,151 +58,82 @@ export const POST = (request: Request) => router.handle(request);
74
58
  app.all("/webhooks/*", (c) => router.handle(c.req.raw));
75
59
  ```
76
60
 
77
- For raw Node `http` servers, use a fetch-to-Node bridge such as [`srvx`](https://srvx.h3.dev) or [`@hono/node-server`](https://github.com/honojs/node-server).
61
+ ## Type-Safe Routing
78
62
 
79
- ## The webhook context
80
-
81
- Hooks, verifiers, and handlers all receive a context object instead of the raw request stream. The router reads the request body exactly once, so the exact bytes stay available for signature verification:
63
+ Handler payload types are inferred from the route schema.
82
64
 
83
65
  ```ts
84
- interface WebhookContext {
85
- request: Request; // headers, method, url — body already consumed
86
- rawBody: Uint8Array; // exact request body bytes
87
- path: string; // matched route key, e.g. "/payments/succeeded"
88
- }
66
+ router.register("/payments/succeeded", {
67
+ schema: z.object({ id: z.string(), amount: z.number() }),
68
+ handler: ({ payload }) => {
69
+ // payload.id: string, payload.amount: number inferred from schema
70
+ return Response.json({ ok: true });
71
+ },
72
+ });
89
73
  ```
90
74
 
91
- Handlers additionally receive `payload`, the schema-validated body.
75
+ ## Standard Schema Validation
92
76
 
93
- ## GitHub webhook example
77
+ Bring Zod, Valibot, ArkType, or any compatible library.
94
78
 
95
79
  ```ts
96
- import { createHmacVerifier, createWebhookRouter } from "@zap-studio/webhooks";
97
80
  import { z } from "zod";
81
+ // or: import * as v from "valibot"; import { type } from "arktype";
98
82
 
99
- const router = createWebhookRouter({
100
- verify: createHmacVerifier({
101
- headerName: "x-hub-signature-256",
102
- secret: process.env.GITHUB_WEBHOOK_SECRET!,
103
- }),
104
- });
105
-
106
- router.register("/github/push", {
107
- schema: z.object({
108
- ref: z.string(),
109
- repository: z.object({
110
- full_name: z.string(),
111
- }),
112
- }),
113
- handler: ({ payload }) => {
114
- console.log(`[github] ${payload.repository.full_name} ${payload.ref}`);
115
- return undefined; // default 200 "ok"
116
- },
83
+ router.register("/event", {
84
+ schema: z.object({ id: z.string() }),
85
+ handler: ({ payload }) => Response.json(payload),
117
86
  });
118
87
  ```
119
88
 
120
- ## Stripe webhook example
89
+ ## Signature Verification
121
90
 
122
- ```ts
123
- import Stripe from "stripe";
124
- import { createWebhookRouter } from "@zap-studio/webhooks";
125
- import { z } from "zod";
91
+ Built-in HMAC verifier with constant-time comparison, or plug in your own `verify` function.
126
92
 
127
- const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
93
+ ```ts
94
+ import {
95
+ createHmacVerifier,
96
+ createWebhookRouter,
97
+ VerificationError,
98
+ } from "@zap-studio/webhooks";
128
99
 
129
100
  const router = createWebhookRouter({
130
- verify: ({ request, rawBody }) => {
131
- const signature = request.headers.get("stripe-signature");
132
- if (!signature) {
133
- throw new Error("Missing Stripe signature");
134
- }
135
-
136
- stripe.webhooks.constructEvent(
137
- Buffer.from(rawBody),
138
- signature,
139
- process.env.STRIPE_WEBHOOK_SECRET!
140
- );
141
- },
142
- });
143
-
144
- router.register("/stripe/payment_intent.succeeded", {
145
- schema: z.object({
146
- id: z.string(),
147
- object: z.literal("event"),
148
- type: z.literal("payment_intent.succeeded"),
101
+ verify: createHmacVerifier({
102
+ headerName: "x-hub-signature-256",
103
+ secret: process.env.WEBHOOK_SECRET!,
149
104
  }),
150
- handler: ({ payload }) => {
151
- console.log(`[stripe] event ${payload.id} (${payload.type})`);
152
- return Response.json("received");
105
+ onError: (error) => {
106
+ if (error instanceof VerificationError) {
107
+ return Response.json({ error: "invalid signature" }, { status: 401 });
108
+ }
153
109
  },
154
110
  });
155
111
  ```
156
112
 
157
- ## Lifecycle hooks
158
-
159
- Lifecycle hooks let you apply cross-cutting behavior without duplicating code in each handler:
113
+ ## Lifecycle Hooks
160
114
 
161
- - `before`: run logic before verify/validation/handler (logging, tracing, rate-limit checks)
162
- - `after`: run logic after successful handler execution (metrics, audit logs)
163
- - `onError`: map thrown errors to consistent responses and centralize error reporting
115
+ Global `before`, `after`, and `onError` hooks for cross-cutting behavior.
164
116
 
165
117
  ```ts
166
118
  const router = createWebhookRouter({
167
- before: (ctx) => {
168
- console.log("incoming", ctx.path);
169
- },
170
- after: (_ctx, response) => {
171
- console.log("status", response.status);
172
- },
119
+ before: (ctx) => console.log("incoming", ctx.path),
120
+ after: (_ctx, response) => console.log("status", response.status),
173
121
  onError: (error) => Response.json({ error: error.message }, { status: 500 }),
174
122
  });
175
123
  ```
176
124
 
177
- After-hooks receive the outgoing `Response` as-is. If a hook needs to read the body, call `response.clone()` first so the stream sent to the client stays readable:
178
-
179
- ```ts
180
- const router = createWebhookRouter({
181
- after: async (_ctx, response) => {
182
- const body = await response.clone().json();
183
- console.log("responded with", body);
184
- },
185
- });
186
- ```
187
-
188
- ## Verification helper
189
-
190
- `@zap-studio/webhooks` exports `createHmacVerifier`, a small helper that builds a `verify` function for HMAC-signed webhook providers.
191
-
192
- It does not depend on Node APIs. The verifier uses the Web Crypto API, so it works in any runtime that provides `globalThis.crypto.subtle`.
125
+ ## Runtime-Agnostic
193
126
 
194
- - reads a signature from the header you choose
195
- - computes an HMAC from `ctx.rawBody`
196
- - compares signatures in constant time
197
- - uses the Web Crypto API instead of Node `crypto`
198
- - works across runtimes that provide `globalThis.crypto.subtle`
199
- - expects a string secret
200
- - throws `VerificationError` on verifier setup or signature failures
127
+ Uses the Web Crypto API, not Node-specific APIs.
201
128
 
202
129
  ```ts
203
- import { createHmacVerifier, VerificationError } from "@zap-studio/webhooks";
204
-
130
+ // Uses globalThis.crypto.subtle no Node `crypto` import required
205
131
  const verify = createHmacVerifier({
206
132
  headerName: "x-hub-signature-256",
207
133
  secret: process.env.WEBHOOK_SECRET!,
208
- algo: "sha256", // optional, defaults to sha256
209
134
  });
210
-
211
- try {
212
- await verify(ctx);
213
- } catch (error) {
214
- if (error instanceof VerificationError) {
215
- console.error("webhook verification failed", error.message);
216
- }
217
- }
218
135
  ```
219
136
 
220
- Use this when your provider uses standard HMAC signatures. For providers with custom signing formats, pass your own `verify` function.
221
-
222
137
  ## Runtime Support
223
138
 
224
139
  | Runtime | Minimum version |
package/dist/errors.d.ts CHANGED
@@ -9,6 +9,22 @@
9
9
  *
10
10
  * This error is used by verifier helpers such as `createHmacVerifier` so
11
11
  * callers can distinguish verification failures from other webhook errors.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * import { VerificationError } from "@zap-studio/webhooks";
16
+ *
17
+ * const response = await router.handle(request);
18
+ * // Verification failures surface as a 500 response by default, or via onError:
19
+ * const routerWithHandler = createWebhookRouter({
20
+ * verify: createHmacVerifier({ headerName: "x-signature", secret }),
21
+ * onError: (error) => {
22
+ * if (error instanceof VerificationError) {
23
+ * return Response.json({ error: error.message }, { status: 401 });
24
+ * }
25
+ * },
26
+ * });
27
+ * ```
12
28
  */
13
29
  declare class VerificationError extends Error {
14
30
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","names":[],"sources":["../src/errors.ts"],"mappings":";;;;;;;;;;;;cAYa,0BAA0B;;;;;;EAMrC,YAAY"}
1
+ {"version":3,"file":"errors.d.ts","names":[],"sources":["../src/errors.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;cA4Ba,0BAA0B;;;;;;EAMrC,YAAY"}
package/dist/errors.js CHANGED
@@ -9,6 +9,22 @@
9
9
  *
10
10
  * This error is used by verifier helpers such as `createHmacVerifier` so
11
11
  * callers can distinguish verification failures from other webhook errors.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * import { VerificationError } from "@zap-studio/webhooks";
16
+ *
17
+ * const response = await router.handle(request);
18
+ * // Verification failures surface as a 500 response by default, or via onError:
19
+ * const routerWithHandler = createWebhookRouter({
20
+ * verify: createHmacVerifier({ headerName: "x-signature", secret }),
21
+ * onError: (error) => {
22
+ * if (error instanceof VerificationError) {
23
+ * return Response.json({ error: error.message }, { status: 401 });
24
+ * }
25
+ * },
26
+ * });
27
+ * ```
12
28
  */
13
29
  var VerificationError = class extends Error {
14
30
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/**\n * Error primitives for webhook verification failures.\n *\n * @module @zap-studio/webhooks/errors\n */\n\n/**\n * Error thrown when webhook request verification fails.\n *\n * This error is used by verifier helpers such as `createHmacVerifier` so\n * callers can distinguish verification failures from other webhook errors.\n */\nexport class VerificationError extends Error {\n /**\n * Creates a verification error with a human-readable message.\n *\n * @param message - Error message describing the verification failure.\n */\n constructor(message: string) {\n super(message);\n this.name = \"VerificationError\";\n }\n}\n"],"mappings":";;;;;;;;;;;;AAYA,IAAa,oBAAb,cAAuC,MAAM;;;;;;CAM3C,YAAY,SAAiB;EAC3B,MAAM,OAAO;EACb,KAAK,OAAO;CACd;AACF"}
1
+ {"version":3,"file":"errors.js","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/**\n * Error primitives for webhook verification failures.\n *\n * @module @zap-studio/webhooks/errors\n */\n\n/**\n * Error thrown when webhook request verification fails.\n *\n * This error is used by verifier helpers such as `createHmacVerifier` so\n * callers can distinguish verification failures from other webhook errors.\n *\n * @example\n * ```ts\n * import { VerificationError } from \"@zap-studio/webhooks\";\n *\n * const response = await router.handle(request);\n * // Verification failures surface as a 500 response by default, or via onError:\n * const routerWithHandler = createWebhookRouter({\n * verify: createHmacVerifier({ headerName: \"x-signature\", secret }),\n * onError: (error) => {\n * if (error instanceof VerificationError) {\n * return Response.json({ error: error.message }, { status: 401 });\n * }\n * },\n * });\n * ```\n */\nexport class VerificationError extends Error {\n /**\n * Creates a verification error with a human-readable message.\n *\n * @param message - Error message describing the verification failure.\n */\n constructor(message: string) {\n super(message);\n this.name = \"VerificationError\";\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,IAAa,oBAAb,cAAuC,MAAM;;;;;;CAM3C,YAAY,SAAiB;EAC3B,MAAM,OAAO;EACb,KAAK,OAAO;CACd;AACF"}
package/dist/index.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { VerificationError } from "./errors.js";
2
- import { AfterHook, BeforeHook, ErrorHook, HandlerContext, HandlerMap, InferSchemaOutput, InferWebhookMapFromRoutes, RegisterOptions, SchemaRouteOptions, SchemaRoutes, VerifyFn, WebhookContext, WebhookHandler } from "./types.js";
3
- import { WebhookRouter, WebhookRouterOptions, createWebhookRouter } from "./router.js";
4
- import { constantTimeEquals } from "./utils.js";
5
- import { createHmacVerifier } from "./verify.js";
2
+ import { AfterHook, BeforeHook, ErrorHook, HandlerContext, HandlerMap, InferSchemaOutput, InferWebhookMapFromRoutes, RegisterOptions, SchemaRouteOptions, SchemaRoutes, VerifyFn, WebhookContext, WebhookHandler, WebhookRouterOptions } from "./types.js";
3
+ import { WebhookRouter, createWebhookRouter } from "./router.js";
4
+ import { constantTimeEquals, createHmacVerifier } from "./verify.js";
6
5
  export { type AfterHook, type BeforeHook, type ErrorHook, type HandlerContext, type HandlerMap, type InferSchemaOutput, type InferWebhookMapFromRoutes, type RegisterOptions, type SchemaRouteOptions, type SchemaRoutes, VerificationError, type VerifyFn, type WebhookContext, type WebhookHandler, WebhookRouter, type WebhookRouterOptions, constantTimeEquals, createHmacVerifier, createWebhookRouter };
package/dist/index.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { VerificationError } from "./errors.js";
2
2
  import { WebhookRouter, createWebhookRouter } from "./router.js";
3
- import { constantTimeEquals } from "./utils.js";
4
- import { createHmacVerifier } from "./verify.js";
3
+ import { constantTimeEquals, createHmacVerifier } from "./verify.js";
5
4
  export { VerificationError, WebhookRouter, constantTimeEquals, createHmacVerifier, createWebhookRouter };
package/dist/router.d.ts CHANGED
@@ -1,27 +1,26 @@
1
- import { AfterHook, BeforeHook, ErrorHook, InferSchemaOutput, RegisterOptions, SchemaRouteOptions, VerifyFn, WebhookHandler } from "./types.js";
1
+ import { InferSchemaOutput, RegisterOptions, SchemaRouteOptions, WebhookHandler, WebhookRouterOptions } from "./types.js";
2
2
  import { StandardSchemaV1 } from "@zap-studio/validation";
3
3
  //#region src/router.d.ts
4
- interface WebhookRouterOptions {
5
- /** Global hooks executed after successful route handler completion. */
6
- after?: AfterHook | AfterHook[];
7
- /** Global hooks executed before route-level hooks and verification. */
8
- before?: BeforeHook | BeforeHook[];
9
- /** Global error hook used to override the default `500` response. */
10
- onError?: ErrorHook;
11
- /**
12
- * Required path prefix for all webhook routes. Defaults to `"/webhooks"`.
13
- *
14
- * Normalized internally: leading slash added, trailing slash stripped,
15
- * duplicate slashes collapsed. Use `""` or `"/"` to mount at the root.
16
- */
17
- prefix?: string;
18
- /** Optional request verification function (for signature checks, auth, etc.). */
19
- verify?: VerifyFn;
20
- }
21
4
  /**
22
5
  * Main webhook router class.
23
6
  *
24
7
  * Register routes with typed schemas and call `handle` with a Web API `Request`.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * import { WebhookRouter } from "@zap-studio/webhooks";
12
+ *
13
+ * const router = new WebhookRouter({ prefix: "/webhooks" });
14
+ *
15
+ * router.register("/stripe", {
16
+ * schema: stripeEventSchema,
17
+ * handler: async ({ payload }) => {
18
+ * console.log("Stripe event:", payload.type);
19
+ * },
20
+ * });
21
+ *
22
+ * export default { fetch: (request: Request) => router.handle(request) };
23
+ * ```
25
24
  */
26
25
  declare class WebhookRouter<TMap = unknown> {
27
26
  private readonly handlers;
@@ -35,6 +34,15 @@ declare class WebhookRouter<TMap = unknown> {
35
34
  * Creates a webhook router with optional global hooks and verification behavior.
36
35
  *
37
36
  * @param opts - Router-level options.
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * const router = new WebhookRouter({
41
+ * prefix: "/webhooks",
42
+ * verify: createHmacVerifier({ headerName: "x-signature", secret }),
43
+ * onError: (error) => Response.json({ error: error.message }, { status: 500 }),
44
+ * });
45
+ * ```
38
46
  */
39
47
  constructor(opts?: WebhookRouterOptions);
40
48
  /**
@@ -45,9 +53,46 @@ declare class WebhookRouter<TMap = unknown> {
45
53
  * @param path - Route path relative to configured prefix, starting with `/` (e.g. `"/stripe"`).
46
54
  * @param handlerOrOptions - Handler function or schema-based registration options.
47
55
  * @returns The same router instance with an updated internal route type map.
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * router.register("/stripe", {
60
+ * schema: stripeEventSchema,
61
+ * handler: async ({ payload }) => {
62
+ * console.log(payload.type); // typed from stripeEventSchema
63
+ * },
64
+ * });
65
+ * ```
48
66
  */
49
67
  register<Path extends `/${string}`, TSchema extends StandardSchemaV1<unknown, unknown>>(path: Path, handlerOrOptions: SchemaRouteOptions<TSchema>): WebhookRouter<TMap & Record<Path, InferSchemaOutput<TSchema>>>;
68
+ /**
69
+ * Register a webhook handler for a specific path, with schema-less registration options.
70
+ *
71
+ * @param path - Route path relative to configured prefix, starting with `/` (e.g. `"/stripe"`).
72
+ * @param handlerOrOptions - Registration options without a schema.
73
+ * @returns The same router instance with an updated internal route type map.
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * router.register("/ping", {
78
+ * before: (ctx) => console.log("received", ctx.path),
79
+ * handler: () => Response.json({ ok: true }),
80
+ * });
81
+ * ```
82
+ */
50
83
  register<Path extends `/${string}`, TPayload>(path: Path, handlerOrOptions: RegisterOptions<TPayload>): WebhookRouter<TMap & Record<Path, TPayload>>;
84
+ /**
85
+ * Register a webhook handler for a specific path, using a plain handler function.
86
+ *
87
+ * @param path - Route path relative to configured prefix, starting with `/` (e.g. `"/stripe"`).
88
+ * @param handlerOrOptions - Handler function to process the webhook.
89
+ * @returns The same router instance with an updated internal route type map.
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * router.register("/health", () => Response.json({ status: "ok" }));
94
+ * ```
95
+ */
51
96
  register<Path extends `/${string}`>(path: Path, handlerOrOptions: WebhookHandler): WebhookRouter<TMap & Record<Path, unknown>>;
52
97
  /**
53
98
  * Handles an incoming webhook request.
@@ -57,15 +102,19 @@ declare class WebhookRouter<TMap = unknown> {
57
102
  *
58
103
  * @param request - Incoming Web API request.
59
104
  * @returns Web API response for the runtime to send back.
105
+ *
106
+ * @example
107
+ * ```ts
108
+ * // Framework-agnostic: works with any Web API Request/Response runtime.
109
+ * export async function POST(request: Request): Promise<Response> {
110
+ * return router.handle(request);
111
+ * }
112
+ * ```
60
113
  */
61
114
  handle(request: Request): Promise<Response>;
115
+ /** Resolves the incoming request's URL to a registered route key, or `null` if it doesn't match the configured prefix. */
62
116
  private matchPath;
63
- private static runBeforeHooks;
64
- private static runAfterHooks;
65
- private static createHandlerEntry;
66
- private static parseRequestBody;
67
- private static validatePayload;
68
- private static executeHandler;
117
+ /** Builds the error response for a failed request, deferring to the global error hook when set. */
69
118
  private handleError;
70
119
  }
71
120
  /**
@@ -73,8 +122,16 @@ declare class WebhookRouter<TMap = unknown> {
73
122
  *
74
123
  * @param opts - Optional global router options.
75
124
  * @returns A new webhook router.
125
+ *
126
+ * @example
127
+ * ```ts
128
+ * import { createWebhookRouter } from "@zap-studio/webhooks";
129
+ *
130
+ * const router = createWebhookRouter({ prefix: "/webhooks" });
131
+ * router.register("/stripe", { schema: stripeEventSchema, handler });
132
+ * ```
76
133
  */
77
134
  declare const createWebhookRouter: (opts?: WebhookRouterOptions) => WebhookRouter;
78
135
  //#endregion
79
- export { WebhookRouter, WebhookRouterOptions, createWebhookRouter };
136
+ export { WebhookRouter, createWebhookRouter };
80
137
  //# sourceMappingURL=router.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"router.d.ts","names":[],"sources":["../src/router.ts"],"mappings":";;;UAiCiB;;EAEf,QAAQ,YAAY;;EAEpB,SAAS,aAAa;;EAEtB,UAAU;;;;;;;EAOV;;EAEA,SAAS;;;;;;;cAoCE,cAAc;mBACR;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;;;;;;EAOjB,YAAY,OAAM;;;;;;;;;;EAkBlB,SACE,2BACA,gBAAgB,oCAEhB,MAAM,MACN,kBAAkB,mBAAmB,WACpC,cAAc,OAAO,OAAO,MAAM,kBAAkB;EACvD,SAAS,2BAA2B,UAClC,MAAM,MACN,kBAAkB,gBAAgB,YACjC,cAAc,OAAO,OAAO,MAAM;EACrC,SAAS,2BACP,MAAM,MACN,kBAAkB,iBACjB,cAAc,OAAO,OAAO;;;;;;;;;;EAwB/B,OAAa,SAAS,UAAU,QAAQ;UAoDhC;iBAqBa;iBAcA;iBAeN;iBA0BA;iBAQM;iBA+BA;UAaP;;;;;;;;cA4BH,sBACX,OAAO,yBACN"}
1
+ {"version":3,"file":"router.d.ts","names":[],"sources":["../src/router.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;cA2La,cAAc;mBACR;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;;;;;;;;;;;;;;;EAgBjB,YAAY,OAAM;;;;;;;;;;;;;;;;;;;;EA4BlB,SACE,2BACA,gBAAgB,oCAEhB,MAAM,MACN,kBAAkB,mBAAmB,WACpC,cAAc,OAAO,OAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;EAgBvD,SAAS,2BAA2B,UAClC,MAAM,MACN,kBAAkB,gBAAgB,YACjC,cAAc,OAAO,OAAO,MAAM;;;;;;;;;;;;;EAarC,SAAS,2BACP,MAAM,MACN,kBAAkB,iBACjB,cAAc,OAAO,OAAO;;;;;;;;;;;;;;;;;;EAgC/B,OAAa,SAAS,UAAU,QAAQ;;UAqDhC;;UAsBM;;;;;;;;;;;;;;;;cAoCH,sBACX,OAAO,yBACN"}