@oneuptime/common 12.0.28 → 12.0.29

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 (182) hide show
  1. package/Models/DatabaseModels/DashboardDomain.ts +43 -0
  2. package/Models/DatabaseModels/MonitorTemplate.ts +15 -3
  3. package/Models/DatabaseModels/StatusPageDomain.ts +43 -0
  4. package/Server/API/DashboardDomainAPI.ts +74 -0
  5. package/Server/API/MicrosoftTeamsAPI.ts +81 -180
  6. package/Server/API/StatusPageDomainAPI.ts +74 -0
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.ts +43 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.ts +40 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  10. package/Server/Middleware/ProjectAuthorization.ts +6 -1
  11. package/Server/Services/DashboardDomainService.ts +131 -0
  12. package/Server/Services/LogPipelineProcessorService.ts +88 -0
  13. package/Server/Services/MonitorTemplateService.ts +70 -11
  14. package/Server/Services/StatusPageDomainService.ts +163 -3
  15. package/Server/Services/StatusPagePrivateUserSessionService.ts +197 -56
  16. package/Server/Types/Domain.ts +17 -0
  17. package/Server/Utils/APIKey/AccessPermission.ts +24 -0
  18. package/Server/Utils/Greenlock/Greenlock.ts +156 -58
  19. package/Server/Utils/LogPipelineProcessorValidation.ts +112 -0
  20. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.ts +1094 -45
  21. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.ts +153 -17
  22. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts +23 -11
  23. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.ts +0 -170
  24. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.ts +158 -0
  25. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +23 -11
  26. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +35 -13
  27. package/Tests/App/Dashboard/MonitorTemplateColumnAndFacet.test.tsx +505 -0
  28. package/Tests/App/StatusPage/StatusPageLoginCodeBootstrap.test.ts +106 -0
  29. package/Tests/App/StatusPage/StatusPageLoginCodeUtil.test.ts +84 -0
  30. package/Tests/App/StatusPage/StatusPageRedirectSafety.test.ts +119 -0
  31. package/Tests/Models/MonitorTemplateMonitorNameColumn.test.ts +258 -0
  32. package/Tests/Server/API/CustomDomainReissueSslAPI.test.ts +469 -0
  33. package/Tests/Server/API/MicrosoftTeamsBotMessagesEndpoint.test.ts +729 -0
  34. package/Tests/Server/API/MicrosoftTeamsBotTestEndpoint.test.ts +26 -0
  35. package/Tests/Server/API/MicrosoftTeamsLegacyWebhookRemoval.test.ts +402 -0
  36. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +4 -0
  37. package/Tests/Server/Services/AddCertificateReissueRequestedAtToDomainsMigration.test.ts +287 -0
  38. package/Tests/Server/Services/CustomDomainCertificateReissue.test.ts +477 -0
  39. package/Tests/Server/Services/LogPipelineProcessorSaveValidation.test.ts +249 -0
  40. package/Tests/Server/Services/MonitorTemplateServiceBulkSync.test.ts +549 -0
  41. package/Tests/Server/Services/MonitorTemplateServiceNetworkDeviceSync.test.ts +16 -3
  42. package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +57 -0
  43. package/Tests/Server/Services/SecurityEventLastErrorColumnWidth.test.ts +151 -15
  44. package/Tests/Server/Services/StatusPagePrivateUserSessionService.test.ts +587 -0
  45. package/Tests/Server/Types/Database/Permissions/ApiKeyFileAccess.test.ts +286 -0
  46. package/Tests/Server/Types/Database/Permissions/MonitorTemplateColumnAccess.test.ts +328 -0
  47. package/Tests/Server/Types/Database/QueryHelperFindWithSameTextAndIsNull.test.ts +83 -0
  48. package/Tests/Server/Utils/AI/SRE/ConfidenceSignal.test.ts +377 -0
  49. package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +16 -1
  50. package/Tests/Server/Utils/Greenlock/CertificateRenewalSchedule.test.ts +250 -0
  51. package/Tests/Server/Utils/LogPipelineProcessorValidation.test.ts +198 -0
  52. package/Tests/Server/Utils/SecurityEvent/ConnectorErrorMessage.test.ts +48 -9
  53. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsAuth.test.ts +726 -0
  54. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsClient.test.ts +74 -11
  55. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsErrorHandling.test.ts +1107 -0
  56. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPoller.test.ts +20 -3
  57. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerFailureTaxonomy.test.ts +51 -11
  58. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerHardening.test.ts +1246 -0
  59. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsRequestContract.test.ts +961 -0
  60. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsResponseParsing.test.ts +653 -0
  61. package/Tests/Server/Utils/Workspace/MicrosoftTeamsActionAuthorization.test.ts +880 -0
  62. package/Tests/Types/API/HTTPResponse.test.ts +175 -0
  63. package/Tests/Types/API/Route.test.ts +36 -4
  64. package/Tests/Types/Database/DatabaseProperty.test.ts +171 -0
  65. package/Tests/Types/DateLocalShortDateTimeString.test.ts +518 -0
  66. package/Tests/Types/DateLocalTimeString.test.ts +259 -0
  67. package/Tests/Types/DateUserPrefers12HourFormat.test.ts +316 -0
  68. package/Tests/UI/Components/LogsHistogram.test.tsx +41 -2
  69. package/Tests/UI/Components/LogsViewerEmptyStateTimeRange.test.tsx +212 -0
  70. package/Tests/UI/Components/TelemetryChartClockFormat.test.tsx +359 -0
  71. package/Tests/UI/Components/TimeRangePickerClockFormat.test.tsx +422 -0
  72. package/Tests/UI/Components/TimeRangePickerDropdown.test.tsx +146 -8
  73. package/Tests/UI/MicrosoftTeams/MicrosoftTeamsMessagingEndpointGuidance.test.tsx +520 -0
  74. package/Tests/UI/Utils/Navigation.test.ts +82 -0
  75. package/Tests/Utils/Array.test.ts +100 -0
  76. package/Tests/Utils/CertificateReissue.test.ts +225 -0
  77. package/Tests/Utils/Grok/Grok.test.ts +492 -0
  78. package/Tests/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.test.ts +181 -13
  79. package/Types/API/Route.ts +20 -5
  80. package/Types/Date.ts +143 -13
  81. package/Types/Rum/SessionReplay.ts +62 -1
  82. package/UI/Components/Date/TimeRangePickerDropdown.tsx +9 -5
  83. package/UI/Components/LogsViewer/LogsViewer.tsx +12 -7
  84. package/UI/Components/LogsViewer/components/HistogramTooltip.tsx +18 -17
  85. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +21 -28
  86. package/UI/Components/LogsViewer/components/LogsHistogram.tsx +2 -4
  87. package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +2 -4
  88. package/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.tsx +17 -16
  89. package/UI/Utils/Navigation.ts +42 -0
  90. package/Utils/Array.ts +37 -0
  91. package/Utils/CertificateReissue.ts +128 -0
  92. package/Utils/Grok/Grok.ts +461 -0
  93. package/Utils/Grok/GrokPatterns.ts +118 -0
  94. package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +58 -6
  95. package/build/dist/Models/DatabaseModels/DashboardDomain.js +44 -0
  96. package/build/dist/Models/DatabaseModels/DashboardDomain.js.map +1 -1
  97. package/build/dist/Models/DatabaseModels/MonitorTemplate.js +17 -4
  98. package/build/dist/Models/DatabaseModels/MonitorTemplate.js.map +1 -1
  99. package/build/dist/Models/DatabaseModels/StatusPageDomain.js +44 -0
  100. package/build/dist/Models/DatabaseModels/StatusPageDomain.js.map +1 -1
  101. package/build/dist/Server/API/DashboardDomainAPI.js +43 -1
  102. package/build/dist/Server/API/DashboardDomainAPI.js.map +1 -1
  103. package/build/dist/Server/API/MicrosoftTeamsAPI.js +74 -118
  104. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  105. package/build/dist/Server/API/StatusPageDomainAPI.js +43 -1
  106. package/build/dist/Server/API/StatusPageDomainAPI.js.map +1 -1
  107. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js +34 -0
  108. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js.map +1 -0
  109. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js +28 -0
  110. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js.map +1 -0
  111. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  112. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  113. package/build/dist/Server/Middleware/ProjectAuthorization.js +6 -1
  114. package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
  115. package/build/dist/Server/Services/DashboardDomainService.js +104 -0
  116. package/build/dist/Server/Services/DashboardDomainService.js.map +1 -1
  117. package/build/dist/Server/Services/LogPipelineProcessorService.js +65 -0
  118. package/build/dist/Server/Services/LogPipelineProcessorService.js.map +1 -1
  119. package/build/dist/Server/Services/MonitorTemplateService.js +57 -11
  120. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  121. package/build/dist/Server/Services/StatusPageDomainService.js +132 -3
  122. package/build/dist/Server/Services/StatusPageDomainService.js.map +1 -1
  123. package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js +112 -32
  124. package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js.map +1 -1
  125. package/build/dist/Server/Types/Domain.js +16 -0
  126. package/build/dist/Server/Types/Domain.js.map +1 -1
  127. package/build/dist/Server/Utils/APIKey/AccessPermission.js +24 -0
  128. package/build/dist/Server/Utils/APIKey/AccessPermission.js.map +1 -1
  129. package/build/dist/Server/Utils/Greenlock/Greenlock.js +110 -35
  130. package/build/dist/Server/Utils/Greenlock/Greenlock.js.map +1 -1
  131. package/build/dist/Server/Utils/LogPipelineProcessorValidation.js +77 -0
  132. package/build/dist/Server/Utils/LogPipelineProcessorValidation.js.map +1 -0
  133. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js +777 -36
  134. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js.map +1 -1
  135. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js +108 -17
  136. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js.map +1 -1
  137. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js +17 -6
  138. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js.map +1 -1
  139. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js +0 -159
  140. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js.map +1 -1
  141. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js +128 -0
  142. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js.map +1 -0
  143. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +17 -6
  144. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
  145. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +25 -13
  146. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  147. package/build/dist/Types/API/Route.js +16 -5
  148. package/build/dist/Types/API/Route.js.map +1 -1
  149. package/build/dist/Types/Date.js +115 -13
  150. package/build/dist/Types/Date.js.map +1 -1
  151. package/build/dist/Types/Rum/SessionReplay.js +38 -0
  152. package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
  153. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js +9 -5
  154. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js.map +1 -1
  155. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +12 -7
  156. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  157. package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js +15 -14
  158. package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js.map +1 -1
  159. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +17 -22
  160. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  161. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +2 -4
  162. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
  163. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +2 -4
  164. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
  165. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js +15 -14
  166. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js.map +1 -1
  167. package/build/dist/UI/Utils/Navigation.js +30 -0
  168. package/build/dist/UI/Utils/Navigation.js.map +1 -1
  169. package/build/dist/Utils/Array.js +25 -0
  170. package/build/dist/Utils/Array.js.map +1 -1
  171. package/build/dist/Utils/CertificateReissue.js +97 -0
  172. package/build/dist/Utils/CertificateReissue.js.map +1 -0
  173. package/build/dist/Utils/Grok/Grok.js +324 -0
  174. package/build/dist/Utils/Grok/Grok.js.map +1 -0
  175. package/build/dist/Utils/Grok/GrokPatterns.js +112 -0
  176. package/build/dist/Utils/Grok/GrokPatterns.js.map +1 -0
  177. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +63 -3
  178. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -1
  179. package/package.json +1 -1
  180. package/UI/Utils/JsonWebToken.ts +0 -14
  181. package/build/dist/UI/Utils/JsonWebToken.js +0 -10
  182. package/build/dist/UI/Utils/JsonWebToken.js.map +0 -1
