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

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 (401) hide show
  1. package/README.md +27 -1
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/agentSessions/SessionJanitor.d.ts +3 -3
  4. package/dist/agentSessions/SessionJanitor.js +3 -3
  5. package/dist/agentSessions/SessionManager.d.ts +15 -5
  6. package/dist/agentSessions/SessionManager.d.ts.map +1 -1
  7. package/dist/agentSessions/SessionManager.js +34 -12
  8. package/dist/agentSessions/SessionManager.js.map +1 -1
  9. package/dist/auth/AuthProviderCatalogRouter.d.ts.map +1 -1
  10. package/dist/auth/AuthProviderCatalogRouter.js +2 -0
  11. package/dist/auth/AuthProviderCatalogRouter.js.map +1 -1
  12. package/dist/auth/index.d.ts.map +1 -1
  13. package/dist/auth/index.js +51 -10
  14. package/dist/auth/index.js.map +1 -1
  15. package/dist/auth/initializeProviders.d.ts +10 -0
  16. package/dist/auth/initializeProviders.d.ts.map +1 -1
  17. package/dist/auth/initializeProviders.js +37 -1
  18. package/dist/auth/initializeProviders.js.map +1 -1
  19. package/dist/auth/localDevAuth.d.ts +79 -0
  20. package/dist/auth/localDevAuth.d.ts.map +1 -0
  21. package/dist/auth/localDevAuth.js +439 -0
  22. package/dist/auth/localDevAuth.js.map +1 -0
  23. package/dist/auth/magicLink/MagicLinkKeys.d.ts +2 -2
  24. package/dist/auth/magicLink/MagicLinkKeys.d.ts.map +1 -1
  25. package/dist/auth/magicLink/MagicLinkKeys.js +1 -1
  26. package/dist/auth/magicLink/MagicLinkKeys.js.map +1 -1
  27. package/dist/auth/magicLink/types.d.ts +12 -0
  28. package/dist/auth/magicLink/types.d.ts.map +1 -1
  29. package/dist/auth/sessionUserClone.d.ts +1 -1
  30. package/dist/auth/sessionUserClone.d.ts.map +1 -1
  31. package/dist/auth/sessionUserClone.js +3 -2
  32. package/dist/auth/sessionUserClone.js.map +1 -1
  33. package/dist/broadcast/BaseCacheInvalidationBroadcastProvider.d.ts +292 -0
  34. package/dist/broadcast/BaseCacheInvalidationBroadcastProvider.d.ts.map +1 -0
  35. package/dist/broadcast/BaseCacheInvalidationBroadcastProvider.js +442 -0
  36. package/dist/broadcast/BaseCacheInvalidationBroadcastProvider.js.map +1 -0
  37. package/dist/broadcast/index.d.ts +6 -0
  38. package/dist/broadcast/index.d.ts.map +1 -0
  39. package/dist/broadcast/index.js +6 -0
  40. package/dist/broadcast/index.js.map +1 -0
  41. package/dist/broadcast/providers/AWSIoTCoreBroadcastProvider.d.ts +43 -0
  42. package/dist/broadcast/providers/AWSIoTCoreBroadcastProvider.d.ts.map +1 -0
  43. package/dist/broadcast/providers/AWSIoTCoreBroadcastProvider.js +144 -0
  44. package/dist/broadcast/providers/AWSIoTCoreBroadcastProvider.js.map +1 -0
  45. package/dist/broadcast/providers/AzureWebPubSubBroadcastProvider.d.ts +46 -0
  46. package/dist/broadcast/providers/AzureWebPubSubBroadcastProvider.d.ts.map +1 -0
  47. package/dist/broadcast/providers/AzureWebPubSubBroadcastProvider.js +149 -0
  48. package/dist/broadcast/providers/AzureWebPubSubBroadcastProvider.js.map +1 -0
  49. package/dist/broadcast/providers/GCPPubSubBroadcastProvider.d.ts +49 -0
  50. package/dist/broadcast/providers/GCPPubSubBroadcastProvider.d.ts.map +1 -0
  51. package/dist/broadcast/providers/GCPPubSubBroadcastProvider.js +131 -0
  52. package/dist/broadcast/providers/GCPPubSubBroadcastProvider.js.map +1 -0
  53. package/dist/broadcast/providers/SequencedRoleBucketBroadcastProvider.d.ts +24 -0
  54. package/dist/broadcast/providers/SequencedRoleBucketBroadcastProvider.d.ts.map +1 -0
  55. package/dist/broadcast/providers/SequencedRoleBucketBroadcastProvider.js +51 -0
  56. package/dist/broadcast/providers/SequencedRoleBucketBroadcastProvider.js.map +1 -0
  57. package/dist/cache.d.ts +3 -2
  58. package/dist/cache.d.ts.map +1 -1
  59. package/dist/cache.js.map +1 -1
  60. package/dist/config.d.ts +496 -0
  61. package/dist/config.d.ts.map +1 -1
  62. package/dist/config.js +61 -0
  63. package/dist/config.js.map +1 -1
  64. package/dist/context.d.ts +3 -1
  65. package/dist/context.d.ts.map +1 -1
  66. package/dist/context.js +29 -8
  67. package/dist/context.js.map +1 -1
  68. package/dist/coreRoutes.d.ts.map +1 -1
  69. package/dist/coreRoutes.js +14 -0
  70. package/dist/coreRoutes.js.map +1 -1
  71. package/dist/generated/graphql-schemas/__mj.d.ts +635 -14
  72. package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
  73. package/dist/generated/graphql-schemas/__mj.js +3366 -418
  74. package/dist/generated/graphql-schemas/__mj.js.map +1 -1
  75. package/dist/generic/CacheInvalidationResolver.d.ts +50 -30
  76. package/dist/generic/CacheInvalidationResolver.d.ts.map +1 -1
  77. package/dist/generic/CacheInvalidationResolver.js +154 -79
  78. package/dist/generic/CacheInvalidationResolver.js.map +1 -1
  79. package/dist/generic/OrphanedConversationDetailReconciler.d.ts.map +1 -1
  80. package/dist/generic/OrphanedConversationDetailReconciler.js +2 -0
  81. package/dist/generic/OrphanedConversationDetailReconciler.js.map +1 -1
  82. package/dist/generic/PubSubManager.d.ts +10 -0
  83. package/dist/generic/PubSubManager.d.ts.map +1 -1
  84. package/dist/generic/PubSubManager.js +44 -1
  85. package/dist/generic/PubSubManager.js.map +1 -1
  86. package/dist/generic/PushStatusResolver.d.ts +39 -6
  87. package/dist/generic/PushStatusResolver.d.ts.map +1 -1
  88. package/dist/generic/PushStatusResolver.js +93 -11
  89. package/dist/generic/PushStatusResolver.js.map +1 -1
  90. package/dist/generic/ResolverBase.d.ts +65 -25
  91. package/dist/generic/ResolverBase.d.ts.map +1 -1
  92. package/dist/generic/ResolverBase.js +201 -101
  93. package/dist/generic/ResolverBase.js.map +1 -1
  94. package/dist/generic/RunViewResolver.d.ts +11 -3
  95. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  96. package/dist/generic/RunViewResolver.js +79 -13
  97. package/dist/generic/RunViewResolver.js.map +1 -1
  98. package/dist/generic/actionOutputFieldSecurity.d.ts +26 -0
  99. package/dist/generic/actionOutputFieldSecurity.d.ts.map +1 -0
  100. package/dist/generic/actionOutputFieldSecurity.js +47 -0
  101. package/dist/generic/actionOutputFieldSecurity.js.map +1 -0
  102. package/dist/generic/fieldSecurityTransport.d.ts +30 -0
  103. package/dist/generic/fieldSecurityTransport.d.ts.map +1 -0
  104. package/dist/generic/fieldSecurityTransport.js +30 -0
  105. package/dist/generic/fieldSecurityTransport.js.map +1 -0
  106. package/dist/index.d.ts +3 -0
  107. package/dist/index.d.ts.map +1 -1
  108. package/dist/index.js +122 -66
  109. package/dist/index.js.map +1 -1
  110. package/dist/integration/CustomColumnPromoter.d.ts.map +1 -1
  111. package/dist/integration/CustomColumnPromoter.js +6 -1
  112. package/dist/integration/CustomColumnPromoter.js.map +1 -1
  113. package/dist/logging/variablesLoggingMiddleware.d.ts +12 -0
  114. package/dist/logging/variablesLoggingMiddleware.d.ts.map +1 -1
  115. package/dist/logging/variablesLoggingMiddleware.js +50 -1
  116. package/dist/logging/variablesLoggingMiddleware.js.map +1 -1
  117. package/dist/realtimeSessions/SessionVerificationPortsBase.d.ts.map +1 -1
  118. package/dist/realtimeSessions/SessionVerificationPortsBase.js +1 -0
  119. package/dist/realtimeSessions/SessionVerificationPortsBase.js.map +1 -1
  120. package/dist/realtimeSessions/ownedRealtimeSession.d.ts +8 -6
  121. package/dist/realtimeSessions/ownedRealtimeSession.d.ts.map +1 -1
  122. package/dist/realtimeSessions/ownedRealtimeSession.js +15 -6
  123. package/dist/realtimeSessions/ownedRealtimeSession.js.map +1 -1
  124. package/dist/realtimeSessions/sessionAccess.d.ts +88 -11
  125. package/dist/realtimeSessions/sessionAccess.d.ts.map +1 -1
  126. package/dist/realtimeSessions/sessionAccess.js +125 -22
  127. package/dist/realtimeSessions/sessionAccess.js.map +1 -1
  128. package/dist/realtimeSessions/sessionConfigStamps.js +1 -1
  129. package/dist/realtimeSessions/sessionConfigStamps.js.map +1 -1
  130. package/dist/realtimeSessions/subscriptionAuthorization.d.ts.map +1 -1
  131. package/dist/realtimeSessions/subscriptionAuthorization.js +5 -2
  132. package/dist/realtimeSessions/subscriptionAuthorization.js.map +1 -1
  133. package/dist/realtimeSessions/verificationWorkflow.d.ts +2 -0
  134. package/dist/realtimeSessions/verificationWorkflow.d.ts.map +1 -1
  135. package/dist/realtimeSessions/verificationWorkflow.js +1 -1
  136. package/dist/realtimeSessions/verificationWorkflow.js.map +1 -1
  137. package/dist/realtimeWidget/WidgetRouter.d.ts.map +1 -1
  138. package/dist/realtimeWidget/WidgetRouter.js +19 -3
  139. package/dist/realtimeWidget/WidgetRouter.js.map +1 -1
  140. package/dist/realtimeWidget/WidgetSessionService.d.ts +67 -7
  141. package/dist/realtimeWidget/WidgetSessionService.d.ts.map +1 -1
  142. package/dist/realtimeWidget/WidgetSessionService.js +201 -19
  143. package/dist/realtimeWidget/WidgetSessionService.js.map +1 -1
  144. package/dist/realtimeWidget/host-identity.d.ts +1 -1
  145. package/dist/realtimeWidget/host-identity.js +1 -1
  146. package/dist/realtimeWidget/widgetCore.d.ts +20 -1
  147. package/dist/realtimeWidget/widgetCore.d.ts.map +1 -1
  148. package/dist/realtimeWidget/widgetCore.js +17 -1
  149. package/dist/realtimeWidget/widgetCore.js.map +1 -1
  150. package/dist/realtimeWidget/widgetGuestElevation.d.ts +1 -1
  151. package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -1
  152. package/dist/realtimeWidget/widgetGuestElevation.js +2 -1
  153. package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
  154. package/dist/resolvers/ActionResolver.d.ts +19 -1
  155. package/dist/resolvers/ActionResolver.d.ts.map +1 -1
  156. package/dist/resolvers/ActionResolver.js +63 -12
  157. package/dist/resolvers/ActionResolver.js.map +1 -1
  158. package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
  159. package/dist/resolvers/AdhocQueryResolver.js +1 -0
  160. package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
  161. package/dist/resolvers/AgentRunStatusPublisher.d.ts +18 -4
  162. package/dist/resolvers/AgentRunStatusPublisher.d.ts.map +1 -1
  163. package/dist/resolvers/AgentRunStatusPublisher.js +32 -8
  164. package/dist/resolvers/AgentRunStatusPublisher.js.map +1 -1
  165. package/dist/resolvers/AgentSessionResolver.d.ts +10 -4
  166. package/dist/resolvers/AgentSessionResolver.d.ts.map +1 -1
  167. package/dist/resolvers/AgentSessionResolver.js +40 -17
  168. package/dist/resolvers/AgentSessionResolver.js.map +1 -1
  169. package/dist/resolvers/EntityCommunicationsResolver.d.ts +13 -1
  170. package/dist/resolvers/EntityCommunicationsResolver.d.ts.map +1 -1
  171. package/dist/resolvers/EntityCommunicationsResolver.js +50 -14
  172. package/dist/resolvers/EntityCommunicationsResolver.js.map +1 -1
  173. package/dist/resolvers/FetchEntityVectorsResolver.d.ts +28 -0
  174. package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
  175. package/dist/resolvers/FetchEntityVectorsResolver.js +117 -21
  176. package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
  177. package/dist/resolvers/FileCategoryResolver.d.ts.map +1 -1
  178. package/dist/resolvers/FileCategoryResolver.js +1 -0
  179. package/dist/resolvers/FileCategoryResolver.js.map +1 -1
  180. package/dist/resolvers/FileResolver.d.ts +5 -0
  181. package/dist/resolvers/FileResolver.d.ts.map +1 -1
  182. package/dist/resolvers/FileResolver.js +13 -4
  183. package/dist/resolvers/FileResolver.js.map +1 -1
  184. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
  185. package/dist/resolvers/IntegrationDiscoveryResolver.js +45 -7
  186. package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
  187. package/dist/resolvers/MergeRecordsResolver.d.ts +8 -1
  188. package/dist/resolvers/MergeRecordsResolver.d.ts.map +1 -1
  189. package/dist/resolvers/MergeRecordsResolver.js +31 -3
  190. package/dist/resolvers/MergeRecordsResolver.js.map +1 -1
  191. package/dist/resolvers/PipelineProgressResolver.d.ts +6 -0
  192. package/dist/resolvers/PipelineProgressResolver.d.ts.map +1 -1
  193. package/dist/resolvers/PipelineProgressResolver.js +5 -3
  194. package/dist/resolvers/PipelineProgressResolver.js.map +1 -1
  195. package/dist/resolvers/QueryResolver.d.ts.map +1 -1
  196. package/dist/resolvers/QueryResolver.js +20 -6
  197. package/dist/resolvers/QueryResolver.js.map +1 -1
  198. package/dist/resolvers/QuerySystemUserResolver.d.ts +2 -0
  199. package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
  200. package/dist/resolvers/QuerySystemUserResolver.js +15 -6
  201. package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
  202. package/dist/resolvers/RSUResolver.d.ts.map +1 -1
  203. package/dist/resolvers/RSUResolver.js +12 -0
  204. package/dist/resolvers/RSUResolver.js.map +1 -1
  205. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +16 -2
  206. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  207. package/dist/resolvers/RealtimeClientSessionResolver.js +82 -28
  208. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  209. package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
  210. package/dist/resolvers/RemoteBrowserActionResolver.js +11 -9
  211. package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
  212. package/dist/resolvers/RunAIAgentResolver.d.ts +56 -8
  213. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  214. package/dist/resolvers/RunAIAgentResolver.js +209 -64
  215. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  216. package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
  217. package/dist/resolvers/RunTemplateResolver.js +4 -4
  218. package/dist/resolvers/RunTemplateResolver.js.map +1 -1
  219. package/dist/resolvers/RunTestResolver.d.ts.map +1 -1
  220. package/dist/resolvers/RunTestResolver.js +3 -1
  221. package/dist/resolvers/RunTestResolver.js.map +1 -1
  222. package/dist/resolvers/SyncDataResolver.d.ts.map +1 -1
  223. package/dist/resolvers/SyncDataResolver.js +4 -0
  224. package/dist/resolvers/SyncDataResolver.js.map +1 -1
  225. package/dist/resolvers/TestQuerySQLResolver.d.ts.map +1 -1
  226. package/dist/resolvers/TestQuerySQLResolver.js +1 -0
  227. package/dist/resolvers/TestQuerySQLResolver.js.map +1 -1
  228. package/dist/resolvers/TransactionGroupResolver.d.ts +17 -2
  229. package/dist/resolvers/TransactionGroupResolver.d.ts.map +1 -1
  230. package/dist/resolvers/TransactionGroupResolver.js +60 -15
  231. package/dist/resolvers/TransactionGroupResolver.js.map +1 -1
  232. package/dist/resolvers/UserViewResolver.d.ts +1 -1
  233. package/dist/resolvers/UserViewResolver.d.ts.map +1 -1
  234. package/dist/resolvers/UserViewResolver.js +2 -1
  235. package/dist/resolvers/UserViewResolver.js.map +1 -1
  236. package/dist/rest/EntityCRUDHandler.d.ts +6 -0
  237. package/dist/rest/EntityCRUDHandler.d.ts.map +1 -1
  238. package/dist/rest/EntityCRUDHandler.js +19 -5
  239. package/dist/rest/EntityCRUDHandler.js.map +1 -1
  240. package/dist/rest/RESTEndpointHandler.d.ts.map +1 -1
  241. package/dist/rest/RESTEndpointHandler.js +8 -5
  242. package/dist/rest/RESTEndpointHandler.js.map +1 -1
  243. package/dist/rest/RealtimeRecordingHandler.d.ts +3 -3
  244. package/dist/rest/RealtimeRecordingHandler.d.ts.map +1 -1
  245. package/dist/rest/RealtimeRecordingHandler.js +7 -7
  246. package/dist/rest/RealtimeRecordingHandler.js.map +1 -1
  247. package/dist/rest/ViewOperationsHandler.d.ts +6 -0
  248. package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
  249. package/dist/rest/ViewOperationsHandler.js +22 -5
  250. package/dist/rest/ViewOperationsHandler.js.map +1 -1
  251. package/dist/serverRoleConfigUnits.d.ts +29 -0
  252. package/dist/serverRoleConfigUnits.d.ts.map +1 -0
  253. package/dist/serverRoleConfigUnits.js +33 -0
  254. package/dist/serverRoleConfigUnits.js.map +1 -0
  255. package/dist/services/TaskGraphContinuationDeliverer.d.ts +5 -0
  256. package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
  257. package/dist/services/TaskGraphContinuationDeliverer.js +42 -15
  258. package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
  259. package/dist/types.d.ts +23 -1
  260. package/dist/types.d.ts.map +1 -1
  261. package/dist/types.js.map +1 -1
  262. package/package.json +98 -98
  263. package/src/__tests__/ActionResolver.fieldSecurity.test.ts +237 -0
  264. package/src/__tests__/AdhocQueryResolver.dialect.test.ts +137 -0
  265. package/src/__tests__/AgentRunStatusPublisher.test.ts +49 -0
  266. package/src/__tests__/AgentSessionResolver.ownership.test.ts +137 -0
  267. package/src/__tests__/CustomColumnPromoterOnePass.test.ts +15 -1
  268. package/src/__tests__/EntityCommunicationsResolver.actingUser.test.ts +151 -0
  269. package/src/__tests__/EvolutionActsOnWhatItDetects.test.ts +113 -0
  270. package/src/__tests__/FetchEntityVectorsResolver.test.ts +206 -3
  271. package/src/__tests__/FileResolver.fieldSecurity.test.ts +82 -0
  272. package/src/__tests__/PipelineProgressResolver.filter.test.ts +58 -0
  273. package/src/__tests__/QuerySystemUserResolver.fieldSecurity.test.ts +49 -0
  274. package/src/__tests__/RESTEndpointHandler.test.ts +25 -0
  275. package/src/__tests__/RealtimeClientSessionResolver.configTamper.test.ts +23 -3
  276. package/src/__tests__/RealtimeClientSessionResolver.test.ts +324 -46
  277. package/src/__tests__/RemoteBrowserActionResolver.ownership.test.ts +98 -0
  278. package/src/__tests__/ResolverBase.clientOldValues.test.ts +37 -4
  279. package/src/__tests__/ResolverBase.deleteReturnProjection.test.ts +52 -0
  280. package/src/__tests__/ResolverBase.entityMessageRelay.test.ts +176 -0
  281. package/src/__tests__/ResolverBase.includeBinaryFields.test.ts +23 -1
  282. package/src/__tests__/ResolverBase.publishStatusUpdate.test.ts +68 -0
  283. package/src/__tests__/ResolverBase.userViewCacheAndDedup.test.ts +130 -0
  284. package/src/__tests__/RunAIAgentResolver.branch.test.ts +68 -13
  285. package/src/__tests__/RunAIAgentResolver.conversationWriter.test.ts +210 -0
  286. package/src/__tests__/RunAIAgentResolver.fireAndForgetCompletion.test.ts +209 -0
  287. package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +17 -27
  288. package/src/__tests__/RunAIAgentResolver.runDataGuard.test.ts +8 -2
  289. package/src/__tests__/RunAIAgentResolver.serverReply.test.ts +358 -0
  290. package/src/__tests__/RunAIAgentResolver.stoppedPredecessor.test.ts +4 -1
  291. package/src/__tests__/RunTemplateResolver.test.ts +35 -2
  292. package/src/__tests__/RunViewResolver.cacheCheckForwarding.test.ts +11 -0
  293. package/src/__tests__/SessionManager.test.ts +95 -0
  294. package/src/__tests__/TaskGraphContinuationDeliverer.test.ts +114 -3
  295. package/src/__tests__/TransactionGroupResolver.fieldSecurity.test.ts +199 -0
  296. package/src/__tests__/UserViewResolver.updateWhereClause.test.ts +15 -0
  297. package/src/__tests__/ViewOperationsHandler.test.ts +7 -2
  298. package/src/__tests__/WidgetSessionService.test.ts +583 -4
  299. package/src/__tests__/apiRootBoot.test.ts +1 -1
  300. package/src/__tests__/buildMagicLinkSessionUser.test.ts +96 -0
  301. package/src/__tests__/cacheBroadcastProviders.test.ts +403 -0
  302. package/src/__tests__/cacheInvalidationRecordData.test.ts +45 -0
  303. package/src/__tests__/fieldSecurityFixtures.ts +91 -0
  304. package/src/__tests__/globalCors.test.ts +3 -3
  305. package/src/__tests__/localDevAuth.test.ts +196 -0
  306. package/src/__tests__/orphanedConversationDetailReconciler.test.ts +10 -0
  307. package/src/__tests__/pushStatusResolver.filter.test.ts +57 -1
  308. package/src/__tests__/realtimeRecordingHandler.test.ts +19 -2
  309. package/src/__tests__/realtimeSessions/RealtimeSessionEventsResolver.test.ts +29 -1
  310. package/src/__tests__/realtimeSessions/ownedRealtimeSession.anonymous.test.ts +31 -0
  311. package/src/__tests__/realtimeSessions/ownedRealtimeSession.namedScoped.test.ts +37 -0
  312. package/src/__tests__/realtimeSessions/sessionAccess.test.ts +146 -7
  313. package/src/__tests__/realtimeSessions/sessionConfigStamps.test.ts +1 -1
  314. package/src/__tests__/realtimeSessions/verificationWorkflow.test.ts +25 -4
  315. package/src/__tests__/resolverBase.cloudEventChangedFields.test.ts +157 -0
  316. package/src/__tests__/resolverBase.fieldSecurityTransport.test.ts +222 -0
  317. package/src/__tests__/resolverBase.fls.test.ts +146 -97
  318. package/src/__tests__/restTransport.fieldSecurity.test.ts +148 -0
  319. package/src/__tests__/runParamsTimeZone.test.ts +179 -0
  320. package/src/__tests__/runStreams.fieldSecurity.test.ts +100 -0
  321. package/src/__tests__/runViewProperties.test.ts +114 -0
  322. package/src/__tests__/scalabilityBenchmarks.test.ts +765 -0
  323. package/src/__tests__/scalabilityDatabaseIntegration.test.ts +207 -0
  324. package/src/__tests__/sessionUserClone.test.ts +5 -1
  325. package/src/__tests__/startupSweepOrder.test.ts +40 -0
  326. package/src/__tests__/unifiedAuth.test.ts +28 -0
  327. package/src/__tests__/widgetGuestElevation.test.ts +2 -0
  328. package/src/agentSessions/SessionJanitor.ts +3 -3
  329. package/src/agentSessions/SessionManager.ts +35 -12
  330. package/src/auth/AuthProviderCatalogRouter.ts +3 -0
  331. package/src/auth/index.ts +56 -12
  332. package/src/auth/initializeProviders.ts +43 -1
  333. package/src/auth/localDevAuth.ts +528 -0
  334. package/src/auth/magicLink/MagicLinkKeys.ts +2 -2
  335. package/src/auth/magicLink/types.ts +10 -0
  336. package/src/auth/sessionUserClone.ts +3 -2
  337. package/src/broadcast/BaseCacheInvalidationBroadcastProvider.ts +674 -0
  338. package/src/broadcast/index.ts +5 -0
  339. package/src/broadcast/providers/AWSIoTCoreBroadcastProvider.ts +180 -0
  340. package/src/broadcast/providers/AzureWebPubSubBroadcastProvider.ts +182 -0
  341. package/src/broadcast/providers/GCPPubSubBroadcastProvider.ts +169 -0
  342. package/src/broadcast/providers/SequencedRoleBucketBroadcastProvider.ts +57 -0
  343. package/src/cache.ts +2 -1
  344. package/src/config.ts +69 -0
  345. package/src/context.ts +32 -8
  346. package/src/coreRoutes.ts +15 -0
  347. package/src/generated/graphql-schemas/__mj.ts +2239 -102
  348. package/src/generic/CacheInvalidationResolver.ts +135 -78
  349. package/src/generic/OrphanedConversationDetailReconciler.ts +4 -2
  350. package/src/generic/PubSubManager.ts +54 -1
  351. package/src/generic/PushStatusResolver.ts +108 -11
  352. package/src/generic/ResolverBase.ts +237 -107
  353. package/src/generic/RunViewResolver.ts +80 -15
  354. package/src/generic/actionOutputFieldSecurity.ts +75 -0
  355. package/src/generic/fieldSecurityTransport.ts +62 -0
  356. package/src/index.ts +127 -68
  357. package/src/integration/CustomColumnPromoter.ts +6 -1
  358. package/src/logging/variablesLoggingMiddleware.ts +59 -3
  359. package/src/realtimeSessions/README.md +7 -3
  360. package/src/realtimeSessions/SessionVerificationPortsBase.ts +1 -0
  361. package/src/realtimeSessions/ownedRealtimeSession.ts +20 -6
  362. package/src/realtimeSessions/sessionAccess.ts +181 -25
  363. package/src/realtimeSessions/sessionConfigStamps.ts +1 -1
  364. package/src/realtimeSessions/subscriptionAuthorization.ts +4 -2
  365. package/src/realtimeSessions/verificationWorkflow.ts +3 -1
  366. package/src/realtimeWidget/WidgetRouter.ts +22 -4
  367. package/src/realtimeWidget/WidgetSessionService.ts +252 -17
  368. package/src/realtimeWidget/host-identity.ts +1 -1
  369. package/src/realtimeWidget/widgetCore.ts +30 -1
  370. package/src/realtimeWidget/widgetGuestElevation.ts +2 -1
  371. package/src/resolvers/ActionResolver.ts +78 -15
  372. package/src/resolvers/AdhocQueryResolver.ts +1 -0
  373. package/src/resolvers/AgentRunStatusPublisher.ts +36 -8
  374. package/src/resolvers/AgentSessionResolver.ts +46 -18
  375. package/src/resolvers/EntityCommunicationsResolver.ts +56 -15
  376. package/src/resolvers/FetchEntityVectorsResolver.ts +132 -21
  377. package/src/resolvers/FileCategoryResolver.ts +1 -0
  378. package/src/resolvers/FileResolver.ts +14 -4
  379. package/src/resolvers/IntegrationDiscoveryResolver.ts +49 -7
  380. package/src/resolvers/MergeRecordsResolver.ts +23 -2
  381. package/src/resolvers/PipelineProgressResolver.ts +12 -3
  382. package/src/resolvers/QueryResolver.ts +20 -6
  383. package/src/resolvers/QuerySystemUserResolver.ts +15 -6
  384. package/src/resolvers/RSUResolver.ts +12 -0
  385. package/src/resolvers/RealtimeClientSessionResolver.ts +104 -33
  386. package/src/resolvers/RemoteBrowserActionResolver.ts +15 -10
  387. package/src/resolvers/RunAIAgentResolver.ts +258 -82
  388. package/src/resolvers/RunTemplateResolver.ts +6 -4
  389. package/src/resolvers/RunTestResolver.ts +3 -1
  390. package/src/resolvers/SyncDataResolver.ts +4 -0
  391. package/src/resolvers/TestQuerySQLResolver.ts +1 -0
  392. package/src/resolvers/TransactionGroupResolver.ts +66 -15
  393. package/src/resolvers/UserViewResolver.ts +2 -1
  394. package/src/resolvers/__tests__/MergeRecordsResolver.test.ts +43 -0
  395. package/src/rest/EntityCRUDHandler.ts +22 -5
  396. package/src/rest/RESTEndpointHandler.ts +8 -5
  397. package/src/rest/RealtimeRecordingHandler.ts +13 -10
  398. package/src/rest/ViewOperationsHandler.ts +24 -6
  399. package/src/serverRoleConfigUnits.ts +48 -0
  400. package/src/services/TaskGraphContinuationDeliverer.ts +52 -20
  401. package/src/types.ts +23 -1
