@oneuptime/common 12.0.23 → 12.0.25

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 (323) hide show
  1. package/Models/AnalyticsModels/Span.ts +101 -0
  2. package/Models/DatabaseModels/DetectionRule.ts +55 -0
  3. package/Models/DatabaseModels/NetworkDeviceAutoImportRule.ts +27 -7
  4. package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +67 -0
  5. package/Models/DatabaseModels/Project.ts +68 -0
  6. package/Models/DatabaseModels/User.ts +68 -0
  7. package/Server/API/BaseAPI.ts +0 -24
  8. package/Server/API/SlackAPI.ts +0 -2
  9. package/Server/API/TelemetryAPI.ts +29 -0
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.ts +28 -0
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.ts +49 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.ts +33 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  14. package/Server/Middleware/GzipRequestBody.ts +311 -0
  15. package/Server/Middleware/MultipartFormData.ts +87 -3
  16. package/Server/Middleware/SlackAuthorization.ts +96 -18
  17. package/Server/Services/DetectionRuleService.ts +73 -6
  18. package/Server/Services/Index.ts +205 -0
  19. package/Server/Services/NetworkDeviceDiscoveryScanService.ts +59 -0
  20. package/Server/Services/NetworkDeviceService.ts +148 -0
  21. package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +65 -0
  22. package/Server/Services/OnCallDutyPolicyFeedService.ts +1 -1
  23. package/Server/Services/ProjectService.ts +29 -19
  24. package/Server/Services/SecurityEventService.ts +90 -13
  25. package/Server/Services/StatusPageResourceService.ts +223 -0
  26. package/Server/Services/TelemetryAttributeService.ts +17 -0
  27. package/Server/Services/UserService.ts +4 -6
  28. package/Server/Types/Markdown.ts +161 -67
  29. package/Server/Utils/AI/Toolbox/OnCallTools.ts +12 -4
  30. package/Server/Utils/AnalyticsDatabase/Statement.ts +13 -1
  31. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +297 -6
  32. package/Server/Utils/Attribution.ts +4 -0
  33. package/Server/Utils/Marketing/MarketingEventUtil.ts +45 -7
  34. package/Server/Utils/SecurityEvent/DetectionRuleEvaluator.ts +55 -3
  35. package/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.ts +21 -0
  36. package/Server/Utils/StartServer.ts +12 -32
  37. package/Server/Utils/Telemetry/LlmCostBudgetEvaluator.ts +101 -4
  38. package/Server/Utils/Telemetry/LlmMetricSpend.ts +181 -0
  39. package/Server/Utils/Telemetry/LlmSpan.ts +46 -0
  40. package/Server/Utils/Workspace/Slack/Actions/Auth.ts +0 -12
  41. package/Server/Views/Partials/AnalyticsConsent.ejs +159 -102
  42. package/Server/Views/Partials/Head.ejs +2 -1
  43. package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +776 -38
  44. package/Tests/App/Dashboard/CorrelateFilterBuilder.test.tsx +359 -0
  45. package/Tests/App/Dashboard/CorrelateGraph.test.tsx +865 -0
  46. package/Tests/App/Dashboard/CorrelationGraph.test.ts +346 -0
  47. package/Tests/App/Dashboard/CriteriaFilterConditionDropdown.test.tsx +256 -0
  48. package/Tests/App/Dashboard/CriteriaFilterDefaults.test.ts +738 -0
  49. package/Tests/App/Dashboard/CriteriaFilterMonitorTypeChange.test.ts +1122 -0
  50. package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +283 -0
  51. package/Tests/App/Dashboard/ExternalStatusPageCriteriaFilter.test.ts +407 -0
  52. package/Tests/App/Dashboard/LlmCallsTableIdentity.test.tsx +322 -0
  53. package/Tests/App/Dashboard/LlmOverview.test.tsx +335 -0
  54. package/Tests/App/Dashboard/LlmSpanDisplay.test.ts +391 -0
  55. package/Tests/App/Dashboard/LlmUsageBreakdown.test.tsx +1007 -0
  56. package/Tests/App/Dashboard/MetricSeriesInvestigateMenu.test.tsx +5 -14
  57. package/Tests/App/Dashboard/MonitorCriteriaActionToggles.test.tsx +426 -0
  58. package/Tests/App/Dashboard/MonitorStepsCriteriaAlignmentWiring.test.tsx +352 -0
  59. package/Tests/App/Dashboard/OnCallLayerCardOverrides.test.tsx +278 -0
  60. package/Tests/App/Dashboard/OnCallLayerShiftPreviewOverrides.test.ts +237 -0
  61. package/Tests/App/Dashboard/OnCallReadinessSurfaces.test.tsx +2 -2
  62. package/Tests/App/Dashboard/OnCallSchedulePreviewOverrides.test.tsx +460 -0
  63. package/Tests/App/Dashboard/SecurityEventAttributeUtil.test.ts +171 -0
  64. package/Tests/App/Dashboard/SecurityEventCorrelation.test.ts +857 -0
  65. package/Tests/App/Dashboard/SecurityEventDetailObservables.test.tsx +194 -0
  66. package/Tests/App/Dashboard/SecurityEventsDetectionRulesPage.test.tsx +124 -0
  67. package/Tests/App/Dashboard/SecurityEventsMonitorStepForm.test.tsx +79 -19
  68. package/Tests/App/Dashboard/SecurityEventsTableColumns.test.tsx +382 -0
  69. package/Tests/App/StatusPage/BulkAddStatusPageMonitors.test.ts +216 -0
  70. package/Tests/App/StatusPage/BulkAddStatusPageMonitorsModal.test.tsx +590 -11
  71. package/Tests/App/StatusPage/StatusPageResourceExplorer.test.tsx +757 -0
  72. package/Tests/Models/DiscoveryScanNameColumn.test.ts +293 -0
  73. package/Tests/Server/API/BaseAPI.test.ts +41 -0
  74. package/Tests/Server/API/BaseAPIUpdatePayloadValidation.test.ts +9 -16
  75. package/Tests/Server/API/SecurityEventAttributesAPI.test.ts +564 -0
  76. package/Tests/Server/Infrastructure/Postgres/SchemaMigrationsOrdering.test.ts +120 -4
  77. package/Tests/Server/Middleware/GzipRequestBody.test.ts +725 -0
  78. package/Tests/Server/Middleware/MultipartFormData.test.ts +298 -0
  79. package/Tests/Server/Middleware/SlackAuthorization.test.ts +262 -5
  80. package/Tests/Server/Services/AddDetectionRuleDistinctCountColumnsMigration.test.ts +311 -0
  81. package/Tests/Server/Services/DetectionRuleService.test.ts +134 -2
  82. package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +7 -1
  83. package/Tests/Server/Services/FeedRetentionConsistency.test.ts +131 -0
  84. package/Tests/Server/Services/NetworkDeviceDiscoveryScanService.test.ts +194 -0
  85. package/Tests/Server/Services/NetworkDeviceMonitorStatusStamp.test.ts +800 -0
  86. package/Tests/Server/Services/OnCallScheduleAttachRosterRefresh.test.ts +244 -0
  87. package/Tests/Server/Services/SecurityEventService.test.ts +376 -0
  88. package/Tests/Server/Services/StatusPageResourceDuplicates.test.ts +703 -0
  89. package/Tests/Server/Services/TelemetryAttributeServiceSecurityEvent.test.ts +256 -0
  90. package/Tests/Server/Types/Workflow/Components/ComponentRegistryParity.test.ts +417 -0
  91. package/Tests/Server/Utils/AnalyticsDatabase/StatementGeneratorWhereOperators.test.ts +677 -0
  92. package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +43 -0
  93. package/Tests/Server/Utils/Monitor/Criteria/ProfileMonitorCriteria.test.ts +15 -11
  94. package/Tests/Server/Utils/SecurityEvent/DetectionRuleEvaluator.test.ts +369 -8
  95. package/Tests/Server/Utils/SecurityEvent/SigmaClickhouseCompiler.test.ts +35 -0
  96. package/Tests/Server/Utils/Telemetry/LlmCostBudgetEvaluator.test.ts +387 -4
  97. package/Tests/Server/Utils/Telemetry/LlmMetricSpend.test.ts +386 -0
  98. package/Tests/Server/Utils/Telemetry/LlmSpan.test.ts +804 -0
  99. package/Tests/Server/Views/AnalyticsConsentPartial.test.ts +48 -5
  100. package/Tests/Types/Code/YamlSyntax.test.ts +396 -0
  101. package/Tests/Types/Monitor/MetricAndProfileDefaultCriteria.test.ts +957 -0
  102. package/Tests/Types/Monitor/MonitorCriteriaActionValidation.test.ts +598 -0
  103. package/Tests/Types/Monitor/MonitorCriteriaInstance.test.ts +49 -0
  104. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +6 -6
  105. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.test.ts +4 -4
  106. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationUtil.test.ts +4 -3
  107. package/Tests/Types/Telemetry/LlmMetricConventions.test.ts +584 -0
  108. package/Tests/UI/Components/AiInvestigationSettingsCard.test.tsx +0 -12
  109. package/Tests/UI/Components/CardModelDetailEdit.test.tsx +1 -17
  110. package/Tests/UI/Components/CodeEditor.test.tsx +474 -10
  111. package/Tests/UI/Components/Detail/YamlField.test.tsx +105 -0
  112. package/Tests/UI/Components/EntityDropdownLabelsBulkAdd.test.tsx +319 -0
  113. package/Tests/UI/Components/Forms/FormFieldYaml.test.tsx +231 -0
  114. package/Tests/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.test.ts +1 -0
  115. package/Tests/UI/Components/Forms/ValidationYAML.test.ts +234 -0
  116. package/Tests/UI/Components/ModelTable/AttributeColumns.test.tsx +573 -0
  117. package/Tests/UI/Components/ModelTable/BaseModelTableAttributeColumns.test.tsx +824 -0
  118. package/Tests/UI/Components/ModelTable/ColumnCustomizationModalAddableColumns.test.tsx +707 -0
  119. package/Tests/UI/Components/SideOver.test.tsx +1 -1
  120. package/Tests/UI/Components/StatusPage/ResourceGridSelection.test.tsx +294 -0
  121. package/Tests/UI/Components/StatusPage/ResourceListSelection.test.tsx +436 -0
  122. package/Tests/UI/Components/TableBulkCsvExport.test.tsx +6 -2
  123. package/Tests/UI/Components/Toggle.test.tsx +25 -0
  124. package/Tests/UI/Components/YamlEditor.test.tsx +847 -0
  125. package/Tests/Utils/NetworkDevice/DeviceReachabilityUtil.test.ts +252 -0
  126. package/Tests/Utils/NetworkDiscovery/DiscoveredHostUtil.test.ts +221 -0
  127. package/Tests/Utils/NetworkDiscovery/ScanNameUtil.test.ts +312 -0
  128. package/Tests/Utils/StatusPage/ResourceExplorer.test.ts +468 -0
  129. package/Tests/Utils/Telemetry/LlmMetricQuery.test.ts +770 -0
  130. package/Types/Code/YamlSyntax.ts +396 -0
  131. package/Types/Exception/ExceptionCode.ts +1 -0
  132. package/Types/Exception/PayloadTooLargeException.ts +8 -0
  133. package/Types/Marketing/Attribution.ts +10 -0
  134. package/Types/Marketing/MarketingEvent.ts +4 -0
  135. package/Types/Monitor/MonitorCriteriaInstance.ts +116 -56
  136. package/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.ts +2 -1
  137. package/Types/SecurityEvent/DetectionFindingConstants.ts +11 -0
  138. package/Types/Telemetry/LlmConventions.ts +255 -0
  139. package/Types/Telemetry/LlmMetricConventions.ts +329 -0
  140. package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +3 -0
  141. package/UI/Components/BulkUpdate/BulkUpdateForm.tsx +10 -1
  142. package/UI/Components/CodeEditor/CodeEditor.tsx +241 -79
  143. package/UI/Components/CodeEditor/YamlEditor.tsx +369 -0
  144. package/UI/Components/Detail/Detail.tsx +10 -0
  145. package/UI/Components/EntityDropdown/EntityDropdown.tsx +33 -0
  146. package/UI/Components/Forms/Fields/FormField.tsx +31 -0
  147. package/UI/Components/Forms/Types/Field.ts +8 -0
  148. package/UI/Components/Forms/Types/FormFieldSchemaType.ts +1 -0
  149. package/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.ts +2 -0
  150. package/UI/Components/Forms/Validation.ts +46 -0
  151. package/UI/Components/Input/Input.tsx +3 -0
  152. package/UI/Components/ModelTable/AttributeColumns.tsx +321 -0
  153. package/UI/Components/ModelTable/BaseModelTable.tsx +263 -14
  154. package/UI/Components/ModelTable/Column.ts +8 -0
  155. package/UI/Components/ModelTable/ColumnCustomizationModal.tsx +310 -0
  156. package/UI/Components/ModelTable/ColumnPreference.ts +13 -1
  157. package/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.tsx +2 -2
  158. package/UI/Components/SideOver/SideOver.tsx +5 -5
  159. package/UI/Components/StatusPage/ResourceGrid.tsx +82 -2
  160. package/UI/Components/StatusPage/ResourceList.tsx +197 -38
  161. package/UI/Components/Toggle/Toggle.tsx +9 -1
  162. package/UI/Components/Types/FieldType.ts +1 -0
  163. package/Utils/NetworkDevice/DeviceReachabilityUtil.ts +73 -1
  164. package/Utils/NetworkDiscovery/ScanNameUtil.ts +181 -0
  165. package/Utils/StatusPage/ResourceExplorer.ts +258 -0
  166. package/Utils/Telemetry/LlmMetricQuery.ts +287 -0
  167. package/build/dist/Models/AnalyticsModels/Span.js +89 -0
  168. package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
  169. package/build/dist/Models/DatabaseModels/DetectionRule.js +57 -0
  170. package/build/dist/Models/DatabaseModels/DetectionRule.js.map +1 -1
  171. package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js +27 -7
  172. package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js.map +1 -1
  173. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js +69 -0
  174. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
  175. package/build/dist/Models/DatabaseModels/Project.js +76 -0
  176. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  177. package/build/dist/Models/DatabaseModels/User.js +76 -0
  178. package/build/dist/Models/DatabaseModels/User.js.map +1 -1
  179. package/build/dist/Server/API/BaseAPI.js +3 -19
  180. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  181. package/build/dist/Server/API/SlackAPI.js +0 -2
  182. package/build/dist/Server/API/SlackAPI.js.map +1 -1
  183. package/build/dist/Server/API/TelemetryAPI.js +17 -0
  184. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  185. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.js +21 -0
  186. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.js.map +1 -0
  187. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.js +42 -0
  188. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.js.map +1 -0
  189. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.js +22 -0
  190. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.js.map +1 -0
  191. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  192. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  193. package/build/dist/Server/Middleware/GzipRequestBody.js +224 -0
  194. package/build/dist/Server/Middleware/GzipRequestBody.js.map +1 -0
  195. package/build/dist/Server/Middleware/MultipartFormData.js +71 -2
  196. package/build/dist/Server/Middleware/MultipartFormData.js.map +1 -1
  197. package/build/dist/Server/Middleware/SlackAuthorization.js +58 -7
  198. package/build/dist/Server/Middleware/SlackAuthorization.js.map +1 -1
  199. package/build/dist/Server/Services/DetectionRuleService.js +49 -4
  200. package/build/dist/Server/Services/DetectionRuleService.js.map +1 -1
  201. package/build/dist/Server/Services/Index.js +204 -0
  202. package/build/dist/Server/Services/Index.js.map +1 -1
  203. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js +50 -0
  204. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js.map +1 -1
  205. package/build/dist/Server/Services/NetworkDeviceService.js +124 -0
  206. package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
  207. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +68 -13
  208. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
  209. package/build/dist/Server/Services/OnCallDutyPolicyFeedService.js +1 -1
  210. package/build/dist/Server/Services/OnCallDutyPolicyFeedService.js.map +1 -1
  211. package/build/dist/Server/Services/ProjectService.js +25 -45
  212. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  213. package/build/dist/Server/Services/SecurityEventService.js +57 -5
  214. package/build/dist/Server/Services/SecurityEventService.js.map +1 -1
  215. package/build/dist/Server/Services/StatusPageResourceService.js +140 -0
  216. package/build/dist/Server/Services/StatusPageResourceService.js.map +1 -1
  217. package/build/dist/Server/Services/TelemetryAttributeService.js +17 -0
  218. package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
  219. package/build/dist/Server/Services/UserService.js +2 -12
  220. package/build/dist/Server/Services/UserService.js.map +1 -1
  221. package/build/dist/Server/Types/Markdown.js +147 -61
  222. package/build/dist/Server/Types/Markdown.js.map +1 -1
  223. package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js +12 -4
  224. package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js.map +1 -1
  225. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js +12 -1
  226. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js.map +1 -1
  227. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +246 -9
  228. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  229. package/build/dist/Server/Utils/Attribution.js.map +1 -1
  230. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js +27 -7
  231. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js.map +1 -1
  232. package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js +42 -7
  233. package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js.map +1 -1
  234. package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js +18 -0
  235. package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js.map +1 -1
  236. package/build/dist/Server/Utils/StartServer.js +12 -21
  237. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  238. package/build/dist/Server/Utils/Telemetry/LlmCostBudgetEvaluator.js +76 -5
  239. package/build/dist/Server/Utils/Telemetry/LlmCostBudgetEvaluator.js.map +1 -1
  240. package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js +151 -0
  241. package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js.map +1 -0
  242. package/build/dist/Server/Utils/Telemetry/LlmSpan.js +24 -1
  243. package/build/dist/Server/Utils/Telemetry/LlmSpan.js.map +1 -1
  244. package/build/dist/Server/Utils/Workspace/Slack/Actions/Auth.js +0 -10
  245. package/build/dist/Server/Utils/Workspace/Slack/Actions/Auth.js.map +1 -1
  246. package/build/dist/Types/Code/YamlSyntax.js +212 -0
  247. package/build/dist/Types/Code/YamlSyntax.js.map +1 -0
  248. package/build/dist/Types/Exception/ExceptionCode.js +1 -0
  249. package/build/dist/Types/Exception/ExceptionCode.js.map +1 -1
  250. package/build/dist/Types/Exception/PayloadTooLargeException.js +8 -0
  251. package/build/dist/Types/Exception/PayloadTooLargeException.js.map +1 -0
  252. package/build/dist/Types/Marketing/Attribution.js +10 -0
  253. package/build/dist/Types/Marketing/Attribution.js.map +1 -1
  254. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +112 -51
  255. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
  256. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js +2 -1
  257. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js.map +1 -1
  258. package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js +9 -0
  259. package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js.map +1 -1
  260. package/build/dist/Types/Telemetry/LlmConventions.js +236 -0
  261. package/build/dist/Types/Telemetry/LlmConventions.js.map +1 -1
  262. package/build/dist/Types/Telemetry/LlmMetricConventions.js +290 -0
  263. package/build/dist/Types/Telemetry/LlmMetricConventions.js.map +1 -0
  264. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +1 -1
  265. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
  266. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js +5 -2
  267. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js.map +1 -1
  268. package/build/dist/UI/Components/CodeEditor/CodeEditor.js +206 -76
  269. package/build/dist/UI/Components/CodeEditor/CodeEditor.js.map +1 -1
  270. package/build/dist/UI/Components/CodeEditor/YamlEditor.js +201 -0
  271. package/build/dist/UI/Components/CodeEditor/YamlEditor.js.map +1 -0
  272. package/build/dist/UI/Components/Detail/Detail.js +9 -0
  273. package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
  274. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js +16 -1
  275. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js.map +1 -1
  276. package/build/dist/UI/Components/Forms/Fields/FormField.js +14 -1
  277. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  278. package/build/dist/UI/Components/Forms/Types/Field.js.map +1 -1
  279. package/build/dist/UI/Components/Forms/Types/FormFieldSchemaType.js +1 -0
  280. package/build/dist/UI/Components/Forms/Types/FormFieldSchemaType.js.map +1 -1
  281. package/build/dist/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.js +2 -0
  282. package/build/dist/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.js.map +1 -1
  283. package/build/dist/UI/Components/Forms/Validation.js +27 -0
  284. package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
  285. package/build/dist/UI/Components/Input/Input.js +1 -1
  286. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  287. package/build/dist/UI/Components/ModelTable/AttributeColumns.js +159 -0
  288. package/build/dist/UI/Components/ModelTable/AttributeColumns.js.map +1 -0
  289. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +179 -17
  290. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  291. package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js +133 -2
  292. package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js.map +1 -1
  293. package/build/dist/UI/Components/ModelTable/ColumnPreference.js +7 -1
  294. package/build/dist/UI/Components/ModelTable/ColumnPreference.js.map +1 -1
  295. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.js +2 -2
  296. package/build/dist/UI/Components/SideOver/SideOver.js +5 -5
  297. package/build/dist/UI/Components/StatusPage/ResourceGrid.js +28 -2
  298. package/build/dist/UI/Components/StatusPage/ResourceGrid.js.map +1 -1
  299. package/build/dist/UI/Components/StatusPage/ResourceList.js +87 -20
  300. package/build/dist/UI/Components/StatusPage/ResourceList.js.map +1 -1
  301. package/build/dist/UI/Components/Toggle/Toggle.js +9 -1
  302. package/build/dist/UI/Components/Toggle/Toggle.js.map +1 -1
  303. package/build/dist/UI/Components/Types/FieldType.js +1 -0
  304. package/build/dist/UI/Components/Types/FieldType.js.map +1 -1
  305. package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js +37 -1
  306. package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js.map +1 -1
  307. package/build/dist/Utils/NetworkDiscovery/ScanNameUtil.js +120 -0
  308. package/build/dist/Utils/NetworkDiscovery/ScanNameUtil.js.map +1 -0
  309. package/build/dist/Utils/StatusPage/ResourceExplorer.js +171 -0
  310. package/build/dist/Utils/StatusPage/ResourceExplorer.js.map +1 -1
  311. package/build/dist/Utils/Telemetry/LlmMetricQuery.js +175 -0
  312. package/build/dist/Utils/Telemetry/LlmMetricQuery.js.map +1 -0
  313. package/jest.config.json +2 -0
  314. package/package.json +1 -1
  315. package/tsconfig.json +2 -1
  316. package/Tests/UI/Components/Toast.test.tsx +0 -197
  317. package/Tests/UI/Components/ToastStacking.test.tsx +0 -198
  318. package/UI/Components/Toast/Toast.tsx +0 -189
  319. package/UI/Components/Toast/ToastInit.tsx +0 -110
  320. package/build/dist/UI/Components/Toast/Toast.js +0 -123
  321. package/build/dist/UI/Components/Toast/Toast.js.map +0 -1
  322. package/build/dist/UI/Components/Toast/ToastInit.js +0 -53
  323. package/build/dist/UI/Components/Toast/ToastInit.js.map +0 -1
