@oneuptime/common 13.0.7 → 13.0.8

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 (463) hide show
  1. package/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.ts +1 -1
  2. package/Models/DatabaseModels/ProjectOnCallCalendarFeed.ts +1 -1
  3. package/Models/DatabaseModels/UserOnCallCalendarFeed.ts +1 -1
  4. package/Server/API/AIBillingAPI.ts +1 -0
  5. package/Server/API/AIInsightAPI.ts +1 -4
  6. package/Server/API/AIInvestigationAPI.ts +1 -4
  7. package/Server/API/AIReadinessAPI.ts +4 -5
  8. package/Server/API/AlertAPI.ts +1 -0
  9. package/Server/API/AutoRemediationAPI.ts +1 -4
  10. package/Server/API/BaseAPI.ts +13 -0
  11. package/Server/API/BaseAnalyticsAPI.ts +80 -9
  12. package/Server/API/BillingAPI.ts +2 -0
  13. package/Server/API/BillingInvoiceAPI.ts +1 -0
  14. package/Server/API/BillingPaymentMethodAPI.ts +2 -0
  15. package/Server/API/CodeFixRunAPI.ts +1 -3
  16. package/Server/API/CommonAPI.ts +68 -4
  17. package/Server/API/DashboardAPI.ts +12 -12
  18. package/Server/API/GlobalConfigAPI.ts +17 -0
  19. package/Server/API/IncidentAPI.ts +2 -0
  20. package/Server/API/IncidentEpisodeAPI.ts +1 -0
  21. package/Server/API/NotificationAPI.ts +1 -0
  22. package/Server/API/OnCallCalendarAPI.ts +4 -5
  23. package/Server/API/OnCallDutyPolicyAPI.ts +7 -0
  24. package/Server/API/OnCallReadinessAPI.ts +4 -1
  25. package/Server/API/OnCallScheduleTimelineAPI.ts +731 -0
  26. package/Server/API/ProjectAPI.ts +1 -0
  27. package/Server/API/ScheduledMaintenanceAPI.ts +1 -0
  28. package/Server/API/StatusPageAPI.ts +161 -32
  29. package/Server/API/TeamMemberAPI.ts +3 -0
  30. package/Server/API/UserCallAPI.ts +2 -0
  31. package/Server/API/UserEmailAPI.ts +2 -0
  32. package/Server/API/UserIncomingCallNumberAPI.ts +2 -0
  33. package/Server/API/UserMicrosoftTeamsAPI.ts +1 -0
  34. package/Server/API/UserSlackAPI.ts +1 -0
  35. package/Server/API/UserSmsAPI.ts +2 -0
  36. package/Server/API/UserTotpAuthAPI.ts +1 -0
  37. package/Server/API/UserWebAuthnAPI.ts +2 -0
  38. package/Server/API/UserWebhookAPI.ts +1 -0
  39. package/Server/API/UserWhatsAppAPI.ts +2 -0
  40. package/Server/Middleware/MasterAdminAuthorization.ts +13 -2
  41. package/Server/Middleware/UserAuthorization.ts +79 -5
  42. package/Server/Services/ApiKeyPermissionService.ts +101 -4
  43. package/Server/Services/DatabaseService.ts +245 -0
  44. package/Server/Services/DetectionRuleService.ts +5 -4
  45. package/Server/Services/OnCallDutyPolicyChildService.ts +261 -0
  46. package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +2 -2
  47. package/Server/Services/OnCallDutyPolicyEscalationRuleService.ts +2 -2
  48. package/Server/Services/OnCallDutyPolicyEscalationRuleTeamService.ts +2 -2
  49. package/Server/Services/OnCallDutyPolicyEscalationRuleUserService.ts +2 -2
  50. package/Server/Services/PayAsYouGoBillingService.ts +23 -28
  51. package/Server/Services/ProjectService.ts +18 -0
  52. package/Server/Services/RumSessionPinService.ts +274 -22
  53. package/Server/Services/TeamPermissionService.ts +120 -26
  54. package/Server/Services/ThreatIntelFeedService.ts +6 -3
  55. package/Server/Types/Database/Permissions/BasePermission.ts +9 -7
  56. package/Server/Types/Database/Permissions/Index.ts +75 -11
  57. package/Server/Types/Database/Permissions/TablePermission.ts +24 -28
  58. package/Server/Utils/AI/AIChatPrivacyFilter.ts +8 -0
  59. package/Server/Utils/AI/AIRunPrivacyFilter.ts +8 -0
  60. package/Server/Utils/MasterAdminPageAccess.ts +61 -0
  61. package/Server/Utils/Monitor/MonitorAlert.ts +3 -1
  62. package/Server/Utils/Monitor/MonitorIncident.ts +3 -1
  63. package/Server/Utils/OnCall/OnCallCalendarFeedRenderer.ts +1 -1
  64. package/Server/Utils/Realtime.ts +163 -45
  65. package/Server/Utils/SecurityEvent/DetectionRuleEvaluator.ts +11 -28
  66. package/Server/Utils/SecurityEvent/GoogleSecOpsSeverityRepair.ts +170 -0
  67. package/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.ts +1 -1
  68. package/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelFeedPoller.ts +6 -2
  69. package/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelMatcher.ts +10 -9
  70. package/Tests/App/Dashboard/AffectedResourcesPicker.test.tsx +1059 -0
  71. package/Tests/App/Dashboard/AffectedResourcesPickerEditForm.test.tsx +376 -0
  72. package/Tests/App/Dashboard/AffectedResourcesPickerHiddenTypes.test.tsx +757 -0
  73. package/Tests/App/Dashboard/AffectedResourcesSummaryStep.test.tsx +307 -0
  74. package/Tests/App/Dashboard/ArgumentsFormGroupByUnlistedAttributes.test.tsx +131 -0
  75. package/Tests/App/Dashboard/DashboardHeaderRequests.test.tsx +1 -1
  76. package/Tests/App/Dashboard/DashboardMonitorLabelVariable.test.tsx +9 -1
  77. package/Tests/App/Dashboard/DashboardVariableAllContract.test.tsx +24 -0
  78. package/Tests/App/Dashboard/DashboardVariableSelector.test.tsx +162 -8
  79. package/Tests/App/Dashboard/EntityFilterDropdownArchivedSlos.test.tsx +105 -6
  80. package/Tests/App/Dashboard/EntityFilterDropdownUnlistedSelection.test.tsx +434 -0
  81. package/Tests/App/Dashboard/EscalationRuleReadiness.test.tsx +276 -1
  82. package/Tests/App/Dashboard/OnCallCalendarFeedSideMenus.test.tsx +9 -1
  83. package/Tests/App/Dashboard/PayAsYouGoConsentGate.test.tsx +58 -42
  84. package/Tests/App/Dashboard/PayAsYouGoNotices.test.tsx +42 -5
  85. package/Tests/App/Dashboard/ResourceAuditLogsSideMenus.test.tsx +34 -23
  86. package/Tests/App/Dashboard/ScheduleTimeline.test.tsx +1580 -0
  87. package/Tests/App/Dashboard/ScheduleTimelineAPI.test.ts +280 -0
  88. package/Tests/App/Dashboard/ScheduleTimelineModel.test.ts +673 -0
  89. package/Tests/App/Dashboard/SecurityEventConnectionDiagnostics.test.tsx +95 -68
  90. package/Tests/App/Dashboard/SecurityEventsDetectionRulesPage.test.tsx +86 -3
  91. package/Tests/App/Dashboard/SecurityEventsEmptyState.test.tsx +28 -3
  92. package/Tests/App/Dashboard/SecurityEventsExplorer.test.tsx +761 -0
  93. package/Tests/App/Dashboard/SecurityEventsGuideContent.test.ts +1313 -0
  94. package/Tests/App/Dashboard/SecurityEventsHowItWorksCard.test.tsx +536 -0
  95. package/Tests/App/Dashboard/SecurityEventsNoResults.test.tsx +222 -0
  96. package/Tests/App/Dashboard/SecurityEventsThreatIntelPage.test.tsx +199 -0
  97. package/Tests/App/Dashboard/SecurityEventsVolumeChart.test.tsx +321 -0
  98. package/Tests/App/Dashboard/SloOverviewHero.test.tsx +171 -36
  99. package/Tests/App/Dashboard/SloViewSideMenu.test.tsx +215 -0
  100. package/Tests/App/Dashboard/SloViewSideMenuCounts.test.tsx +40 -3
  101. package/Tests/App/PublicDashboard/DashboardVariableSelector.test.tsx +12 -4
  102. package/Tests/App/PublicDashboard/PublicDashboardWidgetListAuth.test.tsx +623 -0
  103. package/Tests/Models/OnCallCalendarFeedModels.test.ts +23 -1
  104. package/Tests/Server/API/AIChatRouteAuthorization.test.ts +138 -5
  105. package/Tests/Server/API/AIGenerationProjectAIToggle.test.ts +187 -0
  106. package/Tests/Server/API/AIInvestigationAvailabilityAuthorization.test.ts +51 -3
  107. package/Tests/Server/API/AIInvestigationEvidenceAPI.test.ts +55 -2
  108. package/Tests/Server/API/BaseAnalyticsAPIAggregateCache.test.ts +729 -0
  109. package/Tests/Server/API/BillingPayAsYouGoStatus.test.ts +93 -7
  110. package/Tests/Server/API/BillingPaymentMethodAPI.test.ts +36 -3
  111. package/Tests/Server/API/CommonAPIAuthGuard.test.ts +1003 -24
  112. package/Tests/Server/API/DashboardPublicRateLimit.test.ts +6 -3
  113. package/Tests/Server/API/ExpiredSessionAIRoutes.test.ts +708 -0
  114. package/Tests/Server/API/ExpiredSessionCustomRoutes.test.ts +928 -0
  115. package/Tests/Server/API/ExpiredSessionMonitorTemplate.test.ts +319 -0
  116. package/Tests/Server/API/ExpiredSessionOnCallDutyPolicy.test.ts +336 -0
  117. package/Tests/Server/API/ExpiredSessionRouteGuards.test.ts +820 -0
  118. package/Tests/Server/API/GlobalConfigLicense.test.ts +140 -0
  119. package/Tests/Server/API/NotificationChannelVerificationAPI.test.ts +115 -12
  120. package/Tests/Server/API/OnCallCalendarAPI.test.ts +375 -8
  121. package/Tests/Server/API/OnCallReadinessAPI.test.ts +175 -17
  122. package/Tests/Server/API/OnCallScheduleTimelineAPI.test.ts +1513 -0
  123. package/Tests/Server/API/OnCallSetupReminderAPI.test.ts +119 -7
  124. package/Tests/Server/API/ProjectScopedCustomRouteAuth.test.ts +111 -4
  125. package/Tests/Server/API/PublicRouteInvalidAccessToken.test.ts +615 -0
  126. package/Tests/Server/API/SecurityEventAttributesAPI.test.ts +89 -2
  127. package/Tests/Server/API/SessionReplayAPI.test.ts +121 -0
  128. package/Tests/Server/API/StatusPageMasterPasswordRateLimit.test.ts +4 -2
  129. package/Tests/Server/API/StatusPagePostmortemVisibility.test.ts +441 -0
  130. package/Tests/Server/API/StatusPageTestEmailReportAuth.test.ts +664 -0
  131. package/Tests/Server/API/TeamMemberRemoveUserFromProjectAPI.test.ts +7 -0
  132. package/Tests/Server/API/UserIncomingCallNumberApi.test.ts +6 -0
  133. package/Tests/Server/API/UserNotificationSettingAPI.test.ts +65 -4
  134. package/Tests/Server/API/UserProjectsAPI.test.ts +63 -3
  135. package/Tests/Server/API/UserTotpAuthAPI.test.ts +48 -2
  136. package/Tests/Server/API/UserWebAuthnAuthenticationOptionsRoute.test.ts +11 -2
  137. package/Tests/Server/API/UserWebAuthnBackupCodeMinting.test.ts +46 -3
  138. package/Tests/Server/API/UserWebAuthnPasskeyAPI.test.ts +18 -2
  139. package/Tests/Server/API/ZZVerifyChatsAuth.test.ts +24 -7
  140. package/Tests/Server/Middleware/ExpiredSessionUserMiddleware.test.ts +563 -0
  141. package/Tests/Server/Middleware/MasterAdminAuthorization.test.ts +299 -19
  142. package/Tests/Server/Middleware/PublicRouteUserMiddleware.test.ts +453 -0
  143. package/Tests/Server/Services/AIChatPrivacyPin.test.ts +24 -0
  144. package/Tests/Server/Services/ApiKeyPermissionSecurity.test.ts +2 -0
  145. package/Tests/Server/Services/ApiKeyPermissionService.test.ts +3 -3
  146. package/Tests/Server/Services/DatabaseServiceCreateSuppliedId.test.ts +147 -0
  147. package/Tests/Server/Services/DatabaseServiceLoginBeforeHooks.test.ts +1078 -0
  148. package/Tests/Server/Services/DatabaseServiceTenantRelationGuard.test.ts +259 -0
  149. package/Tests/Server/Services/DatabaseServiceWriteOperatorValues.test.ts +147 -0
  150. package/Tests/Server/Services/MonitorPayAsYouGoBilling.test.ts +115 -1
  151. package/Tests/Server/Services/OnCallChildCreateAuthorizationPostgres.test.ts +711 -0
  152. package/Tests/Server/Services/OnCallDutyPolicyChildService.test.ts +733 -0
  153. package/Tests/Server/Services/PayAsYouGoBillingService.test.ts +190 -140
  154. package/Tests/Server/Services/PayAsYouGoStripeRateLimitBudget.test.ts +4 -9
  155. package/Tests/Server/Services/PermissionBlockDeletion.test.ts +780 -0
  156. package/Tests/Server/Services/PermissionBlockDeletionPostgres.test.ts +628 -0
  157. package/Tests/Server/Services/ProjectServiceCreateSubscriptionStarted.test.ts +77 -1
  158. package/Tests/Server/Services/RumSessionPinCreatePermission.test.ts +715 -0
  159. package/Tests/Server/Services/TeamMemberMultiTenantReadPostgres.test.ts +394 -0
  160. package/Tests/Server/Services/TeamPrivilegeEscalation.test.ts +6 -0
  161. package/Tests/Server/Services/TelemetryPayAsYouGoBilling.test.ts +131 -0
  162. package/Tests/Server/Services/UserNotificationRuleAdminGuards.test.ts +13 -2
  163. package/Tests/Server/Types/Database/Permissions/ModelPermissionAnonymousRefusal.test.ts +518 -0
  164. package/Tests/Server/Types/Database/Permissions/MultiTenantPermissions.test.ts +350 -0
  165. package/Tests/Server/Types/Database/Permissions/OnCallCalendarFeedPermissions.test.ts +282 -25
  166. package/Tests/Server/Types/Database/Permissions/TableLevelBlockPermission.test.ts +459 -0
  167. package/Tests/Server/Utils/AI/AIRunPrivacyFilter.test.ts +101 -9
  168. package/Tests/Server/Utils/AI/ChatAgentHelpers.test.ts +88 -4
  169. package/Tests/Server/Utils/AI/Toolbox/ExceptionTools.test.ts +459 -0
  170. package/Tests/Server/Utils/AI/Toolbox/LogTools.test.ts +794 -0
  171. package/Tests/Server/Utils/AI/Toolbox/RecentChangesTools.test.ts +574 -0
  172. package/Tests/Server/Utils/AI/Toolbox/SecurityEventTools.test.ts +596 -0
  173. package/Tests/Server/Utils/CaptchaRequestAndLogging.test.ts +395 -0
  174. package/Tests/Server/Utils/MasterAdminPageAccess.test.ts +251 -0
  175. package/Tests/Server/Utils/Monitor/MonitorActionTimestamps.test.ts +616 -0
  176. package/Tests/Server/Utils/Realtime.test.ts +502 -0
  177. package/Tests/Server/Utils/SecurityEvent/Connectors/GoogleSecOps/GoogleSecOpsConnectorSeverity.test.ts +216 -0
  178. package/Tests/Server/Utils/SecurityEvent/DetectionRuleEvaluator.test.ts +142 -0
  179. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsSeverityRepair.test.ts +140 -0
  180. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsSeverityRepairClickhouseIntegration.test.ts +542 -0
  181. package/Tests/Server/Utils/Telemetry/ChangeEventRow.test.ts +743 -0
  182. package/Tests/Server/Utils/Telemetry/RuntimeMetrics.test.ts +465 -0
  183. package/Tests/Server/Utils/Workspace/MicrosoftTeams/Messages/Alert.test.ts +202 -0
  184. package/Tests/Server/Utils/Workspace/MicrosoftTeams/Messages/Incident.test.ts +194 -0
  185. package/Tests/Server/Utils/Workspace/MicrosoftTeams/Messages/ScheduledMaintenance.test.ts +231 -0
  186. package/Tests/Server/Utils/Workspace/Slack/Messages/Alert.test.ts +194 -0
  187. package/Tests/Server/Utils/Workspace/Slack/Messages/AlertEpisode.test.ts +194 -0
  188. package/Tests/Server/Utils/Workspace/Slack/Messages/Incident.test.ts +194 -0
  189. package/Tests/Server/Utils/Workspace/Slack/Messages/IncidentEpisode.test.ts +194 -0
  190. package/Tests/Server/Utils/Workspace/Slack/Messages/ScheduledMaintenance.test.ts +247 -0
  191. package/Tests/Types/AI/AIInsightEvidence.test.ts +206 -0
  192. package/Tests/Types/BaseDatabase/DatabaseCommonInteractionPropsUtilAnonymous.test.ts +144 -0
  193. package/Tests/Types/Call/CallProvider.test.ts +528 -0
  194. package/Tests/Types/Kubernetes/KubernetesCostIngest.test.ts +236 -0
  195. package/Tests/Types/OnCallDutyPolicy/ScheduleTimeline.test.ts +736 -0
  196. package/Tests/Types/OnCallDutyPolicy/ScheduleTimelineLayout.test.ts +996 -0
  197. package/Tests/Types/SecurityEvent/SigmaRule.test.ts +142 -0
  198. package/Tests/Types/Workflow/Components/JSON.test.ts +182 -0
  199. package/Tests/UI/Components/BasicFormDropdownNormalization.test.tsx +116 -1
  200. package/Tests/UI/Components/Dropdown/DropdownAsyncOptions.test.tsx +81 -0
  201. package/Tests/UI/Components/EditionLabelLicenseRefresh.test.tsx +89 -1
  202. package/Tests/UI/Components/EventAttachmentList.test.tsx +174 -0
  203. package/Tests/UI/Components/Forms/BasicFormBurnRateTemplateDefaults.test.tsx +395 -0
  204. package/Tests/UI/Components/Forms/BasicFormCollapsibleSections.test.tsx +286 -0
  205. package/Tests/UI/Components/Forms/FormSummary.test.tsx +128 -0
  206. package/Tests/UI/Components/ModelTable/BaseModelTableQueryChange.test.tsx +365 -0
  207. package/Tests/UI/Components/OpenAuthenticatedUrl.test.ts +358 -0
  208. package/Tests/UI/Components/SessionAwareImage.test.tsx +496 -0
  209. package/Tests/UI/Components/TelemetryHistogramZeroBuckets.test.tsx +190 -0
  210. package/Tests/UI/Components/TooltipInteractive.test.tsx +97 -0
  211. package/Tests/UI/MicrosoftTeams/MicrosoftTeamsChatsCardInstallGuidance.test.tsx +1 -1
  212. package/Tests/UI/Monitor/EvaluationLogListTimestamps.test.tsx +367 -0
  213. package/Tests/UI/Utils/API/BaseAPIRefreshSession.test.ts +1087 -0
  214. package/Tests/UI/Utils/API/DashboardHost.ts +12 -0
  215. package/Tests/UI/Utils/API/FakeAxiosServer.ts +183 -0
  216. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +10 -0
  217. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +2 -0
  218. package/Tests/UI/Utils/Cookie.test.ts +440 -0
  219. package/Tests/UI/Utils/ModelAPIRequestClient.test.ts +278 -0
  220. package/Tests/UI/Utils/Navigation.test.ts +33 -0
  221. package/Tests/UI/Utils/PricingPlan.test.ts +295 -0
  222. package/Tests/UI/Utils/Realtime.test.ts +759 -0
  223. package/Tests/UI/Utils/Telemetry/Telemetry.test.ts +500 -0
  224. package/Tests/Utils/APIInstanceDispatch.test.ts +311 -0
  225. package/Tests/Utils/SecurityEvent/GoogleSecOpsAlertNormalizerSeverity.test.ts +565 -0
  226. package/Types/BaseDatabase/DatabaseCommonInteractionPropsUtil.ts +40 -0
  227. package/Types/Monitor/MonitorEvaluationSummary.ts +3 -0
  228. package/Types/OnCallDutyPolicy/ScheduleTimeline.ts +507 -0
  229. package/Types/OnCallDutyPolicy/ScheduleTimelineLayout.ts +589 -0
  230. package/Types/Realtime/EventName.ts +11 -0
  231. package/Types/SecurityEvent/DetectionFindingConstants.ts +14 -0
  232. package/Types/SecurityEvent/OcsfSeverity.ts +31 -26
  233. package/Types/SecurityEvent/SigmaRule.ts +51 -0
  234. package/Types/SecurityEvent/ThreatIntelConstants.ts +18 -0
  235. package/UI/Components/AttachmentList/EventAttachmentList.tsx +30 -2
  236. package/UI/Components/Dashboard/DashboardVariableControl.tsx +12 -2
  237. package/UI/Components/Dropdown/Dropdown.tsx +6 -1
  238. package/UI/Components/EditionLabel/EditionLabel.tsx +12 -0
  239. package/UI/Components/EventItem/EventItem.tsx +12 -1
  240. package/UI/Components/Forms/BasicForm.tsx +151 -126
  241. package/UI/Components/Forms/CollapsibleFormSection.tsx +63 -0
  242. package/UI/Components/Forms/Fields/FormField.tsx +10 -4
  243. package/UI/Components/Forms/FormSummary.tsx +7 -0
  244. package/UI/Components/Forms/Types/Field.ts +10 -0
  245. package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +2 -1
  246. package/UI/Components/Markdown.tsx/SessionAwareImage.tsx +122 -0
  247. package/UI/Components/ModelTable/BaseModelTable.tsx +34 -5
  248. package/UI/Components/ModelTable/TableView.tsx +1 -1
  249. package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +8 -1
  250. package/UI/Components/Tooltip/Tooltip.tsx +8 -1
  251. package/UI/Utils/API/API.ts +173 -12
  252. package/UI/Utils/API/RequestOptions.ts +11 -0
  253. package/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI.ts +31 -7
  254. package/UI/Utils/ModelAPI/ModelAPI.ts +30 -9
  255. package/UI/Utils/Navigation.ts +9 -0
  256. package/UI/Utils/OpenAuthenticatedUrl.ts +154 -0
  257. package/UI/Utils/Realtime.ts +176 -30
  258. package/Utils/API.ts +14 -5
  259. package/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.ts +157 -7
  260. package/Utils/SecurityEvent/Sigma/SigmaRuleParser.ts +9 -17
  261. package/Utils/Slo/SloBurnRateTemplate.ts +2 -2
  262. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.js +1 -1
  263. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.js.map +1 -1
  264. package/build/dist/Models/DatabaseModels/ProjectOnCallCalendarFeed.js +1 -1
  265. package/build/dist/Models/DatabaseModels/ProjectOnCallCalendarFeed.js.map +1 -1
  266. package/build/dist/Models/DatabaseModels/UserOnCallCalendarFeed.js +1 -1
  267. package/build/dist/Models/DatabaseModels/UserOnCallCalendarFeed.js.map +1 -1
  268. package/build/dist/Server/API/AIBillingAPI.js +1 -1
  269. package/build/dist/Server/API/AIBillingAPI.js.map +1 -1
  270. package/build/dist/Server/API/AIInsightAPI.js +1 -4
  271. package/build/dist/Server/API/AIInsightAPI.js.map +1 -1
  272. package/build/dist/Server/API/AIInvestigationAPI.js +1 -4
  273. package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
  274. package/build/dist/Server/API/AIReadinessAPI.js +1 -3
  275. package/build/dist/Server/API/AIReadinessAPI.js.map +1 -1
  276. package/build/dist/Server/API/AlertAPI.js +1 -1
  277. package/build/dist/Server/API/AlertAPI.js.map +1 -1
  278. package/build/dist/Server/API/AutoRemediationAPI.js +1 -4
  279. package/build/dist/Server/API/AutoRemediationAPI.js.map +1 -1
  280. package/build/dist/Server/API/BaseAPI.js +12 -0
  281. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  282. package/build/dist/Server/API/BaseAnalyticsAPI.js +55 -10
  283. package/build/dist/Server/API/BaseAnalyticsAPI.js.map +1 -1
  284. package/build/dist/Server/API/BillingAPI.js +2 -2
  285. package/build/dist/Server/API/BillingAPI.js.map +1 -1
  286. package/build/dist/Server/API/BillingInvoiceAPI.js +1 -1
  287. package/build/dist/Server/API/BillingInvoiceAPI.js.map +1 -1
  288. package/build/dist/Server/API/BillingPaymentMethodAPI.js +2 -2
  289. package/build/dist/Server/API/BillingPaymentMethodAPI.js.map +1 -1
  290. package/build/dist/Server/API/CodeFixRunAPI.js +1 -3
  291. package/build/dist/Server/API/CodeFixRunAPI.js.map +1 -1
  292. package/build/dist/Server/API/CommonAPI.js +50 -5
  293. package/build/dist/Server/API/CommonAPI.js.map +1 -1
  294. package/build/dist/Server/API/DashboardAPI.js +12 -12
  295. package/build/dist/Server/API/DashboardAPI.js.map +1 -1
  296. package/build/dist/Server/API/GlobalConfigAPI.js +12 -0
  297. package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
  298. package/build/dist/Server/API/IncidentAPI.js +2 -2
  299. package/build/dist/Server/API/IncidentAPI.js.map +1 -1
  300. package/build/dist/Server/API/IncidentEpisodeAPI.js +1 -1
  301. package/build/dist/Server/API/IncidentEpisodeAPI.js.map +1 -1
  302. package/build/dist/Server/API/NotificationAPI.js +1 -1
  303. package/build/dist/Server/API/NotificationAPI.js.map +1 -1
  304. package/build/dist/Server/API/OnCallCalendarAPI.js +1 -4
  305. package/build/dist/Server/API/OnCallCalendarAPI.js.map +1 -1
  306. package/build/dist/Server/API/OnCallDutyPolicyAPI.js +6 -0
  307. package/build/dist/Server/API/OnCallDutyPolicyAPI.js.map +1 -1
  308. package/build/dist/Server/API/OnCallReadinessAPI.js +4 -1
  309. package/build/dist/Server/API/OnCallReadinessAPI.js.map +1 -1
  310. package/build/dist/Server/API/OnCallScheduleTimelineAPI.js +494 -0
  311. package/build/dist/Server/API/OnCallScheduleTimelineAPI.js.map +1 -0
  312. package/build/dist/Server/API/ProjectAPI.js +1 -1
  313. package/build/dist/Server/API/ProjectAPI.js.map +1 -1
  314. package/build/dist/Server/API/ScheduledMaintenanceAPI.js +1 -1
  315. package/build/dist/Server/API/ScheduledMaintenanceAPI.js.map +1 -1
  316. package/build/dist/Server/API/StatusPageAPI.js +125 -33
  317. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  318. package/build/dist/Server/API/TeamMemberAPI.js +3 -3
  319. package/build/dist/Server/API/TeamMemberAPI.js.map +1 -1
  320. package/build/dist/Server/API/UserCallAPI.js +2 -2
  321. package/build/dist/Server/API/UserCallAPI.js.map +1 -1
  322. package/build/dist/Server/API/UserEmailAPI.js +2 -2
  323. package/build/dist/Server/API/UserEmailAPI.js.map +1 -1
  324. package/build/dist/Server/API/UserIncomingCallNumberAPI.js +2 -2
  325. package/build/dist/Server/API/UserIncomingCallNumberAPI.js.map +1 -1
  326. package/build/dist/Server/API/UserMicrosoftTeamsAPI.js +1 -1
  327. package/build/dist/Server/API/UserMicrosoftTeamsAPI.js.map +1 -1
  328. package/build/dist/Server/API/UserSlackAPI.js +1 -1
  329. package/build/dist/Server/API/UserSlackAPI.js.map +1 -1
  330. package/build/dist/Server/API/UserSmsAPI.js +2 -2
  331. package/build/dist/Server/API/UserSmsAPI.js.map +1 -1
  332. package/build/dist/Server/API/UserTotpAuthAPI.js +1 -1
  333. package/build/dist/Server/API/UserTotpAuthAPI.js.map +1 -1
  334. package/build/dist/Server/API/UserWebAuthnAPI.js +2 -2
  335. package/build/dist/Server/API/UserWebAuthnAPI.js.map +1 -1
  336. package/build/dist/Server/API/UserWebhookAPI.js +1 -1
  337. package/build/dist/Server/API/UserWebhookAPI.js.map +1 -1
  338. package/build/dist/Server/API/UserWhatsAppAPI.js +2 -2
  339. package/build/dist/Server/API/UserWhatsAppAPI.js.map +1 -1
  340. package/build/dist/Server/Middleware/MasterAdminAuthorization.js +11 -2
  341. package/build/dist/Server/Middleware/MasterAdminAuthorization.js.map +1 -1
  342. package/build/dist/Server/Middleware/UserAuthorization.js +64 -4
  343. package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
  344. package/build/dist/Server/Services/ApiKeyPermissionService.js +80 -4
  345. package/build/dist/Server/Services/ApiKeyPermissionService.js.map +1 -1
  346. package/build/dist/Server/Services/DatabaseService.js +171 -0
  347. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  348. package/build/dist/Server/Services/DetectionRuleService.js +4 -4
  349. package/build/dist/Server/Services/DetectionRuleService.js.map +1 -1
  350. package/build/dist/Server/Services/OnCallDutyPolicyChildService.js +170 -0
  351. package/build/dist/Server/Services/OnCallDutyPolicyChildService.js.map +1 -0
  352. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +2 -2
  353. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
  354. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleService.js +2 -2
  355. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleService.js.map +1 -1
  356. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleTeamService.js +2 -2
  357. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleTeamService.js.map +1 -1
  358. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js +2 -2
  359. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js.map +1 -1
  360. package/build/dist/Server/Services/PayAsYouGoBillingService.js +20 -27
  361. package/build/dist/Server/Services/PayAsYouGoBillingService.js.map +1 -1
  362. package/build/dist/Server/Services/ProjectService.js +17 -0
  363. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  364. package/build/dist/Server/Services/RumSessionPinService.js +180 -16
  365. package/build/dist/Server/Services/RumSessionPinService.js.map +1 -1
  366. package/build/dist/Server/Services/TeamPermissionService.js +89 -27
  367. package/build/dist/Server/Services/TeamPermissionService.js.map +1 -1
  368. package/build/dist/Server/Services/ThreatIntelFeedService.js +5 -4
  369. package/build/dist/Server/Services/ThreatIntelFeedService.js.map +1 -1
  370. package/build/dist/Server/Types/Database/Permissions/BasePermission.js +9 -6
  371. package/build/dist/Server/Types/Database/Permissions/BasePermission.js.map +1 -1
  372. package/build/dist/Server/Types/Database/Permissions/Index.js +60 -6
  373. package/build/dist/Server/Types/Database/Permissions/Index.js.map +1 -1
  374. package/build/dist/Server/Types/Database/Permissions/TablePermission.js +14 -13
  375. package/build/dist/Server/Types/Database/Permissions/TablePermission.js.map +1 -1
  376. package/build/dist/Server/Utils/AI/AIChatPrivacyFilter.js +7 -0
  377. package/build/dist/Server/Utils/AI/AIChatPrivacyFilter.js.map +1 -1
  378. package/build/dist/Server/Utils/AI/AIRunPrivacyFilter.js +7 -0
  379. package/build/dist/Server/Utils/AI/AIRunPrivacyFilter.js.map +1 -1
  380. package/build/dist/Server/Utils/MasterAdminPageAccess.js +39 -0
  381. package/build/dist/Server/Utils/MasterAdminPageAccess.js.map +1 -0
  382. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +3 -1
  383. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  384. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +3 -1
  385. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  386. package/build/dist/Server/Utils/Realtime.js +126 -37
  387. package/build/dist/Server/Utils/Realtime.js.map +1 -1
  388. package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js +7 -25
  389. package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js.map +1 -1
  390. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOpsSeverityRepair.js +127 -0
  391. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOpsSeverityRepair.js.map +1 -0
  392. package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js +1 -1
  393. package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js.map +1 -1
  394. package/build/dist/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelFeedPoller.js +3 -2
  395. package/build/dist/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelFeedPoller.js.map +1 -1
  396. package/build/dist/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelMatcher.js +4 -8
  397. package/build/dist/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelMatcher.js.map +1 -1
  398. package/build/dist/Types/BaseDatabase/DatabaseCommonInteractionPropsUtil.js +31 -1
  399. package/build/dist/Types/BaseDatabase/DatabaseCommonInteractionPropsUtil.js.map +1 -1
  400. package/build/dist/Types/OnCallDutyPolicy/ScheduleTimeline.js +321 -0
  401. package/build/dist/Types/OnCallDutyPolicy/ScheduleTimeline.js.map +1 -0
  402. package/build/dist/Types/OnCallDutyPolicy/ScheduleTimelineLayout.js +372 -0
  403. package/build/dist/Types/OnCallDutyPolicy/ScheduleTimelineLayout.js.map +1 -0
  404. package/build/dist/Types/Realtime/EventName.js +10 -0
  405. package/build/dist/Types/Realtime/EventName.js.map +1 -1
  406. package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js +11 -0
  407. package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js.map +1 -1
  408. package/build/dist/Types/SecurityEvent/OcsfSeverity.js +30 -25
  409. package/build/dist/Types/SecurityEvent/OcsfSeverity.js.map +1 -1
  410. package/build/dist/Types/SecurityEvent/SigmaRule.js +47 -0
  411. package/build/dist/Types/SecurityEvent/SigmaRule.js.map +1 -1
  412. package/build/dist/Types/SecurityEvent/ThreatIntelConstants.js +14 -0
  413. package/build/dist/Types/SecurityEvent/ThreatIntelConstants.js.map +1 -1
  414. package/build/dist/UI/Components/AttachmentList/EventAttachmentList.js +9 -4
  415. package/build/dist/UI/Components/AttachmentList/EventAttachmentList.js.map +1 -1
  416. package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js +10 -2
  417. package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js.map +1 -1
  418. package/build/dist/UI/Components/Dropdown/Dropdown.js +5 -1
  419. package/build/dist/UI/Components/Dropdown/Dropdown.js.map +1 -1
  420. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +11 -0
  421. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  422. package/build/dist/UI/Components/EventItem/EventItem.js +2 -2
  423. package/build/dist/UI/Components/EventItem/EventItem.js.map +1 -1
  424. package/build/dist/UI/Components/Forms/BasicForm.js +70 -64
  425. package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
  426. package/build/dist/UI/Components/Forms/CollapsibleFormSection.js +27 -0
  427. package/build/dist/UI/Components/Forms/CollapsibleFormSection.js.map +1 -0
  428. package/build/dist/UI/Components/Forms/Fields/FormField.js +10 -7
  429. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  430. package/build/dist/UI/Components/Forms/FormSummary.js +7 -0
  431. package/build/dist/UI/Components/Forms/FormSummary.js.map +1 -1
  432. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +2 -1
  433. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
  434. package/build/dist/UI/Components/Markdown.tsx/SessionAwareImage.js +84 -0
  435. package/build/dist/UI/Components/Markdown.tsx/SessionAwareImage.js.map +1 -0
  436. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +14 -1
  437. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  438. package/build/dist/UI/Components/ModelTable/TableView.js +1 -1
  439. package/build/dist/UI/Components/ModelTable/TableView.js.map +1 -1
  440. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +8 -1
  441. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
  442. package/build/dist/UI/Components/Tooltip/Tooltip.js +2 -1
  443. package/build/dist/UI/Components/Tooltip/Tooltip.js.map +1 -1
  444. package/build/dist/UI/Utils/API/API.js +143 -19
  445. package/build/dist/UI/Utils/API/API.js.map +1 -1
  446. package/build/dist/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI.js +21 -7
  447. package/build/dist/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI.js.map +1 -1
  448. package/build/dist/UI/Utils/ModelAPI/ModelAPI.js +26 -9
  449. package/build/dist/UI/Utils/ModelAPI/ModelAPI.js.map +1 -1
  450. package/build/dist/UI/Utils/Navigation.js +8 -0
  451. package/build/dist/UI/Utils/Navigation.js.map +1 -1
  452. package/build/dist/UI/Utils/OpenAuthenticatedUrl.js +92 -0
  453. package/build/dist/UI/Utils/OpenAuthenticatedUrl.js.map +1 -0
  454. package/build/dist/UI/Utils/Realtime.js +120 -15
  455. package/build/dist/UI/Utils/Realtime.js.map +1 -1
  456. package/build/dist/Utils/API.js +13 -5
  457. package/build/dist/Utils/API.js.map +1 -1
  458. package/build/dist/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.js +123 -6
  459. package/build/dist/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.js.map +1 -1
  460. package/build/dist/Utils/SecurityEvent/Sigma/SigmaRuleParser.js +4 -18
  461. package/build/dist/Utils/SecurityEvent/Sigma/SigmaRuleParser.js.map +1 -1
  462. package/build/dist/Utils/Slo/SloBurnRateTemplate.js +2 -2
  463. package/package.json +1 -1
