@zap-studio/webhooks 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
+ ## @zap-studio/webhooks@0.3.0
2
+
3
+ ### Migrate to ultracite lint/format; make the adapter contract generic
4
+
5
+ `Adapter` and `BaseAdapter` are now generic over the framework request/response types (`Adapter<TReq, TRes>`, `BaseAdapter<TReq, TRes>`), replacing the previous per-method generics. The mapping members (`toNormalizedRequest`, `toFrameworkResponse`, `handleWebhook`) are now arrow properties, so custom adapters must override them with property syntax rather than method syntax.
6
+
7
+ Also: `register()` now returns `this`, error hooks always receive a real `Error` instance, and `rawBody` is typed as `Uint8Array`.
8
+
1
9
  # @zap-studio/webhooks
2
10
 
11
+ ## 0.2.2
12
+
13
+ ### Dependencies
14
+
15
+ - Updated dependency `@zap-studio/validation` to `0.3.4`.
16
+
3
17
  ## 0.2.1
4
18
 
5
19
  ### Fixed
package/README.md CHANGED
@@ -90,7 +90,11 @@ const router = createWebhookRouter({
90
90
  throw new Error("Missing Stripe signature");
91
91
  }
92
92
 
93
- stripe.webhooks.constructEvent(req.rawBody, signature, process.env.STRIPE_WEBHOOK_SECRET!);
93
+ stripe.webhooks.constructEvent(
94
+ req.rawBody,
95
+ signature,
96
+ process.env.STRIPE_WEBHOOK_SECRET!
97
+ );
94
98
  },
95
99
  });
96
100
 
@@ -177,25 +181,33 @@ This package is framework-agnostic by design. It does not include Express/Next/H
177
181
 
178
182
  ```ts
179
183
  import { BaseAdapter } from "@zap-studio/webhooks/adapters/base";
180
- import type { NormalizedRequest, NormalizedResponse } from "@zap-studio/webhooks/types";
184
+ import type {
185
+ NormalizedRequest,
186
+ NormalizedResponse,
187
+ } from "@zap-studio/webhooks/types";
181
188
 
189
+ // `BaseAdapter<TReq, TRes>` is generic over your framework request/response
190
+ // types. Override the mapping members with arrow-property syntax.
182
191
  class MyHttpAdapter extends BaseAdapter {
183
- async toNormalizedRequest(req: any): Promise<NormalizedRequest> {
184
- return {
185
- method: req.method,
186
- path: req.url,
187
- headers: new Headers(req.headers),
188
- rawBody: req.rawBody,
189
- };
190
- }
191
-
192
- async toFrameworkResponse(res: any, normalized: NormalizedResponse): Promise<any> {
192
+ toNormalizedRequest = async (req: any): Promise<NormalizedRequest> => ({
193
+ method: req.method,
194
+ path: req.url,
195
+ headers: new Headers(req.headers),
196
+ rawBody: req.rawBody,
197
+ });
198
+
199
+ toFrameworkResponse = async (
200
+ res: any,
201
+ normalized: NormalizedResponse
202
+ ): Promise<any> => {
193
203
  res.statusCode = normalized.status;
194
204
  res.end(
195
- typeof normalized.body === "string" ? normalized.body : JSON.stringify(normalized.body),
205
+ typeof normalized.body === "string"
206
+ ? normalized.body
207
+ : JSON.stringify(normalized.body)
196
208
  );
197
209
  return res;
198
- }
210
+ };
199
211
  }
200
212
  ```
201
213
 
@@ -1,57 +1,58 @@
1
1
  import { NormalizedRequest, NormalizedResponse } from "../types/index.mjs";
2
-
3
2
  //#region src/adapters/base.d.ts
3
+ interface RouterHandler {
4
+ handle: (req: NormalizedRequest) => Promise<NormalizedResponse>;
5
+ }
4
6
  /**
5
- * Minimal framework adapter contract.
6
- *
7
- * Implement this when integrating the webhook router with an HTTP framework.
8
- */
9
- interface Adapter {
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> {
10
15
  /**
11
- * Creates a framework handler that:
12
- * 1. normalizes the incoming framework request
13
- * 2. executes the webhook router
14
- * 3. writes the normalized response back to the framework response
15
- */
16
- handleWebhook<TFrameworkReq = unknown, TFrameworkRes = unknown>(router: {
17
- handle(req: NormalizedRequest): Promise<NormalizedResponse>;
18
- }): (req: TFrameworkReq, res: TFrameworkRes) => Promise<void>;
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>;
19
22
  /**
20
- * Maps a normalized router response to the framework response object.
21
- *
22
- * @param frameworkRes - Framework-specific response object (e.g. `res`)
23
- * @param res - Normalized response returned by the webhook router
24
- */
25
- toFrameworkResponse<TFrameworkRes = unknown>(frameworkRes: TFrameworkRes, res: NormalizedResponse): Promise<TFrameworkRes>;
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>;
26
29
  /**
27
- * Maps a framework request into the normalized request contract.
28
- *
29
- * The returned object must include `rawBody` to support signature verification.
30
- *
31
- * @param req - Framework-specific request object
32
- */
33
- toNormalizedRequest<TReq = unknown>(req: TReq): Promise<NormalizedRequest>;
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>;
34
37
  }
35
38
  /**
36
- * Base adapter helper.
37
- *
38
- * Extend this class in consumers to keep framework integration boilerplate
39
- * in one place while relying on the package router contract.
40
- */
41
- declare abstract class BaseAdapter implements Adapter {
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> {
42
45
  /** @inheritdoc */
43
- abstract toNormalizedRequest<TReq = unknown>(req: TReq): Promise<NormalizedRequest>;
46
+ abstract toNormalizedRequest: (req: TReq) => Promise<NormalizedRequest>;
44
47
  /** @inheritdoc */
45
- abstract toFrameworkResponse<TFrameworkRes = unknown>(frameworkRes: TFrameworkRes, res: NormalizedResponse): Promise<TFrameworkRes>;
48
+ abstract toFrameworkResponse: (frameworkRes: TRes, res: NormalizedResponse) => Promise<TRes>;
46
49
  /**
47
- * Shared adapter pipeline implementation.
48
- *
49
- * Most consumers only need to implement request/response mapping methods and
50
- * can reuse this default orchestration.
51
- */
52
- handleWebhook<TFrameworkReq = unknown, TFrameworkRes = unknown>(router: {
53
- handle(req: NormalizedRequest): Promise<NormalizedResponse>;
54
- }): (req: TFrameworkReq, res: TFrameworkRes) => Promise<void>;
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>);
55
56
  }
56
57
  //#endregion
57
58
  export { Adapter, BaseAdapter };
@@ -1 +1 @@
1
- {"version":3,"file":"base.d.mts","names":[],"sources":["../../src/adapters/base.ts"],"mappings":";;;;;AAOA;;;UAAiB,OAAA;;;;;;;EAOf,aAAA,mDAAgE,MAAA;IAC9D,MAAA,CAAO,GAAA,EAAK,iBAAA,GAAoB,OAAA,CAAQ,kBAAA;EAAA,KACrC,GAAA,EAAK,aAAA,EAAe,GAAA,EAAK,aAAA,KAAkB,OAAA;;;;;;;EAQhD,mBAAA,0BACE,YAAA,EAAc,aAAA,EACd,GAAA,EAAK,kBAAA,GACJ,OAAA,CAAQ,aAAA;;;;;;;;EASX,mBAAA,iBAAoC,GAAA,EAAK,IAAA,GAAO,OAAA,CAAQ,iBAAA;AAAA;;;;;;;uBASpC,WAAA,YAAuB,OAAA;;WAElC,mBAAA,gBAAA,CAAoC,GAAA,EAAK,IAAA,GAAO,OAAA,CAAQ,iBAAA;;WAExD,mBAAA,yBAAA,CACP,YAAA,EAAc,aAAA,EACd,GAAA,EAAK,kBAAA,GACJ,OAAA,CAAQ,aAAA;;;;;;AAPb;EAeE,aAAA,kDAAA,CAAgE,MAAA;IAC9D,MAAA,CAAO,GAAA,EAAK,iBAAA,GAAoB,OAAA,CAAQ,kBAAA;EAAA,KACrC,GAAA,EAAK,aAAA,EAAe,GAAA,EAAK,aAAA,KAAkB,OAAA;AAAA"}
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"}
@@ -12,13 +12,11 @@ var BaseAdapter = class {
12
12
  * Most consumers only need to implement request/response mapping methods and
13
13
  * can reuse this default orchestration.
14
14
  */
15
- handleWebhook(router) {
16
- return async (req, res) => {
17
- const normalizedReq = await this.toNormalizedRequest(req);
18
- const normalizedRes = await router.handle(normalizedReq);
19
- await this.toFrameworkResponse(res, normalizedRes);
20
- };
21
- }
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
+ };
22
20
  };