@@ -0,0 +1,564 @@
1
+ import TelemetryAttributeService from "../../../Server/Services/TelemetryAttributeService";
2
+ import ProjectService from "../../../Server/Services/ProjectService";
3
+ import {
4
+ ExpressRequest,
5
+ ExpressResponse,
6
+ NextFunction,
7
+ } from "../../../Server/Utils/Express";
8
+ import Response from "../../../Server/Utils/Response";
9
+ import Dictionary from "../../../Types/Dictionary";
10
+ import Exception from "../../../Types/Exception/Exception";
11
+ import { JSONObject } from "../../../Types/JSON";
12
+ import ObjectID from "../../../Types/ObjectID";
13
+ import Permission, {
14
+ UserPermission,
15
+ UserTenantAccessPermission,
16
+ } from "../../../Types/Permission";
17
+ import TelemetryType from "../../../Types/Telemetry/TelemetryType";
18
+ import SecurityEvent from "../../../Models/AnalyticsModels/SecurityEvent";
19
+ import UserType from "../../../Types/UserType";
20
+ import {
21
+ afterEach,
22
+ beforeAll,
23
+ beforeEach,
24
+ describe,
25
+ expect,
26
+ jest,
27
+ test,
28
+ } from "@jest/globals";
29
+
30
+ /*
31
+ * ---------------------------------------------------------------------------
32
+ * /telemetry/security-events/get-attributes[-values]
33
+ * ---------------------------------------------------------------------------
34
+ *
35
+ * These back the "Add Attribute Column" search on the security events table:
36
+ * the keys inside an event's `attributes` map differ per event class and per
37
+ * source, so the UI has to ask which ones exist.
38
+ *
39
+ * They do not go through BaseAnalyticsAPI, so nothing downstream re-checks
40
+ * authorization - the tenantId arrives in a caller-controlled header and
41
+ * UserMiddleware lets tokenless requests through as Public. The guard on the
42
+ * route IS the access control, and it has to stay exactly as permissive as
43
+ * the SecurityEvent model's own read list, no more. That parity is the main
44
+ * thing pinned here.
45
+ */
46
+
47
+ type RouterFunction = (
48
+ req: ExpressRequest,
49
+ res: ExpressResponse,
50
+ next: NextFunction,
51
+ ) => void | Promise<void>;
52
+
53
+ type RecordedRoute = {
54
+ method: string;
55
+ uri: string;
56
+ handlers: Array<RouterFunction>;
57
+ };
58
+
59
+ const recordedRoutes: Array<RecordedRoute> = [];
60
+
61
+ type RecordRouteFunction = (
62
+ method: string,
63
+ ) => (uri: string, ...handlers: Array<RouterFunction>) => void;
64
+
65
+ const recordRoute: RecordRouteFunction = (method: string) => {
66
+ return (uri: string, ...handlers: Array<RouterFunction>): void => {
67
+ recordedRoutes.push({
68
+ method: method.toUpperCase(),
69
+ uri: uri,
70
+ handlers: handlers,
71
+ });
72
+ };
73
+ };
74
+
75
+ const telemetryRouter: JSONObject = {
76
+ get: recordRoute("get"),
77
+ post: recordRoute("post"),
78
+ put: recordRoute("put"),
79
+ delete: recordRoute("delete"),
80
+ } as unknown as JSONObject;
81
+
82
+ jest.mock("../../../Server/Utils/Express", () => {
83
+ return {
84
+ getRouter: () => {
85
+ return telemetryRouter;
86
+ },
87
+ getClientIp: () => {
88
+ return "203.0.113.7";
89
+ },
90
+ };
91
+ });
92
+
93
+ jest.mock("../../../Server/Utils/Response", () => {
94
+ return {
95
+ sendEntityArrayResponse: jest.fn(),
96
+ sendJsonObjectResponse: jest.fn(),
97
+ sendEmptySuccessResponse: jest.fn(),
98
+ sendEntityResponse: jest.fn(),
99
+ sendErrorResponse: jest.fn(),
100
+ };
101
+ });
102
+
103
+ const KEYS_ROUTE: string = "/telemetry/security-events/get-attributes";
104
+ const VALUES_ROUTE: string = "/telemetry/security-events/get-attribute-values";
105
+
106
+ type FindRouteFunction = (uri: string) => RecordedRoute;
107
+
108
+ const findRoute: FindRouteFunction = (uri: string): RecordedRoute => {
109
+ const route: RecordedRoute | undefined = recordedRoutes.find(
110
+ (candidate: RecordedRoute): boolean => {
111
+ return candidate.method === "POST" && candidate.uri === uri;
112
+ },
113
+ );
114
+
115
+ if (!route) {
116
+ throw new Error(`Route not registered: ${uri}`);
117
+ }
118
+
119
+ return route;
120
+ };
121
+
122
+ type CallResult = {
123
+ deniedWith: Exception | undefined;
124
+ thrownToNext: unknown;
125
+ reachedHandler: boolean;
126
+ jsonBody: JSONObject | undefined;
127
+ };
128
+
129
+ /*
130
+ * Runs a recorded route's middleware chain for real, starting at
131
+ * requireUserAuthentication. Index 0 is getUserMiddleware, whose only job is
132
+ * to populate the session fields these fixtures set directly - running it
133
+ * would need a live session store and prove nothing about authorization.
134
+ */
135
+ type CallRouteFunction = (data: {
136
+ uri: string;
137
+ request: JSONObject;
138
+ body: JSONObject;
139
+ }) => Promise<CallResult>;
140
+
141
+ const callRoute: CallRouteFunction = async (data: {
142
+ uri: string;
143
+ request: JSONObject;
144
+ body: JSONObject;
145
+ }): Promise<CallResult> => {
146
+ const route: RecordedRoute = findRoute(data.uri);
147
+
148
+ const req: ExpressRequest = {
149
+ ...data.request,
150
+ body: data.body,
151
+ params: {},
152
+ query: {},
153
+ headers: { "user-agent": "jest-agent" },
154
+ } as unknown as ExpressRequest;
155
+
156
+ const res: ExpressResponse = {
157
+ setHeader: (): void => {
158
+ // no-op
159
+ },
160
+ send: (): void => {
161
+ // no-op
162
+ },
163
+ status: jest.fn().mockReturnThis(),
164
+ json: jest.fn(),
165
+ } as unknown as ExpressResponse;
166
+
167
+ const outcome: { thrownToNext: unknown; reachedHandler: boolean } = {
168
+ thrownToNext: undefined,
169
+ reachedHandler: false,
170
+ };
171
+
172
+ for (let index: number = 1; index < route.handlers.length; index++) {
173
+ const handler: RouterFunction | undefined = route.handlers[index];
174
+
175
+ if (!handler) {
176
+ break;
177
+ }
178
+
179
+ if (index === route.handlers.length - 1) {
180
+ outcome.reachedHandler = true;
181
+ }
182
+
183
+ const step: { calledNext: boolean } = { calledNext: false };
184
+
185
+ const next: NextFunction = ((error?: unknown): void => {
186
+ step.calledNext = true;
187
+
188
+ if (error) {
189
+ outcome.thrownToNext = error;
190
+ }
191
+ }) as unknown as NextFunction;
192
+
193
+ await handler(req, res, next);
194
+
195
+ // A guard that answered the request itself never calls next.
196
+ if (!step.calledNext) {
197
+ break;
198
+ }
199
+ }
200
+
201
+ const sendErrorResponse: jest.Mock =
202
+ Response.sendErrorResponse as unknown as jest.Mock;
203
+ const sendJsonObjectResponse: jest.Mock =
204
+ Response.sendJsonObjectResponse as unknown as jest.Mock;
205
+
206
+ const errorCall: Array<unknown> | undefined = sendErrorResponse.mock
207
+ .calls[0] as Array<unknown> | undefined;
208
+ const jsonCall: Array<unknown> | undefined = sendJsonObjectResponse.mock
209
+ .calls[0] as Array<unknown> | undefined;
210
+
211
+ return {
212
+ deniedWith: errorCall ? (errorCall[2] as Exception) : undefined,
213
+ thrownToNext: outcome.thrownToNext,
214
+ reachedHandler: outcome.reachedHandler,
215
+ jsonBody: jsonCall ? (jsonCall[2] as JSONObject) : undefined,
216
+ };
217
+ };
218
+
219
+ type BuildPrincipalFunction = (data: {
220
+ projectId: ObjectID;
221
+ userId: ObjectID;
222
+ permissions: Array<Permission>;
223
+ }) => JSONObject;
224
+
225
+ const buildPrincipal: BuildPrincipalFunction = (data: {
226
+ projectId: ObjectID;
227
+ userId: ObjectID;
228
+ permissions: Array<Permission>;
229
+ }): JSONObject => {
230
+ /*
231
+ * isBlockPermission must be an explicit false: getUserPermissions filters
232
+ * tenant permissions with a strict `=== false`, so undefined would silently
233
+ * drop every permission the fixture grants.
234
+ */
235
+ const userPermissions: Array<UserPermission> = data.permissions.map(
236
+ (permission: Permission): UserPermission => {
237
+ return {
238
+ _type: "UserPermission",
239
+ permission: permission,
240
+ labelIds: [],
241
+ isBlockPermission: false,
242
+ };
243
+ },
244
+ );
245
+
246
+ const tenantPermission: UserTenantAccessPermission = {
247
+ _type: "UserTenantAccessPermission",
248
+ projectId: data.projectId,
249
+ permissions: userPermissions,
250
+ };
251
+
252
+ const permissionMap: Dictionary<UserTenantAccessPermission> = {};
253
+ permissionMap[data.projectId.toString()] = tenantPermission;
254
+
255
+ return {
256
+ userType: UserType.User,
257
+ tenantId: data.projectId,
258
+ userTenantAccessPermission: permissionMap,
259
+ userAuthorization: { userId: data.userId },
260
+ } as unknown as JSONObject;
261
+ };
262
+
263
+ describe("Security event attribute API", () => {
264
+ let projectId: ObjectID;
265
+ let userId: ObjectID;
266
+
267
+ interface ServiceSpy {
268
+ mock: { calls: Array<Array<unknown>> };
269
+ mockResolvedValue: (value: Array<string>) => unknown;
270
+ mockRejectedValue: (value: unknown) => unknown;
271
+ }
272
+
273
+ let fetchAttributes: ServiceSpy;
274
+ let fetchAttributeValues: ServiceSpy;
275
+
276
+ beforeAll(() => {
277
+ recordedRoutes.length = 0;
278
+ /*
279
+ * Loaded lazily rather than with a top-level import: TelemetryAPI calls
280
+ * Express.getRouter() at module scope, so a static import would run the
281
+ * mock factory before `telemetryRouter` is initialised and die in the
282
+ * temporal dead zone.
283
+ */
284
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
285
+ require("../../../Server/API/TelemetryAPI");
286
+ });
287
+
288
+ beforeEach(() => {
289
+ jest.clearAllMocks();
290
+
291
+ projectId = ObjectID.generate();
292
+ userId = ObjectID.generate();
293
+
294
+ /*
295
+ * CI runs the Common suite with BILLING_ENABLED=true (see test-setup.sh),
296
+ * so getDatabaseCommonInteractionProps resolves the caller's plan through
297
+ * ProjectService.getCurrentPlan. These fixtures use a project that never
298
+ * exists in the database, so the real lookup throws "Project ID is invalid"
299
+ * and the handler's error path swallows the request before it ever reaches
300
+ * the attribute service. Billing is orthogonal to what this file pins, so
301
+ * stub the plan lookup to a benign value and let the rest run for real.
302
+ */
303
+ jest
304
+ .spyOn(ProjectService, "getCurrentPlan")
305
+ .mockResolvedValue({ plan: null, isSubscriptionUnpaid: false });
306
+
307
+ fetchAttributes = jest.spyOn(
308
+ TelemetryAttributeService,
309
+ "fetchAttributes" as never,
310
+ ) as unknown as ServiceSpy;
311
+ fetchAttributes.mockResolvedValue([]);
312
+
313
+ fetchAttributeValues = jest.spyOn(
314
+ TelemetryAttributeService,
315
+ "fetchAttributeValues" as never,
316
+ ) as unknown as ServiceSpy;
317
+ fetchAttributeValues.mockResolvedValue([]);
318
+ });
319
+
320
+ afterEach(() => {
321
+ jest.restoreAllMocks();
322
+ });
323
+
324
+ describe("route registration", () => {
325
+ test("both routes exist as POST", () => {
326
+ expect(findRoute(KEYS_ROUTE).uri).toBe(KEYS_ROUTE);
327
+ expect(findRoute(VALUES_ROUTE).uri).toBe(VALUES_ROUTE);
328
+ });
329
+
330
+ /*
331
+ * getUserMiddleware, requireUserAuthentication, requirePermission, then
332
+ * the handler. A route registered without the guard chain would still
333
+ * "work" for the UI and be wide open.
334
+ */
335
+ test("both routes carry a guard chain in front of the handler", () => {
336
+ expect(findRoute(KEYS_ROUTE).handlers.length).toBe(4);
337
+ expect(findRoute(VALUES_ROUTE).handlers.length).toBe(4);
338
+ });
339
+ });
340
+
341
+ describe("access control", () => {
342
+ test("an unauthenticated caller is refused", async () => {
343
+ const result: CallResult = await callRoute({
344
+ uri: KEYS_ROUTE,
345
+ request: { userType: UserType.Public, tenantId: projectId },
346
+ body: {},
347
+ });
348
+
349
+ expect(result.reachedHandler).toBe(false);
350
+ expect(result.deniedWith).toBeDefined();
351
+ expect(fetchAttributes.mock.calls).toHaveLength(0);
352
+ });
353
+
354
+ test("a project member without any telemetry read permission is refused", async () => {
355
+ const result: CallResult = await callRoute({
356
+ uri: KEYS_ROUTE,
357
+ request: buildPrincipal({
358
+ projectId,
359
+ userId,
360
+ permissions: [Permission.ReadProject],
361
+ }),
362
+ body: {},
363
+ });
364
+
365
+ expect(result.reachedHandler).toBe(false);
366
+ expect(result.deniedWith).toBeDefined();
367
+ expect(fetchAttributes.mock.calls).toHaveLength(0);
368
+ });
369
+
370
+ test("ReadSecurityEvent alone is enough", async () => {
371
+ const result: CallResult = await callRoute({
372
+ uri: KEYS_ROUTE,
373
+ request: buildPrincipal({
374
+ projectId,
375
+ userId,
376
+ permissions: [Permission.ReadSecurityEvent],
377
+ }),
378
+ body: {},
379
+ });
380
+
381
+ expect(result.reachedHandler).toBe(true);
382
+ expect(result.deniedWith).toBeUndefined();
383
+ });
384
+
385
+ /*
386
+ * The guard has to mirror the model's table-level read list exactly, or
387
+ * these routes end up either stricter or looser than the CRUD API for the
388
+ * same rows. Every permission the model grants a read to is exercised.
389
+ */
390
+ test.each([
391
+ Permission.ProjectOwner,
392
+ Permission.ProjectAdmin,
393
+ Permission.ProjectMember,
394
+ Permission.Viewer,
395
+ Permission.TelemetryAdmin,
396
+ Permission.TelemetryMember,
397
+ Permission.TelemetryViewer,
398
+ Permission.ReadSecurityEvent,
399
+ ])("%s can read attribute keys", async (permission: Permission) => {
400
+ const result: CallResult = await callRoute({
401
+ uri: KEYS_ROUTE,
402
+ request: buildPrincipal({
403
+ projectId,
404
+ userId,
405
+ permissions: [permission],
406
+ }),
407
+ body: {},
408
+ });
409
+
410
+ expect(result.reachedHandler).toBe(true);
411
+ });
412
+
413
+ test("the model's read list and the guard have not drifted apart", () => {
414
+ const modelReadPermissions: Array<Permission> =
415
+ new SecurityEvent().getReadPermissions();
416
+
417
+ expect(modelReadPermissions).toEqual(
418
+ expect.arrayContaining([Permission.ReadSecurityEvent]),
419
+ );
420
+ expect(modelReadPermissions).toHaveLength(8);
421
+ });
422
+
423
+ test("the values route is guarded the same way", async () => {
424
+ const result: CallResult = await callRoute({
425
+ uri: VALUES_ROUTE,
426
+ request: { userType: UserType.Public, tenantId: projectId },
427
+ body: { attributeKey: "device.hostname" },
428
+ });
429
+
430
+ expect(result.reachedHandler).toBe(false);
431
+ expect(result.deniedWith).toBeDefined();
432
+ });
433
+ });
434
+
435
+ describe("attribute keys", () => {
436
+ type CallKeysFunction = (body?: JSONObject) => Promise<CallResult>;
437
+
438
+ const callKeys: CallKeysFunction = (
439
+ body: JSONObject = {},
440
+ ): Promise<CallResult> => {
441
+ return callRoute({
442
+ uri: KEYS_ROUTE,
443
+ request: buildPrincipal({
444
+ projectId,
445
+ userId,
446
+ permissions: [Permission.ReadSecurityEvent],
447
+ }),
448
+ body: body,
449
+ });
450
+ };
451
+
452
+ /*
453
+ * The whole point of the new source: asking for Log attributes here would
454
+ * return the wrong project's-worth of keys from the wrong table, and look
455
+ * entirely plausible in the picker.
456
+ */
457
+ test("asks the attribute service for SECURITY EVENT attributes", async () => {
458
+ await callKeys();
459
+
460
+ expect(fetchAttributes.mock.calls[0]![0]).toMatchObject({
461
+ telemetryType: TelemetryType.SecurityEvent,
462
+ });
463
+ });
464
+
465
+ test("scopes the lookup to the caller's project", async () => {
466
+ await callKeys();
467
+
468
+ const request: JSONObject = fetchAttributes.mock
469
+ .calls[0]![0] as JSONObject;
470
+
471
+ expect((request["projectId"] as ObjectID).toString()).toBe(
472
+ projectId.toString(),
473
+ );
474
+ });
475
+
476
+ test("returns the keys under an `attributes` field", async () => {
477
+ fetchAttributes.mockResolvedValue(["class_uid", "device.hostname"]);
478
+
479
+ const result: CallResult = await callKeys();
480
+
481
+ expect(result.jsonBody).toEqual({
482
+ attributes: ["class_uid", "device.hostname"],
483
+ });
484
+ });
485
+
486
+ test("no keys is an empty list, not an error", async () => {
487
+ const result: CallResult = await callKeys();
488
+
489
+ expect(result.jsonBody).toEqual({ attributes: [] });
490
+ });
491
+
492
+ test("a service failure goes to the error handler rather than a 200", async () => {
493
+ fetchAttributes.mockRejectedValue(new Error("clickhouse said no"));
494
+
495
+ const result: CallResult = await callKeys();
496
+
497
+ expect(result.thrownToNext).toBeDefined();
498
+ expect(result.jsonBody).toBeUndefined();
499
+ });
500
+ });
501
+
502
+ describe("attribute values", () => {
503
+ type CallValuesFunction = (body: JSONObject) => Promise<CallResult>;
504
+
505
+ const callValues: CallValuesFunction = (
506
+ body: JSONObject,
507
+ ): Promise<CallResult> => {
508
+ return callRoute({
509
+ uri: VALUES_ROUTE,
510
+ request: buildPrincipal({
511
+ projectId,
512
+ userId,
513
+ permissions: [Permission.ReadSecurityEvent],
514
+ }),
515
+ body: body,
516
+ });
517
+ };
518
+
519
+ test("asks for SECURITY EVENT values for the requested key", async () => {
520
+ await callValues({ attributeKey: "device.hostname" });
521
+
522
+ expect(fetchAttributeValues.mock.calls[0]![0]).toMatchObject({
523
+ telemetryType: TelemetryType.SecurityEvent,
524
+ attributeKey: "device.hostname",
525
+ });
526
+ });
527
+
528
+ test("passes the search text through so the narrowing happens server-side", async () => {
529
+ await callValues({ attributeKey: "device.hostname", searchText: "web" });
530
+
531
+ expect(fetchAttributeValues.mock.calls[0]![0]).toMatchObject({
532
+ searchText: "web",
533
+ });
534
+ });
535
+
536
+ test("a missing attribute key is a bad request, not an unbounded scan", async () => {
537
+ const result: CallResult = await callValues({});
538
+
539
+ expect(result.deniedWith).toBeDefined();
540
+ expect(fetchAttributeValues.mock.calls).toHaveLength(0);
541
+ });
542
+
543
+ /*
544
+ * The body is caller-controlled JSON, so a non-string is not a key.
545
+ * Reading it as one would put a non-string into the SQL parameter.
546
+ */
547
+ test("a non-string attribute key is rejected", async () => {
548
+ const result: CallResult = await callValues({ attributeKey: 42 });
549
+
550
+ expect(result.deniedWith).toBeDefined();
551
+ expect(fetchAttributeValues.mock.calls).toHaveLength(0);
552
+ });
553
+
554
+ test("returns the values under a `values` field", async () => {
555
+ fetchAttributeValues.mockResolvedValue(["web-1", "web-2"]);
556
+
557
+ const result: CallResult = await callValues({
558
+ attributeKey: "device.hostname",
559
+ });
560
+
561
+ expect(result.jsonBody).toEqual({ values: ["web-1", "web-2"] });
562
+ });
563
+ });
564
+ });
@@ -50,6 +50,52 @@ const UNTIMESTAMPED_MIGRATION_NAME: string = "InitialMigration";
50
50
 
