@memberjunction/server 6.2.0-edge.0 → 6.2.0-edge.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/agentSessions/ReturningVisitorRecap.d.ts +2 -0
- package/dist/agentSessions/ReturningVisitorRecap.d.ts.map +1 -1
- package/dist/agentSessions/ReturningVisitorRecap.js +5 -1
- package/dist/agentSessions/ReturningVisitorRecap.js.map +1 -1
- package/dist/agentSessions/SessionManager.js +2 -2
- package/dist/agentSessions/remoteBrowserGoalEngine.d.ts +8 -0
- package/dist/agentSessions/remoteBrowserGoalEngine.d.ts.map +1 -1
- package/dist/agentSessions/remoteBrowserGoalEngine.js +21 -5
- package/dist/agentSessions/remoteBrowserGoalEngine.js.map +1 -1
- package/dist/auth/AuthProviderCatalogRouter.d.ts +2 -0
- package/dist/auth/AuthProviderCatalogRouter.d.ts.map +1 -1
- package/dist/auth/AuthProviderCatalogRouter.js +5 -1
- package/dist/auth/AuthProviderCatalogRouter.js.map +1 -1
- package/dist/auth/index.d.ts +20 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +20 -10
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/initializeProviders.d.ts +8 -0
- package/dist/auth/initializeProviders.d.ts.map +1 -1
- package/dist/auth/initializeProviders.js +21 -5
- package/dist/auth/initializeProviders.js.map +1 -1
- package/dist/auth/magicLink/MagicLinkRouter.d.ts +9 -0
- package/dist/auth/magicLink/MagicLinkRouter.d.ts.map +1 -1
- package/dist/auth/magicLink/MagicLinkRouter.js +22 -10
- package/dist/auth/magicLink/MagicLinkRouter.js.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.d.ts +17 -15
- package/dist/auth/magicLink/MagicLinkService.d.ts.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.js +65 -51
- package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
- package/dist/auth/magicLink/index.d.ts +2 -2
- package/dist/auth/magicLink/index.d.ts.map +1 -1
- package/dist/auth/magicLink/index.js +2 -2
- package/dist/auth/magicLink/index.js.map +1 -1
- package/dist/auth/magicLink/magicLinkCore.d.ts +54 -33
- package/dist/auth/magicLink/magicLinkCore.d.ts.map +1 -1
- package/dist/auth/magicLink/magicLinkCore.js +61 -61
- package/dist/auth/magicLink/magicLinkCore.js.map +1 -1
- package/dist/auth/magicLink/redeemLanding.d.ts +4 -0
- package/dist/auth/magicLink/redeemLanding.d.ts.map +1 -1
- package/dist/auth/magicLink/redeemLanding.js +12 -4
- package/dist/auth/magicLink/redeemLanding.js.map +1 -1
- package/dist/auth/magicLink/types.d.ts +4 -4
- package/dist/auth/principals.d.ts +2 -0
- package/dist/auth/principals.d.ts.map +1 -1
- package/dist/auth/principals.js +6 -2
- package/dist/auth/principals.js.map +1 -1
- package/dist/cache.d.ts +2 -0
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +3 -1
- package/dist/cache.js.map +1 -1
- package/dist/config.d.ts +346 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +34 -4
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +20 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +27 -19
- package/dist/context.js.map +1 -1
- package/dist/directives/Public.d.ts +2 -0
- package/dist/directives/Public.d.ts.map +1 -1
- package/dist/directives/Public.js +3 -1
- package/dist/directives/Public.js.map +1 -1
- package/dist/directives/RequireSystemUser.d.ts +2 -0
- package/dist/directives/RequireSystemUser.d.ts.map +1 -1
- package/dist/directives/RequireSystemUser.js +3 -1
- package/dist/directives/RequireSystemUser.js.map +1 -1
- package/dist/generated/graphql-schemas/__mj.d.ts +1021 -0
- package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
- package/dist/generated/graphql-schemas/__mj.js +7368 -2145
- package/dist/generated/graphql-schemas/__mj.js.map +1 -1
- package/dist/generic/CacheInvalidationResolver.d.ts +94 -4
- package/dist/generic/CacheInvalidationResolver.d.ts.map +1 -1
- package/dist/generic/CacheInvalidationResolver.js +122 -3
- package/dist/generic/CacheInvalidationResolver.js.map +1 -1
- package/dist/generic/DeleteOptionsInput.d.ts +8 -1
- package/dist/generic/DeleteOptionsInput.d.ts.map +1 -1
- package/dist/generic/DeleteOptionsInput.js +1 -1
- package/dist/generic/DeleteOptionsInput.js.map +1 -1
- package/dist/generic/FireAndForgetHeartbeat.d.ts +18 -2
- package/dist/generic/FireAndForgetHeartbeat.d.ts.map +1 -1
- package/dist/generic/FireAndForgetHeartbeat.js +24 -6
- package/dist/generic/FireAndForgetHeartbeat.js.map +1 -1
- package/dist/generic/OrphanedConversationDetailReconciler.d.ts +31 -0
- package/dist/generic/OrphanedConversationDetailReconciler.d.ts.map +1 -0
- package/dist/generic/OrphanedConversationDetailReconciler.js +215 -0
- package/dist/generic/OrphanedConversationDetailReconciler.js.map +1 -0
- package/dist/generic/PushStatusResolver.d.ts +48 -1
- package/dist/generic/PushStatusResolver.d.ts.map +1 -1
- package/dist/generic/PushStatusResolver.js +95 -4
- package/dist/generic/PushStatusResolver.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +18 -2
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +64 -55
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +120 -11
- package/dist/index.js.map +1 -1
- package/dist/integration/CustomColumnPromoter.d.ts +2 -0
- package/dist/integration/CustomColumnPromoter.d.ts.map +1 -1
- package/dist/integration/CustomColumnPromoter.js +5 -1
- package/dist/integration/CustomColumnPromoter.js.map +1 -1
- package/dist/integration/EntityMapLifecycle.d.ts +4 -0
- package/dist/integration/EntityMapLifecycle.d.ts.map +1 -1
- package/dist/integration/EntityMapLifecycle.js +10 -2
- package/dist/integration/EntityMapLifecycle.js.map +1 -1
- package/dist/logging/NoLog.d.ts +4 -0
- package/dist/logging/NoLog.d.ts.map +1 -1
- package/dist/logging/NoLog.js +10 -2
- package/dist/logging/NoLog.js.map +1 -1
- package/dist/logging/StartupLogger.d.ts +2 -0
- package/dist/logging/StartupLogger.d.ts.map +1 -1
- package/dist/logging/StartupLogger.js +5 -1
- package/dist/logging/StartupLogger.js.map +1 -1
- package/dist/logging/bootAudit.d.ts +11 -7
- package/dist/logging/bootAudit.d.ts.map +1 -1
- package/dist/logging/bootAudit.js +15 -7
- package/dist/logging/bootAudit.js.map +1 -1
- package/dist/logging/boundaryLogPayload.d.ts +4 -0
- package/dist/logging/boundaryLogPayload.d.ts.map +1 -1
- package/dist/logging/boundaryLogPayload.js +5 -1
- package/dist/logging/boundaryLogPayload.js.map +1 -1
- package/dist/logging/secretRedactor.d.ts +2 -0
- package/dist/logging/secretRedactor.d.ts.map +1 -1
- package/dist/logging/secretRedactor.js +8 -4
- package/dist/logging/secretRedactor.js.map +1 -1
- package/dist/logging/shortenForLog.d.ts +2 -0
- package/dist/logging/shortenForLog.d.ts.map +1 -1
- package/dist/logging/shortenForLog.js +7 -3
- package/dist/logging/shortenForLog.js.map +1 -1
- package/dist/logging/variablesLoggingMiddleware.d.ts +2 -0
- package/dist/logging/variablesLoggingMiddleware.d.ts.map +1 -1
- package/dist/logging/variablesLoggingMiddleware.js +8 -6
- package/dist/logging/variablesLoggingMiddleware.js.map +1 -1
- package/dist/metadataRefreshSignal.d.ts +19 -0
- package/dist/metadataRefreshSignal.d.ts.map +1 -0
- package/dist/metadataRefreshSignal.js +45 -0
- package/dist/metadataRefreshSignal.js.map +1 -0
- package/dist/middleware/MJTenantFilterMiddleware.js +4 -4
- package/dist/multiTenancy/index.d.ts +8 -0
- package/dist/multiTenancy/index.d.ts.map +1 -1
- package/dist/multiTenancy/index.js +21 -5
- package/dist/multiTenancy/index.js.map +1 -1
- package/dist/realtimeWidget/WidgetRouter.d.ts +5 -0
- package/dist/realtimeWidget/WidgetRouter.d.ts.map +1 -1
- package/dist/realtimeWidget/WidgetRouter.js +14 -10
- package/dist/realtimeWidget/WidgetRouter.js.map +1 -1
- package/dist/realtimeWidget/WidgetSessionService.d.ts +6 -6
- package/dist/realtimeWidget/WidgetSessionService.js +27 -27
- package/dist/realtimeWidget/host-identity.d.ts +5 -1
- package/dist/realtimeWidget/host-identity.d.ts.map +1 -1
- package/dist/realtimeWidget/host-identity.js +11 -3
- package/dist/realtimeWidget/host-identity.js.map +1 -1
- package/dist/realtimeWidget/index.d.ts +2 -2
- package/dist/realtimeWidget/index.d.ts.map +1 -1
- package/dist/realtimeWidget/index.js +2 -2
- package/dist/realtimeWidget/index.js.map +1 -1
- package/dist/realtimeWidget/visitorIdentity.d.ts +21 -1
- package/dist/realtimeWidget/visitorIdentity.d.ts.map +1 -1
- package/dist/realtimeWidget/visitorIdentity.js +16 -4
- package/dist/realtimeWidget/visitorIdentity.js.map +1 -1
- package/dist/realtimeWidget/widgetCore.d.ts +48 -0
- package/dist/realtimeWidget/widgetCore.d.ts.map +1 -1
- package/dist/realtimeWidget/widgetCore.js +38 -10
- package/dist/realtimeWidget/widgetCore.js.map +1 -1
- package/dist/realtimeWidget/widgetGuestElevation.d.ts +10 -6
- package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -1
- package/dist/realtimeWidget/widgetGuestElevation.js +14 -6
- package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.js +3 -3
- package/dist/resolvers/AgentRunStatusPublisher.d.ts +55 -0
- package/dist/resolvers/AgentRunStatusPublisher.d.ts.map +1 -0
- package/dist/resolvers/AgentRunStatusPublisher.js +141 -0
- package/dist/resolvers/AgentRunStatusPublisher.js.map +1 -0
- package/dist/resolvers/ConversationRunEventsResolver.d.ts +90 -0
- package/dist/resolvers/ConversationRunEventsResolver.d.ts.map +1 -0
- package/dist/resolvers/ConversationRunEventsResolver.js +283 -0
- package/dist/resolvers/ConversationRunEventsResolver.js.map +1 -0
- package/dist/resolvers/DuplicateEntryCheckResolver.d.ts +98 -0
- package/dist/resolvers/DuplicateEntryCheckResolver.d.ts.map +1 -0
- package/dist/resolvers/DuplicateEntryCheckResolver.js +227 -0
- package/dist/resolvers/DuplicateEntryCheckResolver.js.map +1 -0
- package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.js +2 -1
- package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
- package/dist/resolvers/FileResolver.js +3 -3
- package/dist/resolvers/GeoResolver.d.ts +2 -2
- package/dist/resolvers/GeoResolver.js +4 -4
- package/dist/resolvers/GetDataResolver.d.ts +14 -0
- package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataResolver.js +43 -15
- package/dist/resolvers/GetDataResolver.js.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +2 -2
- package/dist/resolvers/ListOperationsResolver.d.ts +20 -0
- package/dist/resolvers/ListOperationsResolver.d.ts.map +1 -1
- package/dist/resolvers/ListOperationsResolver.js +68 -28
- package/dist/resolvers/ListOperationsResolver.js.map +1 -1
- package/dist/resolvers/MCPResolver.d.ts +11 -1
- package/dist/resolvers/MCPResolver.d.ts.map +1 -1
- package/dist/resolvers/MCPResolver.js +7 -3
- package/dist/resolvers/MCPResolver.js.map +1 -1
- package/dist/resolvers/MergeRecordsResolver.d.ts +7 -0
- package/dist/resolvers/MergeRecordsResolver.d.ts.map +1 -1
- package/dist/resolvers/MergeRecordsResolver.js +16 -2
- package/dist/resolvers/MergeRecordsResolver.js.map +1 -1
- package/dist/resolvers/RealtimeBridgeResolver.js +3 -3
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +44 -6
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +93 -37
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/RemoteBrowserActionResolver.js +5 -5
- package/dist/resolvers/RunAIAgentResolver.d.ts +8 -18
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +85 -184
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/RunAIPromptResolver.d.ts +1 -1
- package/dist/resolvers/RunAIPromptResolver.js +2 -2
- package/dist/resolvers/RunDecisionResolver.d.ts +149 -0
- package/dist/resolvers/RunDecisionResolver.d.ts.map +1 -0
- package/dist/resolvers/RunDecisionResolver.js +396 -0
- package/dist/resolvers/RunDecisionResolver.js.map +1 -0
- package/dist/resolvers/RunTestResolver.js +5 -5
- package/dist/resolvers/SqlLoggingConfigResolver.d.ts +8 -4
- package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.js +5 -5
- package/dist/resolvers/TaskGraphFrameResolver.d.ts +8 -1
- package/dist/resolvers/TaskGraphFrameResolver.d.ts.map +1 -1
- package/dist/resolvers/TaskGraphFrameResolver.js +6 -2
- package/dist/resolvers/TaskGraphFrameResolver.js.map +1 -1
- package/dist/resolvers/VectorizeEntityResolver.d.ts +1 -1
- package/dist/resolvers/VectorizeEntityResolver.d.ts.map +1 -1
- package/dist/resolvers/VectorizeEntityResolver.js +14 -10
- package/dist/resolvers/VectorizeEntityResolver.js.map +1 -1
- package/dist/resolvers/adhoc-query-helpers.d.ts +6 -0
- package/dist/resolvers/adhoc-query-helpers.d.ts.map +1 -1
- package/dist/resolvers/adhoc-query-helpers.js +10 -2
- package/dist/resolvers/adhoc-query-helpers.js.map +1 -1
- package/dist/resolvers/artifact-routing.d.ts +8 -8
- package/dist/resolvers/artifact-routing.js +2 -2
- package/dist/resolvers/artifact-routing.js.map +1 -1
- package/dist/resolvers/meetingRecordingRegistration.d.ts +10 -2
- package/dist/resolvers/meetingRecordingRegistration.d.ts.map +1 -1
- package/dist/resolvers/meetingRecordingRegistration.js +24 -8
- package/dist/resolvers/meetingRecordingRegistration.js.map +1 -1
- package/dist/resolvers/peaksSidecar.d.ts +4 -0
- package/dist/resolvers/peaksSidecar.d.ts.map +1 -1
- package/dist/resolvers/peaksSidecar.js +10 -2
- package/dist/resolvers/peaksSidecar.js.map +1 -1
- package/dist/rest/EntityCRUDHandler.d.ts +28 -0
- package/dist/rest/EntityCRUDHandler.d.ts.map +1 -1
- package/dist/rest/EntityCRUDHandler.js +20 -4
- package/dist/rest/EntityCRUDHandler.js.map +1 -1
- package/dist/rest/MediaStreamHandler.d.ts +2 -0
- package/dist/rest/MediaStreamHandler.d.ts.map +1 -1
- package/dist/rest/MediaStreamHandler.js +10 -6
- package/dist/rest/MediaStreamHandler.js.map +1 -1
- package/dist/rest/OAuthCallbackHandler.d.ts +9 -0
- package/dist/rest/OAuthCallbackHandler.d.ts.map +1 -1
- package/dist/rest/OAuthCallbackHandler.js +15 -3
- package/dist/rest/OAuthCallbackHandler.js.map +1 -1
- package/dist/rest/RESTEndpointHandler.d.ts +6 -4
- package/dist/rest/RESTEndpointHandler.d.ts.map +1 -1
- package/dist/rest/RESTEndpointHandler.js +15 -11
- package/dist/rest/RESTEndpointHandler.js.map +1 -1
- package/dist/rest/RealtimeSdpBrokerHandler.d.ts +5 -1
- package/dist/rest/RealtimeSdpBrokerHandler.d.ts.map +1 -1
- package/dist/rest/RealtimeSdpBrokerHandler.js +12 -4
- package/dist/rest/RealtimeSdpBrokerHandler.js.map +1 -1
- package/dist/rest/SignatureWebhookHandler.d.ts +2 -0
- package/dist/rest/SignatureWebhookHandler.d.ts.map +1 -1
- package/dist/rest/SignatureWebhookHandler.js +5 -1
- package/dist/rest/SignatureWebhookHandler.js.map +1 -1
- package/dist/rest/UploadTokenManager.d.ts +24 -8
- package/dist/rest/UploadTokenManager.d.ts.map +1 -1
- package/dist/rest/UploadTokenManager.js +53 -21
- package/dist/rest/UploadTokenManager.js.map +1 -1
- package/dist/rest/ViewOperationsHandler.d.ts +20 -0
- package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
- package/dist/rest/ViewOperationsHandler.js +20 -4
- package/dist/rest/ViewOperationsHandler.js.map +1 -1
- package/dist/rest/mediaRange.d.ts +4 -0
- package/dist/rest/mediaRange.d.ts.map +1 -1
- package/dist/rest/mediaRange.js +10 -2
- package/dist/rest/mediaRange.js.map +1 -1
- package/dist/rest/setupRESTEndpoints.d.ts +4 -0
- package/dist/rest/setupRESTEndpoints.d.ts.map +1 -1
- package/dist/rest/setupRESTEndpoints.js +13 -7
- package/dist/rest/setupRESTEndpoints.js.map +1 -1
- package/dist/serverExtensionReservedRoots.d.ts +2 -0
- package/dist/serverExtensionReservedRoots.d.ts.map +1 -1
- package/dist/serverExtensionReservedRoots.js +5 -1
- package/dist/serverExtensionReservedRoots.js.map +1 -1
- package/dist/services/TaskGraphActionRunner.d.ts +13 -8
- package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphActionRunner.js +16 -15
- package/dist/services/TaskGraphActionRunner.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +7 -3
- package/dist/types.js.map +1 -1
- package/dist/util.d.ts +6 -0
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +13 -5
- package/dist/util.js.map +1 -1
- package/package.json +94 -92
- package/src/__tests__/AgentRunStatusPublisher.test.ts +175 -0
- package/src/__tests__/DeleteOptionsInput.wireShape.test.ts +62 -0
- package/src/__tests__/DuplicateEntryCheckResolver.test.ts +300 -0
- package/src/__tests__/FileResolverPeaks.test.ts +13 -13
- package/src/__tests__/ListOperationsResolver.test.ts +17 -17
- package/src/__tests__/MediaStreamHandler.test.ts +28 -28
- package/src/__tests__/NoLog.test.ts +11 -11
- package/src/__tests__/PrimaryKeyAlwaysMapped.test.ts +10 -10
- package/src/__tests__/RESTEndpointHandler.test.ts +14 -14
- package/src/__tests__/RealtimeBridgeResolver.test.ts +4 -2
- package/src/__tests__/RealtimeClientSessionResolver.test.ts +216 -2
- package/src/__tests__/RealtimeSdpBrokerHandler.test.ts +6 -6
- package/src/__tests__/RefreshAdoptsNewColumns.test.ts +8 -8
- package/src/__tests__/RemoteBrowserActionRecovery.test.ts +7 -1
- package/src/__tests__/RemoteBrowserAudioStream.test.ts +7 -1
- package/src/__tests__/RemoteBrowserGoalEngine.test.ts +18 -18
- package/src/__tests__/RemoteBrowserSnapshot.test.ts +7 -1
- package/src/__tests__/ResolverBase.clientOldValues.test.ts +52 -0
- package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +398 -0
- package/src/__tests__/RunAIAgentResolver.streaming.test.ts +18 -27
- package/src/__tests__/RunDecisionResolver.test.ts +699 -0
- package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +131 -0
- package/src/__tests__/TaskGraphFrameResolver.test.ts +2 -2
- package/src/__tests__/VectorizeEntityResolver.test.ts +106 -0
- package/src/__tests__/WidgetSessionService.test.ts +8 -4
- package/src/__tests__/adhoc-query-helpers.test.ts +21 -21
- package/src/__tests__/bootAudit.test.ts +11 -11
- package/src/__tests__/boundaryLogPayload.test.ts +6 -6
- package/src/__tests__/cacheInvalidationRecordData.test.ts +177 -0
- package/src/__tests__/conversationRunEvents.test.ts +399 -0
- package/src/__tests__/databaseAbstraction.test.ts +12 -6
- package/src/__tests__/getDataTokenRedaction.test.ts +7 -7
- package/src/__tests__/initializeProviders.refresh.test.ts +4 -4
- package/src/__tests__/magicLink.test.ts +73 -134
- package/src/__tests__/magicLinkService.consume.test.ts +224 -0
- package/src/__tests__/meetingRecordingRegistration.test.ts +12 -12
- package/src/__tests__/metadataRefreshSignal.test.ts +78 -0
- package/src/__tests__/multiTenancy.security.test.ts +40 -40
- package/src/__tests__/multiTenancy.test.ts +18 -18
- package/src/__tests__/newUsers.test.ts +24 -23
- package/src/__tests__/orphanedConversationDetailReconciler.test.ts +283 -0
- package/src/__tests__/principals.test.ts +22 -22
- package/src/__tests__/pushStatusFanOut.test.ts +206 -0
- package/src/__tests__/pushStatusResolver.filter.test.ts +8 -8
- package/src/__tests__/secretRedactor.failOpen.test.ts +12 -12
- package/src/__tests__/secretRedactor.test.ts +12 -12
- package/src/__tests__/serverExtensionReservedRoots.test.ts +7 -7
- package/src/__tests__/subscriptionRedaction.test.ts +5 -5
- package/src/__tests__/unifiedAuth.test.ts +15 -9
- package/src/__tests__/visitorIdentity.resolve.test.ts +6 -6
- package/src/__tests__/widget.test.ts +45 -45
- package/src/__tests__/widgetGuestElevation.test.ts +7 -7
- package/src/__tests__/widgetHostIdentity.test.ts +14 -14
- package/src/agentSessions/ReturningVisitorRecap.ts +12 -1
- package/src/agentSessions/SessionManager.ts +2 -2
- package/src/agentSessions/remoteBrowserGoalEngine.ts +30 -5
- package/src/auth/AuthProviderCatalogRouter.ts +6 -1
- package/src/auth/__tests__/backward-compatibility.test.ts +4 -3
- package/src/auth/index.ts +43 -28
- package/src/auth/initializeProviders.ts +25 -5
- package/src/auth/magicLink/MagicLinkRouter.ts +28 -10
- package/src/auth/magicLink/MagicLinkService.ts +71 -52
- package/src/auth/magicLink/index.ts +7 -7
- package/src/auth/magicLink/magicLinkCore.ts +96 -64
- package/src/auth/magicLink/redeemLanding.ts +14 -4
- package/src/auth/magicLink/types.ts +54 -54
- package/src/auth/newUsers.ts +1 -1
- package/src/auth/principals.ts +11 -2
- package/src/cache.ts +4 -1
- package/src/config.ts +37 -4
- package/src/context.ts +37 -24
- package/src/directives/Public.ts +4 -1
- package/src/directives/RequireSystemUser.ts +4 -1
- package/src/generated/graphql-schemas/__mj.ts +3674 -4
- package/src/generic/CacheInvalidationResolver.ts +148 -4
- package/src/generic/DeleteOptionsInput.ts +9 -2
- package/src/generic/FireAndForgetHeartbeat.ts +35 -16
- package/src/generic/OrphanedConversationDetailReconciler.ts +267 -0
- package/src/generic/PushStatusResolver.ts +118 -8
- package/src/generic/ResolverBase.ts +65 -57
- package/src/generic/RunViewResolver.ts +22 -22
- package/src/generic/__tests__/FireAndForgetHeartbeat.test.ts +7 -7
- package/src/index.ts +137 -11
- package/src/integration/CustomColumnPromoter.ts +6 -1
- package/src/integration/EntityMapLifecycle.ts +20 -2
- package/src/logging/NoLog.ts +16 -2
- package/src/logging/StartupLogger.ts +6 -1
- package/src/logging/bootAudit.ts +26 -16
- package/src/logging/boundaryLogPayload.ts +8 -1
- package/src/logging/secretRedactor.ts +14 -9
- package/src/logging/shortenForLog.ts +8 -3
- package/src/logging/variablesLoggingMiddleware.ts +9 -6
- package/src/metadataRefreshSignal.ts +45 -0
- package/src/middleware/MJTenantFilterMiddleware.ts +4 -4
- package/src/multiTenancy/index.ts +29 -5
- package/src/realtimeWidget/WidgetRouter.ts +15 -10
- package/src/realtimeWidget/WidgetSessionService.ts +76 -76
- package/src/realtimeWidget/host-identity.ts +24 -10
- package/src/realtimeWidget/index.ts +3 -3
- package/src/realtimeWidget/visitorIdentity.ts +39 -8
- package/src/realtimeWidget/widgetCore.ts +75 -10
- package/src/realtimeWidget/widgetGuestElevation.ts +25 -12
- package/src/resolvers/AdhocQueryResolver.ts +3 -3
- package/src/resolvers/AgentRunStatusPublisher.ts +175 -0
- package/src/resolvers/AgentSessionResolver.ts +3 -3
- package/src/resolvers/ColorResolver.ts +6 -6
- package/src/resolvers/ComponentRegistryResolver.ts +36 -36
- package/src/resolvers/ConversationRunEventsResolver.ts +308 -0
- package/src/resolvers/DuplicateEntryCheckResolver.ts +232 -0
- package/src/resolvers/EntityCommunicationsResolver.ts +4 -4
- package/src/resolvers/ExecuteRemoteOperationResolver.ts +9 -9
- package/src/resolvers/FetchEntityVectorsResolver.ts +2 -1
- package/src/resolvers/FileResolver.ts +42 -42
- package/src/resolvers/GeoResolver.ts +4 -4
- package/src/resolvers/GetDataResolver.ts +50 -15
- package/src/resolvers/IntegrationDiscoveryResolver.ts +2 -2
- package/src/resolvers/ListOperationsResolver.ts +78 -28
- package/src/resolvers/MCPResolver.ts +30 -15
- package/src/resolvers/MergeRecordsResolver.ts +14 -2
- package/src/resolvers/QueryResolver.ts +13 -13
- package/src/resolvers/RealtimeBridgeResolver.ts +3 -3
- package/src/resolvers/RealtimeClientSessionResolver.ts +96 -38
- package/src/resolvers/RemoteBrowserActionResolver.ts +5 -5
- package/src/resolvers/RunAIAgentResolver.ts +134 -239
- package/src/resolvers/RunAIPromptResolver.ts +22 -22
- package/src/resolvers/RunDecisionResolver.ts +435 -0
- package/src/resolvers/RunTemplateResolver.ts +4 -4
- package/src/resolvers/RunTestResolver.ts +25 -25
- package/src/resolvers/SqlLoggingConfigResolver.ts +56 -56
- package/src/resolvers/TaskGraphFrameResolver.ts +37 -28
- package/src/resolvers/TelemetryResolver.ts +79 -79
- package/src/resolvers/VectorizeEntityResolver.ts +16 -11
- package/src/resolvers/__tests__/GetDataResolver.tokens.test.ts +40 -38
- package/src/resolvers/__tests__/MergeRecordsResolver.test.ts +93 -0
- package/src/resolvers/__tests__/artifact-routing.test.ts +23 -23
- package/src/resolvers/adhoc-query-helpers.ts +19 -2
- package/src/resolvers/artifact-routing.ts +19 -19
- package/src/resolvers/meetingRecordingRegistration.ts +44 -10
- package/src/resolvers/peaksSidecar.ts +12 -2
- package/src/rest/EntityCRUDHandler.ts +24 -4
- package/src/rest/MediaStreamHandler.ts +11 -6
- package/src/rest/OAuthCallbackHandler.ts +25 -7
- package/src/rest/RESTEndpointHandler.ts +20 -15
- package/src/rest/RealtimeSdpBrokerHandler.ts +14 -4
- package/src/rest/SignatureWebhookHandler.ts +6 -1
- package/src/rest/UploadTokenManager.ts +66 -30
- package/src/rest/ViewOperationsHandler.ts +24 -4
- package/src/rest/mediaRange.ts +16 -6
- package/src/rest/setupRESTEndpoints.ts +24 -12
- package/src/serverExtensionReservedRoots.ts +6 -1
- package/src/services/TaskGraphActionRunner.ts +16 -15
- package/src/types.ts +21 -16
- package/src/util.ts +16 -6
|
@@ -2,30 +2,30 @@ import { describe, it, expect } from 'vitest';
|
|
|
2
2
|
import jwt from 'jsonwebtoken';
|
|
3
3
|
import { createPublicKey, type JsonWebKey } from 'node:crypto';
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
GenerateRawToken,
|
|
6
|
+
HashToken,
|
|
7
|
+
EvaluateInvite,
|
|
8
|
+
BuildSessionClaims,
|
|
9
|
+
BuildConsumeInvitePostgresSQL,
|
|
10
|
+
CanIssueInvites,
|
|
11
|
+
IsRoleGrantable,
|
|
12
|
+
UnionScopes,
|
|
13
13
|
MAGIC_LINK_TOKEN_PREFIX,
|
|
14
14
|
} from '../auth/magicLink/magicLinkCore.js';
|
|
15
|
-
import {
|
|
15
|
+
import { BuildRedeemLandingHtml, EscapeHtml } from '../auth/magicLink/redeemLanding.js';
|
|
16
16
|
import { MagicLinkKeyManager } from '../auth/magicLink/MagicLinkKeys.js';
|
|
17
17
|
|
|
18
18
|
describe('magic-link core', () => {
|
|
19
19
|
describe('generateRawToken', () => {
|
|
20
20
|
it('prefixes tokens and uses 32 bytes of hex entropy', () => {
|
|
21
|
-
const t =
|
|
21
|
+
const t = GenerateRawToken();
|
|
22
22
|
expect(t.startsWith(MAGIC_LINK_TOKEN_PREFIX)).toBe(true);
|
|
23
23
|
const body = t.slice(MAGIC_LINK_TOKEN_PREFIX.length);
|
|
24
24
|
expect(body).toMatch(/^[0-9a-f]{64}$/);
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
it('produces unique tokens', () => {
|
|
28
|
-
const set = new Set(Array.from({ length: 100 }, () =>
|
|
28
|
+
const set = new Set(Array.from({ length: 100 }, () => GenerateRawToken()));
|
|
29
29
|
expect(set.size).toBe(100);
|
|
30
30
|
});
|
|
31
31
|
});
|
|
@@ -33,15 +33,15 @@ describe('magic-link core', () => {
|
|
|
33
33
|
describe('hashToken', () => {
|
|
34
34
|
it('is deterministic and base64url-encoded sha256 (43 chars, URL-safe alphabet)', () => {
|
|
35
35
|
const raw = 'mj_ml_abc';
|
|
36
|
-
expect(
|
|
36
|
+
expect(HashToken(raw)).toBe(HashToken(raw));
|
|
37
37
|
// base64url of 32 bytes = 43 chars, no padding, only [A-Za-z0-9_-]
|
|
38
|
-
expect(
|
|
38
|
+
expect(HashToken(raw)).toMatch(/^[A-Za-z0-9_-]{43}$/);
|
|
39
39
|
});
|
|
40
40
|
|
|
41
41
|
it('differs for different inputs and never equals the raw token', () => {
|
|
42
|
-
const raw =
|
|
43
|
-
expect(
|
|
44
|
-
expect(
|
|
42
|
+
const raw = GenerateRawToken();
|
|
43
|
+
expect(HashToken(raw)).not.toBe(HashToken(GenerateRawToken()));
|
|
44
|
+
expect(HashToken(raw)).not.toBe(raw);
|
|
45
45
|
});
|
|
46
46
|
});
|
|
47
47
|
|
|
@@ -51,37 +51,37 @@ describe('magic-link core', () => {
|
|
|
51
51
|
const past = new Date(now - 3600_000);
|
|
52
52
|
|
|
53
53
|
it('accepts an active, unexpired, unused invite', () => {
|
|
54
|
-
expect(
|
|
54
|
+
expect(EvaluateInvite({ Status: 'Active', ExpiresAt: future, MaxUses: 1, UseCount: 0 }, now)).toEqual({ ok: true });
|
|
55
55
|
});
|
|
56
56
|
|
|
57
57
|
it('rejects revoked invites first', () => {
|
|
58
|
-
expect(
|
|
58
|
+
expect(EvaluateInvite({ Status: 'Revoked', ExpiresAt: future, MaxUses: 1, UseCount: 0 }, now)).toEqual({ ok: false, errorCode: 'revoked' });
|
|
59
59
|
});
|
|
60
60
|
|
|
61
61
|
it('rejects expired invites', () => {
|
|
62
|
-
expect(
|
|
62
|
+
expect(EvaluateInvite({ Status: 'Active', ExpiresAt: past, MaxUses: 1, UseCount: 0 }, now)).toEqual({ ok: false, errorCode: 'expired' });
|
|
63
63
|
});
|
|
64
64
|
|
|
65
65
|
it('rejects consumed invites (by status)', () => {
|
|
66
|
-
expect(
|
|
66
|
+
expect(EvaluateInvite({ Status: 'Consumed', ExpiresAt: future, MaxUses: 1, UseCount: 1 }, now)).toEqual({ ok: false, errorCode: 'consumed' });
|
|
67
67
|
});
|
|
68
68
|
|
|
69
69
|
it('rejects when use count reaches max', () => {
|
|
70
|
-
expect(
|
|
70
|
+
expect(EvaluateInvite({ Status: 'Active', ExpiresAt: future, MaxUses: 3, UseCount: 3 }, now)).toEqual({ ok: false, errorCode: 'consumed' });
|
|
71
71
|
});
|
|
72
72
|
|
|
73
73
|
it('allows multi-use invites that still have uses left', () => {
|
|
74
|
-
expect(
|
|
74
|
+
expect(EvaluateInvite({ Status: 'Active', ExpiresAt: future, MaxUses: 3, UseCount: 2 }, now)).toEqual({ ok: true });
|
|
75
75
|
});
|
|
76
76
|
|
|
77
77
|
it('rejects unknown statuses as invalid', () => {
|
|
78
|
-
expect(
|
|
78
|
+
expect(EvaluateInvite({ Status: 'Pending', ExpiresAt: future, MaxUses: 1, UseCount: 0 }, now)).toEqual({ ok: false, errorCode: 'invalid' });
|
|
79
79
|
});
|
|
80
80
|
});
|
|
81
81
|
|
|
82
82
|
describe('buildSessionClaims', () => {
|
|
83
83
|
it('scopes the token to exactly the given app and role and marks it magic-link', () => {
|
|
84
|
-
const claims =
|
|
84
|
+
const claims = BuildSessionClaims({
|
|
85
85
|
issuer: 'http://localhost:4051',
|
|
86
86
|
audience: 'mj-magic-link',
|
|
87
87
|
inviteId: 'INVITE-1',
|
|
@@ -103,7 +103,7 @@ describe('magic-link core', () => {
|
|
|
103
103
|
});
|
|
104
104
|
|
|
105
105
|
it('carries mj_invited_by when an inviter is supplied (attribution claim)', () => {
|
|
106
|
-
const claims =
|
|
106
|
+
const claims = BuildSessionClaims({
|
|
107
107
|
issuer: 'i', audience: 'a', inviteId: 'INVITE-1', email: 'e@x.com',
|
|
108
108
|
applicationId: 'APP-1', roleName: 'Magic Link Baseline',
|
|
109
109
|
invitedByUserId: 'USER-42', nowSeconds: 1000, ttlSeconds: 3600,
|
|
@@ -112,7 +112,7 @@ describe('magic-link core', () => {
|
|
|
112
112
|
});
|
|
113
113
|
|
|
114
114
|
it('omits mj_invited_by when no inviter is supplied', () => {
|
|
115
|
-
const claims =
|
|
115
|
+
const claims = BuildSessionClaims({
|
|
116
116
|
issuer: 'i', audience: 'a', inviteId: 'INVITE-1', email: 'e@x.com',
|
|
117
117
|
applicationId: 'APP-1', roleName: 'Magic Link Baseline',
|
|
118
118
|
nowSeconds: 1000, ttlSeconds: 3600,
|
|
@@ -121,7 +121,7 @@ describe('magic-link core', () => {
|
|
|
121
121
|
});
|
|
122
122
|
|
|
123
123
|
it('always emits a single-entry mj_scopes for the current link', () => {
|
|
124
|
-
const claims =
|
|
124
|
+
const claims = BuildSessionClaims({
|
|
125
125
|
issuer: 'i', audience: 'a', inviteId: 'INVITE-1', email: 'e@x.com',
|
|
126
126
|
applicationId: 'APP-1', roleName: 'Magic Link Baseline', nowSeconds: 1000, ttlSeconds: 3600,
|
|
127
127
|
});
|
|
@@ -129,7 +129,7 @@ describe('magic-link core', () => {
|
|
|
129
129
|
});
|
|
130
130
|
|
|
131
131
|
it('marks anonymous sessions and carries the per-session id + prior-scope union', () => {
|
|
132
|
-
const claims =
|
|
132
|
+
const claims = BuildSessionClaims({
|
|
133
133
|
issuer: 'i', audience: 'a', inviteId: 'INVITE-2', email: 'anon@x',
|
|
134
134
|
applicationId: 'APP-2', roleName: 'Guest', anonymous: true, sessionId: 'SID-9',
|
|
135
135
|
priorScopes: [{ inviteId: 'INVITE-1', appId: 'APP-1', role: 'Guest' }],
|
|
@@ -142,7 +142,7 @@ describe('magic-link core', () => {
|
|
|
142
142
|
});
|
|
143
143
|
|
|
144
144
|
it('does NOT mark mj_anon for email sessions', () => {
|
|
145
|
-
const claims =
|
|
145
|
+
const claims = BuildSessionClaims({
|
|
146
146
|
issuer: 'i', audience: 'a', inviteId: 'INVITE-1', email: 'e@x.com',
|
|
147
147
|
applicationId: 'APP-1', roleName: 'Magic Link Baseline', nowSeconds: 1000, ttlSeconds: 3600,
|
|
148
148
|
});
|
|
@@ -155,84 +155,26 @@ describe('magic-link core', () => {
|
|
|
155
155
|
const b = { inviteId: 'I2', appId: 'A2', role: 'R' };
|
|
156
156
|
|
|
157
157
|
it('appends a new scope entry', () => {
|
|
158
|
-
expect(
|
|
158
|
+
expect(UnionScopes([a], b)).toEqual([a, b]);
|
|
159
159
|
});
|
|
160
160
|
|
|
161
161
|
it('is idempotent by inviteId — re-redeeming the same link never accretes a duplicate', () => {
|
|
162
|
-
expect(
|
|
162
|
+
expect(UnionScopes([a, b], { inviteId: 'I1', appId: 'A1', role: 'R' })).toEqual([a, b]);
|
|
163
163
|
});
|
|
164
164
|
|
|
165
165
|
it('handles an empty/undefined prior union', () => {
|
|
166
|
-
expect(
|
|
167
|
-
expect(
|
|
166
|
+
expect(UnionScopes(undefined, a)).toEqual([a]);
|
|
167
|
+
expect(UnionScopes([], a)).toEqual([a]);
|
|
168
168
|
});
|
|
169
169
|
});
|
|
170
170
|
|
|
171
|
-
describe('
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
expect(sql).toContain(`UPDATE ${table} `);
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it('increments UseCount', () => {
|
|
180
|
-
expect(sql).toContain('UseCount = UseCount + 1');
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it('stamps ConsumedAt only the first time (COALESCE preserves an existing value)', () => {
|
|
184
|
-
expect(sql).toContain('ConsumedAt = COALESCE(ConsumedAt, SYSUTCDATETIME())');
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it('flips Status to Consumed exactly when the last use is taken', () => {
|
|
188
|
-
expect(sql).toContain("Status = CASE WHEN UseCount + 1 >= MaxUses THEN 'Consumed' ELSE Status END");
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it('returns the affected row via OUTPUT INTO a table var so the caller can detect a win (exactly one row)', () => {
|
|
192
|
-
// Must be OUTPUT ... INTO, not a bare OUTPUT: SQL Server forbids a bare
|
|
193
|
-
// OUTPUT clause on a table with enabled triggers, and CodeGen adds an
|
|
194
|
-
// __mj_UpdatedAt trigger to every MJ table. Regression guard for that bug.
|
|
195
|
-
expect(sql).toContain('OUTPUT INSERTED.ID INTO @consumed');
|
|
196
|
-
expect(sql).not.toMatch(/OUTPUT INSERTED\.ID(?!\s+INTO)/);
|
|
197
|
-
expect(sql).toContain('SELECT ID FROM @consumed');
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it('guards atomically on Active + not-exhausted + not-expired — this IS the single-use gate', () => {
|
|
201
|
-
const where = sql.slice(sql.indexOf('WHERE'));
|
|
202
|
-
expect(where).toContain("Status = 'Active'");
|
|
203
|
-
expect(where).toContain('UseCount < MaxUses');
|
|
204
|
-
expect(where).toContain('ExpiresAt > SYSUTCDATETIME()');
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
it('binds the invite ID as a parameter, never interpolated (injection-safe)', () => {
|
|
208
|
-
expect(sql).toContain('ID = @p0');
|
|
209
|
-
// The id must not be string-interpolated with quotes around a value.
|
|
210
|
-
expect(sql).not.toMatch(/ID = '/);
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it('is a self-contained batch: declare table var, update-with-output, select', () => {
|
|
214
|
-
// Intentionally a 3-statement batch (the OUTPUT-INTO requirement). The ID is
|
|
215
|
-
// still parameterized, so the batch carries no interpolated user input.
|
|
216
|
-
expect(sql).toContain('DECLARE @consumed TABLE (ID UNIQUEIDENTIFIER)');
|
|
217
|
-
expect(sql.match(/;/g)?.length).toBe(3);
|
|
218
|
-
expect(sql.trim().endsWith(';')).toBe(true);
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
it('rejects a non-whitelisted table identifier (defense-in-depth against injection)', () => {
|
|
222
|
-
// Only bracket-quoted [schema].[table] of word chars is accepted, even though
|
|
223
|
-
// the caller derives the table from EntityInfo and never from user input.
|
|
224
|
-
expect(() => buildConsumeInviteSQL('__mj.MagicLinkInvite')).toThrow();
|
|
225
|
-
expect(() => buildConsumeInviteSQL('[__mj].[MagicLinkInvite]; DROP TABLE x;--')).toThrow();
|
|
226
|
-
expect(() => buildConsumeInviteSQL('[__mj].[Magic Link]')).toThrow();
|
|
227
|
-
expect(() => buildConsumeInviteSQL(table)).not.toThrow();
|
|
228
|
-
});
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
describe('buildConsumeInviteSQL (postgresql dialect)', () => {
|
|
232
|
-
// PG uses `schema.table` (unquoted — PostgreSQLDataProvider.ExecuteSQL auto-quotes
|
|
233
|
-
// the PascalCase identifiers). SS uses the bracket-quoted `[schema].[table]` form.
|
|
171
|
+
describe('BuildConsumeInvitePostgresSQL', () => {
|
|
172
|
+
// PostgreSQL only: SQL Server consumes through spConsumeMagicLinkInvite (#4753), but PG has no
|
|
173
|
+
// such function and its runtime roles hold table DML, so PG keeps this direct guarded UPDATE.
|
|
174
|
+
// PG uses `schema.table` unquoted — PostgreSQLDataProvider.ExecuteSQL auto-quotes the
|
|
175
|
+
// PascalCase identifiers.
|
|
234
176
|
const pgTable = '__mj.MagicLinkInvite';
|
|
235
|
-
const pgSql =
|
|
177
|
+
const pgSql = BuildConsumeInvitePostgresSQL(pgTable);
|
|
236
178
|
|
|
237
179
|
it('targets the supplied unquoted schema.table', () => {
|
|
238
180
|
expect(pgSql).toContain(`UPDATE ${pgTable} `);
|
|
@@ -246,63 +188,60 @@ describe('magic-link core', () => {
|
|
|
246
188
|
|
|
247
189
|
it('binds the invite ID as PG positional param $1, not T-SQL @p0 (injection-safe)', () => {
|
|
248
190
|
expect(pgSql).toContain('ID = $1');
|
|
249
|
-
expect(pgSql).not.
|
|
250
|
-
expect(pgSql).not.toMatch(/@/); // no T-SQL @-anything survives on PG
|
|
191
|
+
expect(pgSql).not.toMatch(/@/);
|
|
251
192
|
expect(pgSql).not.toMatch(/ID = '/);
|
|
252
193
|
});
|
|
253
194
|
|
|
254
195
|
it('uses PG timestamp expressions, not T-SQL SYSUTCDATETIME()', () => {
|
|
255
196
|
expect(pgSql).not.toContain('SYSUTCDATETIME');
|
|
256
|
-
expect(pgSql).toContain("(now() AT TIME ZONE 'utc')");
|
|
257
197
|
expect(pgSql).toContain("ConsumedAt = COALESCE(ConsumedAt, (now() AT TIME ZONE 'utc'))");
|
|
258
198
|
});
|
|
259
199
|
|
|
260
|
-
it('guards atomically on Active + not-exhausted + not-expired (same predicate as
|
|
200
|
+
it('guards atomically on Active + not-exhausted + not-expired (same predicate as spConsumeMagicLinkInvite)', () => {
|
|
261
201
|
const where = pgSql.slice(pgSql.indexOf('WHERE'));
|
|
262
202
|
expect(where).toContain("Status = 'Active'");
|
|
263
203
|
expect(where).toContain('UseCount < MaxUses');
|
|
264
204
|
expect(where).toContain("ExpiresAt > (now() AT TIME ZONE 'utc')");
|
|
265
205
|
});
|
|
266
206
|
|
|
267
|
-
it('increments UseCount and flips Status on the last use (same semantics as
|
|
207
|
+
it('increments UseCount and flips Status on the last use (same semantics as spConsumeMagicLinkInvite)', () => {
|
|
268
208
|
expect(pgSql).toContain('UseCount = UseCount + 1');
|
|
269
209
|
expect(pgSql).toContain("Status = CASE WHEN UseCount + 1 >= MaxUses THEN 'Consumed' ELSE Status END");
|
|
270
210
|
});
|
|
271
211
|
|
|
272
|
-
it('rejects a non-whitelisted
|
|
273
|
-
|
|
274
|
-
expect(() =>
|
|
275
|
-
expect(() =>
|
|
276
|
-
expect(() =>
|
|
277
|
-
expect(() => buildConsumeInviteSQL(pgTable, 'postgresql')).not.toThrow();
|
|
212
|
+
it('rejects a non-whitelisted table identifier (defense-in-depth against injection)', () => {
|
|
213
|
+
expect(() => BuildConsumeInvitePostgresSQL('[__mj].[MagicLinkInvite]')).toThrow();
|
|
214
|
+
expect(() => BuildConsumeInvitePostgresSQL('__mj.MagicLinkInvite; DROP TABLE x;--')).toThrow();
|
|
215
|
+
expect(() => BuildConsumeInvitePostgresSQL('__mj.Magic Link')).toThrow();
|
|
216
|
+
expect(() => BuildConsumeInvitePostgresSQL(pgTable)).not.toThrow();
|
|
278
217
|
});
|
|
279
218
|
});
|
|
280
219
|
|
|
281
220
|
describe('canIssueInvites', () => {
|
|
282
221
|
it('always allows Owners, regardless of issuer-role config (case/space-insensitive)', () => {
|
|
283
|
-
expect(
|
|
284
|
-
expect(
|
|
222
|
+
expect(CanIssueInvites('Owner', [], [])).toBe(true);
|
|
223
|
+
expect(CanIssueInvites(' owner ', [], [])).toBe(true);
|
|
285
224
|
});
|
|
286
225
|
|
|
287
226
|
it('Owner-only by default: a non-Owner with no configured issuer roles is denied', () => {
|
|
288
|
-
expect(
|
|
227
|
+
expect(CanIssueInvites('User', ['Developer', 'Magic Link Baseline'], [])).toBe(false);
|
|
289
228
|
});
|
|
290
229
|
|
|
291
230
|
it('denies an external user holding the restricted role (the escalation we are blocking)', () => {
|
|
292
231
|
// restricted role is never an issuer role, so this stays false even if someone
|
|
293
232
|
// mistakenly leaves issuerRoleNames empty
|
|
294
|
-
expect(
|
|
233
|
+
expect(CanIssueInvites('User', ['Magic Link Baseline'], [])).toBe(false);
|
|
295
234
|
});
|
|
296
235
|
|
|
297
236
|
it('allows a non-Owner only when one of their roles is a configured issuer role', () => {
|
|
298
|
-
expect(
|
|
299
|
-
expect(
|
|
300
|
-
expect(
|
|
237
|
+
expect(CanIssueInvites('User', ['Sales Admin'], ['Sales Admin'])).toBe(true);
|
|
238
|
+
expect(CanIssueInvites('User', ['sales admin'], ['Sales Admin'])).toBe(true); // case-insensitive
|
|
239
|
+
expect(CanIssueInvites('User', ['Marketing'], ['Sales Admin'])).toBe(false);
|
|
301
240
|
});
|
|
302
241
|
|
|
303
242
|
it('handles null/undefined type and empty role lists safely', () => {
|
|
304
|
-
expect(
|
|
305
|
-
expect(
|
|
243
|
+
expect(CanIssueInvites(null, [], ['X'])).toBe(false);
|
|
244
|
+
expect(CanIssueInvites(undefined, ['X'], ['X'])).toBe(true);
|
|
306
245
|
});
|
|
307
246
|
});
|
|
308
247
|
|
|
@@ -310,41 +249,41 @@ describe('magic-link core', () => {
|
|
|
310
249
|
const restricted = 'Magic Link Baseline';
|
|
311
250
|
|
|
312
251
|
it('always allows the restricted role (case/space-insensitive)', () => {
|
|
313
|
-
expect(
|
|
314
|
-
expect(
|
|
252
|
+
expect(IsRoleGrantable('Magic Link Baseline', restricted, [])).toBe(true);
|
|
253
|
+
expect(IsRoleGrantable(' magic link baseline ', restricted, [])).toBe(true);
|
|
315
254
|
});
|
|
316
255
|
|
|
317
256
|
it('rejects a privileged role by default — blocks roleId=Owner escalation', () => {
|
|
318
|
-
expect(
|
|
319
|
-
expect(
|
|
257
|
+
expect(IsRoleGrantable('Owner', restricted, [])).toBe(false);
|
|
258
|
+
expect(IsRoleGrantable('Administrator', restricted, [])).toBe(false);
|
|
320
259
|
});
|
|
321
260
|
|
|
322
261
|
it('allows additional roles only when explicitly opted in', () => {
|
|
323
|
-
expect(
|
|
324
|
-
expect(
|
|
325
|
-
expect(
|
|
262
|
+
expect(IsRoleGrantable('Read Only Guest', restricted, ['Read Only Guest'])).toBe(true);
|
|
263
|
+
expect(IsRoleGrantable('read only guest', restricted, ['Read Only Guest'])).toBe(true);
|
|
264
|
+
expect(IsRoleGrantable('Owner', restricted, ['Read Only Guest'])).toBe(false);
|
|
326
265
|
});
|
|
327
266
|
|
|
328
267
|
it('rejects empty/null role names', () => {
|
|
329
|
-
expect(
|
|
330
|
-
expect(
|
|
268
|
+
expect(IsRoleGrantable('', restricted, [])).toBe(false);
|
|
269
|
+
expect(IsRoleGrantable(null, restricted, [])).toBe(false);
|
|
331
270
|
});
|
|
332
271
|
});
|
|
333
272
|
|
|
334
273
|
describe('escapeHtml', () => {
|
|
335
274
|
it('escapes the five significant HTML characters', () => {
|
|
336
|
-
expect(
|
|
275
|
+
expect(EscapeHtml(`<>&"'`)).toBe('<>&"'');
|
|
337
276
|
});
|
|
338
277
|
|
|
339
278
|
it('leaves a normal magic-link token untouched', () => {
|
|
340
|
-
const t =
|
|
341
|
-
expect(
|
|
279
|
+
const t = GenerateRawToken();
|
|
280
|
+
expect(EscapeHtml(t)).toBe(t);
|
|
342
281
|
});
|
|
343
282
|
});
|
|
344
283
|
|
|
345
284
|
describe('buildRedeemLandingHtml', () => {
|
|
346
|
-
const token =
|
|
347
|
-
const html =
|
|
285
|
+
const token = GenerateRawToken();
|
|
286
|
+
const html = BuildRedeemLandingHtml(token, '/magic-link/redeem');
|
|
348
287
|
|
|
349
288
|
it('renders a POST form to the redeem path (GET stays side-effect-free)', () => {
|
|
350
289
|
expect(html).toContain('method="POST"');
|
|
@@ -365,7 +304,7 @@ describe('magic-link core', () => {
|
|
|
365
304
|
});
|
|
366
305
|
|
|
367
306
|
it('escapes a token containing HTML metacharacters into the form value', () => {
|
|
368
|
-
const evil =
|
|
307
|
+
const evil = BuildRedeemLandingHtml('a"><script>x</script>', '/magic-link/redeem');
|
|
369
308
|
expect(evil).not.toContain('<script>x</script>');
|
|
370
309
|
expect(evil).toContain('<script>');
|
|
371
310
|
});
|
|
@@ -377,7 +316,7 @@ describe('MagicLinkKeyManager', () => {
|
|
|
377
316
|
const km = MagicLinkKeyManager.Instance;
|
|
378
317
|
km.Initialize(); // ephemeral keypair
|
|
379
318
|
|
|
380
|
-
const claims =
|
|
319
|
+
const claims = BuildSessionClaims({
|
|
381
320
|
issuer: 'http://localhost:4051',
|
|
382
321
|
audience: 'mj-magic-link',
|
|
383
322
|
inviteId: 'INVITE-1',
|
|
@@ -414,7 +353,7 @@ describe('MagicLinkKeyManager', () => {
|
|
|
414
353
|
it('rejects a tampered token', () => {
|
|
415
354
|
const km = MagicLinkKeyManager.Instance;
|
|
416
355
|
km.Initialize();
|
|
417
|
-
const claims =
|
|
356
|
+
const claims = BuildSessionClaims({
|
|
418
357
|
issuer: 'http://localhost:4051',
|
|
419
358
|
audience: 'mj-magic-link',
|
|
420
359
|
inviteId: 'INVITE-2',
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach, type Mock } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// MagicLinkService is the imperative shell — importing it pulls the full
|
|
4
|
+
// data-provider and communication stacks in at module load. Stub those out;
|
|
5
|
+
// @memberjunction/core stays real (Metadata, RunView, LogError).
|
|
6
|
+
vi.mock('@memberjunction/generic-database-provider', () => ({
|
|
7
|
+
UserCache: { Instance: { Users: [] } },
|
|
8
|
+
}));
|
|
9
|
+
vi.mock('@memberjunction/communication-engine', () => ({
|
|
10
|
+
CommunicationEngine: { Instance: {} },
|
|
11
|
+
}));
|
|
12
|
+
vi.mock('@memberjunction/communication-types', () => ({
|
|
13
|
+
Message: class {},
|
|
14
|
+
}));
|
|
15
|
+
vi.mock('../config.js', () => ({
|
|
16
|
+
configInfo: {},
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
import { Metadata, RunView, type DatabaseProviderBase, type IMetadataProvider, type UserInfo } from '@memberjunction/core';
|
|
20
|
+
import type { MJMagicLinkInviteEntity } from '@memberjunction/core-entities';
|
|
21
|
+
import type { MagicLinkConfig } from '../config.js';
|
|
22
|
+
import { MagicLinkService } from '../auth/magicLink/MagicLinkService.js';
|
|
23
|
+
import { MAGIC_LINK_TOKEN_PREFIX, BuildConsumeInvitePostgresSQL } from '../auth/magicLink/magicLinkCore.js';
|
|
24
|
+
|
|
25
|
+
type Platform = 'sqlserver' | 'postgresql';
|
|
26
|
+
|
|
27
|
+
/** Provider surface consumeInvite/RedeemInvite touch, with real-shaped dialect behavior. */
|
|
28
|
+
interface FakeProvider {
|
|
29
|
+
PlatformKey: Platform;
|
|
30
|
+
MJCoreSchemaName: string;
|
|
31
|
+
Roles: { ID: string; Name: string }[];
|
|
32
|
+
Applications: { ID: string; Name: string; Path: string }[];
|
|
33
|
+
Dialect: { ProcedureCallSyntax: (schema: string, name: string, params: string[]) => string };
|
|
34
|
+
BuildParameterPlaceholder: (index: number) => string;
|
|
35
|
+
ExecuteSQL: Mock;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function makeProvider(platform: Platform): FakeProvider {
|
|
39
|
+
const isPg = platform === 'postgresql';
|
|
40
|
+
return {
|
|
41
|
+
PlatformKey: platform,
|
|
42
|
+
MJCoreSchemaName: '__mj',
|
|
43
|
+
Roles: [{ ID: 'role-1', Name: 'Magic Link Baseline' }],
|
|
44
|
+
Applications: [],
|
|
45
|
+
Dialect: {
|
|
46
|
+
ProcedureCallSyntax: isPg
|
|
47
|
+
? (s, n, p) => `SELECT * FROM ${s}."${n}"(${p.join(', ')})`
|
|
48
|
+
: (s, n, p) => `EXEC [${s}].[${n}] ${p.join(', ')}`,
|
|
49
|
+
},
|
|
50
|
+
BuildParameterPlaceholder: isPg ? (i) => `$${i + 1}` : (i) => `@p${i}`,
|
|
51
|
+
ExecuteSQL: vi.fn(),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const INVITE_ID = '11111111-2222-3333-4444-555555555555';
|
|
56
|
+
|
|
57
|
+
function makeInvite(): MJMagicLinkInviteEntity {
|
|
58
|
+
return {
|
|
59
|
+
ID: INVITE_ID,
|
|
60
|
+
Status: 'Active',
|
|
61
|
+
UseCount: 0,
|
|
62
|
+
MaxUses: 1,
|
|
63
|
+
ExpiresAt: new Date(Date.now() + 3600_000),
|
|
64
|
+
RoleID: 'role-1',
|
|
65
|
+
CreatedByUserID: 'inviter-1',
|
|
66
|
+
ApplicationID: 'app-1',
|
|
67
|
+
Email: 'g@example.com',
|
|
68
|
+
} as unknown as MJMagicLinkInviteEntity;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Private surface of MagicLinkService the tests drive or stub. */
|
|
72
|
+
interface ServicePrivates {
|
|
73
|
+
consumeInvite: (invite: MJMagicLinkInviteEntity, provider: DatabaseProviderBase, contextUser: UserInfo) => Promise<string>;
|
|
74
|
+
resolveProvisioningContextUser: () => UserInfo | null;
|
|
75
|
+
isInviterActive: (id: string) => boolean;
|
|
76
|
+
recordRedemption: () => Promise<void>;
|
|
77
|
+
provisionUser: () => Promise<{ success: boolean; error?: string }>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
describe('MagicLinkService.consumeInvite (#4753)', () => {
|
|
81
|
+
let service: MagicLinkService;
|
|
82
|
+
let privates: ServicePrivates;
|
|
83
|
+
let invite: MJMagicLinkInviteEntity;
|
|
84
|
+
const contextUser = { ID: 'ctx-user' } as UserInfo;
|
|
85
|
+
|
|
86
|
+
beforeEach(() => {
|
|
87
|
+
service = new MagicLinkService('https://mj.example.com', {} as MagicLinkConfig);
|
|
88
|
+
privates = service as unknown as ServicePrivates;
|
|
89
|
+
invite = makeInvite();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
afterEach(() => {
|
|
93
|
+
Metadata.Provider = null as unknown as IMetadataProvider;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
async function consume(provider: FakeProvider): Promise<string> {
|
|
97
|
+
return privates.consumeInvite(invite, provider as unknown as DatabaseProviderBase, contextUser);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
it('SQL Server: calls spConsumeMagicLinkInvite with the ID bound by name, never interpolated', async () => {
|
|
101
|
+
const provider = makeProvider('sqlserver');
|
|
102
|
+
provider.ExecuteSQL.mockResolvedValue([{ ID: INVITE_ID }]);
|
|
103
|
+
await consume(provider);
|
|
104
|
+
|
|
105
|
+
expect(provider.ExecuteSQL).toHaveBeenCalledTimes(1);
|
|
106
|
+
const [sql, params, options, user] = provider.ExecuteSQL.mock.calls[0];
|
|
107
|
+
expect(sql).toBe('EXEC [__mj].[spConsumeMagicLinkInvite] @ID=@p0');
|
|
108
|
+
expect(params).toEqual([INVITE_ID]);
|
|
109
|
+
expect(options).toEqual(expect.objectContaining({ isMutation: true }));
|
|
110
|
+
expect(user).toBe(contextUser);
|
|
111
|
+
expect(sql).not.toContain(INVITE_ID);
|
|
112
|
+
expect(String(sql).toUpperCase()).not.toContain('UPDATE');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// PG has no spConsumeMagicLinkInvite function (the release-time converter does not emit
|
|
116
|
+
// procedures) and PG's runtime roles hold table DML, so PG keeps the direct guarded UPDATE.
|
|
117
|
+
it('PostgreSQL: runs the guarded UPDATE … RETURNING directly, with the ID bound as $1', async () => {
|
|
118
|
+
const provider = makeProvider('postgresql');
|
|
119
|
+
provider.ExecuteSQL.mockResolvedValue([{ ID: INVITE_ID }]);
|
|
120
|
+
await consume(provider);
|
|
121
|
+
|
|
122
|
+
expect(provider.ExecuteSQL).toHaveBeenCalledTimes(1);
|
|
123
|
+
const [sql, params, options, user] = provider.ExecuteSQL.mock.calls[0];
|
|
124
|
+
expect(sql).toBe(BuildConsumeInvitePostgresSQL('__mj.MagicLinkInvite'));
|
|
125
|
+
expect(sql).not.toContain('spConsumeMagicLinkInvite');
|
|
126
|
+
expect(params).toEqual([INVITE_ID]);
|
|
127
|
+
expect(options).toEqual(expect.objectContaining({ isMutation: true }));
|
|
128
|
+
expect(user).toBe(contextUser);
|
|
129
|
+
expect(sql).not.toContain(INVITE_ID);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("PostgreSQL: maps one row to 'won', zero rows to 'lost', and a thrown error to 'failed'", async () => {
|
|
133
|
+
vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
134
|
+
const provider = makeProvider('postgresql');
|
|
135
|
+
provider.ExecuteSQL.mockResolvedValueOnce([{ ID: INVITE_ID }]).mockResolvedValueOnce([]).mockRejectedValueOnce(new Error('connection reset'));
|
|
136
|
+
|
|
137
|
+
expect(await consume(provider)).toBe('won');
|
|
138
|
+
expect(await consume(provider)).toBe('lost');
|
|
139
|
+
expect(await consume(provider)).toBe('failed');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("returns 'won' when exactly one row comes back", async () => {
|
|
143
|
+
const provider = makeProvider('sqlserver');
|
|
144
|
+
provider.ExecuteSQL.mockResolvedValue([{ ID: INVITE_ID }]);
|
|
145
|
+
expect(await consume(provider)).toBe('won');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("returns 'lost' when zero rows come back", async () => {
|
|
149
|
+
const provider = makeProvider('sqlserver');
|
|
150
|
+
provider.ExecuteSQL.mockResolvedValue([]);
|
|
151
|
+
expect(await consume(provider)).toBe('lost');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("returns 'failed' and logs the invite ID when ExecuteSQL throws", async () => {
|
|
155
|
+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
156
|
+
const provider = makeProvider('sqlserver');
|
|
157
|
+
provider.ExecuteSQL.mockRejectedValue(new Error("The EXECUTE permission was denied on the object 'spConsumeMagicLinkInvite'"));
|
|
158
|
+
|
|
159
|
+
expect(await consume(provider)).toBe('failed');
|
|
160
|
+
const logged = errorSpy.mock.calls.map((call) => call.map(String).join(' '));
|
|
161
|
+
expect(logged.some((line) => line.includes(INVITE_ID) && line.includes('spConsumeMagicLinkInvite'))).toBe(true);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("returns 'failed' when ExecuteSQL returns a non-array", async () => {
|
|
165
|
+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
166
|
+
const provider = makeProvider('sqlserver');
|
|
167
|
+
provider.ExecuteSQL.mockResolvedValue(undefined);
|
|
168
|
+
|
|
169
|
+
expect(await consume(provider)).toBe('failed');
|
|
170
|
+
const logged = errorSpy.mock.calls.map((call) => call.map(String).join(' '));
|
|
171
|
+
expect(logged.some((line) => line.includes(INVITE_ID))).toBe(true);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('MagicLinkService.RedeemInvite consume outcome mapping (#4753)', () => {
|
|
176
|
+
let service: MagicLinkService;
|
|
177
|
+
let provider: FakeProvider;
|
|
178
|
+
let provisionUser: Mock;
|
|
179
|
+
const contextUser = { ID: 'ctx-user' } as UserInfo;
|
|
180
|
+
const rawToken = MAGIC_LINK_TOKEN_PREFIX + 'x';
|
|
181
|
+
|
|
182
|
+
beforeEach(() => {
|
|
183
|
+
vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
184
|
+
provider = makeProvider('sqlserver');
|
|
185
|
+
Metadata.Provider = provider as unknown as IMetadataProvider;
|
|
186
|
+
|
|
187
|
+
service = new MagicLinkService('https://mj.example.com', {} as MagicLinkConfig);
|
|
188
|
+
const privates = service as unknown as ServicePrivates;
|
|
189
|
+
vi.spyOn(RunView.prototype, 'RunView').mockResolvedValue({ Success: true, Results: [makeInvite()] } as Awaited<ReturnType<RunView['RunView']>>);
|
|
190
|
+
vi.spyOn(privates, 'resolveProvisioningContextUser').mockReturnValue(contextUser);
|
|
191
|
+
vi.spyOn(privates, 'isInviterActive').mockReturnValue(true);
|
|
192
|
+
vi.spyOn(privates, 'recordRedemption').mockResolvedValue(undefined);
|
|
193
|
+
provisionUser = vi.fn().mockResolvedValue({ success: false, error: 'stop-here' });
|
|
194
|
+
privates.provisionUser = provisionUser;
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
afterEach(() => {
|
|
198
|
+
Metadata.Provider = null as unknown as IMetadataProvider;
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('RedeemInvite maps a failed consume to server_error, not consumed, and never provisions', async () => {
|
|
202
|
+
provider.ExecuteSQL.mockRejectedValue(new Error("The EXECUTE permission was denied on the object 'spConsumeMagicLinkInvite'"));
|
|
203
|
+
const result = await service.RedeemInvite(rawToken);
|
|
204
|
+
|
|
205
|
+
expect(result).toEqual(expect.objectContaining({ success: false, errorCode: 'server_error' }));
|
|
206
|
+
expect(provisionUser).not.toHaveBeenCalled();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('RedeemInvite still reports consumed when the race is lost', async () => {
|
|
210
|
+
provider.ExecuteSQL.mockResolvedValue([]);
|
|
211
|
+
const result = await service.RedeemInvite(rawToken);
|
|
212
|
+
|
|
213
|
+
expect(result).toEqual(expect.objectContaining({ success: false, errorCode: 'consumed' }));
|
|
214
|
+
expect(provisionUser).not.toHaveBeenCalled();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('RedeemInvite proceeds to provisioning when the consume wins', async () => {
|
|
218
|
+
provider.ExecuteSQL.mockResolvedValue([{ ID: INVITE_ID }]);
|
|
219
|
+
const result = await service.RedeemInvite(rawToken);
|
|
220
|
+
|
|
221
|
+
expect(provisionUser).toHaveBeenCalledTimes(1);
|
|
222
|
+
expect(result).toEqual(expect.objectContaining({ success: false, errorCode: 'provisioning_failed' }));
|
|
223
|
+
});
|
|
224
|
+
});
|