@memberjunction/server 6.2.0-edge.3 → 6.2.0-edge.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/agentSessions/BridgeHostInstance.d.ts +25 -0
- package/dist/agentSessions/BridgeHostInstance.d.ts.map +1 -0
- package/dist/agentSessions/BridgeHostInstance.js +30 -0
- package/dist/agentSessions/BridgeHostInstance.js.map +1 -0
- package/dist/agentSessions/HostInstance.d.ts +75 -11
- package/dist/agentSessions/HostInstance.d.ts.map +1 -1
- package/dist/agentSessions/HostInstance.js +99 -28
- package/dist/agentSessions/HostInstance.js.map +1 -1
- package/dist/agentSessions/ReturningVisitorRecap.d.ts.map +1 -1
- package/dist/agentSessions/ReturningVisitorRecap.js +7 -3
- package/dist/agentSessions/ReturningVisitorRecap.js.map +1 -1
- package/dist/agentSessions/SessionJanitor.d.ts +173 -25
- package/dist/agentSessions/SessionJanitor.d.ts.map +1 -1
- package/dist/agentSessions/SessionJanitor.js +452 -54
- package/dist/agentSessions/SessionJanitor.js.map +1 -1
- package/dist/agentSessions/SessionManager.d.ts +43 -2
- package/dist/agentSessions/SessionManager.d.ts.map +1 -1
- package/dist/agentSessions/SessionManager.js +99 -8
- package/dist/agentSessions/SessionManager.js.map +1 -1
- package/dist/agentSessions/SessionRunIDVerifier.d.ts +63 -0
- package/dist/agentSessions/SessionRunIDVerifier.d.ts.map +1 -0
- package/dist/agentSessions/SessionRunIDVerifier.js +129 -0
- package/dist/agentSessions/SessionRunIDVerifier.js.map +1 -0
- package/dist/agentSessions/index.d.ts +2 -0
- package/dist/agentSessions/index.d.ts.map +1 -1
- package/dist/agentSessions/index.js +10 -3
- package/dist/agentSessions/index.js.map +1 -1
- package/dist/auth/actionAuthorization.d.ts +40 -0
- package/dist/auth/actionAuthorization.d.ts.map +1 -0
- package/dist/auth/actionAuthorization.js +94 -0
- package/dist/auth/actionAuthorization.js.map +1 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +3 -10
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.d.ts.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.js +6 -3
- package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
- package/dist/auth/newUsers.d.ts +10 -0
- package/dist/auth/newUsers.d.ts.map +1 -1
- package/dist/auth/newUsers.js +80 -11
- package/dist/auth/newUsers.js.map +1 -1
- package/dist/config.d.ts +2378 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +123 -0
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +8 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +14 -11
- package/dist/context.js.map +1 -1
- package/dist/generated/graphql-schemas/__mj.d.ts +135 -0
- package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
- package/dist/generated/graphql-schemas/__mj.js +706 -0
- package/dist/generated/graphql-schemas/__mj.js.map +1 -1
- package/dist/generic/OrphanedConversationDetailReconciler.js +3 -3
- package/dist/generic/OrphanedConversationDetailReconciler.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +20 -4
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +39 -10
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +4 -0
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +120 -131
- package/dist/index.js.map +1 -1
- package/dist/isolatedProvider.d.ts +20 -0
- package/dist/isolatedProvider.d.ts.map +1 -0
- package/dist/isolatedProvider.js +22 -0
- package/dist/isolatedProvider.js.map +1 -0
- package/dist/realtimeProxy/RealtimeProxyServer.d.ts +44 -4
- package/dist/realtimeProxy/RealtimeProxyServer.d.ts.map +1 -1
- package/dist/realtimeProxy/RealtimeProxyServer.js +121 -16
- package/dist/realtimeProxy/RealtimeProxyServer.js.map +1 -1
- package/dist/realtimeProxy/RealtimeRelayTunnel.d.ts +171 -0
- package/dist/realtimeProxy/RealtimeRelayTunnel.d.ts.map +1 -0
- package/dist/realtimeProxy/RealtimeRelayTunnel.js +532 -0
- package/dist/realtimeProxy/RealtimeRelayTunnel.js.map +1 -0
- package/dist/realtimeSessions/RealtimeSessionEventService.d.ts +148 -0
- package/dist/realtimeSessions/RealtimeSessionEventService.d.ts.map +1 -0
- package/dist/realtimeSessions/RealtimeSessionEventService.js +191 -0
- package/dist/realtimeSessions/RealtimeSessionEventService.js.map +1 -0
- package/dist/realtimeSessions/RealtimeSessionVerificationService.d.ts +66 -0
- package/dist/realtimeSessions/RealtimeSessionVerificationService.d.ts.map +1 -0
- package/dist/realtimeSessions/RealtimeSessionVerificationService.js +138 -0
- package/dist/realtimeSessions/RealtimeSessionVerificationService.js.map +1 -0
- package/dist/realtimeSessions/RealtimeVerifyRouter.d.ts +38 -0
- package/dist/realtimeSessions/RealtimeVerifyRouter.d.ts.map +1 -0
- package/dist/realtimeSessions/RealtimeVerifyRouter.js +98 -0
- package/dist/realtimeSessions/RealtimeVerifyRouter.js.map +1 -0
- package/dist/realtimeSessions/SessionVerificationPortsBase.d.ts +61 -0
- package/dist/realtimeSessions/SessionVerificationPortsBase.d.ts.map +1 -0
- package/dist/realtimeSessions/SessionVerificationPortsBase.js +149 -0
- package/dist/realtimeSessions/SessionVerificationPortsBase.js.map +1 -0
- package/dist/realtimeSessions/SlidingWindowRateLimiter.d.ts +57 -0
- package/dist/realtimeSessions/SlidingWindowRateLimiter.d.ts.map +1 -0
- package/dist/realtimeSessions/SlidingWindowRateLimiter.js +69 -0
- package/dist/realtimeSessions/SlidingWindowRateLimiter.js.map +1 -0
- package/dist/realtimeSessions/index.d.ts +17 -0
- package/dist/realtimeSessions/index.d.ts.map +1 -0
- package/dist/realtimeSessions/index.js +17 -0
- package/dist/realtimeSessions/index.js.map +1 -0
- package/dist/realtimeSessions/sessionAccess.d.ts +86 -0
- package/dist/realtimeSessions/sessionAccess.d.ts.map +1 -0
- package/dist/realtimeSessions/sessionAccess.js +90 -0
- package/dist/realtimeSessions/sessionAccess.js.map +1 -0
- package/dist/realtimeSessions/sessionConfigStamps.d.ts +70 -0
- package/dist/realtimeSessions/sessionConfigStamps.d.ts.map +1 -0
- package/dist/realtimeSessions/sessionConfigStamps.js +93 -0
- package/dist/realtimeSessions/sessionConfigStamps.js.map +1 -0
- package/dist/realtimeSessions/subscriptionAuthorization.d.ts +65 -0
- package/dist/realtimeSessions/subscriptionAuthorization.d.ts.map +1 -0
- package/dist/realtimeSessions/subscriptionAuthorization.js +71 -0
- package/dist/realtimeSessions/subscriptionAuthorization.js.map +1 -0
- package/dist/realtimeSessions/verificationCore.d.ts +243 -0
- package/dist/realtimeSessions/verificationCore.d.ts.map +1 -0
- package/dist/realtimeSessions/verificationCore.js +356 -0
- package/dist/realtimeSessions/verificationCore.js.map +1 -0
- package/dist/realtimeSessions/verificationPages.d.ts +66 -0
- package/dist/realtimeSessions/verificationPages.d.ts.map +1 -0
- package/dist/realtimeSessions/verificationPages.js +110 -0
- package/dist/realtimeSessions/verificationPages.js.map +1 -0
- package/dist/realtimeSessions/verificationPolicy.d.ts +147 -0
- package/dist/realtimeSessions/verificationPolicy.d.ts.map +1 -0
- package/dist/realtimeSessions/verificationPolicy.js +249 -0
- package/dist/realtimeSessions/verificationPolicy.js.map +1 -0
- package/dist/realtimeSessions/verificationWorkflow.d.ts +263 -0
- package/dist/realtimeSessions/verificationWorkflow.d.ts.map +1 -0
- package/dist/realtimeSessions/verificationWorkflow.js +435 -0
- package/dist/realtimeSessions/verificationWorkflow.js.map +1 -0
- package/dist/realtimeWidget/widgetGuestElevation.d.ts +20 -2
- package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -1
- package/dist/realtimeWidget/widgetGuestElevation.js +31 -2
- package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
- package/dist/resolvers/ActionResolver.d.ts +31 -5
- package/dist/resolvers/ActionResolver.d.ts.map +1 -1
- package/dist/resolvers/ActionResolver.js +75 -14
- package/dist/resolvers/ActionResolver.js.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.d.ts +14 -1
- package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.js +21 -5
- package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
- package/dist/resolvers/CacheStatsResolver.d.ts +27 -0
- package/dist/resolvers/CacheStatsResolver.d.ts.map +1 -1
- package/dist/resolvers/CacheStatsResolver.js +113 -2
- package/dist/resolvers/CacheStatsResolver.js.map +1 -1
- package/dist/resolvers/ClientToolRequestResolver.d.ts +4 -2
- package/dist/resolvers/ClientToolRequestResolver.d.ts.map +1 -1
- package/dist/resolvers/ClientToolRequestResolver.js +28 -10
- package/dist/resolvers/ClientToolRequestResolver.js.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.d.ts +12 -1
- package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.js +83 -2
- package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
- package/dist/resolvers/HumanHandoffResolver.d.ts.map +1 -1
- package/dist/resolvers/HumanHandoffResolver.js +7 -1
- package/dist/resolvers/HumanHandoffResolver.js.map +1 -1
- package/dist/resolvers/MCPResolver.d.ts.map +1 -1
- package/dist/resolvers/MCPResolver.js +9 -4
- package/dist/resolvers/MCPResolver.js.map +1 -1
- package/dist/resolvers/RealtimeBridgeResolver.d.ts +54 -2
- package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeBridgeResolver.js +155 -10
- package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +155 -25
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +394 -90
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/RealtimeSessionEventsResolver.d.ts +61 -0
- package/dist/resolvers/RealtimeSessionEventsResolver.d.ts.map +1 -0
- package/dist/resolvers/RealtimeSessionEventsResolver.js +130 -0
- package/dist/resolvers/RealtimeSessionEventsResolver.js.map +1 -0
- package/dist/resolvers/RealtimeSessionVerificationResolver.d.ts +85 -0
- package/dist/resolvers/RealtimeSessionVerificationResolver.d.ts.map +1 -0
- package/dist/resolvers/RealtimeSessionVerificationResolver.js +188 -0
- package/dist/resolvers/RealtimeSessionVerificationResolver.js.map +1 -0
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts +2 -0
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
- package/dist/resolvers/RemoteBrowserActionResolver.js +5 -1
- package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts +10 -0
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +68 -11
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
- package/dist/resolvers/RunTemplateResolver.js +12 -11
- package/dist/resolvers/RunTemplateResolver.js.map +1 -1
- package/dist/resolvers/SyncDataResolver.d.ts.map +1 -1
- package/dist/resolvers/SyncDataResolver.js +6 -2
- package/dist/resolvers/SyncDataResolver.js.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.d.ts +2 -0
- package/dist/resolvers/SyncRolesUsersResolver.d.ts.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.js +23 -6
- package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
- package/dist/resolvers/UserAvatarResolver.d.ts +16 -0
- package/dist/resolvers/UserAvatarResolver.d.ts.map +1 -0
- package/dist/resolvers/UserAvatarResolver.js +124 -0
- package/dist/resolvers/UserAvatarResolver.js.map +1 -0
- package/dist/resolvers/UserResolver.d.ts.map +1 -1
- package/dist/resolvers/UserResolver.js +2 -17
- package/dist/resolvers/UserResolver.js.map +1 -1
- package/dist/resolvers/agentVisionConsentAudit.d.ts +37 -0
- package/dist/resolvers/agentVisionConsentAudit.d.ts.map +1 -0
- package/dist/resolvers/agentVisionConsentAudit.js +63 -0
- package/dist/resolvers/agentVisionConsentAudit.js.map +1 -0
- package/dist/resolvers/avatarInputValidation.d.ts +40 -0
- package/dist/resolvers/avatarInputValidation.d.ts.map +1 -0
- package/dist/resolvers/avatarInputValidation.js +97 -0
- package/dist/resolvers/avatarInputValidation.js.map +1 -0
- package/dist/resolvers/currentUserRoles.d.ts +29 -0
- package/dist/resolvers/currentUserRoles.d.ts.map +1 -1
- package/dist/resolvers/currentUserRoles.js +30 -0
- package/dist/resolvers/currentUserRoles.js.map +1 -1
- package/dist/resolvers/meetings/meetingMutations.d.ts +6 -0
- package/dist/resolvers/meetings/meetingMutations.d.ts.map +1 -1
- package/dist/resolvers/meetings/meetingMutations.js +21 -0
- package/dist/resolvers/meetings/meetingMutations.js.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.d.ts +4 -3
- package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.js +13 -7
- package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
- package/dist/sharedCache.d.ts +105 -0
- package/dist/sharedCache.d.ts.map +1 -0
- package/dist/sharedCache.js +276 -0
- package/dist/sharedCache.js.map +1 -0
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +97 -95
- package/src/__tests__/ActionResolver.authorization.test.ts +353 -0
- package/src/__tests__/AdhocQueryResolver.sqlAuthorizer.test.ts +104 -0
- package/src/__tests__/BridgeHostInstance.test.ts +232 -0
- package/src/__tests__/ClientToolRequestResolver.test.ts +116 -0
- package/src/__tests__/FetchEntityVectorsResolver.test.ts +285 -0
- package/src/__tests__/HostInstance.test.ts +84 -0
- package/src/__tests__/HumanHandoffResolver.test.ts +17 -0
- package/src/__tests__/MeetingResolver.test.ts +75 -1
- package/src/__tests__/RealtimeBridgeResolver.test.ts +415 -6
- package/src/__tests__/RealtimeClientSessionResolver.configTamper.test.ts +383 -0
- package/src/__tests__/RealtimeClientSessionResolver.test.ts +682 -58
- package/src/__tests__/RealtimeProxyServer.test.ts +16 -1
- package/src/__tests__/RealtimeRelay.test.ts +876 -0
- package/src/__tests__/RemoteBrowserActionResolver.goalRunLink.test.ts +129 -0
- package/src/__tests__/ResolverBase.aggregateScreen.test.ts +174 -0
- package/src/__tests__/ResolverBase.clientOldValues.test.ts +6 -3
- package/src/__tests__/ReturningVisitorRecap.test.ts +115 -0
- package/src/__tests__/RunAIAgentResolver.branch.test.ts +169 -0
- package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +4 -4
- package/src/__tests__/RunAIAgentResolver.stoppedPredecessor.test.ts +166 -0
- package/src/__tests__/RunTemplateResolver.test.ts +132 -0
- package/src/__tests__/RunViewResolver.cacheCheckForwarding.test.ts +63 -0
- package/src/__tests__/SessionJanitor.test.ts +845 -18
- package/src/__tests__/SessionManager.test.ts +213 -17
- package/src/__tests__/SessionRunIDVerifier.test.ts +251 -0
- package/src/__tests__/SyncRolesUsersResolver.userRoles.test.ts +97 -0
- package/src/__tests__/TaskGraphContinuationDeliverer.test.ts +50 -3
- package/src/__tests__/UserAvatarResolver.test.ts +255 -0
- package/src/__tests__/UserResolver.currentUser.test.ts +16 -1
- package/src/__tests__/actionAuthorization.test.ts +130 -0
- package/src/__tests__/agentVisionConsentAudit.test.ts +118 -0
- package/src/__tests__/avatarInputValidation.test.ts +160 -0
- package/src/__tests__/isolatedProviderTransactions.test.ts +169 -0
- package/src/__tests__/magicLinkService.consume.test.ts +6 -0
- package/src/__tests__/magicLinkService.provisionLog.test.ts +5 -0
- package/src/__tests__/newUsers.template.test.ts +151 -0
- package/src/__tests__/newUsers.test.ts +74 -5
- package/src/__tests__/orphanedConversationDetailReconciler.test.ts +17 -2
- package/src/__tests__/realtimeSessions/RealtimeSessionEventService.test.ts +201 -0
- package/src/__tests__/realtimeSessions/RealtimeSessionEventsResolver.test.ts +215 -0
- package/src/__tests__/realtimeSessions/RealtimeSessionVerificationResolver.test.ts +102 -0
- package/src/__tests__/realtimeSessions/RealtimeSessionVerificationService.test.ts +349 -0
- package/src/__tests__/realtimeSessions/RealtimeVerifyRouter.test.ts +131 -0
- package/src/__tests__/realtimeSessions/SlidingWindowRateLimiter.test.ts +56 -0
- package/src/__tests__/realtimeSessions/helpers.ts +4 -0
- package/src/__tests__/realtimeSessions/sessionAccess.test.ts +129 -0
- package/src/__tests__/realtimeSessions/sessionConfigStamps.test.ts +124 -0
- package/src/__tests__/realtimeSessions/verificationCore.test.ts +355 -0
- package/src/__tests__/realtimeSessions/verificationPages.test.ts +84 -0
- package/src/__tests__/realtimeSessions/verificationPolicy.test.ts +226 -0
- package/src/__tests__/realtimeSessions/verificationWorkflow.test.ts +633 -0
- package/src/__tests__/resolverBase.fls.test.ts +87 -4
- package/src/__tests__/serverExtensionReservedRoots.test.ts +2 -0
- package/src/__tests__/sharedCache.test.ts +310 -0
- package/src/__tests__/widgetGuestElevation.test.ts +44 -0
- package/src/agentSessions/BridgeHostInstance.ts +32 -0
- package/src/agentSessions/HostInstance.ts +124 -28
- package/src/agentSessions/ReturningVisitorRecap.ts +7 -3
- package/src/agentSessions/SessionJanitor.ts +543 -57
- package/src/agentSessions/SessionManager.ts +125 -25
- package/src/agentSessions/SessionRunIDVerifier.ts +171 -0
- package/src/agentSessions/index.ts +10 -3
- package/src/auth/actionAuthorization.ts +116 -0
- package/src/auth/index.ts +3 -8
- package/src/auth/magicLink/MagicLinkService.ts +6 -3
- package/src/auth/newUsers.ts +99 -11
- package/src/config.ts +125 -0
- package/src/context.ts +15 -10
- package/src/generated/graphql-schemas/__mj.ts +496 -1
- package/src/generic/OrphanedConversationDetailReconciler.ts +3 -3
- package/src/generic/ResolverBase.ts +39 -9
- package/src/generic/RunViewResolver.ts +4 -0
- package/src/index.ts +132 -143
- package/src/isolatedProvider.ts +22 -0
- package/src/realtimeProxy/RealtimeProxyServer.ts +148 -16
- package/src/realtimeProxy/RealtimeRelayTunnel.ts +645 -0
- package/src/realtimeSessions/README.md +179 -0
- package/src/realtimeSessions/RealtimeSessionEventService.ts +274 -0
- package/src/realtimeSessions/RealtimeSessionVerificationService.ts +167 -0
- package/src/realtimeSessions/RealtimeVerifyRouter.ts +108 -0
- package/src/realtimeSessions/SessionVerificationPortsBase.ts +189 -0
- package/src/realtimeSessions/SlidingWindowRateLimiter.ts +92 -0
- package/src/realtimeSessions/index.ts +16 -0
- package/src/realtimeSessions/sessionAccess.ts +141 -0
- package/src/realtimeSessions/sessionConfigStamps.ts +123 -0
- package/src/realtimeSessions/subscriptionAuthorization.ts +116 -0
- package/src/realtimeSessions/verificationCore.ts +502 -0
- package/src/realtimeSessions/verificationPages.ts +149 -0
- package/src/realtimeSessions/verificationPolicy.ts +321 -0
- package/src/realtimeSessions/verificationWorkflow.ts +702 -0
- package/src/realtimeWidget/widgetGuestElevation.ts +34 -2
- package/src/resolvers/ActionResolver.ts +84 -21
- package/src/resolvers/AdhocQueryResolver.ts +24 -6
- package/src/resolvers/CacheStatsResolver.ts +89 -2
- package/src/resolvers/ClientToolRequestResolver.ts +25 -7
- package/src/resolvers/FetchEntityVectorsResolver.ts +97 -2
- package/src/resolvers/HumanHandoffResolver.ts +8 -0
- package/src/resolvers/MCPResolver.ts +8 -5
- package/src/resolvers/RealtimeBridgeResolver.ts +163 -11
- package/src/resolvers/RealtimeClientSessionResolver.ts +501 -96
- package/src/resolvers/RealtimeSessionEventsResolver.ts +133 -0
- package/src/resolvers/RealtimeSessionVerificationResolver.ts +174 -0
- package/src/resolvers/RemoteBrowserActionResolver.ts +11 -1
- package/src/resolvers/RunAIAgentResolver.ts +78 -11
- package/src/resolvers/RunTemplateResolver.ts +13 -12
- package/src/resolvers/SyncDataResolver.ts +6 -2
- package/src/resolvers/SyncRolesUsersResolver.ts +24 -6
- package/src/resolvers/UserAvatarResolver.ts +111 -0
- package/src/resolvers/UserResolver.ts +2 -16
- package/src/resolvers/agentVisionConsentAudit.ts +82 -0
- package/src/resolvers/avatarInputValidation.ts +123 -0
- package/src/resolvers/currentUserRoles.ts +32 -1
- package/src/resolvers/meetings/meetingMutations.ts +21 -0
- package/src/services/TaskGraphContinuationDeliverer.ts +13 -7
- package/src/sharedCache.ts +302 -0
- package/src/types.ts +7 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The authorization of the `RealtimeSessionEvents` subscription, free of GraphQL schema
|
|
3
|
+
* decorators and server configuration so it can be exercised — and reused — without loading either.
|
|
4
|
+
*
|
|
5
|
+
* It is applied twice on purpose (see `RealtimeSessionEventsResolver`): once when the subscription is
|
|
6
|
+
* opened ({@link AuthorizeRealtimeSessionSubscription}) and once for every event
|
|
7
|
+
* ({@link RealtimeSessionEventsFilter}). In neither check does a client-supplied value decide who
|
|
8
|
+
* receives anything: `agentSessionId` only selects WHICH of the caller's own sessions to listen to; the
|
|
9
|
+
* identity it is checked against comes from the server-authenticated connection context.
|
|
10
|
+
*
|
|
11
|
+
* @module @memberjunction/server/realtimeSessions
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { IMetadataProvider, LogError, Metadata, UserInfo } from '@memberjunction/core';
|
|
15
|
+
import { IsValidUUID, UUIDsEqual } from '@memberjunction/global';
|
|
16
|
+
import type { UserPayload } from '../types.js';
|
|
17
|
+
import { AuthorizeSessionAccess, LoadSessionForAccess, type LoadedSessionForAccess } from './sessionAccess.js';
|
|
18
|
+
import type { RealtimeSessionEventTopicPayload } from './RealtimeSessionEventService.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Arguments of the subscription. `agentSessionId` is camelCase because it is the GraphQL argument name
|
|
22
|
+
* graphql-js hands to `subscribe` and to the filter — it is the wire contract, not an API of ours.
|
|
23
|
+
*/
|
|
24
|
+
export interface RealtimeSessionEventsArgs {
|
|
25
|
+
agentSessionId?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The slice of the WebSocket connection context the authorization reads. `userPayload` is camelCase to
|
|
30
|
+
* match `AppContext.userPayload`, the member graphql-ws populates and every resolver already destructures.
|
|
31
|
+
*/
|
|
32
|
+
export interface RealtimeSessionEventsContext {
|
|
33
|
+
userPayload?: UserPayload;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Why a subscription request was refused (logged; the client always gets the same message). */
|
|
37
|
+
const REFUSAL_MESSAGE = 'Realtime session not found.';
|
|
38
|
+
|
|
39
|
+
/** The principal the connection authenticated as, or undefined. */
|
|
40
|
+
function connectionPrincipal(context: RealtimeSessionEventsContext | undefined): UserInfo | undefined {
|
|
41
|
+
return context?.userPayload?.userRecord as UserInfo | undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Per-event filter. Exported because it is the security-critical gate and must be testable in isolation.
|
|
46
|
+
*
|
|
47
|
+
* An event reaches a subscriber only when BOTH hold:
|
|
48
|
+
* 1. it is addressed to the session the subscriber asked for, and
|
|
49
|
+
* 2. the event's owner/scope snapshot passes {@link AuthorizeSessionAccess} for the connection's
|
|
50
|
+
* authenticated principal.
|
|
51
|
+
*
|
|
52
|
+
* Fails closed: a missing connection identity, a missing owner on the event, or a missing argument never matches.
|
|
53
|
+
*/
|
|
54
|
+
export function RealtimeSessionEventsFilter(data: {
|
|
55
|
+
payload: RealtimeSessionEventTopicPayload;
|
|
56
|
+
args: RealtimeSessionEventsArgs;
|
|
57
|
+
context: RealtimeSessionEventsContext | undefined;
|
|
58
|
+
}): boolean {
|
|
59
|
+
const { payload, args, context } = data;
|
|
60
|
+
if (typeof args?.agentSessionId !== 'string' || !payload?.AgentSessionID || !UUIDsEqual(payload.AgentSessionID, args.agentSessionId)) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
const decision = AuthorizeSessionAccess(
|
|
64
|
+
{ SessionUserID: payload.OwnerUserID, ConversationExternalID: payload.ScopeKey },
|
|
65
|
+
connectionPrincipal(context),
|
|
66
|
+
);
|
|
67
|
+
return decision.Allowed;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Dependencies of {@link AuthorizeRealtimeSessionSubscription}, injectable for tests. */
|
|
71
|
+
export interface SubscriptionAuthorizationDeps {
|
|
72
|
+
/** Loads a session under the caller's identity. */
|
|
73
|
+
LoadSession: (agentSessionId: string, user: UserInfo, provider: IMetadataProvider) => Promise<LoadedSessionForAccess | null>;
|
|
74
|
+
/** The provider to read with (a WebSocket context carries no request-scoped providers). */
|
|
75
|
+
GetProvider: () => IMetadataProvider;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Production dependencies. */
|
|
79
|
+
export const DEFAULT_SUBSCRIPTION_AUTHORIZATION_DEPS: SubscriptionAuthorizationDeps = {
|
|
80
|
+
LoadSession: LoadSessionForAccess,
|
|
81
|
+
GetProvider: () => Metadata.Provider, // global-provider-ok: a WebSocket subscription context has no per-request providers
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Subscribe-time authorization: loads the session as the connection's principal and requires
|
|
86
|
+
* {@link AuthorizeSessionAccess} to allow it. Throws {@link REFUSAL_MESSAGE} (uniformly) otherwise.
|
|
87
|
+
*
|
|
88
|
+
* @param args - the subscription arguments
|
|
89
|
+
* @param context - the connection context (server-authenticated)
|
|
90
|
+
* @param deps - injectable dependencies (tests)
|
|
91
|
+
* @returns the validated session id
|
|
92
|
+
*/
|
|
93
|
+
export async function AuthorizeRealtimeSessionSubscription(
|
|
94
|
+
args: RealtimeSessionEventsArgs,
|
|
95
|
+
context: RealtimeSessionEventsContext | undefined,
|
|
96
|
+
deps: SubscriptionAuthorizationDeps = DEFAULT_SUBSCRIPTION_AUTHORIZATION_DEPS,
|
|
97
|
+
): Promise<string> {
|
|
98
|
+
const principal = connectionPrincipal(context);
|
|
99
|
+
const agentSessionId = args?.agentSessionId;
|
|
100
|
+
if (!principal || typeof agentSessionId !== 'string' || !IsValidUUID(agentSessionId)) {
|
|
101
|
+
throw new Error(REFUSAL_MESSAGE);
|
|
102
|
+
}
|
|
103
|
+
const loaded = await deps.LoadSession(agentSessionId, principal, deps.GetProvider());
|
|
104
|
+
if (!loaded) {
|
|
105
|
+
throw new Error(REFUSAL_MESSAGE);
|
|
106
|
+
}
|
|
107
|
+
const decision = AuthorizeSessionAccess(
|
|
108
|
+
{ SessionUserID: loaded.Session.UserID, ConversationExternalID: loaded.ConversationExternalID },
|
|
109
|
+
principal,
|
|
110
|
+
);
|
|
111
|
+
if (decision.Allowed === false) {
|
|
112
|
+
LogError(`RealtimeSessionEvents: refused subscription by user ${principal.ID} to session ${agentSessionId} (${decision.Reason}).`);
|
|
113
|
+
throw new Error(REFUSAL_MESSAGE);
|
|
114
|
+
}
|
|
115
|
+
return agentSessionId;
|
|
116
|
+
}
|
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Pure functional core of mid-session identity verification — no DB, no email, no clock
|
|
3
|
+
* of its own. Everything here is deterministic given its inputs (modulo crypto randomness) and is
|
|
4
|
+
* unit-testable with plain assertions.
|
|
5
|
+
*
|
|
6
|
+
* ## What is stored, and where
|
|
7
|
+
*
|
|
8
|
+
* The verification state of a session lives in `AIAgentSession.Config` under the key
|
|
9
|
+
* `identityVerification` (a server-authoritative key — see `sessionConfigGuard` in
|
|
10
|
+
* `@memberjunction/core-entities-server`). It is an {@link IdentityVerificationState}: the policy
|
|
11
|
+
* snapshot taken at mint, how many emails the session has triggered, at most one *pending*
|
|
12
|
+
* verification, and — once done — the verified identity.
|
|
13
|
+
*
|
|
14
|
+
* ## Secrets
|
|
15
|
+
*
|
|
16
|
+
* Nothing redeemable is ever stored: only hashes.
|
|
17
|
+
*
|
|
18
|
+
* - The **link token** is `mj_rv_<session id>_<256 random bits>`. The session id is deliberately part of
|
|
19
|
+
* it so redemption is a primary-key lookup instead of a scan; it is not a secret. Only the SHA-256 of
|
|
20
|
+
* the whole token is stored, and it is compared in constant time.
|
|
21
|
+
* - The **typed code** is 6 digits (the fallback for opening the email on another device). Six digits
|
|
22
|
+
* cannot be made brute-force-proof by hashing — a million candidates is nothing — so its protection is
|
|
23
|
+
* structural: it expires, it is single-use, it is voided after {@link IdentityVerificationPolicy.maxCodeAttempts}
|
|
24
|
+
* wrong entries, only the session's own principal can submit it, and submissions are rate-limited.
|
|
25
|
+
* It is salted and bound to the session id so equal codes do not produce equal stored hashes.
|
|
26
|
+
*
|
|
27
|
+
* Token generation and hashing reuse the magic-link module's primitives (`HashToken`).
|
|
28
|
+
*
|
|
29
|
+
* @module @memberjunction/server/realtimeSessions
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { createHash, createHmac, randomBytes, randomInt, timingSafeEqual } from 'node:crypto';
|
|
33
|
+
import { HashToken } from '../auth/magicLink/magicLinkCore.js';
|
|
34
|
+
import type { IdentityVerificationPolicy, ResolvedIdentityVerificationPolicy } from './verificationPolicy.js';
|
|
35
|
+
|
|
36
|
+
/** Prefix of a verification link token (mirrors `mj_ml_` / `mj_sk_`). */
|
|
37
|
+
export const VERIFICATION_TOKEN_PREFIX = 'mj_rv_';
|
|
38
|
+
|
|
39
|
+
/** Number of digits in the typed code. */
|
|
40
|
+
export const VERIFICATION_CODE_LENGTH = 6;
|
|
41
|
+
|
|
42
|
+
/** Longest name accepted, in characters. */
|
|
43
|
+
export const MAX_VERIFICATION_NAME_LENGTH = 100;
|
|
44
|
+
|
|
45
|
+
/** A verification that has been requested and not yet redeemed. Holds hashes only. */
|
|
46
|
+
export interface PendingVerification {
|
|
47
|
+
/** SHA-256 (base64url) of the link token. */
|
|
48
|
+
TokenHash: string;
|
|
49
|
+
/** Salted, server-keyed HMAC-SHA-256 (base64url) of the typed code. */
|
|
50
|
+
CodeHash: string;
|
|
51
|
+
/** Random per-verification salt for {@link PendingVerification.CodeHash}. */
|
|
52
|
+
CodeSalt: string;
|
|
53
|
+
/** The address the link and code were sent to (normalised). */
|
|
54
|
+
Email: string;
|
|
55
|
+
/** The name given with the request (sanitised, length-capped). */
|
|
56
|
+
Name: string;
|
|
57
|
+
/** ISO instant the verification was requested. */
|
|
58
|
+
RequestedAt: string;
|
|
59
|
+
/** ISO instant after which neither the link nor the code redeems. */
|
|
60
|
+
ExpiresAt: string;
|
|
61
|
+
/** Wrong code entries so far. */
|
|
62
|
+
CodeAttempts: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The proven identity of a verified session. */
|
|
66
|
+
export interface VerifiedIdentity {
|
|
67
|
+
Email: string;
|
|
68
|
+
Name: string;
|
|
69
|
+
/** ISO instant the verification completed. */
|
|
70
|
+
VerifiedAt: string;
|
|
71
|
+
/** How it was proven. */
|
|
72
|
+
Method: 'link' | 'code';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The whole verification state of one session (the value of `Config.identityVerification`). */
|
|
76
|
+
export interface IdentityVerificationState {
|
|
77
|
+
/** The session-scoped policy layer, snapshotted at mint. Absent when the cascade carried none. */
|
|
78
|
+
Policy?: IdentityVerificationPolicy;
|
|
79
|
+
/** How many verification emails this session has triggered (a send that failed to deliver does not count). */
|
|
80
|
+
SendCount: number;
|
|
81
|
+
/** The outstanding verification, if any. A new request replaces it. */
|
|
82
|
+
Pending?: PendingVerification;
|
|
83
|
+
/** Set once verified. */
|
|
84
|
+
Verified?: VerifiedIdentity;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** A state with nothing in it. */
|
|
88
|
+
export function EmptyIdentityVerificationState(): IdentityVerificationState {
|
|
89
|
+
return { SendCount: 0 };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** True for a plain object. */
|
|
93
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
94
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** True for a non-empty string. */
|
|
98
|
+
function isNonEmptyString(value: unknown): value is string {
|
|
99
|
+
return typeof value === 'string' && value.length > 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Reads a pending verification from untrusted JSON; undefined unless every field is well-formed. */
|
|
103
|
+
function readPending(value: unknown): PendingVerification | undefined {
|
|
104
|
+
if (!isRecord(value)) {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
const { TokenHash, CodeHash, CodeSalt, Email, Name, RequestedAt, ExpiresAt, CodeAttempts } = value;
|
|
108
|
+
if (
|
|
109
|
+
!isNonEmptyString(TokenHash) || !isNonEmptyString(CodeHash) || !isNonEmptyString(CodeSalt) ||
|
|
110
|
+
!isNonEmptyString(Email) || typeof Name !== 'string' || !isNonEmptyString(RequestedAt) ||
|
|
111
|
+
!isNonEmptyString(ExpiresAt) || typeof CodeAttempts !== 'number' || !Number.isFinite(CodeAttempts)
|
|
112
|
+
) {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
return { TokenHash, CodeHash, CodeSalt, Email, Name, RequestedAt, ExpiresAt, CodeAttempts };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Reads a verified identity from untrusted JSON; undefined unless every field is well-formed. */
|
|
119
|
+
function readVerified(value: unknown): VerifiedIdentity | undefined {
|
|
120
|
+
if (!isRecord(value)) {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
const { Email, Name, VerifiedAt, Method } = value;
|
|
124
|
+
if (!isNonEmptyString(Email) || typeof Name !== 'string' || !isNonEmptyString(VerifiedAt) || (Method !== 'link' && Method !== 'code')) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
return { Email, Name, VerifiedAt, Method };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Reads the verification state out of a session's `Config` JSON. Tolerant by design: a missing,
|
|
132
|
+
* malformed or partial value yields an empty / partial state rather than an exception, because a
|
|
133
|
+
* garbled state must never be the reason a session cannot be addressed.
|
|
134
|
+
*
|
|
135
|
+
* @param configRaw - `AIAgentSession.Config` as stored
|
|
136
|
+
* @param readPolicy - reads a policy layer from untrusted JSON (injected to keep this module free of policy wiring)
|
|
137
|
+
*/
|
|
138
|
+
export function ReadIdentityVerificationState(
|
|
139
|
+
configRaw: string | null | undefined,
|
|
140
|
+
readPolicy: (raw: unknown) => IdentityVerificationPolicy,
|
|
141
|
+
): IdentityVerificationState {
|
|
142
|
+
const config = ParseConfigObject(configRaw);
|
|
143
|
+
const stored = config['identityVerification'];
|
|
144
|
+
if (!isRecord(stored)) {
|
|
145
|
+
return EmptyIdentityVerificationState();
|
|
146
|
+
}
|
|
147
|
+
const sends = stored['SendCount'];
|
|
148
|
+
const state: IdentityVerificationState = {
|
|
149
|
+
SendCount: typeof sends === 'number' && Number.isFinite(sends) && sends >= 0 ? Math.floor(sends) : 0,
|
|
150
|
+
};
|
|
151
|
+
const policy = readPolicy(stored['Policy']);
|
|
152
|
+
if (Object.keys(policy).length > 0) {
|
|
153
|
+
state.Policy = policy;
|
|
154
|
+
}
|
|
155
|
+
const pending = readPending(stored['Pending']);
|
|
156
|
+
if (pending) {
|
|
157
|
+
state.Pending = pending;
|
|
158
|
+
}
|
|
159
|
+
const verified = readVerified(stored['Verified']);
|
|
160
|
+
if (verified) {
|
|
161
|
+
state.Verified = verified;
|
|
162
|
+
}
|
|
163
|
+
return state;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Parses a `Config` JSON string into a plain object; anything else is `{}`. */
|
|
167
|
+
export function ParseConfigObject(raw: string | null | undefined): Record<string, unknown> {
|
|
168
|
+
if (typeof raw !== 'string' || raw.trim().length === 0) {
|
|
169
|
+
return {};
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
const parsed: unknown = JSON.parse(raw);
|
|
173
|
+
return isRecord(parsed) ? parsed : {};
|
|
174
|
+
} catch {
|
|
175
|
+
return {};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Returns `configRaw` with `identityVerification` (and, when given, `maxSessionDeadlineIso`) replaced,
|
|
181
|
+
* preserving every other key verbatim.
|
|
182
|
+
*
|
|
183
|
+
* @param configRaw - the stored Config
|
|
184
|
+
* @param state - the new verification state
|
|
185
|
+
* @param deadlineIso - a new absolute deadline to write, or undefined to leave the existing one alone
|
|
186
|
+
*/
|
|
187
|
+
export function WriteIdentityVerificationState(
|
|
188
|
+
configRaw: string | null | undefined,
|
|
189
|
+
state: IdentityVerificationState,
|
|
190
|
+
deadlineIso?: string,
|
|
191
|
+
): string {
|
|
192
|
+
const config = ParseConfigObject(configRaw);
|
|
193
|
+
config['identityVerification'] = state;
|
|
194
|
+
if (deadlineIso !== undefined) {
|
|
195
|
+
config['maxSessionDeadlineIso'] = deadlineIso;
|
|
196
|
+
}
|
|
197
|
+
return JSON.stringify(config);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ───────────────────────── tokens, codes, hashing ─────────────────────────
|
|
201
|
+
|
|
202
|
+
/** Lower-case 32-hex form of a UUID (the dashes dropped), or null when it is not a UUID. */
|
|
203
|
+
function compactUUID(uuid: string): string | null {
|
|
204
|
+
const compact = uuid.replace(/-/g, '').toLowerCase();
|
|
205
|
+
return /^[0-9a-f]{32}$/.test(compact) ? compact : null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Re-inserts the dashes of a compact UUID. */
|
|
209
|
+
function expandUUID(compact: string): string {
|
|
210
|
+
return `${compact.slice(0, 8)}-${compact.slice(8, 12)}-${compact.slice(12, 16)}-${compact.slice(16, 20)}-${compact.slice(20)}`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Generates a verification link token bound (by construction) to one session.
|
|
215
|
+
*
|
|
216
|
+
* @param agentSessionId - the session the token redeems against
|
|
217
|
+
* @throws Error when `agentSessionId` is not a UUID — a programming error, never user input
|
|
218
|
+
*/
|
|
219
|
+
export function GenerateVerificationToken(agentSessionId: string): string {
|
|
220
|
+
const compact = compactUUID(agentSessionId);
|
|
221
|
+
if (!compact) {
|
|
222
|
+
throw new Error(`GenerateVerificationToken: '${agentSessionId}' is not a session id`);
|
|
223
|
+
}
|
|
224
|
+
return `${VERIFICATION_TOKEN_PREFIX}${compact}_${randomBytes(32).toString('hex')}`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** The structure of a token, once parsed. */
|
|
228
|
+
export interface ParsedVerificationToken {
|
|
229
|
+
/** The session id embedded in the token (dashed, lower-case). Not a secret and not yet trusted. */
|
|
230
|
+
AgentSessionID: string;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Token shape: prefix, 32 hex of session id, `_`, 64 hex of randomness. */
|
|
234
|
+
const TOKEN_PATTERN = new RegExp(`^${VERIFICATION_TOKEN_PREFIX}([0-9a-f]{32})_[0-9a-f]{64}$`);
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Parses a presented token's structure. Success means "well-formed", NOT "valid": validity is decided
|
|
238
|
+
* only by comparing hashes ({@link EvaluateRedemption}).
|
|
239
|
+
*
|
|
240
|
+
* @returns the embedded session id, or null for anything malformed
|
|
241
|
+
*/
|
|
242
|
+
export function ParseVerificationToken(token: string): ParsedVerificationToken | null {
|
|
243
|
+
const match = typeof token === 'string' ? TOKEN_PATTERN.exec(token) : null;
|
|
244
|
+
return match ? { AgentSessionID: expandUUID(match[1]) } : null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Generates a uniformly random numeric code of {@link VERIFICATION_CODE_LENGTH} digits. */
|
|
248
|
+
export function GenerateVerificationCode(): string {
|
|
249
|
+
return randomInt(0, 10 ** VERIFICATION_CODE_LENGTH).toString().padStart(VERIFICATION_CODE_LENGTH, '0');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Normalises a typed code: strips spaces and dashes (people type "123 456"). Returns null unless the
|
|
254
|
+
* result is exactly {@link VERIFICATION_CODE_LENGTH} digits.
|
|
255
|
+
*/
|
|
256
|
+
export function NormalizeVerificationCode(input: string): string | null {
|
|
257
|
+
const stripped = (input ?? '').replace(/[\s-]/g, '');
|
|
258
|
+
return new RegExp(`^\\d{${VERIFICATION_CODE_LENGTH}}$`).test(stripped) ? stripped : null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Hash stored for a link token. */
|
|
262
|
+
export function HashVerificationToken(token: string): string {
|
|
263
|
+
return HashToken(token);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Hash stored for a code: an HMAC-SHA-256 under a SERVER-ONLY key, salted and bound to the session so
|
|
268
|
+
* equal codes never hash equal.
|
|
269
|
+
*
|
|
270
|
+
* The key is what makes this safe. A code is only six digits, and the session's owner — the very person
|
|
271
|
+
* being verified — can read their own `Session.Config`, hash and salt included. A plain (even salted)
|
|
272
|
+
* hash would let them recover the code offline in about a million guesses without ever opening the
|
|
273
|
+
* inbox, bypassing the attempt cap entirely. Without the key the stored value is useless to a reader.
|
|
274
|
+
*
|
|
275
|
+
* @param code - the normalised six-digit code
|
|
276
|
+
* @param salt - the per-verification salt stored beside the hash
|
|
277
|
+
* @param agentSessionId - the session the code is bound to
|
|
278
|
+
* @param key - the server-only key (`realtime.identityVerification.hmacSecret`); must be non-empty
|
|
279
|
+
*/
|
|
280
|
+
export function HashVerificationCode(code: string, salt: string, agentSessionId: string, key: string): string {
|
|
281
|
+
if (!key) {
|
|
282
|
+
throw new Error('HashVerificationCode requires a non-empty server key.');
|
|
283
|
+
}
|
|
284
|
+
return createHmac('sha256', key).update(`rv-code:${agentSessionId.toLowerCase()}:${salt}:${code}`).digest('base64url');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Constant-time equality of two hash strings. Both are re-hashed to a fixed length first, so neither
|
|
289
|
+
* the comparison time nor an early length check reveals how much of a guess was right.
|
|
290
|
+
*/
|
|
291
|
+
export function HashesEqual(a: string, b: string): boolean {
|
|
292
|
+
const digestA = createHash('sha256').update(a).digest();
|
|
293
|
+
const digestB = createHash('sha256').update(b).digest();
|
|
294
|
+
return timingSafeEqual(digestA, digestB);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Sanitises a display name: collapses whitespace, strips control characters and the characters that
|
|
299
|
+
* could shape markup or a header (`<`, `>`, `"`), and caps the length. The result is for display
|
|
300
|
+
* only — it is still HTML-escaped wherever it is rendered.
|
|
301
|
+
*
|
|
302
|
+
* @returns the cleaned name, or null when nothing usable is left
|
|
303
|
+
*/
|
|
304
|
+
export function SanitizeVerificationName(raw: string): string | null {
|
|
305
|
+
const cleaned = (raw ?? '')
|
|
306
|
+
// eslint-disable-next-line no-control-regex -- stripping control characters is the point
|
|
307
|
+
.replace(/[\u0000-\u001F\u007F-\u009F<>"]/g, ' ')
|
|
308
|
+
.replace(/\s+/g, ' ')
|
|
309
|
+
.trim()
|
|
310
|
+
.slice(0, MAX_VERIFICATION_NAME_LENGTH)
|
|
311
|
+
.trim();
|
|
312
|
+
return cleaned.length > 0 ? cleaned : null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ───────────────────────── issuing ─────────────────────────
|
|
316
|
+
|
|
317
|
+
/** What {@link IssueVerification} produces: the state to store and the secrets to send. */
|
|
318
|
+
export interface IssuedVerification {
|
|
319
|
+
/** Store this. Contains hashes only. */
|
|
320
|
+
Pending: PendingVerification;
|
|
321
|
+
/** Send this in the link. Never store it. */
|
|
322
|
+
Token: string;
|
|
323
|
+
/** Send this as the typed code. Never store it. */
|
|
324
|
+
Code: string;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Creates a fresh pending verification for a session.
|
|
329
|
+
*
|
|
330
|
+
* @param args.AgentSessionID - the session being verified
|
|
331
|
+
* @param args.Email - the normalised address the secrets will be sent to
|
|
332
|
+
* @param args.Name - the sanitised name
|
|
333
|
+
* @param args.TtlMinutes - how long the secrets stay redeemable
|
|
334
|
+
* @param args.NowMs - the current time, epoch ms
|
|
335
|
+
*/
|
|
336
|
+
export function IssueVerification(args: {
|
|
337
|
+
AgentSessionID: string;
|
|
338
|
+
Email: string;
|
|
339
|
+
Name: string;
|
|
340
|
+
TtlMinutes: number;
|
|
341
|
+
NowMs: number;
|
|
342
|
+
/** The server-only code-hash key (see {@link HashVerificationCode}). */
|
|
343
|
+
CodeHashKey: string;
|
|
344
|
+
}): IssuedVerification {
|
|
345
|
+
const token = GenerateVerificationToken(args.AgentSessionID);
|
|
346
|
+
const code = GenerateVerificationCode();
|
|
347
|
+
const salt = randomBytes(16).toString('base64url');
|
|
348
|
+
return {
|
|
349
|
+
Token: token,
|
|
350
|
+
Code: code,
|
|
351
|
+
Pending: {
|
|
352
|
+
TokenHash: HashVerificationToken(token),
|
|
353
|
+
CodeHash: HashVerificationCode(code, salt, args.AgentSessionID, args.CodeHashKey),
|
|
354
|
+
CodeSalt: salt,
|
|
355
|
+
Email: args.Email,
|
|
356
|
+
Name: args.Name,
|
|
357
|
+
RequestedAt: new Date(args.NowMs).toISOString(),
|
|
358
|
+
ExpiresAt: new Date(args.NowMs + args.TtlMinutes * 60_000).toISOString(),
|
|
359
|
+
CodeAttempts: 0,
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ───────────────────────── redemption ─────────────────────────
|
|
365
|
+
|
|
366
|
+
/** What a person presents to redeem a verification. */
|
|
367
|
+
export type VerificationCredential =
|
|
368
|
+
| { Kind: 'token'; Token: string }
|
|
369
|
+
| { Kind: 'code'; Code: string; AgentSessionID: string };
|
|
370
|
+
|
|
371
|
+
/** Why a redemption was refused. */
|
|
372
|
+
export type RedemptionRefusal =
|
|
373
|
+
| 'already_verified' // the session is already verified (code path; idempotent for the caller)
|
|
374
|
+
| 'no_pending' // nothing outstanding to redeem
|
|
375
|
+
| 'expired' // past ExpiresAt; the pending verification has been cleared
|
|
376
|
+
| 'invalid' // the secret did not match
|
|
377
|
+
| 'attempts_exhausted'; // too many wrong codes; the pending verification has been voided
|
|
378
|
+
|
|
379
|
+
/** Outcome of {@link EvaluateRedemption}. `Next` is the state to persist in every case. */
|
|
380
|
+
export type RedemptionOutcome =
|
|
381
|
+
| { Outcome: 'verified'; Next: IdentityVerificationState; Verified: VerifiedIdentity }
|
|
382
|
+
| { Outcome: 'rejected'; Reason: RedemptionRefusal; Next: IdentityVerificationState; AttemptsRemaining?: number };
|
|
383
|
+
|
|
384
|
+
/** `state` with the pending verification removed. */
|
|
385
|
+
function withoutPending(state: IdentityVerificationState): IdentityVerificationState {
|
|
386
|
+
const { Pending: _removed, ...rest } = state;
|
|
387
|
+
return rest;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Decides a redemption attempt and returns the state to persist. Pure: the caller supplies the time.
|
|
392
|
+
*
|
|
393
|
+
* Properties this function guarantees (and the tests pin):
|
|
394
|
+
*
|
|
395
|
+
* - **Single use.** A success removes the pending verification, so neither the link nor the code works again.
|
|
396
|
+
* - **Expiry.** Past `ExpiresAt` nothing redeems, and the stale pending verification is cleared.
|
|
397
|
+
* - **Bounded guessing.** A wrong code increments `CodeAttempts`; reaching `maxCodeAttempts` voids the
|
|
398
|
+
* pending verification (a new request — itself capped by `maxSendsPerSession` — is needed). A wrong
|
|
399
|
+
* *token* changes nothing: 256 random bits are not guessable, and counting them would let a stranger
|
|
400
|
+
* burn a legitimate person's pending link just by hitting the URL.
|
|
401
|
+
* - **Constant-time compares.** Secrets are compared via {@link HashesEqual}.
|
|
402
|
+
*
|
|
403
|
+
* @param state - the session's current verification state
|
|
404
|
+
* @param credential - what was presented
|
|
405
|
+
* @param nowMs - the current time, epoch ms
|
|
406
|
+
* @param policy - the resolved policy (for `maxCodeAttempts`)
|
|
407
|
+
* @param codeHashKey - the server-only code-hash key (see {@link HashVerificationCode})
|
|
408
|
+
*/
|
|
409
|
+
export function EvaluateRedemption(
|
|
410
|
+
state: IdentityVerificationState,
|
|
411
|
+
credential: VerificationCredential,
|
|
412
|
+
nowMs: number,
|
|
413
|
+
policy: Pick<ResolvedIdentityVerificationPolicy, 'maxCodeAttempts'>,
|
|
414
|
+
codeHashKey: string,
|
|
415
|
+
): RedemptionOutcome {
|
|
416
|
+
if (state.Verified) {
|
|
417
|
+
return { Outcome: 'rejected', Reason: 'already_verified', Next: state };
|
|
418
|
+
}
|
|
419
|
+
const pending = state.Pending;
|
|
420
|
+
if (!pending) {
|
|
421
|
+
return { Outcome: 'rejected', Reason: 'no_pending', Next: state };
|
|
422
|
+
}
|
|
423
|
+
if (nowMs >= Date.parse(pending.ExpiresAt) || Number.isNaN(Date.parse(pending.ExpiresAt))) {
|
|
424
|
+
return { Outcome: 'rejected', Reason: 'expired', Next: withoutPending(state) };
|
|
425
|
+
}
|
|
426
|
+
if (credential.Kind === 'token') {
|
|
427
|
+
return HashesEqual(HashVerificationToken(credential.Token), pending.TokenHash)
|
|
428
|
+
? verifiedOutcome(state, pending, 'link', nowMs)
|
|
429
|
+
: { Outcome: 'rejected', Reason: 'invalid', Next: state };
|
|
430
|
+
}
|
|
431
|
+
const presented = HashVerificationCode(credential.Code, pending.CodeSalt, credential.AgentSessionID, codeHashKey);
|
|
432
|
+
if (HashesEqual(presented, pending.CodeHash)) {
|
|
433
|
+
return verifiedOutcome(state, pending, 'code', nowMs);
|
|
434
|
+
}
|
|
435
|
+
return wrongCodeOutcome(state, pending, policy.maxCodeAttempts);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** The success outcome: pending cleared, identity recorded. */
|
|
439
|
+
function verifiedOutcome(
|
|
440
|
+
state: IdentityVerificationState,
|
|
441
|
+
pending: PendingVerification,
|
|
442
|
+
method: VerifiedIdentity['Method'],
|
|
443
|
+
nowMs: number,
|
|
444
|
+
): RedemptionOutcome {
|
|
445
|
+
const verified: VerifiedIdentity = {
|
|
446
|
+
Email: pending.Email,
|
|
447
|
+
Name: pending.Name,
|
|
448
|
+
VerifiedAt: new Date(nowMs).toISOString(),
|
|
449
|
+
Method: method,
|
|
450
|
+
};
|
|
451
|
+
return { Outcome: 'verified', Next: { ...withoutPending(state), Verified: verified }, Verified: verified };
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** The wrong-code outcome: one more attempt spent, voiding the pending verification at the cap. */
|
|
455
|
+
function wrongCodeOutcome(
|
|
456
|
+
state: IdentityVerificationState,
|
|
457
|
+
pending: PendingVerification,
|
|
458
|
+
maxCodeAttempts: number,
|
|
459
|
+
): RedemptionOutcome {
|
|
460
|
+
const attempts = pending.CodeAttempts + 1;
|
|
461
|
+
if (attempts >= maxCodeAttempts) {
|
|
462
|
+
return { Outcome: 'rejected', Reason: 'attempts_exhausted', Next: withoutPending(state), AttemptsRemaining: 0 };
|
|
463
|
+
}
|
|
464
|
+
return {
|
|
465
|
+
Outcome: 'rejected',
|
|
466
|
+
Reason: 'invalid',
|
|
467
|
+
Next: { ...state, Pending: { ...pending, CodeAttempts: attempts } },
|
|
468
|
+
AttemptsRemaining: maxCodeAttempts - attempts,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// ───────────────────────── deadline ─────────────────────────
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* The session deadline after a successful verification, or `undefined` when verification should not
|
|
476
|
+
* change it.
|
|
477
|
+
*
|
|
478
|
+
* Verification only ever **extends** a deadline that exists: a session with no deadline stays
|
|
479
|
+
* uncapped, and a `verifiedMaxSeconds` shorter than what is already granted never pulls a deadline in.
|
|
480
|
+
* The new deadline is measured from the session's start, so `verifiedMaxSeconds` means "a verified
|
|
481
|
+
* session may run this long in total", the same meaning `unverifiedMaxSeconds` has.
|
|
482
|
+
*
|
|
483
|
+
* @param args.CurrentDeadlineIso - the stamped deadline, if any
|
|
484
|
+
* @param args.SessionStartedAtMs - when the session started, epoch ms
|
|
485
|
+
* @param args.VerifiedMaxSeconds - the verified cap from policy, if any
|
|
486
|
+
* @returns the new ISO deadline, or undefined for "leave it alone"
|
|
487
|
+
*/
|
|
488
|
+
export function ComputeVerifiedDeadline(args: {
|
|
489
|
+
CurrentDeadlineIso: string | undefined;
|
|
490
|
+
SessionStartedAtMs: number;
|
|
491
|
+
VerifiedMaxSeconds: number | undefined;
|
|
492
|
+
}): string | undefined {
|
|
493
|
+
if (!args.VerifiedMaxSeconds || args.VerifiedMaxSeconds <= 0 || !args.CurrentDeadlineIso) {
|
|
494
|
+
return undefined;
|
|
495
|
+
}
|
|
496
|
+
const currentMs = Date.parse(args.CurrentDeadlineIso);
|
|
497
|
+
const candidateMs = args.SessionStartedAtMs + args.VerifiedMaxSeconds * 1000;
|
|
498
|
+
if (Number.isNaN(currentMs) || Number.isNaN(candidateMs) || candidateMs <= currentMs) {
|
|
499
|
+
return undefined;
|
|
500
|
+
}
|
|
501
|
+
return new Date(candidateMs).toISOString();
|
|
502
|
+
}
|