@memberjunction/server 6.2.0-edge.4 → 6.2.0-edge.5

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 (287) hide show
  1. package/README.md +20 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/agentSessions/SessionJanitor.d.ts +91 -30
  4. package/dist/agentSessions/SessionJanitor.d.ts.map +1 -1
  5. package/dist/agentSessions/SessionJanitor.js +311 -81
  6. package/dist/agentSessions/SessionJanitor.js.map +1 -1
  7. package/dist/agentSessions/SessionManager.d.ts +5 -1
  8. package/dist/agentSessions/SessionManager.d.ts.map +1 -1
  9. package/dist/agentSessions/SessionManager.js +13 -1
  10. package/dist/agentSessions/SessionManager.js.map +1 -1
  11. package/dist/apiRoot.d.ts +32 -0
  12. package/dist/apiRoot.d.ts.map +1 -0
  13. package/dist/apiRoot.js +67 -0
  14. package/dist/apiRoot.js.map +1 -0
  15. package/dist/auth/AuthProviderCatalogRouter.d.ts.map +1 -1
  16. package/dist/auth/AuthProviderCatalogRouter.js +25 -0
  17. package/dist/auth/AuthProviderCatalogRouter.js.map +1 -1
  18. package/dist/auth/LoginScreenEngine.d.ts +22 -0
  19. package/dist/auth/LoginScreenEngine.d.ts.map +1 -0
  20. package/dist/auth/LoginScreenEngine.js +57 -0
  21. package/dist/auth/LoginScreenEngine.js.map +1 -0
  22. package/dist/auth/index.d.ts +1 -0
  23. package/dist/auth/index.d.ts.map +1 -1
  24. package/dist/auth/index.js +1 -0
  25. package/dist/auth/index.js.map +1 -1
  26. package/dist/auth/magicLink/MagicLinkRouter.d.ts.map +1 -1
  27. package/dist/auth/magicLink/MagicLinkRouter.js +3 -2
  28. package/dist/auth/magicLink/MagicLinkRouter.js.map +1 -1
  29. package/dist/config.d.ts +136 -22
  30. package/dist/config.d.ts.map +1 -1
  31. package/dist/config.js +43 -4
  32. package/dist/config.js.map +1 -1
  33. package/dist/coreRoutes.d.ts +29 -0
  34. package/dist/coreRoutes.d.ts.map +1 -0
  35. package/dist/coreRoutes.js +142 -0
  36. package/dist/coreRoutes.js.map +1 -0
  37. package/dist/generated/graphql-schemas/__mj.d.ts +369 -3
  38. package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
  39. package/dist/generated/graphql-schemas/__mj.js +1750 -7
  40. package/dist/generated/graphql-schemas/__mj.js.map +1 -1
  41. package/dist/generic/CacheInvalidationResolver.d.ts +43 -8
  42. package/dist/generic/CacheInvalidationResolver.d.ts.map +1 -1
  43. package/dist/generic/CacheInvalidationResolver.js +59 -19
  44. package/dist/generic/CacheInvalidationResolver.js.map +1 -1
  45. package/dist/generic/PushStatusResolver.js +1 -1
  46. package/dist/generic/PushStatusResolver.js.map +1 -1
  47. package/dist/generic/ResolverBase.d.ts +103 -44
  48. package/dist/generic/ResolverBase.d.ts.map +1 -1
  49. package/dist/generic/ResolverBase.js +264 -305
  50. package/dist/generic/ResolverBase.js.map +1 -1
  51. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  52. package/dist/generic/RunViewResolver.js +12 -0
  53. package/dist/generic/RunViewResolver.js.map +1 -1
  54. package/dist/globalCors.d.ts +33 -0
  55. package/dist/globalCors.d.ts.map +1 -0
  56. package/dist/globalCors.js +58 -0
  57. package/dist/globalCors.js.map +1 -0
  58. package/dist/index.d.ts +11 -0
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +112 -160
  61. package/dist/index.js.map +1 -1
  62. package/dist/multiTenancy/index.d.ts.map +1 -1
  63. package/dist/multiTenancy/index.js +22 -2
  64. package/dist/multiTenancy/index.js.map +1 -1
  65. package/dist/realtimeSessions/RealtimeSessionVerificationService.d.ts.map +1 -1
  66. package/dist/realtimeSessions/RealtimeSessionVerificationService.js +2 -1
  67. package/dist/realtimeSessions/RealtimeSessionVerificationService.js.map +1 -1
  68. package/dist/realtimeSessions/RealtimeVerifyRouter.d.ts.map +1 -1
  69. package/dist/realtimeSessions/RealtimeVerifyRouter.js +2 -1
  70. package/dist/realtimeSessions/RealtimeVerifyRouter.js.map +1 -1
  71. package/dist/realtimeSessions/RecordingAssemblyScheduler.d.ts +100 -0
  72. package/dist/realtimeSessions/RecordingAssemblyScheduler.d.ts.map +1 -0
  73. package/dist/realtimeSessions/RecordingAssemblyScheduler.js +173 -0
  74. package/dist/realtimeSessions/RecordingAssemblyScheduler.js.map +1 -0
  75. package/dist/realtimeSessions/index.d.ts +2 -0
  76. package/dist/realtimeSessions/index.d.ts.map +1 -1
  77. package/dist/realtimeSessions/index.js +2 -0
  78. package/dist/realtimeSessions/index.js.map +1 -1
  79. package/dist/realtimeSessions/ownedRealtimeSession.d.ts +30 -0
  80. package/dist/realtimeSessions/ownedRealtimeSession.d.ts.map +1 -0
  81. package/dist/realtimeSessions/ownedRealtimeSession.js +47 -0
  82. package/dist/realtimeSessions/ownedRealtimeSession.js.map +1 -0
  83. package/dist/realtimeSessions/sessionAccess.d.ts +6 -0
  84. package/dist/realtimeSessions/sessionAccess.d.ts.map +1 -1
  85. package/dist/realtimeSessions/sessionAccess.js +7 -3
  86. package/dist/realtimeSessions/sessionAccess.js.map +1 -1
  87. package/dist/resolvers/ArtifactFileResolver.d.ts +5 -1
  88. package/dist/resolvers/ArtifactFileResolver.d.ts.map +1 -1
  89. package/dist/resolvers/ArtifactFileResolver.js +9 -7
  90. package/dist/resolvers/ArtifactFileResolver.js.map +1 -1
  91. package/dist/resolvers/ConversationRunEventsResolver.d.ts +8 -4
  92. package/dist/resolvers/ConversationRunEventsResolver.d.ts.map +1 -1
  93. package/dist/resolvers/ConversationRunEventsResolver.js +6 -6
  94. package/dist/resolvers/ConversationRunEventsResolver.js.map +1 -1
  95. package/dist/resolvers/DatasetResolver.d.ts +24 -3
  96. package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
  97. package/dist/resolvers/DatasetResolver.js +57 -8
  98. package/dist/resolvers/DatasetResolver.js.map +1 -1
  99. package/dist/resolvers/EntityRecordNameResolver.d.ts +2 -1
  100. package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
  101. package/dist/resolvers/EntityRecordNameResolver.js +3 -2
  102. package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
  103. package/dist/resolvers/FetchEntityVectorsResolver.d.ts +18 -3
  104. package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
  105. package/dist/resolvers/FetchEntityVectorsResolver.js +100 -35
  106. package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
  107. package/dist/resolvers/FileResolver.d.ts +82 -11
  108. package/dist/resolvers/FileResolver.d.ts.map +1 -1
  109. package/dist/resolvers/FileResolver.js +279 -101
  110. package/dist/resolvers/FileResolver.js.map +1 -1
  111. package/dist/resolvers/GetDataContextDataResolver.d.ts +33 -7
  112. package/dist/resolvers/GetDataContextDataResolver.d.ts.map +1 -1
  113. package/dist/resolvers/GetDataContextDataResolver.js +117 -38
  114. package/dist/resolvers/GetDataContextDataResolver.js.map +1 -1
  115. package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
  116. package/dist/resolvers/GetDataResolver.js +6 -3
  117. package/dist/resolvers/GetDataResolver.js.map +1 -1
  118. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
  119. package/dist/resolvers/IntegrationDiscoveryResolver.js +4 -2
  120. package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
  121. package/dist/resolvers/QueryResolver.d.ts +2 -0
  122. package/dist/resolvers/QueryResolver.d.ts.map +1 -1
  123. package/dist/resolvers/QueryResolver.js +13 -0
  124. package/dist/resolvers/QueryResolver.js.map +1 -1
  125. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +2 -8
  126. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  127. package/dist/resolvers/RealtimeClientSessionResolver.js +10 -26
  128. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  129. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  130. package/dist/resolvers/RunAIAgentResolver.js +35 -6
  131. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  132. package/dist/resolvers/SearchKnowledgeStreamResolver.d.ts +12 -1
  133. package/dist/resolvers/SearchKnowledgeStreamResolver.d.ts.map +1 -1
  134. package/dist/resolvers/SearchKnowledgeStreamResolver.js +15 -2
  135. package/dist/resolvers/SearchKnowledgeStreamResolver.js.map +1 -1
  136. package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
  137. package/dist/resolvers/SqlLoggingConfigResolver.js +2 -3
  138. package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
  139. package/dist/resolvers/TaskGraphFrameResolver.d.ts +2 -1
  140. package/dist/resolvers/TaskGraphFrameResolver.d.ts.map +1 -1
  141. package/dist/resolvers/TaskGraphFrameResolver.js +28 -1
  142. package/dist/resolvers/TaskGraphFrameResolver.js.map +1 -1
  143. package/dist/resolvers/TestQuerySQLResolver.d.ts +24 -0
  144. package/dist/resolvers/TestQuerySQLResolver.d.ts.map +1 -1
  145. package/dist/resolvers/TestQuerySQLResolver.js +65 -13
  146. package/dist/resolvers/TestQuerySQLResolver.js.map +1 -1
  147. package/dist/resolvers/TransactionGroupResolver.d.ts +19 -2
  148. package/dist/resolvers/TransactionGroupResolver.d.ts.map +1 -1
  149. package/dist/resolvers/TransactionGroupResolver.js +102 -24
  150. package/dist/resolvers/TransactionGroupResolver.js.map +1 -1
  151. package/dist/resolvers/agent-run-data-guard.d.ts +31 -0
  152. package/dist/resolvers/agent-run-data-guard.d.ts.map +1 -0
  153. package/dist/resolvers/agent-run-data-guard.js +72 -0
  154. package/dist/resolvers/agent-run-data-guard.js.map +1 -0
  155. package/dist/rest/RealtimeRecordingHandler.d.ts +49 -0
  156. package/dist/rest/RealtimeRecordingHandler.d.ts.map +1 -0
  157. package/dist/rest/RealtimeRecordingHandler.js +383 -0
  158. package/dist/rest/RealtimeRecordingHandler.js.map +1 -0
  159. package/dist/rest/ViewOperationsHandler.d.ts +3 -1
  160. package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
  161. package/dist/rest/ViewOperationsHandler.js +21 -12
  162. package/dist/rest/ViewOperationsHandler.js.map +1 -1
  163. package/dist/rest/setupRESTEndpoints.d.ts +6 -0
  164. package/dist/rest/setupRESTEndpoints.d.ts.map +1 -1
  165. package/dist/rest/setupRESTEndpoints.js +6 -0
  166. package/dist/rest/setupRESTEndpoints.js.map +1 -1
  167. package/dist/serverExtensionReservedRoots.d.ts +12 -10
  168. package/dist/serverExtensionReservedRoots.d.ts.map +1 -1
  169. package/dist/serverExtensionReservedRoots.js +14 -17
  170. package/dist/serverExtensionReservedRoots.js.map +1 -1
  171. package/dist/services/MJStorageBlobStore.d.ts +8 -1
  172. package/dist/services/MJStorageBlobStore.d.ts.map +1 -1
  173. package/dist/services/MJStorageBlobStore.js +25 -19
  174. package/dist/services/MJStorageBlobStore.js.map +1 -1
  175. package/dist/services/StartTaskGraphDispatcher.js +2 -2
  176. package/dist/services/StartTaskGraphDispatcher.js.map +1 -1
  177. package/dist/services/TaskGraphActionRunner.d.ts +5 -3
  178. package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
  179. package/dist/services/TaskGraphActionRunner.js +27 -5
  180. package/dist/services/TaskGraphActionRunner.js.map +1 -1
  181. package/dist/services/TaskGraphAgentRunner.d.ts +14 -2
  182. package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -1
  183. package/dist/services/TaskGraphAgentRunner.js +157 -9
  184. package/dist/services/TaskGraphAgentRunner.js.map +1 -1
  185. package/dist/services/TaskGraphContinuationDeliverer.d.ts +54 -2
  186. package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
  187. package/dist/services/TaskGraphContinuationDeliverer.js +598 -15
  188. package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
  189. package/dist/sharedCache.d.ts.map +1 -1
  190. package/dist/sharedCache.js +3 -0
  191. package/dist/sharedCache.js.map +1 -1
  192. package/package.json +99 -98
  193. package/src/__tests__/DatasetResolver.itemFilters.test.ts +201 -0
  194. package/src/__tests__/EntityRecordNameResolver.rowSecurity.test.ts +152 -0
  195. package/src/__tests__/FetchEntityVectorsResolver.test.ts +163 -15
  196. package/src/__tests__/FileResolver.storageAccess.test.ts +562 -0
  197. package/src/__tests__/GetDataContextDataResolver.sqlItems.test.ts +416 -0
  198. package/src/__tests__/ListOperationsResolver.adhocScreen.test.ts +137 -0
  199. package/src/__tests__/LoginScreenEngine.test.ts +86 -0
  200. package/src/__tests__/MJStorageBlobStore.storageGate.test.ts +63 -0
  201. package/src/__tests__/RecordingAssemblyScheduler.test.ts +178 -0
  202. package/src/__tests__/ResolverBase.adhocReadScope.test.ts +133 -2
  203. package/src/__tests__/ResolverBase.balancedClause.test.ts +155 -0
  204. package/src/__tests__/ResolverBase.clauseUnscopedRead.test.ts +127 -0
  205. package/src/__tests__/ResolverBase.clientOldValues.test.ts +209 -6
  206. package/src/__tests__/ResolverBase.filterEscaping.test.ts +5 -2
  207. package/src/__tests__/RunAIAgentResolver.branch.test.ts +1 -0
  208. package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +5 -3
  209. package/src/__tests__/RunAIAgentResolver.runDataGuard.test.ts +258 -0
  210. package/src/__tests__/RunViewResolver.cacheCheckScreen.test.ts +165 -0
  211. package/src/__tests__/SearchKnowledgeStreamResolver.test.ts +161 -0
  212. package/src/__tests__/SessionJanitor.test.ts +365 -10
  213. package/src/__tests__/SessionManager.test.ts +27 -0
  214. package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +25 -0
  215. package/src/__tests__/TaskGraphAgentRunner.test.ts +266 -0
  216. package/src/__tests__/TaskGraphContinuationDeliverer.test.ts +356 -22
  217. package/src/__tests__/TaskGraphFrameResolver.test.ts +22 -0
  218. package/src/__tests__/TestQuerySQLResolver.test.ts +274 -0
  219. package/src/__tests__/TransactionGroupResolver.refusals.test.ts +98 -1
  220. package/src/__tests__/UpdateMJDataContextItem.oldValues.test.ts +220 -0
  221. package/src/__tests__/ViewOperationsHandler.clauseScreen.test.ts +157 -0
  222. package/src/__tests__/agent-run-data-guard.test.ts +130 -0
  223. package/src/__tests__/apiRootBoot.test.ts +202 -0
  224. package/src/__tests__/cacheInvalidationRecordData.test.ts +64 -0
  225. package/src/__tests__/conversationRunEvents.test.ts +56 -14
  226. package/src/__tests__/globalCors.test.ts +217 -0
  227. package/src/__tests__/multiTenancy.postgresRewrite.test.ts +181 -0
  228. package/src/__tests__/multiTenancy.security.test.ts +51 -0
  229. package/src/__tests__/multiTenancy.test.ts +1 -1
  230. package/src/__tests__/ownedRealtimeSession.test.ts +39 -0
  231. package/src/__tests__/pushStatusResolver.filter.test.ts +7 -0
  232. package/src/__tests__/realtimeRecordingHandler.test.ts +652 -0
  233. package/src/__tests__/serverExtensionHostOverlay.test.ts +66 -0
  234. package/src/__tests__/serverExtensionReservedRoots.test.ts +11 -98
  235. package/src/__tests__/sharedCache.test.ts +5 -3
  236. package/src/agentSessions/SessionJanitor.ts +369 -93
  237. package/src/agentSessions/SessionManager.ts +13 -1
  238. package/src/apiRoot.ts +78 -0
  239. package/src/auth/AuthProviderCatalogRouter.ts +26 -0
  240. package/src/auth/LoginScreenEngine.ts +50 -0
  241. package/src/auth/index.ts +1 -0
  242. package/src/auth/magicLink/MagicLinkRouter.ts +3 -2
  243. package/src/config.ts +46 -5
  244. package/src/coreRoutes.ts +166 -0
  245. package/src/generated/graphql-schemas/__mj.ts +1273 -10
  246. package/src/generic/CacheInvalidationResolver.ts +73 -18
  247. package/src/generic/PushStatusResolver.ts +1 -1
  248. package/src/generic/ResolverBase.ts +304 -325
  249. package/src/generic/RunViewResolver.ts +17 -0
  250. package/src/globalCors.ts +71 -0
  251. package/src/index.ts +116 -174
  252. package/src/multiTenancy/index.ts +24 -2
  253. package/src/realtimeSessions/RealtimeSessionVerificationService.ts +2 -1
  254. package/src/realtimeSessions/RealtimeVerifyRouter.ts +2 -1
  255. package/src/realtimeSessions/RecordingAssemblyScheduler.ts +234 -0
  256. package/src/realtimeSessions/index.ts +2 -0
  257. package/src/realtimeSessions/ownedRealtimeSession.ts +55 -0
  258. package/src/realtimeSessions/sessionAccess.ts +7 -3
  259. package/src/resolvers/ArtifactFileResolver.ts +9 -8
  260. package/src/resolvers/ConversationRunEventsResolver.ts +15 -11
  261. package/src/resolvers/DatasetResolver.ts +70 -10
  262. package/src/resolvers/EntityRecordNameResolver.ts +3 -2
  263. package/src/resolvers/FetchEntityVectorsResolver.ts +121 -39
  264. package/src/resolvers/FileResolver.ts +321 -106
  265. package/src/resolvers/GetDataContextDataResolver.ts +139 -41
  266. package/src/resolvers/GetDataResolver.ts +6 -3
  267. package/src/resolvers/IntegrationDiscoveryResolver.ts +4 -2
  268. package/src/resolvers/QueryResolver.ts +11 -0
  269. package/src/resolvers/RealtimeClientSessionResolver.ts +10 -33
  270. package/src/resolvers/RunAIAgentResolver.ts +39 -4
  271. package/src/resolvers/SearchKnowledgeStreamResolver.ts +19 -3
  272. package/src/resolvers/SqlLoggingConfigResolver.ts +2 -4
  273. package/src/resolvers/TaskGraphFrameResolver.ts +33 -1
  274. package/src/resolvers/TestQuerySQLResolver.ts +68 -13
  275. package/src/resolvers/TransactionGroupResolver.ts +131 -27
  276. package/src/resolvers/__tests__/GetDataResolver.validation.test.ts +95 -0
  277. package/src/resolvers/agent-run-data-guard.ts +89 -0
  278. package/src/rest/RealtimeRecordingHandler.ts +464 -0
  279. package/src/rest/ViewOperationsHandler.ts +29 -13
  280. package/src/rest/setupRESTEndpoints.ts +6 -0
  281. package/src/serverExtensionReservedRoots.ts +14 -17
  282. package/src/services/MJStorageBlobStore.ts +31 -30
  283. package/src/services/StartTaskGraphDispatcher.ts +2 -2
  284. package/src/services/TaskGraphActionRunner.ts +25 -5
  285. package/src/services/TaskGraphAgentRunner.ts +188 -11
  286. package/src/services/TaskGraphContinuationDeliverer.ts +704 -14
  287. package/src/sharedCache.ts +3 -0
