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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (401) hide show
  1. package/README.md +27 -1
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/agentSessions/SessionJanitor.d.ts +3 -3
  4. package/dist/agentSessions/SessionJanitor.js +3 -3
  5. package/dist/agentSessions/SessionManager.d.ts +15 -5
  6. package/dist/agentSessions/SessionManager.d.ts.map +1 -1
  7. package/dist/agentSessions/SessionManager.js +34 -12
  8. package/dist/agentSessions/SessionManager.js.map +1 -1
  9. package/dist/auth/AuthProviderCatalogRouter.d.ts.map +1 -1
  10. package/dist/auth/AuthProviderCatalogRouter.js +2 -0
  11. package/dist/auth/AuthProviderCatalogRouter.js.map +1 -1
  12. package/dist/auth/index.d.ts.map +1 -1
  13. package/dist/auth/index.js +51 -10
  14. package/dist/auth/index.js.map +1 -1
  15. package/dist/auth/initializeProviders.d.ts +10 -0
  16. package/dist/auth/initializeProviders.d.ts.map +1 -1
  17. package/dist/auth/initializeProviders.js +37 -1
  18. package/dist/auth/initializeProviders.js.map +1 -1
  19. package/dist/auth/localDevAuth.d.ts +79 -0
  20. package/dist/auth/localDevAuth.d.ts.map +1 -0
  21. package/dist/auth/localDevAuth.js +439 -0
  22. package/dist/auth/localDevAuth.js.map +1 -0
  23. package/dist/auth/magicLink/MagicLinkKeys.d.ts +2 -2
  24. package/dist/auth/magicLink/MagicLinkKeys.d.ts.map +1 -1
  25. package/dist/auth/magicLink/MagicLinkKeys.js +1 -1
  26. package/dist/auth/magicLink/MagicLinkKeys.js.map +1 -1
  27. package/dist/auth/magicLink/types.d.ts +12 -0
  28. package/dist/auth/magicLink/types.d.ts.map +1 -1
  29. package/dist/auth/sessionUserClone.d.ts +1 -1
  30. package/dist/auth/sessionUserClone.d.ts.map +1 -1
  31. package/dist/auth/sessionUserClone.js +3 -2
  32. package/dist/auth/sessionUserClone.js.map +1 -1
  33. package/dist/broadcast/BaseCacheInvalidationBroadcastProvider.d.ts +292 -0
  34. package/dist/broadcast/BaseCacheInvalidationBroadcastProvider.d.ts.map +1 -0
  35. package/dist/broadcast/BaseCacheInvalidationBroadcastProvider.js +442 -0
  36. package/dist/broadcast/BaseCacheInvalidationBroadcastProvider.js.map +1 -0
  37. package/dist/broadcast/index.d.ts +6 -0
  38. package/dist/broadcast/index.d.ts.map +1 -0
  39. package/dist/broadcast/index.js +6 -0
  40. package/dist/broadcast/index.js.map +1 -0
  41. package/dist/broadcast/providers/AWSIoTCoreBroadcastProvider.d.ts +43 -0
  42. package/dist/broadcast/providers/AWSIoTCoreBroadcastProvider.d.ts.map +1 -0
  43. package/dist/broadcast/providers/AWSIoTCoreBroadcastProvider.js +144 -0
  44. package/dist/broadcast/providers/AWSIoTCoreBroadcastProvider.js.map +1 -0
  45. package/dist/broadcast/providers/AzureWebPubSubBroadcastProvider.d.ts +46 -0
  46. package/dist/broadcast/providers/AzureWebPubSubBroadcastProvider.d.ts.map +1 -0
  47. package/dist/broadcast/providers/AzureWebPubSubBroadcastProvider.js +149 -0
  48. package/dist/broadcast/providers/AzureWebPubSubBroadcastProvider.js.map +1 -0
  49. package/dist/broadcast/providers/GCPPubSubBroadcastProvider.d.ts +49 -0
  50. package/dist/broadcast/providers/GCPPubSubBroadcastProvider.d.ts.map +1 -0
  51. package/dist/broadcast/providers/GCPPubSubBroadcastProvider.js +131 -0
  52. package/dist/broadcast/providers/GCPPubSubBroadcastProvider.js.map +1 -0
  53. package/dist/broadcast/providers/SequencedRoleBucketBroadcastProvider.d.ts +24 -0
  54. package/dist/broadcast/providers/SequencedRoleBucketBroadcastProvider.d.ts.map +1 -0
  55. package/dist/broadcast/providers/SequencedRoleBucketBroadcastProvider.js +51 -0
  56. package/dist/broadcast/providers/SequencedRoleBucketBroadcastProvider.js.map +1 -0
  57. package/dist/cache.d.ts +3 -2
  58. package/dist/cache.d.ts.map +1 -1
  59. package/dist/cache.js.map +1 -1
  60. package/dist/config.d.ts +496 -0
  61. package/dist/config.d.ts.map +1 -1
  62. package/dist/config.js +61 -0
  63. package/dist/config.js.map +1 -1
  64. package/dist/context.d.ts +3 -1
  65. package/dist/context.d.ts.map +1 -1
  66. package/dist/context.js +29 -8
  67. package/dist/context.js.map +1 -1
  68. package/dist/coreRoutes.d.ts.map +1 -1
  69. package/dist/coreRoutes.js +14 -0
  70. package/dist/coreRoutes.js.map +1 -1
  71. package/dist/generated/graphql-schemas/__mj.d.ts +635 -14
  72. package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
  73. package/dist/generated/graphql-schemas/__mj.js +3366 -418
  74. package/dist/generated/graphql-schemas/__mj.js.map +1 -1
  75. package/dist/generic/CacheInvalidationResolver.d.ts +50 -30
  76. package/dist/generic/CacheInvalidationResolver.d.ts.map +1 -1
  77. package/dist/generic/CacheInvalidationResolver.js +154 -79
  78. package/dist/generic/CacheInvalidationResolver.js.map +1 -1
  79. package/dist/generic/OrphanedConversationDetailReconciler.d.ts.map +1 -1
  80. package/dist/generic/OrphanedConversationDetailReconciler.js +2 -0
  81. package/dist/generic/OrphanedConversationDetailReconciler.js.map +1 -1
  82. package/dist/generic/PubSubManager.d.ts +10 -0
  83. package/dist/generic/PubSubManager.d.ts.map +1 -1
  84. package/dist/generic/PubSubManager.js +44 -1
  85. package/dist/generic/PubSubManager.js.map +1 -1
  86. package/dist/generic/PushStatusResolver.d.ts +39 -6
  87. package/dist/generic/PushStatusResolver.d.ts.map +1 -1
  88. package/dist/generic/PushStatusResolver.js +93 -11
  89. package/dist/generic/PushStatusResolver.js.map +1 -1
  90. package/dist/generic/ResolverBase.d.ts +65 -25
  91. package/dist/generic/ResolverBase.d.ts.map +1 -1
  92. package/dist/generic/ResolverBase.js +201 -101
  93. package/dist/generic/ResolverBase.js.map +1 -1
  94. package/dist/generic/RunViewResolver.d.ts +11 -3
  95. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  96. package/dist/generic/RunViewResolver.js +79 -13
  97. package/dist/generic/RunViewResolver.js.map +1 -1
  98. package/dist/generic/actionOutputFieldSecurity.d.ts +26 -0
  99. package/dist/generic/actionOutputFieldSecurity.d.ts.map +1 -0
  100. package/dist/generic/actionOutputFieldSecurity.js +47 -0
  101. package/dist/generic/actionOutputFieldSecurity.js.map +1 -0
  102. package/dist/generic/fieldSecurityTransport.d.ts +30 -0
  103. package/dist/generic/fieldSecurityTransport.d.ts.map +1 -0
  104. package/dist/generic/fieldSecurityTransport.js +30 -0
  105. package/dist/generic/fieldSecurityTransport.js.map +1 -0
  106. package/dist/index.d.ts +3 -0
  107. package/dist/index.d.ts.map +1 -1
  108. package/dist/index.js +122 -66
  109. package/dist/index.js.map +1 -1
  110. package/dist/integration/CustomColumnPromoter.d.ts.map +1 -1
  111. package/dist/integration/CustomColumnPromoter.js +6 -1
  112. package/dist/integration/CustomColumnPromoter.js.map +1 -1
  113. package/dist/logging/variablesLoggingMiddleware.d.ts +12 -0
  114. package/dist/logging/variablesLoggingMiddleware.d.ts.map +1 -1
  115. package/dist/logging/variablesLoggingMiddleware.js +50 -1
  116. package/dist/logging/variablesLoggingMiddleware.js.map +1 -1
  117. package/dist/realtimeSessions/SessionVerificationPortsBase.d.ts.map +1 -1
  118. package/dist/realtimeSessions/SessionVerificationPortsBase.js +1 -0
  119. package/dist/realtimeSessions/SessionVerificationPortsBase.js.map +1 -1
  120. package/dist/realtimeSessions/ownedRealtimeSession.d.ts +8 -6
  121. package/dist/realtimeSessions/ownedRealtimeSession.d.ts.map +1 -1
  122. package/dist/realtimeSessions/ownedRealtimeSession.js +15 -6
  123. package/dist/realtimeSessions/ownedRealtimeSession.js.map +1 -1
  124. package/dist/realtimeSessions/sessionAccess.d.ts +88 -11
  125. package/dist/realtimeSessions/sessionAccess.d.ts.map +1 -1
  126. package/dist/realtimeSessions/sessionAccess.js +125 -22
  127. package/dist/realtimeSessions/sessionAccess.js.map +1 -1
  128. package/dist/realtimeSessions/sessionConfigStamps.js +1 -1
  129. package/dist/realtimeSessions/sessionConfigStamps.js.map +1 -1
  130. package/dist/realtimeSessions/subscriptionAuthorization.d.ts.map +1 -1
  131. package/dist/realtimeSessions/subscriptionAuthorization.js +5 -2
  132. package/dist/realtimeSessions/subscriptionAuthorization.js.map +1 -1
  133. package/dist/realtimeSessions/verificationWorkflow.d.ts +2 -0
  134. package/dist/realtimeSessions/verificationWorkflow.d.ts.map +1 -1
  135. package/dist/realtimeSessions/verificationWorkflow.js +1 -1
  136. package/dist/realtimeSessions/verificationWorkflow.js.map +1 -1
  137. package/dist/realtimeWidget/WidgetRouter.d.ts.map +1 -1
  138. package/dist/realtimeWidget/WidgetRouter.js +19 -3
  139. package/dist/realtimeWidget/WidgetRouter.js.map +1 -1
  140. package/dist/realtimeWidget/WidgetSessionService.d.ts +67 -7
  141. package/dist/realtimeWidget/WidgetSessionService.d.ts.map +1 -1
  142. package/dist/realtimeWidget/WidgetSessionService.js +201 -19
  143. package/dist/realtimeWidget/WidgetSessionService.js.map +1 -1
  144. package/dist/realtimeWidget/host-identity.d.ts +1 -1
  145. package/dist/realtimeWidget/host-identity.js +1 -1
  146. package/dist/realtimeWidget/widgetCore.d.ts +20 -1
  147. package/dist/realtimeWidget/widgetCore.d.ts.map +1 -1
  148. package/dist/realtimeWidget/widgetCore.js +17 -1
  149. package/dist/realtimeWidget/widgetCore.js.map +1 -1
  150. package/dist/realtimeWidget/widgetGuestElevation.d.ts +1 -1
  151. package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -1
  152. package/dist/realtimeWidget/widgetGuestElevation.js +2 -1
  153. package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
  154. package/dist/resolvers/ActionResolver.d.ts +19 -1
  155. package/dist/resolvers/ActionResolver.d.ts.map +1 -1
  156. package/dist/resolvers/ActionResolver.js +63 -12
  157. package/dist/resolvers/ActionResolver.js.map +1 -1
  158. package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
  159. package/dist/resolvers/AdhocQueryResolver.js +1 -0
  160. package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
  161. package/dist/resolvers/AgentRunStatusPublisher.d.ts +18 -4
  162. package/dist/resolvers/AgentRunStatusPublisher.d.ts.map +1 -1
  163. package/dist/resolvers/AgentRunStatusPublisher.js +32 -8
  164. package/dist/resolvers/AgentRunStatusPublisher.js.map +1 -1
  165. package/dist/resolvers/AgentSessionResolver.d.ts +10 -4
  166. package/dist/resolvers/AgentSessionResolver.d.ts.map +1 -1
  167. package/dist/resolvers/AgentSessionResolver.js +40 -17
  168. package/dist/resolvers/AgentSessionResolver.js.map +1 -1
  169. package/dist/resolvers/EntityCommunicationsResolver.d.ts +13 -1
  170. package/dist/resolvers/EntityCommunicationsResolver.d.ts.map +1 -1
  171. package/dist/resolvers/EntityCommunicationsResolver.js +50 -14
  172. package/dist/resolvers/EntityCommunicationsResolver.js.map +1 -1
  173. package/dist/resolvers/FetchEntityVectorsResolver.d.ts +28 -0
  174. package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
  175. package/dist/resolvers/FetchEntityVectorsResolver.js +117 -21
  176. package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
  177. package/dist/resolvers/FileCategoryResolver.d.ts.map +1 -1
  178. package/dist/resolvers/FileCategoryResolver.js +1 -0
  179. package/dist/resolvers/FileCategoryResolver.js.map +1 -1
  180. package/dist/resolvers/FileResolver.d.ts +5 -0
  181. package/dist/resolvers/FileResolver.d.ts.map +1 -1
  182. package/dist/resolvers/FileResolver.js +13 -4
  183. package/dist/resolvers/FileResolver.js.map +1 -1
  184. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
  185. package/dist/resolvers/IntegrationDiscoveryResolver.js +45 -7
  186. package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
  187. package/dist/resolvers/MergeRecordsResolver.d.ts +8 -1
  188. package/dist/resolvers/MergeRecordsResolver.d.ts.map +1 -1
  189. package/dist/resolvers/MergeRecordsResolver.js +31 -3
  190. package/dist/resolvers/MergeRecordsResolver.js.map +1 -1
  191. package/dist/resolvers/PipelineProgressResolver.d.ts +6 -0
  192. package/dist/resolvers/PipelineProgressResolver.d.ts.map +1 -1
  193. package/dist/resolvers/PipelineProgressResolver.js +5 -3
  194. package/dist/resolvers/PipelineProgressResolver.js.map +1 -1
  195. package/dist/resolvers/QueryResolver.d.ts.map +1 -1
  196. package/dist/resolvers/QueryResolver.js +20 -6
  197. package/dist/resolvers/QueryResolver.js.map +1 -1
  198. package/dist/resolvers/QuerySystemUserResolver.d.ts +2 -0
  199. package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
  200. package/dist/resolvers/QuerySystemUserResolver.js +15 -6
  201. package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
  202. package/dist/resolvers/RSUResolver.d.ts.map +1 -1
  203. package/dist/resolvers/RSUResolver.js +12 -0
  204. package/dist/resolvers/RSUResolver.js.map +1 -1
  205. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +16 -2
  206. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  207. package/dist/resolvers/RealtimeClientSessionResolver.js +82 -28
  208. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  209. package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
  210. package/dist/resolvers/RemoteBrowserActionResolver.js +11 -9
  211. package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
  212. package/dist/resolvers/RunAIAgentResolver.d.ts +56 -8
  213. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  214. package/dist/resolvers/RunAIAgentResolver.js +209 -64
  215. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  216. package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
  217. package/dist/resolvers/RunTemplateResolver.js +4 -4
  218. package/dist/resolvers/RunTemplateResolver.js.map +1 -1
  219. package/dist/resolvers/RunTestResolver.d.ts.map +1 -1
  220. package/dist/resolvers/RunTestResolver.js +3 -1
  221. package/dist/resolvers/RunTestResolver.js.map +1 -1
  222. package/dist/resolvers/SyncDataResolver.d.ts.map +1 -1
  223. package/dist/resolvers/SyncDataResolver.js +4 -0
  224. package/dist/resolvers/SyncDataResolver.js.map +1 -1
  225. package/dist/resolvers/TestQuerySQLResolver.d.ts.map +1 -1
  226. package/dist/resolvers/TestQuerySQLResolver.js +1 -0
  227. package/dist/resolvers/TestQuerySQLResolver.js.map +1 -1
  228. package/dist/resolvers/TransactionGroupResolver.d.ts +17 -2
  229. package/dist/resolvers/TransactionGroupResolver.d.ts.map +1 -1
  230. package/dist/resolvers/TransactionGroupResolver.js +60 -15
  231. package/dist/resolvers/TransactionGroupResolver.js.map +1 -1
  232. package/dist/resolvers/UserViewResolver.d.ts +1 -1
  233. package/dist/resolvers/UserViewResolver.d.ts.map +1 -1
  234. package/dist/resolvers/UserViewResolver.js +2 -1
  235. package/dist/resolvers/UserViewResolver.js.map +1 -1
  236. package/dist/rest/EntityCRUDHandler.d.ts +6 -0
  237. package/dist/rest/EntityCRUDHandler.d.ts.map +1 -1
  238. package/dist/rest/EntityCRUDHandler.js +19 -5
  239. package/dist/rest/EntityCRUDHandler.js.map +1 -1
  240. package/dist/rest/RESTEndpointHandler.d.ts.map +1 -1
  241. package/dist/rest/RESTEndpointHandler.js +8 -5
  242. package/dist/rest/RESTEndpointHandler.js.map +1 -1
  243. package/dist/rest/RealtimeRecordingHandler.d.ts +3 -3
  244. package/dist/rest/RealtimeRecordingHandler.d.ts.map +1 -1
  245. package/dist/rest/RealtimeRecordingHandler.js +7 -7
  246. package/dist/rest/RealtimeRecordingHandler.js.map +1 -1
  247. package/dist/rest/ViewOperationsHandler.d.ts +6 -0
  248. package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
  249. package/dist/rest/ViewOperationsHandler.js +22 -5
  250. package/dist/rest/ViewOperationsHandler.js.map +1 -1
  251. package/dist/serverRoleConfigUnits.d.ts +29 -0
  252. package/dist/serverRoleConfigUnits.d.ts.map +1 -0
  253. package/dist/serverRoleConfigUnits.js +33 -0
  254. package/dist/serverRoleConfigUnits.js.map +1 -0
  255. package/dist/services/TaskGraphContinuationDeliverer.d.ts +5 -0
  256. package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
  257. package/dist/services/TaskGraphContinuationDeliverer.js +42 -15
  258. package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
  259. package/dist/types.d.ts +23 -1
  260. package/dist/types.d.ts.map +1 -1
  261. package/dist/types.js.map +1 -1
  262. package/package.json +98 -98
  263. package/src/__tests__/ActionResolver.fieldSecurity.test.ts +237 -0
  264. package/src/__tests__/AdhocQueryResolver.dialect.test.ts +137 -0
  265. package/src/__tests__/AgentRunStatusPublisher.test.ts +49 -0
  266. package/src/__tests__/AgentSessionResolver.ownership.test.ts +137 -0
  267. package/src/__tests__/CustomColumnPromoterOnePass.test.ts +15 -1
  268. package/src/__tests__/EntityCommunicationsResolver.actingUser.test.ts +151 -0
  269. package/src/__tests__/EvolutionActsOnWhatItDetects.test.ts +113 -0
  270. package/src/__tests__/FetchEntityVectorsResolver.test.ts +206 -3
  271. package/src/__tests__/FileResolver.fieldSecurity.test.ts +82 -0
  272. package/src/__tests__/PipelineProgressResolver.filter.test.ts +58 -0
  273. package/src/__tests__/QuerySystemUserResolver.fieldSecurity.test.ts +49 -0
  274. package/src/__tests__/RESTEndpointHandler.test.ts +25 -0
  275. package/src/__tests__/RealtimeClientSessionResolver.configTamper.test.ts +23 -3
  276. package/src/__tests__/RealtimeClientSessionResolver.test.ts +324 -46
  277. package/src/__tests__/RemoteBrowserActionResolver.ownership.test.ts +98 -0
  278. package/src/__tests__/ResolverBase.clientOldValues.test.ts +37 -4
  279. package/src/__tests__/ResolverBase.deleteReturnProjection.test.ts +52 -0
  280. package/src/__tests__/ResolverBase.entityMessageRelay.test.ts +176 -0
  281. package/src/__tests__/ResolverBase.includeBinaryFields.test.ts +23 -1
  282. package/src/__tests__/ResolverBase.publishStatusUpdate.test.ts +68 -0
  283. package/src/__tests__/ResolverBase.userViewCacheAndDedup.test.ts +130 -0
  284. package/src/__tests__/RunAIAgentResolver.branch.test.ts +68 -13
  285. package/src/__tests__/RunAIAgentResolver.conversationWriter.test.ts +210 -0
  286. package/src/__tests__/RunAIAgentResolver.fireAndForgetCompletion.test.ts +209 -0
  287. package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +17 -27
  288. package/src/__tests__/RunAIAgentResolver.runDataGuard.test.ts +8 -2
  289. package/src/__tests__/RunAIAgentResolver.serverReply.test.ts +358 -0
  290. package/src/__tests__/RunAIAgentResolver.stoppedPredecessor.test.ts +4 -1
  291. package/src/__tests__/RunTemplateResolver.test.ts +35 -2
  292. package/src/__tests__/RunViewResolver.cacheCheckForwarding.test.ts +11 -0
  293. package/src/__tests__/SessionManager.test.ts +95 -0
  294. package/src/__tests__/TaskGraphContinuationDeliverer.test.ts +114 -3
  295. package/src/__tests__/TransactionGroupResolver.fieldSecurity.test.ts +199 -0
  296. package/src/__tests__/UserViewResolver.updateWhereClause.test.ts +15 -0
  297. package/src/__tests__/ViewOperationsHandler.test.ts +7 -2
  298. package/src/__tests__/WidgetSessionService.test.ts +583 -4
  299. package/src/__tests__/apiRootBoot.test.ts +1 -1
  300. package/src/__tests__/buildMagicLinkSessionUser.test.ts +96 -0
  301. package/src/__tests__/cacheBroadcastProviders.test.ts +403 -0
  302. package/src/__tests__/cacheInvalidationRecordData.test.ts +45 -0
  303. package/src/__tests__/fieldSecurityFixtures.ts +91 -0
  304. package/src/__tests__/globalCors.test.ts +3 -3
  305. package/src/__tests__/localDevAuth.test.ts +196 -0
  306. package/src/__tests__/orphanedConversationDetailReconciler.test.ts +10 -0
  307. package/src/__tests__/pushStatusResolver.filter.test.ts +57 -1
  308. package/src/__tests__/realtimeRecordingHandler.test.ts +19 -2
  309. package/src/__tests__/realtimeSessions/RealtimeSessionEventsResolver.test.ts +29 -1
  310. package/src/__tests__/realtimeSessions/ownedRealtimeSession.anonymous.test.ts +31 -0
  311. package/src/__tests__/realtimeSessions/ownedRealtimeSession.namedScoped.test.ts +37 -0
  312. package/src/__tests__/realtimeSessions/sessionAccess.test.ts +146 -7
  313. package/src/__tests__/realtimeSessions/sessionConfigStamps.test.ts +1 -1
  314. package/src/__tests__/realtimeSessions/verificationWorkflow.test.ts +25 -4
  315. package/src/__tests__/resolverBase.cloudEventChangedFields.test.ts +157 -0
  316. package/src/__tests__/resolverBase.fieldSecurityTransport.test.ts +222 -0
  317. package/src/__tests__/resolverBase.fls.test.ts +146 -97
  318. package/src/__tests__/restTransport.fieldSecurity.test.ts +148 -0
  319. package/src/__tests__/runParamsTimeZone.test.ts +179 -0
  320. package/src/__tests__/runStreams.fieldSecurity.test.ts +100 -0
  321. package/src/__tests__/runViewProperties.test.ts +114 -0
  322. package/src/__tests__/scalabilityBenchmarks.test.ts +765 -0
  323. package/src/__tests__/scalabilityDatabaseIntegration.test.ts +207 -0
  324. package/src/__tests__/sessionUserClone.test.ts +5 -1
  325. package/src/__tests__/startupSweepOrder.test.ts +40 -0
  326. package/src/__tests__/unifiedAuth.test.ts +28 -0
  327. package/src/__tests__/widgetGuestElevation.test.ts +2 -0
  328. package/src/agentSessions/SessionJanitor.ts +3 -3
  329. package/src/agentSessions/SessionManager.ts +35 -12
  330. package/src/auth/AuthProviderCatalogRouter.ts +3 -0
  331. package/src/auth/index.ts +56 -12
  332. package/src/auth/initializeProviders.ts +43 -1
  333. package/src/auth/localDevAuth.ts +528 -0
  334. package/src/auth/magicLink/MagicLinkKeys.ts +2 -2
  335. package/src/auth/magicLink/types.ts +10 -0
  336. package/src/auth/sessionUserClone.ts +3 -2
  337. package/src/broadcast/BaseCacheInvalidationBroadcastProvider.ts +674 -0
  338. package/src/broadcast/index.ts +5 -0
  339. package/src/broadcast/providers/AWSIoTCoreBroadcastProvider.ts +180 -0
  340. package/src/broadcast/providers/AzureWebPubSubBroadcastProvider.ts +182 -0
  341. package/src/broadcast/providers/GCPPubSubBroadcastProvider.ts +169 -0
  342. package/src/broadcast/providers/SequencedRoleBucketBroadcastProvider.ts +57 -0
  343. package/src/cache.ts +2 -1
  344. package/src/config.ts +69 -0
  345. package/src/context.ts +32 -8
  346. package/src/coreRoutes.ts +15 -0
  347. package/src/generated/graphql-schemas/__mj.ts +2239 -102
  348. package/src/generic/CacheInvalidationResolver.ts +135 -78
  349. package/src/generic/OrphanedConversationDetailReconciler.ts +4 -2
  350. package/src/generic/PubSubManager.ts +54 -1
  351. package/src/generic/PushStatusResolver.ts +108 -11
  352. package/src/generic/ResolverBase.ts +237 -107
  353. package/src/generic/RunViewResolver.ts +80 -15
  354. package/src/generic/actionOutputFieldSecurity.ts +75 -0
  355. package/src/generic/fieldSecurityTransport.ts +62 -0
  356. package/src/index.ts +127 -68
  357. package/src/integration/CustomColumnPromoter.ts +6 -1
  358. package/src/logging/variablesLoggingMiddleware.ts +59 -3
  359. package/src/realtimeSessions/README.md +7 -3
  360. package/src/realtimeSessions/SessionVerificationPortsBase.ts +1 -0
  361. package/src/realtimeSessions/ownedRealtimeSession.ts +20 -6
  362. package/src/realtimeSessions/sessionAccess.ts +181 -25
  363. package/src/realtimeSessions/sessionConfigStamps.ts +1 -1
  364. package/src/realtimeSessions/subscriptionAuthorization.ts +4 -2
  365. package/src/realtimeSessions/verificationWorkflow.ts +3 -1
  366. package/src/realtimeWidget/WidgetRouter.ts +22 -4
  367. package/src/realtimeWidget/WidgetSessionService.ts +252 -17
  368. package/src/realtimeWidget/host-identity.ts +1 -1
  369. package/src/realtimeWidget/widgetCore.ts +30 -1
  370. package/src/realtimeWidget/widgetGuestElevation.ts +2 -1
  371. package/src/resolvers/ActionResolver.ts +78 -15
  372. package/src/resolvers/AdhocQueryResolver.ts +1 -0
  373. package/src/resolvers/AgentRunStatusPublisher.ts +36 -8
  374. package/src/resolvers/AgentSessionResolver.ts +46 -18
  375. package/src/resolvers/EntityCommunicationsResolver.ts +56 -15
  376. package/src/resolvers/FetchEntityVectorsResolver.ts +132 -21
  377. package/src/resolvers/FileCategoryResolver.ts +1 -0
  378. package/src/resolvers/FileResolver.ts +14 -4
  379. package/src/resolvers/IntegrationDiscoveryResolver.ts +49 -7
  380. package/src/resolvers/MergeRecordsResolver.ts +23 -2
  381. package/src/resolvers/PipelineProgressResolver.ts +12 -3
  382. package/src/resolvers/QueryResolver.ts +20 -6
  383. package/src/resolvers/QuerySystemUserResolver.ts +15 -6
  384. package/src/resolvers/RSUResolver.ts +12 -0
  385. package/src/resolvers/RealtimeClientSessionResolver.ts +104 -33
  386. package/src/resolvers/RemoteBrowserActionResolver.ts +15 -10
  387. package/src/resolvers/RunAIAgentResolver.ts +258 -82
  388. package/src/resolvers/RunTemplateResolver.ts +6 -4
  389. package/src/resolvers/RunTestResolver.ts +3 -1
  390. package/src/resolvers/SyncDataResolver.ts +4 -0
  391. package/src/resolvers/TestQuerySQLResolver.ts +1 -0
  392. package/src/resolvers/TransactionGroupResolver.ts +66 -15
  393. package/src/resolvers/UserViewResolver.ts +2 -1
  394. package/src/resolvers/__tests__/MergeRecordsResolver.test.ts +43 -0
  395. package/src/rest/EntityCRUDHandler.ts +22 -5
  396. package/src/rest/RESTEndpointHandler.ts +8 -5
  397. package/src/rest/RealtimeRecordingHandler.ts +13 -10
  398. package/src/rest/ViewOperationsHandler.ts +24 -6
  399. package/src/serverRoleConfigUnits.ts +48 -0
  400. package/src/services/TaskGraphContinuationDeliverer.ts +52 -20
  401. package/src/types.ts +23 -1
