@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
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { BatchEventType } from '@nest-batch/core';
|
|
2
|
+
/**
|
|
3
|
+
* Public options bag for `WebhookBatchModule.forRoot()`.
|
|
4
|
+
*
|
|
5
|
+
* The contract the test suite (`tests/webhook-observer.test.ts`,
|
|
6
|
+
* T-AC-5) asserts against:
|
|
7
|
+
*
|
|
8
|
+
* - `secret` is REQUIRED at the host level. It is never read from
|
|
9
|
+
* disk, never defaulted to an empty string, never logged in any
|
|
10
|
+
* code path. The optional `WEBHOOK_HMAC_SECRET` env var is the
|
|
11
|
+
* fallback ONLY when the host does not pass `secret` (env is
|
|
12
|
+
* the host-injection's safety net, not its primary).
|
|
13
|
+
* - `urls[]` is the fan-out set. Every subscribed event is POSTed
|
|
14
|
+
* to every URL in `urls`. Empty `urls` is a no-op (the observer
|
|
15
|
+
* still subscribes, it just never POSTs).
|
|
16
|
+
* - `events` is the subscription filter. Defaults to
|
|
17
|
+
* `[JOB_COMPLETED, JOB_FAILED, STEP_FAILED]`. Subscribed events
|
|
18
|
+
* are the only ones the observer signs + POSTs. A
|
|
19
|
+
* `JOB_STARTED` event arrives at `onEvent`, is not in the
|
|
20
|
+
* filter, and is dropped silently (the listener is fire-and-
|
|
21
|
+
* forget by contract — see `BatchObserver.onEvent`).
|
|
22
|
+
* - `attempts` is the number of total POST attempts (1 initial +
|
|
23
|
+
* up to `attempts-1` retries). Defaults to 4 (matching the
|
|
24
|
+
* fixed 1s/5s/25s/125s backoff schedule). Lowering the value
|
|
25
|
+
* is supported for tests; raising it is intentionally NOT
|
|
26
|
+
* supported in v1 (the retry schedule is the contract, see
|
|
27
|
+
* `docs/RELEASE-0.2.0.md` §7.2).
|
|
28
|
+
* - `timeoutMs` is the per-attempt HTTP timeout. Defaults to
|
|
29
|
+
* 10 000 ms (10 seconds). A timeout is treated as a network
|
|
30
|
+
* error and retried through the full attempt budget.
|
|
31
|
+
* - `logger` is the Nest `Logger`-compatible interface used for
|
|
32
|
+
* the dead-letter `warn` line and the bootstrap notice. When
|
|
33
|
+
* omitted, the observer instantiates a `new Logger('WebhookBatchObserver')`.
|
|
34
|
+
*/
|
|
35
|
+
export interface WebhookBatchModuleOptions {
|
|
36
|
+
/**
|
|
37
|
+
* Host-injected HMAC-SHA256 secret used to sign outbound
|
|
38
|
+
* envelopes. REQUIRED when not relying on the `WEBHOOK_HMAC_SECRET`
|
|
39
|
+
* env fallback. Recommended length: 32+ bytes of randomness
|
|
40
|
+
* (a per-environment secret, never re-used across services).
|
|
41
|
+
*
|
|
42
|
+
* The secret is bound to the `WebhookBatchObserver` instance at
|
|
43
|
+
* `forRoot` time and is never exported, logged, serialized into
|
|
44
|
+
* a dead-letter body, or otherwise observable by the host.
|
|
45
|
+
*/
|
|
46
|
+
readonly secret?: string;
|
|
47
|
+
/**
|
|
48
|
+
* One or more absolute URLs the observer will fan out to on
|
|
49
|
+
* every subscribed event. Empty array is a no-op (the observer
|
|
50
|
+
* still subscribes to the event stream but never POSTs).
|
|
51
|
+
*/
|
|
52
|
+
readonly urls: readonly string[];
|
|
53
|
+
/**
|
|
54
|
+
* Subscription filter. Defaults to
|
|
55
|
+
* `[BATCH_EVENT.JOB_COMPLETED, BATCH_EVENT.JOB_FAILED,
|
|
56
|
+
* BATCH_EVENT.STEP_FAILED]`. The v1 contract is these three
|
|
57
|
+
* events only; a future v2 may widen the default to STEP_*
|
|
58
|
+
* events.
|
|
59
|
+
*/
|
|
60
|
+
readonly events?: readonly BatchEventType[];
|
|
61
|
+
/**
|
|
62
|
+
* Total number of POST attempts (initial + retries). Defaults
|
|
63
|
+
* to 4. Must be `>= 1`; `1` means "no retries" (single POST,
|
|
64
|
+
* then dead-letter on failure). Values `> 4` are clamped to 4
|
|
65
|
+
* — the v1 retry schedule is `[1s, 5s, 25s, 125s]` and has
|
|
66
|
+
* exactly 4 entries; further attempts would have no backoff to
|
|
67
|
+
* look up.
|
|
68
|
+
*/
|
|
69
|
+
readonly attempts?: number;
|
|
70
|
+
/**
|
|
71
|
+
* Per-attempt HTTP timeout in milliseconds. Defaults to
|
|
72
|
+
* 10 000 (10 seconds). A timeout is treated as a network
|
|
73
|
+
* error and retried through the full attempt budget.
|
|
74
|
+
*/
|
|
75
|
+
readonly timeoutMs?: number;
|
|
76
|
+
/**
|
|
77
|
+
* Logger override. The observer is built to use a NestJS
|
|
78
|
+
* `Logger`-compatible interface (the four `log` / `warn` /
|
|
79
|
+
* `error` / `debug` methods). When omitted, the observer
|
|
80
|
+
* instantiates a `new Logger('WebhookBatchObserver')` against
|
|
81
|
+
* the `console`-backed Nest logger.
|
|
82
|
+
*/
|
|
83
|
+
readonly logger?: WebhookLogger;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* NestJS-`Logger`-compatible surface used by `WebhookBatchObserver`.
|
|
87
|
+
*
|
|
88
|
+
* We type this as a structural subset of `@nestjs/common`'s
|
|
89
|
+
* `LoggerService` so the host can pass a custom logger without
|
|
90
|
+
* having to import the full Nest surface. The four methods are
|
|
91
|
+
* the only ones the observer calls:
|
|
92
|
+
*
|
|
93
|
+
* - `log` — bootstrap / info-level messages
|
|
94
|
+
* - `warn` — dead-letter payload (post final failure)
|
|
95
|
+
* - `error` — configuration / startup errors
|
|
96
|
+
* - `debug` — per-attempt diagnostic info (URL, status, latency)
|
|
97
|
+
*/
|
|
98
|
+
export interface WebhookLogger {
|
|
99
|
+
log(message: string, context?: string): void;
|
|
100
|
+
warn(message: string, context?: string): void;
|
|
101
|
+
error(message: string, context?: string): void;
|
|
102
|
+
debug(message: string, context?: string): void;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Fully-resolved options bag the observer consumes at runtime.
|
|
106
|
+
* `forRoot` is responsible for filling in every default and
|
|
107
|
+
* freezing the result before handing it to the provider.
|
|
108
|
+
*/
|
|
109
|
+
export interface ResolvedWebhookOptions {
|
|
110
|
+
readonly secret: string;
|
|
111
|
+
readonly urls: readonly string[];
|
|
112
|
+
readonly events: readonly BatchEventType[];
|
|
113
|
+
readonly attempts: number;
|
|
114
|
+
readonly timeoutMs: number;
|
|
115
|
+
readonly logger: WebhookLogger;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The v1 default subscription set. Documented in
|
|
119
|
+
* `docs/RELEASE-0.2.0.md` §7.1 and in the README.
|
|
120
|
+
*/
|
|
121
|
+
export declare const DEFAULT_WEBHOOK_EVENTS: readonly BatchEventType[];
|
|
122
|
+
/**
|
|
123
|
+
* The v1 fixed backoff schedule. Four entries (3 delays between
|
|
124
|
+
* 4 attempts). Documented in `docs/RELEASE-0.2.0.md` §7.2 and
|
|
125
|
+
* in the README. The schedule is the contract the test suite
|
|
126
|
+
* asserts against.
|
|
127
|
+
*/
|
|
128
|
+
export declare const DEFAULT_WEBHOOK_RETRY_DELAYS_MS: readonly number[];
|
|
129
|
+
/**
|
|
130
|
+
* Fast-mode override for the retry schedule. Activated when
|
|
131
|
+
* `process.env.WEBHOOK_TEST_FAST === '1'`. The override exists
|
|
132
|
+
* so the test suite can exercise the 4-attempt retry path
|
|
133
|
+
* without waiting 156 seconds (1+5+25+125). Test-only; never
|
|
134
|
+
* touched in production. Documented in the README.
|
|
135
|
+
*/
|
|
136
|
+
export declare const FAST_WEBHOOK_RETRY_DELAYS_MS: readonly number[];
|
|
137
|
+
/**
|
|
138
|
+
* The DI token under which the resolved options are stored.
|
|
139
|
+
* `Symbol.for` keeps the key process-scoped and stable across
|
|
140
|
+
* module versions, mirroring the pattern in
|
|
141
|
+
* `packages/bullmq/src/module-options.ts`.
|
|
142
|
+
*/
|
|
143
|
+
export declare const WEBHOOK_MODULE_OPTIONS: symbol;
|
|
144
|
+
/**
|
|
145
|
+
* Resolve a partial `WebhookBatchModuleOptions` into a fully-
|
|
146
|
+
* populated `ResolvedWebhookOptions`. Called by `forRoot` so the
|
|
147
|
+
* provider always sees a frozen, default-filled bag.
|
|
148
|
+
*
|
|
149
|
+
* Resolution rules:
|
|
150
|
+
* - `secret`: if absent, fall back to `process.env.WEBHOOK_HMAC_SECRET`.
|
|
151
|
+
* If still absent, throw — the host MUST provide a secret one
|
|
152
|
+
* way or another.
|
|
153
|
+
* - `urls`: required, no default. Empty array is allowed (no-op
|
|
154
|
+
* fan-out).
|
|
155
|
+
* - `events`: defaults to `DEFAULT_WEBHOOK_EVENTS`. A `[]` value
|
|
156
|
+
* is honoured (the observer subscribes to nothing).
|
|
157
|
+
* - `attempts`: defaults to 4. Clamped to `[1, 4]`.
|
|
158
|
+
* - `timeoutMs`: defaults to 10 000. Clamped to `>= 100` so the
|
|
159
|
+
* observer cannot be configured into "immediate timeout" mode.
|
|
160
|
+
* - `logger`: defaults to a `new Logger('WebhookBatchObserver')`.
|
|
161
|
+
*/
|
|
162
|
+
export declare function resolveWebhookOptions(raw: WebhookBatchModuleOptions): ResolvedWebhookOptions;
|
|
163
|
+
//# sourceMappingURL=module-options.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"module-options.d.ts","sourceRoot":"","sources":["../../src/module-options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,WAAW,yBAAyB;IACxC;;;;;;;;;OASG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAEzB;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IAEjC;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;IAE5C;;;;;;;OAOG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAE3B;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAE5B;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,aAAa,CAAC;CACjC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,aAAa;IAC5B,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7C,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9C,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/C,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAChD;AAED;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,SAAS,cAAc,EAAE,CAAC;IAC3C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;CAChC;AAED;;;GAGG;AACH,eAAO,MAAM,sBAAsB,EAAE,SAAS,cAAc,EASlD,CAAC;AAEX;;;;;GAKG;AACH,eAAO,MAAM,+BAA+B,EAAE,SAAS,MAAM,EAEnD,CAAC;AAEX;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B,EAAE,SAAS,MAAM,EAEhD,CAAC;AAEX;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,EAAE,MAEpC,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,yBAAyB,GAC7B,sBAAsB,CAoCxB"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
function _export(target, all) {
|
|
6
|
+
for(var name in all)Object.defineProperty(target, name, {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
get: Object.getOwnPropertyDescriptor(all, name).get
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
_export(exports, {
|
|
12
|
+
get DEFAULT_WEBHOOK_EVENTS () {
|
|
13
|
+
return DEFAULT_WEBHOOK_EVENTS;
|
|
14
|
+
},
|
|
15
|
+
get DEFAULT_WEBHOOK_RETRY_DELAYS_MS () {
|
|
16
|
+
return DEFAULT_WEBHOOK_RETRY_DELAYS_MS;
|
|
17
|
+
},
|
|
18
|
+
get FAST_WEBHOOK_RETRY_DELAYS_MS () {
|
|
19
|
+
return FAST_WEBHOOK_RETRY_DELAYS_MS;
|
|
20
|
+
},
|
|
21
|
+
get WEBHOOK_MODULE_OPTIONS () {
|
|
22
|
+
return WEBHOOK_MODULE_OPTIONS;
|
|
23
|
+
},
|
|
24
|
+
get resolveWebhookOptions () {
|
|
25
|
+
return resolveWebhookOptions;
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
const DEFAULT_WEBHOOK_EVENTS = [
|
|
29
|
+
// JOB_COMPLETED, JOB_FAILED, STEP_FAILED
|
|
30
|
+
// We do not import BATCH_EVENT here to avoid a circular
|
|
31
|
+
// dep (the observer re-uses this list at construction
|
|
32
|
+
// time). The constant is the v1 contract; a future v2
|
|
33
|
+
// may widen the default to STEP_*, CHUNK_*, ITEM_*.
|
|
34
|
+
'nest-batch.job.completed',
|
|
35
|
+
'nest-batch.job.failed',
|
|
36
|
+
'nest-batch.step.failed'
|
|
37
|
+
];
|
|
38
|
+
const DEFAULT_WEBHOOK_RETRY_DELAYS_MS = [
|
|
39
|
+
1_000,
|
|
40
|
+
5_000,
|
|
41
|
+
25_000,
|
|
42
|
+
125_000
|
|
43
|
+
];
|
|
44
|
+
const FAST_WEBHOOK_RETRY_DELAYS_MS = [
|
|
45
|
+
1,
|
|
46
|
+
5,
|
|
47
|
+
25,
|
|
48
|
+
125
|
|
49
|
+
];
|
|
50
|
+
const WEBHOOK_MODULE_OPTIONS = Symbol.for('@nest-batch/webhook/MODULE_OPTIONS');
|
|
51
|
+
function resolveWebhookOptions(raw) {
|
|
52
|
+
if (raw === null || typeof raw !== 'object') {
|
|
53
|
+
throw new Error('[WebhookBatchModule] options must be a non-null object');
|
|
54
|
+
}
|
|
55
|
+
const secret = pickSecret(raw.secret);
|
|
56
|
+
if (typeof secret !== 'string' || secret.length === 0) {
|
|
57
|
+
throw new Error('[WebhookBatchModule] secret is required: pass `secret` to ' + 'forRoot() or set the WEBHOOK_HMAC_SECRET env var');
|
|
58
|
+
}
|
|
59
|
+
const urls = Array.isArray(raw.urls) ? raw.urls.slice() : [];
|
|
60
|
+
for (const url of urls){
|
|
61
|
+
if (typeof url !== 'string' || url.length === 0) {
|
|
62
|
+
throw new Error('[WebhookBatchModule] every entry in `urls` must be a non-empty string');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const events = Array.isArray(raw.events) && raw.events.length > 0 ? raw.events.slice() : DEFAULT_WEBHOOK_EVENTS.slice();
|
|
66
|
+
const rawAttempts = typeof raw.attempts === 'number' ? raw.attempts : 4;
|
|
67
|
+
const attempts = Math.max(1, Math.min(4, Math.floor(rawAttempts)));
|
|
68
|
+
const rawTimeout = typeof raw.timeoutMs === 'number' ? raw.timeoutMs : 10_000;
|
|
69
|
+
const timeoutMs = Math.max(100, Math.floor(rawTimeout));
|
|
70
|
+
return Object.freeze({
|
|
71
|
+
secret,
|
|
72
|
+
urls,
|
|
73
|
+
events,
|
|
74
|
+
attempts,
|
|
75
|
+
timeoutMs,
|
|
76
|
+
logger: raw.logger ?? defaultLogger()
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Pick the secret: host-injected first, env-var fallback second.
|
|
81
|
+
* Returns `undefined` if neither is set so the caller can throw
|
|
82
|
+
* a precise error.
|
|
83
|
+
*/ function pickSecret(hostInjected) {
|
|
84
|
+
if (typeof hostInjected === 'string' && hostInjected.length > 0) {
|
|
85
|
+
return hostInjected;
|
|
86
|
+
}
|
|
87
|
+
const fromEnv = process.env['WEBHOOK_HMAC_SECRET'];
|
|
88
|
+
if (typeof fromEnv === 'string' && fromEnv.length > 0) {
|
|
89
|
+
return fromEnv;
|
|
90
|
+
}
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The default `WebhookLogger` — a thin adapter around
|
|
95
|
+
* `console`. The observer is built to be test-friendly; tests
|
|
96
|
+
* pass a captured-`console.warn` spy via the `logger` option.
|
|
97
|
+
*/ function defaultLogger() {
|
|
98
|
+
// We deliberately do NOT import @nestjs/common's `Logger` here
|
|
99
|
+
// — the `WebhookLogger` is a structural interface, and the
|
|
100
|
+
// adapter lets the package stay test-runner-agnostic. Tests
|
|
101
|
+
// pass a console-backed spy; hosts pass a NestJS `Logger`
|
|
102
|
+
// instance (the structural shape matches the official
|
|
103
|
+
// `LoggerService`).
|
|
104
|
+
return {
|
|
105
|
+
log: (message)=>{
|
|
106
|
+
// eslint-disable-next-line no-console
|
|
107
|
+
console.log(`[WebhookBatchObserver] ${message}`);
|
|
108
|
+
},
|
|
109
|
+
warn: (message)=>{
|
|
110
|
+
// eslint-disable-next-line no-console
|
|
111
|
+
console.warn(`[WebhookBatchObserver] ${message}`);
|
|
112
|
+
},
|
|
113
|
+
error: (message)=>{
|
|
114
|
+
// eslint-disable-next-line no-console
|
|
115
|
+
console.error(`[WebhookBatchObserver] ${message}`);
|
|
116
|
+
},
|
|
117
|
+
debug: (message)=>{
|
|
118
|
+
// eslint-disable-next-line no-console
|
|
119
|
+
console.debug(`[WebhookBatchObserver] ${message}`);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
//# sourceMappingURL=module-options.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/module-options.ts"],"sourcesContent":["import type { BatchEventType } from '@nest-batch/core';\n\n/**\n * Public options bag for `WebhookBatchModule.forRoot()`.\n *\n * The contract the test suite (`tests/webhook-observer.test.ts`,\n * T-AC-5) asserts against:\n *\n * - `secret` is REQUIRED at the host level. It is never read from\n * disk, never defaulted to an empty string, never logged in any\n * code path. The optional `WEBHOOK_HMAC_SECRET` env var is the\n * fallback ONLY when the host does not pass `secret` (env is\n * the host-injection's safety net, not its primary).\n * - `urls[]` is the fan-out set. Every subscribed event is POSTed\n * to every URL in `urls`. Empty `urls` is a no-op (the observer\n * still subscribes, it just never POSTs).\n * - `events` is the subscription filter. Defaults to\n * `[JOB_COMPLETED, JOB_FAILED, STEP_FAILED]`. Subscribed events\n * are the only ones the observer signs + POSTs. A\n * `JOB_STARTED` event arrives at `onEvent`, is not in the\n * filter, and is dropped silently (the listener is fire-and-\n * forget by contract — see `BatchObserver.onEvent`).\n * - `attempts` is the number of total POST attempts (1 initial +\n * up to `attempts-1` retries). Defaults to 4 (matching the\n * fixed 1s/5s/25s/125s backoff schedule). Lowering the value\n * is supported for tests; raising it is intentionally NOT\n * supported in v1 (the retry schedule is the contract, see\n * `docs/RELEASE-0.2.0.md` §7.2).\n * - `timeoutMs` is the per-attempt HTTP timeout. Defaults to\n * 10 000 ms (10 seconds). A timeout is treated as a network\n * error and retried through the full attempt budget.\n * - `logger` is the Nest `Logger`-compatible interface used for\n * the dead-letter `warn` line and the bootstrap notice. When\n * omitted, the observer instantiates a `new Logger('WebhookBatchObserver')`.\n */\nexport interface WebhookBatchModuleOptions {\n /**\n * Host-injected HMAC-SHA256 secret used to sign outbound\n * envelopes. REQUIRED when not relying on the `WEBHOOK_HMAC_SECRET`\n * env fallback. Recommended length: 32+ bytes of randomness\n * (a per-environment secret, never re-used across services).\n *\n * The secret is bound to the `WebhookBatchObserver` instance at\n * `forRoot` time and is never exported, logged, serialized into\n * a dead-letter body, or otherwise observable by the host.\n */\n readonly secret?: string;\n\n /**\n * One or more absolute URLs the observer will fan out to on\n * every subscribed event. Empty array is a no-op (the observer\n * still subscribes to the event stream but never POSTs).\n */\n readonly urls: readonly string[];\n\n /**\n * Subscription filter. Defaults to\n * `[BATCH_EVENT.JOB_COMPLETED, BATCH_EVENT.JOB_FAILED,\n * BATCH_EVENT.STEP_FAILED]`. The v1 contract is these three\n * events only; a future v2 may widen the default to STEP_*\n * events.\n */\n readonly events?: readonly BatchEventType[];\n\n /**\n * Total number of POST attempts (initial + retries). Defaults\n * to 4. Must be `>= 1`; `1` means \"no retries\" (single POST,\n * then dead-letter on failure). Values `> 4` are clamped to 4\n * — the v1 retry schedule is `[1s, 5s, 25s, 125s]` and has\n * exactly 4 entries; further attempts would have no backoff to\n * look up.\n */\n readonly attempts?: number;\n\n /**\n * Per-attempt HTTP timeout in milliseconds. Defaults to\n * 10 000 (10 seconds). A timeout is treated as a network\n * error and retried through the full attempt budget.\n */\n readonly timeoutMs?: number;\n\n /**\n * Logger override. The observer is built to use a NestJS\n * `Logger`-compatible interface (the four `log` / `warn` /\n * `error` / `debug` methods). When omitted, the observer\n * instantiates a `new Logger('WebhookBatchObserver')` against\n * the `console`-backed Nest logger.\n */\n readonly logger?: WebhookLogger;\n}\n\n/**\n * NestJS-`Logger`-compatible surface used by `WebhookBatchObserver`.\n *\n * We type this as a structural subset of `@nestjs/common`'s\n * `LoggerService` so the host can pass a custom logger without\n * having to import the full Nest surface. The four methods are\n * the only ones the observer calls:\n *\n * - `log` — bootstrap / info-level messages\n * - `warn` — dead-letter payload (post final failure)\n * - `error` — configuration / startup errors\n * - `debug` — per-attempt diagnostic info (URL, status, latency)\n */\nexport interface WebhookLogger {\n log(message: string, context?: string): void;\n warn(message: string, context?: string): void;\n error(message: string, context?: string): void;\n debug(message: string, context?: string): void;\n}\n\n/**\n * Fully-resolved options bag the observer consumes at runtime.\n * `forRoot` is responsible for filling in every default and\n * freezing the result before handing it to the provider.\n */\nexport interface ResolvedWebhookOptions {\n readonly secret: string;\n readonly urls: readonly string[];\n readonly events: readonly BatchEventType[];\n readonly attempts: number;\n readonly timeoutMs: number;\n readonly logger: WebhookLogger;\n}\n\n/**\n * The v1 default subscription set. Documented in\n * `docs/RELEASE-0.2.0.md` §7.1 and in the README.\n */\nexport const DEFAULT_WEBHOOK_EVENTS: readonly BatchEventType[] = [\n // JOB_COMPLETED, JOB_FAILED, STEP_FAILED\n // We do not import BATCH_EVENT here to avoid a circular\n // dep (the observer re-uses this list at construction\n // time). The constant is the v1 contract; a future v2\n // may widen the default to STEP_*, CHUNK_*, ITEM_*.\n 'nest-batch.job.completed',\n 'nest-batch.job.failed',\n 'nest-batch.step.failed',\n] as const;\n\n/**\n * The v1 fixed backoff schedule. Four entries (3 delays between\n * 4 attempts). Documented in `docs/RELEASE-0.2.0.md` §7.2 and\n * in the README. The schedule is the contract the test suite\n * asserts against.\n */\nexport const DEFAULT_WEBHOOK_RETRY_DELAYS_MS: readonly number[] = [\n 1_000, 5_000, 25_000, 125_000,\n] as const;\n\n/**\n * Fast-mode override for the retry schedule. Activated when\n * `process.env.WEBHOOK_TEST_FAST === '1'`. The override exists\n * so the test suite can exercise the 4-attempt retry path\n * without waiting 156 seconds (1+5+25+125). Test-only; never\n * touched in production. Documented in the README.\n */\nexport const FAST_WEBHOOK_RETRY_DELAYS_MS: readonly number[] = [\n 1, 5, 25, 125,\n] as const;\n\n/**\n * The DI token under which the resolved options are stored.\n * `Symbol.for` keeps the key process-scoped and stable across\n * module versions, mirroring the pattern in\n * `packages/bullmq/src/module-options.ts`.\n */\nexport const WEBHOOK_MODULE_OPTIONS: symbol = Symbol.for(\n '@nest-batch/webhook/MODULE_OPTIONS',\n);\n\n/**\n * Resolve a partial `WebhookBatchModuleOptions` into a fully-\n * populated `ResolvedWebhookOptions`. Called by `forRoot` so the\n * provider always sees a frozen, default-filled bag.\n *\n * Resolution rules:\n * - `secret`: if absent, fall back to `process.env.WEBHOOK_HMAC_SECRET`.\n * If still absent, throw — the host MUST provide a secret one\n * way or another.\n * - `urls`: required, no default. Empty array is allowed (no-op\n * fan-out).\n * - `events`: defaults to `DEFAULT_WEBHOOK_EVENTS`. A `[]` value\n * is honoured (the observer subscribes to nothing).\n * - `attempts`: defaults to 4. Clamped to `[1, 4]`.\n * - `timeoutMs`: defaults to 10 000. Clamped to `>= 100` so the\n * observer cannot be configured into \"immediate timeout\" mode.\n * - `logger`: defaults to a `new Logger('WebhookBatchObserver')`.\n */\nexport function resolveWebhookOptions(\n raw: WebhookBatchModuleOptions,\n): ResolvedWebhookOptions {\n if (raw === null || typeof raw !== 'object') {\n throw new Error(\n '[WebhookBatchModule] options must be a non-null object',\n );\n }\n const secret = pickSecret(raw.secret);\n if (typeof secret !== 'string' || secret.length === 0) {\n throw new Error(\n '[WebhookBatchModule] secret is required: pass `secret` to ' +\n 'forRoot() or set the WEBHOOK_HMAC_SECRET env var',\n );\n }\n const urls = Array.isArray(raw.urls) ? raw.urls.slice() : [];\n for (const url of urls) {\n if (typeof url !== 'string' || url.length === 0) {\n throw new Error(\n '[WebhookBatchModule] every entry in `urls` must be a non-empty string',\n );\n }\n }\n const events = Array.isArray(raw.events) && raw.events.length > 0\n ? raw.events.slice()\n : DEFAULT_WEBHOOK_EVENTS.slice();\n const rawAttempts = typeof raw.attempts === 'number' ? raw.attempts : 4;\n const attempts = Math.max(1, Math.min(4, Math.floor(rawAttempts)));\n const rawTimeout = typeof raw.timeoutMs === 'number' ? raw.timeoutMs : 10_000;\n const timeoutMs = Math.max(100, Math.floor(rawTimeout));\n return Object.freeze({\n secret,\n urls,\n events,\n attempts,\n timeoutMs,\n logger: raw.logger ?? defaultLogger(),\n });\n}\n\n/**\n * Pick the secret: host-injected first, env-var fallback second.\n * Returns `undefined` if neither is set so the caller can throw\n * a precise error.\n */\nfunction pickSecret(hostInjected: string | undefined): string | undefined {\n if (typeof hostInjected === 'string' && hostInjected.length > 0) {\n return hostInjected;\n }\n const fromEnv = process.env['WEBHOOK_HMAC_SECRET'];\n if (typeof fromEnv === 'string' && fromEnv.length > 0) {\n return fromEnv;\n }\n return undefined;\n}\n\n/**\n * The default `WebhookLogger` — a thin adapter around\n * `console`. The observer is built to be test-friendly; tests\n * pass a captured-`console.warn` spy via the `logger` option.\n */\nfunction defaultLogger(): WebhookLogger {\n // We deliberately do NOT import @nestjs/common's `Logger` here\n // — the `WebhookLogger` is a structural interface, and the\n // adapter lets the package stay test-runner-agnostic. Tests\n // pass a console-backed spy; hosts pass a NestJS `Logger`\n // instance (the structural shape matches the official\n // `LoggerService`).\n return {\n log: (message: string) => {\n // eslint-disable-next-line no-console\n console.log(`[WebhookBatchObserver] ${message}`);\n },\n warn: (message: string) => {\n // eslint-disable-next-line no-console\n console.warn(`[WebhookBatchObserver] ${message}`);\n },\n error: (message: string) => {\n // eslint-disable-next-line no-console\n console.error(`[WebhookBatchObserver] ${message}`);\n },\n debug: (message: string) => {\n // eslint-disable-next-line no-console\n console.debug(`[WebhookBatchObserver] ${message}`);\n },\n };\n}\n"],"names":["DEFAULT_WEBHOOK_EVENTS","DEFAULT_WEBHOOK_RETRY_DELAYS_MS","FAST_WEBHOOK_RETRY_DELAYS_MS","WEBHOOK_MODULE_OPTIONS","resolveWebhookOptions","Symbol","for","raw","Error","secret","pickSecret","length","urls","Array","isArray","slice","url","events","rawAttempts","attempts","Math","max","min","floor","rawTimeout","timeoutMs","Object","freeze","logger","defaultLogger","hostInjected","fromEnv","process","env","undefined","log","message","console","warn","error","debug"],"mappings":";;;;;;;;;;;QAiIaA;eAAAA;;QAiBAC;eAAAA;;QAWAC;eAAAA;;QAUAC;eAAAA;;QAsBGC;eAAAA;;;AA5DT,MAAMJ,yBAAoD;IAC/D,yCAAyC;IACzC,wDAAwD;IACxD,sDAAsD;IACtD,sDAAsD;IACtD,oDAAoD;IACpD;IACA;IACA;CACD;AAQM,MAAMC,kCAAqD;IAChE;IAAO;IAAO;IAAQ;CACvB;AASM,MAAMC,+BAAkD;IAC7D;IAAG;IAAG;IAAI;CACX;AAQM,MAAMC,yBAAiCE,OAAOC,GAAG,CACtD;AAqBK,SAASF,sBACdG,GAA8B;IAE9B,IAAIA,QAAQ,QAAQ,OAAOA,QAAQ,UAAU;QAC3C,MAAM,IAAIC,MACR;IAEJ;IACA,MAAMC,SAASC,WAAWH,IAAIE,MAAM;IACpC,IAAI,OAAOA,WAAW,YAAYA,OAAOE,MAAM,KAAK,GAAG;QACrD,MAAM,IAAIH,MACR,+DACE;IAEN;IACA,MAAMI,OAAOC,MAAMC,OAAO,CAACP,IAAIK,IAAI,IAAIL,IAAIK,IAAI,CAACG,KAAK,KAAK,EAAE;IAC5D,KAAK,MAAMC,OAAOJ,KAAM;QACtB,IAAI,OAAOI,QAAQ,YAAYA,IAAIL,MAAM,KAAK,GAAG;YAC/C,MAAM,IAAIH,MACR;QAEJ;IACF;IACA,MAAMS,SAASJ,MAAMC,OAAO,CAACP,IAAIU,MAAM,KAAKV,IAAIU,MAAM,CAACN,MAAM,GAAG,IAC5DJ,IAAIU,MAAM,CAACF,KAAK,KAChBf,uBAAuBe,KAAK;IAChC,MAAMG,cAAc,OAAOX,IAAIY,QAAQ,KAAK,WAAWZ,IAAIY,QAAQ,GAAG;IACtE,MAAMA,WAAWC,KAAKC,GAAG,CAAC,GAAGD,KAAKE,GAAG,CAAC,GAAGF,KAAKG,KAAK,CAACL;IACpD,MAAMM,aAAa,OAAOjB,IAAIkB,SAAS,KAAK,WAAWlB,IAAIkB,SAAS,GAAG;IACvE,MAAMA,YAAYL,KAAKC,GAAG,CAAC,KAAKD,KAAKG,KAAK,CAACC;IAC3C,OAAOE,OAAOC,MAAM,CAAC;QACnBlB;QACAG;QACAK;QACAE;QACAM;QACAG,QAAQrB,IAAIqB,MAAM,IAAIC;IACxB;AACF;AAEA;;;;CAIC,GACD,SAASnB,WAAWoB,YAAgC;IAClD,IAAI,OAAOA,iBAAiB,YAAYA,aAAanB,MAAM,GAAG,GAAG;QAC/D,OAAOmB;IACT;IACA,MAAMC,UAAUC,QAAQC,GAAG,CAAC,sBAAsB;IAClD,IAAI,OAAOF,YAAY,YAAYA,QAAQpB,MAAM,GAAG,GAAG;QACrD,OAAOoB;IACT;IACA,OAAOG;AACT;AAEA;;;;CAIC,GACD,SAASL;IACP,+DAA+D;IAC/D,2DAA2D;IAC3D,4DAA4D;IAC5D,0DAA0D;IAC1D,sDAAsD;IACtD,oBAAoB;IACpB,OAAO;QACLM,KAAK,CAACC;YACJ,sCAAsC;YACtCC,QAAQF,GAAG,CAAC,CAAC,uBAAuB,EAAEC,SAAS;QACjD;QACAE,MAAM,CAACF;YACL,sCAAsC;YACtCC,QAAQC,IAAI,CAAC,CAAC,uBAAuB,EAAEF,SAAS;QAClD;QACAG,OAAO,CAACH;YACN,sCAAsC;YACtCC,QAAQE,KAAK,CAAC,CAAC,uBAAuB,EAAEH,SAAS;QACnD;QACAI,OAAO,CAACJ;YACN,sCAAsC;YACtCC,QAAQG,KAAK,CAAC,CAAC,uBAAuB,EAAEJ,SAAS;QACnD;IACF;AACF"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type DynamicModule } from '@nestjs/common';
|
|
2
|
+
import { BATCH_EVENT, type BatchEventType } from '@nest-batch/core';
|
|
3
|
+
import { type ResolvedWebhookOptions, type WebhookBatchModuleOptions, type WebhookLogger } from './module-options';
|
|
4
|
+
import { WebhookBatchObserver } from './webhook-batch.observer';
|
|
5
|
+
/**
|
|
6
|
+
* `WebhookBatchModule` — the NestJS dynamic module that wires
|
|
7
|
+
* the `WebhookBatchObserver` into the host's DI container and
|
|
8
|
+
* binds it to the `BatchObserver` token used by the executor
|
|
9
|
+
* (and by `@nest-batch/bullmq` / `@nest-batch/kafka`'s runtime
|
|
10
|
+
* bridge).
|
|
11
|
+
*
|
|
12
|
+
* The host wires it alongside `NestBatchModule.forRoot({...})`:
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* @Module({
|
|
16
|
+
* imports: [
|
|
17
|
+
* NestBatchModule.forRoot({
|
|
18
|
+
* adapters: { persistence: MikroOrmAdapter.forRoot(), transport: BullmqAdapter.forRoot() },
|
|
19
|
+
* }),
|
|
20
|
+
* WebhookBatchModule.forRoot({
|
|
21
|
+
* secret: process.env.WEBHOOK_HMAC_SECRET,
|
|
22
|
+
* urls: ['https://hooks.example.com/nest-batch'],
|
|
23
|
+
* }),
|
|
24
|
+
* ],
|
|
25
|
+
* })
|
|
26
|
+
* export class AppModule {}
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* The observer is auto-registered against the `BatchObserver`
|
|
30
|
+
* token via `useExisting`, so the executor's optional-injection
|
|
31
|
+
* path picks it up without any extra wiring on the host's side.
|
|
32
|
+
*/
|
|
33
|
+
export declare class WebhookBatchModule {
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* `forRoot` — synchronous configuration. Resolves the options
|
|
37
|
+
* up-front (filling in defaults, falling back to the
|
|
38
|
+
* `WEBHOOK_HMAC_SECRET` env var when `secret` is omitted,
|
|
39
|
+
* freezing the result) and emits a `DynamicModule` that:
|
|
40
|
+
*
|
|
41
|
+
* - registers `WebhookBatchObserver` as a provider,
|
|
42
|
+
* - registers a `useExisting` alias so anything injecting
|
|
43
|
+
* `BatchObserver` (or `WebhookBatchObserver` by class)
|
|
44
|
+
* resolves to the same instance,
|
|
45
|
+
* - registers the resolved options under
|
|
46
|
+
* `WEBHOOK_MODULE_OPTIONS` (the observer's `@Inject`
|
|
47
|
+
* key),
|
|
48
|
+
* - marks the module `global: true` so the observer is
|
|
49
|
+
* visible across the host's sub-modules.
|
|
50
|
+
*
|
|
51
|
+
* The `urls: []` case is a no-op (the observer subscribes
|
|
52
|
+
* to the event stream but never POSTs); it does not throw.
|
|
53
|
+
* The `secret` case throws at `forRoot` time with a clear
|
|
54
|
+
* message (the host sees the error at boot, not at the first
|
|
55
|
+
* event).
|
|
56
|
+
*/
|
|
57
|
+
export declare function forRoot(options: WebhookBatchModuleOptions): DynamicModule;
|
|
58
|
+
export { BATCH_EVENT, WebhookBatchObserver, type BatchEventType, type ResolvedWebhookOptions, type WebhookBatchModuleOptions, type WebhookLogger, };
|
|
59
|
+
//# sourceMappingURL=webhook-batch.module.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook-batch.module.d.ts","sourceRoot":"","sources":["../../src/webhook-batch.module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,aAAa,EAAiB,MAAM,gBAAgB,CAAC;AAC3E,OAAO,EAAE,WAAW,EAAE,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEpE,OAAO,EAGL,KAAK,sBAAsB,EAC3B,KAAK,yBAAyB,EAC9B,KAAK,aAAa,EACnB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAEhE;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,qBACa,kBAAkB;CAAG;AAElC;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,OAAO,CAAC,OAAO,EAAE,yBAAyB,GAAG,aAAa,CAQzE;AAoDD,OAAO,EACL,WAAW,EACX,oBAAoB,EACpB,KAAK,cAAc,EACnB,KAAK,sBAAsB,EAC3B,KAAK,yBAAyB,EAC9B,KAAK,aAAa,GACnB,CAAC"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
function _export(target, all) {
|
|
6
|
+
for(var name in all)Object.defineProperty(target, name, {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
get: Object.getOwnPropertyDescriptor(all, name).get
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
_export(exports, {
|
|
12
|
+
get BATCH_EVENT () {
|
|
13
|
+
return _core.BATCH_EVENT;
|
|
14
|
+
},
|
|
15
|
+
get WebhookBatchModule () {
|
|
16
|
+
return WebhookBatchModule;
|
|
17
|
+
},
|
|
18
|
+
get WebhookBatchObserver () {
|
|
19
|
+
return _webhookbatchobserver.WebhookBatchObserver;
|
|
20
|
+
},
|
|
21
|
+
get forRoot () {
|
|
22
|
+
return forRoot;
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
const _common = require("@nestjs/common");
|
|
26
|
+
const _core = require("@nest-batch/core");
|
|
27
|
+
const _moduleoptions = require("./module-options");
|
|
28
|
+
const _webhookbatchobserver = require("./webhook-batch.observer");
|
|
29
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
30
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
31
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
32
|
+
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
33
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
34
|
+
}
|
|
35
|
+
let WebhookBatchModule = class WebhookBatchModule {
|
|
36
|
+
};
|
|
37
|
+
WebhookBatchModule = _ts_decorate([
|
|
38
|
+
(0, _common.Module)({})
|
|
39
|
+
], WebhookBatchModule);
|
|
40
|
+
function forRoot(options) {
|
|
41
|
+
const resolved = (0, _moduleoptions.resolveWebhookOptions)(options);
|
|
42
|
+
return {
|
|
43
|
+
module: WebhookBatchModule,
|
|
44
|
+
global: true,
|
|
45
|
+
providers: buildProviders(resolved),
|
|
46
|
+
exports: [
|
|
47
|
+
_webhookbatchobserver.WebhookBatchObserver
|
|
48
|
+
]
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Build the static provider list shared by `forRoot()`.
|
|
53
|
+
*
|
|
54
|
+
* The list is three entries:
|
|
55
|
+
* - `WebhookBatchObserver` — the concrete class.
|
|
56
|
+
* - `BATCH_OBSERVER_PROVIDER` — a `useExisting` alias so the
|
|
57
|
+
* executor's `@Optional() @Inject(BatchObserver) observer`
|
|
58
|
+
* resolves to the same instance.
|
|
59
|
+
* - `WEBHOOK_MODULE_OPTIONS` — the resolved + frozen
|
|
60
|
+
* options bag, injected into the observer's constructor.
|
|
61
|
+
*
|
|
62
|
+
* Centralising the list keeps the public factory surface
|
|
63
|
+
* (`forRoot`) a one-liner; any future addition (e.g. a
|
|
64
|
+
* per-package health check) only needs to land here.
|
|
65
|
+
*/ function buildProviders(resolved) {
|
|
66
|
+
return [
|
|
67
|
+
_webhookbatchobserver.WebhookBatchObserver,
|
|
68
|
+
{
|
|
69
|
+
provide: BATCH_OBSERVER_TOKEN,
|
|
70
|
+
useExisting: _webhookbatchobserver.WebhookBatchObserver
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
provide: _moduleoptions.WEBHOOK_MODULE_OPTIONS,
|
|
74
|
+
useValue: resolved
|
|
75
|
+
}
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The DI token the executor / runtime services use to inject
|
|
80
|
+
* a `BatchObserver`. We re-export the `BatchObserver` class
|
|
81
|
+
* itself as the token (mirroring the pattern in
|
|
82
|
+
* `@nest-batch/bullmq` and `@nest-batch/kafka`, where the
|
|
83
|
+
* `BatchObserver` interface is the type and the class-as-
|
|
84
|
+
* token resolves to the singleton).
|
|
85
|
+
*
|
|
86
|
+
* Using the `BatchObserver` class (an interface in
|
|
87
|
+
* `@nest-batch/core`) as the token is a NestJS pattern:
|
|
88
|
+
* Nest uses the class reference as the default DI key. We
|
|
89
|
+
* redeclare it as `BATCH_OBSERVER_TOKEN` so the `useExisting`
|
|
90
|
+
* provider above has a stable, imported symbol to bind
|
|
91
|
+
* against.
|
|
92
|
+
*/ const BATCH_OBSERVER_TOKEN = Symbol.for('@nest-batch/webhook/BATCH_OBSERVER');
|
|
93
|
+
|
|
94
|
+
//# sourceMappingURL=webhook-batch.module.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/webhook-batch.module.ts"],"sourcesContent":["import { Module, type DynamicModule, type Provider } from '@nestjs/common';\nimport { BATCH_EVENT, type BatchEventType } from '@nest-batch/core';\n\nimport {\n resolveWebhookOptions,\n WEBHOOK_MODULE_OPTIONS,\n type ResolvedWebhookOptions,\n type WebhookBatchModuleOptions,\n type WebhookLogger,\n} from './module-options';\nimport { WebhookBatchObserver } from './webhook-batch.observer';\n\n/**\n * `WebhookBatchModule` — the NestJS dynamic module that wires\n * the `WebhookBatchObserver` into the host's DI container and\n * binds it to the `BatchObserver` token used by the executor\n * (and by `@nest-batch/bullmq` / `@nest-batch/kafka`'s runtime\n * bridge).\n *\n * The host wires it alongside `NestBatchModule.forRoot({...})`:\n *\n * ```ts\n * @Module({\n * imports: [\n * NestBatchModule.forRoot({\n * adapters: { persistence: MikroOrmAdapter.forRoot(), transport: BullmqAdapter.forRoot() },\n * }),\n * WebhookBatchModule.forRoot({\n * secret: process.env.WEBHOOK_HMAC_SECRET,\n * urls: ['https://hooks.example.com/nest-batch'],\n * }),\n * ],\n * })\n * export class AppModule {}\n * ```\n *\n * The observer is auto-registered against the `BatchObserver`\n * token via `useExisting`, so the executor's optional-injection\n * path picks it up without any extra wiring on the host's side.\n */\n@Module({})\nexport class WebhookBatchModule {}\n\n/**\n * `forRoot` — synchronous configuration. Resolves the options\n * up-front (filling in defaults, falling back to the\n * `WEBHOOK_HMAC_SECRET` env var when `secret` is omitted,\n * freezing the result) and emits a `DynamicModule` that:\n *\n * - registers `WebhookBatchObserver` as a provider,\n * - registers a `useExisting` alias so anything injecting\n * `BatchObserver` (or `WebhookBatchObserver` by class)\n * resolves to the same instance,\n * - registers the resolved options under\n * `WEBHOOK_MODULE_OPTIONS` (the observer's `@Inject`\n * key),\n * - marks the module `global: true` so the observer is\n * visible across the host's sub-modules.\n *\n * The `urls: []` case is a no-op (the observer subscribes\n * to the event stream but never POSTs); it does not throw.\n * The `secret` case throws at `forRoot` time with a clear\n * message (the host sees the error at boot, not at the first\n * event).\n */\nexport function forRoot(options: WebhookBatchModuleOptions): DynamicModule {\n const resolved = resolveWebhookOptions(options);\n return {\n module: WebhookBatchModule,\n global: true,\n providers: buildProviders(resolved),\n exports: [WebhookBatchObserver],\n };\n}\n\n/**\n * Build the static provider list shared by `forRoot()`.\n *\n * The list is three entries:\n * - `WebhookBatchObserver` — the concrete class.\n * - `BATCH_OBSERVER_PROVIDER` — a `useExisting` alias so the\n * executor's `@Optional() @Inject(BatchObserver) observer`\n * resolves to the same instance.\n * - `WEBHOOK_MODULE_OPTIONS` — the resolved + frozen\n * options bag, injected into the observer's constructor.\n *\n * Centralising the list keeps the public factory surface\n * (`forRoot`) a one-liner; any future addition (e.g. a\n * per-package health check) only needs to land here.\n */\nfunction buildProviders(resolved: ResolvedWebhookOptions): Provider[] {\n return [\n WebhookBatchObserver,\n {\n provide: BATCH_OBSERVER_TOKEN,\n useExisting: WebhookBatchObserver,\n },\n {\n provide: WEBHOOK_MODULE_OPTIONS,\n useValue: resolved,\n },\n ];\n}\n\n/**\n * The DI token the executor / runtime services use to inject\n * a `BatchObserver`. We re-export the `BatchObserver` class\n * itself as the token (mirroring the pattern in\n * `@nest-batch/bullmq` and `@nest-batch/kafka`, where the\n * `BatchObserver` interface is the type and the class-as-\n * token resolves to the singleton).\n *\n * Using the `BatchObserver` class (an interface in\n * `@nest-batch/core`) as the token is a NestJS pattern:\n * Nest uses the class reference as the default DI key. We\n * redeclare it as `BATCH_OBSERVER_TOKEN` so the `useExisting`\n * provider above has a stable, imported symbol to bind\n * against.\n */\nconst BATCH_OBSERVER_TOKEN: symbol = Symbol.for(\n '@nest-batch/webhook/BATCH_OBSERVER',\n);\n\n// Re-export the public surface of this module so the\n// package barrel can re-export it in turn.\nexport {\n BATCH_EVENT,\n WebhookBatchObserver,\n type BatchEventType,\n type ResolvedWebhookOptions,\n type WebhookBatchModuleOptions,\n type WebhookLogger,\n};\n"],"names":["BATCH_EVENT","WebhookBatchModule","WebhookBatchObserver","forRoot","options","resolved","resolveWebhookOptions","module","global","providers","buildProviders","exports","provide","BATCH_OBSERVER_TOKEN","useExisting","WEBHOOK_MODULE_OPTIONS","useValue","Symbol","for"],"mappings":";;;;;;;;;;;QA8HEA;eAAAA,iBAAW;;QArFAC;eAAAA;;QAsFXC;eAAAA,0CAAoB;;QA9DNC;eAAAA;;;wBAjE0C;sBACT;+BAQ1C;sCAC8B;;;;;;;AA+B9B,IAAA,AAAMF,qBAAN,MAAMA;AAAoB;;;;AAwB1B,SAASE,QAAQC,OAAkC;IACxD,MAAMC,WAAWC,IAAAA,oCAAqB,EAACF;IACvC,OAAO;QACLG,QAAQN;QACRO,QAAQ;QACRC,WAAWC,eAAeL;QAC1BM,SAAS;YAACT,0CAAoB;SAAC;IACjC;AACF;AAEA;;;;;;;;;;;;;;CAcC,GACD,SAASQ,eAAeL,QAAgC;IACtD,OAAO;QACLH,0CAAoB;QACpB;YACEU,SAASC;YACTC,aAAaZ,0CAAoB;QACnC;QACA;YACEU,SAASG,qCAAsB;YAC/BC,UAAUX;QACZ;KACD;AACH;AAEA;;;;;;;;;;;;;;CAcC,GACD,MAAMQ,uBAA+BI,OAAOC,GAAG,CAC7C"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { BATCH_EVENT, type BatchEvent, type BatchEventType, type BatchObserver } from '@nest-batch/core';
|
|
2
|
+
import { type ResolvedWebhookOptions } from './module-options';
|
|
3
|
+
/**
|
|
4
|
+
* `WebhookBatchObserver` — the v1 webhook delivery observer.
|
|
5
|
+
*
|
|
6
|
+
* Implements `BatchObserver` from `@nest-batch/core`. On every
|
|
7
|
+
* subscribed `BATCH_EVENT.*` (default:
|
|
8
|
+
* `[JOB_COMPLETED, JOB_FAILED, STEP_FAILED]`) the observer:
|
|
9
|
+
*
|
|
10
|
+
* 1. Serializes a normalized JSON envelope
|
|
11
|
+
* `{ version: 1, type, timestamp, jobId, execution }`.
|
|
12
|
+
* 2. Computes the v1 HMAC-SHA256 signature over
|
|
13
|
+
* `<unix>.<raw-body>` (Stripe-style).
|
|
14
|
+
* 3. POSTs the envelope + `X-Nest-Batch-Signature` header to
|
|
15
|
+
* every URL in `urls`.
|
|
16
|
+
* 4. Retries on 5xx and network errors through the fixed
|
|
17
|
+
* 4-attempt budget at `[1s, 5s, 25s, 125s]`. HTTP 4xx
|
|
18
|
+
* responses are NOT retried (client error, won't change).
|
|
19
|
+
* 5. On final failure, emits a `logger.warn` dead-letter line
|
|
20
|
+
* including the URL, attempt count, last status / error,
|
|
21
|
+
* and a SHA-256 fingerprint of the secret (NEVER the
|
|
22
|
+
* secret itself).
|
|
23
|
+
*
|
|
24
|
+
* The observer is the v1 contract documented in
|
|
25
|
+
* `docs/RELEASE-0.2.0.md` §7 and pinned by T-AC-5
|
|
26
|
+
* (`packages/webhook/tests/webhook-observer.test.ts`).
|
|
27
|
+
*/
|
|
28
|
+
export declare class WebhookBatchObserver implements BatchObserver {
|
|
29
|
+
private readonly logger;
|
|
30
|
+
/** Resolved + frozen options. The secret lives here and nowhere else. */
|
|
31
|
+
private readonly options;
|
|
32
|
+
/**
|
|
33
|
+
* Cached lookup of the subscription set. Built once at
|
|
34
|
+
* construction time so `onEvent` is a single `Set.has` check.
|
|
35
|
+
*/
|
|
36
|
+
private readonly subscribed;
|
|
37
|
+
/**
|
|
38
|
+
* Test-only override for the retry schedule. When
|
|
39
|
+
* `process.env.WEBHOOK_TEST_FAST === '1'`, the schedule is
|
|
40
|
+
* `[1ms, 5ms, 25ms, 125ms]` so the suite can exercise the
|
|
41
|
+
* 4-attempt path without waiting 156 seconds. The override
|
|
42
|
+
* is gated behind an env var so production cannot trip it
|
|
43
|
+
* by accident.
|
|
44
|
+
*/
|
|
45
|
+
private readonly retryDelaysMs;
|
|
46
|
+
/**
|
|
47
|
+
* Sentinel subscriber set: defaults to
|
|
48
|
+
* `[JOB_COMPLETED, JOB_FAILED, STEP_FAILED]`. Overridable via
|
|
49
|
+
* the `events` option in `forRoot({...})`.
|
|
50
|
+
*/
|
|
51
|
+
constructor(options: ResolvedWebhookOptions);
|
|
52
|
+
/**
|
|
53
|
+
* `BatchObserver` entry point. Filters by the subscription
|
|
54
|
+
* set, then dispatches to every URL. NEVER throws — a slow /
|
|
55
|
+
* failing observer must not poison the executor (the
|
|
56
|
+
* JobExecutor already swallows observer errors, but we are
|
|
57
|
+
* defensive in depth).
|
|
58
|
+
*/
|
|
59
|
+
onEvent(event: BatchEvent): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Build the envelope once, then POST to every URL in
|
|
62
|
+
* `urls` in parallel. A single URL's retry exhaustion does
|
|
63
|
+
* not affect the other URLs — each URL has its own
|
|
64
|
+
* `deliverToUrl` invocation and its own dead-letter line.
|
|
65
|
+
*
|
|
66
|
+
* The envelope is built with `JSON.stringify` (NOT a Nest
|
|
67
|
+
* serializer) so the bytes are stable and match the HMAC
|
|
68
|
+
* input byte-for-byte. The body string is the literal
|
|
69
|
+
* argument to `fetch`, so the receiver sees the same
|
|
70
|
+
* bytes the observer signed.
|
|
71
|
+
*/
|
|
72
|
+
private deliverToAll;
|
|
73
|
+
/**
|
|
74
|
+
* Build the v1 envelope payload. The shape is the contract
|
|
75
|
+
* the receiver expects; changing it is a breaking change.
|
|
76
|
+
*
|
|
77
|
+
* - `version: 1` — the envelope schema version (the
|
|
78
|
+
* `v1=` in the signature header is the SIGNATURE
|
|
79
|
+
* version, not the ENVELOPE version; they are
|
|
80
|
+
* independent).
|
|
81
|
+
* - `type` — the `BatchEvent.type` string verbatim
|
|
82
|
+
* (e.g. `nest-batch.job.completed`).
|
|
83
|
+
* - `timestamp` — the event's `Date` serialized as
|
|
84
|
+
* ISO-8601 (the original `Date` is not JSON-safe).
|
|
85
|
+
* - `jobId` — the `jobExecutionId` (the `BatchEvent`
|
|
86
|
+
* contract guarantees this is always set).
|
|
87
|
+
* - `execution` — the `JobExecution` shape derived from
|
|
88
|
+
* the event's `data` payload. The observer treats
|
|
89
|
+
* `data` as opaque `JsonValue` and passes it through
|
|
90
|
+
* after a defensive deep-copy via `structuredClone`
|
|
91
|
+
* so the observer cannot mutate the executor's
|
|
92
|
+
* internal state by reference.
|
|
93
|
+
* - `stepId` — present for STEP\_\* / CHUNK\_\* / ITEM\_\*
|
|
94
|
+
* events; absent for JOB\_\* events. Mirrors the
|
|
95
|
+
* `BatchEvent.stepExecutionId` contract.
|
|
96
|
+
*/
|
|
97
|
+
private buildEnvelope;
|
|
98
|
+
/**
|
|
99
|
+
* POST the envelope to one URL with the full retry budget.
|
|
100
|
+
* Stops on the first 2xx; retries on 5xx and network errors;
|
|
101
|
+
* does NOT retry on 4xx; emits a dead-letter `warn` on
|
|
102
|
+
* exhaustion. The body is signed once; the same signed body
|
|
103
|
+
* is sent on every attempt.
|
|
104
|
+
*/
|
|
105
|
+
private deliverToUrl;
|
|
106
|
+
/**
|
|
107
|
+
* Single POST attempt. The signature header is sent on
|
|
108
|
+
* every attempt (the body bytes are identical across
|
|
109
|
+
* attempts; the receiver can verify the signature against
|
|
110
|
+
* any of them).
|
|
111
|
+
*
|
|
112
|
+
* The result is a discriminated union:
|
|
113
|
+
* - `kind: 'success'` — 2xx (or 3xx; we follow
|
|
114
|
+
* the redirect by default in `fetch`, but the
|
|
115
|
+
* receiver's terminal status is what we report)
|
|
116
|
+
* - `kind: 'client-error'` — 4xx (no retry)
|
|
117
|
+
* - `kind: 'server-error'` — 5xx (retry)
|
|
118
|
+
* - `kind: 'network-error'` — fetch threw, or
|
|
119
|
+
* `AbortError` from the timeout (retry)
|
|
120
|
+
*/
|
|
121
|
+
private attemptOnce;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* The v1 webhook envelope payload. This is the contract the
|
|
125
|
+
* receiver's parser expects. Fields are stable; new fields
|
|
126
|
+
* are additive only and use the `x-` prefix to mark them
|
|
127
|
+
* as out-of-contract for v1.
|
|
128
|
+
*/
|
|
129
|
+
export interface WebhookEnvelope {
|
|
130
|
+
/** Envelope schema version. Always `1` for v1. */
|
|
131
|
+
readonly version: 1;
|
|
132
|
+
/** The `BatchEvent.type` string (e.g. `nest-batch.job.completed`). */
|
|
133
|
+
readonly type: BatchEventType;
|
|
134
|
+
/** Event timestamp as ISO-8601. */
|
|
135
|
+
readonly timestamp: string;
|
|
136
|
+
/** The `JobExecution.id` (a.k.a. `jobExecutionId`). */
|
|
137
|
+
readonly jobId: string;
|
|
138
|
+
/** The `StepExecution.id` (a.k.a. `stepExecutionId`). STEP\_\* / CHUNK\_\* / ITEM\_\* events only. */
|
|
139
|
+
readonly stepId?: string;
|
|
140
|
+
/** The `BatchEvent.data` payload, deep-cloned for safety. */
|
|
141
|
+
readonly execution: unknown;
|
|
142
|
+
}
|
|
143
|
+
export { BATCH_EVENT };
|
|
144
|
+
//# sourceMappingURL=webhook-batch.observer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook-batch.observer.d.ts","sourceRoot":"","sources":["../../src/webhook-batch.observer.ts"],"names":[],"mappings":"AACA,OAAO,EACL,WAAW,EACX,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAIL,KAAK,sBAAsB,EAE5B,MAAM,kBAAkB,CAAC;AAO1B;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,qBACa,oBAAqB,YAAW,aAAa;IACxD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgB;IAEvC,yEAAyE;IACzE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyB;IAEjD;;;OAGG;IACH,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA8B;IAEzD;;;;;;;OAOG;IACH,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAoB;IAElD;;;;OAIG;gBAE+B,OAAO,EAAE,sBAAsB;IAWjE;;;;;;OAMG;IACG,OAAO,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IA2B/C;;;;;;;;;;;OAWG;YACW,YAAY;IAQ1B;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,OAAO,CAAC,aAAa;IAiBrB;;;;;;OAMG;YACW,YAAY;IAmF1B;;;;;;;;;;;;;;OAcG;YACW,WAAW;CAmD1B;AA6CD;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,kDAAkD;IAClD,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;IACpB,sEAAsE;IACtE,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAC9B,mCAAmC;IACnC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,uDAAuD;IACvD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,sGAAsG;IACtG,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,6DAA6D;IAC7D,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAMD,OAAO,EAAE,WAAW,EAAE,CAAC"}
|