@openlup/platform 0.29.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.
Files changed (121) hide show
  1. package/CHANGELOG.md +150 -138
  2. package/README.md +80 -3
  3. package/dist/composition/nodeHost.d.ts +1 -1
  4. package/dist/composition/nodeHost.js +3 -2
  5. package/dist/lifecycle/appUpgrade.d.ts +52 -0
  6. package/dist/lifecycle/appUpgrade.js +180 -0
  7. package/dist/lifecycle/index.d.ts +2 -0
  8. package/dist/lifecycle/index.js +2 -0
  9. package/dist/lifecycle/postgresInstallQualification.d.ts +51 -0
  10. package/dist/lifecycle/postgresInstallQualification.js +74 -0
  11. package/dist/migrations/postgresRunner.d.ts +9 -0
  12. package/dist/migrations/postgresRunner.js +17 -10
  13. package/dist/operations/adminAlertsSchemas.d.ts +153 -0
  14. package/dist/operations/adminAlertsSchemas.js +69 -0
  15. package/dist/operations/adminAlertsView.d.ts +73 -0
  16. package/dist/operations/adminAlertsView.js +184 -0
  17. package/dist/operations/alertPagingPolicy.d.ts +15 -0
  18. package/dist/operations/alertPagingPolicy.js +43 -0
  19. package/dist/operations/alertThrottlePolicy.d.ts +3 -0
  20. package/dist/operations/alertThrottlePolicy.js +18 -0
  21. package/dist/operations/clients.d.ts +17 -0
  22. package/dist/operations/clients.js +32 -0
  23. package/dist/operations/contracts/admin.d.ts +93 -0
  24. package/dist/operations/contracts/admin.js +1 -0
  25. package/dist/operations/contracts/adminAlertsContracts.d.ts +40 -0
  26. package/dist/operations/contracts/adminAlertsContracts.js +1 -0
  27. package/dist/operations/contracts/alertHumanContext.d.ts +7 -0
  28. package/dist/operations/contracts/alertHumanContext.js +1 -0
  29. package/dist/operations/contracts/index.d.ts +6 -0
  30. package/dist/operations/contracts/index.js +6 -0
  31. package/dist/operations/contracts/observabilityContracts.d.ts +41 -0
  32. package/dist/operations/contracts/observabilityContracts.js +1 -0
  33. package/dist/operations/contracts/observabilityPorts.d.ts +18 -0
  34. package/dist/operations/contracts/observabilityPorts.js +1 -0
  35. package/dist/operations/contracts/ports.d.ts +19 -0
  36. package/dist/operations/contracts/ports.js +1 -0
  37. package/dist/operations/errors.d.ts +6 -0
  38. package/dist/operations/errors.js +6 -0
  39. package/dist/operations/index.d.ts +9 -0
  40. package/dist/operations/index.js +9 -0
  41. package/dist/operations/schemas.d.ts +138 -0
  42. package/dist/operations/schemas.js +104 -0
  43. package/dist/operations/server/adminAlertsHandlers.d.ts +16 -0
  44. package/dist/operations/server/adminAlertsHandlers.js +35 -0
  45. package/dist/operations/server/adminPipelineHandlers.d.ts +12 -0
  46. package/dist/operations/server/adminPipelineHandlers.js +48 -0
  47. package/dist/operations/server/adminSettingsHandlers.d.ts +14 -0
  48. package/dist/operations/server/adminSettingsHandlers.js +82 -0
  49. package/dist/operations/server/controlPlaneHandler.d.ts +7 -0
  50. package/dist/operations/server/controlPlaneHandler.js +54 -0
  51. package/dist/operations/server/controlPlaneStore.d.ts +12 -0
  52. package/dist/operations/server/controlPlaneStore.js +138 -0
  53. package/dist/operations/server/controlPlaneWatchdog.d.ts +7 -0
  54. package/dist/operations/server/controlPlaneWatchdog.js +20 -0
  55. package/dist/operations/server/index.d.ts +12 -0
  56. package/dist/operations/server/index.js +12 -0
  57. package/dist/operations/server/migrations.d.ts +14 -0
  58. package/dist/operations/server/migrations.js +7 -0
  59. package/dist/operations/server/platformWatchdogDecision.d.ts +34 -0
  60. package/dist/operations/server/platformWatchdogDecision.js +71 -0
  61. package/dist/operations/server/platformWatchdogDeliveryPolicy.d.ts +10 -0
  62. package/dist/operations/server/platformWatchdogDeliveryPolicy.js +53 -0
  63. package/dist/operations/server/platformWatchdogQueue.d.ts +11 -0
  64. package/dist/operations/server/platformWatchdogQueue.js +94 -0
  65. package/dist/operations/server/platformWatchdogService.d.ts +47 -0
  66. package/dist/operations/server/platformWatchdogService.js +105 -0
  67. package/dist/operations/validation.d.ts +2 -0
  68. package/dist/operations/validation.js +3 -0
  69. package/dist/portability/filesystemObjects.d.ts +12 -0
  70. package/dist/portability/filesystemObjects.js +191 -0
  71. package/dist/portability/index.d.ts +5 -0
  72. package/dist/portability/index.js +4 -0
  73. package/dist/portability/objects.d.ts +63 -0
  74. package/dist/portability/objects.js +138 -0
  75. package/dist/portability/postgresRows.d.ts +64 -0
  76. package/dist/portability/postgresRows.js +182 -0
  77. package/dist/scheduler/advisoryLock.js +20 -2
  78. package/dist/scheduler/nodeScheduler.d.ts +2 -2
  79. package/dist/scheduler/nodeScheduler.js +35 -6
  80. package/package.json +27 -2
  81. package/release-gates.json +73 -7
  82. package/sql/20260814010000_platform_control_plane.sql +286 -0
  83. package/src/composition/nodeHost.ts +4 -3
  84. package/src/lifecycle/appUpgrade.ts +186 -0
  85. package/src/lifecycle/index.ts +19 -0
  86. package/src/lifecycle/postgresInstallQualification.ts +125 -0
  87. package/src/migrations/postgresRunner.ts +24 -9
  88. package/src/operations/adminAlertsSchemas.ts +78 -0
  89. package/src/operations/adminAlertsView.ts +238 -0
  90. package/src/operations/alertPagingPolicy.ts +50 -0
  91. package/src/operations/alertThrottlePolicy.ts +22 -0
  92. package/src/operations/clients.ts +44 -0
  93. package/src/operations/contracts/admin.ts +24 -0
  94. package/src/operations/contracts/adminAlertsContracts.ts +13 -0
  95. package/src/operations/contracts/alertHumanContext.ts +17 -0
  96. package/src/operations/contracts/index.ts +6 -0
  97. package/src/operations/contracts/observabilityContracts.ts +40 -0
  98. package/src/operations/contracts/observabilityPorts.ts +30 -0
  99. package/src/operations/contracts/ports.ts +22 -0
  100. package/src/operations/errors.ts +6 -0
  101. package/src/operations/index.ts +9 -0
  102. package/src/operations/schemas.ts +122 -0
  103. package/src/operations/server/adminAlertsHandlers.ts +61 -0
  104. package/src/operations/server/adminPipelineHandlers.ts +73 -0
  105. package/src/operations/server/adminSettingsHandlers.ts +114 -0
  106. package/src/operations/server/controlPlaneHandler.ts +56 -0
  107. package/src/operations/server/controlPlaneStore.ts +150 -0
  108. package/src/operations/server/controlPlaneWatchdog.ts +28 -0
  109. package/src/operations/server/index.ts +12 -0
  110. package/src/operations/server/migrations.ts +7 -0
  111. package/src/operations/server/platformWatchdogDecision.ts +93 -0
  112. package/src/operations/server/platformWatchdogDeliveryPolicy.ts +92 -0
  113. package/src/operations/server/platformWatchdogQueue.ts +96 -0
  114. package/src/operations/server/platformWatchdogService.ts +209 -0
  115. package/src/operations/validation.ts +3 -0
  116. package/src/portability/filesystemObjects.ts +138 -0
  117. package/src/portability/index.ts +12 -0
  118. package/src/portability/objects.ts +160 -0
  119. package/src/portability/postgresRows.ts +169 -0
  120. package/src/scheduler/advisoryLock.ts +20 -5
  121. package/src/scheduler/nodeScheduler.ts +28 -8