51
51
  const INITIAL_MIGRATION_TIMESTAMP: number = 1717605043663;
52
52
 
53
+ /*
54
+ * Timestamps that two migrations already share, and always will.
55
+ *
56
+ * `AddNetworkDeviceAutoImportRule1789100000000` and
57
+ * `AddUserTwoFactorBackupCode1789100000000` were authored in parallel, both
58
+ * picked the same round slot (the hazard the timestamps in this directory are
59
+ * hand-picked rather than wall-clock), and both shipped before anyone noticed.
60
+ *
61
+ * Renaming either one now would re-run it. TypeORM records a migration by
62
+ * name, so a renamed class reads as one that has never executed — and both of
63
+ * these CREATE a table with no IF NOT EXISTS, so the re-run fails against
64
+ * every database that already has it. The collision is therefore permanent
65
+ * history, not a fixable defect, and the assertions below step around it while
66
+ * still failing for a NEW one.
67
+ *
68
+ * The exception names the two classes, so it cannot quietly grow: a third
69
+ * migration on the same stamp is not one of them and fails the duplicate
70
+ * guard, and dropping either migration leaves the exception naming a class
71
+ * that is no longer registered, which fails too.
72
+ */
73
+ const SHIPPED_DUPLICATE_MIGRATIONS: ReadonlyArray<{
74
+ timestamp: number;
75
+ classNames: ReadonlyArray<string>;
76
+ }> = [
77
+ {
78
+ timestamp: 1789100000000,
79
+ classNames: [
80
+ "AddNetworkDeviceAutoImportRule1789100000000",
81
+ "AddUserTwoFactorBackupCode1789100000000",
82
+ ],
83
+ },
84
+ ];
85
+
86
+ /*
87
+ * The exception is written as the exact pair of classes that share the stamp,
88
+ * not as the bare number, so it excuses those two and nothing else. Excusing
89
+ * the number would have excused a THIRD migration landing on it — a brand new
90
+ * collision, waved through by the exception made for the old one.
91
+ */
92
+ const SHIPPED_DUPLICATE_TIMESTAMPS: ReadonlyArray<number> =
93
+ SHIPPED_DUPLICATE_MIGRATIONS.map(
94
+ (duplicate: { timestamp: number }): number => {
95
+ return duplicate.timestamp;
96
+ },
97
+ );
98
+
53
99
  const TIMESTAMPED_FILE: RegExp = new RegExp("^\\d+-");
