@oneuptime/common 11.7.1 → 11.7.3

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 (285) hide show
  1. package/Models/AnalyticsModels/KubernetesCostAllocation.ts +103 -0
  2. package/Models/DatabaseModels/DashboardDomain.ts +1 -0
  3. package/Models/DatabaseModels/Incident.ts +2 -0
  4. package/Models/DatabaseModels/IncidentEpisode.ts +1 -0
  5. package/Models/DatabaseModels/IncidentEpisodePublicNote.ts +1 -0
  6. package/Models/DatabaseModels/IncidentEpisodeStateTimeline.ts +1 -0
  7. package/Models/DatabaseModels/IncidentPublicNote.ts +1 -0
  8. package/Models/DatabaseModels/IncidentStateTimeline.ts +1 -0
  9. package/Models/DatabaseModels/Monitor.ts +1 -0
  10. package/Models/DatabaseModels/MonitorStatusTimeline.ts +1 -0
  11. package/Models/DatabaseModels/NetworkSite.ts +1 -0
  12. package/Models/DatabaseModels/NetworkSiteStatusTimeline.ts +1 -0
  13. package/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.ts +1 -0
  14. package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +2 -0
  15. package/Models/DatabaseModels/Probe.ts +13 -13
  16. package/Models/DatabaseModels/ScheduledMaintenance.ts +1 -0
  17. package/Models/DatabaseModels/ScheduledMaintenancePublicNote.ts +1 -0
  18. package/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.ts +1 -0
  19. package/Models/DatabaseModels/ScheduledMaintenanceTemplate.ts +1 -0
  20. package/Models/DatabaseModels/StatusPage.ts +1 -0
  21. package/Models/DatabaseModels/StatusPageAnnouncement.ts +1 -0
  22. package/Models/DatabaseModels/StatusPageDomain.ts +1 -0
  23. package/Models/DatabaseModels/TableView.ts +40 -0
  24. package/Models/DatabaseModels/WorkspaceNotificationSummary.ts +1 -0
  25. package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +11 -0
  26. package/Server/API/BaseAPI.ts +17 -1
  27. package/Server/API/CommonAPI.ts +32 -0
  28. package/Server/API/MicrosoftTeamsAPI.ts +99 -2
  29. package/Server/API/ProbeAPI.ts +2 -0
  30. package/Server/API/SlackAPI.ts +62 -0
  31. package/Server/API/StatusPageAPI.ts +157 -192
  32. package/Server/Infrastructure/Postgres/SchemaMigrations/1785140242697-AddHotQueryIndexes.ts +89 -0
  33. package/Server/Infrastructure/Postgres/SchemaMigrations/1785148065137-AddHotQueryIndexesSecondPass.ts +61 -0
  34. package/Server/Infrastructure/Postgres/SchemaMigrations/1785240000000-RepairCrossProjectMonitorStatusReferences.ts +280 -0
  35. package/Server/Infrastructure/Postgres/SchemaMigrations/1785241000000-AddColumnsToTableView.ts +13 -0
  36. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  37. package/Server/Services/DatabaseService.ts +11 -3
  38. package/Server/Services/MonitorGroupService.ts +145 -0
  39. package/Server/Services/MonitorService.ts +231 -30
  40. package/Server/Services/ProbeService.ts +319 -16
  41. package/Server/Services/StatusPageService.ts +72 -42
  42. package/Server/Services/WorkspaceNotificationRuleService.ts +232 -2
  43. package/Server/Utils/Database/PostgresErrorTranslator.ts +131 -0
  44. package/Server/Utils/Monitor/MonitorResource.ts +88 -35
  45. package/Server/Utils/Monitor/MonitorStatusTimeline.ts +21 -1
  46. package/Server/Utils/Monitor/MonitorStepsProjectValidator.ts +158 -0
  47. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +706 -6
  48. package/Tests/Models/AnalyticsModels/KubernetesCostAllocation.test.ts +16 -0
  49. package/Tests/Models/DatabaseModels/ProbeColumnAccessControl.test.ts +103 -0
  50. package/Tests/Server/API/BaseAPIUpdateNoOp.test.ts +135 -0
  51. package/Tests/Server/API/CommonAPIAuthGuard.test.ts +317 -0
  52. package/Tests/Server/API/MicrosoftTeamsManifest.test.ts +228 -0
  53. package/Tests/Server/API/ProbeAPI.test.ts +6 -0
  54. package/Tests/Server/API/ZZVerifyChatsAuth.test.ts +167 -0
  55. package/Tests/Server/Services/AddHotQueryIndexesMigration.test.ts +255 -0
  56. package/Tests/Server/Services/AddHotQueryIndexesSecondPassMigration.test.ts +258 -0
  57. package/Tests/Server/Services/DatabaseServiceSanitizeUpdateData.test.ts +1 -1
  58. package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +121 -0
  59. package/Tests/Server/Services/HeartbeatWriteFastPathSafety.test.ts +200 -0
  60. package/Tests/Server/Services/HookFreeWriteSafetySecondPass.test.ts +181 -0
  61. package/Tests/Server/Services/MonitorGroupServiceBatchedStatuses.test.ts +513 -0
  62. package/Tests/Server/Services/MonitorResourceProbeAgreementReuse.test.ts +492 -0
  63. package/Tests/Server/Services/MonitorServiceProbeSelection.test.ts +401 -0
  64. package/Tests/Server/Services/MonitorStatusTimelineFastPath.test.ts +453 -0
  65. package/Tests/Server/Services/ProbeAuthKeyCacheAndHeartbeat.test.ts +679 -0
  66. package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +3 -3
  67. package/Tests/Server/Services/StatusPageDomainResolutionCache.test.ts +245 -0
  68. package/Tests/Server/Services/StatusPageOverviewTimelineWindow.test.ts +283 -0
  69. package/Tests/Server/Services/StatusPageServiceMcp.test.ts +17 -1
  70. package/Tests/Server/Services/StatusPageServiceMonitorGroupBatching.test.ts +331 -0
  71. package/Tests/Server/Services/UserOnCallLogClaimNotificationRule.test.ts +12 -12
  72. package/Tests/Server/Services/WorkspaceNotificationRuleChats.test.ts +1390 -0
  73. package/Tests/Server/Utils/AI/Insights/InsightFixRouting.test.ts +4 -4
  74. package/Tests/Server/Utils/AI/Insights/InsightScanner.test.ts +2 -2
  75. package/Tests/Server/Utils/AI/Insights/InsightStore.test.ts +1 -3
  76. package/Tests/Server/Utils/AI/Insights/InsightStoreHardening.test.ts +1 -3
  77. package/Tests/Server/Utils/AI/Insights/InsightTriage.test.ts +1 -1
  78. package/Tests/Server/Utils/AI/Insights/InsightTriageRunner.test.ts +5 -5
  79. package/Tests/Server/Utils/Alert/AlertPrivacyFilter.test.ts +187 -0
  80. package/Tests/Server/Utils/AlertEpisode/AlertEpisodePrivacyFilter.test.ts +170 -0
  81. package/Tests/Server/Utils/Database/PostgresErrorTranslator.test.ts +125 -0
  82. package/Tests/Server/Utils/Incident/IncidentPrivacyFilter.test.ts +264 -0
  83. package/Tests/Server/Utils/IncidentEpisode/IncidentEpisodePrivacyFilter.test.ts +175 -0
  84. package/Tests/Server/Utils/Monitor/MonitorStepsProjectValidator.test.ts +181 -0
  85. package/Tests/Server/Utils/Monitor/NetworkDeviceWalkUtil.test.ts +1 -1
  86. package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +2 -2
  87. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChannelsList.test.ts +341 -0
  88. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChats.test.ts +2175 -0
  89. package/Tests/Server/Utils/Workspace/SlackChannelsList.test.ts +585 -0
  90. package/Tests/Types/Billing/MeteredPlan.test.ts +46 -0
  91. package/Tests/Types/JSONFunctions.test.ts +103 -1
  92. package/Tests/Types/Workspace/NotificationRules/NotificationRuleCondition.test.ts +219 -1
  93. package/Tests/UI/Components/BulkUpdateForm.test.tsx +174 -0
  94. package/Tests/UI/Components/CardModelDetailEdit.test.tsx +315 -0
  95. package/Tests/UI/Components/ModelDetailSelect.test.tsx +229 -0
  96. package/Tests/UI/Components/ModelTable/BaseModelTableBulkSelectAll.test.tsx +1258 -0
  97. package/Tests/UI/Components/ModelTable/BaseModelTableColumnCustomization.test.tsx +679 -0
  98. package/Tests/UI/Components/ModelTable/ColumnCustomizationModal.test.tsx +572 -0
  99. package/Tests/UI/Components/ModelTable/ColumnPreference.test.ts +1678 -0
  100. package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +1094 -0
  101. package/Tests/UI/Components/ModelTableExportFromColumns.test.ts +153 -0
  102. package/Tests/UI/Components/MonacoLoader.test.ts +67 -0
  103. package/Tests/UI/Components/MonacoRuntime.test.ts +121 -0
  104. package/Tests/UI/Components/TableBulkCsvExport.test.tsx +212 -3
  105. package/Tests/UI/Utils/Project.test.ts +809 -0
  106. package/Tests/UI/Utils/TableColumnsToCsv.test.ts +360 -0
  107. package/Tests/Utils/Alerts/AlertMetricType.test.ts +143 -0
  108. package/Tests/Utils/Dashboard/VariableUrlState.test.ts +219 -0
  109. package/Tests/Utils/Incident/IncidentMetricType.test.ts +174 -0
  110. package/Tests/Utils/Monitor/MonitorProbeSelectionUtil.test.ts +145 -0
  111. package/Tests/Utils/StatusPage/ResourceUptime.test.ts +263 -0
  112. package/Tests/Utils/UserPreferences.test.ts +563 -0
  113. package/Types/JSONFunctions.ts +7 -2
  114. package/Types/Kubernetes/KubernetesCostIngest.ts +37 -1
  115. package/Types/Workspace/NotificationRules/BaseNotificationRule.ts +4 -0
  116. package/Types/Workspace/NotificationRules/NotificationRuleCondition.ts +25 -10
  117. package/Types/Workspace/WorkspaceMessagePayload.ts +1 -0
  118. package/UI/Components/BulkUpdate/BulkUpdateForm.tsx +63 -8
  119. package/UI/Components/CodeEditor/CodeEditor.tsx +3 -0
  120. package/UI/Components/CodeEditor/MonacoLoader.ts +36 -0
  121. package/UI/Components/Forms/ModelForm.tsx +41 -18
  122. package/UI/Components/MasterPage/MasterPage.tsx +65 -0
  123. package/UI/Components/ModelDetail/CardModelDetail.tsx +23 -1
  124. package/UI/Components/ModelDetail/ModelDetail.tsx +46 -1
  125. package/UI/Components/ModelTable/BaseModelTable.tsx +687 -34
  126. package/UI/Components/ModelTable/Column.ts +29 -0
  127. package/UI/Components/ModelTable/ColumnCustomizationModal.tsx +420 -0
  128. package/UI/Components/ModelTable/ColumnPreference.ts +482 -0
  129. package/UI/Components/ModelTable/CustomFieldColumns.tsx +326 -0
  130. package/UI/Components/ModelTable/ExportFromColumns.ts +57 -0
  131. package/UI/Components/ModelTable/TableView.tsx +24 -2
  132. package/UI/Components/ModelTable/useCustomFieldColumns.ts +150 -0
  133. package/UI/Components/Navbar/NavBar.tsx +66 -1
  134. package/UI/Components/SideMenu/SideMenu.tsx +24 -4
  135. package/UI/Components/Table/Table.tsx +66 -14
  136. package/UI/Components/Table/TableRow.tsx +180 -175
  137. package/UI/Components/Table/Types/Column.ts +20 -0
  138. package/UI/Utils/Project.ts +214 -18
  139. package/UI/Utils/TableColumnsToCsv.ts +86 -6
  140. package/UI/esbuild-config.js +34 -0
  141. package/Utils/Monitor/MonitorProbeSelectionUtil.ts +72 -0
  142. package/Utils/UserPreferences.ts +53 -0
  143. package/build/dist/Models/AnalyticsModels/KubernetesCostAllocation.js +86 -0
  144. package/build/dist/Models/AnalyticsModels/KubernetesCostAllocation.js.map +1 -1
  145. package/build/dist/Models/DatabaseModels/DashboardDomain.js +1 -0
  146. package/build/dist/Models/DatabaseModels/DashboardDomain.js.map +1 -1
  147. package/build/dist/Models/DatabaseModels/Incident.js +2 -0
  148. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  149. package/build/dist/Models/DatabaseModels/IncidentEpisode.js +1 -0
  150. package/build/dist/Models/DatabaseModels/IncidentEpisode.js.map +1 -1
  151. package/build/dist/Models/DatabaseModels/IncidentEpisodePublicNote.js +1 -0
  152. package/build/dist/Models/DatabaseModels/IncidentEpisodePublicNote.js.map +1 -1
  153. package/build/dist/Models/DatabaseModels/IncidentEpisodeStateTimeline.js +1 -0
  154. package/build/dist/Models/DatabaseModels/IncidentEpisodeStateTimeline.js.map +1 -1
  155. package/build/dist/Models/DatabaseModels/IncidentPublicNote.js +1 -0
  156. package/build/dist/Models/DatabaseModels/IncidentPublicNote.js.map +1 -1
  157. package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js +1 -0
  158. package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js.map +1 -1
  159. package/build/dist/Models/DatabaseModels/Monitor.js +1 -0
  160. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  161. package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js +1 -0
  162. package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js.map +1 -1
  163. package/build/dist/Models/DatabaseModels/NetworkSite.js +1 -0
  164. package/build/dist/Models/DatabaseModels/NetworkSite.js.map +1 -1
  165. package/build/dist/Models/DatabaseModels/NetworkSiteStatusTimeline.js +1 -0
  166. package/build/dist/Models/DatabaseModels/NetworkSiteStatusTimeline.js.map +1 -1
  167. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js +1 -0
  168. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js.map +1 -1
  169. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +2 -0
  170. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
  171. package/build/dist/Models/DatabaseModels/Probe.js +13 -13
  172. package/build/dist/Models/DatabaseModels/Probe.js.map +1 -1
  173. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +1 -0
  174. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
  175. package/build/dist/Models/DatabaseModels/ScheduledMaintenancePublicNote.js +1 -0
  176. package/build/dist/Models/DatabaseModels/ScheduledMaintenancePublicNote.js.map +1 -1
  177. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.js +1 -0
  178. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.js.map +1 -1
  179. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js +1 -0
  180. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js.map +1 -1
  181. package/build/dist/Models/DatabaseModels/StatusPage.js +1 -0
  182. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  183. package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js +1 -0
  184. package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js.map +1 -1
  185. package/build/dist/Models/DatabaseModels/StatusPageDomain.js +1 -0
  186. package/build/dist/Models/DatabaseModels/StatusPageDomain.js.map +1 -1
  187. package/build/dist/Models/DatabaseModels/TableView.js +40 -0
  188. package/build/dist/Models/DatabaseModels/TableView.js.map +1 -1
  189. package/build/dist/Models/DatabaseModels/WorkspaceNotificationSummary.js +1 -0
  190. package/build/dist/Models/DatabaseModels/WorkspaceNotificationSummary.js.map +1 -1
  191. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
  192. package/build/dist/Server/API/BaseAPI.js +12 -1
  193. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  194. package/build/dist/Server/API/CommonAPI.js +21 -0
  195. package/build/dist/Server/API/CommonAPI.js.map +1 -1
  196. package/build/dist/Server/API/MicrosoftTeamsAPI.js +72 -2
  197. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  198. package/build/dist/Server/API/ProbeAPI.js +2 -0
  199. package/build/dist/Server/API/ProbeAPI.js.map +1 -1
  200. package/build/dist/Server/API/SlackAPI.js +44 -0
  201. package/build/dist/Server/API/SlackAPI.js.map +1 -1
  202. package/build/dist/Server/API/StatusPageAPI.js +98 -152
  203. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  204. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785140242697-AddHotQueryIndexes.js +36 -0
  205. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785140242697-AddHotQueryIndexes.js.map +1 -0
  206. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785148065137-AddHotQueryIndexesSecondPass.js +26 -0
  207. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785148065137-AddHotQueryIndexesSecondPass.js.map +1 -0
  208. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785240000000-RepairCrossProjectMonitorStatusReferences.js +254 -0
  209. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785240000000-RepairCrossProjectMonitorStatusReferences.js.map +1 -0
  210. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785241000000-AddColumnsToTableView.js +12 -0
  211. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785241000000-AddColumnsToTableView.js.map +1 -0
  212. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  213. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  214. package/build/dist/Server/Services/DatabaseService.js +10 -2
  215. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  216. package/build/dist/Server/Services/MonitorGroupService.js +118 -0
  217. package/build/dist/Server/Services/MonitorGroupService.js.map +1 -1
  218. package/build/dist/Server/Services/MonitorService.js +198 -24
  219. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  220. package/build/dist/Server/Services/ProbeService.js +238 -9
  221. package/build/dist/Server/Services/ProbeService.js.map +1 -1
  222. package/build/dist/Server/Services/StatusPageService.js +46 -39
  223. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  224. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js +178 -3
  225. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js.map +1 -1
  226. package/build/dist/Server/Utils/Database/PostgresErrorTranslator.js +80 -0
  227. package/build/dist/Server/Utils/Database/PostgresErrorTranslator.js.map +1 -0
  228. package/build/dist/Server/Utils/Monitor/MonitorResource.js +67 -29
  229. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  230. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js +31 -14
  231. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js.map +1 -1
  232. package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js +117 -0
  233. package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js.map +1 -0
  234. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +549 -7
  235. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  236. package/build/dist/Types/JSONFunctions.js +8 -3
  237. package/build/dist/Types/JSONFunctions.js.map +1 -1
  238. package/build/dist/Types/Kubernetes/KubernetesCostIngest.js.map +1 -1
  239. package/build/dist/Types/Workspace/NotificationRules/NotificationRuleCondition.js +17 -7
  240. package/build/dist/Types/Workspace/NotificationRules/NotificationRuleCondition.js.map +1 -1
  241. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js +37 -11
  242. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js.map +1 -1
  243. package/build/dist/UI/Components/CodeEditor/CodeEditor.js +2 -0
  244. package/build/dist/UI/Components/CodeEditor/CodeEditor.js.map +1 -1
  245. package/build/dist/UI/Components/CodeEditor/MonacoLoader.js +34 -0
  246. package/build/dist/UI/Components/CodeEditor/MonacoLoader.js.map +1 -0
  247. package/build/dist/UI/Components/Forms/ModelForm.js +37 -17
  248. package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
  249. package/build/dist/UI/Components/MasterPage/MasterPage.js +50 -1
  250. package/build/dist/UI/Components/MasterPage/MasterPage.js.map +1 -1
  251. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js +22 -1
  252. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js.map +1 -1
  253. package/build/dist/UI/Components/ModelDetail/ModelDetail.js +31 -1
  254. package/build/dist/UI/Components/ModelDetail/ModelDetail.js.map +1 -1
  255. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +472 -27
  256. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  257. package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js +189 -0
  258. package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js.map +1 -0
  259. package/build/dist/UI/Components/ModelTable/ColumnPreference.js +258 -0
  260. package/build/dist/UI/Components/ModelTable/ColumnPreference.js.map +1 -0
  261. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +168 -0
  262. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -0
  263. package/build/dist/UI/Components/ModelTable/ExportFromColumns.js +30 -0
  264. package/build/dist/UI/Components/ModelTable/ExportFromColumns.js.map +1 -0
  265. package/build/dist/UI/Components/ModelTable/TableView.js +13 -2
  266. package/build/dist/UI/Components/ModelTable/TableView.js.map +1 -1
  267. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js +84 -0
  268. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js.map +1 -0
  269. package/build/dist/UI/Components/Navbar/NavBar.js +39 -1
  270. package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
  271. package/build/dist/UI/Components/SideMenu/SideMenu.js +15 -5
  272. package/build/dist/UI/Components/SideMenu/SideMenu.js.map +1 -1
  273. package/build/dist/UI/Components/Table/Table.js +46 -13
  274. package/build/dist/UI/Components/Table/Table.js.map +1 -1
  275. package/build/dist/UI/Components/Table/TableRow.js +11 -6
  276. package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
  277. package/build/dist/UI/Utils/Project.js +161 -15
  278. package/build/dist/UI/Utils/Project.js.map +1 -1
  279. package/build/dist/UI/Utils/TableColumnsToCsv.js +67 -6
  280. package/build/dist/UI/Utils/TableColumnsToCsv.js.map +1 -1
  281. package/build/dist/Utils/Monitor/MonitorProbeSelectionUtil.js +52 -0
  282. package/build/dist/Utils/Monitor/MonitorProbeSelectionUtil.js.map +1 -0
  283. package/build/dist/Utils/UserPreferences.js +33 -0
  284. package/build/dist/Utils/UserPreferences.js.map +1 -1
  285. package/package.json +4 -3