@@ -1,4 +1,5 @@
1
- import { BaseEntity, BaseEntityEvent, CompositeKey, EntityFieldTSType, EntityPermissionType, KeyValuePair, LogDebug, LogError, LogStatus, Metadata, ReadableFieldsTransportKey, RecordChangeFieldSecurityProjector, RecordChangePayloadFields, } from '@memberjunction/core';
1
+ import { AssertClientInputWritableForUser, BaseEntity, BaseEntityEvent, CompositeKey, EntityFieldTSType, EntityPermissionType, KeyValuePair, LogDebug, LogError, LogErrorEx, LogStatus, Metadata, ProjectEntityValuesForUser, ReadableFieldsTransportKey, RecordChangeFieldSecurityProjector, RecordChangePayloadFields, } from '@memberjunction/core';
2
+ import { UserViewEngine } from '@memberjunction/core-entities';
2
3
  import { ClientClauseScreen, UserCache } from '@memberjunction/generic-database-provider';
3
4
  import { AuthorizationError } from 'type-graphql';
4
5
  import { GraphQLError } from 'graphql';
@@ -6,10 +7,10 @@ import { RefusalExtensions } from './refusalExtensions.js';
6
7
  import { GetAPIKeyEngine } from '@memberjunction/api-keys';
7
8
  import { httpTransport, CloudEvent, emitterFor } from 'cloudevents';
