@zap-studio/webhooks 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,22 @@ 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.0.0]
8
+
9
+ ### Added
10
+
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).
12
+
13
+ ### Changed
14
+
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`.
16
+
17
+ ## [1.1.1]
18
+
19
+ ### Changed
20
+
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.
22
+
7
23
  ## [1.1.0]
8
24
 
9
25
  ### Added
package/README.md CHANGED
@@ -4,6 +4,16 @@ Schema-first, type-safe webhook routing built on the standard Web API [`Request`
4
4
 
5
5
  Full documentation: [zapstudio.dev/webhooks](https://www.zapstudio.dev/webhooks)
6
6
 
7
+ ## Motivation
8
+
9
+ Webhook endpoints are usually built by hand, per provider, and this tends to repeat the same two mistakes. First, signature checks often use Node's `crypto` module, which does not exist on Cloudflare Workers, Deno, or Bun's edge runtimes — so the same code cannot run everywhere the webhook needs to be received.
10
+
11
+ Second, signatures are often compared with `===`, which leaks timing information and opens a timing-attack risk that most teams do not know they have.
12
+
13
+ `@zap-studio/webhooks` fixes both. It is built on the standard `Request`/`Response` objects, so the same router works on Bun, Deno, Cloudflare Workers, or any framework that accepts a `Request`.
14
+
15
+ Signature verification is opt-in through the `verify` option, and the built-in HMAC verifier uses the Web Crypto API (`globalThis.crypto.subtle`) with a constant-time comparison, so once you turn it on, you get the safe comparison without writing it yourself. Payloads are validated and typed straight from your Standard Schema — no `any` from `JSON.parse`, no manual casts.
16
+
7
17
  ## Installation
8
18
 
9
19
  ```bash
@@ -151,6 +161,31 @@ const router = createWebhookRouter({ prefix: "/webhooks", logger });
151
161
 
152
162
  Each delivery attempt and handler dispatch logs at `debug`; verification failures and unmatched routes log at `warn`.
153
163
 
164
+ ## OpenTelemetry
165
+
166
+ `@opentelemetry/api` is a required peer dependency — tiny, side-effect-free, and a no-op until an app registers a real SDK, so installing it costs nothing at runtime for consumers who never set one up.
167
+
168
+ Each delivery gets a `SERVER` span, and the sender's `traceparent` header is extracted so the delivery continues their trace instead of starting a new one. Each handler dispatch gets its own child `INTERNAL` span:
169
+
170
+ ```bash
171
+ npm install @opentelemetry/api
172
+ ```
173
+
174
+ ```ts
175
+ import { createWebhookRouter } from "@zap-studio/webhooks";
176
+
177
+ const router = createWebhookRouter({ prefix: "/webhooks" });
178
+ router.register("/stripe", { schema: stripeEventSchema, handler });
179
+
180
+ // If your app has registered an OpenTelemetry SDK, router.handle(request)
181
+ // now produces a SERVER span per delivery (continuing the sender's trace
182
+ // when a traceparent header is present) and an INTERNAL span per handler
183
+ // dispatch. If not, it's a no-op — no wiring required either way.
184
+ export default { fetch: (request: Request) => router.handle(request) };
185
+ ```
186
+
187
+ A non-2xx response (unmatched route, validation failure, verification failure, handler error) marks the delivery span `ERROR`; a thrown handler error is also recorded as an exception on the handler span.
188
+
154
189
  ## Runtime Support
155
190
 
156
191
  | Runtime | Minimum version |
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  import { VerificationError } from "./errors.js";
2
- import { WebhookRouter, createWebhookRouter } from "./router.js";
2
+ import { n as createWebhookRouter, t as WebhookRouter } from "./router-DyyNi3QB.js";
3
3
  import { constantTimeEquals, createHmacVerifier } from "./verify.js";
4
4
  export { VerificationError, WebhookRouter, constantTimeEquals, createHmacVerifier, createWebhookRouter };
