@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ortha CMS contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # @orthacms/webhooks-server
2
+
3
+ Part of [Ortha CMS](https://github.com/ortha-source/ortha-cms).
4
+
5
+ ```sh
6
+ npm install @orthacms/webhooks-server
7
+ ```
@@ -0,0 +1,19 @@
1
+ /**
2
+ * `@orthacms/webhooks-server` — outgoing webhooks on content changes.
3
+ *
4
+ * Subscribes to the transactional outbox, queues one delivery row per
5
+ * subscribed endpoint, and sends them from a worker that holds no transaction
6
+ * while it waits on someone else's server. Owns three tables and ships their
7
+ * migrations; the rules about what a delivery *means* live in
8
+ * `@orthacms/webhooks-domain`.
9
+ */
10
+ export { WebhooksPlugin, type WebhooksServerPluginType } from './lib/utils/webhooks-plugin';
11
+ export { WebhooksModule } from './lib/webhooks.module';
12
+ export { WEBHOOKS_DEFAULTS, resolveWebhooksConfig, type ResolvedWebhooksConfig, type WebhooksPluginConfig } from './lib/types/webhooks-config';
13
+ export { InjectWebhooksConfig, WEBHOOKS_CONFIG } from './lib/webhooks.tokens';
14
+ export { WebhookEndpointsService, type WebhookEndpointWithSecret } from './lib/application/webhook-endpoints.service';
15
+ export { WebhookDeliveryWorker } from './lib/infrastructure/webhook-delivery.worker';
16
+ export { WebhookDeliveryNotFoundError, WebhookEndpointNotFoundError } from './lib/domain/errors';
17
+ export type { WebhookDeliveryDetailView, WebhookDeliveryPage, WebhookDeliveryView, WebhookEndpointView, WebhookLastDeliveryView, WebhookTestResult } from './lib/domain/webhook-views';
18
+ export { webhookDeliveries, webhookEndpointWorkspaces, webhookEndpoints } from './lib/infrastructure/schema';
19
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EACH,cAAc,EACd,KAAK,wBAAwB,EAChC,MAAM,6BAA6B,CAAC;AAErC,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEvD,OAAO,EACH,iBAAiB,EACjB,qBAAqB,EACrB,KAAK,sBAAsB,EAC3B,KAAK,oBAAoB,EAC5B,MAAM,6BAA6B,CAAC;AAErC,OAAO,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE9E,OAAO,EACH,uBAAuB,EACvB,KAAK,yBAAyB,EACjC,MAAM,6CAA6C,CAAC;AAErD,OAAO,EAAE,qBAAqB,EAAE,MAAM,8CAA8C,CAAC;AAErF,OAAO,EACH,4BAA4B,EAC5B,4BAA4B,EAC/B,MAAM,qBAAqB,CAAC;AAE7B,YAAY,EACR,yBAAyB,EACzB,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,uBAAuB,EACvB,iBAAiB,EACpB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EACH,iBAAiB,EACjB,yBAAyB,EACzB,gBAAgB,EACnB,MAAM,6BAA6B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ /**
3
+ * `@orthacms/webhooks-server` — outgoing webhooks on content changes.
4
+ *
5
+ * Subscribes to the transactional outbox, queues one delivery row per
6
+ * subscribed endpoint, and sends them from a worker that holds no transaction
7
+ * while it waits on someone else's server. Owns three tables and ships their
8
+ * migrations; the rules about what a delivery *means* live in
9
+ * `@orthacms/webhooks-domain`.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.webhookEndpoints = exports.webhookEndpointWorkspaces = exports.webhookDeliveries = exports.WebhookEndpointNotFoundError = exports.WebhookDeliveryNotFoundError = exports.WebhookDeliveryWorker = exports.WebhookEndpointsService = exports.WEBHOOKS_CONFIG = exports.InjectWebhooksConfig = exports.resolveWebhooksConfig = exports.WEBHOOKS_DEFAULTS = exports.WebhooksModule = exports.WebhooksPlugin = void 0;
13
+ var webhooks_plugin_1 = require("./lib/utils/webhooks-plugin");
14
+ Object.defineProperty(exports, "WebhooksPlugin", { enumerable: true, get: function () { return webhooks_plugin_1.WebhooksPlugin; } });
15
+ var webhooks_module_1 = require("./lib/webhooks.module");
16
+ Object.defineProperty(exports, "WebhooksModule", { enumerable: true, get: function () { return webhooks_module_1.WebhooksModule; } });
17
+ var webhooks_config_1 = require("./lib/types/webhooks-config");
18
+ Object.defineProperty(exports, "WEBHOOKS_DEFAULTS", { enumerable: true, get: function () { return webhooks_config_1.WEBHOOKS_DEFAULTS; } });
19
+ Object.defineProperty(exports, "resolveWebhooksConfig", { enumerable: true, get: function () { return webhooks_config_1.resolveWebhooksConfig; } });
20
+ var webhooks_tokens_1 = require("./lib/webhooks.tokens");
21
+ Object.defineProperty(exports, "InjectWebhooksConfig", { enumerable: true, get: function () { return webhooks_tokens_1.InjectWebhooksConfig; } });
22
+ Object.defineProperty(exports, "WEBHOOKS_CONFIG", { enumerable: true, get: function () { return webhooks_tokens_1.WEBHOOKS_CONFIG; } });
23
+ var webhook_endpoints_service_1 = require("./lib/application/webhook-endpoints.service");
24
+ Object.defineProperty(exports, "WebhookEndpointsService", { enumerable: true, get: function () { return webhook_endpoints_service_1.WebhookEndpointsService; } });
25
+ var webhook_delivery_worker_1 = require("./lib/infrastructure/webhook-delivery.worker");
26
+ Object.defineProperty(exports, "WebhookDeliveryWorker", { enumerable: true, get: function () { return webhook_delivery_worker_1.WebhookDeliveryWorker; } });
27
+ var errors_1 = require("./lib/domain/errors");
28
+ Object.defineProperty(exports, "WebhookDeliveryNotFoundError", { enumerable: true, get: function () { return errors_1.WebhookDeliveryNotFoundError; } });
29
+ Object.defineProperty(exports, "WebhookEndpointNotFoundError", { enumerable: true, get: function () { return errors_1.WebhookEndpointNotFoundError; } });
30
+ var schema_1 = require("./lib/infrastructure/schema");
31
+ Object.defineProperty(exports, "webhookDeliveries", { enumerable: true, get: function () { return schema_1.webhookDeliveries; } });
32
+ Object.defineProperty(exports, "webhookEndpointWorkspaces", { enumerable: true, get: function () { return schema_1.webhookEndpointWorkspaces; } });
33
+ Object.defineProperty(exports, "webhookEndpoints", { enumerable: true, get: function () { return schema_1.webhookEndpoints; } });
@@ -0,0 +1,8 @@
1
+ /** Filters and paging for `GET /api/webhooks/:id/deliveries`. */
2
+ export declare class ListDeliveriesQueryDto {
3
+ status?: string;
4
+ eventKind?: string;
5
+ page?: number;
6
+ pageSize?: number;
7
+ }
8
+ //# sourceMappingURL=list-deliveries-query.dto.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list-deliveries-query.dto.d.ts","sourceRoot":"","sources":["../../../../src/lib/application/dto/list-deliveries-query.dto.ts"],"names":[],"mappings":"AAWA,iEAAiE;AACjE,qBAAa,sBAAsB;IAO/B,MAAM,CAAC,EAAE,MAAM,CAAC;IAQhB,SAAS,CAAC,EAAE,MAAM,CAAC;IAYnB,IAAI,CAAC,EAAE,MAAM,CAAC;IAcd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB"}
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ListDeliveriesQueryDto = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const swagger_1 = require("@nestjs/swagger");
6
+ const webhooks_domain_1 = require("@orthacms/webhooks-domain");
7
+ const class_transformer_1 = require("class-transformer");
8
+ const class_validator_1 = require("class-validator");
9
+ /** The largest page the delivery log will return. */
10
+ const MAX_PAGE_SIZE = 100;
11
+ /** Filters and paging for `GET /api/webhooks/:id/deliveries`. */
12
+ class ListDeliveriesQueryDto {
13
+ status;
14
+ eventKind;
15
+ page;
16
+ pageSize;
17
+ }
18
+ exports.ListDeliveriesQueryDto = ListDeliveriesQueryDto;
19
+ tslib_1.__decorate([
20
+ (0, swagger_1.ApiPropertyOptional)({
21
+ description: 'Only deliveries in this state.',
22
+ enum: webhooks_domain_1.DELIVERY_STATUSES
23
+ }),
24
+ (0, class_validator_1.IsOptional)(),
25
+ (0, class_validator_1.IsIn)(webhooks_domain_1.DELIVERY_STATUSES),
26
+ tslib_1.__metadata("design:type", String)
27
+ ], ListDeliveriesQueryDto.prototype, "status", void 0);
28
+ tslib_1.__decorate([
29
+ (0, swagger_1.ApiPropertyOptional)({
30
+ description: 'Only deliveries of this event kind.',
31
+ enum: webhooks_domain_1.WEBHOOK_EVENT_KINDS
32
+ }),
33
+ (0, class_validator_1.IsOptional)(),
34
+ (0, class_validator_1.IsIn)(webhooks_domain_1.WEBHOOK_EVENT_KINDS),
35
+ tslib_1.__metadata("design:type", String)
36
+ ], ListDeliveriesQueryDto.prototype, "eventKind", void 0);
37
+ tslib_1.__decorate([
38
+ (0, swagger_1.ApiPropertyOptional)({
39
+ description: '1-based page number.',
40
+ type: 'integer',
41
+ minimum: 1,
42
+ default: 1
43
+ }),
44
+ (0, class_validator_1.IsOptional)(),
45
+ (0, class_transformer_1.Type)(() => Number),
46
+ (0, class_validator_1.IsInt)(),
47
+ (0, class_validator_1.Min)(1),
48
+ tslib_1.__metadata("design:type", Number)
49
+ ], ListDeliveriesQueryDto.prototype, "page", void 0);
50
+ tslib_1.__decorate([
51
+ (0, swagger_1.ApiPropertyOptional)({
52
+ description: 'Rows per page.',
53
+ type: 'integer',
54
+ minimum: 1,
55
+ maximum: MAX_PAGE_SIZE,
56
+ default: 25
57
+ }),
58
+ (0, class_validator_1.IsOptional)(),
59
+ (0, class_transformer_1.Type)(() => Number),
60
+ (0, class_validator_1.IsInt)(),
61
+ (0, class_validator_1.Min)(1),
62
+ (0, class_validator_1.Max)(MAX_PAGE_SIZE),
63
+ tslib_1.__metadata("design:type", Number)
64
+ ], ListDeliveriesQueryDto.prototype, "pageSize", void 0);
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The body of a create or update.
3
+ *
4
+ * The three filter arrays are **optional, and an omitted or empty one means
5
+ * "everything"** — including things that do not exist yet. That is the whole
6
+ * subscription model, and it is why the shape is three plain lists rather than
7
+ * an expression.
8
+ *
9
+ * The URL is only shape-checked here (`@IsString`); whether it is *reachable*
10
+ * under this deployment's policy is decided in the service, which owns the
11
+ * policy and can say why in a message worth showing.
12
+ */
13
+ export declare class SaveWebhookEndpointDto {
14
+ name: string;
15
+ url: string;
16
+ enabled?: boolean;
17
+ eventKinds?: string[];
18
+ contentTypes?: string[];
19
+ allWorkspaces?: boolean;
20
+ workspaceIds?: string[];
21
+ headers?: Record<string, string>;
22
+ }
23
+ //# sourceMappingURL=save-webhook-endpoint.dto.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"save-webhook-endpoint.dto.d.ts","sourceRoot":"","sources":["../../../../src/lib/application/dto/save-webhook-endpoint.dto.ts"],"names":[],"mappings":"AAoBA;;;;;;;;;;;GAWG;AACH,qBAAa,sBAAsB;IAS/B,IAAI,EAAG,MAAM,CAAC;IAWd,GAAG,EAAG,MAAM,CAAC;IAQb,OAAO,CAAC,EAAE,OAAO,CAAC;IAelB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IAatB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IASxB,aAAa,CAAC,EAAE,OAAO,CAAC;IAaxB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IAWxB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACpC"}
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SaveWebhookEndpointDto = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const swagger_1 = require("@nestjs/swagger");
6
+ const webhooks_domain_1 = require("@orthacms/webhooks-domain");
7
+ const class_validator_1 = require("class-validator");
8
+ /** Bounds on the free-text fields, restated in the OpenAPI schema below. */
9
+ const NAME_MAX = 120;
10
+ const URL_MAX = 2_048;
11
+ const LIST_MAX = 200;
12
+ /**
13
+ * The body of a create or update.
14
+ *
15
+ * The three filter arrays are **optional, and an omitted or empty one means
16
+ * "everything"** — including things that do not exist yet. That is the whole
17
+ * subscription model, and it is why the shape is three plain lists rather than
18
+ * an expression.
19
+ *
20
+ * The URL is only shape-checked here (`@IsString`); whether it is *reachable*
21
+ * under this deployment's policy is decided in the service, which owns the
22
+ * policy and can say why in a message worth showing.
23
+ */
24
+ class SaveWebhookEndpointDto {
25
+ name;
26
+ url;
27
+ enabled;
28
+ eventKinds;
29
+ contentTypes;
30
+ allWorkspaces;
31
+ workspaceIds;
32
+ headers;
33
+ }
34
+ exports.SaveWebhookEndpointDto = SaveWebhookEndpointDto;
35
+ tslib_1.__decorate([
36
+ (0, swagger_1.ApiProperty)({
37
+ description: 'Human-readable name shown in the webhooks list.',
38
+ maxLength: NAME_MAX,
39
+ example: 'Rebuild the storefront'
40
+ }),
41
+ (0, class_validator_1.IsString)(),
42
+ (0, class_validator_1.MinLength)(1),
43
+ (0, class_validator_1.MaxLength)(NAME_MAX),
44
+ tslib_1.__metadata("design:type", String)
45
+ ], SaveWebhookEndpointDto.prototype, "name", void 0);
46
+ tslib_1.__decorate([
47
+ (0, swagger_1.ApiProperty)({
48
+ description: 'Where deliveries are POSTed. Must be https:// and resolve to a public address unless the deployment allows otherwise.',
49
+ maxLength: URL_MAX,
50
+ example: 'https://example.com/hooks/ortha'
51
+ }),
52
+ (0, class_validator_1.IsString)(),
53
+ (0, class_validator_1.MinLength)(1),
54
+ (0, class_validator_1.MaxLength)(URL_MAX),
55
+ tslib_1.__metadata("design:type", String)
56
+ ], SaveWebhookEndpointDto.prototype, "url", void 0);
57
+ tslib_1.__decorate([
58
+ (0, swagger_1.ApiPropertyOptional)({
59
+ description: 'Whether the endpoint receives deliveries.',
60
+ default: true
61
+ }),
62
+ (0, class_validator_1.IsOptional)(),
63
+ (0, class_validator_1.IsBoolean)(),
64
+ tslib_1.__metadata("design:type", Boolean)
65
+ ], SaveWebhookEndpointDto.prototype, "enabled", void 0);
66
+ tslib_1.__decorate([
67
+ (0, swagger_1.ApiPropertyOptional)({
68
+ description: 'Event kinds to subscribe to. Omit or leave empty to receive every kind, including ones added later.',
69
+ type: [String],
70
+ enum: webhooks_domain_1.WEBHOOK_EVENT_KINDS,
71
+ maxItems: LIST_MAX
72
+ }),
73
+ (0, class_validator_1.IsOptional)(),
74
+ (0, class_validator_1.IsArray)(),
75
+ (0, class_validator_1.ArrayMaxSize)(LIST_MAX)
76
+ // Derived from the catalogue, so a new kind is subscribable the moment it
77
+ // is added there and a removed one stops validating.
78
+ ,
79
+ (0, class_validator_1.IsIn)(webhooks_domain_1.WEBHOOK_EVENT_KINDS, { each: true }),
80
+ tslib_1.__metadata("design:type", Array)
81
+ ], SaveWebhookEndpointDto.prototype, "eventKinds", void 0);
82
+ tslib_1.__decorate([
83
+ (0, swagger_1.ApiPropertyOptional)({
84
+ description: 'Content types to subscribe to. Omit or leave empty to receive every type.',
85
+ type: [String],
86
+ maxItems: LIST_MAX
87
+ }),
88
+ (0, class_validator_1.IsOptional)(),
89
+ (0, class_validator_1.IsArray)(),
90
+ (0, class_validator_1.ArrayMaxSize)(LIST_MAX),
91
+ (0, class_validator_1.IsString)({ each: true }),
92
+ (0, class_validator_1.MaxLength)(200, { each: true }),
93
+ tslib_1.__metadata("design:type", Array)
94
+ ], SaveWebhookEndpointDto.prototype, "contentTypes", void 0);
95
+ tslib_1.__decorate([
96
+ (0, swagger_1.ApiPropertyOptional)({
97
+ description: 'Receive events from every workspace, including workspaces created later. When true, workspaceIds is ignored.',
98
+ default: false
99
+ }),
100
+ (0, class_validator_1.IsOptional)(),
101
+ (0, class_validator_1.IsBoolean)(),
102
+ tslib_1.__metadata("design:type", Boolean)
103
+ ], SaveWebhookEndpointDto.prototype, "allWorkspaces", void 0);
104
+ tslib_1.__decorate([
105
+ (0, swagger_1.ApiPropertyOptional)({
106
+ description: 'Workspaces to receive events from. Ignored when allWorkspaces is true; an empty list with allWorkspaces false receives nothing.',
107
+ type: [String],
108
+ format: 'uuid',
109
+ maxItems: LIST_MAX
110
+ }),
111
+ (0, class_validator_1.IsOptional)(),
112
+ (0, class_validator_1.IsArray)(),
113
+ (0, class_validator_1.ArrayMaxSize)(LIST_MAX),
114
+ (0, class_validator_1.IsUUID)('4', { each: true }),
115
+ tslib_1.__metadata("design:type", Array)
116
+ ], SaveWebhookEndpointDto.prototype, "workspaceIds", void 0);
117
+ tslib_1.__decorate([
118
+ (0, swagger_1.ApiPropertyOptional)({
119
+ description: 'Extra static headers sent with every delivery. Delivery metadata headers (X-Ortha-*) and transport headers cannot be set.',
120
+ type: 'object',
121
+ additionalProperties: { type: 'string' },
122
+ example: { Authorization: 'Bearer …' }
123
+ }),
124
+ (0, class_validator_1.IsOptional)(),
125
+ (0, class_validator_1.IsObject)(),
126
+ tslib_1.__metadata("design:type", Object)
127
+ ], SaveWebhookEndpointDto.prototype, "headers", void 0);
@@ -0,0 +1,82 @@
1
+ import type { WebhookDeliveryDetailView, WebhookDeliveryPage, WebhookDeliveryView, WebhookEndpointView, WebhookTestResult } from '../domain/webhook-views';
2
+ import { WebhookDeliveryRepository, type DeliveryListQuery } from '../infrastructure/webhook-delivery.repository';
3
+ import { WebhookEndpointRepository, type EndpointWriteModel } from '../infrastructure/webhook-endpoint.repository';
4
+ import { WebhookHttpClient } from '../infrastructure/webhook-http.client';
5
+ import type { ResolvedWebhooksConfig } from '../types/webhooks-config';
6
+ /** An endpoint plus the secret, returned only from create and rotate. */
7
+ export interface WebhookEndpointWithSecret {
8
+ endpoint: WebhookEndpointView;
9
+ /** The plaintext signing secret. This is the only time it is ever returned. */
10
+ secret: string;
11
+ }
12
+ /** Orchestration over the two repositories. */
13
+ export declare class WebhookEndpointsService {
14
+ private readonly endpoints;
15
+ private readonly deliveries;
16
+ private readonly http;
17
+ private readonly config;
18
+ constructor(endpoints: WebhookEndpointRepository, deliveries: WebhookDeliveryRepository, http: WebhookHttpClient, config: ResolvedWebhooksConfig);
19
+ /** Every endpoint, newest first. */
20
+ list(): Promise<WebhookEndpointView[]>;
21
+ /** One endpoint. Throws {@link WebhookEndpointNotFoundError} when absent. */
22
+ get(id: string): Promise<WebhookEndpointView>;
23
+ /**
24
+ * Creates an endpoint and mints its secret.
25
+ *
26
+ * The URL is checked here as well as in the worker so a typo is refused in
27
+ * the dialog rather than discovered in the delivery log an hour later.
28
+ */
29
+ create(model: EndpointWriteModel, createdBy: string | null): Promise<WebhookEndpointWithSecret>;
30
+ /** Applies a full write model to an existing endpoint. */
31
+ update(id: string, model: EndpointWriteModel): Promise<WebhookEndpointView>;
32
+ /** Deletes the endpoint and, by cascade, its delivery log. */
33
+ delete(id: string): Promise<void>;
34
+ /**
35
+ * Rotates the signing secret.
36
+ *
37
+ * The new secret takes effect immediately, including for deliveries already
38
+ * queued — they are signed when they are sent, not when they were queued.
39
+ * A receiver therefore needs the new secret in place before rotating, which
40
+ * is what the dialog says.
41
+ */
42
+ rotateSecret(id: string): Promise<WebhookEndpointWithSecret>;
43
+ /**
44
+ * Sends a synthetic `ping` and reports the result **synchronously**.
45
+ *
46
+ * Deliberately not queued: the person pressing "Send test" is waiting for
47
+ * an answer, and a row in the log they would have to go and find is not
48
+ * one. It is also why a test never counts against the endpoint's
49
+ * consecutive-failure budget — it is a probe, not traffic.
50
+ */
51
+ sendTest(id: string): Promise<WebhookTestResult>;
52
+ /** One page of an endpoint's delivery log. */
53
+ listDeliveries(endpointId: string, query: DeliveryListQuery): Promise<WebhookDeliveryPage>;
54
+ /** One delivery with its bodies. */
55
+ getDelivery(endpointId: string, deliveryId: string): Promise<WebhookDeliveryDetailView>;
56
+ /**
57
+ * Queues the same event again as a fresh delivery.
58
+ *
59
+ * The body is copied rather than rebuilt, so the receiver gets exactly what
60
+ * it was sent before — with one substitution: `id` becomes the new delivery
61
+ * id, because that is also what the `X-Ortha-Delivery` header will say, and
62
+ * a body naming a different delivery than its own header is a bug waiting
63
+ * to be found by whoever writes the receiver.
64
+ *
65
+ * `eventId` is kept, so a receiver that deduplicates on it correctly
66
+ * ignores the repeat — which is exactly what a redelivery is *for* when the
67
+ * receiver, not the event, was the thing that was broken. Keeping it is
68
+ * only possible because `redeliveryOf` exempts the row from the partial
69
+ * unique index that makes fan-out idempotent.
70
+ */
71
+ redeliver(endpointId: string, deliveryId: string): Promise<WebhookDeliveryView>;
72
+ /**
73
+ * Refuses a write that the delivery path would only reject later.
74
+ *
75
+ * Both checks are also enforced downstream — the URL on every send, the
76
+ * headers when the request is built — because a row can predate a policy
77
+ * change. Doing them here is what turns a silent failure in the log into a
78
+ * message in the form.
79
+ */
80
+ private assertWritable;
81
+ }
82
+ //# sourceMappingURL=webhook-endpoints.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-endpoints.service.d.ts","sourceRoot":"","sources":["../../../src/lib/application/webhook-endpoints.service.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EACR,yBAAyB,EACzB,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,iBAAiB,EACpB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACH,yBAAyB,EACzB,KAAK,iBAAiB,EACzB,MAAM,+CAA+C,CAAC;AACvD,OAAO,EACH,yBAAyB,EACzB,KAAK,kBAAkB,EAC1B,MAAM,+CAA+C,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,uCAAuC,CAAC;AAE1E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAEvE,yEAAyE;AACzE,MAAM,WAAW,yBAAyB;IACtC,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,+EAA+E;IAC/E,MAAM,EAAE,MAAM,CAAC;CAClB;AAED,+CAA+C;AAC/C,qBACa,uBAAuB;IAE5B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,IAAI;IAErB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAJN,SAAS,EAAE,yBAAyB,EACpC,UAAU,EAAE,yBAAyB,EACrC,IAAI,EAAE,iBAAiB,EAEvB,MAAM,EAAE,sBAAsB;IAGnD,oCAAoC;IACpC,IAAI,IAAI,OAAO,CAAC,mBAAmB,EAAE,CAAC;IAItC,6EAA6E;IACvE,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAMnD;;;;;OAKG;IACG,MAAM,CACR,KAAK,EAAE,kBAAkB,EACzB,SAAS,EAAE,MAAM,GAAG,IAAI,GACzB,OAAO,CAAC,yBAAyB,CAAC;IAQrC,0DAA0D;IACpD,MAAM,CACR,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,kBAAkB,GAC1B,OAAO,CAAC,mBAAmB,CAAC;IAU/B,8DAA8D;IACxD,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOvC;;;;;;;OAOG;IACG,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,yBAAyB,CAAC;IAUlE;;;;;;;OAOG;IACG,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAyCtD,8CAA8C;IACxC,cAAc,CAChB,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,iBAAiB,GACzB,OAAO,CAAC,mBAAmB,CAAC;IAO/B,oCAAoC;IAC9B,WAAW,CACb,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACnB,OAAO,CAAC,yBAAyB,CAAC;IASrC;;;;;;;;;;;;;;OAcG;IACG,SAAS,CACX,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACnB,OAAO,CAAC,mBAAmB,CAAC;IAwB/B;;;;;;;OAOG;IACH,OAAO,CAAC,cAAc;CAczB"}
@@ -0,0 +1,213 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WebhookEndpointsService = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const common_1 = require("@nestjs/common");
6
+ const node_crypto_1 = require("node:crypto");
7
+ const webhooks_domain_1 = require("@orthacms/webhooks-domain");
8
+ const errors_1 = require("../domain/errors");
9
+ const webhook_secret_1 = require("../domain/webhook-secret");
10
+ const webhook_delivery_repository_1 = require("../infrastructure/webhook-delivery.repository");
11
+ const webhook_endpoint_repository_1 = require("../infrastructure/webhook-endpoint.repository");
12
+ const webhook_http_client_1 = require("../infrastructure/webhook-http.client");
13
+ const webhooks_tokens_1 = require("../webhooks.tokens");
14
+ /** Orchestration over the two repositories. */
15
+ let WebhookEndpointsService = class WebhookEndpointsService {
16
+ endpoints;
17
+ deliveries;
18
+ http;
19
+ config;
20
+ constructor(endpoints, deliveries, http, config) {
21
+ this.endpoints = endpoints;
22
+ this.deliveries = deliveries;
23
+ this.http = http;
24
+ this.config = config;
25
+ }
26
+ /** Every endpoint, newest first. */
27
+ list() {
28
+ return this.endpoints.list();
29
+ }
30
+ /** One endpoint. Throws {@link WebhookEndpointNotFoundError} when absent. */
31
+ async get(id) {
32
+ const endpoint = await this.endpoints.findById(id);
33
+ if (!endpoint)
34
+ throw new errors_1.WebhookEndpointNotFoundError(id);
35
+ return endpoint;
36
+ }
37
+ /**
38
+ * Creates an endpoint and mints its secret.
39
+ *
40
+ * The URL is checked here as well as in the worker so a typo is refused in
41
+ * the dialog rather than discovered in the delivery log an hour later.
42
+ */
43
+ async create(model, createdBy) {
44
+ this.assertWritable(model);
45
+ const { secret } = (0, webhook_secret_1.generateSecret)();
46
+ const id = await this.endpoints.create(model, secret, createdBy);
47
+ return { endpoint: await this.get(id), secret };
48
+ }
49
+ /** Applies a full write model to an existing endpoint. */
50
+ async update(id, model) {
51
+ if (!(await this.endpoints.exists(id))) {
52
+ throw new errors_1.WebhookEndpointNotFoundError(id);
53
+ }
54
+ this.assertWritable(model);
55
+ await this.endpoints.update(id, model);
56
+ return this.get(id);
57
+ }
58
+ /** Deletes the endpoint and, by cascade, its delivery log. */
59
+ async delete(id) {
60
+ if (!(await this.endpoints.exists(id))) {
61
+ throw new errors_1.WebhookEndpointNotFoundError(id);
62
+ }
63
+ await this.endpoints.delete(id);
64
+ }
65
+ /**
66
+ * Rotates the signing secret.
67
+ *
68
+ * The new secret takes effect immediately, including for deliveries already
69
+ * queued — they are signed when they are sent, not when they were queued.
70
+ * A receiver therefore needs the new secret in place before rotating, which
71
+ * is what the dialog says.
72
+ */
73
+ async rotateSecret(id) {
74
+ if (!(await this.endpoints.exists(id))) {
75
+ throw new errors_1.WebhookEndpointNotFoundError(id);
76
+ }
77
+ const { secret } = (0, webhook_secret_1.generateSecret)();
78
+ await this.endpoints.replaceSecret(id, secret);
79
+ return { endpoint: await this.get(id), secret };
80
+ }
81
+ /**
82
+ * Sends a synthetic `ping` and reports the result **synchronously**.
83
+ *
84
+ * Deliberately not queued: the person pressing "Send test" is waiting for
85
+ * an answer, and a row in the log they would have to go and find is not
86
+ * one. It is also why a test never counts against the endpoint's
87
+ * consecutive-failure budget — it is a probe, not traffic.
88
+ */
89
+ async sendTest(id) {
90
+ const endpoint = await this.endpoints.findForDelivery(id);
91
+ if (!endpoint)
92
+ throw new errors_1.WebhookEndpointNotFoundError(id);
93
+ const deliveryId = (0, node_crypto_1.randomUUID)();
94
+ const eventId = (0, node_crypto_1.randomUUID)();
95
+ const envelope = (0, webhooks_domain_1.buildEnvelope)(deliveryId, {
96
+ eventId,
97
+ kind: 'ping',
98
+ occurredAt: new Date(),
99
+ workspaceId: null,
100
+ aggregateType: 'webhook_endpoint',
101
+ aggregateId: id,
102
+ data: { endpointName: endpoint.name },
103
+ actor: null
104
+ });
105
+ const response = await this.http.send({
106
+ url: endpoint.url,
107
+ secret: endpoint.secret,
108
+ headers: endpoint.headers,
109
+ body: envelope,
110
+ eventKind: 'ping',
111
+ deliveryId,
112
+ eventId,
113
+ workspaceId: null,
114
+ attempt: 1
115
+ });
116
+ return {
117
+ ok: response.statusCode !== null &&
118
+ response.statusCode >= 200 &&
119
+ response.statusCode < 300,
120
+ statusCode: response.statusCode,
121
+ error: response.error,
122
+ durationMs: response.durationMs,
123
+ responseSnippet: response.responseSnippet
124
+ };
125
+ }
126
+ /** One page of an endpoint's delivery log. */
127
+ async listDeliveries(endpointId, query) {
128
+ if (!(await this.endpoints.exists(endpointId))) {
129
+ throw new errors_1.WebhookEndpointNotFoundError(endpointId);
130
+ }
131
+ return this.deliveries.list(endpointId, query);
132
+ }
133
+ /** One delivery with its bodies. */
134
+ async getDelivery(endpointId, deliveryId) {
135
+ const delivery = await this.deliveries.findDetail(endpointId, deliveryId);
136
+ if (!delivery)
137
+ throw new errors_1.WebhookDeliveryNotFoundError(deliveryId);
138
+ return delivery;
139
+ }
140
+ /**
141
+ * Queues the same event again as a fresh delivery.
142
+ *
143
+ * The body is copied rather than rebuilt, so the receiver gets exactly what
144
+ * it was sent before — with one substitution: `id` becomes the new delivery
145
+ * id, because that is also what the `X-Ortha-Delivery` header will say, and
146
+ * a body naming a different delivery than its own header is a bug waiting
147
+ * to be found by whoever writes the receiver.
148
+ *
149
+ * `eventId` is kept, so a receiver that deduplicates on it correctly
150
+ * ignores the repeat — which is exactly what a redelivery is *for* when the
151
+ * receiver, not the event, was the thing that was broken. Keeping it is
152
+ * only possible because `redeliveryOf` exempts the row from the partial
153
+ * unique index that makes fan-out idempotent.
154
+ */
155
+ async redeliver(endpointId, deliveryId) {
156
+ const original = await this.getDelivery(endpointId, deliveryId);
157
+ const id = (0, node_crypto_1.randomUUID)();
158
+ await this.deliveries.enqueueOne({
159
+ id,
160
+ endpointId,
161
+ eventId: original.eventId,
162
+ eventKind: original.eventKind,
163
+ workspaceId: original.workspaceId,
164
+ contentType: original.contentType,
165
+ payload: withDeliveryId(original.payload, id),
166
+ // Points at the delivery this repeats — both as provenance in the
167
+ // log and as the exemption from the fan-out unique index.
168
+ redeliveryOf: original.id
169
+ });
170
+ const queued = await this.deliveries.findDetail(endpointId, id);
171
+ // The row was just written in the same request; its absence would mean
172
+ // the endpoint was deleted underneath us.
173
+ if (!queued)
174
+ throw new errors_1.WebhookEndpointNotFoundError(endpointId);
175
+ return queued;
176
+ }
177
+ /**
178
+ * Refuses a write that the delivery path would only reject later.
179
+ *
180
+ * Both checks are also enforced downstream — the URL on every send, the
181
+ * headers when the request is built — because a row can predate a policy
182
+ * change. Doing them here is what turns a silent failure in the log into a
183
+ * message in the form.
184
+ */
185
+ assertWritable(model) {
186
+ (0, webhooks_domain_1.assertUrlShape)(model.url, {
187
+ allowInsecureUrls: this.config.allowInsecureUrls,
188
+ allowPrivateNetworks: this.config.allowPrivateNetworks
189
+ });
190
+ for (const name of Object.keys(model.headers)) {
191
+ if (!(0, webhooks_domain_1.isAllowedCustomHeader)(name)) {
192
+ throw new webhooks_domain_1.WebhookUrlRejectedError(`The header "${name}" cannot be set on a webhook — it is reserved for the delivery's own metadata.`);
193
+ }
194
+ }
195
+ }
196
+ };
197
+ exports.WebhookEndpointsService = WebhookEndpointsService;
198
+ exports.WebhookEndpointsService = WebhookEndpointsService = tslib_1.__decorate([
199
+ (0, common_1.Injectable)(),
200
+ tslib_1.__param(3, (0, webhooks_tokens_1.InjectWebhooksConfig)()),
201
+ tslib_1.__metadata("design:paramtypes", [webhook_endpoint_repository_1.WebhookEndpointRepository,
202
+ webhook_delivery_repository_1.WebhookDeliveryRepository,
203
+ webhook_http_client_1.WebhookHttpClient, Object])
204
+ ], WebhookEndpointsService);
205
+ /**
206
+ * The stored envelope with its `id` swapped for this delivery's, leaving
207
+ * everything else — `eventId` above all — exactly as it was sent.
208
+ */
209
+ function withDeliveryId(payload, deliveryId) {
210
+ if (typeof payload !== 'object' || payload === null)
211
+ return payload;
212
+ return { ...payload, id: deliveryId };
213
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The webhooks plugin's pass over the host's OpenAPI document.
3
+ *
4
+ * The routes here are ordinary decorated controllers, so `@nestjs/swagger`
5
+ * already knows their paths, parameters and request bodies. What it cannot
6
+ * know is what comes **back**: every response view is a TypeScript
7
+ * `interface`, erased before the scanner runs, which is why each operation
8
+ * arrives carrying a bare `200`/`201` with no content. This pass writes the
9
+ * response schemas onto the operations the plugin owns and nothing else.
10
+ *
11
+ * Pure: it takes the document and mutates only its own paths.
12
+ */
13
+ import type { OpenApiDocument } from '@orthacms/bootstrap-server';
14
+ /** Adds this plugin's schemas and describes its own operations' responses. */
15
+ export declare function describeWebhooksApi(document: OpenApiDocument): void;
16
+ //# sourceMappingURL=describe-webhooks-api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"describe-webhooks-api.d.ts","sourceRoot":"","sources":["../../../src/lib/docs/describe-webhooks-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AA4JlE,8EAA8E;AAC9E,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI,CA4DnE"}