@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
@@ -0,0 +1,207 @@
1
+ import 'reflect-metadata';
2
+ import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
3
+ import sql from 'mssql';
4
+ import { RunViewParams, RunViewResult, SetProvider, UserInfo } from '@memberjunction/core';
5
+ import { GenericDatabaseProvider, UserCache } from '@memberjunction/generic-database-provider';
6
+ import { SQLServerDataProvider, SQLServerProviderConfigData } from '@memberjunction/sqlserver-dataprovider';
7
+ import {
8
+ CacheInvalidationBroadcastManager,
9
+ SequencedRoleBucketBroadcastProvider,
10
+ } from '../broadcast/index.js';
11
+
12
+ const IT_DB_NAME = process.env.MJ_IT_DB;
13
+
14
+ describe.skipIf(!IT_DB_NAME)('MJServer Scalability Architecture — Live SQL Server Integration', () => {
15
+ let pool: sql.ConnectionPool;
16
+ let sharedProvider: SQLServerDataProvider;
17
+ let sysUser: UserInfo;
18
+
19
+ beforeAll(async () => {
20
+ pool = new sql.ConnectionPool({
21
+ server: process.env.MJ_IT_HOST || 'localhost',
22
+ port: parseInt(process.env.MJ_IT_PORT || '1433', 10),
23
+ database: IT_DB_NAME,
24
+ user: process.env.MJ_IT_USER || 'sa',
25
+ password: process.env.MJ_IT_PASSWORD || 'GHWUc7QKZNT8GQiqnJHdEPonPcukY2WA',
26
+ options: {
27
+ encrypt: false,
28
+ trustServerCertificate: true,
29
+ },
30
+ });
31
+ await pool.connect();
32
+
33
+ sharedProvider = new SQLServerDataProvider();
34
+ const config = new SQLServerProviderConfigData(pool, '__mj', 0);
35
+ await sharedProvider.Config(config);
36
+ SetProvider(sharedProvider);
37
+ await UserCache.Instance.Refresh(sharedProvider, 0);
38
+
39
+ const foundUser =
40
+ UserCache.Instance.GetSystemUser() ||
41
+ UserCache.Instance.Users.find((u) => u.IsActive && u.Type === 'Owner') ||
42
+ UserCache.Instance.Users[0];
43
+ if (!foundUser) {
44
+ throw new Error(`No user found in ${IT_DB_NAME}`);
45
+ }
46
+ sysUser = foundUser;
47
+ }, 60_000);
48
+
49
+ afterAll(async () => {
50
+ if (pool?.connected) {
51
+ await pool.close();
52
+ }
53
+ });
54
+
55
+ it('creates per-request SQLServerDataProvider with zero-copy prototype-delegated metadata shell and lazy transaction queues', async () => {
56
+ const reqProvider = new SQLServerDataProvider();
57
+ const reqConfig = new SQLServerProviderConfigData(
58
+ pool,
59
+ '__mj',
60
+ 0,
61
+ undefined,
62
+ undefined,
63
+ false // ignoreExistingMetadata = false -> reuses shared metadata shell
64
+ );
65
+ await reqProvider.Config(reqConfig);
66
+
67
+ // Verify lazy RxJS Subjects are NOT allocated on a read-only request provider
68
+ const internal = reqProvider as unknown as {
69
+ _sqlQueue$: unknown;
70
+ _transactionState$: unknown;
71
+ };
72
+
73
+ expect(internal._sqlQueue$).toBeNull();
74
+ expect(internal._transactionState$).toBeNull();
75
+
76
+ // Verify zero-copy prototype delegation to shared metadata
77
+ expect(Object.getPrototypeOf(reqProvider.AllMetadata)).toBe(sharedProvider.AllMetadata);
78
+ expect(reqProvider.Entities.length).toBeGreaterThan(50);
79
+ expect(reqProvider.Entities).toBe(sharedProvider.Entities);
80
+ });
81
+
82
+ it('coalesces 10 concurrent identical InternalRunView calls against SQL Server into a single SQL execution and isolates result mutations', async () => {
83
+ // Warm up the view column order cache for vwEntities (must return >=1 row so sortViewRowKeys caches sys.columns)
84
+ await sharedProvider.RunView(
85
+ {
86
+ EntityName: 'MJ: Entities',
87
+ ExtraFilter: "Name = 'MJ: Entities'",
88
+ ResultType: 'simple',
89
+ IgnoreMaxRows: true,
90
+ },
91
+ sysUser
92
+ );
93
+
94
+ const executeSpy = vi.spyOn(sharedProvider, 'ExecuteSQL');
95
+ const internalRunner = sharedProvider as unknown as {
96
+ InternalRunView<T>(params: RunViewParams, contextUser?: UserInfo): Promise<RunViewResult<T>>;
97
+ };
98
+
99
+ try {
100
+ const concurrentCount = 10;
101
+ const promises = Array.from({ length: concurrentCount }, () =>
102
+ internalRunner.InternalRunView<Record<string, unknown>>(
103
+ {
104
+ EntityName: 'MJ: Entities',
105
+ ExtraFilter: "SchemaName = '__mj' AND Name LIKE 'MJ: %'",
106
+ OrderBy: 'Name ASC',
107
+ IgnoreMaxRows: true,
108
+ ResultType: 'simple',
109
+ },
110
+ sysUser
111
+ )
112
+ );
113
+
114
+ const results = await Promise.all(promises);
115
+
116
+ // Only 1 physical SQL execution should have been issued for the 10 identical concurrent views
117
+ expect(executeSpy).toHaveBeenCalledTimes(1);
118
+
119
+ for (const res of results) {
120
+ expect(res.Success).toBe(true);
121
+ expect(res.Results.length).toBeGreaterThan(0);
122
+ }
123
+
124
+ // Mutating caller 0's result array or row object must not affect caller 1's result
125
+ const originalName = results[1].Results[0].Name;
126
+ const originalLength = results[1].Results.length;
127
+ results[0].Results[0].Name = 'MUTATED_IN_CALLER_0';
128
+ results[0].Results.pop();
129
+
130
+ expect(results[1].Results[0].Name).toBe(originalName);
131
+ expect(results[0].Results.length).toBe(originalLength - 1);
132
+ expect(results[1].Results.length).toBe(originalLength);
133
+ } finally {
134
+ executeSpy.mockRestore();
135
+ }
136
+ });
137
+
138
+ it('bounds concurrent SQL executions in RunViews to MaxConcurrentViewsPerBatch', async () => {
139
+ let activeCount = 0;
140
+ let maxObservedConcurrency = 0;
141
+
142
+ const originalExecuteSQL = sharedProvider.ExecuteSQL.bind(sharedProvider);
143
+ const executeSpy = vi.spyOn(sharedProvider, 'ExecuteSQL').mockImplementation(async (query, params, options, contextUser) => {
144
+ activeCount++;
145
+ if (activeCount > maxObservedConcurrency) {
146
+ maxObservedConcurrency = activeCount;
147
+ }
148
+ try {
149
+ return await originalExecuteSQL(query, params, options, contextUser);
150
+ } finally {
151
+ activeCount--;
152
+ }
153
+ });
154
+
155
+ try {
156
+ const batchParams = Array.from({ length: 16 }, (_, idx) => ({
157
+ EntityName: 'MJ: Entities',
158
+ ExtraFilter: `SchemaName = '__mj' AND UserViewMaxRows IS NULL AND ${idx} = ${idx}`,
159
+ IgnoreMaxRows: true,
160
+ ResultType: 'simple' as const,
161
+ }));
162
+
163
+ const batchResults = await sharedProvider.RunViews(batchParams, sysUser);
164
+ expect(batchResults).toHaveLength(16);
165
+ for (const r of batchResults) {
166
+ expect(r.Success).toBe(true);
167
+ }
168
+ expect(maxObservedConcurrency).toBeLessThanOrEqual(GenericDatabaseProvider.MaxConcurrentViewsPerBatch);
169
+ expect(maxObservedConcurrency).toBeGreaterThan(1);
170
+ } finally {
171
+ executeSpy.mockRestore();
172
+ }
173
+ });
174
+
175
+ it('sequences and shapes cache invalidation broadcasts against live entity metadata and replays delta catch-up', async () => {
176
+ const provider = CacheInvalidationBroadcastManager.Instance.Configure({
177
+ provider: 'SequencedRoleBucket',
178
+ ringBufferSize: 500,
179
+ }) as SequencedRoleBucketBroadcastProvider;
180
+ expect(provider).toBeInstanceOf(SequencedRoleBucketBroadcastProvider);
181
+
182
+ const baseSeq = provider.CurrentSequence;
183
+ const entityInfo = sharedProvider.Entities.find((e) => e.Name === 'MJ: Entities');
184
+ expect(entityInfo).toBeDefined();
185
+
186
+ const published = provider.RecordInvalidation({
187
+ entityName: 'MJ: Entities',
188
+ primaryKeyValues: JSON.stringify([{ FieldName: 'ID', Value: entityInfo!.ID }]),
189
+ action: 'save',
190
+ sourceServerId: 'it-server-node-1',
191
+ timestamp: new Date(),
192
+ recordData: JSON.stringify({ ID: entityInfo!.ID, Name: entityInfo!.Name }),
193
+ });
194
+
195
+ expect(published.sequence).toBe(baseSeq + 1);
196
+ expect(provider.CurrentSequence).toBe(baseSeq + 1);
197
+
198
+ // Verify delta catch-up shapes payload for sysUser against real MJ: Entities metadata
199
+ const catchUp = provider.GetChangesSince(baseSeq, sysUser);
200
+ expect(catchUp.RequiresFullRefresh).toBe(false);
201
+ expect(catchUp.CurrentSequence).toBe(baseSeq + 1);
202
+ expect(catchUp.Events).toHaveLength(1);
203
+ expect(catchUp.Events[0].EntityName).toBe('MJ: Entities');
204
+ expect(catchUp.Events[0].Sequence).toBe(baseSeq + 1);
205
+ expect(catchUp.Events[0].RecordData).toContain(entityInfo!.Name);
206
+ });
207
+ });
@@ -43,6 +43,7 @@ function buildSourceUser(): UserInfo {
43
43
  user.ReturningVisitorContext = { VisitorKey: 'vk-1' };
44
44
  user.WidgetGuestContext = { WidgetID: 'W0000000-0000-0000-0000-000000000001' };
45
45
  user.IsMagicLinkAnonymous = true;
46
+ user.AnonymousVisitorID = 'visitor-sid-1';
46
47
  return user;
47
48
  }
