@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,196 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import type { Request } from 'express';
3
+ import jwt from 'jsonwebtoken';
4
+ import { AuthProviderFactory } from '@memberjunction/auth-providers';
5
+ import { UserInfo } from '@memberjunction/core';
6
+
7
+ vi.mock('../config.js', () => ({
8
+ configInfo: {
9
+ devAuth: { enabled: false },
10
+ magicLink: {},
11
+ authProviders: [],
12
+ },
13
+ }));
14
+
15
+ import {
16
+ AssertLocalDevAuthAllowed,
17
+ IsLocalDevAuthEnabled,
18
+ IsLoopbackAddress,
19
+ MintLocalDevJwt,
20
+ RegisterLocalDevAuthProvider,
21
+ VerifyLocalhostRequest,
22
+ } from '../auth/localDevAuth.js';
23
+ import { MagicLinkKeyManager } from '../auth/magicLink/MagicLinkKeys.js';
24
+
25
+ function createMockRequest(overrides: {
26
+ remoteAddress?: string;
27
+ host?: string;
28
+ origin?: string;
29
+ referer?: string;
30
+ xff?: string;
31
+ }): Request {
32
+ const headers: Record<string, string | undefined> = {
33
+ host: overrides.host ?? 'localhost:4000',
34
+ origin: overrides.origin,
35
+ referer: overrides.referer,
36
+ 'x-forwarded-for': overrides.xff,
37
+ };
38
+ return {
39
+ headers,
40
+ hostname: (overrides.host ?? 'localhost:4000').split(':')[0],
41
+ ip: overrides.remoteAddress ?? '127.0.0.1',
42
+ socket: {
43
+ remoteAddress: overrides.remoteAddress ?? '127.0.0.1',
44
+ },
45
+ } as unknown as Request;
46
+ }
47
+
48
+ describe('localDevAuth security invariants', () => {
49
+ beforeEach(() => {
50
+ AuthProviderFactory.Instance.clear();
51
+ });
52
+
53
+ it('IsLocalDevAuthEnabled detects MJ_DEV_AUTH_ENABLED=true or 1', () => {
54
+ expect(IsLocalDevAuthEnabled({})).toBe(false);
55
+ expect(IsLocalDevAuthEnabled({ MJ_DEV_AUTH_ENABLED: 'false' })).toBe(false);
56
+ expect(IsLocalDevAuthEnabled({ MJ_DEV_AUTH_ENABLED: 'true' })).toBe(true);
57
+ expect(IsLocalDevAuthEnabled({ MJ_DEV_AUTH_ENABLED: '1' })).toBe(true);
58
+ });
59
+
60
+ it('AssertLocalDevAuthAllowed fails loudly when NODE_ENV=production', () => {
61
+ expect(() =>
62
+ AssertLocalDevAuthAllowed({
63
+ MJ_DEV_AUTH_ENABLED: 'true',
64
+ NODE_ENV: 'development',
65
+ })
66
+ ).not.toThrow();
67
+
68
+ expect(() =>
69
+ AssertLocalDevAuthAllowed({
70
+ MJ_DEV_AUTH_ENABLED: 'true',
71
+ NODE_ENV: 'production',
72
+ })
73
+ ).toThrow(/strictly forbidden when NODE_ENV=production/);
74
+ });
75
+
76
+ it('IsLoopbackAddress accepts loopback and conditional Docker local bridge IPs', () => {
77
+ expect(IsLoopbackAddress('127.0.0.1', false)).toBe(true);
78
+ expect(IsLoopbackAddress('::1', false)).toBe(true);
79
+ expect(IsLoopbackAddress('::ffff:127.0.0.1', false)).toBe(true);
80
+ expect(IsLoopbackAddress('192.168.65.1', false)).toBe(false);
81
+ expect(IsLoopbackAddress('192.168.65.1', true)).toBe(true);
82
+ expect(IsLoopbackAddress('172.18.0.1', true)).toBe(true);
83
+ expect(IsLoopbackAddress('192.168.1.42', true)).toBe(false);
84
+ expect(IsLoopbackAddress('203.0.113.10', true)).toBe(false);
85
+ });
86
+
87
+ it('VerifyLocalhostRequest allows genuine localhost requests and blocks non-localhost vectors', () => {
88
+ // 1. Standard localhost IPv4 and IPv6
89
+ expect(
90
+ VerifyLocalhostRequest(
91
+ createMockRequest({ remoteAddress: '127.0.0.1', host: 'localhost:4000', origin: 'http://localhost:4202' }),
92
+ { NODE_ENV: 'development' }
93
+ ).Allowed
94
+ ).toBe(true);
95
+
96
+ expect(
97
+ VerifyLocalhostRequest(
98
+ createMockRequest({ remoteAddress: '::1', host: '127.0.0.1:4000', origin: 'http://127.0.0.1:4202' }),
99
+ { NODE_ENV: 'development' }
100
+ ).Allowed
101
+ ).toBe(true);
102
+
103
+ // 2. Docker Desktop host-gateway when Host is localhost, inside container, and no XFF is present
104
+ expect(
105
+ VerifyLocalhostRequest(
106
+ createMockRequest({ remoteAddress: '::ffff:192.168.65.1', host: 'localhost:4000' }),
107
+ { NODE_ENV: 'development', MJ_DEV_AUTH_ALLOW_DOCKER_BRIDGE: 'true' }
108
+ ).Allowed
109
+ ).toBe(true);
110
+
111
+ // 2b. Rejects Docker bridge IPs when NOT running inside a container
112
+ expect(
113
+ VerifyLocalhostRequest(
114
+ createMockRequest({ remoteAddress: '172.18.0.1', host: 'localhost:4000' }),
115
+ { NODE_ENV: 'development', MJ_DEV_AUTH_ALLOW_DOCKER_BRIDGE: 'false' }
116
+ ).Allowed
117
+ ).toBe(false);
118
+
119
+ // 2c. Rejects non-gateway RFC1918 172.16.0.0/12 addresses even inside a container
120
+ expect(IsLoopbackAddress('172.20.10.5', true)).toBe(false);
121
+
122
+ // 3. Rejects in production even from 127.0.0.1
123
+ expect(
124
+ VerifyLocalhostRequest(
125
+ createMockRequest({ remoteAddress: '127.0.0.1', host: 'localhost:4000' }),
126
+ { NODE_ENV: 'production' }
127
+ ).Allowed
128
+ ).toBe(false);
129
+
130
+ // 4. Rejects LAN IP on socket
131
+ expect(
132
+ VerifyLocalhostRequest(
133
+ createMockRequest({ remoteAddress: '192.168.1.50', host: 'localhost:4000' }),
134
+ { NODE_ENV: 'development' }
135
+ ).Allowed
136
+ ).toBe(false);
137
+
138
+ // 5. Rejects non-localhost Host header
139
+ expect(
140
+ VerifyLocalhostRequest(
141
+ createMockRequest({ remoteAddress: '127.0.0.1', host: '192.168.1.50:4000' }),
142
+ { NODE_ENV: 'development' }
143
+ ).Allowed
144
+ ).toBe(false);
145
+
146
+ // 6. Rejects external X-Forwarded-For
147
+ expect(
148
+ VerifyLocalhostRequest(
149
+ createMockRequest({ remoteAddress: '127.0.0.1', host: 'localhost:4000', xff: '203.0.113.5' }),
150
+ { NODE_ENV: 'development' }
151
+ ).Allowed
152
+ ).toBe(false);
153
+
154
+ // 7. Rejects external Origin
155
+ expect(
156
+ VerifyLocalhostRequest(
157
+ createMockRequest({
158
+ remoteAddress: '127.0.0.1',
159
+ host: 'localhost:4000',
160
+ origin: 'https://attacker.example.com',
161
+ }),
162
+ { NODE_ENV: 'development' }
163
+ ).Allowed
164
+ ).toBe(false);
165
+ });
166
+
167
+ it('MintLocalDevJwt mints an RS256 token verifiable by the registered LocalDevProvider', () => {
168
+ MagicLinkKeyManager.Instance.Initialize();
169
+ RegisterLocalDevAuthProvider('http://localhost:4000');
170
+
171
+ const mockUser = new UserInfo(undefined, {
172
+ ID: '11111111-2222-3333-4444-555555555555',
173
+ Name: 'Admin User',
174
+ Email: 'admin@memberjunction.org',
175
+ FirstName: 'Admin',
176
+ LastName: 'User',
177
+ IsActive: true,
178
+ Type: 'Owner',
179
+ UserRoles: [],
180
+ });
181
+
182
+ const { token, claims } = MintLocalDevJwt(mockUser, 'http://localhost:4000');
183
+ expect(claims.iss).toBe('http://localhost:4000/local-dev');
184
+ expect(claims.aud).toBe('mj-local-dev');
185
+ expect(claims.email).toBe('admin@memberjunction.org');
186
+ expect(claims['mj_magic_link']).toBeUndefined();
187
+
188
+ const provider = AuthProviderFactory.Instance.getByIssuer('http://localhost:4000/local-dev');
189
+ expect(provider).toBeDefined();
190
+ const decoded = jwt.decode(token) as jwt.JwtPayload;
191
+ const userInfo = provider!.extractUserInfo(decoded);
192
+ expect(userInfo.email).toBe('admin@memberjunction.org');
193
+ expect(userInfo.firstName).toBe('Admin');
194
+ expect(userInfo.lastName).toBe('User');
195
+ });
196
+ });
@@ -74,6 +74,7 @@ function writable(saveResult = true, loadedStatus = 'In-Progress') {
74
74
  ID: DETAIL_ID,
75
75
  Status: loadedStatus,
76
76
  Message: '',
77
+ MarkServerAuthoredReply: vi.fn(),
77
78
  Load: vi.fn(async () => true),
78
79
  Save: vi.fn(async () => saveResult),
79
80
  LatestResult: { CompleteMessage: 'denied' },
@@ -245,6 +246,15 @@ describe('ReconcileOrphanedConversationDetails', () => {
245
246
  );
246
247
  });
