@zap-studio/webhooks 0.2.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/LICENSE +1 -1
- package/README.md +93 -61
- package/dist/errors.d.ts +23 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/{errors.mjs → errors.js} +11 -1
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +5 -0
- package/dist/router.d.ts +80 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +149 -0
- package/dist/router.js.map +1 -0
- package/dist/types.d.ts +93 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +0 -0
- package/dist/utils.d.ts +18 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/{utils/index.mjs → utils.js} +9 -4
- package/dist/utils.js.map +1 -0
- package/dist/verify.d.ts +49 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/{verify.mjs → verify.js} +23 -21
- package/dist/verify.js.map +1 -0
- package/package.json +15 -19
- package/dist/adapters/base.d.mts +0 -58
- package/dist/adapters/base.d.mts.map +0 -1
- package/dist/adapters/base.mjs +0 -26
- package/dist/adapters/base.mjs.map +0 -1
- package/dist/errors.d.mts +0 -13
- package/dist/errors.d.mts.map +0 -1
- package/dist/errors.mjs.map +0 -1
- package/dist/index.d.mts +0 -70
- package/dist/index.d.mts.map +0 -1
- package/dist/index.mjs +0 -159
- package/dist/index.mjs.map +0 -1
- package/dist/types/index.d.mts +0 -93
- package/dist/types/index.d.mts.map +0 -1
- package/dist/types/index.mjs +0 -1
- package/dist/utils/index.d.mts +0 -13
- package/dist/utils/index.d.mts.map +0 -1
- package/dist/utils/index.mjs.map +0 -1
- package/dist/verify.d.mts +0 -54
- package/dist/verify.d.mts.map +0 -1
- package/dist/verify.mjs.map +0 -1
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { StandardSchemaV1 } from "@zap-studio/validation";
|
|
2
|
+
//#region src/types.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Context shared by hooks, verifiers, and handlers for a single webhook request.
|
|
5
|
+
*
|
|
6
|
+
* The router consumes the request body exactly once, so `request.body` is
|
|
7
|
+
* already used by the time hooks or handlers run — read `rawBody` instead.
|
|
8
|
+
*/
|
|
9
|
+
interface WebhookContext {
|
|
10
|
+
/** The matched route key registered on the router (e.g. "stripe") */
|
|
11
|
+
path: string;
|
|
12
|
+
/** The exact request body bytes (for signature verification) */
|
|
13
|
+
rawBody: Uint8Array;
|
|
14
|
+
/** The incoming Web API request (body already consumed by the router) */
|
|
15
|
+
request: Request;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Handler context extending the shared webhook context with the validated payload.
|
|
19
|
+
*
|
|
20
|
+
* @template TPayload - Validated payload type for the matched route.
|
|
21
|
+
*/
|
|
22
|
+
interface HandlerContext<TPayload = unknown> extends WebhookContext {
|
|
23
|
+
/** The validated webhook payload */
|
|
24
|
+
payload: TPayload;
|
|
25
|
+
}
|
|
26
|
+
/** Route registration options for a webhook handler. */
|
|
27
|
+
interface RegisterOptions<T> {
|
|
28
|
+
/** Hooks that run after successful processing (before global after hooks) */
|
|
29
|
+
after?: AfterHook | AfterHook[];
|
|
30
|
+
/** Hooks that run before request processing (after global before hooks) */
|
|
31
|
+
before?: BeforeHook | BeforeHook[];
|
|
32
|
+
/** The handler function to process the webhook */
|
|
33
|
+
handler: WebhookHandler<T>;
|
|
34
|
+
/** Optional Standard Schema validator to validate the webhook payload */
|
|
35
|
+
schema?: StandardSchemaV1<unknown, T>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Infers the output type from a Standard Schema instance.
|
|
39
|
+
*
|
|
40
|
+
* @template TSchema - A Standard Schema type.
|
|
41
|
+
*/
|
|
42
|
+
type InferSchemaOutput<TSchema> = TSchema extends StandardSchemaV1<unknown, infer TOutput> ? TOutput : never;
|
|
43
|
+
/**
|
|
44
|
+
* Route options where schema is required and handler payload is inferred.
|
|
45
|
+
*
|
|
46
|
+
* @template TSchema - Schema used to infer handler payload type.
|
|
47
|
+
*/
|
|
48
|
+
type SchemaRouteOptions<TSchema extends StandardSchemaV1<unknown, unknown>> = Omit<RegisterOptions<InferSchemaOutput<TSchema>>, "schema"> & {
|
|
49
|
+
schema: TSchema;
|
|
50
|
+
};
|
|
51
|
+
interface RouteLike {
|
|
52
|
+
after?: AfterHook | AfterHook[];
|
|
53
|
+
before?: BeforeHook | BeforeHook[];
|
|
54
|
+
handler: WebhookHandler;
|
|
55
|
+
schema: StandardSchemaV1<unknown, unknown>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Applies schema-driven payload inference to each route entry.
|
|
59
|
+
*
|
|
60
|
+
* @template TRoutes - Route dictionary keyed by webhook path.
|
|
61
|
+
*/
|
|
62
|
+
type SchemaRoutes<TRoutes extends Record<string, RouteLike>> = { [P in keyof TRoutes]: SchemaRouteOptions<TRoutes[P]["schema"]>; };
|
|
63
|
+
/**
|
|
64
|
+
* The webhook handler function, responsible for processing incoming webhook events.
|
|
65
|
+
*
|
|
66
|
+
* Return a `Response` to control the reply, or `undefined` to let the router
|
|
67
|
+
* respond with its default `200` acknowledgement.
|
|
68
|
+
*/
|
|
69
|
+
type WebhookHandler<TPayload = unknown> = (ctx: HandlerContext<TPayload>) => Promise<Response | undefined> | Response | undefined;
|
|
70
|
+
/** Maps route keys to their payload-specific webhook handlers. */
|
|
71
|
+
type HandlerMap<TMap extends Record<string, unknown>> = { [P in keyof TMap]: WebhookHandler<TMap[P]>; };
|
|
72
|
+
/**
|
|
73
|
+
* Builds a webhook payload map from a schema-based route dictionary.
|
|
74
|
+
*
|
|
75
|
+
* @template TRoutes - Route dictionary keyed by webhook path.
|
|
76
|
+
*/
|
|
77
|
+
type InferWebhookMapFromRoutes<TRoutes extends Record<string, RouteLike>> = { [P in keyof TRoutes]: InferSchemaOutput<TRoutes[P]["schema"]>; };
|
|
78
|
+
/** Verification function for incoming requests. Throws to reject the request. */
|
|
79
|
+
type VerifyFn = (ctx: WebhookContext) => Promise<void> | void;
|
|
80
|
+
/** Hook function that runs before request processing */
|
|
81
|
+
type BeforeHook = (ctx: WebhookContext) => Promise<void> | void;
|
|
82
|
+
/**
|
|
83
|
+
* Hook function that runs after successful request processing.
|
|
84
|
+
*
|
|
85
|
+
* The hook receives the outgoing response as-is; call `response.clone()`
|
|
86
|
+
* before reading its body to avoid consuming the stream sent to the client.
|
|
87
|
+
*/
|
|
88
|
+
type AfterHook = (ctx: WebhookContext, response: Response) => Promise<void> | void;
|
|
89
|
+
/** Hook function that runs when an error occurs */
|
|
90
|
+
type ErrorHook = (error: Error, ctx: WebhookContext) => Promise<Response | undefined> | Response | undefined;
|
|
91
|
+
//#endregion
|
|
92
|
+
export { AfterHook, BeforeHook, ErrorHook, HandlerContext, HandlerMap, InferSchemaOutput, InferWebhookMapFromRoutes, RegisterOptions, SchemaRouteOptions, SchemaRoutes, VerifyFn, WebhookContext, WebhookHandler };
|
|
93
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;;;;UAciB;;EAEf;;EAEA,SAAS;;EAET,SAAS;;;;;;;UAQM,eAAe,4BAA4B;;EAE1D,SAAS;;;UAIM,gBAAgB;;EAE/B,QAAQ,YAAY;;EAEpB,SAAS,aAAa;;EAEtB,SAAS,eAAe;;EAExB,SAAS,0BAA0B;;;;;;;KAQzB,kBAAkB,WAC5B,gBAAgB,gCAAgC,WAAW;;;;;;KAOjD,mBACV,gBAAgB,sCACd,KAAK,gBAAgB,kBAAkB;EACzC,QAAQ;;UAGA;EACR,QAAQ,YAAY;EACpB,SAAS,aAAa;EACtB,SAAS;EACT,QAAQ;;;;;;;KAQE,aAAa,gBAAgB,eAAe,iBACrD,WAAW,UAAU,mBAAmB,QAAQ;;;;;;;KASvC,eAAe,uBACzB,KAAK,eAAe,cACjB,QAAQ,wBAAwB;;KAGzB,WAAW,aAAa,8BACjC,WAAW,OAAO,eAAe,KAAK;;;;;;KAQ7B,0BACV,gBAAgB,eAAe,iBAE9B,WAAW,UAAU,kBAAkB,QAAQ;;KAItC,YAAY,KAAK,mBAAmB;;KAGpC,cAAc,KAAK,mBAAmB;;;;;;;KAQtC,aACV,KAAK,gBACL,UAAU,aACP;;KAGO,aACV,OAAO,OACP,KAAK,mBACF,QAAQ,wBAAwB"}
|
package/dist/types.js
ADDED
|
File without changes
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/utils.d.ts
|
|
2
|
+
/**
|
|
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;
|
|
16
|
+
//#endregion
|
|
17
|
+
export { constantTimeEquals };
|
|
18
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","names":[],"sources":["../src/utils.ts"],"mappings":";;;;;;;;;;;;;;cAca,qBAAsB,WAAW"}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
//#region src/utils
|
|
1
|
+
//#region src/utils.ts
|
|
2
|
+
/**
|
|
3
|
+
* Utility helpers for webhook internals.
|
|
4
|
+
*
|
|
5
|
+
* @module @zap-studio/webhooks/utils
|
|
6
|
+
*/
|
|
2
7
|
/**
|
|
3
8
|
* Compares two strings in constant time to prevent timing attacks.
|
|
4
9
|
*
|
|
@@ -7,13 +12,13 @@
|
|
|
7
12
|
* const isEqual = constantTimeEquals("string1", "string2"); // returns false
|
|
8
13
|
* ```
|
|
9
14
|
*/
|
|
10
|
-
|
|
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
|
|
|
19
|
-
//# sourceMappingURL=
|
|
24
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","names":[],"sources":["../src/utils.ts"],"sourcesContent":["/**\n * Utility helpers for webhook internals.\n *\n * @module @zap-studio/webhooks/utils\n */\n\n/**\n * Compares two strings in constant time to prevent timing attacks.\n *\n * @example\n * ```ts\n * const isEqual = constantTimeEquals(\"string1\", \"string2\"); // returns false\n * ```\n */\nexport const constantTimeEquals = (a: string, b: string): boolean => {\n if (a.length !== b.length) {\n return false;\n }\n\n let result = 0;\n for (let i = 0; i < a.length; i += 1) {\n // oxlint-disable-next-line no-bitwise, unicorn/prefer-code-point -- XOR is the constant-time compare trick; charCodeAt reads each char with the same cost, and inputs are plain ASCII hex.\n result |= a.charCodeAt(i) ^ b.charCodeAt(i);\n }\n\n return result === 0;\n};\n"],"mappings":";;;;;;;;;;;;;;AAcA,MAAa,sBAAsB,GAAW,MAAuB;CACnE,IAAI,EAAE,WAAW,EAAE,QACjB,OAAO;CAGT,IAAI,SAAS;CACb,KAAK,IAAI,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK,GAEjC,UAAU,EAAE,WAAW,CAAC,IAAI,EAAE,WAAW,CAAC;CAG5C,OAAO,WAAW;AACpB"}
|
package/dist/verify.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { VerifyFn } from "./types.js";
|
|
2
|
+
//#region src/verify.d.ts
|
|
3
|
+
declare const HMAC_HASH: {
|
|
4
|
+
readonly sha1: "SHA-1";
|
|
5
|
+
readonly sha256: "SHA-256";
|
|
6
|
+
readonly sha384: "SHA-384";
|
|
7
|
+
readonly sha512: "SHA-512";
|
|
8
|
+
};
|
|
9
|
+
type HmacAlgorithm = keyof typeof HMAC_HASH;
|
|
10
|
+
/**
|
|
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
|
+
* `ctx.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 }: {
|
|
43
|
+
headerName: string;
|
|
44
|
+
secret: string;
|
|
45
|
+
algo?: HmacAlgorithm;
|
|
46
|
+
}) => VerifyFn;
|
|
47
|
+
//#endregion
|
|
48
|
+
export { createHmacVerifier };
|
|
49
|
+
//# sourceMappingURL=verify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.d.ts","names":[],"sources":["../src/verify.ts"],"mappings":";;cAUM;WACJ;WACA;WACA;WACA;;KAGG,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA2CrB,uBACX,YACA,QACA;EAEA;EACA;EACA,OAAO;MACL"}
|
|
@@ -1,17 +1,24 @@
|
|
|
1
|
-
import { VerificationError } from "./errors.
|
|
2
|
-
import { constantTimeEquals } from "./utils
|
|
1
|
+
import { VerificationError } from "./errors.js";
|
|
2
|
+
import { constantTimeEquals } from "./utils.js";
|
|
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
|
*
|
|
13
20
|
* The verifier imports the provided string secret once, computes an HMAC from
|
|
14
|
-
* `
|
|
21
|
+
* `ctx.rawBody`, normalizes the incoming header value, and compares both
|
|
15
22
|
* signatures in constant time.
|
|
16
23
|
*
|
|
17
24
|
* Header values like `sha256=<hex>` are supported so common provider formats
|
|
@@ -39,30 +46,25 @@ const HMAC_HASH = {
|
|
|
39
46
|
* @throws {VerificationError}
|
|
40
47
|
* Thrown when verifier setup fails or request verification does not pass.
|
|
41
48
|
*/
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
49
|
-
|
|
55
|
+
hash,
|
|
56
|
+
name: "HMAC"
|
|
50
57
|
}, false, ["sign"]);
|
|
51
|
-
return async (
|
|
52
|
-
const actual =
|
|
53
|
-
if (
|
|
58
|
+
return async (ctx) => {
|
|
59
|
+
const actual = ctx.request.headers.get(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,
|
|
56
|
-
|
|
62
|
+
const signature = await subtle.sign("HMAC", key, new Uint8Array(ctx.rawBody));
|
|
63
|
+
const expected = toHex(new Uint8Array(signature));
|
|
64
|
+
if (!constantTimeEquals(expected, normalizeSignature(actual))) throw new VerificationError(`Invalid signature for header: ${headerName}`);
|
|
57
65
|
};
|
|
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
|
-
}
|
|
66
|
+
};
|
|
65
67
|
//#endregion
|
|
66
68
|
export { createHmacVerifier };
|
|
67
69
|
|
|
68
|
-
//# sourceMappingURL=verify.
|
|
70
|
+
//# sourceMappingURL=verify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.js","names":[],"sources":["../src/verify.ts"],"sourcesContent":["/**\n * Signature verification helpers for webhook requests.\n *\n * @module @zap-studio/webhooks/verify\n */\n\nimport { VerificationError } from \"./errors.js\";\nimport type { VerifyFn } from \"./types.js\";\nimport { constantTimeEquals } from \"./utils.js\";\n\nconst HMAC_HASH = {\n sha1: \"SHA-1\",\n sha256: \"SHA-256\",\n sha384: \"SHA-384\",\n sha512: \"SHA-512\",\n} as const;\n\ntype HmacAlgorithm = keyof typeof HMAC_HASH;\n\nconst toHex = (bytes: Uint8Array): string =>\n Array.from(bytes, (byte) => byte.toString(16).padStart(2, \"0\")).join(\"\");\n\nconst normalizeSignature = (signature: string): string =>\n signature\n .replace(/^[a-z0-9-]+=/iu, \"\")\n .trim()\n .toLowerCase();\n\n/**\n * Creates a webhook verifier that validates an HMAC signature from a request header.\n *\n * The verifier imports the provided string secret once, computes an HMAC from\n * `ctx.rawBody`, normalizes the incoming header value, and compares both\n * signatures in constant time.\n *\n * Header values like `sha256=<hex>` are supported so common provider formats\n * such as GitHub work without extra parsing.\n *\n * @example\n * ```ts\n * import { createWebhookRouter } from \"@zap-studio/webhooks\";\n * import { createHmacVerifier } from \"@zap-studio/webhooks/verify\";\n *\n * const router = createWebhookRouter({\n * verify: createHmacVerifier({\n * headerName: \"x-hub-signature-256\",\n * secret: process.env.GITHUB_WEBHOOK_SECRET!,\n * }),\n * });\n * ```\n *\n * @param options - Verifier configuration.\n * @param options.headerName - Header containing the provider signature.\n * @param options.secret - Shared HMAC secret as a string.\n * @param options.algo - HMAC hash algorithm. Defaults to `\"sha256\"`.\n * @returns A router-compatible request verifier.\n *\n * @throws {VerificationError}\n * Thrown when verifier setup fails or request verification does not pass.\n */\nexport const createHmacVerifier = ({\n headerName,\n secret,\n algo = \"sha256\",\n}: {\n headerName: string;\n secret: string;\n algo?: HmacAlgorithm;\n}): VerifyFn => {\n if (globalThis.crypto?.subtle === undefined) {\n throw new VerificationError(\n \"Web Crypto API is unavailable in this runtime\"\n );\n }\n\n const { subtle } = globalThis.crypto;\n\n const hash = HMAC_HASH[algo];\n if (!hash) {\n throw new VerificationError(`Unsupported HMAC algorithm: ${algo}`);\n }\n\n const keyPromise = subtle.importKey(\n \"raw\",\n new TextEncoder().encode(secret),\n { hash, name: \"HMAC\" },\n false,\n [\"sign\"]\n );\n\n return async (ctx) => {\n const actual = ctx.request.headers.get(headerName);\n if (actual === null || actual.length === 0) {\n throw new VerificationError(`Missing signature header: ${headerName}`);\n }\n\n const key = await keyPromise;\n const signature = await subtle.sign(\n \"HMAC\",\n key,\n new Uint8Array(ctx.rawBody)\n );\n const expected = toHex(new Uint8Array(signature));\n\n if (!constantTimeEquals(expected, normalizeSignature(actual))) {\n throw new VerificationError(\n `Invalid signature for header: ${headerName}`\n );\n }\n };\n};\n"],"mappings":";;;;;;;;AAUA,MAAM,YAAY;CAChB,MAAM;CACN,QAAQ;CACR,QAAQ;CACR,QAAQ;AACV;AAIA,MAAM,SAAS,UACb,MAAM,KAAK,QAAQ,SAAS,KAAK,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE;AAEzE,MAAM,sBAAsB,cAC1B,UACG,QAAQ,kBAAkB,EAAE,CAAC,CAC7B,KAAK,CAAC,CACN,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCjB,MAAa,sBAAsB,EACjC,YACA,QACA,OAAO,eAKO;CACd,IAAI,WAAW,QAAQ,WAAW,KAAA,GAChC,MAAM,IAAI,kBACR,+CACF;CAGF,MAAM,EAAE,WAAW,WAAW;CAE9B,MAAM,OAAO,UAAU;CACvB,IAAI,CAAC,MACH,MAAM,IAAI,kBAAkB,+BAA+B,MAAM;CAGnE,MAAM,aAAa,OAAO,UACxB,OACA,IAAI,YAAY,CAAC,CAAC,OAAO,MAAM,GAC/B;EAAE;EAAM,MAAM;CAAO,GACrB,OACA,CAAC,MAAM,CACT;CAEA,OAAO,OAAO,QAAQ;EACpB,MAAM,SAAS,IAAI,QAAQ,QAAQ,IAAI,UAAU;EACjD,IAAI,WAAW,QAAQ,OAAO,WAAW,GACvC,MAAM,IAAI,kBAAkB,6BAA6B,YAAY;EAGvE,MAAM,MAAM,MAAM;EAClB,MAAM,YAAY,MAAM,OAAO,KAC7B,QACA,KACA,IAAI,WAAW,IAAI,OAAO,CAC5B;EACA,MAAM,WAAW,MAAM,IAAI,WAAW,SAAS,CAAC;EAEhD,IAAI,CAAC,mBAAmB,UAAU,mBAAmB,MAAM,CAAC,GAC1D,MAAM,IAAI,kBACR,iCAAiC,YACnC;CAEJ;AACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zap-studio/webhooks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A lightweight, type-safe webhook router with Standard Schema validation, signature verification, and lifecycle hooks.",
|
|
6
6
|
"keywords": [
|
|
@@ -33,34 +33,30 @@
|
|
|
33
33
|
],
|
|
34
34
|
"type": "module",
|
|
35
35
|
"sideEffects": false,
|
|
36
|
-
"types": "./dist/index.d.
|
|
36
|
+
"types": "./dist/index.d.ts",
|
|
37
37
|
"exports": {
|
|
38
|
-
".": "./dist/index.
|
|
39
|
-
"./
|
|
40
|
-
"./
|
|
41
|
-
"./types": "./dist/types
|
|
42
|
-
"./utils": "./dist/utils
|
|
43
|
-
"./verify": "./dist/verify.
|
|
38
|
+
".": "./dist/index.js",
|
|
39
|
+
"./errors": "./dist/errors.js",
|
|
40
|
+
"./router": "./dist/router.js",
|
|
41
|
+
"./types": "./dist/types.js",
|
|
42
|
+
"./utils": "./dist/utils.js",
|
|
43
|
+
"./verify": "./dist/verify.js",
|
|
44
44
|
"./package.json": "./package.json"
|
|
45
45
|
},
|
|
46
46
|
"publishConfig": {
|
|
47
47
|
"access": "public"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@zap-studio/validation": "
|
|
50
|
+
"@zap-studio/validation": "workspace:*"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"typescript": "
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
53
|
+
"@zap-studio/typescript": "workspace:*",
|
|
54
|
+
"tsdown": "catalog:",
|
|
55
|
+
"typescript": "catalog:",
|
|
56
|
+
"vitest": "catalog:",
|
|
57
|
+
"zod": "catalog:"
|
|
57
58
|
},
|
|
58
59
|
"engines": {
|
|
59
60
|
"node": ">=18.0.0"
|
|
60
|
-
},
|
|
61
|
-
"scripts": {
|
|
62
|
-
"build": "vp pack",
|
|
63
|
-
"test": "vp test run",
|
|
64
|
-
"test:watch": "vp test watch"
|
|
65
61
|
}
|
|
66
|
-
}
|
|
62
|
+
}
|
package/dist/adapters/base.d.mts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { NormalizedRequest, NormalizedResponse } from "../types/index.mjs";
|
|
2
|
-
|
|
3
|
-
//#region src/adapters/base.d.ts
|
|
4
|
-
/**
|
|
5
|
-
* Minimal framework adapter contract.
|
|
6
|
-
*
|
|
7
|
-
* Implement this when integrating the webhook router with an HTTP framework.
|
|
8
|
-
*/
|
|
9
|
-
interface Adapter {
|
|
10
|
-
/**
|
|
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>;
|
|
19
|
-
/**
|
|
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>;
|
|
26
|
-
/**
|
|
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>;
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
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 {
|
|
42
|
-
/** @inheritdoc */
|
|
43
|
-
abstract toNormalizedRequest<TReq = unknown>(req: TReq): Promise<NormalizedRequest>;
|
|
44
|
-
/** @inheritdoc */
|
|
45
|
-
abstract toFrameworkResponse<TFrameworkRes = unknown>(frameworkRes: TFrameworkRes, res: NormalizedResponse): Promise<TFrameworkRes>;
|
|
46
|
-
/**
|
|
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>;
|
|
55
|
-
}
|
|
56
|
-
//#endregion
|
|
57
|
-
export { Adapter, BaseAdapter };
|
|
58
|
-
//# sourceMappingURL=base.d.mts.map
|
|
@@ -1 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
//#region src/adapters/base.ts
|
|
2
|
-
/**
|
|
3
|
-
* Base adapter helper.
|
|
4
|
-
*
|
|
5
|
-
* Extend this class in consumers to keep framework integration boilerplate
|
|
6
|
-
* in one place while relying on the package router contract.
|
|
7
|
-
*/
|
|
8
|
-
var BaseAdapter = class {
|
|
9
|
-
/**
|
|
10
|
-
* Shared adapter pipeline implementation.
|
|
11
|
-
*
|
|
12
|
-
* Most consumers only need to implement request/response mapping methods and
|
|
13
|
-
* can reuse this default orchestration.
|
|
14
|
-
*/
|
|
15
|
-
handleWebhook(router) {
|
|
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
|
-
}
|
|
22
|
-
};
|
|
23
|
-
//#endregion
|
|
24
|
-
export { BaseAdapter };
|
|
25
|
-
|
|
26
|
-
//# sourceMappingURL=base.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
package/dist/errors.d.mts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
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
|
package/dist/errors.d.mts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
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.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { AfterHook, BeforeHook, ErrorHook, InferSchemaOutput, NormalizedRequest, NormalizedResponse, RegisterOptions, SchemaRouteOptions, WebhookHandler } from "./types/index.mjs";
|
|
2
|
-
import { StandardSchemaV1 } from "@zap-studio/validation";
|
|
3
|
-
|
|
4
|
-
//#region src/index.d.ts
|
|
5
|
-
interface WebhookRouterOptions {
|
|
6
|
-
/** Global hooks executed after successful route handler completion. */
|
|
7
|
-
after?: AfterHook | AfterHook[];
|
|
8
|
-
/** Global hooks executed before route-level hooks and verification. */
|
|
9
|
-
before?: BeforeHook | BeforeHook[];
|
|
10
|
-
/** Global error hook used to override the default `500` response. */
|
|
11
|
-
onError?: ErrorHook;
|
|
12
|
-
/** Required path prefix for all webhook routes. Defaults to `"/webhooks/"`. */
|
|
13
|
-
prefix?: string;
|
|
14
|
-
/** Optional request verification function (for signature checks, auth, etc.). */
|
|
15
|
-
verify?: (req: NormalizedRequest) => Promise<void> | void;
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Main webhook router class.
|
|
19
|
-
*
|
|
20
|
-
* Register routes with typed schemas and call `handle` with a normalized request.
|
|
21
|
-
*/
|
|
22
|
-
declare class WebhookRouter<TMap = unknown> {
|
|
23
|
-
private readonly handlers;
|
|
24
|
-
private readonly verify;
|
|
25
|
-
private readonly globalBeforeHooks;
|
|
26
|
-
private readonly globalAfterHooks;
|
|
27
|
-
private readonly globalErrorHook;
|
|
28
|
-
private readonly prefix;
|
|
29
|
-
constructor(opts?: WebhookRouterOptions);
|
|
30
|
-
/**
|
|
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
|
-
register<Path extends string, TSchema extends StandardSchemaV1<unknown, unknown>>(path: Path, handlerOrOptions: SchemaRouteOptions<TSchema>): WebhookRouter<TMap & Record<Path, InferSchemaOutput<TSchema>>>;
|
|
40
|
-
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>>;
|
|
42
|
-
/**
|
|
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
|
-
handle(req: NormalizedRequest): Promise<NormalizedResponse>;
|
|
49
|
-
private normalizePath;
|
|
50
|
-
private runGlobalBeforeHooks;
|
|
51
|
-
private createHandlerEntry;
|
|
52
|
-
private runRouteBeforeHooks;
|
|
53
|
-
private parseRequestBody;
|
|
54
|
-
private isErrorResponse;
|
|
55
|
-
private validatePayload;
|
|
56
|
-
private executeHandler;
|
|
57
|
-
private runRouteAfterHooks;
|
|
58
|
-
private runGlobalAfterHooks;
|
|
59
|
-
private handleError;
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
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;
|
|
68
|
-
//#endregion
|
|
69
|
-
export { WebhookRouter, WebhookRouterOptions, createWebhookRouter };
|
|
70
|
-
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
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"}
|