UNPKG

n8n

Version:

n8n Workflow Automation Tool

343 lines • 12.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.InfisicalProvider = void 0; const backend_common_1 = require("@n8n/backend-common"); const backend_network_1 = require("@n8n/backend-network"); const di_1 = require("@n8n/di"); const n8n_workflow_1 = require("n8n-workflow"); const constants_1 = require("../constants"); const secrets_provider_errors_1 = require("../errors/secrets-provider-errors"); const types_1 = require("../types"); const TOKEN_REFRESH_LEEWAY_SECONDS = 60; const MIN_REFRESH_DELAY_MS = 60 * 1000; class InfisicalProvider extends types_1.SecretsProvider { constructor(logger = di_1.Container.get(backend_common_1.Logger), outboundHttp = di_1.Container.get(backend_network_1.OutboundHttp)) { super(); this.logger = logger; this.outboundHttp = outboundHttp; this.properties = [ constants_1.DOCS_HELP_NOTICE, { displayName: 'Site URL', name: 'siteURL', type: 'string', hint: "The absolute URL of the Infisical instance. Change it only if you're self-hosting Infisical.", required: true, noDataExpression: true, 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', }, { 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'], }, }, }, ]; this.displayName = 'Infisical'; this.name = 'infisical'; this.cachedSecrets = {}; this.currentToken = null; this.tokenExpiresAt = null; this.refreshTimeout = null; this.refreshAbort = new AbortController(); this.tokenRefresh = async () => { if (this.refreshAbort.signal.aborted) return; try { await this.loginUniversalAuth(); if (this.refreshAbort.signal.aborted) return; this.setupTokenRefresh(); } catch (error) { this.logOperationFailure('Failed to refresh Infisical token. Attempting reconnect.', { operation: 'tokenRefresh', error, context: (0, secrets_provider_errors_1.buildHttpProviderErrorContext)(error), }); void this.connect(); } }; this.logger = this.logger.scoped('external-secrets'); } async init(settings) { this.settings = settings.settings; this.http = this.outboundHttp.requests({ baseURL: this.settings.siteURL, headers: () => this.buildAuthHeaders(), ssrf: 'disabled', }); this.logger.debug('Infisical provider initialized'); } async doConnect() { this.refreshAbort = new AbortController(); try { if (this.settings.authMethod === 'universalAuth') { if (!this.settings.clientId || !this.settings.clientSecret) { throw new n8n_workflow_1.UnexpectedError('Client ID and Client Secret are required for Universal Auth'); } await this.loginUniversalAuth(); } await this.verifyWorkspaceAccess(); this.setupTokenRefresh(); } catch (error) { const context = error instanceof n8n_workflow_1.UnexpectedError ? undefined : (0, secrets_provider_errors_1.buildHttpProviderErrorContext)(error); this.logOperationFailure('Failed to connect Infisical provider', { operation: 'connect', error, context, }); throw error; } } async disconnect() { if (this.refreshTimeout !== null) { clearTimeout(this.refreshTimeout); this.refreshTimeout = null; } this.refreshAbort.abort(); this.currentToken = null; this.tokenExpiresAt = null; this.cachedSecrets = {}; } async test() { try { await this.verifyWorkspaceAccess(); return [true]; } catch (error) { this.logOperationFailure('Infisical provider test failed', { operation: 'test', error, context: { ...(0, secrets_provider_errors_1.buildHttpProviderErrorContext)(error), endpoint: 'workspace', }, }); if ((0, backend_network_1.isConnectionRefusedError)(error)) { return [false, 'Connection refused. Check the Site URL.']; } return [false, error instanceof Error ? error.message : 'Connection test failed']; } } async update() { if (!this.currentToken) { throw new n8n_workflow_1.UnexpectedError('Update attempted on Infisical before authentication'); } try { await this.ensureTokenFresh(); try { this.cacheSecrets(await this.fetchSecrets()); } catch (error) { if ((0, backend_network_1.httpStatusFromError)(error) === 401) { this.logger.debug('Infisical token rejected during update; re-authenticating and retrying'); await this.loginUniversalAuth(); this.cacheSecrets(await this.fetchSecrets()); return; } throw error; } } catch (error) { this.logOperationFailure('Failed to update Infisical provider secrets', { operation: 'update', error, context: { ...(0, secrets_provider_errors_1.buildHttpProviderErrorContext)(error), endpoint: 'secrets', }, }); throw error; } } async fetchSecrets() { return await this.http.request({ url: '/api/v4/secrets', method: 'GET', qs: { projectId: this.settings.projectId, environment: this.settings.environment, secretPath: this.settings.secretPath, }, json: true, }); } dedupeSecrets(secrets, imports) { const dedupedSecrets = new Map(); 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()); } cacheSecrets(data) { 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`); } async ensureTokenFresh() { if (this.tokenExpiresAt === null) return; if (Date.now() < this.tokenExpiresAt) return; await this.loginUniversalAuth(); } async loginUniversalAuth() { const body = await this.http.request({ url: '/api/v1/auth/universal-auth/login', method: 'POST', body: { clientId: this.settings.clientId, clientSecret: this.settings.clientSecret, }, json: true, }); this.currentToken = body.accessToken; this.tokenExpiresAt = Date.now() + Math.max(body.expiresIn - TOKEN_REFRESH_LEEWAY_SECONDS, 60) * 1000; } setupTokenRefresh() { 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); } getSecret(name) { return this.cachedSecrets[name]; } getSecretNames() { return Object.keys(this.cachedSecrets); } hasSecret(name) { return name in this.cachedSecrets; } buildAuthHeaders() { if (this.currentToken) { return { Authorization: `Bearer ${this.currentToken}`, }; } return {}; } async verifyWorkspaceAccess() { const resp = await this.http.request({ url: `/api/v1/workspace/${encodeURIComponent(this.settings.projectId)}`, method: 'GET', returnFullResponse: true, ignoreHttpStatusErrors: true, }); if (resp.statusCode >= 200 && resp.statusCode < 300) { return; } if (resp.statusCode === 401) { throw new Error('Invalid credentials'); } if (resp.statusCode === 403) { throw new Error('Permission denied. Verify the machine identity has access to this project.'); } if (resp.statusCode === 404) { throw new Error('Project not found. Check the Project ID and Site URL.'); } throw new Error(`Unexpected response from Infisical (status ${resp.statusCode}).`); } logOperationFailure(message, params) { const context = { ...params.context }; if (this.settings) { const { siteURL, projectId, authMethod, environment, secretPath } = this.settings; Object.assign(context, { siteURL, projectId }); if (params.operation === 'connect' || params.operation === 'tokenRefresh') { context.authMethod = authMethod; } else if (params.operation === 'update') { Object.assign(context, { environment, secretPath }); } } (0, secrets_provider_errors_1.logSecretsProviderOperationFailure)({ logger: this.logger, message, providerName: this.name, providerDisplayName: this.displayName, operation: params.operation, error: params.error, context, }); } } exports.InfisicalProvider = InfisicalProvider; //# sourceMappingURL=infisical.js.map