247
248
 
249
+ it("marks the close as the server's write: the reply gate refuses an unmarked change to a reply's status", async () => {
250
+ script([detail()], [{ ConversationDetailID: DETAIL_ID, Status: 'Failed', CompletedAt: longAgo() }]);
251
+
252
+ await ReconcileOrphanedConversationDetails(PROVIDER, USER);
253
+
254
+ expect(writableStore.last?.MarkServerAuthoredReply).toHaveBeenCalledOnce();
255
+ expect(writableStore.last?.Save).toHaveBeenCalled();
256
+ });
257
+
248
258
  it('leaves the detail alone when no owner can be resolved', async () => {
249
259
  script([detail()], [{ ConversationDetailID: DETAIL_ID, Status: 'Failed', CompletedAt: longAgo() }], null);
250
260
 
@@ -10,7 +10,7 @@
10
10
  */
11
11
  import 'reflect-metadata';
12
12
  import { describe, it, expect } from 'vitest';
13
- import { StatusUpdatesFilter, type PushStatusNotificationPayload, type StatusUpdatesFilterContext } from '../generic/PushStatusResolver';
13
+ import { StatusUpdatesFilter, StatusUpdateOwnerID, type PushStatusNotificationPayload, type StatusUpdatesFilterContext } from '../generic/PushStatusResolver';
14
14
  import type { UserPayload } from '../types';
15
15
 
16
16
  const OWNER = 'AA11BB22-0000-4000-8000-000000000001';
@@ -61,3 +61,59 @@ describe('statusUpdatesFilter (B49 session-hijack gate)', () => {
61
61
  });
62
62
  });
