@orthacms/webhooks-server 0.5.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 (90) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +7 -0
  3. package/dist/index.d.ts +19 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +33 -0
  6. package/dist/lib/application/dto/list-deliveries-query.dto.d.ts +8 -0
  7. package/dist/lib/application/dto/list-deliveries-query.dto.d.ts.map +1 -0
  8. package/dist/lib/application/dto/list-deliveries-query.dto.js +64 -0
  9. package/dist/lib/application/dto/save-webhook-endpoint.dto.d.ts +23 -0
  10. package/dist/lib/application/dto/save-webhook-endpoint.dto.d.ts.map +1 -0
  11. package/dist/lib/application/dto/save-webhook-endpoint.dto.js +127 -0
  12. package/dist/lib/application/webhook-endpoints.service.d.ts +82 -0
  13. package/dist/lib/application/webhook-endpoints.service.d.ts.map +1 -0
  14. package/dist/lib/application/webhook-endpoints.service.js +213 -0
  15. package/dist/lib/docs/describe-webhooks-api.d.ts +16 -0
  16. package/dist/lib/docs/describe-webhooks-api.d.ts.map +1 -0
  17. package/dist/lib/docs/describe-webhooks-api.js +170 -0
  18. package/dist/lib/docs/webhook-schemas.d.ts +21 -0
  19. package/dist/lib/docs/webhook-schemas.d.ts.map +1 -0
  20. package/dist/lib/docs/webhook-schemas.js +360 -0
  21. package/dist/lib/domain/errors/index.d.ts +3 -0
  22. package/dist/lib/domain/errors/index.d.ts.map +1 -0
  23. package/dist/lib/domain/errors/index.js +7 -0
  24. package/dist/lib/domain/errors/webhook-delivery-not-found.error.d.ts +12 -0
  25. package/dist/lib/domain/errors/webhook-delivery-not-found.error.d.ts.map +1 -0
  26. package/dist/lib/domain/errors/webhook-delivery-not-found.error.js +19 -0
  27. package/dist/lib/domain/errors/webhook-endpoint-not-found.error.d.ts +6 -0
  28. package/dist/lib/domain/errors/webhook-endpoint-not-found.error.d.ts.map +1 -0
  29. package/dist/lib/domain/errors/webhook-endpoint-not-found.error.js +13 -0
  30. package/dist/lib/domain/webhook-secret.d.ts +29 -0
  31. package/dist/lib/domain/webhook-secret.d.ts.map +1 -0
  32. package/dist/lib/domain/webhook-secret.js +34 -0
  33. package/dist/lib/domain/webhook-views.d.ts +85 -0
  34. package/dist/lib/domain/webhook-views.d.ts.map +1 -0
  35. package/dist/lib/domain/webhook-views.js +2 -0
  36. package/dist/lib/http/controllers/webhook-deliveries.controller.d.ts +28 -0
  37. package/dist/lib/http/controllers/webhook-deliveries.controller.d.ts.map +1 -0
  38. package/dist/lib/http/controllers/webhook-deliveries.controller.js +110 -0
  39. package/dist/lib/http/controllers/webhook-endpoints.controller.d.ts +61 -0
  40. package/dist/lib/http/controllers/webhook-endpoints.controller.d.ts.map +1 -0
  41. package/dist/lib/http/controllers/webhook-endpoints.controller.js +213 -0
  42. package/dist/lib/http/controllers/webhook-events.controller.d.ts +13 -0
  43. package/dist/lib/http/controllers/webhook-events.controller.d.ts.map +1 -0
  44. package/dist/lib/http/controllers/webhook-events.controller.js +38 -0
  45. package/dist/lib/infrastructure/event-mapping.d.ts +13 -0
  46. package/dist/lib/infrastructure/event-mapping.d.ts.map +1 -0
  47. package/dist/lib/infrastructure/event-mapping.js +69 -0
  48. package/dist/lib/infrastructure/purge/webhook-workspaces.purger.d.ts +42 -0
  49. package/dist/lib/infrastructure/purge/webhook-workspaces.purger.d.ts.map +1 -0
  50. package/dist/lib/infrastructure/purge/webhook-workspaces.purger.js +66 -0
  51. package/dist/lib/infrastructure/schema/index.d.ts +3 -0
  52. package/dist/lib/infrastructure/schema/index.d.ts.map +1 -0
  53. package/dist/lib/infrastructure/schema/index.js +8 -0
  54. package/dist/lib/infrastructure/schema/webhook-deliveries.d.ts +332 -0
  55. package/dist/lib/infrastructure/schema/webhook-deliveries.d.ts.map +1 -0
  56. package/dist/lib/infrastructure/schema/webhook-deliveries.js +92 -0
  57. package/dist/lib/infrastructure/schema/webhook-endpoints.d.ts +330 -0
  58. package/dist/lib/infrastructure/schema/webhook-endpoints.d.ts.map +1 -0
  59. package/dist/lib/infrastructure/schema/webhook-endpoints.js +94 -0
  60. package/dist/lib/infrastructure/webhook-delivery.repository.d.ts +99 -0
  61. package/dist/lib/infrastructure/webhook-delivery.repository.d.ts.map +1 -0
  62. package/dist/lib/infrastructure/webhook-delivery.repository.js +255 -0
  63. package/dist/lib/infrastructure/webhook-delivery.worker.d.ts +72 -0
  64. package/dist/lib/infrastructure/webhook-delivery.worker.d.ts.map +1 -0
  65. package/dist/lib/infrastructure/webhook-delivery.worker.js +216 -0
  66. package/dist/lib/infrastructure/webhook-endpoint.repository.d.ts +88 -0
  67. package/dist/lib/infrastructure/webhook-endpoint.repository.d.ts.map +1 -0
  68. package/dist/lib/infrastructure/webhook-endpoint.repository.js +306 -0
  69. package/dist/lib/infrastructure/webhook-fanout.subscriber.d.ts +36 -0
  70. package/dist/lib/infrastructure/webhook-fanout.subscriber.d.ts.map +1 -0
  71. package/dist/lib/infrastructure/webhook-fanout.subscriber.js +83 -0
  72. package/dist/lib/infrastructure/webhook-http.client.d.ts +76 -0
  73. package/dist/lib/infrastructure/webhook-http.client.d.ts.map +1 -0
  74. package/dist/lib/infrastructure/webhook-http.client.js +225 -0
  75. package/dist/lib/types/webhooks-config.d.ts +64 -0
  76. package/dist/lib/types/webhooks-config.d.ts.map +1 -0
  77. package/dist/lib/types/webhooks-config.js +24 -0
  78. package/dist/lib/utils/webhooks-plugin.d.ts +35 -0
  79. package/dist/lib/utils/webhooks-plugin.d.ts.map +1 -0
  80. package/dist/lib/utils/webhooks-plugin.js +90 -0
  81. package/dist/lib/webhooks.module.d.ts +18 -0
  82. package/dist/lib/webhooks.module.d.ts.map +1 -0
  83. package/dist/lib/webhooks.module.js +72 -0
  84. package/dist/lib/webhooks.tokens.d.ts +5 -0
  85. package/dist/lib/webhooks.tokens.d.ts.map +1 -0
  86. package/dist/lib/webhooks.tokens.js +9 -0
  87. package/migrations/0000_init_webhooks.sql +53 -0
  88. package/migrations/meta/0000_snapshot.json +424 -0
  89. package/migrations/meta/_journal.json +13 -0
  90. package/package.json +45 -0
