@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.
Files changed (45) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +1 -1
  3. package/README.md +93 -61
  4. package/dist/errors.d.ts +23 -0
  5. package/dist/errors.d.ts.map +1 -0
  6. package/dist/{errors.mjs → errors.js} +11 -1
  7. package/dist/errors.js.map +1 -0
  8. package/dist/index.d.ts +6 -0
  9. package/dist/index.js +5 -0
  10. package/dist/router.d.ts +80 -0
  11. package/dist/router.d.ts.map +1 -0
  12. package/dist/router.js +149 -0
  13. package/dist/router.js.map +1 -0
  14. package/dist/types.d.ts +93 -0
  15. package/dist/types.d.ts.map +1 -0
  16. package/dist/types.js +0 -0
  17. package/dist/utils.d.ts +18 -0
  18. package/dist/utils.d.ts.map +1 -0
  19. package/dist/{utils/index.mjs → utils.js} +9 -4
  20. package/dist/utils.js.map +1 -0
  21. package/dist/verify.d.ts +49 -0
  22. package/dist/verify.d.ts.map +1 -0
  23. package/dist/{verify.mjs → verify.js} +23 -21
  24. package/dist/verify.js.map +1 -0
  25. package/package.json +15 -19
  26. package/dist/adapters/base.d.mts +0 -58
  27. package/dist/adapters/base.d.mts.map +0 -1
  28. package/dist/adapters/base.mjs +0 -26
  29. package/dist/adapters/base.mjs.map +0 -1
  30. package/dist/errors.d.mts +0 -13
  31. package/dist/errors.d.mts.map +0 -1
  32. package/dist/errors.mjs.map +0 -1
  33. package/dist/index.d.mts +0 -70
  34. package/dist/index.d.mts.map +0 -1
  35. package/dist/index.mjs +0 -159
  36. package/dist/index.mjs.map +0 -1
  37. package/dist/types/index.d.mts +0 -93
  38. package/dist/types/index.d.mts.map +0 -1
  39. package/dist/types/index.mjs +0 -1
  40. package/dist/utils/index.d.mts +0 -13
  41. package/dist/utils/index.d.mts.map +0 -1
  42. package/dist/utils/index.mjs.map +0 -1
  43. package/dist/verify.d.mts +0 -54
  44. package/dist/verify.d.mts.map +0 -1
  45. package/dist/verify.mjs.map +0 -1
