@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
@@ -21,7 +21,7 @@ vi.mock('type-graphql', () => {
21
21
  return Object.fromEntries(exportNames.map((name) => [name, decoratorFactory]));
22
22
  });
23
23
 
24
- const runViewCalls: Array<{ params: RunViewParams; contextUser: UserInfo | undefined }> = [];
24
+ const runViewCalls: Array<{ params: RunViewParams; contextUser: UserInfo | undefined; provider: unknown }> = [];
25
25
  /** Each RunViews call, as the list of entity names it batched. */
26
26
  const runViewsBatches: string[][] = [];
27
27
  let runViewHandler: RunViewHandler = () => ({ Success: true, Results: [] });
@@ -29,8 +29,12 @@ let runViewHandler: RunViewHandler = () => ({ Success: true, Results: [] });
29
29
  vi.mock('@memberjunction/core', async (importOriginal) => {
30
30
  const actual = await importOriginal<typeof import('@memberjunction/core')>();
31
31
  class MockRunView {
32
+ constructor(private readonly provider?: unknown) {}
33
+ static FromMetadataProvider(provider: unknown): MockRunView {
34
+ return new MockRunView(provider);
35
+ }
32
36
  async RunView(params: RunViewParams, contextUser?: UserInfo): Promise<ViewResult> {
33
- runViewCalls.push({ params, contextUser });
37
+ runViewCalls.push({ params, contextUser, provider: this.provider });
34
38
  return runViewHandler(params, contextUser);
35
39
  }
36
40
  async RunViews(params: RunViewParams[], contextUser?: UserInfo): Promise<ViewResult[]> {
@@ -60,9 +64,13 @@ import type { AppContext } from '../types.js';
60
64
  const DOC_ID = 'DOC-1';
61
65
  const contextUser = new UserInfo();
62
66
 
67
+ /** Entity read and field-level security the external-index path checks, per test. */
68
+ const access = { canRead: true, deniedReadFields: new Set<string>() };
69
+
63
70
  const membersEntity = {
64
71
  Name: 'Members',
65
72
  PrimaryKeys: [{ Name: 'ID' }],
73
+ FirstPrimaryKey: { Name: 'ID' },
66
74
  Fields: [
67
75
  { Name: 'ID', IsNameField: false },
68
76
  { Name: 'FirstName', IsNameField: true },
@@ -70,11 +78,16 @@ const membersEntity = {
70
78
  { Name: 'Email', IsNameField: false },
71
79
  ],
72
80
  NameField: undefined,
81
+ GetUserPermisions: (user: UserInfo) => ({ CanRead: user === contextUser && access.canRead }),
82
+ GetDeniedReadFields: (user: UserInfo) => (user === contextUser ? access.deniedReadFields : new Set<string>()),
73
83
  } as unknown as EntityInfo;
74
84
 
75
85
  const orderLinesEntity = {
76
86
  Name: 'Order Lines',
77
87
  PrimaryKeys: [{ Name: 'OrderID' }, { Name: 'LineNo' }],
88
+ FirstPrimaryKey: { Name: 'OrderID' },
89
+ GetUserPermisions: (user: UserInfo) => ({ CanRead: user === contextUser && access.canRead }),
90
+ GetDeniedReadFields: () => new Set<string>(),
78
91
  Fields: [
79
92
  { Name: 'OrderID', IsNameField: false },
80
93
  { Name: 'LineNo', IsNameField: false },
@@ -143,6 +156,19 @@ function erdView(rows: Array<Record<string, string>>) {
143
156
  };
144
157
  }
145
158
 
159
+ /**
160
+ * A fake `Members` view of the rows the caller may read (row-level security already applied):
161
+ * returns the requested `Fields` of the rows named by an `ID IN ('a','b')` filter.
162
+ */
163
+ function readableMembersView(readableIDs: string[]) {
164
+ return (params: RunViewParams): ViewResult => {
165
+ const idList = /^ID IN \((.*)\)$/.exec(params.ExtraFilter ?? '');
166
+ const requested = idList ? idList[1].split(',').map((v) => v.trim().replace(/^'|'$/g, '')) : [];
167
+ const rows = requested.filter((id) => readableIDs.includes(id)).map((id) => ({ ID: id }));
168
+ return { Success: true, Results: rows.map((r) => Object.fromEntries((params.Fields ?? ['ID']).map((f) => [f, r[f as 'ID']]))) };
169
+ };
170
+ }
171
+
146
172
  /** The VectorJSON fetches: Entity Record Documents views filtered by ID. */
147
173
  function vectorFetches() {
148
174
  return callsFor('MJ: Entity Record Documents').filter((c) => c.params.ExtraFilter?.startsWith('ID IN ('));
@@ -160,6 +186,8 @@ describe('FetchEntityVectorsResolver', () => {
160
186
  beforeEach(() => {
161
187
  runViewCalls.length = 0;
162
188
  runViewsBatches.length = 0;
189
+ access.canRead = true;
190
+ access.deniedReadFields = new Set<string>();
163
191
  });
164
192
 
165
193
  afterEach(() => {
@@ -202,6 +230,21 @@ describe('FetchEntityVectorsResolver', () => {
202
230
  expect(vectorCall.params.IgnoreMaxRows).toBe(true);
203
231
  });
204
232
 
233
+ it("REGRESSION: runs every view on the request's provider", async () => {
234
+ useVectorDB(true);
235
+ routeViews('Members', {
236
+ 'MJ: Entity Record Documents': erdView(storedRows),
237
+ Members: () => ({ Success: true, Results: memberRows }),
238
+ });
239
+ const context = appContext(membersEntity);
240
+
241
+ await new FetchEntityVectorsResolver().FetchEntityVectors(DOC_ID, undefined, undefined, context);
242
+
243
+ const requestProvider = context.providers[0].provider;
244
+ expect(callsFor('MJ: Entity Record Documents')).toHaveLength(2);
245
+ expect(runViewCalls.every((c) => c.provider === requestProvider)).toBe(true);
246
+ });
247
+
205
248
  it('bypasses the server RunView cache for both Entity Record Documents views, so Fields is honoured', async () => {
206
249
  useVectorDB(true);
207
250
  routeViews('Members', {
@@ -413,7 +456,7 @@ describe('FetchEntityVectorsResolver', () => {
413
456
  describe('index-keyed providers (Pinecone path, unchanged)', () => {
414
457
  it('queries the provider index by name and never reads Entity Record Documents', async () => {
415
458
  const { queryIndex } = useVectorDB(false);
416
- routeViews('Members', {});
459
+ routeViews('Members', { Members: readableMembersView(['a']) });
417
460
 
418
461
  const result = await new FetchEntityVectorsResolver().FetchEntityVectors(DOC_ID, 50, undefined, appContext(membersEntity));
419
462
 
@@ -427,7 +470,167 @@ describe('FetchEntityVectorsResolver', () => {
427
470
  expect(query.vector).toHaveLength(2);
428
471
  expect(result.Results).toEqual([{ ID: 'pc-1', Values: [0.5, 0.5], Metadata: JSON.stringify({ Entity: 'Members', RecordID: 'a' }) }]);
429
472
  expect(callsFor('MJ: Entity Record Documents')).toHaveLength(0);
473
+ });
474
+
475
+ it('removes metadata keys naming fields the caller may not read', async () => {
476
+ const { queryIndex } = useVectorDB(false);
477
+ queryIndex.mockResolvedValueOnce({
478
+ success: true,
479
+ data: { matches: [{ id: 'pc-1', values: [0.5, 0.5], metadata: { Entity: 'Members', RecordID: 'a', FirstName: 'Ada', Email: 'ada@secret.org' } }] },
480
+ });
481
+ access.deniedReadFields = new Set(['email']);
482
+ routeViews('Members', { Members: readableMembersView(['a']) });
483
+
484
+ const result = await new FetchEntityVectorsResolver().FetchEntityVectors(DOC_ID, 50, undefined, appContext(membersEntity));
485
+
486
+ expect(result.Success).toBe(true);
487
+ expect(JSON.parse(result.Results[0].Metadata)).toEqual({ Entity: 'Members', RecordID: 'a', FirstName: 'Ada' });
488
+ });
489
+
490
+ it('keeps every metadata key for a caller denied nothing', async () => {
491
+ const { queryIndex } = useVectorDB(false);
492
+ const metadata = { Entity: 'Members', RecordID: 'a', FirstName: 'Ada', Email: 'ada@secret.org' };
493
+ queryIndex.mockResolvedValueOnce({ success: true, data: { matches: [{ id: 'pc-1', values: [0.5, 0.5], metadata }] } });
494
+ routeViews('Members', { Members: readableMembersView(['a']) });
495
+
496
+ const result = await new FetchEntityVectorsResolver().FetchEntityVectors(DOC_ID, 50, undefined, appContext(membersEntity));
497
+
498
+ expect(JSON.parse(result.Results[0].Metadata)).toEqual(metadata);
499
+ });
500
+
501
+ it('refuses, without querying the index, a caller who may not read the source entity', async () => {
502
+ const { queryIndex } = useVectorDB(false);
503
+ access.canRead = false;
504
+ routeViews('Members', {});
505
+
506
+ const result = await new FetchEntityVectorsResolver().FetchEntityVectors(DOC_ID, 50, undefined, appContext(membersEntity));
507
+
508
+ expect(result.Success).toBe(false);
509
+ expect(result.Results).toEqual([]);
510
+ expect(result.ErrorMessage).toBe('Entity "Members" does not exist or you do not have permission to read it');
511
+ expect(queryIndex).not.toHaveBeenCalled();
512
+ });
513
+ });
514
+
515
+ describe('index-keyed providers: rows the caller cannot read', () => {
516
+ function matchesFor(recordIDs: Array<string | undefined>) {
517
+ return recordIDs.map((recordID, i) => ({
518
+ id: `pc-${i}`,
519
+ values: [i, i],
520
+ score: 1 - i / 10,
521
+ metadata: recordID === undefined ? { Entity: 'Members' } : { Entity: 'Members', RecordID: recordID },
522
+ }));
523
+ }
524
+
525
+ it('drops matches whose record the caller cannot read, keeping the order of the rest', async () => {
526
+ const { queryIndex } = useVectorDB(false);
527
+ queryIndex.mockResolvedValueOnce({ success: true, data: { matches: matchesFor(['ID|aaa', 'ID|bbb', 'ID|ccc']) } });
528
+ routeViews('Members', { Members: readableMembersView(['ccc', 'aaa']) });
529
+
530
+ const result = await new FetchEntityVectorsResolver().FetchEntityVectors(DOC_ID, 50, undefined, appContext(membersEntity));
531
+
532
+ expect(result.Success).toBe(true);
533
+ expect(result.Results.map((r) => r.ID)).toEqual(['pc-0', 'pc-2']);
534
+ expect(result.Results.map((r) => r.Values)).toEqual([[0, 0], [2, 2]]);
535
+ expect(result.TotalCount).toBe(2);
536
+ });
537
+
538
+ it("REGRESSION: runs every view, the key check included, on the request's provider", async () => {
539
+ const { queryIndex } = useVectorDB(false);
540
+ queryIndex.mockResolvedValueOnce({ success: true, data: { matches: matchesFor(['ID|aaa']) } });
541
+ routeViews('Members', { Members: readableMembersView(['aaa']) });
542
+ const context = appContext(membersEntity);
543
+
544
+ await new FetchEntityVectorsResolver().FetchEntityVectors(DOC_ID, 50, undefined, context);
545
+
546
+ const requestProvider = context.providers[0].provider;
547
+ expect(callsFor('Members')).toHaveLength(1);
548
+ expect(runViewCalls.length).toBeGreaterThan(1);
549
+ expect(runViewCalls.every((c) => c.provider === requestProvider)).toBe(true);
550
+ });
551
+
552
+ it('checks the matched keys with a simple view of the primary key, as the caller', async () => {
553
+ const { queryIndex } = useVectorDB(false);
554
+ queryIndex.mockResolvedValueOnce({ success: true, data: { matches: matchesFor(['ID|aaa', 'bbb']) } });
555
+ routeViews('Members', { Members: readableMembersView(['aaa', 'bbb']) });
556
+
557
+ const result = await new FetchEntityVectorsResolver().FetchEntityVectors(DOC_ID, 50, undefined, appContext(membersEntity));
558
+
559
+ expect(result.Results.map((r) => r.ID)).toEqual(['pc-0', 'pc-1']);
560
+ const [check] = callsFor('Members');
561
+ expect(check.contextUser).toBe(contextUser);
562
+ expect(check.params.ExtraFilter).toBe("ID IN ('aaa','bbb')");
563
+ expect(check.params.Fields).toEqual(['ID']);
564
+ expect(check.params.ResultType).toBe('simple');
565
+ expect(check.params.IgnoreMaxRows).toBe(true);
566
+ });
567
+
568
+ it('drops a match with no record ID, or one naming a field that is not the primary key, without querying for it', async () => {
569
+ const { queryIndex } = useVectorDB(false);
570
+ queryIndex.mockResolvedValueOnce({ success: true, data: { matches: matchesFor([undefined, "Salary|1' OR 1=1 --", 'ID|aaa']) } });
571
+ routeViews('Members', { Members: readableMembersView(['aaa']) });
572
+
573
+ const result = await new FetchEntityVectorsResolver().FetchEntityVectors(DOC_ID, 50, undefined, appContext(membersEntity));
574
+
575
+ expect(result.Results.map((r) => r.ID)).toEqual(['pc-2']);
576
+ expect(callsFor('Members').map((c) => c.params.ExtraFilter)).toEqual(["ID IN ('aaa')"]);
577
+ });
578
+
579
+ it('returns no matches and runs no check when none carries a usable record ID', async () => {
580
+ const { queryIndex } = useVectorDB(false);
581
+ queryIndex.mockResolvedValueOnce({ success: true, data: { matches: matchesFor([undefined]) } });
582
+ routeViews('Members', { Members: readableMembersView([]) });
583
+
584
+ const result = await new FetchEntityVectorsResolver().FetchEntityVectors(DOC_ID, 50, undefined, appContext(membersEntity));
585
+
586
+ expect(result.Success).toBe(true);
587
+ expect(result.Results).toEqual([]);
430
588
  expect(callsFor('Members')).toHaveLength(0);
431
589
  });
590
+
591
+ it('checks composite keys with one clause per record, using the real key columns', async () => {
592
+ const { queryIndex } = useVectorDB(false);
593
+ queryIndex.mockResolvedValueOnce({
594
+ success: true,
595
+ data: { matches: [
596
+ { id: 'pc-a', values: [1], metadata: { Entity: 'Order Lines', RecordID: 'OrderID|o-1||LineNo|2' } },
597
+ { id: 'pc-b', values: [2], metadata: { Entity: 'Order Lines', RecordID: 'OrderID|o-2||LineNo|1' } },
598
+ ] },
599
+ });
600
+ routeViews('Order Lines', {
601
+ 'Order Lines': () => ({ Success: true, Results: [{ OrderID: 'o-2', LineNo: 1 }] }),
602
+ });
603
+
604
+ const result = await new FetchEntityVectorsResolver().FetchEntityVectors(DOC_ID, 50, undefined, appContext(orderLinesEntity));
605
+
606
+ expect(result.Results.map((r) => r.ID)).toEqual(['pc-b']);
607
+ const [check] = callsFor('Order Lines');
608
+ expect(check.params.ExtraFilter).toBe("(OrderID='o-1' AND LineNo='2') OR (OrderID='o-2' AND LineNo='1')");
609
+ expect(check.params.Fields).toEqual(['OrderID', 'LineNo']);
610
+ });
611
+
612
+ it('splits a large key check into lists of at most 1000, sent in one RunViews batch', async () => {
613
+ const { queryIndex } = useVectorDB(false);
614
+ const ids = Array.from({ length: 1500 }, (_, i) => `id-${i}`);
615
+ queryIndex.mockResolvedValueOnce({ success: true, data: { matches: matchesFor(ids.map((id) => `ID|${id}`)) } });
616
+ routeViews('Members', { Members: readableMembersView(ids) });
617
+
618
+ const result = await new FetchEntityVectorsResolver().FetchEntityVectors(DOC_ID, 1500, undefined, appContext(membersEntity));
619
+
620
+ expect(result.TotalCount).toBe(1500);
621
+ expect(callsFor('Members').map((c) => (c.params.ExtraFilter ?? '').split(',').length)).toEqual([1000, 500]);
622
+ expect(runViewsBatches).toEqual([['Members', 'Members']]);
623
+ });
624
+
625
+ it('returns an error result when the key check fails', async () => {
626
+ const { queryIndex } = useVectorDB(false);
627
+ queryIndex.mockResolvedValueOnce({ success: true, data: { matches: matchesFor(['ID|aaa']) } });
628
+ routeViews('Members', { Members: () => ({ Success: false, Results: [], ErrorMessage: 'timeout' }) });
629
+
630
+ const result = await new FetchEntityVectorsResolver().FetchEntityVectors(DOC_ID, 50, undefined, appContext(membersEntity));
631
+
632
+ expect(result.Success).toBe(false);
633
+ expect(result.Results).toEqual([]);
634
+ });
432
635
  });
433
636
  });
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Field-level security on FileResolver's own create path and responses.
3
+ *
4
+ * `CreateFile` applies its input with `NewRecord` rather than through the generated create
5
+ * resolver, so it gates the input itself: a restricted caller's key for a field it may not read,
6
+ * or a key that names no field, is refused with the same message, before any storage work. The
7
+ * `File` it returns (and `UploadStorageFile`'s) is the record just saved or reloaded, which is
8
+ * projected for the caller. The file entity here stands in with the fixture's restricted fields.
9
+ */
10
+ import 'reflect-metadata';
11
+ import { describe, it, expect, vi } from 'vitest';
12
+
13
+ const mocks = vi.hoisted(() => ({ createUploadUrl: vi.fn() }));
14
+
15
+ vi.mock('@memberjunction/server', () => {
16
+ const noopDecorator = () => () => undefined;
17
+ return {
18
+ Arg: noopDecorator, Ctx: noopDecorator, Field: noopDecorator, FieldResolver: noopDecorator, InputType: noopDecorator,
19
+ Mutation: noopDecorator, ObjectType: noopDecorator, PubSub: noopDecorator, Query: noopDecorator, Resolver: noopDecorator,
20
+ Root: noopDecorator, Int: {}, Float: {}, DeleteOptionsInput: class {},
21
+ };
22
+ });
23
+ vi.mock('../generated/generated.js', () => ({
24
+ CreateMJFileInput: class {},
25
+ UpdateMJFileInput: class {},
26
+ MJFile_: class {},
27
+ MJFileResolver: class {
28
+ protected GetUserFromPayload(userPayload: { userRecord?: object } | undefined) {
29
+ return userPayload?.userRecord;
30
+ }
31
+ },
32
+ }));
33
+ vi.mock('@memberjunction/ai-agents', () => ({ readRealtimeRecordingFile: vi.fn() }));
34
+ vi.mock('@memberjunction/graphql-dataprovider', () => ({ FieldMapper: class { MapFields(o: object) { return o; } } }));
35
+ vi.mock('../config.js', () => ({ configInfo: {} }));
36
+ vi.mock('../rest/MediaAccessKeys.js', () => ({ MediaAccessKeyManager: { Instance: {} } }));
37
+ vi.mock('../rest/UploadTokenManager.js', () => ({ UploadTokenManager: { Instance: { Consume: vi.fn() } } }));
38
+ vi.mock('@memberjunction/storage', async () => {
39
+ const actual = await vi.importActual<typeof import('@memberjunction/storage')>('@memberjunction/storage');
40
+ return { ...actual, createUploadUrl: mocks.createUploadUrl };
41
+ });
42
+
43
+ import type { PubSubEngine } from '@memberjunction/server';
44
+ import { FileResolver } from '../resolvers/FileResolver.js';
45
+ import type { AppContext } from '../types.js';
46
+ import { EmployeeEntity, FullWidthEmployeeRow, Intern, NoRecordChangesMetadata, RefusalFor, SECRET_SALARY } from './fieldSecurityFixtures.js';
47
+
48
+ function fileEntity() {
49
+ return { EntityInfo: EmployeeEntity(), CheckPermissions: vi.fn(), NewRecord: vi.fn(() => true), GetAll: () => FullWidthEmployeeRow() };
50
+ }
51
+
52
+ function contextFor(entity: ReturnType<typeof fileEntity>): AppContext {
53
+ const provider = { GetEntityObject: vi.fn(async () => entity), RunView: vi.fn(), RunViews: vi.fn() };
54
+ return { providers: [{ provider, type: 'Read-Write' }], userPayload: { userRecord: Intern() } } as unknown as AppContext;
55
+ }
56
+
57
+ describe('FileResolver.CreateFile: client input is gated for a restricted caller', () => {
58
+ it.each([
59
+ ['a denied field', 'Salary'],
60
+ ['a key that names no field', 'Bogus'],
61
+ ])('REGRESSION: refuses %s with the same message, before any storage work', async (_label, key) => {
62
+ const entity = fileEntity();
63
+ const input = { Name: 'new.pdf', [key]: 1 }; // keys of the stand-in entity
64
+ await expect(new FileResolver().CreateFile(input as never, contextFor(entity), {} as PubSubEngine))
65
+ .rejects.toThrow(RefusalFor(key));
66
+ expect(entity.NewRecord).not.toHaveBeenCalled();
67
+ expect(mocks.createUploadUrl).not.toHaveBeenCalled();
68
+ });
69
+ });
70
+
71
+ describe('FileResolver responses are projected for the caller', () => {
72
+ it('REGRESSION: the File returned never carries a denied value of a full-width record', () => {
73
+ const respond = (FileResolver.prototype as unknown as {
74
+ fileResponse(entity: ReturnType<typeof fileEntity>, user: unknown, provider: unknown): Record<string, unknown>;
75
+ }).fileResponse;
76
+
77
+ const file = respond.call(new FileResolver(), fileEntity(), Intern(), NoRecordChangesMetadata);
78
+
79
+ expect(JSON.stringify(file)).not.toContain(String(SECRET_SALARY));
80
+ expect(file).toMatchObject({ ID: 'emp-1', Name: 'Ada' });
81
+ });
82
+ });
@@ -0,0 +1,58 @@
1
+ import 'reflect-metadata';
2
+ import { describe, it, expect, vi } from 'vitest';
3
+
4
+ vi.mock('type-graphql', () => {
5
+ const noopDecorator = () => () => undefined;
6
+ return {
7
+ Resolver: noopDecorator,
8
+ Mutation: noopDecorator,
9
+ Query: noopDecorator,
10
+ Subscription: noopDecorator,
11
+ ObjectType: noopDecorator,
12
+ InputType: noopDecorator,
13
+ Field: noopDecorator,
14
+ Arg: noopDecorator,
15
+ Args: noopDecorator,
16
+ Ctx: noopDecorator,
17
+ PubSub: noopDecorator,
18
+ Root: noopDecorator,
19
+ Float: class {},
20
+ Int: class {},
21
+ ID: class {},
22
+ };
23
+ });
24
+
25
+ import { PipelineProgressFilter, type PipelineProgressNotification } from '../resolvers/PipelineProgressResolver';
26
+
27
+ const RUN_ID = 'AA11BB22-0000-4000-8000-000000000001';
28
+ const OTHER_RUN_ID = 'CC33DD44-0000-4000-8000-000000000002';
29
+
30
+ function makePayload(pipelineRunID: string): PipelineProgressNotification {
31
+ return {
32
+ PipelineRunID: pipelineRunID,
33
+ Stage: 'extract',
34
+ TotalItems: 100,
35
+ ProcessedItems: 25,
36
+ ElapsedMs: 500,
37
+ PercentComplete: 25,
38
+ };
39
+ }
40
+
41
+ describe('PipelineProgressFilter', () => {
42
+ it('delivers when PipelineRunID matches exactly', () => {
43
+ expect(PipelineProgressFilter({ payload: makePayload(RUN_ID), args: { pipelineRunID: RUN_ID } })).toBe(true);
44
+ });
45
+
46
+ it('delivers when PipelineRunID differs only in UUID casing (SQL Server uppercase vs client lowercase)', () => {
47
+ expect(
48
+ PipelineProgressFilter({
49
+ payload: makePayload(RUN_ID.toUpperCase()),
50
+ args: { pipelineRunID: RUN_ID.toLowerCase() },
51
+ })
52
+ ).toBe(true);
53
+ });
54
+
55
+ it('rejects notifications for a different pipeline run', () => {
56
+ expect(PipelineProgressFilter({ payload: makePayload(OTHER_RUN_ID), args: { pipelineRunID: RUN_ID } })).toBe(false);
57
+ });
58
+ });
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The saved query that `CreateQueryExtended` / `UpdateQueryExtended` return is mapped for the
3
+ * CALLER. These mutations are reachable with the `query:create` / `query:update` API-key scopes,
4
+ * not only by the system user, and the record they return is the one just saved, whose re-read
5
+ * holds every column. Mapping it with no context user skipped the field-security projection
6
+ * altogether, so the caller received columns its roles may not read.
7
+ *
8
+ * The base resolver is stubbed with the real projection (`ProjectValuesForCaller`) standing in
9
+ * for its field mapping, so what is pinned is the identity the result is mapped for.
10
+ */
11
+ import 'reflect-metadata';
12
+ import { describe, it, expect, vi } from 'vitest';
13
+
14
+ vi.mock('type-graphql', () => {
15
+ const decoratorFactory = (..._args: unknown[]) => (..._decorated: unknown[]) => undefined;
16
+ const exportNames = ['Arg', 'Ctx', 'Field', 'InputType', 'Mutation', 'ObjectType', 'Resolver', 'PubSub', 'Int', 'Float'];
17
+ return { ...Object.fromEntries(exportNames.map((name) => [name, decoratorFactory])), registerEnumType: () => undefined };
18
+ });
19
+ vi.mock('@memberjunction/core-entities-server', () => ({ MJQueryEntityServer: class {} }));
20
+ vi.mock('../generic/DeleteOptionsInput.js', () => ({ DeleteOptionsInput: class {} }));
21
+ vi.mock('../generated/generated.js', async () => {
22
+ const { ProjectValuesForCaller } = await import('../generic/fieldSecurityTransport.js');
23
+ const { EmployeeEntity, NoRecordChangesMetadata } = await import('./fieldSecurityFixtures.js');
24
+ return {
25
+ MJQuery_: class {}, MJQueryField_: class {}, MJQueryParameter_: class {}, MJQueryEntity_: class {}, MJQueryPermission_: class {},
26
+ MJQueryResolver: class {
27
+ protected async MapFieldNamesToCodeNames(_entityName: string, values: Record<string, unknown>, contextUser?: unknown) {
28
+ return ProjectValuesForCaller(EmployeeEntity(), values, contextUser as never, NoRecordChangesMetadata);
29
+ }
30
+ },
31
+ };
32
+ });
33
+
34
+ import { MJQueryResolverExtended } from '../resolvers/QuerySystemUserResolver.js';
35
+ import { FullWidthEmployeeRow, Intern, NoRecordChangesMetadata, SECRET_SALARY } from './fieldSecurityFixtures.js';
36
+
37
+ describe('QuerySystemUserResolver: the saved query is mapped for the caller', () => {
38
+ it('REGRESSION: buildSuccessResult never returns a column the caller may not read', async () => {
39
+ const build = (MJQueryResolverExtended.prototype as unknown as {
40
+ buildSuccessResult(entity: unknown, user: unknown, provider: unknown): Promise<{ Query: Record<string, unknown> }>;
41
+ }).buildSuccessResult;
42
+ const entity = { GetAll: () => FullWidthEmployeeRow(), QueryFields: [], QueryParameters: [], QueryEntities: [], QueryPermissions: [] };
43
+
44
+ const result = await build.call(new MJQueryResolverExtended(), entity, Intern(), NoRecordChangesMetadata);
45
+
46
+ expect(JSON.stringify(result.Query)).not.toContain(String(SECRET_SALARY));
47
+ expect(result.Query).toMatchObject({ ID: 'emp-1', Name: 'Ada' });
48
+ });
49
+ });
@@ -110,6 +110,7 @@ interface ReqShape {
110
110
  query?: Record<string, string>;
111
111
  body?: unknown;
112
112
  mjUser?: UserInfo;
113
+ timeZone?: string;
113
114
  }
114
115
 
115
116
  function makeReq(shape: ReqShape = {}): express.Request {
@@ -118,6 +119,7 @@ function makeReq(shape: ReqShape = {}): express.Request {
118
119
  query: shape.query ?? {},
119
120
  body: shape.body ?? {},
120
121
  mjUser: shape.mjUser ?? MJ_USER,
122
+ userPayload: { TimeZone: shape.timeZone },
121
123
  } as unknown as express.Request;
122
124
  }
123
125
 
@@ -540,6 +542,17 @@ describe('RESTEndpointHandler view routes', () => {
540
542
  expect(jsonBody()).toEqual({ Success: true, Results: [{ ID: '1' }] });
541
543
  });
542
544
 
545
+ it('drops fields only server code may set, takes the time zone from the header, and keeps the route\'s entity over the body\'s', async () => {
546
+ mockOpsRunView.mockResolvedValue({ success: true, result: { Success: true, Results: [] } });
547
+ const handler = makeHandler();
548
+ const req = makeReq({ params: { entityName: 'Users' }, body: { EntityName: 'Secrets', TimeZone: 'Asia/Tokyo', MaxRows: 5 }, timeZone: 'Europe/Paris' });
549
+ const { res } = makeRes();
550
+
551
+ await handler.runView(req, res);
552
+
553
+ expect(mockOpsRunView).toHaveBeenCalledWith({ EntityName: 'Users', MaxRows: 5, TimeZone: 'Europe/Paris' }, MJ_USER);
554
+ });
555
+
543
556
  it('maps view-layer failures to 400', async () => {
544
557
  mockOpsRunView.mockResolvedValue({ success: false, error: 'does not have permission' });
545
558
  const handler = makeHandler();
@@ -591,6 +604,18 @@ describe('RESTEndpointHandler view routes', () => {
591
604
  expect(jsonBody()).toEqual([{ Success: true, Results: [] }]);
592
605
  });
593
606
 
607
+ it('drops fields only server code may set from every batched view', async () => {
608
+ mockOpsRunViews.mockResolvedValue({ success: true, results: [{ Success: true, Results: [] }] });
609
+ const handler = makeHandler();
610
+ const req = makeReq({ body: { params: [{ EntityName: 'Users', TimeZone: 'Asia/Tokyo' }] }, timeZone: 'Europe/Paris' });
611
+ const { res } = makeRes();
612
+
613
+ await handler.runViews(req, res);
614
+
615
+ const [sent] = mockOpsRunViews.mock.calls[0] as [RunViewParams[], UserInfo];
616
+ expect(sent).toEqual([{ EntityName: 'Users', TimeZone: 'Europe/Paris' }]);
617
+ });
618
+
594
619
  it('maps view-layer batch failures to 400', async () => {
595
620
  mockOpsRunViews.mockResolvedValue({ success: false, error: 'batch failed' });
596
621
  const handler = makeHandler();
@@ -122,7 +122,14 @@ function makeSession(config: Record<string, unknown>, overrides: Partial<FakeEnt
122
122
  }
123
123
 
124
124
  function makeDetail(): FakeEntity {
125
- return { ID: 'c3c3c3c3-0000-4000-8000-0000000000d1', Save: vi.fn(async () => true), Load: vi.fn(async () => true), NewRecord: vi.fn() };
125
+ return {
126
+ ID: 'c3c3c3c3-0000-4000-8000-0000000000d1',
127
+ ExternalID: 'c3-shared-resource',
128
+ Save: vi.fn(async () => true),
129
+ Load: vi.fn(async () => true),
130
+ NewRecord: vi.fn(),
131
+ MarkServerAuthoredReply: vi.fn(),
132
+ };
126
133
  }
127
134
 
128
135
  /** A provider serving the session plus view rows per entity name; `runView` records every view run. */
@@ -167,9 +174,22 @@ function relayTool(resolver: RealtimeClientSessionResolver, toolName: string, ar
167
174
  }
168
175
 
169
176
  /** A scoped anonymous magic-link caller: the relay runs its work as the system user. */
170
- const SCOPED_ANONYMOUS = { ID: USER_ID, Email: 'guest@magic-link.local', IsMagicLinkAnonymous: true, MagicLinkScope: { ResourceID: 'c3-scope' } };
177
+ const SCOPED_ANONYMOUS = {
178
+ ID: USER_ID,
179
+ Email: 'guest@magic-link.local',
180
+ IsMagicLinkAnonymous: true,
181
+ AnonymousVisitorID: 'c3-redemption-sid',
182
+ // The session's conversation is stamped with the caller's resource scope (ResolveSessionScopeIdentity).
183
+ MagicLinkScope: { ResourceID: 'c3-shared-resource' },
184
+ };
171
185
  /** A public web-widget guest: anonymous, but not elevated. */
172
- const WIDGET_GUEST = { ID: USER_ID, Email: 'guest@widget.local', IsMagicLinkAnonymous: true, WidgetGuestContext: { WidgetID: 'c3c3c3c3-0000-4000-8000-0000000000b1' } };
186
+ const WIDGET_GUEST = {
187
+ ID: USER_ID,
188
+ Email: 'guest@widget.local',
189
+ IsMagicLinkAnonymous: true,
190
+ AnonymousVisitorID: 'c3-shared-resource',
191
+ WidgetGuestContext: { WidgetID: 'c3c3c3c3-0000-4000-8000-0000000000b1' },
192
+ };
173
193
  /** A resource-scoped magic-link session: a normal role pinned to one shared resource. */
174
194
  const RESOURCE_SCOPED = { ID: USER_ID, Email: 'invitee@example.com', MagicLinkScope: { ResourceID: 'c3-shared-resource' } };
175
195
  const SYSTEM_USER = { ID: 'c3c3c3c3-0000-4000-8000-0000000000aa', Email: 'system@example.com' } as UserInfo;