@memberjunction/server 6.2.0-edge.6 → 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 (407) hide show
  1. package/README.md +69 -0
  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 +31 -8
  14. package/dist/agentSessions/SessionJanitor.d.ts.map +1 -1
  15. package/dist/agentSessions/SessionJanitor.js +85 -41
  16. package/dist/agentSessions/SessionJanitor.js.map +1 -1
  17. package/dist/agentSessions/SessionManager.d.ts +14 -4
  18. package/dist/agentSessions/SessionManager.d.ts.map +1 -1
  19. package/dist/agentSessions/SessionManager.js +28 -4
  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/loginEmailResolution.d.ts +41 -0
  33. package/dist/auth/loginEmailResolution.d.ts.map +1 -0
  34. package/dist/auth/loginEmailResolution.js +47 -0
  35. package/dist/auth/loginEmailResolution.js.map +1 -0
  36. package/dist/auth/magicLink/MagicLinkKeys.d.ts +50 -2
  37. package/dist/auth/magicLink/MagicLinkKeys.d.ts.map +1 -1
  38. package/dist/auth/magicLink/MagicLinkKeys.js +129 -12
  39. package/dist/auth/magicLink/MagicLinkKeys.js.map +1 -1
  40. package/dist/auth/magicLink/MagicLinkRouter.d.ts +3 -0
  41. package/dist/auth/magicLink/MagicLinkRouter.d.ts.map +1 -1
  42. package/dist/auth/magicLink/MagicLinkRouter.js +23 -5
  43. package/dist/auth/magicLink/MagicLinkRouter.js.map +1 -1
  44. package/dist/auth/magicLink/MagicLinkService.d.ts +39 -5
  45. package/dist/auth/magicLink/MagicLinkService.d.ts.map +1 -1
  46. package/dist/auth/magicLink/MagicLinkService.js +221 -9
  47. package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
  48. package/dist/auth/magicLink/index.d.ts +1 -1
  49. package/dist/auth/magicLink/index.d.ts.map +1 -1
  50. package/dist/auth/magicLink/index.js +1 -1
  51. package/dist/auth/magicLink/index.js.map +1 -1
  52. package/dist/auth/magicLink/redeemLanding.d.ts +2 -2
  53. package/dist/auth/magicLink/redeemLanding.d.ts.map +1 -1
  54. package/dist/auth/magicLink/redeemLanding.js +7 -4
  55. package/dist/auth/magicLink/redeemLanding.js.map +1 -1
  56. package/dist/auth/magicLink/types.d.ts +27 -3
  57. package/dist/auth/magicLink/types.d.ts.map +1 -1
  58. package/dist/auth/magicLink/types.js +0 -4
  59. package/dist/auth/magicLink/types.js.map +1 -1
  60. package/dist/auth/newUsers.d.ts.map +1 -1
  61. package/dist/auth/newUsers.js +1 -0
  62. package/dist/auth/newUsers.js.map +1 -1
  63. package/dist/backgroundSweepConfigUnits.d.ts +231 -0
  64. package/dist/backgroundSweepConfigUnits.d.ts.map +1 -0
  65. package/dist/backgroundSweepConfigUnits.js +180 -0
  66. package/dist/backgroundSweepConfigUnits.js.map +1 -0
  67. package/dist/config.d.ts +906 -8
  68. package/dist/config.d.ts.map +1 -1
  69. package/dist/config.js +59 -0
  70. package/dist/config.js.map +1 -1
  71. package/dist/context.d.ts +2 -0
  72. package/dist/context.d.ts.map +1 -1
  73. package/dist/context.js +35 -4
  74. package/dist/context.js.map +1 -1
  75. package/dist/coreRoutes.d.ts +3 -1
  76. package/dist/coreRoutes.d.ts.map +1 -1
  77. package/dist/coreRoutes.js +6 -4
  78. package/dist/coreRoutes.js.map +1 -1
  79. package/dist/generated/graphql-schemas/__mj.d.ts +228 -0
  80. package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
  81. package/dist/generated/graphql-schemas/__mj.js +1163 -6
  82. package/dist/generated/graphql-schemas/__mj.js.map +1 -1
  83. package/dist/generic/OrphanedConversationDetailReconciler.d.ts +11 -0
  84. package/dist/generic/OrphanedConversationDetailReconciler.d.ts.map +1 -1
  85. package/dist/generic/OrphanedConversationDetailReconciler.js +27 -1
  86. package/dist/generic/OrphanedConversationDetailReconciler.js.map +1 -1
  87. package/dist/generic/ResolverBase.d.ts +50 -5
  88. package/dist/generic/ResolverBase.d.ts.map +1 -1
  89. package/dist/generic/ResolverBase.js +321 -132
  90. package/dist/generic/ResolverBase.js.map +1 -1
  91. package/dist/generic/RunViewResolver.d.ts +4 -0
  92. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  93. package/dist/generic/RunViewResolver.js +47 -7
  94. package/dist/generic/RunViewResolver.js.map +1 -1
  95. package/dist/index.d.ts +1 -0
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.js +157 -35
  98. package/dist/index.js.map +1 -1
  99. package/dist/integration/ConnectionPause.d.ts +121 -0
  100. package/dist/integration/ConnectionPause.d.ts.map +1 -0
  101. package/dist/integration/ConnectionPause.js +183 -0
  102. package/dist/integration/ConnectionPause.js.map +1 -0
  103. package/dist/integration/CustomColumnPromoter.d.ts +72 -6
  104. package/dist/integration/CustomColumnPromoter.d.ts.map +1 -1
  105. package/dist/integration/CustomColumnPromoter.js +87 -46
  106. package/dist/integration/CustomColumnPromoter.js.map +1 -1
  107. package/dist/integration/EntityMapLifecycle.d.ts +78 -4
  108. package/dist/integration/EntityMapLifecycle.d.ts.map +1 -1
  109. package/dist/integration/EntityMapLifecycle.js +199 -5
  110. package/dist/integration/EntityMapLifecycle.js.map +1 -1
  111. package/dist/integration/InactiveRowWarnings.d.ts +31 -0
  112. package/dist/integration/InactiveRowWarnings.d.ts.map +1 -1
  113. package/dist/integration/InactiveRowWarnings.js +29 -0
  114. package/dist/integration/InactiveRowWarnings.js.map +1 -1
  115. package/dist/integration/ObjectNamesByID.d.ts +28 -0
  116. package/dist/integration/ObjectNamesByID.d.ts.map +1 -0
  117. package/dist/integration/ObjectNamesByID.js +87 -0
  118. package/dist/integration/ObjectNamesByID.js.map +1 -0
  119. package/dist/integration/PostRestartIntrospect.d.ts +26 -0
  120. package/dist/integration/PostRestartIntrospect.d.ts.map +1 -0
  121. package/dist/integration/PostRestartIntrospect.js +27 -0
  122. package/dist/integration/PostRestartIntrospect.js.map +1 -0
  123. package/dist/integration/RSUPostRestartProgress.d.ts +176 -0
  124. package/dist/integration/RSUPostRestartProgress.d.ts.map +1 -0
  125. package/dist/integration/RSUPostRestartProgress.js +323 -0
  126. package/dist/integration/RSUPostRestartProgress.js.map +1 -0
  127. package/dist/integration/RSUProgressBridge.d.ts +41 -2
  128. package/dist/integration/RSUProgressBridge.d.ts.map +1 -1
  129. package/dist/integration/RSUProgressBridge.js +84 -1
  130. package/dist/integration/RSUProgressBridge.js.map +1 -1
  131. package/dist/integration/RunArtifactAuthorization.d.ts +39 -0
  132. package/dist/integration/RunArtifactAuthorization.d.ts.map +1 -0
  133. package/dist/integration/RunArtifactAuthorization.js +45 -0
  134. package/dist/integration/RunArtifactAuthorization.js.map +1 -0
  135. package/dist/integration/SchemaRefreshLaunch.d.ts +9 -0
  136. package/dist/integration/SchemaRefreshLaunch.d.ts.map +1 -1
  137. package/dist/integration/SchemaRefreshLaunch.js +3 -2
  138. package/dist/integration/SchemaRefreshLaunch.js.map +1 -1
  139. package/dist/integration/ScopedFieldsForBuild.d.ts +46 -0
  140. package/dist/integration/ScopedFieldsForBuild.d.ts.map +1 -0
  141. package/dist/integration/ScopedFieldsForBuild.js +91 -0
  142. package/dist/integration/ScopedFieldsForBuild.js.map +1 -0
  143. package/dist/integration/TargetConfigGuard.d.ts +40 -0
  144. package/dist/integration/TargetConfigGuard.d.ts.map +1 -0
  145. package/dist/integration/TargetConfigGuard.js +40 -0
  146. package/dist/integration/TargetConfigGuard.js.map +1 -0
  147. package/dist/realtimeSessions/ownedRealtimeSession.d.ts.map +1 -1
  148. package/dist/realtimeSessions/ownedRealtimeSession.js +2 -3
  149. package/dist/realtimeSessions/ownedRealtimeSession.js.map +1 -1
  150. package/dist/realtimeSessions/sessionAccess.d.ts +15 -0
  151. package/dist/realtimeSessions/sessionAccess.d.ts.map +1 -1
  152. package/dist/realtimeSessions/sessionAccess.js +22 -0
  153. package/dist/realtimeSessions/sessionAccess.js.map +1 -1
  154. package/dist/realtimeWidget/WidgetSessionService.d.ts.map +1 -1
  155. package/dist/realtimeWidget/WidgetSessionService.js +2 -0
  156. package/dist/realtimeWidget/WidgetSessionService.js.map +1 -1
  157. package/dist/realtimeWidget/visitorIdentity.d.ts.map +1 -1
  158. package/dist/realtimeWidget/visitorIdentity.js +1 -0
  159. package/dist/realtimeWidget/visitorIdentity.js.map +1 -1
  160. package/dist/realtimeWidget/widgetCore.d.ts.map +1 -1
  161. package/dist/realtimeWidget/widgetCore.js +4 -7
  162. package/dist/realtimeWidget/widgetCore.js.map +1 -1
  163. package/dist/realtimeWidget/widgetGuestElevation.js +1 -0
  164. package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
  165. package/dist/requestOrigin.d.ts +22 -0
  166. package/dist/requestOrigin.d.ts.map +1 -0
  167. package/dist/requestOrigin.js +60 -0
  168. package/dist/requestOrigin.js.map +1 -0
  169. package/dist/resolvers/AgentSessionResolver.d.ts.map +1 -1
  170. package/dist/resolvers/AgentSessionResolver.js +6 -2
  171. package/dist/resolvers/AgentSessionResolver.js.map +1 -1
  172. package/dist/resolvers/AutotagPipelineResolver.d.ts.map +1 -1
  173. package/dist/resolvers/AutotagPipelineResolver.js +2 -1
  174. package/dist/resolvers/AutotagPipelineResolver.js.map +1 -1
  175. package/dist/resolvers/ConversationRunEventsResolver.d.ts.map +1 -1
  176. package/dist/resolvers/ConversationRunEventsResolver.js +2 -0
  177. package/dist/resolvers/ConversationRunEventsResolver.js.map +1 -1
  178. package/dist/resolvers/ExecuteRemoteOperationResolver.d.ts +6 -0
  179. package/dist/resolvers/ExecuteRemoteOperationResolver.d.ts.map +1 -1
  180. package/dist/resolvers/ExecuteRemoteOperationResolver.js +30 -7
  181. package/dist/resolvers/ExecuteRemoteOperationResolver.js.map +1 -1
  182. package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
  183. package/dist/resolvers/GetDataResolver.js +34 -13
  184. package/dist/resolvers/GetDataResolver.js.map +1 -1
  185. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +401 -11
  186. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
  187. package/dist/resolvers/IntegrationDiscoveryResolver.js +2690 -1305
  188. package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
  189. package/dist/resolvers/MCPResolver.d.ts.map +1 -1
  190. package/dist/resolvers/MCPResolver.js +13 -2
  191. package/dist/resolvers/MCPResolver.js.map +1 -1
  192. package/dist/resolvers/QuerySystemUserResolver.d.ts +36 -0
  193. package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
  194. package/dist/resolvers/QuerySystemUserResolver.js +174 -87
  195. package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
  196. package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
  197. package/dist/resolvers/RealtimeBridgeResolver.js +1 -0
  198. package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
  199. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +6 -0
  200. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  201. package/dist/resolvers/RealtimeClientSessionResolver.js +36 -5
  202. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  203. package/dist/resolvers/RemoteBrowserActionResolver.d.ts +62 -10
  204. package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
  205. package/dist/resolvers/RemoteBrowserActionResolver.js +217 -34
  206. package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
  207. package/dist/resolvers/RunAIAgentResolver.d.ts +24 -8
  208. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  209. package/dist/resolvers/RunAIAgentResolver.js +185 -67
  210. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  211. package/dist/resolvers/RunAIPromptResolver.d.ts +5 -3
  212. package/dist/resolvers/RunAIPromptResolver.d.ts.map +1 -1
  213. package/dist/resolvers/RunAIPromptResolver.js +19 -18
  214. package/dist/resolvers/RunAIPromptResolver.js.map +1 -1
  215. package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
  216. package/dist/resolvers/RunTemplateResolver.js +3 -1
  217. package/dist/resolvers/RunTemplateResolver.js.map +1 -1
  218. package/dist/resolvers/meetingRecordingRegistration.d.ts.map +1 -1
  219. package/dist/resolvers/meetingRecordingRegistration.js +2 -0
  220. package/dist/resolvers/meetingRecordingRegistration.js.map +1 -1
  221. package/dist/rest/MediaAccessKeys.d.ts.map +1 -1
  222. package/dist/rest/MediaAccessKeys.js +6 -2
  223. package/dist/rest/MediaAccessKeys.js.map +1 -1
  224. package/dist/rest/OAuthCallbackHandler.d.ts.map +1 -1
  225. package/dist/rest/OAuthCallbackHandler.js +11 -5
  226. package/dist/rest/OAuthCallbackHandler.js.map +1 -1
  227. package/dist/rest/RealtimeRecordingHandler.d.ts +3 -3
  228. package/dist/rest/RealtimeRecordingHandler.d.ts.map +1 -1
  229. package/dist/rest/RealtimeRecordingHandler.js +2 -4
  230. package/dist/rest/RealtimeRecordingHandler.js.map +1 -1
  231. package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
  232. package/dist/rest/ViewOperationsHandler.js +2 -1
  233. package/dist/rest/ViewOperationsHandler.js.map +1 -1
  234. package/dist/rest/setupRESTEndpoints.d.ts +3 -1
  235. package/dist/rest/setupRESTEndpoints.d.ts.map +1 -1
  236. package/dist/rest/setupRESTEndpoints.js +11 -2
  237. package/dist/rest/setupRESTEndpoints.js.map +1 -1
  238. package/dist/services/DurableEntityActionTaskSubmitter.d.ts.map +1 -1
  239. package/dist/services/DurableEntityActionTaskSubmitter.js +6 -1
  240. package/dist/services/DurableEntityActionTaskSubmitter.js.map +1 -1
  241. package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
  242. package/dist/services/TaskGraphActionRunner.js +2 -26
  243. package/dist/services/TaskGraphActionRunner.js.map +1 -1
  244. package/dist/services/TaskGraphAgentRunner.d.ts +6 -0
  245. package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -1
  246. package/dist/services/TaskGraphAgentRunner.js +38 -0
  247. package/dist/services/TaskGraphAgentRunner.js.map +1 -1
  248. package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
  249. package/dist/services/TaskGraphContinuationDeliverer.js +1 -0
  250. package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
  251. package/dist/services/WorkQueueHostService.d.ts +2 -1
  252. package/dist/services/WorkQueueHostService.d.ts.map +1 -1
  253. package/dist/services/WorkQueueHostService.js +19 -4
  254. package/dist/services/WorkQueueHostService.js.map +1 -1
  255. package/dist/services/workQueueConfig.d.ts +2 -2
  256. package/dist/services/workQueueConfig.js +5 -5
  257. package/dist/services/workQueueConfig.js.map +1 -1
  258. package/dist/sharedCache.js +1 -1
  259. package/dist/sharedCache.js.map +1 -1
  260. package/dist/types.d.ts +17 -0
  261. package/dist/types.d.ts.map +1 -1
  262. package/dist/types.js.map +1 -1
  263. package/package.json +98 -98
  264. package/src/__tests__/APIKeyScopeAuth.test.ts +4 -8
  265. package/src/__tests__/ActiveOperations.test.ts +68 -0
  266. package/src/__tests__/AgentSessionResolver.ownership.test.ts +13 -0
  267. package/src/__tests__/AssertTargetConfigs.test.ts +75 -0
  268. package/src/__tests__/BatchApplyParity.test.ts +109 -0
  269. package/src/__tests__/BridgeHostInstance.test.ts +42 -0
  270. package/src/__tests__/CollectInactiveRowWarnings.test.ts +115 -0
  271. package/src/__tests__/ConnectionPause.test.ts +192 -0
  272. package/src/__tests__/ConnectionPauseResolver.test.ts +166 -0
  273. package/src/__tests__/ConnectionPauseSave.test.ts +143 -0
  274. package/src/__tests__/CustomColumnPromoterCompletion.test.ts +142 -0
  275. package/src/__tests__/DeleteConnectionRemovesPerConnectionCatalog.test.ts +51 -0
  276. package/src/__tests__/DetachedDiscoveryStart.test.ts +65 -0
  277. package/src/__tests__/DurableEntityActionTaskSubmitter.test.ts +73 -0
  278. package/src/__tests__/EntityMapLifecycle.rekey.test.ts +252 -0
  279. package/src/__tests__/ExecuteRemoteOperationResolver.test.ts +213 -0
  280. package/src/__tests__/HandoffOfferExpiry.test.ts +40 -0
  281. package/src/__tests__/IntegrationRunArtifactAuthorization.test.ts +126 -0
  282. package/src/__tests__/MagicLinkKeys.test.ts +102 -0
  283. package/src/__tests__/MediaAccessKeys.test.ts +57 -10
  284. package/src/__tests__/NewArrivalsDefaultDisabled.test.ts +50 -0
  285. package/src/__tests__/ObjectNamesByID.test.ts +176 -0
  286. package/src/__tests__/PostRestartMapsInCatalogScope.test.ts +146 -0
  287. package/src/__tests__/QuerySystemUserResolver.childRows.test.ts +80 -0
  288. package/src/__tests__/QuerySystemUserResolver.collision.test.ts +200 -0
  289. package/src/__tests__/QuerySystemUserResolver.permissions.test.ts +132 -0
  290. package/src/__tests__/RSUPostRestartProgress.test.ts +343 -0
  291. package/src/__tests__/RSUProgressBridge.test.ts +188 -5
  292. package/src/__tests__/RSUStartupSweepWiring.test.ts +30 -0
  293. package/src/__tests__/ReactivateRefreshMessage.test.ts +10 -3
  294. package/src/__tests__/RealtimeClientSessionResolver.test.ts +22 -0
  295. package/src/__tests__/{RefreshAdoptsNewColumns.test.ts → RefreshReportsNewColumns.test.ts} +25 -16
  296. package/src/__tests__/RemoteBrowserActionRecovery.test.ts +287 -0
  297. package/src/__tests__/RemoteBrowserActionResolver.goalRunLink.test.ts +3 -0
  298. package/src/__tests__/RemoteBrowserActionResolver.ownership.test.ts +6 -4
  299. package/src/__tests__/RemoteBrowserAudioStream.test.ts +3 -0
  300. package/src/__tests__/RemoteBrowserSnapshot.test.ts +3 -0
  301. package/src/__tests__/ResolverBase.clientOldValues.test.ts +323 -26
  302. package/src/__tests__/ResolverBase.encryptedApiZeroReencrypt.test.ts +307 -0
  303. package/src/__tests__/ResolverBase.entityMessageRelay.test.ts +53 -2
  304. package/src/__tests__/ResolverBase.includeBinaryFields.test.ts +60 -0
  305. package/src/__tests__/RunAIAgentResolver.branch.test.ts +4 -0
  306. package/src/__tests__/RunAIAgentResolver.consultPredecessor.test.ts +289 -0
  307. package/src/__tests__/RunAIAgentResolver.handoff.test.ts +649 -0
  308. package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +2 -0
  309. package/src/__tests__/RunAIAgentResolver.runDataGuard.test.ts +2 -0
  310. package/src/__tests__/RunAIAgentResolver.stoppedPredecessor.test.ts +87 -12
  311. package/src/__tests__/SchemaEvolutionCustomColumns.test.ts +119 -0
  312. package/src/__tests__/SchemaRefreshLaunch.test.ts +24 -2
  313. package/src/__tests__/ScopedFieldsForBuild.test.ts +192 -0
  314. package/src/__tests__/SessionJanitor.test.ts +195 -4
  315. package/src/__tests__/SessionManager.test.ts +69 -0
  316. package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +2 -2
  317. package/src/__tests__/TaskGraphAgentRunner.test.ts +272 -16
  318. package/src/__tests__/ViewOperationsHandler.test.ts +2 -5
  319. package/src/__tests__/WorkQueueHostService.test.ts +25 -7
  320. package/src/__tests__/backgroundSweepConfigUnits.test.ts +153 -0
  321. package/src/__tests__/contextRequestOrigin.test.ts +105 -0
  322. package/src/__tests__/conversationBrowsers.test.ts +62 -0
  323. package/src/__tests__/magicLinkExtensionCapabilities.test.ts +413 -0
  324. package/src/__tests__/orphanedConversationDetailReconciler.start.test.ts +82 -0
  325. package/src/__tests__/ownedRealtimeSession.test.ts +14 -0
  326. package/src/__tests__/realtimeRecordingHandler.test.ts +7 -2
  327. package/src/__tests__/realtimeSessions/sessionAccess.test.ts +79 -1
  328. package/src/__tests__/requestOrigin.test.ts +104 -0
  329. package/src/__tests__/securityHardeningPr6034.test.ts +120 -0
  330. package/src/__tests__/sessionDeadline.test.ts +51 -0
  331. package/src/__tests__/sharedCache.test.ts +11 -0
  332. package/src/__tests__/unifiedAuth.test.ts +73 -1
  333. package/src/agentSessions/BridgeHostInstance.ts +20 -5
  334. package/src/agentSessions/HandoffOfferExpiry.ts +18 -0
  335. package/src/agentSessions/ReturningVisitorRecap.ts +1 -0
  336. package/src/agentSessions/SessionJanitor.ts +92 -42
  337. package/src/agentSessions/SessionManager.ts +38 -8
  338. package/src/agentSessions/conversationBrowsers.ts +143 -0
  339. package/src/agentSessions/sessionDeadline.ts +81 -0
  340. package/src/auth/APIKeyScopeAuth.ts +5 -4
  341. package/src/auth/__tests__/loginEmailResolution.test.ts +67 -0
  342. package/src/auth/loginEmailResolution.ts +67 -0
  343. package/src/auth/magicLink/MagicLinkKeys.ts +146 -14
  344. package/src/auth/magicLink/MagicLinkRouter.ts +33 -6
  345. package/src/auth/magicLink/MagicLinkService.ts +250 -10
  346. package/src/auth/magicLink/index.ts +1 -1
  347. package/src/auth/magicLink/redeemLanding.ts +7 -4
  348. package/src/auth/magicLink/types.ts +28 -3
  349. package/src/auth/newUsers.ts +1 -0
  350. package/src/backgroundSweepConfigUnits.ts +213 -0
  351. package/src/config.ts +69 -0
  352. package/src/context.ts +39 -6
  353. package/src/coreRoutes.ts +14 -7
  354. package/src/generated/graphql-schemas/__mj.ts +822 -7
  355. package/src/generic/OrphanedConversationDetailReconciler.ts +32 -1
  356. package/src/generic/ResolverBase.ts +414 -143
  357. package/src/generic/RunViewResolver.ts +96 -49
  358. package/src/index.ts +201 -35
  359. package/src/integration/ConnectionPause.ts +222 -0
  360. package/src/integration/CustomColumnPromoter.ts +123 -53
  361. package/src/integration/EntityMapLifecycle.ts +229 -6
  362. package/src/integration/InactiveRowWarnings.ts +50 -0
  363. package/src/integration/ObjectNamesByID.ts +97 -0
  364. package/src/integration/PostRestartIntrospect.ts +49 -0
  365. package/src/integration/RSUPostRestartProgress.ts +359 -0
  366. package/src/integration/RSUProgressBridge.ts +85 -2
  367. package/src/integration/RunArtifactAuthorization.ts +65 -0
  368. package/src/integration/SchemaRefreshLaunch.ts +12 -2
  369. package/src/integration/ScopedFieldsForBuild.ts +113 -0
  370. package/src/integration/TargetConfigGuard.ts +71 -0
  371. package/src/realtimeSessions/ownedRealtimeSession.ts +2 -6
  372. package/src/realtimeSessions/sessionAccess.ts +30 -0
  373. package/src/realtimeWidget/WidgetSessionService.ts +2 -0
  374. package/src/realtimeWidget/visitorIdentity.ts +1 -0
  375. package/src/realtimeWidget/widgetCore.ts +4 -8
  376. package/src/realtimeWidget/widgetGuestElevation.ts +1 -0
  377. package/src/requestOrigin.ts +64 -0
  378. package/src/resolvers/AgentSessionResolver.ts +6 -2
  379. package/src/resolvers/AutotagPipelineResolver.ts +2 -1
  380. package/src/resolvers/ConversationRunEventsResolver.ts +2 -0
  381. package/src/resolvers/ExecuteRemoteOperationResolver.ts +41 -9
  382. package/src/resolvers/GetDataResolver.ts +35 -13
  383. package/src/resolvers/IntegrationDiscoveryResolver.ts +1522 -203
  384. package/src/resolvers/MCPResolver.ts +15 -2
  385. package/src/resolvers/QuerySystemUserResolver.ts +197 -98
  386. package/src/resolvers/RealtimeBridgeResolver.ts +1 -0
  387. package/src/resolvers/RealtimeClientSessionResolver.ts +40 -7
  388. package/src/resolvers/RemoteBrowserActionResolver.ts +242 -32
  389. package/src/resolvers/RunAIAgentResolver.ts +265 -86
  390. package/src/resolvers/RunAIPromptResolver.ts +18 -18
  391. package/src/resolvers/RunTemplateResolver.ts +3 -1
  392. package/src/resolvers/__tests__/GetDataResolver.tokens.test.ts +52 -0
  393. package/src/resolvers/meetingRecordingRegistration.ts +2 -0
  394. package/src/rest/MediaAccessKeys.ts +6 -2
  395. package/src/rest/OAuthCallbackHandler.ts +13 -5
  396. package/src/rest/RealtimeRecordingHandler.ts +5 -9
  397. package/src/rest/ViewOperationsHandler.ts +2 -1
  398. package/src/rest/setupRESTEndpoints.ts +11 -2
  399. package/src/services/DurableEntityActionTaskSubmitter.ts +10 -2
  400. package/src/services/TaskGraphActionRunner.ts +2 -23
  401. package/src/services/TaskGraphAgentRunner.ts +43 -0
  402. package/src/services/TaskGraphContinuationDeliverer.ts +1 -0
  403. package/src/services/WorkQueueHostService.ts +26 -4
  404. package/src/services/workQueueConfig.ts +5 -5
  405. package/src/sharedCache.ts +1 -1
  406. package/src/types.ts +18 -0
  407. package/src/services/TaskOrchestration-Integration.md +0 -188
