@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
@@ -30,6 +30,8 @@ const {
30
30
  mockBeginTransaction,
31
31
  mockCommitTransaction,
32
32
  mockRollbackTransaction,
33
+ mockGlobalProviderCalls,
34
+ mockCreateIsolatedProvider,
33
35
  mockGetDefaultApplicationsForNewUser,
34
36
  mockRolesArray,
35
37
  mockApplicationsArray,
@@ -69,6 +71,10 @@ const {
69
71
  mockBeginTransaction: vi.fn(),
70
72
  mockCommitTransaction: vi.fn(),
71
73
  mockRollbackTransaction: vi.fn(),
74
+ // Every call new-user creation makes on the SHARED Metadata.Provider. Must stay empty: a
75
+ // transaction there nests into every concurrent request's (see CreateIsolatedProvider).
76
+ mockGlobalProviderCalls: [] as string[],
77
+ mockCreateIsolatedProvider: vi.fn(),
72
78
  mockGetDefaultApplicationsForNewUser: vi.fn(),
73
79
  mockRolesArray: [] as Array<{ ID: string; Name: string }>,
74
80
  mockApplicationsArray: [] as Array<{ ID: string; Name: string }>,
@@ -79,6 +85,9 @@ const {
79
85
 
80
86
  vi.mock('../config.js', () => ({ configInfo: mockConfig }));
81
87
 
88
+ // The provider new-user creation must use: its own instance, so its transaction is its own.
89
+ vi.mock('../isolatedProvider.js', () => ({ CreateIsolatedProvider: mockCreateIsolatedProvider }));
90
+
82
91
  vi.mock('@memberjunction/generic-database-provider', () => {
83
92
  const instance = {
84
93
  get Users() {
@@ -89,6 +98,18 @@ vi.mock('@memberjunction/generic-database-provider', () => {
89
98
  },
90
99
  Refresh: mockRefresh,
91
100
  GetSystemUser: vi.fn(),
101
+ /**
102
+ * The real cache falls back to a database read on a miss. The stub answers from
103
+ * the same array `Users` exposes, so these tests still describe the cache's behaviour.
104
+ */
105
+ FindUser: vi.fn(async ({ Email }: { Email?: string }) =>
106
+ mockCacheUsers.find((u: UserInfo) => {
107
+ if (!u.Email || u.Email.trim() === '') {
108
+ console.error(`SYSTEM METADATA ISSUE: User ${u.ID} has no email address`);
109
+ return false;
110
+ }
111
+ return u.Email.toLowerCase().trim() === (Email ?? '').toLowerCase().trim();
112
+ })),
92
113
  };
93
114
  return {
94
115
  UserCache: class MockUserCache {
@@ -104,10 +125,11 @@ vi.mock('@memberjunction/generic-database-provider', () => {
104
125
 
105
126
  vi.mock('@memberjunction/core', async (importOriginal) => {
106
127
  const actual = await importOriginal<typeof import('@memberjunction/core')>();
107
- const provider = {
108
- BeginTransaction: mockBeginTransaction,
109
- CommitTransaction: mockCommitTransaction,
110
- RollbackTransaction: mockRollbackTransaction,
128
+ // The shared global provider: records any use instead of doing anything.
129
+ const globalProvider = {
130
+ BeginTransaction: async () => { mockGlobalProviderCalls.push('BeginTransaction'); },
131
+ CommitTransaction: async () => { mockGlobalProviderCalls.push('CommitTransaction'); },
132
+ RollbackTransaction: async () => { mockGlobalProviderCalls.push('RollbackTransaction'); },
111
133
  Dialect: { BooleanLiteral: (value: boolean) => (value ? '1' : '0') },
112
134
  };
113
135
  class MockMetadata {
@@ -118,9 +140,10 @@ vi.mock('@memberjunction/core', async (importOriginal) => {
118
140
  return mockApplicationsArray;
119
141
  }
120
142
  public GetEntityObject(entityName: string, contextUser?: UserInfo): Promise<unknown> {
143
+ mockGlobalProviderCalls.push(`GetEntityObject:${entityName}`);
121
144
  return mockGetEntityObject(entityName, contextUser);
122
145
  }
123
- static Provider = provider;
146
+ static Provider = globalProvider;
124
147
  }
125
148
  class MockUserInfo {
126
149
  constructor(_provider: unknown, initData: Record<string, unknown>) {
@@ -128,6 +151,9 @@ vi.mock('@memberjunction/core', async (importOriginal) => {
128
151
  }
129
152
  }
130
153
  class MockRunView {
154
+ public static FromMetadataProvider(_provider: unknown): MockRunView {
155
+ return new MockRunView();
156
+ }
131
157
  public RunView(params: unknown, contextUser?: UserInfo): Promise<unknown> {
132
158
  return mockRunViewFn(params, contextUser);
133
159
  }
@@ -295,6 +321,21 @@ beforeEach(() => {
295
321
  });
296
322
  mockRunViewFn.mockResolvedValue({ Success: true, Results: [] });
297
323
  mockGetDefaultApplicationsForNewUser.mockReturnValue([]);
324
+
325
+ mockGlobalProviderCalls.length = 0;
326
+ mockCreateIsolatedProvider.mockImplementation(async () => ({
327
+ BeginTransaction: mockBeginTransaction,
328
+ CommitTransaction: mockCommitTransaction,
329
+ RollbackTransaction: mockRollbackTransaction,
330
+ Dialect: { BooleanLiteral: (value: boolean) => (value ? '1' : '0') },
331
+ get Roles() {
332
+ return mockRolesArray;
333
+ },
334
+ get Applications() {
335
+ return mockApplicationsArray;
336
+ },
337
+ GetEntityObject: (entityName: string, contextUser?: UserInfo) => mockGetEntityObject(entityName, contextUser),
338
+ }));
298
339
  });
299
340
 
300
341
  // ─── NewUserBase.createNewUser ──────────────────────────────────────────────
@@ -303,6 +344,34 @@ describe('NewUserBase.createNewUser', () => {
303
344
  const create = (first = 'Ada', last = 'Lovelace', email = 'ada@example.com') =>
304
345
  new NewUserBase().createNewUser(first, last, email);
305
346
 
347
+ describe('provider isolation', () => {
348
+ it('runs the whole provisioning on its own provider, never the shared Metadata.Provider', async () => {
349
+ mockConfig.userHandling.CreateUserApplicationRecords = true;
350
+ mockConfig.userHandling.UserApplications = ['CRM'];
351
+ mockRunViewFn.mockResolvedValue({ Success: true, Results: [{ EntityID: 'ent-1', Entity: 'Accounts' }] });
352
+
353
+ const user = await create();
354
+
355
+ expect(user).toBeTruthy();
356
+ expect(mockCreateIsolatedProvider).toHaveBeenCalledTimes(1);
357
+ expect(mockBeginTransaction).toHaveBeenCalledTimes(1);
358
+ expect(mockCommitTransaction).toHaveBeenCalledTimes(1);
359
+ // User, role, application and application-entity records all came from the isolated one.
360
+ expect(getEntityObjectCalls.map((c) => c.entityName)).toEqual([
361
+ 'MJ: Users', 'MJ: User Roles', 'MJ: User Applications', 'MJ: User Application Entities',
362
+ ]);
363
+ expect(mockGlobalProviderCalls).toEqual([]);
364
+ });
365
+
366
+ it('rolls back on the isolated provider too, never the shared one', async () => {
367
+ saveBehavior['MJ: Users'] = () => false;
368
+
369
+ expect(await create()).toBeNull();
370
+ expect(mockRollbackTransaction).toHaveBeenCalledTimes(1);
371
+ expect(mockGlobalProviderCalls).toEqual([]);
372
+ });
373
+ });
374
+
306
375
  describe('context-user resolution', () => {
307
376
  it('uses the configured contextUserForNewUserCreation for every entity it creates', async () => {
308
377
  const user = await create();
@@ -20,8 +20,10 @@ const { mockGetEntityObject, writableStore } = vi.hoisted(() => ({
20
20
  writableStore: { last: null as Record<string, unknown> | null },
21
21
  }));
22
22
 
23
- vi.mock('@memberjunction/global', () => ({
24
- UUIDsEqual: (a: string, b: string) => String(a).toLowerCase() === String(b).toLowerCase(),
23
+ // The real UUID helpers: the reconciler indexes users with IndexByUUID, so a stub that only
24
+ // provides UUIDsEqual would make the owner lookup throw (and the pass silently do nothing).
25
+ vi.mock('@memberjunction/global', async (importOriginal) => ({
26
+ ...(await importOriginal<typeof import('@memberjunction/global')>()),
25
27
  }));
26
28
 
27
29
  vi.mock('@memberjunction/sqlserver-dataprovider', () => ({
@@ -230,6 +232,19 @@ describe('ReconcileOrphanedConversationDetails', () => {
230
232
  );
231
233
  });
232
234
 
235
+ it('resolves the owner when the conversation spells the user ID in a different case', async () => {
236
+ // The cached user is 'OWNER-1'; SQL Server and PostgreSQL disagree on UUID case, so the
237
+ // conversation row may carry it lower-cased. The owner lookup must still find the user.
238
+ script([detail()], [{ ConversationDetailID: DETAIL_ID, Status: 'Failed', CompletedAt: longAgo() }], 'owner-1');
239
+
240
+ await ReconcileOrphanedConversationDetails(PROVIDER, USER);
241
+
242
+ expect(mockGetEntityObject).toHaveBeenCalledWith(
243
+ 'MJ: Conversation Details',
244
+ expect.objectContaining({ ID: 'OWNER-1' })
245
+ );
246
+ });
247
+
233
248
  it('leaves the detail alone when no owner can be resolved', async () => {
234
249
  script([detail()], [{ ConversationDetailID: DETAIL_ID, Status: 'Failed', CompletedAt: longAgo() }], null);
235
250
 
@@ -0,0 +1,201 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import type { IMetadataProvider, UserInfo } from '@memberjunction/core';
3
+ import { MJGlobal } from '@memberjunction/global';
4
+ import type { PubSubEngine } from 'type-graphql';
5
+ import { PubSubManager } from '../../generic/PubSubManager.js';
6
+ import {
7
+ ParseReplicatedRealtimeSessionEvent,
8
+ REALTIME_SESSION_EVENTS_TOPIC,
9
+ RealtimeSessionEventService,
10
+ type RealtimeSessionEventTopicPayload,
11
+ } from '../../realtimeSessions/RealtimeSessionEventService.js';
12
+
13
+ const OWNER = 'AAAAAAAA-0000-4000-8000-000000000001';
14
+ const SESSION_ID = 'BBBBBBBB-0000-4000-8000-000000000001';
15
+ const CONVERSATION_ID = 'CCCCCCCC-0000-4000-8000-000000000001';
16
+ const USER = { ID: OWNER } as UserInfo;
17
+
18
+ const VERIFIED = { VerifiedEmail: 'pat@example.com', VerifiedName: 'Pat', VerifiedAt: '2026-10-02T12:00:00.000Z', Method: 'link' as const };
19
+
20
+ /** A provider that serves one session (+ conversation) — or nothing. */
21
+ function provider(found = true, externalId: string | null = 'scope-1'): IMetadataProvider {
22
+ return {
23
+ GetEntityObject: vi.fn(async (name: string) => {
24
+ if (name === 'MJ: AI Agent Sessions') {
25
+ return found
26
+ ? { ID: SESSION_ID, UserID: OWNER, ConversationID: CONVERSATION_ID, Load: vi.fn(async () => true) }
27
+ : { Load: vi.fn(async () => false) };
28
+ }
29
+ return { ExternalID: externalId, Load: vi.fn(async () => true) };
30
+ }),
31
+ } as unknown as IMetadataProvider;
32
+ }
33
+
34
+ describe('RealtimeSessionEventService', () => {
35
+ const published: Array<{ topic: string; payload: RealtimeSessionEventTopicPayload }> = [];
36
+
37
+ beforeEach(() => {
38
+ published.length = 0;
39
+ const engine = { publish: vi.fn(async (topic: string, payload: RealtimeSessionEventTopicPayload) => void published.push({ topic, payload })) };
40
+ PubSubManager.Instance.SetPubSubEngine(engine as unknown as PubSubEngine);
41
+ RealtimeSessionEventService.Instance.SetReplicationHook(undefined);
42
+ });
43
+
44
+ describe('Publish', () => {
45
+ it('publishes on the shared topic with the session routing snapshot', async () => {
46
+ const result = await RealtimeSessionEventService.Instance.Publish({
47
+ AgentSessionID: SESSION_ID,
48
+ Type: 'identity.verified',
49
+ Payload: VERIFIED,
50
+ ContextUser: USER,
51
+ Provider: provider(),
52
+ });
53
+ expect(result).toEqual({ Success: true });
54
+ expect(published).toHaveLength(1);
55
+ expect(published[0].topic).toBe(REALTIME_SESSION_EVENTS_TOPIC);
56
+ expect(published[0].payload).toMatchObject({
57
+ Type: 'identity.verified',
58
+ AgentSessionID: SESSION_ID,
59
+ OwnerUserID: OWNER,
60
+ ScopeKey: 'scope-1',
61
+ SourceServerId: MJGlobal.Instance.ProcessUUID,
62
+ });
63
+ expect(JSON.parse(published[0].payload.PayloadJson)).toEqual(VERIFIED);
64
+ expect(Date.parse(published[0].payload.OccurredAt)).not.toBeNaN();
65
+ });
66
+
67
+ it('snapshots a null scope for a session whose conversation carries none', async () => {
68
+ await RealtimeSessionEventService.Instance.Publish({
69
+ AgentSessionID: SESSION_ID,
70
+ Type: 'identity.verified',
71
+ Payload: VERIFIED,
72
+ ContextUser: USER,
73
+ Provider: provider(true, null),
74
+ });
75
+ expect(published[0].payload.ScopeKey).toBeNull();
76
+ });
77
+
78
+ it('fails (and publishes nothing) when the session cannot be found', async () => {
79
+ const result = await RealtimeSessionEventService.Instance.Publish({
80
+ AgentSessionID: SESSION_ID,
81
+ Type: 'identity.verified',
82
+ Payload: VERIFIED,
83
+ ContextUser: USER,
84
+ Provider: provider(false),
85
+ });
86
+ expect(result.Success).toBe(false);
87
+ expect(result.ErrorMessage).toContain('not found');
88
+ expect(published).toHaveLength(0);
89
+ });
90
+
91
+ it('returns a failure instead of throwing when reading the session throws', async () => {
92
+ const broken = { GetEntityObject: vi.fn(async () => { throw new Error('db down'); }) } as unknown as IMetadataProvider;
93
+ const result = await RealtimeSessionEventService.Instance.Publish({
94
+ AgentSessionID: SESSION_ID,
95
+ Type: 'identity.verified',
96
+ Payload: VERIFIED,
97
+ ContextUser: USER,
98
+ Provider: broken,
99
+ });
100
+ expect(result).toMatchObject({ Success: false });
101
+ expect(result.ErrorMessage).toContain('db down');
102
+ });
103
+ });
104
+
105
+ describe('PublishRouted', () => {
106
+ it('fails loudly when PubSub is not configured', () => {
107
+ PubSubManager.Instance.SetPubSubEngine(null as unknown as PubSubEngine);
108
+ const result = RealtimeSessionEventService.Instance.PublishRouted({ AgentSessionID: SESSION_ID, OwnerUserID: OWNER, ScopeKey: null }, 'identity.verified', VERIFIED);
109
+ expect(result).toMatchObject({ Success: false });
110
+ expect(result.ErrorMessage).toContain('PubSub is not configured');
111
+ });
112
+
113
+ it('hands every locally published event to the replication hook', () => {
114
+ const hook = vi.fn();
115
+ RealtimeSessionEventService.Instance.SetReplicationHook(hook);
116
+ RealtimeSessionEventService.Instance.PublishRouted({ AgentSessionID: SESSION_ID, OwnerUserID: OWNER, ScopeKey: null }, 'identity.verified', VERIFIED);
117
+ expect(hook).toHaveBeenCalledTimes(1);
118
+ expect(hook.mock.calls[0][0]).toMatchObject({ AgentSessionID: SESSION_ID, OwnerUserID: OWNER, Type: 'identity.verified' });
119
+ });
120
+
121
+ it('never lets a failing replication hook break local delivery', () => {
122
+ RealtimeSessionEventService.Instance.SetReplicationHook(() => {
123
+ throw new Error('redis down');
124
+ });
125
+ const result = RealtimeSessionEventService.Instance.PublishRouted({ AgentSessionID: SESSION_ID, OwnerUserID: OWNER, ScopeKey: null }, 'identity.verified', VERIFIED);
126
+ expect(result).toEqual({ Success: true });
127
+ expect(published).toHaveLength(1);
128
+ });
129
+ });
130
+
131
+ describe('PublishReplicated', () => {
132
+ it('republishes locally and never back through the replication hook (no loop)', () => {
133
+ const hook = vi.fn();
134
+ RealtimeSessionEventService.Instance.SetReplicationHook(hook);
135
+ const payload: RealtimeSessionEventTopicPayload = {
136
+ Type: 'identity.verified',
137
+ AgentSessionID: SESSION_ID,
138
+ OccurredAt: '2026-10-02T12:00:00.000Z',
139
+ PayloadJson: JSON.stringify(VERIFIED),
140
+ OwnerUserID: OWNER,
141
+ ScopeKey: null,
142
+ SourceServerId: 'other-instance',
143
+ };
144
+ RealtimeSessionEventService.Instance.PublishReplicated(payload);
145
+ expect(published).toEqual([{ topic: REALTIME_SESSION_EVENTS_TOPIC, payload }]);
146
+ expect(hook).not.toHaveBeenCalled();
147
+ });
148
+ });
149
+ });
150
+
151
+ describe('ParseReplicatedRealtimeSessionEvent', () => {
152
+ const wire = (over: Record<string, unknown> = {}) =>
153
+ JSON.stringify({
154
+ Type: 'identity.verified',
155
+ AgentSessionID: SESSION_ID,
156
+ OccurredAt: '2026-10-02T12:00:00.000Z',
157
+ PayloadJson: '{}',
158
+ OwnerUserID: OWNER,
159
+ ScopeKey: 'scope-1',
160
+ SourceServerId: 'other-instance',
161
+ ...over,
162
+ });
163
+
164
+ it('accepts a well-formed event from another instance', () => {
165
+ expect(ParseReplicatedRealtimeSessionEvent(wire(), 'this-instance')).toEqual({
166
+ Type: 'identity.verified',
167
+ AgentSessionID: SESSION_ID,
168
+ OccurredAt: '2026-10-02T12:00:00.000Z',
169
+ PayloadJson: '{}',
170
+ OwnerUserID: OWNER,
171
+ ScopeKey: 'scope-1',
172
+ SourceServerId: 'other-instance',
173
+ });
174
+ });
175
+
176
+ it('drops our own echo', () => {
177
+ expect(ParseReplicatedRealtimeSessionEvent(wire({ SourceServerId: 'this-instance' }), 'this-instance')).toBeNull();
178
+ });
179
+
180
+ it('fails closed when the routing identity is missing or malformed', () => {
181
+ expect(ParseReplicatedRealtimeSessionEvent(wire({ OwnerUserID: undefined }), 'x')).toBeNull();
182
+ expect(ParseReplicatedRealtimeSessionEvent(wire({ OwnerUserID: '' }), 'x')).toBeNull();
183
+ expect(ParseReplicatedRealtimeSessionEvent(wire({ OwnerUserID: 5 }), 'x')).toBeNull();
184
+ expect(ParseReplicatedRealtimeSessionEvent(wire({ ScopeKey: 5 }), 'x')).toBeNull();
185
+ });
186
+
187
+ it('normalises an absent scope to null and tolerates an absent source', () => {
188
+ const parsed = ParseReplicatedRealtimeSessionEvent(wire({ ScopeKey: undefined, SourceServerId: undefined }), 'x');
189
+ expect(parsed?.ScopeKey).toBeNull();
190
+ expect(parsed?.SourceServerId).toBeUndefined();
191
+ });
192
+
193
+ it('drops non-JSON, non-objects and events missing required strings', () => {
194
+ expect(ParseReplicatedRealtimeSessionEvent('{nope', 'x')).toBeNull();
195
+ expect(ParseReplicatedRealtimeSessionEvent('"text"', 'x')).toBeNull();
196
+ expect(ParseReplicatedRealtimeSessionEvent('null', 'x')).toBeNull();
197
+ expect(ParseReplicatedRealtimeSessionEvent(wire({ Type: undefined }), 'x')).toBeNull();
198
+ expect(ParseReplicatedRealtimeSessionEvent(wire({ PayloadJson: undefined }), 'x')).toBeNull();
199
+ expect(ParseReplicatedRealtimeSessionEvent(wire({ SourceServerId: 7 }), 'x')).toBeNull();
200
+ });
201
+ });
@@ -0,0 +1,215 @@
1
+ import 'reflect-metadata';
2
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
3
+ import { PubSub } from 'graphql-subscriptions';
4
+ import type { IMetadataProvider, UserInfo } from '@memberjunction/core';
5
+ import type { PubSubEngine } from 'type-graphql';
6
+ import { PubSubManager } from '../../generic/PubSubManager.js';
7
+ import type { UserPayload } from '../../types.js';
8
+ import { RealtimeSessionEventService, type RealtimeSessionEventTopicPayload } from '../../realtimeSessions/RealtimeSessionEventService.js';
9
+ import type { LoadedSessionForAccess } from '../../realtimeSessions/sessionAccess.js';
10
+ import { AuthorizeAndSubscribeToRealtimeSessionEvents, RealtimeSessionEventsResolver } from '../../resolvers/RealtimeSessionEventsResolver.js';
11
+ import {
12
+ AuthorizeRealtimeSessionSubscription,
13
+ RealtimeSessionEventsFilter,
14
+ type RealtimeSessionEventsContext,
15
+ type SubscriptionAuthorizationDeps,
16
+ } from '../../realtimeSessions/subscriptionAuthorization.js';
17
+
18
+ const OWNER = 'AAAAAAAA-0000-4000-8000-000000000001';
19
+ const STRANGER = 'AAAAAAAA-0000-4000-8000-000000000002';
20
+ const SESSION_ID = 'BBBBBBBB-0000-4000-8000-000000000001';
21
+ const OTHER_SESSION_ID = 'BBBBBBBB-0000-4000-8000-000000000002';
22
+
23
+ /** A connection context for a principal. */
24
+ function contextFor(user: Partial<UserInfo> & { ID: string }): RealtimeSessionEventsContext {
25
+ return { userPayload: { email: 'x@y.z', sessionId: 's', userRecord: user } as UserPayload };
26
+ }
27
+
28
+ const named = (id: string) => contextFor({ ID: id });
29
+ const guest = (id: string, scope?: string) =>
30
+ contextFor({ ID: id, IsMagicLinkAnonymous: true, MagicLinkScope: scope ? { ResourceID: scope } : undefined } as Partial<UserInfo> & { ID: string });
31
+
32
+ function event(over: Partial<RealtimeSessionEventTopicPayload> = {}): RealtimeSessionEventTopicPayload {
33
+ return {
34
+ Type: 'identity.verified',
35
+ AgentSessionID: SESSION_ID,
36
+ OccurredAt: '2026-10-02T12:00:00.000Z',
37
+ PayloadJson: '{"VerifiedEmail":"pat@example.com"}',
38
+ OwnerUserID: OWNER,
39
+ ScopeKey: null,
40
+ ...over,
41
+ };
42
+ }
43
+
44
+ describe('RealtimeSessionEventsFilter (per-event gate)', () => {
45
+ const filter = (payload: RealtimeSessionEventTopicPayload, agentSessionId: string | undefined, context: RealtimeSessionEventsContext | undefined) =>
46
+ RealtimeSessionEventsFilter({ payload, args: { agentSessionId }, context });
47
+
48
+ it('delivers an event for the requested session to its owner', () => {
49
+ expect(filter(event(), SESSION_ID, named(OWNER))).toBe(true);
50
+ });
51
+
52
+ it('does not deliver an event addressed to a different session', () => {
53
+ expect(filter(event({ AgentSessionID: OTHER_SESSION_ID }), SESSION_ID, named(OWNER))).toBe(false);
54
+ });
55
+
56
+ it('does not deliver to a non-owner who guessed the session id', () => {
57
+ expect(filter(event(), SESSION_ID, named(STRANGER))).toBe(false);
58
+ });
59
+
60
+ it('compares session and user ids case-insensitively', () => {
61
+ expect(filter(event({ AgentSessionID: SESSION_ID.toLowerCase(), OwnerUserID: OWNER.toLowerCase() }), SESSION_ID, named(OWNER))).toBe(true);
62
+ });
63
+
64
+ describe('anonymous guests share one Anonymous user — only the signed scope tells them apart', () => {
65
+ it('delivers to the guest whose scope matches', () => {
66
+ expect(filter(event({ ScopeKey: 'scope-1' }), SESSION_ID, guest(OWNER, 'scope-1'))).toBe(true);
67
+ });
68
+
69
+ it('does not deliver to another guest (same user id, different scope)', () => {
70
+ expect(filter(event({ ScopeKey: 'scope-1' }), SESSION_ID, guest(OWNER, 'scope-2'))).toBe(false);
71
+ });
72
+
73
+ it('does not deliver to an anonymous connection with no scope', () => {
74
+ expect(filter(event({ ScopeKey: 'scope-1' }), SESSION_ID, guest(OWNER))).toBe(false);
75
+ });
76
+ });
77
+
78
+ it('fails closed on a missing connection identity, owner or argument', () => {
79
+ expect(filter(event(), SESSION_ID, undefined)).toBe(false);
80
+ expect(filter(event(), SESSION_ID, { userPayload: undefined })).toBe(false);
81
+ expect(filter(event({ OwnerUserID: '' }), SESSION_ID, named(OWNER))).toBe(false);
82
+ expect(filter(event(), undefined, named(OWNER))).toBe(false);
83
+ });
84
+ });
85
+
86
+ describe('AuthorizeRealtimeSessionSubscription (subscribe-time gate)', () => {
87
+ /** Deps whose session load returns the given owner/scope (or nothing). */
88
+ const deps = (loaded: { UserID: string; ExternalID: string | null } | null): SubscriptionAuthorizationDeps & { LoadSession: ReturnType<typeof vi.fn> } => ({
89
+ LoadSession: vi.fn(async () =>
90
+ loaded ? ({ Session: { ID: SESSION_ID, UserID: loaded.UserID }, ConversationExternalID: loaded.ExternalID } as unknown as LoadedSessionForAccess) : null,
91
+ ),
92
+ GetProvider: () => ({}) as IMetadataProvider,
93
+ });
94
+
95
+ it('lets the owner subscribe and returns the validated id', async () => {
96
+ const d = deps({ UserID: OWNER, ExternalID: null });
97
+ await expect(AuthorizeRealtimeSessionSubscription({ agentSessionId: SESSION_ID }, named(OWNER), d)).resolves.toBe(SESSION_ID);
98
+ expect(d.LoadSession).toHaveBeenCalledWith(SESSION_ID, expect.objectContaining({ ID: OWNER }), expect.anything());
99
+ });
100
+
101
+ it('refuses a non-owner with the uniform not-found message', async () => {
102
+ await expect(AuthorizeRealtimeSessionSubscription({ agentSessionId: SESSION_ID }, named(STRANGER), deps({ UserID: OWNER, ExternalID: null }))).rejects.toThrow('Realtime session not found.');
103
+ });
104
+
105
+ it('gives the SAME message when the session does not exist, so ids cannot be probed', async () => {
106
+ await expect(AuthorizeRealtimeSessionSubscription({ agentSessionId: SESSION_ID }, named(OWNER), deps(null))).rejects.toThrow('Realtime session not found.');
107
+ });
108
+
109
+ it('refuses an anonymous guest whose scope does not match the session conversation', async () => {
110
+ await expect(AuthorizeRealtimeSessionSubscription({ agentSessionId: SESSION_ID }, guest(OWNER, 'scope-2'), deps({ UserID: OWNER, ExternalID: 'scope-1' }))).rejects.toThrow('Realtime session not found.');
111
+ });
112
+
113
+ it('accepts an anonymous guest whose scope matches', async () => {
114
+ await expect(AuthorizeRealtimeSessionSubscription({ agentSessionId: SESSION_ID }, guest(OWNER, 'scope-1'), deps({ UserID: OWNER, ExternalID: 'scope-1' }))).resolves.toBe(SESSION_ID);
115
+ });
116
+
117
+ it('refuses an anonymous connection with no scope', async () => {
118
+ await expect(AuthorizeRealtimeSessionSubscription({ agentSessionId: SESSION_ID }, guest(OWNER), deps({ UserID: OWNER, ExternalID: 'scope-1' }))).rejects.toThrow('Realtime session not found.');
119
+ });
120
+
121
+ it('refuses without touching the database when there is no identity or the id is malformed', async () => {
122
+ const d = deps({ UserID: OWNER, ExternalID: null });
123
+ await expect(AuthorizeRealtimeSessionSubscription({ agentSessionId: SESSION_ID }, undefined, d)).rejects.toThrow('Realtime session not found.');
124
+ await expect(AuthorizeRealtimeSessionSubscription({ agentSessionId: SESSION_ID }, { userPayload: undefined }, d)).rejects.toThrow();
125
+ await expect(AuthorizeRealtimeSessionSubscription({ agentSessionId: 'not-a-uuid' }, named(OWNER), d)).rejects.toThrow();
126
+ await expect(AuthorizeRealtimeSessionSubscription({}, named(OWNER), d)).rejects.toThrow();
127
+ expect(d.LoadSession).not.toHaveBeenCalled();
128
+ });
129
+ });
130
+
131
+ describe('RealtimeSessionEvents subscription, end to end over a real PubSub', () => {
132
+ let engine: PubSub;
133
+ const info = {};
134
+
135
+ beforeEach(() => {
136
+ engine = new PubSub();
137
+ PubSubManager.Instance.SetPubSubEngine(engine as unknown as PubSubEngine);
138
+ RealtimeSessionEventService.Instance.SetReplicationHook(undefined);
139
+ });
140
+
141
+ const allow = (loaded: { UserID: string; ExternalID: string | null }): SubscriptionAuthorizationDeps => ({
142
+ LoadSession: async () => ({ Session: { ID: SESSION_ID, UserID: loaded.UserID }, ConversationExternalID: loaded.ExternalID }) as unknown as LoadedSessionForAccess,
143
+ GetProvider: () => ({}) as IMetadataProvider,
144
+ });
145
+
146
+ const VERIFIED = { VerifiedEmail: 'pat@example.com', VerifiedName: 'Pat', VerifiedAt: '2026-10-02T12:00:00.000Z', Method: 'code' as const };
147
+
148
+ /** graphql-subscriptions registers its PubSub listener lazily, on the first `next()`; let that settle before publishing. */
149
+ const settled = () => new Promise<void>((resolve) => setImmediate(resolve));
150
+
151
+ it('refuses a non-owner before any stream is opened', async () => {
152
+ await expect(
153
+ AuthorizeAndSubscribeToRealtimeSessionEvents({}, { agentSessionId: SESSION_ID }, named(STRANGER), info, allow({ UserID: OWNER, ExternalID: null })),
154
+ ).rejects.toThrow('Realtime session not found.');
155
+ });
156
+
157
+ it('delivers only the owner\'s events for the requested session, in order, and nothing else', async () => {
158
+ const stream = await AuthorizeAndSubscribeToRealtimeSessionEvents({}, { agentSessionId: SESSION_ID }, named(OWNER), info, allow({ UserID: OWNER, ExternalID: null }));
159
+ const service = RealtimeSessionEventService.Instance;
160
+ const firstPull = stream.next(); // starts the subscription
161
+ await settled();
162
+
163
+ // Noise that must NOT arrive: another session of the same owner, and another owner's event for the same id.
164
+ service.PublishRouted({ AgentSessionID: OTHER_SESSION_ID, OwnerUserID: OWNER, ScopeKey: null }, 'identity.verified', VERIFIED);
165
+ service.PublishRouted({ AgentSessionID: SESSION_ID, OwnerUserID: STRANGER, ScopeKey: null }, 'identity.verified', { ...VERIFIED, VerifiedEmail: 'leak@example.com' });
166
+ // The two that must.
167
+ service.PublishRouted({ AgentSessionID: SESSION_ID, OwnerUserID: OWNER, ScopeKey: null }, 'identity.verified', VERIFIED);
168
+ service.PublishRouted({ AgentSessionID: SESSION_ID, OwnerUserID: OWNER, ScopeKey: null }, 'identity.verified', { ...VERIFIED, Method: 'link' });
169
+
170
+ const first = await firstPull;
171
+ const second = await stream.next();
172
+ expect(JSON.parse(first.value.PayloadJson)).toMatchObject({ VerifiedEmail: 'pat@example.com', Method: 'code' });
173
+ expect(JSON.parse(second.value.PayloadJson)).toMatchObject({ Method: 'link' });
174
+ expect(first.value.AgentSessionID).toBe(SESSION_ID);
175
+
176
+ // Nothing else is queued: a third event never arrives until one is published.
177
+ const pending = stream.next();
178
+ const raced = await Promise.race([pending.then(() => 'delivered'), new Promise((resolve) => setTimeout(() => resolve('quiet'), 30))]);
179
+ expect(raced).toBe('quiet');
180
+ await stream.return?.();
181
+ });
182
+
183
+ it('does not deliver a guest\'s event to a different guest sharing the Anonymous user', async () => {
184
+ const guestB = await AuthorizeAndSubscribeToRealtimeSessionEvents({}, { agentSessionId: SESSION_ID }, guest(OWNER, 'scope-B'), info, allow({ UserID: OWNER, ExternalID: 'scope-B' }));
185
+ const pull = guestB.next();
186
+ await settled();
187
+ // An event for guest A's session id would never reach B (the id differs); simulate the worst case: same id, A's scope.
188
+ RealtimeSessionEventService.Instance.PublishRouted({ AgentSessionID: SESSION_ID, OwnerUserID: OWNER, ScopeKey: 'scope-A' }, 'identity.verified', VERIFIED);
189
+ RealtimeSessionEventService.Instance.PublishRouted({ AgentSessionID: SESSION_ID, OwnerUserID: OWNER, ScopeKey: 'scope-B' }, 'identity.verified', { ...VERIFIED, VerifiedEmail: 'b@example.com' });
190
+ const got = await pull;
191
+ expect(JSON.parse(got.value.PayloadJson).VerifiedEmail).toBe('b@example.com');
192
+ await guestB.return?.();
193
+ });
194
+
195
+ it('fails when PubSub is not configured', async () => {
196
+ PubSubManager.Instance.SetPubSubEngine(null as unknown as PubSubEngine);
197
+ await expect(
198
+ AuthorizeAndSubscribeToRealtimeSessionEvents({}, { agentSessionId: SESSION_ID }, named(OWNER), info, allow({ UserID: OWNER, ExternalID: null })),
199
+ ).rejects.toThrow('PubSub is not configured');
200
+ });
201
+ });
202
+
203
+ describe('RealtimeSessionEventsResolver.RealtimeSessionEvents (resolve)', () => {
204
+ it('returns the wire event and never exposes the routing snapshot', () => {
205
+ const out = new RealtimeSessionEventsResolver().RealtimeSessionEvents(event({ ScopeKey: 'scope-1' }), SESSION_ID);
206
+ expect(out).toEqual({
207
+ Type: 'identity.verified',
208
+ AgentSessionID: SESSION_ID,
209
+ OccurredAt: '2026-10-02T12:00:00.000Z',
210
+ PayloadJson: '{"VerifiedEmail":"pat@example.com"}',
211
+ });
212
+ expect(Object.keys(out)).not.toContain('OwnerUserID');
213
+ expect(Object.keys(out)).not.toContain('ScopeKey');
214
+ });
215
+ });