@zap-studio/webhooks 0.2.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +1 -1
  3. package/README.md +93 -61
  4. package/dist/errors.d.ts +23 -0
  5. package/dist/errors.d.ts.map +1 -0
  6. package/dist/{errors.mjs → errors.js} +11 -1
  7. package/dist/errors.js.map +1 -0
  8. package/dist/index.d.ts +6 -0
  9. package/dist/index.js +5 -0
  10. package/dist/router.d.ts +80 -0
  11. package/dist/router.d.ts.map +1 -0
  12. package/dist/router.js +149 -0
  13. package/dist/router.js.map +1 -0
  14. package/dist/types.d.ts +93 -0
  15. package/dist/types.d.ts.map +1 -0
  16. package/dist/types.js +0 -0
  17. package/dist/utils.d.ts +18 -0
  18. package/dist/utils.d.ts.map +1 -0
  19. package/dist/{utils/index.mjs → utils.js} +9 -4
  20. package/dist/utils.js.map +1 -0
  21. package/dist/verify.d.ts +49 -0
  22. package/dist/verify.d.ts.map +1 -0
  23. package/dist/{verify.mjs → verify.js} +23 -21
  24. package/dist/verify.js.map +1 -0
  25. package/package.json +15 -19
  26. package/dist/adapters/base.d.mts +0 -58
  27. package/dist/adapters/base.d.mts.map +0 -1
  28. package/dist/adapters/base.mjs +0 -26
  29. package/dist/adapters/base.mjs.map +0 -1
  30. package/dist/errors.d.mts +0 -13
  31. package/dist/errors.d.mts.map +0 -1
  32. package/dist/errors.mjs.map +0 -1
  33. package/dist/index.d.mts +0 -70
  34. package/dist/index.d.mts.map +0 -1
  35. package/dist/index.mjs +0 -159
  36. package/dist/index.mjs.map +0 -1
  37. package/dist/types/index.d.mts +0 -93
  38. package/dist/types/index.d.mts.map +0 -1
  39. package/dist/types/index.mjs +0 -1
  40. package/dist/utils/index.d.mts +0 -13
  41. package/dist/utils/index.d.mts.map +0 -1
  42. package/dist/utils/index.mjs.map +0 -1
  43. package/dist/verify.d.mts +0 -54
  44. package/dist/verify.d.mts.map +0 -1
  45. package/dist/verify.mjs.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,27 @@
1
+ ## @zap-studio/webhooks@0.4.0
2
+
3
+ ### Rework the package on Web API `Request`/`Response` (breaking)
4
+
5
+ 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
+
7
+ Breaking changes:
8
+
9
+ - `handle(req: NormalizedRequest): Promise<NormalizedResponse>` → `handle(request: Request): Promise<Response>`.
10
+ - 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)`.
11
+ - 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.
12
+ - `Adapter`, `BaseAdapter`, and the `./adapters/base` export are removed. Node `http` users can bridge with `srvx` or `@hono/node-server`.
13
+ - 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`).
14
+
15
+ 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
+
17
+ ## @zap-studio/webhooks@0.3.0
18
+
19
+ ### Migrate to ultracite lint/format; make the adapter contract generic
20
+
21
+ `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
+
23
+ Also: `register()` now returns `this`, error hooks always receive a real `Error` instance, and `rawBody` is typed as `Uint8Array`.
24
+
1
25
  # @zap-studio/webhooks
2
26
 
3
27
  ## 0.2.2
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Alexandre Trotel
3
+ Copyright (c) 2026 alexandretrotel
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
@@ -1,6 +1,6 @@
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
5
  Works with any validation library that implements [Standard Schema](https://github.com/standard-schema/standard-schema), including Zod, Valibot, and ArkType.
6
6
 
@@ -20,7 +20,7 @@ Schemas are the source of truth, and payload types are inferred from them.
20
20
  ## Install
21
21
 
22
22
  ```bash
