@oneuptime/common 12.0.34 → 13.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. package/Models/DatabaseModels/GlobalConfig.ts +12 -12
  2. package/Models/DatabaseModels/InventoryItem.ts +44 -1
  3. package/Models/DatabaseModels/NetworkDevice.ts +126 -0
  4. package/Server/EnvironmentConfig.ts +47 -16
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.ts +25 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  7. package/Server/Infrastructure/Redis.ts +8 -8
  8. package/Server/Infrastructure/Status.ts +10 -4
  9. package/Server/Services/DatabaseService.ts +65 -13
  10. package/Server/Services/InventoryItemService.ts +31 -1
  11. package/Server/Services/MonitorTemplateService.ts +140 -19
  12. package/Server/Services/NetworkDeviceAutoImportRuleEngineService.ts +349 -41
  13. package/Server/Services/NetworkDeviceDiscoveryScanService.ts +87 -2
  14. package/Server/Services/NetworkDeviceService.ts +69 -0
  15. package/Server/Services/UserNotificationRuleService.ts +76 -25
  16. package/Server/Types/Database/Permissions/ReadPermission.ts +45 -5
  17. package/Server/Types/Database/QueryUtil.ts +56 -0
  18. package/Server/Types/Markdown.ts +93 -1
  19. package/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.ts +82 -17
  20. package/Server/Utils/EmailRollup/EmailRollupFlushRunner.ts +10 -1
  21. package/Server/Utils/EmailRollup/EmailRollupRenderer.ts +2 -2
  22. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +82 -35
  23. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +31 -5
  24. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +202 -37
  25. package/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.ts +82 -9
  26. package/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.ts +16 -7
  27. package/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.ts +29 -17
  28. package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +107 -48
  29. package/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.ts +158 -0
  30. package/Server/Utils/Monitor/NetworkInventoryUtil.ts +27 -0
  31. package/Server/Utils/Telemetry/EntityRegistry.ts +11 -1
  32. package/Server/Utils/Telemetry/TelemetryEntity.ts +83 -9
  33. package/Tests/App/Dashboard/AddNeighborToMonitoringModal.test.tsx +32 -0
  34. package/Tests/App/Dashboard/AdoptedDeviceDrawer.test.tsx +374 -0
  35. package/Tests/App/Dashboard/DashboardLabelVariables.test.tsx +580 -0
  36. package/Tests/App/Dashboard/DashboardMonitorLabelVariable.test.tsx +438 -0
  37. package/Tests/App/Dashboard/DashboardVariableAllContract.test.tsx +5 -2
  38. package/Tests/App/Dashboard/DeviceAttachmentCard.test.tsx +273 -0
  39. package/Tests/App/Dashboard/DiscoveryReviewInventoryRefresh.test.tsx +505 -0
  40. package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +163 -0
  41. package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +401 -0
  42. package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +232 -0
  43. package/Tests/App/Dashboard/InventoryItems.test.tsx +201 -0
  44. package/Tests/App/Dashboard/InventoryTypeAndStatusFacets.test.tsx +798 -0
  45. package/Tests/App/Dashboard/NetworkTopologyLiveView.test.tsx +320 -0
  46. package/Tests/App/Dashboard/NetworkTopologyToolbar.test.tsx +275 -0
  47. package/Tests/App/Dashboard/OnCallCalendarFeedEmptyState.test.tsx +975 -0
  48. package/Tests/App/Dashboard/OnCallCalendarFeedPlanGate.test.tsx +63 -0
  49. package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +626 -0
  50. package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +348 -0
  51. package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +331 -0
  52. package/Tests/App/Dashboard/UserSettingsEmailPreferences.test.tsx +603 -0
  53. package/Tests/App/Dashboard/UserSettingsNotificationSettings.test.tsx +324 -0
  54. package/Tests/App/PublicDashboard/DashboardVariableSelector.test.tsx +13 -4
  55. package/Tests/App/StatusPage/StatusPageOidcOrigin.test.tsx +223 -0
  56. package/Tests/Models/InventoryItemStatus.test.ts +147 -0
  57. package/Tests/Models/NetworkDeviceMacAddressColumn.test.ts +294 -0
  58. package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +129 -0
  59. package/Tests/Server/EnvironmentConfigValkey.test.ts +227 -0
  60. package/Tests/Server/Infrastructure/Postgres/AddMacAddressToNetworkDeviceMigration.test.ts +365 -0
  61. package/Tests/Server/Infrastructure/Postgres/CustomFieldValueMappingMigration.test.ts +62 -23
  62. package/Tests/Server/Infrastructure/ValkeyDeploymentConfig.test.ts +265 -0
  63. package/Tests/Server/Middleware/HttpMetricsMiddleware.test.ts +382 -0
  64. package/Tests/Server/Middleware/SCIMAuthorization.test.ts +525 -0
  65. package/Tests/Server/Services/DatabaseServiceAggregateBy.test.ts +291 -56
  66. package/Tests/Server/Services/DatabaseServiceSortTiebreaker.test.ts +151 -0
  67. package/Tests/Server/Services/InventoryItemManualCreate.test.ts +105 -0
  68. package/Tests/Server/Services/InventoryItemPromotionGate.test.ts +178 -0
  69. package/Tests/Server/Services/MonitorTemplateServiceCustomFieldSync.test.ts +502 -0
  70. package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +1133 -81
  71. package/Tests/Server/Services/NetworkDeviceDiscoveryScanRegistration.test.ts +520 -0
  72. package/Tests/Server/Services/NetworkDeviceMacAddressNormalization.test.ts +572 -0
  73. package/Tests/Server/Types/Database/InventoryStatusPostgres.test.ts +368 -0
  74. package/Tests/Server/Types/Database/Permissions/ReadBlockPermission.test.ts +261 -14
  75. package/Tests/Server/Types/Database/QueryUtilIncludesAnyOfGroups.test.ts +169 -0
  76. package/Tests/Server/Types/Database/QueryUtilIncludesAnyOfGroupsPostgres.test.ts +297 -0
  77. package/Tests/Server/Types/Markdown.test.ts +194 -1
  78. package/Tests/Server/Types/Workflow/Components/IncomingWebhookUtils.test.ts +279 -0
  79. package/Tests/Server/Types/Workflow/Components/TextToJson.test.ts +212 -0
  80. package/Tests/Server/Utils/CustomField/CustomFieldMappingRegistry.test.ts +436 -0
  81. package/Tests/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.test.ts +214 -0
  82. package/Tests/Server/Utils/EmailRollup/EmailRollupFlushRunnerBehaviour.test.ts +1 -1
  83. package/Tests/Server/Utils/EmailRollup/EmailRollupRenderer.test.ts +10 -3
  84. package/Tests/Server/Utils/EventLoop.test.ts +359 -0
  85. package/Tests/Server/Utils/JsonWebToken.test.ts +30 -0
  86. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +242 -0
  87. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteriaAggregation.test.ts +13 -7
  88. package/Tests/Server/Utils/Monitor/Criteria/DatabaseMonitorCriteria.test.ts +29 -0
  89. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.ts +30 -6
  90. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +246 -0
  91. package/Tests/Server/Utils/Monitor/MonitorCriteriaEmailEvalLogAgreement.test.ts +280 -0
  92. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +224 -10
  93. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorMetricUnits.test.ts +570 -0
  94. package/Tests/Server/Utils/Monitor/MonitorCriteriaExpectationBuilderUnits.test.ts +30 -5
  95. package/Tests/Server/Utils/Monitor/MonitorCriteriaMessageBuilderUnits.test.ts +68 -21
  96. package/Tests/Server/Utils/Monitor/MonitorCriteriaObservationBuilderUnits.test.ts +36 -19
  97. package/Tests/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.test.ts +573 -0
  98. package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +317 -0
  99. package/Tests/Server/Utils/Telemetry/ContextSpanProcessor.test.ts +281 -0
  100. package/Tests/Server/Utils/Telemetry/InventoryDuplicateIdentity.test.ts +316 -0
  101. package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +111 -8
  102. package/Tests/Types/Database/IncludesAnyOfGroups.test.ts +106 -0
  103. package/Tests/Types/JSONFunctions.test.ts +40 -0
  104. package/Tests/Types/NetworkAutomation/AutoImportRunMerge.test.ts +235 -0
  105. package/Tests/Types/NetworkAutomation/RuleRunResult.test.ts +66 -0
  106. package/Tests/Types/NetworkAutomation/RuleRunResultDescribe.test.ts +309 -1
  107. package/Tests/Types/Telemetry/InventoryLiveness.test.ts +99 -0
  108. package/Tests/UI/Components/CustomFields/CustomFieldsDetail.test.tsx +160 -0
  109. package/Tests/UI/ReactRouterSingletonBuild.test.ts +133 -0
  110. package/Tests/UI/Rum/ReplayHeader.test.tsx +10 -2
  111. package/Tests/UI/Rum/ReplayPlayerChrome.test.tsx +514 -0
  112. package/Tests/UI/Rum/ReplayTimeFormat.test.ts +94 -0
  113. package/Tests/UI/Rum/ReplayUi.test.tsx +559 -0
  114. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +5 -0
  115. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +1 -0
  116. package/Tests/Utils/Dashboard/Components/DashboardMonitorListComponent.test.ts +11 -1
  117. package/Tests/Utils/Dashboard/LabelVariable.test.ts +257 -0
  118. package/Tests/Utils/Dashboard/VariableUrlState.test.ts +144 -14
  119. package/Tests/Utils/MetricUnitUtil.test.ts +81 -0
  120. package/Tests/Utils/Monitor/DeviceMacLearningUtil.test.ts +497 -0
  121. package/Tests/Utils/Monitor/MetricValueFormatter.test.ts +546 -0
  122. package/Tests/Utils/Monitor/MonitorTemplateCustomFieldUtil.test.ts +218 -0
  123. package/Tests/Utils/Monitor/NetworkTopologyEndpointAdoption.test.ts +1755 -0
  124. package/Tests/Utils/NetworkAutomation/AutoImportRunChain.test.ts +320 -0
  125. package/Types/BaseDatabase/IncludesAnyOfGroups.ts +77 -0
  126. package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
  127. package/Types/Dashboard/DashboardComponents/DashboardMonitorListComponent.ts +1 -0
  128. package/Types/Dashboard/DashboardVariable.ts +8 -0
  129. package/Types/JSON.ts +4 -0
  130. package/Types/JSONFunctions.ts +16 -0
  131. package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +13 -0
  132. package/Types/NetworkAutomation/RuleRunResult.ts +402 -13
  133. package/Types/SerializableObjectDictionary.ts +4 -0
  134. package/Types/Telemetry/InventoryLiveness.ts +56 -0
  135. package/UI/Components/CSVFileUpload/CSVFileUpload.tsx +9 -2
  136. package/UI/Components/CustomFields/CustomFieldsDetail.tsx +46 -21
  137. package/UI/Components/Dashboard/DashboardVariableControl.tsx +285 -0
  138. package/UI/esbuild-config.js +6 -0
  139. package/Utils/Dashboard/Components/DashboardMonitorListComponent.ts +12 -1
  140. package/Utils/Dashboard/LabelVariable.ts +152 -0
  141. package/Utils/Dashboard/VariableUrlState.ts +27 -5
  142. package/Utils/MetricUnitUtil.ts +34 -0
  143. package/Utils/Monitor/DeviceMacLearningUtil.ts +177 -0
  144. package/Utils/Monitor/MetricValueFormatter.ts +342 -0
  145. package/Utils/Monitor/MonitorTemplateCustomFieldUtil.ts +124 -0
  146. package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +7 -2
  147. package/Utils/Monitor/NetworkTopologyUtil.ts +693 -0
  148. package/Utils/NetworkAutomation/AutoImportRunChain.ts +138 -0
  149. package/build/dist/Models/DatabaseModels/GlobalConfig.js +12 -12
  150. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  151. package/build/dist/Models/DatabaseModels/InventoryItem.js +34 -1
  152. package/build/dist/Models/DatabaseModels/InventoryItem.js.map +1 -1
  153. package/build/dist/Models/DatabaseModels/NetworkDevice.js +128 -0
  154. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  155. package/build/dist/Server/EnvironmentConfig.js +44 -12
  156. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  157. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.js +14 -0
  158. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.js.map +1 -0
  159. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  160. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  161. package/build/dist/Server/Infrastructure/Redis.js +8 -8
  162. package/build/dist/Server/Infrastructure/Redis.js.map +1 -1
  163. package/build/dist/Server/Infrastructure/Status.js +10 -4
  164. package/build/dist/Server/Infrastructure/Status.js.map +1 -1
  165. package/build/dist/Server/Services/DatabaseService.js +58 -13
  166. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  167. package/build/dist/Server/Services/InventoryItemService.js +28 -1
  168. package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
  169. package/build/dist/Server/Services/MonitorTemplateService.js +116 -17
  170. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  171. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js +270 -37
  172. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js.map +1 -1
  173. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js +65 -0
  174. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js.map +1 -1
  175. package/build/dist/Server/Services/NetworkDeviceService.js +54 -0
  176. package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
  177. package/build/dist/Server/Services/UserNotificationRuleService.js +60 -25
  178. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  179. package/build/dist/Server/Types/Database/Permissions/ReadPermission.js +29 -4
  180. package/build/dist/Server/Types/Database/Permissions/ReadPermission.js.map +1 -1
  181. package/build/dist/Server/Types/Database/QueryUtil.js +36 -0
  182. package/build/dist/Server/Types/Database/QueryUtil.js.map +1 -1
  183. package/build/dist/Server/Types/Markdown.js +75 -0
  184. package/build/dist/Server/Types/Markdown.js.map +1 -1
  185. package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js +53 -18
  186. package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js.map +1 -1
  187. package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js +10 -1
  188. package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js.map +1 -1
  189. package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js +2 -2
  190. package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js.map +1 -1
  191. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +58 -35
  192. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
  193. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +29 -5
  194. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
  195. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +166 -35
  196. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  197. package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js +58 -9
  198. package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js.map +1 -1
  199. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js +9 -3
  200. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js.map +1 -1
  201. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js +26 -12
  202. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js.map +1 -1
  203. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +74 -33
  204. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
  205. package/build/dist/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.js +114 -0
  206. package/build/dist/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.js.map +1 -0
  207. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +25 -0
  208. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
  209. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +9 -1
  210. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  211. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +70 -13
  212. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
  213. package/build/dist/Types/BaseDatabase/IncludesAnyOfGroups.js +54 -0
  214. package/build/dist/Types/BaseDatabase/IncludesAnyOfGroups.js.map +1 -0
  215. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
  216. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
  217. package/build/dist/Types/Dashboard/DashboardVariable.js +1 -0
  218. package/build/dist/Types/Dashboard/DashboardVariable.js.map +1 -1
  219. package/build/dist/Types/JSON.js +1 -0
  220. package/build/dist/Types/JSON.js.map +1 -1
  221. package/build/dist/Types/JSONFunctions.js +13 -0
  222. package/build/dist/Types/JSONFunctions.js.map +1 -1
  223. package/build/dist/Types/NetworkAutomation/RuleRunResult.js +291 -6
  224. package/build/dist/Types/NetworkAutomation/RuleRunResult.js.map +1 -1
  225. package/build/dist/Types/SerializableObjectDictionary.js +4 -0
  226. package/build/dist/Types/SerializableObjectDictionary.js.map +1 -1
  227. package/build/dist/Types/Telemetry/InventoryLiveness.js +37 -0
  228. package/build/dist/Types/Telemetry/InventoryLiveness.js.map +1 -0
  229. package/build/dist/UI/Components/CSVFileUpload/CSVFileUpload.js +7 -0
  230. package/build/dist/UI/Components/CSVFileUpload/CSVFileUpload.js.map +1 -1
  231. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +9 -2
  232. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
  233. package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js +137 -0
  234. package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js.map +1 -0
  235. package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js +9 -1
  236. package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js.map +1 -1
  237. package/build/dist/Utils/Dashboard/LabelVariable.js +96 -0
  238. package/build/dist/Utils/Dashboard/LabelVariable.js.map +1 -0
  239. package/build/dist/Utils/Dashboard/VariableUrlState.js +19 -4
  240. package/build/dist/Utils/Dashboard/VariableUrlState.js.map +1 -1
  241. package/build/dist/Utils/MetricUnitUtil.js +26 -0
  242. package/build/dist/Utils/MetricUnitUtil.js.map +1 -1
  243. package/build/dist/Utils/Monitor/DeviceMacLearningUtil.js +112 -0
  244. package/build/dist/Utils/Monitor/DeviceMacLearningUtil.js.map +1 -0
  245. package/build/dist/Utils/Monitor/MetricValueFormatter.js +276 -0
  246. package/build/dist/Utils/Monitor/MetricValueFormatter.js.map +1 -0
  247. package/build/dist/Utils/Monitor/MonitorTemplateCustomFieldUtil.js +101 -0
  248. package/build/dist/Utils/Monitor/MonitorTemplateCustomFieldUtil.js.map +1 -0
  249. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +7 -2
  250. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -1
  251. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +484 -0
  252. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  253. package/build/dist/Utils/NetworkAutomation/AutoImportRunChain.js +59 -0
  254. package/build/dist/Utils/NetworkAutomation/AutoImportRunChain.js.map +1 -0
  255. package/jest.config.json +2 -0
  256. package/package.json +1 -1
  257. package/test-setup.sh +9 -9
  258. package/tsconfig.json +51 -11
  259. package/Tests/App/Dashboard/NotificationEmailPreferences.test.tsx +0 -399
