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

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 (664) hide show
  1. package/README.md +96 -1
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/agentSessions/BridgeHostInstance.d.ts +8 -3
  4. package/dist/agentSessions/BridgeHostInstance.d.ts.map +1 -1
  5. package/dist/agentSessions/BridgeHostInstance.js +14 -4
  6. package/dist/agentSessions/BridgeHostInstance.js.map +1 -1
  7. package/dist/agentSessions/HandoffOfferExpiry.d.ts +10 -0
  8. package/dist/agentSessions/HandoffOfferExpiry.d.ts.map +1 -0
  9. package/dist/agentSessions/HandoffOfferExpiry.js +18 -0
  10. package/dist/agentSessions/HandoffOfferExpiry.js.map +1 -0
  11. package/dist/agentSessions/ReturningVisitorRecap.js +1 -0
  12. package/dist/agentSessions/ReturningVisitorRecap.js.map +1 -1
  13. package/dist/agentSessions/SessionJanitor.d.ts +34 -11
  14. package/dist/agentSessions/SessionJanitor.d.ts.map +1 -1
  15. package/dist/agentSessions/SessionJanitor.js +88 -44
  16. package/dist/agentSessions/SessionJanitor.js.map +1 -1
  17. package/dist/agentSessions/SessionManager.d.ts +29 -9
  18. package/dist/agentSessions/SessionManager.d.ts.map +1 -1
  19. package/dist/agentSessions/SessionManager.js +62 -16
  20. package/dist/agentSessions/SessionManager.js.map +1 -1
  21. package/dist/agentSessions/conversationBrowsers.d.ts +61 -0
  22. package/dist/agentSessions/conversationBrowsers.d.ts.map +1 -0
  23. package/dist/agentSessions/conversationBrowsers.js +118 -0
  24. package/dist/agentSessions/conversationBrowsers.js.map +1 -0
  25. package/dist/agentSessions/sessionDeadline.d.ts +43 -0
  26. package/dist/agentSessions/sessionDeadline.d.ts.map +1 -0
  27. package/dist/agentSessions/sessionDeadline.js +68 -0
  28. package/dist/agentSessions/sessionDeadline.js.map +1 -0
  29. package/dist/auth/APIKeyScopeAuth.d.ts.map +1 -1
  30. package/dist/auth/APIKeyScopeAuth.js +5 -4
  31. package/dist/auth/APIKeyScopeAuth.js.map +1 -1
  32. package/dist/auth/AuthProviderCatalogRouter.d.ts.map +1 -1
  33. package/dist/auth/AuthProviderCatalogRouter.js +2 -0
  34. package/dist/auth/AuthProviderCatalogRouter.js.map +1 -1
  35. package/dist/auth/index.d.ts.map +1 -1
  36. package/dist/auth/index.js +51 -10
  37. package/dist/auth/index.js.map +1 -1
  38. package/dist/auth/initializeProviders.d.ts +10 -0
  39. package/dist/auth/initializeProviders.d.ts.map +1 -1
  40. package/dist/auth/initializeProviders.js +37 -1
  41. package/dist/auth/initializeProviders.js.map +1 -1
  42. package/dist/auth/localDevAuth.d.ts +79 -0
  43. package/dist/auth/localDevAuth.d.ts.map +1 -0
  44. package/dist/auth/localDevAuth.js +439 -0
  45. package/dist/auth/localDevAuth.js.map +1 -0
  46. package/dist/auth/loginEmailResolution.d.ts +41 -0
  47. package/dist/auth/loginEmailResolution.d.ts.map +1 -0
  48. package/dist/auth/loginEmailResolution.js +47 -0
  49. package/dist/auth/loginEmailResolution.js.map +1 -0
  50. package/dist/auth/magicLink/MagicLinkKeys.d.ts +51 -3
  51. package/dist/auth/magicLink/MagicLinkKeys.d.ts.map +1 -1
  52. package/dist/auth/magicLink/MagicLinkKeys.js +130 -13
  53. package/dist/auth/magicLink/MagicLinkKeys.js.map +1 -1
  54. package/dist/auth/magicLink/MagicLinkRouter.d.ts +3 -0
  55. package/dist/auth/magicLink/MagicLinkRouter.d.ts.map +1 -1
  56. package/dist/auth/magicLink/MagicLinkRouter.js +23 -5
  57. package/dist/auth/magicLink/MagicLinkRouter.js.map +1 -1
  58. package/dist/auth/magicLink/MagicLinkService.d.ts +39 -5
  59. package/dist/auth/magicLink/MagicLinkService.d.ts.map +1 -1
  60. package/dist/auth/magicLink/MagicLinkService.js +221 -9
  61. package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
  62. package/dist/auth/magicLink/index.d.ts +1 -1
  63. package/dist/auth/magicLink/index.d.ts.map +1 -1
  64. package/dist/auth/magicLink/index.js +1 -1
  65. package/dist/auth/magicLink/index.js.map +1 -1
  66. package/dist/auth/magicLink/redeemLanding.d.ts +2 -2
  67. package/dist/auth/magicLink/redeemLanding.d.ts.map +1 -1
  68. package/dist/auth/magicLink/redeemLanding.js +7 -4
  69. package/dist/auth/magicLink/redeemLanding.js.map +1 -1
  70. package/dist/auth/magicLink/types.d.ts +39 -3
  71. package/dist/auth/magicLink/types.d.ts.map +1 -1
  72. package/dist/auth/magicLink/types.js +0 -4
  73. package/dist/auth/magicLink/types.js.map +1 -1
  74. package/dist/auth/newUsers.d.ts.map +1 -1
  75. package/dist/auth/newUsers.js +1 -0
  76. package/dist/auth/newUsers.js.map +1 -1
  77. package/dist/auth/sessionUserClone.d.ts +1 -1
  78. package/dist/auth/sessionUserClone.d.ts.map +1 -1
  79. package/dist/auth/sessionUserClone.js +3 -2
  80. package/dist/auth/sessionUserClone.js.map +1 -1
  81. package/dist/backgroundSweepConfigUnits.d.ts +231 -0
  82. package/dist/backgroundSweepConfigUnits.d.ts.map +1 -0
  83. package/dist/backgroundSweepConfigUnits.js +180 -0
  84. package/dist/backgroundSweepConfigUnits.js.map +1 -0
  85. package/dist/broadcast/BaseCacheInvalidationBroadcastProvider.d.ts +292 -0
  86. package/dist/broadcast/BaseCacheInvalidationBroadcastProvider.d.ts.map +1 -0
  87. package/dist/broadcast/BaseCacheInvalidationBroadcastProvider.js +442 -0
  88. package/dist/broadcast/BaseCacheInvalidationBroadcastProvider.js.map +1 -0
  89. package/dist/broadcast/index.d.ts +6 -0
  90. package/dist/broadcast/index.d.ts.map +1 -0
  91. package/dist/broadcast/index.js +6 -0
  92. package/dist/broadcast/index.js.map +1 -0
  93. package/dist/broadcast/providers/AWSIoTCoreBroadcastProvider.d.ts +43 -0
  94. package/dist/broadcast/providers/AWSIoTCoreBroadcastProvider.d.ts.map +1 -0
  95. package/dist/broadcast/providers/AWSIoTCoreBroadcastProvider.js +144 -0
  96. package/dist/broadcast/providers/AWSIoTCoreBroadcastProvider.js.map +1 -0
  97. package/dist/broadcast/providers/AzureWebPubSubBroadcastProvider.d.ts +46 -0
  98. package/dist/broadcast/providers/AzureWebPubSubBroadcastProvider.d.ts.map +1 -0
  99. package/dist/broadcast/providers/AzureWebPubSubBroadcastProvider.js +149 -0
  100. package/dist/broadcast/providers/AzureWebPubSubBroadcastProvider.js.map +1 -0
  101. package/dist/broadcast/providers/GCPPubSubBroadcastProvider.d.ts +49 -0
  102. package/dist/broadcast/providers/GCPPubSubBroadcastProvider.d.ts.map +1 -0
  103. package/dist/broadcast/providers/GCPPubSubBroadcastProvider.js +131 -0
  104. package/dist/broadcast/providers/GCPPubSubBroadcastProvider.js.map +1 -0
  105. package/dist/broadcast/providers/SequencedRoleBucketBroadcastProvider.d.ts +24 -0
  106. package/dist/broadcast/providers/SequencedRoleBucketBroadcastProvider.d.ts.map +1 -0
  107. package/dist/broadcast/providers/SequencedRoleBucketBroadcastProvider.js +51 -0
  108. package/dist/broadcast/providers/SequencedRoleBucketBroadcastProvider.js.map +1 -0
  109. package/dist/cache.d.ts +3 -2
  110. package/dist/cache.d.ts.map +1 -1
  111. package/dist/cache.js.map +1 -1
  112. package/dist/config.d.ts +1402 -8
  113. package/dist/config.d.ts.map +1 -1
  114. package/dist/config.js +120 -0
  115. package/dist/config.js.map +1 -1
  116. package/dist/context.d.ts +5 -1
  117. package/dist/context.d.ts.map +1 -1
  118. package/dist/context.js +64 -12
  119. package/dist/context.js.map +1 -1
  120. package/dist/coreRoutes.d.ts +3 -1
  121. package/dist/coreRoutes.d.ts.map +1 -1
  122. package/dist/coreRoutes.js +20 -4
  123. package/dist/coreRoutes.js.map +1 -1
  124. package/dist/generated/graphql-schemas/__mj.d.ts +863 -14
  125. package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
  126. package/dist/generated/graphql-schemas/__mj.js +4167 -62
  127. package/dist/generated/graphql-schemas/__mj.js.map +1 -1
  128. package/dist/generic/CacheInvalidationResolver.d.ts +50 -30
  129. package/dist/generic/CacheInvalidationResolver.d.ts.map +1 -1
  130. package/dist/generic/CacheInvalidationResolver.js +154 -79
  131. package/dist/generic/CacheInvalidationResolver.js.map +1 -1
  132. package/dist/generic/OrphanedConversationDetailReconciler.d.ts +11 -0
  133. package/dist/generic/OrphanedConversationDetailReconciler.d.ts.map +1 -1
  134. package/dist/generic/OrphanedConversationDetailReconciler.js +29 -1
  135. package/dist/generic/OrphanedConversationDetailReconciler.js.map +1 -1
  136. package/dist/generic/PubSubManager.d.ts +10 -0
  137. package/dist/generic/PubSubManager.d.ts.map +1 -1
  138. package/dist/generic/PubSubManager.js +44 -1
  139. package/dist/generic/PubSubManager.js.map +1 -1
  140. package/dist/generic/PushStatusResolver.d.ts +39 -6
  141. package/dist/generic/PushStatusResolver.d.ts.map +1 -1
  142. package/dist/generic/PushStatusResolver.js +93 -11
  143. package/dist/generic/PushStatusResolver.js.map +1 -1
  144. package/dist/generic/ResolverBase.d.ts +114 -29
  145. package/dist/generic/ResolverBase.d.ts.map +1 -1
  146. package/dist/generic/ResolverBase.js +509 -220
  147. package/dist/generic/ResolverBase.js.map +1 -1
  148. package/dist/generic/RunViewResolver.d.ts +15 -3
  149. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  150. package/dist/generic/RunViewResolver.js +126 -20
  151. package/dist/generic/RunViewResolver.js.map +1 -1
  152. package/dist/generic/actionOutputFieldSecurity.d.ts +26 -0
  153. package/dist/generic/actionOutputFieldSecurity.d.ts.map +1 -0
  154. package/dist/generic/actionOutputFieldSecurity.js +47 -0
  155. package/dist/generic/actionOutputFieldSecurity.js.map +1 -0
  156. package/dist/generic/fieldSecurityTransport.d.ts +30 -0
  157. package/dist/generic/fieldSecurityTransport.d.ts.map +1 -0
  158. package/dist/generic/fieldSecurityTransport.js +30 -0
  159. package/dist/generic/fieldSecurityTransport.js.map +1 -0
  160. package/dist/index.d.ts +4 -0
  161. package/dist/index.d.ts.map +1 -1
  162. package/dist/index.js +262 -84
  163. package/dist/index.js.map +1 -1
  164. package/dist/integration/ConnectionPause.d.ts +121 -0
  165. package/dist/integration/ConnectionPause.d.ts.map +1 -0
  166. package/dist/integration/ConnectionPause.js +183 -0
  167. package/dist/integration/ConnectionPause.js.map +1 -0
  168. package/dist/integration/CustomColumnPromoter.d.ts +72 -6
  169. package/dist/integration/CustomColumnPromoter.d.ts.map +1 -1
  170. package/dist/integration/CustomColumnPromoter.js +93 -47
  171. package/dist/integration/CustomColumnPromoter.js.map +1 -1
  172. package/dist/integration/EntityMapLifecycle.d.ts +78 -4
  173. package/dist/integration/EntityMapLifecycle.d.ts.map +1 -1
  174. package/dist/integration/EntityMapLifecycle.js +199 -5
  175. package/dist/integration/EntityMapLifecycle.js.map +1 -1
  176. package/dist/integration/InactiveRowWarnings.d.ts +31 -0
  177. package/dist/integration/InactiveRowWarnings.d.ts.map +1 -1
  178. package/dist/integration/InactiveRowWarnings.js +29 -0
  179. package/dist/integration/InactiveRowWarnings.js.map +1 -1
  180. package/dist/integration/ObjectNamesByID.d.ts +28 -0
  181. package/dist/integration/ObjectNamesByID.d.ts.map +1 -0
  182. package/dist/integration/ObjectNamesByID.js +87 -0
  183. package/dist/integration/ObjectNamesByID.js.map +1 -0
  184. package/dist/integration/PostRestartIntrospect.d.ts +26 -0
  185. package/dist/integration/PostRestartIntrospect.d.ts.map +1 -0
  186. package/dist/integration/PostRestartIntrospect.js +27 -0
  187. package/dist/integration/PostRestartIntrospect.js.map +1 -0
  188. package/dist/integration/RSUPostRestartProgress.d.ts +176 -0
  189. package/dist/integration/RSUPostRestartProgress.d.ts.map +1 -0
  190. package/dist/integration/RSUPostRestartProgress.js +323 -0
  191. package/dist/integration/RSUPostRestartProgress.js.map +1 -0
  192. package/dist/integration/RSUProgressBridge.d.ts +41 -2
  193. package/dist/integration/RSUProgressBridge.d.ts.map +1 -1
  194. package/dist/integration/RSUProgressBridge.js +84 -1
  195. package/dist/integration/RSUProgressBridge.js.map +1 -1
  196. package/dist/integration/RunArtifactAuthorization.d.ts +39 -0
  197. package/dist/integration/RunArtifactAuthorization.d.ts.map +1 -0
  198. package/dist/integration/RunArtifactAuthorization.js +45 -0
  199. package/dist/integration/RunArtifactAuthorization.js.map +1 -0
  200. package/dist/integration/SchemaRefreshLaunch.d.ts +9 -0
  201. package/dist/integration/SchemaRefreshLaunch.d.ts.map +1 -1
  202. package/dist/integration/SchemaRefreshLaunch.js +3 -2
  203. package/dist/integration/SchemaRefreshLaunch.js.map +1 -1
  204. package/dist/integration/ScopedFieldsForBuild.d.ts +46 -0
  205. package/dist/integration/ScopedFieldsForBuild.d.ts.map +1 -0
  206. package/dist/integration/ScopedFieldsForBuild.js +91 -0
  207. package/dist/integration/ScopedFieldsForBuild.js.map +1 -0
  208. package/dist/integration/TargetConfigGuard.d.ts +40 -0
  209. package/dist/integration/TargetConfigGuard.d.ts.map +1 -0
  210. package/dist/integration/TargetConfigGuard.js +40 -0
  211. package/dist/integration/TargetConfigGuard.js.map +1 -0
  212. package/dist/logging/variablesLoggingMiddleware.d.ts +12 -0
  213. package/dist/logging/variablesLoggingMiddleware.d.ts.map +1 -1
  214. package/dist/logging/variablesLoggingMiddleware.js +50 -1
  215. package/dist/logging/variablesLoggingMiddleware.js.map +1 -1
  216. package/dist/realtimeSessions/SessionVerificationPortsBase.d.ts.map +1 -1
  217. package/dist/realtimeSessions/SessionVerificationPortsBase.js +1 -0
  218. package/dist/realtimeSessions/SessionVerificationPortsBase.js.map +1 -1
  219. package/dist/realtimeSessions/ownedRealtimeSession.d.ts +8 -6
  220. package/dist/realtimeSessions/ownedRealtimeSession.d.ts.map +1 -1
  221. package/dist/realtimeSessions/ownedRealtimeSession.js +14 -6
  222. package/dist/realtimeSessions/ownedRealtimeSession.js.map +1 -1
  223. package/dist/realtimeSessions/sessionAccess.d.ts +103 -11
  224. package/dist/realtimeSessions/sessionAccess.d.ts.map +1 -1
  225. package/dist/realtimeSessions/sessionAccess.js +147 -22
  226. package/dist/realtimeSessions/sessionAccess.js.map +1 -1
  227. package/dist/realtimeSessions/sessionConfigStamps.js +1 -1
  228. package/dist/realtimeSessions/sessionConfigStamps.js.map +1 -1
  229. package/dist/realtimeSessions/subscriptionAuthorization.d.ts.map +1 -1
  230. package/dist/realtimeSessions/subscriptionAuthorization.js +5 -2
  231. package/dist/realtimeSessions/subscriptionAuthorization.js.map +1 -1
  232. package/dist/realtimeSessions/verificationWorkflow.d.ts +2 -0
  233. package/dist/realtimeSessions/verificationWorkflow.d.ts.map +1 -1
  234. package/dist/realtimeSessions/verificationWorkflow.js +1 -1
  235. package/dist/realtimeSessions/verificationWorkflow.js.map +1 -1
  236. package/dist/realtimeWidget/WidgetRouter.d.ts.map +1 -1
  237. package/dist/realtimeWidget/WidgetRouter.js +19 -3
  238. package/dist/realtimeWidget/WidgetRouter.js.map +1 -1
  239. package/dist/realtimeWidget/WidgetSessionService.d.ts +67 -7
  240. package/dist/realtimeWidget/WidgetSessionService.d.ts.map +1 -1
  241. package/dist/realtimeWidget/WidgetSessionService.js +203 -19
  242. package/dist/realtimeWidget/WidgetSessionService.js.map +1 -1
  243. package/dist/realtimeWidget/host-identity.d.ts +1 -1
  244. package/dist/realtimeWidget/host-identity.js +1 -1
  245. package/dist/realtimeWidget/visitorIdentity.d.ts.map +1 -1
  246. package/dist/realtimeWidget/visitorIdentity.js +1 -0
  247. package/dist/realtimeWidget/visitorIdentity.js.map +1 -1
  248. package/dist/realtimeWidget/widgetCore.d.ts +20 -1
  249. package/dist/realtimeWidget/widgetCore.d.ts.map +1 -1
  250. package/dist/realtimeWidget/widgetCore.js +21 -8
  251. package/dist/realtimeWidget/widgetCore.js.map +1 -1
  252. package/dist/realtimeWidget/widgetGuestElevation.d.ts +1 -1
  253. package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -1
  254. package/dist/realtimeWidget/widgetGuestElevation.js +3 -1
  255. package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
  256. package/dist/requestOrigin.d.ts +22 -0
  257. package/dist/requestOrigin.d.ts.map +1 -0
  258. package/dist/requestOrigin.js +60 -0
  259. package/dist/requestOrigin.js.map +1 -0
  260. package/dist/resolvers/ActionResolver.d.ts +19 -1
  261. package/dist/resolvers/ActionResolver.d.ts.map +1 -1
  262. package/dist/resolvers/ActionResolver.js +63 -12
  263. package/dist/resolvers/ActionResolver.js.map +1 -1
  264. package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
  265. package/dist/resolvers/AdhocQueryResolver.js +1 -0
  266. package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
  267. package/dist/resolvers/AgentRunStatusPublisher.d.ts +18 -4
  268. package/dist/resolvers/AgentRunStatusPublisher.d.ts.map +1 -1
  269. package/dist/resolvers/AgentRunStatusPublisher.js +32 -8
  270. package/dist/resolvers/AgentRunStatusPublisher.js.map +1 -1
  271. package/dist/resolvers/AgentSessionResolver.d.ts +10 -4
  272. package/dist/resolvers/AgentSessionResolver.d.ts.map +1 -1
  273. package/dist/resolvers/AgentSessionResolver.js +44 -17
  274. package/dist/resolvers/AgentSessionResolver.js.map +1 -1
  275. package/dist/resolvers/AutotagPipelineResolver.d.ts.map +1 -1
  276. package/dist/resolvers/AutotagPipelineResolver.js +2 -1
  277. package/dist/resolvers/AutotagPipelineResolver.js.map +1 -1
  278. package/dist/resolvers/ConversationRunEventsResolver.d.ts.map +1 -1
  279. package/dist/resolvers/ConversationRunEventsResolver.js +2 -0
  280. package/dist/resolvers/ConversationRunEventsResolver.js.map +1 -1
  281. package/dist/resolvers/EntityCommunicationsResolver.d.ts +13 -1
  282. package/dist/resolvers/EntityCommunicationsResolver.d.ts.map +1 -1
  283. package/dist/resolvers/EntityCommunicationsResolver.js +50 -14
  284. package/dist/resolvers/EntityCommunicationsResolver.js.map +1 -1
  285. package/dist/resolvers/ExecuteRemoteOperationResolver.d.ts +6 -0
  286. package/dist/resolvers/ExecuteRemoteOperationResolver.d.ts.map +1 -1
  287. package/dist/resolvers/ExecuteRemoteOperationResolver.js +30 -7
  288. package/dist/resolvers/ExecuteRemoteOperationResolver.js.map +1 -1
  289. package/dist/resolvers/FetchEntityVectorsResolver.d.ts +28 -0
  290. package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
  291. package/dist/resolvers/FetchEntityVectorsResolver.js +117 -21
  292. package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
  293. package/dist/resolvers/FileCategoryResolver.d.ts.map +1 -1
  294. package/dist/resolvers/FileCategoryResolver.js +1 -0
  295. package/dist/resolvers/FileCategoryResolver.js.map +1 -1
  296. package/dist/resolvers/FileResolver.d.ts +5 -0
  297. package/dist/resolvers/FileResolver.d.ts.map +1 -1
  298. package/dist/resolvers/FileResolver.js +13 -4
  299. package/dist/resolvers/FileResolver.js.map +1 -1
  300. package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
  301. package/dist/resolvers/GetDataResolver.js +34 -13
  302. package/dist/resolvers/GetDataResolver.js.map +1 -1
  303. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +401 -11
  304. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
  305. package/dist/resolvers/IntegrationDiscoveryResolver.js +2713 -1290
  306. package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
  307. package/dist/resolvers/MCPResolver.d.ts.map +1 -1
  308. package/dist/resolvers/MCPResolver.js +13 -2
  309. package/dist/resolvers/MCPResolver.js.map +1 -1
  310. package/dist/resolvers/MergeRecordsResolver.d.ts +8 -1
  311. package/dist/resolvers/MergeRecordsResolver.d.ts.map +1 -1
  312. package/dist/resolvers/MergeRecordsResolver.js +31 -3
  313. package/dist/resolvers/MergeRecordsResolver.js.map +1 -1
  314. package/dist/resolvers/PipelineProgressResolver.d.ts +6 -0
  315. package/dist/resolvers/PipelineProgressResolver.d.ts.map +1 -1
  316. package/dist/resolvers/PipelineProgressResolver.js +5 -3
  317. package/dist/resolvers/PipelineProgressResolver.js.map +1 -1
  318. package/dist/resolvers/QueryResolver.d.ts.map +1 -1
  319. package/dist/resolvers/QueryResolver.js +20 -6
  320. package/dist/resolvers/QueryResolver.js.map +1 -1
  321. package/dist/resolvers/QuerySystemUserResolver.d.ts +38 -0
  322. package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
  323. package/dist/resolvers/QuerySystemUserResolver.js +184 -88
  324. package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
  325. package/dist/resolvers/RSUResolver.d.ts.map +1 -1
  326. package/dist/resolvers/RSUResolver.js +12 -0
  327. package/dist/resolvers/RSUResolver.js.map +1 -1
  328. package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
  329. package/dist/resolvers/RealtimeBridgeResolver.js +1 -0
  330. package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
  331. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +22 -2
  332. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  333. package/dist/resolvers/RealtimeClientSessionResolver.js +118 -33
  334. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  335. package/dist/resolvers/RemoteBrowserActionResolver.d.ts +62 -10
  336. package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
  337. package/dist/resolvers/RemoteBrowserActionResolver.js +224 -39
  338. package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
  339. package/dist/resolvers/RunAIAgentResolver.d.ts +80 -16
  340. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  341. package/dist/resolvers/RunAIAgentResolver.js +388 -125
  342. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  343. package/dist/resolvers/RunAIPromptResolver.d.ts +5 -3
  344. package/dist/resolvers/RunAIPromptResolver.d.ts.map +1 -1
  345. package/dist/resolvers/RunAIPromptResolver.js +19 -18
  346. package/dist/resolvers/RunAIPromptResolver.js.map +1 -1
  347. package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
  348. package/dist/resolvers/RunTemplateResolver.js +7 -5
  349. package/dist/resolvers/RunTemplateResolver.js.map +1 -1
  350. package/dist/resolvers/RunTestResolver.d.ts.map +1 -1
  351. package/dist/resolvers/RunTestResolver.js +3 -1
  352. package/dist/resolvers/RunTestResolver.js.map +1 -1
  353. package/dist/resolvers/SyncDataResolver.d.ts.map +1 -1
  354. package/dist/resolvers/SyncDataResolver.js +4 -0
  355. package/dist/resolvers/SyncDataResolver.js.map +1 -1
  356. package/dist/resolvers/TestQuerySQLResolver.d.ts.map +1 -1
  357. package/dist/resolvers/TestQuerySQLResolver.js +1 -0
  358. package/dist/resolvers/TestQuerySQLResolver.js.map +1 -1
  359. package/dist/resolvers/TransactionGroupResolver.d.ts +17 -2
  360. package/dist/resolvers/TransactionGroupResolver.d.ts.map +1 -1
  361. package/dist/resolvers/TransactionGroupResolver.js +60 -15
  362. package/dist/resolvers/TransactionGroupResolver.js.map +1 -1
  363. package/dist/resolvers/UserViewResolver.d.ts +1 -1
  364. package/dist/resolvers/UserViewResolver.d.ts.map +1 -1
  365. package/dist/resolvers/UserViewResolver.js +2 -1
  366. package/dist/resolvers/UserViewResolver.js.map +1 -1
  367. package/dist/resolvers/meetingRecordingRegistration.d.ts.map +1 -1
  368. package/dist/resolvers/meetingRecordingRegistration.js +2 -0
  369. package/dist/resolvers/meetingRecordingRegistration.js.map +1 -1
  370. package/dist/rest/EntityCRUDHandler.d.ts +6 -0
  371. package/dist/rest/EntityCRUDHandler.d.ts.map +1 -1
  372. package/dist/rest/EntityCRUDHandler.js +19 -5
  373. package/dist/rest/EntityCRUDHandler.js.map +1 -1
  374. package/dist/rest/MediaAccessKeys.d.ts.map +1 -1
  375. package/dist/rest/MediaAccessKeys.js +6 -2
  376. package/dist/rest/MediaAccessKeys.js.map +1 -1
  377. package/dist/rest/OAuthCallbackHandler.d.ts.map +1 -1
  378. package/dist/rest/OAuthCallbackHandler.js +11 -5
  379. package/dist/rest/OAuthCallbackHandler.js.map +1 -1
  380. package/dist/rest/RESTEndpointHandler.d.ts.map +1 -1
  381. package/dist/rest/RESTEndpointHandler.js +8 -5
  382. package/dist/rest/RESTEndpointHandler.js.map +1 -1
  383. package/dist/rest/RealtimeRecordingHandler.d.ts +3 -3
  384. package/dist/rest/RealtimeRecordingHandler.d.ts.map +1 -1
  385. package/dist/rest/RealtimeRecordingHandler.js +6 -8
  386. package/dist/rest/RealtimeRecordingHandler.js.map +1 -1
  387. package/dist/rest/ViewOperationsHandler.d.ts +6 -0
  388. package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
  389. package/dist/rest/ViewOperationsHandler.js +24 -6
  390. package/dist/rest/ViewOperationsHandler.js.map +1 -1
  391. package/dist/rest/setupRESTEndpoints.d.ts +3 -1
  392. package/dist/rest/setupRESTEndpoints.d.ts.map +1 -1
  393. package/dist/rest/setupRESTEndpoints.js +11 -2
  394. package/dist/rest/setupRESTEndpoints.js.map +1 -1
  395. package/dist/serverRoleConfigUnits.d.ts +29 -0
  396. package/dist/serverRoleConfigUnits.d.ts.map +1 -0
  397. package/dist/serverRoleConfigUnits.js +33 -0
  398. package/dist/serverRoleConfigUnits.js.map +1 -0
  399. package/dist/services/DurableEntityActionTaskSubmitter.d.ts.map +1 -1
  400. package/dist/services/DurableEntityActionTaskSubmitter.js +6 -1
  401. package/dist/services/DurableEntityActionTaskSubmitter.js.map +1 -1
  402. package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
  403. package/dist/services/TaskGraphActionRunner.js +2 -26
  404. package/dist/services/TaskGraphActionRunner.js.map +1 -1
  405. package/dist/services/TaskGraphAgentRunner.d.ts +6 -0
  406. package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -1
  407. package/dist/services/TaskGraphAgentRunner.js +38 -0
  408. package/dist/services/TaskGraphAgentRunner.js.map +1 -1
  409. package/dist/services/TaskGraphContinuationDeliverer.d.ts +5 -0
  410. package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
  411. package/dist/services/TaskGraphContinuationDeliverer.js +43 -15
  412. package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
  413. package/dist/services/WorkQueueHostService.d.ts +2 -1
  414. package/dist/services/WorkQueueHostService.d.ts.map +1 -1
  415. package/dist/services/WorkQueueHostService.js +19 -4
  416. package/dist/services/WorkQueueHostService.js.map +1 -1
  417. package/dist/services/workQueueConfig.d.ts +2 -2
  418. package/dist/services/workQueueConfig.js +5 -5
  419. package/dist/services/workQueueConfig.js.map +1 -1
  420. package/dist/sharedCache.js +1 -1
  421. package/dist/sharedCache.js.map +1 -1
  422. package/dist/types.d.ts +40 -1
  423. package/dist/types.d.ts.map +1 -1
  424. package/dist/types.js.map +1 -1
  425. package/package.json +98 -98
  426. package/src/__tests__/APIKeyScopeAuth.test.ts +4 -8
  427. package/src/__tests__/ActionResolver.fieldSecurity.test.ts +237 -0
  428. package/src/__tests__/ActiveOperations.test.ts +68 -0
  429. package/src/__tests__/AdhocQueryResolver.dialect.test.ts +137 -0
  430. package/src/__tests__/AgentRunStatusPublisher.test.ts +49 -0
  431. package/src/__tests__/AgentSessionResolver.ownership.test.ts +150 -0
  432. package/src/__tests__/AssertTargetConfigs.test.ts +75 -0
  433. package/src/__tests__/BatchApplyParity.test.ts +109 -0
  434. package/src/__tests__/BridgeHostInstance.test.ts +42 -0
  435. package/src/__tests__/CollectInactiveRowWarnings.test.ts +115 -0
  436. package/src/__tests__/ConnectionPause.test.ts +192 -0
  437. package/src/__tests__/ConnectionPauseResolver.test.ts +166 -0
  438. package/src/__tests__/ConnectionPauseSave.test.ts +143 -0
  439. package/src/__tests__/CustomColumnPromoterCompletion.test.ts +142 -0
  440. package/src/__tests__/CustomColumnPromoterOnePass.test.ts +15 -1
  441. package/src/__tests__/DeleteConnectionRemovesPerConnectionCatalog.test.ts +51 -0
  442. package/src/__tests__/DetachedDiscoveryStart.test.ts +65 -0
  443. package/src/__tests__/DurableEntityActionTaskSubmitter.test.ts +73 -0
  444. package/src/__tests__/EntityCommunicationsResolver.actingUser.test.ts +151 -0
  445. package/src/__tests__/EntityMapLifecycle.rekey.test.ts +252 -0
  446. package/src/__tests__/EvolutionActsOnWhatItDetects.test.ts +113 -0
  447. package/src/__tests__/ExecuteRemoteOperationResolver.test.ts +213 -0
  448. package/src/__tests__/FetchEntityVectorsResolver.test.ts +206 -3
  449. package/src/__tests__/FileResolver.fieldSecurity.test.ts +82 -0
  450. package/src/__tests__/HandoffOfferExpiry.test.ts +40 -0
  451. package/src/__tests__/IntegrationRunArtifactAuthorization.test.ts +126 -0
  452. package/src/__tests__/MagicLinkKeys.test.ts +102 -0
  453. package/src/__tests__/MediaAccessKeys.test.ts +57 -10
  454. package/src/__tests__/NewArrivalsDefaultDisabled.test.ts +50 -0
  455. package/src/__tests__/ObjectNamesByID.test.ts +176 -0
  456. package/src/__tests__/PipelineProgressResolver.filter.test.ts +58 -0
  457. package/src/__tests__/PostRestartMapsInCatalogScope.test.ts +146 -0
  458. package/src/__tests__/QuerySystemUserResolver.childRows.test.ts +80 -0
  459. package/src/__tests__/QuerySystemUserResolver.collision.test.ts +200 -0
  460. package/src/__tests__/QuerySystemUserResolver.fieldSecurity.test.ts +49 -0
  461. package/src/__tests__/QuerySystemUserResolver.permissions.test.ts +132 -0
  462. package/src/__tests__/RESTEndpointHandler.test.ts +25 -0
  463. package/src/__tests__/RSUPostRestartProgress.test.ts +343 -0
  464. package/src/__tests__/RSUProgressBridge.test.ts +188 -5
  465. package/src/__tests__/RSUStartupSweepWiring.test.ts +30 -0
  466. package/src/__tests__/ReactivateRefreshMessage.test.ts +10 -3
  467. package/src/__tests__/RealtimeClientSessionResolver.configTamper.test.ts +23 -3
  468. package/src/__tests__/RealtimeClientSessionResolver.test.ts +346 -46
  469. package/src/__tests__/{RefreshAdoptsNewColumns.test.ts → RefreshReportsNewColumns.test.ts} +25 -16
  470. package/src/__tests__/RemoteBrowserActionRecovery.test.ts +287 -0
  471. package/src/__tests__/RemoteBrowserActionResolver.goalRunLink.test.ts +3 -0
  472. package/src/__tests__/RemoteBrowserActionResolver.ownership.test.ts +100 -0
  473. package/src/__tests__/RemoteBrowserAudioStream.test.ts +3 -0
  474. package/src/__tests__/RemoteBrowserSnapshot.test.ts +3 -0
  475. package/src/__tests__/ResolverBase.clientOldValues.test.ts +344 -14
  476. package/src/__tests__/ResolverBase.deleteReturnProjection.test.ts +52 -0
  477. package/src/__tests__/ResolverBase.encryptedApiZeroReencrypt.test.ts +307 -0
  478. package/src/__tests__/ResolverBase.entityMessageRelay.test.ts +227 -0
  479. package/src/__tests__/ResolverBase.includeBinaryFields.test.ts +83 -1
  480. package/src/__tests__/ResolverBase.publishStatusUpdate.test.ts +68 -0
  481. package/src/__tests__/ResolverBase.userViewCacheAndDedup.test.ts +130 -0
  482. package/src/__tests__/RunAIAgentResolver.branch.test.ts +72 -13
  483. package/src/__tests__/RunAIAgentResolver.consultPredecessor.test.ts +289 -0
  484. package/src/__tests__/RunAIAgentResolver.conversationWriter.test.ts +210 -0
  485. package/src/__tests__/RunAIAgentResolver.fireAndForgetCompletion.test.ts +209 -0
  486. package/src/__tests__/RunAIAgentResolver.handoff.test.ts +649 -0
  487. package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +19 -27
  488. package/src/__tests__/RunAIAgentResolver.runDataGuard.test.ts +10 -2
  489. package/src/__tests__/RunAIAgentResolver.serverReply.test.ts +358 -0
  490. package/src/__tests__/RunAIAgentResolver.stoppedPredecessor.test.ts +91 -13
  491. package/src/__tests__/RunTemplateResolver.test.ts +35 -2
  492. package/src/__tests__/RunViewResolver.cacheCheckForwarding.test.ts +11 -0
  493. package/src/__tests__/SchemaEvolutionCustomColumns.test.ts +119 -0
  494. package/src/__tests__/SchemaRefreshLaunch.test.ts +24 -2
  495. package/src/__tests__/ScopedFieldsForBuild.test.ts +192 -0
  496. package/src/__tests__/SessionJanitor.test.ts +195 -4
  497. package/src/__tests__/SessionManager.test.ts +164 -0
  498. package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +2 -2
  499. package/src/__tests__/TaskGraphAgentRunner.test.ts +272 -16
  500. package/src/__tests__/TaskGraphContinuationDeliverer.test.ts +114 -3
  501. package/src/__tests__/TransactionGroupResolver.fieldSecurity.test.ts +199 -0
  502. package/src/__tests__/UserViewResolver.updateWhereClause.test.ts +15 -0
  503. package/src/__tests__/ViewOperationsHandler.test.ts +9 -7
  504. package/src/__tests__/WidgetSessionService.test.ts +583 -4
  505. package/src/__tests__/WorkQueueHostService.test.ts +25 -7
  506. package/src/__tests__/apiRootBoot.test.ts +1 -1
  507. package/src/__tests__/backgroundSweepConfigUnits.test.ts +153 -0
  508. package/src/__tests__/buildMagicLinkSessionUser.test.ts +96 -0
  509. package/src/__tests__/cacheBroadcastProviders.test.ts +403 -0
  510. package/src/__tests__/cacheInvalidationRecordData.test.ts +45 -0
  511. package/src/__tests__/contextRequestOrigin.test.ts +105 -0
  512. package/src/__tests__/conversationBrowsers.test.ts +62 -0
  513. package/src/__tests__/fieldSecurityFixtures.ts +91 -0
  514. package/src/__tests__/globalCors.test.ts +3 -3
  515. package/src/__tests__/localDevAuth.test.ts +196 -0
  516. package/src/__tests__/magicLinkExtensionCapabilities.test.ts +413 -0
  517. package/src/__tests__/orphanedConversationDetailReconciler.start.test.ts +82 -0
  518. package/src/__tests__/orphanedConversationDetailReconciler.test.ts +10 -0
  519. package/src/__tests__/ownedRealtimeSession.test.ts +14 -0
  520. package/src/__tests__/pushStatusResolver.filter.test.ts +57 -1
  521. package/src/__tests__/realtimeRecordingHandler.test.ts +24 -2
  522. package/src/__tests__/realtimeSessions/RealtimeSessionEventsResolver.test.ts +29 -1
  523. package/src/__tests__/realtimeSessions/ownedRealtimeSession.anonymous.test.ts +31 -0
  524. package/src/__tests__/realtimeSessions/ownedRealtimeSession.namedScoped.test.ts +37 -0
  525. package/src/__tests__/realtimeSessions/sessionAccess.test.ts +224 -7
  526. package/src/__tests__/realtimeSessions/sessionConfigStamps.test.ts +1 -1
  527. package/src/__tests__/realtimeSessions/verificationWorkflow.test.ts +25 -4
  528. package/src/__tests__/requestOrigin.test.ts +104 -0
  529. package/src/__tests__/resolverBase.cloudEventChangedFields.test.ts +157 -0
  530. package/src/__tests__/resolverBase.fieldSecurityTransport.test.ts +222 -0
  531. package/src/__tests__/resolverBase.fls.test.ts +146 -97
  532. package/src/__tests__/restTransport.fieldSecurity.test.ts +148 -0
  533. package/src/__tests__/runParamsTimeZone.test.ts +179 -0
  534. package/src/__tests__/runStreams.fieldSecurity.test.ts +100 -0
  535. package/src/__tests__/runViewProperties.test.ts +114 -0
  536. package/src/__tests__/scalabilityBenchmarks.test.ts +765 -0
  537. package/src/__tests__/scalabilityDatabaseIntegration.test.ts +207 -0
  538. package/src/__tests__/securityHardeningPr6034.test.ts +120 -0
  539. package/src/__tests__/sessionDeadline.test.ts +51 -0
  540. package/src/__tests__/sessionUserClone.test.ts +5 -1
  541. package/src/__tests__/sharedCache.test.ts +11 -0
  542. package/src/__tests__/startupSweepOrder.test.ts +40 -0
  543. package/src/__tests__/unifiedAuth.test.ts +101 -1
  544. package/src/__tests__/widgetGuestElevation.test.ts +2 -0
  545. package/src/agentSessions/BridgeHostInstance.ts +20 -5
  546. package/src/agentSessions/HandoffOfferExpiry.ts +18 -0
  547. package/src/agentSessions/ReturningVisitorRecap.ts +1 -0
  548. package/src/agentSessions/SessionJanitor.ts +95 -45
  549. package/src/agentSessions/SessionManager.ts +73 -20
  550. package/src/agentSessions/conversationBrowsers.ts +143 -0
  551. package/src/agentSessions/sessionDeadline.ts +81 -0
  552. package/src/auth/APIKeyScopeAuth.ts +5 -4
  553. package/src/auth/AuthProviderCatalogRouter.ts +3 -0
  554. package/src/auth/__tests__/loginEmailResolution.test.ts +67 -0
  555. package/src/auth/index.ts +56 -12
  556. package/src/auth/initializeProviders.ts +43 -1
  557. package/src/auth/localDevAuth.ts +528 -0
  558. package/src/auth/loginEmailResolution.ts +67 -0
  559. package/src/auth/magicLink/MagicLinkKeys.ts +147 -15
  560. package/src/auth/magicLink/MagicLinkRouter.ts +33 -6
  561. package/src/auth/magicLink/MagicLinkService.ts +250 -10
  562. package/src/auth/magicLink/index.ts +1 -1
  563. package/src/auth/magicLink/redeemLanding.ts +7 -4
  564. package/src/auth/magicLink/types.ts +38 -3
  565. package/src/auth/newUsers.ts +1 -0
  566. package/src/auth/sessionUserClone.ts +3 -2
  567. package/src/backgroundSweepConfigUnits.ts +213 -0
  568. package/src/broadcast/BaseCacheInvalidationBroadcastProvider.ts +674 -0
  569. package/src/broadcast/index.ts +5 -0
  570. package/src/broadcast/providers/AWSIoTCoreBroadcastProvider.ts +180 -0
  571. package/src/broadcast/providers/AzureWebPubSubBroadcastProvider.ts +182 -0
  572. package/src/broadcast/providers/GCPPubSubBroadcastProvider.ts +169 -0
  573. package/src/broadcast/providers/SequencedRoleBucketBroadcastProvider.ts +57 -0
  574. package/src/cache.ts +2 -1
  575. package/src/config.ts +138 -0
  576. package/src/context.ts +71 -14
  577. package/src/coreRoutes.ts +29 -7
  578. package/src/generated/graphql-schemas/__mj.ts +4145 -1193
  579. package/src/generic/CacheInvalidationResolver.ts +135 -78
  580. package/src/generic/OrphanedConversationDetailReconciler.ts +36 -3
  581. package/src/generic/PubSubManager.ts +54 -1
  582. package/src/generic/PushStatusResolver.ts +108 -11
  583. package/src/generic/ResolverBase.ts +635 -234
  584. package/src/generic/RunViewResolver.ts +176 -64
  585. package/src/generic/actionOutputFieldSecurity.ts +75 -0
  586. package/src/generic/fieldSecurityTransport.ts +62 -0
  587. package/src/index.ts +310 -85
  588. package/src/integration/ConnectionPause.ts +222 -0
  589. package/src/integration/CustomColumnPromoter.ts +129 -54
  590. package/src/integration/EntityMapLifecycle.ts +229 -6
  591. package/src/integration/InactiveRowWarnings.ts +50 -0
  592. package/src/integration/ObjectNamesByID.ts +97 -0
  593. package/src/integration/PostRestartIntrospect.ts +49 -0
  594. package/src/integration/RSUPostRestartProgress.ts +359 -0
  595. package/src/integration/RSUProgressBridge.ts +85 -2
  596. package/src/integration/RunArtifactAuthorization.ts +65 -0
  597. package/src/integration/SchemaRefreshLaunch.ts +12 -2
  598. package/src/integration/ScopedFieldsForBuild.ts +113 -0
  599. package/src/integration/TargetConfigGuard.ts +71 -0
  600. package/src/logging/variablesLoggingMiddleware.ts +59 -3
  601. package/src/realtimeSessions/README.md +7 -3
  602. package/src/realtimeSessions/SessionVerificationPortsBase.ts +1 -0
  603. package/src/realtimeSessions/ownedRealtimeSession.ts +16 -6
  604. package/src/realtimeSessions/sessionAccess.ts +211 -25
  605. package/src/realtimeSessions/sessionConfigStamps.ts +1 -1
  606. package/src/realtimeSessions/subscriptionAuthorization.ts +4 -2
  607. package/src/realtimeSessions/verificationWorkflow.ts +3 -1
  608. package/src/realtimeWidget/WidgetRouter.ts +22 -4
  609. package/src/realtimeWidget/WidgetSessionService.ts +254 -17
  610. package/src/realtimeWidget/host-identity.ts +1 -1
  611. package/src/realtimeWidget/visitorIdentity.ts +1 -0
  612. package/src/realtimeWidget/widgetCore.ts +34 -9
  613. package/src/realtimeWidget/widgetGuestElevation.ts +3 -1
  614. package/src/requestOrigin.ts +64 -0
  615. package/src/resolvers/ActionResolver.ts +78 -15
  616. package/src/resolvers/AdhocQueryResolver.ts +1 -0
  617. package/src/resolvers/AgentRunStatusPublisher.ts +36 -8
  618. package/src/resolvers/AgentSessionResolver.ts +50 -18
  619. package/src/resolvers/AutotagPipelineResolver.ts +2 -1
  620. package/src/resolvers/ConversationRunEventsResolver.ts +2 -0
  621. package/src/resolvers/EntityCommunicationsResolver.ts +56 -15
  622. package/src/resolvers/ExecuteRemoteOperationResolver.ts +41 -9
  623. package/src/resolvers/FetchEntityVectorsResolver.ts +132 -21
  624. package/src/resolvers/FileCategoryResolver.ts +1 -0
  625. package/src/resolvers/FileResolver.ts +14 -4
  626. package/src/resolvers/GetDataResolver.ts +35 -13
  627. package/src/resolvers/IntegrationDiscoveryResolver.ts +1570 -209
  628. package/src/resolvers/MCPResolver.ts +15 -2
  629. package/src/resolvers/MergeRecordsResolver.ts +23 -2
  630. package/src/resolvers/PipelineProgressResolver.ts +12 -3
  631. package/src/resolvers/QueryResolver.ts +20 -6
  632. package/src/resolvers/QuerySystemUserResolver.ts +207 -99
  633. package/src/resolvers/RSUResolver.ts +12 -0
  634. package/src/resolvers/RealtimeBridgeResolver.ts +1 -0
  635. package/src/resolvers/RealtimeClientSessionResolver.ts +144 -40
  636. package/src/resolvers/RemoteBrowserActionResolver.ts +252 -37
  637. package/src/resolvers/RunAIAgentResolver.ts +518 -163
  638. package/src/resolvers/RunAIPromptResolver.ts +18 -18
  639. package/src/resolvers/RunTemplateResolver.ts +9 -5
  640. package/src/resolvers/RunTestResolver.ts +3 -1
  641. package/src/resolvers/SyncDataResolver.ts +4 -0
  642. package/src/resolvers/TestQuerySQLResolver.ts +1 -0
  643. package/src/resolvers/TransactionGroupResolver.ts +66 -15
  644. package/src/resolvers/UserViewResolver.ts +2 -1
  645. package/src/resolvers/__tests__/GetDataResolver.tokens.test.ts +52 -0
  646. package/src/resolvers/__tests__/MergeRecordsResolver.test.ts +43 -0
  647. package/src/resolvers/meetingRecordingRegistration.ts +2 -0
  648. package/src/rest/EntityCRUDHandler.ts +22 -5
  649. package/src/rest/MediaAccessKeys.ts +6 -2
  650. package/src/rest/OAuthCallbackHandler.ts +13 -5
  651. package/src/rest/RESTEndpointHandler.ts +8 -5
  652. package/src/rest/RealtimeRecordingHandler.ts +9 -10
  653. package/src/rest/ViewOperationsHandler.ts +26 -7
  654. package/src/rest/setupRESTEndpoints.ts +11 -2
  655. package/src/serverRoleConfigUnits.ts +48 -0
  656. package/src/services/DurableEntityActionTaskSubmitter.ts +10 -2
  657. package/src/services/TaskGraphActionRunner.ts +2 -23
  658. package/src/services/TaskGraphAgentRunner.ts +43 -0
  659. package/src/services/TaskGraphContinuationDeliverer.ts +53 -20
  660. package/src/services/WorkQueueHostService.ts +26 -4
  661. package/src/services/workQueueConfig.ts +5 -5
  662. package/src/sharedCache.ts +1 -1
  663. package/src/types.ts +41 -1
  664. package/src/services/TaskOrchestration-Integration.md +0 -188
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  AggregateExpression,
3
+ AssertClientInputWritableForUser,
3
4
  BaseEntity,
