feat(core): Revamp infisical implementation (#30843)

This commit is contained in:
Adilson Junior 2026-06-04 11:17:55 -03:00 committed by GitHub
parent f1d87fddb2
commit a82384f90d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 730 additions and 226 deletions

View File

@ -179,7 +179,6 @@
"handlebars": "4.7.9",
"helmet": "8.1.0",
"http-proxy-middleware": "^3.0.5",
"infisical-node": "1.3.0",
"ioredis": "5.3.2",
"isbot": "3.6.13",
"isolated-vm": "catalog:",

View File

@ -0,0 +1,436 @@
import { Logger } from '@n8n/backend-common';
import { mockInstance } from '@n8n/backend-test-utils';
import nock from 'nock';
import { InfisicalProvider } from '../infisical';
const SITE_URL = 'https://app.infisical.com';
const PROJECT_ID = 'project-123';
const ENVIRONMENT = 'dev';
const SECRET_PATH = '/';
const universalAuthSettings = {
connected: true,
connectedAt: new Date(),
settings: {
siteURL: SITE_URL,
projectId: PROJECT_ID,
environment: ENVIRONMENT,
secretPath: SECRET_PATH,
authMethod: 'universalAuth',
clientId: 'test-client-id',
clientSecret: 'test-client-secret',
},
};
function workspaceResponse() {
return {
workspace: {
id: PROJECT_ID,
name: 'Test Project',
environments: [{ name: 'Development', slug: 'dev' }],
},
};
}
function universalAuthLoginResponse(accessToken = 'new-access-token', expiresIn = 7200) {
return {
accessToken,
expiresIn,
accessTokenMaxTTL: 43244,
tokenType: 'Bearer',
};
}
type ImportFixture = {
secretPath: string;
environment: string;
secrets: Array<{ secretKey: string; secretValue: string }>;
};
function listSecretsResponse(
secrets: Array<{ secretKey: string; secretValue: string }>,
imports: ImportFixture[] = [],
) {
return { secrets, imports };
}
describe('InfisicalProvider', () => {
const logger = mockInstance(Logger);
logger.scoped.mockReturnValue(logger);
beforeAll(() => {
nock.disableNetConnect();
});
beforeEach(() => {
nock.cleanAll();
});
afterAll(() => {
nock.cleanAll();
nock.enableNetConnect();
});
describe('test', () => {
it('returns success when the workspace endpoint returns 200', async () => {
const provider = new InfisicalProvider(logger);
await provider.init(universalAuthSettings);
const scope = nock(SITE_URL)
.get(`/api/v1/workspace/${PROJECT_ID}`)
.reply(200, workspaceResponse());
const [success] = await provider.test();
expect(success).toBe(true);
scope.done();
});
it('returns "Invalid credentials" on 401', async () => {
const provider = new InfisicalProvider(logger);
await provider.init(universalAuthSettings);
const scope = nock(SITE_URL)
.get(`/api/v1/workspace/${PROJECT_ID}`)
.reply(401, { message: 'Unauthorized' });
const [success, message] = await provider.test();
expect(success).toBe(false);
expect(message).toBe('Invalid credentials');
scope.done();
});
it('returns "Project not found" on 404', async () => {
const provider = new InfisicalProvider(logger);
await provider.init(universalAuthSettings);
const scope = nock(SITE_URL)
.get(`/api/v1/workspace/${PROJECT_ID}`)
.reply(404, { message: 'Not found' });
const [success, message] = await provider.test();
expect(success).toBe(false);
expect(message).toBe('Project not found. Check the Project ID and Site URL.');
scope.done();
});
it('returns "Permission denied" on 403', async () => {
const provider = new InfisicalProvider(logger);
await provider.init(universalAuthSettings);
const scope = nock(SITE_URL)
.get(`/api/v1/workspace/${PROJECT_ID}`)
.reply(403, { message: 'Forbidden' });
const [success, message] = await provider.test();
expect(success).toBe(false);
expect(message).toBe(
'Permission denied. Verify the machine identity has access to this project.',
);
scope.done();
});
});
describe('doConnect with Universal Auth', () => {
it('logs in with clientId/clientSecret and tests the workspace with the issued token', async () => {
const provider = new InfisicalProvider(logger);
await provider.init(universalAuthSettings);
const scope = nock(SITE_URL)
.post('/api/v1/auth/universal-auth/login', {
clientId: 'test-client-id',
clientSecret: 'test-client-secret',
})
.reply(200, universalAuthLoginResponse('issued-token'))
.get(`/api/v1/workspace/${PROJECT_ID}`)
.matchHeader('authorization', 'Bearer issued-token')
.reply(200, workspaceResponse());
await provider.connect();
expect(provider.state).toBe('connected');
scope.done();
});
it('transitions to error state when login fails', async () => {
const provider = new InfisicalProvider(logger);
await provider.init(universalAuthSettings);
const scope = nock(SITE_URL)
.post('/api/v1/auth/universal-auth/login')
.reply(401, { message: 'Invalid client credentials' });
await provider.connect();
expect(provider.state).toBe('error');
scope.done();
});
});
describe('update', () => {
it('caches secrets returned from the v4 list endpoint', async () => {
const provider = new InfisicalProvider(logger);
await provider.init(universalAuthSettings);
const connectScope = nock(SITE_URL)
.post('/api/v1/auth/universal-auth/login')
.reply(200, universalAuthLoginResponse('connect-token'))
.get(`/api/v1/workspace/${PROJECT_ID}`)
.reply(200, workspaceResponse());
await provider.connect();
connectScope.done();
const updateScope = nock(SITE_URL)
.get('/api/v4/secrets')
.query({
projectId: PROJECT_ID,
environment: ENVIRONMENT,
secretPath: SECRET_PATH,
})
.matchHeader('authorization', 'Bearer connect-token')
.reply(
200,
listSecretsResponse([
{ secretKey: 'API_KEY', secretValue: 'secret-value' },
{ secretKey: 'DB_PASSWORD', secretValue: 'hunter2' },
]),
);
await provider.update();
expect(provider.getSecret('API_KEY')).toBe('secret-value');
expect(provider.getSecret('DB_PASSWORD')).toBe('hunter2');
expect(provider.hasSecret('API_KEY')).toBe(true);
expect(provider.hasSecret('UNKNOWN')).toBe(false);
expect(provider.getSecretNames().sort()).toEqual(['API_KEY', 'DB_PASSWORD']);
updateScope.done();
});
it('produces an empty cache when no secrets are returned', async () => {
const provider = new InfisicalProvider(logger);
await provider.init(universalAuthSettings);
const connectScope = nock(SITE_URL)
.post('/api/v1/auth/universal-auth/login')
.reply(200, universalAuthLoginResponse('connect-token'))
.get(`/api/v1/workspace/${PROJECT_ID}`)
.reply(200, workspaceResponse());
await provider.connect();
connectScope.done();
const updateScope = nock(SITE_URL)
.get('/api/v4/secrets')
.query(true)
.reply(200, listSecretsResponse([]));
await provider.update();
expect(provider.getSecretNames()).toHaveLength(0);
updateScope.done();
});
it('re-authenticates and retries once when the token is rejected during update', async () => {
const provider = new InfisicalProvider(logger);
await provider.init(universalAuthSettings);
const connectScope = nock(SITE_URL)
.post('/api/v1/auth/universal-auth/login')
.reply(200, universalAuthLoginResponse('first-token'))
.get(`/api/v1/workspace/${PROJECT_ID}`)
.reply(200, workspaceResponse());
await provider.connect();
connectScope.done();
const updateScope = nock(SITE_URL)
.get('/api/v4/secrets')
.query(true)
.matchHeader('authorization', 'Bearer first-token')
.reply(401, { message: 'Token expired' })
.post('/api/v1/auth/universal-auth/login')
.reply(200, universalAuthLoginResponse('refreshed-token'))
.get('/api/v4/secrets')
.query(true)
.matchHeader('authorization', 'Bearer refreshed-token')
.reply(200, listSecretsResponse([{ secretKey: 'KEY', secretValue: 'val' }]));
await provider.update();
expect(provider.getSecret('KEY')).toBe('val');
updateScope.done();
});
});
describe('update with imports', () => {
const makeImport = (
secrets: Array<{ secretKey: string; secretValue: string }>,
secretPath = '/imported',
environment = ENVIRONMENT,
): ImportFixture => ({ secretPath, environment, secrets });
async function setupConnectedProvider() {
const provider = new InfisicalProvider(logger);
await provider.init(universalAuthSettings);
const connectScope = nock(SITE_URL)
.post('/api/v1/auth/universal-auth/login')
.reply(200, universalAuthLoginResponse('connect-token'))
.get(`/api/v1/workspace/${PROJECT_ID}`)
.reply(200, workspaceResponse());
await provider.connect();
connectScope.done();
return provider;
}
it('caches secrets from imports alongside top-level secrets', async () => {
const provider = await setupConnectedProvider();
const updateScope = nock(SITE_URL)
.get('/api/v4/secrets')
.query(true)
.reply(
200,
listSecretsResponse(
[{ secretKey: 'API_KEY', secretValue: 'top-value' }],
[
makeImport([
{ secretKey: 'IMPORTED_KEY', secretValue: 'from-import' },
{ secretKey: 'ANOTHER', secretValue: 'also' },
]),
],
),
);
await provider.update();
expect(provider.getSecret('API_KEY')).toBe('top-value');
expect(provider.getSecret('IMPORTED_KEY')).toBe('from-import');
expect(provider.getSecret('ANOTHER')).toBe('also');
expect(provider.getSecretNames().sort()).toEqual(['ANOTHER', 'API_KEY', 'IMPORTED_KEY']);
updateScope.done();
});
it('prefers top-level secrets over imports when keys collide', async () => {
const provider = await setupConnectedProvider();
const updateScope = nock(SITE_URL)
.get('/api/v4/secrets')
.query(true)
.reply(
200,
listSecretsResponse(
[{ secretKey: 'SHARED_KEY', secretValue: 'wins' }],
[makeImport([{ secretKey: 'SHARED_KEY', secretValue: 'loses' }])],
),
);
await provider.update();
expect(provider.getSecret('SHARED_KEY')).toBe('wins');
expect(provider.getSecretNames()).toEqual(['SHARED_KEY']);
updateScope.done();
});
it('keeps the value from the first import when later imports define the same key', async () => {
const provider = await setupConnectedProvider();
const updateScope = nock(SITE_URL)
.get('/api/v4/secrets')
.query(true)
.reply(
200,
listSecretsResponse(
[],
[
makeImport([{ secretKey: 'DUP', secretValue: 'first' }], '/imported-a'),
makeImport([{ secretKey: 'DUP', secretValue: 'second' }], '/imported-b'),
],
),
);
await provider.update();
expect(provider.getSecret('DUP')).toBe('first');
updateScope.done();
});
it('caches keys sourced only from imports when top-level secrets is empty', async () => {
const provider = await setupConnectedProvider();
const updateScope = nock(SITE_URL)
.get('/api/v4/secrets')
.query(true)
.reply(
200,
listSecretsResponse(
[],
[
makeImport([{ secretKey: 'FROM_A', secretValue: 'a' }], '/imported-a'),
makeImport([{ secretKey: 'FROM_B', secretValue: 'b' }], '/imported-b'),
],
),
);
await provider.update();
expect(provider.getSecret('FROM_A')).toBe('a');
expect(provider.getSecret('FROM_B')).toBe('b');
expect(provider.getSecretNames().sort()).toEqual(['FROM_A', 'FROM_B']);
updateScope.done();
});
it('handles imports with empty secrets arrays without affecting the cache', async () => {
const provider = await setupConnectedProvider();
const updateScope = nock(SITE_URL)
.get('/api/v4/secrets')
.query(true)
.reply(
200,
listSecretsResponse(
[],
[
makeImport([], '/empty'),
makeImport([{ secretKey: 'REAL', secretValue: 'value' }], '/has-secrets'),
],
),
);
await provider.update();
expect(provider.getSecret('REAL')).toBe('value');
expect(provider.getSecretNames()).toEqual(['REAL']);
updateScope.done();
});
});
describe('disconnect', () => {
it('clears cached secrets and the in-flight token', async () => {
const provider = new InfisicalProvider(logger);
await provider.init(universalAuthSettings);
const scope = nock(SITE_URL)
.post('/api/v1/auth/universal-auth/login')
.reply(200, universalAuthLoginResponse('connect-token'))
.get(`/api/v1/workspace/${PROJECT_ID}`)
.reply(200, workspaceResponse())
.get('/api/v4/secrets')
.query(true)
.reply(200, listSecretsResponse([{ secretKey: 'KEY', secretValue: 'val' }]));
await provider.connect();
await provider.update();
expect(provider.getSecretNames()).toContain('KEY');
await provider.disconnect();
expect(provider.getSecretNames()).toHaveLength(0);
expect(provider.hasSecret('KEY')).toBe(false);
scope.done();
});
});
});

View File

@ -1,47 +1,56 @@
import type InfisicalClient from 'infisical-node';
import { UnexpectedError, type IDataObject, type INodeProperties } from 'n8n-workflow';
import { Logger } from '@n8n/backend-common';
import { Container } from '@n8n/di';
import type { AxiosInstance } from 'axios';
import axios from 'axios';
import { type INodeProperties, UnexpectedError } from 'n8n-workflow';
import { SecretsProvider } from '../types';
import { DOCS_HELP_NOTICE } from '../constants';
import type { SecretsProviderSettings } from '../types';
import { SecretsProvider } from '../types';
export interface InfisicalSettings {
token: string;
type InfisicalAuthMethod = 'universalAuth';
interface InfisicalSettings {
siteURL: string;
cacheTTL: number;
debug: boolean;
projectId: string;
environment: string;
secretPath: string;
authMethod: InfisicalAuthMethod;
// Universal Auth
clientId: string;
clientSecret: string;
}
interface InfisicalUniversalAuthLoginResponse {
accessToken: string;
expiresIn: number;
accessTokenMaxTTL: number;
tokenType: string;
}
interface InfisicalSecret {
secretName: string;
secretValue?: string;
secretKey: string;
secretValue: string;
}
interface InfisicalServiceToken {
environment?: string;
scopes?: Array<{ environment: string; path: string }>;
interface InfisicalImport {
secrets: InfisicalSecret[];
secretPath: string;
environment: string;
}
interface InfisicalListSecretsResponse {
secrets: InfisicalSecret[];
imports: InfisicalImport[];
}
const TOKEN_REFRESH_LEEWAY_SECONDS = 60;
const MIN_REFRESH_DELAY_MS = 60 * 1000;
export class InfisicalProvider extends SecretsProvider {
properties: INodeProperties[] = [
{
displayName:
'<h2>Important information about our infisical integration</h2><br>From the <b>30th July, 2024</b>, we will no longer be supporting new connections to inifiscal secrets vault using service tokens. Existing service tokens will remain usable until <b>July, 2025</b>. After that period, we will be removing support for Infisical from our external secrets integrations. You can find out more information about this change on <a href="https://docs.n8n.io/external-secrets/#connect-n8n-to-your-secrets-store" target="_blank">our docs</a>',
name: 'notice',
type: 'notice',
default: '',
noDataExpression: true,
},
{
displayName: 'Service Token',
name: 'token',
type: 'string',
hint: 'The Infisical Service Token with read access',
default: '',
required: true,
placeholder: 'e.g. st.64ae963e1874ea.374226a166439dce.39557e4a1b7bdd82',
noDataExpression: true,
typeOptions: { password: true },
},
DOCS_HELP_NOTICE,
{
displayName: 'Site URL',
name: 'siteURL',
@ -52,6 +61,76 @@ export class InfisicalProvider extends SecretsProvider {
placeholder: 'https://app.infisical.com',
default: 'https://app.infisical.com',
},
{
displayName: 'Project ID',
name: 'projectId',
type: 'string',
hint: 'The Infisical project to read secrets from',
required: true,
noDataExpression: true,
placeholder: 'e.g. 7c1cbe9c-3f1b-4a92-b3a2-9d5e2f1c8a4b',
default: '',
},
{
displayName: 'Environment',
name: 'environment',
type: 'string',
hint: 'Environment slug (e.g. dev, staging, prod)',
required: true,
noDataExpression: true,
placeholder: 'dev',
default: 'dev',
},
{
displayName: 'Secret Path',
name: 'secretPath',
type: 'string',
hint: 'The path within the project to read secrets from',
required: true,
noDataExpression: true,
placeholder: '/',
default: '/',
},
{
displayName: 'Authentication Method',
name: 'authMethod',
type: 'options',
required: true,
noDataExpression: true,
options: [{ name: 'Universal Auth', value: 'universalAuth' }],
default: 'universalAuth',
},
// Universal Auth
{
displayName: 'Client ID',
name: 'clientId',
type: 'string',
default: '',
required: true,
noDataExpression: true,
placeholder: 'e.g. 8a7b1f1c-9f2a-4d6c-bf1a-2c4e6f8b1d3a',
displayOptions: {
show: {
authMethod: ['universalAuth'],
},
},
},
{
displayName: 'Client Secret',
name: 'clientSecret',
type: 'string',
default: '',
required: true,
noDataExpression: true,
placeholder: '***************',
typeOptions: { password: true },
displayOptions: {
show: {
authMethod: ['universalAuth'],
},
},
},
];
displayName = 'Infisical';
@ -60,87 +139,203 @@ export class InfisicalProvider extends SecretsProvider {
private cachedSecrets: Record<string, string> = {};
private client: InfisicalClient;
private settings: InfisicalSettings;
private environment: string;
private http: AxiosInstance;
private currentToken: string | null = null;
private tokenExpiresAt: number | null = null;
private refreshTimeout: NodeJS.Timeout | null = null;
private refreshAbort = new AbortController();
constructor(readonly logger = Container.get(Logger)) {
super();
this.logger = this.logger.scoped('external-secrets');
}
async init(settings: SecretsProviderSettings): Promise<void> {
this.settings = settings.settings as unknown as InfisicalSettings;
}
async update(): Promise<void> {
if (!this.client) {
throw new UnexpectedError('Updated attempted on Infisical when initialization failed');
}
if (!(await this.test())[0]) {
throw new UnexpectedError('Infisical provider test failed during update');
}
const secrets = (await this.client.getAllSecrets({
environment: this.environment,
path: '/',
attachToProcessEnv: false,
includeImports: true,
})) as InfisicalSecret[];
const newCache = Object.fromEntries(
secrets.map((s) => [s.secretName, s.secretValue]),
) as Record<string, string>;
if (Object.keys(newCache).length === 1 && '' in newCache) {
this.cachedSecrets = {};
} else {
this.cachedSecrets = newCache;
}
const baseURL = new URL(this.settings.siteURL);
this.http = axios.create({ baseURL: baseURL.toString() });
this.http.interceptors.request.use((config) => {
if (this.currentToken) {
config.headers.Authorization = `Bearer ${this.currentToken}`;
}
return config;
});
this.logger.debug('Infisical provider initialized');
}
protected async doConnect(): Promise<void> {
const { default: InfisicalClientClass } = await import('infisical-node');
this.client = new InfisicalClientClass(this.settings);
this.refreshAbort = new AbortController();
const [testSuccess] = await this.test();
if (this.settings.authMethod === 'universalAuth') {
if (!this.settings.clientId || !this.settings.clientSecret) {
throw new UnexpectedError('Client ID and Client Secret are required for Universal Auth');
}
await this.loginUniversalAuth();
}
const [testSuccess, testMessage] = await this.test();
if (!testSuccess) {
throw new Error('Connection test failed');
throw new Error(testMessage ?? 'Connection test failed');
}
this.environment = await this.getEnvironment();
}
async getEnvironment(): Promise<string> {
const { getServiceTokenData } = await import('infisical-node/lib/api/serviceTokenData');
const serviceTokenData = (await getServiceTokenData(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
this.client.clientConfig,
)) as InfisicalServiceToken;
if (serviceTokenData.environment) {
return serviceTokenData.environment;
}
if (serviceTokenData.scopes) {
return serviceTokenData.scopes[0].environment;
}
throw new UnexpectedError("Couldn't find environment for Infisical");
}
async test(): Promise<[boolean] | [boolean, string]> {
if (!this.client) {
return [false, 'Client not initialized'];
}
try {
const { populateClientWorkspaceConfigsHelper } = await import(
'infisical-node/lib/helpers/key'
);
await populateClientWorkspaceConfigsHelper(this.client.clientConfig);
return [true];
} catch (e) {
return [false];
}
this.setupTokenRefresh();
}
async disconnect(): Promise<void> {
//
if (this.refreshTimeout !== null) {
clearTimeout(this.refreshTimeout);
this.refreshTimeout = null;
}
this.refreshAbort.abort();
this.currentToken = null;
this.tokenExpiresAt = null;
this.cachedSecrets = {};
}
getSecret(name: string): IDataObject {
return this.cachedSecrets[name] as unknown as IDataObject;
async test(): Promise<[boolean] | [boolean, string]> {
try {
const resp = await this.http.get(
`/api/v1/workspace/${encodeURIComponent(this.settings.projectId)}`,
{ validateStatus: () => true },
);
if (resp.status >= 200 && resp.status < 300) {
return [true];
}
if (resp.status === 401) {
return [false, 'Invalid credentials'];
}
if (resp.status === 403) {
return [
false,
'Permission denied. Verify the machine identity has access to this project.',
];
}
if (resp.status === 404) {
return [false, 'Project not found. Check the Project ID and Site URL.'];
}
return [false, `Unexpected response from Infisical (status ${resp.status}).`];
} catch (error) {
if (axios.isAxiosError(error) && error.code === 'ECONNREFUSED') {
return [false, 'Connection refused. Check the Site URL.'];
}
return [false, error instanceof Error ? error.message : 'Connection test failed'];
}
}
async update(): Promise<void> {
if (!this.currentToken) {
throw new UnexpectedError('Update attempted on Infisical before authentication');
}
await this.ensureTokenFresh();
try {
this.cacheSecrets(await this.fetchSecrets());
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 401) {
this.logger.debug('Infisical token rejected during update; re-authenticating and retrying');
await this.loginUniversalAuth();
this.cacheSecrets(await this.fetchSecrets());
return;
}
throw error;
}
}
private async fetchSecrets(): Promise<InfisicalListSecretsResponse> {
const resp = await this.http.get<InfisicalListSecretsResponse>('/api/v4/secrets', {
params: {
projectId: this.settings.projectId,
environment: this.settings.environment,
secretPath: this.settings.secretPath,
},
});
return resp.data;
}
private dedupeSecrets(secrets: InfisicalSecret[], imports: InfisicalImport[]): InfisicalSecret[] {
const dedupedSecrets = new Map<string, InfisicalSecret>();
secrets.forEach((s) => {
dedupedSecrets.set(s.secretKey, s);
});
imports.forEach((i) => {
i.secrets.forEach((s) => {
if (!dedupedSecrets.has(s.secretKey)) {
dedupedSecrets.set(s.secretKey, s);
}
});
});
return Array.from(dedupedSecrets.values());
}
private cacheSecrets(data: InfisicalListSecretsResponse): void {
const dedupedSecrets = this.dedupeSecrets(data.secrets, data.imports);
this.cachedSecrets = Object.fromEntries(
dedupedSecrets.map((s) => [s.secretKey, s.secretValue]),
);
this.logger.debug(
`Infisical provider cached ${Object.keys(this.cachedSecrets).length} secrets`,
);
}
private async ensureTokenFresh(): Promise<void> {
if (this.tokenExpiresAt === null) return;
if (Date.now() < this.tokenExpiresAt) return;
await this.loginUniversalAuth();
}
private async loginUniversalAuth(): Promise<void> {
const resp = await this.http.post<InfisicalUniversalAuthLoginResponse>(
'/api/v1/auth/universal-auth/login',
{
clientId: this.settings.clientId,
clientSecret: this.settings.clientSecret,
},
);
this.currentToken = resp.data.accessToken;
this.tokenExpiresAt =
Date.now() + Math.max(resp.data.expiresIn - TOKEN_REFRESH_LEEWAY_SECONDS, 60) * 1000;
}
private setupTokenRefresh(): void {
if (this.refreshTimeout !== null) {
clearTimeout(this.refreshTimeout);
this.refreshTimeout = null;
}
if (this.tokenExpiresAt === null) return;
const remaining = this.tokenExpiresAt - Date.now();
const refreshIn = Math.max(remaining / 2, MIN_REFRESH_DELAY_MS);
this.refreshTimeout = setTimeout(this.tokenRefresh, refreshIn);
}
private tokenRefresh = async (): Promise<void> => {
if (this.refreshAbort.signal.aborted) return;
try {
await this.loginUniversalAuth();
if (this.refreshAbort.signal.aborted) return;
this.setupTokenRefresh();
} catch {
this.logger.error('Failed to refresh Infisical token. Attempting reconnect.');
void this.connect();
}
};
getSecret(name: string): string | undefined {
return this.cachedSecrets[name];
}
getSecretNames(): string[] {

View File

@ -3372,7 +3372,6 @@
"settings.externalSecrets.actionBox.description.link": "More info",
"settings.externalSecrets.actionBox.buttonText": "See plans",
"settings.externalSecrets.card.setUp": "Set Up",
"settings.externalSecrets.card.deprecated": "deprecated",
"settings.externalSecrets.card.secretsCount": "{count} secret | {count} secrets",
"settings.externalSecrets.card.connectedAt": "Connected {date}",
"settings.externalSecrets.card.connected": "Enabled",

View File

@ -12,14 +12,7 @@ import { DateTime } from 'luxon';
import { computed, nextTick, onMounted, toRef } from 'vue';
import { isDateObject } from '@/app/utils/typeGuards';
import {
N8nActionToggle,
N8nBadge,
N8nButton,
N8nCard,
N8nIcon,
N8nText,
} from '@n8n/design-system';
import { N8nActionToggle, N8nButton, N8nCard, N8nText } from '@n8n/design-system';
const props = defineProps<{
provider: ExternalSecretsProvider;
}>();
@ -142,12 +135,6 @@ async function onActionDropdownClick(id: string) {
</span>
</N8nText>
</div>
<div v-if="provider.name === 'infisical'" :class="$style.deprecationWarning">
<N8nIcon :class="$style['warningTriangle']" icon="triangle-alert" />
<N8nBadge class="mr-xs" theme="tertiary" bold data-test-id="card-badge">
{{ i18n.baseText('settings.externalSecrets.card.deprecated') }}
</N8nBadge>
</div>
<div v-if="canConnect" :class="$style.cardActions">
<ExternalSecretsProviderConnectionSwitch
:provider="provider"
@ -198,13 +185,4 @@ async function onActionDropdownClick(id: string) {
align-items: center;
margin-left: var(--spacing--sm);
}
.deprecationWarning {
display: flex;
}
.warningTriangle {
color: var(--color--warning);
margin-right: var(--spacing--2xs);
}
</style>

View File

@ -206,69 +206,6 @@ describe('useConnectionModal', () => {
});
});
describe('infisical deprecation', () => {
beforeEach(() => {
mockModuleSettings['external-secrets'] = {
multipleConnections: true,
forProjects: true,
roleBasedAccess: false,
};
});
it('should not provide option to create new infisical connection if multipleConnections is enabled', () => {
const providerTypesWithInfisical = ref([
...mockProviderTypes,
{
type: 'infisical',
displayName: 'Infisical',
icon: 'infisical',
properties: [],
} as SecretProviderTypeResponse,
]);
const { providerTypeOptions } = useConnectionModal({
...defaultOptions,
providerTypes: providerTypesWithInfisical,
});
expect(providerTypeOptions.value.find((opt) => opt.value === 'infisical')).toBeUndefined();
expect(providerTypeOptions.value.length).toEqual(1);
expect(providerTypeOptions.value[0].value).toEqual('awsSecretsManager');
});
it('should still set selectedProviderType to infisical on existing infisical connection', async () => {
const providerTypesWithInfisical = ref([
...mockProviderTypes,
{
type: 'infisical',
displayName: 'Infisical',
icon: 'infisical',
properties: [],
} as SecretProviderTypeResponse,
]);
// Mock existing infisical connection
mockConnection.getConnection.mockResolvedValue({
id: 'infisical-id',
name: 'infisical-connection',
type: 'infisical',
state: 'connected',
settings: {},
});
const options = {
...defaultOptions,
providerTypes: providerTypesWithInfisical,
providerKey: ref('infisical-key'),
};
const { selectedProviderType, loadConnection } = useConnectionModal(options);
await loadConnection();
expect(selectedProviderType.value?.type).toBe('infisical');
});
});
describe('computed properties', () => {
it('should detect unsaved changes', async () => {
const options = { ...defaultOptions, providerKey: ref('testKey') };

View File

@ -15,8 +15,6 @@ import { getResourcePermissions } from '@n8n/permissions';
import { useProjectsStore } from '@/features/collaboration/projects/projects.store';
import type { ProjectSharingData } from '@/features/collaboration/projects/projects.types';
import { isComponentPublicInstance } from '@/app/utils/typeGuards';
import { useSettingsStore } from '@/app/stores/settings.store';
interface UseConnectionModalOptions {
providerTypes: Ref<SecretProviderTypeResponse[]>;
existingProviderNames?: Ref<string[]>;
@ -41,7 +39,6 @@ export function useConnectionModal(options: UseConnectionModalOptions) {
const rbacStore = useRBACStore();
const toast = useToast();
const projectsStore = useProjectsStore();
const settingsStore = useSettingsStore();
// State
const providerKey = ref<string | undefined>(options.providerKey?.value);
@ -166,22 +163,12 @@ export function useConnectionModal(options: UseConnectionModalOptions) {
const isEditMode = computed(() => !!providerKey.value);
const providerTypeOptions = computed(() => {
const prvdrTypeOptions = providerTypes.value.map((type) => ({
const providerTypeOptions = computed(() =>
providerTypes.value.map((type) => ({
label: type.displayName,
value: type.type,
}));
if (settingsStore.moduleSettings['external-secrets']?.multipleConnections) {
// infisical has been deprecated for a long time.
// In order to be able to fully remove the code for it
// we are no longer showing users the option to create connections to infisical.
// Any previously existing connections will keep working for now.
return prvdrTypeOptions.filter((opt) => opt.value !== 'infisical');
}
return prvdrTypeOptions;
});
})),
);
const settingsUpdated = computed(() => {
return Object.keys(connectionSettings.value).some((key) => {

View File

@ -3197,9 +3197,6 @@ importers:
http-proxy-middleware:
specifier: ^3.0.5
version: 3.0.5
infisical-node:
specifier: 1.3.0
version: 1.3.0
ioredis:
specifier: 5.3.2
version: 5.3.2
@ -15531,10 +15528,6 @@ packages:
infer-owner@1.0.4:
resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==}
infisical-node@1.3.0:
resolution: {integrity: sha512-tTnnExRAO/ZyqiRdnSlBisErNToYWgtunMWh+8opClEt5qjX7l6HC/b4oGo2AuR2Pf41IR+oqo+dzkM1TCvlUA==}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
inflected@2.1.0:
resolution: {integrity: sha512-hAEKNxvHf2Iq3H60oMBHkB4wl5jn3TPF3+fXek/sRwAB5gP9xWs4r7aweSF95f99HFoz69pnZTcu8f0SIHV18w==}
@ -20526,15 +20519,9 @@ packages:
turndown@7.2.2:
resolution: {integrity: sha512-1F7db8BiExOKxjSMU2b7if62D/XOyQyZbPKq/nUwopfgnHlqXHqQ0lvfUTeUIr1lZJzOPFn43dODyMSIfvWRKQ==}
tweetnacl-util@0.15.1:
resolution: {integrity: sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==}
tweetnacl@0.14.5:
resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==}
tweetnacl@1.0.3:
resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==}
type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
@ -34998,16 +34985,6 @@ snapshots:
infer-owner@1.0.4:
optional: true
infisical-node@1.3.0:
dependencies:
axios: 1.16.1(debug@4.4.3)
dotenv: 16.6.1
tweetnacl: 1.0.3
tweetnacl-util: 0.15.1
transitivePeerDependencies:
- debug
- supports-color
inflected@2.1.0: {}
inflight@1.0.6:
@ -41236,12 +41213,8 @@ snapshots:
dependencies:
'@mixmark-io/domino': 2.2.0
tweetnacl-util@0.15.1: {}
tweetnacl@0.14.5: {}
tweetnacl@1.0.3: {}
type-check@0.4.0:
dependencies:
prelude-ls: 1.2.1