@@ -9,7 +9,10 @@ import Permission, {
9
9
  } from "../../../Types/Permission";
10
10
  import BadDataException from "../../../Types/Exception/BadDataException";
11
11
  import NotAuthorizedException from "../../../Types/Exception/NotAuthorizedException";
12
+ import NotAuthenticatedException from "../../../Types/Exception/NotAuthenticatedException";
12
13
  import Exception from "../../../Types/Exception/Exception";
14
+ import ExceptionCode from "../../../Types/Exception/ExceptionCode";
15
+ import UserType from "../../../Types/UserType";
13
16
 
14
17
  /*
15
18
  * Tests for CommonAPI.assertAuthenticatedProjectMember — the shared auth
@@ -18,6 +21,18 @@ import Exception from "../../../Types/Exception/Exception";
18
21
  * through as "public" and takes the tenant id from a caller-supplied
19
22
  * header, so this guard must reject anything that is not an authenticated
20
23
  * member of the tenant project.
24
+ *
25
+ * HOW it rejects matters as much as THAT it rejects. A caller with no
26
+ * credentials at all gets NotAuthenticatedException (401) before anything
27
+ * else is looked at, including the tenant header; every other refusal keeps
28
+ * its old status (400 for a missing tenant, 422 for "not a member" / "not
29
+ * permitted"). That split is what fixed the customer report: the dashboard's
30
+ * access-token cookie expires with the JWT inside it, so an idle tab's next
31
+ * request arrives anonymous, and the browser client only refreshes the
32
+ * session and replays the request on a 401. Answering that request with a
33
+ * 422 left a signed-in admin staring at "You are not authorized to access
34
+ * this project's data." Assertions below therefore pin the exception class
35
+ * AND its code, because the code is the HTTP status the client acts on.
21
36
  */
