@zap-studio/webhooks 2.1.0 → 2.1.1
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 +7 -0
- package/dist/errors.d.ts +1 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/{router-Pf2VoqgE.js → router-rax4SfyZ.js} +2 -2
- package/dist/{router-Pf2VoqgE.js.map → router-rax4SfyZ.js.map} +1 -1
- package/dist/router.d.ts +2 -3
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +1 -1
- package/dist/types.d.ts +16 -17
- package/dist/types.d.ts.map +1 -1
- package/dist/verify.d.ts +2 -3
- package/dist/verify.d.ts.map +1 -1
- package/package.json +8 -8
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,13 @@ 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
|
+
|
|
8
|
+
## 2.1.1
|
|
9
|
+
<sub>2026-09-13</sub>
|
|
10
|
+
|
|
11
|
+
- [#613](https://github.com/zap-studio/monorepo/pull/613) *(patch)* Thanks [@alexandretrotel](https://github.com/alexandretrotel)!
|
|
12
|
+
Widen internal peer dependency ranges from an exact pin to a caret range, so consumers no longer resolve a duplicate copy of the peer on version skew.
|
|
13
|
+
|
|
7
14
|
## [2.1.0]
|
|
8
15
|
|
|
9
16
|
### Added
|
package/dist/errors.d.ts
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* });
|
|
27
27
|
* ```
|
|
28
28
|
*/
|
|
29
|
-
declare class VerificationError extends Error {
|
|
29
|
+
export declare class VerificationError extends Error {
|
|
30
30
|
/**
|
|
31
31
|
* Creates a verification error with a human-readable message.
|
|
32
32
|
*
|
|
@@ -35,5 +35,4 @@ declare class VerificationError extends Error {
|
|
|
35
35
|
constructor(message: string);
|
|
36
36
|
}
|
|
37
37
|
//#endregion
|
|
38
|
-
export { VerificationError };
|
|
39
38
|
//# sourceMappingURL=errors.d.ts.map
|
package/dist/errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","names":[],"sources":["../src/errors.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"errors.d.ts","names":[],"sources":["../src/errors.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;qBA4Ba,0BAA0B;;;;;;EAMrC,YAAY"}
|
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-
|
|
2
|
+
import { n as createWebhookRouter, t as WebhookRouter } from "./router-rax4SfyZ.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.1.
|
|
10
|
+
const tracer = trace.getTracer("@zap-studio/webhooks", "2.1.1");
|
|
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
|
|
@@ -301,4 +301,4 @@ const createWebhookRouter = (opts) => new WebhookRouter(opts);
|
|
|
301
301
|
//#endregion
|
|
302
302
|
export { createWebhookRouter as n, WebhookRouter as t };
|
|
303
303
|
|
|
304
|
-
//# sourceMappingURL=router-
|
|
304
|
+
//# sourceMappingURL=router-rax4SfyZ.js.map
|
|
@@ -1 +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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"}
|
|
1
|
+
{"version":3,"file":"router-rax4SfyZ.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
|
@@ -33,7 +33,7 @@ import { StandardSchemaV1 } from "@zap-studio/validation";
|
|
|
33
33
|
* export default { fetch: (request: Request) => router.handle(request) };
|
|
34
34
|
* ```
|
|
35
35
|
*/
|
|
36
|
-
declare class WebhookRouter<TMap = unknown> {
|
|
36
|
+
export declare class WebhookRouter<TMap = unknown> {
|
|
37
37
|
private readonly handlers;
|
|
38
38
|
private readonly verify;
|
|
39
39
|
private readonly globalBeforeHooks;
|
|
@@ -145,7 +145,6 @@ declare class WebhookRouter<TMap = unknown> {
|
|
|
145
145
|
* router.register("/stripe", { schema: stripeEventSchema, handler });
|
|
146
146
|
* ```
|
|
147
147
|
*/
|
|
148
|
-
declare const createWebhookRouter: (opts?: WebhookRouterOptions) => WebhookRouter;
|
|
148
|
+
export declare const createWebhookRouter: (opts?: WebhookRouterOptions) => WebhookRouter;
|
|
149
149
|
//#endregion
|
|
150
|
-
export { WebhookRouter, createWebhookRouter };
|
|
151
150
|
//# sourceMappingURL=router.d.ts.map
|
package/dist/router.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.d.ts","names":[],"sources":["../src/router.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"router.d.ts","names":[],"sources":["../src/router.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAiPa,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;;;;;;;;;;;;;;;;qBAgCH,sBAAuB,OAAO,yBAAuB"}
|
package/dist/router.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as createWebhookRouter, t as WebhookRouter } from "./router-
|
|
1
|
+
import { n as createWebhookRouter, t as WebhookRouter } from "./router-rax4SfyZ.js";
|
|
2
2
|
export { WebhookRouter, createWebhookRouter };
|
package/dist/types.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { Logger } from "@zap-studio/logger";
|
|
|
14
14
|
* };
|
|
15
15
|
* ```
|
|
16
16
|
*/
|
|
17
|
-
interface WebhookContext {
|
|
17
|
+
export interface WebhookContext {
|
|
18
18
|
/** The matched route key registered on the router (e.g. "stripe") */
|
|
19
19
|
path: string;
|
|
20
20
|
/** The exact request body bytes (for signature verification) */
|
|
@@ -34,12 +34,12 @@ interface WebhookContext {
|
|
|
34
34
|
* };
|
|
35
35
|
* ```
|
|
36
36
|
*/
|
|
37
|
-
interface HandlerContext<TPayload = unknown> extends WebhookContext {
|
|
37
|
+
export interface HandlerContext<TPayload = unknown> extends WebhookContext {
|
|
38
38
|
/** The validated webhook payload */
|
|
39
39
|
payload: TPayload;
|
|
40
40
|
}
|
|
41
41
|
/** Internal handler entry stored per registered route. */
|
|
42
|
-
interface HandlerEntry<TPayload = unknown> {
|
|
42
|
+
export interface HandlerEntry<TPayload = unknown> {
|
|
43
43
|
/** Route-level hooks that run after successful processing. */
|
|
44
44
|
after?: AfterHook[];
|
|
45
45
|
/** Route-level hooks that run before request processing. */
|
|
@@ -62,7 +62,7 @@ interface HandlerEntry<TPayload = unknown> {
|
|
|
62
62
|
* };
|
|
63
63
|
* ```
|
|
64
64
|
*/
|
|
65
|
-
interface WebhookRouterOptions {
|
|
65
|
+
export interface WebhookRouterOptions {
|
|
66
66
|
/** Global hooks executed after successful route handler completion. */
|
|
67
67
|
after?: AfterHook | AfterHook[];
|
|
68
68
|
/** Global hooks executed before route-level hooks and verification. */
|
|
@@ -102,7 +102,7 @@ interface WebhookRouterOptions {
|
|
|
102
102
|
* };
|
|
103
103
|
* ```
|
|
104
104
|
*/
|
|
105
|
-
interface RegisterOptions<T> {
|
|
105
|
+
export interface RegisterOptions<T> {
|
|
106
106
|
/** Hooks that run after successful processing (before global after hooks) */
|
|
107
107
|
after?: AfterHook | AfterHook[];
|
|
108
108
|
/** Hooks that run before request processing (after global before hooks) */
|
|
@@ -123,7 +123,7 @@ interface RegisterOptions<T> {
|
|
|
123
123
|
*
|
|
124
124
|
* @template TSchema - A Standard Schema type.
|
|
125
125
|
*/
|
|
126
|
-
type InferSchemaOutput<TSchema> = TSchema extends StandardSchemaV1<unknown, infer TOutput> ? TOutput : never;
|
|
126
|
+
export type InferSchemaOutput<TSchema> = TSchema extends StandardSchemaV1<unknown, infer TOutput> ? TOutput : never;
|
|
127
127
|
/**
|
|
128
128
|
* Route options where schema is required and handler payload is inferred.
|
|
129
129
|
*
|
|
@@ -137,11 +137,11 @@ type InferSchemaOutput<TSchema> = TSchema extends StandardSchemaV1<unknown, infe
|
|
|
137
137
|
* };
|
|
138
138
|
* ```
|
|
139
139
|
*/
|
|
140
|
-
type SchemaRouteOptions<TSchema extends StandardSchemaV1<unknown, unknown>> = Omit<RegisterOptions<InferSchemaOutput<TSchema>>, "schema"> & {
|
|
140
|
+
export type SchemaRouteOptions<TSchema extends StandardSchemaV1<unknown, unknown>> = Omit<RegisterOptions<InferSchemaOutput<TSchema>>, "schema"> & {
|
|
141
141
|
schema: TSchema;
|
|
142
142
|
};
|
|
143
143
|
/** A single route's registration shape, as used by schema-driven route dictionaries. */
|
|
144
|
-
interface RouteLike {
|
|
144
|
+
export interface RouteLike {
|
|
145
145
|
/** Hooks that run after successful processing. */
|
|
146
146
|
after?: AfterHook | AfterHook[];
|
|
147
147
|
/** Hooks that run before request processing. */
|
|
@@ -163,7 +163,7 @@ interface RouteLike {
|
|
|
163
163
|
* };
|
|
164
164
|
* ```
|
|
165
165
|
*/
|
|
166
|
-
type SchemaRoutes<TRoutes extends Record<string, RouteLike>> = { [P in keyof TRoutes]: SchemaRouteOptions<TRoutes[P]["schema"]>; };
|
|
166
|
+
export type SchemaRoutes<TRoutes extends Record<string, RouteLike>> = { [P in keyof TRoutes]: SchemaRouteOptions<TRoutes[P]["schema"]>; };
|
|
167
167
|
/**
|
|
168
168
|
* The webhook handler function, responsible for processing incoming webhook events.
|
|
169
169
|
*
|
|
@@ -177,7 +177,7 @@ type SchemaRoutes<TRoutes extends Record<string, RouteLike>> = { [P in keyof TRo
|
|
|
177
177
|
* };
|
|
178
178
|
* ```
|
|
179
179
|
*/
|
|
180
|
-
type WebhookHandler<TPayload = unknown> = (ctx: HandlerContext<TPayload>) => Promise<Response | undefined> | Response | undefined;
|
|
180
|
+
export type WebhookHandler<TPayload = unknown> = (ctx: HandlerContext<TPayload>) => Promise<Response | undefined> | Response | undefined;
|
|
181
181
|
/**
|
|
182
182
|
* Maps route keys to their payload-specific webhook handlers.
|
|
183
183
|
*
|
|
@@ -188,7 +188,7 @@ type WebhookHandler<TPayload = unknown> = (ctx: HandlerContext<TPayload>) => Pro
|
|
|
188
188
|
* };
|
|
189
189
|
* ```
|
|
190
190
|
*/
|
|
191
|
-
type HandlerMap<TMap extends Record<string, unknown>> = { [P in keyof TMap]: WebhookHandler<TMap[P]>; };
|
|
191
|
+
export type HandlerMap<TMap extends Record<string, unknown>> = { [P in keyof TMap]: WebhookHandler<TMap[P]>; };
|
|
192
192
|
/**
|
|
193
193
|
* Builds a webhook payload map from a schema-based route dictionary.
|
|
194
194
|
*
|
|
@@ -201,7 +201,7 @@ type HandlerMap<TMap extends Record<string, unknown>> = { [P in keyof TMap]: Web
|
|
|
201
201
|
* }>;
|
|
202
202
|
* ```
|
|
203
203
|
*/
|
|
204
|
-
type InferWebhookMapFromRoutes<TRoutes extends Record<string, RouteLike>> = { [P in keyof TRoutes]: InferSchemaOutput<TRoutes[P]["schema"]>; };
|
|
204
|
+
export type InferWebhookMapFromRoutes<TRoutes extends Record<string, RouteLike>> = { [P in keyof TRoutes]: InferSchemaOutput<TRoutes[P]["schema"]>; };
|
|
205
205
|
/**
|
|
206
206
|
* Verification function for incoming requests. Throws to reject the request.
|
|
207
207
|
*
|
|
@@ -210,7 +210,7 @@ type InferWebhookMapFromRoutes<TRoutes extends Record<string, RouteLike>> = { [P
|
|
|
210
210
|
* const verify: VerifyFn = createHmacVerifier({ headerName: "x-signature", secret });
|
|
211
211
|
* ```
|
|
212
212
|
*/
|
|
213
|
-
type VerifyFn = (ctx: WebhookContext) => Promise<void> | void;
|
|
213
|
+
export type VerifyFn = (ctx: WebhookContext) => Promise<void> | void;
|
|
214
214
|
/**
|
|
215
215
|
* Hook function that runs before request processing
|
|
216
216
|
*
|
|
@@ -219,7 +219,7 @@ type VerifyFn = (ctx: WebhookContext) => Promise<void> | void;
|
|
|
219
219
|
* const before: BeforeHook = (ctx) => console.log("received", ctx.path);
|
|
220
220
|
* ```
|
|
221
221
|
*/
|
|
222
|
-
type BeforeHook = (ctx: WebhookContext) => Promise<void> | void;
|
|
222
|
+
export type BeforeHook = (ctx: WebhookContext) => Promise<void> | void;
|
|
223
223
|
/**
|
|
224
224
|
* Hook function that runs after successful request processing.
|
|
225
225
|
*
|
|
@@ -231,7 +231,7 @@ type BeforeHook = (ctx: WebhookContext) => Promise<void> | void;
|
|
|
231
231
|
* const after: AfterHook = (ctx, response) => console.log(response.status);
|
|
232
232
|
* ```
|
|
233
233
|
*/
|
|
234
|
-
type AfterHook = (ctx: WebhookContext, response: Response) => Promise<void> | void;
|
|
234
|
+
export type AfterHook = (ctx: WebhookContext, response: Response) => Promise<void> | void;
|
|
235
235
|
/**
|
|
236
236
|
* Hook function that runs when an error occurs
|
|
237
237
|
*
|
|
@@ -240,7 +240,6 @@ type AfterHook = (ctx: WebhookContext, response: Response) => Promise<void> | vo
|
|
|
240
240
|
* const onError: ErrorHook = (error) => Response.json({ error: error.message }, { status: 500 });
|
|
241
241
|
* ```
|
|
242
242
|
*/
|
|
243
|
-
type ErrorHook = (error: Error, ctx: WebhookContext) => Promise<Response | undefined> | Response | undefined;
|
|
243
|
+
export type ErrorHook = (error: Error, ctx: WebhookContext) => Promise<Response | undefined> | Response | undefined;
|
|
244
244
|
//#endregion
|
|
245
|
-
export { AfterHook, BeforeHook, ErrorHook, HandlerContext, HandlerEntry, HandlerMap, InferSchemaOutput, InferWebhookMapFromRoutes, RegisterOptions, RouteLike, SchemaRouteOptions, SchemaRoutes, VerifyFn, WebhookContext, WebhookHandler, WebhookRouterOptions };
|
|
246
245
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"types.d.ts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;;;;;;;;;;;;iBAsBiB;;EAEf;;EAEA,SAAS;;EAET,SAAS;;;;;;;;;;;;;;iBAeM,eAAe,4BAA4B;;EAE1D,SAAS;;;iBAIM,aAAa;;EAE5B,QAAQ;;EAER,SAAS;;EAET,SAAS,eAAe;;EAExB,SAAS,0BAA0B;;EAEnC,SAAS;;;;;;;;;;;;;iBAcM;;EAEf,QAAQ,YAAY;;EAEpB,SAAS,aAAa;;EAEtB,UAAU;;;;;;;EAOV,SAAS;;;;;;;EAOT;;;;;;EAMA,SAAS;;;;;;;;;;;;;;iBAeM,gBAAgB;;EAE/B,QAAQ,YAAY;;EAEpB,SAAS,aAAa;;EAEtB,SAAS,eAAe;;EAExB,SAAS,0BAA0B;;;;;;EAMnC,SAAS;;;;;;;YAQC,kBAAkB,WAC5B,gBAAgB,gCAAgC,WAAW;;;;;;;;;;;;;;YAejD,mBAAmB,gBAAgB,sCAAsC,KACnF,gBAAgB,kBAAkB;EAGlC,QAAQ;;;iBAIO;;EAEf,QAAQ,YAAY;;EAEpB,SAAS,aAAa;;EAEtB,SAAS;;EAET,QAAQ;;;;;;;;;;;;;;YAeE,aAAa,gBAAgB,eAAe,iBACrD,WAAW,UAAU,mBAAmB,QAAQ;;;;;;;;;;;;;;YAgBvC,eAAe,uBACzB,KAAK,eAAe,cACjB,QAAQ,wBAAwB;;;;;;;;;;;YAYzB,WAAW,aAAa,8BACjC,WAAW,OAAO,eAAe,KAAK;;;;;;;;;;;;;YAe7B,0BAA0B,gBAAgB,eAAe,iBAClE,WAAW,UAAU,kBAAkB,QAAQ;;;;;;;;;YAWtC,YAAY,KAAK,mBAAmB;;;;;;;;;YAUpC,cAAc,KAAK,mBAAmB;;;;;;;;;;;;YAatC,aAAa,KAAK,gBAAgB,UAAU,aAAa;;;;;;;;;YAUzD,aACV,OAAO,OACP,KAAK,mBACF,QAAQ,wBAAwB"}
|
package/dist/verify.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { VerifyFn } from "./types.js";
|
|
|
3
3
|
/**
|
|
4
4
|
* Compares two byte arrays in constant time to prevent timing attacks.
|
|
5
5
|
*/
|
|
6
|
-
declare const constantTimeEquals: (a: Uint8Array, b: Uint8Array) => boolean;
|
|
6
|
+
export declare const constantTimeEquals: (a: Uint8Array, b: Uint8Array) => boolean;
|
|
7
7
|
declare const HMAC_HASH: {
|
|
8
8
|
readonly sha1: "SHA-1";
|
|
9
9
|
readonly sha256: "SHA-256";
|
|
@@ -43,11 +43,10 @@ type HmacAlgorithm = keyof typeof HMAC_HASH;
|
|
|
43
43
|
* @throws {VerificationError}
|
|
44
44
|
* Thrown when verifier setup fails or request verification does not pass.
|
|
45
45
|
*/
|
|
46
|
-
declare const createHmacVerifier: ({ headerName, secret, algo }: {
|
|
46
|
+
export declare const createHmacVerifier: ({ headerName, secret, algo }: {
|
|
47
47
|
headerName: string;
|
|
48
48
|
secret: string;
|
|
49
49
|
algo?: HmacAlgorithm;
|
|
50
50
|
}) => VerifyFn;
|
|
51
51
|
//#endregion
|
|
52
|
-
export { constantTimeEquals, createHmacVerifier };
|
|
53
52
|
//# sourceMappingURL=verify.d.ts.map
|
package/dist/verify.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verify.d.ts","names":[],"sources":["../src/verify.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"verify.d.ts","names":[],"sources":["../src/verify.ts"],"mappings":";;;;;qBAaa,qBAAsB,GAAG,YAAY,GAAG;cAc/C;WACJ;WACA;WACA;WACA;;KAGG,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAwDrB,uBACX,YACA,QACA;EAEA;EACA;EACA,OAAO;MACL"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zap-studio/webhooks",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.1",
|
|
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": [
|
|
@@ -50,19 +50,19 @@
|
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@opentelemetry/api": "^1.9.1",
|
|
53
|
-
"@opentelemetry/context-async-hooks": "^2.
|
|
54
|
-
"@opentelemetry/core": "^2.
|
|
55
|
-
"@opentelemetry/sdk-trace-base": "^2.
|
|
53
|
+
"@opentelemetry/context-async-hooks": "^2.11.0",
|
|
54
|
+
"@opentelemetry/core": "^2.11.0",
|
|
55
|
+
"@opentelemetry/sdk-trace-base": "^2.11.0",
|
|
56
56
|
"@zap-studio/logger": "2.1.0",
|
|
57
57
|
"@zap-studio/typescript": "0.0.0",
|
|
58
|
-
"tsdown": "^0.
|
|
58
|
+
"tsdown": "^0.23.0",
|
|
59
59
|
"typescript": "^7.0.2",
|
|
60
|
-
"vitest": "^
|
|
61
|
-
"zod": "^4.
|
|
60
|
+
"vitest": "^5.0.0",
|
|
61
|
+
"zod": "^4.6.2"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
64
|
"@opentelemetry/api": "^1.9.1",
|
|
65
|
-
"@zap-studio/logger": "2.1.0"
|
|
65
|
+
"@zap-studio/logger": "^2.1.0"
|
|
66
66
|
},
|
|
67
67
|
"peerDependenciesMeta": {
|
|
68
68
|
"@zap-studio/logger": {
|