@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
@@ -40,7 +40,7 @@ import { DeleteOptionsInput } from './DeleteOptionsInput.js';
40
40
  import { MJEvent, MJEventType, MJGlobal, ENCRYPTED_SENTINEL, ReplaceByteArraysWithBase64, EscapeSQLString, IsValueEncrypted, IsOnlyTimezoneShift } from '@memberjunction/global';
41
41
  import { SQLParser } from '@memberjunction/sql-parser';
42
42
  import { PostgreSQLDialect, SQLServerDialect, type SQLParserDialect } from '@memberjunction/sql-dialect';
43
- import { EncryptionEngine } from '@memberjunction/encryption';
43
+ import { EncryptionEngine, IsEncryptionKeyUnavailable, ReportEncryptionKeyUnavailable } from '@memberjunction/encryption';
44
44
  import { PUSH_STATUS_UPDATES_TOPIC, PublishStatusUpdate } from './PushStatusResolver.js';
45
45
  import { CACHE_INVALIDATION_TOPIC, MayBroadcastRecordData } from './CacheInvalidationResolver.js';
46
46
  import { PubSubManager } from './PubSubManager.js';
@@ -199,7 +199,11 @@ export class ResolverBase {
199
199
  const decryptedValue = await engine.Decrypt(value, contextUser);
200
200
  dataObject[fieldName] = decryptedValue;
201
201
  } catch (err) {
202
- LogError(`Failed to decrypt field ${fieldName} for API response: ${err}`);
202
+ if (IsEncryptionKeyUnavailable(err)) {
203
+ ReportEncryptionKeyUnavailable(err);
204
+ } else {
205
+ LogError(`Failed to decrypt field ${fieldName} for API response: ${err}`);
206
+ }
203
207
  // On decryption failure, use sentinel for safety
204
208
  dataObject[fieldName] = ENCRYPTED_SENTINEL;
205
209
  }
