@dudousxd/nestjs-notifications-webhook 0.7.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 CHANGED
@@ -161,6 +161,7 @@ function _ts_param(paramIndex, decorator) {
161
161
  __name(_ts_param, "_ts_param");
162
162
  var DEFAULT_SIGNATURE_HEADER = "X-Signature-256";
163
163
  var DEFAULT_TIMEOUT_MS = 15e3;
164
+ var MAX_CACHED_TRANSPORTS = 1e3;
164
165
  var WEBHOOK_DISPATCH_CONSUMER = "aviary.notifications.webhook-dispatch";
165
166
  var Webhook = (0, import_nestjs_notifications_core.createChannel)("webhook");
166
167
  var WebhookChannel = class extends import_nestjs_notifications_core.BaseChannel {
@@ -173,7 +174,12 @@ var WebhookChannel = class extends import_nestjs_notifications_core.BaseChannel
173
174
  webhooks;
174
175
  inbox;
175
176
  channel = "webhook";
176
- transports = /* @__PURE__ */ new WeakMap();
177
+ /**
178
+ * `HttpWebhookTransport`s by tenant (`''` without one), rebuilt only when that tenant's
179
+ * `delivery` options change. Keyed by tenant rather than by the resolved options object, since a
180
+ * resolver may return a fresh object on every call.
181
+ */
182
+ transports = /* @__PURE__ */ new Map();
177
183
  constructor(options, resolveOptions, transaction, webhooks, inbox) {
178
184
  super(), this.options = options, this.resolveOptions = resolveOptions, this.transaction = transaction, this.webhooks = webhooks, this.inbox = inbox;
179
185
  }
@@ -202,7 +208,7 @@ var WebhookChannel = class extends import_nestjs_notifications_core.BaseChannel
202
208
  headers[(options.signatureHeader ?? DEFAULT_SIGNATURE_HEADER).toLowerCase()] = sha256Signature(options.secret, body);
203
209
  }
204
210
  }