23
21
  //#endregion
24
22
  export { BaseAdapter };
@@ -1 +1 @@
1
- {"version":3,"file":"base.mjs","names":[],"sources":["../../src/adapters/base.ts"],"sourcesContent":["import type { NormalizedRequest, NormalizedResponse } from \"../types/index.js\";\n\n/**\n * Minimal framework adapter contract.\n *\n * Implement this when integrating the webhook router with an HTTP framework.\n */\nexport interface Adapter {\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<TFrameworkReq = unknown, TFrameworkRes = unknown>(router: {\n handle(req: NormalizedRequest): Promise<NormalizedResponse>;\n }): (req: TFrameworkReq, res: TFrameworkRes) => 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<TFrameworkRes = unknown>(\n frameworkRes: TFrameworkRes,\n res: NormalizedResponse,\n ): Promise<TFrameworkRes>;\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<TReq = unknown>(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 implements Adapter {\n /** @inheritdoc */\n abstract toNormalizedRequest<TReq = unknown>(req: TReq): Promise<NormalizedRequest>;\n /** @inheritdoc */\n abstract toFrameworkResponse<TFrameworkRes = unknown>(\n frameworkRes: TFrameworkRes,\n res: NormalizedResponse,\n ): Promise<TFrameworkRes>;\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<TFrameworkReq = unknown, TFrameworkRes = unknown>(router: {\n handle(req: NormalizedRequest): Promise<NormalizedResponse>;\n }): (req: TFrameworkReq, res: TFrameworkRes) => Promise<void> {\n return 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}\n"],"mappings":";;;;;;;AA6CA,IAAsB,cAAtB,MAAqD;;;;;;;CAenD,cAAgE,QAEF;AAC5D,SAAO,OAAO,KAAK,QAAQ;GACzB,MAAM,gBAAgB,MAAM,KAAK,oBAAoB,IAAI;GACzD,MAAM,gBAAgB,MAAM,OAAO,OAAO,cAAc;AACxD,SAAM,KAAK,oBAAoB,KAAK,cAAc"}
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"}
package/dist/errors.d.mts CHANGED
@@ -1,11 +1,21 @@
1
1
  //#region src/errors.d.ts
2
2
  /**
3
- * Error thrown when webhook request verification fails.
4
- *
5
- * This error is used by verifier helpers such as `createHmacVerifier` so
6
- * callers can distinguish verification failures from other webhook errors.
7
- */
3
+ * Error primitives for webhook verification failures.
4
+ *
5
+ * @module @zap-studio/webhooks/errors
6
+ */
7
+ /**
8
+ * Error thrown when webhook request verification fails.
9
+ *
10
+ * This error is used by verifier helpers such as `createHmacVerifier` so
11
+ * callers can distinguish verification failures from other webhook errors.
12
+ */
8
13
  declare class VerificationError extends Error {
14
+ /**
15
+ * Creates a verification error with a human-readable message.
16
+ *
17
+ * @param message - Error message describing the verification failure.
18
+ */
9
19
  constructor(message: string);
10
20
  }
11
21
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.mts","names":[],"sources":["../src/errors.ts"],"mappings":";;AAMA;;;;;cAAa,iBAAA,SAA0B,KAAA;EACrC,WAAA,CAAY,OAAA;AAAA"}
1
+ {"version":3,"file":"errors.d.mts","names":[],"sources":["../src/errors.ts"],"mappings":";;;;;;;;;;;;cAYa,0BAA0B;;;;;;EAMrC,YAAY"}
package/dist/errors.mjs CHANGED
@@ -1,11 +1,21 @@
1
1
  //#region src/errors.ts
2
2
  /**
3
+ * Error primitives for webhook verification failures.
4
+ *
5
+ * @module @zap-studio/webhooks/errors
6
+ */
7
+ /**
3
8
  * Error thrown when webhook request verification fails.
4
9
  *
5
10
  * This error is used by verifier helpers such as `createHmacVerifier` so
6
11
  * callers can distinguish verification failures from other webhook errors.
7
12
  */
