@oneuptime/common 13.0.8 → 14.0.1

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 (514) hide show
  1. package/Models/AnalyticsModels/AuditLog.ts +23 -15
  2. package/Models/DatabaseModels/GlobalConfig.ts +24 -0
  3. package/Models/DatabaseModels/Index.ts +4 -0
  4. package/Models/DatabaseModels/Monitor.ts +3 -2
  5. package/Models/DatabaseModels/MonitorTemplate.ts +2 -1
  6. package/Models/DatabaseModels/ServiceLevelObjectiveFeed.ts +2 -0
  7. package/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.ts +522 -0
  8. package/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.ts +38 -4
  9. package/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.ts +603 -0
  10. package/Scripts/generate-service-worker.js +65 -4
  11. package/Server/API/BaseAPI.ts +26 -5
  12. package/Server/API/GlobalConfigAPI.ts +228 -462
  13. package/Server/API/ProjectOIDC.ts +25 -16
  14. package/Server/API/ProjectSSO.ts +24 -16
  15. package/Server/API/StatusPageAPI.ts +100 -42
  16. package/Server/API/TeamMemberAPI.ts +10 -1
  17. package/Server/Enterprise/EnterpriseEdition.ts +649 -0
  18. package/Server/Enterprise/EnterpriseFeature.ts +48 -0
  19. package/Server/Enterprise/EnterpriseLicenseSnapshot.ts +192 -0
  20. package/Server/Enterprise/EnterpriseServerModule.ts +233 -0
  21. package/Server/EnvironmentConfig.ts +147 -2
  22. package/Server/Infrastructure/Postgres/SchemaMigrations/1793900000000-AddEnterpriseEditionFirstSeenAt.ts +25 -0
  23. package/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.ts +248 -0
  24. package/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.ts +19 -0
  25. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  26. package/Server/Middleware/UserAuthorization.ts +38 -16
  27. package/Server/Services/AuditLogService.ts +84 -1079
  28. package/Server/Services/DatabaseService.ts +10 -0
  29. package/Server/Services/EnterpriseLicenseService.ts +38 -0
  30. package/Server/Services/GlobalConfigService.ts +80 -1
  31. package/Server/Services/Index.ts +4 -0
  32. package/Server/Services/MonitorProbeService.ts +116 -20
  33. package/Server/Services/MonitorService.ts +59 -6
  34. package/Server/Services/MonitorStatusTimelineService.ts +284 -0
  35. package/Server/Services/MonitorTemplateService.ts +26 -0
  36. package/Server/Services/ProjectService.ts +284 -0
  37. package/Server/Services/RumSessionPinService.ts +25 -6
  38. package/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.ts +385 -0
  39. package/Server/Services/ServiceLevelObjectiveLabelRuleService.ts +51 -0
  40. package/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.ts +92 -16
  41. package/Server/Services/ServiceLevelObjectiveMonitorRuleService.ts +36 -1
  42. package/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.ts +521 -0
  43. package/Server/Services/ServiceLevelObjectiveOwnerRuleService.ts +51 -0
  44. package/Server/Services/ServiceLevelObjectiveService.ts +28 -0
  45. package/Server/Services/StatusPagePrivateUserService.ts +7 -3
  46. package/Server/Services/StatusPageService.ts +78 -1
  47. package/Server/Services/TeamMemberService.ts +11 -0
  48. package/Server/Services/TeamService.ts +10 -0
  49. package/Server/Services/UserService.ts +10 -17
  50. package/Server/Types/Database/Permissions/CreatePermission.ts +15 -0
  51. package/Server/Types/Database/Permissions/EditionPermission.ts +208 -18
  52. package/Server/Types/Database/Permissions/TablePermission.ts +10 -3
  53. package/Server/Types/Database/Permissions/UpdatePermission.ts +29 -0
  54. package/Server/Types/Markdown.ts +41 -0
  55. package/Server/Types/Workflow/Components/Telegram/SendMessageToChat.ts +27 -1
  56. package/Server/Utils/AI/Toolbox/SloTools.ts +1 -0
  57. package/Server/Utils/CommunityEditionSsoReport.ts +361 -0
  58. package/Server/Utils/EditionEnforcement.ts +233 -0
  59. package/Server/Utils/FrontendEnvironment.ts +41 -2
  60. package/Server/Utils/InstanceHealth/RedisHealth.ts +12 -277
  61. package/Server/Utils/Monitor/AffectedResourceList.ts +148 -0
  62. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +1 -1
  63. package/Server/Utils/Monitor/Criteria/EvaluateOverTime.ts +10 -1
  64. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +1 -1
  65. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +720 -465
  66. package/Server/Utils/Monitor/MonitoringIntervalValidator.ts +116 -0
  67. package/Server/Utils/Monitor/RootCauseList.ts +139 -0
  68. package/Server/Utils/Rules/RuleRun/RuleRunRegistry.ts +28 -0
  69. package/Server/Utils/Slo/SloRulePatternValidator.ts +42 -0
  70. package/Server/Utils/Workspace/Slack/Slack.ts +423 -10
  71. package/Tests/App/Accounts/LoginPasskey.test.tsx +18 -0
  72. package/Tests/App/Accounts/LoginSsoEdition.test.tsx +616 -0
  73. package/Tests/App/AdminDashboard/AdminAuthenticationSsoEdition.test.tsx +217 -0
  74. package/Tests/App/AdminDashboard/AdminEnterprisePluginPage.test.tsx +357 -0
  75. package/Tests/App/AdminDashboard/EnterpriseLicensesShell.test.tsx +214 -0
  76. package/Tests/App/AdminDashboard/GlobalSSOEnterpriseShells.test.tsx +271 -0
  77. package/Tests/App/AdminDashboard/HealthShells.test.tsx +659 -0
  78. package/Tests/App/Dashboard/AuditLogsAndComplianceShells.test.tsx +572 -0
  79. package/Tests/App/Dashboard/EnterpriseFeatureUpgrade.test.tsx +547 -0
  80. package/Tests/App/Dashboard/EnterprisePluginPage.test.tsx +667 -0
  81. package/Tests/App/Dashboard/FeedFilterSortIntegration.test.tsx +1632 -0
  82. package/Tests/App/Dashboard/FeedOptionsLocales.test.ts +250 -0
  83. package/Tests/App/Dashboard/FeedOrdering.test.ts +279 -9
  84. package/Tests/App/Dashboard/IdentityEnterpriseShells.test.tsx +319 -0
  85. package/Tests/App/Dashboard/InvestigationFeedRefresh.test.tsx +60 -1
  86. package/Tests/App/Dashboard/InvestigationPanel.test.tsx +594 -3
  87. package/Tests/App/Dashboard/OnboardingSsoEmptyState.test.tsx +185 -0
  88. package/Tests/App/Dashboard/ProductOrientation.test.tsx +646 -0
  89. package/Tests/App/Dashboard/ResourceFeed.test.tsx +612 -2
  90. package/Tests/App/Dashboard/ResourceFeedPageWiring.test.ts +30 -0
  91. package/Tests/App/Dashboard/ResourceFeedPagesFilterSort.test.tsx +1202 -0
  92. package/Tests/App/Dashboard/ScheduledMaintenanceFeedRefresh.test.tsx +52 -1
  93. package/Tests/App/Dashboard/SecurityEventConnectionDiagnostics.test.tsx +45 -9
  94. package/Tests/App/Dashboard/SecurityEventDetailPanel.test.tsx +399 -0
  95. package/Tests/App/Dashboard/SecurityEventListRow.test.tsx +214 -0
  96. package/Tests/App/Dashboard/SecurityEventsEmptyState.test.tsx +1 -83
  97. package/Tests/App/Dashboard/SecurityEventsNavTabs.test.tsx +248 -0
  98. package/Tests/App/Dashboard/SecurityEventsViewer.test.tsx +1203 -0
  99. package/Tests/App/Dashboard/SloFeed.test.tsx +106 -1
  100. package/Tests/App/Dashboard/SloMonitorRuleCriteria.test.tsx +566 -0
  101. package/Tests/App/Dashboard/SloNavigationWiring.test.tsx +6 -4
  102. package/Tests/App/Dashboard/SloNoticeBanner.test.tsx +186 -0
  103. package/Tests/App/Dashboard/SloOverviewKpiStrip.test.tsx +388 -44
  104. package/Tests/App/Dashboard/SloOverviewLayout.test.tsx +613 -0
  105. package/Tests/App/Dashboard/UserEmailCallSites.test.tsx +11 -91
  106. package/Tests/App/StatusPage/StatusPageSsoEmptyState.test.tsx +319 -0
  107. package/Tests/Helpers/RefreshAwareApiScan.ts +757 -0
  108. package/Tests/Helpers/TenantScopedRequestScan.ts +515 -0
  109. package/Tests/Models/DatabaseModels/ResourceFeedModels.test.ts +0 -8
  110. package/Tests/Models/DatabaseModels/SloLabelAndOwnerRuleModels.test.ts +404 -0
  111. package/Tests/Models/DatabaseModels/SloOverhaulModels.test.ts +17 -1
  112. package/Tests/Models/DatabaseModels/StaticRuleCriteriaInheritance.test.ts +4 -2
  113. package/Tests/Scripts/GenerateServiceWorker.test.ts +931 -0
  114. package/Tests/Server/API/BaseAPICreateItemDoesNotMutateModel.test.ts +130 -0
  115. package/Tests/Server/API/GlobalConfigLicense.test.ts +742 -497
  116. package/Tests/Server/API/OnCallReadinessAPI.test.ts +35 -909
  117. package/Tests/Server/API/SsoProviderListingsEdition.test.ts +432 -0
  118. package/Tests/Server/API/TeamMemberLeaveScimLockEdition.test.ts +252 -0
  119. package/Tests/Server/Enterprise/EnterpriseEdition.test.ts +824 -0
  120. package/Tests/Server/Enterprise/EnterpriseFeatureActive.test.ts +643 -0
  121. package/Tests/Server/Enterprise/EnterpriseLicenseSnapshot.test.ts +294 -0
  122. package/Tests/Server/Enterprise/EnterpriseModelFeatureMap.test.ts +93 -0
  123. package/Tests/Server/Enterprise/EnterpriseOnlyModulesLiveInEe.test.ts +582 -0
  124. package/Tests/Server/Enterprise/EnterpriseServerModule.test.ts +247 -0
  125. package/Tests/Server/Enterprise/FakeEnterpriseModule.ts +502 -0
  126. package/Tests/Server/Enterprise/TestBillingFlag.ts +70 -0
  127. package/Tests/Server/EnvironmentConfigEnterpriseEdition.test.ts +314 -0
  128. package/Tests/Server/Infrastructure/ContainerBootConfiguration.test.ts +289 -0
  129. package/Tests/Server/Infrastructure/E2EClickHouseFixtureAccess.test.ts +5 -1
  130. package/Tests/Server/Infrastructure/Postgres/AddConfigurableRuleCriteriaMigration.test.ts +3 -0
  131. package/Tests/Server/Infrastructure/Postgres/AddSloLabelAndOwnerRulesMigration.test.ts +407 -0
  132. package/Tests/Server/Infrastructure/Postgres/AddSloMonitorRuleMonitorTypeMigration.test.ts +171 -0
  133. package/Tests/Server/Infrastructure/Postgres/SloProductOverhaulMigration.test.ts +88 -4
  134. package/Tests/Server/Middleware/UserAuthorization.test.ts +37 -2
  135. package/Tests/Server/Middleware/UserAuthorizationEditionEnforcement.test.ts +516 -0
  136. package/Tests/Server/Services/AuditLogService.test.ts +460 -990
  137. package/Tests/Server/Services/CreateAndReadResponsesKeepIds.test.ts +263 -0
  138. package/Tests/Server/Services/DatabaseServiceRuleCriteriaValidation.test.ts +104 -0
  139. package/Tests/Server/Services/EnterpriseLicenseServiceKeyGeneration.test.ts +151 -0
  140. package/Tests/Server/Services/GlobalConfigServiceLicenseColumns.test.ts +357 -0
  141. package/Tests/Server/Services/MonitorDestinationInfoIPv6.test.ts +101 -0
  142. package/Tests/Server/Services/MonitorProbeServiceIntervalScheduling.test.ts +333 -0
  143. package/Tests/Server/Services/MonitorStatusTimelineDailyAggregates.test.ts +303 -0
  144. package/Tests/Server/Services/ProjectServiceAuditLogLicense.test.ts +701 -0
  145. package/Tests/Server/Services/ProjectServiceAuditLogSettingsCache.test.ts +31 -35
  146. package/Tests/Server/Services/RuleRun/LabelRuleEnginesGroupBRun.test.ts +48 -1
  147. package/Tests/Server/Services/RuleRun/OwnerRuleEnginesGroupBRun.test.ts +30 -0
  148. package/Tests/Server/Services/ScimPushGroupsTeamLocksEdition.test.ts +431 -0
  149. package/Tests/Server/Services/ServiceLevelObjectiveLabelAndOwnerRuleEngines.test.ts +1170 -0
  150. package/Tests/Server/Services/ServiceLevelObjectiveLabelAndOwnerRuleServices.test.ts +172 -0
  151. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.test.ts +323 -0
  152. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleService.test.ts +115 -0
  153. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleTriggers.test.ts +21 -2
  154. package/Tests/Server/Services/ServiceLevelObjectiveMonitorTypePostgres.test.ts +480 -0
  155. package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +162 -0
  156. package/Tests/Server/Services/SsoRequirementReadMasking.test.ts +419 -0
  157. package/Tests/Server/Services/SsoRequirementWriteGuard.test.ts +769 -0
  158. package/Tests/Server/Services/StaticRuleCriteriaRuntimeCoverage.test.ts +45 -6
  159. package/Tests/Server/Services/StatusPagePrivateUserInvitationPolicy.test.ts +94 -0
  160. package/Tests/Server/Services/UserAuthenticationService.test.ts +35 -0
  161. package/Tests/Server/Services/UserServiceSeatLimit.test.ts +94 -56
  162. package/Tests/Server/Types/Database/FeedEventTypeFilterQuery.test.ts +745 -0
  163. package/Tests/Server/Types/Database/Permissions/EditionPermission.test.ts +1823 -0
  164. package/Tests/Server/Types/Markdown.test.ts +183 -1
  165. package/Tests/Server/Types/Workflow/Components/ApiComponentSsrf.test.ts +49 -0
  166. package/Tests/Server/Types/Workflow/Components/TelegramSendMessageToChat.test.ts +331 -0
  167. package/Tests/Server/Utils/AI/SloTools.test.ts +10 -0
  168. package/Tests/Server/Utils/CommunityEditionSsoReport.test.ts +565 -0
  169. package/Tests/Server/Utils/EditionEnforcement.test.ts +303 -0
  170. package/Tests/Server/Utils/FrontendEnvironment.test.ts +367 -0
  171. package/Tests/Server/Utils/InstanceHealth/RedisHealth.test.ts +38 -418
  172. package/Tests/Server/Utils/Monitor/AffectedResourceList.test.ts +518 -0
  173. package/Tests/Server/Utils/Monitor/Criteria/EvaluateOverTime.test.ts +24 -4
  174. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +1 -1
  175. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +1339 -96
  176. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorBreachingSamples.test.ts +1101 -0
  177. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorMetricUnits.test.ts +106 -80
  178. package/Tests/Server/Utils/Monitor/MonitoringIntervalValidator.test.ts +188 -0
  179. package/Tests/Server/Utils/Monitor/RootCauseList.test.ts +252 -0
  180. package/Tests/Server/Utils/Rules/RuleRun/RuleRunPermission.test.ts +154 -0
  181. package/Tests/Server/Utils/SSRFProtection.test.ts +111 -0
  182. package/Tests/Server/Utils/Slo/SloRulePatternValidator.test.ts +67 -0
  183. package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +213 -0
  184. package/Tests/Server/Utils/Workspace/Slack/SlackMarkdownSectionSplit.test.ts +1209 -0
  185. package/Tests/Types/API/HostnameFromAuthority.test.ts +20 -10
  186. package/Tests/Types/API/HostnameIPv6.test.ts +149 -0
  187. package/Tests/Types/API/Route.test.ts +56 -0
  188. package/Tests/Types/API/URLSchemeLikePath.test.ts +304 -0
  189. package/Tests/Types/Database/CanonicalModelInstance.test.ts +190 -0
  190. package/Tests/UI/Components/EditionLabelLapseCopy.ts +104 -0
  191. package/Tests/UI/Components/EditionLabelLicenseManagerWiring.test.tsx +254 -0
  192. package/Tests/UI/Components/EditionLabelLicenseRefresh.test.tsx +84 -178
  193. package/Tests/UI/Components/EditionLabelLicenseStatus.test.tsx +1586 -0
  194. package/Tests/UI/Components/EditionLabelLicenseWritesGuard.test.tsx +157 -0
  195. package/Tests/UI/Components/FeedOptions.test.ts +1404 -0
  196. package/Tests/UI/Components/FeedOptionsButton.test.tsx +2603 -0
  197. package/Tests/UI/Components/Graphs/DayUptimeGraph.test.tsx +49 -2
  198. package/Tests/UI/Components/Icon.test.tsx +26 -0
  199. package/Tests/UI/Components/MarkdownConverters.test.ts +684 -0
  200. package/Tests/UI/Components/ModelListEmptyState.test.tsx +289 -0
  201. package/Tests/UI/Components/RuleCriteria/RuleCriteriaModelTable.test.tsx +3 -3
  202. package/Tests/UI/Components/TelemetryViewer/TelemetryViewerEmptyContent.test.tsx +131 -0
  203. package/Tests/UI/Components/useFeedItemsViewKey.test.tsx +1017 -0
  204. package/Tests/UI/Components/useFeedOptions.test.tsx +1107 -0
  205. package/Tests/UI/EsbuildEnterpriseAlias.test.ts +1127 -0
  206. package/Tests/Utils/HostAddressUtil.test.ts +138 -0
  207. package/Tests/Utils/LabelRuleImportExport.test.ts +102 -0
  208. package/Tests/Utils/Monitor/MonitorDestinationUtil.test.ts +313 -0
  209. package/Tests/Utils/Monitor/MonitoringIntervalUtil.test.ts +304 -0
  210. package/Tests/Utils/Slo/SloDuration.test.ts +115 -24
  211. package/Tests/Utils/Slo/SloHealth.test.ts +93 -10
  212. package/Tests/Utils/Slo/SloMonitorRuleCriteria.test.ts +109 -0
  213. package/Tests/Utils/Slo/SloOverviewText.test.ts +64 -0
  214. package/Tests/Utils/StatusPage/UptimeDailyAggregateUtil.test.ts +248 -0
  215. package/Types/API/Hostname.ts +63 -16
  216. package/Types/API/Route.ts +19 -2
  217. package/Types/API/URL.ts +26 -2
  218. package/Types/Database/AccessControl/ColumnAccessControl.ts +19 -7
  219. package/Types/Database/AccessControl/ColumnBillingAccessControl.ts +19 -7
  220. package/Types/Database/AccessControl/OwnerOnlyColumn.ts +15 -6
  221. package/Types/Database/CanonicalModelInstance.ts +102 -0
  222. package/Types/Database/TableColumn.ts +20 -6
  223. package/Types/EnterpriseLicense/EnterpriseLicensePeriods.ts +29 -0
  224. package/Types/IP/IPv6.ts +6 -1
  225. package/Types/Monitor/MetricMonitor/MetricCriteriaContext.ts +6 -5
  226. package/Types/Monitor/MetricMonitor/MetricMonitorResponse.ts +9 -0
  227. package/Types/Monitor/MonitorStep.ts +6 -1
  228. package/Types/Monitor/ProxmoxAlertTemplates.ts +1 -1
  229. package/Types/Permission.ts +90 -0
  230. package/Types/Rules/RuleCriteriaFieldRegistry.ts +11 -0
  231. package/Types/Rules/RuleRun.ts +4 -0
  232. package/Types/StatusPage/UptimeDailyAggregate.ts +110 -0
  233. package/UI/Components/EditionLabel/EditionLabel.tsx +796 -1195
  234. package/UI/Components/EditionLabel/LicenseManager.ts +119 -0
  235. package/UI/Components/Feed/FeedOptions.ts +427 -0
  236. package/UI/Components/Feed/FeedOptionsButton.tsx +692 -0
  237. package/UI/Components/Feed/useFeedItems.ts +28 -9
  238. package/UI/Components/Feed/useFeedOptions.ts +225 -0
  239. package/UI/Components/Graphs/DayUptimeGraph.tsx +123 -15
  240. package/UI/Components/Icon/Icon.tsx +1 -1
  241. package/UI/Components/Markdown.tsx/MarkdownConverters.ts +0 -0
  242. package/UI/Components/ModelList/ModelList.tsx +11 -1
  243. package/UI/Components/MonitorGraphs/Uptime.tsx +69 -0
  244. package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +28 -12
  245. package/UI/esbuild-enterprise.js +263 -0
  246. package/Utils/EnterpriseLicense/EnterpriseLicenseUsage.ts +7 -2
  247. package/Utils/HostAddressUtil.ts +94 -0
  248. package/Utils/LabelRuleImportExport.ts +27 -9
  249. package/Utils/Monitor/MetricValueFormatter.ts +2 -2
  250. package/Utils/Monitor/MonitorDestinationUtil.ts +157 -0
  251. package/Utils/Monitor/MonitoringIntervalUtil.ts +436 -0
  252. package/Utils/Rules/MonitorTypeRuleCriteria.ts +31 -0
  253. package/Utils/Slo/SloDuration.ts +22 -6
  254. package/Utils/Slo/SloHealth.ts +8 -7
  255. package/Utils/Slo/SloMonitorRuleCriteria.ts +33 -1
  256. package/Utils/Slo/SloOverviewText.ts +33 -0
  257. package/Utils/Slo/SloProjection.ts +4 -3
  258. package/Utils/StatusPage/UptimeDailyAggregateUtil.ts +192 -0
  259. package/build/dist/Models/AnalyticsModels/AuditLog.js +23 -15
  260. package/build/dist/Models/AnalyticsModels/AuditLog.js.map +1 -1
  261. package/build/dist/Models/DatabaseModels/GlobalConfig.js +25 -0
  262. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  263. package/build/dist/Models/DatabaseModels/Index.js +4 -0
  264. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  265. package/build/dist/Models/DatabaseModels/Monitor.js +2 -2
  266. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  267. package/build/dist/Models/DatabaseModels/MonitorTemplate.js +2 -1
  268. package/build/dist/Models/DatabaseModels/MonitorTemplate.js.map +1 -1
  269. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveFeed.js +2 -0
  270. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveFeed.js.map +1 -1
  271. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.js +530 -0
  272. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.js.map +1 -0
  273. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.js +39 -4
  274. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.js.map +1 -1
  275. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.js +612 -0
  276. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.js.map +1 -0
  277. package/build/dist/Server/API/BaseAPI.js +26 -5
  278. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  279. package/build/dist/Server/API/GlobalConfigAPI.js +148 -344
  280. package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
  281. package/build/dist/Server/API/ProjectOIDC.js +24 -16
  282. package/build/dist/Server/API/ProjectOIDC.js.map +1 -1
  283. package/build/dist/Server/API/ProjectSSO.js +24 -16
  284. package/build/dist/Server/API/ProjectSSO.js.map +1 -1
  285. package/build/dist/Server/API/StatusPageAPI.js +93 -43
  286. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  287. package/build/dist/Server/API/TeamMemberAPI.js +8 -1
  288. package/build/dist/Server/API/TeamMemberAPI.js.map +1 -1
  289. package/build/dist/Server/Enterprise/EnterpriseEdition.js +466 -0
  290. package/build/dist/Server/Enterprise/EnterpriseEdition.js.map +1 -0
  291. package/build/dist/Server/Enterprise/EnterpriseFeature.js +39 -0
  292. package/build/dist/Server/Enterprise/EnterpriseFeature.js.map +1 -0
  293. package/build/dist/Server/Enterprise/EnterpriseLicenseSnapshot.js +70 -0
  294. package/build/dist/Server/Enterprise/EnterpriseLicenseSnapshot.js.map +1 -0
  295. package/build/dist/Server/Enterprise/EnterpriseServerModule.js +84 -0
  296. package/build/dist/Server/Enterprise/EnterpriseServerModule.js.map +1 -0
  297. package/build/dist/Server/EnvironmentConfig.js +84 -2
  298. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  299. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1793900000000-AddEnterpriseEditionFirstSeenAt.js +18 -0
  300. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1793900000000-AddEnterpriseEditionFirstSeenAt.js.map +1 -0
  301. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.js +93 -0
  302. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.js.map +1 -0
  303. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.js +12 -0
  304. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.js.map +1 -0
  305. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  306. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  307. package/build/dist/Server/Middleware/UserAuthorization.js +30 -11
  308. package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
  309. package/build/dist/Server/Services/AuditLogService.js +71 -740
  310. package/build/dist/Server/Services/AuditLogService.js.map +1 -1
  311. package/build/dist/Server/Services/DatabaseService.js +7 -0
  312. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  313. package/build/dist/Server/Services/EnterpriseLicenseService.js +34 -0
  314. package/build/dist/Server/Services/EnterpriseLicenseService.js.map +1 -1
  315. package/build/dist/Server/Services/GlobalConfigService.js +63 -0
  316. package/build/dist/Server/Services/GlobalConfigService.js.map +1 -1
  317. package/build/dist/Server/Services/Index.js +4 -0
  318. package/build/dist/Server/Services/Index.js.map +1 -1
  319. package/build/dist/Server/Services/MonitorProbeService.js +90 -17
  320. package/build/dist/Server/Services/MonitorProbeService.js.map +1 -1
  321. package/build/dist/Server/Services/MonitorService.js +48 -6
  322. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  323. package/build/dist/Server/Services/MonitorStatusTimelineService.js +242 -0
  324. package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
  325. package/build/dist/Server/Services/MonitorTemplateService.js +16 -0
  326. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  327. package/build/dist/Server/Services/ProjectService.js +214 -0
  328. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  329. package/build/dist/Server/Services/RumSessionPinService.js +24 -6
  330. package/build/dist/Server/Services/RumSessionPinService.js.map +1 -1
  331. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.js +295 -0
  332. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.js.map +1 -0
  333. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleService.js +56 -0
  334. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleService.js.map +1 -0
  335. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js +59 -14
  336. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js.map +1 -1
  337. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleService.js +22 -1
  338. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleService.js.map +1 -1
  339. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.js +400 -0
  340. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.js.map +1 -0
  341. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleService.js +56 -0
  342. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleService.js.map +1 -0
  343. package/build/dist/Server/Services/ServiceLevelObjectiveService.js +21 -0
  344. package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
  345. package/build/dist/Server/Services/StatusPagePrivateUserService.js +7 -3
  346. package/build/dist/Server/Services/StatusPagePrivateUserService.js.map +1 -1
  347. package/build/dist/Server/Services/StatusPageService.js +71 -0
  348. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  349. package/build/dist/Server/Services/TeamMemberService.js +10 -0
  350. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  351. package/build/dist/Server/Services/TeamService.js +9 -0
  352. package/build/dist/Server/Services/TeamService.js.map +1 -1
  353. package/build/dist/Server/Services/UserService.js +10 -16
  354. package/build/dist/Server/Services/UserService.js.map +1 -1
  355. package/build/dist/Server/Types/Database/Permissions/CreatePermission.js +10 -0
  356. package/build/dist/Server/Types/Database/Permissions/CreatePermission.js.map +1 -1
  357. package/build/dist/Server/Types/Database/Permissions/EditionPermission.js +154 -14
  358. package/build/dist/Server/Types/Database/Permissions/EditionPermission.js.map +1 -1
  359. package/build/dist/Server/Types/Database/Permissions/TablePermission.js +10 -3
  360. package/build/dist/Server/Types/Database/Permissions/TablePermission.js.map +1 -1
  361. package/build/dist/Server/Types/Database/Permissions/UpdatePermission.js +17 -0
  362. package/build/dist/Server/Types/Database/Permissions/UpdatePermission.js.map +1 -1
  363. package/build/dist/Server/Types/Markdown.js +31 -0
  364. package/build/dist/Server/Types/Markdown.js.map +1 -1
  365. package/build/dist/Server/Types/Workflow/Components/Telegram/SendMessageToChat.js +20 -1
  366. package/build/dist/Server/Types/Workflow/Components/Telegram/SendMessageToChat.js.map +1 -1
  367. package/build/dist/Server/Utils/AI/Toolbox/SloTools.js +1 -0
  368. package/build/dist/Server/Utils/AI/Toolbox/SloTools.js.map +1 -1
  369. package/build/dist/Server/Utils/CommunityEditionSsoReport.js +223 -0
  370. package/build/dist/Server/Utils/CommunityEditionSsoReport.js.map +1 -0
  371. package/build/dist/Server/Utils/EditionEnforcement.js +187 -0
  372. package/build/dist/Server/Utils/EditionEnforcement.js.map +1 -0
  373. package/build/dist/Server/Utils/FrontendEnvironment.js +31 -2
  374. package/build/dist/Server/Utils/FrontendEnvironment.js.map +1 -1
  375. package/build/dist/Server/Utils/InstanceHealth/RedisHealth.js +6 -193
  376. package/build/dist/Server/Utils/InstanceHealth/RedisHealth.js.map +1 -1
  377. package/build/dist/Server/Utils/Monitor/AffectedResourceList.js +67 -0
  378. package/build/dist/Server/Utils/Monitor/AffectedResourceList.js.map +1 -0
  379. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +1 -1
  380. package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js +8 -1
  381. package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js.map +1 -1
  382. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +1 -1
  383. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +585 -367
  384. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  385. package/build/dist/Server/Utils/Monitor/MonitoringIntervalValidator.js +88 -0
  386. package/build/dist/Server/Utils/Monitor/MonitoringIntervalValidator.js.map +1 -0
  387. package/build/dist/Server/Utils/Monitor/RootCauseList.js +101 -0
  388. package/build/dist/Server/Utils/Monitor/RootCauseList.js.map +1 -0
  389. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js +28 -0
  390. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js.map +1 -1
  391. package/build/dist/Server/Utils/Slo/SloRulePatternValidator.js +28 -0
  392. package/build/dist/Server/Utils/Slo/SloRulePatternValidator.js.map +1 -0
  393. package/build/dist/Server/Utils/Workspace/Slack/Slack.js +331 -11
  394. package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
  395. package/build/dist/Types/API/Hostname.js +59 -11
  396. package/build/dist/Types/API/Hostname.js.map +1 -1
  397. package/build/dist/Types/API/Route.js +18 -2
  398. package/build/dist/Types/API/Route.js.map +1 -1
  399. package/build/dist/Types/API/URL.js +23 -2
  400. package/build/dist/Types/API/URL.js.map +1 -1
  401. package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js +19 -7
  402. package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js.map +1 -1
  403. package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js +19 -7
  404. package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js.map +1 -1
  405. package/build/dist/Types/Database/AccessControl/OwnerOnlyColumn.js +13 -6
  406. package/build/dist/Types/Database/AccessControl/OwnerOnlyColumn.js.map +1 -1
  407. package/build/dist/Types/Database/CanonicalModelInstance.js +88 -0
  408. package/build/dist/Types/Database/CanonicalModelInstance.js.map +1 -0
  409. package/build/dist/Types/Database/TableColumn.js +19 -6
  410. package/build/dist/Types/Database/TableColumn.js.map +1 -1
  411. package/build/dist/Types/EnterpriseLicense/EnterpriseLicensePeriods.js +28 -0
  412. package/build/dist/Types/EnterpriseLicense/EnterpriseLicensePeriods.js.map +1 -0
  413. package/build/dist/Types/IP/IPv6.js +6 -1
  414. package/build/dist/Types/IP/IPv6.js.map +1 -1
  415. package/build/dist/Types/Monitor/MonitorStep.js +5 -0
  416. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  417. package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js +1 -1
  418. package/build/dist/Types/Permission.js +84 -0
  419. package/build/dist/Types/Permission.js.map +1 -1
  420. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js +11 -0
  421. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js.map +1 -1
  422. package/build/dist/Types/Rules/RuleRun.js +4 -0
  423. package/build/dist/Types/Rules/RuleRun.js.map +1 -1
  424. package/build/dist/Types/StatusPage/UptimeDailyAggregate.js +32 -0
  425. package/build/dist/Types/StatusPage/UptimeDailyAggregate.js.map +1 -0
  426. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +414 -585
  427. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  428. package/build/dist/UI/Components/EditionLabel/LicenseManager.js +2 -0
  429. package/build/dist/UI/Components/EditionLabel/LicenseManager.js.map +1 -0
  430. package/build/dist/UI/Components/Feed/FeedOptions.js +235 -0
  431. package/build/dist/UI/Components/Feed/FeedOptions.js.map +1 -0
  432. package/build/dist/UI/Components/Feed/FeedOptionsButton.js +346 -0
  433. package/build/dist/UI/Components/Feed/FeedOptionsButton.js.map +1 -0
  434. package/build/dist/UI/Components/Feed/useFeedItems.js +17 -7
  435. package/build/dist/UI/Components/Feed/useFeedItems.js.map +1 -1
  436. package/build/dist/UI/Components/Feed/useFeedOptions.js +130 -0
  437. package/build/dist/UI/Components/Feed/useFeedOptions.js.map +1 -0
  438. package/build/dist/UI/Components/Graphs/DayUptimeGraph.js +90 -17
  439. package/build/dist/UI/Components/Graphs/DayUptimeGraph.js.map +1 -1
  440. package/build/dist/UI/Components/Icon/Icon.js +1 -1
  441. package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
  442. package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js +0 -0
  443. package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js.map +1 -1
  444. package/build/dist/UI/Components/ModelList/ModelList.js +4 -1
  445. package/build/dist/UI/Components/ModelList/ModelList.js.map +1 -1
  446. package/build/dist/UI/Components/MonitorGraphs/Uptime.js +42 -1
  447. package/build/dist/UI/Components/MonitorGraphs/Uptime.js.map +1 -1
  448. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +2 -2
  449. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
  450. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js +6 -2
  451. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js.map +1 -1
  452. package/build/dist/Utils/HostAddressUtil.js +80 -0
  453. package/build/dist/Utils/HostAddressUtil.js.map +1 -0
  454. package/build/dist/Utils/LabelRuleImportExport.js +22 -6
  455. package/build/dist/Utils/LabelRuleImportExport.js.map +1 -1
  456. package/build/dist/Utils/Monitor/MetricValueFormatter.js +2 -2
  457. package/build/dist/Utils/Monitor/MonitorDestinationUtil.js +127 -0
  458. package/build/dist/Utils/Monitor/MonitorDestinationUtil.js.map +1 -0
  459. package/build/dist/Utils/Monitor/MonitoringIntervalUtil.js +374 -0
  460. package/build/dist/Utils/Monitor/MonitoringIntervalUtil.js.map +1 -0
  461. package/build/dist/Utils/Rules/MonitorTypeRuleCriteria.js +18 -0
  462. package/build/dist/Utils/Rules/MonitorTypeRuleCriteria.js.map +1 -0
  463. package/build/dist/Utils/Slo/SloDuration.js +13 -5
  464. package/build/dist/Utils/Slo/SloDuration.js.map +1 -1
  465. package/build/dist/Utils/Slo/SloHealth.js +8 -7
  466. package/build/dist/Utils/Slo/SloHealth.js.map +1 -1
  467. package/build/dist/Utils/Slo/SloMonitorRuleCriteria.js +16 -1
  468. package/build/dist/Utils/Slo/SloMonitorRuleCriteria.js.map +1 -1
  469. package/build/dist/Utils/Slo/SloOverviewText.js +16 -0
  470. package/build/dist/Utils/Slo/SloOverviewText.js.map +1 -1
  471. package/build/dist/Utils/Slo/SloProjection.js.map +1 -1
  472. package/build/dist/Utils/StatusPage/UptimeDailyAggregateUtil.js +148 -0
  473. package/build/dist/Utils/StatusPage/UptimeDailyAggregateUtil.js.map +1 -0
  474. package/jest.config.json +3 -1
  475. package/package.json +1 -1
  476. package/test-setup.sh +103 -34
  477. package/tsconfig.json +13 -0
  478. package/Server/API/EnterpriseLicenseAPI.ts +0 -925
  479. package/Server/API/TeamComplianceAPI.ts +0 -158
  480. package/Server/Middleware/SCIMAuthorization.ts +0 -141
  481. package/Server/Services/TeamComplianceService.ts +0 -733
  482. package/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.ts +0 -186
  483. package/Server/Utils/InstanceHealth/PostgresHealth.ts +0 -286
  484. package/Tests/App/AdminDashboard/EnterpriseLicenseInstanceActivity.test.tsx +0 -451
  485. package/Tests/App/Dashboard/AuditLogsTable.test.tsx +0 -598
  486. package/Tests/App/Dashboard/SecurityEventsExplorer.test.tsx +0 -761
  487. package/Tests/App/Dashboard/SecurityEventsSideMenu.test.tsx +0 -247
  488. package/Tests/App/Dashboard/SecurityEventsTableColumns.test.tsx +0 -382
  489. package/Tests/App/Dashboard/SecurityEventsVolumeChart.test.tsx +0 -321
  490. package/Tests/Server/API/EnterpriseLicenseReportUserCount.test.ts +0 -1851
  491. package/Tests/Server/Middleware/SCIMAuthorization.test.ts +0 -525
  492. package/Tests/Server/Services/TeamComplianceServiceBehaviour.test.ts +0 -1845
  493. package/Tests/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.test.ts +0 -495
  494. package/Tests/Server/Utils/InstanceHealth/PostgresHealth.test.ts +0 -337
  495. package/Tests/Utils/EnterpriseLicenseSeats.test.ts +0 -576
  496. package/Tests/Utils/EnterpriseLicenseSync.test.ts +0 -568
  497. package/Utils/EnterpriseLicense/EnterpriseLicenseSeats.ts +0 -287
  498. package/Utils/EnterpriseLicense/EnterpriseLicenseSync.ts +0 -244
  499. package/build/dist/Server/API/EnterpriseLicenseAPI.js +0 -646
  500. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +0 -1
  501. package/build/dist/Server/API/TeamComplianceAPI.js +0 -111
  502. package/build/dist/Server/API/TeamComplianceAPI.js.map +0 -1
  503. package/build/dist/Server/Middleware/SCIMAuthorization.js +0 -124
  504. package/build/dist/Server/Middleware/SCIMAuthorization.js.map +0 -1
  505. package/build/dist/Server/Services/TeamComplianceService.js +0 -537
  506. package/build/dist/Server/Services/TeamComplianceService.js.map +0 -1
  507. package/build/dist/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.js +0 -145
  508. package/build/dist/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.js.map +0 -1
  509. package/build/dist/Server/Utils/InstanceHealth/PostgresHealth.js +0 -194
  510. package/build/dist/Server/Utils/InstanceHealth/PostgresHealth.js.map +0 -1
  511. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSeats.js +0 -168
  512. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSeats.js.map +0 -1
  513. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js +0 -174
  514. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js.map +0 -1