@@ -49,8 +49,8 @@ import { httpTransport, CloudEvent, emitterFor } from 'cloudevents';
49
49
  import { RunViewGenericParams, UserPayload } from '../types.js';
50
50
  import { RunDynamicViewInput, RunViewByIDInput, RunViewByNameInput } from './RunViewResolver.js';
51
51
  import { DeleteOptionsInput } from './DeleteOptionsInput.js';
52
- import { MJEvent, MJEventType, MJGlobal, UUIDsEqual, ENCRYPTED_SENTINEL, ReplaceByteArraysWithBase64, EscapeSQLString, IsValueEncrypted, IsOnlyTimezoneShift } from '@memberjunction/global';
53
- import { SQLParser, IsBalancedSQLFragment, LexSQL } from '@memberjunction/sql-parser';
52
+ import { MJEvent, MJEventType, MJGlobal, UUIDsEqual, ENCRYPTED_SENTINEL, ReplaceByteArraysWithBase64, EscapeSQLString, IsValueEncrypted, IsOnlyTimezoneShift, Canonicalize } from '@memberjunction/global';
53
+ import { SQLParser, IsBalancedSQLFragment, LexSQL, StrictFragmentValidator, type StrictStatementReads } from '@memberjunction/sql-parser';
54
54
  import type { SQLParserDialect } from '@memberjunction/sql-dialect';
