@endora-commerce/mod-webhooks 0.100.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +59 -0
  3. package/dist/admin/index.d.ts +34 -0
  4. package/dist/admin/index.d.ts.map +1 -0
  5. package/dist/admin/index.js +45 -0
  6. package/dist/admin/index.js.map +1 -0
  7. package/dist/admin/pages/WebhooksPage.d.ts +3 -0
  8. package/dist/admin/pages/WebhooksPage.d.ts.map +1 -0
  9. package/dist/admin/pages/WebhooksPage.js +220 -0
  10. package/dist/admin/pages/WebhooksPage.js.map +1 -0
  11. package/dist/backend/entities/webhook-delivery.entity.d.ts +29 -0
  12. package/dist/backend/entities/webhook-delivery.entity.d.ts.map +1 -0
  13. package/dist/backend/entities/webhook-delivery.entity.js +108 -0
  14. package/dist/backend/entities/webhook-delivery.entity.js.map +1 -0
  15. package/dist/backend/entities/webhook.entity.d.ts +33 -0
  16. package/dist/backend/entities/webhook.entity.d.ts.map +1 -0
  17. package/dist/backend/entities/webhook.entity.js +91 -0
  18. package/dist/backend/entities/webhook.entity.js.map +1 -0
  19. package/dist/backend/index.d.ts +85 -0
  20. package/dist/backend/index.d.ts.map +1 -0
  21. package/dist/backend/index.js +129 -0
  22. package/dist/backend/index.js.map +1 -0
  23. package/dist/backend/routes.d.ts +26 -0
  24. package/dist/backend/routes.d.ts.map +1 -0
  25. package/dist/backend/routes.js +84 -0
  26. package/dist/backend/routes.js.map +1 -0
  27. package/dist/backend/services/event-bridge.d.ts +54 -0
  28. package/dist/backend/services/event-bridge.d.ts.map +1 -0
  29. package/dist/backend/services/event-bridge.js +43 -0
  30. package/dist/backend/services/event-bridge.js.map +1 -0
  31. package/dist/backend/services/webhook-delivery-worker.d.ts +34 -0
  32. package/dist/backend/services/webhook-delivery-worker.d.ts.map +1 -0
  33. package/dist/backend/services/webhook-delivery-worker.js +70 -0
  34. package/dist/backend/services/webhook-delivery-worker.js.map +1 -0
  35. package/dist/backend/services/webhook-queue.d.ts +24 -0
  36. package/dist/backend/services/webhook-queue.d.ts.map +1 -0
  37. package/dist/backend/services/webhook-queue.js +31 -0
  38. package/dist/backend/services/webhook-queue.js.map +1 -0
  39. package/dist/backend/services/webhook-service.d.ts +82 -0
  40. package/dist/backend/services/webhook-service.d.ts.map +1 -0
  41. package/dist/backend/services/webhook-service.js +176 -0
  42. package/dist/backend/services/webhook-service.js.map +1 -0
  43. package/dist/manifest.d.ts +169 -0
  44. package/dist/manifest.d.ts.map +1 -0
  45. package/dist/manifest.js +122 -0
  46. package/dist/manifest.js.map +1 -0
  47. package/dist/migrations/20260425T091359_webhooks_us7_init.d.ts +22 -0
  48. package/dist/migrations/20260425T091359_webhooks_us7_init.d.ts.map +1 -0
  49. package/dist/migrations/20260425T091359_webhooks_us7_init.js +86 -0
  50. package/dist/migrations/20260425T091359_webhooks_us7_init.js.map +1 -0
  51. package/dist/migrations/20260724T203140_webhooks_org_filter.d.ts +17 -0
  52. package/dist/migrations/20260724T203140_webhooks_org_filter.d.ts.map +1 -0
  53. package/dist/migrations/20260724T203140_webhooks_org_filter.js +30 -0
  54. package/dist/migrations/20260724T203140_webhooks_org_filter.js.map +1 -0
  55. package/dist/migrations/20260727T200555_webhooks_drop_external_integrations.d.ts +28 -0
  56. package/dist/migrations/20260727T200555_webhooks_drop_external_integrations.d.ts.map +1 -0
  57. package/dist/migrations/20260727T200555_webhooks_drop_external_integrations.js +49 -0
  58. package/dist/migrations/20260727T200555_webhooks_drop_external_integrations.js.map +1 -0
  59. package/dist/migrations/index.d.ts +29 -0
  60. package/dist/migrations/index.d.ts.map +1 -0
  61. package/dist/migrations/index.js +33 -0
  62. package/dist/migrations/index.js.map +1 -0
  63. package/docs/webhooks.md +50 -0
  64. package/i18n/en.json +6 -0
  65. package/i18n/pl.json +6 -0
  66. package/package.json +97 -0
  67. package/tailwind.css +14 -0