@@ -1,56 +1,59 @@
1
- import GlobalConfigAPI from "../../../Server/API/GlobalConfigAPI";
1
+ import GlobalConfigAPI, {
2
+ LICENSE_RESPONSE_CONFIG_SELECT,
3
+ } from "../../../Server/API/GlobalConfigAPI";
2
4
  import MasterAdminAuthorization from "../../../Server/Middleware/MasterAdminAuthorization";
3
5
  import UserMiddleware from "../../../Server/Middleware/UserAuthorization";
4
6
  import GlobalConfigService from "../../../Server/Services/GlobalConfigService";
5
- import UserService from "../../../Server/Services/UserService";
6
7
  import Response from "../../../Server/Utils/Response";
8
+ import { AppVersion } from "../../../Server/EnvironmentConfig";
9
+ import EnterpriseEdition from "../../../Server/Enterprise/EnterpriseEdition";
10
+ import EnterpriseFeature from "../../../Server/Enterprise/EnterpriseFeature";
11
+ import {
12
+ EnterpriseLicenseSnapshot,
13
+ EnterpriseLicenseStatus,
14
+ SeatUsage,
15
+ } from "../../../Server/Enterprise/EnterpriseLicenseSnapshot";
7
16
  import GlobalConfig from "../../../Models/DatabaseModels/GlobalConfig";
