@oneuptime/common 12.0.11 → 12.0.13

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 (251) hide show
  1. package/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.ts +32 -5
  2. package/Models/DatabaseModels/NetworkDevice.ts +64 -0
  3. package/Models/DatabaseModels/StatusPage.ts +59 -0
  4. package/Models/DatabaseModels/StatusPageOidc.ts +2 -0
  5. package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +19 -2
  6. package/Server/API/AIChatAPI.ts +55 -56
  7. package/Server/API/CommonAPI.ts +89 -0
  8. package/Server/API/MicrosoftTeamsAPI.ts +20 -9
  9. package/Server/API/SlackAPI.ts +13 -10
  10. package/Server/API/StatusPageAPI.ts +2197 -2128
  11. package/Server/API/TelemetryAPI.ts +72 -3
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1787500000000-AddEnableSearchEngineIndexingToStatusPage.ts +30 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1787600000000-AddNetworkDeviceReachabilityColumns.ts +47 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  15. package/Server/Middleware/UserAuthorization.ts +11 -2
  16. package/Server/Services/ApiKeyPermissionService.ts +116 -2
  17. package/Server/Services/DatabaseService.ts +17 -6
  18. package/Server/Services/LogAggregationService.ts +34 -0
  19. package/Server/Services/LogService.ts +21 -0
  20. package/Server/Services/NetworkSiteService.ts +12 -0
  21. package/Server/Services/SpanService.ts +21 -0
  22. package/Server/Services/TraceAggregationService.ts +15 -0
  23. package/Server/Services/WorkspaceNotificationRuleService.ts +172 -220
  24. package/Server/Types/AnalyticsDatabase/ModelPermission.ts +9 -0
  25. package/Server/Types/Database/Permissions/AccessControlPermission.ts +47 -16
  26. package/Server/Types/Workflow/Components/Conditions/IfElse.ts +9 -0
  27. package/Server/Types/Workflow/Components/JavaScript.ts +9 -0
  28. package/Server/Utils/APIKey/AccessPermission.ts +16 -40
  29. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +144 -0
  30. package/Server/Utils/LogRedaction.ts +576 -0
  31. package/Server/Utils/Logger.ts +123 -46
  32. package/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.ts +94 -23
  33. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +12 -0
  34. package/Server/Utils/Monitor/MonitorResource.ts +29 -9
  35. package/Server/Utils/Monitor/NetworkInventoryUtil.ts +28 -7
  36. package/Server/Utils/SessionReplay/SessionReplayGateCacheStore.ts +56 -10
  37. package/Server/Utils/StartServer.ts +30 -9
  38. package/Server/Utils/StatusPageSearchEngineIndexing.ts +33 -0
  39. package/Server/Utils/Telemetry/ResourceEntityFilter.ts +441 -0
  40. package/Server/Utils/VM/VMRunner.ts +693 -874
  41. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +488 -46
  42. package/Tests/App/Dashboard/MonitorTypePicker.test.tsx +300 -0
  43. package/Tests/App/StatusPage/StatusPageIndexPageRobotsMeta.test.ts +130 -0
  44. package/Tests/Models/DatabaseModels/DatabaseBaseModelToJSONObjectCache.test.ts +180 -0
  45. package/Tests/Models/StatusPageEnableSearchEngineIndexing.test.ts +157 -0
  46. package/Tests/Server/API/AIChatCancelAndFeedback.test.ts +23 -1
  47. package/Tests/Server/API/AIChatRouteAuthorization.test.ts +785 -0
  48. package/Tests/Server/API/BaseAPIApiKeyAuth.test.ts +21 -8
  49. package/Tests/Server/API/CommonAPIAuthGuard.test.ts +290 -0
  50. package/Tests/Server/API/MicrosoftTeamsManifest.test.ts +7 -1
  51. package/Tests/Server/API/StatusPageOverviewCache.test.ts +474 -0
  52. package/Tests/Server/API/StatusPageSeoSearchEngineIndexing.test.ts +189 -0
  53. package/Tests/Server/Infrastructure/InMemoryTTLCache.test.ts +158 -0
  54. package/Tests/Server/Infrastructure/Postgres/DeviceRoleAndDeclaredLinkParentMigration.test.ts +9 -60
  55. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +24 -15
  56. package/Tests/Server/Middleware/UserAuthorization.test.ts +79 -0
  57. package/Tests/Server/Services/AddNetworkDeviceReachabilityColumnsMigration.test.ts +332 -0
  58. package/Tests/Server/Services/ApiKeyPermissionService.test.ts +347 -0
  59. package/Tests/Server/Services/DatabaseServiceUpdateDebugLogging.test.ts +282 -0
  60. package/Tests/Server/Services/NetworkSiteService.test.ts +56 -2
  61. package/Tests/Server/Services/TelemetryResourceFacetFilters.test.ts +267 -0
  62. package/Tests/Server/Services/WorkspaceNotificationRuleTestRuleChannels.test.ts +702 -0
  63. package/Tests/Server/Types/Database/Permissions/AccessControlPermission.test.ts +165 -0
  64. package/Tests/Server/Types/Workflow/Components/CustomCodeScriptError.test.ts +121 -0
  65. package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +48 -36
  66. package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +213 -0
  67. package/Tests/Server/Utils/LogRedaction.test.ts +415 -0
  68. package/Tests/Server/Utils/LoggerCredentialLeak.test.ts +245 -0
  69. package/Tests/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.test.ts +374 -4
  70. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorJavascriptExpression.test.ts +92 -0
  71. package/Tests/Server/Utils/Monitor/MonitorResourceIngestedStepSelection.test.ts +455 -0
  72. package/Tests/Server/Utils/Monitor/NetworkDeviceReachabilityRoundTrip.test.ts +369 -0
  73. package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +86 -8
  74. package/Tests/Server/Utils/PrivacyFilterUtil.test.ts +64 -0
  75. package/Tests/Server/Utils/SessionReplay/SessionReplayGateCacheStore.test.ts +207 -0
  76. package/Tests/Server/Utils/StartServerBodyVerify.test.ts +335 -0
  77. package/Tests/Server/Utils/StatusPageSearchEngineIndexing.test.ts +60 -0
  78. package/Tests/Server/Utils/Telemetry/ResourceEntityFilter.test.ts +435 -0
  79. package/Tests/Server/Utils/VM/VMRunnerIsolation.test.ts +385 -0
  80. package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +3 -0
  81. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChannelSend.test.ts +607 -62
  82. package/Tests/Server/Utils/Workspace/MicrosoftTeamsInstalledTeamIdSpace.test.ts +778 -0
  83. package/Tests/Server/Utils/Workspace/MicrosoftTeamsTeamInstalls.test.ts +44 -10
  84. package/Tests/Types/API/HostnameFromAuthority.test.ts +164 -0
  85. package/Tests/Types/Database/AccessControl/ColumnAccessControlCache.test.ts +344 -0
  86. package/Tests/Types/Database/DatabasePropertyFindOperator.test.ts +518 -0
  87. package/Tests/Types/Database/TableColumnMetadataCache.test.ts +286 -0
  88. package/Tests/Types/Monitor/MonitorStep.test.ts +100 -0
  89. package/Tests/Types/Monitor/MonitorTypeKeywords.test.ts +360 -0
  90. package/Tests/Types/StatusPage/SearchEngineIndexing.test.ts +86 -0
  91. package/Tests/Types/Telemetry/ResourceEntityFacet.test.ts +254 -0
  92. package/Tests/UI/Components/CardSelect.test.tsx +1021 -0
  93. package/Tests/UI/Components/MasterPage.test.tsx +36 -1
  94. package/Tests/Utils/Monitor/MonitorMetricType.test.ts +66 -1
  95. package/Tests/Utils/Monitor/NetworkTopologyUtil.test.ts +115 -6
  96. package/Tests/Utils/NetworkDevice/DeviceReachabilityUtil.test.ts +610 -0
  97. package/Tests/Utils/NetworkSite/SiteStatusRollupUtil.test.ts +114 -45
  98. package/Types/API/Hostname.ts +79 -0
  99. package/Types/Database/AccessControl/ColumnAccessControl.ts +33 -11
  100. package/Types/Database/AccessControl/ColumnBillingAccessControl.ts +33 -11
  101. package/Types/Database/DatabaseProperty.ts +38 -0
  102. package/Types/Database/TableColumn.ts +34 -7
  103. package/Types/Events/Recurring.ts +16 -1
  104. package/Types/Monitor/CustomCodeMonitor/CustomCodeMonitorResponse.ts +14 -2
  105. package/Types/Monitor/MonitorStep.ts +13 -1
  106. package/Types/Monitor/MonitorType.ts +345 -40
  107. package/Types/Monitor/SSLMonitor/SslMonitorResponse.ts +43 -0
  108. package/Types/StatusPage/SearchEngineIndexing.ts +61 -0
  109. package/Types/Telemetry/ResourceEntityFacet.ts +211 -0
  110. package/UI/Components/CardSelect/CardSelect.tsx +714 -105
  111. package/UI/Components/EmptyState/EmptyState.tsx +8 -1
  112. package/UI/Components/Footer/Footer.tsx +32 -6
  113. package/UI/Components/Forms/Fields/FormField.tsx +3 -0
  114. package/UI/Components/Forms/Types/Field.ts +8 -0
  115. package/UI/Components/Loader/PageLoader.tsx +29 -8
  116. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +26 -4
  117. package/UI/Components/Markdown.tsx/LazyMarkdownViewer.tsx +17 -1
  118. package/UI/Components/MasterPage/MasterPage.tsx +24 -11
  119. package/Utils/Monitor/MonitorMetricType.ts +16 -1
  120. package/Utils/Monitor/NetworkTopologyUtil.ts +60 -8
  121. package/Utils/NetworkDevice/DeviceReachabilityUtil.ts +262 -0
  122. package/Utils/NetworkSite/SiteStatusRollupUtil.ts +41 -24
  123. package/Utils/Telemetry/CrossSignalScope.ts +75 -4
  124. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js +20 -4
  125. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js.map +1 -1
  126. package/build/dist/Models/DatabaseModels/NetworkDevice.js +66 -0
  127. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  128. package/build/dist/Models/DatabaseModels/StatusPage.js +60 -0
  129. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  130. package/build/dist/Models/DatabaseModels/StatusPageOidc.js +2 -0
  131. package/build/dist/Models/DatabaseModels/StatusPageOidc.js.map +1 -1
  132. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
  133. package/build/dist/Server/API/AIChatAPI.js +48 -35
  134. package/build/dist/Server/API/AIChatAPI.js.map +1 -1
  135. package/build/dist/Server/API/CommonAPI.js +59 -0
  136. package/build/dist/Server/API/CommonAPI.js.map +1 -1
  137. package/build/dist/Server/API/MicrosoftTeamsAPI.js +20 -6
  138. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  139. package/build/dist/Server/API/SlackAPI.js +13 -10
  140. package/build/dist/Server/API/SlackAPI.js.map +1 -1
  141. package/build/dist/Server/API/StatusPageAPI.js +745 -673
  142. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  143. package/build/dist/Server/API/TelemetryAPI.js +34 -3
  144. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  145. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787500000000-AddEnableSearchEngineIndexingToStatusPage.js +23 -0
  146. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787500000000-AddEnableSearchEngineIndexingToStatusPage.js.map +1 -0
  147. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787600000000-AddNetworkDeviceReachabilityColumns.js +34 -0
  148. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787600000000-AddNetworkDeviceReachabilityColumns.js.map +1 -0
  149. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  150. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  151. package/build/dist/Server/Middleware/UserAuthorization.js +6 -2
  152. package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
  153. package/build/dist/Server/Services/ApiKeyPermissionService.js +90 -1
  154. package/build/dist/Server/Services/ApiKeyPermissionService.js.map +1 -1
  155. package/build/dist/Server/Services/DatabaseService.js +15 -6
  156. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  157. package/build/dist/Server/Services/LogAggregationService.js +2 -0
  158. package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
  159. package/build/dist/Server/Services/LogService.js +16 -0
  160. package/build/dist/Server/Services/LogService.js.map +1 -1
  161. package/build/dist/Server/Services/NetworkSiteService.js +12 -0
  162. package/build/dist/Server/Services/NetworkSiteService.js.map +1 -1
  163. package/build/dist/Server/Services/SpanService.js +16 -0
  164. package/build/dist/Server/Services/SpanService.js.map +1 -1
  165. package/build/dist/Server/Services/TraceAggregationService.js +2 -0
  166. package/build/dist/Server/Services/TraceAggregationService.js.map +1 -1
  167. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js +137 -164
  168. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js.map +1 -1
  169. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js +9 -0
  170. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
  171. package/build/dist/Server/Types/Database/Permissions/AccessControlPermission.js +21 -13
  172. package/build/dist/Server/Types/Database/Permissions/AccessControlPermission.js.map +1 -1
  173. package/build/dist/Server/Types/Workflow/Components/Conditions/IfElse.js +8 -0
  174. package/build/dist/Server/Types/Workflow/Components/Conditions/IfElse.js.map +1 -1
  175. package/build/dist/Server/Types/Workflow/Components/JavaScript.js +8 -0
  176. package/build/dist/Server/Types/Workflow/Components/JavaScript.js.map +1 -1
  177. package/build/dist/Server/Utils/APIKey/AccessPermission.js +9 -32
  178. package/build/dist/Server/Utils/APIKey/AccessPermission.js.map +1 -1
  179. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +87 -0
  180. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  181. package/build/dist/Server/Utils/LogRedaction.js +449 -0
  182. package/build/dist/Server/Utils/LogRedaction.js.map +1 -0
  183. package/build/dist/Server/Utils/Logger.js +98 -45
  184. package/build/dist/Server/Utils/Logger.js.map +1 -1
  185. package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js +65 -13
  186. package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js.map +1 -1
  187. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +12 -1
  188. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  189. package/build/dist/Server/Utils/Monitor/MonitorResource.js +20 -3
  190. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  191. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +27 -7
  192. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
  193. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCacheStore.js +29 -10
  194. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCacheStore.js.map +1 -1
  195. package/build/dist/Server/Utils/StartServer.js +10 -7
  196. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  197. package/build/dist/Server/Utils/StatusPageSearchEngineIndexing.js +24 -0
  198. package/build/dist/Server/Utils/StatusPageSearchEngineIndexing.js.map +1 -0
  199. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js +311 -0
  200. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js.map +1 -0
  201. package/build/dist/Server/Utils/VM/VMRunner.js +402 -525
  202. package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
  203. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +366 -37
  204. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  205. package/build/dist/Types/API/Hostname.js +63 -0
  206. package/build/dist/Types/API/Hostname.js.map +1 -1
  207. package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js +20 -6
  208. package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js.map +1 -1
  209. package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js +20 -6
  210. package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js.map +1 -1
  211. package/build/dist/Types/Database/DatabaseProperty.js +38 -0
  212. package/build/dist/Types/Database/DatabaseProperty.js.map +1 -1
  213. package/build/dist/Types/Database/TableColumn.js +24 -6
  214. package/build/dist/Types/Database/TableColumn.js.map +1 -1
  215. package/build/dist/Types/Events/Recurring.js +13 -1
  216. package/build/dist/Types/Events/Recurring.js.map +1 -1
  217. package/build/dist/Types/Monitor/MonitorStep.js +9 -1
  218. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  219. package/build/dist/Types/Monitor/MonitorType.js +330 -31
  220. package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
  221. package/build/dist/Types/StatusPage/SearchEngineIndexing.js +55 -0
  222. package/build/dist/Types/StatusPage/SearchEngineIndexing.js.map +1 -0
  223. package/build/dist/Types/Telemetry/ResourceEntityFacet.js +156 -0
  224. package/build/dist/Types/Telemetry/ResourceEntityFacet.js.map +1 -0
  225. package/build/dist/UI/Components/CardSelect/CardSelect.js +360 -40
  226. package/build/dist/UI/Components/CardSelect/CardSelect.js.map +1 -1
  227. package/build/dist/UI/Components/EmptyState/EmptyState.js +1 -1
  228. package/build/dist/UI/Components/EmptyState/EmptyState.js.map +1 -1
  229. package/build/dist/UI/Components/Footer/Footer.js +7 -4
  230. package/build/dist/UI/Components/Footer/Footer.js.map +1 -1
  231. package/build/dist/UI/Components/Forms/Fields/FormField.js +1 -1
  232. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  233. package/build/dist/UI/Components/Loader/PageLoader.js +15 -5
  234. package/build/dist/UI/Components/Loader/PageLoader.js.map +1 -1
  235. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +16 -3
  236. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  237. package/build/dist/UI/Components/Markdown.tsx/LazyMarkdownViewer.js +13 -1
  238. package/build/dist/UI/Components/Markdown.tsx/LazyMarkdownViewer.js.map +1 -1
  239. package/build/dist/UI/Components/MasterPage/MasterPage.js +10 -2
  240. package/build/dist/UI/Components/MasterPage/MasterPage.js.map +1 -1
  241. package/build/dist/Utils/Monitor/MonitorMetricType.js +13 -1
  242. package/build/dist/Utils/Monitor/MonitorMetricType.js.map +1 -1
  243. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +34 -7
  244. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  245. package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js +177 -0
  246. package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js.map +1 -0
  247. package/build/dist/Utils/NetworkSite/SiteStatusRollupUtil.js +23 -27
  248. package/build/dist/Utils/NetworkSite/SiteStatusRollupUtil.js.map +1 -1
  249. package/build/dist/Utils/Telemetry/CrossSignalScope.js +44 -4
  250. package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -1
  251. package/package.json +1 -1
