@oneuptime/common 12.0.6 → 12.0.7

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 (424) hide show
  1. package/Models/DatabaseModels/AIInsight.ts +1 -1
  2. package/Models/DatabaseModels/DatabaseBaseModel/FileModel.ts +2 -3
  3. package/Models/DatabaseModels/DeletedProject.ts +595 -0
  4. package/Models/DatabaseModels/Index.ts +2 -0
  5. package/Models/DatabaseModels/Monitor.ts +111 -0
  6. package/Models/DatabaseModels/Project.ts +3 -3
  7. package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +19 -1
  8. package/Server/API/FileAPI.ts +15 -2
  9. package/Server/API/MicrosoftTeamsAPI.ts +6 -1
  10. package/Server/API/ProjectAPI.ts +214 -0
  11. package/Server/API/StatusPageAPI.ts +39 -6
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1786446545142-MigrationName.ts +50 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1786449497966-AddMonitorDependency.ts +67 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1786461136170-AddDeletedProjectTable.ts +39 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  16. package/Server/Services/AIAgentService.ts +20 -0
  17. package/Server/Services/DashboardDomainService.ts +73 -28
  18. package/Server/Services/DatabaseService.ts +1 -0
  19. package/Server/Services/DeletedProjectService.ts +138 -0
  20. package/Server/Services/FileService.ts +61 -0
  21. package/Server/Services/IncidentEpisodePublicNoteService.ts +18 -0
  22. package/Server/Services/IncidentPublicNoteService.ts +18 -0
  23. package/Server/Services/Index.ts +2 -0
  24. package/Server/Services/MonitorService.ts +311 -0
  25. package/Server/Services/ProbeService.ts +20 -0
  26. package/Server/Services/ProjectService.ts +297 -1
  27. package/Server/Services/PushNotificationService.ts +71 -0
  28. package/Server/Services/ScheduledMaintenancePublicNoteService.ts +18 -0
  29. package/Server/Services/StatusPageAnnouncementService.ts +53 -0
  30. package/Server/Services/StatusPageDomainService.ts +73 -28
  31. package/Server/Services/TeamMemberService.ts +117 -12
  32. package/Server/Services/TelemetryIngestionKeyService.ts +1 -0
  33. package/Server/Services/UserWebhookService.ts +34 -109
  34. package/Server/Services/WorkspaceNotificationRuleService.ts +69 -0
  35. package/Server/Services/WorkspaceProjectAuthTokenService.ts +37 -1
  36. package/Server/Types/Database/DeleteById.ts +2 -0
  37. package/Server/Types/Database/DeleteOneBy.ts +6 -0
  38. package/Server/Types/Domain.ts +41 -0
  39. package/Server/Types/Workflow/Components/API/Delete.ts +2 -1
  40. package/Server/Types/Workflow/Components/API/Get.ts +2 -1
  41. package/Server/Types/Workflow/Components/API/Patch.ts +2 -1
  42. package/Server/Types/Workflow/Components/API/Post.ts +2 -1
  43. package/Server/Types/Workflow/Components/API/Put.ts +2 -1
  44. package/Server/Types/Workflow/Components/API/Utils.ts +41 -2
  45. package/Server/Types/Workflow/Components/BaseModel/CreateManyBaseModel.ts +25 -19
  46. package/Server/Types/Workflow/Components/BaseModel/CreateOneBaseModel.ts +6 -5
  47. package/Server/Types/Workflow/Components/BaseModel/DeleteManyBaseModel.ts +32 -14
  48. package/Server/Types/Workflow/Components/BaseModel/DeleteOneBaseModel.ts +31 -13
  49. package/Server/Types/Workflow/Components/BaseModel/FindManyBaseModel.ts +29 -20
  50. package/Server/Types/Workflow/Components/BaseModel/FindOneBaseModel.ts +28 -21
  51. package/Server/Types/Workflow/Components/BaseModel/LogComponentError.ts +89 -0
  52. package/Server/Types/Workflow/Components/BaseModel/ModelArguments.ts +208 -0
  53. package/Server/Types/Workflow/Components/BaseModel/OnTriggerBaseModel.ts +11 -0
  54. package/Server/Types/Workflow/Components/BaseModel/UpdateManyBaseModel.ts +41 -18
  55. package/Server/Types/Workflow/Components/BaseModel/UpdateOneBaseModel.ts +45 -19
  56. package/Server/Types/Workflow/Components/Discord/SendMessageToChannel.ts +16 -9
  57. package/Server/Types/Workflow/Components/IncomingWebhookUtils.ts +66 -0
  58. package/Server/Types/Workflow/Components/MicrosoftTeams/SendMessageToChannel.ts +15 -9
  59. package/Server/Types/Workflow/Components/Slack/SendMessageToChannel.ts +17 -0
  60. package/Server/Utils/AI/Chat/ObservabilityChatPrompt.ts +1 -1
  61. package/Server/Utils/AI/CodeFix/CodeAgentWorkspaceGuard.ts +171 -0
  62. package/Server/Utils/AI/SRE/FixFromIncidentTaskTrigger.ts +3 -3
  63. package/Server/Utils/AI/SRE/Insights/FixRouting.ts +3 -2
  64. package/Server/Utils/AI/SRE/TelemetryImprovementTaskTrigger.ts +1 -1
  65. package/Server/Utils/AI/Toolbox/CodeWriteTools.ts +6 -6
  66. package/Server/Utils/CodeRepository/GitHub/GitHub.ts +43 -5
  67. package/Server/Utils/DataSource/EgressGuard.ts +176 -16
  68. package/Server/Utils/DataSource/HttpFetch.ts +6 -45
  69. package/Server/Utils/Execute.ts +8 -0
  70. package/Server/Utils/InlineImageAccessTokenSync.ts +23 -0
  71. package/Server/Utils/LLM/LLMService.ts +57 -12
  72. package/Server/Utils/Monitor/MonitorAlert.ts +40 -0
  73. package/Server/Utils/Monitor/MonitorDependencySuppression.ts +192 -0
  74. package/Server/Utils/Monitor/MonitorIncident.ts +40 -0
  75. package/Server/Utils/Monitor/MonitorResource.ts +36 -0
  76. package/Server/Utils/Response.ts +142 -6
  77. package/Server/Utils/SSRFProtection.ts +273 -60
  78. package/Server/Utils/StartServer.ts +8 -0
  79. package/Server/Utils/VM/VMRunner.ts +88 -1
  80. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +555 -49
  81. package/Server/Utils/Workspace/Slack/Slack.ts +25 -2
  82. package/Server/Utils/Workspace/WorkspaceBase.ts +6 -0
  83. package/Tests/App/Dashboard/DangerZone.test.tsx +334 -0
  84. package/Tests/App/Dashboard/DashboardVariableAllContract.test.tsx +539 -0
  85. package/Tests/App/Dashboard/DashboardVariableSelector.test.tsx +678 -0
  86. package/Tests/App/Dashboard/DashboardVariablesDiscard.test.tsx +706 -0
  87. package/Tests/App/Dashboard/DashboardVariablesModal.test.tsx +508 -0
  88. package/Tests/App/PublicDashboard/DashboardVariableSelector.test.tsx +470 -0
  89. package/Tests/App/StatusPage/StatusPageDetailPagesNotFound.test.tsx +308 -0
  90. package/Tests/Models/DatabaseModels/DeletedProjectModel.test.ts +251 -0
  91. package/Tests/Models/DatabaseModels/SavedViewJSONColumnTuples.test.ts +115 -0
  92. package/Tests/Server/API/FileAPIAccessControl.test.ts +316 -0
  93. package/Tests/Server/API/ProjectAPI.test.ts +299 -1
  94. package/Tests/Server/API/ProjectAPIAdjustBalance.test.ts +459 -0
  95. package/Tests/Server/API/ProjectAPIAdjustBalanceBillingDisabled.test.ts +177 -0
  96. package/Tests/Server/Services/DatabaseServiceDeletionReason.test.ts +123 -0
  97. package/Tests/Server/Services/DeletedProjectService.test.ts +415 -0
  98. package/Tests/Server/Services/DeletedProjectServiceSelfHosted.test.ts +70 -0
  99. package/Tests/Server/Services/DomainSubdomainHooks.test.ts +94 -0
  100. package/Tests/Server/Services/FileIsPublicBooleanMigration.test.ts +244 -0
  101. package/Tests/Server/Services/FileServiceMimeType.test.ts +89 -0
  102. package/Tests/Server/Services/MonitorServiceDependencyValidation.test.ts +702 -0
  103. package/Tests/Server/Services/ProjectServiceAdjustBalance.test.ts +615 -0
  104. package/Tests/Server/Services/ProjectServiceDeleteAudit.test.ts +406 -0
  105. package/Tests/Server/Services/PublishTimeImageVisibility.test.ts +450 -0
  106. package/Tests/Server/Services/TeamMemberAutoAcceptInvitation.test.ts +669 -0
  107. package/Tests/Server/Services/UserWebhookSSRF.test.ts +174 -0
  108. package/Tests/Server/Services/WebPushEndpointAllowlist.test.ts +133 -0
  109. package/Tests/Server/Services/WorkspaceProjectAuthTokenRepoint.test.ts +618 -0
  110. package/Tests/Server/Types/Workflow/Components/ApiComponentErrorPort.test.ts +26 -1
  111. package/Tests/Server/Types/Workflow/Components/ApiComponentSsrf.test.ts +215 -0
  112. package/Tests/Server/Types/Workflow/Components/BaseModelDatabaseComponents.test.ts +742 -0
  113. package/Tests/Server/Types/Workflow/Components/ChatWebhookComponents.test.ts +256 -0
  114. package/Tests/Server/Types/Workflow/Components/ModelArguments.test.ts +371 -0
  115. package/Tests/Server/Utils/AI/CodeAgentCommandGuard.test.ts +204 -0
  116. package/Tests/Server/Utils/AI/CodeAgentWriteLimits.test.ts +213 -0
  117. package/Tests/Server/Utils/AI/CodeWriteTools.test.ts +52 -5
  118. package/Tests/Server/Utils/AI/LLMServiceBaseUrl.test.ts +3 -0
  119. package/Tests/Server/Utils/AI/LLMServiceEgressGuard.test.ts +315 -0
  120. package/Tests/Server/Utils/AI/LLMServiceModelCompatibility.test.ts +3 -0
  121. package/Tests/Server/Utils/AI/LLMServiceRequestPolicy.test.ts +17 -5
  122. package/Tests/Server/Utils/AI/LLMServiceToolCalling.test.ts +14 -1
  123. package/Tests/Server/Utils/AI/ObservabilityChatPrompt.test.ts +35 -0
  124. package/Tests/Server/Utils/AI/StubLLMEgressGuard.ts +25 -0
  125. package/Tests/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.test.ts +62 -7
  126. package/Tests/Server/Utils/DataSource/EgressGuard.test.ts +69 -0
  127. package/Tests/Server/Utils/DataSource/EgressGuardPinning.test.ts +330 -0
  128. package/Tests/Server/Utils/GitHubCreatePullRequestWithToken.test.ts +393 -0
  129. package/Tests/Server/Utils/InlineImageAccessTokenSync.test.ts +202 -0
  130. package/Tests/Server/Utils/Monitor/MonitorDependencySuppression.test.ts +811 -0
  131. package/Tests/Server/Utils/Monitor/MonitorDependencySuppressionCreatorSkip.test.ts +604 -0
  132. package/Tests/Server/Utils/ResponseSendFileResponse.test.ts +192 -0
  133. package/Tests/Server/Utils/SSRFProtectionBypasses.test.ts +406 -0
  134. package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +194 -0
  135. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChannelSend.test.ts +1169 -0
  136. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChannelsList.test.ts +2 -0
  137. package/Tests/Server/Utils/Workspace/MicrosoftTeamsTeamInstalls.test.ts +1605 -0
  138. package/Tests/Server/Utils/Workspace/MicrosoftTeamsTenantResolution.test.ts +1218 -0
  139. package/Tests/Types/AI/AIChatMessageStatus.test.ts +79 -0
  140. package/Tests/Types/AI/AIInsightStatus.test.ts +40 -0
  141. package/Tests/Types/API/HostnameAuthorityParsing.test.ts +157 -0
  142. package/Tests/Types/Dashboard/DashboardTemplateInvariants.test.ts +27 -0
  143. package/Tests/Types/DataSource/DataSourceLimits.test.ts +71 -0
  144. package/Tests/Types/DataSource/DataSourceType.test.ts +181 -0
  145. package/Tests/Types/DomainSubdomainValidation.test.ts +90 -0
  146. package/Tests/Types/JSONFunctions.test.ts +252 -1
  147. package/Tests/Types/Monitor/SnmpMonitor/SnmpAuthProtocol.test.ts +61 -0
  148. package/Tests/Types/Monitor/SnmpMonitor/SnmpPrivProtocol.test.ts +62 -0
  149. package/Tests/Types/Monitor/SnmpMonitor/SnmpSecurityLevel.test.ts +68 -0
  150. package/Tests/Types/Monitor/SnmpMonitor/SnmpVersion.test.ts +51 -0
  151. package/Tests/UI/Components/404.test.tsx +1 -1
  152. package/Tests/UI/Components/Alert.test.tsx +1 -1
  153. package/Tests/UI/Components/Badge.test.tsx +1 -1
  154. package/Tests/UI/Components/BasicForm.test.tsx +39 -10
  155. package/Tests/UI/Components/Button.test.tsx +1 -1
  156. package/Tests/UI/Components/Card.test.tsx +1 -1
  157. package/Tests/UI/Components/Charts/AxisTickColor.test.tsx +1 -1
  158. package/Tests/UI/Components/ColorViewer.test.tsx +1 -1
  159. package/Tests/UI/Components/ComponentsModal.test.tsx +7 -1
  160. package/Tests/UI/Components/DictionaryOfStrings.test.tsx +1 -1
  161. package/Tests/UI/Components/Dropdown.test.tsx +1 -1
  162. package/Tests/UI/Components/FilePicker.test.tsx +1 -1
  163. package/Tests/UI/Components/HiddenText.test.tsx +1 -1
  164. package/Tests/UI/Components/Input.test.tsx +1 -1
  165. package/Tests/UI/Components/Loader.test.tsx +1 -1
  166. package/Tests/UI/Components/LogTimeRangePicker.test.tsx +1 -1
  167. package/Tests/UI/Components/MasterPage.test.tsx +1 -1
  168. package/Tests/UI/Components/ModalPortalDismissal.test.tsx +489 -0
  169. package/Tests/UI/Components/ModelDelete.test.tsx +214 -0
  170. package/Tests/UI/Components/ModelTable/BaseModelTableBulkSelectAll.test.tsx +1 -1
  171. package/Tests/UI/Components/ModelTable/BaseModelTableColumnCustomization.test.tsx +1 -1
  172. package/Tests/UI/Components/ModelTable/BaseModelTableSearchLabels.test.tsx +1 -1
  173. package/Tests/UI/Components/ModelTable/BaseModelTableUrlState.test.tsx +1 -1
  174. package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +1 -1
  175. package/Tests/UI/Components/NavBar.test.tsx +1 -1
  176. package/Tests/UI/Components/OperatorSelectorKeyboard.test.tsx +1 -1
  177. package/Tests/UI/Components/Pagination.test.tsx +1 -1
  178. package/Tests/UI/Components/Pill.test.tsx +1 -1
  179. package/Tests/UI/Components/Probe.test.tsx +1 -1
  180. package/Tests/UI/Components/ProgressBar.test.tsx +1 -1
  181. package/Tests/UI/Components/RadioButtons.test.tsx +1 -1
  182. package/Tests/UI/Components/SideMenuItem.test.tsx +1 -1
  183. package/Tests/UI/Components/SideOver.test.tsx +500 -6
  184. package/Tests/UI/Components/Tabs.test.tsx +1 -1
  185. package/Tests/UI/Components/TelemetrySearchBar.test.tsx +1 -1
  186. package/Tests/UI/Components/TelemetryTimeRangePicker.test.tsx +1 -1
  187. package/Tests/UI/Components/TextArea.test.tsx +1 -1
  188. package/Tests/UI/Components/Toast.test.tsx +1 -1
  189. package/Tests/UI/Components/Toggle.test.tsx +1 -1
  190. package/Tests/UI/Components/TopSection.test.tsx +1 -1
  191. package/Tests/UI/Components/UseLiveLogsRefresh.test.tsx +1 -1
  192. package/Tests/UI/Rum/ReplayScrubber.test.tsx +1 -1
  193. package/Tests/UI/Rum/ReplayStage.test.tsx +1 -1
  194. package/Tests/UI/Telemetry/TelemetrySnapshotWindowAlert.test.tsx +1 -1
  195. package/Tests/UI/Utils/LocalStorageArrayRoundTrip.test.ts +72 -0
  196. package/Tests/Utils/Dashboard/HtmlWidgetDocument.test.ts +19 -2
  197. package/Tests/Utils/Dashboard/ModelQueryVariableInterpolation.test.ts +44 -0
  198. package/Tests/Utils/Dashboard/VariableInterpolation.test.ts +288 -5
  199. package/Tests/Utils/Dashboard/VariableUrlState.test.ts +42 -0
  200. package/Tests/Utils/DataSource/DataSourceQueryText.test.ts +27 -0
  201. package/Tests/Utils/LegacySerializedArray.test.ts +139 -0
  202. package/Tests/Utils/Monitor/MonitorDependencyRule.test.ts +190 -0
  203. package/Tests/Utils/Realtime.test.ts +78 -0
  204. package/Tests/Utils/Telemetry/SavedViewFilters.test.ts +246 -0
  205. package/Tests/__mocks__/i18next-browser-languagedetector.js +21 -0
  206. package/Types/AI/FixPullRequestCiStatus.ts +1 -1
  207. package/Types/AI/FixVerificationStatus.ts +2 -2
  208. package/Types/API/Hostname.ts +92 -4
  209. package/Types/API/URL.ts +43 -25
  210. package/Types/Billing/BalanceAdjustmentType.ts +14 -0
  211. package/Types/Billing/ProjectBalanceType.ts +13 -0
  212. package/Types/Domain.ts +26 -0
  213. package/Types/File/MimeType.ts +11 -0
  214. package/Types/JSONFunctions.ts +15 -0
  215. package/Types/Telemetry/TelemetrySavedViewState.ts +9 -1
  216. package/Types/Workflow/Components/BaseModel.ts +92 -28
  217. package/Typings/Index.d.ts +12 -0
  218. package/UI/Components/GanttChart/ChartContainer.tsx +4 -3
  219. package/UI/Components/Modal/ConfirmModal.tsx +10 -5
  220. package/UI/Components/Modal/Modal.tsx +27 -47
  221. package/UI/Components/ModelDelete/ModelDelete.tsx +23 -6
  222. package/UI/Components/SideOver/SideOver.tsx +195 -68
  223. package/UI/Utils/PageScrollLock.ts +66 -0
  224. package/Utils/Dashboard/HtmlWidgetDocument.ts +4 -2
  225. package/Utils/Dashboard/VariableInterpolation.ts +19 -1
  226. package/Utils/LegacySerializedArray.ts +60 -0
  227. package/Utils/Monitor/MonitorDependencyRule.ts +88 -0
  228. package/Utils/Telemetry/SavedViewFilters.ts +88 -0
  229. package/build/dist/Models/DatabaseModels/AIInsight.js +1 -1
  230. package/build/dist/Models/DatabaseModels/AIInsight.js.map +1 -1
  231. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/FileModel.js +2 -3
  232. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/FileModel.js.map +1 -1
  233. package/build/dist/Models/DatabaseModels/DeletedProject.js +640 -0
  234. package/build/dist/Models/DatabaseModels/DeletedProject.js.map +1 -0
  235. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  236. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  237. package/build/dist/Models/DatabaseModels/Monitor.js +106 -0
  238. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  239. package/build/dist/Models/DatabaseModels/Project.js +3 -3
  240. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  241. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
  242. package/build/dist/Server/API/FileAPI.js +14 -2
  243. package/build/dist/Server/API/FileAPI.js.map +1 -1
  244. package/build/dist/Server/API/MicrosoftTeamsAPI.js +6 -1
  245. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  246. package/build/dist/Server/API/ProjectAPI.js +157 -4
  247. package/build/dist/Server/API/ProjectAPI.js.map +1 -1
  248. package/build/dist/Server/API/StatusPageAPI.js +27 -3
  249. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  250. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786446545142-MigrationName.js +34 -0
  251. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786446545142-MigrationName.js.map +1 -0
  252. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786449497966-AddMonitorDependency.js +30 -0
  253. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786449497966-AddMonitorDependency.js.map +1 -0
  254. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786461136170-AddDeletedProjectTable.js +20 -0
  255. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786461136170-AddDeletedProjectTable.js.map +1 -0
  256. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  257. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  258. package/build/dist/Server/Services/AIAgentService.js +17 -0
  259. package/build/dist/Server/Services/AIAgentService.js.map +1 -1
  260. package/build/dist/Server/Services/DashboardDomainService.js +55 -18
  261. package/build/dist/Server/Services/DashboardDomainService.js.map +1 -1
  262. package/build/dist/Server/Services/DatabaseService.js +1 -0
  263. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  264. package/build/dist/Server/Services/DeletedProjectService.js +124 -0
  265. package/build/dist/Server/Services/DeletedProjectService.js.map +1 -0
  266. package/build/dist/Server/Services/FileService.js +52 -0
  267. package/build/dist/Server/Services/FileService.js.map +1 -1
  268. package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js +10 -1
  269. package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js.map +1 -1
  270. package/build/dist/Server/Services/IncidentPublicNoteService.js +10 -1
  271. package/build/dist/Server/Services/IncidentPublicNoteService.js.map +1 -1
  272. package/build/dist/Server/Services/Index.js +2 -0
  273. package/build/dist/Server/Services/Index.js.map +1 -1
  274. package/build/dist/Server/Services/MonitorService.js +232 -0
  275. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  276. package/build/dist/Server/Services/ProbeService.js +17 -0
  277. package/build/dist/Server/Services/ProbeService.js.map +1 -1
  278. package/build/dist/Server/Services/ProjectService.js +228 -6
  279. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  280. package/build/dist/Server/Services/PushNotificationService.js +56 -0
  281. package/build/dist/Server/Services/PushNotificationService.js.map +1 -1
  282. package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js +10 -1
  283. package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js.map +1 -1
  284. package/build/dist/Server/Services/StatusPageAnnouncementService.js +54 -0
  285. package/build/dist/Server/Services/StatusPageAnnouncementService.js.map +1 -1
  286. package/build/dist/Server/Services/StatusPageDomainService.js +55 -18
  287. package/build/dist/Server/Services/StatusPageDomainService.js.map +1 -1
  288. package/build/dist/Server/Services/TeamMemberService.js +97 -6
  289. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  290. package/build/dist/Server/Services/TelemetryIngestionKeyService.js +1 -0
  291. package/build/dist/Server/Services/TelemetryIngestionKeyService.js.map +1 -1
  292. package/build/dist/Server/Services/UserWebhookService.js +31 -87
  293. package/build/dist/Server/Services/UserWebhookService.js.map +1 -1
  294. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js +65 -0
  295. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js.map +1 -1
  296. package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js +23 -0
  297. package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js.map +1 -1
  298. package/build/dist/Server/Types/Domain.js +37 -0
  299. package/build/dist/Server/Types/Domain.js.map +1 -1
  300. package/build/dist/Server/Types/Workflow/Components/API/Delete.js +2 -1
  301. package/build/dist/Server/Types/Workflow/Components/API/Delete.js.map +1 -1
  302. package/build/dist/Server/Types/Workflow/Components/API/Get.js +2 -1
  303. package/build/dist/Server/Types/Workflow/Components/API/Get.js.map +1 -1
  304. package/build/dist/Server/Types/Workflow/Components/API/Patch.js +2 -1
  305. package/build/dist/Server/Types/Workflow/Components/API/Patch.js.map +1 -1
  306. package/build/dist/Server/Types/Workflow/Components/API/Post.js +2 -1
  307. package/build/dist/Server/Types/Workflow/Components/API/Post.js.map +1 -1
  308. package/build/dist/Server/Types/Workflow/Components/API/Put.js +2 -1
  309. package/build/dist/Server/Types/Workflow/Components/API/Put.js.map +1 -1
  310. package/build/dist/Server/Types/Workflow/Components/API/Utils.js +35 -2
  311. package/build/dist/Server/Types/Workflow/Components/API/Utils.js.map +1 -1
  312. package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateManyBaseModel.js +15 -11
  313. package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateManyBaseModel.js.map +1 -1
  314. package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateOneBaseModel.js +2 -3
  315. package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateOneBaseModel.js.map +1 -1
  316. package/build/dist/Server/Types/Workflow/Components/BaseModel/DeleteManyBaseModel.js +21 -10
  317. package/build/dist/Server/Types/Workflow/Components/BaseModel/DeleteManyBaseModel.js.map +1 -1
  318. package/build/dist/Server/Types/Workflow/Components/BaseModel/DeleteOneBaseModel.js +21 -10
  319. package/build/dist/Server/Types/Workflow/Components/BaseModel/DeleteOneBaseModel.js.map +1 -1
  320. package/build/dist/Server/Types/Workflow/Components/BaseModel/FindManyBaseModel.js +18 -13
  321. package/build/dist/Server/Types/Workflow/Components/BaseModel/FindManyBaseModel.js.map +1 -1
  322. package/build/dist/Server/Types/Workflow/Components/BaseModel/FindOneBaseModel.js +18 -13
  323. package/build/dist/Server/Types/Workflow/Components/BaseModel/FindOneBaseModel.js.map +1 -1
  324. package/build/dist/Server/Types/Workflow/Components/BaseModel/LogComponentError.js +43 -0
  325. package/build/dist/Server/Types/Workflow/Components/BaseModel/LogComponentError.js.map +1 -0
  326. package/build/dist/Server/Types/Workflow/Components/BaseModel/ModelArguments.js +116 -0
  327. package/build/dist/Server/Types/Workflow/Components/BaseModel/ModelArguments.js.map +1 -0
  328. package/build/dist/Server/Types/Workflow/Components/BaseModel/OnTriggerBaseModel.js +7 -0
  329. package/build/dist/Server/Types/Workflow/Components/BaseModel/OnTriggerBaseModel.js.map +1 -1
  330. package/build/dist/Server/Types/Workflow/Components/BaseModel/UpdateManyBaseModel.js +24 -13
  331. package/build/dist/Server/Types/Workflow/Components/BaseModel/UpdateManyBaseModel.js.map +1 -1
  332. package/build/dist/Server/Types/Workflow/Components/BaseModel/UpdateOneBaseModel.js +27 -13
  333. package/build/dist/Server/Types/Workflow/Components/BaseModel/UpdateOneBaseModel.js.map +1 -1
  334. package/build/dist/Server/Types/Workflow/Components/Discord/SendMessageToChannel.js +14 -6
  335. package/build/dist/Server/Types/Workflow/Components/Discord/SendMessageToChannel.js.map +1 -1
  336. package/build/dist/Server/Types/Workflow/Components/IncomingWebhookUtils.js +42 -0
  337. package/build/dist/Server/Types/Workflow/Components/IncomingWebhookUtils.js.map +1 -0
  338. package/build/dist/Server/Types/Workflow/Components/MicrosoftTeams/SendMessageToChannel.js +15 -6
  339. package/build/dist/Server/Types/Workflow/Components/MicrosoftTeams/SendMessageToChannel.js.map +1 -1
  340. package/build/dist/Server/Types/Workflow/Components/Slack/SendMessageToChannel.js +10 -2
  341. package/build/dist/Server/Types/Workflow/Components/Slack/SendMessageToChannel.js.map +1 -1
  342. package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js +1 -1
  343. package/build/dist/Server/Utils/AI/CodeFix/CodeAgentWorkspaceGuard.js +141 -0
  344. package/build/dist/Server/Utils/AI/CodeFix/CodeAgentWorkspaceGuard.js.map +1 -1
  345. package/build/dist/Server/Utils/AI/SRE/Insights/FixRouting.js.map +1 -1
  346. package/build/dist/Server/Utils/AI/SRE/TelemetryImprovementTaskTrigger.js +1 -1
  347. package/build/dist/Server/Utils/AI/Toolbox/CodeWriteTools.js +6 -6
  348. package/build/dist/Server/Utils/AI/Toolbox/CodeWriteTools.js.map +1 -1
  349. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js +39 -5
  350. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js.map +1 -1
  351. package/build/dist/Server/Utils/DataSource/EgressGuard.js +94 -10
  352. package/build/dist/Server/Utils/DataSource/EgressGuard.js.map +1 -1
  353. package/build/dist/Server/Utils/DataSource/HttpFetch.js +1 -19
  354. package/build/dist/Server/Utils/DataSource/HttpFetch.js.map +1 -1
  355. package/build/dist/Server/Utils/Execute.js +2 -2
  356. package/build/dist/Server/Utils/Execute.js.map +1 -1
  357. package/build/dist/Server/Utils/InlineImageAccessTokenSync.js +13 -0
  358. package/build/dist/Server/Utils/InlineImageAccessTokenSync.js.map +1 -1
  359. package/build/dist/Server/Utils/LLM/LLMService.js +43 -12
  360. package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
  361. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +44 -24
  362. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  363. package/build/dist/Server/Utils/Monitor/MonitorDependencySuppression.js +158 -0
  364. package/build/dist/Server/Utils/Monitor/MonitorDependencySuppression.js.map +1 -0
  365. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +50 -30
  366. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  367. package/build/dist/Server/Utils/Monitor/MonitorResource.js +32 -0
  368. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  369. package/build/dist/Server/Utils/Response.js +112 -5
  370. package/build/dist/Server/Utils/Response.js.map +1 -1
  371. package/build/dist/Server/Utils/SSRFProtection.js +219 -53
  372. package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
  373. package/build/dist/Server/Utils/StartServer.js +7 -0
  374. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  375. package/build/dist/Server/Utils/VM/VMRunner.js +73 -1
  376. package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
  377. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +445 -43
  378. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  379. package/build/dist/Server/Utils/Workspace/Slack/Slack.js +18 -1
  380. package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
  381. package/build/dist/Server/Utils/Workspace/WorkspaceBase.js.map +1 -1
  382. package/build/dist/Types/AI/FixPullRequestCiStatus.js +1 -1
  383. package/build/dist/Types/AI/FixVerificationStatus.js +2 -2
  384. package/build/dist/Types/API/Hostname.js +73 -4
  385. package/build/dist/Types/API/Hostname.js.map +1 -1
  386. package/build/dist/Types/API/URL.js +42 -24
  387. package/build/dist/Types/API/URL.js.map +1 -1
  388. package/build/dist/Types/Billing/BalanceAdjustmentType.js +15 -0
  389. package/build/dist/Types/Billing/BalanceAdjustmentType.js.map +1 -0
  390. package/build/dist/Types/Billing/ProjectBalanceType.js +14 -0
  391. package/build/dist/Types/Billing/ProjectBalanceType.js.map +1 -0
  392. package/build/dist/Types/Domain.js +22 -0
  393. package/build/dist/Types/Domain.js.map +1 -1
  394. package/build/dist/Types/File/MimeType.js +11 -0
  395. package/build/dist/Types/File/MimeType.js.map +1 -1
  396. package/build/dist/Types/JSONFunctions.js +14 -0
  397. package/build/dist/Types/JSONFunctions.js.map +1 -1
  398. package/build/dist/Types/Workflow/Components/BaseModel.js +83 -28
  399. package/build/dist/Types/Workflow/Components/BaseModel.js.map +1 -1
  400. package/build/dist/UI/Components/GanttChart/ChartContainer.js +4 -3
  401. package/build/dist/UI/Components/GanttChart/ChartContainer.js.map +1 -1
  402. package/build/dist/UI/Components/Modal/ConfirmModal.js +3 -1
  403. package/build/dist/UI/Components/Modal/ConfirmModal.js.map +1 -1
  404. package/build/dist/UI/Components/Modal/Modal.js +24 -34
  405. package/build/dist/UI/Components/Modal/Modal.js.map +1 -1
  406. package/build/dist/UI/Components/ModelDelete/ModelDelete.js +11 -6
  407. package/build/dist/UI/Components/ModelDelete/ModelDelete.js.map +1 -1
  408. package/build/dist/UI/Components/SideOver/SideOver.js +105 -30
  409. package/build/dist/UI/Components/SideOver/SideOver.js.map +1 -1
  410. package/build/dist/UI/Utils/PageScrollLock.js +52 -0
  411. package/build/dist/UI/Utils/PageScrollLock.js.map +1 -0
  412. package/build/dist/Utils/Dashboard/HtmlWidgetDocument.js +4 -2
  413. package/build/dist/Utils/Dashboard/HtmlWidgetDocument.js.map +1 -1
  414. package/build/dist/Utils/Dashboard/VariableInterpolation.js +19 -1
  415. package/build/dist/Utils/Dashboard/VariableInterpolation.js.map +1 -1
  416. package/build/dist/Utils/LegacySerializedArray.js +51 -0
  417. package/build/dist/Utils/LegacySerializedArray.js.map +1 -0
  418. package/build/dist/Utils/Monitor/MonitorDependencyRule.js +47 -0
  419. package/build/dist/Utils/Monitor/MonitorDependencyRule.js.map +1 -0
  420. package/build/dist/Utils/Telemetry/SavedViewFilters.js +52 -0
  421. package/build/dist/Utils/Telemetry/SavedViewFilters.js.map +1 -0
  422. package/jest.config.json +3 -0
  423. package/package.json +1 -1
  424. package/tsconfig.json +3 -1
