@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,4 +1,5 @@
1
- import { BaseEntity, BaseEntityEvent, CompositeKey, EntityFieldTSType, EntityPermissionType, KeyValuePair, LogDebug, LogError, LogStatus, Metadata, ReadableFieldsTransportKey, RecordChangeFieldSecurityProjector, RecordChangePayloadFields, } from '@memberjunction/core';
1
+ import { AssertClientInputWritableForUser, BaseEntity, BaseEntityEvent, CompositeKey, EntityFieldTSType, EntityPermissionType, KeyValuePair, LogDebug, LogError, LogErrorEx, LogStatus, Metadata, ProjectEntityValuesForUser, ReadableFieldsTransportKey, RecordChangeFieldSecurityProjector, RecordChangePayloadFields, } from '@memberjunction/core';
2
+ import { UserViewEngine } from '@memberjunction/core-entities';
2
3
  import { ClientClauseScreen, UserCache } from '@memberjunction/generic-database-provider';
3
4
  import { AuthorizationError } from 'type-graphql';
4
5
  import { GraphQLError } from 'graphql';
@@ -6,10 +7,10 @@ import { RefusalExtensions } from './refusalExtensions.js';
6
7
  import { GetAPIKeyEngine } from '@memberjunction/api-keys';
7
8
  import { httpTransport, CloudEvent, emitterFor } from 'cloudevents';
8
9
  import { DeleteOptionsInput } from './DeleteOptionsInput.js';
9
- import { MJEventType, MJGlobal, ENCRYPTED_SENTINEL, ReplaceByteArraysWithBase64, EscapeSQLString, IsValueEncrypted, IsOnlyTimezoneShift } from '@memberjunction/global';
10
- import { SQLParser, IsBalancedSQLFragment, LexSQL } from '@memberjunction/sql-parser';
10
+ import { MJEventType, MJGlobal, UUIDsEqual, ENCRYPTED_SENTINEL, ReplaceByteArraysWithBase64, EscapeSQLString, IsValueEncrypted, IsOnlyTimezoneShift, Canonicalize } from '@memberjunction/global';
11
+ import { SQLParser, IsBalancedSQLFragment, LexSQL, StrictFragmentValidator } from '@memberjunction/sql-parser';
11
12
  import { EncryptionEngine, IsEncryptionKeyUnavailable, ReportEncryptionKeyUnavailable } from '@memberjunction/encryption';
12
- import { PublishStatusUpdate } from './PushStatusResolver.js';
13
+ import { PublishStatusUpdate, StatusUpdateOwnerID } from './PushStatusResolver.js';
13
14
  import { CACHE_INVALIDATION_TOPIC, MayBroadcastRecordData } from './CacheInvalidationResolver.js';
14
15
  import { PubSubManager } from './PubSubManager.js';
15
16
  import { FieldMapper } from '@memberjunction/graphql-dataprovider';
@@ -17,16 +18,26 @@ import { FieldMapper } from '@memberjunction/graphql-dataprovider';
17
18
  * Entities whose update is always hydrated from the stored row, never from the client's `OldValues___`, though they
18
19
  * do not track record changes: their server-side write rules compare against the prior state, which a client could
19
20
  * otherwise supply itself (lower-case names). See `ResolverBase.MustLoadTruthFromDatabase`.