23
- pnpm add @zap-studio/webhooks
23
+ npm install @zap-studio/webhooks
24
24
  ```
25
25
 
26
26
  ## Quickstart
@@ -30,27 +30,70 @@ import { createWebhookRouter } from "@zap-studio/webhooks";
30
30
  import { z } from "zod";
31
31
 
32
32
  const router = createWebhookRouter({
33
- prefix: "/webhooks/",
33
+ prefix: "/webhooks", // default
34
34
  });
35
35
 
36
- router.register("payments/succeeded", {
36
+ router.register("/payments/succeeded", {
37
37
  schema: z.object({
38
38
  id: z.string(),
39
39
  amount: z.number().positive(),
40
40
  currency: z.string().length(3),
41
41
  }),
42
- handler: async ({ payload, ack }) => {
42
+ handler: ({ payload }) => {
43
43
  // payload is inferred from schema
44
- return ack({ status: 200, body: `processed ${payload.id}` });
44
+ return Response.json(`processed ${payload.id}`);
45
45
  },
46
46
  });
47
+
48
+ // Any fetch-compatible runtime: Bun, Deno, Cloudflare Workers, ...
49
+ export default {
50
+ fetch: (request: Request) => router.handle(request),
51
+ };
52
+ ```
53
+
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
63
+
64
+ Because `handle(request)` speaks fetch, integration is one line in most environments:
65
+
66
+ ```ts
67
+ // Bun / Deno / Cloudflare Workers
68
+ export default { fetch: (request: Request) => router.handle(request) };
69
+
70
+ // Next.js route handler (app/webhooks/[...path]/route.ts)
71
+ export const POST = (request: Request) => router.handle(request);
72
+
73
+ // Hono
74
+ app.all("/webhooks/*", (c) => router.handle(c.req.raw));
75
+ ```
76
+
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).
78
+
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:
82
+
83
+ ```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
+ }
47
89
  ```
48
90
 
91
+ Handlers additionally receive `payload`, the schema-validated body.
92
+
49
93
  ## GitHub webhook example
50
94
 
51
95
  ```ts
52
- import { createWebhookRouter } from "@zap-studio/webhooks";
53
- import { createHmacVerifier } from "@zap-studio/webhooks/verify";
96
+ import { createHmacVerifier, createWebhookRouter } from "@zap-studio/webhooks";
54
97
  import { z } from "zod";
55
98
 
56
99
  const router = createWebhookRouter({
@@ -60,16 +103,16 @@ const router = createWebhookRouter({
60
103
  }),
61
104
  });
62
105
 
63
- router.register("github/push", {
106
+ router.register("/github/push", {
64
107
  schema: z.object({
65
108
  ref: z.string(),
66
109
  repository: z.object({
67
110
  full_name: z.string(),
68
111
  }),
69
112
  }),
70
- handler: async ({ payload, ack }) => {
113
+ handler: ({ payload }) => {
71
114
  console.log(`[github] ${payload.repository.full_name} ${payload.ref}`);
72
- return ack();
115
+ return undefined; // default 200 "ok"
73
116
  },
74
117
  });
75
118
  ```
@@ -84,25 +127,29 @@ import { z } from "zod";
84
127
  const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
85
128
 