@@ -0,0 +1,291 @@
1
+ import { SpanKind, SpanStatusCode, context, propagation, trace } from "@opentelemetry/api";
2
+ import { standardValidate } from "@zap-studio/validation";
3
+ //#endregion
4
+ //#region src/_otel.ts
5
+ /**
6
+ * OpenTelemetry tracer for this package. Resolved once against the global
7
+ * `TracerProvider`; a no-op provider (the default until an app registers an
8
+ * SDK) makes every span/propagation call below a no-op too.
9
+ */
10
+ const tracer = trace.getTracer("@zap-studio/webhooks", "2.0.0");
11
+ /**
12
+ * `TextMapGetter` for the Web `Headers` API, used to extract an inbound
13
+ * delivery's `traceparent` (and any other registered propagator fields) so
14
+ * the delivery span continues the sender's trace instead of starting a new one.
15
+ */
16
+ const HEADERS_GETTER = {
17
+ get(carrier, key) {
18
+ return carrier.get(key) ?? void 0;
19
+ },
20
+ keys(carrier) {
21
+ return [...carrier.keys()];
22
+ }
23
+ };
24
+ /**
25
+ * Records `error` on `span` and marks it as failed. `recordException` only
26
+ * accepts an `Error` or `string`, so other thrown values just get the
27
+ * `ERROR` status without an attached exception event.
28
+ */
29
+ const recordSpanError = (span, error) => {
30
+ if (error instanceof Error || typeof error === "string") span.recordException(error);
31
+ span.setStatus({ code: SpanStatusCode.ERROR });
32
+ };
33
+ //#endregion
34
+ //#region src/router.ts
35
+ /**
36
+ * Schema-first webhook router with path dispatching, validation, and optional verification.
37
+ *
38
+ * @template TMap - Internal route payload map built incrementally via `register`.
39
+ */
40
+ const toArray = (value) => {
41
+ if (value === void 0) return [];
42
+ return Array.isArray(value) ? value : [value];
43
+ };
44
+ const notFoundResponse = () => Response.json({ error: "not found" }, { status: 404 });
45
+ /** Sets `http.response.status_code` and marks `span` `ERROR` on a non-2xx response. */
46
+ const finishDelivery = (span, response) => {
47
+ span.setAttribute("http.response.status_code", response.status);
48
+ if (!response.ok) span.setStatus({ code: SpanStatusCode.ERROR });
49
+ return response;
50
+ };
51
+ const bodyDecoder = new TextDecoder();
52
+ /**
53
+ * Normalizes a path to its canonical form: leading slash, no trailing slash,
54
+ * duplicate slashes collapsed. The root path is `"/"`.
55
+ */
56
+ const normalizePath = (path) => {
57
+ const withLeadingSlash = path.startsWith("/") ? path : `/${path}`;
58
+ const collapsed = withLeadingSlash.includes("//") ? withLeadingSlash.replaceAll(/\/{2,}/gu, "/") : withLeadingSlash;
59
+ return collapsed.length > 1 && collapsed.endsWith("/") ? collapsed.slice(0, -1) : collapsed;
60
+ };
61
+ /** Runs the given before-hooks in order against the request context. */
62
+ const runBeforeHooks = async (ctx, hooks) => {
63
+ if (!hooks || hooks.length === 0) return;
64
+ for (const hook of hooks) await hook(ctx);
65
+ };
66
+ /** Runs the given after-hooks in order against the request context and response. */
67
+ const runAfterHooks = async (ctx, response, hooks) => {
68
+ if (!hooks || hooks.length === 0) return;
69
+ for (const hook of hooks) await hook(ctx, response);
70
+ };
71
+ /** Builds an internal handler entry from route registration options. */
72
+ const createHandlerEntry = (options) => {
73
+ const entry = { handler: options.handler };
74
+ if (options.schema !== void 0) entry.schema = options.schema;
75
+ if (options.before !== void 0) entry.before = toArray(options.before);
76
+ if (options.after !== void 0) entry.after = toArray(options.after);
77
+ return entry;
78
+ };
79
+ /** Parses the request's raw body bytes as JSON, returning `undefined` on invalid JSON. */
80
+ const parseRequestBody = (ctx) => {
81
+ try {
82
+ return JSON.parse(bodyDecoder.decode(ctx.rawBody));
83
+ } catch {
84
+ return;
85
+ }
86
+ };
87
+ /** Validates the parsed payload against the route schema, returning either the validated value or a `400` response. */
88
+ const validatePayload = async (parsedJson, schema) => {
89
+ if (!schema) return parsedJson;
90
+ const result = await standardValidate(parsedJson, schema, { throwOnError: false });
91
+ if (result.issues) return Response.json({
92
+ error: "validation failed",
93
+ issues: result.issues.map((issue) => ({
94
+ message: issue.message,
95
+ path: issue.path?.map((p) => typeof p === "object" && "key" in p ? String(p.key) : String(p))
96
+ }))
97
+ }, { status: 400 });
98
+ return result.value;
99
+ };
100
+ /** Invokes the route handler with the validated payload, defaulting to a `200 "ok"` response. */
101
+ const executeHandler = async (handler, ctx, validatedPayload) => {
102
+ return await handler({
103
+ ...ctx,
104
+ payload: validatedPayload
105
+ }) ?? Response.json("ok");
106
+ };
107
+ /** Runs the route handler inside its own `INTERNAL` span, nested under the delivery span. */
108
+ const dispatchHandler = async (handlerEntry, ctx, validatedPayload, deliveryContext) => {
109
+ const handlerSpan = tracer.startSpan(`webhook.handler ${ctx.path}`, { kind: SpanKind.INTERNAL }, deliveryContext);
110
+ try {
111
+ return await context.with(trace.setSpan(deliveryContext, handlerSpan), async () => await executeHandler(handlerEntry.handler, ctx, validatedPayload));
112
+ } catch (error) {
113
+ recordSpanError(handlerSpan, error);
114
+ throw error;
115
+ } finally {
116
+ handlerSpan.end();
117
+ }
118
+ };
119
+ /**
120
+ * Main webhook router class.
121
+ *
122
+ * Register routes with typed schemas and call `handle` with a Web API `Request`.
123
+ *
124
+ * @example
125
+ * ```ts
126
+ * import { WebhookRouter } from "@zap-studio/webhooks";
127
+ *
128
+ * const router = new WebhookRouter({ prefix: "/webhooks" });
129
+ *
130
+ * router.register("/stripe", {
131
+ * schema: stripeEventSchema,
132
+ * handler: async ({ payload }) => {
133
+ * console.log("Stripe event:", payload.type);
134
+ * },
135
+ * });
136
+ *
137
+ * export default { fetch: (request: Request) => router.handle(request) };
138
+ * ```
139
+ */
140
+ var WebhookRouter = class {
141
+ handlers = /* @__PURE__ */ new Map();
142
+ verify;
143
+ globalBeforeHooks = [];
144
+ globalAfterHooks = [];
145
+ globalErrorHook;
146
+ logger;
147
+ prefix;
148
+ prefixWithSlash;
149
+ /**
150
+ * Creates a webhook router with optional global hooks and verification behavior.
151
+ *
152
+ * @param opts - Router-level options.
153
+ *
154
+ * @example
155
+ * ```ts
156
+ * const router = new WebhookRouter({
157
+ * prefix: "/webhooks",
158
+ * verify: createHmacVerifier({ headerName: "x-signature", secret }),
159
+ * onError: (error) => Response.json({ error: error.message }, { status: 500 }),
160
+ * });
161
+ * ```
162
+ */
163
+ constructor(opts = {}) {
164
+ this.prefix = normalizePath(opts.prefix ?? "/webhooks");
165
+ this.prefixWithSlash = `${this.prefix}/`;
166
+ this.verify = opts.verify;
167
+ this.globalBeforeHooks = toArray(opts.before);
168
+ this.globalAfterHooks = toArray(opts.after);
169
+ this.globalErrorHook = opts.onError;
170
+ this.logger = opts.logger;
171
+ }
172
+ register(path, handlerOrOptions) {
173
+ this.handlers.set(normalizePath(path), typeof handlerOrOptions === "function" ? { handler: handlerOrOptions } : createHandlerEntry(handlerOrOptions));
174
+ return this;
175
+ }
176
+ /**
177
+ * Handles an incoming webhook request.
178
+ *
179
+ * The request body is read exactly once; hooks and handlers receive the raw
180
+ * bytes through the webhook context instead of the request stream.
181
+ *
182
+ * @param request - Incoming Web API request.
183
+ * @returns Web API response for the runtime to send back.
184
+ *
185
+ * @example
186
+ * ```ts
187
+ * // Framework-agnostic: works with any Web API Request/Response runtime.
188
+ * export async function POST(request: Request): Promise<Response> {
189
+ * return router.handle(request);
190
+ * }
191
+ * ```
192
+ */
193
+ async handle(request) {
194
+ const requestPath = new URL(request.url).pathname;
195
+ const { method } = request;
196
+ this.logger?.debug("webhook delivery attempt", { path: requestPath });
197
+ const parentContext = propagation.extract(context.active(), request.headers, HEADERS_GETTER);
198
+ const deliverySpan = tracer.startSpan(`${method} ${requestPath}`, {
199
+ attributes: {
200
+ "http.request.method": method,
201
+ "url.path": requestPath
202
+ },
203
+ kind: SpanKind.SERVER
204
+ }, parentContext);
205
+ const deliveryContext = trace.setSpan(parentContext, deliverySpan);
206
+ try {
207
+ const response = await context.with(deliveryContext, async () => await this.dispatch(request, requestPath, deliveryContext));
208
+ return finishDelivery(deliverySpan, response);
209
+ } finally {
210
+ deliverySpan.end();
211
+ }
212
+ }
213
+ /** Matches the route, runs hooks/verification/validation, and dispatches the handler. */
214
+ async dispatch(request, requestPath, deliveryContext) {
215
+ const path = this.matchPath(request);
216
+ if (path === null) {
217
+ this.logger?.warn("webhook route not matched", { path: requestPath });
218
+ return notFoundResponse();
219
+ }
220
+ const handlerEntry = this.handlers.get(path);
221
+ if (!handlerEntry) {
222
+ this.logger?.warn("webhook route not matched", { path });
223
+ return notFoundResponse();
224
+ }
225
+ const ctx = {
226
+ path,
227
+ rawBody: /* @__PURE__ */ new Uint8Array(0),
228
+ request
229
+ };
230
+ try {
231
+ ctx.rawBody = new Uint8Array(await request.arrayBuffer());
232
+ await runBeforeHooks(ctx, this.globalBeforeHooks);
233
+ await runBeforeHooks(ctx, handlerEntry.before);
234
+ if (this.verify) try {
235
+ await this.verify(ctx);
236
+ } catch (error) {
237
+ this.logger?.warn("webhook verification failed", {
238
+ error,
239
+ path
240
+ });
241
+ throw error;
242
+ }
243
+ const parsedJson = parseRequestBody(ctx);
244
+ const validationResult = await validatePayload(parsedJson, handlerEntry.schema);
245
+ if (validationResult instanceof Response) return validationResult;
246
+ this.logger?.debug("webhook handler dispatch", { path });
247
+ const response = await dispatchHandler(handlerEntry, ctx, validationResult, deliveryContext);
248
+ await runAfterHooks(ctx, response, handlerEntry.after);
249
+ await runAfterHooks(ctx, response, this.globalAfterHooks);
250
+ return response;
251
+ } catch (error) {
252
+ return await this.handleError(error, ctx);
253
+ }
254
+ }
255
+ /** Resolves the incoming request's URL to a registered route key, or `null` if it doesn't match the configured prefix. */
256
+ matchPath(request) {
257
+ const pathname = normalizePath(new URL(request.url).pathname);
258
+ if (this.prefix === "/") return pathname;
259
+ if (pathname === this.prefix) return "/";
260
+ if (!pathname.startsWith(this.prefixWithSlash)) return null;
261
+ return pathname.slice(this.prefix.length);
262
+ }
263
+ /** Builds the error response for a failed request, deferring to the global error hook when set. */
264
+ async handleError(error, ctx) {
265
+ if (this.globalErrorHook) {
266
+ const normalizedError = error instanceof Error ? error : /* @__PURE__ */ new Error("Internal server error");
267
+ const errorResponse = await this.globalErrorHook(normalizedError, ctx);
268
+ if (errorResponse) return errorResponse;
269
+ }
270
+ return Response.json({ error: error instanceof Error ? error.message : "Internal server error" }, { status: 500 });
271
+ }
272
+ };
273
+ /**
274
+ * Factory helper for creating a webhook router instance.
275
+ *
276
+ * @param opts - Optional global router options.
277
+ * @returns A new webhook router.
278
+ *
279
+ * @example
280
+ * ```ts
281
+ * import { createWebhookRouter } from "@zap-studio/webhooks";
282
+ *
283
+ * const router = createWebhookRouter({ prefix: "/webhooks" });
284
+ * router.register("/stripe", { schema: stripeEventSchema, handler });
285
+ * ```
286
+ */
287
+ const createWebhookRouter = (opts) => new WebhookRouter(opts);
288
+ //#endregion
289
+ export { createWebhookRouter as n, WebhookRouter as t };
290
+
291
+ //# sourceMappingURL=router-DyyNi3QB.js.map
@@ -0,0 +1 @@
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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"}
package/dist/router.d.ts CHANGED
@@ -113,6 +113,8 @@ declare class WebhookRouter<TMap = unknown> {
113
113
  * ```
114
114
  */
115
115
  handle(request: Request): Promise<Response>;
116
+ /** Matches the route, runs hooks/verification/validation, and dispatches the handler. */
117
+ private dispatch;
116
118
  /** Resolves the incoming request's URL to a registered route key, or `null` if it doesn't match the configured prefix. */
117
119
  private matchPath;
118
120
  /** Builds the error response for a failed request, deferring to the global error hook when set. */
@@ -1 +1 @@
1
- {"version":3,"file":"router.d.ts","names":[],"sources":["../src/router.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;cA4La,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;;UAgEhC;;UAsBM;;;;;;;;;;;;;;;;cAoCH,sBACX,OAAO,yBACN"}
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"}
package/dist/router.js CHANGED
@@ -1,222 +1,2 @@
1
- import { standardValidate } from "@zap-studio/validation";
2
- //#region src/router.ts
3
- /**
4
- * Schema-first webhook router with path dispatching, validation, and optional verification.
5
- *
6
- * @template TMap - Internal route payload map built incrementally via `register`.
7
- */
8
- const toArray = (value) => {
9
- if (value === void 0) return [];
10
- return Array.isArray(value) ? value : [value];
11
- };
12
- const notFoundResponse = () => Response.json({ error: "not found" }, { status: 404 });
13
- const bodyDecoder = new TextDecoder();
14
- /**
15
- * Normalizes a path to its canonical form: leading slash, no trailing slash,
16
- * duplicate slashes collapsed. The root path is `"/"`.
17
- */
18
- const normalizePath = (path) => {
19
- const withLeadingSlash = path.startsWith("/") ? path : `/${path}`;
20
- const collapsed = withLeadingSlash.includes("//") ? withLeadingSlash.replaceAll(/\/{2,}/gu, "/") : withLeadingSlash;
21
- return collapsed.length > 1 && collapsed.endsWith("/") ? collapsed.slice(0, -1) : collapsed;
22
- };
23
- /** Runs the given before-hooks in order against the request context. */
24
- const runBeforeHooks = async (ctx, hooks) => {
25
- if (!hooks || hooks.length === 0) return;
26
- for (const hook of hooks) await hook(ctx);
27
- };
28
- /** Runs the given after-hooks in order against the request context and response. */
29
- const runAfterHooks = async (ctx, response, hooks) => {
30
- if (!hooks || hooks.length === 0) return;
31
- for (const hook of hooks) await hook(ctx, response);
32
- };
33
- /** Builds an internal handler entry from route registration options. */
34
- const createHandlerEntry = (options) => {
35
- const entry = { handler: options.handler };
36
- if (options.schema !== void 0) entry.schema = options.schema;
37
- if (options.before !== void 0) entry.before = toArray(options.before);
38
- if (options.after !== void 0) entry.after = toArray(options.after);
39
- return entry;
40
- };
41
- /** Parses the request's raw body bytes as JSON, returning `undefined` on invalid JSON. */
42
- const parseRequestBody = (ctx) => {
43
- try {
44
- return JSON.parse(bodyDecoder.decode(ctx.rawBody));
45
- } catch {
46
- return;
47
- }
48
- };
49
- /** Validates the parsed payload against the route schema, returning either the validated value or a `400` response. */
50
- const validatePayload = async (parsedJson, schema) => {
51
- if (!schema) return parsedJson;
52
- const result = await standardValidate(parsedJson, schema, { throwOnError: false });
53
- if (result.issues) return Response.json({
54
- error: "validation failed",
55
- issues: result.issues.map((issue) => ({
56
- message: issue.message,
57
- path: issue.path?.map((p) => typeof p === "object" && "key" in p ? String(p.key) : String(p))
58
- }))
59
- }, { status: 400 });
60
- return result.value;
61
- };
62
- /** Invokes the route handler with the validated payload, defaulting to a `200 "ok"` response. */
63
- const executeHandler = async (handler, ctx, validatedPayload) => {
64
- return await handler({
65
- ...ctx,
66
- payload: validatedPayload
67
- }) ?? Response.json("ok");
68
- };
69
- /**
70
- * Main webhook router class.
71
- *
72
- * Register routes with typed schemas and call `handle` with a Web API `Request`.
73
- *
74
- * @example
75
- * ```ts
76
- * import { WebhookRouter } from "@zap-studio/webhooks";
77
- *
78
- * const router = new WebhookRouter({ prefix: "/webhooks" });
79
- *
80
- * router.register("/stripe", {
81
- * schema: stripeEventSchema,
82
- * handler: async ({ payload }) => {
83
- * console.log("Stripe event:", payload.type);
84
- * },
85
- * });
86
- *
87
- * export default { fetch: (request: Request) => router.handle(request) };
88
- * ```
89
- */
90
- var WebhookRouter = class {
91
- handlers = /* @__PURE__ */ new Map();
92
- verify;
93
- globalBeforeHooks = [];
94
- globalAfterHooks = [];
95
- globalErrorHook;
96
- logger;
97
- prefix;
98
- prefixWithSlash;
99
- /**
100
- * Creates a webhook router with optional global hooks and verification behavior.
101
- *
102
- * @param opts - Router-level options.
103
- *
104
- * @example
105
- * ```ts
106
- * const router = new WebhookRouter({
107
- * prefix: "/webhooks",
108
- * verify: createHmacVerifier({ headerName: "x-signature", secret }),
109
- * onError: (error) => Response.json({ error: error.message }, { status: 500 }),
110
- * });
111
- * ```
112
- */
113
- constructor(opts = {}) {
114
- this.prefix = normalizePath(opts.prefix ?? "/webhooks");
115
- this.prefixWithSlash = `${this.prefix}/`;
116
- this.verify = opts.verify;
117
- this.globalBeforeHooks = toArray(opts.before);
118
- this.globalAfterHooks = toArray(opts.after);
119
- this.globalErrorHook = opts.onError;
120
- this.logger = opts.logger;
121
- }
122
- register(path, handlerOrOptions) {
123
- this.handlers.set(normalizePath(path), typeof handlerOrOptions === "function" ? { handler: handlerOrOptions } : createHandlerEntry(handlerOrOptions));
124
- return this;
125
- }
126
- /**
127
- * Handles an incoming webhook request.
128
- *
129
- * The request body is read exactly once; hooks and handlers receive the raw
130
- * bytes through the webhook context instead of the request stream.
131
- *
132
- * @param request - Incoming Web API request.
133
- * @returns Web API response for the runtime to send back.
134
- *
135
- * @example
136
- * ```ts
137
- * // Framework-agnostic: works with any Web API Request/Response runtime.
138
- * export async function POST(request: Request): Promise<Response> {
139
- * return router.handle(request);
140
- * }
141
- * ```
142
- */
143
- async handle(request) {
144
- const requestPath = new URL(request.url).pathname;
145
- this.logger?.debug("webhook delivery attempt", { path: requestPath });
146
- const path = this.matchPath(request);
147
- if (path === null) {
148
- this.logger?.warn("webhook route not matched", { path: requestPath });
149
- return notFoundResponse();
150
- }
151
- const handlerEntry = this.handlers.get(path);
152
- if (!handlerEntry) {
153
- this.logger?.warn("webhook route not matched", { path });
154
- return notFoundResponse();
155
- }
156
- const ctx = {
157
- path,
158
- rawBody: /* @__PURE__ */ new Uint8Array(0),
159
- request
160
- };
161
- try {
162
- ctx.rawBody = new Uint8Array(await request.arrayBuffer());
163
- await runBeforeHooks(ctx, this.globalBeforeHooks);
164
- await runBeforeHooks(ctx, handlerEntry.before);
165
- if (this.verify) try {
166
- await this.verify(ctx);
167
- } catch (error) {
168
- this.logger?.warn("webhook verification failed", {
169
- error,
170
- path
171
- });
172
- throw error;
173
- }
174
- const parsedJson = parseRequestBody(ctx);
175
- const validationResult = await validatePayload(parsedJson, handlerEntry.schema);
176
- if (validationResult instanceof Response) return validationResult;
177
- this.logger?.debug("webhook handler dispatch", { path });
178
- const response = await executeHandler(handlerEntry.handler, ctx, validationResult);
179
- await runAfterHooks(ctx, response, handlerEntry.after);
180
- await runAfterHooks(ctx, response, this.globalAfterHooks);
181
- return response;
182
- } catch (error) {
183
- return await this.handleError(error, ctx);
184
- }
185
- }
186
- /** Resolves the incoming request's URL to a registered route key, or `null` if it doesn't match the configured prefix. */
187
- matchPath(request) {
188
- const pathname = normalizePath(new URL(request.url).pathname);
189
- if (this.prefix === "/") return pathname;
190
- if (pathname === this.prefix) return "/";
191
- if (!pathname.startsWith(this.prefixWithSlash)) return null;
192
- return pathname.slice(this.prefix.length);
193
- }
194
- /** Builds the error response for a failed request, deferring to the global error hook when set. */
195
- async handleError(error, ctx) {
196
- if (this.globalErrorHook) {
197
- const normalizedError = error instanceof Error ? error : /* @__PURE__ */ new Error("Internal server error");
198
- const errorResponse = await this.globalErrorHook(normalizedError, ctx);
199
- if (errorResponse) return errorResponse;
200
- }
201
- return Response.json({ error: error instanceof Error ? error.message : "Internal server error" }, { status: 500 });
202
- }
203
- };
204
- /**
205
- * Factory helper for creating a webhook router instance.
206
- *
207
- * @param opts - Optional global router options.
208
- * @returns A new webhook router.
209
- *
210
- * @example
211
- * ```ts
212
- * import { createWebhookRouter } from "@zap-studio/webhooks";
213
- *
214
- * const router = createWebhookRouter({ prefix: "/webhooks" });
215
- * router.register("/stripe", { schema: stripeEventSchema, handler });
216
- * ```
217
- */
218
- const createWebhookRouter = (opts) => new WebhookRouter(opts);
219
- //#endregion
1
+ import { n as createWebhookRouter, t as WebhookRouter } from "./router-DyyNi3QB.js";
220
2
  export { WebhookRouter, createWebhookRouter };
221
-
222
- //# sourceMappingURL=router.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zap-studio/webhooks",
3
- "version": "1.1.0",
3
+ "version": "2.0.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,15 +46,28 @@
46
46
  "access": "public"
47
47
  },
48
48
  "dependencies": {
49
- "@zap-studio/logger": "1.0.0",
50
49
  "@zap-studio/validation": "1.0.0"
51
50
  },
52
51
  "devDependencies": {
52
+ "@opentelemetry/api": "^1.9.0",
53
+ "@opentelemetry/context-async-hooks": "^2.10.0",
54
+ "@opentelemetry/core": "^2.10.0",
55
+ "@opentelemetry/sdk-trace-base": "^2.10.0",
53
56
  "tsdown": "^0.22.14",
54
57
  "typescript": "^7.0.2",
55
58
  "vitest": "^4.1.10",
56
59
  "zod": "^4.4.3",
57
- "@zap-studio/typescript": "0.0.0"
60
+ "@zap-studio/typescript": "0.0.0",
61
+ "@zap-studio/logger": "2.0.0"
62
+ },
63
+ "peerDependencies": {
64
+ "@opentelemetry/api": "^1.9.0",
65
+ "@zap-studio/logger": "2.0.0"
66
+ },
67
+ "peerDependenciesMeta": {
68
+ "@zap-studio/logger": {
69
+ "optional": true
70
+ }
58
71
  },
59
72
  "engines": {
60
73
  "node": ">=18.0.0"
@@ -1 +0,0 @@
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 { Logger } from \"@zap-studio/logger\";\nimport type { StandardSchemaV1 } from \"@zap-studio/validation\";\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.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\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/**\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 this.logger?.debug(\"webhook delivery attempt\", { path: requestPath });\n\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 executeHandler(\n handlerEntry.handler,\n ctx,\n validationResult\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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"}