4
5
  BaseEntityEvent,
5
6
  CompositeKey,
@@ -14,8 +15,10 @@ import {
14
15
  KeyValuePair,
15
16
  LogDebug,
16
17
  LogError,
18
+ LogErrorEx,
17
19
  LogStatus,
18
20
  Metadata,
21
+ ProjectEntityValuesForUser,
19
22
  ReadableFieldsTransportKey,
20
23
  RecordChangeFieldSecurityProjector,
21
24
  RecordChangePayloadFields,
@@ -23,8 +26,17 @@ import {
23
26
  RunViewParams,
24
27
  RunViewResult,
25
28
  UserInfo,
29
+ ViewPropertyValues,
26
30
  } from '@memberjunction/core';
27
- import { MJAuditLogEntity, MJErrorLogEntity, MJUserViewEntityExtended } from '@memberjunction/core-entities';
31
+ import { MJAuditLogEntity, MJErrorLogEntity, MJUserViewEntityExtended, UserViewEngine } from '@memberjunction/core-entities';
32
+
33
+ /**
34
+ * Extended RunViewResult carrying a non-enumerable reference to the resolved view entity
35
+ * so downstream resolvers (e.g. RunViewResolver) do not re-query `MJ: User Views` a third time.
36
+ */
37
+ export type RunViewResultWithResolvedView<T = unknown> = RunViewResult<T> & {
38
+ _resolvedViewEntity?: MJUserViewEntityExtended;
39
+ };
28
40
  import { SQLServerDataProvider } from '@memberjunction/sqlserver-dataprovider';
29
41
  import { ClientClauseScreen, UserCache, type EntityBaseViewAllowList } from '@memberjunction/generic-database-provider';
30
42
  import { PubSubEngine, AuthorizationError } from 'type-graphql';
@@ -37,11 +49,11 @@ import { httpTransport, CloudEvent, emitterFor } from 'cloudevents';
37
49
  import { RunViewGenericParams, UserPayload } from '../types.js';
38
50
  import { RunDynamicViewInput, RunViewByIDInput, RunViewByNameInput } from './RunViewResolver.js';
39
51
  import { DeleteOptionsInput } from './DeleteOptionsInput.js';
40
- import { MJEvent, MJEventType, MJGlobal, ENCRYPTED_SENTINEL, ReplaceByteArraysWithBase64, EscapeSQLString, IsValueEncrypted, IsOnlyTimezoneShift } from '@memberjunction/global';
41
- import { SQLParser, IsBalancedSQLFragment, LexSQL } from '@memberjunction/sql-parser';
52
+ import { MJEvent, MJEventType, MJGlobal, UUIDsEqual, ENCRYPTED_SENTINEL, ReplaceByteArraysWithBase64, EscapeSQLString, IsValueEncrypted, IsOnlyTimezoneShift, Canonicalize } from '@memberjunction/global';
53
+ import { SQLParser, IsBalancedSQLFragment, LexSQL, StrictFragmentValidator, type StrictStatementReads } from '@memberjunction/sql-parser';
42
54
  import type { SQLParserDialect } from '@memberjunction/sql-dialect';
43
55
  import { EncryptionEngine, IsEncryptionKeyUnavailable, ReportEncryptionKeyUnavailable } from '@memberjunction/encryption';
44
- import { PUSH_STATUS_UPDATES_TOPIC, PublishStatusUpdate } from './PushStatusResolver.js';
56
+ import { PUSH_STATUS_UPDATES_TOPIC, PublishStatusUpdate, StatusUpdateOwnerID } from './PushStatusResolver.js';
45
57
  import { CACHE_INVALIDATION_TOPIC, MayBroadcastRecordData } from './CacheInvalidationResolver.js';
46
58
  import { PubSubManager } from './PubSubManager.js';
47
59
  import { FieldMapper } from '@memberjunction/graphql-dataprovider';
@@ -51,18 +63,43 @@ import { Subscription } from 'rxjs';
51
63
  * Entities whose update is always hydrated from the stored row, never from the client's `OldValues___`, though they
52
64
  * do not track record changes: their server-side write rules compare against the prior state, which a client could
53
65
  * otherwise supply itself (lower-case names). See `ResolverBase.MustLoadTruthFromDatabase`.
66
+ *
67
+ * `mj: conversations` and `mj: ai agent sessions` carry the anonymous realtime-session ownership key
68
+ * (`ExternalID` / `ConversationID`), which their server entities refuse an anonymous visitor to change — a rule
69
+ * that decides on `Dirty`, so the baseline must be the stored row, not a client-supplied one (#4931).
54
70
  */
55
71
  const ENTITIES_THAT_ALWAYS_LOAD_STORED_ROW: ReadonlySet<string> = new Set([
56
72
  'mj: record changes',
57
73
  'mj: conversation details',
58
74
  'mj: data context items',
75
+ 'mj: conversations',
76
+ 'mj: ai agent sessions',
77
+ 'mj: ai agent runs',
59
78
  ]);
60
79
 
80
+ /** One field's values in a concurrency-conflict diagnostic. */
81
+ interface ConflictDiagnosticEntry<TValue> {
82
+ FieldName: string;
83
+ ClientOldValue?: TValue;
84
+ ClientNewValue?: TValue;
85
+ DBValue?: TValue;
86
+ }
87
+
88
+ interface ActiveEntitySessionListener {
89
+ pubSub: PubSubEngine;
90
+ userPayload: UserPayload;
91
+ lastSeenAt: number;
92
+ }
93
+
94
+ const ACTIVE_ENTITY_SESSION_TTL_MS = 24 * 60 * 60 * 1000;
95
+ const ACTIVE_ENTITY_SESSION_MAX_PER_ENTITY = 2048;
96
+
61
97
  export class ResolverBase {
62
98
  private static _emit = process.env.CLOUDEVENTS_HTTP_TRANSPORT ? emitterFor(httpTransport(process.env.CLOUDEVENTS_HTTP_TRANSPORT)) : null;
63
99
  private static _cloudeventsHeaders = process.env.CLOUDEVENTS_HTTP_HEADERS ? JSON.parse(process.env.CLOUDEVENTS_HTTP_HEADERS) : {};
64
100
 
65
101
  private static _eventSubscriptionKey: string = '___MJServer___ResolverBase___EventSubscriptions';
102
+ private static _activeEntitySessionsKey: string = '___MJServer___ResolverBase___ActiveEntitySessions';
66
103
  private get eventSubscriptions(): Map<string, Subscription> {
67
104
  // here we use the global object store instead of a static member becuase in some cases based on import code paths/bundling/etc, the static member
68
105
  // could actually be duplicated and we'd end up with multiple instances of the same map, which would be bad.
@@ -71,7 +108,15 @@ export class ResolverBase {
71
108
  LogDebug(`>>>>> MJServer.ResolverBase.EventSubscriptions: Creating new Map - this should only happen once per server instance <<<<<<`);
72
109
  g[ResolverBase._eventSubscriptionKey] = new Map<string, Subscription>();
73
110
  }
74
- return g[ResolverBase._eventSubscriptionKey];
111
+ return g[ResolverBase._eventSubscriptionKey] as Map<string, Subscription>;
112
+ }
113
+
114
+ private get activeEntitySessions(): Map<string, Map<string, ActiveEntitySessionListener>> {
115
+ const g = MJGlobal.Instance.GetGlobalObjectStore();
116
+ if (!g[ResolverBase._activeEntitySessionsKey]) {
117
+ g[ResolverBase._activeEntitySessionsKey] = new Map<string, Map<string, ActiveEntitySessionListener>>();
118
+ }
119
+ return g[ResolverBase._activeEntitySessionsKey] as Map<string, Map<string, ActiveEntitySessionListener>>;
75
120
  }
76
121
 
77
122
  /**
@@ -120,7 +165,7 @@ export class ResolverBase {
120
165
  if (!entityInfo) throw new Error(`Entity ${entityName} not found in metadata`);
121
166
 
122
167
  const denied = deniedReadFields ?? (
123
- entityInfo.EnableFieldLevelSecurity && contextUser
168
+ entityInfo.FieldLevelSecurityApplies && contextUser
124
169
  ? entityInfo.GetDeniedReadFields(contextUser)
125
170
  : null
126
171
  );
@@ -369,6 +414,7 @@ export class ResolverBase {
369
414
  if (!loaded) {
370
415
  return null;
371
416
  }
417
+ // fls-guard: MapFieldNamesToCodeNames drops the fields the caller may not read
372
418
  const mapped = await this.MapFieldNamesToCodeNames(entityName, entityObject.GetAll(), contextUser);
373
419
  return mapped as T;
374
420
  }
@@ -418,30 +464,29 @@ export class ResolverBase {
418
464
  if (field.AllowDecryptInAPI) continue;
419
465
 
420
466
  // AllowDecryptInAPI is false - we need to filter the value
467
+ if (!field.SendEncryptedValue) {
468
+ // SendEncryptedValue is false - replace with sentinel immediately without touching EncryptionEngine
469
+ dataObject[fieldName] = ENCRYPTED_SENTINEL;
470
+ continue;
471
+ }
472
+
421
473
  const engine = EncryptionEngine.Instance;
422
474
  await engine.Config(false, contextUser);
423
475
  const keyMarker = field.EncryptionKeyID ? engine.GetKeyByID(field.EncryptionKeyID)?.Marker : undefined;
424
- if (field.SendEncryptedValue) {
425
- // Re-encrypt the value before sending
426
- // Only re-encrypt if it's not already encrypted (data provider decrypted it)
427
- if (typeof value === 'string' && !IsValueEncrypted(value, keyMarker)) {
428
- try {
429
- const encryptedValue = await engine.Encrypt(
430
- value,
431
- field.EncryptionKeyID as string,
432
- contextUser
433
- );
434
- dataObject[fieldName] = encryptedValue;
435
- } catch (err) {
436
- // If re-encryption fails, use sentinel for safety
437
- LogError(`Failed to re-encrypt field ${fieldName} for API response: ${err}`);
438
- dataObject[fieldName] = ENCRYPTED_SENTINEL;
439
- }
476
+ // Only encrypt if the value is not already encrypted (PreserveRestrictedApiCiphertext keeps DB/cache ciphertext intact)
477
+ if (typeof value === 'string' && !IsValueEncrypted(value, keyMarker)) {
478
+ try {
479
+ const encryptedValue = await engine.Encrypt(
480
+ value,
481
+ field.EncryptionKeyID as string,
482
+ contextUser
483
+ );
484
+ dataObject[fieldName] = encryptedValue;
485
+ } catch (err) {
486
+ // If re-encryption fails, use sentinel for safety
487
+ LogError(`Failed to re-encrypt field ${fieldName} for API response: ${err}`);
488
+ dataObject[fieldName] = ENCRYPTED_SENTINEL;
440
489
  }
441
- // If already encrypted (shouldn't happen normally), keep as-is
442
- } else {
443
- // SendEncryptedValue is false - replace with sentinel
444
- dataObject[fieldName] = ENCRYPTED_SENTINEL;
445
490
  }
446
491
  }
447
492
 
@@ -559,6 +604,53 @@ export class ResolverBase {
559
604
  }
560
605
  }
561
606
 
607
+ /**
608
+ * Resolves a User View by name, consulting the in-memory {@link UserViewEngine} cache first
609
+ * and falling back to a database `RunView` only on a cache miss.
610
+ */
611
+ protected async resolveUserViewByName(
612
+ viewName: string,
613
+ provider: DatabaseProviderBase,
614
+ contextUser?: UserInfo,
615
+ ): Promise<MJUserViewEntityExtended | null> {
616
+ if (UserViewEngine.Instance.Loaded) {
617
+ const cached = UserViewEngine.Instance.GetViewByName(viewName);
618
+ if (cached) {
619
+ return cached;
620
+ }
621
+ }
622
+ const rv = provider as unknown as IRunViewProvider;
623
+ const result = await rv.RunView<MJUserViewEntityExtended>({
624
+ EntityName: 'MJ: User Views',
625
+ // SECURITY: the view name is client-supplied and lands inside a SQL literal.
626
+ ExtraFilter: "Name='" + EscapeSQLString(viewName) + "'",
627
+ }, contextUser);
628
+ if (result && result.Success && result.Results.length > 0) {
629
+ return result.Results[0];
630
+ }
631
+ return null;
632
+ }
633
+
634
+ /**
635
+ * Resolves a User View by ID, consulting the in-memory {@link UserViewEngine} cache first
636
+ * and falling back to an entity `Load()` only on a cache miss.
637
+ */
638
+ protected async resolveUserViewByID(
639
+ viewID: string,
640
+ provider: DatabaseProviderBase,
641
+ contextUser?: UserInfo,
642
+ ): Promise<MJUserViewEntityExtended | null> {
643
+ if (UserViewEngine.Instance.Loaded) {
644
+ const cached = UserViewEngine.Instance.GetViewById(viewID);
645
+ if (cached) {
646
+ return cached;
647
+ }
648
+ }
649
+ const viewInfo = await provider.GetEntityObject<MJUserViewEntityExtended>('MJ: User Views', contextUser);
650
+ await viewInfo.Load(viewID);
651
+ return viewInfo;
652
+ }
653
+
562
654
  async RunViewByNameGeneric(viewInput: RunViewByNameInput, provider: DatabaseProviderBase, userPayload: UserPayload, pubSub: PubSubEngine) {
563
655
  try {
564
656
  // Log aggregate input for debugging
@@ -566,14 +658,8 @@ export class ResolverBase {
566
658
  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 })))}`);
567
659
  }
568
660
 
569
- const rv = provider as any as IRunViewProvider;
570
- const result = await rv.RunView<MJUserViewEntityExtended>({
571
- EntityName: 'MJ: User Views',
572
- // SECURITY: the view name is client-supplied and lands inside a SQL literal.
573
- ExtraFilter: "Name='" + EscapeSQLString(viewInput.ViewName) + "'",
574
- }, userPayload.userRecord);
575
- if (result && result.Success && result.Results.length > 0) {
576
- const viewInfo = result.Results[0];
661
+ const viewInfo = await this.resolveUserViewByName(viewInput.ViewName, provider, userPayload.userRecord);
662
+ if (viewInfo) {
577
663
  return this.RunViewGenericInternal(
578
664
  provider,
579
665
  viewInfo,
@@ -598,7 +684,10 @@ export class ResolverBase {
598
684
  : undefined,
599
685
  viewInput.BypassCache,
600
686
  viewInput.DataSource,
601
- viewInput.IncludeBinaryFields
687
+ viewInput.IncludeBinaryFields,
688
+ viewInput.AlternateBaseView,
689
+ viewInput.Properties,
690
+ viewInput.SkipTotalRowCount
602
691
  );
603
692
  }
604
693
  else {
@@ -619,8 +708,10 @@ export class ResolverBase {
619
708
  }
620
709
 
621
710
  const contextUser = this.GetUserFromPayload(userPayload);
622
- const viewInfo = await provider.GetEntityObject<MJUserViewEntityExtended>('MJ: User Views', contextUser);
623
- await viewInfo.Load(viewInput.ViewID);
711
+ const viewInfo = await this.resolveUserViewByID(viewInput.ViewID, provider, contextUser);
712
+ if (!viewInfo) {
713
+ return null;
714
+ }
624
715
  return this.RunViewGenericInternal(
625
716
  provider,
626
717
  viewInfo,
@@ -643,7 +734,10 @@ export class ResolverBase {
643
734
  undefined,
644
735
  viewInput.BypassCache,
645
736
  viewInput.DataSource,
646
- viewInput.IncludeBinaryFields
737
+ viewInput.IncludeBinaryFields,
738
+ viewInput.AlternateBaseView,
739
+ viewInput.Properties,
740
+ viewInput.SkipTotalRowCount
647
741
  );
648
742
  } catch (err) {
649
743
  console.log(err);
@@ -691,7 +785,10 @@ export class ResolverBase {
691
785
  undefined,
692
786
  viewInput.BypassCache,
693
787
  viewInput.DataSource,
694
- viewInput.IncludeBinaryFields
788
+ viewInput.IncludeBinaryFields,
789
+ viewInput.AlternateBaseView,
790
+ undefined,
791
+ viewInput.SkipTotalRowCount
695
792
  );
696
793
  } catch (err) {
697
794
  console.log(err);
@@ -712,15 +809,14 @@ export class ResolverBase {
712
809
  let viewInfo: MJUserViewEntityExtended | null = null;
713
810
 
714
811
  if (viewInput.ViewName) {
715
- viewInfo = this.safeFirstArrayElement(await this.findBy(provider, 'MJ: User Views', { Name: viewInput.ViewName }, userPayload.userRecord));
812
+ viewInfo = await this.resolveUserViewByName(viewInput.ViewName, provider, userPayload.userRecord);
716
813
  // Populate EntityName on the input so callers (e.g. RunViews resolver) can
717
814
  // look up the entity without re-querying the view
718
815
  if (viewInfo && !viewInput.EntityName) {
719
816
  viewInput.EntityName = viewInfo.Entity;
720
817
  }
721
818
  } else if (viewInput.ViewID) {
722
- viewInfo = await provider.GetEntityObject<MJUserViewEntityExtended>('MJ: User Views', contextUser);
723
- await viewInfo.Load(viewInput.ViewID);
819
+ viewInfo = await this.resolveUserViewByID(viewInput.ViewID, provider, contextUser);
724
820
  // Populate EntityName on the input so callers can look up the entity
725
821
  if (viewInfo && !viewInput.EntityName) {
726
822
  viewInput.EntityName = viewInfo.Entity;
@@ -767,6 +863,9 @@ export class ResolverBase {
767
863
  bypassCache: viewInput.BypassCache,
768
864
  dataSource: viewInput.DataSource,
769
865
  includeBinaryFields: viewInput.IncludeBinaryFields,
866
+ skipTotalRowCount: viewInput.SkipTotalRowCount,
867
+ alternateBaseView: viewInput.AlternateBaseView,
868
+ properties: viewInput.Properties,
770
869
  });
771
870
  } catch (err) {
772
871
  LogError(err);
@@ -778,6 +877,31 @@ export class ResolverBase {
778
877
  return results;
779
878
  }
780
879
 
880
+ /**
881
+ * What an entity event may carry to a CloudEvents sink or a log line.
882
+ *
883
+ * The whole row for an entity without field security. For one where it applies (its own flag
884
+ * or an IS-A ancestor's), and for `MJ: Record Changes` (whose payload columns hold other
885
+ * entities' values), only the primary key and the names of the fields that changed: the sink
886
+ * is outside MemberJunction with no reader to project the row for, and these events fire for
887
+ * server-internal saves too, whose objects hold every column.
888
+ */
889
+ protected EntityEventPayload(baseEntity: BaseEntity, changedFieldNames?: string[]): Record<string, unknown> {
890
+ const entityInfo = baseEntity.EntityInfo;
891
+ if (!entityInfo?.FieldLevelSecurityApplies && !RecordChangeFieldSecurityProjector.IsRecordChangesEntity(entityInfo)) {
892
+ // fls-guard: entity without field security and not the audit trail; whole row is the documented payload
893
+ return baseEntity.GetAll();
894
+ }
895
+ return {
896
+ // Read through Get() rather than PrimaryKey, which builds every field object of a raw-mode record.
897
+ PrimaryKey: (entityInfo.PrimaryKeys ?? []).map((pk) => ({ FieldName: pk.Name, Value: baseEntity.Get(pk.Name) })),
898
+ // A completed save has already rebased the object, so nothing on it is dirty: the event
899
+ // carries the names the save wrote. Other events (save_started) still read them off the object.
900
+ // GetDirtyFieldNames answers without building field objects for a record still in raw mode.
901
+ ChangedFields: changedFieldNames ?? baseEntity.GetDirtyFieldNames(),
902
+ };
903
+ }
904
+
781
905
  private static _priorEmittedData: {Entity: string, PKey: CompositeKey}[] = [];
782
906
  protected async EmitCloudEvent({ component, event, eventCode, args }: MJEvent) {
783
907
  if (ResolverBase._emit && event === MJEventType.ComponentEvent && eventCode === BaseEntity.BaseEventCode) {
@@ -785,7 +909,7 @@ export class ResolverBase {
785
909
  const type = `MemberJunction.${event}${extendedType}`;
786
910
  const source = `${process.env.CLOUDEVENTS_SOURCE ?? 'MemberJunction'}`;
787
911
  const subject = args instanceof BaseEntityEvent ? args.baseEntity.EntityInfo.CodeName : undefined;
788
- const data = args?.baseEntity?.GetAll() ?? {};
912
+ const data = args?.baseEntity ? this.EntityEventPayload(args.baseEntity, args.ChangedFieldNames) : {};
789
913
 
790
914
  const cloudEvent = new CloudEvent({ type, source, subject, data });
791
915
 
@@ -1007,7 +1131,7 @@ export class ResolverBase {
1007
1131
  * Fails closed: no user, or an unresolvable reference, refuses.
1008
1132
  *
1009
1133
  * The references come from a walk of the whole parse tree
1010
- * (`ClientClauseScreen.CollectStatementReads`). The statement is refused when that walk does not
1134
+ * (`StrictFragmentValidator.ValidateReadOnlyStatement`). The statement is refused when that walk does not
1011
1135
  * account for every SELECT in the text, or when a FROM item names no table (a table function or
1012
1136
  * a row list).
1013
1137
  *
@@ -1028,14 +1152,7 @@ export class ResolverBase {
1028
1152
  }
1029
1153
  const dialect = this.dialectForProvider(provider);
1030
1154
  ClientClauseScreen.AssertNoIdentifierAliasNames(sqlText, dialect, label);
1031
- const parser = new SQLParser(sqlText, dialect);
1032
- if (!parser.IsValid || parser.HasWriteStatement || parser.StatementKind !== 'select') {
1033
- throw new Error(`Invalid ${label}: not a safe read-only statement — refusing under uncertainty`);
1034
- }
1035
- if (this.astContainsWriteNode(parser.AST)) {
1036
- throw new Error(`Invalid ${label}: write/DDL nested in a subquery is not permitted`);
1037
- }
1038
- const reads = ClientClauseScreen.CollectStatementReads(parser);
1155
+ const reads = this.strictReadOnlyStatementReads(sqlText, dialect, label);
1039
1156
  ClientClauseScreen.AssertEverySelectAccountedFor(sqlText, dialect, reads, label);
1040
1157
  const allowed = this.entityBaseViewAllowList(provider);
1041
1158
  for (const relation of reads.Relations) {
@@ -1104,9 +1221,19 @@ export class ResolverBase {
1104
1221
  return LexSQL(clause, dialect).some((t) => (t.Kind === 'string' || t.Kind === 'identifier') && t.Text.includes('\\'));
1105
1222
  }
1106
1223
 
1107
- /** Write/DDL node walk shared with the clause screen; see `ClientClauseScreen.AstContainsWriteNode`. */
1108
- private astContainsWriteNode(node: unknown): boolean {
1109
- return ClientClauseScreen.AstContainsWriteNode(node);
1224
+ /**
1225
+ * Parses a whole statement with the strict validator and returns what it reads; throws unless it
1226
+ * is one plain read-only SELECT with no write or DDL at any depth.
1227
+ */
1228
+ private strictReadOnlyStatementReads(sqlText: string, dialect: SQLParserDialect, label: string): StrictStatementReads {
1229
+ const result = StrictFragmentValidator.ValidateReadOnlyStatement(sqlText, dialect, { CollectReads: true });
1230
+ if (result.Refusal === 'NestedWrite') {
1231
+ throw new Error(`Invalid ${label}: write/DDL nested in a subquery is not permitted`);
1232
+ }
1233
+ if (!result.IsValid || !result.Reads) {
1234
+ throw new Error(`Invalid ${label}: not a safe read-only statement — refusing under uncertainty`);
1235
+ }
1236
+ return result.Reads;
1110
1237
  }
1111
1238
 
1112
1239
  private dialectForProvider(provider?: IMetadataProvider): SQLParserDialect {
@@ -1145,7 +1272,10 @@ export class ResolverBase {
1145
1272
  afterKey?: CompositeKey,
1146
1273
  bypassCache?: boolean,
1147
1274
  dataSource?: 'Live' | 'Materialized',
1148
- includeBinaryFields?: boolean
1275
+ includeBinaryFields?: boolean,
1276
+ alternateBaseView?: string,
1277
+ properties?: ViewPropertyValues,
1278
+ skipTotalRowCount?: boolean
1149
1279
  ) {
1150
1280
  try {
1151
1281
  if (!viewInfo || !userPayload) return null;
@@ -1207,6 +1337,7 @@ export class ResolverBase {
1207
1337
  {
1208
1338
  ViewID: viewInfo.ID,
1209
1339
  ViewName: viewInfo.Name,
1340
+ ViewEntity: viewInfo.ID ? viewInfo : undefined,
1210
1341
  EntityName: viewInfo.Entity,
1211
1342
  ExtraFilter: extraFilter,
1212
1343
  OrderBy: orderBy,
@@ -1227,10 +1358,24 @@ export class ResolverBase {
1227
1358
  BypassCache: bypassCache,
1228
1359
  DataSource: dataSource,
1229
1360
  IncludeBinaryFields: includeBinaryFields,
1361
+ SkipTotalRowCount: skipTotalRowCount,
1362
+ AlternateBaseView: alternateBaseView,
1363
+ Properties: properties,
1364
+ TimeZone: userPayload?.TimeZone,
1365
+ PreserveRestrictedApiCiphertext: true,
1230
1366
  },
1231
1367
  user
1232
1368
  );
1233
1369
 
1370
+ if (result && typeof result === 'object') {
1371
+ Object.defineProperty(result, '_resolvedViewEntity', {
1372
+ value: viewInfo,
1373
+ enumerable: false,
1374
+ configurable: true,
1375
+ writable: true,
1376
+ });
1377
+ }
1378
+
1234
1379
  // Log aggregate results for debugging
1235
1380
  if (aggregates?.length) {
1236
1381
  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)}`);
