@oneuptime/common 12.0.11 → 12.0.13

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 (251) hide show
  1. package/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.ts +32 -5
  2. package/Models/DatabaseModels/NetworkDevice.ts +64 -0
  3. package/Models/DatabaseModels/StatusPage.ts +59 -0
  4. package/Models/DatabaseModels/StatusPageOidc.ts +2 -0
  5. package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +19 -2
  6. package/Server/API/AIChatAPI.ts +55 -56
  7. package/Server/API/CommonAPI.ts +89 -0
  8. package/Server/API/MicrosoftTeamsAPI.ts +20 -9
  9. package/Server/API/SlackAPI.ts +13 -10
  10. package/Server/API/StatusPageAPI.ts +2197 -2128
  11. package/Server/API/TelemetryAPI.ts +72 -3
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1787500000000-AddEnableSearchEngineIndexingToStatusPage.ts +30 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1787600000000-AddNetworkDeviceReachabilityColumns.ts +47 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  15. package/Server/Middleware/UserAuthorization.ts +11 -2
  16. package/Server/Services/ApiKeyPermissionService.ts +116 -2
  17. package/Server/Services/DatabaseService.ts +17 -6
  18. package/Server/Services/LogAggregationService.ts +34 -0
  19. package/Server/Services/LogService.ts +21 -0
  20. package/Server/Services/NetworkSiteService.ts +12 -0
  21. package/Server/Services/SpanService.ts +21 -0
  22. package/Server/Services/TraceAggregationService.ts +15 -0
  23. package/Server/Services/WorkspaceNotificationRuleService.ts +172 -220
  24. package/Server/Types/AnalyticsDatabase/ModelPermission.ts +9 -0
  25. package/Server/Types/Database/Permissions/AccessControlPermission.ts +47 -16
  26. package/Server/Types/Workflow/Components/Conditions/IfElse.ts +9 -0
  27. package/Server/Types/Workflow/Components/JavaScript.ts +9 -0
  28. package/Server/Utils/APIKey/AccessPermission.ts +16 -40
  29. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +144 -0
  30. package/Server/Utils/LogRedaction.ts +576 -0
  31. package/Server/Utils/Logger.ts +123 -46
  32. package/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.ts +94 -23
  33. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +12 -0
  34. package/Server/Utils/Monitor/MonitorResource.ts +29 -9
  35. package/Server/Utils/Monitor/NetworkInventoryUtil.ts +28 -7
  36. package/Server/Utils/SessionReplay/SessionReplayGateCacheStore.ts +56 -10
  37. package/Server/Utils/StartServer.ts +30 -9
  38. package/Server/Utils/StatusPageSearchEngineIndexing.ts +33 -0
  39. package/Server/Utils/Telemetry/ResourceEntityFilter.ts +441 -0
  40. package/Server/Utils/VM/VMRunner.ts +693 -874
  41. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +488 -46
  42. package/Tests/App/Dashboard/MonitorTypePicker.test.tsx +300 -0
  43. package/Tests/App/StatusPage/StatusPageIndexPageRobotsMeta.test.ts +130 -0
  44. package/Tests/Models/DatabaseModels/DatabaseBaseModelToJSONObjectCache.test.ts +180 -0
  45. package/Tests/Models/StatusPageEnableSearchEngineIndexing.test.ts +157 -0
  46. package/Tests/Server/API/AIChatCancelAndFeedback.test.ts +23 -1
  47. package/Tests/Server/API/AIChatRouteAuthorization.test.ts +785 -0
  48. package/Tests/Server/API/BaseAPIApiKeyAuth.test.ts +21 -8
  49. package/Tests/Server/API/CommonAPIAuthGuard.test.ts +290 -0
  50. package/Tests/Server/API/MicrosoftTeamsManifest.test.ts +7 -1
  51. package/Tests/Server/API/StatusPageOverviewCache.test.ts +474 -0
  52. package/Tests/Server/API/StatusPageSeoSearchEngineIndexing.test.ts +189 -0
  53. package/Tests/Server/Infrastructure/InMemoryTTLCache.test.ts +158 -0
  54. package/Tests/Server/Infrastructure/Postgres/DeviceRoleAndDeclaredLinkParentMigration.test.ts +9 -60
  55. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +24 -15
  56. package/Tests/Server/Middleware/UserAuthorization.test.ts +79 -0
  57. package/Tests/Server/Services/AddNetworkDeviceReachabilityColumnsMigration.test.ts +332 -0
  58. package/Tests/Server/Services/ApiKeyPermissionService.test.ts +347 -0
  59. package/Tests/Server/Services/DatabaseServiceUpdateDebugLogging.test.ts +282 -0
  60. package/Tests/Server/Services/NetworkSiteService.test.ts +56 -2
  61. package/Tests/Server/Services/TelemetryResourceFacetFilters.test.ts +267 -0
  62. package/Tests/Server/Services/WorkspaceNotificationRuleTestRuleChannels.test.ts +702 -0
  63. package/Tests/Server/Types/Database/Permissions/AccessControlPermission.test.ts +165 -0
  64. package/Tests/Server/Types/Workflow/Components/CustomCodeScriptError.test.ts +121 -0
  65. package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +48 -36
  66. package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +213 -0
  67. package/Tests/Server/Utils/LogRedaction.test.ts +415 -0
  68. package/Tests/Server/Utils/LoggerCredentialLeak.test.ts +245 -0
  69. package/Tests/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.test.ts +374 -4
  70. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorJavascriptExpression.test.ts +92 -0
  71. package/Tests/Server/Utils/Monitor/MonitorResourceIngestedStepSelection.test.ts +455 -0
  72. package/Tests/Server/Utils/Monitor/NetworkDeviceReachabilityRoundTrip.test.ts +369 -0
  73. package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +86 -8
  74. package/Tests/Server/Utils/PrivacyFilterUtil.test.ts +64 -0
  75. package/Tests/Server/Utils/SessionReplay/SessionReplayGateCacheStore.test.ts +207 -0
  76. package/Tests/Server/Utils/StartServerBodyVerify.test.ts +335 -0
  77. package/Tests/Server/Utils/StatusPageSearchEngineIndexing.test.ts +60 -0
  78. package/Tests/Server/Utils/Telemetry/ResourceEntityFilter.test.ts +435 -0
  79. package/Tests/Server/Utils/VM/VMRunnerIsolation.test.ts +385 -0
  80. package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +3 -0
  81. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChannelSend.test.ts +607 -62
  82. package/Tests/Server/Utils/Workspace/MicrosoftTeamsInstalledTeamIdSpace.test.ts +778 -0
  83. package/Tests/Server/Utils/Workspace/MicrosoftTeamsTeamInstalls.test.ts +44 -10
  84. package/Tests/Types/API/HostnameFromAuthority.test.ts +164 -0
  85. package/Tests/Types/Database/AccessControl/ColumnAccessControlCache.test.ts +344 -0
  86. package/Tests/Types/Database/DatabasePropertyFindOperator.test.ts +518 -0
  87. package/Tests/Types/Database/TableColumnMetadataCache.test.ts +286 -0
  88. package/Tests/Types/Monitor/MonitorStep.test.ts +100 -0
  89. package/Tests/Types/Monitor/MonitorTypeKeywords.test.ts +360 -0
  90. package/Tests/Types/StatusPage/SearchEngineIndexing.test.ts +86 -0
  91. package/Tests/Types/Telemetry/ResourceEntityFacet.test.ts +254 -0
  92. package/Tests/UI/Components/CardSelect.test.tsx +1021 -0
  93. package/Tests/UI/Components/MasterPage.test.tsx +36 -1
  94. package/Tests/Utils/Monitor/MonitorMetricType.test.ts +66 -1
  95. package/Tests/Utils/Monitor/NetworkTopologyUtil.test.ts +115 -6
  96. package/Tests/Utils/NetworkDevice/DeviceReachabilityUtil.test.ts +610 -0
  97. package/Tests/Utils/NetworkSite/SiteStatusRollupUtil.test.ts +114 -45
  98. package/Types/API/Hostname.ts +79 -0
  99. package/Types/Database/AccessControl/ColumnAccessControl.ts +33 -11
  100. package/Types/Database/AccessControl/ColumnBillingAccessControl.ts +33 -11
  101. package/Types/Database/DatabaseProperty.ts +38 -0
  102. package/Types/Database/TableColumn.ts +34 -7
  103. package/Types/Events/Recurring.ts +16 -1
  104. package/Types/Monitor/CustomCodeMonitor/CustomCodeMonitorResponse.ts +14 -2
  105. package/Types/Monitor/MonitorStep.ts +13 -1
  106. package/Types/Monitor/MonitorType.ts +345 -40
  107. package/Types/Monitor/SSLMonitor/SslMonitorResponse.ts +43 -0
  108. package/Types/StatusPage/SearchEngineIndexing.ts +61 -0
  109. package/Types/Telemetry/ResourceEntityFacet.ts +211 -0
  110. package/UI/Components/CardSelect/CardSelect.tsx +714 -105
  111. package/UI/Components/EmptyState/EmptyState.tsx +8 -1
  112. package/UI/Components/Footer/Footer.tsx +32 -6
  113. package/UI/Components/Forms/Fields/FormField.tsx +3 -0
  114. package/UI/Components/Forms/Types/Field.ts +8 -0
  115. package/UI/Components/Loader/PageLoader.tsx +29 -8
  116. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +26 -4
  117. package/UI/Components/Markdown.tsx/LazyMarkdownViewer.tsx +17 -1
  118. package/UI/Components/MasterPage/MasterPage.tsx +24 -11
  119. package/Utils/Monitor/MonitorMetricType.ts +16 -1
  120. package/Utils/Monitor/NetworkTopologyUtil.ts +60 -8
  121. package/Utils/NetworkDevice/DeviceReachabilityUtil.ts +262 -0
  122. package/Utils/NetworkSite/SiteStatusRollupUtil.ts +41 -24
  123. package/Utils/Telemetry/CrossSignalScope.ts +75 -4
  124. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js +20 -4
  125. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js.map +1 -1
  126. package/build/dist/Models/DatabaseModels/NetworkDevice.js +66 -0
  127. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  128. package/build/dist/Models/DatabaseModels/StatusPage.js +60 -0
  129. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  130. package/build/dist/Models/DatabaseModels/StatusPageOidc.js +2 -0
  131. package/build/dist/Models/DatabaseModels/StatusPageOidc.js.map +1 -1
  132. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
  133. package/build/dist/Server/API/AIChatAPI.js +48 -35
  134. package/build/dist/Server/API/AIChatAPI.js.map +1 -1
  135. package/build/dist/Server/API/CommonAPI.js +59 -0
  136. package/build/dist/Server/API/CommonAPI.js.map +1 -1
  137. package/build/dist/Server/API/MicrosoftTeamsAPI.js +20 -6
  138. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  139. package/build/dist/Server/API/SlackAPI.js +13 -10
  140. package/build/dist/Server/API/SlackAPI.js.map +1 -1
  141. package/build/dist/Server/API/StatusPageAPI.js +745 -673
  142. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  143. package/build/dist/Server/API/TelemetryAPI.js +34 -3
  144. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  145. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787500000000-AddEnableSearchEngineIndexingToStatusPage.js +23 -0
  146. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787500000000-AddEnableSearchEngineIndexingToStatusPage.js.map +1 -0
  147. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787600000000-AddNetworkDeviceReachabilityColumns.js +34 -0
  148. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787600000000-AddNetworkDeviceReachabilityColumns.js.map +1 -0
  149. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  150. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  151. package/build/dist/Server/Middleware/UserAuthorization.js +6 -2
  152. package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
  153. package/build/dist/Server/Services/ApiKeyPermissionService.js +90 -1
  154. package/build/dist/Server/Services/ApiKeyPermissionService.js.map +1 -1
  155. package/build/dist/Server/Services/DatabaseService.js +15 -6
  156. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  157. package/build/dist/Server/Services/LogAggregationService.js +2 -0
  158. package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
  159. package/build/dist/Server/Services/LogService.js +16 -0
  160. package/build/dist/Server/Services/LogService.js.map +1 -1
  161. package/build/dist/Server/Services/NetworkSiteService.js +12 -0
  162. package/build/dist/Server/Services/NetworkSiteService.js.map +1 -1
  163. package/build/dist/Server/Services/SpanService.js +16 -0
  164. package/build/dist/Server/Services/SpanService.js.map +1 -1
  165. package/build/dist/Server/Services/TraceAggregationService.js +2 -0
  166. package/build/dist/Server/Services/TraceAggregationService.js.map +1 -1
  167. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js +137 -164
  168. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js.map +1 -1
  169. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js +9 -0
  170. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
  171. package/build/dist/Server/Types/Database/Permissions/AccessControlPermission.js +21 -13
  172. package/build/dist/Server/Types/Database/Permissions/AccessControlPermission.js.map +1 -1
  173. package/build/dist/Server/Types/Workflow/Components/Conditions/IfElse.js +8 -0
  174. package/build/dist/Server/Types/Workflow/Components/Conditions/IfElse.js.map +1 -1
  175. package/build/dist/Server/Types/Workflow/Components/JavaScript.js +8 -0
  176. package/build/dist/Server/Types/Workflow/Components/JavaScript.js.map +1 -1
  177. package/build/dist/Server/Utils/APIKey/AccessPermission.js +9 -32
  178. package/build/dist/Server/Utils/APIKey/AccessPermission.js.map +1 -1
  179. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +87 -0
  180. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  181. package/build/dist/Server/Utils/LogRedaction.js +449 -0
  182. package/build/dist/Server/Utils/LogRedaction.js.map +1 -0
  183. package/build/dist/Server/Utils/Logger.js +98 -45
  184. package/build/dist/Server/Utils/Logger.js.map +1 -1
  185. package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js +65 -13
  186. package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js.map +1 -1
  187. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +12 -1
  188. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  189. package/build/dist/Server/Utils/Monitor/MonitorResource.js +20 -3
  190. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  191. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +27 -7
  192. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
  193. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCacheStore.js +29 -10
  194. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCacheStore.js.map +1 -1
  195. package/build/dist/Server/Utils/StartServer.js +10 -7
  196. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  197. package/build/dist/Server/Utils/StatusPageSearchEngineIndexing.js +24 -0
  198. package/build/dist/Server/Utils/StatusPageSearchEngineIndexing.js.map +1 -0
  199. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js +311 -0
  200. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js.map +1 -0
  201. package/build/dist/Server/Utils/VM/VMRunner.js +402 -525
  202. package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
  203. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +366 -37
  204. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  205. package/build/dist/Types/API/Hostname.js +63 -0
  206. package/build/dist/Types/API/Hostname.js.map +1 -1
  207. package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js +20 -6
  208. package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js.map +1 -1
  209. package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js +20 -6
  210. package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js.map +1 -1
  211. package/build/dist/Types/Database/DatabaseProperty.js +38 -0
  212. package/build/dist/Types/Database/DatabaseProperty.js.map +1 -1
  213. package/build/dist/Types/Database/TableColumn.js +24 -6
  214. package/build/dist/Types/Database/TableColumn.js.map +1 -1
  215. package/build/dist/Types/Events/Recurring.js +13 -1
  216. package/build/dist/Types/Events/Recurring.js.map +1 -1
  217. package/build/dist/Types/Monitor/MonitorStep.js +9 -1
  218. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  219. package/build/dist/Types/Monitor/MonitorType.js +330 -31
  220. package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
  221. package/build/dist/Types/StatusPage/SearchEngineIndexing.js +55 -0
  222. package/build/dist/Types/StatusPage/SearchEngineIndexing.js.map +1 -0
  223. package/build/dist/Types/Telemetry/ResourceEntityFacet.js +156 -0
  224. package/build/dist/Types/Telemetry/ResourceEntityFacet.js.map +1 -0
  225. package/build/dist/UI/Components/CardSelect/CardSelect.js +360 -40
  226. package/build/dist/UI/Components/CardSelect/CardSelect.js.map +1 -1
  227. package/build/dist/UI/Components/EmptyState/EmptyState.js +1 -1
  228. package/build/dist/UI/Components/EmptyState/EmptyState.js.map +1 -1
  229. package/build/dist/UI/Components/Footer/Footer.js +7 -4
  230. package/build/dist/UI/Components/Footer/Footer.js.map +1 -1
  231. package/build/dist/UI/Components/Forms/Fields/FormField.js +1 -1
  232. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  233. package/build/dist/UI/Components/Loader/PageLoader.js +15 -5
  234. package/build/dist/UI/Components/Loader/PageLoader.js.map +1 -1
  235. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +16 -3
  236. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  237. package/build/dist/UI/Components/Markdown.tsx/LazyMarkdownViewer.js +13 -1
  238. package/build/dist/UI/Components/Markdown.tsx/LazyMarkdownViewer.js.map +1 -1
  239. package/build/dist/UI/Components/MasterPage/MasterPage.js +10 -2
  240. package/build/dist/UI/Components/MasterPage/MasterPage.js.map +1 -1
  241. package/build/dist/Utils/Monitor/MonitorMetricType.js +13 -1
  242. package/build/dist/Utils/Monitor/MonitorMetricType.js.map +1 -1
  243. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +34 -7
  244. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  245. package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js +177 -0
  246. package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js.map +1 -0
  247. package/build/dist/Utils/NetworkSite/SiteStatusRollupUtil.js +23 -27
  248. package/build/dist/Utils/NetworkSite/SiteStatusRollupUtil.js.map +1 -1
  249. package/build/dist/Utils/Telemetry/CrossSignalScope.js +44 -4
  250. package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -1
  251. package/package.json +1 -1
