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

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 (346) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/agentSessions/BridgeHostInstance.d.ts +25 -0
  3. package/dist/agentSessions/BridgeHostInstance.d.ts.map +1 -0
  4. package/dist/agentSessions/BridgeHostInstance.js +30 -0
  5. package/dist/agentSessions/BridgeHostInstance.js.map +1 -0
  6. package/dist/agentSessions/HostInstance.d.ts +75 -11
  7. package/dist/agentSessions/HostInstance.d.ts.map +1 -1
  8. package/dist/agentSessions/HostInstance.js +99 -28
  9. package/dist/agentSessions/HostInstance.js.map +1 -1
  10. package/dist/agentSessions/ReturningVisitorRecap.d.ts.map +1 -1
  11. package/dist/agentSessions/ReturningVisitorRecap.js +7 -3
  12. package/dist/agentSessions/ReturningVisitorRecap.js.map +1 -1
  13. package/dist/agentSessions/SessionJanitor.d.ts +173 -25
  14. package/dist/agentSessions/SessionJanitor.d.ts.map +1 -1
  15. package/dist/agentSessions/SessionJanitor.js +452 -54
  16. package/dist/agentSessions/SessionJanitor.js.map +1 -1
  17. package/dist/agentSessions/SessionManager.d.ts +43 -2
  18. package/dist/agentSessions/SessionManager.d.ts.map +1 -1
  19. package/dist/agentSessions/SessionManager.js +99 -8
  20. package/dist/agentSessions/SessionManager.js.map +1 -1
  21. package/dist/agentSessions/SessionRunIDVerifier.d.ts +63 -0
  22. package/dist/agentSessions/SessionRunIDVerifier.d.ts.map +1 -0
  23. package/dist/agentSessions/SessionRunIDVerifier.js +129 -0
  24. package/dist/agentSessions/SessionRunIDVerifier.js.map +1 -0
  25. package/dist/agentSessions/index.d.ts +2 -0
  26. package/dist/agentSessions/index.d.ts.map +1 -1
  27. package/dist/agentSessions/index.js +10 -3
  28. package/dist/agentSessions/index.js.map +1 -1
  29. package/dist/auth/actionAuthorization.d.ts +40 -0
  30. package/dist/auth/actionAuthorization.d.ts.map +1 -0
  31. package/dist/auth/actionAuthorization.js +94 -0
  32. package/dist/auth/actionAuthorization.js.map +1 -0
  33. package/dist/auth/index.d.ts.map +1 -1
  34. package/dist/auth/index.js +3 -10
  35. package/dist/auth/index.js.map +1 -1
  36. package/dist/auth/magicLink/MagicLinkService.d.ts.map +1 -1
  37. package/dist/auth/magicLink/MagicLinkService.js +6 -3
  38. package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
  39. package/dist/auth/newUsers.d.ts +10 -0
  40. package/dist/auth/newUsers.d.ts.map +1 -1
  41. package/dist/auth/newUsers.js +80 -11
  42. package/dist/auth/newUsers.js.map +1 -1
  43. package/dist/config.d.ts +2378 -0
  44. package/dist/config.d.ts.map +1 -1
  45. package/dist/config.js +123 -0
  46. package/dist/config.js.map +1 -1
  47. package/dist/context.d.ts +8 -0
  48. package/dist/context.d.ts.map +1 -1
  49. package/dist/context.js +14 -11
  50. package/dist/context.js.map +1 -1
  51. package/dist/generated/graphql-schemas/__mj.d.ts +135 -0
  52. package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
  53. package/dist/generated/graphql-schemas/__mj.js +706 -0
  54. package/dist/generated/graphql-schemas/__mj.js.map +1 -1
  55. package/dist/generic/OrphanedConversationDetailReconciler.js +3 -3
  56. package/dist/generic/OrphanedConversationDetailReconciler.js.map +1 -1
  57. package/dist/generic/ResolverBase.d.ts +20 -4
  58. package/dist/generic/ResolverBase.d.ts.map +1 -1
  59. package/dist/generic/ResolverBase.js +39 -10
  60. package/dist/generic/ResolverBase.js.map +1 -1
  61. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  62. package/dist/generic/RunViewResolver.js +4 -0
  63. package/dist/generic/RunViewResolver.js.map +1 -1
  64. package/dist/index.d.ts +6 -0
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +120 -131
  67. package/dist/index.js.map +1 -1
  68. package/dist/isolatedProvider.d.ts +20 -0
  69. package/dist/isolatedProvider.d.ts.map +1 -0
  70. package/dist/isolatedProvider.js +22 -0
  71. package/dist/isolatedProvider.js.map +1 -0
  72. package/dist/realtimeProxy/RealtimeProxyServer.d.ts +44 -4
  73. package/dist/realtimeProxy/RealtimeProxyServer.d.ts.map +1 -1
  74. package/dist/realtimeProxy/RealtimeProxyServer.js +121 -16
  75. package/dist/realtimeProxy/RealtimeProxyServer.js.map +1 -1
  76. package/dist/realtimeProxy/RealtimeRelayTunnel.d.ts +171 -0
  77. package/dist/realtimeProxy/RealtimeRelayTunnel.d.ts.map +1 -0
  78. package/dist/realtimeProxy/RealtimeRelayTunnel.js +532 -0
  79. package/dist/realtimeProxy/RealtimeRelayTunnel.js.map +1 -0
  80. package/dist/realtimeSessions/RealtimeSessionEventService.d.ts +148 -0
  81. package/dist/realtimeSessions/RealtimeSessionEventService.d.ts.map +1 -0
  82. package/dist/realtimeSessions/RealtimeSessionEventService.js +191 -0
  83. package/dist/realtimeSessions/RealtimeSessionEventService.js.map +1 -0
  84. package/dist/realtimeSessions/RealtimeSessionVerificationService.d.ts +66 -0
  85. package/dist/realtimeSessions/RealtimeSessionVerificationService.d.ts.map +1 -0
  86. package/dist/realtimeSessions/RealtimeSessionVerificationService.js +138 -0
  87. package/dist/realtimeSessions/RealtimeSessionVerificationService.js.map +1 -0
  88. package/dist/realtimeSessions/RealtimeVerifyRouter.d.ts +38 -0
  89. package/dist/realtimeSessions/RealtimeVerifyRouter.d.ts.map +1 -0
  90. package/dist/realtimeSessions/RealtimeVerifyRouter.js +98 -0
  91. package/dist/realtimeSessions/RealtimeVerifyRouter.js.map +1 -0
  92. package/dist/realtimeSessions/SessionVerificationPortsBase.d.ts +61 -0
  93. package/dist/realtimeSessions/SessionVerificationPortsBase.d.ts.map +1 -0
  94. package/dist/realtimeSessions/SessionVerificationPortsBase.js +149 -0
  95. package/dist/realtimeSessions/SessionVerificationPortsBase.js.map +1 -0
  96. package/dist/realtimeSessions/SlidingWindowRateLimiter.d.ts +57 -0
  97. package/dist/realtimeSessions/SlidingWindowRateLimiter.d.ts.map +1 -0
  98. package/dist/realtimeSessions/SlidingWindowRateLimiter.js +69 -0
  99. package/dist/realtimeSessions/SlidingWindowRateLimiter.js.map +1 -0
  100. package/dist/realtimeSessions/index.d.ts +17 -0
  101. package/dist/realtimeSessions/index.d.ts.map +1 -0
  102. package/dist/realtimeSessions/index.js +17 -0
  103. package/dist/realtimeSessions/index.js.map +1 -0
  104. package/dist/realtimeSessions/sessionAccess.d.ts +86 -0
  105. package/dist/realtimeSessions/sessionAccess.d.ts.map +1 -0
  106. package/dist/realtimeSessions/sessionAccess.js +90 -0
  107. package/dist/realtimeSessions/sessionAccess.js.map +1 -0
  108. package/dist/realtimeSessions/sessionConfigStamps.d.ts +70 -0
  109. package/dist/realtimeSessions/sessionConfigStamps.d.ts.map +1 -0
  110. package/dist/realtimeSessions/sessionConfigStamps.js +93 -0
  111. package/dist/realtimeSessions/sessionConfigStamps.js.map +1 -0
  112. package/dist/realtimeSessions/subscriptionAuthorization.d.ts +65 -0
  113. package/dist/realtimeSessions/subscriptionAuthorization.d.ts.map +1 -0
  114. package/dist/realtimeSessions/subscriptionAuthorization.js +71 -0
  115. package/dist/realtimeSessions/subscriptionAuthorization.js.map +1 -0
  116. package/dist/realtimeSessions/verificationCore.d.ts +243 -0
  117. package/dist/realtimeSessions/verificationCore.d.ts.map +1 -0
  118. package/dist/realtimeSessions/verificationCore.js +356 -0
  119. package/dist/realtimeSessions/verificationCore.js.map +1 -0
  120. package/dist/realtimeSessions/verificationPages.d.ts +66 -0
  121. package/dist/realtimeSessions/verificationPages.d.ts.map +1 -0
  122. package/dist/realtimeSessions/verificationPages.js +110 -0
  123. package/dist/realtimeSessions/verificationPages.js.map +1 -0
  124. package/dist/realtimeSessions/verificationPolicy.d.ts +147 -0
  125. package/dist/realtimeSessions/verificationPolicy.d.ts.map +1 -0
  126. package/dist/realtimeSessions/verificationPolicy.js +249 -0
  127. package/dist/realtimeSessions/verificationPolicy.js.map +1 -0
  128. package/dist/realtimeSessions/verificationWorkflow.d.ts +263 -0
  129. package/dist/realtimeSessions/verificationWorkflow.d.ts.map +1 -0
  130. package/dist/realtimeSessions/verificationWorkflow.js +435 -0
  131. package/dist/realtimeSessions/verificationWorkflow.js.map +1 -0
  132. package/dist/realtimeWidget/widgetGuestElevation.d.ts +20 -2
  133. package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -1
  134. package/dist/realtimeWidget/widgetGuestElevation.js +31 -2
  135. package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
  136. package/dist/resolvers/ActionResolver.d.ts +31 -5
  137. package/dist/resolvers/ActionResolver.d.ts.map +1 -1
  138. package/dist/resolvers/ActionResolver.js +75 -14
  139. package/dist/resolvers/ActionResolver.js.map +1 -1
  140. package/dist/resolvers/AdhocQueryResolver.d.ts +14 -1
  141. package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
  142. package/dist/resolvers/AdhocQueryResolver.js +21 -5
  143. package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
  144. package/dist/resolvers/CacheStatsResolver.d.ts +27 -0
  145. package/dist/resolvers/CacheStatsResolver.d.ts.map +1 -1
  146. package/dist/resolvers/CacheStatsResolver.js +113 -2
  147. package/dist/resolvers/CacheStatsResolver.js.map +1 -1
  148. package/dist/resolvers/ClientToolRequestResolver.d.ts +4 -2
  149. package/dist/resolvers/ClientToolRequestResolver.d.ts.map +1 -1
  150. package/dist/resolvers/ClientToolRequestResolver.js +28 -10
  151. package/dist/resolvers/ClientToolRequestResolver.js.map +1 -1
  152. package/dist/resolvers/FetchEntityVectorsResolver.d.ts +12 -1
  153. package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
  154. package/dist/resolvers/FetchEntityVectorsResolver.js +83 -2
  155. package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
  156. package/dist/resolvers/HumanHandoffResolver.d.ts.map +1 -1
  157. package/dist/resolvers/HumanHandoffResolver.js +7 -1
  158. package/dist/resolvers/HumanHandoffResolver.js.map +1 -1
  159. package/dist/resolvers/MCPResolver.d.ts.map +1 -1
  160. package/dist/resolvers/MCPResolver.js +9 -4
  161. package/dist/resolvers/MCPResolver.js.map +1 -1
  162. package/dist/resolvers/RealtimeBridgeResolver.d.ts +54 -2
  163. package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
  164. package/dist/resolvers/RealtimeBridgeResolver.js +155 -10
  165. package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
  166. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +155 -25
  167. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  168. package/dist/resolvers/RealtimeClientSessionResolver.js +394 -90
  169. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  170. package/dist/resolvers/RealtimeSessionEventsResolver.d.ts +61 -0
  171. package/dist/resolvers/RealtimeSessionEventsResolver.d.ts.map +1 -0
  172. package/dist/resolvers/RealtimeSessionEventsResolver.js +130 -0
  173. package/dist/resolvers/RealtimeSessionEventsResolver.js.map +1 -0
  174. package/dist/resolvers/RealtimeSessionVerificationResolver.d.ts +85 -0
  175. package/dist/resolvers/RealtimeSessionVerificationResolver.d.ts.map +1 -0
  176. package/dist/resolvers/RealtimeSessionVerificationResolver.js +188 -0
  177. package/dist/resolvers/RealtimeSessionVerificationResolver.js.map +1 -0
  178. package/dist/resolvers/RemoteBrowserActionResolver.d.ts +2 -0
  179. package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
  180. package/dist/resolvers/RemoteBrowserActionResolver.js +5 -1
  181. package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
  182. package/dist/resolvers/RunAIAgentResolver.d.ts +10 -0
  183. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  184. package/dist/resolvers/RunAIAgentResolver.js +68 -11
  185. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  186. package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
  187. package/dist/resolvers/RunTemplateResolver.js +12 -11
  188. package/dist/resolvers/RunTemplateResolver.js.map +1 -1
  189. package/dist/resolvers/SyncDataResolver.d.ts.map +1 -1
  190. package/dist/resolvers/SyncDataResolver.js +6 -2
  191. package/dist/resolvers/SyncDataResolver.js.map +1 -1
  192. package/dist/resolvers/SyncRolesUsersResolver.d.ts +2 -0
  193. package/dist/resolvers/SyncRolesUsersResolver.d.ts.map +1 -1
  194. package/dist/resolvers/SyncRolesUsersResolver.js +23 -6
  195. package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
  196. package/dist/resolvers/UserAvatarResolver.d.ts +16 -0
  197. package/dist/resolvers/UserAvatarResolver.d.ts.map +1 -0
  198. package/dist/resolvers/UserAvatarResolver.js +124 -0
  199. package/dist/resolvers/UserAvatarResolver.js.map +1 -0
  200. package/dist/resolvers/UserResolver.d.ts.map +1 -1
  201. package/dist/resolvers/UserResolver.js +2 -17
  202. package/dist/resolvers/UserResolver.js.map +1 -1
  203. package/dist/resolvers/agentVisionConsentAudit.d.ts +37 -0
  204. package/dist/resolvers/agentVisionConsentAudit.d.ts.map +1 -0
  205. package/dist/resolvers/agentVisionConsentAudit.js +63 -0
  206. package/dist/resolvers/agentVisionConsentAudit.js.map +1 -0
  207. package/dist/resolvers/avatarInputValidation.d.ts +40 -0
  208. package/dist/resolvers/avatarInputValidation.d.ts.map +1 -0
  209. package/dist/resolvers/avatarInputValidation.js +97 -0
  210. package/dist/resolvers/avatarInputValidation.js.map +1 -0
  211. package/dist/resolvers/currentUserRoles.d.ts +29 -0
  212. package/dist/resolvers/currentUserRoles.d.ts.map +1 -1
  213. package/dist/resolvers/currentUserRoles.js +30 -0
  214. package/dist/resolvers/currentUserRoles.js.map +1 -1
  215. package/dist/resolvers/meetings/meetingMutations.d.ts +6 -0
  216. package/dist/resolvers/meetings/meetingMutations.d.ts.map +1 -1
  217. package/dist/resolvers/meetings/meetingMutations.js +21 -0
  218. package/dist/resolvers/meetings/meetingMutations.js.map +1 -1
  219. package/dist/services/TaskGraphContinuationDeliverer.d.ts +4 -3
  220. package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
  221. package/dist/services/TaskGraphContinuationDeliverer.js +13 -7
  222. package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
  223. package/dist/sharedCache.d.ts +105 -0
  224. package/dist/sharedCache.d.ts.map +1 -0
  225. package/dist/sharedCache.js +276 -0
  226. package/dist/sharedCache.js.map +1 -0
  227. package/dist/types.d.ts +6 -0
  228. package/dist/types.d.ts.map +1 -1
  229. package/dist/types.js.map +1 -1
  230. package/package.json +97 -95
  231. package/src/__tests__/ActionResolver.authorization.test.ts +353 -0
  232. package/src/__tests__/AdhocQueryResolver.sqlAuthorizer.test.ts +104 -0
  233. package/src/__tests__/BridgeHostInstance.test.ts +232 -0
  234. package/src/__tests__/ClientToolRequestResolver.test.ts +116 -0
  235. package/src/__tests__/FetchEntityVectorsResolver.test.ts +285 -0
  236. package/src/__tests__/HostInstance.test.ts +84 -0
  237. package/src/__tests__/HumanHandoffResolver.test.ts +17 -0
  238. package/src/__tests__/MeetingResolver.test.ts +75 -1
  239. package/src/__tests__/RealtimeBridgeResolver.test.ts +415 -6
  240. package/src/__tests__/RealtimeClientSessionResolver.configTamper.test.ts +383 -0
  241. package/src/__tests__/RealtimeClientSessionResolver.test.ts +682 -58
  242. package/src/__tests__/RealtimeProxyServer.test.ts +16 -1
  243. package/src/__tests__/RealtimeRelay.test.ts +876 -0
  244. package/src/__tests__/RemoteBrowserActionResolver.goalRunLink.test.ts +129 -0
  245. package/src/__tests__/ResolverBase.aggregateScreen.test.ts +174 -0
  246. package/src/__tests__/ResolverBase.clientOldValues.test.ts +6 -3
  247. package/src/__tests__/ReturningVisitorRecap.test.ts +115 -0
  248. package/src/__tests__/RunAIAgentResolver.branch.test.ts +169 -0
  249. package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +4 -4
  250. package/src/__tests__/RunAIAgentResolver.stoppedPredecessor.test.ts +166 -0
  251. package/src/__tests__/RunTemplateResolver.test.ts +132 -0
  252. package/src/__tests__/RunViewResolver.cacheCheckForwarding.test.ts +63 -0
  253. package/src/__tests__/SessionJanitor.test.ts +845 -18
  254. package/src/__tests__/SessionManager.test.ts +213 -17
  255. package/src/__tests__/SessionRunIDVerifier.test.ts +251 -0
  256. package/src/__tests__/SyncRolesUsersResolver.userRoles.test.ts +97 -0
  257. package/src/__tests__/TaskGraphContinuationDeliverer.test.ts +50 -3
  258. package/src/__tests__/UserAvatarResolver.test.ts +255 -0
  259. package/src/__tests__/UserResolver.currentUser.test.ts +16 -1
  260. package/src/__tests__/actionAuthorization.test.ts +130 -0
  261. package/src/__tests__/agentVisionConsentAudit.test.ts +118 -0
  262. package/src/__tests__/avatarInputValidation.test.ts +160 -0
  263. package/src/__tests__/isolatedProviderTransactions.test.ts +169 -0
  264. package/src/__tests__/magicLinkService.consume.test.ts +6 -0
  265. package/src/__tests__/magicLinkService.provisionLog.test.ts +5 -0
  266. package/src/__tests__/newUsers.template.test.ts +151 -0
  267. package/src/__tests__/newUsers.test.ts +74 -5
  268. package/src/__tests__/orphanedConversationDetailReconciler.test.ts +17 -2
  269. package/src/__tests__/realtimeSessions/RealtimeSessionEventService.test.ts +201 -0
  270. package/src/__tests__/realtimeSessions/RealtimeSessionEventsResolver.test.ts +215 -0
  271. package/src/__tests__/realtimeSessions/RealtimeSessionVerificationResolver.test.ts +102 -0
  272. package/src/__tests__/realtimeSessions/RealtimeSessionVerificationService.test.ts +349 -0
  273. package/src/__tests__/realtimeSessions/RealtimeVerifyRouter.test.ts +131 -0
  274. package/src/__tests__/realtimeSessions/SlidingWindowRateLimiter.test.ts +56 -0
  275. package/src/__tests__/realtimeSessions/helpers.ts +4 -0
  276. package/src/__tests__/realtimeSessions/sessionAccess.test.ts +129 -0
  277. package/src/__tests__/realtimeSessions/sessionConfigStamps.test.ts +124 -0
  278. package/src/__tests__/realtimeSessions/verificationCore.test.ts +355 -0
  279. package/src/__tests__/realtimeSessions/verificationPages.test.ts +84 -0
  280. package/src/__tests__/realtimeSessions/verificationPolicy.test.ts +226 -0
  281. package/src/__tests__/realtimeSessions/verificationWorkflow.test.ts +633 -0
  282. package/src/__tests__/resolverBase.fls.test.ts +87 -4
  283. package/src/__tests__/serverExtensionReservedRoots.test.ts +2 -0
  284. package/src/__tests__/sharedCache.test.ts +310 -0
  285. package/src/__tests__/widgetGuestElevation.test.ts +44 -0
  286. package/src/agentSessions/BridgeHostInstance.ts +32 -0
  287. package/src/agentSessions/HostInstance.ts +124 -28
  288. package/src/agentSessions/ReturningVisitorRecap.ts +7 -3
  289. package/src/agentSessions/SessionJanitor.ts +543 -57
  290. package/src/agentSessions/SessionManager.ts +125 -25
  291. package/src/agentSessions/SessionRunIDVerifier.ts +171 -0
  292. package/src/agentSessions/index.ts +10 -3
  293. package/src/auth/actionAuthorization.ts +116 -0
  294. package/src/auth/index.ts +3 -8
  295. package/src/auth/magicLink/MagicLinkService.ts +6 -3
  296. package/src/auth/newUsers.ts +99 -11
  297. package/src/config.ts +125 -0
  298. package/src/context.ts +15 -10
  299. package/src/generated/graphql-schemas/__mj.ts +496 -1
  300. package/src/generic/OrphanedConversationDetailReconciler.ts +3 -3
  301. package/src/generic/ResolverBase.ts +39 -9
  302. package/src/generic/RunViewResolver.ts +4 -0
  303. package/src/index.ts +132 -143
  304. package/src/isolatedProvider.ts +22 -0
  305. package/src/realtimeProxy/RealtimeProxyServer.ts +148 -16
  306. package/src/realtimeProxy/RealtimeRelayTunnel.ts +645 -0
  307. package/src/realtimeSessions/README.md +179 -0
  308. package/src/realtimeSessions/RealtimeSessionEventService.ts +274 -0
  309. package/src/realtimeSessions/RealtimeSessionVerificationService.ts +167 -0
  310. package/src/realtimeSessions/RealtimeVerifyRouter.ts +108 -0
  311. package/src/realtimeSessions/SessionVerificationPortsBase.ts +189 -0
  312. package/src/realtimeSessions/SlidingWindowRateLimiter.ts +92 -0
  313. package/src/realtimeSessions/index.ts +16 -0
  314. package/src/realtimeSessions/sessionAccess.ts +141 -0
  315. package/src/realtimeSessions/sessionConfigStamps.ts +123 -0
  316. package/src/realtimeSessions/subscriptionAuthorization.ts +116 -0
  317. package/src/realtimeSessions/verificationCore.ts +502 -0
  318. package/src/realtimeSessions/verificationPages.ts +149 -0
  319. package/src/realtimeSessions/verificationPolicy.ts +321 -0
  320. package/src/realtimeSessions/verificationWorkflow.ts +702 -0
  321. package/src/realtimeWidget/widgetGuestElevation.ts +34 -2
  322. package/src/resolvers/ActionResolver.ts +84 -21
  323. package/src/resolvers/AdhocQueryResolver.ts +24 -6
  324. package/src/resolvers/CacheStatsResolver.ts +89 -2
  325. package/src/resolvers/ClientToolRequestResolver.ts +25 -7
  326. package/src/resolvers/FetchEntityVectorsResolver.ts +97 -2
  327. package/src/resolvers/HumanHandoffResolver.ts +8 -0
  328. package/src/resolvers/MCPResolver.ts +8 -5
  329. package/src/resolvers/RealtimeBridgeResolver.ts +163 -11
  330. package/src/resolvers/RealtimeClientSessionResolver.ts +501 -96
  331. package/src/resolvers/RealtimeSessionEventsResolver.ts +133 -0
  332. package/src/resolvers/RealtimeSessionVerificationResolver.ts +174 -0
  333. package/src/resolvers/RemoteBrowserActionResolver.ts +11 -1
  334. package/src/resolvers/RunAIAgentResolver.ts +78 -11
  335. package/src/resolvers/RunTemplateResolver.ts +13 -12
  336. package/src/resolvers/SyncDataResolver.ts +6 -2
  337. package/src/resolvers/SyncRolesUsersResolver.ts +24 -6
  338. package/src/resolvers/UserAvatarResolver.ts +111 -0
  339. package/src/resolvers/UserResolver.ts +2 -16
  340. package/src/resolvers/agentVisionConsentAudit.ts +82 -0
  341. package/src/resolvers/avatarInputValidation.ts +123 -0
  342. package/src/resolvers/currentUserRoles.ts +32 -1
  343. package/src/resolvers/meetings/meetingMutations.ts +21 -0
  344. package/src/services/TaskGraphContinuationDeliverer.ts +13 -7
  345. package/src/sharedCache.ts +302 -0
  346. package/src/types.ts +7 -0
