@dudousxd/nestjs-notifications-webhook 0.6.0 → 0.7.1
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/dist/index.cjs +191 -57
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +112 -48
- package/dist/index.d.ts +112 -48
- package/dist/index.js +186 -57
- package/dist/index.js.map +1 -1
- package/package.json +18 -5
package/dist/index.js
CHANGED
|
@@ -7,7 +7,6 @@ var WebhookMessage = class {
|
|
|
7
7
|
__name(this, "WebhookMessage");
|
|
8
8
|
}
|
|
9
9
|
_url;
|
|
10
|
-
_method = "POST";
|
|
11
10
|
_headers = {};
|
|
12
11
|
_payload = {};
|
|
13
12
|
/** Set the target URL (overrides the route and configured default). */
|
|
@@ -33,17 +32,9 @@ var WebhookMessage = class {
|
|
|
33
32
|
};
|
|
34
33
|
return this;
|
|
35
34
|
}
|
|
36
|
-
/** Set the HTTP method (default `POST`). */
|
|
37
|
-
method(m) {
|
|
38
|
-
this._method = m;
|
|
39
|
-
return this;
|
|
40
|
-
}
|
|
41
35
|
get urlValue() {
|
|
42
36
|
return this._url;
|
|
43
37
|
}
|
|
44
|
-
get methodValue() {
|
|
45
|
-
return this._method;
|
|
46
|
-
}
|
|
47
38
|
get headerValues() {
|
|
48
39
|
return this._headers;
|
|
49
40
|
}
|
|
@@ -54,7 +45,6 @@ var WebhookMessage = class {
|
|
|
54
45
|
toRequest() {
|
|
55
46
|
return {
|
|
56
47
|
url: this._url,
|
|
57
|
-
method: this._method,
|
|
58
48
|
headers: {
|
|
59
49
|
...this._headers
|
|
60
50
|
},
|
|
@@ -62,15 +52,51 @@ var WebhookMessage = class {
|
|
|
62
52
|
};
|
|
63
53
|
}
|
|
64
54
|
};
|
|
55
|
+
var WebhookEvent = class {
|
|
56
|
+
static {
|
|
57
|
+
__name(this, "WebhookEvent");
|
|
58
|
+
}
|
|
59
|
+
type;
|
|
60
|
+
data;
|
|
61
|
+
constructor(type, data) {
|
|
62
|
+
this.type = type;
|
|
63
|
+
this.data = data;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
65
66
|
|
|
66
67
|
// src/webhook.channel.ts
|
|
67
|
-
import {
|
|
68
|
-
import { BaseChannel, createChannel,
|
|
68
|
+
import { createHash, randomUUID } from "crypto";
|
|
69
|
+
import { BaseChannel, createChannel, routeFor } from "@dudousxd/nestjs-notifications-core";
|
|
69
70
|
import { Inject, Injectable, Optional } from "@nestjs/common";
|
|
71
|
+
import { OutboxInbox } from "@nestjs/outbox";
|
|
72
|
+
import { HttpWebhookTransport, Webhooks } from "@nestjs/webhooks";
|
|
73
|
+
|
|
74
|
+
// src/signing.ts
|
|
75
|
+
import { createHmac } from "crypto";
|
|
76
|
+
function standardWebhookHeaders(secret, id, body, now = /* @__PURE__ */ new Date()) {
|
|
77
|
+
const timestamp = Math.floor(now.getTime() / 1e3);
|
|
78
|
+
const encoded = secret.startsWith("whsec_") ? secret.slice("whsec_".length) : secret;
|
|
79
|
+
const key = Buffer.from(encoded, "base64");
|
|
80
|
+
if (key.length < 24 || key.length > 64) {
|
|
81
|
+
throw new TypeError("A Standard Webhooks secret is `whsec_` followed by base64 of 24 to 64 random bytes.");
|
|
82
|
+
}
|
|
83
|
+
const signature = createHmac("sha256", key).update(`${id}.${timestamp}.`).update(body).digest("base64");
|
|
84
|
+
return {
|
|
85
|
+
"webhook-id": id,
|
|
86
|
+
"webhook-timestamp": String(timestamp),
|
|
87
|
+
"webhook-signature": `v1,${signature}`
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
__name(standardWebhookHeaders, "standardWebhookHeaders");
|
|
91
|
+
function sha256Signature(secret, body) {
|
|
92
|
+
return `sha256=${createHmac("sha256", secret).update(body).digest("hex")}`;
|
|
93
|
+
}
|
|
94
|
+
__name(sha256Signature, "sha256Signature");
|
|
70
95
|
|
|
71
96
|
// src/tokens.ts
|
|
72
97
|
var WEBHOOK_OPTIONS = /* @__PURE__ */ Symbol("WEBHOOK_OPTIONS");
|
|
73
98
|
var WEBHOOK_OPTIONS_RESOLVER = /* @__PURE__ */ Symbol("WEBHOOK_OPTIONS_RESOLVER");
|
|
99
|
+
var WEBHOOK_TRANSACTION = /* @__PURE__ */ Symbol("WEBHOOK_TRANSACTION");
|
|
74
100
|
|
|
75
101
|
// src/webhook.channel.ts
|
|
76
102
|
function _ts_decorate(decorators, target, key, desc) {
|
|
@@ -100,6 +126,9 @@ function _ts_param(paramIndex, decorator) {
|
|
|
100
126
|
}
|
|
101
127
|
__name(_ts_param, "_ts_param");
|
|
102
128
|
var DEFAULT_SIGNATURE_HEADER = "X-Signature-256";
|
|
129
|
+
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
130
|
+
var MAX_CACHED_TRANSPORTS = 1e3;
|
|
131
|
+
var WEBHOOK_DISPATCH_CONSUMER = "aviary.notifications.webhook-dispatch";
|
|
103
132
|
var Webhook = createChannel("webhook");
|
|
104
133
|
var WebhookChannel = class extends BaseChannel {
|
|
105
134
|
static {
|
|
@@ -107,13 +136,23 @@ var WebhookChannel = class extends BaseChannel {
|
|
|
107
136
|
}
|
|
108
137
|
options;
|
|
109
138
|
resolveOptions;
|
|
139
|
+
transaction;
|
|
140
|
+
webhooks;
|
|
141
|
+
inbox;
|
|
110
142
|
channel = "webhook";
|
|
111
|
-
|
|
112
|
-
|
|
143
|
+
/**
|
|
144
|
+
* `HttpWebhookTransport`s by tenant (`''` without one), rebuilt only when that tenant's
|
|
145
|
+
* `delivery` options change. Keyed by tenant rather than by the resolved options object, since a
|
|
146
|
+
* resolver may return a fresh object on every call.
|
|
147
|
+
*/
|
|
148
|
+
transports = /* @__PURE__ */ new Map();
|
|
149
|
+
constructor(options, resolveOptions, transaction, webhooks, inbox) {
|
|
150
|
+
super(), this.options = options, this.resolveOptions = resolveOptions, this.transaction = transaction, this.webhooks = webhooks, this.inbox = inbox;
|
|
113
151
|
}
|
|
114
152
|
async send(notifiable, notification, context) {
|
|
115
|
-
const options = this.forTenant(this.options, context, this.resolveOptions);
|
|
116
153
|
const result = this.buildPayload(notification, notifiable, "toWebhook", context);
|
|
154
|
+
if (result instanceof WebhookEvent) return this.dispatch(result, context);
|
|
155
|
+
const options = this.forTenant(this.options, context, this.resolveOptions);
|
|
117
156
|
const message = result instanceof WebhookMessage ? result : new WebhookMessage().payload(result);
|
|
118
157
|
const request = message.toRequest();
|
|
119
158
|
const route = routeFor(notifiable, "webhook", notification);
|
|
@@ -121,25 +160,87 @@ var WebhookChannel = class extends BaseChannel {
|
|
|
121
160
|
if (!url) {
|
|
122
161
|
throw new Error('The webhook channel needs a target URL. Set one on the WebhookMessage via .url(), return one from routeNotificationFor("webhook"), or set url in forRoot().');
|
|
123
162
|
}
|
|
163
|
+
const body = JSON.stringify(request.body);
|
|
164
|
+
const id = context?.idempotencyKey ?? `msg_${randomUUID().replaceAll("-", "")}`;
|
|
124
165
|
const headers = {
|
|
125
|
-
|
|
126
|
-
...
|
|
166
|
+
"content-type": "application/json",
|
|
167
|
+
...lowerCase(options.headers),
|
|
168
|
+
...lowerCase(request.headers)
|
|
127
169
|
};
|
|
128
170
|
if (options.secret) {
|
|
129
|
-
|
|
130
|
-
|
|
171
|
+
if (options.signing === "standard") {
|
|
172
|
+
Object.assign(headers, standardWebhookHeaders(options.secret, id, body));
|
|
173
|
+
} else {
|
|
174
|
+
headers[(options.signatureHeader ?? DEFAULT_SIGNATURE_HEADER).toLowerCase()] = sha256Signature(options.secret, body);
|
|
175
|
+
}
|
|
131
176
|
}
|
|
132
|
-
await
|
|
133
|
-
|
|
134
|
-
method: request.method,
|
|
177
|
+
const response = await this.transportFor(options, context?.tenant).send({
|
|
178
|
+
url,
|
|
135
179
|
headers,
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
180
|
+
body,
|
|
181
|
+
endpointId: "aviary",
|
|
182
|
+
deliveryId: id,
|
|
183
|
+
messageId: id,
|
|
184
|
+
type: "notification"
|
|
185
|
+
}, {
|
|
186
|
+
signal: AbortSignal.timeout(options.timeoutMs ?? DEFAULT_TIMEOUT_MS),
|
|
187
|
+
attempt: 1
|
|
142
188
|
});
|
|
189
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
190
|
+
throw new Error(`Webhook request to ${url} failed with status ${response.statusCode}.`);
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
id,
|
|
194
|
+
statusCode: response.statusCode
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
async dispatch(event, context) {
|
|
198
|
+
const webhooks = this.webhooks;
|
|
199
|
+
if (!webhooks) {
|
|
200
|
+
throw new Error("A WebhookEvent needs WebhooksModule (and OutboxModule) from @nestjs/webhooks.");
|
|
201
|
+
}
|
|
202
|
+
if (!this.transaction) {
|
|
203
|
+
throw new Error("A WebhookEvent needs WebhookChannelModule.forRoot({ transaction: (work) => db.transaction(work) }): @nestjs/webhooks writes the event through a transaction.");
|
|
204
|
+
}
|
|
205
|
+
const id = context?.idempotencyKey ? webhookId(context.idempotencyKey) : void 0;
|
|
206
|
+
const inbox = this.inbox;
|
|
207
|
+
const outcome = await this.transaction(async (tx) => {
|
|
208
|
+
const dispatch = /* @__PURE__ */ __name(() => webhooks.dispatch(tx, {
|
|
209
|
+
type: event.type,
|
|
210
|
+
data: event.data,
|
|
211
|
+
tenant: context?.tenant ?? null,
|
|
212
|
+
...id ? {
|
|
213
|
+
id
|
|
214
|
+
} : {}
|
|
215
|
+
}), "dispatch");
|
|
216
|
+
if (!id || !inbox) return {
|
|
217
|
+
duplicate: false,
|
|
218
|
+
result: await dispatch()
|
|
219
|
+
};
|
|
220
|
+
return inbox.processInTransaction(tx, WEBHOOK_DISPATCH_CONSUMER, id, dispatch);
|
|
221
|
+
});
|
|
222
|
+
if (outcome.duplicate) return {
|
|
223
|
+
id,
|
|
224
|
+
duplicate: true
|
|
225
|
+
};
|
|
226
|
+
webhooks.notify();
|
|
227
|
+
return outcome.result;
|
|
228
|
+
}
|
|
229
|
+
transportFor(options, tenant) {
|
|
230
|
+
if (options.transport) return options.transport;
|
|
231
|
+
const key = tenant && this.resolveOptions ? tenant : "";
|
|
232
|
+
const cached = this.transports.get(key);
|
|
233
|
+
if (cached && sameDelivery(cached.delivery, options.delivery)) return cached.transport;
|
|
234
|
+
const transport = new HttpWebhookTransport(options.delivery);
|
|
235
|
+
this.transports.delete(key);
|
|
236
|
+
this.transports.set(key, {
|
|
237
|
+
delivery: options.delivery,
|
|
238
|
+
transport
|
|
239
|
+
});
|
|
240
|
+
while (this.transports.size > MAX_CACHED_TRANSPORTS) {
|
|
241
|
+
this.transports.delete(this.transports.keys().next().value);
|
|
242
|
+
}
|
|
243
|
+
return transport;
|
|
143
244
|
}
|
|
144
245
|
};
|
|
145
246
|
WebhookChannel = _ts_decorate([
|
|
@@ -147,12 +248,46 @@ WebhookChannel = _ts_decorate([
|
|
|
147
248
|
_ts_param(0, Inject(WEBHOOK_OPTIONS)),
|
|
148
249
|
_ts_param(1, Optional()),
|
|
149
250
|
_ts_param(1, Inject(WEBHOOK_OPTIONS_RESOLVER)),
|
|
251
|
+
_ts_param(2, Optional()),
|
|
252
|
+
_ts_param(2, Inject(WEBHOOK_TRANSACTION)),
|
|
253
|
+
_ts_param(3, Optional()),
|
|
254
|
+
_ts_param(4, Optional()),
|
|
150
255
|
_ts_metadata("design:type", Function),
|
|
151
256
|
_ts_metadata("design:paramtypes", [
|
|
152
257
|
typeof WebhookChannelOptions === "undefined" ? Object : WebhookChannelOptions,
|
|
153
|
-
typeof WebhookOptionsResolver === "undefined" ? Object : WebhookOptionsResolver
|
|
258
|
+
typeof WebhookOptionsResolver === "undefined" ? Object : WebhookOptionsResolver,
|
|
259
|
+
typeof WebhookTransactionRunner === "undefined" ? Object : WebhookTransactionRunner,
|
|
260
|
+
typeof Webhooks === "undefined" ? Object : Webhooks,
|
|
261
|
+
typeof OutboxInbox === "undefined" ? Object : OutboxInbox
|
|
154
262
|
])
|
|
155
263
|
], WebhookChannel);
|
|
264
|
+
function lowerCase(headers) {
|
|
265
|
+
const out = {};
|
|
266
|
+
for (const [name, value] of Object.entries(headers ?? {})) out[name.toLowerCase()] = value;
|
|
267
|
+
return out;
|
|
268
|
+
}
|
|
269
|
+
__name(lowerCase, "lowerCase");
|
|
270
|
+
function sameDelivery(a, b) {
|
|
271
|
+
if (a === b) return true;
|
|
272
|
+
if (!a || !b) return false;
|
|
273
|
+
const keys = Object.keys(a);
|
|
274
|
+
if (keys.length !== Object.keys(b).length) return false;
|
|
275
|
+
return keys.every((key) => {
|
|
276
|
+
const x = a[key];
|
|
277
|
+
const y = b[key];
|
|
278
|
+
if (Array.isArray(x) && Array.isArray(y)) {
|
|
279
|
+
return x.length === y.length && x.every((item, i) => Object.is(item, y[i]));
|
|
280
|
+
}
|
|
281
|
+
return Object.is(x, y);
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
__name(sameDelivery, "sameDelivery");
|
|
285
|
+
function webhookId(key) {
|
|
286
|
+
const kept = `msg_${key}`;
|
|
287
|
+
if (/^[A-Za-z0-9_-]+$/.test(key) && kept.length <= 128) return kept;
|
|
288
|
+
return `msgh_${createHash("sha256").update(key).digest("hex")}`;
|
|
289
|
+
}
|
|
290
|
+
__name(webhookId, "webhookId");
|
|
156
291
|
|
|
157
292
|
// src/webhook.module.ts
|
|
158
293
|
import { defineChannelModule } from "@dudousxd/nestjs-notifications-core";
|
|
@@ -176,52 +311,46 @@ var WebhookChannelModule = class _WebhookChannelModule {
|
|
|
176
311
|
__name(this, "WebhookChannelModule");
|
|
177
312
|
}
|
|
178
313
|
static forRoot(options = {}) {
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
// per-notifiable URL and default headers when these are absent.
|
|
182
|
-
...options.url !== void 0 ? {
|
|
183
|
-
url: options.url
|
|
184
|
-
} : {},
|
|
185
|
-
...options.headers !== void 0 ? {
|
|
186
|
-
headers: options.headers
|
|
187
|
-
} : {},
|
|
188
|
-
...options.secret !== void 0 ? {
|
|
189
|
-
secret: options.secret
|
|
190
|
-
} : {},
|
|
191
|
-
...options.signatureHeader !== void 0 ? {
|
|
192
|
-
signatureHeader: options.signatureHeader
|
|
193
|
-
} : {},
|
|
194
|
-
...options.timeoutMs !== void 0 ? {
|
|
195
|
-
timeoutMs: options.timeoutMs
|
|
196
|
-
} : {},
|
|
197
|
-
...options.redirect !== void 0 ? {
|
|
198
|
-
redirect: options.redirect
|
|
199
|
-
} : {}
|
|
200
|
-
};
|
|
201
|
-
return defineChannelModule({
|
|
314
|
+
const { resolveOptions, transaction, global, ...channelOptions } = options;
|
|
315
|
+
const module = defineChannelModule({
|
|
202
316
|
module: _WebhookChannelModule,
|
|
203
317
|
channel: WebhookChannel,
|
|
204
318
|
optionsToken: WEBHOOK_OPTIONS,
|
|
205
|
-
options:
|
|
319
|
+
options: channelOptions,
|
|
206
320
|
resolver: {
|
|
207
321
|
token: WEBHOOK_OPTIONS_RESOLVER,
|
|
208
|
-
value:
|
|
322
|
+
value: resolveOptions
|
|
209
323
|
},
|
|
210
|
-
...
|
|
211
|
-
global
|
|
324
|
+
...global !== void 0 ? {
|
|
325
|
+
global
|
|
212
326
|
} : {}
|
|
213
327
|
});
|
|
328
|
+
return {
|
|
329
|
+
...module,
|
|
330
|
+
providers: [
|
|
331
|
+
...module.providers ?? [],
|
|
332
|
+
{
|
|
333
|
+
provide: WEBHOOK_TRANSACTION,
|
|
334
|
+
useValue: transaction ?? null
|
|
335
|
+
}
|
|
336
|
+
]
|
|
337
|
+
};
|
|
214
338
|
}
|
|
215
339
|
};
|
|
216
340
|
WebhookChannelModule = _ts_decorate2([
|
|
217
341
|
Module({})
|
|
218
342
|
], WebhookChannelModule);
|
|
219
343
|
export {
|
|
344
|
+
WEBHOOK_DISPATCH_CONSUMER,
|
|
220
345
|
WEBHOOK_OPTIONS,
|
|
221
346
|
WEBHOOK_OPTIONS_RESOLVER,
|
|
347
|
+
WEBHOOK_TRANSACTION,
|
|
222
348
|
Webhook,
|
|
223
349
|
WebhookChannel,
|
|
224
350
|
WebhookChannelModule,
|
|
225
|
-
|
|
351
|
+
WebhookEvent,
|
|
352
|
+
WebhookMessage,
|
|
353
|
+
sha256Signature,
|
|
354
|
+
standardWebhookHeaders
|
|
226
355
|
};
|
|
227
356
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/webhook-message.ts","../src/webhook.channel.ts","../src/tokens.ts","../src/webhook.module.ts"],"sourcesContent":["/** The HTTP method used for a webhook request. */\nexport type WebhookMethod = 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'GET';\n\n/** The resolved request description produced by {@link WebhookMessage.toRequest}. */\nexport interface WebhookRequest {\n /** Target URL, when set on the message; otherwise resolved by the channel. */\n url?: string | undefined;\n method: WebhookMethod;\n headers: Record<string, string>;\n /** The JSON body to send (a plain object). */\n body: Record<string, unknown>;\n}\n\n/**\n * A fluent builder for a webhook request.\n *\n * ```ts\n * new WebhookMessage()\n * .url('https://example.com/hooks/orders')\n * .header('X-Signature', sign(payload))\n * .payload({ event: 'order.paid', id: 42 });\n * ```\n */\nexport class WebhookMessage {\n private _url?: string;\n private _method: WebhookMethod = 'POST';\n private _headers: Record<string, string> = {};\n private _payload: Record<string, unknown> = {};\n\n /** Set the target URL (overrides the route and configured default). */\n url(u: string): this {\n this._url = u;\n return this;\n }\n\n /** Set the JSON body to send. */\n payload(obj: Record<string, unknown>): this {\n this._payload = obj;\n return this;\n }\n\n /** Set a single request header. */\n header(key: string, value: string): this {\n this._headers[key] = value;\n return this;\n }\n\n /** Merge multiple request headers. */\n headers(obj: Record<string, string>): this {\n this._headers = { ...this._headers, ...obj };\n return this;\n }\n\n /** Set the HTTP method (default `POST`). */\n method(m: WebhookMethod): this {\n this._method = m;\n return this;\n }\n\n get urlValue(): string | undefined {\n return this._url;\n }\n\n get methodValue(): WebhookMethod {\n return this._method;\n }\n\n get headerValues(): Record<string, string> {\n return this._headers;\n }\n\n get payloadValue(): Record<string, unknown> {\n return this._payload;\n }\n\n /** Serialize to a {@link WebhookRequest}. */\n toRequest(): WebhookRequest {\n return {\n url: this._url,\n method: this._method,\n headers: { ...this._headers },\n body: this._payload,\n };\n }\n}\n","import { createHmac } from 'node:crypto';\nimport {\n BaseChannel,\n type ChannelContext,\n type DeliveryContext,\n type Notifiable,\n type Notification,\n createChannel,\n postJson,\n routeFor,\n} from '@dudousxd/nestjs-notifications-core';\nimport { Inject, Injectable, Optional } from '@nestjs/common';\nimport { WEBHOOK_OPTIONS, WEBHOOK_OPTIONS_RESOLVER } from './tokens';\nimport { WebhookMessage } from './webhook-message';\n\nconst DEFAULT_SIGNATURE_HEADER = 'X-Signature-256';\n\n/** Channel handle: use as `@Webhook()` on a payload method, or as a token in `via()`. */\nexport const Webhook = createChannel('webhook');\n\n/** Resolved runtime options for the webhook channel. */\nexport interface WebhookChannelOptions {\n /** Default URL used when neither the message nor the route supplies one. */\n url?: string;\n /** Default headers merged into every request. */\n headers?: Record<string, string>;\n /**\n * HMAC-SHA256 secret. When set, the request is signed: the header (default\n * `X-Signature-256`) is set to `sha256=<hex digest of the raw JSON body>`, so the receiver\n * can verify authenticity. Per-tenant secrets work via `resolveOptions`.\n */\n secret?: string;\n /** Header name for the signature. Default `X-Signature-256`. */\n signatureHeader?: string;\n /** Abort a request that takes longer than this (ms). Unset = no timeout. */\n timeoutMs?: number;\n /**\n * Redirect handling. Default `'follow'` (the `fetch` default). Use `'error'` when webhook URLs\n * are user-supplied, so a 3xx can't bounce the (possibly signed) request somewhere else.\n */\n redirect?: 'follow' | 'error' | 'manual';\n}\n\n/**\n * Implement this on a notification to define its webhook payload. Returning a plain\n * object is treated as the JSON body to POST; return a {@link WebhookMessage} for full\n * control over the url, method and headers.\n */\nexport interface WebhookNotification extends Notification {\n toWebhook(ctx: ChannelContext): WebhookMessage | Record<string, unknown>;\n}\n\n/** Resolves per-tenant {@link WebhookChannelOptions} from a tenant id. */\nexport type WebhookOptionsResolver = (tenant: string) => WebhookChannelOptions;\n\n/**\n * Delivers a notification by sending an HTTP request (JSON body) to a webhook endpoint.\n * The target URL comes from the {@link WebhookMessage}, else from\n * `routeNotificationFor('webhook')` (a URL string), else from the configured default.\n */\n@Injectable()\nexport class WebhookChannel extends BaseChannel {\n readonly channel = 'webhook';\n\n constructor(\n @Inject(WEBHOOK_OPTIONS)\n private readonly options: WebhookChannelOptions,\n @Optional()\n @Inject(WEBHOOK_OPTIONS_RESOLVER)\n private readonly resolveOptions?: WebhookOptionsResolver,\n ) {\n super();\n }\n\n async send(\n notifiable: Notifiable,\n notification: Notification,\n context?: DeliveryContext,\n ): Promise<void> {\n const options = this.forTenant(this.options, context, this.resolveOptions);\n const result = this.buildPayload<WebhookMessage | Record<string, unknown>>(\n notification,\n notifiable,\n 'toWebhook',\n context,\n );\n const message =\n result instanceof WebhookMessage ? result : new WebhookMessage().payload(result);\n const request = message.toRequest();\n\n const route = routeFor(notifiable, 'webhook', notification);\n const url = request.url ?? (typeof route === 'string' ? route : undefined) ?? options.url;\n\n if (!url) {\n throw new Error(\n 'The webhook channel needs a target URL. Set one on the WebhookMessage via .url(), ' +\n 'return one from routeNotificationFor(\"webhook\"), or set url in forRoot().',\n );\n }\n\n const headers: Record<string, string> = { ...options.headers, ...request.headers };\n if (options.secret) {\n const signature = createHmac('sha256', options.secret)\n .update(JSON.stringify(request.body))\n .digest('hex');\n headers[options.signatureHeader ?? DEFAULT_SIGNATURE_HEADER] = `sha256=${signature}`;\n }\n\n await postJson(url, request.body, {\n label: 'Webhook',\n method: request.method,\n headers,\n ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),\n ...(options.redirect !== undefined ? { redirect: options.redirect } : {}),\n });\n }\n}\n","/** DI token for the resolved {@link WebhookChannelOptions} (default url, headers). */\nexport const WEBHOOK_OPTIONS = Symbol('WEBHOOK_OPTIONS');\n\n/** DI token for the optional per-tenant {@link WebhookChannelOptions} resolver. */\nexport const WEBHOOK_OPTIONS_RESOLVER = Symbol('WEBHOOK_OPTIONS_RESOLVER');\n","import { defineChannelModule } from '@dudousxd/nestjs-notifications-core';\nimport { type DynamicModule, Module } from '@nestjs/common';\nimport { WEBHOOK_OPTIONS, WEBHOOK_OPTIONS_RESOLVER } from './tokens';\nimport {\n WebhookChannel,\n type WebhookChannelOptions,\n type WebhookOptionsResolver,\n} from './webhook.channel';\n\nexport interface WebhookChannelModuleOptions {\n /** Default URL used when neither the message nor the route supplies one. */\n url?: string;\n /** Default headers merged into every request. */\n headers?: Record<string, string>;\n /**\n * HMAC-SHA256 signing secret. When set, every request carries\n * `<signatureHeader>: sha256=<hex digest of the JSON body>`.\n */\n secret?: string;\n /** Header name for the signature. Default `X-Signature-256`. */\n signatureHeader?: string;\n /** Abort a request that takes longer than this (ms). Unset = no timeout. */\n timeoutMs?: number;\n /** Redirect handling: `'follow'` (default), `'error'` or `'manual'`. */\n redirect?: 'follow' | 'error' | 'manual';\n /**\n * Optional per-tenant options resolver. When a notification is delivered with a\n * `context.tenant`, the returned options are used instead of the defaults.\n */\n resolveOptions?: WebhookOptionsResolver;\n /** Register globally so the channel is discoverable app-wide. Default true. */\n global?: boolean;\n}\n\n/**\n * Registers the webhook channel.\n *\n * ```ts\n * WebhookChannelModule.forRoot({ url: 'https://example.com/hooks/notifications' });\n * ```\n */\n@Module({})\nexport class WebhookChannelModule {\n static forRoot(options: WebhookChannelModuleOptions = {}): DynamicModule {\n const webhookOptions: WebhookChannelOptions = {\n // Set each field only when provided (exactOptionalPropertyTypes); the channel resolves a\n // per-notifiable URL and default headers when these are absent.\n ...(options.url !== undefined ? { url: options.url } : {}),\n ...(options.headers !== undefined ? { headers: options.headers } : {}),\n ...(options.secret !== undefined ? { secret: options.secret } : {}),\n ...(options.signatureHeader !== undefined\n ? { signatureHeader: options.signatureHeader }\n : {}),\n ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),\n ...(options.redirect !== undefined ? { redirect: options.redirect } : {}),\n };\n\n return defineChannelModule({\n module: WebhookChannelModule,\n channel: WebhookChannel,\n optionsToken: WEBHOOK_OPTIONS,\n options: webhookOptions,\n resolver: { token: WEBHOOK_OPTIONS_RESOLVER, value: options.resolveOptions },\n ...(options.global !== undefined ? { global: options.global } : {}),\n });\n }\n}\n"],"mappings":"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","names":["WebhookMessage","_url","_method","_headers","_payload","url","u","payload","obj","header","key","value","headers","method","m","urlValue","methodValue","headerValues","payloadValue","toRequest","body","createHmac","BaseChannel","createChannel","postJson","routeFor","Inject","Injectable","Optional","WEBHOOK_OPTIONS","Symbol","WEBHOOK_OPTIONS_RESOLVER","DEFAULT_SIGNATURE_HEADER","Webhook","createChannel","WebhookChannel","BaseChannel","channel","options","resolveOptions","send","notifiable","notification","context","forTenant","result","buildPayload","message","WebhookMessage","payload","request","toRequest","route","routeFor","url","undefined","Error","headers","secret","signature","createHmac","update","JSON","stringify","body","digest","signatureHeader","postJson","label","method","timeoutMs","redirect","defineChannelModule","Module","WebhookChannelModule","forRoot","options","webhookOptions","url","undefined","headers","secret","signatureHeader","timeoutMs","redirect","defineChannelModule","module","channel","WebhookChannel","optionsToken","WEBHOOK_OPTIONS","resolver","token","WEBHOOK_OPTIONS_RESOLVER","value","resolveOptions","global"]}
|
|
1
|
+
{"version":3,"sources":["../src/webhook-message.ts","../src/webhook.channel.ts","../src/signing.ts","../src/tokens.ts","../src/webhook.module.ts"],"sourcesContent":["/** The resolved request description produced by {@link WebhookMessage.toRequest}. */\nexport interface WebhookRequest {\n /** Target URL, when set on the message; otherwise resolved by the channel. */\n url?: string | undefined;\n headers: Record<string, string>;\n /** The JSON body to POST (a plain object). */\n body: Record<string, unknown>;\n}\n\n/**\n * A fluent builder for a direct webhook request (always a `POST` with a JSON body).\n *\n * ```ts\n * new WebhookMessage()\n * .url('https://example.com/hooks/orders')\n * .header('X-App', 'shop')\n * .payload({ event: 'order.paid', id: 42 });\n * ```\n */\nexport class WebhookMessage {\n private _url?: string;\n private _headers: Record<string, string> = {};\n private _payload: Record<string, unknown> = {};\n\n /** Set the target URL (overrides the route and configured default). */\n url(u: string): this {\n this._url = u;\n return this;\n }\n\n /** Set the JSON body to send. */\n payload(obj: Record<string, unknown>): this {\n this._payload = obj;\n return this;\n }\n\n /** Set a single request header. */\n header(key: string, value: string): this {\n this._headers[key] = value;\n return this;\n }\n\n /** Merge multiple request headers. */\n headers(obj: Record<string, string>): this {\n this._headers = { ...this._headers, ...obj };\n return this;\n }\n\n get urlValue(): string | undefined {\n return this._url;\n }\n\n get headerValues(): Record<string, string> {\n return this._headers;\n }\n\n get payloadValue(): Record<string, unknown> {\n return this._payload;\n }\n\n /** Serialize to a {@link WebhookRequest}. */\n toRequest(): WebhookRequest {\n return { url: this._url, headers: { ...this._headers }, body: this._payload };\n }\n}\n\n/**\n * A webhook event for subscription delivery: `@nestjs/webhooks` fans it out to every endpoint of\n * the tenant subscribed to `type`, signs it (Standard Webhooks), retries and logs each delivery.\n * Return one from `toWebhook(ctx)` instead of a {@link WebhookMessage}.\n *\n * ```ts\n * toWebhook() { return new WebhookEvent('invoice.paid', { id: this.invoice.id }); }\n * ```\n */\nexport class WebhookEvent<D = unknown> {\n constructor(\n /** Full-stop delimited: `invoice.paid`. In `WebhooksModule`'s `eventTypes` when that is set. */\n readonly type: string,\n /** JSON-serializable data, sent as the body's `data`. */\n readonly data: D,\n ) {}\n}\n","import { createHash, randomUUID } from 'node:crypto';\nimport {\n BaseChannel,\n type ChannelContext,\n type DeliveryContext,\n type Notifiable,\n type Notification,\n createChannel,\n routeFor,\n} from '@dudousxd/nestjs-notifications-core';\nimport { Inject, Injectable, Optional } from '@nestjs/common';\nimport { OutboxInbox } from '@nestjs/outbox';\nimport {\n type WebhookMessage as DispatchedWebhookMessage,\n HttpWebhookTransport,\n type HttpWebhookTransportOptions,\n WebhookTransport,\n Webhooks,\n} from '@nestjs/webhooks';\nimport { sha256Signature, standardWebhookHeaders } from './signing';\nimport { WEBHOOK_OPTIONS, WEBHOOK_OPTIONS_RESOLVER, WEBHOOK_TRANSACTION } from './tokens';\nimport { WebhookEvent, WebhookMessage } from './webhook-message';\n\nconst DEFAULT_SIGNATURE_HEADER = 'X-Signature-256';\nconst DEFAULT_TIMEOUT_MS = 15_000;\n/** Tenants whose `HttpWebhookTransport` is kept (oldest built first out). */\nconst MAX_CACHED_TRANSPORTS = 1000;\n\n/**\n * The outbox inbox consumer under which subscription deliveries record the `webhook-id`s they\n * dispatched, so a retried notification doesn't dispatch the same event twice.\n */\nexport const WEBHOOK_DISPATCH_CONSUMER = 'aviary.notifications.webhook-dispatch';\n\n/** Channel handle: use as `@Webhook()` on a payload method, or as a token in `via()`. */\nexport const Webhook = createChannel('webhook');\n\n/** Opens a transaction for subscription delivery, e.g. `(work) => db.transaction(work)`. */\nexport type WebhookTransactionRunner = <T>(work: (tx: unknown) => Promise<T>) => Promise<T>;\n\n/** Resolved runtime options for direct-URL delivery. */\nexport interface WebhookChannelOptions {\n /** Default URL used when neither the message nor the route supplies one. */\n url?: string;\n /** Default headers merged into every request. */\n headers?: Record<string, string>;\n /** Signing secret. Unset: requests are not signed. */\n secret?: string;\n /**\n * `'sha256'` (default): `<signatureHeader>: sha256=<hex HMAC of the body>`.\n * `'standard'`: Standard Webhooks headers, verifiable with `@nestjs/webhooks`'\n * `@VerifyWebhook()` and a `scheme: 'standard'` receiver; `secret` is then `whsec_…`.\n */\n signing?: 'sha256' | 'standard';\n /** Header for the `'sha256'` signature. Default `X-Signature-256`. */\n signatureHeader?: string;\n /** Abort a request that takes longer than this (ms). Default 15000. */\n timeoutMs?: number;\n /**\n * The SSRF policy of `@nestjs/webhooks`' `HttpWebhookTransport` (https only, every resolved\n * address checked and pinned, redirects never followed): `allowHttp`, `allowPrivateNetworks`,\n * `allowedAddresses`, `ca`, `maxResponseSize`, `lookup`.\n */\n delivery?: HttpWebhookTransportOptions;\n /**\n * A `@nestjs/webhooks` transport instance to send through instead (a proxy, mTLS, or\n * `InMemoryWebhookTransport` in tests). Takes precedence over `delivery`.\n */\n transport?: WebhookTransport;\n}\n\n/**\n * Implement this on a notification to define its webhook payload: a plain object (the JSON body),\n * a {@link WebhookMessage} (url, headers, body), or a {@link WebhookEvent} for subscription\n * delivery through `@nestjs/webhooks`.\n */\nexport interface WebhookNotification extends Notification {\n toWebhook(ctx: ChannelContext): WebhookMessage | WebhookEvent | Record<string, unknown>;\n}\n\n/** Resolves per-tenant {@link WebhookChannelOptions} from a tenant id. */\nexport type WebhookOptionsResolver = (tenant: string) => WebhookChannelOptions;\n\n/** What a direct delivery returns (the channel response). */\nexport interface WebhookDeliveryResult {\n /** The `webhook-id` (Standard signing) or a generated id. */\n id: string;\n statusCode: number;\n}\n\n/**\n * What a subscription delivery returns when its `webhook-id` was already dispatched by an earlier\n * attempt of the same delivery: nothing was dispatched this time.\n */\nexport interface WebhookAlreadyDispatched {\n id: string;\n duplicate: true;\n}\n\n/**\n * Delivers a notification as a webhook, two ways:\n *\n * - **Direct**: a signed `POST` of the JSON body to one URL (the message's, else the\n * `routeNotificationFor('webhook')` URL, else the configured default), through `@nestjs/webhooks`'\n * SSRF-guarded `HttpWebhookTransport`. A non-2xx answer fails the delivery.\n * - **Subscriptions**: a {@link WebhookEvent} is handed to `Webhooks.dispatch()`, which fans it\n * out to the endpoints subscribed to its type (for the delivery's tenant), with retries and a\n * delivery log. Needs `WebhooksModule` + `OutboxModule` and the `transaction` option. A\n * delivery with an `idempotencyKey` (the outbox dispatcher's message id) records its\n * `webhook-id` in the outbox inbox in the same transaction as the dispatch, so a retry of it\n * dispatches nothing and returns {@link WebhookAlreadyDispatched}.\n */\n@Injectable()\nexport class WebhookChannel extends BaseChannel {\n readonly channel = 'webhook';\n /**\n * `HttpWebhookTransport`s by tenant (`''` without one), rebuilt only when that tenant's\n * `delivery` options change. Keyed by tenant rather than by the resolved options object, since a\n * resolver may return a fresh object on every call.\n */\n private readonly transports = new Map<\n string,\n { delivery: HttpWebhookTransportOptions | undefined; transport: WebhookTransport }\n >();\n\n constructor(\n @Inject(WEBHOOK_OPTIONS)\n private readonly options: WebhookChannelOptions,\n @Optional()\n @Inject(WEBHOOK_OPTIONS_RESOLVER)\n private readonly resolveOptions?: WebhookOptionsResolver,\n @Optional()\n @Inject(WEBHOOK_TRANSACTION)\n private readonly transaction?: WebhookTransactionRunner,\n @Optional()\n private readonly webhooks?: Webhooks,\n @Optional()\n private readonly inbox?: OutboxInbox,\n ) {\n super();\n }\n\n async send(\n notifiable: Notifiable,\n notification: Notification,\n context?: DeliveryContext,\n ): Promise<WebhookDeliveryResult | DispatchedWebhookMessage | WebhookAlreadyDispatched> {\n const result = this.buildPayload<WebhookMessage | WebhookEvent | Record<string, unknown>>(\n notification,\n notifiable,\n 'toWebhook',\n context,\n );\n if (result instanceof WebhookEvent) return this.dispatch(result, context);\n\n const options = this.forTenant(this.options, context, this.resolveOptions);\n const message =\n result instanceof WebhookMessage ? result : new WebhookMessage().payload(result);\n const request = message.toRequest();\n const route = routeFor(notifiable, 'webhook', notification);\n const url = request.url ?? (typeof route === 'string' ? route : undefined) ?? options.url;\n if (!url) {\n throw new Error(\n 'The webhook channel needs a target URL. Set one on the WebhookMessage via .url(), ' +\n 'return one from routeNotificationFor(\"webhook\"), or set url in forRoot().',\n );\n }\n\n const body = JSON.stringify(request.body);\n const id = context?.idempotencyKey ?? `msg_${randomUUID().replaceAll('-', '')}`;\n const headers: Record<string, string> = {\n 'content-type': 'application/json',\n ...lowerCase(options.headers),\n ...lowerCase(request.headers),\n };\n if (options.secret) {\n if (options.signing === 'standard') {\n Object.assign(headers, standardWebhookHeaders(options.secret, id, body));\n } else {\n headers[(options.signatureHeader ?? DEFAULT_SIGNATURE_HEADER).toLowerCase()] =\n sha256Signature(options.secret, body);\n }\n }\n\n const response = await this.transportFor(options, context?.tenant).send(\n {\n url,\n headers,\n body,\n endpointId: 'aviary',\n deliveryId: id,\n messageId: id,\n type: 'notification',\n },\n { signal: AbortSignal.timeout(options.timeoutMs ?? DEFAULT_TIMEOUT_MS), attempt: 1 },\n );\n if (response.statusCode < 200 || response.statusCode >= 300) {\n throw new Error(`Webhook request to ${url} failed with status ${response.statusCode}.`);\n }\n return { id, statusCode: response.statusCode };\n }\n\n private async dispatch(\n event: WebhookEvent,\n context: DeliveryContext | undefined,\n ): Promise<DispatchedWebhookMessage | WebhookAlreadyDispatched> {\n const webhooks = this.webhooks;\n if (!webhooks) {\n throw new Error(\n 'A WebhookEvent needs WebhooksModule (and OutboxModule) from @nestjs/webhooks.',\n );\n }\n if (!this.transaction) {\n throw new Error(\n 'A WebhookEvent needs WebhookChannelModule.forRoot({ transaction: (work) => db.transaction(work) }): ' +\n '@nestjs/webhooks writes the event through a transaction.',\n );\n }\n // The outbox message id of this delivery: a retry derives the same webhook-id.\n const id = context?.idempotencyKey ? webhookId(context.idempotencyKey) : undefined;\n const inbox = this.inbox;\n const outcome = await this.transaction(async (tx) => {\n const dispatch = () =>\n webhooks.dispatch(tx, {\n type: event.type,\n data: event.data,\n tenant: context?.tenant ?? null,\n ...(id ? { id } : {}),\n });\n if (!id || !inbox) return { duplicate: false as const, result: await dispatch() };\n // The record commits with the dispatched message, so an attempt whose transaction\n // committed is the only one that dispatches: a retry finds the record and skips it,\n // instead of adding the webhook-id to the outbox again (a duplicate key while the first\n // message is pending).\n return inbox.processInTransaction(tx, WEBHOOK_DISPATCH_CONSUMER, id, dispatch);\n });\n if (outcome.duplicate) return { id: id as string, duplicate: true };\n webhooks.notify();\n return outcome.result;\n }\n\n private transportFor(\n options: WebhookChannelOptions,\n tenant: string | undefined,\n ): WebhookTransport {\n if (options.transport) return options.transport;\n const key = tenant && this.resolveOptions ? tenant : '';\n const cached = this.transports.get(key);\n if (cached && sameDelivery(cached.delivery, options.delivery)) return cached.transport;\n const transport = new HttpWebhookTransport(options.delivery);\n this.transports.delete(key);\n this.transports.set(key, { delivery: options.delivery, transport });\n while (this.transports.size > MAX_CACHED_TRANSPORTS) {\n this.transports.delete(this.transports.keys().next().value as string);\n }\n return transport;\n }\n}\n\nfunction lowerCase(headers: Record<string, string> | undefined): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [name, value] of Object.entries(headers ?? {})) out[name.toLowerCase()] = value;\n return out;\n}\n\n/**\n * Whether two `delivery` option objects configure the same transport: the same object, or the\n * same keys with identical values (arrays compared element by element), so a resolver returning a\n * fresh literal with the same settings reuses the transport.\n */\nfunction sameDelivery(\n a: HttpWebhookTransportOptions | undefined,\n b: HttpWebhookTransportOptions | undefined,\n): boolean {\n if (a === b) return true;\n if (!a || !b) return false;\n const keys = Object.keys(a) as (keyof HttpWebhookTransportOptions)[];\n if (keys.length !== Object.keys(b).length) return false;\n return keys.every((key) => {\n const x = a[key];\n const y = b[key];\n if (Array.isArray(x) && Array.isArray(y)) {\n return x.length === y.length && x.every((item, i) => Object.is(item, y[i]));\n }\n return Object.is(x, y);\n });\n}\n\n/**\n * The `webhook-id` for an idempotency key. `webhook-id` allows letters, digits, `_` and `-`, at\n * most 128 characters: a key that fits is kept readable (`msg_<key>`); any other is hashed\n * (`msgh_<sha256 hex>`), so distinct keys never share an id. The two prefixes differ, so a kept key\n * can't collide with a hashed one.\n */\nexport function webhookId(key: string): string {\n const kept = `msg_${key}`;\n if (/^[A-Za-z0-9_-]+$/.test(key) && kept.length <= 128) return kept;\n return `msgh_${createHash('sha256').update(key).digest('hex')}`;\n}\n","import { createHmac } from 'node:crypto';\n\n/**\n * Standard Webhooks headers (`webhook-id`, `webhook-timestamp`, `webhook-signature: v1,…`) for\n * `body`, verifiable by a `@nestjs/webhooks` receiver with `scheme: 'standard'` (`@VerifyWebhook`).\n * `secret` is `whsec_` + base64 (the prefix is optional).\n */\nexport function standardWebhookHeaders(\n secret: string,\n id: string,\n body: string,\n now: Date = new Date(),\n): Record<string, string> {\n const timestamp = Math.floor(now.getTime() / 1000);\n const encoded = secret.startsWith('whsec_') ? secret.slice('whsec_'.length) : secret;\n const key = Buffer.from(encoded, 'base64');\n if (key.length < 24 || key.length > 64) {\n throw new TypeError(\n 'A Standard Webhooks secret is `whsec_` followed by base64 of 24 to 64 random bytes.',\n );\n }\n const signature = createHmac('sha256', key)\n .update(`${id}.${timestamp}.`)\n .update(body)\n .digest('base64');\n return {\n 'webhook-id': id,\n 'webhook-timestamp': String(timestamp),\n 'webhook-signature': `v1,${signature}`,\n };\n}\n\n/** The legacy `sha256=<hex HMAC of the body>` signature (GitHub-style). */\nexport function sha256Signature(secret: string, body: string): string {\n return `sha256=${createHmac('sha256', secret).update(body).digest('hex')}`;\n}\n","/** DI token for the resolved {@link WebhookChannelOptions}. */\nexport const WEBHOOK_OPTIONS = Symbol('WEBHOOK_OPTIONS');\n\n/** DI token for the optional per-tenant {@link WebhookChannelOptions} resolver. */\nexport const WEBHOOK_OPTIONS_RESOLVER = Symbol('WEBHOOK_OPTIONS_RESOLVER');\n\n/** DI token for the transaction opener used by subscription delivery. */\nexport const WEBHOOK_TRANSACTION = Symbol('WEBHOOK_TRANSACTION');\n","import { defineChannelModule } from '@dudousxd/nestjs-notifications-core';\nimport { type DynamicModule, Module } from '@nestjs/common';\nimport { WEBHOOK_OPTIONS, WEBHOOK_OPTIONS_RESOLVER, WEBHOOK_TRANSACTION } from './tokens';\nimport {\n WebhookChannel,\n type WebhookChannelOptions,\n type WebhookOptionsResolver,\n type WebhookTransactionRunner,\n} from './webhook.channel';\n\nexport interface WebhookChannelModuleOptions extends WebhookChannelOptions {\n /**\n * Optional per-tenant options resolver. When a notification is delivered with a\n * `context.tenant`, the returned options are used instead of the defaults.\n */\n resolveOptions?: WebhookOptionsResolver;\n /**\n * Opens a transaction for subscription delivery ({@link WebhookEvent}): `@nestjs/webhooks`\n * writes the event to the outbox through it, e.g. `(work) => db.transaction(work)`.\n */\n transaction?: WebhookTransactionRunner;\n /** Register globally so the channel is discoverable app-wide. Default true. */\n global?: boolean;\n}\n\n/**\n * Registers the webhook channel.\n *\n * ```ts\n * WebhookChannelModule.forRoot({ url: 'https://example.com/hooks', secret: 'whsec_…', signing: 'standard' });\n * ```\n */\n@Module({})\nexport class WebhookChannelModule {\n static forRoot(options: WebhookChannelModuleOptions = {}): DynamicModule {\n const { resolveOptions, transaction, global, ...channelOptions } = options;\n const module = defineChannelModule({\n module: WebhookChannelModule,\n channel: WebhookChannel,\n optionsToken: WEBHOOK_OPTIONS,\n options: channelOptions,\n resolver: { token: WEBHOOK_OPTIONS_RESOLVER, value: resolveOptions },\n ...(global !== undefined ? { global } : {}),\n });\n return {\n ...module,\n providers: [\n ...(module.providers ?? []),\n { provide: WEBHOOK_TRANSACTION, useValue: transaction ?? null },\n ],\n };\n }\n}\n"],"mappings":"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","names":["WebhookMessage","_url","_headers","_payload","url","u","payload","obj","header","key","value","headers","urlValue","headerValues","payloadValue","toRequest","body","WebhookEvent","type","data","createHash","randomUUID","BaseChannel","createChannel","routeFor","Inject","Injectable","Optional","OutboxInbox","HttpWebhookTransport","Webhooks","createHmac","standardWebhookHeaders","secret","id","body","now","Date","timestamp","Math","floor","getTime","encoded","startsWith","slice","length","key","Buffer","from","TypeError","signature","createHmac","update","digest","String","sha256Signature","WEBHOOK_OPTIONS","Symbol","WEBHOOK_OPTIONS_RESOLVER","WEBHOOK_TRANSACTION","DEFAULT_SIGNATURE_HEADER","DEFAULT_TIMEOUT_MS","MAX_CACHED_TRANSPORTS","WEBHOOK_DISPATCH_CONSUMER","Webhook","createChannel","WebhookChannel","BaseChannel","channel","transports","Map","options","resolveOptions","transaction","webhooks","inbox","send","notifiable","notification","context","result","buildPayload","WebhookEvent","dispatch","forTenant","message","WebhookMessage","payload","request","toRequest","route","routeFor","url","undefined","Error","body","JSON","stringify","id","idempotencyKey","randomUUID","replaceAll","headers","lowerCase","secret","signing","Object","assign","standardWebhookHeaders","signatureHeader","toLowerCase","sha256Signature","response","transportFor","tenant","endpointId","deliveryId","messageId","type","signal","AbortSignal","timeout","timeoutMs","attempt","statusCode","event","webhookId","outcome","tx","data","duplicate","processInTransaction","notify","transport","key","cached","get","sameDelivery","delivery","HttpWebhookTransport","delete","set","size","keys","next","value","out","name","entries","a","b","length","every","x","y","Array","isArray","item","i","is","kept","test","createHash","update","digest","defineChannelModule","Module","WebhookChannelModule","forRoot","options","resolveOptions","transaction","global","channelOptions","module","defineChannelModule","channel","WebhookChannel","optionsToken","WEBHOOK_OPTIONS","resolver","token","WEBHOOK_OPTIONS_RESOLVER","value","undefined","providers","provide","WEBHOOK_TRANSACTION","useValue"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dudousxd/nestjs-notifications-webhook",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Webhook channel for nestjs-notifications —
|
|
3
|
+
"version": "0.7.1",
|
|
4
|
+
"description": "Webhook channel for nestjs-notifications — direct signed POSTs over @nestjs/webhooks' SSRF-guarded transport, or subscription delivery through @nestjs/webhooks",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Davide Carvalho",
|
|
7
7
|
"repository": {
|
|
@@ -26,13 +26,21 @@
|
|
|
26
26
|
],
|
|
27
27
|
"peerDependencies": {
|
|
28
28
|
"@dudousxd/nestjs-notifications-core": ">=0.1.0 <1.0.0",
|
|
29
|
-
"@nestjs/common": "^
|
|
29
|
+
"@nestjs/common": "^12.0.0",
|
|
30
|
+
"@nestjs/webhooks": ">=0.0.4 <0.1.0",
|
|
31
|
+
"@nestjs/outbox": ">=0.1.4 <0.2.0"
|
|
30
32
|
},
|
|
31
33
|
"devDependencies": {
|
|
32
|
-
"@nestjs/common": "12.
|
|
34
|
+
"@nestjs/common": "12.3.2",
|
|
33
35
|
"reflect-metadata": "0.2.2",
|
|
34
36
|
"typescript": "5.9.3",
|
|
35
|
-
"@
|
|
37
|
+
"@nestjs/webhooks": "0.0.4",
|
|
38
|
+
"@nestjs/outbox": "0.1.4",
|
|
39
|
+
"@nestjs/core": "12.3.2",
|
|
40
|
+
"@nestjs/testing": "12.3.2",
|
|
41
|
+
"@nestjs/event-emitter": "12.0.1",
|
|
42
|
+
"rxjs": "7.8.2",
|
|
43
|
+
"@dudousxd/nestjs-notifications-core": "^0.11.1"
|
|
36
44
|
},
|
|
37
45
|
"module": "./dist/index.js",
|
|
38
46
|
"exports": {
|
|
@@ -47,6 +55,11 @@
|
|
|
47
55
|
}
|
|
48
56
|
}
|
|
49
57
|
},
|
|
58
|
+
"peerDependenciesMeta": {
|
|
59
|
+
"@nestjs/outbox": {
|
|
60
|
+
"optional": true
|
|
61
|
+
}
|
|
62
|
+
},
|
|
50
63
|
"scripts": {
|
|
51
64
|
"build": "tsup",
|
|
52
65
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|