@oneuptime/common 12.0.2 → 12.0.4

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 (384) hide show
  1. package/Models/DatabaseModels/CephCluster.ts +14 -0
  2. package/Models/DatabaseModels/CodeRepository.ts +14 -6
  3. package/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.ts +14 -0
  4. package/Models/DatabaseModels/DockerHost.ts +14 -0
  5. package/Models/DatabaseModels/GlobalOidcProject.ts +18 -0
  6. package/Models/DatabaseModels/GlobalSsoProject.ts +18 -0
  7. package/Models/DatabaseModels/Index.ts +2 -0
  8. package/Models/DatabaseModels/IoTFleet.ts +14 -0
  9. package/Models/DatabaseModels/KubernetesCluster.ts +14 -0
  10. package/Models/DatabaseModels/LogDropFilter.ts +9 -20
  11. package/Models/DatabaseModels/NetworkDevice.ts +14 -0
  12. package/Models/DatabaseModels/NetworkInterface.ts +18 -0
  13. package/Models/DatabaseModels/Project.ts +16 -2
  14. package/Models/DatabaseModels/ProxmoxCluster.ts +14 -0
  15. package/Models/DatabaseModels/Service.ts +30 -0
  16. package/Models/DatabaseModels/StatusPageMonitorRule.ts +911 -0
  17. package/Models/DatabaseModels/StatusPagePrivateUser.ts +30 -0
  18. package/Models/DatabaseModels/StatusPageResource.ts +73 -0
  19. package/Models/DatabaseModels/TraceDropFilter.ts +9 -20
  20. package/Models/DatabaseModels/User.ts +30 -0
  21. package/Server/API/AIAgentDataAPI.ts +31 -0
  22. package/Server/API/BaseAPI.ts +13 -2
  23. package/Server/API/DashboardAPI.ts +458 -12
  24. package/Server/API/GitHubAPI.ts +119 -283
  25. package/Server/API/UserAPI.ts +263 -1
  26. package/Server/EnvironmentConfig.ts +38 -4
  27. package/Server/Infrastructure/GlobalCache.ts +152 -0
  28. package/Server/Infrastructure/Postgres/DataSourceOptions.ts +16 -0
  29. package/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.ts +123 -0
  30. package/Server/Infrastructure/Postgres/SchemaMigrations/1786018109307-AddPerUserPasswordSalt.ts +21 -0
  31. package/Server/Infrastructure/Postgres/SchemaMigrations/1786023262402-WidenHashedStringColumnsForScrypt.ts +68 -0
  32. package/Server/Infrastructure/Postgres/SchemaMigrations/1786100000000-RestoreServiceLowerNameIndex.ts +100 -0
  33. package/Server/Infrastructure/Postgres/SchemaMigrations/1786200000000-RestoreDroppedUniqueIndexes.ts +214 -0
  34. package/Server/Infrastructure/Postgres/SchemaMigrations/1786300000000-QuarantineUnboundGitHubInstallations.ts +69 -0
  35. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +12 -0
  36. package/Server/Services/AlertStateTimelineService.ts +20 -0
  37. package/Server/Services/AnalyticsDatabaseService.ts +72 -1
  38. package/Server/Services/CephClusterService.ts +26 -86
  39. package/Server/Services/CloudResourceService.ts +23 -83
  40. package/Server/Services/CodeRepositoryService.ts +105 -2
  41. package/Server/Services/DatabaseService.ts +296 -2
  42. package/Server/Services/DockerHostService.ts +19 -79
  43. package/Server/Services/DockerSwarmClusterService.ts +27 -88
  44. package/Server/Services/HostService.ts +46 -99
  45. package/Server/Services/IncidentStateTimelineService.ts +20 -0
  46. package/Server/Services/Index.ts +2 -0
  47. package/Server/Services/IoTFleetService.ts +19 -79
  48. package/Server/Services/KubernetesClusterService.ts +17 -78
  49. package/Server/Services/LabelService.ts +57 -10
  50. package/Server/Services/MetricTypeService.ts +84 -0
  51. package/Server/Services/MonitorService.ts +89 -14
  52. package/Server/Services/MonitorStatusTimelineService.ts +29 -0
  53. package/Server/Services/MonitorTemplateService.ts +65 -0
  54. package/Server/Services/OpenTelemetryIngestService.ts +230 -26
  55. package/Server/Services/PodmanHostService.ts +19 -79
  56. package/Server/Services/ProxmoxClusterService.ts +23 -83
  57. package/Server/Services/RumApplicationService.ts +19 -80
  58. package/Server/Services/ScheduledMaintenanceStateTimelineService.ts +21 -0
  59. package/Server/Services/ServerlessFunctionService.ts +24 -85
  60. package/Server/Services/ServiceService.ts +299 -73
  61. package/Server/Services/StatusPageMonitorRuleEngineService.ts +752 -0
  62. package/Server/Services/StatusPageMonitorRuleService.ts +471 -0
  63. package/Server/Services/TelemetryEntityRelationshipService.ts +1 -0
  64. package/Server/Services/TelemetryEntityService.ts +1 -0
  65. package/Server/Services/UserService.ts +184 -0
  66. package/Server/Utils/AI/Toolbox/CodeTools.ts +45 -4
  67. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +13 -0
  68. package/Server/Utils/CodeRepository/GitHub/GitHub.ts +157 -4
  69. package/Server/Utils/CodeRepository/GitHub/GitHubInstallationBinding.ts +130 -0
  70. package/Server/Utils/Database/ProjectScopedReferenceValidator.ts +170 -39
  71. package/Server/Utils/Monitor/MonitorResource.ts +63 -36
  72. package/Server/Utils/Monitor/MonitorStatusTimeline.ts +36 -0
  73. package/Server/Utils/Monitor/MonitorStepsProjectValidator.ts +148 -127
  74. package/Server/Utils/Monitor/MonitorStepsReferenceExtractor.ts +434 -0
  75. package/Server/Utils/PasswordHash.ts +306 -0
  76. package/Server/Utils/SingleFlight.ts +83 -0
  77. package/Server/Utils/StatusPage/MonitorRulePatternValidator.ts +46 -0
  78. package/Server/Utils/Telemetry/EntityRegistry.ts +107 -26
  79. package/Server/Utils/Telemetry/ResourceHeartbeat.ts +314 -0
  80. package/Server/Utils/Telemetry/Telemetry.ts +351 -122
  81. package/Tests/App/Dashboard/OverviewCustomFields.test.tsx +333 -0
  82. package/Tests/App/Dashboard/UserCustomFields.test.tsx +392 -0
  83. package/Tests/App/Dashboard/WidgetCatalog.test.ts +2 -0
  84. package/Tests/Models/DatabaseModels/ColumnTransformerInvariants.test.ts +306 -0
  85. package/Tests/Models/DatabaseModels/NumberColumnDeserialization.test.ts +333 -0
  86. package/Tests/Models/DatabaseModels/StatusPageMonitorRuleModel.test.ts +190 -0
  87. package/Tests/Server/API/AIAgentDataRepositoryToken.test.ts +381 -0
  88. package/Tests/Server/API/DashboardPublicAttributeValuesAPI.test.ts +943 -0
  89. package/Tests/Server/API/DashboardPublicMetricsAggregateAPI.test.ts +1296 -0
  90. package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +26 -0
  91. package/Tests/Server/API/DashboardPublicTemplatePayloads.test.ts +598 -0
  92. package/Tests/Server/API/GitHubAppInstallationBindingAPI.test.ts +539 -0
  93. package/Tests/Server/API/Helpers.ts +6 -1
  94. package/Tests/Server/API/UserProjectsAPI.test.ts +852 -0
  95. package/Tests/Server/Infrastructure/GlobalCache.test.ts +354 -0
  96. package/Tests/Server/Infrastructure/PostgresLockTimeoutOptions.test.ts +181 -0
  97. package/Tests/Server/Infrastructure/SemaphoreMutex.test.ts +215 -0
  98. package/Tests/Server/Services/AddPerUserPasswordSaltMigration.test.ts +178 -0
  99. package/Tests/Server/Services/AnalyticsDatabasePaginationStability.test.ts +582 -0
  100. package/Tests/Server/Services/AnalyticsDatabaseService.test.ts +5 -0
  101. package/Tests/Server/Services/CodeRepositoryInstallationBinding.test.ts +305 -0
  102. package/Tests/Server/Services/CodeRepositoryResolutionBinding.test.ts +147 -0
  103. package/Tests/Server/Services/DatabaseServicePerUserPasswordSalt.test.ts +870 -0
  104. package/Tests/Server/Services/DatabaseServiceUpdateColumnsIfUnlocked.test.ts +342 -0
  105. package/Tests/Server/Services/DropFilterSaveValidation.test.ts +245 -0
  106. package/Tests/Server/Services/HostServiceUpdateLastSeen.test.ts +163 -15
  107. package/Tests/Server/Services/MetricTypeAttachServices.test.ts +222 -0
  108. package/Tests/Server/Services/MissingReferenceWriteGuard.test.ts +614 -0
  109. package/Tests/Server/Services/MonitorStatusTimelineFastPath.test.ts +23 -0
  110. package/Tests/Server/Services/OpenTelemetryServiceResolutionCache.test.ts +568 -0
  111. package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +305 -56
  112. package/Tests/Server/Services/RestoreDroppedUniqueIndexesMigration.test.ts +386 -0
  113. package/Tests/Server/Services/RestoreServiceLowerNameIndexMigration.test.ts +240 -0
  114. package/Tests/Server/Services/ServiceUpdateLastSeenSplitGates.test.ts +705 -0
  115. package/Tests/Server/Services/StatusPageMonitorRuleEngineService.test.ts +1719 -0
  116. package/Tests/Server/Services/StatusPageMonitorRuleHooks.test.ts +1212 -0
  117. package/Tests/Server/Services/StatusPageMonitorRuleMonitorCreateHook.test.ts +228 -0
  118. package/Tests/Server/Services/UserServiceFirstMasterAdminElection.test.ts +885 -0
  119. package/Tests/Server/Services/WidenHashedStringColumnsForScryptMigration.test.ts +220 -0
  120. package/Tests/Server/Utils/AI/CodeTools.test.ts +47 -1
  121. package/Tests/Server/Utils/AI/CodeWriteTools.test.ts +10 -0
  122. package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +267 -0
  123. package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +120 -0
  124. package/Tests/Server/Utils/CodeRepository/GitHubInstallationBinding.test.ts +292 -0
  125. package/Tests/Server/Utils/Database/ProjectScopedReferenceValidator.test.ts +260 -13
  126. package/Tests/Server/Utils/EntityRegistryRowFence.test.ts +302 -0
  127. package/Tests/Server/Utils/GitHubInstallationOwnershipVerification.test.ts +340 -0
  128. package/Tests/Server/Utils/GitHubWebhookAndTreeCacheIsolation.test.ts +250 -0
  129. package/Tests/Server/Utils/MetricTypeIndexConvergence.test.ts +598 -0
  130. package/Tests/Server/Utils/Monitor/MonitorStatusTimelineUnusableStatus.test.ts +154 -0
  131. package/Tests/Server/Utils/Monitor/MonitorStepsProjectValidator.test.ts +638 -92
  132. package/Tests/Server/Utils/Monitor/MonitorStepsReferenceExtractor.test.ts +724 -0
  133. package/Tests/Server/Utils/PasswordHash.test.ts +555 -0
  134. package/Tests/Server/Utils/SingleFlight.test.ts +221 -0
  135. package/Tests/Types/Dashboard/NetworkDashboardTemplate.test.ts +793 -0
  136. package/Tests/Types/Database/BigIntColumnTransformer.test.ts +147 -0
  137. package/Tests/Types/Database/ColumnLength.test.ts +7 -1
  138. package/Tests/Types/Database/DatabasePropertyTransformer.test.ts +140 -0
  139. package/Tests/Types/Database/NumericColumnValue.test.ts +127 -0
  140. package/Tests/Types/Decimal.test.ts +37 -0
  141. package/Tests/Types/HashedStringPerUserSalt.test.ts +476 -0
  142. package/Tests/Types/Monitor/DockerAlertTemplates.test.ts +293 -0
  143. package/Tests/Types/Monitor/HostAlertTemplates.test.ts +249 -0
  144. package/Tests/Types/Monitor/IotAlertTemplates.test.ts +320 -0
  145. package/Tests/Types/Monitor/PodmanAlertTemplates.test.ts +271 -0
  146. package/Tests/Types/Monitor/SnmpVersionParsing.test.ts +152 -0
  147. package/Tests/Types/Port.test.ts +38 -0
  148. package/Tests/UI/Components/CustomFields/CustomFieldsDetail.test.tsx +696 -0
  149. package/Tests/UI/Components/LogsHistogram.test.tsx +276 -0
  150. package/Tests/UI/Components/UseLiveLogsRefresh.test.tsx +489 -0
  151. package/Tests/UI/Components/UseLogsHistogram.test.tsx +500 -0
  152. package/Tests/UI/Utils/UserProjectsModelAPI.test.ts +714 -0
  153. package/Tests/Utils/Dashboard/Components/DashboardComponentsUtil.test.ts +120 -0
  154. package/Tests/Utils/Dashboard/DashboardNetworkMapComponent.test.ts +327 -0
  155. package/Tests/Utils/TeamMembersByProject.test.ts +655 -0
  156. package/Types/Dashboard/DashboardComponentType.ts +1 -0
  157. package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
  158. package/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.ts +40 -0
  159. package/Types/Dashboard/DashboardTemplates.ts +397 -0
  160. package/Types/Database/BigIntColumnTransformer.ts +74 -0
  161. package/Types/Database/ColumnLength.ts +9 -1
  162. package/Types/Database/DatabaseProperty.ts +26 -0
  163. package/Types/Database/NumericColumnValue.ts +128 -0
  164. package/Types/Database/TableColumn.ts +11 -0
  165. package/Types/Database/UnsynchronizedIndex.ts +48 -0
  166. package/Types/Decimal.ts +16 -4
  167. package/Types/HashedString.ts +139 -4
  168. package/Types/Permission.ts +48 -0
  169. package/Types/Port.ts +16 -4
  170. package/UI/Components/CustomFields/CustomFieldsDetail.tsx +85 -11
  171. package/UI/Components/LogsViewer/useLiveLogsRefresh.ts +86 -0
  172. package/UI/Components/LogsViewer/useLogsHistogram.ts +113 -0
  173. package/UI/Components/Navbar/NavBar.tsx +12 -2
  174. package/UI/Components/Navbar/NavBarMenuModal.tsx +19 -7
  175. package/UI/Utils/ModelAPI/UserProjectsModelAPI.ts +257 -0
  176. package/Utils/Dashboard/Components/DashboardNetworkMapComponent.ts +136 -0
  177. package/Utils/Dashboard/Components/Index.ts +7 -0
  178. package/Utils/TeamMembersByProject.ts +237 -0
  179. package/build/dist/Models/DatabaseModels/CephCluster.js +16 -1
  180. package/build/dist/Models/DatabaseModels/CephCluster.js.map +1 -1
  181. package/build/dist/Models/DatabaseModels/CodeRepository.js +14 -6
  182. package/build/dist/Models/DatabaseModels/CodeRepository.js.map +1 -1
  183. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js +12 -0
  184. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js.map +1 -1
  185. package/build/dist/Models/DatabaseModels/DockerHost.js +16 -1
  186. package/build/dist/Models/DatabaseModels/DockerHost.js.map +1 -1
  187. package/build/dist/Models/DatabaseModels/GlobalOidcProject.js +16 -1
  188. package/build/dist/Models/DatabaseModels/GlobalOidcProject.js.map +1 -1
  189. package/build/dist/Models/DatabaseModels/GlobalSsoProject.js +16 -1
  190. package/build/dist/Models/DatabaseModels/GlobalSsoProject.js.map +1 -1
  191. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  192. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  193. package/build/dist/Models/DatabaseModels/IoTFleet.js +16 -1
  194. package/build/dist/Models/DatabaseModels/IoTFleet.js.map +1 -1
  195. package/build/dist/Models/DatabaseModels/KubernetesCluster.js +16 -1
  196. package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
  197. package/build/dist/Models/DatabaseModels/LogDropFilter.js +9 -20
  198. package/build/dist/Models/DatabaseModels/LogDropFilter.js.map +1 -1
  199. package/build/dist/Models/DatabaseModels/NetworkDevice.js +16 -1
  200. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  201. package/build/dist/Models/DatabaseModels/NetworkInterface.js +16 -1
  202. package/build/dist/Models/DatabaseModels/NetworkInterface.js.map +1 -1
  203. package/build/dist/Models/DatabaseModels/Project.js +16 -2
  204. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  205. package/build/dist/Models/DatabaseModels/ProxmoxCluster.js +16 -1
  206. package/build/dist/Models/DatabaseModels/ProxmoxCluster.js.map +1 -1
  207. package/build/dist/Models/DatabaseModels/Service.js +28 -1
  208. package/build/dist/Models/DatabaseModels/Service.js.map +1 -1
  209. package/build/dist/Models/DatabaseModels/StatusPageMonitorRule.js +925 -0
  210. package/build/dist/Models/DatabaseModels/StatusPageMonitorRule.js.map +1 -0
  211. package/build/dist/Models/DatabaseModels/StatusPagePrivateUser.js +32 -0
  212. package/build/dist/Models/DatabaseModels/StatusPagePrivateUser.js.map +1 -1
  213. package/build/dist/Models/DatabaseModels/StatusPageResource.js +72 -0
  214. package/build/dist/Models/DatabaseModels/StatusPageResource.js.map +1 -1
  215. package/build/dist/Models/DatabaseModels/TraceDropFilter.js +9 -20
  216. package/build/dist/Models/DatabaseModels/TraceDropFilter.js.map +1 -1
  217. package/build/dist/Models/DatabaseModels/User.js +32 -0
  218. package/build/dist/Models/DatabaseModels/User.js.map +1 -1
  219. package/build/dist/Server/API/AIAgentDataAPI.js +21 -2
  220. package/build/dist/Server/API/AIAgentDataAPI.js.map +1 -1
  221. package/build/dist/Server/API/BaseAPI.js +11 -1
  222. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  223. package/build/dist/Server/API/DashboardAPI.js +343 -10
  224. package/build/dist/Server/API/DashboardAPI.js.map +1 -1
  225. package/build/dist/Server/API/GitHubAPI.js +94 -167
  226. package/build/dist/Server/API/GitHubAPI.js.map +1 -1
  227. package/build/dist/Server/API/UserAPI.js +187 -2
  228. package/build/dist/Server/API/UserAPI.js.map +1 -1
  229. package/build/dist/Server/EnvironmentConfig.js +34 -4
  230. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  231. package/build/dist/Server/Infrastructure/GlobalCache.js +120 -0
  232. package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
  233. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js +5 -3
  234. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js.map +1 -1
  235. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.js +48 -0
  236. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.js.map +1 -0
  237. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786018109307-AddPerUserPasswordSalt.js +14 -0
  238. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786018109307-AddPerUserPasswordSalt.js.map +1 -0
  239. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786023262402-WidenHashedStringColumnsForScrypt.js +59 -0
  240. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786023262402-WidenHashedStringColumnsForScrypt.js.map +1 -0
  241. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786100000000-RestoreServiceLowerNameIndex.js +93 -0
  242. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786100000000-RestoreServiceLowerNameIndex.js.map +1 -0
  243. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786200000000-RestoreDroppedUniqueIndexes.js +101 -0
  244. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786200000000-RestoreDroppedUniqueIndexes.js.map +1 -0
  245. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786300000000-QuarantineUnboundGitHubInstallations.js +66 -0
  246. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786300000000-QuarantineUnboundGitHubInstallations.js.map +1 -0
  247. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +12 -0
  248. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  249. package/build/dist/Server/Services/AlertStateTimelineService.js +19 -0
  250. package/build/dist/Server/Services/AlertStateTimelineService.js.map +1 -1
  251. package/build/dist/Server/Services/AnalyticsDatabaseService.js +57 -1
  252. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  253. package/build/dist/Server/Services/CephClusterService.js +26 -76
  254. package/build/dist/Server/Services/CephClusterService.js.map +1 -1
  255. package/build/dist/Server/Services/CloudResourceService.js +23 -73
  256. package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
  257. package/build/dist/Server/Services/CodeRepositoryService.js +90 -2
  258. package/build/dist/Server/Services/CodeRepositoryService.js.map +1 -1
  259. package/build/dist/Server/Services/DatabaseService.js +246 -2
  260. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  261. package/build/dist/Server/Services/DockerHostService.js +19 -69
  262. package/build/dist/Server/Services/DockerHostService.js.map +1 -1
  263. package/build/dist/Server/Services/DockerSwarmClusterService.js +27 -78
  264. package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
  265. package/build/dist/Server/Services/HostService.js +58 -102
  266. package/build/dist/Server/Services/HostService.js.map +1 -1
  267. package/build/dist/Server/Services/IncidentStateTimelineService.js +19 -0
  268. package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
  269. package/build/dist/Server/Services/Index.js +2 -0
  270. package/build/dist/Server/Services/Index.js.map +1 -1
  271. package/build/dist/Server/Services/IoTFleetService.js +19 -69
  272. package/build/dist/Server/Services/IoTFleetService.js.map +1 -1
  273. package/build/dist/Server/Services/KubernetesClusterService.js +17 -68
  274. package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
  275. package/build/dist/Server/Services/LabelService.js +42 -7
  276. package/build/dist/Server/Services/LabelService.js.map +1 -1
  277. package/build/dist/Server/Services/MetricTypeService.js +74 -0
  278. package/build/dist/Server/Services/MetricTypeService.js.map +1 -1
  279. package/build/dist/Server/Services/MonitorService.js +81 -9
  280. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  281. package/build/dist/Server/Services/MonitorStatusTimelineService.js +28 -0
  282. package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
  283. package/build/dist/Server/Services/MonitorTemplateService.js +60 -0
  284. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  285. package/build/dist/Server/Services/OpenTelemetryIngestService.js +155 -25
  286. package/build/dist/Server/Services/OpenTelemetryIngestService.js.map +1 -1
  287. package/build/dist/Server/Services/PodmanHostService.js +19 -69
  288. package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
  289. package/build/dist/Server/Services/ProxmoxClusterService.js +23 -73
  290. package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
  291. package/build/dist/Server/Services/RumApplicationService.js +19 -70
  292. package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
  293. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js +20 -0
  294. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js.map +1 -1
  295. package/build/dist/Server/Services/ServerlessFunctionService.js +24 -75
  296. package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
  297. package/build/dist/Server/Services/ServiceService.js +236 -61
  298. package/build/dist/Server/Services/ServiceService.js.map +1 -1
  299. package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js +603 -0
  300. package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js.map +1 -0
  301. package/build/dist/Server/Services/StatusPageMonitorRuleService.js +401 -0
  302. package/build/dist/Server/Services/StatusPageMonitorRuleService.js.map +1 -0
  303. package/build/dist/Server/Services/TelemetryEntityRelationshipService.js +1 -0
  304. package/build/dist/Server/Services/TelemetryEntityRelationshipService.js.map +1 -1
  305. package/build/dist/Server/Services/TelemetryEntityService.js +1 -0
  306. package/build/dist/Server/Services/TelemetryEntityService.js.map +1 -1
  307. package/build/dist/Server/Services/UserService.js +165 -0
  308. package/build/dist/Server/Services/UserService.js.map +1 -1
  309. package/build/dist/Server/Utils/AI/Toolbox/CodeTools.js +31 -1
  310. package/build/dist/Server/Utils/AI/Toolbox/CodeTools.js.map +1 -1
  311. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +13 -0
  312. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  313. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js +126 -4
  314. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js.map +1 -1
  315. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubInstallationBinding.js +136 -0
  316. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubInstallationBinding.js.map +1 -0
  317. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js +105 -29
  318. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js.map +1 -1
  319. package/build/dist/Server/Utils/Monitor/MonitorResource.js +56 -32
  320. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  321. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js +30 -3
  322. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js.map +1 -1
  323. package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js +110 -98
  324. package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js.map +1 -1
  325. package/build/dist/Server/Utils/Monitor/MonitorStepsReferenceExtractor.js +312 -0
  326. package/build/dist/Server/Utils/Monitor/MonitorStepsReferenceExtractor.js.map +1 -0
  327. package/build/dist/Server/Utils/PasswordHash.js +237 -0
  328. package/build/dist/Server/Utils/PasswordHash.js.map +1 -0
  329. package/build/dist/Server/Utils/SingleFlight.js +76 -0
  330. package/build/dist/Server/Utils/SingleFlight.js.map +1 -0
  331. package/build/dist/Server/Utils/StatusPage/MonitorRulePatternValidator.js +29 -0
  332. package/build/dist/Server/Utils/StatusPage/MonitorRulePatternValidator.js.map +1 -0
  333. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +78 -19
  334. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  335. package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js +160 -0
  336. package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js.map +1 -0
  337. package/build/dist/Server/Utils/Telemetry/Telemetry.js +279 -104
  338. package/build/dist/Server/Utils/Telemetry/Telemetry.js.map +1 -1
  339. package/build/dist/Types/Dashboard/DashboardComponentType.js +1 -0
  340. package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
  341. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
  342. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
  343. package/build/dist/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.js +2 -0
  344. package/build/dist/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.js.map +1 -0
  345. package/build/dist/Types/Dashboard/DashboardTemplates.js +368 -0
  346. package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
  347. package/build/dist/Types/Database/BigIntColumnTransformer.js +66 -0
  348. package/build/dist/Types/Database/BigIntColumnTransformer.js.map +1 -0
  349. package/build/dist/Types/Database/ColumnLength.js +9 -1
  350. package/build/dist/Types/Database/ColumnLength.js.map +1 -1
  351. package/build/dist/Types/Database/DatabaseProperty.js +25 -0
  352. package/build/dist/Types/Database/DatabaseProperty.js.map +1 -1
  353. package/build/dist/Types/Database/NumericColumnValue.js +97 -0
  354. package/build/dist/Types/Database/NumericColumnValue.js.map +1 -0
  355. package/build/dist/Types/Database/TableColumn.js.map +1 -1
  356. package/build/dist/Types/Database/UnsynchronizedIndex.js +46 -0
  357. package/build/dist/Types/Database/UnsynchronizedIndex.js.map +1 -0
  358. package/build/dist/Types/Decimal.js +14 -3
  359. package/build/dist/Types/Decimal.js.map +1 -1
  360. package/build/dist/Types/HashedString.js +95 -5
  361. package/build/dist/Types/HashedString.js.map +1 -1
  362. package/build/dist/Types/Permission.js +42 -0
  363. package/build/dist/Types/Permission.js.map +1 -1
  364. package/build/dist/Types/Port.js +15 -5
  365. package/build/dist/Types/Port.js.map +1 -1
  366. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +51 -10
  367. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
  368. package/build/dist/UI/Components/LogsViewer/useLiveLogsRefresh.js +44 -0
  369. package/build/dist/UI/Components/LogsViewer/useLiveLogsRefresh.js.map +1 -0
  370. package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js +75 -0
  371. package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js.map +1 -0
  372. package/build/dist/UI/Components/Navbar/NavBar.js +3 -1
  373. package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
  374. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +19 -7
  375. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
  376. package/build/dist/UI/Utils/ModelAPI/UserProjectsModelAPI.js +156 -0
  377. package/build/dist/UI/Utils/ModelAPI/UserProjectsModelAPI.js.map +1 -0
  378. package/build/dist/Utils/Dashboard/Components/DashboardNetworkMapComponent.js +114 -0
  379. package/build/dist/Utils/Dashboard/Components/DashboardNetworkMapComponent.js.map +1 -0
  380. package/build/dist/Utils/Dashboard/Components/Index.js +4 -0
  381. package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
  382. package/build/dist/Utils/TeamMembersByProject.js +145 -0
  383. package/build/dist/Utils/TeamMembersByProject.js.map +1 -0
  384. package/package.json +1 -1