@@ -6,6 +6,11 @@ import SiteStatusRollupUtil, {
6
6
  /*
7
7
  * MonitorStatus priority is HIGHER = WORSE (seeded: Operational 1,
8
8
  * Degraded 2, Offline 3).
9
+ *
10
+ * The SNMP half of this rollup now asks DeviceReachabilityUtil — "did the
11
+ * last poll succeed" — rather than "was the last success recent". That is
12
+ * what stops a site card going red because its probe is behind on a large
13
+ * fleet while every device under it is answering (issue #3220).
9
14
  */
10
15
  const OPERATIONAL: RollupStatusOption = {
11
16
  monitorStatusId: "status-operational",
@@ -26,12 +31,31 @@ function minutesAgo(minutes: number): Date {
26
31
  return new Date(NOW.getTime() - minutes * 60 * 1000);
27
32
  }
28
33
 
34
+ // A device whose last poll succeeded `minutes` ago.
35
+ function answered(minutes: number): DeviceHealthState {
36
+ return {
37
+ isReachable: true,
38
+ lastPolledAt: minutesAgo(minutes),
39
+ lastSeenAt: minutesAgo(minutes),
40
+ pollingIntervalInMinutes: 5,
41
+ };
42
+ }
43
+
44
+ // A device whose last poll failed `minutes` ago.
45
+ function failed(minutes: number): DeviceHealthState {
46
+ return {
47
+ isReachable: false,
48
+ lastPolledAt: minutesAgo(minutes),
49
+ lastSeenAt: minutesAgo(minutes + 60),
50
+ pollingIntervalInMinutes: 5,
51
+ };
52
+ }
53
+
29
54
  function worst(
30
55
  deviceStates: Array<DeviceHealthState>,
31
56
  overrides?: {
32
57
  operationalStatus?: RollupStatusOption | null;
33
58
  offlineStatus?: RollupStatusOption | null;
34
- freshnessWindowInMinutes?: number;
35
59
  },
36
60
  ): string | null {
37
61
  return SiteStatusRollupUtil.worstStatus({
@@ -45,7 +69,6 @@ function worst(
45
69
  ? overrides.offlineStatus
46
70
  : OFFLINE,
47
71
  now: NOW,
48
- freshnessWindowInMinutes: overrides?.freshnessWindowInMinutes,
49
72
  });
50
73
  }
51
74
 
@@ -84,65 +107,103 @@ describe("SiteStatusRollupUtil.worstStatus", () => {
84
107
  ).toBe(OFFLINE.monitorStatusId);
85
108
  });
86
109
 
87
- it("a fresh unmonitored device maps to the operational equivalent", () => {
88
- expect(worst([{ lastSeenAt: minutesAgo(5) }])).toBe(
110
+ it("a device whose last poll succeeded maps to the operational equivalent", () => {
111
+ expect(worst([answered(5)])).toBe(OPERATIONAL.monitorStatusId);
112
+ });
113
+
114
+ it("a device whose last poll failed maps to the offline equivalent", () => {
115
+ expect(worst([failed(2)])).toBe(OFFLINE.monitorStatusId);
116
+ });
117
+
118
+ /*
119
+ * The rollup half of issue #3220. 21 minutes was the reported lag on a
120
+ * fleet the probe could not get round in 15, and every device under the
121
+ * site was answering.
122
+ */
123
+ it("issue #3220: a site whose devices are all answering, 21 minutes behind, is operational", () => {
124
+ expect(worst([answered(21), answered(19), answered(25)])).toBe(
89
125
  OPERATIONAL.monitorStatusId,
90
126
  );
91
127
  });
92
128
 
93
- it("a stale unmonitored device maps to the offline equivalent", () => {
94
- expect(worst([{ lastSeenAt: minutesAgo(30) }])).toBe(
95
- OFFLINE.monitorStatusId,
96
- );
129
+ it("a slow-polled device is operational right up to its next poll", () => {
130
+ expect(
131
+ worst([
132
+ {
133
+ isReachable: true,
134
+ lastPolledAt: minutesAgo(29),
135
+ lastSeenAt: minutesAgo(29),
136
+ pollingIntervalInMinutes: 30,
137
+ },
138
+ ]),
139
+ ).toBe(OPERATIONAL.monitorStatusId);
97
140
  });
98
141
 
99
- it("a never-seen unmonitored device maps to the offline equivalent", () => {
100
- expect(worst([{}])).toBe(OFFLINE.monitorStatusId);
101
- expect(worst([{ lastSeenAt: null }])).toBe(OFFLINE.monitorStatusId);
142
+ /*
143
+ * A site nothing has polled for hours keeps its last known status rather
144
+ * than flipping offline. Turning "nobody has checked lately" into a red
145
+ * site card is the same inference this change removed from the device
146
+ * pill — and doing it here alone would put the site card at odds with
147
+ * every device under it, which is the inconsistency issue #3220 reported.
148
+ */
149
+ it("a device out of contact past its stale window keeps its last verdict", () => {
150
+ expect(worst([answered(61)])).toBe(OPERATIONAL.monitorStatusId);
151
+ expect(worst([answered(60 * 24)])).toBe(OPERATIONAL.monitorStatusId);
102
152
  });
103
153
 
104
- it("freshness boundary: exactly 15 minutes is still fresh, beyond is not", () => {
105
- expect(worst([{ lastSeenAt: minutesAgo(15) }])).toBe(
106
- OPERATIONAL.monitorStatusId,
107
- );
108
- expect(
109
- worst([{ lastSeenAt: new Date(minutesAgo(15).getTime() - 1) }]),
110
- ).toBe(OFFLINE.monitorStatusId);
154
+ it("...but a device that actually failed its last poll still rolls up offline", () => {
155
+ expect(worst([failed(60 * 24)])).toBe(OFFLINE.monitorStatusId);
156
+ });
157
+
158
+ /*
159
+ * Changed deliberately: a never-polled device used to be counted as
160
+ * offline, which pinned a brand-new site red until its first walk landed
161
+ * — while the device list showed the very same device as a gray
162
+ * "Pending". It now contributes nothing, and the caller reads null as
163
+ * "leave the site's status alone".
164
+ */
165
+ it("a never-polled device contributes nothing at all", () => {
166
+ expect(worst([{}])).toBeNull();
167
+ expect(worst([{ isReachable: null, lastSeenAt: null }])).toBeNull();
168
+ });
169
+
170
+ it("a never-polled device does not drag down a site with healthy devices", () => {
171
+ expect(worst([answered(1), {}])).toBe(OPERATIONAL.monitorStatusId);
111
172
  });
112
173
 
113
- it("mix: a stale unmonitored device outranks a monitored operational one", () => {
174
+ it("mix: a failing device outranks a monitored operational one", () => {
114
175
  expect(
115
176
  worst([
116
177
  {
117
178
  currentMonitorStatusId: OPERATIONAL.monitorStatusId,
118
179
  monitorStatusPriority: OPERATIONAL.priority,
119
180
  },
120
- { lastSeenAt: minutesAgo(60) },
181
+ failed(1),
121
182
  ]),
122
183
  ).toBe(OFFLINE.monitorStatusId);
123
184
  });
124
185
 
125
- it("mix: a monitored offline device outranks fresh unmonitored ones", () => {
186
+ it("mix: a monitored offline device outranks answering ones", () => {
126
187
  expect(
127
188
  worst([
128
- { lastSeenAt: minutesAgo(1) },
189
+ answered(1),
129
190
  {
130
191
  currentMonitorStatusId: OFFLINE.monitorStatusId,
131
192
  monitorStatusPriority: OFFLINE.priority,
132
193
  },
133
- { lastSeenAt: minutesAgo(2) },
194
+ answered(2),
134
195
  ]),
135
196
  ).toBe(OFFLINE.monitorStatusId);
136
197
  });
137
198
 
138
- it("a stamped status whose priority is unknown falls back to freshness", () => {
139
- // The MonitorStatus row was deleted: treat the device by lastSeenAt.
199
+ it("a stamped status whose priority is unknown falls back to reachability", () => {
200
+ // The MonitorStatus row was deleted: treat the device by its last poll.
140
201
  expect(
141
202
  worst([
142
203
  {
143
204
  currentMonitorStatusId: "deleted-status",
144
205
  monitorStatusPriority: undefined,
145
- lastSeenAt: minutesAgo(1),
206
+ ...answered(1),
146
207
  },
147
208
  ]),
148
209
  ).toBe(OPERATIONAL.monitorStatusId);
@@ -151,7 +212,7 @@ describe("SiteStatusRollupUtil.worstStatus", () => {
151
212
  {
152
213
  currentMonitorStatusId: "deleted-status",
153
214
  monitorStatusPriority: null,
154
- lastSeenAt: minutesAgo(120),
215
+ ...failed(1),
155
216
  },
156
217
  ]),
157
218
  ).toBe(OFFLINE.monitorStatusId);
@@ -172,9 +233,9 @@ describe("SiteStatusRollupUtil.worstStatus", () => {
172
233
  ).toBe("status-a");
173
234
  });
174
235
 
175
- it("returns null when only freshness fallbacks apply but the project has no flagged rows", () => {
236
+ it("returns null when only SNMP fallbacks apply but the project has no flagged rows", () => {
176
237
  expect(
177
- worst([{ lastSeenAt: minutesAgo(1) }, { lastSeenAt: minutesAgo(60) }], {
238
+ worst([answered(1), failed(1)], {
178
239
  operationalStatus: null,
179
240
  offlineStatus: null,
180
241
  }),
@@ -185,7 +246,7 @@ describe("SiteStatusRollupUtil.worstStatus", () => {
185
246
  expect(
186
247
  worst(
187
248
  [
188
- { lastSeenAt: minutesAgo(60) }, // offline equivalent missing -> skipped
249
+ failed(1), // offline equivalent missing -> skipped
189
250
  {
190
251
  currentMonitorStatusId: DEGRADED.monitorStatusId,
191
252
  monitorStatusPriority: DEGRADED.priority,
@@ -196,26 +257,34 @@ describe("SiteStatusRollupUtil.worstStatus", () => {
196
257
  ).toBe(DEGRADED.monitorStatusId);
197
258
  });
198
259
 
199
- it("honors a custom freshness window", () => {
260
+ it("a lastPolledAt in the future counts as fresh, not an error", () => {
200
261
  expect(
201
- worst([{ lastSeenAt: minutesAgo(20) }], {
202
- freshnessWindowInMinutes: 30,
203
- }),
262
+ worst([
263
+ {
264
+ isReachable: true,
265
+ lastPolledAt: minutesAgo(-5),
266
+ lastSeenAt: minutesAgo(-5),
267
+ },
268
+ ]),
204
269
  ).toBe(OPERATIONAL.monitorStatusId);
205
- expect(
206
- worst([{ lastSeenAt: minutesAgo(20) }], {
207
- freshnessWindowInMinutes: 10,
208
- }),
209
- ).toBe(OFFLINE.monitorStatusId);
210
270
  });
211
271
 
212
- it("a lastSeenAt in the future counts as fresh, not an error", () => {
213
- expect(worst([{ lastSeenAt: minutesAgo(-5) }])).toBe(
272
+ /*
273
+ * Rows written before the reachability columns existed carry only
274
+ * lastSeenAt. They must keep rolling up sensibly until the upgrade
275
+ * migration (or their next walk) fills the rest in.
276
+ */
277
+ it("legacy rows with only lastSeenAt still roll up by freshness", () => {
278
+ /*
279
+ * The one place freshness survives: a row written before isReachable
280
+ * existed has nothing else to go on. The upgrade migration backfills
281
+ * these, so it is a short-lived path.
282
+ */
283
+ expect(worst([{ lastSeenAt: minutesAgo(5) }])).toBe(
214
284
  OPERATIONAL.monitorStatusId,
215
285
  );
216
- });
217
-
218
- it("exposes the default freshness window as 15 minutes", () => {
219
- expect(SiteStatusRollupUtil.DEFAULT_FRESHNESS_WINDOW_IN_MINUTES).toBe(15);
286
+ expect(worst([{ lastSeenAt: minutesAgo(600) }])).toBe(
287
+ OFFLINE.monitorStatusId,
288
+ );
220
289
  });
221
290
  });
@@ -182,6 +182,85 @@ export default class Hostname extends DatabaseProperty {
182
182
  return new Hostname(hostname);
183
183
  }
184
184
 
185
+ /*
186
+ * Splits a URL authority into its bare host and its port, using the same
187
+ * structure isValid() already understands: optional userinfo, then either
188
+ * a bracketed IPv6 literal, an unbracketed IPv6 literal, or a host with an
189
+ * optional ":port".
190
+ *
191
+ * fromString() is NOT a substitute. It splits on the FIRST colon, so it
192
+ * turns "[::1]:8080" into a host of "[" and a port of "", and any IPv6
193
+ * literal into nonsense. Callers that hold an authority (a URL's host
194
+ * component) and need the two parts apart must use this.
195
+ *
196
+ * Userinfo is dropped rather than preserved: the caller asked for a host
197
+ * to connect to, and credentials are not part of one. Anything that needs
198
+ * the original string still has the authority it passed in.
199
+ */
200
+ public static fromAuthority(authority: string): Hostname {
201
+ const trimmedAuthority: string = (authority || "").trim();
202
+
203
+ if (!trimmedAuthority) {
204
+ return new Hostname("");
205
+ }
206
+
207
+ /*
208
+ * Split on the LAST "@" for the same reason isValid() does: userinfo may
209
+ * itself contain an "@", and everything after the final one is the
210
+ * authority proper.
211
+ */
212
+ const atIndex: number = trimmedAuthority.lastIndexOf("@");
213
+ const hostAndPort: string =
214
+ atIndex === -1
215
+ ? trimmedAuthority
216
+ : trimmedAuthority.substring(atIndex + 1);
217
+
218
+ if (!hostAndPort) {
219
+ return new Hostname("");
220
+ }
221
+
222
+ // Bracketed IPv6, with or without a port: "[::1]", "[::1]:8080".
223
+ const bracketedIpv6Match: RegExpMatchArray | null = hostAndPort.match(
224
+ /^(\[[0-9a-fA-F:.]+\])(?::(\d{1,5}))?$/,
225
+ );
226
+
227
+ if (bracketedIpv6Match) {
228
+ const ipv6Host: string = bracketedIpv6Match[1] as string;
229
+ const ipv6Port: string | undefined = bracketedIpv6Match[2];
230
+
231
+ return ipv6Port
232
+ ? new Hostname(ipv6Host, ipv6Port)
233
+ : new Hostname(ipv6Host);
234
+ }
235
+
236
+ /*
237
+ * Unbracketed IPv6 literal: two or more colons cannot be a "host:port",
238
+ * so every colon belongs to the address itself and there is no port.
239
+ */
240
+ if ((hostAndPort.match(/:/g) || []).length > 1) {
241
+ return new Hostname(hostAndPort);
242
+ }
243
+
244
+ const colonIndex: number = hostAndPort.indexOf(":");
245
+
246
+ if (colonIndex === -1) {
247
+ return new Hostname(hostAndPort);
248
+ }
249
+
250
+ const host: string = hostAndPort.substring(0, colonIndex);
251
+ const port: string = hostAndPort.substring(colonIndex + 1);
252
+
253
+ /*
254
+ * A trailing colon with no digits ("host:") is not a port. Keep the host
255
+ * and drop the empty port rather than constructing an invalid Port.
256
+ */
257
+ if (!port) {
258
+ return new Hostname(host);
259
+ }
260
+
261
+ return new Hostname(host, port);
262
+ }
263
+
185
264
  public static override toDatabase(
186
265
  value: Hostname | FindOperator<Hostname>,
187
266
  ): string | null {
@@ -30,20 +30,42 @@ type GetColumnAccessControlForAllColumnsFunction = <T extends BaseModel>(
30
30
  target: T,
31
31
  ) => Dictionary<ColumnAccessControl>;
32
32
 
33
+ /*
34
+ * Per-class cache. Safe to key on the constructor because every decorated
35
+ * column is initialised to `undefined` in the class body, so Object.keys()
36
+ * is identical for every instance of a class (see OwnerOnlyColumn.ts).
37
+ * Callers must treat the returned dictionary as read-only.
38
+ */
39
+ const accessControlCache: WeakMap<
40
+ { new (): BaseModel },
41
+ Dictionary<ColumnAccessControl>
42
+ > = new WeakMap();
43
+
33
44
  export const getColumnAccessControlForAllColumns: GetColumnAccessControlForAllColumnsFunction =
34
45
  <T extends BaseModel>(target: T): Dictionary<ColumnAccessControl> => {
35
- const dictonary: Dictionary<ColumnAccessControl> = {};
36
- const keys: Array<string> = Object.keys(target);
37
-
38
- for (const key of keys) {
39
- if (Reflect.getMetadata(accessControlSymbol, target, key)) {
40
- dictonary[key] = Reflect.getMetadata(
41
- accessControlSymbol,
42
- target,
43
- key,
44
- ) as ColumnAccessControl;
46
+ const modelClass: { new (): BaseModel } = target.constructor as {
47
+ new (): BaseModel;
48
+ };
49
+ let cached: Dictionary<ColumnAccessControl> | undefined =
50
+ accessControlCache.get(modelClass);
51
+
52
+ if (!cached) {
53
+ const dictonary: Dictionary<ColumnAccessControl> = {};
54
+ const keys: Array<string> = Object.keys(target);
55
+
56
+ for (const key of keys) {
57
+ const accessControl: ColumnAccessControl | undefined =
58
+ Reflect.getMetadata(accessControlSymbol, target, key) as
59
+ | ColumnAccessControl
60
+ | undefined;
61
+ if (accessControl) {
62
+ dictonary[key] = accessControl;
63
+ }
45
64
  }
65
+
66
+ cached = dictonary;
67
+ accessControlCache.set(modelClass, cached);
46
68
  }
47
69
 
48
- return dictonary;
70
+ return cached;
49
71
  };
@@ -30,20 +30,42 @@ type GetColumnBillingAccessControlForAllColumnsFunction = <T extends BaseModel>(
30
30
  target: T,
31
31
  ) => Dictionary<ColumnBillingAccessControl>;
32
32
 
33
+ /*
34
+ * Per-class cache. Safe to key on the constructor because every decorated
35
+ * column is initialised to `undefined` in the class body, so Object.keys()
36
+ * is identical for every instance of a class (see OwnerOnlyColumn.ts).
37
+ * Callers must treat the returned dictionary as read-only.
38
+ */
39
+ const billingAccessControlCache: WeakMap<
40
+ { new (): BaseModel },
41
+ Dictionary<ColumnBillingAccessControl>
42
+ > = new WeakMap();
43
+
33
44
  export const getColumnBillingAccessControlForAllColumns: GetColumnBillingAccessControlForAllColumnsFunction =
34
45
  <T extends BaseModel>(target: T): Dictionary<ColumnBillingAccessControl> => {
35
- const dictonary: Dictionary<ColumnBillingAccessControl> = {};
36
- const keys: Array<string> = Object.keys(target);
37
-
38
- for (const key of keys) {
39
- if (Reflect.getMetadata(accessControlSymbol, target, key)) {
40
- dictonary[key] = Reflect.getMetadata(
41
- accessControlSymbol,
42
- target,
43
- key,
44
- ) as ColumnBillingAccessControl;
46
+ const modelClass: { new (): BaseModel } = target.constructor as {
47
+ new (): BaseModel;
48
+ };
49
+ let cached: Dictionary<ColumnBillingAccessControl> | undefined =
50
+ billingAccessControlCache.get(modelClass);
51
+
52
+ if (!cached) {
53
+ const dictonary: Dictionary<ColumnBillingAccessControl> = {};
54
+ const keys: Array<string> = Object.keys(target);
55
+
56
+ for (const key of keys) {
57
+ const accessControl: ColumnBillingAccessControl | undefined =
58
+ Reflect.getMetadata(accessControlSymbol, target, key) as
59
+ | ColumnBillingAccessControl
60
+ | undefined;
61
+ if (accessControl) {
62
+ dictonary[key] = accessControl;
63
+ }
45
64
  }
65
+
66
+ cached = dictonary;
67
+ billingAccessControlCache.set(modelClass, cached);
46
68
  }
47
69
 
48
- return dictonary;
70
+ return cached;
49
71
  };
@@ -39,6 +39,44 @@ export default class DatabaseProperty extends SerializableObject {
39
39
  return value as any;
40
40
  }
41
41
 
42
+ /*
43
+ * A FindOperator reaches a transformer when the query value is an
44
+ * operator rather than a plain value. Two ways in:
45
+ *
46
+ * - Nested. `And(Equal(id), Raw(privacyClause))` — the shape every
47
+ * privacy/scope filter builds (Server/Utils/PrivacyFilterUtil.ts).
48
+ * TypeORM calls `operator.transformValue(column.transformer)`
49
+ * (SelectQueryBuilder.buildWhere), and FindOperator.transformValue
50
+ * hands each CHILD OPERATOR to this function whenever the operator
51
+ * carries multiple parameters — which And() and Or() both do.
52
+ * - Top level. ColumnMetadata.getEntityValue applies the transformer to
53
+ * the whole criteria of a .where()/.update()/.delete() call.
54
+ *
55
+ * Every toDatabase() below expects a value, so an operator makes it
56
+ * produce garbage: "[object Object]" from the string-ish types (ObjectID
57
+ * included — a uuid column then fails with Postgres 22P02 invalid input
58
+ * syntax for type uuid), `null` from Port, a serialized operator from the
59
+ * JSON types. The child is REPLACED by that garbage, so the caller's own
60
+ * predicate is lost and the privacy clause it was ANDed with can no
61
+ * longer match anything.
62
+ *
63
+ * Transform the operator's leaf value(s) in place and hand the operator
64
+ * back, so `And`/`Or`/`In`/`Not`/`Between` keep their structure and only
65
+ * the leaves are converted. Recursion terminates because transformValue
66
+ * always descends into the operator's value, and a leaf is never a
67
+ * FindOperator.
68
+ *
69
+ * NOTE: transformValue mutates. One query object can legitimately be
70
+ * transformed more than once — BaseAPI.getList shares it between findBy
71
+ * and countBy — so every toDatabase() has to be a fixpoint: handed its
72
+ * own output it must return that output unchanged. Tests/Types/Database/
73
+ * DatabasePropertyFindOperator.test.ts enforces that for each type.
74
+ */
75
+ if (value instanceof FindOperator) {
76
+ value.transformValue(this.getDatabaseTransformer());
77
+ return value as any;
78
+ }
79
+
42
80
  return this.toDatabase(value);
43
81
  }
44
82
 
@@ -63,21 +63,48 @@ type GetTableColumnsFunction = <T extends BaseModel>(
63
63
  target: T,
64
64
  ) => Dictionary<TableColumnMetadata>;
65
65
 
66
+ /*
67
+ * Per-class cache. Safe to key on the constructor because every decorated
68
+ * column is initialised to `undefined` in the class body, so Object.keys()
69
+ * is identical for every instance of a class (see OwnerOnlyColumn.ts).
70
+ */
71
+ const tableColumnsCache: WeakMap<
72
+ { new (): BaseModel },
73
+ Dictionary<TableColumnMetadata>
74
+ > = new WeakMap();
75
+
66
76
  export const getTableColumns: GetTableColumnsFunction = <T extends BaseModel>(
67
77
  target: T,
68
78
  ): Dictionary<TableColumnMetadata> => {
69
- const dictonary: Dictionary<TableColumnMetadata> = {};
70
- const keys: Array<string> = Object.keys(target);
79
+ const modelClass: { new (): BaseModel } = target.constructor as {
80
+ new (): BaseModel;
81
+ };
82
+ let cached: Dictionary<TableColumnMetadata> | undefined =
83
+ tableColumnsCache.get(modelClass);
84
+
85
+ if (!cached) {
86
+ const dictonary: Dictionary<TableColumnMetadata> = {};
87
+ const keys: Array<string> = Object.keys(target);
71
88
 
72
- for (const key of keys) {
73
- if (Reflect.getMetadata(tableColumn, target, key)) {
74
- dictonary[key] = Reflect.getMetadata(
89
+ for (const key of keys) {
90
+ const metadata: TableColumnMetadata | undefined = Reflect.getMetadata(
75
91
  tableColumn,
76
92
  target,
77
93
  key,
78
- ) as TableColumnMetadata;
94
+ ) as TableColumnMetadata | undefined;
95
+ if (metadata) {
96
+ dictonary[key] = metadata;
97
+ }
79
98
  }
99
+
100
+ cached = dictonary;
101
+ tableColumnsCache.set(modelClass, cached);
80
102
  }
81
103
 
82
- return dictonary;
104
+ /*
105
+ * Hand out a copy, not the cached dictionary itself: callers mutate the
106
+ * result (the API reference docs delete entries from it), which would
107
+ * otherwise strip columns from every later call for the same class.
108
+ */
109
+ return { ...cached };
83
110
  };
@@ -313,7 +313,22 @@ export default class Recurring extends DatabaseProperty {
313
313
  value: Recurring | Array<Recurring> | FindOperator<Recurring>,
314
314
  ): JSONObject | Array<JSONObject> | null {
315
315
  if (Array.isArray(value)) {
316
- return this.toJSONArray(value as Array<Recurring>);
316
+ /*
317
+ * Per item rather than through toJSONArray, which assumes every item is
318
+ * still a Recurring instance and throws "item.toJSON is not a function"
319
+ * on an already-serialized array. TypeORM can hand a transformer its own
320
+ * output — BaseAPI.getList shares one query object between findBy and
321
+ * countBy — so this has to be a fixpoint.
322
+ */
323
+ return (value as Array<Recurring | JSONObject>).map(
324
+ (item: Recurring | JSONObject) => {
325
+ if (item instanceof Recurring) {
326
+ return item.toJSON();
327
+ }
328
+
329
+ return JSONFunctions.serialize(item as JSONObject);
330
+ },
331
+ );
317
332
  }
318
333
 
319
334
  if (value && value instanceof Recurring) {
@@ -1,5 +1,17 @@
1
1
  import CapturedMetric from "./CapturedMetric";
2
- import { JSONObject } from "../../JSON";
2
+
3
+ export interface CustomCodeMonitorResultObject {
4
+ [key: string]: CustomCodeMonitorResult;
5
+ }
6
+
7
+ /** JSON-safe data returned from a custom or synthetic monitor script. */
8
+ export type CustomCodeMonitorResult =
9
+ | string
10
+ | number
11
+ | boolean
12
+ | null
13
+ | CustomCodeMonitorResultObject
14
+ | Array<CustomCodeMonitorResult>;
3
15
 
4
16
  export interface RetryAttempt {
5
17
  attemptNumber: number;
@@ -8,7 +20,7 @@ export interface RetryAttempt {
8
20
  }
9
21
 
10
22
  export default interface CustomCodeMonitorResponse {
11
- result: string | number | boolean | JSONObject | undefined;
23
+ result: CustomCodeMonitorResult | undefined;
12
24
  scriptError?: string | undefined;
13
25
  logMessages: string[];
14
26
  capturedMetrics: CapturedMetric[];
@@ -1156,8 +1156,20 @@ export default class MonitorStep extends DatabaseProperty {
1156
1156
  ),
1157
1157
  )
1158
1158
  : undefined,
1159
+ /*
1160
+ * Normalized for the same reason domainMonitor above is: a config
1161
+ * authored through the API or a template need not carry `resolvers`,
1162
+ * and the probe iterates that list without a guard. A raw passthrough
1163
+ * therefore threw a TypeError inside the probe BEFORE it posted any
1164
+ * result, so the monitor produced nothing at all - no check, no
1165
+ * status change, no error the user could see.
1166
+ */
1159
1167
  dnssecMonitor: json["dnssecMonitor"]
1160
- ? (json["dnssecMonitor"] as JSONObject)
1168
+ ? MonitorStepDnssecMonitorUtil.toJSON(
1169
+ MonitorStepDnssecMonitorUtil.fromJSON(
1170
+ json["dnssecMonitor"] as JSONObject,
1171
+ ),
1172
+ )
1161
1173
  : undefined,
1162
1174
  sqlMonitor: json["sqlMonitor"]
1163
1175
  ? (json["sqlMonitor"] as JSONObject)