@zap-studio/webhooks 0.3.0 → 1.0.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 +55 -24
- package/README.md +89 -154
- package/dist/{errors.d.mts → errors.d.ts} +17 -1
- package/dist/errors.d.ts.map +1 -0
- package/dist/{errors.mjs → errors.js} +17 -1
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/router.d.ts +137 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +203 -0
- package/dist/router.js.map +1 -0
- package/dist/types.d.ts +217 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +0 -0
- package/dist/{verify.d.mts → verify.d.ts} +8 -4
- package/dist/verify.d.ts.map +1 -0
- package/dist/{verify.mjs → verify.js} +32 -11
- package/dist/verify.js.map +1 -0
- package/package.json +10 -14
- package/dist/adapters/base.d.mts +0 -59
- package/dist/adapters/base.d.mts.map +0 -1
- package/dist/adapters/base.mjs +0 -24
- package/dist/adapters/base.mjs.map +0 -1
- package/dist/errors.d.mts.map +0 -1
- package/dist/errors.mjs.map +0 -1
- package/dist/index.d.mts +0 -75
- package/dist/index.d.mts.map +0 -1
- package/dist/index.mjs +0 -175
- package/dist/index.mjs.map +0 -1
- package/dist/types/index.d.mts +0 -92
- package/dist/types/index.d.mts.map +0 -1
- package/dist/types/index.mjs +0 -1
- package/dist/utils/index.d.mts +0 -18
- package/dist/utils/index.d.mts.map +0 -1
- package/dist/utils/index.mjs +0 -24
- package/dist/utils/index.mjs.map +0 -1
- package/dist/verify.d.mts.map +0 -1
- package/dist/verify.mjs.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,70 +1,101 @@
|
|
|
1
|
-
|
|
1
|
+
# Changelog
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## [1.0.0]
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
### Changed
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
`WebhookRouter`'s stateless private static methods (`runBeforeHooks`, `runAfterHooks`, `createHandlerEntry`, `parseRequestBody`, `validatePayload`, `executeHandler`) are now module-level functions in `router.ts`. Internal-only change; the public API (`WebhookRouter`, `createWebhookRouter`, `.register()`, `.handle()`) is unaffected.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
HMAC signature verification decodes the incoming header's hex signature to bytes and compares it against the computed digest byte-for-byte, instead of hex-encoding the digest and comparing hex text. Behavior is unchanged for valid requests; this only affects internals (fewer bytes compared, and the header's hex is no longer case-normalized as text since decoding handles case natively).
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
`constantTimeEquals` moved from `utils.ts` into `verify.ts` (its only consumer) and now compares `Uint8Array`s (bytes) instead of strings. Still exported from `@zap-studio/webhooks` and `@zap-studio/webhooks/verify`.
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
### Removed
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
Removed the `./utils` subpath export.
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
## [0.4.0]
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
The custom `NormalizedRequest`/`NormalizedResponse` contract is gone. `router.handle` now takes a standard Web API `Request` and returns a standard `Response`, so the router plugs directly into fetch-native runtimes (Bun, Deno, Cloudflare Workers, Next.js route handlers, Hono) with no adapter layer.
|
|
26
|
+
|
|
27
|
+
**Breaking changes:**
|
|
28
|
+
|
|
29
|
+
- `handle(req: NormalizedRequest): Promise<NormalizedResponse>` → `handle(request: Request): Promise<Response>`.
|
|
30
|
+
- Handlers receive `{ request, rawBody, path, payload }` (a `WebhookContext` plus the validated `payload`) and return a `Response` or `undefined` (default `200` `"ok"`). The `ack` helper is removed — use `Response.json(body, init)`.
|
|
31
|
+
- Hooks and `verify` are retyped against the context: `BeforeHook(ctx)`, `AfterHook(ctx, response)`, `ErrorHook(error, ctx)`, `VerifyFn(ctx)`. After hooks must `clone()` the response before reading its body.
|
|
32
|
+
- `Adapter`, `BaseAdapter`, and the `./adapters/base` export are removed. Node `http` users can bridge with `srvx` or `@hono/node-server`.
|
|
33
|
+
- The prefix is normalized to a trailing slash and only matches on a path boundary: `prefix: "/api"` now behaves as `/api/`, so `/apihello` no longer matches a route (previously it matched `ihello`).
|
|
34
|
+
|
|
35
|
+
Behavior kept: hook execution order, prefix semantics (default `/webhooks/`), exact-match routing, HMAC verification, and the `404`/`400`/`500` error body shapes. Unknown routes now return `404` without reading the request body.
|
|
36
|
+
|
|
37
|
+
## [0.3.0]
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
`Adapter` and `BaseAdapter` are now generic over the framework request/response types (`Adapter<TReq, TRes>`, `BaseAdapter<TReq, TRes>`), replacing the previous per-method generics. The mapping members (`toNormalizedRequest`, `toFrameworkResponse`, `handleWebhook`) are now arrow properties, so custom adapters must override them with property syntax rather than method syntax.
|
|
42
|
+
|
|
43
|
+
Also: `register()` now returns `this`, error hooks always receive a real `Error` instance, and `rawBody` is typed as `Uint8Array`. Internal formatting and lint cleanup migrated to ultracite.
|
|
44
|
+
|
|
45
|
+
## [0.2.2]
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
|
|
49
|
+
- Updated dependency `@zap-studio/validation` to `0.3.4`.
|
|
50
|
+
|
|
51
|
+
## [0.2.1]
|
|
22
52
|
|
|
23
53
|
### Changed
|
|
24
54
|
|
|
25
55
|
- 5fa58b1: Reduced webhook router complexity by consolidating hook normalization and handler entry creation.
|
|
26
56
|
- 7004e9f: Allow explicit `undefined` in option handling, then follow with d707800 to remove redundant `| undefined` unions from public types.
|
|
27
57
|
- 9f31f87: Switched the package build to ESNext-aligned output and updated package tooling and publish metadata.
|
|
58
|
+
- Updated dependency `@zap-studio/validation` to `0.3.3`.
|
|
28
59
|
|
|
29
|
-
###
|
|
60
|
+
### Fixed
|
|
30
61
|
|
|
31
|
-
-
|
|
62
|
+
- 3a950dc: Preserve registered hook assignment types while keeping the schema-first router API unchanged.
|
|
32
63
|
|
|
33
|
-
## 0.2.0
|
|
64
|
+
## [0.2.0]
|
|
34
65
|
|
|
35
|
-
###
|
|
66
|
+
### Changed
|
|
36
67
|
|
|
37
68
|
- c686862: Switch `createHmacVerifier` to Web Crypto and standardize the verifier around string secrets.
|
|
38
69
|
|
|
39
70
|
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.
|
|
40
71
|
|
|
41
|
-
## 0.1.4
|
|
72
|
+
## [0.1.4]
|
|
42
73
|
|
|
43
|
-
###
|
|
74
|
+
### Changed
|
|
44
75
|
|
|
45
76
|
- e26293e: Updated dependencies.
|
|
46
77
|
- @zap-studio/validation@0.3.2
|
|
47
78
|
|
|
48
|
-
## 0.1.3
|
|
79
|
+
## [0.1.3]
|
|
49
80
|
|
|
50
|
-
###
|
|
81
|
+
### Changed
|
|
51
82
|
|
|
52
83
|
- 5ea3d3b: Updated dependencies.
|
|
53
84
|
- @zap-studio/validation@0.3.1
|
|
54
85
|
|
|
55
|
-
## 0.1.2
|
|
86
|
+
## [0.1.2]
|
|
56
87
|
|
|
57
|
-
###
|
|
88
|
+
### Fixed
|
|
58
89
|
|
|
59
90
|
- c209a27: Fix payload schema validation internals to use the current async `standardValidate` options API (`{ throwOnError: false }`), restoring typecheck compatibility after the validation helper signature update.
|
|
60
91
|
|
|
61
|
-
## 0.1.1
|
|
92
|
+
## [0.1.1]
|
|
62
93
|
|
|
63
|
-
###
|
|
94
|
+
### Changed
|
|
64
95
|
|
|
65
96
|
- f75b984: Updated dependency `@zap-studio/validation` to `0.3.0`.
|
|
66
97
|
|
|
67
|
-
## 0.1.0
|
|
98
|
+
## [0.1.0]
|
|
68
99
|
|
|
69
100
|
### Added
|
|
70
101
|
|
package/README.md
CHANGED
|
@@ -1,215 +1,150 @@
|
|
|
1
1
|
# @zap-studio/webhooks
|
|
2
2
|
|
|
3
|
-
Schema-first, type-safe webhook routing with runtime-agnostic signature verification support.
|
|
3
|
+
Schema-first, type-safe webhook routing built on the standard Web API [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) primitives, with runtime-agnostic signature verification support.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Full documentation: [zapstudio.dev/webhooks](https://www.zapstudio.dev/webhooks)
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Installation
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
```bash
|
|
10
|
+
npm install @zap-studio/webhooks
|
|
11
|
+
```
|
|
10
12
|
|
|
11
|
-
-
|
|
12
|
-
- parse and validate payloads
|
|
13
|
-
- route by event path
|
|
14
|
-
- normalize success/error responses
|
|
13
|
+
You also need a schema library that implements [Standard Schema](https://github.com/standard-schema/standard-schema), such as Zod, Valibot, or ArkType.
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
## Features
|
|
17
16
|
|
|
18
|
-
|
|
17
|
+
- **Web API native** — `handle(request: Request)` returns a `Response`, so the router plugs directly into Bun, Deno, Cloudflare Workers, Next.js route handlers, Hono, and any other fetch-compatible runtime.
|
|
18
|
+
- **Type-safe routing** — handler payload types are inferred from the route schema.
|
|
19
|
+
- **Standard Schema validation** — bring Zod, Valibot, ArkType, or any compatible library.
|
|
20
|
+
- **Signature verification** — built-in HMAC verifier with constant-time comparison, or plug in your own `verify` function.
|
|
21
|
+
- **Lifecycle hooks** — global `before`, `after`, and `onError` hooks for cross-cutting behavior.
|
|
22
|
+
- **Runtime-agnostic** — uses the Web Crypto API, not Node-specific APIs.
|
|
23
|
+
- **Tree-shakeable** — validation and hook-running internals are standalone functions; unused exports are dropped by any modern bundler.
|
|
19
24
|
|
|
20
|
-
##
|
|
21
|
-
|
|
22
|
-
```bash
|
|
23
|
-
pnpm add @zap-studio/webhooks
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
## Quickstart
|
|
25
|
+
## Quick Start
|
|
27
26
|
|
|
28
27
|
```ts
|
|
29
28
|
import { createWebhookRouter } from "@zap-studio/webhooks";
|
|
30
29
|
import { z } from "zod";
|
|
31
30
|
|
|
32
|
-
const router = createWebhookRouter({
|
|
33
|
-
prefix: "/webhooks/",
|
|
34
|
-
});
|
|
31
|
+
const router = createWebhookRouter({ prefix: "/webhooks" });
|
|
35
32
|
|
|
36
|
-
router.register("payments/succeeded", {
|
|
37
|
-
schema: z.object({
|
|
38
|
-
|
|
39
|
-
amount: z.number().positive(),
|
|
40
|
-
currency: z.string().length(3),
|
|
41
|
-
}),
|
|
42
|
-
handler: async ({ payload, ack }) => {
|
|
33
|
+
router.register("/payments/succeeded", {
|
|
34
|
+
schema: z.object({ id: z.string(), amount: z.number().positive() }),
|
|
35
|
+
handler: ({ payload }) => {
|
|
43
36
|
// payload is inferred from schema
|
|
44
|
-
return
|
|
37
|
+
return Response.json({ processed: payload.id });
|
|
45
38
|
},
|
|
46
39
|
});
|
|
40
|
+
|
|
41
|
+
export default {
|
|
42
|
+
fetch: (request: Request) => router.handle(request),
|
|
43
|
+
};
|
|
47
44
|
```
|
|
48
45
|
|
|
49
|
-
##
|
|
46
|
+
## Web API Native
|
|
47
|
+
|
|
48
|
+
`handle(request: Request)` returns a `Response`, so the router plugs directly into any fetch-compatible runtime.
|
|
50
49
|
|
|
51
50
|
```ts
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
import { z } from "zod";
|
|
51
|
+
// Bun / Deno / Cloudflare Workers
|
|
52
|
+
export default { fetch: (request: Request) => router.handle(request) };
|
|
55
53
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
headerName: "x-hub-signature-256",
|
|
59
|
-
secret: process.env.GITHUB_WEBHOOK_SECRET!,
|
|
60
|
-
}),
|
|
61
|
-
});
|
|
54
|
+
// Next.js route handler (app/webhooks/[...path]/route.ts)
|
|
55
|
+
export const POST = (request: Request) => router.handle(request);
|
|
62
56
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
57
|
+
// Hono
|
|
58
|
+
app.all("/webhooks/*", (c) => router.handle(c.req.raw));
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Type-Safe Routing
|
|
62
|
+
|
|
63
|
+
Handler payload types are inferred from the route schema.
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
router.register("/payments/succeeded", {
|
|
67
|
+
schema: z.object({ id: z.string(), amount: z.number() }),
|
|
68
|
+
handler: ({ payload }) => {
|
|
69
|
+
// payload.id: string, payload.amount: number — inferred from schema
|
|
70
|
+
return Response.json({ ok: true });
|
|
73
71
|
},
|
|
74
72
|
});
|
|
75
73
|
```
|
|
76
74
|
|
|
77
|
-
##
|
|
75
|
+
## Standard Schema Validation
|
|
76
|
+
|
|
77
|
+
Bring Zod, Valibot, ArkType, or any compatible library.
|
|
78
78
|
|
|
79
79
|
```ts
|
|
80
|
-
import Stripe from "stripe";
|
|
81
|
-
import { createWebhookRouter } from "@zap-studio/webhooks";
|
|
82
80
|
import { z } from "zod";
|
|
81
|
+
// or: import * as v from "valibot"; import { type } from "arktype";
|
|
83
82
|
|
|
84
|
-
|
|
83
|
+
router.register("/event", {
|
|
84
|
+
schema: z.object({ id: z.string() }),
|
|
85
|
+
handler: ({ payload }) => Response.json(payload),
|
|
86
|
+
});
|
|
87
|
+
```
|
|
85
88
|
|
|
86
|
-
|
|
87
|
-
verify: async (req) => {
|
|
88
|
-
const signature = req.headers.get("stripe-signature");
|
|
89
|
-
if (!signature) {
|
|
90
|
-
throw new Error("Missing Stripe signature");
|
|
91
|
-
}
|
|
89
|
+
## Signature Verification
|
|
92
90
|
|
|
93
|
-
|
|
94
|
-
req.rawBody,
|
|
95
|
-
signature,
|
|
96
|
-
process.env.STRIPE_WEBHOOK_SECRET!
|
|
97
|
-
);
|
|
98
|
-
},
|
|
99
|
-
});
|
|
91
|
+
Built-in HMAC verifier with constant-time comparison, or plug in your own `verify` function.
|
|
100
92
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
93
|
+
```ts
|
|
94
|
+
import {
|
|
95
|
+
createHmacVerifier,
|
|
96
|
+
createWebhookRouter,
|
|
97
|
+
VerificationError,
|
|
98
|
+
} from "@zap-studio/webhooks";
|
|
99
|
+
|
|
100
|
+
const router = createWebhookRouter({
|
|
101
|
+
verify: createHmacVerifier({
|
|
102
|
+
headerName: "x-hub-signature-256",
|
|
103
|
+
secret: process.env.WEBHOOK_SECRET!,
|
|
106
104
|
}),
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
105
|
+
onError: (error) => {
|
|
106
|
+
if (error instanceof VerificationError) {
|
|
107
|
+
return Response.json({ error: "invalid signature" }, { status: 401 });
|
|
108
|
+
}
|
|
110
109
|
},
|
|
111
110
|
});
|
|
112
111
|
```
|
|
113
112
|
|
|
114
|
-
## Lifecycle
|
|
115
|
-
|
|
116
|
-
Lifecycle hooks let you apply cross-cutting behavior without duplicating code in each handler:
|
|
113
|
+
## Lifecycle Hooks
|
|
117
114
|
|
|
118
|
-
|
|
119
|
-
- `after`: run logic after successful handler execution (metrics, audit logs)
|
|
120
|
-
- `onError`: map thrown errors to consistent responses and centralize error reporting
|
|
115
|
+
Global `before`, `after`, and `onError` hooks for cross-cutting behavior.
|
|
121
116
|
|
|
122
117
|
```ts
|
|
123
118
|
const router = createWebhookRouter({
|
|
124
|
-
before: (
|
|
125
|
-
|
|
126
|
-
},
|
|
127
|
-
after: (_req, res) => {
|
|
128
|
-
console.log("status", res.status);
|
|
129
|
-
},
|
|
130
|
-
onError: (error) => ({
|
|
131
|
-
status: 500,
|
|
132
|
-
body: { error: error.message },
|
|
133
|
-
}),
|
|
119
|
+
before: (ctx) => console.log("incoming", ctx.path),
|
|
120
|
+
after: (_ctx, response) => console.log("status", response.status),
|
|
121
|
+
onError: (error) => Response.json({ error: error.message }, { status: 500 }),
|
|
134
122
|
});
|
|
135
123
|
```
|
|
136
124
|
|
|
137
|
-
##
|
|
125
|
+
## Runtime-Agnostic
|
|
138
126
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
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`.
|
|
142
|
-
|
|
143
|
-
- reads a signature from the header you choose
|
|
144
|
-
- computes an HMAC from `req.rawBody`
|
|
145
|
-
- compares signatures in constant time
|
|
146
|
-
- uses the Web Crypto API instead of Node `crypto`
|
|
147
|
-
- works across runtimes that provide `globalThis.crypto.subtle`
|
|
148
|
-
- expects a string secret
|
|
149
|
-
- throws `VerificationError` on verifier setup or signature failures
|
|
127
|
+
Uses the Web Crypto API, not Node-specific APIs.
|
|
150
128
|
|
|
151
129
|
```ts
|
|
152
|
-
|
|
153
|
-
import { VerificationError } from "@zap-studio/webhooks/errors";
|
|
154
|
-
|
|
130
|
+
// Uses globalThis.crypto.subtle — no Node `crypto` import required
|
|
155
131
|
const verify = createHmacVerifier({
|
|
156
132
|
headerName: "x-hub-signature-256",
|
|
157
133
|
secret: process.env.WEBHOOK_SECRET!,
|
|
158
|
-
algo: "sha256", // optional, defaults to sha256
|
|
159
134
|
});
|
|
160
|
-
|
|
161
|
-
try {
|
|
162
|
-
await verify(req);
|
|
163
|
-
} catch (error) {
|
|
164
|
-
if (error instanceof VerificationError) {
|
|
165
|
-
console.error("webhook verification failed", error.message);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
135
|
```
|
|
169
136
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
## Why `BaseAdapter` exists
|
|
173
|
-
|
|
174
|
-
This package is framework-agnostic by design. It does not include Express/Next/Hono/Elysia adapters.
|
|
137
|
+
## Runtime Support
|
|
175
138
|
|
|
176
|
-
|
|
139
|
+
| Runtime | Minimum version |
|
|
140
|
+
| ------------------ | ------------------------------------------------ |
|
|
141
|
+
| Node.js | 18.0.0 (router), 19.0.0 (verification helper) |
|
|
142
|
+
| Bun | 1.0.0 |
|
|
143
|
+
| Deno | 1.42 |
|
|
144
|
+
| Cloudflare Workers | Any current release |
|
|
145
|
+
| Browsers | Latest evergreen (Chrome, Edge, Firefox, Safari) |
|
|
177
146
|
|
|
178
|
-
|
|
179
|
-
- `BaseAdapter` handles the common `handleWebhook()` flow
|
|
180
|
-
- teams can reuse one adapter implementation across all webhook routes
|
|
181
|
-
|
|
182
|
-
```ts
|
|
183
|
-
import { BaseAdapter } from "@zap-studio/webhooks/adapters/base";
|
|
184
|
-
import type {
|
|
185
|
-
NormalizedRequest,
|
|
186
|
-
NormalizedResponse,
|
|
187
|
-
} from "@zap-studio/webhooks/types";
|
|
188
|
-
|
|
189
|
-
// `BaseAdapter<TReq, TRes>` is generic over your framework request/response
|
|
190
|
-
// types. Override the mapping members with arrow-property syntax.
|
|
191
|
-
class MyHttpAdapter extends BaseAdapter {
|
|
192
|
-
toNormalizedRequest = async (req: any): Promise<NormalizedRequest> => ({
|
|
193
|
-
method: req.method,
|
|
194
|
-
path: req.url,
|
|
195
|
-
headers: new Headers(req.headers),
|
|
196
|
-
rawBody: req.rawBody,
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
toFrameworkResponse = async (
|
|
200
|
-
res: any,
|
|
201
|
-
normalized: NormalizedResponse
|
|
202
|
-
): Promise<any> => {
|
|
203
|
-
res.statusCode = normalized.status;
|
|
204
|
-
res.end(
|
|
205
|
-
typeof normalized.body === "string"
|
|
206
|
-
? normalized.body
|
|
207
|
-
: JSON.stringify(normalized.body)
|
|
208
|
-
);
|
|
209
|
-
return res;
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
```
|
|
147
|
+
The router only needs the standard `Request`/`Response` APIs, available globally since Node.js 18. The verification helper additionally needs `globalThis.crypto.subtle`, which is global by default from Node.js 19 (on Node.js 18, pass the `--experimental-global-webcrypto` flag). In browsers, Web Crypto requires a secure context (HTTPS). Deno 1.42 is the first release that can install packages from JSR (`deno add jsr:@zap-studio/webhooks`).
|
|
213
148
|
|
|
214
149
|
## License
|
|
215
150
|
|
|
@@ -9,6 +9,22 @@
|
|
|
9
9
|
*
|
|
10
10
|
* This error is used by verifier helpers such as `createHmacVerifier` so
|
|
11
11
|
* callers can distinguish verification failures from other webhook errors.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { VerificationError } from "@zap-studio/webhooks";
|
|
16
|
+
*
|
|
17
|
+
* const response = await router.handle(request);
|
|
18
|
+
* // Verification failures surface as a 500 response by default, or via onError:
|
|
19
|
+
* const routerWithHandler = createWebhookRouter({
|
|
20
|
+
* verify: createHmacVerifier({ headerName: "x-signature", secret }),
|
|
21
|
+
* onError: (error) => {
|
|
22
|
+
* if (error instanceof VerificationError) {
|
|
23
|
+
* return Response.json({ error: error.message }, { status: 401 });
|
|
24
|
+
* }
|
|
25
|
+
* },
|
|
26
|
+
* });
|
|
27
|
+
* ```
|
|
12
28
|
*/
|
|
13
29
|
declare class VerificationError extends Error {
|
|
14
30
|
/**
|
|
@@ -20,4 +36,4 @@ declare class VerificationError extends Error {
|
|
|
20
36
|
}
|
|
21
37
|
//#endregion
|
|
22
38
|
export { VerificationError };
|
|
23
|
-
//# sourceMappingURL=errors.d.
|
|
39
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","names":[],"sources":["../src/errors.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;cA4Ba,0BAA0B;;;;;;EAMrC,YAAY"}
|
|
@@ -9,6 +9,22 @@
|
|
|
9
9
|
*
|
|
10
10
|
* This error is used by verifier helpers such as `createHmacVerifier` so
|
|
11
11
|
* callers can distinguish verification failures from other webhook errors.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { VerificationError } from "@zap-studio/webhooks";
|
|
16
|
+
*
|
|
17
|
+
* const response = await router.handle(request);
|
|
18
|
+
* // Verification failures surface as a 500 response by default, or via onError:
|
|
19
|
+
* const routerWithHandler = createWebhookRouter({
|
|
20
|
+
* verify: createHmacVerifier({ headerName: "x-signature", secret }),
|
|
21
|
+
* onError: (error) => {
|
|
22
|
+
* if (error instanceof VerificationError) {
|
|
23
|
+
* return Response.json({ error: error.message }, { status: 401 });
|
|
24
|
+
* }
|
|
25
|
+
* },
|
|
26
|
+
* });
|
|
27
|
+
* ```
|
|
12
28
|
*/
|
|
13
29
|
var VerificationError = class extends Error {
|
|
14
30
|
/**
|
|
@@ -24,4 +40,4 @@ var VerificationError = class extends Error {
|
|
|
24
40
|
//#endregion
|
|
25
41
|
export { VerificationError };
|
|
26
42
|
|
|
27
|
-
//# sourceMappingURL=errors.
|
|
43
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/**\n * Error primitives for webhook verification failures.\n *\n * @module @zap-studio/webhooks/errors\n */\n\n/**\n * Error thrown when webhook request verification fails.\n *\n * This error is used by verifier helpers such as `createHmacVerifier` so\n * callers can distinguish verification failures from other webhook errors.\n *\n * @example\n * ```ts\n * import { VerificationError } from \"@zap-studio/webhooks\";\n *\n * const response = await router.handle(request);\n * // Verification failures surface as a 500 response by default, or via onError:\n * const routerWithHandler = createWebhookRouter({\n * verify: createHmacVerifier({ headerName: \"x-signature\", secret }),\n * onError: (error) => {\n * if (error instanceof VerificationError) {\n * return Response.json({ error: error.message }, { status: 401 });\n * }\n * },\n * });\n * ```\n */\nexport class VerificationError extends Error {\n /**\n * Creates a verification error with a human-readable message.\n *\n * @param message - Error message describing the verification failure.\n */\n constructor(message: string) {\n super(message);\n this.name = \"VerificationError\";\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,IAAa,oBAAb,cAAuC,MAAM;;;;;;CAM3C,YAAY,SAAiB;EAC3B,MAAM,OAAO;EACb,KAAK,OAAO;CACd;AACF"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { VerificationError } from "./errors.js";
|
|
2
|
+
import { AfterHook, BeforeHook, ErrorHook, HandlerContext, HandlerMap, InferSchemaOutput, InferWebhookMapFromRoutes, RegisterOptions, SchemaRouteOptions, SchemaRoutes, VerifyFn, WebhookContext, WebhookHandler, WebhookRouterOptions } from "./types.js";
|
|
3
|
+
import { WebhookRouter, createWebhookRouter } from "./router.js";
|
|
4
|
+
import { constantTimeEquals, createHmacVerifier } from "./verify.js";
|
|
5
|
+
export { type AfterHook, type BeforeHook, type ErrorHook, type HandlerContext, type HandlerMap, type InferSchemaOutput, type InferWebhookMapFromRoutes, type RegisterOptions, type SchemaRouteOptions, type SchemaRoutes, VerificationError, type VerifyFn, type WebhookContext, type WebhookHandler, WebhookRouter, type WebhookRouterOptions, constantTimeEquals, createHmacVerifier, createWebhookRouter };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { VerificationError } from "./errors.js";
|
|
2
|
+
import { WebhookRouter, createWebhookRouter } from "./router.js";
|
|
3
|
+
import { constantTimeEquals, createHmacVerifier } from "./verify.js";
|
|
4
|
+
export { VerificationError, WebhookRouter, constantTimeEquals, createHmacVerifier, createWebhookRouter };
|
package/dist/router.d.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { InferSchemaOutput, RegisterOptions, SchemaRouteOptions, WebhookHandler, WebhookRouterOptions } from "./types.js";
|
|
2
|
+
import { StandardSchemaV1 } from "@zap-studio/validation";
|
|
3
|
+
//#region src/router.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Main webhook router class.
|
|
6
|
+
*
|
|
7
|
+
* Register routes with typed schemas and call `handle` with a Web API `Request`.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { WebhookRouter } from "@zap-studio/webhooks";
|
|
12
|
+
*
|
|
13
|
+
* const router = new WebhookRouter({ prefix: "/webhooks" });
|
|
14
|
+
*
|
|
15
|
+
* router.register("/stripe", {
|
|
16
|
+
* schema: stripeEventSchema,
|
|
17
|
+
* handler: async ({ payload }) => {
|
|
18
|
+
* console.log("Stripe event:", payload.type);
|
|
19
|
+
* },
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* export default { fetch: (request: Request) => router.handle(request) };
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
declare class WebhookRouter<TMap = unknown> {
|
|
26
|
+
private readonly handlers;
|
|
27
|
+
private readonly verify;
|
|
28
|
+
private readonly globalBeforeHooks;
|
|
29
|
+
private readonly globalAfterHooks;
|
|
30
|
+
private readonly globalErrorHook;
|
|
31
|
+
private readonly prefix;
|
|
32
|
+
private readonly prefixWithSlash;
|
|
33
|
+
/**
|
|
34
|
+
* Creates a webhook router with optional global hooks and verification behavior.
|
|
35
|
+
*
|
|
36
|
+
* @param opts - Router-level options.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```ts
|
|
40
|
+
* const router = new WebhookRouter({
|
|
41
|
+
* prefix: "/webhooks",
|
|
42
|
+
* verify: createHmacVerifier({ headerName: "x-signature", secret }),
|
|
43
|
+
* onError: (error) => Response.json({ error: error.message }, { status: 500 }),
|
|
44
|
+
* });
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
constructor(opts?: WebhookRouterOptions);
|
|
48
|
+
/**
|
|
49
|
+
* Register a webhook handler for a specific path.
|
|
50
|
+
*
|
|
51
|
+
* When a schema is provided, `payload` is inferred from the schema output type.
|
|
52
|
+
*
|
|
53
|
+
* @param path - Route path relative to configured prefix, starting with `/` (e.g. `"/stripe"`).
|
|
54
|
+
* @param handlerOrOptions - Handler function or schema-based registration options.
|
|
55
|
+
* @returns The same router instance with an updated internal route type map.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```ts
|
|
59
|
+
* router.register("/stripe", {
|
|
60
|
+
* schema: stripeEventSchema,
|
|
61
|
+
* handler: async ({ payload }) => {
|
|
62
|
+
* console.log(payload.type); // typed from stripeEventSchema
|
|
63
|
+
* },
|
|
64
|
+
* });
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
register<Path extends `/${string}`, TSchema extends StandardSchemaV1<unknown, unknown>>(path: Path, handlerOrOptions: SchemaRouteOptions<TSchema>): WebhookRouter<TMap & Record<Path, InferSchemaOutput<TSchema>>>;
|
|
68
|
+
/**
|
|
69
|
+
* Register a webhook handler for a specific path, with schema-less registration options.
|
|
70
|
+
*
|
|
71
|
+
* @param path - Route path relative to configured prefix, starting with `/` (e.g. `"/stripe"`).
|
|
72
|
+
* @param handlerOrOptions - Registration options without a schema.
|
|
73
|
+
* @returns The same router instance with an updated internal route type map.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```ts
|
|
77
|
+
* router.register("/ping", {
|
|
78
|
+
* before: (ctx) => console.log("received", ctx.path),
|
|
79
|
+
* handler: () => Response.json({ ok: true }),
|
|
80
|
+
* });
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
register<Path extends `/${string}`, TPayload>(path: Path, handlerOrOptions: RegisterOptions<TPayload>): WebhookRouter<TMap & Record<Path, TPayload>>;
|
|
84
|
+
/**
|
|
85
|
+
* Register a webhook handler for a specific path, using a plain handler function.
|
|
86
|
+
*
|
|
87
|
+
* @param path - Route path relative to configured prefix, starting with `/` (e.g. `"/stripe"`).
|
|
88
|
+
* @param handlerOrOptions - Handler function to process the webhook.
|
|
89
|
+
* @returns The same router instance with an updated internal route type map.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```ts
|
|
93
|
+
* router.register("/health", () => Response.json({ status: "ok" }));
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
register<Path extends `/${string}`>(path: Path, handlerOrOptions: WebhookHandler): WebhookRouter<TMap & Record<Path, unknown>>;
|
|
97
|
+
/**
|
|
98
|
+
* Handles an incoming webhook request.
|
|
99
|
+
*
|
|
100
|
+
* The request body is read exactly once; hooks and handlers receive the raw
|
|
101
|
+
* bytes through the webhook context instead of the request stream.
|
|
102
|
+
*
|
|
103
|
+
* @param request - Incoming Web API request.
|
|
104
|
+
* @returns Web API response for the runtime to send back.
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```ts
|
|
108
|
+
* // Framework-agnostic: works with any Web API Request/Response runtime.
|
|
109
|
+
* export async function POST(request: Request): Promise<Response> {
|
|
110
|
+
* return router.handle(request);
|
|
111
|
+
* }
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
handle(request: Request): Promise<Response>;
|
|
115
|
+
/** Resolves the incoming request's URL to a registered route key, or `null` if it doesn't match the configured prefix. */
|
|
116
|
+
private matchPath;
|
|
117
|
+
/** Builds the error response for a failed request, deferring to the global error hook when set. */
|
|
118
|
+
private handleError;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Factory helper for creating a webhook router instance.
|
|
122
|
+
*
|
|
123
|
+
* @param opts - Optional global router options.
|
|
124
|
+
* @returns A new webhook router.
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* ```ts
|
|
128
|
+
* import { createWebhookRouter } from "@zap-studio/webhooks";
|
|
129
|
+
*
|
|
130
|
+
* const router = createWebhookRouter({ prefix: "/webhooks" });
|
|
131
|
+
* router.register("/stripe", { schema: stripeEventSchema, handler });
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
declare const createWebhookRouter: (opts?: WebhookRouterOptions) => WebhookRouter;
|
|
135
|
+
//#endregion
|
|
136
|
+
export { WebhookRouter, createWebhookRouter };
|
|
137
|
+
//# sourceMappingURL=router.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"router.d.ts","names":[],"sources":["../src/router.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;cA2La,cAAc;mBACR;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;;;;;;;;;;;;;;;EAgBjB,YAAY,OAAM;;;;;;;;;;;;;;;;;;;;EA4BlB,SACE,2BACA,gBAAgB,oCAEhB,MAAM,MACN,kBAAkB,mBAAmB,WACpC,cAAc,OAAO,OAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;EAgBvD,SAAS,2BAA2B,UAClC,MAAM,MACN,kBAAkB,gBAAgB,YACjC,cAAc,OAAO,OAAO,MAAM;;;;;;;;;;;;;EAarC,SAAS,2BACP,MAAM,MACN,kBAAkB,iBACjB,cAAc,OAAO,OAAO;;;;;;;;;;;;;;;;;;EAgC/B,OAAa,SAAS,UAAU,QAAQ;;UAqDhC;;UAsBM;;;;;;;;;;;;;;;;cAoCH,sBACX,OAAO,yBACN"}
|