@zap-studio/webhooks 0.3.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,70 +1,101 @@
1
- ## @zap-studio/webhooks@0.3.0
1
+ # Changelog
2
2
 
3
- ### Migrate to ultracite lint/format; make the adapter contract generic
3
+ All notable changes to this project will be documented in this file.
4
4
 
5
- `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.
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
6
 
7
- Also: `register()` now returns `this`, error hooks always receive a real `Error` instance, and `rawBody` is typed as `Uint8Array`.
7
+ ## [1.0.0]
8
8
 
9
- # @zap-studio/webhooks
9
+ ### Changed
10
10
 
11
- ## 0.2.2
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
12
 
13
- ### Dependencies
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
14
 
15
- - Updated dependency `@zap-studio/validation` to `0.3.4`.
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
16
 
17
- ## 0.2.1
17
+ ### Removed
18
18
 
19
- ### Fixed
19
+ Removed the `./utils` subpath export.
20
20
 
21
- - 3a950dc: Preserve registered hook assignment types while keeping the schema-first router API unchanged.
21
+ ## [0.4.0]
22
+
23
+ ### Changed
24
+
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.
26
+
27
+ **Breaking changes:**
28
+
29
+ - `handle(req: NormalizedRequest): Promise<NormalizedResponse>` → `handle(request: Request): Promise<Response>`.
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)`.
31
+ - Hooks and `verify` are retyped against the context: `BeforeHook(ctx)`, `AfterHook(ctx, response)`, `ErrorHook(error, ctx)`, `VerifyFn(ctx)`. After hooks must `clone()` the response before reading its body.
32
+ - `Adapter`, `BaseAdapter`, and the `./adapters/base` export are removed. Node `http` users can bridge with `srvx` or `@hono/node-server`.
33
+ - The prefix is normalized to a trailing slash and only matches on a path boundary: `prefix: "/api"` now behaves as `/api/`, so `/apihello` no longer matches a route (previously it matched `ihello`).
34
+
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.
36
+
37
+ ## [0.3.0]
38
+
39
+ ### Changed
40
+
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.
42
+
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.
44
+
45
+ ## [0.2.2]
46
+
47
+ ### Changed
48
+
49
+ - Updated dependency `@zap-studio/validation` to `0.3.4`.
50
+
51
+ ## [0.2.1]
22
52
 
23
53
  ### Changed
24
54
 
25
55
  - 5fa58b1: Reduced webhook router complexity by consolidating hook normalization and handler entry creation.
26
56
  - 7004e9f: Allow explicit `undefined` in option handling, then follow with d707800 to remove redundant `| undefined` unions from public types.
27
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`.
28
59
 
29
- ### Dependencies
60
+ ### Fixed
30
61
 
31
- - Updated dependency `@zap-studio/validation` to `0.3.3`.
62
+ - 3a950dc: Preserve registered hook assignment types while keeping the schema-first router API unchanged.
32
63
 
33
- ## 0.2.0
64
+ ## [0.2.0]
34
65
 
35
- ### Minor Changes
66
+ ### Changed
36
67
 
37
68
  - c686862: Switch `createHmacVerifier` to Web Crypto and standardize the verifier around string secrets.
38
69
 
39
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.
40
71
 
41
- ## 0.1.4
72
+ ## [0.1.4]
42
73
 
43
- ### Patch Changes
74
+ ### Changed
44
75
 
45
76
  - e26293e: Updated dependencies.
46
77
  - @zap-studio/validation@0.3.2
47
78
 
48
- ## 0.1.3
79
+ ## [0.1.3]
49
80
 
50
- ### Patch Changes
81
+ ### Changed
51
82
 
52
83
  - 5ea3d3b: Updated dependencies.
53
84
  - @zap-studio/validation@0.3.1
54
85
 
55
- ## 0.1.2
86
+ ## [0.1.2]
56
87
 
57
- ### Patch Changes
88
+ ### Fixed
58
89
 
59
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.
60
91
 
61
- ## 0.1.1
92
+ ## [0.1.1]
62
93
 
63
- ### Dependencies
94
+ ### Changed
64
95
 
65
96
  - f75b984: Updated dependency `@zap-studio/validation` to `0.3.0`.
66
97
 
67
- ## 0.1.0
98
+ ## [0.1.0]
68
99
 
69
100
  ### Added
70
101
 
package/README.md CHANGED
@@ -1,215 +1,150 @@
1
1
  # @zap-studio/webhooks
2
2
 
3
- Schema-first, type-safe webhook routing with runtime-agnostic signature verification support.
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
7
+ ## Installation
8
8
 
9
- Webhook handlers usually repeat the same plumbing:
9
+ ```bash
10
+ npm install @zap-studio/webhooks
11
+ ```
10
12
 
11
- - verify request authenticity
12
- - parse and validate payloads
13
- - route by event path
14
- - normalize success/error responses
13
+ You also need a schema library that implements [Standard Schema](https://github.com/standard-schema/standard-schema), such as Zod, Valibot, or ArkType.
15
14
 
16
- `@zap-studio/webhooks` isolates that plumbing so your handler code stays focused on business logic.
15
+ ## Features
17
16
 
18
- Schemas are the source of truth, and payload types are inferred from them.
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.
19
24
 
20
- ## Install
21
-
22
- ```bash
23
- pnpm add @zap-studio/webhooks
24
- ```
25
-
26
- ## Quickstart
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/",
34
- });
31
+ const router = createWebhookRouter({ prefix: "/webhooks" });
35
32
 
36
- router.register("payments/succeeded", {
37
- schema: z.object({
38
- id: z.string(),
39
- amount: z.number().positive(),
40
- currency: z.string().length(3),
41
- }),
42
- handler: async ({ payload, ack }) => {
33
+ router.register("/payments/succeeded", {
34
+ schema: z.object({ id: z.string(), amount: z.number().positive() }),
35
+ handler: ({ payload }) => {
43
36
  // payload is inferred from schema
44
- return ack({ status: 200, body: `processed ${payload.id}` });
37
+ return Response.json({ processed: payload.id });
45
38
  },
46
39
  });
40
+
41
+ export default {
42
+ fetch: (request: Request) => router.handle(request),
43
+ };
47
44
  ```
