@nestarc/webhook 0.13.0 → 0.13.2

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +441 -0
  2. package/README.md +384 -367
  3. package/SECURITY.md +27 -0
  4. package/dist/adapters/prisma-delivery.repository.d.ts +1 -1
  5. package/dist/adapters/prisma-delivery.repository.d.ts.map +1 -1
  6. package/dist/adapters/prisma-delivery.repository.js +79 -18
  7. package/dist/adapters/prisma-delivery.repository.js.map +1 -1
  8. package/dist/adapters/prisma-event.repository.d.ts +2 -2
  9. package/dist/adapters/prisma-event.repository.d.ts.map +1 -1
  10. package/dist/adapters/prisma-event.repository.js +6 -6
  11. package/dist/adapters/prisma-event.repository.js.map +1 -1
  12. package/dist/index.d.ts +1 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js.map +1 -1
  15. package/dist/interfaces/webhook-delivery.interface.d.ts +4 -0
  16. package/dist/interfaces/webhook-delivery.interface.d.ts.map +1 -1
  17. package/dist/interfaces/webhook-endpoint.interface.d.ts +2 -2
  18. package/dist/interfaces/webhook-endpoint.interface.d.ts.map +1 -1
  19. package/dist/interfaces/webhook-options.interface.d.ts +2 -1
  20. package/dist/interfaces/webhook-options.interface.d.ts.map +1 -1
  21. package/dist/ports/webhook-delivery.repository.d.ts +9 -1
  22. package/dist/ports/webhook-delivery.repository.d.ts.map +1 -1
  23. package/dist/ports/webhook-event.repository.d.ts +7 -3
  24. package/dist/ports/webhook-event.repository.d.ts.map +1 -1
  25. package/dist/webhook.delivery-admin.service.d.ts +3 -1
  26. package/dist/webhook.delivery-admin.service.d.ts.map +1 -1
  27. package/dist/webhook.delivery-admin.service.js +7 -3
  28. package/dist/webhook.delivery-admin.service.js.map +1 -1
  29. package/dist/webhook.service.d.ts +13 -0
  30. package/dist/webhook.service.d.ts.map +1 -1
  31. package/dist/webhook.service.js +14 -1
  32. package/dist/webhook.service.js.map +1 -1
  33. package/docs/usage.md +109 -0
  34. package/examples/quick-start/README.md +83 -0
  35. package/examples/quick-start/main.ts +247 -0
  36. package/examples/quick-start/package.json +29 -0
  37. package/examples/quick-start/prisma/schema.prisma +10 -0
  38. package/examples/quick-start/prisma.config.ts +6 -0
  39. package/examples/quick-start/tsconfig.json +15 -0
  40. package/llms.txt +12 -0
  41. package/package.json +32 -13
@@ -17,8 +17,10 @@ const common_1 = require("@nestjs/common");
17
17
  const webhook_constants_1 = require("./webhook.constants");
