@memberjunction/server 6.2.0-edge.6 → 6.2.0-edge.8

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 (463) 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/broadcast/BaseCacheInvalidationBroadcastProvider.d.ts.map +1 -1
  68. package/dist/broadcast/BaseCacheInvalidationBroadcastProvider.js +2 -1
  69. package/dist/broadcast/BaseCacheInvalidationBroadcastProvider.js.map +1 -1
  70. package/dist/config.d.ts +906 -8
  71. package/dist/config.d.ts.map +1 -1
  72. package/dist/config.js +59 -0
  73. package/dist/config.js.map +1 -1
  74. package/dist/context.d.ts +2 -0
  75. package/dist/context.d.ts.map +1 -1
  76. package/dist/context.js +41 -12
  77. package/dist/context.js.map +1 -1
  78. package/dist/coreRoutes.d.ts +3 -1
  79. package/dist/coreRoutes.d.ts.map +1 -1
  80. package/dist/coreRoutes.js +6 -4
  81. package/dist/coreRoutes.js.map +1 -1
  82. package/dist/generated/graphql-schemas/__mj.d.ts +1072 -5
  83. package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
  84. package/dist/generated/graphql-schemas/__mj.js +10174 -4618
  85. package/dist/generated/graphql-schemas/__mj.js.map +1 -1
  86. package/dist/generic/OrphanedConversationDetailReconciler.d.ts +11 -0
  87. package/dist/generic/OrphanedConversationDetailReconciler.d.ts.map +1 -1
  88. package/dist/generic/OrphanedConversationDetailReconciler.js +27 -1
  89. package/dist/generic/OrphanedConversationDetailReconciler.js.map +1 -1
  90. package/dist/generic/ResolverBase.d.ts +50 -5
  91. package/dist/generic/ResolverBase.d.ts.map +1 -1
  92. package/dist/generic/ResolverBase.js +329 -132
  93. package/dist/generic/ResolverBase.js.map +1 -1
  94. package/dist/generic/RunViewResolver.d.ts +4 -0
  95. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  96. package/dist/generic/RunViewResolver.js +47 -7
  97. package/dist/generic/RunViewResolver.js.map +1 -1
  98. package/dist/globalCors.d.ts.map +1 -1
  99. package/dist/globalCors.js +1 -0
  100. package/dist/globalCors.js.map +1 -1
  101. package/dist/index.d.ts +3 -0
  102. package/dist/index.d.ts.map +1 -1
  103. package/dist/index.js +201 -43
  104. package/dist/index.js.map +1 -1
  105. package/dist/integration/ConnectionPause.d.ts +121 -0
  106. package/dist/integration/ConnectionPause.d.ts.map +1 -0
  107. package/dist/integration/ConnectionPause.js +183 -0
  108. package/dist/integration/ConnectionPause.js.map +1 -0
  109. package/dist/integration/CustomColumnPromoter.d.ts +72 -6
  110. package/dist/integration/CustomColumnPromoter.d.ts.map +1 -1
  111. package/dist/integration/CustomColumnPromoter.js +87 -46
  112. package/dist/integration/CustomColumnPromoter.js.map +1 -1
  113. package/dist/integration/EntityMapLifecycle.d.ts +78 -4
  114. package/dist/integration/EntityMapLifecycle.d.ts.map +1 -1
  115. package/dist/integration/EntityMapLifecycle.js +199 -5
  116. package/dist/integration/EntityMapLifecycle.js.map +1 -1
  117. package/dist/integration/InactiveRowWarnings.d.ts +31 -0
  118. package/dist/integration/InactiveRowWarnings.d.ts.map +1 -1
  119. package/dist/integration/InactiveRowWarnings.js +29 -0
  120. package/dist/integration/InactiveRowWarnings.js.map +1 -1
  121. package/dist/integration/ObjectNamesByID.d.ts +28 -0
  122. package/dist/integration/ObjectNamesByID.d.ts.map +1 -0
  123. package/dist/integration/ObjectNamesByID.js +87 -0
  124. package/dist/integration/ObjectNamesByID.js.map +1 -0
  125. package/dist/integration/PostRestartIntrospect.d.ts +26 -0
  126. package/dist/integration/PostRestartIntrospect.d.ts.map +1 -0
  127. package/dist/integration/PostRestartIntrospect.js +27 -0
  128. package/dist/integration/PostRestartIntrospect.js.map +1 -0
  129. package/dist/integration/RSUPostRestartProgress.d.ts +176 -0
  130. package/dist/integration/RSUPostRestartProgress.d.ts.map +1 -0
  131. package/dist/integration/RSUPostRestartProgress.js +323 -0
  132. package/dist/integration/RSUPostRestartProgress.js.map +1 -0
  133. package/dist/integration/RSUProgressBridge.d.ts +41 -2
  134. package/dist/integration/RSUProgressBridge.d.ts.map +1 -1
  135. package/dist/integration/RSUProgressBridge.js +84 -1
  136. package/dist/integration/RSUProgressBridge.js.map +1 -1
  137. package/dist/integration/RunArtifactAuthorization.d.ts +39 -0
  138. package/dist/integration/RunArtifactAuthorization.d.ts.map +1 -0
  139. package/dist/integration/RunArtifactAuthorization.js +45 -0
  140. package/dist/integration/RunArtifactAuthorization.js.map +1 -0
  141. package/dist/integration/SchemaRefreshLaunch.d.ts +9 -0
  142. package/dist/integration/SchemaRefreshLaunch.d.ts.map +1 -1
  143. package/dist/integration/SchemaRefreshLaunch.js +3 -2
  144. package/dist/integration/SchemaRefreshLaunch.js.map +1 -1
  145. package/dist/integration/ScopedFieldsForBuild.d.ts +46 -0
  146. package/dist/integration/ScopedFieldsForBuild.d.ts.map +1 -0
  147. package/dist/integration/ScopedFieldsForBuild.js +91 -0
  148. package/dist/integration/ScopedFieldsForBuild.js.map +1 -0
  149. package/dist/integration/TargetConfigGuard.d.ts +40 -0
  150. package/dist/integration/TargetConfigGuard.d.ts.map +1 -0
  151. package/dist/integration/TargetConfigGuard.js +40 -0
  152. package/dist/integration/TargetConfigGuard.js.map +1 -0
  153. package/dist/logging/StartupLogger.d.ts +50 -0
  154. package/dist/logging/StartupLogger.d.ts.map +1 -1
  155. package/dist/logging/StartupLogger.js +15 -22
  156. package/dist/logging/StartupLogger.js.map +1 -1
  157. package/dist/middleware/BaseServerMiddleware.d.ts +21 -3
  158. package/dist/middleware/BaseServerMiddleware.d.ts.map +1 -1
  159. package/dist/middleware/BaseServerMiddleware.js +4 -2
  160. package/dist/middleware/BaseServerMiddleware.js.map +1 -1
  161. package/dist/realtimeSessions/ownedRealtimeSession.d.ts.map +1 -1
  162. package/dist/realtimeSessions/ownedRealtimeSession.js +2 -3
  163. package/dist/realtimeSessions/ownedRealtimeSession.js.map +1 -1
  164. package/dist/realtimeSessions/sessionAccess.d.ts +15 -0
  165. package/dist/realtimeSessions/sessionAccess.d.ts.map +1 -1
  166. package/dist/realtimeSessions/sessionAccess.js +22 -0
  167. package/dist/realtimeSessions/sessionAccess.js.map +1 -1
  168. package/dist/realtimeWidget/WidgetSessionService.d.ts.map +1 -1
  169. package/dist/realtimeWidget/WidgetSessionService.js +2 -0
  170. package/dist/realtimeWidget/WidgetSessionService.js.map +1 -1
  171. package/dist/realtimeWidget/visitorIdentity.d.ts.map +1 -1
  172. package/dist/realtimeWidget/visitorIdentity.js +1 -0
  173. package/dist/realtimeWidget/visitorIdentity.js.map +1 -1
  174. package/dist/realtimeWidget/widgetCore.d.ts.map +1 -1
  175. package/dist/realtimeWidget/widgetCore.js +4 -7
  176. package/dist/realtimeWidget/widgetCore.js.map +1 -1
  177. package/dist/realtimeWidget/widgetGuestElevation.js +1 -0
  178. package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
  179. package/dist/requestOrigin.d.ts +22 -0
  180. package/dist/requestOrigin.d.ts.map +1 -0
  181. package/dist/requestOrigin.js +60 -0
  182. package/dist/requestOrigin.js.map +1 -0
  183. package/dist/resolvers/AgentRunStatusPublisher.d.ts +10 -0
  184. package/dist/resolvers/AgentRunStatusPublisher.d.ts.map +1 -1
  185. package/dist/resolvers/AgentRunStatusPublisher.js +18 -2
  186. package/dist/resolvers/AgentRunStatusPublisher.js.map +1 -1
  187. package/dist/resolvers/AgentSessionResolver.d.ts.map +1 -1
  188. package/dist/resolvers/AgentSessionResolver.js +6 -2
  189. package/dist/resolvers/AgentSessionResolver.js.map +1 -1
  190. package/dist/resolvers/AutotagPipelineResolver.d.ts +1 -1
  191. package/dist/resolvers/AutotagPipelineResolver.d.ts.map +1 -1
  192. package/dist/resolvers/AutotagPipelineResolver.js +61 -16
  193. package/dist/resolvers/AutotagPipelineResolver.js.map +1 -1
  194. package/dist/resolvers/ConversationRunEventsResolver.d.ts.map +1 -1
  195. package/dist/resolvers/ConversationRunEventsResolver.js +2 -0
  196. package/dist/resolvers/ConversationRunEventsResolver.js.map +1 -1
  197. package/dist/resolvers/ExecuteRemoteOperationResolver.d.ts +6 -0
  198. package/dist/resolvers/ExecuteRemoteOperationResolver.d.ts.map +1 -1
  199. package/dist/resolvers/ExecuteRemoteOperationResolver.js +30 -7
  200. package/dist/resolvers/ExecuteRemoteOperationResolver.js.map +1 -1
  201. package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
  202. package/dist/resolvers/GetDataResolver.js +34 -13
  203. package/dist/resolvers/GetDataResolver.js.map +1 -1
  204. package/dist/resolvers/InfoResolver.js +1 -1
  205. package/dist/resolvers/InfoResolver.js.map +1 -1
  206. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +401 -11
  207. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
  208. package/dist/resolvers/IntegrationDiscoveryResolver.js +2690 -1305
  209. package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
  210. package/dist/resolvers/MCPResolver.d.ts.map +1 -1
  211. package/dist/resolvers/MCPResolver.js +13 -2
  212. package/dist/resolvers/MCPResolver.js.map +1 -1
  213. package/dist/resolvers/QuerySystemUserResolver.d.ts +38 -1
  214. package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
  215. package/dist/resolvers/QuerySystemUserResolver.js +181 -88
  216. package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
  217. package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
  218. package/dist/resolvers/RealtimeBridgeResolver.js +1 -0
  219. package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
  220. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +6 -0
  221. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  222. package/dist/resolvers/RealtimeClientSessionResolver.js +36 -5
  223. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  224. package/dist/resolvers/RemoteBrowserActionResolver.d.ts +62 -10
  225. package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
  226. package/dist/resolvers/RemoteBrowserActionResolver.js +217 -34
  227. package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
  228. package/dist/resolvers/RunAIAgentResolver.d.ts +24 -8
  229. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  230. package/dist/resolvers/RunAIAgentResolver.js +185 -67
  231. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  232. package/dist/resolvers/RunAIPromptResolver.d.ts +5 -3
  233. package/dist/resolvers/RunAIPromptResolver.d.ts.map +1 -1
  234. package/dist/resolvers/RunAIPromptResolver.js +19 -18
  235. package/dist/resolvers/RunAIPromptResolver.js.map +1 -1
  236. package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
  237. package/dist/resolvers/RunTemplateResolver.js +3 -1
  238. package/dist/resolvers/RunTemplateResolver.js.map +1 -1
  239. package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
  240. package/dist/resolvers/SqlLoggingConfigResolver.js +7 -8
  241. package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
  242. package/dist/resolvers/TagGovernanceResolver.d.ts +12 -0
  243. package/dist/resolvers/TagGovernanceResolver.d.ts.map +1 -1
  244. package/dist/resolvers/TagGovernanceResolver.js +84 -0
  245. package/dist/resolvers/TagGovernanceResolver.js.map +1 -1
  246. package/dist/resolvers/TelemetryResolver.d.ts +13 -8
  247. package/dist/resolvers/TelemetryResolver.d.ts.map +1 -1
  248. package/dist/resolvers/TelemetryResolver.js +40 -9
  249. package/dist/resolvers/TelemetryResolver.js.map +1 -1
  250. package/dist/resolvers/meetingRecordingRegistration.d.ts.map +1 -1
  251. package/dist/resolvers/meetingRecordingRegistration.js +2 -0
  252. package/dist/resolvers/meetingRecordingRegistration.js.map +1 -1
  253. package/dist/resolvers/meetings/meetingMutations.d.ts.map +1 -1
  254. package/dist/resolvers/meetings/meetingMutations.js +34 -4
  255. package/dist/resolvers/meetings/meetingMutations.js.map +1 -1
  256. package/dist/rest/MediaAccessKeys.d.ts.map +1 -1
  257. package/dist/rest/MediaAccessKeys.js +6 -2
  258. package/dist/rest/MediaAccessKeys.js.map +1 -1
  259. package/dist/rest/OAuthCallbackHandler.d.ts.map +1 -1
  260. package/dist/rest/OAuthCallbackHandler.js +11 -5
  261. package/dist/rest/OAuthCallbackHandler.js.map +1 -1
  262. package/dist/rest/RealtimeRecordingHandler.d.ts +3 -3
  263. package/dist/rest/RealtimeRecordingHandler.d.ts.map +1 -1
  264. package/dist/rest/RealtimeRecordingHandler.js +2 -4
  265. package/dist/rest/RealtimeRecordingHandler.js.map +1 -1
  266. package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
  267. package/dist/rest/ViewOperationsHandler.js +2 -1
  268. package/dist/rest/ViewOperationsHandler.js.map +1 -1
  269. package/dist/rest/setupRESTEndpoints.d.ts +3 -1
  270. package/dist/rest/setupRESTEndpoints.d.ts.map +1 -1
  271. package/dist/rest/setupRESTEndpoints.js +11 -2
  272. package/dist/rest/setupRESTEndpoints.js.map +1 -1
  273. package/dist/services/DurableEntityActionTaskSubmitter.d.ts.map +1 -1
  274. package/dist/services/DurableEntityActionTaskSubmitter.js +6 -1
  275. package/dist/services/DurableEntityActionTaskSubmitter.js.map +1 -1
  276. package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
  277. package/dist/services/TaskGraphActionRunner.js +2 -26
  278. package/dist/services/TaskGraphActionRunner.js.map +1 -1
  279. package/dist/services/TaskGraphAgentRunner.d.ts +6 -0
  280. package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -1
  281. package/dist/services/TaskGraphAgentRunner.js +38 -0
  282. package/dist/services/TaskGraphAgentRunner.js.map +1 -1
  283. package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
  284. package/dist/services/TaskGraphContinuationDeliverer.js +1 -0
  285. package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
  286. package/dist/services/WorkQueueHostService.d.ts +2 -1
  287. package/dist/services/WorkQueueHostService.d.ts.map +1 -1
  288. package/dist/services/WorkQueueHostService.js +19 -4
  289. package/dist/services/WorkQueueHostService.js.map +1 -1
  290. package/dist/services/workQueueConfig.d.ts +2 -2
  291. package/dist/services/workQueueConfig.js +5 -5
  292. package/dist/services/workQueueConfig.js.map +1 -1
  293. package/dist/sharedCache.js +1 -1
  294. package/dist/sharedCache.js.map +1 -1
  295. package/dist/types.d.ts +17 -0
  296. package/dist/types.d.ts.map +1 -1
  297. package/dist/types.js.map +1 -1
  298. package/dist/util.d.ts.map +1 -1
  299. package/dist/util.js +55 -2
  300. package/dist/util.js.map +1 -1
  301. package/package.json +107 -100
  302. package/src/__tests__/APIKeyScopeAuth.test.ts +4 -8
  303. package/src/__tests__/ActiveOperations.test.ts +68 -0
  304. package/src/__tests__/AgentSessionResolver.ownership.test.ts +13 -0
  305. package/src/__tests__/AssertTargetConfigs.test.ts +75 -0
  306. package/src/__tests__/BatchApplyParity.test.ts +109 -0
  307. package/src/__tests__/BridgeHostInstance.test.ts +42 -0
  308. package/src/__tests__/CollectInactiveRowWarnings.test.ts +115 -0
  309. package/src/__tests__/ConnectionPause.test.ts +192 -0
  310. package/src/__tests__/ConnectionPauseResolver.test.ts +166 -0
  311. package/src/__tests__/ConnectionPauseSave.test.ts +143 -0
  312. package/src/__tests__/CustomColumnPromoterCompletion.test.ts +142 -0
  313. package/src/__tests__/DeleteConnectionRemovesPerConnectionCatalog.test.ts +51 -0
  314. package/src/__tests__/DetachedDiscoveryStart.test.ts +65 -0
  315. package/src/__tests__/DurableEntityActionTaskSubmitter.test.ts +73 -0
  316. package/src/__tests__/EntityMapLifecycle.rekey.test.ts +252 -0
  317. package/src/__tests__/ExecuteRemoteOperationResolver.test.ts +213 -0
  318. package/src/__tests__/HandoffOfferExpiry.test.ts +40 -0
  319. package/src/__tests__/IntegrationRunArtifactAuthorization.test.ts +126 -0
  320. package/src/__tests__/MagicLinkKeys.test.ts +102 -0
  321. package/src/__tests__/MediaAccessKeys.test.ts +57 -10
  322. package/src/__tests__/NewArrivalsDefaultDisabled.test.ts +50 -0
  323. package/src/__tests__/ObjectNamesByID.test.ts +176 -0
  324. package/src/__tests__/PostRestartMapsInCatalogScope.test.ts +146 -0
  325. package/src/__tests__/QuerySystemUserResolver.childRows.test.ts +80 -0
  326. package/src/__tests__/QuerySystemUserResolver.collision.test.ts +200 -0
  327. package/src/__tests__/QuerySystemUserResolver.permissions.test.ts +132 -0
  328. package/src/__tests__/RSUPostRestartProgress.test.ts +343 -0
  329. package/src/__tests__/RSUProgressBridge.test.ts +188 -5
  330. package/src/__tests__/RSUStartupSweepWiring.test.ts +30 -0
  331. package/src/__tests__/ReactivateRefreshMessage.test.ts +10 -3
  332. package/src/__tests__/RealtimeClientSessionResolver.test.ts +22 -0
  333. package/src/__tests__/{RefreshAdoptsNewColumns.test.ts → RefreshReportsNewColumns.test.ts} +25 -16
  334. package/src/__tests__/RemoteBrowserActionRecovery.test.ts +287 -0
  335. package/src/__tests__/RemoteBrowserActionResolver.goalRunLink.test.ts +3 -0
  336. package/src/__tests__/RemoteBrowserActionResolver.ownership.test.ts +6 -4
  337. package/src/__tests__/RemoteBrowserAudioStream.test.ts +3 -0
  338. package/src/__tests__/RemoteBrowserSnapshot.test.ts +3 -0
  339. package/src/__tests__/ResolverBase.clientOldValues.test.ts +327 -26
  340. package/src/__tests__/ResolverBase.diagnosticSenderRegistrations.test.ts +26 -0
  341. package/src/__tests__/ResolverBase.encryptedApiZeroReencrypt.test.ts +307 -0
  342. package/src/__tests__/ResolverBase.entityMessageRelay.test.ts +53 -2
  343. package/src/__tests__/ResolverBase.includeBinaryFields.test.ts +60 -0
  344. package/src/__tests__/RunAIAgentResolver.branch.test.ts +4 -0
  345. package/src/__tests__/RunAIAgentResolver.consultPredecessor.test.ts +289 -0
  346. package/src/__tests__/RunAIAgentResolver.handoff.test.ts +649 -0
  347. package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +2 -0
  348. package/src/__tests__/RunAIAgentResolver.runDataGuard.test.ts +2 -0
  349. package/src/__tests__/RunAIAgentResolver.stoppedPredecessor.test.ts +87 -12
  350. package/src/__tests__/RunAIAgentResolver.streaming.test.ts +28 -4
  351. package/src/__tests__/SchemaEvolutionCustomColumns.test.ts +119 -0
  352. package/src/__tests__/SchemaRefreshLaunch.test.ts +24 -2
  353. package/src/__tests__/ScopedFieldsForBuild.test.ts +192 -0
  354. package/src/__tests__/SessionJanitor.test.ts +195 -4
  355. package/src/__tests__/SessionManager.test.ts +69 -0
  356. package/src/__tests__/TagGovernanceAndAutotagResolvers.test.ts +261 -0
  357. package/src/__tests__/TagGovernanceResolver.test.ts +60 -2
  358. package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +2 -2
  359. package/src/__tests__/TaskGraphAgentRunner.test.ts +272 -16
  360. package/src/__tests__/ViewOperationsHandler.test.ts +2 -5
  361. package/src/__tests__/WorkQueueHostService.test.ts +25 -7
  362. package/src/__tests__/backgroundSweepConfigUnits.test.ts +153 -0
  363. package/src/__tests__/contextRequestOrigin.test.ts +105 -0
  364. package/src/__tests__/conversationBrowsers.test.ts +62 -0
  365. package/src/__tests__/globalCors.test.ts +12 -0
  366. package/src/__tests__/magicLinkExtensionCapabilities.test.ts +413 -0
  367. package/src/__tests__/orphanedConversationDetailReconciler.start.test.ts +82 -0
  368. package/src/__tests__/ownedRealtimeSession.test.ts +14 -0
  369. package/src/__tests__/realtimeRecordingHandler.test.ts +7 -2
  370. package/src/__tests__/realtimeSessions/sessionAccess.test.ts +79 -1
  371. package/src/__tests__/requestOrigin.test.ts +104 -0
  372. package/src/__tests__/review-capture-valid-token-log.test.ts +98 -0
  373. package/src/__tests__/runStreams.fieldSecurity.test.ts +5 -5
  374. package/src/__tests__/securityHardeningPr6034.test.ts +120 -0
  375. package/src/__tests__/sessionDeadline.test.ts +51 -0
  376. package/src/__tests__/sharedCache.test.ts +11 -0
  377. package/src/__tests__/unifiedAuth.test.ts +73 -1
  378. package/src/agentSessions/BridgeHostInstance.ts +20 -5
  379. package/src/agentSessions/HandoffOfferExpiry.ts +18 -0
  380. package/src/agentSessions/ReturningVisitorRecap.ts +1 -0
  381. package/src/agentSessions/SessionJanitor.ts +92 -42
  382. package/src/agentSessions/SessionManager.ts +38 -8
  383. package/src/agentSessions/conversationBrowsers.ts +143 -0
  384. package/src/agentSessions/sessionDeadline.ts +81 -0
  385. package/src/auth/APIKeyScopeAuth.ts +5 -4
  386. package/src/auth/__tests__/loginEmailResolution.test.ts +67 -0
  387. package/src/auth/loginEmailResolution.ts +67 -0
  388. package/src/auth/magicLink/MagicLinkKeys.ts +146 -14
  389. package/src/auth/magicLink/MagicLinkRouter.ts +33 -6
  390. package/src/auth/magicLink/MagicLinkService.ts +250 -10
  391. package/src/auth/magicLink/index.ts +1 -1
  392. package/src/auth/magicLink/redeemLanding.ts +7 -4
  393. package/src/auth/magicLink/types.ts +28 -3
  394. package/src/auth/newUsers.ts +1 -0
  395. package/src/backgroundSweepConfigUnits.ts +213 -0
  396. package/src/broadcast/BaseCacheInvalidationBroadcastProvider.ts +2 -1
  397. package/src/config.ts +69 -0
  398. package/src/context.ts +46 -15
  399. package/src/coreRoutes.ts +14 -7
  400. package/src/generated/graphql-schemas/__mj.ts +10360 -6479
  401. package/src/generic/OrphanedConversationDetailReconciler.ts +32 -1
  402. package/src/generic/ResolverBase.ts +422 -143
  403. package/src/generic/RunViewResolver.ts +96 -49
  404. package/src/globalCors.ts +1 -0
  405. package/src/index.ts +246 -44
  406. package/src/integration/ConnectionPause.ts +222 -0
  407. package/src/integration/CustomColumnPromoter.ts +123 -53
  408. package/src/integration/EntityMapLifecycle.ts +229 -6
  409. package/src/integration/InactiveRowWarnings.ts +50 -0
  410. package/src/integration/ObjectNamesByID.ts +97 -0
  411. package/src/integration/PostRestartIntrospect.ts +49 -0
  412. package/src/integration/RSUPostRestartProgress.ts +359 -0
  413. package/src/integration/RSUProgressBridge.ts +85 -2
  414. package/src/integration/RunArtifactAuthorization.ts +65 -0
  415. package/src/integration/SchemaRefreshLaunch.ts +12 -2
  416. package/src/integration/ScopedFieldsForBuild.ts +113 -0
  417. package/src/integration/TargetConfigGuard.ts +71 -0
  418. package/src/logging/StartupLogger.ts +24 -0
  419. package/src/middleware/BaseServerMiddleware.ts +20 -3
  420. package/src/realtimeSessions/ownedRealtimeSession.ts +2 -6
  421. package/src/realtimeSessions/sessionAccess.ts +30 -0
  422. package/src/realtimeWidget/WidgetSessionService.ts +2 -0
  423. package/src/realtimeWidget/visitorIdentity.ts +1 -0
  424. package/src/realtimeWidget/widgetCore.ts +4 -8
  425. package/src/realtimeWidget/widgetGuestElevation.ts +1 -0
  426. package/src/requestOrigin.ts +64 -0
  427. package/src/resolvers/AgentRunStatusPublisher.ts +27 -2
  428. package/src/resolvers/AgentSessionResolver.ts +6 -2
  429. package/src/resolvers/AutotagPipelineResolver.ts +59 -14
  430. package/src/resolvers/ConversationRunEventsResolver.ts +2 -0
  431. package/src/resolvers/ExecuteRemoteOperationResolver.ts +41 -9
  432. package/src/resolvers/GetDataResolver.ts +35 -13
  433. package/src/resolvers/InfoResolver.ts +1 -1
  434. package/src/resolvers/IntegrationDiscoveryResolver.ts +1522 -203
  435. package/src/resolvers/MCPResolver.ts +15 -2
  436. package/src/resolvers/QuerySystemUserResolver.ts +211 -100
  437. package/src/resolvers/RealtimeBridgeResolver.ts +1 -0
  438. package/src/resolvers/RealtimeClientSessionResolver.ts +40 -7
  439. package/src/resolvers/RemoteBrowserActionResolver.ts +242 -32
  440. package/src/resolvers/RunAIAgentResolver.ts +265 -86
  441. package/src/resolvers/RunAIPromptResolver.ts +18 -18
  442. package/src/resolvers/RunTemplateResolver.ts +3 -1
  443. package/src/resolvers/SqlLoggingConfigResolver.ts +7 -9
  444. package/src/resolvers/TagGovernanceResolver.ts +65 -0
  445. package/src/resolvers/TelemetryResolver.ts +44 -9
  446. package/src/resolvers/__tests__/GetDataResolver.tokens.test.ts +52 -0
  447. package/src/resolvers/meetingRecordingRegistration.ts +2 -0
  448. package/src/resolvers/meetings/meetingMutations.ts +37 -7
  449. package/src/rest/MediaAccessKeys.ts +6 -2
  450. package/src/rest/OAuthCallbackHandler.ts +13 -5
  451. package/src/rest/RealtimeRecordingHandler.ts +5 -9
  452. package/src/rest/ViewOperationsHandler.ts +2 -1
  453. package/src/rest/setupRESTEndpoints.ts +11 -2
  454. package/src/services/DurableEntityActionTaskSubmitter.ts +10 -2
  455. package/src/services/TaskGraphActionRunner.ts +2 -23
  456. package/src/services/TaskGraphAgentRunner.ts +43 -0
  457. package/src/services/TaskGraphContinuationDeliverer.ts +1 -0
  458. package/src/services/WorkQueueHostService.ts +26 -4
  459. package/src/services/workQueueConfig.ts +5 -5
  460. package/src/sharedCache.ts +1 -1
  461. package/src/types.ts +18 -0
  462. package/src/util.ts +55 -3
  463. package/src/services/TaskOrchestration-Integration.md +0 -188