54
100
 
55
101
  type MigrationClass = new () => MigrationInterface;
@@ -125,13 +171,58 @@ describe("the migration registry", () => {
125
171
  return timestamp !== null;
126
172
  });
127
173
 
128
- const duplicates: Array<number> = timestamps.filter(
129
- (timestamp: number, index: number): boolean => {
174
+ const duplicates: Array<number> = timestamps
175
+ .filter((timestamp: number, index: number): boolean => {
130
176
  return timestamps.indexOf(timestamp) !== index;
177
+ })
178
+ /*
179
+ * A collision that has already shipped cannot be undone — see
180
+ * SHIPPED_DUPLICATE_TIMESTAMPS. A new one still fails here, which is
181
+ * the only case this assertion can actually do anything about.
182
+ */
183
+ .filter((timestamp: number): boolean => {
184
+ return !SHIPPED_DUPLICATE_TIMESTAMPS.includes(timestamp);
185
+ });
186
+
187
+ expect(duplicates).toEqual([]);
188
+ });
189
+
190
+ /*
191
+ * Keeps the exception list honest. Every entry has to name a collision
192
+ * that is really there, so the day one of those migrations is deleted its
193
+ * entry goes with it — and nobody can quietly widen the list to wave a new
194
+ * collision through, because a wrong entry fails right here.
195
+ */
196
+ test("grandfathers exactly the collisions that exist, and no more", () => {
197
+ const names: Array<string> = registered.map(
198
+ (migrationClass: MigrationClass): string => {
199
+ return migrationClass.name;
131
200
  },
132
201
  );
133
202
 
134
- expect(duplicates).toEqual([]);
203
+ for (const duplicate of SHIPPED_DUPLICATE_MIGRATIONS) {
204
+ const onThisTimestamp: Array<string> = registered
205
+ .filter((migrationClass: MigrationClass): boolean => {
206
+ return timestampOf(migrationClass) === duplicate.timestamp;
207
+ })
208
+ .map((migrationClass: MigrationClass): string => {
209
+ return migrationClass.name;
210
+ })
211
+ .sort();
212
+
213
+ /*
214
+ * Both halves matter. Naming a class that is no longer registered
215
+ * means the exception has outlived the collision and must go; finding
216
+ * a class the exception does not name means a NEW migration has
217
+ * landed on the shipped stamp, which is the very thing the duplicate
218
+ * guard exists to catch.
219
+ */
220
+ expect(onThisTimestamp).toEqual([...duplicate.classNames].sort());
221
+
222
+ for (const className of duplicate.classNames) {
223
+ expect(names).toContain(className);
224
+ }
225
+ }
135
226
  });