@@ -0,0 +1,330 @@
1
+ /**
2
+ * One configured destination for outgoing webhooks.
3
+ *
4
+ * Global rather than workspace-scoped: an endpoint spans whichever workspaces
5
+ * it was pointed at, holds a signing secret, and is deployment configuration
6
+ * rather than editorial content — the same reasoning that puts API tokens in
7
+ * the directory section instead of inside a workspace.
8
+ *
9
+ * **The secret is stored in the clear.** That is a decision, not an oversight:
10
+ * an API token is only ever *verified*, so a hash is enough, but a webhook
11
+ * secret is used to *sign*, and a signing key that cannot be read back cannot
12
+ * sign anything. What is done instead is to show it exactly once on mint, never
13
+ * return it from the API again (`secretHint` is what the UI shows), and let a
14
+ * deployment encrypt the column at rest.
15
+ *
16
+ * `createdBy` is a plain uuid with no FK, like `alarm_rules.createdBy`: the
17
+ * person who added the endpoint may be deleted and the endpoint must outlive
18
+ * them.
19
+ */
20
+ export declare const webhookEndpoints: import("drizzle-orm/pg-core").PgTableWithColumns<{
21
+ name: "webhook_endpoints";
22
+ schema: undefined;
23
+ columns: {
24
+ id: import("drizzle-orm/pg-core").PgColumn<{
25
+ name: "id";
26
+ tableName: "webhook_endpoints";
27
+ dataType: "string";
28
+ columnType: "PgUUID";
29
+ data: string;
30
+ driverParam: string;
31
+ notNull: true;
32
+ hasDefault: true;
33
+ isPrimaryKey: true;
34
+ isAutoincrement: false;
35
+ hasRuntimeDefault: false;
36
+ enumValues: undefined;
37
+ baseColumn: never;
38
+ identity: undefined;
39
+ generated: undefined;
40
+ }, {}, {}>;
41
+ name: import("drizzle-orm/pg-core").PgColumn<{
42
+ name: "name";
43
+ tableName: "webhook_endpoints";
44
+ dataType: "string";
45
+ columnType: "PgText";
46
+ data: string;
47
+ driverParam: string;
48
+ notNull: true;
49
+ hasDefault: false;
50
+ isPrimaryKey: false;
51
+ isAutoincrement: false;
52
+ hasRuntimeDefault: false;
53
+ enumValues: [string, ...string[]];
54
+ baseColumn: never;
55
+ identity: undefined;
56
+ generated: undefined;
57
+ }, {}, {}>;
58
+ url: import("drizzle-orm/pg-core").PgColumn<{
59
+ name: "url";
60
+ tableName: "webhook_endpoints";
61
+ dataType: "string";
62
+ columnType: "PgText";
63
+ data: string;
64
+ driverParam: string;
65
+ notNull: true;
66
+ hasDefault: false;
67
+ isPrimaryKey: false;
68
+ isAutoincrement: false;
69
+ hasRuntimeDefault: false;
70
+ enumValues: [string, ...string[]];
71
+ baseColumn: never;
72
+ identity: undefined;
73
+ generated: undefined;
74
+ }, {}, {}>;
75
+ secret: import("drizzle-orm/pg-core").PgColumn<{
76
+ name: "secret";
77
+ tableName: "webhook_endpoints";
78
+ dataType: "string";
79
+ columnType: "PgText";
80
+ data: string;
81
+ driverParam: string;
82
+ notNull: true;
83
+ hasDefault: false;
84
+ isPrimaryKey: false;
85
+ isAutoincrement: false;
86
+ hasRuntimeDefault: false;
87
+ enumValues: [string, ...string[]];
88
+ baseColumn: never;
89
+ identity: undefined;
90
+ generated: undefined;
91
+ }, {}, {}>;
92
+ secretHint: import("drizzle-orm/pg-core").PgColumn<{
93
+ name: "secret_hint";
94
+ tableName: "webhook_endpoints";
95
+ dataType: "string";
96
+ columnType: "PgText";
97
+ data: string;
98
+ driverParam: string;
99
+ notNull: true;
100
+ hasDefault: false;
101
+ isPrimaryKey: false;
102
+ isAutoincrement: false;
103
+ hasRuntimeDefault: false;
104
+ enumValues: [string, ...string[]];
105
+ baseColumn: never;
106
+ identity: undefined;
107
+ generated: undefined;
108
+ }, {}, {}>;
109
+ enabled: import("drizzle-orm/pg-core").PgColumn<{
110
+ name: "enabled";
111
+ tableName: "webhook_endpoints";
112
+ dataType: "boolean";
113
+ columnType: "PgBoolean";
114
+ data: boolean;
115
+ driverParam: boolean;
116
+ notNull: true;
117
+ hasDefault: true;
118
+ isPrimaryKey: false;
119
+ isAutoincrement: false;
120
+ hasRuntimeDefault: false;
121
+ enumValues: undefined;
122
+ baseColumn: never;
123
+ identity: undefined;
124
+ generated: undefined;
125
+ }, {}, {}>;
126
+ eventKinds: import("drizzle-orm/pg-core").PgColumn<{
127
+ name: "event_kinds";
128
+ tableName: "webhook_endpoints";
129
+ dataType: "json";
130
+ columnType: "PgJsonb";
131
+ data: unknown;
132
+ driverParam: unknown;
133
+ notNull: true;
134
+ hasDefault: true;
135
+ isPrimaryKey: false;
136
+ isAutoincrement: false;
137
+ hasRuntimeDefault: false;
138
+ enumValues: undefined;
139
+ baseColumn: never;
140
+ identity: undefined;
141
+ generated: undefined;
142
+ }, {}, {}>;
143
+ contentTypes: import("drizzle-orm/pg-core").PgColumn<{
144
+ name: "content_types";
145
+ tableName: "webhook_endpoints";
146
+ dataType: "json";
147
+ columnType: "PgJsonb";
148
+ data: unknown;
149
+ driverParam: unknown;
150
+ notNull: true;
151
+ hasDefault: true;
152
+ isPrimaryKey: false;
153
+ isAutoincrement: false;
154
+ hasRuntimeDefault: false;
155
+ enumValues: undefined;
156
+ baseColumn: never;
157
+ identity: undefined;
158
+ generated: undefined;
159
+ }, {}, {}>;
160
+ allWorkspaces: import("drizzle-orm/pg-core").PgColumn<{
161
+ name: "all_workspaces";
162
+ tableName: "webhook_endpoints";
163
+ dataType: "boolean";
164
+ columnType: "PgBoolean";
165
+ data: boolean;
166
+ driverParam: boolean;
167
+ notNull: true;
168
+ hasDefault: true;
169
+ isPrimaryKey: false;
170
+ isAutoincrement: false;
171
+ hasRuntimeDefault: false;
172
+ enumValues: undefined;
173
+ baseColumn: never;
174
+ identity: undefined;
175
+ generated: undefined;
176
+ }, {}, {}>;
177
+ headers: import("drizzle-orm/pg-core").PgColumn<{
178
+ name: "headers";
179
+ tableName: "webhook_endpoints";
180
+ dataType: "json";
181
+ columnType: "PgJsonb";
182
+ data: unknown;
183
+ driverParam: unknown;
184
+ notNull: true;
185
+ hasDefault: true;
186
+ isPrimaryKey: false;
187
+ isAutoincrement: false;
188
+ hasRuntimeDefault: false;
189
+ enumValues: undefined;
190
+ baseColumn: never;
191
+ identity: undefined;
192
+ generated: undefined;
193
+ }, {}, {}>;
194
+ disabledReason: import("drizzle-orm/pg-core").PgColumn<{
195
+ name: "disabled_reason";
196
+ tableName: "webhook_endpoints";
197
+ dataType: "string";
198
+ columnType: "PgText";
199
+ data: string;
200
+ driverParam: string;
201
+ notNull: false;
202
+ hasDefault: false;
203
+ isPrimaryKey: false;
204
+ isAutoincrement: false;
205
+ hasRuntimeDefault: false;
206
+ enumValues: [string, ...string[]];
207
+ baseColumn: never;
208
+ identity: undefined;
209
+ generated: undefined;
210
+ }, {}, {}>;
211
+ consecutiveFailures: import("drizzle-orm/pg-core").PgColumn<{
212
+ name: "consecutive_failures";
213
+ tableName: "webhook_endpoints";
214
+ dataType: "number";
215
+ columnType: "PgInteger";
216
+ data: number;
217
+ driverParam: string | number;
218
+ notNull: true;
219
+ hasDefault: true;
220
+ isPrimaryKey: false;
221
+ isAutoincrement: false;
222
+ hasRuntimeDefault: false;
223
+ enumValues: undefined;
224
+ baseColumn: never;
225
+ identity: undefined;
226
+ generated: undefined;
227
+ }, {}, {}>;
228
+ createdBy: import("drizzle-orm/pg-core").PgColumn<{
229
+ name: "created_by";
230
+ tableName: "webhook_endpoints";
231
+ dataType: "string";
232
+ columnType: "PgUUID";
233
+ data: string;
234
+ driverParam: string;
235
+ notNull: false;
236
+ hasDefault: false;
237
+ isPrimaryKey: false;
238
+ isAutoincrement: false;
239
+ hasRuntimeDefault: false;
240
+ enumValues: undefined;
241
+ baseColumn: never;
242
+ identity: undefined;
243
+ generated: undefined;
244
+ }, {}, {}>;
245
+ createdAt: import("drizzle-orm/pg-core").PgColumn<{
246
+ name: "created_at";
247
+ tableName: "webhook_endpoints";
248
+ dataType: "date";
249
+ columnType: "PgTimestamp";
250
+ data: Date;
251
+ driverParam: string;
252
+ notNull: true;
253
+ hasDefault: true;
254
+ isPrimaryKey: false;
255
+ isAutoincrement: false;
256
+ hasRuntimeDefault: false;
257
+ enumValues: undefined;
258
+ baseColumn: never;
259
+ identity: undefined;
260
+ generated: undefined;
261
+ }, {}, {}>;
262
+ updatedAt: import("drizzle-orm/pg-core").PgColumn<{
263
+ name: "updated_at";
264
+ tableName: "webhook_endpoints";
265
+ dataType: "date";
266
+ columnType: "PgTimestamp";
267
+ data: Date;
268
+ driverParam: string;
269
+ notNull: true;
270
+ hasDefault: true;
271
+ isPrimaryKey: false;
272
+ isAutoincrement: false;
273
+ hasRuntimeDefault: false;
274
+ enumValues: undefined;
275
+ baseColumn: never;
276
+ identity: undefined;
277
+ generated: undefined;
278
+ }, {}, {}>;
279
+ };
280
+ dialect: "pg";
281
+ }>;
282
+ /**
283
+ * The workspaces an endpoint subscribes to, when it does not take them all.
284
+ *
285
+ * No FK on `workspaceId`: the `workspaces` table belongs to another plugin, and
286
+ * cross-plugin foreign keys are not how this codebase scopes rows — the same
287
+ * convention as `api_token_workspaces` and the generated content tables.
288
+ */
289
+ export declare const webhookEndpointWorkspaces: import("drizzle-orm/pg-core").PgTableWithColumns<{
290
+ name: "webhook_endpoint_workspaces";
291
+ schema: undefined;
292
+ columns: {
293
+ endpointId: import("drizzle-orm/pg-core").PgColumn<{
294
+ name: "endpoint_id";
295
+ tableName: "webhook_endpoint_workspaces";
296
+ dataType: "string";
297
+ columnType: "PgUUID";
298
+ data: string;
299
+ driverParam: string;
300
+ notNull: true;
301
+ hasDefault: false;
302
+ isPrimaryKey: false;
303
+ isAutoincrement: false;
304
+ hasRuntimeDefault: false;
305
+ enumValues: undefined;
306
+ baseColumn: never;
307
+ identity: undefined;
308
+ generated: undefined;
309
+ }, {}, {}>;
310
+ workspaceId: import("drizzle-orm/pg-core").PgColumn<{
311
+ name: "workspace_id";
312
+ tableName: "webhook_endpoint_workspaces";
313
+ dataType: "string";
314
+ columnType: "PgUUID";
315
+ data: string;
316
+ driverParam: string;
317
+ notNull: true;
318
+ hasDefault: false;
319
+ isPrimaryKey: false;
320
+ isAutoincrement: false;
321
+ hasRuntimeDefault: false;
322
+ enumValues: undefined;
323
+ baseColumn: never;
324
+ identity: undefined;
325
+ generated: undefined;
326
+ }, {}, {}>;
327
+ };
328
+ dialect: "pg";
329
+ }>;
330
+ //# sourceMappingURL=webhook-endpoints.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-endpoints.d.ts","sourceRoot":"","sources":["../../../../src/lib/infrastructure/schema/webhook-endpoints.ts"],"names":[],"mappings":"AAYA;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAyD5B,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAarC,CAAC"}
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.webhookEndpointWorkspaces = exports.webhookEndpoints = void 0;
4
+ const pg_core_1 = require("drizzle-orm/pg-core");
5
+ /**
6
+ * One configured destination for outgoing webhooks.
7
+ *
8
+ * Global rather than workspace-scoped: an endpoint spans whichever workspaces
9
+ * it was pointed at, holds a signing secret, and is deployment configuration
10
+ * rather than editorial content — the same reasoning that puts API tokens in
11
+ * the directory section instead of inside a workspace.
12
+ *
13
+ * **The secret is stored in the clear.** That is a decision, not an oversight:
14
+ * an API token is only ever *verified*, so a hash is enough, but a webhook
15
+ * secret is used to *sign*, and a signing key that cannot be read back cannot
16
+ * sign anything. What is done instead is to show it exactly once on mint, never
17
+ * return it from the API again (`secretHint` is what the UI shows), and let a
18
+ * deployment encrypt the column at rest.
19
+ *
20
+ * `createdBy` is a plain uuid with no FK, like `alarm_rules.createdBy`: the
21
+ * person who added the endpoint may be deleted and the endpoint must outlive
22
+ * them.
23
+ */
24
+ exports.webhookEndpoints = (0, pg_core_1.pgTable)('webhook_endpoints', {
25
+ id: (0, pg_core_1.uuid)('id').primaryKey().defaultRandom(),
26
+ /** Human label shown in the list, e.g. "Rebuild the storefront". */
27
+ name: (0, pg_core_1.text)('name').notNull(),
28
+ /** Where deliveries are POSTed. Re-checked against the URL policy on
29
+ * every send, not only when it was saved. */
30
+ url: (0, pg_core_1.text)('url').notNull(),
31
+ /** The HMAC signing key. See the note above on why it is not hashed. */
32
+ secret: (0, pg_core_1.text)('secret').notNull(),
33
+ /** The last few characters, so the UI can tell two secrets apart after
34
+ * a rotation without ever showing one again. */
35
+ secretHint: (0, pg_core_1.text)('secret_hint').notNull(),
36
+ /** The manual switch. Auto-disabling clears this too. */
37
+ enabled: (0, pg_core_1.boolean)('enabled').notNull().default(true),
38
+ /**
39
+ * Subscribed event kinds. **Empty means every kind**, including ones
40
+ * added to the catalogue later — an endpoint that asked for everything
41
+ * should not silently stop covering a new event.
42
+ */
43
+ eventKinds: (0, pg_core_1.jsonb)('event_kinds').notNull().default([]),
44
+ /** Subscribed content types. **Empty means every type.** */
45
+ contentTypes: (0, pg_core_1.jsonb)('content_types').notNull().default([]),
46
+ /**
47
+ * Whether the endpoint takes every workspace.
48
+ *
49
+ * Stored explicitly rather than inferred from an empty
50
+ * {@link webhookEndpointWorkspaces} set: fan-out runs on every event
51
+ * and must never have to disambiguate "no rows" between "all" and
52
+ * "none".
53
+ */
54
+ allWorkspaces: (0, pg_core_1.boolean)('all_workspaces').notNull().default(false),
55
+ /** Extra static headers, filtered through the reserved-name allow-list. */
56
+ headers: (0, pg_core_1.jsonb)('headers').notNull().default({}),
57
+ /** Why the endpoint was switched off automatically; null otherwise. */
58
+ disabledReason: (0, pg_core_1.text)('disabled_reason'),
59
+ /** Consecutive dead deliveries; any success resets it to zero. */
60
+ consecutiveFailures: (0, pg_core_1.integer)('consecutive_failures')
61
+ .notNull()
62
+ .default(0),
63
+ /** Who created it. No FK — the endpoint outlives its author. */
64
+ createdBy: (0, pg_core_1.uuid)('created_by'),
65
+ createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true })
66
+ .notNull()
67
+ .defaultNow(),
68
+ updatedAt: (0, pg_core_1.timestamp)('updated_at', { withTimezone: true })
69
+ .notNull()
70
+ .defaultNow()
71
+ }, (table) => [
72
+ // Fan-out's only query is "every enabled endpoint", run once per
73
+ // content write. The table is small enough that this index mostly
74
+ // documents the access path — it earns its keep on an install with
75
+ // many endpoints, most of them switched off.
76
+ (0, pg_core_1.index)('webhook_endpoints_enabled_idx').on(table.enabled)
77
+ ]);
78
+ /**
79
+ * The workspaces an endpoint subscribes to, when it does not take them all.
80
+ *
81
+ * No FK on `workspaceId`: the `workspaces` table belongs to another plugin, and
82
+ * cross-plugin foreign keys are not how this codebase scopes rows — the same
83
+ * convention as `api_token_workspaces` and the generated content tables.
84
+ */
85
+ exports.webhookEndpointWorkspaces = (0, pg_core_1.pgTable)('webhook_endpoint_workspaces', {
86
+ endpointId: (0, pg_core_1.uuid)('endpoint_id')
87
+ .notNull()
88
+ .references(() => exports.webhookEndpoints.id, { onDelete: 'cascade' }),
89
+ workspaceId: (0, pg_core_1.uuid)('workspace_id').notNull()
90
+ }, (table) => [
91
+ (0, pg_core_1.primaryKey)({ columns: [table.endpointId, table.workspaceId] }),
92
+ // "Which endpoints take this workspace" — the fan-out lookup.
93
+ (0, pg_core_1.index)('webhook_endpoint_workspaces_workspace_idx').on(table.workspaceId)
94
+ ]);
@@ -0,0 +1,99 @@
1
+ import { type Database } from '@orthacms/database';
2
+ import type { DeliveryStatus } from '@orthacms/webhooks-domain';
3
+ import type { WebhookDeliveryDetailView, WebhookDeliveryPage } from '../domain/webhook-views';
4
+ /** A row to queue. */
5
+ export interface DeliveryToQueue {
6
+ /**
7
+ * Generated by the caller, not by the database.
8
+ *
9
+ * The envelope frozen in `payload` carries this id and so does the
10
+ * `X-Ortha-Delivery` header, so it has to exist before the row is written
11
+ * or the body and the header would name different deliveries.
12
+ */
13
+ id: string;
14
+ endpointId: string;
15
+ eventId: string;
16
+ eventKind: string;
17
+ workspaceId: string | null;
18
+ contentType: string | null;
19
+ payload: unknown;
20
+ /** The delivery this one repeats, for a redelivery; null otherwise. */
21
+ redeliveryOf?: string | null;
22
+ }
23
+ /** A claimed row, as the worker needs it. */
24
+ export interface ClaimedDelivery {
25
+ id: string;
26
+ endpointId: string;
27
+ eventId: string;
28
+ eventKind: string;
29
+ workspaceId: string | null;
30
+ payload: unknown;
31
+ attempts: number;
32
+ }
33
+ /** What one attempt produced. */
34
+ export interface AttemptOutcome {
35
+ statusCode: number | null;
36
+ error: string | null;
37
+ responseSnippet: string | null;
38
+ durationMs: number;
39
+ }
40
+ /** Filters the delivery log accepts. */
41
+ export interface DeliveryListQuery {
42
+ status?: DeliveryStatus;
43
+ eventKind?: string;
44
+ page: number;
45
+ pageSize: number;
46
+ }
47
+ /** The queue and the log, over one table. */
48
+ export declare class WebhookDeliveryRepository {
49
+ private readonly db;
50
+ constructor(db: Database);
51
+ /**
52
+ * Queues one delivery per endpoint, ignoring rows that already exist.
53
+ *
54
+ * `ON CONFLICT DO NOTHING` against `unique(endpoint_id, event_id)` is the
55
+ * whole idempotency story for fan-out: outbox delivery is at-least-once, so
56
+ * this subscriber may be handed the same event twice, and the second pass
57
+ * must be a no-op rather than a duplicate POST.
58
+ */
59
+ enqueue(deliveries: DeliveryToQueue[]): Promise<number>;
60
+ /** Queues a single delivery and returns its id (used by redelivery). */
61
+ enqueueOne(delivery: DeliveryToQueue): Promise<string>;
62
+ /**
63
+ * Claims up to `limit` deliveries and marks them `delivering`.
64
+ *
65
+ * The transaction covers **only the claim**. That is the point of the whole
66
+ * design: the HTTP request happens after this returns, with nothing open,
67
+ * so a slow receiver cannot hold a database transaction or a pool client.
68
+ *
69
+ * `FOR UPDATE SKIP LOCKED` lets several API processes claim disjoint rows
70
+ * without blocking each other, exactly as the outbox drain does.
71
+ *
72
+ * Rows stuck in `delivering` past `claimTimeoutMs` are reclaimed here
73
+ * rather than by a separate reaper: a worker that died mid-request left the
74
+ * row with no error, no retry and no way out, and folding the recovery into
75
+ * the claim means there is one query to get right instead of two.
76
+ */
77
+ claim(limit: number, claimTimeoutMs: number): Promise<ClaimedDelivery[]>;
78
+ /** Records a successful attempt. */
79
+ markSucceeded(id: string, outcome: AttemptOutcome): Promise<void>;
80
+ /** Records a failed attempt that will be tried again at `nextAttemptAt`. */
81
+ markRetrying(id: string, outcome: AttemptOutcome, nextAttemptAt: Date): Promise<void>;
82
+ /** Records a final failure — no further attempt will be made. */
83
+ markDead(id: string, outcome: AttemptOutcome): Promise<void>;
84
+ /** One page of an endpoint's delivery log, newest first. */
85
+ list(endpointId: string, query: DeliveryListQuery): Promise<WebhookDeliveryPage>;
86
+ /**
87
+ * One delivery with its bodies — scoped to its endpoint so an id from
88
+ * another endpoint reads as "not found" rather than leaking across.
89
+ */
90
+ findDetail(endpointId: string, deliveryId: string): Promise<WebhookDeliveryDetailView | null>;
91
+ /**
92
+ * Deletes completed rows older than `before`.
93
+ *
94
+ * Only `succeeded` and `dead` are eligible: a `failed` row is still
95
+ * scheduled, and a `delivering` row may be in flight.
96
+ */
97
+ pruneCompletedBefore(before: Date): Promise<number>;
98
+ }
99
+ //# sourceMappingURL=webhook-delivery.repository.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-delivery.repository.d.ts","sourceRoot":"","sources":["../../../src/lib/infrastructure/webhook-delivery.repository.ts"],"names":[],"mappings":"AACA,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AACnE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAahE,OAAO,KAAK,EACR,yBAAyB,EACzB,mBAAmB,EAEtB,MAAM,yBAAyB,CAAC;AAGjC,sBAAsB;AACtB,MAAM,WAAW,eAAe;IAC5B;;;;;;OAMG;IACH,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,uEAAuE;IACvE,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED,6CAA6C;AAC7C,MAAM,WAAW,eAAe;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CACpB;AAED,iCAAiC;AACjC,MAAM,WAAW,cAAc;IAC3B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,UAAU,EAAE,MAAM,CAAC;CACtB;AAED,wCAAwC;AACxC,MAAM,WAAW,iBAAiB;IAC9B,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CACpB;AAED,6CAA6C;AAC7C,qBACa,yBAAyB;IACJ,OAAO,CAAC,QAAQ,CAAC,EAAE;gBAAF,EAAE,EAAE,QAAQ;IAE3D;;;;;;;OAOG;IACG,OAAO,CAAC,UAAU,EAAE,eAAe,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IAgC7D,wEAAwE;IAClE,UAAU,CAAC,QAAQ,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC;IAkB5D;;;;;;;;;;;;;;OAcG;IACG,KAAK,CACP,KAAK,EAAE,MAAM,EACb,cAAc,EAAE,MAAM,GACvB,OAAO,CAAC,eAAe,EAAE,CAAC;IAsD7B,oCAAoC;IAC9B,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBvE,4EAA4E;IACtE,YAAY,CACd,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,cAAc,EACvB,aAAa,EAAE,IAAI,GACpB,OAAO,CAAC,IAAI,CAAC;IAgBhB,iEAAiE;IAC3D,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBlE,4DAA4D;IACtD,IAAI,CACN,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,iBAAiB,GACzB,OAAO,CAAC,mBAAmB,CAAC;IAsC/B;;;OAGG;IACG,UAAU,CACZ,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACnB,OAAO,CAAC,yBAAyB,GAAG,IAAI,CAAC;IAoB5C;;;;;OAKG;IACG,oBAAoB,CAAC,MAAM,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC;CAa5D"}