@@ -0,0 +1,238 @@
1
+ import type {
2
+ AdminAlert,
3
+ AdminAlertHealth,
4
+ AdminAlertLane,
5
+ AdminAlertSeverity,
6
+ AdminAlertsHeartbeat,
7
+ AdminAlertsOverviewResponse,
8
+ AdminAlertsSummary,
9
+ } from "./contracts/adminAlertsContracts.js";
10
+ import { DEFAULT_PAGING_MIN_SEVERITY, shouldPage } from "./alertPagingPolicy.js";
11
+
12
+ /**
13
+ * Pure projection of platform_alerts rows onto the admin shell's badges.
14
+ *
15
+ * All filtering lives here rather than in SQL so the `now` boundary is injectable
16
+ * and every rule is unit-testable without a database.
17
+ */
18
+
19
+ /** Raw ledger row as selected from public.platform_alerts. */
20
+ export type PlatformAlertLedgerRow = {
21
+ id: string;
22
+ dedupe_key: string;
23
+ owner: string;
24
+ severity: string;
25
+ status: string;
26
+ title: string;
27
+ message: string | null;
28
+ support_code: string | null;
29
+ runbook_url: string | null;
30
+ first_seen_at: string;
31
+ last_seen_at: string;
32
+ snoozed_until: string | null;
33
+ };
34
+
35
+ export type WatchdogHeartbeatRow = {
36
+ last_success_at: string | null;
37
+ } | null;
38
+
39
+ /**
40
+ * Generic scheduled-job alerts inherit `owner` from the job catalog entry, so a
41
+ * commerce-owned job leaks engineer-only noise ("this cron failed") into what
42
+ * should be an operator's queue. They still count toward platform health.
43
+ */
44
+ export const GENERIC_JOB_DEDUPE_PREFIXES = [
45
+ "job_failed:",
46
+ "job_missed:",
47
+ "job_never_succeeded:",
48
+ "job_stuck_running:",
49
+ "job_ledger_missing:",
50
+ ] as const;
51
+
52
+ /**
53
+ * How old a watchdog heartbeat may be before the health pill stops trusting it.
54
+ *
55
+ * The development-preview reference uses a conservative six-hour stale window
56
+ * independent from any scheduler cadence. It is a policy default, not a
57
+ * scheduling SLA; adopters must review it against their expected maximum
58
+ * heartbeat delay and queue jitter.
59
+ */
60
+ export const WATCHDOG_STALE_AFTER_SECONDS = 6 * 60 * 60;
61
+
62
+ const SEVERITY_RANK: Record<AdminAlertSeverity, number> = { p0: 0, p1: 1, p2: 2, p3: 3 };
63
+
64
+ /**
65
+ * Derived from the paging policy's DEFAULT floor, not from the live threshold:
66
+ * this is a pure projection with no access to PLATFORM_ALERT_PAGING_MIN_SEVERITY,
67
+ * so an operator who overrides that variable makes panel and pager disagree
68
+ * again. Threading the parsed floor through the admin BFF is wave-3 work.
69
+ */
70
+ function isPageableSeverity(severity: AdminAlertSeverity): boolean {
71
+ return shouldPage(severity, DEFAULT_PAGING_MIN_SEVERITY);
72
+ }
73
+
74
+ export function isGenericJobAlert(dedupeKey: string): boolean {
75
+ return GENERIC_JOB_DEDUPE_PREFIXES.some((prefix) => dedupeKey.startsWith(prefix));
76
+ }
77
+
78
+ /**
79
+ * Display bucket. `commerce` means "a business operator can act on this".
80
+ *
81
+ * `commerce/platform` is deliberately excluded: despite the prefix it owns
82
+ * engineering-run plumbing (outbox-dispatch, outbox-prune, abandoned-cart,
83
+ * review-request, reorder-reminder).
84
+ */
85
+ export function classifyLane(owner: string, dedupeKey: string): AdminAlertLane {
86
+ if (isGenericJobAlert(dedupeKey)) return "platform";
87
+ if (!owner.startsWith("commerce/")) return "platform";
88
+ if (owner === "commerce/platform") return "platform";
89
+ return "commerce";
90
+ }
91
+
92
+ export function isSnoozed(snoozedUntil: string | null, now: Date): boolean {
93
+ if (!snoozedUntil) return false;
94
+ const until = Date.parse(snoozedUntil);
95
+ return Number.isFinite(until) && until > now.getTime();
96
+ }
97
+
98
+ /**
99
+ * Unknown severities are treated as p0 — mirrors alertPagingPolicy's fail-safe,
100
+ * where an unrecognised severity always pages rather than silently disappearing.
101
+ */
102
+ function normalizeSeverity(severity: string): AdminAlertSeverity {
103
+ return severity in SEVERITY_RANK ? (severity as AdminAlertSeverity) : "p0";
104
+ }
105
+
106
+ function normalizeIso(value: string): string {
107
+ const parsed = Date.parse(value);
108
+ return Number.isFinite(parsed) ? new Date(parsed).toISOString() : value;
109
+ }
110
+
111
+ /** Unresolved ledger state, including a controlled snoozed exception. */
112
+ function isOpen(row: PlatformAlertLedgerRow): boolean {
113
+ if (row.status !== "open" && row.status !== "acknowledged") return false;
114
+ return true;
115
+ }
116
+
117
+ /** Live enough to affect the health badge and paging. */
118
+ function isLive(row: PlatformAlertLedgerRow, now: Date): boolean {
119
+ return isOpen(row) && !isSnoozed(row.snoozed_until, now);
120
+ }
121
+
122
+ /**
123
+ * Pageable means "this would wake someone up": live and above the paging floor.
124
+ * Acknowledged rows stay pageable on purpose — acknowledged is not resolved, the
125
+ * system is still broken.
126
+ */
127
+ export function isPageable(row: PlatformAlertLedgerRow, now: Date): boolean {
128
+ return isLive(row, now) && isPageableSeverity(normalizeSeverity(row.severity));
129
+ }
130
+
131
+ export function toAdminAlert(row: PlatformAlertLedgerRow, now: Date): AdminAlert {
132
+ return {
133
+ id: row.id,
134
+ dedupeKey: row.dedupe_key,
135
+ lane: classifyLane(row.owner, row.dedupe_key),
136
+ owner: row.owner,
137
+ severity: normalizeSeverity(row.severity),
138
+ status: row.status === "acknowledged" ? "acknowledged" : "open",
139
+ pageable: isPageable(row, now),
140
+ title: row.title,
141
+ message: row.message ?? "",
142
+ supportCode: row.support_code,
143
+ runbookUrl: row.runbook_url,
144
+ firstSeenAt: normalizeIso(row.first_seen_at),
145
+ lastSeenAt: normalizeIso(row.last_seen_at),
146
+ snoozedUntil: row.snoozed_until ? normalizeIso(row.snoozed_until) : null,
147
+ };
148
+ }
149
+
150
+ export function worstSeverity(alerts: readonly AdminAlert[]): AdminAlertSeverity | null {
151
+ let worst: AdminAlertSeverity | null = null;
152
+ for (const alert of alerts) {
153
+ if (!worst || SEVERITY_RANK[alert.severity] < SEVERITY_RANK[worst]) worst = alert.severity;
154
+ }
155
+ return worst;
156
+ }
157
+
158
+ /**
159
+ * `commercePageableCount` and `platformPageableCount` partition the pageable set,
160
+ * so no alert that would page is ever invisible in the popover.
161
+ */
162
+ export function summarizeAlerts(alerts: readonly AdminAlert[]): AdminAlertsSummary {
163
+ const pageable = alerts.filter((alert) => alert.pageable);
164
+ return {
165
+ commercePageableCount: pageable.filter((alert) => alert.lane === "commerce").length,
166
+ commerceTotalCount: alerts.filter((alert) => alert.lane === "commerce").length,
167
+ platformPageableCount: pageable.filter((alert) => alert.lane === "platform").length,
168
+ snoozedCount: 0,
169
+ maxOpenSeverity: worstSeverity(alerts),
170
+ };
171
+ }
172
+
173
+ export function buildHeartbeat(row: WatchdogHeartbeatRow, now: Date): AdminAlertsHeartbeat {
174
+ const lastSuccessAt = row?.last_success_at ?? null;
175
+ const parsed = lastSuccessAt ? Date.parse(lastSuccessAt) : Number.NaN;
176
+ const stale =
177
+ !Number.isFinite(parsed) || now.getTime() - parsed > WATCHDOG_STALE_AFTER_SECONDS * 1000;
178
+ return {
179
+ lastSuccessAt: Number.isFinite(parsed) ? new Date(parsed).toISOString() : null,
180
+ staleAfterSeconds: WATCHDOG_STALE_AFTER_SECONDS,
181
+ stale,
182
+ };
183
+ }
184
+
185
+ /**
186
+ * Staleness beats severity: if the watchdog has not reported recently we do not
187
+ * know the current state, and "unknown" is the only honest answer. There is no
188
+ * path here that renders a green "systems ok" from missing data.
189
+ */
190
+ export function deriveHealth(
191
+ summary: AdminAlertsSummary,
192
+ heartbeat: AdminAlertsHeartbeat,
193
+ ): AdminAlertHealth {
194
+ if (heartbeat.stale) return "unknown";
195
+ if (summary.maxOpenSeverity === "p0") return "down";
196
+ if (summary.maxOpenSeverity === "p1") return "degraded";
197
+ return "ok";
198
+ }
199
+
200
+ /** Alerts returned to the client; counts above stay accurate even when this is truncated. */
201
+ export const ADMIN_ALERTS_LIST_LIMIT = 50;
202
+
203
+ function compareForDisplay(a: AdminAlert, b: AdminAlert): number {
204
+ const bySeverity = SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity];
205
+ if (bySeverity !== 0) return bySeverity;
206
+ return Date.parse(b.lastSeenAt) - Date.parse(a.lastSeenAt);
207
+ }
208
+
209
+ export function buildAlertsOverview(
210
+ rows: readonly PlatformAlertLedgerRow[],
211
+ heartbeatRow: WatchdogHeartbeatRow,
212
+ now: Date,
213
+ ): AdminAlertsOverviewResponse {
214
+ const openRows = rows.filter(isOpen);
215
+ const alerts = openRows
216
+ .filter((row) => isLive(row, now))
217
+ .map((row) => toAdminAlert(row, now))
218
+ .sort(compareForDisplay);
219
+ const snoozedAlerts = openRows
220
+ .filter((row) => isSnoozed(row.snoozed_until, now))
221
+ .map((row) => toAdminAlert(row, now))
222
+ .sort(compareForDisplay);
223
+ const summary = {
224
+ ...summarizeAlerts(alerts),
225
+ snoozedCount: snoozedAlerts.length,
226
+ };
227
+ const heartbeat = buildHeartbeat(heartbeatRow, now);
228
+ return {
229
+ health: deriveHealth(summary, heartbeat),
230
+ summary,
231
+ heartbeat,
232
+ alerts: alerts.slice(0, ADMIN_ALERTS_LIST_LIMIT),
233
+ // Deliberately separate from the active list's limit: controlled exceptions
234
+ // are relevant audit context, but must not turn the health pill red.
235
+ snoozedAlerts,
236
+ generatedAt: now.toISOString(),
237
+ };
238
+ }
@@ -0,0 +1,50 @@
1
+ import type { AlertSeverity } from "./contracts/observabilityContracts.js";
2
+ const ALERT_SEVERITIES: readonly AlertSeverity[] = ["p0", "p1", "p2", "p3"];
3
+
4
+ /**
5
+ * Paging policy for platform watchdog alerts.
6
+ *
7
+ * The alert ledger records every severity, but only sufficiently severe alerts
8
+ * should page an on-call channel (e.g. ntfy). Lower-severity alerts stay
9
+ * ledger-only so real incidents are not buried under routine noise.
10
+ *
11
+ * The tiering is settled: **p0 pages** (customers cannot buy, renewals cannot
12
+ * charge, the platform or database is down); **p1 is panel-urgent** and turns
13
+ * the admin health pill `degraded` without waking anyone; **p2/p3** are the
14
+ * panel list and the ledger. An unknown severity still pages: the fail-open
15
+ * rule is unchanged, because silence must never be the result of a
16
+ * classification error.
17
+ *
18
+ * Lower rank == more severe.
19
+ */
20
+ const SEVERITY_RANK: Record<AlertSeverity, number> = { p0: 0, p1: 1, p2: 2, p3: 3 };
21
+
22
+ /** Alerts at or above this severity page by default; below it are ledger-only. */
23
+ export const DEFAULT_PAGING_MIN_SEVERITY: AlertSeverity = "p0";
24
+
25
+ /**
26
+ * Whether an alert of `severity` should page given the `minSeverity` threshold.
27
+ *
28
+ * Fail-safe: an unknown/unparseable severity always pages (treated as most
29
+ * urgent). Silence must never be the result of a classification error.
30
+ */
31
+ export function shouldPage(
32
+ severity: string,
33
+ minSeverity: AlertSeverity = DEFAULT_PAGING_MIN_SEVERITY,
34
+ ): boolean {
35
+ const rank = SEVERITY_RANK[severity as AlertSeverity];
36
+ if (rank === undefined) return true;
37
+ return rank <= SEVERITY_RANK[minSeverity];
38
+ }
39
+
40
+ /**
41
+ * Parse a paging threshold from configuration (e.g. an env var), falling back to
42
+ * {@link DEFAULT_PAGING_MIN_SEVERITY} for any missing or invalid value.
43
+ */
44
+ export function parsePagingMinSeverity(value: string | null | undefined): AlertSeverity {
45
+ const normalized = value?.trim().toLowerCase();
46
+ if (normalized && (ALERT_SEVERITIES as readonly string[]).includes(normalized)) {
47
+ return normalized as AlertSeverity;
48
+ }
49
+ return DEFAULT_PAGING_MIN_SEVERITY;
50
+ }
@@ -0,0 +1,22 @@
1
+ import type { AlertSeverity } from "./contracts/observabilityContracts.js";
2
+
3
+ /**
4
+ * Per-severity re-notification cadence for platform watchdog alerts.
5
+ *
6
+ * A firing alert re-pages at most once per this window (keyed off
7
+ * `last_notified_at`). Lower severities re-page less often so a persistent
8
+ * low-severity alert cannot flood the channel, while p0 stays loud.
9
+ */
10
+ const THROTTLE_SECONDS_BY_SEVERITY: Record<AlertSeverity, number> = {
11
+ p0: 1 * 60 * 60, // 1h
12
+ p1: 4 * 60 * 60, // 4h
13
+ p2: 12 * 60 * 60, // 12h
14
+ p3: 24 * 60 * 60, // 24h
15
+ };
16
+
17
+ /** Fallback window for an unknown/unparseable severity. */
18
+ export const DEFAULT_THROTTLE_SECONDS = 60 * 60;
19
+
20
+ export function throttleSecondsForSeverity(severity: string): number {
21
+ return THROTTLE_SECONDS_BY_SEVERITY[severity as AlertSeverity] ?? DEFAULT_THROTTLE_SECONDS;
22
+ }
@@ -0,0 +1,44 @@
1
+ import type { z } from "zod";
2
+ import { adminSettingsReadResponseSchema, adminSettingsUpdateResponseSchema, adminPipelineReadResponseSchema } from "./schemas.js";
3
+ import { adminAlertsOverviewResponseSchema } from "./adminAlertsSchemas.js";
4
+ import type { AdminSettingsReadResponse, AdminSettingsUpdateRequest, AdminSettingsUpdateResponse, AdminPipelineReadResponse } from "./contracts/admin.js";
5
+ import type { AdminAlertsOverviewResponse } from "./contracts/adminAlertsContracts.js";
6
+
7
+ /** The existing BFF requester owns fetch, envelope validation, cancellation and diagnostics. */
8
+ export interface OperationsRequestOptions extends Omit<RequestInit, "body"> {
9
+ body?: unknown;
10
+ fetcher?: typeof fetch;
11
+ timeoutMs?: number;
12
+ }
13
+ export type OperationsRequestBff = <T>(path: string, schema: z.ZodType<T>, options?: OperationsRequestOptions) => Promise<T>;
14
+
15
+ /** Bind the existing browser calls without installing an ambient fetch implementation. */
16
+ export function createOperationsClients(requestBff: OperationsRequestBff) {
17
+ return {
18
+ getAdminSettings(accessToken: string, options: OperationsRequestOptions = {}): Promise<AdminSettingsReadResponse> {
19
+ return requestBff("/api/bff/admin/platform/settings", adminSettingsReadResponseSchema, {
20
+ ...options, method: "GET", headers: authHeaders(accessToken, options),
21
+ });
22
+ },
23
+ updateAdminSetting(accessToken: string, request: AdminSettingsUpdateRequest, options: OperationsRequestOptions = {}): Promise<AdminSettingsUpdateResponse> {
24
+ return requestBff("/api/bff/admin/platform/settings/update", adminSettingsUpdateResponseSchema, {
25
+ ...options, method: "POST", headers: authHeaders(accessToken, options), body: request,
26
+ });
27
+ },
28
+ getAdminPipeline(accessToken: string, options: OperationsRequestOptions = {}): Promise<AdminPipelineReadResponse> {
29
+ return requestBff("/api/bff/admin/platform/pipeline", adminPipelineReadResponseSchema, {
30
+ ...options, method: "GET", headers: authHeaders(accessToken, options),
31
+ });
32
+ },
33
+ getAdminAlertsOverview(accessToken: string, options: OperationsRequestOptions = {}): Promise<AdminAlertsOverviewResponse> {
34
+ return requestBff("/api/bff/admin/platform/alerts", adminAlertsOverviewResponseSchema, {
35
+ ...options, method: "GET", headers: authHeaders(accessToken, options),
36
+ });
37
+ },
38
+ };
39
+ }
40
+ function authHeaders(accessToken: string, options: OperationsRequestOptions): Headers {
41
+ const headers = new Headers(options.headers);
42
+ headers.set("Authorization", `Bearer ${accessToken}`);
43
+ return headers;
44
+ }
@@ -0,0 +1,24 @@
1
+ /** DTOs for the existing operational settings, pipeline and control-plane surfaces. */
2
+ export type AdminPipelineTester = { id: string; status: string; email_sequence_step: number | null; email_sequence_paused: boolean | null };
3
+ export type AdminPipelineEmailTemplate = { id: string; name: string; sequence_order: number | null };
4
+ export type AdminPipelineEmailEvent = { event_type: string };
5
+ export type AdminPipelineReadRequest = Record<string, never>;
6
+ export type AdminPipelineReadResponse = { testers: AdminPipelineTester[]; templates: AdminPipelineEmailTemplate[]; emailSendCount: number; emailEvents: AdminPipelineEmailEvent[] };
7
+ export type AdminPipelineDhlTrackingRefreshRequest = Record<string, never>;
8
+ export type AdminPipelineDhlTrackingRefreshResponse = { checked: number; updated: number };
9
+ export type AdminSettingsAdminUser = { id: string; email: string | null; role: string | null };
10
+ export type AdminSettingsReadRequest = Record<string, never>;
11
+ export type AdminSettingsReadResponse = { settings: Record<string, unknown>; adminUsers: AdminSettingsAdminUser[] };
12
+ export type AdminSettingWriteValue = string | number | boolean | null;
13
+ export type AdminSettingsUpdateRequest = { key: string; value: AdminSettingWriteValue };
14
+ export type AdminSettingsUpdateResponse = { key: string; saved: true };
15
+ export type PlatformControlPlaneReadRequest = Record<string, never>;
16
+ export type PlatformControlPlaneReadResponse = {
17
+ controls: { controlKey: string; enabled: boolean; revision: number; updatedAt: string }[];
18
+ jobs: { jobName: string; enabled: boolean; activeDriver: "worker" | "scheduler" | "operator"; lastStatus: "running" | "success" | "failed" | "skipped" | null; lastStartedAt: string | null; lastFinishedAt: string | null; lastSuccessAt: string | null }[];
19
+ alerts: { dedupeKey: string; severity: "p0" | "p1" | "p2" | "p3"; status: "open" | "acknowledged" | "resolved"; owner: string; title: string; firstSeenAt: string; lastSeenAt: string; resolvedAt: string | null }[];
20
+ };
21
+ export type PlatformControlPlaneMutation =
22
+ | { action: "set-control"; idempotencyKey: string; controlKey: string; enabled: boolean }
23
+ | { action: "record-heartbeat"; idempotencyKey: string; health: "healthy" | "degraded" | "failed"; firingCount: number; maxSeverity: "p0" | "p1" | "p2" | "p3" | null; promotionReady: boolean };
24
+ export type PlatformControlPlaneMutationResponse = { action: "set-control" | "record-heartbeat"; replayed: boolean; revision: number | null; recordedAt: string };
@@ -0,0 +1,13 @@
1
+ /** Operator projection; persistence and read authority remain host-owned. */
2
+ export type AdminAlertSeverity = "p0" | "p1" | "p2" | "p3";
3
+ export type AdminAlertLane = "commerce" | "platform";
4
+ export type AdminAlertHealth = "ok" | "degraded" | "down" | "unknown";
5
+ export type AdminAlert = {
6
+ id: string; dedupeKey: string; lane: AdminAlertLane; owner: string;
7
+ severity: AdminAlertSeverity; status: "open" | "acknowledged"; pageable: boolean;
8
+ title: string; message: string; supportCode: string | null; runbookUrl: string | null;
9
+ firstSeenAt: string; lastSeenAt: string; snoozedUntil: string | null;
10
+ };
11
+ export type AdminAlertsHeartbeat = { lastSuccessAt: string | null; staleAfterSeconds: number; stale: boolean };
12
+ export type AdminAlertsSummary = { commercePageableCount: number; commerceTotalCount: number; platformPageableCount: number; snoozedCount: number; maxOpenSeverity: AdminAlertSeverity | null };
13
+ export type AdminAlertsOverviewResponse = { health: AdminAlertHealth; summary: AdminAlertsSummary; heartbeat: AdminAlertsHeartbeat; alerts: AdminAlert[]; snoozedAlerts: AdminAlert[]; generatedAt: string };
@@ -0,0 +1,17 @@
1
+ export type AlertIncidentClass =
2
+ | "customer_communication_failure"
3
+ | "scheduled_job_failed"
4
+ | "scheduled_job_missed"
5
+ | "fulfillment_tracking_stalled"
6
+ | "configuration_drift"
7
+ | "data_integrity"
8
+ | "queue_backlog"
9
+ | "recipient_configuration_failure"
10
+ | "platform_watchdog";
11
+
12
+ export type AlertHumanContext = {
13
+ incidentClass: AlertIncidentClass;
14
+ impact: string;
15
+ firstAction: string;
16
+ urgency: string;
17
+ };
@@ -0,0 +1,6 @@
1
+ export * from "./admin.js";
2
+ export * from "./adminAlertsContracts.js";
3
+ export * from "./ports.js";
4
+ export * from "./observabilityContracts.js";
5
+ export * from "./observabilityPorts.js";
6
+ export * from "./alertHumanContext.js";
@@ -0,0 +1,40 @@
1
+ export type AlertSeverity = "p0" | "p1" | "p2" | "p3";
2
+ /** Opaque host-owned transport key; this module declares no vendor capability. */
3
+ export type AlertChannel = string;
4
+ export type WatchdogSnapshot = { checkedAt: string };
5
+
6
+ export type AlertDecision<Channel extends string = AlertChannel> = {
7
+ dedupeKey: string;
8
+ severity: AlertSeverity;
9
+ owner: string;
10
+ runbookUrl: string;
11
+ title: string;
12
+ message: string; humanContext?: import("./alertHumanContext.js").AlertHumanContext;
13
+ channels: Channel[];
14
+ payload: Record<string, unknown>; paging?: "default" | "never";
15
+ };
16
+
17
+ export type OpenAlert = {
18
+ id: string;
19
+ dedupeKey: string;
20
+ status: "open" | "acknowledged";
21
+ severity: AlertSeverity;
22
+ lastNotifiedAt: string | null;
23
+ /** Most recent watchdog delivery decision, including skipped/failed attempts. */
24
+ lastNotificationAttemptAt?: string | null;
25
+ lastNotificationStatus?: AlertNotificationOutcome["status"] | null;
26
+ /** Earliest time the watchdog may create another delivery-attempt ledger row. */
27
+ nextNotificationAttemptAt?: string | null;
28
+ /** Consecutive failed webhook attempts; reset by sent/skipped outcomes. */
29
+ notificationFailureCount?: number;
30
+ /** Operator silence window; when set and in the future, paging is suppressed. */
31
+ snoozedUntil?: string | null;
32
+ };
33
+
34
+ export type AlertNotificationOutcome<Channel extends string = AlertChannel> = {
35
+ channel: Channel;
36
+ status: "sent" | "failed" | "skipped";
37
+ provider?: string | null;
38
+ providerResponse?: Record<string, unknown> | null;
39
+ error?: string | null;
40
+ };
@@ -0,0 +1,30 @@
1
+ import type {
2
+ AlertDecision,
3
+ AlertNotificationOutcome,
4
+ WatchdogSnapshot,
5
+ OpenAlert,
6
+ } from "./observabilityContracts.js";
7
+
8
+ export type ObservabilityEvidencePort<Snapshot extends WatchdogSnapshot = WatchdogSnapshot> = {
9
+ collectSnapshot(now: Date): Promise<Snapshot>;
10
+ };
11
+
12
+ export type AlertLedgerPort<Channel extends string = string> = {
13
+ listOpenAlerts(): Promise<OpenAlert[]>;
14
+ upsertOpenAlert: (decision: AlertDecision<Channel>, now: Date) => Promise<OpenAlert>;
15
+ resolveAlert(dedupeKey: string, now: Date): Promise<void>;
16
+ recordNotification: (
17
+ alertId: string,
18
+ /** The kernel retains its existing webhook-keyed skipped-attempt audit rows. */
19
+ outcome: AlertNotificationOutcome<Channel | "webhook">,
20
+ now: Date,
21
+ schedule: {
22
+ nextAttemptAt: Date;
23
+ failureCount: number;
24
+ },
25
+ ) => Promise<void>;
26
+ };
27
+
28
+ export type AlertSinkPort<Channel extends string = string> = {
29
+ send: (decision: AlertDecision<Channel>, alert: OpenAlert) => Promise<AlertNotificationOutcome<Channel>>;
30
+ };
@@ -0,0 +1,22 @@
1
+ import type {
2
+ AdminPipelineReadRequest, AdminPipelineReadResponse,
3
+ AdminSettingsReadRequest, AdminSettingsReadResponse,
4
+ AdminSettingsUpdateRequest, AdminSettingsUpdateResponse,
5
+ PlatformControlPlaneReadRequest, PlatformControlPlaneReadResponse,
6
+ PlatformControlPlaneMutation, PlatformControlPlaneMutationResponse,
7
+ } from "./admin.js";
8
+
9
+ export interface AdminPipelineReadPort {
10
+ readPipeline(request: AdminPipelineReadRequest): Promise<AdminPipelineReadResponse>;
11
+ }
12
+ export interface AdminSettingsOperationsPort {
13
+ readSettings(request: AdminSettingsReadRequest): Promise<AdminSettingsReadResponse>;
14
+ updateSetting(request: AdminSettingsUpdateRequest): Promise<AdminSettingsUpdateResponse>;
15
+ }
16
+ export interface PlatformControlPlanePort {
17
+ readControlPlane(request: PlatformControlPlaneReadRequest): Promise<PlatformControlPlaneReadResponse>;
18
+ mutateControlPlane(request: PlatformControlPlaneMutation): Promise<PlatformControlPlaneMutationResponse>;
19
+ }
20
+ export type OperationsAuthorizationResult =
21
+ | { ok: true }
22
+ | { ok: false; code: "UNAUTHORIZED" | "FORBIDDEN"; message: string };
@@ -0,0 +1,6 @@
1
+ export class PlatformControlPlaneConflictError extends Error {
2
+ override readonly name = "PlatformControlPlaneConflictError";
3
+ }
4
+ export class PlatformControlPlaneUnavailableError extends Error {
5
+ override readonly name = "PlatformControlPlaneUnavailableError";
6
+ }
@@ -0,0 +1,9 @@
1
+ /** Browser-safe operational contracts, validation and operator projections. */
2
+ export * from "./contracts/index.js";
3
+ export * from "./schemas.js";
4
+ export * from "./adminAlertsSchemas.js";
5
+ export * from "./adminAlertsView.js";
6
+ export * from "./alertPagingPolicy.js";
7
+ export * from "./alertThrottlePolicy.js";
8
+ export * from "./errors.js";
9
+ export * from "./clients.js";
@@ -0,0 +1,122 @@
1
+ import { z } from "./validation.js";
2
+
3
+ export const adminPipelineTesterSchema = z.object({
4
+ id: z.string().min(1),
5
+ status: z.string().min(1),
6
+ email_sequence_step: z.number().int().min(0).nullable(),
7
+ email_sequence_paused: z.boolean().nullable(),
8
+ });
9
+
10
+ export const adminPipelineEmailTemplateSchema = z.object({
11
+ id: z.string().min(1),
12
+ name: z.string().min(1),
13
+ sequence_order: z.number().int().min(0).nullable(),
14
+ });
15
+
16
+ export const adminPipelineEmailEventSchema = z.object({
17
+ event_type: z.string().min(1),
18
+ });
19
+
20
+ export const adminPipelineReadRequestSchema = z.object({}).strict();
21
+
22
+ export const adminPipelineReadResponseSchema = z.object({
23
+ testers: z.array(adminPipelineTesterSchema),
24
+ templates: z.array(adminPipelineEmailTemplateSchema),
25
+ emailSendCount: z.number().int().min(0),
26
+ emailEvents: z.array(adminPipelineEmailEventSchema),
27
+ });
28
+
29
+ export const adminPipelineDhlTrackingRefreshRequestSchema = z.object({}).strict();
30
+
31
+ export const adminPipelineDhlTrackingRefreshResponseSchema = z.object({
32
+ checked: z.number().int().min(0),
33
+ updated: z.number().int().min(0),
34
+ });
35
+
36
+ export const adminSettingsReadRequestSchema = z.object({}).strict();
37
+
38
+ export const adminSettingsAdminUserSchema = z.object({
39
+ id: z.string().min(1),
40
+ email: z.string().nullable(),
41
+ role: z.string().nullable(),
42
+ });
43
+
44
+ export const adminSettingsReadResponseSchema = z.object({
45
+ settings: z.record(z.string(), z.unknown()),
46
+ adminUsers: z.array(adminSettingsAdminUserSchema),
47
+ });
48
+
49
+ export const adminSettingWriteValueSchema = z.union([
50
+ z.string(),
51
+ z.number(),
52
+ z.boolean(),
53
+ z.null(),
54
+ ]);
55
+
56
+ export const adminSettingsUpdateRequestSchema = z.object({
57
+ key: z.string().trim().min(1),
58
+ value: adminSettingWriteValueSchema,
59
+ });
60
+
61
+ export const adminSettingsUpdateResponseSchema = z.object({
62
+ key: z.string().min(1),
63
+ saved: z.literal(true),
64
+ });
65
+
66
+ const platformControlKeySchema = z.string().trim().regex(/^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$/);
67
+ const platformOperationKeySchema = z.string().trim().regex(/^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/);
68
+ const platformTimestampSchema = z.string().datetime({ offset: true });
69
+
70
+ export const platformControlPlaneReadRequestSchema = z.object({}).strict();
71
+
72
+ export const platformControlPlaneReadResponseSchema = z.object({
73
+ controls: z.array(z.object({
74
+ controlKey: platformControlKeySchema,
75
+ enabled: z.boolean(),
76
+ revision: z.number().int().min(1),
77
+ updatedAt: platformTimestampSchema,
78
+ })),
79
+ jobs: z.array(z.object({
80
+ jobName: z.string().min(1),
81
+ enabled: z.boolean(),
82
+ activeDriver: z.enum(["worker", "scheduler", "operator"]),
83
+ lastStatus: z.enum(["running", "success", "failed", "skipped"]).nullable(),
84
+ lastStartedAt: platformTimestampSchema.nullable(),
85
+ lastFinishedAt: platformTimestampSchema.nullable(),
86
+ lastSuccessAt: platformTimestampSchema.nullable(),
87
+ })),
88
+ alerts: z.array(z.object({
89
+ dedupeKey: z.string().min(1),
90
+ severity: z.enum(["p0", "p1", "p2", "p3"]),
91
+ status: z.enum(["open", "acknowledged", "resolved"]),
92
+ owner: z.string().min(1),
93
+ title: z.string().min(1),
94
+ firstSeenAt: platformTimestampSchema,
95
+ lastSeenAt: platformTimestampSchema,
96
+ resolvedAt: platformTimestampSchema.nullable(),
97
+ })),
98
+ });
99
+
100
+ export const platformControlPlaneMutationSchema = z.union([
101
+ z.object({
102
+ action: z.literal("set-control"),
103
+ idempotencyKey: platformOperationKeySchema,
104
+ controlKey: platformControlKeySchema,
105
+ enabled: z.boolean(),
106
+ }).strict(),
107
+ z.object({
108
+ action: z.literal("record-heartbeat"),
109
+ idempotencyKey: platformOperationKeySchema,
110
+ health: z.enum(["healthy", "degraded", "failed"]),
111
+ firingCount: z.number().int().min(0),
112
+ maxSeverity: z.enum(["p0", "p1", "p2", "p3"]).nullable(),
113
+ promotionReady: z.boolean(),
114
+ }).strict(),
115
+ ]);
116
+
117
+ export const platformControlPlaneMutationResponseSchema = z.object({
118
+ action: z.enum(["set-control", "record-heartbeat"]),
119
+ replayed: z.boolean(),
120
+ revision: z.number().int().min(1).nullable(),
121
+ recordedAt: platformTimestampSchema,
122
+ });