22
37
 
23
38
  function buildTenantPermission(
@@ -41,6 +56,7 @@ function buildTenantPermission(
41
56
  function buildProps(overrides?: {
42
57
  tenantId?: ObjectID | undefined;
43
58
  userId?: ObjectID | undefined;
59
+ userType?: UserType | undefined;
44
60
  userTenantAccessPermission?:
45
61
  | Dictionary<UserTenantAccessPermission>
46
62
  | undefined;
@@ -48,11 +64,29 @@ function buildProps(overrides?: {
48
64
  const props: DatabaseCommonInteractionProps = {
49
65
  tenantId: overrides?.tenantId,
50
66
  userId: overrides?.userId,
67
+ userType: overrides?.userType,
51
68
  userTenantAccessPermission: overrides?.userTenantAccessPermission,
52
69
  };
53
70
  return props;
54
71
  }
55
72
 
73
+ /*
74
+ * The props a project API key produces. ProjectMiddleware sets userType API,
75
+ * the tenant the KEY belongs to, and the permissions granted to the key - and
76
+ * never a userId, which is the whole reason API keys need their own cases in
77
+ * this file: "no userId" alone does not mean "anonymous".
78
+ */
79
+ function buildApiKeyProps(projectId: ObjectID): DatabaseCommonInteractionProps {
80
+ return buildProps({
81
+ tenantId: projectId,
82
+ userId: undefined,
83
+ userType: UserType.API,
84
+ userTenantAccessPermission: {
85
+ [projectId.toString()]: buildTenantPermission(projectId),
86
+ },
87
+ });
88
+ }
89
+
56
90
  /*
57
91
  * The guard is synchronous, so failures are captured with try/catch (via
58
92
  * this helper) or expect(() => ...).toThrow — never .rejects.
@@ -66,15 +100,66 @@ function captureThrown(fn: () => void): unknown {
66
100
  return undefined;
67
101
  }
68
102
 
103
+ /*
104
+ * The refusal an expired session must get: NotAuthenticatedException, code
105
+ * 401, with the shared message. Checked on the class, the code and the text,
106
+ * because each is load-bearing somewhere different - the class for the
107
+ * server's error middleware, the code for the HTTP status the browser client
108
+ * keys its refresh-and-replay on, the text for what a user sees if the
109
+ * refresh itself fails.
110
+ */
111
+ function expectAuthenticationRequired(thrown: unknown): void {
112
+ expect(thrown).toBeInstanceOf(NotAuthenticatedException);
113
+ expect(thrown).not.toBeInstanceOf(NotAuthorizedException);
114
+ expect(thrown).not.toBeInstanceOf(BadDataException);
115
+ expect((thrown as Exception).code).toBe(
116
+ ExceptionCode.NotAuthenticatedException,
117
+ );
118
+ expect((thrown as Exception).code).toBe(401);
119
+ expect((thrown as Exception).message).toBe(
120
+ CommonAPI.AUTHENTICATION_REQUIRED_MESSAGE,
121
+ );
122
+ }
123
+
124
+ // An authenticated caller refused access: the pre-existing 422.
125
+ function expectNotAuthorized(thrown: unknown, message?: string): void {
126
+ expect(thrown).toBeInstanceOf(NotAuthorizedException);
127
+ expect(thrown).not.toBeInstanceOf(NotAuthenticatedException);
128
+ expect((thrown as Exception).code).toBe(ExceptionCode.NotAuthorizedException);
129
+ expect((thrown as Exception).code).toBe(422);
130
+
131
+ if (message !== undefined) {
132
+ expect((thrown as Exception).message).toBe(message);
133
+ }
134
+ }
135
+
136
+ // An authenticated caller that named no project: the pre-existing 400.
137
+ function expectBadData(thrown: unknown, messageFragment: string): void {
138
+ expect(thrown).toBeInstanceOf(BadDataException);
139
+ expect((thrown as Exception).code).toBe(ExceptionCode.BadDataException);
140
+ expect((thrown as Exception).code).toBe(400);
141
+ expect((thrown as Exception).message).toContain(messageFragment);
142
+ }
143
+
144
+ const NOT_AUTHORIZED_MESSAGE: string =
145
+ "You are not authorized to access this project's data.";
146
+
69
147
  afterEach(() => {
70
148
  jest.restoreAllMocks();
71
149
  });
72
150
 
73
151
  describe("CommonAPI.assertAuthenticatedProjectMember", () => {
74
152
  describe("missing tenant id", () => {
75
- test("throws BadDataException with 'Project ID is required' when tenantId is undefined", () => {
153
+ /*
154
+ * The caller is logged in (a userId), so this pins the 400 an
155
+ * authenticated caller has always had for forgetting the header. The same
156
+ * request WITHOUT a userId is an expired session and is pinned as a 401
157
+ * below; before the credential check existed both got this 400.
158
+ */
159
+ test("an authenticated user with no tenantId gets BadDataException 'Project ID is required' (400)", () => {
76
160
  const props: DatabaseCommonInteractionProps = buildProps({
77
161
  tenantId: undefined,
162
+ userId: ObjectID.generate(),
78
163
  });
79
164
 
80
165
  expect(() => {
@@ -83,9 +168,16 @@ describe("CommonAPI.assertAuthenticatedProjectMember", () => {
83
168
  expect(() => {
84
169
  CommonAPI.assertAuthenticatedProjectMember(props);
85
170
  }).toThrow("Project ID is required");
171
+
172
+ expectBadData(
173
+ captureThrown(() => {
174
+ CommonAPI.assertAuthenticatedProjectMember(props);
175
+ }),
176
+ "Project ID is required",
177
+ );
86
178
  });
87
179
 
88
- test("missing tenantId wins over everything else — even a fully authenticated user gets BadDataException", () => {
180
+ test("missing tenantId wins over the membership check — a user holding permissions for some project still gets BadDataException", () => {
89
181
  const projectId: ObjectID = ObjectID.generate();
90
182
  const props: DatabaseCommonInteractionProps = buildProps({
91
183
  tenantId: undefined,
@@ -106,17 +198,31 @@ describe("CommonAPI.assertAuthenticatedProjectMember", () => {
106
198
  );
107
199
  });
108
200
 
109
- test("completely empty props object throws BadDataException", () => {
201
+ /*
202
+ * Was a BadDataException (400) before the credential check. An empty
203
+ * props object is the most anonymous caller there is, and "who are you?"
204
+ * now comes before "which project?": a 400 would not make the browser
205
+ * refresh the session, a 401 does.
206
+ */
207
+ test("a completely empty props object is anonymous: NotAuthenticatedException (401), not the missing-tenant 400", () => {
110
208
  const props: DatabaseCommonInteractionProps = {};
111
209
 
112
- expect(() => {
113
- CommonAPI.assertAuthenticatedProjectMember(props);
114
- }).toThrow(BadDataException);
210
+ expectAuthenticationRequired(
211
+ captureThrown(() => {
212
+ CommonAPI.assertAuthenticatedProjectMember(props);
213
+ }),
214
+ );
115
215
  });
116
216
  });
117
217
 
118
218
  describe("unauthenticated / unauthorized callers", () => {
119
- test("tenantId present but no userId throws NotAuthorizedException", () => {
219
+ /*
220
+ * The customer's request, exactly: the tab sat idle, the access-token
221
+ * cookie expired and stopped being sent, so getUserMiddleware produced a
222
+ * Public request that still carries the tenant header. Previously a 422
223
+ * "not authorized"; now a 401 the client answers by refreshing.
224
+ */
225
+ test("tenantId present but no userId and no credentialed userType (an expired session) throws NotAuthenticatedException (401)", () => {
120
226
  const projectId: ObjectID = ObjectID.generate();
121
227
  const props: DatabaseCommonInteractionProps = buildProps({
122
228
  tenantId: projectId,
@@ -126,13 +232,43 @@ describe("CommonAPI.assertAuthenticatedProjectMember", () => {
126
232
  },
127
233
  });
128
234
 
129
- const thrown: unknown = captureThrown(() => {
130
- CommonAPI.assertAuthenticatedProjectMember(props);
235
+ expectAuthenticationRequired(
236
+ captureThrown(() => {
237
+ CommonAPI.assertAuthenticatedProjectMember(props);
238
+ }),
239
+ );
240
+ });
241
+
242
+ test("an explicit UserType.Public caller with a tenant header gets the same 401", () => {
243
+ const projectId: ObjectID = ObjectID.generate();
244
+ const props: DatabaseCommonInteractionProps = buildProps({
245
+ tenantId: projectId,
246
+ userType: UserType.Public,
131
247
  });
132
248
 
133
- expect(thrown).toBeInstanceOf(NotAuthorizedException);
134
- expect((thrown as NotAuthorizedException).message).toBe(
135
- "You are not authorized to access this project's data.",
249
+ expectAuthenticationRequired(
250
+ captureThrown(() => {
251
+ CommonAPI.assertAuthenticatedProjectMember(props);
252
+ }),
253
+ );
254
+ });
255
+
256
+ /*
257
+ * The original intent of the "no userId" case, kept: a caller with tenant
258
+ * permissions but no user is still not a logged-in member. A project API
259
+ * key IS authenticated, though, so it keeps the 422 - a 401 would send an
260
+ * API client off to refresh a session it never had.
261
+ */
262
+ test("a project API key (tenant permissions, no userId) is still refused with NotAuthorizedException (422)", () => {
263
+ const projectId: ObjectID = ObjectID.generate();
264
+
265
+ expectNotAuthorized(
266
+ captureThrown(() => {
267
+ CommonAPI.assertAuthenticatedProjectMember(
268
+ buildApiKeyProps(projectId),
269
+ );
270
+ }),
271
+ NOT_AUTHORIZED_MESSAGE,
136
272
  );
137
273
  });
138
274
 
@@ -198,7 +334,8 @@ describe("CommonAPI.assertAuthenticatedProjectMember", () => {
198
334
  expect(thrown).toBeInstanceOf(Error);
199
335
  });
200
336
 
201
- test("no userId AND no permission map throws NotAuthorizedException (public caller)", () => {
337
+ // Was NotAuthorizedException (422) before the credential check.
338
+ test("no userId AND no permission map (public caller) throws NotAuthenticatedException (401)", () => {
202
339
  const props: DatabaseCommonInteractionProps = buildProps({
203
340
  tenantId: ObjectID.generate(),
204
341
  userId: undefined,
@@ -207,10 +344,16 @@ describe("CommonAPI.assertAuthenticatedProjectMember", () => {
207
344
 
208
345
  expect(() => {
209
346
  CommonAPI.assertAuthenticatedProjectMember(props);
210
- }).toThrow(NotAuthorizedException);
347
+ }).toThrow(NotAuthenticatedException);
211
348
  expect(() => {
212
349
  CommonAPI.assertAuthenticatedProjectMember(props);
213
- }).toThrow("You are not authorized to access this project's data.");
350
+ }).toThrow(CommonAPI.AUTHENTICATION_REQUIRED_MESSAGE);
351
+
352
+ expectAuthenticationRequired(
353
+ captureThrown(() => {
354
+ CommonAPI.assertAuthenticatedProjectMember(props);
355
+ }),
356
+ );
214
357
  });
215
358
  });
216
359
 
@@ -307,11 +450,118 @@ describe("CommonAPI.assertAuthenticatedProjectMember", () => {
307
450
  /*
308
451
  * Pin current behavior: the guard checks userId + the tenant access
309
452
  * permission map ONLY — root / master-admin flags do not grant
310
- * access to these endpoints.
453
+ * access to these endpoints. They do not count as credentials either:
454
+ * with no userId and no API / MasterAdmin userType this is an anonymous
455
+ * caller, so the refusal is now the 401 (it was a 422 before the
456
+ * credential check existed). Still refused, which is what matters.
311
457
  */
312
458
  expect(() => {
313
459
  CommonAPI.assertAuthenticatedProjectMember(props);
314
- }).toThrow(NotAuthorizedException);
460
+ }).toThrow(NotAuthenticatedException);
461
+
462
+ expectAuthenticationRequired(
463
+ captureThrown(() => {
464
+ CommonAPI.assertAuthenticatedProjectMember(props);
465
+ }),
466
+ );
467
+ });
468
+
469
+ test("a logged-in member keeps working when the request also carries isRoot / isMasterAdmin flags", () => {
470
+ const projectId: ObjectID = ObjectID.generate();
471
+ const props: DatabaseCommonInteractionProps = {
472
+ ...buildProps({
473
+ tenantId: projectId,
474
+ userId: ObjectID.generate(),
475
+ userTenantAccessPermission: {
476
+ [projectId.toString()]: buildTenantPermission(projectId),
477
+ },
478
+ }),
479
+ isRoot: true,
480
+ isMasterAdmin: true,
481
+ };
482
+
483
+ expect(CommonAPI.assertAuthenticatedProjectMember(props)).toBe(projectId);
484
+ });
485
+ });
486
+
487
+ /*
488
+ * The credential check is the FIRST thing the guard does, and each refusal
489
+ * a caller can get has a fixed status. These tests pin the precedence
490
+ * directly: every pair differs in exactly one fact about the caller.
491
+ */
492
+ describe("refusal precedence and status codes", () => {
493
+ test("anonymous with no tenant header: 401 wins over the missing-tenant 400", () => {
494
+ expectAuthenticationRequired(
495
+ captureThrown(() => {
496
+ CommonAPI.assertAuthenticatedProjectMember(
497
+ buildProps({ tenantId: undefined, userId: undefined }),
498
+ );
499
+ }),
500
+ );
501
+ });
502
+
503
+ test("authenticated with no tenant header: still the 400", () => {
504
+ expectBadData(
505
+ captureThrown(() => {
506
+ CommonAPI.assertAuthenticatedProjectMember(
507
+ buildProps({
508
+ tenantId: undefined,
509
+ userId: ObjectID.generate(),
510
+ userType: UserType.User,
511
+ }),
512
+ );
513
+ }),
514
+ "Project ID is required",
515
+ );
516
+ });
517
+
518
+ test("an API key with no tenant: the 400, because a key is a credential", () => {
519
+ expectBadData(
520
+ captureThrown(() => {
521
+ CommonAPI.assertAuthenticatedProjectMember(
522
+ buildProps({ tenantId: undefined, userType: UserType.API }),
523
+ );
524
+ }),
525
+ "Project ID is required",
526
+ );
527
+ });
528
+
529
+ test("a logged-in member of ANOTHER project: still the 422", () => {
530
+ const requestedProjectId: ObjectID = ObjectID.generate();
531
+ const otherProjectId: ObjectID = ObjectID.generate();
532
+
533
+ expectNotAuthorized(
534
+ captureThrown(() => {
535
+ CommonAPI.assertAuthenticatedProjectMember(
536
+ buildProps({
537
+ tenantId: requestedProjectId,
538
+ userId: ObjectID.generate(),
539
+ userType: UserType.User,
540
+ userTenantAccessPermission: {
541
+ [otherProjectId.toString()]:
542
+ buildTenantPermission(otherProjectId),
543
+ },
544
+ }),
545
+ );
546
+ }),
547
+ NOT_AUTHORIZED_MESSAGE,
548
+ );
549
+ });
550
+
551
+ test("a master-admin session with no userId is credentialed, so it gets the member check's 422 rather than a 401", () => {
552
+ const projectId: ObjectID = ObjectID.generate();
553
+
554
+ expectNotAuthorized(
555
+ captureThrown(() => {
556
+ CommonAPI.assertAuthenticatedProjectMember(
557
+ buildProps({
558
+ tenantId: projectId,
559
+ userType: UserType.MasterAdmin,
560
+ }),
561
+ );
562
+ }),
563
+ NOT_AUTHORIZED_MESSAGE,
564
+ );
315
565
  });
316
566
  });
317
567
  });
@@ -417,8 +667,13 @@ describe("CommonAPI.assertTenantScoped", () => {
417
667
  });
418
668
 
419
669
  test("the message points at the header rather than at permissions", () => {
670
+ /*
671
+ * An authenticated caller: with no credentials at all the guard now asks
672
+ * for a login instead (pinned below), so `{}` no longer reaches the
673
+ * header message this test is about.
674
+ */
420
675
  const thrown: unknown = captureThrown(() => {
421
- CommonAPI.assertTenantScoped({});
676
+ CommonAPI.assertTenantScoped(buildProps({ userId: ObjectID.generate() }));
422
677
  });
423
678
 
424
679
  /*
@@ -429,6 +684,15 @@ describe("CommonAPI.assertTenantScoped", () => {
429
684
  expect((thrown as Exception).message).not.toContain("permission");
430
685
  });
431
686
 
687
+ test("an API key with no tenant gets the header message too", () => {
688
+ expectBadData(
689
+ captureThrown(() => {
690
+ CommonAPI.assertTenantScoped(buildProps({ userType: UserType.API }));
691
+ }),
692
+ "tenantid",
693
+ );
694
+ });
695
+
432
696
  test("returns the tenant id when the request is project scoped", () => {
433
697
  const projectId: ObjectID = ObjectID.generate();
434
698
  const props: DatabaseCommonInteractionProps = buildProps({
@@ -450,33 +714,234 @@ describe("CommonAPI.assertTenantScoped", () => {
450
714
  const permissions: Dictionary<UserTenantAccessPermission> = {};
451
715
  permissions[projectId.toString()] = buildTenantPermission(projectId);
452
716
 
717
+ /*
718
+ * userType API is what ProjectMiddleware sets for a key, and it is now
719
+ * what tells the guard this caller is credentialed. Without it the same
720
+ * props are an anonymous request (see the 401 cases below).
721
+ */
453
722
  const props: DatabaseCommonInteractionProps = buildProps({
454
723
  tenantId: projectId,
455
724
  userId: undefined,
725
+ userType: UserType.API,
456
726
  userTenantAccessPermission: permissions,
457
727
  });
458
728
 
459
729
  expect(() => {
460
730
  CommonAPI.assertTenantScoped(props);
461
731
  }).not.toThrow();
732
+ expect(CommonAPI.assertTenantScoped(props)).toBe(projectId);
462
733
  });
463
734
 
464
735
  test("does not require membership — that stays with the tenant-scoped read", () => {
465
736
  /*
466
737
  * assertTenantScoped is a diagnostic for a missing header, not an
467
- * authorization check. A caller who sends a tenant id for a project they
468
- * are not a member of passes here and is rejected by the read itself, so
469
- * adding this guard cannot loosen or tighten access.
738
+ * authorization check. A logged-in caller who sends a tenant id for a
739
+ * project they are not a member of passes here and is rejected by the
740
+ * read itself, so adding this guard cannot loosen or tighten access for
741
+ * an authenticated caller. (This used to be shown with a caller carrying
742
+ * no userId at all; that caller is anonymous and is now a 401, below.)
470
743
  */
471
744
  const props: DatabaseCommonInteractionProps = buildProps({
472
745
  tenantId: ObjectID.generate(),
473
- userId: undefined,
746
+ userId: ObjectID.generate(),
474
747
  userTenantAccessPermission: undefined,
475
748
  });
476
749
 
477
750
  expect(() => {
478
751
  CommonAPI.assertTenantScoped(props);
479
752
  }).not.toThrow();
753
+
754
+ // Nor for an API key that has no grants in the project it named.
755
+ expect(() => {
756
+ CommonAPI.assertTenantScoped(
757
+ buildProps({
758
+ tenantId: ObjectID.generate(),
759
+ userType: UserType.API,
760
+ userTenantAccessPermission: undefined,
761
+ }),
762
+ );
763
+ }).not.toThrow();
764
+ });
765
+
766
+ /*
767
+ * A tenant header on its own proves nothing. Before the credential check,
768
+ * the guard let this caller through to a tenant-scoped read that then
769
+ * failed as "You do not have permissions to read <model>" - the permissions
770
+ * message this guard exists to avoid - or, for a custom route, to whatever
771
+ * refusal the route produced. Now the caller is told to log in.
772
+ */
773
+ test("an anonymous caller carrying only a tenant header is refused with NotAuthenticatedException (401)", () => {
774
+ expectAuthenticationRequired(
775
+ captureThrown(() => {
776
+ CommonAPI.assertTenantScoped(
777
+ buildProps({ tenantId: ObjectID.generate() }),
778
+ );
779
+ }),
780
+ );
781
+ });
782
+
783
+ test("an explicit UserType.Public caller with a tenant header is the same 401", () => {
784
+ expectAuthenticationRequired(
785
+ captureThrown(() => {
786
+ CommonAPI.assertTenantScoped(
787
+ buildProps({
788
+ tenantId: ObjectID.generate(),
789
+ userType: UserType.Public,
790
+ }),
791
+ );
792
+ }),
793
+ );
794
+ });
795
+
796
+ test("anonymous with no tenant header: 401 wins over the missing-tenant 400", () => {
797
+ expectAuthenticationRequired(
798
+ captureThrown(() => {
799
+ CommonAPI.assertTenantScoped({});
800
+ }),
801
+ );
802
+ });
803
+
804
+ test("a master-admin session is admitted without a userId", () => {
805
+ const projectId: ObjectID = ObjectID.generate();
806
+
807
+ expect(
808
+ CommonAPI.assertTenantScoped(
809
+ buildProps({ tenantId: projectId, userType: UserType.MasterAdmin }),
810
+ ),
811
+ ).toBe(projectId);
812
+ });
813
+ });
814
+
815
+ /*
816
+ * Tests for CommonAPI.assertAuthenticatedProjectPrincipal — the member guard
817
+ * that ALSO admits a project API key, for endpoints meant to be automatable.
818
+ * Same precedence as the member guard: credentials first (401), then the
819
+ * tenant (400), then access to that tenant (422).
820
+ */
821
+ describe("CommonAPI.assertAuthenticatedProjectPrincipal", () => {
822
+ test("admits a logged-in member of the tenant project", () => {
823
+ const projectId: ObjectID = ObjectID.generate();
824
+
825
+ expect(
826
+ CommonAPI.assertAuthenticatedProjectPrincipal(
827
+ buildProps({
828
+ tenantId: projectId,
829
+ userId: ObjectID.generate(),
830
+ userType: UserType.User,
831
+ userTenantAccessPermission: {
832
+ [projectId.toString()]: buildTenantPermission(projectId),
833
+ },
834
+ }),
835
+ ),
836
+ ).toBe(projectId);
837
+ });
838
+
839
+ test("admits a project API key with permissions in the tenant project", () => {
840
+ const projectId: ObjectID = ObjectID.generate();
841
+
842
+ expect(
843
+ CommonAPI.assertAuthenticatedProjectPrincipal(
844
+ buildApiKeyProps(projectId),
845
+ ),
846
+ ).toBe(projectId);
847
+ });
848
+
849
+ test("an API key with no permission entry for the tenant is still the 422", () => {
850
+ expectNotAuthorized(
851
+ captureThrown(() => {
852
+ CommonAPI.assertAuthenticatedProjectPrincipal(
853
+ buildProps({
854
+ tenantId: ObjectID.generate(),
855
+ userType: UserType.API,
856
+ userTenantAccessPermission: {},
857
+ }),
858
+ );
859
+ }),
860
+ NOT_AUTHORIZED_MESSAGE,
861
+ );
862
+ });
863
+
864
+ test("a logged-in member of ANOTHER project is still the 422", () => {
865
+ const requestedProjectId: ObjectID = ObjectID.generate();
866
+ const otherProjectId: ObjectID = ObjectID.generate();
867
+
868
+ expectNotAuthorized(
869
+ captureThrown(() => {
870
+ CommonAPI.assertAuthenticatedProjectPrincipal(
871
+ buildProps({
872
+ tenantId: requestedProjectId,
873
+ userId: ObjectID.generate(),
874
+ userTenantAccessPermission: {
875
+ [otherProjectId.toString()]:
876
+ buildTenantPermission(otherProjectId),
877
+ },
878
+ }),
879
+ );
880
+ }),
881
+ NOT_AUTHORIZED_MESSAGE,
882
+ );
883
+ });
884
+
885
+ test("an authenticated caller with no tenant is still the 400", () => {
886
+ expectBadData(
887
+ captureThrown(() => {
888
+ CommonAPI.assertAuthenticatedProjectPrincipal(
889
+ buildProps({ userId: ObjectID.generate() }),
890
+ );
891
+ }),
892
+ "Project ID is required",
893
+ );
894
+
895
+ expectBadData(
896
+ captureThrown(() => {
897
+ CommonAPI.assertAuthenticatedProjectPrincipal(
898
+ buildProps({ userType: UserType.API }),
899
+ );
900
+ }),
901
+ "Project ID is required",
902
+ );
903
+ });
904
+
905
+ test("an anonymous caller with a tenant header and a permission map is refused with 401", () => {
906
+ /*
907
+ * The permission map is there to prove it is not what decides this: an
908
+ * anonymous request never legitimately carries one, but if it did, the
909
+ * missing credential still wins.
910
+ */
911
+ const projectId: ObjectID = ObjectID.generate();
912
+
913
+ expectAuthenticationRequired(
914
+ captureThrown(() => {
915
+ CommonAPI.assertAuthenticatedProjectPrincipal(
916
+ buildProps({
917
+ tenantId: projectId,
918
+ userTenantAccessPermission: {
919
+ [projectId.toString()]: buildTenantPermission(projectId),
920
+ },
921
+ }),
922
+ );
923
+ }),
924
+ );
925
+ });
926
+
927
+ test("anonymous with no tenant header: 401 wins over the missing-tenant 400", () => {
928
+ expectAuthenticationRequired(
929
+ captureThrown(() => {
930
+ CommonAPI.assertAuthenticatedProjectPrincipal({});
931
+ }),
932
+ );
933
+ });
934
+
935
+ test("isRoot / isMasterAdmin flags are not credentials", () => {
936
+ expectAuthenticationRequired(
937
+ captureThrown(() => {
938
+ CommonAPI.assertAuthenticatedProjectPrincipal({
939
+ tenantId: ObjectID.generate(),
940
+ isRoot: true,
941
+ isMasterAdmin: true,
942
+ });
943
+ }),
944
+ );
480
945
  });
481
946
  });
482
947
 
@@ -630,7 +1095,12 @@ describe("CommonAPI.assertPermittedInProject", () => {
630
1095
  }).toThrow(NotAuthorizedException);
631
1096
  });
632
1097
 
633
- test("Permission.Public in the allowed list does not admit an anonymous caller", () => {
1098
+ /*
1099
+ * Still refused - and now refused as what it is. This caller was a 422 from
1100
+ * the permission intersection; the credential check answers it with a 401
1101
+ * before the permission lists are even read.
1102
+ */
1103
+ test("Permission.Public in the allowed list does not admit an anonymous caller: NotAuthenticatedException (401)", () => {
634
1104
  const props: DatabaseCommonInteractionProps = {
635
1105
  tenantId: ObjectID.generate(),
636
1106
  };
@@ -640,7 +1110,50 @@ describe("CommonAPI.assertPermittedInProject", () => {
640
1110
  databaseProps: props,
641
1111
  allowedPermissions: [Permission.Public],
642
1112
  });
643
- }).toThrow(NotAuthorizedException);
1113
+ }).toThrow(NotAuthenticatedException);
1114
+
1115
+ expectAuthenticationRequired(
1116
+ captureThrown(() => {
1117
+ CommonAPI.assertPermittedInProject({
1118
+ databaseProps: props,
1119
+ allowedPermissions: [Permission.Public],
1120
+ });
1121
+ }),
1122
+ );
1123
+ });
1124
+
1125
+ test("an anonymous caller is asked to log in even when the route supplied its own refusal message", () => {
1126
+ /*
1127
+ * errorMessage words the "not permitted" refusal; it must not leak onto
1128
+ * the 401, whose text is the shared log-in prompt.
1129
+ */
1130
+ expectAuthenticationRequired(
1131
+ captureThrown(() => {
1132
+ CommonAPI.assertPermittedInProject({
1133
+ databaseProps: { tenantId: ObjectID.generate() },
1134
+ allowedPermissions: [Permission.ProjectOwner],
1135
+ errorMessage:
1136
+ "You do not have permission to read this project's AI providers.",
1137
+ });
1138
+ }),
1139
+ );
1140
+ });
1141
+
1142
+ test("the refusal for an authenticated member without the permission keeps its 422", () => {
1143
+ const projectId: ObjectID = ObjectID.generate();
1144
+
1145
+ expectNotAuthorized(
1146
+ captureThrown(() => {
1147
+ CommonAPI.assertPermittedInProject({
1148
+ databaseProps: buildPermittedProps({
1149
+ projectId: projectId,
1150
+ granted: [Permission.ProjectMember],
1151
+ }),
1152
+ allowedPermissions: [Permission.ProjectOwner],
1153
+ });
1154
+ }),
1155
+ "You do not have permission to access this project's data.",
1156
+ );
644
1157
  });
645
1158
 
646
1159
  test("Permission.CurrentUser, which every logged-in caller carries, does not admit them either", () => {
@@ -740,10 +1253,67 @@ describe("CommonAPI.assertPermittedInProject", () => {
740
1253
  }).not.toThrow();
741
1254
  });
742
1255
 
1256
+ test("a master-admin session (userType MasterAdmin, no userId) bypasses the permission check", () => {
1257
+ expect(() => {
1258
+ CommonAPI.assertPermittedInProject({
1259
+ databaseProps: {
1260
+ tenantId: ObjectID.generate(),
1261
+ userType: UserType.MasterAdmin,
1262
+ isMasterAdmin: true,
1263
+ },
1264
+ allowedPermissions: [Permission.ProjectOwner],
1265
+ });
1266
+ }).not.toThrow();
1267
+ });
1268
+
1269
+ /*
1270
+ * The bypass is for a master admin, and being one is a credential. The
1271
+ * isMasterAdmin FLAG on its own, on a request with no user and no
1272
+ * MasterAdmin userType, is not - so it is asked to log in rather than
1273
+ * waved through.
1274
+ */
1275
+ test("the isMasterAdmin flag without credentials is a 401, not a bypass", () => {
1276
+ expectAuthenticationRequired(
1277
+ captureThrown(() => {
1278
+ CommonAPI.assertPermittedInProject({
1279
+ databaseProps: {
1280
+ tenantId: ObjectID.generate(),
1281
+ isMasterAdmin: true,
1282
+ },
1283
+ allowedPermissions: [Permission.ProjectOwner],
1284
+ });
1285
+ }),
1286
+ );
1287
+ });
1288
+
1289
+ test("an API key holding an allowed permission is admitted", () => {
1290
+ const projectId: ObjectID = ObjectID.generate();
1291
+ const props: DatabaseCommonInteractionProps = buildPermittedProps({
1292
+ projectId: projectId,
1293
+ granted: [Permission.ProjectOwner],
1294
+ });
1295
+
1296
+ props.userId = undefined;
1297
+ props.userType = UserType.API;
1298
+
1299
+ expect(() => {
1300
+ CommonAPI.assertPermittedInProject({
1301
+ databaseProps: props,
1302
+ allowedPermissions: [Permission.ProjectOwner],
1303
+ });
1304
+ }).not.toThrow();
1305
+ });
1306
+
743
1307
  /*
744
1308
  * The guard answers one question only. Membership is assertAuthenticated-
745
1309
  * ProjectMember's job, and a route must call both — this test documents
746
1310
  * that calling only this one is not enough.
1311
+ *
1312
+ * The caller that proves it is one assertAuthenticatedProjectMember would
1313
+ * refuse: no userId. It is an API key here (userType API) because a caller
1314
+ * with no userId AND no credentialed userType is anonymous, and every
1315
+ * guard, this one included, now answers that with a 401 first - pinned at
1316
+ * the end of this test.
747
1317
  */
748
1318
  test("does not itself check membership — that stays with assertAuthenticatedProjectMember", () => {
749
1319
  const projectId: ObjectID = ObjectID.generate();
@@ -760,6 +1330,7 @@ describe("CommonAPI.assertPermittedInProject", () => {
760
1330
  }).not.toThrow();
761
1331
 
762
1332
  props.userId = undefined;
1333
+ props.userType = UserType.API;
763
1334
 
764
1335
  expect(() => {
765
1336
  CommonAPI.assertPermittedInProject({
@@ -767,5 +1338,413 @@ describe("CommonAPI.assertPermittedInProject", () => {
767
1338
  allowedPermissions: [Permission.ProjectOwner],
768
1339
  });
769
1340
  }).not.toThrow();
1341
+
1342
+ // The member guard, by contrast, refuses this caller.
1343
+ expectNotAuthorized(
1344
+ captureThrown(() => {
1345
+ CommonAPI.assertAuthenticatedProjectMember(props);
1346
+ }),
1347
+ NOT_AUTHORIZED_MESSAGE,
1348
+ );
1349
+
1350
+ // With the credential gone too, the grants no longer matter: 401.
1351
+ props.userType = undefined;
1352
+
1353
+ expectAuthenticationRequired(
1354
+ captureThrown(() => {
1355
+ CommonAPI.assertPermittedInProject({
1356
+ databaseProps: props,
1357
+ allowedPermissions: [Permission.ProjectOwner],
1358
+ });
1359
+ }),
1360
+ );
1361
+ });
1362
+ });
1363
+
1364
+ /*
1365
+ * ---------------------------------------------------------------------------
1366
+ * The credential helpers every guard above is built on.
1367
+ * ---------------------------------------------------------------------------
1368
+ */
1369
+
1370
+ /*
1371
+ * isAnonymous decides who gets a 401. It must be exactly "no user, no API
1372
+ * key, no master key": too narrow and an expired session keeps getting 422s
1373
+ * the browser will not refresh on; too wide and an API client gets a 401 it
1374
+ * answers by trying to refresh a session it never had.
1375
+ */
1376
+ describe("CommonAPI.isAnonymous", () => {
1377
+ type IsAnonymousCase = {
1378
+ name: string;
1379
+ props: DatabaseCommonInteractionProps;
1380
+ expected: boolean;
1381
+ };
1382
+
1383
+ const cases: Array<IsAnonymousCase> = [
1384
+ {
1385
+ name: "a userId and nothing else (a logged-in user)",
1386
+ props: { userId: ObjectID.generate() },
1387
+ expected: false,
1388
+ },
1389
+ {
1390
+ name: "a userId with userType User",
1391
+ props: { userId: ObjectID.generate(), userType: UserType.User },
1392
+ expected: false,
1393
+ },
1394
+ {
1395
+ name: "userType API with no userId (a project API key)",
1396
+ props: { userType: UserType.API },
1397
+ expected: false,
1398
+ },
1399
+ {
1400
+ name: "userType MasterAdmin with no userId (the master API key)",
1401
+ props: { userType: UserType.MasterAdmin },
1402
+ expected: false,
1403
+ },
1404
+ {
1405
+ name: "userType Public (getUserMiddleware found no token)",
1406
+ props: { userType: UserType.Public },
1407
+ expected: true,
1408
+ },
1409
+ {
1410
+ name: "userType Public with a tenant header",
1411
+ props: { userType: UserType.Public, tenantId: ObjectID.generate() },
1412
+ expected: true,
1413
+ },
1414
+ {
1415
+ name: "userType User WITHOUT a userId",
1416
+ props: { userType: UserType.User },
1417
+ expected: true,
1418
+ },
1419
+ {
1420
+ name: "no userType and no userId at all",
1421
+ props: { userType: undefined, userId: undefined },
1422
+ expected: true,
1423
+ },
1424
+ {
1425
+ name: "an empty props object",
1426
+ props: {},
1427
+ expected: true,
1428
+ },
1429
+ {
1430
+ name: "isRoot without a userType or userId",
1431
+ props: { isRoot: true },
1432
+ expected: true,
1433
+ },
1434
+ {
1435
+ name: "isMasterAdmin without a userType or userId",
1436
+ props: { isMasterAdmin: true },
1437
+ expected: true,
1438
+ },
1439
+ {
1440
+ name: "isRoot and isMasterAdmin together, with a tenant and permissions",
1441
+ props: {
1442
+ isRoot: true,
1443
+ isMasterAdmin: true,
1444
+ tenantId: ObjectID.generate(),
1445
+ userTenantAccessPermission: {},
1446
+ },
1447
+ expected: true,
1448
+ },
1449
+ ];
1450
+
1451
+ test.each(cases)(
1452
+ "$name -> anonymous: $expected",
1453
+ (testCase: IsAnonymousCase) => {
1454
+ expect(CommonAPI.isAnonymous(testCase.props)).toBe(testCase.expected);
1455
+ },
1456
+ );
1457
+
1458
+ test("a userId makes the caller non-anonymous whatever the userType says", () => {
1459
+ expect(
1460
+ CommonAPI.isAnonymous({
1461
+ userId: ObjectID.generate(),
1462
+ userType: UserType.Public,
1463
+ }),
1464
+ ).toBe(false);
1465
+ });
1466
+ });
1467
+
1468
+ describe("CommonAPI.assertCredentialsPresent", () => {
1469
+ test("the shared message is the exact log-in prompt", () => {
1470
+ expect(CommonAPI.AUTHENTICATION_REQUIRED_MESSAGE).toBe(
1471
+ "Authentication required. Please log in to access this resource.",
1472
+ );
1473
+ });
1474
+
1475
+ test("the 401 is ExceptionCode.NotAuthenticatedException, distinct from the 422 and the 400", () => {
1476
+ /*
1477
+ * Pinned numerically because the numbers are the contract with the
1478
+ * browser client, which refreshes on 401 and on nothing else.
1479
+ */
1480
+ expect(ExceptionCode.NotAuthenticatedException).toBe(401);
1481
+ expect(ExceptionCode.NotAuthorizedException).toBe(422);
1482
+ expect(ExceptionCode.BadDataException).toBe(400);
1483
+ });
1484
+
1485
+ test("throws NotAuthenticatedException (401) with the exact message for an anonymous caller", () => {
1486
+ expectAuthenticationRequired(
1487
+ captureThrown(() => {
1488
+ CommonAPI.assertCredentialsPresent({});
1489
+ }),
1490
+ );
1491
+ });
1492
+
1493
+ test("throws for a UserType.Public caller carrying a tenant and a permission map", () => {
1494
+ const projectId: ObjectID = ObjectID.generate();
1495
+
1496
+ expectAuthenticationRequired(
1497
+ captureThrown(() => {
1498
+ CommonAPI.assertCredentialsPresent(
1499
+ buildProps({
1500
+ tenantId: projectId,
1501
+ userType: UserType.Public,
1502
+ userTenantAccessPermission: {
1503
+ [projectId.toString()]: buildTenantPermission(projectId),
1504
+ },
1505
+ }),
1506
+ );
1507
+ }),
1508
+ );
1509
+ });
1510
+
1511
+ test("throws for isRoot / isMasterAdmin flags with no credentials", () => {
1512
+ expectAuthenticationRequired(
1513
+ captureThrown(() => {
1514
+ CommonAPI.assertCredentialsPresent({
1515
+ isRoot: true,
1516
+ isMasterAdmin: true,
1517
+ });
1518
+ }),
1519
+ );
1520
+ });
1521
+
1522
+ test("passes for a logged-in user", () => {
1523
+ expect(() => {
1524
+ CommonAPI.assertCredentialsPresent({
1525
+ userId: ObjectID.generate(),
1526
+ userType: UserType.User,
1527
+ });
1528
+ }).not.toThrow();
1529
+ });
1530
+
1531
+ test("passes for a project API key", () => {
1532
+ expect(() => {
1533
+ CommonAPI.assertCredentialsPresent({ userType: UserType.API });
1534
+ }).not.toThrow();
1535
+ });
1536
+
1537
+ test("passes for a master admin", () => {
1538
+ expect(() => {
1539
+ CommonAPI.assertCredentialsPresent({ userType: UserType.MasterAdmin });
1540
+ }).not.toThrow();
1541
+ });
1542
+
1543
+ test("does not look at the tenant: a credentialed caller with no tenant passes", () => {
1544
+ /*
1545
+ * The tenant is the NEXT question, asked by the guard that needs it. This
1546
+ * helper answers "who are you?" only.
1547
+ */
1548
+ expect(() => {
1549
+ CommonAPI.assertCredentialsPresent({
1550
+ userId: ObjectID.generate(),
1551
+ tenantId: undefined,
1552
+ });
1553
+ }).not.toThrow();
1554
+ });
1555
+ });
1556
+
1557
+ /*
1558
+ * For routes that act AS a person. Three outcomes: no credentials -> 401,
1559
+ * credentials that are not a person (an API key) -> 422, a person -> their
1560
+ * userId.
1561
+ */
1562
+ describe("CommonAPI.assertAuthenticatedUser", () => {
1563
+ test("returns the caller's userId", () => {
1564
+ const userId: ObjectID = ObjectID.generate();
1565
+
1566
+ const returned: ObjectID = CommonAPI.assertAuthenticatedUser({
1567
+ userId: userId,
1568
+ userType: UserType.User,
1569
+ });
1570
+
1571
+ expect(returned).toBe(userId);
1572
+ });
1573
+
1574
+ test("returns the userId of a master admin who has one", () => {
1575
+ const userId: ObjectID = ObjectID.generate();
1576
+
1577
+ expect(
1578
+ CommonAPI.assertAuthenticatedUser({
1579
+ userId: userId,
1580
+ userType: UserType.MasterAdmin,
1581
+ }),
1582
+ ).toBe(userId);
770
1583
  });
1584
+
1585
+ test("an anonymous caller is NotAuthenticatedException (401)", () => {
1586
+ expectAuthenticationRequired(
1587
+ captureThrown(() => {
1588
+ CommonAPI.assertAuthenticatedUser({ tenantId: ObjectID.generate() });
1589
+ }),
1590
+ );
1591
+ });
1592
+
1593
+ test("an anonymous caller gets the log-in prompt, never the route's not-a-user message", () => {
1594
+ expectAuthenticationRequired(
1595
+ captureThrown(() => {
1596
+ CommonAPI.assertAuthenticatedUser(
1597
+ { userType: UserType.Public },
1598
+ "A logged-in user is required.",
1599
+ );
1600
+ }),
1601
+ );
1602
+ });
1603
+
1604
+ test("a project API key is NotAuthorizedException (422) with the default message", () => {
1605
+ expectNotAuthorized(
1606
+ captureThrown(() => {
1607
+ CommonAPI.assertAuthenticatedUser(
1608
+ buildApiKeyProps(ObjectID.generate()),
1609
+ );
1610
+ }),
1611
+ "A logged-in user session is required.",
1612
+ );
1613
+ });
1614
+
1615
+ test("a project API key gets the route's own message when one is given", () => {
1616
+ expectNotAuthorized(
1617
+ captureThrown(() => {
1618
+ CommonAPI.assertAuthenticatedUser(
1619
+ { userType: UserType.API },
1620
+ "AI readiness requires a logged-in user session.",
1621
+ );
1622
+ }),
1623
+ "AI readiness requires a logged-in user session.",
1624
+ );
1625
+ });
1626
+
1627
+ test("a master-key caller (MasterAdmin, no userId) is credentialed but not a person: 422", () => {
1628
+ expectNotAuthorized(
1629
+ captureThrown(() => {
1630
+ CommonAPI.assertAuthenticatedUser({ userType: UserType.MasterAdmin });
1631
+ }),
1632
+ "A logged-in user session is required.",
1633
+ );
1634
+ });
1635
+
1636
+ test("an empty custom message falls back to the default", () => {
1637
+ expectNotAuthorized(
1638
+ captureThrown(() => {
1639
+ CommonAPI.assertAuthenticatedUser({ userType: UserType.API }, "");
1640
+ }),
1641
+ "A logged-in user session is required.",
1642
+ );
1643
+ });
1644
+ });
1645
+
1646
+ /*
1647
+ * One table across all four project guards: the anonymous caller is a 401 in
1648
+ * every shape it can arrive in, and nothing about the tenant or the grants it
1649
+ * claims changes that.
1650
+ */
1651
+ describe("every project guard answers an anonymous caller with 401", () => {
1652
+ type GuardCase = {
1653
+ name: string;
1654
+ run: (props: DatabaseCommonInteractionProps) => void;
1655
+ };
1656
+
1657
+ const guards: Array<GuardCase> = [
1658
+ {
1659
+ name: "assertAuthenticatedProjectMember",
1660
+ run: (props: DatabaseCommonInteractionProps): void => {
1661
+ CommonAPI.assertAuthenticatedProjectMember(props);
1662
+ },
1663
+ },
1664
+ {
1665
+ name: "assertAuthenticatedProjectPrincipal",
1666
+ run: (props: DatabaseCommonInteractionProps): void => {
1667
+ CommonAPI.assertAuthenticatedProjectPrincipal(props);
1668
+ },
1669
+ },
1670
+ {
1671
+ name: "assertTenantScoped",
1672
+ run: (props: DatabaseCommonInteractionProps): void => {
1673
+ CommonAPI.assertTenantScoped(props);
1674
+ },
1675
+ },
1676
+ {
1677
+ name: "assertPermittedInProject",
1678
+ run: (props: DatabaseCommonInteractionProps): void => {
1679
+ CommonAPI.assertPermittedInProject({
1680
+ databaseProps: props,
1681
+ allowedPermissions: [Permission.ProjectOwner, Permission.Public],
1682
+ });
1683
+ },
1684
+ },
1685
+ {
1686
+ name: "assertAuthenticatedUser",
1687
+ run: (props: DatabaseCommonInteractionProps): void => {
1688
+ CommonAPI.assertAuthenticatedUser(props);
1689
+ },
1690
+ },
1691
+ ];
1692
+
1693
+ test.each(guards)("$name: no tenant header", (guard: GuardCase) => {
1694
+ expectAuthenticationRequired(
1695
+ captureThrown(() => {
1696
+ guard.run({});
1697
+ }),
1698
+ );
1699
+ });
1700
+
1701
+ test.each(guards)(
1702
+ "$name: tenant header only (the expired-session request)",
1703
+ (guard: GuardCase) => {
1704
+ expectAuthenticationRequired(
1705
+ captureThrown(() => {
1706
+ guard.run({
1707
+ tenantId: ObjectID.generate(),
1708
+ userType: UserType.Public,
1709
+ });
1710
+ }),
1711
+ );
1712
+ },
1713
+ );
1714
+
1715
+ test.each(guards)(
1716
+ "$name: tenant header plus a permission map claiming ProjectOwner",
1717
+ (guard: GuardCase) => {
1718
+ const projectId: ObjectID = ObjectID.generate();
1719
+ const props: DatabaseCommonInteractionProps = buildPermittedProps({
1720
+ projectId: projectId,
1721
+ granted: [Permission.ProjectOwner],
1722
+ });
1723
+
1724
+ props.userId = undefined;
1725
+
1726
+ expectAuthenticationRequired(
1727
+ captureThrown(() => {
1728
+ guard.run(props);
1729
+ }),
1730
+ );
1731
+ },
1732
+ );
1733
+
1734
+ test.each(guards)(
1735
+ "$name: a logged-in owner of the project is not refused as anonymous",
1736
+ (guard: GuardCase) => {
1737
+ const projectId: ObjectID = ObjectID.generate();
1738
+ const props: DatabaseCommonInteractionProps = buildPermittedProps({
1739
+ projectId: projectId,
1740
+ granted: [Permission.ProjectOwner],
1741
+ });
1742
+
1743
+ props.userType = UserType.User;
1744
+
1745
+ expect(() => {
1746
+ guard.run(props);
1747
+ }).not.toThrow();
1748
+ },
1749
+ );
771
1750
  });