55
55
  import { EncryptionEngine, IsEncryptionKeyUnavailable, ReportEncryptionKeyUnavailable } from '@memberjunction/encryption';
56
56
  import { PUSH_STATUS_UPDATES_TOPIC, PublishStatusUpdate, StatusUpdateOwnerID } from './PushStatusResolver.js';
@@ -77,11 +77,29 @@ const ENTITIES_THAT_ALWAYS_LOAD_STORED_ROW: ReadonlySet<string> = new Set([
77
77
  'mj: ai agent runs',
78
78
  ]);
79
79
 
80
+ /** One field's values in a concurrency-conflict diagnostic. */
81
+ interface ConflictDiagnosticEntry<TValue> {
82
+ FieldName: string;
83
+ ClientOldValue?: TValue;
84
+ ClientNewValue?: TValue;
85
+ DBValue?: TValue;
86
+ }
87
+
88
+ interface ActiveEntitySessionListener {
89
+ pubSub: PubSubEngine;
90
+ userPayload: UserPayload;
91
+ lastSeenAt: number;
92
+ }
93
+
94
+ const ACTIVE_ENTITY_SESSION_TTL_MS = 24 * 60 * 60 * 1000;
95
+ const ACTIVE_ENTITY_SESSION_MAX_PER_ENTITY = 2048;
96
+
80
97
  export class ResolverBase {
81
98
  private static _emit = process.env.CLOUDEVENTS_HTTP_TRANSPORT ? emitterFor(httpTransport(process.env.CLOUDEVENTS_HTTP_TRANSPORT)) : null;
82
99
  private static _cloudeventsHeaders = process.env.CLOUDEVENTS_HTTP_HEADERS ? JSON.parse(process.env.CLOUDEVENTS_HTTP_HEADERS) : {};
83
100
 
84
101
  private static _eventSubscriptionKey: string = '___MJServer___ResolverBase___EventSubscriptions';
102
+ private static _activeEntitySessionsKey: string = '___MJServer___ResolverBase___ActiveEntitySessions';
85
103
  private get eventSubscriptions(): Map<string, Subscription> {
86
104
  // here we use the global object store instead of a static member becuase in some cases based on import code paths/bundling/etc, the static member
87
105
  // could actually be duplicated and we'd end up with multiple instances of the same map, which would be bad.
@@ -90,7 +108,15 @@ export class ResolverBase {
90
108
  LogDebug(`>>>>> MJServer.ResolverBase.EventSubscriptions: Creating new Map - this should only happen once per server instance <<<<<<`);
91
109
  g[ResolverBase._eventSubscriptionKey] = new Map<string, Subscription>();
92
110
  }
93
- return g[ResolverBase._eventSubscriptionKey];
111
+ return g[ResolverBase._eventSubscriptionKey] as Map<string, Subscription>;
112
+ }
113
+
114
+ private get activeEntitySessions(): Map<string, Map<string, ActiveEntitySessionListener>> {
115
+ const g = MJGlobal.Instance.GetGlobalObjectStore();
116
+ if (!g[ResolverBase._activeEntitySessionsKey]) {
117
+ g[ResolverBase._activeEntitySessionsKey] = new Map<string, Map<string, ActiveEntitySessionListener>>();
118
+ }
119
+ return g[ResolverBase._activeEntitySessionsKey] as Map<string, Map<string, ActiveEntitySessionListener>>;
94
120
  }
95
121
 
96
122
  /**
@@ -438,30 +464,29 @@ export class ResolverBase {
438
464
  if (field.AllowDecryptInAPI) continue;
439
465
 
440
466
  // AllowDecryptInAPI is false - we need to filter the value
467
+ if (!field.SendEncryptedValue) {
468
+ // SendEncryptedValue is false - replace with sentinel immediately without touching EncryptionEngine
469
+ dataObject[fieldName] = ENCRYPTED_SENTINEL;
470
+ continue;
471
+ }
472
+
441
473
  const engine = EncryptionEngine.Instance;
442
474
  await engine.Config(false, contextUser);
443
475
  const keyMarker = field.EncryptionKeyID ? engine.GetKeyByID(field.EncryptionKeyID)?.Marker : undefined;
444
- if (field.SendEncryptedValue) {
445
- // Re-encrypt the value before sending
446
- // Only re-encrypt if it's not already encrypted (data provider decrypted it)
447
- if (typeof value === 'string' && !IsValueEncrypted(value, keyMarker)) {
448
- try {
449
- const encryptedValue = await engine.Encrypt(
450
- value,
451
- field.EncryptionKeyID as string,
452
- contextUser
453
- );
454
- dataObject[fieldName] = encryptedValue;
455
- } catch (err) {
456
- // If re-encryption fails, use sentinel for safety
457
- LogError(`Failed to re-encrypt field ${fieldName} for API response: ${err}`);
458
- dataObject[fieldName] = ENCRYPTED_SENTINEL;
459
- }
476
+ // Only encrypt if the value is not already encrypted (PreserveRestrictedApiCiphertext keeps DB/cache ciphertext intact)
477
+ if (typeof value === 'string' && !IsValueEncrypted(value, keyMarker)) {
478
+ try {
479
+ const encryptedValue = await engine.Encrypt(
480
+ value,
481
+ field.EncryptionKeyID as string,
482
+ contextUser
483
+ );
484
+ dataObject[fieldName] = encryptedValue;
485
+ } catch (err) {
486
+ // If re-encryption fails, use sentinel for safety
487
+ LogError(`Failed to re-encrypt field ${fieldName} for API response: ${err}`);
488
+ dataObject[fieldName] = ENCRYPTED_SENTINEL;
460
489
  }
461
- // If already encrypted (shouldn't happen normally), keep as-is
462
- } else {
463
- // SendEncryptedValue is false - replace with sentinel
464
- dataObject[fieldName] = ENCRYPTED_SENTINEL;
465
490
  }
466
491
  }
467
492
 
@@ -661,7 +686,8 @@ export class ResolverBase {
661
686
  viewInput.DataSource,
662
687
  viewInput.IncludeBinaryFields,
663
688
  viewInput.AlternateBaseView,
664
- viewInput.Properties
689
+ viewInput.Properties,
690
+ viewInput.SkipTotalRowCount
665
691
  );
666
692
  }
667
693
  else {
@@ -710,7 +736,8 @@ export class ResolverBase {
710
736
  viewInput.DataSource,
711
737
  viewInput.IncludeBinaryFields,
712
738
  viewInput.AlternateBaseView,
713
- viewInput.Properties
739
+ viewInput.Properties,
740
+ viewInput.SkipTotalRowCount
714
741
  );
715
742
  } catch (err) {
716
743
  console.log(err);
@@ -759,7 +786,9 @@ export class ResolverBase {
759
786
  viewInput.BypassCache,
760
787
  viewInput.DataSource,
761
788
  viewInput.IncludeBinaryFields,
762
- viewInput.AlternateBaseView
789
+ viewInput.AlternateBaseView,
790
+ undefined,
791
+ viewInput.SkipTotalRowCount
763
792
  );
764
793
  } catch (err) {
765
794
  console.log(err);
@@ -834,6 +863,7 @@ export class ResolverBase {
834
863
  bypassCache: viewInput.BypassCache,
835
864
  dataSource: viewInput.DataSource,
836
865
  includeBinaryFields: viewInput.IncludeBinaryFields,
866
+ skipTotalRowCount: viewInput.SkipTotalRowCount,
837
867
  alternateBaseView: viewInput.AlternateBaseView,
838
868
  properties: viewInput.Properties,
839
869
  });
@@ -1101,7 +1131,7 @@ export class ResolverBase {
1101
1131
  * Fails closed: no user, or an unresolvable reference, refuses.
1102
1132
  *
1103
1133
  * The references come from a walk of the whole parse tree
1104
- * (`ClientClauseScreen.CollectStatementReads`). The statement is refused when that walk does not
1134
+ * (`StrictFragmentValidator.ValidateReadOnlyStatement`). The statement is refused when that walk does not
1105
1135
  * account for every SELECT in the text, or when a FROM item names no table (a table function or
1106
1136
  * a row list).
1107
1137
  *
@@ -1122,14 +1152,7 @@ export class ResolverBase {
1122
1152
  }
1123
1153
  const dialect = this.dialectForProvider(provider);
1124
1154
  ClientClauseScreen.AssertNoIdentifierAliasNames(sqlText, dialect, label);
1125
- const parser = new SQLParser(sqlText, dialect);
1126
- if (!parser.IsValid || parser.HasWriteStatement || parser.StatementKind !== 'select') {
1127
- throw new Error(`Invalid ${label}: not a safe read-only statement — refusing under uncertainty`);
1128
- }
1129
- if (this.astContainsWriteNode(parser.AST)) {
1130
- throw new Error(`Invalid ${label}: write/DDL nested in a subquery is not permitted`);
1131
- }
1132
- const reads = ClientClauseScreen.CollectStatementReads(parser);
1155
+ const reads = this.strictReadOnlyStatementReads(sqlText, dialect, label);
1133
1156
  ClientClauseScreen.AssertEverySelectAccountedFor(sqlText, dialect, reads, label);
1134
1157
  const allowed = this.entityBaseViewAllowList(provider);
1135
1158
  for (const relation of reads.Relations) {
@@ -1198,9 +1221,19 @@ export class ResolverBase {
1198
1221
  return LexSQL(clause, dialect).some((t) => (t.Kind === 'string' || t.Kind === 'identifier') && t.Text.includes('\\'));
1199
1222
  }
1200
1223
 
1201
- /** Write/DDL node walk shared with the clause screen; see `ClientClauseScreen.AstContainsWriteNode`. */
1202
- private astContainsWriteNode(node: unknown): boolean {
1203
- return ClientClauseScreen.AstContainsWriteNode(node);
1224
+ /**
1225
+ * Parses a whole statement with the strict validator and returns what it reads; throws unless it
1226
+ * is one plain read-only SELECT with no write or DDL at any depth.
1227
+ */
1228
+ private strictReadOnlyStatementReads(sqlText: string, dialect: SQLParserDialect, label: string): StrictStatementReads {
1229
+ const result = StrictFragmentValidator.ValidateReadOnlyStatement(sqlText, dialect, { CollectReads: true });
1230
+ if (result.Refusal === 'NestedWrite') {
1231
+ throw new Error(`Invalid ${label}: write/DDL nested in a subquery is not permitted`);
1232
+ }
1233
+ if (!result.IsValid || !result.Reads) {
1234
+ throw new Error(`Invalid ${label}: not a safe read-only statement — refusing under uncertainty`);
1235
+ }
1236
+ return result.Reads;
1204
1237
  }
1205
1238
 
1206
1239
  private dialectForProvider(provider?: IMetadataProvider): SQLParserDialect {
@@ -1241,7 +1274,8 @@ export class ResolverBase {
1241
1274
  dataSource?: 'Live' | 'Materialized',
1242
1275
  includeBinaryFields?: boolean,
1243
1276
  alternateBaseView?: string,
1244
- properties?: ViewPropertyValues
1277
+ properties?: ViewPropertyValues,
1278
+ skipTotalRowCount?: boolean
1245
1279
  ) {
1246
1280
  try {
1247
1281
  if (!viewInfo || !userPayload) return null;
@@ -1324,9 +1358,11 @@ export class ResolverBase {
1324
1358
  BypassCache: bypassCache,
1325
1359
  DataSource: dataSource,
1326
1360
  IncludeBinaryFields: includeBinaryFields,
1361
+ SkipTotalRowCount: skipTotalRowCount,
1327
1362
  AlternateBaseView: alternateBaseView,
1328
1363
  Properties: properties,
1329
1364
  TimeZone: userPayload?.TimeZone,
1365
+ PreserveRestrictedApiCiphertext: true,
1330
1366
  },
1331
1367
  user
1332
1368
  );
@@ -1488,9 +1524,11 @@ export class ResolverBase {
1488
1524
  BypassCache: param.bypassCache,
1489
1525
  DataSource: param.dataSource,
1490
1526
  IncludeBinaryFields: param.includeBinaryFields,
1527
+ SkipTotalRowCount: param.skipTotalRowCount,
1491
1528
  AlternateBaseView: param.alternateBaseView,
1492
1529
  Properties: param.properties,
1493
1530
  TimeZone: param.userPayload?.TimeZone,
1531
+ PreserveRestrictedApiCiphertext: true,
1494
1532
  });
1495
1533
  }
1496
1534
 
@@ -1691,12 +1729,40 @@ export class ResolverBase {
1691
1729
  }
1692
1730
 
1693
1731
  protected ListenForEntityMessages(entityObject: BaseEntity, pubSub: PubSubEngine, userPayload: UserPayload) {
1694
- // 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
1695
- // entity in the system. This is important because we don't want to have multiple listeners for the same entity as it could
1696
- // cause issues with multiple messages for the same event.
1732
+ // Maintain a single MJGlobal event listener per entity name so CloudEvents are emitted once
1733
+ // per event, while routing WebSocket status updates dynamically across all active user sessions
1734
+ // that have interacted with this entity type (instead of closing over only the first caller).
1697
1735
  const uniqueKey = entityObject.EntityInfo.Name;
1736
+ const now = Date.now();
1737
+ const isFreshSubscription = !this.eventSubscriptions.has(uniqueKey);
1738
+
1739
+ let sessionsForEntity = isFreshSubscription ? undefined : this.activeEntitySessions.get(uniqueKey);
1740
+ if (!sessionsForEntity) {
1741
+ sessionsForEntity = new Map<string, ActiveEntitySessionListener>();
1742
+ this.activeEntitySessions.set(uniqueKey, sessionsForEntity);
1743
+ } else {
1744
+ // Opportunistically evict expired session entries and enforce a safe per-entity cap
1745
+ for (const [key, entry] of sessionsForEntity) {
1746
+ if (now - entry.lastSeenAt > ACTIVE_ENTITY_SESSION_TTL_MS) {
1747
+ sessionsForEntity.delete(key);
1748
+ }
1749
+ }
1750
+ if (sessionsForEntity.size >= ACTIVE_ENTITY_SESSION_MAX_PER_ENTITY) {
1751
+ const oldestKey = sessionsForEntity.keys().next().value;
1752
+ if (oldestKey !== undefined) {
1753
+ sessionsForEntity.delete(oldestKey);
1754
+ }
1755
+ }
1756
+ }
1698
1757
 
1699
- if (!this.eventSubscriptions.has(uniqueKey)) {
1758
+ if (userPayload) {
1759
+ const sessionKey = userPayload.sessionId
1760
+ ? `${userPayload.userRecord?.ID ?? ''}:${userPayload.sessionId}`
1761
+ : (userPayload.userRecord?.ID ?? '__anonymous__');
1762
+ sessionsForEntity.set(sessionKey, { pubSub, userPayload, lastSeenAt: now });
1763
+ }
1764
+
1765
+ if (isFreshSubscription) {
1700
1766
  // listen for events from the entityObject in case it is a long running task and we can push messages back to the client via pubSub
1701
1767
  LogDebug(`ResolverBase.ListenForEntityMessages: About to call MJGlobal.Instance.GetEventListener() to get the event listener subscription for ${uniqueKey}`);
1702
1768
  const theSub = MJGlobal.Instance.GetEventListener(false).subscribe(async (event: MJEvent) => {
@@ -1710,15 +1776,28 @@ export class ResolverBase {
1710
1776
  }
1711
1777
 
1712
1778
  const baseEntityValues = baseEntity ? this.EntityEventPayload(baseEntity, event.args instanceof BaseEntityEvent ? event.args.ChangedFieldNames : undefined) : null;
1713
- const eventToLog = { entityName: entityObject.EntityInfo.Name, baseEntity: baseEntityValues, event: event.event, eventCode: event.eventCode };
1779
+ const eventToLog = { entityName: uniqueKey, baseEntity: baseEntityValues, event: event.event, eventCode: event.eventCode };
1714
1780
  LogDebug(`ResolverBase.ListenForEntityMessages: Received Event from within MJGlobal.Instance.GetEventListener() callback. Will call EmitCloudEvent() next\nEvent data:\n${JSON.stringify(eventToLog)}`);
1715
1781
  await this.EmitCloudEvent(event);
1716
- LogDebug(`ResolverBase.ListenForEntityMessages: EmitCloudEvent() completed successfully`);
1717
-
1718
- if (event.args && event.args instanceof BaseEntityEvent && this.EntityEventBelongsToSession(event.args, userPayload)) {
1719
- // message from an entity object of this session's user, relay it to the client
1720
- LogDebug('ResolverBase.ListenForEntityMessages: About to publish PUSH_STATUS_UPDATES_TOPIC');
1721
- this.PublishStatusUpdate(pubSub, userPayload.sessionId, this.EntityStatusMessage(event.args), userPayload);
1782
+ LogDebug(`ResolverBase.ListenForEntityMessages: EmitCloudEvent() completed successfully`);
1783
+
1784
+ if (event.args && event.args instanceof BaseEntityEvent) {
1785
+ const activeSessions = this.activeEntitySessions.get(uniqueKey);
1786
+ if (activeSessions && activeSessions.size > 0) {
1787
+ const eventNow = Date.now();
1788
+ let serializedMessage: string | undefined;
1789
+ for (const [sKey, session] of activeSessions) {
1790
+ if (eventNow - session.lastSeenAt > ACTIVE_ENTITY_SESSION_TTL_MS) {
1791
+ activeSessions.delete(sKey);
1792
+ continue;
1793
+ }
1794
+ if (this.EntityEventBelongsToSession(event.args, session.userPayload)) {
1795
+ LogDebug('ResolverBase.ListenForEntityMessages: About to publish PUSH_STATUS_UPDATES_TOPIC');
1796
+ serializedMessage ??= this.EntityStatusMessage(event.args);
1797
+ this.PublishStatusUpdate(session.pubSub, session.userPayload.sessionId, serializedMessage, session.userPayload);
1798
+ }
1799
+ }
1800
+ }
1722
1801
  }
1723
1802
  }
1724
1803
  });
@@ -2243,143 +2322,335 @@ export class ResolverBase {
2243
2322
  return entityInfo.Fields.find((f) => f.CodeName === key || f.Name === key);
2244
2323
  }
2245
2324
 
2246
- protected async TestAndSetClientOldValuesToDBValues(input: any, clientNewValues: any, entityObject: BaseEntity, contextUser: UserInfo) {
2247
- // we have OldValues, so we need to compare them to the values we just loaded from the DB
2248
- const clientOldValues = {};
2249
- // for each item in the oldValues array, add it to the clientOldValues object
2250
- input.OldValues___.forEach((item) => {
2251
- // 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
2252
- const field = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, item.Key);
2253
- const val = this.ClientOldValueToFieldValue(field, item.Value);
2254
- clientOldValues[item.Key] = val;
2325
+ /**
2326
+ * Whether the API never gives a client this field's stored value: an encrypted field that is not
2327
+ * decrypted for the API reaches the client only as the sentinel or as fresh ciphertext, so the
2328
+ * client's old value for it can never be compared with the database value.
2329
+ */
2330
+ private isFieldOpaqueToClient(field: EntityFieldInfo | undefined): boolean {
2331
+ return Boolean(field?.Encrypt && !field.AllowDecryptInAPI);
2332
+ }
2333
+
2334
+ /**
2335
+ * The client's new values without the opaque fields it only echoed back: the sentinel, or the same
2336
+ * value it received as the old value. Writing either would replace the stored secret.
2337
+ */
2338
+ private withoutEchoedOpaqueValues<TOldVal, TNewVal>(
2339
+ entityInfo: EntityInfo,
2340
+ clientNewValues: Record<string, TNewVal>,
2341
+ clientOldValues: Record<string, TOldVal>,
2342
+ ): Record<string, TNewVal> {
2343
+ const kept: Record<string, TNewVal> = {};
2344
+ for (const key of Object.keys(clientNewValues)) {
2345
+ const f = this.findEntityFieldByCodeNameOrName(entityInfo, key);
2346
+ const newValue = clientNewValues[key];
2347
+ const unchanged = key in clientOldValues && !this.areEntityFieldValuesDifferent(f, clientOldValues[key], newValue, false);
2348
+ const echoed = this.isFieldOpaqueToClient(f) && (newValue === ENCRYPTED_SENTINEL || unchanged);
2349
+ if (!echoed) {
2350
+ kept[key] = newValue;
2351
+ }
2352
+ }
2353
+ return kept;
2354
+ }
2355
+
2356
+ /**
2357
+ * Copies concurrency-conflict diagnostic entries with every encrypted field's values replaced by
2358
+ * the sentinel, so the console, the error log and `MJ: Error Logs` never hold a secret.
2359
+ */
2360
+ private redactEncryptedConflictValues<TValue>(
2361
+ entityInfo: EntityInfo,
2362
+ entries: ReadonlyArray<ConflictDiagnosticEntry<TValue>>,
2363
+ ): ConflictDiagnosticEntry<TValue | string>[] {
2364
+ return entries.map((entry) => {
2365
+ if (!this.findEntityFieldByCodeNameOrName(entityInfo, entry.FieldName)?.Encrypt) {
2366
+ return entry;
2367
+ }
2368
+ const redacted: ConflictDiagnosticEntry<TValue | string> = { FieldName: entry.FieldName };
2369
+ if ('ClientOldValue' in entry) redacted.ClientOldValue = ENCRYPTED_SENTINEL;
2370
+ if ('ClientNewValue' in entry) redacted.ClientNewValue = ENCRYPTED_SENTINEL;
2371
+ if ('DBValue' in entry) redacted.DBValue = ENCRYPTED_SENTINEL;
2372
+ return redacted;
2255
2373
  });
2374
+ }
2375
+
2376
+ /**
2377
+ * Compares two typed entity field values for semantic inequality, handling `Date` epoch equality
2378
+ * (plus optional non-`datetimeoffset` timezone-hour-shift tolerance) and `null`/`undefined` normalization.
2379
+ */
2380
+ private areEntityFieldValuesDifferent<TLeft, TRight>(
2381
+ field: EntityFieldInfo | undefined,
2382
+ left: TLeft,
2383
+ right: TRight,
2384
+ allowTimezoneShiftWarning: boolean,
2385
+ fieldKeyForLog?: string
2386
+ ): boolean {
2387
+ const normalizedLeft = left === undefined ? null : left;
2388
+ const normalizedRight = right === undefined ? null : right;
2389
+
2390
+ if (normalizedLeft instanceof Date) {
2391
+ const rightDate =
2392
+ normalizedRight instanceof Date
2393
+ ? normalizedRight
2394
+ : field?.TSType === EntityFieldTSType.Date && normalizedRight !== null
2395
+ ? this.ClientOldValueToFieldValue(field, normalizedRight)
2396
+ : null;
2397
+ if (!(rightDate instanceof Date) || isNaN(rightDate.getTime())) {
2398
+ return true;
2399
+ }
2400
+ if (this.datesEqualAtColumnResolution(field, normalizedLeft, rightDate)) {
2401
+ return false;
2402
+ }
2403
+ const sqlType = field?.SQLFullType?.trim().toLowerCase() || '';
2404
+ if (sqlType !== 'datetimeoffset' && IsOnlyTimezoneShift(normalizedLeft, rightDate)) {
2405
+ if (allowTimezoneShiftWarning && fieldKeyForLog) {
2406
+ console.warn(
2407
+ `Timezone hour shift detected on field "${fieldKeyForLog}" (${sqlType || 'datetime'}). ` +
2408
+ `Client: ${normalizedLeft.toISOString()}, DB: ${rightDate.toISOString()}. ` +
2409
+ `Consider using datetimeoffset to avoid timezone ambiguity.`
2410
+ );
2411
+ }
2412
+ return false;
2413
+ }
2414
+ return true;
2415
+ }
2416
+
2417
+ if (normalizedRight instanceof Date && field?.TSType === EntityFieldTSType.Date && normalizedLeft !== null) {
2418
+ const leftDate = this.ClientOldValueToFieldValue(field, normalizedLeft);
2419
+ if (leftDate instanceof Date && !isNaN(leftDate.getTime())) {
2420
+ return !this.datesEqualAtColumnResolution(field, leftDate, normalizedRight);
2421
+ }
2422
+ }
2423
+
2424
+ if (typeof normalizedLeft === 'string' && typeof normalizedRight === 'string' && this.jsonStringsEquivalent(normalizedLeft, normalizedRight)) {
2425
+ return false;
2426
+ }
2427
+
2428
+ return Object.is(normalizedLeft, normalizedRight) ? false : normalizedLeft !== (normalizedRight as never);
2429
+ }
2430
+
2431
+ /**
2432
+ * True when two instants would be stored as the same value in the field's column: SQL Server
2433
+ * `datetime` resolves to 1/300 s and `smalldatetime` to one minute; every other type keeps milliseconds.
2434
+ */
2435
+ private datesEqualAtColumnResolution(field: EntityFieldInfo | undefined, left: Date, right: Date): boolean {
2436
+ return Math.abs(left.getTime() - right.getTime()) < this.dateColumnResolutionMs(field);
2437
+ }
2438
+
2439
+ private dateColumnResolutionMs(field: EntityFieldInfo | undefined): number {
2440
+ switch ((field?.Type ?? '').trim().toLowerCase()) {
2441
+ case 'datetime':
2442
+ return 1000 / 300;
2443
+ case 'smalldatetime':
2444
+ return 60_000;
2445
+ default:
2446
+ return 1;
2447
+ }
2448
+ }
2449
+
2450
+ /**
2451
+ * True when both strings are JSON documents with the same content, ignoring whitespace and key order.
2452
+ */
2453
+ private jsonStringsEquivalent(left: string, right: string): boolean {
2454
+ const canonicalLeft = this.canonicalJSONDocument(left);
2455
+ return canonicalLeft !== null && canonicalLeft === this.canonicalJSONDocument(right);
2456
+ }
2457
+
2458
+ /**
2459
+ * Canonical form of a JSON object or array string, or null when the text is not one.
2460
+ */
2461
+ private canonicalJSONDocument(text: string): string | null {
2462
+ const first = text.trimStart().charAt(0);
2463
+ if (first !== '{' && first !== '[') {
2464
+ return null;
2465
+ }
2466
+ try {
2467
+ return Canonicalize(JSON.parse(text));
2468
+ } catch {
2469
+ return null;
2470
+ }
2471
+ }
2472
+
2473
+ protected async TestAndSetClientOldValuesToDBValues<
2474
+ TOldVal,
2475
+ TNewVal,
2476
+ TInput extends { OldValues___?: ReadonlyArray<{ Key: string; Value: TOldVal }> | null }
2477
+ >(
2478
+ input: TInput,
2479
+ rawClientNewValues: Record<string, TNewVal>,
2480
+ entityObject: BaseEntity,
2481
+ contextUser: UserInfo
2482
+ ): Promise<void> {
2483
+ type ConvertedFieldValue = ReturnType<ResolverBase['ClientOldValueToFieldValue']>;
2484
+ const clientOldValues: Record<string, ConvertedFieldValue> = {};
2485
+ for (const item of input.OldValues___ ?? []) {
2486
+ // Transform string-serialized wire values to match the field's TypeScript type
2487
+ const field = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, item.Key);
2488
+ clientOldValues[item.Key] = this.ClientOldValueToFieldValue(field, item.Value);
2489
+ }
2490
+ const clientNewValues = this.withoutEchoedOpaqueValues(entityObject.EntityInfo, rawClientNewValues, clientOldValues);
2256
2491
 
2257
- // clientOldValues now has all of the oldValues the CLIENT passed us. Now we need to build the same kind of object
2258
- // with the DB values
2259
2492
  // fls-guard: compared on the server only, never returned
2260
2493
  const dbValues = entityObject.GetAll();
2261
2494
 
2262
- // 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
2263
- const dbDifferences = [];
2264
- Object.keys(clientOldValues).forEach((key) => {
2495
+ interface DBDifferenceEntry {
2496
+ FieldName: string;
2497
+ ClientOldValue: ConvertedFieldValue;
2498
+ DBValue: ConvertedFieldValue;
2499
+ }
2500
+ interface ClientDifferenceEntry {
2501
+ FieldName: string;
2502
+ ClientOldValue: ConvertedFieldValue;
2503
+ ClientNewValue: TNewVal;
2504
+ }
2505
+
2506
+ const dbDifferences: DBDifferenceEntry[] = [];
2507
+ const dbDiffByField = new Map<string, DBDifferenceEntry>();
2508
+
2509
+ for (const key of Object.keys(clientOldValues)) {
2265
2510
  const f = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, key);
2266
- const dbVal = f && f.Name in dbValues ? dbValues[f.Name] : dbValues[key];
2267
- let different = false;
2268
- switch (typeof clientOldValues[key]) {
2269
- case 'number':
2270
- different = clientOldValues[key] !== dbVal;
2271
- break;
2272
- case 'boolean':
2273
- different = clientOldValues[key] !== dbVal;
2274
- break;
2275
- case 'object':
2276
- if (clientOldValues[key] instanceof Date) {
2277
- const clientDate = clientOldValues[key] as Date;
2278
- const dbDate = dbVal;
2279
- if (dbDate == null || !(dbDate instanceof Date)) {
2280
- different = true;
2281
- } else if (clientDate.getTime() !== dbDate.getTime()) {
2282
- // Check if this is a datetime/datetime2 field with only a timezone hour shift
2283
- const sqlType = f?.SQLFullType?.trim().toLowerCase() || '';
2284
- if (sqlType !== 'datetimeoffset' && IsOnlyTimezoneShift(clientDate, dbDate)) {
2285
- console.warn(
2286
- `Timezone hour shift detected on field "${key}" (${sqlType || 'datetime'}). ` +
2287
- `Client: ${clientDate.toISOString()}, DB: ${dbDate.toISOString()}. ` +
2288
- `Consider using datetimeoffset to avoid timezone ambiguity.`
2289
- );
2290
- different = false; // Allow timezone shifts through for non-datetimeoffset fields
2291
- } else {
2292
- different = true;
2293
- }
2294
- }
2295
- } else if (clientOldValues[key] === null) {
2296
- different = dbVal !== null;
2297
- }
2298
- break;
2299
- default:
2300
- different = clientOldValues[key] !== dbVal;
2301
- break;
2511
+ if (this.isFieldOpaqueToClient(f)) {
2512
+ continue;
2302
2513
  }
2514
+ const dbVal: ConvertedFieldValue = f && f.Name in dbValues ? dbValues[f.Name] : dbValues[key];
2515
+ const different = this.areEntityFieldValuesDifferent(f, clientOldValues[key], dbVal, true, key);
2303
2516
  if (different && f && !f.ReadOnly) {
2304
- // only include updateable fields
2305
- dbDifferences.push({
2517
+ const entry: DBDifferenceEntry = {
2306
2518
  FieldName: key,
2307
2519
  ClientOldValue: clientOldValues[key],
2308
2520
  DBValue: dbVal,
2309
- });
2521
+ };
2522
+ dbDifferences.push(entry);
2523
+ dbDiffByField.set(key, entry);
2310
2524
  }
2311
- });
2525
+ }
2526
+
2527
+ let fieldsToPreserveFromDB: Set<string> | undefined;
2312
2528
 
2313
2529
  if (dbDifferences.length > 0) {
2314
- // 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
2315
- // 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
2316
- const clientDifferences = [];
2317
- Object.keys(clientOldValues).forEach((key) => {
2530
+ const clientDifferences: ClientDifferenceEntry[] = [];
2531
+ const clientDiffFieldNames = new Set<string>();
2532
+
2533
+ for (const key of Object.keys(clientOldValues)) {
2318
2534
  const f = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, key);
2319
- if (key in clientNewValues && clientOldValues[key] !== clientNewValues[key] && f && f.AllowUpdateAPI && !f.IsPrimaryKey) {
2320
- // only include updateable fields
2535
+ if (
2536
+ key in clientNewValues &&
2537
+ f &&
2538
+ f.AllowUpdateAPI &&
2539
+ !f.IsPrimaryKey &&
2540
+ this.areEntityFieldValuesDifferent(f, clientOldValues[key], clientNewValues[key], false)
2541
+ ) {
2321
2542
  clientDifferences.push({
2322
2543
  FieldName: key,
2323
2544
  ClientOldValue: clientOldValues[key],
2324
2545
  ClientNewValue: clientNewValues[key],
2325
2546
  });
2547
+ clientDiffFieldNames.add(key);
2548
+ }
2549
+ }
2550
+
2551
+ // True conflicting overlap: client modified a field whose authoritative DB value differs from BOTH
2552
+ // ClientOldValue AND ClientNewValue (idempotent concurrent writes to the same value converge cleanly).
2553
+ const overlap = clientDifferences.filter((cd) => {
2554
+ const dd = dbDiffByField.get(cd.FieldName);
2555
+ if (!dd) {
2556
+ return false;
2326
2557
  }
2558
+ const f = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, cd.FieldName);
2559
+ return this.areEntityFieldValuesDifferent(f, cd.ClientNewValue, dd.DBValue, true);
2327
2560
  });
2328
2561
 
2329
- // 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
2330
- // 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
2331
- const dbDiffFieldNames = new Set(dbDifferences.map((dd) => dd.FieldName));
2332
- const overlap = clientDifferences.filter((cd) => dbDiffFieldNames.has(cd.FieldName));
2333
2562
  if (overlap.length > 0) {
2563
+ const conflictingFields = overlap.map((o) => o.FieldName);
2564
+ const entityInfo = entityObject.EntityInfo;
2565
+ const shownClientDifferences = this.redactEncryptedConflictValues<ConvertedFieldValue | TNewVal>(entityInfo, clientDifferences);
2566
+ const shownDBDifferences = this.redactEncryptedConflictValues<ConvertedFieldValue | TNewVal>(entityInfo, dbDifferences);
2567
+ const shownOverlap = this.redactEncryptedConflictValues<ConvertedFieldValue | TNewVal>(entityInfo, overlap);
2334
2568
  const msg = {
2335
2569
  Message:
2336
- '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.',
2337
- ClientDifferences: clientDifferences,
2338
- DBDifferences: dbDifferences,
2339
- Overlap: overlap,
2570
+ 'Concurrent modification conflict: one or more fields modified by the client were updated in the database since the record was loaded. Save rejected to prevent overwriting concurrent changes.',
2571
+ ClientDifferences: shownClientDifferences,
2572
+ DBDifferences: shownDBDifferences,
2573
+ Overlap: shownOverlap,
2340
2574
  };
2341
-
2342
- // Log as warning to console and ErrorLog table instead of throwing error
2343
- console.warn('Entity save inconsistency detected but allowing save to continue:', JSON.stringify(msg));
2575
+
2576
+ console.warn('Entity save concurrency conflict detected — rejecting conflicting update:', JSON.stringify(msg));
2344
2577
  LogErrorEx({
2345
2578
  message: `Entity save inconsistency detected: ${JSON.stringify(msg)}`,
2346
2579
  severity: 'warning',
2347
2580
  category: 'ResolverBase.TestAndSetClientOldValuesToDBValues',
2348
2581
  metadata: {
2349
2582
  entityName: entityObject.EntityInfo.Name,
2350
- clientDifferences: clientDifferences,
2351
- dbDifferences: dbDifferences,
2352
- overlap: overlap,
2583
+ clientDifferences: shownClientDifferences,
2584
+ dbDifferences: shownDBDifferences,
2585
+ overlap: shownOverlap,
2353
2586
  },
2354
2587
  });
2355
2588
 
2356
- // Create ErrorLog record in the database — use the entity's bound provider so the
2357
- // ErrorLog write goes to the same connection as the entity that triggered it.
2589
+ // Persist SAVE_INCONSISTENCY ErrorLog record on the entity's bound provider before rejecting
2358
2590
  try {
2359
- const md = entityObject.ProviderToUse as unknown as IMetadataProvider;
2360
- const errorLogEntity = await md.GetEntityObject<MJErrorLogEntity>('MJ: Error Logs', contextUser);
2361
- // Code is NCHAR(20); a longer value is rejected at the database
2362
- errorLogEntity.Code = 'SAVE_INCONSISTENCY';
2363
- errorLogEntity.Message = `Entity save inconsistency detected for ${entityObject.EntityInfo.Name}: ${JSON.stringify(msg)}`;
2364
- errorLogEntity.Status = 'Warning';
2365
- errorLogEntity.Category = 'Entity Save';
2366
- errorLogEntity.CreatedBy = contextUser.Email || contextUser.Name;
2367
-
2368
- const saveResult = await errorLogEntity.Save();
2369
- if (!saveResult) {
2370
- console.error(
2371
- `Failed to save ErrorLog record for ${entityObject.EntityInfo.Name}: ${errorLogEntity.LatestResult?.CompleteMessage}`
2372
- );
2591
+ const hasEntityObjectFactory = (
2592
+ p: object | null | undefined
2593
+ ): p is Pick<IMetadataProvider, 'GetEntityObject'> => Boolean(p && 'GetEntityObject' in p);
2594
+ const providerCandidate = entityObject.ProviderToUse;
2595
+ if (hasEntityObjectFactory(providerCandidate)) {
2596
+ const errorLogEntity = await providerCandidate.GetEntityObject<MJErrorLogEntity>('MJ: Error Logs', contextUser);
2597
+ // Code is NCHAR(20); a longer value is rejected at the database
2598
+ errorLogEntity.Code = 'SAVE_INCONSISTENCY';
2599
+ errorLogEntity.Message = `Entity save inconsistency detected for ${entityObject.EntityInfo.Name}: ${JSON.stringify(msg)}`;
2600
+ errorLogEntity.Status = 'Warning';
2601
+ errorLogEntity.Category = 'Entity Save';
2602
+ errorLogEntity.CreatedBy = contextUser.Email || contextUser.Name;
2603
+
2604
+ const saveResult = await errorLogEntity.Save();
2605
+ if (!saveResult) {
2606
+ console.error(
2607
+ `Failed to save ErrorLog record for ${entityObject.EntityInfo.Name}: ${errorLogEntity.LatestResult?.CompleteMessage}`
2608
+ );
2609
+ }
2373
2610
  }
2374
2611
  } catch (errorLogError) {
2375
2612
  console.error('Error creating ErrorLog record:', errorLogError);
2376
2613
  }
2614
+
2615
+ throw new GraphQLError(
2616
+ `Concurrent modification conflict on ${entityObject.EntityInfo.Name} for field(s): ${conflictingFields.join(', ')}. Reload the record and retry your changes.`,
2617
+ {
2618
+ extensions: {
2619
+ code: 'CONCURRENT_MODIFICATION_CONFLICT',
2620
+ entityName: entityObject.EntityInfo.Name,
2621
+ conflictingFields,
2622
+ },
2623
+ }
2624
+ );
2625
+ }
2626
+
2627
+ // Non-overlapping concurrent DB edits: preserve any field that changed in the database
2628
+ // when the client itself did NOT modify that field (prevents full-payload clients or forged
2629
+ // OldValues___ from clobbering concurrent DB updates on untouched columns).
2630
+ fieldsToPreserveFromDB = new Set<string>();
2631
+ for (const dd of dbDifferences) {
2632
+ if (!clientDiffFieldNames.has(dd.FieldName)) {
2633
+ fieldsToPreserveFromDB.add(dd.FieldName);
2634
+ const f = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, dd.FieldName);
2635
+ if (f) {
2636
+ fieldsToPreserveFromDB.add(f.Name);
2637
+ fieldsToPreserveFromDB.add(f.CodeName);
2638
+ }
2639
+ }
2377
2640
  }
2378
2641
  }
2379
2642
 
2380
- // If we get here that means we've not thrown an exception, so there is
2381
- // NO OVERLAP, so we can set the new values from the data provided from the client now...
2382
- entityObject.SetMany(clientNewValues);
2643
+ if (fieldsToPreserveFromDB && fieldsToPreserveFromDB.size > 0) {
2644
+ const mergedValues: Record<string, TNewVal> = {};
2645
+ for (const key of Object.keys(clientNewValues)) {
2646
+ if (!fieldsToPreserveFromDB.has(key)) {
2647
+ mergedValues[key] = clientNewValues[key];
2648
+ }
2649
+ }
2650
+ entityObject.SetMany(mergedValues);
2651
+ } else {
2652
+ entityObject.SetMany(clientNewValues);
2653
+ }
2383
2654
  }
2384
2655
 
2385
2656
  protected async DeleteRecord(