@memberjunction/server 6.2.0-edge.3 → 6.2.0-edge.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/agentSessions/BridgeHostInstance.d.ts +25 -0
- package/dist/agentSessions/BridgeHostInstance.d.ts.map +1 -0
- package/dist/agentSessions/BridgeHostInstance.js +30 -0
- package/dist/agentSessions/BridgeHostInstance.js.map +1 -0
- package/dist/agentSessions/HostInstance.d.ts +75 -11
- package/dist/agentSessions/HostInstance.d.ts.map +1 -1
- package/dist/agentSessions/HostInstance.js +99 -28
- package/dist/agentSessions/HostInstance.js.map +1 -1
- package/dist/agentSessions/ReturningVisitorRecap.d.ts.map +1 -1
- package/dist/agentSessions/ReturningVisitorRecap.js +7 -3
- package/dist/agentSessions/ReturningVisitorRecap.js.map +1 -1
- package/dist/agentSessions/SessionJanitor.d.ts +236 -27
- package/dist/agentSessions/SessionJanitor.d.ts.map +1 -1
- package/dist/agentSessions/SessionJanitor.js +682 -54
- package/dist/agentSessions/SessionJanitor.js.map +1 -1
- package/dist/agentSessions/SessionManager.d.ts +48 -3
- package/dist/agentSessions/SessionManager.d.ts.map +1 -1
- package/dist/agentSessions/SessionManager.js +112 -9
- package/dist/agentSessions/SessionManager.js.map +1 -1
- package/dist/agentSessions/SessionRunIDVerifier.d.ts +63 -0
- package/dist/agentSessions/SessionRunIDVerifier.d.ts.map +1 -0
- package/dist/agentSessions/SessionRunIDVerifier.js +129 -0
- package/dist/agentSessions/SessionRunIDVerifier.js.map +1 -0
- package/dist/agentSessions/index.d.ts +2 -0
- package/dist/agentSessions/index.d.ts.map +1 -1
- package/dist/agentSessions/index.js +10 -3
- package/dist/agentSessions/index.js.map +1 -1
- package/dist/apiRoot.d.ts +32 -0
- package/dist/apiRoot.d.ts.map +1 -0
- package/dist/apiRoot.js +67 -0
- package/dist/apiRoot.js.map +1 -0
- package/dist/auth/AuthProviderCatalogRouter.d.ts.map +1 -1
- package/dist/auth/AuthProviderCatalogRouter.js +25 -0
- package/dist/auth/AuthProviderCatalogRouter.js.map +1 -1
- package/dist/auth/LoginScreenEngine.d.ts +22 -0
- package/dist/auth/LoginScreenEngine.d.ts.map +1 -0
- package/dist/auth/LoginScreenEngine.js +57 -0
- package/dist/auth/LoginScreenEngine.js.map +1 -0
- package/dist/auth/actionAuthorization.d.ts +40 -0
- package/dist/auth/actionAuthorization.d.ts.map +1 -0
- package/dist/auth/actionAuthorization.js +94 -0
- package/dist/auth/actionAuthorization.js.map +1 -0
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +4 -10
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/magicLink/MagicLinkRouter.d.ts.map +1 -1
- package/dist/auth/magicLink/MagicLinkRouter.js +3 -2
- package/dist/auth/magicLink/MagicLinkRouter.js.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.d.ts.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.js +6 -3
- package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
- package/dist/auth/newUsers.d.ts +10 -0
- package/dist/auth/newUsers.d.ts.map +1 -1
- package/dist/auth/newUsers.js +80 -11
- package/dist/auth/newUsers.js.map +1 -1
- package/dist/config.d.ts +2514 -22
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +166 -4
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +8 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +14 -11
- package/dist/context.js.map +1 -1
- package/dist/coreRoutes.d.ts +29 -0
- package/dist/coreRoutes.d.ts.map +1 -0
- package/dist/coreRoutes.js +142 -0
- package/dist/coreRoutes.js.map +1 -0
- package/dist/generated/graphql-schemas/__mj.d.ts +504 -3
- package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
- package/dist/generated/graphql-schemas/__mj.js +2456 -7
- package/dist/generated/graphql-schemas/__mj.js.map +1 -1
- package/dist/generic/CacheInvalidationResolver.d.ts +43 -8
- package/dist/generic/CacheInvalidationResolver.d.ts.map +1 -1
- package/dist/generic/CacheInvalidationResolver.js +59 -19
- package/dist/generic/CacheInvalidationResolver.js.map +1 -1
- package/dist/generic/OrphanedConversationDetailReconciler.js +3 -3
- package/dist/generic/OrphanedConversationDetailReconciler.js.map +1 -1
- package/dist/generic/PushStatusResolver.js +1 -1
- package/dist/generic/PushStatusResolver.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +123 -48
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +293 -305
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +16 -0
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/globalCors.d.ts +33 -0
- package/dist/globalCors.d.ts.map +1 -0
- package/dist/globalCors.js +58 -0
- package/dist/globalCors.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +219 -278
- package/dist/index.js.map +1 -1
- package/dist/isolatedProvider.d.ts +20 -0
- package/dist/isolatedProvider.d.ts.map +1 -0
- package/dist/isolatedProvider.js +22 -0
- package/dist/isolatedProvider.js.map +1 -0
- package/dist/multiTenancy/index.d.ts.map +1 -1
- package/dist/multiTenancy/index.js +22 -2
- package/dist/multiTenancy/index.js.map +1 -1
- package/dist/realtimeProxy/RealtimeProxyServer.d.ts +44 -4
- package/dist/realtimeProxy/RealtimeProxyServer.d.ts.map +1 -1
- package/dist/realtimeProxy/RealtimeProxyServer.js +121 -16
- package/dist/realtimeProxy/RealtimeProxyServer.js.map +1 -1
- package/dist/realtimeProxy/RealtimeRelayTunnel.d.ts +171 -0
- package/dist/realtimeProxy/RealtimeRelayTunnel.d.ts.map +1 -0
- package/dist/realtimeProxy/RealtimeRelayTunnel.js +532 -0
- package/dist/realtimeProxy/RealtimeRelayTunnel.js.map +1 -0
- package/dist/realtimeSessions/RealtimeSessionEventService.d.ts +148 -0
- package/dist/realtimeSessions/RealtimeSessionEventService.d.ts.map +1 -0
- package/dist/realtimeSessions/RealtimeSessionEventService.js +191 -0
- package/dist/realtimeSessions/RealtimeSessionEventService.js.map +1 -0
- package/dist/realtimeSessions/RealtimeSessionVerificationService.d.ts +66 -0
- package/dist/realtimeSessions/RealtimeSessionVerificationService.d.ts.map +1 -0
- package/dist/realtimeSessions/RealtimeSessionVerificationService.js +139 -0
- package/dist/realtimeSessions/RealtimeSessionVerificationService.js.map +1 -0
- package/dist/realtimeSessions/RealtimeVerifyRouter.d.ts +38 -0
- package/dist/realtimeSessions/RealtimeVerifyRouter.d.ts.map +1 -0
- package/dist/realtimeSessions/RealtimeVerifyRouter.js +99 -0
- package/dist/realtimeSessions/RealtimeVerifyRouter.js.map +1 -0
- package/dist/realtimeSessions/RecordingAssemblyScheduler.d.ts +100 -0
- package/dist/realtimeSessions/RecordingAssemblyScheduler.d.ts.map +1 -0
- package/dist/realtimeSessions/RecordingAssemblyScheduler.js +173 -0
- package/dist/realtimeSessions/RecordingAssemblyScheduler.js.map +1 -0
- package/dist/realtimeSessions/SessionVerificationPortsBase.d.ts +61 -0
- package/dist/realtimeSessions/SessionVerificationPortsBase.d.ts.map +1 -0
- package/dist/realtimeSessions/SessionVerificationPortsBase.js +149 -0
- package/dist/realtimeSessions/SessionVerificationPortsBase.js.map +1 -0
- package/dist/realtimeSessions/SlidingWindowRateLimiter.d.ts +57 -0
- package/dist/realtimeSessions/SlidingWindowRateLimiter.d.ts.map +1 -0
- package/dist/realtimeSessions/SlidingWindowRateLimiter.js +69 -0
- package/dist/realtimeSessions/SlidingWindowRateLimiter.js.map +1 -0
- package/dist/realtimeSessions/index.d.ts +19 -0
- package/dist/realtimeSessions/index.d.ts.map +1 -0
- package/dist/realtimeSessions/index.js +19 -0
- package/dist/realtimeSessions/index.js.map +1 -0
- package/dist/realtimeSessions/ownedRealtimeSession.d.ts +30 -0
- package/dist/realtimeSessions/ownedRealtimeSession.d.ts.map +1 -0
- package/dist/realtimeSessions/ownedRealtimeSession.js +47 -0
- package/dist/realtimeSessions/ownedRealtimeSession.js.map +1 -0
- package/dist/realtimeSessions/sessionAccess.d.ts +92 -0
- package/dist/realtimeSessions/sessionAccess.d.ts.map +1 -0
- package/dist/realtimeSessions/sessionAccess.js +94 -0
- package/dist/realtimeSessions/sessionAccess.js.map +1 -0
- package/dist/realtimeSessions/sessionConfigStamps.d.ts +70 -0
- package/dist/realtimeSessions/sessionConfigStamps.d.ts.map +1 -0
- package/dist/realtimeSessions/sessionConfigStamps.js +93 -0
- package/dist/realtimeSessions/sessionConfigStamps.js.map +1 -0
- package/dist/realtimeSessions/subscriptionAuthorization.d.ts +65 -0
- package/dist/realtimeSessions/subscriptionAuthorization.d.ts.map +1 -0
- package/dist/realtimeSessions/subscriptionAuthorization.js +71 -0
- package/dist/realtimeSessions/subscriptionAuthorization.js.map +1 -0
- package/dist/realtimeSessions/verificationCore.d.ts +243 -0
- package/dist/realtimeSessions/verificationCore.d.ts.map +1 -0
- package/dist/realtimeSessions/verificationCore.js +356 -0
- package/dist/realtimeSessions/verificationCore.js.map +1 -0
- package/dist/realtimeSessions/verificationPages.d.ts +66 -0
- package/dist/realtimeSessions/verificationPages.d.ts.map +1 -0
- package/dist/realtimeSessions/verificationPages.js +110 -0
- package/dist/realtimeSessions/verificationPages.js.map +1 -0
- package/dist/realtimeSessions/verificationPolicy.d.ts +147 -0
- package/dist/realtimeSessions/verificationPolicy.d.ts.map +1 -0
- package/dist/realtimeSessions/verificationPolicy.js +249 -0
- package/dist/realtimeSessions/verificationPolicy.js.map +1 -0
- package/dist/realtimeSessions/verificationWorkflow.d.ts +263 -0
- package/dist/realtimeSessions/verificationWorkflow.d.ts.map +1 -0
- package/dist/realtimeSessions/verificationWorkflow.js +435 -0
- package/dist/realtimeSessions/verificationWorkflow.js.map +1 -0
- package/dist/realtimeWidget/widgetGuestElevation.d.ts +20 -2
- package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -1
- package/dist/realtimeWidget/widgetGuestElevation.js +31 -2
- package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
- package/dist/resolvers/ActionResolver.d.ts +31 -5
- package/dist/resolvers/ActionResolver.d.ts.map +1 -1
- package/dist/resolvers/ActionResolver.js +75 -14
- package/dist/resolvers/ActionResolver.js.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.d.ts +14 -1
- package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.js +21 -5
- package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
- package/dist/resolvers/ArtifactFileResolver.d.ts +5 -1
- package/dist/resolvers/ArtifactFileResolver.d.ts.map +1 -1
- package/dist/resolvers/ArtifactFileResolver.js +9 -7
- package/dist/resolvers/ArtifactFileResolver.js.map +1 -1
- package/dist/resolvers/CacheStatsResolver.d.ts +27 -0
- package/dist/resolvers/CacheStatsResolver.d.ts.map +1 -1
- package/dist/resolvers/CacheStatsResolver.js +113 -2
- package/dist/resolvers/CacheStatsResolver.js.map +1 -1
- package/dist/resolvers/ClientToolRequestResolver.d.ts +4 -2
- package/dist/resolvers/ClientToolRequestResolver.d.ts.map +1 -1
- package/dist/resolvers/ClientToolRequestResolver.js +28 -10
- package/dist/resolvers/ClientToolRequestResolver.js.map +1 -1
- package/dist/resolvers/ConversationRunEventsResolver.d.ts +8 -4
- package/dist/resolvers/ConversationRunEventsResolver.d.ts.map +1 -1
- package/dist/resolvers/ConversationRunEventsResolver.js +6 -6
- package/dist/resolvers/ConversationRunEventsResolver.js.map +1 -1
- package/dist/resolvers/DatasetResolver.d.ts +24 -3
- package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
- package/dist/resolvers/DatasetResolver.js +57 -8
- package/dist/resolvers/DatasetResolver.js.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.d.ts +2 -1
- package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.js +3 -2
- package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.d.ts +27 -1
- package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.js +148 -2
- package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
- package/dist/resolvers/FileResolver.d.ts +82 -11
- package/dist/resolvers/FileResolver.d.ts.map +1 -1
- package/dist/resolvers/FileResolver.js +279 -101
- package/dist/resolvers/FileResolver.js.map +1 -1
- package/dist/resolvers/GetDataContextDataResolver.d.ts +33 -7
- package/dist/resolvers/GetDataContextDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataContextDataResolver.js +117 -38
- package/dist/resolvers/GetDataContextDataResolver.js.map +1 -1
- package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataResolver.js +6 -3
- package/dist/resolvers/GetDataResolver.js.map +1 -1
- package/dist/resolvers/HumanHandoffResolver.d.ts.map +1 -1
- package/dist/resolvers/HumanHandoffResolver.js +7 -1
- package/dist/resolvers/HumanHandoffResolver.js.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +4 -2
- package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
- package/dist/resolvers/MCPResolver.d.ts.map +1 -1
- package/dist/resolvers/MCPResolver.js +9 -4
- package/dist/resolvers/MCPResolver.js.map +1 -1
- package/dist/resolvers/QueryResolver.d.ts +2 -0
- package/dist/resolvers/QueryResolver.d.ts.map +1 -1
- package/dist/resolvers/QueryResolver.js +13 -0
- package/dist/resolvers/QueryResolver.js.map +1 -1
- package/dist/resolvers/RealtimeBridgeResolver.d.ts +54 -2
- package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeBridgeResolver.js +155 -10
- package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +157 -33
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +404 -116
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/RealtimeSessionEventsResolver.d.ts +61 -0
- package/dist/resolvers/RealtimeSessionEventsResolver.d.ts.map +1 -0
- package/dist/resolvers/RealtimeSessionEventsResolver.js +130 -0
- package/dist/resolvers/RealtimeSessionEventsResolver.js.map +1 -0
- package/dist/resolvers/RealtimeSessionVerificationResolver.d.ts +85 -0
- package/dist/resolvers/RealtimeSessionVerificationResolver.d.ts.map +1 -0
- package/dist/resolvers/RealtimeSessionVerificationResolver.js +188 -0
- package/dist/resolvers/RealtimeSessionVerificationResolver.js.map +1 -0
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts +2 -0
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
- package/dist/resolvers/RemoteBrowserActionResolver.js +5 -1
- package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts +10 -0
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +100 -14
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
- package/dist/resolvers/RunTemplateResolver.js +12 -11
- package/dist/resolvers/RunTemplateResolver.js.map +1 -1
- package/dist/resolvers/SearchKnowledgeStreamResolver.d.ts +12 -1
- package/dist/resolvers/SearchKnowledgeStreamResolver.d.ts.map +1 -1
- package/dist/resolvers/SearchKnowledgeStreamResolver.js +15 -2
- package/dist/resolvers/SearchKnowledgeStreamResolver.js.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.js +2 -3
- package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
- package/dist/resolvers/SyncDataResolver.d.ts.map +1 -1
- package/dist/resolvers/SyncDataResolver.js +6 -2
- package/dist/resolvers/SyncDataResolver.js.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.d.ts +2 -0
- package/dist/resolvers/SyncRolesUsersResolver.d.ts.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.js +23 -6
- package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
- package/dist/resolvers/TaskGraphFrameResolver.d.ts +2 -1
- package/dist/resolvers/TaskGraphFrameResolver.d.ts.map +1 -1
- package/dist/resolvers/TaskGraphFrameResolver.js +28 -1
- package/dist/resolvers/TaskGraphFrameResolver.js.map +1 -1
- package/dist/resolvers/TestQuerySQLResolver.d.ts +24 -0
- package/dist/resolvers/TestQuerySQLResolver.d.ts.map +1 -1
- package/dist/resolvers/TestQuerySQLResolver.js +65 -13
- package/dist/resolvers/TestQuerySQLResolver.js.map +1 -1
- package/dist/resolvers/TransactionGroupResolver.d.ts +19 -2
- package/dist/resolvers/TransactionGroupResolver.d.ts.map +1 -1
- package/dist/resolvers/TransactionGroupResolver.js +102 -24
- package/dist/resolvers/TransactionGroupResolver.js.map +1 -1
- package/dist/resolvers/UserAvatarResolver.d.ts +16 -0
- package/dist/resolvers/UserAvatarResolver.d.ts.map +1 -0
- package/dist/resolvers/UserAvatarResolver.js +124 -0
- package/dist/resolvers/UserAvatarResolver.js.map +1 -0
- package/dist/resolvers/UserResolver.d.ts.map +1 -1
- package/dist/resolvers/UserResolver.js +2 -17
- package/dist/resolvers/UserResolver.js.map +1 -1
- package/dist/resolvers/agent-run-data-guard.d.ts +31 -0
- package/dist/resolvers/agent-run-data-guard.d.ts.map +1 -0
- package/dist/resolvers/agent-run-data-guard.js +72 -0
- package/dist/resolvers/agent-run-data-guard.js.map +1 -0
- package/dist/resolvers/agentVisionConsentAudit.d.ts +37 -0
- package/dist/resolvers/agentVisionConsentAudit.d.ts.map +1 -0
- package/dist/resolvers/agentVisionConsentAudit.js +63 -0
- package/dist/resolvers/agentVisionConsentAudit.js.map +1 -0
- package/dist/resolvers/avatarInputValidation.d.ts +40 -0
- package/dist/resolvers/avatarInputValidation.d.ts.map +1 -0
- package/dist/resolvers/avatarInputValidation.js +97 -0
- package/dist/resolvers/avatarInputValidation.js.map +1 -0
- package/dist/resolvers/currentUserRoles.d.ts +29 -0
- package/dist/resolvers/currentUserRoles.d.ts.map +1 -1
- package/dist/resolvers/currentUserRoles.js +30 -0
- package/dist/resolvers/currentUserRoles.js.map +1 -1
- package/dist/resolvers/meetings/meetingMutations.d.ts +6 -0
- package/dist/resolvers/meetings/meetingMutations.d.ts.map +1 -1
- package/dist/resolvers/meetings/meetingMutations.js +21 -0
- package/dist/resolvers/meetings/meetingMutations.js.map +1 -1
- package/dist/rest/RealtimeRecordingHandler.d.ts +49 -0
- package/dist/rest/RealtimeRecordingHandler.d.ts.map +1 -0
- package/dist/rest/RealtimeRecordingHandler.js +383 -0
- package/dist/rest/RealtimeRecordingHandler.js.map +1 -0
- package/dist/rest/ViewOperationsHandler.d.ts +3 -1
- package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
- package/dist/rest/ViewOperationsHandler.js +21 -12
- package/dist/rest/ViewOperationsHandler.js.map +1 -1
- package/dist/rest/setupRESTEndpoints.d.ts +6 -0
- package/dist/rest/setupRESTEndpoints.d.ts.map +1 -1
- package/dist/rest/setupRESTEndpoints.js +6 -0
- package/dist/rest/setupRESTEndpoints.js.map +1 -1
- package/dist/serverExtensionReservedRoots.d.ts +12 -10
- package/dist/serverExtensionReservedRoots.d.ts.map +1 -1
- package/dist/serverExtensionReservedRoots.js +14 -17
- package/dist/serverExtensionReservedRoots.js.map +1 -1
- package/dist/services/MJStorageBlobStore.d.ts +8 -1
- package/dist/services/MJStorageBlobStore.d.ts.map +1 -1
- package/dist/services/MJStorageBlobStore.js +25 -19
- package/dist/services/MJStorageBlobStore.js.map +1 -1
- package/dist/services/StartTaskGraphDispatcher.js +2 -2
- package/dist/services/StartTaskGraphDispatcher.js.map +1 -1
- package/dist/services/TaskGraphActionRunner.d.ts +5 -3
- package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphActionRunner.js +27 -5
- package/dist/services/TaskGraphActionRunner.js.map +1 -1
- package/dist/services/TaskGraphAgentRunner.d.ts +14 -2
- package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphAgentRunner.js +157 -9
- package/dist/services/TaskGraphAgentRunner.js.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.d.ts +57 -4
- package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.js +607 -18
- package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
- package/dist/sharedCache.d.ts +105 -0
- package/dist/sharedCache.d.ts.map +1 -0
- package/dist/sharedCache.js +279 -0
- package/dist/sharedCache.js.map +1 -0
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +99 -96
- package/src/__tests__/ActionResolver.authorization.test.ts +353 -0
- package/src/__tests__/AdhocQueryResolver.sqlAuthorizer.test.ts +104 -0
- package/src/__tests__/BridgeHostInstance.test.ts +232 -0
- package/src/__tests__/ClientToolRequestResolver.test.ts +116 -0
- package/src/__tests__/DatasetResolver.itemFilters.test.ts +201 -0
- package/src/__tests__/EntityRecordNameResolver.rowSecurity.test.ts +152 -0
- package/src/__tests__/FetchEntityVectorsResolver.test.ts +433 -0
- package/src/__tests__/FileResolver.storageAccess.test.ts +562 -0
- package/src/__tests__/GetDataContextDataResolver.sqlItems.test.ts +416 -0
- package/src/__tests__/HostInstance.test.ts +84 -0
- package/src/__tests__/HumanHandoffResolver.test.ts +17 -0
- package/src/__tests__/ListOperationsResolver.adhocScreen.test.ts +137 -0
- package/src/__tests__/LoginScreenEngine.test.ts +86 -0
- package/src/__tests__/MJStorageBlobStore.storageGate.test.ts +63 -0
- package/src/__tests__/MeetingResolver.test.ts +75 -1
- package/src/__tests__/RealtimeBridgeResolver.test.ts +415 -6
- package/src/__tests__/RealtimeClientSessionResolver.configTamper.test.ts +383 -0
- package/src/__tests__/RealtimeClientSessionResolver.test.ts +682 -58
- package/src/__tests__/RealtimeProxyServer.test.ts +16 -1
- package/src/__tests__/RealtimeRelay.test.ts +876 -0
- package/src/__tests__/RecordingAssemblyScheduler.test.ts +178 -0
- package/src/__tests__/RemoteBrowserActionResolver.goalRunLink.test.ts +129 -0
- package/src/__tests__/ResolverBase.adhocReadScope.test.ts +133 -2
- package/src/__tests__/ResolverBase.aggregateScreen.test.ts +174 -0
- package/src/__tests__/ResolverBase.balancedClause.test.ts +155 -0
- package/src/__tests__/ResolverBase.clauseUnscopedRead.test.ts +127 -0
- package/src/__tests__/ResolverBase.clientOldValues.test.ts +212 -6
- package/src/__tests__/ResolverBase.filterEscaping.test.ts +5 -2
- package/src/__tests__/ReturningVisitorRecap.test.ts +115 -0
- package/src/__tests__/RunAIAgentResolver.branch.test.ts +170 -0
- package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +6 -4
- package/src/__tests__/RunAIAgentResolver.runDataGuard.test.ts +258 -0
- package/src/__tests__/RunAIAgentResolver.stoppedPredecessor.test.ts +166 -0
- package/src/__tests__/RunTemplateResolver.test.ts +132 -0
- package/src/__tests__/RunViewResolver.cacheCheckForwarding.test.ts +63 -0
- package/src/__tests__/RunViewResolver.cacheCheckScreen.test.ts +165 -0
- package/src/__tests__/SearchKnowledgeStreamResolver.test.ts +161 -0
- package/src/__tests__/SessionJanitor.test.ts +1203 -21
- package/src/__tests__/SessionManager.test.ts +240 -17
- package/src/__tests__/SessionRunIDVerifier.test.ts +251 -0
- package/src/__tests__/SyncRolesUsersResolver.userRoles.test.ts +97 -0
- package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +25 -0
- package/src/__tests__/TaskGraphAgentRunner.test.ts +266 -0
- package/src/__tests__/TaskGraphContinuationDeliverer.test.ts +403 -22
- package/src/__tests__/TaskGraphFrameResolver.test.ts +22 -0
- package/src/__tests__/TestQuerySQLResolver.test.ts +274 -0
- package/src/__tests__/TransactionGroupResolver.refusals.test.ts +98 -1
- package/src/__tests__/UpdateMJDataContextItem.oldValues.test.ts +220 -0
- package/src/__tests__/UserAvatarResolver.test.ts +255 -0
- package/src/__tests__/UserResolver.currentUser.test.ts +16 -1
- package/src/__tests__/ViewOperationsHandler.clauseScreen.test.ts +157 -0
- package/src/__tests__/actionAuthorization.test.ts +130 -0
- package/src/__tests__/agent-run-data-guard.test.ts +130 -0
- package/src/__tests__/agentVisionConsentAudit.test.ts +118 -0
- package/src/__tests__/apiRootBoot.test.ts +202 -0
- package/src/__tests__/avatarInputValidation.test.ts +160 -0
- package/src/__tests__/cacheInvalidationRecordData.test.ts +64 -0
- package/src/__tests__/conversationRunEvents.test.ts +56 -14
- package/src/__tests__/globalCors.test.ts +217 -0
- package/src/__tests__/isolatedProviderTransactions.test.ts +169 -0
- package/src/__tests__/magicLinkService.consume.test.ts +6 -0
- package/src/__tests__/magicLinkService.provisionLog.test.ts +5 -0
- package/src/__tests__/multiTenancy.postgresRewrite.test.ts +181 -0
- package/src/__tests__/multiTenancy.security.test.ts +51 -0
- package/src/__tests__/multiTenancy.test.ts +1 -1
- package/src/__tests__/newUsers.template.test.ts +151 -0
- package/src/__tests__/newUsers.test.ts +74 -5
- package/src/__tests__/orphanedConversationDetailReconciler.test.ts +17 -2
- package/src/__tests__/ownedRealtimeSession.test.ts +39 -0
- package/src/__tests__/pushStatusResolver.filter.test.ts +7 -0
- package/src/__tests__/realtimeRecordingHandler.test.ts +652 -0
- package/src/__tests__/realtimeSessions/RealtimeSessionEventService.test.ts +201 -0
- package/src/__tests__/realtimeSessions/RealtimeSessionEventsResolver.test.ts +215 -0
- package/src/__tests__/realtimeSessions/RealtimeSessionVerificationResolver.test.ts +102 -0
- package/src/__tests__/realtimeSessions/RealtimeSessionVerificationService.test.ts +349 -0
- package/src/__tests__/realtimeSessions/RealtimeVerifyRouter.test.ts +131 -0
- package/src/__tests__/realtimeSessions/SlidingWindowRateLimiter.test.ts +56 -0
- package/src/__tests__/realtimeSessions/helpers.ts +4 -0
- package/src/__tests__/realtimeSessions/sessionAccess.test.ts +129 -0
- package/src/__tests__/realtimeSessions/sessionConfigStamps.test.ts +124 -0
- package/src/__tests__/realtimeSessions/verificationCore.test.ts +355 -0
- package/src/__tests__/realtimeSessions/verificationPages.test.ts +84 -0
- package/src/__tests__/realtimeSessions/verificationPolicy.test.ts +226 -0
- package/src/__tests__/realtimeSessions/verificationWorkflow.test.ts +633 -0
- package/src/__tests__/resolverBase.fls.test.ts +87 -4
- package/src/__tests__/serverExtensionHostOverlay.test.ts +66 -0
- package/src/__tests__/serverExtensionReservedRoots.test.ts +11 -96
- package/src/__tests__/sharedCache.test.ts +312 -0
- package/src/__tests__/widgetGuestElevation.test.ts +44 -0
- package/src/agentSessions/BridgeHostInstance.ts +32 -0
- package/src/agentSessions/HostInstance.ts +124 -28
- package/src/agentSessions/ReturningVisitorRecap.ts +7 -3
- package/src/agentSessions/SessionJanitor.ts +821 -59
- package/src/agentSessions/SessionManager.ts +138 -26
- package/src/agentSessions/SessionRunIDVerifier.ts +171 -0
- package/src/agentSessions/index.ts +10 -3
- package/src/apiRoot.ts +78 -0
- package/src/auth/AuthProviderCatalogRouter.ts +26 -0
- package/src/auth/LoginScreenEngine.ts +50 -0
- package/src/auth/actionAuthorization.ts +116 -0
- package/src/auth/index.ts +4 -8
- package/src/auth/magicLink/MagicLinkRouter.ts +3 -2
- package/src/auth/magicLink/MagicLinkService.ts +6 -3
- package/src/auth/newUsers.ts +99 -11
- package/src/config.ts +171 -5
- package/src/context.ts +15 -10
- package/src/coreRoutes.ts +166 -0
- package/src/generated/graphql-schemas/__mj.ts +1815 -57
- package/src/generic/CacheInvalidationResolver.ts +73 -18
- package/src/generic/OrphanedConversationDetailReconciler.ts +3 -3
- package/src/generic/PushStatusResolver.ts +1 -1
- package/src/generic/ResolverBase.ts +335 -326
- package/src/generic/RunViewResolver.ts +21 -0
- package/src/globalCors.ts +71 -0
- package/src/index.ts +231 -300
- package/src/isolatedProvider.ts +22 -0
- package/src/multiTenancy/index.ts +24 -2
- package/src/realtimeProxy/RealtimeProxyServer.ts +148 -16
- package/src/realtimeProxy/RealtimeRelayTunnel.ts +645 -0
- package/src/realtimeSessions/README.md +179 -0
- package/src/realtimeSessions/RealtimeSessionEventService.ts +274 -0
- package/src/realtimeSessions/RealtimeSessionVerificationService.ts +168 -0
- package/src/realtimeSessions/RealtimeVerifyRouter.ts +109 -0
- package/src/realtimeSessions/RecordingAssemblyScheduler.ts +234 -0
- package/src/realtimeSessions/SessionVerificationPortsBase.ts +189 -0
- package/src/realtimeSessions/SlidingWindowRateLimiter.ts +92 -0
- package/src/realtimeSessions/index.ts +18 -0
- package/src/realtimeSessions/ownedRealtimeSession.ts +55 -0
- package/src/realtimeSessions/sessionAccess.ts +145 -0
- package/src/realtimeSessions/sessionConfigStamps.ts +123 -0
- package/src/realtimeSessions/subscriptionAuthorization.ts +116 -0
- package/src/realtimeSessions/verificationCore.ts +502 -0
- package/src/realtimeSessions/verificationPages.ts +149 -0
- package/src/realtimeSessions/verificationPolicy.ts +321 -0
- package/src/realtimeSessions/verificationWorkflow.ts +702 -0
- package/src/realtimeWidget/widgetGuestElevation.ts +34 -2
- package/src/resolvers/ActionResolver.ts +84 -21
- package/src/resolvers/AdhocQueryResolver.ts +24 -6
- package/src/resolvers/ArtifactFileResolver.ts +9 -8
- package/src/resolvers/CacheStatsResolver.ts +89 -2
- package/src/resolvers/ClientToolRequestResolver.ts +25 -7
- package/src/resolvers/ConversationRunEventsResolver.ts +15 -11
- package/src/resolvers/DatasetResolver.ts +70 -10
- package/src/resolvers/EntityRecordNameResolver.ts +3 -2
- package/src/resolvers/FetchEntityVectorsResolver.ts +179 -2
- package/src/resolvers/FileResolver.ts +321 -106
- package/src/resolvers/GetDataContextDataResolver.ts +139 -41
- package/src/resolvers/GetDataResolver.ts +6 -3
- package/src/resolvers/HumanHandoffResolver.ts +8 -0
- package/src/resolvers/IntegrationDiscoveryResolver.ts +4 -2
- package/src/resolvers/MCPResolver.ts +8 -5
- package/src/resolvers/QueryResolver.ts +11 -0
- package/src/resolvers/RealtimeBridgeResolver.ts +163 -11
- package/src/resolvers/RealtimeClientSessionResolver.ts +511 -129
- package/src/resolvers/RealtimeSessionEventsResolver.ts +133 -0
- package/src/resolvers/RealtimeSessionVerificationResolver.ts +174 -0
- package/src/resolvers/RemoteBrowserActionResolver.ts +11 -1
- package/src/resolvers/RunAIAgentResolver.ts +116 -14
- package/src/resolvers/RunTemplateResolver.ts +13 -12
- package/src/resolvers/SearchKnowledgeStreamResolver.ts +19 -3
- package/src/resolvers/SqlLoggingConfigResolver.ts +2 -4
- package/src/resolvers/SyncDataResolver.ts +6 -2
- package/src/resolvers/SyncRolesUsersResolver.ts +24 -6
- package/src/resolvers/TaskGraphFrameResolver.ts +33 -1
- package/src/resolvers/TestQuerySQLResolver.ts +68 -13
- package/src/resolvers/TransactionGroupResolver.ts +131 -27
- package/src/resolvers/UserAvatarResolver.ts +111 -0
- package/src/resolvers/UserResolver.ts +2 -16
- package/src/resolvers/__tests__/GetDataResolver.validation.test.ts +95 -0
- package/src/resolvers/agent-run-data-guard.ts +89 -0
- package/src/resolvers/agentVisionConsentAudit.ts +82 -0
- package/src/resolvers/avatarInputValidation.ts +123 -0
- package/src/resolvers/currentUserRoles.ts +32 -1
- package/src/resolvers/meetings/meetingMutations.ts +21 -0
- package/src/rest/RealtimeRecordingHandler.ts +464 -0
- package/src/rest/ViewOperationsHandler.ts +29 -13
- package/src/rest/setupRESTEndpoints.ts +6 -0
- package/src/serverExtensionReservedRoots.ts +14 -17
- package/src/services/MJStorageBlobStore.ts +31 -30
- package/src/services/StartTaskGraphDispatcher.ts +2 -2
- package/src/services/TaskGraphActionRunner.ts +25 -5
- package/src/services/TaskGraphAgentRunner.ts +188 -11
- package/src/services/TaskGraphContinuationDeliverer.ts +713 -17
- package/src/sharedCache.ts +305 -0
- package/src/types.ts +7 -0
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FileResolver.storageAccess.test.ts — the storage-account gate on every account-keyed storage route (A12.16).
|
|
3
|
+
*
|
|
4
|
+
* These routes used to check only entity-level permission on an empty `MJ: Files` object and then act on a
|
|
5
|
+
* client-supplied account through `FileStorageEngine.GetDriver` (a process-wide cache with no user check). They now run
|
|
6
|
+
* the shared `StorageAccessEvaluator` first. The evaluator here is the REAL one from `@memberjunction/storage`, and so is
|
|
7
|
+
* `FileStorageEngine.ResolveFileObject` (the gate for routes that go from a file ID to an object); only the data layer
|
|
8
|
+
* behind them (the provider's RunView/RunViews), the system-user lookup, the engine's cache and drivers and the driver
|
|
9
|
+
* utilities are faked — so "denied", "zero rows → open" and "tracked object" are decided by production code.
|
|
10
|
+
*/
|
|
11
|
+
import 'reflect-metadata';
|
|
12
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
13
|
+
|
|
14
|
+
const mocks = vi.hoisted(() => ({
|
|
15
|
+
engine: {
|
|
16
|
+
Config: vi.fn(async () => undefined),
|
|
17
|
+
GetDriver: vi.fn(),
|
|
18
|
+
GetAccountById: vi.fn(),
|
|
19
|
+
GetAccountWithProvider: vi.fn(),
|
|
20
|
+
ResolveStorageAccount: vi.fn(),
|
|
21
|
+
UploadFile: vi.fn(),
|
|
22
|
+
/** Shadows the engine's `Base` getter: the metadata cache behind the REAL ResolveFileObject. */
|
|
23
|
+
Base: { GetAccountsByProviderID: vi.fn() },
|
|
24
|
+
},
|
|
25
|
+
listObjects: vi.fn(async () => ({ objects: [], prefixes: [] })),
|
|
26
|
+
deleteObject: vi.fn(async () => true),
|
|
27
|
+
moveObject: vi.fn(async () => true),
|
|
28
|
+
copyObject: vi.fn(async () => true),
|
|
29
|
+
copyObjectBetweenProviders: vi.fn(async () => ({ success: true, message: 'ok', sourcePath: 'a', destinationPath: 'b' })),
|
|
30
|
+
searchAcrossAccounts: vi.fn(),
|
|
31
|
+
consumeUploadToken: vi.fn(),
|
|
32
|
+
createUploadUrl: vi.fn(),
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
// The resolver's import chain reaches the generated GraphQL schema and the type-graphql decorators; neither is under
|
|
36
|
+
// test here, so neutralize them (the pattern UserViewResolver.updateWhereClause.test.ts uses).
|
|
37
|
+
vi.mock('@memberjunction/server', () => {
|
|
38
|
+
const noopDecorator = () => () => undefined;
|
|
39
|
+
return {
|
|
40
|
+
Arg: noopDecorator, Ctx: noopDecorator, Field: noopDecorator, FieldResolver: noopDecorator, InputType: noopDecorator,
|
|
41
|
+
Mutation: noopDecorator, ObjectType: noopDecorator, PubSub: noopDecorator, Query: noopDecorator, Resolver: noopDecorator,
|
|
42
|
+
Root: noopDecorator, Int: {}, Float: {}, DeleteOptionsInput: class {},
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
vi.mock('../generated/generated.js', () => ({
|
|
46
|
+
CreateMJFileInput: class {},
|
|
47
|
+
UpdateMJFileInput: class {},
|
|
48
|
+
MJFile_: class {},
|
|
49
|
+
MJFileResolver: class {
|
|
50
|
+
protected GetUserFromPayload(userPayload: { userRecord?: object } | undefined) {
|
|
51
|
+
return userPayload?.userRecord;
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
}));
|
|
55
|
+
vi.mock('@memberjunction/ai-agents', () => ({ readRealtimeRecordingFile: vi.fn() }));
|
|
56
|
+
vi.mock('@memberjunction/graphql-dataprovider', () => ({ FieldMapper: class { MapFields(o: object) { return o; } } }));
|
|
57
|
+
vi.mock('../config.js', () => ({ configInfo: {} }));
|
|
58
|
+
vi.mock('../rest/MediaAccessKeys.js', () => ({ MediaAccessKeyManager: { Instance: {} } }));
|
|
59
|
+
vi.mock('../rest/UploadTokenManager.js', () => ({ UploadTokenManager: { Instance: { Consume: mocks.consumeUploadToken } } }));
|
|
60
|
+
vi.mock('@memberjunction/storage', async () => {
|
|
61
|
+
const actual = await vi.importActual<typeof import('@memberjunction/storage')>('@memberjunction/storage');
|
|
62
|
+
// The engine's cache and drivers are faked (own properties above); its ResolveFileObject is the real one.
|
|
63
|
+
Object.setPrototypeOf(mocks.engine, actual.FileStorageEngine.prototype);
|
|
64
|
+
return {
|
|
65
|
+
...actual,
|
|
66
|
+
FileStorageEngine: { Instance: mocks.engine },
|
|
67
|
+
listObjects: mocks.listObjects,
|
|
68
|
+
deleteObject: mocks.deleteObject,
|
|
69
|
+
moveObject: mocks.moveObject,
|
|
70
|
+
copyObject: mocks.copyObject,
|
|
71
|
+
copyObjectBetweenProviders: mocks.copyObjectBetweenProviders,
|
|
72
|
+
searchAcrossAccounts: mocks.searchAcrossAccounts,
|
|
73
|
+
createUploadUrl: mocks.createUploadUrl,
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
import { WellKnownUserSource } from '@memberjunction/core';
|
|
78
|
+
import type { RunViewParams, RunViewResult, UserInfo } from '@memberjunction/core';
|
|
79
|
+
import {
|
|
80
|
+
NormalizeStorageObjectKey,
|
|
81
|
+
STORAGE_ACCOUNT_ACCESS_DENIED_MESSAGE,
|
|
82
|
+
TRACKED_FILE_ACCESS_DENIED_MESSAGE,
|
|
83
|
+
type StorageAccountPermissionRow,
|
|
84
|
+
} from '@memberjunction/storage';
|
|
85
|
+
import { DeleteOptionsInput, type PubSubEngine } from '@memberjunction/server';
|
|
86
|
+
import { FileResolver } from '../resolvers/FileResolver.js';
|
|
87
|
+
import type { AppContext } from '../types.js';
|
|
88
|
+
|
|
89
|
+
const PROVIDER_S3 = 'AAAAAAAA-0000-4000-8000-000000000001';
|
|
90
|
+
const PROVIDER_FINANCE = 'AAAAAAAA-0000-4000-8000-0000000000F1'; // the provider whose only account is ACCOUNT_FINANCE
|
|
91
|
+
const ACCOUNT_OPEN = 'BBBBBBBB-0000-4000-8000-000000000002'; // no permission rows → open (current rule)
|
|
92
|
+
const ACCOUNT_FINANCE = 'CCCCCCCC-0000-4000-8000-000000000003'; // Read+Write for the Finance role only
|
|
93
|
+
const ACCOUNT_READONLY = 'DDDDDDDD-0000-4000-8000-000000000004'; // Read (no Write) for Everyone
|
|
94
|
+
const ACCOUNT_MISSING = 'EEEEEEEE-0000-4000-8000-000000000005'; // does not exist
|
|
95
|
+
const ROLE_FINANCE = 'FFFFFFFF-0000-4000-8000-000000000006';
|
|
96
|
+
const FILE_SECRET = '99999999-0000-4000-8000-000000000007';
|
|
97
|
+
const FILE_MINE = '88888888-0000-4000-8000-000000000008';
|
|
98
|
+
const FILE_ALIAS = '77777777-0000-4000-8000-000000000009'; // plainUser's own row, pointed at HR's object
|
|
99
|
+
const FILE_Q3 = '66666666-0000-4000-8000-00000000000A'; // plainUser's row on the Finance-only provider
|
|
100
|
+
const FILE_NOKEY = '55555555-0000-4000-8000-00000000000B'; // plainUser's row stored with an empty ProviderKey
|
|
101
|
+
|
|
102
|
+
function makeUser(id: string, roleIDs: string[]): UserInfo {
|
|
103
|
+
return { ID: id, Name: id, Email: `${id}@example.com`, UserRoles: roleIDs.map(RoleID => ({ RoleID })) } as unknown as UserInfo;
|
|
104
|
+
}
|
|
105
|
+
const plainUser = makeUser('11111111-0000-4000-8000-000000000001', []);
|
|
106
|
+
const financeUser = makeUser('22222222-0000-4000-8000-000000000002', [ROLE_FINANCE]);
|
|
107
|
+
const systemUser = makeUser('33333333-0000-4000-8000-000000000003', []);
|
|
108
|
+
|
|
109
|
+
const ACCOUNTS = [ACCOUNT_OPEN, ACCOUNT_FINANCE, ACCOUNT_READONLY];
|
|
110
|
+
const PERMISSIONS: StorageAccountPermissionRow[] = [
|
|
111
|
+
{ FileStorageAccountID: ACCOUNT_FINANCE, Type: 'Role', RoleID: ROLE_FINANCE, UserID: null, CanRead: true, CanWrite: true },
|
|
112
|
+
{ FileStorageAccountID: ACCOUNT_READONLY, Type: 'Everyone', RoleID: null, UserID: null, CanRead: true, CanWrite: false },
|
|
113
|
+
];
|
|
114
|
+
/** MJ: Files rows on the S3 provider; plainUser may read only FILE_MINE. */
|
|
115
|
+
const FILES = [
|
|
116
|
+
{ ID: FILE_SECRET, Name: 'secret.pdf', ProviderKey: 'hr/secret.pdf', ProviderID: PROVIDER_S3, Status: 'Uploaded' },
|
|
117
|
+
{ ID: FILE_MINE, Name: 'mine.pdf', ProviderKey: 'me/mine.pdf', ProviderID: PROVIDER_S3, Status: 'Uploaded' },
|
|
118
|
+
{ ID: FILE_NOKEY, Name: 'open/brochure.pdf', ProviderKey: '', ProviderID: PROVIDER_S3, Status: 'Uploaded' },
|
|
119
|
+
{ ID: FILE_ALIAS, Name: 'innocent.pdf', ProviderKey: '/HR/secret.pdf', ProviderID: PROVIDER_S3, Status: 'Uploaded' },
|
|
120
|
+
{ ID: FILE_Q3, Name: 'q3.xlsx', ProviderKey: 'q3.xlsx', ProviderID: PROVIDER_FINANCE, Status: 'Uploaded' },
|
|
121
|
+
];
|
|
122
|
+
const READABLE_FILES: Record<string, string[]> = {
|
|
123
|
+
[plainUser.ID]: [FILE_MINE, FILE_ALIAS, FILE_Q3, FILE_NOKEY],
|
|
124
|
+
[financeUser.ID]: [FILE_SECRET, FILE_MINE, FILE_ALIAS, FILE_Q3],
|
|
125
|
+
};
|
|
126
|
+
/** The accounts on each provider, first one first — what ResolveFileObject resolves a row's provider to. */
|
|
127
|
+
const ACCOUNTS_BY_PROVIDER: Record<string, string[]> = { [PROVIDER_S3]: [ACCOUNT_OPEN], [PROVIDER_FINANCE]: [ACCOUNT_FINANCE] };
|
|
128
|
+
|
|
129
|
+
function namedIn(params: RunViewParams, value: string): boolean {
|
|
130
|
+
return (params.ExtraFilter ?? '').toLowerCase().includes(value.toLowerCase());
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The fake database behind the evaluator's RunView/RunViews. */
|
|
134
|
+
function answerView(params: RunViewParams, user: UserInfo | undefined): RunViewResult<object> {
|
|
135
|
+
const ok = (Results: object[]) => ({ Success: true, Results } as RunViewResult<object>);
|
|
136
|
+
switch (params.EntityName) {
|
|
137
|
+
case 'MJ: File Storage Accounts':
|
|
138
|
+
return ok(ACCOUNTS.filter(id => namedIn(params, id)).map(ID => ({ ID })));
|
|
139
|
+
case 'MJ: File Storage Account Permissions':
|
|
140
|
+
return ok(PERMISSIONS.filter(p => namedIn(params, p.FileStorageAccountID)));
|
|
141
|
+
case 'MJ: Files':
|
|
142
|
+
if (user?.ID === systemUser.ID) {
|
|
143
|
+
return ok(FILES.filter(f => namedIn(params, f.ProviderID) && namedIn(params, `'${(f.ProviderKey || f.Name).toLowerCase()}'`)));
|
|
144
|
+
}
|
|
145
|
+
return ok(FILES.filter(f => namedIn(params, f.ID) && (READABLE_FILES[user?.ID ?? ''] ?? []).includes(f.ID)).map(f => ({ ID: f.ID })));
|
|
146
|
+
default:
|
|
147
|
+
return { Success: false, Results: [], ErrorMessage: 'unknown entity' } as RunViewResult<object>;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const accountLoads: string[] = [];
|
|
152
|
+
|
|
153
|
+
function fakeEntity(entityName: string): object {
|
|
154
|
+
if (entityName === 'MJ: File Storage Accounts') {
|
|
155
|
+
const account = { ID: '', Name: '', ProviderID: PROVIDER_S3, Load: vi.fn(async (id: string) => {
|
|
156
|
+
accountLoads.push(id);
|
|
157
|
+
account.ID = id;
|
|
158
|
+
account.Name = `account ${id}`;
|
|
159
|
+
return ACCOUNTS.includes(id);
|
|
160
|
+
}) };
|
|
161
|
+
return account;
|
|
162
|
+
}
|
|
163
|
+
if (entityName === 'MJ: File Storage Providers') {
|
|
164
|
+
return { ID: PROVIDER_S3, Name: 'AWS S3 Storage', ServerDriverKey: 'AWS S3 Storage', Load: vi.fn(async () => true), Get: () => null };
|
|
165
|
+
}
|
|
166
|
+
return fakeFileEntity();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** An `MJ: Files` entity whose Load reads the fake table — as the caller's own row-level view would. */
|
|
170
|
+
function fakeFileEntity(): object {
|
|
171
|
+
const entity = {
|
|
172
|
+
ID: '', Name: '', ProviderKey: null as string | null, ProviderID: '', Status: '', ContentType: 'application/pdf',
|
|
173
|
+
CheckPermissions: vi.fn(),
|
|
174
|
+
GetAll: () => ({}),
|
|
175
|
+
Load: vi.fn(async (id: string) => {
|
|
176
|
+
const row = FILES.find(f => f.ID === id);
|
|
177
|
+
if (row) Object.assign(entity, row);
|
|
178
|
+
return !!row;
|
|
179
|
+
}),
|
|
180
|
+
};
|
|
181
|
+
return entity;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const fakeProvider = {
|
|
185
|
+
RunView: vi.fn(async (params: RunViewParams, user?: UserInfo) => answerView(params, user)),
|
|
186
|
+
RunViews: vi.fn(async (params: RunViewParams[], user?: UserInfo) => params.map(p => answerView(p, user))),
|
|
187
|
+
GetEntityObject: vi.fn(async (entityName: string) => fakeEntity(entityName)),
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
function contextFor(user: UserInfo): AppContext {
|
|
191
|
+
return {
|
|
192
|
+
providers: [{ provider: fakeProvider, type: 'Read-Write' }],
|
|
193
|
+
userPayload: { userRecord: user },
|
|
194
|
+
} as unknown as AppContext;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function accountEntity(id: string): object {
|
|
198
|
+
return { ID: id, Name: `account ${id}`, ProviderID: PROVIDER_S3 };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const fakeDriver = {
|
|
202
|
+
NormalizeObjectKey: (key: string) => NormalizeStorageObjectKey(key),
|
|
203
|
+
GetObject: vi.fn(async () => Buffer.from('bytes')),
|
|
204
|
+
CreatePreAuthDownloadUrl: vi.fn(async (key: string) => `https://signed.example/${key}`),
|
|
205
|
+
CreatePreAuthUploadUrl: vi.fn(async (key: string) => ({ UploadUrl: `https://upload.example/${key}` })),
|
|
206
|
+
CreateDirectory: vi.fn(async () => true),
|
|
207
|
+
SupportsPreAuthUpload: true,
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
describe('FileResolver — storage routes honour account permissions', () => {
|
|
211
|
+
const resolver = new FileResolver();
|
|
212
|
+
let systemUserSpy: ReturnType<typeof vi.spyOn>;
|
|
213
|
+
|
|
214
|
+
beforeEach(() => {
|
|
215
|
+
vi.clearAllMocks();
|
|
216
|
+
accountLoads.length = 0;
|
|
217
|
+
systemUserSpy = vi.spyOn(WellKnownUserSource.Instance, 'GetSystemUser').mockResolvedValue(systemUser);
|
|
218
|
+
mocks.engine.GetDriver.mockResolvedValue(fakeDriver);
|
|
219
|
+
mocks.engine.Base.GetAccountsByProviderID.mockImplementation((pid: string) => (ACCOUNTS_BY_PROVIDER[pid] ?? []).map(accountEntity));
|
|
220
|
+
mocks.engine.GetAccountById.mockImplementation((id: string) => (ACCOUNTS.includes(id) ? accountEntity(id) : undefined));
|
|
221
|
+
mocks.engine.GetAccountWithProvider.mockImplementation((id: string) =>
|
|
222
|
+
ACCOUNTS.some(a => a.toLowerCase() === id.toLowerCase())
|
|
223
|
+
? { account: accountEntity(id), provider: { ID: PROVIDER_S3, Name: 'AWS S3 Storage', SupportsSearch: true } }
|
|
224
|
+
: null);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
afterEach(() => {
|
|
228
|
+
systemUserSpy.mockRestore();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe('CreatePreAuthDownloadUrl', () => {
|
|
232
|
+
it('refuses an account restricted to a role the caller lacks — before any driver call', async () => {
|
|
233
|
+
await expect(resolver.CreatePreAuthDownloadUrl({ AccountID: ACCOUNT_FINANCE, ObjectName: 'q3.pdf' }, contextFor(plainUser)))
|
|
234
|
+
.rejects.toThrow(STORAGE_ACCOUNT_ACCESS_DENIED_MESSAGE);
|
|
235
|
+
expect(mocks.engine.GetDriver).not.toHaveBeenCalled();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('refuses an account that does not exist with the SAME message (existence is not revealed)', async () => {
|
|
239
|
+
await expect(resolver.CreatePreAuthDownloadUrl({ AccountID: ACCOUNT_MISSING, ObjectName: 'q3.pdf' }, contextFor(financeUser)))
|
|
240
|
+
.rejects.toThrow(STORAGE_ACCOUNT_ACCESS_DENIED_MESSAGE);
|
|
241
|
+
expect(mocks.engine.GetDriver).not.toHaveBeenCalled();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('allows an account with zero permission rows (current rule) for an untracked object', async () => {
|
|
245
|
+
const url = await resolver.CreatePreAuthDownloadUrl({ AccountID: ACCOUNT_OPEN, ObjectName: 'public/brochure.pdf' }, contextFor(plainUser));
|
|
246
|
+
expect(url).toBe('https://signed.example/public/brochure.pdf');
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('refuses a tracked object whose MJ: Files row the caller cannot read, with GetFileContents\' message', async () => {
|
|
250
|
+
await expect(resolver.CreatePreAuthDownloadUrl({ AccountID: ACCOUNT_OPEN, ObjectName: 'hr/secret.pdf' }, contextFor(plainUser)))
|
|
251
|
+
.rejects.toThrow(TRACKED_FILE_ACCESS_DENIED_MESSAGE);
|
|
252
|
+
expect(fakeDriver.CreatePreAuthDownloadUrl).not.toHaveBeenCalled();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('refuses the tracked object however the key is spelled (leading/trailing slash, case) — and refuses traversal', async () => {
|
|
256
|
+
for (const spelling of ['/hr/secret.pdf', 'hr/secret.pdf/', 'HR/SECRET.PDF', 'me/../hr/secret.pdf', 'hr%2Fsecret.pdf']) {
|
|
257
|
+
await expect(resolver.CreatePreAuthDownloadUrl({ AccountID: ACCOUNT_OPEN, ObjectName: spelling }, contextFor(plainUser)))
|
|
258
|
+
.rejects.toThrow(TRACKED_FILE_ACCESS_DENIED_MESSAGE);
|
|
259
|
+
}
|
|
260
|
+
expect(fakeDriver.CreatePreAuthDownloadUrl).not.toHaveBeenCalled();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('allows a tracked object whose MJ: Files row the caller can read', async () => {
|
|
264
|
+
const url = await resolver.CreatePreAuthDownloadUrl({ AccountID: ACCOUNT_OPEN, ObjectName: 'me/mine.pdf' }, contextFor(plainUser));
|
|
265
|
+
expect(url).toBe('https://signed.example/me/mine.pdf');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('lets the permitted role through the gate', async () => {
|
|
269
|
+
const url = await resolver.CreatePreAuthDownloadUrl({ AccountID: ACCOUNT_FINANCE, ObjectName: 'q3.pdf' }, contextFor(financeUser));
|
|
270
|
+
expect(url).toBe('https://signed.example/q3.pdf');
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
describe('write routes need CanWrite, and are refused before the account is loaded or a driver is touched', () => {
|
|
275
|
+
const ctx = () => contextFor(plainUser);
|
|
276
|
+
const cases: Array<[string, () => Promise<unknown>, () => void]> = [
|
|
277
|
+
['DeleteStorageObject', () => resolver.DeleteStorageObject({ AccountID: ACCOUNT_READONLY, ObjectName: 'x' }, ctx()),
|
|
278
|
+
() => expect(mocks.deleteObject).not.toHaveBeenCalled()],
|
|
279
|
+
['MoveStorageObject', () => resolver.MoveStorageObject({ AccountID: ACCOUNT_READONLY, OldName: 'x', NewName: 'y' }, ctx()),
|
|
280
|
+
() => expect(mocks.moveObject).not.toHaveBeenCalled()],
|
|
281
|
+
['CopyStorageObject', () => resolver.CopyStorageObject({ AccountID: ACCOUNT_READONLY, SourceName: 'x', DestinationName: 'y' }, ctx()),
|
|
282
|
+
() => expect(mocks.copyObject).not.toHaveBeenCalled()],
|
|
283
|
+
['CreateDirectory', () => resolver.CreateDirectory({ AccountID: ACCOUNT_READONLY, Path: 'new/' }, ctx()),
|
|
284
|
+
() => expect(fakeDriver.CreateDirectory).not.toHaveBeenCalled()],
|
|
285
|
+
['CreatePreAuthUploadUrl', () => resolver.CreatePreAuthUploadUrl({ AccountID: ACCOUNT_READONLY, ObjectName: 'x' }, ctx()),
|
|
286
|
+
() => expect(fakeDriver.CreatePreAuthUploadUrl).not.toHaveBeenCalled()],
|
|
287
|
+
];
|
|
288
|
+
for (const [name, call, assertNoSideEffect] of cases) {
|
|
289
|
+
it(`${name} refuses a read-only (CanWrite=false) account`, async () => {
|
|
290
|
+
await expect(call()).rejects.toThrow(STORAGE_ACCOUNT_ACCESS_DENIED_MESSAGE);
|
|
291
|
+
assertNoSideEffect();
|
|
292
|
+
expect(mocks.engine.GetDriver).not.toHaveBeenCalled();
|
|
293
|
+
expect(accountLoads).toHaveLength(0);
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
it('DeleteStorageObject proceeds on a zero-row account', async () => {
|
|
298
|
+
await expect(resolver.DeleteStorageObject({ AccountID: ACCOUNT_OPEN, ObjectName: 'x' }, ctx())).resolves.toBe(true);
|
|
299
|
+
expect(mocks.deleteObject).toHaveBeenCalledTimes(1);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
describe('read routes', () => {
|
|
304
|
+
it('ListStorageObjects refuses a restricted account before loading it', async () => {
|
|
305
|
+
await expect(resolver.ListStorageObjects({ AccountID: ACCOUNT_FINANCE, Prefix: '' }, contextFor(plainUser)))
|
|
306
|
+
.rejects.toThrow(STORAGE_ACCOUNT_ACCESS_DENIED_MESSAGE);
|
|
307
|
+
expect(mocks.listObjects).not.toHaveBeenCalled();
|
|
308
|
+
expect(accountLoads).toHaveLength(0);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('ListStorageObjects lists a read-only account (CanRead suffices)', async () => {
|
|
312
|
+
await resolver.ListStorageObjects({ AccountID: ACCOUNT_READONLY, Prefix: '' }, contextFor(plainUser));
|
|
313
|
+
expect(mocks.listObjects).toHaveBeenCalledTimes(1);
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
describe('CopyObjectBetweenAccounts', () => {
|
|
318
|
+
it('needs Read on the source and Write on the destination', async () => {
|
|
319
|
+
const call = (src: string, dst: string) => resolver.CopyObjectBetweenAccounts(
|
|
320
|
+
{ SourceAccountID: src, DestinationAccountID: dst, SourcePath: 'a', DestinationPath: 'b' }, contextFor(plainUser));
|
|
321
|
+
await expect(call(ACCOUNT_READONLY, ACCOUNT_READONLY)).rejects.toThrow(STORAGE_ACCOUNT_ACCESS_DENIED_MESSAGE);
|
|
322
|
+
await expect(call(ACCOUNT_FINANCE, ACCOUNT_OPEN)).rejects.toThrow(STORAGE_ACCOUNT_ACCESS_DENIED_MESSAGE);
|
|
323
|
+
expect(mocks.copyObjectBetweenProviders).not.toHaveBeenCalled();
|
|
324
|
+
|
|
325
|
+
await call(ACCOUNT_READONLY, ACCOUNT_OPEN);
|
|
326
|
+
expect(mocks.copyObjectBetweenProviders).toHaveBeenCalledTimes(1);
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
describe('UploadStorageFile', () => {
|
|
331
|
+
it('refuses an account the caller may not write to, without consuming the staged upload', async () => {
|
|
332
|
+
const result = await resolver.UploadStorageFile({ AccountID: ACCOUNT_READONLY, UploadToken: 'tok' }, contextFor(plainUser));
|
|
333
|
+
expect(result.Success).toBe(false);
|
|
334
|
+
expect(result.ErrorMessage).toBe(STORAGE_ACCOUNT_ACCESS_DENIED_MESSAGE);
|
|
335
|
+
expect(mocks.consumeUploadToken).not.toHaveBeenCalled();
|
|
336
|
+
expect(mocks.engine.UploadFile).not.toHaveBeenCalled();
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('gates the engine\'s default account when none is requested, and uploads to exactly that account', async () => {
|
|
340
|
+
mocks.engine.ResolveStorageAccount.mockReturnValue({ account: accountEntity(ACCOUNT_OPEN), provider: {} });
|
|
341
|
+
mocks.engine.UploadFile.mockResolvedValue({ FileID: FILE_MINE });
|
|
342
|
+
const result = await resolver.UploadStorageFile({ Base64Data: 'aGk=', FileName: 'hi.txt' }, contextFor(plainUser));
|
|
343
|
+
expect(result.Success).toBe(true);
|
|
344
|
+
expect(mocks.engine.UploadFile.mock.calls[0][0].storageAccountId).toBe(ACCOUNT_OPEN);
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
describe('SearchAcrossAccounts', () => {
|
|
349
|
+
beforeEach(() => {
|
|
350
|
+
mocks.searchAcrossAccounts.mockImplementation(async (accounts: Array<{ accountEntity: { ID: string; Name: string } }>) => ({
|
|
351
|
+
accountResults: accounts.map(a => ({
|
|
352
|
+
accountID: a.accountEntity.ID, accountName: a.accountEntity.Name, providerID: PROVIDER_S3, providerName: 'AWS S3 Storage',
|
|
353
|
+
success: true, hasMore: false,
|
|
354
|
+
results: ['hr/secret.pdf', 'me/mine.pdf', 'open/notes.md'].map(path => ({
|
|
355
|
+
path, name: path, size: 1, contentType: 'text/plain', lastModified: new Date(0),
|
|
356
|
+
})),
|
|
357
|
+
})),
|
|
358
|
+
totalResultsReturned: accounts.length * 3, successfulAccounts: accounts.length, failedAccounts: 0,
|
|
359
|
+
}));
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('drops denied (and unknown) accounts before any driver call, reporting them as failed with "access denied"', async () => {
|
|
363
|
+
const payload = await resolver.SearchAcrossAccounts(
|
|
364
|
+
{ AccountIDs: [ACCOUNT_OPEN, ACCOUNT_FINANCE, ACCOUNT_MISSING], Query: 'q' }, contextFor(plainUser));
|
|
365
|
+
|
|
366
|
+
const searched = mocks.searchAcrossAccounts.mock.calls[0][0] as Array<{ accountEntity: { ID: string } }>;
|
|
367
|
+
expect(searched.map(a => a.accountEntity.ID)).toEqual([ACCOUNT_OPEN]);
|
|
368
|
+
|
|
369
|
+
const refused = payload.accountResults.filter(r => !r.success);
|
|
370
|
+
expect(refused.map(r => r.accountID).sort()).toEqual([ACCOUNT_FINANCE, ACCOUNT_MISSING].sort());
|
|
371
|
+
expect(refused.every(r => /access denied/i.test(r.errorMessage ?? '') && r.accountName === '')).toBe(true);
|
|
372
|
+
expect(payload.failedAccounts).toBe(2);
|
|
373
|
+
expect(payload.successfulAccounts).toBe(1);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('drops hits that map to an MJ: Files row the caller cannot read, and recomputes the total', async () => {
|
|
377
|
+
const payload = await resolver.SearchAcrossAccounts({ AccountIDs: [ACCOUNT_OPEN], Query: 'q' }, contextFor(plainUser));
|
|
378
|
+
const paths = payload.accountResults[0].results.map(r => r.path);
|
|
379
|
+
expect(paths).toEqual(['me/mine.pdf', 'open/notes.md']);
|
|
380
|
+
expect(payload.totalResultsReturned).toBe(2);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('searches nothing and reports every account refused when none is readable', async () => {
|
|
384
|
+
const payload = await resolver.SearchAcrossAccounts({ AccountIDs: [ACCOUNT_FINANCE], Query: 'q' }, contextFor(plainUser));
|
|
385
|
+
expect(mocks.searchAcrossAccounts).not.toHaveBeenCalled();
|
|
386
|
+
expect(payload.failedAccounts).toBe(1);
|
|
387
|
+
expect(payload.totalResultsReturned).toBe(0);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('reports accounts in the order requested — a refused account sits where the caller put it', async () => {
|
|
391
|
+
const payload = await resolver.SearchAcrossAccounts(
|
|
392
|
+
{ AccountIDs: [ACCOUNT_FINANCE, ACCOUNT_OPEN, ACCOUNT_MISSING], Query: 'q' }, contextFor(plainUser));
|
|
393
|
+
expect(payload.accountResults.map(r => r.accountID)).toEqual([ACCOUNT_FINANCE, ACCOUNT_OPEN, ACCOUNT_MISSING]);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('withholds the provider total and page token once a hit was dropped (they counted it)', async () => {
|
|
397
|
+
mocks.searchAcrossAccounts.mockImplementationOnce(async (accounts: Array<{ accountEntity: { ID: string; Name: string } }>) => ({
|
|
398
|
+
accountResults: accounts.map(a => ({
|
|
399
|
+
accountID: a.accountEntity.ID, accountName: a.accountEntity.Name, providerID: PROVIDER_S3, providerName: 'AWS S3 Storage',
|
|
400
|
+
success: true, hasMore: true, totalMatches: 40, nextPageToken: 'page-2',
|
|
401
|
+
results: ['hr/secret.pdf', 'open/notes.md'].map(path => ({
|
|
402
|
+
path, name: path, size: 1, contentType: 'text/plain', lastModified: new Date(0),
|
|
403
|
+
})),
|
|
404
|
+
})),
|
|
405
|
+
totalResultsReturned: 2, successfulAccounts: 1, failedAccounts: 0,
|
|
406
|
+
}));
|
|
407
|
+
const payload = await resolver.SearchAcrossAccounts({ AccountIDs: [ACCOUNT_OPEN], Query: 'q' }, contextFor(plainUser));
|
|
408
|
+
const account = payload.accountResults[0];
|
|
409
|
+
expect(account.results.map(r => r.path)).toEqual(['open/notes.md']);
|
|
410
|
+
expect(account.totalMatches).toBeUndefined();
|
|
411
|
+
expect(account.nextPageToken).toBeUndefined();
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
describe('routes that go from a file ID to an object gate the account the row resolves to, and its object', () => {
|
|
416
|
+
const fileRoot = (id: string) => {
|
|
417
|
+
const row = FILES.find(f => f.ID === id)!;
|
|
418
|
+
const root = { ID: row.ID, Name: row.Name, ProviderKey: row.ProviderKey, ProviderID: row.ProviderID };
|
|
419
|
+
return root as unknown as Parameters<FileResolver['DownloadUrl']>[0];
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
it('DownloadUrl refuses a row whose provider resolves to an account the caller may not read', async () => {
|
|
423
|
+
await expect(resolver.DownloadUrl(fileRoot(FILE_Q3), contextFor(plainUser))).rejects.toThrow(STORAGE_ACCOUNT_ACCESS_DENIED_MESSAGE);
|
|
424
|
+
expect(fakeDriver.CreatePreAuthDownloadUrl).not.toHaveBeenCalled();
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('DownloadUrl refuses a readable row that aliases another row\'s object the caller cannot read', async () => {
|
|
428
|
+
await expect(resolver.DownloadUrl(fileRoot(FILE_ALIAS), contextFor(plainUser))).rejects.toThrow(TRACKED_FILE_ACCESS_DENIED_MESSAGE);
|
|
429
|
+
expect(fakeDriver.CreatePreAuthDownloadUrl).not.toHaveBeenCalled();
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it('DownloadUrl signs a readable row on a readable account; the Finance role reads its own provider', async () => {
|
|
433
|
+
await expect(resolver.DownloadUrl(fileRoot(FILE_MINE), contextFor(plainUser))).resolves.toBe('https://signed.example/me/mine.pdf');
|
|
434
|
+
await expect(resolver.DownloadUrl(fileRoot(FILE_Q3), contextFor(financeUser))).resolves.toBe('https://signed.example/q3.xlsx');
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it('GetFileContents refuses an aliasing row and a refused account, and returns a readable file\'s bytes', async () => {
|
|
438
|
+
const alias = await resolver.GetFileContents(FILE_ALIAS, contextFor(plainUser));
|
|
439
|
+
expect(alias).toEqual({ Success: false, ErrorMessage: TRACKED_FILE_ACCESS_DENIED_MESSAGE });
|
|
440
|
+
const q3 = await resolver.GetFileContents(FILE_Q3, contextFor(plainUser));
|
|
441
|
+
expect(q3).toEqual({ Success: false, ErrorMessage: STORAGE_ACCOUNT_ACCESS_DENIED_MESSAGE });
|
|
442
|
+
expect(fakeDriver.GetObject).not.toHaveBeenCalled();
|
|
443
|
+
|
|
444
|
+
const mine = await resolver.GetFileContents(FILE_MINE, contextFor(plainUser));
|
|
445
|
+
expect(mine.Success).toBe(true);
|
|
446
|
+
expect(mine.Base64).toBe(Buffer.from('bytes').toString('base64'));
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it('GetFileContents reads a row stored with an empty ProviderKey from its Name, as DownloadUrl does', async () => {
|
|
450
|
+
const result = await resolver.GetFileContents(FILE_NOKEY, contextFor(plainUser));
|
|
451
|
+
expect(result.Success).toBe(true);
|
|
452
|
+
expect(fakeDriver.GetObject).toHaveBeenCalledWith({ fullPath: 'open/brochure.pdf' });
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it('CreateMediaAccessToken mints nothing for an aliasing row', async () => {
|
|
456
|
+
const result = await resolver.CreateMediaAccessToken(FILE_ALIAS, contextFor(plainUser));
|
|
457
|
+
expect(result).toEqual({ Success: false, ErrorMessage: TRACKED_FILE_ACCESS_DENIED_MESSAGE });
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it('DeleteFile refuses to delete the object behind an aliasing row', async () => {
|
|
461
|
+
await expect(resolver.DeleteFile(FILE_ALIAS, new DeleteOptionsInput(), contextFor(plainUser), {} as unknown as PubSubEngine))
|
|
462
|
+
.rejects.toThrow(TRACKED_FILE_ACCESS_DENIED_MESSAGE);
|
|
463
|
+
expect(mocks.deleteObject).not.toHaveBeenCalled();
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
describe('CreateFile (legacy upload) signs an overwrite only where the caller may write it', () => {
|
|
468
|
+
const create = (input: { Name: string; ProviderID: string; ProviderKey?: string }, user: UserInfo) =>
|
|
469
|
+
resolver.CreateFile(input as unknown as Parameters<FileResolver['CreateFile']>[0], contextFor(user), {} as unknown as PubSubEngine);
|
|
470
|
+
|
|
471
|
+
it('refuses a Name that is another row\'s tracked object the caller cannot read, however it is spelled', async () => {
|
|
472
|
+
for (const Name of ['hr/secret.pdf', '/HR/secret.pdf']) {
|
|
473
|
+
await expect(create({ Name, ProviderID: PROVIDER_S3 }, plainUser)).rejects.toThrow(TRACKED_FILE_ACCESS_DENIED_MESSAGE);
|
|
474
|
+
}
|
|
475
|
+
expect(mocks.createUploadUrl).not.toHaveBeenCalled();
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it('refuses a client ProviderKey that is a tracked object the caller cannot read', async () => {
|
|
479
|
+
await expect(create({ Name: 'new.pdf', ProviderID: PROVIDER_S3, ProviderKey: 'hr/secret.pdf' }, plainUser))
|
|
480
|
+
.rejects.toThrow(TRACKED_FILE_ACCESS_DENIED_MESSAGE);
|
|
481
|
+
expect(mocks.createUploadUrl).not.toHaveBeenCalled();
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it('refuses a provider whose account the caller may not write', async () => {
|
|
485
|
+
await expect(create({ Name: 'new.xlsx', ProviderID: PROVIDER_FINANCE }, plainUser)).rejects.toThrow(STORAGE_ACCOUNT_ACCESS_DENIED_MESSAGE);
|
|
486
|
+
expect(mocks.createUploadUrl).not.toHaveBeenCalled();
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
describe('account-keyed writes apply the tracked-file rule to what they read, replace or remove', () => {
|
|
491
|
+
const ctx = () => contextFor(plainUser);
|
|
492
|
+
const cases: Array<[string, () => Promise<unknown>, () => void]> = [
|
|
493
|
+
['DeleteStorageObject', () => resolver.DeleteStorageObject({ AccountID: ACCOUNT_OPEN, ObjectName: '/hr/secret.pdf' }, ctx()),
|
|
494
|
+
() => expect(mocks.deleteObject).not.toHaveBeenCalled()],
|
|
495
|
+
['MoveStorageObject (tracked source)',
|
|
496
|
+
() => resolver.MoveStorageObject({ AccountID: ACCOUNT_OPEN, OldName: 'hr/secret.pdf', NewName: 'mine/x.pdf' }, ctx()),
|
|
497
|
+
() => expect(mocks.moveObject).not.toHaveBeenCalled()],
|
|
498
|
+
['MoveStorageObject (tracked destination)',
|
|
499
|
+
() => resolver.MoveStorageObject({ AccountID: ACCOUNT_OPEN, OldName: 'scratch/x.pdf', NewName: 'HR/secret.pdf' }, ctx()),
|
|
500
|
+
() => expect(mocks.moveObject).not.toHaveBeenCalled()],
|
|
501
|
+
['CopyStorageObject (tracked source)',
|
|
502
|
+
() => resolver.CopyStorageObject({ AccountID: ACCOUNT_OPEN, SourceName: 'hr/secret.pdf/', DestinationName: 'mine/copy.pdf' }, ctx()),
|
|
503
|
+
() => expect(mocks.copyObject).not.toHaveBeenCalled()],
|
|
504
|
+
['CopyObjectBetweenAccounts (tracked source)', () => resolver.CopyObjectBetweenAccounts(
|
|
505
|
+
{ SourceAccountID: ACCOUNT_OPEN, DestinationAccountID: ACCOUNT_OPEN, SourcePath: 'hr/secret.pdf', DestinationPath: 'mine/copy.pdf' }, ctx()),
|
|
506
|
+
() => expect(mocks.copyObjectBetweenProviders).not.toHaveBeenCalled()],
|
|
507
|
+
['CreatePreAuthUploadUrl (overwrite)', () => resolver.CreatePreAuthUploadUrl({ AccountID: ACCOUNT_OPEN, ObjectName: 'hr/secret.pdf' }, ctx()),
|
|
508
|
+
() => expect(fakeDriver.CreatePreAuthUploadUrl).not.toHaveBeenCalled()],
|
|
509
|
+
];
|
|
510
|
+
for (const [name, call, assertNoSideEffect] of cases) {
|
|
511
|
+
it(`${name} refuses an object behind an MJ: Files row the caller cannot read`, async () => {
|
|
512
|
+
await expect(call()).rejects.toThrow(TRACKED_FILE_ACCESS_DENIED_MESSAGE);
|
|
513
|
+
assertNoSideEffect();
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
it('ListStorageObjects drops the tracked object the caller cannot read and keeps the rest', async () => {
|
|
518
|
+
mocks.listObjects.mockResolvedValueOnce({
|
|
519
|
+
objects: [
|
|
520
|
+
{ name: 'secret.pdf', path: 'hr', fullPath: 'hr/secret.pdf', size: 1, contentType: 'application/pdf',
|
|
521
|
+
lastModified: new Date(0), isDirectory: false },
|
|
522
|
+
{ name: 'notes.md', path: 'hr', fullPath: 'hr/notes.md', size: 1, contentType: 'text/plain',
|
|
523
|
+
lastModified: new Date(0), isDirectory: false },
|
|
524
|
+
],
|
|
525
|
+
prefixes: [],
|
|
526
|
+
});
|
|
527
|
+
const result = await resolver.ListStorageObjects({ AccountID: ACCOUNT_OPEN, Prefix: 'hr/' }, contextFor(plainUser));
|
|
528
|
+
expect(result.objects.map(o => o.fullPath)).toEqual(['hr/notes.md']);
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
describe('a cache miss is refreshed as the system user, never as the caller', () => {
|
|
532
|
+
/** Each engine lookup misses until a forced Config has run, as it would for an account added after the last load. */
|
|
533
|
+
function missUntilRefreshed(): void {
|
|
534
|
+
let refreshed = false;
|
|
535
|
+
mocks.engine.Config.mockImplementation(async (force?: boolean) => { if (force) refreshed = true; });
|
|
536
|
+
const byId = mocks.engine.GetAccountById.getMockImplementation();
|
|
537
|
+
const withProvider = mocks.engine.GetAccountWithProvider.getMockImplementation();
|
|
538
|
+
mocks.engine.GetAccountById.mockImplementation((id: string) => (refreshed ? byId?.(id) : undefined));
|
|
539
|
+
mocks.engine.GetAccountWithProvider.mockImplementation((id: string) => (refreshed ? withProvider?.(id) : null));
|
|
540
|
+
}
|
|
541
|
+
const forcedConfigUsers = () => mocks.engine.Config.mock.calls.filter(c => c[0] === true).map(c => c[1]);
|
|
542
|
+
|
|
543
|
+
it('ListStorageObjects: the tracked-file check reloads the account as the system user', async () => {
|
|
544
|
+
missUntilRefreshed();
|
|
545
|
+
mocks.listObjects.mockResolvedValueOnce({
|
|
546
|
+
objects: [{ name: 'notes.md', path: 'hr', fullPath: 'hr/notes.md', size: 1, contentType: 'text/plain',
|
|
547
|
+
lastModified: new Date(0), isDirectory: false }],
|
|
548
|
+
prefixes: [],
|
|
549
|
+
});
|
|
550
|
+
await resolver.ListStorageObjects({ AccountID: ACCOUNT_OPEN, Prefix: 'hr/' }, contextFor(plainUser)).catch(() => undefined);
|
|
551
|
+
expect(forcedConfigUsers().length).toBeGreaterThan(0);
|
|
552
|
+
expect(forcedConfigUsers().every(u => u === systemUser)).toBe(true);
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it('SearchAcrossAccounts: a readable account missing from the cache is reloaded as the system user', async () => {
|
|
556
|
+
missUntilRefreshed();
|
|
557
|
+
mocks.searchAcrossAccounts.mockResolvedValue({ accountResults: [], totalResultsReturned: 0, successfulAccounts: 0, failedAccounts: 0 });
|
|
558
|
+
await resolver.SearchAcrossAccounts({ AccountIDs: [ACCOUNT_OPEN], Query: 'q' }, contextFor(plainUser));
|
|
559
|
+
expect(forcedConfigUsers()).toEqual([systemUser]);
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
});
|