@@ -0,0 +1,91 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ import { Entity, Index, OptionalProps, PrimaryKey, Property } from '@mikro-orm/core';
11
+ import { GlobalEntity } from '@endora-commerce/platform/tenancy';
12
+ import { randomUUID } from 'crypto';
13
+ /**
14
+ * Outbound webhook subscription (FR-121).
15
+ *
16
+ * - `eventTypes` array gates which events trigger a delivery for this row.
17
+ * - `secret` is shared with the receiver so they can verify the HMAC the
18
+ * delivery worker stamps on every POST body.
19
+ * - `status='paused'` stops new deliveries without deleting historical
20
+ * WebhookDelivery rows.
21
+ */
22
+ let Webhook = class Webhook {
23
+ [OptionalProps];
24
+ id = randomUUID();
25
+ name;
26
+ url;
27
+ /** Shared secret, stored plaintext (already at rest in the DB; the HMAC is
28
+ * what protects payload integrity in transit). 64 hex chars by default. */
29
+ secret;
30
+ eventTypes = [];
31
+ status = 'active';
32
+ createdByAdminUserId;
33
+ /**
34
+ * Feature 062 — delivery filter (data-model.md §3). NULL = platform-wide
35
+ * subscription (legacy semantics); a value restricts delivery to events
36
+ * whose payload `organizationId` matches. Evaluated in the event-bridge
37
+ * lookup; the row itself stays `@GlobalEntity()` (admin-managed platform
38
+ * configuration — research §R12).
39
+ */
40
+ organizationId;
41
+ createdAt = new Date();
42
+ updatedAt = new Date();
43
+ };
44
+ __decorate([
45
+ PrimaryKey({ type: 'uuid' }),
46
+ __metadata("design:type", String)
47
+ ], Webhook.prototype, "id", void 0);
48
+ __decorate([
49
+ Property({ type: 'string', length: 160 }),
50
+ __metadata("design:type", String)
51
+ ], Webhook.prototype, "name", void 0);
52
+ __decorate([
53
+ Property({ type: 'string', length: 2048 }),
54
+ __metadata("design:type", String)
55
+ ], Webhook.prototype, "url", void 0);
56
+ __decorate([
57
+ Property({ type: 'string', length: 128 }),
58
+ __metadata("design:type", String)
59
+ ], Webhook.prototype, "secret", void 0);
60
+ __decorate([
61
+ Property({ type: 'json' }),
62
+ __metadata("design:type", Array)
63
+ ], Webhook.prototype, "eventTypes", void 0);
64
+ __decorate([
65
+ Property({ type: 'string', length: 16 }),
66
+ Index(),
67
+ __metadata("design:type", String)
68
+ ], Webhook.prototype, "status", void 0);
69
+ __decorate([
70
+ Property({ type: 'uuid', nullable: true }),
71
+ __metadata("design:type", Object)
72
+ ], Webhook.prototype, "createdByAdminUserId", void 0);
73
+ __decorate([
74
+ Property({ type: 'uuid', nullable: true }),
75
+ Index(),
76
+ __metadata("design:type", Object)
77
+ ], Webhook.prototype, "organizationId", void 0);
78
+ __decorate([
79
+ Property({ type: 'datetime', onCreate: () => new Date() }),
80
+ __metadata("design:type", Date)
81
+ ], Webhook.prototype, "createdAt", void 0);
82
+ __decorate([
83
+ Property({ type: 'datetime', onUpdate: () => new Date() }),
84
+ __metadata("design:type", Date)
85
+ ], Webhook.prototype, "updatedAt", void 0);
86
+ Webhook = __decorate([
87
+ GlobalEntity(),
88
+ Entity({ tableName: 'webhooks' })
89
+ ], Webhook);
90
+ export { Webhook };
91
+ //# sourceMappingURL=webhook.entity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook.entity.js","sourceRoot":"","sources":["../../../src/backend/entities/webhook.entity.ts"],"names":[],"mappings":";;;;;;;;;AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AACrF,OAAO,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AACjE,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAEpC;;;;;;;;GAQG;AAGI,IAAM,OAAO,GAAb,MAAM,OAAO;IAClB,CAAC,aAAa,CAAC,CAOM;IAGrB,EAAE,GAAW,UAAU,EAAE,CAAC;IAG1B,IAAI,CAAU;IAGd,GAAG,CAAU;IAEb;gFAC4E;IAE5E,MAAM,CAAU;IAGhB,UAAU,GAAa,EAAE,CAAC;IAI1B,MAAM,GAAwB,QAAQ,CAAC;IAGvC,oBAAoB,CAAiB;IAErC;;;;;;OAMG;IAGH,cAAc,CAAiB;IAG/B,SAAS,GAAS,IAAI,IAAI,EAAE,CAAC;IAG7B,SAAS,GAAS,IAAI,IAAI,EAAE,CAAC;CAC9B,CAAA;AAvCC;IADC,UAAU,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;;mCACH;AAG1B;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;;qCAC5B;AAGd;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;;oCAC9B;AAKb;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;;uCAC1B;AAGhB;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;;2CACD;AAI1B;IAFC,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;IACxC,KAAK,EAAE;;uCAC+B;AAGvC;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;qDACN;AAWrC;IAFC,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1C,KAAK,EAAE;;+CACuB;AAG/B;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,EAAE,CAAC;8BAChD,IAAI;0CAAc;AAG7B;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,EAAE,CAAC;8BAChD,IAAI;0CAAc;AAjDlB,OAAO;IAFnB,YAAY,EAAE;IACd,MAAM,CAAC,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC;GACrB,OAAO,CAkDnB"}
@@ -0,0 +1,85 @@
1
+ import type { FastifyRequest } from 'fastify';
2
+ import type { Queue, Worker } from 'bullmq';
3
+ import type { EntityManager } from '@mikro-orm/postgresql';
4
+ import type { AuditPort } from '@endora-commerce/platform/kernel';
5
+ import { type ModuleContext } from '@endora-commerce/platform/kernel';
6
+ import type { RequireAdminFactory } from '@endora-commerce/platform/kernel';
7
+ import { WebhookService } from './services/webhook-service.js';
8
+ import { type WebhookJobData } from './services/webhook-queue.js';
9
+ import { WebhookDelivery } from './entities/webhook-delivery.entity.js';
10
+ import { Webhook } from './entities/webhook.entity.js';
11
+ /**
12
+ * `webhooks` — the module whose subscriptions are rows, not registrations
13
+ * (feature 072, wave 1, T098).
14
+ *
15
+ * `api_keys/plugin.ts` built the `WebhookService` and mounted the admin routes;
16
+ * `composition.ts` created the queue, wired the EventBus bridge and started the
17
+ * delivery worker. Three files for one module, none of them its own.
18
+ *
19
+ * **The gating question this module poses is the interesting part.** A webhook
20
+ * subscription is a database row an operator creates at runtime, so there is no
21
+ * registration seam per subscription to wrap — the obvious reading of
22
+ * Constitution XVII ("gate every subscription") has nothing to attach to. But
23
+ * the module owns exactly **two** EventBus subscriptions, one per bridged event
24
+ * type, and every delivery in the platform passes through them. Gating the
25
+ * bridge stops all of it at one seam, and does so without consulting the
26
+ * database: while the module is off, the subscription lookup is never reached,
27
+ * so no row is read and no job is enqueued.
28
+ *
29
+ * That also gives the right off-state semantics. Switching `webhooks` off is
30
+ * not uninstalling: the subscriptions stay in the table, deliveries already
31
+ * recorded stay in `webhook_deliveries`, and switching it back on resumes
32
+ * bridging without the operator re-creating anything. Events emitted while it
33
+ * was off are simply not delivered, which is what "behaves as if never
34
+ * installed" means for a module whose job is to react to events.
35
+ *
36
+ * **The delivery consumer came home in T143a cluster 6.** T098 left it in
37
+ * `composition.ts` on the grounds that "whether workers run at all is a
38
+ * deployment decision" — true of the *flag*, and not of the worker. Built by a
39
+ * root it was the one part of this module no lifecycle seam covered: the
40
+ * bridge stopped enqueuing when the module went off, while the consumer kept
41
+ * draining whatever was already on the queue and kept writing
42
+ * `webhook_deliveries` rows. `ctx.worker` puts it behind `defineModuleWorker`,
43
+ * which pauses it with the module and resumes it with it. The deployment half
44
+ * stays a root's, as `webhooksRunWorkers` — the same shape `searchRunWorkers`
45
+ * already has, and for the same reason: the harness runs no consumer, and
46
+ * deriving that from `BACKEND_ROLE` here would start one in every test file.
47
+ */
48
+ /** The events bridged to the delivery queue (contracts/order-webhooks.md §2). */
49
+ export declare const BRIDGED_EVENT_TYPES: readonly ["order.created.v1", "order.status_changed.v1"];
50
+ export interface WebhooksCradle {
51
+ readonly emFactory: () => EntityManager;
52
+ readonly auditLogService: AuditPort;
53
+ readonly requireAdmin: RequireAdminFactory;
54
+ readonly redis: import('ioredis').Redis;
55
+ /**
56
+ * Whether this composition runs the delivery consumer (Principle X).
57
+ * Root-supplied: production follows `BACKEND_ROLE`, the harness runs none.
58
+ */
59
+ readonly webhooksRunWorkers: boolean;
60
+ readonly webhookService: WebhookService;
61
+ readonly webhookQueue: Queue<WebhookJobData>;
62
+ readonly webhookDeliveryWorker: Worker<WebhookJobData>;
63
+ /**
64
+ * Who the acting admin is, from the production actor (feature 080, T051).
65
+ * Root-supplied, like the other request resolvers; both roots answer it from
66
+ * `request.actor` and throw 401 for a non-admin.
67
+ */
68
+ readonly adminContextResolver: (req: FastifyRequest) => {
69
+ adminUserId: string;
70
+ };
71
+ }
72
+ export declare function registerModule(ctx: ModuleContext): void;
73
+ /**
74
+ * The module's persisted entity classes, on the `./backend` subpath, as one
75
+ * array and **no named class export** (D-168).
76
+ *
77
+ * This is the shape the platform reads when the package is *installed*: the
78
+ * boot-time loader (`src/packages/package-runtime.ts`, `exported['entities']`)
79
+ * and the static declaration reader (`scripts/lib/package-declarations.ts`),
80
+ * which is the third source of `check:module-boundary`'s `table→owner` map and
81
+ * the package pass of `check-entity-tenant-classification`. A missing array is
82
+ * answered with `[]` — zero entities registered, no error anywhere.
83
+ */
84
+ export declare const entities: (typeof Webhook | typeof WebhookDelivery)[];
85
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/backend/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC5C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAClE,OAAO,EAAY,KAAK,aAAa,EAAE,MAAM,kCAAkC,CAAC;AAChF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAG5E,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,EAGL,KAAK,cAAc,EACpB,MAAM,6BAA6B,CAAC;AAErC,OAAO,EAAE,eAAe,EAAE,MAAM,uCAAuC,CAAC;AACxE,OAAO,EAAE,OAAO,EAAE,MAAM,8BAA8B,CAAC;AAEvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,iFAAiF;AACjF,eAAO,MAAM,mBAAmB,0DAA2D,CAAC;AAE5F,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,aAAa,CAAC;IACxC,QAAQ,CAAC,eAAe,EAAE,SAAS,CAAC;IACpC,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAC3C,QAAQ,CAAC,KAAK,EAAE,OAAO,SAAS,EAAE,KAAK,CAAC;IACxC;;;OAGG;IACH,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAAC;IACrC,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAC;IACxC,QAAQ,CAAC,YAAY,EAAE,KAAK,CAAC,cAAc,CAAC,CAAC;IAC7C,QAAQ,CAAC,qBAAqB,EAAE,MAAM,CAAC,cAAc,CAAC,CAAC;IACvD;;;;OAIG;IACH,QAAQ,CAAC,oBAAoB,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK;QAAE,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC;CACjF;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAmFvD;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,QAAQ,6CAGpB,CAAC"}
@@ -0,0 +1,129 @@
1
+ import { lazyPort } from '@endora-commerce/platform/kernel';
2
+ import { registerWebhooksAdminRoutes } from './routes.js';
3
+ import { bridgeEventHandler } from './services/event-bridge.js';
4
+ import { WebhookService } from './services/webhook-service.js';
5
+ import { createWebhookQueue, createWebhookWorker, } from './services/webhook-queue.js';
6
+ import { createDeliveryProcessor } from './services/webhook-delivery-worker.js';
7
+ import { WebhookDelivery } from './entities/webhook-delivery.entity.js';
8
+ import { Webhook } from './entities/webhook.entity.js';
9
+ /**
10
+ * `webhooks` — the module whose subscriptions are rows, not registrations
11
+ * (feature 072, wave 1, T098).
12
+ *
13
+ * `api_keys/plugin.ts` built the `WebhookService` and mounted the admin routes;
14
+ * `composition.ts` created the queue, wired the EventBus bridge and started the
15
+ * delivery worker. Three files for one module, none of them its own.
16
+ *
17
+ * **The gating question this module poses is the interesting part.** A webhook
18
+ * subscription is a database row an operator creates at runtime, so there is no
19
+ * registration seam per subscription to wrap — the obvious reading of
20
+ * Constitution XVII ("gate every subscription") has nothing to attach to. But
21
+ * the module owns exactly **two** EventBus subscriptions, one per bridged event
22
+ * type, and every delivery in the platform passes through them. Gating the
23
+ * bridge stops all of it at one seam, and does so without consulting the
24
+ * database: while the module is off, the subscription lookup is never reached,
25
+ * so no row is read and no job is enqueued.
26
+ *
27
+ * That also gives the right off-state semantics. Switching `webhooks` off is
28
+ * not uninstalling: the subscriptions stay in the table, deliveries already
29
+ * recorded stay in `webhook_deliveries`, and switching it back on resumes
30
+ * bridging without the operator re-creating anything. Events emitted while it
31
+ * was off are simply not delivered, which is what "behaves as if never
32
+ * installed" means for a module whose job is to react to events.
33
+ *
34
+ * **The delivery consumer came home in T143a cluster 6.** T098 left it in
35
+ * `composition.ts` on the grounds that "whether workers run at all is a
36
+ * deployment decision" — true of the *flag*, and not of the worker. Built by a
37
+ * root it was the one part of this module no lifecycle seam covered: the
38
+ * bridge stopped enqueuing when the module went off, while the consumer kept
39
+ * draining whatever was already on the queue and kept writing
40
+ * `webhook_deliveries` rows. `ctx.worker` puts it behind `defineModuleWorker`,
41
+ * which pauses it with the module and resumes it with it. The deployment half
42
+ * stays a root's, as `webhooksRunWorkers` — the same shape `searchRunWorkers`
43
+ * already has, and for the same reason: the harness runs no consumer, and
44
+ * deriving that from `BACKEND_ROLE` here would start one in every test file.
45
+ */
46
+ /** The events bridged to the delivery queue (contracts/order-webhooks.md §2). */
47
+ export const BRIDGED_EVENT_TYPES = ['order.created.v1', 'order.status_changed.v1'];
48
+ export function registerModule(ctx) {
49
+ ctx.di.providePort('webhookService', ctx
50
+ .asFunction(({ emFactory, auditLogService }) => new WebhookService(emFactory, auditLogService))
51
+ .singleton());
52
+ ctx.di.register({
53
+ webhookQueue: ctx
54
+ .asFunction(({ redis }) => createWebhookQueue(redis))
55
+ .singleton()
56
+ .disposer((queue) => queue.close().catch(() => undefined)),
57
+ /**
58
+ * The delivery consumer: HMAC signing, retries and the
59
+ * `webhook_deliveries` bookkeeping (T143a).
60
+ *
61
+ * A registration rather than a `new` inside the route body, so it carries
62
+ * a **disposer** exactly as the queue does — a composition that disposes
63
+ * its container drains the worker before the Redis sockets go, which is
64
+ * what the graceful shutdown in `composition.ts` used to spell by hand.
65
+ * Nothing resolves it unless `webhooksRunWorkers` is true, and a
66
+ * registration nobody resolves is never constructed, so a composition that
67
+ * runs no consumer pays nothing for this.
68
+ */
69
+ webhookDeliveryWorker: ctx
70
+ .asFunction(({ redis }) => createWebhookWorker(redis, createDeliveryProcessor({
71
+ // Read per call rather than captured: `webhookService` is this
72
+ // module's own gated port, and a job draining mid-flight must
73
+ // still meet the gate.
74
+ recordDelivery: async (input) => {
75
+ await ctx.cradle().webhookService.recordDelivery(input);
76
+ },
77
+ })))
78
+ .singleton()
79
+ .disposer((worker) => worker.close().catch(() => undefined)),
80
+ });
81
+ for (const eventType of BRIDGED_EVENT_TYPES) {
82
+ // One `ctx.subscribe` per bridged type. The kernel's wrapper is what makes
83
+ // the module's effective state decide whether the handler runs at all, so
84
+ // the lookup below is never reached while the module is off.
85
+ ctx.subscribe(eventType, (payload) => {
86
+ const { webhookQueue, webhookService } = ctx.cradle();
87
+ return bridgeEventHandler(eventType, {
88
+ queue: webhookQueue,
89
+ subscriptionLookup: webhookService,
90
+ })(payload);
91
+ });
92
+ }
93
+ ctx.routes(async (app) => {
94
+ const { requireAdmin, webhooksRunWorkers } = ctx.cradle();
95
+ // `ctx.worker` wraps it in `defineModuleWorker`, so it starts paused when
96
+ // the module is off and the orchestrator pauses and resumes it as the
97
+ // operator flips the module. Attached here rather than at registration
98
+ // because that is where `app.log` exists — the same place
99
+ // `google_analytics` attaches its delivery consumer.
100
+ if (webhooksRunWorkers) {
101
+ ctx.worker(ctx.cradle().webhookDeliveryWorker, { logger: app.log });
102
+ }
103
+ await registerWebhooksAdminRoutes(app, {
104
+ // Lazily, even though the module owns this port: route *registration* runs
105
+ // inside `buildServer` whatever the module's effective state is, so
106
+ // destructuring the gate here would stop the next start instead of
107
+ // stopping the routes (D-40).
108
+ webhookService: lazyPort(ctx, 'webhookService'),
109
+ requireAdmin,
110
+ resolveAdminUserId: (request) => ctx.cradle().adminContextResolver(request).adminUserId,
111
+ });
112
+ });
113
+ }
114
+ /**
115
+ * The module's persisted entity classes, on the `./backend` subpath, as one
116
+ * array and **no named class export** (D-168).
117
+ *
118
+ * This is the shape the platform reads when the package is *installed*: the
119
+ * boot-time loader (`src/packages/package-runtime.ts`, `exported['entities']`)
120
+ * and the static declaration reader (`scripts/lib/package-declarations.ts`),
121
+ * which is the third source of `check:module-boundary`'s `table→owner` map and
122
+ * the package pass of `check-entity-tenant-classification`. A missing array is
123
+ * answered with `[]` — zero entities registered, no error anywhere.
124
+ */
125
+ export const entities = [
126
+ WebhookDelivery,
127
+ Webhook,
128
+ ];
129
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/backend/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ import type { FastifyInstance, FastifyRequest } from 'fastify';
2
+ import type { WebhookService } from './services/webhook-service.js';
3
+ import type { RequireAdminFactory } from '@endora-commerce/platform/kernel';
4
+ export interface WebhooksAdminDeps {
5
+ webhookService: WebhookService;
6
+ requireAdmin: RequireAdminFactory;
7
+ /**
8
+ * The acting admin's id, resolved from the request the guard in front of
9
+ * these routes has already accepted.
10
+ *
11
+ * Injected rather than read here, because the read used to be
12
+ * `testAdminUserId(request)` — `http/test-actor-carrier`, which
13
+ * `contracts/host-package.md` §1.4j classifies **A**: the file exists to
14
+ * narrow this repository's test-harness Fastify augmentation, and an
15
+ * installed package has no relationship to that harness. It also answered
16
+ * `undefined` in production for every request, because nothing under `src/`
17
+ * writes `request.testActor`.
18
+ *
19
+ * Both composition roots supply it from `adminContextResolver`, which reads
20
+ * the production actor and throws 401 for a non-admin. Every call site sits
21
+ * behind `requireAdmin(...)`, so the actor is an admin by the time it runs.
22
+ */
23
+ resolveAdminUserId: (request: FastifyRequest) => string;
24
+ }
25
+ export declare function registerWebhooksAdminRoutes(app: FastifyInstance, deps: WebhooksAdminDeps): Promise<void>;
26
+ //# sourceMappingURL=routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/backend/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAGpE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAE5E,MAAM,WAAW,iBAAiB;IAChC,cAAc,EAAE,cAAc,CAAC;IAC/B,YAAY,EAAE,mBAAmB,CAAC;IAClC;;;;;;;;;;;;;;;OAeG;IACH,kBAAkB,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,MAAM,CAAC;CACzD;AAED,wBAAsB,2BAA2B,CAC/C,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,iBAAiB,GACtB,OAAO,CAAC,IAAI,CAAC,CAmFf"}
@@ -0,0 +1,84 @@
1
+ import { createWebhookRequestSchema, updateWebhookRequestSchema } from '@endora-commerce/contracts';
2
+ export async function registerWebhooksAdminRoutes(app, deps) {
3
+ const { webhookService, requireAdmin, resolveAdminUserId } = deps;
4
+ app.get('/api/v1/admin/webhooks', { preHandler: requireAdmin('integrations:manage') }, async () => {
5
+ const rows = await webhookService.list();
6
+ return { data: rows.map(serializeWebhook) };
7
+ });
8
+ app.post('/api/v1/admin/webhooks', {
9
+ preHandler: requireAdmin('integrations:manage'),
10
+ schema: { body: createWebhookRequestSchema },
11
+ }, async (request, reply) => {
12
+ const body = createWebhookRequestSchema.parse(request.body);
13
+ const w = await webhookService.create({
14
+ name: body.name,
15
+ url: body.url,
16
+ eventTypes: body.eventTypes,
17
+ // Feature 062 — optional organization binding (additive).
18
+ organizationId: body.organizationId ?? null,
19
+ createdByAdminUserId: resolveAdminUserId(request),
20
+ });
21
+ reply.status(201);
22
+ // Reveal the signing secret once, on creation, so the operator can share
23
+ // it with the receiving system (it is never returned by the list/update
24
+ // endpoints again).
25
+ return { data: { ...serializeWebhook(w), secret: w.secret } };
26
+ });
27
+ app.patch('/api/v1/admin/webhooks/:id', {
28
+ preHandler: requireAdmin('integrations:manage'),
29
+ schema: { body: updateWebhookRequestSchema },
30
+ }, async (request) => {
31
+ const body = updateWebhookRequestSchema.parse(request.body);
32
+ const w = await webhookService.update(request.params.id, body);
33
+ return { data: serializeWebhook(w) };
34
+ });
35
+ app.delete('/api/v1/admin/webhooks/:id', { preHandler: requireAdmin('integrations:manage') }, async (request, reply) => {
36
+ await webhookService.remove(request.params.id);
37
+ return reply.status(204).send();
38
+ });
39
+ app.get('/api/v1/admin/webhooks/deliveries', { preHandler: requireAdmin('integrations:manage') }, async (request) => {
40
+ const { webhookId, status, limit } = request.query;
41
+ const rows = await webhookService.listDeliveries({
42
+ ...(webhookId ? { webhookId } : {}),
43
+ ...(status
44
+ ? { status: status }
45
+ : {}),
46
+ ...(limit ? { limit: Number(limit) } : {}),
47
+ });
48
+ return { data: rows.map(serializeDelivery) };
49
+ });
50
+ app.post('/api/v1/admin/webhooks/deliveries/:id/replay', { preHandler: requireAdmin('integrations:manage') }, async (request, reply) => {
51
+ const copy = await webhookService.replay(request.params.id);
52
+ reply.status(202);
53
+ return { data: serializeDelivery(copy) };
54
+ });
55
+ }
56
+ function serializeWebhook(w) {
57
+ return {
58
+ id: w.id,
59
+ name: w.name,
60
+ url: w.url,
61
+ eventTypes: w.eventTypes,
62
+ status: w.status,
63
+ organizationId: w.organizationId ?? null,
64
+ createdAt: w.createdAt.toISOString(),
65
+ updatedAt: w.updatedAt.toISOString(),
66
+ };
67
+ }
68
+ function serializeDelivery(d) {
69
+ return {
70
+ id: d.id,
71
+ webhookId: d.webhookId,
72
+ eventId: d.eventId,
73
+ eventType: d.eventType,
74
+ status: d.status,
75
+ attemptCount: d.attemptCount,
76
+ dispatchedAt: d.dispatchedAt?.toISOString() ?? null,
77
+ completedAt: d.completedAt?.toISOString() ?? null,
78
+ deadLetteredAt: d.deadLetteredAt?.toISOString() ?? null,
79
+ lastResponseStatus: d.lastResponseStatus ?? null,
80
+ lastError: d.lastError ?? null,
81
+ createdAt: d.createdAt.toISOString(),
82
+ };
83
+ }
84
+ //# sourceMappingURL=routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.js","sourceRoot":"","sources":["../../src/backend/routes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,54 @@
1
+ import type { Queue } from 'bullmq';
2
+ import type { EventBase } from '@endora-commerce/platform/events';
3
+ import type { WebhookJobData } from './webhook-queue.js';
4
+ /**
5
+ * Bridges the in-process event bus (R-17) to the durable webhook queue (R-13).
6
+ *
7
+ * On each emitted domain event:
8
+ * 1. Look up active Webhook subscriptions that match the event type.
9
+ * 2. For every subscriber, enqueue one BullMQ job on the webhook.deliver queue.
10
+ *
11
+ * The actual lookup of Webhook rows is injected; this keeps the bridge independent
12
+ * of MikroORM and easy to test.
13
+ */
14
+ export interface SubscriptionLookup {
15
+ /**
16
+ * Feature 062 — org-scoped delivery (contracts/order-webhooks.md §2).
17
+ * `organizationId` is the value extracted from the event payload (or null
18
+ * when the payload carries none). Implementations return active
19
+ * subscriptions matching the event type where the subscription is
20
+ * platform-wide (`organizationId` NULL, legacy semantics — FR-015) OR bound
21
+ * to exactly this organization. An event without an organization must never
22
+ * reach an org-scoped subscription (fail closed — Principle XI).
23
+ */
24
+ findActiveByEventType(eventType: string, organizationId: string | null): Promise<Array<{
25
+ webhookId: string;
26
+ url: string;
27
+ secret: string;
28
+ }>>;
29
+ }
30
+ /**
31
+ * Delivery-filter predicate shared by the subscription lookup: a subscription
32
+ * receives the event iff it is platform-wide, or the event carries an
33
+ * organization and it matches the subscription's binding.
34
+ */
35
+ export declare function subscriptionReceivesOrganization(subscriptionOrganizationId: string | null, eventOrganizationId: string | null): boolean;
36
+ export interface EventBridgeOptions {
37
+ queue: Pick<Queue<WebhookJobData>, 'add'>;
38
+ subscriptionLookup: SubscriptionLookup;
39
+ }
40
+ export interface BridgedEventPayload extends EventBase {
41
+ [key: string]: unknown;
42
+ }
43
+ /**
44
+ * The per-event-type half of the bridge, extracted so a converted module can
45
+ * hand it to `ctx.subscribe` (feature 072, T098).
46
+ *
47
+ * That is the whole gating story for this module. A webhook subscription is a
48
+ * database row created at runtime, so there is no registration seam per
49
+ * subscription to gate — but there are exactly **two** EventBus subscriptions,
50
+ * and gating those stops every delivery at once. Gate the bridge, not the
51
+ * subscriptions.
52
+ */
53
+ export declare function bridgeEventHandler(eventType: string, deps: EventBridgeOptions): (payload: unknown) => Promise<void>;
54
+ //# sourceMappingURL=event-bridge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-bridge.d.ts","sourceRoot":"","sources":["../../../src/backend/services/event-bridge.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AACpC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAClE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzD;;;;;;;;;GASG;AAEH,MAAM,WAAW,kBAAkB;IACjC;;;;;;;;OAQG;IACH,qBAAqB,CACnB,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,GAAG,IAAI,GAC5B,OAAO,CAAC,KAAK,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAC;CACvE;AAED;;;;GAIG;AACH,wBAAgB,gCAAgC,CAC9C,0BAA0B,EAAE,MAAM,GAAG,IAAI,EACzC,mBAAmB,EAAE,MAAM,GAAG,IAAI,GACjC,OAAO,CAGT;AAQD,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,EAAE,KAAK,CAAC,CAAC;IAC1C,kBAAkB,EAAE,kBAAkB,CAAC;CACxC;AAED,MAAM,WAAW,mBAAoB,SAAQ,SAAS;IAGpD,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,kBAAkB,GACvB,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAkBrC"}
@@ -0,0 +1,43 @@
1
+ import { randomUUID } from 'crypto';
2
+ /**
3
+ * Delivery-filter predicate shared by the subscription lookup: a subscription
4
+ * receives the event iff it is platform-wide, or the event carries an
5
+ * organization and it matches the subscription's binding.
6
+ */
7
+ export function subscriptionReceivesOrganization(subscriptionOrganizationId, eventOrganizationId) {
8
+ if (subscriptionOrganizationId === null)
9
+ return true;
10
+ return eventOrganizationId !== null && subscriptionOrganizationId === eventOrganizationId;
11
+ }
12
+ /** Extract the tenant key from a bridged event payload; non-string values count as absent. */
13
+ function extractOrganizationId(payload) {
14
+ const value = payload['organizationId'];
15
+ return typeof value === 'string' && value.length > 0 ? value : null;
16
+ }
17
+ /**
18
+ * The per-event-type half of the bridge, extracted so a converted module can
19
+ * hand it to `ctx.subscribe` (feature 072, T098).
20
+ *
21
+ * That is the whole gating story for this module. A webhook subscription is a
22
+ * database row created at runtime, so there is no registration seam per
23
+ * subscription to gate — but there are exactly **two** EventBus subscriptions,
24
+ * and gating those stops every delivery at once. Gate the bridge, not the
25
+ * subscriptions.
26
+ */
27
+ export function bridgeEventHandler(eventType, deps) {
28
+ return async (payload) => {
29
+ const subs = await deps.subscriptionLookup.findActiveByEventType(eventType, extractOrganizationId(payload));
30
+ for (const sub of subs) {
31
+ const jobData = {
32
+ webhookId: sub.webhookId,
33
+ eventId: payload.eventId ?? randomUUID(),
34
+ eventType,
35
+ payload,
36
+ url: sub.url,
37
+ secret: sub.secret,
38
+ };
39
+ await deps.queue.add(`${eventType}.${jobData.eventId}`, jobData);
40
+ }
41
+ };
42
+ }
43
+ //# sourceMappingURL=event-bridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-bridge.js","sourceRoot":"","sources":["../../../src/backend/services/event-bridge.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAgCpC;;;;GAIG;AACH,MAAM,UAAU,gCAAgC,CAC9C,0BAAyC,EACzC,mBAAkC;IAElC,IAAI,0BAA0B,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACrD,OAAO,mBAAmB,KAAK,IAAI,IAAI,0BAA0B,KAAK,mBAAmB,CAAC;AAC5F,CAAC;AAED,8FAA8F;AAC9F,SAAS,qBAAqB,CAAC,OAAgB;IAC7C,MAAM,KAAK,GAAI,OAAmC,CAAC,gBAAgB,CAAC,CAAC;IACrE,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;AACtE,CAAC;AAaD;;;;;;;;;GASG;AACH,MAAM,UAAU,kBAAkB,CAChC,SAAiB,EACjB,IAAwB;IAExB,OAAO,KAAK,EAAE,OAAO,EAAE,EAAE;QACvB,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,kBAAkB,CAAC,qBAAqB,CAC9D,SAAS,EACT,qBAAqB,CAAC,OAAO,CAAC,CAC/B,CAAC;QACF,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;YACvB,MAAM,OAAO,GAAmB;gBAC9B,SAAS,EAAE,GAAG,CAAC,SAAS;gBACxB,OAAO,EAAG,OAA+B,CAAC,OAAO,IAAI,UAAU,EAAE;gBACjE,SAAS;gBACT,OAAO;gBACP,GAAG,EAAE,GAAG,CAAC,GAAG;gBACZ,MAAM,EAAE,GAAG,CAAC,MAAM;aACnB,CAAC;YACF,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,SAAS,IAAI,OAAO,CAAC,OAAO,EAAE,EAAE,OAAO,CAAC,CAAC;QACnE,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,34 @@
1
+ import type { Job } from 'bullmq';
2
+ import type { WebhookJobData } from './webhook-queue.js';
3
+ /**
4
+ * Worker-side processor: signs the payload with HMAC-SHA-256, POSTs it to the
5
+ * receiver URL, and throws on non-2xx so BullMQ retries (R-13).
6
+ */
7
+ export interface DeliveryRecordInput {
8
+ webhookId: string;
9
+ eventId: string;
10
+ eventType: string;
11
+ payload: unknown;
12
+ status: 'succeeded' | 'failed' | 'dead_lettered';
13
+ attemptCount: number;
14
+ lastResponseStatus?: number;
15
+ lastError?: string;
16
+ }
17
+ export interface ProcessorDependencies {
18
+ /** Injectable for tests; defaults to globalThis.fetch at call time. */
19
+ fetchFn?: typeof fetch;
20
+ /** Per-attempt timeout in ms. */
21
+ timeoutMs?: number;
22
+ /** Logger for failed deliveries (pino.Logger compatible). */
23
+ onFailure?: (err: unknown, job: Job<WebhookJobData>) => void;
24
+ /**
25
+ * Feature 062 — persistence hook for the `webhook_deliveries` audit trail
26
+ * (typically `WebhookService.recordDelivery`). One row per attempt outcome:
27
+ * `succeeded` on 2xx, `failed` on a retryable attempt, `dead_lettered` on
28
+ * the final exhausted attempt. Best-effort: a bookkeeping failure never
29
+ * changes the delivery outcome.
30
+ */
31
+ recordDelivery?: (input: DeliveryRecordInput) => Promise<unknown>;
32
+ }
33
+ export declare function createDeliveryProcessor(deps?: ProcessorDependencies): (job: Job<WebhookJobData>) => Promise<void>;
34
+ //# sourceMappingURL=webhook-delivery-worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-delivery-worker.d.ts","sourceRoot":"","sources":["../../../src/backend/services/webhook-delivery-worker.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,QAAQ,CAAC;AAClC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzD;;;GAGG;AAEH,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,WAAW,GAAG,QAAQ,GAAG,eAAe,CAAC;IACjD,YAAY,EAAE,MAAM,CAAC;IACrB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,qBAAqB;IACpC,uEAAuE;IACvE,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IACvB,iCAAiC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,CAAC,cAAc,CAAC,KAAK,IAAI,CAAC;IAC7D;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACnE;AAED,wBAAgB,uBAAuB,CAAC,IAAI,GAAE,qBAA0B,IAYxC,KAAK,GAAG,CAAC,cAAc,CAAC,KAAG,OAAO,CAAC,IAAI,CAAC,CAsDvE"}