@dudousxd/nestjs-notifications-webhook 0.5.2 → 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 CHANGED
@@ -21,12 +21,17 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  // src/index.ts
22
22
  var index_exports = {};
23
23
  __export(index_exports, {
24
+ WEBHOOK_DISPATCH_CONSUMER: () => WEBHOOK_DISPATCH_CONSUMER,
24
25
  WEBHOOK_OPTIONS: () => WEBHOOK_OPTIONS,
25
26
  WEBHOOK_OPTIONS_RESOLVER: () => WEBHOOK_OPTIONS_RESOLVER,
27
+ WEBHOOK_TRANSACTION: () => WEBHOOK_TRANSACTION,
26
28
  Webhook: () => Webhook,
27
29
  WebhookChannel: () => WebhookChannel,
28
30
  WebhookChannelModule: () => WebhookChannelModule,
29
- WebhookMessage: () => WebhookMessage
31
+ WebhookEvent: () => WebhookEvent,
32
+ WebhookMessage: () => WebhookMessage,
33
+ sha256Signature: () => sha256Signature,
34
+ standardWebhookHeaders: () => standardWebhookHeaders
30
35
  });
31
36
  module.exports = __toCommonJS(index_exports);
32
37
 
@@ -36,7 +41,6 @@ var WebhookMessage = class {
36
41
  __name(this, "WebhookMessage");
37
42
  }
38
43
  _url;
39
- _method = "POST";
40
44
  _headers = {};
41
45
  _payload = {};
42
46
  /** Set the target URL (overrides the route and configured default). */
@@ -62,17 +66,9 @@ var WebhookMessage = class {
62
66
  };
63
67
  return this;
64
68
  }
65
- /** Set the HTTP method (default `POST`). */
66
- method(m) {
67
- this._method = m;
68
- return this;
69
- }
70
69
  get urlValue() {
71
70
  return this._url;
72
71
  }
73
- get methodValue() {
74
- return this._method;
75
- }
76
72
  get headerValues() {
77
73
  return this._headers;
78
74
  }
@@ -83,7 +79,6 @@ var WebhookMessage = class {
83
79
  toRequest() {
84
80
  return {
85
81
  url: this._url,
86
- method: this._method,
87
82
  headers: {
88
83
  ...this._headers
89
84
  },
@@ -91,26 +86,71 @@ var WebhookMessage = class {
91
86
  };
92
87
  }
93
88
  };
89
+ var WebhookEvent = class {
90
+ static {
91
+ __name(this, "WebhookEvent");
92
+ }
93
+ type;
94
+ data;
95
+ constructor(type, data) {
96
+ this.type = type;
97
+ this.data = data;
98
+ }
99
+ };
94
100
 
95
101
  // src/webhook.channel.ts
96
- var import_node_crypto = require("crypto");
102
+ var import_node_crypto2 = require("crypto");
97
103
  var import_nestjs_notifications_core = require("@dudousxd/nestjs-notifications-core");
98
104
  var import_common = require("@nestjs/common");
105
+ var import_outbox = require("@nestjs/outbox");
106
+ var import_webhooks = require("@nestjs/webhooks");
107
+
108
+ // src/signing.ts
109
+ var import_node_crypto = require("crypto");
110
+ function standardWebhookHeaders(secret, id, body, now = /* @__PURE__ */ new Date()) {
111
+ const timestamp = Math.floor(now.getTime() / 1e3);
112
+ const encoded = secret.startsWith("whsec_") ? secret.slice("whsec_".length) : secret;
113
+ const key = Buffer.from(encoded, "base64");
114
+ if (key.length < 24 || key.length > 64) {
115
+ throw new TypeError("A Standard Webhooks secret is `whsec_` followed by base64 of 24 to 64 random bytes.");
116
+ }
117
+ const signature = (0, import_node_crypto.createHmac)("sha256", key).update(`${id}.${timestamp}.`).update(body).digest("base64");
118
+ return {
119
+ "webhook-id": id,
120
+ "webhook-timestamp": String(timestamp),
121
+ "webhook-signature": `v1,${signature}`
122
+ };
123
+ }
124
+ __name(standardWebhookHeaders, "standardWebhookHeaders");
125
+ function sha256Signature(secret, body) {
126
+ return `sha256=${(0, import_node_crypto.createHmac)("sha256", secret).update(body).digest("hex")}`;
127
+ }
128
+ __name(sha256Signature, "sha256Signature");
99
129
 
100
130
  // src/tokens.ts
101
131
  var WEBHOOK_OPTIONS = /* @__PURE__ */ Symbol("WEBHOOK_OPTIONS");
102
132
  var WEBHOOK_OPTIONS_RESOLVER = /* @__PURE__ */ Symbol("WEBHOOK_OPTIONS_RESOLVER");
133
+ var WEBHOOK_TRANSACTION = /* @__PURE__ */ Symbol("WEBHOOK_TRANSACTION");
103
134
 
104
135
  // src/webhook.channel.ts
105
136
  function _ts_decorate(decorators, target, key, desc) {
106
137
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
107
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
108
- 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;
138
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
139
+ r = Reflect.decorate(decorators, target, key, desc);
140
+ } else {
141
+ for (var i = decorators.length - 1; i >= 0; i--) {
142
+ if (d = decorators[i]) {
143
+ r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
144
+ }
145
+ }
146
+ }
109
147
  return c > 3 && r && Object.defineProperty(target, key, r), r;
110
148
  }