48
49
 
@@ -99,13 +100,14 @@ describe('CloneUserForSessionContext', () => {
99
100
  expect(clone.APIKeyRowFilters).toHaveLength(1);
100
101
  });
101
102
 
102
- it('preserves ReturningVisitorContext, WidgetGuestContext, and IsMagicLinkAnonymous', () => {
103
+ it('preserves ReturningVisitorContext, WidgetGuestContext, IsMagicLinkAnonymous, and AnonymousVisitorID', () => {
103
104
  const source = buildSourceUser();
104
105
  const clone = CloneUserForSessionContext(source);
105
106
 
106
107
  expect(clone.ReturningVisitorContext).toEqual({ VisitorKey: 'vk-1' });
107
108
  expect(clone.WidgetGuestContext).toEqual({ WidgetID: 'W0000000-0000-0000-0000-000000000001' });
108
109
  expect(clone.IsMagicLinkAnonymous).toBe(true);
110
+ expect(clone.AnonymousVisitorID).toBe('visitor-sid-1');
109
111
  });
110
112
 
111
113
  it('stamping new contexts on the clone does NOT write through to the source', () => {
@@ -120,6 +122,7 @@ describe('CloneUserForSessionContext', () => {
120
122
  clone.APIKeyActingContext = { ActingOrganizationID: 'ORG-OTHER' };
121
123
  clone.APIKeyRowFilters = newBindings;
122
124
  clone.IsMagicLinkAnonymous = false;
125
+ clone.AnonymousVisitorID = 'visitor-sid-OTHER';
123
126
 
124
127
  // The source (potentially the shared UserCache instance) must be untouched
125
128
  expect(source.TenantContext).toEqual({ TenantID: 'tenant-1', Source: 'header' });
@@ -128,6 +131,7 @@ describe('CloneUserForSessionContext', () => {
128
131
  expect(source.APIKeyRowFilters).toHaveLength(1);
129
132
  expect(source.APIKeyRowFilters?.[0].FilterID).toBe('F0000000-0000-0000-0000-000000000001');
130
133
  expect(source.IsMagicLinkAnonymous).toBe(true);
134
+ expect(source.AnonymousVisitorID).toBe('visitor-sid-1');
131
135
  });
132
136
 
133
137
  it('clearing contexts on the clone does NOT clear them on the source', () => {
@@ -0,0 +1,40 @@
1
+ /**
2
+ * `SweepInterruptedRuns` closes out run artifacts a dead process left open. It is only sound
3
+ * BEFORE anything in this process can start a run: a run started first is still in flight when
4
+ * the sweep looks, so the sweep marks a live run "killed" and the wizard and active-operations
5
+ * panel report a failure for work that is still going.
6
+ *
7
+ * Booting the server is far too heavy for a unit test, so this pins the order in the source:
8
+ * the sweep is awaited before the scheduler, the integration sync worker (whose Start() polls
9
+ * immediately), the HTTP listener, and pending RSU work.
10
+ */
11
+ import { readFileSync } from 'node:fs';
12
+ import { join } from 'node:path';
13
+ import { describe, expect, it } from 'vitest';
14
+
15
+ const source = readFileSync(join(__dirname, '..', 'index.ts'), 'utf8')
16
+ .replace(/\/\*[\s\S]*?\*\//g, '')
17
+ .replace(/^\s*\/\/.*$/gm, '');
18
+
19
+ function at(needle: string): number {
20
+ const i = source.indexOf(needle);
21
+ expect(i, `not found in index.ts: ${needle}`).toBeGreaterThan(-1);
22
+ return i;
23
+ }
24
+
25
+ describe('startup: the interrupted-run sweep runs before anything can start a run', () => {
26
+ const sweep = () => at('await SweepInterruptedRuns(');
27
+
28
+ it.each([
29
+ ['the scheduled jobs service', 'scheduledJobsService.Start()'],
30
+ ['the integration sync worker', 'integrationSyncWorker.Start()'],
31
+ ['the HTTP listener', 'httpServer.listen('],
32
+ ['pending RSU work', 'processRSUPendingWork().catch('],
33
+ ])('precedes %s', (_label, needle) => {
34
+ expect(sweep()).toBeLessThan(at(needle));
35
+ });
36
+
37
+ it('is called exactly once', () => {
38
+ expect(source.split('await SweepInterruptedRuns(').length - 1).toBe(1);
39
+ });
40
+ });
@@ -263,6 +263,34 @@ describe('createUnifiedAuthMiddleware', () => {
263
263
  });
264
264
  });
265
265
 
266
+ describe('x-mj-timezone header', () => {
267
+ beforeEach(() => {
268
+ setupSuccessfulAuth();
269
+ });
270
+
271
+ async function payloadFor(headers: Record<string, string>) {
272
+ const req = makeMockReq({ headers: { authorization: 'Bearer valid-token', ...headers } });
273
+ await middleware(req, makeMockRes(), vi.fn());
274
+ return req.userPayload!;
275
+ }
276
+
277
+ it('carries a valid IANA zone on the user payload', async () => {
278
+ expect((await payloadFor({ 'x-mj-timezone': 'Pacific/Auckland' })).TimeZone).toBe('Pacific/Auckland');
279
+ });
280
+
281
+ it('drops an invalid zone, so callers fall back to UTC', async () => {
282
+ expect((await payloadFor({ 'x-mj-timezone': 'Mars/Olympus_Mons' })).TimeZone).toBeUndefined();
283
+ expect((await payloadFor({ 'x-mj-timezone': "UTC'; DROP TABLE x;--" })).TimeZone).toBeUndefined();
284
+ expect((await payloadFor({ 'x-mj-timezone': '' })).TimeZone).toBeUndefined();
285
+ });
286
+
287
+ it('leaves the payload exactly as before when the header is absent', async () => {
288
+ const payload = await payloadFor({});
289
+ expect(payload.TimeZone).toBeUndefined();
290
+ expect(Object.keys(payload)).not.toContain('TimeZone');
291
+ });
292
+ });
293
+
266
294
  describe('Authentication failures', () => {
267
295
  it('should return 401 when no authorization header is provided, quietly (no stack-trace log)', async () => {
268
296
  mockGetReadOnlyDS.mockReturnValue(MOCK_POOL);
@@ -155,6 +155,8 @@ describe('widgetGuestElevation — elevateUserPayload', () => {
155
155
  expect(elevated.isSystemUser).toBe(true);
156
156
  // The session id MUST be preserved so progress/streaming still routes to the guest's websocket.
157
157
  expect(elevated.sessionId).toBe('sess-123');
158
+ // The guest it stands in for, so conversation writes can stay the guest's (MJ#4791).
159
+ expect(elevated.ElevatedFrom).toBe(guestPayload.userRecord);
158
160
  });
159
161
 
160
162
  it('does not mutate the original guest payload', () => {
@@ -647,9 +647,9 @@ export class SessionJanitor extends BaseSingleton<SessionJanitor> implements ISh
647
647
 
648
648
  /**
649
649
  * Gives a status-less session with ledger shards back a `Pending` status so it can be claimed (the
650
- * claim only takes Pending/Partial/stale Assembling). The status is lost when a `BaseEntity.Save` of
651
- * the session (e.g. its close) loaded the row before a shard or terminal proc wrote it and saved
652
- * after: `spUpdate` rewrites every column, so it puts back the stale NULL status and counts.
650
+ * claim only takes Pending/Partial/stale Assembling). That is how statuses were lost before #6010
651
+ * made the ledger columns not updatable through an entity save (a stale `Save` of the session, e.g. its
652
+ * close, put back the NULL status and counts). The restore stays for rows hit before then.
653
653
  *
654
654
  * The terminal proc is the existing way to set a NULL status to `Pending`. It is stamped with the
655
655
  * counts the session still stores, or 0 for a NULL count (the proc accepts any count over NULL), and
@@ -13,6 +13,7 @@ import { WriteReturningVisitorRecap } from './ReturningVisitorRecap.js';
13
13
  import { SessionRunIDVerifier, SessionRunIDs } from './SessionRunIDVerifier.js';
14
14
  import { ResolveScopedAnonymousRunUser } from '../realtimeWidget/widgetGuestElevation.js';
15
15
  import { RecordingAssemblyScheduler } from '../realtimeSessions/RecordingAssemblyScheduler.js';
16
+ import { ResolveSessionScopeIdentity } from '../realtimeSessions/sessionAccess.js';
16
17
 
17
18
  /** Entity names — centralised so the `MJ:`-prefix convention is applied in exactly one place. */
18
19
  const SESSION_ENTITY = 'MJ: AI Agent Sessions';
@@ -153,11 +154,13 @@ export class SessionManager {
153
154
  /**
154
155
  * Authorize and create a new session. Flow:
155
156
  * 1. `CanRun` check on the agent — denied → {@link SessionAuthorizationError}, no row written.
156
- * 2. Resolve the conversation (use the supplied one, or create+link a fresh Conversation).
157
+ * 2. Resolve the conversation (use the supplied one, or create+link a fresh Conversation). An anonymous
158
+ * caller may only supply a conversation stamped with its own scope identity.
157
159
  * 3. Persist the `AIAgentSession` as `Active`, stamped with this host and `LastActiveAt = now`.
158
160
  *
159
161
  * @returns the saved {@link MJAIAgentSessionEntity}.
160
- * @throws {SessionAuthorizationError} when the user may not run the agent.
162
+ * @throws {SessionAuthorizationError} when the user may not run the agent, or an anonymous caller supplies a
163
+ * conversation that is not its own.
161
164
  * @throws {Error} when conversation creation or session save fails.
162
165
  */
163
166
  public async CreateSession(
@@ -166,6 +169,9 @@ export class SessionManager {
166
169
  provider: IMetadataProvider,
167
170
  ): Promise<MJAIAgentSessionEntity> {
168
171
  await this.assertCanRun(input.agentID, contextUser);
172
+ if (input.conversationID && contextUser.IsMagicLinkAnonymous) {
173
+ await this.assertAnonymousOwnsConversation(input.conversationID, contextUser, provider);
174
+ }
169
175
 
170
176
  const conversationID = input.conversationID
171
177
  ?? await this.createConversation(input, contextUser, provider);
@@ -448,6 +454,23 @@ export class SessionManager {
448
454
 
449
455
  // ----- internals -------------------------------------------------------------------------
450
456
 
457
+ /**
458
+ * Throws {@link SessionAuthorizationError} unless the anonymous caller owns the conversation it wants to bind a
459
+ * new session to: the conversation, read as the caller, loads, and its `ExternalID` equals the caller's scope
460
+ * identity ({@link ResolveSessionScopeIdentity}). A caller with no scope identity is refused. Every anonymous
461
+ * visitor shares one Anonymous user, so row-level security alone may let one visitor read another's
462
+ * conversation; without this check the new session row and its relays would point into that conversation.
463
+ */
464
+ private async assertAnonymousOwnsConversation(conversationID: string, contextUser: UserInfo, provider: IMetadataProvider): Promise<void> {
465
+ const conversation = await provider.GetEntityObject<MJConversationEntity>(CONVERSATION_ENTITY, contextUser);
466
+ const loaded = await conversation.Load(conversationID);
467
+ const scopeIdentity = ResolveSessionScopeIdentity(contextUser);
468
+ if (!loaded || !scopeIdentity || conversation.ExternalID !== scopeIdentity) {
469
+ LogError(`SessionManager.CreateSession: refused anonymous user ${contextUser.ID} binding a session to conversation ${conversationID} (${loaded ? 'not its own' : 'not readable'}).`);
470
+ throw new SessionAuthorizationError('Not authorized to start a session on this conversation.');
471
+ }
472
+ }
473
+
451
474
  /** Throws {@link SessionAuthorizationError} unless the user has `CanRun` on the agent. */
452
475
  private async assertCanRun(agentID: string, contextUser: UserInfo): Promise<void> {
453
476
  const canRun = await AIAgentPermissionHelper.HasPermission(agentID, contextUser, 'run');
@@ -468,13 +491,13 @@ export class SessionManager {
468
491
  conversation.NewRecord();
469
492
  conversation.UserID = input.userID;
470
493
  conversation.Name = 'Agent Session';
471
- // For a magic-link-anonymous principal (e.g. a public web-widget voice guest), stamp the
472
- // session's conversation with the signed per-session scope so the Widget Guest RLS filters
473
- // ({{ScopeResourceID}}) isolate this session — and everything chained to it (AI Agent
474
- // Sessions via ConversationID, Session Channels via the session) — from other guests
475
- // sharing the Anonymous UserID. No-op for named principals (no scope → default ExternalID).
476
- if (contextUser.MagicLinkScope?.ResourceID) {
477
- conversation.ExternalID = contextUser.MagicLinkScope.ResourceID;
494
+ // Stamp the scope identity AuthorizeSessionAccess compares against (one rule, one place:
495
+ // ResolveSessionScopeIdentity) — the token's resource scope when it has one, else an anonymous
496
+ // visitor's mj_sid. This isolates anonymous visitors that share the Anonymous UserID, and keeps
497
+ // the {{ScopeResourceID}} RLS filters matching the visitor's own rows. No-op for unscoped named principals.
498
+ const scopeIdentity = ResolveSessionScopeIdentity(contextUser);
499
+ if (scopeIdentity) {
500
+ conversation.ExternalID = scopeIdentity;
478
501
  }
479
502
 
480
503
  // Returning-visitor memory (RV1/RV2/RV4) for the VOICE path: the widget guest token carries the
@@ -539,9 +562,9 @@ export class SessionManager {
539
562
  }
540
563
 
541
564
  /**
542
- * Loads a session entity by ID, returning null when it cannot be loaded. The server entity
543
- * (`MJAIAgentSessionEntityServer`) keeps the lifecycle saves that follow from writing back the
544
- * recording ledger's columns (#5294).
565
+ * Loads a session entity by ID, returning null when it cannot be loaded. The lifecycle saves that
566
+ * follow cannot write back the recording ledger's columns: those have `AllowUpdateAPI=0`, so they are
567
+ * not parameters of `spUpdateAIAgentSession` (#5294, #6010).
545
568
  */
546
569
  private async loadSession(
547
570
  agentSessionID: string,
@@ -13,6 +13,7 @@ import { rateLimit } from 'express-rate-limit';
13
13
  import { LogError, type PublicAuthProviderCatalog } from '@memberjunction/core';
14
14
  import { AuthProviderEngine } from './AuthProviderEngine.js';
15
15
  import { EMPTY_PUBLIC_LOGIN_SCREEN, LoginScreenEngine } from './LoginScreenEngine.js';
16
+ import { AttachLocalDevAuthRoutes } from './localDevAuth.js';
16
17
 
17
18
  /** Mount path for the public catalog router. */
18
19
  export const AUTH_CATALOG_MOUNT_PATH = '/auth';
@@ -98,6 +99,8 @@ export function CreateAuthProviderCatalogRouter(): Router {
98
99
  }
99
100
  });
100
101
 
102
+ AttachLocalDevAuthRoutes(router);
103
+
101
104
  return router;
102
105
  }
103
106
 
package/src/auth/index.ts CHANGED
@@ -2,7 +2,7 @@ import { JwtHeader, SigningKeyCallback, JwtPayload } from 'jsonwebtoken';
2
2
  import { configInfo } from '../config.js';
3
3
  import { UserCache } from '@memberjunction/generic-database-provider';
4
4
  import sql from 'mssql';
5
- import { DatabaseProviderBase, Metadata, RoleInfo, UserInfo } from '@memberjunction/core';
5
+ import { DatabaseProviderBase, LogError, Metadata, RoleInfo, UserInfo } from '@memberjunction/core';
6
6
  import { NewUserBase } from './newUsers.js';
7
7
  import { MJGlobal } from '@memberjunction/global';
8
8
  import { MJUserEntity, MJUserEntityType } from '@memberjunction/core-entities';
@@ -254,11 +254,63 @@ export const VerifyUserRecord = async (
254
254
  ): Promise<UserInfo | undefined> => {
255
255
  if (!email) return undefined;
256
256
 
257
- // FindUser, not Users.find: a user this process has not loaded yet (created seconds ago on
258
- // another server) is looked up in the database instead of being treated as nonexistent.
259
- let user = await UserCache.Instance.FindUser({ Email: email });
257
+ // Collect ALL matches rather than taking .find()'s first hit. Two User rows can share an email
258
+ // case-insensitively (see the duplicate-provisioning path below), and .find() then resolved to
259
+ // whichever row happened to sit earlier in UserCache.Instance.Users — i.e. database row order.
260
+ // That order changes when the cache is rebuilt, so an MJAPI restart silently moved a user onto a
261
+ // different record: their conversations, artifacts, items shared with them and their profile
262
+ // picture (UserImageURL lives on the User row) all vanished at once, and came back if the order
263
+ // flipped again. Observed in production 2026-09-15. Resolution must not depend on row order.
264
+ const emailMatches = UserCache.Instance.Users.filter((u) => {
265
+ if (!u.Email || u.Email.trim() === '') {
266
+ // this condition should never occur. If it doesn throw a console error including the user id
267
+ // DB requires non-null but this is just an extra check and we could in theory have a blank string in the DB
268
+ console.error(`SYSTEM METADATA ISSUE: User ${u.ID} has no email address`);
269
+ return false;
270
+ } else return u.Email.toLowerCase().trim() === email.toLowerCase().trim();
271
+ });
272
+
273
+ if (emailMatches.length > 1) {
274
+ // Deterministic tie-break by ID so every process and every restart agrees, and a LOUD error
275
+ // because this is a data problem an operator has to resolve — silently picking a row is what
276
+ // made it look like data loss to the customer. Sorting by ID is arbitrary but stable; the point
277
+ // is that it never changes, not that it picks the "right" one.
278
+ emailMatches.sort((a, b) => a.ID.localeCompare(b.ID));
279
+ LogError(
280
+ `AMBIGUOUS LOGIN: ${emailMatches.length} User records share the email '${email}' ` +
281
+ `(case-insensitively): ${emailMatches.map((u) => `${u.ID} (${u.Email})`).join(', ')}. ` +
282
+ `Resolving to ${emailMatches[0].ID} by stable ID order. Until the duplicates are merged, ` +
283
+ `content owned by the other records is invisible to this login. Find every affected user ` +
284
+ `with: SELECT LOWER(Email), COUNT(*) FROM __mj.[User] GROUP BY LOWER(Email) HAVING COUNT(*) > 1`
285
+ );
286
+ }
287
+
288
+ // Nothing cached: ask FindUser, which reads the database for a user this process has not loaded
289
+ // yet (created seconds ago on another server) instead of treating them as nonexistent.
290
+ let user = emailMatches[0] ?? (await UserCache.Instance.FindUser({ Email: email }));
260
291
 
261
292
  if (!user) {
293
+ // REFRESH THE CACHE BEFORE PROVISIONING. This block used to sit AFTER the auto-create below,
294
+ // gated on `!user`, so it could only ever run when creation had already failed or was not
295
+ // attempted. That made it dead code in the exact case it exists to handle: a user who IS in the
296
+ // database but is NOT yet in this process's UserCache. Users provisioned out of band (an admin,
297
+ // an invite, a direct insert) are absent from the cache until it is rebuilt, so the lookup above
298
+ // missed and auto-create minted a SECOND record for someone who already had one — which is how
299
+ // the duplicate emails that Fix 1 above has to tolerate get created in the first place.
300
+ // Observed in production: three users created 2026-09-14 18:18, a duplicate for one of them
301
+ // created 2026-09-15 01:38 on that user's next sign-in.
302
+ //
303
+ // The recursive call passes attemptCacheUpdateIfNeeded=false, so the second pass skips straight
304
+ // to auto-create if the refreshed cache still has no match. Auto-provisioning of genuinely new
305
+ // users is therefore unchanged — it just no longer runs before we have looked properly.
306
+ if (configInfo.userHandling.updateCacheWhenNotFound && dataSource && attemptCacheUpdateIfNeeded) {
307
+ console.warn(`User ${email} not found in cache. Updating cache in attempt to find the user...`);
308
+
309
+ await refreshUserCache();
310
+
311
+ return VerifyUserRecord(email, firstName, lastName, requestDomain, dataSource, false);
312
+ }
313
+
262
314
  // NOTE: `requestDomain` (parsed from the spoofable `Origin` header) is deliberately NOT part of
263
315
  // this condition. It was previously required here, which meant a non-browser client sending no
264
316
  // Origin could never auto-provision while an attacker simply forged one — it gated entry without
@@ -313,14 +365,6 @@ export const VerifyUserRecord = async (
313
365
  }
314
366
  }
315
367
 
316
- if (!user && configInfo.userHandling.updateCacheWhenNotFound && dataSource && attemptCacheUpdateIfNeeded) {
317
- // if we get here that means in the above, if we were attempting to create a new user, it did not work, or it wasn't attempted and we have a config that asks us to auto update the cache
318
- console.warn(`User ${email} not found in cache. Updating cache in attempt to find the user...`);
319
-
320
- await refreshUserCache();
321
-
322
- return VerifyUserRecord(email, firstName, lastName, requestDomain, dataSource, false); // try one more time but do not update cache next time if not found
323
- }
324
368
  }
325
369
 
326
370
  return user;
@@ -1,8 +1,35 @@
1
1
  import { configInfo } from '../config.js';
2
2
  import { AuthProviderConfig, LogError, LogStatus, LogStatusEx, UserInfo, type IMetadataProvider } from '@memberjunction/core';
3
- import { AuthProviderFactory } from '@memberjunction/auth-providers';
3
+ import { AuthProviderFactory, LocalDevProvider } from '@memberjunction/auth-providers';
4
4
  import { AuthProviderEngine } from './AuthProviderEngine.js';
5
5
 
6
+ /**
7
+ * Returns true when local development auth is enabled via `MJ_DEV_AUTH_ENABLED=true`
8
+ * or `configInfo.devAuth?.enabled === true`.
9
+ */
10
+ export function IsLocalDevAuthEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
11
+ const rawEnv = env.MJ_DEV_AUTH_ENABLED?.trim().toLowerCase();
12
+ if (rawEnv === 'true' || rawEnv === '1') {
13
+ return true;
14
+ }
15
+ if (rawEnv === 'false' || rawEnv === '0') {
16
+ return false;
17
+ }
18
+ return configInfo?.devAuth?.enabled === true;
19
+ }
20
+
21
+ /**
22
+ * Enforces the production safety invariant: throws a fatal error if local dev auth
23
+ * is enabled while `NODE_ENV === 'production'`.
24
+ */
25
+ export function AssertLocalDevAuthAllowed(env: NodeJS.ProcessEnv = process.env): void {
26
+ if (IsLocalDevAuthEnabled(env) && env.NODE_ENV?.trim().toLowerCase() === 'production') {
27
+ throw new Error(
28
+ '[SECURITY] Local dev auth (MJ_DEV_AUTH_ENABLED) is strictly forbidden when NODE_ENV=production. Refusing to start.'
29
+ );
30
+ }
31
+ }
32
+
6
33
  /**
7
34
  * Initialize authentication providers from configuration.
8
35
  *
@@ -15,6 +42,8 @@ import { AuthProviderEngine } from './AuthProviderEngine.js';
15
42
  * connection and a system user exist.
16
43
  */
17
44
  export function InitializeAuthProviders(): void {
45
+ AssertLocalDevAuthAllowed();
46
+
18
47
  const factory = AuthProviderFactory.Instance;
19
48
 
20
49
  // Clear any existing providers
@@ -40,6 +69,19 @@ export function InitializeAuthProviders(): void {
40
69
  }
41
70
  }
42
71
 
72
+ if (IsLocalDevAuthEnabled()) {
73
+ configInfo.authProviders = configInfo.authProviders ?? [];
74
+ if (!configInfo.authProviders.some((p) => p.type === 'local' || p.name === 'local')) {
75
+ const localConfig = LocalDevProvider.ConfigFromEnvironment({
76
+ ...process.env,
77
+ MJ_DEV_AUTH_ENABLED: 'true',
78
+ });
79
+ if (localConfig) {
80
+ configInfo.authProviders.push(localConfig);
81
+ }
82
+ }
83
+ }
84
+
43
85
  // Initialize providers from authProviders config
44
86
  if (configInfo.authProviders && configInfo.authProviders.length > 0) {
45
87
  for (const providerConfig of configInfo.authProviders) {