@openlup/platform 0.28.0 → 0.30.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/CHANGELOG.md +369 -156
- package/README.md +220 -4
- package/dist/composition/index.d.ts +1 -0
- package/dist/composition/index.js +1 -0
- package/dist/composition/marketingContribution.d.ts +21 -0
- package/dist/composition/marketingContribution.js +29 -0
- package/dist/composition/nodeHost.d.ts +1 -1
- package/dist/composition/nodeHost.js +3 -2
- package/dist/lifecycle/appUpgrade.d.ts +52 -0
- package/dist/lifecycle/appUpgrade.js +180 -0
- package/dist/lifecycle/index.d.ts +2 -0
- package/dist/lifecycle/index.js +2 -0
- package/dist/lifecycle/postgresInstallQualification.d.ts +51 -0
- package/dist/lifecycle/postgresInstallQualification.js +74 -0
- package/dist/marketing/acquisitionCaseSchemas.d.ts +103 -0
- package/dist/marketing/acquisitionCaseSchemas.js +53 -0
- package/dist/marketing/acquisitionEvidenceSchemas.d.ts +105 -0
- package/dist/marketing/acquisitionEvidenceSchemas.js +55 -0
- package/dist/marketing/analytics/dataLayer.d.ts +18 -0
- package/dist/marketing/analytics/dataLayer.js +111 -0
- package/dist/marketing/analytics/noop.d.ts +9 -0
- package/dist/marketing/analytics/noop.js +8 -0
- package/dist/marketing/analytics/purchaseFacts.d.ts +4 -0
- package/dist/marketing/analytics/purchaseFacts.js +69 -0
- package/dist/marketing/analytics/routePolicy.d.ts +10 -0
- package/dist/marketing/analytics/routePolicy.js +59 -0
- package/dist/marketing/consentEvidence.d.ts +8 -0
- package/dist/marketing/consentEvidence.js +32 -0
- package/dist/marketing/contactLegal.d.ts +14 -0
- package/dist/marketing/contactLegal.js +19 -0
- package/dist/marketing/contracts/acquisition.d.ts +88 -0
- package/dist/marketing/contracts/acquisition.js +1 -0
- package/dist/marketing/contracts/acquisitionPorts.d.ts +49 -0
- package/dist/marketing/contracts/acquisitionPorts.js +1 -0
- package/dist/marketing/contracts/analytics.d.ts +144 -0
- package/dist/marketing/contracts/analytics.js +1 -0
- package/dist/marketing/contracts/contactLegal.d.ts +53 -0
- package/dist/marketing/contracts/contactLegal.js +1 -0
- package/dist/marketing/contracts/index.d.ts +6 -0
- package/dist/marketing/contracts/index.js +1 -0
- package/dist/marketing/contracts/prelaunch.d.ts +39 -0
- package/dist/marketing/contracts/prelaunch.js +1 -0
- package/dist/marketing/index.d.ts +16 -0
- package/dist/marketing/index.js +14 -0
- package/dist/marketing/prelaunchAcquisitionSchemas.d.ts +116 -0
- package/dist/marketing/prelaunchAcquisitionSchemas.js +51 -0
- package/dist/marketing/seo/jsonLd.d.ts +157 -0
- package/dist/marketing/seo/jsonLd.js +127 -0
- package/dist/marketing/seo/metadata.d.ts +41 -0
- package/dist/marketing/seo/metadata.js +36 -0
- package/dist/marketing/seo/serializeJsonLd.d.ts +1 -0
- package/dist/marketing/seo/serializeJsonLd.js +6 -0
- package/dist/marketing/seo/urls.d.ts +14 -0
- package/dist/marketing/seo/urls.js +23 -0
- package/dist/marketing/server/acquisitionCaseDirect.d.ts +4 -0
- package/dist/marketing/server/acquisitionCaseDirect.js +88 -0
- package/dist/marketing/server/acquisitionCaseHandlers.d.ts +28 -0
- package/dist/marketing/server/acquisitionCaseHandlers.js +40 -0
- package/dist/marketing/server/acquisitionCasePorts.d.ts +42 -0
- package/dist/marketing/server/acquisitionCasePorts.js +1 -0
- package/dist/marketing/server/acquisitionEvidenceDirect.d.ts +5 -0
- package/dist/marketing/server/acquisitionEvidenceDirect.js +100 -0
- package/dist/marketing/server/analyticsContext.d.ts +3 -0
- package/dist/marketing/server/analyticsContext.js +65 -0
- package/dist/marketing/server/analyticsContextPorts.d.ts +31 -0
- package/dist/marketing/server/analyticsContextPorts.js +1 -0
- package/dist/marketing/server/bindings.d.ts +14 -0
- package/dist/marketing/server/bindings.js +18 -0
- package/dist/marketing/server/directPorts.d.ts +26 -0
- package/dist/marketing/server/directPorts.js +1 -0
- package/dist/marketing/server/index.d.ts +14 -0
- package/dist/marketing/server/index.js +11 -0
- package/dist/marketing/server/postgres/acquisitionCaseRoutines.d.ts +10 -0
- package/dist/marketing/server/postgres/acquisitionCaseRoutines.js +204 -0
- package/dist/marketing/server/postgres/acquisitionEvidenceRoutines.d.ts +10 -0
- package/dist/marketing/server/postgres/acquisitionEvidenceRoutines.js +107 -0
- package/dist/marketing/server/postgres/index.d.ts +3 -0
- package/dist/marketing/server/postgres/index.js +3 -0
- package/dist/marketing/server/prelaunchAcquisitionDirect.d.ts +4 -0
- package/dist/marketing/server/prelaunchAcquisitionDirect.js +122 -0
- package/dist/marketing/server/privacyContributor.d.ts +66 -0
- package/dist/marketing/server/privacyContributor.js +87 -0
- package/dist/marketing/server/research/adminSurveyResponsesHandler.d.ts +9 -0
- package/dist/marketing/server/research/adminSurveyResponsesHandler.js +53 -0
- package/dist/marketing/server/research/managedSurveyResponseSubmit.d.ts +39 -0
- package/dist/marketing/server/research/managedSurveyResponseSubmit.js +102 -0
- package/dist/marketing/server/research/surveyResponseSubmitHandler.d.ts +8 -0
- package/dist/marketing/server/research/surveyResponseSubmitHandler.js +41 -0
- package/dist/marketing/validation.d.ts +2 -0
- package/dist/marketing/validation.js +3 -0
- package/dist/migrations/postgresRunner.d.ts +9 -0
- package/dist/migrations/postgresRunner.js +17 -10
- package/dist/operations/adminAlertsSchemas.d.ts +153 -0
- package/dist/operations/adminAlertsSchemas.js +69 -0
- package/dist/operations/adminAlertsView.d.ts +73 -0
- package/dist/operations/adminAlertsView.js +184 -0
- package/dist/operations/alertPagingPolicy.d.ts +15 -0
- package/dist/operations/alertPagingPolicy.js +43 -0
- package/dist/operations/alertThrottlePolicy.d.ts +3 -0
- package/dist/operations/alertThrottlePolicy.js +18 -0
- package/dist/operations/clients.d.ts +17 -0
- package/dist/operations/clients.js +32 -0
- package/dist/operations/contracts/admin.d.ts +93 -0
- package/dist/operations/contracts/admin.js +1 -0
- package/dist/operations/contracts/adminAlertsContracts.d.ts +40 -0
- package/dist/operations/contracts/adminAlertsContracts.js +1 -0
- package/dist/operations/contracts/alertHumanContext.d.ts +7 -0
- package/dist/operations/contracts/alertHumanContext.js +1 -0
- package/dist/operations/contracts/index.d.ts +6 -0
- package/dist/operations/contracts/index.js +6 -0
- package/dist/operations/contracts/observabilityContracts.d.ts +41 -0
- package/dist/operations/contracts/observabilityContracts.js +1 -0
- package/dist/operations/contracts/observabilityPorts.d.ts +18 -0
- package/dist/operations/contracts/observabilityPorts.js +1 -0
- package/dist/operations/contracts/ports.d.ts +19 -0
- package/dist/operations/contracts/ports.js +1 -0
- package/dist/operations/errors.d.ts +6 -0
- package/dist/operations/errors.js +6 -0
- package/dist/operations/index.d.ts +9 -0
- package/dist/operations/index.js +9 -0
- package/dist/operations/schemas.d.ts +138 -0
- package/dist/operations/schemas.js +104 -0
- package/dist/operations/server/adminAlertsHandlers.d.ts +16 -0
- package/dist/operations/server/adminAlertsHandlers.js +35 -0
- package/dist/operations/server/adminPipelineHandlers.d.ts +12 -0
- package/dist/operations/server/adminPipelineHandlers.js +48 -0
- package/dist/operations/server/adminSettingsHandlers.d.ts +14 -0
- package/dist/operations/server/adminSettingsHandlers.js +82 -0
- package/dist/operations/server/controlPlaneHandler.d.ts +7 -0
- package/dist/operations/server/controlPlaneHandler.js +54 -0
- package/dist/operations/server/controlPlaneStore.d.ts +12 -0
- package/dist/operations/server/controlPlaneStore.js +138 -0
- package/dist/operations/server/controlPlaneWatchdog.d.ts +7 -0
- package/dist/operations/server/controlPlaneWatchdog.js +20 -0
- package/dist/operations/server/index.d.ts +12 -0
- package/dist/operations/server/index.js +12 -0
- package/dist/operations/server/migrations.d.ts +14 -0
- package/dist/operations/server/migrations.js +7 -0
- package/dist/operations/server/platformWatchdogDecision.d.ts +34 -0
- package/dist/operations/server/platformWatchdogDecision.js +71 -0
- package/dist/operations/server/platformWatchdogDeliveryPolicy.d.ts +10 -0
- package/dist/operations/server/platformWatchdogDeliveryPolicy.js +53 -0
- package/dist/operations/server/platformWatchdogQueue.d.ts +11 -0
- package/dist/operations/server/platformWatchdogQueue.js +94 -0
- package/dist/operations/server/platformWatchdogService.d.ts +47 -0
- package/dist/operations/server/platformWatchdogService.js +105 -0
- package/dist/operations/validation.d.ts +2 -0
- package/dist/operations/validation.js +3 -0
- package/dist/portability/filesystemObjects.d.ts +12 -0
- package/dist/portability/filesystemObjects.js +191 -0
- package/dist/portability/index.d.ts +5 -0
- package/dist/portability/index.js +4 -0
- package/dist/portability/objects.d.ts +63 -0
- package/dist/portability/objects.js +138 -0
- package/dist/portability/postgresRows.d.ts +64 -0
- package/dist/portability/postgresRows.js +182 -0
- package/dist/scheduler/advisoryLock.js +20 -2
- package/dist/scheduler/nodeScheduler.d.ts +2 -2
- package/dist/scheduler/nodeScheduler.js +35 -6
- package/package.json +47 -2
- package/release-gates.json +143 -9
- package/sql/20260814010000_platform_control_plane.sql +286 -0
- package/src/composition/index.ts +1 -0
- package/src/composition/marketingContribution.ts +37 -0
- package/src/composition/nodeHost.ts +4 -3
- package/src/lifecycle/appUpgrade.ts +186 -0
- package/src/lifecycle/index.ts +19 -0
- package/src/lifecycle/postgresInstallQualification.ts +125 -0
- package/src/marketing/acquisitionCaseSchemas.ts +75 -0
- package/src/marketing/acquisitionEvidenceSchemas.ts +72 -0
- package/src/marketing/analytics/dataLayer.ts +100 -0
- package/src/marketing/analytics/noop.ts +14 -0
- package/src/marketing/analytics/purchaseFacts.ts +63 -0
- package/src/marketing/analytics/routePolicy.ts +55 -0
- package/src/marketing/consentEvidence.ts +36 -0
- package/src/marketing/contactLegal.ts +22 -0
- package/src/marketing/contracts/acquisition.ts +50 -0
- package/src/marketing/contracts/acquisitionPorts.ts +48 -0
- package/src/marketing/contracts/analytics.ts +88 -0
- package/src/marketing/contracts/contactLegal.ts +41 -0
- package/src/marketing/contracts/index.ts +17 -0
- package/src/marketing/contracts/prelaunch.ts +15 -0
- package/src/marketing/index.ts +16 -0
- package/src/marketing/prelaunchAcquisitionSchemas.ts +64 -0
- package/src/marketing/seo/jsonLd.ts +153 -0
- package/src/marketing/seo/metadata.ts +62 -0
- package/src/marketing/seo/serializeJsonLd.ts +6 -0
- package/src/marketing/seo/urls.ts +34 -0
- package/src/marketing/server/acquisitionCaseDirect.ts +67 -0
- package/src/marketing/server/acquisitionCaseHandlers.ts +57 -0
- package/src/marketing/server/acquisitionCasePorts.ts +42 -0
- package/src/marketing/server/acquisitionEvidenceDirect.ts +76 -0
- package/src/marketing/server/analyticsContext.ts +55 -0
- package/src/marketing/server/analyticsContextPorts.ts +28 -0
- package/src/marketing/server/bindings.ts +27 -0
- package/src/marketing/server/directPorts.ts +27 -0
- package/src/marketing/server/index.ts +14 -0
- package/src/marketing/server/postgres/acquisitionCaseRoutines.ts +218 -0
- package/src/marketing/server/postgres/acquisitionEvidenceRoutines.ts +103 -0
- package/src/marketing/server/postgres/index.ts +3 -0
- package/src/marketing/server/prelaunchAcquisitionDirect.ts +121 -0
- package/src/marketing/server/privacyContributor.ts +123 -0
- package/src/marketing/server/research/adminSurveyResponsesHandler.ts +78 -0
- package/src/marketing/server/research/managedSurveyResponseSubmit.ts +158 -0
- package/src/marketing/server/research/surveyResponseSubmitHandler.ts +57 -0
- package/src/marketing/validation.ts +3 -0
- package/src/migrations/postgresRunner.ts +24 -9
- package/src/operations/adminAlertsSchemas.ts +78 -0
- package/src/operations/adminAlertsView.ts +238 -0
- package/src/operations/alertPagingPolicy.ts +50 -0
- package/src/operations/alertThrottlePolicy.ts +22 -0
- package/src/operations/clients.ts +44 -0
- package/src/operations/contracts/admin.ts +24 -0
- package/src/operations/contracts/adminAlertsContracts.ts +13 -0
- package/src/operations/contracts/alertHumanContext.ts +17 -0
- package/src/operations/contracts/index.ts +6 -0
- package/src/operations/contracts/observabilityContracts.ts +40 -0
- package/src/operations/contracts/observabilityPorts.ts +30 -0
- package/src/operations/contracts/ports.ts +22 -0
- package/src/operations/errors.ts +6 -0
- package/src/operations/index.ts +9 -0
- package/src/operations/schemas.ts +122 -0
- package/src/operations/server/adminAlertsHandlers.ts +61 -0
- package/src/operations/server/adminPipelineHandlers.ts +73 -0
- package/src/operations/server/adminSettingsHandlers.ts +114 -0
- package/src/operations/server/controlPlaneHandler.ts +56 -0
- package/src/operations/server/controlPlaneStore.ts +150 -0
- package/src/operations/server/controlPlaneWatchdog.ts +28 -0
- package/src/operations/server/index.ts +12 -0
- package/src/operations/server/migrations.ts +7 -0
- package/src/operations/server/platformWatchdogDecision.ts +93 -0
- package/src/operations/server/platformWatchdogDeliveryPolicy.ts +92 -0
- package/src/operations/server/platformWatchdogQueue.ts +96 -0
- package/src/operations/server/platformWatchdogService.ts +209 -0
- package/src/operations/validation.ts +3 -0
- package/src/portability/filesystemObjects.ts +138 -0
- package/src/portability/index.ts +12 -0
- package/src/portability/objects.ts +160 -0
- package/src/portability/postgresRows.ts +169 -0
- package/src/scheduler/advisoryLock.ts +20 -5
- package/src/scheduler/nodeScheduler.ts +28 -8
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createBffResponder } from "../../bff/index.js";
|
|
2
|
+
import { adminPipelineReadRequestSchema, adminPipelineReadResponseSchema, } from "../index.js";
|
|
3
|
+
export function createAdminPipelineReadHandler({ pipelinePort, authorizeAdmin, responseRequestId, }) {
|
|
4
|
+
const responder = createBffResponder({ responseRequestId });
|
|
5
|
+
const { sendBffError, sendBffSuccess, sendMethodNotAllowed } = responder;
|
|
6
|
+
return async function handler(req, res) {
|
|
7
|
+
if (req.method !== "GET") {
|
|
8
|
+
sendMethodNotAllowed(res, ["GET"]);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const authorization = await readAuthorization(req, res, authorizeAdmin, responder);
|
|
12
|
+
if (!authorization)
|
|
13
|
+
return;
|
|
14
|
+
const request = adminPipelineReadRequestSchema.safeParse(req.query);
|
|
15
|
+
if (!request.success) {
|
|
16
|
+
sendBffError(res, "BAD_REQUEST", "Invalid admin pipeline read request", {
|
|
17
|
+
details: request.error.flatten(),
|
|
18
|
+
});
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const result = await pipelinePort.readPipeline(request.data);
|
|
23
|
+
const response = adminPipelineReadResponseSchema.safeParse(result);
|
|
24
|
+
if (!response.success) {
|
|
25
|
+
sendBffError(res, "INVALID_RESPONSE", "Admin pipeline read returned invalid response");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
sendBffSuccess(res, response.data);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
sendBffError(res, "UPSTREAM_UNAVAILABLE", "Admin pipeline read failed");
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
async function readAuthorization(req, res, authorizeAdmin, { sendBffError }) {
|
|
36
|
+
try {
|
|
37
|
+
const authorization = await authorizeAdmin(req);
|
|
38
|
+
if (authorization.ok === false) {
|
|
39
|
+
sendBffError(res, authorization.code, authorization.message);
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
sendBffError(res, "UPSTREAM_UNAVAILABLE", "Admin authorization failed");
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type BffResponderOptions } from "../../bff/index.js";
|
|
2
|
+
import type { HttpRequest, HttpResponse } from "../../http/index.js";
|
|
3
|
+
import type { AdminSettingsOperationsPort } from "../contracts/index.js";
|
|
4
|
+
import type { OperationsAuthorizationResult } from "../contracts/index.js";
|
|
5
|
+
export interface AdminSettingsReadDeps extends BffResponderOptions {
|
|
6
|
+
settingsPort: Pick<AdminSettingsOperationsPort, "readSettings">;
|
|
7
|
+
authorizeAdmin: (req: HttpRequest) => Promise<OperationsAuthorizationResult>;
|
|
8
|
+
}
|
|
9
|
+
export interface AdminSettingsUpdateDeps extends BffResponderOptions {
|
|
10
|
+
settingsPort: Pick<AdminSettingsOperationsPort, "updateSetting">;
|
|
11
|
+
authorizeAdmin: (req: HttpRequest) => Promise<OperationsAuthorizationResult>;
|
|
12
|
+
}
|
|
13
|
+
export declare function createAdminSettingsReadHandler({ settingsPort, authorizeAdmin, responseRequestId, }: AdminSettingsReadDeps): (req: HttpRequest, res: HttpResponse) => Promise<void>;
|
|
14
|
+
export declare function createAdminSettingsUpdateHandler({ settingsPort, authorizeAdmin, responseRequestId, }: AdminSettingsUpdateDeps): (req: HttpRequest, res: HttpResponse) => Promise<void>;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { createBffResponder } from "../../bff/index.js";
|
|
2
|
+
import { adminSettingsReadRequestSchema, adminSettingsReadResponseSchema, adminSettingsUpdateRequestSchema, adminSettingsUpdateResponseSchema, } from "../index.js";
|
|
3
|
+
export function createAdminSettingsReadHandler({ settingsPort, authorizeAdmin, responseRequestId, }) {
|
|
4
|
+
const { sendBffError, sendBffSuccess, sendMethodNotAllowed } = createBffResponder({ responseRequestId });
|
|
5
|
+
return async function handler(req, res) {
|
|
6
|
+
if (req.method !== "GET") {
|
|
7
|
+
sendMethodNotAllowed(res, ["GET"]);
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
let authorization;
|
|
11
|
+
try {
|
|
12
|
+
authorization = await authorizeAdmin(req);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
sendBffError(res, "UPSTREAM_UNAVAILABLE", "Admin authorization failed");
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (authorization.ok === false) {
|
|
19
|
+
sendBffError(res, authorization.code, authorization.message);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const request = adminSettingsReadRequestSchema.safeParse(req.query);
|
|
23
|
+
if (!request.success) {
|
|
24
|
+
sendBffError(res, "BAD_REQUEST", "Invalid admin settings read request", {
|
|
25
|
+
details: request.error.flatten(),
|
|
26
|
+
});
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const result = await settingsPort.readSettings(request.data);
|
|
31
|
+
const response = adminSettingsReadResponseSchema.safeParse(result);
|
|
32
|
+
if (!response.success) {
|
|
33
|
+
sendBffError(res, "INVALID_RESPONSE", "Admin settings read returned invalid response");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
sendBffSuccess(res, response.data);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
sendBffError(res, "UPSTREAM_UNAVAILABLE", "Admin settings read failed");
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function createAdminSettingsUpdateHandler({ settingsPort, authorizeAdmin, responseRequestId, }) {
|
|
44
|
+
const { sendBffError, sendBffSuccess, sendMethodNotAllowed } = createBffResponder({ responseRequestId });
|
|
45
|
+
return async function handler(req, res) {
|
|
46
|
+
if (req.method !== "POST") {
|
|
47
|
+
sendMethodNotAllowed(res, ["POST"]);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
let authorization;
|
|
51
|
+
try {
|
|
52
|
+
authorization = await authorizeAdmin(req);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
sendBffError(res, "UPSTREAM_UNAVAILABLE", "Admin authorization failed");
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (authorization.ok === false) {
|
|
59
|
+
sendBffError(res, authorization.code, authorization.message);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const request = adminSettingsUpdateRequestSchema.safeParse(req.body);
|
|
63
|
+
if (!request.success) {
|
|
64
|
+
sendBffError(res, "BAD_REQUEST", "Invalid admin settings update request", {
|
|
65
|
+
details: request.error.flatten(),
|
|
66
|
+
});
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const result = await settingsPort.updateSetting(request.data);
|
|
71
|
+
const response = adminSettingsUpdateResponseSchema.safeParse(result);
|
|
72
|
+
if (!response.success) {
|
|
73
|
+
sendBffError(res, "INVALID_RESPONSE", "Admin settings update returned invalid response");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
sendBffSuccess(res, response.data);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
sendBffError(res, "UPSTREAM_UNAVAILABLE", "Admin settings update failed");
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type BffResponderOptions } from "../../bff/index.js";
|
|
2
|
+
import type { HttpRequest, HttpResponse } from "../../http/index.js";
|
|
3
|
+
import type { PlatformControlPlanePort } from "../contracts/index.js";
|
|
4
|
+
export declare function createPlatformControlPlaneHandler(deps: {
|
|
5
|
+
port: PlatformControlPlanePort;
|
|
6
|
+
authorizeAdmin: () => Promise<boolean>;
|
|
7
|
+
} & BffResponderOptions): (req: HttpRequest, res: HttpResponse) => Promise<void>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createBffResponder } from "../../bff/index.js";
|
|
2
|
+
import { platformControlPlaneMutationResponseSchema, platformControlPlaneMutationSchema, platformControlPlaneReadResponseSchema } from "../index.js";
|
|
3
|
+
import { PlatformControlPlaneConflictError } from "../index.js";
|
|
4
|
+
export function createPlatformControlPlaneHandler(deps) {
|
|
5
|
+
const { sendBffError, sendBffSuccess, sendMethodNotAllowed } = createBffResponder(deps);
|
|
6
|
+
return async (req, res) => {
|
|
7
|
+
if (!(await deps.authorizeAdmin())) {
|
|
8
|
+
sendBffError(res, "UNAUTHORIZED", "Admin session required");
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
if (req.method === "GET") {
|
|
13
|
+
const parsed = platformControlPlaneReadResponseSchema.safeParse(await deps.port.readControlPlane({}));
|
|
14
|
+
if (!parsed.success)
|
|
15
|
+
return invalidResponse(res, sendBffError);
|
|
16
|
+
sendBffSuccess(res, parsed.data);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (req.method === "POST") {
|
|
20
|
+
const parsed = platformControlPlaneMutationSchema.safeParse(body(req.body));
|
|
21
|
+
if (!parsed.success) {
|
|
22
|
+
sendBffError(res, "BAD_REQUEST", "Invalid platform control-plane request");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const response = platformControlPlaneMutationResponseSchema.safeParse(await deps.port.mutateControlPlane(parsed.data));
|
|
26
|
+
if (!response.success)
|
|
27
|
+
return invalidResponse(res, sendBffError);
|
|
28
|
+
sendBffSuccess(res, response.data);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
sendMethodNotAllowed(res, ["GET", "POST"]);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (error instanceof PlatformControlPlaneConflictError) {
|
|
35
|
+
sendBffError(res, "CONFLICT", error.message);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
sendBffError(res, "UPSTREAM_UNAVAILABLE", "Platform control plane unavailable");
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function body(value) {
|
|
43
|
+
if (typeof value !== "string")
|
|
44
|
+
return value;
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(value);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function invalidResponse(res, sendBffError) {
|
|
53
|
+
sendBffError(res, "INVALID_RESPONSE", "Platform control plane returned invalid response");
|
|
54
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type PlatformControlPlanePort } from "../contracts/index.js";
|
|
2
|
+
import type { PgQueryExecutor } from "../../data/index.js";
|
|
3
|
+
export declare const platformControlPlaneSql: {
|
|
4
|
+
readonly active: string;
|
|
5
|
+
readonly setControl: string;
|
|
6
|
+
readonly heartbeat: string;
|
|
7
|
+
readonly controls: string;
|
|
8
|
+
readonly jobs: string;
|
|
9
|
+
readonly alerts: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function createPlatformControlPlanePort(client: PgQueryExecutor, operatorId: string): PlatformControlPlanePort;
|
|
12
|
+
export declare function isPlatformControlOperatorActive(client: PgQueryExecutor, principalId: string): Promise<boolean>;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { PlatformControlPlaneConflictError, PlatformControlPlaneUnavailableError } from "../index.js";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
const rpc = (name, types) => `SELECT * FROM public.${name}(${types.map((type, index) => `$${index + 1}::${type}`).join(", ")})`;
|
|
4
|
+
export const platformControlPlaneSql = {
|
|
5
|
+
active: rpc("platform_control_operator_is_active", ["uuid"]),
|
|
6
|
+
setControl: rpc("platform_control_set_control", ["uuid", "text", "text", "text", "boolean"]),
|
|
7
|
+
heartbeat: rpc("platform_control_record_heartbeat", ["uuid", "text", "text", "text", "integer", "text", "boolean"]),
|
|
8
|
+
controls: rpc("platform_control_list_controls", ["uuid"]),
|
|
9
|
+
jobs: rpc("platform_control_list_jobs", ["uuid"]),
|
|
10
|
+
alerts: rpc("platform_control_list_alerts", ["uuid"]),
|
|
11
|
+
};
|
|
12
|
+
export function createPlatformControlPlanePort(client, operatorId) {
|
|
13
|
+
return {
|
|
14
|
+
async readControlPlane() {
|
|
15
|
+
try {
|
|
16
|
+
const [controls, jobs, alerts] = await Promise.all([
|
|
17
|
+
client.query(platformControlPlaneSql.controls, [operatorId]),
|
|
18
|
+
client.query(platformControlPlaneSql.jobs, [operatorId]),
|
|
19
|
+
client.query(platformControlPlaneSql.alerts, [operatorId]),
|
|
20
|
+
]);
|
|
21
|
+
return {
|
|
22
|
+
controls: controls.rows.map((row) => ({
|
|
23
|
+
controlKey: text(row.control_key), enabled: bool(row.enabled),
|
|
24
|
+
revision: positive(row.revision), updatedAt: timestamp(row.updated_at),
|
|
25
|
+
})),
|
|
26
|
+
jobs: jobs.rows.map((row) => ({
|
|
27
|
+
jobName: text(row.job_name), enabled: bool(row.enabled),
|
|
28
|
+
activeDriver: driver(row.active_driver), lastStatus: jobStatus(row.last_status),
|
|
29
|
+
lastStartedAt: nullableTimestamp(row.last_started_at),
|
|
30
|
+
lastFinishedAt: nullableTimestamp(row.last_finished_at),
|
|
31
|
+
lastSuccessAt: nullableTimestamp(row.last_success_at),
|
|
32
|
+
})),
|
|
33
|
+
alerts: alerts.rows.map((row) => ({
|
|
34
|
+
dedupeKey: text(row.dedupe_key), severity: severity(row.severity),
|
|
35
|
+
status: alertStatus(row.status), owner: text(row.owner), title: text(row.title),
|
|
36
|
+
firstSeenAt: timestamp(row.first_seen_at), lastSeenAt: timestamp(row.last_seen_at),
|
|
37
|
+
resolvedAt: nullableTimestamp(row.resolved_at),
|
|
38
|
+
})),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
throw mapError(error);
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
async mutateControlPlane(request) {
|
|
46
|
+
try {
|
|
47
|
+
const fingerprint = digest(JSON.stringify(request));
|
|
48
|
+
const result = request.action === "set-control"
|
|
49
|
+
? await client.query(platformControlPlaneSql.setControl, [
|
|
50
|
+
operatorId, request.idempotencyKey, fingerprint, request.controlKey, request.enabled,
|
|
51
|
+
])
|
|
52
|
+
: await client.query(platformControlPlaneSql.heartbeat, [
|
|
53
|
+
operatorId, request.idempotencyKey, fingerprint, request.health,
|
|
54
|
+
request.firingCount, request.maxSeverity, request.promotionReady,
|
|
55
|
+
]);
|
|
56
|
+
return mutation(first(result.rows), request.action);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
throw mapError(error);
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export async function isPlatformControlOperatorActive(client, principalId) {
|
|
65
|
+
try {
|
|
66
|
+
return first((await client.query(platformControlPlaneSql.active, [principalId])).rows)
|
|
67
|
+
?.platform_control_operator_is_active === true;
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
throw mapError(error);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function mutation(row, expected) {
|
|
74
|
+
if (!row || row.action !== expected)
|
|
75
|
+
throw new PlatformControlPlaneUnavailableError();
|
|
76
|
+
return {
|
|
77
|
+
action: expected,
|
|
78
|
+
replayed: bool(row.replayed),
|
|
79
|
+
revision: row.revision == null ? null : positive(row.revision),
|
|
80
|
+
recordedAt: timestamp(row.recorded_at),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function digest(value) {
|
|
84
|
+
return createHash("sha256").update(value).digest("hex");
|
|
85
|
+
}
|
|
86
|
+
function first(rows) { return rows[0]; }
|
|
87
|
+
function text(value) {
|
|
88
|
+
if (typeof value !== "string" || value.length === 0)
|
|
89
|
+
throw new PlatformControlPlaneUnavailableError();
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
function bool(value) {
|
|
93
|
+
if (typeof value !== "boolean")
|
|
94
|
+
throw new PlatformControlPlaneUnavailableError();
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
function positive(value) {
|
|
98
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1)
|
|
99
|
+
throw new PlatformControlPlaneUnavailableError();
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
function timestamp(value) {
|
|
103
|
+
const result = text(value);
|
|
104
|
+
if (!Number.isFinite(Date.parse(result)))
|
|
105
|
+
throw new PlatformControlPlaneUnavailableError();
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
function nullableTimestamp(value) { return value == null ? null : timestamp(value); }
|
|
109
|
+
function driver(value) {
|
|
110
|
+
if (value === "worker" || value === "scheduler" || value === "operator")
|
|
111
|
+
return value;
|
|
112
|
+
throw new PlatformControlPlaneUnavailableError();
|
|
113
|
+
}
|
|
114
|
+
function jobStatus(value) {
|
|
115
|
+
if (value == null)
|
|
116
|
+
return null;
|
|
117
|
+
if (value === "running" || value === "success" || value === "failed" || value === "skipped")
|
|
118
|
+
return value;
|
|
119
|
+
throw new PlatformControlPlaneUnavailableError();
|
|
120
|
+
}
|
|
121
|
+
function severity(value) {
|
|
122
|
+
if (value === "p0" || value === "p1" || value === "p2" || value === "p3")
|
|
123
|
+
return value;
|
|
124
|
+
throw new PlatformControlPlaneUnavailableError();
|
|
125
|
+
}
|
|
126
|
+
function alertStatus(value) {
|
|
127
|
+
if (value === "open" || value === "acknowledged" || value === "resolved")
|
|
128
|
+
return value;
|
|
129
|
+
throw new PlatformControlPlaneUnavailableError();
|
|
130
|
+
}
|
|
131
|
+
function mapError(error) {
|
|
132
|
+
if (error instanceof PlatformControlPlaneUnavailableError || error instanceof PlatformControlPlaneConflictError)
|
|
133
|
+
return error;
|
|
134
|
+
const code = typeof error === "object" && error && "code" in error ? error.code : undefined;
|
|
135
|
+
if (code === "23505")
|
|
136
|
+
return new PlatformControlPlaneConflictError("platform_control_idempotency_conflict");
|
|
137
|
+
return new PlatformControlPlaneUnavailableError("platform_control_plane_unavailable");
|
|
138
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PlatformControlPlanePort } from "../contracts/index.js";
|
|
2
|
+
export type DirectPlatformWatchdogResult = {
|
|
3
|
+
status: number;
|
|
4
|
+
body: Record<string, unknown>;
|
|
5
|
+
};
|
|
6
|
+
/** Existing public-bundle evaluation of persisted neutral job facts. No provider calls. */
|
|
7
|
+
export declare function runControlPlaneWatchdogTick(port: PlatformControlPlanePort, now: Date, checkOnly: boolean): Promise<DirectPlatformWatchdogResult>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Existing public-bundle evaluation of persisted neutral job facts. No provider calls. */
|
|
2
|
+
export async function runControlPlaneWatchdogTick(port, now, checkOnly) {
|
|
3
|
+
const snapshot = await port.readControlPlane({});
|
|
4
|
+
const failedJobs = snapshot.jobs.filter((job) => job.enabled && job.lastStatus === "failed");
|
|
5
|
+
const health = failedJobs.length > 0 ? "degraded" : "healthy";
|
|
6
|
+
const recorded = checkOnly ? null : await port.mutateControlPlane({
|
|
7
|
+
action: "record-heartbeat",
|
|
8
|
+
idempotencyKey: `platform-watchdog:${now.toISOString()}`,
|
|
9
|
+
health,
|
|
10
|
+
firingCount: failedJobs.length,
|
|
11
|
+
maxSeverity: failedJobs.length > 0 ? "p2" : null,
|
|
12
|
+
promotionReady: failedJobs.length === 0,
|
|
13
|
+
});
|
|
14
|
+
return { status: 200, body: {
|
|
15
|
+
ok: true, health, firingCount: failedJobs.length,
|
|
16
|
+
failedJobs: failedJobs.map((job) => job.jobName),
|
|
17
|
+
checkedAt: now.toISOString(), heartbeatRecorded: recorded != null,
|
|
18
|
+
replayed: recorded?.replayed ?? false,
|
|
19
|
+
} };
|
|
20
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Server mechanisms; authorization, persistence and provider transports are injected. */
|
|
2
|
+
export * from "./adminSettingsHandlers.js";
|
|
3
|
+
export * from "./adminPipelineHandlers.js";
|
|
4
|
+
export * from "./adminAlertsHandlers.js";
|
|
5
|
+
export * from "./controlPlaneHandler.js";
|
|
6
|
+
export * from "./controlPlaneStore.js";
|
|
7
|
+
export * from "./platformWatchdogService.js";
|
|
8
|
+
export * from "./platformWatchdogDecision.js";
|
|
9
|
+
export * from "./platformWatchdogDeliveryPolicy.js";
|
|
10
|
+
export * from "./platformWatchdogQueue.js";
|
|
11
|
+
export * from "./controlPlaneWatchdog.js";
|
|
12
|
+
export * from "./migrations.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Server mechanisms; authorization, persistence and provider transports are injected. */
|
|
2
|
+
export * from "./adminSettingsHandlers.js";
|
|
3
|
+
export * from "./adminPipelineHandlers.js";
|
|
4
|
+
export * from "./adminAlertsHandlers.js";
|
|
5
|
+
export * from "./controlPlaneHandler.js";
|
|
6
|
+
export * from "./controlPlaneStore.js";
|
|
7
|
+
export * from "./platformWatchdogService.js";
|
|
8
|
+
export * from "./platformWatchdogDecision.js";
|
|
9
|
+
export * from "./platformWatchdogDeliveryPolicy.js";
|
|
10
|
+
export * from "./platformWatchdogQueue.js";
|
|
11
|
+
export * from "./controlPlaneWatchdog.js";
|
|
12
|
+
export * from "./migrations.js";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Sparse projection only: install the complete canonical native prefix, never this unit alone. */
|
|
2
|
+
export declare const OPERATIONS_MIGRATIONS: {
|
|
3
|
+
readonly native: {
|
|
4
|
+
readonly units: readonly [{
|
|
5
|
+
readonly position: 28;
|
|
6
|
+
readonly catalogPath: "db/platform/migrations/20260814010000_platform_control_plane.sql";
|
|
7
|
+
readonly sha256: "b0c8ef6424ebc6f1a84cb132c37908f4831fa40484a729c3713cfb35936559c9";
|
|
8
|
+
readonly url: URL;
|
|
9
|
+
}];
|
|
10
|
+
};
|
|
11
|
+
readonly managed: {
|
|
12
|
+
readonly verified: false;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Sparse projection only: install the complete canonical native prefix, never this unit alone. */
|
|
2
|
+
export const OPERATIONS_MIGRATIONS = {
|
|
3
|
+
native: { units: [{ position: 28, catalogPath: "db/platform/migrations/20260814010000_platform_control_plane.sql",
|
|
4
|
+
sha256: "b0c8ef6424ebc6f1a84cb132c37908f4831fa40484a729c3713cfb35936559c9",
|
|
5
|
+
url: new URL("../../../sql/20260814010000_platform_control_plane.sql", import.meta.url) }] },
|
|
6
|
+
managed: { verified: false },
|
|
7
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { AlertDecision, AlertSeverity } from "../contracts/index.js";
|
|
2
|
+
import type { AlertLedgerPort, AlertSinkPort } from "../contracts/index.js";
|
|
3
|
+
export declare function emptyWatchdogDecisionCounts(): {
|
|
4
|
+
notified: number;
|
|
5
|
+
notificationFailures: number;
|
|
6
|
+
deliveryBackoffCount: number;
|
|
7
|
+
throttledNotifications: number;
|
|
8
|
+
skippedNotifications: number;
|
|
9
|
+
belowThreshold: number;
|
|
10
|
+
suppressed: number;
|
|
11
|
+
actionableCriticalCount: number;
|
|
12
|
+
suppressedCriticalCount: number;
|
|
13
|
+
actionablePageableCount: number;
|
|
14
|
+
};
|
|
15
|
+
/** One complete ledger transition; callers must serialize the same dedupe key. */
|
|
16
|
+
export declare function processWatchdogDecision<Channel extends string>(decision: AlertDecision<Channel>, { ledgerPort, sinkPort, now, pagingMinSeverity, notificationThrottleSeconds, }: {
|
|
17
|
+
ledgerPort: AlertLedgerPort<Channel>;
|
|
18
|
+
sinkPort: AlertSinkPort<Channel>;
|
|
19
|
+
now: Date;
|
|
20
|
+
pagingMinSeverity: AlertSeverity;
|
|
21
|
+
notificationThrottleSeconds?: number;
|
|
22
|
+
}): Promise<{
|
|
23
|
+
notified: number;
|
|
24
|
+
notificationFailures: number;
|
|
25
|
+
deliveryBackoffCount: number;
|
|
26
|
+
throttledNotifications: number;
|
|
27
|
+
skippedNotifications: number;
|
|
28
|
+
belowThreshold: number;
|
|
29
|
+
suppressed: number;
|
|
30
|
+
actionableCriticalCount: number;
|
|
31
|
+
suppressedCriticalCount: number;
|
|
32
|
+
actionablePageableCount: number;
|
|
33
|
+
}>;
|
|
34
|
+
export declare function isCriticalDecision(decision: AlertDecision): boolean;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { shouldPage } from "../index.js";
|
|
2
|
+
import { throttleSecondsForSeverity } from "../index.js";
|
|
3
|
+
import { isActionabilitySuppressed, isDeliveryBackoff, isPagingSuppressed, notificationSchedule, shouldAttemptNotification, } from "./platformWatchdogDeliveryPolicy.js";
|
|
4
|
+
const BELOW_PAGING_SEVERITY_OUTCOME = {
|
|
5
|
+
channel: "webhook", status: "skipped", provider: "webhook", error: "below_paging_severity",
|
|
6
|
+
};
|
|
7
|
+
const NON_PAGEABLE_DIAGNOSTIC_OUTCOME = {
|
|
8
|
+
channel: "webhook", status: "skipped", provider: "webhook", error: "non_pageable_diagnostic",
|
|
9
|
+
};
|
|
10
|
+
export function emptyWatchdogDecisionCounts() {
|
|
11
|
+
return {
|
|
12
|
+
notified: 0,
|
|
13
|
+
notificationFailures: 0,
|
|
14
|
+
deliveryBackoffCount: 0,
|
|
15
|
+
throttledNotifications: 0,
|
|
16
|
+
skippedNotifications: 0,
|
|
17
|
+
belowThreshold: 0,
|
|
18
|
+
suppressed: 0,
|
|
19
|
+
actionableCriticalCount: 0,
|
|
20
|
+
suppressedCriticalCount: 0,
|
|
21
|
+
actionablePageableCount: 0,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** One complete ledger transition; callers must serialize the same dedupe key. */
|
|
25
|
+
export async function processWatchdogDecision(decision, { ledgerPort, sinkPort, now, pagingMinSeverity, notificationThrottleSeconds, }) {
|
|
26
|
+
const counts = emptyWatchdogDecisionCounts();
|
|
27
|
+
const alert = await ledgerPort.upsertOpenAlert(decision, now);
|
|
28
|
+
const critical = isCriticalDecision(decision);
|
|
29
|
+
const actionableSuppressed = isActionabilitySuppressed(alert, now);
|
|
30
|
+
const pagingSuppressed = isPagingSuppressed(alert, now);
|
|
31
|
+
const pageable = decision.paging !== "never" && shouldPage(decision.severity, pagingMinSeverity);
|
|
32
|
+
// Acknowledgement silences pages, but only a snooze suppresses actionability.
|
|
33
|
+
if (critical) {
|
|
34
|
+
if (actionableSuppressed)
|
|
35
|
+
counts.suppressedCriticalCount += 1;
|
|
36
|
+
else
|
|
37
|
+
counts.actionableCriticalCount += 1;
|
|
38
|
+
}
|
|
39
|
+
if (pageable && !pagingSuppressed)
|
|
40
|
+
counts.actionablePageableCount += 1;
|
|
41
|
+
if (pageable && !pagingSuppressed && isDeliveryBackoff(alert, now))
|
|
42
|
+
counts.deliveryBackoffCount += 1;
|
|
43
|
+
if (pagingSuppressed) {
|
|
44
|
+
counts.suppressed += 1;
|
|
45
|
+
return counts;
|
|
46
|
+
}
|
|
47
|
+
const throttleSeconds = notificationThrottleSeconds ?? throttleSecondsForSeverity(decision.severity);
|
|
48
|
+
if (!shouldAttemptNotification(alert, now, throttleSeconds)) {
|
|
49
|
+
counts.throttledNotifications += 1;
|
|
50
|
+
return counts;
|
|
51
|
+
}
|
|
52
|
+
// Non-pageable and below-threshold alerts retain their skipped-attempt audit.
|
|
53
|
+
const nonPageable = decision.paging === "never";
|
|
54
|
+
const paging = !nonPageable && shouldPage(decision.severity, pagingMinSeverity);
|
|
55
|
+
const outcome = paging
|
|
56
|
+
? await sinkPort.send(decision, alert)
|
|
57
|
+
: nonPageable ? NON_PAGEABLE_DIAGNOSTIC_OUTCOME : BELOW_PAGING_SEVERITY_OUTCOME;
|
|
58
|
+
if (outcome.status === "sent")
|
|
59
|
+
counts.notified += 1;
|
|
60
|
+
else if (outcome.status === "failed")
|
|
61
|
+
counts.notificationFailures += 1;
|
|
62
|
+
else
|
|
63
|
+
counts.skippedNotifications += 1;
|
|
64
|
+
if (!paging)
|
|
65
|
+
counts.belowThreshold += 1;
|
|
66
|
+
await ledgerPort.recordNotification(alert.id, outcome, now, notificationSchedule(alert, outcome, now, throttleSeconds));
|
|
67
|
+
return counts;
|
|
68
|
+
}
|
|
69
|
+
export function isCriticalDecision(decision) {
|
|
70
|
+
return decision.paging !== "never" && (decision.severity === "p0" || decision.severity === "p1");
|
|
71
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AlertDecision, AlertNotificationOutcome, OpenAlert } from "../contracts/index.js";
|
|
2
|
+
export declare function shouldAttemptNotification(alert: Pick<OpenAlert, "lastNotifiedAt" | "lastNotificationStatus" | "nextNotificationAttemptAt" | "snoozedUntil">, now: Date, throttleSeconds: number): boolean;
|
|
3
|
+
export declare function notificationSchedule(alert: Pick<OpenAlert, "notificationFailureCount">, outcome: AlertNotificationOutcome, now: Date, throttleSeconds: number): {
|
|
4
|
+
nextAttemptAt: Date;
|
|
5
|
+
failureCount: number;
|
|
6
|
+
};
|
|
7
|
+
export declare function isActionabilitySuppressed(alert: Pick<OpenAlert, "snoozedUntil">, now: Date): boolean;
|
|
8
|
+
export declare function isPagingSuppressed(alert: Pick<OpenAlert, "status" | "snoozedUntil">, now: Date): boolean;
|
|
9
|
+
export declare function isDeliveryBackoff(alert: Pick<OpenAlert, "lastNotificationStatus" | "nextNotificationAttemptAt">, now: Date): boolean;
|
|
10
|
+
export declare function isSeverityEscalation(decision: Pick<AlertDecision, "severity">, alert: Pick<OpenAlert, "severity">): boolean;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const FAILURE_RETRY_INITIAL_SECONDS = 10 * 60;
|
|
2
|
+
const FAILURE_RETRY_MAX_SECONDS = 60 * 60;
|
|
3
|
+
export function shouldAttemptNotification(alert, now, throttleSeconds) {
|
|
4
|
+
const snoozeExpiry = alert.snoozedUntil ? Date.parse(alert.snoozedUntil) : Number.NaN;
|
|
5
|
+
const lastSentAt = alert.lastNotifiedAt ? Date.parse(alert.lastNotifiedAt) : Number.NaN;
|
|
6
|
+
if (alert.lastNotificationStatus !== "failed"
|
|
7
|
+
&& Number.isFinite(snoozeExpiry)
|
|
8
|
+
&& snoozeExpiry <= now.getTime()
|
|
9
|
+
&& (!Number.isFinite(lastSentAt) || snoozeExpiry > lastSentAt)) {
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
const nextAttemptAt = alert.nextNotificationAttemptAt
|
|
13
|
+
? Date.parse(alert.nextNotificationAttemptAt)
|
|
14
|
+
: Number.NaN;
|
|
15
|
+
if (Number.isFinite(nextAttemptAt) && nextAttemptAt > now.getTime())
|
|
16
|
+
return false;
|
|
17
|
+
if (!alert.lastNotifiedAt)
|
|
18
|
+
return true;
|
|
19
|
+
return now.getTime() - new Date(alert.lastNotifiedAt).getTime() >= throttleSeconds * 1000;
|
|
20
|
+
}
|
|
21
|
+
export function notificationSchedule(alert, outcome, now, throttleSeconds) {
|
|
22
|
+
if (outcome.status === "failed") {
|
|
23
|
+
const failureCount = (alert.notificationFailureCount ?? 0) + 1;
|
|
24
|
+
const exponent = Math.max(0, Math.min(failureCount - 1, 16));
|
|
25
|
+
const retrySeconds = Math.min(FAILURE_RETRY_MAX_SECONDS, FAILURE_RETRY_INITIAL_SECONDS * 2 ** exponent);
|
|
26
|
+
return {
|
|
27
|
+
nextAttemptAt: new Date(now.getTime() + retrySeconds * 1000),
|
|
28
|
+
failureCount,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
// Sent uses the severity cadence. Skipped uses the same due field only to
|
|
33
|
+
// avoid duplicate audit rows; last_notified_at still advances on sent only.
|
|
34
|
+
nextAttemptAt: new Date(now.getTime() + throttleSeconds * 1000),
|
|
35
|
+
failureCount: 0,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function isActionabilitySuppressed(alert, now) {
|
|
39
|
+
return alert.snoozedUntil != null && new Date(alert.snoozedUntil).getTime() > now.getTime();
|
|
40
|
+
}
|
|
41
|
+
export function isPagingSuppressed(alert, now) {
|
|
42
|
+
return alert.status === "acknowledged" || isActionabilitySuppressed(alert, now);
|
|
43
|
+
}
|
|
44
|
+
export function isDeliveryBackoff(alert, now) {
|
|
45
|
+
if (alert.lastNotificationStatus !== "failed" || !alert.nextNotificationAttemptAt)
|
|
46
|
+
return false;
|
|
47
|
+
const nextAttemptAt = Date.parse(alert.nextNotificationAttemptAt);
|
|
48
|
+
return Number.isFinite(nextAttemptAt) && nextAttemptAt > now.getTime();
|
|
49
|
+
}
|
|
50
|
+
export function isSeverityEscalation(decision, alert) {
|
|
51
|
+
const rank = { p0: 0, p1: 1, p2: 2, p3: 3 };
|
|
52
|
+
return rank[decision.severity] < rank[alert.severity];
|
|
53
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AlertDecision } from "../contracts/index.js";
|
|
2
|
+
import type { AlertSinkPort } from "../contracts/index.js";
|
|
3
|
+
type WatchdogQueueItem = Pick<AlertDecision, "dedupeKey" | "severity" | "paging">;
|
|
4
|
+
/**
|
|
5
|
+
* Only each key's first unfinished item is eligible. A head heap prioritizes
|
|
6
|
+
* normalized severity without reordering the caller's decisions or duplicates.
|
|
7
|
+
*/
|
|
8
|
+
export declare function runWatchdogQueue(items: readonly WatchdogQueueItem[], process: (index: number) => Promise<void>): Promise<void>;
|
|
9
|
+
/** Per-tick transport limit; admitted waiters still run if an earlier send throws. */
|
|
10
|
+
export declare function createSerialWatchdogSink<Channel extends string>(sinkPort: AlertSinkPort<Channel>): AlertSinkPort<Channel>;
|
|
11
|
+
export {};
|