21
+ *
22
+ * `mj: conversations` and `mj: ai agent sessions` carry the anonymous realtime-session ownership key
23
+ * (`ExternalID` / `ConversationID`), which their server entities refuse an anonymous visitor to change — a rule
24
+ * that decides on `Dirty`, so the baseline must be the stored row, not a client-supplied one (#4931).
20
25
  */
21
26
  const ENTITIES_THAT_ALWAYS_LOAD_STORED_ROW = new Set([
22
27
  'mj: record changes',
23
28
  'mj: conversation details',
24
29
  'mj: data context items',
30
+ 'mj: conversations',
31
+ 'mj: ai agent sessions',
32
+ 'mj: ai agent runs',
25
33
  ]);
34
+ const ACTIVE_ENTITY_SESSION_TTL_MS = 24 * 60 * 60 * 1000;
35
+ const ACTIVE_ENTITY_SESSION_MAX_PER_ENTITY = 2048;
26
36
  export class ResolverBase {
27
37
  static { this._emit = process.env.CLOUDEVENTS_HTTP_TRANSPORT ? emitterFor(httpTransport(process.env.CLOUDEVENTS_HTTP_TRANSPORT)) : null; }
28
38
  static { this._cloudeventsHeaders = process.env.CLOUDEVENTS_HTTP_HEADERS ? JSON.parse(process.env.CLOUDEVENTS_HTTP_HEADERS) : {}; }
29
39
  static { this._eventSubscriptionKey = '___MJServer___ResolverBase___EventSubscriptions'; }
40
+ static { this._activeEntitySessionsKey = '___MJServer___ResolverBase___ActiveEntitySessions'; }
30
41
  get eventSubscriptions() {
31
42
  // 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
32
43
  // could actually be duplicated and we'd end up with multiple instances of the same map, which would be bad.
@@ -37,6 +48,13 @@ export class ResolverBase {
37
48
  }
38
49
  return g[ResolverBase._eventSubscriptionKey];
39
50
  }
51
+ get activeEntitySessions() {
52
+ const g = MJGlobal.Instance.GetGlobalObjectStore();
53
+ if (!g[ResolverBase._activeEntitySessionsKey]) {
54
+ g[ResolverBase._activeEntitySessionsKey] = new Map();
55
+ }
56
+ return g[ResolverBase._activeEntitySessionsKey];
57
+ }
40
58
  /**
41
59
  * Maps field names to their GraphQL-safe CodeNames and handles encryption for API responses.
42
60
  *
@@ -69,7 +87,7 @@ export class ResolverBase {
69
87
  const entityInfo = md.EntityByName(entityName);
70
88
  if (!entityInfo)
71
89
  throw new Error(`Entity ${entityName} not found in metadata`);
72
- const denied = deniedReadFields ?? (entityInfo.EnableFieldLevelSecurity && contextUser
90
+ const denied = deniedReadFields ?? (entityInfo.FieldLevelSecurityApplies && contextUser
73
91
  ? entityInfo.GetDeniedReadFields(contextUser)
74
92
  : null);
75
93
  const mapper = new FieldMapper();
@@ -284,6 +302,7 @@ export class ResolverBase {
284
302
  if (!loaded) {
285
303
  return null;
286
304
  }
305
+ // fls-guard: MapFieldNamesToCodeNames drops the fields the caller may not read
287
306
  const mapped = await this.MapFieldNamesToCodeNames(entityName, entityObject.GetAll(), contextUser);
288
307
  return mapped;
289
308
  }
@@ -325,28 +344,25 @@ export class ResolverBase {
325
344
  if (field.AllowDecryptInAPI)
326
345
  continue;
327
346
  // AllowDecryptInAPI is false - we need to filter the value
347
+ if (!field.SendEncryptedValue) {
348
+ // SendEncryptedValue is false - replace with sentinel immediately without touching EncryptionEngine
349
+ dataObject[fieldName] = ENCRYPTED_SENTINEL;
350
+ continue;
351
+ }
328
352
  const engine = EncryptionEngine.Instance;
329
353
  await engine.Config(false, contextUser);
330
354
  const keyMarker = field.EncryptionKeyID ? engine.GetKeyByID(field.EncryptionKeyID)?.Marker : undefined;
331
- if (field.SendEncryptedValue) {
332
- // Re-encrypt the value before sending
333
- // Only re-encrypt if it's not already encrypted (data provider decrypted it)
334
- if (typeof value === 'string' && !IsValueEncrypted(value, keyMarker)) {
335
- try {
336
- const encryptedValue = await engine.Encrypt(value, field.EncryptionKeyID, contextUser);
337
- dataObject[fieldName] = encryptedValue;
338
- }
339
- catch (err) {
340
- // If re-encryption fails, use sentinel for safety
341
- LogError(`Failed to re-encrypt field ${fieldName} for API response: ${err}`);
342
- dataObject[fieldName] = ENCRYPTED_SENTINEL;
343
- }
355
+ // Only encrypt if the value is not already encrypted (PreserveRestrictedApiCiphertext keeps DB/cache ciphertext intact)
356
+ if (typeof value === 'string' && !IsValueEncrypted(value, keyMarker)) {
357
+ try {
358
+ const encryptedValue = await engine.Encrypt(value, field.EncryptionKeyID, contextUser);
359
+ dataObject[fieldName] = encryptedValue;
360
+ }
361
+ catch (err) {
362
+ // If re-encryption fails, use sentinel for safety
363
+ LogError(`Failed to re-encrypt field ${fieldName} for API response: ${err}`);
364
+ dataObject[fieldName] = ENCRYPTED_SENTINEL;
344
365
  }
345
- // If already encrypted (shouldn't happen normally), keep as-is
346
- }
347
- else {
348
- // SendEncryptedValue is false - replace with sentinel
349
- dataObject[fieldName] = ENCRYPTED_SENTINEL;
350
366
  }
351
367
  }
352
368
  return dataObject;
@@ -449,23 +465,54 @@ export class ResolverBase {
449
465
  return [];
450
466
  }
451
467
  }
468
+ /**
469
+ * Resolves a User View by name, consulting the in-memory {@link UserViewEngine} cache first
470
+ * and falling back to a database `RunView` only on a cache miss.
471
+ */
472
+ async resolveUserViewByName(viewName, provider, contextUser) {
473
+ if (UserViewEngine.Instance.Loaded) {
474
+ const cached = UserViewEngine.Instance.GetViewByName(viewName);
475
+ if (cached) {
476
+ return cached;
477
+ }
478
+ }
479
+ const rv = provider;
480
+ const result = await rv.RunView({
481
+ EntityName: 'MJ: User Views',
482
+ // SECURITY: the view name is client-supplied and lands inside a SQL literal.
483
+ ExtraFilter: "Name='" + EscapeSQLString(viewName) + "'",
484
+ }, contextUser);
485
+ if (result && result.Success && result.Results.length > 0) {
486
+ return result.Results[0];
487
+ }
488
+ return null;
489
+ }
490
+ /**
491
+ * Resolves a User View by ID, consulting the in-memory {@link UserViewEngine} cache first
492
+ * and falling back to an entity `Load()` only on a cache miss.
493
+ */
494
+ async resolveUserViewByID(viewID, provider, contextUser) {
495
+ if (UserViewEngine.Instance.Loaded) {
496
+ const cached = UserViewEngine.Instance.GetViewById(viewID);
497
+ if (cached) {
498
+ return cached;
499
+ }
500
+ }
501
+ const viewInfo = await provider.GetEntityObject('MJ: User Views', contextUser);
502
+ await viewInfo.Load(viewID);
503
+ return viewInfo;
504
+ }
452
505
  async RunViewByNameGeneric(viewInput, provider, userPayload, pubSub) {
453
506
  try {
454
507
  // Log aggregate input for debugging
455
508
  if (viewInput.Aggregates?.length) {
456
509
  LogStatus(`[ResolverBase] RunViewByNameGeneric received aggregates: viewName=${viewInput.ViewName}, aggregateCount=${viewInput.Aggregates.length}, aggregates=${JSON.stringify(viewInput.Aggregates.map(a => ({ expression: a.expression, alias: a.alias })))}`);
457
510
  }
458
- const rv = provider;
459
- const result = await rv.RunView({
460
- EntityName: 'MJ: User Views',
461
- // SECURITY: the view name is client-supplied and lands inside a SQL literal.
462
- ExtraFilter: "Name='" + EscapeSQLString(viewInput.ViewName) + "'",
463
- }, userPayload.userRecord);
464
- if (result && result.Success && result.Results.length > 0) {
465
- const viewInfo = result.Results[0];
511
+ const viewInfo = await this.resolveUserViewByName(viewInput.ViewName, provider, userPayload.userRecord);
512
+ if (viewInfo) {
466
513
  return this.RunViewGenericInternal(provider, viewInfo, viewInput.ExtraFilter, viewInput.OrderBy, viewInput.UserSearchString, viewInput.ExcludeUserViewRunID, viewInput.OverrideExcludeFilter, viewInput.SaveViewResults, viewInput.Fields, viewInput.IgnoreMaxRows, viewInput.ExcludeDataFromAllPriorViewRuns, viewInput.ForceAuditLog, viewInput.AuditLogDescription, viewInput.ResultType, userPayload, viewInput.MaxRows, viewInput.StartRow, viewInput.Aggregates, viewInput.AfterKey
467
514
  ? CompositeKey.FromKeyValuePairs(viewInput.AfterKey.KeyValuePairs)
468
- : undefined, viewInput.BypassCache, viewInput.DataSource, viewInput.IncludeBinaryFields);
515
+ : undefined, viewInput.BypassCache, viewInput.DataSource, viewInput.IncludeBinaryFields, viewInput.AlternateBaseView, viewInput.Properties, viewInput.SkipTotalRowCount);
469
516
  }
470
517
  else {
471
518
  LogError(`RunViewByNameGeneric: View ${viewInput.ViewName} not found or no results returned`);
@@ -484,9 +531,11 @@ export class ResolverBase {
484
531
  LogStatus(`[ResolverBase] RunViewByIDGeneric received aggregates: viewID=${viewInput.ViewID}, aggregateCount=${viewInput.Aggregates.length}, aggregates=${JSON.stringify(viewInput.Aggregates.map(a => ({ expression: a.expression, alias: a.alias })))}`);
485
532
  }
486
533
  const contextUser = this.GetUserFromPayload(userPayload);
487
- const viewInfo = await provider.GetEntityObject('MJ: User Views', contextUser);
488
- await viewInfo.Load(viewInput.ViewID);
489
- return this.RunViewGenericInternal(provider, viewInfo, viewInput.ExtraFilter, viewInput.OrderBy, viewInput.UserSearchString, viewInput.ExcludeUserViewRunID, viewInput.OverrideExcludeFilter, viewInput.SaveViewResults, viewInput.Fields, viewInput.IgnoreMaxRows, viewInput.ExcludeDataFromAllPriorViewRuns, viewInput.ForceAuditLog, viewInput.AuditLogDescription, viewInput.ResultType, userPayload, viewInput.MaxRows, viewInput.StartRow, viewInput.Aggregates, undefined, viewInput.BypassCache, viewInput.DataSource, viewInput.IncludeBinaryFields);
534
+ const viewInfo = await this.resolveUserViewByID(viewInput.ViewID, provider, contextUser);
535
+ if (!viewInfo) {
536
+ return null;
537
+ }
538
+ return this.RunViewGenericInternal(provider, viewInfo, viewInput.ExtraFilter, viewInput.OrderBy, viewInput.UserSearchString, viewInput.ExcludeUserViewRunID, viewInput.OverrideExcludeFilter, viewInput.SaveViewResults, viewInput.Fields, viewInput.IgnoreMaxRows, viewInput.ExcludeDataFromAllPriorViewRuns, viewInput.ForceAuditLog, viewInput.AuditLogDescription, viewInput.ResultType, userPayload, viewInput.MaxRows, viewInput.StartRow, viewInput.Aggregates, undefined, viewInput.BypassCache, viewInput.DataSource, viewInput.IncludeBinaryFields, viewInput.AlternateBaseView, viewInput.Properties, viewInput.SkipTotalRowCount);
490
539
  }
491
540
  catch (err) {
492
541
  console.log(err);
@@ -509,7 +558,7 @@ export class ResolverBase {
509
558
  EntityID: entity.ID,
510
559
  EntityBaseView: entity.BaseView,
511
560
  }; // only providing a few bits of data here, but it's enough to get the view to run
512
- return this.RunViewGenericInternal(provider, viewInfo, viewInput.ExtraFilter, viewInput.OrderBy, viewInput.UserSearchString, viewInput.ExcludeUserViewRunID, viewInput.OverrideExcludeFilter, false, viewInput.Fields, viewInput.IgnoreMaxRows, false, viewInput.ForceAuditLog, viewInput.AuditLogDescription, viewInput.ResultType, userPayload, viewInput.MaxRows, viewInput.StartRow, viewInput.Aggregates, undefined, viewInput.BypassCache, viewInput.DataSource, viewInput.IncludeBinaryFields);
561
+ return this.RunViewGenericInternal(provider, viewInfo, viewInput.ExtraFilter, viewInput.OrderBy, viewInput.UserSearchString, viewInput.ExcludeUserViewRunID, viewInput.OverrideExcludeFilter, false, viewInput.Fields, viewInput.IgnoreMaxRows, false, viewInput.ForceAuditLog, viewInput.AuditLogDescription, viewInput.ResultType, userPayload, viewInput.MaxRows, viewInput.StartRow, viewInput.Aggregates, undefined, viewInput.BypassCache, viewInput.DataSource, viewInput.IncludeBinaryFields, viewInput.AlternateBaseView, undefined, viewInput.SkipTotalRowCount);
513
562
  }
514
563
  catch (err) {
515
564
  console.log(err);
@@ -524,7 +573,7 @@ export class ResolverBase {
524
573
  try {
525
574
  let viewInfo = null;
526
575
  if (viewInput.ViewName) {
527
- viewInfo = this.safeFirstArrayElement(await this.findBy(provider, 'MJ: User Views', { Name: viewInput.ViewName }, userPayload.userRecord));
576
+ viewInfo = await this.resolveUserViewByName(viewInput.ViewName, provider, userPayload.userRecord);
528
577
  // Populate EntityName on the input so callers (e.g. RunViews resolver) can
529
578
  // look up the entity without re-querying the view
530
579
  if (viewInfo && !viewInput.EntityName) {
@@ -532,8 +581,7 @@ export class ResolverBase {
532
581
  }
533
582
  }
534
583
  else if (viewInput.ViewID) {
535
- viewInfo = await provider.GetEntityObject('MJ: User Views', contextUser);
536
- await viewInfo.Load(viewInput.ViewID);
584
+ viewInfo = await this.resolveUserViewByID(viewInput.ViewID, provider, contextUser);
537
585
  // Populate EntityName on the input so callers can look up the entity
538
586
  if (viewInfo && !viewInput.EntityName) {
539
587
  viewInput.EntityName = viewInfo.Entity;
@@ -580,6 +628,9 @@ export class ResolverBase {
580
628
  bypassCache: viewInput.BypassCache,
581
629
  dataSource: viewInput.DataSource,
582
630
  includeBinaryFields: viewInput.IncludeBinaryFields,
631
+ skipTotalRowCount: viewInput.SkipTotalRowCount,
632
+ alternateBaseView: viewInput.AlternateBaseView,
633
+ properties: viewInput.Properties,
583
634
  });
584
635
  }
585
636
  catch (err) {
@@ -590,6 +641,30 @@ export class ResolverBase {
590
641
  let results = await this.RunViewsGenericInternal(params);
591
642
  return results;
592
643
  }
644
+ /**
645
+ * What an entity event may carry to a CloudEvents sink or a log line.
646
+ *
647
+ * The whole row for an entity without field security. For one where it applies (its own flag
648
+ * or an IS-A ancestor's), and for `MJ: Record Changes` (whose payload columns hold other
649
+ * entities' values), only the primary key and the names of the fields that changed: the sink
650
+ * is outside MemberJunction with no reader to project the row for, and these events fire for
651
+ * server-internal saves too, whose objects hold every column.
652
+ */
653
+ EntityEventPayload(baseEntity, changedFieldNames) {
654
+ const entityInfo = baseEntity.EntityInfo;
655
+ if (!entityInfo?.FieldLevelSecurityApplies && !RecordChangeFieldSecurityProjector.IsRecordChangesEntity(entityInfo)) {
656
+ // fls-guard: entity without field security and not the audit trail; whole row is the documented payload
657
+ return baseEntity.GetAll();
658
+ }
659
+ return {
660
+ // Read through Get() rather than PrimaryKey, which builds every field object of a raw-mode record.
661
+ PrimaryKey: (entityInfo.PrimaryKeys ?? []).map((pk) => ({ FieldName: pk.Name, Value: baseEntity.Get(pk.Name) })),
662
+ // A completed save has already rebased the object, so nothing on it is dirty: the event
663
+ // carries the names the save wrote. Other events (save_started) still read them off the object.
664
+ // GetDirtyFieldNames answers without building field objects for a record still in raw mode.
665
+ ChangedFields: changedFieldNames ?? baseEntity.GetDirtyFieldNames(),
666
+ };
667
+ }
593
668
  static { this._priorEmittedData = []; }
594
669
  async EmitCloudEvent({ component, event, eventCode, args }) {
595
670
  if (ResolverBase._emit && event === MJEventType.ComponentEvent && eventCode === BaseEntity.BaseEventCode) {
@@ -597,7 +672,7 @@ export class ResolverBase {
597
672
  const type = `MemberJunction.${event}${extendedType}`;
598
673
  const source = `${process.env.CLOUDEVENTS_SOURCE ?? 'MemberJunction'}`;
599
674
  const subject = args instanceof BaseEntityEvent ? args.baseEntity.EntityInfo.CodeName : undefined;
600
- const data = args?.baseEntity?.GetAll() ?? {};
675
+ const data = args?.baseEntity ? this.EntityEventPayload(args.baseEntity, args.ChangedFieldNames) : {};
601
676
  const cloudEvent = new CloudEvent({ type, source, subject, data });
602
677
  try {
603
678
  // check to see if the combination of Entity and pkey was already emitted, if so, Log that condtion next
@@ -777,7 +852,7 @@ export class ResolverBase {
777
852
  * Fails closed: no user, or an unresolvable reference, refuses.
778
853
  *
779
854
  * The references come from a walk of the whole parse tree
780
- * (`ClientClauseScreen.CollectStatementReads`). The statement is refused when that walk does not
855
+ * (`StrictFragmentValidator.ValidateReadOnlyStatement`). The statement is refused when that walk does not
781
856
  * account for every SELECT in the text, or when a FROM item names no table (a table function or
782
857
  * a row list).
783
858
  *
@@ -793,14 +868,7 @@ export class ResolverBase {
793
868
  }
794
869
  const dialect = this.dialectForProvider(provider);
795
870
  ClientClauseScreen.AssertNoIdentifierAliasNames(sqlText, dialect, label);
796
- const parser = new SQLParser(sqlText, dialect);
797
- if (!parser.IsValid || parser.HasWriteStatement || parser.StatementKind !== 'select') {
798
- throw new Error(`Invalid ${label}: not a safe read-only statement — refusing under uncertainty`);
799
- }
800
- if (this.astContainsWriteNode(parser.AST)) {
801
- throw new Error(`Invalid ${label}: write/DDL nested in a subquery is not permitted`);
802
- }
803
- const reads = ClientClauseScreen.CollectStatementReads(parser);
871
+ const reads = this.strictReadOnlyStatementReads(sqlText, dialect, label);
804
872
  ClientClauseScreen.AssertEverySelectAccountedFor(sqlText, dialect, reads, label);
805
873
  const allowed = this.entityBaseViewAllowList(provider);
806
874
  for (const relation of reads.Relations) {
@@ -857,9 +925,19 @@ export class ResolverBase {
857
925
  hasBackslashInQuotes(clause, dialect) {
858
926
  return LexSQL(clause, dialect).some((t) => (t.Kind === 'string' || t.Kind === 'identifier') && t.Text.includes('\\'));
859
927
  }
860
- /** Write/DDL node walk shared with the clause screen; see `ClientClauseScreen.AstContainsWriteNode`. */
861
- astContainsWriteNode(node) {
862
- return ClientClauseScreen.AstContainsWriteNode(node);
928
+ /**
929
+ * Parses a whole statement with the strict validator and returns what it reads; throws unless it
930
+ * is one plain read-only SELECT with no write or DDL at any depth.
931
+ */
932
+ strictReadOnlyStatementReads(sqlText, dialect, label) {
933
+ const result = StrictFragmentValidator.ValidateReadOnlyStatement(sqlText, dialect, { CollectReads: true });
934
+ if (result.Refusal === 'NestedWrite') {
935
+ throw new Error(`Invalid ${label}: write/DDL nested in a subquery is not permitted`);
936
+ }
937
+ if (!result.IsValid || !result.Reads) {
938
+ throw new Error(`Invalid ${label}: not a safe read-only statement — refusing under uncertainty`);
939
+ }
940
+ return result.Reads;
863
941
  }
864
942
  dialectForProvider(provider) {
865
943
  return ClientClauseScreen.DialectFor(provider);
@@ -873,7 +951,7 @@ export class ResolverBase {
873
951
  * - Field filtering at source (Fix #7)
874
952
  * - Improved error handling (Fix #9)
875
953
  */
876
- async RunViewGenericInternal(provider, viewInfo, extraFilter, orderBy, userSearchString, excludeUserViewRunID, overrideExcludeFilter, saveViewResults, fields, ignoreMaxRows, excludeDataFromAllPriorViewRuns, forceAuditLog, auditLogDescription, resultType, userPayload, maxRows, startRow, aggregates, afterKey, bypassCache, dataSource, includeBinaryFields) {
954
+ async RunViewGenericInternal(provider, viewInfo, extraFilter, orderBy, userSearchString, excludeUserViewRunID, overrideExcludeFilter, saveViewResults, fields, ignoreMaxRows, excludeDataFromAllPriorViewRuns, forceAuditLog, auditLogDescription, resultType, userPayload, maxRows, startRow, aggregates, afterKey, bypassCache, dataSource, includeBinaryFields, alternateBaseView, properties, skipTotalRowCount) {
877
955
  try {
878
956
  if (!viewInfo || !userPayload)
879
957
  return null;
@@ -919,6 +997,7 @@ export class ResolverBase {
919
997
  const result = await rv.RunView({
920
998
  ViewID: viewInfo.ID,
921
999
  ViewName: viewInfo.Name,
1000
+ ViewEntity: viewInfo.ID ? viewInfo : undefined,
922
1001
  EntityName: viewInfo.Entity,
923
1002
  ExtraFilter: extraFilter,
924
1003
  OrderBy: orderBy,
@@ -939,7 +1018,20 @@ export class ResolverBase {
939
1018
  BypassCache: bypassCache,
940
1019
  DataSource: dataSource,
941
1020
  IncludeBinaryFields: includeBinaryFields,
1021
+ SkipTotalRowCount: skipTotalRowCount,
1022
+ AlternateBaseView: alternateBaseView,
1023
+ Properties: properties,
1024
+ TimeZone: userPayload?.TimeZone,
1025
+ PreserveRestrictedApiCiphertext: true,
942
1026
  }, user);
1027
+ if (result && typeof result === 'object') {
1028
+ Object.defineProperty(result, '_resolvedViewEntity', {
1029
+ value: viewInfo,
1030
+ enumerable: false,
1031
+ configurable: true,
1032
+ writable: true,
1033
+ });
1034
+ }
943
1035
  // Log aggregate results for debugging
944
1036
  if (aggregates?.length) {
945
1037
  LogStatus(`[ResolverBase] RunView result aggregate info: entityName=${viewInfo.Entity}, hasAggregateResults=${!!result?.AggregateResults}, aggregateResultCount=${result?.AggregateResults?.length || 0}, aggregateExecutionTime=${result?.AggregateExecutionTime}, aggregateResults=${JSON.stringify(result?.AggregateResults)}`);
@@ -966,7 +1058,7 @@ export class ResolverBase {
966
1058
  if (result?.Success && result.Results?.length) {
967
1059
  result.Results = result.Results.map(r => mapper.MapFields({ ...r }));
968
1060
  // Filter encrypted fields before sending to API client
969
- await this.ArrayFilterEncryptedFieldsForAPI(viewInfo.Entity, result.Results, user);
1061
+ await this.ArrayFilterEncryptedFieldsForAPI(viewInfo.Entity, result.Results, user, md);
970
1062
  }
971
1063
  return result;
972
1064
  }
@@ -1050,6 +1142,7 @@ export class ResolverBase {
1050
1142
  runViewParams.push({
1051
1143
  ViewID: param.viewInfo.ID,
1052
1144
  ViewName: param.viewInfo.Name,
1145
+ ViewEntity: param.viewInfo.ID ? param.viewInfo : undefined,
1053
1146
  EntityName: param.viewInfo.Entity,
1054
1147
  ExtraFilter: param.extraFilter,
1055
1148
  OrderBy: param.orderBy,
@@ -1070,6 +1163,11 @@ export class ResolverBase {
1070
1163
  BypassCache: param.bypassCache,
1071
1164
  DataSource: param.dataSource,
1072
1165
  IncludeBinaryFields: param.includeBinaryFields,
1166
+ SkipTotalRowCount: param.skipTotalRowCount,
1167
+ AlternateBaseView: param.alternateBaseView,
1168
+ Properties: param.properties,
1169
+ TimeZone: param.userPayload?.TimeZone,
1170
+ PreserveRestrictedApiCiphertext: true,
1073
1171
  });
1074
1172
  }
1075
1173
  // Fix #4: Run views in a single batch through RunViews
@@ -1086,7 +1184,7 @@ export class ResolverBase {
1086
1184
  // Use the corresponding param's entity name
1087
1185
  const entityName = params[i]?.viewInfo?.Entity;
1088
1186
  if (entityName && contextUser) {
1089
- await this.ArrayFilterEncryptedFieldsForAPI(entityName, runViewResult.Results, contextUser);
1187
+ await this.ArrayFilterEncryptedFieldsForAPI(entityName, runViewResult.Results, contextUser, md);
1090
1188
  }
1091
1189
  }
1092
1190
  }
@@ -1205,8 +1303,9 @@ export class ResolverBase {
1205
1303
  * skip redundant re-fetches (it already handled the event locally).
1206
1304
  *
1207
1305
  * The row itself rides along ONLY for entities opted in via
1208
- * `cacheSettings.recordDataBroadcastEntities` — this event reaches every connected client
1209
- * unfiltered, so the row would otherwise be readable by sessions that could not read the record.
1306
+ * `cacheSettings.recordDataBroadcastEntities`, and never for one under field-level security —
1307
+ * this event reaches every connected client unprojected, so the row would otherwise be readable
1308
+ * by sessions that could not read the record or its restricted fields.
1210
1309
  */
1211
1310
  PublishCacheInvalidation(entityObject, action, userPayload) {
1212
1311
  const entityName = entityObject.EntityInfo.Name;
@@ -1217,32 +1316,61 @@ export class ResolverBase {
1217
1316
  sourceServerId: MJGlobal.Instance.ProcessUUID,
1218
1317
  timestamp: new Date(),
1219
1318
  originSessionId: userPayload?.sessionId || null,
1220
- recordData: action === 'save' && MayBroadcastRecordData(entityName)
1319
+ recordData: action === 'save' && MayBroadcastRecordData(entityName, entityObject.EntityInfo)
1320
+ // fls-guard: MayBroadcastRecordData refuses FLS entities and Record Changes
1221
1321
  ? JSON.stringify(entityObject.GetAll())
1222
1322
  : undefined,
1223
1323
  });
1224
1324
  }
1225
1325
  /**
1226
1326
  * Publishes a push-status update to the client on {@link PUSH_STATUS_UPDATES_TOPIC}, stamping the
1227
- * authenticated owner's user ID from `userPayload` so the subscription filter can bind delivery
1228
- * to identity (see B49 / `statusUpdatesFilter`). The ergonomic wrapper every resolver should use
1229
- * instead of calling `pubSub.publish` on the topic directly — it makes omitting identity
1230
- * impossible. Non-resolver publishers (services, the liveness heartbeat) call the shared
1231
- * `publishStatusUpdate()` function directly with an explicit `ownerUserId`.
1327
+ * owner key of `userPayload` (`StatusUpdateOwnerID`: the request's user, or the widget guest an
1328
+ * elevated payload stands in for) so the subscription filter can bind delivery to identity (see B49 /
1329
+ * `StatusUpdatesFilter`). The ergonomic wrapper every resolver should use instead of calling
1330
+ * `pubSub.publish` on the topic directly — it makes omitting identity impossible. Non-resolver
1331
+ * publishers (services, the liveness heartbeat) call the shared `PublishStatusUpdate()` function
1332
+ * directly with an explicit `ownerUserId`.
1232
1333
  */
1233
1334
  PublishStatusUpdate(pubSub, sessionId, message, userPayload) {
1234
1335
  PublishStatusUpdate(pubSub, {
1235
1336
  sessionId,
1236
- ownerUserId: userPayload?.userRecord?.ID ?? '',
1337
+ ownerUserId: StatusUpdateOwnerID(userPayload),
1237
1338
  message,
1238
1339
  });
1239
1340
  }
1240
1341
  ListenForEntityMessages(entityObject, pubSub, userPayload) {
1241
- // 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
1242
- // entity in the system. This is important because we don't want to have multiple listeners for the same entity as it could
1243
- // cause issues with multiple messages for the same event.
1342
+ // Maintain a single MJGlobal event listener per entity name so CloudEvents are emitted once
1343
+ // per event, while routing WebSocket status updates dynamically across all active user sessions
1344
+ // that have interacted with this entity type (instead of closing over only the first caller).
1244
1345
  const uniqueKey = entityObject.EntityInfo.Name;
1245
- if (!this.eventSubscriptions.has(uniqueKey)) {
1346
+ const now = Date.now();
1347
+ const isFreshSubscription = !this.eventSubscriptions.has(uniqueKey);
1348
+ let sessionsForEntity = isFreshSubscription ? undefined : this.activeEntitySessions.get(uniqueKey);
1349
+ if (!sessionsForEntity) {
1350
+ sessionsForEntity = new Map();
1351
+ this.activeEntitySessions.set(uniqueKey, sessionsForEntity);
1352
+ }
1353
+ else {
1354
+ // Opportunistically evict expired session entries and enforce a safe per-entity cap
1355
+ for (const [key, entry] of sessionsForEntity) {
1356
+ if (now - entry.lastSeenAt > ACTIVE_ENTITY_SESSION_TTL_MS) {
1357
+ sessionsForEntity.delete(key);
1358
+ }
1359
+ }
1360
+ if (sessionsForEntity.size >= ACTIVE_ENTITY_SESSION_MAX_PER_ENTITY) {
1361
+ const oldestKey = sessionsForEntity.keys().next().value;
1362
+ if (oldestKey !== undefined) {
1363
+ sessionsForEntity.delete(oldestKey);
1364
+ }
1365
+ }
1366
+ }
1367
+ if (userPayload) {
1368
+ const sessionKey = userPayload.sessionId
1369
+ ? `${userPayload.userRecord?.ID ?? ''}:${userPayload.sessionId}`
1370
+ : (userPayload.userRecord?.ID ?? '__anonymous__');
1371
+ sessionsForEntity.set(sessionKey, { pubSub, userPayload, lastSeenAt: now });
1372
+ }
1373
+ if (isFreshSubscription) {
1246
1374
  // 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
1247
1375
  LogDebug(`ResolverBase.ListenForEntityMessages: About to call MJGlobal.Instance.GetEventListener() to get the event listener subscription for ${uniqueKey}`);
1248
1376
  const theSub = MJGlobal.Instance.GetEventListener(false).subscribe(async (event) => {
@@ -1253,28 +1381,63 @@ export class ResolverBase {
1253
1381
  if (baseEntity?.EntityInfo?.Name !== uniqueKey) {
1254
1382
  return;
1255
1383
  }
1256
- const baseEntityValues = baseEntity ? baseEntity.GetAll() : null;
1257
- const eventToLog = { entityName: entityObject.EntityInfo.Name, baseEntity: baseEntityValues, event: event.event, eventCode: event.eventCode };
1384
+ const baseEntityValues = baseEntity ? this.EntityEventPayload(baseEntity, event.args instanceof BaseEntityEvent ? event.args.ChangedFieldNames : undefined) : null;
1385
+ const eventToLog = { entityName: uniqueKey, baseEntity: baseEntityValues, event: event.event, eventCode: event.eventCode };
1258
1386
  LogDebug(`ResolverBase.ListenForEntityMessages: Received Event from within MJGlobal.Instance.GetEventListener() callback. Will call EmitCloudEvent() next\nEvent data:\n${JSON.stringify(eventToLog)}`);
1259
1387
  await this.EmitCloudEvent(event);
1260
1388
  LogDebug(`ResolverBase.ListenForEntityMessages: EmitCloudEvent() completed successfully`);
1261
1389
  if (event.args && event.args instanceof BaseEntityEvent) {
1262
- const baseEntityEvent = event.args;
1263
- // message from our entity object, relay it to the client
1264
- LogDebug('ResolverBase.ListenForEntityMessages: About to publish PUSH_STATUS_UPDATES_TOPIC');
1265
- this.PublishStatusUpdate(pubSub, userPayload.sessionId, JSON.stringify({
1266
- status: 'OK',
1267
- type: 'EntityObjectStatusMessage',
1268
- entityName: baseEntityEvent.baseEntity.EntityInfo.Name,
1269
- primaryKey: baseEntityEvent.baseEntity.PrimaryKey,
1270
- message: event.args.payload,
1271
- }), userPayload);
1390
+ const activeSessions = this.activeEntitySessions.get(uniqueKey);
1391
+ if (activeSessions && activeSessions.size > 0) {
1392
+ const eventNow = Date.now();
1393
+ let serializedMessage;
1394
+ for (const [sKey, session] of activeSessions) {
1395
+ if (eventNow - session.lastSeenAt > ACTIVE_ENTITY_SESSION_TTL_MS) {
1396
+ activeSessions.delete(sKey);
1397
+ continue;
1398
+ }
1399
+ if (this.EntityEventBelongsToSession(event.args, session.userPayload)) {
1400
+ LogDebug('ResolverBase.ListenForEntityMessages: About to publish PUSH_STATUS_UPDATES_TOPIC');
1401
+ serializedMessage ??= this.EntityStatusMessage(event.args);
1402
+ this.PublishStatusUpdate(session.pubSub, session.userPayload.sessionId, serializedMessage, session.userPayload);
1403
+ }
1404
+ }
1405
+ }
1272
1406
  }
1273
1407
  }
1274
1408
  });
1275
1409
  this.eventSubscriptions.set(uniqueKey, theSub);
1276
1410
  }
1277
1411
  }
1412
+ /**
1413
+ * Whether an entity event may be relayed to the session whose call created the listener. The
1414
+ * listener is shared by every caller in the process for that entity, so an event raised by an
1415
+ * object loaded for another user (or for the system user) is never relayed to this session.
1416
+ */
1417
+ EntityEventBelongsToSession(event, userPayload) {
1418
+ const sessionUserId = userPayload?.userRecord?.ID;
1419
+ const objectUserId = event.baseEntity?.ContextCurrentUser?.ID;
1420
+ return Boolean(sessionUserId && objectUserId && UUIDsEqual(sessionUserId, objectUserId));
1421
+ }
1422
+ /**
1423
+ * The status message relayed for an entity event: the entity, key and event type. The event's
1424
+ * payload is included only when it cannot carry record values: never for a delete (its payload
1425
+ * is the whole deleted row), an entity under field-level security, or `MJ: Record Changes`.
1426
+ */
1427
+ EntityStatusMessage(event) {
1428
+ const entityInfo = event.baseEntity.EntityInfo;
1429
+ const mayCarryPayload = event.type !== 'delete'
1430
+ && !entityInfo.FieldLevelSecurityApplies
1431
+ && !RecordChangeFieldSecurityProjector.IsRecordChangesEntity(entityInfo);
1432
+ return JSON.stringify({
1433
+ status: 'OK',
1434
+ type: 'EntityObjectStatusMessage',
1435
+ entityName: entityInfo.Name,
1436
+ primaryKey: event.baseEntity.PrimaryKey,
1437
+ eventType: event.type,
1438
+ message: mayCarryPayload ? event.payload : undefined,
1439
+ });
1440
+ }
1278
1441
  /**
1279
1442
  * IS-A PROMOTION over the wire: an EXISTING parent record gaining a subtype — an Animal that is
1280
1443
  * now also a Dog. It arrives as the CHILD's create mutation carrying the existing parent's
@@ -1348,6 +1511,9 @@ export class ResolverBase {
1348
1511
  // IS-A promotion: bind the new child to its EXISTING parent row BEFORE the field
1349
1512
  // assignments, so values the client sent for parent fields land on the loaded parent
1350
1513
  // (as an update) instead of being wiped by the load.
1514
+ // Field-level security: refuse a key the caller may not read, or that names no field, before
1515
+ // anything is applied (the same gate as the update path).
1516
+ AssertClientInputWritableForUser(entityObject.EntityInfo, fieldsForSet, this.GetUserFromPayload(userPayload));
1351
1517
  await this.attachToExistingParentIfPromotion(entityObject, fieldsForSet);
1352
1518
  entityObject.SetMany(fieldsForSet);
1353
1519
  // Reconstruct the client-side restore context, if any, on this server
@@ -1361,6 +1527,7 @@ export class ResolverBase {
1361
1527
  // Cache invalidation is now handled globally by the MJGlobal listener in index.ts
1362
1528
  const contextUser = this.GetUserFromPayload(userPayload);
1363
1529
  // MapFieldNamesToCodeNames now handles encryption filtering as well
1530
+ // fls-guard: MapFieldNamesToCodeNames drops the fields the caller may not read
1364
1531
  return await this.MapFieldNamesToCodeNames(entityName, entityObject.GetAll(), contextUser);
1365
1532
  }
1366
1533
  // save failed, throw error with message
@@ -1398,12 +1565,11 @@ export class ResolverBase {
1398
1565
  }
1399
1566
  });
1400
1567
  // Field-level security: any value the client sends for a field it cannot READ is
1401
- // fabricated by construction (the field was stripped from every payload the client ever
1402
- // received), so strip such values before anything applies them. When any field is
1403
- // denied we also force the truth-load branch below — client OldValues for denied fields
1404
- // are equally fabricated, so hydrating from them (the no-DB-load path) would write
1405
- // fabricated state into the denied columns on save.
1406
- const hasDeniedReadFields = this.StripDeniedReadFieldsFromClientInput(entityInfo, userInfo, input, clientNewValues);
1568
+ // fabricated by construction (the field was absent from every payload the client ever
1569
+ // received), so the input is refused before anything applies it. When any field is
1570
+ // denied we also force the truth-load branch below, so the denied columns keep what the
1571
+ // database holds rather than anything derived from the client's OldValues.
1572
+ const hasDeniedReadFields = this.AssertClientInputFieldSecurity(entityInfo, userInfo, input, clientNewValues);
1407
1573
  // The audit-trail equivalent. Its `true` matters most on THIS entity: 'MJ: Record Changes'
1408
1574
  // has TrackRecordChanges off, so without it a client sending OldValues takes the branch below
1409
1575
  // that hydrates from those values — never loading what the database actually holds.
@@ -1459,6 +1625,7 @@ export class ResolverBase {
1459
1625
  await this.AfterUpdate(provider, input); // fire event
1460
1626
  // Cache invalidation is now handled globally by the MJGlobal listener in index.ts
1461
1627
  // MapFieldNamesToCodeNames now handles encryption filtering as well
1628
+ // fls-guard: MapFieldNamesToCodeNames drops the fields the caller may not read
1462
1629
  return await this.MapFieldNamesToCodeNames(entityName, entityObject.GetAll(), userInfo);
1463
1630
  }
1464
1631
  else {
@@ -1510,7 +1677,7 @@ export class ResolverBase {
1510
1677
  * writable fields in `OldValues___`, so {@link hasCompleteOldValues} returns `false` and forces the
1511
1678
  * database load (`InnerLoad`) so untouched columns retain their stored values.
1512
1679
  *
1513
- * `EnableFieldLevelSecurity` is the security-critical one, and it is NOT redundant with
1680
+ * `FieldLevelSecurityApplies` (own flag or an IS-A ancestor's) is the security-critical one, and it is NOT redundant with
1514
1681
  * `hasDeniedReadFields`. That flag reports READ denials, and the canonical FLS configuration is
1515
1682
  * Read Allow + Update Deny — which leaves it false. Such a caller would be hydrated from its own
1516
1683
  * `OldValues___`, and a value it supplies there for an update-denied field arrives through
@@ -1558,60 +1725,43 @@ export class ResolverBase {
1558
1725
  !this.hasCompleteOldValues(entityInfo, input.OldValues___) ||
1559
1726
  hasDeniedReadFields ||
1560
1727
  hasNarrowedAuditPayload ||
1561
- entityInfo.EnableFieldLevelSecurity ||
1728
+ entityInfo.FieldLevelSecurityApplies ||
1562
1729
  !user ||
1563
1730
  entityInfo.GetEffectiveRowFilterWhereClause(user, EntityPermissionType.Update, '').length > 0);
1564
1731
  }
1565
1732
  /**
1566
- * Field-level security guard for the update path.
1733
+ * Field-level security gate on the update path's client input.
1567
1734
  *
1568
- * Every denied-read field is stripped — new values AND OldValues. A field the user cannot
1569
- * read was absent from every payload that client ever received, so any value coming back for
1570
- * it is the transport's invention rather than user intent, and applying it would silently
1571
- * overwrite the real column. Stripping is what makes "load a record, edit an unrelated field,
1572
- * save" safe for a restricted user.
1735
+ * For a caller denied at least one field on the entity, refuses (with a `FieldSecurityError`)
1736
+ * any key in the new values or in `OldValues___` that names a field the caller cannot READ, or
1737
+ * that names no field at all, with the same message for both (see
1738
+ * `AssertClientInputWritableForUser`), so the refusal does not reveal which columns exist.
1573
1739
  *
1574
- * There is no split by update permission: Read is required for Update, so a user denied read
1575
- * is denied update too and denied-read ∩ denied-update is just denied-read.
1740
+ * Refused rather than dropped: a field the caller cannot read was absent from every payload the
1741
+ * client received, so a value for it can only be fabricated. A well-behaved client never sends
1742
+ * one (it omits fields it did not load), so ordinary traffic is unaffected.
1576
1743
  *
1577
- * Silent narrowing, not rejection — consistent with the output projection the client already
1578
- * experiences, and with the ambiguous-error rule (naming the field would confirm it exists
1579
- * and is restricted).
1744
+ * There is no split by update permission: Read is required for Update, so a user denied read is
1745
+ * denied update too.
1580
1746
  *
1581
- * Returns true when the user has a non-empty denied-read set on this entity. The caller
1582
- * must then hydrate the entity from the DATABASE (never from client OldValues), so denied
1583
- * fields hold true values that an omitted key leaves untouched.
1747
+ * Returns true when the user has a non-empty denied-read set on this entity. The caller must
1748
+ * then hydrate the entity from the DATABASE (never from client OldValues), so denied fields hold
1749
+ * true values the client input never touches.
1584
1750
  */
1585
- StripDeniedReadFieldsFromClientInput(entityInfo, userInfo, input, clientNewValues) {
1586
- if (!entityInfo.EnableFieldLevelSecurity || !userInfo) {
1751
+ AssertClientInputFieldSecurity(entityInfo, userInfo, input, clientNewValues) {
1752
+ if (!entityInfo.FieldLevelSecurityApplies || !userInfo) {
1587
1753
  return false;
1588
1754
  }
1589
- const deniedReadNames = entityInfo.GetDeniedReadFields(userInfo);
1590
- if (deniedReadNames.size === 0) {
1755
+ if (entityInfo.GetDeniedReadFields(userInfo).size === 0) {
1591
1756
  return false;
1592
1757
  }
1593
- // Input keys are entity CodeNames (post ReverseMapInputFieldNames); the denied set holds
1594
- // lowercased field Names — bridge via the field metadata once.
1595
- const deniedReadCodeNames = new Set();
1596
- for (const field of entityInfo.Fields) {
1597
- if (deniedReadNames.has(field.Name.trim().toLowerCase())) {
1598
- deniedReadCodeNames.add(field.CodeName.trim().toLowerCase());
1599
- }
1600
- }
1601
- const stripped = [];
1602
- for (const key of Object.keys(clientNewValues)) {
1603
- if (deniedReadCodeNames.has(key.trim().toLowerCase())) {
1604
- delete clientNewValues[key];
1605
- delete input[key];
1606
- stripped.push(key);
1607
- }
1608
- }
1758
+ AssertClientInputWritableForUser(entityInfo, clientNewValues, userInfo);
1609
1759
  if (Array.isArray(input.OldValues___)) {
1610
- input.OldValues___ = input.OldValues___.filter((item) => !deniedReadCodeNames.has(String(item.Key).trim().toLowerCase()));
1611
- }
1612
- if (stripped.length > 0) {
1613
- LogDebug(`[FieldSecurity] UpdateRecord on '${entityInfo.Name}' for user ${userInfo.Email}: ` +
1614
- `stripped client-sent value(s) for denied-read field(s) ${stripped.join(', ')}`);
1760
+ const oldValueKeys = {};
1761
+ for (const item of input.OldValues___) {
1762
+ oldValueKeys[String(item.Key)] = null;
1763
+ }
1764
+ AssertClientInputWritableForUser(entityInfo, oldValueKeys, userInfo);
1615
1765
  }
1616
1766
  return true;
1617
1767
  }
@@ -1728,132 +1878,268 @@ export class ResolverBase {
1728
1878
  }
1729
1879
  return entityInfo.Fields.find((f) => f.CodeName === key || f.Name === key);
1730
1880
  }
1731
- async TestAndSetClientOldValuesToDBValues(input, clientNewValues, entityObject, contextUser) {
1732
- // we have OldValues, so we need to compare them to the values we just loaded from the DB
1881
+ /**
1882
+ * Whether the API never gives a client this field's stored value: an encrypted field that is not
1883
+ * decrypted for the API reaches the client only as the sentinel or as fresh ciphertext, so the
1884
+ * client's old value for it can never be compared with the database value.
1885
+ */
1886
+ isFieldOpaqueToClient(field) {
1887
+ return Boolean(field?.Encrypt && !field.AllowDecryptInAPI);
1888
+ }
1889
+ /**
1890
+ * The client's new values without the opaque fields it only echoed back: the sentinel, or the same
1891
+ * value it received as the old value. Writing either would replace the stored secret.
1892
+ */
1893
+ withoutEchoedOpaqueValues(entityInfo, clientNewValues, clientOldValues) {
1894
+ const kept = {};
1895
+ for (const key of Object.keys(clientNewValues)) {
1896
+ const f = this.findEntityFieldByCodeNameOrName(entityInfo, key);
1897
+ const newValue = clientNewValues[key];
1898
+ const unchanged = key in clientOldValues && !this.areEntityFieldValuesDifferent(f, clientOldValues[key], newValue, false);
1899
+ const echoed = this.isFieldOpaqueToClient(f) && (newValue === ENCRYPTED_SENTINEL || unchanged);
1900
+ if (!echoed) {
1901
+ kept[key] = newValue;
1902
+ }
1903
+ }
1904
+ return kept;
1905
+ }
1906
+ /**
1907
+ * Copies concurrency-conflict diagnostic entries with every encrypted field's values replaced by
1908
+ * the sentinel, so the console, the error log and `MJ: Error Logs` never hold a secret.
1909
+ */
1910
+ redactEncryptedConflictValues(entityInfo, entries) {
1911
+ return entries.map((entry) => {
1912
+ if (!this.findEntityFieldByCodeNameOrName(entityInfo, entry.FieldName)?.Encrypt) {
1913
+ return entry;
1914
+ }
1915
+ const redacted = { FieldName: entry.FieldName };
1916
+ if ('ClientOldValue' in entry)
1917
+ redacted.ClientOldValue = ENCRYPTED_SENTINEL;
1918
+ if ('ClientNewValue' in entry)
1919
+ redacted.ClientNewValue = ENCRYPTED_SENTINEL;
1920
+ if ('DBValue' in entry)
1921
+ redacted.DBValue = ENCRYPTED_SENTINEL;
1922
+ return redacted;
1923
+ });
1924
+ }
1925
+ /**
1926
+ * Compares two typed entity field values for semantic inequality, handling `Date` epoch equality
1927
+ * (plus optional non-`datetimeoffset` timezone-hour-shift tolerance) and `null`/`undefined` normalization.
1928
+ */
1929
+ areEntityFieldValuesDifferent(field, left, right, allowTimezoneShiftWarning, fieldKeyForLog) {
1930
+ const normalizedLeft = left === undefined ? null : left;
1931
+ const normalizedRight = right === undefined ? null : right;
1932
+ if (normalizedLeft instanceof Date) {
1933
+ const rightDate = normalizedRight instanceof Date
1934
+ ? normalizedRight
1935
+ : field?.TSType === EntityFieldTSType.Date && normalizedRight !== null
1936
+ ? this.ClientOldValueToFieldValue(field, normalizedRight)
1937
+ : null;
1938
+ if (!(rightDate instanceof Date) || isNaN(rightDate.getTime())) {
1939
+ return true;
1940
+ }
1941
+ if (this.datesEqualAtColumnResolution(field, normalizedLeft, rightDate)) {
1942
+ return false;
1943
+ }
1944
+ const sqlType = field?.SQLFullType?.trim().toLowerCase() || '';
1945
+ if (sqlType !== 'datetimeoffset' && IsOnlyTimezoneShift(normalizedLeft, rightDate)) {
1946
+ if (allowTimezoneShiftWarning && fieldKeyForLog) {
1947
+ console.warn(`Timezone hour shift detected on field "${fieldKeyForLog}" (${sqlType || 'datetime'}). ` +
1948
+ `Client: ${normalizedLeft.toISOString()}, DB: ${rightDate.toISOString()}. ` +
1949
+ `Consider using datetimeoffset to avoid timezone ambiguity.`);
1950
+ }
1951
+ return false;
1952
+ }
1953
+ return true;
1954
+ }
1955
+ if (normalizedRight instanceof Date && field?.TSType === EntityFieldTSType.Date && normalizedLeft !== null) {
1956
+ const leftDate = this.ClientOldValueToFieldValue(field, normalizedLeft);
1957
+ if (leftDate instanceof Date && !isNaN(leftDate.getTime())) {
1958
+ return !this.datesEqualAtColumnResolution(field, leftDate, normalizedRight);
1959
+ }
1960
+ }
1961
+ if (typeof normalizedLeft === 'string' && typeof normalizedRight === 'string' && this.jsonStringsEquivalent(normalizedLeft, normalizedRight)) {
1962
+ return false;
1963
+ }
1964
+ return Object.is(normalizedLeft, normalizedRight) ? false : normalizedLeft !== normalizedRight;
1965
+ }
1966
+ /**
1967
+ * True when two instants would be stored as the same value in the field's column: SQL Server
1968
+ * `datetime` resolves to 1/300 s and `smalldatetime` to one minute; every other type keeps milliseconds.
1969
+ */
1970
+ datesEqualAtColumnResolution(field, left, right) {
1971
+ return Math.abs(left.getTime() - right.getTime()) < this.dateColumnResolutionMs(field);
1972
+ }
1973
+ dateColumnResolutionMs(field) {
1974
+ switch ((field?.Type ?? '').trim().toLowerCase()) {
1975
+ case 'datetime':
1976
+ return 1000 / 300;
1977
+ case 'smalldatetime':
1978
+ return 60_000;
1979
+ default:
1980
+ return 1;
1981
+ }
1982
+ }
1983
+ /**
1984
+ * True when both strings are JSON documents with the same content, ignoring whitespace and key order.
1985
+ */
1986
+ jsonStringsEquivalent(left, right) {
1987
+ const canonicalLeft = this.canonicalJSONDocument(left);
1988
+ return canonicalLeft !== null && canonicalLeft === this.canonicalJSONDocument(right);
1989
+ }
1990
+ /**
1991
+ * Canonical form of a JSON object or array string, or null when the text is not one.
1992
+ */
1993
+ canonicalJSONDocument(text) {
1994
+ const first = text.trimStart().charAt(0);
1995
+ if (first !== '{' && first !== '[') {
1996
+ return null;
1997
+ }
1998
+ try {
1999
+ return Canonicalize(JSON.parse(text));
2000
+ }
2001
+ catch {
2002
+ return null;
2003
+ }
2004
+ }
2005
+ async TestAndSetClientOldValuesToDBValues(input, rawClientNewValues, entityObject, contextUser) {
1733
2006
  const clientOldValues = {};
1734
- // for each item in the oldValues array, add it to the clientOldValues object
1735
- input.OldValues___.forEach((item) => {
1736
- // 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
2007
+ for (const item of input.OldValues___ ?? []) {
2008
+ // Transform string-serialized wire values to match the field's TypeScript type
1737
2009
  const field = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, item.Key);
1738
- const val = this.ClientOldValueToFieldValue(field, item.Value);
1739
- clientOldValues[item.Key] = val;
1740
- });
1741
- // clientOldValues now has all of the oldValues the CLIENT passed us. Now we need to build the same kind of object
1742
- // with the DB values
2010
+ clientOldValues[item.Key] = this.ClientOldValueToFieldValue(field, item.Value);
2011
+ }
2012
+ const clientNewValues = this.withoutEchoedOpaqueValues(entityObject.EntityInfo, rawClientNewValues, clientOldValues);
2013
+ // fls-guard: compared on the server only, never returned
1743
2014
  const dbValues = entityObject.GetAll();
1744
- // now we need to compare clientOldValues and dbValues and have a new array that has entries for any differences and have FieldName, clientOldValue and dbValue as properties
1745
2015
  const dbDifferences = [];
1746
- Object.keys(clientOldValues).forEach((key) => {
2016
+ const dbDiffByField = new Map();
2017
+ for (const key of Object.keys(clientOldValues)) {
1747
2018
  const f = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, key);
1748
- const dbVal = f && f.Name in dbValues ? dbValues[f.Name] : dbValues[key];
1749
- let different = false;
1750
- switch (typeof clientOldValues[key]) {
1751
- case 'number':
1752
- different = clientOldValues[key] !== dbVal;
1753
- break;
1754
- case 'boolean':
1755
- different = clientOldValues[key] !== dbVal;
1756
- break;
1757
- case 'object':
1758
- if (clientOldValues[key] instanceof Date) {
1759
- const clientDate = clientOldValues[key];
1760
- const dbDate = dbVal;
1761
- if (dbDate == null || !(dbDate instanceof Date)) {
1762
- different = true;
1763
- }
1764
- else if (clientDate.getTime() !== dbDate.getTime()) {
1765
- // Check if this is a datetime/datetime2 field with only a timezone hour shift
1766
- const sqlType = f?.SQLFullType?.trim().toLowerCase() || '';
1767
- if (sqlType !== 'datetimeoffset' && IsOnlyTimezoneShift(clientDate, dbDate)) {
1768
- console.warn(`Timezone hour shift detected on field "${key}" (${sqlType || 'datetime'}). ` +
1769
- `Client: ${clientDate.toISOString()}, DB: ${dbDate.toISOString()}. ` +
1770
- `Consider using datetimeoffset to avoid timezone ambiguity.`);
1771
- different = false; // Allow timezone shifts through for non-datetimeoffset fields
1772
- }
1773
- else {
1774
- different = true;
1775
- }
1776
- }
1777
- }
1778
- else if (clientOldValues[key] === null) {
1779
- different = dbVal !== null;
1780
- }
1781
- break;
1782
- default:
1783
- different = clientOldValues[key] !== dbVal;
1784
- break;
2019
+ if (this.isFieldOpaqueToClient(f)) {
2020
+ continue;
1785
2021
  }
2022
+ const dbVal = f && f.Name in dbValues ? dbValues[f.Name] : dbValues[key];
2023
+ const different = this.areEntityFieldValuesDifferent(f, clientOldValues[key], dbVal, true, key);
1786
2024
  if (different && f && !f.ReadOnly) {
1787
- // only include updateable fields
1788
- dbDifferences.push({
2025
+ const entry = {
1789
2026
  FieldName: key,
1790
2027
  ClientOldValue: clientOldValues[key],
1791
2028
  DBValue: dbVal,
1792
- });
2029
+ };
2030
+ dbDifferences.push(entry);
2031
+ dbDiffByField.set(key, entry);
1793
2032
  }
1794
- });
2033
+ }
2034
+ let fieldsToPreserveFromDB;
1795
2035
  if (dbDifferences.length > 0) {
1796
- // 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
1797
- // 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
1798
2036
  const clientDifferences = [];
1799
- Object.keys(clientOldValues).forEach((key) => {
2037
+ const clientDiffFieldNames = new Set();
2038
+ for (const key of Object.keys(clientOldValues)) {
1800
2039
  const f = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, key);
1801
- if (key in clientNewValues && clientOldValues[key] !== clientNewValues[key] && f && f.AllowUpdateAPI && !f.IsPrimaryKey) {
1802
- // only include updateable fields
2040
+ if (key in clientNewValues &&
2041
+ f &&
2042
+ f.AllowUpdateAPI &&
2043
+ !f.IsPrimaryKey &&
2044
+ this.areEntityFieldValuesDifferent(f, clientOldValues[key], clientNewValues[key], false)) {
1803
2045
  clientDifferences.push({
1804
2046
  FieldName: key,
1805
2047
  ClientOldValue: clientOldValues[key],
1806
2048
  ClientNewValue: clientNewValues[key],
1807
2049
  });
2050
+ clientDiffFieldNames.add(key);
1808
2051
  }
2052
+ }
2053
+ // True conflicting overlap: client modified a field whose authoritative DB value differs from BOTH
2054
+ // ClientOldValue AND ClientNewValue (idempotent concurrent writes to the same value converge cleanly).
2055
+ const overlap = clientDifferences.filter((cd) => {
2056
+ const dd = dbDiffByField.get(cd.FieldName);
2057
+ if (!dd) {
2058
+ return false;
2059
+ }
2060
+ const f = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, cd.FieldName);
2061
+ return this.areEntityFieldValuesDifferent(f, cd.ClientNewValue, dd.DBValue, true);
1809
2062
  });
1810
- // 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
1811
- // 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
1812
- const dbDiffFieldNames = new Set(dbDifferences.map((dd) => dd.FieldName));
1813
- const overlap = clientDifferences.filter((cd) => dbDiffFieldNames.has(cd.FieldName));
1814
2063
  if (overlap.length > 0) {
2064
+ const conflictingFields = overlap.map((o) => o.FieldName);
2065
+ const entityInfo = entityObject.EntityInfo;
2066
+ const shownClientDifferences = this.redactEncryptedConflictValues(entityInfo, clientDifferences);
2067
+ const shownDBDifferences = this.redactEncryptedConflictValues(entityInfo, dbDifferences);
2068
+ const shownOverlap = this.redactEncryptedConflictValues(entityInfo, overlap);
1815
2069
  const msg = {
1816
- Message: '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.',
1817
- ClientDifferences: clientDifferences,
1818
- DBDifferences: dbDifferences,
1819
- Overlap: overlap,
2070
+ Message: '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.',
2071
+ ClientDifferences: shownClientDifferences,
2072
+ DBDifferences: shownDBDifferences,
2073
+ Overlap: shownOverlap,
1820
2074
  };
1821
- // Log as warning to console and ErrorLog table instead of throwing error
1822
- console.warn('Entity save inconsistency detected but allowing save to continue:', JSON.stringify(msg));
1823
- LogError({
1824
- service: 'ResolverBase',
1825
- operation: 'TestAndSetClientOldValuesToDBValues',
1826
- error: `Entity save inconsistency detected: ${JSON.stringify(msg)}`,
1827
- details: {
2075
+ console.warn('Entity save concurrency conflict detected — rejecting conflicting update:', JSON.stringify(msg));
2076
+ LogErrorEx({
2077
+ message: `Entity save inconsistency detected: ${JSON.stringify(msg)}`,
2078
+ severity: 'warning',
2079
+ category: 'ResolverBase.TestAndSetClientOldValuesToDBValues',
2080
+ metadata: {
1828
2081
  entityName: entityObject.EntityInfo.Name,
1829
- clientDifferences: clientDifferences,
1830
- dbDifferences: dbDifferences,
1831
- overlap: overlap
1832
- }
2082
+ clientDifferences: shownClientDifferences,
2083
+ dbDifferences: shownDBDifferences,
2084
+ overlap: shownOverlap,
2085
+ },
1833
2086
  });
1834
- // Create ErrorLog record in the database — use the entity's bound provider so the
1835
- // ErrorLog write goes to the same connection as the entity that triggered it.
2087
+ // Persist SAVE_INCONSISTENCY ErrorLog record on the entity's bound provider before rejecting
1836
2088
  try {
1837
- const md = entityObject.ProviderToUse;
1838
- const errorLogEntity = await md.GetEntityObject('MJ: Error Logs', contextUser);
1839
- errorLogEntity.Code = 'ENTITY_SAVE_INCONSISTENCY';
1840
- errorLogEntity.Message = `Entity save inconsistency detected for ${entityObject.EntityInfo.Name}: ${JSON.stringify(msg)}`;
1841
- errorLogEntity.Status = 'Warning';
1842
- errorLogEntity.Category = 'Entity Save';
1843
- errorLogEntity.CreatedBy = contextUser.Email || contextUser.Name;
1844
- const saveResult = await errorLogEntity.Save();
1845
- if (!saveResult) {
1846
- console.error('Failed to save ErrorLog record');
2089
+ const hasEntityObjectFactory = (p) => Boolean(p && 'GetEntityObject' in p);
2090
+ const providerCandidate = entityObject.ProviderToUse;
2091
+ if (hasEntityObjectFactory(providerCandidate)) {
2092
+ const errorLogEntity = await providerCandidate.GetEntityObject('MJ: Error Logs', contextUser);
2093
+ // Code is NCHAR(20); a longer value is rejected at the database
2094
+ errorLogEntity.Code = 'SAVE_INCONSISTENCY';
2095
+ errorLogEntity.Message = `Entity save inconsistency detected for ${entityObject.EntityInfo.Name}: ${JSON.stringify(msg)}`;
2096
+ errorLogEntity.Status = 'Warning';
2097
+ errorLogEntity.Category = 'Entity Save';
2098
+ errorLogEntity.CreatedBy = contextUser.Email || contextUser.Name;
2099
+ const saveResult = await errorLogEntity.Save();
2100
+ if (!saveResult) {
2101
+ console.error(`Failed to save ErrorLog record for ${entityObject.EntityInfo.Name}: ${errorLogEntity.LatestResult?.CompleteMessage}`);
2102
+ }
1847
2103
  }
1848
2104
  }
1849
2105
  catch (errorLogError) {
1850
2106
  console.error('Error creating ErrorLog record:', errorLogError);
1851
2107
  }
2108
+ throw new GraphQLError(`Concurrent modification conflict on ${entityObject.EntityInfo.Name} for field(s): ${conflictingFields.join(', ')}. Reload the record and retry your changes.`, {
2109
+ extensions: {
2110
+ code: 'CONCURRENT_MODIFICATION_CONFLICT',
2111
+ entityName: entityObject.EntityInfo.Name,
2112
+ conflictingFields,
2113
+ },
2114
+ });
1852
2115
  }
2116
+ // Non-overlapping concurrent DB edits: preserve any field that changed in the database
2117
+ // when the client itself did NOT modify that field (prevents full-payload clients or forged
2118
+ // OldValues___ from clobbering concurrent DB updates on untouched columns).
2119
+ fieldsToPreserveFromDB = new Set();
2120
+ for (const dd of dbDifferences) {
2121
+ if (!clientDiffFieldNames.has(dd.FieldName)) {
2122
+ fieldsToPreserveFromDB.add(dd.FieldName);
2123
+ const f = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, dd.FieldName);
2124
+ if (f) {
2125
+ fieldsToPreserveFromDB.add(f.Name);
2126
+ fieldsToPreserveFromDB.add(f.CodeName);
2127
+ }
2128
+ }
2129
+ }
2130
+ }
2131
+ if (fieldsToPreserveFromDB && fieldsToPreserveFromDB.size > 0) {
2132
+ const mergedValues = {};
2133
+ for (const key of Object.keys(clientNewValues)) {
2134
+ if (!fieldsToPreserveFromDB.has(key)) {
2135
+ mergedValues[key] = clientNewValues[key];
2136
+ }
2137
+ }
2138
+ entityObject.SetMany(mergedValues);
2139
+ }
2140
+ else {
2141
+ entityObject.SetMany(clientNewValues);
1853
2142
  }
1854
- // If we get here that means we've not thrown an exception, so there is
1855
- // NO OVERLAP, so we can set the new values from the data provided from the client now...
1856
- entityObject.SetMany(clientNewValues);
1857
2143
  }
1858
2144
  async DeleteRecord(entityName, key, options, provider, userPayload, pubSub) {
1859
2145
  // Check API key scope authorization for entity delete operations
@@ -1872,7 +2158,10 @@ export class ResolverBase {
1872
2158
  extensions: { code: 'LOAD_ENTITY_ERROR', entityName },
1873
2159
  });
1874
2160
  }
1875
- const returnValue = entityObject.GetAll(); // grab the values before we delete so we can return last state before delete if we are successful.
2161
+ // Grab the values before we delete so we can return the last state if the delete succeeds,
2162
+ // projected for the caller in case the object holds a column they may not read.
2163
+ // fls-guard: projected by ProjectEntityValuesForUser
2164
+ const returnValue = ProjectEntityValuesForUser(provider, entityObject.EntityInfo, entityObject.GetAll(), this.GetUserFromPayload(userPayload));
1876
2165
  this.ListenForEntityMessages(entityObject, pubSub, userPayload);
1877
2166
  if (await entityObject.Delete(options)) {
1878
2167
  await this.AfterDelete(provider, key); // fire event