63
63
 
64
+
65
+ describe('StatusUpdateOwnerID (#4931)', () => {
66
+ it('is the request user, or the user an elevated payload stands in for', () => {
67
+ expect(StatusUpdateOwnerID({ sessionId: 's', email: 'e', userRecord: { ID: 'user-1' } } as UserPayload)).toBe('user-1');
68
+ expect(StatusUpdateOwnerID({ sessionId: 's', email: 'e', userRecord: { ID: 'system' }, ElevatedFrom: { ID: 'guest' } } as unknown as UserPayload)).toBe('guest');
69
+ expect(StatusUpdateOwnerID(undefined)).toBe('');
70
+ });
71
+ });
72
+
73
+ describe('statusUpdatesFilter — anonymous visitors share one user id (#4931)', () => {
74
+ const ANON = 'AAAAAAAA-0000-4000-8000-0000000000A0';
75
+ const visitor = (sid: string) => ({ ID: ANON, IsMagicLinkAnonymous: true, AnonymousVisitorID: sid, MagicLinkScope: { ResourceID: sid } });
76
+ const connection = (user: object): StatusUpdatesFilterContext => ({ userPayload: { sessionId: 'same-session', email: 'anonymous@magic-link.local', userRecord: user } as unknown as UserPayload });
77
+ // Visitor A's run, elevated to the system user: the event's owner comes from ElevatedFrom.
78
+ const aRun = { sessionId: 'same-session', email: 'system@example.com', userRecord: { ID: 'system' }, ElevatedFrom: visitor('sid-a') } as unknown as UserPayload;
79
+ const event = (): PushStatusNotificationPayload => ({ sessionId: 'same-session', ownerUserId: StatusUpdateOwnerID(aRun), message: 'complete' });
80
+
81
+ it("delivers visitor A's run events to visitor A's connection", () => {
82
+ expect(StatusUpdatesFilter({ payload: event(), args: { sessionId: 'same-session' }, context: connection(visitor('sid-a')) })).toBe(true);
83
+ });
84
+
85
+ it('does not deliver them to visitor B, even with the same (client-chosen) session id', () => {
86
+ expect(StatusUpdatesFilter({ payload: event(), args: { sessionId: 'same-session' }, context: connection(visitor('sid-b')) })).toBe(false);
87
+ });
88
+
89
+ it('does not deliver them to an anonymous connection with no scope identity (fail closed)', () => {
90
+ expect(StatusUpdatesFilter({ payload: event(), args: { sessionId: 'same-session' }, context: connection({ ID: ANON, IsMagicLinkAnonymous: true }) })).toBe(false);
91
+ });
92
+ });
93
+
94
+ describe('statusUpdatesFilter — an anonymous owner key matches exactly', () => {
95
+ // The scope part is a signed, case-significant identity (an mj_sid or a resource scope), so the anonymous key is
96
+ // compared exactly. A UUID comparison (case- and whitespace-insensitive) would let a visitor whose scope differs
97
+ // only in case or padding receive another visitor's run events.
98
+ const ANON = 'AAAAAAAA-0000-4000-8000-0000000000A0';
99
+ const visitor = (sid: string) => ({ ID: ANON, IsMagicLinkAnonymous: true, AnonymousVisitorID: sid, MagicLinkScope: { ResourceID: sid } });
100
+ const connection = (user: object): StatusUpdatesFilterContext => ({ userPayload: { sessionId: 's', email: 'anonymous@magic-link.local', userRecord: user } as UserPayload });
101
+ const eventOwnedBy = (sid: string): PushStatusNotificationPayload => ({
102
+ sessionId: 's',
103
+ ownerUserId: `${ANON.toLowerCase()}|${sid}`,
104
+ message: 'complete',
105
+ });
106
+
107
+ it('delivers to the visitor whose scope is identical', () => {
108
+ expect(StatusUpdatesFilter({ payload: eventOwnedBy('sid-a'), args: { sessionId: 's' }, context: connection(visitor('sid-a')) })).toBe(true);
109
+ });
110
+
111
+ it('does not deliver when the scopes differ only by case', () => {
112
+ expect(StatusUpdatesFilter({ payload: eventOwnedBy('SID-A'), args: { sessionId: 's' }, context: connection(visitor('sid-a')) })).toBe(false);
113
+ });
114
+
115
+ it('does not deliver when the scopes differ only by surrounding whitespace', () => {
116
+ expect(StatusUpdatesFilter({ payload: eventOwnedBy('sid-a '), args: { sessionId: 's' }, context: connection(visitor('sid-a')) })).toBe(false);
117
+ expect(StatusUpdatesFilter({ payload: eventOwnedBy('sid-a'), args: { sessionId: 's' }, context: connection(visitor(' sid-a')) })).toBe(false);
118
+ });
119
+ });
@@ -135,6 +135,10 @@ class FakeLedger implements IRealtimeRecordingLedger {
135
135
  throw new Error('not used by the routes');
136
136
  }
