@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,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SECRET_PREFIX = void 0;
4
+ exports.generateSecret = generateSecret;
5
+ exports.secretHint = secretHint;
6
+ const node_crypto_1 = require("node:crypto");
7
+ /**
8
+ * The prefix every signing secret carries, so one found in a log or a config
9
+ * file is recognisable for what it is — and so a scanner can be taught to spot
10
+ * a leaked one.
11
+ */
12
+ exports.SECRET_PREFIX = 'whsec_';
13
+ /** How many characters of the secret the UI may show after mint. */
14
+ const HINT_LENGTH = 4;
15
+ /**
16
+ * Mints a signing secret.
17
+ *
18
+ * 32 bytes of `randomBytes` — the same order of entropy as the API tokens, and
19
+ * far more than an HMAC key needs. Base64url so it survives a shell, a YAML
20
+ * file and an environment variable without quoting.
21
+ */
22
+ function generateSecret() {
23
+ const secret = `${exports.SECRET_PREFIX}${(0, node_crypto_1.randomBytes)(32).toString('base64url')}`;
24
+ return { secret, hint: secretHint(secret) };
25
+ }
26
+ /**
27
+ * The displayable tail of a secret.
28
+ *
29
+ * Four characters is enough to tell "the one I rotated to" from "the one I
30
+ * replaced" and far too little to shorten a brute force against a 256-bit key.
31
+ */
32
+ function secretHint(secret) {
33
+ return secret.slice(-HINT_LENGTH);
34
+ }
@@ -0,0 +1,85 @@
1
+ import type { DeliveryStatus } from '@orthacms/webhooks-domain';
2
+ /**
3
+ * The read shapes the application layer hands to the HTTP layer.
4
+ *
5
+ * They are what the API returns, so the one rule that matters here is what is
6
+ * **missing**: no `secret`. It is written on mint and on rotation, returned in
7
+ * that one response, and never present in a shape that a `GET` can reach.
8
+ */
9
+ /** An endpoint as the list and detail routes return it. */
10
+ export interface WebhookEndpointView {
11
+ id: string;
12
+ name: string;
13
+ url: string;
14
+ /** The trailing characters of the secret. Never the secret itself. */
15
+ secretHint: string;
16
+ enabled: boolean;
17
+ /** Empty means every kind. */
18
+ eventKinds: string[];
19
+ /** Empty means every type. */
20
+ contentTypes: string[];
21
+ allWorkspaces: boolean;
22
+ /** Empty when {@link allWorkspaces} is true. */
23
+ workspaceIds: string[];
24
+ headers: Record<string, string>;
25
+ /** Set when the endpoint switched itself off; null otherwise. */
26
+ disabledReason: string | null;
27
+ consecutiveFailures: number;
28
+ createdAt: string;
29
+ updatedAt: string;
30
+ /** The most recent delivery's outcome, for the list's status column. */
31
+ lastDelivery: WebhookLastDeliveryView | null;
32
+ }
33
+ /** The one-line delivery summary shown against an endpoint in the list. */
34
+ export interface WebhookLastDeliveryView {
35
+ id: string;
36
+ status: DeliveryStatus;
37
+ eventKind: string;
38
+ statusCode: number | null;
39
+ createdAt: string;
40
+ }
41
+ /** A delivery as the log's list route returns it — no bodies. */
42
+ export interface WebhookDeliveryView {
43
+ id: string;
44
+ endpointId: string;
45
+ eventId: string;
46
+ eventKind: string;
47
+ workspaceId: string | null;
48
+ contentType: string | null;
49
+ status: DeliveryStatus;
50
+ attempts: number;
51
+ nextAttemptAt: string | null;
52
+ statusCode: number | null;
53
+ error: string | null;
54
+ durationMs: number | null;
55
+ createdAt: string;
56
+ completedAt: string | null;
57
+ }
58
+ /** A delivery with its request and response bodies, for the detail panel. */
59
+ export interface WebhookDeliveryDetailView extends WebhookDeliveryView {
60
+ /** The frozen envelope — exactly what was, or will be, posted. */
61
+ payload: unknown;
62
+ /** The first couple of kilobytes of the receiver's response. */
63
+ responseSnippet: string | null;
64
+ }
65
+ /** A page of deliveries. */
66
+ export interface WebhookDeliveryPage {
67
+ items: WebhookDeliveryView[];
68
+ total: number;
69
+ page: number;
70
+ pageSize: number;
71
+ pageCount: number;
72
+ }
73
+ /** The result of "Send test" — reported synchronously, never queued. */
74
+ export interface WebhookTestResult {
75
+ /** Whether the receiver answered 2xx. */
76
+ ok: boolean;
77
+ /** The HTTP status, or null when nothing answered. */
78
+ statusCode: number | null;
79
+ /** Why it failed, when it did. */
80
+ error: string | null;
81
+ durationMs: number;
82
+ /** The first couple of kilobytes of the response. */
83
+ responseSnippet: string | null;
84
+ }
85
+ //# sourceMappingURL=webhook-views.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-views.d.ts","sourceRoot":"","sources":["../../../src/lib/domain/webhook-views.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAEhE;;;;;;GAMG;AAEH,2DAA2D;AAC3D,MAAM,WAAW,mBAAmB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,sEAAsE;IACtE,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,8BAA8B;IAC9B,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,8BAA8B;IAC9B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,aAAa,EAAE,OAAO,CAAC;IACvB,gDAAgD;IAChD,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,iEAAiE;IACjE,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,YAAY,EAAE,uBAAuB,GAAG,IAAI,CAAC;CAChD;AAED,2EAA2E;AAC3E,MAAM,WAAW,uBAAuB;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,cAAc,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;CACrB;AAED,iEAAiE;AACjE,MAAM,WAAW,mBAAmB;IAChC,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,MAAM,EAAE,cAAc,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,6EAA6E;AAC7E,MAAM,WAAW,yBAA0B,SAAQ,mBAAmB;IAClE,kEAAkE;IAClE,OAAO,EAAE,OAAO,CAAC;IACjB,gEAAgE;IAChE,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAED,4BAA4B;AAC5B,MAAM,WAAW,mBAAmB;IAChC,KAAK,EAAE,mBAAmB,EAAE,CAAC;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACrB;AAED,wEAAwE;AACxE,MAAM,WAAW,iBAAiB;IAC9B,yCAAyC;IACzC,EAAE,EAAE,OAAO,CAAC;IACZ,sDAAsD;IACtD,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,kCAAkC;IAClC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,qDAAqD;IACrD,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC"}
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,28 @@
1
+ import { WebhookEndpointsService } from '../../application/webhook-endpoints.service';
2
+ import { ListDeliveriesQueryDto } from '../../application/dto/list-deliveries-query.dto';
3
+ import type { WebhookDeliveryDetailView, WebhookDeliveryPage, WebhookDeliveryView } from '../../domain/webhook-views';
4
+ /**
5
+ * The delivery log: `/api/webhooks/:id/deliveries`.
6
+ *
7
+ * Every route is scoped to the endpoint in the path, so a delivery id belonging
8
+ * to another endpoint reads as "not found" rather than resolving — the log is
9
+ * not a way to enumerate ids across endpoints.
10
+ */
11
+ export declare class WebhookDeliveriesController {
12
+ private readonly endpoints;
13
+ constructor(endpoints: WebhookEndpointsService);
14
+ /** One page of the endpoint's log, newest first. */
15
+ list(endpointId: string, query: ListDeliveriesQueryDto): Promise<WebhookDeliveryPage>;
16
+ /** One delivery, with the body that was sent and the response. */
17
+ get(endpointId: string, deliveryId: string): Promise<WebhookDeliveryDetailView>;
18
+ /**
19
+ * Queues the same event again.
20
+ *
21
+ * Needs `webhooks:manage`, not `webhooks:read`: it causes an outgoing
22
+ * request to someone else's system, which is a write however it is spelled.
23
+ */
24
+ redeliver(endpointId: string, deliveryId: string): Promise<WebhookDeliveryView>;
25
+ /** Maps this plugin's not-found errors onto 404. */
26
+ private run;
27
+ }
28
+ //# sourceMappingURL=webhook-deliveries.controller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-deliveries.controller.d.ts","sourceRoot":"","sources":["../../../../src/lib/http/controllers/webhook-deliveries.controller.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAE,uBAAuB,EAAE,MAAM,6CAA6C,CAAC;AACtF,OAAO,EAAE,sBAAsB,EAAE,MAAM,iDAAiD,CAAC;AAKzF,OAAO,KAAK,EACR,yBAAyB,EACzB,mBAAmB,EACnB,mBAAmB,EACtB,MAAM,4BAA4B,CAAC;AAKpC;;;;;;GAMG;AACH,qBAGa,2BAA2B;IACxB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAAT,SAAS,EAAE,uBAAuB;IAE/D,oDAAoD;IAS9C,IAAI,CACsB,UAAU,EAAE,MAAM,EACrC,KAAK,EAAE,sBAAsB,GACvC,OAAO,CAAC,mBAAmB,CAAC;IAW/B,kEAAkE;IAS5D,GAAG,CACuB,UAAU,EAAE,MAAM,EACV,UAAU,EAAE,MAAM,GACvD,OAAO,CAAC,yBAAyB,CAAC;IAMrC;;;;;OAKG;IAUG,SAAS,CACiB,UAAU,EAAE,MAAM,EACV,UAAU,EAAE,MAAM,GACvD,OAAO,CAAC,mBAAmB,CAAC;IAI/B,oDAAoD;YACtC,GAAG;CAapB"}
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WebhookDeliveriesController = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const common_1 = require("@nestjs/common");
6
+ const swagger_1 = require("@nestjs/swagger");
7
+ const identity_server_1 = require("@orthacms/identity-server");
8
+ const webhook_endpoints_service_1 = require("../../application/webhook-endpoints.service");
9
+ const list_deliveries_query_dto_1 = require("../../application/dto/list-deliveries-query.dto");
10
+ const errors_1 = require("../../domain/errors");
11
+ /** Default rows per page when the caller does not say. */
12
+ const DEFAULT_PAGE_SIZE = 25;
13
+ /**
14
+ * The delivery log: `/api/webhooks/:id/deliveries`.
15
+ *
16
+ * Every route is scoped to the endpoint in the path, so a delivery id belonging
17
+ * to another endpoint reads as "not found" rather than resolving — the log is
18
+ * not a way to enumerate ids across endpoints.
19
+ */
20
+ let WebhookDeliveriesController = class WebhookDeliveriesController {
21
+ endpoints;
22
+ constructor(endpoints) {
23
+ this.endpoints = endpoints;
24
+ }
25
+ /** One page of the endpoint's log, newest first. */
26
+ async list(endpointId, query) {
27
+ return this.run(() => this.endpoints.listDeliveries(endpointId, {
28
+ status: query.status,
29
+ eventKind: query.eventKind,
30
+ page: query.page ?? 1,
31
+ pageSize: query.pageSize ?? DEFAULT_PAGE_SIZE
32
+ }));
33
+ }
34
+ /** One delivery, with the body that was sent and the response. */
35
+ async get(endpointId, deliveryId) {
36
+ return this.run(() => this.endpoints.getDelivery(endpointId, deliveryId));
37
+ }
38
+ /**
39
+ * Queues the same event again.
40
+ *
41
+ * Needs `webhooks:manage`, not `webhooks:read`: it causes an outgoing
42
+ * request to someone else's system, which is a write however it is spelled.
43
+ */
44
+ async redeliver(endpointId, deliveryId) {
45
+ return this.run(() => this.endpoints.redeliver(endpointId, deliveryId));
46
+ }
47
+ /** Maps this plugin's not-found errors onto 404. */
48
+ async run(operation) {
49
+ try {
50
+ return await operation();
51
+ }
52
+ catch (error) {
53
+ if (error instanceof errors_1.WebhookEndpointNotFoundError ||
54
+ error instanceof errors_1.WebhookDeliveryNotFoundError) {
55
+ throw new common_1.NotFoundException(error.message);
56
+ }
57
+ throw error;
58
+ }
59
+ }
60
+ };
61
+ exports.WebhookDeliveriesController = WebhookDeliveriesController;
62
+ tslib_1.__decorate([
63
+ (0, swagger_1.ApiOperation)({
64
+ summary: 'List webhook deliveries',
65
+ description: 'One page of the endpoint’s delivery log, newest first, ' +
66
+ 'optionally filtered by state or event kind.'
67
+ }),
68
+ (0, identity_server_1.RequirePermissions)(identity_server_1.PERMISSIONS.WEBHOOKS_READ),
69
+ (0, common_1.Get)(),
70
+ tslib_1.__param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)),
71
+ tslib_1.__param(1, (0, common_1.Query)()),
72
+ tslib_1.__metadata("design:type", Function),
73
+ tslib_1.__metadata("design:paramtypes", [String, list_deliveries_query_dto_1.ListDeliveriesQueryDto]),
74
+ tslib_1.__metadata("design:returntype", Promise)
75
+ ], WebhookDeliveriesController.prototype, "list", null);
76
+ tslib_1.__decorate([
77
+ (0, swagger_1.ApiOperation)({
78
+ summary: 'Get a webhook delivery',
79
+ description: 'The delivery with its frozen request body and the first couple ' +
80
+ 'of kilobytes of the response.'
81
+ }),
82
+ (0, identity_server_1.RequirePermissions)(identity_server_1.PERMISSIONS.WEBHOOKS_READ),
83
+ (0, common_1.Get)(':deliveryId'),
84
+ tslib_1.__param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)),
85
+ tslib_1.__param(1, (0, common_1.Param)('deliveryId', common_1.ParseUUIDPipe)),
86
+ tslib_1.__metadata("design:type", Function),
87
+ tslib_1.__metadata("design:paramtypes", [String, String]),
88
+ tslib_1.__metadata("design:returntype", Promise)
89
+ ], WebhookDeliveriesController.prototype, "get", null);
90
+ tslib_1.__decorate([
91
+ (0, swagger_1.ApiOperation)({
92
+ summary: 'Send a delivery again',
93
+ description: 'Queues a fresh delivery with the same body and the same event ' +
94
+ 'id, so a receiver that deduplicates still recognises the repeat.'
95
+ }),
96
+ (0, identity_server_1.RequirePermissions)(identity_server_1.PERMISSIONS.WEBHOOKS_MANAGE),
97
+ (0, common_1.UseGuards)(identity_server_1.OriginGuard),
98
+ (0, common_1.Post)(':deliveryId/redeliver'),
99
+ tslib_1.__param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)),
100
+ tslib_1.__param(1, (0, common_1.Param)('deliveryId', common_1.ParseUUIDPipe)),
101
+ tslib_1.__metadata("design:type", Function),
102
+ tslib_1.__metadata("design:paramtypes", [String, String]),
103
+ tslib_1.__metadata("design:returntype", Promise)
104
+ ], WebhookDeliveriesController.prototype, "redeliver", null);
105
+ exports.WebhookDeliveriesController = WebhookDeliveriesController = tslib_1.__decorate([
106
+ (0, swagger_1.ApiTags)('webhooks'),
107
+ (0, common_1.UseGuards)(identity_server_1.PermissionsGuard),
108
+ (0, common_1.Controller)('webhooks/:id/deliveries'),
109
+ tslib_1.__metadata("design:paramtypes", [webhook_endpoints_service_1.WebhookEndpointsService])
110
+ ], WebhookDeliveriesController);
@@ -0,0 +1,61 @@
1
+ import { type PublicUser } from '@orthacms/identity-server';
2
+ import { WebhookEndpointsService } from '../../application/webhook-endpoints.service';
3
+ import { SaveWebhookEndpointDto } from '../../application/dto/save-webhook-endpoint.dto';
4
+ import type { WebhookEndpointView, WebhookTestResult } from '../../domain/webhook-views';
5
+ /** An endpoint, plus the secret — the create and rotate responses only. */
6
+ interface EndpointWithSecretResponse {
7
+ endpoint: WebhookEndpointView;
8
+ secret: string;
9
+ }
10
+ /**
11
+ * The endpoint surface: `/api/webhooks`.
12
+ *
13
+ * Global rather than workspace-scoped, so there is no `WorkspaceGuard` here:
14
+ * an endpoint spans whichever workspaces it names, and which systems this
15
+ * installation notifies is a deployment decision. Both permissions are
16
+ * admin-only for the same reason — the rows hold a signing secret and reach
17
+ * across every workspace.
18
+ *
19
+ * `OriginGuard` is applied per write route, matching content's and alarms'
20
+ * controllers: these are cookie-authenticated and therefore CSRF-able, while
21
+ * the reads change nothing.
22
+ */
23
+ export declare class WebhookEndpointsController {
24
+ private readonly endpoints;
25
+ constructor(endpoints: WebhookEndpointsService);
26
+ /** Every endpoint, newest first, each with its last delivery. */
27
+ list(): Promise<WebhookEndpointView[]>;
28
+ /** One endpoint. */
29
+ get(id: string): Promise<WebhookEndpointView>;
30
+ /**
31
+ * Creates an endpoint and mints its signing secret.
32
+ *
33
+ * **This is the only response that ever contains the secret.** Every other
34
+ * route returns `secretHint` instead, so a secret cannot be recovered by
35
+ * anyone who missed it — including whoever created it.
36
+ */
37
+ create(body: SaveWebhookEndpointDto, user: PublicUser): Promise<EndpointWithSecretResponse>;
38
+ /** Replaces an endpoint's configuration. */
39
+ update(id: string, body: SaveWebhookEndpointDto): Promise<WebhookEndpointView>;
40
+ /** Deletes the endpoint and its delivery log. */
41
+ delete(id: string): Promise<void>;
42
+ /** Rotates the signing secret and returns the new one, once. */
43
+ rotateSecret(id: string): Promise<EndpointWithSecretResponse>;
44
+ /**
45
+ * Sends a synthetic `ping` and reports the result.
46
+ *
47
+ * Synchronous and unqueued: the person who pressed the button is waiting,
48
+ * and it does not count against the endpoint's failure budget.
49
+ */
50
+ test(id: string): Promise<WebhookTestResult>;
51
+ /**
52
+ * Maps this plugin's domain errors onto HTTP.
53
+ *
54
+ * A rejected URL is a 422 rather than a 400 because the request was
55
+ * well-formed and the value was refused by policy — and the message is
56
+ * written to be shown in the form that produced it.
57
+ */
58
+ private run;
59
+ }
60
+ export {};
61
+ //# sourceMappingURL=webhook-endpoints.controller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-endpoints.controller.d.ts","sourceRoot":"","sources":["../../../../src/lib/http/controllers/webhook-endpoints.controller.ts"],"names":[],"mappings":"AAeA,OAAO,EAMH,KAAK,UAAU,EAClB,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EAAE,uBAAuB,EAAE,MAAM,6CAA6C,CAAC;AACtF,OAAO,EAAE,sBAAsB,EAAE,MAAM,iDAAiD,CAAC;AAEzF,OAAO,KAAK,EACR,mBAAmB,EACnB,iBAAiB,EACpB,MAAM,4BAA4B,CAAC;AAGpC,2EAA2E;AAC3E,UAAU,0BAA0B;IAChC,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,MAAM,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;GAYG;AACH,qBAGa,0BAA0B;IACvB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAAT,SAAS,EAAE,uBAAuB;IAE/D,iEAAiE;IASjE,IAAI,IAAI,OAAO,CAAC,mBAAmB,EAAE,CAAC;IAItC,oBAAoB;IAId,GAAG,CACuB,EAAE,EAAE,MAAM,GACvC,OAAO,CAAC,mBAAmB,CAAC;IAI/B;;;;;;OAMG;IAUG,MAAM,CACA,IAAI,EAAE,sBAAsB,EACrB,IAAI,EAAE,UAAU,GAChC,OAAO,CAAC,0BAA0B,CAAC;IAMtC,4CAA4C;IAUtC,MAAM,CACoB,EAAE,EAAE,MAAM,EAC9B,IAAI,EAAE,sBAAsB,GACrC,OAAO,CAAC,mBAAmB,CAAC;IAI/B,iDAAiD;IAS3C,MAAM,CAA6B,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAInE,gEAAgE;IAW1D,YAAY,CACc,EAAE,EAAE,MAAM,GACvC,OAAO,CAAC,0BAA0B,CAAC;IAItC;;;;;OAKG;IAWG,IAAI,CACsB,EAAE,EAAE,MAAM,GACvC,OAAO,CAAC,iBAAiB,CAAC;IAI7B;;;;;;OAMG;YACW,GAAG;CAapB"}
@@ -0,0 +1,213 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WebhookEndpointsController = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const common_1 = require("@nestjs/common");
6
+ const swagger_1 = require("@nestjs/swagger");
7
+ const identity_server_1 = require("@orthacms/identity-server");
8
+ const webhooks_domain_1 = require("@orthacms/webhooks-domain");
9
+ const webhook_endpoints_service_1 = require("../../application/webhook-endpoints.service");
10
+ const save_webhook_endpoint_dto_1 = require("../../application/dto/save-webhook-endpoint.dto");
11
+ const errors_1 = require("../../domain/errors");
12
+ /**
13
+ * The endpoint surface: `/api/webhooks`.
14
+ *
15
+ * Global rather than workspace-scoped, so there is no `WorkspaceGuard` here:
16
+ * an endpoint spans whichever workspaces it names, and which systems this
17
+ * installation notifies is a deployment decision. Both permissions are
18
+ * admin-only for the same reason — the rows hold a signing secret and reach
19
+ * across every workspace.
20
+ *
21
+ * `OriginGuard` is applied per write route, matching content's and alarms'
22
+ * controllers: these are cookie-authenticated and therefore CSRF-able, while
23
+ * the reads change nothing.
24
+ */
25
+ let WebhookEndpointsController = class WebhookEndpointsController {
26
+ endpoints;
27
+ constructor(endpoints) {
28
+ this.endpoints = endpoints;
29
+ }
30
+ /** Every endpoint, newest first, each with its last delivery. */
31
+ list() {
32
+ return this.endpoints.list();
33
+ }
34
+ /** One endpoint. */
35
+ async get(id) {
36
+ return this.run(() => this.endpoints.get(id));
37
+ }
38
+ /**
39
+ * Creates an endpoint and mints its signing secret.
40
+ *
41
+ * **This is the only response that ever contains the secret.** Every other
42
+ * route returns `secretHint` instead, so a secret cannot be recovered by
43
+ * anyone who missed it — including whoever created it.
44
+ */
45
+ async create(body, user) {
46
+ return this.run(() => this.endpoints.create(toWriteModel(body), user.id));
47
+ }
48
+ /** Replaces an endpoint's configuration. */
49
+ async update(id, body) {
50
+ return this.run(() => this.endpoints.update(id, toWriteModel(body)));
51
+ }
52
+ /** Deletes the endpoint and its delivery log. */
53
+ async delete(id) {
54
+ await this.run(() => this.endpoints.delete(id));
55
+ }
56
+ /** Rotates the signing secret and returns the new one, once. */
57
+ async rotateSecret(id) {
58
+ return this.run(() => this.endpoints.rotateSecret(id));
59
+ }
60
+ /**
61
+ * Sends a synthetic `ping` and reports the result.
62
+ *
63
+ * Synchronous and unqueued: the person who pressed the button is waiting,
64
+ * and it does not count against the endpoint's failure budget.
65
+ */
66
+ async test(id) {
67
+ return this.run(() => this.endpoints.sendTest(id));
68
+ }
69
+ /**
70
+ * Maps this plugin's domain errors onto HTTP.
71
+ *
72
+ * A rejected URL is a 422 rather than a 400 because the request was
73
+ * well-formed and the value was refused by policy — and the message is
74
+ * written to be shown in the form that produced it.
75
+ */
76
+ async run(operation) {
77
+ try {
78
+ return await operation();
79
+ }
80
+ catch (error) {
81
+ if (error instanceof errors_1.WebhookEndpointNotFoundError) {
82
+ throw new common_1.NotFoundException(error.message);
83
+ }
84
+ if (error instanceof webhooks_domain_1.WebhookUrlRejectedError) {
85
+ throw new common_1.UnprocessableEntityException(error.message);
86
+ }
87
+ throw error;
88
+ }
89
+ }
90
+ };
91
+ exports.WebhookEndpointsController = WebhookEndpointsController;
92
+ tslib_1.__decorate([
93
+ (0, swagger_1.ApiOperation)({
94
+ summary: 'List webhook endpoints',
95
+ description: 'Every configured endpoint with its filters and the outcome of ' +
96
+ 'its most recent delivery. Never includes the signing secret.'
97
+ }),
98
+ (0, identity_server_1.RequirePermissions)(identity_server_1.PERMISSIONS.WEBHOOKS_READ),
99
+ (0, common_1.Get)(),
100
+ tslib_1.__metadata("design:type", Function),
101
+ tslib_1.__metadata("design:paramtypes", []),
102
+ tslib_1.__metadata("design:returntype", Promise)
103
+ ], WebhookEndpointsController.prototype, "list", null);
104
+ tslib_1.__decorate([
105
+ (0, swagger_1.ApiOperation)({ summary: 'Get a webhook endpoint' }),
106
+ (0, identity_server_1.RequirePermissions)(identity_server_1.PERMISSIONS.WEBHOOKS_READ),
107
+ (0, common_1.Get)(':id'),
108
+ tslib_1.__param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)),
109
+ tslib_1.__metadata("design:type", Function),
110
+ tslib_1.__metadata("design:paramtypes", [String]),
111
+ tslib_1.__metadata("design:returntype", Promise)
112
+ ], WebhookEndpointsController.prototype, "get", null);
113
+ tslib_1.__decorate([
114
+ (0, swagger_1.ApiOperation)({
115
+ summary: 'Create a webhook endpoint',
116
+ description: 'Creates the endpoint and returns its signing secret. The secret ' +
117
+ 'is shown once and is never returned again.'
118
+ }),
119
+ (0, identity_server_1.RequirePermissions)(identity_server_1.PERMISSIONS.WEBHOOKS_MANAGE),
120
+ (0, common_1.UseGuards)(identity_server_1.OriginGuard),
121
+ (0, common_1.Post)(),
122
+ tslib_1.__param(0, (0, common_1.Body)()),
123
+ tslib_1.__param(1, (0, identity_server_1.CurrentUser)()),
124
+ tslib_1.__metadata("design:type", Function),
125
+ tslib_1.__metadata("design:paramtypes", [save_webhook_endpoint_dto_1.SaveWebhookEndpointDto, Object]),
126
+ tslib_1.__metadata("design:returntype", Promise)
127
+ ], WebhookEndpointsController.prototype, "create", null);
128
+ tslib_1.__decorate([
129
+ (0, swagger_1.ApiOperation)({
130
+ summary: 'Update a webhook endpoint',
131
+ description: 'Replaces the endpoint’s configuration. Re-enabling it by hand ' +
132
+ 'also clears an automatic disable and its failure count.'
133
+ }),
134
+ (0, identity_server_1.RequirePermissions)(identity_server_1.PERMISSIONS.WEBHOOKS_MANAGE),
135
+ (0, common_1.UseGuards)(identity_server_1.OriginGuard),
136
+ (0, common_1.Patch)(':id'),
137
+ tslib_1.__param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)),
138
+ tslib_1.__param(1, (0, common_1.Body)()),
139
+ tslib_1.__metadata("design:type", Function),
140
+ tslib_1.__metadata("design:paramtypes", [String, save_webhook_endpoint_dto_1.SaveWebhookEndpointDto]),
141
+ tslib_1.__metadata("design:returntype", Promise)
142
+ ], WebhookEndpointsController.prototype, "update", null);
143
+ tslib_1.__decorate([
144
+ (0, swagger_1.ApiOperation)({
145
+ summary: 'Delete a webhook endpoint',
146
+ description: 'Deletes the endpoint and, with it, its delivery log.'
147
+ }),
148
+ (0, identity_server_1.RequirePermissions)(identity_server_1.PERMISSIONS.WEBHOOKS_MANAGE),
149
+ (0, common_1.UseGuards)(identity_server_1.OriginGuard),
150
+ (0, common_1.Delete)(':id'),
151
+ (0, common_1.HttpCode)(204),
152
+ tslib_1.__param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)),
153
+ tslib_1.__metadata("design:type", Function),
154
+ tslib_1.__metadata("design:paramtypes", [String]),
155
+ tslib_1.__metadata("design:returntype", Promise)
156
+ ], WebhookEndpointsController.prototype, "delete", null);
157
+ tslib_1.__decorate([
158
+ (0, swagger_1.ApiOperation)({
159
+ summary: 'Rotate a webhook signing secret',
160
+ description: 'Mints a new signing secret and returns it once. It takes effect ' +
161
+ 'immediately, including for deliveries already queued, so the ' +
162
+ 'receiver needs it in place before rotating.'
163
+ }),
164
+ (0, identity_server_1.RequirePermissions)(identity_server_1.PERMISSIONS.WEBHOOKS_MANAGE),
165
+ (0, common_1.UseGuards)(identity_server_1.OriginGuard),
166
+ (0, common_1.Post)(':id/secret'),
167
+ tslib_1.__param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)),
168
+ tslib_1.__metadata("design:type", Function),
169
+ tslib_1.__metadata("design:paramtypes", [String]),
170
+ tslib_1.__metadata("design:returntype", Promise)
171
+ ], WebhookEndpointsController.prototype, "rotateSecret", null);
172
+ tslib_1.__decorate([
173
+ (0, swagger_1.ApiOperation)({
174
+ summary: 'Send a test delivery',
175
+ description: 'Posts a synthetic `ping` event and reports the response. It is ' +
176
+ 'not queued and does not count towards automatic disabling.'
177
+ }),
178
+ (0, identity_server_1.RequirePermissions)(identity_server_1.PERMISSIONS.WEBHOOKS_MANAGE),
179
+ (0, common_1.UseGuards)(identity_server_1.OriginGuard),
180
+ (0, common_1.Post)(':id/test'),
181
+ (0, common_1.HttpCode)(200),
182
+ tslib_1.__param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)),
183
+ tslib_1.__metadata("design:type", Function),
184
+ tslib_1.__metadata("design:paramtypes", [String]),
185
+ tslib_1.__metadata("design:returntype", Promise)
186
+ ], WebhookEndpointsController.prototype, "test", null);
187
+ exports.WebhookEndpointsController = WebhookEndpointsController = tslib_1.__decorate([
188
+ (0, swagger_1.ApiTags)('webhooks'),
189
+ (0, common_1.UseGuards)(identity_server_1.PermissionsGuard),
190
+ (0, common_1.Controller)('webhooks'),
191
+ tslib_1.__metadata("design:paramtypes", [webhook_endpoints_service_1.WebhookEndpointsService])
192
+ ], WebhookEndpointsController);
193
+ /**
194
+ * DTO → the repository's write model, resolving what an omitted field means.
195
+ *
196
+ * Every default here says "everything": an omitted filter is not an empty
197
+ * subscription, it is an unfiltered one. The one exception is `allWorkspaces`,
198
+ * which defaults to false — "all workspaces" reaches across tenants and should
199
+ * be something someone chose, not something they forgot to say.
200
+ */
201
+ function toWriteModel(dto) {
202
+ const allWorkspaces = dto.allWorkspaces ?? false;
203
+ return {
204
+ name: dto.name.trim(),
205
+ url: dto.url.trim(),
206
+ enabled: dto.enabled ?? true,
207
+ eventKinds: dto.eventKinds ?? [],
208
+ contentTypes: dto.contentTypes ?? [],
209
+ allWorkspaces,
210
+ workspaceIds: allWorkspaces ? [] : (dto.workspaceIds ?? []),
211
+ headers: dto.headers ?? {}
212
+ };
213
+ }
@@ -0,0 +1,13 @@
1
+ import { type WebhookEventDescriptor } from '@orthacms/webhooks-domain';
2
+ /**
3
+ * `GET /api/webhook-events` — the catalogue of subscribable event kinds.
4
+ *
5
+ * The admin's event picker is built from this rather than from a list compiled
6
+ * into the SPA, so a deployment running a newer server offers the kinds that
7
+ * server actually knows about. Same idea as `GET /api/content-schema`.
8
+ */
9
+ export declare class WebhookEventsController {
10
+ /** Every kind an endpoint may subscribe to. */
11
+ list(): readonly WebhookEventDescriptor[];
12
+ }
13
+ //# sourceMappingURL=webhook-events.controller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-events.controller.d.ts","sourceRoot":"","sources":["../../../../src/lib/http/controllers/webhook-events.controller.ts"],"names":[],"mappings":"AAOA,OAAO,EAEH,KAAK,sBAAsB,EAC9B,MAAM,2BAA2B,CAAC;AAEnC;;;;;;GAMG;AACH,qBAGa,uBAAuB;IAChC,+CAA+C;IAQ/C,IAAI,IAAI,SAAS,sBAAsB,EAAE;CAG5C"}
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WebhookEventsController = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const common_1 = require("@nestjs/common");
6
+ const swagger_1 = require("@nestjs/swagger");
7
+ const identity_server_1 = require("@orthacms/identity-server");
8
+ const webhooks_domain_1 = require("@orthacms/webhooks-domain");
9
+ /**
10
+ * `GET /api/webhook-events` — the catalogue of subscribable event kinds.
11
+ *
12
+ * The admin's event picker is built from this rather than from a list compiled
13
+ * into the SPA, so a deployment running a newer server offers the kinds that
14
+ * server actually knows about. Same idea as `GET /api/content-schema`.
15
+ */
16
+ let WebhookEventsController = class WebhookEventsController {
17
+ /** Every kind an endpoint may subscribe to. */
18
+ list() {
19
+ return webhooks_domain_1.WEBHOOK_EVENTS;
20
+ }
21
+ };
22
+ exports.WebhookEventsController = WebhookEventsController;
23
+ tslib_1.__decorate([
24
+ (0, swagger_1.ApiOperation)({
25
+ summary: 'List subscribable webhook events',
26
+ description: 'The catalogue the endpoint editor’s event picker is built from.'
27
+ }),
28
+ (0, identity_server_1.RequirePermissions)(identity_server_1.PERMISSIONS.WEBHOOKS_READ),
29
+ (0, common_1.Get)(),
30
+ tslib_1.__metadata("design:type", Function),
31
+ tslib_1.__metadata("design:paramtypes", []),
32
+ tslib_1.__metadata("design:returntype", Array)
33
+ ], WebhookEventsController.prototype, "list", null);
34
+ exports.WebhookEventsController = WebhookEventsController = tslib_1.__decorate([
35
+ (0, swagger_1.ApiTags)('webhooks'),
36
+ (0, common_1.UseGuards)(identity_server_1.PermissionsGuard),
37
+ (0, common_1.Controller)('webhook-events')
38
+ ], WebhookEventsController);
@@ -0,0 +1,13 @@
1
+ import type { DomainEvent } from '@orthacms/database';
2
+ import type { WebhookRoutableEvent, WebhookSourceEvent } from '@orthacms/webhooks-domain';
3
+ /** The routing facts, for the subscription filters. */
4
+ export declare function toRoutableEvent(event: DomainEvent): WebhookRoutableEvent;
5
+ /**
6
+ * The full source event, for the envelope.
7
+ *
8
+ * `actor` and `workspaceId` are lifted out of the payload into the envelope's
9
+ * own fields, so `data` carries what the event kind is *about* and nothing that
10
+ * every event has.
11
+ */
12
+ export declare function toSourceEvent(event: DomainEvent): WebhookSourceEvent;
13
+ //# sourceMappingURL=event-mapping.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-mapping.d.ts","sourceRoot":"","sources":["../../../src/lib/infrastructure/event-mapping.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,KAAK,EAER,oBAAoB,EACpB,kBAAkB,EACrB,MAAM,2BAA2B,CAAC;AAcnC,uDAAuD;AACvD,wBAAgB,eAAe,CAAC,KAAK,EAAE,WAAW,GAAG,oBAAoB,CAMxE;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,WAAW,GAAG,kBAAkB,CAgBpE"}