@@ -72,6 +72,7 @@ import {
72
72
  ConfigurationBotFrameworkAuthentication,
73
73
  TeamsActivityHandler,
74
74
  TeamsInfo,
75
+ TeamDetails,
75
76
  TeamsChannelAccount,
76
77
  TeamsPagedMembersResult,
77
78
  TurnContext,
@@ -125,6 +126,20 @@ export interface MicrosoftTeamsTenantResolution {
125
126
  candidateProjectIds: Array<ObjectID>;
126
127
  }
127
128
 
129
+ /*
130
+ * Whether the OneUptime Teams app is installed in a given team.
131
+ *
132
+ * Unknown is a first-class outcome, not an error: checking installation needs a
133
+ * Graph permission that deployments connected before it was documented have not
134
+ * consented to, and a tenant that answers "I won't tell you" must never be
135
+ * reported to an admin as "the app is not installed".
136
+ */
137
+ export enum MicrosoftTeamsAppInstallState {
138
+ Installed = "Installed",
139
+ NotInstalled = "NotInstalled",
140
+ Unknown = "Unknown",
141
+ }
142
+
128
143
  /*
129
144
  * Microsoft's wording when the Bot Framework refuses a proactive post because
130
145
  * the app is not a member of the target conversation. Matched case-insensitively
@@ -1443,6 +1458,14 @@ export default class MicrosoftTeamsUtil extends WorkspaceBase {
1443
1458
  logger.debug(`Team ID: ${data.teamId}`);
1444
1459
  logger.debug(`Adaptive card: ${JSON.stringify(data.adaptiveCard)}`);
1445
1460
 
1461
+ /*
1462
+ * Declared out here so the catch block can tell a rejection we predicted
1463
+ * from one we could not, and word the error accordingly.
1464
+ */
1465
+ let installState: MicrosoftTeamsAppInstallState =
1466
+ MicrosoftTeamsAppInstallState.Unknown;
1467
+ let installStateCameFromGraph: boolean = false;
1468
+
1446
1469
  try {
1447
1470
  // Get project auth to retrieve bot ID
1448
1471
  const projectAuth: WorkspaceProjectAuthToken | null =
@@ -1493,30 +1516,42 @@ export default class MicrosoftTeamsUtil extends WorkspaceBase {
1493
1516
  }
1494
1517
 
1495
1518
  /*
1496
- * Preflight: refuse before calling the Bot Framework when we know the
1497
- * app was never installed into this team. Channels are discovered with
1498
- * tenant-wide Graph application permissions, which see every team
1499
- * regardless of installation — so reaching this point proves nothing
1500
- * about whether we can actually post.
1519
+ * Preflight. Channels are discovered with tenant-wide Graph application
1520
+ * permissions, which see every team regardless of installation so
1521
+ * reaching this point proves nothing about whether we can actually post.
1501
1522
  *
1502
- * installedTeams is only populated from install events, so an empty map
1503
- * means "we have not observed any installs" (for example on a workspace
1504
- * connected before this shipped), not "nothing is installed". Only
1505
- * enforce once we have at least one record, and let the Bot Framework
1506
- * error be translated below otherwise.
1523
+ * An install we recorded from a bot activity is the cheap positive
1524
+ * signal, and it also carries the serviceUrl this send needs. When we
1525
+ * have no record we ask Graph rather than assuming: absence from
1526
+ * installedTeams used to be reported to admins as "the app is not
1527
+ * installed", which is a claim install events alone cannot support (they
1528
+ * only arrive for installs that happened while OneUptime was reachable).
1529
+ * Only a verified negative refuses the send; anything we cannot verify
1530
+ * goes to Microsoft, which is the real authority.
1507
1531
  */
1508
- const installedTeams: Record<string, MicrosoftTeamsInstalledTeam> =
1509
- miscData.installedTeams || {};
1510
1532
  const installedTeam: MicrosoftTeamsInstalledTeam | undefined =
1511
- installedTeams[data.teamId];
1533
+ this.indexInstalledTeamsByGraphTeamId(miscData.installedTeams)[
1534
+ data.teamId
1535
+ ];
1512
1536
 
1513
- if (Object.keys(installedTeams).length > 0 && !installedTeam) {
1514
- throw new BadDataException(
1515
- this.getBotNotInTeamMessage({
1516
- channelName: data.workspaceChannel.name,
1517
- membershipType: data.workspaceChannel.membershipType,
1518
- }),
1519
- );
1537
+ if (installedTeam) {
1538
+ installState = MicrosoftTeamsAppInstallState.Installed;
1539
+ } else {
1540
+ installState = await this.isAppInstalledInTeam({
1541
+ authToken: data.authToken,
1542
+ projectId: data.projectId,
1543
+ teamId: data.teamId,
1544
+ });
1545
+ installStateCameFromGraph = true;
1546
+
1547
+ if (installState === MicrosoftTeamsAppInstallState.NotInstalled) {
1548
+ throw new BadDataException(
1549
+ this.getBotNotInTeamMessage({
1550
+ channelName: data.workspaceChannel.name,
1551
+ membershipType: data.workspaceChannel.membershipType,
1552
+ }),
1553
+ );
1554
+ }
1520
1555
  }
1521
1556
 
1522
1557
  // Get Bot Framework adapter
@@ -1592,14 +1627,47 @@ export default class MicrosoftTeamsUtil extends WorkspaceBase {
1592
1627
 
1593
1628
  /*
1594
1629
  * Microsoft's roster rejection is meaningless to an admin ("The bot is
1595
- * not part of the conversation roster"). Replace it with the action that
1596
- * actually fixes it.
1630
+ * not part of the conversation roster"). Replace it with something
1631
+ * actionable but only claim the app is missing from the team when we
1632
+ * checked and it is. Otherwise the app can be installed exactly as
1633
+ * documented and still be told to install it, which is how this error
1634
+ * sent admins in circles.
1597
1635
  */
1598
1636
  if (this.isBotNotInConversationRosterError(error)) {
1637
+ /*
1638
+ * A local install record got us past the preflight without asking
1639
+ * Graph, and it can be stale — the app may have been removed from the
1640
+ * team since, and an uninstall event only reaches us if OneUptime was
1641
+ * reachable at the time. So confirm before wording the error. This is
1642
+ * the failure path, so the extra call costs nothing in normal operation.
1643
+ */
1644
+ let verifiedInstallState: MicrosoftTeamsAppInstallState = installState;
1645
+
1646
+ if (!installStateCameFromGraph) {
1647
+ verifiedInstallState = await this.isAppInstalledInTeam({
1648
+ authToken: data.authToken,
1649
+ projectId: data.projectId,
1650
+ teamId: data.teamId,
1651
+ });
1652
+ }
1653
+
1654
+ if (
1655
+ verifiedInstallState === MicrosoftTeamsAppInstallState.NotInstalled
1656
+ ) {
1657
+ throw new BadDataException(
1658
+ this.getBotNotInTeamMessage({
1659
+ channelName: data.workspaceChannel.name,
1660
+ membershipType: data.workspaceChannel.membershipType,
1661
+ }),
1662
+ );
1663
+ }
1664
+
1599
1665
  throw new BadDataException(
1600
- this.getBotNotInTeamMessage({
1666
+ this.getRosterRejectionMessage({
1601
1667
  channelName: data.workspaceChannel.name,
1602
1668
  membershipType: data.workspaceChannel.membershipType,
1669
+ installState: verifiedInstallState,
1670
+ microsoftError: error,
1603
1671
  }),
1604
1672
  );
1605
1673
  }
@@ -1642,6 +1710,138 @@ export default class MicrosoftTeamsUtil extends WorkspaceBase {
1642
1710
  return `The OneUptime app is not installed in the Microsoft Teams team that owns "${data.channelName}". In Microsoft Teams, click the "..." next to the team name, then Manage team > Apps > More apps, and add OneUptime. Installing OneUptime for yourself or in a chat is not the same as adding it to the team.`;
1643
1711
  }
1644
1712
 
1713
+ /*
1714
+ * Microsoft rejected the post and we could not confirm why. Everything here
1715
+ * is a real cause of that rejection for an app that IS present in the team's
1716
+ * app list, which is the case getBotNotInTeamMessage got wrong: the app tile
1717
+ * showing up in Manage team > Apps does not mean the bot behind it is the one
1718
+ * this deployment authenticates as. Microsoft's own wording is included so
1719
+ * the raw error is not lost in the rewrite.
1720
+ */
1721
+ public static getRosterRejectionMessage(data: {
1722
+ channelName: string;
1723
+ membershipType?: string | undefined;
1724
+ installState: MicrosoftTeamsAppInstallState;
1725
+ microsoftError?: unknown;
1726
+ }): string {
1727
+ const causes: Array<string> = [];
1728
+
1729
+ if (data.membershipType === "private") {
1730
+ causes.push(
1731
+ `"${data.channelName}" is a private channel, which needs the app installed into the channel itself (channel "..." > Manage channel > Apps > Add an app) — a team-level install does not cover it`,
1732
+ );
1733
+ } else if (data.installState === MicrosoftTeamsAppInstallState.Installed) {
1734
+ causes.push(
1735
+ "the app is installed in this team, so the bot behind it may not be the one this OneUptime deployment uses — check that the Teams app package was built from this deployment (Project Settings > Workspace > Microsoft Teams) and that its bot id matches MICROSOFT_TEAMS_APP_CLIENT_ID",
1736
+ );
1737
+ } else {
1738
+ causes.push(
1739
+ 'the OneUptime app has not been added to this team (team "..." > Manage team > Apps > More apps), or the app that was added is a different package whose bot id is not this deployment\'s MICROSOFT_TEAMS_APP_CLIENT_ID',
1740
+ );
1741
+ }
1742
+
1743
+ causes.push(
1744
+ "the Azure Bot resource for this deployment does not have the Microsoft Teams channel enabled",
1745
+ );
1746
+
1747
+ const microsoftMessage: string =
1748
+ data.microsoftError instanceof Error
1749
+ ? data.microsoftError.message
1750
+ : String(data.microsoftError || "");
1751
+
1752
+ const suffix: string = microsoftMessage
1753
+ ? ` Microsoft's response was: "${microsoftMessage}"`
1754
+ : "";
1755
+
1756
+ return `Microsoft Teams refused the message to "${data.channelName}" because the OneUptime bot is not a member of that conversation. Likely causes: ${causes.join("; ")}.${suffix}`;
1757
+ }
1758
+
1759
+ /*
1760
+ * Asks Graph whether this deployment's Teams app is installed in a team.
1761
+ *
1762
+ * Matching is on teamsApp.externalId, which is the manifest id — and the
1763
+ * manifest OneUptime generates sets both that and the bot id to
1764
+ * MICROSOFT_TEAMS_APP_CLIENT_ID. So this answers the question that actually
1765
+ * matters ("is the app THIS deployment authenticates as installed here?"),
1766
+ * not the weaker one an admin can answer by eye ("is something called
1767
+ * OneUptime in the app list?").
1768
+ *
1769
+ * Every failure is Unknown. The call needs TeamsAppInstallation.ReadForTeam.All,
1770
+ * which deployments connected before it was documented have not granted, and a
1771
+ * missing permission must not be reported as a missing install.
1772
+ */
1773
+ @CaptureSpan()
1774
+ public static async isAppInstalledInTeam(data: {
1775
+ authToken: string;
1776
+ projectId: ObjectID;
1777
+ teamId: string;
1778
+ }): Promise<MicrosoftTeamsAppInstallState> {
1779
+ if (!MicrosoftTeamsAppClientId) {
1780
+ return MicrosoftTeamsAppInstallState.Unknown;
1781
+ }
1782
+
1783
+ try {
1784
+ const accessToken: string = await this.getValidAccessToken({
1785
+ authToken: data.authToken,
1786
+ projectId: data.projectId,
1787
+ });
1788
+
1789
+ let nextLink: string | null =
1790
+ `https://graph.microsoft.com/v1.0/teams/${data.teamId}/installedApps?$expand=teamsApp`;
1791
+ let pageCount: number = 0;
1792
+
1793
+ while (nextLink) {
1794
+ pageCount++;
1795
+
1796
+ if (pageCount > MICROSOFT_TEAMS_MAX_PAGES) {
1797
+ logger.warn(
1798
+ `Stopped paginating installed apps for team ${data.teamId} after ${MICROSOFT_TEAMS_MAX_PAGES} pages.`,
1799
+ );
1800
+ return MicrosoftTeamsAppInstallState.Unknown;
1801
+ }
1802
+
1803
+ const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
1804
+ await API.get({
1805
+ url: URL.fromString(nextLink),
1806
+ headers: {
1807
+ Authorization: `Bearer ${accessToken}`,
1808
+ "Content-Type": "application/json",
1809
+ },
1810
+ });
1811
+
1812
+ if (response instanceof HTTPErrorResponse) {
1813
+ logger.debug(
1814
+ `Could not read installed apps for team ${data.teamId}; treating installation as unknown.`,
1815
+ );
1816
+ logger.debug(response);
1817
+ return MicrosoftTeamsAppInstallState.Unknown;
1818
+ }
1819
+
1820
+ const installedApps: Array<JSONObject> =
1821
+ (response.data["value"] as Array<JSONObject>) || [];
1822
+
1823
+ for (const installedApp of installedApps) {
1824
+ const teamsApp: JSONObject =
1825
+ (installedApp["teamsApp"] as JSONObject) || {};
1826
+
1827
+ if (teamsApp["externalId"] === MicrosoftTeamsAppClientId) {
1828
+ return MicrosoftTeamsAppInstallState.Installed;
1829
+ }
1830
+ }
1831
+
1832
+ nextLink = (response.data["@odata.nextLink"] as string) || null;
1833
+ }
1834
+
1835
+ return MicrosoftTeamsAppInstallState.NotInstalled;
1836
+ } catch (err) {
1837
+ logger.debug(
1838
+ `Error checking whether the OneUptime app is installed in team ${data.teamId}; treating installation as unknown.`,
1839
+ );
1840
+ logger.debug(err);
1841
+ return MicrosoftTeamsAppInstallState.Unknown;
1842
+ }
1843
+ }
1844
+
1645
1845
  /*
1646
1846
  * Sends an adaptive card to a Teams group chat or personal (1:1) chat via a
1647
1847
  * proactive Bot Framework message. The OneUptime app must have been added to
@@ -2357,6 +2557,7 @@ export default class MicrosoftTeamsUtil extends WorkspaceBase {
2357
2557
  await this.captureTeamFromBotActivity({
2358
2558
  activity: data.activity,
2359
2559
  turnContext: data.turnContext,
2560
+ onlyIfMissingOrStale: true,
2360
2561
  });
2361
2562
  }
2362
2563
 
@@ -4246,10 +4447,121 @@ All monitoring checks are passing normally.`;
4246
4447
  * (channels are not chats), so before this existed we threw away the only
4247
4448
  * signal that tells us a proactive channel post will be accepted.
4248
4449
  */
4450
+ /*
4451
+ * The AAD group id for a team, which is what Graph calls the team id and what
4452
+ * every notification rule stores. Teams normally puts it on the activity as
4453
+ * channelData.team.aadGroupId; when it does not, TeamsInfo.getTeamDetails
4454
+ * fetches it. Returns undefined rather than throwing — a missing group id
4455
+ * degrades the record, it does not invalidate the install.
4456
+ */
4457
+ public static async resolveGraphTeamIdFromBotActivity(data: {
4458
+ team: JSONObject | undefined;
4459
+ turnContext: TurnContext;
4460
+ }): Promise<string | undefined> {
4461
+ const aadGroupIdOnActivity: string =
4462
+ (data.team?.["aadGroupId"] as string) || "";
4463
+
4464
+ if (aadGroupIdOnActivity) {
4465
+ return aadGroupIdOnActivity;
4466
+ }
4467
+
4468
+ try {
4469
+ const teamDetails: TeamDetails = await TeamsInfo.getTeamDetails(
4470
+ data.turnContext,
4471
+ );
4472
+
4473
+ return teamDetails?.aadGroupId || undefined;
4474
+ } catch (err) {
4475
+ /*
4476
+ * Uninstall activities are the common case here: the bot is already out
4477
+ * of the team by the time we ask, so the call fails. Uninstall matches on
4478
+ * the thread id anyway, so this is not worth an error-level log.
4479
+ */
4480
+ logger.debug(
4481
+ "Could not resolve the Graph team id for a Microsoft Teams install:",
4482
+ );
4483
+ logger.debug(err);
4484
+ return undefined;
4485
+ }
4486
+ }
4487
+
4488
+ /*
4489
+ * installedTeams re-keyed by Graph team id, which is the id every caller
4490
+ * outside the bot handlers actually holds. Records that could not be resolved
4491
+ * to a group id are dropped: they cannot answer "is this team installed?" for
4492
+ * a notification rule, and including them under their thread id would only
4493
+ * invite the same confusion this replaced.
4494
+ */
4495
+ public static indexInstalledTeamsByGraphTeamId(
4496
+ installedTeams: Record<string, MicrosoftTeamsInstalledTeam> | undefined,
4497
+ ): Record<string, MicrosoftTeamsInstalledTeam> {
4498
+ const index: Record<string, MicrosoftTeamsInstalledTeam> = {};
4499
+
4500
+ for (const installedTeam of Object.values(installedTeams || {})) {
4501
+ const graphTeamId: string | undefined =
4502
+ installedTeam?.graphTeamId || undefined;
4503
+
4504
+ if (!graphTeamId) {
4505
+ continue;
4506
+ }
4507
+
4508
+ index[graphTeamId] = installedTeam;
4509
+ }
4510
+
4511
+ return index;
4512
+ }
4513
+
4514
+ /*
4515
+ * The stored record for a team in a tenant, if any, matched on either id.
4516
+ * Used to keep the message-driven backfill cheap.
4517
+ */
4518
+ public static async getInstalledTeamForTenant(data: {
4519
+ tenantId: string;
4520
+ teamsThreadId?: string | undefined;
4521
+ graphTeamId?: string | undefined;
4522
+ }): Promise<MicrosoftTeamsInstalledTeam | null> {
4523
+ const projectAuths: Array<WorkspaceProjectAuthToken> =
4524
+ await WorkspaceProjectAuthTokenService.findBy({
4525
+ query: {
4526
+ workspaceType: WorkspaceType.MicrosoftTeams,
4527
+ workspaceProjectId: data.tenantId,
4528
+ },
4529
+ select: {
4530
+ _id: true,
4531
+ miscData: true,
4532
+ },
4533
+ limit: LIMIT_MAX,
4534
+ skip: 0,
4535
+ props: {
4536
+ isRoot: true,
4537
+ },
4538
+ });
4539
+
4540
+ for (const projectAuth of projectAuths) {
4541
+ const miscData: MicrosoftTeamsMiscData =
4542
+ (projectAuth.miscData as MicrosoftTeamsMiscData) || {};
4543
+
4544
+ for (const existingTeam of Object.values(miscData.installedTeams || {})) {
4545
+ if (
4546
+ this.isSameInstalledTeam({
4547
+ team: existingTeam,
4548
+ graphTeamId: data.graphTeamId,
4549
+ teamsThreadId: data.teamsThreadId,
4550
+ })
4551
+ ) {
4552
+ return existingTeam;
4553
+ }
4554
+ }
4555
+ }
4556
+
4557
+ return null;
4558
+ }
4559
+
4249
4560
  @CaptureSpan()
4250
4561
  public static async captureTeamFromBotActivity(data: {
4251
4562
  activity: JSONObject;
4252
4563
  turnContext: TurnContext;
4564
+ onlyIfMissingOrStale?: boolean | undefined;
4253
4565
  }): Promise<void> {
4254
4566
  try {
4255
4567
  const channelData: JSONObject =
@@ -4259,9 +4571,9 @@ All monitoring checks are passing normally.`;
4259
4571
  | JSONObject
4260
4572
  | undefined;
4261
4573
 
4262
- const teamId: string = (team?.["id"] as string) || "";
4574
+ const teamsThreadId: string = (team?.["id"] as string) || "";
4263
4575
 
4264
- if (!teamId) {
4576
+ if (!teamsThreadId) {
4265
4577
  // Not a team-scoped activity (personal or group chat). Nothing to do.
4266
4578
  return;
4267
4579
  }
@@ -4274,13 +4586,55 @@ All monitoring checks are passing normally.`;
4274
4586
  return;
4275
4587
  }
4276
4588
 
4589
+ const activityServiceUrl: string | undefined =
4590
+ (data.activity["serviceUrl"] as string) ||
4591
+ data.turnContext.activity.serviceUrl ||
4592
+ undefined;
4593
+
4594
+ /*
4595
+ * The backfill runs on every inbound channel message, so it must be cheap
4596
+ * once the team is already on record: one read, then nothing. Without this
4597
+ * every message meant a write, and now potentially a Bot Framework call to
4598
+ * resolve the group id as well. A record that is missing the group id, or
4599
+ * whose serviceUrl has moved, is still worth redoing.
4600
+ */
4601
+ if (data.onlyIfMissingOrStale) {
4602
+ const existingTeam: MicrosoftTeamsInstalledTeam | null =
4603
+ await this.getInstalledTeamForTenant({
4604
+ tenantId: tenantId,
4605
+ teamsThreadId: teamsThreadId,
4606
+ graphTeamId: (team?.["aadGroupId"] as string) || undefined,
4607
+ });
4608
+
4609
+ if (
4610
+ existingTeam &&
4611
+ existingTeam.graphTeamId &&
4612
+ existingTeam.serviceUrl === activityServiceUrl
4613
+ ) {
4614
+ return;
4615
+ }
4616
+ }
4617
+
4618
+ /*
4619
+ * The send path looks installs up by Graph team id, so resolving it here
4620
+ * is what makes the record usable at all. Teams puts it on the activity
4621
+ * as aadGroupId most of the time; when it does not, one Bot Framework
4622
+ * call gets it. A record without it is still worth keeping for the
4623
+ * serviceUrl and for uninstall matching, it just cannot be matched to a
4624
+ * notification rule.
4625
+ */
4626
+ const graphTeamId: string | undefined =
4627
+ await this.resolveGraphTeamIdFromBotActivity({
4628
+ team: team,
4629
+ turnContext: data.turnContext,
4630
+ });
4631
+
4277
4632
  const installedTeam: MicrosoftTeamsInstalledTeam = {
4278
- id: teamId,
4633
+ id: graphTeamId || teamsThreadId,
4634
+ graphTeamId: graphTeamId,
4635
+ teamsThreadId: teamsThreadId,
4279
4636
  name: (team?.["name"] as string) || undefined,
4280
- serviceUrl:
4281
- (data.activity["serviceUrl"] as string) ||
4282
- data.turnContext.activity.serviceUrl ||
4283
- undefined,
4637
+ serviceUrl: activityServiceUrl,
4284
4638
  addedAt: OneUptimeDate.getCurrentDate().toISOString(),
4285
4639
  };
4286
4640
 
@@ -4290,7 +4644,7 @@ All monitoring checks are passing normally.`;
4290
4644
  });
4291
4645
 
4292
4646
  logger.debug(
4293
- `Captured Microsoft Teams team install ${teamId} for tenant ${tenantId}`,
4647
+ `Captured Microsoft Teams team install ${installedTeam.id} (thread ${teamsThreadId}, graph ${graphTeamId || "unresolved"}) for tenant ${tenantId}`,
4294
4648
  );
4295
4649
  } catch (err) {
4296
4650
  logger.error("Error capturing Microsoft Teams team from bot activity:");
@@ -4306,22 +4660,33 @@ All monitoring checks are passing normally.`;
4306
4660
  const channelData: JSONObject =
4307
4661
  (data.activity["channelData"] as JSONObject) || {};
4308
4662
 
4309
- const teamId: string =
4310
- ((channelData["team"] as JSONObject)?.["id"] as string) || "";
4663
+ const team: JSONObject | undefined = channelData["team"] as
4664
+ | JSONObject
4665
+ | undefined;
4666
+
4667
+ const teamsThreadId: string = (team?.["id"] as string) || "";
4311
4668
  const tenantId: string =
4312
4669
  ((channelData["tenant"] as JSONObject)?.["id"] as string) || "";
4313
4670
 
4314
- if (!teamId || !tenantId) {
4671
+ if (!teamsThreadId || !tenantId) {
4315
4672
  return;
4316
4673
  }
4317
4674
 
4675
+ /*
4676
+ * Uninstall matches on both ids because we cannot know which one the
4677
+ * record was keyed by: the bot is already out of the team, so
4678
+ * getTeamDetails will not answer, and records written before this shipped
4679
+ * only have a thread id. aadGroupId is still on the activity often enough
4680
+ * to be worth passing through.
4681
+ */
4318
4682
  await this.removeTeamFromProjectAuthTokens({
4319
4683
  tenantId: tenantId,
4320
- teamId: teamId,
4684
+ teamsThreadId: teamsThreadId,
4685
+ graphTeamId: (team?.["aadGroupId"] as string) || undefined,
4321
4686
  });
4322
4687
 
4323
4688
  logger.debug(
4324
- `Removed Microsoft Teams team install ${teamId} for tenant ${tenantId}`,
4689
+ `Removed Microsoft Teams team install ${teamsThreadId} for tenant ${tenantId}`,
4325
4690
  );
4326
4691
  } catch (err) {
4327
4692
  logger.error("Error removing Microsoft Teams team from bot activity:");
@@ -4361,10 +4726,32 @@ All monitoring checks are passing normally.`;
4361
4726
  ...((projectAuth.miscData as MicrosoftTeamsMiscData) || {}),
4362
4727
  } as MicrosoftTeamsMiscData;
4363
4728
 
4364
- miscData.installedTeams = {
4365
- ...(miscData.installedTeams || {}),
4366
- [data.team.id]: data.team,
4367
- };
4729
+ /*
4730
+ * Drop any other record describing the same team before inserting. A
4731
+ * team captured before the group id was resolvable sits under its thread
4732
+ * id, and re-capturing it under the group id would otherwise leave two
4733
+ * records for one team — one of which can never be matched or removed.
4734
+ */
4735
+ const installedTeams: Record<string, MicrosoftTeamsInstalledTeam> = {};
4736
+
4737
+ for (const [key, existingTeam] of Object.entries(
4738
+ miscData.installedTeams || {},
4739
+ )) {
4740
+ if (
4741
+ this.isSameInstalledTeam({
4742
+ team: existingTeam,
4743
+ graphTeamId: data.team.graphTeamId,
4744
+ teamsThreadId: data.team.teamsThreadId,
4745
+ })
4746
+ ) {
4747
+ continue;
4748
+ }
4749
+
4750
+ installedTeams[key] = existingTeam;
4751
+ }
4752
+
4753
+ installedTeams[data.team.id] = data.team;
4754
+ miscData.installedTeams = installedTeams;
4368
4755
 
4369
4756
  await WorkspaceProjectAuthTokenService.updateOneById({
4370
4757
  id: projectAuth.id!,
@@ -4378,10 +4765,45 @@ All monitoring checks are passing normally.`;
4378
4765
  }
4379
4766
  }
4380
4767
 
4768
+ /*
4769
+ * Whether a stored record describes the team identified by either id. Matches
4770
+ * on the record's explicit ids and on its key, so records written before
4771
+ * teamsThreadId existed — where the thread id is only in `id` — still match.
4772
+ * Ids are compared case-sensitively, as Microsoft returns them.
4773
+ */
4774
+ public static isSameInstalledTeam(data: {
4775
+ team: MicrosoftTeamsInstalledTeam | undefined;
4776
+ graphTeamId?: string | undefined;
4777
+ teamsThreadId?: string | undefined;
4778
+ }): boolean {
4779
+ if (!data.team) {
4780
+ return false;
4781
+ }
4782
+
4783
+ if (
4784
+ data.graphTeamId &&
4785
+ (data.team.graphTeamId === data.graphTeamId ||
4786
+ data.team.id === data.graphTeamId)
4787
+ ) {
4788
+ return true;
4789
+ }
4790
+
4791
+ if (
4792
+ data.teamsThreadId &&
4793
+ (data.team.teamsThreadId === data.teamsThreadId ||
4794
+ data.team.id === data.teamsThreadId)
4795
+ ) {
4796
+ return true;
4797
+ }
4798
+
4799
+ return false;
4800
+ }
4801
+
4381
4802
  @CaptureSpan()
4382
4803
  public static async removeTeamFromProjectAuthTokens(data: {
4383
4804
  tenantId: string;
4384
- teamId: string;
4805
+ teamsThreadId?: string | undefined;
4806
+ graphTeamId?: string | undefined;
4385
4807
  }): Promise<void> {
4386
4808
  const projectAuths: Array<WorkspaceProjectAuthToken> =
4387
4809
  await WorkspaceProjectAuthTokenService.findBy({
@@ -4405,14 +4827,34 @@ All monitoring checks are passing normally.`;
4405
4827
  ...((projectAuth.miscData as MicrosoftTeamsMiscData) || {}),
4406
4828
  } as MicrosoftTeamsMiscData;
4407
4829
 
4408
- if (!miscData.installedTeams || !miscData.installedTeams[data.teamId]) {
4830
+ if (!miscData.installedTeams) {
4831
+ continue;
4832
+ }
4833
+
4834
+ const installedTeams: Record<string, MicrosoftTeamsInstalledTeam> = {};
4835
+ let removedAny: boolean = false;
4836
+
4837
+ for (const [key, existingTeam] of Object.entries(
4838
+ miscData.installedTeams,
4839
+ )) {
4840
+ if (
4841
+ this.isSameInstalledTeam({
4842
+ team: existingTeam,
4843
+ graphTeamId: data.graphTeamId,
4844
+ teamsThreadId: data.teamsThreadId,
4845
+ })
4846
+ ) {
4847
+ removedAny = true;
4848
+ continue;
4849
+ }
4850
+
4851
+ installedTeams[key] = existingTeam;
4852
+ }
4853
+
4854
+ if (!removedAny) {
4409
4855
  continue;
4410
4856
  }
4411
4857
 
4412
- const installedTeams: Record<string, MicrosoftTeamsInstalledTeam> = {
4413
- ...miscData.installedTeams,
4414
- };
4415
- delete installedTeams[data.teamId];
4416
4858
  miscData.installedTeams = installedTeams;
4417
4859
 
4418
4860
  await WorkspaceProjectAuthTokenService.updateOneById({