@memberjunction/server 6.2.0-edge.3 → 6.2.0-edge.5
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 +20 -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 +236 -27
- package/dist/agentSessions/SessionJanitor.d.ts.map +1 -1
- package/dist/agentSessions/SessionJanitor.js +682 -54
- package/dist/agentSessions/SessionJanitor.js.map +1 -1
- package/dist/agentSessions/SessionManager.d.ts +48 -3
- package/dist/agentSessions/SessionManager.d.ts.map +1 -1
- package/dist/agentSessions/SessionManager.js +112 -9
- 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/apiRoot.d.ts +32 -0
- package/dist/apiRoot.d.ts.map +1 -0
- package/dist/apiRoot.js +67 -0
- package/dist/apiRoot.js.map +1 -0
- package/dist/auth/AuthProviderCatalogRouter.d.ts.map +1 -1
- package/dist/auth/AuthProviderCatalogRouter.js +25 -0
- package/dist/auth/AuthProviderCatalogRouter.js.map +1 -1
- package/dist/auth/LoginScreenEngine.d.ts +22 -0
- package/dist/auth/LoginScreenEngine.d.ts.map +1 -0
- package/dist/auth/LoginScreenEngine.js +57 -0
- package/dist/auth/LoginScreenEngine.js.map +1 -0
- 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 +1 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +4 -10
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/magicLink/MagicLinkRouter.d.ts.map +1 -1
- package/dist/auth/magicLink/MagicLinkRouter.js +3 -2
- package/dist/auth/magicLink/MagicLinkRouter.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 +2514 -22
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +166 -4
- 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/coreRoutes.d.ts +29 -0
- package/dist/coreRoutes.d.ts.map +1 -0
- package/dist/coreRoutes.js +142 -0
- package/dist/coreRoutes.js.map +1 -0
- package/dist/generated/graphql-schemas/__mj.d.ts +504 -3
- package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
- package/dist/generated/graphql-schemas/__mj.js +2456 -7
- package/dist/generated/graphql-schemas/__mj.js.map +1 -1
- package/dist/generic/CacheInvalidationResolver.d.ts +43 -8
- package/dist/generic/CacheInvalidationResolver.d.ts.map +1 -1
- package/dist/generic/CacheInvalidationResolver.js +59 -19
- package/dist/generic/CacheInvalidationResolver.js.map +1 -1
- package/dist/generic/OrphanedConversationDetailReconciler.js +3 -3
- package/dist/generic/OrphanedConversationDetailReconciler.js.map +1 -1
- package/dist/generic/PushStatusResolver.js +1 -1
- package/dist/generic/PushStatusResolver.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +123 -48
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +293 -305
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +16 -0
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/globalCors.d.ts +33 -0
- package/dist/globalCors.d.ts.map +1 -0
- package/dist/globalCors.js +58 -0
- package/dist/globalCors.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +219 -278
- 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/multiTenancy/index.d.ts.map +1 -1
- package/dist/multiTenancy/index.js +22 -2
- package/dist/multiTenancy/index.js.map +1 -1
- 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 +139 -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 +99 -0
- package/dist/realtimeSessions/RealtimeVerifyRouter.js.map +1 -0
- package/dist/realtimeSessions/RecordingAssemblyScheduler.d.ts +100 -0
- package/dist/realtimeSessions/RecordingAssemblyScheduler.d.ts.map +1 -0
- package/dist/realtimeSessions/RecordingAssemblyScheduler.js +173 -0
- package/dist/realtimeSessions/RecordingAssemblyScheduler.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 +19 -0
- package/dist/realtimeSessions/index.d.ts.map +1 -0
- package/dist/realtimeSessions/index.js +19 -0
- package/dist/realtimeSessions/index.js.map +1 -0
- package/dist/realtimeSessions/ownedRealtimeSession.d.ts +30 -0
- package/dist/realtimeSessions/ownedRealtimeSession.d.ts.map +1 -0
- package/dist/realtimeSessions/ownedRealtimeSession.js +47 -0
- package/dist/realtimeSessions/ownedRealtimeSession.js.map +1 -0
- package/dist/realtimeSessions/sessionAccess.d.ts +92 -0
- package/dist/realtimeSessions/sessionAccess.d.ts.map +1 -0
- package/dist/realtimeSessions/sessionAccess.js +94 -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/ArtifactFileResolver.d.ts +5 -1
- package/dist/resolvers/ArtifactFileResolver.d.ts.map +1 -1
- package/dist/resolvers/ArtifactFileResolver.js +9 -7
- package/dist/resolvers/ArtifactFileResolver.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/ConversationRunEventsResolver.d.ts +8 -4
- package/dist/resolvers/ConversationRunEventsResolver.d.ts.map +1 -1
- package/dist/resolvers/ConversationRunEventsResolver.js +6 -6
- package/dist/resolvers/ConversationRunEventsResolver.js.map +1 -1
- package/dist/resolvers/DatasetResolver.d.ts +24 -3
- package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
- package/dist/resolvers/DatasetResolver.js +57 -8
- package/dist/resolvers/DatasetResolver.js.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.d.ts +2 -1
- package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.js +3 -2
- package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.d.ts +27 -1
- package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.js +148 -2
- package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
- package/dist/resolvers/FileResolver.d.ts +82 -11
- package/dist/resolvers/FileResolver.d.ts.map +1 -1
- package/dist/resolvers/FileResolver.js +279 -101
- package/dist/resolvers/FileResolver.js.map +1 -1
- package/dist/resolvers/GetDataContextDataResolver.d.ts +33 -7
- package/dist/resolvers/GetDataContextDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataContextDataResolver.js +117 -38
- package/dist/resolvers/GetDataContextDataResolver.js.map +1 -1
- package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataResolver.js +6 -3
- package/dist/resolvers/GetDataResolver.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/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +4 -2
- package/dist/resolvers/IntegrationDiscoveryResolver.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/QueryResolver.d.ts +2 -0
- package/dist/resolvers/QueryResolver.d.ts.map +1 -1
- package/dist/resolvers/QueryResolver.js +13 -0
- package/dist/resolvers/QueryResolver.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 +157 -33
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +404 -116
- 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 +100 -14
- 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/SearchKnowledgeStreamResolver.d.ts +12 -1
- package/dist/resolvers/SearchKnowledgeStreamResolver.d.ts.map +1 -1
- package/dist/resolvers/SearchKnowledgeStreamResolver.js +15 -2
- package/dist/resolvers/SearchKnowledgeStreamResolver.js.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.js +2 -3
- package/dist/resolvers/SqlLoggingConfigResolver.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/TaskGraphFrameResolver.d.ts +2 -1
- package/dist/resolvers/TaskGraphFrameResolver.d.ts.map +1 -1
- package/dist/resolvers/TaskGraphFrameResolver.js +28 -1
- package/dist/resolvers/TaskGraphFrameResolver.js.map +1 -1
- package/dist/resolvers/TestQuerySQLResolver.d.ts +24 -0
- package/dist/resolvers/TestQuerySQLResolver.d.ts.map +1 -1
- package/dist/resolvers/TestQuerySQLResolver.js +65 -13
- package/dist/resolvers/TestQuerySQLResolver.js.map +1 -1
- package/dist/resolvers/TransactionGroupResolver.d.ts +19 -2
- package/dist/resolvers/TransactionGroupResolver.d.ts.map +1 -1
- package/dist/resolvers/TransactionGroupResolver.js +102 -24
- package/dist/resolvers/TransactionGroupResolver.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/agent-run-data-guard.d.ts +31 -0
- package/dist/resolvers/agent-run-data-guard.d.ts.map +1 -0
- package/dist/resolvers/agent-run-data-guard.js +72 -0
- package/dist/resolvers/agent-run-data-guard.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/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/rest/RealtimeRecordingHandler.d.ts +49 -0
- package/dist/rest/RealtimeRecordingHandler.d.ts.map +1 -0
- package/dist/rest/RealtimeRecordingHandler.js +383 -0
- package/dist/rest/RealtimeRecordingHandler.js.map +1 -0
- package/dist/rest/ViewOperationsHandler.d.ts +3 -1
- package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
- package/dist/rest/ViewOperationsHandler.js +21 -12
- package/dist/rest/ViewOperationsHandler.js.map +1 -1
- package/dist/rest/setupRESTEndpoints.d.ts +6 -0
- package/dist/rest/setupRESTEndpoints.d.ts.map +1 -1
- package/dist/rest/setupRESTEndpoints.js +6 -0
- package/dist/rest/setupRESTEndpoints.js.map +1 -1
- package/dist/serverExtensionReservedRoots.d.ts +12 -10
- package/dist/serverExtensionReservedRoots.d.ts.map +1 -1
- package/dist/serverExtensionReservedRoots.js +14 -17
- package/dist/serverExtensionReservedRoots.js.map +1 -1
- package/dist/services/MJStorageBlobStore.d.ts +8 -1
- package/dist/services/MJStorageBlobStore.d.ts.map +1 -1
- package/dist/services/MJStorageBlobStore.js +25 -19
- package/dist/services/MJStorageBlobStore.js.map +1 -1
- package/dist/services/StartTaskGraphDispatcher.js +2 -2
- package/dist/services/StartTaskGraphDispatcher.js.map +1 -1
- package/dist/services/TaskGraphActionRunner.d.ts +5 -3
- package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphActionRunner.js +27 -5
- package/dist/services/TaskGraphActionRunner.js.map +1 -1
- package/dist/services/TaskGraphAgentRunner.d.ts +14 -2
- package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphAgentRunner.js +157 -9
- package/dist/services/TaskGraphAgentRunner.js.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.d.ts +57 -4
- package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.js +607 -18
- 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 +279 -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 +99 -96
- 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__/DatasetResolver.itemFilters.test.ts +201 -0
- package/src/__tests__/EntityRecordNameResolver.rowSecurity.test.ts +152 -0
- package/src/__tests__/FetchEntityVectorsResolver.test.ts +433 -0
- package/src/__tests__/FileResolver.storageAccess.test.ts +562 -0
- package/src/__tests__/GetDataContextDataResolver.sqlItems.test.ts +416 -0
- package/src/__tests__/HostInstance.test.ts +84 -0
- package/src/__tests__/HumanHandoffResolver.test.ts +17 -0
- package/src/__tests__/ListOperationsResolver.adhocScreen.test.ts +137 -0
- package/src/__tests__/LoginScreenEngine.test.ts +86 -0
- package/src/__tests__/MJStorageBlobStore.storageGate.test.ts +63 -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__/RecordingAssemblyScheduler.test.ts +178 -0
- package/src/__tests__/RemoteBrowserActionResolver.goalRunLink.test.ts +129 -0
- package/src/__tests__/ResolverBase.adhocReadScope.test.ts +133 -2
- package/src/__tests__/ResolverBase.aggregateScreen.test.ts +174 -0
- package/src/__tests__/ResolverBase.balancedClause.test.ts +155 -0
- package/src/__tests__/ResolverBase.clauseUnscopedRead.test.ts +127 -0
- package/src/__tests__/ResolverBase.clientOldValues.test.ts +212 -6
- package/src/__tests__/ResolverBase.filterEscaping.test.ts +5 -2
- package/src/__tests__/ReturningVisitorRecap.test.ts +115 -0
- package/src/__tests__/RunAIAgentResolver.branch.test.ts +170 -0
- package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +6 -4
- package/src/__tests__/RunAIAgentResolver.runDataGuard.test.ts +258 -0
- 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__/RunViewResolver.cacheCheckScreen.test.ts +165 -0
- package/src/__tests__/SearchKnowledgeStreamResolver.test.ts +161 -0
- package/src/__tests__/SessionJanitor.test.ts +1203 -21
- package/src/__tests__/SessionManager.test.ts +240 -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 +25 -0
- package/src/__tests__/TaskGraphAgentRunner.test.ts +266 -0
- package/src/__tests__/TaskGraphContinuationDeliverer.test.ts +403 -22
- package/src/__tests__/TaskGraphFrameResolver.test.ts +22 -0
- package/src/__tests__/TestQuerySQLResolver.test.ts +274 -0
- package/src/__tests__/TransactionGroupResolver.refusals.test.ts +98 -1
- package/src/__tests__/UpdateMJDataContextItem.oldValues.test.ts +220 -0
- package/src/__tests__/UserAvatarResolver.test.ts +255 -0
- package/src/__tests__/UserResolver.currentUser.test.ts +16 -1
- package/src/__tests__/ViewOperationsHandler.clauseScreen.test.ts +157 -0
- package/src/__tests__/actionAuthorization.test.ts +130 -0
- package/src/__tests__/agent-run-data-guard.test.ts +130 -0
- package/src/__tests__/agentVisionConsentAudit.test.ts +118 -0
- package/src/__tests__/apiRootBoot.test.ts +202 -0
- package/src/__tests__/avatarInputValidation.test.ts +160 -0
- package/src/__tests__/cacheInvalidationRecordData.test.ts +64 -0
- package/src/__tests__/conversationRunEvents.test.ts +56 -14
- package/src/__tests__/globalCors.test.ts +217 -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__/multiTenancy.postgresRewrite.test.ts +181 -0
- package/src/__tests__/multiTenancy.security.test.ts +51 -0
- package/src/__tests__/multiTenancy.test.ts +1 -1
- 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__/ownedRealtimeSession.test.ts +39 -0
- package/src/__tests__/pushStatusResolver.filter.test.ts +7 -0
- package/src/__tests__/realtimeRecordingHandler.test.ts +652 -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__/serverExtensionHostOverlay.test.ts +66 -0
- package/src/__tests__/serverExtensionReservedRoots.test.ts +11 -96
- package/src/__tests__/sharedCache.test.ts +312 -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 +821 -59
- package/src/agentSessions/SessionManager.ts +138 -26
- package/src/agentSessions/SessionRunIDVerifier.ts +171 -0
- package/src/agentSessions/index.ts +10 -3
- package/src/apiRoot.ts +78 -0
- package/src/auth/AuthProviderCatalogRouter.ts +26 -0
- package/src/auth/LoginScreenEngine.ts +50 -0
- package/src/auth/actionAuthorization.ts +116 -0
- package/src/auth/index.ts +4 -8
- package/src/auth/magicLink/MagicLinkRouter.ts +3 -2
- package/src/auth/magicLink/MagicLinkService.ts +6 -3
- package/src/auth/newUsers.ts +99 -11
- package/src/config.ts +171 -5
- package/src/context.ts +15 -10
- package/src/coreRoutes.ts +166 -0
- package/src/generated/graphql-schemas/__mj.ts +1815 -57
- package/src/generic/CacheInvalidationResolver.ts +73 -18
- package/src/generic/OrphanedConversationDetailReconciler.ts +3 -3
- package/src/generic/PushStatusResolver.ts +1 -1
- package/src/generic/ResolverBase.ts +335 -326
- package/src/generic/RunViewResolver.ts +21 -0
- package/src/globalCors.ts +71 -0
- package/src/index.ts +231 -300
- package/src/isolatedProvider.ts +22 -0
- package/src/multiTenancy/index.ts +24 -2
- 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 +168 -0
- package/src/realtimeSessions/RealtimeVerifyRouter.ts +109 -0
- package/src/realtimeSessions/RecordingAssemblyScheduler.ts +234 -0
- package/src/realtimeSessions/SessionVerificationPortsBase.ts +189 -0
- package/src/realtimeSessions/SlidingWindowRateLimiter.ts +92 -0
- package/src/realtimeSessions/index.ts +18 -0
- package/src/realtimeSessions/ownedRealtimeSession.ts +55 -0
- package/src/realtimeSessions/sessionAccess.ts +145 -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/ArtifactFileResolver.ts +9 -8
- package/src/resolvers/CacheStatsResolver.ts +89 -2
- package/src/resolvers/ClientToolRequestResolver.ts +25 -7
- package/src/resolvers/ConversationRunEventsResolver.ts +15 -11
- package/src/resolvers/DatasetResolver.ts +70 -10
- package/src/resolvers/EntityRecordNameResolver.ts +3 -2
- package/src/resolvers/FetchEntityVectorsResolver.ts +179 -2
- package/src/resolvers/FileResolver.ts +321 -106
- package/src/resolvers/GetDataContextDataResolver.ts +139 -41
- package/src/resolvers/GetDataResolver.ts +6 -3
- package/src/resolvers/HumanHandoffResolver.ts +8 -0
- package/src/resolvers/IntegrationDiscoveryResolver.ts +4 -2
- package/src/resolvers/MCPResolver.ts +8 -5
- package/src/resolvers/QueryResolver.ts +11 -0
- package/src/resolvers/RealtimeBridgeResolver.ts +163 -11
- package/src/resolvers/RealtimeClientSessionResolver.ts +511 -129
- 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 +116 -14
- package/src/resolvers/RunTemplateResolver.ts +13 -12
- package/src/resolvers/SearchKnowledgeStreamResolver.ts +19 -3
- package/src/resolvers/SqlLoggingConfigResolver.ts +2 -4
- package/src/resolvers/SyncDataResolver.ts +6 -2
- package/src/resolvers/SyncRolesUsersResolver.ts +24 -6
- package/src/resolvers/TaskGraphFrameResolver.ts +33 -1
- package/src/resolvers/TestQuerySQLResolver.ts +68 -13
- package/src/resolvers/TransactionGroupResolver.ts +131 -27
- package/src/resolvers/UserAvatarResolver.ts +111 -0
- package/src/resolvers/UserResolver.ts +2 -16
- package/src/resolvers/__tests__/GetDataResolver.validation.test.ts +95 -0
- package/src/resolvers/agent-run-data-guard.ts +89 -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/meetingMutations.ts +21 -0
- package/src/rest/RealtimeRecordingHandler.ts +464 -0
- package/src/rest/ViewOperationsHandler.ts +29 -13
- package/src/rest/setupRESTEndpoints.ts +6 -0
- package/src/serverExtensionReservedRoots.ts +14 -17
- package/src/services/MJStorageBlobStore.ts +31 -30
- package/src/services/StartTaskGraphDispatcher.ts +2 -2
- package/src/services/TaskGraphActionRunner.ts +25 -5
- package/src/services/TaskGraphAgentRunner.ts +188 -11
- package/src/services/TaskGraphContinuationDeliverer.ts +713 -17
- package/src/sharedCache.ts +305 -0
- package/src/types.ts +7 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import type { IMetadataProvider, UserInfo } from '@memberjunction/core';
|
|
3
|
+
import { AuthorizeSessionAccess, LoadSessionForAccess } from '../../realtimeSessions/sessionAccess.js';
|
|
4
|
+
|
|
5
|
+
const OWNER = 'AAAAAAAA-0000-4000-8000-000000000001';
|
|
6
|
+
const OTHER = 'AAAAAAAA-0000-4000-8000-000000000002';
|
|
7
|
+
const SESSION_ID = 'BBBBBBBB-0000-4000-8000-000000000001';
|
|
8
|
+
const CONVERSATION_ID = 'CCCCCCCC-0000-4000-8000-000000000001';
|
|
9
|
+
|
|
10
|
+
describe('AuthorizeSessionAccess', () => {
|
|
11
|
+
it('allows the named owner', () => {
|
|
12
|
+
expect(AuthorizeSessionAccess({ SessionUserID: OWNER, ConversationExternalID: null }, { ID: OWNER })).toEqual({ Allowed: true });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('compares user ids case-insensitively (SQL Server uppercases, PostgreSQL lowercases)', () => {
|
|
16
|
+
expect(AuthorizeSessionAccess({ SessionUserID: OWNER, ConversationExternalID: null }, { ID: OWNER.toLowerCase() })).toEqual({ Allowed: true });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('refuses a non-owner', () => {
|
|
20
|
+
expect(AuthorizeSessionAccess({ SessionUserID: OWNER, ConversationExternalID: null }, { ID: OTHER })).toEqual({ Allowed: false, Reason: 'not_owner' });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('refuses a missing principal and a missing owner (fail closed)', () => {
|
|
24
|
+
expect(AuthorizeSessionAccess({ SessionUserID: OWNER, ConversationExternalID: null }, null)).toEqual({ Allowed: false, Reason: 'not_owner' });
|
|
25
|
+
expect(AuthorizeSessionAccess({ SessionUserID: OWNER, ConversationExternalID: null }, undefined)).toEqual({ Allowed: false, Reason: 'not_owner' });
|
|
26
|
+
expect(AuthorizeSessionAccess({ SessionUserID: '', ConversationExternalID: null }, { ID: OWNER })).toEqual({ Allowed: false, Reason: 'not_owner' });
|
|
27
|
+
expect(AuthorizeSessionAccess({ SessionUserID: OWNER, ConversationExternalID: null }, { ID: '' })).toEqual({ Allowed: false, Reason: 'not_owner' });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('anonymous widget guests (one shared Anonymous user)', () => {
|
|
31
|
+
const guest = (scope?: string) => ({ ID: OWNER, IsMagicLinkAnonymous: true, MagicLinkScope: scope ? { ResourceID: scope } : undefined });
|
|
32
|
+
|
|
33
|
+
it('allows a guest whose signed scope matches the conversation ExternalID', () => {
|
|
34
|
+
expect(AuthorizeSessionAccess({ SessionUserID: OWNER, ConversationExternalID: 'scope-1' }, guest('scope-1'))).toEqual({ Allowed: true });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('refuses a different guest (same Anonymous user id, different scope) — the cross-guest leak', () => {
|
|
38
|
+
expect(AuthorizeSessionAccess({ SessionUserID: OWNER, ConversationExternalID: 'scope-1' }, guest('scope-2'))).toEqual({
|
|
39
|
+
Allowed: false,
|
|
40
|
+
Reason: 'scope_mismatch',
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('refuses an anonymous principal with no scope at all', () => {
|
|
45
|
+
expect(AuthorizeSessionAccess({ SessionUserID: OWNER, ConversationExternalID: 'scope-1' }, guest())).toEqual({
|
|
46
|
+
Allowed: false,
|
|
47
|
+
Reason: 'scope_missing',
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('refuses when the conversation carries no scope id (unreadable or unscoped)', () => {
|
|
52
|
+
expect(AuthorizeSessionAccess({ SessionUserID: OWNER, ConversationExternalID: null }, guest('scope-1'))).toEqual({ Allowed: false, Reason: 'scope_mismatch' });
|
|
53
|
+
expect(AuthorizeSessionAccess({ SessionUserID: OWNER, ConversationExternalID: undefined }, guest('scope-1'))).toEqual({ Allowed: false, Reason: 'scope_mismatch' });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('compares scope ids exactly — they are case-significant opaque strings, not UUIDs', () => {
|
|
57
|
+
expect(AuthorizeSessionAccess({ SessionUserID: OWNER, ConversationExternalID: 'AbC' }, guest('abc'))).toEqual({ Allowed: false, Reason: 'scope_mismatch' });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('still requires the owner to match', () => {
|
|
61
|
+
expect(AuthorizeSessionAccess({ SessionUserID: OTHER, ConversationExternalID: 'scope-1' }, guest('scope-1'))).toEqual({ Allowed: false, Reason: 'not_owner' });
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('requires a scope match for a NAMED principal that carries a resource scope', () => {
|
|
66
|
+
const scoped = { ID: OWNER, MagicLinkScope: { ResourceID: 'share-1' } };
|
|
67
|
+
expect(AuthorizeSessionAccess({ SessionUserID: OWNER, ConversationExternalID: 'share-1' }, scoped)).toEqual({ Allowed: true });
|
|
68
|
+
expect(AuthorizeSessionAccess({ SessionUserID: OWNER, ConversationExternalID: 'other' }, scoped)).toEqual({ Allowed: false, Reason: 'scope_mismatch' });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('does not require a scope of a named principal without one, whatever the conversation says', () => {
|
|
72
|
+
expect(AuthorizeSessionAccess({ SessionUserID: OWNER, ConversationExternalID: 'scope-1' }, { ID: OWNER })).toEqual({ Allowed: true });
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
/** Builds a fake provider whose entities load from the given rows. */
|
|
77
|
+
function fakeProvider(rows: { session?: Record<string, unknown> | null; conversation?: Record<string, unknown> | null; conversationThrows?: boolean }): IMetadataProvider {
|
|
78
|
+
const entity = (row: Record<string, unknown> | null | undefined, throws = false) => ({
|
|
79
|
+
...(row ?? {}),
|
|
80
|
+
Load: vi.fn(async () => {
|
|
81
|
+
if (throws) throw new Error('db down');
|
|
82
|
+
return row != null;
|
|
83
|
+
}),
|
|
84
|
+
});
|
|
85
|
+
return {
|
|
86
|
+
GetEntityObject: vi.fn(async (name: string) => {
|
|
87
|
+
if (name === 'MJ: AI Agent Sessions') return entity(rows.session);
|
|
88
|
+
if (name === 'MJ: Conversations') return entity(rows.conversation, rows.conversationThrows);
|
|
89
|
+
throw new Error(`unexpected entity ${name}`);
|
|
90
|
+
}),
|
|
91
|
+
} as unknown as IMetadataProvider;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const USER = { ID: OWNER } as UserInfo;
|
|
95
|
+
|
|
96
|
+
describe('LoadSessionForAccess', () => {
|
|
97
|
+
it('loads the session and its conversation scope id', async () => {
|
|
98
|
+
const provider = fakeProvider({ session: { ID: SESSION_ID, UserID: OWNER, ConversationID: CONVERSATION_ID }, conversation: { ExternalID: 'scope-1' } });
|
|
99
|
+
const loaded = await LoadSessionForAccess(SESSION_ID, USER, provider);
|
|
100
|
+
expect(loaded?.Session.ID).toBe(SESSION_ID);
|
|
101
|
+
expect(loaded?.ConversationExternalID).toBe('scope-1');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('returns null for a malformed id without touching the database', async () => {
|
|
105
|
+
const provider = fakeProvider({});
|
|
106
|
+
expect(await LoadSessionForAccess("x'; DROP TABLE y;--", USER, provider)).toBeNull();
|
|
107
|
+
expect(provider.GetEntityObject).not.toHaveBeenCalled();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('returns null when the session does not exist (or the caller cannot read it)', async () => {
|
|
111
|
+
expect(await LoadSessionForAccess(SESSION_ID, USER, fakeProvider({ session: null }))).toBeNull();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('reports a null scope id when the session has no conversation', async () => {
|
|
115
|
+
const loaded = await LoadSessionForAccess(SESSION_ID, USER, fakeProvider({ session: { ID: SESSION_ID, UserID: OWNER, ConversationID: null } }));
|
|
116
|
+
expect(loaded?.ConversationExternalID).toBeNull();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('reports a null scope id when the conversation is unreadable, so a scoped caller is refused downstream', async () => {
|
|
120
|
+
const unreadable = await LoadSessionForAccess(SESSION_ID, USER, fakeProvider({ session: { ID: SESSION_ID, UserID: OWNER, ConversationID: CONVERSATION_ID }, conversation: null }));
|
|
121
|
+
expect(unreadable?.ConversationExternalID).toBeNull();
|
|
122
|
+
const throwing = await LoadSessionForAccess(
|
|
123
|
+
SESSION_ID,
|
|
124
|
+
USER,
|
|
125
|
+
fakeProvider({ session: { ID: SESSION_ID, UserID: OWNER, ConversationID: CONVERSATION_ID }, conversation: {}, conversationThrows: true }),
|
|
126
|
+
);
|
|
127
|
+
expect(throwing?.ConversationExternalID).toBeNull();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import type { IMetadataProvider, UserInfo } from '@memberjunction/core';
|
|
3
|
+
import { BuildMintConfigStamps, LoadInheritedVerifiedIdentity } from '../../realtimeSessions/sessionConfigStamps.js';
|
|
4
|
+
import type { VerifiedIdentity } from '../../realtimeSessions/verificationCore.js';
|
|
5
|
+
|
|
6
|
+
const NOW = Date.parse('2026-10-02T12:00:00.000Z');
|
|
7
|
+
const VERIFIED: VerifiedIdentity = { Email: 'pat@acme.com', Name: 'Pat', VerifiedAt: '2026-10-02T11:00:00.000Z', Method: 'link' };
|
|
8
|
+
const configOf = (plan: { NextConfigRaw: string } | null) => JSON.parse((plan as { NextConfigRaw: string }).NextConfigRaw) as Record<string, unknown>;
|
|
9
|
+
|
|
10
|
+
describe('BuildMintConfigStamps', () => {
|
|
11
|
+
it('stamps nothing when the cascade carries no verification policy and nothing is inherited', () => {
|
|
12
|
+
expect(BuildMintConfigStamps({ EffectiveRealtime: undefined, ExistingConfigRaw: '{"targetAgentID":"t"}', NowMs: NOW })).toBeNull();
|
|
13
|
+
expect(BuildMintConfigStamps({ EffectiveRealtime: { session: { turnDetection: {} } }, ExistingConfigRaw: null, NowMs: NOW })).toBeNull();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('snapshots the channel policy onto the session and preserves every other key', () => {
|
|
17
|
+
const plan = BuildMintConfigStamps({
|
|
18
|
+
EffectiveRealtime: { channels: { config: { IdentityVerification: { requireBusinessDomain: true, maxSendsPerSession: 2 } } } },
|
|
19
|
+
ExistingConfigRaw: '{"targetAgentID":"t","mediaCollectionID":"m"}',
|
|
20
|
+
NowMs: NOW,
|
|
21
|
+
});
|
|
22
|
+
const config = configOf(plan);
|
|
23
|
+
expect(config).toMatchObject({ targetAgentID: 't', mediaCollectionID: 'm' });
|
|
24
|
+
expect(config['identityVerification']).toEqual({ SendCount: 0, Policy: { requireBusinessDomain: true, maxSendsPerSession: 2 } });
|
|
25
|
+
expect(config['maxSessionDeadlineIso']).toBeUndefined();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('stamps the unverified cap as the absolute deadline for ANY principal (not only widget guests)', () => {
|
|
29
|
+
const plan = BuildMintConfigStamps({ EffectiveRealtime: { session: { unverifiedMaxSeconds: 300 } }, ExistingConfigRaw: '{}', NowMs: NOW });
|
|
30
|
+
expect(configOf(plan)['maxSessionDeadlineIso']).toBe('2026-10-02T12:05:00.000Z');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('never loosens a tighter deadline already stamped (the widget voice cap)', () => {
|
|
34
|
+
const tighter = BuildMintConfigStamps({
|
|
35
|
+
EffectiveRealtime: { session: { unverifiedMaxSeconds: 600 } },
|
|
36
|
+
ExistingConfigRaw: JSON.stringify({ maxSessionDeadlineIso: '2026-10-02T12:02:00.000Z' }),
|
|
37
|
+
NowMs: NOW,
|
|
38
|
+
});
|
|
39
|
+
expect(configOf(tighter)['maxSessionDeadlineIso']).toBe('2026-10-02T12:02:00.000Z');
|
|
40
|
+
const looser = BuildMintConfigStamps({
|
|
41
|
+
EffectiveRealtime: { session: { unverifiedMaxSeconds: 60 } },
|
|
42
|
+
ExistingConfigRaw: JSON.stringify({ maxSessionDeadlineIso: '2026-10-02T12:10:00.000Z' }),
|
|
43
|
+
NowMs: NOW,
|
|
44
|
+
});
|
|
45
|
+
expect(configOf(looser)['maxSessionDeadlineIso']).toBe('2026-10-02T12:01:00.000Z');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('does not touch the deadline when only a verified cap is configured (it applies after verification)', () => {
|
|
49
|
+
const plan = BuildMintConfigStamps({ EffectiveRealtime: { session: { verifiedMaxSeconds: 3600 } }, ExistingConfigRaw: '{}', NowMs: NOW });
|
|
50
|
+
const config = configOf(plan);
|
|
51
|
+
expect(config['maxSessionDeadlineIso']).toBeUndefined();
|
|
52
|
+
expect((config['identityVerification'] as { Policy: unknown }).Policy).toEqual({ verifiedMaxSeconds: 3600 });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('carries an inherited verified identity forward even with no policy, and applies the VERIFIED cap to such a session', () => {
|
|
56
|
+
const noPolicy = BuildMintConfigStamps({ EffectiveRealtime: undefined, ExistingConfigRaw: '{}', InheritedVerified: VERIFIED, NowMs: NOW });
|
|
57
|
+
expect((configOf(noPolicy)['identityVerification'] as { Verified: unknown }).Verified).toEqual(VERIFIED);
|
|
58
|
+
|
|
59
|
+
const capped = BuildMintConfigStamps({
|
|
60
|
+
EffectiveRealtime: { session: { unverifiedMaxSeconds: 300, verifiedMaxSeconds: 1800 } },
|
|
61
|
+
ExistingConfigRaw: '{}',
|
|
62
|
+
InheritedVerified: VERIFIED,
|
|
63
|
+
NowMs: NOW,
|
|
64
|
+
});
|
|
65
|
+
expect(configOf(capped)['maxSessionDeadlineIso']).toBe('2026-10-02T12:30:00.000Z');
|
|
66
|
+
|
|
67
|
+
const fallsBack = BuildMintConfigStamps({
|
|
68
|
+
EffectiveRealtime: { session: { unverifiedMaxSeconds: 300 } },
|
|
69
|
+
ExistingConfigRaw: '{}',
|
|
70
|
+
InheritedVerified: VERIFIED,
|
|
71
|
+
NowMs: NOW,
|
|
72
|
+
});
|
|
73
|
+
expect(configOf(fallsBack)['maxSessionDeadlineIso']).toBe('2026-10-02T12:05:00.000Z');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('keeps existing verification state when stamping (a re-stamp must not reset the send count)', () => {
|
|
77
|
+
const plan = BuildMintConfigStamps({
|
|
78
|
+
EffectiveRealtime: { channels: { config: { IdentityVerification: { requireBusinessDomain: true } } } },
|
|
79
|
+
ExistingConfigRaw: JSON.stringify({ identityVerification: { SendCount: 2 } }),
|
|
80
|
+
NowMs: NOW,
|
|
81
|
+
});
|
|
82
|
+
expect((configOf(plan)['identityVerification'] as { SendCount: number }).SendCount).toBe(2);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('LoadInheritedVerifiedIdentity', () => {
|
|
87
|
+
const OWNER = 'AAAAAAAA-0000-4000-8000-000000000001';
|
|
88
|
+
const STRANGER = 'AAAAAAAA-0000-4000-8000-000000000002';
|
|
89
|
+
const PRIOR = 'BBBBBBBB-0000-4000-8000-000000000001';
|
|
90
|
+
const config = JSON.stringify({ identityVerification: { SendCount: 1, Verified: VERIFIED } });
|
|
91
|
+
|
|
92
|
+
const provider = (opts: { found?: boolean; owner?: string; externalId?: string | null; config?: string | null } = {}): IMetadataProvider =>
|
|
93
|
+
({
|
|
94
|
+
GetEntityObject: vi.fn(async (name: string) => {
|
|
95
|
+
if (name === 'MJ: AI Agent Sessions') {
|
|
96
|
+
return { ID: PRIOR, UserID: opts.owner ?? OWNER, ConversationID: 'C1', Config_: opts.config === undefined ? config : opts.config, Load: vi.fn(async () => opts.found ?? true) };
|
|
97
|
+
}
|
|
98
|
+
return { ExternalID: opts.externalId ?? null, Load: vi.fn(async () => true) };
|
|
99
|
+
}),
|
|
100
|
+
}) as unknown as IMetadataProvider;
|
|
101
|
+
|
|
102
|
+
const user = (over: Partial<UserInfo> = {}) => ({ ID: OWNER, ...over }) as UserInfo;
|
|
103
|
+
|
|
104
|
+
it('returns the prior session\'s verified identity for its owner', async () => {
|
|
105
|
+
expect(await LoadInheritedVerifiedIdentity(PRIOR, user(), provider())).toEqual(VERIFIED);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('returns undefined with no lastSessionId, an unknown session, or an unverified prior', async () => {
|
|
109
|
+
expect(await LoadInheritedVerifiedIdentity(undefined, user(), provider())).toBeUndefined();
|
|
110
|
+
expect(await LoadInheritedVerifiedIdentity(PRIOR, user(), provider({ found: false }))).toBeUndefined();
|
|
111
|
+
expect(await LoadInheritedVerifiedIdentity(PRIOR, user(), provider({ config: '{}' }))).toBeUndefined();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('refuses to hand a stranger someone else\'s verification by naming their session id', async () => {
|
|
115
|
+
expect(await LoadInheritedVerifiedIdentity(PRIOR, user({ ID: STRANGER }), provider())).toBeUndefined();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('refuses another anonymous guest (same Anonymous user, different scope) and accepts the same guest', async () => {
|
|
119
|
+
const prov = provider({ externalId: 'scope-A' });
|
|
120
|
+
const guest = (scope: string) => user({ IsMagicLinkAnonymous: true, MagicLinkScope: { ResourceID: scope } });
|
|
121
|
+
expect(await LoadInheritedVerifiedIdentity(PRIOR, guest('scope-B'), prov)).toBeUndefined();
|
|
122
|
+
expect(await LoadInheritedVerifiedIdentity(PRIOR, guest('scope-A'), prov)).toEqual(VERIFIED);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// Wrap crypto.timingSafeEqual in a spy (keeping its behaviour) so the constant-time requirement is asserted, not assumed.
|
|
4
|
+
vi.mock('node:crypto', async (importOriginal) => {
|
|
5
|
+
const actual = await importOriginal<typeof import('node:crypto')>();
|
|
6
|
+
return { ...actual, timingSafeEqual: vi.fn(actual.timingSafeEqual) };
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
10
|
+
import {
|
|
11
|
+
ComputeVerifiedDeadline,
|
|
12
|
+
EmptyIdentityVerificationState,
|
|
13
|
+
EvaluateRedemption,
|
|
14
|
+
GenerateVerificationCode,
|
|
15
|
+
GenerateVerificationToken,
|
|
16
|
+
HashVerificationCode,
|
|
17
|
+
HashVerificationToken,
|
|
18
|
+
HashesEqual,
|
|
19
|
+
IssueVerification,
|
|
20
|
+
NormalizeVerificationCode,
|
|
21
|
+
ParseVerificationToken,
|
|
22
|
+
ReadIdentityVerificationState,
|
|
23
|
+
SanitizeVerificationName,
|
|
24
|
+
VERIFICATION_CODE_LENGTH,
|
|
25
|
+
VERIFICATION_TOKEN_PREFIX,
|
|
26
|
+
WriteIdentityVerificationState,
|
|
27
|
+
type IdentityVerificationState,
|
|
28
|
+
} from '../../realtimeSessions/verificationCore.js';
|
|
29
|
+
import { ReadIdentityVerificationPolicyLayer } from '../../realtimeSessions/verificationPolicy.js';
|
|
30
|
+
|
|
31
|
+
const SESSION_ID = 'A1B2C3D4-0000-4000-8000-00000000A001';
|
|
32
|
+
const NOW = Date.parse('2026-10-02T12:00:00.000Z');
|
|
33
|
+
const POLICY = { maxCodeAttempts: 3 };
|
|
34
|
+
|
|
35
|
+
const KEY = 'unit-test-code-hash-key-0123456789';
|
|
36
|
+
|
|
37
|
+
function issue(overrides: Partial<Parameters<typeof IssueVerification>[0]> = {}) {
|
|
38
|
+
return IssueVerification({ AgentSessionID: SESSION_ID, Email: 'pat@example.com', Name: 'Pat', TtlMinutes: 30, NowMs: NOW, CodeHashKey: KEY, ...overrides });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function pendingState(issued = issue()): IdentityVerificationState {
|
|
42
|
+
return { SendCount: 1, Pending: issued.Pending };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe('GenerateVerificationToken / ParseVerificationToken', () => {
|
|
46
|
+
it('embeds the session id and 256 bits of randomness', () => {
|
|
47
|
+
const token = GenerateVerificationToken(SESSION_ID);
|
|
48
|
+
expect(token).toMatch(new RegExp(`^${VERIFICATION_TOKEN_PREFIX}[0-9a-f]{32}_[0-9a-f]{64}$`));
|
|
49
|
+
expect(ParseVerificationToken(token)).toEqual({ AgentSessionID: SESSION_ID.toLowerCase() });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('produces unique tokens', () => {
|
|
53
|
+
expect(new Set(Array.from({ length: 200 }, () => GenerateVerificationToken(SESSION_ID))).size).toBe(200);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('rejects a non-UUID session id at generation (a programming error)', () => {
|
|
57
|
+
expect(() => GenerateVerificationToken('not-a-uuid')).toThrow();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it.each([
|
|
61
|
+
['empty', ''],
|
|
62
|
+
['wrong prefix', 'mj_ml_' + 'a'.repeat(32) + '_' + 'b'.repeat(64)],
|
|
63
|
+
['short randomness', `${VERIFICATION_TOKEN_PREFIX}${'a'.repeat(32)}_${'b'.repeat(63)}`],
|
|
64
|
+
['uppercase hex', `${VERIFICATION_TOKEN_PREFIX}${'A'.repeat(32)}_${'b'.repeat(64)}`],
|
|
65
|
+
['trailing junk', `${VERIFICATION_TOKEN_PREFIX}${'a'.repeat(32)}_${'b'.repeat(64)}x`],
|
|
66
|
+
['missing separator', `${VERIFICATION_TOKEN_PREFIX}${'a'.repeat(96)}`],
|
|
67
|
+
])('rejects a malformed token (%s)', (_label, token) => {
|
|
68
|
+
expect(ParseVerificationToken(token)).toBeNull();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('rejects non-string input', () => {
|
|
72
|
+
expect(ParseVerificationToken(undefined as unknown as string)).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('verification code', () => {
|
|
77
|
+
it('is always exactly the configured number of digits, including leading zeros', () => {
|
|
78
|
+
for (let i = 0; i < 500; i++) {
|
|
79
|
+
expect(GenerateVerificationCode()).toMatch(new RegExp(`^\\d{${VERIFICATION_CODE_LENGTH}}$`));
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('normalises spaces and dashes and rejects everything else', () => {
|
|
84
|
+
expect(NormalizeVerificationCode('123 456')).toBe('123456');
|
|
85
|
+
expect(NormalizeVerificationCode(' 123-456 ')).toBe('123456');
|
|
86
|
+
expect(NormalizeVerificationCode('12345')).toBeNull();
|
|
87
|
+
expect(NormalizeVerificationCode('1234567')).toBeNull();
|
|
88
|
+
expect(NormalizeVerificationCode('12345a')).toBeNull();
|
|
89
|
+
expect(NormalizeVerificationCode('')).toBeNull();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('hashing', () => {
|
|
94
|
+
it('hashes tokens deterministically into base64url and never equals the token', () => {
|
|
95
|
+
const token = GenerateVerificationToken(SESSION_ID);
|
|
96
|
+
expect(HashVerificationToken(token)).toBe(HashVerificationToken(token));
|
|
97
|
+
expect(HashVerificationToken(token)).toMatch(/^[A-Za-z0-9_-]{43}$/);
|
|
98
|
+
expect(HashVerificationToken(token)).not.toBe(token);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('salts and session-binds code hashes', () => {
|
|
102
|
+
const a = HashVerificationCode('123456', 'salt-a', SESSION_ID, KEY);
|
|
103
|
+
expect(a).toBe(HashVerificationCode('123456', 'salt-a', SESSION_ID, KEY));
|
|
104
|
+
expect(a).not.toBe(HashVerificationCode('123456', 'salt-b', SESSION_ID, KEY));
|
|
105
|
+
expect(a).not.toBe(HashVerificationCode('123456', 'salt-a', 'A1B2C3D4-0000-4000-8000-00000000A002', KEY));
|
|
106
|
+
expect(a).not.toBe(HashVerificationCode('654321', 'salt-a', SESSION_ID, KEY));
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('keys the code hash: without the server key the stored value cannot be reversed by enumeration', () => {
|
|
110
|
+
const stored = HashVerificationCode('123456', 'salt', SESSION_ID, KEY);
|
|
111
|
+
// What a session owner who can read Config (salt + hash) could try: every code under a different key.
|
|
112
|
+
expect(HashVerificationCode('123456', 'salt', SESSION_ID, 'some-other-key-0123456789abcdef')).not.toBe(stored);
|
|
113
|
+
const recovered = Array.from({ length: 1000 }, (_, i) => String(i).padStart(6, '0')).filter(
|
|
114
|
+
(guess) => HashVerificationCode(guess, 'salt', SESSION_ID, 'attacker-guess-key-0123456789ab') === stored,
|
|
115
|
+
);
|
|
116
|
+
expect(recovered).toEqual([]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('refuses to hash without a key (fail closed, never an unkeyed hash)', () => {
|
|
120
|
+
expect(() => HashVerificationCode('123456', 'salt', SESSION_ID, '')).toThrow(/key/i);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('a code issued under one key is not redeemable under another', () => {
|
|
124
|
+
const issued = issue();
|
|
125
|
+
const credential = { Kind: 'code' as const, Code: issued.Code, AgentSessionID: SESSION_ID };
|
|
126
|
+
const other = EvaluateRedemption(pendingState(issued), credential, NOW, POLICY, 'rotated-key-0123456789abcdefgh');
|
|
127
|
+
expect(other.Outcome).toBe('rejected');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('treats the session id case-insensitively when binding a code', () => {
|
|
131
|
+
expect(HashVerificationCode('123456', 's', SESSION_ID, KEY)).toBe(HashVerificationCode('123456', 's', SESSION_ID.toLowerCase(), KEY));
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('HashesEqual', () => {
|
|
136
|
+
it('is true for equal and false for unequal hashes, including different lengths', () => {
|
|
137
|
+
expect(HashesEqual('abc', 'abc')).toBe(true);
|
|
138
|
+
expect(HashesEqual('abc', 'abd')).toBe(false);
|
|
139
|
+
expect(HashesEqual('abc', 'abcd')).toBe(false);
|
|
140
|
+
expect(HashesEqual('', '')).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('compares through crypto.timingSafeEqual on fixed-length digests (constant time)', () => {
|
|
144
|
+
vi.mocked(timingSafeEqual).mockClear();
|
|
145
|
+
HashesEqual('short', 'a-much-longer-value-than-the-other');
|
|
146
|
+
expect(timingSafeEqual).toHaveBeenCalledTimes(1);
|
|
147
|
+
const [a, b] = vi.mocked(timingSafeEqual).mock.calls[0] as [Buffer, Buffer];
|
|
148
|
+
expect(a.length).toBe(32);
|
|
149
|
+
expect(b.length).toBe(32);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('SanitizeVerificationName', () => {
|
|
154
|
+
it('collapses whitespace and trims', () => {
|
|
155
|
+
expect(SanitizeVerificationName(' Pat Smith ')).toBe('Pat Smith');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('strips control characters and markup characters', () => {
|
|
159
|
+
expect(SanitizeVerificationName('Pat\u0000 <b>"Smith"</b>\r\nBcc: x')).toBe('Pat b Smith /b Bcc: x');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('caps the length', () => {
|
|
163
|
+
expect(SanitizeVerificationName('x'.repeat(500))).toHaveLength(100);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('returns null when nothing usable is left', () => {
|
|
167
|
+
expect(SanitizeVerificationName(' ')).toBeNull();
|
|
168
|
+
expect(SanitizeVerificationName('<>"')).toBeNull();
|
|
169
|
+
expect(SanitizeVerificationName(undefined as unknown as string)).toBeNull();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('IssueVerification', () => {
|
|
174
|
+
it('stores hashes only — neither the token nor the code appear in the pending state', () => {
|
|
175
|
+
const issued = issue();
|
|
176
|
+
const serialized = JSON.stringify(issued.Pending);
|
|
177
|
+
expect(serialized).not.toContain(issued.Token);
|
|
178
|
+
expect(serialized).not.toContain(issued.Code);
|
|
179
|
+
expect(issued.Pending.TokenHash).toBe(HashVerificationToken(issued.Token));
|
|
180
|
+
expect(issued.Pending.CodeHash).toBe(HashVerificationCode(issued.Code, issued.Pending.CodeSalt, SESSION_ID, KEY));
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('sets the expiry from the TTL and starts with zero attempts', () => {
|
|
184
|
+
const { Pending } = issue({ TtlMinutes: 15 });
|
|
185
|
+
expect(Pending.RequestedAt).toBe('2026-10-02T12:00:00.000Z');
|
|
186
|
+
expect(Pending.ExpiresAt).toBe('2026-10-02T12:15:00.000Z');
|
|
187
|
+
expect(Pending.CodeAttempts).toBe(0);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('uses a fresh salt each time', () => {
|
|
191
|
+
expect(issue().Pending.CodeSalt).not.toBe(issue().Pending.CodeSalt);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe('EvaluateRedemption — link token', () => {
|
|
196
|
+
it('verifies with the right token, records the identity and clears the pending verification', () => {
|
|
197
|
+
const issued = issue();
|
|
198
|
+
const result = EvaluateRedemption(pendingState(issued), { Kind: 'token', Token: issued.Token }, NOW + 1000, POLICY, KEY);
|
|
199
|
+
expect(result.Outcome).toBe('verified');
|
|
200
|
+
if (result.Outcome !== 'verified') return;
|
|
201
|
+
expect(result.Verified).toEqual({ Email: 'pat@example.com', Name: 'Pat', VerifiedAt: '2026-10-02T12:00:01.000Z', Method: 'link' });
|
|
202
|
+
expect(result.Next.Pending).toBeUndefined();
|
|
203
|
+
expect(result.Next.Verified).toEqual(result.Verified);
|
|
204
|
+
expect(result.Next.SendCount).toBe(1);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('is single-use: after success the same token no longer redeems', () => {
|
|
208
|
+
const issued = issue();
|
|
209
|
+
const first = EvaluateRedemption(pendingState(issued), { Kind: 'token', Token: issued.Token }, NOW, POLICY, KEY);
|
|
210
|
+
if (first.Outcome !== 'verified') throw new Error('expected verified');
|
|
211
|
+
const second = EvaluateRedemption(first.Next, { Kind: 'token', Token: issued.Token }, NOW + 1, POLICY, KEY);
|
|
212
|
+
expect(second).toMatchObject({ Outcome: 'rejected', Reason: 'already_verified' });
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('rejects a wrong token without changing state (a stranger cannot burn a pending link)', () => {
|
|
216
|
+
const state = pendingState();
|
|
217
|
+
const result = EvaluateRedemption(state, { Kind: 'token', Token: GenerateVerificationToken(SESSION_ID) }, NOW, POLICY, KEY);
|
|
218
|
+
expect(result).toMatchObject({ Outcome: 'rejected', Reason: 'invalid' });
|
|
219
|
+
expect(result.Next).toBe(state);
|
|
220
|
+
expect(result.Next.Pending?.CodeAttempts).toBe(0);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('rejects an expired token and clears the stale pending verification', () => {
|
|
224
|
+
const issued = issue({ TtlMinutes: 5 });
|
|
225
|
+
const result = EvaluateRedemption(pendingState(issued), { Kind: 'token', Token: issued.Token }, NOW + 5 * 60_000, POLICY, KEY);
|
|
226
|
+
expect(result).toMatchObject({ Outcome: 'rejected', Reason: 'expired' });
|
|
227
|
+
expect(result.Next.Pending).toBeUndefined();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('is still valid the instant before expiry', () => {
|
|
231
|
+
const issued = issue({ TtlMinutes: 5 });
|
|
232
|
+
expect(EvaluateRedemption(pendingState(issued), { Kind: 'token', Token: issued.Token }, NOW + 5 * 60_000 - 1, POLICY, KEY).Outcome).toBe('verified');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('treats an unparseable expiry as expired (fail closed)', () => {
|
|
236
|
+
const issued = issue();
|
|
237
|
+
const state: IdentityVerificationState = { SendCount: 1, Pending: { ...issued.Pending, ExpiresAt: 'garbage' } };
|
|
238
|
+
expect(EvaluateRedemption(state, { Kind: 'token', Token: issued.Token }, NOW, POLICY, KEY)).toMatchObject({ Outcome: 'rejected', Reason: 'expired' });
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('rejects when nothing is pending', () => {
|
|
242
|
+
expect(EvaluateRedemption(EmptyIdentityVerificationState(), { Kind: 'token', Token: 'x' }, NOW, POLICY, KEY)).toMatchObject({ Outcome: 'rejected', Reason: 'no_pending' });
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe('EvaluateRedemption — typed code', () => {
|
|
247
|
+
const codeCredential = (code: string) => ({ Kind: 'code' as const, Code: code, AgentSessionID: SESSION_ID });
|
|
248
|
+
|
|
249
|
+
it('verifies with the right code', () => {
|
|
250
|
+
const issued = issue();
|
|
251
|
+
const result = EvaluateRedemption(pendingState(issued), codeCredential(issued.Code), NOW, POLICY, KEY);
|
|
252
|
+
expect(result.Outcome).toBe('verified');
|
|
253
|
+
if (result.Outcome === 'verified') {
|
|
254
|
+
expect(result.Verified.Method).toBe('code');
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('counts wrong codes and reports the attempts left', () => {
|
|
259
|
+
const issued = issue();
|
|
260
|
+
const wrong = issued.Code === '000000' ? '111111' : '000000';
|
|
261
|
+
const first = EvaluateRedemption(pendingState(issued), codeCredential(wrong), NOW, POLICY, KEY);
|
|
262
|
+
expect(first).toMatchObject({ Outcome: 'rejected', Reason: 'invalid', AttemptsRemaining: 2 });
|
|
263
|
+
expect(first.Next.Pending?.CodeAttempts).toBe(1);
|
|
264
|
+
const second = EvaluateRedemption(first.Next, codeCredential(wrong), NOW, POLICY, KEY);
|
|
265
|
+
expect(second).toMatchObject({ Reason: 'invalid', AttemptsRemaining: 1 });
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('voids the pending verification at the attempt cap — even for the right code afterwards', () => {
|
|
269
|
+
const issued = issue();
|
|
270
|
+
const wrong = issued.Code === '000000' ? '111111' : '000000';
|
|
271
|
+
let state = pendingState(issued);
|
|
272
|
+
for (let i = 0; i < 2; i++) {
|
|
273
|
+
state = EvaluateRedemption(state, codeCredential(wrong), NOW, POLICY, KEY).Next;
|
|
274
|
+
}
|
|
275
|
+
const last = EvaluateRedemption(state, codeCredential(wrong), NOW, POLICY, KEY);
|
|
276
|
+
expect(last).toMatchObject({ Outcome: 'rejected', Reason: 'attempts_exhausted', AttemptsRemaining: 0 });
|
|
277
|
+
expect(last.Next.Pending).toBeUndefined();
|
|
278
|
+
expect(EvaluateRedemption(last.Next, codeCredential(issued.Code), NOW, POLICY, KEY)).toMatchObject({ Reason: 'no_pending' });
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('does not accept a code issued for a different session', () => {
|
|
282
|
+
const issued = issue();
|
|
283
|
+
const result = EvaluateRedemption(pendingState(issued), { Kind: 'code', Code: issued.Code, AgentSessionID: 'A1B2C3D4-0000-4000-8000-00000000A002' }, NOW, POLICY, KEY);
|
|
284
|
+
expect(result).toMatchObject({ Outcome: 'rejected', Reason: 'invalid' });
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('is single-use and expires like the link', () => {
|
|
288
|
+
const issued = issue({ TtlMinutes: 1 });
|
|
289
|
+
expect(EvaluateRedemption(pendingState(issued), codeCredential(issued.Code), NOW + 60_000, POLICY, KEY)).toMatchObject({ Reason: 'expired' });
|
|
290
|
+
const ok = EvaluateRedemption(pendingState(issued), codeCredential(issued.Code), NOW, POLICY, KEY);
|
|
291
|
+
if (ok.Outcome !== 'verified') throw new Error('expected verified');
|
|
292
|
+
expect(EvaluateRedemption(ok.Next, codeCredential(issued.Code), NOW, POLICY, KEY)).toMatchObject({ Reason: 'already_verified' });
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('lets a successful link redemption invalidate the code too (one pending verification, two secrets)', () => {
|
|
296
|
+
const issued = issue();
|
|
297
|
+
const viaLink = EvaluateRedemption(pendingState(issued), { Kind: 'token', Token: issued.Token }, NOW, POLICY, KEY);
|
|
298
|
+
if (viaLink.Outcome !== 'verified') throw new Error('expected verified');
|
|
299
|
+
expect(viaLink.Next.Pending).toBeUndefined();
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
describe('state persistence', () => {
|
|
304
|
+
it('round-trips through the Config JSON, preserving every other key', () => {
|
|
305
|
+
const issued = issue();
|
|
306
|
+
const state: IdentityVerificationState = { Policy: { requireBusinessDomain: true }, SendCount: 2, Pending: issued.Pending };
|
|
307
|
+
const raw = WriteIdentityVerificationState('{"targetAgentID":"t","maxSessionDeadlineIso":"2026-10-02T13:00:00.000Z"}', state);
|
|
308
|
+
expect(JSON.parse(raw)).toMatchObject({ targetAgentID: 't', maxSessionDeadlineIso: '2026-10-02T13:00:00.000Z' });
|
|
309
|
+
expect(ReadIdentityVerificationState(raw, ReadIdentityVerificationPolicyLayer)).toEqual(state);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('writes a new deadline only when asked', () => {
|
|
313
|
+
const base = '{"maxSessionDeadlineIso":"2026-10-02T13:00:00.000Z"}';
|
|
314
|
+
expect(JSON.parse(WriteIdentityVerificationState(base, EmptyIdentityVerificationState())).maxSessionDeadlineIso).toBe('2026-10-02T13:00:00.000Z');
|
|
315
|
+
expect(JSON.parse(WriteIdentityVerificationState(base, EmptyIdentityVerificationState(), '2026-10-02T14:00:00.000Z')).maxSessionDeadlineIso).toBe('2026-10-02T14:00:00.000Z');
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('reads garbage and partial values as an empty/partial state, never throwing', () => {
|
|
319
|
+
const read = (raw: string | null) => ReadIdentityVerificationState(raw, ReadIdentityVerificationPolicyLayer);
|
|
320
|
+
expect(read(null)).toEqual(EmptyIdentityVerificationState());
|
|
321
|
+
expect(read('{not json')).toEqual(EmptyIdentityVerificationState());
|
|
322
|
+
expect(read('{"identityVerification":"x"}')).toEqual(EmptyIdentityVerificationState());
|
|
323
|
+
expect(read('{"identityVerification":{"SendCount":"many","Pending":{"TokenHash":"x"},"Verified":{"Email":"a@b.c","Method":"sms"}}}')).toEqual({ SendCount: 0 });
|
|
324
|
+
expect(read('{"identityVerification":{"SendCount":-4}}')).toEqual({ SendCount: 0 });
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
describe('ComputeVerifiedDeadline', () => {
|
|
329
|
+
const start = NOW;
|
|
330
|
+
|
|
331
|
+
it('extends an existing deadline out to start + verifiedMaxSeconds', () => {
|
|
332
|
+
expect(
|
|
333
|
+
ComputeVerifiedDeadline({ CurrentDeadlineIso: '2026-10-02T12:05:00.000Z', SessionStartedAtMs: start, VerifiedMaxSeconds: 1800 }),
|
|
334
|
+
).toBe('2026-10-02T12:30:00.000Z');
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('never pulls a deadline in', () => {
|
|
338
|
+
expect(
|
|
339
|
+
ComputeVerifiedDeadline({ CurrentDeadlineIso: '2026-10-02T13:00:00.000Z', SessionStartedAtMs: start, VerifiedMaxSeconds: 1800 }),
|
|
340
|
+
).toBeUndefined();
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('leaves an uncapped session uncapped', () => {
|
|
344
|
+
expect(ComputeVerifiedDeadline({ CurrentDeadlineIso: undefined, SessionStartedAtMs: start, VerifiedMaxSeconds: 1800 })).toBeUndefined();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('does nothing without a verified cap', () => {
|
|
348
|
+
expect(ComputeVerifiedDeadline({ CurrentDeadlineIso: '2026-10-02T12:05:00.000Z', SessionStartedAtMs: start, VerifiedMaxSeconds: undefined })).toBeUndefined();
|
|
349
|
+
expect(ComputeVerifiedDeadline({ CurrentDeadlineIso: '2026-10-02T12:05:00.000Z', SessionStartedAtMs: start, VerifiedMaxSeconds: 0 })).toBeUndefined();
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('does nothing when the current deadline is unparseable', () => {
|
|
353
|
+
expect(ComputeVerifiedDeadline({ CurrentDeadlineIso: 'garbage', SessionStartedAtMs: start, VerifiedMaxSeconds: 1800 })).toBeUndefined();
|
|
354
|
+
});
|
|
355
|
+
});
|