@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,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Field-level security on ResolverBase's own transports, other than the GraphQL field mapping:
|
|
3
|
+
*
|
|
4
|
+
* - **Create input.** A restricted caller's create payload is refused when it names a field the
|
|
5
|
+
* caller may not read, or a key that names no field, with the same message for both.
|
|
6
|
+
* - **Cache-invalidation broadcast.** The event reaches every signed-in session unprojected, so an
|
|
7
|
+
* entity under field security never carries its row, whatever the allowlist says.
|
|
8
|
+
* - **CloudEvents and their debug log.** The sink is external and has no reader identity to
|
|
9
|
+
* project for, so an entity under field security emits only its key and the names of changed
|
|
10
|
+
* fields, and the same goes for the debug log of the event.
|
|
11
|
+
*
|
|
12
|
+
* The entity objects here return FULL-WIDTH rows, standing in for an object saved by the system
|
|
13
|
+
* user (server-internal saves fire the same events) or otherwise not narrowed at its source.
|
|
14
|
+
*/
|
|
15
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
16
|
+
|
|
17
|
+
const { published, logDebugCalls, emitted, cloudEventInits } = vi.hoisted(() => ({
|
|
18
|
+
published: [] as Array<{ topic: string; payload: Record<string, unknown> }>,
|
|
19
|
+
logDebugCalls: [] as string[],
|
|
20
|
+
emitted: [] as unknown[],
|
|
21
|
+
cloudEventInits: [] as Array<Record<string, unknown>>,
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
vi.mock('@memberjunction/sqlserver-dataprovider', () => ({ SQLServerDataProvider: class {} }));
|
|
25
|
+
vi.mock('@memberjunction/generic-database-provider', () => ({ UserCache: { Users: [] } }));
|
|
26
|
+
vi.mock('cloudevents', () => ({
|
|
27
|
+
CloudEvent: class {
|
|
28
|
+
constructor(init: Record<string, unknown>) {
|
|
29
|
+
cloudEventInits.push(init);
|
|
30
|
+
Object.assign(this, init);
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
httpTransport: () => () => undefined,
|
|
34
|
+
emitterFor: () => () => undefined,
|
|
35
|
+
}));
|
|
36
|
+
vi.mock('type-graphql', () => {
|
|
37
|
+
const noop = () => () => undefined;
|
|
38
|
+
return {
|
|
39
|
+
Resolver: noop, Mutation: noop, Query: noop, Subscription: noop, Ctx: noop, Arg: noop, PubSub: noop,
|
|
40
|
+
Root: noop, ObjectType: noop, InputType: noop, Field: noop, FieldResolver: noop,
|
|
41
|
+
Int: undefined, Float: undefined, registerEnumType: () => undefined,
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
vi.mock('graphql', () => ({ GraphQLError: class extends Error {} }));
|
|
45
|
+
vi.mock('mssql', () => ({}));
|
|
46
|
+
vi.mock('@memberjunction/api-keys', () => ({ GetAPIKeyEngine: vi.fn() }));
|
|
47
|
+
vi.mock('@memberjunction/encryption', () => ({ EncryptionEngine: { Instance: {} } }));
|
|
48
|
+
vi.mock('@memberjunction/graphql-dataprovider', () => ({ FieldMapper: class { MapFieldName(n: string) { return n; } ReverseMapFieldName(n: string) { return n; } } }));
|
|
49
|
+
vi.mock('../generic/PubSubManager.js', () => ({
|
|
50
|
+
PubSubManager: {
|
|
51
|
+
Instance: {
|
|
52
|
+
Publish: (topic: string, payload: Record<string, unknown>) => { published.push({ topic, payload }); },
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
}));
|
|
56
|
+
vi.mock('../generic/PushStatusResolver.js', () => ({
|
|
57
|
+
PUSH_STATUS_UPDATES_TOPIC: 'test-push-topic',
|
|
58
|
+
PublishStatusUpdate: vi.fn(),
|
|
59
|
+
}));
|
|
60
|
+
vi.mock('../generic/RunViewResolver.js', () => ({ RunViewByIDInput: class {}, RunViewByNameInput: class {}, RunDynamicViewInput: class {} }));
|
|
61
|
+
vi.mock('../generic/DeleteOptionsInput.js', () => ({ DeleteOptionsInput: class {} }));
|
|
62
|
+
vi.mock('../types.js', () => ({ RunViewGenericParams: class {} }));
|
|
63
|
+
vi.mock('@memberjunction/core-entities', () => ({}));
|
|
64
|
+
vi.mock('@memberjunction/core', async () => {
|
|
65
|
+
const actual = await vi.importActual<typeof import('@memberjunction/core')>('@memberjunction/core');
|
|
66
|
+
return {
|
|
67
|
+
...actual,
|
|
68
|
+
LogError: vi.fn(),
|
|
69
|
+
LogStatus: (msg: string) => { logDebugCalls.push(String(msg)); },
|
|
70
|
+
LogDebug: (msg: string) => { logDebugCalls.push(String(msg)); },
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
import { BaseEntity, BaseEntityEvent, EntityInfo, UserInfo } from '@memberjunction/core';
|
|
75
|
+
import { MJEventType, MJGlobal } from '@memberjunction/global';
|
|
76
|
+
import type { PubSubEngine } from 'type-graphql';
|
|
77
|
+
import { ResolverBase } from '../generic/ResolverBase';
|
|
78
|
+
import { ConfigureRecordDataBroadcast } from '../generic/CacheInvalidationResolver.js';
|
|
79
|
+
import type { UserPayload } from '../types.js';
|
|
80
|
+
import { EmployeeEntity, FullWidthEmployeeRow, HRUser, Intern, RefusalFor, SECRET_SALARY } from './fieldSecurityFixtures.js';
|
|
81
|
+
|
|
82
|
+
/** An entity object that holds a full-width row: system-saved, or not narrowed at its source. */
|
|
83
|
+
function fullWidthEntity(entityInfo: EntityInfo, dirty: string[] = ['Notes']) {
|
|
84
|
+
const row = FullWidthEmployeeRow();
|
|
85
|
+
return {
|
|
86
|
+
EntityInfo: entityInfo,
|
|
87
|
+
GetAll: () => ({ ...row }),
|
|
88
|
+
PrimaryKey: { KeyValuePairs: [{ FieldName: 'ID', Value: row.ID }] },
|
|
89
|
+
PrimaryKeys: [],
|
|
90
|
+
Fields: entityInfo.Fields.map(f => ({ Name: f.Name, Dirty: dirty.includes(f.Name) })),
|
|
91
|
+
Get: (name: string) => (row as Record<string, unknown>)[name],
|
|
92
|
+
GetDirtyFieldNames: () => entityInfo.Fields.filter(f => dirty.includes(f.Name)).map(f => f.Name),
|
|
93
|
+
SetManyCalls: [] as Array<Record<string, unknown>>,
|
|
94
|
+
NewRecord(): boolean { return true; },
|
|
95
|
+
SetMany(values: Record<string, unknown>): void { this.SetManyCalls.push(values); },
|
|
96
|
+
Save: async () => true,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
type FullWidthEntity = ReturnType<typeof fullWidthEntity>;
|
|
100
|
+
|
|
101
|
+
class TestResolver extends ResolverBase {
|
|
102
|
+
public Create(entity: FullWidthEntity, input: Record<string, unknown>, user: UserInfo) {
|
|
103
|
+
const provider = { GetEntityObject: async () => entity };
|
|
104
|
+
return this.CreateRecord('Employees', input, provider as never, { userRecord: user } as UserPayload, {} as PubSubEngine);
|
|
105
|
+
}
|
|
106
|
+
public Broadcast(entity: FullWidthEntity) {
|
|
107
|
+
this.PublishCacheInvalidation(entity as unknown as BaseEntity, 'save', { sessionId: 's' } as UserPayload);
|
|
108
|
+
}
|
|
109
|
+
public Emit(entity: FullWidthEntity) {
|
|
110
|
+
const args = new BaseEntityEvent();
|
|
111
|
+
args.type = 'save';
|
|
112
|
+
args.baseEntity = entity as unknown as BaseEntity;
|
|
113
|
+
return this.EmitCloudEvent({ component: null, event: MJEventType.ComponentEvent, eventCode: BaseEntity.BaseEventCode, args });
|
|
114
|
+
}
|
|
115
|
+
public Listen(entity: FullWidthEntity) {
|
|
116
|
+
this.ListenForEntityMessages(entity as unknown as BaseEntity, {} as PubSubEngine, { sessionId: 's' } as UserPayload);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
describe('ResolverBase.CreateRecord: client input is gated for a restricted caller', () => {
|
|
121
|
+
it.each([
|
|
122
|
+
['a denied field', 'Salary'],
|
|
123
|
+
['a key that names no field', 'Bogus'],
|
|
124
|
+
])('REGRESSION: refuses %s with the same message, before anything is applied', async (_label, key) => {
|
|
125
|
+
const entity = fullWidthEntity(EmployeeEntity());
|
|
126
|
+
await expect(new TestResolver().Create(entity, { ID: 'emp-1', Name: 'Ada', [key]: 1 }, Intern()))
|
|
127
|
+
.rejects.toThrow(RefusalFor(key));
|
|
128
|
+
expect(entity.SetManyCalls).toHaveLength(0);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('applies a well-behaved restricted client\'s create', async () => {
|
|
132
|
+
const entity = fullWidthEntity(EmployeeEntity());
|
|
133
|
+
// The response mapping needs metadata this unit test does not stand up; reaching it means the gate passed.
|
|
134
|
+
await new TestResolver().Create(entity, { ID: 'emp-1', Name: 'Ada', RestoreContext___: { SourceChangeID: 'x' } }, Intern()).catch(() => undefined);
|
|
135
|
+
expect(entity.SetManyCalls).toEqual([{ ID: 'emp-1', Name: 'Ada' }]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('does not gate a caller who is denied nothing', async () => {
|
|
139
|
+
const entity = fullWidthEntity(EmployeeEntity());
|
|
140
|
+
await new TestResolver().Create(entity, { ID: 'emp-1', Salary: 5 }, HRUser()).catch(() => undefined);
|
|
141
|
+
expect(entity.SetManyCalls).toEqual([{ ID: 'emp-1', Salary: 5 }]);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('ResolverBase.PublishCacheInvalidation: no row data for entities under field security', () => {
|
|
146
|
+
beforeEach(() => { published.length = 0; });
|
|
147
|
+
afterEach(() => ConfigureRecordDataBroadcast([]));
|
|
148
|
+
|
|
149
|
+
it("REGRESSION: an FLS entity's row never rides along, even under an explicit '*'", () => {
|
|
150
|
+
ConfigureRecordDataBroadcast(['*']);
|
|
151
|
+
new TestResolver().Broadcast(fullWidthEntity(EmployeeEntity(true)));
|
|
152
|
+
|
|
153
|
+
expect(published).toHaveLength(1);
|
|
154
|
+
expect(published[0].payload.recordData).toBeUndefined();
|
|
155
|
+
expect(published[0].payload.entityName).toBe('Employees');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('an allowlisted entity without field security still carries its row', () => {
|
|
159
|
+
ConfigureRecordDataBroadcast(['*']);
|
|
160
|
+
new TestResolver().Broadcast(fullWidthEntity(EmployeeEntity(false)));
|
|
161
|
+
|
|
162
|
+
expect(JSON.parse(String(published[0].payload.recordData))).toMatchObject({ ID: 'emp-1' });
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('ResolverBase CloudEvents: no field values for entities under field security', () => {
|
|
167
|
+
const emitterKey = '_emit';
|
|
168
|
+
let previousEmitter: unknown;
|
|
169
|
+
|
|
170
|
+
beforeEach(() => {
|
|
171
|
+
cloudEventInits.length = 0;
|
|
172
|
+
emitted.length = 0;
|
|
173
|
+
logDebugCalls.length = 0;
|
|
174
|
+
const statics = ResolverBase as unknown as Record<string, unknown>;
|
|
175
|
+
previousEmitter = statics[emitterKey];
|
|
176
|
+
statics[emitterKey] = async (event: unknown) => { emitted.push(event); return {}; };
|
|
177
|
+
});
|
|
178
|
+
afterEach(() => {
|
|
179
|
+
(ResolverBase as unknown as Record<string, unknown>)[emitterKey] = previousEmitter;
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('REGRESSION: emits only the key and the changed-field names', async () => {
|
|
183
|
+
await new TestResolver().Emit(fullWidthEntity(EmployeeEntity(true), ['Notes', 'Salary']));
|
|
184
|
+
|
|
185
|
+
expect(cloudEventInits).toHaveLength(1);
|
|
186
|
+
expect(cloudEventInits[0].data).toEqual({
|
|
187
|
+
PrimaryKey: [{ FieldName: 'ID', Value: 'emp-1' }],
|
|
188
|
+
ChangedFields: ['Salary', 'Notes'],
|
|
189
|
+
});
|
|
190
|
+
expect(logDebugCalls.join('\n')).not.toContain(String(SECRET_SALARY));
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('an entity without field security still emits its row', async () => {
|
|
194
|
+
await new TestResolver().Emit(fullWidthEntity(EmployeeEntity(false)));
|
|
195
|
+
expect(cloudEventInits[0].data).toMatchObject({ ID: 'emp-1', Name: 'Ada' });
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('REGRESSION: a Record Changes row emits only its key and changed-field names', async () => {
|
|
199
|
+
// The audit trail has no field security of its own, but its payload columns hold other
|
|
200
|
+
// entities' values, so its rows must not ride along to the sink either.
|
|
201
|
+
const recordChanges = EmployeeEntity(false);
|
|
202
|
+
recordChanges.Name = 'MJ: Record Changes';
|
|
203
|
+
await new TestResolver().Emit(fullWidthEntity(recordChanges, ['Notes']));
|
|
204
|
+
expect(cloudEventInits[0].data).toEqual({
|
|
205
|
+
PrimaryKey: [{ FieldName: 'ID', Value: 'emp-1' }],
|
|
206
|
+
ChangedFields: ['Notes'],
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('REGRESSION: the entity-event debug log carries no field values for an FLS entity', async () => {
|
|
211
|
+
const entity = fullWidthEntity(EmployeeEntity(true));
|
|
212
|
+
new TestResolver().Listen(entity);
|
|
213
|
+
const args = new BaseEntityEvent();
|
|
214
|
+
args.type = 'save';
|
|
215
|
+
args.baseEntity = entity as unknown as BaseEntity;
|
|
216
|
+
MJGlobal.Instance.RaiseEvent({ component: null, event: MJEventType.ComponentEvent, eventCode: BaseEntity.BaseEventCode, args });
|
|
217
|
+
await vi.waitFor(() => expect(logDebugCalls.some(m => m.includes('Received Event'))).toBe(true));
|
|
218
|
+
|
|
219
|
+
expect(logDebugCalls.join('\n')).not.toContain(String(SECRET_SALARY));
|
|
220
|
+
expect(logDebugCalls.join('\n')).toContain('emp-1');
|
|
221
|
+
});
|
|
222
|
+
});
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for ResolverBase.
|
|
3
|
-
*
|
|
2
|
+
* Tests for ResolverBase.AssertClientInputFieldSecurity — the field-level-security gate on the
|
|
3
|
+
* update path — and MustLoadTruthFromDatabase.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* it
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* load-truth-from-DB branch so denied fields hold real values a stripped SetMany never
|
|
12
|
-
* touches.
|
|
5
|
+
* A read-denied field is absent from every payload the client receives, so a value coming back
|
|
6
|
+
* for it can only be fabricated. The gate REFUSES such a value (new values and OldValues alike)
|
|
7
|
+
* rather than dropping it, and refuses a key that names no field with the identical message, so
|
|
8
|
+
* the refusal cannot be used to learn which columns exist. Its `true` return (the caller is
|
|
9
|
+
* denied something) forces the caller onto the load-truth-from-DB branch, so denied fields keep
|
|
10
|
+
* the values the database holds.
|
|
13
11
|
*/
|
|
14
12
|
import { describe, it, expect, vi, beforeEach, beforeAll, afterAll } from 'vitest';
|
|
15
13
|
|
|
@@ -116,7 +114,7 @@ vi.mock('@memberjunction/core-entities', () => ({}));
|
|
|
116
114
|
|
|
117
115
|
// ─── Import after mocks ──────────────────────────────────────────────────
|
|
118
116
|
import { ResolverBase } from '../generic/ResolverBase';
|
|
119
|
-
import { EntityInfo, Metadata, RowLevelSecurityFilterInfo, UserInfo, UserRoleInfo } from '@memberjunction/core';
|
|
117
|
+
import { EntityInfo, FieldSecurityClientInputMessage, IMetadataProvider, Metadata, RowLevelSecurityFilterInfo, UserInfo, UserRoleInfo } from '@memberjunction/core';
|
|
120
118
|
|
|
121
119
|
// ─── Metadata builders (mirrors MJCore's fieldSecurity.enforcement tests) ──
|
|
122
120
|
|
|
@@ -189,7 +187,7 @@ type ClientInput = { OldValues___?: Array<{ Key: string; Value: unknown }> } & R
|
|
|
189
187
|
/** Subclass that exposes the protected guards for testing. */
|
|
190
188
|
class TestResolver extends ResolverBase {
|
|
191
189
|
public TestStrip(entityInfo: EntityInfo, userInfo: UserInfo, input: ClientInput, clientNewValues: Record<string, unknown>): boolean {
|
|
192
|
-
return this.
|
|
190
|
+
return this.AssertClientInputFieldSecurity(entityInfo, userInfo, input, clientNewValues);
|
|
193
191
|
}
|
|
194
192
|
|
|
195
193
|
/** The user defaults to HR, whose permission rows carry no row filter; pass null for a missing user. */
|
|
@@ -210,7 +208,10 @@ function makeClientPayload(fields: Record<string, unknown>, oldValues?: Array<{
|
|
|
210
208
|
|
|
211
209
|
// ─── Tests ────────────────────────────────────────────────────────────────
|
|
212
210
|
|
|
213
|
-
|
|
211
|
+
/** The refusal for `key` on Employees: one wording for a denied key and for a key that names no field. */
|
|
212
|
+
const refusalFor = (key: string) => FieldSecurityClientInputMessage(key, 'Employees');
|
|
213
|
+
|
|
214
|
+
describe('ResolverBase.AssertClientInputFieldSecurity', () => {
|
|
214
215
|
let resolver: TestResolver;
|
|
215
216
|
let flsEntity: EntityInfo;
|
|
216
217
|
let noFlsEntity: EntityInfo;
|
|
@@ -221,93 +222,78 @@ describe('ResolverBase.StripDeniedReadFieldsFromClientInput', () => {
|
|
|
221
222
|
noFlsEntity = new EntityInfo(employeeEntityInit(false));
|
|
222
223
|
});
|
|
223
224
|
|
|
224
|
-
it('
|
|
225
|
+
it('REGRESSION: refuses a value for a denied-read field instead of silently dropping it', () => {
|
|
225
226
|
const { input, clientNewValues } = makeClientPayload({ ID: '1', Notes: 'edited', Salary: 0 });
|
|
226
|
-
|
|
227
|
+
expect(() => resolver.TestStrip(flsEntity, buildUser([INTERN_ROLE_ID]), input, clientNewValues))
|
|
228
|
+
.toThrow(refusalFor('Salary'));
|
|
229
|
+
});
|
|
227
230
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
231
|
+
it('REGRESSION: refuses a key that names no field with the SAME message as a denied one', () => {
|
|
232
|
+
const { input, clientNewValues } = makeClientPayload({ ID: '1', Notes: 'edited', Bogus: 0 });
|
|
233
|
+
let denied = '';
|
|
234
|
+
let unknown = '';
|
|
235
|
+
try { resolver.TestStrip(flsEntity, buildUser([INTERN_ROLE_ID]), makeClientPayload({ ID: '1', Salary: 0 }).input, { ID: '1', Salary: 0 }); }
|
|
236
|
+
catch (e) { denied = (e as Error).message; }
|
|
237
|
+
try { resolver.TestStrip(flsEntity, buildUser([INTERN_ROLE_ID]), input, clientNewValues); }
|
|
238
|
+
catch (e) { unknown = (e as Error).message; }
|
|
239
|
+
|
|
240
|
+
expect(unknown).toBe(refusalFor('Bogus'));
|
|
241
|
+
expect(denied.replace("'Salary'", "'X'")).toBe(unknown.replace("'Bogus'", "'X'"));
|
|
232
242
|
});
|
|
233
243
|
|
|
234
|
-
it('
|
|
244
|
+
it('REGRESSION: refuses a denied field named only in OldValues___', () => {
|
|
235
245
|
const { input, clientNewValues } = makeClientPayload(
|
|
236
|
-
{ ID: '1', Notes: 'edited'
|
|
246
|
+
{ ID: '1', Notes: 'edited' },
|
|
237
247
|
[{ Key: 'ID', Value: '1' }, { Key: 'Notes', Value: 'orig' }, { Key: 'Salary', Value: '0' }]
|
|
238
248
|
);
|
|
239
|
-
resolver.TestStrip(flsEntity, buildUser([INTERN_ROLE_ID]), input, clientNewValues)
|
|
240
|
-
|
|
241
|
-
expect(input.OldValues___).toEqual([{ Key: 'ID', Value: '1' }, { Key: 'Notes', Value: 'orig' }]);
|
|
249
|
+
expect(() => resolver.TestStrip(flsEntity, buildUser([INTERN_ROLE_ID]), input, clientNewValues))
|
|
250
|
+
.toThrow(refusalFor('Salary'));
|
|
242
251
|
});
|
|
243
252
|
|
|
244
|
-
it('
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
);
|
|
250
|
-
const result = resolver.TestStrip(flsEntity, buildUser([INTERN_ROLE_ID]), input, clientNewValues);
|
|
253
|
+
it('matches the CodeName of a denied field with a space in its name', () => {
|
|
254
|
+
const { input, clientNewValues } = makeClientPayload({ ID: '1', Notes: 'edited', Base_Salary: 100 });
|
|
255
|
+
expect(() => resolver.TestStrip(flsEntity, buildUser([INTERN_ROLE_ID]), input, clientNewValues))
|
|
256
|
+
.toThrow(refusalFor('Base_Salary'));
|
|
257
|
+
});
|
|
251
258
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
expect(
|
|
259
|
+
it('matches input keys case-insensitively (wire casing must not defeat the gate)', () => {
|
|
260
|
+
const { input, clientNewValues } = makeClientPayload({ ID: '1', SALARY: 0 });
|
|
261
|
+
expect(() => resolver.TestStrip(flsEntity, buildUser([INTERN_ROLE_ID]), input, clientNewValues))
|
|
262
|
+
.toThrow(refusalFor('SALARY'));
|
|
255
263
|
});
|
|
256
264
|
|
|
257
|
-
it('
|
|
265
|
+
it('accepts permitted fields and metadata blobs untouched, and returns true', () => {
|
|
258
266
|
const restore = { marker: true };
|
|
259
|
-
const { input, clientNewValues } = makeClientPayload({ ID: '1', Name: 'Ada', Notes: 'n',
|
|
267
|
+
const { input, clientNewValues } = makeClientPayload({ ID: '1', Name: 'Ada', Notes: 'n' }, [{ Key: 'Notes', Value: 'o' }]);
|
|
260
268
|
(input as Record<string, unknown>)['RestoreContext___'] = restore;
|
|
261
|
-
resolver.TestStrip(flsEntity, buildUser([INTERN_ROLE_ID]), input, clientNewValues);
|
|
262
269
|
|
|
270
|
+
expect(resolver.TestStrip(flsEntity, buildUser([INTERN_ROLE_ID]), input, clientNewValues)).toBe(true);
|
|
263
271
|
expect(clientNewValues).toEqual({ ID: '1', Name: 'Ada', Notes: 'n' });
|
|
264
272
|
expect(input['RestoreContext___']).toBe(restore);
|
|
265
|
-
expect(input
|
|
273
|
+
expect(input.OldValues___).toEqual([{ Key: 'Notes', Value: 'o' }]);
|
|
266
274
|
});
|
|
267
275
|
|
|
268
|
-
it('returns true
|
|
276
|
+
it('returns true when the caller is denied anything, so the truth-load is still forced', () => {
|
|
269
277
|
const { input, clientNewValues } = makeClientPayload({ ID: '1', Notes: 'edited' });
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
expect(result).toBe(true);
|
|
273
|
-
expect(clientNewValues).toEqual({ ID: '1', Notes: 'edited' });
|
|
278
|
+
expect(resolver.TestStrip(flsEntity, buildUser([INTERN_ROLE_ID]), input, clientNewValues)).toBe(true);
|
|
274
279
|
});
|
|
275
280
|
|
|
276
281
|
it('is a no-op returning false when the entity has no field security', () => {
|
|
277
|
-
const { input, clientNewValues } = makeClientPayload({ ID: '1', Salary: 12345 });
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
expect(result).toBe(false);
|
|
282
|
+
const { input, clientNewValues } = makeClientPayload({ ID: '1', Salary: 12345, Bogus: 1 });
|
|
283
|
+
expect(resolver.TestStrip(noFlsEntity, buildUser([INTERN_ROLE_ID]), input, clientNewValues)).toBe(false);
|
|
281
284
|
expect(clientNewValues['Salary']).toBe(12345);
|
|
282
|
-
expect(input['Salary']).toBe(12345);
|
|
283
285
|
});
|
|
284
286
|
|
|
285
287
|
it('is a no-op returning false for a user who HOLDS the granting role', () => {
|
|
286
|
-
const { input, clientNewValues } = makeClientPayload(
|
|
287
|
-
|
|
288
|
-
[{ Key: 'Salary', Value: '85000' }]
|
|
289
|
-
);
|
|
290
|
-
const result = resolver.TestStrip(flsEntity, buildUser([HR_ROLE_ID]), input, clientNewValues);
|
|
291
|
-
|
|
292
|
-
expect(result).toBe(false);
|
|
288
|
+
const { input, clientNewValues } = makeClientPayload({ ID: '1', Salary: 99000 }, [{ Key: 'Salary', Value: '85000' }]);
|
|
289
|
+
expect(resolver.TestStrip(flsEntity, buildUser([HR_ROLE_ID]), input, clientNewValues)).toBe(false);
|
|
293
290
|
expect(clientNewValues['Salary']).toBe(99000);
|
|
294
291
|
expect(input.OldValues___).toHaveLength(1);
|
|
295
292
|
});
|
|
296
293
|
|
|
297
294
|
it('is a no-op returning false without a user', () => {
|
|
298
295
|
const { input, clientNewValues } = makeClientPayload({ ID: '1', Salary: 1 });
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
expect(result).toBe(false);
|
|
302
|
-
expect(clientNewValues['Salary']).toBe(1);
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
it('matches input keys case-insensitively (wire casing must not defeat the strip)', () => {
|
|
306
|
-
const { input, clientNewValues } = makeClientPayload({ ID: '1', SALARY: 0 });
|
|
307
|
-
const result = resolver.TestStrip(flsEntity, buildUser([INTERN_ROLE_ID]), input, clientNewValues);
|
|
308
|
-
|
|
309
|
-
expect(result).toBe(true);
|
|
310
|
-
expect(clientNewValues).not.toHaveProperty('SALARY');
|
|
296
|
+
expect(resolver.TestStrip(flsEntity, undefined as unknown as UserInfo, input, clientNewValues)).toBe(false);
|
|
311
297
|
});
|
|
312
298
|
});
|
|
313
299
|
|
|
@@ -318,7 +304,7 @@ describe('ResolverBase.StripDeniedReadFieldsFromClientInput', () => {
|
|
|
318
304
|
// cannot see. These tests write the forbidden rows DIRECTLY into metadata, bypassing the
|
|
319
305
|
// constraint, and assert the runtime still refuses to produce a write-only outcome.
|
|
320
306
|
|
|
321
|
-
describe('write-only fields are unreachable, so
|
|
307
|
+
describe('write-only fields are unreachable, so a value for any denied-read field is refused', () => {
|
|
322
308
|
const resolver = new TestResolver();
|
|
323
309
|
|
|
324
310
|
/**
|
|
@@ -345,37 +331,11 @@ describe('write-only fields are unreachable, so every denied-read field is strip
|
|
|
345
331
|
expect(entity.GetDeniedUpdateFields(intern).has('salary')).toBe(true);
|
|
346
332
|
});
|
|
347
333
|
|
|
348
|
-
it('
|
|
334
|
+
it('refuses the value anyway — a client cannot have a legitimate value for a field it never saw', () => {
|
|
349
335
|
const entity = forbiddenWriteOnlyEntity();
|
|
350
336
|
const { input, clientNewValues } = makeClientPayload({ ID: '1', Salary: 123456 });
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
expect(result).toBe(true); // truth-load still forced — the denied-read set is non-empty
|
|
354
|
-
expect(clientNewValues).not.toHaveProperty('Salary');
|
|
355
|
-
expect(input).not.toHaveProperty('Salary');
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
it('removes the OldValues entry too (the client cannot know a true old value)', () => {
|
|
359
|
-
const entity = forbiddenWriteOnlyEntity();
|
|
360
|
-
const { input, clientNewValues } = makeClientPayload(
|
|
361
|
-
{ ID: '1', Salary: 123456 },
|
|
362
|
-
[{ Key: 'ID', Value: '1' }, { Key: 'Salary', Value: '99' }]
|
|
363
|
-
);
|
|
364
|
-
resolver.TestStrip(entity, buildUser([INTERN_ROLE_ID]), input, clientNewValues);
|
|
365
|
-
|
|
366
|
-
expect(input.OldValues___).toEqual([{ Key: 'ID', Value: '1' }]);
|
|
367
|
-
expect(clientNewValues).not.toHaveProperty('Salary');
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
it('strips EVERY denied-read field, with no update-permission split', () => {
|
|
371
|
-
// Base Salary is denied read AND update; Salary carries the forbidden write-only row.
|
|
372
|
-
// Both are stripped, because denied-read ∩ denied-update is now just denied-read.
|
|
373
|
-
const entity = forbiddenWriteOnlyEntity();
|
|
374
|
-
const { input, clientNewValues } = makeClientPayload({ ID: '1', Salary: 123456, Base_Salary: 7 });
|
|
375
|
-
resolver.TestStrip(entity, buildUser([INTERN_ROLE_ID]), input, clientNewValues);
|
|
376
|
-
|
|
377
|
-
expect(clientNewValues).not.toHaveProperty('Salary');
|
|
378
|
-
expect(clientNewValues).not.toHaveProperty('Base_Salary');
|
|
337
|
+
expect(() => resolver.TestStrip(entity, buildUser([INTERN_ROLE_ID]), input, clientNewValues))
|
|
338
|
+
.toThrow(refusalFor('Salary'));
|
|
379
339
|
});
|
|
380
340
|
});
|
|
381
341
|
|
|
@@ -564,3 +524,92 @@ describe('ResolverBase.MustLoadTruthFromDatabase: Update row filters (#4919)', (
|
|
|
564
524
|
expect(resolver.TestMustLoadTruth(entity, claimsNewStatusAsOld(), false, false, buildUser([INTERN_ROLE_ID]))).toBe(false);
|
|
565
525
|
});
|
|
566
526
|
});
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* IS-A children. A child carries its parent's fields as mirror fields of its own and nearly never
|
|
530
|
+
* has its own `EnableFieldLevelSecurity` on, so both guards must ask whether field security
|
|
531
|
+
* applies through the chain (`FieldLevelSecurityApplies`), not the child's own flag.
|
|
532
|
+
*/
|
|
533
|
+
describe('ResolverBase field-security guards on an IS-A child', () => {
|
|
534
|
+
const CHILD_ID = 'entity-contractors';
|
|
535
|
+
let resolver: TestResolver;
|
|
536
|
+
|
|
537
|
+
/** A `Contractors` IS-A `Employees` child with its own flag OFF and a mirror of every parent field. */
|
|
538
|
+
function childOf(parentInit: Record<string, unknown>): EntityInfo {
|
|
539
|
+
const entities: EntityInfo[] = [];
|
|
540
|
+
const provider = {
|
|
541
|
+
EntityByID: (id: string) => entities.find((e) => e.ID === id),
|
|
542
|
+
Entities: entities,
|
|
543
|
+
} as unknown as IMetadataProvider;
|
|
544
|
+
const parentFields = parentInit['Fields'] as Array<Record<string, unknown>>;
|
|
545
|
+
const childInit: Record<string, unknown> = {
|
|
546
|
+
...parentInit,
|
|
547
|
+
ID: CHILD_ID,
|
|
548
|
+
Name: 'Contractors',
|
|
549
|
+
BaseTable: 'Contractor',
|
|
550
|
+
BaseView: 'vwContractors',
|
|
551
|
+
ParentID: ENTITY_ID,
|
|
552
|
+
EnableFieldLevelSecurity: false,
|
|
553
|
+
Permissions: (parentInit['Permissions'] as Array<Record<string, unknown>>).map((p) => ({ ...p, EntityID: CHILD_ID })),
|
|
554
|
+
Fields: parentFields.map((f) => ({ ID: `c-${f['ID']}`, EntityID: CHILD_ID, Sequence: f['Sequence'], Name: f['Name'], Entity: 'Contractors', Type: f['Type'], IsPrimaryKey: f['IsPrimaryKey'] })),
|
|
555
|
+
};
|
|
556
|
+
entities.push(new EntityInfo(parentInit, provider), new EntityInfo(childInit, provider));
|
|
557
|
+
return entities[1];
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
beforeEach(() => {
|
|
561
|
+
resolver = new TestResolver();
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it('REGRESSION: AssertClientInputFieldSecurity refuses a field the parent denies in the child\'s input', () => {
|
|
565
|
+
const child = childOf(employeeEntityInit(true));
|
|
566
|
+
expect(child.EnableFieldLevelSecurity).toBe(false);
|
|
567
|
+
const { input, clientNewValues } = makeClientPayload({ ID: '1', Notes: 'edited', Salary: 0 }, [{ Key: 'Salary', Value: '0' }]);
|
|
568
|
+
|
|
569
|
+
expect(() => resolver.TestStrip(child, buildUser([INTERN_ROLE_ID]), input, clientNewValues))
|
|
570
|
+
.toThrow(FieldSecurityClientInputMessage('Salary', 'Contractors'));
|
|
571
|
+
const allowed = makeClientPayload({ ID: '1', Notes: 'edited' });
|
|
572
|
+
expect(resolver.TestStrip(child, buildUser([INTERN_ROLE_ID]), allowed.input, allowed.clientNewValues)).toBe(true);
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
it('AssertClientInputFieldSecurity leaves the child\'s input alone when no ancestor has field security', () => {
|
|
576
|
+
const child = childOf(employeeEntityInit(false));
|
|
577
|
+
const { input, clientNewValues } = makeClientPayload({ ID: '1', Salary: 0 });
|
|
578
|
+
expect(resolver.TestStrip(child, buildUser([INTERN_ROLE_ID]), input, clientNewValues)).toBe(false);
|
|
579
|
+
expect(clientNewValues).toEqual({ ID: '1', Salary: 0 });
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
/** Employees where the intern may read every field but not update Salary, with tracking off. */
|
|
583
|
+
function readAllowUpdateDenyInit(withFls: boolean): Record<string, unknown> {
|
|
584
|
+
const init = employeeEntityInit(withFls);
|
|
585
|
+
init['TrackRecordChanges'] = false;
|
|
586
|
+
const fields = init['Fields'] as Array<Record<string, unknown>>;
|
|
587
|
+
for (const f of fields) {
|
|
588
|
+
if (f['ID'] !== 'f-id') f['EntityFieldPermissions'] = openTo(f['ID'] as string);
|
|
589
|
+
}
|
|
590
|
+
fields.find((f) => f['ID'] === 'f-salary')!['EntityFieldPermissions'] = [
|
|
591
|
+
...openTo('f-salary', [HR_ROLE_ID]),
|
|
592
|
+
{ ID: 'f-salary-intern', EntityFieldID: 'f-salary', RoleID: INTERN_ROLE_ID, ReadAccess: 'Allow', UpdateAccess: 'Deny', CreateAccess: 'Deny' },
|
|
593
|
+
];
|
|
594
|
+
return init;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const pinnedOldValues = (): ClientInput => ({
|
|
598
|
+
ID: '1',
|
|
599
|
+
Notes: 'edited',
|
|
600
|
+
OldValues___: [{ Key: 'ID', Value: '1' }, { Key: 'Notes', Value: 'original' }, { Key: 'Salary', Value: '999999' }],
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
it('REGRESSION: MustLoadTruthFromDatabase forces the truth-load on a child whose parent has field security', () => {
|
|
604
|
+
const child = childOf(readAllowUpdateDenyInit(true));
|
|
605
|
+
child.TrackRecordChanges = false;
|
|
606
|
+
expect(child.GetDeniedReadFields(buildUser([INTERN_ROLE_ID])).size).toBe(0);
|
|
607
|
+
expect(resolver.TestMustLoadTruth(child, pinnedOldValues(), false, false)).toBe(true);
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
it('MustLoadTruthFromDatabase keeps the OldValues shortcut on a child when no ancestor has field security', () => {
|
|
611
|
+
const child = childOf(readAllowUpdateDenyInit(false));
|
|
612
|
+
child.TrackRecordChanges = false;
|
|
613
|
+
expect(resolver.TestMustLoadTruth(child, pinnedOldValues(), false, false)).toBe(false);
|
|
614
|
+
});
|
|
615
|
+
});
|