@@ -19,6 +19,8 @@ import {
19
19
  } from '@memberjunction/ai-agents';
20
20
  import { GetCurrentHostInstance, GetHostInstanceID, HostInstanceIdentity } from './HostInstance.js';
21
21
  import { SessionManager, SessionCloseReason } from './SessionManager.js';
22
+ import type { SessionJanitorSettings } from '../backgroundSweepConfigUnits.js';
23
+ import { ExpiryCloseReason, ExpiryReason } from './sessionDeadline.js';
22
24
 
23
25
  /** Entity name for session rows (kept in sync with {@link SessionManager}). */
24
26
  const SESSION_ENTITY = 'MJ: AI Agent Sessions';
@@ -195,6 +197,24 @@ interface RecoveryVisit {
195
197
  Recovered: boolean;
196
198
  }
197
199
 
200
+ /**
201
+ * The close reason for a row the staleness sweep's SQL already judged stale (`LastActiveAt < cutoff`).
202
+ * {@link ExpiryCloseReason} returns null for such a row only when `LastActiveAt` will not parse (or the
203
+ * threshold is non-finite); skipping it would leave it `Active` forever, so it closes as `'Idle'` and the
204
+ * fallback is logged with the session ID.
205
+ */
206
+ function staleRowCloseReason(session: MJAIAgentSessionEntity, nowMs: number, thresholdMs: number): ExpiryReason {
207
+ const reason = ExpiryCloseReason(session, nowMs, thresholdMs);
208
+ if (reason !== null) {
209
+ return reason;
210
+ }
211
+ LogError(
212
+ `[SessionJanitor] Staleness sweep: session ${session.ID} has LastActiveAt ${String(session.LastActiveAt)}; ` +
213
+ 'LastActiveAt unparseable; closing as Idle',
214
+ );
215
+ return 'Idle';
216
+ }
217
+
198
218
  /**
199
219
  * `BaseSingleton` background reconciler that keeps the durable `AIAgentSession` state from drifting
200
220
  * away from volatile process reality. A crash/redeploy vaporizes a host's in-memory sockets but
@@ -212,9 +232,12 @@ interface RecoveryVisit {
212
232
  * `closeThresholdMinutes`, regardless of host. Catches sessions whose owner died without a clean
213
233
  * reboot (OOM, scaled-down pod).
214
234
  * 3. **Max-duration sweep ({@link RunMaxDurationSweep})** — run on every periodic tick after the
215
- * staleness sweep. Closes any session past its stored hard deadline (the public voice cost cap).
235
+ * staleness sweep. Closes any session past its stored hard deadline (the public voice cost cap), and
236
+ * any deadline-carrying session whose idle moment has passed; it stamps that row `'Idle'` when the idle
237
+ * moment came before the deadline.
216
238
  *
217
- * The close sweeps stamp `CloseReason = 'Janitor'`. A shutdown-time path — {@link RunShutdownDrain},
239
+ * Startup recovery stamps `CloseReason = 'Janitor'`; the staleness and max-duration sweeps stamp `'Idle'` or
240
+ * `'TimeLimit'`, whichever expiry moment passed first ({@link ExpiryCloseReason}). A shutdown-time path — {@link RunShutdownDrain},
218
241
  * invoked from {@link Shutdown} during the graceful ShutdownRegistry drain — closes this exact host
219
242
  * instance's own live sessions with `CloseReason = 'Shutdown'`.
220
243
  *
@@ -318,6 +341,39 @@ export class SessionJanitor extends BaseSingleton<SessionJanitor> implements ISh
318
341
  this.scheduleSweep();
319
342
  }
320
343
 
344
+ /**
345
+ * Starts the janitor the way the host's `agentSessions.janitor` settings ask: applies the interval
346
+ * and close threshold, then {@link Start} when enabled, or {@link StartRecoveryOnly} when not.
347
+ * Throws (before doing any work) on an invalid interval or threshold; see {@link Configure}.
348
+ */
349
+ public async StartWithSettings(settings: SessionJanitorSettings, provider: DatabaseProviderBase, systemUser: UserInfo): Promise<void> {
350
+ this.Configure({ sweepIntervalMs: settings.sweepIntervalMs, closeThresholdMinutes: settings.closeThresholdMinutes });
351
+ if (settings.enabled) {
352
+ await this.Start(provider, systemUser);
353
+ } else {
354
+ await this.StartRecoveryOnly(provider, systemUser);
355
+ }
356
+ }
357
+
358
+ /**
359
+ * {@link Start} without the periodic tick, for hosts that turned the janitor off: run own-host
360
+ * startup recovery once and register for the graceful-shutdown drain, but schedule nothing, so an
361
+ * idle process issues no periodic queries. Stale-session, max-duration and recording-recovery
362
+ * sweeps then do not run in this process. Stops a tick an earlier {@link Start} scheduled.
363
+ */
364
+ public async StartRecoveryOnly(provider: DatabaseProviderBase, systemUser: UserInfo): Promise<void> {
365
+ this.Stop();
366
+ this._provider = provider;
367
+ this._systemUser = systemUser;
368
+ this.ensureRegistered();
369
+ await this.RunStartupRecovery(provider, systemUser);
370
+ }
371
+
372
+ /** Whether the periodic tick is currently scheduled (diagnostics and tests). */
373
+ public get IsSweepScheduled(): boolean {
374
+ return this._sweepTimer !== null;
375
+ }
376
+
321
377
  /**
322
378
  * Stops the periodic tick and keeps any new recording recovery pass from starting until the next
323
379
  * {@link Start}, including one a tick already in progress would have started. A pass that is already
@@ -335,7 +391,7 @@ export class SessionJanitor extends BaseSingleton<SessionJanitor> implements ISh
335
391
  * {@link IShutdownable}: stop the timer, then drain this host's own live sessions so a
336
392
  * graceful stop never strands `Active`/`Idle` rows for the next boot's janitor to mop up, then wait
337
393
  * (at most {@link RECOVERY_SHUTDOWN_WAIT_MS}) for an in-flight recording recovery pass.
338
- * Drained sessions are stamped `CloseReason = 'Shutdown'` (vs. `'Janitor'` for crash orphans),
394
+ * Drained sessions are stamped `CloseReason = 'Shutdown'` (vs. `'Janitor'` for crash orphans from an earlier boot),
339
395
  * so the dashboards can tell a clean redeploy from a reconciled crash. Never throws; the drain
340
396
  * is skipped when {@link Start} was never called (no captured provider/user).
341
397
  */
