@zap-studio/webhooks 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,37 +4,65 @@ All notable changes to this project will be documented in this file.
4
4
 
5
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
+ ## [2.1.0]
8
+
9
+ ### Added
10
+
11
+ Route-level `verify` on `register()`. Previously `verify` was only a router-wide `createWebhookRouter({ verify })` option, applied to every route — a router handling more than one provider had to hand-roll a dispatcher keyed on `ctx.path` to give each route its own verifier. Now each route can carry its own `verify` directly:
12
+
13
+ ```ts
14
+ router.register("/stripe", {
15
+ schema: stripeEventSchema,
16
+ verify: createHmacVerifier({ headerName: "stripe-signature", secret: stripeSecret }),
17
+ handler: stripeHandler,
18
+ });
19
+
20
+ router.register("/github", {
21
+ schema: githubEventSchema,
22
+ verify: createHmacVerifier({ headerName: "x-hub-signature-256", secret: githubSecret }),
23
+ handler: githubHandler,
24
+ });
25
+ ```
26
+
27
+ A route's own `verify` overrides the router-level one when both are set; the router-level `verify` still applies as the default for routes that don't set their own. Fully backward compatible — existing single-provider setups using only the router-level option are unaffected.
28
+
29
+ ## [2.0.1]
30
+
31
+ ### Fixed
32
+
33
+ The `@opentelemetry/api` peer dependency was published as the raw pnpm `catalog:` protocol string instead of a resolved version range, an invalid semver range. This release republishes with it resolved.
34
+
7
35
  ## [2.0.0]
8
36
 
9
37
  ### Added
10
38
 
11
- Native OpenTelemetry support. Every `handle(request)` call gets a `SERVER` delivery span, extracting the sender's `traceparent` header so the delivery continues their trace instead of starting a new one. Each handler dispatch gets its own child `INTERNAL` span. A non-2xx response marks the delivery span `ERROR`; a thrown handler error is also recorded as an exception on the handler span. See [OpenTelemetry](https://www.zapstudio.dev/webhooks/opentelemetry).
39
+ Native OpenTelemetry support. Every `handle(request)` call now gets a `SERVER` delivery span. It reads the sender's `traceparent` header, so the delivery continues their trace instead of starting a new one. Each handler dispatch gets its own child `INTERNAL` span. A non-2xx response marks the delivery span `ERROR`. A thrown handler error is also recorded as an exception on the handler span. See [OpenTelemetry](https://www.zapstudio.dev/webhooks/opentelemetry).
12
40
 
13
41
  ### Changed
14
42
 
15
- **Breaking:** `@opentelemetry/api` is now a required peer dependency. It's tiny, side-effect-free, and a no-op until an app registers a real SDK, so nothing changes at runtime for consumers who don't set one up but the package won't resolve without it installed: `npm install @opentelemetry/api`.
43
+ **Breaking:** `@opentelemetry/api` is now a required peer dependency. It is small, has no side effects, and does nothing until an app registers a real SDK. So nothing changes at runtime if you don't set one up. But the package will not resolve unless it is installed: `npm install @opentelemetry/api`.
16
44
 
17
45
  ## [1.1.1]
18
46
 
19
47
  ### Changed
20
48
 
21
- `@zap-studio/logger` is now an optional peer dependency instead of a regular dependency. Every import from it is type-only (`import type { Logger }`), so it was never pulled in at runtime pass any object matching the `Logger` shape (including `pino`) with no install required. Existing consumers of `logger?: Logger` are unaffected.
49
+ `@zap-studio/logger` is now an optional peer dependency, not a regular one. Every import from it is type-only (`import type { Logger }`), so it was never loaded at runtime anyway. You can pass any object with the `Logger` shape (`pino` included) with no install needed. This does not affect existing use of `logger?: Logger`.
22
50
 
23
51
  ## [1.1.0]
24
52
 
25
53
  ### Added
26
54
 