@@ -0,0 +1,1218 @@
1
+ import { describe, expect, test, afterEach, beforeEach } from "@jest/globals";
2
+
3
+ /*
4
+ * Tests for tenant -> project resolution in MicrosoftTeamsUtil.
5
+ *
6
+ * Bot Framework activities carry a Microsoft tenant id and nothing else, so
7
+ * the tenant is the only key the bot can resolve a OneUptime project by. A
8
+ * single tenant may legitimately be connected to several OneUptime projects
9
+ * (saveChatToProjectAuthTokens fans out across every matching row for exactly
10
+ * that reason).
11
+ *
12
+ * This used to be a bare
13
+ * WorkspaceProjectAuthTokenService.findOneBy({ workspaceType, workspaceProjectId })
14
+ * with no projectId scope and no unique constraint behind it, so when a tenant
15
+ * was connected to two projects the bot silently served whichever row sorted
16
+ * first — its incidents, alerts and AI assistant answers — to anyone in the
17
+ * tenant. The bot message path performs no per-user authorization, so guessing
18
+ * is a cross-project data disclosure.
19
+ *
20
+ * The fix reads ALL matching rows (findBy + LIMIT_MAX) and refuses to guess:
21
+ *
22
+ * - resolveProjectByTenantId returns { projectAuth, isAmbiguous,
23
+ * candidateProjectIds }; more than one usable PROJECT means projectAuth ===
24
+ * null and isAmbiguous === true.
25
+ * - Rows with a falsy projectId are dropped, and rows repeating a projectId
26
+ * already seen are collapsed, BEFORE the count is taken - so one good row
27
+ * plus one orphaned row, or two rows for the same project, still resolve
28
+ * cleanly.
29
+ * - getTenantResolutionFailureMessage gives the ambiguous case a different
30
+ * remedy from the not-found case.
31
+ * - Both bot entry points (handleBotMessageActivity for typed messages and
32
+ * handleBotInvokeActivity for adaptive-card clicks) go through it, and both
33
+ * scope everything they read afterwards to the project it resolved.
34
+ *
35
+ * These tests fail if the code ever reverts to findOneBy, if either entry point
36
+ * starts serving a project it merely guessed at, or if a resolved tenant's
37
+ * downstream reads are scoped to any project other than the resolved one.
38
+ */
39
+
40
+ jest.mock("../../../../Server/EnvironmentConfig", () => {
41
+ return {
42
+ ...(jest.requireActual("../../../../Server/EnvironmentConfig") as Record<
43
+ string,
44
+ unknown
45
+ >),
46
+ MicrosoftTeamsAppClientId: "11111111-2222-3333-4444-555555555555",
47
+ MicrosoftTeamsAppClientSecret: "test-secret",
48
+ MicrosoftTeamsAppTenantId: "test-tenant",
49
+ };
50
+ });
51
+
52
+ /*
53
+ * Same botbuilder module factory as MicrosoftTeamsChats.test.ts — the repo-wide
54
+ * manual mock does not expose everything MicrosoftTeams.ts touches at import
55
+ * time.
56
+ */
57
+ jest.mock("botbuilder", () => {
58
+ return {
59
+ CloudAdapter: class CloudAdapter {},
60
+ ConfigurationBotFrameworkAuthentication: class ConfigurationBotFrameworkAuthentication {},
61
+ TeamsActivityHandler: class TeamsActivityHandler {},
62
+ TurnContext: class TurnContext {},
63
+ ActivityHandler: class ActivityHandler {},
64
+ MessageFactory: {
65
+ text: jest.fn(),
66
+ attachment: jest.fn(),
67
+ },
68
+ CardFactory: { heroCard: jest.fn() },
69
+ TeamsInfo: {
70
+ getMembers: jest.fn(),
71
+ getPagedMembers: jest.fn(),
72
+ },
73
+ };
74
+ });
75
+
76
+ import MicrosoftTeamsUtil, {
77
+ MicrosoftTeamsTenantResolution,
78
+ } from "../../../../Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams";
79
+ import MicrosoftTeamsAuthAction from "../../../../Server/Utils/Workspace/MicrosoftTeams/Actions/Auth";
80
+ import WorkspaceProjectAuthTokenService from "../../../../Server/Services/WorkspaceProjectAuthTokenService";
81
+ import WorkspaceProjectAuthToken from "../../../../Models/DatabaseModels/WorkspaceProjectAuthToken";
82
+ import WorkspaceType from "../../../../Types/Workspace/WorkspaceType";
83
+ import ObjectID from "../../../../Types/ObjectID";
84
+ import LIMIT_MAX from "../../../../Types/Database/LimitMax";
85
+ import { JSONObject } from "../../../../Types/JSON";
86
+ import {
87
+ MessageFactory,
88
+ CardFactory,
89
+ TeamsInfo,
90
+ type TurnContext,
91
+ } from "botbuilder";
92
+
93
+ const TENANT_ID: string = "tenant-abc";
94
+ const BOT_RECIPIENT_ID: string = "bot-recipient-id";
95
+ const AAD_OBJECT_ID: string = "aad-object-id-1";
96
+ const TURN_CONTEXT_SERVICE_URL: string =
97
+ "https://smba.trafficmanager.net/emea/";
98
+
99
+ /*
100
+ * Distinguishing substrings of the two failure messages. Deliberately short so
101
+ * that copy edits to the surrounding sentence do not break these tests.
102
+ */
103
+ const AMBIGUOUS_FRAGMENT: string = "more than one OneUptime project";
104
+ const NOT_FOUND_FRAGMENT: string = "couldn't find your project configuration";
105
+ const NO_TENANT_FRAGMENT: string = "couldn't identify your organization";
106
+ const HELP_FRAGMENT: string = "Available Commands";
107
+
108
+ interface CapturedFindByCall {
109
+ query: JSONObject;
110
+ select: JSONObject;
111
+ limit: number;
112
+ skip: number;
113
+ props: JSONObject;
114
+ }
115
+
116
+ interface FakeTurnContext {
117
+ turnContext: TurnContext;
118
+ sendActivity: jest.Mock;
119
+ }
120
+
121
+ function buildTurnContext(): FakeTurnContext {
122
+ const sendActivity: jest.Mock = jest.fn(async (): Promise<JSONObject> => {
123
+ return { id: "sent-message-id" };
124
+ });
125
+
126
+ const turnContext: TurnContext = {
127
+ activity: {
128
+ recipient: { id: BOT_RECIPIENT_ID },
129
+ serviceUrl: TURN_CONTEXT_SERVICE_URL,
130
+ conversation: { id: "ctx-conversation-id" },
131
+ },
132
+ sendActivity: sendActivity,
133
+ } as unknown as TurnContext;
134
+
135
+ return { turnContext: turnContext, sendActivity: sendActivity };
136
+ }
137
+
138
+ function buildProjectAuthRow(data?: {
139
+ projectId?: ObjectID | null | undefined;
140
+ workspaceProjectId?: string | undefined;
141
+ authToken?: string | undefined;
142
+ }): WorkspaceProjectAuthToken {
143
+ return {
144
+ id: ObjectID.generate(),
145
+ projectId:
146
+ data && "projectId" in data ? data.projectId : ObjectID.generate(),
147
+ workspaceProjectId: data?.workspaceProjectId || TENANT_ID,
148
+ authToken: data?.authToken || "stored-auth-token",
149
+ miscData: { tenantId: TENANT_ID, teamId: "team-1", botId: "bot-1" },
150
+ } as unknown as WorkspaceProjectAuthToken;
151
+ }
152
+
153
+ function mockFindBy(rows: Array<WorkspaceProjectAuthToken>): jest.SpyInstance {
154
+ return jest
155
+ .spyOn(WorkspaceProjectAuthTokenService, "findBy")
156
+ .mockResolvedValue(rows);
157
+ }
158
+
159
+ /*
160
+ * findOneBy is what the vulnerable version used. It is stubbed (never left
161
+ * live) so that a regression cannot reach a real database, and asserted
162
+ * not-called so that a regression is caught rather than silently tolerated.
163
+ */
164
+ function spyOnFindOneBy(): jest.SpyInstance {
165
+ return jest
166
+ .spyOn(WorkspaceProjectAuthTokenService, "findOneBy")
167
+ .mockResolvedValue(null);
168
+ }
169
+
170
+ /*
171
+ * The message path backfills captured chats before it resolves the tenant, and
172
+ * that backfill hits WorkspaceProjectAuthTokenService itself. Stub it out so
173
+ * findBy call counts belong to resolveProjectByTenantId alone.
174
+ */
175
+ function stubChatCapture(): jest.SpyInstance {
176
+ return jest
177
+ .spyOn(MicrosoftTeamsUtil as any, "captureChatFromBotActivity")
178
+ .mockResolvedValue(undefined as never);
179
+ }
180
+
181
+ /*
182
+ * The readers the message path serves project data with are private statics,
183
+ * so they are spied through the class object. Stubbing them keeps the test off
184
+ * the database while capturing the one argument that matters: the project id
185
+ * the handler decided to scope the read to.
186
+ */
187
+ function stubDownstreamReader(
188
+ methodName: string,
189
+ resolvedValue?: unknown,
190
+ ): jest.SpyInstance {
191
+ return jest
192
+ .spyOn(MicrosoftTeamsUtil as any, methodName)
193
+ .mockResolvedValue(resolvedValue as never);
194
+ }
195
+
196
+ function firstArgProjectId(spy: jest.SpyInstance): ObjectID {
197
+ return spy.mock.calls[0]?.[0] as ObjectID;
198
+ }
199
+
200
+ function messageActivity(data?: {
201
+ tenantId?: string | undefined;
202
+ omitTenant?: boolean | undefined;
203
+ text?: string | undefined;
204
+ }): JSONObject {
205
+ const channelData: JSONObject = {};
206
+
207
+ if (!data?.omitTenant) {
208
+ channelData["tenant"] = { id: data?.tenantId || TENANT_ID };
209
+ }
210
+
211
+ return {
212
+ type: "message",
213
+ // "help" is answered from a constant, so the path needs no AI or DB mocks.
214
+ text: data?.text || "help",
215
+ from: { id: "user-1", name: "Alice", aadObjectId: AAD_OBJECT_ID },
216
+ recipient: { id: BOT_RECIPIENT_ID },
217
+ conversation: { conversationType: "personal", id: "19:personal@thread.v2" },
218
+ channelData: channelData,
219
+ serviceUrl: TURN_CONTEXT_SERVICE_URL,
220
+ entities: [],
221
+ };
222
+ }
223
+
224
+ function invokeActivity(data?: {
225
+ tenantId?: string | undefined;
226
+ omitTenant?: boolean | undefined;
227
+ omitAadObjectId?: boolean | undefined;
228
+ }): JSONObject {
229
+ const channelData: JSONObject = {};
230
+
231
+ if (!data?.omitTenant) {
232
+ channelData["tenant"] = { id: data?.tenantId || TENANT_ID };
233
+ }
234
+
235
+ const from: JSONObject = { id: "user-1", name: "Alice" };
236
+
237
+ if (!data?.omitAadObjectId) {
238
+ from["aadObjectId"] = AAD_OBJECT_ID;
239
+ }
240
+
241
+ return {
242
+ type: "invoke",
243
+ name: "adaptiveCard/action",
244
+ value: { action: "unknown-card-action", actionValue: "some-id" },
245
+ from: from,
246
+ recipient: { id: BOT_RECIPIENT_ID },
247
+ conversation: { conversationType: "personal", id: "19:personal@thread.v2" },
248
+ channelData: channelData,
249
+ serviceUrl: TURN_CONTEXT_SERVICE_URL,
250
+ };
251
+ }
252
+
253
+ function sentTexts(sendActivity: jest.Mock): Array<string> {
254
+ return sendActivity.mock.calls.map((call: Array<unknown>) => {
255
+ return typeof call[0] === "string" ? call[0] : JSON.stringify(call[0]);
256
+ });
257
+ }
258
+
259
+ function joinedSentText(sendActivity: jest.Mock): string {
260
+ return sentTexts(sendActivity).join("\n");
261
+ }
262
+
263
+ afterEach(() => {
264
+ jest.restoreAllMocks();
265
+ });
266
+
267
+ /*
268
+ * The botbuilder module mock's jest.fn()s are shared across tests — jest.spyOn
269
+ * on an existing mock returns the same function, so call history and
270
+ * mockResolvedValueOnce queues leak between tests unless reset here.
271
+ */
272
+ beforeEach(() => {
273
+ (MessageFactory.text as jest.Mock).mockReset();
274
+ (MessageFactory.attachment as jest.Mock).mockReset();
275
+ (CardFactory.heroCard as jest.Mock).mockReset();
276
+ (TeamsInfo.getMembers as jest.Mock).mockReset();
277
+ (TeamsInfo.getPagedMembers as jest.Mock).mockReset();
278
+ });
279
+
280
+ describe("MicrosoftTeamsUtil.resolveProjectByTenantId - row counting", () => {
281
+ test("zero matching rows resolves to no project and is NOT ambiguous", async () => {
282
+ mockFindBy([]);
283
+
284
+ const resolution: MicrosoftTeamsTenantResolution =
285
+ await MicrosoftTeamsUtil.resolveProjectByTenantId({
286
+ tenantId: TENANT_ID,
287
+ });
288
+
289
+ expect(resolution.projectAuth).toBeNull();
290
+ expect(resolution.isAmbiguous).toBe(false);
291
+ expect(resolution.candidateProjectIds).toEqual([]);
292
+ });
293
+
294
+ test("exactly one matching row resolves to that row", async () => {
295
+ const projectId: ObjectID = ObjectID.generate();
296
+ const row: WorkspaceProjectAuthToken = buildProjectAuthRow({
297
+ projectId: projectId,
298
+ });
299
+ mockFindBy([row]);
300
+
301
+ const resolution: MicrosoftTeamsTenantResolution =
302
+ await MicrosoftTeamsUtil.resolveProjectByTenantId({
303
+ tenantId: TENANT_ID,
304
+ });
305
+
306
+ expect(resolution.projectAuth).toBe(row);
307
+ expect(resolution.isAmbiguous).toBe(false);
308
+ expect(resolution.candidateProjectIds).toHaveLength(1);
309
+ expect(resolution.candidateProjectIds[0]?.toString()).toBe(
310
+ projectId.toString(),
311
+ );
312
+ });
313
+
314
+ test("two rows for one tenant are ambiguous and resolve to NO project (the disclosure bug)", async () => {
315
+ const projectIdOne: ObjectID = ObjectID.generate();
316
+ const projectIdTwo: ObjectID = ObjectID.generate();
317
+ mockFindBy([
318
+ buildProjectAuthRow({ projectId: projectIdOne }),
319
+ buildProjectAuthRow({ projectId: projectIdTwo }),
320
+ ]);
321
+
322
+ const resolution: MicrosoftTeamsTenantResolution =
323
+ await MicrosoftTeamsUtil.resolveProjectByTenantId({
324
+ tenantId: TENANT_ID,
325
+ });
326
+
327
+ // The old findOneBy returned the first row here — that is the regression.
328
+ expect(resolution.projectAuth).toBeNull();
329
+ expect(resolution.isAmbiguous).toBe(true);
330
+ expect(resolution.candidateProjectIds).toHaveLength(2);
331
+ });
332
+
333
+ test("three rows are ambiguous and list all three candidates", async () => {
334
+ const projectIds: Array<ObjectID> = [
335
+ ObjectID.generate(),
336
+ ObjectID.generate(),
337
+ ObjectID.generate(),
338
+ ];
339
+ mockFindBy(
340
+ projectIds.map((projectId: ObjectID) => {
341
+ return buildProjectAuthRow({ projectId: projectId });
342
+ }),
343
+ );
344
+
345
+ const resolution: MicrosoftTeamsTenantResolution =
346
+ await MicrosoftTeamsUtil.resolveProjectByTenantId({
347
+ tenantId: TENANT_ID,
348
+ });
349
+
350
+ expect(resolution.projectAuth).toBeNull();
351
+ expect(resolution.isAmbiguous).toBe(true);
352
+ expect(resolution.candidateProjectIds).toHaveLength(3);
353
+ });
354
+
355
+ test("candidateProjectIds are the project ids, in the order the rows came back", async () => {
356
+ const projectIdOne: ObjectID = ObjectID.generate();
357
+ const projectIdTwo: ObjectID = ObjectID.generate();
358
+ mockFindBy([
359
+ buildProjectAuthRow({ projectId: projectIdOne }),
360
+ buildProjectAuthRow({ projectId: projectIdTwo }),
361
+ ]);
362
+
363
+ const resolution: MicrosoftTeamsTenantResolution =
364
+ await MicrosoftTeamsUtil.resolveProjectByTenantId({
365
+ tenantId: TENANT_ID,
366
+ });
367
+
368
+ expect(
369
+ resolution.candidateProjectIds.map((projectId: ObjectID) => {
370
+ return projectId.toString();
371
+ }),
372
+ ).toEqual([projectIdOne.toString(), projectIdTwo.toString()]);
373
+ });
374
+ });
375
+
376
+ /*
377
+ * Ambiguity is a question about distinct PROJECTS, not about rows. Duplicate
378
+ * rows for the same project are a data-integrity wart (a re-install can leave
379
+ * one behind) and must not lock that tenant's bot out forever, so resolution
380
+ * de-duplicates by projectId BEFORE it counts.
381
+ */
382
+ describe("MicrosoftTeamsUtil.resolveProjectByTenantId - de-duplication by projectId", () => {
383
+ test("two rows pointing at the SAME project resolve cleanly and are NOT ambiguous", async () => {
384
+ const projectId: ObjectID = ObjectID.generate();
385
+ const firstRow: WorkspaceProjectAuthToken = buildProjectAuthRow({
386
+ projectId: projectId,
387
+ });
388
+ mockFindBy([
389
+ firstRow,
390
+ /*
391
+ * A separate ObjectID instance carrying the same value - de-duplication
392
+ * has to compare ids by value, not by object identity.
393
+ */
394
+ buildProjectAuthRow({ projectId: new ObjectID(projectId.toString()) }),
395
+ ]);
396
+
397
+ const resolution: MicrosoftTeamsTenantResolution =
398
+ await MicrosoftTeamsUtil.resolveProjectByTenantId({
399
+ tenantId: TENANT_ID,
400
+ });
401
+
402
+ expect(resolution.projectAuth).toBe(firstRow);
403
+ expect(resolution.isAmbiguous).toBe(false);
404
+ expect(
405
+ resolution.candidateProjectIds.map((candidate: ObjectID) => {
406
+ return candidate.toString();
407
+ }),
408
+ ).toEqual([projectId.toString()]);
409
+ });
410
+
411
+ test("two otherwise-identical rows with DIFFERENT projects stay ambiguous - projectId is the only key", async () => {
412
+ const projectIdOne: ObjectID = ObjectID.generate();
413
+ const projectIdTwo: ObjectID = ObjectID.generate();
414
+ /*
415
+ * Everything except projectId matches, so a de-duplication keyed on the
416
+ * tenant or the auth token would collapse these into one and hand the user
417
+ * whichever project happened to sort first - the original disclosure bug.
418
+ */
419
+ mockFindBy([
420
+ buildProjectAuthRow({
421
+ projectId: projectIdOne,
422
+ workspaceProjectId: TENANT_ID,
423
+ authToken: "identical-auth-token",
424
+ }),
425
+ buildProjectAuthRow({
426
+ projectId: projectIdTwo,
427
+ workspaceProjectId: TENANT_ID,
428
+ authToken: "identical-auth-token",
429
+ }),
430
+ ]);
431
+
432
+ const resolution: MicrosoftTeamsTenantResolution =
433
+ await MicrosoftTeamsUtil.resolveProjectByTenantId({
434
+ tenantId: TENANT_ID,
435
+ });
436
+
437
+ expect(resolution.projectAuth).toBeNull();
438
+ expect(resolution.isAmbiguous).toBe(true);
439
+ expect(
440
+ resolution.candidateProjectIds.map((candidate: ObjectID) => {
441
+ return candidate.toString();
442
+ }),
443
+ ).toEqual([projectIdOne.toString(), projectIdTwo.toString()]);
444
+ });
445
+
446
+ test("three rows collapsing to two distinct projects are still ambiguous with two candidates", async () => {
447
+ const projectIdOne: ObjectID = ObjectID.generate();
448
+ const projectIdTwo: ObjectID = ObjectID.generate();
449
+ mockFindBy([
450
+ buildProjectAuthRow({ projectId: projectIdOne }),
451
+ buildProjectAuthRow({ projectId: projectIdTwo }),
452
+ buildProjectAuthRow({ projectId: new ObjectID(projectIdOne.toString()) }),
453
+ ]);
454
+
455
+ const resolution: MicrosoftTeamsTenantResolution =
456
+ await MicrosoftTeamsUtil.resolveProjectByTenantId({
457
+ tenantId: TENANT_ID,
458
+ });
459
+
460
+ expect(resolution.projectAuth).toBeNull();
461
+ expect(resolution.isAmbiguous).toBe(true);
462
+ // Three rows, two projects: the duplicate collapses, the conflict does not.
463
+ expect(
464
+ resolution.candidateProjectIds.map((candidate: ObjectID) => {
465
+ return candidate.toString();
466
+ }),
467
+ ).toEqual([projectIdOne.toString(), projectIdTwo.toString()]);
468
+ });
469
+
470
+ test("a duplicated project is served by the message path instead of refused", async () => {
471
+ const projectId: ObjectID = ObjectID.generate();
472
+ stubChatCapture();
473
+ mockFindBy([
474
+ buildProjectAuthRow({ projectId: projectId }),
475
+ buildProjectAuthRow({ projectId: new ObjectID(projectId.toString()) }),
476
+ ]);
477
+ const context: FakeTurnContext = buildTurnContext();
478
+
479
+ await MicrosoftTeamsUtil.handleBotMessageActivity({
480
+ activity: messageActivity(),
481
+ turnContext: context.turnContext,
482
+ });
483
+
484
+ const sent: string = joinedSentText(context.sendActivity);
485
+ expect(sent).toContain(HELP_FRAGMENT);
486
+ expect(sent).not.toContain(AMBIGUOUS_FRAGMENT);
487
+ expect(sent).not.toContain(NOT_FOUND_FRAGMENT);
488
+ });
489
+ });
490
+
491
+ describe("MicrosoftTeamsUtil.resolveProjectByTenantId - unusable rows", () => {
492
+ test("a single row with an undefined projectId counts as not found, not ambiguous", async () => {
493
+ mockFindBy([buildProjectAuthRow({ projectId: undefined })]);
494
+
495
+ const resolution: MicrosoftTeamsTenantResolution =
496
+ await MicrosoftTeamsUtil.resolveProjectByTenantId({
497
+ tenantId: TENANT_ID,
498
+ });
499
+
500
+ expect(resolution.projectAuth).toBeNull();
501
+ expect(resolution.isAmbiguous).toBe(false);
502
+ expect(resolution.candidateProjectIds).toEqual([]);
503
+ });
504
+
505
+ test("a single row with a null projectId counts as not found", async () => {
506
+ mockFindBy([buildProjectAuthRow({ projectId: null })]);
507
+
508
+ const resolution: MicrosoftTeamsTenantResolution =
509
+ await MicrosoftTeamsUtil.resolveProjectByTenantId({
510
+ tenantId: TENANT_ID,
511
+ });
512
+
513
+ expect(resolution.projectAuth).toBeNull();
514
+ expect(resolution.isAmbiguous).toBe(false);
515
+ expect(resolution.candidateProjectIds).toEqual([]);
516
+ });
517
+
518
+ test("one usable row plus one projectId-less row resolves cleanly and is NOT ambiguous", async () => {
519
+ const projectId: ObjectID = ObjectID.generate();
520
+ const usableRow: WorkspaceProjectAuthToken = buildProjectAuthRow({
521
+ projectId: projectId,
522
+ });
523
+ mockFindBy([buildProjectAuthRow({ projectId: undefined }), usableRow]);
524
+
525
+ const resolution: MicrosoftTeamsTenantResolution =
526
+ await MicrosoftTeamsUtil.resolveProjectByTenantId({
527
+ tenantId: TENANT_ID,
528
+ });
529
+
530
+ // Filtering must happen BEFORE the count, otherwise this would be ambiguous.
531
+ expect(resolution.projectAuth).toBe(usableRow);
532
+ expect(resolution.isAmbiguous).toBe(false);
533
+ expect(
534
+ resolution.candidateProjectIds.map((candidate: ObjectID) => {
535
+ return candidate.toString();
536
+ }),
537
+ ).toEqual([projectId.toString()]);
538
+ });
539
+
540
+ test("the usable row wins regardless of which position it is in", async () => {
541
+ const projectId: ObjectID = ObjectID.generate();
542
+ const usableRow: WorkspaceProjectAuthToken = buildProjectAuthRow({
543
+ projectId: projectId,
544
+ });
545
+ mockFindBy([
546
+ usableRow,
547
+ buildProjectAuthRow({ projectId: null }),
548
+ buildProjectAuthRow({ projectId: undefined }),
549
+ ]);
550
+
551
+ const resolution: MicrosoftTeamsTenantResolution =
552
+ await MicrosoftTeamsUtil.resolveProjectByTenantId({
553
+ tenantId: TENANT_ID,
554
+ });
555
+
556
+ expect(resolution.projectAuth).toBe(usableRow);
557
+ expect(resolution.isAmbiguous).toBe(false);
558
+ });
559
+
560
+ test("only projectId-less rows count as not found even when there are several", async () => {
561
+ mockFindBy([
562
+ buildProjectAuthRow({ projectId: undefined }),
563
+ buildProjectAuthRow({ projectId: null }),
564
+ ]);
565
+
566
+ const resolution: MicrosoftTeamsTenantResolution =
567
+ await MicrosoftTeamsUtil.resolveProjectByTenantId({
568
+ tenantId: TENANT_ID,
569
+ });
570
+
571
+ expect(resolution.projectAuth).toBeNull();
572
+ expect(resolution.isAmbiguous).toBe(false);
573
+ expect(resolution.candidateProjectIds).toEqual([]);
574
+ });
575
+
576
+ test("two usable rows plus an unusable one are still ambiguous with two candidates", async () => {
577
+ const projectIdOne: ObjectID = ObjectID.generate();
578
+ const projectIdTwo: ObjectID = ObjectID.generate();
579
+ mockFindBy([
580
+ buildProjectAuthRow({ projectId: projectIdOne }),
581
+ buildProjectAuthRow({ projectId: undefined }),
582
+ buildProjectAuthRow({ projectId: projectIdTwo }),
583
+ ]);
584
+
585
+ const resolution: MicrosoftTeamsTenantResolution =
586
+ await MicrosoftTeamsUtil.resolveProjectByTenantId({
587
+ tenantId: TENANT_ID,
588
+ });
589
+
590
+ expect(resolution.projectAuth).toBeNull();
591
+ expect(resolution.isAmbiguous).toBe(true);
592
+ expect(
593
+ resolution.candidateProjectIds.map((candidate: ObjectID) => {
594
+ return candidate.toString();
595
+ }),
596
+ ).toEqual([projectIdOne.toString(), projectIdTwo.toString()]);
597
+ });
598
+ });
599
+
600
+ describe("MicrosoftTeamsUtil.resolveProjectByTenantId - the query it issues", () => {
601
+ test("queries MicrosoftTeams rows whose workspaceProjectId is the tenant id, as root", async () => {
602
+ const findBySpy: jest.SpyInstance = mockFindBy([]);
603
+
604
+ await MicrosoftTeamsUtil.resolveProjectByTenantId({
605
+ tenantId: "tenant-under-test",
606
+ });
607
+
608
+ expect(findBySpy).toHaveBeenCalledTimes(1);
609
+ const call: CapturedFindByCall = findBySpy.mock
610
+ .calls[0]?.[0] as CapturedFindByCall;
611
+ expect(call.query).toEqual({
612
+ workspaceType: WorkspaceType.MicrosoftTeams,
613
+ workspaceProjectId: "tenant-under-test",
614
+ });
615
+ expect(call.props).toEqual({ isRoot: true });
616
+ });
617
+
618
+ test("reads with LIMIT_MAX from offset 0 so a second connected project cannot hide behind the page size", async () => {
619
+ const findBySpy: jest.SpyInstance = mockFindBy([]);
620
+
621
+ await MicrosoftTeamsUtil.resolveProjectByTenantId({
622
+ tenantId: TENANT_ID,
623
+ });
624
+
625
+ const call: CapturedFindByCall = findBySpy.mock
626
+ .calls[0]?.[0] as CapturedFindByCall;
627
+ expect(call.limit).toBe(LIMIT_MAX);
628
+ expect(call.skip).toBe(0);
629
+ });
630
+
631
+ test("selects projectId so the caller can scope every downstream read", async () => {
632
+ const findBySpy: jest.SpyInstance = mockFindBy([]);
633
+
634
+ await MicrosoftTeamsUtil.resolveProjectByTenantId({
635
+ tenantId: TENANT_ID,
636
+ });
637
+
638
+ const call: CapturedFindByCall = findBySpy.mock
639
+ .calls[0]?.[0] as CapturedFindByCall;
640
+ expect(call.select["projectId"]).toBe(true);
641
+ });
642
+
643
+ test("uses findBy and NEVER findOneBy (findOneBy silently picked one of several projects)", async () => {
644
+ const findBySpy: jest.SpyInstance = mockFindBy([
645
+ buildProjectAuthRow(),
646
+ buildProjectAuthRow(),
647
+ ]);
648
+ const findOneBySpy: jest.SpyInstance = spyOnFindOneBy();
649
+
650
+ await MicrosoftTeamsUtil.resolveProjectByTenantId({
651
+ tenantId: TENANT_ID,
652
+ });
653
+
654
+ expect(findBySpy).toHaveBeenCalledTimes(1);
655
+ expect(findOneBySpy).not.toHaveBeenCalled();
656
+ });
657
+
658
+ test("an empty tenant id is still queried literally (no accidental match-all)", async () => {
659
+ const findBySpy: jest.SpyInstance = mockFindBy([]);
660
+
661
+ const resolution: MicrosoftTeamsTenantResolution =
662
+ await MicrosoftTeamsUtil.resolveProjectByTenantId({
663
+ tenantId: "",
664
+ });
665
+
666
+ const call: CapturedFindByCall = findBySpy.mock
667
+ .calls[0]?.[0] as CapturedFindByCall;
668
+ expect(call.query["workspaceProjectId"]).toBe("");
669
+ expect(resolution.projectAuth).toBeNull();
670
+ });
671
+
672
+ test("a rejected findBy propagates instead of resolving to some project", async () => {
673
+ jest
674
+ .spyOn(WorkspaceProjectAuthTokenService, "findBy")
675
+ .mockRejectedValue(new Error("db is down") as never);
676
+
677
+ await expect(
678
+ MicrosoftTeamsUtil.resolveProjectByTenantId({ tenantId: TENANT_ID }),
679
+ ).rejects.toThrow("db is down");
680
+ });
681
+ });
682
+
683
+ describe("MicrosoftTeamsUtil.getTenantResolutionFailureMessage", () => {
684
+ function ambiguousResolution(): MicrosoftTeamsTenantResolution {
685
+ return {
686
+ projectAuth: null,
687
+ isAmbiguous: true,
688
+ candidateProjectIds: [ObjectID.generate(), ObjectID.generate()],
689
+ };
690
+ }
691
+
692
+ function notFoundResolution(): MicrosoftTeamsTenantResolution {
693
+ return {
694
+ projectAuth: null,
695
+ isAmbiguous: false,
696
+ candidateProjectIds: [],
697
+ };
698
+ }
699
+
700
+ test("the ambiguous and not-found cases produce different messages", () => {
701
+ const ambiguousMessage: string =
702
+ MicrosoftTeamsUtil.getTenantResolutionFailureMessage(
703
+ ambiguousResolution(),
704
+ );
705
+ const notFoundMessage: string =
706
+ MicrosoftTeamsUtil.getTenantResolutionFailureMessage(
707
+ notFoundResolution(),
708
+ );
709
+
710
+ expect(ambiguousMessage).not.toBe(notFoundMessage);
711
+ });
712
+
713
+ test("the ambiguous message says the organization is connected to more than one project", () => {
714
+ const message: string =
715
+ MicrosoftTeamsUtil.getTenantResolutionFailureMessage(
716
+ ambiguousResolution(),
717
+ );
718
+
719
+ expect(message).toContain(AMBIGUOUS_FRAGMENT);
720
+ });
721
+
722
+ test("the not-found message does not claim ambiguity", () => {
723
+ const message: string =
724
+ MicrosoftTeamsUtil.getTenantResolutionFailureMessage(
725
+ notFoundResolution(),
726
+ );
727
+
728
+ expect(message).toContain(NOT_FOUND_FRAGMENT);
729
+ expect(message).not.toContain(AMBIGUOUS_FRAGMENT);
730
+ });
731
+
732
+ test("neither message leaks project ids to the Teams user", () => {
733
+ const candidateProjectId: ObjectID = ObjectID.generate();
734
+ const message: string =
735
+ MicrosoftTeamsUtil.getTenantResolutionFailureMessage({
736
+ projectAuth: null,
737
+ isAmbiguous: true,
738
+ candidateProjectIds: [candidateProjectId],
739
+ });
740
+
741
+ expect(message).not.toContain(candidateProjectId.toString());
742
+ });
743
+ });
744
+
745
+ describe("MicrosoftTeamsUtil.handleBotMessageActivity - tenant resolution", () => {
746
+ beforeEach(() => {
747
+ stubChatCapture();
748
+ });
749
+
750
+ test("a tenant that maps to exactly one project proceeds and sends no failure message", async () => {
751
+ mockFindBy([buildProjectAuthRow()]);
752
+ const context: FakeTurnContext = buildTurnContext();
753
+
754
+ await MicrosoftTeamsUtil.handleBotMessageActivity({
755
+ activity: messageActivity(),
756
+ turnContext: context.turnContext,
757
+ });
758
+
759
+ const sent: string = joinedSentText(context.sendActivity);
760
+ expect(sent).toContain(HELP_FRAGMENT);
761
+ expect(sent).not.toContain(AMBIGUOUS_FRAGMENT);
762
+ expect(sent).not.toContain(NOT_FOUND_FRAGMENT);
763
+ });
764
+
765
+ test("a tenant with no rows replies with the not-found message and does not answer the command", async () => {
766
+ mockFindBy([]);
767
+ const context: FakeTurnContext = buildTurnContext();
768
+
769
+ await MicrosoftTeamsUtil.handleBotMessageActivity({
770
+ activity: messageActivity(),
771
+ turnContext: context.turnContext,
772
+ });
773
+
774
+ expect(context.sendActivity).toHaveBeenCalledTimes(1);
775
+ const sent: string = joinedSentText(context.sendActivity);
776
+ expect(sent).toContain(NOT_FOUND_FRAGMENT);
777
+ expect(sent).not.toContain(HELP_FRAGMENT);
778
+ });
779
+
780
+ test("a tenant connected to two projects replies with the AMBIGUOUS message and serves nothing", async () => {
781
+ mockFindBy([buildProjectAuthRow(), buildProjectAuthRow()]);
782
+ const findOneBySpy: jest.SpyInstance = spyOnFindOneBy();
783
+ const context: FakeTurnContext = buildTurnContext();
784
+
785
+ await MicrosoftTeamsUtil.handleBotMessageActivity({
786
+ activity: messageActivity(),
787
+ turnContext: context.turnContext,
788
+ });
789
+
790
+ expect(context.sendActivity).toHaveBeenCalledTimes(1);
791
+ const sent: string = joinedSentText(context.sendActivity);
792
+ expect(sent).toContain(AMBIGUOUS_FRAGMENT);
793
+ // Refusing must not look like "not connected" - the remedies differ.
794
+ expect(sent).not.toContain(NOT_FOUND_FRAGMENT);
795
+ // ...and the user must not get the other project's answer.
796
+ expect(sent).not.toContain(HELP_FRAGMENT);
797
+ expect(findOneBySpy).not.toHaveBeenCalled();
798
+ });
799
+
800
+ test("a tenant whose only row has no projectId replies with the not-found message", async () => {
801
+ mockFindBy([buildProjectAuthRow({ projectId: undefined })]);
802
+ const context: FakeTurnContext = buildTurnContext();
803
+
804
+ await MicrosoftTeamsUtil.handleBotMessageActivity({
805
+ activity: messageActivity(),
806
+ turnContext: context.turnContext,
807
+ });
808
+
809
+ const sent: string = joinedSentText(context.sendActivity);
810
+ expect(sent).toContain(NOT_FOUND_FRAGMENT);
811
+ expect(sent).not.toContain(HELP_FRAGMENT);
812
+ });
813
+
814
+ test("the tenant id from channelData is the one resolved", async () => {
815
+ const resolveSpy: jest.SpyInstance = jest
816
+ .spyOn(MicrosoftTeamsUtil, "resolveProjectByTenantId")
817
+ .mockResolvedValue({
818
+ projectAuth: buildProjectAuthRow(),
819
+ isAmbiguous: false,
820
+ candidateProjectIds: [ObjectID.generate()],
821
+ });
822
+ const context: FakeTurnContext = buildTurnContext();
823
+
824
+ await MicrosoftTeamsUtil.handleBotMessageActivity({
825
+ activity: messageActivity({ tenantId: "tenant-from-activity" }),
826
+ turnContext: context.turnContext,
827
+ });
828
+
829
+ expect(resolveSpy).toHaveBeenCalledTimes(1);
830
+ expect(resolveSpy).toHaveBeenCalledWith({
831
+ tenantId: "tenant-from-activity",
832
+ });
833
+ });
834
+
835
+ test("a missing channelData.tenant.id replies about the organization and never resolves a tenant", async () => {
836
+ const resolveSpy: jest.SpyInstance = jest
837
+ .spyOn(MicrosoftTeamsUtil, "resolveProjectByTenantId")
838
+ .mockResolvedValue({
839
+ projectAuth: buildProjectAuthRow(),
840
+ isAmbiguous: false,
841
+ candidateProjectIds: [ObjectID.generate()],
842
+ });
843
+ const context: FakeTurnContext = buildTurnContext();
844
+
845
+ await MicrosoftTeamsUtil.handleBotMessageActivity({
846
+ activity: messageActivity({ omitTenant: true }),
847
+ turnContext: context.turnContext,
848
+ });
849
+
850
+ expect(resolveSpy).not.toHaveBeenCalled();
851
+ expect(context.sendActivity).toHaveBeenCalledTimes(1);
852
+ expect(joinedSentText(context.sendActivity)).toContain(NO_TENANT_FRAGMENT);
853
+ });
854
+ });
855
+
856
+ /*
857
+ * This is the disclosure vector itself.
858
+ *
859
+ * handleBotMessageActivity serves incidents, alerts, scheduled maintenance and
860
+ * AI assistant answers with NO per-user authorization at all - whoever is in
861
+ * the tenant gets whatever project the handler picked. So the project id it
862
+ * hands the downstream readers IS the entire access-control decision, and it
863
+ * must be exactly the projectId of the row resolveProjectByTenantId returned.
864
+ *
865
+ * Resolving the right tenant and then reading a different project's data would
866
+ * satisfy every other test in this file, which is why each reader is asserted
867
+ * on its own argument here.
868
+ */
869
+ describe("MicrosoftTeamsUtil.handleBotMessageActivity - the resolved project scopes every downstream read", () => {
870
+ beforeEach(() => {
871
+ stubChatCapture();
872
+ });
873
+
874
+ test('"show active incidents" reads incidents for the RESOLVED project', async () => {
875
+ const projectAuth: WorkspaceProjectAuthToken = buildProjectAuthRow({
876
+ projectId: ObjectID.generate(),
877
+ });
878
+ mockFindBy([projectAuth]);
879
+ const readerSpy: jest.SpyInstance = stubDownstreamReader(
880
+ "getActiveIncidentsMessage",
881
+ "incidents-of-the-resolved-project",
882
+ );
883
+ const context: FakeTurnContext = buildTurnContext();
884
+
885
+ await MicrosoftTeamsUtil.handleBotMessageActivity({
886
+ activity: messageActivity({ text: "show active incidents" }),
887
+ turnContext: context.turnContext,
888
+ });
889
+
890
+ expect(readerSpy).toHaveBeenCalledTimes(1);
891
+ expect(firstArgProjectId(readerSpy).toString()).toBe(
892
+ projectAuth.projectId?.toString(),
893
+ );
894
+ // ...and the answer that came back is what the user was actually served.
895
+ expect(joinedSentText(context.sendActivity)).toContain(
896
+ "incidents-of-the-resolved-project",
897
+ );
898
+ });
899
+
900
+ test('"show active alerts" reads alerts for the RESOLVED project', async () => {
901
+ const projectAuth: WorkspaceProjectAuthToken = buildProjectAuthRow({
902
+ projectId: ObjectID.generate(),
903
+ });
904
+ mockFindBy([projectAuth]);
905
+ const readerSpy: jest.SpyInstance = stubDownstreamReader(
906
+ "getActiveAlertsMessage",
907
+ "alerts-of-the-resolved-project",
908
+ );
909
+ const context: FakeTurnContext = buildTurnContext();
910
+
911
+ await MicrosoftTeamsUtil.handleBotMessageActivity({
912
+ activity: messageActivity({ text: "show active alerts" }),
913
+ turnContext: context.turnContext,
914
+ });
915
+
916
+ expect(readerSpy).toHaveBeenCalledTimes(1);
917
+ expect(firstArgProjectId(readerSpy).toString()).toBe(
918
+ projectAuth.projectId?.toString(),
919
+ );
920
+ expect(joinedSentText(context.sendActivity)).toContain(
921
+ "alerts-of-the-resolved-project",
922
+ );
923
+ });
924
+
925
+ test('"show scheduled maintenance" reads maintenance for the RESOLVED project', async () => {
926
+ const projectAuth: WorkspaceProjectAuthToken = buildProjectAuthRow({
927
+ projectId: ObjectID.generate(),
928
+ });
929
+ mockFindBy([projectAuth]);
930
+ const readerSpy: jest.SpyInstance = stubDownstreamReader(
931
+ "getScheduledMaintenanceMessage",
932
+ "maintenance-of-the-resolved-project",
933
+ );
934
+ const context: FakeTurnContext = buildTurnContext();
935
+
936
+ await MicrosoftTeamsUtil.handleBotMessageActivity({
937
+ activity: messageActivity({ text: "show scheduled maintenance" }),
938
+ turnContext: context.turnContext,
939
+ });
940
+
941
+ expect(readerSpy).toHaveBeenCalledTimes(1);
942
+ expect(firstArgProjectId(readerSpy).toString()).toBe(
943
+ projectAuth.projectId?.toString(),
944
+ );
945
+ expect(joinedSentText(context.sendActivity)).toContain(
946
+ "maintenance-of-the-resolved-project",
947
+ );
948
+ });
949
+
950
+ test("a free-form question asks the AI assistant about the RESOLVED project", async () => {
951
+ const question: string = "why did checkout latency spike last night";
952
+ const projectAuth: WorkspaceProjectAuthToken = buildProjectAuthRow({
953
+ projectId: ObjectID.generate(),
954
+ });
955
+ mockFindBy([projectAuth]);
956
+ const answerSpy: jest.SpyInstance = stubDownstreamReader(
957
+ "answerObservabilityQuestion",
958
+ );
959
+ const context: FakeTurnContext = buildTurnContext();
960
+
961
+ await MicrosoftTeamsUtil.handleBotMessageActivity({
962
+ activity: messageActivity({ text: question }),
963
+ turnContext: context.turnContext,
964
+ });
965
+
966
+ expect(answerSpy).toHaveBeenCalledTimes(1);
967
+ const answerArgs: { projectId: ObjectID; question: string } = answerSpy.mock
968
+ .calls[0]?.[0] as { projectId: ObjectID; question: string };
969
+ expect(answerArgs.projectId.toString()).toBe(
970
+ projectAuth.projectId?.toString(),
971
+ );
972
+ expect(answerArgs.question).toBe(question);
973
+ });
974
+
975
+ test("an AMBIGUOUS tenant reaches NONE of the readers", async () => {
976
+ mockFindBy([buildProjectAuthRow(), buildProjectAuthRow()]);
977
+ const incidentsSpy: jest.SpyInstance = stubDownstreamReader(
978
+ "getActiveIncidentsMessage",
979
+ "incidents",
980
+ );
981
+ const alertsSpy: jest.SpyInstance = stubDownstreamReader(
982
+ "getActiveAlertsMessage",
983
+ "alerts",
984
+ );
985
+ const maintenanceSpy: jest.SpyInstance = stubDownstreamReader(
986
+ "getScheduledMaintenanceMessage",
987
+ "maintenance",
988
+ );
989
+ const answerSpy: jest.SpyInstance = stubDownstreamReader(
990
+ "answerObservabilityQuestion",
991
+ );
992
+
993
+ const commands: Array<string> = [
994
+ "show active incidents",
995
+ "show active alerts",
996
+ "show scheduled maintenance",
997
+ "why did checkout latency spike last night",
998
+ ];
999
+
1000
+ for (const text of commands) {
1001
+ const context: FakeTurnContext = buildTurnContext();
1002
+
1003
+ await MicrosoftTeamsUtil.handleBotMessageActivity({
1004
+ activity: messageActivity({ text: text }),
1005
+ turnContext: context.turnContext,
1006
+ });
1007
+
1008
+ expect(joinedSentText(context.sendActivity)).toContain(
1009
+ AMBIGUOUS_FRAGMENT,
1010
+ );
1011
+ }
1012
+
1013
+ // Refusing has to stop the read, not just change the reply.
1014
+ expect(incidentsSpy).not.toHaveBeenCalled();
1015
+ expect(alertsSpy).not.toHaveBeenCalled();
1016
+ expect(maintenanceSpy).not.toHaveBeenCalled();
1017
+ expect(answerSpy).not.toHaveBeenCalled();
1018
+ });
1019
+ });
1020
+
1021
+ describe("MicrosoftTeamsUtil.handleBotInvokeActivity - tenant resolution", () => {
1022
+ test("the resolved project id is what the card click is authorized against", async () => {
1023
+ const projectId: ObjectID = ObjectID.generate();
1024
+ mockFindBy([buildProjectAuthRow({ projectId: projectId })]);
1025
+ const userLookupSpy: jest.SpyInstance = jest
1026
+ .spyOn(MicrosoftTeamsAuthAction, "getOneUptimeUserIdFromTeamsUserId")
1027
+ .mockResolvedValue(ObjectID.generate());
1028
+ const context: FakeTurnContext = buildTurnContext();
1029
+
1030
+ await MicrosoftTeamsUtil.handleBotInvokeActivity({
1031
+ activity: invokeActivity(),
1032
+ turnContext: context.turnContext,
1033
+ });
1034
+
1035
+ expect(userLookupSpy).toHaveBeenCalledTimes(1);
1036
+ const lookupArgs: { teamsUserId: string; projectId: ObjectID } =
1037
+ userLookupSpy.mock.calls[0]?.[0] as {
1038
+ teamsUserId: string;
1039
+ projectId: ObjectID;
1040
+ };
1041
+ expect(lookupArgs.teamsUserId).toBe(AAD_OBJECT_ID);
1042
+ expect(lookupArgs.projectId.toString()).toBe(projectId.toString());
1043
+ });
1044
+
1045
+ test("a tenant with no rows replies with the not-found message and never looks the user up", async () => {
1046
+ mockFindBy([]);
1047
+ const userLookupSpy: jest.SpyInstance = jest
1048
+ .spyOn(MicrosoftTeamsAuthAction, "getOneUptimeUserIdFromTeamsUserId")
1049
+ .mockResolvedValue(ObjectID.generate());
1050
+ const context: FakeTurnContext = buildTurnContext();
1051
+
1052
+ await MicrosoftTeamsUtil.handleBotInvokeActivity({
1053
+ activity: invokeActivity(),
1054
+ turnContext: context.turnContext,
1055
+ });
1056
+
1057
+ expect(context.sendActivity).toHaveBeenCalledTimes(1);
1058
+ expect(joinedSentText(context.sendActivity)).toContain(NOT_FOUND_FRAGMENT);
1059
+ expect(userLookupSpy).not.toHaveBeenCalled();
1060
+ });
1061
+
1062
+ test("a tenant connected to two projects replies with the AMBIGUOUS message and never resolves a project", async () => {
1063
+ mockFindBy([buildProjectAuthRow(), buildProjectAuthRow()]);
1064
+ const findOneBySpy: jest.SpyInstance = spyOnFindOneBy();
1065
+ const userLookupSpy: jest.SpyInstance = jest
1066
+ .spyOn(MicrosoftTeamsAuthAction, "getOneUptimeUserIdFromTeamsUserId")
1067
+ .mockResolvedValue(ObjectID.generate());
1068
+ const context: FakeTurnContext = buildTurnContext();
1069
+
1070
+ await MicrosoftTeamsUtil.handleBotInvokeActivity({
1071
+ activity: invokeActivity(),
1072
+ turnContext: context.turnContext,
1073
+ });
1074
+
1075
+ expect(context.sendActivity).toHaveBeenCalledTimes(1);
1076
+ const sent: string = joinedSentText(context.sendActivity);
1077
+ expect(sent).toContain(AMBIGUOUS_FRAGMENT);
1078
+ expect(sent).not.toContain(NOT_FOUND_FRAGMENT);
1079
+ expect(userLookupSpy).not.toHaveBeenCalled();
1080
+ expect(findOneBySpy).not.toHaveBeenCalled();
1081
+ });
1082
+
1083
+ test("a tenant whose only row has no projectId replies with the not-found message", async () => {
1084
+ mockFindBy([buildProjectAuthRow({ projectId: null })]);
1085
+ const userLookupSpy: jest.SpyInstance = jest
1086
+ .spyOn(MicrosoftTeamsAuthAction, "getOneUptimeUserIdFromTeamsUserId")
1087
+ .mockResolvedValue(ObjectID.generate());
1088
+ const context: FakeTurnContext = buildTurnContext();
1089
+
1090
+ await MicrosoftTeamsUtil.handleBotInvokeActivity({
1091
+ activity: invokeActivity(),
1092
+ turnContext: context.turnContext,
1093
+ });
1094
+
1095
+ expect(joinedSentText(context.sendActivity)).toContain(NOT_FOUND_FRAGMENT);
1096
+ expect(userLookupSpy).not.toHaveBeenCalled();
1097
+ });
1098
+
1099
+ test("the tenant id from channelData is the one resolved", async () => {
1100
+ const resolveSpy: jest.SpyInstance = jest
1101
+ .spyOn(MicrosoftTeamsUtil, "resolveProjectByTenantId")
1102
+ .mockResolvedValue({
1103
+ projectAuth: buildProjectAuthRow(),
1104
+ isAmbiguous: false,
1105
+ candidateProjectIds: [ObjectID.generate()],
1106
+ });
1107
+ jest
1108
+ .spyOn(MicrosoftTeamsAuthAction, "getOneUptimeUserIdFromTeamsUserId")
1109
+ .mockResolvedValue(ObjectID.generate());
1110
+ const context: FakeTurnContext = buildTurnContext();
1111
+
1112
+ await MicrosoftTeamsUtil.handleBotInvokeActivity({
1113
+ activity: invokeActivity({ tenantId: "tenant-from-invoke" }),
1114
+ turnContext: context.turnContext,
1115
+ });
1116
+
1117
+ expect(resolveSpy).toHaveBeenCalledTimes(1);
1118
+ expect(resolveSpy).toHaveBeenCalledWith({ tenantId: "tenant-from-invoke" });
1119
+ });
1120
+
1121
+ test("a missing channelData.tenant.id replies about the organization and never resolves a tenant", async () => {
1122
+ const resolveSpy: jest.SpyInstance = jest
1123
+ .spyOn(MicrosoftTeamsUtil, "resolveProjectByTenantId")
1124
+ .mockResolvedValue({
1125
+ projectAuth: buildProjectAuthRow(),
1126
+ isAmbiguous: false,
1127
+ candidateProjectIds: [ObjectID.generate()],
1128
+ });
1129
+ const userLookupSpy: jest.SpyInstance = jest
1130
+ .spyOn(MicrosoftTeamsAuthAction, "getOneUptimeUserIdFromTeamsUserId")
1131
+ .mockResolvedValue(ObjectID.generate());
1132
+ const context: FakeTurnContext = buildTurnContext();
1133
+
1134
+ await MicrosoftTeamsUtil.handleBotInvokeActivity({
1135
+ activity: invokeActivity({ omitTenant: true }),
1136
+ turnContext: context.turnContext,
1137
+ });
1138
+
1139
+ expect(resolveSpy).not.toHaveBeenCalled();
1140
+ expect(userLookupSpy).not.toHaveBeenCalled();
1141
+ expect(context.sendActivity).toHaveBeenCalledTimes(1);
1142
+ expect(joinedSentText(context.sendActivity)).toContain(NO_TENANT_FRAGMENT);
1143
+ });
1144
+
1145
+ test("an empty channelData.tenant object is treated as a missing tenant id", async () => {
1146
+ const resolveSpy: jest.SpyInstance = jest
1147
+ .spyOn(MicrosoftTeamsUtil, "resolveProjectByTenantId")
1148
+ .mockResolvedValue({
1149
+ projectAuth: buildProjectAuthRow(),
1150
+ isAmbiguous: false,
1151
+ candidateProjectIds: [ObjectID.generate()],
1152
+ });
1153
+ const activity: JSONObject = invokeActivity();
1154
+ (activity["channelData"] as JSONObject)["tenant"] = {};
1155
+ const context: FakeTurnContext = buildTurnContext();
1156
+
1157
+ await MicrosoftTeamsUtil.handleBotInvokeActivity({
1158
+ activity: activity,
1159
+ turnContext: context.turnContext,
1160
+ });
1161
+
1162
+ expect(resolveSpy).not.toHaveBeenCalled();
1163
+ expect(joinedSentText(context.sendActivity)).toContain(NO_TENANT_FRAGMENT);
1164
+ });
1165
+ });
1166
+
1167
+ describe("MicrosoftTeamsUtil bot entry points - both go through resolveProjectByTenantId", () => {
1168
+ test("the message path and the invoke path give the SAME ambiguous refusal", async () => {
1169
+ mockFindBy([buildProjectAuthRow(), buildProjectAuthRow()]);
1170
+ stubChatCapture();
1171
+ jest
1172
+ .spyOn(MicrosoftTeamsAuthAction, "getOneUptimeUserIdFromTeamsUserId")
1173
+ .mockResolvedValue(ObjectID.generate());
1174
+
1175
+ const messageContext: FakeTurnContext = buildTurnContext();
1176
+ await MicrosoftTeamsUtil.handleBotMessageActivity({
1177
+ activity: messageActivity(),
1178
+ turnContext: messageContext.turnContext,
1179
+ });
1180
+
1181
+ const invokeContext: FakeTurnContext = buildTurnContext();
1182
+ await MicrosoftTeamsUtil.handleBotInvokeActivity({
1183
+ activity: invokeActivity(),
1184
+ turnContext: invokeContext.turnContext,
1185
+ });
1186
+
1187
+ expect(joinedSentText(messageContext.sendActivity)).toBe(
1188
+ joinedSentText(invokeContext.sendActivity),
1189
+ );
1190
+ expect(joinedSentText(invokeContext.sendActivity)).toContain(
1191
+ AMBIGUOUS_FRAGMENT,
1192
+ );
1193
+ });
1194
+
1195
+ test("the message path and the invoke path give the SAME not-found refusal", async () => {
1196
+ mockFindBy([]);
1197
+ stubChatCapture();
1198
+
1199
+ const messageContext: FakeTurnContext = buildTurnContext();
1200
+ await MicrosoftTeamsUtil.handleBotMessageActivity({
1201
+ activity: messageActivity(),
1202
+ turnContext: messageContext.turnContext,
1203
+ });
1204
+
1205
+ const invokeContext: FakeTurnContext = buildTurnContext();
1206
+ await MicrosoftTeamsUtil.handleBotInvokeActivity({
1207
+ activity: invokeActivity(),
1208
+ turnContext: invokeContext.turnContext,
1209
+ });
1210
+
1211
+ expect(joinedSentText(messageContext.sendActivity)).toBe(
1212
+ joinedSentText(invokeContext.sendActivity),
1213
+ );
1214
+ expect(joinedSentText(invokeContext.sendActivity)).toContain(
1215
+ NOT_FOUND_FRAGMENT,
1216
+ );
1217
+ });
1218
+ });