@memberjunction/server 6.2.0-edge.2 → 6.2.0-edge.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/agentSessions/BridgeHostInstance.d.ts +25 -0
- package/dist/agentSessions/BridgeHostInstance.d.ts.map +1 -0
- package/dist/agentSessions/BridgeHostInstance.js +30 -0
- package/dist/agentSessions/BridgeHostInstance.js.map +1 -0
- package/dist/agentSessions/HostInstance.d.ts +75 -11
- package/dist/agentSessions/HostInstance.d.ts.map +1 -1
- package/dist/agentSessions/HostInstance.js +99 -28
- package/dist/agentSessions/HostInstance.js.map +1 -1
- package/dist/agentSessions/ReturningVisitorRecap.d.ts.map +1 -1
- package/dist/agentSessions/ReturningVisitorRecap.js +7 -3
- package/dist/agentSessions/ReturningVisitorRecap.js.map +1 -1
- package/dist/agentSessions/SessionJanitor.d.ts +173 -25
- package/dist/agentSessions/SessionJanitor.d.ts.map +1 -1
- package/dist/agentSessions/SessionJanitor.js +452 -54
- package/dist/agentSessions/SessionJanitor.js.map +1 -1
- package/dist/agentSessions/SessionManager.d.ts +43 -2
- package/dist/agentSessions/SessionManager.d.ts.map +1 -1
- package/dist/agentSessions/SessionManager.js +99 -8
- package/dist/agentSessions/SessionManager.js.map +1 -1
- package/dist/agentSessions/SessionRunIDVerifier.d.ts +63 -0
- package/dist/agentSessions/SessionRunIDVerifier.d.ts.map +1 -0
- package/dist/agentSessions/SessionRunIDVerifier.js +129 -0
- package/dist/agentSessions/SessionRunIDVerifier.js.map +1 -0
- package/dist/agentSessions/index.d.ts +2 -0
- package/dist/agentSessions/index.d.ts.map +1 -1
- package/dist/agentSessions/index.js +10 -3
- package/dist/agentSessions/index.js.map +1 -1
- package/dist/auth/actionAuthorization.d.ts +40 -0
- package/dist/auth/actionAuthorization.d.ts.map +1 -0
- package/dist/auth/actionAuthorization.js +94 -0
- package/dist/auth/actionAuthorization.js.map +1 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +3 -10
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.d.ts.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.js +6 -3
- package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
- package/dist/auth/newUsers.d.ts +10 -0
- package/dist/auth/newUsers.d.ts.map +1 -1
- package/dist/auth/newUsers.js +80 -11
- package/dist/auth/newUsers.js.map +1 -1
- package/dist/config.d.ts +4567 -71
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +246 -3
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +8 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +25 -34
- package/dist/context.js.map +1 -1
- package/dist/generated/graphql-schemas/__mj.d.ts +1073 -10
- package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
- package/dist/generated/graphql-schemas/__mj.js +9188 -3649
- package/dist/generated/graphql-schemas/__mj.js.map +1 -1
- package/dist/generic/OrphanedConversationDetailReconciler.js +3 -3
- package/dist/generic/OrphanedConversationDetailReconciler.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +62 -6
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +179 -35
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts +4 -0
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +37 -1
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +214 -159
- package/dist/index.js.map +1 -1
- package/dist/isolatedProvider.d.ts +20 -0
- package/dist/isolatedProvider.d.ts.map +1 -0
- package/dist/isolatedProvider.js +22 -0
- package/dist/isolatedProvider.js.map +1 -0
- package/dist/postgresqlCompat.d.ts +8 -0
- package/dist/postgresqlCompat.d.ts.map +1 -0
- package/dist/postgresqlCompat.js +19 -0
- package/dist/postgresqlCompat.js.map +1 -0
- package/dist/postgresqlPoolSettings.d.ts +108 -0
- package/dist/postgresqlPoolSettings.d.ts.map +1 -0
- package/dist/postgresqlPoolSettings.js +134 -0
- package/dist/postgresqlPoolSettings.js.map +1 -0
- package/dist/realtimeProxy/RealtimeProxyServer.d.ts +44 -4
- package/dist/realtimeProxy/RealtimeProxyServer.d.ts.map +1 -1
- package/dist/realtimeProxy/RealtimeProxyServer.js +121 -16
- package/dist/realtimeProxy/RealtimeProxyServer.js.map +1 -1
- package/dist/realtimeProxy/RealtimeRelayTunnel.d.ts +171 -0
- package/dist/realtimeProxy/RealtimeRelayTunnel.d.ts.map +1 -0
- package/dist/realtimeProxy/RealtimeRelayTunnel.js +532 -0
- package/dist/realtimeProxy/RealtimeRelayTunnel.js.map +1 -0
- package/dist/realtimeSessions/RealtimeSessionEventService.d.ts +148 -0
- package/dist/realtimeSessions/RealtimeSessionEventService.d.ts.map +1 -0
- package/dist/realtimeSessions/RealtimeSessionEventService.js +191 -0
- package/dist/realtimeSessions/RealtimeSessionEventService.js.map +1 -0
- package/dist/realtimeSessions/RealtimeSessionVerificationService.d.ts +66 -0
- package/dist/realtimeSessions/RealtimeSessionVerificationService.d.ts.map +1 -0
- package/dist/realtimeSessions/RealtimeSessionVerificationService.js +138 -0
- package/dist/realtimeSessions/RealtimeSessionVerificationService.js.map +1 -0
- package/dist/realtimeSessions/RealtimeVerifyRouter.d.ts +38 -0
- package/dist/realtimeSessions/RealtimeVerifyRouter.d.ts.map +1 -0
- package/dist/realtimeSessions/RealtimeVerifyRouter.js +98 -0
- package/dist/realtimeSessions/RealtimeVerifyRouter.js.map +1 -0
- package/dist/realtimeSessions/SessionVerificationPortsBase.d.ts +61 -0
- package/dist/realtimeSessions/SessionVerificationPortsBase.d.ts.map +1 -0
- package/dist/realtimeSessions/SessionVerificationPortsBase.js +149 -0
- package/dist/realtimeSessions/SessionVerificationPortsBase.js.map +1 -0
- package/dist/realtimeSessions/SlidingWindowRateLimiter.d.ts +57 -0
- package/dist/realtimeSessions/SlidingWindowRateLimiter.d.ts.map +1 -0
- package/dist/realtimeSessions/SlidingWindowRateLimiter.js +69 -0
- package/dist/realtimeSessions/SlidingWindowRateLimiter.js.map +1 -0
- package/dist/realtimeSessions/index.d.ts +17 -0
- package/dist/realtimeSessions/index.d.ts.map +1 -0
- package/dist/realtimeSessions/index.js +17 -0
- package/dist/realtimeSessions/index.js.map +1 -0
- package/dist/realtimeSessions/sessionAccess.d.ts +86 -0
- package/dist/realtimeSessions/sessionAccess.d.ts.map +1 -0
- package/dist/realtimeSessions/sessionAccess.js +90 -0
- package/dist/realtimeSessions/sessionAccess.js.map +1 -0
- package/dist/realtimeSessions/sessionConfigStamps.d.ts +70 -0
- package/dist/realtimeSessions/sessionConfigStamps.d.ts.map +1 -0
- package/dist/realtimeSessions/sessionConfigStamps.js +93 -0
- package/dist/realtimeSessions/sessionConfigStamps.js.map +1 -0
- package/dist/realtimeSessions/subscriptionAuthorization.d.ts +65 -0
- package/dist/realtimeSessions/subscriptionAuthorization.d.ts.map +1 -0
- package/dist/realtimeSessions/subscriptionAuthorization.js +71 -0
- package/dist/realtimeSessions/subscriptionAuthorization.js.map +1 -0
- package/dist/realtimeSessions/verificationCore.d.ts +243 -0
- package/dist/realtimeSessions/verificationCore.d.ts.map +1 -0
- package/dist/realtimeSessions/verificationCore.js +356 -0
- package/dist/realtimeSessions/verificationCore.js.map +1 -0
- package/dist/realtimeSessions/verificationPages.d.ts +66 -0
- package/dist/realtimeSessions/verificationPages.d.ts.map +1 -0
- package/dist/realtimeSessions/verificationPages.js +110 -0
- package/dist/realtimeSessions/verificationPages.js.map +1 -0
- package/dist/realtimeSessions/verificationPolicy.d.ts +147 -0
- package/dist/realtimeSessions/verificationPolicy.d.ts.map +1 -0
- package/dist/realtimeSessions/verificationPolicy.js +249 -0
- package/dist/realtimeSessions/verificationPolicy.js.map +1 -0
- package/dist/realtimeSessions/verificationWorkflow.d.ts +263 -0
- package/dist/realtimeSessions/verificationWorkflow.d.ts.map +1 -0
- package/dist/realtimeSessions/verificationWorkflow.js +435 -0
- package/dist/realtimeSessions/verificationWorkflow.js.map +1 -0
- package/dist/realtimeWidget/widgetGuestElevation.d.ts +20 -2
- package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -1
- package/dist/realtimeWidget/widgetGuestElevation.js +31 -2
- package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
- package/dist/resolvers/ActionResolver.d.ts +31 -5
- package/dist/resolvers/ActionResolver.d.ts.map +1 -1
- package/dist/resolvers/ActionResolver.js +75 -14
- package/dist/resolvers/ActionResolver.js.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.d.ts +35 -17
- package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.js +92 -166
- package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
- package/dist/resolvers/CacheStatsResolver.d.ts +27 -0
- package/dist/resolvers/CacheStatsResolver.d.ts.map +1 -1
- package/dist/resolvers/CacheStatsResolver.js +113 -2
- package/dist/resolvers/CacheStatsResolver.js.map +1 -1
- package/dist/resolvers/ClientToolRequestResolver.d.ts +4 -2
- package/dist/resolvers/ClientToolRequestResolver.d.ts.map +1 -1
- package/dist/resolvers/ClientToolRequestResolver.js +28 -10
- package/dist/resolvers/ClientToolRequestResolver.js.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.d.ts +7 -23
- package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.js +9 -47
- package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.d.ts +12 -1
- package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.js +83 -2
- package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
- package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataResolver.js +9 -0
- package/dist/resolvers/GetDataResolver.js.map +1 -1
- package/dist/resolvers/HumanHandoffResolver.d.ts +107 -0
- package/dist/resolvers/HumanHandoffResolver.d.ts.map +1 -0
- package/dist/resolvers/HumanHandoffResolver.js +330 -0
- package/dist/resolvers/HumanHandoffResolver.js.map +1 -0
- package/dist/resolvers/MCPResolver.d.ts.map +1 -1
- package/dist/resolvers/MCPResolver.js +9 -4
- package/dist/resolvers/MCPResolver.js.map +1 -1
- package/dist/resolvers/MeetingResolver.d.ts +69 -0
- package/dist/resolvers/MeetingResolver.d.ts.map +1 -0
- package/dist/resolvers/MeetingResolver.js +381 -0
- package/dist/resolvers/MeetingResolver.js.map +1 -0
- package/dist/resolvers/RealtimeBridgeResolver.d.ts +88 -3
- package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeBridgeResolver.js +352 -12
- package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +156 -108
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +395 -293
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/RealtimeSessionEventsResolver.d.ts +61 -0
- package/dist/resolvers/RealtimeSessionEventsResolver.d.ts.map +1 -0
- package/dist/resolvers/RealtimeSessionEventsResolver.js +130 -0
- package/dist/resolvers/RealtimeSessionEventsResolver.js.map +1 -0
- package/dist/resolvers/RealtimeSessionVerificationResolver.d.ts +85 -0
- package/dist/resolvers/RealtimeSessionVerificationResolver.d.ts.map +1 -0
- package/dist/resolvers/RealtimeSessionVerificationResolver.js +188 -0
- package/dist/resolvers/RealtimeSessionVerificationResolver.js.map +1 -0
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts +2 -0
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
- package/dist/resolvers/RemoteBrowserActionResolver.js +5 -1
- package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts +10 -0
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +68 -11
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
- package/dist/resolvers/RunTemplateResolver.js +12 -11
- package/dist/resolvers/RunTemplateResolver.js.map +1 -1
- package/dist/resolvers/SyncDataResolver.d.ts.map +1 -1
- package/dist/resolvers/SyncDataResolver.js +6 -2
- package/dist/resolvers/SyncDataResolver.js.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.d.ts +2 -0
- package/dist/resolvers/SyncRolesUsersResolver.d.ts.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.js +23 -6
- package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
- package/dist/resolvers/UserAvatarResolver.d.ts +16 -0
- package/dist/resolvers/UserAvatarResolver.d.ts.map +1 -0
- package/dist/resolvers/UserAvatarResolver.js +124 -0
- package/dist/resolvers/UserAvatarResolver.js.map +1 -0
- package/dist/resolvers/UserResolver.d.ts.map +1 -1
- package/dist/resolvers/UserResolver.js +2 -17
- package/dist/resolvers/UserResolver.js.map +1 -1
- package/dist/resolvers/adhocTimeout.d.ts +7 -0
- package/dist/resolvers/adhocTimeout.d.ts.map +1 -0
- package/dist/resolvers/adhocTimeout.js +12 -0
- package/dist/resolvers/adhocTimeout.js.map +1 -0
- package/dist/resolvers/agentVisionConsentAudit.d.ts +37 -0
- package/dist/resolvers/agentVisionConsentAudit.d.ts.map +1 -0
- package/dist/resolvers/agentVisionConsentAudit.js +63 -0
- package/dist/resolvers/agentVisionConsentAudit.js.map +1 -0
- package/dist/resolvers/avatarInputValidation.d.ts +40 -0
- package/dist/resolvers/avatarInputValidation.d.ts.map +1 -0
- package/dist/resolvers/avatarInputValidation.js +97 -0
- package/dist/resolvers/avatarInputValidation.js.map +1 -0
- package/dist/resolvers/currentUserRoles.d.ts +29 -0
- package/dist/resolvers/currentUserRoles.d.ts.map +1 -1
- package/dist/resolvers/currentUserRoles.js +30 -0
- package/dist/resolvers/currentUserRoles.js.map +1 -1
- package/dist/resolvers/meetings/index.d.ts +5 -0
- package/dist/resolvers/meetings/index.d.ts.map +1 -0
- package/dist/resolvers/meetings/index.js +5 -0
- package/dist/resolvers/meetings/index.js.map +1 -0
- package/dist/resolvers/meetings/meetingHelpers.d.ts +17 -0
- package/dist/resolvers/meetings/meetingHelpers.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingHelpers.js +212 -0
- package/dist/resolvers/meetings/meetingHelpers.js.map +1 -0
- package/dist/resolvers/meetings/meetingMutations.d.ts +18 -0
- package/dist/resolvers/meetings/meetingMutations.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingMutations.js +439 -0
- package/dist/resolvers/meetings/meetingMutations.js.map +1 -0
- package/dist/resolvers/meetings/meetingQueries.d.ts +7 -0
- package/dist/resolvers/meetings/meetingQueries.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingQueries.js +113 -0
- package/dist/resolvers/meetings/meetingQueries.js.map +1 -0
- package/dist/resolvers/meetings/meetingTypes.d.ts +89 -0
- package/dist/resolvers/meetings/meetingTypes.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingTypes.js +345 -0
- package/dist/resolvers/meetings/meetingTypes.js.map +1 -0
- package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
- package/dist/rest/ViewOperationsHandler.js +8 -5
- package/dist/rest/ViewOperationsHandler.js.map +1 -1
- package/dist/services/TaskGraphActionRunner.d.ts +8 -0
- package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphActionRunner.js +27 -1
- package/dist/services/TaskGraphActionRunner.js.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.d.ts +4 -3
- package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.js +13 -7
- package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
- package/dist/services/WorkQueueHostService.d.ts +36 -0
- package/dist/services/WorkQueueHostService.d.ts.map +1 -0
- package/dist/services/WorkQueueHostService.js +97 -0
- package/dist/services/WorkQueueHostService.js.map +1 -0
- package/dist/services/workQueueConfig.d.ts +103 -0
- package/dist/services/workQueueConfig.d.ts.map +1 -0
- package/dist/services/workQueueConfig.js +48 -0
- package/dist/services/workQueueConfig.js.map +1 -0
- package/dist/sharedCache.d.ts +105 -0
- package/dist/sharedCache.d.ts.map +1 -0
- package/dist/sharedCache.js +276 -0
- package/dist/sharedCache.js.map +1 -0
- package/dist/types.d.ts +11 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +97 -94
- package/src/__tests__/ActionResolver.authorization.test.ts +353 -0
- package/src/__tests__/AdhocQueryResolver.bugs.test.ts +14 -35
- package/src/__tests__/AdhocQueryResolver.pagination.test.ts +6 -2
- package/src/__tests__/AdhocQueryResolver.scopeGuard.test.ts +3 -3
- package/src/__tests__/AdhocQueryResolver.sqlAuthorizer.test.ts +104 -0
- package/src/__tests__/BridgeHostInstance.test.ts +232 -0
- package/src/__tests__/ClientToolRequestResolver.test.ts +116 -0
- package/src/__tests__/EntityRecordNameResolver.fieldSecurity.test.ts +27 -72
- package/src/__tests__/FetchEntityVectorsResolver.test.ts +285 -0
- package/src/__tests__/HostInstance.test.ts +84 -0
- package/src/__tests__/HumanHandoffResolver.test.ts +250 -0
- package/src/__tests__/MeetingResolver.test.ts +448 -0
- package/src/__tests__/RealtimeBridgeResolver.test.ts +651 -4
- package/src/__tests__/RealtimeClientSessionResolver.configTamper.test.ts +383 -0
- package/src/__tests__/RealtimeClientSessionResolver.test.ts +682 -58
- package/src/__tests__/RealtimeProxyServer.test.ts +16 -1
- package/src/__tests__/RealtimeRelay.test.ts +876 -0
- package/src/__tests__/RemoteBrowserActionResolver.goalRunLink.test.ts +129 -0
- package/src/__tests__/ResolverBase.adhocReadScope.test.ts +96 -0
- package/src/__tests__/ResolverBase.aggregateScreen.test.ts +174 -0
- package/src/__tests__/ResolverBase.clientOldValues.test.ts +6 -3
- package/src/__tests__/ResolverBase.filterEscaping.test.ts +21 -2
- package/src/__tests__/ResolverBase.includeBinaryFields.test.ts +291 -0
- package/src/__tests__/ReturningVisitorRecap.test.ts +115 -0
- package/src/__tests__/RunAIAgentResolver.branch.test.ts +169 -0
- package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +4 -4
- package/src/__tests__/RunAIAgentResolver.stoppedPredecessor.test.ts +166 -0
- package/src/__tests__/RunTemplateResolver.test.ts +132 -0
- package/src/__tests__/RunViewResolver.cacheCheckForwarding.test.ts +63 -0
- package/src/__tests__/SessionJanitor.test.ts +845 -18
- package/src/__tests__/SessionManager.test.ts +213 -17
- package/src/__tests__/SessionRunIDVerifier.test.ts +251 -0
- package/src/__tests__/SyncRolesUsersResolver.userRoles.test.ts +97 -0
- package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +41 -0
- package/src/__tests__/TaskGraphContinuationDeliverer.test.ts +50 -3
- package/src/__tests__/UserAvatarResolver.test.ts +255 -0
- package/src/__tests__/UserResolver.currentUser.test.ts +16 -1
- package/src/__tests__/WorkQueueHostService.test.ts +133 -0
- package/src/__tests__/actionAuthorization.test.ts +130 -0
- package/src/__tests__/adhocTimeoutClamp.test.ts +26 -0
- package/src/__tests__/agentVisionConsentAudit.test.ts +118 -0
- package/src/__tests__/avatarInputValidation.test.ts +160 -0
- package/src/__tests__/isolatedProviderTransactions.test.ts +169 -0
- package/src/__tests__/magicLinkService.consume.test.ts +6 -0
- package/src/__tests__/magicLinkService.provisionLog.test.ts +5 -0
- package/src/__tests__/newUsers.template.test.ts +151 -0
- package/src/__tests__/newUsers.test.ts +74 -5
- package/src/__tests__/orphanedConversationDetailReconciler.test.ts +17 -2
- package/src/__tests__/postgresqlPoolSettings.test.ts +131 -0
- package/src/__tests__/realtimeSessions/RealtimeSessionEventService.test.ts +201 -0
- package/src/__tests__/realtimeSessions/RealtimeSessionEventsResolver.test.ts +215 -0
- package/src/__tests__/realtimeSessions/RealtimeSessionVerificationResolver.test.ts +102 -0
- package/src/__tests__/realtimeSessions/RealtimeSessionVerificationService.test.ts +349 -0
- package/src/__tests__/realtimeSessions/RealtimeVerifyRouter.test.ts +131 -0
- package/src/__tests__/realtimeSessions/SlidingWindowRateLimiter.test.ts +56 -0
- package/src/__tests__/realtimeSessions/helpers.ts +4 -0
- package/src/__tests__/realtimeSessions/sessionAccess.test.ts +129 -0
- package/src/__tests__/realtimeSessions/sessionConfigStamps.test.ts +124 -0
- package/src/__tests__/realtimeSessions/verificationCore.test.ts +355 -0
- package/src/__tests__/realtimeSessions/verificationPages.test.ts +84 -0
- package/src/__tests__/realtimeSessions/verificationPolicy.test.ts +226 -0
- package/src/__tests__/realtimeSessions/verificationWorkflow.test.ts +633 -0
- package/src/__tests__/resolverBase.fls.test.ts +87 -4
- package/src/__tests__/serverExtensionReservedRoots.test.ts +2 -0
- package/src/__tests__/sharedCache.test.ts +310 -0
- package/src/__tests__/translateBracketsToPG.test.ts +20 -0
- package/src/__tests__/widgetGuestElevation.test.ts +44 -0
- package/src/agentSessions/BridgeHostInstance.ts +32 -0
- package/src/agentSessions/HostInstance.ts +124 -28
- package/src/agentSessions/ReturningVisitorRecap.ts +7 -3
- package/src/agentSessions/SessionJanitor.ts +543 -57
- package/src/agentSessions/SessionManager.ts +125 -25
- package/src/agentSessions/SessionRunIDVerifier.ts +171 -0
- package/src/agentSessions/index.ts +10 -3
- package/src/auth/actionAuthorization.ts +116 -0
- package/src/auth/index.ts +3 -8
- package/src/auth/magicLink/MagicLinkService.ts +6 -3
- package/src/auth/newUsers.ts +99 -11
- package/src/config.ts +255 -3
- package/src/context.ts +26 -34
- package/src/generated/graphql-schemas/__mj.ts +22701 -18838
- package/src/generic/OrphanedConversationDetailReconciler.ts +3 -3
- package/src/generic/ResolverBase.ts +210 -36
- package/src/generic/RunViewResolver.ts +37 -2
- package/src/index.ts +236 -170
- package/src/isolatedProvider.ts +22 -0
- package/src/postgresqlCompat.ts +21 -0
- package/src/postgresqlPoolSettings.ts +213 -0
- package/src/realtimeProxy/RealtimeProxyServer.ts +148 -16
- package/src/realtimeProxy/RealtimeRelayTunnel.ts +645 -0
- package/src/realtimeSessions/README.md +179 -0
- package/src/realtimeSessions/RealtimeSessionEventService.ts +274 -0
- package/src/realtimeSessions/RealtimeSessionVerificationService.ts +167 -0
- package/src/realtimeSessions/RealtimeVerifyRouter.ts +108 -0
- package/src/realtimeSessions/SessionVerificationPortsBase.ts +189 -0
- package/src/realtimeSessions/SlidingWindowRateLimiter.ts +92 -0
- package/src/realtimeSessions/index.ts +16 -0
- package/src/realtimeSessions/sessionAccess.ts +141 -0
- package/src/realtimeSessions/sessionConfigStamps.ts +123 -0
- package/src/realtimeSessions/subscriptionAuthorization.ts +116 -0
- package/src/realtimeSessions/verificationCore.ts +502 -0
- package/src/realtimeSessions/verificationPages.ts +149 -0
- package/src/realtimeSessions/verificationPolicy.ts +321 -0
- package/src/realtimeSessions/verificationWorkflow.ts +702 -0
- package/src/realtimeWidget/widgetGuestElevation.ts +34 -2
- package/src/resolvers/ActionResolver.ts +84 -21
- package/src/resolvers/AdhocQueryResolver.ts +105 -187
- package/src/resolvers/CacheStatsResolver.ts +89 -2
- package/src/resolvers/ClientToolRequestResolver.ts +25 -7
- package/src/resolvers/EntityRecordNameResolver.ts +9 -51
- package/src/resolvers/FetchEntityVectorsResolver.ts +97 -2
- package/src/resolvers/GetDataResolver.ts +10 -0
- package/src/resolvers/HumanHandoffResolver.ts +312 -0
- package/src/resolvers/MCPResolver.ts +8 -5
- package/src/resolvers/MeetingResolver.ts +438 -0
- package/src/resolvers/RealtimeBridgeResolver.ts +385 -19
- package/src/resolvers/RealtimeClientSessionResolver.ts +503 -344
- package/src/resolvers/RealtimeSessionEventsResolver.ts +133 -0
- package/src/resolvers/RealtimeSessionVerificationResolver.ts +174 -0
- package/src/resolvers/RemoteBrowserActionResolver.ts +11 -1
- package/src/resolvers/RunAIAgentResolver.ts +78 -11
- package/src/resolvers/RunTemplateResolver.ts +13 -12
- package/src/resolvers/SyncDataResolver.ts +6 -2
- package/src/resolvers/SyncRolesUsersResolver.ts +24 -6
- package/src/resolvers/UserAvatarResolver.ts +111 -0
- package/src/resolvers/UserResolver.ts +2 -16
- package/src/resolvers/adhocTimeout.ts +12 -0
- package/src/resolvers/agentVisionConsentAudit.ts +82 -0
- package/src/resolvers/avatarInputValidation.ts +123 -0
- package/src/resolvers/currentUserRoles.ts +32 -1
- package/src/resolvers/meetings/index.ts +4 -0
- package/src/resolvers/meetings/meetingHelpers.ts +307 -0
- package/src/resolvers/meetings/meetingMutations.ts +556 -0
- package/src/resolvers/meetings/meetingQueries.ts +172 -0
- package/src/resolvers/meetings/meetingTypes.ts +238 -0
- package/src/rest/ViewOperationsHandler.ts +8 -5
- package/src/services/TaskGraphActionRunner.ts +29 -1
- package/src/services/TaskGraphContinuationDeliverer.ts +13 -7
- package/src/services/WorkQueueHostService.ts +129 -0
- package/src/services/workQueueConfig.ts +52 -0
- package/src/sharedCache.ts +302 -0
- package/src/types.ts +12 -0
- package/dist/resolvers/adhoc-query-helpers.d.ts +0 -33
- package/dist/resolvers/adhoc-query-helpers.d.ts.map +0 -1
- package/dist/resolvers/adhoc-query-helpers.js +0 -50
- package/dist/resolvers/adhoc-query-helpers.js.map +0 -1
- package/src/__tests__/adhoc-query-helpers.test.ts +0 -50
- package/src/resolvers/adhoc-query-helpers.ts +0 -67
|
@@ -0,0 +1,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
|
+
});
|