@zap-studio/webhooks 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/LICENSE +1 -1
  3. package/README.md +89 -69
  4. package/dist/{errors.d.mts → errors.d.ts} +1 -1
  5. package/dist/errors.d.ts.map +1 -0
  6. package/dist/{errors.mjs → errors.js} +1 -1
  7. package/dist/errors.js.map +1 -0
  8. package/dist/index.d.ts +6 -0
  9. package/dist/index.js +5 -0
  10. package/dist/{index.d.mts → router.d.ts} +26 -21
  11. package/dist/router.d.ts.map +1 -0
  12. package/dist/router.js +149 -0
  13. package/dist/router.js.map +1 -0
  14. package/dist/{types/index.d.mts → types.d.ts} +40 -39
  15. package/dist/types.d.ts.map +1 -0
  16. package/dist/types.js +0 -0
  17. package/dist/{utils/index.d.mts → utils.d.ts} +2 -2
  18. package/dist/utils.d.ts.map +1 -0
  19. package/dist/{utils/index.mjs → utils.js} +2 -2
  20. package/dist/utils.js.map +1 -0
  21. package/dist/{verify.d.mts → verify.d.ts} +3 -3
  22. package/dist/verify.d.ts.map +1 -0
  23. package/dist/{verify.mjs → verify.js} +9 -8
  24. package/dist/verify.js.map +1 -0
  25. package/package.json +15 -18
  26. package/dist/adapters/base.d.mts +0 -59
  27. package/dist/adapters/base.d.mts.map +0 -1
  28. package/dist/adapters/base.mjs +0 -24
  29. package/dist/adapters/base.mjs.map +0 -1
  30. package/dist/errors.d.mts.map +0 -1
  31. package/dist/errors.mjs.map +0 -1
  32. package/dist/index.d.mts.map +0 -1
  33. package/dist/index.mjs +0 -175
  34. package/dist/index.mjs.map +0 -1
  35. package/dist/types/index.d.mts.map +0 -1
  36. package/dist/types/index.mjs +0 -1
  37. package/dist/utils/index.d.mts.map +0 -1
  38. package/dist/utils/index.mjs.map +0 -1
  39. package/dist/verify.d.mts.map +0 -1
  40. package/dist/verify.mjs.map +0 -1
@@ -1,32 +1,27 @@
1
1
  import { StandardSchemaV1 } from "@zap-studio/validation";
