@zap-studio/webhooks 0.1.3 → 0.2.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 +15 -0
- package/LICENSE +1 -1
- package/README.md +19 -13
- package/bin/intent.js +4 -0
- package/dist/adapters/base.d.mts +32 -32
- package/dist/adapters/base.d.mts.map +1 -0
- package/dist/adapters/base.mjs +3 -2
- package/dist/adapters/base.mjs.map +1 -0
- package/dist/errors.d.mts +13 -0
- package/dist/errors.d.mts.map +1 -0
- package/dist/errors.mjs +17 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/index.d.mts +25 -24
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +17 -15
- package/dist/index.mjs.map +1 -0
- package/dist/types/index.d.mts +20 -19
- package/dist/types/index.d.mts.map +1 -0
- package/dist/types/index.mjs +1 -1
- package/dist/utils/index.d.mts +9 -8
- package/dist/utils/index.d.mts.map +1 -0
- package/dist/utils/index.mjs +3 -2
- package/dist/utils/index.mjs.map +1 -0
- package/dist/verify.d.mts +42 -37
- package/dist/verify.d.mts.map +1 -0
- package/dist/verify.mjs +43 -22
- package/dist/verify.mjs.map +1 -0
- package/package.json +44 -44
- package/skills/zap-webhooks-routing-and-verification/SKILL.md +172 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @zap-studio/webhooks
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- c686862: Switch `createHmacVerifier` to Web Crypto and standardize the verifier around string secrets.
|
|
8
|
+
|
|
9
|
+
This change removes the Node `crypto` dependency from the verifier path, keeps `req.rawBody` as `Uint8Array`, simplifies `createHmacVerifier` to take a string secret, and adds public `VerificationError` in `@zap-studio/webhooks/errors` for verifier setup and signature failures.
|
|
10
|
+
|
|
11
|
+
## 0.1.4
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- e26293e: Updated dependencies.
|
|
16
|
+
- @zap-studio/validation@0.3.2
|
|
17
|
+
|
|
3
18
|
## 0.1.3
|
|
4
19
|
|
|
5
20
|
### Patch Changes
|
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @zap-studio/webhooks
|
|
2
2
|
|
|
3
|
-
Schema-first, type-safe webhook routing with signature verification support.
|
|
3
|
+
Schema-first, type-safe webhook routing with runtime-agnostic signature verification support.
|
|
4
4
|
|
|
5
5
|
Works with any validation library that implements [Standard Schema](https://github.com/standard-schema/standard-schema), including Zod, Valibot, and ArkType.
|
|
6
6
|
|
|
@@ -90,11 +90,7 @@ const router = createWebhookRouter({
|
|
|
90
90
|
throw new Error("Missing Stripe signature");
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
stripe.webhooks.constructEvent(
|
|
94
|
-
req.rawBody,
|
|
95
|
-
signature,
|
|
96
|
-
process.env.STRIPE_WEBHOOK_SECRET!
|
|
97
|
-
);
|
|
93
|
+
stripe.webhooks.constructEvent(req.rawBody, signature, process.env.STRIPE_WEBHOOK_SECRET!);
|
|
98
94
|
},
|
|
99
95
|
});
|
|
100
96
|
|
|
@@ -138,18 +134,33 @@ const router = createWebhookRouter({
|
|
|
138
134
|
|
|
139
135
|
`@zap-studio/webhooks/verify` exports `createHmacVerifier`, a small helper that builds a `verify` function for HMAC-signed webhook providers.
|
|
140
136
|
|
|
137
|
+
It does not depend on Node APIs. The verifier uses the Web Crypto API, so it works in any runtime that provides `globalThis.crypto.subtle`.
|
|
138
|
+
|
|
141
139
|
- reads a signature from the header you choose
|
|
142
140
|
- computes an HMAC from `req.rawBody`
|
|
143
141
|
- compares signatures in constant time
|
|
142
|
+
- uses the Web Crypto API instead of Node `crypto`
|
|
143
|
+
- works across runtimes that provide `globalThis.crypto.subtle`
|
|
144
|
+
- expects a string secret
|
|
145
|
+
- throws `VerificationError` on verifier setup or signature failures
|
|
144
146
|
|
|
145
147
|
```ts
|
|
146
148
|
import { createHmacVerifier } from "@zap-studio/webhooks/verify";
|
|
149
|
+
import { VerificationError } from "@zap-studio/webhooks/errors";
|
|
147
150
|
|
|
148
151
|
const verify = createHmacVerifier({
|
|
149
152
|
headerName: "x-hub-signature-256",
|
|
150
153
|
secret: process.env.WEBHOOK_SECRET!,
|
|
151
154
|
algo: "sha256", // optional, defaults to sha256
|
|
152
155
|
});
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
await verify(req);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (error instanceof VerificationError) {
|
|
161
|
+
console.error("webhook verification failed", error.message);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
153
164
|
```
|
|
154
165
|
|
|
155
166
|
Use this when your provider uses standard HMAC signatures. For providers with custom signing formats, pass your own `verify` function.
|
|
@@ -178,15 +189,10 @@ class MyHttpAdapter extends BaseAdapter {
|
|
|
178
189
|
};
|
|
179
190
|
}
|
|
180
191
|
|
|
181
|
-
async toFrameworkResponse(
|
|
182
|
-
res: any,
|
|
183
|
-
normalized: NormalizedResponse
|
|
184
|
-
): Promise<any> {
|
|
192
|
+
async toFrameworkResponse(res: any, normalized: NormalizedResponse): Promise<any> {
|
|
185
193
|
res.statusCode = normalized.status;
|
|
186
194
|
res.end(
|
|
187
|
-
typeof normalized.body === "string"
|
|
188
|
-
? normalized.body
|
|
189
|
-
: JSON.stringify(normalized.body)
|
|
195
|
+
typeof normalized.body === "string" ? normalized.body : JSON.stringify(normalized.body),
|
|
190
196
|
);
|
|
191
197
|
return res;
|
|
192
198
|
}
|
package/bin/intent.js
ADDED
package/dist/adapters/base.d.mts
CHANGED
|
@@ -1,58 +1,58 @@
|
|
|
1
1
|
import { NormalizedRequest, NormalizedResponse } from "../types/index.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/adapters/base.d.ts
|
|
4
|
-
|
|
5
4
|
/**
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
* Minimal framework adapter contract.
|
|
6
|
+
*
|
|
7
|
+
* Implement this when integrating the webhook router with an HTTP framework.
|
|
8
|
+
*/
|
|
10
9
|
interface Adapter {
|
|
11
10
|
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
+
*/
|
|
17
16
|
handleWebhook<TFrameworkReq = unknown, TFrameworkRes = unknown>(router: {
|
|
18
17
|
handle(req: NormalizedRequest): Promise<NormalizedResponse>;
|
|
19
18
|
}): (req: TFrameworkReq, res: TFrameworkRes) => Promise<void>;
|
|
20
19
|
/**
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
+
*/
|
|
26
25
|
toFrameworkResponse<TFrameworkRes = unknown>(frameworkRes: TFrameworkRes, res: NormalizedResponse): Promise<TFrameworkRes>;
|
|
27
26
|
/**
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
+
*/
|
|
34
33
|
toNormalizedRequest<TReq = unknown>(req: TReq): Promise<NormalizedRequest>;
|
|
35
34
|
}
|
|
36
35
|
/**
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
+
*/
|
|
42
41
|
declare abstract class BaseAdapter implements Adapter {
|
|
43
42
|
/** @inheritdoc */
|
|
44
43
|
abstract toNormalizedRequest<TReq = unknown>(req: TReq): Promise<NormalizedRequest>;
|
|
45
44
|
/** @inheritdoc */
|
|
46
45
|
abstract toFrameworkResponse<TFrameworkRes = unknown>(frameworkRes: TFrameworkRes, res: NormalizedResponse): Promise<TFrameworkRes>;
|
|
47
46
|
/**
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
+
*/
|
|
53
52
|
handleWebhook<TFrameworkReq = unknown, TFrameworkRes = unknown>(router: {
|
|
54
53
|
handle(req: NormalizedRequest): Promise<NormalizedResponse>;
|
|
55
54
|
}): (req: TFrameworkReq, res: TFrameworkRes) => Promise<void>;
|
|
56
55
|
}
|
|
57
56
|
//#endregion
|
|
58
|
-
export { Adapter, BaseAdapter };
|
|
57
|
+
export { Adapter, BaseAdapter };
|
|
58
|
+
//# sourceMappingURL=base.d.mts.map
|
|
@@ -0,0 +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"}
|
package/dist/adapters/base.mjs
CHANGED
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/errors.d.ts
|
|
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
|
+
*/
|
|
8
|
+
declare class VerificationError extends Error {
|
|
9
|
+
constructor(message: string);
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
export { VerificationError };
|
|
13
|
+
//# sourceMappingURL=errors.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.mts","names":[],"sources":["../src/errors.ts"],"mappings":";;AAMA;;;;;cAAa,iBAAA,SAA0B,KAAA;EACrC,WAAA,CAAY,OAAA;AAAA"}
|
package/dist/errors.mjs
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region src/errors.ts
|
|
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
|
+
*/
|
|
8
|
+
var VerificationError = class extends Error {
|
|
9
|
+
constructor(message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "VerificationError";
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
//#endregion
|
|
15
|
+
export { VerificationError };
|
|
16
|
+
|
|
17
|
+
//# sourceMappingURL=errors.mjs.map
|
|
@@ -0,0 +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"}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AfterHook, BeforeHook, ErrorHook, InferSchemaOutput, NormalizedRequest, NormalizedResponse, RegisterOptions, SchemaRouteOptions, WebhookHandler } from "./types/index.mjs";
|
|
2
|
-
import { StandardSchemaV1 } from "@
|
|
2
|
+
import { StandardSchemaV1 } from "@zap-studio/validation";
|
|
3
3
|
|
|
4
4
|
//#region src/index.d.ts
|
|
5
5
|
interface WebhookRouterOptions {
|
|
@@ -15,10 +15,10 @@ interface WebhookRouterOptions {
|
|
|
15
15
|
verify?: (req: NormalizedRequest) => Promise<void> | void;
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
* Main webhook router class.
|
|
19
|
+
*
|
|
20
|
+
* Register routes with typed schemas and call `handle` with a normalized request.
|
|
21
|
+
*/
|
|
22
22
|
declare class WebhookRouter<TMap = unknown> {
|
|
23
23
|
private readonly handlers;
|
|
24
24
|
private readonly verify?;
|
|
@@ -28,23 +28,23 @@ declare class WebhookRouter<TMap = unknown> {
|
|
|
28
28
|
private readonly prefix;
|
|
29
29
|
constructor(opts?: WebhookRouterOptions);
|
|
30
30
|
/**
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
+
*/
|
|
39
39
|
register<Path extends string, TSchema extends StandardSchemaV1<unknown, unknown>>(path: Path, handlerOrOptions: SchemaRouteOptions<TSchema>): WebhookRouter<TMap & Record<Path, InferSchemaOutput<TSchema>>>;
|
|
40
40
|
register<Path extends string, TPayload>(path: Path, handlerOrOptions: RegisterOptions<TPayload>): WebhookRouter<TMap & Record<Path, TPayload>>;
|
|
41
41
|
register<Path extends string>(path: Path, handlerOrOptions: WebhookHandler<unknown>): WebhookRouter<TMap & Record<Path, unknown>>;
|
|
42
42
|
/**
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
* Handles a normalized incoming webhook request.
|
|
44
|
+
*
|
|
45
|
+
* @param req - Normalized request object.
|
|
46
|
+
* @returns Normalized response for the adapter/framework layer.
|
|
47
|
+
*/
|
|
48
48
|
handle(req: NormalizedRequest): Promise<NormalizedResponse>;
|
|
49
49
|
private normalizePath;
|
|
50
50
|
private runGlobalBeforeHooks;
|
|
@@ -58,11 +58,12 @@ declare class WebhookRouter<TMap = unknown> {
|
|
|
58
58
|
private handleError;
|
|
59
59
|
}
|
|
60
60
|
/**
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
61
|
+
* Factory helper for creating a webhook router instance.
|
|
62
|
+
*
|
|
63
|
+
* @param opts - Optional global router options.
|
|
64
|
+
* @returns A new webhook router.
|
|
65
|
+
*/
|
|
66
66
|
declare function createWebhookRouter(opts?: WebhookRouterOptions): WebhookRouter;
|
|
67
67
|
//#endregion
|
|
68
|
-
export { WebhookRouter, WebhookRouterOptions, createWebhookRouter };
|
|
68
|
+
export { WebhookRouter, WebhookRouterOptions, createWebhookRouter };
|
|
69
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +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;;;;;;cAQ1B,aAAA;EAAA,iBACM,QAAA;EAAA,iBACA,MAAA;EAAA,iBACA,iBAAA;EAAA,iBACA,gBAAA;EAAA,iBACA,eAAA;EAAA,iBACA,MAAA;EAEjB,WAAA,CAAY,IAAA,GAAO,oBAAA;;;;;AARrB;;;;;EAkCE,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;;;;;;;EAkD/B,MAAA,CAAa,GAAA,EAAK,iBAAA,GAAoB,OAAA,CAAQ,kBAAA;EAAA,QAsCtC,aAAA;EAAA,QA0BM,oBAAA;EAAA,QAMA,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"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { standardValidate } from "@zap-studio/validation";
|
|
2
|
-
|
|
3
2
|
//#region src/index.ts
|
|
4
3
|
/**
|
|
5
4
|
* Main webhook router class.
|
|
@@ -27,12 +26,11 @@ var WebhookRouter = class {
|
|
|
27
26
|
if (handlerOrOptions.before) beforeHooks = Array.isArray(handlerOrOptions.before) ? handlerOrOptions.before : [handlerOrOptions.before];
|
|
28
27
|
let afterHooks;
|
|
29
28
|
if (handlerOrOptions.after) afterHooks = Array.isArray(handlerOrOptions.after) ? handlerOrOptions.after : [handlerOrOptions.after];
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
};
|
|
29
|
+
const entry = { handler: handlerOrOptions.handler };
|
|
30
|
+
if (handlerOrOptions.schema !== void 0) entry.schema = handlerOrOptions.schema;
|
|
31
|
+
if (beforeHooks !== void 0) entry.before = beforeHooks;
|
|
32
|
+
if (afterHooks !== void 0) entry.after = afterHooks;
|
|
33
|
+
this.handlers[path] = entry;
|
|
36
34
|
}
|
|
37
35
|
return this;
|
|
38
36
|
}
|
|
@@ -86,7 +84,7 @@ var WebhookRouter = class {
|
|
|
86
84
|
}
|
|
87
85
|
parseRequestBody(req) {
|
|
88
86
|
try {
|
|
89
|
-
const parsed = JSON.parse(req.rawBody
|
|
87
|
+
const parsed = JSON.parse(new TextDecoder().decode(req.rawBody));
|
|
90
88
|
req.json = parsed;
|
|
91
89
|
return parsed;
|
|
92
90
|
} catch {
|
|
@@ -115,11 +113,14 @@ var WebhookRouter = class {
|
|
|
115
113
|
return await handler({
|
|
116
114
|
req,
|
|
117
115
|
payload: validatedPayload,
|
|
118
|
-
ack: async (r) =>
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
116
|
+
ack: async (r) => {
|
|
117
|
+
const response = {
|
|
118
|
+
status: r?.status ?? 200,
|
|
119
|
+
body: r?.body ?? "ok"
|
|
120
|
+
};
|
|
121
|
+
if (r?.headers !== void 0) response.headers = r.headers;
|
|
122
|
+
return response;
|
|
123
|
+
}
|
|
123
124
|
}) ?? {
|
|
124
125
|
status: 200,
|
|
125
126
|
body: "ok"
|
|
@@ -151,6 +152,7 @@ var WebhookRouter = class {
|
|
|
151
152
|
function createWebhookRouter(opts) {
|
|
152
153
|
return new WebhookRouter(opts);
|
|
153
154
|
}
|
|
154
|
-
|
|
155
155
|
//#endregion
|
|
156
|
-
export { WebhookRouter, createWebhookRouter };
|
|
156
|
+
export { WebhookRouter, createWebhookRouter };
|
|
157
|
+
|
|
158
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +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\";\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 * @typeParam TMap - Internal route payload map built incrementally via `register`.\n */\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\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;\n private readonly globalBeforeHooks: BeforeHook[] = [];\n private readonly globalAfterHooks: AfterHook[] = [];\n private readonly globalErrorHook?: ErrorHook;\n private readonly prefix: string;\n\n constructor(opts?: WebhookRouterOptions) {\n this.prefix = opts?.prefix ?? \"/webhooks/\";\n\n if (opts?.verify) {\n this.verify = opts.verify;\n }\n if (opts?.before) {\n this.globalBeforeHooks = Array.isArray(opts.before) ? opts.before : [opts.before];\n }\n if (opts?.after) {\n this.globalAfterHooks = Array.isArray(opts.after) ? opts.after : [opts.after];\n }\n if (opts?.onError) {\n this.globalErrorHook = opts.onError;\n }\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] = {\n handler: handlerOrOptions,\n };\n } else {\n let beforeHooks: BeforeHook[] | undefined;\n if (handlerOrOptions.before) {\n beforeHooks = Array.isArray(handlerOrOptions.before)\n ? handlerOrOptions.before\n : [handlerOrOptions.before];\n }\n\n let afterHooks: AfterHook[] | undefined;\n if (handlerOrOptions.after) {\n afterHooks = Array.isArray(handlerOrOptions.after)\n ? handlerOrOptions.after\n : [handlerOrOptions.after];\n }\n\n const entry: HandlerEntry<unknown> = {\n handler: handlerOrOptions.handler,\n };\n\n if (handlerOrOptions.schema !== undefined) {\n entry.schema = handlerOrOptions.schema;\n }\n if (beforeHooks !== undefined) {\n entry.before = beforeHooks;\n }\n if (afterHooks !== undefined) {\n entry.after = afterHooks;\n }\n\n this.handlers[path] = entry;\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 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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"}
|
package/dist/types/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { StandardSchemaV1 } from "@
|
|
1
|
+
import { StandardSchemaV1 } from "@zap-studio/validation";
|
|
2
2
|
|
|
3
3
|
//#region src/types/index.d.ts
|
|
4
4
|
/** Framework-agnostic request shape consumed by the webhook router. */
|
|
@@ -16,7 +16,7 @@ interface NormalizedRequest {
|
|
|
16
16
|
/** The query parameters of the request */
|
|
17
17
|
query?: Record<string, string | string[]>;
|
|
18
18
|
/** The raw body of the request (for signature) */
|
|
19
|
-
rawBody:
|
|
19
|
+
rawBody: Uint8Array<ArrayBufferLike>;
|
|
20
20
|
/** The parsed text body of the request if applicable */
|
|
21
21
|
text?: string;
|
|
22
22
|
}
|
|
@@ -41,16 +41,16 @@ interface RegisterOptions<T> {
|
|
|
41
41
|
schema?: StandardSchemaV1<unknown, T>;
|
|
42
42
|
}
|
|
43
43
|
/**
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
* Infers the output type from a Standard Schema instance.
|
|
45
|
+
*
|
|
46
|
+
* @typeParam TSchema - A Standard Schema type.
|
|
47
|
+
*/
|
|
48
48
|
type InferSchemaOutput<TSchema> = TSchema extends StandardSchemaV1<unknown, infer TOutput> ? TOutput : never;
|
|
49
49
|
/**
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
* Route options where schema is required and handler payload is inferred.
|
|
51
|
+
*
|
|
52
|
+
* @typeParam TSchema - Schema used to infer handler payload type.
|
|
53
|
+
*/
|
|
54
54
|
type SchemaRouteOptions<TSchema extends StandardSchemaV1<unknown, unknown>> = Omit<RegisterOptions<InferSchemaOutput<TSchema>>, "schema"> & {
|
|
55
55
|
schema: TSchema;
|
|
56
56
|
};
|
|
@@ -61,10 +61,10 @@ interface RouteLike {
|
|
|
61
61
|
schema: StandardSchemaV1<unknown, unknown>;
|
|
62
62
|
}
|
|
63
63
|
/**
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
* Applies schema-driven payload inference to each route entry.
|
|
65
|
+
*
|
|
66
|
+
* @typeParam TRoutes - Route dictionary keyed by webhook path.
|
|
67
|
+
*/
|
|
68
68
|
type SchemaRoutes<TRoutes extends Record<string, RouteLike>> = { [P in keyof TRoutes]: SchemaRouteOptions<TRoutes[P]["schema"]> };
|
|
69
69
|
/** The webhook handler function, responsible for processing incoming webhook events. */
|
|
70
70
|
type WebhookHandler<TPayload = unknown> = (ctx: {
|
|
@@ -75,10 +75,10 @@ type WebhookHandler<TPayload = unknown> = (ctx: {
|
|
|
75
75
|
/** Maps route keys to their payload-specific webhook handlers. */
|
|
76
76
|
type HandlerMap<TMap extends Record<string, unknown>> = { [P in keyof TMap]: WebhookHandler<TMap[P]> };
|
|
77
77
|
/**
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
* Builds a webhook payload map from a schema-based route dictionary.
|
|
79
|
+
*
|
|
80
|
+
* @typeParam TRoutes - Route dictionary keyed by webhook path.
|
|
81
|
+
*/
|
|
82
82
|
type InferWebhookMapFromRoutes<TRoutes extends Record<string, RouteLike>> = { [P in keyof TRoutes]: InferSchemaOutput<TRoutes[P]["schema"]> };
|
|
83
83
|
/** Verification function for incoming requests */
|
|
84
84
|
type VerifyFn = (req: NormalizedRequest) => Promise<void> | void;
|
|
@@ -89,4 +89,5 @@ type AfterHook = (req: NormalizedRequest, res: NormalizedResponse) => Promise<vo
|
|
|
89
89
|
/** Hook function that runs when an error occurs */
|
|
90
90
|
type ErrorHook = (error: Error, req: NormalizedRequest) => Promise<NormalizedResponse | undefined> | NormalizedResponse | undefined;
|
|
91
91
|
//#endregion
|
|
92
|
-
export { AfterHook, BeforeHook, ErrorHook, HandlerMap, InferSchemaOutput, InferWebhookMapFromRoutes, NormalizedRequest, NormalizedResponse, RegisterOptions, SchemaRouteOptions, SchemaRoutes, VerifyFn, WebhookHandler };
|
|
92
|
+
export { AfterHook, BeforeHook, ErrorHook, HandlerMap, InferSchemaOutput, InferWebhookMapFromRoutes, NormalizedRequest, NormalizedResponse, RegisterOptions, SchemaRouteOptions, SchemaRoutes, VerifyFn, WebhookHandler };
|
|
93
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +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"}
|
package/dist/types/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {};
|
package/dist/utils/index.d.mts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
//#region src/utils/index.d.ts
|
|
2
2
|
/**
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
10
|
declare function constantTimeEquals(a: string, b: string): boolean;
|
|
11
11
|
//#endregion
|
|
12
|
-
export { constantTimeEquals };
|
|
12
|
+
export { constantTimeEquals };
|
|
13
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/utils/index.ts"],"mappings":";;AAQA;;;;;;;iBAAgB,kBAAA,CAAmB,CAAA,UAAW,CAAA"}
|
package/dist/utils/index.mjs
CHANGED
|
@@ -13,6 +13,7 @@ function constantTimeEquals(a, b) {
|
|
|
13
13
|
for (let i = 0; i < a.length; i += 1) result |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
14
14
|
return result === 0;
|
|
15
15
|
}
|
|
16
|
-
|
|
17
16
|
//#endregion
|
|
18
|
-
export { constantTimeEquals };
|
|
17
|
+
export { constantTimeEquals };
|
|
18
|
+
|
|
19
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +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"}
|
package/dist/verify.d.mts
CHANGED
|
@@ -1,49 +1,54 @@
|
|
|
1
1
|
import { VerifyFn } from "./types/index.mjs";
|
|
2
|
-
import { BinaryLike, KeyObject } from "node:crypto";
|
|
3
2
|
|
|
4
3
|
//#region src/verify.d.ts
|
|
5
|
-
|
|
4
|
+
declare const HMAC_HASH: {
|
|
5
|
+
readonly sha1: "SHA-1";
|
|
6
|
+
readonly sha256: "SHA-256";
|
|
7
|
+
readonly sha384: "SHA-384";
|
|
8
|
+
readonly sha512: "SHA-512";
|
|
9
|
+
};
|
|
10
|
+
type HmacAlgorithm = keyof typeof HMAC_HASH;
|
|
6
11
|
/**
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
*/
|
|
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
|
+
*/
|
|
39
43
|
declare function createHmacVerifier({
|
|
40
44
|
headerName,
|
|
41
45
|
secret,
|
|
42
46
|
algo
|
|
43
47
|
}: {
|
|
44
48
|
headerName: string;
|
|
45
|
-
secret:
|
|
46
|
-
algo?:
|
|
49
|
+
secret: string;
|
|
50
|
+
algo?: HmacAlgorithm;
|
|
47
51
|
}): VerifyFn;
|
|
48
52
|
//#endregion
|
|
49
|
-
export { createHmacVerifier };
|
|
53
|
+
export { createHmacVerifier };
|
|
54
|
+
//# sourceMappingURL=verify.d.mts.map
|
|
@@ -0,0 +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"}
|
package/dist/verify.mjs
CHANGED
|
@@ -1,20 +1,26 @@
|
|
|
1
|
+
import { VerificationError } from "./errors.mjs";
|
|
1
2
|
import { constantTimeEquals } from "./utils/index.mjs";
|
|
2
|
-
|
|
3
3
|
//#region src/verify.ts
|
|
4
|
-
const
|
|
4
|
+
const HMAC_HASH = {
|
|
5
|
+
sha1: "SHA-1",
|
|
6
|
+
sha256: "SHA-256",
|
|
7
|
+
sha384: "SHA-384",
|
|
8
|
+
sha512: "SHA-512"
|
|
9
|
+
};
|
|
5
10
|
/**
|
|
6
|
-
* Creates an HMAC
|
|
11
|
+
* Creates a webhook verifier that validates an HMAC signature from a request header.
|
|
7
12
|
*
|
|
8
|
-
* The
|
|
9
|
-
*
|
|
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.
|
|
10
16
|
*
|
|
11
|
-
*
|
|
17
|
+
* Header values like `sha256=<hex>` are supported so common provider formats
|
|
18
|
+
* such as GitHub work without extra parsing.
|
|
12
19
|
*
|
|
13
20
|
* @example
|
|
14
21
|
* ```ts
|
|
15
22
|
* import { createWebhookRouter } from "@zap-studio/webhooks";
|
|
16
23
|
* import { createHmacVerifier } from "@zap-studio/webhooks/verify";
|
|
17
|
-
* import { z } from "zod";
|
|
18
24
|
*
|
|
19
25
|
* const router = createWebhookRouter({
|
|
20
26
|
* verify: createHmacVerifier({
|
|
@@ -22,26 +28,41 @@ const SIGNATURE_REGEX = /^sha256=/;
|
|
|
22
28
|
* secret: process.env.GITHUB_WEBHOOK_SECRET!,
|
|
23
29
|
* }),
|
|
24
30
|
* });
|
|
25
|
-
*
|
|
26
|
-
* router.register("github/push", {
|
|
27
|
-
* schema: z.object({ ref: z.string() }),
|
|
28
|
-
* handler: async ({ ack }) => ack(),
|
|
29
|
-
* });
|
|
30
31
|
* ```
|
|
31
32
|
*
|
|
32
|
-
* @param options -
|
|
33
|
-
* @param options.headerName - Header containing provider signature.
|
|
34
|
-
* @param options.secret - HMAC secret
|
|
35
|
-
* @param options.algo -
|
|
36
|
-
* @returns A
|
|
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.
|
|
37
41
|
*/
|
|
38
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");
|
|
45
|
+
const hash = HMAC_HASH[algo];
|
|
46
|
+
if (!hash) throw new VerificationError(`Unsupported HMAC algorithm: ${algo}`);
|
|
47
|
+
const keyPromise = subtle.importKey("raw", new TextEncoder().encode(secret), {
|
|
48
|
+
name: "HMAC",
|
|
49
|
+
hash
|
|
50
|
+
}, false, ["sign"]);
|
|
39
51
|
return async (req) => {
|
|
40
|
-
const
|
|
41
|
-
if (!
|
|
42
|
-
|
|
52
|
+
const actual = req.headers.get(headerName);
|
|
53
|
+
if (!actual) throw new VerificationError(`Missing signature header: ${headerName}`);
|
|
54
|
+
const key = await keyPromise;
|
|
55
|
+
const signature = await subtle.sign("HMAC", key, req.rawBody);
|
|
56
|
+
if (!constantTimeEquals(toHex(new Uint8Array(signature)), normalizeSignature(actual))) throw new VerificationError(`Invalid signature for header: ${headerName}`);
|
|
43
57
|
};
|
|
44
58
|
}
|
|
45
|
-
|
|
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
|
+
}
|
|
46
65
|
//#endregion
|
|
47
|
-
export { createHmacVerifier };
|
|
66
|
+
export { createHmacVerifier };
|
|
67
|
+
|
|
68
|
+
//# sourceMappingURL=verify.mjs.map
|
|
@@ -0,0 +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"}
|
package/package.json
CHANGED
|
@@ -1,70 +1,70 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zap-studio/webhooks",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"type": "module",
|
|
5
|
-
"license": "MIT",
|
|
3
|
+
"version": "0.2.0",
|
|
6
4
|
"private": false,
|
|
7
|
-
"homepage": "https://www.zapstudio.dev/packages/webhooks",
|
|
8
|
-
"repository": {
|
|
9
|
-
"type": "git",
|
|
10
|
-
"url": "https://github.com/zap-studio/monorepo.git",
|
|
11
|
-
"directory": "packages/webhooks"
|
|
12
|
-
},
|
|
13
5
|
"description": "A lightweight, type-safe webhook router with Standard Schema validation, signature verification, and lifecycle hooks.",
|
|
14
6
|
"keywords": [
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"typescript",
|
|
19
|
-
"validation",
|
|
7
|
+
"arktype",
|
|
8
|
+
"lifecycle hooks",
|
|
9
|
+
"payload validation",
|
|
20
10
|
"schema",
|
|
11
|
+
"signature verification",
|
|
21
12
|
"standard schema",
|
|
22
|
-
"
|
|
13
|
+
"type-safe",
|
|
14
|
+
"typescript",
|
|
23
15
|
"valibot",
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
16
|
+
"validation",
|
|
17
|
+
"webhook router",
|
|
18
|
+
"webhooks",
|
|
19
|
+
"zod"
|
|
28
20
|
],
|
|
29
|
-
"
|
|
30
|
-
|
|
21
|
+
"homepage": "https://www.zapstudio.dev/packages/webhooks",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/zap-studio/monorepo.git",
|
|
26
|
+
"directory": "packages/webhooks"
|
|
27
|
+
},
|
|
28
|
+
"bin": {
|
|
29
|
+
"intent": "./bin/intent.js"
|
|
31
30
|
},
|
|
32
31
|
"files": [
|
|
33
32
|
"dist",
|
|
34
33
|
"CHANGELOG.md",
|
|
35
34
|
"LICENSE",
|
|
36
|
-
"README.md"
|
|
35
|
+
"README.md",
|
|
36
|
+
"skills",
|
|
37
|
+
"bin",
|
|
38
|
+
"!skills/_artifacts"
|
|
37
39
|
],
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
},
|
|
42
|
-
"devDependencies": {
|
|
43
|
-
"@types/node": "^25.0.2",
|
|
44
|
-
"@vitest/coverage-v8": "^4.0.15",
|
|
45
|
-
"tsdown": "^0.18.0",
|
|
46
|
-
"typescript": "^5.9.3",
|
|
47
|
-
"vitest": "^4.0.18",
|
|
48
|
-
"zod": "^4.2.0",
|
|
49
|
-
"@zap-studio/tsdown-config": "0.0.0",
|
|
50
|
-
"@zap-studio/vitest-config": "0.0.0",
|
|
51
|
-
"@zap-studio/typescript-config": "0.0.0"
|
|
52
|
-
},
|
|
40
|
+
"type": "module",
|
|
41
|
+
"sideEffects": false,
|
|
42
|
+
"types": "./dist/index.d.mts",
|
|
53
43
|
"exports": {
|
|
54
44
|
".": "./dist/index.mjs",
|
|
55
45
|
"./adapters/base": "./dist/adapters/base.mjs",
|
|
46
|
+
"./errors": "./dist/errors.mjs",
|
|
56
47
|
"./types": "./dist/types/index.mjs",
|
|
57
48
|
"./utils": "./dist/utils/index.mjs",
|
|
58
49
|
"./verify": "./dist/verify.mjs",
|
|
59
50
|
"./package.json": "./package.json"
|
|
60
51
|
},
|
|
61
|
-
"
|
|
62
|
-
|
|
63
|
-
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public"
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@zap-studio/validation": "0.3.2"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@types/node": "^25.5.0",
|
|
60
|
+
"typescript": "^5.9.3",
|
|
61
|
+
"vite-plus": "latest",
|
|
62
|
+
"zod": "^4.2.0",
|
|
63
|
+
"@zap-studio/typescript": "0.0.0"
|
|
64
|
+
},
|
|
64
65
|
"scripts": {
|
|
65
|
-
"build": "
|
|
66
|
-
"
|
|
67
|
-
"test": "
|
|
68
|
-
"test:watch": "vitest --watch"
|
|
66
|
+
"build": "vp pack",
|
|
67
|
+
"test": "vp test run",
|
|
68
|
+
"test:watch": "vp test watch"
|
|
69
69
|
}
|
|
70
70
|
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: zap-webhooks-routing-and-verification
|
|
3
|
+
description: >
|
|
4
|
+
Build webhook ingestion with @zap-studio/webhooks using createWebhookRouter,
|
|
5
|
+
register path keys, prefix normalization, schema validation, lifecycle hooks,
|
|
6
|
+
createHmacVerifier, and BaseAdapter request/response mapping.
|
|
7
|
+
type: core
|
|
8
|
+
library: "@zap-studio/webhooks"
|
|
9
|
+
library_version: "0.1.3"
|
|
10
|
+
sources:
|
|
11
|
+
- "zap-studio/monorepo:packages/webhooks/README.md"
|
|
12
|
+
- "zap-studio/monorepo:packages/webhooks/src/index.ts"
|
|
13
|
+
- "zap-studio/monorepo:packages/webhooks/src/verify.ts"
|
|
14
|
+
- "zap-studio/monorepo:packages/webhooks/src/adapters/base.ts"
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# @zap-studio/webhooks — Routing and Verification
|
|
18
|
+
|
|
19
|
+
## Setup
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { createWebhookRouter } from "@zap-studio/webhooks";
|
|
23
|
+
import { createHmacVerifier } from "@zap-studio/webhooks/verify";
|
|
24
|
+
import { z } from "zod";
|
|
25
|
+
|
|
26
|
+
const router = createWebhookRouter({
|
|
27
|
+
prefix: "/webhooks/",
|
|
28
|
+
verify: createHmacVerifier({
|
|
29
|
+
headerName: "x-hub-signature-256",
|
|
30
|
+
secret: process.env.WEBHOOK_SECRET!,
|
|
31
|
+
}),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
router.register("github/push", {
|
|
35
|
+
schema: z.object({ ref: z.string() }),
|
|
36
|
+
handler: async ({ payload, ack }) => {
|
|
37
|
+
console.log(payload.ref);
|
|
38
|
+
return ack({ status: 200, body: "ok" });
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Core Patterns
|
|
44
|
+
|
|
45
|
+
### Add global hooks for observability and error shaping
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
const router = createWebhookRouter({
|
|
49
|
+
before: (req) => {
|
|
50
|
+
console.log("incoming", req.path);
|
|
51
|
+
},
|
|
52
|
+
after: (_req, res) => {
|
|
53
|
+
console.log("status", res.status);
|
|
54
|
+
},
|
|
55
|
+
onError: (error) => ({
|
|
56
|
+
status: 500,
|
|
57
|
+
body: { error: error.message },
|
|
58
|
+
}),
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Register route-specific hooks
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
router.register("payments/succeeded", {
|
|
66
|
+
schema: PaymentSchema,
|
|
67
|
+
before: (req) => {
|
|
68
|
+
req.headers.set("x-processed", "1");
|
|
69
|
+
},
|
|
70
|
+
after: (_req, res) => {
|
|
71
|
+
console.log("finished", res.status);
|
|
72
|
+
},
|
|
73
|
+
handler: async ({ payload, ack }) => ack({ body: { id: payload.id } }),
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Implement an adapter with `BaseAdapter`
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import { BaseAdapter } from "@zap-studio/webhooks/adapters/base";
|
|
81
|
+
|
|
82
|
+
class MyAdapter extends BaseAdapter {
|
|
83
|
+
async toNormalizedRequest(req: Request) {
|
|
84
|
+
return {
|
|
85
|
+
method: req.method,
|
|
86
|
+
path: req.url,
|
|
87
|
+
headers: req.headers,
|
|
88
|
+
rawBody: Buffer.from(await req.text()),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async toFrameworkResponse(res: Response, normalized) {
|
|
93
|
+
return new Response(JSON.stringify(normalized.body), {
|
|
94
|
+
status: normalized.status,
|
|
95
|
+
headers: normalized.headers,
|
|
96
|
+
}) as unknown as Response;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Common Mistakes
|
|
102
|
+
|
|
103
|
+
### HIGH Registering paths with leading slash
|
|
104
|
+
|
|
105
|
+
Wrong:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
router.register("/github/push", {
|
|
109
|
+
schema: PushSchema,
|
|
110
|
+
handler,
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Correct:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
router.register("github/push", {
|
|
118
|
+
schema: PushSchema,
|
|
119
|
+
handler,
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Incoming paths normalize to slashless keys; leading slash route keys do not match and return 404.
|
|
124
|
+
|
|
125
|
+
Source: zap-studio/monorepo:packages/webhooks/src/index.ts
|
|
126
|
+
|
|
127
|
+
### CRITICAL Verifying a transformed payload instead of raw body
|
|
128
|
+
|
|
129
|
+
Wrong:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
const parsed = JSON.parse(req.rawBody.toString());
|
|
133
|
+
await verifyProvider(JSON.stringify(parsed));
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Correct:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
await verify(req); // uses req.rawBody bytes
|
|
140
|
+
const parsed = JSON.parse(req.rawBody.toString());
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Signature checks must run on exact raw bytes; any parse/serialize transformation can invalidate signatures.
|
|
144
|
+
|
|
145
|
+
Source: zap-studio/monorepo:packages/webhooks/src/verify.ts
|
|
146
|
+
|
|
147
|
+
### HIGH Assuming `createHmacVerifier` is Node-only
|
|
148
|
+
|
|
149
|
+
Wrong:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
const verify = createHmacVerifier({
|
|
153
|
+
headerName: "x-signature",
|
|
154
|
+
secret: env.WEBHOOK_SECRET,
|
|
155
|
+
});
|
|
156
|
+
// used in edge runtime
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Correct:
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
const verify = createHmacVerifier({
|
|
163
|
+
headerName: "x-signature",
|
|
164
|
+
secret: env.WEBHOOK_SECRET,
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`createHmacVerifier` uses the Web Crypto API and works across runtimes that expose `globalThis.crypto.subtle`.
|
|
169
|
+
|
|
170
|
+
Source: zap-studio/monorepo:packages/webhooks/src/verify.ts
|
|
171
|
+
|
|
172
|
+
See also: zap-validation-standard-schema/SKILL.md — payload validation result handling.
|