@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
@@ -0,0 +1,113 @@
1
+ /**
2
+ * The FIELD rows a schema build materializes, for a connection with its own catalog.
3
+ *
4
+ * A schema build (ApplySchema, ApplyAll, ApplyAllBatch, the connector build, schema evolution)
5
+ * reconstructs the source schema from persisted catalog rows and hands it to SchemaBuilder. It used
6
+ * to read each object's fields from the engine's in-memory catalog. Once a connection's field rows
7
+ * are no longer held resident — they are warmed per object on demand, and the only site that warms
8
+ * them is the sync loop — that read answers `[]` for every object of the connection, and an object
9
+ * with no fields materializes nothing. Observed on a per-connection catalog (364 objects, 7,230
10
+ * active fields): the apply logged "Reusing 364 persisted IOs", then ran a migration with an empty
11
+ * table list in 4 ms — no CREATE, no ALTER, no error.
12
+ *
13
+ * So the build reads the connection's field rows itself: one read per {@link SCOPED_FIELDS_CHUNK}
14
+ * objects, plain rows, bypassing the query cache (an answer cached before discovery's persist is the
15
+ * empty set), and without the row cap (one object can carry thousands of fields). A failed read
16
+ * THROWS: an empty answer here is exactly what produced the zero-table apply.
17
+ *
18
+ * Outside a per-connection scope — or for a connection without its own catalog — this returns
19
+ * `null`, and the build reads the shared catalog from memory exactly as before.
20
+ */
21
+ import { RunView, type UserInfo } from '@memberjunction/core';
22
+ import type { MJIntegrationObjectFieldEntity } from '@memberjunction/core-entities';
23
+ import {
24
+ IntegrationEngineBase,
25
+ ENTITY_COMPANY_INTEGRATION_OBJECT_FIELDS,
26
+ type CompanyIntegrationObjectFieldRow,
27
+ } from '@memberjunction/integration-engine-base';
28
+ import { CurrentCatalogCI } from '@memberjunction/integration-engine';
29
+
30
+ /**
31
+ * What a schema build — and a refresh's re-key check — reads off a field row. The shared entity and
32
+ * the per-connection row both satisfy it.
33
+ */
34
+ export type BuildFieldRow = Pick<
35
+ MJIntegrationObjectFieldEntity,
36
+ 'Name' | 'DisplayName' | 'Description' | 'Type' | 'IsRequired' | 'AllowsNull' | 'Length' | 'Precision'
37
+ | 'Scale' | 'DefaultValue' | 'IsPrimaryKey' | 'IsUniqueKey' | 'IsReadOnly' | 'RelatedIntegrationObjectID' | 'Status'
38
+ | 'Sequence'
39
+ >;
40
+
41
+ /** A connection's field rows, keyed by lower-cased per-connection object id. */
42
+ export type ScopedBuildFields = Map<string, CompanyIntegrationObjectFieldRow[]>;
43
+
44
+ /** Objects per read. Each read is one `IN (…)` list, so this bounds the filter's length. */
45
+ export const SCOPED_FIELDS_CHUNK = 200;
46
+
47
+ /** A per-connection field row as the database returns it — before the legacy read names are derived. */
48
+ type StoredFieldRow = Omit<CompanyIntegrationObjectFieldRow, 'IntegrationObjectID' | 'RelatedIntegrationObjectID'>;
49
+
50
+ /**
51
+ * Loads the field rows of the connection in catalog scope for a schema build, or returns `null`
52
+ * when the build should read the shared catalog (no scope, or no per-connection catalog).
53
+ *
54
+ * @throws PER_CONNECTION_FIELDS_UNREADABLE when a read fails — never an empty answer.
55
+ */
56
+ export async function LoadScopedFieldsForBuild(integrationID: string, user: UserInfo): Promise<ScopedBuildFields | null> {
57
+ const ciID = CurrentCatalogCI();
58
+ if (!ciID) return null;
59
+ const engine = IntegrationEngineBase.Instance;
60
+ if (!engine.HasCompanyIntegrationCatalog(ciID)) return null;
61
+
62
+ const objects = engine.GetActiveIntegrationObjects(integrationID);
63
+ const byObject: ScopedBuildFields = new Map();
64
+ const rv = new RunView();
65
+ for (let i = 0; i < objects.length; i += SCOPED_FIELDS_CHUNK) {
66
+ const chunk = objects.slice(i, i + SCOPED_FIELDS_CHUNK);
67
+ const ids = chunk.map(o => `'${String(o.ID).replace(/'/g, "''")}'`).join(',');
68
+ const res = await rv.RunView<StoredFieldRow>({
69
+ EntityName: ENTITY_COMPANY_INTEGRATION_OBJECT_FIELDS,
70
+ ExtraFilter: `CompanyIntegrationObjectID IN (${ids})`,
71
+ ResultType: 'simple',
72
+ BypassCache: true,
73
+ IgnoreMaxRows: true,
74
+ }, user);
75
+ if (!res?.Success) {
76
+ throw new Error(
77
+ `PER_CONNECTION_FIELDS_UNREADABLE: could not read the field rows of connection ${ciID} ` +
78
+ `(objects ${i + 1}-${i + chunk.length} of ${objects.length}): ${res?.ErrorMessage ?? 'unknown error'}. ` +
79
+ `Refusing to build a schema from an empty field list.`
80
+ );
81
+ }
82
+ for (const row of res.Results ?? []) addRow(byObject, row);
83
+ }
84
+ const rowCount = [...byObject.values()].reduce((n, rows) => n + rows.length, 0);
85
+ console.log(`[LoadScopedFieldsForBuild] connection ${ciID}: ${rowCount} field rows for ${byObject.size} of ${objects.length} active objects`);
86
+ return byObject;
87
+ }
88
+
89
+ /**
90
+ * The fields a build reads for one object: the connection's rows when they were loaded, the
91
+ * engine's catalog otherwise. With loaded rows the engine is not consulted at all, so a cold
92
+ * per-connection field cache cannot empty the build.
93
+ */
94
+ export function FieldsForBuild(objectID: string, scoped: ScopedBuildFields | null | undefined, engine: IntegrationEngineBase): BuildFieldRow[] {
95
+ if (scoped) return scoped.get(String(objectID).trim().toLowerCase()) ?? [];
96
+ return engine.GetIntegrationObjectFields(objectID);
97
+ }
98
+
99
+ /**
100
+ * Files one stored row under its object, deriving the two legacy names the build reads
101
+ * (`IntegrationObjectID`, `RelatedIntegrationObjectID`) exactly as the engine's projection does.
102
+ */
103
+ function addRow(byObject: ScopedBuildFields, row: StoredFieldRow): void {
104
+ const key = String(row.CompanyIntegrationObjectID).trim().toLowerCase();
105
+ const projected: CompanyIntegrationObjectFieldRow = {
106
+ ...row,
107
+ IntegrationObjectID: row.CompanyIntegrationObjectID,
108
+ RelatedIntegrationObjectID: row.RelatedCompanyIntegrationObjectID ?? null,
109
+ };
110
+ const bucket = byObject.get(key);
111
+ if (bucket) bucket.push(projected);
112
+ else byObject.set(key, [projected]);
113
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Refuses a schema build whose selected objects produced no target table.
3
+ *
4
+ * `buildTargetConfigs` drops a selected object that is not in the source schema, has no fields or
5
+ * has no primary key, and logs each drop — but every execution caller then built and ran a migration
6
+ * anyway. With nothing accepted that migration is a header-only file: RSU wrote it, opened and merged
7
+ * a PR, ran CodeGen, redeployed Explorer, and the sync "completed" over zero entity maps, every signal
8
+ * green. Seen 2026-10-29 on a PropFuel connection: three selections requested, three dropped as
9
+ * notInSchema, three empty migrations committed.
10
+ *
11
+ * A caller that asked for tables and gets none has failed, and should hear so with the names it asked
12
+ * for and why each was dropped — in the error itself, because an API or UI caller cannot read the
13
+ * server log line that used to be the only place the reasons appeared. Preview is deliberately not
14
+ * gated: an empty preview is an honest answer.
15
+ */
16
+ import type { TargetTableConfig } from '@memberjunction/integration-schema-builder';
17
+
18
+ /** Why `buildTargetConfigs` left each selected object out. */
19
+ export interface TargetConfigDrops {
20
+ NotInSchema: string[];
21
+ NoFields: string[];
22
+ NoPrimaryKey: string[];
23
+ }
24
+
25
+ /** How many names one list in the error shows before summarising the rest — the summary log's cap. */
26
+ export const TARGET_CONFIG_NAME_SAMPLE = 5;
27
+
28
+ /** An empty set of drop lists, for `buildTargetConfigs` to fill. */
29
+ export function NewTargetConfigDrops(): TargetConfigDrops {
30
+ return { NotInSchema: [], NoFields: [], NoPrimaryKey: [] };
31
+ }
32
+
33
+ /** `a, b, c, d, e and 163 more` — so a large selection cannot make a multi-KB error. */
34
+ export function SampleNames(names: ReadonlyArray<string>): string {
35
+ const head = names.slice(0, TARGET_CONFIG_NAME_SAMPLE).join(', ');
36
+ const rest = names.length - TARGET_CONFIG_NAME_SAMPLE;
37
+ return rest > 0 ? `${head} and ${rest} more` : head;
38
+ }
39
+
40
+ /**
41
+ * Throws `NO_TARGET_TABLES` when objects were selected and none produced a target table.
42
+ *
43
+ * @param drops why each object was dropped; without it the error lists the three possible reasons.
44
+ * @param alreadyCommitted what the caller had already written before the build, for a caller that
45
+ * cannot honestly say "Nothing was applied" (schema evolution persists the catalog and disables the
46
+ * removed objects' maps first).
47
+ */
48
+ export function AssertTargetConfigs(
49
+ objects: ReadonlyArray<{ SourceObjectName: string }>,
50
+ targetConfigs: ReadonlyArray<TargetTableConfig>,
51
+ sourceType: string,
52
+ drops?: TargetConfigDrops,
53
+ alreadyCommitted?: string,
54
+ ): void {
55
+ if (objects.length === 0 || targetConfigs.length > 0) return;
56
+ const reasons = drops ? describeDrops(drops) : '';
57
+ throw new Error(
58
+ `NO_TARGET_TABLES: none of the ${objects.length} selected object(s) for ${sourceType} produced a target table ` +
59
+ `(${SampleNames(objects.map(o => o.SourceObjectName))}). ` +
60
+ (reasons || 'Each was dropped as not in the source schema, without fields, or without a primary key.') +
61
+ ` ${alreadyCommitted ?? 'Nothing was applied.'} Re-run discovery for this connection and apply again.`
62
+ );
63
+ }
64
+
65
+ function describeDrops(drops: TargetConfigDrops): string {
66
+ const parts: string[] = [];
67
+ if (drops.NotInSchema.length > 0) parts.push(`not in the source schema: ${SampleNames(drops.NotInSchema)}`);
68
+ if (drops.NoFields.length > 0) parts.push(`no fields: ${SampleNames(drops.NoFields)}`);
69
+ if (drops.NoPrimaryKey.length > 0) parts.push(`no primary key: ${SampleNames(drops.NoPrimaryKey)}`);
70
+ return parts.length > 0 ? `Dropped as ${parts.join('; ')}.` : '';
71
+ }
@@ -8,7 +8,7 @@
8
8
  import { IMetadataProvider, LogError, UserInfo } from '@memberjunction/core';
9
9
  import { UUIDsEqual } from '@memberjunction/global';
10
10
  import type { MJAIAgentSessionEntity } from '@memberjunction/core-entities';
11
- import { AuthorizeSessionAccess, LoadSessionConversationScope } from './sessionAccess.js';
11
+ import { AuthorizeLoadedSession } from './sessionAccess.js';
12
12
 
13
13
  const SESSION_ENTITY = 'MJ: AI Agent Sessions';
14
14
 
@@ -54,11 +54,7 @@ export async function LoadOwnedRealtimeSession(
54
54
  );
55
55
  throw new RealtimeSessionOwnershipError(agentSessionId);
56
56
  }