27
- `WebhookRouter` (and `createWebhookRouter(...)`) gain an optional `logger?: Logger` option (from `@zap-studio/logger`). When provided, it logs each delivery attempt and handler dispatch at `debug`, and verification failures and unmatched routes at `warn`. Omitting it keeps zero logging overhead. See [Logging](https://www.zapstudio.dev/webhooks/logging).
55
+ `WebhookRouter` (and `createWebhookRouter(...)`) now take an optional `logger?: Logger` option, from `@zap-studio/logger`. When you pass one, it logs each delivery attempt and handler dispatch at `debug`, and logs verification failures and unmatched routes at `warn`. Leave it out, and there is no logging cost at all. See [Logging](https://www.zapstudio.dev/webhooks/logging).
28
56
 
29
57
  ## [1.0.0]
30
58
 
31
59
  ### Changed
32
60
 
33
- `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.
61
+ `WebhookRouter`'s stateless private static methods `runBeforeHooks`, `runAfterHooks`, `createHandlerEntry`, `parseRequestBody`, `validatePayload`, `executeHandler` are now plain functions at the module level in `router.ts`. This is internal only. The public API (`WebhookRouter`, `createWebhookRouter`, `.register()`, `.handle()`) does not change.
34
62
 
35
- 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).
63
+ HMAC signature verification now decodes the header's hex signature into bytes, and compares it to the computed digest byte by byte. Before, it turned the digest into hex text and compared that text instead. Behavior stays the same for valid requests. This only changes internals: fewer bytes get compared, and the header's hex case no longer needs normalizing as text, since decoding handles the case on its own.
36
64
 
37
- `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`.
65
+ `constantTimeEquals` moved from `utils.ts` to `verify.ts`, its only user, and now compares `Uint8Array`s (bytes) instead of strings. It is still exported from `@zap-studio/webhooks` and `@zap-studio/webhooks/verify`.
38
66
 
39
67
  ### Removed
40
68
 
@@ -44,25 +72,25 @@ Removed the `./utils` subpath export.
44
72
 
45
73
  ### Changed
46
74
 
47
- 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.
75
+ The custom `NormalizedRequest`/`NormalizedResponse` contract is gone. `router.handle` now takes a standard Web API `Request` and returns a standard `Response`. So the router works directly with fetch-native runtimes Bun, Deno, Cloudflare Workers, Next.js route handlers, Hono with no adapter layer needed.
48
76
 
49
77
  **Breaking changes:**
50
78
 
51
79
  - `handle(req: NormalizedRequest): Promise<NormalizedResponse>` → `handle(request: Request): Promise<Response>`.
52
- - 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)`.
53
- - 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.
54
- - `Adapter`, `BaseAdapter`, and the `./adapters/base` export are removed. Node `http` users can bridge with `srvx` or `@hono/node-server`.
55
- - 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`).
80
+ - Handlers now get `{ request, rawBody, path, payload }` a `WebhookContext` plus the validated `payload` and return a `Response`, or `undefined` for the default `200 "ok"`. The `ack` helper is removed. Use `Response.json(body, init)` instead.
81
+ - Hooks and `verify` now use the context type: `BeforeHook(ctx)`, `AfterHook(ctx, response)`, `ErrorHook(error, ctx)`, `VerifyFn(ctx)`. An after hook must call `clone()` on the response before it reads the body.
82
+ - `Adapter`, `BaseAdapter`, and the `./adapters/base` export are removed. If you use Node's `http`, bridge with `srvx` or `@hono/node-server`.
83
+ - The prefix now always gets a trailing slash, and only matches at a path boundary. `prefix: "/api"` now behaves like `/api/`. So `/apihello` no longer matches a route before this fix, it matched the route `ihello`.
56
84
 
57
- 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.
85
+ Kept the same: hook order, how the prefix works (default `/webhooks/`), exact-match routing, HMAC verification, and the `404`/`400`/`500` error body shapes. One change: an unknown route now returns `404` without reading the request body.
58
86
 
59
87
  ## [0.3.0]
60
88
 
61
89
  ### Changed
62
90
 
63
- `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.
91
+ `Adapter` and `BaseAdapter` are now generic over the framework's request/response types: `Adapter<TReq, TRes>`, `BaseAdapter<TReq, TRes>`. This replaces the old per-method generics. The mapping members `toNormalizedRequest`, `toFrameworkResponse`, `handleWebhook` are now arrow properties. So a custom adapter must now override them as properties, not as methods.
64
92
 
65
- 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.
93
+ Also: `register()` now returns `this`. Error hooks always get a real `Error` instance. `rawBody` is now typed as `Uint8Array`. Internal formatting and lint cleanup moved to ultracite.
66
94
 
67
95
  ## [0.2.2]
68
96
 
@@ -74,22 +102,22 @@ Also: `register()` now returns `this`, error hooks always receive a real `Error`
74
102
 
75
103
  ### Changed
76
104
 
77
- - 5fa58b1: Reduced webhook router complexity by consolidating hook normalization and handler entry creation.
78
- - 7004e9f: Allow explicit `undefined` in option handling, then follow with d707800 to remove redundant `| undefined` unions from public types.
79
- - 9f31f87: Switched the package build to ESNext-aligned output and updated package tooling and publish metadata.
105
+ - 5fa58b1: Made the webhook router simpler. Hook normalization and handler entry creation are now combined.
106
+ - 7004e9f: Option handling now allows explicit `undefined`. d707800 then removed extra `| undefined` unions from public types that this made redundant.
107
+ - 9f31f87: Switched the package build to ESNext-aligned output, and updated package tooling and publish metadata.
80
108
  - Updated dependency `@zap-studio/validation` to `0.3.3`.
81
109
 
82
110
  ### Fixed
83
111
 
84
- - 3a950dc: Preserve registered hook assignment types while keeping the schema-first router API unchanged.
112
+ - 3a950dc: Kept the types of registered hook assignments correct, with no change to the schema-first router API.
85
113
 
86
114
  ## [0.2.0]
87
115
 
88
116
  ### Changed
89
117
 
90
- - c686862: Switch `createHmacVerifier` to Web Crypto and standardize the verifier around string secrets.
118
+ - c686862: Switched `createHmacVerifier` to Web Crypto, and made all verifiers use string secrets.
91
119
 
92
- 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.
120
+ This removes the Node `crypto` dependency from the verifier path. `req.rawBody` stays a `Uint8Array`. `createHmacVerifier` is now simpler: it takes a string secret. It also adds a public `VerificationError` in `@zap-studio/webhooks/errors`, for verifier setup and signature failures.
93
121
 
94
122
  ## [0.1.4]
95
123
 
@@ -109,7 +137,7 @@ Also: `register()` now returns `this`, error hooks always receive a real `Error`
109
137
 
110
138
  ### Fixed
111
139
 
112
- - 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.
140
+ - c209a27: Fixed payload schema validation internals to use the current async `standardValidate` options API (`{ throwOnError: false }`). This restores typecheck compatibility after a signature update to the validation helper.
113
141
 
114
142
  ## [0.1.1]
115
143
 
package/README.md CHANGED
@@ -104,11 +104,7 @@ router.register("/event", {
104
104
  Built-in HMAC verifier with constant-time comparison, or plug in your own `verify` function.
105
105
 
106
106
  ```ts
107
- import {
108
- createHmacVerifier,
109
- createWebhookRouter,
110
- VerificationError,
111
- } from "@zap-studio/webhooks";
107
+ import { createHmacVerifier, createWebhookRouter, VerificationError } from "@zap-studio/webhooks";
112
108
 
113
109
  const router = createWebhookRouter({
114
110
  verify: createHmacVerifier({
@@ -123,6 +119,22 @@ const router = createWebhookRouter({
123
119
  });
124
120
  ```
125
121
 
122
+ `verify` also works per-route on `register()`, overriding the router-level one — useful for a router handling multiple providers, each with its own signing scheme:
123
+
124
+ ```ts
125
+ router.register("/github", {
126
+ verify: createHmacVerifier({ headerName: "x-hub-signature-256", secret: githubSecret }),
127
+ schema: githubEventSchema,
128
+ handler: githubHandler,
129
+ });
130
+
131
+ router.register("/stripe", {
132
+ verify: stripeVerify, // a different scheme entirely
133
+ schema: stripeEventSchema,
134
+ handler: stripeHandler,
135
+ });
136
+ ```
137
+
126
138
  ## Lifecycle Hooks
127
139
 
128
140
  Global `before`, `after`, and `onError` hooks for cross-cutting behavior.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  import { VerificationError } from "./errors.js";
2
- import { n as createWebhookRouter, t as WebhookRouter } from "./router-DyyNi3QB.js";
2
+ import { n as createWebhookRouter, t as WebhookRouter } from "./router-Pf2VoqgE.js";
3
3
  import { constantTimeEquals, createHmacVerifier } from "./verify.js";
4
4
  export { VerificationError, WebhookRouter, constantTimeEquals, createHmacVerifier, createWebhookRouter };
@@ -7,7 +7,7 @@ import { standardValidate } from "@zap-studio/validation";
7
7
  * `TracerProvider`; a no-op provider (the default until an app registers an
8
8
  * SDK) makes every span/propagation call below a no-op too.
9
9
  */
10
- const tracer = trace.getTracer("@zap-studio/webhooks", "2.0.0");
10
+ const tracer = trace.getTracer("@zap-studio/webhooks", "2.1.0");
11
11
  /**
12
12
  * `TextMapGetter` for the Web `Headers` API, used to extract an inbound
13
13
  * delivery's `traceparent` (and any other registered propagator fields) so
@@ -74,6 +74,7 @@ const createHandlerEntry = (options) => {
74
74
  if (options.schema !== void 0) entry.schema = options.schema;
75
75
  if (options.before !== void 0) entry.before = toArray(options.before);
76
76
  if (options.after !== void 0) entry.after = toArray(options.after);
77
+ if (options.verify !== void 0) entry.verify = options.verify;
77
78
  return entry;
78
79
  };
79
80
  /** Parses the request's raw body bytes as JSON, returning `undefined` on invalid JSON. */
@@ -129,11 +130,22 @@ const dispatchHandler = async (handlerEntry, ctx, validatedPayload, deliveryCont
129
130
  *
130
131
  * router.register("/stripe", {
131
132
  * schema: stripeEventSchema,
133
+ * verify: createHmacVerifier({ headerName: "stripe-signature", secret: stripeSecret }),
132
134
  * handler: async ({ payload }) => {
133
135
  * console.log("Stripe event:", payload.type);
134
136
  * },
135
137
  * });
136
138
  *
139
+ * // Each route can carry its own verifier for multi-provider setups —
140
+ * // no router-level `verify` needed unless routes should share a default.
141
+ * router.register("/github", {
142
+ * schema: githubEventSchema,
143
+ * verify: createHmacVerifier({ headerName: "x-hub-signature-256", secret: githubSecret }),
144
+ * handler: async ({ payload }) => {
145
+ * console.log("GitHub event:", payload.ref);
146
+ * },
147
+ * });
148
+ *
137
149
  * export default { fetch: (request: Request) => router.handle(request) };
138
150
  * ```
139
151
  */
@@ -231,8 +243,9 @@ var WebhookRouter = class {
231
243
  ctx.rawBody = new Uint8Array(await request.arrayBuffer());
232
244
  await runBeforeHooks(ctx, this.globalBeforeHooks);
233
245
  await runBeforeHooks(ctx, handlerEntry.before);
234
- if (this.verify) try {
235
- await this.verify(ctx);
246
+ const verify = handlerEntry.verify ?? this.verify;
247
+ if (verify) try {
248
+ await verify(ctx);
236
249
  } catch (error) {
237
250
  this.logger?.warn("webhook verification failed", {
238
251
  error,
@@ -288,4 +301,4 @@ const createWebhookRouter = (opts) => new WebhookRouter(opts);
288
301
  //#endregion
289
302
  export { createWebhookRouter as n, WebhookRouter as t };
290
303
 
291
- //# sourceMappingURL=router-DyyNi3QB.js.map
304
+ //# sourceMappingURL=router-Pf2VoqgE.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router-Pf2VoqgE.js","names":["pkg.name","pkg.version","otelContext"],"sources":["../package.json","../src/_otel.ts","../src/router.ts"],"sourcesContent":["","/**\n * Internal OpenTelemetry wiring for the webhooks package: tracer resolution,\n * the `Headers` extraction carrier, and span error recording. Kept out of\n * `router.ts` so dispatch logic doesn't get tangled with tracing concerns.\n *\n * @module @zap-studio/webhooks/otel\n */\n\nimport type { Span, TextMapGetter, Tracer } from \"@opentelemetry/api\";\n\nimport { SpanStatusCode, trace } from \"@opentelemetry/api\";\n\nimport pkg from \"../package.json\" with { type: \"json\" };\n\n/**\n * OpenTelemetry tracer for this package. Resolved once against the global\n * `TracerProvider`; a no-op provider (the default until an app registers an\n * SDK) makes every span/propagation call below a no-op too.\n */\nexport const tracer: Tracer = trace.getTracer(pkg.name, pkg.version);\n\n/**\n * `TextMapGetter` for the Web `Headers` API, used to extract an inbound\n * delivery's `traceparent` (and any other registered propagator fields) so\n * the delivery span continues the sender's trace instead of starting a new one.\n */\nexport const HEADERS_GETTER: TextMapGetter<Headers> = {\n get(carrier, key) {\n return carrier.get(key) ?? undefined;\n },\n keys(carrier) {\n return [...carrier.keys()];\n },\n};\n\n/**\n * Records `error` on `span` and marks it as failed. `recordException` only\n * accepts an `Error` or `string`, so other thrown values just get the\n * `ERROR` status without an attached exception event.\n */\nexport const recordSpanError = (span: Span, error: unknown): void => {\n if (error instanceof Error || typeof error === \"string\") {\n span.recordException(error);\n }\n span.setStatus({ code: SpanStatusCode.ERROR });\n};\n","/**\n * Schema-first webhook router primitives.\n *\n * @module @zap-studio/webhooks/router\n */\n\nimport type { Context, Span } from \"@opentelemetry/api\";\nimport type { Logger } from \"@zap-studio/logger\";\nimport type { StandardSchemaV1 } from \"@zap-studio/validation\";\n\nimport {\n SpanKind,\n SpanStatusCode,\n context as otelContext,\n propagation,\n trace,\n} from \"@opentelemetry/api\";\nimport { standardValidate } from \"@zap-studio/validation\";\n\nimport type {\n AfterHook,\n BeforeHook,\n ErrorHook,\n HandlerEntry,\n InferSchemaOutput,\n RegisterOptions,\n SchemaRouteOptions,\n VerifyFn,\n WebhookContext,\n WebhookHandler,\n WebhookRouterOptions,\n} from \"./types.ts\";\n\nimport { HEADERS_GETTER, recordSpanError, tracer } from \"./_otel.ts\";\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 */\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 => Response.json({ error: \"not found\" }, { status: 404 });\n\n/** Sets `http.response.status_code` and marks `span` `ERROR` on a non-2xx response. */\nconst finishDelivery = (span: Span, response: Response): Response => {\n span.setAttribute(\"http.response.status_code\", response.status);\n if (!response.ok) {\n span.setStatus({ code: SpanStatusCode.ERROR });\n }\n return response;\n};\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(\"/\") ? collapsed.slice(0, -1) : collapsed;\n};\n\n/** Runs the given before-hooks in order against the request context. */\nconst runBeforeHooks = async (ctx: WebhookContext, hooks?: BeforeHook[]): Promise<void> => {\n if (!hooks || hooks.length === 0) {\n return;\n }\n\n for (const hook of hooks) {\n // oxlint-disable-next-line react-doctor/async-await-in-loop -- false positive: before-hooks must run in the order they were added, because a later hook can depend on an earlier one. A throw must also stop the hooks that are left. Promise.all would run them at the same time and lose both rules.\n await hook(ctx);\n }\n};\n\n/** Runs the given after-hooks in order against the request context and response. */\nconst runAfterHooks = async (\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 react-doctor/async-await-in-loop -- false positive: same rules as runBeforeHooks above. After-hooks must run in the order they were added, and a throw must stop the hooks that are left.\n await hook(ctx, response);\n }\n};\n\n/** Builds an internal handler entry from route registration options. */\nconst createHandlerEntry = (options: RegisterOptions<unknown>): 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 = toArray(options.before);\n }\n\n if (options.after !== undefined) {\n entry.after = toArray(options.after);\n }\n\n if (options.verify !== undefined) {\n entry.verify = options.verify;\n }\n\n return entry;\n};\n\n/** Parses the request's raw body bytes as JSON, returning `undefined` on invalid JSON. */\nconst parseRequestBody = (ctx: WebhookContext): unknown => {\n try {\n return JSON.parse(bodyDecoder.decode(ctx.rawBody));\n } catch {\n return undefined;\n }\n};\n\n/** Validates the parsed payload against the route schema, returning either the validated value or a `400` response. */\nconst validatePayload = async <TPayload>(\n parsedJson: unknown,\n schema?: StandardSchemaV1<unknown, TPayload>,\n): Promise<TPayload | Response> => {\n if (!schema) {\n // SAFETY: no schema was given, so there is nothing to validate against. The `TPayload` type declared by the caller is the route's only contract.\n return parsedJson as TPayload;\n }\n\n const result = await standardValidate(parsedJson, schema, {\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/** Invokes the route handler with the validated payload, defaulting to a `200 \"ok\"` response. */\nconst executeHandler = async <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/** Runs the route handler inside its own `INTERNAL` span, nested under the delivery span. */\nconst dispatchHandler = async (\n handlerEntry: HandlerEntry,\n ctx: WebhookContext,\n validatedPayload: unknown,\n deliveryContext: Context,\n): Promise<Response> => {\n const handlerSpan = tracer.startSpan(\n `webhook.handler ${ctx.path}`,\n { kind: SpanKind.INTERNAL },\n deliveryContext,\n );\n\n try {\n return await otelContext.with(\n trace.setSpan(deliveryContext, handlerSpan),\n async () => await executeHandler(handlerEntry.handler, ctx, validatedPayload),\n );\n } catch (error) {\n recordSpanError(handlerSpan, error);\n throw error;\n } finally {\n handlerSpan.end();\n }\n};\n\n/**\n * Main webhook router class.\n *\n * Register routes with typed schemas and call `handle` with a Web API `Request`.\n *\n * @example\n * ```ts\n * import { WebhookRouter } from \"@zap-studio/webhooks\";\n *\n * const router = new WebhookRouter({ prefix: \"/webhooks\" });\n *\n * router.register(\"/stripe\", {\n * schema: stripeEventSchema,\n * verify: createHmacVerifier({ headerName: \"stripe-signature\", secret: stripeSecret }),\n * handler: async ({ payload }) => {\n * console.log(\"Stripe event:\", payload.type);\n * },\n * });\n *\n * // Each route can carry its own verifier for multi-provider setups —\n * // no router-level `verify` needed unless routes should share a default.\n * router.register(\"/github\", {\n * schema: githubEventSchema,\n * verify: createHmacVerifier({ headerName: \"x-hub-signature-256\", secret: githubSecret }),\n * handler: async ({ payload }) => {\n * console.log(\"GitHub event:\", payload.ref);\n * },\n * });\n *\n * export default { fetch: (request: Request) => router.handle(request) };\n * ```\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 logger: Logger | 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 * @example\n * ```ts\n * const router = new WebhookRouter({\n * prefix: \"/webhooks\",\n * verify: createHmacVerifier({ headerName: \"x-signature\", secret }),\n * onError: (error) => Response.json({ error: error.message }, { status: 500 }),\n * });\n * ```\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 this.logger = opts.logger;\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 * @example\n * ```ts\n * router.register(\"/stripe\", {\n * schema: stripeEventSchema,\n * handler: async ({ payload }) => {\n * console.log(payload.type); // typed from stripeEventSchema\n * },\n * });\n * ```\n */\n register<Path extends `/${string}`, TSchema extends StandardSchemaV1<unknown, unknown>>(\n path: Path,\n handlerOrOptions: SchemaRouteOptions<TSchema>,\n ): WebhookRouter<TMap & Record<Path, InferSchemaOutput<TSchema>>>;\n /**\n * Register a webhook handler for a specific path, with schema-less registration options.\n *\n * @param path - Route path relative to configured prefix, starting with `/` (e.g. `\"/stripe\"`).\n * @param handlerOrOptions - Registration options without a schema.\n * @returns The same router instance with an updated internal route type map.\n *\n * @example\n * ```ts\n * router.register(\"/ping\", {\n * before: (ctx) => console.log(\"received\", ctx.path),\n * handler: () => Response.json({ ok: true }),\n * });\n * ```\n */\n register<Path extends `/${string}`, TPayload>(\n path: Path,\n handlerOrOptions: RegisterOptions<TPayload>,\n ): WebhookRouter<TMap & Record<Path, TPayload>>;\n /**\n * Register a webhook handler for a specific path, using a plain handler function.\n *\n * @param path - Route path relative to configured prefix, starting with `/` (e.g. `\"/stripe\"`).\n * @param handlerOrOptions - Handler function to process the webhook.\n * @returns The same router instance with an updated internal route type map.\n *\n * @example\n * ```ts\n * router.register(\"/health\", () => Response.json({ status: \"ok\" }));\n * ```\n */\n register<Path extends `/${string}`>(\n path: Path,\n handlerOrOptions: WebhookHandler,\n ): WebhookRouter<TMap & Record<Path, unknown>>;\n register(path: string, handlerOrOptions: WebhookHandler | RegisterOptions<unknown>): this {\n this.handlers.set(\n normalizePath(path),\n typeof handlerOrOptions === \"function\"\n ? { handler: handlerOrOptions }\n : 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 * @example\n * ```ts\n * // Framework-agnostic: works with any Web API Request/Response runtime.\n * export async function POST(request: Request): Promise<Response> {\n * return router.handle(request);\n * }\n * ```\n */\n async handle(request: Request): Promise<Response> {\n const requestPath = new URL(request.url).pathname;\n const { method } = request;\n this.logger?.debug(\"webhook delivery attempt\", { path: requestPath });\n\n const parentContext = propagation.extract(\n otelContext.active(),\n request.headers,\n HEADERS_GETTER,\n );\n const deliverySpan = tracer.startSpan(\n `${method} ${requestPath}`,\n {\n attributes: {\n \"http.request.method\": method,\n \"url.path\": requestPath,\n },\n kind: SpanKind.SERVER,\n },\n parentContext,\n );\n const deliveryContext = trace.setSpan(parentContext, deliverySpan);\n\n try {\n const response = await otelContext.with(\n deliveryContext,\n async () => await this.dispatch(request, requestPath, deliveryContext),\n );\n return finishDelivery(deliverySpan, response);\n } finally {\n deliverySpan.end();\n }\n }\n\n /** Matches the route, runs hooks/verification/validation, and dispatches the handler. */\n private async dispatch(\n request: Request,\n requestPath: string,\n deliveryContext: Context,\n ): Promise<Response> {\n const path = this.matchPath(request);\n if (path === null) {\n this.logger?.warn(\"webhook route not matched\", { path: requestPath });\n return notFoundResponse();\n }\n\n const handlerEntry = this.handlers.get(path);\n if (!handlerEntry) {\n this.logger?.warn(\"webhook route not matched\", { path });\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 runBeforeHooks(ctx, this.globalBeforeHooks);\n await runBeforeHooks(ctx, handlerEntry.before);\n\n const verify = handlerEntry.verify ?? this.verify;\n if (verify) {\n try {\n await verify(ctx);\n } catch (error) {\n this.logger?.warn(\"webhook verification failed\", { error, path });\n throw error;\n }\n }\n\n const parsedJson = parseRequestBody(ctx);\n const validationResult = await validatePayload(parsedJson, handlerEntry.schema);\n\n if (validationResult instanceof Response) {\n return validationResult;\n }\n\n this.logger?.debug(\"webhook handler dispatch\", { path });\n const response = await dispatchHandler(handlerEntry, ctx, validationResult, deliveryContext);\n\n await runAfterHooks(ctx, response, handlerEntry.after);\n await runAfterHooks(ctx, response, this.globalAfterHooks);\n\n return response;\n } catch (error) {\n return await this.handleError(error, ctx);\n }\n }\n\n /** Resolves the incoming request's URL to a registered route key, or `null` if it doesn't match the configured prefix. */\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 /** Builds the error response for a failed request, deferring to the global error hook when set. */\n private async handleError(error: unknown, ctx: WebhookContext): Promise<Response> {\n if (this.globalErrorHook) {\n const normalizedError = 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 *\n * @example\n * ```ts\n * import { createWebhookRouter } from \"@zap-studio/webhooks\";\n *\n * const router = createWebhookRouter({ prefix: \"/webhooks\" });\n * router.register(\"/stripe\", { schema: stripeEventSchema, handler });\n * ```\n */\nexport const createWebhookRouter = (opts?: WebhookRouterOptions): WebhookRouter =>\n new WebhookRouter(opts);\n"],"mappings":"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"}
package/dist/router.d.ts CHANGED
@@ -14,11 +14,22 @@ import { StandardSchemaV1 } from "@zap-studio/validation";
14
14
  *
15
15
  * router.register("/stripe", {
16
16
  * schema: stripeEventSchema,
17
+ * verify: createHmacVerifier({ headerName: "stripe-signature", secret: stripeSecret }),
17
18
  * handler: async ({ payload }) => {
18
19
  * console.log("Stripe event:", payload.type);
19
20
  * },
20
21
  * });
21
22
  *
23
+ * // Each route can carry its own verifier for multi-provider setups —
24
+ * // no router-level `verify` needed unless routes should share a default.
25
+ * router.register("/github", {
26
+ * schema: githubEventSchema,
27
+ * verify: createHmacVerifier({ headerName: "x-hub-signature-256", secret: githubSecret }),
28
+ * handler: async ({ payload }) => {
29
+ * console.log("GitHub event:", payload.ref);
30
+ * },
31
+ * });
32
+ *
22
33
  * export default { fetch: (request: Request) => router.handle(request) };
23
34
  * ```
24
35
  */
@@ -1 +1 @@
1
- {"version":3,"file":"router.d.ts","names":[],"sources":["../src/router.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;cAyOa,cAAc;mBACR;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;;;;;;;;;;;;;;;EAgBjB,YAAY,OAAM;;;;;;;;;;;;;;;;;;;;EA6BlB,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;;UAmC1B;;UAkEN;;UAsBM;;;;;;;;;;;;;;;;cAoCH,sBACX,OAAO,yBACN"}
1
+ {"version":3,"file":"router.d.ts","names":[],"sources":["../src/router.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAiPa,cAAc;mBACR;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;;;;;;;;;;;;;;;EAgBjB,YAAY,OAAM;;;;;;;;;;;;;;;;;;;;EA6BlB,SAAS,2BAA2B,gBAAgB,oCAClD,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;;;;;;;;;;;;;;;;;;EA6B/B,OAAa,SAAS,UAAU,QAAQ;;UAmC1B;;UA2DN;;UAsBM;;;;;;;;;;;;;;;;cAgCH,sBAAuB,OAAO,yBAAuB"}
package/dist/router.js CHANGED
@@ -1,2 +1,2 @@
1
- import { n as createWebhookRouter, t as WebhookRouter } from "./router-DyyNi3QB.js";
1
+ import { n as createWebhookRouter, t as WebhookRouter } from "./router-Pf2VoqgE.js";
2
2
  export { WebhookRouter, createWebhookRouter };
package/dist/types.d.ts CHANGED
@@ -48,6 +48,8 @@ interface HandlerEntry<TPayload = unknown> {
48
48
  handler: WebhookHandler<TPayload>;
49
49
  /** Optional Standard Schema validator to validate the webhook payload. */
50
50
  schema?: StandardSchemaV1<unknown, TPayload>;
51
+ /** Route-specific request verifier. Overrides the router-level `verify` for this route only. */
52
+ verify?: VerifyFn;
51
53
  }
52
54
  /**
53
55
  * Configuration options for creating a `WebhookRouter`.
@@ -81,7 +83,11 @@ interface WebhookRouterOptions {
81
83
  * duplicate slashes collapsed. Use `""` or `"/"` to mount at the root.
82
84
  */
83
85
  prefix?: string;
84
- /** Optional request verification function (for signature checks, auth, etc.). */
86
+ /**
87
+ * Optional request verification function (for signature checks, auth, etc.),
88
+ * used as the default for any route that doesn't set its own `verify` in
89
+ * {@link RegisterOptions}.
90
+ */
85
91
  verify?: VerifyFn;
86
92
  }
87
93
  /**
@@ -91,6 +97,7 @@ interface WebhookRouterOptions {
91
97
  * ```ts
92
98
  * const options: RegisterOptions<{ type: string }> = {
93
99
  * schema: stripeEventSchema,
100
+ * verify: createHmacVerifier({ headerName: "stripe-signature", secret }),
94
101
  * handler: ({ payload }) => console.log(payload.type),
95
102
  * };
96
103
  * ```
@@ -104,6 +111,12 @@ interface RegisterOptions<T> {
104
111
  handler: WebhookHandler<T>;
105
112
  /** Optional Standard Schema validator to validate the webhook payload */
106
113
  schema?: StandardSchemaV1<unknown, T>;
114
+ /**
115
+ * Route-specific request verifier. When set, overrides the router-level
116
+ * `verify` (`WebhookRouterOptions.verify`) for this route only — useful when
117
+ * different routes on the same router are signed by different providers.
118
+ */
119
+ verify?: VerifyFn;
107
120
  }
108
121
  /**
109
122
  * Infers the output type from a Standard Schema instance.
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;;;;;;;;;;;;UAsBiB;;EAEf;;EAEA,SAAS;;EAET,SAAS;;;;;;;;;;;;;;UAeM,eAAe,4BAA4B;;EAE1D,SAAS;;;UAIM,aAAa;;EAE5B,QAAQ;;EAER,SAAS;;EAET,SAAS,eAAe;;EAExB,SAAS,0BAA0B;;;;;;;;;;;;;UAcpB;;EAEf,QAAQ,YAAY;;EAEpB,SAAS,aAAa;;EAEtB,UAAU;;;;;;;EAOV,SAAS;;;;;;;EAOT;;EAEA,SAAS;;;;;;;;;;;;;UAcM,gBAAgB;;EAE/B,QAAQ,YAAY;;EAEpB,SAAS,aAAa;;EAEtB,SAAS,eAAe;;EAExB,SAAS,0BAA0B;;;;;;;KAQzB,kBAAkB,WAC5B,gBAAgB,gCAAgC,WAAW;;;;;;;;;;;;;;KAejD,mBACV,gBAAgB,sCACd,KAAK,gBAAgB,kBAAkB;EACzC,QAAQ;;;UAIO;;EAEf,QAAQ,YAAY;;EAEpB,SAAS,aAAa;;EAEtB,SAAS;;EAET,QAAQ;;;;;;;;;;;;;;KAeE,aAAa,gBAAgB,eAAe,iBACrD,WAAW,UAAU,mBAAmB,QAAQ;;;;;;;;;;;;;;KAgBvC,eAAe,uBACzB,KAAK,eAAe,cACjB,QAAQ,wBAAwB;;;;;;;;;;;KAYzB,WAAW,aAAa,8BACjC,WAAW,OAAO,eAAe,KAAK;;;;;;;;;;;;;KAe7B,0BACV,gBAAgB,eAAe,iBAE9B,WAAW,UAAU,kBAAkB,QAAQ;;;;;;;;;KAWtC,YAAY,KAAK,mBAAmB;;;;;;;;;KAUpC,cAAc,KAAK,mBAAmB;;;;;;;;;;;;KAatC,aACV,KAAK,gBACL,UAAU,aACP;;;;;;;;;KAUO,aACV,OAAO,OACP,KAAK,mBACF,QAAQ,wBAAwB"}
1
+ {"version":3,"file":"types.d.ts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;;;;;;;;;;;;UAsBiB;;EAEf;;EAEA,SAAS;;EAET,SAAS;;;;;;;;;;;;;;UAeM,eAAe,4BAA4B;;EAE1D,SAAS;;;UAIM,aAAa;;EAE5B,QAAQ;;EAER,SAAS;;EAET,SAAS,eAAe;;EAExB,SAAS,0BAA0B;;EAEnC,SAAS;;;;;;;;;;;;;UAcM;;EAEf,QAAQ,YAAY;;EAEpB,SAAS,aAAa;;EAEtB,UAAU;;;;;;;EAOV,SAAS;;;;;;;EAOT;;;;;;EAMA,SAAS;;;;;;;;;;;;;;UAeM,gBAAgB;;EAE/B,QAAQ,YAAY;;EAEpB,SAAS,aAAa;;EAEtB,SAAS,eAAe;;EAExB,SAAS,0BAA0B;;;;;;EAMnC,SAAS;;;;;;;KAQC,kBAAkB,WAC5B,gBAAgB,gCAAgC,WAAW;;;;;;;;;;;;;;KAejD,mBAAmB,gBAAgB,sCAAsC,KACnF,gBAAgB,kBAAkB;EAGlC,QAAQ;;;UAIO;;EAEf,QAAQ,YAAY;;EAEpB,SAAS,aAAa;;EAEtB,SAAS;;EAET,QAAQ;;;;;;;;;;;;;;KAeE,aAAa,gBAAgB,eAAe,iBACrD,WAAW,UAAU,mBAAmB,QAAQ;;;;;;;;;;;;;;KAgBvC,eAAe,uBACzB,KAAK,eAAe,cACjB,QAAQ,wBAAwB;;;;;;;;;;;KAYzB,WAAW,aAAa,8BACjC,WAAW,OAAO,eAAe,KAAK;;;;;;;;;;;;;KAe7B,0BAA0B,gBAAgB,eAAe,iBAClE,WAAW,UAAU,kBAAkB,QAAQ;;;;;;;;;KAWtC,YAAY,KAAK,mBAAmB;;;;;;;;;KAUpC,cAAc,KAAK,mBAAmB;;;;;;;;;;;;KAatC,aAAa,KAAK,gBAAgB,UAAU,aAAa;;;;;;;;;KAUzD,aACV,OAAO,OACP,KAAK,mBACF,QAAQ,wBAAwB"}
@@ -1 +1 @@
1
- {"version":3,"file":"verify.d.ts","names":[],"sources":["../src/verify.ts"],"mappings":";;;;;cAYa,qBAAsB,GAAG,YAAY,GAAG;cAc/C;WACJ;WACA;WACA;WACA;;KAGG,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAuDrB,uBACX,YACA,QACA;EAEA;EACA;EACA,OAAO;MACL"}
1
+ {"version":3,"file":"verify.d.ts","names":[],"sources":["../src/verify.ts"],"mappings":";;;;;cAaa,qBAAsB,GAAG,YAAY,GAAG;cAc/C;WACJ;WACA;WACA;WACA;;KAGG,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAwDrB,uBACX,YACA,QACA;EAEA;EACA;EACA,OAAO;MACL"}
package/dist/verify.js CHANGED
@@ -1,18 +1,13 @@
1
1
  import { VerificationError } from "./errors.js";
2
2
  //#region src/verify.ts
3
3
  /**
4
- * Signature verification helpers for webhook requests.
5
- *
6
- * @module @zap-studio/webhooks/verify
7
- */
8
- /**
9
4
  * Compares two byte arrays in constant time to prevent timing attacks.
10
5
  */
11
6
  const constantTimeEquals = (a, b) => {
12
7
  if (a.length !== b.length) return false;
13
8
  let result = 0;
14
9
  for (let i = 0; i < a.length; i += 1)
15
- // v8 ignore next -- `?? 0` fallback is unreachable: a Uint8Array never holds `undefined` at an in-bounds index, this exists only to satisfy noUncheckedIndexedAccess.
10
+ // v8 ignore next -- the `?? 0` fallback never runs, because a Uint8Array never holds `undefined` at a valid index. It is only here for noUncheckedIndexedAccess.
16
11
  result |= (a[i] ?? 0) ^ (b[i] ?? 0);
17
12
  return result === 0;
18
13
  };
@@ -23,6 +18,7 @@ const HMAC_HASH = {
23
18
  sha512: "SHA-512"
24
19
  };
25
20
  const HEX_PATTERN = /^[0-9a-f]*$/iu;
21
+ const SIGNATURE_PREFIX_PATTERN = /^[a-z0-9-]+=/iu;
26
22
  /**
27
23
  * Decodes a hex string into bytes, or `undefined` when it is not valid hex.
28
24
  */
@@ -32,7 +28,7 @@ const hexToBytes = (hex) => {
32
28
  for (let i = 0; i < bytes.length; i += 1) bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
33
29
  return bytes;
34
30
  };
35
- const normalizeSignature = (signature) => signature.replace(/^[a-z0-9-]+=/iu, "").trim();
31
+ const normalizeSignature = (signature) => signature.replace(SIGNATURE_PREFIX_PATTERN, "").trim();
36
32
  /**
37
33
  * Creates a webhook verifier that validates an HMAC signature from a request header.
38
34
  *
@@ -1 +1 @@
1
- {"version":3,"file":"verify.js","names":[],"sources":["../src/verify.ts"],"sourcesContent":["/**\n * Signature verification helpers for webhook requests.\n *\n * @module @zap-studio/webhooks/verify\n */\n\nimport { VerificationError } from \"./errors.js\";\nimport type { VerifyFn } from \"./types.js\";\n\n/**\n * Compares two byte arrays in constant time to prevent timing attacks.\n */\nexport const constantTimeEquals = (a: Uint8Array, b: Uint8Array): boolean => {\n if (a.length !== b.length) {\n return false;\n }\n\n let result = 0;\n for (let i = 0; i < a.length; i += 1) {\n // v8 ignore next -- `?? 0` fallback is unreachable: a Uint8Array never holds `undefined` at an in-bounds index, this exists only to satisfy noUncheckedIndexedAccess.\n result |= (a[i] ?? 0) ^ (b[i] ?? 0); // oxlint-disable-line no-bitwise -- XOR is the constant-time compare trick.\n }\n\n return result === 0;\n};\n\nconst HMAC_HASH = {\n sha1: \"SHA-1\",\n sha256: \"SHA-256\",\n sha384: \"SHA-384\",\n sha512: \"SHA-512\",\n} as const;\n\ntype HmacAlgorithm = keyof typeof HMAC_HASH;\n\nconst HEX_PATTERN = /^[0-9a-f]*$/iu;\n\n/**\n * Decodes a hex string into bytes, or `undefined` when it is not valid hex.\n */\nconst hexToBytes = (hex: string): Uint8Array | undefined => {\n if (hex.length % 2 !== 0 || !HEX_PATTERN.test(hex)) {\n return undefined;\n }\n\n const bytes = new Uint8Array(hex.length / 2);\n for (let i = 0; i < bytes.length; i += 1) {\n bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);\n }\n\n return bytes;\n};\n\nconst normalizeSignature = (signature: string): string =>\n signature.replace(/^[a-z0-9-]+=/iu, \"\").trim();\n\n/**\n * Creates a webhook verifier that validates an HMAC signature from a request header.\n *\n * The verifier imports the provided string secret once, computes an HMAC from\n * `ctx.rawBody`, normalizes the incoming header value, and compares both\n * signatures in constant time.\n *\n * Header values like `sha256=<hex>` are supported so common provider formats\n * such as GitHub work without extra parsing.\n *\n * @example\n * ```ts\n * import { createWebhookRouter } from \"@zap-studio/webhooks\";\n * import { createHmacVerifier } from \"@zap-studio/webhooks/verify\";\n *\n * const router = createWebhookRouter({\n * verify: createHmacVerifier({\n * headerName: \"x-hub-signature-256\",\n * secret: process.env.GITHUB_WEBHOOK_SECRET!,\n * }),\n * });\n * ```\n *\n * @param options - Verifier configuration.\n * @param options.headerName - Header containing the provider signature.\n * @param options.secret - Shared HMAC secret as a string.\n * @param options.algo - HMAC hash algorithm. Defaults to `\"sha256\"`.\n * @returns A router-compatible request verifier.\n *\n * @throws {VerificationError}\n * Thrown when verifier setup fails or request verification does not pass.\n */\nexport const createHmacVerifier = ({\n headerName,\n secret,\n algo = \"sha256\",\n}: {\n headerName: string;\n secret: string;\n algo?: HmacAlgorithm;\n}): VerifyFn => {\n if (globalThis.crypto?.subtle === undefined) {\n throw new VerificationError(\n \"Web Crypto API is unavailable in this runtime\"\n );\n }\n\n const { subtle } = globalThis.crypto;\n\n const hash = HMAC_HASH[algo];\n if (!hash) {\n throw new VerificationError(`Unsupported HMAC algorithm: ${algo}`);\n }\n\n const keyPromise = subtle.importKey(\n \"raw\",\n new TextEncoder().encode(secret),\n { hash, name: \"HMAC\" },\n false,\n [\"sign\"]\n );\n\n return async (ctx) => {\n const actual = ctx.request.headers.get(headerName);\n if (actual === null || actual.length === 0) {\n throw new VerificationError(`Missing signature header: ${headerName}`);\n }\n\n const key = await keyPromise;\n const signature = await subtle.sign(\n \"HMAC\",\n key,\n new Uint8Array(ctx.rawBody)\n );\n const expected = new Uint8Array(signature);\n const provided = hexToBytes(normalizeSignature(actual));\n\n if (provided === undefined || !constantTimeEquals(expected, provided)) {\n throw new VerificationError(\n `Invalid signature for header: ${headerName}`\n );\n }\n };\n};\n"],"mappings":";;;;;;;;;;AAYA,MAAa,sBAAsB,GAAe,MAA2B;CAC3E,IAAI,EAAE,WAAW,EAAE,QACjB,OAAO;CAGT,IAAI,SAAS;CACb,KAAK,IAAI,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK;;CAEjC,WAAW,EAAE,MAAM,MAAM,EAAE,MAAM;CAGnC,OAAO,WAAW;AACpB;AAEA,MAAM,YAAY;CAChB,MAAM;CACN,QAAQ;CACR,QAAQ;CACR,QAAQ;AACV;AAIA,MAAM,cAAc;;;;AAKpB,MAAM,cAAc,QAAwC;CAC1D,IAAI,IAAI,SAAS,MAAM,KAAK,CAAC,YAAY,KAAK,GAAG,GAC/C;CAGF,MAAM,QAAQ,IAAI,WAAW,IAAI,SAAS,CAAC;CAC3C,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,GACrC,MAAM,KAAK,OAAO,SAAS,IAAI,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE;CAG5D,OAAO;AACT;AAEA,MAAM,sBAAsB,cAC1B,UAAU,QAAQ,kBAAkB,EAAE,CAAC,CAAC,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkC/C,MAAa,sBAAsB,EACjC,YACA,QACA,OAAO,eAKO;CACd,IAAI,WAAW,QAAQ,WAAW,KAAA,GAChC,MAAM,IAAI,kBACR,+CACF;CAGF,MAAM,EAAE,WAAW,WAAW;CAE9B,MAAM,OAAO,UAAU;CACvB,IAAI,CAAC,MACH,MAAM,IAAI,kBAAkB,+BAA+B,MAAM;CAGnE,MAAM,aAAa,OAAO,UACxB,OACA,IAAI,YAAY,CAAC,CAAC,OAAO,MAAM,GAC/B;EAAE;EAAM,MAAM;CAAO,GACrB,OACA,CAAC,MAAM,CACT;CAEA,OAAO,OAAO,QAAQ;EACpB,MAAM,SAAS,IAAI,QAAQ,QAAQ,IAAI,UAAU;EACjD,IAAI,WAAW,QAAQ,OAAO,WAAW,GACvC,MAAM,IAAI,kBAAkB,6BAA6B,YAAY;EAGvE,MAAM,MAAM,MAAM;EAClB,MAAM,YAAY,MAAM,OAAO,KAC7B,QACA,KACA,IAAI,WAAW,IAAI,OAAO,CAC5B;EACA,MAAM,WAAW,IAAI,WAAW,SAAS;EACzC,MAAM,WAAW,WAAW,mBAAmB,MAAM,CAAC;EAEtD,IAAI,aAAa,KAAA,KAAa,CAAC,mBAAmB,UAAU,QAAQ,GAClE,MAAM,IAAI,kBACR,iCAAiC,YACnC;CAEJ;AACF"}
1
+ {"version":3,"file":"verify.js","names":[],"sources":["../src/verify.ts"],"sourcesContent":["/**\n * Signature verification helpers for webhook requests.\n *\n * @module @zap-studio/webhooks/verify\n */\n\nimport type { VerifyFn } from \"./types.ts\";\n\nimport { VerificationError } from \"./errors.ts\";\n\n/**\n * Compares two byte arrays in constant time to prevent timing attacks.\n */\nexport const constantTimeEquals = (a: Uint8Array, b: Uint8Array): boolean => {\n if (a.length !== b.length) {\n return false;\n }\n\n let result = 0;\n for (let i = 0; i < a.length; i += 1) {\n // v8 ignore next -- the `?? 0` fallback never runs, because a Uint8Array never holds `undefined` at a valid index. It is only here for noUncheckedIndexedAccess.\n result |= (a[i] ?? 0) ^ (b[i] ?? 0);\n }\n\n return result === 0;\n};\n\nconst HMAC_HASH = {\n sha1: \"SHA-1\",\n sha256: \"SHA-256\",\n sha384: \"SHA-384\",\n sha512: \"SHA-512\",\n} as const;\n\ntype HmacAlgorithm = keyof typeof HMAC_HASH;\n\nconst HEX_PATTERN = /^[0-9a-f]*$/iu;\nconst SIGNATURE_PREFIX_PATTERN = /^[a-z0-9-]+=/iu;\n\n/**\n * Decodes a hex string into bytes, or `undefined` when it is not valid hex.\n */\nconst hexToBytes = (hex: string): Uint8Array | undefined => {\n if (hex.length % 2 !== 0 || !HEX_PATTERN.test(hex)) {\n return undefined;\n }\n\n const bytes = new Uint8Array(hex.length / 2);\n for (let i = 0; i < bytes.length; i += 1) {\n bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);\n }\n\n return bytes;\n};\n\nconst normalizeSignature = (signature: string): string =>\n signature.replace(SIGNATURE_PREFIX_PATTERN, \"\").trim();\n\n/**\n * Creates a webhook verifier that validates an HMAC signature from a request header.\n *\n * The verifier imports the provided string secret once, computes an HMAC from\n * `ctx.rawBody`, normalizes the incoming header value, and compares both\n * signatures in constant time.\n *\n * Header values like `sha256=<hex>` are supported so common provider formats\n * such as GitHub work without extra parsing.\n *\n * @example\n * ```ts\n * import { createWebhookRouter } from \"@zap-studio/webhooks\";\n * import { createHmacVerifier } from \"@zap-studio/webhooks/verify\";\n *\n * const router = createWebhookRouter({\n * verify: createHmacVerifier({\n * headerName: \"x-hub-signature-256\",\n * secret: process.env.GITHUB_WEBHOOK_SECRET!,\n * }),\n * });\n * ```\n *\n * @param options - Verifier configuration.\n * @param options.headerName - Header containing the provider signature.\n * @param options.secret - Shared HMAC secret as a string.\n * @param options.algo - HMAC hash algorithm. Defaults to `\"sha256\"`.\n * @returns A router-compatible request verifier.\n *\n * @throws {VerificationError}\n * Thrown when verifier setup fails or request verification does not pass.\n */\nexport const createHmacVerifier = ({\n headerName,\n secret,\n algo = \"sha256\",\n}: {\n headerName: string;\n secret: string;\n algo?: HmacAlgorithm;\n}): VerifyFn => {\n if (globalThis.crypto?.subtle === undefined) {\n throw new VerificationError(\"Web Crypto API is unavailable in this runtime\");\n }\n\n const { subtle } = globalThis.crypto;\n\n const hash = HMAC_HASH[algo];\n if (!hash) {\n throw new VerificationError(`Unsupported HMAC algorithm: ${algo}`);\n }\n\n const keyPromise = subtle.importKey(\n \"raw\",\n new TextEncoder().encode(secret),\n { hash, name: \"HMAC\" },\n false,\n [\"sign\"],\n );\n\n return async (ctx) => {\n const actual = ctx.request.headers.get(headerName);\n if (actual === null || actual.length === 0) {\n throw new VerificationError(`Missing signature header: ${headerName}`);\n }\n\n const key = await keyPromise;\n const signature = await subtle.sign(\"HMAC\", key, new Uint8Array(ctx.rawBody));\n const expected = new Uint8Array(signature);\n const provided = hexToBytes(normalizeSignature(actual));\n\n if (provided === undefined || !constantTimeEquals(expected, provided)) {\n throw new VerificationError(`Invalid signature for header: ${headerName}`);\n }\n };\n};\n"],"mappings":";;;;;AAaA,MAAa,sBAAsB,GAAe,MAA2B;CAC3E,IAAI,EAAE,WAAW,EAAE,QACjB,OAAO;CAGT,IAAI,SAAS;CACb,KAAK,IAAI,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK;;CAEjC,WAAW,EAAE,MAAM,MAAM,EAAE,MAAM;CAGnC,OAAO,WAAW;AACpB;AAEA,MAAM,YAAY;CAChB,MAAM;CACN,QAAQ;CACR,QAAQ;CACR,QAAQ;AACV;AAIA,MAAM,cAAc;AACpB,MAAM,2BAA2B;;;;AAKjC,MAAM,cAAc,QAAwC;CAC1D,IAAI,IAAI,SAAS,MAAM,KAAK,CAAC,YAAY,KAAK,GAAG,GAC/C;CAGF,MAAM,QAAQ,IAAI,WAAW,IAAI,SAAS,CAAC;CAC3C,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,GACrC,MAAM,KAAK,OAAO,SAAS,IAAI,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE;CAG5D,OAAO;AACT;AAEA,MAAM,sBAAsB,cAC1B,UAAU,QAAQ,0BAA0B,EAAE,CAAC,CAAC,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCvD,MAAa,sBAAsB,EACjC,YACA,QACA,OAAO,eAKO;CACd,IAAI,WAAW,QAAQ,WAAW,KAAA,GAChC,MAAM,IAAI,kBAAkB,+CAA+C;CAG7E,MAAM,EAAE,WAAW,WAAW;CAE9B,MAAM,OAAO,UAAU;CACvB,IAAI,CAAC,MACH,MAAM,IAAI,kBAAkB,+BAA+B,MAAM;CAGnE,MAAM,aAAa,OAAO,UACxB,OACA,IAAI,YAAY,CAAC,CAAC,OAAO,MAAM,GAC/B;EAAE;EAAM,MAAM;CAAO,GACrB,OACA,CAAC,MAAM,CACT;CAEA,OAAO,OAAO,QAAQ;EACpB,MAAM,SAAS,IAAI,QAAQ,QAAQ,IAAI,UAAU;EACjD,IAAI,WAAW,QAAQ,OAAO,WAAW,GACvC,MAAM,IAAI,kBAAkB,6BAA6B,YAAY;EAGvE,MAAM,MAAM,MAAM;EAClB,MAAM,YAAY,MAAM,OAAO,KAAK,QAAQ,KAAK,IAAI,WAAW,IAAI,OAAO,CAAC;EAC5E,MAAM,WAAW,IAAI,WAAW,SAAS;EACzC,MAAM,WAAW,WAAW,mBAAmB,MAAM,CAAC;EAEtD,IAAI,aAAa,KAAA,KAAa,CAAC,mBAAmB,UAAU,QAAQ,GAClE,MAAM,IAAI,kBAAkB,iCAAiC,YAAY;CAE7E;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zap-studio/webhooks",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "private": false,
5
5
  "description": "A lightweight, type-safe, tree-shakeable webhook router with Standard Schema validation, signature verification, and lifecycle hooks.",
6
6
  "keywords": [
@@ -46,23 +46,23 @@
46
46
  "access": "public"
47
47
  },
48
48
  "dependencies": {
49
- "@zap-studio/validation": "1.0.0"
49
+ "@zap-studio/validation": "1.1.1"
50
50
  },
51
51
  "devDependencies": {
52
- "@opentelemetry/api": "^1.9.0",
52
+ "@opentelemetry/api": "^1.9.1",
53
53
  "@opentelemetry/context-async-hooks": "^2.10.0",
54
54
  "@opentelemetry/core": "^2.10.0",
55
55
  "@opentelemetry/sdk-trace-base": "^2.10.0",
56
+ "@zap-studio/logger": "2.1.0",
57
+ "@zap-studio/typescript": "0.0.0",
56
58
  "tsdown": "^0.22.14",
57
59
  "typescript": "^7.0.2",
58
- "vitest": "^4.1.10",
59
- "zod": "^4.4.3",
60
- "@zap-studio/typescript": "0.0.0",
61
- "@zap-studio/logger": "2.0.0"
60
+ "vitest": "^4.1.11",
61
+ "zod": "^4.5.4"
62
62
  },
63
63
  "peerDependencies": {
64
- "@opentelemetry/api": "^1.9.0",
65
- "@zap-studio/logger": "2.0.0"
64
+ "@opentelemetry/api": "^1.9.1",
65
+ "@zap-studio/logger": "2.1.0"
66
66
  },
67
67
  "peerDependenciesMeta": {
68
68
  "@zap-studio/logger": {
@@ -71,5 +71,6 @@
71
71
  },
72
72
  "engines": {
73
73
  "node": ">=18.0.0"
74
- }
74
+ },
75
+ "scripts": {}
75
76
  }
@@ -1 +0,0 @@
1
- {"version":3,"file":"router-DyyNi3QB.js","names":["pkg.name","pkg.version","otelContext"],"sources":["../package.json","../src/_otel.ts","../src/router.ts"],"sourcesContent":["","/**\n * Internal OpenTelemetry wiring for the webhooks package: tracer resolution,\n * the `Headers` extraction carrier, and span error recording. Kept out of\n * `router.ts` so dispatch logic doesn't get tangled with tracing concerns.\n *\n * @module @zap-studio/webhooks/otel\n */\n\nimport type { Span, TextMapGetter, Tracer } from \"@opentelemetry/api\";\nimport { SpanStatusCode, trace } from \"@opentelemetry/api\";\n\nimport pkg from \"../package.json\" with { type: \"json\" };\n\n/**\n * OpenTelemetry tracer for this package. Resolved once against the global\n * `TracerProvider`; a no-op provider (the default until an app registers an\n * SDK) makes every span/propagation call below a no-op too.\n */\nexport const tracer: Tracer = trace.getTracer(pkg.name, pkg.version);\n\n/**\n * `TextMapGetter` for the Web `Headers` API, used to extract an inbound\n * delivery's `traceparent` (and any other registered propagator fields) so\n * the delivery span continues the sender's trace instead of starting a new one.\n */\nexport const HEADERS_GETTER: TextMapGetter<Headers> = {\n get(carrier, key) {\n return carrier.get(key) ?? undefined;\n },\n keys(carrier) {\n return [...carrier.keys()];\n },\n};\n\n/**\n * Records `error` on `span` and marks it as failed. `recordException` only\n * accepts an `Error` or `string`, so other thrown values just get the\n * `ERROR` status without an attached exception event.\n */\nexport const recordSpanError = (span: Span, error: unknown): void => {\n if (error instanceof Error || typeof error === \"string\") {\n span.recordException(error);\n }\n span.setStatus({ code: SpanStatusCode.ERROR });\n};\n","/**\n * Schema-first webhook router primitives.\n *\n * @module @zap-studio/webhooks/router\n */\n\nimport type { Context, Span } from \"@opentelemetry/api\";\nimport {\n SpanKind,\n SpanStatusCode,\n context as otelContext,\n propagation,\n trace,\n} from \"@opentelemetry/api\";\nimport type { Logger } from \"@zap-studio/logger\";\nimport type { StandardSchemaV1 } from \"@zap-studio/validation\";\nimport { standardValidate } from \"@zap-studio/validation\";\n\nimport { HEADERS_GETTER, recordSpanError, tracer } from \"./_otel.js\";\nimport type {\n AfterHook,\n BeforeHook,\n ErrorHook,\n HandlerEntry,\n InferSchemaOutput,\n RegisterOptions,\n SchemaRouteOptions,\n VerifyFn,\n WebhookContext,\n WebhookHandler,\n WebhookRouterOptions,\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 */\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\n/** Sets `http.response.status_code` and marks `span` `ERROR` on a non-2xx response. */\nconst finishDelivery = (span: Span, response: Response): Response => {\n span.setAttribute(\"http.response.status_code\", response.status);\n if (!response.ok) {\n span.setStatus({ code: SpanStatusCode.ERROR });\n }\n return response;\n};\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/** Runs the given before-hooks in order against the request context. */\nconst runBeforeHooks = async (\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/** Runs the given after-hooks in order against the request context and response. */\nconst runAfterHooks = async (\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/** Builds an internal handler entry from route registration options. */\nconst 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 = toArray(options.before);\n }\n\n if (options.after !== undefined) {\n entry.after = toArray(options.after);\n }\n\n return entry;\n};\n\n/** Parses the request's raw body bytes as JSON, returning `undefined` on invalid JSON. */\nconst parseRequestBody = (ctx: WebhookContext): unknown => {\n try {\n return JSON.parse(bodyDecoder.decode(ctx.rawBody));\n } catch {\n return undefined;\n }\n};\n\n/** Validates the parsed payload against the route schema, returning either the validated value or a `400` response. */\nconst validatePayload = async <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(parsedJson, schema, {\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/** Invokes the route handler with the validated payload, defaulting to a `200 \"ok\"` response. */\nconst executeHandler = async <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/** Runs the route handler inside its own `INTERNAL` span, nested under the delivery span. */\nconst dispatchHandler = async (\n handlerEntry: HandlerEntry,\n ctx: WebhookContext,\n validatedPayload: unknown,\n deliveryContext: Context\n): Promise<Response> => {\n const handlerSpan = tracer.startSpan(\n `webhook.handler ${ctx.path}`,\n { kind: SpanKind.INTERNAL },\n deliveryContext\n );\n\n try {\n return await otelContext.with(\n trace.setSpan(deliveryContext, handlerSpan),\n async () =>\n await executeHandler(handlerEntry.handler, ctx, validatedPayload)\n );\n } catch (error) {\n recordSpanError(handlerSpan, error);\n throw error;\n } finally {\n handlerSpan.end();\n }\n};\n\n/**\n * Main webhook router class.\n *\n * Register routes with typed schemas and call `handle` with a Web API `Request`.\n *\n * @example\n * ```ts\n * import { WebhookRouter } from \"@zap-studio/webhooks\";\n *\n * const router = new WebhookRouter({ prefix: \"/webhooks\" });\n *\n * router.register(\"/stripe\", {\n * schema: stripeEventSchema,\n * handler: async ({ payload }) => {\n * console.log(\"Stripe event:\", payload.type);\n * },\n * });\n *\n * export default { fetch: (request: Request) => router.handle(request) };\n * ```\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 logger: Logger | 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 * @example\n * ```ts\n * const router = new WebhookRouter({\n * prefix: \"/webhooks\",\n * verify: createHmacVerifier({ headerName: \"x-signature\", secret }),\n * onError: (error) => Response.json({ error: error.message }, { status: 500 }),\n * });\n * ```\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 this.logger = opts.logger;\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 * @example\n * ```ts\n * router.register(\"/stripe\", {\n * schema: stripeEventSchema,\n * handler: async ({ payload }) => {\n * console.log(payload.type); // typed from stripeEventSchema\n * },\n * });\n * ```\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 /**\n * Register a webhook handler for a specific path, with schema-less registration options.\n *\n * @param path - Route path relative to configured prefix, starting with `/` (e.g. `\"/stripe\"`).\n * @param handlerOrOptions - Registration options without a schema.\n * @returns The same router instance with an updated internal route type map.\n *\n * @example\n * ```ts\n * router.register(\"/ping\", {\n * before: (ctx) => console.log(\"received\", ctx.path),\n * handler: () => Response.json({ ok: true }),\n * });\n * ```\n */\n register<Path extends `/${string}`, TPayload>(\n path: Path,\n handlerOrOptions: RegisterOptions<TPayload>\n ): WebhookRouter<TMap & Record<Path, TPayload>>;\n /**\n * Register a webhook handler for a specific path, using a plain handler function.\n *\n * @param path - Route path relative to configured prefix, starting with `/` (e.g. `\"/stripe\"`).\n * @param handlerOrOptions - Handler function to process the webhook.\n * @returns The same router instance with an updated internal route type map.\n *\n * @example\n * ```ts\n * router.register(\"/health\", () => Response.json({ status: \"ok\" }));\n * ```\n */\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 : 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 * @example\n * ```ts\n * // Framework-agnostic: works with any Web API Request/Response runtime.\n * export async function POST(request: Request): Promise<Response> {\n * return router.handle(request);\n * }\n * ```\n */\n async handle(request: Request): Promise<Response> {\n const requestPath = new URL(request.url).pathname;\n const { method } = request;\n this.logger?.debug(\"webhook delivery attempt\", { path: requestPath });\n\n const parentContext = propagation.extract(\n otelContext.active(),\n request.headers,\n HEADERS_GETTER\n );\n const deliverySpan = tracer.startSpan(\n `${method} ${requestPath}`,\n {\n attributes: {\n \"http.request.method\": method,\n \"url.path\": requestPath,\n },\n kind: SpanKind.SERVER,\n },\n parentContext\n );\n const deliveryContext = trace.setSpan(parentContext, deliverySpan);\n\n try {\n const response = await otelContext.with(\n deliveryContext,\n async () => await this.dispatch(request, requestPath, deliveryContext)\n );\n return finishDelivery(deliverySpan, response);\n } finally {\n deliverySpan.end();\n }\n }\n\n /** Matches the route, runs hooks/verification/validation, and dispatches the handler. */\n private async dispatch(\n request: Request,\n requestPath: string,\n deliveryContext: Context\n ): Promise<Response> {\n const path = this.matchPath(request);\n if (path === null) {\n this.logger?.warn(\"webhook route not matched\", { path: requestPath });\n return notFoundResponse();\n }\n\n const handlerEntry = this.handlers.get(path);\n if (!handlerEntry) {\n this.logger?.warn(\"webhook route not matched\", { path });\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 runBeforeHooks(ctx, this.globalBeforeHooks);\n await runBeforeHooks(ctx, handlerEntry.before);\n\n if (this.verify) {\n try {\n await this.verify(ctx);\n } catch (error) {\n this.logger?.warn(\"webhook verification failed\", { error, path });\n throw error;\n }\n }\n\n const parsedJson = parseRequestBody(ctx);\n const validationResult = await validatePayload(\n parsedJson,\n handlerEntry.schema\n );\n\n if (validationResult instanceof Response) {\n return validationResult;\n }\n\n this.logger?.debug(\"webhook handler dispatch\", { path });\n const response = await dispatchHandler(\n handlerEntry,\n ctx,\n validationResult,\n deliveryContext\n );\n\n await runAfterHooks(ctx, response, handlerEntry.after);\n await runAfterHooks(ctx, response, this.globalAfterHooks);\n\n return response;\n } catch (error) {\n return await this.handleError(error, ctx);\n }\n }\n\n /** Resolves the incoming request's URL to a registered route key, or `null` if it doesn't match the configured prefix. */\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 /** Builds the error response for a failed request, deferring to the global error hook when set. */\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 *\n * @example\n * ```ts\n * import { createWebhookRouter } from \"@zap-studio/webhooks\";\n *\n * const router = createWebhookRouter({ prefix: \"/webhooks\" });\n * router.register(\"/stripe\", { schema: stripeEventSchema, handler });\n * ```\n */\nexport const createWebhookRouter = (\n opts?: WebhookRouterOptions\n): WebhookRouter => new WebhookRouter(opts);\n"],"mappings":"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"}