@memberjunction/server 6.2.0-edge.2 → 6.2.0-edge.4
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.
- package/README.md +30 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/agentSessions/BridgeHostInstance.d.ts +25 -0
- package/dist/agentSessions/BridgeHostInstance.d.ts.map +1 -0
- package/dist/agentSessions/BridgeHostInstance.js +30 -0
- package/dist/agentSessions/BridgeHostInstance.js.map +1 -0
- package/dist/agentSessions/HostInstance.d.ts +75 -11
- package/dist/agentSessions/HostInstance.d.ts.map +1 -1
- package/dist/agentSessions/HostInstance.js +99 -28
- package/dist/agentSessions/HostInstance.js.map +1 -1
- package/dist/agentSessions/ReturningVisitorRecap.d.ts.map +1 -1
- package/dist/agentSessions/ReturningVisitorRecap.js +7 -3
- package/dist/agentSessions/ReturningVisitorRecap.js.map +1 -1
- package/dist/agentSessions/SessionJanitor.d.ts +173 -25
- package/dist/agentSessions/SessionJanitor.d.ts.map +1 -1
- package/dist/agentSessions/SessionJanitor.js +452 -54
- package/dist/agentSessions/SessionJanitor.js.map +1 -1
- package/dist/agentSessions/SessionManager.d.ts +43 -2
- package/dist/agentSessions/SessionManager.d.ts.map +1 -1
- package/dist/agentSessions/SessionManager.js +99 -8
- package/dist/agentSessions/SessionManager.js.map +1 -1
- package/dist/agentSessions/SessionRunIDVerifier.d.ts +63 -0
- package/dist/agentSessions/SessionRunIDVerifier.d.ts.map +1 -0
- package/dist/agentSessions/SessionRunIDVerifier.js +129 -0
- package/dist/agentSessions/SessionRunIDVerifier.js.map +1 -0
- package/dist/agentSessions/index.d.ts +2 -0
- package/dist/agentSessions/index.d.ts.map +1 -1
- package/dist/agentSessions/index.js +10 -3
- package/dist/agentSessions/index.js.map +1 -1
- package/dist/auth/actionAuthorization.d.ts +40 -0
- package/dist/auth/actionAuthorization.d.ts.map +1 -0
- package/dist/auth/actionAuthorization.js +94 -0
- package/dist/auth/actionAuthorization.js.map +1 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +3 -10
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.d.ts.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.js +6 -3
- package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
- package/dist/auth/newUsers.d.ts +10 -0
- package/dist/auth/newUsers.d.ts.map +1 -1
- package/dist/auth/newUsers.js +80 -11
- package/dist/auth/newUsers.js.map +1 -1
- package/dist/config.d.ts +4567 -71
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +246 -3
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +8 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +25 -34
- package/dist/context.js.map +1 -1
- package/dist/generated/graphql-schemas/__mj.d.ts +1073 -10
- package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
- package/dist/generated/graphql-schemas/__mj.js +9188 -3649
- package/dist/generated/graphql-schemas/__mj.js.map +1 -1
- package/dist/generic/OrphanedConversationDetailReconciler.js +3 -3
- package/dist/generic/OrphanedConversationDetailReconciler.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +62 -6
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +179 -35
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts +4 -0
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +37 -1
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +214 -159
- package/dist/index.js.map +1 -1
- package/dist/isolatedProvider.d.ts +20 -0
- package/dist/isolatedProvider.d.ts.map +1 -0
- package/dist/isolatedProvider.js +22 -0
- package/dist/isolatedProvider.js.map +1 -0
- package/dist/postgresqlCompat.d.ts +8 -0
- package/dist/postgresqlCompat.d.ts.map +1 -0
- package/dist/postgresqlCompat.js +19 -0
- package/dist/postgresqlCompat.js.map +1 -0
- package/dist/postgresqlPoolSettings.d.ts +108 -0
- package/dist/postgresqlPoolSettings.d.ts.map +1 -0
- package/dist/postgresqlPoolSettings.js +134 -0
- package/dist/postgresqlPoolSettings.js.map +1 -0
- package/dist/realtimeProxy/RealtimeProxyServer.d.ts +44 -4
- package/dist/realtimeProxy/RealtimeProxyServer.d.ts.map +1 -1
- package/dist/realtimeProxy/RealtimeProxyServer.js +121 -16
- package/dist/realtimeProxy/RealtimeProxyServer.js.map +1 -1
- package/dist/realtimeProxy/RealtimeRelayTunnel.d.ts +171 -0
- package/dist/realtimeProxy/RealtimeRelayTunnel.d.ts.map +1 -0
- package/dist/realtimeProxy/RealtimeRelayTunnel.js +532 -0
- package/dist/realtimeProxy/RealtimeRelayTunnel.js.map +1 -0
- package/dist/realtimeSessions/RealtimeSessionEventService.d.ts +148 -0
- package/dist/realtimeSessions/RealtimeSessionEventService.d.ts.map +1 -0
- package/dist/realtimeSessions/RealtimeSessionEventService.js +191 -0
- package/dist/realtimeSessions/RealtimeSessionEventService.js.map +1 -0
- package/dist/realtimeSessions/RealtimeSessionVerificationService.d.ts +66 -0
- package/dist/realtimeSessions/RealtimeSessionVerificationService.d.ts.map +1 -0
- package/dist/realtimeSessions/RealtimeSessionVerificationService.js +138 -0
- package/dist/realtimeSessions/RealtimeSessionVerificationService.js.map +1 -0
- package/dist/realtimeSessions/RealtimeVerifyRouter.d.ts +38 -0
- package/dist/realtimeSessions/RealtimeVerifyRouter.d.ts.map +1 -0
- package/dist/realtimeSessions/RealtimeVerifyRouter.js +98 -0
- package/dist/realtimeSessions/RealtimeVerifyRouter.js.map +1 -0
- package/dist/realtimeSessions/SessionVerificationPortsBase.d.ts +61 -0
- package/dist/realtimeSessions/SessionVerificationPortsBase.d.ts.map +1 -0
- package/dist/realtimeSessions/SessionVerificationPortsBase.js +149 -0
- package/dist/realtimeSessions/SessionVerificationPortsBase.js.map +1 -0
- package/dist/realtimeSessions/SlidingWindowRateLimiter.d.ts +57 -0
- package/dist/realtimeSessions/SlidingWindowRateLimiter.d.ts.map +1 -0
- package/dist/realtimeSessions/SlidingWindowRateLimiter.js +69 -0
- package/dist/realtimeSessions/SlidingWindowRateLimiter.js.map +1 -0
- package/dist/realtimeSessions/index.d.ts +17 -0
- package/dist/realtimeSessions/index.d.ts.map +1 -0
- package/dist/realtimeSessions/index.js +17 -0
- package/dist/realtimeSessions/index.js.map +1 -0
- package/dist/realtimeSessions/sessionAccess.d.ts +86 -0
- package/dist/realtimeSessions/sessionAccess.d.ts.map +1 -0
- package/dist/realtimeSessions/sessionAccess.js +90 -0
- package/dist/realtimeSessions/sessionAccess.js.map +1 -0
- package/dist/realtimeSessions/sessionConfigStamps.d.ts +70 -0
- package/dist/realtimeSessions/sessionConfigStamps.d.ts.map +1 -0
- package/dist/realtimeSessions/sessionConfigStamps.js +93 -0
- package/dist/realtimeSessions/sessionConfigStamps.js.map +1 -0
- package/dist/realtimeSessions/subscriptionAuthorization.d.ts +65 -0
- package/dist/realtimeSessions/subscriptionAuthorization.d.ts.map +1 -0
- package/dist/realtimeSessions/subscriptionAuthorization.js +71 -0
- package/dist/realtimeSessions/subscriptionAuthorization.js.map +1 -0
- package/dist/realtimeSessions/verificationCore.d.ts +243 -0
- package/dist/realtimeSessions/verificationCore.d.ts.map +1 -0
- package/dist/realtimeSessions/verificationCore.js +356 -0
- package/dist/realtimeSessions/verificationCore.js.map +1 -0
- package/dist/realtimeSessions/verificationPages.d.ts +66 -0
- package/dist/realtimeSessions/verificationPages.d.ts.map +1 -0
- package/dist/realtimeSessions/verificationPages.js +110 -0
- package/dist/realtimeSessions/verificationPages.js.map +1 -0
- package/dist/realtimeSessions/verificationPolicy.d.ts +147 -0
- package/dist/realtimeSessions/verificationPolicy.d.ts.map +1 -0
- package/dist/realtimeSessions/verificationPolicy.js +249 -0
- package/dist/realtimeSessions/verificationPolicy.js.map +1 -0
- package/dist/realtimeSessions/verificationWorkflow.d.ts +263 -0
- package/dist/realtimeSessions/verificationWorkflow.d.ts.map +1 -0
- package/dist/realtimeSessions/verificationWorkflow.js +435 -0
- package/dist/realtimeSessions/verificationWorkflow.js.map +1 -0
- package/dist/realtimeWidget/widgetGuestElevation.d.ts +20 -2
- package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -1
- package/dist/realtimeWidget/widgetGuestElevation.js +31 -2
- package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
- package/dist/resolvers/ActionResolver.d.ts +31 -5
- package/dist/resolvers/ActionResolver.d.ts.map +1 -1
- package/dist/resolvers/ActionResolver.js +75 -14
- package/dist/resolvers/ActionResolver.js.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.d.ts +35 -17
- package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.js +92 -166
- package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
- package/dist/resolvers/CacheStatsResolver.d.ts +27 -0
- package/dist/resolvers/CacheStatsResolver.d.ts.map +1 -1
- package/dist/resolvers/CacheStatsResolver.js +113 -2
- package/dist/resolvers/CacheStatsResolver.js.map +1 -1
- package/dist/resolvers/ClientToolRequestResolver.d.ts +4 -2
- package/dist/resolvers/ClientToolRequestResolver.d.ts.map +1 -1
- package/dist/resolvers/ClientToolRequestResolver.js +28 -10
- package/dist/resolvers/ClientToolRequestResolver.js.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.d.ts +7 -23
- package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.js +9 -47
- package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.d.ts +12 -1
- package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.js +83 -2
- package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
- package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataResolver.js +9 -0
- package/dist/resolvers/GetDataResolver.js.map +1 -1
- package/dist/resolvers/HumanHandoffResolver.d.ts +107 -0
- package/dist/resolvers/HumanHandoffResolver.d.ts.map +1 -0
- package/dist/resolvers/HumanHandoffResolver.js +330 -0
- package/dist/resolvers/HumanHandoffResolver.js.map +1 -0
- package/dist/resolvers/MCPResolver.d.ts.map +1 -1
- package/dist/resolvers/MCPResolver.js +9 -4
- package/dist/resolvers/MCPResolver.js.map +1 -1
- package/dist/resolvers/MeetingResolver.d.ts +69 -0
- package/dist/resolvers/MeetingResolver.d.ts.map +1 -0
- package/dist/resolvers/MeetingResolver.js +381 -0
- package/dist/resolvers/MeetingResolver.js.map +1 -0
- package/dist/resolvers/RealtimeBridgeResolver.d.ts +88 -3
- package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeBridgeResolver.js +352 -12
- package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +156 -108
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +395 -293
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/RealtimeSessionEventsResolver.d.ts +61 -0
- package/dist/resolvers/RealtimeSessionEventsResolver.d.ts.map +1 -0
- package/dist/resolvers/RealtimeSessionEventsResolver.js +130 -0
- package/dist/resolvers/RealtimeSessionEventsResolver.js.map +1 -0
- package/dist/resolvers/RealtimeSessionVerificationResolver.d.ts +85 -0
- package/dist/resolvers/RealtimeSessionVerificationResolver.d.ts.map +1 -0
- package/dist/resolvers/RealtimeSessionVerificationResolver.js +188 -0
- package/dist/resolvers/RealtimeSessionVerificationResolver.js.map +1 -0
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts +2 -0
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
- package/dist/resolvers/RemoteBrowserActionResolver.js +5 -1
- package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts +10 -0
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +68 -11
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
- package/dist/resolvers/RunTemplateResolver.js +12 -11
- package/dist/resolvers/RunTemplateResolver.js.map +1 -1
- package/dist/resolvers/SyncDataResolver.d.ts.map +1 -1
- package/dist/resolvers/SyncDataResolver.js +6 -2
- package/dist/resolvers/SyncDataResolver.js.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.d.ts +2 -0
- package/dist/resolvers/SyncRolesUsersResolver.d.ts.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.js +23 -6
- package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
- package/dist/resolvers/UserAvatarResolver.d.ts +16 -0
- package/dist/resolvers/UserAvatarResolver.d.ts.map +1 -0
- package/dist/resolvers/UserAvatarResolver.js +124 -0
- package/dist/resolvers/UserAvatarResolver.js.map +1 -0
- package/dist/resolvers/UserResolver.d.ts.map +1 -1
- package/dist/resolvers/UserResolver.js +2 -17
- package/dist/resolvers/UserResolver.js.map +1 -1
- package/dist/resolvers/adhocTimeout.d.ts +7 -0
- package/dist/resolvers/adhocTimeout.d.ts.map +1 -0
- package/dist/resolvers/adhocTimeout.js +12 -0
- package/dist/resolvers/adhocTimeout.js.map +1 -0
- package/dist/resolvers/agentVisionConsentAudit.d.ts +37 -0
- package/dist/resolvers/agentVisionConsentAudit.d.ts.map +1 -0
- package/dist/resolvers/agentVisionConsentAudit.js +63 -0
- package/dist/resolvers/agentVisionConsentAudit.js.map +1 -0
- package/dist/resolvers/avatarInputValidation.d.ts +40 -0
- package/dist/resolvers/avatarInputValidation.d.ts.map +1 -0
- package/dist/resolvers/avatarInputValidation.js +97 -0
- package/dist/resolvers/avatarInputValidation.js.map +1 -0
- package/dist/resolvers/currentUserRoles.d.ts +29 -0
- package/dist/resolvers/currentUserRoles.d.ts.map +1 -1
- package/dist/resolvers/currentUserRoles.js +30 -0
- package/dist/resolvers/currentUserRoles.js.map +1 -1
- package/dist/resolvers/meetings/index.d.ts +5 -0
- package/dist/resolvers/meetings/index.d.ts.map +1 -0
- package/dist/resolvers/meetings/index.js +5 -0
- package/dist/resolvers/meetings/index.js.map +1 -0
- package/dist/resolvers/meetings/meetingHelpers.d.ts +17 -0
- package/dist/resolvers/meetings/meetingHelpers.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingHelpers.js +212 -0
- package/dist/resolvers/meetings/meetingHelpers.js.map +1 -0
- package/dist/resolvers/meetings/meetingMutations.d.ts +18 -0
- package/dist/resolvers/meetings/meetingMutations.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingMutations.js +439 -0
- package/dist/resolvers/meetings/meetingMutations.js.map +1 -0
- package/dist/resolvers/meetings/meetingQueries.d.ts +7 -0
- package/dist/resolvers/meetings/meetingQueries.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingQueries.js +113 -0
- package/dist/resolvers/meetings/meetingQueries.js.map +1 -0
- package/dist/resolvers/meetings/meetingTypes.d.ts +89 -0
- package/dist/resolvers/meetings/meetingTypes.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingTypes.js +345 -0
- package/dist/resolvers/meetings/meetingTypes.js.map +1 -0
- package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
- package/dist/rest/ViewOperationsHandler.js +8 -5
- package/dist/rest/ViewOperationsHandler.js.map +1 -1
- package/dist/services/TaskGraphActionRunner.d.ts +8 -0
- package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphActionRunner.js +27 -1
- package/dist/services/TaskGraphActionRunner.js.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.d.ts +4 -3
- package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.js +13 -7
- package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
- package/dist/services/WorkQueueHostService.d.ts +36 -0
- package/dist/services/WorkQueueHostService.d.ts.map +1 -0
- package/dist/services/WorkQueueHostService.js +97 -0
- package/dist/services/WorkQueueHostService.js.map +1 -0
- package/dist/services/workQueueConfig.d.ts +103 -0
- package/dist/services/workQueueConfig.d.ts.map +1 -0
- package/dist/services/workQueueConfig.js +48 -0
- package/dist/services/workQueueConfig.js.map +1 -0
- package/dist/sharedCache.d.ts +105 -0
- package/dist/sharedCache.d.ts.map +1 -0
- package/dist/sharedCache.js +276 -0
- package/dist/sharedCache.js.map +1 -0
- package/dist/types.d.ts +11 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +97 -94
- package/src/__tests__/ActionResolver.authorization.test.ts +353 -0
- package/src/__tests__/AdhocQueryResolver.bugs.test.ts +14 -35
- package/src/__tests__/AdhocQueryResolver.pagination.test.ts +6 -2
- package/src/__tests__/AdhocQueryResolver.scopeGuard.test.ts +3 -3
- package/src/__tests__/AdhocQueryResolver.sqlAuthorizer.test.ts +104 -0
- package/src/__tests__/BridgeHostInstance.test.ts +232 -0
- package/src/__tests__/ClientToolRequestResolver.test.ts +116 -0
- package/src/__tests__/EntityRecordNameResolver.fieldSecurity.test.ts +27 -72
- package/src/__tests__/FetchEntityVectorsResolver.test.ts +285 -0
- package/src/__tests__/HostInstance.test.ts +84 -0
- package/src/__tests__/HumanHandoffResolver.test.ts +250 -0
- package/src/__tests__/MeetingResolver.test.ts +448 -0
- package/src/__tests__/RealtimeBridgeResolver.test.ts +651 -4
- package/src/__tests__/RealtimeClientSessionResolver.configTamper.test.ts +383 -0
- package/src/__tests__/RealtimeClientSessionResolver.test.ts +682 -58
- package/src/__tests__/RealtimeProxyServer.test.ts +16 -1
- package/src/__tests__/RealtimeRelay.test.ts +876 -0
- package/src/__tests__/RemoteBrowserActionResolver.goalRunLink.test.ts +129 -0
- package/src/__tests__/ResolverBase.adhocReadScope.test.ts +96 -0
- package/src/__tests__/ResolverBase.aggregateScreen.test.ts +174 -0
- package/src/__tests__/ResolverBase.clientOldValues.test.ts +6 -3
- package/src/__tests__/ResolverBase.filterEscaping.test.ts +21 -2
- package/src/__tests__/ResolverBase.includeBinaryFields.test.ts +291 -0
- package/src/__tests__/ReturningVisitorRecap.test.ts +115 -0
- package/src/__tests__/RunAIAgentResolver.branch.test.ts +169 -0
- package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +4 -4
- package/src/__tests__/RunAIAgentResolver.stoppedPredecessor.test.ts +166 -0
- package/src/__tests__/RunTemplateResolver.test.ts +132 -0
- package/src/__tests__/RunViewResolver.cacheCheckForwarding.test.ts +63 -0
- package/src/__tests__/SessionJanitor.test.ts +845 -18
- package/src/__tests__/SessionManager.test.ts +213 -17
- package/src/__tests__/SessionRunIDVerifier.test.ts +251 -0
- package/src/__tests__/SyncRolesUsersResolver.userRoles.test.ts +97 -0
- package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +41 -0
- package/src/__tests__/TaskGraphContinuationDeliverer.test.ts +50 -3
- package/src/__tests__/UserAvatarResolver.test.ts +255 -0
- package/src/__tests__/UserResolver.currentUser.test.ts +16 -1
- package/src/__tests__/WorkQueueHostService.test.ts +133 -0
- package/src/__tests__/actionAuthorization.test.ts +130 -0
- package/src/__tests__/adhocTimeoutClamp.test.ts +26 -0
- package/src/__tests__/agentVisionConsentAudit.test.ts +118 -0
- package/src/__tests__/avatarInputValidation.test.ts +160 -0
- package/src/__tests__/isolatedProviderTransactions.test.ts +169 -0
- package/src/__tests__/magicLinkService.consume.test.ts +6 -0
- package/src/__tests__/magicLinkService.provisionLog.test.ts +5 -0
- package/src/__tests__/newUsers.template.test.ts +151 -0
- package/src/__tests__/newUsers.test.ts +74 -5
- package/src/__tests__/orphanedConversationDetailReconciler.test.ts +17 -2
- package/src/__tests__/postgresqlPoolSettings.test.ts +131 -0
- package/src/__tests__/realtimeSessions/RealtimeSessionEventService.test.ts +201 -0
- package/src/__tests__/realtimeSessions/RealtimeSessionEventsResolver.test.ts +215 -0
- package/src/__tests__/realtimeSessions/RealtimeSessionVerificationResolver.test.ts +102 -0
- package/src/__tests__/realtimeSessions/RealtimeSessionVerificationService.test.ts +349 -0
- package/src/__tests__/realtimeSessions/RealtimeVerifyRouter.test.ts +131 -0
- package/src/__tests__/realtimeSessions/SlidingWindowRateLimiter.test.ts +56 -0
- package/src/__tests__/realtimeSessions/helpers.ts +4 -0
- package/src/__tests__/realtimeSessions/sessionAccess.test.ts +129 -0
- package/src/__tests__/realtimeSessions/sessionConfigStamps.test.ts +124 -0
- package/src/__tests__/realtimeSessions/verificationCore.test.ts +355 -0
- package/src/__tests__/realtimeSessions/verificationPages.test.ts +84 -0
- package/src/__tests__/realtimeSessions/verificationPolicy.test.ts +226 -0
- package/src/__tests__/realtimeSessions/verificationWorkflow.test.ts +633 -0
- package/src/__tests__/resolverBase.fls.test.ts +87 -4
- package/src/__tests__/serverExtensionReservedRoots.test.ts +2 -0
- package/src/__tests__/sharedCache.test.ts +310 -0
- package/src/__tests__/translateBracketsToPG.test.ts +20 -0
- package/src/__tests__/widgetGuestElevation.test.ts +44 -0
- package/src/agentSessions/BridgeHostInstance.ts +32 -0
- package/src/agentSessions/HostInstance.ts +124 -28
- package/src/agentSessions/ReturningVisitorRecap.ts +7 -3
- package/src/agentSessions/SessionJanitor.ts +543 -57
- package/src/agentSessions/SessionManager.ts +125 -25
- package/src/agentSessions/SessionRunIDVerifier.ts +171 -0
- package/src/agentSessions/index.ts +10 -3
- package/src/auth/actionAuthorization.ts +116 -0
- package/src/auth/index.ts +3 -8
- package/src/auth/magicLink/MagicLinkService.ts +6 -3
- package/src/auth/newUsers.ts +99 -11
- package/src/config.ts +255 -3
- package/src/context.ts +26 -34
- package/src/generated/graphql-schemas/__mj.ts +22701 -18838
- package/src/generic/OrphanedConversationDetailReconciler.ts +3 -3
- package/src/generic/ResolverBase.ts +210 -36
- package/src/generic/RunViewResolver.ts +37 -2
- package/src/index.ts +236 -170
- package/src/isolatedProvider.ts +22 -0
- package/src/postgresqlCompat.ts +21 -0
- package/src/postgresqlPoolSettings.ts +213 -0
- package/src/realtimeProxy/RealtimeProxyServer.ts +148 -16
- package/src/realtimeProxy/RealtimeRelayTunnel.ts +645 -0
- package/src/realtimeSessions/README.md +179 -0
- package/src/realtimeSessions/RealtimeSessionEventService.ts +274 -0
- package/src/realtimeSessions/RealtimeSessionVerificationService.ts +167 -0
- package/src/realtimeSessions/RealtimeVerifyRouter.ts +108 -0
- package/src/realtimeSessions/SessionVerificationPortsBase.ts +189 -0
- package/src/realtimeSessions/SlidingWindowRateLimiter.ts +92 -0
- package/src/realtimeSessions/index.ts +16 -0
- package/src/realtimeSessions/sessionAccess.ts +141 -0
- package/src/realtimeSessions/sessionConfigStamps.ts +123 -0
- package/src/realtimeSessions/subscriptionAuthorization.ts +116 -0
- package/src/realtimeSessions/verificationCore.ts +502 -0
- package/src/realtimeSessions/verificationPages.ts +149 -0
- package/src/realtimeSessions/verificationPolicy.ts +321 -0
- package/src/realtimeSessions/verificationWorkflow.ts +702 -0
- package/src/realtimeWidget/widgetGuestElevation.ts +34 -2
- package/src/resolvers/ActionResolver.ts +84 -21
- package/src/resolvers/AdhocQueryResolver.ts +105 -187
- package/src/resolvers/CacheStatsResolver.ts +89 -2
- package/src/resolvers/ClientToolRequestResolver.ts +25 -7
- package/src/resolvers/EntityRecordNameResolver.ts +9 -51
- package/src/resolvers/FetchEntityVectorsResolver.ts +97 -2
- package/src/resolvers/GetDataResolver.ts +10 -0
- package/src/resolvers/HumanHandoffResolver.ts +312 -0
- package/src/resolvers/MCPResolver.ts +8 -5
- package/src/resolvers/MeetingResolver.ts +438 -0
- package/src/resolvers/RealtimeBridgeResolver.ts +385 -19
- package/src/resolvers/RealtimeClientSessionResolver.ts +503 -344
- package/src/resolvers/RealtimeSessionEventsResolver.ts +133 -0
- package/src/resolvers/RealtimeSessionVerificationResolver.ts +174 -0
- package/src/resolvers/RemoteBrowserActionResolver.ts +11 -1
- package/src/resolvers/RunAIAgentResolver.ts +78 -11
- package/src/resolvers/RunTemplateResolver.ts +13 -12
- package/src/resolvers/SyncDataResolver.ts +6 -2
- package/src/resolvers/SyncRolesUsersResolver.ts +24 -6
- package/src/resolvers/UserAvatarResolver.ts +111 -0
- package/src/resolvers/UserResolver.ts +2 -16
- package/src/resolvers/adhocTimeout.ts +12 -0
- package/src/resolvers/agentVisionConsentAudit.ts +82 -0
- package/src/resolvers/avatarInputValidation.ts +123 -0
- package/src/resolvers/currentUserRoles.ts +32 -1
- package/src/resolvers/meetings/index.ts +4 -0
- package/src/resolvers/meetings/meetingHelpers.ts +307 -0
- package/src/resolvers/meetings/meetingMutations.ts +556 -0
- package/src/resolvers/meetings/meetingQueries.ts +172 -0
- package/src/resolvers/meetings/meetingTypes.ts +238 -0
- package/src/rest/ViewOperationsHandler.ts +8 -5
- package/src/services/TaskGraphActionRunner.ts +29 -1
- package/src/services/TaskGraphContinuationDeliverer.ts +13 -7
- package/src/services/WorkQueueHostService.ts +129 -0
- package/src/services/workQueueConfig.ts +52 -0
- package/src/sharedCache.ts +302 -0
- package/src/types.ts +12 -0
- package/dist/resolvers/adhoc-query-helpers.d.ts +0 -33
- package/dist/resolvers/adhoc-query-helpers.d.ts.map +0 -1
- package/dist/resolvers/adhoc-query-helpers.js +0 -50
- package/dist/resolvers/adhoc-query-helpers.js.map +0 -1
- package/src/__tests__/adhoc-query-helpers.test.ts +0 -50
- package/src/resolvers/adhoc-query-helpers.ts +0 -67
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview GraphQL subscription that delivers **realtime session events** to the session they are
|
|
3
|
+
* addressed to — `subscription RealtimeSessionEvents(agentSessionId: String!)`.
|
|
4
|
+
*
|
|
5
|
+
* The event contract lives in `@memberjunction/ai-core-plus` (`realtime-session-events.ts`); the publisher
|
|
6
|
+
* apps call is `RealtimeSessionEventService`. This resolver is the delivery edge, and its whole job is
|
|
7
|
+
* authorization, done twice on purpose:
|
|
8
|
+
*
|
|
9
|
+
* 1. **At subscribe time** ({@link AuthorizeRealtimeSessionSubscription}). The session row is loaded and
|
|
10
|
+
* {@link AuthorizeSessionAccess} must allow the connection's authenticated principal: the session's
|
|
11
|
+
* `UserID` must be the caller's user and — for an anonymous widget guest, who shares one Anonymous
|
|
12
|
+
* user with every other guest — the guest's signed per-session scope must match the session's
|
|
13
|
+
* conversation. A refusal is an error on THIS subscription only (the socket and its other
|
|
14
|
+
* subscriptions are untouched) and is the same "not found" whether the session is missing or simply
|
|
15
|
+
* not yours, so ids cannot be probed.
|
|
16
|
+
* 2. **For every event** ({@link RealtimeSessionEventsFilter}). Each published event carries the owner and
|
|
17
|
+
* scope snapshot of its session; the filter re-applies the same decision against the CONNECTION's
|
|
18
|
+
* identity. Authorization therefore does not rest on a single check at one moment, and a replicated
|
|
19
|
+
* event from another server instance is judged exactly like a local one.
|
|
20
|
+
*
|
|
21
|
+
* In neither check does a client-supplied value decide who receives anything: `agentSessionId` only
|
|
22
|
+
* selects WHICH of the caller's own sessions to listen to; the identity it is checked against comes from
|
|
23
|
+
* the server-authenticated connection context.
|
|
24
|
+
*
|
|
25
|
+
* Events have no replay. A subscriber that was disconnected when an event fired recovers durable state
|
|
26
|
+
* from `RealtimeSessionVerificationStatus` (and from the session itself).
|
|
27
|
+
*
|
|
28
|
+
* @module @memberjunction/server
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { Resolver, Subscription, Root, Arg, ObjectType, Field } from 'type-graphql';
|
|
32
|
+
import { withFilter } from 'graphql-subscriptions';
|
|
33
|
+
import { PubSubManager } from '../generic/PubSubManager.js';
|
|
34
|
+
import {
|
|
35
|
+
AuthorizeRealtimeSessionSubscription,
|
|
36
|
+
RealtimeSessionEventsFilter,
|
|
37
|
+
type RealtimeSessionEventsArgs,
|
|
38
|
+
type RealtimeSessionEventsContext,
|
|
39
|
+
type SubscriptionAuthorizationDeps,
|
|
40
|
+
DEFAULT_SUBSCRIPTION_AUTHORIZATION_DEPS,
|
|
41
|
+
} from '../realtimeSessions/subscriptionAuthorization.js';
|
|
42
|
+
import { REALTIME_SESSION_EVENTS_TOPIC, type RealtimeSessionEventTopicPayload } from '../realtimeSessions/RealtimeSessionEventService.js';
|
|
43
|
+
|
|
44
|
+
/** One session event as the client receives it. The routing snapshot is deliberately NOT exposed. */
|
|
45
|
+
@ObjectType()
|
|
46
|
+
export class RealtimeSessionEventNotification {
|
|
47
|
+
/** Event type, e.g. `identity.verified`. Clients ignore types they do not know. */
|
|
48
|
+
@Field(() => String)
|
|
49
|
+
Type: string;
|
|
50
|
+
|
|
51
|
+
/** The session the event is addressed to. */
|
|
52
|
+
@Field(() => String)
|
|
53
|
+
AgentSessionID: string;
|
|
54
|
+
|
|
55
|
+
/** ISO-8601 instant the server emitted the event. */
|
|
56
|
+
@Field(() => String)
|
|
57
|
+
OccurredAt: string;
|
|
58
|
+
|
|
59
|
+
/** The event payload, JSON-encoded. Parse with `ParseRealtimeSessionEvent` from `@memberjunction/ai-core-plus`. */
|
|
60
|
+
@Field(() => String)
|
|
61
|
+
PayloadJson: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Builds the event stream for one authorized subscription: the shared topic, narrowed by
|
|
66
|
+
* {@link RealtimeSessionEventsFilter}.
|
|
67
|
+
*/
|
|
68
|
+
function openEventStream(
|
|
69
|
+
root: unknown,
|
|
70
|
+
args: RealtimeSessionEventsArgs,
|
|
71
|
+
context: RealtimeSessionEventsContext,
|
|
72
|
+
info: unknown,
|
|
73
|
+
): AsyncIterator<RealtimeSessionEventTopicPayload> {
|
|
74
|
+
const engine = PubSubManager.Instance.PubSubEngine;
|
|
75
|
+
if (!engine) {
|
|
76
|
+
throw new Error('Realtime session events are unavailable: PubSub is not configured.');
|
|
77
|
+
}
|
|
78
|
+
const filtered = withFilter(
|
|
79
|
+
() => engine.asyncIterator<RealtimeSessionEventTopicPayload>(REALTIME_SESSION_EVENTS_TOPIC),
|
|
80
|
+
(payload: RealtimeSessionEventTopicPayload, filterArgs: RealtimeSessionEventsArgs, filterContext: RealtimeSessionEventsContext) =>
|
|
81
|
+
RealtimeSessionEventsFilter({ payload, args: filterArgs, context: filterContext }),
|
|
82
|
+
);
|
|
83
|
+
return filtered(root, args, context, info);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The `subscribe` function: authorize FIRST (asynchronously), then open the stream. Exported (with an
|
|
88
|
+
* injectable `deps` argument graphql-js never passes) so the whole path is testable end to end.
|
|
89
|
+
*
|
|
90
|
+
* graphql-js awaits the value `subscribe` returns and turns a thrown error into a per-operation error
|
|
91
|
+
* result, which is exactly what a refusal should be (a failure of this subscription, not the socket).
|
|
92
|
+
*/
|
|
93
|
+
export async function AuthorizeAndSubscribeToRealtimeSessionEvents(
|
|
94
|
+
root: unknown,
|
|
95
|
+
args: RealtimeSessionEventsArgs,
|
|
96
|
+
context: RealtimeSessionEventsContext,
|
|
97
|
+
info: unknown,
|
|
98
|
+
deps: SubscriptionAuthorizationDeps = DEFAULT_SUBSCRIPTION_AUTHORIZATION_DEPS,
|
|
99
|
+
): Promise<AsyncIterator<RealtimeSessionEventTopicPayload>> {
|
|
100
|
+
await AuthorizeRealtimeSessionSubscription(args, context, deps);
|
|
101
|
+
return openEventStream(root, args, context, info);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@Resolver()
|
|
105
|
+
export class RealtimeSessionEventsResolver {
|
|
106
|
+
/**
|
|
107
|
+
* Subscribe to the events addressed to one of YOUR realtime sessions.
|
|
108
|
+
*
|
|
109
|
+
* @param agentSessionId - the `MJ: AI Agent Sessions` id to listen to. Must be a session the
|
|
110
|
+
* authenticated connection owns (and, for an anonymous widget guest, whose signed scope matches);
|
|
111
|
+
* otherwise the subscription is refused.
|
|
112
|
+
*/
|
|
113
|
+
@Subscription(() => RealtimeSessionEventNotification, {
|
|
114
|
+
// graphql-subscriptions types `ResolverFn` as returning a bare AsyncIterator, but graphql-js
|
|
115
|
+
// AWAITS what `subscribe` returns (execute/subscribe.js: `await resolveFn(...)`), and awaiting is
|
|
116
|
+
// the only way to authorize against the database before opening the stream. The typing gap is in
|
|
117
|
+
// the library; `@ts-expect-error` (not a cast) so this fails to compile the day the type is fixed.
|
|
118
|
+
// @ts-expect-error subscribe may return Promise<AsyncIterator>; see comment above
|
|
119
|
+
subscribe: AuthorizeAndSubscribeToRealtimeSessionEvents,
|
|
120
|
+
})
|
|
121
|
+
RealtimeSessionEvents(
|
|
122
|
+
@Root() payload: RealtimeSessionEventTopicPayload,
|
|
123
|
+
@Arg('agentSessionId', () => String) _agentSessionId: string,
|
|
124
|
+
): RealtimeSessionEventNotification {
|
|
125
|
+
// OwnerUserID / ScopeKey are server-internal filter keys and are intentionally not returned.
|
|
126
|
+
return {
|
|
127
|
+
Type: payload.Type,
|
|
128
|
+
AgentSessionID: payload.AgentSessionID,
|
|
129
|
+
OccurredAt: payload.OccurredAt,
|
|
130
|
+
PayloadJson: payload.PayloadJson,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview GraphQL operations for **mid-session identity verification** of a realtime session.
|
|
3
|
+
*
|
|
4
|
+
* A person talking to an agent (often an anonymous widget guest) proves they control an email address
|
|
5
|
+
* without leaving the conversation. Three operations, all callable only as the session's OWN principal:
|
|
6
|
+
*
|
|
7
|
+
* | Operation | Kind | Purpose |
|
|
8
|
+
* |---|---|---|
|
|
9
|
+
* | `RequestRealtimeSessionVerification(agentSessionId, name, email)` | mutation | store a pending verification and email a one-time link + a typed code |
|
|
10
|
+
* | `SubmitRealtimeSessionVerificationCode(agentSessionId, code)` | mutation | redeem the typed code (the fallback for opening the email on another device) |
|
|
11
|
+
* | `RealtimeSessionVerificationStatus(agentSessionId)` | query | read the current state — the durable backstop for a missed `identity.verified` event |
|
|
12
|
+
*
|
|
13
|
+
* The emailed link is redeemed on a different surface — the public `GET/POST /realtime/verify` routes
|
|
14
|
+
* (`RealtimeVerifyRouter`) — because the person opening it holds no MJ token.
|
|
15
|
+
*
|
|
16
|
+
* The decisions, policy and limits live in `realtimeSessions/verificationWorkflow.ts`; this resolver
|
|
17
|
+
* only identifies the caller from the server-authenticated payload and maps the result. Note that it
|
|
18
|
+
* reads `userPayload.userRecord` directly and does **not** use `ResolverBase.GetUserFromPayload`: that
|
|
19
|
+
* helper falls back to an email lookup in the user cache, which would drop an anonymous guest's
|
|
20
|
+
* per-session scope — and a scope-less anonymous principal must never be treated as a named owner of
|
|
21
|
+
* the shared Anonymous user's sessions. No authenticated `userRecord` means no access.
|
|
22
|
+
*
|
|
23
|
+
* @module @memberjunction/server
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { Resolver, Mutation, Query, Arg, Ctx, ObjectType, Field, Int } from 'type-graphql';
|
|
27
|
+
import type { UserInfo } from '@memberjunction/core';
|
|
28
|
+
import type { AppContext } from '../types.js';
|
|
29
|
+
import { GetReadWriteProvider } from '../util.js';
|
|
30
|
+
import { NoLog } from '../logging/NoLog.js';
|
|
31
|
+
import { RealtimeSessionVerificationService } from '../realtimeSessions/RealtimeSessionVerificationService.js';
|
|
32
|
+
import type { PrincipalCaller, VerificationOperationResult } from '../realtimeSessions/verificationWorkflow.js';
|
|
33
|
+
|
|
34
|
+
/** Result of every verification operation. */
|
|
35
|
+
@ObjectType()
|
|
36
|
+
export class RealtimeSessionVerificationResult {
|
|
37
|
+
/** True when the operation did what was asked. */
|
|
38
|
+
@Field(() => Boolean)
|
|
39
|
+
Success: boolean;
|
|
40
|
+
|
|
41
|
+
/** The session's verification state after the operation: `unverified`, `pending` or `verified`. */
|
|
42
|
+
@Field(() => String)
|
|
43
|
+
VerificationState: string;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Machine-readable failure reason when `Success` is false — e.g. `invalid_email`, `consumer_domain`,
|
|
47
|
+
* `domain_blocked`, `rate_limited`, `send_limit_reached`, `invalid_code`, `attempts_exhausted`,
|
|
48
|
+
* `code_expired`, `session_not_found`, `session_closed`, `verification_unavailable`.
|
|
49
|
+
*/
|
|
50
|
+
@Field(() => String, { nullable: true })
|
|
51
|
+
ErrorCode?: string;
|
|
52
|
+
|
|
53
|
+
/** A message safe to show a person. */
|
|
54
|
+
@Field(() => String, { nullable: true })
|
|
55
|
+
Message?: string;
|
|
56
|
+
|
|
57
|
+
/** ISO instant the pending link/code expires. */
|
|
58
|
+
@Field(() => String, { nullable: true })
|
|
59
|
+
ExpiresAt?: string;
|
|
60
|
+
|
|
61
|
+
/** Verification emails this session may still trigger. */
|
|
62
|
+
@Field(() => Int, { nullable: true })
|
|
63
|
+
SendsRemaining?: number;
|
|
64
|
+
|
|
65
|
+
/** Wrong code entries left before the pending verification is voided. */
|
|
66
|
+
@Field(() => Int, { nullable: true })
|
|
67
|
+
AttemptsRemaining?: number;
|
|
68
|
+
|
|
69
|
+
/** Seconds to wait before retrying, when rate limited. */
|
|
70
|
+
@Field(() => Int, { nullable: true })
|
|
71
|
+
RetryAfterSeconds?: number;
|
|
72
|
+
|
|
73
|
+
/** The verified address, once verified. */
|
|
74
|
+
@Field(() => String, { nullable: true })
|
|
75
|
+
VerifiedEmail?: string;
|
|
76
|
+
|
|
77
|
+
/** ISO instant verification completed. */
|
|
78
|
+
@Field(() => String, { nullable: true })
|
|
79
|
+
VerifiedAt?: string;
|
|
80
|
+
|
|
81
|
+
/** The session's new absolute deadline (ISO) when verification extended it. The server enforces it; this is informational. */
|
|
82
|
+
@Field(() => String, { nullable: true })
|
|
83
|
+
MaxSessionDeadlineIso?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Maps the workflow result onto the GraphQL type. */
|
|
87
|
+
function toResult(result: VerificationOperationResult): RealtimeSessionVerificationResult {
|
|
88
|
+
return { ...result };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The result for a caller with no usable authenticated identity. */
|
|
92
|
+
function notAuthenticated(): RealtimeSessionVerificationResult {
|
|
93
|
+
return { Success: false, VerificationState: 'unverified', ErrorCode: 'session_not_found', Message: 'Session not found.' };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@Resolver()
|
|
97
|
+
export class RealtimeSessionVerificationResolver {
|
|
98
|
+
/**
|
|
99
|
+
* Ask to verify an email for your realtime session. Applies the session's policy (business-domain
|
|
100
|
+
* rules, blocked domains, send and rate limits), then emails a one-time link plus a short code.
|
|
101
|
+
*
|
|
102
|
+
* @param agentSessionId - your `MJ: AI Agent Sessions` id
|
|
103
|
+
* @param name - the name to greet in the email (sanitised and length-capped server-side)
|
|
104
|
+
* @param email - the address to verify
|
|
105
|
+
*/
|
|
106
|
+
@Mutation(() => RealtimeSessionVerificationResult)
|
|
107
|
+
async RequestRealtimeSessionVerification(
|
|
108
|
+
@Arg('agentSessionId', () => String) agentSessionId: string,
|
|
109
|
+
@Arg('name', () => String) name: string,
|
|
110
|
+
@Arg('email', () => String) @NoLog email: string,
|
|
111
|
+
@Ctx() context: AppContext,
|
|
112
|
+
): Promise<RealtimeSessionVerificationResult> {
|
|
113
|
+
const caller = this.callerFrom(context);
|
|
114
|
+
if (!caller) {
|
|
115
|
+
return notAuthenticated();
|
|
116
|
+
}
|
|
117
|
+
return toResult(
|
|
118
|
+
await RealtimeSessionVerificationService.Instance.RequestVerification(
|
|
119
|
+
{ AgentSessionID: agentSessionId, Name: name, Email: email },
|
|
120
|
+
caller,
|
|
121
|
+
),
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Redeem the typed code from the verification email. Single use; voided after too many wrong entries.
|
|
127
|
+
*
|
|
128
|
+
* @param agentSessionId - your `MJ: AI Agent Sessions` id
|
|
129
|
+
* @param code - the code from the email (spaces and dashes are ignored)
|
|
130
|
+
*/
|
|
131
|
+
@Mutation(() => RealtimeSessionVerificationResult)
|
|
132
|
+
async SubmitRealtimeSessionVerificationCode(
|
|
133
|
+
@Arg('agentSessionId', () => String) agentSessionId: string,
|
|
134
|
+
@Arg('code', () => String) @NoLog code: string,
|
|
135
|
+
@Ctx() context: AppContext,
|
|
136
|
+
): Promise<RealtimeSessionVerificationResult> {
|
|
137
|
+
const caller = this.callerFrom(context);
|
|
138
|
+
if (!caller) {
|
|
139
|
+
return notAuthenticated();
|
|
140
|
+
}
|
|
141
|
+
return toResult(await RealtimeSessionVerificationService.Instance.SubmitCode({ AgentSessionID: agentSessionId, Code: code }, caller));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Read the current verification state of your realtime session. Use it after a reconnect: session
|
|
146
|
+
* events have no replay, so this is how a client that missed `identity.verified` finds out.
|
|
147
|
+
*
|
|
148
|
+
* @param agentSessionId - your `MJ: AI Agent Sessions` id
|
|
149
|
+
*/
|
|
150
|
+
@Query(() => RealtimeSessionVerificationResult)
|
|
151
|
+
async RealtimeSessionVerificationStatus(
|
|
152
|
+
@Arg('agentSessionId', () => String) agentSessionId: string,
|
|
153
|
+
@Ctx() context: AppContext,
|
|
154
|
+
): Promise<RealtimeSessionVerificationResult> {
|
|
155
|
+
const caller = this.callerFrom(context);
|
|
156
|
+
if (!caller) {
|
|
157
|
+
return notAuthenticated();
|
|
158
|
+
}
|
|
159
|
+
return toResult(await RealtimeSessionVerificationService.Instance.GetStatus({ AgentSessionID: agentSessionId }, caller));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Builds the caller from the server-authenticated payload, or null when there is none. Identity comes
|
|
164
|
+
* from `userPayload.userRecord` ONLY (see the module header).
|
|
165
|
+
*/
|
|
166
|
+
private callerFrom(context: AppContext): PrincipalCaller | null {
|
|
167
|
+
const contextUser = context.userPayload?.userRecord as UserInfo | undefined;
|
|
168
|
+
const provider = GetReadWriteProvider(context.providers);
|
|
169
|
+
if (!contextUser?.ID || !provider) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
return { Kind: 'principal', ContextUser: contextUser, Provider: provider, ClientIp: context.ClientIp };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -24,6 +24,7 @@ import { MJAIAgentSessionEntity, MJAIAgentEntity } from '@memberjunction/core-en
|
|
|
24
24
|
import { NormalizeInstanceKey, RemoteBrowserEngine } from '@memberjunction/remote-browser-server';
|
|
25
25
|
import { BeginBrowserGoalStep, FinalizeBrowserGoalStep, ExtractCoAgentRunID } from '../agentSessions/remoteBrowserGoalEngine.js';
|
|
26
26
|
import { RemoteBrowserGoalRegistry } from '../agentSessions/remoteBrowserGoalRegistry.js';
|
|
27
|
+
import { SessionRunIDVerifier } from '../agentSessions/SessionRunIDVerifier.js';
|
|
27
28
|
import { randomUUID } from 'node:crypto';
|
|
28
29
|
import {
|
|
29
30
|
RemoteBrowserAction,
|
|
@@ -261,6 +262,9 @@ export class RemoteBrowserActionResolver extends ResolverBase {
|
|
|
261
262
|
*/
|
|
262
263
|
private startedScreencasts = new Map<string, number>();
|
|
263
264
|
|
|
265
|
+
/** Checks the co-agent run id read from a session's owner-editable `Config_` before a step is attached to it. */
|
|
266
|
+
private readonly runIDVerifier = new SessionRunIDVerifier();
|
|
267
|
+
|
|
264
268
|
/**
|
|
265
269
|
* Surfaces whose live tab-audio stream this resolver has already started, keyed by
|
|
266
270
|
* {@link RemoteBrowserActionResolver.streamKey} for the same reason the screencast map is. Entries
|
|
@@ -400,7 +404,13 @@ export class RemoteBrowserActionResolver extends ResolverBase {
|
|
|
400
404
|
const providerName = await this.resolveProviderName(session, contextUser, provider);
|
|
401
405
|
// Observability: nest this goal's many prompt runs under ONE "Browser goal" step on the realtime
|
|
402
406
|
// co-agent run (when the session has one). Best-effort — a null step just means the goal runs unlinked.
|
|
403
|
-
|
|
407
|
+
// The owner can edit Config_, so its run id is used only when it is a run of this session.
|
|
408
|
+
const { CoAgentRunID: coAgentRunID } = await this.runIDVerifier.Verify(
|
|
409
|
+
session.ID,
|
|
410
|
+
{ CoAgentRunID: ExtractCoAgentRunID(session.Config_) },
|
|
411
|
+
contextUser,
|
|
412
|
+
provider,
|
|
413
|
+
);
|
|
404
414
|
const goalStep = await BeginBrowserGoalStep(provider, contextUser, coAgentRunID, goal);
|
|
405
415
|
|
|
406
416
|
// ASYNC START: a goal loop can run for minutes; do NOT hold this request open for it (browser
|
|
@@ -293,7 +293,9 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
293
293
|
taskGraphDebug?: string,
|
|
294
294
|
/** History floor for a conversation run — threaded into ExecuteAgentParams.ConversationHistoryFrom,
|
|
295
295
|
* so the framework's own conversation reads (retrieval tools, artifacts, compaction) honour it too. */
|
|
296
|
-
conversationHistoryFrom?: Date
|
|
296
|
+
conversationHistoryFrom?: Date,
|
|
297
|
+
/** The conversation branch of the run — threaded into ExecuteAgentParams.ConversationBranchID. */
|
|
298
|
+
conversationBranchId?: string | null
|
|
297
299
|
): Promise<AIAgentRunResult> {
|
|
298
300
|
const startTime = Date.now();
|
|
299
301
|
// Best-effort handle for persistInFlightAgentFailure. Populated from a
|
|
@@ -362,6 +364,7 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
362
364
|
requestedSkillIDs: requestedSkillIDs,
|
|
363
365
|
taskGraphDebug: parseTaskGraphDebug(taskGraphDebug),
|
|
364
366
|
ConversationHistoryFrom: conversationHistoryFrom,
|
|
367
|
+
...(conversationBranchId ? { ConversationBranchID: conversationBranchId } : {}),
|
|
365
368
|
data: parsedData,
|
|
366
369
|
context: {
|
|
367
370
|
dataSource: dataSource
|
|
@@ -752,6 +755,45 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
752
755
|
}
|
|
753
756
|
}
|
|
754
757
|
|
|
758
|
+
/**
|
|
759
|
+
* The ID of this agent's newest root run in the conversation IF the user stopped it, else
|
|
760
|
+
* null. The query spans the runs that could be the predecessor (settled, or stopped by the
|
|
761
|
+
* user) and the newest decides, so a stopped run that a later settled run already followed
|
|
762
|
+
* is not chained to again. Uses the same filter and recognizer as `BaseAgent.findUserStoppedPredecessorRunId`
|
|
763
|
+
* (`MJAIAgentRunEntityExtended.BuildStoppedPredecessorFilter` / `IsUserStopped`), which carries the stopped run's
|
|
764
|
+
* completed results into the new run's context, so the two cannot pick different runs.
|
|
765
|
+
* Fail-soft: a lookup failure means no chaining, never a failed turn.
|
|
766
|
+
*/
|
|
767
|
+
private async findUserStoppedPredecessorRunId(
|
|
768
|
+
conversationId: string,
|
|
769
|
+
agentId: string,
|
|
770
|
+
branchId: string | null,
|
|
771
|
+
contextUser: UserInfo,
|
|
772
|
+
provider: IMetadataProvider
|
|
773
|
+
): Promise<string | null> {
|
|
774
|
+
try {
|
|
775
|
+
// The run must have replied on this turn's branch path: a run stopped on a sibling branch is not
|
|
776
|
+
// this path's history. The trunk needs no branch rows; a branch needs them to walk its ancestry.
|
|
777
|
+
const branches = branchId ? await ConversationEngine.LoadBranchesFresh(conversationId, contextUser, provider) : [];
|
|
778
|
+
const rv = RunView.FromMetadataProvider(provider);
|
|
779
|
+
const newest = await rv.RunView<{ ID: string; Status: string; CancellationReason: string | null }>({
|
|
780
|
+
EntityName: 'MJ: AI Agent Runs',
|
|
781
|
+
ExtraFilter: MJAIAgentRunEntityExtended.BuildStoppedPredecessorFilter(conversationId, agentId, branchId, branches),
|
|
782
|
+
OrderBy: '__mj_CreatedAt DESC',
|
|
783
|
+
MaxRows: 1,
|
|
784
|
+
Fields: ['ID', 'Status', 'CancellationReason'],
|
|
785
|
+
ResultType: 'simple',
|
|
786
|
+
// The row this looks for was written by the Stop button moments ago; read the database, not a cache.
|
|
787
|
+
BypassCache: true,
|
|
788
|
+
}, contextUser);
|
|
789
|
+
const run = newest.Success ? newest.Results?.[0] : undefined;
|
|
790
|
+
return run && MJAIAgentRunEntityExtended.IsUserStopped(run.Status, run.CancellationReason) ? run.ID : null;
|
|
791
|
+
} catch (error) {
|
|
792
|
+
LogError(`findUserStoppedPredecessorRunId failed; the turn runs unchained: ${error instanceof Error ? error.message : String(error)}`);
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
755
797
|
/**
|
|
756
798
|
* When a continuation run completes (lastRunId was provided), sync the corresponding
|
|
757
799
|
* AIAgentRequest by marking it as responded. This keeps the dashboard accurate when
|
|
@@ -934,6 +976,24 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
934
976
|
throw new Error(`Conversation detail ${conversationDetailId} not found`);
|
|
935
977
|
}
|
|
936
978
|
const conversationId = currentDetail.ConversationID;
|
|
979
|
+
// The branch the placeholder row was created on decides which path the run reads.
|
|
980
|
+
const branchId: string | null = currentDetail.BranchID ?? null;
|
|
981
|
+
|
|
982
|
+
// A turn that follows a run the user STOPPED continues that run rather than starting
|
|
983
|
+
// over: chain it by LastRunID (the chat client never sends one) and, when the caller
|
|
984
|
+
// supplied no payload, roll the stopped run's FinalPayload into this run's start.
|
|
985
|
+
let effectiveLastRunId = lastRunId;
|
|
986
|
+
let effectiveAutoPopulate = autoPopulateLastRunPayload;
|
|
987
|
+
if (!effectiveLastRunId) {
|
|
988
|
+
const stoppedRunId = await this.findUserStoppedPredecessorRunId(conversationId, effectiveAgentId, branchId, currentUser, p);
|
|
989
|
+
if (stoppedRunId) {
|
|
990
|
+
effectiveLastRunId = stoppedRunId;
|
|
991
|
+
if (effectiveAutoPopulate === undefined && payload == null) {
|
|
992
|
+
effectiveAutoPopulate = true;
|
|
993
|
+
}
|
|
994
|
+
LogStatus(`↩️ Continuing from user-stopped run ${stoppedRunId} (chained by LastRunID)`);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
937
997
|
|
|
938
998
|
// Load conversation history with attachments from DB
|
|
939
999
|
const messages = await this.loadConversationHistoryWithAttachments(
|
|
@@ -944,7 +1004,8 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
944
1004
|
// The UI creates the agent-response placeholder row ('⏳ ...') before invoking
|
|
945
1005
|
// this mutation — exclude it so the model never sees an empty assistant turn.
|
|
946
1006
|
[conversationDetailId],
|
|
947
|
-
historyFrom
|
|
1007
|
+
historyFrom,
|
|
1008
|
+
branchId
|
|
948
1009
|
);
|
|
949
1010
|
|
|
950
1011
|
// Convert to JSON string for the existing executeAIAgent method
|
|
@@ -955,11 +1016,12 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
955
1016
|
// The client will receive the result via WebSocket PubSub completion event.
|
|
956
1017
|
this.executeAgentInBackground(
|
|
957
1018
|
p, dataSource, effectiveAgentId, effectiveUserPayload, messagesJson, sessionId, pubSub,
|
|
958
|
-
data, payload,
|
|
1019
|
+
data, payload, effectiveLastRunId, effectiveAutoPopulate, configurationId,
|
|
959
1020
|
conversationDetailId, createArtifacts || false, createNotification || false,
|
|
960
1021
|
sourceArtifactId, sourceArtifactVersionId, conversationId, planMode, requestedSkillIDs,
|
|
961
1022
|
undefined, // taskGraphDebug
|
|
962
|
-
historyFrom
|
|
1023
|
+
historyFrom,
|
|
1024
|
+
branchId
|
|
963
1025
|
);
|
|
964
1026
|
|
|
965
1027
|
LogStatus(`🔥 Fire-and-forget: Agent ${effectiveAgentId} execution started in background for session ${sessionId}`);
|
|
@@ -982,8 +1044,8 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
982
1044
|
data,
|
|
983
1045
|
payload,
|
|
984
1046
|
undefined, // templateData
|
|
985
|
-
|
|
986
|
-
|
|
1047
|
+
effectiveLastRunId,
|
|
1048
|
+
effectiveAutoPopulate,
|
|
987
1049
|
configurationId,
|
|
988
1050
|
conversationDetailId,
|
|
989
1051
|
createArtifacts || false,
|
|
@@ -995,7 +1057,8 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
995
1057
|
planMode,
|
|
996
1058
|
requestedSkillIDs,
|
|
997
1059
|
undefined, // taskGraphDebug
|
|
998
|
-
historyFrom
|
|
1060
|
+
historyFrom,
|
|
1061
|
+
branchId
|
|
999
1062
|
);
|
|
1000
1063
|
} catch (error) {
|
|
1001
1064
|
const errorMessage = (error as Error).message || 'Unknown error loading conversation history';
|
|
@@ -1217,7 +1280,9 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
1217
1280
|
requestedSkillIDs?: string[],
|
|
1218
1281
|
taskGraphDebug?: string,
|
|
1219
1282
|
/** History floor — threaded through to ExecuteAgentParams.ConversationHistoryFrom. */
|
|
1220
|
-
conversationHistoryFrom?: Date
|
|
1283
|
+
conversationHistoryFrom?: Date,
|
|
1284
|
+
/** The conversation branch of the run — threaded through to ExecuteAgentParams.ConversationBranchID. */
|
|
1285
|
+
conversationBranchId?: string | null
|
|
1221
1286
|
): void {
|
|
1222
1287
|
// Ref the liveness pulse reads to enrich heartbeats once the run is created.
|
|
1223
1288
|
const runRef: { current: MJAIAgentRunEntityExtended | null } = { current: null };
|
|
@@ -1237,7 +1302,7 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
1237
1302
|
data, payload, undefined, lastRunId, autoPopulateLastRunPayload,
|
|
1238
1303
|
configurationId, conversationDetailId, createArtifacts, createNotification,
|
|
1239
1304
|
sourceArtifactId, sourceArtifactVersionId, conversationId, runRef, planMode, requestedSkillIDs, taskGraphDebug,
|
|
1240
|
-
conversationHistoryFrom
|
|
1305
|
+
conversationHistoryFrom, conversationBranchId
|
|
1241
1306
|
).catch((error: unknown) => {
|
|
1242
1307
|
// Background execution failed unexpectedly (executeAIAgent has its own try-catch,
|
|
1243
1308
|
// so this would only fire for truly unexpected errors).
|
|
@@ -1277,7 +1342,9 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
1277
1342
|
provider: IMetadataProvider,
|
|
1278
1343
|
excludeDetailIds?: string[],
|
|
1279
1344
|
/** History floor: only rows written at or after it are loaded, and no summary is used. */
|
|
1280
|
-
historyFrom?: Date
|
|
1345
|
+
historyFrom?: Date,
|
|
1346
|
+
/** The branch whose path to load; null reads the trunk. */
|
|
1347
|
+
branchId?: string | null
|
|
1281
1348
|
): Promise<ChatMessage[]> {
|
|
1282
1349
|
// Context windowing (summary boundary + raw tail, or legacy last-N when no
|
|
1283
1350
|
// summary exists) shares ONE fold implementation with all other callers —
|
|
@@ -1294,7 +1361,7 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
1294
1361
|
// the agent against zero history.
|
|
1295
1362
|
// 4. A history floor (`historyFrom`) is applied in the query, so rows before it never
|
|
1296
1363
|
// leave the database, and again in the assembly, which then skips the summary.
|
|
1297
|
-
const rows = await ConversationEngine.LoadWindowRowsFresh(conversationId, contextUser, provider, historyFrom);
|
|
1364
|
+
const rows = await ConversationEngine.LoadWindowRowsFresh(conversationId, contextUser, provider, historyFrom, branchId);
|
|
1298
1365
|
const window = ConversationEngine.AssembleContextWindow(rows, {
|
|
1299
1366
|
maxTailMessages: maxMessages,
|
|
1300
1367
|
excludeDetailIds,
|
|
@@ -2,7 +2,7 @@ import { Resolver, Mutation, Arg, Ctx, ObjectType, Field } from 'type-graphql';
|
|
|
2
2
|
import { AppContext, UserPayload } from '../types.js';
|
|
3
3
|
import { LogError, LogStatus, Metadata, RunView } from '@memberjunction/core';
|
|
4
4
|
import { MJTemplateContentEntity, MJTemplateEntityExtended } from '@memberjunction/core-entities';
|
|
5
|
-
import { TemplateEngineServer } from '@memberjunction/templates';
|
|
5
|
+
import { GetTemplateRunRefusal, TemplateEngineServer } from '@memberjunction/templates';
|
|
6
6
|
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
7
7
|
import { GetReadWriteProvider } from '../util.js';
|
|
8
8
|
|
|
@@ -45,6 +45,18 @@ export class RunTemplateResolver extends ResolverBase {
|
|
|
45
45
|
try {
|
|
46
46
|
LogStatus(`=== RUNNING TEMPLATE FOR ID: ${templateId} ===`);
|
|
47
47
|
|
|
48
|
+
// Same rule as the Template.Run remote operation, checked before anything is loaded
|
|
49
|
+
const currentUser = this.GetUserFromPayload(userPayload);
|
|
50
|
+
const p = GetReadWriteProvider(providers);
|
|
51
|
+
const refusal = GetTemplateRunRefusal(currentUser, p);
|
|
52
|
+
if (refusal) {
|
|
53
|
+
return {
|
|
54
|
+
success: false,
|
|
55
|
+
error: refusal,
|
|
56
|
+
executionTimeMs: Date.now() - startTime
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
48
60
|
// Parse context data (JSON string)
|
|
49
61
|
let data = {};
|
|
50
62
|
if (contextData) {
|
|
@@ -59,17 +71,6 @@ export class RunTemplateResolver extends ResolverBase {
|
|
|
59
71
|
}
|
|
60
72
|
}
|
|
61
73
|
|
|
62
|
-
// Get current user from payload
|
|
63
|
-
const currentUser = this.GetUserFromPayload(userPayload);
|
|
64
|
-
if (!currentUser) {
|
|
65
|
-
return {
|
|
66
|
-
success: false,
|
|
67
|
-
error: 'Unable to determine current user',
|
|
68
|
-
executionTimeMs: Date.now() - startTime
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const p = GetReadWriteProvider(providers);
|
|
73
74
|
// Load the template entity
|
|
74
75
|
const templateEntity = await p.GetEntityObject<MJTemplateEntityExtended>('MJ: Templates', currentUser);
|
|
75
76
|
await templateEntity.Load(templateId);
|
|
@@ -3,6 +3,7 @@ import { AppContext, UserPayload } from '../types.js';
|
|
|
3
3
|
import { BaseEntity, CompositeKey, DatabaseProviderBase, EntityDeleteOptions, EntitySaveOptions, IMetadataProvider, LogError, Metadata, RunView, UserInfo } from '@memberjunction/core';
|
|
4
4
|
import { RequireSystemUser } from '../directives/RequireSystemUser.js';
|
|
5
5
|
import { GetReadWriteProvider } from '../util.js';
|
|
6
|
+
import { CreateIsolatedProvider } from '../isolatedProvider.js';
|
|
6
7
|
import { CompositeKeyInputType, CompositeKeyOutputType } from '../generic/KeyInputOutputTypes.js';
|
|
7
8
|
import { MJDatasetItemEntity } from '@memberjunction/core-entities';
|
|
8
9
|
|
|
@@ -216,7 +217,11 @@ export class SyncDataResolver {
|
|
|
216
217
|
try {
|
|
217
218
|
// Run the view to find matching records, then delete them all atomically —
|
|
218
219
|
// any single failure rolls back the entire batch so the dataset stays consistent.
|
|
219
|
-
|
|
220
|
+
// The rows are loaded FROM the transaction's provider so each Delete() runs inside it;
|
|
221
|
+
// with no request provider, an isolated one, never the shared Metadata.Provider, whose
|
|
222
|
+
// transaction would swallow every concurrent caller's writes (see CreateIsolatedProvider).
|
|
223
|
+
const provider = (providerOverride as unknown as DatabaseProviderBase) ?? await CreateIsolatedProvider();
|
|
224
|
+
const rv = RunView.FromMetadataProvider(provider);
|
|
220
225
|
const data = await rv.RunView<BaseEntity>({
|
|
221
226
|
EntityName: entityName,
|
|
222
227
|
ExtraFilter: filter,
|
|
@@ -234,7 +239,6 @@ export class SyncDataResolver {
|
|
|
234
239
|
return;
|
|
235
240
|
}
|
|
236
241
|
|
|
237
|
-
const provider = (providerOverride ?? Metadata.Provider) as unknown as DatabaseProviderBase;
|
|
238
242
|
await provider.BeginTransaction();
|
|
239
243
|
try {
|
|
240
244
|
for (const entityObject of data.Results) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Arg, Ctx, Field, InputType, Mutation, ObjectType, registerEnumType } from 'type-graphql';
|
|
2
2
|
import { AppContext, UserPayload } from '../types.js';
|
|
3
3
|
import { DatabaseProviderBase, EntityDeleteOptions, EntitySaveOptions, IMetadataProvider, LogError, Metadata, RunView, UserInfo } from '@memberjunction/core';
|
|
4
|
-
import { UUIDsEqual } from '@memberjunction/global';
|
|
4
|
+
import { GroupByUUID, IndexByUUID, UUIDsEqual } from '@memberjunction/global';
|
|
5
5
|
import { RequireSystemUser } from '../directives/RequireSystemUser.js';
|
|
6
6
|
import { MJRoleEntity, MJUserEntity, MJUserRoleEntity } from '@memberjunction/core-entities';
|
|
7
7
|
import { UserCache } from '@memberjunction/generic-database-provider';
|
|
@@ -430,14 +430,23 @@ export class SyncRolesAndUsersResolver {
|
|
|
430
430
|
const dbRoles = rResult.Results;
|
|
431
431
|
const dbUserRoles = urResult.Results;
|
|
432
432
|
|
|
433
|
+
// Index everything once: looking each user, role and assignment up by scanning the full lists
|
|
434
|
+
// made this O(users x user roles).
|
|
435
|
+
const dbUsersByEmail = this.indexByTrimmedLowerCase(dbUsers, u => u.Email);
|
|
436
|
+
const dbRolesByName = this.indexByTrimmedLowerCase(dbRoles, r => r.Name);
|
|
437
|
+
const dbRolesByID = IndexByUUID(dbRoles);
|
|
438
|
+
const dbUserRolesByUser = GroupByUUID(dbUserRoles, ur => ur.UserID);
|
|
439
|
+
|
|
433
440
|
// Saves/Deletes below run inside the outer SyncUsers/SyncRolesAndUsers transaction.
|
|
434
441
|
for (const user of users) {
|
|
435
|
-
const dbUser =
|
|
442
|
+
const dbUser = dbUsersByEmail.get(user.Email.trim().toLowerCase());
|
|
436
443
|
if (!dbUser) continue;
|
|
437
444
|
|
|
445
|
+
// Read before any save below; the saves never add to dbUserRoles, so this matches the full-list scans it replaces.
|
|
446
|
+
const thisUserDBRoles = dbUserRolesByUser.Get(dbUser.ID) ?? [];
|
|
438
447
|
for (const role of user.Roles) {
|
|
439
|
-
const dbRole =
|
|
440
|
-
if (dbRole && !
|
|
448
|
+
const dbRole = dbRolesByName.get(role.Name.trim().toLowerCase());
|
|
449
|
+
if (dbRole && !thisUserDBRoles.some(ur => UUIDsEqual(ur.RoleID, dbRole.ID))) {
|
|
441
450
|
const ur = await md.GetEntityObject<MJUserRoleEntity>("MJ: User Roles", u);
|
|
442
451
|
ur.UserID = dbUser.ID;
|
|
443
452
|
ur.RoleID = dbRole.ID;
|
|
@@ -448,9 +457,8 @@ export class SyncRolesAndUsersResolver {
|
|
|
448
457
|
}
|
|
449
458
|
|
|
450
459
|
// Check for DB user roles that are NOT in the user.Roles input — those need to be removed
|
|
451
|
-
const thisUserDBRoles = dbUserRoles.filter(ur => UUIDsEqual(ur.UserID, dbUser.ID));
|
|
452
460
|
for (const dbUserRole of thisUserDBRoles) {
|
|
453
|
-
const role = user.Roles.find(r => r.Name.trim().toLowerCase() ===
|
|
461
|
+
const role = user.Roles.find(r => r.Name.trim().toLowerCase() === dbRolesByID.Get(dbUserRole.RoleID)?.Name.trim().toLowerCase());
|
|
454
462
|
if (!role && !this.IsStandardRole(dbUserRole.Role)) {
|
|
455
463
|
if (!await dbUserRole.Delete()) {
|
|
456
464
|
throw new Error(`Failed to remove role from user '${dbUser.Email}': ${dbUserRole.LatestResult?.CompleteMessage ?? 'unknown error'}`);
|
|
@@ -459,5 +467,15 @@ export class SyncRolesAndUsersResolver {
|
|
|
459
467
|
}
|
|
460
468
|
}
|
|
461
469
|
}
|
|
470
|
+
|
|
471
|
+
/** Keys rows by a trimmed, lower-cased name; the first row wins, as `find` would return. */
|
|
472
|
+
private indexByTrimmedLowerCase<T>(rows: T[], keyOf: (row: T) => string | null | undefined): Map<string, T> {
|
|
473
|
+
const index = new Map<string, T>();
|
|
474
|
+
for (const row of rows) {
|
|
475
|
+
const key = keyOf(row)?.trim().toLowerCase();
|
|
476
|
+
if (key != null && !index.has(key)) index.set(key, row);
|
|
477
|
+
}
|
|
478
|
+
return index;
|
|
479
|
+
}
|
|
462
480
|
}
|
|
463
481
|
|