@@ -360,7 +416,7 @@ export class SessionJanitor extends BaseSingleton<SessionJanitor> implements ISh
360
416
  public async RunShutdownDrain(provider: IMetadataProvider, systemUser: UserInfo): Promise<number> {
361
417
  const current = GetHostInstanceID().replace(/'/g, "''");
362
418
  const filter = `Status IN ('Active','Idle') AND HostInstanceID = '${current}'`;
363
- const closed = await this.sweepAndClose(filter, provider, systemUser, 'Shutdown');
419
+ const closed = await this.sweepAndClose(filter, provider, systemUser, () => 'Shutdown');
364
420
  if (closed > 0) {
365
421
  LogStatus(`[SessionJanitor] Shutdown drain closed ${closed} live session(s) owned by this host instance`);
366
422
  }
@@ -389,8 +445,9 @@ export class SessionJanitor extends BaseSingleton<SessionJanitor> implements ISh
389
445
  `AND HostInstanceID LIKE '${prefix}%' ` +
390
446
  `AND HostInstanceID <> '${current}'`;
391
447
  // The LIKE narrows the read; the exact check decides, since LIKE reads a `_` in a host name as any character.
392
- const isPriorBoot = (session: MJAIAgentSessionEntity): boolean => hostInstance.IsPriorBoot(session.HostInstanceID);
393
- const closed = await this.sweepAndClose(filter, provider, systemUser, 'Janitor', isPriorBoot);
448
+ const reasonFor = (session: MJAIAgentSessionEntity): SessionCloseReason | null =>
449
+ hostInstance.IsPriorBoot(session.HostInstanceID) ? 'Janitor' : null;
450
+ const closed = await this.sweepAndClose(filter, provider, systemUser, reasonFor);
394
451
  if (closed > 0) {
395
452
  LogStatus(
396
453
  `[SessionJanitor] Startup recovery closed ${closed} orphaned session(s) from an earlier boot of this instance ` +
@@ -402,12 +459,18 @@ export class SessionJanitor extends BaseSingleton<SessionJanitor> implements ISh
402
459
 
403
460
  /**
404
461
  * Force-close any `Active`/`Idle` session whose `LastActiveAt` is older than the configured
405
- * close threshold, regardless of host. Idempotent and concurrency-safe. Returns the count closed.
462
+ * close threshold, regardless of host. Stamped `'Idle'`, or `'TimeLimit'` when the row's authored deadline
463
+ * passed before its idle moment ({@link ExpiryCloseReason}). A row the SQL judged stale is never skipped:
464
+ * when its `LastActiveAt` will not parse it closes as `'Idle'` with a `LogError` ({@link staleRowCloseReason}).
465
+ * Idempotent and concurrency-safe. Returns the count closed.
406
466
  */
407
467
  public async RunStalenessSweep(provider: IMetadataProvider, systemUser: UserInfo): Promise<number> {
408
- const cutoffIso = new Date(Date.now() - this._config.closeThresholdMinutes * 60_000).toISOString();
468
+ // One clock read feeds both the SQL cutoff and the per-row decision, so they cannot disagree.
469
+ const nowMs = Date.now();
470
+ const thresholdMs = this._config.closeThresholdMinutes * 60_000;
471
+ const cutoffIso = new Date(nowMs - thresholdMs).toISOString();
409
472
  const filter = `Status IN ('Active','Idle') AND LastActiveAt < '${cutoffIso}'`;
410
- const closed = await this.sweepAndClose(filter, provider, systemUser, 'Janitor');
473
+ const closed = await this.sweepAndClose(filter, provider, systemUser, s => staleRowCloseReason(s, nowMs, thresholdMs));
411
474
  if (closed > 0) {
412
475
  LogStatus(`[SessionJanitor] Staleness sweep closed ${closed} stale session(s) (>${this._config.closeThresholdMinutes}m idle)`);
413
476
  }
@@ -422,20 +485,18 @@ export class SessionJanitor extends BaseSingleton<SessionJanitor> implements ISh
422
485
  *
423
486
  * Only sessions that CARRY a deadline are loaded (a cheap `Config_ LIKE` pre-filter), then the exact
424
487
  * ISO deadline is parsed and compared in JS (`Config_` is JSON, not a queryable column). Stamped
425
- * `CloseReason = 'Janitor'`, idempotent + concurrency-safe like the other sweeps. Returns the count closed.
488
+ * `'TimeLimit'`, or `'Idle'` when the row's idle moment passed before its deadline ({@link ExpiryCloseReason});
489
+ * idempotent + concurrency-safe like the other sweeps. Returns the count closed.
426
490
  */
427
491
  public async RunMaxDurationSweep(provider: IMetadataProvider, systemUser: UserInfo): Promise<number> {
428
492
  const nowMs = Date.now();
429
493
  // Only consider sessions that carry a deadline marker — keeps this sweep cheap (it never touches
430
494
  // the far-more-common uncapped sessions) while the JS check below enforces the exact deadline.
431
495
  const filter = `Status IN ('Active','Idle') AND Config LIKE '%maxSessionDeadlineIso%'`;
432
- const isExpired = (session: MJAIAgentSessionEntity): boolean => {
433
- const deadlineMs = this.parseSessionDeadlineMs(session.Config_);
434
- return deadlineMs != null && deadlineMs <= nowMs;
435
- };
436
- const closed = await this.sweepAndClose(filter, provider, systemUser, 'Janitor', isExpired);
496
+ const thresholdMs = this._config.closeThresholdMinutes * 60_000;
497
+ const closed = await this.sweepAndClose(filter, provider, systemUser, s => ExpiryCloseReason(s, nowMs, thresholdMs));
437
498
  if (closed > 0) {
438
- LogStatus(`[SessionJanitor] Max-duration sweep closed ${closed} session(s) past their hard duration cap`);
499
+ LogStatus(`[SessionJanitor] Max-duration sweep closed ${closed} capped session(s) past their deadline or idle threshold`);
439
500
  }
440
501
  return closed;
441
502
  }
@@ -705,23 +766,6 @@ export class SessionJanitor extends BaseSingleton<SessionJanitor> implements ISh
705
766
  }
706
767
  }
707
768
 
708
- /** Parses the absolute deadline (ms) from a session's `Config_` JSON, or null when absent/malformed. */
709
- private parseSessionDeadlineMs(configJson: string | null | undefined): number | null {
710
- if (!configJson) {
711
- return null;
712
- }
713
- try {
714
- const parsed = JSON.parse(configJson) as { maxSessionDeadlineIso?: string };
715
- if (typeof parsed.maxSessionDeadlineIso !== 'string') {
716
- return null;
717
- }
718
- const ms = Date.parse(parsed.maxSessionDeadlineIso);
719
- return Number.isNaN(ms) ? null : ms;
720
- } catch {
721
- return null;
722
- }
723
- }
724
-
725
769
  // ----- internals -------------------------------------------------------------------------
726
770
 
727
771
  /** Register for graceful shutdown exactly once. */
@@ -993,7 +1037,9 @@ export class SessionJanitor extends BaseSingleton<SessionJanitor> implements ISh
993
1037
 
994
1038
  /**
995
1039
  * Page through every session matching `filter` with keyset (`AfterKey`) pagination and close each
996
- * via {@link SessionManager.CloseSession}, stamping `closeReason` on every row it transitions.
1040
+ * via {@link SessionManager.CloseSession}, stamping the reason `reasonFor` returns for that row.
1041
+ * A `null` reason skips the row (the SQL filter only narrows; `reasonFor` decides), and skipped rows
1042
+ * are dropped before the channel batch load so it covers only rows that will close.
997
1043
  * Returns the number successfully closed. A `Closed`-by-now row (raced by another instance) is a
998
1044
  * harmless no-op that keeps its original reason.
999
1045
  */
@@ -1001,30 +1047,34 @@ export class SessionJanitor extends BaseSingleton<SessionJanitor> implements ISh
1001
1047
  filter: string,
1002
1048
  provider: IMetadataProvider,
1003
1049
  systemUser: UserInfo,
1004
- closeReason: SessionCloseReason,
1005
- shouldClose?: (session: MJAIAgentSessionEntity) => boolean,
1050
+ reasonFor: (session: MJAIAgentSessionEntity) => SessionCloseReason | null,
1006
1051
  ): Promise<number> {
1007
1052
  let closedCount = 0;
1008
1053
  await this.forEachSessionPage(filter, provider, systemUser, async fetched => {
1009
- // Optional in-JS narrowing (e.g. exact deadline check the SQL pre-filter can't express).
1010
- const page = shouldClose ? fetched.filter(shouldClose) : fetched;
1011
- if (page.length === 0) {
1054
+ const toClose: Array<{ session: MJAIAgentSessionEntity; reason: SessionCloseReason }> = [];
1055
+ for (const session of fetched) {
1056
+ const reason = reasonFor(session);
1057
+ if (reason !== null) {
1058
+ toClose.push({ session, reason });
1059
+ }
1060
+ }
1061
+ if (toClose.length === 0) {
1012
1062
  return;
1013
1063
  }
1014
1064
  // Batch-load every channel for this whole page of sessions in ONE query, then hand each
1015
1065
  // session its own slice to CloseSession — avoids the N+1 channel read (one RunView per
1016
1066
  // closing session) that tripped the sequential / multiple-same-entity telemetry.
1017
1067
  const channelsBySession = await this.sessionManager.LoadActiveChannelsBySession(
1018
- page.map(s => s.ID),
1068
+ toClose.map(c => c.session.ID),
1019
1069
  systemUser,
1020
1070
  provider,
1021
1071
  );
1022
- for (const session of page) {
1072
+ for (const { session, reason } of toClose) {
1023
1073
  const closed = await this.sessionManager.CloseSession(
1024
1074
  session.ID,
1025
1075
  systemUser,
1026
1076
  provider,
1027
- closeReason,
1077
+ reason,
1028
1078
  channelsBySession.get(session.ID.toLowerCase()) ?? [],
1029
1079
  );
1030
1080
  if (closed) {
@@ -1,13 +1,15 @@
1
- import { IMetadataProvider, UserInfo, LogError, RunView } from '@memberjunction/core';
1
+ import { IMetadataProvider, UserInfo, LogError, LogStatus, RunView } from '@memberjunction/core';
2
2
  import { MJLruCache } from '@memberjunction/global';
3
3
  import {
4
4
  MJAIAgentSessionEntity,
5
5
  MJAIAgentSessionChannelEntity,
6
6
  MJConversationEntity,
7
7
  } from '@memberjunction/core-entities';
8
+ import { RunWithTrustedSessionConfigWrites } from '@memberjunction/core-entities-server';
8
9
  import { AIAgentPermissionHelper } from '@memberjunction/ai-engine-base';
9
10
  import { RealtimeClientSessionService, RealtimeChannelServerHost } from '@memberjunction/ai-agents';
10
11
  import type { BridgeDisconnectReason } from '@memberjunction/ai-bridge-base';
12
+ import { ParseSessionDeadlineMs, VerifyTimeLimitClaim } from './sessionDeadline.js';
11
13
  import { GetHostInstanceID } from './HostInstance.js';
12
14
  import { WriteReturningVisitorRecap } from './ReturningVisitorRecap.js';
13
15
  import { SessionRunIDVerifier, SessionRunIDs } from './SessionRunIDVerifier.js';
@@ -40,11 +42,14 @@ export interface CreateSessionInput {
40
42
  /**
41
43
  * Why a session was closed — persisted to `AIAgentSession.CloseReason` alongside the
42
44
  * `Status = 'Closed'` transition so the admin dashboards can distinguish a user hanging up
43
- * (`Explicit`) from janitor reconciliation (`Janitor`), a graceful host drain (`Shutdown`),
44
- * or a failure-path teardown (`Error`). `NULL` in the column means "legacy/unknown" — rows
45
- * closed before this column existed.
45
+ * (`Explicit`) from janitor reconciliation (`Janitor`, an orphan from an earlier boot), a graceful
46
+ * host drain (`Shutdown`), a failure-path teardown (`Error`), an expired authored deadline
47
+ * (`TimeLimit`) or a session left quiet past the idle threshold (`Idle`). `NULL` in the column means
48
+ * "legacy/unknown" — rows closed before this column existed.
49
+ *
50
+ * Derived from the generated entity so the column's CHECK constraint stays the single source of truth.
46
51
  */
47
- export type SessionCloseReason = 'Explicit' | 'Janitor' | 'Shutdown' | 'Error';
52
+ export type SessionCloseReason = NonNullable<MJAIAgentSessionEntity['CloseReason']>;
48
53
 
49
54
  /**
50
55
  * The {@link SessionCloseReason} for a session whose bridged agent has left (see
@@ -191,6 +196,8 @@ export class SessionManager {
191
196
  * call sites (the user-initiated close mutations) stamp the common case without modification;
192
197
  * background callers (janitor sweeps, shutdown drain, error teardowns) must pass their reason
193
198
  * explicitly. `NULL` rows remain only for legacy data closed before the column existed.
199
+ * A `'TimeLimit'` is verified against the session's stored deadline ({@link VerifyTimeLimitClaim}); a claim the
200
+ * deadline does not back is recorded as `'Explicit'` and logged, never refused.
194
201
  *
195
202
  * @remarks P5 adds in-flight-run abort + `ClientToolRequestManager.ClearSession` here.
196
203
  */
@@ -215,7 +222,9 @@ export class SessionManager {
215
222
  return true;
216
223
  }
217
224
 
218
- const closed = await this.markSessionClosed(session, closeReason);
225
+ // Verified once: the stamped reason and the plugin notification must agree, so a client's unbacked claim steers neither.
226
+ const verifiedReason = this.verifiedCloseReason(session, closeReason);
227
+ const closed = await this.markSessionClosed(session, verifiedReason);
219
228
  if (!closed) {
220
229
  return false;
221
230
  }
@@ -228,7 +237,7 @@ export class SessionManager {
228
237
  }
229
238
  await this.disconnectChannels(agentSessionID, contextUser, provider, preloadedChannels);
230
239
  await this.finalizeObservabilityRuns(session, contextUser, provider);
231
- await this.notifyChannelPluginsSessionClosed(agentSessionID, closeReason);
240
+ await this.notifyChannelPluginsSessionClosed(agentSessionID, verifiedReason);
232
241
  // Returning-visitor recap (RV2): if this session's conversation is returning-visitor-enabled,
233
242
  // summarize it into an Active memory note so the visitor's next session opens with prior context.
234
243
  // Best-effort + no-op for non-returning-visitor conversations; never blocks teardown.
@@ -236,6 +245,25 @@ export class SessionManager {
236
245
  return true;
237
246
  }
238
247
 
248
+ /**
249
+ * Returns the reason to stamp: a `'TimeLimit'` claim the session's stored deadline does not back becomes
250
+ * `'Explicit'` (logged with the stored deadline); every other reason passes through.
251
+ */
252
+ private verifiedCloseReason(session: MJAIAgentSessionEntity, closeReason: SessionCloseReason): SessionCloseReason {
253
+ if (closeReason !== 'TimeLimit') {
254
+ return closeReason;
255
+ }
256
+ const verified = VerifyTimeLimitClaim(session, Date.now());
257
+ if (verified === 'Explicit') {
258
+ const deadlineMs = ParseSessionDeadlineMs(session.Config_);
259
+ LogStatus(
260
+ `SessionManager.CloseSession: TimeLimit claim for session ${session.ID} not backed by its stored deadline ` +
261
+ `(${deadlineMs === null ? 'none stored' : new Date(deadlineMs).toISOString()}); closing as Explicit.`,
262
+ );
263
+ }
264
+ return verified;
265
+ }
266
+
239
267
  /**
240
268
  * Closes a session that was created for one bridged agent (a bot in a meeting room) once that agent's bridge has
241
269
  * ended, whatever ended it: the agent was stopped, the meeting was ended, everyone left, the model session was lost.
@@ -584,7 +612,9 @@ export class SessionManager {
584
612
  session.Status = 'Closed';
585
613
  session.ClosedAt = new Date();
586
614
  session.CloseReason = closeReason;
587
- return this.saveOrLog(session, 'CloseSession');
615
+ // The session entity refuses a server-only CloseReason from an untrusted save; this is the server's own
616
+ // (already verified) close, so it saves inside the trusted session-write scope.
617
+ return RunWithTrustedSessionConfigWrites(() => this.saveOrLog(session, 'CloseSession'));
588
618
  }
589
619
 
590
620
  /** Marks every channel row of the session `Disconnected` (with `DisconnectedAt`), if not already. */
@@ -0,0 +1,143 @@
1
+ /**
2
+ * @fileoverview The remote browsers this process holds for TEXT conversations: who owns each and when it
3
+ * was last used. Backs the caps (`realtime.conversationBrowser.maxPerUser` and `maxAnonymousTotal`) and ends browsers
4
+ * nobody uses any more, since a reloaded or closed tab never ends its own
5
+ * (`realtime.conversationBrowser.idleMinutes`). Realtime calls' browsers are not tracked here: they end
6
+ * with their session.
7
+ *
8
+ * @module @memberjunction/server/agentSessions
9
+ */
10
+ import { GraphQLError } from 'graphql';
11
+ import { LogError, LogStatus } from '@memberjunction/core';
12
+ import { BaseSingleton, IShutdownable, ShutdownRegistry } from '@memberjunction/global';
13
+ import { RemoteBrowserEngine } from '@memberjunction/remote-browser-server';
14
+ import type { ConversationBrowserConfig } from '../config.js';
15
+
16
+ /** How often idle conversation browsers are looked for. */
17
+ const REAP_INTERVAL_MS = 60_000;
18
+
19
+ /** The `extensions.code` of a {@link RemoteBrowserRefusedError}, which a client reads to stop asking. */
20
+ export const REMOTE_BROWSER_REFUSED = 'REMOTE_BROWSER_REFUSED';
21
+
22
+ /**
23
+ * A remote-browser request this server refuses however often it is asked: browsers not enabled, a session
24
+ * that is not the caller's or does not exist, or a cap reached.
25
+ */
26
+ export class RemoteBrowserRefusedError extends GraphQLError {
27
+ constructor(message: string) {
28
+ super(message, { extensions: { code: REMOTE_BROWSER_REFUSED } });
29
+ this.name = 'RemoteBrowserRefusedError';
30
+ }
31
+ }
32
+
33
+ /** Who a conversation browser counts against: a user, or an anonymous guest's own scope. */
34
+ export interface ConversationBrowserOwner {
35
+ Key: string;
36
+ Anonymous: boolean;
37
+ }
38
+
39
+ interface HeldBrowser {
40
+ Owner: ConversationBrowserOwner;
41
+ LastUsedAt: number;
42
+ IdleMs: number;
43
+ }
44
+
45
+ export class ConversationBrowserLedger extends BaseSingleton<ConversationBrowserLedger> implements IShutdownable {
46
+ public readonly ShutdownName = 'ConversationBrowserLedger';
47
+ private readonly held = new Map<string, HeldBrowser>();
48
+ private reapTimer: ReturnType<typeof setInterval> | null = null;
49
+
50
+ /**
51
+ * How an idle browser is ended. The resolver points this at its own end path, so a reaped browser
52
+ * leaves no stream bookkeeping behind; until then the engine ends it directly.
53
+ */
54
+ public EndBrowser: (conversationID: string) => Promise<unknown> = (conversationID) =>
55
+ RemoteBrowserEngine.Instance.EndSessionForAgentSession(conversationID);
56
+
57
+ protected constructor() {
58
+ super();
59
+ }
60
+
61
+ public static get Instance(): ConversationBrowserLedger {
62
+ return super.getInstance<ConversationBrowserLedger>();
63
+ }
64
+
65
+ /**
66
+ * Records that a conversation's browser is about to be used. A conversation the ledger does not hold
67
+ * yet counts against its owner's cap and, for an anonymous guest, against the cap all guests share: a
68
+ * guest can start a new scope cheaply, so its own cap alone bounds nothing.
69
+ *
70
+ * @throws RemoteBrowserRefusedError when the owner already holds `maxPerUser` conversation browsers, or
71
+ * guests together already hold `maxAnonymousTotal`.
72
+ */
73
+ public Claim(conversationID: string, owner: ConversationBrowserOwner, limits: Pick<ConversationBrowserConfig, 'maxPerUser' | 'maxAnonymousTotal' | 'idleMinutes'>): void {
74
+ const key = conversationID.toLowerCase();
75
+ if (!this.held.has(key)) {
76
+ const held = Array.from(this.held.values());
77
+ const count = held.filter((b) => b.Owner.Key === owner.Key).length;
78
+ if (count >= limits.maxPerUser) {
79
+ throw new RemoteBrowserRefusedError(`You already have ${count} conversation browser(s) open, the most this server allows. Close one first.`);
80
+ }
81
+ if (owner.Anonymous && held.filter((b) => b.Owner.Anonymous).length >= limits.maxAnonymousTotal) {
82
+ throw new RemoteBrowserRefusedError('Every conversation browser this server allows for guests is in use. Try again later.');
83
+ }
84
+ }
85
+ this.held.set(key, { Owner: owner, LastUsedAt: Date.now(), IdleMs: limits.idleMinutes * 60_000 });
86
+ this.startReaper();
87
+ }
88
+
89
+ /** Marks a held browser as used (an action, a snapshot poll, human input), which puts off its idle end. */
90
+ public Touch(conversationID: string): void {
91
+ const browser = this.held.get(conversationID.toLowerCase());
92
+ if (browser) {
93
+ browser.LastUsedAt = Date.now();
94
+ }
95
+ }
96
+
97
+ /** Forgets a conversation's browser once it has been ended. */
98
+ public Release(conversationID: string): void {
99
+ this.held.delete(conversationID.toLowerCase());
100
+ }
101
+
102
+ /** Ends every held browser unused for longer than its idle limit. Returns how many were ended. */
103
+ public async ReapIdle(): Promise<number> {
104
+ const now = Date.now();
105
+ const idle = Array.from(this.held).filter(([, b]) => now - b.LastUsedAt > b.IdleMs).map(([id]) => id);
106
+ for (const conversationID of idle) {
107
+ this.held.delete(conversationID);
108
+ try {
109
+ await this.EndBrowser(conversationID);
110
+ } catch (err) {
111
+ LogError(`[ConversationBrowsers] Failed to end the idle browser of conversation ${conversationID}: ${err instanceof Error ? err.message : String(err)}`);
112
+ }
113
+ }
114
+ if (idle.length > 0) {
115
+ LogStatus(`[ConversationBrowsers] Ended ${idle.length} idle conversation browser(s).`);
116
+ }
117
+ return idle.length;
118
+ }
119
+
120
+ /** Ends every held browser on a graceful shutdown, so none outlives the server. Idempotent. */
121
+ public async Shutdown(): Promise<void> {
122
+ if (this.reapTimer) {
123
+ clearInterval(this.reapTimer);
124
+ this.reapTimer = null;
125
+ }
126
+ const held = Array.from(this.held.keys());
127
+ this.held.clear();
128
+ await Promise.allSettled(held.map((conversationID) => this.EndBrowser(conversationID)));
129
+ }
130
+
131
+ /**
132
+ * One unref'd timer per process, started with the first browser, so the reaper costs nothing until then.
133
+ * The ledger joins the graceful shutdown at the same time.
134
+ */
135
+ private startReaper(): void {
136
+ if (this.reapTimer) {
137
+ return;
138
+ }
139
+ this.reapTimer = setInterval(() => void this.ReapIdle(), REAP_INTERVAL_MS);
140
+ this.reapTimer.unref?.();
141
+ ShutdownRegistry.Instance.Register(this);
142
+ }
143
+ }
@@ -0,0 +1,81 @@
1
+ import type { SessionCloseReason } from './SessionManager.js';
2
+
3
+ /** The close reasons an expiry check can yield: the two ways a live session runs out of time. */
4
+ export type ExpiryReason = Extract<SessionCloseReason, 'Idle' | 'TimeLimit'>;
5
+
6
+ /**
7
+ * Parses the absolute deadline (ms) from a session's `Config_` JSON, or null when absent/malformed.
8
+ * A malformed deadline counts as absent by design: a sweep must never throw on one bad row, so null
9
+ * is the documented failure result here, not a swallowed error.
10
+ */
11
+ export function ParseSessionDeadlineMs(configJson: string | null | undefined): number | null {
12
+ if (!configJson) {
13
+ return null;
14
+ }
15
+ try {
16
+ const parsed = JSON.parse(configJson) as { maxSessionDeadlineIso?: unknown };
17
+ if (typeof parsed.maxSessionDeadlineIso !== 'string') {
18
+ return null;
19
+ }
20
+ const ms = Date.parse(parsed.maxSessionDeadlineIso);
21
+ return Number.isNaN(ms) ? null : ms;
22
+ } catch {
23
+ // Malformed JSON: treated as "no deadline" (see doc comment).
24
+ return null;
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Decides why a session has expired, or null when it has not.
30
+ *
31
+ * Two moments can expire a session: its idle moment (`LastActiveAt + closeThresholdMs`) and its authored
32
+ * hard deadline (`Config_.maxSessionDeadlineIso`). Only a moment that has already passed (`<= nowMs`)
33
+ * counts, and the EARLIER of the passed moments wins. A tie at the same instant yields `TimeLimit`:
34
+ * the authored rule beats the safety net. A row without a usable `LastActiveAt` (null, or a value that
35
+ * does not parse to a date) is never `Idle`, and neither is any row when `closeThresholdMs` is non-finite.
36
+ * `LastActiveAt` is typed `Date` but may arrive as an ISO string from a provider, so both are accepted.
37
+ */
38
+ export function ExpiryCloseReason(
39
+ session: { LastActiveAt: Date | string | null | undefined; Config_: string | null | undefined },
40
+ nowMs: number,
41
+ closeThresholdMs: number,
42
+ ): ExpiryReason | null {
43
+ const lastActiveMs = session.LastActiveAt ? new Date(session.LastActiveAt).getTime() : Number.NaN;
44
+ const idleMs = lastActiveMs + closeThresholdMs;
45
+ const idleAt = Number.isFinite(idleMs) ? idleMs : null;
46
+ const deadlineAt = ParseSessionDeadlineMs(session.Config_);
47
+ const idlePassed = idleAt !== null && idleAt <= nowMs;
48
+ const deadlinePassed = deadlineAt !== null && deadlineAt <= nowMs;
49
+ if (deadlinePassed && idlePassed) {
50
+ return deadlineAt <= idleAt ? 'TimeLimit' : 'Idle';
51
+ }
52
+ if (deadlinePassed) {
53
+ return 'TimeLimit';
54
+ }
55
+ return idlePassed ? 'Idle' : null;
56
+ }
57
+
58
+ /**
59
+ * How far ahead of a stored deadline a browser's `TimeLimit` claim is still believed. The browser's
60
+ * alarm runs on its own clock: when it extends to a server ISO deadline it anchors that time against its
61
+ * own `Date.now()`, so a client clock running ahead of the server fires early by the difference (a throttled
62
+ * tab only fires late). A minute absorbs that skew without letting a client end a session meant to run for
63
+ * minutes more.
64
+ */
65
+ export const DEADLINE_CLAIM_SKEW_MS = 60_000;
66
+
67
+ /**
68
+ * Verifies a `TimeLimit` close claim against the session's stored deadline. The browser claims, the server
69
+ * verifies: the claim holds when the stored deadline has passed or falls within {@link DEADLINE_CLAIM_SKEW_MS},
70
+ * otherwise (no deadline, or one far in the future) the close is recorded as `Explicit`. The janitor's own
71
+ * `TimeLimit` trivially passes, so every caller shares one path, and this module remains the single place that
72
+ * answers "did the deadline pass?" for both the sweep and the client claim. Idleness never counts here.
73
+ */
74
+ export function VerifyTimeLimitClaim(
75
+ session: { LastActiveAt: Date | string | null | undefined; Config_: string | null | undefined },
76
+ nowMs: number,
77
+ ): 'TimeLimit' | 'Explicit' {
78
+ return ExpiryCloseReason(session, nowMs + DEADLINE_CLAIM_SKEW_MS, Number.POSITIVE_INFINITY) === 'TimeLimit'
79
+ ? 'TimeLimit'
80
+ : 'Explicit';
81
+ }
@@ -7,6 +7,7 @@
7
7
  import { AuthorizationError } from 'type-graphql';
8
8
  import { GetAPIKeyEngine, AuthorizationResult, AuthorizationRequest } from '@memberjunction/api-keys';
9
9
  import { UserInfo, RunView } from '@memberjunction/core';
10
+ import { EscapeSQLString } from '@memberjunction/global';
10
11
  import { MJAPIKeyEntity, MJAPIApplicationEntity } from '@memberjunction/core-entities';
11
12
 
12
13
  /**
@@ -96,7 +97,7 @@ export async function CheckAPIKeyScope(
96
97
  const rv = new RunView();
97
98
  const keyResult = await rv.RunView<MJAPIKeyEntity>({
98
99
  EntityName: 'MJ: API Keys',
99
- ExtraFilter: `ID='${apiKeyId}'`,
100
+ ExtraFilter: `ID='${EscapeSQLString(apiKeyId)}'`,
100
101
  ResultType: 'entity_object'
101
102
  }, contextUser);
102
103
 
@@ -117,7 +118,7 @@ export async function CheckAPIKeyScope(
117
118
  // Get the application by name
118
119
  const appResult = await rv.RunView<MJAPIApplicationEntity>({
119
120
  EntityName: 'MJ: API Applications',
120
- ExtraFilter: `Name='${applicationName}'`,
121
+ ExtraFilter: `Name='${EscapeSQLString(applicationName)}'`,
121
122
  ResultType: 'entity_object'
122
123
  }, contextUser);
123
124
 
@@ -224,7 +225,7 @@ export async function CheckAPIKeyScopeAndLog(
224
225
  // Get the API key
225
226
  const keyResult = await rv.RunView<MJAPIKeyEntity>({
226
227
  EntityName: 'MJ: API Keys',
227
- ExtraFilter: `ID='${apiKeyId}'`,
228
+ ExtraFilter: `ID='${EscapeSQLString(apiKeyId)}'`,
228
229
  ResultType: 'entity_object'
229
230
  }, contextUser);
230
231
 
@@ -245,7 +246,7 @@ export async function CheckAPIKeyScopeAndLog(
245
246
  // Get the application
246
247
  const appResult = await rv.RunView<MJAPIApplicationEntity>({
247
248
  EntityName: 'MJ: API Applications',
248
- ExtraFilter: `Name='${applicationName}'`,
249
+ ExtraFilter: `Name='${EscapeSQLString(applicationName)}'`,
249
250
  ResultType: 'entity_object'
250
251
  }, contextUser);
251
252