@nest-batch/webhook 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +395 -0
- package/dist/src/index.d.ts +32 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +71 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/module-options.d.ts +163 -0
- package/dist/src/module-options.d.ts.map +1 -0
- package/dist/src/module-options.js +124 -0
- package/dist/src/module-options.js.map +1 -0
- package/dist/src/webhook-batch.module.d.ts +59 -0
- package/dist/src/webhook-batch.module.d.ts.map +1 -0
- package/dist/src/webhook-batch.module.js +94 -0
- package/dist/src/webhook-batch.module.js.map +1 -0
- package/dist/src/webhook-batch.observer.d.ts +144 -0
- package/dist/src/webhook-batch.observer.d.ts.map +1 -0
- package/dist/src/webhook-batch.observer.js +306 -0
- package/dist/src/webhook-batch.observer.js.map +1 -0
- package/dist/src/webhook-signing.d.ts +70 -0
- package/dist/src/webhook-signing.d.ts.map +1 -0
- package/dist/src/webhook-signing.js +129 -0
- package/dist/src/webhook-signing.js.map +1 -0
- package/package.json +69 -0
- package/src/index.ts +46 -0
- package/src/module-options.ts +276 -0
- package/src/webhook-batch.module.ts +133 -0
- package/src/webhook-batch.observer.ts +408 -0
- package/src/webhook-signing.ts +185 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 easdkr
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
# `@nest-batch/webhook`
|
|
2
|
+
|
|
3
|
+
Webhook delivery observer for
|
|
4
|
+
[`@nest-batch/core`](../core). The package ships a
|
|
5
|
+
`WebhookBatchObserver` that subscribes to the `BATCH_EVENT.*`
|
|
6
|
+
lifecycle stream and POSTs an HMAC-SHA256-signed JSON envelope
|
|
7
|
+
to one or more URLs, with exponential-backoff retry on
|
|
8
|
+
5xx / network errors, a `logger.warn` dead-letter on final
|
|
9
|
+
failure, and a hard `no retry on 4xx` rule.
|
|
10
|
+
|
|
11
|
+
> **The observer is transport-agnostic.** It signs + POSTs
|
|
12
|
+
> envelopes; it does not care whether the job was driven by
|
|
13
|
+
> BullMQ, Kafka, or the in-process strategy. Any transport
|
|
14
|
+
> package that bridges `QueueEvents` (or equivalent) to the
|
|
15
|
+
> `BatchObserver.onEvent` entry point will deliver events to
|
|
16
|
+
> this observer without any extra wiring on the host's side.
|
|
17
|
+
|
|
18
|
+
The package is a **sibling**, not a replacement. The dependency
|
|
19
|
+
direction is strict and one-way:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
@nest-batch/webhook ──▶ @nest-batch/core
|
|
23
|
+
│
|
|
24
|
+
└──────────────▶ @nestjs/common, @nestjs/core (peer)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`@nest-batch/core` does not know this package exists. It
|
|
28
|
+
cannot — the boundary is enforced by
|
|
29
|
+
[`packages/core/tests/core/boundary/no-forbidden-imports.test.ts`](../core/tests/core/boundary/no-forbidden-imports.test.ts),
|
|
30
|
+
which scans the core source tree and fails the build if a
|
|
31
|
+
forbidden package — `bullmq`, `kafkajs`, `mikro-orm`, `typeorm`,
|
|
32
|
+
`drizzle-orm`, `cron` — appears as a core import.
|
|
33
|
+
|
|
34
|
+
The observer uses **native `fetch`** (Node 20+). No HTTP client
|
|
35
|
+
(`undici` / `axios` / `node-fetch`) is added as a peer dep —
|
|
36
|
+
the host does not need to ship a separate HTTP library to
|
|
37
|
+
enable webhook delivery. `AbortController` provides the
|
|
38
|
+
per-attempt timeout.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pnpm add @nest-batch/webhook
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Peer dependencies the host must already provide:
|
|
49
|
+
|
|
50
|
+
| Package | Range |
|
|
51
|
+
| ------------------ | ------------- |
|
|
52
|
+
| `@nest-batch/core` | `workspace:*` |
|
|
53
|
+
| `@nestjs/common` | `^10 \|\| ^11` |
|
|
54
|
+
| `@nestjs/core` | `^10 \|\| ^11` |
|
|
55
|
+
|
|
56
|
+
Node 20+ is required for the native `fetch` / `AbortController`
|
|
57
|
+
runtime. Older Node versions are not supported.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Peer dependencies
|
|
62
|
+
|
|
63
|
+
| Package | Range | Notes |
|
|
64
|
+
| ------------------ | --------------- | -------------------------------------------------------------------------------------- |
|
|
65
|
+
| `@nest-batch/core` | `workspace:*` | The batch engine. The observer only consumes the `BatchObserver` / `BATCH_EVENT` surface. |
|
|
66
|
+
| `@nestjs/common` | `^10 \|\| ^11` | For `@Module` / `Module` / injection tokens. Nest 10 and 11 are both supported. |
|
|
67
|
+
| `@nestjs/core` | `^10 \|\| ^11` | Peer-declared for the dynamic-module surface; not used at runtime. |
|
|
68
|
+
|
|
69
|
+
The package deliberately does **not** declare a peer dep on
|
|
70
|
+
`undici` / `axios` / `node-fetch`. Webhook delivery uses the
|
|
71
|
+
runtime's built-in `fetch` + `AbortController` (Node 20+). Hosts
|
|
72
|
+
that prefer a different HTTP client can monkey-patch the global
|
|
73
|
+
`fetch` at bootstrap time, but no such override is necessary in
|
|
74
|
+
the common case.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Wiring
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { Module } from '@nestjs/common';
|
|
82
|
+
import { NestBatchModule } from '@nest-batch/core';
|
|
83
|
+
import { BullmqAdapter } from '@nest-batch/bullmq';
|
|
84
|
+
import { WebhookBatchModule } from '@nest-batch/webhook';
|
|
85
|
+
|
|
86
|
+
@Module({
|
|
87
|
+
imports: [
|
|
88
|
+
NestBatchModule.forRoot({
|
|
89
|
+
// ... your persistence + transport adapters
|
|
90
|
+
}),
|
|
91
|
+
BullmqAdapter.forRoot({
|
|
92
|
+
connection: { host: process.env.REDIS_HOST, port: 6379 },
|
|
93
|
+
autoStartWorker: true,
|
|
94
|
+
}),
|
|
95
|
+
WebhookBatchModule.forRoot({
|
|
96
|
+
secret: process.env.WEBHOOK_HMAC_SECRET, // 32+ bytes recommended
|
|
97
|
+
urls: [
|
|
98
|
+
'https://hooks.example.com/nest-batch',
|
|
99
|
+
'https://ops.example.com/ingest/nest-batch',
|
|
100
|
+
],
|
|
101
|
+
}),
|
|
102
|
+
],
|
|
103
|
+
})
|
|
104
|
+
export class AppModule {}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`WebhookBatchModule.forRoot({...})` accepts:
|
|
108
|
+
|
|
109
|
+
| Field | Type | Required | Default | Notes |
|
|
110
|
+
| ------------ | --------------- | -------- | ----------------------------------------- | --------------------------------------------------------------------- |
|
|
111
|
+
| `secret` | `string` | required (or via env) | — | Host-injected HMAC-SHA256 secret. 32+ bytes of randomness recommended. |
|
|
112
|
+
| `urls` | `string[]` | yes | — | One or more absolute URLs the observer fans out to on every event. |
|
|
113
|
+
| `events` | `BatchEventType[]` | no | `[JOB_COMPLETED, JOB_FAILED, STEP_FAILED]` | Subscription filter. Events not in the set are dropped silently. |
|
|
114
|
+
| `attempts` | `number` | no | `4` | Total POST attempts. Clamped to `[1, 4]`. `1` = no retries. |
|
|
115
|
+
| `timeoutMs` | `number` | no | `10_000` | Per-attempt HTTP timeout in ms. A timeout is treated as a network error. |
|
|
116
|
+
| `logger` | `WebhookLogger` | no | `new Logger('WebhookBatchObserver')` | Nest-`Logger`-compatible surface for the dead-letter `warn` line. |
|
|
117
|
+
|
|
118
|
+
`WebhookBatchModule` is registered as `global: true` (matching
|
|
119
|
+
`NestBatchModule` and the transport adapters) so consumers do
|
|
120
|
+
not need to re-import it in every sub-module. The observer is
|
|
121
|
+
auto-registered against the `BatchObserver` token, so the
|
|
122
|
+
executor / runtime services pick it up via the
|
|
123
|
+
`@Optional() observer: BatchObserver = new NoopBatchObserver()`
|
|
124
|
+
injection path without any extra wiring.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Events
|
|
129
|
+
|
|
130
|
+
The v1 subscription default is three events. A `BatchEvent`
|
|
131
|
+
whose `type` is not in the set is dropped silently — the
|
|
132
|
+
observer never holds the executor back.
|
|
133
|
+
|
|
134
|
+
| Event | Constant | When it fires |
|
|
135
|
+
| ---------------------------------- | ------------------------------ | -------------------------------------------- |
|
|
136
|
+
| `nest-batch.job.completed` | `BATCH_EVENT.JOB_COMPLETED` | A `JobExecution` reached the `COMPLETED` terminal state. |
|
|
137
|
+
| `nest-batch.job.failed` | `BATCH_EVENT.JOB_FAILED` | A `JobExecution` reached the `FAILED` terminal state. |
|
|
138
|
+
| `nest-batch.step.failed` | `BATCH_EVENT.STEP_FAILED` | A `StepExecution` reached the `FAILED` terminal state. |
|
|
139
|
+
|
|
140
|
+
Override the default via the `events` option:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
WebhookBatchModule.forRoot({
|
|
144
|
+
secret: process.env.WEBHOOK_HMAC_SECRET,
|
|
145
|
+
urls: ['https://hooks.example.com/nest-batch'],
|
|
146
|
+
events: [BATCH_EVENT.JOB_COMPLETED, BATCH_EVENT.JOB_FAILED],
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
A future v2 may widen the default to include `STEP_*` /
|
|
151
|
+
`CHUNK_*` / `ITEM_*` events; in v1 only the three
|
|
152
|
+
terminal-state events trigger a POST.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Retry policy
|
|
157
|
+
|
|
158
|
+
The fixed backoff schedule is `[1s, 5s, 25s, 125s]` — four
|
|
159
|
+
attempts total (one initial POST plus three retries). The
|
|
160
|
+
schedule is the v1 contract; the test suite
|
|
161
|
+
([`tests/webhook-observer.test.ts`](./tests/webhook-observer.test.ts),
|
|
162
|
+
T-AC-5) asserts against it.
|
|
163
|
+
|
|
164
|
+
| Outcome | Behavior |
|
|
165
|
+
| --------------- | --------------------------------------------------------------------------------------- |
|
|
166
|
+
| `2xx` | Success. The observer marks the delivery done. |
|
|
167
|
+
| `3xx` | Treated as a redirect. The observer follows the redirect once; if the redirect target returns 4xx / 5xx, that target's status drives the retry decision. |
|
|
168
|
+
| `4xx` | **No retry.** Logged at `warn` level with the URL and the response body. The host is expected to fix the misconfiguration (bad URL, missing auth, malformed payload). |
|
|
169
|
+
| `5xx` | **Retried** through the full 4-attempt budget at the 1s / 5s / 25s / 125s schedule. After the final attempt, dead-letter. |
|
|
170
|
+
| Network error | **Retried** through the full 4-attempt budget. After the final attempt, dead-letter. |
|
|
171
|
+
| Timeout | Treated as a network error. **Retried** through the full 4-attempt budget. |
|
|
172
|
+
|
|
173
|
+
### Fast-mode test override
|
|
174
|
+
|
|
175
|
+
The retry schedule can be overridden to `[1ms, 5ms, 25ms, 125ms]`
|
|
176
|
+
by setting the `WEBHOOK_TEST_FAST=1` env var. The override lets
|
|
177
|
+
the test suite exercise the 4-attempt retry path in <200ms
|
|
178
|
+
instead of the 156-second production schedule. The override is
|
|
179
|
+
gated behind an env var so production cannot trip it by
|
|
180
|
+
accident.
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
WEBHOOK_TEST_FAST=1 pnpm --filter @nest-batch/webhook test -- tests/webhook-observer.test.ts
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Dead-letter
|
|
187
|
+
|
|
188
|
+
After the final failed attempt (4xx not retried, or 5xx /
|
|
189
|
+
network / timeout after all 4 attempts), the observer emits:
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
logger.warn(
|
|
193
|
+
`[WebhookBatchObserver] dead-letter url=${url} attempts=${attempts} ` +
|
|
194
|
+
`lastStatus=${lastStatus ?? 'n/a'} lastError=${lastError} ` +
|
|
195
|
+
`type=${event.type} jobExecutionId=${event.jobExecutionId} ` +
|
|
196
|
+
`secret_sha256=${fingerprint}`,
|
|
197
|
+
);
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
The dead-letter is a log line, not a database row. The host
|
|
201
|
+
ships its log aggregator (Datadog, CloudWatch, Loki, ...)
|
|
202
|
+
to recover dead-lettered URLs. A future v2 may add a
|
|
203
|
+
`DeadLetterStore` token; v1 ships the log line only.
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## HMAC signature
|
|
208
|
+
|
|
209
|
+
Every outbound POST carries a Stripe-style signature header:
|
|
210
|
+
|
|
211
|
+
```
|
|
212
|
+
X-Nest-Batch-Signature: t=<unix-seconds>,v1=<hex-hmac-sha256>
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Where:
|
|
216
|
+
|
|
217
|
+
- `t=<unix>` is the unix-seconds timestamp the signature is
|
|
218
|
+
pinned to. The receiver uses it to enforce a replay window
|
|
219
|
+
(recommended: 5 minutes). The timestamp is also sent as a
|
|
220
|
+
separate `X-Nest-Batch-Timestamp: <unix>` header for
|
|
221
|
+
receivers that prefer parsing HTTP headers to parsing the
|
|
222
|
+
signature header.
|
|
223
|
+
- `v1=<hex>` is the lowercase hex of
|
|
224
|
+
`HMAC_SHA256(secret, "<unix>.<raw-body>")`.
|
|
225
|
+
- The `<raw-body>` is the EXACT JSON-serialized request body
|
|
226
|
+
bytes (not a re-serialization). The receiver MUST HMAC the
|
|
227
|
+
body it received, byte-for-byte; the observer does not
|
|
228
|
+
re-serialize, so a different key order on the receiver
|
|
229
|
+
side will fail verification.
|
|
230
|
+
|
|
231
|
+
### Receiver-side verification
|
|
232
|
+
|
|
233
|
+
The package exports `verifyV1` / `parseSignatureHeader` from
|
|
234
|
+
`@nest-batch/webhook` so a Node receiver can verify the
|
|
235
|
+
signature without re-implementing the crypto:
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
import {
|
|
239
|
+
parseSignatureHeader,
|
|
240
|
+
verifyV1,
|
|
241
|
+
SIGNATURE_HEADER_NAME,
|
|
242
|
+
} from '@nest-batch/webhook';
|
|
243
|
+
|
|
244
|
+
app.post('/hook', (req, res) => {
|
|
245
|
+
const header = String(req.headers[SIGNATURE_HEADER_NAME.toLowerCase()]);
|
|
246
|
+
const raw = req.rawBody; // capture in your body parser
|
|
247
|
+
const { timestamp, v1 } = parseSignatureHeader(header);
|
|
248
|
+
if (!verifyV1(process.env.WEBHOOK_HMAC_SECRET, timestamp, raw, v1)) {
|
|
249
|
+
return res.status(401).json({ error: 'invalid signature' });
|
|
250
|
+
}
|
|
251
|
+
// ... accept the envelope
|
|
252
|
+
res.status(200).json({ ok: true });
|
|
253
|
+
});
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Express users: mount `express.raw({ type: 'application/json' })`
|
|
257
|
+
on the webhook route so `req.body` is a `Buffer` (not the
|
|
258
|
+
default JSON-parsed object). The v1 signature is over the
|
|
259
|
+
**raw bytes**, not the parsed JSON. The example above
|
|
260
|
+
assumes the host mounted a raw-body parser that stashes the
|
|
261
|
+
bytes on `req.rawBody`.
|
|
262
|
+
|
|
263
|
+
### Why v1?
|
|
264
|
+
|
|
265
|
+
The `v1` key is the signature-scheme version, not the
|
|
266
|
+
envelope version. A future v2 may add `v2=`-prefixed schemes
|
|
267
|
+
(e.g. a SHA-512 variant or a multi-rotation scheme);
|
|
268
|
+
receivers MUST reject unknown `vN` keys. The v1 contract is
|
|
269
|
+
HMAC-SHA256 over `<unix>.<raw-body>`, lowercase hex, single
|
|
270
|
+
field.
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## Secret handling
|
|
275
|
+
|
|
276
|
+
The HMAC secret is the single most sensitive value the package
|
|
277
|
+
handles. The contract:
|
|
278
|
+
|
|
279
|
+
- **Host-injection is primary.** Pass `secret` to `forRoot({...})`
|
|
280
|
+
at module-build time. The package binds it to the
|
|
281
|
+
`WebhookBatchObserver` instance via `WEBHOOK_MODULE_OPTIONS`
|
|
282
|
+
(a private `Symbol.for` token); it is not exported, not
|
|
283
|
+
injectable, not reachable from any public API.
|
|
284
|
+
- **Env fallback is secondary.** If `secret` is omitted,
|
|
285
|
+
`forRoot({...})` falls back to `process.env.WEBHOOK_HMAC_SECRET`.
|
|
286
|
+
Env is the safety net for hosts that do not want to thread
|
|
287
|
+
the secret through their config service explicitly. The
|
|
288
|
+
host's `ConfigModule` should set the env var from the
|
|
289
|
+
secret manager.
|
|
290
|
+
- **Neither is the secret on disk.** The package does not read
|
|
291
|
+
a file, does not read a CLI arg, does not read a Vault path.
|
|
292
|
+
The host owns the secret source.
|
|
293
|
+
- **The secret is never logged.** The dead-letter line emits a
|
|
294
|
+
SHA-256 fingerprint (`secret_sha256=abc123...`, first 12 hex
|
|
295
|
+
chars) so operators can correlate dead-letters across
|
|
296
|
+
services without exposing the secret. The full secret value
|
|
297
|
+
is never written to a `logger` call, never serialized into a
|
|
298
|
+
dead-letter body, never returned by any public API, never
|
|
299
|
+
echoed in a stack trace.
|
|
300
|
+
|
|
301
|
+
The pinned acceptance test (T-AC-5) asserts no
|
|
302
|
+
`WEBHOOK_HMAC_SECRET` substring (or, equivalently, the
|
|
303
|
+
`secret` value the test injected) appears in the captured log
|
|
304
|
+
stream across the full 4-attempt retry budget, the 4xx
|
|
305
|
+
dead-letter path, the 5xx dead-letter path, the success path,
|
|
306
|
+
and the debug path.
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## Launcher-only deployment — events do NOT fire without a worker
|
|
311
|
+
|
|
312
|
+
`@nest-batch/webhook` is a `BatchObserver` — it consumes the
|
|
313
|
+
event stream the executor / runtime services produce. The
|
|
314
|
+
event stream only fires when the host has wired a transport
|
|
315
|
+
**with** a running consumer:
|
|
316
|
+
|
|
317
|
+
- `BullmqAdapter.forRoot({ autoStartWorker: true })` — events
|
|
318
|
+
fire (the worker drives the lifecycle and the
|
|
319
|
+
`QueueEvents` bridge fans them out).
|
|
320
|
+
- `BullmqAdapter.forRoot({ autoStartWorker: false })` — **no
|
|
321
|
+
events fire.** The launcher enqueues; no worker consumes;
|
|
322
|
+
the lifecycle never reaches `COMPLETED` / `FAILED`; the
|
|
323
|
+
observer never sees anything.
|
|
324
|
+
- `InProcessAdapter.forRoot()` — events fire (the strategy
|
|
325
|
+
runs the lifecycle in-process).
|
|
326
|
+
- `KafkaAdapter.forRoot({ autoStartConsumer: true })` — events
|
|
327
|
+
fire (the consumer drives the lifecycle and the consumer
|
|
328
|
+
bridge fans them out).
|
|
329
|
+
- `KafkaAdapter.forRoot({ autoStartConsumer: false })` — **no
|
|
330
|
+
events fire.** Same reason as the BullMQ launcher-only case.
|
|
331
|
+
|
|
332
|
+
This is the v1 contract. A launcher-only deployment (an API
|
|
333
|
+
service that only enqueues) does NOT need to install
|
|
334
|
+
`@nest-batch/webhook` — the observer would be dead code.
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
## What is NOT in this package
|
|
339
|
+
|
|
340
|
+
- A persistence adapter. Use `@nest-batch/mikro-orm`,
|
|
341
|
+
`@nest-batch/typeorm`, `@nest-batch/drizzle`, or
|
|
342
|
+
`@nest-batch/prisma` to wire a `JobRepository`. The observer
|
|
343
|
+
is event-stream-only; it does not read or write any
|
|
344
|
+
`JobExecution` rows.
|
|
345
|
+
- A batch engine. Job / Step / Chunk / Tasklet semantics,
|
|
346
|
+
checkpoint, restart, skip, business retry, and the event
|
|
347
|
+
stream itself live in
|
|
348
|
+
[`@nest-batch/core`](../core). The observer is the
|
|
349
|
+
downstream consumer.
|
|
350
|
+
- A transport. Use `@nest-batch/bullmq` or
|
|
351
|
+
`@nest-batch/kafka` to drive the lifecycle. The observer
|
|
352
|
+
does not enqueue or consume.
|
|
353
|
+
- A retry-policy module. The fixed `[1s, 5s, 25s, 125s]`
|
|
354
|
+
backoff is the v1 contract. A future v2 may add a
|
|
355
|
+
`retryDelaysMs` override; v1 ships the fixed schedule only.
|
|
356
|
+
- A dead-letter database. The dead-letter is a `logger.warn`
|
|
357
|
+
line; a future v2 may add a `DeadLetterStore` token.
|
|
358
|
+
- A scheduler. Cron-style scheduling lives in
|
|
359
|
+
`@nest-batch/core`'s `@BatchScheduled` decorator. The
|
|
360
|
+
observer does not fire on a timer; it fires on the
|
|
361
|
+
executor's event stream.
|
|
362
|
+
- An admin UI, metrics backend, or tracing backend. Hook a
|
|
363
|
+
different `BatchObserver` (or extend this one) to ship
|
|
364
|
+
events where you need them.
|
|
365
|
+
- Alternative HTTP transports (e.g. webhook-over-mqtt, gRPC
|
|
366
|
+
webhooks). The observer uses HTTP POST + HMAC-SHA256. A
|
|
367
|
+
future sibling package could ship a webhook-over-mqtt
|
|
368
|
+
observer that implements the same `BatchObserver`
|
|
369
|
+
contract.
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
373
|
+
## Scripts
|
|
374
|
+
|
|
375
|
+
```bash
|
|
376
|
+
pnpm --filter @nest-batch/webhook build # SWC transpile + tsc declarations
|
|
377
|
+
pnpm --filter @nest-batch/webhook test # vitest run (T-AC-5; see env note below)
|
|
378
|
+
pnpm --filter @nest-batch/webhook test:watch # vitest watch
|
|
379
|
+
pnpm --filter @nest-batch/webhook typecheck # tsc --noEmit
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
The T-AC-5 test
|
|
383
|
+
([`tests/webhook-observer.test.ts`](./tests/webhook-observer.test.ts))
|
|
384
|
+
uses `WEBHOOK_TEST_FAST=1` to override the retry schedule to
|
|
385
|
+
milliseconds. Run it with:
|
|
386
|
+
|
|
387
|
+
```bash
|
|
388
|
+
WEBHOOK_TEST_FAST=1 pnpm --filter @nest-batch/webhook test -- tests/webhook-observer.test.ts --reporter=verbose
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
The full `pnpm test` run uses the same env var (set at the
|
|
392
|
+
top of the test file) so the suite finishes in <1s. The test
|
|
393
|
+
stands up a real `http.createServer().listen(0)` server on a
|
|
394
|
+
random port; no external service (Redis, Postgres) is
|
|
395
|
+
required.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public API barrel for `@nest-batch/webhook`.
|
|
3
|
+
*
|
|
4
|
+
* Hosts import the factory (`forRoot`) and the observer class
|
|
5
|
+
* (`WebhookBatchObserver`) from this barrel; everything else
|
|
6
|
+
* is an implementation detail. The barrel re-exports:
|
|
7
|
+
*
|
|
8
|
+
* - `forRoot` — the synchronous `DynamicModule` factory. The
|
|
9
|
+
* host calls `WebhookBatchModule.forRoot({...})` (the
|
|
10
|
+
* `WebhookBatchModule` is re-exported alongside so the
|
|
11
|
+
* type is reachable from this entry point).
|
|
12
|
+
* - `WebhookBatchObserver` — the concrete class. Useful for
|
|
13
|
+
* type-strict consumers that prefer class injection.
|
|
14
|
+
* - `BATCH_EVENT` — the `BATCH_EVENT` constants from
|
|
15
|
+
* `@nest-batch/core`, re-exported so a host that wants to
|
|
16
|
+
* filter subscriptions does not have to add
|
|
17
|
+
* `@nest-batch/core` as a direct dep.
|
|
18
|
+
* - `signV1` / `buildSignatureHeader` / `parseSignatureHeader`
|
|
19
|
+
* / `verifyV1` / `fingerprintSecret` — the HMAC signing
|
|
20
|
+
* helpers, useful for hosts that want to write their own
|
|
21
|
+
* webhook receiver against the same contract.
|
|
22
|
+
* - The TypeScript types: `WebhookBatchModuleOptions`,
|
|
23
|
+
* `ResolvedWebhookOptions`, `WebhookLogger`,
|
|
24
|
+
* `WebhookEnvelope`.
|
|
25
|
+
*/
|
|
26
|
+
export { forRoot, WebhookBatchModule, WebhookBatchObserver } from './webhook-batch.module';
|
|
27
|
+
export { BATCH_EVENT } from '@nest-batch/core';
|
|
28
|
+
export type { BatchEvent, BatchEventType, BatchObserver, } from '@nest-batch/core';
|
|
29
|
+
export { signV1, buildSignatureHeader, parseSignatureHeader, verifyV1, fingerprintSecret, SIGNATURE_HEADER_NAME, } from './webhook-signing';
|
|
30
|
+
export type { WebhookEnvelope } from './webhook-batch.observer';
|
|
31
|
+
export type { ResolvedWebhookOptions, WebhookBatchModuleOptions, WebhookLogger, } from './module-options';
|
|
32
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,EAAE,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC3F,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,YAAY,EACV,UAAU,EACV,cAAc,EACd,aAAa,GACd,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,MAAM,EACN,oBAAoB,EACpB,oBAAoB,EACpB,QAAQ,EACR,iBAAiB,EACjB,qBAAqB,GACtB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAChE,YAAY,EACV,sBAAsB,EACtB,yBAAyB,EACzB,aAAa,GACd,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public API barrel for `@nest-batch/webhook`.
|
|
3
|
+
*
|
|
4
|
+
* Hosts import the factory (`forRoot`) and the observer class
|
|
5
|
+
* (`WebhookBatchObserver`) from this barrel; everything else
|
|
6
|
+
* is an implementation detail. The barrel re-exports:
|
|
7
|
+
*
|
|
8
|
+
* - `forRoot` — the synchronous `DynamicModule` factory. The
|
|
9
|
+
* host calls `WebhookBatchModule.forRoot({...})` (the
|
|
10
|
+
* `WebhookBatchModule` is re-exported alongside so the
|
|
11
|
+
* type is reachable from this entry point).
|
|
12
|
+
* - `WebhookBatchObserver` — the concrete class. Useful for
|
|
13
|
+
* type-strict consumers that prefer class injection.
|
|
14
|
+
* - `BATCH_EVENT` — the `BATCH_EVENT` constants from
|
|
15
|
+
* `@nest-batch/core`, re-exported so a host that wants to
|
|
16
|
+
* filter subscriptions does not have to add
|
|
17
|
+
* `@nest-batch/core` as a direct dep.
|
|
18
|
+
* - `signV1` / `buildSignatureHeader` / `parseSignatureHeader`
|
|
19
|
+
* / `verifyV1` / `fingerprintSecret` — the HMAC signing
|
|
20
|
+
* helpers, useful for hosts that want to write their own
|
|
21
|
+
* webhook receiver against the same contract.
|
|
22
|
+
* - The TypeScript types: `WebhookBatchModuleOptions`,
|
|
23
|
+
* `ResolvedWebhookOptions`, `WebhookLogger`,
|
|
24
|
+
* `WebhookEnvelope`.
|
|
25
|
+
*/ "use strict";
|
|
26
|
+
Object.defineProperty(exports, "__esModule", {
|
|
27
|
+
value: true
|
|
28
|
+
});
|
|
29
|
+
function _export(target, all) {
|
|
30
|
+
for(var name in all)Object.defineProperty(target, name, {
|
|
31
|
+
enumerable: true,
|
|
32
|
+
get: Object.getOwnPropertyDescriptor(all, name).get
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
_export(exports, {
|
|
36
|
+
get BATCH_EVENT () {
|
|
37
|
+
return _core.BATCH_EVENT;
|
|
38
|
+
},
|
|
39
|
+
get SIGNATURE_HEADER_NAME () {
|
|
40
|
+
return _webhooksigning.SIGNATURE_HEADER_NAME;
|
|
41
|
+
},
|
|
42
|
+
get WebhookBatchModule () {
|
|
43
|
+
return _webhookbatchmodule.WebhookBatchModule;
|
|
44
|
+
},
|
|
45
|
+
get WebhookBatchObserver () {
|
|
46
|
+
return _webhookbatchmodule.WebhookBatchObserver;
|
|
47
|
+
},
|
|
48
|
+
get buildSignatureHeader () {
|
|
49
|
+
return _webhooksigning.buildSignatureHeader;
|
|
50
|
+
},
|
|
51
|
+
get fingerprintSecret () {
|
|
52
|
+
return _webhooksigning.fingerprintSecret;
|
|
53
|
+
},
|
|
54
|
+
get forRoot () {
|
|
55
|
+
return _webhookbatchmodule.forRoot;
|
|
56
|
+
},
|
|
57
|
+
get parseSignatureHeader () {
|
|
58
|
+
return _webhooksigning.parseSignatureHeader;
|
|
59
|
+
},
|
|
60
|
+
get signV1 () {
|
|
61
|
+
return _webhooksigning.signV1;
|
|
62
|
+
},
|
|
63
|
+
get verifyV1 () {
|
|
64
|
+
return _webhooksigning.verifyV1;
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
const _webhookbatchmodule = require("./webhook-batch.module");
|
|
68
|
+
const _core = require("@nest-batch/core");
|
|
69
|
+
const _webhooksigning = require("./webhook-signing");
|
|
70
|
+
|
|
71
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/index.ts"],"sourcesContent":["/**\n * Public API barrel for `@nest-batch/webhook`.\n *\n * Hosts import the factory (`forRoot`) and the observer class\n * (`WebhookBatchObserver`) from this barrel; everything else\n * is an implementation detail. The barrel re-exports:\n *\n * - `forRoot` — the synchronous `DynamicModule` factory. The\n * host calls `WebhookBatchModule.forRoot({...})` (the\n * `WebhookBatchModule` is re-exported alongside so the\n * type is reachable from this entry point).\n * - `WebhookBatchObserver` — the concrete class. Useful for\n * type-strict consumers that prefer class injection.\n * - `BATCH_EVENT` — the `BATCH_EVENT` constants from\n * `@nest-batch/core`, re-exported so a host that wants to\n * filter subscriptions does not have to add\n * `@nest-batch/core` as a direct dep.\n * - `signV1` / `buildSignatureHeader` / `parseSignatureHeader`\n * / `verifyV1` / `fingerprintSecret` — the HMAC signing\n * helpers, useful for hosts that want to write their own\n * webhook receiver against the same contract.\n * - The TypeScript types: `WebhookBatchModuleOptions`,\n * `ResolvedWebhookOptions`, `WebhookLogger`,\n * `WebhookEnvelope`.\n */\nexport { forRoot, WebhookBatchModule, WebhookBatchObserver } from './webhook-batch.module';\nexport { BATCH_EVENT } from '@nest-batch/core';\nexport type {\n BatchEvent,\n BatchEventType,\n BatchObserver,\n} from '@nest-batch/core';\nexport {\n signV1,\n buildSignatureHeader,\n parseSignatureHeader,\n verifyV1,\n fingerprintSecret,\n SIGNATURE_HEADER_NAME,\n} from './webhook-signing';\nexport type { WebhookEnvelope } from './webhook-batch.observer';\nexport type {\n ResolvedWebhookOptions,\n WebhookBatchModuleOptions,\n WebhookLogger,\n} from './module-options';\n"],"names":["BATCH_EVENT","SIGNATURE_HEADER_NAME","WebhookBatchModule","WebhookBatchObserver","buildSignatureHeader","fingerprintSecret","forRoot","parseSignatureHeader","signV1","verifyV1"],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;CAwBC;;;;;;;;;;;QAEQA;eAAAA,iBAAW;;QAYlBC;eAAAA,qCAAqB;;QAbLC;eAAAA,sCAAkB;;QAAEC;eAAAA,wCAAoB;;QASxDC;eAAAA,oCAAoB;;QAGpBC;eAAAA,iCAAiB;;QAZVC;eAAAA,2BAAO;;QAUdC;eAAAA,oCAAoB;;QAFpBC;eAAAA,sBAAM;;QAGNC;eAAAA,wBAAQ;;;oCAXwD;sBACtC;gCAarB"}
|