@@ -1261,7 +1406,8 @@ export class ResolverBase {
1261
1406
  await this.ArrayFilterEncryptedFieldsForAPI(
1262
1407
  viewInfo.Entity,
1263
1408
  result.Results as Record<string, unknown>[],
1264
- user
1409
+ user,
1410
+ md
1265
1411
  );
1266
1412
  }
1267
1413
 
@@ -1357,6 +1503,7 @@ export class ResolverBase {
1357
1503
  runViewParams.push({
1358
1504
  ViewID: param.viewInfo.ID,
1359
1505
  ViewName: param.viewInfo.Name,
1506
+ ViewEntity: param.viewInfo.ID ? param.viewInfo : undefined,
1360
1507
  EntityName: param.viewInfo.Entity,
1361
1508
  ExtraFilter: param.extraFilter,
1362
1509
  OrderBy: param.orderBy,
@@ -1377,6 +1524,11 @@ export class ResolverBase {
1377
1524
  BypassCache: param.bypassCache,
1378
1525
  DataSource: param.dataSource,
1379
1526
  IncludeBinaryFields: param.includeBinaryFields,
1527
+ SkipTotalRowCount: param.skipTotalRowCount,
1528
+ AlternateBaseView: param.alternateBaseView,
1529
+ Properties: param.properties,
1530
+ TimeZone: param.userPayload?.TimeZone,
1531
+ PreserveRestrictedApiCiphertext: true,
1380
1532
  });
1381
1533
  }
1382
1534
 
@@ -1398,7 +1550,8 @@ export class ResolverBase {
1398
1550
  await this.ArrayFilterEncryptedFieldsForAPI(
1399
1551
  entityName,
1400
1552
  runViewResult.Results as Record<string, unknown>[],
1401
- contextUser
1553
+ contextUser,
1554
+ md
1402
1555
  );
1403
1556
  }
1404
1557
  }
