@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
@@ -7,8 +7,8 @@ import { RefusalExtensions } from './refusalExtensions.js';
7
7
  import { GetAPIKeyEngine } from '@memberjunction/api-keys';
8
8
  import { httpTransport, CloudEvent, emitterFor } from 'cloudevents';
9
9
  import { DeleteOptionsInput } from './DeleteOptionsInput.js';
10
- import { MJEventType, MJGlobal, UUIDsEqual, ENCRYPTED_SENTINEL, ReplaceByteArraysWithBase64, EscapeSQLString, IsValueEncrypted, IsOnlyTimezoneShift } from '@memberjunction/global';
11
- 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';
12
12
  import { EncryptionEngine, IsEncryptionKeyUnavailable, ReportEncryptionKeyUnavailable } from '@memberjunction/encryption';
13
13
  import { PublishStatusUpdate, StatusUpdateOwnerID } from './PushStatusResolver.js';
14
14
  import { CACHE_INVALIDATION_TOPIC, MayBroadcastRecordData } from './CacheInvalidationResolver.js';
@@ -31,10 +31,13 @@ const ENTITIES_THAT_ALWAYS_LOAD_STORED_ROW = new Set([
31
31
  'mj: ai agent sessions',
32
32
  'mj: ai agent runs',
33
33
  ]);