136
227
 
137
228
  test("never registers the same migration class twice", () => {
@@ -168,10 +259,35 @@ describe("the migration registry", () => {
168
259
  return false;
169
260
  }
170
261
 
262
+ /*
263
+ * One file per registered class, which for every timestamp but the
264
+ * grandfathered collision means exactly one file. For that one it
265
+ * means two — both classes are real, both have a file, and neither can
266
+ * be renamed. See SHIPPED_DUPLICATE_TIMESTAMPS.
267
+ */
268
+ const grandfathered:
269
+ | { timestamp: number; classNames: ReadonlyArray<string> }
270
+ | undefined = SHIPPED_DUPLICATE_MIGRATIONS.find(
271
+ (duplicate: { timestamp: number }): boolean => {
272
+ return duplicate.timestamp === timestamp;
273
+ },
274
+ );
275
+
276
+ /*
277
+ * One file per registered class, which for every timestamp but the
278
+ * grandfathered collision means exactly one file. For that one it
279
+ * means exactly as many files as the exception names — a fixed
280
+ * number, never the live count, so an unexpected third file on that
281
+ * stamp fails here rather than being counted and excused.
282
+ */
283
+ const expectedFileCount: number = grandfathered
284
+ ? grandfathered.classNames.length
285
+ : 1;
286
+
171
287
  return (
172
288
  files.filter((file: string): boolean => {
173
289
  return file.startsWith(`${timestamp}-`);
174
- }).length !== 1
290
+ }).length !== expectedFileCount
175
291
  );
176
292
  })
177
293
  .map((migrationClass: MigrationClass): string => {