205
- const response = await this.transportFor(options).send({
211
+ const response = await this.transportFor(options, context?.tenant).send({
206
212
  url,
207
213
  headers,
208
214
  body,
@@ -254,12 +260,19 @@ var WebhookChannel = class extends import_nestjs_notifications_core.BaseChannel
254
260
  webhooks.notify();
255
261
  return outcome.result;
256
262
  }
257
- transportFor(options) {
263
+ transportFor(options, tenant) {
258
264
  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);
265
+ const key = tenant && this.resolveOptions ? tenant : "";
266
+ const cached = this.transports.get(key);
267
+ if (cached && sameDelivery(cached.delivery, options.delivery)) return cached.transport;
268
+ const transport = new import_webhooks.HttpWebhookTransport(options.delivery);
269
+ this.transports.delete(key);
270
+ this.transports.set(key, {
271
+ delivery: options.delivery,
272
+ transport
273
+ });
274
+ while (this.transports.size > MAX_CACHED_TRANSPORTS) {
275
+ this.transports.delete(this.transports.keys().next().value);
263
276
  }
264
277
  return transport;
265
278
  }
@@ -288,8 +301,25 @@ function lowerCase(headers) {
288
301
  return out;
289
302
  }
290
303
  __name(lowerCase, "lowerCase");
304
+ function sameDelivery(a, b) {
305
+ if (a === b) return true;
306
+ if (!a || !b) return false;
307
+ const keys = Object.keys(a);
308
+ if (keys.length !== Object.keys(b).length) return false;
309
+ return keys.every((key) => {
310
+ const x = a[key];
311
+ const y = b[key];
312
+ if (Array.isArray(x) && Array.isArray(y)) {
313
+ return x.length === y.length && x.every((item, i) => Object.is(item, y[i]));
314
+ }
315
+ return Object.is(x, y);
316
+ });
317
+ }
318
+ __name(sameDelivery, "sameDelivery");
291
319
  function webhookId(key) {
292
- return `msg_${key.replace(/[^A-Za-z0-9_-]/g, "_")}`.slice(0, 128);
320
+ const kept = `msg_${key}`;
321
+ if (/^[A-Za-z0-9_-]+$/.test(key) && kept.length <= 128) return kept;
322
+ return `msgh_${(0, import_node_crypto2.createHash)("sha256").update(key).digest("hex")}`;
293
323
  }
294
324
  __name(webhookId, "webhookId");
295
325
 
@@ -1 +1 @@
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"]}
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 { 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","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","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","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"]}
package/dist/index.d.cts CHANGED
@@ -144,6 +144,11 @@ declare class WebhookChannel extends BaseChannel {
144
144
  private readonly webhooks?;
145
145
  private readonly inbox?;
146
146
  readonly channel = "webhook";
147
+ /**
148
+ * `HttpWebhookTransport`s by tenant (`''` without one), rebuilt only when that tenant's
149
+ * `delivery` options change. Keyed by tenant rather than by the resolved options object, since a
150
+ * resolver may return a fresh object on every call.
151
+ */
147
152
  private readonly transports;
148
153
  constructor(options: WebhookChannelOptions, resolveOptions?: WebhookOptionsResolver | undefined, transaction?: WebhookTransactionRunner | undefined, webhooks?: Webhooks | undefined, inbox?: OutboxInbox | undefined);
149
154
  send(notifiable: Notifiable, notification: Notification, context?: DeliveryContext): Promise<WebhookDeliveryResult | WebhookMessage$1 | WebhookAlreadyDispatched>;
package/dist/index.d.ts CHANGED
@@ -144,6 +144,11 @@ declare class WebhookChannel extends BaseChannel {
144
144
  private readonly webhooks?;
145
145
  private readonly inbox?;
146
146
  readonly channel = "webhook";
147
+ /**
148
+ * `HttpWebhookTransport`s by tenant (`''` without one), rebuilt only when that tenant's
149
+ * `delivery` options change. Keyed by tenant rather than by the resolved options object, since a
150
+ * resolver may return a fresh object on every call.
151
+ */
147
152
  private readonly transports;
148
153
  constructor(options: WebhookChannelOptions, resolveOptions?: WebhookOptionsResolver | undefined, transaction?: WebhookTransactionRunner | undefined, webhooks?: Webhooks | undefined, inbox?: OutboxInbox | undefined);
149
154
  send(notifiable: Notifiable, notification: Notification, context?: DeliveryContext): Promise<WebhookDeliveryResult | WebhookMessage$1 | WebhookAlreadyDispatched>;
package/dist/index.js CHANGED
@@ -65,7 +65,7 @@ var WebhookEvent = class {
65
65
  };
66
66
 
67
67
  // src/webhook.channel.ts
68
- import { randomUUID } from "crypto";
68
+ import { createHash, randomUUID } from "crypto";
69
69
  import { BaseChannel, createChannel, routeFor } from "@dudousxd/nestjs-notifications-core";
70
70
  import { Inject, Injectable, Optional } from "@nestjs/common";
71
71
  import { OutboxInbox } from "@nestjs/outbox";
@@ -127,6 +127,7 @@ function _ts_param(paramIndex, decorator) {
127
127
  __name(_ts_param, "_ts_param");
128
128
  var DEFAULT_SIGNATURE_HEADER = "X-Signature-256";
129
129
  var DEFAULT_TIMEOUT_MS = 15e3;
130
+ var MAX_CACHED_TRANSPORTS = 1e3;
130
131
  var WEBHOOK_DISPATCH_CONSUMER = "aviary.notifications.webhook-dispatch";
131
132
  var Webhook = createChannel("webhook");
132
133
  var WebhookChannel = class extends BaseChannel {
@@ -139,7 +140,12 @@ var WebhookChannel = class extends BaseChannel {
139
140
  webhooks;
140
141
  inbox;
141
142
  channel = "webhook";
142
- transports = /* @__PURE__ */ new WeakMap();
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();
143
149
  constructor(options, resolveOptions, transaction, webhooks, inbox) {
144
150
  super(), this.options = options, this.resolveOptions = resolveOptions, this.transaction = transaction, this.webhooks = webhooks, this.inbox = inbox;
145
151
  }
@@ -168,7 +174,7 @@ var WebhookChannel = class extends BaseChannel {
168
174
  headers[(options.signatureHeader ?? DEFAULT_SIGNATURE_HEADER).toLowerCase()] = sha256Signature(options.secret, body);
169
175
  }
170
176
  }
171
- const response = await this.transportFor(options).send({
177
+ const response = await this.transportFor(options, context?.tenant).send({
172
178
  url,
173
179
  headers,
174
180
  body,
@@ -220,12 +226,19 @@ var WebhookChannel = class extends BaseChannel {
220
226
  webhooks.notify();
221
227
  return outcome.result;
222
228
  }
223
- transportFor(options) {
229
+ transportFor(options, tenant) {
224
230
  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);
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);
229
242
  }
230
243
  return transport;
231
244
  }
@@ -254,8 +267,25 @@ function lowerCase(headers) {
254
267
  return out;
255
268
  }
256
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");
257
285
  function webhookId(key) {
258
- return `msg_${key.replace(/[^A-Za-z0-9_-]/g, "_")}`.slice(0, 128);
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")}`;
259
289
  }
260
290
  __name(webhookId, "webhookId");
261
291
 
package/dist/index.js.map CHANGED
@@ -1 +1 @@
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"]}
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@dudousxd/nestjs-notifications-webhook",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
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",
@@ -40,7 +40,7 @@
40
40
  "@nestjs/testing": "12.3.2",
41
41
  "@nestjs/event-emitter": "12.0.1",
42
42
  "rxjs": "7.8.2",
43
- "@dudousxd/nestjs-notifications-core": "^0.11.0"
43
+ "@dudousxd/nestjs-notifications-core": "^0.11.1"
44
44
  },
45
45
  "module": "./dist/index.js",
46
46
  "exports": {