@@ -1153,9 +1153,9 @@ export default class GlobalConfig extends GlobalConfigModel {
1153
1153
  })
1154
1154
  @TableColumn({
1155
1155
  type: TableColumnType.Boolean,
1156
- title: "Redis Memory Notifications",
1156
+ title: "Valkey Memory Notifications",
1157
1157
  description:
1158
- "Notify master administrators when Redis memory usage rises above the configured threshold.",
1158
+ "Notify master administrators when Valkey memory usage rises above the configured threshold.",
1159
1159
  defaultValue: false,
1160
1160
  isDefaultValueColumn: true,
1161
1161
  required: true,
@@ -1174,9 +1174,9 @@ export default class GlobalConfig extends GlobalConfigModel {
1174
1174
  })
1175
1175
  @TableColumn({
1176
1176
  type: TableColumnType.Number,
1177
- title: "Redis Memory Notification Threshold (%)",
1177
+ title: "Valkey Memory Notification Threshold (%)",
1178
1178
  description:
1179
- "Notify master administrators when Redis memory usage rises above this percentage of maxmemory.",
1179
+ "Notify master administrators when Valkey memory usage rises above this percentage of maxmemory.",
1180
1180
  defaultValue: 80,
1181
1181
  isDefaultValueColumn: true,
1182
1182
  required: true,
@@ -1195,9 +1195,9 @@ export default class GlobalConfig extends GlobalConfigModel {
1195
1195
  })
1196
1196
  @TableColumn({
1197
1197
  type: TableColumnType.Boolean,
1198
- title: "Redis Connection Saturation Notifications",
1198
+ title: "Valkey Connection Saturation Notifications",
1199
1199
  description:
1200
- "Notify master administrators when Redis client connections approach the maxclients limit or connections are being rejected.",
1200
+ "Notify master administrators when Valkey client connections approach the maxclients limit or connections are being rejected.",
1201
1201
  defaultValue: false,
1202
1202
  isDefaultValueColumn: true,
1203
1203
  required: true,
@@ -1216,9 +1216,9 @@ export default class GlobalConfig extends GlobalConfigModel {
1216
1216
  })
1217
1217
  @TableColumn({
1218
1218
  type: TableColumnType.Number,
1219
- title: "Redis Connection Notification Threshold (%)",
1219
+ title: "Valkey Connection Notification Threshold (%)",
1220
1220
  description:
1221
- "Notify master administrators when connected Redis clients rise above this percentage of maxclients.",
1221
+ "Notify master administrators when connected Valkey clients rise above this percentage of maxclients.",
1222
1222
  defaultValue: 80,
1223
1223
  isDefaultValueColumn: true,
1224
1224
  required: true,
@@ -1237,9 +1237,9 @@ export default class GlobalConfig extends GlobalConfigModel {
1237
1237
  })
1238
1238
  @TableColumn({
1239
1239
  type: TableColumnType.Boolean,
1240
- title: "Redis Key Eviction Notifications",
1240
+ title: "Valkey Key Eviction Notifications",
1241
1241
  description:
1242
- "Notify master administrators when Redis evicts keys because it reached its memory limit.",
1242
+ "Notify master administrators when Valkey evicts keys because it reached its memory limit.",
1243
1243
  defaultValue: false,
1244
1244
  isDefaultValueColumn: true,
1245
1245
  required: true,
@@ -1258,9 +1258,9 @@ export default class GlobalConfig extends GlobalConfigModel {
1258
1258
  })
1259
1259
  @TableColumn({
1260
1260
  type: TableColumnType.Boolean,
1261
- title: "Redis Persistence Failure Notifications",
1261
+ title: "Valkey Persistence Failure Notifications",
1262
1262
  description:
1263
- "Notify master administrators when a Redis RDB snapshot or AOF write fails.",
1263
+ "Notify master administrators when a Valkey RDB snapshot or AOF write fails.",
1264
1264
  defaultValue: false,
1265
1265
  isDefaultValueColumn: true,
1266
1266
  required: true,
@@ -18,7 +18,19 @@ import ObjectID from "../../Types/ObjectID";
18
18
  import Permission from "../../Types/Permission";
19
19
  import EntitySource from "../../Types/Telemetry/EntitySource";
20
20
  import EntityType from "../../Types/Telemetry/EntityType";
21
- import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
21
+ import {
22
+ InventoryLiveness,
23
+ INVENTORY_LIVE_WINDOW_MINUTES,
24
+ INVENTORY_STALE_AFTER_MINUTES,
25
+ } from "../../Types/Telemetry/InventoryLiveness";
26
+ import {
27
+ Column,
28
+ Entity,
29
+ Index,
30
+ JoinColumn,
31
+ ManyToOne,
32
+ VirtualColumn,
33
+ } from "typeorm";
22
34
  import DatabaseBaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
23
35
 
24
36
  /*
@@ -368,6 +380,37 @@ export default class InventoryItem extends DatabaseBaseModel {
368
380
  @Column({ type: ColumnType.Date, nullable: true })
369
381
  public lastSeenAt?: Date = undefined;
370
382
 
383
+ /**
384
+ * Computed at read time so filters, counts and pagination use the same
385
+ * status without persisting a value that becomes stale as time passes.
386
+ * A virtual column needs no schema migration and is never written.
387
+ *
388
+ * The badge floors elapsed minutes before applying inclusive thresholds:
389
+ * 30m59s is still Live, and 24h00m59s is still Recent. Strict comparisons
390
+ * against the following minute preserve those exact boundaries.
391
+ */
392
+ @ColumnAccessControl({ create: [], read: READ_PERMS, update: [] })
393
+ @TableColumn({
394
+ type: TableColumnType.ShortText,
395
+ required: false,
396
+ title: "Status",
397
+ description:
398
+ "Current heartbeat status: live, recent, stale, never seen, or not tracked.",
399
+ })
400
+ @VirtualColumn({
401
+ type: ColumnType.ShortText,
402
+ query: (alias: string): string => {
403
+ return `CASE
404
+ WHEN ${alias}."source" IS DISTINCT FROM '${EntitySource.Discovered}' THEN '${InventoryLiveness.NotTracked}'
405
+ WHEN ${alias}."lastSeenAt" IS NULL THEN '${InventoryLiveness.Never}'
406
+ WHEN ${alias}."lastSeenAt" > CURRENT_TIMESTAMP - INTERVAL '${INVENTORY_LIVE_WINDOW_MINUTES + 1} minutes' THEN '${InventoryLiveness.Live}'
407
+ WHEN ${alias}."lastSeenAt" > CURRENT_TIMESTAMP - INTERVAL '${INVENTORY_STALE_AFTER_MINUTES + 1} minutes' THEN '${InventoryLiveness.Recent}'
408
+ ELSE '${InventoryLiveness.Stale}'
409
+ END`;
410
+ },
411
+ })
412
+ public inventoryStatus?: InventoryLiveness = undefined;
413
+
371
414
  /*
372
415
  * Archiving is the disposal route that actually works for this table.
373
416
  *
@@ -434,6 +434,132 @@ export default class NetworkDevice extends BaseModel {
434
434
  })
435
435
  public hostname?: string = undefined;
436
436
 
437
+ /*
438
+ * --- Which switch port is this device on? ---
439
+ *
440
+ * LLDP and CDP answer that for a device that speaks them. A device that
441
+ * does not - a POS register, a handset, a kiosk, anything monitored by
442
+ * ping alone - is invisible to both, and until now the only way to draw
443
+ * its cable on the map was a NetworkDeviceLink somebody typed in and then
444
+ * had to keep true by hand (issue #3489).
445
+ *
446
+ * The switches already know. Every walked switch reports its forwarding
447
+ * table, and the server keeps one NetworkEndpoint row per MAC it learned
448
+ * with the switch and port that learned it. This column is what lets a
449
+ * device be recognised in that inventory: a device whose MAC matches an
450
+ * endpoint's is drawn on the port that endpoint was learned on, and the
451
+ * link moves with the device when it is re-cabled, because the next walk
452
+ * learns the MAC somewhere else.
453
+ *
454
+ * Optional twice over. A device whose hostname is an IP address that a
455
+ * walked device's ARP table resolves is matched by address without it,
456
+ * and the MAC learned that way is written here so the match survives the
457
+ * ARP entry ageing out. The server fills an EMPTY value, and corrects a
458
+ * value it learned itself when the table later binds the address to a
459
+ * different MAC (isMacAddressLearned says which); anything an operator
460
+ * typed is left alone. Normalised to lowercase colon form on write; any
461
+ * of the usual spellings is accepted.
462
+ */
463
+ @ColumnAccessControl({
464
+ create: [
465
+ Permission.ProjectOwner,
466
+ Permission.ProjectAdmin,
467
+ Permission.ProjectMember,
468
+ Permission.SettingsAdmin,
469
+ Permission.SettingsMember,
470
+ Permission.CreateNetworkDevice,
471
+ ],
472
+ read: [
473
+ Permission.ProjectOwner,
474
+ Permission.ProjectAdmin,
475
+ Permission.ProjectMember,
476
+ Permission.Viewer,
477
+ Permission.SettingsAdmin,
478
+ Permission.SettingsMember,
479
+ Permission.SettingsViewer,
480
+ Permission.ReadNetworkDevice,
481
+ ],
482
+ update: [
483
+ Permission.ProjectOwner,
484
+ Permission.ProjectAdmin,
485
+ Permission.ProjectMember,
486
+ Permission.SettingsAdmin,
487
+ Permission.SettingsMember,
488
+ Permission.EditNetworkDevice,
489
+ ],
490
+ })
491
+ @TableColumn({
492
+ required: false,
493
+ type: TableColumnType.ShortText,
494
+ canReadOnRelationQuery: true,
495
+ title: "MAC Address",
496
+ description:
497
+ "MAC address of this device. Lets the topology map find the switch port it is plugged into from the forwarding tables of walked switches, for a device that speaks neither LLDP nor CDP (one monitored by ping alone). Optional: a device whose hostname is an IP address that a walked router's ARP table resolves is matched by address, and the MAC learned that way is stored here.",
498
+ example: "aa:bb:cc:dd:ee:ff",
499
+ })
500
+ @Column({
501
+ nullable: true,
502
+ type: ColumnType.ShortText,
503
+ length: ColumnLength.ShortText,
504
+ })
505
+ public macAddress?: string = undefined;
506
+
507
+ /*
508
+ * Where the MAC above came from, when the server wrote it.
509
+ *
510
+ * A MAC an operator typed is theirs and is never touched. A MAC the ARP
511
+ * pass learned is only as good as the router's table was on the walk
512
+ * that learned it - an address re-leased to a laptop the day before the
513
+ * register was plugged in stamps the laptop's MAC on the register - so
514
+ * the pass may correct its OWN answer when a later walk binds the
515
+ * address to a different MAC, and this flag is what tells the two apart.
516
+ * Set by the learner's write, cleared by any write of macAddress that
517
+ * comes through the API (see NetworkDeviceService).
518
+ */
519
+ @ColumnAccessControl({
520
+ create: [
521
+ Permission.ProjectOwner,
522
+ Permission.ProjectAdmin,
523
+ Permission.ProjectMember,
524
+ Permission.SettingsAdmin,
525
+ Permission.SettingsMember,
526
+ Permission.CreateNetworkDevice,
527
+ ],
528
+ read: [
529
+ Permission.ProjectOwner,
530
+ Permission.ProjectAdmin,
531
+ Permission.ProjectMember,
532
+ Permission.Viewer,
533
+ Permission.SettingsAdmin,
534
+ Permission.SettingsMember,
535
+ Permission.SettingsViewer,
536
+ Permission.ReadNetworkDevice,
537
+ ],
538
+ update: [
539
+ Permission.ProjectOwner,
540
+ Permission.ProjectAdmin,
541
+ Permission.ProjectMember,
542
+ Permission.SettingsAdmin,
543
+ Permission.SettingsMember,
544
+ Permission.EditNetworkDevice,
545
+ ],
546
+ })
547
+ @TableColumn({
548
+ required: false,
549
+ type: TableColumnType.Boolean,
550
+ canReadOnRelationQuery: true,
551
+ title: "MAC Address Learned",
552
+ description:
553
+ "True when the MAC Address was filled in from a walked device's ARP table rather than typed. A learned MAC is corrected when a later walk binds the device's address to a different MAC; a typed one is never touched.",
554
+ defaultValue: false,
555
+ })
556
+ @Column({
557
+ nullable: true,
558
+ type: ColumnType.Boolean,
559
+ default: false,
560
+ })
561
+ public isMacAddressLearned?: boolean = undefined;
562
+
437
563
  @ColumnAccessControl({
438
564
  create: [
439
565
  Permission.ProjectOwner,
@@ -472,32 +472,63 @@ export const HomeHostname: Hostname = Hostname.fromString(
472
472
 
473
473
  export const Env: string = process.env["NODE_ENV"] || "production";
474
474
 
475
- // Redis does not require password.
476
- export const RedisHostname: string = process.env["REDIS_HOST"] || "redis";
477
- export const RedisPort: Port = new Port(process.env["REDIS_PORT"] || "6379");
478
- export const RedisDb: number = Number(process.env["REDIS_DB"]) || 0;
479
- export const RedisUsername: string = process.env["REDIS_USERNAME"] || "default";
480
- export const RedisPassword: string =
481
- process.env["REDIS_PASSWORD"] || "password";
475
+ /*
476
+ * The cache and queue tier runs Valkey -- the BSD-licensed fork of Redis 7.2 --
477
+ * so its settings are named VALKEY_*. Any Redis-protocol server works here,
478
+ * including real Redis, which is why the in-code names below stay `Redis*`:
479
+ * they describe the protocol, not the vendor.
480
+ *
481
+ * The REDIS_* names these shipped under are still read, so an existing
482
+ * config.env or values.yaml keeps working untouched. They are DEPRECATED, not
483
+ * removed; VALKEY_* wins when both are set. Nothing here may start requiring
484
+ * the new name -- a self-hoster who never edits config.env must keep running.
485
+ *
486
+ * `||` here is load-bearing and must not be "modernised" to `??`. Compose
487
+ * materialises an unset variable as an EMPTY STRING, and docker-compose.base.yml
488
+ * lists every VALKEY_* name, so the new name is always *defined* inside our
489
+ * containers. Under `??` an empty VALKEY_PASSWORD would beat the operator's real
490
+ * REDIS_PASSWORD and the app would silently fall through to the literal default
491
+ * below.
492
+ */
493
+ export function getCacheEnvVar(suffix: string): string | undefined {
494
+ return (
495
+ process.env[`VALKEY_${suffix}`] ||
496
+ process.env[`REDIS_${suffix}`] ||
497
+ undefined
498
+ );
499
+ }
500
+
501
+ /*
502
+ * The cache does not require a password.
503
+ *
504
+ * The default hostname stays "redis" even though the compose service is now
505
+ * `valkey`. This default is only reached when NEITHER variable is set, which
506
+ * never happens in our own compose or Helm -- both always set them. What it does
507
+ * cover is hand-written Kubernetes manifests, a bare `docker run`, and third
508
+ * party compose files, where the Service has been called `redis` for years.
509
+ * Compose is unaffected either way: its valkey service answers to `redis` too.
510
+ */
511
+ export const RedisHostname: string = getCacheEnvVar("HOST") || "redis";
512
+ export const RedisPort: Port = new Port(getCacheEnvVar("PORT") || "6379");
513
+ export const RedisDb: number = Number(getCacheEnvVar("DB")) || 0;
514
+ export const RedisUsername: string = getCacheEnvVar("USERNAME") || "default";
515
+ export const RedisPassword: string = getCacheEnvVar("PASSWORD") || "password";
482
516
 
483
- export const RedisTlsCa: string | undefined =
484
- process.env["REDIS_TLS_CA"] || undefined;
517
+ export const RedisTlsCa: string | undefined = getCacheEnvVar("TLS_CA");
485
518
 
486
- export const RedisTlsCert: string | undefined =
487
- process.env["REDIS_TLS_CERT"] || undefined;
519
+ export const RedisTlsCert: string | undefined = getCacheEnvVar("TLS_CERT");
488
520
 
489
- export const RedisTlsKey: string | undefined =
490
- process.env["REDIS_TLS_KEY"] || undefined;
521
+ export const RedisTlsKey: string | undefined = getCacheEnvVar("TLS_KEY");
491
522
 
492
523
  export const RedisTlsSentinelMode: boolean =
493
- process.env["REDIS_TLS_SENTINEL_MODE"] === "true";
524
+ getCacheEnvVar("TLS_SENTINEL_MODE") === "true";
494
525
 
495
526
  export const ShouldRedisTlsEnable: boolean = Boolean(
496
527
  RedisTlsCa || (RedisTlsCert && RedisTlsKey),
497
528
  );
498
529
 
499
- export const RedisIPFamily: number = process.env["REDIS_IP_FAMILY"]
500
- ? Number(process.env["REDIS_IP_FAMILY"])
530
+ export const RedisIPFamily: number = getCacheEnvVar("IP_FAMILY")
531
+ ? Number(getCacheEnvVar("IP_FAMILY"))
501
532
  : 4;
502
533
 
503
534
  export const IsProduction: boolean =
@@ -0,0 +1,25 @@
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ export class AddMacAddressToNetworkDevice1791700000000
4
+ implements MigrationInterface
5
+ {
6
+ public name: string = "AddMacAddressToNetworkDevice1791700000000";
7
+
8
+ public async up(queryRunner: QueryRunner): Promise<void> {
9
+ await queryRunner.query(
10
+ `ALTER TABLE "NetworkDevice" ADD "macAddress" character varying(100)`,
11
+ );
12
+ await queryRunner.query(
13
+ `ALTER TABLE "NetworkDevice" ADD "isMacAddressLearned" boolean DEFAULT false`,
14
+ );
15
+ }
16
+
17
+ public async down(queryRunner: QueryRunner): Promise<void> {
18
+ await queryRunner.query(
19
+ `ALTER TABLE "NetworkDevice" DROP COLUMN "isMacAddressLearned"`,
20
+ );
21
+ await queryRunner.query(
22
+ `ALTER TABLE "NetworkDevice" DROP COLUMN "macAddress"`,
23
+ );
24
+ }
25
+ }
@@ -577,6 +577,7 @@ import { AddTelemetryIngestionKeyType1791300000000 } from "./1791300000000-AddTe
577
577
  import { SessionReplayRecordEverySessionByDefault1791400000000 } from "./1791400000000-SessionReplayRecordEverySessionByDefault";
578
578
  import { WidenCustomFieldDropdownOptions1791500000000 } from "./1791500000000-WidenCustomFieldDropdownOptions";
579
579
  import { AddCustomFieldValueMapping1791600000000 } from "./1791600000000-AddCustomFieldValueMapping";
580
+ import { AddMacAddressToNetworkDevice1791700000000 } from "./1791700000000-AddMacAddressToNetworkDevice";
580
581
 
581
582
  export default [
582
583
  InitialMigration,
@@ -1158,4 +1159,5 @@ export default [
1158
1159
  SessionReplayRecordEverySessionByDefault1791400000000,
1159
1160
  WidenCustomFieldDropdownOptions1791500000000,
1160
1161
  AddCustomFieldValueMapping1791600000000,
1162
+ AddMacAddressToNetworkDevice1791700000000,
1161
1163
  ];
@@ -71,9 +71,9 @@ export default abstract class Redis {
71
71
  // Listen to 'error' events to the Redis connection
72
72
  this.client.on("error", (error: Error) => {
73
73
  if ((error as any).code === "ECONNRESET") {
74
- logger.error("Connection to Redis Session Store timed out.");
74
+ logger.error("Connection to Valkey Session Store timed out.");
75
75
  } else if ((error as any).code === "ECONNREFUSED") {
76
- logger.error("Connection to Redis Session Store refused!");
76
+ logger.error("Connection to Valkey Session Store refused!");
77
77
  } else {
78
78
  logger.error(error);
79
79
  }
@@ -82,16 +82,16 @@ export default abstract class Redis {
82
82
  // Listen to 'reconnecting' event to Redis
83
83
  this.client.on("reconnecting", () => {
84
84
  if (this.client?.status === "reconnecting") {
85
- logger.error("Reconnecting to Redis Session Store...");
85
+ logger.error("Reconnecting to Valkey Session Store...");
86
86
  } else {
87
- logger.error("Error reconnecting to Redis Session Store.");
87
+ logger.error("Error reconnecting to Valkey Session Store.");
88
88
  }
89
89
  });
90
90
 
91
91
  // Listen to the 'connect' event to Redis
92
92
  this.client.on("connect", (err: Error) => {
93
93
  if (!err) {
94
- logger.debug("Connected to Redis Session Store!");
94
+ logger.debug("Connected to Valkey Session Store!");
95
95
  }
96
96
  });
97
97
 
@@ -137,7 +137,7 @@ export default abstract class Redis {
137
137
  );
138
138
  }
139
139
 
140
- logger.debug("Cannot connect to Redis. Retrying again in 5 seconds");
140
+ logger.debug("Cannot connect to Valkey. Retrying again in 5 seconds");
141
141
  // sleep for 5 seconds.
142
142
 
143
143
  await Sleep.sleep(5000);
@@ -163,7 +163,7 @@ export default abstract class Redis {
163
163
 
164
164
  return this.client;
165
165
  } catch (err) {
166
- logger.error("Redis Connection Failed");
166
+ logger.error("Valkey Connection Failed");
167
167
  logger.error(err);
168
168
  throw err;
169
169
  }
@@ -191,7 +191,7 @@ export default abstract class Redis {
191
191
 
192
192
  return true;
193
193
  } catch (err) {
194
- logger.error("Redis Connection Lost");
194
+ logger.error("Valkey Connection Lost");
195
195
  logger.error(err);
196
196
  return false;
197
197
  }
@@ -17,12 +17,18 @@ export default class InfrastructureStatus {
17
17
  logger.info("Checking infrastructure status");
18
18
 
19
19
  if (data.checkRedisStatus) {
20
- logger.info("Checking Redis status");
20
+ logger.info("Checking Valkey status");
21
21
  if (!(await Redis.checkConnnectionStatus())) {
22
- logger.info("Redis is not connected");
23
- throw new DatabaseNotConnectedException("Redis is not connected");
22
+ logger.info("Valkey is not connected");
23
+ /*
24
+ * This message is not log-only: StatusAPI passes it straight to
25
+ * sendErrorResponse, so it is the body of GET /api/status/global-cache,
26
+ * /ready and /live. It is the first thing an operator curls when the
27
+ * cache is down, which is why it says what the container actually is.
28
+ */
29
+ throw new DatabaseNotConnectedException("Valkey is not connected");
24
30
  }
25
- logger.info("Redis is connected");
31
+ logger.info("Valkey is connected");
26
32
  }
27
33
 
28
34
  if (data.checkPostgresStatus) {
@@ -46,6 +46,7 @@ import Protocol from "../../Types/API/Protocol";
46
46
  import Route from "../../Types/API/Route";
47
47
  import URL from "../../Types/API/URL";
48
48
  import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
49
+ import Sort from "../../Types/BaseDatabase/Sort";
49
50
  import SortOrder from "../../Types/BaseDatabase/SortOrder";
50
51
  import { getMaxLengthFromTableColumnType } from "../../Types/Database/ColumnLength";
51
52
  import Columns from "../../Types/Database/Columns";
@@ -1636,12 +1637,28 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
1636
1637
  * A query builder scoped to exactly the rows this read may see, and — this
1637
1638
  * is the whole point — scoped so that each of them appears ONCE.
1638
1639
  *
1639
- * The permission pipeline expresses a label-scoped read as a condition on
1640
- * the access-control RELATION: `{ labels: [permittedIds] }` for an allow
1641
- * permission, `{ labels: { _id: NotIn([...]) } }` for a block. Handed to
1642
- * TypeORM's FindOptions machinery those become a join through the
1643
- * many-to-many junction table — and a join multiplies rows. A device
1644
- * carrying two permitted labels comes back twice.
1640
+ * The permission pipeline expresses a label-scoped ALLOW as a condition on
1641
+ * the access-control RELATION `{ labels: [permittedIds] }`, which
1642
+ * QueryUtil.serializeQuery then nests into `{ labels: { _id: <a set
1643
+ * membership operator> } }`. Handed to TypeORM's FindOptions machinery that
1644
+ * becomes a join through the many-to-many junction table — and a join
1645
+ * multiplies rows. A device carrying two permitted labels comes back twice.
1646
+ *
1647
+ * A label BLOCK no longer takes that route, and it is worth being precise
1648
+ * about why, because the shape reads like the allow half's mirror image and
1649
+ * is not. A relation condition cannot express "has NONE of these labels" at
1650
+ * all: a device labelled {blocked, other} still matches the join through its
1651
+ * "other" row. So ReadPermission writes the block as a flat predicate on the
1652
+ * owner's own id instead — `_id NOT IN (SELECT ownerId FROM the junction
1653
+ * table WHERE labelId IN (...))`. Flat means no join, which means no
1654
+ * duplicate rows, which means the block half needs nothing from this method
1655
+ * beyond the cheap path below.
1656
+ *
1657
+ * The allow half is not the only producer of relation-keyed conditions
1658
+ * either. `@CanAccessIfCanReadOn` makes BasePermission write
1659
+ * `query[relation] = { <the related model's access-control column>: ids }`
1660
+ * for StatusPageResource, IncidentInternalNote, AlertEpisodeMember and
1661
+ * others, and that arrives here indistinguishable from a label allow.
1645
1662
  *
1646
1663
  * `findBy` never notices, because entity hydration de-duplicates by primary
1647
1664
  * key on the way out. An aggregate has no such step: COUNT(*) would report
@@ -1656,9 +1673,10 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
1656
1673
  * by a DISTINCT subquery of ids. The joins live inside that subquery, where
1657
1674
  * duplicate rows collapse before anything is added up.
1658
1675
  *
1659
- * The subquery is skipped when the query is flat — which is every read by a
1660
- * user whose permissions are not label-scoped, i.e. the hot path so the
1661
- * ordinary full-fleet aggregate stays a single sequential scan.
1676
+ * The subquery is skipped when the query is flat — every read by a user with
1677
+ * no label-scoped allow grant and no `@CanAccessIfCanReadOn` relation, which
1678
+ * is the hot path, and the block half above with it — so the ordinary
1679
+ * full-fleet aggregate stays a single sequential scan.
1662
1680
  */
1663
1681
  private buildAggregateScope(
1664
1682
  query: Query<TBaseModel>,
@@ -1698,10 +1716,14 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
1698
1716
  `"${this.modelName}"."_id" IN (${scopeBuilder.getQuery()})`,
1699
1717
  );
1700
1718
  /*
1701
- * The subquery's own bound values. Its placeholders are auto-named
1702
- * (`orm_param_N`) and the outer builder generates none of its own here —
1703
- * its only WHERE is the raw string above so there is nothing to clash
1704
- * with. Caller-supplied parameters are named and applied later.
1719
+ * The subquery's own bound values, copied across under whatever names they
1720
+ * already carry. Two naming schemes reach this line: TypeORM auto-names
1721
+ * the parameters it creates itself (`orm_param_N`), but a label predicate
1722
+ * arrives as a `Raw` operator carrying its own object-literal parameters,
1723
+ * and TypeORM registers those verbatim — QueryHelper names them with ten
1724
+ * random characters apiece. Neither can clash with the outer builder,
1725
+ * which generates no parameters of its own here: its only WHERE is the raw
1726
+ * string above. Caller-supplied parameters are named and applied later.
1705
1727
  */
1706
1728
  queryBuilder.setParameters(scopeBuilder.getParameters());
1707
1729
 
@@ -2149,6 +2171,36 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
2149
2171
  throw new BadDataException("GroupBy is currently not supported");
2150
2172
  }
2151
2173
 
2174
+ /*
2175
+ * Complete the caller's sort into a TOTAL order.
2176
+ *
2177
+ * Everything here pages with LIMIT/OFFSET, and OFFSET only means
2178
+ * anything against an ordering that has no ties: rows that compare
2179
+ * equal may come back in any order, and Postgres is free to return them
2180
+ * differently for the two queries that fetch page 1 and page 2. A tied
2181
+ * row can therefore be served on both pages while another is served on
2182
+ * neither — the same row apparently listed twice.
2183
+ *
2184
+ * Ties are not exotic. The Inventory list sorts on `lastSeenAt`, which
2185
+ * the entity reconciler rewrites for every live entity every few
2186
+ * minutes, and the inventory mirror stamps one identical timestamp
2187
+ * across a whole page of rows; "created at" sorts tie for anything
2188
+ * bulk-inserted in one statement. Appending the primary key breaks
2189
+ * every tie deterministically and costs nothing — `_id` is the PK, and
2190
+ * it is already in the select (see above), so this adds no column to
2191
+ * the projection and nothing to strip afterwards.
2192
+ */
2193
+ const sortSoFar: Dictionary<SortOrder> =
2194
+ (onBeforeFind.sort as Dictionary<SortOrder> | undefined) || {};
2195
+
2196
+ if (!sortSoFar["_id"]) {
2197
+ // Copied, never mutated: callers reuse their sort objects across queries.
2198
+ onBeforeFind.sort = {
2199
+ ...sortSoFar,
2200
+ _id: SortOrder.Ascending,
2201
+ } as Sort<TBaseModel>;
2202
+ }
2203
+
2152
2204
  const items: Array<TBaseModel> = await this.getRepository().find({
2153
2205
  skip: onBeforeFind.skip.toNumber(),
2154
2206
  take: onBeforeFind.limit.toNumber(),
@@ -57,6 +57,27 @@ export class InventoryItemService extends DatabaseService<Model> {
57
57
  ): Promise<OnCreate<Model>> {
58
58
  const data: Model = createBy.data;
59
59
 
60
+ /*
61
+ * Who is calling decides this, not what they sent.
62
+ *
63
+ * The two machine writers — the ingest reconciler and the inventory
64
+ * mirror — construct the model themselves and create it with
65
+ * `props: { isRoot: true }`. Everything else is a person or an API
66
+ * client. Keying the branch on "did the payload include an entityKey"
67
+ * instead let any caller opt out of every check below simply by sending
68
+ * one: the manual-type restriction, the required display name, the
69
+ * canonicalize-then-hash identity derivation and the firstSeenAt /
70
+ * lastSeenAt stamp were all skipped, and a hand-supplied
71
+ * `source: "discovered"` was taken at face value — putting a row in
72
+ * Inventory that claims to have been discovered from telemetry, of a type
73
+ * only ingest is supposed to create, which the prune sweep then treats as
74
+ * TTL-reapable.
75
+ */
76
+ if (createBy.props.isRoot !== true) {
77
+ delete data.entityKey;
78
+ data.source = EntitySource.Manual;
79
+ }
80
+
60
81
  if (!data.entityKey) {
61
82
  data.source = EntitySource.Manual;
62
83
  }
@@ -150,7 +171,16 @@ export class InventoryItemService extends DatabaseService<Model> {
150
171
  const countByType: Map<EntityType, number> = new Map<EntityType, number>();
151
172
 
152
173
  for (const entity of data.entities) {
153
- if (!REGISTRY_PROMOTED_TYPES.has(entity.entityType)) {
174
+ /*
175
+ * Both promotion gates, repeated at the write boundary. The caller
176
+ * applies them too, but this method is public and the cost of a missed
177
+ * gate here is a registry row that should never have existed — see
178
+ * `ExtractedEntity.membershipOnly` for what the per-entity flag means.
179
+ */
180
+ if (
181
+ entity.membershipOnly ||
182
+ !REGISTRY_PROMOTED_TYPES.has(entity.entityType)
183
+ ) {
154
184
  continue;
155
185
  }
156
186
  try {