48
45
 
49
- ## GitHub webhook example
46
+ ## Web API Native
47
+
48
+ `handle(request: Request)` returns a `Response`, so the router plugs directly into any fetch-compatible runtime.
50
49
 
51
50
  ```ts
52
- import { createWebhookRouter } from "@zap-studio/webhooks";
53
- import { createHmacVerifier } from "@zap-studio/webhooks/verify";
54
- import { z } from "zod";
51
+ // Bun / Deno / Cloudflare Workers
52
+ export default { fetch: (request: Request) => router.handle(request) };
55
53
 
56
- const router = createWebhookRouter({
57
- verify: createHmacVerifier({
58
- headerName: "x-hub-signature-256",
59
- secret: process.env.GITHUB_WEBHOOK_SECRET!,
60
- }),
61
- });
54
+ // Next.js route handler (app/webhooks/[...path]/route.ts)
55
+ export const POST = (request: Request) => router.handle(request);
62
56
 
63
- router.register("github/push", {
64
- schema: z.object({
65
- ref: z.string(),
66
- repository: z.object({
67
- full_name: z.string(),
68
- }),
69
- }),
70
- handler: async ({ payload, ack }) => {
71
- console.log(`[github] ${payload.repository.full_name} ${payload.ref}`);
72
- return ack();
57
+ // Hono
58
+ app.all("/webhooks/*", (c) => router.handle(c.req.raw));
59
+ ```
60
+
61
+ ## Type-Safe Routing
62
+
63
+ Handler payload types are inferred from the route schema.
64
+
65
+ ```ts
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 });
73
71
  },
74
72
  });
75
73
  ```
76
74
 
77
- ## Stripe webhook example
75
+ ## Standard Schema Validation
76
+
77
+ Bring Zod, Valibot, ArkType, or any compatible library.
78
78
 
79
79
  ```ts
80
- import Stripe from "stripe";
81
- import { createWebhookRouter } from "@zap-studio/webhooks";
82
80
  import { z } from "zod";
81
+ // or: import * as v from "valibot"; import { type } from "arktype";
83
82
 
84
- const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
83
+ router.register("/event", {
84
+ schema: z.object({ id: z.string() }),
85
+ handler: ({ payload }) => Response.json(payload),
86
+ });
87
+ ```
85
88
 
86
- const router = createWebhookRouter({
87
- verify: async (req) => {
88
- const signature = req.headers.get("stripe-signature");
89
- if (!signature) {
90
- throw new Error("Missing Stripe signature");
91
- }
89
+ ## Signature Verification
92
90
 
93
- stripe.webhooks.constructEvent(
94
- req.rawBody,
95
- signature,
96
- process.env.STRIPE_WEBHOOK_SECRET!
97
- );
98
- },
99
- });
91
+ Built-in HMAC verifier with constant-time comparison, or plug in your own `verify` function.
100
92
 