@@ -0,0 +1,870 @@
1
+ import UserService from "../../../Server/Services/UserService";
2
+ import StatusPagePrivateUserService from "../../../Server/Services/StatusPagePrivateUserService";
3
+ import DashboardService from "../../../Server/Services/DashboardService";
4
+ import ColumnPermissions from "../../../Server/Types/Database/Permissions/ColumnPermission";
5
+ import DatabaseRequestType from "../../../Server/Types/BaseDatabase/DatabaseRequestType";
6
+ import PasswordHash from "../../../Server/Utils/PasswordHash";
7
+ import { EncryptionSecret } from "../../../Server/EnvironmentConfig";
8
+ import User from "../../../Models/DatabaseModels/User";
9
+ import StatusPagePrivateUser from "../../../Models/DatabaseModels/StatusPagePrivateUser";
10
+ import Dashboard from "../../../Models/DatabaseModels/Dashboard";
11
+ import DatabaseCommonInteractionProps from "../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
12
+ import { TableColumnMetadata } from "../../../Types/Database/TableColumn";
13
+ import TableColumnType from "../../../Types/Database/TableColumnType";
14
+ import ColumnLength from "../../../Types/Database/ColumnLength";
15
+ import Email from "../../../Types/Email";
16
+ import HashedString from "../../../Types/HashedString";
17
+ import ObjectID from "../../../Types/ObjectID";
18
+ import { JSONObject } from "../../../Types/JSON";
19
+ import CryptoJS from "crypto-js";
20
+ import crypto from "crypto";
21
+ import { afterEach, describe, expect, jest, test } from "@jest/globals";
22
+
23
+ /*
24
+ * Per-user password salts, at the layer that actually writes them.
25
+ *
26
+ * The salt is not something callers pass in — it is minted by the write path
27
+ * itself, onto the same payload as the hash it was computed from, so the two
28
+ * can never be persisted out of step. These tests exercise that from both
29
+ * write paths (create goes through `hash()`, update through
30
+ * `sanitizeCreateOrUpdate`) and pin the pieces that are easy to break
31
+ * without noticing:
32
+ *
33
+ * - a salt column that forgets `computed: true` breaks every non-root
34
+ * create of a row carrying a password, because the create permission
35
+ * check runs AFTER hashing and would reject a column nobody may write;
36
+ * - a hashed column with no declared salt column (session refresh tokens,
37
+ * master passwords) must keep hashing exactly as it always did;
38
+ * - an update that does not touch the password must not touch the salt,
39
+ * or every password in the affected rows stops verifying.
40
+ *
41
+ * No Postgres anywhere: the write pipeline's own hashing steps are called
42
+ * directly, and the one method that would write is stubbed.
43
+ */
44
+
45
+ type SanitizeFunction = (
46
+ data: unknown,
47
+ props: DatabaseCommonInteractionProps,
48
+ isUpdate?: boolean,
49
+ ) => Promise<JSONObject>;
50
+
51
+ const sanitize: SanitizeFunction = (
52
+ data: unknown,
53
+ props: DatabaseCommonInteractionProps,
54
+ isUpdate: boolean = true,
55
+ ): Promise<JSONObject> => {
56
+ return (
57
+ UserService as unknown as {
58
+ sanitizeCreateOrUpdate: SanitizeFunction;
59
+ }
60
+ ).sanitizeCreateOrUpdate(data, props, isUpdate) as Promise<JSONObject>;
61
+ };
62
+
63
+ type HashFunction<T> = (data: T) => Promise<T>;
64
+
65
+ const hashUser: HashFunction<User> = (data: User): Promise<User> => {
66
+ return (UserService as unknown as { hash: HashFunction<User> }).hash(data);
67
+ };
68
+
69
+ afterEach(() => {
70
+ jest.restoreAllMocks();
71
+ });
72
+
73
+ describe("Model metadata — the password columns declare their salt", () => {
74
+ test("User.password points at User.passwordSalt", () => {
75
+ const metadata: TableColumnMetadata = new User().getTableColumnMetadata(
76
+ "password",
77
+ );
78
+
79
+ expect(metadata.hashed).toBe(true);
80
+ expect(metadata.hashSaltColumn).toBe("passwordSalt");
81
+ });
82
+
83
+ test("StatusPagePrivateUser.password points at StatusPagePrivateUser.passwordSalt", () => {
84
+ const metadata: TableColumnMetadata =
85
+ new StatusPagePrivateUser().getTableColumnMetadata("password");
86
+
87
+ expect(metadata.hashed).toBe(true);
88
+ expect(metadata.hashSaltColumn).toBe("passwordSalt");
89
+ });
90
+
91
+ test("the salt columns actually exist on both models", () => {
92
+ expect(new User().hasColumn("passwordSalt")).toBe(true);
93
+ expect(new StatusPagePrivateUser().hasColumn("passwordSalt")).toBe(true);
94
+ });
95
+
96
+ test("both salt columns are marked computed", () => {
97
+ /*
98
+ * Load bearing. On create, hash() mints the salt BEFORE
99
+ * checkCreatePermissions runs, and checkDataColumnPermissions only lets
100
+ * an unpermitted column through on create when it is computed. Drop the
101
+ * flag and every public signup starts failing with "User is not allowed
102
+ * to create on passwordSalt column".
103
+ */
104
+ expect(new User().getTableColumnMetadata("passwordSalt").computed).toBe(
105
+ true,
106
+ );
107
+ expect(
108
+ new StatusPagePrivateUser().getTableColumnMetadata("passwordSalt")
109
+ .computed,
110
+ ).toBe(true);
111
+ });
112
+
113
+ test("nobody may create, read or update the salt through the API", () => {
114
+ for (const model of [new User(), new StatusPagePrivateUser()]) {
115
+ const accessControl: {
116
+ create?: Array<unknown>;
117
+ read?: Array<unknown>;
118
+ update?: Array<unknown>;
119
+ } = model.getColumnAccessControlFor("passwordSalt") as {
120
+ create?: Array<unknown>;
121
+ read?: Array<unknown>;
122
+ update?: Array<unknown>;
123
+ };
124
+
125
+ expect(accessControl.create).toEqual([]);
126
+ expect(accessControl.read).toEqual([]);
127
+ expect(accessControl.update).toEqual([]);
128
+ }
129
+ });
130
+
131
+ test("a salt fits the column that stores it", () => {
132
+ const metadata: TableColumnMetadata = new User().getTableColumnMetadata(
133
+ "passwordSalt",
134
+ );
135
+
136
+ // ShortText — 100 characters. A generated salt is 64.
137
+ expect(metadata.type).toBe(TableColumnType.ShortText);
138
+ expect(PasswordHash.generateSalt().length).toBeLessThanOrEqual(100);
139
+ });
140
+ });
141
+
142
+ describe("Create path — hash() mints a per-record salt", () => {
143
+ test("hashes the password and writes the salt it used onto the same model", async () => {
144
+ const user: User = new User();
145
+ user.email = new Email("salted@example.com");
146
+ user.password = new HashedString("my-password");
147
+
148
+ await hashUser(user);
149
+
150
+ expect(user.passwordSalt).toMatch(/^[0-9a-f]{64}$/);
151
+ expect(user.password!.isValueHashed()).toBe(true);
152
+ expect(user.password!.toString()).not.toBe("my-password");
153
+
154
+ await expect(
155
+ PasswordHash.verify({
156
+ plainValue: "my-password",
157
+ storedValue: user.password!.toString(),
158
+ salt: user.passwordSalt || null,
159
+ }),
160
+ ).resolves.toBe(true);
161
+ });
162
+
163
+ test("two users signing up with the SAME password get different salts and different hashes", async () => {
164
+ const first: User = new User();
165
+ first.email = new Email("one@example.com");
166
+ first.password = new HashedString("shared-password");
167
+
168
+ const second: User = new User();
169
+ second.email = new Email("two@example.com");
170
+ second.password = new HashedString("shared-password");
171
+
172
+ await hashUser(first);
173
+ await hashUser(second);
174
+
175
+ expect(first.passwordSalt).not.toBe(second.passwordSalt);
176
+ expect(first.password!.toString()).not.toBe(second.password!.toString());
177
+ });
178
+
179
+ test("the write path produces a scrypt hash, not a SHA-256 digest", async () => {
180
+ /*
181
+ * A password column is a credential, and credentials go through the slow
182
+ * KDF. If this ever reverts to a bare digest, a leaked table becomes
183
+ * crackable at billions of guesses per second again.
184
+ */
185
+ const user: User = new User();
186
+ user.password = new HashedString("my-password");
187
+
188
+ await hashUser(user);
189
+
190
+ expect(user.password!.toString()).toMatch(
191
+ /^scrypt\$N=\d+,r=\d+,p=\d+\$[0-9a-f]{64}$/,
192
+ );
193
+ expect(PasswordHash.needsUpgrade(user.password!.toString())).toBe(false);
194
+
195
+ // And specifically not either SHA-256 scheme it replaced.
196
+ expect(user.password!.toString()).not.toBe(
197
+ CryptoJS.SHA256(EncryptionSecret.toString() + "my-password").toString(),
198
+ );
199
+ expect(user.password!.toString()).not.toBe(
200
+ HashedString.computeHash(
201
+ "my-password",
202
+ EncryptionSecret,
203
+ user.passwordSalt,
204
+ ),
205
+ );
206
+ });
207
+
208
+ test("the hash it writes fits the column that stores it", async () => {
209
+ const user: User = new User();
210
+ user.password = new HashedString("my-password");
211
+
212
+ await hashUser(user);
213
+
214
+ expect(user.password!.toString().length).toBeLessThanOrEqual(
215
+ ColumnLength.HashedString,
216
+ );
217
+ expect((user.passwordSalt as string).length).toBeLessThanOrEqual(
218
+ ColumnLength.ShortText,
219
+ );
220
+ });
221
+
222
+ test("an already-hashed value is left alone and gets no new salt", async () => {
223
+ const user: User = new User();
224
+ user.password = new HashedString("already-a-digest", true);
225
+
226
+ await hashUser(user);
227
+
228
+ expect(user.password!.toString()).toBe("already-a-digest");
229
+ expect(user.passwordSalt).toBeUndefined();
230
+ });
231
+
232
+ test("a user created without a password gets no salt", async () => {
233
+ const user: User = new User();
234
+ user.email = new Email("invited@example.com");
235
+
236
+ await hashUser(user);
237
+
238
+ expect(user.passwordSalt).toBeUndefined();
239
+ });
240
+
241
+ test("the minted salt survives the create permission check for a public signup", () => {
242
+ /*
243
+ * This is the ordering trap. Signup is not a root write, and
244
+ * checkCreatePermissions runs after hash() has already put passwordSalt
245
+ * on the payload.
246
+ */
247
+ const user: User = new User();
248
+ user.email = new Email("signup@example.com");
249
+ user.password = new HashedString("digest", true);
250
+ user.passwordSalt = PasswordHash.generateSalt();
251
+
252
+ expect(() => {
253
+ ColumnPermissions.checkDataColumnPermissions(
254
+ User,
255
+ user,
256
+ {} as DatabaseCommonInteractionProps,
257
+ DatabaseRequestType.Create,
258
+ );
259
+ }).not.toThrow();
260
+ });
261
+
262
+ test("a caller still cannot smuggle a salt in on an update", () => {
263
+ /*
264
+ * The mirror of the test above, and the reason the update path adds the
265
+ * salt AFTER its permission check rather than before: on update there is
266
+ * no `computed` escape hatch.
267
+ */
268
+ const user: User = new User();
269
+ user.passwordSalt = PasswordHash.generateSalt();
270
+
271
+ expect(() => {
272
+ ColumnPermissions.checkDataColumnPermissions(
273
+ User,
274
+ user,
275
+ {} as DatabaseCommonInteractionProps,
276
+ DatabaseRequestType.Update,
277
+ );
278
+ }).toThrow("not allowed to update on passwordSalt");
279
+ });
280
+ });
281
+
282
+ describe("Update path — sanitizeCreateOrUpdate mints a per-record salt", () => {
283
+ test("hashes the new password and adds the salt to the same update payload", async () => {
284
+ const data: JSONObject = await sanitize(
285
+ { password: new HashedString("new-password") },
286
+ { isRoot: true },
287
+ );
288
+
289
+ expect(typeof data["password"]).toBe("string");
290
+ expect(data["passwordSalt"]).toMatch(/^[0-9a-f]{64}$/);
291
+
292
+ await expect(
293
+ PasswordHash.verify({
294
+ plainValue: "new-password",
295
+ storedValue: data["password"] as string,
296
+ salt: data["passwordSalt"] as string,
297
+ }),
298
+ ).resolves.toBe(true);
299
+ });
300
+
301
+ test("two password updates to the same value produce different salts", async () => {
302
+ const first: JSONObject = await sanitize(
303
+ { password: new HashedString("same-password") },
304
+ { isRoot: true },
305
+ );
306
+ const second: JSONObject = await sanitize(
307
+ { password: new HashedString("same-password") },
308
+ { isRoot: true },
309
+ );
310
+
311
+ expect(first["passwordSalt"]).not.toBe(second["passwordSalt"]);
312
+ expect(first["password"]).not.toBe(second["password"]);
313
+ });
314
+
315
+ test("an update that does not touch the password does not touch the salt", async () => {
316
+ /*
317
+ * If an unrelated update stamped a fresh salt, it would be written next
318
+ * to the OLD hash and lock the account out.
319
+ */
320
+ const data: JSONObject = await sanitize(
321
+ { name: "Renamed User" },
322
+ { isRoot: true },
323
+ );
324
+
325
+ expect(data["passwordSalt"]).toBeUndefined();
326
+ expect(Object.keys(data)).not.toContain("passwordSalt");
327
+ });
328
+
329
+ test("refuses a pre-hashed bare string, which would desync hash and salt", async () => {
330
+ await expect(
331
+ sanitize({ password: "a-raw-digest-string" }, { isRoot: true }),
332
+ ).rejects.toThrow("must be supplied as a HashedString");
333
+ });
334
+
335
+ test("status page private users are salted the same way", async () => {
336
+ const data: JSONObject = (await (
337
+ StatusPagePrivateUserService as unknown as {
338
+ sanitizeCreateOrUpdate: SanitizeFunction;
339
+ }
340
+ ).sanitizeCreateOrUpdate(
341
+ { password: new HashedString("sp-password") },
342
+ { isRoot: true },
343
+ true,
344
+ )) as JSONObject;
345
+
346
+ expect(data["passwordSalt"]).toMatch(/^[0-9a-f]{64}$/);
347
+
348
+ await expect(
349
+ PasswordHash.verify({
350
+ plainValue: "sp-password",
351
+ storedValue: data["password"] as string,
352
+ salt: data["passwordSalt"] as string,
353
+ }),
354
+ ).resolves.toBe(true);
355
+ });
356
+ });
357
+
358
+ describe("Hashed columns that declare no salt column are untouched", () => {
359
+ test("a dashboard master password still uses the legacy unsalted digest", async () => {
360
+ /*
361
+ * Dashboard.masterPassword is `hashed: true` with no hashSaltColumn — a
362
+ * shared secret, not a per-user credential. Every master password
363
+ * already stored has to keep verifying, so this digest must not move.
364
+ */
365
+ const dashboard: Dashboard = new Dashboard();
366
+ dashboard.masterPassword = new HashedString("master-password");
367
+
368
+ await (
369
+ DashboardService as unknown as { hash: HashFunction<Dashboard> }
370
+ ).hash(dashboard);
371
+
372
+ expect(dashboard.masterPassword!.toString()).toBe(
373
+ CryptoJS.SHA256(
374
+ EncryptionSecret.toString() + "master-password",
375
+ ).toString(),
376
+ );
377
+ });
378
+
379
+ test("no stray salt column is invented for it", async () => {
380
+ const dashboard: Dashboard = new Dashboard();
381
+ dashboard.masterPassword = new HashedString("master-password");
382
+
383
+ await (
384
+ DashboardService as unknown as { hash: HashFunction<Dashboard> }
385
+ ).hash(dashboard);
386
+
387
+ expect(
388
+ new Dashboard().getTableColumnMetadata("masterPassword").hashSaltColumn,
389
+ ).toBeUndefined();
390
+ expect(
391
+ (dashboard as unknown as JSONObject)["passwordSalt"],
392
+ ).toBeUndefined();
393
+ });
394
+
395
+ test("a bare string is still allowed on an unsalted hashed column", async () => {
396
+ // The desync guard is scoped to salted columns only.
397
+ const data: JSONObject = (await (
398
+ DashboardService as unknown as {
399
+ sanitizeCreateOrUpdate: SanitizeFunction;
400
+ }
401
+ ).sanitizeCreateOrUpdate(
402
+ { masterPassword: "a-raw-digest" },
403
+ { isRoot: true },
404
+ true,
405
+ )) as JSONObject;
406
+
407
+ expect(data["masterPassword"]).toBe("a-raw-digest");
408
+ });
409
+ });
410
+
411
+ /*
412
+ * Records every write the upgrade path would make, without making one.
413
+ * `captured` is reset by each call.
414
+ */
415
+ type UpdateColumnsInput = {
416
+ id: ObjectID;
417
+ data: JSONObject;
418
+ skipUpdateDateColumn?: boolean;
419
+ };
420
+
421
+ let captured: Array<UpdateColumnsInput> = [];
422
+
423
+ type SpyOnUpgradeWriteFunction = (options?: { fail?: boolean }) => void;
424
+
425
+ const spyOnUpgradeWrite: SpyOnUpgradeWriteFunction = (options?: {
426
+ fail?: boolean;
427
+ }): void => {
428
+ captured = [];
429
+
430
+ jest
431
+ .spyOn(
432
+ UserService as unknown as {
433
+ updateColumnsByIdWithoutHooks: (
434
+ input: UpdateColumnsInput,
435
+ ) => Promise<void>;
436
+ },
437
+ "updateColumnsByIdWithoutHooks",
438
+ )
439
+ .mockImplementation(((input: UpdateColumnsInput): Promise<void> => {
440
+ captured.push(input);
441
+
442
+ if (options?.fail) {
443
+ return Promise.reject(new Error("database is on fire"));
444
+ }
445
+
446
+ return Promise.resolve();
447
+ }) as never);
448
+ };
449
+
450
+ describe("verifyHashedColumnValue", () => {
451
+ type BuildUserFunction = (data: {
452
+ password: string;
453
+ salt: string;
454
+ }) => Promise<User>;
455
+
456
+ const buildUser: BuildUserFunction = async (data: {
457
+ password: string;
458
+ salt: string;
459
+ }): Promise<User> => {
460
+ const user: User = new User();
461
+ user._id = ObjectID.generate().toString();
462
+ user.password = new HashedString(
463
+ await PasswordHash.hash({ plainValue: data.password, salt: data.salt }),
464
+ true,
465
+ );
466
+
467
+ if (data.salt) {
468
+ user.passwordSalt = data.salt;
469
+ }
470
+
471
+ return user;
472
+ };
473
+
474
+ test("accepts the correct password for a salted user", async () => {
475
+ const salt: string = PasswordHash.generateSalt();
476
+ const user: User = await buildUser({ password: "correct", salt });
477
+ spyOnUpgradeWrite();
478
+
479
+ await expect(
480
+ UserService.verifyHashedColumnValue({
481
+ item: user,
482
+ columnName: "password",
483
+ plainValue: "correct",
484
+ }),
485
+ ).resolves.toBe(true);
486
+
487
+ // Already salted — nothing to upgrade.
488
+ expect(captured).toHaveLength(0);
489
+ });
490
+
491
+ test("rejects the wrong password for a salted user", async () => {
492
+ const salt: string = PasswordHash.generateSalt();
493
+ const user: User = await buildUser({ password: "correct", salt });
494
+ spyOnUpgradeWrite();
495
+
496
+ await expect(
497
+ UserService.verifyHashedColumnValue({
498
+ item: user,
499
+ columnName: "password",
500
+ plainValue: "wrong",
501
+ }),
502
+ ).resolves.toBe(false);
503
+
504
+ expect(captured).toHaveLength(0);
505
+ });
506
+
507
+ test("rejects one user's password against another user's salt", async () => {
508
+ const user: User = await buildUser({
509
+ password: "correct",
510
+ salt: PasswordHash.generateSalt(),
511
+ });
512
+ spyOnUpgradeWrite();
513
+
514
+ // Same password, someone else's salt.
515
+ user.passwordSalt = PasswordHash.generateSalt();
516
+
517
+ await expect(
518
+ UserService.verifyHashedColumnValue({
519
+ item: user,
520
+ columnName: "password",
521
+ plainValue: "correct",
522
+ }),
523
+ ).resolves.toBe(false);
524
+ });
525
+
526
+ test("rejects a row with no stored password at all", async () => {
527
+ const user: User = new User();
528
+ user._id = ObjectID.generate().toString();
529
+
530
+ await expect(
531
+ UserService.verifyHashedColumnValue({
532
+ item: user,
533
+ columnName: "password",
534
+ plainValue: "anything",
535
+ }),
536
+ ).resolves.toBe(false);
537
+ });
538
+
539
+ test("rejects an empty submitted password", async () => {
540
+ const user: User = await buildUser({
541
+ password: "correct",
542
+ salt: PasswordHash.generateSalt(),
543
+ });
544
+
545
+ await expect(
546
+ UserService.verifyHashedColumnValue({
547
+ item: user,
548
+ columnName: "password",
549
+ plainValue: "",
550
+ }),
551
+ ).resolves.toBe(false);
552
+ });
553
+ });
554
+
555
+ describe("verifyHashedColumnValue — transparent upgrade of legacy hashes", () => {
556
+ type LegacyUserFunction = (password: string) => Promise<User>;
557
+
558
+ const legacyUser: LegacyUserFunction = async (
559
+ password: string,
560
+ ): Promise<User> => {
561
+ const user: User = new User();
562
+ user._id = ObjectID.generate().toString();
563
+ // Hashed the way it was before salts existed: no salt column value.
564
+ user.password = new HashedString(
565
+ await HashedString.hashValue(password, EncryptionSecret),
566
+ true,
567
+ );
568
+ return user;
569
+ };
570
+
571
+ test("a password stored before salts existed still logs in", async () => {
572
+ spyOnUpgradeWrite();
573
+ const user: User = await legacyUser("old-password");
574
+
575
+ await expect(
576
+ UserService.verifyHashedColumnValue({
577
+ item: user,
578
+ columnName: "password",
579
+ plainValue: "old-password",
580
+ }),
581
+ ).resolves.toBe(true);
582
+ });
583
+
584
+ test("and is re-hashed with a fresh salt, both columns written together", async () => {
585
+ spyOnUpgradeWrite();
586
+ const user: User = await legacyUser("old-password");
587
+
588
+ await UserService.verifyHashedColumnValue({
589
+ item: user,
590
+ columnName: "password",
591
+ plainValue: "old-password",
592
+ });
593
+
594
+ expect(captured).toHaveLength(1);
595
+
596
+ const write: UpdateColumnsInput = captured[0]!;
597
+
598
+ expect(write.id.toString()).toBe(user._id);
599
+ expect(write.data["passwordSalt"]).toMatch(/^[0-9a-f]{64}$/);
600
+
601
+ // The written hash verifies under the written salt.
602
+ await expect(
603
+ PasswordHash.verify({
604
+ plainValue: "old-password",
605
+ storedValue: write.data["password"] as string,
606
+ salt: write.data["passwordSalt"] as string,
607
+ }),
608
+ ).resolves.toBe(true);
609
+
610
+ // ...and is no longer the legacy digest.
611
+ expect(write.data["password"]).not.toBe(user.password!.toString());
612
+ });
613
+
614
+ test("the upgrade does not masquerade as a user-initiated change", async () => {
615
+ /*
616
+ * No hooks (so no session revocation, no "your password changed" mail)
617
+ * and no updatedAt bump. The user did not change their password.
618
+ */
619
+ spyOnUpgradeWrite();
620
+ const user: User = await legacyUser("old-password");
621
+
622
+ await UserService.verifyHashedColumnValue({
623
+ item: user,
624
+ columnName: "password",
625
+ plainValue: "old-password",
626
+ });
627
+
628
+ expect(captured[0]!.skipUpdateDateColumn).toBe(true);
629
+ });
630
+
631
+ test("a wrong password against a legacy hash upgrades nothing", async () => {
632
+ spyOnUpgradeWrite();
633
+ const user: User = await legacyUser("old-password");
634
+
635
+ await expect(
636
+ UserService.verifyHashedColumnValue({
637
+ item: user,
638
+ columnName: "password",
639
+ plainValue: "guess",
640
+ }),
641
+ ).resolves.toBe(false);
642
+
643
+ expect(captured).toHaveLength(0);
644
+ });
645
+
646
+ test("a failed upgrade write never costs the user their login", async () => {
647
+ spyOnUpgradeWrite({ fail: true });
648
+
649
+ const user: User = await legacyUser("old-password");
650
+
651
+ await expect(
652
+ UserService.verifyHashedColumnValue({
653
+ item: user,
654
+ columnName: "password",
655
+ plainValue: "old-password",
656
+ }),
657
+ ).resolves.toBe(true);
658
+ });
659
+
660
+ test("a failed upgrade leaves the in-memory row as a consistent legacy pair", async () => {
661
+ spyOnUpgradeWrite({ fail: true });
662
+
663
+ const user: User = await legacyUser("old-password");
664
+ const originalHash: string = user.password!.toString();
665
+
666
+ await UserService.verifyHashedColumnValue({
667
+ item: user,
668
+ columnName: "password",
669
+ plainValue: "old-password",
670
+ });
671
+
672
+ expect(user.passwordSalt).toBeUndefined();
673
+ expect(user.password!.toString()).toBe(originalHash);
674
+
675
+ // Re-verifying the same in-memory row still works.
676
+ await expect(
677
+ UserService.verifyHashedColumnValue({
678
+ item: user,
679
+ columnName: "password",
680
+ plainValue: "old-password",
681
+ }),
682
+ ).resolves.toBe(true);
683
+ });
684
+
685
+ test("the upgrade lands a scrypt hash, not another SHA-256 one", async () => {
686
+ spyOnUpgradeWrite();
687
+ const user: User = await legacyUser("old-password");
688
+
689
+ await UserService.verifyHashedColumnValue({
690
+ item: user,
691
+ columnName: "password",
692
+ plainValue: "old-password",
693
+ });
694
+
695
+ expect(captured[0]!.data["password"]).toMatch(/^scrypt\$/);
696
+ expect(
697
+ PasswordHash.needsUpgrade(captured[0]!.data["password"] as string),
698
+ ).toBe(false);
699
+ });
700
+
701
+ test("a row still on the interim salted-SHA scheme logs in and is upgraded", async () => {
702
+ /*
703
+ * Between per-user salts landing and scrypt landing, passwords were
704
+ * `SHA256("v2:" + salt + ":" + secret + ":" + password)`. Those rows have
705
+ * a salt, so the old "upgrade when there is no salt" rule would have
706
+ * skipped them and left them on a fast hash forever.
707
+ */
708
+ spyOnUpgradeWrite();
709
+
710
+ const salt: string = PasswordHash.generateSalt();
711
+ const user: User = new User();
712
+ user._id = ObjectID.generate().toString();
713
+ user.password = new HashedString(
714
+ await HashedString.hashValue("interim-password", EncryptionSecret, salt),
715
+ true,
716
+ );
717
+ user.passwordSalt = salt;
718
+
719
+ await expect(
720
+ UserService.verifyHashedColumnValue({
721
+ item: user,
722
+ columnName: "password",
723
+ plainValue: "interim-password",
724
+ }),
725
+ ).resolves.toBe(true);
726
+
727
+ expect(captured).toHaveLength(1);
728
+ expect(captured[0]!.data["password"]).toMatch(/^scrypt\$/);
729
+ // A brand new salt, not the interim one it came in with.
730
+ expect(captured[0]!.data["passwordSalt"]).not.toBe(salt);
731
+ });
732
+
733
+ test("a scrypt hash at stale cost parameters logs in and is re-hashed at the current cost", async () => {
734
+ /*
735
+ * This is what makes raising the cost a one-line change: bump the
736
+ * parameters and every user migrates at their next login, with no reset
737
+ * and no migration. The hash below is a genuine scrypt hash at a cheaper
738
+ * N, built from Node primitives so it is a real artefact of the old cost
739
+ * rather than a forged prefix.
740
+ */
741
+ spyOnUpgradeWrite();
742
+
743
+ const salt: string = PasswordHash.generateSalt();
744
+
745
+ const cheapDigest: string = crypto
746
+ .scryptSync(
747
+ crypto
748
+ .createHmac("sha256", EncryptionSecret.toString())
749
+ .update("password", "utf8")
750
+ .digest(),
751
+ salt,
752
+ 32,
753
+ { N: 1024, r: 8, p: 1, maxmem: 128 * 1024 * 8 * 2 },
754
+ )
755
+ .toString("hex");
756
+
757
+ const cheap: string = `scrypt$N=1024,r=8,p=1$${cheapDigest}`;
758
+
759
+ expect(PasswordHash.needsUpgrade(cheap)).toBe(true);
760
+
761
+ const user: User = new User();
762
+ user._id = ObjectID.generate().toString();
763
+ user.password = new HashedString(cheap, true);
764
+ user.passwordSalt = salt;
765
+
766
+ // The old hash still verifies, at the parameters recorded in it.
767
+ await expect(
768
+ UserService.verifyHashedColumnValue({
769
+ item: user,
770
+ columnName: "password",
771
+ plainValue: "password",
772
+ }),
773
+ ).resolves.toBe(true);
774
+
775
+ expect(captured).toHaveLength(1);
776
+
777
+ const rewritten: string = captured[0]!.data["password"] as string;
778
+
779
+ // ...and has been rewritten at today's cost, under a brand new salt.
780
+ expect(rewritten.startsWith(PasswordHash.getCurrentPrefix())).toBe(true);
781
+ expect(PasswordHash.needsUpgrade(rewritten)).toBe(false);
782
+ expect(captured[0]!.data["passwordSalt"]).not.toBe(salt);
783
+
784
+ await expect(
785
+ PasswordHash.verify({
786
+ plainValue: "password",
787
+ storedValue: rewritten,
788
+ salt: captured[0]!.data["passwordSalt"] as string,
789
+ }),
790
+ ).resolves.toBe(true);
791
+ });
792
+
793
+ test("a wrong password against a stale-cost hash is still rejected", async () => {
794
+ spyOnUpgradeWrite();
795
+
796
+ const salt: string = PasswordHash.generateSalt();
797
+ const cheapDigest: string = crypto
798
+ .scryptSync(
799
+ crypto
800
+ .createHmac("sha256", EncryptionSecret.toString())
801
+ .update("password", "utf8")
802
+ .digest(),
803
+ salt,
804
+ 32,
805
+ { N: 1024, r: 8, p: 1, maxmem: 128 * 1024 * 8 * 2 },
806
+ )
807
+ .toString("hex");
808
+
809
+ const user: User = new User();
810
+ user._id = ObjectID.generate().toString();
811
+ user.password = new HashedString(
812
+ `scrypt$N=1024,r=8,p=1$${cheapDigest}`,
813
+ true,
814
+ );
815
+ user.passwordSalt = salt;
816
+
817
+ await expect(
818
+ UserService.verifyHashedColumnValue({
819
+ item: user,
820
+ columnName: "password",
821
+ plainValue: "guess",
822
+ }),
823
+ ).resolves.toBe(false);
824
+
825
+ expect(captured).toHaveLength(0);
826
+ });
827
+
828
+ test("a hash written at today's parameters is not re-hashed", async () => {
829
+ spyOnUpgradeWrite();
830
+
831
+ const salt: string = PasswordHash.generateSalt();
832
+ const user: User = new User();
833
+ user._id = ObjectID.generate().toString();
834
+ user.password = new HashedString(
835
+ await PasswordHash.hash({ plainValue: "current", salt: salt }),
836
+ true,
837
+ );
838
+ user.passwordSalt = salt;
839
+
840
+ await expect(
841
+ UserService.verifyHashedColumnValue({
842
+ item: user,
843
+ columnName: "password",
844
+ plainValue: "current",
845
+ }),
846
+ ).resolves.toBe(true);
847
+
848
+ expect(captured).toHaveLength(0);
849
+ });
850
+
851
+ test("a row with no id is verified but not upgraded", async () => {
852
+ spyOnUpgradeWrite();
853
+
854
+ const user: User = new User();
855
+ user.password = new HashedString(
856
+ await HashedString.hashValue("old-password", EncryptionSecret),
857
+ true,
858
+ );
859
+
860
+ await expect(
861
+ UserService.verifyHashedColumnValue({
862
+ item: user,
863
+ columnName: "password",
864
+ plainValue: "old-password",
865
+ }),
866
+ ).resolves.toBe(true);
867
+
868
+ expect(captured).toHaveLength(0);
869
+ });
870
+ });