8
9
  import { DeleteOptionsInput } from './DeleteOptionsInput.js';
9
- import { MJEventType, MJGlobal, ENCRYPTED_SENTINEL, ReplaceByteArraysWithBase64, EscapeSQLString, IsValueEncrypted, IsOnlyTimezoneShift } from '@memberjunction/global';
10
+ import { MJEventType, MJGlobal, UUIDsEqual, ENCRYPTED_SENTINEL, ReplaceByteArraysWithBase64, EscapeSQLString, IsValueEncrypted, IsOnlyTimezoneShift } from '@memberjunction/global';
10
11
  import { SQLParser, IsBalancedSQLFragment, LexSQL } from '@memberjunction/sql-parser';
11
12
  import { EncryptionEngine, IsEncryptionKeyUnavailable, ReportEncryptionKeyUnavailable } from '@memberjunction/encryption';
12
- import { PublishStatusUpdate } from './PushStatusResolver.js';
13
+ import { PublishStatusUpdate, StatusUpdateOwnerID } from './PushStatusResolver.js';
13
14
  import { CACHE_INVALIDATION_TOPIC, MayBroadcastRecordData } from './CacheInvalidationResolver.js';
14
15
  import { PubSubManager } from './PubSubManager.js';
15
16
  import { FieldMapper } from '@memberjunction/graphql-dataprovider';