34
+ const ACTIVE_ENTITY_SESSION_TTL_MS = 24 * 60 * 60 * 1000;
35
+ const ACTIVE_ENTITY_SESSION_MAX_PER_ENTITY = 2048;
34
36
  export class ResolverBase {
35
37
  static { this._emit = process.env.CLOUDEVENTS_HTTP_TRANSPORT ? emitterFor(httpTransport(process.env.CLOUDEVENTS_HTTP_TRANSPORT)) : null; }
36
38
  static { this._cloudeventsHeaders = process.env.CLOUDEVENTS_HTTP_HEADERS ? JSON.parse(process.env.CLOUDEVENTS_HTTP_HEADERS) : {}; }
37
39
  static { this._eventSubscriptionKey = '___MJServer___ResolverBase___EventSubscriptions'; }
40
+ static { this._activeEntitySessionsKey = '___MJServer___ResolverBase___ActiveEntitySessions'; }
38
41
  get eventSubscriptions() {
39
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
40
43
  // could actually be duplicated and we'd end up with multiple instances of the same map, which would be bad.
@@ -45,6 +48,13 @@ export class ResolverBase {
45
48
  }
46
49
  return g[ResolverBase._eventSubscriptionKey];
47
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
+ }
48
58
  /**
49
59
  * Maps field names to their GraphQL-safe CodeNames and handles encryption for API responses.
50
60
  *
@@ -334,28 +344,25 @@ export class ResolverBase {
334
344
  if (field.AllowDecryptInAPI)
335
345
  continue;
336
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
+ }
337
352
  const engine = EncryptionEngine.Instance;
338
353
  await engine.Config(false, contextUser);
339
354
  const keyMarker = field.EncryptionKeyID ? engine.GetKeyByID(field.EncryptionKeyID)?.Marker : undefined;
340
- if (field.SendEncryptedValue) {
341
- // Re-encrypt the value before sending
342
- // Only re-encrypt if it's not already encrypted (data provider decrypted it)
343
- if (typeof value === 'string' && !IsValueEncrypted(value, keyMarker)) {
344
- try {
345
- const encryptedValue = await engine.Encrypt(value, field.EncryptionKeyID, contextUser);
346
- dataObject[fieldName] = encryptedValue;
347
- }
348
- catch (err) {
349
- // If re-encryption fails, use sentinel for safety
350
- LogError(`Failed to re-encrypt field ${fieldName} for API response: ${err}`);
351
- dataObject[fieldName] = ENCRYPTED_SENTINEL;
352
- }
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;
353
365
  }
354
- // If already encrypted (shouldn't happen normally), keep as-is
355
- }
356
- else {
357
- // SendEncryptedValue is false - replace with sentinel
358
- dataObject[fieldName] = ENCRYPTED_SENTINEL;
359
366
  }
360
367
  }
361
368
  return dataObject;
@@ -505,7 +512,7 @@ export class ResolverBase {
505
512
  if (viewInfo) {
506
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
507
514
  ? CompositeKey.FromKeyValuePairs(viewInput.AfterKey.KeyValuePairs)
508
- : undefined, viewInput.BypassCache, viewInput.DataSource, viewInput.IncludeBinaryFields, viewInput.AlternateBaseView, viewInput.Properties);
515
+ : undefined, viewInput.BypassCache, viewInput.DataSource, viewInput.IncludeBinaryFields, viewInput.AlternateBaseView, viewInput.Properties, viewInput.SkipTotalRowCount);
509
516
  }
510
517
  else {
511
518
  LogError(`RunViewByNameGeneric: View ${viewInput.ViewName} not found or no results returned`);
@@ -528,7 +535,7 @@ export class ResolverBase {
528
535
  if (!viewInfo) {
529
536
  return null;
530
537
  }
531
- return this.RunViewGenericInternal(provider, viewInfo, viewInput.ExtraFilter, viewInput.OrderBy, viewInput.UserSearchString, viewInput.ExcludeUserViewRunID, viewInput.OverrideExcludeFilter, viewInput.SaveViewResults, viewInput.Fields, viewInput.IgnoreMaxRows, viewInput.ExcludeDataFromAllPriorViewRuns, viewInput.ForceAuditLog, viewInput.AuditLogDescription, viewInput.ResultType, userPayload, viewInput.MaxRows, viewInput.StartRow, viewInput.Aggregates, undefined, viewInput.BypassCache, viewInput.DataSource, viewInput.IncludeBinaryFields, viewInput.AlternateBaseView, viewInput.Properties);
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);
532
539
  }
533
540
  catch (err) {
534
541
  console.log(err);
@@ -551,7 +558,7 @@ export class ResolverBase {
551
558
  EntityID: entity.ID,
552
559
  EntityBaseView: entity.BaseView,
553
560
  }; // only providing a few bits of data here, but it's enough to get the view to run
554
- return this.RunViewGenericInternal(provider, viewInfo, viewInput.ExtraFilter, viewInput.OrderBy, viewInput.UserSearchString, viewInput.ExcludeUserViewRunID, viewInput.OverrideExcludeFilter, false, viewInput.Fields, viewInput.IgnoreMaxRows, false, viewInput.ForceAuditLog, viewInput.AuditLogDescription, viewInput.ResultType, userPayload, viewInput.MaxRows, viewInput.StartRow, viewInput.Aggregates, undefined, viewInput.BypassCache, viewInput.DataSource, viewInput.IncludeBinaryFields, viewInput.AlternateBaseView);
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);
555
562
  }
556
563
  catch (err) {
557
564
  console.log(err);
@@ -621,6 +628,7 @@ export class ResolverBase {
621
628
  bypassCache: viewInput.BypassCache,
622
629
  dataSource: viewInput.DataSource,
623
630
  includeBinaryFields: viewInput.IncludeBinaryFields,
631
+ skipTotalRowCount: viewInput.SkipTotalRowCount,
624
632
  alternateBaseView: viewInput.AlternateBaseView,
625
633
  properties: viewInput.Properties,
626
634
  });
@@ -844,7 +852,7 @@ export class ResolverBase {
844
852
  * Fails closed: no user, or an unresolvable reference, refuses.
845
853
  *
846
854
  * The references come from a walk of the whole parse tree
847
- * (`ClientClauseScreen.CollectStatementReads`). The statement is refused when that walk does not
855
+ * (`StrictFragmentValidator.ValidateReadOnlyStatement`). The statement is refused when that walk does not
848
856
  * account for every SELECT in the text, or when a FROM item names no table (a table function or
849
857
  * a row list).
850
858
  *
@@ -860,14 +868,7 @@ export class ResolverBase {
860
868
  }
861
869
  const dialect = this.dialectForProvider(provider);
862
870
  ClientClauseScreen.AssertNoIdentifierAliasNames(sqlText, dialect, label);
863
- const parser = new SQLParser(sqlText, dialect);
864
- if (!parser.IsValid || parser.HasWriteStatement || parser.StatementKind !== 'select') {
865
- throw new Error(`Invalid ${label}: not a safe read-only statement — refusing under uncertainty`);
866
- }
867
- if (this.astContainsWriteNode(parser.AST)) {
868
- throw new Error(`Invalid ${label}: write/DDL nested in a subquery is not permitted`);
869
- }
870
- const reads = ClientClauseScreen.CollectStatementReads(parser);
871
+ const reads = this.strictReadOnlyStatementReads(sqlText, dialect, label);
871
872
  ClientClauseScreen.AssertEverySelectAccountedFor(sqlText, dialect, reads, label);
872
873
  const allowed = this.entityBaseViewAllowList(provider);
873
874
  for (const relation of reads.Relations) {
@@ -924,9 +925,19 @@ export class ResolverBase {
924
925
  hasBackslashInQuotes(clause, dialect) {
925
926
  return LexSQL(clause, dialect).some((t) => (t.Kind === 'string' || t.Kind === 'identifier') && t.Text.includes('\\'));
926
927
  }
927
- /** Write/DDL node walk shared with the clause screen; see `ClientClauseScreen.AstContainsWriteNode`. */
928
- astContainsWriteNode(node) {
929
- 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;
930
941
  }
931
942
  dialectForProvider(provider) {
932
943
  return ClientClauseScreen.DialectFor(provider);
@@ -940,7 +951,7 @@ export class ResolverBase {
940
951
  * - Field filtering at source (Fix #7)
941
952
  * - Improved error handling (Fix #9)
942
953
  */
943
- async RunViewGenericInternal(provider, viewInfo, extraFilter, orderBy, userSearchString, excludeUserViewRunID, overrideExcludeFilter, saveViewResults, fields, ignoreMaxRows, excludeDataFromAllPriorViewRuns, forceAuditLog, auditLogDescription, resultType, userPayload, maxRows, startRow, aggregates, afterKey, bypassCache, dataSource, includeBinaryFields, alternateBaseView, properties) {
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) {
944
955
  try {
945
956
  if (!viewInfo || !userPayload)
946
957
  return null;
@@ -1007,9 +1018,11 @@ export class ResolverBase {
1007
1018
  BypassCache: bypassCache,
1008
1019
  DataSource: dataSource,
1009
1020
  IncludeBinaryFields: includeBinaryFields,
1021
+ SkipTotalRowCount: skipTotalRowCount,
1010
1022
  AlternateBaseView: alternateBaseView,
1011
1023
  Properties: properties,
1012
1024
  TimeZone: userPayload?.TimeZone,
1025
+ PreserveRestrictedApiCiphertext: true,
1013
1026
  }, user);
1014
1027
  if (result && typeof result === 'object') {
1015
1028
  Object.defineProperty(result, '_resolvedViewEntity', {
@@ -1150,9 +1163,11 @@ export class ResolverBase {
1150
1163
  BypassCache: param.bypassCache,
1151
1164
  DataSource: param.dataSource,
1152
1165
  IncludeBinaryFields: param.includeBinaryFields,
1166
+ SkipTotalRowCount: param.skipTotalRowCount,
1153
1167
  AlternateBaseView: param.alternateBaseView,
1154
1168
  Properties: param.properties,
1155
1169
  TimeZone: param.userPayload?.TimeZone,
1170
+ PreserveRestrictedApiCiphertext: true,
1156
1171
  });
1157
1172
  }
1158
1173
  // Fix #4: Run views in a single batch through RunViews
@@ -1324,11 +1339,38 @@ export class ResolverBase {
1324
1339
  });
1325
1340
  }
1326
1341
  ListenForEntityMessages(entityObject, pubSub, userPayload) {
1327
- // 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
1328
- // entity in the system. This is important because we don't want to have multiple listeners for the same entity as it could
1329
- // 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).
1330
1345
  const uniqueKey = entityObject.EntityInfo.Name;
1331
- 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) {
1332
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
1333
1375
  LogDebug(`ResolverBase.ListenForEntityMessages: About to call MJGlobal.Instance.GetEventListener() to get the event listener subscription for ${uniqueKey}`);
1334
1376
  const theSub = MJGlobal.Instance.GetEventListener(false).subscribe(async (event) => {
@@ -1340,14 +1382,27 @@ export class ResolverBase {
1340
1382
  return;
1341
1383
  }
1342
1384
  const baseEntityValues = baseEntity ? this.EntityEventPayload(baseEntity, event.args instanceof BaseEntityEvent ? event.args.ChangedFieldNames : undefined) : null;
1343
- const eventToLog = { entityName: entityObject.EntityInfo.Name, baseEntity: baseEntityValues, event: event.event, eventCode: event.eventCode };
1385
+ const eventToLog = { entityName: uniqueKey, baseEntity: baseEntityValues, event: event.event, eventCode: event.eventCode };
1344
1386
  LogDebug(`ResolverBase.ListenForEntityMessages: Received Event from within MJGlobal.Instance.GetEventListener() callback. Will call EmitCloudEvent() next\nEvent data:\n${JSON.stringify(eventToLog)}`);
1345
1387
  await this.EmitCloudEvent(event);
1346
1388
  LogDebug(`ResolverBase.ListenForEntityMessages: EmitCloudEvent() completed successfully`);
1347
- if (event.args && event.args instanceof BaseEntityEvent && this.EntityEventBelongsToSession(event.args, userPayload)) {
1348
- // message from an entity object of this session's user, relay it to the client
1349
- LogDebug('ResolverBase.ListenForEntityMessages: About to publish PUSH_STATUS_UPDATES_TOPIC');
1350
- this.PublishStatusUpdate(pubSub, userPayload.sessionId, this.EntityStatusMessage(event.args), userPayload);
1389
+ if (event.args && event.args instanceof BaseEntityEvent) {
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
+ }
1351
1406
  }
1352
1407
  }
1353
1408
  });
@@ -1823,134 +1878,268 @@ export class ResolverBase {
1823
1878
  }
1824
1879
  return entityInfo.Fields.find((f) => f.CodeName === key || f.Name === key);
1825
1880
  }
1826
- async TestAndSetClientOldValuesToDBValues(input, clientNewValues, entityObject, contextUser) {
1827
- // 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) {
1828
2006
  const clientOldValues = {};
1829
- // for each item in the oldValues array, add it to the clientOldValues object
1830
- input.OldValues___.forEach((item) => {
1831
- // 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
1832
2009
  const field = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, item.Key);
1833
- const val = this.ClientOldValueToFieldValue(field, item.Value);
1834
- clientOldValues[item.Key] = val;
1835
- });
1836
- // clientOldValues now has all of the oldValues the CLIENT passed us. Now we need to build the same kind of object
1837
- // with the DB values
2010
+ clientOldValues[item.Key] = this.ClientOldValueToFieldValue(field, item.Value);
2011
+ }
2012
+ const clientNewValues = this.withoutEchoedOpaqueValues(entityObject.EntityInfo, rawClientNewValues, clientOldValues);
1838
2013
  // fls-guard: compared on the server only, never returned
1839
2014
  const dbValues = entityObject.GetAll();
1840
- // now we need to compare clientOldValues and dbValues and have a new array that has entries for any differences and have FieldName, clientOldValue and dbValue as properties
1841
2015
  const dbDifferences = [];
1842
- Object.keys(clientOldValues).forEach((key) => {
2016
+ const dbDiffByField = new Map();
2017
+ for (const key of Object.keys(clientOldValues)) {
1843
2018
  const f = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, key);
1844
- const dbVal = f && f.Name in dbValues ? dbValues[f.Name] : dbValues[key];
1845
- let different = false;
1846
- switch (typeof clientOldValues[key]) {
1847
- case 'number':
1848
- different = clientOldValues[key] !== dbVal;
1849
- break;
1850
- case 'boolean':
1851
- different = clientOldValues[key] !== dbVal;
1852
- break;
1853
- case 'object':
1854
- if (clientOldValues[key] instanceof Date) {
1855
- const clientDate = clientOldValues[key];
1856
- const dbDate = dbVal;
1857
- if (dbDate == null || !(dbDate instanceof Date)) {
1858
- different = true;
1859
- }
1860
- else if (clientDate.getTime() !== dbDate.getTime()) {
1861
- // Check if this is a datetime/datetime2 field with only a timezone hour shift
1862
- const sqlType = f?.SQLFullType?.trim().toLowerCase() || '';
1863
- if (sqlType !== 'datetimeoffset' && IsOnlyTimezoneShift(clientDate, dbDate)) {
1864
- console.warn(`Timezone hour shift detected on field "${key}" (${sqlType || 'datetime'}). ` +
1865
- `Client: ${clientDate.toISOString()}, DB: ${dbDate.toISOString()}. ` +
1866
- `Consider using datetimeoffset to avoid timezone ambiguity.`);
1867
- different = false; // Allow timezone shifts through for non-datetimeoffset fields
1868
- }
1869
- else {
1870
- different = true;
1871
- }
1872
- }
1873
- }
1874
- else if (clientOldValues[key] === null) {
1875
- different = dbVal !== null;
1876
- }
1877
- break;
1878
- default:
1879
- different = clientOldValues[key] !== dbVal;
1880
- break;
2019
+ if (this.isFieldOpaqueToClient(f)) {
2020
+ continue;
1881
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);
1882
2024
  if (different && f && !f.ReadOnly) {
1883
- // only include updateable fields
1884
- dbDifferences.push({
2025
+ const entry = {
1885
2026
  FieldName: key,
1886
2027
  ClientOldValue: clientOldValues[key],
1887
2028
  DBValue: dbVal,
1888
- });
2029
+ };
2030
+ dbDifferences.push(entry);
2031
+ dbDiffByField.set(key, entry);
1889
2032
  }
1890
- });
2033
+ }
2034
+ let fieldsToPreserveFromDB;
1891
2035
  if (dbDifferences.length > 0) {
1892
- // 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
1893
- // 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
1894
2036
  const clientDifferences = [];
1895
- Object.keys(clientOldValues).forEach((key) => {
2037
+ const clientDiffFieldNames = new Set();
2038
+ for (const key of Object.keys(clientOldValues)) {
1896
2039
  const f = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, key);
1897
- if (key in clientNewValues && clientOldValues[key] !== clientNewValues[key] && f && f.AllowUpdateAPI && !f.IsPrimaryKey) {
1898
- // 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)) {
1899
2045
  clientDifferences.push({
1900
2046
  FieldName: key,
1901
2047
  ClientOldValue: clientOldValues[key],
1902
2048
  ClientNewValue: clientNewValues[key],
1903
2049
  });
2050
+ clientDiffFieldNames.add(key);
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;
1904
2059
  }
2060
+ const f = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, cd.FieldName);
2061
+ return this.areEntityFieldValuesDifferent(f, cd.ClientNewValue, dd.DBValue, true);
1905
2062
  });
1906
- // 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
1907
- // 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
1908
- const dbDiffFieldNames = new Set(dbDifferences.map((dd) => dd.FieldName));
1909
- const overlap = clientDifferences.filter((cd) => dbDiffFieldNames.has(cd.FieldName));
1910
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);
1911
2069
  const msg = {
1912
- 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.',
1913
- ClientDifferences: clientDifferences,
1914
- DBDifferences: dbDifferences,
1915
- 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,
1916
2074
  };
1917
- // Log as warning to console and ErrorLog table instead of throwing error
1918
- console.warn('Entity save inconsistency detected but allowing save to continue:', JSON.stringify(msg));
2075
+ console.warn('Entity save concurrency conflict detected — rejecting conflicting update:', JSON.stringify(msg));
1919
2076
  LogErrorEx({
1920
2077
  message: `Entity save inconsistency detected: ${JSON.stringify(msg)}`,
1921
2078
  severity: 'warning',
1922
2079
  category: 'ResolverBase.TestAndSetClientOldValuesToDBValues',
1923
2080
  metadata: {
1924
2081
  entityName: entityObject.EntityInfo.Name,
1925
- clientDifferences: clientDifferences,
1926
- dbDifferences: dbDifferences,
1927
- overlap: overlap,
2082
+ clientDifferences: shownClientDifferences,
2083
+ dbDifferences: shownDBDifferences,
2084
+ overlap: shownOverlap,
1928
2085
  },
1929
2086
  });
1930
- // Create ErrorLog record in the database — use the entity's bound provider so the
1931
- // 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
1932
2088
  try {
1933
- const md = entityObject.ProviderToUse;
1934
- const errorLogEntity = await md.GetEntityObject('MJ: Error Logs', contextUser);
1935
- // Code is NCHAR(20); a longer value is rejected at the database
1936
- errorLogEntity.Code = 'SAVE_INCONSISTENCY';
1937
- errorLogEntity.Message = `Entity save inconsistency detected for ${entityObject.EntityInfo.Name}: ${JSON.stringify(msg)}`;
1938
- errorLogEntity.Status = 'Warning';
1939
- errorLogEntity.Category = 'Entity Save';
1940
- errorLogEntity.CreatedBy = contextUser.Email || contextUser.Name;
1941
- const saveResult = await errorLogEntity.Save();
1942
- if (!saveResult) {
1943
- console.error(`Failed to save ErrorLog record for ${entityObject.EntityInfo.Name}: ${errorLogEntity.LatestResult?.CompleteMessage}`);
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
+ }
1944
2103
  }
1945
2104
  }
1946
2105
  catch (errorLogError) {
1947
2106
  console.error('Error creating ErrorLog record:', errorLogError);
1948
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
+ });
1949
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);
1950
2142
  }
1951
- // If we get here that means we've not thrown an exception, so there is
1952
- // NO OVERLAP, so we can set the new values from the data provided from the client now...
1953
- entityObject.SetMany(clientNewValues);
1954
2143
  }
1955
2144
  async DeleteRecord(entityName, key, options, provider, userPayload, pubSub) {
1956
2145
  // Check API key scope authorization for entity delete operations