@dudousxd/nestjs-notifications-webhook 0.6.0 → 0.7.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/dist/index.cjs +161 -57
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +107 -48
- package/dist/index.d.ts +107 -48
- package/dist/index.js +156 -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 { 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,8 @@ 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 WEBHOOK_DISPATCH_CONSUMER = "aviary.notifications.webhook-dispatch";
|
|
103
131
|
var Webhook = createChannel("webhook");
|
|
104
132
|
var WebhookChannel = class extends BaseChannel {
|
|
105
133
|
static {
|
|
@@ -107,13 +135,18 @@ var WebhookChannel = class extends BaseChannel {
|
|
|
107
135
|
}
|
|
108
136
|
options;
|
|
109
137
|
resolveOptions;
|
|
138
|
+
transaction;
|
|
139
|
+
webhooks;
|
|
140
|
+
inbox;
|
|
110
141
|
channel = "webhook";
|
|
111
|
-
|
|
112
|
-
|
|
142
|
+
transports = /* @__PURE__ */ new WeakMap();
|
|
143
|
+
constructor(options, resolveOptions, transaction, webhooks, inbox) {
|
|
144
|
+
super(), this.options = options, this.resolveOptions = resolveOptions, this.transaction = transaction, this.webhooks = webhooks, this.inbox = inbox;
|
|
113
145
|
}
|
|
114
146
|
async send(notifiable, notification, context) {
|
|
115
|
-
const options = this.forTenant(this.options, context, this.resolveOptions);
|
|
116
147
|
const result = this.buildPayload(notification, notifiable, "toWebhook", context);
|
|
148
|
+
if (result instanceof WebhookEvent) return this.dispatch(result, context);
|
|
149
|
+
const options = this.forTenant(this.options, context, this.resolveOptions);
|
|
117
150
|
const message = result instanceof WebhookMessage ? result : new WebhookMessage().payload(result);
|
|
118
151
|
const request = message.toRequest();
|
|
119
152
|
const route = routeFor(notifiable, "webhook", notification);
|
|
@@ -121,25 +154,80 @@ var WebhookChannel = class extends BaseChannel {
|
|
|
121
154
|
if (!url) {
|
|
122
155
|
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
156
|
}
|
|
157
|
+
const body = JSON.stringify(request.body);
|
|
158
|
+
const id = context?.idempotencyKey ?? `msg_${randomUUID().replaceAll("-", "")}`;
|
|
124
159
|
const headers = {
|
|
125
|
-
|
|
126
|
-
...
|
|
160
|
+
"content-type": "application/json",
|
|
161
|
+
...lowerCase(options.headers),
|
|
162
|
+
...lowerCase(request.headers)
|
|
127
163
|
};
|
|
128
164
|
if (options.secret) {
|
|
129
|
-
|
|
130
|
-
|
|
165
|
+
if (options.signing === "standard") {
|
|
166
|
+
Object.assign(headers, standardWebhookHeaders(options.secret, id, body));
|
|
167
|
+
} else {
|
|
168
|
+
headers[(options.signatureHeader ?? DEFAULT_SIGNATURE_HEADER).toLowerCase()] = sha256Signature(options.secret, body);
|
|
169
|
+
}
|
|
131
170
|
}
|
|
132
|
-
await
|
|
133
|
-
|
|
134
|
-
method: request.method,
|
|
171
|
+
const response = await this.transportFor(options).send({
|
|
172
|
+
url,
|
|
135
173
|
headers,
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
174
|
+
body,
|
|
175
|
+
endpointId: "aviary",
|
|
176
|
+
deliveryId: id,
|
|
177
|
+
messageId: id,
|
|
178
|
+
type: "notification"
|
|
179
|
+
}, {
|
|
180
|
+
signal: AbortSignal.timeout(options.timeoutMs ?? DEFAULT_TIMEOUT_MS),
|
|
181
|
+
attempt: 1
|
|
142
182
|
});
|
|
183
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
184
|
+
throw new Error(`Webhook request to ${url} failed with status ${response.statusCode}.`);
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
id,
|
|
188
|
+
statusCode: response.statusCode
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
async dispatch(event, context) {
|
|
192
|
+
const webhooks = this.webhooks;
|
|
193
|
+
if (!webhooks) {
|
|
194
|
+
throw new Error("A WebhookEvent needs WebhooksModule (and OutboxModule) from @nestjs/webhooks.");
|
|
195
|
+
}
|
|
196
|
+
if (!this.transaction) {
|
|
197
|
+
throw new Error("A WebhookEvent needs WebhookChannelModule.forRoot({ transaction: (work) => db.transaction(work) }): @nestjs/webhooks writes the event through a transaction.");
|
|
198
|
+
}
|
|
199
|
+
const id = context?.idempotencyKey ? webhookId(context.idempotencyKey) : void 0;
|
|
200
|
+
const inbox = this.inbox;
|
|
201
|
+
const outcome = await this.transaction(async (tx) => {
|
|
202
|
+
const dispatch = /* @__PURE__ */ __name(() => webhooks.dispatch(tx, {
|
|
203
|
+
type: event.type,
|
|
204
|
+
data: event.data,
|
|
205
|
+
tenant: context?.tenant ?? null,
|
|
206
|
+
...id ? {
|
|
207
|
+
id
|
|
208
|
+
} : {}
|
|
209
|
+
}), "dispatch");
|
|
210
|
+
if (!id || !inbox) return {
|
|
211
|
+
duplicate: false,
|
|
212
|
+
result: await dispatch()
|
|
213
|
+
};
|
|
214
|
+
return inbox.processInTransaction(tx, WEBHOOK_DISPATCH_CONSUMER, id, dispatch);
|
|
215
|
+
});
|
|
216
|
+
if (outcome.duplicate) return {
|
|
217
|
+
id,
|
|
218
|
+
duplicate: true
|
|
219
|
+
};
|
|
220
|
+
webhooks.notify();
|
|
221
|
+
return outcome.result;
|
|
222
|
+
}
|
|
223
|
+
transportFor(options) {
|
|
224
|
+
if (options.transport) return options.transport;
|
|
225
|
+
let transport = this.transports.get(options);
|
|
226
|
+
if (!transport) {
|
|
227
|
+
transport = new HttpWebhookTransport(options.delivery);
|
|
228
|
+
this.transports.set(options, transport);
|
|
229
|
+
}
|
|
230
|
+
return transport;
|
|
143
231
|
}
|
|
144
232
|
};
|
|
145
233
|
WebhookChannel = _ts_decorate([
|
|
@@ -147,12 +235,29 @@ WebhookChannel = _ts_decorate([
|
|
|
147
235
|
_ts_param(0, Inject(WEBHOOK_OPTIONS)),
|
|
148
236
|
_ts_param(1, Optional()),
|
|
149
237
|
_ts_param(1, Inject(WEBHOOK_OPTIONS_RESOLVER)),
|
|
238
|
+
_ts_param(2, Optional()),
|
|
239
|
+
_ts_param(2, Inject(WEBHOOK_TRANSACTION)),
|
|
240
|
+
_ts_param(3, Optional()),
|
|
241
|
+
_ts_param(4, Optional()),
|
|
150
242
|
_ts_metadata("design:type", Function),
|
|
151
243
|
_ts_metadata("design:paramtypes", [
|
|
152
244
|
typeof WebhookChannelOptions === "undefined" ? Object : WebhookChannelOptions,
|
|
153
|
-
typeof WebhookOptionsResolver === "undefined" ? Object : WebhookOptionsResolver
|
|
245
|
+
typeof WebhookOptionsResolver === "undefined" ? Object : WebhookOptionsResolver,
|
|
246
|
+
typeof WebhookTransactionRunner === "undefined" ? Object : WebhookTransactionRunner,
|
|
247
|
+
typeof Webhooks === "undefined" ? Object : Webhooks,
|
|
248
|
+
typeof OutboxInbox === "undefined" ? Object : OutboxInbox
|
|
154
249
|
])
|
|
155
250
|
], WebhookChannel);
|
|
251
|
+
function lowerCase(headers) {
|
|
252
|
+
const out = {};
|
|
253
|
+
for (const [name, value] of Object.entries(headers ?? {})) out[name.toLowerCase()] = value;
|
|
254
|
+
return out;
|
|
255
|
+
}
|
|
256
|
+
__name(lowerCase, "lowerCase");
|
|
257
|
+
function webhookId(key) {
|
|
258
|
+
return `msg_${key.replace(/[^A-Za-z0-9_-]/g, "_")}`.slice(0, 128);
|
|
259
|
+
}
|
|
260
|
+
__name(webhookId, "webhookId");
|
|
156
261
|
|
|
157
262
|
// src/webhook.module.ts
|
|
158
263
|
import { defineChannelModule } from "@dudousxd/nestjs-notifications-core";
|
|
@@ -176,52 +281,46 @@ var WebhookChannelModule = class _WebhookChannelModule {
|
|
|
176
281
|
__name(this, "WebhookChannelModule");
|
|
177
282
|
}
|
|
178
283
|
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({
|
|
284
|
+
const { resolveOptions, transaction, global, ...channelOptions } = options;
|
|
285
|
+
const module = defineChannelModule({
|
|
202
286
|
module: _WebhookChannelModule,
|
|
203
287
|
channel: WebhookChannel,
|
|
204
288
|
optionsToken: WEBHOOK_OPTIONS,
|
|
205
|
-
options:
|
|
289
|
+
options: channelOptions,
|
|
206
290
|
resolver: {
|
|
207
291
|
token: WEBHOOK_OPTIONS_RESOLVER,
|
|
208
|
-
value:
|
|
292
|
+
value: resolveOptions
|
|
209
293
|
},
|
|
210
|
-
...
|
|
211
|
-
global
|
|
294
|
+
...global !== void 0 ? {
|
|
295
|
+
global
|
|
212
296
|
} : {}
|
|
213
297
|
});
|
|
298
|
+
return {
|
|
299
|
+
...module,
|
|
300
|
+
providers: [
|
|
301
|
+
...module.providers ?? [],
|
|
302
|
+
{
|
|
303
|
+
provide: WEBHOOK_TRANSACTION,
|
|
304
|
+
useValue: transaction ?? null
|
|
305
|
+
}
|
|
306
|
+
]
|
|
307
|
+
};
|
|
214
308
|
}
|
|
215
309
|
};
|
|
216
310
|
WebhookChannelModule = _ts_decorate2([
|
|
217
311
|
Module({})
|
|
218
312
|
], WebhookChannelModule);
|
|
219
313
|
export {
|
|
314
|
+
WEBHOOK_DISPATCH_CONSUMER,
|
|
220
315
|
WEBHOOK_OPTIONS,
|
|
221
316
|
WEBHOOK_OPTIONS_RESOLVER,
|
|
317
|
+
WEBHOOK_TRANSACTION,
|
|
222
318
|
Webhook,
|
|
223
319
|
WebhookChannel,
|
|
224
320
|
WebhookChannelModule,
|
|
225
|
-
|
|
321
|
+
WebhookEvent,
|
|
322
|
+
WebhookMessage,
|
|
323
|
+
sha256Signature,
|
|
324
|
+
standardWebhookHeaders
|
|
226
325
|
};
|
|
227
326
|
//# 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 { 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\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 private readonly transports = new WeakMap<WebhookChannelOptions, WebhookTransport>();\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).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(options: WebhookChannelOptions): WebhookTransport {\n if (options.transport) return options.transport;\n let transport = this.transports.get(options);\n if (!transport) {\n transport = new HttpWebhookTransport(options.delivery);\n this.transports.set(options, transport);\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/** `webhook-id` allows letters, digits, `_` and `-`, at most 128. */\nfunction webhookId(key: string): string {\n return `msg_${key.replace(/[^A-Za-z0-9_-]/g, '_')}`.slice(0, 128);\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","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","WEBHOOK_DISPATCH_CONSUMER","Webhook","createChannel","WebhookChannel","BaseChannel","channel","transports","WeakMap","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","endpointId","deliveryId","messageId","type","signal","AbortSignal","timeout","timeoutMs","attempt","statusCode","event","webhookId","outcome","tx","data","tenant","duplicate","processInTransaction","notify","transport","get","HttpWebhookTransport","delivery","set","out","name","value","entries","key","replace","slice","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.0",
|
|
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.0"
|
|
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"
|