8
- import HTTPErrorResponse from "../../../Types/API/HTTPErrorResponse";
9
- import HTTPResponse from "../../../Types/API/HTTPResponse";
10
- import BadDataException from "../../../Types/Exception/BadDataException";
11
17
  import Exception from "../../../Types/Exception/Exception";
12
18
  import NotAuthenticatedException from "../../../Types/Exception/NotAuthenticatedException";
13
19
  import { JSONObject } from "../../../Types/JSON";
14
20
  import ObjectID from "../../../Types/ObjectID";
15
- import PositiveNumber from "../../../Types/PositiveNumber";
16
21
  import UserType from "../../../Types/UserType";
17
- import API from "../../../Utils/API";
18
22
  import {
19
23
  NextFunction,
20
24
  OneUptimeRequest,
21
25
  OneUptimeResponse,
22
26
  } from "../../../Server/Utils/Express";
23
27
  import { mockRouter } from "./Helpers";
24
- import { beforeEach, afterEach, describe, expect, it } from "@jest/globals";
28
+ import FakeEnterpriseModule, {
29
+ createLicenseSnapshot,
30
+ createLicenseSnapshotWithStatus,
31
+ installFakeEnterpriseModule,
32
+ uninstallEnterpriseModule,
33
+ } from "../Enterprise/FakeEnterpriseModule";
34
+ import { setTestBillingEnabled } from "../Enterprise/TestBillingFlag";
35
+ import { afterEach, beforeEach, describe, expect, it } from "@jest/globals";
25
36
 