18
18
  let WebhookDeliveryAdminService = class WebhookDeliveryAdminService {
19
19
  deliveryRepo;
20
- constructor(deliveryRepo) {
20
+ options;
21
+ constructor(deliveryRepo, options) {
21
22
  this.deliveryRepo = deliveryRepo;
23
+ this.options = options;
22
24
  }
23
25
  async getDeliveryLogs(endpointId, filters) {
24
26
  return this.deliveryRepo.getDeliveryLogs(endpointId, filters);
@@ -39,13 +41,15 @@ let WebhookDeliveryAdminService = class WebhookDeliveryAdminService {
39
41
  if (!this.deliveryRepo.replayEvent) {
40
42
  throw new Error('WebhookDeliveryRepository does not support event replay');
41
43
  }
42
- return this.deliveryRepo.replayEvent(eventId, options);
44
+ return this.deliveryRepo.replayEvent(eventId, options, this.options?.delivery?.maxRetries ?? webhook_constants_1.DEFAULT_MAX_RETRIES);
43
45
  }
44
46
  };
45
47
  exports.WebhookDeliveryAdminService = WebhookDeliveryAdminService;
46
48
  exports.WebhookDeliveryAdminService = WebhookDeliveryAdminService = __decorate([
47
49
  (0, common_1.Injectable)(),
48
50
  __param(0, (0, common_1.Inject)(webhook_constants_1.WEBHOOK_DELIVERY_REPOSITORY)),
49
- __metadata("design:paramtypes", [Object])
51
+ __param(1, (0, common_1.Optional)()),
52
+ __param(1, (0, common_1.Inject)(webhook_constants_1.WEBHOOK_MODULE_OPTIONS)),
53
+ __metadata("design:paramtypes", [Object, Object])
50
54
  ], WebhookDeliveryAdminService);
51
55
  //# sourceMappingURL=webhook.delivery-admin.service.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"webhook.delivery-admin.service.js","sourceRoot":"","sources":["../src/webhook.delivery-admin.service.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAAoD;AACpD,2DAAkE;AAc3D,IAAM,2BAA2B,GAAjC,MAAM,2BAA2B;IAGnB;IAFnB,YAEmB,YAAuC;QAAvC,iBAAY,GAAZ,YAAY,CAA2B;IACvD,CAAC;IAEJ,KAAK,CAAC,eAAe,CACnB,UAAkB,EAClB,OAA4B;QAE5B,OAAO,IAAI,CAAC,YAAY,CAAC,eAAe,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IAChE,CAAC;IAED,KAAK,CAAC,mBAAmB,CAAC,UAAkB;QAC1C,OAAO,IAAI,CAAC,YAAY,CAAC,mBAAmB,CAAC,UAAU,CAAC,CAAC;IAC3D,CAAC;IAED,KAAK,CAAC,aAAa,CACjB,UAAkB,EAClB,OAA8B;QAE9B,OAAO,IAAI,CAAC,YAAY,CAAC,aAAa,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IAC9D,CAAC;IAED,KAAK,CAAC,qBAAqB,CACzB,OAAqC,EACrC,OAA8B;QAE9B,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,qBAAqB,EAAE,CAAC;YAC7C,MAAM,IAAI,KAAK,CACb,uEAAuE,CACxE,CAAC;QACJ,CAAC;QAED,OAAO,IAAI,CAAC,YAAY,CAAC,qBAAqB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IACnE,CAAC;IAED,KAAK,CAAC,WAAW,CACf,OAAe,EACf,OAA4B;QAE5B,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,WAAW,EAAE,CAAC;YACnC,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAC;QAC7E,CAAC;QAED,OAAO,IAAI,CAAC,YAAY,CAAC,WAAW,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IACzD,CAAC;CACF,CAAA;AA/CY,kEAA2B;sCAA3B,2BAA2B;IADvC,IAAA,mBAAU,GAAE;IAGR,WAAA,IAAA,eAAM,EAAC,+CAA2B,CAAC,CAAA;;GAF3B,2BAA2B,CA+CvC"}
1
+ {"version":3,"file":"webhook.delivery-admin.service.js","sourceRoot":"","sources":["../src/webhook.delivery-admin.service.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAA8D;AAC9D,2DAI6B;AAetB,IAAM,2BAA2B,GAAjC,MAAM,2BAA2B;IAGnB;IAGA;IALnB,YAEmB,YAAuC,EAGvC,OAA8B;QAH9B,iBAAY,GAAZ,YAAY,CAA2B;QAGvC,YAAO,GAAP,OAAO,CAAuB;IAC9C,CAAC;IAEJ,KAAK,CAAC,eAAe,CACnB,UAAkB,EAClB,OAA4B;QAE5B,OAAO,IAAI,CAAC,YAAY,CAAC,eAAe,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IAChE,CAAC;IAED,KAAK,CAAC,mBAAmB,CAAC,UAAkB;QAC1C,OAAO,IAAI,CAAC,YAAY,CAAC,mBAAmB,CAAC,UAAU,CAAC,CAAC;IAC3D,CAAC;IAED,KAAK,CAAC,aAAa,CACjB,UAAkB,EAClB,OAA8B;QAE9B,OAAO,IAAI,CAAC,YAAY,CAAC,aAAa,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IAC9D,CAAC;IAED,KAAK,CAAC,qBAAqB,CACzB,OAAqC,EACrC,OAA8B;QAE9B,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,qBAAqB,EAAE,CAAC;YAC7C,MAAM,IAAI,KAAK,CACb,uEAAuE,CACxE,CAAC;QACJ,CAAC;QAED,OAAO,IAAI,CAAC,YAAY,CAAC,qBAAqB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IACnE,CAAC;IAED,KAAK,CAAC,WAAW,CACf,OAAe,EACf,OAA4B;QAE5B,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,WAAW,EAAE,CAAC;YACnC,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAC;QAC7E,CAAC;QAED,OAAO,IAAI,CAAC,YAAY,CAAC,WAAW,CAClC,OAAO,EACP,OAAO,EACP,IAAI,CAAC,OAAO,EAAE,QAAQ,EAAE,UAAU,IAAI,uCAAmB,CAC1D,CAAC;IACJ,CAAC;CACF,CAAA;AAtDY,kEAA2B;sCAA3B,2BAA2B;IADvC,IAAA,mBAAU,GAAE;IAGR,WAAA,IAAA,eAAM,EAAC,+CAA2B,CAAC,CAAA;IAEnC,WAAA,IAAA,iBAAQ,GAAE,CAAA;IACV,WAAA,IAAA,eAAM,EAAC,0CAAsB,CAAC,CAAA;;GALtB,2BAA2B,CAsDvC"}
@@ -11,8 +11,21 @@ export declare class WebhookService {
11
11
  private readonly maxAttempts;
12
12
  private readonly redaction?;
13
13
  constructor(eventRepo: WebhookEventRepository, endpointRepo: WebhookEndpointRepository, deliveryRepo: WebhookDeliveryRepository, options: WebhookModuleOptions);
14
+ /**
15
+ * Publish to active matching endpoints across all tenants.
16
+ * Returns the saved event ID even when no endpoints match. correlationId is
17
+ * stored independently of idempotencyKey; duplicate keys return the original
18
+ * event without creating deliveries or updating its metadata.
19
+ */
14
20
  send(event: WebhookEvent, options?: WebhookPublishOptions): Promise<string>;
21
+ /** Publish to active matching endpoints belonging to the specified tenant. */
15
22
  sendToTenant(tenantId: string, event: WebhookEvent, options?: WebhookPublishOptions): Promise<string>;
23
+ /**
24
+ * Publish to eligible endpoints among the specified IDs. The built-in repository
25
+ * requires active endpoints subscribed to the exact event type or literal '*'.
26
+ * When a tenant ID is provided, endpoint ownership must match that tenant.
27
+ * Pass publish options as the third argument when no tenant ID is needed.
28
+ */
16
29
  sendToEndpoints(endpointIds: string[], event: WebhookEvent, tenantIdOrOptions?: string | WebhookPublishOptions, options?: WebhookPublishOptions): Promise<string>;
17
30
  private sendInternal;
18
31
  private saveEventInTransaction;
@@ -1 +1 @@
1
- {"version":3,"file":"webhook.service.d.ts","sourceRoot":"","sources":["../src/webhook.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAO/C,OAAO,EACL,oBAAoB,EACpB,qBAAqB,EAEtB,MAAM,wCAAwC,CAAC;AAChD,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAC1E,OAAO,EAAE,yBAAyB,EAAE,MAAM,qCAAqC,CAAC;AAChF,OAAO,EACL,yBAAyB,EAE1B,MAAM,qCAAqC,CAAC;AAE7C,qBACa,cAAc;IAOvB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAE1B,OAAO,CAAC,QAAQ,CAAC,YAAY;IAE7B,OAAO,CAAC,QAAQ,CAAC,YAAY;IAV/B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmC;IAC1D,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,CAA0B;gBAIlC,SAAS,EAAE,sBAAsB,EAEjC,YAAY,EAAE,yBAAyB,EAEvC,YAAY,EAAE,yBAAyB,EAExD,OAAO,EAAE,oBAAoB;IAMzB,IAAI,CAAC,KAAK,EAAE,YAAY,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,MAAM,CAAC;IAI3E,YAAY,CAChB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,YAAY,EACnB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,MAAM,CAAC;IAIZ,eAAe,CACnB,WAAW,EAAE,MAAM,EAAE,EACrB,KAAK,EAAE,YAAY,EACnB,iBAAiB,CAAC,EAAE,MAAM,GAAG,qBAAqB,EAClD,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,MAAM,CAAC;YAmDJ,YAAY;YA2DZ,sBAAsB;IA6BpC,OAAO,CAAC,eAAe;CAOxB"}
1
+ {"version":3,"file":"webhook.service.d.ts","sourceRoot":"","sources":["../src/webhook.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAO/C,OAAO,EACL,oBAAoB,EACpB,qBAAqB,EAEtB,MAAM,wCAAwC,CAAC;AAChD,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAC1E,OAAO,EAAE,yBAAyB,EAAE,MAAM,qCAAqC,CAAC;AAChF,OAAO,EACL,yBAAyB,EAE1B,MAAM,qCAAqC,CAAC;AAE7C,qBACa,cAAc;IAOvB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAE1B,OAAO,CAAC,QAAQ,CAAC,YAAY;IAE7B,OAAO,CAAC,QAAQ,CAAC,YAAY;IAV/B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmC;IAC1D,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,CAA0B;gBAIlC,SAAS,EAAE,sBAAsB,EAEjC,YAAY,EAAE,yBAAyB,EAEvC,YAAY,EAAE,yBAAyB,EAExD,OAAO,EAAE,oBAAoB;IAM/B;;;;;OAKG;IACG,IAAI,CAAC,KAAK,EAAE,YAAY,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,MAAM,CAAC;IAIjF,8EAA8E;IACxE,YAAY,CAChB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,YAAY,EACnB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,MAAM,CAAC;IAIlB;;;;;OAKG;IACG,eAAe,CACnB,WAAW,EAAE,MAAM,EAAE,EACrB,KAAK,EAAE,YAAY,EACnB,iBAAiB,CAAC,EAAE,MAAM,GAAG,qBAAqB,EAClD,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,MAAM,CAAC;YAmDJ,YAAY;YA2DZ,sBAAsB;IA8BpC,OAAO,CAAC,eAAe;CAOxB"}
@@ -30,12 +30,25 @@ let WebhookService = WebhookService_1 = class WebhookService {
30
30
  this.maxAttempts = options.delivery?.maxRetries ?? 5;
31
31
  this.redaction = options.redaction;
32
32
  }
33
+ /**
34
+ * Publish to active matching endpoints across all tenants.
35
+ * Returns the saved event ID even when no endpoints match. correlationId is
36
+ * stored independently of idempotencyKey; duplicate keys return the original
37
+ * event without creating deliveries or updating its metadata.
38
+ */
33
39
  async send(event, options) {
34
40
  return this.sendInternal(event, undefined, options);
35
41
  }
42
+ /** Publish to active matching endpoints belonging to the specified tenant. */
36
43
  async sendToTenant(tenantId, event, options) {
37
44
  return this.sendInternal(event, tenantId, options);
38
45
  }
46
+ /**
47
+ * Publish to eligible endpoints among the specified IDs. The built-in repository
48
+ * requires active endpoints subscribed to the exact event type or literal '*'.
49
+ * When a tenant ID is provided, endpoint ownership must match that tenant.
50
+ * Pass publish options as the third argument when no tenant ID is needed.
51
+ */
39
52
  async sendToEndpoints(endpointIds, event, tenantIdOrOptions, options) {
40
53
  const tenantId = typeof tenantIdOrOptions === 'string' ? tenantIdOrOptions : undefined;
41
54
  const publishOptions = typeof tenantIdOrOptions === 'object' ? tenantIdOrOptions : options;
@@ -80,7 +93,7 @@ let WebhookService = WebhookService_1 = class WebhookService {
80
93
  }
81
94
  async saveEventInTransaction(tx, eventType, payload, tenantId, options) {
82
95
  if (!options?.idempotencyKey) {
83
- const id = await this.eventRepo.saveEventInTransaction(tx, eventType, payload, tenantId);
96
+ const id = await this.eventRepo.saveEventInTransaction(tx, eventType, payload, tenantId, options);
84
97
  return { id, created: true };
85
98
  }
86
99
  if (!this.eventRepo.saveEventOnceInTransaction) {
@@ -1 +1 @@
1
- {"version":3,"file":"webhook.service.js","sourceRoot":"","sources":["../src/webhook.service.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,2CAA4D;AAE5D,2DAK6B;AActB,IAAM,cAAc,sBAApB,MAAM,cAAc;IAON;IAEA;IAEA;IAVF,MAAM,GAAG,IAAI,eAAM,CAAC,gBAAc,CAAC,IAAI,CAAC,CAAC;IACzC,WAAW,CAAS;IACpB,SAAS,CAA2B;IAErD,YAEmB,SAAiC,EAEjC,YAAuC,EAEvC,YAAuC,EAExD,OAA6B;QANZ,cAAS,GAAT,SAAS,CAAwB;QAEjC,iBAAY,GAAZ,YAAY,CAA2B;QAEvC,iBAAY,GAAZ,YAAY,CAA2B;QAIxD,IAAI,CAAC,WAAW,GAAG,OAAO,CAAC,QAAQ,EAAE,UAAU,IAAI,CAAC,CAAC;QACrD,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;IACrC,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,KAAmB,EAAE,OAA+B;QAC7D,OAAO,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;IACtD,CAAC;IAED,KAAK,CAAC,YAAY,CAChB,QAAgB,EAChB,KAAmB,EACnB,OAA+B;QAE/B,OAAO,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;IACrD,CAAC;IAED,KAAK,CAAC,eAAe,CACnB,WAAqB,EACrB,KAAmB,EACnB,iBAAkD,EAClD,OAA+B;QAE/B,MAAM,QAAQ,GACZ,OAAO,iBAAiB,KAAK,QAAQ,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,SAAS,CAAC;QACxE,MAAM,cAAc,GAClB,OAAO,iBAAiB,KAAK,QAAQ,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,OAAO,CAAC;QACtE,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC;QAClC,MAAM,OAAO,GAAG,IAAI,CAAC,eAAe,CAClC,KAAK,CAAC,SAAS,EAAE,EACjB,SAAS,EACT,QAAQ,IAAI,IAAI,CACjB,CAAC;QAEF,OAAO,IAAI,CAAC,YAAY,CAAC,gBAAgB,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;YACrD,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,sBAAsB,CAClD,EAAE,EACF,SAAS,EACT,OAAO,EACP,QAAQ,IAAI,IAAI,EAChB,cAAc,CACf,CAAC;YACF,MAAM,OAAO,GAAG,UAAU,CAAC,EAAE,CAAC;YAE9B,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC;gBACxB,IAAI,CAAC,MAAM,CAAC,KAAK,CACf,oBAAoB,SAAS,KAAK,OAAO,uDAAuD,CACjG,CAAC;gBACF,OAAO,OAAO,CAAC;YACjB,CAAC;YAED,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBAC7B,IAAI,CAAC,MAAM,CAAC,KAAK,CACf,sCAAsC,SAAS,aAAa,OAAO,GAAG,CACvE,CAAC;gBACF,OAAO,OAAO,CAAC;YACjB,CAAC;YAED,MAAM,IAAI,CAAC,YAAY,CAAC,6BAA6B,CACnD,EAAE,EACF,OAAO,EACP,WAAW,EACX,IAAI,CAAC,WAAW,CACjB,CAAC;YAEF,IAAI,CAAC,MAAM,CAAC,GAAG,CACb,SAAS,SAAS,KAAK,OAAO,OAAO,WAAW,CAAC,MAAM,uBAAuB,CAC/E,CAAC;YAEF,OAAO,OAAO,CAAC;QACjB,CAAC,CAAC,CAAC;IACL,CAAC;IAEO,KAAK,CAAC,YAAY,CACxB,KAAmB,EACnB,QAA4B,EAC5B,OAA+B;QAE/B,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC;QAClC,MAAM,OAAO,GAAG,IAAI,CAAC,eAAe,CAClC,KAAK,CAAC,SAAS,EAAE,EACjB,SAAS,EACT,QAAQ,IAAI,IAAI,CACjB,CAAC;QAEF,OAAO,IAAI,CAAC,YAAY,CAAC,gBAAgB,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;YACrD,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,sBAAsB,CAClD,EAAE,EACF,SAAS,EACT,OAAO,EACP,QAAQ,IAAI,IAAI,EAChB,OAAO,CACR,CAAC;YACF,MAAM,OAAO,GAAG,UAAU,CAAC,EAAE,CAAC;YAE9B,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC;gBACxB,IAAI,CAAC,MAAM,CAAC,KAAK,CACf,oBAAoB,SAAS,KAAK,OAAO,8CAA8C,CACxF,CAAC;gBACF,OAAO,OAAO,CAAC;YACjB,CAAC;YAED,MAAM,SAAS,GACb,MAAM,IAAI,CAAC,YAAY,CAAC,kCAAkC,CACxD,EAAE,EACF,SAAS,EACT,QAAQ,CACT,CAAC;YAEJ,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBAC3B,IAAI,CAAC,MAAM,CAAC,KAAK,CACf,mCAAmC,SAAS,aAAa,OAAO,GAAG,CACpE,CAAC;gBACF,OAAO,OAAO,CAAC;YACjB,CAAC;YAED,MAAM,WAAW,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;YACjD,MAAM,IAAI,CAAC,YAAY,CAAC,6BAA6B,CACnD,EAAE,EACF,OAAO,EACP,WAAW,EACX,IAAI,CAAC,WAAW,CACjB,CAAC;YAEF,IAAI,CAAC,MAAM,CAAC,GAAG,CACb,SAAS,SAAS,KAAK,OAAO,OAAO,SAAS,CAAC,MAAM,cAAc,CACpE,CAAC;YAEF,OAAO,OAAO,CAAC;QACjB,CAAC,CAAC,CAAC;IACL,CAAC;IAEO,KAAK,CAAC,sBAAsB,CAClC,EAAsB,EACtB,SAAiB,EACjB,OAAgC,EAChC,QAAuB,EACvB,OAA+B;QAE/B,IAAI,CAAC,OAAO,EAAE,cAAc,EAAE,CAAC;YAC7B,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,sBAAsB,CACpD,EAAE,EACF,SAAS,EACT,OAAO,EACP,QAAQ,CACT,CAAC;YACF,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QAC/B,CAAC;QAED,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,0BAA0B,EAAE,CAAC;YAC/C,MAAM,IAAI,KAAK,CACb,sEAAsE,CACvE,CAAC;QACJ,CAAC;QAED,OAAO,IAAI,CAAC,SAAS,CAAC,0BAA0B,CAAC,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE;YACjF,cAAc,EAAE,OAAO,CAAC,cAAc;YACtC,aAAa,EAAE,OAAO,CAAC,aAAa;SACrC,CAAC,CAAC;IACL,CAAC;IAEO,eAAe,CACrB,OAAgC,EAChC,SAAiB,EACjB,QAAuB;QAEvB,OAAO,IAAI,CAAC,SAAS,EAAE,eAAe,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,IAAI,OAAO,CAAC;IACxF,CAAC;CACF,CAAA;AAtLY,wCAAc;yBAAd,cAAc;IAD1B,IAAA,mBAAU,GAAE;IAOR,WAAA,IAAA,eAAM,EAAC,4CAAwB,CAAC,CAAA;IAEhC,WAAA,IAAA,eAAM,EAAC,+CAA2B,CAAC,CAAA;IAEnC,WAAA,IAAA,eAAM,EAAC,+CAA2B,CAAC,CAAA;IAEnC,WAAA,IAAA,eAAM,EAAC,0CAAsB,CAAC,CAAA;;GAZtB,cAAc,CAsL1B"}
1
+ {"version":3,"file":"webhook.service.js","sourceRoot":"","sources":["../src/webhook.service.ts"],"names":[],"mappings":"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"}
package/docs/usage.md ADDED
@@ -0,0 +1,109 @@
1
+ # Using @nestarc/webhook
2
+
3
+ This is the consumer contract for **0.13.2**. See the [0.13.2 changes](../CHANGELOG.md#0132---2026-09-11); if that version is not yet available on npm, use a locally packed build. In 0.13.1, correlation IDs are only stored with idempotency keys, replay always uses five attempts, and manual retry does not guard against purged payloads.
4
+
5
+ Start with the [README](../README.md) and the [executable example](../examples/quick-start/README.md). Online references: [usage documentation](https://nestarc.dev/packages/webhook/) and [API reference](https://nestarc.dev/api/webhook/). Use the types in your installed package when they differ from an unreleased example.
6
+
7
+ ## Runtime and ownership
8
+
9
+ - The default adapters use PostgreSQL and a Prisma client supplied by the application. No Redis, broker, or separate webhook server is required.
10
+ - Supported peers: NestJS 10/11, `@nestjs/schedule` 4/5, Prisma Client 5/6/7. Match peer versions; do not bypass peer validation.
11
+ - The package declares Node >=20. Prisma 7.10.0 requires Node `^20.19 || ^22.12 || >=24.0`; use a supported version for your selected dependencies.
12
+ - Prisma 7 needs a driver adapter and a generated client. For a CommonJS Nest application, configure `moduleFormat = "cjs"` in the `prisma-client` generator. The runnable example includes generation and compilation.
13
+ - The application owns Prisma connection setup and shutdown. For signal handling, enable Nest shutdown hooks or call `app.close()` explicitly; then disconnect the application-owned database client.
14
+ - Apply SQL to the same database/schema used by the client. New installs use `src/sql/create-webhook-tables.sql` from the installed package. Existing pre-0.9 installations run v0.9.0, v0.12.0, then v0.13.0 migrations; skip migrations already applied. v0.12.0 adds performance indexes.
15
+
16
+ ## First delivery
17
+
18
+ 1. Prepare the database, client, and receiving HTTP endpoint.
19
+ 2. Register `WebhookModule.forRoot()` or `forRootAsync()` with that client.
20
+ 3. Create an endpoint with the intended tenant and subscriptions; provision its returned secret to the receiver.
21
+ 4. Define a `WebhookEvent` subclass with a nonempty static `eventType` and publish it.
22
+ 5. Verify the raw request body and headers at the receiver, deduplicate the event, then acknowledge with `2xx`.
23
+ 6. Inspect `getDeliveryLogs()` and `getDeliveryAttempts()` for the delivery result.
24
+
25
+ Publishing without any matching endpoints stores the event but creates no deliveries. Creating an endpoint later does not automatically deliver past events. Explicit replay can enqueue a retained event for current matching endpoints.
26
+
27
+ ## Publish and tenant contract
28
+
29
+ | API | Destination selection with default repositories | Result |
30
+ | --- | --- | --- |
31
+ | `send(event, options?)` | Every active endpoint subscribed to the event, across all tenants, including endpoints with no tenant | `Promise<string>` event ID |
32
+ | `sendToTenant(tenantId, event, options?)` | Active subscribed endpoints whose tenant equals `tenantId`; global endpoints are excluded | `Promise<string>` event ID |
33
+ | `sendToEndpoints(ids, event, options?)` | Requested IDs, still filtered by active status and subscription, across tenants | `Promise<string>` event ID |
34
+ | `sendToEndpoints(ids, event, tenantId, options?)` | Requested IDs, also filtered to the specified tenant | `Promise<string>` event ID |
35
+
36
+ Subscriptions match an exact event name or the literal `"*"`. Patterns such as `"order.*"` are not supported. Endpoint IDs and event IDs are UUIDs with the PostgreSQL adapters. Tenant IDs are opaque strings.
37
+
38
+ `await send(...)` means the event and initial delivery rows have committed, not that HTTP delivery succeeded. The package transaction does not include an application operation such as `saveOrder()`. If both must commit atomically, integrate an application outbox/transaction strategy; do not assume two awaited calls provide that guarantee.
39
+
40
+ All publish methods accept `WebhookPublishOptions`:
41
+
42
+ - `idempotencyKey`: deduplicates by tenant, event type, and key. A duplicate returns the original event ID and creates no new delivery rows, even if the payload or targeted endpoint list changed. Reuse a key only for the same business event. This is producer-side deduplication, not receiver-side exactly-once processing.
43
+ - `correlationId`: diagnostic event metadata. In this source version it is stored independently of idempotency. It is not added to HTTP headers or the delivered payload. It does not deduplicate events. Duplicate idempotent publishes retain the original metadata.
44
+
45
+ The delivered JSON is `{ "type": "order.created", "data": { ...eventFields } }`. `sanitizePayload` receives the data object before persistence; its output is also what receivers receive.
46
+
47
+ ## Delivery, retry, and concurrency
48
+
49
+ Workers atomically claim database rows using `FOR UPDATE SKIP LOCKED`. Retries, lost acknowledgements, crashes, and database write failures can produce duplicate HTTP requests. Delivery uses a bounded attempt budget; it does not guarantee eventual success or exactly-once delivery. No ordering guarantee is provided between events.
50
+
51
+ `delivery.maxRetries` means **total attempts including the first**. The default five attempts allow four retries: 30 seconds, 5 minutes, 30 minutes, and 2 hours. A sixth attempt uses the 24-hour delay; later retries reuse 24 hours. Jitter adds 0–10% to a delay, never subtracts from it. These are minimum scheduling delays; polling, backlog, and outages can delay actual dispatch.
52
+
53
+ | Result | Action |
54
+ | --- | --- |
55
+ | `2xx` | Mark `SENT` |
56
+ | `3xx`, `408`, `409`, `425`, `429`, `5xx`, network/validation/dispatch failure | Retry while attempts remain; otherwise mark `FAILED` |
57
+ | Other `4xx` | Mark `FAILED` immediately |
58
+
59
+ The fixed schedule does not interpret `Retry-After`. Redirects are not followed. The default class is named `FetchHttpClient` for compatibility but uses Node `http`/`https.request`, connecting to prevalidated IPs while preserving the original hostname for HTTP/TLS. Custom clients must honor the supplied `resolvedIpAddresses` to retain that DNS-rebinding protection.
60
+
61
+ The circuit breaker disables an endpoint for **new normal deliveries** after consecutive failures. Already queued deliveries and retries continue. A successful queued delivery may reset the breaker before cooldown; otherwise cooldown recovery can reactivate breaker-disabled endpoints. Manually disabled endpoints are not automatically reactivated by breaker recovery. Notification and observer callbacks are best-effort, not a durable notification queue.
62
+
63
+ ## Signature verification and rotation
64
+
65
+ The HMAC-SHA256 input and `webhook-id`, `webhook-timestamp`, `webhook-signature` headers follow the Standard Webhooks signing format. This package accepts bare base64 secrets with at least 16 decoded bytes for compatibility and generates 32-byte secrets. It does not accept `whsec_`-prefixed secrets through endpoint management; remove the prefix only when converting a known compatible secret. Do not describe all secret formats or every Standard Webhooks recommendation as supported.
66
+
67
+ Verify the **original UTF-8 body**, not JSON that has been parsed and serialized again. Prefer `verifyWithTolerance(..., { toleranceSeconds: 300 })`; `verify()` alone performs no freshness check. Freshness permits repeated requests within the tolerance window. Store a receiver-scoped `webhook-id` deduplication key atomically with your business effect. Use durable shared storage across receiver instances. The example's in-memory set is only a single-process demonstration.
68
+
69
+ `createEndpoint()` and `rotateSecret()` return the new provisioning secret. Read APIs omit it. The built-in vault stores secrets as plaintext unless you supply an encryption vault.
70
+
71
+ New delivery rows snapshot their URL and signing secret **when queued**. Legacy rows created before snapshot support, with null snapshot columns, fall back to the endpoint's live URL/current key. Rotation expiry controls whether a previous secret is copied as a secondary signature into a new delivery at that moment:
72
+
73
+ | When the delivery was queued | Keys retained for dispatch and retries |
74
+ | --- | --- |
75
+ | Before rotation | Its original key |
76
+ | During the overlap window | Current and previous keys |
77
+ | After the overlap window | Current key only |
78
+
79
+ Dispatch does not reevaluate expiry for existing snapshots. Rotation neither rewrites the queue nor immediately revokes a compromised key for queued deliveries. Receiver acceptance windows and operational handling of existing deliveries must account for this behavior. Replay creates fresh snapshots; manual retry preserves existing snapshots.
80
+
81
+ ## Administration and retention
82
+
83
+ | Operation | Contract |
84
+ | --- | --- |
85
+ | `createEndpoint(dto)` | Returns endpoint and secret; invalid URL or secret throws |
86
+ | `getEndpoint(id)` / `updateEndpoint(id, dto)` | Return endpoint without secret, or `null` when missing |
87
+ | `rotateSecret(id, dto)` | Requires a future `Date`; returns endpoint and new secret, or `null` |
88
+ | `deleteEndpoint(id)` | Returns `false` when missing. The default schema prevents deletion while delivery rows reference the endpoint, raising a foreign-key error; disable it instead when retaining history |
89
+ | `sendTestEvent(id)` | Returns event ID, or `null` when missing; explicit diagnostic delivery bypasses normal subscription matching |
90
+ | `retryDelivery(id, options?)` | Requeues a failed delivery with retained payload and grants at least one additional attempt; returns `false` if ineligible/missing |
91
+ | `retryFailedDeliveries(filters, options?)` | Returns `{ matched, retried, skipped }`; limit defaults to 100, allowed range 1–1000 |
92
+ | `replayEvent(id, options?)` | Returns `{ eventId, deliveriesCreated, endpointIds }`; creates new rows for currently active matching endpoints using the configured total attempt limit |
93
+ | `purgeExpiredData(now?)` | Returns payload/response purge counts; must be invoked by the application's scheduler |
94
+
95
+ Replay reuses the original event ID. Receivers deduplicating that ID may acknowledge replay without processing it again. Coordinate intentional reprocessing with the receiver. `options.tenantId` must match the **source event's tenant**, not just the intended destination: a global event cannot be replayed with a different tenant option. `endpointIds` further restricts the destination list. Missing or payload-purged source events throw. Zero matching destinations returns zero created deliveries.
96
+
97
+ The optional retry/replay `reason` field is metadata for custom adapters. The built-in Prisma adapter does not persist it. Record support actions in your application's audit log if needed.
98
+
99
+ Retention is disabled by default. Payload purging replaces data with `{}` and marks it purged; it does not delete the event/idempotency key. This source version rejects retry and replay of purged payloads. Response retention clears response bodies only; delivery/attempt metadata remains. `sanitizeResponseBody` can suppress responses before storage.
100
+
101
+ Delivery logs show the latest persisted state of a delivery. Attempt records show recorded attempts separately. The default transport retains at most 4096 UTF-16 code units of a response body; logs are not a complete copy of every response. An interrupted process can lose the response before recording it; stale recovery records the recovery, not the lost HTTP response. The `responseBodyTruncated` flag reflects truncation by the repository and cannot detect bytes already discarded by a custom or built-in HTTP client.
102
+
103
+ ## Custom adapters and authoritative references
104
+
105
+ Import public services, options, records, tokens, and ports from `@nestarc/webhook`. `WebhookDeliveryWorker`, dispatcher, retry policy, and circuit breaker exports are advanced/internal integration surfaces; normal applications use `WebhookModule` and public services.
106
+
107
+ Custom repositories must preserve transaction and tenant boundaries. Optional port capabilities include idempotent persistence, bulk retry, replay, retention, and backlog summary; calling unsupported operations throws or is unavailable. Event repositories should store the optional correlation metadata. Replay repositories receive the configured attempt budget as an optional third argument. A method with fewer parameters remains TypeScript-compatible, but must be updated to use the new metadata/budget.
108
+
109
+ For this checkout, prefer current public types and this guide over dated files in `docs/`. Historical plans explain past decisions and are not installation recipes. The [changelog](../CHANGELOG.md) identifies released versus pending behavior.
@@ -0,0 +1,83 @@
1
+ # Runnable NestJS webhook example
2
+
3
+ This example starts a NestJS application context and a receiver on an ephemeral
4
+ loopback port, registers an endpoint, publishes a tenant event, and lets the normal
5
+ scheduled worker deliver it. It verifies the raw request signature, rejects a
6
+ tampered body, acknowledges a duplicate without processing it again, and checks
7
+ the persisted `SENT` delivery and its attempt log.
8
+
9
+ ## Run
10
+
11
+ This example targets `@nestarc/webhook@0.13.2`. Before that version is available
12
+ on npm, use the [packed-source verification](#verify-the-repository-build) below.
13
+
14
+ Use Node.js **20.19+, 22.12+, or 24+** (the supported Prisma 7 release lines) and
15
+ PostgreSQL **14+**. The database user needs permission to create schemas. Use a
16
+ local test database; each run creates a random schema and drops only that schema
17
+ when it finishes. Existing webhook tables are not used.
18
+
19
+ From this directory:
20
+
21
+ ```bash
22
+ export DATABASE_URL='postgresql://webhook_test:webhook_test@localhost:5433/webhook_test'
23
+ npm install --strict-peer-deps
24
+ npm start
25
+ ```
26
+
27
+ For example, start a disposable PostgreSQL database with Docker first:
28
+
29
+ ```bash
30
+ docker run --rm --name webhook-quick-start-db \
31
+ -e POSTGRES_USER=webhook_test \
32
+ -e POSTGRES_PASSWORD=webhook_test \
33
+ -e POSTGRES_DB=webhook_test \
34
+ -p 127.0.0.1:5433:5432 postgres:16-alpine
35
+ ```
36
+
37
+ Wait for PostgreSQL to report that it is ready to accept connections, then run the
38
+ example in a second terminal. Stop the Docker command when finished.
39
+
40
+ Expected output:
41
+
42
+ ```text
43
+ Verified webhook: type=order.created tenant=tenant_example delivery=SENT attempts=1
44
+ Verified receiver: tampered=401 duplicate=200 processed=1
45
+ Quick start passed; temporary schema removed and connections closed.
46
+ ```
47
+
48
+ `npm start` generates the Prisma 7 client, compiles the CommonJS application, and
49
+ runs it. The package uses raw SQL, so the Prisma schema needs no webhook models.
50
+ The example applies the SQL shipped with the installed package to its temporary
51
+ schema; it skips the `pgcrypto` extension because PostgreSQL 14+ already supplies
52
+ `gen_random_uuid()`.
53
+
54
+ ## Use the pattern in your application
55
+
56
+ - Register an endpoint and provide its secret to the receiver **before** publishing.
57
+ `sendToTenant()` limits matching destinations to that tenant. `send()` broadcasts
58
+ to matching endpoints across tenants.
59
+ - Verify the untouched request body before parsing JSON. A successful
60
+ `verifyWithTolerance()` check accepts repeated valid requests within the window;
61
+ deduplicate by `webhook-id` separately.
62
+ - The in-memory `Set` here lasts for one process. In production, store the event ID
63
+ durably with a unique constraint and commit it atomically with the business
64
+ change. Concurrent requests, restarts, and retries must not repeat that change.
65
+ - `allowPrivateUrls: true` permits this loopback destination. Omit it for production
66
+ public destinations, use HTTPS, and store signing secrets securely.
67
+ - Replace the temporary-schema setup with your normal versioned migrations, and
68
+ keep your application's existing Prisma client and Nest lifecycle. This finite
69
+ example closes Nest before disconnecting the database.
70
+
71
+ ## Verify the repository build
72
+
73
+ From the repository root, with `DATABASE_URL` set, run:
74
+
75
+ ```bash
76
+ npm run test:consumer:quick-start
77
+ ```
78
+
79
+ The verifier packs the current build, copies this example into a temporary
80
+ consumer directory, installs the tarball with strict peer dependency resolution,
81
+ and runs the same `npm start` command. Application imports use only the package's
82
+ public root exports; SQL comes from the installed package. No repository source
83
+ imports or mocked database/HTTP adapters are used.
@@ -0,0 +1,247 @@
1
+ import 'reflect-metadata';
2
+ import assert from 'node:assert/strict';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { readFileSync } from 'node:fs';
5
+ import { createServer, IncomingHttpHeaders, ServerResponse } from 'node:http';
6
+ import { setTimeout as delay } from 'node:timers/promises';
7
+ import { INestApplicationContext, Module } from '@nestjs/common';
8
+ import { NestFactory } from '@nestjs/core';
9
+ import { PrismaPg } from '@prisma/adapter-pg';
10
+ import {
11
+ WebhookDeliveryAdminService,
12
+ WebhookEndpointAdminService,
13
+ WebhookEvent,
14
+ WebhookModule,
15
+ WebhookService,
16
+ WebhookSigner,
17
+ } from '@nestarc/webhook';
18
+ import { PrismaClient } from './generated/prisma/client';
19
+
20
+ class OrderCreatedEvent extends WebhookEvent {
21
+ static readonly eventType = 'order.created';
22
+
23
+ constructor(public readonly orderId: string) {
24
+ super();
25
+ }
26
+ }
27
+
28
+ async function main(): Promise<void> {
29
+ const connectionString = process.env.DATABASE_URL;
30
+ if (!connectionString) throw new Error('Set DATABASE_URL to a local test PostgreSQL database');
31
+
32
+ // Each run owns one temporary schema; no existing webhook tables are changed.
33
+ const schema = `webhook_example_${randomUUID().replaceAll('-', '')}`;
34
+ const scopedUrl = new URL(connectionString);
35
+ scopedUrl.searchParams.set('options', `-c search_path=${schema}`);
36
+ const database = new PrismaClient({
37
+ adapter: new PrismaPg({ connectionString, connectionTimeoutMillis: 5000 }),
38
+ });
39
+ const prisma = new PrismaClient({
40
+ adapter: new PrismaPg({
41
+ connectionString: scopedUrl.toString(),
42
+ connectionTimeoutMillis: 5000,
43
+ }),
44
+ });
45
+ const signer = new WebhookSigner();
46
+ // This Set demonstrates the check for a single process. Production receivers
47
+ // must persist a unique event ID with their business change in one transaction.
48
+ const processed = new Set<string>();
49
+ const received: Array<{ headers: IncomingHttpHeaders; body: string }> = [];
50
+ let secret = '';
51
+ let app: INestApplicationContext | undefined;
52
+ let schemaCreated = false;
53
+
54
+ function respond(response: ServerResponse, status: number, body: string): void {
55
+ response.writeHead(status, { 'content-type': 'text/plain' });
56
+ response.end(body);
57
+ }
58
+
59
+ const receiver = createServer((request, response) => {
60
+ if (request.method !== 'POST' || request.url !== '/webhooks') {
61
+ respond(response, 404, 'Not found');
62
+ return;
63
+ }
64
+ const chunks: Buffer[] = [];
65
+ let bytes = 0;
66
+ request.on('data', (chunk: Buffer) => {
67
+ bytes += chunk.length;
68
+ if (bytes <= 1024 * 1024) chunks.push(chunk);
69
+ });
70
+ request.on('error', () => {
71
+ if (!response.headersSent) respond(response, 400, 'Invalid request');
72
+ });
73
+ request.on('end', () => {
74
+ if (bytes > 1024 * 1024) return respond(response, 413, 'Body too large');
75
+ // Verify the exact request body before JSON parsing or any business changes.
76
+ const body = Buffer.concat(chunks).toString('utf8');
77
+ const eventId = request.headers['webhook-id'];
78
+ const timestamp = request.headers['webhook-timestamp'];
79
+ const signature = request.headers['webhook-signature'];
80
+ if (
81
+ !secret ||
82
+ typeof eventId !== 'string' ||
83
+ !eventId ||
84
+ typeof timestamp !== 'string' ||
85
+ !/^\d+$/.test(timestamp) ||
86
+ typeof signature !== 'string' ||
87
+ !signer.verifyWithTolerance(eventId, Number(timestamp), body, secret, signature, {
88
+ toleranceSeconds: 300,
89
+ })
90
+ ) {
91
+ return respond(response, 401, 'Invalid or expired signature');
92
+ }
93
+ if (processed.has(eventId)) return respond(response, 200, 'Already processed');
94
+
95
+ let event: { type?: string; data?: { orderId?: string } };
96
+ try {
97
+ event = JSON.parse(body);
98
+ } catch {
99
+ return respond(response, 400, 'Invalid JSON');
100
+ }
101
+ if (event?.type !== 'order.created' || event.data?.orderId !== 'order_123') {
102
+ return respond(response, 400, 'Unexpected event');
103
+ }
104
+ // The example's business action is recording this verified request.
105
+ received.push({ headers: request.headers, body });
106
+ processed.add(eventId);
107
+ respond(response, 200, 'Processed');
108
+ });
109
+ });
110
+
111
+ try {
112
+ await database.$connect();
113
+ // schema is generated locally from UUID characters, never from user input.
114
+ await database.$executeRawUnsafe(`CREATE SCHEMA "${schema}"`);
115
+ schemaCreated = true;
116
+ await prisma.$connect();
117
+ const sql = readFileSync(
118
+ require.resolve('@nestarc/webhook/src/sql/create-webhook-tables.sql'),
119
+ 'utf8',
120
+ );
121
+ const statements = sql
122
+ .split('\n')
123
+ .filter((line) => !line.trimStart().startsWith('--'))
124
+ .join('\n')
125
+ .split(';')
126
+ .map((statement) => statement.trim())
127
+ .filter(Boolean);
128
+ for (const statement of statements) {
129
+ // PostgreSQL 14+ supplies gen_random_uuid(); avoid creating a global extension.
130
+ if (/^CREATE EXTENSION\b/i.test(statement)) continue;
131
+ await prisma.$executeRawUnsafe(statement);
132
+ }
133
+
134
+ await new Promise<void>((resolve, reject) => {
135
+ receiver.once('error', reject);
136
+ receiver.listen(0, '127.0.0.1', () => {
137
+ receiver.removeListener('error', reject);
138
+ resolve();
139
+ });
140
+ });
141
+ const address = receiver.address();
142
+ assert(address && typeof address !== 'string');
143
+ const url = `http://127.0.0.1:${address.port}/webhooks`;
144
+
145
+ @Module({
146
+ imports: [
147
+ WebhookModule.forRoot({
148
+ prisma,
149
+ polling: { interval: 100 },
150
+ delivery: { timeout: 5000, maxRetries: 1 },
151
+ allowPrivateUrls: true, // Only for this loopback example; omit in production.
152
+ }),
153
+ ],
154
+ })
155
+ class AppModule {}
156
+
157
+ app = await NestFactory.createApplicationContext(AppModule, {
158
+ logger: ['error', 'warn'],
159
+ abortOnError: false,
160
+ });
161
+ const endpoints = app.get(WebhookEndpointAdminService);
162
+ const deliveries = app.get(WebhookDeliveryAdminService);
163
+ const webhooks = app.get(WebhookService);
164
+
165
+ // Register the destination and securely provision its secret before publishing.
166
+ const endpoint = await endpoints.createEndpoint({
167
+ url,
168
+ events: ['order.created'],
169
+ tenantId: 'tenant_example',
170
+ });
171
+ secret = endpoint.secret;
172
+ const eventId = await webhooks.sendToTenant(
173
+ 'tenant_example',
174
+ new OrderCreatedEvent('order_123'),
175
+ { idempotencyKey: 'order_123:created', correlationId: 'example_request' },
176
+ );
177
+
178
+ let logs = await deliveries.getDeliveryLogs(endpoint.id);
179
+ const deadline = Date.now() + 15_000;
180
+ while (logs[0]?.status !== 'SENT') {
181
+ assert.notEqual(logs[0]?.status, 'FAILED', 'Delivery failed');
182
+ assert(Date.now() < deadline, 'Timed out waiting for scheduled delivery');
183
+ await delay(50);
184
+ logs = await deliveries.getDeliveryLogs(endpoint.id);
185
+ }
186
+ assert.equal(logs.length, 1);
187
+ assert.equal(logs[0].eventId, eventId);
188
+ assert.equal(logs[0].tenantId, 'tenant_example');
189
+ assert.equal(logs[0].attempts, 1);
190
+ const attempts = await deliveries.getDeliveryAttempts(logs[0].id);
191
+ assert.equal(attempts.length, 1);
192
+ assert.equal(attempts[0].status, 'SENT');
193
+ assert.equal(attempts[0].responseStatus, 200);
194
+ assert.equal(received.length, 1);
195
+ assert.equal(received[0].headers['webhook-id'], eventId);
196
+
197
+ // A timestamp check alone permits a duplicate within its tolerance window.
198
+ const original = received[0];
199
+ const headers = {
200
+ 'content-type': 'application/json',
201
+ 'webhook-id': eventId,
202
+ 'webhook-timestamp': String(original.headers['webhook-timestamp']),
203
+ 'webhook-signature': String(original.headers['webhook-signature']),
204
+ };
205
+ const tampered = await fetch(url, {
206
+ method: 'POST', headers, body: `${original.body} `, signal: AbortSignal.timeout(5000),
207
+ });
208
+ assert.equal(tampered.status, 401);
209
+ await tampered.text();
210
+ const duplicate = await fetch(url, {
211
+ method: 'POST', headers, body: original.body, signal: AbortSignal.timeout(5000),
212
+ });
213
+ assert.equal(duplicate.status, 200);
214
+ assert.equal(await duplicate.text(), 'Already processed');
215
+ assert.equal(received.length, 1);
216
+ assert.equal(processed.size, 1);
217
+
218
+ console.log('Verified webhook: type=order.created tenant=tenant_example delivery=SENT attempts=1');
219
+ console.log('Verified receiver: tampered=401 duplicate=200 processed=1');
220
+ } finally {
221
+ // Stop polling and await in-flight work before closing its resources.
222
+ try {
223
+ await app?.close();
224
+ } finally {
225
+ try {
226
+ if (receiver.listening) {
227
+ await new Promise<void>((resolve, reject) =>
228
+ receiver.close((error) => error ? reject(error) : resolve()),
229
+ );
230
+ }
231
+ } finally {
232
+ try {
233
+ await prisma.$disconnect();
234
+ if (schemaCreated) await database.$executeRawUnsafe(`DROP SCHEMA "${schema}" CASCADE`);
235
+ } finally {
236
+ await database.$disconnect();
237
+ }
238
+ }
239
+ }
240
+ }
241
+ console.log('Quick start passed; temporary schema removed and connections closed.');
242
+ }
243
+
244
+ main().catch((error: unknown) => {
245
+ console.error(error);
246
+ process.exitCode = 1;
247
+ });
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "webhook-quick-start",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "scripts": {
6
+ "generate": "prisma generate",
7
+ "build": "tsc -p tsconfig.json",
8
+ "start": "npm run generate && npm run build && node dist/main.js"
9
+ },
10
+ "engines": {
11
+ "node": "^20.19 || ^22.12 || >=24.0"
12
+ },
13
+ "dependencies": {
14
+ "@nestarc/webhook": "0.13.2",
15
+ "@nestjs/common": "11.2.1",
16
+ "@nestjs/core": "11.2.1",
17
+ "@nestjs/schedule": "5.0.1",
18
+ "@prisma/adapter-pg": "7.10.0",
19
+ "@prisma/client": "7.10.0",
20
+ "pg": "8.23.0",
21
+ "reflect-metadata": "0.2.2",
22
+ "rxjs": "7.8.2"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "25.6.0",
26
+ "prisma": "7.10.0",
27
+ "typescript": "5.9.3"
28
+ }
29
+ }
@@ -0,0 +1,10 @@
1
+ generator client {
2
+ provider = "prisma-client"
3
+ output = "../generated/prisma"
4
+ moduleFormat = "cjs"
5
+ engineType = "client"
6
+ }
7
+
8
+ datasource db {
9
+ provider = "postgresql"
10
+ }
@@ -0,0 +1,6 @@
1
+ import { defineConfig, env } from 'prisma/config';
2
+
3
+ export default defineConfig({
4
+ schema: 'prisma/schema.prisma',
5
+ datasource: { url: env('DATABASE_URL') },
6
+ });
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "commonjs",
5
+ "moduleResolution": "node",
6
+ "esModuleInterop": true,
7
+ "experimentalDecorators": true,
8
+ "emitDecoratorMetadata": true,
9
+ "strict": true,
10
+ "skipLibCheck": false,
11
+ "outDir": "dist",
12
+ "rootDir": "."
13
+ },
14
+ "include": ["main.ts", "generated/**/*.ts"]
15
+ }