@@ -1,9 +1,13 @@
1
1
  import { Arg, Ctx, Field, ObjectType, Query, Resolver } from "type-graphql";
2
+ import type { DatabaseProviderBase, UserInfo } from "@memberjunction/core";
3
+ import { IsValidUUID, UUIDsEqual } from "@memberjunction/global";
4
+ import { DataContext, DataContextItem } from "@memberjunction/data-context";
5
+ import { MJDataContextEntity, MJDataContextItemEntity } from "@memberjunction/core-entities";
6
+ import { RenderPipeline } from "@memberjunction/generic-database-provider";
2
7
  import { AppContext } from "../types.js";
3
- import { DataContext } from "@memberjunction/data-context";
4
- import { GetReadOnlyDataSource, GetReadOnlyProvider } from "../util.js";
5
- import { MJDataContextItemEntity } from "@memberjunction/core-entities";
8
+ import { GetReadOnlyProvider } from "../util.js";
6
9
  import { ResolverBase } from "../generic/ResolverBase.js";
10
+ import { IsScopeLimitedPrincipal } from "../auth/scopeLimitedPrincipal.js";
7
11
 
8
12
  @ObjectType()
9
13
  export class GetDataContextItemDataOutputType {
@@ -39,6 +43,28 @@ export class GetDataContextDataOutputType {
39
43
  }
40
44
 
41
45
 
46
+ /** The outcome of loading one data context item: its data as JSON, or why it has none. */
47
+ interface ItemOutcome {
48
+ Result: string | null;
49
+ Error: string | null;
50
+ }
51
+
52
+ /** Which `sql` items of a data context may run: the read-only provider, and each refused item's reason. */
53
+ interface SQLItemGate {
54
+ Provider: DatabaseProviderBase | null;
55
+ Refusals: Map<DataContextItem, string>;
56
+ }
57
+
58
+ /**
59
+ * Loads the data of data contexts and their items.
60
+ *
61
+ * Items of type `sql` hold raw SQL, which is held to the rules ExecuteAdhocQuery applies to
62
+ * caller-supplied SQL. It runs only for the data context's owner or an administrator (Owner-type
63
+ * user), never for a scope-limited session, only on the read-only provider, and only as a single read
64
+ * statement over entity base views the caller may read in full. A SQL item that may not run is
65
+ * refused on its own, and the other items still load. Items of the other types load through entity
66
+ * permissions for the calling user.
67
+ */
42
68
  @Resolver()
43
69
  export class GetDataContextDataResolver extends ResolverBase {
44
70
  /**
@@ -52,30 +78,25 @@ export class GetDataContextDataResolver extends ResolverBase {
52
78
  ) {
53
79
  // Check API key scope authorization for data context read
54
80
  await this.CheckAPIKeyScopeAuthorization('datacontext:read', DataContextItemID, appCtx.userPayload);
81
+ if (!IsValidUUID(DataContextItemID)) {
82
+ return { Success: false, ErrorMessage: 'DataContextItemID must be a UUID', Result: null };
83
+ }
55
84
 
56
85
  try {
57
- const ds = GetReadOnlyDataSource(appCtx.dataSources, {
58
- allowFallbackToReadWrite: true,
59
- })
86
+ const user: UserInfo = appCtx.userPayload.userRecord;
60
87
  const md = GetReadOnlyProvider(appCtx.providers, {allowFallbackToReadWrite: true});
61
- const dciData = await md.GetEntityObject<MJDataContextItemEntity>("MJ: Data Context Items", appCtx.userPayload.userRecord);
88
+ const dciData = await md.GetEntityObject<MJDataContextItemEntity>("MJ: Data Context Items", user);
62
89
  if (await dciData.Load(DataContextItemID)) {
63
90
  const dci = DataContext.CreateDataContextItem(); // use class factory to get whatever lowest level sub-class is registered
64
- await dci.LoadMetadataFromEntityRecord(dciData, md, appCtx.userPayload.userRecord);
91
+ await dci.LoadMetadataFromEntityRecord(dciData, md, user);
92
+ const ownerID = dci.Type === 'sql' ? await this.loadDataContextOwnerID(dciData.DataContextID, md, user) : null;
93
+ const gate = this.authorizeSQLItems([dci], ownerID, appCtx);
65
94
  // now the metadata is loaded so we can call the regular load function
66
- if (await dci.LoadData(ds)) {
67
- return {
68
- Success: true,
69
- ErrorMessage: null,
70
- Result: JSON.stringify(dci.Data),
71
- }
72
- }
73
- else {
74
- return {
75
- Success: false,
76
- ErrorMessage: 'Error loading data context item data',
77
- Result: null,
78
- }
95
+ const outcome = await this.loadItem(dci, gate, user, false);
96
+ return {
97
+ Success: outcome.Error === null,
98
+ ErrorMessage: outcome.Error,
99
+ Result: outcome.Result,
79
100
  }
80
101
  }
81
102
  else {
@@ -89,14 +110,15 @@ export class GetDataContextDataResolver extends ResolverBase {
89
110
  catch (e) {
90
111
  return {
91
112
  Success: false,
92
- ErrorMessage: e,
113
+ ErrorMessage: e instanceof Error ? e.message : String(e),
93
114
  Result: null,
94
115
  }
95
116
  }
96
117
  }
97
118
 
98
119
  /**
99
- * Returns data for a given data context.
120
+ * Returns data for a given data context. `Results` and `ErrorMessages` hold one entry per item, in
121
+ * item order. `Success` is true, and `ErrorMessages` null, only when every item loaded.
100
122
  * @param DataContextID
101
123
  */
102
124
  @Query(() => GetDataContextDataOutputType)
@@ -106,38 +128,114 @@ export class GetDataContextDataResolver extends ResolverBase {
106
128
  ) {
107
129
  // Check API key scope authorization for data context read
108
130
  await this.CheckAPIKeyScopeAuthorization('datacontext:read', DataContextID, appCtx.userPayload);
131
+ // DataContext.LoadMetadata places this ID in the item filter, so it must be a plain UUID.
132
+ if (!IsValidUUID(DataContextID)) {
133
+ return { Success: false, ErrorMessages: ['DataContextID must be a UUID'], Results: null };
134
+ }
109
135
 
110
136
  try {
111
137
  // our job here is to load the entire data context, so we do that with the Data Context object
138
+ const user: UserInfo = appCtx.userPayload.userRecord;
139
+ const md = GetReadOnlyProvider(appCtx.providers, { allowFallbackToReadWrite: true });
112
140
  const dc = new DataContext();
113
- const ds = GetReadOnlyDataSource(appCtx.dataSources, {
114
- allowFallbackToReadWrite: true,
115
- })
116
- const success = await dc.Load(DataContextID, ds, true, false, 0, appCtx.userPayload.userRecord);
117
- if (success) {
118
- const retVal = {
119
- Success: true,
120
- ErrorMessages: null,
121
- Results: dc.Items.map((item) => {
122
- return JSON.stringify(item.Data);
123
- }),
124
- }
125
- return retVal;
126
- }
127
- else {
141
+ if (!(await dc.LoadMetadata(DataContextID, user, md))) {
128
142
  return {
129
143
  Success: false,
130
144
  ErrorMessages: ['Error loading data context'],
131
- Results: null,
145
+ Results: null,
132
146
  }
133
147
  }
148
+ const gate = this.authorizeSQLItems(dc.Items, dc.MJDataContextEntity.UserID, appCtx);
149
+ const outcomes = await Promise.all(dc.Items.map((item) => this.loadItem(item, gate, user, true)));
150
+ const allLoaded = outcomes.every((outcome) => outcome.Error === null);
151
+ return {
152
+ Success: allLoaded,
153
+ ErrorMessages: allLoaded ? null : outcomes.map((outcome) => outcome.Error),
154
+ Results: outcomes.map((outcome) => outcome.Result),
155
+ }
134
156
  }
135
157
  catch (e) {
136
158
  return {
137
159
  Success: false,
138
- ErrorMessages: [e],
160
+ ErrorMessages: [e instanceof Error ? e.message : String(e)],
139
161
  Results: null,
140
162
  }
141
163
  }
142
164
  }
143
- }
165
+
166
+ /**
167
+ * Decides which `sql` items among `items` may run. Returns the read-only provider they run on, and
168
+ * the reason for each SQL item that may not run.
169
+ */
170
+ private authorizeSQLItems(items: DataContextItem[], ownerID: string | null, appCtx: AppContext): SQLItemGate {
171
+ const refusals = new Map<DataContextItem, string>();
172
+ const sqlItems = items.filter((item) => item.Type === 'sql');
173
+ if (sqlItems.length === 0) {
174
+ return { Provider: null, Refusals: refusals };
175
+ }
176
+ const user: UserInfo | undefined = appCtx.userPayload?.userRecord;
177
+ const provider = GetReadOnlyProvider(appCtx.providers, { allowFallbackToReadWrite: false });
178
+ for (const item of sqlItems) {
179
+ const refusal = this.sqlItemRefusal(item, ownerID, user, provider);
180
+ if (refusal !== null) {
181
+ refusals.set(item, refusal);
182
+ }
183
+ }
184
+ return { Provider: provider, Refusals: refusals };
185
+ }
186
+
187
+ /** Returns why `item` may not run for `user`, or null when it may. */
188
+ private sqlItemRefusal(item: DataContextItem, ownerID: string | null, user: UserInfo | undefined, provider: DatabaseProviderBase | null): string | null {
189
+ if (!user || IsScopeLimitedPrincipal(user)) {
190
+ return 'SQL data context items cannot be run by a scope-limited or unidentified session.';
191
+ }
192
+ if (!this.mayRunDataContextSQL(ownerID, user)) {
193
+ return 'Only the owner of this data context, or an administrator, may run its SQL items.';
194
+ }
195
+ if (!provider) {
196
+ return 'No read-only data source is configured, so SQL data context items cannot run.';
197
+ }
198
+ return this.screenItemSQL(item.SQL ?? '', provider, user);
199
+ }
200
+
201
+ /** True when `user` owns the data context or is an administrator (Owner-type user). */
202
+ private mayRunDataContextSQL(ownerID: string | null, user: UserInfo): boolean {
203
+ const isOwner = !!ownerID && UUIDsEqual(ownerID, user.ID);
204
+ return isOwner || user.Type?.trim().toLowerCase() === 'owner';
205
+ }
206
+
207
+ /**
208
+ * Returns why `sqlText` may not run, or null when it is a single read statement whose every table
209
+ * reference is an entity base view `user` may read without row-level or field-level narrowing. It is
210
+ * the check ExecuteAdhocQuery makes on the rendered SQL.
211
+ */
212
+ private screenItemSQL(sqlText: string, provider: DatabaseProviderBase, user: UserInfo): string | null {
213
+ try {
214
+ const rendered = RenderPipeline.Run(sqlText, { Platform: provider.PlatformKey, ContextUser: user, RequireReadStatement: true });
215
+ this.assertFullQueryUsesReadableEntityViews(rendered.Trace.AfterTemplates, provider, user, 'data context SQL');
216
+ return null;
217
+ }
218
+ catch (e) {
219
+ const reason = e instanceof Error ? e.message : String(e);
220
+ return reason || 'The SQL of this item did not pass the read-only check.';
221
+ }
222
+ }
223
+
224
+ /** Loads one item's data unless `gate` refused it. Returns the data as JSON, or why there is none. */
225
+ private async loadItem(item: DataContextItem, gate: SQLItemGate, user: UserInfo, forceRefresh: boolean): Promise<ItemOutcome> {
226
+ const refusal = gate.Refusals.get(item);
227
+ if (refusal !== undefined) {
228
+ return { Result: null, Error: refusal };
229
+ }
230
+ if (await item.LoadData(gate.Provider, forceRefresh, false, 0, user)) {
231
+ return { Result: JSON.stringify(item.Data), Error: null };
232
+ }
233
+ return { Result: null, Error: item.DataLoadingError || 'Error loading data context item data' };
234
+ }
235
+
236
+ /** Returns the owner (`UserID`) of a data context, or null when the caller cannot load it. */
237
+ private async loadDataContextOwnerID(dataContextID: string, provider: DatabaseProviderBase, user: UserInfo): Promise<string | null> {
238
+ const dataContext = await provider.GetEntityObject<MJDataContextEntity>('MJ: Data Contexts', user);
239
+ return (await dataContext.Load(dataContextID)) ? dataContext.UserID : null;
240
+ }
241
+ }
@@ -158,14 +158,17 @@ export class GetDataResolver {
158
158
  // Defense-in-depth: even system-token holders get SELECT-only SQL here.
159
159
  // The read-only pool is the primary control, but if it is ever
160
160
  // misconfigured to a read-write login this validator still refuses DML/DDL
161
- // (mirrors AdhocQueryResolver).
162
- const validation = SQLExpressionValidator.Instance.validateFullQuery(resolvedSQL);
161
+ // (mirrors AdhocQueryResolver). The validator reads exactly the text that
162
+ // runs: it is trimmed here as validate() trims it, and literal \n sequences
163
+ // are not rewritten as ValidateFullQuery would.
164
+ const sqlToRun = resolvedSQL.trim();
165
+ const validation = SQLExpressionValidator.Instance.validate(sqlToRun, { context: 'full_query', Dialect: platform });
163
166
  if (!validation.valid) {
164
167
  return { result: null, error: validation.error || 'SQL validation failed' };
165
168
  }
166
169
 
167
170
  const request = new sql.Request(readOnlyDataSource);
168
- const result = await request.query(resolvedSQL);
171
+ const result = await request.query(sqlToRun);
169
172
  return { result: result.recordset, error: null };
170
173
  } catch (err) {
171
174
  // Extract clean SQL error message
@@ -3,7 +3,7 @@ import { CompositeKey, DatabaseProviderBase, EntityInfo, LocalCacheManager, Meta
3
3
  import { GetReadOnlyProvider, GetReadWriteProvider } from "../util.js";
4
4
  import { NoLog } from "../logging/NoLog.js";
5
5
  import { UUIDsEqual } from "@memberjunction/global";
6
- import { DrainResponseBody } from "@memberjunction/network-utils";
6
+ import { DrainResponseBody, SafeFetch } from "@memberjunction/network-utils";
7
7
  import { CronExpressionHelper } from "@memberjunction/scheduling-engine";
8
8
  import {
9
9
  MJCompanyIntegrationEntity,
@@ -6467,7 +6467,9 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
6467
6467
 
6468
6468
  private async sendWebhook(url: string, payload: Record<string, unknown>): Promise<void> {
6469
6469
  try {
6470
- const response = await fetch(url, {
6470
+ // SECURITY: the webhook URL is caller-supplied, so go through the SSRF guard —
6471
+ // SafeFetch validates the URL (and every redirect hop) resolves to a public address.
6472
+ const response = await SafeFetch(url, {
6471
6473
  method: 'POST',
6472
6474
  headers: { 'Content-Type': 'application/json' },
6473
6475
  body: JSON.stringify(payload)
@@ -101,6 +101,12 @@ export class RunQueryCacheStatusInput {
101
101
 
102
102
  @Field(() => Int, { description: 'The number of rows in cached results' })
103
103
  rowCount: number; // case-violation-ok-legacy-back-compat: the property name is the GraphQL schema field name — renaming it breaks every client query
104
+
105
+ @Field(() => String, {
106
+ nullable: true,
107
+ description: "The query definition hash the cached results were written under, as this server returned it. When it differs from the server's, the result is 'stale' with fresh rows. Empty string for a slot with no hash; omit to skip the definition check."
108
+ })
109
+ definitionHash?: string; // case-violation-ok-legacy-back-compat: the property name is the GraphQL schema field name, matching its siblings
104
110
  }
105
111
 
106
112
  @InputType()
@@ -140,6 +146,9 @@ export class RunQueryWithCacheCheckResultOutput {
140
146
 
141
147
  @Field(() => String, { nullable: true, description: 'Error message if status is error' })
142
148
  errorMessage?: string; // case-violation-ok-legacy-back-compat: the property name is the GraphQL schema field name — renaming it breaks every client query
149
+
150
+ @Field(() => String, { nullable: true, description: "The server's definition hash for the query; store it with the cached results and send it back in cacheStatus.definitionHash" })
151
+ definitionHash?: string; // case-violation-ok-legacy-back-compat: the property name is the GraphQL schema field name, matching its siblings
143
152
  }
144
153
 
145
154
  @ObjectType()
@@ -558,6 +567,7 @@ export class RunQueryResolver extends ResolverBase {
558
567
  cacheStatus: item.cacheStatus ? {
559
568
  maxUpdatedAt: item.cacheStatus.maxUpdatedAt,
560
569
  rowCount: item.cacheStatus.rowCount,
570
+ definitionHash: item.cacheStatus.definitionHash ?? undefined,
561
571
  } : undefined,
562
572
  }));
563
573
 
@@ -572,6 +582,7 @@ export class RunQueryResolver extends ResolverBase {
572
582
  maxUpdatedAt: result.maxUpdatedAt,
573
583
  rowCount: result.rowCount,
574
584
  errorMessage: result.errorMessage,
585
+ definitionHash: result.definitionHash,
575
586
  }));
576
587
 
577
588
  return {
@@ -87,6 +87,7 @@ import { ResolveWidgetGuestRunContext, ResolveScopedAnonymousRunUser, ResolveRec
87
87
  import { RunWithTrustedSessionConfigWrites } from '@memberjunction/core-entities-server';
88
88
  import { BuildMintConfigStamps, LoadInheritedVerifiedIdentity } from '../realtimeSessions/sessionConfigStamps.js';
89
89
  import { ParseConfigObject } from '../realtimeSessions/verificationCore.js';
90
+ import { LoadOwnedRealtimeSession } from '../realtimeSessions/ownedRealtimeSession.js';
90
91
 
91
92
  /**
92
93
  * Progress steps worth narrating to the realtime model — mirrors the normal agent-run path's filter
@@ -415,7 +416,7 @@ export class CancelRealtimeSessionToolResult {
415
416
  * Result of {@link RealtimeClientSessionResolver.UploadRealtimeRecording} — a structured
416
417
  * success/failure envelope. A recording-storage failure must never throw to the browser (the
417
418
  * session it belongs to has already ended), so problems come back as `Success: false` with a
418
- * human-readable reason; only ownership/authn violations (from `loadOwnedSession`) still throw.
419
+ * human-readable reason; only ownership/authn violations (from `LoadOwnedRealtimeSession`) still throw.
419
420
  */
420
421
  @ObjectType()
421
422
  export class UploadRealtimeRecordingResult {
@@ -944,7 +945,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
944
945
  * the call and uploads the assembled blob (base64) once the session ends.
945
946
  *
946
947
  * Hard gates (in order):
947
- * 1. Ownership — `loadOwnedSession` enforces `UserID === contextUser.ID` (throws on violation).
948
+ * 1. Ownership — `LoadOwnedRealtimeSession` enforces `UserID === contextUser.ID` (throws on violation).
948
949
  * 2. Consent — `consent !== true` is a hard refusal: no audio is ever stored without it.
949
950
  * 3. Storage configuration — the session's agent must resolve a recording storage account (the
950
951
  * agent's `RecordingStorageProviderID`, else `AttachmentStorageProviderID`).
@@ -973,7 +974,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
973
974
  @Arg('peaks', () => [Float], { nullable: true }) peaks?: number[],
974
975
  ): Promise<UploadRealtimeRecordingResult> {
975
976
  const { contextUser, provider } = this.requireUserAndProvider(ctx.userPayload, ctx.providers);
976
- const session = await this.loadOwnedSession(agentSessionId, contextUser, provider);
977
+ const session = await LoadOwnedRealtimeSession(agentSessionId, contextUser, provider);
977
978
 
978
979
  // HARD consent gate — no audio is ever stored without an explicit grant.
979
980
  if (consent !== true) {
@@ -1060,7 +1061,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
1060
1061
  ): Promise<boolean> {
1061
1062
  try {
1062
1063
  const { contextUser, provider } = this.requireUserAndProvider(ctx.userPayload, ctx.providers);
1063
- const session = await this.loadOwnedSession(agentSessionId, contextUser, provider);
1064
+ const session = await LoadOwnedRealtimeSession(agentSessionId, contextUser, provider);
1064
1065
  // The consolidated recording already landed and its upload deleted the shards; a shard
1065
1066
  // arriving now (one still on the wire at hang-up) would be an orphan nothing deletes.
1066
1067
  if (session.RecordingFileID) {
@@ -1183,7 +1184,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
1183
1184
  @Arg('callId', () => String, { nullable: true }) callId?: string,
1184
1185
  ): Promise<CancelRealtimeSessionToolResult> {
1185
1186
  const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
1186
- await this.loadOwnedSession(agentSessionId, contextUser, provider);
1187
+ await LoadOwnedRealtimeSession(agentSessionId, contextUser, provider);
1187
1188
  try {
1188
1189
  const aborted = this.clientSessionService.CancelInFlightDelegations(agentSessionId, callId);
1189
1190
  return { AbortedCount: aborted, Success: true };
@@ -1230,7 +1231,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
1230
1231
  @Arg('usageDetailsJson', () => String, { nullable: true }) usageDetailsJson?: string,
1231
1232
  ): Promise<boolean> {
1232
1233
  const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
1233
- const session = await this.loadOwnedSession(agentSessionId, contextUser, provider);
1234
+ const session = await LoadOwnedRealtimeSession(agentSessionId, contextUser, provider);
1234
1235
 
1235
1236
  const inputDelta = this.clampTokenDelta(inputTokens);
1236
1237
  const outputDelta = this.clampTokenDelta(outputTokens);
@@ -1358,7 +1359,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
1358
1359
  @Ctx() { userPayload, providers }: AppContext,
1359
1360
  ): Promise<boolean> {
1360
1361
  const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
1361
- const session = await this.loadOwnedSession(agentSessionId, contextUser, provider);
1362
+ const session = await LoadOwnedRealtimeSession(agentSessionId, contextUser, provider);
1362
1363
 
1363
1364
  if (!this.isChannelInSessionScope(session, channelName)) {
1364
1365
  LogError(`SaveSessionChannelState: channel '${channelName}' is not in session ${agentSessionId}'s resolved scope — state not saved.`);
@@ -1441,7 +1442,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
1441
1442
  @Ctx() { userPayload, providers }: AppContext,
1442
1443
  ): Promise<SaveSessionChannelArtifactResult> {
1443
1444
  const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
1444
- const session = await this.loadOwnedSession(agentSessionId, contextUser, provider);
1445
+ const session = await LoadOwnedRealtimeSession(agentSessionId, contextUser, provider);
1445
1446
 
1446
1447
  if (!this.isChannelInSessionScope(session, channelName)) {
1447
1448
  const message = `SaveSessionChannelArtifact: channel '${channelName}' is not in session ${agentSessionId}'s resolved scope — nothing saved.`;
@@ -1954,37 +1955,13 @@ export class RealtimeClientSessionResolver extends ResolverBase {
1954
1955
  contextUser: UserInfo,
1955
1956
  provider: IMetadataProvider,
1956
1957
  ): Promise<MJAIAgentSessionEntity> {
1957
- const session = await this.loadOwnedSession(agentSessionId, contextUser, provider);
1958
+ const session = await LoadOwnedRealtimeSession(agentSessionId, contextUser, provider);
1958
1959
  if (session.Status === 'Closed') {
1959
1960
  throw new Error(`Realtime session ${agentSessionId} is closed`);
1960
1961
  }
1961
1962
  return session;
1962
1963
  }
1963
1964
 
1964
- /**
1965
- * Loads a session and enforces inbound ownership (`UserID === contextUser.ID`) WITHOUT
1966
- * rejecting `Closed` sessions — {@link SaveSessionChannelState}'s final on-end flush
1967
- * legitimately arrives after the session closed. Throws when not found / not owned.
1968
- */
1969
- private async loadOwnedSession(
1970
- agentSessionId: string,
1971
- contextUser: UserInfo,
1972
- provider: IMetadataProvider,
1973
- ): Promise<MJAIAgentSessionEntity> {
1974
- const session = await provider.GetEntityObject<MJAIAgentSessionEntity>(SESSION_ENTITY, contextUser);
1975
- const loaded = await session.Load(agentSessionId);
1976
- if (!loaded) {
1977
- throw new Error(`Realtime session ${agentSessionId} not found`);
1978
- }
1979
- if (!UUIDsEqual(session.UserID, contextUser.ID)) {
1980
- LogError(
1981
- `RealtimeClientSessionResolver: ownership check failed — user ${contextUser.ID} attempted to operate on session ${agentSessionId} owned by ${session.UserID}`,
1982
- );
1983
- throw new Error('Not authorized: you do not own this realtime session');
1984
- }
1985
- return session;
1986
- }
1987
-
1988
1965
  /**
1989
1966
  * Resolves an ACTIVE channel definition (`MJ: AI Agent Channels`) by name from
1990
1967
  * {@link AIEngineBase}'s cached `AgentChannels` (provider-scoped engine instance, lazy
@@ -13,6 +13,7 @@ import { StartLivenessPulse } from '../generic/FireAndForgetHeartbeat.js';
13
13
  import { RequireSystemUser } from '../directives/RequireSystemUser.js';
14
14
  import { GetReadWriteProvider } from '../util.js';
15
15
  import { ResolveWidgetGuestRunContext, ElevateUserPayload } from '../realtimeWidget/widgetGuestElevation.js';
16
+ import { IsTrustedAgentRunCaller } from './agent-run-data-guard.js';
16
17
  import { SafeJSONParse, UUIDsEqual } from '@memberjunction/global';
17
18
  import { GetAttachmentService } from '@memberjunction/aiengine';
18
19
  import { NotificationEngine } from '@memberjunction/notifications';
@@ -252,6 +253,14 @@ export class RunAIAgentResolver extends ResolverBase {
252
253
  throw new Error(`AI Agent "${agentEntity.Name}" is not active (Status: ${agentEntity.Status})`);
253
254
  }
254
255
 
256
+ // Enforce agent run permissions for the calling user
257
+ if (currentUser && typeof AIEngine.Instance.CanUserRunAgent === 'function') {
258
+ const canRun = await AIEngine.Instance.CanUserRunAgent(agentEntity.ID, currentUser);
259
+ if (!canRun) {
260
+ throw new Error(`User does not have permission to run AI Agent "${agentEntity.Name}" (${agentEntity.ID})`);
261
+ }
262
+ }
263
+
255
264
  return agentEntity;
256
265
  }
257
266
 
@@ -294,6 +303,8 @@ export class RunAIAgentResolver extends ResolverBase {
294
303
  /** History floor for a conversation run — threaded into ExecuteAgentParams.ConversationHistoryFrom,
295
304
  * so the framework's own conversation reads (retrieval tools, artifacts, compaction) honour it too. */
296
305
  conversationHistoryFrom?: Date,
306
+ /** ExecuteAgentParams.TrustReservedRunData: true only for a caller IsTrustedAgentRunCaller accepts. */
307
+ trustReservedRunData: boolean = false,
297
308
  /** The conversation branch of the run — threaded into ExecuteAgentParams.ConversationBranchID. */
298
309
  conversationBranchId?: string | null
299
310
  ): Promise<AIAgentRunResult> {
@@ -366,6 +377,8 @@ export class RunAIAgentResolver extends ResolverBase {
366
377
  ConversationHistoryFrom: conversationHistoryFrom,
367
378
  ...(conversationBranchId ? { ConversationBranchID: conversationBranchId } : {}),
368
379
  data: parsedData,
380
+ // Whether the run may read the reserved data keys (scope, agent-type params); BaseAgent drops them otherwise.
381
+ TrustReservedRunData: trustReservedRunData,
369
382
  context: {
370
383
  dataSource: dataSource
371
384
  }
@@ -564,6 +577,9 @@ export class RunAIAgentResolver extends ResolverBase {
564
577
  await this.CheckAPIKeyScopeAuthorization('agent:execute', agentId, userPayload);
565
578
 
566
579
  const p = GetReadWriteProvider(providers);
580
+ // Only the system user or an API key holding the agent:trusted scope may set a run's scope and agent-type
581
+ // params through data; for anyone else BaseAgent drops those keys (ExecuteAgentParams.TrustReservedRunData).
582
+ const trustRunData = await IsTrustedAgentRunCaller({ UserPayload: userPayload, IsWidgetGuestRun: false, AgentID: agentId });
567
583
 
568
584
  if (fireAndForget) {
569
585
  // Fire-and-forget mode: start execution in background, return immediately.
@@ -572,7 +588,8 @@ export class RunAIAgentResolver extends ResolverBase {
572
588
  p, dataSource, agentId, userPayload, messagesJson, sessionId, pubSub,
573
589
  data, payload, lastRunId, autoPopulateLastRunPayload, configurationId,
574
590
  conversationDetailId, createArtifacts || false, createNotification || false,
575
- sourceArtifactId, sourceArtifactVersionId, undefined /*conversationId*/, planMode, requestedSkillIDs, taskGraphDebug
591
+ sourceArtifactId, sourceArtifactVersionId, undefined /*conversationId*/, planMode, requestedSkillIDs, taskGraphDebug,
592
+ undefined /*conversationHistoryFrom*/, trustRunData
576
593
  );
577
594
 
578
595
  LogStatus(`🔥 Fire-and-forget: Agent ${agentId} execution started in background for session ${sessionId}`);
@@ -607,7 +624,9 @@ export class RunAIAgentResolver extends ResolverBase {
607
624
  undefined, // runRef
608
625
  planMode,
609
626
  requestedSkillIDs,
610
- taskGraphDebug
627
+ taskGraphDebug,
628
+ undefined, // conversationHistoryFrom
629
+ trustRunData
611
630
  );
612
631
  }
613
632
 
@@ -640,6 +659,9 @@ export class RunAIAgentResolver extends ResolverBase {
640
659
  @Arg('requestedSkillIDs', () => [String], { nullable: true }) requestedSkillIDs?: string[]
641
660
  ): Promise<AIAgentRunResult> {
642
661
  const p = GetReadWriteProvider(providers);
662
+ // @RequireSystemUser admits only the system user, whom the rule trusts; it is still asked, so every
663
+ // resolver entry that takes a client's data decides the same way.
664
+ const trustRunData = await IsTrustedAgentRunCaller({ UserPayload: userPayload, IsWidgetGuestRun: false, AgentID: agentId });
643
665
  return this.executeAIAgent(
644
666
  p,
645
667
  dataSource,
@@ -662,7 +684,10 @@ export class RunAIAgentResolver extends ResolverBase {
662
684
  undefined, // conversationId (not pre-resolved on this path)
663
685
  undefined, // runRef
664
686
  planMode,
665
- requestedSkillIDs
687
+ requestedSkillIDs,
688
+ undefined, // taskGraphDebug
689
+ undefined, // conversationHistoryFrom
690
+ trustRunData
666
691
  );
667
692
  }
668
693
 
@@ -958,6 +983,12 @@ export class RunAIAgentResolver extends ResolverBase {
958
983
  const widgetElevation = await ResolveWidgetGuestRunContext(userPayload, p);
959
984
  const effectiveAgentId = widgetElevation ? widgetElevation.PinnedAgentId : agentId;
960
985
  const effectiveUserPayload = widgetElevation ? ElevateUserPayload(userPayload, widgetElevation.ElevatedUser) : userPayload;
986
+ // Only the system user or an API key holding the agent:trusted scope may set a run's scope and agent-type
987
+ // params through data (ExecuteAgentParams.TrustReservedRunData). Judged on the caller's own payload: a widget
988
+ // guest runs under an elevated server principal but is never trusted.
989
+ const trustRunData = await IsTrustedAgentRunCaller({
990
+ UserPayload: userPayload, IsWidgetGuestRun: widgetElevation !== null, AgentID: agentId,
991
+ });
961
992
 
962
993
  try {
963
994
  // Parsed before anything loads: a floor the caller asked for must never be dropped.
@@ -1021,6 +1052,7 @@ export class RunAIAgentResolver extends ResolverBase {
1021
1052
  sourceArtifactId, sourceArtifactVersionId, conversationId, planMode, requestedSkillIDs,
1022
1053
  undefined, // taskGraphDebug
1023
1054
  historyFrom,
1055
+ trustRunData,
1024
1056
  branchId
1025
1057
  );
1026
1058
 
@@ -1058,6 +1090,7 @@ export class RunAIAgentResolver extends ResolverBase {
1058
1090
  requestedSkillIDs,
1059
1091
  undefined, // taskGraphDebug
1060
1092
  historyFrom,
1093
+ trustRunData,
1061
1094
  branchId
1062
1095
  );
1063
1096
  } catch (error) {
@@ -1281,6 +1314,8 @@ export class RunAIAgentResolver extends ResolverBase {
1281
1314
  taskGraphDebug?: string,
1282
1315
  /** History floor — threaded through to ExecuteAgentParams.ConversationHistoryFrom. */
1283
1316
  conversationHistoryFrom?: Date,
1317
+ /** ExecuteAgentParams.TrustReservedRunData — see executeAIAgent. */
1318
+ trustReservedRunData: boolean = false,
1284
1319
  /** The conversation branch of the run — threaded through to ExecuteAgentParams.ConversationBranchID. */
1285
1320
  conversationBranchId?: string | null
1286
1321
  ): void {
@@ -1302,7 +1337,7 @@ export class RunAIAgentResolver extends ResolverBase {
1302
1337
  data, payload, undefined, lastRunId, autoPopulateLastRunPayload,
1303
1338
  configurationId, conversationDetailId, createArtifacts, createNotification,
1304
1339
  sourceArtifactId, sourceArtifactVersionId, conversationId, runRef, planMode, requestedSkillIDs, taskGraphDebug,
1305
- conversationHistoryFrom, conversationBranchId
1340
+ conversationHistoryFrom, trustReservedRunData, conversationBranchId
1306
1341
  ).catch((error: unknown) => {
1307
1342
  // Background execution failed unexpectedly (executeAIAgent has its own try-catch,
1308
1343
  // so this would only fire for truly unexpected errors).
@@ -18,8 +18,14 @@
18
18
  * Permission enforcement is identical to SearchKnowledge: each requested
19
19
  * scopeID is run through SearchScopePermissionResolver before the engine
20
20
  * is invoked. A denial publishes a single 'error' event and ends.
21
+ *
22
+ * 'provider' events are progress only — ProviderName, DurationMs and
23
+ * ResultCount. They arrive before the engine's permission pass, so they never
24
+ * carry a result: rows the caller's row filters would drop and unverified
25
+ * external-index hits must not reach the client. Results arrive in 'fused'
26
+ * and 'final'.
21
27
  */
22
- import { Resolver, Mutation, Arg, Ctx, ObjectType, Field, Float, ID, Subscription, Root, PubSub, PubSubEngine } from 'type-graphql';
28
+ import { Resolver, Mutation, Arg, Ctx, ObjectType, Field, Float, Int, ID, Subscription, Root, PubSub, PubSubEngine } from 'type-graphql';
23
29
  import { v4 as uuidv4 } from 'uuid';
24
30
  import { GraphQLJSON } from 'graphql-type-json';
25
31
  import { AppContext } from '../types.js';
@@ -59,10 +65,17 @@ export class SearchStreamNotification {
59
65
  @Field(() => Float, { nullable: true })
60
66
  DurationMs?: number;
61
67
 
62
- /** Set on `provider`, `fused`, `reranked`, `final` events. */
68
+ /**
69
+ * Set on `fused`, `reranked` and `final` events. Always empty on a `provider` event: a provider's
70
+ * hits arrive before the permission pass, so the stream reports only their count (`ResultCount`).
71
+ */
63
72
  @Field(() => [SearchKnowledgeResultItem], { nullable: true })
64
73
  Results?: SearchKnowledgeResultItem[];
65
74
 
75
+ /** Set on `provider` events: how many hits the provider returned, capped at `maxResults`. */
76
+ @Field(() => Int, { nullable: true })
77
+ ResultCount?: number;
78
+
66
79
  /** Set on `final` events. */
67
80
  @Field(() => SearchSourceCounts, { nullable: true })
68
81
  SourceCounts?: SearchSourceCounts;
@@ -253,12 +266,15 @@ export class SearchKnowledgeStreamResolver extends ResolverBase {
253
266
 
254
267
  switch (ev.phase) {
255
268
  case 'provider':
269
+ // Progress only. `ev.results` is never forwarded, whatever it holds: these hits have
270
+ // not been through the permission pass.
256
271
  return {
257
272
  StreamID: streamID,
258
273
  Phase: 'provider',
259
274
  ProviderName: ev.providerName,
260
275
  DurationMs: ev.durationMs,
261
- Results: ev.results.map(mapItem),
276
+ ResultCount: ev.resultCount,
277
+ Results: [],
262
278
  };
263
279
  case 'fused':
264
280
  return {
@@ -670,10 +670,8 @@ export class SqlLoggingConfigResolver extends ResolverBase {
670
670
  }
671
671
 
672
672
  try {
673
- // Check if file exists
674
- await fs.access(session.filePath);
675
-
676
- // Read file content
673
+ // Read directly: a missing file surfaces as ENOENT below, so a separate existence check
674
+ // would only open a window for the file to change between the check and the read.
677
675
  const content = await fs.readFile(session.filePath, 'utf-8');
678
676
 
679
677
  // Apply line limit if specified