26
37
  /*
27
- * The self-hosted half of the licence: activating a key, refreshing the key
28
- * the installation already holds, and reporting what the seat limit means
29
- * right now.
38
+ * GET /global-config/license: the one license route that stays in core.
30
39
  *
31
- * Two things brought this suite into being.
40
+ * Activating and refreshing a license moved into the Enterprise license client
41
+ * (ee/Server/License, tested in ee/Tests/Server/License). What stays here is
42
+ * the read, because the Community Edition still needs the version and
43
+ * update-check half of the edition dialog, and because the answer has to be
44
+ * cut down per caller:
32
45
  *
33
- * The seat limit is set on oneuptime.com and changes on any day — a customer
34
- * buys ten more seats at noon. It reaches the installation through the daily
35
- * report job, so until now the only way to apply it sooner was for somebody to
36
- * re-type the licence key into a box that is HIDDEN while the licence is
37
- * valid. /license/refresh is that missing button, and it exists precisely
38
- * because the installation now refuses users above the limit: waiting a day to
39
- * learn about seats you have already paid for is a different feature when the
40
- * old number is being enforced.
46
+ * - this route serves the signed-out login page, so it cannot require
47
+ * authentication, and anonymous callers used to be told the seat limit,
48
+ * the current user count and more;
49
+ * - any signed-in user - including a status-page subscriber, whose token
50
+ * also decodes to a userId - used to receive the license key, the token
51
+ * and the host of every instance on the license.
41
52
  *
42
- * And both writes used to run on UserMiddleware.getUserMiddleware, which lets
43
- * anonymous callers through — it has to, because the GET on the same path
44
- * serves the signed-out login page. That made the route that decides this
45
- * installation's seat ceiling reachable without signing in at all.
53
+ * Now a master admin gets everything and everybody else gets only what the
54
+ * edition pill shows.
46
55
  */
47
56
 
