diff --git a/packages/cli/src/modules/dynamic-credentials.ee/dynamic-credentials.module.ts b/packages/cli/src/modules/dynamic-credentials.ee/dynamic-credentials.module.ts index ee858a6c268..f96a094807c 100644 --- a/packages/cli/src/modules/dynamic-credentials.ee/dynamic-credentials.module.ts +++ b/packages/cli/src/modules/dynamic-credentials.ee/dynamic-credentials.module.ts @@ -3,20 +3,43 @@ import type { ModuleInterface } from '@n8n/decorators'; import { BackendModule, OnShutdown } from '@n8n/decorators'; import { Container } from '@n8n/di'; -function isFeatureFlagEnabled(): boolean { +/** + * Base capability: per-user "private credentials" resolved through the seeded + * system resolver. Enabled by either the private-credentials flag or the + * superset dynamic-credentials flag. + */ +function isPrivateCredentialsEnabled(): boolean { + return ( + process.env.N8N_ENV_FEAT_PRIVATE_CREDENTIALS === 'true' || + process.env.N8N_ENV_FEAT_DYNAMIC_CREDENTIALS === 'true' + ); +} + +/** + * Superset capability: external/custom credential resolvers (OAuth/Slack) plus + * their management surfaces and identity-extractor hooks. + */ +function isExternalResolversEnabled(): boolean { return process.env.N8N_ENV_FEAT_DYNAMIC_CREDENTIALS === 'true'; } @BackendModule({ name: 'dynamic-credentials', licenseFlag: LICENSE_FEATURES.DYNAMIC_CREDENTIALS }) export class DynamicCredentialsModule implements ModuleInterface { async init() { - if (!isFeatureFlagEnabled()) { + if (!isPrivateCredentialsEnabled()) { return; } await import('./dynamic-credentials.controller'); - await import('./credential-resolvers.controller'); - await import('./context-establishment-hooks'); - await import('./credential-resolvers'); + + // System resolver powers private credentials; OAuth/Slack resolvers and + // their management/identity-extractor surfaces are external-only. + await import('./credential-resolvers/n8n-credential-resolver'); + if (isExternalResolversEnabled()) { + await import('./credential-resolvers.controller'); + await import('./context-establishment-hooks'); + await import('./credential-resolvers/oauth-credential-resolver'); + await import('./credential-resolvers/slack-credential-resolver'); + } const { DynamicCredentialResolverRegistry, DynamicCredentialStorageService, @@ -48,7 +71,7 @@ export class DynamicCredentialsModule implements ModuleInterface { } async entities() { - if (!isFeatureFlagEnabled()) { + if (!isPrivateCredentialsEnabled()) { return []; } const { DynamicCredentialResolver } = await import('./database/entities/credential-resolver'); @@ -61,7 +84,7 @@ export class DynamicCredentialsModule implements ModuleInterface { } async context() { - if (!isFeatureFlagEnabled()) { + if (!isPrivateCredentialsEnabled()) { return {}; } const { CredentialCheckProxyService } = await import( diff --git a/packages/frontend/editor-ui/src/app/components/WorkflowCard.vue b/packages/frontend/editor-ui/src/app/components/WorkflowCard.vue index 5bb1d73b428..71433c397ee 100644 --- a/packages/frontend/editor-ui/src/app/components/WorkflowCard.vue +++ b/packages/frontend/editor-ui/src/app/components/WorkflowCard.vue @@ -49,7 +49,7 @@ import { useMCPStore } from '@/features/ai/mcpAccess/mcp.store'; import { useMcp } from '@/features/ai/mcpAccess/composables/useMcp'; import { useWorkflowActivate } from '@/app/composables/useWorkflowActivate'; import { createEventBus } from '@n8n/utils/event-bus'; -import { useDynamicCredentials } from '@/features/resolvers/composables/useDynamicCredentials'; +import { usePrivateCredentials } from '@/features/resolvers/composables/usePrivateCredentials'; import { useDependencies } from '@/app/composables/useDependencies'; const WORKFLOW_LIST_ITEM_ACTIONS = { @@ -117,7 +117,7 @@ const locale = useI18n(); const router = useRouter(); const route = useRoute(); const telemetry = useTelemetry(); -const { isEnabled: isDynamicCredentialsEnabled } = useDynamicCredentials(); +const { isEnabled: isPrivateCredentialsEnabled } = usePrivateCredentials(); const { hasDependencies } = useDependencies(); const uiStore = useUIStore(); @@ -318,7 +318,7 @@ const isWorkflowPublished = computed(() => { }); const hasDynamicCredentials = computed(() => { - return isDynamicCredentialsEnabled.value && props.data.hasResolvableCredentials; + return isPrivateCredentialsEnabled.value && props.data.hasResolvableCredentials; }); const workflowHasDependencies = computed(() => hasDependencies(props.data.id)); diff --git a/packages/frontend/editor-ui/src/app/composables/useNodeHelpers.test.ts b/packages/frontend/editor-ui/src/app/composables/useNodeHelpers.test.ts index 002389be31f..c639caf8fe9 100644 --- a/packages/frontend/editor-ui/src/app/composables/useNodeHelpers.test.ts +++ b/packages/frontend/editor-ui/src/app/composables/useNodeHelpers.test.ts @@ -25,13 +25,13 @@ import type { import { useNodeTypesStore } from '@/app/stores/nodeTypes.store'; import { useCredentialsStore } from '@/features/credentials/credentials.store'; -vi.mock('@/features/resolvers/composables/useDynamicCredentials', () => ({ - useDynamicCredentials: vi.fn(), +vi.mock('@/features/resolvers/composables/usePrivateCredentials', () => ({ + usePrivateCredentials: vi.fn(), })); -import { useDynamicCredentials } from '@/features/resolvers/composables/useDynamicCredentials'; +import { usePrivateCredentials } from '@/features/resolvers/composables/usePrivateCredentials'; -const mockedUseDynamicCredentials = vi.mocked(useDynamicCredentials); +const mockedUseDynamicCredentials = vi.mocked(usePrivateCredentials); const mockDocumentStoreUsedCredentials: Record = {}; @@ -83,7 +83,7 @@ describe('useNodeHelpers()', () => { beforeEach(() => { mockedUseDynamicCredentials.mockReturnValue({ isEnabled: computed(() => true), - } as ReturnType); + } as ReturnType); }); afterEach(() => { @@ -980,7 +980,7 @@ describe('useNodeHelpers()', () => { it('emits no issue when dynamic credentials feature is disabled', () => { mockedUseDynamicCredentials.mockReturnValue({ isEnabled: computed(() => false), - } as ReturnType); + } as ReturnType); const cred = makePrivateCred({ connectedByMe: false }); mockedStore(useCredentialsStore).getCredentialById = vi.fn().mockReturnValue(cred); @@ -1108,7 +1108,7 @@ describe('useNodeHelpers()', () => { it('does not warn when dynamic credentials feature is disabled', () => { mockedUseDynamicCredentials.mockReturnValue({ isEnabled: computed(() => false), - } as ReturnType); + } as ReturnType); mockConnectedPrivateCred(true); mockDocumentStore.workflowTriggerNodes = [buildTriggerNode(WEBHOOK_TRIGGER)]; diff --git a/packages/frontend/editor-ui/src/app/composables/useNodeHelpers.ts b/packages/frontend/editor-ui/src/app/composables/useNodeHelpers.ts index 4e52549b902..9f45e61f67b 100644 --- a/packages/frontend/editor-ui/src/app/composables/useNodeHelpers.ts +++ b/packages/frontend/editor-ui/src/app/composables/useNodeHelpers.ts @@ -52,7 +52,7 @@ import { useCanvasStore } from '@/app/stores/canvas.store'; import { useSettingsStore } from '@/app/stores/settings.store'; import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store'; import { injectWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store'; -import { useDynamicCredentials } from '@/features/resolvers/composables/useDynamicCredentials'; +import { usePrivateCredentials } from '@/features/resolvers/composables/usePrivateCredentials'; declare namespace HttpRequestNode { namespace V2 { @@ -74,7 +74,7 @@ export function useNodeHelpers() { const canvasStore = useCanvasStore(); const workflowDocumentStore = injectWorkflowDocumentStore(); const workflowExecutionStateStore = injectWorkflowExecutionStateStore(); - const { isEnabled: isDynamicCredentialsEnabled } = useDynamicCredentials(); + const { isEnabled: isPrivateCredentialsEnabled } = usePrivateCredentials(); const isInsertingNodes = ref(false); const credentialsUpdated = ref(false); @@ -434,7 +434,7 @@ export function useNodeHelpers() { node: INodeUi, foundIssues: INodeIssueObjectProperty, ): void { - if (!isDynamicCredentialsEnabled.value) return; + if (!isPrivateCredentialsEnabled.value) return; const incompatibleTrigger = workflowHasIncompatibleTrigger(); diff --git a/packages/frontend/editor-ui/src/features/ai/chatHub/ChatView.vue b/packages/frontend/editor-ui/src/features/ai/chatHub/ChatView.vue index 55173b6c6c8..bd0e19728fe 100644 --- a/packages/frontend/editor-ui/src/features/ai/chatHub/ChatView.vue +++ b/packages/frontend/editor-ui/src/features/ai/chatHub/ChatView.vue @@ -59,7 +59,7 @@ import DynamicCredentialsDrawer from './components/DynamicCredentialsDrawer.vue' import { useChatArtifacts } from './composables/useChatArtifacts'; import { useChatInputFocus } from './composables/useChatInputFocus'; import { useDynamicCredentialsStatus } from './composables/useDynamicCredentialsStatus'; -import { useDynamicCredentials } from '@/features/resolvers/composables/useDynamicCredentials'; +import { usePrivateCredentials } from '@/features/resolvers/composables/usePrivateCredentials'; const router = useRouter(); const route = useRoute(); @@ -241,9 +241,9 @@ const { credentialsByProvider, selectCredential } = useChatCredentials( ); // Dynamic credentials -const { isEnabled: dynamicCredentialsEnabled } = useDynamicCredentials(); +const { isEnabled: privateCredentialsEnabled } = usePrivateCredentials(); const dynamicCredsWorkflowId = computed(() => - selectedModel.value?.model.provider === 'n8n' && dynamicCredentialsEnabled.value + selectedModel.value?.model.provider === 'n8n' && privateCredentialsEnabled.value ? selectedModel.value.model.workflowId : null, ); diff --git a/packages/frontend/editor-ui/src/features/credentials/components/CredentialCard.vue b/packages/frontend/editor-ui/src/features/credentials/components/CredentialCard.vue index 3b516addad0..6172a5df777 100644 --- a/packages/frontend/editor-ui/src/features/credentials/components/CredentialCard.vue +++ b/packages/frontend/editor-ui/src/features/credentials/components/CredentialCard.vue @@ -17,7 +17,7 @@ import DependencyPill from '@/app/components/DependencyPill.vue'; import { useI18n } from '@n8n/i18n'; import { ResourceType } from '@/features/collaboration/projects/projects.utils'; import type { CredentialsResource } from '@/Interface'; -import { useDynamicCredentials } from '@/features/resolvers/composables/useDynamicCredentials'; +import { usePrivateCredentials } from '@/features/resolvers/composables/usePrivateCredentials'; import { useCredentialOAuth } from '../composables/useCredentialOAuth'; import { @@ -59,7 +59,7 @@ const toast = useToast(); const uiStore = useUIStore(); const credentialsStore = useCredentialsStore(); const projectsStore = useProjectsStore(); -const { isEnabled: isDynamicCredentialsEnabled } = useDynamicCredentials(); +const { isEnabled: isPrivateCredentialsEnabled } = usePrivateCredentials(); const { hasDependencies } = useDependencies(); const { authorize, isOAuthCredentialType } = useCredentialOAuth(); @@ -73,7 +73,7 @@ const credentialPermissions = computed(() => getResourcePermissions(props.data.s const isPrivateUnconnected = computed( () => - isDynamicCredentialsEnabled.value && + isPrivateCredentialsEnabled.value && props.data.isResolvable === true && props.data.connectedByMe === false && credentialPermissions.value.update === true, @@ -101,7 +101,7 @@ const actions = computed(() => { }); } - if (isDynamicCredentialsEnabled.value && props.data.isResolvable && props.data.connectedByMe) { + if (isPrivateCredentialsEnabled.value && props.data.isResolvable && props.data.connectedByMe) { items.push({ label: locale.baseText('credentials.item.disconnect'), value: CREDENTIAL_LIST_ITEM_ACTIONS.DISCONNECT, @@ -238,7 +238,7 @@ function moveResource() { {{ locale.baseText('credentials.item.needsSetup') }} - +