@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,122 @@
1
+ import { defineModuleManifest } from '@endora-commerce/contracts';
2
+ /**
3
+ * Webhooks module — manifest backfill (Module Lifecycle, feature 018).
4
+ *
5
+ * Predates the lifecycle system; this manifest is the static record
6
+ * required so the module participates in the registry. No install /
7
+ * uninstall hook today — the module's schema is owned by earlier
8
+ * platform-wide migrations.
9
+ */
10
+ export const manifest = defineModuleManifest({
11
+ id: 'webhooks',
12
+ name: 'Webhooks',
13
+ description: 'Outbound webhook subscription registry and dispatcher.',
14
+ version: '1.0.0',
15
+ // `auth` owns the `requireAdmin` port the admin routes are gated by; the
16
+ // edge became real with the conversion (feature 072, T098).
17
+ // `webhooks.organization_id` scopes a subscription to a tenant — a real
18
+ // foreign key, surfaced by feature 072 exporting this module's entities.
19
+ dependencies: ['auth', 'organizations'],
20
+ settings: {
21
+ moduleCode: 'webhooks',
22
+ groups: [{ code: 'webhooks', name: 'Webhooks' }],
23
+ settings: [
24
+ {
25
+ // Feature 073 — the operator's activation control. Platform-wide.
26
+ code: 'webhooks.enabled',
27
+ name: 'Webhooks enabled',
28
+ description: 'Switches outbound webhook delivery and its admin screens on or off. While off no event is bridged and no delivery is attempted; events emitted during that time are not delivered retroactively. Nothing is deleted — subscriptions and delivery history are preserved and resume when you switch it back on.',
29
+ groupCode: 'webhooks',
30
+ valueType: 'boolean',
31
+ defaultValue: true,
32
+ },
33
+ ],
34
+ },
35
+ // Issue #213 — `integrations:manage` is a **shared** gate: it guards this
36
+ // module's admin surface and `api_keys`', and the core `PERMISSION_CATALOGUE`
37
+ // row that carries its label names `api_keys` alone. Declaring it here makes
38
+ // this module a second *owner*, so the presence filter on `/admin-roles` keeps
39
+ // it grantable while either surface is on. Without this line, switching
40
+ // `api_keys` off would take the code off the role editor while every route in
41
+ // `webhooks/routes.ts` went on enforcing it — a gate nobody can be granted.
42
+ permissions: [{ code: 'integrations:manage', label: 'Manage API keys + webhooks' }],
43
+ /**
44
+ * The module's command-palette entry — feature 091, Phase 4 (the plan's
45
+ * batch 6), and one of the fifteen Principle XVI entries
46
+ * `specs/deferred-defects.md` records as owed.
47
+ *
48
+ * It arrives with the drain rather than before it, by the mechanism that
49
+ * register predicts: the batch owes an off-state proof over every surface the
50
+ * module contributes, and until this declaration existed the `AppShell.tsx`
51
+ * `PALETTE_ITEMS` row that advertised /webhooks was the admin's own — a
52
+ * hand-written copy no server-side presence check was ever asked about, so it
53
+ * went on offering the screen to an operator who had switched the module off.
54
+ * The Actions group is resolved by `AdminActionsService` against the effective
55
+ * enabled-set, which is what makes the withdrawal real.
56
+ *
57
+ * `requiredPermission` is the code the target route enforces, which
58
+ * `check:action-route-permissions` compares against the registration on
59
+ * `GET /api/v1/admin/webhooks` itself. It is `integrations:manage` and not a
60
+ * `webhooks:`-prefixed code because that is the gate the module actually
61
+ * has: issue #213 records why both owners declare it.
62
+ */
63
+ actions: [
64
+ {
65
+ id: 'open-webhooks',
66
+ labelKey: 'actions.openWebhooks.label',
67
+ descriptionKey: 'actions.openWebhooks.description',
68
+ icon: 'Webhook',
69
+ targetRoute: '/webhooks',
70
+ requiredPermission: 'integrations:manage',
71
+ keywords: ['webhook', 'webhooks', 'webhooki', 'events', 'zdarzenia', 'signing secret', 'integration', 'integracja'],
72
+ weight: 800,
73
+ },
74
+ ],
75
+ activation: { settingCode: 'webhooks.enabled', default: true },
76
+ /**
77
+ * This module's first i18n bundle — D-129's remaining sweep, MR 5.
78
+ *
79
+ * It exists because a **declaring** module that contributes no bundle file is
80
+ * `check:error-translations` exit 2 for the whole tree rather than a finding
81
+ * (`specs/090-module-owned-error-codes/contracts/error-translation-population.md`
82
+ * §2.1, §2.3; `d129-sweep.md` §3.4). The bundle lives at the **package
83
+ * root**, not under `dist`: `manifest-locations.ts` resolves a packaged
84
+ * module's `manifestPath` to its `package.json`, so `dirname` is the package
85
+ * directory. The file is a **flat** `{"a.b.c": "text"}` map, because a nested
86
+ * object fails `TranslationBundleEntriesSchema` and the boot reconciler logs
87
+ * and skips it — silently.
88
+ */
89
+ i18n: { bundlesDir: 'i18n' },
90
+ docs: { dir: 'docs' },
91
+ /**
92
+ * `WEBHOOK_DELIVERY_NOT_REPLAYABLE` — D-129's remaining sweep, Tier B
93
+ * (`specs/090-module-owned-error-codes/d129-sweep.md` §5.2, Appendix A;
94
+ * MR 5).
95
+ *
96
+ * It was declared by `_i18n` until this merge request, not because anybody
97
+ * judged it the platform's but because the deleted prefix chain had no rule
98
+ * for it and its last line was `return 'core'`. **D-121 T1 puts it here**:
99
+ * the noun is a webhook delivery, this module's own `webhook_deliveries` row,
100
+ * and the refusal is an invariant of that row's status — only `failed` and
101
+ * `dead_lettered` can be re-queued. This module is also the only one that
102
+ * raises it, from `services/webhook-service.ts`.
103
+ *
104
+ * **It arrives with a sentence.** It carried a placeholder in `_i18n`'s
105
+ * bundle — `"Webhook Delivery Not Replayable."` /
106
+ * `"Błąd: webhook delivery not replayable."` — which D-186 §2 deletes rather
107
+ * than carries, because in this module's own bundle it would read as this
108
+ * module's answer. §5.4 keeps writing the prose available and is not a
109
+ * re-opening of that ruling, and this is the case for taking it: the reader
110
+ * is an operator looking at the delivery row on the Webhooks screen, so the
111
+ * one thing the raise site's message carries that a fixed sentence cannot —
112
+ * `status=<status>` — is already on the screen beside the button they
113
+ * pressed.
114
+ *
115
+ * **`tokens` is derived from the raise sites, not from the bundle**
116
+ * (runbook §5), and there are none: the one raise is a bare
117
+ * `HttpError(409, code, message)` with no `details` argument, measured by
118
+ * balanced-paren extraction of the call's own arguments.
119
+ */
120
+ errorCodes: [{ code: 'WEBHOOK_DELIVERY_NOT_REPLAYABLE' }],
121
+ });
122
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAElE;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,oBAAoB,CAAC;IAC3C,EAAE,EAAE,UAAU;IACd,IAAI,EAAE,UAAU;IAChB,WAAW,EACT,wDAAwD;IAC1D,OAAO,EAAE,OAAO;IAChB,yEAAyE;IACzE,4DAA4D;IAC5D,wEAAwE;IACxE,yEAAyE;IACzE,YAAY,EAAE,CAAC,MAAM,EAAE,eAAe,CAAC;IACvC,QAAQ,EAAE;QACR,UAAU,EAAE,UAAU;QACtB,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;QAChD,QAAQ,EAAE;YACR;gBACE,kEAAkE;gBAClE,IAAI,EAAE,kBAAkB;gBACxB,IAAI,EAAE,kBAAkB;gBACxB,WAAW,EACT,+SAA+S;gBACjT,SAAS,EAAE,UAAU;gBACrB,SAAS,EAAE,SAAS;gBACpB,YAAY,EAAE,IAAI;aACnB;SACF;KACF;IACD,0EAA0E;IAC1E,8EAA8E;IAC9E,6EAA6E;IAC7E,+EAA+E;IAC/E,wEAAwE;IACxE,8EAA8E;IAC9E,4EAA4E;IAC5E,WAAW,EAAE,CAAC,EAAE,IAAI,EAAE,qBAAqB,EAAE,KAAK,EAAE,4BAA4B,EAAE,CAAC;IACnF;;;;;;;;;;;;;;;;;;;OAmBG;IACH,OAAO,EAAE;QACP;YACE,EAAE,EAAE,eAAe;YACnB,QAAQ,EAAE,4BAA4B;YACtC,cAAc,EAAE,kCAAkC;YAClD,IAAI,EAAE,SAAS;YACf,WAAW,EAAE,WAAW;YACxB,kBAAkB,EAAE,qBAAqB;YACzC,QAAQ,EAAE,CAAC,SAAS,EAAE,UAAU,EAAE,UAAU,EAAE,QAAQ,EAAE,WAAW,EAAE,gBAAgB,EAAE,aAAa,EAAE,YAAY,CAAC;YACnH,MAAM,EAAE,GAAG;SACZ;KACF;IACD,UAAU,EAAE,EAAE,WAAW,EAAE,kBAAkB,EAAE,OAAO,EAAE,IAAI,EAAE;IAC9D;;;;;;;;;;;;OAYG;IACH,IAAI,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE;IAC5B,IAAI,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE;IACrB;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4BG;IACH,UAAU,EAAE,CAAC,EAAE,IAAI,EAAE,iCAAiC,EAAE,CAAC;CAC1D,CAAC,CAAC"}
@@ -0,0 +1,22 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ /**
3
+ * US7 surface — Phase 9 (T225). Lands webhooks + webhook_deliveries +
4
+ * external_integrations.
5
+ *
6
+ * It also created `api_keys` until
7
+ * `specs/120-migration-closure-bridge-ownership/` Phase 3. The two modules were
8
+ * one surface when this was written and split afterwards, which left the
9
+ * creation stranded here: this module names `api_keys` in no statement of its
10
+ * own, and an instance that installs `api_keys` without `webhooks` had no
11
+ * migration that builds the table. Under D-226 the creation belongs to the
12
+ * module that owns the table, so it is
13
+ * `Migration20260724T173916ApiKeysDistributorBinding`'s now — its own frozen
14
+ * body, above its own `alter table`, and **not** a new migration above the
15
+ * watermark, which would have run after the two frozen migrations that
16
+ * reference `api_keys` and broken a fresh database (FR-014).
17
+ */
18
+ export declare class Migration20260425T091359WebhooksUs7Init extends Migration {
19
+ up(): Promise<void>;
20
+ down(): Promise<void>;
21
+ }
22
+ //# sourceMappingURL=20260425T091359_webhooks_us7_init.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260425T091359_webhooks_us7_init.d.ts","sourceRoot":"","sources":["../../src/migrations/20260425T091359_webhooks_us7_init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,uCAAwC,SAAQ,SAAS;IACrD,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;IAgEnB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAKrC"}
@@ -0,0 +1,86 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ /**
3
+ * US7 surface — Phase 9 (T225). Lands webhooks + webhook_deliveries +
4
+ * external_integrations.
5
+ *
6
+ * It also created `api_keys` until
7
+ * `specs/120-migration-closure-bridge-ownership/` Phase 3. The two modules were
8
+ * one surface when this was written and split afterwards, which left the
9
+ * creation stranded here: this module names `api_keys` in no statement of its
10
+ * own, and an instance that installs `api_keys` without `webhooks` had no
11
+ * migration that builds the table. Under D-226 the creation belongs to the
12
+ * module that owns the table, so it is
13
+ * `Migration20260724T173916ApiKeysDistributorBinding`'s now — its own frozen
14
+ * body, above its own `alter table`, and **not** a new migration above the
15
+ * watermark, which would have run after the two frozen migrations that
16
+ * reference `api_keys` and broken a fresh database (FR-014).
17
+ */
18
+ export class Migration20260425T091359WebhooksUs7Init extends Migration {
19
+ async up() {
20
+ this.addSql(`
21
+ create table "webhooks" (
22
+ "id" uuid not null,
23
+ "name" varchar(160) not null,
24
+ "url" varchar(2048) not null,
25
+ "secret" varchar(128) not null,
26
+ "event_types" jsonb not null default '[]'::jsonb,
27
+ "status" varchar(16) not null default 'active',
28
+ "created_by_admin_user_id" uuid null,
29
+ "created_at" timestamptz not null,
30
+ "updated_at" timestamptz not null,
31
+ constraint "webhooks_pkey" primary key ("id")
32
+ );
33
+ `);
34
+ this.addSql('create index "webhooks_status_index" on "webhooks" ("status");');
35
+ this.addSql(`
36
+ create table "webhook_deliveries" (
37
+ "id" uuid not null,
38
+ "webhook_id" uuid not null,
39
+ "event_id" varchar(64) not null,
40
+ "event_type" varchar(64) not null,
41
+ "payload" jsonb not null,
42
+ "status" varchar(16) not null default 'pending',
43
+ "attempt_count" int not null default 0,
44
+ "dispatched_at" timestamptz null,
45
+ "last_response_status" int null,
46
+ "last_response_body" varchar(4000) null,
47
+ "last_error" varchar(4000) null,
48
+ "completed_at" timestamptz null,
49
+ "dead_lettered_at" timestamptz null,
50
+ "created_at" timestamptz not null,
51
+ "updated_at" timestamptz not null,
52
+ constraint "webhook_deliveries_pkey" primary key ("id"),
53
+ constraint "webhook_deliveries_webhook_fk" foreign key ("webhook_id")
54
+ references "webhooks" ("id") on delete cascade
55
+ );
56
+ `);
57
+ this.addSql('create index "webhook_deliveries_webhook_id_index" on "webhook_deliveries" ("webhook_id");');
58
+ this.addSql('create index "webhook_deliveries_event_id_index" on "webhook_deliveries" ("event_id");');
59
+ this.addSql('create index "webhook_deliveries_event_type_index" on "webhook_deliveries" ("event_type");');
60
+ this.addSql('create index "webhook_deliveries_status_index" on "webhook_deliveries" ("status");');
61
+ this.addSql(`
62
+ create table "external_integrations" (
63
+ "id" uuid not null,
64
+ "name" varchar(160) not null,
65
+ "vendor" varchar(64) not null,
66
+ "kind" varchar(32) not null,
67
+ "encrypted_config" text not null,
68
+ "status" varchar(16) not null default 'inactive',
69
+ "last_tested_at" timestamptz null,
70
+ "last_error" varchar(4000) null,
71
+ "created_by_admin_user_id" uuid null,
72
+ "created_at" timestamptz not null,
73
+ "updated_at" timestamptz not null,
74
+ constraint "external_integrations_pkey" primary key ("id"),
75
+ constraint "external_integrations_vendor_unique" unique ("vendor")
76
+ );
77
+ `);
78
+ this.addSql('create index "external_integrations_vendor_index" on "external_integrations" ("vendor");');
79
+ }
80
+ async down() {
81
+ this.addSql('drop table if exists "external_integrations" cascade;');
82
+ this.addSql('drop table if exists "webhook_deliveries" cascade;');
83
+ this.addSql('drop table if exists "webhooks" cascade;');
84
+ }
85
+ }
86
+ //# sourceMappingURL=20260425T091359_webhooks_us7_init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260425T091359_webhooks_us7_init.js","sourceRoot":"","sources":["../../src/migrations/20260425T091359_webhooks_us7_init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,OAAO,uCAAwC,SAAQ,SAAS;IAC3D,KAAK,CAAC,EAAE;QACf,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;KAaX,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CAAC,gEAAgE,CAAC,CAAC;QAE9E,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;;;;;;KAqBX,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CAAC,4FAA4F,CAAC,CAAC;QAC1G,IAAI,CAAC,MAAM,CAAC,wFAAwF,CAAC,CAAC;QACtG,IAAI,CAAC,MAAM,CAAC,4FAA4F,CAAC,CAAC;QAC1G,IAAI,CAAC,MAAM,CAAC,oFAAoF,CAAC,CAAC;QAElG,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;KAgBX,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CAAC,0FAA0F,CAAC,CAAC;IAC1G,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,IAAI,CAAC,MAAM,CAAC,uDAAuD,CAAC,CAAC;QACrE,IAAI,CAAC,MAAM,CAAC,oDAAoD,CAAC,CAAC;QAClE,IAAI,CAAC,MAAM,CAAC,0CAA0C,CAAC,CAAC;IAC1D,CAAC;CACF"}
@@ -0,0 +1,17 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ /**
3
+ * Feature 062 — org-scoped webhook delivery (data-model.md §3).
4
+ *
5
+ * Adds `webhooks.organization_id uuid NULL`:
6
+ * - NULL = platform-wide subscription (legacy semantics preserved, FR-015);
7
+ * - value = the subscription only receives events whose payload
8
+ * `organizationId` matches (evaluated in the event-bridge lookup —
9
+ * research §R9; Principle XI).
10
+ *
11
+ * Existing rows stay NULL and behave exactly as before once delivery turns on.
12
+ */
13
+ export declare class Migration20260724T203140WebhooksOrgFilter extends Migration {
14
+ up(): Promise<void>;
15
+ down(): Promise<void>;
16
+ }
17
+ //# sourceMappingURL=20260724T203140_webhooks_org_filter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260724T203140_webhooks_org_filter.d.ts","sourceRoot":"","sources":["../../src/migrations/20260724T203140_webhooks_org_filter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;GAUG;AACH,qBAAa,yCAA0C,SAAQ,SAAS;IACvD,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;IAWnB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAKrC"}
@@ -0,0 +1,30 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ /**
3
+ * Feature 062 — org-scoped webhook delivery (data-model.md §3).
4
+ *
5
+ * Adds `webhooks.organization_id uuid NULL`:
6
+ * - NULL = platform-wide subscription (legacy semantics preserved, FR-015);
7
+ * - value = the subscription only receives events whose payload
8
+ * `organizationId` matches (evaluated in the event-bridge lookup —
9
+ * research §R9; Principle XI).
10
+ *
11
+ * Existing rows stay NULL and behave exactly as before once delivery turns on.
12
+ */
13
+ export class Migration20260724T203140WebhooksOrgFilter extends Migration {
14
+ async up() {
15
+ this.addSql(`alter table "webhooks" add column "organization_id" uuid null;`);
16
+ this.addSql(`
17
+ alter table "webhooks"
18
+ add constraint "webhooks_organization_id_foreign"
19
+ foreign key ("organization_id") references "organizations" ("id")
20
+ on update cascade on delete cascade;
21
+ `);
22
+ this.addSql(`create index "webhooks_organization_id_index" on "webhooks" ("organization_id");`);
23
+ }
24
+ async down() {
25
+ this.addSql(`drop index if exists "webhooks_organization_id_index";`);
26
+ this.addSql(`alter table "webhooks" drop constraint if exists "webhooks_organization_id_foreign";`);
27
+ this.addSql(`alter table "webhooks" drop column if exists "organization_id";`);
28
+ }
29
+ }
30
+ //# sourceMappingURL=20260724T203140_webhooks_org_filter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260724T203140_webhooks_org_filter.js","sourceRoot":"","sources":["../../src/migrations/20260724T203140_webhooks_org_filter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;GAUG;AACH,MAAM,OAAO,yCAA0C,SAAQ,SAAS;IAC7D,KAAK,CAAC,EAAE;QACf,IAAI,CAAC,MAAM,CAAC,gEAAgE,CAAC,CAAC;QAC9E,IAAI,CAAC,MAAM,CAAC;;;;;KAKX,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CAAC,kFAAkF,CAAC,CAAC;IAClG,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,IAAI,CAAC,MAAM,CAAC,wDAAwD,CAAC,CAAC;QACtE,IAAI,CAAC,MAAM,CAAC,sFAAsF,CAAC,CAAC;QACpG,IAAI,CAAC,MAAM,CAAC,iEAAiE,CAAC,CAAC;IACjF,CAAC;CACF"}
@@ -0,0 +1,28 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ /**
3
+ * Drops `external_integrations` together with the removal of the
4
+ * `integrations` module.
5
+ *
6
+ * The module was scaffolding that never grew a consumer: no adapter registry
7
+ * was ever built, nothing decrypted `encrypted_config` outside its own
8
+ * service, and its role is covered by `credentials` (encrypted vendor
9
+ * credentials referenced from Settings), `api_keys` (inbound machine access),
10
+ * `webhooks` (outbound notifications), and the per-vendor integration modules.
11
+ *
12
+ * The table lives here rather than in an `integrations` migration directory
13
+ * because `009_us7_init.ts` — which also creates `webhooks` and
14
+ * `webhook_deliveries` — is what created it. That migration has already run in
15
+ * production, and what follows from that is that its class name cannot move,
16
+ * not that its body cannot change: the storage persists the name and no
17
+ * checksum, so an edited body is never re-offered.
18
+ * `specs/120-migration-closure-bridge-ownership/` Phase 3 took the `api_keys`
19
+ * creation out of it on exactly that reasoning.
20
+ *
21
+ * `down()` recreates the table exactly as `009_us7_init.ts` left it, so a
22
+ * rollback restores the schema. Row data is not recoverable.
23
+ */
24
+ export declare class Migration20260727T200555WebhooksDropExternalIntegrations extends Migration {
25
+ up(): Promise<void>;
26
+ down(): Promise<void>;
27
+ }
28
+ //# sourceMappingURL=20260727T200555_webhooks_drop_external_integrations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260727T200555_webhooks_drop_external_integrations.d.ts","sourceRoot":"","sources":["../../src/migrations/20260727T200555_webhooks_drop_external_integrations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAAa,wDAAyD,SAAQ,SAAS;IACtE,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;IAInB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAoBrC"}
@@ -0,0 +1,49 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ /**
3
+ * Drops `external_integrations` together with the removal of the
4
+ * `integrations` module.
5
+ *
6
+ * The module was scaffolding that never grew a consumer: no adapter registry
7
+ * was ever built, nothing decrypted `encrypted_config` outside its own
8
+ * service, and its role is covered by `credentials` (encrypted vendor
9
+ * credentials referenced from Settings), `api_keys` (inbound machine access),
10
+ * `webhooks` (outbound notifications), and the per-vendor integration modules.
11
+ *
12
+ * The table lives here rather than in an `integrations` migration directory
13
+ * because `009_us7_init.ts` — which also creates `webhooks` and
14
+ * `webhook_deliveries` — is what created it. That migration has already run in
15
+ * production, and what follows from that is that its class name cannot move,
16
+ * not that its body cannot change: the storage persists the name and no
17
+ * checksum, so an edited body is never re-offered.
18
+ * `specs/120-migration-closure-bridge-ownership/` Phase 3 took the `api_keys`
19
+ * creation out of it on exactly that reasoning.
20
+ *
21
+ * `down()` recreates the table exactly as `009_us7_init.ts` left it, so a
22
+ * rollback restores the schema. Row data is not recoverable.
23
+ */
24
+ export class Migration20260727T200555WebhooksDropExternalIntegrations extends Migration {
25
+ async up() {
26
+ this.addSql('drop table if exists "external_integrations" cascade;');
27
+ }
28
+ async down() {
29
+ this.addSql(`
30
+ create table "external_integrations" (
31
+ "id" uuid not null,
32
+ "name" varchar(160) not null,
33
+ "vendor" varchar(64) not null,
34
+ "kind" varchar(32) not null,
35
+ "encrypted_config" text not null,
36
+ "status" varchar(16) not null default 'inactive',
37
+ "last_tested_at" timestamptz null,
38
+ "last_error" varchar(4000) null,
39
+ "created_by_admin_user_id" uuid null,
40
+ "created_at" timestamptz not null,
41
+ "updated_at" timestamptz not null,
42
+ constraint "external_integrations_pkey" primary key ("id"),
43
+ constraint "external_integrations_vendor_unique" unique ("vendor")
44
+ );
45
+ `);
46
+ this.addSql('create index "external_integrations_vendor_index" on "external_integrations" ("vendor");');
47
+ }
48
+ }
49
+ //# sourceMappingURL=20260727T200555_webhooks_drop_external_integrations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260727T200555_webhooks_drop_external_integrations.js","sourceRoot":"","sources":["../../src/migrations/20260727T200555_webhooks_drop_external_integrations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,OAAO,wDAAyD,SAAQ,SAAS;IAC5E,KAAK,CAAC,EAAE;QACf,IAAI,CAAC,MAAM,CAAC,uDAAuD,CAAC,CAAC;IACvE,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;KAgBX,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CAAC,0FAA0F,CAAC,CAAC;IAC1G,CAAC;CACF"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The `./migrations` subpath — every migration class this module owns, as one
3
+ * ordered `migrations` array.
4
+ *
5
+ * The array is what the platform reads when this module is **installed**:
6
+ * `src/packages/package-runtime.ts` takes `exported['migrations']` and refuses
7
+ * the package outright when it is absent (D-168).
8
+ *
9
+ * Listed in ascending timestamp, which is the order of this module's own
10
+ * migrations and of nothing else (feature 081): a manifest `dependencies` array
11
+ * is the only thing ordering this block against another module's.
12
+ *
13
+ * The **named** exports stay beside the array, and the asymmetry with
14
+ * `./backend` — which publishes an array and no named class (D-168) — is
15
+ * deliberate. `db/migrations-registry.generated.ts` imports each class by name
16
+ * from this specifier, and a migration class name is contract in a way an entity
17
+ * class name is not: `mikro_orm_migrations` persists it, so it is a string every
18
+ * already-migrated database holds.
19
+ *
20
+ * A class that is in neither the array nor the barrel is a migration that does
21
+ * not run: `migration:pending` reports nothing pending and the first symptom is
22
+ * a query against a table nobody created.
23
+ */
24
+ import { Migration20260425T091359WebhooksUs7Init } from './20260425T091359_webhooks_us7_init.js';
25
+ import { Migration20260724T203140WebhooksOrgFilter } from './20260724T203140_webhooks_org_filter.js';
26
+ import { Migration20260727T200555WebhooksDropExternalIntegrations } from './20260727T200555_webhooks_drop_external_integrations.js';
27
+ export declare const migrations: (typeof Migration20260425T091359WebhooksUs7Init)[];
28
+ export { Migration20260425T091359WebhooksUs7Init, Migration20260724T203140WebhooksOrgFilter, Migration20260727T200555WebhooksDropExternalIntegrations, };
29
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,uCAAuC,EAAE,MAAM,wCAAwC,CAAC;AACjG,OAAO,EAAE,yCAAyC,EAAE,MAAM,0CAA0C,CAAC;AACrG,OAAO,EAAE,wDAAwD,EAAE,MAAM,0DAA0D,CAAC;AAEpI,eAAO,MAAM,UAAU,oDAItB,CAAC;AAEF,OAAO,EACL,uCAAuC,EACvC,yCAAyC,EACzC,wDAAwD,GACzD,CAAC"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The `./migrations` subpath — every migration class this module owns, as one
3
+ * ordered `migrations` array.
4
+ *
5
+ * The array is what the platform reads when this module is **installed**:
6
+ * `src/packages/package-runtime.ts` takes `exported['migrations']` and refuses
7
+ * the package outright when it is absent (D-168).
8
+ *
9
+ * Listed in ascending timestamp, which is the order of this module's own
10
+ * migrations and of nothing else (feature 081): a manifest `dependencies` array
11
+ * is the only thing ordering this block against another module's.
12
+ *
13
+ * The **named** exports stay beside the array, and the asymmetry with
14
+ * `./backend` — which publishes an array and no named class (D-168) — is
15
+ * deliberate. `db/migrations-registry.generated.ts` imports each class by name
16
+ * from this specifier, and a migration class name is contract in a way an entity
17
+ * class name is not: `mikro_orm_migrations` persists it, so it is a string every
18
+ * already-migrated database holds.
19
+ *
20
+ * A class that is in neither the array nor the barrel is a migration that does
21
+ * not run: `migration:pending` reports nothing pending and the first symptom is
22
+ * a query against a table nobody created.
23
+ */
24
+ import { Migration20260425T091359WebhooksUs7Init } from './20260425T091359_webhooks_us7_init.js';
25
+ import { Migration20260724T203140WebhooksOrgFilter } from './20260724T203140_webhooks_org_filter.js';
26
+ import { Migration20260727T200555WebhooksDropExternalIntegrations } from './20260727T200555_webhooks_drop_external_integrations.js';
27
+ export const migrations = [
28
+ Migration20260425T091359WebhooksUs7Init,
29
+ Migration20260724T203140WebhooksOrgFilter,
30
+ Migration20260727T200555WebhooksDropExternalIntegrations,
31
+ ];
32
+ export { Migration20260425T091359WebhooksUs7Init, Migration20260724T203140WebhooksOrgFilter, Migration20260727T200555WebhooksDropExternalIntegrations, };
33
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,uCAAuC,EAAE,MAAM,wCAAwC,CAAC;AACjG,OAAO,EAAE,yCAAyC,EAAE,MAAM,0CAA0C,CAAC;AACrG,OAAO,EAAE,wDAAwD,EAAE,MAAM,0DAA0D,CAAC;AAEpI,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,uCAAuC;IACvC,yCAAyC;IACzC,wDAAwD;CACzD,CAAC;AAEF,OAAO,EACL,uCAAuC,EACvC,yCAAyC,EACzC,wDAAwD,GACzD,CAAC"}
@@ -0,0 +1,50 @@
1
+ ---
2
+ title: webhooks
3
+ description: Outbound HMAC-signed event subscriptions
4
+ ---
5
+
6
+ # `webhooks`
7
+
8
+ Outbound HMAC-signed event subscriptions. Domain events on the in-process
9
+ event bus are bridged to a BullMQ queue; the worker signs and POSTs each
10
+ delivery to the subscriber URL.
11
+
12
+ ## Public surface
13
+
14
+ | Verb + Path | Purpose |
15
+ | --- | --- |
16
+ | `GET /api/v1/admin/webhooks` | List subscriptions |
17
+ | `POST /api/v1/admin/webhooks` | Create — secret returned once |
18
+ | `PATCH /api/v1/admin/webhooks/:id` | Update (status, eventTypes, url) |
19
+ | `DELETE /api/v1/admin/webhooks/:id` | Remove |
20
+ | `GET /api/v1/admin/webhooks/deliveries` | Recent deliveries (filterable by status) |
21
+ | `POST /api/v1/admin/webhooks/deliveries/:id/replay` | Replay a `failed` / `dead_lettered` delivery |
22
+
23
+ ## Delivery contract
24
+
25
+ Headers: `Content-Type: application/json`,
26
+ `X-Webhook-Event-Id`, `X-Webhook-Event-Type`,
27
+ `X-Webhook-Signature-256`, `X-Webhook-Attempt`. Receivers MUST verify the
28
+ signature with `timingSafeEqual` and dedupe on the event id.
29
+ The *Subscribing to webhooks* section of Endora's Integrations guide carries the
30
+ verification example.
31
+
32
+ ## Retry model
33
+
34
+ Up to 8 attempts per delivery, exponential backoff starting at 1 s, 10 s
35
+ per-attempt timeout. Final attempt → `dead_lettered`. The replay endpoint
36
+ copies a failed/dead-lettered row into a fresh `pending` delivery.
37
+
38
+ ## Entities
39
+
40
+ `Webhook` (name, url, eventTypes, secret, status), `WebhookDelivery`
41
+ (per-attempt audit row).
42
+
43
+ ## Extension points
44
+
45
+ - **Custom signing scheme** — `webhook-delivery-worker.ts#process` is the
46
+ single place HMAC headers are set; replace with a different signature
47
+ format or add a JWS variant here.
48
+ - **Subscription scope policy** — `webhook-service.ts#create` accepts any
49
+ event type today; layer authorization (e.g. only certain integrations
50
+ may subscribe to `payment.*`) by injecting a policy callback.
package/i18n/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "errors.WEBHOOK_DELIVERY_NOT_REPLAYABLE": "Only a delivery that failed or was dead-lettered can be replayed. This one is still pending or has already succeeded.",
3
+ "nav.webhooks.label": "Webhooks",
4
+ "actions.openWebhooks.label": "Webhooks",
5
+ "actions.openWebhooks.description": "Event subscriptions & deliveries"
6
+ }
package/i18n/pl.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "errors.WEBHOOK_DELIVERY_NOT_REPLAYABLE": "Ponowić można tylko dostarczenie, które zakończyło się niepowodzeniem albo trafiło do kolejki martwych komunikatów. To wciąż oczekuje albo już się powiodło.",
3
+ "nav.webhooks.label": "Webhooki",
4
+ "actions.openWebhooks.label": "Webhooki",
5
+ "actions.openWebhooks.description": "Subskrypcje zdarzeń i dostarczenia"
6
+ }
package/package.json ADDED
@@ -0,0 +1,97 @@
1
+ {
2
+ "name": "@endora-commerce/mod-webhooks",
3
+ "version": "0.100.0",
4
+ "type": "module",
5
+ "sideEffects": false,
6
+ "description": "Outbound webhook subscription registry and dispatcher.",
7
+ "license": "MIT",
8
+ "endora": {
9
+ "type": "module",
10
+ "id": "webhooks"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/endora-commerce/endora-commerce.git",
15
+ "directory": "packages/modules/webhooks"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/manifest.d.ts",
23
+ "default": "./dist/manifest.js"
24
+ },
25
+ "./backend": {
26
+ "types": "./dist/backend/index.d.ts",
27
+ "default": "./dist/backend/index.js"
28
+ },
29
+ "./migrations": {
30
+ "types": "./dist/migrations/index.d.ts",
31
+ "default": "./dist/migrations/index.js"
32
+ },
33
+ "./admin": {
34
+ "types": "./dist/admin/index.d.ts",
35
+ "default": "./dist/admin/index.js"
36
+ },
37
+ "./tailwind.css": "./tailwind.css",
38
+ "./package.json": "./package.json"
39
+ },
40
+ "files": [
41
+ "dist",
42
+ "i18n",
43
+ "docs",
44
+ "tailwind.css"
45
+ ],
46
+ "engines": {
47
+ "node": ">=22.18.0"
48
+ },
49
+ "peerDependencies": {
50
+ "@mikro-orm/core": "^6",
51
+ "@mikro-orm/migrations": "^6",
52
+ "@mikro-orm/postgresql": "^6",
53
+ "bullmq": "^5",
54
+ "fastify": "^5",
55
+ "ioredis": "^5",
56
+ "lucide-react": "^1",
57
+ "react": "^19",
58
+ "@endora-commerce/admin-kit": "0.100.0",
59
+ "@endora-commerce/contracts": "0.100.0",
60
+ "@endora-commerce/platform": "0.100.0"
61
+ },
62
+ "peerDependenciesMeta": {
63
+ "@endora-commerce/admin-kit": {
64
+ "optional": true
65
+ },
66
+ "lucide-react": {
67
+ "optional": true
68
+ },
69
+ "react": {
70
+ "optional": true
71
+ }
72
+ },
73
+ "devDependencies": {
74
+ "@fastify/type-provider-zod": "^1.0.0",
75
+ "@mikro-orm/core": "^6.6.13",
76
+ "@mikro-orm/migrations": "^6.6.13",
77
+ "@mikro-orm/postgresql": "^6.6.13",
78
+ "@types/node": "^22.9.0",
79
+ "@types/react": "^19.2.14",
80
+ "bullmq": "^5.76.1",
81
+ "fastify": "^5.12.5",
82
+ "ioredis": "^5.10.1",
83
+ "lucide-react": "^1.11.0",
84
+ "react": "^19.2.5",
85
+ "typescript": "^5.9.3",
86
+ "vitest": "^4.1.11",
87
+ "@endora-commerce/admin-kit": "0.100.0",
88
+ "@endora-commerce/contracts": "0.100.0",
89
+ "@endora-commerce/platform": "0.100.0"
90
+ },
91
+ "scripts": {
92
+ "build": "tsc -p tsconfig.build.json && tsc -p tsconfig.ui.json",
93
+ "typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.ui.json --noEmit",
94
+ "lint": "eslint src",
95
+ "test": "vitest run"
96
+ }
97
+ }
package/tailwind.css ADDED
@@ -0,0 +1,14 @@
1
+ /* @endora-commerce/mod-webhooks — AUTO-GENERATED by `pnpm --filter backend run manifests:generate`.
2
+ *
3
+ * The `@source` directives this package asks its host to scan
4
+ * (`specs/110-instance-repository/contracts/admin-stylesheet-composition.md` R1).
5
+ * They resolve relative to **this file**, so they hold wherever the package is
6
+ * installed — a workspace link here, `node_modules` in a client's instance.
7
+ *
8
+ * The `dist` line is what a published tarball ships and is what an instance
9
+ * scans; the `src` line is inert there and is what keeps `pnpm --filter admin
10
+ * run dev` reading source in this repository. Do not edit: run
11
+ * `pnpm --filter backend run manifests:generate`.
12
+ */
13
+ @source "./dist/admin";
14
+ @source "./src/admin";