package/dist/index.mjs DELETED
@@ -1,159 +0,0 @@
1
- import { standardValidate } from "@zap-studio/validation";
2
- //#region src/index.ts
3
- function toArray(value) {
4
- if (value === void 0) return [];
5
- return Array.isArray(value) ? value : [value];
6
- }
7
- /**
8
- * Main webhook router class.
9
- *
10
- * Register routes with typed schemas and call `handle` with a normalized request.
11
- */
12
- var WebhookRouter = class {
13
- handlers = {};
14
- verify;
15
- globalBeforeHooks = [];
16
- globalAfterHooks = [];
17
- globalErrorHook;
18
- prefix;
19
- constructor(opts = {}) {
20
- this.prefix = opts.prefix ?? "/webhooks/";
21
- this.verify = opts.verify;
22
- this.globalBeforeHooks = toArray(opts.before);
23
- this.globalAfterHooks = toArray(opts.after);
24
- this.globalErrorHook = opts.onError;
25
- }
26
- register(path, handlerOrOptions) {
27
- if (typeof handlerOrOptions === "function") this.handlers[path] = { handler: handlerOrOptions };
28
- else this.handlers[path] = this.createHandlerEntry(handlerOrOptions);
29
- return this;
30
- }
31
- /**
32
- * Handles a normalized incoming webhook request.
33
- *
34
- * @param req - Normalized request object.
35
- * @returns Normalized response for the adapter/framework layer.
36
- */
37
- async handle(req) {
38
- try {
39
- const normalizedPath = this.normalizePath(req);
40
- if (normalizedPath === null) return {
41
- status: 404,
42
- body: { error: "not found" }
43
- };
44
- const handlerEntry = this.handlers[normalizedPath];
45
- if (!handlerEntry) return {
46
- status: 404,
47
- body: { error: "not found" }
48
- };
49
- await this.runGlobalBeforeHooks(req);
50
- await this.runRouteBeforeHooks(req, handlerEntry.before);
51
- if (this.verify) await this.verify(req);
52
- const parsedJson = this.parseRequestBody(req);
53
- const validationResult = await this.validatePayload(parsedJson, handlerEntry.schema);
54
- if (this.isErrorResponse(validationResult)) return validationResult;
55
- const response = await this.executeHandler(handlerEntry.handler, req, validationResult);
56
- await this.runRouteAfterHooks(req, response, handlerEntry.after);
57
- await this.runGlobalAfterHooks(req, response);
58
- return response;
59
- } catch (error) {
60
- return this.handleError(error, req);
61
- }
62
- }
63
- normalizePath(req) {
64
- let pathname = req.path;
65
- try {
66
- pathname = new URL(req.path).pathname;
67
- } catch {}
68
- if (!pathname.startsWith(this.prefix)) return null;
69
- pathname = pathname.slice(this.prefix.length - 1);
70
- req.path = pathname;
71
- return pathname.startsWith("/") ? pathname.slice(1) : pathname;
72
- }
73
- async runGlobalBeforeHooks(req) {
74
- for (const hook of this.globalBeforeHooks) await hook(req);
75
- }
76
- createHandlerEntry(options) {
77
- const entry = { handler: options.handler };
78
- if (options.schema !== void 0) entry.schema = options.schema;
79
- if (options.before !== void 0) entry.before = Array.isArray(options.before) ? options.before : [options.before];
80
- if (options.after !== void 0) entry.after = Array.isArray(options.after) ? options.after : [options.after];
81
- return entry;
82
- }
83
- async runRouteBeforeHooks(req, before) {
84
- if (before) for (const hook of before) await hook(req);
85
- }
86
- parseRequestBody(req) {
87
- try {
88
- const parsed = JSON.parse(new TextDecoder().decode(req.rawBody));
89
- req.json = parsed;
90
- return parsed;
91
- } catch {
92
- return;
93
- }
94
- }
95
- isErrorResponse(value) {
96
- return typeof value === "object" && value !== null && "status" in value && typeof value.status === "number";
97
- }
98
- async validatePayload(parsedJson, schema) {
99
- if (!schema) return parsedJson;
100
- const result = await standardValidate(schema, parsedJson, { throwOnError: false });
101
- if (result.issues) return {
102
- status: 400,
103
- body: {
104
- error: "validation failed",
105
- issues: result.issues.map((issue) => ({
106
- path: issue.path?.map((p) => typeof p === "object" && "key" in p ? String(p.key) : String(p)),
107
- message: issue.message
108
- }))
109
- }
110
- };
111
- return result.value;
112
- }
113
- async executeHandler(handler, req, validatedPayload) {
114
- return await handler({
115
- req,
116
- payload: validatedPayload,
117
- ack: async (r) => {
118
- const response = {
119
- status: r?.status ?? 200,
120
- body: r?.body ?? "ok"
121
- };
122
- if (r?.headers !== void 0) response.headers = r.headers;
123
- return response;
124
- }
125
- }) ?? {
126
- status: 200,
127
- body: "ok"
128
- };
129
- }
130
- async runRouteAfterHooks(req, response, after) {
131
- if (after) for (const hook of after) await hook(req, response);
132
- }
133
- async runGlobalAfterHooks(req, response) {
134
- for (const hook of this.globalAfterHooks) await hook(req, response);
135
- }
136
- async handleError(error, req) {
137
- if (this.globalErrorHook) {
138
- const errorResponse = await this.globalErrorHook(error, req);
139
- if (errorResponse) return errorResponse;
140
- }
141
- return {
142
- status: 500,
143
- body: { error: error instanceof Error ? error.message : "Internal server error" }
144
- };
145
- }
146
- };
147
- /**
148
- * Factory helper for creating a webhook router instance.
149
- *
150
- * @param opts - Optional global router options.
151
- * @returns A new webhook router.
152
- */
153
- function createWebhookRouter(opts) {
154
- return new WebhookRouter(opts);
155
- }
156
- //#endregion
157
- export { WebhookRouter, createWebhookRouter };
158
-
159
- //# sourceMappingURL=index.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@zap-studio/validation\";\nimport { standardValidate } from \"@zap-studio/validation\";\n\nimport type {\n AfterHook,\n BeforeHook,\n ErrorHook,\n InferSchemaOutput,\n NormalizedRequest,\n NormalizedResponse,\n RegisterOptions,\n SchemaRouteOptions,\n WebhookHandler,\n} from \"./types/index.js\";\n\n/**\n * Schema-first webhook router with path dispatching, validation, and optional verification.\n *\n * @template TMap - Internal route payload map built incrementally via `register`.\n */\ninterface HandlerEntry<TPayload = unknown> {\n after?: AfterHook[];\n before?: BeforeHook[];\n handler: WebhookHandler<TPayload>;\n schema?: StandardSchemaV1<unknown, TPayload>;\n}\n\ntype HandlerStore = Record<string, HandlerEntry<unknown>>;\n\nexport interface WebhookRouterOptions {\n /** Global hooks executed after successful route handler completion. */\n after?: AfterHook | AfterHook[];\n /** Global hooks executed before route-level hooks and verification. */\n before?: BeforeHook | BeforeHook[];\n /** Global error hook used to override the default `500` response. */\n onError?: ErrorHook;\n /** Required path prefix for all webhook routes. Defaults to `\"/webhooks/\"`. */\n prefix?: string;\n /** Optional request verification function (for signature checks, auth, etc.). */\n verify?: (req: NormalizedRequest) => Promise<void> | void;\n}\n\nfunction toArray<T>(value: T | T[] | undefined): T[] {\n if (value === undefined) {\n return [];\n }\n\n return Array.isArray(value) ? value : [value];\n}\n\n/**\n * Main webhook router class.\n *\n * Register routes with typed schemas and call `handle` with a normalized request.\n */\nexport class WebhookRouter<TMap = unknown> {\n private readonly handlers: HandlerStore = {};\n private readonly verify: ((req: NormalizedRequest) => Promise<void> | void) | undefined;\n private readonly globalBeforeHooks: BeforeHook[] = [];\n private readonly globalAfterHooks: AfterHook[] = [];\n private readonly globalErrorHook: ErrorHook | undefined;\n private readonly prefix: string;\n\n constructor(opts: WebhookRouterOptions = {}) {\n this.prefix = opts.prefix ?? \"/webhooks/\";\n this.verify = opts.verify;\n this.globalBeforeHooks = toArray(opts.before);\n this.globalAfterHooks = toArray(opts.after);\n this.globalErrorHook = opts.onError;\n }\n\n /**\n * Register a webhook handler for a specific path.\n *\n * When a schema is provided, `payload` is inferred from the schema output type.\n *\n * @param path - Route path relative to configured prefix.\n * @param handlerOrOptions - Handler function or schema-based registration options.\n * @returns The same router instance with an updated internal route type map.\n */\n register<Path extends string, TSchema extends StandardSchemaV1<unknown, unknown>>(\n path: Path,\n handlerOrOptions: SchemaRouteOptions<TSchema>,\n ): WebhookRouter<TMap & Record<Path, InferSchemaOutput<TSchema>>>;\n register<Path extends string, TPayload>(\n path: Path,\n handlerOrOptions: RegisterOptions<TPayload>,\n ): WebhookRouter<TMap & Record<Path, TPayload>>;\n register<Path extends string>(\n path: Path,\n handlerOrOptions: WebhookHandler<unknown>,\n ): WebhookRouter<TMap & Record<Path, unknown>>;\n register(\n path: string,\n handlerOrOptions: WebhookHandler<unknown> | RegisterOptions<unknown>,\n ): WebhookRouter<TMap> {\n if (typeof handlerOrOptions === \"function\") {\n this.handlers[path] = { handler: handlerOrOptions };\n } else {\n this.handlers[path] = this.createHandlerEntry(handlerOrOptions);\n }\n\n return this;\n }\n\n /**\n * Handles a normalized incoming webhook request.\n *\n * @param req - Normalized request object.\n * @returns Normalized response for the adapter/framework layer.\n */\n async handle(req: NormalizedRequest): Promise<NormalizedResponse> {\n try {\n const normalizedPath = this.normalizePath(req);\n\n if (normalizedPath === null) {\n return { status: 404, body: { error: \"not found\" } };\n }\n\n const handlerEntry = this.handlers[normalizedPath];\n if (!handlerEntry) {\n return { status: 404, body: { error: \"not found\" } };\n }\n\n await this.runGlobalBeforeHooks(req);\n await this.runRouteBeforeHooks(req, handlerEntry.before);\n\n if (this.verify) {\n await this.verify(req);\n }\n\n const parsedJson = this.parseRequestBody(req);\n const validationResult = await this.validatePayload(parsedJson, handlerEntry.schema);\n\n if (this.isErrorResponse(validationResult)) {\n return validationResult;\n }\n\n const response = await this.executeHandler(handlerEntry.handler, req, validationResult);\n\n await this.runRouteAfterHooks(req, response, handlerEntry.after);\n await this.runGlobalAfterHooks(req, response);\n\n return response;\n } catch (error) {\n return this.handleError(error, req);\n }\n }\n\n private normalizePath(req: NormalizedRequest): string | null {\n let pathname = req.path;\n try {\n // Try to parse as URL (e.g. handles full URLs like https://example.com/webhooks/path -> /webhooks/path)\n const url = new URL(req.path);\n pathname = url.pathname;\n } catch {\n // Not a full URL, use the path as-is\n }\n\n // Require prefix (e.g. /webhooks/path -> /path)\n if (!pathname.startsWith(this.prefix)) {\n // Path doesn't start with the required prefix - not a webhook route\n return null;\n }\n\n // Strip prefix and keep the leading slash\n pathname = pathname.slice(this.prefix.length - 1);\n req.path = pathname;\n\n // Normalize path by removing leading slash for handler matching (e.g. /path -> path)\n const normalizedPath = pathname.startsWith(\"/\") ? pathname.slice(1) : pathname;\n\n return normalizedPath;\n }\n\n private async runGlobalBeforeHooks(req: NormalizedRequest): Promise<void> {\n for (const hook of this.globalBeforeHooks) {\n await hook(req);\n }\n }\n\n private createHandlerEntry(options: RegisterOptions<unknown>): HandlerEntry<unknown> {\n const entry: HandlerEntry<unknown> = {\n handler: options.handler,\n };\n\n if (options.schema !== undefined) {\n entry.schema = options.schema;\n }\n\n if (options.before !== undefined) {\n entry.before = Array.isArray(options.before) ? options.before : [options.before];\n }\n\n if (options.after !== undefined) {\n entry.after = Array.isArray(options.after) ? options.after : [options.after];\n }\n\n return entry;\n }\n\n private async runRouteBeforeHooks(req: NormalizedRequest, before?: BeforeHook[]): Promise<void> {\n if (before) {\n for (const hook of before) {\n await hook(req);\n }\n }\n }\n\n private parseRequestBody<TParsed = unknown>(req: NormalizedRequest): TParsed | undefined {\n try {\n const parsed = JSON.parse(new TextDecoder().decode(req.rawBody));\n req.json = parsed;\n return parsed as TParsed;\n } catch {\n return;\n }\n }\n\n private isErrorResponse(value: unknown): value is NormalizedResponse {\n return (\n typeof value === \"object\" &&\n value !== null &&\n \"status\" in value &&\n typeof value.status === \"number\"\n );\n }\n\n private async validatePayload<TPayload>(\n parsedJson: unknown,\n schema?: StandardSchemaV1<unknown, TPayload>,\n ): Promise<TPayload | NormalizedResponse> {\n if (!schema) {\n return parsedJson as TPayload;\n }\n\n const result = await standardValidate(schema, parsedJson, {\n throwOnError: false,\n });\n\n if (result.issues) {\n return {\n status: 400,\n body: {\n error: \"validation failed\",\n issues: result.issues.map((issue) => ({\n path: issue.path?.map((p) =>\n typeof p === \"object\" && \"key\" in p ? String(p.key) : String(p),\n ),\n message: issue.message,\n })),\n },\n };\n }\n\n return result.value as TPayload;\n }\n\n private async executeHandler<TPayload = unknown>(\n handler: WebhookHandler<TPayload>,\n req: NormalizedRequest,\n validatedPayload: TPayload,\n ): Promise<NormalizedResponse> {\n const responded = await handler({\n req,\n payload: validatedPayload,\n ack: async (r?: Partial<NormalizedResponse>) => {\n const response: NormalizedResponse = {\n status: r?.status ?? 200,\n body: r?.body ?? \"ok\",\n };\n\n if (r?.headers !== undefined) {\n response.headers = r.headers;\n }\n\n return response;\n },\n });\n\n return responded ?? { status: 200, body: \"ok\" };\n }\n\n private async runRouteAfterHooks(\n req: NormalizedRequest,\n response: NormalizedResponse,\n after?: AfterHook[],\n ): Promise<void> {\n if (after) {\n for (const hook of after) {\n await hook(req, response);\n }\n }\n }\n\n private async runGlobalAfterHooks(\n req: NormalizedRequest,\n response: NormalizedResponse,\n ): Promise<void> {\n for (const hook of this.globalAfterHooks) {\n await hook(req, response);\n }\n }\n\n private async handleError<TError = unknown>(\n error: TError,\n req: NormalizedRequest,\n ): Promise<NormalizedResponse> {\n if (this.globalErrorHook) {\n const errorResponse = await this.globalErrorHook(error as Error, req);\n if (errorResponse) {\n return errorResponse;\n }\n }\n\n return {\n status: 500,\n body: {\n error: error instanceof Error ? error.message : \"Internal server error\",\n },\n };\n }\n}\n\n/**\n * Factory helper for creating a webhook router instance.\n *\n * @param opts - Optional global router options.\n * @returns A new webhook router.\n */\nexport function createWebhookRouter(opts?: WebhookRouterOptions): WebhookRouter {\n return new WebhookRouter(opts);\n}\n"],"mappings":"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"}
@@ -1,93 +0,0 @@
1
- import { StandardSchemaV1 } from "@zap-studio/validation";
2
-
3
- //#region src/types/index.d.ts
4
- /** Framework-agnostic request shape consumed by the webhook router. */
5
- interface NormalizedRequest {
6
- /** The headers of the request (e.g. { "Authorization": "Bearer token" }) */
7
- headers: Headers;
8
- /** The parsed JSON body of the request if applicable */
9
- json?: unknown;
10
- /** The HTTP method of the request */
11
- method: Request["method"];
12
- /** The route parameters of the request */
13
- params?: Record<string, string>;
14
- /** The path of the request you registered in the router (e.g. "payment", "subscription") */
15
- path: string;
16
- /** The query parameters of the request */
17
- query?: Record<string, string | string[]>;
18
- /** The raw body of the request (for signature) */
19
- rawBody: Uint8Array<ArrayBufferLike>;
20
- /** The parsed text body of the request if applicable */
21
- text?: string;
22
- }
23
- /** Framework-agnostic response shape returned by the webhook router. */
24
- interface NormalizedResponse<TBody = unknown> {
25
- /** The body of the response */
26
- body?: TBody;
27
- /** The headers of the response */
28
- headers?: Headers;
29
- /** The HTTP status code of the response */
30
- status: number;
31
- }
32
- /** Route registration options for a webhook handler. */
33
- interface RegisterOptions<T> {
34
- /** Hooks that run after successful processing (before global after hooks) */
35
- after?: AfterHook | AfterHook[];
36
- /** Hooks that run before request processing (after global before hooks) */
37
- before?: BeforeHook | BeforeHook[];
38
- /** The handler function to process the webhook */
39
- handler: WebhookHandler<T>;
40
- /** Optional Standard Schema validator to validate the webhook payload */
41
- schema?: StandardSchemaV1<unknown, T>;
42
- }
43
- /**
44
- * Infers the output type from a Standard Schema instance.
45
- *
46
- * @template TSchema - A Standard Schema type.
47
- */
48
- type InferSchemaOutput<TSchema> = TSchema extends StandardSchemaV1<unknown, infer TOutput> ? TOutput : never;
49
- /**
50
- * Route options where schema is required and handler payload is inferred.
51
- *
52
- * @template TSchema - Schema used to infer handler payload type.
53
- */
54
- type SchemaRouteOptions<TSchema extends StandardSchemaV1<unknown, unknown>> = Omit<RegisterOptions<InferSchemaOutput<TSchema>>, "schema"> & {
55
- schema: TSchema;
56
- };
57
- interface RouteLike {
58
- after?: AfterHook | AfterHook[];
59
- before?: BeforeHook | BeforeHook[];
60
- handler: WebhookHandler<unknown>;
61
- schema: StandardSchemaV1<unknown, unknown>;
62
- }
63
- /**
64
- * Applies schema-driven payload inference to each route entry.
65
- *
66
- * @template TRoutes - Route dictionary keyed by webhook path.
67
- */
68
- type SchemaRoutes<TRoutes extends Record<string, RouteLike>> = { [P in keyof TRoutes]: SchemaRouteOptions<TRoutes[P]["schema"]> };
69
- /** The webhook handler function, responsible for processing incoming webhook events. */
70
- type WebhookHandler<TPayload = unknown> = (ctx: {
71
- req: NormalizedRequest;
72
- payload: TPayload;
73
- ack: (res?: Partial<NormalizedResponse>) => Promise<NormalizedResponse>;
74
- }) => Promise<NormalizedResponse | undefined> | NormalizedResponse | undefined;
75
- /** Maps route keys to their payload-specific webhook handlers. */
76
- type HandlerMap<TMap extends Record<string, unknown>> = { [P in keyof TMap]: WebhookHandler<TMap[P]> };
77
- /**
78
- * Builds a webhook payload map from a schema-based route dictionary.
79
- *
80
- * @template TRoutes - Route dictionary keyed by webhook path.
81
- */
82
- type InferWebhookMapFromRoutes<TRoutes extends Record<string, RouteLike>> = { [P in keyof TRoutes]: InferSchemaOutput<TRoutes[P]["schema"]> };
83
- /** Verification function for incoming requests */
84
- type VerifyFn = (req: NormalizedRequest) => Promise<void> | void;
85
- /** Hook function that runs before request processing */
86
- type BeforeHook = (req: NormalizedRequest) => Promise<void> | void;
87
- /** Hook function that runs after successful request processing */
88
- type AfterHook = (req: NormalizedRequest, res: NormalizedResponse) => Promise<void> | void;
89
- /** Hook function that runs when an error occurs */
90
- type ErrorHook = (error: Error, req: NormalizedRequest) => Promise<NormalizedResponse | undefined> | NormalizedResponse | undefined;
91
- //#endregion
92
- export { AfterHook, BeforeHook, ErrorHook, HandlerMap, InferSchemaOutput, InferWebhookMapFromRoutes, NormalizedRequest, NormalizedResponse, RegisterOptions, SchemaRouteOptions, SchemaRoutes, VerifyFn, WebhookHandler };
93
- //# sourceMappingURL=index.d.mts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/types/index.ts"],"mappings":";;;;UAGiB,iBAAA;EAAjB;EAEE,OAAA,EAAS,OAAA;;EAET,IAAA;;EAEA,MAAA,EAAQ,OAAA;;EAER,MAAA,GAAS,MAAA;;EAET,IAAA;EAIS;EAFT,KAAA,GAAQ,MAAA;;EAER,OAAA,EAAS,UAAA,CAAW,eAAA;;EAEpB,IAAA;AAAA;;UAIe,kBAAA;;EAEf,IAAA,GAAO,KAAA;;EAEP,OAAA,GAAU,OAAA;;EAEV,MAAA;AAAA;;UAIe,eAAA;EAVA;EAYf,KAAA,GAAQ,SAAA,GAAY,SAAA;EARV;EAUV,MAAA,GAAS,UAAA,GAAa,UAAA;;EAEtB,OAAA,EAAS,cAAA,CAAe,CAAA;;EAExB,MAAA,GAAS,gBAAA,UAA0B,CAAA;AAAA;;;AARrC;;;KAgBY,iBAAA,YACV,OAAA,SAAgB,gBAAA,2BAA2C,OAAA;;;;;;KAOjD,kBAAA,iBAAmC,gBAAA,sBAAsC,IAAA,CACnF,eAAA,CAAgB,iBAAA,CAAkB,OAAA;EAGlC,MAAA,EAAQ,OAAA;AAAA;AAAA,UAGA,SAAA;EACR,KAAA,GAAQ,SAAA,GAAY,SAAA;EACpB,MAAA,GAAS,UAAA,GAAa,UAAA;EACtB,OAAA,EAAS,cAAA;EACT,MAAA,EAAQ,gBAAA;AAAA;;;;;;KAQE,YAAA,iBAA6B,MAAA,SAAe,SAAA,mBAC1C,OAAA,GAAU,kBAAA,CAAmB,OAAA,CAAQ,CAAA;;KAIvC,cAAA,wBAAsC,GAAA;EAChD,GAAA,EAAK,iBAAA;EACL,OAAA,EAAS,QAAA;EACT,GAAA,GAAM,GAAA,GAAM,OAAA,CAAQ,kBAAA,MAAwB,OAAA,CAAQ,kBAAA;AAAA,MAChD,OAAA,CAAQ,kBAAA,gBAAkC,kBAAA;;KAGpC,UAAA,cAAwB,MAAA,mCACtB,IAAA,GAAO,cAAA,CAAe,IAAA,CAAK,CAAA;;;;;;KAQ7B,yBAAA,iBAA0C,MAAA,SAAe,SAAA,mBACvD,OAAA,GAAU,iBAAA,CAAkB,OAAA,CAAQ,CAAA;;KAItC,QAAA,IAAY,GAAA,EAAK,iBAAA,KAAsB,OAAA;;KAGvC,UAAA,IAAc,GAAA,EAAK,iBAAA,KAAsB,OAAA;;KAGzC,SAAA,IAAa,GAAA,EAAK,iBAAA,EAAmB,GAAA,EAAK,kBAAA,KAAuB,OAAA;;KAGjE,SAAA,IACV,KAAA,EAAO,KAAA,EACP,GAAA,EAAK,iBAAA,KACF,OAAA,CAAQ,kBAAA,gBAAkC,kBAAA"}
@@ -1 +0,0 @@
1
- export {};
@@ -1,13 +0,0 @@
1
- //#region src/utils/index.d.ts
2
- /**
3
- * Compares two strings in constant time to prevent timing attacks.
4
- *
5
- * @example
6
- * ```ts
7
- * const isEqual = constantTimeEquals("string1", "string2"); // returns false
8
- * ```
9
- */
10
- declare function constantTimeEquals(a: string, b: string): boolean;
11
- //#endregion
12
- export { constantTimeEquals };
13
- //# sourceMappingURL=index.d.mts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/utils/index.ts"],"mappings":";;AAQA;;;;;;;iBAAgB,kBAAA,CAAmB,CAAA,UAAW,CAAA"}
@@ -1 +0,0 @@
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 DELETED
@@ -1,54 +0,0 @@
1
- import { VerifyFn } from "./types/index.mjs";
2
-
3
- //#region src/verify.d.ts
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;
11
- /**
12
- * Creates a webhook verifier that validates an HMAC signature from a request header.
13
- *
14
- * The verifier imports the provided string secret once, computes an HMAC from
15
- * `req.rawBody`, normalizes the incoming header value, and compares both
16
- * signatures in constant time.
17
- *
18
- * Header values like `sha256=<hex>` are supported so common provider formats
19
- * such as GitHub work without extra parsing.
20
- *
21
- * @example
22
- * ```ts
23
- * import { createWebhookRouter } from "@zap-studio/webhooks";
24
- * import { createHmacVerifier } from "@zap-studio/webhooks/verify";
25
- *
26
- * const router = createWebhookRouter({
27
- * verify: createHmacVerifier({
28
- * headerName: "x-hub-signature-256",
29
- * secret: process.env.GITHUB_WEBHOOK_SECRET!,
30
- * }),
31
- * });
32
- * ```
33
- *
34
- * @param options - Verifier configuration.
35
- * @param options.headerName - Header containing the provider signature.
36
- * @param options.secret - Shared HMAC secret as a string.
37
- * @param options.algo - HMAC hash algorithm. Defaults to `"sha256"`.
38
- * @returns A router-compatible request verifier.
39
- *
40
- * @throws {VerificationError}
41
- * Thrown when verifier setup fails or request verification does not pass.
42
- */
43
- declare function createHmacVerifier({
44
- headerName,
45
- secret,
46
- algo
47
- }: {
48
- headerName: string;
49
- secret: string;
50
- algo?: HmacAlgorithm;
51
- }): VerifyFn;
52
- //#endregion
53
- export { createHmacVerifier };
54
- //# sourceMappingURL=verify.d.mts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"verify.d.mts","names":[],"sources":["../src/verify.ts"],"mappings":";;;cAIM,SAAA;EAAA,SACJ,IAAA;EAAA,SACA,MAAA;EAAA,SACA,MAAA;EAAA,SACA,MAAA;AAAA;AAAA,KAGG,aAAA,gBAA6B,SAAA;;;;;;AAHhC;;;;;AAqCF;;;;;;;;;;;;;;;;;;;;;;iBAAgB,kBAAA,CAAA;EACd,UAAA;EACA,MAAA;EACA;AAAA;EAEA,UAAA;EACA,MAAA;EACA,IAAA,GAAO,aAAA;AAAA,IACL,QAAA"}
@@ -1 +0,0 @@
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"}