@@ -0,0 +1,377 @@
1
+ import AIConfidenceSignal, {
2
+ ConfidenceClassificationToken,
3
+ ConfidenceSignal,
4
+ ConfidenceSource,
5
+ EvidenceInput,
6
+ } from "../../../../../Server/Utils/AI/SRE/ConfidenceSignal";
7
+ import { AIChatCitation } from "../../../../../Types/AI/AIChatTypes";
8
+ import { describe, expect, test } from "@jest/globals";
9
+
10
+ /*
11
+ * AI SRE confidence signal — the pure, LLM-free half of the module (the
12
+ * deterministic evidence floor, the defensive token parser, and the
13
+ * per-consumer decision helpers). These encode the safety-critical fail
14
+ * directions from the module header's decision table:
15
+ *
16
+ * | source / verdict | ping | instrumentation | auto code fix |
17
+ * |-------------------------------|------|-----------------|---------------|
18
+ * | deterministic-floor | no | yes | no |
19
+ * | classification / CODE_FIX | yes | no | yes |
20
+ * | classification / NO_CODE_FIX | yes | no | no |
21
+ * | classification / INCONCLUSIVE | no | yes | no |
22
+ * | classification-failed | YES | no | no |
23
+ *
24
+ * The two load-bearing invariants these pin: (1) autonomous PR creation
25
+ * (instrumentation + auto code fix) fails toward doing nothing on a broken
26
+ * classifier, and (2) the on-call ping fails LOUDER — a broken classifier must
27
+ * never suppress it. Getting either direction backwards is the failure the
28
+ * threat model (vision §6) is written against, so it must not regress silently.
29
+ */
30
+
31
+ function citation(rowCount: number): AIChatCitation {
32
+ return {
33
+ id: "C1",
34
+ toolName: "query_logs",
35
+ label: "logs",
36
+ queryArguments: {},
37
+ rowCount,
38
+ };
39
+ }
40
+
41
+ function signal(
42
+ source: ConfidenceSource,
43
+ confident: boolean,
44
+ codeFixRecommended?: boolean,
45
+ ): ConfidenceSignal {
46
+ return { source, confident, codeFixRecommended };
47
+ }
48
+
49
+ describe("AIConfidenceSignal.isDataBearingRowCount", () => {
50
+ test("a positive row count is data-bearing", () => {
51
+ expect(AIConfidenceSignal.isDataBearingRowCount(1)).toBe(true);
52
+ expect(AIConfidenceSignal.isDataBearingRowCount(1000)).toBe(true);
53
+ });
54
+
55
+ test("zero rows is server-verified but NOT data-bearing (proof of absence)", () => {
56
+ expect(AIConfidenceSignal.isDataBearingRowCount(0)).toBe(false);
57
+ });
58
+
59
+ test("null / undefined coalesce to zero → not data-bearing", () => {
60
+ expect(AIConfidenceSignal.isDataBearingRowCount(null)).toBe(false);
61
+ expect(AIConfidenceSignal.isDataBearingRowCount(undefined)).toBe(false);
62
+ });
63
+
64
+ test("a negative row count is not data-bearing", () => {
65
+ expect(AIConfidenceSignal.isDataBearingRowCount(-5)).toBe(false);
66
+ });
67
+ });
68
+
69
+ describe("AIConfidenceSignal.evidenceFromCitations", () => {
70
+ test("counts every citation but only data-bearing ones as data-bearing", () => {
71
+ const evidence: EvidenceInput = AIConfidenceSignal.evidenceFromCitations([
72
+ citation(3),
73
+ citation(0),
74
+ citation(7),
75
+ ]);
76
+
77
+ expect(evidence.citationCount).toBe(3);
78
+ expect(evidence.dataBearingToolCallCount).toBe(2);
79
+ expect(evidence.anyToolReturnedData).toBe(true);
80
+ });
81
+
82
+ test("citations that all returned zero rows count but bear no data", () => {
83
+ const evidence: EvidenceInput = AIConfidenceSignal.evidenceFromCitations([
84
+ citation(0),
85
+ citation(0),
86
+ ]);
87
+
88
+ expect(evidence.citationCount).toBe(2);
89
+ expect(evidence.dataBearingToolCallCount).toBe(0);
90
+ expect(evidence.anyToolReturnedData).toBe(false);
91
+ });
92
+
93
+ test("no citations → an all-zero evidence input", () => {
94
+ const evidence: EvidenceInput = AIConfidenceSignal.evidenceFromCitations(
95
+ [],
96
+ );
97
+
98
+ expect(evidence.citationCount).toBe(0);
99
+ expect(evidence.dataBearingToolCallCount).toBe(0);
100
+ expect(evidence.anyToolReturnedData).toBe(false);
101
+ });
102
+ });
103
+
104
+ describe("AIConfidenceSignal.hasVerifiableEvidence", () => {
105
+ test("true when any of the three signals is present", () => {
106
+ expect(
107
+ AIConfidenceSignal.hasVerifiableEvidence({
108
+ citationCount: 1,
109
+ dataBearingToolCallCount: 0,
110
+ anyToolReturnedData: false,
111
+ }),
112
+ ).toBe(true);
113
+
114
+ expect(
115
+ AIConfidenceSignal.hasVerifiableEvidence({
116
+ citationCount: 0,
117
+ dataBearingToolCallCount: 2,
118
+ anyToolReturnedData: false,
119
+ }),
120
+ ).toBe(true);
121
+
122
+ expect(
123
+ AIConfidenceSignal.hasVerifiableEvidence({
124
+ citationCount: 0,
125
+ dataBearingToolCallCount: 0,
126
+ anyToolReturnedData: true,
127
+ }),
128
+ ).toBe(true);
129
+ });
130
+
131
+ test("false only when the run minted nothing at all", () => {
132
+ expect(
133
+ AIConfidenceSignal.hasVerifiableEvidence({
134
+ citationCount: 0,
135
+ dataBearingToolCallCount: 0,
136
+ anyToolReturnedData: false,
137
+ }),
138
+ ).toBe(false);
139
+ });
140
+
141
+ test("a citation with zero data-bearing rows still passes the floor", () => {
142
+ // A tool that ran and proved absence is server-verified evidence.
143
+ const evidence: EvidenceInput = AIConfidenceSignal.evidenceFromCitations([
144
+ citation(0),
145
+ ]);
146
+ expect(AIConfidenceSignal.hasVerifiableEvidence(evidence)).toBe(true);
147
+ });
148
+ });
149
+
150
+ describe("AIConfidenceSignal.parseClassificationToken", () => {
151
+ test("recognises each of the three verdicts when it is the only one", () => {
152
+ expect(AIConfidenceSignal.parseClassificationToken("CODE_FIX")).toBe(
153
+ "CODE_FIX",
154
+ );
155
+ expect(AIConfidenceSignal.parseClassificationToken("NO_CODE_FIX")).toBe(
156
+ "NO_CODE_FIX",
157
+ );
158
+ expect(AIConfidenceSignal.parseClassificationToken("INCONCLUSIVE")).toBe(
159
+ "INCONCLUSIVE",
160
+ );
161
+ });
162
+
163
+ test("is case-insensitive", () => {
164
+ expect(AIConfidenceSignal.parseClassificationToken("code_fix")).toBe(
165
+ "CODE_FIX",
166
+ );
167
+ expect(AIConfidenceSignal.parseClassificationToken("Inconclusive")).toBe(
168
+ "INCONCLUSIVE",
169
+ );
170
+ });
171
+
172
+ test("tolerates editorializing prose around a single verdict", () => {
173
+ expect(
174
+ AIConfidenceSignal.parseClassificationToken(
175
+ "The verdict is CODE_FIX because the null check is missing.",
176
+ ),
177
+ ).toBe("CODE_FIX");
178
+ });
179
+
180
+ test("NO_CODE_FIX is not mis-read as CODE_FIX (underscore is a word char)", () => {
181
+ // Only NO_CODE_FIX appears as a whole word; CODE_FIX must not also match.
182
+ expect(AIConfidenceSignal.parseClassificationToken("NO_CODE_FIX")).toBe(
183
+ "NO_CODE_FIX",
184
+ );
185
+ });
186
+
187
+ test("multiple distinct verdicts → null (unparseable)", () => {
188
+ expect(
189
+ AIConfidenceSignal.parseClassificationToken("CODE_FIX or INCONCLUSIVE"),
190
+ ).toBeNull();
191
+ });
192
+
193
+ test("no verdict token → null", () => {
194
+ expect(
195
+ AIConfidenceSignal.parseClassificationToken("I am not sure what to say."),
196
+ ).toBeNull();
197
+ });
198
+
199
+ test("empty / null / undefined → null", () => {
200
+ expect(AIConfidenceSignal.parseClassificationToken("")).toBeNull();
201
+ expect(AIConfidenceSignal.parseClassificationToken(null)).toBeNull();
202
+ expect(AIConfidenceSignal.parseClassificationToken(undefined)).toBeNull();
203
+ });
204
+
205
+ test("word boundaries required — a substring inside another word does not match", () => {
206
+ // "PRECODE_FIXED" contains CODE_FIX as a substring but not as a word.
207
+ expect(
208
+ AIConfidenceSignal.parseClassificationToken("PRECODE_FIXED"),
209
+ ).toBeNull();
210
+ });
211
+ });
212
+
213
+ describe("AIConfidenceSignal.shouldSendWorkspaceNotification", () => {
214
+ test("classification-failed pings LOUDER regardless of the confident flag", () => {
215
+ expect(
216
+ AIConfidenceSignal.shouldSendWorkspaceNotification(
217
+ signal("classification-failed", false),
218
+ ),
219
+ ).toBe(true);
220
+ expect(
221
+ AIConfidenceSignal.shouldSendWorkspaceNotification(
222
+ signal("classification-failed", true),
223
+ ),
224
+ ).toBe(true);
225
+ });
226
+
227
+ test("otherwise it follows the confident flag", () => {
228
+ expect(
229
+ AIConfidenceSignal.shouldSendWorkspaceNotification(
230
+ signal("classification", true, true),
231
+ ),
232
+ ).toBe(true);
233
+ expect(
234
+ AIConfidenceSignal.shouldSendWorkspaceNotification(
235
+ signal("deterministic-floor", false, false),
236
+ ),
237
+ ).toBe(false);
238
+ });
239
+ });
240
+
241
+ describe("AIConfidenceSignal.shouldEnqueueInstrumentationTask", () => {
242
+ test("classification-failed fails toward doing nothing", () => {
243
+ expect(
244
+ AIConfidenceSignal.shouldEnqueueInstrumentationTask(
245
+ signal("classification-failed", false),
246
+ ),
247
+ ).toBe(false);
248
+ });
249
+
250
+ test("enqueues on a positive inconclusive verdict (floor or classifier)", () => {
251
+ expect(
252
+ AIConfidenceSignal.shouldEnqueueInstrumentationTask(
253
+ signal("deterministic-floor", false, false),
254
+ ),
255
+ ).toBe(true);
256
+ expect(
257
+ AIConfidenceSignal.shouldEnqueueInstrumentationTask(
258
+ signal("classification", false, false),
259
+ ),
260
+ ).toBe(true);
261
+ });
262
+
263
+ test("does not enqueue when the run was confident", () => {
264
+ expect(
265
+ AIConfidenceSignal.shouldEnqueueInstrumentationTask(
266
+ signal("classification", true, true),
267
+ ),
268
+ ).toBe(false);
269
+ });
270
+ });
271
+
272
+ describe("AIConfidenceSignal.isCodeFixRecommended / shouldAutoEnqueueCodeFixTask", () => {
273
+ test("requires classification source, confident, AND codeFixRecommended", () => {
274
+ const positive: ConfidenceSignal = signal("classification", true, true);
275
+ expect(AIConfidenceSignal.isCodeFixRecommended(positive)).toBe(true);
276
+ expect(AIConfidenceSignal.shouldAutoEnqueueCodeFixTask(positive)).toBe(
277
+ true,
278
+ );
279
+ });
280
+
281
+ test("NO_CODE_FIX (confident but not recommended) is insufficient", () => {
282
+ const noFix: ConfidenceSignal = signal("classification", true, false);
283
+ expect(AIConfidenceSignal.isCodeFixRecommended(noFix)).toBe(false);
284
+ expect(AIConfidenceSignal.shouldAutoEnqueueCodeFixTask(noFix)).toBe(false);
285
+ });
286
+
287
+ test("the deterministic floor never triggers an auto code fix", () => {
288
+ // Even if some caller mislabeled the floor as recommended, source gates it.
289
+ const floor: ConfidenceSignal = signal("deterministic-floor", false, true);
290
+ expect(AIConfidenceSignal.isCodeFixRecommended(floor)).toBe(false);
291
+ });
292
+
293
+ test("classification-failed never triggers an auto code fix", () => {
294
+ const failed: ConfidenceSignal = signal(
295
+ "classification-failed",
296
+ true,
297
+ true,
298
+ );
299
+ expect(AIConfidenceSignal.isCodeFixRecommended(failed)).toBe(false);
300
+ });
301
+
302
+ test("an absent (legacy) codeFixRecommended fails closed", () => {
303
+ const legacy: ConfidenceSignal = {
304
+ source: "classification",
305
+ confident: true,
306
+ };
307
+ expect(AIConfidenceSignal.isCodeFixRecommended(legacy)).toBe(false);
308
+ });
309
+ });
310
+
311
+ describe("AIConfidenceSignal decision table (cross-consumer)", () => {
312
+ interface Row {
313
+ signal: ConfidenceSignal;
314
+ ping: boolean;
315
+ instrumentation: boolean;
316
+ autoCodeFix: boolean;
317
+ }
318
+
319
+ const rows: Array<Row> = [
320
+ {
321
+ signal: signal("deterministic-floor", false, false),
322
+ ping: false,
323
+ instrumentation: true,
324
+ autoCodeFix: false,
325
+ },
326
+ {
327
+ signal: signal("classification", true, true),
328
+ ping: true,
329
+ instrumentation: false,
330
+ autoCodeFix: true,
331
+ },
332
+ {
333
+ signal: signal("classification", true, false),
334
+ ping: true,
335
+ instrumentation: false,
336
+ autoCodeFix: false,
337
+ },
338
+ {
339
+ signal: signal("classification", false, false),
340
+ ping: false,
341
+ instrumentation: true,
342
+ autoCodeFix: false,
343
+ },
344
+ {
345
+ signal: signal("classification-failed", false),
346
+ ping: true,
347
+ instrumentation: false,
348
+ autoCodeFix: false,
349
+ },
350
+ ];
351
+
352
+ test("every documented row maps to the three consumer decisions", () => {
353
+ for (const row of rows) {
354
+ const context: ConfidenceSource = row.signal.source;
355
+
356
+ expect({
357
+ context,
358
+ ping: AIConfidenceSignal.shouldSendWorkspaceNotification(row.signal),
359
+ instrumentation: AIConfidenceSignal.shouldEnqueueInstrumentationTask(
360
+ row.signal,
361
+ ),
362
+ autoCodeFix: AIConfidenceSignal.shouldAutoEnqueueCodeFixTask(
363
+ row.signal,
364
+ ),
365
+ }).toEqual({
366
+ context,
367
+ ping: row.ping,
368
+ instrumentation: row.instrumentation,
369
+ autoCodeFix: row.autoCodeFix,
370
+ });
371
+ }
372
+ });
373
+ });
374
+
375
+ // Keep the imported token type referenced for readers of this spec.
376
+ const _tokenType: ConfidenceClassificationToken = "CODE_FIX";
377
+ void _tokenType;
@@ -41,6 +41,15 @@ import { getJestSpyOn } from "../../../Spy";
41
41
  * key full control of the project; one that dropped it from the master path