86
129
  const router = createWebhookRouter({
87
- verify: async (req) => {
88
- const signature = req.headers.get("stripe-signature");
130
+ verify: ({ request, rawBody }) => {
131
+ const signature = request.headers.get("stripe-signature");
89
132
  if (!signature) {
90
133
  throw new Error("Missing Stripe signature");
91
134
  }
92
135
 
93
- stripe.webhooks.constructEvent(req.rawBody, signature, process.env.STRIPE_WEBHOOK_SECRET!);
136
+ stripe.webhooks.constructEvent(
137
+ Buffer.from(rawBody),
138
+ signature,
139
+ process.env.STRIPE_WEBHOOK_SECRET!
140
+ );
94
141
  },
95
142
  });
96
143
 
97
- router.register("stripe/payment_intent.succeeded", {
144
+ router.register("/stripe/payment_intent.succeeded", {
98
145
  schema: z.object({
99
146
  id: z.string(),
100
147
  object: z.literal("event"),
101
148
  type: z.literal("payment_intent.succeeded"),
102
149
  }),
103
- handler: async ({ payload, ack }) => {
150
+ handler: ({ payload }) => {
104
151
  console.log(`[stripe] event ${payload.id} (${payload.type})`);
105
- return ack({ status: 200 });
152
+ return Response.json("received");
106
153
  },
107
154
  });
108
155
  ```
@@ -117,27 +164,35 @@ Lifecycle hooks let you apply cross-cutting behavior without duplicating code in
117
164
 
118
165
  ```ts
119
166
  const router = createWebhookRouter({
120
- before: (req) => {
121
- console.log("incoming", req.path);
167
+ before: (ctx) => {
168
+ console.log("incoming", ctx.path);
122
169
  },
123
- after: (_req, res) => {
124
- console.log("status", res.status);
170
+ after: (_ctx, response) => {
171
+ console.log("status", response.status);
172
+ },
173
+ onError: (error) => Response.json({ error: error.message }, { status: 500 }),
174
+ });
175
+ ```
176
+
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);
125
184
  },
126
- onError: (error) => ({
127
- status: 500,
128
- body: { error: error.message },
129
- }),
130
185
  });
131
186
  ```
132
187
 
133
188
  ## Verification helper
134
189
 
135
- `@zap-studio/webhooks/verify` exports `createHmacVerifier`, a small helper that builds a `verify` function for HMAC-signed webhook providers.
190
+ `@zap-studio/webhooks` exports `createHmacVerifier`, a small helper that builds a `verify` function for HMAC-signed webhook providers.
136
191
 
137
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`.
138
193
 
139
194
  - reads a signature from the header you choose
140
- - computes an HMAC from `req.rawBody`
195
+ - computes an HMAC from `ctx.rawBody`
141
196
  - compares signatures in constant time
142
197
  - uses the Web Crypto API instead of Node `crypto`
143
198
  - works across runtimes that provide `globalThis.crypto.subtle`
@@ -145,8 +200,7 @@ It does not depend on Node APIs. The verifier uses the Web Crypto API, so it wor
145
200
  - throws `VerificationError` on verifier setup or signature failures
146
201
 
147
202
  ```ts
148
- import { createHmacVerifier } from "@zap-studio/webhooks/verify";
149
- import { VerificationError } from "@zap-studio/webhooks/errors";
203
+ import { createHmacVerifier, VerificationError } from "@zap-studio/webhooks";
150
204
 
151
205
  const verify = createHmacVerifier({
152
206
  headerName: "x-hub-signature-256",
@@ -155,7 +209,7 @@ const verify = createHmacVerifier({
155
209
  });
156
210
 
157
211
  try {
158
- await verify(req);
212
+ await verify(ctx);
159
213
  } catch (error) {
160
214
  if (error instanceof VerificationError) {
161
215
  console.error("webhook verification failed", error.message);
@@ -165,39 +219,17 @@ try {
165
219
 
166
220
  Use this when your provider uses standard HMAC signatures. For providers with custom signing formats, pass your own `verify` function.
167
221
 
168
- ## Why `BaseAdapter` exists
222
+ ## Runtime Support
169
223
 
170
- This package is framework-agnostic by design. It does not include Express/Next/Hono/Elysia adapters.
224
+ | Runtime | Minimum version |
225
+ | ------------------ | ------------------------------------------------ |
226
+ | Node.js | 18.0.0 (router), 19.0.0 (verification helper) |
227
+ | Bun | 1.0.0 |
228
+ | Deno | 1.42 |
229
+ | Cloudflare Workers | Any current release |
230
+ | Browsers | Latest evergreen (Chrome, Edge, Firefox, Safari) |
171
231
 
172
- `BaseAdapter` exists to help consumers implement adapters consistently:
173
-
174
- - you only implement `toNormalizedRequest` and `toFrameworkResponse`
175
- - `BaseAdapter` handles the common `handleWebhook()` flow
176
- - teams can reuse one adapter implementation across all webhook routes
177
-
178
- ```ts
179
- import { BaseAdapter } from "@zap-studio/webhooks/adapters/base";
180
- import type { NormalizedRequest, NormalizedResponse } from "@zap-studio/webhooks/types";
181
-
182
- class MyHttpAdapter extends BaseAdapter {
183
- async toNormalizedRequest(req: any): Promise<NormalizedRequest> {
184
- return {
185
- method: req.method,
186
- path: req.url,
187
- headers: new Headers(req.headers),
188
- rawBody: req.rawBody,
189
- };
190
- }
191
-
192
- async toFrameworkResponse(res: any, normalized: NormalizedResponse): Promise<any> {
193
- res.statusCode = normalized.status;
194
- res.end(
195
- typeof normalized.body === "string" ? normalized.body : JSON.stringify(normalized.body),
196
- );
197
- return res;
198
- }
199
- }
200
- ```
232
+ 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`).
201
233
 
202
234
  ## License
203
235
 
@@ -0,0 +1,23 @@
1
+ //#region src/errors.d.ts
2
+ /**
3
+ * Error primitives for webhook verification failures.
4
+ *
5
+ * @module @zap-studio/webhooks/errors
6
+ */
7
+ /**
8
+ * Error thrown when webhook request verification fails.
9
+ *
10
+ * This error is used by verifier helpers such as `createHmacVerifier` so
11
+ * callers can distinguish verification failures from other webhook errors.
12
+ */
13
+ declare class VerificationError extends Error {
14
+ /**
15
+ * Creates a verification error with a human-readable message.
16
+ *
17
+ * @param message - Error message describing the verification failure.
18
+ */
19
+ constructor(message: string);
20
+ }
21
+ //#endregion
22
+ export { VerificationError };
23
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","names":[],"sources":["../src/errors.ts"],"mappings":";;;;;;;;;;;;cAYa,0BAA0B;;;;;;EAMrC,YAAY"}
@@ -1,11 +1,21 @@
1
1
  //#region src/errors.ts
2
2
  /**
3
+ * Error primitives for webhook verification failures.
4
+ *
5
+ * @module @zap-studio/webhooks/errors
6
+ */
7
+ /**
3
8
  * Error thrown when webhook request verification fails.
4
9
  *
5
10
  * This error is used by verifier helpers such as `createHmacVerifier` so
6
11
  * callers can distinguish verification failures from other webhook errors.
7
12
  */
8
13
  var VerificationError = class extends Error {
14
+ /**
15
+ * Creates a verification error with a human-readable message.
16
+ *
17
+ * @param message - Error message describing the verification failure.
18
+ */
9
19
  constructor(message) {
10
20
  super(message);
11
21
  this.name = "VerificationError";
@@ -14,4 +24,4 @@ var VerificationError = class extends Error {
14
24
  //#endregion
15
25
  export { VerificationError };
16
26
 
17
- //# sourceMappingURL=errors.mjs.map
27
+ //# 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 */\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"}
@@ -0,0 +1,6 @@
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";
6
+ 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,5 @@
1
+ import { VerificationError } from "./errors.js";
2
+ import { WebhookRouter, createWebhookRouter } from "./router.js";
3
+ import { constantTimeEquals } from "./utils.js";
4
+ import { createHmacVerifier } from "./verify.js";
5
+ export { VerificationError, WebhookRouter, constantTimeEquals, createHmacVerifier, createWebhookRouter };
@@ -0,0 +1,80 @@
1
+ import { AfterHook, BeforeHook, ErrorHook, InferSchemaOutput, RegisterOptions, SchemaRouteOptions, VerifyFn, WebhookHandler } from "./types.js";
2
+ import { StandardSchemaV1 } from "@zap-studio/validation";
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
+ /**
22
+ * Main webhook router class.
23
+ *
24
+ * Register routes with typed schemas and call `handle` with a Web API `Request`.
25
+ */
26
+ declare class WebhookRouter<TMap = unknown> {
27
+ private readonly handlers;
28
+ private readonly verify;
29
+ private readonly globalBeforeHooks;
30
+ private readonly globalAfterHooks;
31
+ private readonly globalErrorHook;
32
+ private readonly prefix;
33
+ private readonly prefixWithSlash;
34
+ /**
35
+ * Creates a webhook router with optional global hooks and verification behavior.
36
+ *
37
+ * @param opts - Router-level options.
38
+ */
39
+ constructor(opts?: WebhookRouterOptions);
40
+ /**
41
+ * Register a webhook handler for a specific path.
42
+ *
43
+ * When a schema is provided, `payload` is inferred from the schema output type.
44
+ *
45
+ * @param path - Route path relative to configured prefix, starting with `/` (e.g. `"/stripe"`).
46
+ * @param handlerOrOptions - Handler function or schema-based registration options.
47
+ * @returns The same router instance with an updated internal route type map.
48
+ */
49
+ register<Path extends `/${string}`, TSchema extends StandardSchemaV1<unknown, unknown>>(path: Path, handlerOrOptions: SchemaRouteOptions<TSchema>): WebhookRouter<TMap & Record<Path, InferSchemaOutput<TSchema>>>;
50
+ register<Path extends `/${string}`, TPayload>(path: Path, handlerOrOptions: RegisterOptions<TPayload>): WebhookRouter<TMap & Record<Path, TPayload>>;
51
+ register<Path extends `/${string}`>(path: Path, handlerOrOptions: WebhookHandler): WebhookRouter<TMap & Record<Path, unknown>>;
52
+ /**
53
+ * Handles an incoming webhook request.
54
+ *
55
+ * The request body is read exactly once; hooks and handlers receive the raw
56
+ * bytes through the webhook context instead of the request stream.
57
+ *
58
+ * @param request - Incoming Web API request.
59
+ * @returns Web API response for the runtime to send back.
60
+ */
61
+ handle(request: Request): Promise<Response>;
62
+ 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;
69
+ private handleError;
70
+ }
71
+ /**
72
+ * Factory helper for creating a webhook router instance.
73
+ *
74
+ * @param opts - Optional global router options.
75
+ * @returns A new webhook router.
76
+ */
77
+ declare const createWebhookRouter: (opts?: WebhookRouterOptions) => WebhookRouter;
78
+ //#endregion
79
+ export { WebhookRouter, WebhookRouterOptions, createWebhookRouter };
80
+ //# sourceMappingURL=router.d.ts.map
@@ -0,0 +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"}
package/dist/router.js ADDED
@@ -0,0 +1,149 @@
1
+ import { standardValidate } from "@zap-studio/validation";
2
+ //#region src/router.ts
3
+ const toArray = (value) => {
4
+ if (value === void 0) return [];
5
+ return Array.isArray(value) ? value : [value];
6
+ };
7
+ const notFoundResponse = () => Response.json({ error: "not found" }, { status: 404 });
8
+ const bodyDecoder = new TextDecoder();
9
+ /**
10
+ * Normalizes a path to its canonical form: leading slash, no trailing slash,
11
+ * duplicate slashes collapsed. The root path is `"/"`.
12
+ */
13
+ const normalizePath = (path) => {
14
+ const withLeadingSlash = path.startsWith("/") ? path : `/${path}`;
15
+ const collapsed = withLeadingSlash.includes("//") ? withLeadingSlash.replaceAll(/\/{2,}/gu, "/") : withLeadingSlash;
16
+ return collapsed.length > 1 && collapsed.endsWith("/") ? collapsed.slice(0, -1) : collapsed;
17
+ };
18
+ /**
19
+ * Main webhook router class.
20
+ *
21
+ * Register routes with typed schemas and call `handle` with a Web API `Request`.
22
+ */
23
+ var WebhookRouter = class WebhookRouter {
24
+ handlers = /* @__PURE__ */ new Map();
25
+ verify;
26
+ globalBeforeHooks = [];
27
+ globalAfterHooks = [];
28
+ globalErrorHook;
29
+ prefix;
30
+ prefixWithSlash;
31
+ /**
32
+ * Creates a webhook router with optional global hooks and verification behavior.
33
+ *
34
+ * @param opts - Router-level options.
35
+ */
36
+ constructor(opts = {}) {
37
+ this.prefix = normalizePath(opts.prefix ?? "/webhooks");
38
+ this.prefixWithSlash = `${this.prefix}/`;
39
+ this.verify = opts.verify;
40
+ this.globalBeforeHooks = toArray(opts.before);
41
+ this.globalAfterHooks = toArray(opts.after);
42
+ this.globalErrorHook = opts.onError;
43
+ }
44
+ register(path, handlerOrOptions) {
45
+ this.handlers.set(normalizePath(path), typeof handlerOrOptions === "function" ? { handler: handlerOrOptions } : WebhookRouter.createHandlerEntry(handlerOrOptions));
46
+ return this;
47
+ }
48
+ /**
49
+ * Handles an incoming webhook request.
50
+ *
51
+ * The request body is read exactly once; hooks and handlers receive the raw
52
+ * bytes through the webhook context instead of the request stream.
53
+ *
54
+ * @param request - Incoming Web API request.
55
+ * @returns Web API response for the runtime to send back.
56
+ */
57
+ async handle(request) {
58
+ const path = this.matchPath(request);
59
+ if (path === null) return notFoundResponse();
60
+ const handlerEntry = this.handlers.get(path);
61
+ if (!handlerEntry) return notFoundResponse();
62
+ const ctx = {
63
+ path,
64
+ rawBody: /* @__PURE__ */ new Uint8Array(0),
65
+ request
66
+ };
67
+ try {
68
+ ctx.rawBody = new Uint8Array(await request.arrayBuffer());
69
+ await WebhookRouter.runBeforeHooks(ctx, this.globalBeforeHooks);
70
+ await WebhookRouter.runBeforeHooks(ctx, handlerEntry.before);
71
+ if (this.verify) await this.verify(ctx);
72
+ const parsedJson = WebhookRouter.parseRequestBody(ctx);
73
+ const validationResult = await WebhookRouter.validatePayload(parsedJson, handlerEntry.schema);
74
+ if (validationResult instanceof Response) return validationResult;
75
+ const response = await WebhookRouter.executeHandler(handlerEntry.handler, ctx, validationResult);
76
+ await WebhookRouter.runAfterHooks(ctx, response, handlerEntry.after);
77
+ await WebhookRouter.runAfterHooks(ctx, response, this.globalAfterHooks);
78
+ return response;
79
+ } catch (error) {
80
+ return await this.handleError(error, ctx);
81
+ }
82
+ }
83
+ matchPath(request) {
84
+ const pathname = normalizePath(new URL(request.url).pathname);
85
+ if (this.prefix === "/") return pathname;
86
+ if (pathname === this.prefix) return "/";
87
+ if (!pathname.startsWith(this.prefixWithSlash)) return null;
88
+ return pathname.slice(this.prefix.length);
89
+ }
90
+ static async runBeforeHooks(ctx, hooks) {
91
+ if (!hooks || hooks.length === 0) return;
92
+ for (const hook of hooks) await hook(ctx);
93
+ }
94
+ static async runAfterHooks(ctx, response, hooks) {
95
+ if (!hooks || hooks.length === 0) return;
96
+ for (const hook of hooks) await hook(ctx, response);
97
+ }
98
+ static createHandlerEntry(options) {
99
+ const entry = { handler: options.handler };
100
+ if (options.schema !== void 0) entry.schema = options.schema;
101
+ if (options.before !== void 0) entry.before = Array.isArray(options.before) ? options.before : [options.before];
102
+ if (options.after !== void 0) entry.after = Array.isArray(options.after) ? options.after : [options.after];
103
+ return entry;
104
+ }
105
+ static parseRequestBody(ctx) {
106
+ try {
107
+ return JSON.parse(bodyDecoder.decode(ctx.rawBody));
108
+ } catch {
109
+ return;
110
+ }
111
+ }
112
+ static async validatePayload(parsedJson, schema) {
113
+ if (!schema) return parsedJson;
114
+ const result = await standardValidate(schema, parsedJson, { throwOnError: false });
115
+ if (result.issues) return Response.json({
116
+ error: "validation failed",
117
+ issues: result.issues.map((issue) => ({
118
+ message: issue.message,
119
+ path: issue.path?.map((p) => typeof p === "object" && "key" in p ? String(p.key) : String(p))
120
+ }))
121
+ }, { status: 400 });
122
+ return result.value;
123
+ }
124
+ static async executeHandler(handler, ctx, validatedPayload) {
125
+ return await handler({
126
+ ...ctx,
127
+ payload: validatedPayload
128
+ }) ?? Response.json("ok");
129
+ }
130
+ async handleError(error, ctx) {
131
+ if (this.globalErrorHook) {
132
+ const normalizedError = error instanceof Error ? error : /* @__PURE__ */ new Error("Internal server error");
133
+ const errorResponse = await this.globalErrorHook(normalizedError, ctx);
134
+ if (errorResponse) return errorResponse;
135
+ }
136
+ return Response.json({ error: error instanceof Error ? error.message : "Internal server error" }, { status: 500 });
137
+ }
138
+ };
139
+ /**
140
+ * Factory helper for creating a webhook router instance.
141
+ *
142
+ * @param opts - Optional global router options.
143
+ * @returns A new webhook router.
144
+ */
145
+ const createWebhookRouter = (opts) => new WebhookRouter(opts);
146
+ //#endregion
147
+ export { WebhookRouter, createWebhookRouter };
148
+
149
+ //# sourceMappingURL=router.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router.js","names":[],"sources":["../src/router.ts"],"sourcesContent":["/**\n * Schema-first webhook router primitives.\n *\n * @module @zap-studio/webhooks/router\n */\n\nimport type { StandardSchemaV1 } from \"@zap-studio/validation\";\nimport { standardValidate } from \"@zap-studio/validation\";\n\nimport type {\n AfterHook,\n BeforeHook,\n ErrorHook,\n InferSchemaOutput,\n RegisterOptions,\n SchemaRouteOptions,\n VerifyFn,\n WebhookContext,\n WebhookHandler,\n} from \"./types.js\";\n\n/**\n * Schema-first webhook router with path dispatching, validation, and optional verification.\n *\n * @template TMap - Internal route payload map built incrementally via `register`.\n */\ninterface HandlerEntry<TPayload = unknown> {\n after?: AfterHook[];\n before?: BeforeHook[];\n handler: WebhookHandler<TPayload>;\n schema?: StandardSchemaV1<unknown, TPayload>;\n}\n\nexport interface WebhookRouterOptions {\n /** Global hooks executed after successful route handler completion. */\n after?: AfterHook | AfterHook[];\n /** Global hooks executed before route-level hooks and verification. */\n before?: BeforeHook | BeforeHook[];\n /** Global error hook used to override the default `500` response. */\n onError?: ErrorHook;\n /**\n * Required path prefix for all webhook routes. Defaults to `\"/webhooks\"`.\n *\n * Normalized internally: leading slash added, trailing slash stripped,\n * duplicate slashes collapsed. Use `\"\"` or `\"/\"` to mount at the root.\n */\n prefix?: string;\n /** Optional request verification function (for signature checks, auth, etc.). */\n verify?: VerifyFn;\n}\n\nconst toArray = <T>(value: T | T[] | undefined): T[] => {\n if (value === undefined) {\n return [];\n }\n\n return Array.isArray(value) ? value : [value];\n};\n\nconst notFoundResponse = (): Response =>\n Response.json({ error: \"not found\" }, { status: 404 });\n\nconst bodyDecoder = new TextDecoder();\n\n/**\n * Normalizes a path to its canonical form: leading slash, no trailing slash,\n * duplicate slashes collapsed. The root path is `\"/\"`.\n */\nconst normalizePath = (path: string): string => {\n const withLeadingSlash = path.startsWith(\"/\") ? path : `/${path}`;\n const collapsed = withLeadingSlash.includes(\"//\")\n ? withLeadingSlash.replaceAll(/\\/{2,}/gu, \"/\")\n : withLeadingSlash;\n\n return collapsed.length > 1 && collapsed.endsWith(\"/\")\n ? collapsed.slice(0, -1)\n : collapsed;\n};\n\n/**\n * Main webhook router class.\n *\n * Register routes with typed schemas and call `handle` with a Web API `Request`.\n */\nexport class WebhookRouter<TMap = unknown> {\n private readonly handlers = new Map<string, HandlerEntry>();\n private readonly verify: VerifyFn | undefined;\n private readonly globalBeforeHooks: BeforeHook[] = [];\n private readonly globalAfterHooks: AfterHook[] = [];\n private readonly globalErrorHook: ErrorHook | undefined;\n private readonly prefix: string;\n private readonly prefixWithSlash: string;\n\n /**\n * Creates a webhook router with optional global hooks and verification behavior.\n *\n * @param opts - Router-level options.\n */\n constructor(opts: WebhookRouterOptions = {}) {\n this.prefix = normalizePath(opts.prefix ?? \"/webhooks\");\n this.prefixWithSlash = `${this.prefix}/`;\n this.verify = opts.verify;\n this.globalBeforeHooks = toArray(opts.before);\n this.globalAfterHooks = toArray(opts.after);\n this.globalErrorHook = opts.onError;\n }\n\n /**\n * Register a webhook handler for a specific path.\n *\n * When a schema is provided, `payload` is inferred from the schema output type.\n *\n * @param path - Route path relative to configured prefix, starting with `/` (e.g. `\"/stripe\"`).\n * @param handlerOrOptions - Handler function or schema-based registration options.\n * @returns The same router instance with an updated internal route type map.\n */\n register<\n Path extends `/${string}`,\n TSchema extends StandardSchemaV1<unknown, unknown>,\n >(\n path: Path,\n handlerOrOptions: SchemaRouteOptions<TSchema>\n ): WebhookRouter<TMap & Record<Path, InferSchemaOutput<TSchema>>>;\n register<Path extends `/${string}`, TPayload>(\n path: Path,\n handlerOrOptions: RegisterOptions<TPayload>\n ): WebhookRouter<TMap & Record<Path, TPayload>>;\n register<Path extends `/${string}`>(\n path: Path,\n handlerOrOptions: WebhookHandler\n ): WebhookRouter<TMap & Record<Path, unknown>>;\n register(\n path: string,\n handlerOrOptions: WebhookHandler | RegisterOptions<unknown>\n ): this {\n this.handlers.set(\n normalizePath(path),\n typeof handlerOrOptions === \"function\"\n ? { handler: handlerOrOptions }\n : WebhookRouter.createHandlerEntry(handlerOrOptions)\n );\n\n return this;\n }\n\n /**\n * Handles an incoming webhook request.\n *\n * The request body is read exactly once; hooks and handlers receive the raw\n * bytes through the webhook context instead of the request stream.\n *\n * @param request - Incoming Web API request.\n * @returns Web API response for the runtime to send back.\n */\n async handle(request: Request): Promise<Response> {\n const path = this.matchPath(request);\n if (path === null) {\n return notFoundResponse();\n }\n\n const handlerEntry = this.handlers.get(path);\n if (!handlerEntry) {\n return notFoundResponse();\n }\n\n const ctx: WebhookContext = {\n path,\n rawBody: new Uint8Array(0),\n request,\n };\n\n try {\n ctx.rawBody = new Uint8Array(await request.arrayBuffer());\n\n await WebhookRouter.runBeforeHooks(ctx, this.globalBeforeHooks);\n await WebhookRouter.runBeforeHooks(ctx, handlerEntry.before);\n\n if (this.verify) {\n await this.verify(ctx);\n }\n\n const parsedJson = WebhookRouter.parseRequestBody(ctx);\n const validationResult = await WebhookRouter.validatePayload(\n parsedJson,\n handlerEntry.schema\n );\n\n if (validationResult instanceof Response) {\n return validationResult;\n }\n\n const response = await WebhookRouter.executeHandler(\n handlerEntry.handler,\n ctx,\n validationResult\n );\n\n await WebhookRouter.runAfterHooks(ctx, response, handlerEntry.after);\n await WebhookRouter.runAfterHooks(ctx, response, this.globalAfterHooks);\n\n return response;\n } catch (error) {\n return await this.handleError(error, ctx);\n }\n }\n\n private matchPath(request: Request): string | null {\n const pathname = normalizePath(new URL(request.url).pathname);\n\n // Root mount: the whole pathname is the route path.\n if (this.prefix === \"/\") {\n return pathname;\n }\n\n if (pathname === this.prefix) {\n return \"/\";\n }\n\n // Require prefix followed by a segment boundary, then match handlers on\n // the remainder (e.g. /webhooks/stripe -> /stripe).\n if (!pathname.startsWith(this.prefixWithSlash)) {\n return null;\n }\n\n return pathname.slice(this.prefix.length);\n }\n\n private static async runBeforeHooks(\n ctx: WebhookContext,\n hooks?: BeforeHook[]\n ): Promise<void> {\n if (!hooks || hooks.length === 0) {\n return;\n }\n\n for (const hook of hooks) {\n // oxlint-disable-next-line no-await-in-loop -- hooks run sequentially; order + short-circuit matter.\n await hook(ctx);\n }\n }\n\n private static async runAfterHooks(\n ctx: WebhookContext,\n response: Response,\n hooks?: AfterHook[]\n ): Promise<void> {\n if (!hooks || hooks.length === 0) {\n return;\n }\n\n for (const hook of hooks) {\n // oxlint-disable-next-line no-await-in-loop -- hooks run sequentially; order + short-circuit matter.\n await hook(ctx, response);\n }\n }\n\n private static createHandlerEntry(\n options: RegisterOptions<unknown>\n ): HandlerEntry {\n const entry: HandlerEntry = {\n handler: options.handler,\n };\n\n if (options.schema !== undefined) {\n entry.schema = options.schema;\n }\n\n if (options.before !== undefined) {\n entry.before = Array.isArray(options.before)\n ? options.before\n : [options.before];\n }\n\n if (options.after !== undefined) {\n entry.after = Array.isArray(options.after)\n ? options.after\n : [options.after];\n }\n\n return entry;\n }\n\n private static parseRequestBody(ctx: WebhookContext): unknown {\n try {\n return JSON.parse(bodyDecoder.decode(ctx.rawBody)) as unknown;\n } catch {\n return undefined;\n }\n }\n\n private static async validatePayload<TPayload>(\n parsedJson: unknown,\n schema?: StandardSchemaV1<unknown, TPayload>\n ): Promise<TPayload | Response> {\n if (!schema) {\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- Without a schema, caller-declared payload type is the route contract.\n return parsedJson as TPayload;\n }\n\n const result = await standardValidate(schema, parsedJson, {\n throwOnError: false,\n });\n\n if (result.issues) {\n return Response.json(\n {\n error: \"validation failed\",\n issues: result.issues.map((issue) => ({\n message: issue.message,\n path: issue.path?.map((p) =>\n typeof p === \"object\" && \"key\" in p ? String(p.key) : String(p)\n ),\n })),\n },\n { status: 400 }\n );\n }\n\n return result.value;\n }\n\n private static async executeHandler<TPayload = unknown>(\n handler: WebhookHandler<TPayload>,\n ctx: WebhookContext,\n validatedPayload: TPayload\n ): Promise<Response> {\n const responded = await handler({\n ...ctx,\n payload: validatedPayload,\n });\n\n return responded ?? Response.json(\"ok\");\n }\n\n private async handleError(\n error: unknown,\n ctx: WebhookContext\n ): Promise<Response> {\n if (this.globalErrorHook) {\n const normalizedError =\n error instanceof Error ? error : new Error(\"Internal server error\");\n const errorResponse = await this.globalErrorHook(normalizedError, ctx);\n if (errorResponse) {\n return errorResponse;\n }\n }\n\n return Response.json(\n {\n error: error instanceof Error ? error.message : \"Internal server error\",\n },\n { status: 500 }\n );\n }\n}\n\n/**\n * Factory helper for creating a webhook router instance.\n *\n * @param opts - Optional global router options.\n * @returns A new webhook router.\n */\nexport const createWebhookRouter = (\n opts?: WebhookRouterOptions\n): WebhookRouter => new WebhookRouter(opts);\n"],"mappings":"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"}