2
- //#region src/types/index.d.ts
3
- /** Framework-agnostic request shape consumed by the webhook router. */
4
- interface NormalizedRequest {
5
- /** The headers of the request (e.g. { "Authorization": "Bearer token" }) */
6
- headers: Headers;
7
- /** The parsed JSON body of the request if applicable */
8
- json?: unknown;
9
- /** The HTTP method of the request */
10
- method: Request["method"];
11
- /** The route parameters of the request */
12
- params?: Record<string, string>;
13
- /** The path of the request you registered in the router (e.g. "payment", "subscription") */
2
+ //#region src/types.d.ts
3
+ /**
4
+ * Context shared by hooks, verifiers, and handlers for a single webhook request.
5
+ *
6
+ * The router consumes the request body exactly once, so `request.body` is
7
+ * already used by the time hooks or handlers run — read `rawBody` instead.
8
+ */
9
+ interface WebhookContext {
10
+ /** The matched route key registered on the router (e.g. "stripe") */
14
11
  path: string;
15
- /** The query parameters of the request */
16
- query?: Record<string, string | string[]>;
17
- /** The raw body of the request (for signature) */
12
+ /** The exact request body bytes (for signature verification) */
18
13
  rawBody: Uint8Array;
19
- /** The parsed text body of the request if applicable */
20
- text?: string;
14
+ /** The incoming Web API request (body already consumed by the router) */
15
+ request: Request;
21
16
  }
22
- /** Framework-agnostic response shape returned by the webhook router. */
23
- interface NormalizedResponse<TBody = unknown> {
24
- /** The body of the response */
25
- body?: TBody;
26
- /** The headers of the response */
27
- headers?: Headers;
28
- /** The HTTP status code of the response */
29
- status: number;
17
+ /**
18
+ * Handler context extending the shared webhook context with the validated payload.
19
+ *
20
+ * @template TPayload - Validated payload type for the matched route.
21
+ */
22
+ interface HandlerContext<TPayload = unknown> extends WebhookContext {
23
+ /** The validated webhook payload */
24
+ payload: TPayload;
30
25
  }
31
26
  /** Route registration options for a webhook handler. */
32
27
  interface RegisterOptions<T> {
@@ -65,12 +60,13 @@ interface RouteLike {
65
60
  * @template TRoutes - Route dictionary keyed by webhook path.
66
61
  */
67
62
  type SchemaRoutes<TRoutes extends Record<string, RouteLike>> = { [P in keyof TRoutes]: SchemaRouteOptions<TRoutes[P]["schema"]>; };
68
- /** The webhook handler function, responsible for processing incoming webhook events. */
69
- type WebhookHandler<TPayload = unknown> = (ctx: {
70
- req: NormalizedRequest;
71
- payload: TPayload;
72
- ack: (res?: Partial<NormalizedResponse>) => Promise<NormalizedResponse>;
73
- }) => Promise<NormalizedResponse | undefined> | NormalizedResponse | undefined;
63
+ /**
64
+ * The webhook handler function, responsible for processing incoming webhook events.
65
+ *
66
+ * Return a `Response` to control the reply, or `undefined` to let the router
67
+ * respond with its default `200` acknowledgement.
68
+ */
69
+ type WebhookHandler<TPayload = unknown> = (ctx: HandlerContext<TPayload>) => Promise<Response | undefined> | Response | undefined;
74
70
  /** Maps route keys to their payload-specific webhook handlers. */
75
71
  type HandlerMap<TMap extends Record<string, unknown>> = { [P in keyof TMap]: WebhookHandler<TMap[P]>; };
76
72
  /**
@@ -79,14 +75,19 @@ type HandlerMap<TMap extends Record<string, unknown>> = { [P in keyof TMap]: Web
79
75
  * @template TRoutes - Route dictionary keyed by webhook path.
80
76
  */
81
77
  type InferWebhookMapFromRoutes<TRoutes extends Record<string, RouteLike>> = { [P in keyof TRoutes]: InferSchemaOutput<TRoutes[P]["schema"]>; };
82
- /** Verification function for incoming requests */
83
- type VerifyFn = (req: NormalizedRequest) => Promise<void> | void;
78
+ /** Verification function for incoming requests. Throws to reject the request. */
79
+ type VerifyFn = (ctx: WebhookContext) => Promise<void> | void;
84
80
  /** Hook function that runs before request processing */
85
- type BeforeHook = (req: NormalizedRequest) => Promise<void> | void;
86
- /** Hook function that runs after successful request processing */
87
- type AfterHook = (req: NormalizedRequest, res: NormalizedResponse) => Promise<void> | void;
81
+ type BeforeHook = (ctx: WebhookContext) => Promise<void> | void;
82
+ /**
83
+ * Hook function that runs after successful request processing.
84
+ *
85
+ * The hook receives the outgoing response as-is; call `response.clone()`
86
+ * before reading its body to avoid consuming the stream sent to the client.
87
+ */
88
+ type AfterHook = (ctx: WebhookContext, response: Response) => Promise<void> | void;
88
89
  /** Hook function that runs when an error occurs */
89
- type ErrorHook = (error: Error, req: NormalizedRequest) => Promise<NormalizedResponse | undefined> | NormalizedResponse | undefined;
90
+ type ErrorHook = (error: Error, ctx: WebhookContext) => Promise<Response | undefined> | Response | undefined;
90
91
  //#endregion
91
- export { AfterHook, BeforeHook, ErrorHook, HandlerMap, InferSchemaOutput, InferWebhookMapFromRoutes, NormalizedRequest, NormalizedResponse, RegisterOptions, SchemaRouteOptions, SchemaRoutes, VerifyFn, WebhookHandler };
92
- //# sourceMappingURL=index.d.mts.map
92
+ export { AfterHook, BeforeHook, ErrorHook, HandlerContext, HandlerMap, InferSchemaOutput, InferWebhookMapFromRoutes, RegisterOptions, SchemaRouteOptions, SchemaRoutes, VerifyFn, WebhookContext, WebhookHandler };
93
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;;;;UAciB;;EAEf;;EAEA,SAAS;;EAET,SAAS;;;;;;;UAQM,eAAe,4BAA4B;;EAE1D,SAAS;;;UAIM,gBAAgB;;EAE/B,QAAQ,YAAY;;EAEpB,SAAS,aAAa;;EAEtB,SAAS,eAAe;;EAExB,SAAS,0BAA0B;;;;;;;KAQzB,kBAAkB,WAC5B,gBAAgB,gCAAgC,WAAW;;;;;;KAOjD,mBACV,gBAAgB,sCACd,KAAK,gBAAgB,kBAAkB;EACzC,QAAQ;;UAGA;EACR,QAAQ,YAAY;EACpB,SAAS,aAAa;EACtB,SAAS;EACT,QAAQ;;;;;;;KAQE,aAAa,gBAAgB,eAAe,iBACrD,WAAW,UAAU,mBAAmB,QAAQ;;;;;;;KASvC,eAAe,uBACzB,KAAK,eAAe,cACjB,QAAQ,wBAAwB;;KAGzB,WAAW,aAAa,8BACjC,WAAW,OAAO,eAAe,KAAK;;;;;;KAQ7B,0BACV,gBAAgB,eAAe,iBAE9B,WAAW,UAAU,kBAAkB,QAAQ;;KAItC,YAAY,KAAK,mBAAmB;;KAGpC,cAAc,KAAK,mBAAmB;;;;;;;KAQtC,aACV,KAAK,gBACL,UAAU,aACP;;KAGO,aACV,OAAO,OACP,KAAK,mBACF,QAAQ,wBAAwB"}
package/dist/types.js ADDED
File without changes
@@ -1,4 +1,4 @@
1
- //#region src/utils/index.d.ts
1
+ //#region src/utils.d.ts
2
2
  /**
3
3
  * Utility helpers for webhook internals.
4
4
  *
@@ -15,4 +15,4 @@
15
15
  declare const constantTimeEquals: (a: string, b: string) => boolean;
16
16
  //#endregion
17
17
  export { constantTimeEquals };
18
- //# sourceMappingURL=index.d.mts.map
18
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","names":[],"sources":["../src/utils.ts"],"mappings":";;;;;;;;;;;;;;cAca,qBAAsB,WAAW"}
@@ -1,4 +1,4 @@
1
- //#region src/utils/index.ts
1
+ //#region src/utils.ts
2
2
  /**
3
3
  * Utility helpers for webhook internals.
4
4
  *
@@ -21,4 +21,4 @@ const constantTimeEquals = (a, b) => {
21
21
  //#endregion
22
22
  export { constantTimeEquals };
23
23
 
24
- //# sourceMappingURL=index.mjs.map
24
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","names":[],"sources":["../src/utils.ts"],"sourcesContent":["/**\n * Utility helpers for webhook internals.\n *\n * @module @zap-studio/webhooks/utils\n */\n\n/**\n * Compares two strings in constant time to prevent timing attacks.\n *\n * @example\n * ```ts\n * const isEqual = constantTimeEquals(\"string1\", \"string2\"); // returns false\n * ```\n */\nexport const constantTimeEquals = (a: string, b: string): boolean => {\n if (a.length !== b.length) {\n return false;\n }\n\n let result = 0;\n for (let i = 0; i < a.length; i += 1) {\n // oxlint-disable-next-line no-bitwise, unicorn/prefer-code-point -- XOR is the constant-time compare trick; charCodeAt reads each char with the same cost, and inputs are plain ASCII hex.\n result |= a.charCodeAt(i) ^ b.charCodeAt(i);\n }\n\n return result === 0;\n};\n"],"mappings":";;;;;;;;;;;;;;AAcA,MAAa,sBAAsB,GAAW,MAAuB;CACnE,IAAI,EAAE,WAAW,EAAE,QACjB,OAAO;CAGT,IAAI,SAAS;CACb,KAAK,IAAI,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK,GAEjC,UAAU,EAAE,WAAW,CAAC,IAAI,EAAE,WAAW,CAAC;CAG5C,OAAO,WAAW;AACpB"}
@@ -1,4 +1,4 @@
1
- import { VerifyFn } from "./types/index.mjs";
1
+ import { VerifyFn } from "./types.js";
2
2
  //#region src/verify.d.ts
3
3
  declare const HMAC_HASH: {
4
4
  readonly sha1: "SHA-1";
@@ -11,7 +11,7 @@ type HmacAlgorithm = keyof typeof HMAC_HASH;
11
11
  * Creates a webhook verifier that validates an HMAC signature from a request header.
12
12
  *
13
13
  * The verifier imports the provided string secret once, computes an HMAC from
14
- * `req.rawBody`, normalizes the incoming header value, and compares both
14
+ * `ctx.rawBody`, normalizes the incoming header value, and compares both
15
15
  * signatures in constant time.
16
16
  *
17
17
  * Header values like `sha256=<hex>` are supported so common provider formats
@@ -46,4 +46,4 @@ declare const createHmacVerifier: ({ headerName, secret, algo }: {
46
46
  }) => VerifyFn;
47
47
  //#endregion
48
48
  export { createHmacVerifier };
49
- //# sourceMappingURL=verify.d.mts.map
49
+ //# sourceMappingURL=verify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.d.ts","names":[],"sources":["../src/verify.ts"],"mappings":";;cAUM;WACJ;WACA;WACA;WACA;;KAGG,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA2CrB,uBACX,YACA,QACA;EAEA;EACA;EACA,OAAO;MACL"}
@@ -1,5 +1,5 @@
1
- import { VerificationError } from "./errors.mjs";
2
- import { constantTimeEquals } from "./utils/index.mjs";
1
+ import { VerificationError } from "./errors.js";
2
+ import { constantTimeEquals } from "./utils.js";
3
3
  //#region src/verify.ts
4
4
  /**
5
5
  * Signature verification helpers for webhook requests.
@@ -18,7 +18,7 @@ const normalizeSignature = (signature) => signature.replace(/^[a-z0-9-]+=/iu, ""
18
18
  * Creates a webhook verifier that validates an HMAC signature from a request header.
19
19
  *
20
20
  * The verifier imports the provided string secret once, computes an HMAC from
21
- * `req.rawBody`, normalizes the incoming header value, and compares both
21
+ * `ctx.rawBody`, normalizes the incoming header value, and compares both
22
22
  * signatures in constant time.
23
23
  *
24
24
  * Header values like `sha256=<hex>` are supported so common provider formats
@@ -55,15 +55,16 @@ const createHmacVerifier = ({ headerName, secret, algo = "sha256" }) => {
55
55
  hash,
56
56
  name: "HMAC"
57
57
  }, false, ["sign"]);
58
- return async (req) => {
59
- const actual = req.headers.get(headerName);
58
+ return async (ctx) => {
59
+ const actual = ctx.request.headers.get(headerName);
60
60
  if (actual === null || actual.length === 0) throw new VerificationError(`Missing signature header: ${headerName}`);
61
61
  const key = await keyPromise;
62
- const signature = await subtle.sign("HMAC", key, new Uint8Array(req.rawBody));
63
- if (!constantTimeEquals(toHex(new Uint8Array(signature)), normalizeSignature(actual))) throw new VerificationError(`Invalid signature for header: ${headerName}`);
62
+ const signature = await subtle.sign("HMAC", key, new Uint8Array(ctx.rawBody));
63
+ const expected = toHex(new Uint8Array(signature));
64
+ if (!constantTimeEquals(expected, normalizeSignature(actual))) throw new VerificationError(`Invalid signature for header: ${headerName}`);
64
65
  };
65
66
  };
66
67
  //#endregion
67
68
  export { createHmacVerifier };
68
69
 
69
- //# sourceMappingURL=verify.mjs.map
70
+ //# sourceMappingURL=verify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.js","names":[],"sources":["../src/verify.ts"],"sourcesContent":["/**\n * Signature verification helpers for webhook requests.\n *\n * @module @zap-studio/webhooks/verify\n */\n\nimport { VerificationError } from \"./errors.js\";\nimport type { VerifyFn } from \"./types.js\";\nimport { constantTimeEquals } from \"./utils.js\";\n\nconst HMAC_HASH = {\n sha1: \"SHA-1\",\n sha256: \"SHA-256\",\n sha384: \"SHA-384\",\n sha512: \"SHA-512\",\n} as const;\n\ntype HmacAlgorithm = keyof typeof HMAC_HASH;\n\nconst toHex = (bytes: Uint8Array): string =>\n Array.from(bytes, (byte) => byte.toString(16).padStart(2, \"0\")).join(\"\");\n\nconst normalizeSignature = (signature: string): string =>\n signature\n .replace(/^[a-z0-9-]+=/iu, \"\")\n .trim()\n .toLowerCase();\n\n/**\n * Creates a webhook verifier that validates an HMAC signature from a request header.\n *\n * The verifier imports the provided string secret once, computes an HMAC from\n * `ctx.rawBody`, normalizes the incoming header value, and compares both\n * signatures in constant time.\n *\n * Header values like `sha256=<hex>` are supported so common provider formats\n * such as GitHub work without extra parsing.\n *\n * @example\n * ```ts\n * import { createWebhookRouter } from \"@zap-studio/webhooks\";\n * import { createHmacVerifier } from \"@zap-studio/webhooks/verify\";\n *\n * const router = createWebhookRouter({\n * verify: createHmacVerifier({\n * headerName: \"x-hub-signature-256\",\n * secret: process.env.GITHUB_WEBHOOK_SECRET!,\n * }),\n * });\n * ```\n *\n * @param options - Verifier configuration.\n * @param options.headerName - Header containing the provider signature.\n * @param options.secret - Shared HMAC secret as a string.\n * @param options.algo - HMAC hash algorithm. Defaults to `\"sha256\"`.\n * @returns A router-compatible request verifier.\n *\n * @throws {VerificationError}\n * Thrown when verifier setup fails or request verification does not pass.\n */\nexport const createHmacVerifier = ({\n headerName,\n secret,\n algo = \"sha256\",\n}: {\n headerName: string;\n secret: string;\n algo?: HmacAlgorithm;\n}): VerifyFn => {\n if (globalThis.crypto?.subtle === undefined) {\n throw new VerificationError(\n \"Web Crypto API is unavailable in this runtime\"\n );\n }\n\n const { subtle } = globalThis.crypto;\n\n const hash = HMAC_HASH[algo];\n if (!hash) {\n throw new VerificationError(`Unsupported HMAC algorithm: ${algo}`);\n }\n\n const keyPromise = subtle.importKey(\n \"raw\",\n new TextEncoder().encode(secret),\n { hash, name: \"HMAC\" },\n false,\n [\"sign\"]\n );\n\n return async (ctx) => {\n const actual = ctx.request.headers.get(headerName);\n if (actual === null || actual.length === 0) {\n throw new VerificationError(`Missing signature header: ${headerName}`);\n }\n\n const key = await keyPromise;\n const signature = await subtle.sign(\n \"HMAC\",\n key,\n new Uint8Array(ctx.rawBody)\n );\n const expected = toHex(new Uint8Array(signature));\n\n if (!constantTimeEquals(expected, normalizeSignature(actual))) {\n throw new VerificationError(\n `Invalid signature for header: ${headerName}`\n );\n }\n };\n};\n"],"mappings":";;;;;;;;AAUA,MAAM,YAAY;CAChB,MAAM;CACN,QAAQ;CACR,QAAQ;CACR,QAAQ;AACV;AAIA,MAAM,SAAS,UACb,MAAM,KAAK,QAAQ,SAAS,KAAK,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE;AAEzE,MAAM,sBAAsB,cAC1B,UACG,QAAQ,kBAAkB,EAAE,CAAC,CAC7B,KAAK,CAAC,CACN,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCjB,MAAa,sBAAsB,EACjC,YACA,QACA,OAAO,eAKO;CACd,IAAI,WAAW,QAAQ,WAAW,KAAA,GAChC,MAAM,IAAI,kBACR,+CACF;CAGF,MAAM,EAAE,WAAW,WAAW;CAE9B,MAAM,OAAO,UAAU;CACvB,IAAI,CAAC,MACH,MAAM,IAAI,kBAAkB,+BAA+B,MAAM;CAGnE,MAAM,aAAa,OAAO,UACxB,OACA,IAAI,YAAY,CAAC,CAAC,OAAO,MAAM,GAC/B;EAAE;EAAM,MAAM;CAAO,GACrB,OACA,CAAC,MAAM,CACT;CAEA,OAAO,OAAO,QAAQ;EACpB,MAAM,SAAS,IAAI,QAAQ,QAAQ,IAAI,UAAU;EACjD,IAAI,WAAW,QAAQ,OAAO,WAAW,GACvC,MAAM,IAAI,kBAAkB,6BAA6B,YAAY;EAGvE,MAAM,MAAM,MAAM;EAClB,MAAM,YAAY,MAAM,OAAO,KAC7B,QACA,KACA,IAAI,WAAW,IAAI,OAAO,CAC5B;EACA,MAAM,WAAW,MAAM,IAAI,WAAW,SAAS,CAAC;EAEhD,IAAI,CAAC,mBAAmB,UAAU,mBAAmB,MAAM,CAAC,GAC1D,MAAM,IAAI,kBACR,iCAAiC,YACnC;CAEJ;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zap-studio/webhooks",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "private": false,
5
5
  "description": "A lightweight, type-safe webhook router with Standard Schema validation, signature verification, and lifecycle hooks.",
6
6
  "keywords": [
@@ -33,33 +33,30 @@
33
33
  ],
34
34
  "type": "module",
35
35
  "sideEffects": false,
36
- "types": "./dist/index.d.mts",
36
+ "types": "./dist/index.d.ts",
37
37
  "exports": {
38
- ".": "./dist/index.mjs",
39
- "./adapters/base": "./dist/adapters/base.mjs",
40
- "./errors": "./dist/errors.mjs",
41
- "./types": "./dist/types/index.mjs",
42
- "./utils": "./dist/utils/index.mjs",
43
- "./verify": "./dist/verify.mjs",
38
+ ".": "./dist/index.js",
39
+ "./errors": "./dist/errors.js",
40
+ "./router": "./dist/router.js",
41
+ "./types": "./dist/types.js",
42
+ "./utils": "./dist/utils.js",
43
+ "./verify": "./dist/verify.js",
44
44
  "./package.json": "./package.json"
45
45
  },
46
46
  "publishConfig": {
47
47
  "access": "public"
48
48
  },
49
49
  "dependencies": {
50
- "@zap-studio/validation": "0.3.5"
50
+ "@zap-studio/validation": "workspace:*"
51
51
  },
52
52
  "devDependencies": {
53
- "tsdown": "^0.22.4",
54
- "typescript": "^7.0.2",
55
- "vitest": "^4.1.10",
56
- "zod": "^4.4.3",
57
- "@zap-studio/typescript": "0.0.0"
53
+ "@zap-studio/typescript": "workspace:*",
54
+ "tsdown": "catalog:",
55
+ "typescript": "catalog:",
56
+ "vitest": "catalog:",
57
+ "zod": "catalog:"
58
58
  },
59
59
  "engines": {
60
60
  "node": ">=18.0.0"
61
- },
62
- "scripts": {
63
- "build": "tsdown --config ./tsdown.config.ts"
64
61
  }
65
- }
62
+ }
@@ -1,59 +0,0 @@
1
- import { NormalizedRequest, NormalizedResponse } from "../types/index.mjs";
2
- //#region src/adapters/base.d.ts
3
- interface RouterHandler {
4
- handle: (req: NormalizedRequest) => Promise<NormalizedResponse>;
5
- }
6
- /**
7
- * Minimal framework adapter contract.
8
- *
9
- * Implement this when integrating the webhook router with an HTTP framework.
10
- *
11
- * @template TReq - Framework-specific request type (e.g. `express.Request`).
12
- * @template TRes - Framework-specific response type (e.g. `express.Response`).
13
- */
14
- interface Adapter<TReq = unknown, TRes = unknown> {
15
- /**
16
- * Creates a framework handler that:
17
- * 1. normalizes the incoming framework request
18
- * 2. executes the webhook router
19
- * 3. writes the normalized response back to the framework response
20
- */
21
- handleWebhook: (router: RouterHandler) => (req: TReq, res: TRes) => Promise<void>;
22
- /**
23
- * Maps a normalized router response to the framework response object.
24
- *
25
- * @param frameworkRes - Framework-specific response object (e.g. `res`)
26
- * @param res - Normalized response returned by the webhook router
27
- */
28
- toFrameworkResponse: (frameworkRes: TRes, res: NormalizedResponse) => Promise<TRes>;
29
- /**
30
- * Maps a framework request into the normalized request contract.
31
- *
32
- * The returned object must include `rawBody` to support signature verification.
33
- *
34
- * @param req - Framework-specific request object
35
- */
36
- toNormalizedRequest: (req: TReq) => Promise<NormalizedRequest>;
37
- }
38
- /**
39
- * Base adapter helper.
40
- *
41
- * Extend this class in consumers to keep framework integration boilerplate
42
- * in one place while relying on the package router contract.
43
- */
44
- declare abstract class BaseAdapter<TReq = unknown, TRes = unknown> implements Adapter<TReq, TRes> {
45
- /** @inheritdoc */
46
- abstract toNormalizedRequest: (req: TReq) => Promise<NormalizedRequest>;
47
- /** @inheritdoc */
48
- abstract toFrameworkResponse: (frameworkRes: TRes, res: NormalizedResponse) => Promise<TRes>;
49
- /**
50
- * Shared adapter pipeline implementation.
51
- *
52
- * Most consumers only need to implement request/response mapping methods and
53
- * can reuse this default orchestration.
54
- */
55
- handleWebhook: (router: RouterHandler) => ((req: TReq, res: TRes) => Promise<void>);
56
- }
57
- //#endregion
58
- export { Adapter, BaseAdapter };
59
- //# sourceMappingURL=base.d.mts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"base.d.mts","names":[],"sources":["../../src/adapters/base.ts"],"mappings":";;UAQU;EACR,SAAS,KAAK,sBAAsB,QAAQ;;;;;;;;;;UAW7B,QAAQ,gBAAgB;;;;;;;EAOvC,gBACE,QAAQ,mBACJ,KAAK,MAAM,KAAK,SAAS;;;;;;;EAQ/B,sBACE,cAAc,MACd,KAAK,uBACF,QAAQ;;;;;;;;EASb,sBAAsB,KAAK,SAAS,QAAQ;;;;;;;;uBASxB,YACpB,gBACA,2BACW,QAAQ,MAAM;;WAEhB,sBAAsB,KAAK,SAAS,QAAQ;;WAE5C,sBACP,cAAc,MACd,KAAK,uBACF,QAAQ;;;;;;;EAQb,gBACG,QAAQ,oBAAkB,KAAK,MAAM,KAAK,SAAS"}
@@ -1,24 +0,0 @@
1
- //#region src/adapters/base.ts
2
- /**
3
- * Base adapter helper.
4
- *
5
- * Extend this class in consumers to keep framework integration boilerplate
6
- * in one place while relying on the package router contract.
7
- */
8
- var BaseAdapter = class {
9
- /**
10
- * Shared adapter pipeline implementation.
11
- *
12
- * Most consumers only need to implement request/response mapping methods and
13
- * can reuse this default orchestration.
14
- */
15
- handleWebhook = (router) => async (req, res) => {
16
- const normalizedReq = await this.toNormalizedRequest(req);
17
- const normalizedRes = await router.handle(normalizedReq);
18
- await this.toFrameworkResponse(res, normalizedRes);
19
- };
20
- };
21
- //#endregion
22
- export { BaseAdapter };
23
-
24
- //# sourceMappingURL=base.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"base.mjs","names":[],"sources":["../../src/adapters/base.ts"],"sourcesContent":["/**\n * Framework adapter contracts for webhook router integration.\n *\n * @module @zap-studio/webhooks/adapters/base\n */\n\nimport type { NormalizedRequest, NormalizedResponse } from \"../types/index.js\";\n\ninterface RouterHandler {\n handle: (req: NormalizedRequest) => Promise<NormalizedResponse>;\n}\n\n/**\n * Minimal framework adapter contract.\n *\n * Implement this when integrating the webhook router with an HTTP framework.\n *\n * @template TReq - Framework-specific request type (e.g. `express.Request`).\n * @template TRes - Framework-specific response type (e.g. `express.Response`).\n */\nexport interface Adapter<TReq = unknown, TRes = unknown> {\n /**\n * Creates a framework handler that:\n * 1. normalizes the incoming framework request\n * 2. executes the webhook router\n * 3. writes the normalized response back to the framework response\n */\n handleWebhook: (\n router: RouterHandler\n ) => (req: TReq, res: TRes) => Promise<void>;\n\n /**\n * Maps a normalized router response to the framework response object.\n *\n * @param frameworkRes - Framework-specific response object (e.g. `res`)\n * @param res - Normalized response returned by the webhook router\n */\n toFrameworkResponse: (\n frameworkRes: TRes,\n res: NormalizedResponse\n ) => Promise<TRes>;\n\n /**\n * Maps a framework request into the normalized request contract.\n *\n * The returned object must include `rawBody` to support signature verification.\n *\n * @param req - Framework-specific request object\n */\n toNormalizedRequest: (req: TReq) => Promise<NormalizedRequest>;\n}\n\n/**\n * Base adapter helper.\n *\n * Extend this class in consumers to keep framework integration boilerplate\n * in one place while relying on the package router contract.\n */\nexport abstract class BaseAdapter<\n TReq = unknown,\n TRes = unknown,\n> implements Adapter<TReq, TRes> {\n /** @inheritdoc */\n abstract toNormalizedRequest: (req: TReq) => Promise<NormalizedRequest>;\n /** @inheritdoc */\n abstract toFrameworkResponse: (\n frameworkRes: TRes,\n res: NormalizedResponse\n ) => Promise<TRes>;\n\n /**\n * Shared adapter pipeline implementation.\n *\n * Most consumers only need to implement request/response mapping methods and\n * can reuse this default orchestration.\n */\n handleWebhook =\n (router: RouterHandler): ((req: TReq, res: TRes) => Promise<void>) =>\n async (req, res) => {\n const normalizedReq = await this.toNormalizedRequest(req);\n const normalizedRes = await router.handle(normalizedReq);\n await this.toFrameworkResponse(res, normalizedRes);\n };\n}\n"],"mappings":";;;;;;;AA0DA,IAAsB,cAAtB,MAGiC;;;;;;;CAe/B,iBACG,WACD,OAAO,KAAK,QAAQ;EAClB,MAAM,gBAAgB,MAAM,KAAK,oBAAoB,GAAG;EACxD,MAAM,gBAAgB,MAAM,OAAO,OAAO,aAAa;EACvD,MAAM,KAAK,oBAAoB,KAAK,aAAa;CACnD;AACJ"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"errors.d.mts","names":[],"sources":["../src/errors.ts"],"mappings":";;;;;;;;;;;;cAYa,0BAA0B;;;;;;EAMrC,YAAY"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"errors.mjs","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/**\n * Error primitives for webhook verification failures.\n *\n * @module @zap-studio/webhooks/errors\n */\n\n/**\n * Error thrown when webhook request verification fails.\n *\n * This error is used by verifier helpers such as `createHmacVerifier` so\n * callers can distinguish verification failures from other webhook errors.\n */\nexport class VerificationError extends Error {\n /**\n * Creates a verification error with a human-readable message.\n *\n * @param message - Error message describing the verification failure.\n */\n constructor(message: string) {\n super(message);\n this.name = \"VerificationError\";\n }\n}\n"],"mappings":";;;;;;;;;;;;AAYA,IAAa,oBAAb,cAAuC,MAAM;;;;;;CAM3C,YAAY,SAAiB;EAC3B,MAAM,OAAO;EACb,KAAK,OAAO;CACd;AACF"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":";;;UAmCiB;;EAEf,QAAQ,YAAY;;EAEpB,SAAS,aAAa;;EAEtB,UAAU;;EAEV;;EAEA,UAAU,KAAK,sBAAsB;;;;;;;cAgB1B,cAAc;mBACR;mBACA;mBAGA;mBACA;mBACA;mBACA;;;;;;EAOjB,YAAY,OAAM;;;;;;;;;;EAiBlB,SACE,qBACA,gBAAgB,oCAEhB,MAAM,MACN,kBAAkB,mBAAmB,WACpC,cAAc,OAAO,OAAO,MAAM,kBAAkB;EACvD,SAAS,qBAAqB,UAC5B,MAAM,MACN,kBAAkB,gBAAgB,YACjC,cAAc,OAAO,OAAO,MAAM;EACrC,SAAS,qBACP,MAAM,MACN,kBAAkB,iBACjB,cAAc,OAAO,OAAO;;;;;;;EAmB/B,OAAa,KAAK,oBAAoB,QAAQ;UA6CtC;iBA2Ba;UAUP;iBAMC;iBA0BM;iBAWN;iBAYA;iBASM;iBA+BA;iBA0BA;UAYP;UASA;;;;;;;;cA4BH,sBACX,OAAO,yBACN"}
package/dist/index.mjs DELETED
@@ -1,175 +0,0 @@
1
- import { standardValidate } from "@zap-studio/validation";
2
- //#region src/index.ts
3
- const toArray = (value) => {
4
- if (value === void 0) return [];
5
- return Array.isArray(value) ? value : [value];
6
- };
7
- /**
8
- * Main webhook router class.
9
- *
10
- * Register routes with typed schemas and call `handle` with a normalized request.
11
- */
12
- var WebhookRouter = class WebhookRouter {
13
- handlers = {};
14
- verify;
15
- globalBeforeHooks = [];
16
- globalAfterHooks = [];
17
- globalErrorHook;
18
- prefix;
19
- /**
20
- * Creates a webhook router with optional global hooks and verification behavior.
21
- *
22
- * @param opts - Router-level options.
23
- */
24
- constructor(opts = {}) {
25
- this.prefix = opts.prefix ?? "/webhooks/";
26
- this.verify = opts.verify;
27
- this.globalBeforeHooks = toArray(opts.before);
28
- this.globalAfterHooks = toArray(opts.after);
29
- this.globalErrorHook = opts.onError;
30
- }
31
- register(path, handlerOrOptions) {
32
- this.handlers[path] = typeof handlerOrOptions === "function" ? { handler: handlerOrOptions } : WebhookRouter.createHandlerEntry(handlerOrOptions);
33
- return this;
34
- }
35
- /**
36
- * Handles a normalized incoming webhook request.
37
- *
38
- * @param req - Normalized request object.
39
- * @returns Normalized response for the adapter/framework layer.
40
- */
41
- async handle(req) {
42
- try {
43
- const normalizedPath = this.normalizePath(req);
44
- if (normalizedPath === null) return {
45
- body: { error: "not found" },
46
- status: 404
47
- };
48
- const handlerEntry = this.handlers[normalizedPath];
49
- if (!handlerEntry) return {
50
- body: { error: "not found" },
51
- status: 404
52
- };
53
- await this.runGlobalBeforeHooks(req);
54
- await WebhookRouter.runRouteBeforeHooks(req, handlerEntry.before);
55
- if (this.verify) await this.verify(req);
56
- const parsedJson = WebhookRouter.parseRequestBody(req);
57
- const validationResult = await WebhookRouter.validatePayload(parsedJson, handlerEntry.schema);
58
- if (WebhookRouter.isErrorResponse(validationResult)) return validationResult;
59
- const response = await WebhookRouter.executeHandler(handlerEntry.handler, req, validationResult);
60
- await WebhookRouter.runRouteAfterHooks(req, response, handlerEntry.after);
61
- await this.runGlobalAfterHooks(req, response);
62
- return response;
63
- } catch (error) {
64
- return await this.handleError(error, req);
65
- }
66
- }
67
- normalizePath(req) {
68
- let pathname = req.path;
69
- try {
70
- const url = new URL(req.path);
71
- ({pathname} = url);
72
- } catch {}
73
- pathname = pathname.startsWith(this.prefix) ? pathname.slice(this.prefix.length - 1) : "";
74
- if (pathname.length === 0) return null;
75
- req.path = pathname;
76
- return pathname.startsWith("/") ? pathname.slice(1) : pathname;
77
- }
78
- static async runHooks(hooks, run) {
79
- for (const hook of hooks) await run(hook);
80
- }
81
- async runGlobalBeforeHooks(req) {
82
- await WebhookRouter.runHooks(this.globalBeforeHooks, async (hook) => {
83
- await hook(req);
84
- });
85
- }
86
- static createHandlerEntry(options) {
87
- const entry = { handler: options.handler };
88
- if (options.schema !== void 0) entry.schema = options.schema;
89
- if (options.before !== void 0) entry.before = Array.isArray(options.before) ? options.before : [options.before];
90
- if (options.after !== void 0) entry.after = Array.isArray(options.after) ? options.after : [options.after];
91
- return entry;
92
- }
93
- static async runRouteBeforeHooks(req, before) {
94
- if (before) await WebhookRouter.runHooks(before, async (hook) => {
95
- await hook(req);
96
- });
97
- }
98
- static parseRequestBody(req) {
99
- try {
100
- const parsed = JSON.parse(new TextDecoder().decode(req.rawBody));
101
- req.json = parsed;
102
- return parsed;
103
- } catch {
104
- return;
105
- }
106
- }
107
- static isErrorResponse(value) {
108
- return typeof value === "object" && value !== null && "status" in value && typeof value.status === "number";
109
- }
110
- static async validatePayload(parsedJson, schema) {
111
- if (!schema) return parsedJson;
112
- const result = await standardValidate(schema, parsedJson, { throwOnError: false });
113
- if (result.issues) return {
114
- body: {
115
- error: "validation failed",
116
- issues: result.issues.map((issue) => ({
117
- message: issue.message,
118
- path: issue.path?.map((p) => typeof p === "object" && "key" in p ? String(p.key) : String(p))
119
- }))
120
- },
121
- status: 400
122
- };
123
- return result.value;
124
- }
125
- static async executeHandler(handler, req, validatedPayload) {
126
- return await handler({
127
- ack: async (r) => {
128
- await Promise.resolve();
129
- const response = {
130
- body: r?.body ?? "ok",
131
- status: r?.status ?? 200
132
- };
133
- if (r?.headers !== void 0) response.headers = r.headers;
134
- return response;
135
- },
136
- payload: validatedPayload,
137
- req
138
- }) ?? {
139
- body: "ok",
140
- status: 200
141
- };
142
- }
143
- static async runRouteAfterHooks(req, response, after) {
144
- if (after) await WebhookRouter.runHooks(after, async (hook) => {
145
- await hook(req, response);
146
- });
147
- }
148
- async runGlobalAfterHooks(req, response) {
149
- await WebhookRouter.runHooks(this.globalAfterHooks, async (hook) => {
150
- await hook(req, response);
151
- });
152
- }
153
- async handleError(error, req) {
154
- if (this.globalErrorHook) {
155
- const normalizedError = error instanceof Error ? error : /* @__PURE__ */ new Error("Internal server error");
156
- const errorResponse = await this.globalErrorHook(normalizedError, req);
157
- if (errorResponse) return errorResponse;
158
- }
159
- return {
160
- body: { error: error instanceof Error ? error.message : "Internal server error" },
161
- status: 500
162
- };
163
- }
164
- };
165
- /**
166
- * Factory helper for creating a webhook router instance.
167
- *
168
- * @param opts - Optional global router options.
169
- * @returns A new webhook router.
170
- */
171
- const createWebhookRouter = (opts) => new WebhookRouter(opts);
172
- //#endregion
173
- export { WebhookRouter, createWebhookRouter };
174
-
175
- //# sourceMappingURL=index.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * Schema-first webhook router primitives.\n *\n * @module @zap-studio/webhooks\n */\n\nimport type { StandardSchemaV1 } from \"@zap-studio/validation\";\nimport { standardValidate } from \"@zap-studio/validation\";\n\nimport type {\n AfterHook,\n BeforeHook,\n ErrorHook,\n InferSchemaOutput,\n NormalizedRequest,\n NormalizedResponse,\n RegisterOptions,\n SchemaRouteOptions,\n WebhookHandler,\n} from \"./types/index.js\";\n\n/**\n * Schema-first webhook router with path dispatching, validation, and optional verification.\n *\n * @template TMap - Internal route payload map built incrementally via `register`.\n */\ninterface HandlerEntry<TPayload = unknown> {\n after?: AfterHook[];\n before?: BeforeHook[];\n handler: WebhookHandler<TPayload>;\n schema?: StandardSchemaV1<unknown, TPayload>;\n}\n\ntype HandlerStore = Record<string, HandlerEntry>;\n\nexport interface WebhookRouterOptions {\n /** Global hooks executed after successful route handler completion. */\n after?: AfterHook | AfterHook[];\n /** Global hooks executed before route-level hooks and verification. */\n before?: BeforeHook | BeforeHook[];\n /** Global error hook used to override the default `500` response. */\n onError?: ErrorHook;\n /** Required path prefix for all webhook routes. Defaults to `\"/webhooks/\"`. */\n prefix?: string;\n /** Optional request verification function (for signature checks, auth, etc.). */\n verify?: (req: NormalizedRequest) => Promise<void> | void;\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\n/**\n * Main webhook router class.\n *\n * Register routes with typed schemas and call `handle` with a normalized request.\n */\nexport class WebhookRouter<TMap = unknown> {\n private readonly handlers: HandlerStore = {};\n private readonly verify:\n | ((req: NormalizedRequest) => Promise<void> | void)\n | undefined;\n private readonly globalBeforeHooks: BeforeHook[] = [];\n private readonly globalAfterHooks: AfterHook[] = [];\n private readonly globalErrorHook: ErrorHook | undefined;\n private readonly prefix: string;\n\n /**\n * Creates a webhook router with optional global hooks and verification behavior.\n *\n * @param opts - Router-level options.\n */\n constructor(opts: WebhookRouterOptions = {}) {\n this.prefix = opts.prefix ?? \"/webhooks/\";\n this.verify = opts.verify;\n this.globalBeforeHooks = toArray(opts.before);\n this.globalAfterHooks = toArray(opts.after);\n this.globalErrorHook = opts.onError;\n }\n\n /**\n * Register a webhook handler for a specific path.\n *\n * When a schema is provided, `payload` is inferred from the schema output type.\n *\n * @param path - Route path relative to configured prefix.\n * @param handlerOrOptions - Handler function or schema-based registration options.\n * @returns The same router instance with an updated internal route type map.\n */\n register<\n Path extends string,\n TSchema extends StandardSchemaV1<unknown, unknown>,\n >(\n path: Path,\n handlerOrOptions: SchemaRouteOptions<TSchema>\n ): WebhookRouter<TMap & Record<Path, InferSchemaOutput<TSchema>>>;\n register<Path extends string, TPayload>(\n path: Path,\n handlerOrOptions: RegisterOptions<TPayload>\n ): WebhookRouter<TMap & Record<Path, TPayload>>;\n register<Path extends string>(\n path: Path,\n handlerOrOptions: WebhookHandler\n ): WebhookRouter<TMap & Record<Path, unknown>>;\n register(\n path: string,\n handlerOrOptions: WebhookHandler | RegisterOptions<unknown>\n ): this {\n this.handlers[path] =\n typeof handlerOrOptions === \"function\"\n ? { handler: handlerOrOptions }\n : WebhookRouter.createHandlerEntry(handlerOrOptions);\n\n return this;\n }\n\n /**\n * Handles a normalized incoming webhook request.\n *\n * @param req - Normalized request object.\n * @returns Normalized response for the adapter/framework layer.\n */\n async handle(req: NormalizedRequest): Promise<NormalizedResponse> {\n try {\n const normalizedPath = this.normalizePath(req);\n\n if (normalizedPath === null) {\n return { body: { error: \"not found\" }, status: 404 };\n }\n\n const handlerEntry = this.handlers[normalizedPath];\n if (!handlerEntry) {\n return { body: { error: \"not found\" }, status: 404 };\n }\n\n await this.runGlobalBeforeHooks(req);\n await WebhookRouter.runRouteBeforeHooks(req, handlerEntry.before);\n\n if (this.verify) {\n await this.verify(req);\n }\n\n const parsedJson = WebhookRouter.parseRequestBody(req);\n const validationResult = await WebhookRouter.validatePayload(\n parsedJson,\n handlerEntry.schema\n );\n\n if (WebhookRouter.isErrorResponse(validationResult)) {\n return validationResult;\n }\n\n const response = await WebhookRouter.executeHandler(\n handlerEntry.handler,\n req,\n validationResult\n );\n\n await WebhookRouter.runRouteAfterHooks(req, response, handlerEntry.after);\n await this.runGlobalAfterHooks(req, response);\n\n return response;\n } catch (error) {\n return await this.handleError(error, req);\n }\n }\n\n private normalizePath(req: NormalizedRequest): string | null {\n let pathname = req.path;\n try {\n // Try to parse as URL (e.g. handles full URLs like https://example.com/webhooks/path -> /webhooks/path)\n const url = new URL(req.path);\n ({ pathname } = url);\n } catch {\n // Not a full URL, use the path as-is\n }\n\n // Require prefix (e.g. /webhooks/path -> /path)\n pathname = pathname.startsWith(this.prefix)\n ? pathname.slice(this.prefix.length - 1)\n : \"\";\n if (pathname.length === 0) {\n return null;\n }\n req.path = pathname;\n\n // Normalize path by removing leading slash for handler matching (e.g. /path -> path)\n const normalizedPath = pathname.startsWith(\"/\")\n ? pathname.slice(1)\n : pathname;\n\n return normalizedPath;\n }\n\n private static async runHooks<T>(\n hooks: T[],\n run: (hook: T) => void | Promise<void>\n ): Promise<void> {\n for (const hook of hooks) {\n // oxlint-disable-next-line no-await-in-loop -- hooks run sequentially; order + short-circuit matter.\n await run(hook);\n }\n }\n\n private async runGlobalBeforeHooks(req: NormalizedRequest): Promise<void> {\n await WebhookRouter.runHooks(this.globalBeforeHooks, async (hook) => {\n await hook(req);\n });\n }\n\n private static createHandlerEntry(\n options: RegisterOptions<unknown>\n ): HandlerEntry {\n const entry: HandlerEntry = {\n handler: options.handler,\n };\n\n if (options.schema !== undefined) {\n entry.schema = options.schema;\n }\n\n if (options.before !== undefined) {\n entry.before = Array.isArray(options.before)\n ? options.before\n : [options.before];\n }\n\n if (options.after !== undefined) {\n entry.after = Array.isArray(options.after)\n ? options.after\n : [options.after];\n }\n\n return entry;\n }\n\n private static async runRouteBeforeHooks(\n req: NormalizedRequest,\n before?: BeforeHook[]\n ): Promise<void> {\n if (before) {\n await WebhookRouter.runHooks(before, async (hook) => {\n await hook(req);\n });\n }\n }\n\n private static parseRequestBody(req: NormalizedRequest): unknown {\n try {\n const parsed = JSON.parse(\n new TextDecoder().decode(req.rawBody)\n ) as unknown;\n req.json = parsed;\n return parsed;\n } catch {\n return undefined;\n }\n }\n\n private static isErrorResponse(value: unknown): value is NormalizedResponse {\n return (\n typeof value === \"object\" &&\n value !== null &&\n \"status\" in value &&\n typeof value.status === \"number\"\n );\n }\n\n private static async validatePayload<TPayload>(\n parsedJson: unknown,\n schema?: StandardSchemaV1<unknown, TPayload>\n ): Promise<TPayload | NormalizedResponse> {\n if (!schema) {\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- Without a schema, caller-declared payload type is the route contract.\n return parsedJson as TPayload;\n }\n\n const result = await standardValidate(schema, parsedJson, {\n throwOnError: false,\n });\n\n if (result.issues) {\n return {\n body: {\n error: \"validation failed\",\n issues: result.issues.map((issue) => ({\n message: issue.message,\n path: issue.path?.map((p) =>\n typeof p === \"object\" && \"key\" in p ? String(p.key) : String(p)\n ),\n })),\n },\n status: 400,\n };\n }\n\n return result.value;\n }\n\n private static async executeHandler<TPayload = unknown>(\n handler: WebhookHandler<TPayload>,\n req: NormalizedRequest,\n validatedPayload: TPayload\n ): Promise<NormalizedResponse> {\n const responded = await handler({\n ack: async (r?: Partial<NormalizedResponse>) => {\n await Promise.resolve();\n const response: NormalizedResponse = {\n body: r?.body ?? \"ok\",\n status: r?.status ?? 200,\n };\n\n if (r?.headers !== undefined) {\n response.headers = r.headers;\n }\n\n return response;\n },\n payload: validatedPayload,\n req,\n });\n\n return responded ?? { body: \"ok\", status: 200 };\n }\n\n private static async runRouteAfterHooks(\n req: NormalizedRequest,\n response: NormalizedResponse,\n after?: AfterHook[]\n ): Promise<void> {\n if (after) {\n await WebhookRouter.runHooks(after, async (hook) => {\n await hook(req, response);\n });\n }\n }\n\n private async runGlobalAfterHooks(\n req: NormalizedRequest,\n response: NormalizedResponse\n ): Promise<void> {\n await WebhookRouter.runHooks(this.globalAfterHooks, async (hook) => {\n await hook(req, response);\n });\n }\n\n private async handleError(\n error: unknown,\n req: NormalizedRequest\n ): Promise<NormalizedResponse> {\n if (this.globalErrorHook) {\n const normalizedError =\n error instanceof Error ? error : new Error(\"Internal server error\");\n const errorResponse = await this.globalErrorHook(normalizedError, req);\n if (errorResponse) {\n return errorResponse;\n }\n }\n\n return {\n body: {\n error: error instanceof Error ? error.message : \"Internal server error\",\n },\n status: 500,\n };\n }\n}\n\n/**\n * Factory helper for creating a webhook router instance.\n *\n * @param opts - Optional global router options.\n * @returns A new webhook router.\n */\nexport const createWebhookRouter = (\n opts?: WebhookRouterOptions\n): WebhookRouter => new WebhookRouter(opts);\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/types/index.ts"],"mappings":";;;UASiB;;EAEf,SAAS;;EAET;;EAEA,QAAQ;;EAER,SAAS;;EAET;;EAEA,QAAQ;;EAER,SAAS;;EAET;;;UAIe,mBAAmB;;EAElC,OAAO;;EAEP,UAAU;;EAEV;;;UAIe,gBAAgB;;EAE/B,QAAQ,YAAY;;EAEpB,SAAS,aAAa;;EAEtB,SAAS,eAAe;;EAExB,SAAS,0BAA0B;;;;;;;KAQzB,kBAAkB,WAC5B,gBAAgB,gCAAgC,WAAW;;;;;;KAOjD,mBACV,gBAAgB,sCACd,KAAK,gBAAgB,kBAAkB;EACzC,QAAQ;;UAGA;EACR,QAAQ,YAAY;EACpB,SAAS,aAAa;EACtB,SAAS;EACT,QAAQ;;;;;;;KAQE,aAAa,gBAAgB,eAAe,iBACrD,WAAW,UAAU,mBAAmB,QAAQ;;KAIvC,eAAe,uBAAuB;EAChD,KAAK;EACL,SAAS;EACT,MAAM,MAAM,QAAQ,wBAAwB,QAAQ;MAChD,QAAQ,kCAAkC;;KAGpC,WAAW,aAAa,8BACjC,WAAW,OAAO,eAAe,KAAK;;;;;;KAQ7B,0BACV,gBAAgB,eAAe,iBAE9B,WAAW,UAAU,kBAAkB,QAAQ;;KAItC,YAAY,KAAK,sBAAsB;;KAGvC,cAAc,KAAK,sBAAsB;;KAGzC,aACV,KAAK,mBACL,KAAK,uBACF;;KAGO,aACV,OAAO,OACP,KAAK,sBACF,QAAQ,kCAAkC"}
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/utils/index.ts"],"mappings":";;;;;;;;;;;;;;cAca,qBAAsB,WAAW"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../src/utils/index.ts"],"sourcesContent":["/**\n * Utility helpers for webhook internals.\n *\n * @module @zap-studio/webhooks/utils\n */\n\n/**\n * Compares two strings in constant time to prevent timing attacks.\n *\n * @example\n * ```ts\n * const isEqual = constantTimeEquals(\"string1\", \"string2\"); // returns false\n * ```\n */\nexport const constantTimeEquals = (a: string, b: string): boolean => {\n if (a.length !== b.length) {\n return false;\n }\n\n let result = 0;\n for (let i = 0; i < a.length; i += 1) {\n // oxlint-disable-next-line no-bitwise, unicorn/prefer-code-point -- XOR is the constant-time compare trick; charCodeAt reads each char with the same cost, and inputs are plain ASCII hex.\n result |= a.charCodeAt(i) ^ b.charCodeAt(i);\n }\n\n return result === 0;\n};\n"],"mappings":";;;;;;;;;;;;;;AAcA,MAAa,sBAAsB,GAAW,MAAuB;CACnE,IAAI,EAAE,WAAW,EAAE,QACjB,OAAO;CAGT,IAAI,SAAS;CACb,KAAK,IAAI,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK,GAEjC,UAAU,EAAE,WAAW,CAAC,IAAI,EAAE,WAAW,CAAC;CAG5C,OAAO,WAAW;AACpB"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"verify.d.mts","names":[],"sources":["../src/verify.ts"],"mappings":";;cAUM;WACJ;WACA;WACA;WACA;;KAGG,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA2CrB,uBACX,YACA,QACA;EAEA;EACA;EACA,OAAO;MACL"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"verify.mjs","names":[],"sources":["../src/verify.ts"],"sourcesContent":["/**\n * Signature verification helpers for webhook requests.\n *\n * @module @zap-studio/webhooks/verify\n */\n\nimport { VerificationError } from \"./errors.js\";\nimport type { VerifyFn } from \"./types/index.js\";\nimport { constantTimeEquals } from \"./utils/index.js\";\n\nconst HMAC_HASH = {\n sha1: \"SHA-1\",\n sha256: \"SHA-256\",\n sha384: \"SHA-384\",\n sha512: \"SHA-512\",\n} as const;\n\ntype HmacAlgorithm = keyof typeof HMAC_HASH;\n\nconst toHex = (bytes: Uint8Array): string =>\n Array.from(bytes, (byte) => byte.toString(16).padStart(2, \"0\")).join(\"\");\n\nconst normalizeSignature = (signature: string): string =>\n signature\n .replace(/^[a-z0-9-]+=/iu, \"\")\n .trim()\n .toLowerCase();\n\n/**\n * Creates a webhook verifier that validates an HMAC signature from a request header.\n *\n * The verifier imports the provided string secret once, computes an HMAC from\n * `req.rawBody`, normalizes the incoming header value, and compares both\n * signatures in constant time.\n *\n * Header values like `sha256=<hex>` are supported so common provider formats\n * such as GitHub work without extra parsing.\n *\n * @example\n * ```ts\n * import { createWebhookRouter } from \"@zap-studio/webhooks\";\n * import { createHmacVerifier } from \"@zap-studio/webhooks/verify\";\n *\n * const router = createWebhookRouter({\n * verify: createHmacVerifier({\n * headerName: \"x-hub-signature-256\",\n * secret: process.env.GITHUB_WEBHOOK_SECRET!,\n * }),\n * });\n * ```\n *\n * @param options - Verifier configuration.\n * @param options.headerName - Header containing the provider signature.\n * @param options.secret - Shared HMAC secret as a string.\n * @param options.algo - HMAC hash algorithm. Defaults to `\"sha256\"`.\n * @returns A router-compatible request verifier.\n *\n * @throws {VerificationError}\n * Thrown when verifier setup fails or request verification does not pass.\n */\nexport const createHmacVerifier = ({\n headerName,\n secret,\n algo = \"sha256\",\n}: {\n headerName: string;\n secret: string;\n algo?: HmacAlgorithm;\n}): VerifyFn => {\n if (globalThis.crypto?.subtle === undefined) {\n throw new VerificationError(\n \"Web Crypto API is unavailable in this runtime\"\n );\n }\n\n const { subtle } = globalThis.crypto;\n\n const hash = HMAC_HASH[algo];\n if (!hash) {\n throw new VerificationError(`Unsupported HMAC algorithm: ${algo}`);\n }\n\n const keyPromise = subtle.importKey(\n \"raw\",\n new TextEncoder().encode(secret),\n { hash, name: \"HMAC\" },\n false,\n [\"sign\"]\n );\n\n return async (req) => {\n const actual = req.headers.get(headerName);\n if (actual === null || actual.length === 0) {\n throw new VerificationError(`Missing signature header: ${headerName}`);\n }\n\n const key = await keyPromise;\n const signature = await subtle.sign(\n \"HMAC\",\n key,\n new Uint8Array(req.rawBody)\n );\n const expected = toHex(new Uint8Array(signature));\n\n if (!constantTimeEquals(expected, normalizeSignature(actual))) {\n throw new VerificationError(\n `Invalid signature for header: ${headerName}`\n );\n }\n };\n};\n"],"mappings":";;;;;;;;AAUA,MAAM,YAAY;CAChB,MAAM;CACN,QAAQ;CACR,QAAQ;CACR,QAAQ;AACV;AAIA,MAAM,SAAS,UACb,MAAM,KAAK,QAAQ,SAAS,KAAK,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE;AAEzE,MAAM,sBAAsB,cAC1B,UACG,QAAQ,kBAAkB,EAAE,CAAC,CAC7B,KAAK,CAAC,CACN,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCjB,MAAa,sBAAsB,EACjC,YACA,QACA,OAAO,eAKO;CACd,IAAI,WAAW,QAAQ,WAAW,KAAA,GAChC,MAAM,IAAI,kBACR,+CACF;CAGF,MAAM,EAAE,WAAW,WAAW;CAE9B,MAAM,OAAO,UAAU;CACvB,IAAI,CAAC,MACH,MAAM,IAAI,kBAAkB,+BAA+B,MAAM;CAGnE,MAAM,aAAa,OAAO,UACxB,OACA,IAAI,YAAY,CAAC,CAAC,OAAO,MAAM,GAC/B;EAAE;EAAM,MAAM;CAAO,GACrB,OACA,CAAC,MAAM,CACT;CAEA,OAAO,OAAO,QAAQ;EACpB,MAAM,SAAS,IAAI,QAAQ,IAAI,UAAU;EACzC,IAAI,WAAW,QAAQ,OAAO,WAAW,GACvC,MAAM,IAAI,kBAAkB,6BAA6B,YAAY;EAGvE,MAAM,MAAM,MAAM;EAClB,MAAM,YAAY,MAAM,OAAO,KAC7B,QACA,KACA,IAAI,WAAW,IAAI,OAAO,CAC5B;EAGA,IAAI,CAAC,mBAFY,MAAM,IAAI,WAAW,SAAS,CAEhB,GAAG,mBAAmB,MAAM,CAAC,GAC1D,MAAM,IAAI,kBACR,iCAAiC,YACnC;CAEJ;AACF"}