111
149
  __name(_ts_decorate, "_ts_decorate");
112
- function _ts_metadata(k, v) {
113
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
150
+ function _ts_metadata(metadataKey, metadataValue) {
151
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") {
152
+ return Reflect.metadata(metadataKey, metadataValue);
153
+ }
114
154
  }
115
155
  __name(_ts_metadata, "_ts_metadata");
116
156
  function _ts_param(paramIndex, decorator) {
@@ -120,6 +160,8 @@ function _ts_param(paramIndex, decorator) {
120
160
  }
121
161
  __name(_ts_param, "_ts_param");
122
162
  var DEFAULT_SIGNATURE_HEADER = "X-Signature-256";
163
+ var DEFAULT_TIMEOUT_MS = 15e3;
164
+ var WEBHOOK_DISPATCH_CONSUMER = "aviary.notifications.webhook-dispatch";
123
165
  var Webhook = (0, import_nestjs_notifications_core.createChannel)("webhook");
124
166
  var WebhookChannel = class extends import_nestjs_notifications_core.BaseChannel {
125
167
  static {
@@ -127,13 +169,18 @@ var WebhookChannel = class extends import_nestjs_notifications_core.BaseChannel
127
169
  }
128
170
  options;
129
171
  resolveOptions;
172
+ transaction;
173
+ webhooks;
174
+ inbox;
130
175
  channel = "webhook";
131
- constructor(options, resolveOptions) {
132
- super(), this.options = options, this.resolveOptions = resolveOptions;
176
+ transports = /* @__PURE__ */ new WeakMap();
177
+ constructor(options, resolveOptions, transaction, webhooks, inbox) {
178
+ super(), this.options = options, this.resolveOptions = resolveOptions, this.transaction = transaction, this.webhooks = webhooks, this.inbox = inbox;
133
179
  }
134
180
  async send(notifiable, notification, context) {
135
- const options = this.forTenant(this.options, context, this.resolveOptions);
136
181
  const result = this.buildPayload(notification, notifiable, "toWebhook", context);
182
+ if (result instanceof WebhookEvent) return this.dispatch(result, context);
183
+ const options = this.forTenant(this.options, context, this.resolveOptions);
137
184
  const message = result instanceof WebhookMessage ? result : new WebhookMessage().payload(result);
138
185
  const request = message.toRequest();
139
186
  const route = (0, import_nestjs_notifications_core.routeFor)(notifiable, "webhook", notification);
@@ -141,19 +188,80 @@ var WebhookChannel = class extends import_nestjs_notifications_core.BaseChannel
141
188
  if (!url) {
142
189
  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().');
143
190
  }
191
+ const body = JSON.stringify(request.body);
192
+ const id = context?.idempotencyKey ?? `msg_${(0, import_node_crypto2.randomUUID)().replaceAll("-", "")}`;
144
193
  const headers = {
145
- ...options.headers,
146
- ...request.headers
194
+ "content-type": "application/json",
195
+ ...lowerCase(options.headers),
196
+ ...lowerCase(request.headers)
147
197
  };
148
198
  if (options.secret) {
149
- const signature = (0, import_node_crypto.createHmac)("sha256", options.secret).update(JSON.stringify(request.body)).digest("hex");
150
- headers[options.signatureHeader ?? DEFAULT_SIGNATURE_HEADER] = `sha256=${signature}`;
199
+ if (options.signing === "standard") {
200
+ Object.assign(headers, standardWebhookHeaders(options.secret, id, body));
201
+ } else {
202
+ headers[(options.signatureHeader ?? DEFAULT_SIGNATURE_HEADER).toLowerCase()] = sha256Signature(options.secret, body);
203
+ }
151
204
  }
152
- await (0, import_nestjs_notifications_core.postJson)(url, request.body, {
153
- label: "Webhook",
154
- method: request.method,
155
- headers
205
+ const response = await this.transportFor(options).send({
206
+ url,
207
+ headers,
208
+ body,
209
+ endpointId: "aviary",
210
+ deliveryId: id,
211
+ messageId: id,
212
+ type: "notification"
213
+ }, {
214
+ signal: AbortSignal.timeout(options.timeoutMs ?? DEFAULT_TIMEOUT_MS),
215
+ attempt: 1
156
216
  });
217
+ if (response.statusCode < 200 || response.statusCode >= 300) {
218
+ throw new Error(`Webhook request to ${url} failed with status ${response.statusCode}.`);
219
+ }
220
+ return {
221
+ id,
222
+ statusCode: response.statusCode
223
+ };
224
+ }
225
+ async dispatch(event, context) {
226
+ const webhooks = this.webhooks;
227
+ if (!webhooks) {
228
+ throw new Error("A WebhookEvent needs WebhooksModule (and OutboxModule) from @nestjs/webhooks.");
229
+ }
230
+ if (!this.transaction) {
231
+ throw new Error("A WebhookEvent needs WebhookChannelModule.forRoot({ transaction: (work) => db.transaction(work) }): @nestjs/webhooks writes the event through a transaction.");
232
+ }
233
+ const id = context?.idempotencyKey ? webhookId(context.idempotencyKey) : void 0;
234
+ const inbox = this.inbox;
235
+ const outcome = await this.transaction(async (tx) => {
236
+ const dispatch = /* @__PURE__ */ __name(() => webhooks.dispatch(tx, {
237
+ type: event.type,
238
+ data: event.data,
239
+ tenant: context?.tenant ?? null,
240
+ ...id ? {
241
+ id
242
+ } : {}
243
+ }), "dispatch");
244
+ if (!id || !inbox) return {
245
+ duplicate: false,
246
+ result: await dispatch()
247
+ };
248
+ return inbox.processInTransaction(tx, WEBHOOK_DISPATCH_CONSUMER, id, dispatch);
249
+ });
250
+ if (outcome.duplicate) return {
251
+ id,
252
+ duplicate: true
253
+ };
254
+ webhooks.notify();
255
+ return outcome.result;
256
+ }
257
+ transportFor(options) {
258
+ if (options.transport) return options.transport;
259
+ let transport = this.transports.get(options);
260
+ if (!transport) {
261
+ transport = new import_webhooks.HttpWebhookTransport(options.delivery);
262
+ this.transports.set(options, transport);
263
+ }
264
+ return transport;
157
265
  }
158
266
  };
159
267
  WebhookChannel = _ts_decorate([
@@ -161,20 +269,44 @@ WebhookChannel = _ts_decorate([
161
269
  _ts_param(0, (0, import_common.Inject)(WEBHOOK_OPTIONS)),
162
270
  _ts_param(1, (0, import_common.Optional)()),
163
271
  _ts_param(1, (0, import_common.Inject)(WEBHOOK_OPTIONS_RESOLVER)),
272
+ _ts_param(2, (0, import_common.Optional)()),
273
+ _ts_param(2, (0, import_common.Inject)(WEBHOOK_TRANSACTION)),
274
+ _ts_param(3, (0, import_common.Optional)()),
275
+ _ts_param(4, (0, import_common.Optional)()),
164
276
  _ts_metadata("design:type", Function),
165
277
  _ts_metadata("design:paramtypes", [
166
278
  typeof WebhookChannelOptions === "undefined" ? Object : WebhookChannelOptions,
167
- typeof WebhookOptionsResolver === "undefined" ? Object : WebhookOptionsResolver
279
+ typeof WebhookOptionsResolver === "undefined" ? Object : WebhookOptionsResolver,
280
+ typeof WebhookTransactionRunner === "undefined" ? Object : WebhookTransactionRunner,
281
+ typeof import_webhooks.Webhooks === "undefined" ? Object : import_webhooks.Webhooks,
282
+ typeof import_outbox.OutboxInbox === "undefined" ? Object : import_outbox.OutboxInbox
168
283
  ])
169
284
  ], WebhookChannel);
285
+ function lowerCase(headers) {
286
+ const out = {};
287
+ for (const [name, value] of Object.entries(headers ?? {})) out[name.toLowerCase()] = value;
288
+ return out;
289
+ }
290
+ __name(lowerCase, "lowerCase");
291
+ function webhookId(key) {
292
+ return `msg_${key.replace(/[^A-Za-z0-9_-]/g, "_")}`.slice(0, 128);
293
+ }
294
+ __name(webhookId, "webhookId");
170
295
 
171
296
  // src/webhook.module.ts
172
297
  var import_nestjs_notifications_core2 = require("@dudousxd/nestjs-notifications-core");
173
298
  var import_common2 = require("@nestjs/common");
174
299
  function _ts_decorate2(decorators, target, key, desc) {
175
300
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
176
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
177
- 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;
301
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
302
+ r = Reflect.decorate(decorators, target, key, desc);
303
+ } else {
304
+ for (var i = decorators.length - 1; i >= 0; i--) {
305
+ if (d = decorators[i]) {
306
+ r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
307
+ }
308
+ }
309
+ }
178
310
  return c > 3 && r && Object.defineProperty(target, key, r), r;
179
311
  }
180
312
  __name(_ts_decorate2, "_ts_decorate");
@@ -183,29 +315,30 @@ var WebhookChannelModule = class _WebhookChannelModule {
183
315
  __name(this, "WebhookChannelModule");
184
316
  }
185
317
  static forRoot(options = {}) {
186
- const webhookOptions = {
187
- // Set each field only when provided (exactOptionalPropertyTypes); the channel resolves a
188
- // per-notifiable URL and default headers when these are absent.
189
- ...options.url !== void 0 ? {
190
- url: options.url
191
- } : {},
192
- ...options.headers !== void 0 ? {
193
- headers: options.headers
194
- } : {}
195
- };
196
- return (0, import_nestjs_notifications_core2.defineChannelModule)({
318
+ const { resolveOptions, transaction, global, ...channelOptions } = options;
319
+ const module2 = (0, import_nestjs_notifications_core2.defineChannelModule)({
197
320
  module: _WebhookChannelModule,
198
321
  channel: WebhookChannel,
199
322
  optionsToken: WEBHOOK_OPTIONS,
200
- options: webhookOptions,
323
+ options: channelOptions,
201
324
  resolver: {
202
325
  token: WEBHOOK_OPTIONS_RESOLVER,
203
- value: options.resolveOptions
326
+ value: resolveOptions
204
327
  },
205
- ...options.global !== void 0 ? {
206
- global: options.global
328
+ ...global !== void 0 ? {
329
+ global
207
330
  } : {}
208
331
  });
332
+ return {
333
+ ...module2,
334
+ providers: [
335
+ ...module2.providers ?? [],
336
+ {
337
+ provide: WEBHOOK_TRANSACTION,
338
+ useValue: transaction ?? null
339
+ }
340
+ ]
341
+ };
209
342
  }
210
343
  };
211
344
  WebhookChannelModule = _ts_decorate2([
@@ -213,11 +346,16 @@ WebhookChannelModule = _ts_decorate2([
213
346
  ], WebhookChannelModule);
214
347
  // Annotate the CommonJS export names for ESM import in node:
215
348
  0 && (module.exports = {
349
+ WEBHOOK_DISPATCH_CONSUMER,
216
350
  WEBHOOK_OPTIONS,
217
351
  WEBHOOK_OPTIONS_RESOLVER,
352
+ WEBHOOK_TRANSACTION,
218
353
  Webhook,
219
354
  WebhookChannel,
220
355
  WebhookChannelModule,
221
- WebhookMessage
356
+ WebhookEvent,
357
+ WebhookMessage,
358
+ sha256Signature,
359
+ standardWebhookHeaders
222
360
  });
223
361
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/webhook-message.ts","../src/webhook.channel.ts","../src/tokens.ts","../src/webhook.module.ts"],"sourcesContent":["export {\n WebhookMessage,\n type WebhookMethod,\n type WebhookRequest,\n} from './webhook-message';\nexport {\n Webhook,\n WebhookChannel,\n type WebhookChannelOptions,\n type WebhookNotification,\n type WebhookOptionsResolver,\n} from './webhook.channel';\nexport {\n WebhookChannelModule,\n type WebhookChannelModuleOptions,\n} from './webhook.module';\nexport { WEBHOOK_OPTIONS, WEBHOOK_OPTIONS_RESOLVER } from './tokens';\n","/** 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}\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, { label: 'Webhook', method: request.method, headers });\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 * 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 };\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","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","import_nestjs_notifications_core","import_common","WebhookChannelModule","forRoot","options","webhookOptions","url","undefined","headers","defineChannelModule","module","channel","WebhookChannel","optionsToken","WEBHOOK_OPTIONS","resolver","token","WEBHOOK_OPTIONS_RESOLVER","value","resolveOptions","global"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/webhook-message.ts","../src/webhook.channel.ts","../src/signing.ts","../src/tokens.ts","../src/webhook.module.ts"],"sourcesContent":["export { WebhookMessage, WebhookEvent, type WebhookRequest } from './webhook-message';\nexport {\n WEBHOOK_DISPATCH_CONSUMER,\n Webhook,\n type WebhookAlreadyDispatched,\n WebhookChannel,\n type WebhookChannelOptions,\n type WebhookDeliveryResult,\n type WebhookNotification,\n type WebhookOptionsResolver,\n type WebhookTransactionRunner,\n} from './webhook.channel';\nexport { WebhookChannelModule, type WebhookChannelModuleOptions } from './webhook.module';\nexport { standardWebhookHeaders, sha256Signature } from './signing';\nexport { WEBHOOK_OPTIONS, WEBHOOK_OPTIONS_RESOLVER, WEBHOOK_TRANSACTION } from './tokens';\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 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","import_node_crypto","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","import_nestjs_notifications_core","import_common","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"]}