@@ -0,0 +1,133 @@
1
+ /**
2
+ * @fileoverview GraphQL subscription that delivers **realtime session events** to the session they are
3
+ * addressed to — `subscription RealtimeSessionEvents(agentSessionId: String!)`.
4
+ *
5
+ * The event contract lives in `@memberjunction/ai-core-plus` (`realtime-session-events.ts`); the publisher
6
+ * apps call is `RealtimeSessionEventService`. This resolver is the delivery edge, and its whole job is
7
+ * authorization, done twice on purpose:
8
+ *
9
+ * 1. **At subscribe time** ({@link AuthorizeRealtimeSessionSubscription}). The session row is loaded and
10
+ * {@link AuthorizeSessionAccess} must allow the connection's authenticated principal: the session's
11
+ * `UserID` must be the caller's user and — for an anonymous widget guest, who shares one Anonymous
12
+ * user with every other guest — the guest's signed per-session scope must match the session's
13
+ * conversation. A refusal is an error on THIS subscription only (the socket and its other
14
+ * subscriptions are untouched) and is the same "not found" whether the session is missing or simply
15
+ * not yours, so ids cannot be probed.
16
+ * 2. **For every event** ({@link RealtimeSessionEventsFilter}). Each published event carries the owner and
17
+ * scope snapshot of its session; the filter re-applies the same decision against the CONNECTION's
18
+ * identity. Authorization therefore does not rest on a single check at one moment, and a replicated
19
+ * event from another server instance is judged exactly like a local one.
20
+ *
21
+ * In neither check does a client-supplied value decide who receives anything: `agentSessionId` only
22
+ * selects WHICH of the caller's own sessions to listen to; the identity it is checked against comes from
23
+ * the server-authenticated connection context.
24
+ *
25
+ * Events have no replay. A subscriber that was disconnected when an event fired recovers durable state
26
+ * from `RealtimeSessionVerificationStatus` (and from the session itself).
27
+ *
28
+ * @module @memberjunction/server
29
+ */
30
+
31
+ import { Resolver, Subscription, Root, Arg, ObjectType, Field } from 'type-graphql';
32
+ import { withFilter } from 'graphql-subscriptions';
33
+ import { PubSubManager } from '../generic/PubSubManager.js';
34
+ import {
35
+ AuthorizeRealtimeSessionSubscription,
36
+ RealtimeSessionEventsFilter,
37
+ type RealtimeSessionEventsArgs,
38
+ type RealtimeSessionEventsContext,
39
+ type SubscriptionAuthorizationDeps,
40
+ DEFAULT_SUBSCRIPTION_AUTHORIZATION_DEPS,
41
+ } from '../realtimeSessions/subscriptionAuthorization.js';
42
+ import { REALTIME_SESSION_EVENTS_TOPIC, type RealtimeSessionEventTopicPayload } from '../realtimeSessions/RealtimeSessionEventService.js';
43
+
44
+ /** One session event as the client receives it. The routing snapshot is deliberately NOT exposed. */
45
+ @ObjectType()
46
+ export class RealtimeSessionEventNotification {
47
+ /** Event type, e.g. `identity.verified`. Clients ignore types they do not know. */
48
+ @Field(() => String)
49
+ Type: string;
50
+
51
+ /** The session the event is addressed to. */
52
+ @Field(() => String)
53
+ AgentSessionID: string;
54
+
55
+ /** ISO-8601 instant the server emitted the event. */
56
+ @Field(() => String)
57
+ OccurredAt: string;
58
+
59
+ /** The event payload, JSON-encoded. Parse with `ParseRealtimeSessionEvent` from `@memberjunction/ai-core-plus`. */
60
+ @Field(() => String)
61
+ PayloadJson: string;
62
+ }
63
+
64
+ /**
65
+ * Builds the event stream for one authorized subscription: the shared topic, narrowed by
66
+ * {@link RealtimeSessionEventsFilter}.
67
+ */
68
+ function openEventStream(
69
+ root: unknown,
70
+ args: RealtimeSessionEventsArgs,
71
+ context: RealtimeSessionEventsContext,
72
+ info: unknown,
73
+ ): AsyncIterator<RealtimeSessionEventTopicPayload> {
74
+ const engine = PubSubManager.Instance.PubSubEngine;
75
+ if (!engine) {
76
+ throw new Error('Realtime session events are unavailable: PubSub is not configured.');
77
+ }
78
+ const filtered = withFilter(
79
+ () => engine.asyncIterator<RealtimeSessionEventTopicPayload>(REALTIME_SESSION_EVENTS_TOPIC),
80
+ (payload: RealtimeSessionEventTopicPayload, filterArgs: RealtimeSessionEventsArgs, filterContext: RealtimeSessionEventsContext) =>
81
+ RealtimeSessionEventsFilter({ payload, args: filterArgs, context: filterContext }),
82
+ );
83
+ return filtered(root, args, context, info);
84
+ }
85
+
86
+ /**
87
+ * The `subscribe` function: authorize FIRST (asynchronously), then open the stream. Exported (with an
88
+ * injectable `deps` argument graphql-js never passes) so the whole path is testable end to end.
89
+ *
90
+ * graphql-js awaits the value `subscribe` returns and turns a thrown error into a per-operation error
91
+ * result, which is exactly what a refusal should be (a failure of this subscription, not the socket).
92
+ */
93
+ export async function AuthorizeAndSubscribeToRealtimeSessionEvents(
94
+ root: unknown,
95
+ args: RealtimeSessionEventsArgs,
96
+ context: RealtimeSessionEventsContext,
97
+ info: unknown,
98
+ deps: SubscriptionAuthorizationDeps = DEFAULT_SUBSCRIPTION_AUTHORIZATION_DEPS,
99
+ ): Promise<AsyncIterator<RealtimeSessionEventTopicPayload>> {
100
+ await AuthorizeRealtimeSessionSubscription(args, context, deps);
101
+ return openEventStream(root, args, context, info);
102
+ }
103
+
104
+ @Resolver()
105
+ export class RealtimeSessionEventsResolver {
106
+ /**
107
+ * Subscribe to the events addressed to one of YOUR realtime sessions.
108
+ *
109
+ * @param agentSessionId - the `MJ: AI Agent Sessions` id to listen to. Must be a session the
110
+ * authenticated connection owns (and, for an anonymous widget guest, whose signed scope matches);
111
+ * otherwise the subscription is refused.
112
+ */
113
+ @Subscription(() => RealtimeSessionEventNotification, {
114
+ // graphql-subscriptions types `ResolverFn` as returning a bare AsyncIterator, but graphql-js
115
+ // AWAITS what `subscribe` returns (execute/subscribe.js: `await resolveFn(...)`), and awaiting is
116
+ // the only way to authorize against the database before opening the stream. The typing gap is in
117
+ // the library; `@ts-expect-error` (not a cast) so this fails to compile the day the type is fixed.
118
+ // @ts-expect-error subscribe may return Promise<AsyncIterator>; see comment above
119
+ subscribe: AuthorizeAndSubscribeToRealtimeSessionEvents,
120
+ })
121
+ RealtimeSessionEvents(
122
+ @Root() payload: RealtimeSessionEventTopicPayload,
123
+ @Arg('agentSessionId', () => String) _agentSessionId: string,
124
+ ): RealtimeSessionEventNotification {
125
+ // OwnerUserID / ScopeKey are server-internal filter keys and are intentionally not returned.
126
+ return {
127
+ Type: payload.Type,
128
+ AgentSessionID: payload.AgentSessionID,
129
+ OccurredAt: payload.OccurredAt,
130
+ PayloadJson: payload.PayloadJson,
131
+ };
132
+ }
133
+ }
@@ -0,0 +1,174 @@
1
+ /**
2
+ * @fileoverview GraphQL operations for **mid-session identity verification** of a realtime session.
3
+ *
4
+ * A person talking to an agent (often an anonymous widget guest) proves they control an email address
5
+ * without leaving the conversation. Three operations, all callable only as the session's OWN principal:
6
+ *
7
+ * | Operation | Kind | Purpose |
8
+ * |---|---|---|
9
+ * | `RequestRealtimeSessionVerification(agentSessionId, name, email)` | mutation | store a pending verification and email a one-time link + a typed code |
10
+ * | `SubmitRealtimeSessionVerificationCode(agentSessionId, code)` | mutation | redeem the typed code (the fallback for opening the email on another device) |
11
+ * | `RealtimeSessionVerificationStatus(agentSessionId)` | query | read the current state — the durable backstop for a missed `identity.verified` event |
12
+ *
13
+ * The emailed link is redeemed on a different surface — the public `GET/POST /realtime/verify` routes
14
+ * (`RealtimeVerifyRouter`) — because the person opening it holds no MJ token.
15
+ *
16
+ * The decisions, policy and limits live in `realtimeSessions/verificationWorkflow.ts`; this resolver
17
+ * only identifies the caller from the server-authenticated payload and maps the result. Note that it
18
+ * reads `userPayload.userRecord` directly and does **not** use `ResolverBase.GetUserFromPayload`: that
19
+ * helper falls back to an email lookup in the user cache, which would drop an anonymous guest's
20
+ * per-session scope — and a scope-less anonymous principal must never be treated as a named owner of
21
+ * the shared Anonymous user's sessions. No authenticated `userRecord` means no access.
22
+ *
23
+ * @module @memberjunction/server
24
+ */
25
+
26
+ import { Resolver, Mutation, Query, Arg, Ctx, ObjectType, Field, Int } from 'type-graphql';
27
+ import type { UserInfo } from '@memberjunction/core';
28
+ import type { AppContext } from '../types.js';
29
+ import { GetReadWriteProvider } from '../util.js';
30
+ import { NoLog } from '../logging/NoLog.js';
31
+ import { RealtimeSessionVerificationService } from '../realtimeSessions/RealtimeSessionVerificationService.js';
32
+ import type { PrincipalCaller, VerificationOperationResult } from '../realtimeSessions/verificationWorkflow.js';
33
+
34
+ /** Result of every verification operation. */
35
+ @ObjectType()
36
+ export class RealtimeSessionVerificationResult {
37
+ /** True when the operation did what was asked. */
38
+ @Field(() => Boolean)
39
+ Success: boolean;
40
+
41
+ /** The session's verification state after the operation: `unverified`, `pending` or `verified`. */
42
+ @Field(() => String)
43
+ VerificationState: string;
44
+
45
+ /**
46
+ * Machine-readable failure reason when `Success` is false — e.g. `invalid_email`, `consumer_domain`,
47
+ * `domain_blocked`, `rate_limited`, `send_limit_reached`, `invalid_code`, `attempts_exhausted`,
48
+ * `code_expired`, `session_not_found`, `session_closed`, `verification_unavailable`.
49
+ */
50
+ @Field(() => String, { nullable: true })
51
+ ErrorCode?: string;
52
+
53
+ /** A message safe to show a person. */
54
+ @Field(() => String, { nullable: true })
55
+ Message?: string;
56
+
57
+ /** ISO instant the pending link/code expires. */
58
+ @Field(() => String, { nullable: true })
59
+ ExpiresAt?: string;
60
+
61
+ /** Verification emails this session may still trigger. */
62
+ @Field(() => Int, { nullable: true })
63
+ SendsRemaining?: number;
64
+
65
+ /** Wrong code entries left before the pending verification is voided. */
66
+ @Field(() => Int, { nullable: true })
67
+ AttemptsRemaining?: number;
68
+
69
+ /** Seconds to wait before retrying, when rate limited. */
70
+ @Field(() => Int, { nullable: true })
71
+ RetryAfterSeconds?: number;
72
+
73
+ /** The verified address, once verified. */
74
+ @Field(() => String, { nullable: true })
75
+ VerifiedEmail?: string;
76
+
77
+ /** ISO instant verification completed. */
78
+ @Field(() => String, { nullable: true })
79
+ VerifiedAt?: string;
80
+
81
+ /** The session's new absolute deadline (ISO) when verification extended it. The server enforces it; this is informational. */
82
+ @Field(() => String, { nullable: true })
83
+ MaxSessionDeadlineIso?: string;
84
+ }
85
+
86
+ /** Maps the workflow result onto the GraphQL type. */
87
+ function toResult(result: VerificationOperationResult): RealtimeSessionVerificationResult {
88
+ return { ...result };
89
+ }
90
+
91
+ /** The result for a caller with no usable authenticated identity. */
92
+ function notAuthenticated(): RealtimeSessionVerificationResult {
93
+ return { Success: false, VerificationState: 'unverified', ErrorCode: 'session_not_found', Message: 'Session not found.' };
94
+ }
95
+
96
+ @Resolver()
97
+ export class RealtimeSessionVerificationResolver {
98
+ /**
99
+ * Ask to verify an email for your realtime session. Applies the session's policy (business-domain
100
+ * rules, blocked domains, send and rate limits), then emails a one-time link plus a short code.
101
+ *
102
+ * @param agentSessionId - your `MJ: AI Agent Sessions` id
103
+ * @param name - the name to greet in the email (sanitised and length-capped server-side)
104
+ * @param email - the address to verify
105
+ */
106
+ @Mutation(() => RealtimeSessionVerificationResult)
107
+ async RequestRealtimeSessionVerification(
108
+ @Arg('agentSessionId', () => String) agentSessionId: string,
109
+ @Arg('name', () => String) name: string,
110
+ @Arg('email', () => String) @NoLog email: string,
111
+ @Ctx() context: AppContext,
112
+ ): Promise<RealtimeSessionVerificationResult> {
113
+ const caller = this.callerFrom(context);
114
+ if (!caller) {
115
+ return notAuthenticated();
116
+ }
117
+ return toResult(
118
+ await RealtimeSessionVerificationService.Instance.RequestVerification(
119
+ { AgentSessionID: agentSessionId, Name: name, Email: email },
120
+ caller,
121
+ ),
122
+ );
123
+ }
124
+
125
+ /**
126
+ * Redeem the typed code from the verification email. Single use; voided after too many wrong entries.
127
+ *
128
+ * @param agentSessionId - your `MJ: AI Agent Sessions` id
129
+ * @param code - the code from the email (spaces and dashes are ignored)
130
+ */
131
+ @Mutation(() => RealtimeSessionVerificationResult)
132
+ async SubmitRealtimeSessionVerificationCode(
133
+ @Arg('agentSessionId', () => String) agentSessionId: string,
134
+ @Arg('code', () => String) @NoLog code: string,
135
+ @Ctx() context: AppContext,
136
+ ): Promise<RealtimeSessionVerificationResult> {
137
+ const caller = this.callerFrom(context);
138
+ if (!caller) {
139
+ return notAuthenticated();
140
+ }
141
+ return toResult(await RealtimeSessionVerificationService.Instance.SubmitCode({ AgentSessionID: agentSessionId, Code: code }, caller));
142
+ }
143
+
144
+ /**
145
+ * Read the current verification state of your realtime session. Use it after a reconnect: session
146
+ * events have no replay, so this is how a client that missed `identity.verified` finds out.
147
+ *
148
+ * @param agentSessionId - your `MJ: AI Agent Sessions` id
149
+ */
150
+ @Query(() => RealtimeSessionVerificationResult)
151
+ async RealtimeSessionVerificationStatus(
152
+ @Arg('agentSessionId', () => String) agentSessionId: string,
153
+ @Ctx() context: AppContext,
154
+ ): Promise<RealtimeSessionVerificationResult> {
155
+ const caller = this.callerFrom(context);
156
+ if (!caller) {
157
+ return notAuthenticated();
158
+ }
159
+ return toResult(await RealtimeSessionVerificationService.Instance.GetStatus({ AgentSessionID: agentSessionId }, caller));
160
+ }
161
+
162
+ /**
163
+ * Builds the caller from the server-authenticated payload, or null when there is none. Identity comes
164
+ * from `userPayload.userRecord` ONLY (see the module header).
165
+ */
166
+ private callerFrom(context: AppContext): PrincipalCaller | null {
167
+ const contextUser = context.userPayload?.userRecord as UserInfo | undefined;
168
+ const provider = GetReadWriteProvider(context.providers);
169
+ if (!contextUser?.ID || !provider) {
170
+ return null;
171
+ }
172
+ return { Kind: 'principal', ContextUser: contextUser, Provider: provider, ClientIp: context.ClientIp };
173
+ }
174
+ }
@@ -24,6 +24,7 @@ import { MJAIAgentSessionEntity, MJAIAgentEntity } from '@memberjunction/core-en
24
24
  import { NormalizeInstanceKey, RemoteBrowserEngine } from '@memberjunction/remote-browser-server';
