@memberjunction/server 6.2.0-edge.3 → 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/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 +2378 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +123 -0
- 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 +14 -11
- package/dist/context.js.map +1 -1
- package/dist/generated/graphql-schemas/__mj.d.ts +135 -0
- package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
- package/dist/generated/graphql-schemas/__mj.js +706 -0
- 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 +20 -4
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +39 -10
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +4 -0
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +120 -131
- 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/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 +14 -1
- package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.js +21 -5
- 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/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/HumanHandoffResolver.d.ts.map +1 -1
- package/dist/resolvers/HumanHandoffResolver.js +7 -1
- package/dist/resolvers/HumanHandoffResolver.js.map +1 -1
- 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/RealtimeBridgeResolver.d.ts +54 -2
- package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeBridgeResolver.js +155 -10
- package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +155 -25
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +394 -90
- 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/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/meetingMutations.d.ts +6 -0
- package/dist/resolvers/meetings/meetingMutations.d.ts.map +1 -1
- package/dist/resolvers/meetings/meetingMutations.js +21 -0
- package/dist/resolvers/meetings/meetingMutations.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/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 +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +97 -95
- package/src/__tests__/ActionResolver.authorization.test.ts +353 -0
- 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__/FetchEntityVectorsResolver.test.ts +285 -0
- package/src/__tests__/HostInstance.test.ts +84 -0
- package/src/__tests__/HumanHandoffResolver.test.ts +17 -0
- package/src/__tests__/MeetingResolver.test.ts +75 -1
- package/src/__tests__/RealtimeBridgeResolver.test.ts +415 -6
- 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.aggregateScreen.test.ts +174 -0
- package/src/__tests__/ResolverBase.clientOldValues.test.ts +6 -3
- 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__/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__/actionAuthorization.test.ts +130 -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__/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__/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 +125 -0
- package/src/context.ts +15 -10
- package/src/generated/graphql-schemas/__mj.ts +496 -1
- package/src/generic/OrphanedConversationDetailReconciler.ts +3 -3
- package/src/generic/ResolverBase.ts +39 -9
- package/src/generic/RunViewResolver.ts +4 -0
- package/src/index.ts +132 -143
- package/src/isolatedProvider.ts +22 -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 +24 -6
- package/src/resolvers/CacheStatsResolver.ts +89 -2
- package/src/resolvers/ClientToolRequestResolver.ts +25 -7
- package/src/resolvers/FetchEntityVectorsResolver.ts +97 -2
- package/src/resolvers/HumanHandoffResolver.ts +8 -0
- package/src/resolvers/MCPResolver.ts +8 -5
- package/src/resolvers/RealtimeBridgeResolver.ts +163 -11
- package/src/resolvers/RealtimeClientSessionResolver.ts +501 -96
- 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/agentVisionConsentAudit.ts +82 -0
- package/src/resolvers/avatarInputValidation.ts +123 -0
- package/src/resolvers/currentUserRoles.ts +32 -1
- package/src/resolvers/meetings/meetingMutations.ts +21 -0
- package/src/services/TaskGraphContinuationDeliverer.ts +13 -7
- package/src/sharedCache.ts +302 -0
- package/src/types.ts +7 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The PUBLIC verify-link routes of mid-session identity verification.
|
|
3
|
+
*
|
|
4
|
+
* Mounted BEFORE the unified auth middleware (the person opening the emailed link — often on another
|
|
5
|
+
* device — holds no MJ token; the single-use token IS the capability).
|
|
6
|
+
*
|
|
7
|
+
* | Route | Effect |
|
|
8
|
+
* |---|---|
|
|
9
|
+
* | `GET /realtime/verify/:token` | Renders a one-button confirmation page. **No side effects.** |
|
|
10
|
+
* | `POST /realtime/verify` (form field `token`) | Redeems the token; renders "You're verified" or the uniform failure page. |
|
|
11
|
+
*
|
|
12
|
+
* ## Why the redemption is a POST
|
|
13
|
+
*
|
|
14
|
+
* Mail scanners and link previewers GET every URL in a message. A GET that consumed the single-use token
|
|
15
|
+
* would let a scanner burn it before the human clicks. The magic-link redeem endpoint made the same
|
|
16
|
+
* choice. The human's click submits the form.
|
|
17
|
+
*
|
|
18
|
+
* ## What the responses reveal
|
|
19
|
+
*
|
|
20
|
+
* Nothing about any session. A malformed token, an unknown session, an expired link, a used link and a
|
|
21
|
+
* voided verification all render the SAME page with the SAME status; success is a bare "You're verified".
|
|
22
|
+
* Responses are `no-store`, never framed, sent with `Referrer-Policy: no-referrer` (the token is in the
|
|
23
|
+
* URL of the GET), and carry a CSP that forbids everything but the page's own inline style.
|
|
24
|
+
*
|
|
25
|
+
* @module @memberjunction/server/realtimeSessions
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { Router, urlencoded, type Request, type Response } from 'express';
|
|
29
|
+
import { rateLimit } from 'express-rate-limit';
|
|
30
|
+
import { LogError } from '@memberjunction/core';
|
|
31
|
+
import { configInfo } from '../config.js';
|
|
32
|
+
import { RealtimeSessionVerificationService } from './RealtimeSessionVerificationService.js';
|
|
33
|
+
import { BuildVerifyConfirmHtml, BuildVerifyErrorHtml, BuildVerifyFailureHtml, BuildVerifySuccessHtml } from './verificationPages.js';
|
|
34
|
+
|
|
35
|
+
/** Mount path of the public verify routes. Under `/realtime`, which is already reserved against server extensions. */
|
|
36
|
+
export const REALTIME_VERIFY_MOUNT_PATH = '/realtime/verify';
|
|
37
|
+
|
|
38
|
+
/** Headers every verify response carries. */
|
|
39
|
+
function setSecurityHeaders(res: Response): void {
|
|
40
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
41
|
+
res.setHeader('Referrer-Policy', 'no-referrer');
|
|
42
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
43
|
+
res.setHeader('X-Frame-Options', 'DENY');
|
|
44
|
+
res.setHeader(
|
|
45
|
+
'Content-Security-Policy',
|
|
46
|
+
"default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'",
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Sends an HTML page with the verify security headers. */
|
|
51
|
+
function sendPage(res: Response, status: number, html: string): void {
|
|
52
|
+
setSecurityHeaders(res);
|
|
53
|
+
res.status(status).type('html').send(html);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Builds the public verify router. Mount at {@link REALTIME_VERIFY_MOUNT_PATH} BEFORE the auth
|
|
58
|
+
* middleware.
|
|
59
|
+
*
|
|
60
|
+
* @param service - the verification service (injectable for tests; defaults to the process singleton)
|
|
61
|
+
*/
|
|
62
|
+
export function CreateRealtimeVerifyRouter(
|
|
63
|
+
service: Pick<RealtimeSessionVerificationService, 'RedeemLink'> = RealtimeSessionVerificationService.Instance,
|
|
64
|
+
): Router {
|
|
65
|
+
const limits = configInfo.realtime?.identityVerification?.rateLimits;
|
|
66
|
+
// The route is public and a redemption does a database read + write, so it is throttled per IP — the
|
|
67
|
+
// same posture as the magic-link redeem endpoint. The message is the failure page, not JSON: this
|
|
68
|
+
// route talks to a browser.
|
|
69
|
+
const limiter = rateLimit({
|
|
70
|
+
windowMs: limits?.redeemWindowMs ?? 60_000,
|
|
71
|
+
limit: limits?.redeemPerIp ?? 30,
|
|
72
|
+
standardHeaders: 'draft-7',
|
|
73
|
+
legacyHeaders: false,
|
|
74
|
+
handler: (_req: Request, res: Response) => sendPage(res, 429, BuildVerifyFailureHtml()),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const router = Router();
|
|
78
|
+
|
|
79
|
+
// SAFE: renders the confirmation button. Never consumes the token (see the module header).
|
|
80
|
+
router.get('/:token', limiter, (req: Request, res: Response) => {
|
|
81
|
+
const token = typeof req.params.token === 'string' ? req.params.token : '';
|
|
82
|
+
sendPage(res, 200, BuildVerifyConfirmHtml(token, REALTIME_VERIFY_MOUNT_PATH));
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// The side-effecting redemption, gated by a human gesture.
|
|
86
|
+
router.post('/', limiter, urlencoded({ extended: false, limit: '4kb' }), async (req: Request, res: Response) => {
|
|
87
|
+
const body = (req.body ?? {}) as { token?: unknown };
|
|
88
|
+
const token = typeof body.token === 'string' ? body.token : '';
|
|
89
|
+
try {
|
|
90
|
+
const result = await service.RedeemLink(token, req.ip);
|
|
91
|
+
if (result.Success) {
|
|
92
|
+
sendPage(res, 200, BuildVerifySuccessHtml());
|
|
93
|
+
} else if (result.ErrorCode === 'persist_failed') {
|
|
94
|
+
// A server fault after a VALID token: the link is still redeemable, so say "try again".
|
|
95
|
+
sendPage(res, 500, BuildVerifyErrorHtml());
|
|
96
|
+
} else {
|
|
97
|
+
// 410 for every other refusal: the page and status are identical whether the token was unknown,
|
|
98
|
+
// malformed, expired or used, so the route cannot be used to probe sessions.
|
|
99
|
+
sendPage(res, 410, BuildVerifyFailureHtml());
|
|
100
|
+
}
|
|
101
|
+
} catch (error) {
|
|
102
|
+
LogError(`RealtimeVerifyRouter: redemption threw: ${error instanceof Error ? error.message : String(error)}`);
|
|
103
|
+
sendPage(res, 500, BuildVerifyErrorHtml());
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return router;
|
|
108
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview {@link SessionVerificationPortsBase} — the config-free half of the production
|
|
3
|
+
* {@link VerificationPorts}: reading and writing a session, publishing `identity.verified`, and the
|
|
4
|
+
* audit entry. Everything that depends on server configuration (the settings, the service principal,
|
|
5
|
+
* the mail provider) is left abstract.
|
|
6
|
+
*
|
|
7
|
+
* The split exists so the DB-facing behaviour can be exercised with real sessions by anything that
|
|
8
|
+
* cannot — or must not — load the server's configuration at import time (the integration-test bundle
|
|
9
|
+
* subclasses it with captured email and a fixed principal). `RuntimeVerificationPorts`, in
|
|
10
|
+
* `RealtimeSessionVerificationService.ts`, is the production subclass that supplies the configuration.
|
|
11
|
+
*
|
|
12
|
+
* ## Which identity performs the writes, and why
|
|
13
|
+
*
|
|
14
|
+
* Reads of the session for **authorization** run as the CALLER, so row-level security is the first gate
|
|
15
|
+
* (a widget guest cannot even load another guest's session). Every **write** of server-decided state
|
|
16
|
+
* runs as a service principal ({@link SessionVerificationPortsBase.ResolveServiceUser}), inside
|
|
17
|
+
* `RunWithTrustedSessionConfigWrites`. That is not a convenience: the session owner can already `Update`
|
|
18
|
+
* their own session row, which is exactly why `MJAIAgentSessionEntityServer` refuses owner writes to
|
|
19
|
+
* `identityVerification` and `maxSessionDeadlineIso`. Server-decided state must be written by the server,
|
|
20
|
+
* after the caller was authorized, in a scope no remote caller can enter. The verify link has no caller
|
|
21
|
+
* identity at all (the token is the capability), so it needs the service principal for its reads too.
|
|
22
|
+
*
|
|
23
|
+
* @module @memberjunction/server/realtimeSessions
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { LogError, Metadata, type IMetadataProvider, type UserInfo } from '@memberjunction/core';
|
|
27
|
+
import { RunWithTrustedSessionConfigWrites } from '@memberjunction/core-entities-server';
|
|
28
|
+
import type { MJAuditLogEntity } from '@memberjunction/core-entities';
|
|
29
|
+
import type { IdentityVerifiedEventPayload } from '@memberjunction/ai-core-plus';
|
|
30
|
+
import { LoadSessionForAccess, type LoadedSessionForAccess } from './sessionAccess.js';
|
|
31
|
+
import { RealtimeSessionEventService } from './RealtimeSessionEventService.js';
|
|
32
|
+
import {
|
|
33
|
+
type ConfigMutation,
|
|
34
|
+
type ConfigMutationResult,
|
|
35
|
+
type OutboundEmail,
|
|
36
|
+
type SessionSnapshot,
|
|
37
|
+
type VerificationAuditEntry,
|
|
38
|
+
type VerificationCaller,
|
|
39
|
+
type VerificationPorts,
|
|
40
|
+
type VerificationSettings,
|
|
41
|
+
} from './verificationWorkflow.js';
|
|
42
|
+
|
|
43
|
+
/** The seeded audit-log type (see `metadata/audit-log-types/.realtime-session-audit-types.json`). */
|
|
44
|
+
export const REALTIME_IDENTITY_VERIFIED_AUDIT_TYPE = 'Realtime Identity Verified';
|
|
45
|
+
|
|
46
|
+
const AUDIT_LOG_ENTITY = 'MJ: Audit Logs';
|
|
47
|
+
const SESSION_ENTITY_NAME = 'MJ: AI Agent Sessions';
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
/** Reduces a loaded session to the snapshot the workflow reasons about. */
|
|
51
|
+
function toSnapshot(loaded: LoadedSessionForAccess): SessionSnapshot {
|
|
52
|
+
const { Session } = loaded;
|
|
53
|
+
return {
|
|
54
|
+
AgentSessionID: Session.ID,
|
|
55
|
+
OwnerUserID: Session.UserID,
|
|
56
|
+
ConversationExternalID: loaded.ConversationExternalID,
|
|
57
|
+
Status: Session.Status,
|
|
58
|
+
ConfigRaw: Session.Config_,
|
|
59
|
+
// `__mj_CreatedAt` is the row's creation instant — i.e. the session start. It is read-only and
|
|
60
|
+
// set by the database, so a client cannot move it.
|
|
61
|
+
StartedAtMs: Session.__mj_CreatedAt instanceof Date ? Session.__mj_CreatedAt.getTime() : Date.now(),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The session-facing {@link VerificationPorts}, minus configuration. Subclasses supply the settings, the
|
|
68
|
+
* service principal and the mail delivery.
|
|
69
|
+
*/
|
|
70
|
+
export abstract class SessionVerificationPortsBase implements VerificationPorts {
|
|
71
|
+
/** The live settings. */
|
|
72
|
+
public abstract GetSettings(): VerificationSettings;
|
|
73
|
+
|
|
74
|
+
/** Delivers an email. Must not throw; reports failure in the result. */
|
|
75
|
+
public abstract SendEmail(email: OutboundEmail): Promise<{ Success: boolean; ErrorMessage?: string }>;
|
|
76
|
+
|
|
77
|
+
/** The principal state writes (and the link caller's reads) run as; null when none is available. */
|
|
78
|
+
protected abstract ResolveServiceUser(): UserInfo | null;
|
|
79
|
+
|
|
80
|
+
public Now(): number {
|
|
81
|
+
return Date.now();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
public async LoadSession(agentSessionId: string, caller: VerificationCaller): Promise<SessionSnapshot | null> {
|
|
85
|
+
const reader = this.readerFor(caller);
|
|
86
|
+
if (!reader) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const loaded = await LoadSessionForAccess(agentSessionId, reader.User, reader.Provider);
|
|
90
|
+
return loaded ? toSnapshot(loaded) : null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public async MutateSessionConfig<T>(
|
|
94
|
+
agentSessionId: string,
|
|
95
|
+
caller: VerificationCaller,
|
|
96
|
+
mutate: ConfigMutation<T>,
|
|
97
|
+
): Promise<ConfigMutationResult<T>> {
|
|
98
|
+
const writer = this.writerFor(caller);
|
|
99
|
+
if (!writer) {
|
|
100
|
+
return { Persisted: false, ErrorMessage: 'No service principal is available to record verification state.' };
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const loaded = await LoadSessionForAccess(agentSessionId, writer.User, writer.Provider);
|
|
104
|
+
if (!loaded) {
|
|
105
|
+
return { Persisted: false, ErrorMessage: `Session ${agentSessionId} could not be read.` };
|
|
106
|
+
}
|
|
107
|
+
const session = toSnapshot(loaded);
|
|
108
|
+
const { NextConfigRaw, Outcome } = mutate(session);
|
|
109
|
+
if (NextConfigRaw === null) {
|
|
110
|
+
return { Session: session, Outcome, Persisted: false };
|
|
111
|
+
}
|
|
112
|
+
loaded.Session.Config_ = NextConfigRaw;
|
|
113
|
+
const saved = await RunWithTrustedSessionConfigWrites(() => loaded.Session.Save());
|
|
114
|
+
if (!saved) {
|
|
115
|
+
const detail = loaded.Session.LatestResult?.CompleteMessage ?? 'unknown error';
|
|
116
|
+
LogError(`RealtimeSessionVerification: saving session ${agentSessionId} failed: ${detail}`);
|
|
117
|
+
return { Session: session, Outcome, Persisted: false, ErrorMessage: detail };
|
|
118
|
+
}
|
|
119
|
+
return { Session: session, Outcome, Persisted: true };
|
|
120
|
+
} catch (error) {
|
|
121
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
122
|
+
LogError(`RealtimeSessionVerification: updating session ${agentSessionId} threw: ${message}`);
|
|
123
|
+
return { Persisted: false, ErrorMessage: message };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
public PublishVerified(
|
|
128
|
+
routing: { AgentSessionID: string; OwnerUserID: string; ScopeKey: string | null },
|
|
129
|
+
payload: IdentityVerifiedEventPayload,
|
|
130
|
+
): { Success: boolean; ErrorMessage?: string } {
|
|
131
|
+
return RealtimeSessionEventService.Instance.PublishRouted(routing, 'identity.verified', payload);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
public async WriteAudit(entry: VerificationAuditEntry): Promise<void> {
|
|
135
|
+
const writer = this.ResolveServiceUser();
|
|
136
|
+
const provider = Metadata.Provider; // global-provider-ok: server-side audit write under the server's single default provider
|
|
137
|
+
const type = provider?.AuditLogTypes?.find((t) => t.Name?.trim().toLowerCase() === REALTIME_IDENTITY_VERIFIED_AUDIT_TYPE.toLowerCase());
|
|
138
|
+
const sessionEntityId = provider?.EntityByName(SESSION_ENTITY_NAME)?.ID;
|
|
139
|
+
if (!writer || !type) {
|
|
140
|
+
// The audit type ships as metadata (a release seed); without it, fall back to the server log so
|
|
141
|
+
// the verification is still recorded somewhere durable-ish rather than silently unaudited.
|
|
142
|
+
LogError(
|
|
143
|
+
`RealtimeSessionVerification: audit entry not written for session ${entry.AgentSessionID} ` +
|
|
144
|
+
`(${writer ? `audit log type '${REALTIME_IDENTITY_VERIFIED_AUDIT_TYPE}' is not seeded` : 'no service principal'}): ` +
|
|
145
|
+
`${entry.Email} via ${entry.Method}.`,
|
|
146
|
+
);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
const row = await provider.GetEntityObject<MJAuditLogEntity>(AUDIT_LOG_ENTITY, writer);
|
|
151
|
+
row.NewRecord();
|
|
152
|
+
row.UserID = entry.OwnerUserID;
|
|
153
|
+
row.AuditLogTypeID = type.ID;
|
|
154
|
+
row.Status = 'Success';
|
|
155
|
+
row.EntityID = sessionEntityId ?? null;
|
|
156
|
+
row.RecordID = entry.AgentSessionID;
|
|
157
|
+
row.Description = `Realtime session verified ${entry.Email} by ${entry.Method === 'link' ? 'emailed link' : 'typed code'}.`;
|
|
158
|
+
row.Details = JSON.stringify({
|
|
159
|
+
email: entry.Email,
|
|
160
|
+
method: entry.Method,
|
|
161
|
+
ipAddress: entry.ClientIp ?? null,
|
|
162
|
+
maxSessionDeadlineIso: entry.MaxSessionDeadlineIso ?? null,
|
|
163
|
+
});
|
|
164
|
+
if (!(await row.Save())) {
|
|
165
|
+
LogError(`RealtimeSessionVerification: audit row save failed for session ${entry.AgentSessionID}: ${row.LatestResult?.CompleteMessage ?? 'unknown error'}`);
|
|
166
|
+
}
|
|
167
|
+
} catch (error) {
|
|
168
|
+
LogError(`RealtimeSessionVerification: audit write threw for session ${entry.AgentSessionID}: ${error instanceof Error ? error.message : String(error)}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** The identity + provider an authorization read runs under. */
|
|
173
|
+
private readerFor(caller: VerificationCaller): { User: UserInfo; Provider: IMetadataProvider } | null {
|
|
174
|
+
if (caller.Kind === 'principal') {
|
|
175
|
+
return { User: caller.ContextUser, Provider: caller.Provider };
|
|
176
|
+
}
|
|
177
|
+
const user = this.ResolveServiceUser();
|
|
178
|
+
return user ? { User: user, Provider: Metadata.Provider } : null; // global-provider-ok: public verify route has no request-scoped provider
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** The identity + provider a state write runs under: always the service principal. */
|
|
182
|
+
private writerFor(caller: VerificationCaller): { User: UserInfo; Provider: IMetadataProvider } | null {
|
|
183
|
+
const user = this.ResolveServiceUser();
|
|
184
|
+
if (!user) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
return { User: user, Provider: caller.Kind === 'principal' ? caller.Provider : Metadata.Provider }; // global-provider-ok: public verify route has no request-scoped provider
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview A small in-memory sliding-window rate limiter keyed by string, with an injectable
|
|
3
|
+
* clock so it is deterministic under test.
|
|
4
|
+
*
|
|
5
|
+
* Used for the limits that the HTTP-layer `express-rate-limit` cannot express because the request is a
|
|
6
|
+
* GraphQL mutation (per IP / per email domain / per recipient on verification sends, and per session on
|
|
7
|
+
* code attempts). The HTTP verify route uses `express-rate-limit` directly, the same way the widget
|
|
8
|
+
* and magic-link routers do.
|
|
9
|
+
*
|
|
10
|
+
* **Per-instance, best-effort.** State is in process memory, so with several server replicas each
|
|
11
|
+
* enforces its own budget. That is the same trade the widget mint limiter makes. The limits that must
|
|
12
|
+
* hold across replicas — sends per session and code attempts per verification — are NOT here: they are
|
|
13
|
+
* persisted on the session (`identityVerification`) and enforced from the database.
|
|
14
|
+
*
|
|
15
|
+
* @module @memberjunction/server/realtimeSessions
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** Outcome of {@link SlidingWindowRateLimiter.TryConsume}. */
|
|
19
|
+
export interface RateLimitDecision {
|
|
20
|
+
/** True when the event fits the budget (and was counted). */
|
|
21
|
+
Allowed: boolean;
|
|
22
|
+
/** When refused, ms until the oldest counted event leaves the window. 0 when allowed. */
|
|
23
|
+
RetryAfterMs: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Construction options. */
|
|
27
|
+
export interface SlidingWindowRateLimiterOptions {
|
|
28
|
+
/** Events allowed per key per window. A limit of 0 or less disables the limiter (everything allowed). */
|
|
29
|
+
Limit: number;
|
|
30
|
+
/** Window length in ms. */
|
|
31
|
+
WindowMs: number;
|
|
32
|
+
/** Upper bound on distinct keys held in memory; the oldest-seen keys are evicted past it. Default 10 000. */
|
|
33
|
+
MaxKeys?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Default bound on distinct keys held. */
|
|
37
|
+
const DEFAULT_MAX_KEYS = 10_000;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Counts events per key over a sliding window.
|
|
41
|
+
*
|
|
42
|
+
* Memory is bounded two ways: a key's timestamps older than the window are dropped whenever the key is
|
|
43
|
+
* touched, and when the number of keys exceeds `MaxKeys` the least-recently-inserted keys are evicted
|
|
44
|
+
* (an evicted key simply starts a fresh window — the limiter fails open by forgetting, never closed).
|
|
45
|
+
*/
|
|
46
|
+
export class SlidingWindowRateLimiter {
|
|
47
|
+
private readonly events = new Map<string, number[]>();
|
|
48
|
+
private readonly maxKeys: number;
|
|
49
|
+
|
|
50
|
+
constructor(private readonly options: SlidingWindowRateLimiterOptions) {
|
|
51
|
+
this.maxKeys = options.MaxKeys ?? DEFAULT_MAX_KEYS;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Counts one event for `key` if the budget allows.
|
|
56
|
+
*
|
|
57
|
+
* @param key - what is being limited (an IP, a domain, a session id…); callers normalise it
|
|
58
|
+
* @param nowMs - the current time, epoch ms
|
|
59
|
+
*/
|
|
60
|
+
public TryConsume(key: string, nowMs: number): RateLimitDecision {
|
|
61
|
+
if (this.options.Limit <= 0) {
|
|
62
|
+
return { Allowed: true, RetryAfterMs: 0 };
|
|
63
|
+
}
|
|
64
|
+
const windowStart = nowMs - this.options.WindowMs;
|
|
65
|
+
const live = (this.events.get(key) ?? []).filter((t) => t > windowStart);
|
|
66
|
+
if (live.length >= this.options.Limit) {
|
|
67
|
+
this.store(key, live);
|
|
68
|
+
return { Allowed: false, RetryAfterMs: Math.max(0, live[0] + this.options.WindowMs - nowMs) };
|
|
69
|
+
}
|
|
70
|
+
live.push(nowMs);
|
|
71
|
+
this.store(key, live);
|
|
72
|
+
return { Allowed: true, RetryAfterMs: 0 };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Number of keys currently tracked (exposed for tests and diagnostics). */
|
|
76
|
+
public get TrackedKeyCount(): number {
|
|
77
|
+
return this.events.size;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Re-inserts `key` (moving it to the newest position) and evicts the oldest keys past the bound. */
|
|
81
|
+
private store(key: string, timestamps: number[]): void {
|
|
82
|
+
this.events.delete(key);
|
|
83
|
+
this.events.set(key, timestamps);
|
|
84
|
+
while (this.events.size > this.maxKeys) {
|
|
85
|
+
const oldest = this.events.keys().next();
|
|
86
|
+
if (oldest.done) {
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
this.events.delete(oldest.value);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Realtime session events + mid-session identity verification — module barrel.
|
|
3
|
+
* @module @memberjunction/server/realtimeSessions
|
|
4
|
+
*/
|
|
5
|
+
export * from './sessionAccess.js';
|
|
6
|
+
export * from './subscriptionAuthorization.js';
|
|
7
|
+
export * from './RealtimeSessionEventService.js';
|
|
8
|
+
export * from './verificationPolicy.js';
|
|
9
|
+
export * from './verificationCore.js';
|
|
10
|
+
export * from './SlidingWindowRateLimiter.js';
|
|
11
|
+
export * from './verificationPages.js';
|
|
12
|
+
export * from './verificationWorkflow.js';
|
|
13
|
+
export * from './sessionConfigStamps.js';
|
|
14
|
+
export * from './SessionVerificationPortsBase.js';
|
|
15
|
+
export * from './RealtimeSessionVerificationService.js';
|
|
16
|
+
export * from './RealtimeVerifyRouter.js';
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Who may see and act on ONE realtime session.
|
|
3
|
+
*
|
|
4
|
+
* One pure decision ({@link AuthorizeSessionAccess}) shared by everything that addresses a live
|
|
5
|
+
* session on behalf of a caller: the `RealtimeSessionEvents` subscription (at subscribe time AND for
|
|
6
|
+
* every delivered event), the verification mutations, and the status query. Keeping it in one place
|
|
7
|
+
* is what stops "who owns this session?" from being re-derived — slightly differently — in each.
|
|
8
|
+
*
|
|
9
|
+
* ## The two ownership rules
|
|
10
|
+
*
|
|
11
|
+
* 1. **Owner.** The session's `UserID` must be the caller's user.
|
|
12
|
+
* 2. **Scope.** A *named* user is fully identified by (1). An **anonymous widget guest** is not:
|
|
13
|
+
* every guest authenticates as the one shared Anonymous user, so (1) alone would let any guest
|
|
14
|
+
* address any other guest's session. What separates guests is the signed per-session scope in
|
|
15
|
+
* their token (`MagicLinkScope.ResourceID`), which `SessionManager` stamps onto the session's
|
|
16
|
+
* Conversation as `ExternalID`. So a principal that carries a scope must match it, and an
|
|
17
|
+
* anonymous principal that carries NO scope is refused outright — it cannot be told apart from
|
|
18
|
+
* every other anonymous principal.
|
|
19
|
+
*
|
|
20
|
+
* The decision is deliberately pure (no I/O) so it can be applied identically to a freshly loaded
|
|
21
|
+
* row at subscribe time and to the owner/scope snapshot carried inside each published event.
|
|
22
|
+
*
|
|
23
|
+
* @module @memberjunction/server/realtimeSessions
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { IMetadataProvider, LogError, UserInfo } from '@memberjunction/core';
|
|
27
|
+
import { IsValidUUID, UUIDsEqual } from '@memberjunction/global';
|
|
28
|
+
import type { MJAIAgentSessionEntity, MJConversationEntity } from '@memberjunction/core-entities';
|
|
29
|
+
|
|
30
|
+
/** Entity names, centralised so the `MJ: ` prefix convention lives in one place. */
|
|
31
|
+
const SESSION_ENTITY = 'MJ: AI Agent Sessions';
|
|
32
|
+
const CONVERSATION_ENTITY = 'MJ: Conversations';
|
|
33
|
+
|
|
34
|
+
/** What the decision needs to know about the session. */
|
|
35
|
+
export interface SessionAccessSubject {
|
|
36
|
+
/** `AIAgentSession.UserID`. */
|
|
37
|
+
SessionUserID: string;
|
|
38
|
+
/**
|
|
39
|
+
* `Conversation.ExternalID` of the session's conversation — the signed per-session scope id for an
|
|
40
|
+
* anonymous guest. `null`/`undefined` when the conversation has none (a named user's session) or
|
|
41
|
+
* could not be loaded.
|
|
42
|
+
*/
|
|
43
|
+
ConversationExternalID: string | null | undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** What the decision needs to know about the caller (a subset of `UserInfo`). */
|
|
47
|
+
export interface SessionAccessPrincipal {
|
|
48
|
+
/** The caller's user id. */
|
|
49
|
+
ID: string;
|
|
50
|
+
/** True for the shared-Anonymous-user principal of a public-widget guest. */
|
|
51
|
+
IsMagicLinkAnonymous?: boolean;
|
|
52
|
+
/** The signed per-session scope carried by the caller's token, when any. */
|
|
53
|
+
MagicLinkScope?: { ResourceID?: string } | undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Why access was refused. Callers log the reason; clients get a uniform refusal. */
|
|
57
|
+
export type SessionAccessRefusal = 'not_owner' | 'scope_missing' | 'scope_mismatch';
|
|
58
|
+
|
|
59
|
+
/** Outcome of {@link AuthorizeSessionAccess}. */
|
|
60
|
+
export type SessionAccessDecision = { Allowed: true } | { Allowed: false; Reason: SessionAccessRefusal };
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Decides whether `principal` may address the session described by `subject`.
|
|
64
|
+
*
|
|
65
|
+
* Fails closed: anything that cannot be positively matched is refused.
|
|
66
|
+
*
|
|
67
|
+
* @param subject - owner + scope of the session
|
|
68
|
+
* @param principal - the authenticated caller (from the server-built `UserInfo`, never from client args)
|
|
69
|
+
*/
|
|
70
|
+
export function AuthorizeSessionAccess(
|
|
71
|
+
subject: SessionAccessSubject,
|
|
72
|
+
principal: SessionAccessPrincipal | null | undefined,
|
|
73
|
+
): SessionAccessDecision {
|
|
74
|
+
if (!principal?.ID || !subject.SessionUserID || !UUIDsEqual(subject.SessionUserID, principal.ID)) {
|
|
75
|
+
return { Allowed: false, Reason: 'not_owner' };
|
|
76
|
+
}
|
|
77
|
+
const scopeId = principal.MagicLinkScope?.ResourceID;
|
|
78
|
+
if (!scopeId) {
|
|
79
|
+
// An anonymous principal without a per-session scope is indistinguishable from every other
|
|
80
|
+
// anonymous principal: owning "the Anonymous user's" session proves nothing.
|
|
81
|
+
return principal.IsMagicLinkAnonymous ? { Allowed: false, Reason: 'scope_missing' } : { Allowed: true };
|
|
82
|
+
}
|
|
83
|
+
// Scope ids are opaque base64url strings (case-significant), so compare exactly — not UUIDsEqual.
|
|
84
|
+
return subject.ConversationExternalID === scopeId ? { Allowed: true } : { Allowed: false, Reason: 'scope_mismatch' };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** A session row together with the one fact about its conversation the decision needs. */
|
|
88
|
+
export interface LoadedSessionForAccess {
|
|
89
|
+
/** The session entity, loaded under the caller's own identity (so RLS applied to the read). */
|
|
90
|
+
Session: MJAIAgentSessionEntity;
|
|
91
|
+
/** The session's conversation `ExternalID` (anonymous scope id), or null. */
|
|
92
|
+
ConversationExternalID: string | null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Loads a session (and its conversation's scope id) under `contextUser`'s identity.
|
|
97
|
+
*
|
|
98
|
+
* Loading as the caller — rather than as a privileged user — means the entity layer's row-level
|
|
99
|
+
* security (`Widget Guest: Own Agent Sessions`, `UI: Own Agent Sessions`) is the FIRST gate; the
|
|
100
|
+
* explicit {@link AuthorizeSessionAccess} check that follows is defence in depth.
|
|
101
|
+
*
|
|
102
|
+
* @returns the loaded session, or `null` when the id is malformed, the row does not exist, or the
|
|
103
|
+
* caller cannot read it. Failure to read the conversation yields `ConversationExternalID: null`,
|
|
104
|
+
* which {@link AuthorizeSessionAccess} then treats as "no scope to match" (refused for a scoped caller).
|
|
105
|
+
*/
|
|
106
|
+
export async function LoadSessionForAccess(
|
|
107
|
+
agentSessionId: string,
|
|
108
|
+
contextUser: UserInfo,
|
|
109
|
+
provider: IMetadataProvider,
|
|
110
|
+
): Promise<LoadedSessionForAccess | null> {
|
|
111
|
+
if (!IsValidUUID(agentSessionId)) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
const session = await provider.GetEntityObject<MJAIAgentSessionEntity>(SESSION_ENTITY, contextUser);
|
|
115
|
+
if (!(await session.Load(agentSessionId))) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
return { Session: session, ConversationExternalID: await loadConversationExternalID(session, contextUser, provider) };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Reads the session conversation's `ExternalID`; null when there is no conversation or it cannot be read. */
|
|
122
|
+
async function loadConversationExternalID(
|
|
123
|
+
session: MJAIAgentSessionEntity,
|
|
124
|
+
contextUser: UserInfo,
|
|
125
|
+
provider: IMetadataProvider,
|
|
126
|
+
): Promise<string | null> {
|
|
127
|
+
if (!session.ConversationID) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
const conversation = await provider.GetEntityObject<MJConversationEntity>(CONVERSATION_ENTITY, contextUser);
|
|
132
|
+
if (!(await conversation.Load(session.ConversationID))) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
return conversation.ExternalID ?? null;
|
|
136
|
+
} catch (error) {
|
|
137
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
138
|
+
LogError(`LoadSessionForAccess: could not read conversation ${session.ConversationID} of session ${session.ID}: ${message}`);
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview What the session-start path stamps into a new realtime session's `Config` for identity
|
|
3
|
+
* verification: the policy snapshot, the unverified duration cap, and any verified identity carried
|
|
4
|
+
* forward from the session being resumed.
|
|
5
|
+
*
|
|
6
|
+
* Split in two so the decision is testable without a database:
|
|
7
|
+
*
|
|
8
|
+
* - {@link BuildMintConfigStamps} — **pure**. Given the resolved realtime config, the existing `Config`
|
|
9
|
+
* and an optional inherited identity, returns the `Config` to write (or `null` when there is nothing
|
|
10
|
+
* to stamp).
|
|
11
|
+
* - {@link LoadInheritedVerifiedIdentity} — the one read: the prior session's verified identity, only
|
|
12
|
+
* when the new caller is authorized on that prior session.
|
|
13
|
+
*
|
|
14
|
+
* Everything stamped here lives under server-authoritative keys (`identityVerification`,
|
|
15
|
+
* `maxSessionDeadlineIso`); the caller writes it inside `RunWithTrustedSessionConfigWrites`.
|
|
16
|
+
*
|
|
17
|
+
* ## Resume after the cap
|
|
18
|
+
*
|
|
19
|
+
* If an unverified session hits its cap before the person verifies, the janitor closes it. A verify link
|
|
20
|
+
* redeemed afterwards still records the verification on that (closed) session, and a **new** session
|
|
21
|
+
* started with `lastSessionId` pointing at it inherits the verified identity (and, with it, the
|
|
22
|
+
* `verifiedMaxSeconds` cap instead of the unverified one). Inheritance requires the same ownership/scope
|
|
23
|
+
* authorization as any other access to the prior session, so a stranger cannot adopt someone else's
|
|
24
|
+
* verification by naming their session id.
|
|
25
|
+
*
|
|
26
|
+
* @module @memberjunction/server/realtimeSessions
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import type { IMetadataProvider, UserInfo } from '@memberjunction/core';
|
|
30
|
+
import { AuthorizeSessionAccess, LoadSessionForAccess } from './sessionAccess.js';
|
|
31
|
+
import {
|
|
32
|
+
ExtractIdentityVerificationPolicyFromRealtimeConfig,
|
|
33
|
+
ReadIdentityVerificationPolicyLayer,
|
|
34
|
+
TightenSessionDeadline,
|
|
35
|
+
} from './verificationPolicy.js';
|
|
36
|
+
import {
|
|
37
|
+
ParseConfigObject,
|
|
38
|
+
ReadIdentityVerificationState,
|
|
39
|
+
WriteIdentityVerificationState,
|
|
40
|
+
type IdentityVerificationState,
|
|
41
|
+
type VerifiedIdentity,
|
|
42
|
+
} from './verificationCore.js';
|
|
43
|
+
|
|
44
|
+
/** The `Config` to persist at session start. */
|
|
45
|
+
export interface MintConfigStampPlan {
|
|
46
|
+
NextConfigRaw: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Decides what to stamp into a freshly created session's `Config`.
|
|
51
|
+
*
|
|
52
|
+
* - **Policy snapshot.** The cascade's verification layer (`channels.config.IdentityVerification` plus the
|
|
53
|
+
* duration caps) is copied onto the session, so the rules a person is held to are fixed at mint,
|
|
54
|
+
* server-side, and cannot be chosen by the caller later.
|
|
55
|
+
* - **Duration cap.** When the cascade configures `unverifiedMaxSeconds` (or, for a session that inherited
|
|
56
|
+
* a verified identity, `verifiedMaxSeconds`), the session's absolute deadline is set to now + cap —
|
|
57
|
+
* applied to ANY principal, not only widget guests — and never loosens a tighter deadline already
|
|
58
|
+
* stamped (the widget voice cap).
|
|
59
|
+
* - **Inherited identity.** A verified identity carried forward from the resumed session.
|
|
60
|
+
*
|
|
61
|
+
* @param args.EffectiveRealtime - `EffectiveConfig.realtime` of the resolved config (any shape; read defensively)
|
|
62
|
+
* @param args.ExistingConfigRaw - the session's current `Config`
|
|
63
|
+
* @param args.InheritedVerified - verified identity from the session being resumed, if any
|
|
64
|
+
* @param args.NowMs - the current time, epoch ms
|
|
65
|
+
* @returns the `Config` to write, or `null` when nothing needs stamping
|
|
66
|
+
*/
|
|
67
|
+
export function BuildMintConfigStamps(args: {
|
|
68
|
+
EffectiveRealtime: unknown;
|
|
69
|
+
ExistingConfigRaw: string | null;
|
|
70
|
+
InheritedVerified?: VerifiedIdentity;
|
|
71
|
+
NowMs: number;
|
|
72
|
+
}): MintConfigStampPlan | null {
|
|
73
|
+
const layer = ExtractIdentityVerificationPolicyFromRealtimeConfig(args.EffectiveRealtime);
|
|
74
|
+
const hasPolicy = Object.keys(layer).length > 0;
|
|
75
|
+
if (!hasPolicy && !args.InheritedVerified) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const existingState = ReadIdentityVerificationState(args.ExistingConfigRaw, ReadIdentityVerificationPolicyLayer);
|
|
79
|
+
const state: IdentityVerificationState = {
|
|
80
|
+
...existingState,
|
|
81
|
+
...(hasPolicy ? { Policy: layer } : {}),
|
|
82
|
+
...(args.InheritedVerified ? { Verified: args.InheritedVerified } : {}),
|
|
83
|
+
};
|
|
84
|
+
const capSeconds = args.InheritedVerified ? (layer.verifiedMaxSeconds ?? layer.unverifiedMaxSeconds) : layer.unverifiedMaxSeconds;
|
|
85
|
+
const existingDeadline = ParseConfigObject(args.ExistingConfigRaw)['maxSessionDeadlineIso'];
|
|
86
|
+
const deadlineIso = capSeconds
|
|
87
|
+
? TightenSessionDeadline(typeof existingDeadline === 'string' ? existingDeadline : undefined, args.NowMs + capSeconds * 1000)
|
|
88
|
+
: undefined;
|
|
89
|
+
return { NextConfigRaw: WriteIdentityVerificationState(args.ExistingConfigRaw, state, deadlineIso) };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The verified identity of the session being resumed, when the caller may inherit it.
|
|
94
|
+
*
|
|
95
|
+
* Reads the prior session as the caller and requires {@link AuthorizeSessionAccess} to allow it; any
|
|
96
|
+
* failure (no such session, not the caller's, unreadable) yields `undefined` — inheritance is a
|
|
97
|
+
* convenience and must never block a session start.
|
|
98
|
+
*
|
|
99
|
+
* @param lastSessionId - the `lastSessionId` the client supplied at start
|
|
100
|
+
* @param contextUser - the starting caller
|
|
101
|
+
* @param provider - request-scoped provider
|
|
102
|
+
*/
|
|
103
|
+
export async function LoadInheritedVerifiedIdentity(
|
|
104
|
+
lastSessionId: string | undefined,
|
|
105
|
+
contextUser: UserInfo,
|
|
106
|
+
provider: IMetadataProvider,
|
|
107
|
+
): Promise<VerifiedIdentity | undefined> {
|
|
108
|
+
if (!lastSessionId) {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
const prior = await LoadSessionForAccess(lastSessionId, contextUser, provider);
|
|
112
|
+
if (!prior) {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
const decision = AuthorizeSessionAccess(
|
|
116
|
+
{ SessionUserID: prior.Session.UserID, ConversationExternalID: prior.ConversationExternalID },
|
|
117
|
+
contextUser,
|
|
118
|
+
);
|
|
119
|
+
if (decision.Allowed === false) {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
return ReadIdentityVerificationState(prior.Session.Config_, ReadIdentityVerificationPolicyLayer).Verified;
|
|
123
|
+
}
|