@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,170 @@
1
+ "use strict";
2
+ /**
3
+ * The webhooks plugin's pass over the host's OpenAPI document.
4
+ *
5
+ * The routes here are ordinary decorated controllers, so `@nestjs/swagger`
6
+ * already knows their paths, parameters and request bodies. What it cannot
7
+ * know is what comes **back**: every response view is a TypeScript
8
+ * `interface`, erased before the scanner runs, which is why each operation
9
+ * arrives carrying a bare `200`/`201` with no content. This pass writes the
10
+ * response schemas onto the operations the plugin owns and nothing else.
11
+ *
12
+ * Pure: it takes the document and mutates only its own paths.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.describeWebhooksApi = describeWebhooksApi;
16
+ const webhook_schemas_1 = require("./webhook-schemas");
17
+ /** The endpoint routes, keyed by what follows `/webhooks`. */
18
+ const ENDPOINT_ROUTES = {
19
+ '': {
20
+ get: {
21
+ schema: 'WebhookEndpoint',
22
+ list: true,
23
+ description: 'Every endpoint, newest first. No signing secrets.'
24
+ },
25
+ post: {
26
+ schema: 'WebhookEndpointWithSecret',
27
+ description: 'The created endpoint and its signing secret — the secret’s only appearance.',
28
+ validatesUrl: true
29
+ }
30
+ },
31
+ '/{id}': {
32
+ get: { schema: 'WebhookEndpoint', description: 'The endpoint.' },
33
+ patch: {
34
+ schema: 'WebhookEndpoint',
35
+ description: 'The endpoint as it now stands.',
36
+ validatesUrl: true
37
+ }
38
+ },
39
+ '/{id}/secret': {
40
+ post: {
41
+ schema: 'WebhookEndpointWithSecret',
42
+ description: 'The endpoint and the new signing secret, returned once. The previous secret stops verifying immediately, including for deliveries already queued.'
43
+ }
44
+ },
45
+ '/{id}/test': {
46
+ post: {
47
+ schema: 'WebhookTestResult',
48
+ description: 'What the receiver answered.'
49
+ }
50
+ },
51
+ '/{id}/deliveries': {
52
+ get: {
53
+ schema: 'WebhookDeliveryPage',
54
+ description: 'One page of the endpoint’s delivery log, newest first.'
55
+ }
56
+ },
57
+ '/{id}/deliveries/{deliveryId}': {
58
+ get: {
59
+ schema: 'WebhookDeliveryDetail',
60
+ description: 'The delivery, with its frozen body and the response.'
61
+ }
62
+ },
63
+ // Declared as the detail shape, and that is not a copy-paste of the route
64
+ // above: `redeliver` is *typed* `WebhookDeliveryView` but returns
65
+ // `findDetail(...)`, so the queued row comes back with `payload` and
66
+ // `responseSnippet` (null, since nothing has been sent yet) on it. The
67
+ // document describes what the route answers with.
68
+ '/{id}/deliveries/{deliveryId}/redeliver': {
69
+ post: {
70
+ schema: 'WebhookDeliveryDetail',
71
+ description: 'The newly queued delivery. It carries a new delivery `id` and the original `eventId`, so a receiver that deduplicates still recognises the repeat.'
72
+ }
73
+ }
74
+ };
75
+ /** The event catalogue, keyed by what follows `/webhook-events`. */
76
+ const EVENT_ROUTES = {
77
+ '': {
78
+ get: {
79
+ schema: 'WebhookEventDescriptor',
80
+ list: true,
81
+ description: 'Every kind an endpoint may subscribe to.'
82
+ }
83
+ }
84
+ };
85
+ /**
86
+ * Matches `<prefix>/webhooks<rest>` and `<prefix>/webhook-events<rest>`.
87
+ *
88
+ * The prefix is whatever the host configured (`/api` here), so it is matched
89
+ * rather than assumed — but it is matched as segments **containing no `{`**,
90
+ * which is what keeps a hypothetical `/api/workspaces/{id}/webhooks` from being
91
+ * described with the global endpoint's shapes. A global prefix never holds a
92
+ * path parameter; a nested resource route almost always does. The content
93
+ * plugin learned this the expensive way, where a laxer pattern put the admin's
94
+ * schemas onto thirteen published operations.
95
+ */
96
+ const ENDPOINT_ROUTE_RE = /^(?:\/[^/{}]+)*\/webhooks(\/.*)?$/;
97
+ /** Matches `<prefix>/webhook-events<rest>`, under the same rule. */
98
+ const EVENT_ROUTE_RE = /^(?:\/[^/{}]+)*\/webhook-events(\/.*)?$/;
99
+ /**
100
+ * Writes a success response's schema onto whichever 2xx key the scanner already
101
+ * emitted (Nest's default is 201 for `@Post`, 200 elsewhere, and a `@HttpCode`
102
+ * moves it), so this never invents a status code the API does not return. A
103
+ * `204` is left exactly as it is: it has no body, and describing one would be
104
+ * a lie about the delete route.
105
+ */
106
+ function setSuccessResponse(operation, schema, description) {
107
+ const responses = operation.responses ?? {};
108
+ const key = Object.keys(responses).find((code) => /^2\d\d$/.test(code));
109
+ if (!key || key === '204') {
110
+ return;
111
+ }
112
+ responses[key] = {
113
+ description,
114
+ content: { 'application/json': { schema } }
115
+ };
116
+ operation.responses = responses;
117
+ }
118
+ /** Adds a documented failure response, leaving any existing one alone. */
119
+ function addErrorResponse(operation, code, description) {
120
+ const responses = operation.responses ?? {};
121
+ if (responses[code]) {
122
+ return;
123
+ }
124
+ responses[code] = { description };
125
+ operation.responses = responses;
126
+ }
127
+ /** Adds this plugin's schemas and describes its own operations' responses. */
128
+ function describeWebhooksApi(document) {
129
+ document.components ??= {};
130
+ document.components.schemas ??= {};
131
+ Object.assign(document.components.schemas, (0, webhook_schemas_1.buildWebhookSchemas)());
132
+ const surfaces = [
133
+ [EVENT_ROUTE_RE, EVENT_ROUTES],
134
+ [ENDPOINT_ROUTE_RE, ENDPOINT_ROUTES]
135
+ ];
136
+ for (const [route, item] of Object.entries(document.paths)) {
137
+ let rest;
138
+ let routes;
139
+ for (const [pattern, candidate] of surfaces) {
140
+ const match = pattern.exec(route);
141
+ if (match) {
142
+ rest = match[1] ?? '';
143
+ routes = candidate;
144
+ break;
145
+ }
146
+ }
147
+ if (rest === undefined || !routes) {
148
+ continue;
149
+ }
150
+ const byMethod = routes[rest];
151
+ if (!byMethod) {
152
+ continue;
153
+ }
154
+ for (const [method, operation] of Object.entries(item)) {
155
+ const spec = byMethod[method];
156
+ if (!spec || !operation || typeof operation !== 'object') {
157
+ continue;
158
+ }
159
+ setSuccessResponse(operation, spec.list
160
+ ? { type: 'array', items: (0, webhook_schemas_1.ref)(spec.schema) }
161
+ : (0, webhook_schemas_1.ref)(spec.schema), spec.description);
162
+ if (rest !== '') {
163
+ addErrorResponse(operation, '404', 'No endpoint with this id — or, on a delivery route, no delivery with this id belonging to it.');
164
+ }
165
+ if (spec.validatesUrl) {
166
+ addErrorResponse(operation, '422', 'The URL was refused by policy (scheme, or a private/loopback address), or a header name is reserved for the delivery’s own metadata.');
167
+ }
168
+ }
169
+ }
170
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The OpenAPI schemas the webhooks plugin contributes.
3
+ *
4
+ * Every response view in this package is a TypeScript `interface`, which is
5
+ * erased before `@nestjs/swagger` ever runs — so the scanner emits an empty
6
+ * `200` for each route and a reader learns nothing. These are the same shapes
7
+ * written as plain schema objects, attached to the finished document by
8
+ * {@link describeWebhooksApi}.
9
+ *
10
+ * The vocabularies (`DELIVERY_STATUSES`, the event catalogue) are **imported**
11
+ * from `@orthacms/webhooks-domain` rather than restated: an enum in the
12
+ * reference that drifts from the one the server validates against is worse
13
+ * than a bare string, because it looks authoritative.
14
+ */
15
+ /** A JSON Schema fragment, as it appears in the OpenAPI document. */
16
+ export type OpenApiSchema = Record<string, unknown>;
17
+ /** `#/components/schemas/<name>`. */
18
+ export declare function ref(name: string): OpenApiSchema;
19
+ /** Every schema this plugin adds, keyed by component name. */
20
+ export declare function buildWebhookSchemas(): Record<string, OpenApiSchema>;
21
+ //# sourceMappingURL=webhook-schemas.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-schemas.d.ts","sourceRoot":"","sources":["../../../src/lib/docs/webhook-schemas.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAQH,qEAAqE;AACrE,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEpD,qCAAqC;AACrC,wBAAgB,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,CAE/C;AAuWD,8DAA8D;AAC9D,wBAAgB,mBAAmB,IAAI,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAYnE"}
@@ -0,0 +1,360 @@
1
+ "use strict";
2
+ /**
3
+ * The OpenAPI schemas the webhooks plugin contributes.
4
+ *
5
+ * Every response view in this package is a TypeScript `interface`, which is
6
+ * erased before `@nestjs/swagger` ever runs — so the scanner emits an empty
7
+ * `200` for each route and a reader learns nothing. These are the same shapes
8
+ * written as plain schema objects, attached to the finished document by
9
+ * {@link describeWebhooksApi}.
10
+ *
11
+ * The vocabularies (`DELIVERY_STATUSES`, the event catalogue) are **imported**
12
+ * from `@orthacms/webhooks-domain` rather than restated: an enum in the
13
+ * reference that drifts from the one the server validates against is worse
14
+ * than a bare string, because it looks authoritative.
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.ref = ref;
18
+ exports.buildWebhookSchemas = buildWebhookSchemas;
19
+ const webhooks_domain_1 = require("@orthacms/webhooks-domain");
20
+ /** `#/components/schemas/<name>`. */
21
+ function ref(name) {
22
+ return { $ref: `#/components/schemas/${name}` };
23
+ }
24
+ const UUID = { type: 'string', format: 'uuid' };
25
+ const DATE_TIME = { type: 'string', format: 'date-time' };
26
+ /** A nullable `date-time`, which several delivery timestamps are. */
27
+ const NULLABLE_DATE_TIME = {
28
+ type: 'string',
29
+ format: 'date-time',
30
+ nullable: true
31
+ };
32
+ /** The delivery state vocabulary, straight from the domain. */
33
+ const DELIVERY_STATUS = {
34
+ type: 'string',
35
+ enum: [...webhooks_domain_1.DELIVERY_STATUSES],
36
+ description: '`succeeded` and `dead` are terminal; a `failed` delivery has another attempt scheduled.'
37
+ };
38
+ /**
39
+ * The endpoint as every read returns it.
40
+ *
41
+ * The absent property is the point: there is no `secret` here, and no route
42
+ * that answers with this schema can produce one. `secretHint` is the trailing
43
+ * four characters, which is enough to tell two secrets apart and far too little
44
+ * to shorten an attack on a 256-bit key.
45
+ */
46
+ const ENDPOINT = {
47
+ type: 'object',
48
+ description: 'A configured webhook endpoint. Never carries the signing secret — see WebhookEndpointWithSecret.',
49
+ required: [
50
+ 'id',
51
+ 'name',
52
+ 'url',
53
+ 'secretHint',
54
+ 'enabled',
55
+ 'eventKinds',
56
+ 'contentTypes',
57
+ 'allWorkspaces',
58
+ 'workspaceIds',
59
+ 'headers',
60
+ 'disabledReason',
61
+ 'consecutiveFailures',
62
+ 'createdAt',
63
+ 'updatedAt',
64
+ 'lastDelivery'
65
+ ],
66
+ properties: {
67
+ id: UUID,
68
+ name: { type: 'string' },
69
+ url: {
70
+ type: 'string',
71
+ format: 'uri',
72
+ description: 'Where deliveries are POSTed.'
73
+ },
74
+ secretHint: {
75
+ type: 'string',
76
+ description: 'The last four characters of the signing secret. The secret itself is returned only on create and rotate.'
77
+ },
78
+ enabled: { type: 'boolean' },
79
+ eventKinds: {
80
+ type: 'array',
81
+ items: { type: 'string', enum: [...webhooks_domain_1.WEBHOOK_EVENT_KINDS] },
82
+ description: 'The subscribed kinds. **Empty means every kind, including ones added later.**'
83
+ },
84
+ contentTypes: {
85
+ type: 'array',
86
+ items: { type: 'string' },
87
+ description: 'Content types this endpoint is scoped to. Empty means every type, including ones that do not exist yet.'
88
+ },
89
+ allWorkspaces: {
90
+ type: 'boolean',
91
+ description: 'True when the endpoint spans every workspace.'
92
+ },
93
+ workspaceIds: {
94
+ type: 'array',
95
+ items: UUID,
96
+ description: 'Empty when `allWorkspaces` is true.'
97
+ },
98
+ headers: {
99
+ type: 'object',
100
+ additionalProperties: { type: 'string' },
101
+ description: 'Extra request headers sent with every delivery.'
102
+ },
103
+ disabledReason: {
104
+ type: 'string',
105
+ nullable: true,
106
+ description: 'Set when the endpoint switched itself off after consecutive failures; null otherwise.'
107
+ },
108
+ consecutiveFailures: { type: 'integer' },
109
+ createdAt: DATE_TIME,
110
+ updatedAt: DATE_TIME,
111
+ // A bare `$ref`: OpenAPI 3.0 ignores keywords written beside one, so
112
+ // "or null" lives on the referenced schema (`nullable`) rather than in
113
+ // a sibling `description` that no reader would ever be shown.
114
+ lastDelivery: ref('WebhookLastDelivery')
115
+ }
116
+ };
117
+ /**
118
+ * The one-line delivery summary the list column renders.
119
+ *
120
+ * Nullable through the property that references it rather than here: OpenAPI
121
+ * 3.0 ignores sibling keywords next to a `$ref`, so `lastDelivery` is described
122
+ * as `allOf`-free and this schema itself carries the `nullable`.
123
+ */
124
+ const LAST_DELIVERY = {
125
+ type: 'object',
126
+ nullable: true,
127
+ required: ['id', 'status', 'eventKind', 'statusCode', 'createdAt'],
128
+ properties: {
129
+ id: UUID,
130
+ status: DELIVERY_STATUS,
131
+ eventKind: { type: 'string' },
132
+ statusCode: {
133
+ type: 'integer',
134
+ nullable: true,
135
+ description: 'The receiver’s HTTP status, or null if nothing answered.'
136
+ },
137
+ createdAt: DATE_TIME
138
+ }
139
+ };
140
+ /**
141
+ * Create and rotate — and **only** those two — answer with the endpoint plus
142
+ * the secret.
143
+ *
144
+ * A separate schema rather than an optional `secret` on {@link ENDPOINT}: an
145
+ * optional field says "sometimes present, look and see", which is exactly the
146
+ * belief that makes someone build a UI that reads it on a `GET` and shows an
147
+ * empty box forever. Two schemas say which two responses carry it.
148
+ */
149
+ const ENDPOINT_WITH_SECRET = {
150
+ type: 'object',
151
+ description: 'Returned by POST /webhooks and POST /webhooks/{id}/secret only. The secret is shown once and is never retrievable again — not by any read, and not by whoever created it.',
152
+ required: ['endpoint', 'secret'],
153
+ properties: {
154
+ endpoint: ref('WebhookEndpoint'),
155
+ secret: {
156
+ type: 'string',
157
+ pattern: '^whsec_',
158
+ description: 'The signing secret: `whsec_` followed by 32 random bytes, base64url-encoded. Store it now; every later read returns only `secretHint`.',
159
+ example: 'whsec_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
160
+ }
161
+ }
162
+ };
163
+ /** The delivery row, without the bodies. */
164
+ const DELIVERY = {
165
+ type: 'object',
166
+ description: 'One queued or completed delivery attempt-set.',
167
+ required: [
168
+ 'id',
169
+ 'endpointId',
170
+ 'eventId',
171
+ 'eventKind',
172
+ 'workspaceId',
173
+ 'contentType',
174
+ 'status',
175
+ 'attempts',
176
+ 'nextAttemptAt',
177
+ 'statusCode',
178
+ 'error',
179
+ 'durationMs',
180
+ 'createdAt',
181
+ 'completedAt'
182
+ ],
183
+ properties: {
184
+ id: {
185
+ ...UUID,
186
+ description: 'The delivery’s own id, sent as `X-Ortha-Delivery`. A redelivery gets a new one.'
187
+ },
188
+ endpointId: UUID,
189
+ eventId: {
190
+ ...UUID,
191
+ description: 'The originating outbox event, sent as `X-Ortha-Event-Id`. Stable across every redelivery — this is the key a receiver deduplicates on.'
192
+ },
193
+ eventKind: { type: 'string' },
194
+ workspaceId: { ...UUID, nullable: true },
195
+ contentType: { type: 'string', nullable: true },
196
+ status: DELIVERY_STATUS,
197
+ attempts: { type: 'integer' },
198
+ nextAttemptAt: {
199
+ ...NULLABLE_DATE_TIME,
200
+ description: 'When the next attempt is due; null when none is scheduled.'
201
+ },
202
+ statusCode: { type: 'integer', nullable: true },
203
+ error: { type: 'string', nullable: true },
204
+ durationMs: { type: 'integer', nullable: true },
205
+ createdAt: DATE_TIME,
206
+ completedAt: NULLABLE_DATE_TIME
207
+ }
208
+ };
209
+ /** The delivery row with the frozen request body and the response snippet. */
210
+ const DELIVERY_DETAIL = {
211
+ allOf: [
212
+ ref('WebhookDelivery'),
213
+ {
214
+ type: 'object',
215
+ required: ['payload', 'responseSnippet'],
216
+ properties: {
217
+ payload: ref('WebhookEnvelope'),
218
+ responseSnippet: {
219
+ type: 'string',
220
+ nullable: true,
221
+ description: 'The first couple of kilobytes of the receiver’s response body, or null when nothing answered.'
222
+ }
223
+ }
224
+ }
225
+ ],
226
+ description: 'A delivery with the exact body that was, or will be, posted.'
227
+ };
228
+ /**
229
+ * The body a receiver is sent.
230
+ *
231
+ * Described here because it is frozen onto the delivery row and handed back by
232
+ * the detail route, so the reference is where an integrator reads what their
233
+ * own handler will be parsing. It carries **references, not content**: a
234
+ * receiver reads the record back through the public API with its own token, so
235
+ * the read still passes through visibility rules and audience entitlements.
236
+ */
237
+ const ENVELOPE = {
238
+ type: 'object',
239
+ description: 'The JSON body POSTed to a receiver, signed with the endpoint’s secret in `X-Ortha-Signature`.',
240
+ required: [
241
+ 'id',
242
+ 'event',
243
+ 'eventId',
244
+ 'occurredAt',
245
+ 'workspaceId',
246
+ 'actor',
247
+ 'data'
248
+ ],
249
+ properties: {
250
+ id: {
251
+ ...UUID,
252
+ description: 'The delivery id. Changes on a redelivery.'
253
+ },
254
+ event: { type: 'string', description: 'The event kind.' },
255
+ eventId: {
256
+ ...UUID,
257
+ description: 'The outbox event id — stable across redeliveries, and the receiver’s deduplication key.'
258
+ },
259
+ occurredAt: {
260
+ ...DATE_TIME,
261
+ description: 'Domain time: when the fact happened, not when it was sent.'
262
+ },
263
+ workspaceId: { ...UUID, nullable: true },
264
+ actor: {
265
+ type: 'object',
266
+ nullable: true,
267
+ description: 'Who did it, or null for a token-authenticated or system write.',
268
+ required: ['id', 'email'],
269
+ properties: {
270
+ id: UUID,
271
+ email: { type: 'string', nullable: true }
272
+ }
273
+ },
274
+ data: {
275
+ type: 'object',
276
+ description: 'What the event is about: the aggregate’s `kind` and `id`, plus whatever else the kind carries.',
277
+ required: ['kind', 'id'],
278
+ properties: {
279
+ kind: {
280
+ type: 'string',
281
+ description: 'The aggregate type, e.g. `content_entry`.',
282
+ example: 'content_entry'
283
+ },
284
+ id: UUID
285
+ },
286
+ additionalProperties: true
287
+ }
288
+ }
289
+ };
290
+ /** One page of the delivery log. */
291
+ const DELIVERY_PAGE = {
292
+ type: 'object',
293
+ required: ['items', 'total', 'page', 'pageSize', 'pageCount'],
294
+ properties: {
295
+ items: { type: 'array', items: ref('WebhookDelivery') },
296
+ total: { type: 'integer' },
297
+ page: { type: 'integer' },
298
+ pageSize: { type: 'integer' },
299
+ pageCount: { type: 'integer' }
300
+ }
301
+ };
302
+ /** What "Send test" reports. Synchronous — nothing is queued. */
303
+ const TEST_RESULT = {
304
+ type: 'object',
305
+ description: 'The outcome of a synthetic `ping`, reported synchronously. It is not queued and does not count towards automatic disabling.',
306
+ required: ['ok', 'statusCode', 'error', 'durationMs', 'responseSnippet'],
307
+ properties: {
308
+ ok: {
309
+ type: 'boolean',
310
+ description: 'Whether the receiver answered 2xx.'
311
+ },
312
+ statusCode: { type: 'integer', nullable: true },
313
+ error: {
314
+ type: 'string',
315
+ nullable: true,
316
+ description: 'Why it failed, when it did.'
317
+ },
318
+ durationMs: { type: 'integer' },
319
+ responseSnippet: { type: 'string', nullable: true }
320
+ }
321
+ };
322
+ /** One entry of the subscribable-event catalogue. */
323
+ const EVENT_DESCRIPTOR = {
324
+ type: 'object',
325
+ description: 'One kind an endpoint may subscribe to. Read this rather than compiling a list into a client: a newer server offers the kinds it actually knows about.',
326
+ required: [
327
+ 'kind',
328
+ 'group',
329
+ 'label',
330
+ 'scopedByContentType',
331
+ 'carriesWorkspace'
332
+ ],
333
+ properties: {
334
+ kind: { type: 'string', enum: [...webhooks_domain_1.WEBHOOK_EVENT_KINDS] },
335
+ group: { type: 'string', enum: [...webhooks_domain_1.WEBHOOK_EVENT_GROUPS] },
336
+ label: { type: 'string', description: 'Default English label.' },
337
+ scopedByContentType: {
338
+ type: 'boolean',
339
+ description: 'Whether an endpoint’s content-type filter applies to this kind.'
340
+ },
341
+ carriesWorkspace: {
342
+ type: 'boolean',
343
+ description: 'Whether an endpoint’s workspace filter applies to this kind.'
344
+ }
345
+ }
346
+ };
347
+ /** Every schema this plugin adds, keyed by component name. */
348
+ function buildWebhookSchemas() {
349
+ return {
350
+ WebhookEndpoint: ENDPOINT,
351
+ WebhookLastDelivery: LAST_DELIVERY,
352
+ WebhookEndpointWithSecret: ENDPOINT_WITH_SECRET,
353
+ WebhookDelivery: DELIVERY,
354
+ WebhookDeliveryDetail: DELIVERY_DETAIL,
355
+ WebhookDeliveryPage: DELIVERY_PAGE,
356
+ WebhookEnvelope: ENVELOPE,
357
+ WebhookTestResult: TEST_RESULT,
358
+ WebhookEventDescriptor: EVENT_DESCRIPTOR
359
+ };
360
+ }
@@ -0,0 +1,3 @@
1
+ export { WebhookEndpointNotFoundError } from './webhook-endpoint-not-found.error';
2
+ export { WebhookDeliveryNotFoundError } from './webhook-delivery-not-found.error';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/domain/errors/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,4BAA4B,EAAE,MAAM,oCAAoC,CAAC;AAClF,OAAO,EAAE,4BAA4B,EAAE,MAAM,oCAAoC,CAAC"}
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WebhookDeliveryNotFoundError = exports.WebhookEndpointNotFoundError = void 0;
4
+ var webhook_endpoint_not_found_error_1 = require("./webhook-endpoint-not-found.error");
5
+ Object.defineProperty(exports, "WebhookEndpointNotFoundError", { enumerable: true, get: function () { return webhook_endpoint_not_found_error_1.WebhookEndpointNotFoundError; } });
6
+ var webhook_delivery_not_found_error_1 = require("./webhook-delivery-not-found.error");
7
+ Object.defineProperty(exports, "WebhookDeliveryNotFoundError", { enumerable: true, get: function () { return webhook_delivery_not_found_error_1.WebhookDeliveryNotFoundError; } });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Raised when a delivery id does not name a row **on the endpoint asked for**.
3
+ *
4
+ * Scoped to the endpoint deliberately: a delivery that exists elsewhere and one
5
+ * that does not exist at all must be indistinguishable, or the log becomes a
6
+ * way to probe for ids across endpoints.
7
+ */
8
+ export declare class WebhookDeliveryNotFoundError extends Error {
9
+ readonly deliveryId: string;
10
+ constructor(deliveryId: string);
11
+ }
12
+ //# sourceMappingURL=webhook-delivery-not-found.error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-delivery-not-found.error.d.ts","sourceRoot":"","sources":["../../../../src/lib/domain/errors/webhook-delivery-not-found.error.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,qBAAa,4BAA6B,SAAQ,KAAK;aACvB,UAAU,EAAE,MAAM;gBAAlB,UAAU,EAAE,MAAM;CAIjD"}
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WebhookDeliveryNotFoundError = void 0;
4
+ /**
5
+ * Raised when a delivery id does not name a row **on the endpoint asked for**.
6
+ *
7
+ * Scoped to the endpoint deliberately: a delivery that exists elsewhere and one
8
+ * that does not exist at all must be indistinguishable, or the log becomes a
9
+ * way to probe for ids across endpoints.
10
+ */
11
+ class WebhookDeliveryNotFoundError extends Error {
12
+ deliveryId;
13
+ constructor(deliveryId) {
14
+ super(`Webhook delivery ${deliveryId} was not found.`);
15
+ this.deliveryId = deliveryId;
16
+ this.name = 'WebhookDeliveryNotFoundError';
17
+ }
18
+ }
19
+ exports.WebhookDeliveryNotFoundError = WebhookDeliveryNotFoundError;
@@ -0,0 +1,6 @@
1
+ /** Raised when an endpoint id does not name a row. Mapped to 404 by the http layer. */
2
+ export declare class WebhookEndpointNotFoundError extends Error {
3
+ readonly endpointId: string;
4
+ constructor(endpointId: string);
5
+ }
6
+ //# sourceMappingURL=webhook-endpoint-not-found.error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-endpoint-not-found.error.d.ts","sourceRoot":"","sources":["../../../../src/lib/domain/errors/webhook-endpoint-not-found.error.ts"],"names":[],"mappings":"AAAA,uFAAuF;AACvF,qBAAa,4BAA6B,SAAQ,KAAK;aACvB,UAAU,EAAE,MAAM;gBAAlB,UAAU,EAAE,MAAM;CAIjD"}
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WebhookEndpointNotFoundError = void 0;
4
+ /** Raised when an endpoint id does not name a row. Mapped to 404 by the http layer. */
5
+ class WebhookEndpointNotFoundError extends Error {
6
+ endpointId;
7
+ constructor(endpointId) {
8
+ super(`Webhook endpoint ${endpointId} was not found.`);
9
+ this.endpointId = endpointId;
10
+ this.name = 'WebhookEndpointNotFoundError';
11
+ }
12
+ }
13
+ exports.WebhookEndpointNotFoundError = WebhookEndpointNotFoundError;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The prefix every signing secret carries, so one found in a log or a config
3
+ * file is recognisable for what it is — and so a scanner can be taught to spot
4
+ * a leaked one.
5
+ */
6
+ export declare const SECRET_PREFIX = "whsec_";
7
+ /** A freshly minted signing secret and the hint stored alongside it. */
8
+ export interface GeneratedSecret {
9
+ /** The full secret. Shown to the operator exactly once. */
10
+ secret: string;
11
+ /** The trailing characters, safe to display forever. */
12
+ hint: string;
13
+ }
14
+ /**
15
+ * Mints a signing secret.
16
+ *
17
+ * 32 bytes of `randomBytes` — the same order of entropy as the API tokens, and
18
+ * far more than an HMAC key needs. Base64url so it survives a shell, a YAML
19
+ * file and an environment variable without quoting.
20
+ */
21
+ export declare function generateSecret(): GeneratedSecret;
22
+ /**
23
+ * The displayable tail of a secret.
24
+ *
25
+ * Four characters is enough to tell "the one I rotated to" from "the one I
26
+ * replaced" and far too little to shorten a brute force against a 256-bit key.
27
+ */
28
+ export declare function secretHint(secret: string): string;
29
+ //# sourceMappingURL=webhook-secret.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-secret.d.ts","sourceRoot":"","sources":["../../../src/lib/domain/webhook-secret.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,eAAO,MAAM,aAAa,WAAW,CAAC;AAKtC,wEAAwE;AACxE,MAAM,WAAW,eAAe;IAC5B,2DAA2D;IAC3D,MAAM,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,IAAI,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,IAAI,eAAe,CAGhD;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEjD"}