@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
@@ -0,0 +1,785 @@
1
+ import { mockRouter } from "./Helpers";
2
+ import "../../../Server/API/AIChatAPI";
3
+ import CommonAPI from "../../../Server/API/CommonAPI";
4
+ import UserMiddleware from "../../../Server/Middleware/UserAuthorization";
5
+ import AIConversationService from "../../../Server/Services/AIConversationService";
6
+ import AIConversationMessageService from "../../../Server/Services/AIConversationMessageService";
7
+ import LlmProviderService from "../../../Server/Services/LlmProviderService";
8
+ import ProjectService from "../../../Server/Services/ProjectService";
9
+ import {
10
+ ExpressRequest,
11
+ ExpressResponse,
12
+ NextFunction,
13
+ } from "../../../Server/Utils/Express";
14
+ import Response from "../../../Server/Utils/Response";
15
+ import LlmProvider from "../../../Models/DatabaseModels/LlmProvider";
16
+ import DatabaseCommonInteractionProps from "../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
17
+ import BadDataException from "../../../Types/Exception/BadDataException";
18
+ import NotAuthorizedException from "../../../Types/Exception/NotAuthorizedException";
19
+ import Dictionary from "../../../Types/Dictionary";
20
+ import { JSONObject } from "../../../Types/JSON";
21
+ import LlmType from "../../../Types/LLM/LlmType";
22
+ import ObjectID from "../../../Types/ObjectID";
23
+ import Permission, {
24
+ PermissionHelper,
25
+ PermissionProps,
26
+ UserGlobalAccessPermission,
27
+ UserPermission,
28
+ UserTenantAccessPermission,
29
+ } from "../../../Types/Permission";
30
+ import UserType from "../../../Types/UserType";
31
+ import { beforeEach, describe, expect, it, jest, test } from "@jest/globals";
32
+
33
+ jest.mock("../../../Server/Utils/Express", () => {
34
+ return {
35
+ getRouter: () => {
36
+ return mockRouter;
37
+ },
38
+ };
39
+ });
40
+
41
+ jest.mock("../../../Server/Utils/Response", () => {
42
+ return {
43
+ sendJsonObjectResponse: jest.fn(),
44
+ };
45
+ });
46
+
47
+ /*
48
+ * GHSA-hm7m-9qjj-xj5x - "AI chat provider listing discloses another project's
49
+ * provider inventory".
50
+ *
51
+ * Every /ai-chat/* route is a custom (non-CRUD) route mounted with nothing but
52
+ * UserMiddleware.getUserMiddleware, and that middleware is not an authorisation
53
+ * gate: it lets an unauthenticated request through as UserType.Public, and the
54
+ * tenant id it attaches comes from the caller-supplied `tenantid` header before
55
+ * any authorisation has run. The routes used to check only that props carried
56
+ * SOME user id and SOME tenant id, and then read with `isRoot: true`.
57
+ *
58
+ * /ai-chat/providers was the reportable consequence: a user of project A could
59
+ * send project B's UUID in that header and be handed B's LLM provider inventory
60
+ * - ids, names, internal descriptions, provider types and model names - without
61
+ * belonging to B at all.
62
+ *
63
+ * Two things are asserted here, and they are separate concerns:
64
+ *
65
+ * 1. MEMBERSHIP of the project named in the header (all five routes).
66
+ * 2. PERMISSION to read that project's providers once inside it
67
+ * (/ai-chat/providers), because being a member is not the same as being
68
+ * allowed to read every model the project owns - a plain GET
69
+ * /llm-provider is gated by LlmProvider's own read list and the picker
70
+ * must not be a way around it.
71
+ *
72
+ * The refusals are also asserted to happen BEFORE any lookup runs. An endpoint
73
+ * that rejects only after querying still leaks through timing, and it still
74
+ * confirms that a project id exists.
75
+ */
76
+
77
+ const PROJECT_A_ID: ObjectID = new ObjectID(
78
+ "11111111-1111-4111-8111-111111111111",
79
+ );
80
+
81
+ // The victim project. The attacker belongs to A and never to B.
82
+ const PROJECT_B_ID: ObjectID = new ObjectID(
83
+ "22222222-2222-4222-8222-222222222222",
84
+ );
85
+
86
+ const ATTACKER_USER_ID: ObjectID = new ObjectID(
87
+ "33333333-3333-4333-8333-333333333333",
88
+ );
89
+
90
+ const PROVIDER_ID: ObjectID = new ObjectID(
91
+ "44444444-4444-4444-8444-444444444444",
92
+ );
93
+
94
+ const CONVERSATION_ID: ObjectID = new ObjectID(
95
+ "55555555-5555-4555-8555-555555555555",
96
+ );
97
+
98
+ const MESSAGE_ID: ObjectID = new ObjectID(
99
+ "66666666-6666-4666-8666-666666666666",
100
+ );
101
+
102
+ const PROVIDERS_ROUTE: string = "/ai-chat/providers";
103
+
104
+ type PropsOverrides = {
105
+ userId?: ObjectID | undefined;
106
+ tenantId?: ObjectID | undefined;
107
+
108
+ // The project the caller actually belongs to, if any.
109
+ memberOfProjectId?: ObjectID | undefined;
110
+
111
+ // Permissions that membership grants inside memberOfProjectId.
112
+ permissions?: Array<Permission> | undefined;
113
+
114
+ // Permissions the caller's teams explicitly BLOCK inside that project.
115
+ blockedPermissions?: Array<Permission> | undefined;
116
+
117
+ userType?: UserType | undefined;
118
+ isMasterAdmin?: boolean | undefined;
119
+ userGlobalAccessPermission?: UserGlobalAccessPermission | undefined;
120
+ };
121
+
122
+ /*
123
+ * Props shaped the way UserAuthorization actually builds them. That middleware
124
+ * resolves the caller's tenant permissions for THE PROJECT IN THE HEADER and
125
+ * keys the dictionary by that project alone, so a user reaching for a project
126
+ * they do not belong to arrives with `userTenantAccessPermission` undefined -
127
+ * which is the default here when no membership is given.
128
+ */
129
+ function buildProps(
130
+ overrides?: PropsOverrides,
131
+ ): DatabaseCommonInteractionProps {
132
+ const props: DatabaseCommonInteractionProps = {
133
+ userId: overrides?.userId,
134
+ tenantId: overrides?.tenantId,
135
+ userType: overrides?.userType,
136
+ isMasterAdmin: overrides?.isMasterAdmin,
137
+ userGlobalAccessPermission: overrides?.userGlobalAccessPermission,
138
+ };
139
+
140
+ if (overrides?.memberOfProjectId) {
141
+ const permissions: Array<UserPermission> = (
142
+ overrides.permissions || [Permission.ProjectMember]
143
+ ).map((permission: Permission) => {
144
+ return {
145
+ _type: "UserPermission",
146
+ permission: permission,
147
+ labelIds: [],
148
+ isBlockPermission: false,
149
+ } as UserPermission;
150
+ });
151
+
152
+ for (const blocked of overrides.blockedPermissions || []) {
153
+ permissions.push({
154
+ _type: "UserPermission",
155
+ permission: blocked,
156
+ labelIds: [],
157
+ isBlockPermission: true,
158
+ } as UserPermission);
159
+ }
160
+
161
+ const tenantPermission: UserTenantAccessPermission = {
162
+ _type: "UserTenantAccessPermission",
163
+ projectId: overrides.memberOfProjectId,
164
+ permissions: permissions,
165
+ isBlockPermission: false,
166
+ } as UserTenantAccessPermission;
167
+
168
+ const dictionary: Dictionary<UserTenantAccessPermission> = {};
169
+ dictionary[overrides.memberOfProjectId.toString()] = tenantPermission;
170
+
171
+ props.userTenantAccessPermission = dictionary;
172
+ }
173
+
174
+ return props;
175
+ }
176
+
177
+ // A legitimate member of project B with the default Member role.
178
+ function memberOfVictimProject(
179
+ permissions?: Array<Permission>,
180
+ ): DatabaseCommonInteractionProps {
181
+ return buildProps({
182
+ userId: ATTACKER_USER_ID,
183
+ tenantId: PROJECT_B_ID,
184
+ memberOfProjectId: PROJECT_B_ID,
185
+ permissions: permissions,
186
+ });
187
+ }
188
+
189
+ /*
190
+ * The advisory's reproduction, in props: a real, logged-in user whose only
191
+ * membership is project A, naming project B in the `tenantid` header.
192
+ */
193
+ function outsiderReachingForVictimProject(): DatabaseCommonInteractionProps {
194
+ return buildProps({
195
+ userId: ATTACKER_USER_ID,
196
+ tenantId: PROJECT_B_ID,
197
+ memberOfProjectId: PROJECT_A_ID,
198
+ permissions: [Permission.ProjectOwner],
199
+ });
200
+ }
201
+
202
+ function requestFor(body?: JSONObject): ExpressRequest {
203
+ return {
204
+ body: body || {},
205
+ headers: {},
206
+ params: {},
207
+ query: {},
208
+ } as unknown as ExpressRequest;
209
+ }
210
+
211
+ function response(): ExpressResponse {
212
+ return {} as ExpressResponse;
213
+ }
214
+
215
+ type RouteCall = {
216
+ thrown: unknown;
217
+ nextCallCount: number;
218
+ };
219
+
220
+ async function callRoute(data: {
221
+ uri: string;
222
+ body?: JSONObject | undefined;
223
+ }): Promise<RouteCall> {
224
+ const next: ReturnType<typeof jest.fn> = jest.fn();
225
+
226
+ await mockRouter
227
+ .match("post", data.uri)
228
+ .handlerFunction(
229
+ requestFor(data.body),
230
+ response(),
231
+ next as unknown as NextFunction,
232
+ );
233
+
234
+ return {
235
+ thrown: next.mock.calls[0]?.[0],
236
+ nextCallCount: next.mock.calls.length,
237
+ };
238
+ }
239
+
240
+ function callProviders(): Promise<RouteCall> {
241
+ return callRoute({ uri: PROVIDERS_ROUTE });
242
+ }
243
+
244
+ function sentPayload(): JSONObject {
245
+ const send: jest.Mock =
246
+ Response.sendJsonObjectResponse as unknown as jest.Mock;
247
+ return send.mock.calls[0]![2] as JSONObject;
248
+ }
249
+
250
+ function withProps(props: DatabaseCommonInteractionProps): void {
251
+ jest
252
+ .spyOn(CommonAPI, "getDatabaseCommonInteractionProps")
253
+ .mockResolvedValue(props);
254
+ }
255
+
256
+ // A provider row with the exact metadata the advisory says leaked.
257
+ function victimProvider(): LlmProvider {
258
+ const provider: LlmProvider = new LlmProvider(PROVIDER_ID);
259
+ provider.name = "acme-internal-gpt";
260
+ provider.description = "Acme's private evaluation cluster";
261
+ provider.llmType = LlmType.OpenAI;
262
+ provider.modelName = "gpt-4o-secret-eval";
263
+ provider.isDefault = true;
264
+ provider.isGlobalLlm = false;
265
+ provider.projectId = PROJECT_B_ID;
266
+ return provider;
267
+ }
268
+
269
+ function stubProviderLookups(): void {
270
+ jest
271
+ .spyOn(LlmProviderService, "getSelectableProvidersForProject")
272
+ .mockResolvedValue([victimProvider()]);
273
+ jest
274
+ .spyOn(LlmProviderService, "getLLMProviderForProject")
275
+ .mockResolvedValue(victimProvider());
276
+ }
277
+
278
+ function providerLookupsRan(): boolean {
279
+ const selectable: jest.Mock =
280
+ LlmProviderService.getSelectableProvidersForProject as unknown as jest.Mock;
281
+ const forProject: jest.Mock =
282
+ LlmProviderService.getLLMProviderForProject as unknown as jest.Mock;
283
+
284
+ return selectable.mock.calls.length > 0 || forProject.mock.calls.length > 0;
285
+ }
286
+
287
+ beforeEach(() => {
288
+ jest.restoreAllMocks();
289
+ jest.clearAllMocks();
290
+ stubProviderLookups();
291
+ });
292
+
293
+ describe("POST /ai-chat/providers - cross-tenant disclosure (GHSA-hm7m-9qjj-xj5x)", () => {
294
+ test("a user of project A cannot list project B's providers by sending B's tenantid", async () => {
295
+ withProps(outsiderReachingForVictimProject());
296
+
297
+ const call: RouteCall = await callRoute({ uri: PROVIDERS_ROUTE });
298
+
299
+ expect(call.thrown).toBeInstanceOf(NotAuthorizedException);
300
+ expect(Response.sendJsonObjectResponse).not.toHaveBeenCalled();
301
+ });
302
+
303
+ test("the refusal happens before any provider lookup runs, so nothing is queried for the victim project", async () => {
304
+ withProps(outsiderReachingForVictimProject());
305
+
306
+ await callProviders();
307
+
308
+ expect(providerLookupsRan()).toBe(false);
309
+ expect(
310
+ LlmProviderService.getSelectableProvidersForProject,
311
+ ).not.toHaveBeenCalled();
312
+ expect(LlmProviderService.getLLMProviderForProject).not.toHaveBeenCalled();
313
+ });
314
+
315
+ test("none of the leaked metadata reaches the response body", async () => {
316
+ withProps(outsiderReachingForVictimProject());
317
+
318
+ await callProviders();
319
+
320
+ expect(Response.sendJsonObjectResponse).not.toHaveBeenCalled();
321
+ });
322
+
323
+ test("the refusal does not distinguish a project that exists from one that does not", async () => {
324
+ withProps(outsiderReachingForVictimProject());
325
+ const realProject: RouteCall = await callProviders();
326
+
327
+ jest.clearAllMocks();
328
+ stubProviderLookups();
329
+
330
+ withProps(
331
+ buildProps({
332
+ userId: ATTACKER_USER_ID,
333
+ tenantId: new ObjectID("99999999-9999-4999-8999-999999999999"),
334
+ memberOfProjectId: PROJECT_A_ID,
335
+ }),
336
+ );
337
+ const madeUpProject: RouteCall = await callProviders();
338
+
339
+ expect((madeUpProject.thrown as NotAuthorizedException).message).toBe(
340
+ (realProject.thrown as NotAuthorizedException).message,
341
+ );
342
+ });
343
+
344
+ test("holding every permission in the caller's OWN project grants nothing in the target project", async () => {
345
+ withProps(
346
+ buildProps({
347
+ userId: ATTACKER_USER_ID,
348
+ tenantId: PROJECT_B_ID,
349
+ memberOfProjectId: PROJECT_A_ID,
350
+ permissions: [
351
+ Permission.ProjectOwner,
352
+ Permission.ProjectAdmin,
353
+ Permission.SettingsAdmin,
354
+ Permission.ReadProjectLlm,
355
+ ],
356
+ }),
357
+ );
358
+
359
+ const call: RouteCall = await callProviders();
360
+
361
+ expect(call.thrown).toBeInstanceOf(NotAuthorizedException);
362
+ expect(providerLookupsRan()).toBe(false);
363
+ });
364
+
365
+ test("a tenant permission dictionary that is present but keyed for another project is refused", async () => {
366
+ const props: DatabaseCommonInteractionProps = buildProps({
367
+ userId: ATTACKER_USER_ID,
368
+ tenantId: PROJECT_B_ID,
369
+ memberOfProjectId: PROJECT_A_ID,
370
+ });
371
+
372
+ expect(props.userTenantAccessPermission).toBeDefined();
373
+ expect(
374
+ props.userTenantAccessPermission![PROJECT_B_ID.toString()],
375
+ ).toBeUndefined();
376
+
377
+ withProps(props);
378
+
379
+ const call: RouteCall = await callProviders();
380
+
381
+ expect(call.thrown).toBeInstanceOf(NotAuthorizedException);
382
+ });
383
+
384
+ test("global permissions cannot stand in for membership of the target project", async () => {
385
+ withProps(
386
+ buildProps({
387
+ userId: ATTACKER_USER_ID,
388
+ tenantId: PROJECT_B_ID,
389
+ userGlobalAccessPermission: {
390
+ _type: "UserGlobalAccessPermission",
391
+ projectIds: [PROJECT_A_ID],
392
+ globalPermissions: [Permission.Public, Permission.CurrentUser],
393
+ } as UserGlobalAccessPermission,
394
+ }),
395
+ );
396
+
397
+ const call: RouteCall = await callProviders();
398
+
399
+ expect(call.thrown).toBeInstanceOf(NotAuthorizedException);
400
+ expect(providerLookupsRan()).toBe(false);
401
+ });
402
+ });
403
+
404
+ describe("POST /ai-chat/providers - anonymous and unscoped callers", () => {
405
+ test("an unauthenticated caller (getUserMiddleware admits these as public) is refused", async () => {
406
+ withProps(buildProps({ tenantId: PROJECT_B_ID }));
407
+
408
+ const call: RouteCall = await callProviders();
409
+
410
+ expect(call.thrown).toBeInstanceOf(NotAuthorizedException);
411
+ expect(providerLookupsRan()).toBe(false);
412
+ });
413
+
414
+ test("an anonymous caller carrying a stray tenant permission entry but no user id is refused", async () => {
415
+ withProps(
416
+ buildProps({
417
+ userId: undefined,
418
+ tenantId: PROJECT_B_ID,
419
+ memberOfProjectId: PROJECT_B_ID,
420
+ }),
421
+ );
422
+
423
+ const call: RouteCall = await callProviders();
424
+
425
+ expect(call.thrown).toBeInstanceOf(NotAuthorizedException);
426
+ expect(providerLookupsRan()).toBe(false);
427
+ });
428
+
429
+ test("a request with no tenantid header is a bad request, not an authorization failure", async () => {
430
+ withProps(
431
+ buildProps({
432
+ userId: ATTACKER_USER_ID,
433
+ memberOfProjectId: PROJECT_A_ID,
434
+ }),
435
+ );
436
+
437
+ const call: RouteCall = await callProviders();
438
+
439
+ expect(call.thrown).toBeInstanceOf(BadDataException);
440
+ expect((call.thrown as BadDataException).message).toBe(
441
+ "Project ID is required",
442
+ );
443
+ expect(providerLookupsRan()).toBe(false);
444
+ });
445
+
446
+ test("a project API key (no user id) cannot use this route - it is a logged-in-member route", async () => {
447
+ withProps(
448
+ buildProps({
449
+ tenantId: PROJECT_B_ID,
450
+ memberOfProjectId: PROJECT_B_ID,
451
+ userType: UserType.API,
452
+ }),
453
+ );
454
+
455
+ const call: RouteCall = await callProviders();
456
+
457
+ expect(call.thrown).toBeInstanceOf(NotAuthorizedException);
458
+ expect(providerLookupsRan()).toBe(false);
459
+ });
460
+ });
461
+
462
+ describe("POST /ai-chat/providers - membership alone is not read authorization", () => {
463
+ test("a member whose teams grant no provider-read permission is refused", async () => {
464
+ withProps(memberOfVictimProject([Permission.ReadProjectIncident]));
465
+
466
+ const call: RouteCall = await callProviders();
467
+
468
+ expect(call.thrown).toBeInstanceOf(NotAuthorizedException);
469
+ expect((call.thrown as NotAuthorizedException).message).toBe(
470
+ "You do not have permission to read this project's AI providers.",
471
+ );
472
+ expect(providerLookupsRan()).toBe(false);
473
+ });
474
+
475
+ test("a member with no tenant permissions at all is refused", async () => {
476
+ withProps(memberOfVictimProject([]));
477
+
478
+ const call: RouteCall = await callProviders();
479
+
480
+ expect(call.thrown).toBeInstanceOf(NotAuthorizedException);
481
+ expect(providerLookupsRan()).toBe(false);
482
+ });
483
+
484
+ test("Permission.Public in LlmProvider's read list does not admit an ordinary member", async () => {
485
+ /*
486
+ * LlmProvider declares Permission.Public as a reader so the shared global
487
+ * providers stay visible, and getUserPermissions merges Public into EVERY
488
+ * caller's permissions. A guard that intersected the two lists naively
489
+ * would therefore pass for anyone at all - this is the regression test for
490
+ * that specific mistake.
491
+ */
492
+ expect(new LlmProvider().getReadPermissions()).toContain(Permission.Public);
493
+
494
+ withProps(memberOfVictimProject([Permission.ReadProjectIncident]));
495
+
496
+ const call: RouteCall = await callProviders();
497
+
498
+ expect(call.thrown).toBeInstanceOf(NotAuthorizedException);
499
+ });
500
+
501
+ test("Permission.CurrentUser, which every logged-in caller carries, does not admit them either", async () => {
502
+ withProps(memberOfVictimProject([Permission.CurrentUser]));
503
+
504
+ const call: RouteCall = await callProviders();
505
+
506
+ expect(call.thrown).toBeInstanceOf(NotAuthorizedException);
507
+ });
508
+
509
+ test("a team's BLOCK row for a reader permission is not counted as a grant of it", async () => {
510
+ withProps(
511
+ buildProps({
512
+ userId: ATTACKER_USER_ID,
513
+ tenantId: PROJECT_B_ID,
514
+ memberOfProjectId: PROJECT_B_ID,
515
+ permissions: [Permission.ReadProjectIncident],
516
+ blockedPermissions: [
517
+ Permission.ProjectMember,
518
+ Permission.ReadProjectLlm,
519
+ ],
520
+ }),
521
+ );
522
+
523
+ const call: RouteCall = await callProviders();
524
+
525
+ expect(call.thrown).toBeInstanceOf(NotAuthorizedException);
526
+ expect(providerLookupsRan()).toBe(false);
527
+ });
528
+ });
529
+
530
+ describe("POST /ai-chat/providers - legitimate members still get their picker", () => {
531
+ const readerPermissions: Array<Permission> = [
532
+ Permission.ProjectOwner,
533
+ Permission.ProjectAdmin,
534
+ Permission.ProjectMember,
535
+ Permission.Viewer,
536
+ Permission.SettingsAdmin,
537
+ Permission.SettingsMember,
538
+ Permission.SettingsViewer,
539
+ Permission.ReadProjectLlm,
540
+ ];
541
+
542
+ test.each(readerPermissions)(
543
+ "a member holding %s is served the provider list",
544
+ async (permission: Permission) => {
545
+ withProps(memberOfVictimProject([permission]));
546
+
547
+ const call: RouteCall = await callProviders();
548
+
549
+ expect(call.nextCallCount).toBe(0);
550
+ expect(Response.sendJsonObjectResponse).toHaveBeenCalled();
551
+ expect((sentPayload()["providers"] as Array<JSONObject>).length).toBe(1);
552
+ },
553
+ );
554
+
555
+ test("every tenant-assignable reader of LlmProvider is covered by the cases above", () => {
556
+ /*
557
+ * Pins the positive cases to the model rather than to a copy of its list:
558
+ * if LlmProvider gains a new reader role, this fails until the cases above
559
+ * cover it too. The filter is the guard's own rule - only permissions a
560
+ * team can actually grant inside a project count.
561
+ */
562
+ const tenantAssignable: Array<Permission> =
563
+ PermissionHelper.getTenantPermissionProps().map(
564
+ (permissionProps: PermissionProps) => {
565
+ return permissionProps.permission;
566
+ },
567
+ );
568
+
569
+ const tenantAssignableReaders: Array<Permission> = new LlmProvider()
570
+ .getReadPermissions()
571
+ .filter((permission: Permission) => {
572
+ return tenantAssignable.includes(permission);
573
+ });
574
+
575
+ expect([...tenantAssignableReaders].sort()).toEqual(
576
+ [...readerPermissions].sort(),
577
+ );
578
+
579
+ // ...and the guard's filter really does drop Public, which everyone holds.
580
+ expect(tenantAssignableReaders).not.toContain(Permission.Public);
581
+ });
582
+
583
+ test("the lookup is made for the caller's own project, never for a header-supplied stranger", async () => {
584
+ withProps(memberOfVictimProject());
585
+
586
+ await callProviders();
587
+
588
+ expect(
589
+ LlmProviderService.getSelectableProvidersForProject,
590
+ ).toHaveBeenCalledWith(PROJECT_B_ID);
591
+ expect(LlmProviderService.getLLMProviderForProject).toHaveBeenCalledWith(
592
+ PROJECT_B_ID,
593
+ );
594
+ });
595
+
596
+ test("the response carries picker metadata and the default provider id", async () => {
597
+ withProps(memberOfVictimProject());
598
+
599
+ await callProviders();
600
+
601
+ const payload: JSONObject = sentPayload();
602
+
603
+ expect(payload["defaultProviderId"]).toBe(PROVIDER_ID.toString());
604
+
605
+ const providers: Array<JSONObject> = payload[
606
+ "providers"
607
+ ] as Array<JSONObject>;
608
+
609
+ expect(providers).toEqual([
610
+ {
611
+ id: PROVIDER_ID.toString(),
612
+ name: "acme-internal-gpt",
613
+ description: "Acme's private evaluation cluster",
614
+ llmType: LlmType.OpenAI.toString(),
615
+ modelName: "gpt-4o-secret-eval",
616
+ isDefault: true,
617
+ isGlobal: false,
618
+ },
619
+ ]);
620
+ });
621
+
622
+ test("secrets are never included in the response", async () => {
623
+ withProps(memberOfVictimProject());
624
+
625
+ await callProviders();
626
+
627
+ const providers: Array<JSONObject> = sentPayload()[
628
+ "providers"
629
+ ] as Array<JSONObject>;
630
+
631
+ for (const provider of providers) {
632
+ expect(provider).not.toHaveProperty("apiKey");
633
+ expect(provider).not.toHaveProperty("baseUrl");
634
+ expect(provider).not.toHaveProperty("additionalParams");
635
+ }
636
+ });
637
+
638
+ test("a master admin who is a member of the project is not blocked by the permission gate", async () => {
639
+ withProps(
640
+ buildProps({
641
+ userId: ATTACKER_USER_ID,
642
+ tenantId: PROJECT_B_ID,
643
+ memberOfProjectId: PROJECT_B_ID,
644
+ permissions: [Permission.ReadProjectIncident],
645
+ isMasterAdmin: true,
646
+ }),
647
+ );
648
+
649
+ const call: RouteCall = await callProviders();
650
+
651
+ expect(call.nextCallCount).toBe(0);
652
+ expect(Response.sendJsonObjectResponse).toHaveBeenCalled();
653
+ });
654
+
655
+ test("master admin does NOT bypass the membership check", async () => {
656
+ withProps(
657
+ buildProps({
658
+ userId: ATTACKER_USER_ID,
659
+ tenantId: PROJECT_B_ID,
660
+ memberOfProjectId: PROJECT_A_ID,
661
+ isMasterAdmin: true,
662
+ }),
663
+ );
664
+
665
+ const call: RouteCall = await callProviders();
666
+
667
+ expect(call.thrown).toBeInstanceOf(NotAuthorizedException);
668
+ expect(providerLookupsRan()).toBe(false);
669
+ });
670
+ });
671
+
672
+ /*
673
+ * The other four /ai-chat/* routes shared the same "logged in somewhere +
674
+ * a tenantid header" check. None of them disclosed another project's data the
675
+ * way the provider listing did - their reads resolve under the caller's own
676
+ * props - but each still handed the header's project id to root-privileged
677
+ * work, so each is now gated on membership too. These cases lock that in.
678
+ */
679
+ describe("every /ai-chat/* route requires membership of the project it was given", () => {
680
+ const routes: Array<{ uri: string; body: JSONObject }> = [
681
+ {
682
+ uri: "/ai-chat/send-message",
683
+ body: { content: "hello" },
684
+ },
685
+ {
686
+ uri: "/ai-chat/respond-to-approval",
687
+ body: {
688
+ conversationId: CONVERSATION_ID.toString(),
689
+ assistantMessageId: MESSAGE_ID.toString(),
690
+ approved: true,
691
+ },
692
+ },
693
+ {
694
+ uri: "/ai-chat/cancel-run",
695
+ body: { conversationId: CONVERSATION_ID.toString() },
696
+ },
697
+ {
698
+ uri: "/ai-chat/message-feedback",
699
+ body: { messageId: MESSAGE_ID.toString(), feedback: "Up" },
700
+ },
701
+ {
702
+ uri: PROVIDERS_ROUTE,
703
+ body: {},
704
+ },
705
+ ];
706
+
707
+ beforeEach(() => {
708
+ jest.spyOn(AIConversationService, "findOneById").mockResolvedValue(null);
709
+ jest
710
+ .spyOn(AIConversationMessageService, "findOneById")
711
+ .mockResolvedValue(null);
712
+ jest.spyOn(ProjectService, "findOneById").mockResolvedValue(null);
713
+ });
714
+
715
+ test.each(routes)(
716
+ "$uri refuses a caller who belongs to another project",
717
+ async (route: { uri: string; body: JSONObject }) => {
718
+ withProps(outsiderReachingForVictimProject());
719
+
720
+ const call: RouteCall = await callRoute({
721
+ uri: route.uri,
722
+ body: route.body,
723
+ });
724
+
725
+ expect(call.thrown).toBeInstanceOf(NotAuthorizedException);
726
+ expect(Response.sendJsonObjectResponse).not.toHaveBeenCalled();
727
+ },
728
+ );
729
+
730
+ test.each(routes)(
731
+ "$uri refuses an unauthenticated caller",
732
+ async (route: { uri: string; body: JSONObject }) => {
733
+ withProps(buildProps({ tenantId: PROJECT_B_ID }));
734
+
735
+ const call: RouteCall = await callRoute({
736
+ uri: route.uri,
737
+ body: route.body,
738
+ });
739
+
740
+ expect(call.thrown).toBeInstanceOf(NotAuthorizedException);
741
+ },
742
+ );
743
+
744
+ test.each(routes)(
745
+ "$uri refuses before it touches the database",
746
+ async (route: { uri: string; body: JSONObject }) => {
747
+ withProps(outsiderReachingForVictimProject());
748
+
749
+ await callRoute({ uri: route.uri, body: route.body });
750
+
751
+ expect(AIConversationService.findOneById).not.toHaveBeenCalled();
752
+ expect(AIConversationMessageService.findOneById).not.toHaveBeenCalled();
753
+ expect(ProjectService.findOneById).not.toHaveBeenCalled();
754
+ expect(providerLookupsRan()).toBe(false);
755
+ },
756
+ );
757
+
758
+ test.each(routes)(
759
+ "$uri is mounted behind UserMiddleware.getUserMiddleware",
760
+ (route: { uri: string; body: JSONObject }) => {
761
+ expect(mockRouter.match("post", route.uri).middlewares).toContain(
762
+ UserMiddleware.getUserMiddleware,
763
+ );
764
+ },
765
+ );
766
+ });
767
+
768
+ describe("the fixed routes still reject a missing tenantid the same way", () => {
769
+ it("send-message reports the missing project id as a bad request", async () => {
770
+ withProps(
771
+ buildProps({
772
+ userId: ATTACKER_USER_ID,
773
+ memberOfProjectId: PROJECT_A_ID,
774
+ }),
775
+ );
776
+
777
+ const call: RouteCall = await callRoute({
778
+ uri: "/ai-chat/send-message",
779
+ body: { content: "hello" },
780
+ });
781
+
782
+ expect(call.thrown).toBeInstanceOf(BadDataException);
783
+ expect(call.thrown).not.toBeInstanceOf(NotAuthorizedException);
784
+ });
785
+ });