101
- router.register("stripe/payment_intent.succeeded", {
102
- schema: z.object({
103
- id: z.string(),
104
- object: z.literal("event"),
105
- type: z.literal("payment_intent.succeeded"),
93
+ ```ts
94
+ import {
95
+ createHmacVerifier,
96
+ createWebhookRouter,
97
+ VerificationError,
98
+ } from "@zap-studio/webhooks";
99
+
100
+ const router = createWebhookRouter({
101
+ verify: createHmacVerifier({
102
+ headerName: "x-hub-signature-256",
103
+ secret: process.env.WEBHOOK_SECRET!,
106
104
  }),
107
- handler: async ({ payload, ack }) => {
108
- console.log(`[stripe] event ${payload.id} (${payload.type})`);
109
- return ack({ status: 200 });
105
+ onError: (error) => {
106
+ if (error instanceof VerificationError) {
107
+ return Response.json({ error: "invalid signature" }, { status: 401 });
108
+ }
110
109
  },
111
110
  });
112
111
  ```
113
112
 
114
- ## Lifecycle hooks
115
-
116
- Lifecycle hooks let you apply cross-cutting behavior without duplicating code in each handler:
113
+ ## Lifecycle Hooks
117
114
 
118
- - `before`: run logic before verify/validation/handler (logging, tracing, rate-limit checks)
119
- - `after`: run logic after successful handler execution (metrics, audit logs)
120
- - `onError`: map thrown errors to consistent responses and centralize error reporting
115
+ Global `before`, `after`, and `onError` hooks for cross-cutting behavior.
121
116
 
122
117
  ```ts
123
118
  const router = createWebhookRouter({
124
- before: (req) => {
125
- console.log("incoming", req.path);
126
- },
127
- after: (_req, res) => {
128
- console.log("status", res.status);
129
- },
130
- onError: (error) => ({
131
- status: 500,
132
- body: { error: error.message },
133
- }),
119
+ before: (ctx) => console.log("incoming", ctx.path),
120
+ after: (_ctx, response) => console.log("status", response.status),
121
+ onError: (error) => Response.json({ error: error.message }, { status: 500 }),
134
122
  });
135
123
  ```
136
124
 
137
- ## Verification helper
125
+ ## Runtime-Agnostic
138
126
 
139
- `@zap-studio/webhooks/verify` exports `createHmacVerifier`, a small helper that builds a `verify` function for HMAC-signed webhook providers.
140
-
141
- 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`.
142
-
143
- - reads a signature from the header you choose
144
- - computes an HMAC from `req.rawBody`
145
- - compares signatures in constant time
146
- - uses the Web Crypto API instead of Node `crypto`
147
- - works across runtimes that provide `globalThis.crypto.subtle`
148
- - expects a string secret
149
- - throws `VerificationError` on verifier setup or signature failures
127
+ Uses the Web Crypto API, not Node-specific APIs.
150
128
 
151
129
  ```ts
152
- import { createHmacVerifier } from "@zap-studio/webhooks/verify";
153
- import { VerificationError } from "@zap-studio/webhooks/errors";
154
-
130
+ // Uses globalThis.crypto.subtle no Node `crypto` import required
155
131
  const verify = createHmacVerifier({
156
132
  headerName: "x-hub-signature-256",
157
133
  secret: process.env.WEBHOOK_SECRET!,
158
- algo: "sha256", // optional, defaults to sha256
159
134
  });
160
-
161
- try {
162
- await verify(req);
163
- } catch (error) {
164
- if (error instanceof VerificationError) {
165
- console.error("webhook verification failed", error.message);
166
- }
167
- }
168
135
  ```
169
136
 
170
- Use this when your provider uses standard HMAC signatures. For providers with custom signing formats, pass your own `verify` function.
171
-
172
- ## Why `BaseAdapter` exists
173
-
174
- This package is framework-agnostic by design. It does not include Express/Next/Hono/Elysia adapters.
137
+ ## Runtime Support
175
138
 
176
- `BaseAdapter` exists to help consumers implement adapters consistently:
139
+ | Runtime | Minimum version |
140
+ | ------------------ | ------------------------------------------------ |
141
+ | Node.js | 18.0.0 (router), 19.0.0 (verification helper) |
142
+ | Bun | 1.0.0 |
143
+ | Deno | 1.42 |
144
+ | Cloudflare Workers | Any current release |
145
+ | Browsers | Latest evergreen (Chrome, Edge, Firefox, Safari) |
177
146
 
178
- - you only implement `toNormalizedRequest` and `toFrameworkResponse`
179
- - `BaseAdapter` handles the common `handleWebhook()` flow
180
- - teams can reuse one adapter implementation across all webhook routes
181
-
182
- ```ts
183
- import { BaseAdapter } from "@zap-studio/webhooks/adapters/base";
184
- import type {
185
- NormalizedRequest,
186
- NormalizedResponse,
187
- } from "@zap-studio/webhooks/types";
188
-
189
- // `BaseAdapter<TReq, TRes>` is generic over your framework request/response
190
- // types. Override the mapping members with arrow-property syntax.
191
- class MyHttpAdapter extends BaseAdapter {
192
- toNormalizedRequest = async (req: any): Promise<NormalizedRequest> => ({
193
- method: req.method,
194
- path: req.url,
195
- headers: new Headers(req.headers),
196
- rawBody: req.rawBody,
197
- });
198
-
199
- toFrameworkResponse = async (
200
- res: any,
201
- normalized: NormalizedResponse
202
- ): Promise<any> => {
203
- res.statusCode = normalized.status;
204
- res.end(
205
- typeof normalized.body === "string"
206
- ? normalized.body
207
- : JSON.stringify(normalized.body)
208
- );
209
- return res;
210
- };
211
- }
212
- ```
147
+ The router only needs the standard `Request`/`Response` APIs, available globally since Node.js 18. The verification helper additionally needs `globalThis.crypto.subtle`, which is global by default from Node.js 19 (on Node.js 18, pass the `--experimental-global-webcrypto` flag). In browsers, Web Crypto requires a secure context (HTTPS). Deno 1.42 is the first release that can install packages from JSR (`deno add jsr:@zap-studio/webhooks`).
213
148
 
214
149
  ## License
215
150
 
@@ -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
  /**
@@ -20,4 +36,4 @@ declare class VerificationError extends Error {
20
36
  }
21
37
  //#endregion
22
38
  export { VerificationError };
23
- //# sourceMappingURL=errors.d.mts.map
39
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","names":[],"sources":["../src/errors.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;cA4Ba,0BAA0B;;;;;;EAMrC,YAAY"}
@@ -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
  /**
@@ -24,4 +40,4 @@ var VerificationError = class extends Error {
24
40
  //#endregion
25
41
  export { VerificationError };
26
42
 
27
- //# sourceMappingURL=errors.mjs.map
43
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +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 *\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"}
@@ -0,0 +1,5 @@
1
+ import { VerificationError } from "./errors.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";
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 ADDED
@@ -0,0 +1,4 @@
1
+ import { VerificationError } from "./errors.js";
2
+ import { WebhookRouter, createWebhookRouter } from "./router.js";
3
+ import { constantTimeEquals, createHmacVerifier } from "./verify.js";
4
+ export { VerificationError, WebhookRouter, constantTimeEquals, createHmacVerifier, createWebhookRouter };
@@ -0,0 +1,137 @@
1
+ import { InferSchemaOutput, RegisterOptions, SchemaRouteOptions, WebhookHandler, WebhookRouterOptions } from "./types.js";
2
+ import { StandardSchemaV1 } from "@zap-studio/validation";
3
+ //#region src/router.d.ts
4
+ /**
5
+ * Main webhook router class.
6
+ *
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
+ * ```
24
+ */
25
+ declare class WebhookRouter<TMap = unknown> {
26
+ private readonly handlers;
27
+ private readonly verify;
28
+ private readonly globalBeforeHooks;
29
+ private readonly globalAfterHooks;
30
+ private readonly globalErrorHook;
31
+ private readonly prefix;
32
+ private readonly prefixWithSlash;
33
+ /**
34
+ * Creates a webhook router with optional global hooks and verification behavior.
35
+ *
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
+ * ```
46
+ */
47
+ constructor(opts?: WebhookRouterOptions);
48
+ /**
49
+ * Register a webhook handler for a specific path.
50
+ *
51
+ * When a schema is provided, `payload` is inferred from the schema output type.
52
+ *
53
+ * @param path - Route path relative to configured prefix, starting with `/` (e.g. `"/stripe"`).
54
+ * @param handlerOrOptions - Handler function or schema-based registration options.
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
+ * ```
66
+ */
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
+ */
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
+ */
96
+ register<Path extends `/${string}`>(path: Path, handlerOrOptions: WebhookHandler): WebhookRouter<TMap & Record<Path, unknown>>;
97
+ /**
98
+ * Handles an incoming webhook request.
99
+ *
100
+ * The request body is read exactly once; hooks and handlers receive the raw
101
+ * bytes through the webhook context instead of the request stream.
102
+ *
103
+ * @param request - Incoming Web API request.
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
+ * ```
113
+ */
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. */
116
+ private matchPath;
117
+ /** Builds the error response for a failed request, deferring to the global error hook when set. */
118
+ private handleError;
119
+ }
120
+ /**
121
+ * Factory helper for creating a webhook router instance.
122
+ *
123
+ * @param opts - Optional global router options.
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
+ * ```
133
+ */
134
+ declare const createWebhookRouter: (opts?: WebhookRouterOptions) => WebhookRouter;
135
+ //#endregion
136
+ export { WebhookRouter, createWebhookRouter };
137
+ //# sourceMappingURL=router.d.ts.map
@@ -0,0 +1 @@
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"}