137
137
 
138
+ public async StampFile(): Promise<{ RecordingStatus: string | null }> {
139
+ throw new Error('not used by the routes');
140
+ }
141
+
138
142
  public async LoadShards(sessionID: string): Promise<LedgerShard[]> {
139
143
  return [...this.shardsOf(sessionID).values()].sort((a, b) => a.Index - b.Index)
140
144
  .map(s => ({ Index: s.Index, StartSample: s.StartSample, SampleCount: s.SampleCount, Data: s.Data }));
@@ -198,6 +202,7 @@ function callerLedger(inner: FakeLedger): IRealtimeRecordingLedger {
198
202
  ClaimAssembly: id => inner.ClaimAssembly(id),
199
203
  FinishAssembly: input => inner.FinishAssembly(input),
200
204
  StampAssemblyGaps: () => inner.StampAssemblyGaps(),
205
+ StampFile: () => inner.StampFile(),
201
206
  LoadShards: refuse,
202
207
  LoadShardMetadata: refuse,
203
208
  CountShardsBelow: refuse,
@@ -221,7 +226,7 @@ describe('realtime recording REST routes', () => {
221
226
  const app = express();
222
227
  app.use(REALTIME_RECORDING_MOUNT_PATH, CreateRealtimeRecordingRouter(deps));
223
228
  server = await new Promise<Server>(resolve => {
224
- const s = app.listen(0, () => resolve(s));
229
+ const s = app.listen(0, '127.0.0.1', () => resolve(s));
225
230
  });
226
231
  baseUrl = `http://127.0.0.1:${(server.address() as AddressInfo).port}${REALTIME_RECORDING_MOUNT_PATH}`;
227
232
  }
@@ -264,11 +269,15 @@ describe('realtime recording REST routes', () => {
264
269
  const user = {
265
270
  ID: id,
266
271
  IsMagicLinkAnonymous: req.header('x-test-anonymous') === 'true',
272
+ AnonymousVisitorID: scope ?? undefined,
267
273
  MagicLinkScope: scope ? { ResourceID: scope } : undefined,
268
274
  };
269
275
  return id ? { ContextUser: user as unknown as UserInfo, Provider: {} as unknown as DatabaseProviderBase } : null;
270
276
  },
271
- LoadConversationExternalID: async session => ledger.Sessions.get(session.ID)?.ConversationExternalID ?? null,
277
+ LoadConversationScope: async session => {
278
+ const row = ledger.Sessions.get(session.ID);
279
+ return { ExternalID: row?.ConversationExternalID ?? null, Readable: row?.ConversationExternalID !== undefined };
280
+ },
272
281
  LoadOwnedSession: async (agentSessionId, contextUser) => {
273
282
  const row = ledger.Sessions.get(agentSessionId);
274
283
  if (!row) {
@@ -391,6 +400,14 @@ describe('realtime recording REST routes', () => {
391
400
  expect((await post(`/${SESSION}/end?lastShardIndex=0&capturedSamples=${SHARD_SAMPLES}`, null, OWNER_ID, headers)).status).toBe(200);
392
401
  });
393
402
 
403
+ it('accepts a named owner holding a resource-scoped link on its own unstamped conversation, and refuses another stamp (#4931)', async () => {
404
+ const headers = { 'x-test-scope': 'res-1' };
405
+ addSession(SESSION, { ConversationExternalID: null });
406
+ expect((await post(`/${SESSION}/shard/0`, shardBytes(0), OWNER_ID, headers)).status).toBe(200);
407
+ addSession(SESSION, { ConversationExternalID: 'res-2' });
408
+ expect((await post(`/${SESSION}/shard/1`, shardBytes(1), OWNER_ID, headers)).status).toBe(403);
409
+ });
410
+
394
411
  it('leaves a named owner (no scope, not anonymous) unaffected', async () => {
395
412
  expect((await postShard(0)).status).toBe(200);
396
413
  });
@@ -27,7 +27,12 @@ function contextFor(user: Partial<UserInfo> & { ID: string }): RealtimeSessionEv
27
27
 
28
28
  const named = (id: string) => contextFor({ ID: id });
29
29
  const guest = (id: string, scope?: string) =>
30
- contextFor({ ID: id, IsMagicLinkAnonymous: true, MagicLinkScope: scope ? { ResourceID: scope } : undefined } as Partial<UserInfo> & { ID: string });
30
+ contextFor({
31
+ ID: id,
32
+ IsMagicLinkAnonymous: true,
33
+ AnonymousVisitorID: scope,
34
+ MagicLinkScope: scope ? { ResourceID: scope } : undefined,
35
+ } as Partial<UserInfo> & { ID: string });
31
36
 
32
37
  function event(over: Partial<RealtimeSessionEventTopicPayload> = {}): RealtimeSessionEventTopicPayload {
33
38
  return {
@@ -75,6 +80,18 @@ describe('RealtimeSessionEventsFilter (per-event gate)', () => {
75
80
  });
76
81
  });
77
82
 
83
+ describe('a named user holding a resource-scoped magic link (#4931)', () => {
84
+ const namedScoped = (scope: string) => contextFor({ ID: OWNER, MagicLinkScope: { ResourceID: scope } } as Partial<UserInfo> & { ID: string });
85
+
86
+ it('delivers its own session\'s events when the conversation carries no scope stamp', () => {
87
+ expect(filter(event({ ScopeKey: null }), SESSION_ID, namedScoped('res-1'))).toBe(true);
88
+ });
89
+
90
+ it('does not deliver events of a session stamped with its other link\'s scope', () => {
91
+ expect(filter(event({ ScopeKey: 'res-2' }), SESSION_ID, namedScoped('res-1'))).toBe(false);
92
+ });
93
+ });
94
+
78
95
  it('fails closed on a missing connection identity, owner or argument', () => {
79
96
  expect(filter(event(), SESSION_ID, undefined)).toBe(false);
80
97
  expect(filter(event(), SESSION_ID, { userPayload: undefined })).toBe(false);
@@ -98,6 +115,17 @@ describe('AuthorizeRealtimeSessionSubscription (subscribe-time gate)', () => {
98
115
  expect(d.LoadSession).toHaveBeenCalledWith(SESSION_ID, expect.objectContaining({ ID: OWNER }), expect.anything());
99
116
  });
100
117
 
118
+ it('lets a named scoped owner subscribe to its session on a readable, unstamped conversation, and refuses it when unreadable (#4931)', async () => {
119
+ const namedScoped = contextFor({ ID: OWNER, MagicLinkScope: { ResourceID: 'res-1' } } as Partial<UserInfo> & { ID: string });
120
+ const load = (readable: boolean): SubscriptionAuthorizationDeps => ({
121
+ LoadSession: vi.fn(async () =>
122
+ ({ Session: { ID: SESSION_ID, UserID: OWNER }, ConversationExternalID: null, ConversationReadable: readable } as unknown as LoadedSessionForAccess)),
123
+ GetProvider: () => ({}) as IMetadataProvider,
124
+ });
125
+ await expect(AuthorizeRealtimeSessionSubscription({ agentSessionId: SESSION_ID }, namedScoped, load(true))).resolves.toBe(SESSION_ID);
126
+ await expect(AuthorizeRealtimeSessionSubscription({ agentSessionId: SESSION_ID }, namedScoped, load(false))).rejects.toThrow('Realtime session not found.');
127
+ });
128
+
101
129
  it('refuses a non-owner with the uniform not-found message', async () => {
102
130
  await expect(AuthorizeRealtimeSessionSubscription({ agentSessionId: SESSION_ID }, named(STRANGER), deps({ UserID: OWNER, ExternalID: null }))).rejects.toThrow('Realtime session not found.');
103
131
  });
@@ -0,0 +1,31 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import type { IMetadataProvider, UserInfo } from '@memberjunction/core';
3
+ import { LoadOwnedRealtimeSession, RealtimeSessionOwnershipError } from '../../realtimeSessions/ownedRealtimeSession.js';
4
+
5
+ // Fixture UUIDs are assembled at runtime so the brief itself carries no record id.
6
+ const uuid = (a: string, b: string) => [a.repeat(8), b.repeat(4), '4' + b.repeat(3), '8' + b.repeat(3), b.repeat(12)].join('-');
7
+ const ANON = uuid('A', '0');
8
+ const SESSION = uuid('1', '1');
9
+ const CONVERSATION = uuid('2', '2');
10
+ const guest = (scope: string) =>
11
+ ({ ID: ANON, IsMagicLinkAnonymous: true, AnonymousVisitorID: scope, MagicLinkScope: { ResourceID: scope } } as unknown as UserInfo);
12
+
13
+ /** A session owned by the shared Anonymous user; its conversation is stamped with visitor 1's scope. */
14
+ function provider(): IMetadataProvider {
15
+ return {
16
+ GetEntityObject: vi.fn(async (entity: string) =>
17
+ entity === 'MJ: AI Agent Sessions'
18
+ ? { UserID: ANON, ConversationID: CONVERSATION, Load: vi.fn(async () => true) }
19
+ : { ExternalID: 'scope-visitor-1', Load: vi.fn(async () => true) }),
20
+ } as unknown as IMetadataProvider;
21
+ }
22
+
23
+ describe('LoadOwnedRealtimeSession with two anonymous visitors', () => {
24
+ it('allows the visitor whose scope matches', async () => {
25
+ await expect(LoadOwnedRealtimeSession(SESSION, guest('scope-visitor-1'), provider())).resolves.toBeDefined();
26
+ });
27
+ it('refuses a second anonymous visitor (same Anonymous user id, different scope)', async () => {
28
+ await expect(LoadOwnedRealtimeSession(SESSION, guest('scope-visitor-2'), provider()))
29
+ .rejects.toBeInstanceOf(RealtimeSessionOwnershipError);
30
+ });
31
+ });
@@ -0,0 +1,37 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import type { IMetadataProvider, UserInfo } from '@memberjunction/core';
3
+ import { LoadOwnedRealtimeSession, RealtimeSessionOwnershipError } from '../../realtimeSessions/ownedRealtimeSession.js';
4
+
5
+ // Fixture UUIDs are assembled at runtime so no literal record id appears in the source.
6
+ const uuid = (a: string, b: string) => [a.repeat(8), b.repeat(4), '4' + b.repeat(3), '8' + b.repeat(3), b.repeat(12)].join('-');
7
+ const NAMED = uuid('B', '1');
8
+ const SESSION = uuid('1', '1');
9
+ const CONVERSATION = uuid('2', '2');
10
+
11
+ /** A named magic-link user (found by email) holding a link scoped to `scope`. */
12
+ const namedScoped = (scope: string) => ({ ID: NAMED, IsMagicLinkAnonymous: false, MagicLinkScope: { ResourceID: scope } } as unknown as UserInfo);
13
+
14
+ /** The named user's own session; its conversation loads with `externalID`, or not at all when `readable` is false. */
15
+ function provider(externalID: string | null, readable = true): IMetadataProvider {
16
+ return {
17
+ GetEntityObject: vi.fn(async (entity: string) =>
18
+ entity === 'MJ: AI Agent Sessions'
19
+ ? { UserID: NAMED, ConversationID: CONVERSATION, Load: vi.fn(async () => true) }
20
+ : { ExternalID: externalID, Load: vi.fn(async () => readable) }),
21
+ } as unknown as IMetadataProvider;
22
+ }
23
+
24
+ describe('LoadOwnedRealtimeSession for a named user with a resource-scoped link', () => {
25
+ it('allows its own session on a conversation it can read that carries no scope stamp (one started from the Conversations app)', async () => {
26
+ await expect(LoadOwnedRealtimeSession(SESSION, namedScoped('res-1'), provider(null))).resolves.toBeDefined();
27
+ });
28
+ it('allows its own session on a conversation stamped with the same scope', async () => {
29
+ await expect(LoadOwnedRealtimeSession(SESSION, namedScoped('res-1'), provider('res-1'))).resolves.toBeDefined();
30
+ });
31
+ it('refuses its own session on a conversation stamped with another scope (a session from its other link)', async () => {
32
+ await expect(LoadOwnedRealtimeSession(SESSION, namedScoped('res-2'), provider('res-1'))).rejects.toBeInstanceOf(RealtimeSessionOwnershipError);
33
+ });
34
+ it('refuses when it cannot read the conversation (fail closed)', async () => {
35
+ await expect(LoadOwnedRealtimeSession(SESSION, namedScoped('res-1'), provider(null, false))).rejects.toBeInstanceOf(RealtimeSessionOwnershipError);
36
+ });
37
+ });