@@ -17,11 +18,18 @@ import { FieldMapper } from '@memberjunction/graphql-dataprovider';
17
18
  * Entities whose update is always hydrated from the stored row, never from the client's `OldValues___`, though they
18
19
  * do not track record changes: their server-side write rules compare against the prior state, which a client could
19
20
  * otherwise supply itself (lower-case names). See `ResolverBase.MustLoadTruthFromDatabase`.
21
+ *
22
+ * `mj: conversations` and `mj: ai agent sessions` carry the anonymous realtime-session ownership key
23
+ * (`ExternalID` / `ConversationID`), which their server entities refuse an anonymous visitor to change — a rule
24
+ * that decides on `Dirty`, so the baseline must be the stored row, not a client-supplied one (#4931).
20
25
  */
21
26
  const ENTITIES_THAT_ALWAYS_LOAD_STORED_ROW = new Set([
22
27
  'mj: record changes',
23
28
  'mj: conversation details',
24
29
  'mj: data context items',
30
+ 'mj: conversations',
31
+ 'mj: ai agent sessions',
32
+ 'mj: ai agent runs',
25
33
  ]);
26
34
  export class ResolverBase {
27
35
  static { this._emit = process.env.CLOUDEVENTS_HTTP_TRANSPORT ? emitterFor(httpTransport(process.env.CLOUDEVENTS_HTTP_TRANSPORT)) : null; }
@@ -69,7 +77,7 @@ export class ResolverBase {
69
77
  const entityInfo = md.EntityByName(entityName);
70
78
  if (!entityInfo)
71
79
  throw new Error(`Entity ${entityName} not found in metadata`);
72
- const denied = deniedReadFields ?? (entityInfo.EnableFieldLevelSecurity && contextUser
80
+ const denied = deniedReadFields ?? (entityInfo.FieldLevelSecurityApplies && contextUser
73
81
  ? entityInfo.GetDeniedReadFields(contextUser)
74
82
  : null);
75
83
  const mapper = new FieldMapper();
@@ -284,6 +292,7 @@ export class ResolverBase {
284
292
  if (!loaded) {
285
293
  return null;
286
294
  }
295
+ // fls-guard: MapFieldNamesToCodeNames drops the fields the caller may not read
287
296
  const mapped = await this.MapFieldNamesToCodeNames(entityName, entityObject.GetAll(), contextUser);
288
297
  return mapped;
289
298
  }
@@ -449,23 +458,54 @@ export class ResolverBase {
449
458
  return [];
450
459
  }
451
460
  }
461
+ /**
462
+ * Resolves a User View by name, consulting the in-memory {@link UserViewEngine} cache first
463
+ * and falling back to a database `RunView` only on a cache miss.
464
+ */
465
+ async resolveUserViewByName(viewName, provider, contextUser) {
466
+ if (UserViewEngine.Instance.Loaded) {
467
+ const cached = UserViewEngine.Instance.GetViewByName(viewName);
468
+ if (cached) {
469
+ return cached;
470
+ }
471
+ }
472
+ const rv = provider;
473
+ const result = await rv.RunView({
474
+ EntityName: 'MJ: User Views',
475
+ // SECURITY: the view name is client-supplied and lands inside a SQL literal.
476
+ ExtraFilter: "Name='" + EscapeSQLString(viewName) + "'",
477
+ }, contextUser);
478
+ if (result && result.Success && result.Results.length > 0) {
479
+ return result.Results[0];
480
+ }
481
+ return null;
482
+ }
483
+ /**
484
+ * Resolves a User View by ID, consulting the in-memory {@link UserViewEngine} cache first
485
+ * and falling back to an entity `Load()` only on a cache miss.
486
+ */
487
+ async resolveUserViewByID(viewID, provider, contextUser) {
488
+ if (UserViewEngine.Instance.Loaded) {
489
+ const cached = UserViewEngine.Instance.GetViewById(viewID);
490
+ if (cached) {
491
+ return cached;
492
+ }
493
+ }
494
+ const viewInfo = await provider.GetEntityObject('MJ: User Views', contextUser);
495
+ await viewInfo.Load(viewID);
496
+ return viewInfo;
497
+ }
452
498
  async RunViewByNameGeneric(viewInput, provider, userPayload, pubSub) {
453
499
  try {
454
500
  // Log aggregate input for debugging
455
501
  if (viewInput.Aggregates?.length) {
456
502
  LogStatus(`[ResolverBase] RunViewByNameGeneric received aggregates: viewName=${viewInput.ViewName}, aggregateCount=${viewInput.Aggregates.length}, aggregates=${JSON.stringify(viewInput.Aggregates.map(a => ({ expression: a.expression, alias: a.alias })))}`);
457
503
  }
458
- const rv = provider;
459
- const result = await rv.RunView({
460
- EntityName: 'MJ: User Views',
461
- // SECURITY: the view name is client-supplied and lands inside a SQL literal.
462
- ExtraFilter: "Name='" + EscapeSQLString(viewInput.ViewName) + "'",
463
- }, userPayload.userRecord);
464
- if (result && result.Success && result.Results.length > 0) {
465
- const viewInfo = result.Results[0];
504
+ const viewInfo = await this.resolveUserViewByName(viewInput.ViewName, provider, userPayload.userRecord);
505
+ if (viewInfo) {
466
506
  return this.RunViewGenericInternal(provider, viewInfo, viewInput.ExtraFilter, viewInput.OrderBy, viewInput.UserSearchString, viewInput.ExcludeUserViewRunID, viewInput.OverrideExcludeFilter, viewInput.SaveViewResults, viewInput.Fields, viewInput.IgnoreMaxRows, viewInput.ExcludeDataFromAllPriorViewRuns, viewInput.ForceAuditLog, viewInput.AuditLogDescription, viewInput.ResultType, userPayload, viewInput.MaxRows, viewInput.StartRow, viewInput.Aggregates, viewInput.AfterKey
467
507
  ? CompositeKey.FromKeyValuePairs(viewInput.AfterKey.KeyValuePairs)
468
- : undefined, viewInput.BypassCache, viewInput.DataSource, viewInput.IncludeBinaryFields);
508
+ : undefined, viewInput.BypassCache, viewInput.DataSource, viewInput.IncludeBinaryFields, viewInput.AlternateBaseView, viewInput.Properties);
469
509
  }
470
510
  else {
471
511
  LogError(`RunViewByNameGeneric: View ${viewInput.ViewName} not found or no results returned`);
@@ -484,9 +524,11 @@ export class ResolverBase {
484
524
  LogStatus(`[ResolverBase] RunViewByIDGeneric received aggregates: viewID=${viewInput.ViewID}, aggregateCount=${viewInput.Aggregates.length}, aggregates=${JSON.stringify(viewInput.Aggregates.map(a => ({ expression: a.expression, alias: a.alias })))}`);
485
525
  }
486
526
  const contextUser = this.GetUserFromPayload(userPayload);
487
- const viewInfo = await provider.GetEntityObject('MJ: User Views', contextUser);
488
- await viewInfo.Load(viewInput.ViewID);
489
- return this.RunViewGenericInternal(provider, viewInfo, viewInput.ExtraFilter, viewInput.OrderBy, viewInput.UserSearchString, viewInput.ExcludeUserViewRunID, viewInput.OverrideExcludeFilter, viewInput.SaveViewResults, viewInput.Fields, viewInput.IgnoreMaxRows, viewInput.ExcludeDataFromAllPriorViewRuns, viewInput.ForceAuditLog, viewInput.AuditLogDescription, viewInput.ResultType, userPayload, viewInput.MaxRows, viewInput.StartRow, viewInput.Aggregates, undefined, viewInput.BypassCache, viewInput.DataSource, viewInput.IncludeBinaryFields);
527
+ const viewInfo = await this.resolveUserViewByID(viewInput.ViewID, provider, contextUser);
528
+ if (!viewInfo) {
529
+ return null;
530
+ }
531
+ return this.RunViewGenericInternal(provider, viewInfo, viewInput.ExtraFilter, viewInput.OrderBy, viewInput.UserSearchString, viewInput.ExcludeUserViewRunID, viewInput.OverrideExcludeFilter, viewInput.SaveViewResults, viewInput.Fields, viewInput.IgnoreMaxRows, viewInput.ExcludeDataFromAllPriorViewRuns, viewInput.ForceAuditLog, viewInput.AuditLogDescription, viewInput.ResultType, userPayload, viewInput.MaxRows, viewInput.StartRow, viewInput.Aggregates, undefined, viewInput.BypassCache, viewInput.DataSource, viewInput.IncludeBinaryFields, viewInput.AlternateBaseView, viewInput.Properties);
490
532
  }
491
533
  catch (err) {
492
534
  console.log(err);
@@ -509,7 +551,7 @@ export class ResolverBase {
509
551
  EntityID: entity.ID,
510
552
  EntityBaseView: entity.BaseView,
511
553
  }; // only providing a few bits of data here, but it's enough to get the view to run
512
- return this.RunViewGenericInternal(provider, viewInfo, viewInput.ExtraFilter, viewInput.OrderBy, viewInput.UserSearchString, viewInput.ExcludeUserViewRunID, viewInput.OverrideExcludeFilter, false, viewInput.Fields, viewInput.IgnoreMaxRows, false, viewInput.ForceAuditLog, viewInput.AuditLogDescription, viewInput.ResultType, userPayload, viewInput.MaxRows, viewInput.StartRow, viewInput.Aggregates, undefined, viewInput.BypassCache, viewInput.DataSource, viewInput.IncludeBinaryFields);
554
+ return this.RunViewGenericInternal(provider, viewInfo, viewInput.ExtraFilter, viewInput.OrderBy, viewInput.UserSearchString, viewInput.ExcludeUserViewRunID, viewInput.OverrideExcludeFilter, false, viewInput.Fields, viewInput.IgnoreMaxRows, false, viewInput.ForceAuditLog, viewInput.AuditLogDescription, viewInput.ResultType, userPayload, viewInput.MaxRows, viewInput.StartRow, viewInput.Aggregates, undefined, viewInput.BypassCache, viewInput.DataSource, viewInput.IncludeBinaryFields, viewInput.AlternateBaseView);
513
555
  }
514
556
  catch (err) {
515
557
  console.log(err);
@@ -524,7 +566,7 @@ export class ResolverBase {
524
566
  try {
525
567
  let viewInfo = null;
526
568
  if (viewInput.ViewName) {
527
- viewInfo = this.safeFirstArrayElement(await this.findBy(provider, 'MJ: User Views', { Name: viewInput.ViewName }, userPayload.userRecord));
569
+ viewInfo = await this.resolveUserViewByName(viewInput.ViewName, provider, userPayload.userRecord);
528
570
  // Populate EntityName on the input so callers (e.g. RunViews resolver) can
529
571
  // look up the entity without re-querying the view
530
572
  if (viewInfo && !viewInput.EntityName) {
@@ -532,8 +574,7 @@ export class ResolverBase {
532
574
  }
533
575
  }
534
576
  else if (viewInput.ViewID) {
535
- viewInfo = await provider.GetEntityObject('MJ: User Views', contextUser);
536
- await viewInfo.Load(viewInput.ViewID);
577
+ viewInfo = await this.resolveUserViewByID(viewInput.ViewID, provider, contextUser);
537
578
  // Populate EntityName on the input so callers can look up the entity
538
579
  if (viewInfo && !viewInput.EntityName) {
539
580
  viewInput.EntityName = viewInfo.Entity;
@@ -580,6 +621,8 @@ export class ResolverBase {
580
621
  bypassCache: viewInput.BypassCache,
581
622
  dataSource: viewInput.DataSource,
582
623
  includeBinaryFields: viewInput.IncludeBinaryFields,
624
+ alternateBaseView: viewInput.AlternateBaseView,
625
+ properties: viewInput.Properties,
583
626
  });
584
627
  }
585
628
  catch (err) {
@@ -590,6 +633,30 @@ export class ResolverBase {
590
633
  let results = await this.RunViewsGenericInternal(params);
591
634
  return results;
592
635
  }
636
+ /**
637
+ * What an entity event may carry to a CloudEvents sink or a log line.
638
+ *
639
+ * The whole row for an entity without field security. For one where it applies (its own flag
640
+ * or an IS-A ancestor's), and for `MJ: Record Changes` (whose payload columns hold other
641
+ * entities' values), only the primary key and the names of the fields that changed: the sink
642
+ * is outside MemberJunction with no reader to project the row for, and these events fire for
643
+ * server-internal saves too, whose objects hold every column.
644
+ */
645
+ EntityEventPayload(baseEntity, changedFieldNames) {
646
+ const entityInfo = baseEntity.EntityInfo;
647
+ if (!entityInfo?.FieldLevelSecurityApplies && !RecordChangeFieldSecurityProjector.IsRecordChangesEntity(entityInfo)) {
648
+ // fls-guard: entity without field security and not the audit trail; whole row is the documented payload
649
+ return baseEntity.GetAll();
650
+ }
651
+ return {
652
+ // Read through Get() rather than PrimaryKey, which builds every field object of a raw-mode record.
653
+ PrimaryKey: (entityInfo.PrimaryKeys ?? []).map((pk) => ({ FieldName: pk.Name, Value: baseEntity.Get(pk.Name) })),
654
+ // A completed save has already rebased the object, so nothing on it is dirty: the event
655
+ // carries the names the save wrote. Other events (save_started) still read them off the object.
656
+ // GetDirtyFieldNames answers without building field objects for a record still in raw mode.
657
+ ChangedFields: changedFieldNames ?? baseEntity.GetDirtyFieldNames(),
658
+ };
659
+ }
593
660
  static { this._priorEmittedData = []; }
594
661
  async EmitCloudEvent({ component, event, eventCode, args }) {
595
662
  if (ResolverBase._emit && event === MJEventType.ComponentEvent && eventCode === BaseEntity.BaseEventCode) {
@@ -597,7 +664,7 @@ export class ResolverBase {
597
664
  const type = `MemberJunction.${event}${extendedType}`;
598
665
  const source = `${process.env.CLOUDEVENTS_SOURCE ?? 'MemberJunction'}`;
599
666
  const subject = args instanceof BaseEntityEvent ? args.baseEntity.EntityInfo.CodeName : undefined;
600
- const data = args?.baseEntity?.GetAll() ?? {};
667
+ const data = args?.baseEntity ? this.EntityEventPayload(args.baseEntity, args.ChangedFieldNames) : {};
601
668
  const cloudEvent = new CloudEvent({ type, source, subject, data });
602
669
  try {
603
670
  // check to see if the combination of Entity and pkey was already emitted, if so, Log that condtion next
@@ -873,7 +940,7 @@ export class ResolverBase {
873
940
  * - Field filtering at source (Fix #7)
874
941
  * - Improved error handling (Fix #9)
875
942
  */
876
- async RunViewGenericInternal(provider, viewInfo, extraFilter, orderBy, userSearchString, excludeUserViewRunID, overrideExcludeFilter, saveViewResults, fields, ignoreMaxRows, excludeDataFromAllPriorViewRuns, forceAuditLog, auditLogDescription, resultType, userPayload, maxRows, startRow, aggregates, afterKey, bypassCache, dataSource, includeBinaryFields) {
943
+ async RunViewGenericInternal(provider, viewInfo, extraFilter, orderBy, userSearchString, excludeUserViewRunID, overrideExcludeFilter, saveViewResults, fields, ignoreMaxRows, excludeDataFromAllPriorViewRuns, forceAuditLog, auditLogDescription, resultType, userPayload, maxRows, startRow, aggregates, afterKey, bypassCache, dataSource, includeBinaryFields, alternateBaseView, properties) {
877
944
  try {
878
945
  if (!viewInfo || !userPayload)
879
946
  return null;
@@ -919,6 +986,7 @@ export class ResolverBase {
919
986
  const result = await rv.RunView({
920
987
  ViewID: viewInfo.ID,
921
988
  ViewName: viewInfo.Name,
989
+ ViewEntity: viewInfo.ID ? viewInfo : undefined,
922
990
  EntityName: viewInfo.Entity,
923
991
  ExtraFilter: extraFilter,
924
992
  OrderBy: orderBy,
@@ -939,7 +1007,18 @@ export class ResolverBase {
939
1007
  BypassCache: bypassCache,
940
1008
  DataSource: dataSource,
941
1009
  IncludeBinaryFields: includeBinaryFields,
1010
+ AlternateBaseView: alternateBaseView,
1011
+ Properties: properties,
1012
+ TimeZone: userPayload?.TimeZone,
942
1013
  }, user);
1014
+ if (result && typeof result === 'object') {
1015
+ Object.defineProperty(result, '_resolvedViewEntity', {
1016
+ value: viewInfo,
1017
+ enumerable: false,
1018
+ configurable: true,
1019
+ writable: true,
1020
+ });
1021
+ }
943
1022
  // Log aggregate results for debugging
944
1023
  if (aggregates?.length) {
945
1024
  LogStatus(`[ResolverBase] RunView result aggregate info: entityName=${viewInfo.Entity}, hasAggregateResults=${!!result?.AggregateResults}, aggregateResultCount=${result?.AggregateResults?.length || 0}, aggregateExecutionTime=${result?.AggregateExecutionTime}, aggregateResults=${JSON.stringify(result?.AggregateResults)}`);
@@ -966,7 +1045,7 @@ export class ResolverBase {
966
1045
  if (result?.Success && result.Results?.length) {
967
1046
  result.Results = result.Results.map(r => mapper.MapFields({ ...r }));
968
1047
  // Filter encrypted fields before sending to API client
969
- await this.ArrayFilterEncryptedFieldsForAPI(viewInfo.Entity, result.Results, user);
1048
+ await this.ArrayFilterEncryptedFieldsForAPI(viewInfo.Entity, result.Results, user, md);
970
1049
  }
971
1050
  return result;
972
1051
  }
@@ -1050,6 +1129,7 @@ export class ResolverBase {
1050
1129
  runViewParams.push({
1051
1130
  ViewID: param.viewInfo.ID,
1052
1131
  ViewName: param.viewInfo.Name,
1132
+ ViewEntity: param.viewInfo.ID ? param.viewInfo : undefined,
1053
1133
  EntityName: param.viewInfo.Entity,
1054
1134
  ExtraFilter: param.extraFilter,
1055
1135
  OrderBy: param.orderBy,
@@ -1070,6 +1150,9 @@ export class ResolverBase {
1070
1150
  BypassCache: param.bypassCache,
1071
1151
  DataSource: param.dataSource,
1072
1152
  IncludeBinaryFields: param.includeBinaryFields,
1153
+ AlternateBaseView: param.alternateBaseView,
1154
+ Properties: param.properties,
1155
+ TimeZone: param.userPayload?.TimeZone,
1073
1156
  });
1074
1157
  }
1075
1158
  // Fix #4: Run views in a single batch through RunViews
@@ -1086,7 +1169,7 @@ export class ResolverBase {
1086
1169
  // Use the corresponding param's entity name
1087
1170
  const entityName = params[i]?.viewInfo?.Entity;
1088
1171
  if (entityName && contextUser) {
1089
- await this.ArrayFilterEncryptedFieldsForAPI(entityName, runViewResult.Results, contextUser);
1172
+ await this.ArrayFilterEncryptedFieldsForAPI(entityName, runViewResult.Results, contextUser, md);
1090
1173
  }
1091
1174
  }
1092
1175
  }
@@ -1205,8 +1288,9 @@ export class ResolverBase {
1205
1288
  * skip redundant re-fetches (it already handled the event locally).
1206
1289
  *
1207
1290
  * The row itself rides along ONLY for entities opted in via
1208
- * `cacheSettings.recordDataBroadcastEntities` — this event reaches every connected client
1209
- * unfiltered, so the row would otherwise be readable by sessions that could not read the record.
1291
+ * `cacheSettings.recordDataBroadcastEntities`, and never for one under field-level security —
1292
+ * this event reaches every connected client unprojected, so the row would otherwise be readable
1293
+ * by sessions that could not read the record or its restricted fields.
1210
1294
  */
1211
1295
  PublishCacheInvalidation(entityObject, action, userPayload) {
1212
1296
  const entityName = entityObject.EntityInfo.Name;
@@ -1217,23 +1301,25 @@ export class ResolverBase {
1217
1301
  sourceServerId: MJGlobal.Instance.ProcessUUID,
1218
1302
  timestamp: new Date(),
1219
1303
  originSessionId: userPayload?.sessionId || null,
1220
- recordData: action === 'save' && MayBroadcastRecordData(entityName)
1304
+ recordData: action === 'save' && MayBroadcastRecordData(entityName, entityObject.EntityInfo)
1305
+ // fls-guard: MayBroadcastRecordData refuses FLS entities and Record Changes
1221
1306
  ? JSON.stringify(entityObject.GetAll())
1222
1307
  : undefined,
1223
1308
  });
1224
1309
  }
1225
1310
  /**
1226
1311
  * Publishes a push-status update to the client on {@link PUSH_STATUS_UPDATES_TOPIC}, stamping the
1227
- * authenticated owner's user ID from `userPayload` so the subscription filter can bind delivery
1228
- * to identity (see B49 / `statusUpdatesFilter`). The ergonomic wrapper every resolver should use
1229
- * instead of calling `pubSub.publish` on the topic directly — it makes omitting identity
1230
- * impossible. Non-resolver publishers (services, the liveness heartbeat) call the shared
1231
- * `publishStatusUpdate()` function directly with an explicit `ownerUserId`.
1312
+ * owner key of `userPayload` (`StatusUpdateOwnerID`: the request's user, or the widget guest an
1313
+ * elevated payload stands in for) so the subscription filter can bind delivery to identity (see B49 /
1314
+ * `StatusUpdatesFilter`). The ergonomic wrapper every resolver should use instead of calling
1315
+ * `pubSub.publish` on the topic directly — it makes omitting identity impossible. Non-resolver
1316
+ * publishers (services, the liveness heartbeat) call the shared `PublishStatusUpdate()` function
1317
+ * directly with an explicit `ownerUserId`.
1232
1318
  */
1233
1319
  PublishStatusUpdate(pubSub, sessionId, message, userPayload) {
1234
1320
  PublishStatusUpdate(pubSub, {
1235
1321
  sessionId,
1236
- ownerUserId: userPayload?.userRecord?.ID ?? '',
1322
+ ownerUserId: StatusUpdateOwnerID(userPayload),
1237
1323
  message,
1238
1324
  });
1239
1325
  }
@@ -1253,28 +1339,50 @@ export class ResolverBase {
1253
1339
  if (baseEntity?.EntityInfo?.Name !== uniqueKey) {
1254
1340
  return;
1255
1341
  }
1256
- const baseEntityValues = baseEntity ? baseEntity.GetAll() : null;
1342
+ const baseEntityValues = baseEntity ? this.EntityEventPayload(baseEntity, event.args instanceof BaseEntityEvent ? event.args.ChangedFieldNames : undefined) : null;
1257
1343
  const eventToLog = { entityName: entityObject.EntityInfo.Name, baseEntity: baseEntityValues, event: event.event, eventCode: event.eventCode };
1258
1344
  LogDebug(`ResolverBase.ListenForEntityMessages: Received Event from within MJGlobal.Instance.GetEventListener() callback. Will call EmitCloudEvent() next\nEvent data:\n${JSON.stringify(eventToLog)}`);
1259
1345
  await this.EmitCloudEvent(event);
1260
1346
  LogDebug(`ResolverBase.ListenForEntityMessages: EmitCloudEvent() completed successfully`);
1261
- if (event.args && event.args instanceof BaseEntityEvent) {
1262
- const baseEntityEvent = event.args;
1263
- // message from our entity object, relay it to the client
1347
+ if (event.args && event.args instanceof BaseEntityEvent && this.EntityEventBelongsToSession(event.args, userPayload)) {
1348
+ // message from an entity object of this session's user, relay it to the client
1264
1349
  LogDebug('ResolverBase.ListenForEntityMessages: About to publish PUSH_STATUS_UPDATES_TOPIC');
1265
- this.PublishStatusUpdate(pubSub, userPayload.sessionId, JSON.stringify({
1266
- status: 'OK',
1267
- type: 'EntityObjectStatusMessage',
1268
- entityName: baseEntityEvent.baseEntity.EntityInfo.Name,
1269
- primaryKey: baseEntityEvent.baseEntity.PrimaryKey,
1270
- message: event.args.payload,
1271
- }), userPayload);
1350
+ this.PublishStatusUpdate(pubSub, userPayload.sessionId, this.EntityStatusMessage(event.args), userPayload);
1272
1351
  }
1273
1352
  }
1274
1353
  });
1275
1354
  this.eventSubscriptions.set(uniqueKey, theSub);
1276
1355
  }
1277
1356
  }
1357
+ /**
1358
+ * Whether an entity event may be relayed to the session whose call created the listener. The
1359
+ * listener is shared by every caller in the process for that entity, so an event raised by an
1360
+ * object loaded for another user (or for the system user) is never relayed to this session.
1361
+ */
1362
+ EntityEventBelongsToSession(event, userPayload) {
1363
+ const sessionUserId = userPayload?.userRecord?.ID;
1364
+ const objectUserId = event.baseEntity?.ContextCurrentUser?.ID;
1365
+ return Boolean(sessionUserId && objectUserId && UUIDsEqual(sessionUserId, objectUserId));
1366
+ }
1367
+ /**
1368
+ * The status message relayed for an entity event: the entity, key and event type. The event's
1369
+ * payload is included only when it cannot carry record values: never for a delete (its payload
1370
+ * is the whole deleted row), an entity under field-level security, or `MJ: Record Changes`.
1371
+ */
1372
+ EntityStatusMessage(event) {
1373
+ const entityInfo = event.baseEntity.EntityInfo;
1374
+ const mayCarryPayload = event.type !== 'delete'
1375
+ && !entityInfo.FieldLevelSecurityApplies
1376
+ && !RecordChangeFieldSecurityProjector.IsRecordChangesEntity(entityInfo);
1377
+ return JSON.stringify({
1378
+ status: 'OK',
1379
+ type: 'EntityObjectStatusMessage',
1380
+ entityName: entityInfo.Name,
1381
+ primaryKey: event.baseEntity.PrimaryKey,
1382
+ eventType: event.type,
1383
+ message: mayCarryPayload ? event.payload : undefined,
1384
+ });
1385
+ }
1278
1386
  /**
1279
1387
  * IS-A PROMOTION over the wire: an EXISTING parent record gaining a subtype — an Animal that is
1280
1388
  * now also a Dog. It arrives as the CHILD's create mutation carrying the existing parent's
@@ -1348,6 +1456,9 @@ export class ResolverBase {
1348
1456
  // IS-A promotion: bind the new child to its EXISTING parent row BEFORE the field
1349
1457
  // assignments, so values the client sent for parent fields land on the loaded parent
1350
1458
  // (as an update) instead of being wiped by the load.
1459
+ // Field-level security: refuse a key the caller may not read, or that names no field, before
1460
+ // anything is applied (the same gate as the update path).
1461
+ AssertClientInputWritableForUser(entityObject.EntityInfo, fieldsForSet, this.GetUserFromPayload(userPayload));
1351
1462
  await this.attachToExistingParentIfPromotion(entityObject, fieldsForSet);
1352
1463
  entityObject.SetMany(fieldsForSet);
1353
1464
  // Reconstruct the client-side restore context, if any, on this server
@@ -1361,6 +1472,7 @@ export class ResolverBase {
1361
1472
  // Cache invalidation is now handled globally by the MJGlobal listener in index.ts
1362
1473
  const contextUser = this.GetUserFromPayload(userPayload);
1363
1474
  // MapFieldNamesToCodeNames now handles encryption filtering as well
1475
+ // fls-guard: MapFieldNamesToCodeNames drops the fields the caller may not read
1364
1476
  return await this.MapFieldNamesToCodeNames(entityName, entityObject.GetAll(), contextUser);
1365
1477
  }
1366
1478
  // save failed, throw error with message
@@ -1398,12 +1510,11 @@ export class ResolverBase {
1398
1510
  }
1399
1511
  });
1400
1512
  // Field-level security: any value the client sends for a field it cannot READ is
1401
- // fabricated by construction (the field was stripped from every payload the client ever
1402
- // received), so strip such values before anything applies them. When any field is
1403
- // denied we also force the truth-load branch below — client OldValues for denied fields
1404
- // are equally fabricated, so hydrating from them (the no-DB-load path) would write
1405
- // fabricated state into the denied columns on save.
1406
- const hasDeniedReadFields = this.StripDeniedReadFieldsFromClientInput(entityInfo, userInfo, input, clientNewValues);
1513
+ // fabricated by construction (the field was absent from every payload the client ever
1514
+ // received), so the input is refused before anything applies it. When any field is
1515
+ // denied we also force the truth-load branch below, so the denied columns keep what the
1516
+ // database holds rather than anything derived from the client's OldValues.
1517
+ const hasDeniedReadFields = this.AssertClientInputFieldSecurity(entityInfo, userInfo, input, clientNewValues);
1407
1518
  // The audit-trail equivalent. Its `true` matters most on THIS entity: 'MJ: Record Changes'
1408
1519
  // has TrackRecordChanges off, so without it a client sending OldValues takes the branch below
1409
1520
  // that hydrates from those values — never loading what the database actually holds.
@@ -1459,6 +1570,7 @@ export class ResolverBase {
1459
1570
  await this.AfterUpdate(provider, input); // fire event
1460
1571
  // Cache invalidation is now handled globally by the MJGlobal listener in index.ts
1461
1572
  // MapFieldNamesToCodeNames now handles encryption filtering as well
1573
+ // fls-guard: MapFieldNamesToCodeNames drops the fields the caller may not read
1462
1574
  return await this.MapFieldNamesToCodeNames(entityName, entityObject.GetAll(), userInfo);
1463
1575
  }
1464
1576
  else {
@@ -1510,7 +1622,7 @@ export class ResolverBase {
1510
1622
  * writable fields in `OldValues___`, so {@link hasCompleteOldValues} returns `false` and forces the
1511
1623
  * database load (`InnerLoad`) so untouched columns retain their stored values.
1512
1624
  *
1513
- * `EnableFieldLevelSecurity` is the security-critical one, and it is NOT redundant with
1625
+ * `FieldLevelSecurityApplies` (own flag or an IS-A ancestor's) is the security-critical one, and it is NOT redundant with
1514
1626
  * `hasDeniedReadFields`. That flag reports READ denials, and the canonical FLS configuration is
1515
1627
  * Read Allow + Update Deny — which leaves it false. Such a caller would be hydrated from its own
1516
1628
  * `OldValues___`, and a value it supplies there for an update-denied field arrives through
@@ -1558,60 +1670,43 @@ export class ResolverBase {
1558
1670
  !this.hasCompleteOldValues(entityInfo, input.OldValues___) ||
1559
1671
  hasDeniedReadFields ||
1560
1672
  hasNarrowedAuditPayload ||
1561
- entityInfo.EnableFieldLevelSecurity ||
1673
+ entityInfo.FieldLevelSecurityApplies ||
1562
1674
  !user ||
1563
1675
  entityInfo.GetEffectiveRowFilterWhereClause(user, EntityPermissionType.Update, '').length > 0);
1564
1676
  }
1565
1677
  /**
1566
- * Field-level security guard for the update path.
1678
+ * Field-level security gate on the update path's client input.
1567
1679
  *
1568
- * Every denied-read field is stripped — new values AND OldValues. A field the user cannot
1569
- * read was absent from every payload that client ever received, so any value coming back for
1570
- * it is the transport's invention rather than user intent, and applying it would silently
1571
- * overwrite the real column. Stripping is what makes "load a record, edit an unrelated field,
1572
- * save" safe for a restricted user.
1680
+ * For a caller denied at least one field on the entity, refuses (with a `FieldSecurityError`)
1681
+ * any key in the new values or in `OldValues___` that names a field the caller cannot READ, or
1682
+ * that names no field at all, with the same message for both (see
1683
+ * `AssertClientInputWritableForUser`), so the refusal does not reveal which columns exist.
1573
1684
  *
1574
- * There is no split by update permission: Read is required for Update, so a user denied read
1575
- * is denied update too and denied-read ∩ denied-update is just denied-read.
1685
+ * Refused rather than dropped: a field the caller cannot read was absent from every payload the
1686
+ * client received, so a value for it can only be fabricated. A well-behaved client never sends
1687
+ * one (it omits fields it did not load), so ordinary traffic is unaffected.
1576
1688
  *
1577
- * Silent narrowing, not rejection — consistent with the output projection the client already
1578
- * experiences, and with the ambiguous-error rule (naming the field would confirm it exists
1579
- * and is restricted).
1689
+ * There is no split by update permission: Read is required for Update, so a user denied read is
1690
+ * denied update too.
1580
1691
  *
1581
- * Returns true when the user has a non-empty denied-read set on this entity. The caller
1582
- * must then hydrate the entity from the DATABASE (never from client OldValues), so denied
1583
- * fields hold true values that an omitted key leaves untouched.
1692
+ * Returns true when the user has a non-empty denied-read set on this entity. The caller must
1693
+ * then hydrate the entity from the DATABASE (never from client OldValues), so denied fields hold
1694
+ * true values the client input never touches.
1584
1695
  */
1585
- StripDeniedReadFieldsFromClientInput(entityInfo, userInfo, input, clientNewValues) {
1586
- if (!entityInfo.EnableFieldLevelSecurity || !userInfo) {
1696
+ AssertClientInputFieldSecurity(entityInfo, userInfo, input, clientNewValues) {
1697
+ if (!entityInfo.FieldLevelSecurityApplies || !userInfo) {
1587
1698
  return false;
1588
1699
  }
1589
- const deniedReadNames = entityInfo.GetDeniedReadFields(userInfo);
1590
- if (deniedReadNames.size === 0) {
1700
+ if (entityInfo.GetDeniedReadFields(userInfo).size === 0) {
1591
1701
  return false;
1592
1702
  }
1593
- // Input keys are entity CodeNames (post ReverseMapInputFieldNames); the denied set holds
1594
- // lowercased field Names — bridge via the field metadata once.
1595
- const deniedReadCodeNames = new Set();
1596
- for (const field of entityInfo.Fields) {
1597
- if (deniedReadNames.has(field.Name.trim().toLowerCase())) {
1598
- deniedReadCodeNames.add(field.CodeName.trim().toLowerCase());
1599
- }
1600
- }
1601
- const stripped = [];
1602
- for (const key of Object.keys(clientNewValues)) {
1603
- if (deniedReadCodeNames.has(key.trim().toLowerCase())) {
1604
- delete clientNewValues[key];
1605
- delete input[key];
1606
- stripped.push(key);
1607
- }
1608
- }
1703
+ AssertClientInputWritableForUser(entityInfo, clientNewValues, userInfo);
1609
1704
  if (Array.isArray(input.OldValues___)) {
1610
- input.OldValues___ = input.OldValues___.filter((item) => !deniedReadCodeNames.has(String(item.Key).trim().toLowerCase()));
1611
- }
1612
- if (stripped.length > 0) {
1613
- LogDebug(`[FieldSecurity] UpdateRecord on '${entityInfo.Name}' for user ${userInfo.Email}: ` +
1614
- `stripped client-sent value(s) for denied-read field(s) ${stripped.join(', ')}`);
1705
+ const oldValueKeys = {};
1706
+ for (const item of input.OldValues___) {
1707
+ oldValueKeys[String(item.Key)] = null;
1708
+ }
1709
+ AssertClientInputWritableForUser(entityInfo, oldValueKeys, userInfo);
1615
1710
  }
1616
1711
  return true;
1617
1712
  }
@@ -1740,6 +1835,7 @@ export class ResolverBase {
1740
1835
  });
1741
1836
  // clientOldValues now has all of the oldValues the CLIENT passed us. Now we need to build the same kind of object
1742
1837
  // with the DB values
1838
+ // fls-guard: compared on the server only, never returned
1743
1839
  const dbValues = entityObject.GetAll();
1744
1840
  // now we need to compare clientOldValues and dbValues and have a new array that has entries for any differences and have FieldName, clientOldValue and dbValue as properties
1745
1841
  const dbDifferences = [];
@@ -1820,30 +1916,31 @@ export class ResolverBase {
1820
1916
  };
1821
1917
  // Log as warning to console and ErrorLog table instead of throwing error
1822
1918
  console.warn('Entity save inconsistency detected but allowing save to continue:', JSON.stringify(msg));
1823
- LogError({
1824
- service: 'ResolverBase',
1825
- operation: 'TestAndSetClientOldValuesToDBValues',
1826
- error: `Entity save inconsistency detected: ${JSON.stringify(msg)}`,
1827
- details: {
1919
+ LogErrorEx({
1920
+ message: `Entity save inconsistency detected: ${JSON.stringify(msg)}`,
1921
+ severity: 'warning',
1922
+ category: 'ResolverBase.TestAndSetClientOldValuesToDBValues',
1923
+ metadata: {
1828
1924
  entityName: entityObject.EntityInfo.Name,
1829
1925
  clientDifferences: clientDifferences,
1830
1926
  dbDifferences: dbDifferences,
1831
- overlap: overlap
1832
- }
1927
+ overlap: overlap,
1928
+ },
1833
1929
  });
1834
1930
  // Create ErrorLog record in the database — use the entity's bound provider so the
1835
1931
  // ErrorLog write goes to the same connection as the entity that triggered it.
1836
1932
  try {
1837
1933
  const md = entityObject.ProviderToUse;
1838
1934
  const errorLogEntity = await md.GetEntityObject('MJ: Error Logs', contextUser);
1839
- errorLogEntity.Code = 'ENTITY_SAVE_INCONSISTENCY';
1935
+ // Code is NCHAR(20); a longer value is rejected at the database
1936
+ errorLogEntity.Code = 'SAVE_INCONSISTENCY';
1840
1937
  errorLogEntity.Message = `Entity save inconsistency detected for ${entityObject.EntityInfo.Name}: ${JSON.stringify(msg)}`;
1841
1938
  errorLogEntity.Status = 'Warning';
1842
1939
  errorLogEntity.Category = 'Entity Save';
1843
1940
  errorLogEntity.CreatedBy = contextUser.Email || contextUser.Name;
1844
1941
  const saveResult = await errorLogEntity.Save();
1845
1942
  if (!saveResult) {
1846
- console.error('Failed to save ErrorLog record');
1943
+ console.error(`Failed to save ErrorLog record for ${entityObject.EntityInfo.Name}: ${errorLogEntity.LatestResult?.CompleteMessage}`);
1847
1944
  }
1848
1945
  }
1849
1946
  catch (errorLogError) {
@@ -1872,7 +1969,10 @@ export class ResolverBase {
1872
1969
  extensions: { code: 'LOAD_ENTITY_ERROR', entityName },
1873
1970
  });
1874
1971
  }
1875
- const returnValue = entityObject.GetAll(); // grab the values before we delete so we can return last state before delete if we are successful.
1972
+ // Grab the values before we delete so we can return the last state if the delete succeeds,
1973
+ // projected for the caller in case the object holds a column they may not read.
1974
+ // fls-guard: projected by ProjectEntityValuesForUser
1975
+ const returnValue = ProjectEntityValuesForUser(provider, entityObject.EntityInfo, entityObject.GetAll(), this.GetUserFromPayload(userPayload));
1876
1976
  this.ListenForEntityMessages(entityObject, pubSub, userPayload);
1877
1977
  if (await entityObject.Delete(options)) {
1878
1978
  await this.AfterDelete(provider, key); // fire event