@cat-factory/integrations 0.143.1 → 0.145.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.
- package/dist/modules/environments/EnvironmentConnectionService.d.ts +16 -1
- package/dist/modules/environments/EnvironmentConnectionService.d.ts.map +1 -1
- package/dist/modules/environments/EnvironmentConnectionService.js +24 -4
- package/dist/modules/environments/EnvironmentConnectionService.js.map +1 -1
- package/dist/modules/environments/EnvironmentProvisioningService.d.ts +18 -1
- package/dist/modules/environments/EnvironmentProvisioningService.d.ts.map +1 -1
- package/dist/modules/environments/EnvironmentProvisioningService.js +36 -14
- package/dist/modules/environments/EnvironmentProvisioningService.js.map +1 -1
- package/dist/modules/environments/EnvironmentTeardownService.d.ts +21 -1
- package/dist/modules/environments/EnvironmentTeardownService.d.ts.map +1 -1
- package/dist/modules/environments/EnvironmentTeardownService.js +25 -5
- package/dist/modules/environments/EnvironmentTeardownService.js.map +1 -1
- package/dist/modules/incidentEnrichment/WorkspaceIncidentEnrichmentProvider.d.ts +14 -2
- package/dist/modules/incidentEnrichment/WorkspaceIncidentEnrichmentProvider.d.ts.map +1 -1
- package/dist/modules/incidentEnrichment/WorkspaceIncidentEnrichmentProvider.js +21 -6
- package/dist/modules/incidentEnrichment/WorkspaceIncidentEnrichmentProvider.js.map +1 -1
- package/dist/modules/notificationWebhook/NotificationWebhookService.d.ts +9 -7
- package/dist/modules/notificationWebhook/NotificationWebhookService.d.ts.map +1 -1
- package/dist/modules/notificationWebhook/NotificationWebhookService.js +57 -12
- package/dist/modules/notificationWebhook/NotificationWebhookService.js.map +1 -1
- package/dist/modules/notificationWebhook/WebhookNotificationChannel.d.ts +2 -0
- package/dist/modules/notificationWebhook/WebhookNotificationChannel.d.ts.map +1 -1
- package/dist/modules/notificationWebhook/WebhookNotificationChannel.js +29 -22
- package/dist/modules/notificationWebhook/WebhookNotificationChannel.js.map +1 -1
- package/dist/modules/notificationWebhook/WebhookPlatformAlertSink.d.ts +1 -0
- package/dist/modules/notificationWebhook/WebhookPlatformAlertSink.d.ts.map +1 -1
- package/dist/modules/notificationWebhook/WebhookPlatformAlertSink.js +10 -11
- package/dist/modules/notificationWebhook/WebhookPlatformAlertSink.js.map +1 -1
- package/dist/modules/notificationWebhook/WebhookRunLifecycleSink.d.ts +1 -0
- package/dist/modules/notificationWebhook/WebhookRunLifecycleSink.d.ts.map +1 -1
- package/dist/modules/notificationWebhook/WebhookRunLifecycleSink.js +12 -11
- package/dist/modules/notificationWebhook/WebhookRunLifecycleSink.js.map +1 -1
- package/dist/modules/notificationWebhook/signedDelivery.d.ts +56 -0
- package/dist/modules/notificationWebhook/signedDelivery.d.ts.map +1 -1
- package/dist/modules/notificationWebhook/signedDelivery.js +114 -12
- package/dist/modules/notificationWebhook/signedDelivery.js.map +1 -1
- package/dist/modules/notificationWebhook/support.d.ts +3 -0
- package/dist/modules/notificationWebhook/support.d.ts.map +1 -1
- package/dist/modules/notificationWebhook/support.js.map +1 -1
- package/dist/modules/observability/RegistryReleaseHealthProvider.d.ts +8 -1
- package/dist/modules/observability/RegistryReleaseHealthProvider.d.ts.map +1 -1
- package/dist/modules/observability/RegistryReleaseHealthProvider.js +7 -1
- package/dist/modules/observability/RegistryReleaseHealthProvider.js.map +1 -1
- package/dist/modules/tasks/BugHuntService.d.ts.map +1 -1
- package/dist/modules/tasks/BugHuntService.js +25 -17
- package/dist/modules/tasks/BugHuntService.js.map +1 -1
- package/dist/modules/tasks/GitLabIssuesProvider.d.ts +52 -1
- package/dist/modules/tasks/GitLabIssuesProvider.d.ts.map +1 -1
- package/dist/modules/tasks/GitLabIssuesProvider.js +122 -4
- package/dist/modules/tasks/GitLabIssuesProvider.js.map +1 -1
- package/dist/modules/tasks/LinearTaskProvider.d.ts +5 -0
- package/dist/modules/tasks/LinearTaskProvider.d.ts.map +1 -1
- package/dist/modules/tasks/LinearTaskProvider.js +5 -0
- package/dist/modules/tasks/LinearTaskProvider.js.map +1 -1
- package/dist/modules/tasks/TaskConnectionService.d.ts.map +1 -1
- package/dist/modules/tasks/TaskConnectionService.js +4 -0
- package/dist/modules/tasks/TaskConnectionService.js.map +1 -1
- package/dist/modules/tasks/github-issues.logic.d.ts +8 -12
- package/dist/modules/tasks/github-issues.logic.d.ts.map +1 -1
- package/dist/modules/tasks/github-issues.logic.js +7 -32
- package/dist/modules/tasks/github-issues.logic.js.map +1 -1
- package/dist/modules/tasks/gitlab-issues.logic.d.ts +56 -1
- package/dist/modules/tasks/gitlab-issues.logic.d.ts.map +1 -1
- package/dist/modules/tasks/gitlab-issues.logic.js +67 -0
- package/dist/modules/tasks/gitlab-issues.logic.js.map +1 -1
- package/dist/modules/tasks/intakeMatch.logic.js +7 -0
- package/dist/modules/tasks/intakeMatch.logic.js.map +1 -1
- package/dist/modules/tasks/jira.logic.d.ts.map +1 -1
- package/dist/modules/tasks/jira.logic.js +1 -2
- package/dist/modules/tasks/jira.logic.js.map +1 -1
- package/dist/modules/tasks/linear.logic.d.ts.map +1 -1
- package/dist/modules/tasks/linear.logic.js +1 -2
- package/dist/modules/tasks/linear.logic.js.map +1 -1
- package/dist/modules/tasks/repo-issues.logic.d.ts +28 -0
- package/dist/modules/tasks/repo-issues.logic.d.ts.map +1 -0
- package/dist/modules/tasks/repo-issues.logic.js +58 -0
- package/dist/modules/tasks/repo-issues.logic.js.map +1 -0
- package/dist/modules/tasks/tasks.logic.d.ts +6 -0
- package/dist/modules/tasks/tasks.logic.d.ts.map +1 -1
- package/dist/modules/tasks/tasks.logic.js +6 -0
- package/dist/modules/tasks/tasks.logic.js.map +1 -1
- package/package.json +4 -4
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DEFAULT_NOTIFICATION_WEBHOOK_TYPES, } from '@cat-factory/contracts';
|
|
2
|
-
import {
|
|
2
|
+
import { fanOutSignedWebhook } from './signedDelivery.js';
|
|
3
3
|
import { WEBHOOK_SIGNATURE_HEADERS } from './webhookSignature.js';
|
|
4
4
|
export class WebhookNotificationChannel {
|
|
5
5
|
deps;
|
|
@@ -14,30 +14,26 @@ export class WebhookNotificationChannel {
|
|
|
14
14
|
// Best-effort: never let a receiver outage/misconfig break the notification lifecycle
|
|
15
15
|
// (CompositeNotificationChannel also isolates us, belt-and-braces). Surface it through the
|
|
16
16
|
// optional observability hook so the failure is diagnosable instead of silently dropped.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
// The notification TYPE is a bounded enum, so it is safe as a dimension and is the one
|
|
23
|
-
// split worth having: an endpoint that only fails on a particular card is a receiver
|
|
24
|
-
// bug, where a flat failure across types is an outage.
|
|
25
|
-
this.deps.operationalMetrics?.increment('notification.delivery_failed', {
|
|
26
|
-
channel: 'webhook',
|
|
27
|
-
type: notification.type,
|
|
28
|
-
});
|
|
17
|
+
//
|
|
18
|
+
// With the fan-out below reporting each endpoint's own failure, what reaches HERE is the
|
|
19
|
+
// read or the compose failing — a fault of the workspace's configuration rather than of any
|
|
20
|
+
// one receiver, which is why this report names no `webhookId`.
|
|
21
|
+
this.reportFailure(error, workspaceId, notification);
|
|
29
22
|
}
|
|
30
23
|
}
|
|
31
24
|
async post(workspaceId, notification) {
|
|
32
|
-
const
|
|
33
|
-
if (
|
|
34
|
-
return;
|
|
35
|
-
if (!deliversType(webhook, notification.type))
|
|
25
|
+
const endpoints = (await this.deps.notificationWebhookRepository.list(workspaceId)).filter((webhook) => webhook.enabled && deliversType(webhook, notification.type));
|
|
26
|
+
if (endpoints.length === 0)
|
|
36
27
|
return;
|
|
37
28
|
const body = {
|
|
38
29
|
// Stable per notification-and-status: a retried attempt repeats it, so a receiver dedupes on
|
|
39
30
|
// it. The status suffix keeps the delivery of a RESOLVED card (channels are re-delivered on
|
|
40
31
|
// resolve) distinct from the original open one, which is a genuinely different event.
|
|
32
|
+
//
|
|
33
|
+
// Deliberately NOT scoped by endpoint: each receiver only ever sees its own copy, so an
|
|
34
|
+
// endpoint segment would put a value in the dedupe key that no receiver can act on, and
|
|
35
|
+
// would break the one case where two subscriptions DO collapse correctly (the same URL
|
|
36
|
+
// registered twice, which should deliver one card once).
|
|
41
37
|
deliveryId: `${notification.id}-${notification.status}`,
|
|
42
38
|
sentAt: this.deps.clock.now(),
|
|
43
39
|
workspaceId,
|
|
@@ -45,11 +41,22 @@ export class WebhookNotificationChannel {
|
|
|
45
41
|
taskId: notification.blockId,
|
|
46
42
|
notification,
|
|
47
43
|
};
|
|
48
|
-
await
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
44
|
+
await fanOutSignedWebhook(this.deps, endpoints, { payload: JSON.stringify(body), sentAt: body.sentAt }, (error, target) => this.reportFailure(error, workspaceId, notification, target.id));
|
|
45
|
+
}
|
|
46
|
+
reportFailure(error, workspaceId, notification, webhookId) {
|
|
47
|
+
this.deps.onError?.(error, {
|
|
48
|
+
workspaceId,
|
|
49
|
+
notificationId: notification.id,
|
|
50
|
+
type: notification.type,
|
|
51
|
+
...(webhookId === undefined ? {} : { webhookId }),
|
|
52
|
+
});
|
|
53
|
+
// The notification TYPE is a bounded enum, so it is safe as a dimension and is the one
|
|
54
|
+
// split worth having: an endpoint that only fails on a particular card is a receiver
|
|
55
|
+
// bug, where a flat failure across types is an outage. The webhook ID is deliberately NOT a
|
|
56
|
+
// dimension: it is operator-chosen, so every workspace's naming would mint its own series.
|
|
57
|
+
this.deps.operationalMetrics?.increment('notification.delivery_failed', {
|
|
58
|
+
channel: 'webhook',
|
|
59
|
+
type: notification.type,
|
|
53
60
|
});
|
|
54
61
|
}
|
|
55
62
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WebhookNotificationChannel.js","sourceRoot":"","sources":["../../../src/modules/notificationWebhook/WebhookNotificationChannel.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kCAAkC,GAEnC,MAAM,wBAAwB,CAAA;AAY/B,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"WebhookNotificationChannel.js","sourceRoot":"","sources":["../../../src/modules/notificationWebhook/WebhookNotificationChannel.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WebhookPlatformAlertSink.d.ts","sourceRoot":"","sources":["../../../src/modules/notificationWebhook/WebhookPlatformAlertSink.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,KAAK,EACL,6BAA6B,EAC7B,kBAAkB,EAClB,iBAAiB,EACjB,YAAY,EACZ,eAAe,EAChB,MAAM,qBAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"WebhookPlatformAlertSink.d.ts","sourceRoot":"","sources":["../../../src/modules/notificationWebhook/WebhookPlatformAlertSink.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,KAAK,EACL,6BAA6B,EAC7B,kBAAkB,EAClB,iBAAiB,EACjB,YAAY,EACZ,eAAe,EAChB,MAAM,qBAAqB,CAAA;AAwB5B,MAAM,WAAW,oCAAoC;IACnD,6BAA6B,EAAE,6BAA6B,CAAA;IAC5D,YAAY,EAAE,YAAY,CAAA;IAC1B,KAAK,EAAE,KAAK,CAAA;IACZ,oFAAoF;IACpF,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;IACxB,yFAAyF;IACzF,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACrC,8FAA8F;IAC9F,eAAe,CAAC,EAAE,eAAe,CAAA;IACjC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,CACR,KAAK,EAAE,OAAO,EACd,OAAO,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,KACnF,IAAI,CAAA;CACV;AAED,qBAAa,wBAAyB,YAAW,iBAAiB;IACpD,OAAO,CAAC,QAAQ,CAAC,IAAI;IAAjC,YAA6B,IAAI,EAAE,oCAAoC,EAAI;IAErE,qBAAqB,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAUzF;YAEa,IAAI;CA8CnB"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { fanOutSignedWebhook } from './signedDelivery.js';
|
|
2
2
|
export class WebhookPlatformAlertSink {
|
|
3
3
|
deps;
|
|
4
4
|
constructor(deps) {
|
|
@@ -17,13 +17,12 @@ export class WebhookPlatformAlertSink {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
async post(workspaceId, event) {
|
|
20
|
-
const
|
|
21
|
-
if (!webhook || !webhook.enabled)
|
|
22
|
-
return;
|
|
20
|
+
const endpoints = (await this.deps.notificationWebhookRepository.list(workspaceId)).filter(
|
|
23
21
|
// EMPTY means NONE, like the run-lifecycle filter and unlike the notification `types` one: an
|
|
24
22
|
// endpoint registered to hear about parked decisions must never start paging an on-call
|
|
25
23
|
// rotation because the deployment shipped a new event family.
|
|
26
|
-
|
|
24
|
+
(webhook) => webhook.enabled && webhook.alertEvents.includes(event.event));
|
|
25
|
+
if (endpoints.length === 0)
|
|
27
26
|
return;
|
|
28
27
|
const body = {
|
|
29
28
|
deliveryId: deliveryIdFor(event),
|
|
@@ -49,12 +48,12 @@ export class WebhookPlatformAlertSink {
|
|
|
49
48
|
failedTotal: event.failedTotal,
|
|
50
49
|
},
|
|
51
50
|
};
|
|
52
|
-
await
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
});
|
|
51
|
+
await fanOutSignedWebhook(this.deps, endpoints, { payload: JSON.stringify(body), sentAt: body.sentAt }, (error, target) => this.deps.onError?.(error, {
|
|
52
|
+
workspaceId,
|
|
53
|
+
accountId: event.accountId,
|
|
54
|
+
event: event.event,
|
|
55
|
+
webhookId: target.id,
|
|
56
|
+
}));
|
|
58
57
|
}
|
|
59
58
|
}
|
|
60
59
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WebhookPlatformAlertSink.js","sourceRoot":"","sources":["../../../src/modules/notificationWebhook/WebhookPlatformAlertSink.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"WebhookPlatformAlertSink.js","sourceRoot":"","sources":["../../../src/modules/notificationWebhook/WebhookPlatformAlertSink.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WebhookRunLifecycleSink.d.ts","sourceRoot":"","sources":["../../../src/modules/notificationWebhook/WebhookRunLifecycleSink.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,KAAK,EACL,6BAA6B,EAC7B,iBAAiB,EACjB,gBAAgB,EAChB,YAAY,EACZ,eAAe,EAChB,MAAM,qBAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"WebhookRunLifecycleSink.d.ts","sourceRoot":"","sources":["../../../src/modules/notificationWebhook/WebhookRunLifecycleSink.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,KAAK,EACL,6BAA6B,EAC7B,iBAAiB,EACjB,gBAAgB,EAChB,YAAY,EACZ,eAAe,EAChB,MAAM,qBAAqB,CAAA;AAmB5B,MAAM,WAAW,mCAAmC;IAClD,6BAA6B,EAAE,6BAA6B,CAAA;IAC5D,YAAY,EAAE,YAAY,CAAA;IAC1B,KAAK,EAAE,KAAK,CAAA;IACZ,oFAAoF;IACpF,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;IACxB,yFAAyF;IACzF,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACrC,8FAA8F;IAC9F,eAAe,CAAC,EAAE,eAAe,CAAA;IACjC;;;;OAIG;IACH,OAAO,CAAC,EAAE,CACR,KAAK,EAAE,OAAO,EACd,OAAO,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,KAC/E,IAAI,CAAA;CACV;AAED,qBAAa,uBAAwB,YAAW,gBAAgB;IAClD,OAAO,CAAC,QAAQ,CAAC,IAAI;IAAjC,YAA6B,IAAI,EAAE,mCAAmC,EAAI;IAEpE,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAYlF;YAEa,IAAI;CA0CnB"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { fanOutSignedWebhook } from './signedDelivery.js';
|
|
2
2
|
export class WebhookRunLifecycleSink {
|
|
3
3
|
deps;
|
|
4
4
|
constructor(deps) {
|
|
@@ -9,6 +9,8 @@ export class WebhookRunLifecycleSink {
|
|
|
9
9
|
await this.post(workspaceId, event);
|
|
10
10
|
}
|
|
11
11
|
catch (error) {
|
|
12
|
+
// A per-endpoint failure is reported by the fan-out with the endpoint named; what reaches
|
|
13
|
+
// here is the read or the compose failing, which belongs to no one receiver.
|
|
12
14
|
this.deps.onError?.(error, {
|
|
13
15
|
workspaceId,
|
|
14
16
|
runId: event.runId,
|
|
@@ -17,12 +19,11 @@ export class WebhookRunLifecycleSink {
|
|
|
17
19
|
}
|
|
18
20
|
}
|
|
19
21
|
async post(workspaceId, event) {
|
|
20
|
-
const
|
|
21
|
-
if (!webhook || !webhook.enabled)
|
|
22
|
-
return;
|
|
22
|
+
const endpoints = (await this.deps.notificationWebhookRepository.list(workspaceId)).filter(
|
|
23
23
|
// EMPTY means NONE here, unlike the notification `types` filter next door: an endpoint
|
|
24
24
|
// registered before run events existed must not silently start receiving a new event family.
|
|
25
|
-
|
|
25
|
+
(webhook) => webhook.enabled && webhook.runEvents.includes(event.event));
|
|
26
|
+
if (endpoints.length === 0)
|
|
26
27
|
return;
|
|
27
28
|
const body = {
|
|
28
29
|
// `<runId>:<event>` — stable across retries AND across a re-delivery, so it is the dedupe
|
|
@@ -45,12 +46,12 @@ export class WebhookRunLifecycleSink {
|
|
|
45
46
|
failure: event.failure,
|
|
46
47
|
},
|
|
47
48
|
};
|
|
48
|
-
await
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
});
|
|
49
|
+
await fanOutSignedWebhook(this.deps, endpoints, { payload: JSON.stringify(body), sentAt: body.sentAt }, (error, target) => this.deps.onError?.(error, {
|
|
50
|
+
workspaceId,
|
|
51
|
+
runId: event.runId,
|
|
52
|
+
event: event.event,
|
|
53
|
+
webhookId: target.id,
|
|
54
|
+
}));
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
57
|
//# sourceMappingURL=WebhookRunLifecycleSink.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WebhookRunLifecycleSink.js","sourceRoot":"","sources":["../../../src/modules/notificationWebhook/WebhookRunLifecycleSink.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"WebhookRunLifecycleSink.js","sourceRoot":"","sources":["../../../src/modules/notificationWebhook/WebhookRunLifecycleSink.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAuCzD,MAAM,OAAO,uBAAuB;IACL,IAAI;IAAjC,YAA6B,IAAyC;oBAAzC,IAAI;IAAwC,CAAC;IAE1E,KAAK,CAAC,eAAe,CAAC,WAAmB,EAAE,KAAwB;QACjE,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,CAAA;QACrC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,0FAA0F;YAC1F,6EAA6E;YAC7E,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE;gBACzB,WAAW;gBACX,KAAK,EAAE,KAAK,CAAC,KAAK;gBAClB,KAAK,EAAE,KAAK,CAAC,KAAK;aACnB,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,IAAI,CAAC,WAAmB,EAAE,KAAwB;QAC9D,MAAM,SAAS,GAAG,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,6BAA6B,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM;QACxF,uFAAuF;QACvF,6FAA6F;QAC7F,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,IAAI,OAAO,CAAC,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,CACxE,CAAA;QACD,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;YAAE,OAAM;QAElC,MAAM,IAAI,GAAuB;YAC/B,0FAA0F;YAC1F,2FAA2F;YAC3F,wFAAwF;YACxF,wFAAwF;YACxF,UAAU,EAAE,GAAG,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,KAAK,EAAE;YAC3C,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE;YAC7B,WAAW;YACX,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,GAAG,EAAE;gBACH,KAAK,EAAE,KAAK,CAAC,KAAK;gBAClB,MAAM,EAAE,KAAK,CAAC,MAAM;gBACpB,SAAS,EAAE,KAAK,CAAC,SAAS;gBAC1B,UAAU,EAAE,KAAK,CAAC,UAAU;gBAC5B,YAAY,EAAE,KAAK,CAAC,YAAY;gBAChC,SAAS,EAAE,KAAK,CAAC,SAAS;gBAC1B,UAAU,EAAE,KAAK,CAAC,UAAU;gBAC5B,cAAc,EAAE,KAAK,CAAC,cAAc;gBACpC,OAAO,EAAE,KAAK,CAAC,OAAO;aACvB;SACF,CAAA;QACD,MAAM,mBAAmB,CACvB,IAAI,CAAC,IAAI,EACT,SAAS,EACT,EAAE,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,EACtD,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,CAChB,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE;YACzB,WAAW;YACX,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,SAAS,EAAE,MAAM,CAAC,EAAE;SACrB,CAAC,CACL,CAAA;IACH,CAAC;CACF"}
|
|
@@ -23,6 +23,24 @@ export interface SignedDeliveryRequest {
|
|
|
23
23
|
payload: string;
|
|
24
24
|
/** The epoch-ms stamp inside the payload; signed alongside it so a replay is detectable. */
|
|
25
25
|
sentAt: number;
|
|
26
|
+
/**
|
|
27
|
+
* Absolute epoch-ms ceiling for this delivery across all of its attempts. Omitted ⇒
|
|
28
|
+
* {@link TOTAL_DELIVERY_BUDGET_MS} from now, which is the single-delivery case.
|
|
29
|
+
*
|
|
30
|
+
* A fan-out supplies its own so that N endpoints share ONE budget: the whole point of the number
|
|
31
|
+
* is the latency the caller pays on a run's terminal path, and that is a property of the fan-out,
|
|
32
|
+
* not of each delivery in it.
|
|
33
|
+
*/
|
|
34
|
+
deadline?: number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Raised for an endpoint the fan-out never got to before the shared budget ran out. Distinct from
|
|
38
|
+
* a delivery error on purpose: "the receiver rejected us" and "we ran out of time to ask" need
|
|
39
|
+
* different fixes, and reporting the second as the first would send an operator to debug a
|
|
40
|
+
* receiver that was never contacted.
|
|
41
|
+
*/
|
|
42
|
+
export declare class WebhookDeliveryNotAttemptedError extends Error {
|
|
43
|
+
constructor(endpointId: string);
|
|
26
44
|
}
|
|
27
45
|
/**
|
|
28
46
|
* POST one signed delivery, retrying within the shared wall-clock budget. Throws the last error
|
|
@@ -30,4 +48,42 @@ export interface SignedDeliveryRequest {
|
|
|
30
48
|
* treat it as best-effort and report it through their observability hook).
|
|
31
49
|
*/
|
|
32
50
|
export declare function postSignedWebhook(deps: SignedDeliveryDependencies, request: SignedDeliveryRequest): Promise<void>;
|
|
51
|
+
/** One subscribed endpoint, as the fan-out needs it: where to POST and what to sign with. */
|
|
52
|
+
export interface SignedDeliveryTarget {
|
|
53
|
+
id: string;
|
|
54
|
+
url: string;
|
|
55
|
+
secretSealed: string | null;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* POST one already-composed body to EVERY subscribed endpoint, isolating each.
|
|
59
|
+
*
|
|
60
|
+
* Three properties are the whole reason this is a helper rather than a loop at each of the three
|
|
61
|
+
* call sites:
|
|
62
|
+
*
|
|
63
|
+
* - **Concurrent, but BOUNDED.** The caller awaits the fan-out, so its cost is latency on the
|
|
64
|
+
* engine step that produced the event: the step that parks or settles a run. Running the
|
|
65
|
+
* deliveries in series would multiply the budget by the endpoint count, so a workspace would pay
|
|
66
|
+
* for enrolling a second integration in the latency of every run. Running them all at once is
|
|
67
|
+
* the opposite mistake, and a quieter one, because the concurrency past `MAX_CONCURRENT_DELIVERIES`
|
|
68
|
+
* is imaginary on the Worker (see that constant) while the budget it burns is real.
|
|
69
|
+
* - **ONE wall-clock budget for the whole fan-out**, not one per endpoint. The number exists to
|
|
70
|
+
* bound what the caller waits for, and the caller waits for all of this, so a per-endpoint budget
|
|
71
|
+
* would let ten endpoints bound nothing at all. Each delivery is clamped to what is left when it
|
|
72
|
+
* actually starts, so its own retry loop still stops on time.
|
|
73
|
+
* - **One failing receiver costs only its own delivery.** A rejected `Promise.all` would abandon
|
|
74
|
+
* nothing (the others are already in flight) but would report ONE failure for the batch, so a
|
|
75
|
+
* permanently broken endpoint would mask the health of every sibling. Each result is settled and
|
|
76
|
+
* reported on its own, naming the endpoint.
|
|
77
|
+
*
|
|
78
|
+
* An endpoint the budget never reached is reported too, as {@link WebhookDeliveryNotAttemptedError}
|
|
79
|
+
* rather than as a delivery failure: it is a cap, and a cap records what it dropped. Silence there
|
|
80
|
+
* would read as a delivery that succeeded, and a generic failure would read as a broken receiver.
|
|
81
|
+
*
|
|
82
|
+
* Never throws: every outcome goes to `onEndpointError` and the fan-out carries on, which is what
|
|
83
|
+
* "best-effort" means for this transport.
|
|
84
|
+
*/
|
|
85
|
+
export declare function fanOutSignedWebhook(deps: SignedDeliveryDependencies, targets: readonly SignedDeliveryTarget[], delivery: {
|
|
86
|
+
payload: string;
|
|
87
|
+
sentAt: number;
|
|
88
|
+
}, onEndpointError: (error: unknown, target: SignedDeliveryTarget) => void): Promise<void>;
|
|
33
89
|
//# sourceMappingURL=signedDelivery.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"signedDelivery.d.ts","sourceRoot":"","sources":["../../../src/modules/notificationWebhook/signedDelivery.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"signedDelivery.d.ts","sourceRoot":"","sources":["../../../src/modules/notificationWebhook/signedDelivery.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AA8C/E,yFAAyF;AACzF,MAAM,WAAW,0BAA0B;IACzC,YAAY,EAAE,YAAY,CAAA;IAC1B,KAAK,EAAE,KAAK,CAAA;IACZ,oFAAoF;IACpF,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;IACxB,yFAAyF;IACzF,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACrC;;;;OAIG;IACH,eAAe,CAAC,EAAE,eAAe,CAAA;CAClC;AAoBD,oFAAoF;AACpF,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,MAAM,CAAA;IACX,4EAA4E;IAC5E,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,6FAA6F;IAC7F,OAAO,EAAE,MAAM,CAAA;IACf,4FAA4F;IAC5F,MAAM,EAAE,MAAM,CAAA;IACd;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED;;;;;GAKG;AACH,qBAAa,gCAAiC,SAAQ,KAAK;IACzD,YAAY,UAAU,EAAE,MAAM,EAK7B;CACF;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,0BAA0B,EAChC,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,IAAI,CAAC,CAiEf;AAED,6FAA6F;AAC7F,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAA;IACV,GAAG,EAAE,MAAM,CAAA;IACX,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;CAC5B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,0BAA0B,EAChC,OAAO,EAAE,SAAS,oBAAoB,EAAE,EACxC,QAAQ,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EAC7C,eAAe,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,oBAAoB,KAAK,IAAI,GACtE,OAAO,CAAC,IAAI,CAAC,CAiCf"}
|
|
@@ -2,30 +2,71 @@ import { ValidationError } from '@cat-factory/kernel';
|
|
|
2
2
|
import { DEFAULT_MAX_REDIRECTS, safeFetch } from '../shared/safe-fetch.js';
|
|
3
3
|
import { assertSafeNotificationWebhookUrl } from './webhookUrl.js';
|
|
4
4
|
import { signWebhookDelivery } from './webhookSignature.js';
|
|
5
|
-
// The one outbound-delivery core
|
|
6
|
-
//
|
|
7
|
-
// this rather than each carrying its own retry loop, because
|
|
8
|
-
// delivery (the wall-clock budget, which failures are worth
|
|
9
|
-
// every redirect hop, the signature headers) is a property of
|
|
10
|
-
// A second copy would be a second place to get the SSRF guard
|
|
5
|
+
// The one outbound-delivery core every registered webhook endpoint is driven through. All three
|
|
6
|
+
// families that POST to one — the notification cards, the run-lifecycle events and the
|
|
7
|
+
// platform-health alerts — share this rather than each carrying its own retry loop, because
|
|
8
|
+
// everything interesting about the delivery (the wall-clock budget, which failures are worth
|
|
9
|
+
// retrying, re-validating the URL on every redirect hop, the signature headers) is a property of
|
|
10
|
+
// the ENDPOINT, not of the payload. A second copy would be a second place to get the SSRF guard
|
|
11
|
+
// subtly wrong.
|
|
12
|
+
//
|
|
13
|
+
// A workspace registers SEVERAL endpoints, so the three families reach this through
|
|
14
|
+
// `fanOutSignedWebhook` rather than calling `postSignedWebhook` per endpoint themselves: the
|
|
15
|
+
// bounded concurrency, the shared wall-clock budget and the per-endpoint isolation are properties
|
|
16
|
+
// of the fan-out, and three hand-rolled copies of it is exactly the drift this file exists to
|
|
17
|
+
// prevent one layer down.
|
|
11
18
|
/** How many attempts one delivery gets, and how long to wait between them (exponential). */
|
|
12
19
|
const MAX_ATTEMPTS = 3;
|
|
13
20
|
const BASE_RETRY_MS = 250;
|
|
14
21
|
/** Give up on a single HTTP attempt after this long, so a black-holing endpoint can't hang us. */
|
|
15
22
|
const REQUEST_TIMEOUT_MS = 5000;
|
|
16
23
|
/**
|
|
17
|
-
* The ceiling on
|
|
18
|
-
* is not the per-attempt timeout multiplied out
|
|
19
|
-
* millisecond spent here is latency added to the engine step that
|
|
20
|
-
* step that parks or settles a run. Three 5s attempts plus backoff
|
|
21
|
-
* ~15.8s, which the in-app and Slack channels never do.
|
|
24
|
+
* The ceiling on one EMISSION: a single delivery, or a whole fan-out across every subscribed
|
|
25
|
+
* endpoint. This is the number that matters, and it is not the per-attempt timeout multiplied out.
|
|
26
|
+
* The caller AWAITS it, so every millisecond spent here is latency added to the engine step that
|
|
27
|
+
* produced the event, the very step that parks or settles a run. Three 5s attempts plus backoff
|
|
28
|
+
* would let a dead receiver add ~15.8s, which the in-app and Slack channels never do.
|
|
22
29
|
*
|
|
23
30
|
* So the retry budget is a WALL-CLOCK deadline, not an attempt count: attempts stop as soon as the
|
|
24
31
|
* remaining budget is gone, and each attempt's own timeout is clamped to what is left. Delivery is
|
|
25
32
|
* best-effort by contract, so a receiver too slow to answer inside the budget is treated exactly
|
|
26
33
|
* like one that failed.
|
|
34
|
+
*
|
|
35
|
+
* `fanOutSignedWebhook` hands the SAME deadline to every endpoint it drives rather than giving each
|
|
36
|
+
* a fresh one, so registering a tenth webhook cannot turn a bounded wait into a ten-times-bounded
|
|
37
|
+
* one. What that trades away is stated at the fan-out: past the budget, endpoints are reported as
|
|
38
|
+
* not attempted rather than delivered late.
|
|
27
39
|
*/
|
|
28
40
|
const TOTAL_DELIVERY_BUDGET_MS = 6000;
|
|
41
|
+
/**
|
|
42
|
+
* How many deliveries may be in flight at once during a fan-out.
|
|
43
|
+
*
|
|
44
|
+
* Six because that is the Cloudflare Workers ceiling on simultaneous open connections per
|
|
45
|
+
* invocation: a seventh `fetch` does not fail, it QUEUES, invisibly to the code that issued it. An
|
|
46
|
+
* unbounded fan-out therefore does not buy parallelism past six, it only hides the queue, and the
|
|
47
|
+
* hiding is the bug: each delivery's wall-clock budget starts when `postSignedWebhook` is entered,
|
|
48
|
+
* so a queued endpoint spends its budget waiting for a connection and is then reported as a
|
|
49
|
+
* failure it never actually attempted. Bounding the fan-out is what makes a reported failure mean
|
|
50
|
+
* the receiver failed.
|
|
51
|
+
*
|
|
52
|
+
* Deliberately ONE number rather than a per-runtime one. Node has no such ceiling, but the cap is
|
|
53
|
+
* ten endpoints, so the parallelism it would unlock is a second wave at most, and both facades
|
|
54
|
+
* behaving identically is worth more than that: the fan-out's timing is what the conformance and
|
|
55
|
+
* unit suites assert against.
|
|
56
|
+
*/
|
|
57
|
+
const MAX_CONCURRENT_DELIVERIES = 6;
|
|
58
|
+
/**
|
|
59
|
+
* Raised for an endpoint the fan-out never got to before the shared budget ran out. Distinct from
|
|
60
|
+
* a delivery error on purpose: "the receiver rejected us" and "we ran out of time to ask" need
|
|
61
|
+
* different fixes, and reporting the second as the first would send an operator to debug a
|
|
62
|
+
* receiver that was never contacted.
|
|
63
|
+
*/
|
|
64
|
+
export class WebhookDeliveryNotAttemptedError extends Error {
|
|
65
|
+
constructor(endpointId) {
|
|
66
|
+
super(`Webhook delivery to \`${endpointId}\` was not attempted: the fan-out spent its ${TOTAL_DELIVERY_BUDGET_MS}ms budget on earlier endpoints`);
|
|
67
|
+
this.name = 'WebhookDeliveryNotAttemptedError';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
29
70
|
/**
|
|
30
71
|
* POST one signed delivery, retrying within the shared wall-clock budget. Throws the last error
|
|
31
72
|
* when every attempt is spent — the CALLER decides what a failure means (both current callers
|
|
@@ -49,7 +90,7 @@ export async function postSignedWebhook(deps, request) {
|
|
|
49
90
|
// `safeFetch` also strips the body and credential headers on a cross-origin hop, so a permitted
|
|
50
91
|
// host cannot bounce the delivery to a different one.
|
|
51
92
|
const assertSafe = (u) => assertSafeNotificationWebhookUrl(u, deps.urlSafetyPolicy);
|
|
52
|
-
const deadline = deps.clock.now() + TOTAL_DELIVERY_BUDGET_MS;
|
|
93
|
+
const deadline = request.deadline ?? deps.clock.now() + TOTAL_DELIVERY_BUDGET_MS;
|
|
53
94
|
let lastError;
|
|
54
95
|
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
55
96
|
if (attempt > 0) {
|
|
@@ -92,6 +133,67 @@ export async function postSignedWebhook(deps, request) {
|
|
|
92
133
|
}
|
|
93
134
|
throw lastError ?? new Error('Webhook delivery failed');
|
|
94
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* POST one already-composed body to EVERY subscribed endpoint, isolating each.
|
|
138
|
+
*
|
|
139
|
+
* Three properties are the whole reason this is a helper rather than a loop at each of the three
|
|
140
|
+
* call sites:
|
|
141
|
+
*
|
|
142
|
+
* - **Concurrent, but BOUNDED.** The caller awaits the fan-out, so its cost is latency on the
|
|
143
|
+
* engine step that produced the event: the step that parks or settles a run. Running the
|
|
144
|
+
* deliveries in series would multiply the budget by the endpoint count, so a workspace would pay
|
|
145
|
+
* for enrolling a second integration in the latency of every run. Running them all at once is
|
|
146
|
+
* the opposite mistake, and a quieter one, because the concurrency past `MAX_CONCURRENT_DELIVERIES`
|
|
147
|
+
* is imaginary on the Worker (see that constant) while the budget it burns is real.
|
|
148
|
+
* - **ONE wall-clock budget for the whole fan-out**, not one per endpoint. The number exists to
|
|
149
|
+
* bound what the caller waits for, and the caller waits for all of this, so a per-endpoint budget
|
|
150
|
+
* would let ten endpoints bound nothing at all. Each delivery is clamped to what is left when it
|
|
151
|
+
* actually starts, so its own retry loop still stops on time.
|
|
152
|
+
* - **One failing receiver costs only its own delivery.** A rejected `Promise.all` would abandon
|
|
153
|
+
* nothing (the others are already in flight) but would report ONE failure for the batch, so a
|
|
154
|
+
* permanently broken endpoint would mask the health of every sibling. Each result is settled and
|
|
155
|
+
* reported on its own, naming the endpoint.
|
|
156
|
+
*
|
|
157
|
+
* An endpoint the budget never reached is reported too, as {@link WebhookDeliveryNotAttemptedError}
|
|
158
|
+
* rather than as a delivery failure: it is a cap, and a cap records what it dropped. Silence there
|
|
159
|
+
* would read as a delivery that succeeded, and a generic failure would read as a broken receiver.
|
|
160
|
+
*
|
|
161
|
+
* Never throws: every outcome goes to `onEndpointError` and the fan-out carries on, which is what
|
|
162
|
+
* "best-effort" means for this transport.
|
|
163
|
+
*/
|
|
164
|
+
export async function fanOutSignedWebhook(deps, targets, delivery, onEndpointError) {
|
|
165
|
+
const deadline = deps.clock.now() + TOTAL_DELIVERY_BUDGET_MS;
|
|
166
|
+
// A shared cursor rather than pre-sliced waves: a worker that finishes early picks up the next
|
|
167
|
+
// endpoint immediately, so one slow receiver delays only itself and not the whole batch behind
|
|
168
|
+
// it. With fixed waves, six healthy endpoints would all wait on the slowest of their six.
|
|
169
|
+
let next = 0;
|
|
170
|
+
const deliverFrom = async () => {
|
|
171
|
+
for (;;) {
|
|
172
|
+
const index = next;
|
|
173
|
+
next += 1;
|
|
174
|
+
const target = targets[index];
|
|
175
|
+
if (!target)
|
|
176
|
+
return;
|
|
177
|
+
if (deps.clock.now() >= deadline) {
|
|
178
|
+
onEndpointError(new WebhookDeliveryNotAttemptedError(target.id), target);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
await postSignedWebhook(deps, {
|
|
183
|
+
url: target.url,
|
|
184
|
+
secretSealed: target.secretSealed,
|
|
185
|
+
payload: delivery.payload,
|
|
186
|
+
sentAt: delivery.sentAt,
|
|
187
|
+
deadline,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
onEndpointError(error, target);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
await Promise.all(Array.from({ length: Math.min(MAX_CONCURRENT_DELIVERIES, targets.length) }, deliverFrom));
|
|
196
|
+
}
|
|
95
197
|
/** Builds the redirect/size errors `safeFetch` raises, carrying its status for the log line. */
|
|
96
198
|
function makeWebhookError(status, message) {
|
|
97
199
|
return new Error(`Webhook delivery failed (${status}): ${message}`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"signedDelivery.js","sourceRoot":"","sources":["../../../src/modules/notificationWebhook/signedDelivery.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AACrD,OAAO,EAAE,qBAAqB,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AAC1E,OAAO,EAAE,gCAAgC,EAAE,MAAM,iBAAiB,CAAA;AAClE,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAA;AAE3D,gGAAgG;AAChG,
|
|
1
|
+
{"version":3,"file":"signedDelivery.js","sourceRoot":"","sources":["../../../src/modules/notificationWebhook/signedDelivery.ts"],"names":[],"mappings":"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"}
|
|
@@ -25,12 +25,14 @@ export interface NotificationWebhookSupportDependencies {
|
|
|
25
25
|
workspaceId: string;
|
|
26
26
|
notificationId: string;
|
|
27
27
|
type: string;
|
|
28
|
+
webhookId?: string;
|
|
28
29
|
}) => void;
|
|
29
30
|
/** The same hook for a RUN-LIFECYCLE delivery, whose context names a run rather than a card. */
|
|
30
31
|
onRunEventError?: (error: unknown, context: {
|
|
31
32
|
workspaceId: string;
|
|
32
33
|
runId: string;
|
|
33
34
|
event: string;
|
|
35
|
+
webhookId?: string;
|
|
34
36
|
}) => void;
|
|
35
37
|
/**
|
|
36
38
|
* The same hook for a PLATFORM-HEALTH delivery, whose context names the account whose health
|
|
@@ -41,6 +43,7 @@ export interface NotificationWebhookSupportDependencies {
|
|
|
41
43
|
workspaceId: string;
|
|
42
44
|
accountId: string;
|
|
43
45
|
event: string;
|
|
46
|
+
webhookId?: string;
|
|
44
47
|
}) => void;
|
|
45
48
|
}
|
|
46
49
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"support.d.ts","sourceRoot":"","sources":["../../../src/modules/notificationWebhook/support.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,KAAK,EACL,6BAA6B,EAC7B,YAAY,EACZ,eAAe,EAChB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAA;AAC5E,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAA;AAC5E,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAA;AACxE,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAA;AAEtE;;;;GAIG;AACH,eAAO,MAAM,gCAAgC,qCAAqC,CAAA;AAElF,MAAM,WAAW,sCAAsC;IACrD,6BAA6B,EAAE,6BAA6B,CAAA;IAC5D,YAAY,EAAE,YAAY,CAAA;IAC1B,KAAK,EAAE,KAAK,CAAA;IACZ;;;;OAIG;IACH,eAAe,CAAC,EAAE,eAAe,CAAA;IACjC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;
|
|
1
|
+
{"version":3,"file":"support.d.ts","sourceRoot":"","sources":["../../../src/modules/notificationWebhook/support.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,KAAK,EACL,6BAA6B,EAC7B,YAAY,EACZ,eAAe,EAChB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAA;AAC5E,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAA;AAC5E,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAA;AACxE,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAA;AAEtE;;;;GAIG;AACH,eAAO,MAAM,gCAAgC,qCAAqC,CAAA;AAElF,MAAM,WAAW,sCAAsC;IACrD,6BAA6B,EAAE,6BAA6B,CAAA;IAC5D,YAAY,EAAE,YAAY,CAAA;IAC1B,KAAK,EAAE,KAAK,CAAA;IACZ;;;;OAIG;IACH,eAAe,CAAC,EAAE,eAAe,CAAA;IACjC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;IAKxB,8FAA8F;IAC9F,OAAO,CAAC,EAAE,CACR,KAAK,EAAE,OAAO,EACd,OAAO,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,KACvF,IAAI,CAAA;IACT,gGAAgG;IAChG,eAAe,CAAC,EAAE,CAChB,KAAK,EAAE,OAAO,EACd,OAAO,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,KAC/E,IAAI,CAAA;IACT;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,CACrB,KAAK,EAAE,OAAO,EACd,OAAO,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,KACnF,IAAI,CAAA;CACV;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,+BAA+B,CAAC,IAAI,EAAE,sCAAsC,GAAG;IAC7F,OAAO,EAAE,0BAA0B,CAAA;IACnC,OAAO,EAAE,0BAA0B,CAAA;IACnC,gBAAgB,EAAE,uBAAuB,CAAA;IACzC,iBAAiB,EAAE,wBAAwB,CAAA;CAC5C,CAiCA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"support.js","sourceRoot":"","sources":["../../../src/modules/notificationWebhook/support.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAA;AAC5E,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAA;AAC5E,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAA;AACxE,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAA;AAEtE;;;;GAIG;AACH,MAAM,CAAC,MAAM,gCAAgC,GAAG,kCAAkC,CAAA;
|
|
1
|
+
{"version":3,"file":"support.js","sourceRoot":"","sources":["../../../src/modules/notificationWebhook/support.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAA;AAC5E,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAA;AAC5E,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAA;AACxE,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAA;AAEtE;;;;GAIG;AACH,MAAM,CAAC,MAAM,gCAAgC,GAAG,kCAAkC,CAAA;AAsClF;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,+BAA+B,CAAC,IAA4C;IAM1F,OAAO;QACL,iBAAiB,EAAE,IAAI,wBAAwB,CAAC;YAC9C,6BAA6B,EAAE,IAAI,CAAC,6BAA6B;YACjE,YAAY,EAAE,IAAI,CAAC,YAAY;YAC/B,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,eAAe,EAAE,IAAI,CAAC,eAAe;YACrC,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,OAAO,EAAE,IAAI,CAAC,oBAAoB;SACnC,CAAC;QACF,gBAAgB,EAAE,IAAI,uBAAuB,CAAC;YAC5C,6BAA6B,EAAE,IAAI,CAAC,6BAA6B;YACjE,YAAY,EAAE,IAAI,CAAC,YAAY;YAC/B,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,eAAe,EAAE,IAAI,CAAC,eAAe;YACrC,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,OAAO,EAAE,IAAI,CAAC,eAAe;SAC9B,CAAC;QACF,OAAO,EAAE,IAAI,0BAA0B,CAAC;YACtC,6BAA6B,EAAE,IAAI,CAAC,6BAA6B;YACjE,YAAY,EAAE,IAAI,CAAC,YAAY;YAC/B,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,eAAe,EAAE,IAAI,CAAC,eAAe;SACtC,CAAC;QACF,OAAO,EAAE,IAAI,0BAA0B,CAAC;YACtC,6BAA6B,EAAE,IAAI,CAAC,6BAA6B;YACjE,YAAY,EAAE,IAAI,CAAC,YAAY;YAC/B,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,eAAe,EAAE,IAAI,CAAC,eAAe;YACrC,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,OAAO,EAAE,IAAI,CAAC,OAAO;SACtB,CAAC;KACH,CAAA;AACH,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BlockRepository, ObservabilityConnectionRepository, ReleaseEvidence, ReleaseHealthConfigRecord, ReleaseHealthConfigRepository, ReleaseHealthProvider, ReleaseHealthReport, ReleaseSignal, SecretCipher } from '@cat-factory/kernel';
|
|
1
|
+
import type { BlockRepository, ObservabilityConnectionRepository, ReleaseEvidence, ReleaseHealthConfigRecord, ReleaseHealthConfigRepository, ReleaseHealthProvider, ReleaseHealthReport, ReleaseSignal, SecretCipher, SecretDelegate } from '@cat-factory/kernel';
|
|
2
2
|
import type { ObservabilityProviderRegistry } from './registry.js';
|
|
3
3
|
/**
|
|
4
4
|
* A single observability vendor's reads, built from its already-decrypted credentials.
|
|
@@ -21,6 +21,12 @@ export interface RegistryReleaseHealthProviderDependencies {
|
|
|
21
21
|
blockRepository: BlockRepository;
|
|
22
22
|
/** Decrypts the workspace's sealed credentials blob at call time. */
|
|
23
23
|
secretCipher: SecretCipher;
|
|
24
|
+
/**
|
|
25
|
+
* Present ONLY on a mothership-mode node, where the connection row was sealed under the
|
|
26
|
+
* MOTHERSHIP's key and this process holds none. The gate probe runs wherever the RUN runs, so
|
|
27
|
+
* without it a mothership-mode deployment could save a connection and never probe with it.
|
|
28
|
+
*/
|
|
29
|
+
secretDelegate?: SecretDelegate;
|
|
24
30
|
/** The provider adapters this facade can build. */
|
|
25
31
|
registry: ObservabilityProviderRegistry;
|
|
26
32
|
/** Override fetch (tests). */
|
|
@@ -36,6 +42,7 @@ export interface RegistryReleaseHealthProviderDependencies {
|
|
|
36
42
|
*/
|
|
37
43
|
export declare class RegistryReleaseHealthProvider implements ReleaseHealthProvider {
|
|
38
44
|
private readonly deps;
|
|
45
|
+
private readonly orgSecrets;
|
|
39
46
|
constructor(deps: RegistryReleaseHealthProviderDependencies);
|
|
40
47
|
probe(workspaceId: string, blockId: string, since: number): Promise<ReleaseHealthReport>;
|
|
41
48
|
gatherEvidence(workspaceId: string, blockId: string, since: number): Promise<ReleaseEvidence>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RegistryReleaseHealthProvider.d.ts","sourceRoot":"","sources":["../../../src/modules/observability/RegistryReleaseHealthProvider.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"RegistryReleaseHealthProvider.d.ts","sourceRoot":"","sources":["../../../src/modules/observability/RegistryReleaseHealthProvider.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,eAAe,EAEf,iCAAiC,EACjC,eAAe,EACf,yBAAyB,EACzB,6BAA6B,EAC7B,qBAAqB,EACrB,mBAAmB,EACnB,aAAa,EACb,YAAY,EACZ,cAAc,EACf,MAAM,qBAAqB,CAAA;AAC5B,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,eAAe,CAAA;AAElE;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACnC,WAAW,CAAC,MAAM,EAAE,yBAAyB,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAAA;IACvF,YAAY,CAAC,MAAM,EAAE,yBAAyB,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC,CAAA;CACnG;AAED,kFAAkF;AAClF,MAAM,MAAM,2BAA2B,GAAG,CACxC,WAAW,EAAE,OAAO,EACpB,IAAI,EAAE;IAAE,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;CAAE,KAC/B,oBAAoB,CAAA;AAEzB,MAAM,WAAW,yCAAyC;IACxD,iCAAiC,EAAE,iCAAiC,CAAA;IACpE,6BAA6B,EAAE,6BAA6B,CAAA;IAC5D,+EAA+E;IAC/E,eAAe,EAAE,eAAe,CAAA;IAChC,qEAAqE;IACrE,YAAY,EAAE,YAAY,CAAA;IAC1B;;;;OAIG;IACH,cAAc,CAAC,EAAE,cAAc,CAAA;IAC/B,mDAAmD;IACnD,QAAQ,EAAE,6BAA6B,CAAA;IACvC,8BAA8B;IAC9B,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;CACzB;AAED;;;;;;;GAOG;AACH,qBAAa,6BAA8B,YAAW,qBAAqB;IAG7D,OAAO,CAAC,QAAQ,CAAC,IAAI;IAFjC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAiB;IAE5C,YAA6B,IAAI,EAAE,yCAAyC,EAK3E;IAEK,KAAK,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAU7F;IAEK,cAAc,CAClB,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,eAAe,CAAC,CAc1B;IAED,iFAAiF;YACnE,OAAO;IAsBrB,mFAAmF;YACrE,aAAa;CAiB5B"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createOrgSecretCipher } from '@cat-factory/kernel';
|
|
1
2
|
/**
|
|
2
3
|
* The pluggable `ReleaseHealthProvider` the post-release-health gate reads: resolve the
|
|
3
4
|
* block's monitor/SLO config (its own, else its service frame's) → load the workspace's
|
|
@@ -8,8 +9,13 @@
|
|
|
8
9
|
*/
|
|
9
10
|
export class RegistryReleaseHealthProvider {
|
|
10
11
|
deps;
|
|
12
|
+
orgSecrets;
|
|
11
13
|
constructor(deps) {
|
|
12
14
|
this.deps = deps;
|
|
15
|
+
this.orgSecrets = createOrgSecretCipher({
|
|
16
|
+
cipher: deps.secretCipher,
|
|
17
|
+
...(deps.secretDelegate ? { delegate: deps.secretDelegate } : {}),
|
|
18
|
+
});
|
|
13
19
|
}
|
|
14
20
|
async probe(workspaceId, blockId, since) {
|
|
15
21
|
const resolved = await this.resolve(workspaceId, blockId);
|
|
@@ -50,7 +56,7 @@ export class RegistryReleaseHealthProvider {
|
|
|
50
56
|
const factory = this.deps.registry.get(connection.provider);
|
|
51
57
|
if (!factory)
|
|
52
58
|
return null;
|
|
53
|
-
const credentials = JSON.parse(await this.
|
|
59
|
+
const credentials = JSON.parse(await this.orgSecrets.decryptFor({ source: 'observability_connection', workspaceId }, connection.credentials));
|
|
54
60
|
const adapter = factory(credentials, { fetchImpl: this.deps.fetchImpl });
|
|
55
61
|
return { adapter, config };
|
|
56
62
|
}
|