@@ -0,0 +1,679 @@
1
+ /*
2
+ * Probe liveness hardening in ProbeService: a customer's probes were flagged
3
+ * Disconnected because /probe-ingest/alive hung 45+ seconds — every probe
4
+ * request re-authenticated with a Probe SELECT that queued behind a starved
5
+ * Postgres pool, and the throttled lastAlive stamp could silently suppress
6
+ * its own retry after a failed write. This suite pins the fixes:
7
+ *
8
+ * - verifyProbeKey: Redis-cached SHA-256 key verification (namespace
9
+ * "probe-auth-key-hash", TTL 120s) with DB fallback, so repeat probe
10
+ * requests skip the per-request Probe SELECT entirely.
11
+ * - boundedCacheOperation: a 2s cap on every cache op — ioredis has no
12
+ * command timeout, so a stalled-but-open Redis socket must not hang
13
+ * probe auth.
14
+ * - updateLastAlive: clears the 30s "probe-last-alive" throttle stamp when
15
+ * the DB write FAILS, so the next request retries instead of silently
16
+ * letting lastAlive cross the 3-minute Disconnected cutoff.
17
+ * - onUpdateSuccess/onDeleteSuccess: key rotation and probe deletion
18
+ * overwrite the cached auth hash cluster-wide with a revocation
19
+ * sentinel, and verifyProbeKey's write-only-if-unchanged re-read keeps
20
+ * an in-flight verification from resurrecting the revoked hash.
21
+ *
22
+ * GlobalCache and MonitorService are mocked at the module boundary — the
23
+ * real modules pull in Redis and the whole database stack. DB calls are
24
+ * spied on the ProbeService singleton. No Postgres, no Redis.
25
+ */
26
+
27
+ const mockGetString: jest.Mock = jest.fn();
28
+ const mockSetString: jest.Mock = jest.fn();
29
+ const mockDeleteKey: jest.Mock = jest.fn();
30
+
31
+ jest.mock("../../../Server/Infrastructure/GlobalCache", () => {
32
+ return {
33
+ __esModule: true,
34
+ default: {
35
+ getString: (...args: Array<unknown>) => {
36
+ return mockGetString(...args);
37
+ },
38
+ setString: (...args: Array<unknown>) => {
39
+ return mockSetString(...args);
40
+ },
41
+ deleteKey: (...args: Array<unknown>) => {
42
+ return mockDeleteKey(...args);
43
+ },
44
+ },
45
+ };
46
+ });
47
+
48
+ const mockRefreshProbeStatus: jest.Mock = jest.fn();
49
+
50
+ jest.mock("../../../Server/Services/MonitorService", () => {
51
+ return {
52
+ __esModule: true,
53
+ default: {
54
+ refreshProbeStatus: (...args: Array<unknown>) => {
55
+ return mockRefreshProbeStatus(...args);
56
+ },
57
+ },
58
+ };
59
+ });
60
+
61
+ jest.mock("../../../Server/Utils/Logger", () => {
62
+ return {
63
+ __esModule: true,
64
+ default: {
65
+ debug: jest.fn(),
66
+ info: jest.fn(),
67
+ warn: jest.fn(),
68
+ error: jest.fn(),
69
+ },
70
+ getLogAttributesFromRequest: jest.fn(() => {
71
+ return {};
72
+ }),
73
+ };
74
+ });
75
+
76
+ import crypto from "crypto";
77
+ import logger from "../../../Server/Utils/Logger";
78
+ import ProbeService from "../../../Server/Services/ProbeService";
79
+ import Probe, {
80
+ ProbeConnectionStatus,
81
+ } from "../../../Models/DatabaseModels/Probe";
82
+ import ObjectID from "../../../Types/ObjectID";
83
+ import OneUptimeDate from "../../../Types/Date";
84
+ import BadDataException from "../../../Types/Exception/BadDataException";
85
+ import UpdateBy from "../../../Server/Types/Database/UpdateBy";
86
+ import DeleteBy from "../../../Server/Types/Database/DeleteBy";
87
+ import { OnDelete, OnUpdate } from "../../../Server/Types/Database/Hooks";
88
+ import { describe, expect, test, beforeEach, afterEach } from "@jest/globals";
89
+
90
+ const PROBE_ID: ObjectID = new ObjectID("11111111-1111-4111-8111-111111111111");
91
+ const SECOND_PROBE_ID: ObjectID = new ObjectID(
92
+ "22222222-2222-4222-8222-222222222222",
93
+ );
94
+ const PROBE_KEY: string = "probe-key-abc-123";
95
+ const ROTATED_PROBE_KEY: string = "probe-key-rotated-456";
96
+
97
+ const AUTH_CACHE_NAMESPACE: string = "probe-auth-key-hash";
98
+ const AUTH_CACHE_TTL_IN_SECONDS: number = 120;
99
+ const AUTH_CACHE_REVOKED_SENTINEL: string = "revoked";
100
+ const LAST_ALIVE_CACHE_NAMESPACE: string = "probe-last-alive";
101
+ const CACHE_OPERATION_TIMEOUT_IN_MS: number = 2000;
102
+
103
+ type Sha256Function = (value: string) => string;
104
+
105
+ /*
106
+ * Computed independently of ProbeService.hashProbeKey so the tests pin the
107
+ * EXACT on-the-wire cache format, not whatever the service happens to do.
108
+ */
109
+ const sha256: Sha256Function = (value: string): string => {
110
+ return crypto.createHash("sha256").update(value).digest("hex");
111
+ };
112
+
113
+ type MakeProbeRowFunction = () => Probe;
114
+
115
+ const makeProbeRow: MakeProbeRowFunction = (): Probe => {
116
+ const probe: Probe = new Probe();
117
+ probe.id = PROBE_ID;
118
+ return probe;
119
+ };
120
+
121
+ describe("ProbeService probe-auth cache and heartbeat throttle", () => {
122
+ beforeEach(() => {
123
+ jest.clearAllMocks();
124
+ mockGetString.mockResolvedValue(null);
125
+ mockSetString.mockResolvedValue(undefined);
126
+ mockDeleteKey.mockResolvedValue(undefined);
127
+ });
128
+
129
+ afterEach(() => {
130
+ jest.restoreAllMocks();
131
+ jest.useRealTimers();
132
+ });
133
+
134
+ describe("verifyProbeKey (cache-backed auth)", () => {
135
+ /*
136
+ * The whole point of the cache: a repeat request from a known probe must
137
+ * authenticate WITHOUT touching Postgres. If findOneBy fires here, the
138
+ * per-request Probe SELECT is back and probe auth queues behind the pool
139
+ * again — the original 45s-hang ingredient.
140
+ */
141
+ test("cache hit with matching SHA-256 authenticates with NO Probe SELECT", async () => {
142
+ mockGetString.mockResolvedValue(sha256(PROBE_KEY));
143
+ // Would return "not found" if consulted — proving the cache decided.
144
+ const findOneBySpy: jest.SpyInstance = jest
145
+ .spyOn(ProbeService, "findOneBy")
146
+ .mockResolvedValue(null);
147
+
148
+ const result: boolean = await ProbeService.verifyProbeKey({
149
+ probeId: PROBE_ID,
150
+ probeKey: PROBE_KEY,
151
+ });
152
+
153
+ expect(result).toBe(true);
154
+ expect(findOneBySpy).not.toHaveBeenCalled();
155
+ expect(mockSetString).not.toHaveBeenCalled();
156
+ expect(mockGetString).toHaveBeenCalledWith(
157
+ AUTH_CACHE_NAMESPACE,
158
+ PROBE_ID.toString(),
159
+ );
160
+ });
161
+
162
+ /*
163
+ * Security invariant: the cache stores only a SHA-256 hex digest of the
164
+ * key with a bounded TTL — a Redis dump must never contain a usable
165
+ * probe credential.
166
+ */
167
+ test("caches only the SHA-256 hex of the key with the 120s TTL — never the raw key", async () => {
168
+ const findOneBySpy: jest.SpyInstance = jest
169
+ .spyOn(ProbeService, "findOneBy")
170
+ .mockResolvedValue(makeProbeRow());
171
+
172
+ const result: boolean = await ProbeService.verifyProbeKey({
173
+ probeId: PROBE_ID,
174
+ probeKey: PROBE_KEY,
175
+ });
176
+
177
+ expect(result).toBe(true);
178
+ expect(findOneBySpy).toHaveBeenCalledTimes(1);
179
+ expect(mockSetString).toHaveBeenCalledTimes(1);
180
+ expect(mockSetString).toHaveBeenCalledWith(
181
+ AUTH_CACHE_NAMESPACE,
182
+ PROBE_ID.toString(),
183
+ sha256(PROBE_KEY),
184
+ { expiresInSeconds: AUTH_CACHE_TTL_IN_SECONDS },
185
+ );
186
+ // The raw key must not appear ANYWHERE in the cache-write arguments.
187
+ expect(JSON.stringify(mockSetString.mock.calls)).not.toContain(PROBE_KEY);
188
+ });
189
+
190
+ test("cache miss verifies against the DB with the presented id+key pair", async () => {
191
+ const findOneBySpy: jest.SpyInstance = jest
192
+ .spyOn(ProbeService, "findOneBy")
193
+ .mockResolvedValue(makeProbeRow());
194
+
195
+ const result: boolean = await ProbeService.verifyProbeKey({
196
+ probeId: PROBE_ID,
197
+ probeKey: PROBE_KEY,
198
+ });
199
+
200
+ expect(result).toBe(true);
201
+ expect(findOneBySpy).toHaveBeenCalledTimes(1);
202
+ expect(findOneBySpy).toHaveBeenCalledWith(
203
+ expect.objectContaining({
204
+ query: { _id: PROBE_ID.toString(), key: PROBE_KEY },
205
+ props: { isRoot: true },
206
+ }),
207
+ );
208
+ });
209
+
210
+ /*
211
+ * A cache entry may only ever be written from a value that was JUST
212
+ * verified against the database — caching an UNVERIFIED hash would let a
213
+ * wrong key poison the cache and then authenticate itself on retry.
214
+ */
215
+ test("DB not-found returns false and writes NOTHING to the cache", async () => {
216
+ const findOneBySpy: jest.SpyInstance = jest
217
+ .spyOn(ProbeService, "findOneBy")
218
+ .mockResolvedValue(null);
219
+
220
+ const result: boolean = await ProbeService.verifyProbeKey({
221
+ probeId: PROBE_ID,
222
+ probeKey: "wrong-key",
223
+ });
224
+
225
+ expect(result).toBe(false);
226
+ expect(findOneBySpy).toHaveBeenCalledTimes(1);
227
+ expect(mockSetString).not.toHaveBeenCalled();
228
+ });
229
+
230
+ /*
231
+ * Key rotation: the cached hash belongs to the OLD key, the probe
232
+ * presents the NEW one. The mismatch must fall through to the DB (so a
233
+ * freshly rotated key works immediately, no TTL wait). The cache write
234
+ * is SKIPPED while a conflicting entry is still present — the
235
+ * write-only-if-unchanged rule never overwrites a value someone else
236
+ * put there — so requests keep DB-verifying until the old entry
237
+ * expires, at which point caching resumes (pinned below).
238
+ */
239
+ test("stale cached hash (rotated key) falls through to the DB and does not clobber the existing entry", async () => {
240
+ mockGetString.mockResolvedValue(sha256(PROBE_KEY));
241
+ const findOneBySpy: jest.SpyInstance = jest
242
+ .spyOn(ProbeService, "findOneBy")
243
+ .mockResolvedValue(makeProbeRow());
244
+
245
+ const result: boolean = await ProbeService.verifyProbeKey({
246
+ probeId: PROBE_ID,
247
+ probeKey: ROTATED_PROBE_KEY,
248
+ });
249
+
250
+ expect(result).toBe(true);
251
+ expect(findOneBySpy).toHaveBeenCalledTimes(1);
252
+ expect(mockSetString).not.toHaveBeenCalled();
253
+ });
254
+
255
+ /*
256
+ * The revocation sentinel written by invalidation can never equal a
257
+ * SHA-256 digest, so it must behave as a miss on the auth path (DB
258
+ * decides) and must NOT be overwritten by the verification it forced —
259
+ * that write-back is exactly the rotation race the sentinel exists to
260
+ * stop.
261
+ */
262
+ test("the revocation sentinel forces a DB check and is never overwritten", async () => {
263
+ mockGetString.mockResolvedValue(AUTH_CACHE_REVOKED_SENTINEL);
264
+ const findOneBySpy: jest.SpyInstance = jest
265
+ .spyOn(ProbeService, "findOneBy")
266
+ .mockResolvedValue(makeProbeRow());
267
+
268
+ const result: boolean = await ProbeService.verifyProbeKey({
269
+ probeId: PROBE_ID,
270
+ probeKey: PROBE_KEY,
271
+ });
272
+
273
+ expect(result).toBe(true);
274
+ expect(findOneBySpy).toHaveBeenCalledTimes(1);
275
+ expect(mockSetString).not.toHaveBeenCalled();
276
+ });
277
+
278
+ /*
279
+ * THE rotation-TOCTOU pin. A verification can straddle a key rotation:
280
+ * its DB SELECT resolves against the pre-rotation row, THEN the
281
+ * rotation commits and invalidation lands. An unconditional cache write
282
+ * here would re-cache the just-revoked key for a fresh TTL —
283
+ * resurrecting a credential the rotation was meant to kill. The
284
+ * pre-write re-read must see the change and skip the write.
285
+ */
286
+ test("a rotation landing mid-verification wins: the straddling write is skipped", async () => {
287
+ mockGetString
288
+ .mockResolvedValueOnce(null) // initial read: plain miss
289
+ .mockResolvedValueOnce(AUTH_CACHE_REVOKED_SENTINEL); // pre-write read: rotation landed
290
+ const findOneBySpy: jest.SpyInstance = jest
291
+ .spyOn(ProbeService, "findOneBy")
292
+ .mockResolvedValue(makeProbeRow());
293
+
294
+ const result: boolean = await ProbeService.verifyProbeKey({
295
+ probeId: PROBE_ID,
296
+ probeKey: PROBE_KEY,
297
+ });
298
+
299
+ // The DB verified the (pre-rotation) key, so THIS request passes...
300
+ expect(result).toBe(true);
301
+ expect(findOneBySpy).toHaveBeenCalledTimes(1);
302
+ // ...but nothing may be written over the invalidation.
303
+ expect(mockSetString).not.toHaveBeenCalled();
304
+ });
305
+
306
+ test("caching resumes once the conflicting entry has expired", async () => {
307
+ // Both reads see an empty slot — the sentinel/old hash TTL'd out.
308
+ mockGetString.mockResolvedValue(null);
309
+ jest.spyOn(ProbeService, "findOneBy").mockResolvedValue(makeProbeRow());
310
+
311
+ await ProbeService.verifyProbeKey({
312
+ probeId: PROBE_ID,
313
+ probeKey: ROTATED_PROBE_KEY,
314
+ });
315
+
316
+ expect(mockSetString).toHaveBeenCalledWith(
317
+ AUTH_CACHE_NAMESPACE,
318
+ PROBE_ID.toString(),
319
+ sha256(ROTATED_PROBE_KEY),
320
+ { expiresInSeconds: AUTH_CACHE_TTL_IN_SECONDS },
321
+ );
322
+ });
323
+
324
+ /*
325
+ * Redis down must degrade to "cache off", not "auth off": the DB is the
326
+ * source of truth and a cache failure may neither fail the request
327
+ * (closed) nor skip verification (open).
328
+ */
329
+ test("cache read rejection falls back to the DB and still authenticates", async () => {
330
+ mockGetString.mockRejectedValue(new Error("Redis connection refused"));
331
+ const findOneBySpy: jest.SpyInstance = jest
332
+ .spyOn(ProbeService, "findOneBy")
333
+ .mockResolvedValue(makeProbeRow());
334
+
335
+ const result: boolean = await ProbeService.verifyProbeKey({
336
+ probeId: PROBE_ID,
337
+ probeKey: PROBE_KEY,
338
+ });
339
+
340
+ expect(result).toBe(true);
341
+ expect(findOneBySpy).toHaveBeenCalledTimes(1);
342
+ });
343
+
344
+ /*
345
+ * THE hang pin. ioredis has no command timeout: a stalled-but-open Redis
346
+ * socket returns a promise that never settles, and before the 2s
347
+ * boundedCacheOperation cap that promise would have hung probe auth
348
+ * forever — the async middleware's rejection-free stall was exactly the
349
+ * silent-hang mode that got healthy probes flagged Disconnected. A
350
+ * never-settling cache read must time out and fall back to the DB.
351
+ */
352
+ test("a never-settling cache read is capped at 2s and falls back to the DB", async () => {
353
+ jest.useFakeTimers();
354
+
355
+ mockGetString.mockReturnValue(
356
+ new Promise<string | null>(() => {
357
+ // Never settles — simulates a dead-but-open Redis socket.
358
+ }),
359
+ );
360
+ const findOneBySpy: jest.SpyInstance = jest
361
+ .spyOn(ProbeService, "findOneBy")
362
+ .mockResolvedValue(makeProbeRow());
363
+
364
+ const resultPromise: Promise<boolean> = ProbeService.verifyProbeKey({
365
+ probeId: PROBE_ID,
366
+ probeKey: PROBE_KEY,
367
+ });
368
+
369
+ /*
370
+ * Two caps fire on this path: the initial read (→ DB fallback) and
371
+ * the pre-write re-read (→ skip the cache write). The microtask
372
+ * flushes between the advances let the promise chain progress far
373
+ * enough that the second bounded op has registered its cap timer
374
+ * before it is advanced.
375
+ */
376
+ jest.advanceTimersByTime(CACHE_OPERATION_TIMEOUT_IN_MS);
377
+ for (let i: number = 0; i < 10; i++) {
378
+ await Promise.resolve();
379
+ }
380
+ jest.advanceTimersByTime(CACHE_OPERATION_TIMEOUT_IN_MS);
381
+
382
+ await expect(resultPromise).resolves.toBe(true);
383
+ expect(findOneBySpy).toHaveBeenCalledTimes(1);
384
+ });
385
+
386
+ /*
387
+ * The cap timer must not outlive a fast cache operation: a leaked 2s
388
+ * timer per probe request would accumulate thousands of live timers on
389
+ * a busy ingest pod.
390
+ */
391
+ test("a settled cache operation leaves no cap timer behind", async () => {
392
+ jest.useFakeTimers();
393
+ mockGetString.mockResolvedValue(sha256(PROBE_KEY));
394
+
395
+ const result: boolean = await ProbeService.verifyProbeKey({
396
+ probeId: PROBE_ID,
397
+ probeKey: PROBE_KEY,
398
+ });
399
+
400
+ expect(result).toBe(true);
401
+ expect(jest.getTimerCount()).toBe(0);
402
+ });
403
+
404
+ test("missing probeId or probeKey is rejected with BadDataException", async () => {
405
+ await expect(
406
+ ProbeService.verifyProbeKey({
407
+ probeId: undefined as unknown as ObjectID,
408
+ probeKey: PROBE_KEY,
409
+ }),
410
+ ).rejects.toThrow(BadDataException);
411
+
412
+ await expect(
413
+ ProbeService.verifyProbeKey({
414
+ probeId: PROBE_ID,
415
+ probeKey: "",
416
+ }),
417
+ ).rejects.toThrow(BadDataException);
418
+ });
419
+ });
420
+
421
+ describe("updateLastAlive (throttled heartbeat stamp)", () => {
422
+ /*
423
+ * The throttle is what makes it safe to stamp lastAlive on EVERY
424
+ * authenticated probe request: at most one single-statement UPDATE per
425
+ * probe per 30s, no matter how chatty the probe is.
426
+ */
427
+ test("skips the DB write while the 30s throttle stamp is fresh", async () => {
428
+ mockGetString.mockResolvedValue(
429
+ OneUptimeDate.toString(OneUptimeDate.getSomeSecondsAgo(10)),
430
+ );
431
+ const updateSpy: jest.SpyInstance = jest
432
+ .spyOn(ProbeService, "updateColumnsByIdWithoutHooks")
433
+ .mockResolvedValue(undefined);
434
+
435
+ await ProbeService.updateLastAlive(PROBE_ID);
436
+
437
+ expect(updateSpy).not.toHaveBeenCalled();
438
+ // The stamp is not refreshed either — that would extend the throttle.
439
+ expect(mockSetString).not.toHaveBeenCalled();
440
+ });
441
+
442
+ test("writes exactly one hookless lastAlive UPDATE once the stamp is stale", async () => {
443
+ mockGetString.mockResolvedValue(
444
+ OneUptimeDate.toString(OneUptimeDate.getSomeSecondsAgo(45)),
445
+ );
446
+ const updateSpy: jest.SpyInstance = jest
447
+ .spyOn(ProbeService, "updateColumnsByIdWithoutHooks")
448
+ .mockResolvedValue(undefined);
449
+
450
+ await ProbeService.updateLastAlive(PROBE_ID);
451
+
452
+ expect(updateSpy).toHaveBeenCalledTimes(1);
453
+ expect(updateSpy).toHaveBeenCalledWith({
454
+ id: PROBE_ID,
455
+ data: { lastAlive: expect.any(Date) },
456
+ });
457
+ // The throttle stamp is re-written for the next 30s window.
458
+ expect(mockSetString).toHaveBeenCalledWith(
459
+ LAST_ALIVE_CACHE_NAMESPACE,
460
+ PROBE_ID.toString(),
461
+ expect.any(String),
462
+ );
463
+ });
464
+
465
+ test("writes when no throttle stamp exists (first heartbeat)", async () => {
466
+ // Default mockGetString resolves null — nothing cached yet.
467
+ const updateSpy: jest.SpyInstance = jest
468
+ .spyOn(ProbeService, "updateColumnsByIdWithoutHooks")
469
+ .mockResolvedValue(undefined);
470
+
471
+ await ProbeService.updateLastAlive(PROBE_ID);
472
+
473
+ expect(updateSpy).toHaveBeenCalledTimes(1);
474
+ });
475
+
476
+ /*
477
+ * THE self-heal pin. The throttle stamp is written BEFORE the DB write
478
+ * is attempted (deliberately, as dogpile protection). Leaving it in
479
+ * place after a FAILED write means every request for the next 30s
480
+ * silently skips the retry; a couple of consecutive failures and
481
+ * lastAlive crosses the connection-status worker's 3-minute staleness
482
+ * cutoff — a HEALTHY probe gets flagged Disconnected, which is the
483
+ * customer-reported failure mode. A failed write must clear the stamp
484
+ * so the very next request retries, and must rethrow so the caller's
485
+ * error logging still fires.
486
+ */
487
+ test("a FAILED lastAlive write clears the throttle stamp and rethrows", async () => {
488
+ const updateSpy: jest.SpyInstance = jest
489
+ .spyOn(ProbeService, "updateColumnsByIdWithoutHooks")
490
+ .mockRejectedValue(new Error("connection pool exhausted"));
491
+
492
+ await expect(ProbeService.updateLastAlive(PROBE_ID)).rejects.toThrow(
493
+ "connection pool exhausted",
494
+ );
495
+
496
+ expect(updateSpy).toHaveBeenCalledTimes(1);
497
+ expect(mockDeleteKey).toHaveBeenCalledWith(
498
+ LAST_ALIVE_CACHE_NAMESPACE,
499
+ PROBE_ID.toString(),
500
+ );
501
+ });
502
+
503
+ /*
504
+ * The throttle-clear is best-effort: if Redis is ALSO down, the stamp
505
+ * expires on its own — but the ORIGINAL database error must surface,
506
+ * not the secondary cache error.
507
+ */
508
+ test("throttle-clear failure does not mask the original DB error", async () => {
509
+ mockDeleteKey.mockRejectedValue(new Error("redis down"));
510
+ jest
511
+ .spyOn(ProbeService, "updateColumnsByIdWithoutHooks")
512
+ .mockRejectedValue(new Error("connection pool exhausted"));
513
+
514
+ await expect(ProbeService.updateLastAlive(PROBE_ID)).rejects.toThrow(
515
+ "connection pool exhausted",
516
+ );
517
+ });
518
+
519
+ test("a successful write leaves the throttle stamp in place", async () => {
520
+ jest
521
+ .spyOn(ProbeService, "updateColumnsByIdWithoutHooks")
522
+ .mockResolvedValue(undefined);
523
+
524
+ await ProbeService.updateLastAlive(PROBE_ID);
525
+
526
+ expect(mockDeleteKey).not.toHaveBeenCalled();
527
+ });
528
+ });
529
+
530
+ describe("auth cache invalidation hooks", () => {
531
+ /*
532
+ * The auth middleware trusts a cached hash of the key, so the moment
533
+ * the key column changes the cached entries for the updated probes must
534
+ * be revoked — otherwise the OLD key keeps authenticating until the
535
+ * 120s TTL runs out. Invalidation WRITES the sentinel rather than
536
+ * deleting: a bare delete can be silently undone by an in-flight
537
+ * verification re-caching the old hash (see the TOCTOU pin above),
538
+ * while the sentinel makes that straggler's write-only-if-unchanged
539
+ * check fail.
540
+ */
541
+ test("onUpdateSuccess writes the revocation sentinel for every updated probe when the key changes", async () => {
542
+ const onUpdate: OnUpdate<Probe> = {
543
+ updateBy: {
544
+ query: {},
545
+ data: { key: ROTATED_PROBE_KEY },
546
+ props: { isRoot: true },
547
+ } as UpdateBy<Probe>,
548
+ carryForward: { probesToNotifyOwners: [] },
549
+ };
550
+
551
+ await (ProbeService as any)["onUpdateSuccess"](onUpdate, [
552
+ PROBE_ID,
553
+ SECOND_PROBE_ID,
554
+ ]);
555
+
556
+ expect(mockSetString).toHaveBeenCalledTimes(2);
557
+ expect(mockSetString).toHaveBeenCalledWith(
558
+ AUTH_CACHE_NAMESPACE,
559
+ PROBE_ID.toString(),
560
+ AUTH_CACHE_REVOKED_SENTINEL,
561
+ { expiresInSeconds: AUTH_CACHE_TTL_IN_SECONDS },
562
+ );
563
+ expect(mockSetString).toHaveBeenCalledWith(
564
+ AUTH_CACHE_NAMESPACE,
565
+ SECOND_PROBE_ID.toString(),
566
+ AUTH_CACHE_REVOKED_SENTINEL,
567
+ { expiresInSeconds: AUTH_CACHE_TTL_IN_SECONDS },
568
+ );
569
+ });
570
+
571
+ test("onUpdateSuccess leaves the cache alone when data does not touch the key", async () => {
572
+ const onUpdate: OnUpdate<Probe> = {
573
+ updateBy: {
574
+ query: {},
575
+ data: { name: "renamed probe" },
576
+ props: { isRoot: true },
577
+ } as UpdateBy<Probe>,
578
+ carryForward: { probesToNotifyOwners: [] },
579
+ };
580
+
581
+ await (ProbeService as any)["onUpdateSuccess"](onUpdate, [PROBE_ID]);
582
+
583
+ expect(mockSetString).not.toHaveBeenCalled();
584
+ expect(mockRefreshProbeStatus).not.toHaveBeenCalled();
585
+ });
586
+
587
+ /*
588
+ * Invalidation is best-effort per probe: one Redis failure must be
589
+ * logged loudly (a revoked key may keep working for up to the TTL) but
590
+ * must neither fail the surrounding update nor stop the remaining
591
+ * probes from being invalidated.
592
+ */
593
+ test("an invalidation failure is logged, does not throw, and does not stop the remaining probes", async () => {
594
+ mockSetString
595
+ .mockRejectedValueOnce(new Error("redis write failed"))
596
+ .mockResolvedValue(undefined);
597
+
598
+ await expect(
599
+ ProbeService.invalidateProbeAuthCache([PROBE_ID, SECOND_PROBE_ID]),
600
+ ).resolves.toBeUndefined();
601
+
602
+ // Both probes were attempted despite the first failure...
603
+ expect(mockSetString).toHaveBeenCalledTimes(2);
604
+ expect(mockSetString).toHaveBeenCalledWith(
605
+ AUTH_CACHE_NAMESPACE,
606
+ SECOND_PROBE_ID.toString(),
607
+ AUTH_CACHE_REVOKED_SENTINEL,
608
+ { expiresInSeconds: AUTH_CACHE_TTL_IN_SECONDS },
609
+ );
610
+ // ...and the failure was surfaced in the logs.
611
+ expect(logger.error).toHaveBeenCalledWith(
612
+ expect.stringContaining("CRITICAL"),
613
+ expect.anything(),
614
+ );
615
+ });
616
+
617
+ /*
618
+ * Existing behavior preserved: the cache-invalidation addition must not
619
+ * displace the connection-status owner-notification path that
620
+ * onUpdateSuccess already ran.
621
+ */
622
+ test("connection-status change still refreshes monitors and notifies owners", async () => {
623
+ const probeRow: Probe = makeProbeRow();
624
+ const notifySpy: jest.SpyInstance = jest
625
+ .spyOn(ProbeService, "notifyOwnersOnStatusChange")
626
+ .mockResolvedValue(undefined);
627
+
628
+ const onUpdate: OnUpdate<Probe> = {
629
+ updateBy: {
630
+ query: {},
631
+ data: { connectionStatus: ProbeConnectionStatus.Disconnected },
632
+ props: { isRoot: true },
633
+ } as UpdateBy<Probe>,
634
+ carryForward: { probesToNotifyOwners: [probeRow] },
635
+ };
636
+
637
+ await (ProbeService as any)["onUpdateSuccess"](onUpdate, [PROBE_ID]);
638
+
639
+ expect(mockRefreshProbeStatus).toHaveBeenCalledTimes(1);
640
+ expect(mockRefreshProbeStatus).toHaveBeenCalledWith(PROBE_ID);
641
+ expect(notifySpy).toHaveBeenCalledWith({ probeId: PROBE_ID });
642
+ // A status flip is not a key change — cached auth stays valid.
643
+ expect(mockDeleteKey).not.toHaveBeenCalled();
644
+ });
645
+
646
+ /*
647
+ * A deleted probe's key must stop authenticating immediately, not after
648
+ * the cache TTL.
649
+ */
650
+ test("onDeleteSuccess writes the revocation sentinel for every deleted probe", async () => {
651
+ const onDelete: OnDelete<Probe> = {
652
+ deleteBy: {
653
+ query: {},
654
+ props: { isRoot: true },
655
+ } as DeleteBy<Probe>,
656
+ carryForward: null,
657
+ };
658
+
659
+ await (ProbeService as any)["onDeleteSuccess"](onDelete, [
660
+ PROBE_ID,
661
+ SECOND_PROBE_ID,
662
+ ]);
663
+
664
+ expect(mockSetString).toHaveBeenCalledTimes(2);
665
+ expect(mockSetString).toHaveBeenCalledWith(
666
+ AUTH_CACHE_NAMESPACE,
667
+ PROBE_ID.toString(),
668
+ AUTH_CACHE_REVOKED_SENTINEL,
669
+ { expiresInSeconds: AUTH_CACHE_TTL_IN_SECONDS },
670
+ );
671
+ expect(mockSetString).toHaveBeenCalledWith(
672
+ AUTH_CACHE_NAMESPACE,
673
+ SECOND_PROBE_ID.toString(),
674
+ AUTH_CACHE_REVOKED_SENTINEL,
675
+ { expiresInSeconds: AUTH_CACHE_TTL_IN_SECONDS },
676
+ );
677
+ });
678
+ });
679
+ });
@@ -858,7 +858,7 @@ describe("ServiceLevelObjectiveService.onCreateSuccess - burn rate rule seeding
858
858
  beforeEach(() => {
859
859
  updateOneByIdSpy = jest
860
860
  .spyOn(ServiceLevelObjectiveService, "updateOneById")
861
- .mockResolvedValue(undefined);
861
+ .mockResolvedValue(1);
862
862
 
863
863
  const severity: AlertSeverity = new AlertSeverity();
864
864
  severity._id = SEVERITY_ID.toString();
@@ -1143,7 +1143,7 @@ describe("ServiceLevelObjectiveService.onCreateSuccess - default alert severity"
1143
1143
  beforeEach(() => {
1144
1144
  jest
1145
1145
  .spyOn(ServiceLevelObjectiveService, "updateOneById")
1146
- .mockResolvedValue(undefined);
1146
+ .mockResolvedValue(1);
1147
1147
 
1148
1148
  burnRateRuleCreateSpy = jest
1149
1149
  .spyOn(ServiceLevelObjectiveBurnRateRuleService, "create")
@@ -1234,7 +1234,7 @@ describe("ServiceLevelObjectiveService.onUpdateSuccess", () => {
1234
1234
 
1235
1235
  updateOneByIdSpy = jest
1236
1236
  .spyOn(ServiceLevelObjectiveService, "updateOneById")
1237
- .mockResolvedValue(undefined);
1237
+ .mockResolvedValue(1);
1238
1238
 
1239
1239
  findOneByIdSpy = jest
1240
1240
  .spyOn(ServiceLevelObjectiveService, "findOneById")