42
42
  * would break trusted internal automation. Both directions are pinned below.
43
43
  *
44
+ * AuthenticatedRequest sits in the same list but is not a capability in that
45
+ * sense. It is the marker for "some authenticated principal made this call" -
46
+ * it has no PermissionProps, never appears in the admin picker, and no
47
+ * administrator can grant or revoke it (see PermissionCatalogueCoverage.test).
48
+ * Only File/FileModel name it, so what it widens is bounded by those two
49
+ * models. It is here so an API key is not mistaken for a bare logged-in user
50
+ * by TenantPermission.isAccessGrantedOnlyByCurrentUser - see
51
+ * ApiKeyFileAccess.test for the behaviour that depends on it.
52
+ *
44
53
  * getApiTenantAccessPermission also reshapes the (cached) permission rows into
45
54
  * the runtime UserPermission shape (labelIds carried over, block flag
46
55
  * preserved). That mapping is where a dropped label would quietly widen a
@@ -62,7 +71,7 @@ const permissionValues: (
62
71
  };
63
72
 
64
73
  describe("APIKeyAccessPermission.getDefaultApiGlobalPermission", () => {
65
- it("grants Public, User and CurrentUser for the project", async () => {
74
+ it("grants Public, User, CurrentUser and AuthenticatedRequest for the project", async () => {
66
75
  const perm: UserGlobalAccessPermission =
67
76
  await APIKeyAccessPermission.getDefaultApiGlobalPermission(projectId);
68
77
 
@@ -72,10 +81,16 @@ describe("APIKeyAccessPermission.getDefaultApiGlobalPermission", () => {
72
81
  return p.toString();
73
82
  }),
74
83
  ).toEqual([projectId.toString()]);
84
+ /*
85
+ * Exact equality rather than four toContain calls: this list IS the
86
+ * privilege boundary, so an addition has to be argued for here rather than
87
+ * slipping in unnoticed.
88
+ */
75
89
  expect(perm.globalPermissions).toEqual([
76
90
  Permission.Public,
77
91
  Permission.User,
78
92
  Permission.CurrentUser,
93
+ Permission.AuthenticatedRequest,
79
94
  ]);
80
95
  });
81
96
 
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Regression tests for status page certificate renewal scheduling.
3
+ *
4
+ * These pin the two properties whose absence let customer status pages serve
5
+ * expired certificates:
6
+ *
7
+ * - Renewal must be PROACTIVE. A run picks up a certificate weeks before it
8
+ * expires. Previously renewal ran once a day over every certificate inside a
9
+ * fixed window, strictly one domain at a time; once the fleet outgrew a
10
+ * single run the queue stopped draining, and certificates were reordered only
11
+ * after they had already expired and the provisioning sweep noticed the page
12
+ * was serving a dead certificate.
13
+ *
14
+ * - Renewal must be SPREAD OUT. Let's Encrypt issues for 90 days, so domains
15
+ * ordered together expire together. With one fixed lead time that batch stays
16
+ * together forever and one bad run expires all of it at once. The lead time
17
+ * is jittered per domain so batches disperse.
18
+ *
19
+ * A run is also capped, because the CA rate-limits new orders per account and
20
+ * the reactive sweep spends from the same allowance.
21
+ *
22
+ * AcmeCertificateService.findBy is spied on, so no database is touched, and
23
+ * orderCert is spied on so no ACME order is ever attempted.
24
+ */
25
+
26
+ import GreenlockUtil from "../../../../Server/Utils/Greenlock/Greenlock";
27
+ import AcmeCertificateService from "../../../../Server/Services/AcmeCertificateService";
28
+ import AcmeCertificate from "../../../../Models/DatabaseModels/AcmeCertificate";
29
+ import OneUptimeDate from "../../../../Types/Date";
30
+ import { afterEach, describe, expect, test, jest } from "@jest/globals";
31
+
32
+ function certificateExpiringInDays(
33
+ domain: string,
34
+ days: number,
35
+ ): AcmeCertificate {
36
+ return {
37
+ domain: domain,
38
+ expiresAt: OneUptimeDate.addRemoveDays(
39
+ OneUptimeDate.getCurrentDate(),
40
+ days,
41
+ ),
42
+ } as unknown as AcmeCertificate;
43
+ }
44
+
45
+ /*
46
+ * Drive a renewal run over a fixed set of certificates and report which domains
47
+ * it actually tried to order. The CNAME check is stubbed valid so the run
48
+ * reaches the order step for every domain it selects.
49
+ */
50
+ async function domainsRenewedFor(
51
+ certificates: Array<AcmeCertificate>,
52
+ ): Promise<Array<string>> {
53
+ const ordered: Array<string> = [];
54
+
55
+ jest
56
+ .spyOn(AcmeCertificateService, "findBy")
57
+ .mockResolvedValue(certificates as never);
58
+
59
+ jest
60
+ .spyOn(GreenlockUtil, "orderCert")
61
+ .mockImplementation(async (data: { domain: string }): Promise<void> => {
62
+ ordered.push(data.domain);
63
+ });
64
+
65
+ await GreenlockUtil.renewAllCertsWhichAreExpiringSoon({
66
+ validateCname: async (): Promise<boolean> => {
67
+ return true;
68
+ },
69
+ notifyDomainRemoved: async (): Promise<void> => {
70
+ return undefined;
71
+ },
72
+ });
73
+
74
+ return ordered;
75
+ }
76
+
77
+ describe("Certificate renewal lead time", () => {
78
+ test("every domain renews well before it expires", () => {
79
+ const domains: Array<string> = [
80
+ "status.aleyant.com",
81
+ "status.example.com",
82
+ "a.status.syniti.com",
83
+ "b.status.avsw.io",
84
+ "x",
85
+ "",
86
+ ];
87
+
88
+ for (const domain of domains) {
89
+ const leadTime: number = GreenlockUtil.getRenewalLeadTimeInDays(domain);
90
+
91
+ expect(leadTime).toBeGreaterThanOrEqual(
92
+ GreenlockUtil.RENEW_LEAD_TIME_MIN_IN_DAYS,
93
+ );
94
+ expect(leadTime).toBeLessThanOrEqual(
95
+ GreenlockUtil.RENEW_LEAD_TIME_MAX_IN_DAYS,
96
+ );
97
+ }
98
+ });
99
+
100
+ test("the shortest possible lead time still leaves days of margin", () => {
101
+ /*
102
+ * The whole point is that renewal happens before expiry, not at it. Even
103
+ * the least eager domain must have room for several failed runs.
104
+ */
105
+ expect(GreenlockUtil.RENEW_LEAD_TIME_MIN_IN_DAYS).toBeGreaterThanOrEqual(7);
106
+ });
107
+
108
+ test("a domain's lead time is stable across calls", () => {
109
+ const first: number =
110
+ GreenlockUtil.getRenewalLeadTimeInDays("status.aleyant.com");
111
+ const second: number =
112
+ GreenlockUtil.getRenewalLeadTimeInDays("status.aleyant.com");
113
+
114
+ expect(first).toBe(second);
115
+ });
116
+
117
+ test("domains expiring on the same day do not all come due on the same day", () => {
118
+ /*
119
+ * The thundering herd: a batch ordered together must not renew together.
120
+ */
121
+ const leadTimes: Set<number> = new Set<number>();
122
+
123
+ for (let i: number = 0; i < 200; i++) {
124
+ leadTimes.add(
125
+ GreenlockUtil.getRenewalLeadTimeInDays(`status${i}.example.com`),
126
+ );
127
+ }
128
+
129
+ expect(leadTimes.size).toBeGreaterThan(1);
130
+ });
131
+ });
132
+
133
+ describe("Certificate renewal run", () => {
134
+ afterEach(() => {
135
+ jest.restoreAllMocks();
136
+ });
137
+
138
+ test("renews a certificate that is close to expiry", async () => {
139
+ const domain: string = "status.aleyant.com";
140
+
141
+ // one day left: due under any lead time in the range.
142
+ const renewed: Array<string> = await domainsRenewedFor([
143
+ certificateExpiringInDays(domain, 1),
144
+ ]);
145
+
146
+ expect(renewed).toEqual([domain]);
147
+ });
148
+
149
+ test("renews before expiry rather than after it", async () => {
150
+ /*
151
+ * This is the regression. A certificate with three weeks of life left is
152
+ * inside every domain's lead time, so a run must pick it up now - not wait
153
+ * until it has expired and the page is already broken.
154
+ */
155
+ const domain: string = "status.aleyant.com";
156
+
157
+ const renewed: Array<string> = await domainsRenewedFor([
158
+ certificateExpiringInDays(domain, 21),
159
+ ]);
160
+
161
+ expect(renewed).toEqual([domain]);
162
+ });
163
+
164
+ test("leaves a freshly issued certificate alone", async () => {
165
+ const renewed: Array<string> = await domainsRenewedFor([
166
+ certificateExpiringInDays("status.aleyant.com", 89),
167
+ ]);
168
+
169
+ expect(renewed).toEqual([]);
170
+ });
171
+
172
+ test("caps how many certificates one run orders", async () => {
173
+ /*
174
+ * A large backlog must not be dispatched as a single burst - the CA
175
+ * rate-limits new orders per account, and orders refused for rate limiting
176
+ * are renewals that did not happen.
177
+ */
178
+ const certificates: Array<AcmeCertificate> = Array.from(
179
+ { length: GreenlockUtil.RENEW_MAX_PER_RUN + 25 },
180
+ (_v: unknown, i: number) => {
181
+ return certificateExpiringInDays(`status${i}.example.com`, 1);
182
+ },
183
+ );
184
+
185
+ const renewed: Array<string> = await domainsRenewedFor(certificates);
186
+
187
+ expect(renewed).toHaveLength(GreenlockUtil.RENEW_MAX_PER_RUN);
188
+ });
189
+
190
+ test("spends a capped run on the certificates closest to expiring", async () => {
191
+ /*
192
+ * findBy returns rows sorted by expiry, and the cap is applied after the
193
+ * due filter - so the domains at the edge of expiry must be the ones a
194
+ * short run pays for, never the ones with weeks left.
195
+ */
196
+ const urgent: Array<AcmeCertificate> = Array.from(
197
+ { length: GreenlockUtil.RENEW_MAX_PER_RUN },
198
+ (_v: unknown, i: number) => {
199
+ return certificateExpiringInDays(`urgent${i}.example.com`, 1);
200
+ },
201
+ );
202
+
203
+ const lessUrgent: Array<AcmeCertificate> = Array.from(
204
+ { length: 10 },
205
+ (_v: unknown, i: number) => {
206
+ return certificateExpiringInDays(`later${i}.example.com`, 24);
207
+ },
208
+ );
209
+
210
+ const renewed: Array<string> = await domainsRenewedFor([
211
+ ...urgent,
212
+ ...lessUrgent,
213
+ ]);
214
+
215
+ for (const domain of renewed) {
216
+ expect(domain.startsWith("urgent")).toBe(true);
217
+ }
218
+ });
219
+
220
+ test("one domain that cannot be renewed does not abandon the rest of the run", async () => {
221
+ const ordered: Array<string> = [];
222
+
223
+ jest
224
+ .spyOn(AcmeCertificateService, "findBy")
225
+ .mockResolvedValue([
226
+ certificateExpiringInDays("broken.example.com", 1),
227
+ certificateExpiringInDays("healthy.example.com", 1),
228
+ ] as never);
229
+
230
+ jest
231
+ .spyOn(GreenlockUtil, "orderCert")
232
+ .mockImplementation(async (data: { domain: string }): Promise<void> => {
233
+ if (data.domain === "broken.example.com") {
234
+ throw new Error("CA refused the order");
235
+ }
236
+ ordered.push(data.domain);
237
+ });
238
+
239
+ await GreenlockUtil.renewAllCertsWhichAreExpiringSoon({
240
+ validateCname: async (): Promise<boolean> => {
241
+ return true;
242
+ },
243
+ notifyDomainRemoved: async (): Promise<void> => {
244
+ return undefined;
245
+ },
246
+ });
247
+
248
+ expect(ordered).toContain("healthy.example.com");
249
+ });
250
+ });