@memberjunction/server 6.2.0-edge.5 → 6.2.0-edge.6
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 +27 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/agentSessions/SessionJanitor.d.ts +3 -3
- package/dist/agentSessions/SessionJanitor.js +3 -3
- package/dist/agentSessions/SessionManager.d.ts +15 -5
- package/dist/agentSessions/SessionManager.d.ts.map +1 -1
- package/dist/agentSessions/SessionManager.js +34 -12
- package/dist/agentSessions/SessionManager.js.map +1 -1
- package/dist/auth/AuthProviderCatalogRouter.d.ts.map +1 -1
- package/dist/auth/AuthProviderCatalogRouter.js +2 -0
- package/dist/auth/AuthProviderCatalogRouter.js.map +1 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +51 -10
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/initializeProviders.d.ts +10 -0
- package/dist/auth/initializeProviders.d.ts.map +1 -1
- package/dist/auth/initializeProviders.js +37 -1
- package/dist/auth/initializeProviders.js.map +1 -1
- package/dist/auth/localDevAuth.d.ts +79 -0
- package/dist/auth/localDevAuth.d.ts.map +1 -0
- package/dist/auth/localDevAuth.js +439 -0
- package/dist/auth/localDevAuth.js.map +1 -0
- package/dist/auth/magicLink/MagicLinkKeys.d.ts +2 -2
- package/dist/auth/magicLink/MagicLinkKeys.d.ts.map +1 -1
- package/dist/auth/magicLink/MagicLinkKeys.js +1 -1
- package/dist/auth/magicLink/MagicLinkKeys.js.map +1 -1
- package/dist/auth/magicLink/types.d.ts +12 -0
- package/dist/auth/magicLink/types.d.ts.map +1 -1
- package/dist/auth/sessionUserClone.d.ts +1 -1
- package/dist/auth/sessionUserClone.d.ts.map +1 -1
- package/dist/auth/sessionUserClone.js +3 -2
- package/dist/auth/sessionUserClone.js.map +1 -1
- package/dist/broadcast/BaseCacheInvalidationBroadcastProvider.d.ts +292 -0
- package/dist/broadcast/BaseCacheInvalidationBroadcastProvider.d.ts.map +1 -0
- package/dist/broadcast/BaseCacheInvalidationBroadcastProvider.js +442 -0
- package/dist/broadcast/BaseCacheInvalidationBroadcastProvider.js.map +1 -0
- package/dist/broadcast/index.d.ts +6 -0
- package/dist/broadcast/index.d.ts.map +1 -0
- package/dist/broadcast/index.js +6 -0
- package/dist/broadcast/index.js.map +1 -0
- package/dist/broadcast/providers/AWSIoTCoreBroadcastProvider.d.ts +43 -0
- package/dist/broadcast/providers/AWSIoTCoreBroadcastProvider.d.ts.map +1 -0
- package/dist/broadcast/providers/AWSIoTCoreBroadcastProvider.js +144 -0
- package/dist/broadcast/providers/AWSIoTCoreBroadcastProvider.js.map +1 -0
- package/dist/broadcast/providers/AzureWebPubSubBroadcastProvider.d.ts +46 -0
- package/dist/broadcast/providers/AzureWebPubSubBroadcastProvider.d.ts.map +1 -0
- package/dist/broadcast/providers/AzureWebPubSubBroadcastProvider.js +149 -0
- package/dist/broadcast/providers/AzureWebPubSubBroadcastProvider.js.map +1 -0
- package/dist/broadcast/providers/GCPPubSubBroadcastProvider.d.ts +49 -0
- package/dist/broadcast/providers/GCPPubSubBroadcastProvider.d.ts.map +1 -0
- package/dist/broadcast/providers/GCPPubSubBroadcastProvider.js +131 -0
- package/dist/broadcast/providers/GCPPubSubBroadcastProvider.js.map +1 -0
- package/dist/broadcast/providers/SequencedRoleBucketBroadcastProvider.d.ts +24 -0
- package/dist/broadcast/providers/SequencedRoleBucketBroadcastProvider.d.ts.map +1 -0
- package/dist/broadcast/providers/SequencedRoleBucketBroadcastProvider.js +51 -0
- package/dist/broadcast/providers/SequencedRoleBucketBroadcastProvider.js.map +1 -0
- package/dist/cache.d.ts +3 -2
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js.map +1 -1
- package/dist/config.d.ts +496 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +61 -0
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +3 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +29 -8
- package/dist/context.js.map +1 -1
- package/dist/coreRoutes.d.ts.map +1 -1
- package/dist/coreRoutes.js +14 -0
- package/dist/coreRoutes.js.map +1 -1
- package/dist/generated/graphql-schemas/__mj.d.ts +635 -14
- package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
- package/dist/generated/graphql-schemas/__mj.js +3366 -418
- package/dist/generated/graphql-schemas/__mj.js.map +1 -1
- package/dist/generic/CacheInvalidationResolver.d.ts +50 -30
- package/dist/generic/CacheInvalidationResolver.d.ts.map +1 -1
- package/dist/generic/CacheInvalidationResolver.js +154 -79
- package/dist/generic/CacheInvalidationResolver.js.map +1 -1
- package/dist/generic/OrphanedConversationDetailReconciler.d.ts.map +1 -1
- package/dist/generic/OrphanedConversationDetailReconciler.js +2 -0
- package/dist/generic/OrphanedConversationDetailReconciler.js.map +1 -1
- package/dist/generic/PubSubManager.d.ts +10 -0
- package/dist/generic/PubSubManager.d.ts.map +1 -1
- package/dist/generic/PubSubManager.js +44 -1
- package/dist/generic/PubSubManager.js.map +1 -1
- package/dist/generic/PushStatusResolver.d.ts +39 -6
- package/dist/generic/PushStatusResolver.d.ts.map +1 -1
- package/dist/generic/PushStatusResolver.js +93 -11
- package/dist/generic/PushStatusResolver.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +65 -25
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +201 -101
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts +11 -3
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +79 -13
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/generic/actionOutputFieldSecurity.d.ts +26 -0
- package/dist/generic/actionOutputFieldSecurity.d.ts.map +1 -0
- package/dist/generic/actionOutputFieldSecurity.js +47 -0
- package/dist/generic/actionOutputFieldSecurity.js.map +1 -0
- package/dist/generic/fieldSecurityTransport.d.ts +30 -0
- package/dist/generic/fieldSecurityTransport.d.ts.map +1 -0
- package/dist/generic/fieldSecurityTransport.js +30 -0
- package/dist/generic/fieldSecurityTransport.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +122 -66
- package/dist/index.js.map +1 -1
- package/dist/integration/CustomColumnPromoter.d.ts.map +1 -1
- package/dist/integration/CustomColumnPromoter.js +6 -1
- package/dist/integration/CustomColumnPromoter.js.map +1 -1
- package/dist/logging/variablesLoggingMiddleware.d.ts +12 -0
- package/dist/logging/variablesLoggingMiddleware.d.ts.map +1 -1
- package/dist/logging/variablesLoggingMiddleware.js +50 -1
- package/dist/logging/variablesLoggingMiddleware.js.map +1 -1
- package/dist/realtimeSessions/SessionVerificationPortsBase.d.ts.map +1 -1
- package/dist/realtimeSessions/SessionVerificationPortsBase.js +1 -0
- package/dist/realtimeSessions/SessionVerificationPortsBase.js.map +1 -1
- package/dist/realtimeSessions/ownedRealtimeSession.d.ts +8 -6
- package/dist/realtimeSessions/ownedRealtimeSession.d.ts.map +1 -1
- package/dist/realtimeSessions/ownedRealtimeSession.js +15 -6
- package/dist/realtimeSessions/ownedRealtimeSession.js.map +1 -1
- package/dist/realtimeSessions/sessionAccess.d.ts +88 -11
- package/dist/realtimeSessions/sessionAccess.d.ts.map +1 -1
- package/dist/realtimeSessions/sessionAccess.js +125 -22
- package/dist/realtimeSessions/sessionAccess.js.map +1 -1
- package/dist/realtimeSessions/sessionConfigStamps.js +1 -1
- package/dist/realtimeSessions/sessionConfigStamps.js.map +1 -1
- package/dist/realtimeSessions/subscriptionAuthorization.d.ts.map +1 -1
- package/dist/realtimeSessions/subscriptionAuthorization.js +5 -2
- package/dist/realtimeSessions/subscriptionAuthorization.js.map +1 -1
- package/dist/realtimeSessions/verificationWorkflow.d.ts +2 -0
- package/dist/realtimeSessions/verificationWorkflow.d.ts.map +1 -1
- package/dist/realtimeSessions/verificationWorkflow.js +1 -1
- package/dist/realtimeSessions/verificationWorkflow.js.map +1 -1
- package/dist/realtimeWidget/WidgetRouter.d.ts.map +1 -1
- package/dist/realtimeWidget/WidgetRouter.js +19 -3
- package/dist/realtimeWidget/WidgetRouter.js.map +1 -1
- package/dist/realtimeWidget/WidgetSessionService.d.ts +67 -7
- package/dist/realtimeWidget/WidgetSessionService.d.ts.map +1 -1
- package/dist/realtimeWidget/WidgetSessionService.js +201 -19
- package/dist/realtimeWidget/WidgetSessionService.js.map +1 -1
- package/dist/realtimeWidget/host-identity.d.ts +1 -1
- package/dist/realtimeWidget/host-identity.js +1 -1
- package/dist/realtimeWidget/widgetCore.d.ts +20 -1
- package/dist/realtimeWidget/widgetCore.d.ts.map +1 -1
- package/dist/realtimeWidget/widgetCore.js +17 -1
- package/dist/realtimeWidget/widgetCore.js.map +1 -1
- package/dist/realtimeWidget/widgetGuestElevation.d.ts +1 -1
- package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -1
- package/dist/realtimeWidget/widgetGuestElevation.js +2 -1
- package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
- package/dist/resolvers/ActionResolver.d.ts +19 -1
- package/dist/resolvers/ActionResolver.d.ts.map +1 -1
- package/dist/resolvers/ActionResolver.js +63 -12
- package/dist/resolvers/ActionResolver.js.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.js +1 -0
- package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
- package/dist/resolvers/AgentRunStatusPublisher.d.ts +18 -4
- package/dist/resolvers/AgentRunStatusPublisher.d.ts.map +1 -1
- package/dist/resolvers/AgentRunStatusPublisher.js +32 -8
- package/dist/resolvers/AgentRunStatusPublisher.js.map +1 -1
- package/dist/resolvers/AgentSessionResolver.d.ts +10 -4
- package/dist/resolvers/AgentSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/AgentSessionResolver.js +40 -17
- package/dist/resolvers/AgentSessionResolver.js.map +1 -1
- package/dist/resolvers/EntityCommunicationsResolver.d.ts +13 -1
- package/dist/resolvers/EntityCommunicationsResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityCommunicationsResolver.js +50 -14
- package/dist/resolvers/EntityCommunicationsResolver.js.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.d.ts +28 -0
- package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.js +117 -21
- package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
- package/dist/resolvers/FileCategoryResolver.d.ts.map +1 -1
- package/dist/resolvers/FileCategoryResolver.js +1 -0
- package/dist/resolvers/FileCategoryResolver.js.map +1 -1
- package/dist/resolvers/FileResolver.d.ts +5 -0
- package/dist/resolvers/FileResolver.d.ts.map +1 -1
- package/dist/resolvers/FileResolver.js +13 -4
- package/dist/resolvers/FileResolver.js.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +45 -7
- package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
- package/dist/resolvers/MergeRecordsResolver.d.ts +8 -1
- package/dist/resolvers/MergeRecordsResolver.d.ts.map +1 -1
- package/dist/resolvers/MergeRecordsResolver.js +31 -3
- package/dist/resolvers/MergeRecordsResolver.js.map +1 -1
- package/dist/resolvers/PipelineProgressResolver.d.ts +6 -0
- package/dist/resolvers/PipelineProgressResolver.d.ts.map +1 -1
- package/dist/resolvers/PipelineProgressResolver.js +5 -3
- package/dist/resolvers/PipelineProgressResolver.js.map +1 -1
- package/dist/resolvers/QueryResolver.d.ts.map +1 -1
- package/dist/resolvers/QueryResolver.js +20 -6
- package/dist/resolvers/QueryResolver.js.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.d.ts +2 -0
- package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.js +15 -6
- package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
- package/dist/resolvers/RSUResolver.d.ts.map +1 -1
- package/dist/resolvers/RSUResolver.js +12 -0
- package/dist/resolvers/RSUResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +16 -2
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +82 -28
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
- package/dist/resolvers/RemoteBrowserActionResolver.js +11 -9
- package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts +56 -8
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +209 -64
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
- package/dist/resolvers/RunTemplateResolver.js +4 -4
- package/dist/resolvers/RunTemplateResolver.js.map +1 -1
- package/dist/resolvers/RunTestResolver.d.ts.map +1 -1
- package/dist/resolvers/RunTestResolver.js +3 -1
- package/dist/resolvers/RunTestResolver.js.map +1 -1
- package/dist/resolvers/SyncDataResolver.d.ts.map +1 -1
- package/dist/resolvers/SyncDataResolver.js +4 -0
- package/dist/resolvers/SyncDataResolver.js.map +1 -1
- package/dist/resolvers/TestQuerySQLResolver.d.ts.map +1 -1
- package/dist/resolvers/TestQuerySQLResolver.js +1 -0
- package/dist/resolvers/TestQuerySQLResolver.js.map +1 -1
- package/dist/resolvers/TransactionGroupResolver.d.ts +17 -2
- package/dist/resolvers/TransactionGroupResolver.d.ts.map +1 -1
- package/dist/resolvers/TransactionGroupResolver.js +60 -15
- package/dist/resolvers/TransactionGroupResolver.js.map +1 -1
- package/dist/resolvers/UserViewResolver.d.ts +1 -1
- package/dist/resolvers/UserViewResolver.d.ts.map +1 -1
- package/dist/resolvers/UserViewResolver.js +2 -1
- package/dist/resolvers/UserViewResolver.js.map +1 -1
- package/dist/rest/EntityCRUDHandler.d.ts +6 -0
- package/dist/rest/EntityCRUDHandler.d.ts.map +1 -1
- package/dist/rest/EntityCRUDHandler.js +19 -5
- package/dist/rest/EntityCRUDHandler.js.map +1 -1
- package/dist/rest/RESTEndpointHandler.d.ts.map +1 -1
- package/dist/rest/RESTEndpointHandler.js +8 -5
- package/dist/rest/RESTEndpointHandler.js.map +1 -1
- package/dist/rest/RealtimeRecordingHandler.d.ts +3 -3
- package/dist/rest/RealtimeRecordingHandler.d.ts.map +1 -1
- package/dist/rest/RealtimeRecordingHandler.js +7 -7
- package/dist/rest/RealtimeRecordingHandler.js.map +1 -1
- package/dist/rest/ViewOperationsHandler.d.ts +6 -0
- package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
- package/dist/rest/ViewOperationsHandler.js +22 -5
- package/dist/rest/ViewOperationsHandler.js.map +1 -1
- package/dist/serverRoleConfigUnits.d.ts +29 -0
- package/dist/serverRoleConfigUnits.d.ts.map +1 -0
- package/dist/serverRoleConfigUnits.js +33 -0
- package/dist/serverRoleConfigUnits.js.map +1 -0
- package/dist/services/TaskGraphContinuationDeliverer.d.ts +5 -0
- package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.js +42 -15
- package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
- package/dist/types.d.ts +23 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +98 -98
- package/src/__tests__/ActionResolver.fieldSecurity.test.ts +237 -0
- package/src/__tests__/AdhocQueryResolver.dialect.test.ts +137 -0
- package/src/__tests__/AgentRunStatusPublisher.test.ts +49 -0
- package/src/__tests__/AgentSessionResolver.ownership.test.ts +137 -0
- package/src/__tests__/CustomColumnPromoterOnePass.test.ts +15 -1
- package/src/__tests__/EntityCommunicationsResolver.actingUser.test.ts +151 -0
- package/src/__tests__/EvolutionActsOnWhatItDetects.test.ts +113 -0
- package/src/__tests__/FetchEntityVectorsResolver.test.ts +206 -3
- package/src/__tests__/FileResolver.fieldSecurity.test.ts +82 -0
- package/src/__tests__/PipelineProgressResolver.filter.test.ts +58 -0
- package/src/__tests__/QuerySystemUserResolver.fieldSecurity.test.ts +49 -0
- package/src/__tests__/RESTEndpointHandler.test.ts +25 -0
- package/src/__tests__/RealtimeClientSessionResolver.configTamper.test.ts +23 -3
- package/src/__tests__/RealtimeClientSessionResolver.test.ts +324 -46
- package/src/__tests__/RemoteBrowserActionResolver.ownership.test.ts +98 -0
- package/src/__tests__/ResolverBase.clientOldValues.test.ts +37 -4
- package/src/__tests__/ResolverBase.deleteReturnProjection.test.ts +52 -0
- package/src/__tests__/ResolverBase.entityMessageRelay.test.ts +176 -0
- package/src/__tests__/ResolverBase.includeBinaryFields.test.ts +23 -1
- package/src/__tests__/ResolverBase.publishStatusUpdate.test.ts +68 -0
- package/src/__tests__/ResolverBase.userViewCacheAndDedup.test.ts +130 -0
- package/src/__tests__/RunAIAgentResolver.branch.test.ts +68 -13
- package/src/__tests__/RunAIAgentResolver.conversationWriter.test.ts +210 -0
- package/src/__tests__/RunAIAgentResolver.fireAndForgetCompletion.test.ts +209 -0
- package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +17 -27
- package/src/__tests__/RunAIAgentResolver.runDataGuard.test.ts +8 -2
- package/src/__tests__/RunAIAgentResolver.serverReply.test.ts +358 -0
- package/src/__tests__/RunAIAgentResolver.stoppedPredecessor.test.ts +4 -1
- package/src/__tests__/RunTemplateResolver.test.ts +35 -2
- package/src/__tests__/RunViewResolver.cacheCheckForwarding.test.ts +11 -0
- package/src/__tests__/SessionManager.test.ts +95 -0
- package/src/__tests__/TaskGraphContinuationDeliverer.test.ts +114 -3
- package/src/__tests__/TransactionGroupResolver.fieldSecurity.test.ts +199 -0
- package/src/__tests__/UserViewResolver.updateWhereClause.test.ts +15 -0
- package/src/__tests__/ViewOperationsHandler.test.ts +7 -2
- package/src/__tests__/WidgetSessionService.test.ts +583 -4
- package/src/__tests__/apiRootBoot.test.ts +1 -1
- package/src/__tests__/buildMagicLinkSessionUser.test.ts +96 -0
- package/src/__tests__/cacheBroadcastProviders.test.ts +403 -0
- package/src/__tests__/cacheInvalidationRecordData.test.ts +45 -0
- package/src/__tests__/fieldSecurityFixtures.ts +91 -0
- package/src/__tests__/globalCors.test.ts +3 -3
- package/src/__tests__/localDevAuth.test.ts +196 -0
- package/src/__tests__/orphanedConversationDetailReconciler.test.ts +10 -0
- package/src/__tests__/pushStatusResolver.filter.test.ts +57 -1
- package/src/__tests__/realtimeRecordingHandler.test.ts +19 -2
- package/src/__tests__/realtimeSessions/RealtimeSessionEventsResolver.test.ts +29 -1
- package/src/__tests__/realtimeSessions/ownedRealtimeSession.anonymous.test.ts +31 -0
- package/src/__tests__/realtimeSessions/ownedRealtimeSession.namedScoped.test.ts +37 -0
- package/src/__tests__/realtimeSessions/sessionAccess.test.ts +146 -7
- package/src/__tests__/realtimeSessions/sessionConfigStamps.test.ts +1 -1
- package/src/__tests__/realtimeSessions/verificationWorkflow.test.ts +25 -4
- package/src/__tests__/resolverBase.cloudEventChangedFields.test.ts +157 -0
- package/src/__tests__/resolverBase.fieldSecurityTransport.test.ts +222 -0
- package/src/__tests__/resolverBase.fls.test.ts +146 -97
- package/src/__tests__/restTransport.fieldSecurity.test.ts +148 -0
- package/src/__tests__/runParamsTimeZone.test.ts +179 -0
- package/src/__tests__/runStreams.fieldSecurity.test.ts +100 -0
- package/src/__tests__/runViewProperties.test.ts +114 -0
- package/src/__tests__/scalabilityBenchmarks.test.ts +765 -0
- package/src/__tests__/scalabilityDatabaseIntegration.test.ts +207 -0
- package/src/__tests__/sessionUserClone.test.ts +5 -1
- package/src/__tests__/startupSweepOrder.test.ts +40 -0
- package/src/__tests__/unifiedAuth.test.ts +28 -0
- package/src/__tests__/widgetGuestElevation.test.ts +2 -0
- package/src/agentSessions/SessionJanitor.ts +3 -3
- package/src/agentSessions/SessionManager.ts +35 -12
- package/src/auth/AuthProviderCatalogRouter.ts +3 -0
- package/src/auth/index.ts +56 -12
- package/src/auth/initializeProviders.ts +43 -1
- package/src/auth/localDevAuth.ts +528 -0
- package/src/auth/magicLink/MagicLinkKeys.ts +2 -2
- package/src/auth/magicLink/types.ts +10 -0
- package/src/auth/sessionUserClone.ts +3 -2
- package/src/broadcast/BaseCacheInvalidationBroadcastProvider.ts +674 -0
- package/src/broadcast/index.ts +5 -0
- package/src/broadcast/providers/AWSIoTCoreBroadcastProvider.ts +180 -0
- package/src/broadcast/providers/AzureWebPubSubBroadcastProvider.ts +182 -0
- package/src/broadcast/providers/GCPPubSubBroadcastProvider.ts +169 -0
- package/src/broadcast/providers/SequencedRoleBucketBroadcastProvider.ts +57 -0
- package/src/cache.ts +2 -1
- package/src/config.ts +69 -0
- package/src/context.ts +32 -8
- package/src/coreRoutes.ts +15 -0
- package/src/generated/graphql-schemas/__mj.ts +2239 -102
- package/src/generic/CacheInvalidationResolver.ts +135 -78
- package/src/generic/OrphanedConversationDetailReconciler.ts +4 -2
- package/src/generic/PubSubManager.ts +54 -1
- package/src/generic/PushStatusResolver.ts +108 -11
- package/src/generic/ResolverBase.ts +237 -107
- package/src/generic/RunViewResolver.ts +80 -15
- package/src/generic/actionOutputFieldSecurity.ts +75 -0
- package/src/generic/fieldSecurityTransport.ts +62 -0
- package/src/index.ts +127 -68
- package/src/integration/CustomColumnPromoter.ts +6 -1
- package/src/logging/variablesLoggingMiddleware.ts +59 -3
- package/src/realtimeSessions/README.md +7 -3
- package/src/realtimeSessions/SessionVerificationPortsBase.ts +1 -0
- package/src/realtimeSessions/ownedRealtimeSession.ts +20 -6
- package/src/realtimeSessions/sessionAccess.ts +181 -25
- package/src/realtimeSessions/sessionConfigStamps.ts +1 -1
- package/src/realtimeSessions/subscriptionAuthorization.ts +4 -2
- package/src/realtimeSessions/verificationWorkflow.ts +3 -1
- package/src/realtimeWidget/WidgetRouter.ts +22 -4
- package/src/realtimeWidget/WidgetSessionService.ts +252 -17
- package/src/realtimeWidget/host-identity.ts +1 -1
- package/src/realtimeWidget/widgetCore.ts +30 -1
- package/src/realtimeWidget/widgetGuestElevation.ts +2 -1
- package/src/resolvers/ActionResolver.ts +78 -15
- package/src/resolvers/AdhocQueryResolver.ts +1 -0
- package/src/resolvers/AgentRunStatusPublisher.ts +36 -8
- package/src/resolvers/AgentSessionResolver.ts +46 -18
- package/src/resolvers/EntityCommunicationsResolver.ts +56 -15
- package/src/resolvers/FetchEntityVectorsResolver.ts +132 -21
- package/src/resolvers/FileCategoryResolver.ts +1 -0
- package/src/resolvers/FileResolver.ts +14 -4
- package/src/resolvers/IntegrationDiscoveryResolver.ts +49 -7
- package/src/resolvers/MergeRecordsResolver.ts +23 -2
- package/src/resolvers/PipelineProgressResolver.ts +12 -3
- package/src/resolvers/QueryResolver.ts +20 -6
- package/src/resolvers/QuerySystemUserResolver.ts +15 -6
- package/src/resolvers/RSUResolver.ts +12 -0
- package/src/resolvers/RealtimeClientSessionResolver.ts +104 -33
- package/src/resolvers/RemoteBrowserActionResolver.ts +15 -10
- package/src/resolvers/RunAIAgentResolver.ts +258 -82
- package/src/resolvers/RunTemplateResolver.ts +6 -4
- package/src/resolvers/RunTestResolver.ts +3 -1
- package/src/resolvers/SyncDataResolver.ts +4 -0
- package/src/resolvers/TestQuerySQLResolver.ts +1 -0
- package/src/resolvers/TransactionGroupResolver.ts +66 -15
- package/src/resolvers/UserViewResolver.ts +2 -1
- package/src/resolvers/__tests__/MergeRecordsResolver.test.ts +43 -0
- package/src/rest/EntityCRUDHandler.ts +22 -5
- package/src/rest/RESTEndpointHandler.ts +8 -5
- package/src/rest/RealtimeRecordingHandler.ts +13 -10
- package/src/rest/ViewOperationsHandler.ts +24 -6
- package/src/serverRoleConfigUnits.ts +48 -0
- package/src/services/TaskGraphContinuationDeliverer.ts +52 -20
- package/src/types.ts +23 -1
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
|
|
3
|
+
import sql from 'mssql';
|
|
4
|
+
import { RunViewParams, RunViewResult, SetProvider, UserInfo } from '@memberjunction/core';
|
|
5
|
+
import { GenericDatabaseProvider, UserCache } from '@memberjunction/generic-database-provider';
|
|
6
|
+
import { SQLServerDataProvider, SQLServerProviderConfigData } from '@memberjunction/sqlserver-dataprovider';
|
|
7
|
+
import {
|
|
8
|
+
CacheInvalidationBroadcastManager,
|
|
9
|
+
SequencedRoleBucketBroadcastProvider,
|
|
10
|
+
} from '../broadcast/index.js';
|
|
11
|
+
|
|
12
|
+
const IT_DB_NAME = process.env.MJ_IT_DB;
|
|
13
|
+
|
|
14
|
+
describe.skipIf(!IT_DB_NAME)('MJServer Scalability Architecture — Live SQL Server Integration', () => {
|
|
15
|
+
let pool: sql.ConnectionPool;
|
|
16
|
+
let sharedProvider: SQLServerDataProvider;
|
|
17
|
+
let sysUser: UserInfo;
|
|
18
|
+
|
|
19
|
+
beforeAll(async () => {
|
|
20
|
+
pool = new sql.ConnectionPool({
|
|
21
|
+
server: process.env.MJ_IT_HOST || 'localhost',
|
|
22
|
+
port: parseInt(process.env.MJ_IT_PORT || '1433', 10),
|
|
23
|
+
database: IT_DB_NAME,
|
|
24
|
+
user: process.env.MJ_IT_USER || 'sa',
|
|
25
|
+
password: process.env.MJ_IT_PASSWORD || 'GHWUc7QKZNT8GQiqnJHdEPonPcukY2WA',
|
|
26
|
+
options: {
|
|
27
|
+
encrypt: false,
|
|
28
|
+
trustServerCertificate: true,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
await pool.connect();
|
|
32
|
+
|
|
33
|
+
sharedProvider = new SQLServerDataProvider();
|
|
34
|
+
const config = new SQLServerProviderConfigData(pool, '__mj', 0);
|
|
35
|
+
await sharedProvider.Config(config);
|
|
36
|
+
SetProvider(sharedProvider);
|
|
37
|
+
await UserCache.Instance.Refresh(sharedProvider, 0);
|
|
38
|
+
|
|
39
|
+
const foundUser =
|
|
40
|
+
UserCache.Instance.GetSystemUser() ||
|
|
41
|
+
UserCache.Instance.Users.find((u) => u.IsActive && u.Type === 'Owner') ||
|
|
42
|
+
UserCache.Instance.Users[0];
|
|
43
|
+
if (!foundUser) {
|
|
44
|
+
throw new Error(`No user found in ${IT_DB_NAME}`);
|
|
45
|
+
}
|
|
46
|
+
sysUser = foundUser;
|
|
47
|
+
}, 60_000);
|
|
48
|
+
|
|
49
|
+
afterAll(async () => {
|
|
50
|
+
if (pool?.connected) {
|
|
51
|
+
await pool.close();
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('creates per-request SQLServerDataProvider with zero-copy prototype-delegated metadata shell and lazy transaction queues', async () => {
|
|
56
|
+
const reqProvider = new SQLServerDataProvider();
|
|
57
|
+
const reqConfig = new SQLServerProviderConfigData(
|
|
58
|
+
pool,
|
|
59
|
+
'__mj',
|
|
60
|
+
0,
|
|
61
|
+
undefined,
|
|
62
|
+
undefined,
|
|
63
|
+
false // ignoreExistingMetadata = false -> reuses shared metadata shell
|
|
64
|
+
);
|
|
65
|
+
await reqProvider.Config(reqConfig);
|
|
66
|
+
|
|
67
|
+
// Verify lazy RxJS Subjects are NOT allocated on a read-only request provider
|
|
68
|
+
const internal = reqProvider as unknown as {
|
|
69
|
+
_sqlQueue$: unknown;
|
|
70
|
+
_transactionState$: unknown;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
expect(internal._sqlQueue$).toBeNull();
|
|
74
|
+
expect(internal._transactionState$).toBeNull();
|
|
75
|
+
|
|
76
|
+
// Verify zero-copy prototype delegation to shared metadata
|
|
77
|
+
expect(Object.getPrototypeOf(reqProvider.AllMetadata)).toBe(sharedProvider.AllMetadata);
|
|
78
|
+
expect(reqProvider.Entities.length).toBeGreaterThan(50);
|
|
79
|
+
expect(reqProvider.Entities).toBe(sharedProvider.Entities);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('coalesces 10 concurrent identical InternalRunView calls against SQL Server into a single SQL execution and isolates result mutations', async () => {
|
|
83
|
+
// Warm up the view column order cache for vwEntities (must return >=1 row so sortViewRowKeys caches sys.columns)
|
|
84
|
+
await sharedProvider.RunView(
|
|
85
|
+
{
|
|
86
|
+
EntityName: 'MJ: Entities',
|
|
87
|
+
ExtraFilter: "Name = 'MJ: Entities'",
|
|
88
|
+
ResultType: 'simple',
|
|
89
|
+
IgnoreMaxRows: true,
|
|
90
|
+
},
|
|
91
|
+
sysUser
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const executeSpy = vi.spyOn(sharedProvider, 'ExecuteSQL');
|
|
95
|
+
const internalRunner = sharedProvider as unknown as {
|
|
96
|
+
InternalRunView<T>(params: RunViewParams, contextUser?: UserInfo): Promise<RunViewResult<T>>;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const concurrentCount = 10;
|
|
101
|
+
const promises = Array.from({ length: concurrentCount }, () =>
|
|
102
|
+
internalRunner.InternalRunView<Record<string, unknown>>(
|
|
103
|
+
{
|
|
104
|
+
EntityName: 'MJ: Entities',
|
|
105
|
+
ExtraFilter: "SchemaName = '__mj' AND Name LIKE 'MJ: %'",
|
|
106
|
+
OrderBy: 'Name ASC',
|
|
107
|
+
IgnoreMaxRows: true,
|
|
108
|
+
ResultType: 'simple',
|
|
109
|
+
},
|
|
110
|
+
sysUser
|
|
111
|
+
)
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const results = await Promise.all(promises);
|
|
115
|
+
|
|
116
|
+
// Only 1 physical SQL execution should have been issued for the 10 identical concurrent views
|
|
117
|
+
expect(executeSpy).toHaveBeenCalledTimes(1);
|
|
118
|
+
|
|
119
|
+
for (const res of results) {
|
|
120
|
+
expect(res.Success).toBe(true);
|
|
121
|
+
expect(res.Results.length).toBeGreaterThan(0);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Mutating caller 0's result array or row object must not affect caller 1's result
|
|
125
|
+
const originalName = results[1].Results[0].Name;
|
|
126
|
+
const originalLength = results[1].Results.length;
|
|
127
|
+
results[0].Results[0].Name = 'MUTATED_IN_CALLER_0';
|
|
128
|
+
results[0].Results.pop();
|
|
129
|
+
|
|
130
|
+
expect(results[1].Results[0].Name).toBe(originalName);
|
|
131
|
+
expect(results[0].Results.length).toBe(originalLength - 1);
|
|
132
|
+
expect(results[1].Results.length).toBe(originalLength);
|
|
133
|
+
} finally {
|
|
134
|
+
executeSpy.mockRestore();
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('bounds concurrent SQL executions in RunViews to MaxConcurrentViewsPerBatch', async () => {
|
|
139
|
+
let activeCount = 0;
|
|
140
|
+
let maxObservedConcurrency = 0;
|
|
141
|
+
|
|
142
|
+
const originalExecuteSQL = sharedProvider.ExecuteSQL.bind(sharedProvider);
|
|
143
|
+
const executeSpy = vi.spyOn(sharedProvider, 'ExecuteSQL').mockImplementation(async (query, params, options, contextUser) => {
|
|
144
|
+
activeCount++;
|
|
145
|
+
if (activeCount > maxObservedConcurrency) {
|
|
146
|
+
maxObservedConcurrency = activeCount;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
return await originalExecuteSQL(query, params, options, contextUser);
|
|
150
|
+
} finally {
|
|
151
|
+
activeCount--;
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const batchParams = Array.from({ length: 16 }, (_, idx) => ({
|
|
157
|
+
EntityName: 'MJ: Entities',
|
|
158
|
+
ExtraFilter: `SchemaName = '__mj' AND UserViewMaxRows IS NULL AND ${idx} = ${idx}`,
|
|
159
|
+
IgnoreMaxRows: true,
|
|
160
|
+
ResultType: 'simple' as const,
|
|
161
|
+
}));
|
|
162
|
+
|
|
163
|
+
const batchResults = await sharedProvider.RunViews(batchParams, sysUser);
|
|
164
|
+
expect(batchResults).toHaveLength(16);
|
|
165
|
+
for (const r of batchResults) {
|
|
166
|
+
expect(r.Success).toBe(true);
|
|
167
|
+
}
|
|
168
|
+
expect(maxObservedConcurrency).toBeLessThanOrEqual(GenericDatabaseProvider.MaxConcurrentViewsPerBatch);
|
|
169
|
+
expect(maxObservedConcurrency).toBeGreaterThan(1);
|
|
170
|
+
} finally {
|
|
171
|
+
executeSpy.mockRestore();
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('sequences and shapes cache invalidation broadcasts against live entity metadata and replays delta catch-up', async () => {
|
|
176
|
+
const provider = CacheInvalidationBroadcastManager.Instance.Configure({
|
|
177
|
+
provider: 'SequencedRoleBucket',
|
|
178
|
+
ringBufferSize: 500,
|
|
179
|
+
}) as SequencedRoleBucketBroadcastProvider;
|
|
180
|
+
expect(provider).toBeInstanceOf(SequencedRoleBucketBroadcastProvider);
|
|
181
|
+
|
|
182
|
+
const baseSeq = provider.CurrentSequence;
|
|
183
|
+
const entityInfo = sharedProvider.Entities.find((e) => e.Name === 'MJ: Entities');
|
|
184
|
+
expect(entityInfo).toBeDefined();
|
|
185
|
+
|
|
186
|
+
const published = provider.RecordInvalidation({
|
|
187
|
+
entityName: 'MJ: Entities',
|
|
188
|
+
primaryKeyValues: JSON.stringify([{ FieldName: 'ID', Value: entityInfo!.ID }]),
|
|
189
|
+
action: 'save',
|
|
190
|
+
sourceServerId: 'it-server-node-1',
|
|
191
|
+
timestamp: new Date(),
|
|
192
|
+
recordData: JSON.stringify({ ID: entityInfo!.ID, Name: entityInfo!.Name }),
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
expect(published.sequence).toBe(baseSeq + 1);
|
|
196
|
+
expect(provider.CurrentSequence).toBe(baseSeq + 1);
|
|
197
|
+
|
|
198
|
+
// Verify delta catch-up shapes payload for sysUser against real MJ: Entities metadata
|
|
199
|
+
const catchUp = provider.GetChangesSince(baseSeq, sysUser);
|
|
200
|
+
expect(catchUp.RequiresFullRefresh).toBe(false);
|
|
201
|
+
expect(catchUp.CurrentSequence).toBe(baseSeq + 1);
|
|
202
|
+
expect(catchUp.Events).toHaveLength(1);
|
|
203
|
+
expect(catchUp.Events[0].EntityName).toBe('MJ: Entities');
|
|
204
|
+
expect(catchUp.Events[0].Sequence).toBe(baseSeq + 1);
|
|
205
|
+
expect(catchUp.Events[0].RecordData).toContain(entityInfo!.Name);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
@@ -43,6 +43,7 @@ function buildSourceUser(): UserInfo {
|
|
|
43
43
|
user.ReturningVisitorContext = { VisitorKey: 'vk-1' };
|
|
44
44
|
user.WidgetGuestContext = { WidgetID: 'W0000000-0000-0000-0000-000000000001' };
|
|
45
45
|
user.IsMagicLinkAnonymous = true;
|
|
46
|
+
user.AnonymousVisitorID = 'visitor-sid-1';
|
|
46
47
|
return user;
|
|
47
48
|
}
|
|
48
49
|
|
|
@@ -99,13 +100,14 @@ describe('CloneUserForSessionContext', () => {
|
|
|
99
100
|
expect(clone.APIKeyRowFilters).toHaveLength(1);
|
|
100
101
|
});
|
|
101
102
|
|
|
102
|
-
it('preserves ReturningVisitorContext, WidgetGuestContext, and
|
|
103
|
+
it('preserves ReturningVisitorContext, WidgetGuestContext, IsMagicLinkAnonymous, and AnonymousVisitorID', () => {
|
|
103
104
|
const source = buildSourceUser();
|
|
104
105
|
const clone = CloneUserForSessionContext(source);
|
|
105
106
|
|
|
106
107
|
expect(clone.ReturningVisitorContext).toEqual({ VisitorKey: 'vk-1' });
|
|
107
108
|
expect(clone.WidgetGuestContext).toEqual({ WidgetID: 'W0000000-0000-0000-0000-000000000001' });
|
|
108
109
|
expect(clone.IsMagicLinkAnonymous).toBe(true);
|
|
110
|
+
expect(clone.AnonymousVisitorID).toBe('visitor-sid-1');
|
|
109
111
|
});
|
|
110
112
|
|
|
111
113
|
it('stamping new contexts on the clone does NOT write through to the source', () => {
|
|
@@ -120,6 +122,7 @@ describe('CloneUserForSessionContext', () => {
|
|
|
120
122
|
clone.APIKeyActingContext = { ActingOrganizationID: 'ORG-OTHER' };
|
|
121
123
|
clone.APIKeyRowFilters = newBindings;
|
|
122
124
|
clone.IsMagicLinkAnonymous = false;
|
|
125
|
+
clone.AnonymousVisitorID = 'visitor-sid-OTHER';
|
|
123
126
|
|
|
124
127
|
// The source (potentially the shared UserCache instance) must be untouched
|
|
125
128
|
expect(source.TenantContext).toEqual({ TenantID: 'tenant-1', Source: 'header' });
|
|
@@ -128,6 +131,7 @@ describe('CloneUserForSessionContext', () => {
|
|
|
128
131
|
expect(source.APIKeyRowFilters).toHaveLength(1);
|
|
129
132
|
expect(source.APIKeyRowFilters?.[0].FilterID).toBe('F0000000-0000-0000-0000-000000000001');
|
|
130
133
|
expect(source.IsMagicLinkAnonymous).toBe(true);
|
|
134
|
+
expect(source.AnonymousVisitorID).toBe('visitor-sid-1');
|
|
131
135
|
});
|
|
132
136
|
|
|
133
137
|
it('clearing contexts on the clone does NOT clear them on the source', () => {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `SweepInterruptedRuns` closes out run artifacts a dead process left open. It is only sound
|
|
3
|
+
* BEFORE anything in this process can start a run: a run started first is still in flight when
|
|
4
|
+
* the sweep looks, so the sweep marks a live run "killed" and the wizard and active-operations
|
|
5
|
+
* panel report a failure for work that is still going.
|
|
6
|
+
*
|
|
7
|
+
* Booting the server is far too heavy for a unit test, so this pins the order in the source:
|
|
8
|
+
* the sweep is awaited before the scheduler, the integration sync worker (whose Start() polls
|
|
9
|
+
* immediately), the HTTP listener, and pending RSU work.
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { describe, expect, it } from 'vitest';
|
|
14
|
+
|
|
15
|
+
const source = readFileSync(join(__dirname, '..', 'index.ts'), 'utf8')
|
|
16
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
17
|
+
.replace(/^\s*\/\/.*$/gm, '');
|
|
18
|
+
|
|
19
|
+
function at(needle: string): number {
|
|
20
|
+
const i = source.indexOf(needle);
|
|
21
|
+
expect(i, `not found in index.ts: ${needle}`).toBeGreaterThan(-1);
|
|
22
|
+
return i;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('startup: the interrupted-run sweep runs before anything can start a run', () => {
|
|
26
|
+
const sweep = () => at('await SweepInterruptedRuns(');
|
|
27
|
+
|
|
28
|
+
it.each([
|
|
29
|
+
['the scheduled jobs service', 'scheduledJobsService.Start()'],
|
|
30
|
+
['the integration sync worker', 'integrationSyncWorker.Start()'],
|
|
31
|
+
['the HTTP listener', 'httpServer.listen('],
|
|
32
|
+
['pending RSU work', 'processRSUPendingWork().catch('],
|
|
33
|
+
])('precedes %s', (_label, needle) => {
|
|
34
|
+
expect(sweep()).toBeLessThan(at(needle));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('is called exactly once', () => {
|
|
38
|
+
expect(source.split('await SweepInterruptedRuns(').length - 1).toBe(1);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -263,6 +263,34 @@ describe('createUnifiedAuthMiddleware', () => {
|
|
|
263
263
|
});
|
|
264
264
|
});
|
|
265
265
|
|
|
266
|
+
describe('x-mj-timezone header', () => {
|
|
267
|
+
beforeEach(() => {
|
|
268
|
+
setupSuccessfulAuth();
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
async function payloadFor(headers: Record<string, string>) {
|
|
272
|
+
const req = makeMockReq({ headers: { authorization: 'Bearer valid-token', ...headers } });
|
|
273
|
+
await middleware(req, makeMockRes(), vi.fn());
|
|
274
|
+
return req.userPayload!;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
it('carries a valid IANA zone on the user payload', async () => {
|
|
278
|
+
expect((await payloadFor({ 'x-mj-timezone': 'Pacific/Auckland' })).TimeZone).toBe('Pacific/Auckland');
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('drops an invalid zone, so callers fall back to UTC', async () => {
|
|
282
|
+
expect((await payloadFor({ 'x-mj-timezone': 'Mars/Olympus_Mons' })).TimeZone).toBeUndefined();
|
|
283
|
+
expect((await payloadFor({ 'x-mj-timezone': "UTC'; DROP TABLE x;--" })).TimeZone).toBeUndefined();
|
|
284
|
+
expect((await payloadFor({ 'x-mj-timezone': '' })).TimeZone).toBeUndefined();
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('leaves the payload exactly as before when the header is absent', async () => {
|
|
288
|
+
const payload = await payloadFor({});
|
|
289
|
+
expect(payload.TimeZone).toBeUndefined();
|
|
290
|
+
expect(Object.keys(payload)).not.toContain('TimeZone');
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
266
294
|
describe('Authentication failures', () => {
|
|
267
295
|
it('should return 401 when no authorization header is provided, quietly (no stack-trace log)', async () => {
|
|
268
296
|
mockGetReadOnlyDS.mockReturnValue(MOCK_POOL);
|
|
@@ -155,6 +155,8 @@ describe('widgetGuestElevation — elevateUserPayload', () => {
|
|
|
155
155
|
expect(elevated.isSystemUser).toBe(true);
|
|
156
156
|
// The session id MUST be preserved so progress/streaming still routes to the guest's websocket.
|
|
157
157
|
expect(elevated.sessionId).toBe('sess-123');
|
|
158
|
+
// The guest it stands in for, so conversation writes can stay the guest's (MJ#4791).
|
|
159
|
+
expect(elevated.ElevatedFrom).toBe(guestPayload.userRecord);
|
|
158
160
|
});
|
|
159
161
|
|
|
160
162
|
it('does not mutate the original guest payload', () => {
|
|
@@ -647,9 +647,9 @@ export class SessionJanitor extends BaseSingleton<SessionJanitor> implements ISh
|
|
|
647
647
|
|
|
648
648
|
/**
|
|
649
649
|
* Gives a status-less session with ledger shards back a `Pending` status so it can be claimed (the
|
|
650
|
-
* claim only takes Pending/Partial/stale Assembling).
|
|
651
|
-
* the
|
|
652
|
-
*
|
|
650
|
+
* claim only takes Pending/Partial/stale Assembling). That is how statuses were lost before #6010
|
|
651
|
+
* made the ledger columns not updatable through an entity save (a stale `Save` of the session, e.g. its
|
|
652
|
+
* close, put back the NULL status and counts). The restore stays for rows hit before then.
|
|
653
653
|
*
|
|
654
654
|
* The terminal proc is the existing way to set a NULL status to `Pending`. It is stamped with the
|
|
655
655
|
* counts the session still stores, or 0 for a NULL count (the proc accepts any count over NULL), and
|
|
@@ -13,6 +13,7 @@ import { WriteReturningVisitorRecap } from './ReturningVisitorRecap.js';
|
|
|
13
13
|
import { SessionRunIDVerifier, SessionRunIDs } from './SessionRunIDVerifier.js';
|
|
14
14
|
import { ResolveScopedAnonymousRunUser } from '../realtimeWidget/widgetGuestElevation.js';
|
|
15
15
|
import { RecordingAssemblyScheduler } from '../realtimeSessions/RecordingAssemblyScheduler.js';
|
|
16
|
+
import { ResolveSessionScopeIdentity } from '../realtimeSessions/sessionAccess.js';
|
|
16
17
|
|
|
17
18
|
/** Entity names — centralised so the `MJ:`-prefix convention is applied in exactly one place. */
|
|
18
19
|
const SESSION_ENTITY = 'MJ: AI Agent Sessions';
|
|
@@ -153,11 +154,13 @@ export class SessionManager {
|
|
|
153
154
|
/**
|
|
154
155
|
* Authorize and create a new session. Flow:
|
|
155
156
|
* 1. `CanRun` check on the agent — denied → {@link SessionAuthorizationError}, no row written.
|
|
156
|
-
* 2. Resolve the conversation (use the supplied one, or create+link a fresh Conversation).
|
|
157
|
+
* 2. Resolve the conversation (use the supplied one, or create+link a fresh Conversation). An anonymous
|
|
158
|
+
* caller may only supply a conversation stamped with its own scope identity.
|
|
157
159
|
* 3. Persist the `AIAgentSession` as `Active`, stamped with this host and `LastActiveAt = now`.
|
|
158
160
|
*
|
|
159
161
|
* @returns the saved {@link MJAIAgentSessionEntity}.
|
|
160
|
-
* @throws {SessionAuthorizationError} when the user may not run the agent
|
|
162
|
+
* @throws {SessionAuthorizationError} when the user may not run the agent, or an anonymous caller supplies a
|
|
163
|
+
* conversation that is not its own.
|
|
161
164
|
* @throws {Error} when conversation creation or session save fails.
|
|
162
165
|
*/
|
|
163
166
|
public async CreateSession(
|
|
@@ -166,6 +169,9 @@ export class SessionManager {
|
|
|
166
169
|
provider: IMetadataProvider,
|
|
167
170
|
): Promise<MJAIAgentSessionEntity> {
|
|
168
171
|
await this.assertCanRun(input.agentID, contextUser);
|
|
172
|
+
if (input.conversationID && contextUser.IsMagicLinkAnonymous) {
|
|
173
|
+
await this.assertAnonymousOwnsConversation(input.conversationID, contextUser, provider);
|
|
174
|
+
}
|
|
169
175
|
|
|
170
176
|
const conversationID = input.conversationID
|
|
171
177
|
?? await this.createConversation(input, contextUser, provider);
|
|
@@ -448,6 +454,23 @@ export class SessionManager {
|
|
|
448
454
|
|
|
449
455
|
// ----- internals -------------------------------------------------------------------------
|
|
450
456
|
|
|
457
|
+
/**
|
|
458
|
+
* Throws {@link SessionAuthorizationError} unless the anonymous caller owns the conversation it wants to bind a
|
|
459
|
+
* new session to: the conversation, read as the caller, loads, and its `ExternalID` equals the caller's scope
|
|
460
|
+
* identity ({@link ResolveSessionScopeIdentity}). A caller with no scope identity is refused. Every anonymous
|
|
461
|
+
* visitor shares one Anonymous user, so row-level security alone may let one visitor read another's
|
|
462
|
+
* conversation; without this check the new session row and its relays would point into that conversation.
|
|
463
|
+
*/
|
|
464
|
+
private async assertAnonymousOwnsConversation(conversationID: string, contextUser: UserInfo, provider: IMetadataProvider): Promise<void> {
|
|
465
|
+
const conversation = await provider.GetEntityObject<MJConversationEntity>(CONVERSATION_ENTITY, contextUser);
|
|
466
|
+
const loaded = await conversation.Load(conversationID);
|
|
467
|
+
const scopeIdentity = ResolveSessionScopeIdentity(contextUser);
|
|
468
|
+
if (!loaded || !scopeIdentity || conversation.ExternalID !== scopeIdentity) {
|
|
469
|
+
LogError(`SessionManager.CreateSession: refused anonymous user ${contextUser.ID} binding a session to conversation ${conversationID} (${loaded ? 'not its own' : 'not readable'}).`);
|
|
470
|
+
throw new SessionAuthorizationError('Not authorized to start a session on this conversation.');
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
451
474
|
/** Throws {@link SessionAuthorizationError} unless the user has `CanRun` on the agent. */
|
|
452
475
|
private async assertCanRun(agentID: string, contextUser: UserInfo): Promise<void> {
|
|
453
476
|
const canRun = await AIAgentPermissionHelper.HasPermission(agentID, contextUser, 'run');
|
|
@@ -468,13 +491,13 @@ export class SessionManager {
|
|
|
468
491
|
conversation.NewRecord();
|
|
469
492
|
conversation.UserID = input.userID;
|
|
470
493
|
conversation.Name = 'Agent Session';
|
|
471
|
-
//
|
|
472
|
-
//
|
|
473
|
-
//
|
|
474
|
-
//
|
|
475
|
-
|
|
476
|
-
if (
|
|
477
|
-
conversation.ExternalID =
|
|
494
|
+
// Stamp the scope identity AuthorizeSessionAccess compares against (one rule, one place:
|
|
495
|
+
// ResolveSessionScopeIdentity) — the token's resource scope when it has one, else an anonymous
|
|
496
|
+
// visitor's mj_sid. This isolates anonymous visitors that share the Anonymous UserID, and keeps
|
|
497
|
+
// the {{ScopeResourceID}} RLS filters matching the visitor's own rows. No-op for unscoped named principals.
|
|
498
|
+
const scopeIdentity = ResolveSessionScopeIdentity(contextUser);
|
|
499
|
+
if (scopeIdentity) {
|
|
500
|
+
conversation.ExternalID = scopeIdentity;
|
|
478
501
|
}
|
|
479
502
|
|
|
480
503
|
// Returning-visitor memory (RV1/RV2/RV4) for the VOICE path: the widget guest token carries the
|
|
@@ -539,9 +562,9 @@ export class SessionManager {
|
|
|
539
562
|
}
|
|
540
563
|
|
|
541
564
|
/**
|
|
542
|
-
* Loads a session entity by ID, returning null when it cannot be loaded. The
|
|
543
|
-
*
|
|
544
|
-
*
|
|
565
|
+
* Loads a session entity by ID, returning null when it cannot be loaded. The lifecycle saves that
|
|
566
|
+
* follow cannot write back the recording ledger's columns: those have `AllowUpdateAPI=0`, so they are
|
|
567
|
+
* not parameters of `spUpdateAIAgentSession` (#5294, #6010).
|
|
545
568
|
*/
|
|
546
569
|
private async loadSession(
|
|
547
570
|
agentSessionID: string,
|
|
@@ -13,6 +13,7 @@ import { rateLimit } from 'express-rate-limit';
|
|
|
13
13
|
import { LogError, type PublicAuthProviderCatalog } from '@memberjunction/core';
|
|
14
14
|
import { AuthProviderEngine } from './AuthProviderEngine.js';
|
|
15
15
|
import { EMPTY_PUBLIC_LOGIN_SCREEN, LoginScreenEngine } from './LoginScreenEngine.js';
|
|
16
|
+
import { AttachLocalDevAuthRoutes } from './localDevAuth.js';
|
|
16
17
|
|
|
17
18
|
/** Mount path for the public catalog router. */
|
|
18
19
|
export const AUTH_CATALOG_MOUNT_PATH = '/auth';
|
|
@@ -98,6 +99,8 @@ export function CreateAuthProviderCatalogRouter(): Router {
|
|
|
98
99
|
}
|
|
99
100
|
});
|
|
100
101
|
|
|
102
|
+
AttachLocalDevAuthRoutes(router);
|
|
103
|
+
|
|
101
104
|
return router;
|
|
102
105
|
}
|
|
103
106
|
|
package/src/auth/index.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { JwtHeader, SigningKeyCallback, JwtPayload } from 'jsonwebtoken';
|
|
|
2
2
|
import { configInfo } from '../config.js';
|
|
3
3
|
import { UserCache } from '@memberjunction/generic-database-provider';
|
|
4
4
|
import sql from 'mssql';
|
|
5
|
-
import { DatabaseProviderBase, Metadata, RoleInfo, UserInfo } from '@memberjunction/core';
|
|
5
|
+
import { DatabaseProviderBase, LogError, Metadata, RoleInfo, UserInfo } from '@memberjunction/core';
|
|
6
6
|
import { NewUserBase } from './newUsers.js';
|
|
7
7
|
import { MJGlobal } from '@memberjunction/global';
|
|
8
8
|
import { MJUserEntity, MJUserEntityType } from '@memberjunction/core-entities';
|
|
@@ -254,11 +254,63 @@ export const VerifyUserRecord = async (
|
|
|
254
254
|
): Promise<UserInfo | undefined> => {
|
|
255
255
|
if (!email) return undefined;
|
|
256
256
|
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
|
|
257
|
+
// Collect ALL matches rather than taking .find()'s first hit. Two User rows can share an email
|
|
258
|
+
// case-insensitively (see the duplicate-provisioning path below), and .find() then resolved to
|
|
259
|
+
// whichever row happened to sit earlier in UserCache.Instance.Users — i.e. database row order.
|
|
260
|
+
// That order changes when the cache is rebuilt, so an MJAPI restart silently moved a user onto a
|
|
261
|
+
// different record: their conversations, artifacts, items shared with them and their profile
|
|
262
|
+
// picture (UserImageURL lives on the User row) all vanished at once, and came back if the order
|
|
263
|
+
// flipped again. Observed in production 2026-09-15. Resolution must not depend on row order.
|
|
264
|
+
const emailMatches = UserCache.Instance.Users.filter((u) => {
|
|
265
|
+
if (!u.Email || u.Email.trim() === '') {
|
|
266
|
+
// this condition should never occur. If it doesn throw a console error including the user id
|
|
267
|
+
// DB requires non-null but this is just an extra check and we could in theory have a blank string in the DB
|
|
268
|
+
console.error(`SYSTEM METADATA ISSUE: User ${u.ID} has no email address`);
|
|
269
|
+
return false;
|
|
270
|
+
} else return u.Email.toLowerCase().trim() === email.toLowerCase().trim();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
if (emailMatches.length > 1) {
|
|
274
|
+
// Deterministic tie-break by ID so every process and every restart agrees, and a LOUD error
|
|
275
|
+
// because this is a data problem an operator has to resolve — silently picking a row is what
|
|
276
|
+
// made it look like data loss to the customer. Sorting by ID is arbitrary but stable; the point
|
|
277
|
+
// is that it never changes, not that it picks the "right" one.
|
|
278
|
+
emailMatches.sort((a, b) => a.ID.localeCompare(b.ID));
|
|
279
|
+
LogError(
|
|
280
|
+
`AMBIGUOUS LOGIN: ${emailMatches.length} User records share the email '${email}' ` +
|
|
281
|
+
`(case-insensitively): ${emailMatches.map((u) => `${u.ID} (${u.Email})`).join(', ')}. ` +
|
|
282
|
+
`Resolving to ${emailMatches[0].ID} by stable ID order. Until the duplicates are merged, ` +
|
|
283
|
+
`content owned by the other records is invisible to this login. Find every affected user ` +
|
|
284
|
+
`with: SELECT LOWER(Email), COUNT(*) FROM __mj.[User] GROUP BY LOWER(Email) HAVING COUNT(*) > 1`
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Nothing cached: ask FindUser, which reads the database for a user this process has not loaded
|
|
289
|
+
// yet (created seconds ago on another server) instead of treating them as nonexistent.
|
|
290
|
+
let user = emailMatches[0] ?? (await UserCache.Instance.FindUser({ Email: email }));
|
|
260
291
|
|
|
261
292
|
if (!user) {
|
|
293
|
+
// REFRESH THE CACHE BEFORE PROVISIONING. This block used to sit AFTER the auto-create below,
|
|
294
|
+
// gated on `!user`, so it could only ever run when creation had already failed or was not
|
|
295
|
+
// attempted. That made it dead code in the exact case it exists to handle: a user who IS in the
|
|
296
|
+
// database but is NOT yet in this process's UserCache. Users provisioned out of band (an admin,
|
|
297
|
+
// an invite, a direct insert) are absent from the cache until it is rebuilt, so the lookup above
|
|
298
|
+
// missed and auto-create minted a SECOND record for someone who already had one — which is how
|
|
299
|
+
// the duplicate emails that Fix 1 above has to tolerate get created in the first place.
|
|
300
|
+
// Observed in production: three users created 2026-09-14 18:18, a duplicate for one of them
|
|
301
|
+
// created 2026-09-15 01:38 on that user's next sign-in.
|
|
302
|
+
//
|
|
303
|
+
// The recursive call passes attemptCacheUpdateIfNeeded=false, so the second pass skips straight
|
|
304
|
+
// to auto-create if the refreshed cache still has no match. Auto-provisioning of genuinely new
|
|
305
|
+
// users is therefore unchanged — it just no longer runs before we have looked properly.
|
|
306
|
+
if (configInfo.userHandling.updateCacheWhenNotFound && dataSource && attemptCacheUpdateIfNeeded) {
|
|
307
|
+
console.warn(`User ${email} not found in cache. Updating cache in attempt to find the user...`);
|
|
308
|
+
|
|
309
|
+
await refreshUserCache();
|
|
310
|
+
|
|
311
|
+
return VerifyUserRecord(email, firstName, lastName, requestDomain, dataSource, false);
|
|
312
|
+
}
|
|
313
|
+
|
|
262
314
|
// NOTE: `requestDomain` (parsed from the spoofable `Origin` header) is deliberately NOT part of
|
|
263
315
|
// this condition. It was previously required here, which meant a non-browser client sending no
|
|
264
316
|
// Origin could never auto-provision while an attacker simply forged one — it gated entry without
|
|
@@ -313,14 +365,6 @@ export const VerifyUserRecord = async (
|
|
|
313
365
|
}
|
|
314
366
|
}
|
|
315
367
|
|
|
316
|
-
if (!user && configInfo.userHandling.updateCacheWhenNotFound && dataSource && attemptCacheUpdateIfNeeded) {
|
|
317
|
-
// if we get here that means in the above, if we were attempting to create a new user, it did not work, or it wasn't attempted and we have a config that asks us to auto update the cache
|
|
318
|
-
console.warn(`User ${email} not found in cache. Updating cache in attempt to find the user...`);
|
|
319
|
-
|
|
320
|
-
await refreshUserCache();
|
|
321
|
-
|
|
322
|
-
return VerifyUserRecord(email, firstName, lastName, requestDomain, dataSource, false); // try one more time but do not update cache next time if not found
|
|
323
|
-
}
|
|
324
368
|
}
|
|
325
369
|
|
|
326
370
|
return user;
|
|
@@ -1,8 +1,35 @@
|
|
|
1
1
|
import { configInfo } from '../config.js';
|
|
2
2
|
import { AuthProviderConfig, LogError, LogStatus, LogStatusEx, UserInfo, type IMetadataProvider } from '@memberjunction/core';
|
|
3
|
-
import { AuthProviderFactory } from '@memberjunction/auth-providers';
|
|
3
|
+
import { AuthProviderFactory, LocalDevProvider } from '@memberjunction/auth-providers';
|
|
4
4
|
import { AuthProviderEngine } from './AuthProviderEngine.js';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Returns true when local development auth is enabled via `MJ_DEV_AUTH_ENABLED=true`
|
|
8
|
+
* or `configInfo.devAuth?.enabled === true`.
|
|
9
|
+
*/
|
|
10
|
+
export function IsLocalDevAuthEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
11
|
+
const rawEnv = env.MJ_DEV_AUTH_ENABLED?.trim().toLowerCase();
|
|
12
|
+
if (rawEnv === 'true' || rawEnv === '1') {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
if (rawEnv === 'false' || rawEnv === '0') {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
return configInfo?.devAuth?.enabled === true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Enforces the production safety invariant: throws a fatal error if local dev auth
|
|
23
|
+
* is enabled while `NODE_ENV === 'production'`.
|
|
24
|
+
*/
|
|
25
|
+
export function AssertLocalDevAuthAllowed(env: NodeJS.ProcessEnv = process.env): void {
|
|
26
|
+
if (IsLocalDevAuthEnabled(env) && env.NODE_ENV?.trim().toLowerCase() === 'production') {
|
|
27
|
+
throw new Error(
|
|
28
|
+
'[SECURITY] Local dev auth (MJ_DEV_AUTH_ENABLED) is strictly forbidden when NODE_ENV=production. Refusing to start.'
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
6
33
|
/**
|
|
7
34
|
* Initialize authentication providers from configuration.
|
|
8
35
|
*
|
|
@@ -15,6 +42,8 @@ import { AuthProviderEngine } from './AuthProviderEngine.js';
|
|
|
15
42
|
* connection and a system user exist.
|
|
16
43
|
*/
|
|
17
44
|
export function InitializeAuthProviders(): void {
|
|
45
|
+
AssertLocalDevAuthAllowed();
|
|
46
|
+
|
|
18
47
|
const factory = AuthProviderFactory.Instance;
|
|
19
48
|
|
|
20
49
|
// Clear any existing providers
|
|
@@ -40,6 +69,19 @@ export function InitializeAuthProviders(): void {
|
|
|
40
69
|
}
|
|
41
70
|
}
|
|
42
71
|
|
|
72
|
+
if (IsLocalDevAuthEnabled()) {
|
|
73
|
+
configInfo.authProviders = configInfo.authProviders ?? [];
|
|
74
|
+
if (!configInfo.authProviders.some((p) => p.type === 'local' || p.name === 'local')) {
|
|
75
|
+
const localConfig = LocalDevProvider.ConfigFromEnvironment({
|
|
76
|
+
...process.env,
|
|
77
|
+
MJ_DEV_AUTH_ENABLED: 'true',
|
|
78
|
+
});
|
|
79
|
+
if (localConfig) {
|
|
80
|
+
configInfo.authProviders.push(localConfig);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
43
85
|
// Initialize providers from authProviders config
|
|
44
86
|
if (configInfo.authProviders && configInfo.authProviders.length > 0) {
|
|
45
87
|
for (const providerConfig of configInfo.authProviders) {
|