8
13
  var VerificationError = class extends Error {
14
+ /**
15
+ * Creates a verification error with a human-readable message.
16
+ *
17
+ * @param message - Error message describing the verification failure.
18
+ */
9
19
  constructor(message) {
10
20
  super(message);
11
21
  this.name = "VerificationError";
@@ -1 +1 @@
1
- {"version":3,"file":"errors.mjs","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/**\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 constructor(message: string) {\n super(message);\n this.name = \"VerificationError\";\n }\n}\n"],"mappings":";;;;;;;AAMA,IAAa,oBAAb,cAAuC,MAAM;CAC3C,YAAY,SAAiB;AAC3B,QAAM,QAAQ;AACd,OAAK,OAAO"}
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"}
package/dist/index.d.mts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { AfterHook, BeforeHook, ErrorHook, InferSchemaOutput, NormalizedRequest, NormalizedResponse, RegisterOptions, SchemaRouteOptions, WebhookHandler } from "./types/index.mjs";
2
2
  import { StandardSchemaV1 } from "@zap-studio/validation";
3
-
4
3
  //#region src/index.d.ts
5
4
  interface WebhookRouterOptions {
6
5
  /** Global hooks executed after successful route handler completion. */
@@ -15,10 +14,10 @@ interface WebhookRouterOptions {
15
14
  verify?: (req: NormalizedRequest) => Promise<void> | void;
16
15
  }
17
16
  /**
18
- * Main webhook router class.
19
- *
20
- * Register routes with typed schemas and call `handle` with a normalized request.
21
- */
17
+ * Main webhook router class.
18
+ *
19
+ * Register routes with typed schemas and call `handle` with a normalized request.
20
+ */
22
21
  declare class WebhookRouter<TMap = unknown> {
23
22
  private readonly handlers;
24
23
  private readonly verify;
@@ -26,45 +25,51 @@ declare class WebhookRouter<TMap = unknown> {
26
25
  private readonly globalAfterHooks;
27
26
  private readonly globalErrorHook;
28
27
  private readonly prefix;
28
+ /**
29
+ * Creates a webhook router with optional global hooks and verification behavior.
30
+ *
31
+ * @param opts - Router-level options.
32
+ */
29
33
  constructor(opts?: WebhookRouterOptions);
30
34
  /**
31
- * Register a webhook handler for a specific path.
32
- *
33
- * When a schema is provided, `payload` is inferred from the schema output type.
34
- *
35
- * @param path - Route path relative to configured prefix.
36
- * @param handlerOrOptions - Handler function or schema-based registration options.
37
- * @returns The same router instance with an updated internal route type map.
38
- */
35
+ * Register a webhook handler for a specific path.
36
+ *
37
+ * When a schema is provided, `payload` is inferred from the schema output type.
38
+ *
39
+ * @param path - Route path relative to configured prefix.
40
+ * @param handlerOrOptions - Handler function or schema-based registration options.
41
+ * @returns The same router instance with an updated internal route type map.
42
+ */
39
43
  register<Path extends string, TSchema extends StandardSchemaV1<unknown, unknown>>(path: Path, handlerOrOptions: SchemaRouteOptions<TSchema>): WebhookRouter<TMap & Record<Path, InferSchemaOutput<TSchema>>>;
40
44
  register<Path extends string, TPayload>(path: Path, handlerOrOptions: RegisterOptions<TPayload>): WebhookRouter<TMap & Record<Path, TPayload>>;
41
- register<Path extends string>(path: Path, handlerOrOptions: WebhookHandler<unknown>): WebhookRouter<TMap & Record<Path, unknown>>;
45
+ register<Path extends string>(path: Path, handlerOrOptions: WebhookHandler): WebhookRouter<TMap & Record<Path, unknown>>;
42
46
  /**
43
- * Handles a normalized incoming webhook request.
44
- *
45
- * @param req - Normalized request object.
46
- * @returns Normalized response for the adapter/framework layer.
47
- */
47
+ * Handles a normalized incoming webhook request.
48
+ *
49
+ * @param req - Normalized request object.
50
+ * @returns Normalized response for the adapter/framework layer.
51
+ */
48
52
  handle(req: NormalizedRequest): Promise<NormalizedResponse>;
49
53
  private normalizePath;
54
+ private static runHooks;
50
55
  private runGlobalBeforeHooks;
51
- private createHandlerEntry;
52
- private runRouteBeforeHooks;
53
- private parseRequestBody;
54
- private isErrorResponse;
55
- private validatePayload;
56
- private executeHandler;
57
- private runRouteAfterHooks;
56
+ private static createHandlerEntry;
57
+ private static runRouteBeforeHooks;
58
+ private static parseRequestBody;
59
+ private static isErrorResponse;
60
+ private static validatePayload;
61
+ private static executeHandler;
62
+ private static runRouteAfterHooks;
58
63
  private runGlobalAfterHooks;
59
64
  private handleError;
60
65
  }
61
66
  /**
62
- * Factory helper for creating a webhook router instance.
63
- *
64
- * @param opts - Optional global router options.
65
- * @returns A new webhook router.
66
- */
67
- declare function createWebhookRouter(opts?: WebhookRouterOptions): WebhookRouter;
67
+ * Factory helper for creating a webhook router instance.
68
+ *
69
+ * @param opts - Optional global router options.
70
+ * @returns A new webhook router.
71
+ */
72
+ declare const createWebhookRouter: (opts?: WebhookRouterOptions) => WebhookRouter;
68
73
  //#endregion
69
74
  export { WebhookRouter, WebhookRouterOptions, createWebhookRouter };
70
75
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":";;;;UA6BiB,oBAAA;;EAEf,KAAA,GAAQ,SAAA,GAAY,SAAA;EAFL;EAIf,MAAA,GAAS,UAAA,GAAa,UAAA;;EAEtB,OAAA,GAAU,SAAA;;EAEV,MAAA;;EAEA,MAAA,IAAU,GAAA,EAAK,iBAAA,KAAsB,OAAA;AAAA;;;;;;cAgB1B,aAAA;EAAA,iBACM,QAAA;EAAA,iBACA,MAAA;EAAA,iBACA,iBAAA;EAAA,iBACA,gBAAA;EAAA,iBACA,eAAA;EAAA,iBACA,MAAA;EAEjB,WAAA,CAAY,IAAA,GAAM,oBAAA;;;;;AARpB;;;;;EAyBE,QAAA,sCAA8C,gBAAA,mBAAA,CAC5C,IAAA,EAAM,IAAA,EACN,gBAAA,EAAkB,kBAAA,CAAmB,OAAA,IACpC,aAAA,CAAc,IAAA,GAAO,MAAA,CAAO,IAAA,EAAM,iBAAA,CAAkB,OAAA;EACvD,QAAA,+BAAA,CACE,IAAA,EAAM,IAAA,EACN,gBAAA,EAAkB,eAAA,CAAgB,QAAA,IACjC,aAAA,CAAc,IAAA,GAAO,MAAA,CAAO,IAAA,EAAM,QAAA;EACrC,QAAA,qBAAA,CACE,IAAA,EAAM,IAAA,EACN,gBAAA,EAAkB,cAAA,YACjB,aAAA,CAAc,IAAA,GAAO,MAAA,CAAO,IAAA;;;;;;;EAoB/B,MAAA,CAAa,GAAA,EAAK,iBAAA,GAAoB,OAAA,CAAQ,kBAAA;EAAA,QAsCtC,aAAA;EAAA,QA0BM,oBAAA;EAAA,QAMN,kBAAA;EAAA,QAoBM,mBAAA;EAAA,QAQN,gBAAA;EAAA,QAUA,eAAA;EAAA,QASM,eAAA;EAAA,QA8BA,cAAA;EAAA,QAyBA,kBAAA;EAAA,QAYA,mBAAA;EAAA,QASA,WAAA;AAAA;;;;;;;iBA0BA,mBAAA,CAAoB,IAAA,GAAO,oBAAA,GAAuB,aAAA"}
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 CHANGED
@@ -1,21 +1,26 @@
1
1
  import { standardValidate } from "@zap-studio/validation";
2
2
  //#region src/index.ts
3
- function toArray(value) {
3
+ const toArray = (value) => {
4
4
  if (value === void 0) return [];
5
5
  return Array.isArray(value) ? value : [value];
6
- }
6
+ };
7
7
  /**
8
8
  * Main webhook router class.
9
9
  *
10
10
  * Register routes with typed schemas and call `handle` with a normalized request.
11
11
  */
12
- var WebhookRouter = class {
12
+ var WebhookRouter = class WebhookRouter {
13
13
  handlers = {};
14
14
  verify;
15
15
  globalBeforeHooks = [];
16
16
  globalAfterHooks = [];
17
17
  globalErrorHook;
18
18
  prefix;
19
+ /**
20
+ * Creates a webhook router with optional global hooks and verification behavior.
21
+ *
22
+ * @param opts - Router-level options.
23
+ */
19
24
  constructor(opts = {}) {
20
25
  this.prefix = opts.prefix ?? "/webhooks/";
21
26
  this.verify = opts.verify;
@@ -24,8 +29,7 @@ var WebhookRouter = class {
24
29
  this.globalErrorHook = opts.onError;
25
30
  }
26
31
  register(path, handlerOrOptions) {
27
- if (typeof handlerOrOptions === "function") this.handlers[path] = { handler: handlerOrOptions };
28
- else this.handlers[path] = this.createHandlerEntry(handlerOrOptions);
32
+ this.handlers[path] = typeof handlerOrOptions === "function" ? { handler: handlerOrOptions } : WebhookRouter.createHandlerEntry(handlerOrOptions);
29
33
  return this;
30
34
  }
31
35
  /**
@@ -38,52 +42,60 @@ var WebhookRouter = class {
38
42
  try {
39
43
  const normalizedPath = this.normalizePath(req);
40
44
  if (normalizedPath === null) return {
41
- status: 404,
42
- body: { error: "not found" }
45
+ body: { error: "not found" },
46
+ status: 404
43
47
  };
44
48
  const handlerEntry = this.handlers[normalizedPath];
45
49
  if (!handlerEntry) return {
46
- status: 404,
47
- body: { error: "not found" }
50
+ body: { error: "not found" },
51
+ status: 404
48
52
  };
49
53
  await this.runGlobalBeforeHooks(req);
50
- await this.runRouteBeforeHooks(req, handlerEntry.before);
54
+ await WebhookRouter.runRouteBeforeHooks(req, handlerEntry.before);
51
55
  if (this.verify) await this.verify(req);
52
- const parsedJson = this.parseRequestBody(req);
53
- const validationResult = await this.validatePayload(parsedJson, handlerEntry.schema);
54
- if (this.isErrorResponse(validationResult)) return validationResult;
55
- const response = await this.executeHandler(handlerEntry.handler, req, validationResult);
56
- await this.runRouteAfterHooks(req, response, handlerEntry.after);
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);
57
61
  await this.runGlobalAfterHooks(req, response);
58
62
  return response;
59
63
  } catch (error) {
60
- return this.handleError(error, req);
64
+ return await this.handleError(error, req);
61
65
  }
62
66
  }
63
67
  normalizePath(req) {
64
68
  let pathname = req.path;
65
69
  try {
66
- pathname = new URL(req.path).pathname;
70
+ const url = new URL(req.path);
71
+ ({pathname} = url);
67
72
  } catch {}
68
- if (!pathname.startsWith(this.prefix)) return null;
69
- pathname = pathname.slice(this.prefix.length - 1);
73
+ pathname = pathname.startsWith(this.prefix) ? pathname.slice(this.prefix.length - 1) : "";
74
+ if (pathname.length === 0) return null;
70
75
  req.path = pathname;
71
76
  return pathname.startsWith("/") ? pathname.slice(1) : pathname;
72
77
  }
78
+ static async runHooks(hooks, run) {
79
+ for (const hook of hooks) await run(hook);
80
+ }
73
81
  async runGlobalBeforeHooks(req) {
74
- for (const hook of this.globalBeforeHooks) await hook(req);
82
+ await WebhookRouter.runHooks(this.globalBeforeHooks, async (hook) => {
83
+ await hook(req);
84
+ });
75
85
  }
76
- createHandlerEntry(options) {
86
+ static createHandlerEntry(options) {
77
87
  const entry = { handler: options.handler };
78
88
  if (options.schema !== void 0) entry.schema = options.schema;
79
89
  if (options.before !== void 0) entry.before = Array.isArray(options.before) ? options.before : [options.before];
80
90
  if (options.after !== void 0) entry.after = Array.isArray(options.after) ? options.after : [options.after];
81
91
  return entry;
82
92
  }
83
- async runRouteBeforeHooks(req, before) {
84
- if (before) for (const hook of before) await hook(req);
93
+ static async runRouteBeforeHooks(req, before) {
94
+ if (before) await WebhookRouter.runHooks(before, async (hook) => {
95
+ await hook(req);
96
+ });
85
97
  }
86
- parseRequestBody(req) {
98
+ static parseRequestBody(req) {
87
99
  try {
88
100
  const parsed = JSON.parse(new TextDecoder().decode(req.rawBody));
89
101
  req.json = parsed;
@@ -92,55 +104,61 @@ var WebhookRouter = class {
92
104
  return;
93
105
  }
94
106
  }
95
- isErrorResponse(value) {
107
+ static isErrorResponse(value) {
96
108
  return typeof value === "object" && value !== null && "status" in value && typeof value.status === "number";
97
109
  }
98
- async validatePayload(parsedJson, schema) {
110
+ static async validatePayload(parsedJson, schema) {
99
111
  if (!schema) return parsedJson;
100
112
  const result = await standardValidate(schema, parsedJson, { throwOnError: false });
101
113
  if (result.issues) return {
102
- status: 400,
103
114
  body: {
104
115
  error: "validation failed",
105
116
  issues: result.issues.map((issue) => ({
106
- path: issue.path?.map((p) => typeof p === "object" && "key" in p ? String(p.key) : String(p)),
107
- message: issue.message
117
+ message: issue.message,
118
+ path: issue.path?.map((p) => typeof p === "object" && "key" in p ? String(p.key) : String(p))
108
119
  }))
109
- }
120
+ },
121
+ status: 400
110
122
  };
111
123
  return result.value;
112
124
  }
113
- async executeHandler(handler, req, validatedPayload) {
125
+ static async executeHandler(handler, req, validatedPayload) {
114
126
  return await handler({
115
- req,
116
- payload: validatedPayload,
117
127
  ack: async (r) => {
128
+ await Promise.resolve();
118
129
  const response = {
119
- status: r?.status ?? 200,
120
- body: r?.body ?? "ok"
130
+ body: r?.body ?? "ok",
131
+ status: r?.status ?? 200
121
132
  };
122
133
  if (r?.headers !== void 0) response.headers = r.headers;
123
134
  return response;
124
- }
135
+ },
136
+ payload: validatedPayload,
137
+ req
125
138
  }) ?? {
126
- status: 200,
127
- body: "ok"
139
+ body: "ok",
140
+ status: 200
128
141
  };
129
142
  }
130
- async runRouteAfterHooks(req, response, after) {
131
- if (after) for (const hook of after) await hook(req, response);
143
+ static async runRouteAfterHooks(req, response, after) {
144
+ if (after) await WebhookRouter.runHooks(after, async (hook) => {
145
+ await hook(req, response);
146
+ });
132
147
  }
133
148
  async runGlobalAfterHooks(req, response) {
134
- for (const hook of this.globalAfterHooks) await hook(req, response);
149
+ await WebhookRouter.runHooks(this.globalAfterHooks, async (hook) => {
150
+ await hook(req, response);
151
+ });
135
152
  }
136
153
  async handleError(error, req) {
137
154
  if (this.globalErrorHook) {
138
- const errorResponse = await this.globalErrorHook(error, req);
155
+ const normalizedError = error instanceof Error ? error : /* @__PURE__ */ new Error("Internal server error");
156
+ const errorResponse = await this.globalErrorHook(normalizedError, req);
139
157
  if (errorResponse) return errorResponse;
140
158
  }
141
159
  return {
142
- status: 500,
143
- body: { error: error instanceof Error ? error.message : "Internal server error" }
160
+ body: { error: error instanceof Error ? error.message : "Internal server error" },
161
+ status: 500
144
162
  };
145
163
  }
146
164
  };
@@ -150,9 +168,7 @@ var WebhookRouter = class {
150
168
  * @param opts - Optional global router options.
151
169
  * @returns A new webhook router.
152
170
  */
153
- function createWebhookRouter(opts) {
154
- return new WebhookRouter(opts);
155
- }
171
+ const createWebhookRouter = (opts) => new WebhookRouter(opts);
156
172
  //#endregion
157
173
  export { WebhookRouter, createWebhookRouter };
158
174
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["import 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<unknown>>;\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\nfunction 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: ((req: NormalizedRequest) => Promise<void> | void) | undefined;\n private readonly globalBeforeHooks: BeforeHook[] = [];\n private readonly globalAfterHooks: AfterHook[] = [];\n private readonly globalErrorHook: ErrorHook | undefined;\n private readonly prefix: string;\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<Path extends string, TSchema extends StandardSchemaV1<unknown, unknown>>(\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<unknown>,\n ): WebhookRouter<TMap & Record<Path, unknown>>;\n register(\n path: string,\n handlerOrOptions: WebhookHandler<unknown> | RegisterOptions<unknown>,\n ): WebhookRouter<TMap> {\n if (typeof handlerOrOptions === \"function\") {\n this.handlers[path] = { handler: handlerOrOptions };\n } else {\n this.handlers[path] = this.createHandlerEntry(handlerOrOptions);\n }\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 { status: 404, body: { error: \"not found\" } };\n }\n\n const handlerEntry = this.handlers[normalizedPath];\n if (!handlerEntry) {\n return { status: 404, body: { error: \"not found\" } };\n }\n\n await this.runGlobalBeforeHooks(req);\n await this.runRouteBeforeHooks(req, handlerEntry.before);\n\n if (this.verify) {\n await this.verify(req);\n }\n\n const parsedJson = this.parseRequestBody(req);\n const validationResult = await this.validatePayload(parsedJson, handlerEntry.schema);\n\n if (this.isErrorResponse(validationResult)) {\n return validationResult;\n }\n\n const response = await this.executeHandler(handlerEntry.handler, req, validationResult);\n\n await this.runRouteAfterHooks(req, response, handlerEntry.after);\n await this.runGlobalAfterHooks(req, response);\n\n return response;\n } catch (error) {\n return 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.pathname;\n } catch {\n // Not a full URL, use the path as-is\n }\n\n // Require prefix (e.g. /webhooks/path -> /path)\n if (!pathname.startsWith(this.prefix)) {\n // Path doesn't start with the required prefix - not a webhook route\n return null;\n }\n\n // Strip prefix and keep the leading slash\n pathname = pathname.slice(this.prefix.length - 1);\n req.path = pathname;\n\n // Normalize path by removing leading slash for handler matching (e.g. /path -> path)\n const normalizedPath = pathname.startsWith(\"/\") ? pathname.slice(1) : pathname;\n\n return normalizedPath;\n }\n\n private async runGlobalBeforeHooks(req: NormalizedRequest): Promise<void> {\n for (const hook of this.globalBeforeHooks) {\n await hook(req);\n }\n }\n\n private createHandlerEntry(options: RegisterOptions<unknown>): HandlerEntry<unknown> {\n const entry: HandlerEntry<unknown> = {\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) ? options.before : [options.before];\n }\n\n if (options.after !== undefined) {\n entry.after = Array.isArray(options.after) ? options.after : [options.after];\n }\n\n return entry;\n }\n\n private async runRouteBeforeHooks(req: NormalizedRequest, before?: BeforeHook[]): Promise<void> {\n if (before) {\n for (const hook of before) {\n await hook(req);\n }\n }\n }\n\n private parseRequestBody<TParsed = unknown>(req: NormalizedRequest): TParsed | undefined {\n try {\n const parsed = JSON.parse(new TextDecoder().decode(req.rawBody));\n req.json = parsed;\n return parsed as TParsed;\n } catch {\n return;\n }\n }\n\n private 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 async validatePayload<TPayload>(\n parsedJson: unknown,\n schema?: StandardSchemaV1<unknown, TPayload>,\n ): Promise<TPayload | NormalizedResponse> {\n if (!schema) {\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 status: 400,\n body: {\n error: \"validation failed\",\n issues: result.issues.map((issue) => ({\n path: issue.path?.map((p) =>\n typeof p === \"object\" && \"key\" in p ? String(p.key) : String(p),\n ),\n message: issue.message,\n })),\n },\n };\n }\n\n return result.value as TPayload;\n }\n\n private async executeHandler<TPayload = unknown>(\n handler: WebhookHandler<TPayload>,\n req: NormalizedRequest,\n validatedPayload: TPayload,\n ): Promise<NormalizedResponse> {\n const responded = await handler({\n req,\n payload: validatedPayload,\n ack: async (r?: Partial<NormalizedResponse>) => {\n const response: NormalizedResponse = {\n status: r?.status ?? 200,\n body: r?.body ?? \"ok\",\n };\n\n if (r?.headers !== undefined) {\n response.headers = r.headers;\n }\n\n return response;\n },\n });\n\n return responded ?? { status: 200, body: \"ok\" };\n }\n\n private async runRouteAfterHooks(\n req: NormalizedRequest,\n response: NormalizedResponse,\n after?: AfterHook[],\n ): Promise<void> {\n if (after) {\n for (const hook of after) {\n await hook(req, response);\n }\n }\n }\n\n private async runGlobalAfterHooks(\n req: NormalizedRequest,\n response: NormalizedResponse,\n ): Promise<void> {\n for (const hook of this.globalAfterHooks) {\n await hook(req, response);\n }\n }\n\n private async handleError<TError = unknown>(\n error: TError,\n req: NormalizedRequest,\n ): Promise<NormalizedResponse> {\n if (this.globalErrorHook) {\n const errorResponse = await this.globalErrorHook(error as Error, req);\n if (errorResponse) {\n return errorResponse;\n }\n }\n\n return {\n status: 500,\n body: {\n error: error instanceof Error ? error.message : \"Internal server error\",\n },\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 function createWebhookRouter(opts?: WebhookRouterOptions): WebhookRouter {\n return new WebhookRouter(opts);\n}\n"],"mappings":"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"}
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,5 +1,4 @@
1
1
  import { StandardSchemaV1 } from "@zap-studio/validation";
2
-
3
2
  //#region src/types/index.d.ts
4
3
  /** Framework-agnostic request shape consumed by the webhook router. */
5
4
  interface NormalizedRequest {
@@ -16,7 +15,7 @@ interface NormalizedRequest {
16
15
  /** The query parameters of the request */
17
16
  query?: Record<string, string | string[]>;
18
17
  /** The raw body of the request (for signature) */
19
- rawBody: Uint8Array<ArrayBufferLike>;
18
+ rawBody: Uint8Array;
20
19
  /** The parsed text body of the request if applicable */
21
20
  text?: string;
22
21
  }
@@ -41,31 +40,31 @@ interface RegisterOptions<T> {
41
40
  schema?: StandardSchemaV1<unknown, T>;
42
41
  }
43
42
  /**
44
- * Infers the output type from a Standard Schema instance.
45
- *
46
- * @template TSchema - A Standard Schema type.
47
- */
43
+ * Infers the output type from a Standard Schema instance.
44
+ *
45
+ * @template TSchema - A Standard Schema type.
46
+ */
48
47
  type InferSchemaOutput<TSchema> = TSchema extends StandardSchemaV1<unknown, infer TOutput> ? TOutput : never;
49
48
  /**
50
- * Route options where schema is required and handler payload is inferred.
51
- *
52
- * @template TSchema - Schema used to infer handler payload type.
53
- */
49
+ * Route options where schema is required and handler payload is inferred.
50
+ *
51
+ * @template TSchema - Schema used to infer handler payload type.
52
+ */
54
53
  type SchemaRouteOptions<TSchema extends StandardSchemaV1<unknown, unknown>> = Omit<RegisterOptions<InferSchemaOutput<TSchema>>, "schema"> & {
55
54
  schema: TSchema;
56
55
  };
57
56
  interface RouteLike {
58
57
  after?: AfterHook | AfterHook[];
59
58
  before?: BeforeHook | BeforeHook[];
60
- handler: WebhookHandler<unknown>;
59
+ handler: WebhookHandler;
61
60
  schema: StandardSchemaV1<unknown, unknown>;
62
61
  }
63
62
  /**
64
- * Applies schema-driven payload inference to each route entry.
65
- *
66
- * @template TRoutes - Route dictionary keyed by webhook path.
67
- */
68
- type SchemaRoutes<TRoutes extends Record<string, RouteLike>> = { [P in keyof TRoutes]: SchemaRouteOptions<TRoutes[P]["schema"]> };
63
+ * Applies schema-driven payload inference to each route entry.
64
+ *
65
+ * @template TRoutes - Route dictionary keyed by webhook path.
66
+ */
67
+ type SchemaRoutes<TRoutes extends Record<string, RouteLike>> = { [P in keyof TRoutes]: SchemaRouteOptions<TRoutes[P]["schema"]>; };
69
68
  /** The webhook handler function, responsible for processing incoming webhook events. */
70
69
  type WebhookHandler<TPayload = unknown> = (ctx: {
71
70
  req: NormalizedRequest;
@@ -73,13 +72,13 @@ type WebhookHandler<TPayload = unknown> = (ctx: {
73
72
  ack: (res?: Partial<NormalizedResponse>) => Promise<NormalizedResponse>;
74
73
  }) => Promise<NormalizedResponse | undefined> | NormalizedResponse | undefined;
75
74
  /** Maps route keys to their payload-specific webhook handlers. */
76
- type HandlerMap<TMap extends Record<string, unknown>> = { [P in keyof TMap]: WebhookHandler<TMap[P]> };
75
+ type HandlerMap<TMap extends Record<string, unknown>> = { [P in keyof TMap]: WebhookHandler<TMap[P]>; };
77
76
  /**
78
- * Builds a webhook payload map from a schema-based route dictionary.
79
- *
80
- * @template TRoutes - Route dictionary keyed by webhook path.
81
- */
82
- type InferWebhookMapFromRoutes<TRoutes extends Record<string, RouteLike>> = { [P in keyof TRoutes]: InferSchemaOutput<TRoutes[P]["schema"]> };
77
+ * Builds a webhook payload map from a schema-based route dictionary.
78
+ *
79
+ * @template TRoutes - Route dictionary keyed by webhook path.
80
+ */
81
+ type InferWebhookMapFromRoutes<TRoutes extends Record<string, RouteLike>> = { [P in keyof TRoutes]: InferSchemaOutput<TRoutes[P]["schema"]>; };
83
82
  /** Verification function for incoming requests */
84
83
  type VerifyFn = (req: NormalizedRequest) => Promise<void> | void;
85
84
  /** Hook function that runs before request processing */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/types/index.ts"],"mappings":";;;;UAGiB,iBAAA;EAAjB;EAEE,OAAA,EAAS,OAAA;;EAET,IAAA;;EAEA,MAAA,EAAQ,OAAA;;EAER,MAAA,GAAS,MAAA;;EAET,IAAA;EAIS;EAFT,KAAA,GAAQ,MAAA;;EAER,OAAA,EAAS,UAAA,CAAW,eAAA;;EAEpB,IAAA;AAAA;;UAIe,kBAAA;;EAEf,IAAA,GAAO,KAAA;;EAEP,OAAA,GAAU,OAAA;;EAEV,MAAA;AAAA;;UAIe,eAAA;EAVA;EAYf,KAAA,GAAQ,SAAA,GAAY,SAAA;EARV;EAUV,MAAA,GAAS,UAAA,GAAa,UAAA;;EAEtB,OAAA,EAAS,cAAA,CAAe,CAAA;;EAExB,MAAA,GAAS,gBAAA,UAA0B,CAAA;AAAA;;;AARrC;;;KAgBY,iBAAA,YACV,OAAA,SAAgB,gBAAA,2BAA2C,OAAA;;;;;;KAOjD,kBAAA,iBAAmC,gBAAA,sBAAsC,IAAA,CACnF,eAAA,CAAgB,iBAAA,CAAkB,OAAA;EAGlC,MAAA,EAAQ,OAAA;AAAA;AAAA,UAGA,SAAA;EACR,KAAA,GAAQ,SAAA,GAAY,SAAA;EACpB,MAAA,GAAS,UAAA,GAAa,UAAA;EACtB,OAAA,EAAS,cAAA;EACT,MAAA,EAAQ,gBAAA;AAAA;;;;;;KAQE,YAAA,iBAA6B,MAAA,SAAe,SAAA,mBAC1C,OAAA,GAAU,kBAAA,CAAmB,OAAA,CAAQ,CAAA;;KAIvC,cAAA,wBAAsC,GAAA;EAChD,GAAA,EAAK,iBAAA;EACL,OAAA,EAAS,QAAA;EACT,GAAA,GAAM,GAAA,GAAM,OAAA,CAAQ,kBAAA,MAAwB,OAAA,CAAQ,kBAAA;AAAA,MAChD,OAAA,CAAQ,kBAAA,gBAAkC,kBAAA;;KAGpC,UAAA,cAAwB,MAAA,mCACtB,IAAA,GAAO,cAAA,CAAe,IAAA,CAAK,CAAA;;;;;;KAQ7B,yBAAA,iBAA0C,MAAA,SAAe,SAAA,mBACvD,OAAA,GAAU,iBAAA,CAAkB,OAAA,CAAQ,CAAA;;KAItC,QAAA,IAAY,GAAA,EAAK,iBAAA,KAAsB,OAAA;;KAGvC,UAAA,IAAc,GAAA,EAAK,iBAAA,KAAsB,OAAA;;KAGzC,SAAA,IAAa,GAAA,EAAK,iBAAA,EAAmB,GAAA,EAAK,kBAAA,KAAuB,OAAA;;KAGjE,SAAA,IACV,KAAA,EAAO,KAAA,EACP,GAAA,EAAK,iBAAA,KACF,OAAA,CAAQ,kBAAA,gBAAkC,kBAAA"}
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,13 +1,18 @@
1
1
  //#region src/utils/index.d.ts
2
2
  /**
3
- * Compares two strings in constant time to prevent timing attacks.
4
- *
5
- * @example
6
- * ```ts
7
- * const isEqual = constantTimeEquals("string1", "string2"); // returns false
8
- * ```
9
- */
10
- declare function constantTimeEquals(a: string, b: string): boolean;
3
+ * Utility helpers for webhook internals.
4
+ *
5
+ * @module @zap-studio/webhooks/utils
6
+ */
7
+ /**
8
+ * Compares two strings in constant time to prevent timing attacks.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * const isEqual = constantTimeEquals("string1", "string2"); // returns false
13
+ * ```
14
+ */
15
+ declare const constantTimeEquals: (a: string, b: string) => boolean;
11
16
  //#endregion
12
17
  export { constantTimeEquals };
13
18
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/utils/index.ts"],"mappings":";;AAQA;;;;;;;iBAAgB,kBAAA,CAAmB,CAAA,UAAW,CAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/utils/index.ts"],"mappings":";;;;;;;;;;;;;;cAca,qBAAsB,WAAW"}
@@ -1,5 +1,10 @@
1
1
  //#region src/utils/index.ts
2
2
  /**
3
+ * Utility helpers for webhook internals.
4
+ *
5
+ * @module @zap-studio/webhooks/utils
6
+ */
7
+ /**
3
8
  * Compares two strings in constant time to prevent timing attacks.
4
9
  *
5
10
  * @example
@@ -7,12 +12,12 @@
7
12
  * const isEqual = constantTimeEquals("string1", "string2"); // returns false
8
13
  * ```
9
14
  */
10
- function constantTimeEquals(a, b) {
15
+ const constantTimeEquals = (a, b) => {
11
16
  if (a.length !== b.length) return false;
12
17
  let result = 0;
13
18
  for (let i = 0; i < a.length; i += 1) result |= a.charCodeAt(i) ^ b.charCodeAt(i);
14
19
  return result === 0;
15
- }
20
+ };
16
21
  //#endregion
17
22
  export { constantTimeEquals };
18
23
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../src/utils/index.ts"],"sourcesContent":["/**\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 function 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 result |= a.charCodeAt(i) ^ b.charCodeAt(i);\n }\n\n return result === 0;\n}\n"],"mappings":";;;;;;;;;AAQA,SAAgB,mBAAmB,GAAW,GAAoB;AAChE,KAAI,EAAE,WAAW,EAAE,OACjB,QAAO;CAGT,IAAI,SAAS;AACb,MAAK,IAAI,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK,EACjC,WAAU,EAAE,WAAW,EAAE,GAAG,EAAE,WAAW,EAAE;AAG7C,QAAO,WAAW"}
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"}
package/dist/verify.d.mts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { VerifyFn } from "./types/index.mjs";
2
-
3
2
  //#region src/verify.d.ts
4
3
  declare const HMAC_HASH: {
5
4
  readonly sha1: "SHA-1";
@@ -9,46 +8,42 @@ declare const HMAC_HASH: {
9
8
  };
10
9
  type HmacAlgorithm = keyof typeof HMAC_HASH;
11
10
  /**
12
- * Creates a webhook verifier that validates an HMAC signature from a request header.
13
- *
14
- * The verifier imports the provided string secret once, computes an HMAC from
15
- * `req.rawBody`, normalizes the incoming header value, and compares both
16
- * signatures in constant time.
17
- *
18
- * Header values like `sha256=<hex>` are supported so common provider formats
19
- * such as GitHub work without extra parsing.
20
- *
21
- * @example
22
- * ```ts
23
- * import { createWebhookRouter } from "@zap-studio/webhooks";
24
- * import { createHmacVerifier } from "@zap-studio/webhooks/verify";
25
- *
26
- * const router = createWebhookRouter({
27
- * verify: createHmacVerifier({
28
- * headerName: "x-hub-signature-256",
29
- * secret: process.env.GITHUB_WEBHOOK_SECRET!,
30
- * }),
31
- * });
32
- * ```
33
- *
34
- * @param options - Verifier configuration.
35
- * @param options.headerName - Header containing the provider signature.
36
- * @param options.secret - Shared HMAC secret as a string.
37
- * @param options.algo - HMAC hash algorithm. Defaults to `"sha256"`.
38
- * @returns A router-compatible request verifier.
39
- *
40
- * @throws {VerificationError}
41
- * Thrown when verifier setup fails or request verification does not pass.
42
- */
43
- declare function createHmacVerifier({
44
- headerName,
45
- secret,
46
- algo
47
- }: {
11
+ * Creates a webhook verifier that validates an HMAC signature from a request header.
12
+ *
13
+ * The verifier imports the provided string secret once, computes an HMAC from
14
+ * `req.rawBody`, normalizes the incoming header value, and compares both
15
+ * signatures in constant time.
16
+ *
17
+ * Header values like `sha256=<hex>` are supported so common provider formats
18
+ * such as GitHub work without extra parsing.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * import { createWebhookRouter } from "@zap-studio/webhooks";
23
+ * import { createHmacVerifier } from "@zap-studio/webhooks/verify";
24
+ *
25
+ * const router = createWebhookRouter({
26
+ * verify: createHmacVerifier({
27
+ * headerName: "x-hub-signature-256",
28
+ * secret: process.env.GITHUB_WEBHOOK_SECRET!,
29
+ * }),
30
+ * });
31
+ * ```
32
+ *
33
+ * @param options - Verifier configuration.
34
+ * @param options.headerName - Header containing the provider signature.
35
+ * @param options.secret - Shared HMAC secret as a string.
36
+ * @param options.algo - HMAC hash algorithm. Defaults to `"sha256"`.
37
+ * @returns A router-compatible request verifier.
38
+ *
39
+ * @throws {VerificationError}
40
+ * Thrown when verifier setup fails or request verification does not pass.
41
+ */
42
+ declare const createHmacVerifier: ({ headerName, secret, algo }: {
48
43
  headerName: string;
49
44
  secret: string;
50
45
  algo?: HmacAlgorithm;
51
- }): VerifyFn;
46
+ }) => VerifyFn;
52
47
  //#endregion
53
48
  export { createHmacVerifier };
54
49
  //# sourceMappingURL=verify.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"verify.d.mts","names":[],"sources":["../src/verify.ts"],"mappings":";;;cAIM,SAAA;EAAA,SACJ,IAAA;EAAA,SACA,MAAA;EAAA,SACA,MAAA;EAAA,SACA,MAAA;AAAA;AAAA,KAGG,aAAA,gBAA6B,SAAA;;;;;;AAHhC;;;;;AAqCF;;;;;;;;;;;;;;;;;;;;;;iBAAgB,kBAAA,CAAA;EACd,UAAA;EACA,MAAA;EACA;AAAA;EAEA,UAAA;EACA,MAAA;EACA,IAAA,GAAO,aAAA;AAAA,IACL,QAAA"}
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"}
package/dist/verify.mjs CHANGED
@@ -1,12 +1,19 @@
1
1
  import { VerificationError } from "./errors.mjs";
2
2
  import { constantTimeEquals } from "./utils/index.mjs";
3
3
  //#region src/verify.ts
4
+ /**
5
+ * Signature verification helpers for webhook requests.
6
+ *
7
+ * @module @zap-studio/webhooks/verify
8
+ */
4
9
  const HMAC_HASH = {
5
10
  sha1: "SHA-1",
6
11
  sha256: "SHA-256",
7
12
  sha384: "SHA-384",
8
13
  sha512: "SHA-512"
9
14
  };
15
+ const toHex = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
16
+ const normalizeSignature = (signature) => signature.replace(/^[a-z0-9-]+=/iu, "").trim().toLowerCase();
10
17
  /**
11
18
  * Creates a webhook verifier that validates an HMAC signature from a request header.
12
19
  *
@@ -39,29 +46,23 @@ const HMAC_HASH = {
39
46
  * @throws {VerificationError}
40
47
  * Thrown when verifier setup fails or request verification does not pass.
41
48
  */
42
- function createHmacVerifier({ headerName, secret, algo = "sha256" }) {
43
- const subtle = globalThis.crypto?.subtle;
44
- if (!subtle) throw new VerificationError("Web Crypto API is unavailable in this runtime");
49
+ const createHmacVerifier = ({ headerName, secret, algo = "sha256" }) => {
50
+ if (globalThis.crypto?.subtle === void 0) throw new VerificationError("Web Crypto API is unavailable in this runtime");
51
+ const { subtle } = globalThis.crypto;
45
52
  const hash = HMAC_HASH[algo];
46
53
  if (!hash) throw new VerificationError(`Unsupported HMAC algorithm: ${algo}`);
47
54
  const keyPromise = subtle.importKey("raw", new TextEncoder().encode(secret), {
48
- name: "HMAC",
49
- hash
55
+ hash,
56
+ name: "HMAC"
50
57
  }, false, ["sign"]);
51
58
  return async (req) => {
52
59
  const actual = req.headers.get(headerName);
53
- if (!actual) throw new VerificationError(`Missing signature header: ${headerName}`);
60
+ if (actual === null || actual.length === 0) throw new VerificationError(`Missing signature header: ${headerName}`);
54
61
  const key = await keyPromise;
55
- const signature = await subtle.sign("HMAC", key, req.rawBody);
62
+ const signature = await subtle.sign("HMAC", key, new Uint8Array(req.rawBody));
56
63
  if (!constantTimeEquals(toHex(new Uint8Array(signature)), normalizeSignature(actual))) throw new VerificationError(`Invalid signature for header: ${headerName}`);
57
64
  };
58
- }
59
- function toHex(bytes) {
60
- return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
61
- }
62
- function normalizeSignature(signature) {
63
- return signature.replace(/^[a-z0-9-]+=/i, "").trim().toLowerCase();
64
- }
65
+ };
65
66
  //#endregion
66
67
  export { createHmacVerifier };
67
68
 
@@ -1 +1 @@
1
- {"version":3,"file":"verify.mjs","names":[],"sources":["../src/verify.ts"],"sourcesContent":["import { 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\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 function createHmacVerifier({\n headerName,\n secret,\n algo = \"sha256\",\n}: {\n headerName: string;\n secret: string;\n algo?: HmacAlgorithm;\n}): VerifyFn {\n const subtle = globalThis.crypto?.subtle;\n if (!subtle) {\n throw new VerificationError(\"Web Crypto API is unavailable in this runtime\");\n }\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 { name: \"HMAC\", hash },\n false,\n [\"sign\"],\n );\n\n return async (req) => {\n const actual = req.headers.get(headerName);\n if (!actual) {\n throw new VerificationError(`Missing signature header: ${headerName}`);\n }\n\n const key = await keyPromise;\n const signature = await subtle.sign(\"HMAC\", key, req.rawBody as BufferSource);\n const expected = toHex(new Uint8Array(signature));\n\n if (!constantTimeEquals(expected, normalizeSignature(actual))) {\n throw new VerificationError(`Invalid signature for header: ${headerName}`);\n }\n };\n}\n\nfunction toHex(bytes: Uint8Array): string {\n return Array.from(bytes, (byte) => byte.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\nfunction normalizeSignature(signature: string): string {\n return signature\n .replace(/^[a-z0-9-]+=/i, \"\")\n .trim()\n .toLowerCase();\n}\n"],"mappings":";;;AAIA,MAAM,YAAY;CAChB,MAAM;CACN,QAAQ;CACR,QAAQ;CACR,QAAQ;CACT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCD,SAAgB,mBAAmB,EACjC,YACA,QACA,OAAO,YAKI;CACX,MAAM,SAAS,WAAW,QAAQ;AAClC,KAAI,CAAC,OACH,OAAM,IAAI,kBAAkB,gDAAgD;CAG9E,MAAM,OAAO,UAAU;AACvB,KAAI,CAAC,KACH,OAAM,IAAI,kBAAkB,+BAA+B,OAAO;CAGpE,MAAM,aAAa,OAAO,UACxB,OACA,IAAI,aAAa,CAAC,OAAO,OAAO,EAChC;EAAE,MAAM;EAAQ;EAAM,EACtB,OACA,CAAC,OAAO,CACT;AAED,QAAO,OAAO,QAAQ;EACpB,MAAM,SAAS,IAAI,QAAQ,IAAI,WAAW;AAC1C,MAAI,CAAC,OACH,OAAM,IAAI,kBAAkB,6BAA6B,aAAa;EAGxE,MAAM,MAAM,MAAM;EAClB,MAAM,YAAY,MAAM,OAAO,KAAK,QAAQ,KAAK,IAAI,QAAwB;AAG7E,MAAI,CAAC,mBAFY,MAAM,IAAI,WAAW,UAAU,CAAC,EAEf,mBAAmB,OAAO,CAAC,CAC3D,OAAM,IAAI,kBAAkB,iCAAiC,aAAa;;;AAKhF,SAAS,MAAM,OAA2B;AACxC,QAAO,MAAM,KAAK,QAAQ,SAAS,KAAK,SAAS,GAAG,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC,KAAK,GAAG;;AAGjF,SAAS,mBAAmB,WAA2B;AACrD,QAAO,UACJ,QAAQ,iBAAiB,GAAG,CAC5B,MAAM,CACN,aAAa"}
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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zap-studio/webhooks",
3
- "version": "0.2.1",
3
+ "version": "0.3.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": [
@@ -47,20 +47,19 @@
47
47
  "access": "public"
48
48
  },
49
49
  "dependencies": {
50
- "@zap-studio/validation": "0.3.3"
50
+ "@zap-studio/validation": "0.3.5"
51
51
  },
52
52
  "devDependencies": {
53
- "typescript": "^6.0.3",
54
- "vite-plus": "^0.1.19",
55
- "zod": "^4.3.6",
53
+ "tsdown": "^0.22.4",
54
+ "typescript": "^7.0.2",
55
+ "vitest": "^4.1.10",
56
+ "zod": "^4.4.3",
56
57
  "@zap-studio/typescript": "0.0.0"
57
58
  },
58
59
  "engines": {
59
60
  "node": ">=18.0.0"
60
61
  },
61
62
  "scripts": {
62
- "build": "vp pack",
63
- "test": "vp test run",
64
- "test:watch": "vp test watch"
63
+ "build": "tsdown --config ./tsdown.config.ts"
65
64
  }
66
65
  }