@@ -932,6 +936,7 @@ export class ResolverBase {
932
936
  label: string,
933
937
  provider?: IMetadataProvider,
934
938
  user?: UserInfo,
939
+ options?: { LeaveUnparseableToProvider?: boolean },
935
940
  ): void {
936
941
  if (!clause?.trim()) return;
937
942
 
@@ -945,6 +950,9 @@ export class ResolverBase {
945
950
  ? `SELECT 1 FROM __mj_clause_screen ORDER BY ${clause}`
946
951
  : `SELECT 1 FROM __mj_clause_screen WHERE (${clause})`;
947
952
  const parser = new SQLParser(wrapped, dialect);
953
+ // A clause the provider checks again on its own (an aggregate expression) leaves text the parser
954
+ // cannot read to that per-item check instead of failing the whole request.
955
+ if (!parser.IsValid && options?.LeaveUnparseableToProvider) return;
948
956
  if (!parser.IsValid || parser.HasWriteStatement || parser.StatementKind !== 'select') {
949
957
  throw new Error(
950
958
  `Invalid ${label}: not a safe read-only filter fragment — refusing under uncertainty`,
@@ -1062,6 +1070,11 @@ export class ResolverBase {
1062
1070
  * The single place that is not true is a field carrying `UserSearchParamFormatAPI`, whose
1063
1071
  * admin-authored format may splice the term in unquoted; `createViewUserSearchSQL` re-applies
1064
1072
  * `ValidateUserProvidedSQLClause` for exactly those entities.
1073
+ *
1074
+ * Each `Aggregates[].expression` gets the same screen, except that text the parser cannot read
1075
+ * goes on to the provider, which refuses it per aggregate. The provider never runs the caller's
1076
+ * aggregate text: it runs the SQL that `CheckAggregateExpression` rebuilds from a single
1077
+ * aggregate call over the entity's columns, and refuses everything else per aggregate.
1065
1078
  */
1066
1079
  protected screenClientViewClauses(
1067
1080
  clauses: {
@@ -1069,6 +1082,7 @@ export class ResolverBase {
1069
1082
  orderBy?: string | null;
1070
1083
  userSearchString?: string | null;
1071
1084
  overrideExcludeFilter?: string | null;
1085
+ aggregates?: AggregateExpression[] | null;
1072
1086
  },
1073
1087
  provider?: IMetadataProvider,
1074
1088
  user?: UserInfo,
@@ -1076,6 +1090,9 @@ export class ResolverBase {
1076
1090
  this.assertClientClauseUsesEntityBaseViews(clauses.extraFilter, 'ExtraFilter', provider, user);
1077
1091
  this.assertClientClauseUsesEntityBaseViews(clauses.orderBy, 'OrderBy', provider, user);
1078
1092
  this.assertClientClauseUsesEntityBaseViews(clauses.overrideExcludeFilter, 'OverrideExcludeFilter', provider, user);
1093
+ for (const aggregate of clauses.aggregates ?? []) {
1094
+ this.assertClientClauseUsesEntityBaseViews(aggregate?.expression, 'Aggregate', provider, user, { LeaveUnparseableToProvider: true });
1095
+ }
1079
1096
  }
1080
1097
 
1081
1098
  /** Same write-node walk as EDS `sqlReadOnlyScreen.astContainsWriteNode`. */
@@ -1223,7 +1240,7 @@ export class ResolverBase {
1223
1240
  // Screen at this boundary: parse, reject writes, allow only entity BaseViews the
1224
1241
  // acting user can read (subqueries do not inherit the top entity's permission check).
1225
1242
  this.screenClientViewClauses(
1226
- { extraFilter, orderBy, userSearchString, overrideExcludeFilter },
1243
+ { extraFilter, orderBy, userSearchString, overrideExcludeFilter, aggregates },
1227
1244
  provider as unknown as IMetadataProvider,
1228
1245
  user,
1229
1246
  );
@@ -1390,6 +1407,7 @@ export class ResolverBase {
1390
1407
  orderBy: param.orderBy,
1391
1408
  userSearchString: param.userSearchString,
1392
1409
  overrideExcludeFilter: param.overrideExcludeFilter,
1410
+ aggregates: param.aggregates,
1393
1411
  },
1394
1412
  md,
1395
1413
  contextUser ?? undefined,
@@ -1818,9 +1836,10 @@ export class ResolverBase {
1818
1836
  // that hydrates from those values — never loading what the database actually holds.
1819
1837
  const hasNarrowedAuditPayload = this.StripRecordChangePayloadFromClientInput(entityInfo, userInfo, input, clientNewValues);
1820
1838
 
1821
- if (this.MustLoadTruthFromDatabase(entityInfo, input, hasDeniedReadFields, hasNarrowedAuditPayload)) {
1839
+ if (this.MustLoadTruthFromDatabase(entityInfo, input, hasDeniedReadFields, hasNarrowedAuditPayload, userInfo)) {
1822
1840
  // We get here because the entity tracks record changes, OR the client did not provide OldValues,
1823
- // OR field-level security is in play — in every case we need the true old values from the DB
1841
+ // OR field-level security or an Update row filter is in play — in every case we need the true
1842
+ // old values from the DB
1824
1843
  const cKey = new CompositeKey(
1825
1844
  entityInfo.PrimaryKeys.map((pk) => {
1826
1845
  return {
@@ -1918,19 +1937,28 @@ export class ResolverBase {
1918
1937
  * it never applies them to the entity. An update-denied field can then only become dirty by being
1919
1938
  * named in the mutation input itself, which is precisely the case that check does catch.
1920
1939
  *
1921
- * Ordered so the boolean flag is evaluated last: the extra load lands only on entities that have
1922
- * the feature switched on, which is almost none of them.
1940
+ * The flag comes after the cheap terms, so the extra load lands only on entities that have the
1941
+ * feature switched on, which is almost none of them.
1923
1942
  *
1924
1943
  * `MJ: Record Changes` always loads from the database: its server class allows an update only when
1925
1944
  * Comments is the one dirty field, and "dirty" compared against client-supplied OldValues lets a
1926
1945
  * caller pin forged audit columns as both old and new values. It doesn't track its own changes, so
1927
1946
  * nothing else forces the load.
1947
+ *
1948
+ * A caller under an Update row filter (a role's Update RLS filter or an API-key row filter) always
1949
+ * loads from the database too (#4919). The save's post-image check skips its query when no column
1950
+ * the filter reads is dirty, and dirtiness measured against client OldValues is the caller's to
1951
+ * choose: claiming the new value of a filter column as its old value made it clean, so the update
1952
+ * moved the row outside the caller's scope without the check ever running. Evaluated last: it
1953
+ * builds the filter clause, and only callers with such a filter pay for the load. A missing user
1954
+ * (a user-cache miss) also loads: with no user the filter can't be ruled out.
1928
1955
  */
1929
1956
  protected MustLoadTruthFromDatabase(
1930
1957
  entityInfo: EntityInfo,
1931
1958
  input: { OldValues___?: Array<{ Key: string; Value: unknown }> },
1932
1959
  hasDeniedReadFields: boolean,
1933
- hasNarrowedAuditPayload: boolean
1960
+ hasNarrowedAuditPayload: boolean,
1961
+ user: UserInfo | undefined
1934
1962
  ): boolean {
1935
1963
  return (
1936
1964
  entityInfo.TrackRecordChanges ||
@@ -1938,7 +1966,9 @@ export class ResolverBase {
1938
1966
  !input.OldValues___ ||
1939
1967
  hasDeniedReadFields ||
1940
1968
  hasNarrowedAuditPayload ||
1941
- entityInfo.EnableFieldLevelSecurity
1969
+ entityInfo.EnableFieldLevelSecurity ||
1970
+ !user ||
1971
+ entityInfo.GetEffectiveRowFilterWhereClause(user, EntityPermissionType.Update, '').length > 0
1942
1972
  );
1943
1973
  }
1944
1974
 
@@ -1248,6 +1248,10 @@ export class RunViewResolver extends ResolverBase {
1248
1248
  // otherwise a CacheLocal batch's DataSource:'Materialized' request silently reads the live view.
1249
1249
  DataSource: item.params.DataSource,
1250
1250
  IncludeBinaryFields: item.params.IncludeBinaryFields,
1251
+ // BypassCache likewise — the input type (RunDynamicViewInput) has always declared it, and its
1252
+ // own description promises "the pre-check cache lookup is skipped". Dropping it here left the
1253
+ // provider free to answer a deliberate database read from the server cache.
1254
+ BypassCache: item.params.BypassCache,
1251
1255
  AfterKey: item.params.AfterKey
1252
1256
  ? CompositeKey.FromKeyValuePairs(item.params.AfterKey.KeyValuePairs)
1253
1257
  : undefined,
package/src/index.ts CHANGED
@@ -4,7 +4,7 @@ dotenv.config({ quiet: true });
4
4
 
5
5
  import { expressMiddleware } from '@as-integrations/express5';
6
6
  import { mergeSchemas } from '@graphql-tools/schema';
7
- import { Metadata, DatabasePlatform, SetProvider, StartupManager as StartupManagerImport, BaseEntity, BaseEntityEvent, RunView, DatabaseProviderBase, ResolveStartupMode } from '@memberjunction/core';
7
+ import { Metadata, DatabasePlatform, SetProvider, StartupManager as StartupManagerImport, BaseEntity, BaseEntityEvent, RunView, DatabaseProviderBase, ProviderBase, ResolveStartupMode } from '@memberjunction/core';
8
8
  import { UserCache, resolveDbPlatformFromEnv } from '@memberjunction/generic-database-provider';
9
9
  import { MJGlobal, MJEventType, UUIDsEqual, ShutdownRegistry } from '@memberjunction/global';
10
10
  import { setupSQLServerClient, SQLServerDataProvider, SQLServerProviderConfigData } from '@memberjunction/sqlserver-dataprovider';
@@ -19,9 +19,10 @@ import { default as fg } from 'fast-glob';
19
19
  import { useServer } from 'graphql-ws/lib/use/ws';
20
20
  import { createServer } from 'node:http';
21
21
  import { fileURLToPath } from 'node:url';
22
- import { readFileSync, writeFileSync } from 'node:fs';
22
+ import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
23
+ import { tmpdir } from 'node:os';
23
24
  import { Session as InspectorSession } from 'node:inspector';
24
- import { sep } from 'node:path';
25
+ import { join, sep } from 'node:path';
25
26
  import 'reflect-metadata';
26
27
  import { ReplaySubject } from 'rxjs';
27
28
  import { BuildSchemaOptions, buildSchemaSync, getMetadataStorage, GraphQLTimestamp, PubSubEngine } from 'type-graphql';
@@ -34,7 +35,7 @@ import { configInfo, configFilePath, dbDatabase, dbHost, dbPort, dbUsername, gra
34
35
  import { TranslateBracketsToPG } from './postgresqlCompat.js';
35
36
  import { BuildPostgreSQLConnectionConfig, DescribeReadOnlyLoginOverreach, PostgreSQLReadOnlyPool, ResolvePostgreSQLEndpoint, ResolvePostgreSQLReadOnlyCredentials, ToPGPoolConfig } from './postgresqlPoolSettings.js';
36
37
  import { default as jwt } from 'jsonwebtoken';
37
- import { contextFunction, CreateUnifiedAuthMiddleware, getUserPayload } from './context.js';
38
+ import { contextFunction, CreateReadOnlyProvider, CreateUnifiedAuthMiddleware, getUserPayload } from './context.js';
38
39
  import { UserPayload } from './types.js';
39
40
  import { requireSystemUserDirective, publicDirective } from './directives/index.js';
40
41
  import { variablesLoggingMiddleware } from './logging/variablesLoggingMiddleware.js';
@@ -50,6 +51,14 @@ import { createRealtimeSdpBrokerRouter } from './rest/RealtimeSdpBrokerHandler.j
50
51
  import { REALTIME_SDP_EXCHANGE_PATH } from '@memberjunction/ai';
51
52
  import { createMagicLinkHandler, createMagicLinkJwksRouter, registerMagicLinkAuthProvider, MAGIC_LINK_MOUNT_PATH } from './auth/magicLink/index.js';
52
53
  import { createWidgetHandler, WIDGET_MOUNT_PATH } from './realtimeWidget/index.js';
54
+ import {
55
+ CreateRealtimeVerifyRouter,
56
+ REALTIME_VERIFY_MOUNT_PATH,
57
+ RealtimeSessionEventService,
58
+ RealtimeSessionVerificationService,
59
+ ParseReplicatedRealtimeSessionEvent,
60
+ type RealtimeSessionEventTopicPayload,
61
+ } from './realtimeSessions/index.js';
53
62
  import { resolve } from 'node:path';
54
63
  import { DataSourceInfo, raiseEvent } from './types.js';
55
64
 
@@ -60,15 +69,11 @@ import { LocalCacheManager, StartupManager, TelemetryManager, TelemetryLevel, Lo
60
69
  import { getSystemUser, validateAuthProvidersRegistered } from './auth/index.js';
61
70
  import { createAuthProviderCatalogRouter, AUTH_CATALOG_MOUNT_PATH } from './auth/AuthProviderCatalogRouter.js';
62
71
  import { GetAPIKeyEngine } from '@memberjunction/api-keys';
63
- import { RedisLocalStorageProvider } from '@memberjunction/redis-provider';
64
- import { GenericDatabaseProvider } from '@memberjunction/generic-database-provider';
72
+ import { ActionEngineServer } from '@memberjunction/actions';
73
+ import { CreateAdhocSQLAuthorizer } from './resolvers/AdhocQueryResolver.js';
74
+ import { CacheManagerConfigFromSettings, CreateSharedCacheFromEnvironment, StartEngineSweeper, StartMetadataSweep, StartUserCacheChecks, WarmupLeaseMsFromSettings, WirePushStatusFanOut, WireSharedCacheEvents } from './sharedCache.js';
65
75
  import { PubSubManager } from './generic/PubSubManager.js';
66
76
  import { ReconcileOrphanedConversationDetails } from './generic/OrphanedConversationDetailReconciler.js';
67
- import {
68
- PUSH_STATUS_UPDATES_TOPIC,
69
- SetPushStatusPublishHook,
70
- ParseReplicatedStatusUpdate,
71
- } from './generic/PushStatusResolver.js';
72
77
  import {
73
78
  HANDOFF_OFFER_FANOUT_CHANNEL,
74
79
  HANDOFF_OFFER_TOPIC,
@@ -76,11 +81,14 @@ import {
76
81
  ParseReplicatedHandoffOfferUpdate,
77
82
  } from './resolvers/HumanHandoffResolver.js';
78
83
  import { RoomHandoffEngine } from '@memberjunction/livekit-room-server';
84
+ // Type-only: the provider is built in ./sharedCache.ts. Still needed here because the handoff-offer
85
+ // fan-out below stays in this file — see the note at its call site.
86
+ import type { RedisLocalStorageProvider } from '@memberjunction/redis-provider';
79
87
  import { IntegrationProgressEmitter } from '@memberjunction/integration-progress-artifacts';
80
88
  import { PublishIntegrationProgress } from './resolvers/IntegrationProgressResolver.js';
81
89
  import { RegisterRSUProgressBridge } from './integration/RSUProgressBridge.js';
82
90
  import { ClientToolRequestManager, AgentRunWatchdog } from '@memberjunction/ai-agents';
83
- import { SessionJanitor } from './agentSessions/index.js';
91
+ import { BindBridgeEngineHostInstance, SessionJanitor, SetHostInstancePort, StartBridgeOrphanReconciliation } from './agentSessions/index.js';
84
92
  import { StartTaskGraphDispatcher } from './services/StartTaskGraphDispatcher.js';
85
93
  import { MJServerWorkQueueProviderSource, StartWorkQueueHost } from './services/WorkQueueHostService.js';
86
94
  import { GetAttachmentService } from '@memberjunction/aiengine';
@@ -132,6 +140,7 @@ export * from 'type-graphql';
132
140
  // later importers, which makes schema build fail on ViewResult.RowCount.
133
141
  export { Int, Float, ID } from 'type-graphql';
134
142
  export { NewUserBase } from './auth/newUsers.js';
143
+ export { CreateIsolatedProvider } from './isolatedProvider.js';
135
144
  export { configInfo, DEFAULT_SERVER_CONFIG } from './config.js';
136
145
  export { ServerExtensionLoader, BaseServerExtension, DefaultServerExtensionServiceRegistry } from '@memberjunction/server-extensions-core';
137
146
  export type {
@@ -196,6 +205,8 @@ export * from './resolvers/GenerateSeedTaxonomyResolver.js';
196
205
  export * from './resolvers/PipelineProgressResolver.js';
197
206
  export * from './resolvers/IntegrationProgressResolver.js';
198
207
  export * from './resolvers/IdentityClaimRedemptionResolver.js';
208
+ export * from './resolvers/UserAvatarResolver.js';
209
+ export * from './resolvers/avatarInputValidation.js';
199
210
  export * from './resolvers/ClientToolRequestResolver.js';
200
211
  export * from './resolvers/AutotagPipelineResolver.js';
201
212
  export * from './resolvers/TagGovernanceResolver.js';
@@ -253,6 +264,9 @@ export * from './resolvers/CurrentUserContextResolver.js';
253
264
  export * from './resolvers/RSUResolver.js';
254
265
  export * from './resolvers/AgentSessionResolver.js';
255
266
  export * from './resolvers/RealtimeClientSessionResolver.js';
267
+ export * from './resolvers/RealtimeSessionEventsResolver.js';
268
+ export * from './resolvers/RealtimeSessionVerificationResolver.js';
269
+ export * from './realtimeSessions/index.js';
256
270
  export * from './resolvers/MeetingResolver.js';
257
271
  export * from './resolvers/RemoteBrowserActionResolver.js';
258
272
  export * from './agentSessions/index.js';
@@ -316,59 +330,6 @@ function resolveServerVersion(): string | undefined {
316
330
  /** How often to re-check for conversation details left behind by finished runs. */
317
331
  const ORPHAN_DETAIL_SWEEP_INTERVAL_MS = 5 * 60_000;
318
332
 
319
- /** Redis channel carrying replicated push-status updates between server instances. */
320
- const PUSH_STATUS_FANOUT_CHANNEL = 'push-status-updates';
321
-
322
- /**
323
- * Replicate push-status updates across server instances over Redis (MJ #4222).
324
- *
325
- * Outbound: every locally-published update is forwarded on a shared channel. Inbound: a message
326
- * from another instance is republished onto THIS instance's local topic, where the normal
327
- * subscription filter decides who receives it — so the identity gate (`ownerUserId` vs. the
328
- * connection's authenticated user) still applies to a replicated message exactly as it does to a
329
- * local one. The replica has no say in who sees what.
330
- *
331
- * Republishing goes straight to `PubSubManager`, never back through `publishStatusUpdate`, so an
332
- * inbound message cannot be re-broadcast and loop. `SourceServerId` guards the remaining case: a
333
- * publisher also receives its own message from Redis.
334
- */
335
- async function wirePushStatusFanOut(redisProvider: RedisLocalStorageProvider, startupLog: StartupLogger): Promise<void> {
336
- try {
337
- await redisProvider.SubscribeToChannel(PUSH_STATUS_FANOUT_CHANNEL, (raw: string) => {
338
- try {
339
- const payload = ParseReplicatedStatusUpdate(raw, MJGlobal.Instance.ProcessUUID);
340
- if (!payload) {
341
- return;
342
- }
343
- // Rebuilt as a plain record: the topic's publish signature takes an index-signature type,
344
- // and listing the fields keeps the wire shape explicit at the one place it crosses hosts.
345
- PubSubManager.Instance.Publish(PUSH_STATUS_UPDATES_TOPIC, {
346
- sessionId: payload.sessionId,
347
- ownerUserId: payload.ownerUserId,
348
- message: payload.message,
349
- SourceServerId: payload.SourceServerId,
350
- });
351
- } catch (err) {
352
- // A malformed message on a shared channel must not take down the subscriber.
353
- LogError('Error processing push-status fan-out message', undefined, err);
354
- }
355
- });
356
-
357
- SetPushStatusPublishHook((payload) => {
358
- redisProvider.PublishMessage(PUSH_STATUS_FANOUT_CHANNEL, JSON.stringify(payload));
359
- });
360
-
361
- // Printed unconditionally, not verbose-gated. "Is fan-out actually on?" is the first question
362
- // anyone debugging a hung conversation behind a load balancer asks, and a silent default left
363
- // no way to answer it.
364
- console.log('[MJAPI] Push-status updates: cross-instance fan-out enabled via Redis');
365
- } catch (err) {
366
- // Single-instance delivery still works, and the durable tail query covers the rest. Degraded,
367
- // not broken — so this must not stop the server from starting.
368
- console.warn(`Push-status fan-out unavailable: ${(err as Error).message}`);
369
- }
370
- }
371
-
372
333
  /**
373
334
  * Replicate handoff-offer updates across server instances over Redis.
374
335
  *
@@ -408,6 +369,43 @@ async function wireHandoffOfferFanOut(redisProvider: RedisLocalStorageProvider,
408
369
  }
409
370
  }
410
371
 
372
+ /** Redis channel carrying replicated realtime session events between server instances. */
373
+ const REALTIME_SESSION_EVENTS_FANOUT_CHANNEL = 'realtime-session-events';
374
+
375
+ /**
376
+ * Replicate realtime session events across server instances over Redis.
377
+ *
378
+ * Behind a load balancer the browser's WebSocket (the `RealtimeSessionEvents` subscription) lives on one
379
+ * replica while the request that raises the event — typically the public verify-link redemption — can be
380
+ * handled by another. The session-events topic is an in-process PubSub, so without this an
381
+ * `identity.verified` published on replica B never reaches the subscriber on replica A.
382
+ *
383
+ * Inbound messages are republished onto THIS instance's local topic only (never back through the
384
+ * replication hook, so there is no loop) and are still judged by the subscription filter against the
385
+ * connection's identity — a replica has no say in who sees what. The durable
386
+ * `RealtimeSessionVerificationStatus` query remains the backstop if Redis is down.
387
+ */
388
+ async function wireRealtimeSessionEventFanOut(redisProvider: RedisLocalStorageProvider): Promise<void> {
389
+ try {
390
+ await redisProvider.SubscribeToChannel(REALTIME_SESSION_EVENTS_FANOUT_CHANNEL, (raw: string) => {
391
+ try {
392
+ const payload = ParseReplicatedRealtimeSessionEvent(raw, MJGlobal.Instance.ProcessUUID);
393
+ if (payload) {
394
+ RealtimeSessionEventService.Instance.PublishReplicated(payload);
395
+ }
396
+ } catch (err) {
397
+ LogError(`Realtime session event fan-out: dropping a malformed replicated event: ${(err as Error).message}`);
398
+ }
399
+ });
400
+ RealtimeSessionEventService.Instance.SetReplicationHook((payload: RealtimeSessionEventTopicPayload) => {
401
+ redisProvider.PublishMessage(REALTIME_SESSION_EVENTS_FANOUT_CHANNEL, JSON.stringify(payload));
402
+ });
403
+ console.log('[MJAPI] Realtime session events: cross-instance fan-out enabled via Redis');
404
+ } catch (err) {
405
+ // Single-instance delivery still works and the status query covers the rest: degraded, not broken.
406
+ console.warn(`Realtime session event fan-out unavailable: ${(err as Error).message}`);
407
+ }
408
+ }
411
409
 
412
410
  // Bind MJStorage as the conversation-attachment blob store. The attachment service itself no longer
413
411
  // imports `@memberjunction/storage` — that dependency made it unusable from any browser or React
@@ -422,6 +420,12 @@ GetAttachmentService().BlobStore = new MJStorageBlobStore();
422
420
 
423
421
  export const Serve = async (resolverPaths: Array<string>, app: Application = CreateApp(), options?: MJServerOptions): Promise<void> => {
424
422
  const t0 = performance.now();
423
+ // Name this MJAPI instance by its port before anything stamps a session row with its identity, so the janitor's
424
+ // startup recovery closes only the sessions an earlier boot of this instance left, never those of another MJAPI on
425
+ // the same host (#5309).
426
+ SetHostInstancePort(graphqlPort);
427
+ // The realtime bridge engine stamps its bridge rows with the same identity and reconciles orphans by it (#5310).
428
+ BindBridgeEngineHostInstance();
425
429
  // Level-gated startup logger. Resolves verbosity from telemetry.level (single
426
430
  // operator knob). At `standard` (default), per-phase timings are collapsed into
427
431
  // the one-line summary; at `verbose`+ each phase prints inline as before.
@@ -450,6 +454,25 @@ const setupComplete$ = new ReplaySubject(1);
450
454
  const dbType = GetDbType();
451
455
  const dataSources: DataSourceInfo[] = [];
452
456
 
457
+ // Shared cache first: the database provider loads metadata and startup engines through
458
+ // it, so the first server to start fills it and later servers read it instead of each querying
459
+ // the database and broadcasting what they loaded. Subscribed before engines load so they hear
460
+ // other servers while they start.
461
+ const sharedCache = CreateSharedCacheFromEnvironment(configInfo.cacheSettings);
462
+ if (sharedCache) {
463
+ await WireSharedCacheEvents(sharedCache, () => (Metadata.Provider instanceof ProviderBase ? Metadata.Provider : undefined)); // global-provider-ok: bootstrap
464
+ // The cross-instance fan-outs ride this connection. Push-status moved into sharedCache.ts
465
+ // with the rest of the Redis wiring; the handoff and realtime-session ones stay here deliberately —
466
+ // they reach into RoomHandoffEngine and RealtimeSessionEventService, and the cache module should not
467
+ // acquire a dependency on either.
468
+ await WirePushStatusFanOut(sharedCache);
469
+ await wireHandoffOfferFanOut(sharedCache, startupLog);
470
+ await wireRealtimeSessionEventFanOut(sharedCache);
471
+ startupLog.LogIf('verbose', `Redis cache provider connected: ${process.env.REDIS_URL}`);
472
+ }
473
+ const cacheManagerConfig = CacheManagerConfigFromSettings(configInfo.cacheSettings);
474
+ const warmupLeaseMs = WarmupLeaseMsFromSettings(configInfo.cacheSettings);
475
+
453
476
  if (dbType === 'postgresql') {
454
477
  // ─── PostgreSQL Path ───────────────────────────────────────────
455
478
  startupLog.BeginPhase('Connecting to database');
@@ -509,6 +532,7 @@ const setupComplete$ = new ReplaySubject(1);
509
532
  mj_core_schema,
510
533
  cacheRefreshInterval / 1000, // convert ms to seconds
511
534
  );
535
+ pgConfigData.LocalStorageProvider = sharedCache ?? undefined;
512
536
  const provider = new PostgreSQLDataProvider();
513
537
  await provider.Config(pgConfigData);
514
538
  SetProvider(provider);
@@ -520,7 +544,7 @@ const setupComplete$ = new ReplaySubject(1);
520
544
  const sysUser = UserCache.Instance.GetSystemUser();
521
545
  const backupSysUser = UserCache.Instance.Users.find(u => u.IsActive && u.Type === 'Owner');
522
546
  const pgStartupMode = ResolveStartupMode({ configValue: configInfo.startup?.mode, defaultMode: 'full' });
523
- await StartupManagerImport.Instance.Startup(false, sysUser || backupSysUser, provider, { mode: pgStartupMode.mode });
547
+ await StartupManagerImport.Instance.Startup(false, sysUser || backupSysUser, provider, { mode: pgStartupMode.mode, cacheManagerConfig, warmupLeaseMs });
524
548
 
525
549
  // Both provider sources have now had their turn — config/env at module load, metadata via
526
550
  // AuthProviderEngine's startup hook — so "no providers at all" is finally a meaningful check.
@@ -678,10 +702,11 @@ const setupComplete$ = new ReplaySubject(1);
678
702
 
679
703
  // cacheRefreshInterval is configured in ms; checkRefreshIntervalSeconds declares seconds — see providerConfigUnits.ts
680
704
  const config = new SQLServerProviderConfigData(pool, mj_core_schema, MetadataCacheRefreshIntervalSeconds(cacheRefreshInterval));
705
+ config.LocalStorageProvider = sharedCache ?? undefined;
681
706
  // MJAPI is a long-running server, so entry-point default is 'full' engine pre-warm;
682
707
  // MJ_STARTUP_MODE / mj.config.cjs startup.mode can override per the shared precedence chain
683
708
  const startupMode = ResolveStartupMode({ configValue: configInfo.startup?.mode, defaultMode: 'full' });
684
- await setupSQLServerClient(config, { mode: startupMode.mode });
709
+ await setupSQLServerClient(config, { mode: startupMode.mode, cacheManagerConfig, warmupLeaseMs });
685
710
 
686
711
  // See the note on the PostgreSQL path above: this is the first point at which both the
687
712
  // config/env providers and the metadata catalog have been registered.
@@ -806,84 +831,31 @@ const setupComplete$ = new ReplaySubject(1);
806
831
  startupLog.LogIf('verbose', 'Server telemetry disabled');
807
832
  }
808
833
 
809
- // Optionally inject Redis as the shared storage provider for cross-server cache invalidation
810
- if (process.env.REDIS_URL) {
811
- const redisProvider = new RedisLocalStorageProvider({
812
- url: process.env.REDIS_URL,
813
- keyPrefix: process.env.REDIS_KEY_PREFIX || 'mj',
814
- enablePubSub: true,
815
- enableLogging: configInfo.cacheSettings?.verboseLogging ?? false,
816
- });
817
- (Metadata.Provider as GenericDatabaseProvider).SetLocalStorageProvider(redisProvider); // global-provider-ok: bootstrap (Redis cache wiring)
818
- await redisProvider.StartListening();
819
-
820
- // Connect Redis pub/sub events to LocalCacheManager callback dispatch
821
- // so cross-server cache invalidation messages are routed to registered callbacks
822
- redisProvider.OnCacheChanged((event) => {
823
- const sourceShort = event.SourceServerId ? event.SourceServerId.substring(0, 8) : 'unknown';
824
- console.log(`[MJAPI] Redis pub/sub → DispatchCacheChange: ${event.Action} for "${event.CacheKey}" from server ${sourceShort}`);
825
- LocalCacheManager.Instance.DispatchCacheChange(event);
826
-
827
- // Also broadcast to connected browser clients via GraphQL subscription
828
- // Extract entity name from the cache key (format: EntityName|Filter|OrderBy|...)
829
- const entityName = event.CacheKey ? event.CacheKey.split('|')[0] : '';
830
- if (entityName) {
831
- PubSubManager.Instance.Publish(CACHE_INVALIDATION_TOPIC, {
832
- entityName,
833
- primaryKeyValues: null, // entity-level invalidation
834
- action: event.Action || 'save',
835
- sourceServerId: event.SourceServerId || 'unknown',
836
- timestamp: new Date(),
837
- });
838
- }
839
- });
840
-
841
- // Fan push-status updates across server instances (MJ #4222).
842
- //
843
- // Behind a load balancer the browser's WebSocket lives on one replica while the mutation that
844
- // drives the agent can be handled by another. The push topic is an in-process PubSub, so a
845
- // completion published on replica B never reaches a subscriber on replica A — the browser waits
846
- // forever for an event that was delivered to nobody. Replicating progress and completion over
847
- // Redis closes that, and the durable tail query remains the backstop if Redis is down.
848
- await wirePushStatusFanOut(redisProvider, startupLog);
849
- await wireHandoffOfferFanOut(redisProvider, startupLog);
850
-
851
- startupLog.LogIf('verbose', `Redis cache provider connected: ${process.env.REDIS_URL}`);
852
- }
853
-
854
- // If Redis is available, swap LocalCacheManager's storage provider to Redis.
855
- // LocalCacheManager may have already been initialized (with in-memory provider)
856
- // during engine loading. SetStorageProvider migrates cached data to Redis.
857
- if (process.env.REDIS_URL) {
858
- await LocalCacheManager.Instance.SetStorageProvider(Metadata.Provider.LocalStorageProvider); // global-provider-ok: bootstrap
859
- startupLog.LogIf('verbose', 'LocalCacheManager: storage provider swapped to Redis');
860
- }
861
- // Ensure LocalCacheManager is initialized (no-op if already done during engine loading)
862
- if (!LocalCacheManager.Instance.IsInitialized) {
863
- // Build cache config from mj.config.cjs cacheSettings
864
- const cs = configInfo.cacheSettings;
865
- const cacheConfig = {
866
- maxSizeBytes: (cs.maxMemoryMB ?? 150) * 1024 * 1024,
867
- maxPercentOfCachePerEntity: cs.maxPercentOfCachePerEntity ?? 50,
868
- defaultTTLMs: (cs.defaultTTLSeconds ?? 0) * 1000,
869
- evictionSweepIntervalMs: (cs.evictionSweepIntervalSeconds ?? 300) * 1000,
870
- verboseLogging: cs.verboseLogging ?? false,
871
- };
872
- await LocalCacheManager.Instance.Initialize(Metadata.Provider.LocalStorageProvider, cacheConfig); // global-provider-ok: bootstrap
873
- if (startupLog.IsAtLeast('verbose')) {
874
- // eslint-disable-next-line no-console
875
- console.log('LocalCacheManager initialized with cache config:', JSON.stringify({
876
- maxMemoryMB: cs.maxMemoryMB ?? 150,
877
- maxPercentOfCachePerEntity: cs.maxPercentOfCachePerEntity ?? 50,
878
- evictionSweepIntervalSeconds: cs.evictionSweepIntervalSeconds ?? 300,
879
- }));
880
- }
834
+ // The database provider already initialized LocalCacheManager from inside its own Config(), with
835
+ // no settings — this call is what makes `cacheSettings` take effect (Initialize merges a config
836
+ // handed to it later). Unconditional on purpose: the guard that used to sit here
837
+ // skipped exactly the case that needed it.
838
+ await LocalCacheManager.Instance.Initialize(Metadata.Provider.LocalStorageProvider, cacheManagerConfig); // global-provider-ok: bootstrap
839
+ if (sharedCache) {
840
+ await LocalCacheManager.Instance.SetStorageProvider(sharedCache);
881
841
  }
842
+ const engineSweepMs = StartEngineSweeper(configInfo.cacheSettings);
843
+ startupLog.LogIf('verbose', engineSweepMs > 0 ? `Engine/database sweep every ${engineSweepMs / 1000}s` : 'Engine/database sweep disabled');
844
+ const userCacheCheckMs = StartUserCacheChecks(configInfo.cacheSettings);
845
+ startupLog.LogIf('verbose', userCacheCheckMs > 0 ? `User cache staleness check every ${userCacheCheckMs / 1000}s` : 'User cache staleness check disabled');
846
+ // Costs nothing unless a metadata entity declares TrustServerCacheCompletely = false.
847
+ const metadataSweepMs = StartMetadataSweep(configInfo.cacheSettings, () => (Metadata.Provider instanceof ProviderBase ? Metadata.Provider : undefined)); // global-provider-ok: bootstrap (this process's one provider)
848
+ startupLog.LogIf('verbose', metadataSweepMs > 0 ? `Metadata/database sweep every ${metadataSweepMs / 1000}s (only for entities declaring drift)` : 'Metadata/database sweep disabled');
882
849
 
883
850
  // Initialize APIKeyEngine singleton — reads apiKeyGeneration from mj.config.cjs automatically
884
851
  // This must happen before any request handler calls GetAPIKeyEngine()
885
852
  GetAPIKeyEngine();
886
853
 
854
+ // Actions that run caller-supplied SQL (Run Ad-hoc Query) run only on the read-only login, with
855
+ // ExecuteAdhocQuery's table check and timeout limit.
856
+ ActionEngineServer.Instance.SetReadOnlyProviderFactory(() => CreateReadOnlyProvider(dataSources));
857
+ ActionEngineServer.Instance.SetAdhocSQLAuthorizer(CreateAdhocSQLAuthorizer(configInfo.databaseSettings.requestTimeout));
858
+
887
859
  setupComplete$.next(true);
888
860
  raiseEvent('setupComplete', dataSources, null, this);
889
861
 
@@ -1131,7 +1103,7 @@ const setupComplete$ = new ReplaySubject(1);
1131
1103
  const profile = await new Promise<unknown>((res, rej) =>
1132
1104
  profSession!.post('Profiler.stop', (e, r) => (e ? rej(e) : res((r as { profile: unknown }).profile)))
1133
1105
  );
1134
- const outPath = `/tmp/mjapi_schema_build_${process.pid}.cpuprofile`;
1106
+ const outPath = join(mkdtempSync(join(tmpdir(), 'mjapi-schema-')), `mjapi_schema_build_${process.pid}.cpuprofile`);
1135
1107
  writeFileSync(outPath, JSON.stringify(profile));
1136
1108
  profSession.disconnect();
1137
1109
  // eslint-disable-next-line no-console
@@ -1414,6 +1386,19 @@ const setupComplete$ = new ReplaySubject(1);
1414
1386
  startupLog.LogIf('verbose', `[Widget] Public routes registered at ${WIDGET_MOUNT_PATH}/session and ${WIDGET_MOUNT_PATH}/session/refresh`);
1415
1387
  }
1416
1388
 
1389
+ // ─── Realtime identity verification: PUBLIC verify-link routes (before auth mw) ───
1390
+ // The person opening the emailed link holds no MJ token — the single-use token is the capability.
1391
+ // GET only renders a confirm button; the POST redeems (mail scanners GET every link). Mounted only
1392
+ // when verification is enabled AND able to send mail AND able to protect its codes (see
1393
+ // realtime.identityVerification config: communicationProvider + hmacSecret).
1394
+ if (configInfo.realtime?.identityVerification?.enabled && !RealtimeSessionVerificationService.Instance.IsEnabled) {
1395
+ console.warn('[RealtimeVerify] realtime.identityVerification.enabled is true but verification is unavailable: it needs a communicationProvider and an hmacSecret of at least 16 characters.');
1396
+ }
1397
+ if (RealtimeSessionVerificationService.Instance.IsEnabled) {
1398
+ app.use(REALTIME_VERIFY_MOUNT_PATH, cors<cors.CorsRequest>(), CreateRealtimeVerifyRouter());
1399
+ startupLog.LogIf('verbose', `[RealtimeVerify] Public routes registered at ${REALTIME_VERIFY_MOUNT_PATH}/:token (GET) and ${REALTIME_VERIFY_MOUNT_PATH} (POST)`);
1400
+ }
1401
+
1417
1402
  // ─── Server extensions loader & shared service registry ───────────────────
1418
1403
  const extensionLoader = new ServerExtensionLoader();
1419
1404
  _currentExtensionLoader = extensionLoader;
@@ -1757,12 +1742,16 @@ const setupComplete$ = new ReplaySubject(1);
1757
1742
  });
1758
1743
  }
1759
1744
 
1760
- // Launch the AI Agent Session janitor: run own-host orphan recovery once at boot, then keep a
1761
- // periodic staleness sweep running. Self-registers with ShutdownRegistry, so its timer is cleared
1762
- // by the gracefulShutdown drain below (no explicit Stop() wiring needed here).
1745
+ // Launch the AI Agent Session janitor: run own-host orphan recovery once at boot, then a periodic tick
1746
+ // that closes stale and over-deadline sessions and rebuilds lost realtime recordings from their
1747
+ // crash-recovery shards (storage reads and writes). Self-registers with ShutdownRegistry, so its timer
1748
+ // is cleared by the gracefulShutdown drain below (no explicit Stop() wiring needed here).
1763
1749
  if (resumeUser && Metadata.Provider instanceof DatabaseProviderBase) { // global-provider-ok: server startup recovery — boot-time session janitor uses the server's own provider
1764
1750
  SessionJanitor.Instance.Start(Metadata.Provider, resumeUser) // global-provider-ok: server-owned background reconciler runs under the server's provider + system user
1765
1751
  .catch(err => console.warn(`[SessionJanitor] Startup failed: ${err}`));
1752
+ // The bridge engine's orphan reconcile likewise starts only now that this process holds its port, so it can't
1753
+ // take the live bridges of another MJAPI still serving that port for its own orphans (#5310).
1754
+ StartBridgeOrphanReconciliation(Metadata.Provider, resumeUser); // global-provider-ok: server-owned background reconciler runs under the server's provider + system user
1766
1755
  }
1767
1756
 
1768
1757
  // Launch the durable task-graph dispatcher: claim Pending tasks, execute them with a fresh
@@ -0,0 +1,22 @@
1
+ import { DatabaseProviderBase, Metadata } from '@memberjunction/core';
2
+
3
+ /**
4
+ * A fresh Read-Write provider with its OWN transaction state, sharing the server's connection pool.
5
+ *
6
+ * A provider's ambient transaction (`BeginTransaction` / `CommitTransaction` / `RollbackTransaction`)
7
+ * is a field on the provider INSTANCE. Two units of work that open transactions on one instance nest
8
+ * into each other, and every write anyone else makes through that instance while one is open lands
9
+ * inside it (MJ#2140; `entityTransactionScope.ts`). The process-global `Metadata.Provider` is shared
10
+ * by every request, scheduled job and background service, so transactional work must never run on
11
+ * it: one transaction left open there silently swallows every later global-provider write until the
12
+ * process restarts.
13
+ *
14
+ * GraphQL resolvers already get a per-request provider from `AppContext.providers`. Code that runs
15
+ * outside one, such as the auth flow before the context exists, calls this.
16
+ *
17
+ * Cheap: it is the global provider's `CreateIndependentInstance()` (SQL Server and PostgreSQL), which
18
+ * reuses the already-loaded metadata and the shared pool.
19
+ */
20
+ export async function CreateIsolatedProvider(): Promise<DatabaseProviderBase> {
21
+ return (Metadata.Provider as DatabaseProviderBase).CreateIndependentInstance(); // global-provider-ok: borrows only its pool and metadata, never its transaction state
22
+ }