feat(core): Split private credentials behind a dedicated release flag (no-changelog) (#32332)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Guillaume Jacquart
2026-06-16 10:38:14 +02:00
committed by GitHub
parent 1b6a3bd814
commit e978fa8135
16 changed files with 173 additions and 62 deletions
@@ -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(
@@ -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));
@@ -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<string, IUsedCredential> = {};
@@ -83,7 +83,7 @@ describe('useNodeHelpers()', () => {
beforeEach(() => {
mockedUseDynamicCredentials.mockReturnValue({
isEnabled: computed(() => true),
} as ReturnType<typeof useDynamicCredentials>);
} as ReturnType<typeof usePrivateCredentials>);
});
afterEach(() => {
@@ -980,7 +980,7 @@ describe('useNodeHelpers()', () => {
it('emits no issue when dynamic credentials feature is disabled', () => {
mockedUseDynamicCredentials.mockReturnValue({
isEnabled: computed(() => false),
} as ReturnType<typeof useDynamicCredentials>);
} as ReturnType<typeof usePrivateCredentials>);
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<typeof useDynamicCredentials>);
} as ReturnType<typeof usePrivateCredentials>);
mockConnectedPrivateCred(true);
mockDocumentStore.workflowTriggerNodes = [buildTriggerNode(WEBHOOK_TRIGGER)];
@@ -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();
@@ -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,
);
@@ -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() {
<N8nBadge v-if="needsSetup" class="ml-3xs" theme="warning">
{{ locale.baseText('credentials.item.needsSetup') }}
</N8nBadge>
<N8nTooltip v-if="isDynamicCredentialsEnabled && data.isResolvable" placement="top">
<N8nTooltip v-if="isPrivateCredentialsEnabled && data.isResolvable" placement="top">
<template #content>
<div :class="$style.tooltipContent">
<strong>{{ locale.baseText('credentials.private.tooltipTitle') }}</strong>
@@ -177,7 +177,7 @@ describe('CredentialConfig', () => {
});
describe('Dynamic Credentials Section', () => {
it('should not display dynamic credentials section when isDynamicCredentialsEnabled is false', async () => {
it('should not display dynamic credentials section when isPrivateCredentialsEnabled is false', async () => {
renderComponent({
props: {
isManaged: false,
@@ -185,7 +185,7 @@ describe('CredentialConfig', () => {
credentialType: mockCredentialType,
credentialProperties: [],
credentialData: {} as ICredentialDataDecryptedObject,
isDynamicCredentialsEnabled: false,
isPrivateCredentialsEnabled: false,
isOAuthType: true,
isNewCredential: true,
credentialPermissions: {
@@ -211,7 +211,7 @@ describe('CredentialConfig', () => {
credentialType: mockCredentialType,
credentialProperties: [],
credentialData: {} as ICredentialDataDecryptedObject,
isDynamicCredentialsEnabled: true,
isPrivateCredentialsEnabled: true,
isOAuthType: false,
isNewCredential: true,
credentialPermissions: {
@@ -237,7 +237,7 @@ describe('CredentialConfig', () => {
credentialType: mockCredentialType,
credentialProperties: [],
credentialData: {} as ICredentialDataDecryptedObject,
isDynamicCredentialsEnabled: true,
isPrivateCredentialsEnabled: true,
isOAuthType: true,
isNewCredential: true,
credentialPermissions: {
@@ -263,7 +263,7 @@ describe('CredentialConfig', () => {
credentialType: mockCredentialType,
credentialProperties: [],
credentialData: {} as ICredentialDataDecryptedObject,
isDynamicCredentialsEnabled: true,
isPrivateCredentialsEnabled: true,
isOAuthType: true,
isNewCredential: false,
credentialPermissions: {
@@ -289,7 +289,7 @@ describe('CredentialConfig', () => {
credentialType: mockCredentialType,
credentialProperties: [],
credentialData: {} as ICredentialDataDecryptedObject,
isDynamicCredentialsEnabled: true,
isPrivateCredentialsEnabled: true,
isOAuthType: true,
isNewCredential: true,
isResolvable: false,
@@ -317,7 +317,7 @@ describe('CredentialConfig', () => {
credentialType: mockCredentialType,
credentialProperties: [],
credentialData: {} as ICredentialDataDecryptedObject,
isDynamicCredentialsEnabled: true,
isPrivateCredentialsEnabled: true,
isOAuthType: true,
isNewCredential: false,
isResolvable: false,
@@ -345,7 +345,7 @@ describe('CredentialConfig', () => {
credentialType: mockCredentialType,
credentialProperties: [],
credentialData: {} as ICredentialDataDecryptedObject,
isDynamicCredentialsEnabled: true,
isPrivateCredentialsEnabled: true,
isOAuthType: true,
isNewCredential: false,
isResolvable: false,
@@ -393,7 +393,7 @@ describe('CredentialConfig', () => {
renderComponent({
props: {
...oAuthConnectedProps,
isDynamicCredentialsEnabled: true,
isPrivateCredentialsEnabled: true,
isResolvable: true,
connectedByMe: true,
},
@@ -406,7 +406,7 @@ describe('CredentialConfig', () => {
renderComponent({
props: {
...oAuthConnectedProps,
isDynamicCredentialsEnabled: true,
isPrivateCredentialsEnabled: true,
isResolvable: true,
connectedByMe: false,
},
@@ -419,7 +419,7 @@ describe('CredentialConfig', () => {
renderComponent({
props: {
...oAuthConnectedProps,
isDynamicCredentialsEnabled: true,
isPrivateCredentialsEnabled: true,
isResolvable: false,
connectedByMe: true,
},
@@ -432,7 +432,7 @@ describe('CredentialConfig', () => {
renderComponent({
props: {
...oAuthConnectedProps,
isDynamicCredentialsEnabled: false,
isPrivateCredentialsEnabled: false,
isResolvable: true,
connectedByMe: true,
},
@@ -445,7 +445,7 @@ describe('CredentialConfig', () => {
const { emitted } = renderComponent({
props: {
...oAuthConnectedProps,
isDynamicCredentialsEnabled: true,
isPrivateCredentialsEnabled: true,
isResolvable: true,
connectedByMe: true,
},
@@ -66,7 +66,7 @@ type Props = {
isRetesting?: boolean;
requiredPropertiesFilled?: boolean;
isManaged?: boolean;
isDynamicCredentialsEnabled?: boolean;
isPrivateCredentialsEnabled?: boolean;
isResolvable?: boolean;
isShared?: boolean;
connectedByMe?: boolean;
@@ -209,7 +209,7 @@ const showOAuthNotConnectedBanner = computed(() => {
});
const showDisconnectButton = computed(
() => !!props.isDynamicCredentialsEnabled && !!props.isResolvable && !!props.connectedByMe,
() => !!props.isPrivateCredentialsEnabled && !!props.isResolvable && !!props.connectedByMe,
);
const isMissingCredentials = computed(() => props.credentialType === null);
@@ -452,7 +452,7 @@ watch(showOAuthSuccessBanner, (newValue, oldValue) => {
<div
v-if="
isDynamicCredentialsEnabled &&
isPrivateCredentialsEnabled &&
// Only OAuth credentials can be dynamic for now, as they are the only ones with the managed authorize endpoint
isOAuthType &&
canWrite
@@ -48,9 +48,9 @@ vi.mock('@/app/composables/useMessage', () => ({
useMessage: () => ({ confirm: confirmMock }),
}));
vi.mock('@/features/resolvers/composables/useDynamicCredentials', async () => {
vi.mock('@/features/resolvers/composables/usePrivateCredentials', async () => {
const { ref } = await vi.importActual<typeof import('vue')>('vue');
return { useDynamicCredentials: () => ({ isEnabled: ref(true) }) };
return { usePrivateCredentials: () => ({ isEnabled: ref(true) }) };
});
const oAuth2Api: ICredentialType = {
@@ -67,7 +67,7 @@ import {
} from '@n8n/design-system';
import { setParameterValue } from '@/app/utils/parameterUtils';
import get from 'lodash/get';
import { useDynamicCredentials } from '@/features/resolvers/composables/useDynamicCredentials';
import { usePrivateCredentials } from '@/features/resolvers/composables/usePrivateCredentials';
import { useQuickConnect } from '../../quickConnect/composables/useQuickConnect';
import type { CredentialModeOption } from './CredentialModeSelector.vue';
@@ -124,7 +124,7 @@ async function confirmModal(
const telemetry = useTelemetry();
const router = useRouter();
const rootStore = useRootStore();
const { isEnabled: isDynamicCredentialsEnabled } = useDynamicCredentials();
const { isEnabled: isPrivateCredentialsEnabled } = usePrivateCredentials();
const { getQuickConnectOption, connect: quickConnect } = useQuickConnect();
const isQuickConnectMode = ref(false);
const activeTab = ref('connection');
@@ -1680,7 +1680,7 @@ const { width } = useElementSize(credNameRef);
:credential-permissions="credentialPermissions"
:mode="mode"
:selected-credential="selectedCredential"
:is-dynamic-credentials-enabled="isDynamicCredentialsEnabled"
:is-private-credentials-enabled="isPrivateCredentialsEnabled"
:is-resolvable="isResolvable"
:is-shared="isCurrentlyShared"
:connected-by-me="connectedByMe"
@@ -37,7 +37,7 @@ import { assert } from '@n8n/utils/assert';
import { isEmpty } from '@/app/utils/typesUtils';
import { getResourcePermissions } from '@n8n/permissions';
import { useNodeCredentialOptions } from '../composables/useNodeCredentialOptions';
import { useDynamicCredentials } from '@/features/resolvers/composables/useDynamicCredentials';
import { usePrivateCredentials } from '@/features/resolvers/composables/usePrivateCredentials';
import { SYSTEM_RESOLVER_ID } from '@n8n/api-types';
import { useAiGateway } from '@/app/composables/useAiGateway';
import AiGatewaySelector from '@/app/components/AiGatewaySelector.vue';
@@ -100,7 +100,7 @@ const uiStore = useUIStore();
const projectsStore = useProjectsStore();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = props.standalone ? undefined : injectWorkflowDocumentStore();
const { isEnabled: isDynamicCredentialsEnabled } = useDynamicCredentials();
const { isEnabled: isPrivateCredentialsEnabled } = usePrivateCredentials();
// Quick connect
const {
@@ -163,7 +163,7 @@ const selected = computed<Record<string, INodeCredentialsDetails>>(
);
function isCredentialResolvable(credentialType: string): boolean {
if (!isDynamicCredentialsEnabled.value) return false;
if (!isPrivateCredentialsEnabled.value) return false;
const credentialId = selected.value[credentialType]?.id;
if (!credentialId) return false;
const credential = credentialsStore.getCredentialById(credentialId);
@@ -171,7 +171,7 @@ function isCredentialResolvable(credentialType: string): boolean {
}
function getSelectedPrivateCredential(credentialType: string): ICredentialsResponse | null {
if (!isDynamicCredentialsEnabled.value) return null;
if (!isPrivateCredentialsEnabled.value) return null;
const id = selected.value[credentialType]?.id;
if (!id) return null;
const credential = credentialsStore.getCredentialById(id);
@@ -841,7 +841,7 @@ async function onQuickConnectSignIn(credentialTypeName: string) {
<div :class="$style.credentialOptionName">
<N8nText bold>{{ item.name }}</N8nText>
<N8nTooltip
v-if="isDynamicCredentialsEnabled && item.isResolvable"
v-if="isPrivateCredentialsEnabled && item.isResolvable"
placement="top"
>
<template #content>{{
@@ -2,6 +2,14 @@ import { computed } from 'vue';
import { useSettingsStore } from '@/app/stores/settings.store';
import { useEnvFeatureFlag } from '@/features/shared/envFeatureFlag/useEnvFeatureFlag';
/**
* Gates the external/custom credential resolver surfaces (resolver management
* page, workflow-level resolver dropdown, custom resolver create/edit). This is
* the superset capability.
*
* For the base "private credentials" surfaces (per-user self-connect via the
* system resolver) use `usePrivateCredentials`.
*/
export const useDynamicCredentials = () => {
const settingsStore = useSettingsStore();
const { check } = useEnvFeatureFlag();
@@ -0,0 +1,56 @@
import { createTestingPinia } from '@pinia/testing';
import { setActivePinia } from 'pinia';
import { usePrivateCredentials } from './usePrivateCredentials';
import { useSettingsStore } from '@/app/stores/settings.store';
import type { FrontendSettings } from '@n8n/api-types';
describe('usePrivateCredentials', () => {
let settingsStore: ReturnType<typeof useSettingsStore>;
const setup = ({
moduleActive,
privateFlag,
dynamicFlag,
}: {
moduleActive: boolean;
privateFlag: boolean;
dynamicFlag: boolean;
}) => {
const pinia = createTestingPinia();
setActivePinia(pinia);
settingsStore = useSettingsStore();
settingsStore.settings = {
envFeatureFlags: {
N8N_ENV_FEAT_PRIVATE_CREDENTIALS: privateFlag,
N8N_ENV_FEAT_DYNAMIC_CREDENTIALS: dynamicFlag,
},
activeModules: moduleActive ? ['dynamic-credentials'] : [],
} as unknown as FrontendSettings;
vi.spyOn(settingsStore, 'isModuleActive').mockImplementation(
(name: string) => moduleActive && name === 'dynamic-credentials',
);
return usePrivateCredentials();
};
it('should be enabled when module is active and the private flag is on', () => {
const { isEnabled } = setup({ moduleActive: true, privateFlag: true, dynamicFlag: false });
expect(isEnabled.value).toBe(true);
});
it('should be enabled when module is active and only the superset dynamic flag is on', () => {
const { isEnabled } = setup({ moduleActive: true, privateFlag: false, dynamicFlag: true });
expect(isEnabled.value).toBe(true);
});
it('should be disabled when module is not active', () => {
const { isEnabled } = setup({ moduleActive: false, privateFlag: true, dynamicFlag: true });
expect(isEnabled.value).toBe(false);
});
it('should be disabled when neither flag is on', () => {
const { isEnabled } = setup({ moduleActive: true, privateFlag: false, dynamicFlag: false });
expect(isEnabled.value).toBe(false);
});
});
@@ -0,0 +1,24 @@
import { computed } from 'vue';
import { useSettingsStore } from '@/app/stores/settings.store';
import { useEnvFeatureFlag } from '@/features/shared/envFeatureFlag/useEnvFeatureFlag';
/**
* Gates the "private credentials" surfaces (per-user self-connect via the seeded
* system resolver). This is the base capability and is enabled by either the
* `PRIVATE_CREDENTIALS` flag or the superset `DYNAMIC_CREDENTIALS` flag.
*
* For the external/custom resolver surfaces (resolver management, workflow
* resolver dropdown, custom resolver create/edit) use `useDynamicCredentials`.
*/
export const usePrivateCredentials = () => {
const settingsStore = useSettingsStore();
const { check } = useEnvFeatureFlag();
const isEnabled = computed(
() =>
settingsStore.isModuleActive('dynamic-credentials') &&
(check.value('PRIVATE_CREDENTIALS') || check.value('DYNAMIC_CREDENTIALS')),
);
return { isEnabled };
};
@@ -17,8 +17,8 @@ import { computed } from 'vue';
const WORKFLOW_ID = 'test-workflow-id';
vi.mock('@/features/resolvers/composables/useDynamicCredentials', () => ({
useDynamicCredentials: vi.fn(),
vi.mock('@/features/resolvers/composables/usePrivateCredentials', () => ({
usePrivateCredentials: vi.fn(),
}));
vi.mock('@/features/workflows/canvas/canvas.utils', async (importOriginal) => ({
@@ -33,9 +33,9 @@ vi.mock('@/features/workflows/canvas/canvas.utils', async (importOriginal) => ({
})),
}));
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 renderComponent = createComponentRenderer(CanvasNodeSettingsIcons, {
pinia: createTestingPinia(),
@@ -44,7 +44,7 @@ const renderComponent = createComponentRenderer(CanvasNodeSettingsIcons, {
const mockFeatureFlag = (enabled: boolean) => {
mockedUseDynamicCredentials.mockReturnValue({
isEnabled: computed(() => enabled),
} as ReturnType<typeof useDynamicCredentials>);
} as ReturnType<typeof usePrivateCredentials>);
};
describe('CanvasNodeSettingsIcons', () => {
@@ -3,7 +3,7 @@ import { computed } from 'vue';
import { useCanvasNode } from '../../../../../composables/useCanvasNode';
import { useI18n } from '@n8n/i18n';
import { useCredentialsStore } from '@/features/credentials/credentials.store';
import { useDynamicCredentials } from '@/features/resolvers/composables/useDynamicCredentials';
import { usePrivateCredentials } from '@/features/resolvers/composables/usePrivateCredentials';
import { N8nIcon, N8nTooltip } from '@n8n/design-system';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
@@ -11,7 +11,7 @@ const { name } = useCanvasNode();
const i18n = useI18n();
const workflowDocumentStore = injectWorkflowDocumentStore();
const credentialsStore = useCredentialsStore();
const { isEnabled: isDynamicCredentialsEnabled } = useDynamicCredentials();
const { isEnabled: isPrivateCredentialsEnabled } = usePrivateCredentials();
const node = computed(() => workflowDocumentStore.value.getNodeByName(name.value));
const size = 'small';
@@ -117,7 +117,7 @@ const hasDynamicCredentials = computed(
</div>
</N8nTooltip>
<N8nTooltip v-if="isDynamicCredentialsEnabled && hasDynamicCredentials">
<N8nTooltip v-if="isPrivateCredentialsEnabled && hasDynamicCredentials">
<template #content>
<div :class="$style.tooltipHeader">
<N8nIcon icon="key-round" :size="size" />