25
25
  import { BeginBrowserGoalStep, FinalizeBrowserGoalStep, ExtractCoAgentRunID } from '../agentSessions/remoteBrowserGoalEngine.js';
26
26
  import { RemoteBrowserGoalRegistry } from '../agentSessions/remoteBrowserGoalRegistry.js';
27
+ import { SessionRunIDVerifier } from '../agentSessions/SessionRunIDVerifier.js';
27
28
  import { randomUUID } from 'node:crypto';
28
29
  import {
29
30
  RemoteBrowserAction,
@@ -261,6 +262,9 @@ export class RemoteBrowserActionResolver extends ResolverBase {
261
262
  */
262
263
  private startedScreencasts = new Map<string, number>();
263
264
 
265
+ /** Checks the co-agent run id read from a session's owner-editable `Config_` before a step is attached to it. */
266
+ private readonly runIDVerifier = new SessionRunIDVerifier();
267
+
264
268
  /**
265
269
  * Surfaces whose live tab-audio stream this resolver has already started, keyed by
266
270
  * {@link RemoteBrowserActionResolver.streamKey} for the same reason the screencast map is. Entries
@@ -400,7 +404,13 @@ export class RemoteBrowserActionResolver extends ResolverBase {
400
404
  const providerName = await this.resolveProviderName(session, contextUser, provider);
401
405
  // Observability: nest this goal's many prompt runs under ONE "Browser goal" step on the realtime
402
406
  // co-agent run (when the session has one). Best-effort — a null step just means the goal runs unlinked.
403
- const coAgentRunID = ExtractCoAgentRunID(session.Config_);
407
+ // The owner can edit Config_, so its run id is used only when it is a run of this session.
408
+ const { CoAgentRunID: coAgentRunID } = await this.runIDVerifier.Verify(
409
+ session.ID,
410
+ { CoAgentRunID: ExtractCoAgentRunID(session.Config_) },
411
+ contextUser,
412
+ provider,
413
+ );
404
414
  const goalStep = await BeginBrowserGoalStep(provider, contextUser, coAgentRunID, goal);
405
415
 
406
416
  // ASYNC START: a goal loop can run for minutes; do NOT hold this request open for it (browser
@@ -293,7 +293,9 @@ export class RunAIAgentResolver extends ResolverBase {
293
293
  taskGraphDebug?: string,
294
294
  /** History floor for a conversation run — threaded into ExecuteAgentParams.ConversationHistoryFrom,
295
295
  * so the framework's own conversation reads (retrieval tools, artifacts, compaction) honour it too. */
296
- conversationHistoryFrom?: Date
296
+ conversationHistoryFrom?: Date,
297
+ /** The conversation branch of the run — threaded into ExecuteAgentParams.ConversationBranchID. */
298
+ conversationBranchId?: string | null
297
299
  ): Promise<AIAgentRunResult> {
298
300
  const startTime = Date.now();
299
301
  // Best-effort handle for persistInFlightAgentFailure. Populated from a
@@ -362,6 +364,7 @@ export class RunAIAgentResolver extends ResolverBase {
362
364
  requestedSkillIDs: requestedSkillIDs,
363
365
  taskGraphDebug: parseTaskGraphDebug(taskGraphDebug),
364
366
  ConversationHistoryFrom: conversationHistoryFrom,
367
+ ...(conversationBranchId ? { ConversationBranchID: conversationBranchId } : {}),
365
368
  data: parsedData,
366
369
  context: {
367
370
  dataSource: dataSource
@@ -752,6 +755,45 @@ export class RunAIAgentResolver extends ResolverBase {
752
755
  }
753
756
  }
754
757
 
758
+ /**
759
+ * The ID of this agent's newest root run in the conversation IF the user stopped it, else
760
+ * null. The query spans the runs that could be the predecessor (settled, or stopped by the
761
+ * user) and the newest decides, so a stopped run that a later settled run already followed
762
+ * is not chained to again. Uses the same filter and recognizer as `BaseAgent.findUserStoppedPredecessorRunId`
763
+ * (`MJAIAgentRunEntityExtended.BuildStoppedPredecessorFilter` / `IsUserStopped`), which carries the stopped run's
764
+ * completed results into the new run's context, so the two cannot pick different runs.
765
+ * Fail-soft: a lookup failure means no chaining, never a failed turn.
766
+ */
767
+ private async findUserStoppedPredecessorRunId(
768
+ conversationId: string,
769
+ agentId: string,
770
+ branchId: string | null,
771
+ contextUser: UserInfo,
772
+ provider: IMetadataProvider
773
+ ): Promise<string | null> {
774
+ try {
775
+ // The run must have replied on this turn's branch path: a run stopped on a sibling branch is not
776
+ // this path's history. The trunk needs no branch rows; a branch needs them to walk its ancestry.
777
+ const branches = branchId ? await ConversationEngine.LoadBranchesFresh(conversationId, contextUser, provider) : [];
778
+ const rv = RunView.FromMetadataProvider(provider);
779
+ const newest = await rv.RunView<{ ID: string; Status: string; CancellationReason: string | null }>({
780
+ EntityName: 'MJ: AI Agent Runs',
781
+ ExtraFilter: MJAIAgentRunEntityExtended.BuildStoppedPredecessorFilter(conversationId, agentId, branchId, branches),
782
+ OrderBy: '__mj_CreatedAt DESC',
783
+ MaxRows: 1,
784
+ Fields: ['ID', 'Status', 'CancellationReason'],
785
+ ResultType: 'simple',
786
+ // The row this looks for was written by the Stop button moments ago; read the database, not a cache.
787
+ BypassCache: true,
788
+ }, contextUser);
789
+ const run = newest.Success ? newest.Results?.[0] : undefined;
790
+ return run && MJAIAgentRunEntityExtended.IsUserStopped(run.Status, run.CancellationReason) ? run.ID : null;
791
+ } catch (error) {
792
+ LogError(`findUserStoppedPredecessorRunId failed; the turn runs unchained: ${error instanceof Error ? error.message : String(error)}`);
793
+ return null;
794
+ }
795
+ }
796
+
755
797
  /**
756
798
  * When a continuation run completes (lastRunId was provided), sync the corresponding
757
799
  * AIAgentRequest by marking it as responded. This keeps the dashboard accurate when
@@ -934,6 +976,24 @@ export class RunAIAgentResolver extends ResolverBase {
934
976
  throw new Error(`Conversation detail ${conversationDetailId} not found`);
935
977
  }
936
978
  const conversationId = currentDetail.ConversationID;
979
+ // The branch the placeholder row was created on decides which path the run reads.
980
+ const branchId: string | null = currentDetail.BranchID ?? null;
981
+
982
+ // A turn that follows a run the user STOPPED continues that run rather than starting
983
+ // over: chain it by LastRunID (the chat client never sends one) and, when the caller
984
+ // supplied no payload, roll the stopped run's FinalPayload into this run's start.
985
+ let effectiveLastRunId = lastRunId;
986
+ let effectiveAutoPopulate = autoPopulateLastRunPayload;
987
+ if (!effectiveLastRunId) {
988
+ const stoppedRunId = await this.findUserStoppedPredecessorRunId(conversationId, effectiveAgentId, branchId, currentUser, p);
989
+ if (stoppedRunId) {
990
+ effectiveLastRunId = stoppedRunId;
991
+ if (effectiveAutoPopulate === undefined && payload == null) {
992
+ effectiveAutoPopulate = true;
993
+ }
994
+ LogStatus(`↩️ Continuing from user-stopped run ${stoppedRunId} (chained by LastRunID)`);
995
+ }
996
+ }
937
997
 
938
998
  // Load conversation history with attachments from DB
939
999
  const messages = await this.loadConversationHistoryWithAttachments(
@@ -944,7 +1004,8 @@ export class RunAIAgentResolver extends ResolverBase {
944
1004
  // The UI creates the agent-response placeholder row ('⏳ ...') before invoking
945
1005
  // this mutation — exclude it so the model never sees an empty assistant turn.
946
1006
  [conversationDetailId],
947
- historyFrom
1007
+ historyFrom,
1008
+ branchId
948
1009
  );
949
1010
 
950
1011
  // Convert to JSON string for the existing executeAIAgent method
@@ -955,11 +1016,12 @@ export class RunAIAgentResolver extends ResolverBase {
955
1016
  // The client will receive the result via WebSocket PubSub completion event.
956
1017
  this.executeAgentInBackground(
957
1018
  p, dataSource, effectiveAgentId, effectiveUserPayload, messagesJson, sessionId, pubSub,
958
- data, payload, lastRunId, autoPopulateLastRunPayload, configurationId,
1019
+ data, payload, effectiveLastRunId, effectiveAutoPopulate, configurationId,
959
1020
  conversationDetailId, createArtifacts || false, createNotification || false,
960
1021
  sourceArtifactId, sourceArtifactVersionId, conversationId, planMode, requestedSkillIDs,
961
1022
  undefined, // taskGraphDebug
962
- historyFrom
1023
+ historyFrom,
1024
+ branchId
963
1025
  );
964
1026
 
965
1027
  LogStatus(`🔥 Fire-and-forget: Agent ${effectiveAgentId} execution started in background for session ${sessionId}`);
@@ -982,8 +1044,8 @@ export class RunAIAgentResolver extends ResolverBase {
982
1044
  data,
983
1045
  payload,
984
1046
  undefined, // templateData
985
- lastRunId,
986
- autoPopulateLastRunPayload,
1047
+ effectiveLastRunId,
1048
+ effectiveAutoPopulate,
987
1049
  configurationId,
988
1050
  conversationDetailId,
989
1051
  createArtifacts || false,
@@ -995,7 +1057,8 @@ export class RunAIAgentResolver extends ResolverBase {
995
1057
  planMode,
996
1058
  requestedSkillIDs,
997
1059
  undefined, // taskGraphDebug
998
- historyFrom
1060
+ historyFrom,
1061
+ branchId
999
1062
  );
1000
1063
  } catch (error) {
1001
1064
  const errorMessage = (error as Error).message || 'Unknown error loading conversation history';
@@ -1217,7 +1280,9 @@ export class RunAIAgentResolver extends ResolverBase {
1217
1280
  requestedSkillIDs?: string[],
1218
1281
  taskGraphDebug?: string,
1219
1282
  /** History floor — threaded through to ExecuteAgentParams.ConversationHistoryFrom. */
1220
- conversationHistoryFrom?: Date
1283
+ conversationHistoryFrom?: Date,
1284
+ /** The conversation branch of the run — threaded through to ExecuteAgentParams.ConversationBranchID. */
1285
+ conversationBranchId?: string | null
1221
1286
  ): void {
1222
1287
  // Ref the liveness pulse reads to enrich heartbeats once the run is created.
1223
1288
  const runRef: { current: MJAIAgentRunEntityExtended | null } = { current: null };
@@ -1237,7 +1302,7 @@ export class RunAIAgentResolver extends ResolverBase {
1237
1302
  data, payload, undefined, lastRunId, autoPopulateLastRunPayload,
1238
1303
  configurationId, conversationDetailId, createArtifacts, createNotification,
1239
1304
  sourceArtifactId, sourceArtifactVersionId, conversationId, runRef, planMode, requestedSkillIDs, taskGraphDebug,
1240
- conversationHistoryFrom
1305
+ conversationHistoryFrom, conversationBranchId
1241
1306
  ).catch((error: unknown) => {
1242
1307
  // Background execution failed unexpectedly (executeAIAgent has its own try-catch,
1243
1308
  // so this would only fire for truly unexpected errors).
@@ -1277,7 +1342,9 @@ export class RunAIAgentResolver extends ResolverBase {
1277
1342
  provider: IMetadataProvider,
1278
1343
  excludeDetailIds?: string[],
1279
1344
  /** History floor: only rows written at or after it are loaded, and no summary is used. */
1280
- historyFrom?: Date
1345
+ historyFrom?: Date,
1346
+ /** The branch whose path to load; null reads the trunk. */
1347
+ branchId?: string | null
1281
1348
  ): Promise<ChatMessage[]> {
1282
1349
  // Context windowing (summary boundary + raw tail, or legacy last-N when no
1283
1350
  // summary exists) shares ONE fold implementation with all other callers —
@@ -1294,7 +1361,7 @@ export class RunAIAgentResolver extends ResolverBase {
1294
1361
  // the agent against zero history.
1295
1362
  // 4. A history floor (`historyFrom`) is applied in the query, so rows before it never
1296
1363
  // leave the database, and again in the assembly, which then skips the summary.
1297
- const rows = await ConversationEngine.LoadWindowRowsFresh(conversationId, contextUser, provider, historyFrom);
1364
+ const rows = await ConversationEngine.LoadWindowRowsFresh(conversationId, contextUser, provider, historyFrom, branchId);
1298
1365
  const window = ConversationEngine.AssembleContextWindow(rows, {
1299
1366
  maxTailMessages: maxMessages,
1300
1367
  excludeDetailIds,
@@ -2,7 +2,7 @@ import { Resolver, Mutation, Arg, Ctx, ObjectType, Field } from 'type-graphql';
2
2
  import { AppContext, UserPayload } from '../types.js';
3
3
  import { LogError, LogStatus, Metadata, RunView } from '@memberjunction/core';
4
4
  import { MJTemplateContentEntity, MJTemplateEntityExtended } from '@memberjunction/core-entities';
5
- import { TemplateEngineServer } from '@memberjunction/templates';
5
+ import { GetTemplateRunRefusal, TemplateEngineServer } from '@memberjunction/templates';
6
6
  import { ResolverBase } from '../generic/ResolverBase.js';
7
7
  import { GetReadWriteProvider } from '../util.js';
8
8
 
@@ -45,6 +45,18 @@ export class RunTemplateResolver extends ResolverBase {
45
45
  try {
46
46
  LogStatus(`=== RUNNING TEMPLATE FOR ID: ${templateId} ===`);
47
47
 
48
+ // Same rule as the Template.Run remote operation, checked before anything is loaded
49
+ const currentUser = this.GetUserFromPayload(userPayload);
50
+ const p = GetReadWriteProvider(providers);
51
+ const refusal = GetTemplateRunRefusal(currentUser, p);
52
+ if (refusal) {
53
+ return {
54
+ success: false,
55
+ error: refusal,
56
+ executionTimeMs: Date.now() - startTime
57
+ };
58
+ }
59
+
48
60
  // Parse context data (JSON string)
49
61
  let data = {};
50
62
  if (contextData) {
@@ -59,17 +71,6 @@ export class RunTemplateResolver extends ResolverBase {
59
71
  }
60
72
  }
61
73
 
62
- // Get current user from payload
63
- const currentUser = this.GetUserFromPayload(userPayload);
64
- if (!currentUser) {
65
- return {
66
- success: false,
67
- error: 'Unable to determine current user',
68
- executionTimeMs: Date.now() - startTime
69
- };
70
- }
71
-
72
- const p = GetReadWriteProvider(providers);
73
74
  // Load the template entity
74
75
  const templateEntity = await p.GetEntityObject<MJTemplateEntityExtended>('MJ: Templates', currentUser);
75
76
  await templateEntity.Load(templateId);
@@ -3,6 +3,7 @@ import { AppContext, UserPayload } from '../types.js';
3
3
  import { BaseEntity, CompositeKey, DatabaseProviderBase, EntityDeleteOptions, EntitySaveOptions, IMetadataProvider, LogError, Metadata, RunView, UserInfo } from '@memberjunction/core';
4
4
  import { RequireSystemUser } from '../directives/RequireSystemUser.js';
5
5
  import { GetReadWriteProvider } from '../util.js';
6
+ import { CreateIsolatedProvider } from '../isolatedProvider.js';
6
7
  import { CompositeKeyInputType, CompositeKeyOutputType } from '../generic/KeyInputOutputTypes.js';
7
8
  import { MJDatasetItemEntity } from '@memberjunction/core-entities';
8
9
 
@@ -216,7 +217,11 @@ export class SyncDataResolver {
216
217
  try {
217
218
  // Run the view to find matching records, then delete them all atomically —
218
219
  // any single failure rolls back the entire batch so the dataset stays consistent.
219
- const rv = new RunView();
220
+ // The rows are loaded FROM the transaction's provider so each Delete() runs inside it;
221
+ // with no request provider, an isolated one, never the shared Metadata.Provider, whose
222
+ // transaction would swallow every concurrent caller's writes (see CreateIsolatedProvider).
223
+ const provider = (providerOverride as unknown as DatabaseProviderBase) ?? await CreateIsolatedProvider();
224
+ const rv = RunView.FromMetadataProvider(provider);
220
225
  const data = await rv.RunView<BaseEntity>({
221
226
  EntityName: entityName,
222
227
  ExtraFilter: filter,
@@ -234,7 +239,6 @@ export class SyncDataResolver {
234
239
  return;
235
240
  }
236
241
 
237
- const provider = (providerOverride ?? Metadata.Provider) as unknown as DatabaseProviderBase;
238
242
  await provider.BeginTransaction();
239
243
  try {
240
244
  for (const entityObject of data.Results) {
@@ -1,7 +1,7 @@
1
1
  import { Arg, Ctx, Field, InputType, Mutation, ObjectType, registerEnumType } from 'type-graphql';
2
2
  import { AppContext, UserPayload } from '../types.js';
3
3
  import { DatabaseProviderBase, EntityDeleteOptions, EntitySaveOptions, IMetadataProvider, LogError, Metadata, RunView, UserInfo } from '@memberjunction/core';
4
- import { UUIDsEqual } from '@memberjunction/global';
4
+ import { GroupByUUID, IndexByUUID, UUIDsEqual } from '@memberjunction/global';
5
5
  import { RequireSystemUser } from '../directives/RequireSystemUser.js';
6
6
  import { MJRoleEntity, MJUserEntity, MJUserRoleEntity } from '@memberjunction/core-entities';
7
7
  import { UserCache } from '@memberjunction/generic-database-provider';
@@ -430,14 +430,23 @@ export class SyncRolesAndUsersResolver {
430
430
  const dbRoles = rResult.Results;
431
431
  const dbUserRoles = urResult.Results;
432
432
 
433
+ // Index everything once: looking each user, role and assignment up by scanning the full lists
434
+ // made this O(users x user roles).
435
+ const dbUsersByEmail = this.indexByTrimmedLowerCase(dbUsers, u => u.Email);
436
+ const dbRolesByName = this.indexByTrimmedLowerCase(dbRoles, r => r.Name);
437
+ const dbRolesByID = IndexByUUID(dbRoles);
438
+ const dbUserRolesByUser = GroupByUUID(dbUserRoles, ur => ur.UserID);
439
+
433
440
  // Saves/Deletes below run inside the outer SyncUsers/SyncRolesAndUsers transaction.
434
441
  for (const user of users) {
435
- const dbUser = dbUsers.find(u => u.Email.trim().toLowerCase() === user.Email.trim().toLowerCase());
442
+ const dbUser = dbUsersByEmail.get(user.Email.trim().toLowerCase());
436
443
  if (!dbUser) continue;
437
444
 
445
+ // Read before any save below; the saves never add to dbUserRoles, so this matches the full-list scans it replaces.
446
+ const thisUserDBRoles = dbUserRolesByUser.Get(dbUser.ID) ?? [];
438
447
  for (const role of user.Roles) {
439
- const dbRole = dbRoles.find(r => r.Name.trim().toLowerCase() === role.Name.trim().toLowerCase());
440
- if (dbRole && !dbUserRoles.find(ur => UUIDsEqual(ur.UserID, dbUser.ID) && UUIDsEqual(ur.RoleID, dbRole.ID))) {
448
+ const dbRole = dbRolesByName.get(role.Name.trim().toLowerCase());
449
+ if (dbRole && !thisUserDBRoles.some(ur => UUIDsEqual(ur.RoleID, dbRole.ID))) {
441
450
  const ur = await md.GetEntityObject<MJUserRoleEntity>("MJ: User Roles", u);
442
451
  ur.UserID = dbUser.ID;
443
452
  ur.RoleID = dbRole.ID;
@@ -448,9 +457,8 @@ export class SyncRolesAndUsersResolver {
448
457
  }
449
458
 
450
459
  // Check for DB user roles that are NOT in the user.Roles input — those need to be removed
451
- const thisUserDBRoles = dbUserRoles.filter(ur => UUIDsEqual(ur.UserID, dbUser.ID));
452
460
  for (const dbUserRole of thisUserDBRoles) {
453
- const role = user.Roles.find(r => r.Name.trim().toLowerCase() === dbRoles.find(rr => UUIDsEqual(rr.ID, dbUserRole.RoleID))?.Name.trim().toLowerCase());
461
+ const role = user.Roles.find(r => r.Name.trim().toLowerCase() === dbRolesByID.Get(dbUserRole.RoleID)?.Name.trim().toLowerCase());
454
462
  if (!role && !this.IsStandardRole(dbUserRole.Role)) {
455
463
  if (!await dbUserRole.Delete()) {
456
464
  throw new Error(`Failed to remove role from user '${dbUser.Email}': ${dbUserRole.LatestResult?.CompleteMessage ?? 'unknown error'}`);
@@ -459,5 +467,15 @@ export class SyncRolesAndUsersResolver {
459
467
  }
460
468
  }
461
469
  }
470
+
471
+ /** Keys rows by a trimmed, lower-cased name; the first row wins, as `find` would return. */
472
+ private indexByTrimmedLowerCase<T>(rows: T[], keyOf: (row: T) => string | null | undefined): Map<string, T> {
473
+ const index = new Map<string, T>();
474
+ for (const row of rows) {
475
+ const key = keyOf(row)?.trim().toLowerCase();
476
+ if (key != null && !index.has(key)) index.set(key, row);
477
+ }
478
+ return index;
479
+ }
462
480
  }
463
481