@@ -1537,8 +1690,9 @@ export class ResolverBase {
1537
1690
  * skip redundant re-fetches (it already handled the event locally).
1538
1691
  *
1539
1692
  * The row itself rides along ONLY for entities opted in via
1540
- * `cacheSettings.recordDataBroadcastEntities` — this event reaches every connected client
1541
- * unfiltered, so the row would otherwise be readable by sessions that could not read the record.
1693
+ * `cacheSettings.recordDataBroadcastEntities`, and never for one under field-level security —
1694
+ * this event reaches every connected client unprojected, so the row would otherwise be readable
1695
+ * by sessions that could not read the record or its restricted fields.
1542
1696
  */
1543
1697
  protected PublishCacheInvalidation(entityObject: BaseEntity, action: 'save' | 'delete', userPayload: UserPayload): void {
1544
1698
  const entityName = entityObject.EntityInfo.Name;
@@ -1550,7 +1704,8 @@ export class ResolverBase {
1550
1704
  timestamp: new Date(),
1551
1705
  originSessionId: userPayload?.sessionId || null,
1552
1706
  recordData:
1553
- action === 'save' && MayBroadcastRecordData(entityName)
1707
+ action === 'save' && MayBroadcastRecordData(entityName, entityObject.EntityInfo)
1708
+ // fls-guard: MayBroadcastRecordData refuses FLS entities and Record Changes
1554
1709
  ? JSON.stringify(entityObject.GetAll())
1555
1710
  : undefined,
1556
1711
  });
@@ -1558,27 +1713,56 @@ export class ResolverBase {
1558
1713
 
1559
1714
  /**
1560
1715
  * Publishes a push-status update to the client on {@link PUSH_STATUS_UPDATES_TOPIC}, stamping the
1561
- * authenticated owner's user ID from `userPayload` so the subscription filter can bind delivery
1562
- * to identity (see B49 / `statusUpdatesFilter`). The ergonomic wrapper every resolver should use
1563
- * instead of calling `pubSub.publish` on the topic directly — it makes omitting identity
1564
- * impossible. Non-resolver publishers (services, the liveness heartbeat) call the shared
1565
- * `publishStatusUpdate()` function directly with an explicit `ownerUserId`.
1716
+ * owner key of `userPayload` (`StatusUpdateOwnerID`: the request's user, or the widget guest an
1717
+ * elevated payload stands in for) so the subscription filter can bind delivery to identity (see B49 /
1718
+ * `StatusUpdatesFilter`). The ergonomic wrapper every resolver should use instead of calling
1719
+ * `pubSub.publish` on the topic directly — it makes omitting identity impossible. Non-resolver
1720
+ * publishers (services, the liveness heartbeat) call the shared `PublishStatusUpdate()` function
1721
+ * directly with an explicit `ownerUserId`.
1566
1722
  */
1567
1723
  protected PublishStatusUpdate(pubSub: PubSubEngine, sessionId: string, message: string | undefined, userPayload: UserPayload): void {
1568
1724
  PublishStatusUpdate(pubSub, {
1569
1725
  sessionId,
1570
- ownerUserId: userPayload?.userRecord?.ID ?? '',
1726
+ ownerUserId: StatusUpdateOwnerID(userPayload),
1571
1727
  message,
1572
1728
  });
1573
1729
  }
1574
1730
 
1575
1731
  protected ListenForEntityMessages(entityObject: BaseEntity, pubSub: PubSubEngine, userPayload: UserPayload) {
1576
- // The unique key is set up for each entity object via it's primary key to ensure that we only have one listener at most for each unique
1577
- // entity in the system. This is important because we don't want to have multiple listeners for the same entity as it could
1578
- // cause issues with multiple messages for the same event.
1732
+ // Maintain a single MJGlobal event listener per entity name so CloudEvents are emitted once
1733
+ // per event, while routing WebSocket status updates dynamically across all active user sessions
1734
+ // that have interacted with this entity type (instead of closing over only the first caller).
1579
1735
  const uniqueKey = entityObject.EntityInfo.Name;
1736
+ const now = Date.now();
1737
+ const isFreshSubscription = !this.eventSubscriptions.has(uniqueKey);
1738
+
1739
+ let sessionsForEntity = isFreshSubscription ? undefined : this.activeEntitySessions.get(uniqueKey);
1740
+ if (!sessionsForEntity) {
1741
+ sessionsForEntity = new Map<string, ActiveEntitySessionListener>();
1742
+ this.activeEntitySessions.set(uniqueKey, sessionsForEntity);
1743
+ } else {
1744
+ // Opportunistically evict expired session entries and enforce a safe per-entity cap
1745
+ for (const [key, entry] of sessionsForEntity) {
1746
+ if (now - entry.lastSeenAt > ACTIVE_ENTITY_SESSION_TTL_MS) {
1747
+ sessionsForEntity.delete(key);
1748
+ }
1749
+ }
1750
+ if (sessionsForEntity.size >= ACTIVE_ENTITY_SESSION_MAX_PER_ENTITY) {
1751
+ const oldestKey = sessionsForEntity.keys().next().value;
1752
+ if (oldestKey !== undefined) {
1753
+ sessionsForEntity.delete(oldestKey);
1754
+ }
1755
+ }
1756
+ }
1757
+
1758
+ if (userPayload) {
1759
+ const sessionKey = userPayload.sessionId
1760
+ ? `${userPayload.userRecord?.ID ?? ''}:${userPayload.sessionId}`
1761
+ : (userPayload.userRecord?.ID ?? '__anonymous__');
1762
+ sessionsForEntity.set(sessionKey, { pubSub, userPayload, lastSeenAt: now });
1763
+ }
1580
1764
 
1581
- if (!this.eventSubscriptions.has(uniqueKey)) {
1765
+ if (isFreshSubscription) {
1582
1766
  // listen for events from the entityObject in case it is a long running task and we can push messages back to the client via pubSub
1583
1767
  LogDebug(`ResolverBase.ListenForEntityMessages: About to call MJGlobal.Instance.GetEventListener() to get the event listener subscription for ${uniqueKey}`);
1584
1768
  const theSub = MJGlobal.Instance.GetEventListener(false).subscribe(async (event: MJEvent) => {
@@ -1591,23 +1775,29 @@ export class ResolverBase {
1591
1775
  return;
1592
1776
  }
1593
1777
 
1594
- const baseEntityValues = baseEntity ? baseEntity.GetAll() : null;
1595
- const eventToLog = { entityName: entityObject.EntityInfo.Name, baseEntity: baseEntityValues, event: event.event, eventCode: event.eventCode };
1778
+ const baseEntityValues = baseEntity ? this.EntityEventPayload(baseEntity, event.args instanceof BaseEntityEvent ? event.args.ChangedFieldNames : undefined) : null;
1779
+ const eventToLog = { entityName: uniqueKey, baseEntity: baseEntityValues, event: event.event, eventCode: event.eventCode };
1596
1780
  LogDebug(`ResolverBase.ListenForEntityMessages: Received Event from within MJGlobal.Instance.GetEventListener() callback. Will call EmitCloudEvent() next\nEvent data:\n${JSON.stringify(eventToLog)}`);
1597
1781
  await this.EmitCloudEvent(event);
1598
- LogDebug(`ResolverBase.ListenForEntityMessages: EmitCloudEvent() completed successfully`);
1782
+ LogDebug(`ResolverBase.ListenForEntityMessages: EmitCloudEvent() completed successfully`);
1599
1783
 
1600
1784
  if (event.args && event.args instanceof BaseEntityEvent) {
1601
- const baseEntityEvent = event.args as BaseEntityEvent;
1602
- // message from our entity object, relay it to the client
1603
- LogDebug('ResolverBase.ListenForEntityMessages: About to publish PUSH_STATUS_UPDATES_TOPIC');
1604
- this.PublishStatusUpdate(pubSub, userPayload.sessionId, JSON.stringify({
1605
- status: 'OK',
1606
- type: 'EntityObjectStatusMessage',
1607
- entityName: baseEntityEvent.baseEntity.EntityInfo.Name,
1608
- primaryKey: baseEntityEvent.baseEntity.PrimaryKey,
1609
- message: event.args.payload,
1610
- }), userPayload);
1785
+ const activeSessions = this.activeEntitySessions.get(uniqueKey);
1786
+ if (activeSessions && activeSessions.size > 0) {
1787
+ const eventNow = Date.now();
1788
+ let serializedMessage: string | undefined;
1789
+ for (const [sKey, session] of activeSessions) {
1790
+ if (eventNow - session.lastSeenAt > ACTIVE_ENTITY_SESSION_TTL_MS) {
1791
+ activeSessions.delete(sKey);
1792
+ continue;
1793
+ }
1794
+ if (this.EntityEventBelongsToSession(event.args, session.userPayload)) {
1795
+ LogDebug('ResolverBase.ListenForEntityMessages: About to publish PUSH_STATUS_UPDATES_TOPIC');
1796
+ serializedMessage ??= this.EntityStatusMessage(event.args);
1797
+ this.PublishStatusUpdate(session.pubSub, session.userPayload.sessionId, serializedMessage, session.userPayload);
1798
+ }
1799
+ }
1800
+ }
1611
1801
  }
1612
1802
  }
1613
1803
  });
@@ -1615,6 +1805,37 @@ export class ResolverBase {
1615
1805
  }
1616
1806
  }
1617
1807
 
1808
+ /**
1809
+ * Whether an entity event may be relayed to the session whose call created the listener. The
1810
+ * listener is shared by every caller in the process for that entity, so an event raised by an
1811
+ * object loaded for another user (or for the system user) is never relayed to this session.
1812
+ */
1813
+ protected EntityEventBelongsToSession(event: BaseEntityEvent, userPayload: UserPayload): boolean {
1814
+ const sessionUserId = userPayload?.userRecord?.ID;
1815
+ const objectUserId = event.baseEntity?.ContextCurrentUser?.ID;
1816
+ return Boolean(sessionUserId && objectUserId && UUIDsEqual(sessionUserId, objectUserId));
1817
+ }
1818
+
1819
+ /**
1820
+ * The status message relayed for an entity event: the entity, key and event type. The event's
1821
+ * payload is included only when it cannot carry record values: never for a delete (its payload
1822
+ * is the whole deleted row), an entity under field-level security, or `MJ: Record Changes`.
1823
+ */
1824
+ protected EntityStatusMessage(event: BaseEntityEvent): string {
1825
+ const entityInfo = event.baseEntity.EntityInfo;
1826
+ const mayCarryPayload = event.type !== 'delete'
1827
+ && !entityInfo.FieldLevelSecurityApplies
1828
+ && !RecordChangeFieldSecurityProjector.IsRecordChangesEntity(entityInfo);
1829
+ return JSON.stringify({
1830
+ status: 'OK',
1831
+ type: 'EntityObjectStatusMessage',
1832
+ entityName: entityInfo.Name,
1833
+ primaryKey: event.baseEntity.PrimaryKey,
1834
+ eventType: event.type,
1835
+ message: mayCarryPayload ? event.payload : undefined,
1836
+ });
1837
+ }
1838
+
1618
1839
  /**
1619
1840
  * IS-A PROMOTION over the wire: an EXISTING parent record gaining a subtype — an Animal that is
1620
1841
  * now also a Dog. It arrives as the CHILD's create mutation carrying the existing parent's
@@ -1691,6 +1912,9 @@ export class ResolverBase {
1691
1912
  // IS-A promotion: bind the new child to its EXISTING parent row BEFORE the field
1692
1913
  // assignments, so values the client sent for parent fields land on the loaded parent
1693
1914
  // (as an update) instead of being wiped by the load.
1915
+ // Field-level security: refuse a key the caller may not read, or that names no field, before
1916
+ // anything is applied (the same gate as the update path).
1917
+ AssertClientInputWritableForUser(entityObject.EntityInfo, fieldsForSet, this.GetUserFromPayload(userPayload));
1694
1918
  await this.attachToExistingParentIfPromotion(entityObject, fieldsForSet);
1695
1919
  entityObject.SetMany(fieldsForSet);
1696
1920
 
@@ -1707,6 +1931,7 @@ export class ResolverBase {
1707
1931
  // Cache invalidation is now handled globally by the MJGlobal listener in index.ts
1708
1932
  const contextUser = this.GetUserFromPayload(userPayload);
1709
1933
  // MapFieldNamesToCodeNames now handles encryption filtering as well
1934
+ // fls-guard: MapFieldNamesToCodeNames drops the fields the caller may not read
1710
1935
  return await this.MapFieldNamesToCodeNames(entityName, entityObject.GetAll(), contextUser);
1711
1936
  }
1712
1937
  // save failed, throw error with message
@@ -1747,12 +1972,11 @@ export class ResolverBase {
1747
1972
  });
1748
1973
 
1749
1974
  // Field-level security: any value the client sends for a field it cannot READ is
1750
- // fabricated by construction (the field was stripped from every payload the client ever
1751
- // received), so strip such values before anything applies them. When any field is
1752
- // denied we also force the truth-load branch below — client OldValues for denied fields
1753
- // are equally fabricated, so hydrating from them (the no-DB-load path) would write
1754
- // fabricated state into the denied columns on save.
1755
- const hasDeniedReadFields = this.StripDeniedReadFieldsFromClientInput(entityInfo, userInfo, input, clientNewValues);
1975
+ // fabricated by construction (the field was absent from every payload the client ever
1976
+ // received), so the input is refused before anything applies it. When any field is
1977
+ // denied we also force the truth-load branch below, so the denied columns keep what the
1978
+ // database holds rather than anything derived from the client's OldValues.
1979
+ const hasDeniedReadFields = this.AssertClientInputFieldSecurity(entityInfo, userInfo, input, clientNewValues);
1756
1980
  // The audit-trail equivalent. Its `true` matters most on THIS entity: 'MJ: Record Changes'
1757
1981
  // has TrackRecordChanges off, so without it a client sending OldValues takes the branch below
1758
1982
  // that hydrates from those values — never loading what the database actually holds.
@@ -1815,6 +2039,7 @@ export class ResolverBase {
1815
2039
  // Cache invalidation is now handled globally by the MJGlobal listener in index.ts
1816
2040
 
1817
2041
  // MapFieldNamesToCodeNames now handles encryption filtering as well
2042
+ // fls-guard: MapFieldNamesToCodeNames drops the fields the caller may not read
1818
2043
  return await this.MapFieldNamesToCodeNames(entityName, entityObject.GetAll(), userInfo);
1819
2044
  } else {
1820
2045
  // CompleteMessage, not Message. A validation refusal puts its reasons in `Errors` and leaves
@@ -1871,7 +2096,7 @@ export class ResolverBase {
1871
2096
  * writable fields in `OldValues___`, so {@link hasCompleteOldValues} returns `false` and forces the
1872
2097
  * database load (`InnerLoad`) so untouched columns retain their stored values.
1873
2098
  *
1874
- * `EnableFieldLevelSecurity` is the security-critical one, and it is NOT redundant with
2099
+ * `FieldLevelSecurityApplies` (own flag or an IS-A ancestor's) is the security-critical one, and it is NOT redundant with
1875
2100
  * `hasDeniedReadFields`. That flag reports READ denials, and the canonical FLS configuration is
1876
2101
  * Read Allow + Update Deny — which leaves it false. Such a caller would be hydrated from its own
1877
2102
  * `OldValues___`, and a value it supplies there for an update-denied field arrives through
@@ -1926,73 +2151,50 @@ export class ResolverBase {
1926
2151
  !this.hasCompleteOldValues(entityInfo, input.OldValues___) ||
1927
2152
  hasDeniedReadFields ||
1928
2153
  hasNarrowedAuditPayload ||
1929
- entityInfo.EnableFieldLevelSecurity ||
2154
+ entityInfo.FieldLevelSecurityApplies ||
1930
2155
  !user ||
1931
2156
  entityInfo.GetEffectiveRowFilterWhereClause(user, EntityPermissionType.Update, '').length > 0
1932
2157
  );
1933
2158
  }
1934
2159
 
1935
2160
  /**
1936
- * Field-level security guard for the update path.
2161
+ * Field-level security gate on the update path's client input.
1937
2162
  *
1938
- * Every denied-read field is stripped — new values AND OldValues. A field the user cannot
1939
- * read was absent from every payload that client ever received, so any value coming back for
1940
- * it is the transport's invention rather than user intent, and applying it would silently
1941
- * overwrite the real column. Stripping is what makes "load a record, edit an unrelated field,
1942
- * save" safe for a restricted user.
2163
+ * For a caller denied at least one field on the entity, refuses (with a `FieldSecurityError`)
2164
+ * any key in the new values or in `OldValues___` that names a field the caller cannot READ, or
2165
+ * that names no field at all, with the same message for both (see
2166
+ * `AssertClientInputWritableForUser`), so the refusal does not reveal which columns exist.
1943
2167
  *
1944
- * There is no split by update permission: Read is required for Update, so a user denied read
1945
- * is denied update too and denied-read ∩ denied-update is just denied-read.
2168
+ * Refused rather than dropped: a field the caller cannot read was absent from every payload the
2169
+ * client received, so a value for it can only be fabricated. A well-behaved client never sends
2170
+ * one (it omits fields it did not load), so ordinary traffic is unaffected.
1946
2171
  *
1947
- * Silent narrowing, not rejection — consistent with the output projection the client already
1948
- * experiences, and with the ambiguous-error rule (naming the field would confirm it exists
1949
- * and is restricted).
2172
+ * There is no split by update permission: Read is required for Update, so a user denied read is
2173
+ * denied update too.
1950
2174
  *
1951
- * Returns true when the user has a non-empty denied-read set on this entity. The caller
1952
- * must then hydrate the entity from the DATABASE (never from client OldValues), so denied
1953
- * fields hold true values that an omitted key leaves untouched.
2175
+ * Returns true when the user has a non-empty denied-read set on this entity. The caller must
2176
+ * then hydrate the entity from the DATABASE (never from client OldValues), so denied fields hold
2177
+ * true values the client input never touches.
1954
2178
  */
1955
- protected StripDeniedReadFieldsFromClientInput(
2179
+ protected AssertClientInputFieldSecurity(
1956
2180
  entityInfo: EntityInfo,
1957
2181
  userInfo: UserInfo,
1958
2182
  input: { OldValues___?: Array<{ Key: string; Value: unknown }> } & Record<string, unknown>,
1959
2183
  clientNewValues: Record<string, unknown>
1960
2184
  ): boolean {
1961
- if (!entityInfo.EnableFieldLevelSecurity || !userInfo) {
2185
+ if (!entityInfo.FieldLevelSecurityApplies || !userInfo) {
1962
2186
  return false;
1963
2187
  }
1964
- const deniedReadNames = entityInfo.GetDeniedReadFields(userInfo);
1965
- if (deniedReadNames.size === 0) {
2188
+ if (entityInfo.GetDeniedReadFields(userInfo).size === 0) {
1966
2189
  return false;
1967
2190
  }
1968
-
1969
- // Input keys are entity CodeNames (post ReverseMapInputFieldNames); the denied set holds
1970
- // lowercased field Names — bridge via the field metadata once.
1971
- const deniedReadCodeNames = new Set<string>();
1972
- for (const field of entityInfo.Fields) {
1973
- if (deniedReadNames.has(field.Name.trim().toLowerCase())) {
1974
- deniedReadCodeNames.add(field.CodeName.trim().toLowerCase());
1975
- }
1976
- }
1977
-
1978
- const stripped: string[] = [];
1979
- for (const key of Object.keys(clientNewValues)) {
1980
- if (deniedReadCodeNames.has(key.trim().toLowerCase())) {
1981
- delete clientNewValues[key];
1982
- delete input[key];
1983
- stripped.push(key);
1984
- }
1985
- }
2191
+ AssertClientInputWritableForUser(entityInfo, clientNewValues, userInfo);
1986
2192
  if (Array.isArray(input.OldValues___)) {
1987
- input.OldValues___ = input.OldValues___.filter(
1988
- (item) => !deniedReadCodeNames.has(String(item.Key).trim().toLowerCase())
1989
- );
1990
- }
1991
- if (stripped.length > 0) {
1992
- LogDebug(
1993
- `[FieldSecurity] UpdateRecord on '${entityInfo.Name}' for user ${userInfo.Email}: ` +
1994
- `stripped client-sent value(s) for denied-read field(s) ${stripped.join(', ')}`
1995
- );
2193
+ const oldValueKeys: Record<string, unknown> = {};
2194
+ for (const item of input.OldValues___) {
2195
+ oldValueKeys[String(item.Key)] = null;
2196
+ }
2197
+ AssertClientInputWritableForUser(entityInfo, oldValueKeys, userInfo);
1996
2198
  }
1997
2199
  return true;
1998
2200
  }
@@ -2120,139 +2322,335 @@ export class ResolverBase {
2120
2322
  return entityInfo.Fields.find((f) => f.CodeName === key || f.Name === key);
2121
2323
  }
2122
2324
 
2123
- protected async TestAndSetClientOldValuesToDBValues(input: any, clientNewValues: any, entityObject: BaseEntity, contextUser: UserInfo) {
2124
- // we have OldValues, so we need to compare them to the values we just loaded from the DB
2125
- const clientOldValues = {};
2126
- // for each item in the oldValues array, add it to the clientOldValues object
2127
- input.OldValues___.forEach((item) => {
2128
- // we need to do a quick transform on the values to make sure they match the TS Type for the given field because item.Value will always be a string
2129
- const field = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, item.Key);
2130
- const val = this.ClientOldValueToFieldValue(field, item.Value);
2131
- clientOldValues[item.Key] = val;
2325
+ /**
2326
+ * Whether the API never gives a client this field's stored value: an encrypted field that is not
2327
+ * decrypted for the API reaches the client only as the sentinel or as fresh ciphertext, so the
2328
+ * client's old value for it can never be compared with the database value.
2329
+ */
2330
+ private isFieldOpaqueToClient(field: EntityFieldInfo | undefined): boolean {
2331
+ return Boolean(field?.Encrypt && !field.AllowDecryptInAPI);
2332
+ }
2333
+
2334
+ /**
2335
+ * The client's new values without the opaque fields it only echoed back: the sentinel, or the same
2336
+ * value it received as the old value. Writing either would replace the stored secret.
2337
+ */
2338
+ private withoutEchoedOpaqueValues<TOldVal, TNewVal>(
2339
+ entityInfo: EntityInfo,
2340
+ clientNewValues: Record<string, TNewVal>,
2341
+ clientOldValues: Record<string, TOldVal>,
2342
+ ): Record<string, TNewVal> {
2343
+ const kept: Record<string, TNewVal> = {};
2344
+ for (const key of Object.keys(clientNewValues)) {
2345
+ const f = this.findEntityFieldByCodeNameOrName(entityInfo, key);
2346
+ const newValue = clientNewValues[key];
2347
+ const unchanged = key in clientOldValues && !this.areEntityFieldValuesDifferent(f, clientOldValues[key], newValue, false);
2348
+ const echoed = this.isFieldOpaqueToClient(f) && (newValue === ENCRYPTED_SENTINEL || unchanged);
2349
+ if (!echoed) {
2350
+ kept[key] = newValue;
2351
+ }
2352
+ }
2353
+ return kept;
2354
+ }
2355
+
2356
+ /**
2357
+ * Copies concurrency-conflict diagnostic entries with every encrypted field's values replaced by
2358
+ * the sentinel, so the console, the error log and `MJ: Error Logs` never hold a secret.
2359
+ */
2360
+ private redactEncryptedConflictValues<TValue>(
2361
+ entityInfo: EntityInfo,
2362
+ entries: ReadonlyArray<ConflictDiagnosticEntry<TValue>>,
2363
+ ): ConflictDiagnosticEntry<TValue | string>[] {
2364
+ return entries.map((entry) => {
2365
+ if (!this.findEntityFieldByCodeNameOrName(entityInfo, entry.FieldName)?.Encrypt) {
2366
+ return entry;
2367
+ }
2368
+ const redacted: ConflictDiagnosticEntry<TValue | string> = { FieldName: entry.FieldName };
2369
+ if ('ClientOldValue' in entry) redacted.ClientOldValue = ENCRYPTED_SENTINEL;
2370
+ if ('ClientNewValue' in entry) redacted.ClientNewValue = ENCRYPTED_SENTINEL;
2371
+ if ('DBValue' in entry) redacted.DBValue = ENCRYPTED_SENTINEL;
2372
+ return redacted;
2132
2373
  });
2374
+ }
2375
+
2376
+ /**
2377
+ * Compares two typed entity field values for semantic inequality, handling `Date` epoch equality
2378
+ * (plus optional non-`datetimeoffset` timezone-hour-shift tolerance) and `null`/`undefined` normalization.
2379
+ */
2380
+ private areEntityFieldValuesDifferent<TLeft, TRight>(
2381
+ field: EntityFieldInfo | undefined,
2382
+ left: TLeft,
2383
+ right: TRight,
2384
+ allowTimezoneShiftWarning: boolean,
2385
+ fieldKeyForLog?: string
2386
+ ): boolean {
2387
+ const normalizedLeft = left === undefined ? null : left;
2388
+ const normalizedRight = right === undefined ? null : right;
2389
+
2390
+ if (normalizedLeft instanceof Date) {
2391
+ const rightDate =
2392
+ normalizedRight instanceof Date
2393
+ ? normalizedRight
2394
+ : field?.TSType === EntityFieldTSType.Date && normalizedRight !== null
2395
+ ? this.ClientOldValueToFieldValue(field, normalizedRight)
2396
+ : null;
2397
+ if (!(rightDate instanceof Date) || isNaN(rightDate.getTime())) {
2398
+ return true;
2399
+ }
2400
+ if (this.datesEqualAtColumnResolution(field, normalizedLeft, rightDate)) {
2401
+ return false;
2402
+ }
2403
+ const sqlType = field?.SQLFullType?.trim().toLowerCase() || '';
2404
+ if (sqlType !== 'datetimeoffset' && IsOnlyTimezoneShift(normalizedLeft, rightDate)) {
2405
+ if (allowTimezoneShiftWarning && fieldKeyForLog) {
2406
+ console.warn(
2407
+ `Timezone hour shift detected on field "${fieldKeyForLog}" (${sqlType || 'datetime'}). ` +
2408
+ `Client: ${normalizedLeft.toISOString()}, DB: ${rightDate.toISOString()}. ` +
2409
+ `Consider using datetimeoffset to avoid timezone ambiguity.`
2410
+ );
2411
+ }
2412
+ return false;
2413
+ }
2414
+ return true;
2415
+ }
2416
+
2417
+ if (normalizedRight instanceof Date && field?.TSType === EntityFieldTSType.Date && normalizedLeft !== null) {
2418
+ const leftDate = this.ClientOldValueToFieldValue(field, normalizedLeft);
2419
+ if (leftDate instanceof Date && !isNaN(leftDate.getTime())) {
2420
+ return !this.datesEqualAtColumnResolution(field, leftDate, normalizedRight);
2421
+ }
2422
+ }
2423
+
2424
+ if (typeof normalizedLeft === 'string' && typeof normalizedRight === 'string' && this.jsonStringsEquivalent(normalizedLeft, normalizedRight)) {
2425
+ return false;
2426
+ }
2427
+
2428
+ return Object.is(normalizedLeft, normalizedRight) ? false : normalizedLeft !== (normalizedRight as never);
2429
+ }
2430
+
2431
+ /**
2432
+ * True when two instants would be stored as the same value in the field's column: SQL Server
2433
+ * `datetime` resolves to 1/300 s and `smalldatetime` to one minute; every other type keeps milliseconds.
2434
+ */
2435
+ private datesEqualAtColumnResolution(field: EntityFieldInfo | undefined, left: Date, right: Date): boolean {
2436
+ return Math.abs(left.getTime() - right.getTime()) < this.dateColumnResolutionMs(field);
2437
+ }
2438
+
2439
+ private dateColumnResolutionMs(field: EntityFieldInfo | undefined): number {
2440
+ switch ((field?.Type ?? '').trim().toLowerCase()) {
2441
+ case 'datetime':
2442
+ return 1000 / 300;
2443
+ case 'smalldatetime':
2444
+ return 60_000;
2445
+ default:
2446
+ return 1;
2447
+ }
2448
+ }
2449
+
2450
+ /**
2451
+ * True when both strings are JSON documents with the same content, ignoring whitespace and key order.
2452
+ */
2453
+ private jsonStringsEquivalent(left: string, right: string): boolean {
2454
+ const canonicalLeft = this.canonicalJSONDocument(left);
2455
+ return canonicalLeft !== null && canonicalLeft === this.canonicalJSONDocument(right);
2456
+ }
2133
2457
 
2134
- // clientOldValues now has all of the oldValues the CLIENT passed us. Now we need to build the same kind of object
2135
- // with the DB values
2458
+ /**
2459
+ * Canonical form of a JSON object or array string, or null when the text is not one.
2460
+ */
2461
+ private canonicalJSONDocument(text: string): string | null {
2462
+ const first = text.trimStart().charAt(0);
2463
+ if (first !== '{' && first !== '[') {
2464
+ return null;
2465
+ }
2466
+ try {
2467
+ return Canonicalize(JSON.parse(text));
2468
+ } catch {
2469
+ return null;
2470
+ }
2471
+ }
2472
+
2473
+ protected async TestAndSetClientOldValuesToDBValues<
2474
+ TOldVal,
2475
+ TNewVal,
2476
+ TInput extends { OldValues___?: ReadonlyArray<{ Key: string; Value: TOldVal }> | null }
2477
+ >(
2478
+ input: TInput,
2479
+ rawClientNewValues: Record<string, TNewVal>,
2480
+ entityObject: BaseEntity,
2481
+ contextUser: UserInfo
2482
+ ): Promise<void> {
2483
+ type ConvertedFieldValue = ReturnType<ResolverBase['ClientOldValueToFieldValue']>;
2484
+ const clientOldValues: Record<string, ConvertedFieldValue> = {};
2485
+ for (const item of input.OldValues___ ?? []) {
2486
+ // Transform string-serialized wire values to match the field's TypeScript type
2487
+ const field = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, item.Key);
2488
+ clientOldValues[item.Key] = this.ClientOldValueToFieldValue(field, item.Value);
2489
+ }
2490
+ const clientNewValues = this.withoutEchoedOpaqueValues(entityObject.EntityInfo, rawClientNewValues, clientOldValues);
2491
+
2492
+ // fls-guard: compared on the server only, never returned
2136
2493
  const dbValues = entityObject.GetAll();
2137
2494
 
2138
- // 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
2139
- const dbDifferences = [];
2140
- Object.keys(clientOldValues).forEach((key) => {
2495
+ interface DBDifferenceEntry {
2496
+ FieldName: string;
2497
+ ClientOldValue: ConvertedFieldValue;
2498
+ DBValue: ConvertedFieldValue;
2499
+ }
2500
+ interface ClientDifferenceEntry {
2501
+ FieldName: string;
2502
+ ClientOldValue: ConvertedFieldValue;
2503
+ ClientNewValue: TNewVal;
2504
+ }
2505
+
2506
+ const dbDifferences: DBDifferenceEntry[] = [];
2507
+ const dbDiffByField = new Map<string, DBDifferenceEntry>();
2508
+
2509
+ for (const key of Object.keys(clientOldValues)) {
2141
2510
  const f = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, key);
2142
- const dbVal = f && f.Name in dbValues ? dbValues[f.Name] : dbValues[key];
2143
- let different = false;
2144
- switch (typeof clientOldValues[key]) {
2145
- case 'number':
2146
- different = clientOldValues[key] !== dbVal;
2147
- break;
2148
- case 'boolean':
2149
- different = clientOldValues[key] !== dbVal;
2150
- break;
2151
- case 'object':
2152
- if (clientOldValues[key] instanceof Date) {
2153
- const clientDate = clientOldValues[key] as Date;
2154
- const dbDate = dbVal;
2155
- if (dbDate == null || !(dbDate instanceof Date)) {
2156
- different = true;
2157
- } else if (clientDate.getTime() !== dbDate.getTime()) {
2158
- // Check if this is a datetime/datetime2 field with only a timezone hour shift
2159
- const sqlType = f?.SQLFullType?.trim().toLowerCase() || '';
2160
- if (sqlType !== 'datetimeoffset' && IsOnlyTimezoneShift(clientDate, dbDate)) {
2161
- console.warn(
2162
- `Timezone hour shift detected on field "${key}" (${sqlType || 'datetime'}). ` +
2163
- `Client: ${clientDate.toISOString()}, DB: ${dbDate.toISOString()}. ` +
2164
- `Consider using datetimeoffset to avoid timezone ambiguity.`
2165
- );
2166
- different = false; // Allow timezone shifts through for non-datetimeoffset fields
2167
- } else {
2168
- different = true;
2169
- }
2170
- }
2171
- } else if (clientOldValues[key] === null) {
2172
- different = dbVal !== null;
2173
- }
2174
- break;
2175
- default:
2176
- different = clientOldValues[key] !== dbVal;
2177
- break;
2511
+ if (this.isFieldOpaqueToClient(f)) {
2512
+ continue;
2178
2513
  }
2514
+ const dbVal: ConvertedFieldValue = f && f.Name in dbValues ? dbValues[f.Name] : dbValues[key];
2515
+ const different = this.areEntityFieldValuesDifferent(f, clientOldValues[key], dbVal, true, key);
2179
2516
  if (different && f && !f.ReadOnly) {
2180
- // only include updateable fields
2181
- dbDifferences.push({
2517
+ const entry: DBDifferenceEntry = {
2182
2518
  FieldName: key,
2183
2519
  ClientOldValue: clientOldValues[key],
2184
2520
  DBValue: dbVal,
2185
- });
2521
+ };
2522
+ dbDifferences.push(entry);
2523
+ dbDiffByField.set(key, entry);
2186
2524
  }
2187
- });
2525
+ }
2526
+
2527
+ let fieldsToPreserveFromDB: Set<string> | undefined;
2188
2528
 
2189
2529
  if (dbDifferences.length > 0) {
2190
- // now we have an array of any dbDifferences with length > 0, between the clientOldValues and the dbValues, we need to check to see if any of the differences are on fields that the client is trying to update
2191
- // first step is to get clientNewValues into an object that is like clientOldValues, get the diff and then compare that diff to the differences array that shows diff between DB and ClientOld
2192
- const clientDifferences = [];
2193
- Object.keys(clientOldValues).forEach((key) => {
2530
+ const clientDifferences: ClientDifferenceEntry[] = [];
2531
+ const clientDiffFieldNames = new Set<string>();
2532
+
2533
+ for (const key of Object.keys(clientOldValues)) {
2194
2534
  const f = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, key);
2195
- if (key in clientNewValues && clientOldValues[key] !== clientNewValues[key] && f && f.AllowUpdateAPI && !f.IsPrimaryKey) {
2196
- // only include updateable fields
2535
+ if (
2536
+ key in clientNewValues &&
2537
+ f &&
2538
+ f.AllowUpdateAPI &&
2539
+ !f.IsPrimaryKey &&
2540
+ this.areEntityFieldValuesDifferent(f, clientOldValues[key], clientNewValues[key], false)
2541
+ ) {
2197
2542
  clientDifferences.push({
2198
2543
  FieldName: key,
2199
2544
  ClientOldValue: clientOldValues[key],
2200
2545
  ClientNewValue: clientNewValues[key],
2201
2546
  });
2547
+ clientDiffFieldNames.add(key);
2548
+ }
2549
+ }
2550
+
2551
+ // True conflicting overlap: client modified a field whose authoritative DB value differs from BOTH
2552
+ // ClientOldValue AND ClientNewValue (idempotent concurrent writes to the same value converge cleanly).
2553
+ const overlap = clientDifferences.filter((cd) => {
2554
+ const dd = dbDiffByField.get(cd.FieldName);
2555
+ if (!dd) {
2556
+ return false;
2202
2557
  }
2558
+ const f = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, cd.FieldName);
2559
+ return this.areEntityFieldValuesDifferent(f, cd.ClientNewValue, dd.DBValue, true);
2203
2560
  });
2204
2561
 
2205
- // now we have clientDifferences which shows what the client thinks they are changing. And, we have the dbDifferences array that shows changes between the clientOldValues and the dbValues
2206
- // if there is ANY overlap in the FIELDS that appear in both arrays, we need to log a warning but allow the save to continue
2207
- const dbDiffFieldNames = new Set(dbDifferences.map((dd) => dd.FieldName));
2208
- const overlap = clientDifferences.filter((cd) => dbDiffFieldNames.has(cd.FieldName));
2209
2562
  if (overlap.length > 0) {
2563
+ const conflictingFields = overlap.map((o) => o.FieldName);
2564
+ const entityInfo = entityObject.EntityInfo;
2565
+ const shownClientDifferences = this.redactEncryptedConflictValues<ConvertedFieldValue | TNewVal>(entityInfo, clientDifferences);
2566
+ const shownDBDifferences = this.redactEncryptedConflictValues<ConvertedFieldValue | TNewVal>(entityInfo, dbDifferences);
2567
+ const shownOverlap = this.redactEncryptedConflictValues<ConvertedFieldValue | TNewVal>(entityInfo, overlap);
2210
2568
  const msg = {
2211
2569
  Message:
2212
- 'Inconsistency between old values provided for changed fields, and the values of one or more of those fields in the database. Save operation continued with warning.',
2213
- ClientDifferences: clientDifferences,
2214
- DBDifferences: dbDifferences,
2215
- Overlap: overlap,
2570
+ 'Concurrent modification conflict: one or more fields modified by the client were updated in the database since the record was loaded. Save rejected to prevent overwriting concurrent changes.',
2571
+ ClientDifferences: shownClientDifferences,
2572
+ DBDifferences: shownDBDifferences,
2573
+ Overlap: shownOverlap,
2216
2574
  };
2217
-
2218
- // Log as warning to console and ErrorLog table instead of throwing error
2219
- console.warn('Entity save inconsistency detected but allowing save to continue:', JSON.stringify(msg));
2220
- LogError({
2221
- service: 'ResolverBase',
2222
- operation: 'TestAndSetClientOldValuesToDBValues',
2223
- error: `Entity save inconsistency detected: ${JSON.stringify(msg)}`,
2224
- details: {
2575
+
2576
+ console.warn('Entity save concurrency conflict detected — rejecting conflicting update:', JSON.stringify(msg));
2577
+ LogErrorEx({
2578
+ message: `Entity save inconsistency detected: ${JSON.stringify(msg)}`,
2579
+ severity: 'warning',
2580
+ category: 'ResolverBase.TestAndSetClientOldValuesToDBValues',
2581
+ metadata: {
2225
2582
  entityName: entityObject.EntityInfo.Name,
2226
- clientDifferences: clientDifferences,
2227
- dbDifferences: dbDifferences,
2228
- overlap: overlap
2229
- }
2583
+ clientDifferences: shownClientDifferences,
2584
+ dbDifferences: shownDBDifferences,
2585
+ overlap: shownOverlap,
2586
+ },
2230
2587
  });
2231
2588
 
2232
- // Create ErrorLog record in the database — use the entity's bound provider so the
2233
- // ErrorLog write goes to the same connection as the entity that triggered it.
2589
+ // Persist SAVE_INCONSISTENCY ErrorLog record on the entity's bound provider before rejecting
2234
2590
  try {
2235
- const md = entityObject.ProviderToUse as unknown as IMetadataProvider;
2236
- const errorLogEntity = await md.GetEntityObject<MJErrorLogEntity>('MJ: Error Logs', contextUser);
2237
- errorLogEntity.Code = 'ENTITY_SAVE_INCONSISTENCY';
2238
- errorLogEntity.Message = `Entity save inconsistency detected for ${entityObject.EntityInfo.Name}: ${JSON.stringify(msg)}`;
2239
- errorLogEntity.Status = 'Warning';
2240
- errorLogEntity.Category = 'Entity Save';
2241
- errorLogEntity.CreatedBy = contextUser.Email || contextUser.Name;
2242
-
2243
- const saveResult = await errorLogEntity.Save();
2244
- if (!saveResult) {
2245
- console.error('Failed to save ErrorLog record');
2591
+ const hasEntityObjectFactory = (
2592
+ p: object | null | undefined
2593
+ ): p is Pick<IMetadataProvider, 'GetEntityObject'> => Boolean(p && 'GetEntityObject' in p);
2594
+ const providerCandidate = entityObject.ProviderToUse;
2595
+ if (hasEntityObjectFactory(providerCandidate)) {
2596
+ const errorLogEntity = await providerCandidate.GetEntityObject<MJErrorLogEntity>('MJ: Error Logs', contextUser);
2597
+ // Code is NCHAR(20); a longer value is rejected at the database
2598
+ errorLogEntity.Code = 'SAVE_INCONSISTENCY';
2599
+ errorLogEntity.Message = `Entity save inconsistency detected for ${entityObject.EntityInfo.Name}: ${JSON.stringify(msg)}`;
2600
+ errorLogEntity.Status = 'Warning';
2601
+ errorLogEntity.Category = 'Entity Save';
2602
+ errorLogEntity.CreatedBy = contextUser.Email || contextUser.Name;
2603
+
2604
+ const saveResult = await errorLogEntity.Save();
2605
+ if (!saveResult) {
2606
+ console.error(
2607
+ `Failed to save ErrorLog record for ${entityObject.EntityInfo.Name}: ${errorLogEntity.LatestResult?.CompleteMessage}`
2608
+ );
2609
+ }
2246
2610
  }
2247
2611
  } catch (errorLogError) {
2248
2612
  console.error('Error creating ErrorLog record:', errorLogError);
2249
2613
  }
2614
+
2615
+ throw new GraphQLError(
2616
+ `Concurrent modification conflict on ${entityObject.EntityInfo.Name} for field(s): ${conflictingFields.join(', ')}. Reload the record and retry your changes.`,
2617
+ {
2618
+ extensions: {
2619
+ code: 'CONCURRENT_MODIFICATION_CONFLICT',
2620
+ entityName: entityObject.EntityInfo.Name,
2621
+ conflictingFields,
2622
+ },
2623
+ }
2624
+ );
2625
+ }
2626
+
2627
+ // Non-overlapping concurrent DB edits: preserve any field that changed in the database
2628
+ // when the client itself did NOT modify that field (prevents full-payload clients or forged
2629
+ // OldValues___ from clobbering concurrent DB updates on untouched columns).
2630
+ fieldsToPreserveFromDB = new Set<string>();
2631
+ for (const dd of dbDifferences) {
2632
+ if (!clientDiffFieldNames.has(dd.FieldName)) {
2633
+ fieldsToPreserveFromDB.add(dd.FieldName);
2634
+ const f = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, dd.FieldName);
2635
+ if (f) {
2636
+ fieldsToPreserveFromDB.add(f.Name);
2637
+ fieldsToPreserveFromDB.add(f.CodeName);
2638
+ }
2639
+ }
2250
2640
  }
2251
2641
  }
2252
2642
 
2253
- // If we get here that means we've not thrown an exception, so there is
2254
- // NO OVERLAP, so we can set the new values from the data provided from the client now...
2255
- entityObject.SetMany(clientNewValues);
2643
+ if (fieldsToPreserveFromDB && fieldsToPreserveFromDB.size > 0) {
2644
+ const mergedValues: Record<string, TNewVal> = {};
2645
+ for (const key of Object.keys(clientNewValues)) {
2646
+ if (!fieldsToPreserveFromDB.has(key)) {
2647
+ mergedValues[key] = clientNewValues[key];
2648
+ }
2649
+ }
2650
+ entityObject.SetMany(mergedValues);
2651
+ } else {
2652
+ entityObject.SetMany(clientNewValues);
2653
+ }
2256
2654
  }
2257
2655
 
2258
2656
  protected async DeleteRecord(
@@ -2281,7 +2679,10 @@ export class ResolverBase {
2281
2679
  extensions: { code: 'LOAD_ENTITY_ERROR', entityName },
2282
2680
  });
2283
2681
  }
2284
- const returnValue = entityObject.GetAll(); // grab the values before we delete so we can return last state before delete if we are successful.
2682
+ // Grab the values before we delete so we can return the last state if the delete succeeds,
2683
+ // projected for the caller in case the object holds a column they may not read.
2684
+ // fls-guard: projected by ProjectEntityValuesForUser
2685
+ const returnValue = ProjectEntityValuesForUser(provider, entityObject.EntityInfo, entityObject.GetAll(), this.GetUserFromPayload(userPayload));
2285
2686
 
2286
2687
  this.ListenForEntityMessages(entityObject, pubSub, userPayload);
2287
2688