57
- const conversation = await LoadSessionConversationScope(session, contextUser, provider);
58
- const access = AuthorizeSessionAccess(
59
- { SessionUserID: session.UserID, ConversationExternalID: conversation.ExternalID, ConversationReadable: conversation.Readable },
60
- contextUser,
61
- );
57
+ const access = await AuthorizeLoadedSession(session, contextUser, provider);
62
58
  if (!access.Allowed) {
63
59
  LogError(
64
60
  `LoadOwnedRealtimeSession: session access refused (${access.Reason}) — user ${contextUser.ID} attempted to operate on session ${agentSessionId}`,
@@ -199,6 +199,36 @@ export async function LoadSessionConversationScope(
199
199
  return { ExternalID, Readable };
200
200
  }
201
201
 
202
+ /**
203
+ * Decides whether `contextUser` may address an already-loaded `session`: the {@link AuthorizeSessionAccess}
204
+ * decision, reading the session's conversation only when that decision needs it.
205
+ *
206
+ * {@link AuthorizeSessionAccess} decides a principal without a scope identity — a named user with no magic-link
207
+ * scope, the usual signed-in caller — from the owner check alone, and refuses a non-owner before it looks at
208
+ * anything else, so neither needs the conversation read. This function and that one encode the same rule on
209
+ * purpose and live together for that reason: change the rule there, change the read here. The
210
+ * `AuthorizeLoadedSession` table test in `sessionAccess.test.ts` fails if the two ever disagree.
211
+ *
212
+ * @param session - the session, loaded through the caller's own gate
213
+ * @param contextUser - the authenticated caller; the conversation, when read, is read as this user (RLS applies)
214
+ * @param provider - the request-scoped provider
215
+ */
216
+ export async function AuthorizeLoadedSession(
217
+ session: MJAIAgentSessionEntity,
218
+ contextUser: UserInfo,
219
+ provider: IMetadataProvider,
220
+ ): Promise<SessionAccessDecision> {
221
+ const ownsSession = !!contextUser?.ID && !!session.UserID && UUIDsEqual(session.UserID, contextUser.ID);
222
+ if (!ownsSession || !ResolveSessionScopeIdentity(contextUser)) {
223
+ return AuthorizeSessionAccess({ SessionUserID: session.UserID, ConversationExternalID: null, ConversationReadable: false }, contextUser);
224
+ }
225
+ const conversation = await LoadSessionConversationScope(session, contextUser, provider);
226
+ return AuthorizeSessionAccess(
227
+ { SessionUserID: session.UserID, ConversationExternalID: conversation.ExternalID, ConversationReadable: conversation.Readable },
228
+ contextUser,
229
+ );
230
+ }
231
+
202
232
  /**
203
233
  * Reads a conversation's owner and scope id under `contextUser` (so its row-level security applies), for a
204
234
  * caller that addresses the conversation directly rather than through a session — a text agent turn, for
@@ -861,6 +861,7 @@ export class WidgetSessionService {
861
861
  ExtraFilter: `VisitorKey = '${visitorKey}' AND ApplicationID = '${applicationId.replace(/'/g, "''")}'`,
862
862
  OrderBy: '__mj_CreatedAt DESC',
863
863
  MaxRows: 1,
864
+ SkipTotalRowCount: true,
864
865
  Fields: ['ID'],
865
866
  ResultType: 'simple',
866
867
  },
@@ -885,6 +886,7 @@ export class WidgetSessionService {
885
886
  EntityName: WIDGET_ENTITY,
886
887
  ExtraFilter: `PublicKey = '${key.replace(/'/g, "''")}'`,
887
888
  MaxRows: 1,
889
+ SkipTotalRowCount: true,
888
890
  ResultType: 'entity_object',
889
891
  },
890
892
  contextUser,
@@ -79,6 +79,7 @@ export async function ResolveIdentityByEmail(
79
79
  ExtraFilter: `${emailField} = '${trimmed.replace(/'/g, "''")}'`,
80
80
  Fields: pkNames,
81
81
  MaxRows: 1,
82
+ SkipTotalRowCount: true,
82
83
  ResultType: 'simple',
83
84
  },
84
85
  contextUser,
@@ -14,6 +14,7 @@
14
14
 
15
15
  import { BuildSessionClaims, IsRoleGrantable } from '../auth/magicLink/magicLinkCore.js';
16
16
  import type { MagicLinkJWTClaims } from '../auth/magicLink/types.js';
17
+ import { NormalizeOrigin } from '../requestOrigin.js';
17
18
 
18
19
  /**
19
20
  * The magic-link resource-scope `resourceType` used for a widget guest session. Pairs with the
@@ -77,12 +78,12 @@ export function ParseAllowedOrigins(allowedOrigins: string | null | undefined):
77
78
  list = JSON.parse(raw);
78
79
  } catch {
79
80
  // Not JSON — tolerate a CSV form.
80
- return raw.split(',').map((s) => normalizeOrigin(s)).filter((s): s is string => !!s);
81
+ return raw.split(',').map((s) => NormalizeOrigin(s)).filter((s): s is string => !!s);
81
82
  }
82
83
  if (!Array.isArray(list)) {
83
84
  return [];
84
85
  }
85
- return list.filter((s): s is string => typeof s === 'string').map(normalizeOrigin).filter((s): s is string => !!s);
86
+ return list.filter((s): s is string => typeof s === 'string').map(NormalizeOrigin).filter((s): s is string => !!s);
86
87
  }
87
88
 
88
89
  /** @deprecated Use {@link ParseAllowedOrigins}. */
@@ -90,11 +91,6 @@ export function parseAllowedOrigins(allowedOrigins: string | null | undefined):
90
91
  return ParseAllowedOrigins(allowedOrigins);
91
92
  }
92
93
 
93
- /** Lowercases + trims an origin and strips a trailing slash for stable comparison. */
94
- function normalizeOrigin(origin: string): string {
95
- return origin.trim().toLowerCase().replace(/\/+$/, '');
96
- }
97
-
98
94
  /**
99
95
  * Parses the widget's `EnabledChannels` column into a clean list of channel names (Phase 2). Accepts a
100
96
  * JSON array (preferred, e.g. `["Whiteboard"]`) or a comma-separated string (tolerated). Returns an
@@ -129,7 +125,7 @@ export function IsOriginAllowed(requestOrigin: string | null | undefined, allowe
129
125
  if (!requestOrigin || allowedOrigins.length === 0) {
130
126
  return false;
131
127
  }
132
- const candidate = normalizeOrigin(requestOrigin);
128
+ const candidate = NormalizeOrigin(requestOrigin);
133
129
  return allowedOrigins.some((o) => o === candidate);
134
130
  }
135
131
 
@@ -190,6 +190,7 @@ async function loadWidgetById(
190
190
  EntityName: WIDGET_ENTITY,
191
191
  ExtraFilter: `ID = '${widgetId.replace(/'/g, "''")}'`,
192
192
  MaxRows: 1,
193
+ SkipTotalRowCount: true,
193
194
  ResultType: 'entity_object',
194
195
  },
195
196
  elevatedUser,
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Request-origin helpers shared by the GraphQL context (`AppContext.RequestOrigin`) and the realtime
3
+ * widget origin allowlist, so an origin read from a request and an origin read from an allowlist are
4
+ * normalized the same way and compare with plain string equality.
5
+ */
6
+
7
+ /**
8
+ * Shape of a serialized http(s) origin: scheme, host and optional port, with no path, query,
9
+ * fragment or credentials. Applied after {@link NormalizeOrigin}, so it only needs lowercase.
10
+ */
11
+ const HTTP_ORIGIN_PATTERN = /^https?:\/\/[^/?#@\s]+$/;
12
+
13
+ /** Character code of `/`. */
14
+ const SLASH_CHAR_CODE = 47;
15
+
16
+ /**
17
+ * Normalizes an origin for stable comparison: trims whitespace, lowercases, and strips trailing
18
+ * slashes. It does not parse the value or rewrite ports, so `https://a.com:443` stays as written.
19
+ * @param origin an origin as written in a header or an allowlist entry
20
+ */
21
+ export function NormalizeOrigin(origin: string): string {
22
+ return StripTrailingSlashes(origin.trim().toLowerCase());
23
+ }
24
+
25
+ /**
26
+ * Removes trailing `/` characters with a linear scan. A `/\/+$/` regex is quadratic on input with
27
+ * many `/` not at the end, and this runs on a request header the client controls.
28
+ */
29
+ function StripTrailingSlashes(value: string): string {
30
+ let end = value.length;
31
+ while (end > 0 && value.charCodeAt(end - 1) === SLASH_CHAR_CODE) {
32
+ end--;
33
+ }
34
+ return value.slice(0, end);
35
+ }
36
+
37
+ /**
38
+ * Reads a request's `Origin` header into a normalized origin (`scheme://host[:port]`).
39
+ *
40
+ * Returns undefined when the header is absent, repeated, blank, opaque (`null`, which sandboxed
41
+ * frames and privacy-sensitive redirects send), or anything other than a parseable http(s) origin.
42
+ * Ports are kept as sent: browsers omit default ports from `Origin`, and allowlist entries pass
43
+ * through the same {@link NormalizeOrigin}, so both sides agree without rewriting.
44
+ * @param originHeader the raw `Origin` header value from the request
45
+ */
46
+ export function ReadRequestOrigin(originHeader: string | string[] | undefined): string | undefined {
47
+ if (typeof originHeader !== 'string') {
48
+ return undefined;
49
+ }
50
+ const normalized = NormalizeOrigin(originHeader);
51
+ return IsHttpOrigin(normalized) ? normalized : undefined;
52
+ }
53
+
54
+ /** True when a normalized value is a parseable http(s) origin with no path, query, fragment or credentials. */
55
+ function IsHttpOrigin(normalized: string): boolean {
56
+ if (!HTTP_ORIGIN_PATTERN.test(normalized)) {
57
+ return false;
58
+ }
59
+ try {
60
+ return new URL(normalized).host.length > 0;
61
+ } catch {
62
+ return false;
63
+ }
64
+ }
@@ -23,8 +23,12 @@ import {
23
23
  RealtimeSessionOwnershipError,
24
24
  } from '../realtimeSessions/ownedRealtimeSession.js';
25
25
 
26
- /** The `CloseReason` values a client may stamp on its own session; `Janitor` and `Shutdown` are the server's. */
27
- const CLIENT_CLOSE_REASONS: readonly SessionCloseReason[] = ['Explicit', 'Error'];
26
+ /**
27
+ * The `CloseReason` values a client may stamp on its own session; `Idle`, `Janitor` and `Shutdown` are the server's.
28
+ * `TimeLimit` is a claim: the browser's deadline alarm fired, and {@link SessionManager.CloseSession} verifies it
29
+ * against the session's stored deadline, recording `Explicit` instead when the deadline is not near.
30
+ */
31
+ const CLIENT_CLOSE_REASONS: readonly SessionCloseReason[] = ['Explicit', 'Error', 'TimeLimit'];
28
32
 
29
33
  /** Reads the mutation's optional `closeReason`: absent means `Explicit`; a server-only or unknown value is refused. */
30
34
  function ParseClientCloseReason(closeReason: string | null | undefined): SessionCloseReason {
@@ -170,7 +170,8 @@ export class AutotagPipelineResolver extends ResolverBase {
170
170
  EntityName: 'MJ: Content Sources',
171
171
  Fields: ['ID'],
172
172
  ResultType: 'simple',
173
- MaxRows: 1
173
+ MaxRows: 1,
174
+ SkipTotalRowCount: true
174
175
  }, currentUser);
175
176
  if (result.Success && result.Results.length > 0) {
176
177
  sourceID = result.Results[0].ID;
@@ -235,6 +235,7 @@ export class ConversationRunEventsResolver {
235
235
  EntityName: 'MJ: Conversation Details',
236
236
  ExtraFilter: `ID='${this.escape(conversationDetailID)}'`,
237
237
  MaxRows: 1,
238
+ SkipTotalRowCount: true,
238
239
  ResultType: 'entity_object',
239
240
  BypassCache: true,
240
241
  },
@@ -259,6 +260,7 @@ export class ConversationRunEventsResolver {
259
260
  ExtraFilter: `ConversationDetailID='${this.escape(conversationDetailID)}'`,
260
261
  OrderBy: '__mj_CreatedAt DESC',
261
262
  MaxRows: 1,
263
+ SkipTotalRowCount: true,
262
264
  Fields: RUN_FIELDS,
263
265
  ResultType: 'simple',
264
266
  BypassCache: true,
@@ -12,13 +12,13 @@ const REMOTE_OP_PROGRESS_TOPIC = 'RemoteOperationProgress';
12
12
  /** Input for the generic Remote Operation transport mutation. */
13
13
  @InputType()
14
14
  export class ExecuteRemoteOperationInput {
15
- @Field()
15
+ @Field(() => String)
16
16
  operationKey: string; // case-violation-ok-legacy-back-compat: the property name is the GraphQL schema field name — renaming it breaks every client query
17
17
 
18
- @Field()
18
+ @Field(() => String)
19
19
  inputJSON: string; // case-violation-ok-legacy-back-compat: the property name is the GraphQL schema field name — renaming it breaks every client query
20
20
 
21
- @Field()
21
+ @Field(() => String)
22
22
  invokeMode: string; // case-violation-ok-legacy-back-compat: the property name is the GraphQL schema field name — renaming it breaks every client query
23
23
 
24
24
  /**
@@ -28,16 +28,24 @@ export class ExecuteRemoteOperationInput {
28
28
  */
29
29
  @Field(() => String, { nullable: true })
30
30
  progressChannelId?: string; // case-violation-ok-legacy-back-compat: the property name is the GraphQL schema field name — renaming it breaks every client query
31
+
32
+ /**
33
+ * Optional structural contract fingerprint emitted by CodeGen (or supplied by the caller).
34
+ * When provided, the server rejects requests whose fingerprint diverges from the server's
35
+ * active operation contract with `ResultCode = 'CONTRACT_MISMATCH'`.
36
+ */
37
+ @Field(() => String, { nullable: true })
38
+ contractFingerprint?: string; // case-violation-ok-legacy-back-compat: matches camelCase GraphQL input field convention on ExecuteRemoteOperationInput
31
39
  }
32
40
 
33
41
  /** A single streamed progress envelope delivered over the `RemoteOperationProgress` subscription. */
34
42
  @ObjectType()
35
43
  export class RemoteOperationProgressNotification {
36
- @Field()
44
+ @Field(() => String)
37
45
  ChannelId: string;
38
46
 
39
47
  /** JSON-serialized {@link RemoteOpProgress}. */
40
- @Field()
48
+ @Field(() => String)
41
49
  ProgressJSON: string;
42
50
  }
43
51
 
@@ -71,7 +79,7 @@ export class ExecuteRemoteOperationResultType {
71
79
  export class ExecuteRemoteOperationResolver extends ResolverBase {
72
80
  @Mutation(() => ExecuteRemoteOperationResultType)
73
81
  async ExecuteRemoteOperation(
74
- @Arg('input') input: ExecuteRemoteOperationInput,
82
+ @Arg('input', () => ExecuteRemoteOperationInput) input: ExecuteRemoteOperationInput,
75
83
  @Ctx() ctx: AppContext,
76
84
  @PubSub() pubSub: PubSubEngine,
77
85
  ): Promise<ExecuteRemoteOperationResultType> {
@@ -111,9 +119,24 @@ export class ExecuteRemoteOperationResolver extends ResolverBase {
111
119
  return fail(invokability.ResultCode ?? 'OPERATION_UNAVAILABLE', invokability.Reason ?? `Operation '${key}' is unavailable`);
112
120
  }
113
121
 
114
- let parsedInput: unknown;
122
+ // (5) Contract fingerprint gate: when the client sends a contractFingerprint, verify it against
123
+ // the active server operation's ContractFingerprint (or metadata-derived fingerprint) so stale
124
+ // client bundles fail loudly with CONTRACT_MISMATCH rather than mis-deserializing.
125
+ const clientFingerprint = input.contractFingerprint?.trim();
126
+ if (clientFingerprint) {
127
+ const expectedFingerprint =
128
+ opDecl?.ContractFingerprint ?? RemoteOperationEngineBase.Instance.GetContractFingerprint(key);
129
+ if (expectedFingerprint && clientFingerprint !== expectedFingerprint) {
130
+ return fail(
131
+ 'CONTRACT_MISMATCH',
132
+ `Contract fingerprint mismatch for operation '${key}' (client='${clientFingerprint}', server='${expectedFingerprint}')`,
133
+ );
134
+ }
135
+ }
136
+
137
+ let parsedInput: object | string | number | boolean | null;
115
138
  try {
116
- parsedInput = input.inputJSON ? JSON.parse(input.inputJSON) : {};
139
+ parsedInput = input.inputJSON ? (JSON.parse(input.inputJSON) as object | string | number | boolean | null) : {};
117
140
  } catch (e) {
118
141
  return fail('INVALID_INPUT_JSON', `Invalid input JSON: ${e instanceof Error ? e.message : String(e)}`);
119
142
  }
@@ -127,7 +150,16 @@ export class ExecuteRemoteOperationResolver extends ResolverBase {
127
150
  : undefined;
128
151
 
129
152
  // Route through the per-request provider (server in-process path; runs the op's Authorize + InternalExecute).
130
- const result = await provider.RouteOperation(key, parsedInput, { user, mode: input.invokeMode as RemoteOpInvokeMode, onProgress });
153
+ const result = await provider.RouteOperation(key, parsedInput, {
154
+ user,
155
+ mode: input.invokeMode as RemoteOpInvokeMode,
156
+ onProgress,
157
+ contractFingerprint: clientFingerprint,
158
+ requestOrigin: ctx.RequestOrigin,
159
+ clientIp: ctx.ClientIp,
160
+ sessionId: ctx.userPayload?.sessionId,
161
+ apiKeyHash: ctx.userPayload?.apiKeyHash,
162
+ });
131
163
 
132
164
  return {
133
165
  success: result.Success,
@@ -6,6 +6,7 @@ import { QueryCompositionEngine } from '@memberjunction/generic-database-provide
6
6
  import { RequireSystemUser } from '../directives/RequireSystemUser.js';
7
7
  import { NoLog } from '../logging/NoLog.js';
8
8
  import { v4 as uuidv4 } from 'uuid';
9
+ import { createHash, timingSafeEqual } from 'node:crypto';
9
10
  import { GetReadOnlyDataSource, GetReadOnlyProvider } from '../util.js';
10
11
  import { GetDbType } from '../index.js';
11
12
  import { GetSystemUser } from '../auth/index.js';
@@ -125,9 +126,9 @@ export class GetDataResolver {
125
126
  });
126
127
  LogStatus(`GetData invoked: ${input.Queries.length} queries`);
127
128
 
128
- // validate the token
129
+ // validate the token (never echo the raw token into error messages or logs — Token is @NoLog)
129
130
  if (!IsTokenValid(input.Token)) {
130
- throw new Error(`Token ${input.Token} is not valid or has expired`);
131
+ throw new Error('Access token is not valid or has expired');
131
132
  }
132
133
 
133
134
  // Use the read-only connection for executing queries
@@ -296,6 +297,26 @@ export class GetDataAccessToken {
296
297
  const __accessTokens: GetDataAccessToken[] = [];
297
298
  const __defaultTokenLifeSpan = 1000 * 60 * 5; // 5 minutes
298
299
 
300
+ /**
301
+ * Non-short-circuiting constant-time scan over active access tokens: hashes the presented token
302
+ * once and compares against every stored token digest with `timingSafeEqual` without breaking early,
303
+ * so neither prefix similarity, token length, nor array index leaks through timing.
304
+ */
305
+ function findMatchingAccessTokenIndex(token: string): number {
306
+ if (typeof token !== 'string' || token.length === 0) {
307
+ return -1;
308
+ }
309
+ const targetDigest = createHash('sha256').update(token).digest();
310
+ let matchedIndex = -1;
311
+ for (let i = 0; i < __accessTokens.length; i++) {
312
+ const candidateDigest = createHash('sha256').update(__accessTokens[i].Token).digest();
313
+ if (timingSafeEqual(candidateDigest, targetDigest)) {
314
+ matchedIndex = i;
315
+ }
316
+ }
317
+ return matchedIndex;
318
+ }
319
+
299
320
  /**
300
321
  * Removes every token whose `ExpiresAt` has already passed (and its `TokenUses`
301
322
  * log along with it). Without this sweep `__accessTokens` grows forever — nothing
@@ -339,7 +360,7 @@ export function RegisterAccessToken(token?: string, lifeSpan: number = __default
339
360
 
340
361
  if (TokenExists(tokenToUse)) {
341
362
  // should never happen if we used the uuidv4() function but could happen if someone tries to use a custom token
342
- throw new Error(`Token ${tokenToUse} already exists`);
363
+ throw new Error('Access token already exists');
343
364
  }
344
365
 
345
366
  const newToken = new GetDataAccessToken();
@@ -356,12 +377,12 @@ export function registerAccessToken(token?: string, lifeSpan: number = __default
356
377
  return RegisterAccessToken(token, lifeSpan, requestorPayload);
357
378
  }
358
379
  export function DeleteAccessToken(token: string) {
359
- const index = __accessTokens.findIndex((t) => t.Token === token);
380
+ const index = findMatchingAccessTokenIndex(token);
360
381
  if (index >= 0) {
361
382
  __accessTokens.splice(index, 1);
362
383
  }
363
384
  else {
364
- throw new Error(`Token ${token} does not exist`);
385
+ throw new Error('Access token does not exist');
365
386
  }
366
387
  }
367
388
 
@@ -370,7 +391,7 @@ export function deleteAccessToken(token: string) {
370
391
  return DeleteAccessToken(token);
371
392
  }
372
393
  export function TokenExists(token: string) {
373
- return __accessTokens.find((t) => t.Token === token) !== undefined;
394
+ return findMatchingAccessTokenIndex(token) >= 0;
374
395
  }
375
396
 
376
397
  /** @deprecated Use {@link TokenExists}. */
@@ -378,9 +399,9 @@ export function tokenExists(token: string) {
378
399
  return TokenExists(token);
379
400
  }
380
401
  export function IsTokenValid(token: string) {
381
- const t = __accessTokens.find((t) => t.Token === token);
382
- if (t) {
383
- return t.ExpiresAt > new Date();
402
+ const index = findMatchingAccessTokenIndex(token);
403
+ if (index >= 0) {
404
+ return __accessTokens[index].ExpiresAt > new Date();
384
405
  }
385
406
  return false;
386
407
  }
@@ -390,15 +411,16 @@ export function isTokenValid(token: string) {
390
411
  return IsTokenValid(token);
391
412
  }
392
413
  export function RecordTokenUse(token: string, usePayload: any) {
393
- const t = __accessTokens.find((t) => t.Token === token);
394
- if (t) {
414
+ const index = findMatchingAccessTokenIndex(token);
415
+ if (index >= 0) {
416
+ const t = __accessTokens[index];
395
417
  if (!t.TokenUses) {
396
418
  t.TokenUses = [];
397
419
  }
398
- t.TokenUses.push({ Token: token, UsedAt: new Date(), UsePayload: usePayload });
420
+ t.TokenUses.push({ Token: t.Token, UsedAt: new Date(), UsePayload: usePayload });
399
421
  }
400
422
  else {
401
- throw new Error(`Token ${token} does not exist`);
423
+ throw new Error('Access token does not exist');
402
424
  }
403
425
  }
404
426