48
- /*
49
- * PasswordHash carries a pre-existing TS diagnostic that fails any suite whose
50
- * require graph reaches it, and BaseAPI's graph still reaches it. Replaced with
51
- * a factory rather than automocked, because an automock still type-checks the
52
- * real file.
53
- */
54
57
  jest.mock("../../../Server/Utils/PasswordHash", () => {
55
58
  return {
56
59
  __esModule: true,
@@ -64,7 +67,6 @@ jest.mock("../../../Server/Utils/PasswordHash", () => {
64
67
  };
65
68
  });
66
69
 
67
- // Same story as PasswordHash: a local-only diagnostic in a module dragged in.
68
70
  jest.mock("../../../Server/Utils/VerificationCode", () => {
69
71
  return {
70
72
  __esModule: true,
@@ -103,79 +105,19 @@ jest.mock("../../../Server/Utils/Response", () => {
103
105
  };
104
106
  });
105
107
 
106
- /*
107
- * The deployment flags live on globalThis rather than in module-scope
108
- * variables, and the getters below reference nothing but globalThis.
109
- *
110
- * jest hoists the mock factory above every declaration in this file, and
111
- * BaseAPI's import graph reaches IncidentFeedService, which reads
112
- * IsBillingEnabled at module scope. That read lands inside these getters
113
- * during the import — before a `let` in this file has initialised — and a
114
- * getter that closed over one would throw a temporal-dead-zone
115
- * ReferenceError before a single test ran. Absent flags read as false, which
116
- * is a fine thing for an unrelated service to see on its way past.
117
- *
118
- * They are also live accessors rather than values: the routes read the flags
119
- * when they run, and object spread would flatten them at import time.
120
- */
121
- const BILLING_FLAG_KEY: string = "__oneUptimeTestIsBillingEnabled";
122
- const ENTERPRISE_FLAG_KEY: string = "__oneUptimeTestIsEnterpriseEdition";
123
-
124
- type SetDeploymentFlagFunction = (key: string, value: boolean) => void;
125
-
126
- const setDeploymentFlag: SetDeploymentFlagFunction = (
127
- key: string,
128
- value: boolean,
129
- ): void => {
130
- (globalThis as unknown as Record<string, unknown>)[key] = value;
131
- };
132
-
108
+ // CI's config.env sets BILLING_ENABLED=true; this suite pins it per test.
133
109
  jest.mock("../../../Server/EnvironmentConfig", () => {
134
- const actual: Record<string, unknown> = jest.requireActual(
135
- "../../../Server/EnvironmentConfig",
136
- ) as Record<string, unknown>;
137
-
138
- const mocked: Record<string, unknown> = {
139
- ...actual,
140
- __esModule: true,
141
- };
142
-
143
- Object.defineProperty(mocked, "IsBillingEnabled", {
144
- get: (): boolean => {
145
- return (
146
- (globalThis as unknown as Record<string, unknown>)[
147
- "__oneUptimeTestIsBillingEnabled"
148
- ] === true
149
- );
150
- },
151
- });
152
-
153
- Object.defineProperty(mocked, "IsEnterpriseEdition", {
154
- get: (): boolean => {
155
- return (
156
- (globalThis as unknown as Record<string, unknown>)[
157
- "__oneUptimeTestIsEnterpriseEdition"
158
- ] === true
159
- );
160
- },
161
- });
162
-
163
- return mocked;
164
- });
165
-
166
- /*
167
- * Factories rather than automocks. An automock still loads the real module to
168
- * copy its shape, and loading UserService drags in most of the service graph -
169
- * some of which reads IsBillingEnabled at module scope, before the flags above
170
- * have initialised. Nothing here needs the real implementations.
171
- */
172
- jest.mock("../../../Utils/API", () => {
173
- return {
174
- __esModule: true,
175
- default: {
176
- post: jest.fn(),
177
- },
178
- };
110
+ const billingFlag: typeof import("../Enterprise/TestBillingFlag") =
111
+ jest.requireActual(
112
+ "../Enterprise/TestBillingFlag",
113
+ ) as typeof import("../Enterprise/TestBillingFlag");
114
+
115
+ return billingFlag.withLiveBillingFlag(
116
+ jest.requireActual("../../../Server/EnvironmentConfig") as Record<
117
+ string,
118
+ unknown
119
+ >,
120
+ );
179
121
  });
180
122
 
181
123
  jest.mock("../../../Server/Services/GlobalConfigService", () => {
@@ -202,7 +144,47 @@ const LICENSE_ROUTE: string = "/global-config/license";
202
144
  const REFRESH_ROUTE: string = "/global-config/license/refresh";
203
145
 
204
146
  const STORED_LICENSE_KEY: string = "acme-stored-license-key";
147
+ const STORED_TOKEN: string = "stored.license.token";
205
148
  const INSTANCE_ID: ObjectID = ObjectID.generate();
149
+ const DAY_IN_MS: number = 24 * 60 * 60 * 1000;
150
+
151
+ // Exactly what anybody but a master admin may learn.
152
+ const PUBLIC_KEYS: Array<string> = [
153
+ "companyName",
154
+ "edition",
155
+ "expiresAt",
156
+ "features",
157
+ "graceEndsAt",
158
+ "graceReason",
159
+ "isEvaluation",
160
+ "isEvaluationLicense",
161
+ "licenseValid",
162
+ "status",
163
+ "verification",
164
+ ].sort();
165
+
166
+ // Never in a response to anybody but a master admin.
167
+ const MASTER_ADMIN_ONLY_KEYS: Array<string> = [
168
+ "activationMode",
169
+ "canAddMoreUsers",
170
+ "currentUserCount",
171
+ "currentVersion",
172
+ "instanceId",
173
+ "instances",
174
+ "isSeatLimitEnforced",
175
+ "isUpdateAvailable",
176
+ "isUpdateCheckDisabled",
177
+ "latestVersion",
178
+ "latestVersionCheckedAt",
179
+ "latestVersionPublishedAt",
180
+ "licenseKey",
181
+ "message",
182
+ "seatsInUse",
183
+ "seatsRemaining",
184
+ "token",
185
+ "userCountUpdatedAt",
186
+ "userLimit",
187
+ ].sort();
206
188
 
207
189
  type MakeStoredConfigFunction = (
208
190
  overrides?: Record<string, unknown>,
@@ -215,100 +197,103 @@ const makeStoredConfig: MakeStoredConfigFunction = (
215
197
  config.id = ObjectID.getZeroObjectID();
216
198
  config.instanceId = INSTANCE_ID;
217
199
  config.enterpriseLicenseKey = STORED_LICENSE_KEY;
200
+ config.enterpriseLicenseToken = STORED_TOKEN;
201
+ config.enterpriseLicenseUserLimit = 999;
202
+ config.enterpriseLicenseCurrentUserCount = 42;
203
+ config.enterpriseLicenseUserCountUpdatedAt = new Date(
204
+ "2026-09-01T00:00:00.000Z",
205
+ );
206
+ config.enterpriseLicenseInstances = [
207
+ {
208
+ instanceId: INSTANCE_ID.toString(),
209
+ host: "prod.acme.internal",
210
+ userCount: 42,
211
+ lastReportedAt: "2026-09-01T00:00:00.000Z",
212
+ version: "13.0.0",
213
+ },
214
+ ];
215
+ config.latestReleaseVersion = "99.0.0";
216
+ config.latestReleasePublishedAt = new Date("2026-09-10T00:00:00.000Z");
217
+ config.latestReleaseCheckedAt = new Date("2026-09-11T00:00:00.000Z");
218
218
 
219
219
  return Object.assign(config, overrides || {});
220
220
  };
221
221
 
222
- type LicenseServerPayloadFunction = (
223
- overrides?: Record<string, unknown>,
224
- ) => JSONObject;
225
-
226
- const licenseServerPayload: LicenseServerPayloadFunction = (
227
- overrides?: Record<string, unknown>,
228
- ): JSONObject => {
229
- return {
230
- companyName: "Acme Inc",
231
- expiresAt: "2030-01-01T00:00:00.000Z",
232
- licenseKey: STORED_LICENSE_KEY,
233
- token: "signed.jwt.token",
234
- isEvaluationLicense: false,
235
- userLimit: 150,
236
- currentUserCount: 42,
237
- userCountUpdatedAt: "2026-01-01T00:00:00.000Z",
238
- instances: [],
239
- ...overrides,
240
- };
241
- };
242
-
243
- type GetResponseBodyFunction = () => JSONObject;
244
-
245
- const getResponseBody: GetResponseBodyFunction = (): JSONObject => {
246
- const calls: Array<Array<unknown>> = (
247
- Response.sendJsonObjectResponse as unknown as jest.Mock
248
- ).mock.calls as Array<Array<unknown>>;
249
-
250
- expect(calls).toHaveLength(1);
251
-
252
- return calls[0]![2] as JSONObject;
222
+ const ENFORCED_SEAT_USAGE: SeatUsage = {
223
+ isEnforced: true,
224
+ userLimit: 50,
225
+ seatsInUse: 49,
226
+ seatsRemaining: 1,
227
+ hasSeatForNewUser: true,
228
+ seatsUsedByOtherInstances: 0,
253
229
  };
254
230
 
255
- type GetStoredUpdateFunction = () => JSONObject;
256
-
257
- const getStoredUpdate: GetStoredUpdateFunction = (): JSONObject => {
258
- const calls: Array<Array<unknown>> = (
259
- GlobalConfigService.updateOneById as unknown as jest.Mock
260
- ).mock.calls as Array<Array<unknown>>;
231
+ type CallerKind = "anonymous" | "signed-in user" | "master admin";
261
232
 
262
- expect(calls.length).toBeGreaterThan(0);
263
-
264
- return (calls[0]![0] as Record<string, unknown>)["data"] as JSONObject;
265
- };
266
-
267
- describe("GlobalConfigAPI licence routes", () => {
233
+ describe("GET /global-config/license", () => {
268
234
  let mockRequest: OneUptimeRequest;
269
235
  let mockResponse: OneUptimeResponse;
270
236
  let nextFunction: NextFunction;
271
237
 
272
- type CallRouteFunction = (route: string) => Promise<void>;
238
+ const setCaller: (caller: CallerKind) => void = (
239
+ caller: CallerKind,
240
+ ): void => {
241
+ const request: Record<string, unknown> = mockRequest as unknown as Record<
242
+ string,
243
+ unknown
244
+ >;
245
+
246
+ if (caller === "anonymous") {
247
+ delete request["userAuthorization"];
248
+ return;
249
+ }
250
+
251
+ request["userAuthorization"] = {
252
+ userId: ObjectID.generate(),
253
+ isMasterAdmin: caller === "master admin",
254
+ };
255
+ };
273
256
 
274
- const callRoute: CallRouteFunction = async (route: string): Promise<void> => {
257
+ const callGet: () => Promise<JSONObject> = async (): Promise<JSONObject> => {
275
258
  await mockRouter
276
- .match("post", route)
259
+ .match("get", LICENSE_ROUTE)
277
260
  .handlerFunction(mockRequest, mockResponse, nextFunction);
278
- };
279
261
 
280
- type NextErrorFunction = () => Error;
262
+ expect(nextFunction).not.toHaveBeenCalled();
281
263
 
282
- const nextError: NextErrorFunction = (): Error => {
283
- const calls: Array<Array<unknown>> = (nextFunction as unknown as jest.Mock)
284
- .mock.calls as Array<Array<unknown>>;
264
+ const calls: Array<Array<unknown>> = (
265
+ Response.sendJsonObjectResponse as unknown as jest.Mock
266
+ ).mock.calls as Array<Array<unknown>>;
285
267
 
286
268
  expect(calls).toHaveLength(1);
287
269
 
288
- return calls[0]![0] as Error;
270
+ return calls[0]![2] as JSONObject;
271
+ };
272
+
273
+ const installWithSnapshot: (
274
+ snapshot: EnterpriseLicenseSnapshot | null,
275
+ seatUsage?: SeatUsage | null,
276
+ ) => FakeEnterpriseModule = (
277
+ snapshot: EnterpriseLicenseSnapshot | null,
278
+ seatUsage?: SeatUsage | null,
279
+ ): FakeEnterpriseModule => {
280
+ return installFakeEnterpriseModule({
281
+ snapshot,
282
+ seatUsage: seatUsage === undefined ? ENFORCED_SEAT_USAGE : seatUsage,
283
+ });
289
284
  };
290
285
 
291
286
  beforeEach(() => {
292
287
  jest.clearAllMocks();
293
288
  mockRouter.routes = [];
294
- setDeploymentFlag(BILLING_FLAG_KEY, false);
295
- setDeploymentFlag(ENTERPRISE_FLAG_KEY, true);
289
+ setTestBillingEnabled(false);
290
+ uninstallEnterpriseModule();
296
291
 
297
292
  new GlobalConfigAPI();
298
293
 
299
294
  GlobalConfigService.findOneById = jest
300
295
  .fn()
301
296
  .mockResolvedValue(makeStoredConfig());
302
- GlobalConfigService.updateOneById = jest.fn().mockResolvedValue(undefined);
303
- GlobalConfigService.create = jest.fn().mockResolvedValue(undefined);
304
-
305
- UserService.countBy = jest.fn().mockResolvedValue(new PositiveNumber(42));
306
-
307
- (API.post as unknown as jest.Mock) = jest
308
- .fn()
309
- .mockResolvedValue(
310
- new HTTPResponse<JSONObject>(200, licenseServerPayload(), {}),
311
- );
312
297
 
313
298
  /*
314
299
  * Express always gives a request a `query` object; the licence GET reads
@@ -329,469 +314,729 @@ describe("GlobalConfigAPI licence routes", () => {
329
314
  });
330
315
 
331
316
  afterEach(() => {
317
+ uninstallEnterpriseModule();
318
+ setTestBillingEnabled(false);
332
319
  jest.restoreAllMocks();
333
320
  });
334
321
 
335
- describe("who is allowed to write the licence", () => {
336
- /*
337
- * The seat limit these routes store is the number UserService refuses new
338
- * users against. UserMiddleware.getUserMiddleware — which the GET on the
339
- * same path uses, and has to, because it serves the signed-out login page —
340
- * lets anonymous callers straight through, so it is not a guard here.
341
- */
342
- it.each([
343
- ["activating a licence key", LICENSE_ROUTE],
344
- ["refreshing the stored licence", REFRESH_ROUTE],
345
- ])("requires a master admin for %s", (_label: string, route: string) => {
346
- expect(mockRouter.match("post", route).middlewares).toContain(
347
- MasterAdminAuthorization.isAuthorizedMasterAdminMiddleware,
322
+ describe("the routes core still serves", () => {
323
+ it("serves the GET to anyone, so the login page keeps working", () => {
324
+ const route: ReturnType<typeof mockRouter.match> = mockRouter.match(
325
+ "get",
326
+ LICENSE_ROUTE,
348
327
  );
349
- });
350
328
 
351
- it("still serves the licence GET to anyone, so the login page keeps working", () => {
352
- expect(mockRouter.match("get", LICENSE_ROUTE).middlewares).not.toContain(
329
+ expect(route.middlewares).toEqual([UserMiddleware.getUserMiddleware]);
330
+ expect(route.middlewares).not.toContain(
353
331
  MasterAdminAuthorization.isAuthorizedMasterAdminMiddleware,
354
332
  );
355
333
  });
334
+
335
+ /*
336
+ * Activation and refresh are the Enterprise license client's now. A copy
337
+ * left behind in core would be a second writer of the license, served
338
+ * even by the Community Edition.
339
+ */
340
+ it.each([LICENSE_ROUTE, REFRESH_ROUTE])(
341
+ "no longer registers POST %s in core",
342
+ (route: string) => {
343
+ const posts: Array<string> = mockRouter.routes
344
+ .filter((registered: { method: string; uri: string }): boolean => {
345
+ return registered.method === "POST";
346
+ })
347
+ .map((registered: { uri: string }): string => {
348
+ return registered.uri;
349
+ });
350
+
351
+ expect(posts).not.toContain(route);
352
+ },
353
+ );
354
+
355
+ it("keeps /global-config/vars", () => {
356
+ expect(
357
+ mockRouter.match("get", "/global-config/vars").handlerFunction,
358
+ ).toBeDefined();
359
+ });
356
360
  });
357
361
 
358
- describe("POST /global-config/license - activating a key", () => {
359
- it("rejects a request with no licence key", async () => {
360
- mockRequest.body = {};
362
+ describe("on the Community Edition", () => {
363
+ it.each([
364
+ "anonymous",
365
+ "signed-in user",
366
+ "master admin",
367
+ ] as Array<CallerKind>)(
368
+ "says community with no license to a %s",
369
+ async (caller: CallerKind) => {
370
+ setCaller(caller);
371
+
372
+ const body: JSONObject = await callGet();
373
+
374
+ expect(body["edition"]).toBe("community");
375
+ expect(body["status"]).toBeNull();
376
+ expect(body["verification"]).toBeNull();
377
+ expect(body["licenseValid"]).toBe(false);
378
+ expect(body["companyName"]).toBeNull();
379
+ expect(body["expiresAt"]).toBeNull();
380
+ },
381
+ );
382
+
383
+ /*
384
+ * The Community Edition dialog still has a version card: which build is
385
+ * running and whether a newer one exists is a property of the
386
+ * installation, not of a license.
387
+ */
388
+ it("still gives a master admin the version and update-check fields", async () => {
389
+ setCaller("master admin");
390
+
391
+ const body: JSONObject = await callGet();
361
392
 
362
- await callRoute(LICENSE_ROUTE);
393
+ expect(body["currentVersion"]).toBe(AppVersion);
394
+ expect(body["latestVersion"]).toBe("99.0.0");
395
+ expect(body["latestVersionPublishedAt"]).toBe("2026-09-10T00:00:00.000Z");
396
+ expect(body["latestVersionCheckedAt"]).toBe("2026-09-11T00:00:00.000Z");
397
+ expect(typeof body["isUpdateAvailable"]).toBe("boolean");
398
+ expect(typeof body["isUpdateCheckDisabled"]).toBe("boolean");
399
+ });
363
400
 
364
- expect(nextError()).toBeInstanceOf(BadDataException);
365
- expect(API.post).not.toHaveBeenCalled();
401
+ /*
402
+ * A key or token left over from an earlier Enterprise image licenses
403
+ * nothing on the Community Edition, so it is not reported as if it did.
404
+ */
405
+ it("reports no license key, token, topology or seats even when old license columns remain", async () => {
406
+ setCaller("master admin");
407
+
408
+ const body: JSONObject = await callGet();
409
+
410
+ expect(body["licenseKey"]).toBeNull();
411
+ expect(body["token"]).toBeNull();
412
+ expect(body["activationMode"]).toBeNull();
413
+ expect(body["instances"]).toEqual([]);
414
+ expect(body["instanceId"]).toBeNull();
415
+ expect(body["userLimit"]).toBeNull();
416
+ expect(body["currentUserCount"]).toBeNull();
417
+ expect(body["isSeatLimitEnforced"]).toBe(false);
418
+ expect(body["canAddMoreUsers"]).toBe(true);
366
419
  });
367
420
 
368
- it("rejects a licence key that is only whitespace", async () => {
369
- mockRequest.body = { licenseKey: " " };
421
+ it("gives everybody else only the public fields and reads nothing", async () => {
422
+ setCaller("signed-in user");
370
423
 
371
- await callRoute(LICENSE_ROUTE);
424
+ const body: JSONObject = await callGet();
372
425
 
373
- expect(nextError()).toBeInstanceOf(BadDataException);
426
+ expect(Object.keys(body).sort()).toEqual(PUBLIC_KEYS);
427
+ expect(GlobalConfigService.findOneById).not.toHaveBeenCalled();
428
+ });
429
+ });
430
+
431
+ describe("on the Enterprise Edition - what each caller gets", () => {
432
+ beforeEach(() => {
433
+ installWithSnapshot(createLicenseSnapshot({ userLimit: 50 }));
374
434
  });
375
435
 
376
- it("validates the supplied key against oneuptime.com", async () => {
377
- mockRequest.body = { licenseKey: " a-new-key " };
436
+ it.each(["anonymous", "signed-in user"] as Array<CallerKind>)(
437
+ "gives a %s exactly the public fields",
438
+ async (caller: CallerKind) => {
439
+ setCaller(caller);
378
440
 
379
- await callRoute(LICENSE_ROUTE);
441
+ const body: JSONObject = await callGet();
380
442
 
381
- const sent: JSONObject = (
382
- (API.post as unknown as jest.Mock).mock.calls[0]![0] as Record<
383
- string,
384
- JSONObject
385
- >
386
- )["data"] as JSONObject;
443
+ expect(Object.keys(body).sort()).toEqual(PUBLIC_KEYS);
387
444
 
388
- expect(sent["licenseKey"]).toBe("a-new-key");
389
- expect(sent["instanceId"]).toBe(INSTANCE_ID.toString());
445
+ for (const key of MASTER_ADMIN_ONLY_KEYS) {
446
+ expect(body).not.toHaveProperty(key);
447
+ }
448
+ },
449
+ );
450
+
451
+ /*
452
+ * The finding this closes: a signed-in (non-admin) user, or anybody whose
453
+ * token decodes to a userId, used to receive the license key, the token
454
+ * and every instance's host.
455
+ */
456
+ it("never gives a signed-in user who is not a master admin the key, the token or the topology", async () => {
457
+ setCaller("signed-in user");
458
+
459
+ const body: JSONObject = await callGet();
460
+ const serialized: string = JSON.stringify(body);
461
+
462
+ expect(serialized).not.toContain(STORED_LICENSE_KEY);
463
+ expect(serialized).not.toContain(STORED_TOKEN);
464
+ expect(serialized).not.toContain("prod.acme.internal");
465
+ expect(serialized).not.toContain(INSTANCE_ID.toString());
390
466
  });
391
467
 
392
- it("stores the seat limit the licence server reported", async () => {
393
- mockRequest.body = { licenseKey: STORED_LICENSE_KEY };
468
+ it("does not read the config row or count seats for anybody but a master admin", async () => {
469
+ const fake: FakeEnterpriseModule = installWithSnapshot(
470
+ createLicenseSnapshot(),
471
+ );
472
+ const seatUsageSpy: jest.SpyInstance = jest.spyOn(
473
+ fake.licensing,
474
+ "getSeatUsage",
475
+ );
394
476
 
395
- await callRoute(LICENSE_ROUTE);
477
+ setCaller("anonymous");
396
478
 
397
- expect(getStoredUpdate()["enterpriseLicenseUserLimit"]).toBe(150);
479
+ await callGet();
480
+
481
+ expect(GlobalConfigService.findOneById).not.toHaveBeenCalled();
482
+ expect(seatUsageSpy).not.toHaveBeenCalled();
483
+ });
484
+
485
+ it("gives a master admin the public fields plus everything needed to manage the license", async () => {
486
+ setCaller("master admin");
487
+
488
+ const body: JSONObject = await callGet();
489
+
490
+ expect(Object.keys(body).sort()).toEqual(
491
+ [...PUBLIC_KEYS, ...MASTER_ADMIN_ONLY_KEYS].sort(),
492
+ );
493
+ expect(body["licenseKey"]).toBe(STORED_LICENSE_KEY);
494
+ expect(body["token"]).toBe(STORED_TOKEN);
495
+ expect(body["instanceId"]).toBe(INSTANCE_ID.toString());
496
+ expect(body["instances"]).toHaveLength(1);
497
+ expect(body["currentUserCount"]).toBe(42);
498
+ expect(body["userCountUpdatedAt"]).toBe("2026-09-01T00:00:00.000Z");
499
+ expect(body["currentVersion"]).toBe(AppVersion);
500
+ expect(body["activationMode"]).toBe("online");
501
+ });
502
+
503
+ it("reads the row with exactly the columns the response needs, as root", async () => {
504
+ setCaller("master admin");
505
+
506
+ await callGet();
507
+
508
+ const call: Record<string, unknown> = (
509
+ GlobalConfigService.findOneById as unknown as jest.Mock
510
+ ).mock.calls[0]![0] as Record<string, unknown>;
511
+
512
+ expect(call["select"]).toEqual(LICENSE_RESPONSE_CONFIG_SELECT);
513
+ expect((call["props"] as Record<string, unknown>)["isRoot"]).toBe(true);
398
514
  });
399
- });
400
515
 
401
- describe("POST /global-config/license/refresh", () => {
402
516
  /*
403
- * The whole point of the route: no key in the body. A refresh that
404
- * accepted one would be an activation with a friendlier name, and a
405
- * mistyped key would be able to replace a working licence by accident.
517
+ * A verified license's limits come from its signed claims; the column is
518
+ * only a mirror a master admin could once overwrite. The snapshot is the
519
+ * truth, so the response reports its limit, not the column's 999.
406
520
  */
407
- it("refreshes using the stored key and ignores anything in the body", async () => {
408
- mockRequest.body = { licenseKey: "somebody-elses-key" };
521
+ it("reports the seat limit from the license snapshot, not the stored column", async () => {
522
+ setCaller("master admin");
523
+
524
+ const body: JSONObject = await callGet();
525
+
526
+ expect(body["userLimit"]).toBe(50);
527
+ });
528
+
529
+ it("reports the seat enforcement the license client computed", async () => {
530
+ setCaller("master admin");
531
+
532
+ const body: JSONObject = await callGet();
533
+
534
+ expect(body["isSeatLimitEnforced"]).toBe(true);
535
+ expect(body["seatsInUse"]).toBe(49);
536
+ expect(body["seatsRemaining"]).toBe(1);
537
+ expect(body["canAddMoreUsers"]).toBe(true);
538
+ });
409
539
 
410
- await callRoute(REFRESH_ROUTE);
540
+ it("says the limit is not enforced when the license client reports no usage", async () => {
541
+ installWithSnapshot(createLicenseSnapshot(), null);
542
+ setCaller("master admin");
411
543
 
412
- const sent: JSONObject = (
413
- (API.post as unknown as jest.Mock).mock.calls[0]![0] as Record<
414
- string,
415
- JSONObject
416
- >
417
- )["data"] as JSONObject;
544
+ const body: JSONObject = await callGet();
418
545
 
419
- expect(sent["licenseKey"]).toBe(STORED_LICENSE_KEY);
546
+ expect(body["isSeatLimitEnforced"]).toBe(false);
547
+ expect(body["seatsInUse"]).toBeNull();
548
+ expect(body["seatsRemaining"]).toBeNull();
549
+ expect(body["canAddMoreUsers"]).toBe(true);
420
550
  });
421
551
 
422
- it("refuses to refresh an installation that has no licence key yet", async () => {
552
+ it("calls an installation with a token but no key offline-activated", async () => {
423
553
  GlobalConfigService.findOneById = jest
424
554
  .fn()
425
555
  .mockResolvedValue(
426
556
  makeStoredConfig({ enterpriseLicenseKey: undefined }),
427
557
  );
558
+ setCaller("master admin");
559
+
560
+ const body: JSONObject = await callGet();
561
+
562
+ expect(body["activationMode"]).toBe("offline");
563
+ expect(body["licenseKey"]).toBeNull();
564
+ });
428
565
 
429
- await callRoute(REFRESH_ROUTE);
566
+ it("has no activation mode when there is no token at all", async () => {
567
+ GlobalConfigService.findOneById = jest.fn().mockResolvedValue(
568
+ makeStoredConfig({
569
+ enterpriseLicenseToken: undefined,
570
+ }),
571
+ );
572
+ setCaller("master admin");
573
+
574
+ const body: JSONObject = await callGet();
430
575
 
431
- expect(nextError()).toBeInstanceOf(BadDataException);
432
- expect(API.post).not.toHaveBeenCalled();
576
+ expect(body["activationMode"]).toBeNull();
433
577
  });
434
578
 
435
- it("refuses to refresh when there is no config row at all", async () => {
579
+ it("survives a missing config row", async () => {
436
580
  GlobalConfigService.findOneById = jest.fn().mockResolvedValue(null);
581
+ setCaller("master admin");
437
582
 
438
- await callRoute(REFRESH_ROUTE);
583
+ const body: JSONObject = await callGet();
439
584
 
440
- expect(nextError()).toBeInstanceOf(BadDataException);
441
- expect(API.post).not.toHaveBeenCalled();
585
+ expect(body["licenseKey"]).toBeNull();
586
+ expect(body["instances"]).toEqual([]);
587
+ expect(body["latestVersion"]).toBeNull();
588
+ expect(body["edition"]).toBe("enterprise");
442
589
  });
590
+ });
443
591
 
592
+ describe("on the Enterprise Edition - what each license status looks like", () => {
444
593
  /*
445
- * The reason the button exists. The customer raised the limit on
446
- * oneuptime.com; the stored 50 is what this installation is refusing users
447
- * against until something writes the new number down.
594
+ * licenseValid is the pill's verdict. Grace must read as valid: nothing
595
+ * has changed yet, and telling an administrator the license is gone while
596
+ * everything still works would be a false alarm.
448
597
  */
449
- it("applies a seat limit that has been raised on oneuptime.com", async () => {
450
- (API.post as unknown as jest.Mock).mockResolvedValue(
451
- new HTTPResponse<JSONObject>(
452
- 200,
453
- licenseServerPayload({ userLimit: 500 }),
454
- {},
455
- ),
598
+ it.each<[EnterpriseLicenseStatus, boolean]>([
599
+ ["valid", true],
600
+ ["grace", true],
601
+ ["expired", false],
602
+ ["missing", false],
603
+ ["invalid", false],
604
+ ])(
605
+ "status %s reads as licenseValid=%s to everybody",
606
+ async (status: EnterpriseLicenseStatus, expected: boolean) => {
607
+ installWithSnapshot(createLicenseSnapshotWithStatus(status));
608
+
609
+ for (const caller of [
610
+ "anonymous",
611
+ "signed-in user",
612
+ "master admin",
613
+ ] as Array<CallerKind>) {
614
+ (Response.sendJsonObjectResponse as unknown as jest.Mock).mockClear();
615
+ setCaller(caller);
616
+
617
+ const body: JSONObject = await callGet();
618
+
619
+ expect(body["edition"]).toBe("enterprise");
620
+ expect(body["status"]).toBe(status);
621
+ expect(body["licenseValid"]).toBe(expected);
622
+ }
623
+ },
624
+ );
625
+
626
+ it("tells everybody when an expired license's grace period ends", async () => {
627
+ const graceEndsAt: Date = new Date(Date.now() + 5 * DAY_IN_MS);
628
+ const expiresAt: Date = new Date(Date.now() - 9 * DAY_IN_MS);
629
+
630
+ installWithSnapshot(
631
+ createLicenseSnapshotWithStatus("grace", {
632
+ graceReason: "expired",
633
+ graceEndsAt,
634
+ expiresAt,
635
+ }),
456
636
  );
637
+ setCaller("anonymous");
457
638
 
458
- await callRoute(REFRESH_ROUTE);
639
+ const body: JSONObject = await callGet();
459
640
 
460
- expect(getStoredUpdate()["enterpriseLicenseUserLimit"]).toBe(500);
461
- expect(getResponseBody()["userLimit"]).toBe(500);
641
+ expect(body["graceReason"]).toBe("expired");
642
+ expect(body["graceEndsAt"]).toBe(graceEndsAt.toISOString());
643
+ expect(body["expiresAt"]).toBe(expiresAt.toISOString());
462
644
  });
463
645
 
464
- it("applies a seat limit that has been lowered on oneuptime.com", async () => {
465
- (API.post as unknown as jest.Mock).mockResolvedValue(
466
- new HTTPResponse<JSONObject>(
467
- 200,
468
- licenseServerPayload({ userLimit: 5 }),
469
- {},
470
- ),
471
- );
646
+ it("reports an unlicensed installation inside its first-seen grace as a trial", async () => {
647
+ const graceEndsAt: Date = new Date(Date.now() + 10 * DAY_IN_MS);
648
+
649
+ installWithSnapshot({
650
+ status: "grace",
651
+ verification: "none",
652
+ graceReason: "unlicensed",
653
+ graceEndsAt,
654
+ userLimit: null,
655
+ isEvaluation: false,
656
+ features: "all",
657
+ });
658
+ setCaller("signed-in user");
472
659
 
473
- await callRoute(REFRESH_ROUTE);
660
+ const body: JSONObject = await callGet();
474
661
 
475
- expect(getStoredUpdate()["enterpriseLicenseUserLimit"]).toBe(5);
662
+ expect(body["status"]).toBe("grace");
663
+ expect(body["graceReason"]).toBe("unlicensed");
664
+ expect(body["verification"]).toBe("none");
665
+ expect(body["licenseValid"]).toBe(true);
666
+ expect(body["graceEndsAt"]).toBe(graceEndsAt.toISOString());
667
+ expect(body["companyName"]).toBeNull();
476
668
  });
477
669
 
478
- it("clears the seat limit when the licence no longer carries one", async () => {
479
- (API.post as unknown as jest.Mock).mockResolvedValue(
480
- new HTTPResponse<JSONObject>(
481
- 200,
482
- licenseServerPayload({ userLimit: null }),
483
- {},
484
- ),
670
+ it("reports an unverified legacy license as valid but unverified", async () => {
671
+ installWithSnapshot(
672
+ createLicenseSnapshot({ verification: "unverified" }),
485
673
  );
674
+ setCaller("anonymous");
675
+
676
+ const body: JSONObject = await callGet();
677
+
678
+ expect(body["licenseValid"]).toBe(true);
679
+ expect(body["verification"]).toBe("unverified");
680
+ });
681
+
682
+ it("reports the evaluation flag under both its names", async () => {
683
+ installWithSnapshot(createLicenseSnapshot({ isEvaluation: true }));
684
+ setCaller("anonymous");
486
685
 
487
- await callRoute(REFRESH_ROUTE);
686
+ const body: JSONObject = await callGet();
488
687
 
489
- expect(getStoredUpdate()["enterpriseLicenseUserLimit"]).toBeNull();
688
+ expect(body["isEvaluation"]).toBe(true);
689
+ expect(body["isEvaluationLicense"]).toBe(true);
490
690
  });
491
691
 
492
- it("refreshes the expiry as well as the seat limit", async () => {
493
- (API.post as unknown as jest.Mock).mockResolvedValue(
494
- new HTTPResponse<JSONObject>(
495
- 200,
496
- licenseServerPayload({ expiresAt: "2031-06-01T00:00:00.000Z" }),
497
- {},
498
- ),
692
+ it("explains an invalid license to a master admin only", async () => {
693
+ installWithSnapshot(
694
+ createLicenseSnapshotWithStatus("invalid", {
695
+ message: "The license is bound to a different OneUptime instance.",
696
+ }),
499
697
  );
500
698
 
501
- await callRoute(REFRESH_ROUTE);
699
+ setCaller("master admin");
700
+ expect((await callGet())["message"]).toBe(
701
+ "The license is bound to a different OneUptime instance.",
702
+ );
502
703
 
503
- expect(
504
- (
505
- getStoredUpdate()["enterpriseLicenseExpiresAt"] as unknown as Date
506
- ).toISOString(),
507
- ).toBe("2031-06-01T00:00:00.000Z");
704
+ (Response.sendJsonObjectResponse as unknown as jest.Mock).mockClear();
705
+ setCaller("signed-in user");
706
+ expect(await callGet()).not.toHaveProperty("message");
508
707
  });
509
708
 
510
709
  /*
511
- * An installation that cannot reach oneuptime.com must be told so rather
512
- * than quietly keeping the old terms and reporting success — the
513
- * administrator pressed this button precisely because they believe the old
514
- * terms are wrong.
710
+ * The snapshot could not be read at all (the facade falls back to the
711
+ * cached snapshot, and there is none yet). The answer is "not licensed",
712
+ * never an error page on the login screen.
515
713
  */
516
- it("surfaces a failure from the licence server instead of storing anything", async () => {
517
- (API.post as unknown as jest.Mock).mockResolvedValue(
518
- new HTTPErrorResponse(500, { message: "License key is invalid" }, {}),
519
- );
714
+ it("reads as not licensed when no snapshot is available", async () => {
715
+ const fake: FakeEnterpriseModule = installWithSnapshot(null);
716
+ fake.licensing.getSnapshotError = new Error("database is down");
717
+ setCaller("anonymous");
520
718
 
521
- await callRoute(REFRESH_ROUTE);
719
+ const body: JSONObject = await callGet();
522
720
 
523
- const error: Error = nextError();
524
-
525
- expect(error).toBeInstanceOf(BadDataException);
526
- expect(error.message).toBe("License key is invalid");
527
- expect(GlobalConfigService.updateOneById).not.toHaveBeenCalled();
721
+ expect(body["edition"]).toBe("enterprise");
722
+ expect(body["status"]).toBeNull();
723
+ expect(body["licenseValid"]).toBe(false);
528
724
  });
529
725
 
530
- it("does not store anything when the returned expiry is not a date", async () => {
531
- (API.post as unknown as jest.Mock).mockResolvedValue(
532
- new HTTPResponse<JSONObject>(
533
- 200,
534
- licenseServerPayload({ expiresAt: "the-first-of-never" }),
535
- {},
536
- ),
726
+ it("asks the facade for the snapshot rather than reading license columns itself", async () => {
727
+ installWithSnapshot(createLicenseSnapshot());
728
+ const snapshotSpy: jest.SpyInstance = jest.spyOn(
729
+ EnterpriseEdition,
730
+ "getLicenseSnapshot",
537
731
  );
732
+ setCaller("anonymous");
538
733
 
539
- await callRoute(REFRESH_ROUTE);
734
+ await callGet();
540
735
 
541
- expect(nextError()).toBeInstanceOf(BadDataException);
542
- expect(GlobalConfigService.updateOneById).not.toHaveBeenCalled();
736
+ expect(snapshotSpy).toHaveBeenCalledTimes(1);
543
737
  });
544
738
  });
545
739
 
546
- describe("the seat enforcement the response reports", () => {
547
- it("reports the seats in use against the freshly refreshed limit", async () => {
548
- UserService.countBy = jest.fn().mockResolvedValue(new PositiveNumber(42));
549
-
550
- await callRoute(REFRESH_ROUTE);
740
+ /*
741
+ * THE EXPIRED-SESSION CASE
742
+ *
743
+ * The dashboard's access-token cookie expires with the JWT inside it, so a
744
+ * signed-in tab left idle past the token lifetime asks for the licence
745
+ * with no session at all. On its own this route cannot tell that apart
746
+ * from the login page, and answers with the reduced anonymous payload and
747
+ * a 200 - which the edition pill reads as "no licence, no instances". The
748
+ * dashboards now say `?signedIn=true`; a caller that says so but carries
749
+ * no credentials is answered 401, the one status the browser client
750
+ * refreshes the session and replays on.
751
+ */
752
+ describe("?signedIn=true", () => {
753
+ const setRequest: (fields: Record<string, unknown>) => void = (
754
+ fields: Record<string, unknown>,
755
+ ): void => {
756
+ Object.assign(mockRequest as unknown as Record<string, unknown>, fields);
757
+ };
551
758
 
552
- const body: JSONObject = getResponseBody();
759
+ const callGetExpectingError: () => Promise<Error> =
760
+ async (): Promise<Error> => {
761
+ await mockRouter
762
+ .match("get", LICENSE_ROUTE)
763
+ .handlerFunction(mockRequest, mockResponse, nextFunction);
553
764
 
554
- expect(body["isSeatLimitEnforced"]).toBe(true);
555
- expect(body["seatsInUse"]).toBe(42);
556
- expect(body["seatsRemaining"]).toBe(108);
557
- expect(body["canAddMoreUsers"]).toBe(true);
558
- });
765
+ expect(nextFunction).toHaveBeenCalledTimes(1);
559
766
 
560
- /*
561
- * The live count is what enforcement uses, and it is allowed to be higher
562
- * than the licence-wide figure oneuptime.com last computed — that figure is
563
- * up to a day old.
564
- */
565
- it("prefers the live user count over the licence server's stale one", async () => {
566
- UserService.countBy = jest
567
- .fn()
568
- .mockResolvedValue(new PositiveNumber(150));
767
+ return (nextFunction as unknown as jest.Mock).mock
768
+ .calls[0]![0] as Error;
769
+ };
569
770
 
570
- await callRoute(REFRESH_ROUTE);
771
+ it.each([
772
+ ["no userType at all", {}],
773
+ [
774
+ "userType Public (what getUserMiddleware stamps)",
775
+ { userType: UserType.Public },
776
+ ],
777
+ ])(
778
+ "answers an anonymous caller (%s) with a 401 before reading anything",
779
+ async (_label: string, caller: Record<string, unknown>) => {
780
+ installWithSnapshot(createLicenseSnapshot());
781
+ const snapshotSpy: jest.SpyInstance = jest.spyOn(
782
+ EnterpriseEdition,
783
+ "getLicenseSnapshot",
784
+ );
571
785
 
572
- const body: JSONObject = getResponseBody();
786
+ setRequest({ ...caller, query: { signedIn: "true" } });
573
787
 
574
- expect(body["seatsInUse"]).toBe(150);
575
- expect(body["seatsRemaining"]).toBe(0);
576
- expect(body["canAddMoreUsers"]).toBe(false);
577
- });
788
+ const error: Error = await callGetExpectingError();
578
789
 
579
- it("says the limit is not enforced when the licence has none", async () => {
580
- (API.post as unknown as jest.Mock).mockResolvedValue(
581
- new HTTPResponse<JSONObject>(
582
- 200,
583
- licenseServerPayload({ userLimit: null }),
584
- {},
585
- ),
586
- );
790
+ expect(error).toBeInstanceOf(NotAuthenticatedException);
791
+ expect((error as Exception).code).toBe(401);
792
+ expect(error.message).toBe(
793
+ UserMiddleware.AUTHENTICATION_REQUIRED_MESSAGE,
794
+ );
795
+ expect(snapshotSpy).not.toHaveBeenCalled();
796
+ expect(GlobalConfigService.findOneById).not.toHaveBeenCalled();
797
+ expect(Response.sendJsonObjectResponse).not.toHaveBeenCalled();
798
+ },
799
+ );
587
800
 
588
- await callRoute(REFRESH_ROUTE);
801
+ it("answers the 401 on the Community Edition too", async () => {
802
+ setRequest({ userType: UserType.Public, query: { signedIn: "true" } });
589
803
 
590
- const body: JSONObject = getResponseBody();
804
+ const error: Error = await callGetExpectingError();
591
805
 
592
- expect(body["isSeatLimitEnforced"]).toBe(false);
593
- expect(body["seatsInUse"]).toBeNull();
594
- expect(body["canAddMoreUsers"]).toBe(true);
806
+ expect(error).toBeInstanceOf(NotAuthenticatedException);
807
+ expect(Response.sendJsonObjectResponse).not.toHaveBeenCalled();
595
808
  });
596
809
 
597
- it("says the limit is not enforced on Community Edition", async () => {
598
- setDeploymentFlag(ENTERPRISE_FLAG_KEY, false);
810
+ // The login page does not send the flag and must keep working.
811
+ it("still serves an anonymous caller without the flag the public payload", async () => {
812
+ installWithSnapshot(createLicenseSnapshot());
813
+ setRequest({ userType: UserType.Public, query: {} });
599
814
 
600
- await callRoute(REFRESH_ROUTE);
815
+ const body: JSONObject = await callGet();
601
816
 
602
- const body: JSONObject = getResponseBody();
603
-
604
- expect(body["isSeatLimitEnforced"]).toBe(false);
605
- expect(body["canAddMoreUsers"]).toBe(true);
606
- expect(UserService.countBy).not.toHaveBeenCalled();
817
+ expect(Object.keys(body).sort()).toEqual(PUBLIC_KEYS);
818
+ expect(body["licenseKey"]).toBeUndefined();
607
819
  });
608
- });
609
820
 
610
- describe("GET /global-config/license", () => {
611
- type CallGetFunction = () => Promise<void>;
821
+ it.each([
822
+ ["signedIn=false", { signedIn: "false" }],
823
+ ["an empty signedIn", { signedIn: "" }],
824
+ ["signedIn=1", { signedIn: "1" }],
825
+ ])(
826
+ 'only the exact string "true" asks for a 401 (%s serves the public payload)',
827
+ async (_label: string, query: Record<string, unknown>) => {
828
+ setRequest({ userType: UserType.Public, query: query });
612
829
 
613
- const callGet: CallGetFunction = async (): Promise<void> => {
614
- await mockRouter
615
- .match("get", LICENSE_ROUTE)
616
- .handlerFunction(mockRequest, mockResponse, nextFunction);
617
- };
830
+ const body: JSONObject = await callGet();
618
831
 
619
- beforeEach(() => {
620
- GlobalConfigService.findOneById = jest.fn().mockResolvedValue(
621
- makeStoredConfig({
622
- enterpriseLicenseUserLimit: 150,
623
- enterpriseLicenseCurrentUserCount: 42,
624
- enterpriseLicenseInstances: [],
625
- }),
626
- );
627
- });
832
+ expect(Object.keys(body).sort()).toEqual(PUBLIC_KEYS);
833
+ },
834
+ );
628
835
 
629
- it("tells a signed-in administrator how close the installation is to refusing users", async () => {
630
- (mockRequest as unknown as Record<string, unknown>)["userAuthorization"] =
631
- {
632
- userId: ObjectID.generate(),
633
- };
634
- UserService.countBy = jest
635
- .fn()
636
- .mockResolvedValue(new PositiveNumber(149));
836
+ it("serves a signed-in master admin the full payload with the flag", async () => {
837
+ installWithSnapshot(createLicenseSnapshot({ userLimit: 50 }));
838
+ setCaller("master admin");
839
+ setRequest({ userType: UserType.User, query: { signedIn: "true" } });
637
840
 
638
- await callGet();
639
-
640
- const body: JSONObject = getResponseBody();
841
+ const body: JSONObject = await callGet();
641
842
 
642
- expect(body["isSeatLimitEnforced"]).toBe(true);
643
- expect(body["seatsInUse"]).toBe(149);
644
- expect(body["seatsRemaining"]).toBe(1);
645
- expect(body["canAddMoreUsers"]).toBe(true);
843
+ expect(body["licenseKey"]).toBe(STORED_LICENSE_KEY);
844
+ expect(body["instanceId"]).toBe(INSTANCE_ID.toString());
646
845
  });
647
846
 
648
- /*
649
- * The same route serves the signed-out login page. How near this server is
650
- * to refusing new accounts is operational detail, and counting its users
651
- * for an anonymous visitor would be a free query on an unauthenticated
652
- * endpoint besides.
653
- */
654
- it("tells an anonymous visitor nothing about seat enforcement", async () => {
655
- await callGet();
847
+ it("serves a signed-in user who is not a master admin the public payload with the flag", async () => {
848
+ installWithSnapshot(createLicenseSnapshot());
849
+ setCaller("signed-in user");
850
+ setRequest({ userType: UserType.User, query: { signedIn: "true" } });
656
851
 
657
- const body: JSONObject = getResponseBody();
852
+ const body: JSONObject = await callGet();
658
853
 
659
- expect(body["isSeatLimitEnforced"]).toBe(false);
660
- expect(body["seatsInUse"]).toBeNull();
661
- expect(body["seatsRemaining"]).toBeNull();
662
- expect(body["canAddMoreUsers"]).toBe(true);
663
- expect(UserService.countBy).not.toHaveBeenCalled();
854
+ expect(Object.keys(body).sort()).toEqual(PUBLIC_KEYS);
664
855
  });
665
856
 
666
857
  /*
667
- * THE EXPIRED-SESSION CASE
668
- *
669
- * The dashboard's access-token cookie expires with the JWT inside it, so a
670
- * signed-in tab left idle past the token lifetime asks for the licence
671
- * with no session at all. On its own this route cannot tell that apart
672
- * from the login page, and answers with the reduced anonymous payload and
673
- * a 200 - which the edition pill reads as "no licence, no instances". The
674
- * dashboards now say `?signedIn=true`; a caller that says so but carries
675
- * no credentials is answered 401, the one status the browser client
676
- * refreshes the session and replays on.
858
+ * A project API key has credentials, so it is not an expired session:
859
+ * no 401. It has no user either, so it still gets only what an
860
+ * anonymous caller gets.
677
861
  */
678
- describe("?signedIn=true", () => {
679
- type SetCallerFunction = (caller: Record<string, unknown>) => void;
680
-
681
- const setCaller: SetCallerFunction = (
682
- caller: Record<string, unknown>,
683
- ): void => {
684
- Object.assign(
685
- mockRequest as unknown as Record<string, unknown>,
686
- caller,
687
- );
688
- };
862
+ it("does not 401 an API-key caller that sends the flag", async () => {
863
+ installWithSnapshot(createLicenseSnapshot());
864
+ setRequest({ userType: UserType.API, query: { signedIn: "true" } });
689
865
 
690
- it.each([
691
- ["no userType at all", {}],
692
- [
693
- "userType Public (what getUserMiddleware stamps)",
694
- { userType: UserType.Public },
695
- ],
696
- ])(
697
- "answers an anonymous caller (%s) with a 401 before reading the config",
698
- async (_label: string, caller: Record<string, unknown>) => {
699
- setCaller({ ...caller, query: { signedIn: "true" } });
700
-
701
- await callGet();
702
-
703
- const error: Error = nextError();
704
-
705
- expect(error).toBeInstanceOf(NotAuthenticatedException);
706
- expect((error as Exception).code).toBe(401);
707
- expect(error.message).toBe(
708
- UserMiddleware.AUTHENTICATION_REQUIRED_MESSAGE,
709
- );
710
- expect(GlobalConfigService.findOneById).not.toHaveBeenCalled();
711
- expect(UserService.countBy).not.toHaveBeenCalled();
712
- expect(Response.sendJsonObjectResponse).not.toHaveBeenCalled();
713
- },
714
- );
866
+ const body: JSONObject = await callGet();
715
867
 
716
- // The login page does not send the flag and must keep working.
717
- it("still serves an anonymous caller without the flag the reduced payload", async () => {
718
- setCaller({ userType: UserType.Public, query: {} });
868
+ expect(Object.keys(body).sort()).toEqual(PUBLIC_KEYS);
869
+ });
870
+ });
719
871
 
720
- await callGet();
872
+ describe("errors", () => {
873
+ it("passes a database failure to next instead of answering", async () => {
874
+ GlobalConfigService.findOneById = jest
875
+ .fn()
876
+ .mockRejectedValue(new Error("database is down"));
877
+ setCaller("master admin");
721
878
 
722
- expect(nextFunction).not.toHaveBeenCalled();
723
- expect(GlobalConfigService.findOneById).toHaveBeenCalledTimes(1);
879
+ await mockRouter
880
+ .match("get", LICENSE_ROUTE)
881
+ .handlerFunction(mockRequest, mockResponse, nextFunction);
724
882
 
725
- const body: JSONObject = getResponseBody();
883
+ expect(nextFunction).toHaveBeenCalledTimes(1);
884
+ expect(Response.sendJsonObjectResponse).not.toHaveBeenCalled();
885
+ });
886
+ });
887
+ });
726
888
 
727
- expect(body["licenseKey"]).toBeNull();
728
- expect(body["token"]).toBeNull();
729
- expect(body["instanceId"]).toBeNull();
730
- expect(body["instances"]).toEqual([]);
731
- expect(body["currentVersion"]).toBeNull();
732
- expect(body["seatsInUse"]).toBeNull();
733
- });
889
+ describe("GlobalConfigAPI.buildLicenseResponse", () => {
890
+ it("ignores a snapshot handed to it when the Enterprise Edition is not loaded", () => {
891
+ const body: JSONObject = GlobalConfigAPI.buildLicenseResponse({
892
+ audience: "master-admin",
893
+ isEnterpriseEditionLoaded: false,
894
+ snapshot: createLicenseSnapshot(),
895
+ config: makeStoredConfig(),
896
+ seatUsage: ENFORCED_SEAT_USAGE,
897
+ });
734
898
 
735
- it.each([
736
- ["signedIn=false", { signedIn: "false" }],
737
- ["an empty signedIn", { signedIn: "" }],
738
- ["signedIn=1", { signedIn: "1" }],
739
- ])(
740
- 'only the exact string "true" asks for a 401 (%s serves the reduced payload)',
741
- async (_label: string, query: Record<string, unknown>) => {
742
- setCaller({ userType: UserType.Public, query: query });
899
+ expect(body["edition"]).toBe("community");
900
+ expect(body["licenseValid"]).toBe(false);
901
+ expect(body["licenseKey"]).toBeNull();
902
+ expect(body["isSeatLimitEnforced"]).toBe(false);
903
+ });
743
904
 
744
- await callGet();
905
+ it("has the same keys for every audience whatever the license state", () => {
906
+ const statuses: Array<EnterpriseLicenseStatus> = [
907
+ "valid",
908
+ "grace",
909
+ "expired",
910
+ "missing",
911
+ "invalid",
912
+ ];
913
+
914
+ for (const status of statuses) {
915
+ const publicBody: JSONObject = GlobalConfigAPI.buildLicenseResponse({
916
+ audience: "public",
917
+ isEnterpriseEditionLoaded: true,
918
+ snapshot: createLicenseSnapshotWithStatus(status),
919
+ config: makeStoredConfig(),
920
+ seatUsage: ENFORCED_SEAT_USAGE,
921
+ });
922
+ const adminBody: JSONObject = GlobalConfigAPI.buildLicenseResponse({
923
+ audience: "master-admin",
924
+ isEnterpriseEditionLoaded: true,
925
+ snapshot: createLicenseSnapshotWithStatus(status),
926
+ config: makeStoredConfig(),
927
+ seatUsage: ENFORCED_SEAT_USAGE,
928
+ });
745
929
 
746
- expect(nextFunction).not.toHaveBeenCalled();
747
- expect(getResponseBody()["licenseKey"]).toBeNull();
748
- },
930
+ expect(Object.keys(publicBody).sort()).toEqual(PUBLIC_KEYS);
931
+ expect(Object.keys(adminBody).sort()).toEqual(
932
+ [...PUBLIC_KEYS, ...MASTER_ADMIN_ONLY_KEYS].sort(),
749
933
  );
934
+ }
935
+ });
750
936
 
751
- it("serves a signed-in user the full payload with the flag", async () => {
752
- setCaller({
753
- userType: UserType.User,
754
- userAuthorization: { userId: ObjectID.generate() },
755
- query: { signedIn: "true" },
756
- });
937
+ describe("features", () => {
938
+ const buildPublic: (
939
+ snapshot: EnterpriseLicenseSnapshot | null,
940
+ isEnterpriseEditionLoaded?: boolean,
941
+ ) => JSONObject = (
942
+ snapshot: EnterpriseLicenseSnapshot | null,
943
+ isEnterpriseEditionLoaded?: boolean,
944
+ ): JSONObject => {
945
+ return GlobalConfigAPI.buildLicenseResponse({
946
+ audience: "public",
947
+ isEnterpriseEditionLoaded: isEnterpriseEditionLoaded !== false,
948
+ snapshot,
949
+ config: null,
950
+ seatUsage: null,
951
+ });
952
+ };
757
953
 
758
- await callGet();
954
+ it('is "all" for a license that covers everything', () => {
955
+ expect(buildPublic(createLicenseSnapshot())["features"]).toBe("all");
956
+ });
759
957
 
760
- expect(nextFunction).not.toHaveBeenCalled();
958
+ /*
959
+ * A valid license that leaves SSO out stops SSO (isFeatureActive), and
960
+ * licenseValid alone cannot tell a settings page that.
961
+ */
962
+ it("lists the features of a license that names them, so a page can tell one is left out", () => {
963
+ const body: JSONObject = buildPublic(
964
+ createLicenseSnapshot({
965
+ features: [EnterpriseFeature.SCIM, EnterpriseFeature.AuditLogs],
966
+ }),
967
+ );
761
968
 
762
- const body: JSONObject = getResponseBody();
969
+ expect(body["licenseValid"]).toBe(true);
970
+ expect(body["features"]).toEqual(["scim", "audit-logs"]);
971
+ expect(body["features"]).not.toContain(EnterpriseFeature.SSO);
972
+ });
973
+
974
+ it("is an empty list when the license entitles nothing", () => {
975
+ expect(
976
+ buildPublic(createLicenseSnapshotWithStatus("missing"))["features"],
977
+ ).toEqual([]);
978
+ });
979
+
980
+ it("is null on the Community Edition, whatever snapshot is handed in", () => {
981
+ expect(buildPublic(null)["features"]).toBeNull();
982
+ expect(
983
+ buildPublic(createLicenseSnapshot(), false)["features"],
984
+ ).toBeNull();
985
+ });
763
986
 
764
- expect(body["licenseKey"]).toBe(STORED_LICENSE_KEY);
765
- expect(body["instanceId"]).toBe(INSTANCE_ID.toString());
766
- expect(body["seatsInUse"]).toBe(42);
987
+ it("hands out a copy, never the snapshot's own list", () => {
988
+ const snapshot: EnterpriseLicenseSnapshot = createLicenseSnapshot({
989
+ features: [EnterpriseFeature.SSO],
767
990
  });
991
+ const features: Array<string> = buildPublic(snapshot)[
992
+ "features"
993
+ ] as Array<string>;
768
994
 
769
- it("serves a signed-in user the same full payload without the flag", async () => {
770
- setCaller({
771
- userType: UserType.User,
772
- userAuthorization: { userId: ObjectID.generate() },
773
- query: {},
774
- });
995
+ features.push("mutated");
775
996
 
776
- await callGet();
997
+ expect(snapshot.features).toEqual([EnterpriseFeature.SSO]);
998
+ });
777
999
 
778
- expect(nextFunction).not.toHaveBeenCalled();
779
- expect(getResponseBody()["licenseKey"]).toBe(STORED_LICENSE_KEY);
1000
+ it("is the same for a master admin", () => {
1001
+ const body: JSONObject = GlobalConfigAPI.buildLicenseResponse({
1002
+ audience: "master-admin",
1003
+ isEnterpriseEditionLoaded: true,
1004
+ snapshot: createLicenseSnapshot({ features: [EnterpriseFeature.SSO] }),
1005
+ config: makeStoredConfig(),
1006
+ seatUsage: ENFORCED_SEAT_USAGE,
780
1007
  });
781
1008
 
782
- /*
783
- * A project API key has credentials, so it is not an expired session:
784
- * no 401. It has no user either, so it still gets only what an
785
- * anonymous caller gets.
786
- */
787
- it("does not 401 an API-key caller that sends the flag", async () => {
788
- setCaller({ userType: UserType.API, query: { signedIn: "true" } });
1009
+ expect(body["features"]).toEqual(["sso"]);
1010
+ });
1011
+ });
789
1012
 
790
- await callGet();
1013
+ it("maps unenforced seat usage to the 'nothing to enforce' fields", () => {
1014
+ expect(
1015
+ GlobalConfigAPI.getSeatUsageResponseFields({
1016
+ ...ENFORCED_SEAT_USAGE,
1017
+ isEnforced: false,
1018
+ }),
1019
+ ).toEqual({
1020
+ isSeatLimitEnforced: false,
1021
+ seatsInUse: null,
1022
+ seatsRemaining: null,
1023
+ canAddMoreUsers: true,
1024
+ });
1025
+ });
791
1026
 
792
- expect(nextFunction).not.toHaveBeenCalled();
793
- expect(getResponseBody()["licenseKey"]).toBeNull();
794
- });
1027
+ it("reports a full license as unable to take more users", () => {
1028
+ expect(
1029
+ GlobalConfigAPI.getSeatUsageResponseFields({
1030
+ ...ENFORCED_SEAT_USAGE,
1031
+ seatsInUse: 50,
1032
+ seatsRemaining: 0,
1033
+ hasSeatForNewUser: false,
1034
+ }),
1035
+ ).toEqual({
1036
+ isSeatLimitEnforced: true,
1037
+ seatsInUse: 50,
1038
+ seatsRemaining: 0,
1039
+ canAddMoreUsers: false,
795
1040
  });
796
1041
  });
797
1042
  });