@@ -1,10 +1,12 @@
1
1
  /**
2
2
  * @fileoverview The conversation branch on `RunAIAgentFromConversationDetail`.
3
3
  *
4
- * The branch comes from the placeholder row the mutation is given, never from a request argument.
5
- * These tests pin that the mutation loads history along that branch's path, hands the branch to the
6
- * agent run on both the synchronous and fire-and-forget paths, and reads the trunk when the row has
7
- * no branch; and that `executeAIAgent` puts the branch on the run's params only when there is one.
4
+ * The branch defaults to the person's message the mutation is given, or — when `replyBranchId` is supplied
5
+ * (such as Regenerate forking from an earlier message) — is validated to be a branch of the conversation
6
+ * whose path holds the person's message. These tests pin that the mutation loads history along that branch's
7
+ * path, creates the reply on that branch, hands the branch to the agent run on both the synchronous and
8
+ * fire-and-forget paths, and reads the trunk when the message has no branch; and that `executeAIAgent` puts
9
+ * the branch on the run's params only when there is one.
8
10
  */
9
11
  import 'reflect-metadata'; // must precede any type-graphql decorator evaluation
10
12
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
@@ -26,6 +28,7 @@ vi.mock('type-graphql', () => {
26
28
 
27
29
  const hoisted = vi.hoisted(() => ({
28
30
  runAgentInConversation: vi.fn(),
31
+ createAgentReplyDetail: vi.fn(),
29
32
  }));
30
33
 
31
34
  vi.mock('@memberjunction/ai-agents', () => ({
@@ -33,6 +36,9 @@ vi.mock('@memberjunction/ai-agents', () => ({
33
36
  RunAgentInConversation(...args: unknown[]) {
34
37
  return hoisted.runAgentInConversation(...args);
35
38
  }
39
+ CreateAgentReplyDetail(...args: unknown[]) {
40
+ return hoisted.createAgentReplyDetail(...args);
41
+ }
36
42
  },
37
43
  ArtifactToolManager: class {},
38
44
  }));
@@ -70,11 +76,12 @@ function makeResolver(): { resolver: RunAIAgentResolver; seams: ResolverSeams }
70
76
  return { resolver, seams };
71
77
  }
72
78
 
73
- function makeContext(branchId: string | null): AppContext {
79
+ function makeContext(branchId: string | null, sequence = 3): AppContext {
74
80
  const provider = {
75
81
  GetEntityObject: vi.fn().mockResolvedValue({
76
82
  ConversationID: 'conv-1',
77
83
  BranchID: branchId,
84
+ Sequence: sequence,
78
85
  Load: vi.fn().mockResolvedValue(true),
79
86
  }),
80
87
  };
@@ -87,11 +94,18 @@ function makeContext(branchId: string | null): AppContext {
87
94
 
88
95
  const pubSub = { publish: vi.fn() } as unknown as PubSubEngine;
89
96
 
90
- async function runMutation(resolver: RunAIAgentResolver, branchId: string | null, fireAndForget?: boolean) {
97
+ async function runMutation(
98
+ resolver: RunAIAgentResolver,
99
+ branchId: string | null,
100
+ fireAndForget?: boolean,
101
+ replyBranchId?: string,
102
+ sequence = 3
103
+ ) {
91
104
  return resolver.RunAIAgentFromConversationDetail(
92
- 'detail-1', 'agent-1', makeContext(branchId), 'session-1', pubSub,
105
+ 'detail-1', 'agent-1', makeContext(branchId, sequence), 'session-1', pubSub,
93
106
  undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
94
- undefined, undefined, fireAndForget, undefined, undefined, undefined
107
+ undefined, undefined, fireAndForget, undefined, undefined, undefined,
108
+ undefined, undefined, replyBranchId
95
109
  );
96
110
  }
97
111
 
@@ -108,23 +122,64 @@ describe('RunAIAgentFromConversationDetail — branch', () => {
108
122
  seams.loadConversationHistoryWithAttachments = vi.fn().mockResolvedValue([]);
109
123
  seams.executeAIAgent = vi.fn().mockResolvedValue({ success: true, result: '{}' });
110
124
  seams.executeAgentInBackground = vi.fn();
125
+ hoisted.createAgentReplyDetail.mockReset().mockResolvedValue({ ID: 'reply-1', __mj_CreatedAt: new Date() });
111
126
  });
112
127
 
113
- it('loads history on the placeholder row\'s branch and hands the branch to the run', async () => {
128
+ // executeAIAgent / executeAgentInBackground end with (..., branchId, replyDetail).
129
+ it('loads history on the message\'s branch, creates the reply there, and hands the branch to the run', async () => {
114
130
  await runMutation(resolver, 'branch-b');
115
131
  expect(seams.loadConversationHistoryWithAttachments.mock.calls[0][6]).toBe('branch-b');
116
- expect(seams.executeAIAgent.mock.calls[0].at(-1)).toBe('branch-b');
132
+ expect(hoisted.createAgentReplyDetail).toHaveBeenCalledWith(expect.objectContaining({ branchId: 'branch-b' }));
133
+ expect(seams.executeAIAgent.mock.calls[0].at(-2)).toBe('branch-b');
117
134
  });
118
135
 
119
136
  it('passes the branch to the background path too', async () => {
120
137
  await runMutation(resolver, 'branch-b', true);
121
- expect(seams.executeAgentInBackground.mock.calls[0].at(-1)).toBe('branch-b');
138
+ expect(seams.executeAgentInBackground.mock.calls[0].at(-2)).toBe('branch-b');
122
139
  });
123
140
 
124
- it('reads the trunk when the placeholder has no branch (control)', async () => {
141
+ it('reads the trunk when the message has no branch (control)', async () => {
125
142
  await runMutation(resolver, null);
126
143
  expect(seams.loadConversationHistoryWithAttachments.mock.calls[0][6]).toBeNull();
127
- expect(seams.executeAIAgent.mock.calls[0].at(-1)).toBeNull();
144
+ expect(hoisted.createAgentReplyDetail).toHaveBeenCalledWith(expect.objectContaining({ branchId: null }));
145
+ expect(seams.executeAIAgent.mock.calls[0].at(-2)).toBeNull();
146
+ });
147
+
148
+ it('places the reply and history on replyBranchId when a regeneration forks from a message on its ancestor path', async () => {
149
+ vi.spyOn(ConversationEngine, 'LoadBranchesFresh').mockResolvedValue([
150
+ {
151
+ ID: 'branch-regen',
152
+ ConversationID: 'conv-1',
153
+ ParentBranchID: null,
154
+ ForkFromSequence: 5,
155
+ } as never,
156
+ ]);
157
+
158
+ const result = await runMutation(resolver, null, false, 'branch-regen', 3);
159
+
160
+ expect(result.success).toBe(true);
161
+ expect(seams.loadConversationHistoryWithAttachments.mock.calls[0][6]).toBe('branch-regen');
162
+ expect(hoisted.createAgentReplyDetail).toHaveBeenCalledWith(expect.objectContaining({ branchId: 'branch-regen' }));
163
+ expect(seams.executeAIAgent.mock.calls[0].at(-2)).toBe('branch-regen');
164
+ });
165
+
166
+ it('refuses a replyBranchId whose path does not include the user message, and creates nothing', async () => {
167
+ vi.spyOn(console, 'error').mockImplementation(() => undefined);
168
+ vi.spyOn(ConversationEngine, 'LoadBranchesFresh').mockResolvedValue([
169
+ {
170
+ ID: 'branch-regen',
171
+ ConversationID: 'conv-1',
172
+ ParentBranchID: null,
173
+ ForkFromSequence: 5,
174
+ } as never,
175
+ ]);
176
+
177
+ const result = await runMutation(resolver, null, false, 'branch-regen', 8);
178
+
179
+ expect(result.success).toBe(false);
180
+ expect(result.errorMessage).toContain('branch-regen');
181
+ expect(hoisted.createAgentReplyDetail).not.toHaveBeenCalled();
182
+ expect(seams.executeAIAgent).not.toHaveBeenCalled();
128
183
  });
129
184
  });
130
185
 
@@ -0,0 +1,210 @@
1
+ /**
2
+ * @fileoverview A widget guest's agent run executes as the system user, but the conversation belongs to the guest,
3
+ * and only the owner (or a grantee) may write a conversation's messages (MJConversationDetailEntityExtended, MJ#4791).
4
+ * So the resolver names the guest as `ConversationWriter`: AgentRunner then writes the reply as the guest and the
5
+ * run's own records as the system user. Before this, every reply save of a widget guest was refused and the widget
6
+ * never showed the agent's answer.
7
+ */
8
+ import 'reflect-metadata'; // must precede any type-graphql decorator evaluation
9
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
10
+ import type { PubSubEngine } from 'type-graphql';
11
+
12
+ // type-graphql's real decorators need emitDecoratorMetadata, which vitest's esbuild transform
13
+ // does not emit; the schema is irrelevant here, so every decorator is a no-op.
14
+ vi.mock('type-graphql', () => {
15
+ const decoratorFactory = (..._args: unknown[]) => (..._decorated: unknown[]) => undefined;
16
+ const exportNames = [
17
+ 'Resolver', 'Query', 'Mutation', 'Subscription', 'Arg', 'Args', 'ArgsType', 'Ctx', 'Root',
18
+ 'Info', 'Field', 'FieldResolver', 'ObjectType', 'InputType', 'InterfaceType', 'Authorized',
19
+ 'UseMiddleware', 'Extensions', 'Directive', 'ID', 'Int', 'Float', 'GraphQLISODateTime',
20
+ 'GraphQLTimestamp', 'registerEnumType', 'createMethodDecorator', 'createParamDecorator',
21
+ 'buildSchema', 'buildSchemaSync', 'PubSub',
22
+ ];
23
+ return Object.fromEntries(exportNames.map((name) => [name, decoratorFactory]));
24
+ });
25
+
26
+ const hoisted = vi.hoisted(() => ({
27
+ runAgentInConversation: vi.fn(),
28
+ createAgentReplyDetail: vi.fn(),
29
+ resolveWidgetGuestRunContext: vi.fn(),
30
+ authorize: vi.fn(),
31
+ }));
32
+
33
+ // Only the key engine is stubbed: Authorize allows `agent:trusted` for the key whose hash is 'hash-trusted'.
34
+ vi.mock('@memberjunction/api-keys', async (importOriginal) => ({
35
+ ...(await importOriginal<typeof import('@memberjunction/api-keys')>()),
36
+ GetAPIKeyEngine: () => ({ Authorize: hoisted.authorize }),
37
+ }));
38
+
39
+ vi.mock('@memberjunction/ai-agents', () => ({
40
+ AgentRunner: class {
41
+ RunAgentInConversation(...args: unknown[]) {
42
+ return hoisted.runAgentInConversation(...args);
43
+ }
44
+ CreateAgentReplyDetail(...args: unknown[]) {
45
+ return hoisted.createAgentReplyDetail(...args);
46
+ }
47
+ },
48
+ ArtifactToolManager: class {},
49
+ }));
50
+
51
+ // Only the widget lookup is stubbed; ElevateUserPayload is the real one, so the widget-guest test runs
52
+ // against the elevated system-user payload the resolver really builds.
53
+ vi.mock('../realtimeWidget/widgetGuestElevation.js', async (importOriginal) => ({
54
+ ...(await importOriginal<typeof import('../realtimeWidget/widgetGuestElevation.js')>()),
55
+ ResolveWidgetGuestRunContext: hoisted.resolveWidgetGuestRunContext,
56
+ }));
57
+
58
+ import { UserCache } from '@memberjunction/generic-database-provider';
59
+ import type { UserInfo } from '@memberjunction/core';
60
+ import { RunAIAgentResolver } from '../resolvers/RunAIAgentResolver.js';
61
+ import type { AppContext, UserPayload } from '../types.js';
62
+
63
+ const SCOPE_RECORD_ID = 'B1A2C3D4-0000-4000-8000-00000000A115';
64
+ const ORDINARY = { conversationId: 'conv-1', latestMessageId: 'detail-1', invocationReason: 'user-message' };
65
+ const CLIENT_DATA = JSON.stringify({
66
+ ...ORDINARY,
67
+ PrimaryScopeRecordID: SCOPE_RECORD_ID,
68
+ PrimaryScopeEntityName: 'MJ: Companies',
69
+ SecondaryScopes: { TeamID: 'team-other' },
70
+ __agentTypePromptParams: { enableTaskGraphs: true },
71
+ });
72
+
73
+ const USER = { ID: 'user-1', Email: 'browser.user@example.com' };
74
+ const SYSTEM_USER = { ID: 'system-user', Email: 'system@example.com' };
75
+ // A widget guest always carries its per-visitor scope id (mj_sid) as its MagicLinkScope.ResourceID, and its
76
+ // conversation is stamped with the same value as ExternalID: that stamp is what tells one guest from another
77
+ // under the shared Anonymous user (MJ#6032), so the fake conversation below carries it.
78
+ const GUEST = { ID: 'guest-1', Email: 'anonymous@magic-link.local', IsMagicLinkAnonymous: true, AnonymousVisitorID: 'sid-1', MagicLinkScope: { ResourceID: 'sid-1' } };
79
+
80
+ type Fn = ReturnType<typeof vi.fn>;
81
+
82
+ /** The private members this test reaches, through an explicit shape (no `any`). */
83
+ interface ResolverSeams {
84
+ CheckAPIKeyScopeAuthorization: Fn;
85
+ GetUserFromPayload: Fn;
86
+ loadConversationHistoryWithAttachments: Fn;
87
+ validateAgent: Fn;
88
+ persistInFlightAgentFailure: Fn;
89
+ }
90
+
91
+ const pubSub = { publish: vi.fn().mockResolvedValue(undefined) } as unknown as PubSubEngine;
92
+
93
+ function makeResolver(): { resolver: RunAIAgentResolver; seams: ResolverSeams } {
94
+ const resolver = new RunAIAgentResolver();
95
+ const seams = resolver as unknown as ResolverSeams;
96
+ seams.CheckAPIKeyScopeAuthorization = vi.fn().mockResolvedValue(undefined);
97
+ seams.GetUserFromPayload = vi.fn((payload: UserPayload) => payload.userRecord);
98
+ seams.loadConversationHistoryWithAttachments = vi.fn().mockResolvedValue([{ role: 'user', content: 'hi' }]);
99
+ seams.validateAgent = vi.fn().mockResolvedValue({ ID: 'agent-1', Name: 'Test Agent' });
100
+ seams.persistInFlightAgentFailure = vi.fn().mockResolvedValue(undefined);
101
+ return { resolver, seams };
102
+ }
103
+
104
+ function makeContext(userPayload: Partial<UserPayload>): AppContext {
105
+ const provider = {
106
+ GetEntityObject: vi.fn().mockResolvedValue({ ConversationID: 'conv-1', UserID: GUEST.ID, ExternalID: 'sid-1', Load: vi.fn().mockResolvedValue(true) }),
107
+ };
108
+ return {
109
+ userPayload: { sessionId: 'session-1', email: USER.Email, userRecord: USER, ...userPayload } as UserPayload,
110
+ providers: [{ type: 'Read-Write', provider }],
111
+ dataSource: {},
112
+ } as unknown as AppContext;
113
+ }
114
+
115
+ /** The params the n-th run was handed by the real executeAIAgent (what AgentRunner.RunAgentInConversation received). */
116
+ interface HandedParams {
117
+ data: Record<string, unknown>;
118
+ TrustReservedRunData?: boolean;
119
+ contextUser: { ID: string };
120
+ }
121
+
122
+ /** Waits for the n-th run to start (the fire-and-forget path starts it after the resolver has returned). */
123
+ async function handedParams(n = 0): Promise<HandedParams> {
124
+ await vi.waitFor(() => expect(hoisted.runAgentInConversation.mock.calls.length).toBeGreaterThan(n));
125
+ return hoisted.runAgentInConversation.mock.calls[n][0] as HandedParams;
126
+ }
127
+
128
+ function runAIAgent(resolver: RunAIAgentResolver, ctx: AppContext, fireAndForget?: boolean) {
129
+ return resolver.RunAIAgent(
130
+ 'agent-1', ctx, '[{"role":"user","content":"hi"}]', 'session-1', pubSub, CLIENT_DATA,
131
+ undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
132
+ fireAndForget
133
+ );
134
+ }
135
+
136
+ function runFromDetail(resolver: RunAIAgentResolver, ctx: AppContext, fireAndForget?: boolean) {
137
+ return resolver.RunAIAgentFromConversationDetail(
138
+ 'detail-1', 'agent-1', ctx, 'session-1', pubSub, undefined, CLIENT_DATA,
139
+ undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, fireAndForget
140
+ );
141
+ }
142
+
143
+ beforeEach(() => {
144
+ hoisted.resolveWidgetGuestRunContext.mockReset().mockResolvedValue(null);
145
+ // Stop right after the run's params are captured — the rest of the run is out of scope.
146
+ hoisted.runAgentInConversation.mockReset().mockRejectedValue(new Error('stop here'));
147
+ hoisted.createAgentReplyDetail.mockReset().mockResolvedValue({ ID: 'reply-1', __mj_CreatedAt: new Date() });
148
+ hoisted.authorize.mockReset().mockImplementation(async (apiKeyHash: string, _app: string, scopePath: string) =>
149
+ apiKeyHash === 'hash-trusted' && scopePath === 'agent:trusted'
150
+ ? { Allowed: true, MatchedKeyRule: { ScopePath: scopePath }, EvaluatedRules: [] }
151
+ : { Allowed: false, Reason: 'No matching key scope rules', EvaluatedRules: [] });
152
+ vi.spyOn(UserCache.Instance, 'GetSystemUser').mockReturnValue(SYSTEM_USER as UserInfo);
153
+ vi.spyOn(console, 'log').mockImplementation(() => undefined);
154
+ vi.spyOn(console, 'error').mockImplementation(() => undefined);
155
+ });
156
+
157
+ afterEach(() => {
158
+ vi.restoreAllMocks();
159
+ });
160
+
161
+ /** The options the n-th run was handed (the second argument of AgentRunner.RunAgentInConversation). */
162
+ async function handedOptions(n = 0): Promise<{ ConversationWriter?: { ID: string } }> {
163
+ await vi.waitFor(() => expect(hoisted.runAgentInConversation.mock.calls.length).toBeGreaterThan(n));
164
+ return hoisted.runAgentInConversation.mock.calls[n][1] as { ConversationWriter?: { ID: string } };
165
+ }
166
+
167
+ describe('RunAIAgentFromConversationDetail — who writes the conversation', () => {
168
+ it('runs a widget guest as the system user but writes its conversation as the guest, on both paths', async () => {
169
+ const { resolver } = makeResolver();
170
+ hoisted.resolveWidgetGuestRunContext.mockResolvedValue({ ElevatedUser: SYSTEM_USER, Widget: {}, PinnedAgentId: 'pinned-agent' });
171
+ const ctx = makeContext({ email: GUEST.Email, userRecord: GUEST });
172
+
173
+ await runFromDetail(resolver, ctx);
174
+ await runFromDetail(resolver, ctx, true);
175
+
176
+ expect((await handedParams(0)).contextUser.ID).toBe(SYSTEM_USER.ID);
177
+ expect((await handedOptions(0)).ConversationWriter?.ID).toBe(GUEST.ID);
178
+ expect((await handedParams(1)).contextUser.ID).toBe(SYSTEM_USER.ID);
179
+ expect((await handedOptions(1)).ConversationWriter?.ID).toBe(GUEST.ID);
180
+ // The server-created reply row (A19) is written as the guest too, never as the elevated system user.
181
+ for (const [args] of hoisted.createAgentReplyDetail.mock.calls) {
182
+ expect((args as { contextUser: { ID: string } }).contextUser.ID).toBe(GUEST.ID);
183
+ }
184
+ expect(hoisted.createAgentReplyDetail).toHaveBeenCalledTimes(2);
185
+ });
186
+
187
+ it('refuses a widget guest on another visitor\'s conversation before any row is written (MJ#6032)', async () => {
188
+ const { resolver } = makeResolver();
189
+ hoisted.resolveWidgetGuestRunContext.mockResolvedValue({ ElevatedUser: SYSTEM_USER, Widget: {}, PinnedAgentId: 'pinned-agent' });
190
+ // The conversation is stamped sid-1; this guest's scope is sid-2, so the shared Anonymous user must not be
191
+ // enough. Without the AuthorizeConversationTurn gate the run would proceed and the reply row below be created.
192
+ const ctx = makeContext({ email: GUEST.Email, userRecord: { ...GUEST, AnonymousVisitorID: 'sid-2', MagicLinkScope: { ResourceID: 'sid-2' } } as UserPayload['userRecord'] });
193
+
194
+ const result = await runFromDetail(resolver, ctx);
195
+
196
+ expect(result.success).toBe(false);
197
+ expect(result.errorMessage).toContain('Not authorized: you do not own this conversation');
198
+ expect(hoisted.createAgentReplyDetail).not.toHaveBeenCalled();
199
+ expect(hoisted.runAgentInConversation).not.toHaveBeenCalled();
200
+ });
201
+
202
+ it('names no separate writer for an ordinary user (it runs and writes as itself)', async () => {
203
+ const { resolver } = makeResolver();
204
+
205
+ await runFromDetail(resolver, makeContext({}));
206
+
207
+ expect((await handedOptions()).ConversationWriter).toBeUndefined();
208
+ expect((await handedParams()).contextUser.ID).toBe(USER.ID);
209
+ });
210
+ });
@@ -0,0 +1,209 @@
1
+ /**
2
+ * @fileoverview A fire-and-forget run always ends with one completion on the caller's socket.
3
+ *
4
+ * The widget and Explorer return as soon as the mutation acks, then wait for a `complete` status message. When the
5
+ * run fails (the agent cannot be validated, the run throws, or the agent's reply row cannot be saved), that message
6
+ * must still arrive, exactly once, with the reason, addressed to the owner the caller's socket is authenticated as.
7
+ * For a widget guest that is the guest, not the system user its run is elevated to. Without it the client waits for
8
+ * its idle timeout and then reconciles the run as a success.
9
+ *
10
+ * The liveness pulse of the same run is addressed to the same owner.
11
+ */
12
+ import 'reflect-metadata'; // must precede any type-graphql decorator evaluation
13
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
14
+ import type { PubSubEngine } from 'type-graphql';
15
+
16
+ vi.mock('type-graphql', () => {
17
+ const decoratorFactory = (..._args: unknown[]) => (..._decorated: unknown[]) => undefined;
18
+ const exportNames = [
19
+ 'Resolver', 'Query', 'Mutation', 'Subscription', 'Arg', 'Args', 'ArgsType', 'Ctx', 'Root',
20
+ 'Info', 'Field', 'FieldResolver', 'ObjectType', 'InputType', 'InterfaceType', 'Authorized',
21
+ 'UseMiddleware', 'Extensions', 'Directive', 'ID', 'Int', 'Float', 'GraphQLISODateTime',
22
+ 'GraphQLTimestamp', 'registerEnumType', 'createMethodDecorator', 'createParamDecorator',
23
+ 'buildSchema', 'buildSchemaSync', 'PubSub',
24
+ ];
25
+ return Object.fromEntries(exportNames.map((name) => [name, decoratorFactory]));
26
+ });
27
+
28
+ const hoisted = vi.hoisted(() => ({
29
+ runAgentInConversation: vi.fn(),
30
+ agents: new Map<string, { ID: string; Name: string; Status: string }>(),
31
+ }));
32
+
33
+ vi.mock('@memberjunction/aiengine', () => ({
34
+ AIEngine: {
35
+ Instance: {
36
+ Config: async () => undefined,
37
+ GetAgentByID: (id: string) => hoisted.agents.get(id),
38
+ CanUserRunAgent: async () => true,
39
+ },
40
+ },
41
+ GetAttachmentService: () => ({}),
42
+ }));
43
+
44
+ vi.mock('@memberjunction/ai-agents', () => ({
45
+ AgentRunner: class {
46
+ RunAgentInConversation(...args: unknown[]) {
47
+ return hoisted.runAgentInConversation(...args);
48
+ }
49
+ },
50
+ ArtifactToolManager: class {},
51
+ }));
52
+
53
+ import { UserInfo } from '@memberjunction/core';
54
+ import { RunAIAgentResolver } from '../resolvers/RunAIAgentResolver.js';
55
+ import { StatusUpdateOwnerID } from '../generic/PushStatusResolver.js';
56
+ import { DEFAULT_PULSE_INTERVAL_MS, HEARTBEAT_MESSAGE_TYPE } from '../generic/FireAndForgetHeartbeat.js';
57
+ import type { AppContext, UserPayload } from '../types.js';
58
+
59
+ const ANONYMOUS_USER_ID = 'AAAAAAAA-0000-4000-8000-0000000000A0';
60
+ const SYSTEM_USER_ID = 'BBBBBBBB-0000-4000-8000-0000000000B0';
61
+ const SESSION = 'session-1';
62
+
63
+ /** A public-widget guest: the shared Anonymous user, told apart by its scope identity. */
64
+ function widgetGuest(): UserInfo {
65
+ const guest = new UserInfo(null, { ID: ANONYMOUS_USER_ID, Email: 'anonymous@magic-link.local' });
66
+ guest.IsMagicLinkAnonymous = true;
67
+ guest.AnonymousVisitorID = 'sid-guest-1';
68
+ return guest;
69
+ }
70
+
71
+ /** The payload a widget guest's run executes under: elevated to the system user, standing in for the guest. */
72
+ function elevatedGuestPayload(): UserPayload {
73
+ return {
74
+ sessionId: SESSION,
75
+ email: 'system@example.com',
76
+ userRecord: new UserInfo(null, { ID: SYSTEM_USER_ID, Email: 'system@example.com' }),
77
+ ElevatedFrom: widgetGuest(),
78
+ };
79
+ }
80
+
81
+ interface PublishedEnvelope {
82
+ sessionId: string;
83
+ ownerUserId: string;
84
+ message: { resolver: string; type: string; data: Record<string, unknown> };
85
+ }
86
+
87
+ function makePubSub(): { pubSub: PubSubEngine; published: () => PublishedEnvelope[] } {
88
+ const publish = vi.fn().mockResolvedValue(undefined);
89
+ const pubSub: PubSubEngine = { publish, subscribe: vi.fn(), unsubscribe: vi.fn(), asyncIterator: vi.fn() };
90
+ const published = (): PublishedEnvelope[] =>
91
+ publish.mock.calls.map((call) => {
92
+ const payload = call[1] as { sessionId: string; ownerUserId: string; message: string };
93
+ return { sessionId: payload.sessionId, ownerUserId: payload.ownerUserId, message: JSON.parse(payload.message) };
94
+ });
95
+ return { pubSub, published };
96
+ }
97
+
98
+ function completions(envelopes: PublishedEnvelope[]): PublishedEnvelope[] {
99
+ return envelopes.filter((e) => e.message.type === 'StreamingContent' && e.message.data.type === 'complete');
100
+ }
101
+
102
+ /** Starts the public RunAIAgent mutation in fire-and-forget mode for `agentId`, answering into `reply-1`. */
103
+ async function startFireAndForget(pubSub: PubSubEngine, userPayload: UserPayload, agentId = 'agent-1') {
104
+ const ctx = { userPayload, providers: [], dataSource: {} } as AppContext;
105
+ return new RunAIAgentResolver().RunAIAgent(
106
+ agentId, ctx, '[{"role":"user","content":"hi"}]', SESSION, pubSub,
107
+ undefined, undefined, undefined, undefined, undefined, undefined,
108
+ 'reply-1', // conversationDetailId
109
+ undefined, undefined, undefined, undefined,
110
+ true, // fireAndForget
111
+ );
112
+ }
113
+
114
+ /** Lets the background run settle, then gives any late (double) publish a chance to land. */
115
+ async function settle(published: () => PublishedEnvelope[]): Promise<void> {
116
+ await vi.waitFor(() => expect(completions(published()).length).toBeGreaterThan(0), { timeout: 1000 });
117
+ await new Promise((resolve) => setTimeout(resolve, 20));
118
+ }
119
+
120
+ beforeEach(() => {
121
+ hoisted.agents.clear();
122
+ hoisted.agents.set('agent-1', { ID: 'agent-1', Name: 'Research', Status: 'Active' });
123
+ hoisted.runAgentInConversation.mockReset();
124
+ vi.spyOn(console, 'log').mockImplementation(() => undefined);
125
+ vi.spyOn(console, 'error').mockImplementation(() => undefined);
126
+ });
127
+
128
+ afterEach(() => {
129
+ vi.useRealTimers();
130
+ vi.restoreAllMocks();
131
+ });
132
+
133
+ describe('RunAIAgent fire-and-forget — the failure completion', () => {
134
+ it("publishes one failed completion with the reason when the agent's reply cannot be saved, to the guest's socket", async () => {
135
+ const reason = "The agent's reply could not be saved: Message exceeds the maximum length";
136
+ hoisted.runAgentInConversation.mockRejectedValue(new Error(reason));
137
+ const { pubSub, published } = makePubSub();
138
+ const userPayload = elevatedGuestPayload();
139
+
140
+ const ack = await startFireAndForget(pubSub, userPayload);
141
+ expect(ack.success).toBe(true); // the ack only says the run started
142
+
143
+ await settle(published);
144
+ const done = completions(published());
145
+ expect(done).toHaveLength(1);
146
+ expect(done[0].message.data).toMatchObject({
147
+ type: 'complete',
148
+ success: false,
149
+ errorMessage: reason,
150
+ conversationDetailId: 'reply-1',
151
+ });
152
+ expect(done[0].sessionId).toBe(SESSION);
153
+ // The guest's owner key, never the system user the run was elevated to.
154
+ expect(done[0].ownerUserId).toBe(StatusUpdateOwnerID(userPayload));
155
+ expect(done[0].ownerUserId).toBe(`${ANONYMOUS_USER_ID.toLowerCase()}|sid-guest-1`);
156
+ });
157
+
158
+ it('publishes one failed completion when the run fails before the agent starts (agent not runnable)', async () => {
159
+ const { pubSub, published } = makePubSub();
160
+
161
+ await startFireAndForget(pubSub, elevatedGuestPayload(), 'missing-agent');
162
+
163
+ await settle(published);
164
+ const done = completions(published());
165
+ expect(done).toHaveLength(1);
166
+ expect(done[0].message.data).toMatchObject({
167
+ success: false,
168
+ errorMessage: 'AI Agent with ID missing-agent not found',
169
+ conversationDetailId: 'reply-1',
170
+ });
171
+ expect(hoisted.runAgentInConversation).not.toHaveBeenCalled();
172
+ });
173
+
174
+ it('publishes only the run\'s own completion when the run returns a failed result (no second completion)', async () => {
175
+ const run = { ID: 'run-1', ConversationDetailID: 'reply-1', ErrorMessage: 'model refused', Steps: [] };
176
+ hoisted.runAgentInConversation.mockResolvedValue({
177
+ agentResult: { success: false, agentRun: run },
178
+ agentResponseDetailId: 'reply-1',
179
+ conversationId: 'conv-1',
180
+ });
181
+ const { pubSub, published } = makePubSub();
182
+
183
+ await startFireAndForget(pubSub, elevatedGuestPayload());
184
+
185
+ await settle(published);
186
+ const done = completions(published());
187
+ expect(done).toHaveLength(1);
188
+ expect(done[0].message.data).toMatchObject({ agentRunId: 'run-1', success: false, errorMessage: 'model refused' });
189
+ });
190
+ });
191
+
192
+ describe("RunAIAgent fire-and-forget — the liveness pulse", () => {
193
+ it("addresses heartbeats to the guest's owner key, not to the elevated system user", async () => {
194
+ vi.useFakeTimers();
195
+ hoisted.runAgentInConversation.mockReturnValue(new Promise(() => undefined)); // the run is still going
196
+ const { pubSub, published } = makePubSub();
197
+ const userPayload = elevatedGuestPayload();
198
+
199
+ await startFireAndForget(pubSub, userPayload);
200
+ await vi.advanceTimersByTimeAsync(DEFAULT_PULSE_INTERVAL_MS);
201
+
202
+ const heartbeats = published().filter((e) => e.message.type === HEARTBEAT_MESSAGE_TYPE);
203
+ expect(heartbeats.length).toBeGreaterThan(0);
204
+ for (const heartbeat of heartbeats) {
205
+ expect(heartbeat.ownerUserId).toBe(StatusUpdateOwnerID(userPayload));
206
+ expect(heartbeat.ownerUserId).not.toBe(SYSTEM_USER_ID);
207
+ }
208
+ });
209
+ });
@@ -28,6 +28,7 @@ vi.mock('type-graphql', () => {
28
28
 
29
29
  const hoisted = vi.hoisted(() => ({
30
30
  runAgentInConversation: vi.fn(),
31
+ reply: { ID: 'reply-1', ConversationID: 'conv-1', __mj_CreatedAt: new Date('2026-10-12T10:00:00.000Z') },
31
32
  }));
32
33
 
33
34
  vi.mock('@memberjunction/ai-agents', () => ({
@@ -35,6 +36,9 @@ vi.mock('@memberjunction/ai-agents', () => ({
35
36
  RunAgentInConversation(...args: unknown[]) {
36
37
  return hoisted.runAgentInConversation(...args);
37
38
  }
39
+ async CreateAgentReplyDetail() {
40
+ return hoisted.reply;
41
+ }
38
42
  },
39
43
  ArtifactToolManager: class {},
40
44
  }));
@@ -94,7 +98,7 @@ async function runMutation(
94
98
  options: { agentHistoryFrom?: string; fireAndForget?: boolean }
95
99
  ) {
96
100
  return resolver.RunAIAgentFromConversationDetail(
97
- 'detail-1', 'agent-1', makeContext(), 'session-1', pubSub,
101
+ 'user-msg-1', 'agent-1', makeContext(), 'session-1', pubSub,
98
102
  undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
99
103
  undefined, undefined, options.fireAndForget, undefined, undefined,
100
104
  options.agentHistoryFrom
@@ -108,8 +112,8 @@ afterEach(() => {
108
112
  describe('RunAIAgentFromConversationDetail — agentHistoryFrom', () => {
109
113
  let resolver: RunAIAgentResolver;
110
114
  let seams: ResolverSeams;
111
- /** The floor's position in executeAIAgent / executeAgentInBackground: third from last, before TrustReservedRunData and the branch. */
112
- const FLOOR_ARG = -3;
115
+ /** The floor's position in executeAIAgent / executeAgentInBackground: fourth from last, before TrustReservedRunData, the branch, and the reply detail. */
116
+ const FLOOR_ARG = -4;
113
117
 
114
118
  beforeEach(() => {
115
119
  ({ resolver, seams } = makeResolver());
@@ -124,7 +128,7 @@ describe('RunAIAgentFromConversationDetail — agentHistoryFrom', () => {
124
128
  expect(result.success).toBe(true);
125
129
  const loaderArgs = seams.loadConversationHistoryWithAttachments.mock.calls[0];
126
130
  expect(loaderArgs[0]).toBe('conv-1');
127
- expect(loaderArgs[4]).toEqual(['detail-1']);
131
+ expect(loaderArgs[4]).toBeUndefined(); // the reply row is created after the history loads, so nothing to exclude
128
132
  expect((loaderArgs[5] as Date).toISOString()).toBe(FLOOR_ISO);
129
133
  const runFloor = seams.executeAIAgent.mock.calls[0].at(FLOOR_ARG) as Date;
130
134
  expect(runFloor.toISOString()).toBe(FLOOR_ISO);
@@ -290,10 +294,13 @@ describe('executeAIAgent — status publishing', () => {
290
294
  expect(completion.success).toBe(true);
291
295
  });
292
296
 
293
- it('publishes a failure completion when a fire-and-forget run rejects', async () => {
297
+ it('logs a fire-and-forget run that rejects, and publishes no second completion', async () => {
298
+ // executeAIAgent resolves on every failure and publishes the failure completion itself
299
+ // (RunAIAgentResolver.fireAndForgetCompletion.test.ts). A rejection breaks that contract; the background
300
+ // runner only logs it, with the agent and session, instead of leaving an unhandled rejection.
294
301
  const { resolver, seams } = makeResolver();
295
302
  seams.executeAIAgent = vi.fn().mockRejectedValue(new Error('background blew up'));
296
- vi.spyOn(console, 'error').mockImplementation(() => undefined);
303
+ const errors = vi.spyOn(console, 'error').mockImplementation(() => undefined);
297
304
  vi.spyOn(console, 'log').mockImplementation(() => undefined);
298
305
  const publish = vi.fn().mockResolvedValue(undefined);
299
306
  const localPubSub = { publish } as unknown as PubSubEngine;
@@ -306,28 +313,11 @@ describe('executeAIAgent — status publishing', () => {
306
313
  {}, {}, 'agent-1', userPayload, '[]', 'session-1', localPubSub,
307
314
  undefined, undefined, undefined, undefined, undefined, 'detail-1',
308
315
  );
309
- await vi.waitFor(() => expect(publish).toHaveBeenCalled());
316
+ await vi.waitFor(() =>
317
+ expect(errors.mock.calls.some((call) => String(call[0]).includes('agent-1') && String(call[0]).includes('session-1'))).toBe(true),
318
+ );
310
319
 
311
- const sent = envelopes(publish);
312
- expect(sent).toHaveLength(1);
313
- expect(sent[0].type).toBe('StreamingContent');
314
- expect(sent[0].resolver).toBe('RunAIAgentResolver');
315
- const completion = sent[0].data as {
316
- type: string;
317
- agentRunId: string;
318
- conversationDetailId: string;
319
- success: boolean;
320
- errorMessage: string;
321
- result: string;
322
- };
323
- expect(completion).toMatchObject({
324
- type: 'complete',
325
- agentRunId: 'unknown',
326
- conversationDetailId: 'detail-1',
327
- success: false,
328
- errorMessage: 'background blew up',
329
- result: JSON.stringify({ success: false, errorMessage: 'background blew up' }),
330
- });
320
+ expect(publish).not.toHaveBeenCalled();
331
321
  });
332
322
 
333
323
  it('saves a Running run reported by progress as Failed when the agent throws', async () => {