diff --git a/packages/cli/src/__tests__/workflow-execute-additional-data.test.ts b/packages/cli/src/__tests__/workflow-execute-additional-data.test.ts index 08d9141201..559da8a2ef 100644 --- a/packages/cli/src/__tests__/workflow-execute-additional-data.test.ts +++ b/packages/cli/src/__tests__/workflow-execute-additional-data.test.ts @@ -4,6 +4,7 @@ import { ExecutionRepository } from '@n8n/db'; import { WorkflowRepository } from '@n8n/db'; import { Container } from '@n8n/di'; import { mock } from 'jest-mock-extended'; +import { ExternalSecretsProxy } from 'n8n-core'; import type { IWorkflowBase } from 'n8n-workflow'; import type { IExecuteWorkflowInfo, @@ -23,7 +24,6 @@ import { SubworkflowPolicyChecker, } from '@/executions/pre-execution-checks'; import { ExternalHooks } from '@/external-hooks'; -import { SecretsHelper } from '@/secrets-helpers.ee'; import { UrlService } from '@/services/url.service'; import { WorkflowStatisticsService } from '@/services/workflow-statistics.service'; import { Telemetry } from '@/telemetry'; @@ -86,12 +86,12 @@ describe('WorkflowExecuteAdditionalData', () => { const variablesService = mockInstance(VariablesService); variablesService.getAllCached.mockResolvedValue([]); const credentialsHelper = mockInstance(CredentialsHelper); - const secretsHelper = mockInstance(SecretsHelper); + const externalSecretsProxy = mockInstance(ExternalSecretsProxy); const eventService = mockInstance(EventService); mockInstance(ExternalHooks); Container.set(VariablesService, variablesService); Container.set(CredentialsHelper, credentialsHelper); - Container.set(SecretsHelper, secretsHelper); + Container.set(ExternalSecretsProxy, externalSecretsProxy); const executionRepository = mockInstance(ExecutionRepository); mockInstance(Telemetry); const workflowRepository = mockInstance(WorkflowRepository); @@ -306,7 +306,7 @@ describe('WorkflowExecuteAdditionalData', () => { userId: undefined, setExecutionStatus: expect.any(Function), variables: mockVariables, - secretsHelpers: secretsHelper, + externalSecretsProxy, startRunnerTask: expect.any(Function), logAiEvent: expect.any(Function), }); diff --git a/packages/cli/src/commands/base-command.ts b/packages/cli/src/commands/base-command.ts index aa258d1fec..91600bbdd0 100644 --- a/packages/cli/src/commands/base-command.ts +++ b/packages/cli/src/commands/base-command.ts @@ -11,6 +11,7 @@ import { ObjectStoreService, DataDeduplicationService, ErrorReporter, + ExternalSecretsProxy, } from 'n8n-core'; import { ensureError, sleep, UserError } from 'n8n-workflow'; @@ -278,6 +279,7 @@ export abstract class BaseCommand extends Command { async initExternalSecrets() { const secretsManager = Container.get(ExternalSecretsManager); await secretsManager.init(); + Container.get(ExternalSecretsProxy).setManager(secretsManager); } initWorkflowHistory() { diff --git a/packages/cli/src/controllers/oauth/__tests__/oauth1-credential.controller.test.ts b/packages/cli/src/controllers/oauth/__tests__/oauth1-credential.controller.test.ts index a714cf2431..baa0e1c503 100644 --- a/packages/cli/src/controllers/oauth/__tests__/oauth1-credential.controller.test.ts +++ b/packages/cli/src/controllers/oauth/__tests__/oauth1-credential.controller.test.ts @@ -6,7 +6,7 @@ import { Container } from '@n8n/di'; import Csrf from 'csrf'; import type { Response } from 'express'; import { captor, mock } from 'jest-mock-extended'; -import { Cipher, type InstanceSettings } from 'n8n-core'; +import { Cipher, type InstanceSettings, ExternalSecretsProxy } from 'n8n-core'; import type { IWorkflowExecuteAdditionalData } from 'n8n-workflow'; import nock from 'nock'; @@ -19,7 +19,6 @@ import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { ExternalHooks } from '@/external-hooks'; import type { OAuthRequest } from '@/requests'; -import { SecretsHelper } from '@/secrets-helpers.ee'; import * as WorkflowExecuteAdditionalData from '@/workflow-execute-additional-data'; import { mockInstance } from '@test/mocking'; @@ -28,7 +27,7 @@ jest.mock('@/workflow-execute-additional-data'); describe('OAuth1CredentialController', () => { mockInstance(Logger); mockInstance(ExternalHooks); - mockInstance(SecretsHelper); + mockInstance(ExternalSecretsProxy); mockInstance(VariablesService, { getAllCached: async () => [], }); diff --git a/packages/cli/src/controllers/oauth/__tests__/oauth2-credential.controller.test.ts b/packages/cli/src/controllers/oauth/__tests__/oauth2-credential.controller.test.ts index 203fb1c1e0..129a552919 100644 --- a/packages/cli/src/controllers/oauth/__tests__/oauth2-credential.controller.test.ts +++ b/packages/cli/src/controllers/oauth/__tests__/oauth2-credential.controller.test.ts @@ -6,7 +6,7 @@ import { Container } from '@n8n/di'; import Csrf from 'csrf'; import { type Response } from 'express'; import { captor, mock } from 'jest-mock-extended'; -import { Cipher, type InstanceSettings } from 'n8n-core'; +import { Cipher, type InstanceSettings, ExternalSecretsProxy } from 'n8n-core'; import type { IWorkflowExecuteAdditionalData } from 'n8n-workflow'; import nock from 'nock'; @@ -19,7 +19,6 @@ import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { ExternalHooks } from '@/external-hooks'; import type { OAuthRequest } from '@/requests'; -import { SecretsHelper } from '@/secrets-helpers.ee'; import * as WorkflowExecuteAdditionalData from '@/workflow-execute-additional-data'; import { mockInstance } from '@test/mocking'; @@ -27,7 +26,7 @@ jest.mock('@/workflow-execute-additional-data'); describe('OAuth2CredentialController', () => { mockInstance(Logger); - mockInstance(SecretsHelper); + mockInstance(ExternalSecretsProxy); mockInstance(VariablesService, { getAllCached: async () => [], }); diff --git a/packages/cli/src/credentials-helper.ts b/packages/cli/src/credentials-helper.ts index c284f75af6..e4cdf8b9bf 100644 --- a/packages/cli/src/credentials-helper.ts +++ b/packages/cli/src/credentials-helper.ts @@ -327,8 +327,6 @@ export class CredentialsHelper extends ICredentialsHelper { return decryptedDataOriginal; } - await additionalData?.secretsHelpers?.waitForInit(); - return await this.applyDefaultsAndOverwrites( additionalData, decryptedDataOriginal, diff --git a/packages/cli/src/eventbus/message-event-bus-destination/message-event-bus-destination-webhook.ee.ts b/packages/cli/src/eventbus/message-event-bus-destination/message-event-bus-destination-webhook.ee.ts index 521e920fee..35fd6357bb 100644 --- a/packages/cli/src/eventbus/message-event-bus-destination/message-event-bus-destination-webhook.ee.ts +++ b/packages/cli/src/eventbus/message-event-bus-destination/message-event-bus-destination-webhook.ee.ts @@ -4,6 +4,7 @@ import { Container } from '@n8n/di'; import axios from 'axios'; import type { AxiosRequestConfig, Method } from 'axios'; import { Agent as HTTPSAgent } from 'https'; +import { ExternalSecretsProxy } from 'n8n-core'; import { jsonParse, MessageEventBusDestinationTypeNames } from 'n8n-workflow'; import type { MessageEventBusDestinationOptions, @@ -14,7 +15,6 @@ import type { } from 'n8n-workflow'; import { CredentialsHelper } from '@/credentials-helper'; -import { SecretsHelper } from '@/secrets-helpers.ee'; import { MessageEventBusDestination } from './message-event-bus-destination.ee'; import { eventMessageGenericDestinationTestEvent } from '../event-message-classes/event-message-generic'; @@ -103,7 +103,7 @@ export class MessageEventBusDestinationWebhook if (foundCredential) { const credentialsDecrypted = await this.credentialsHelper?.getDecrypted( { - secretsHelpers: Container.get(SecretsHelper), + externalSecretsProxy: Container.get(ExternalSecretsProxy), } as unknown as IWorkflowExecuteAdditionalData, foundCredential[1], foundCredential[0], diff --git a/packages/cli/src/external-secrets.ee/__tests__/external-secrets-manager.ee.test.ts b/packages/cli/src/external-secrets.ee/__tests__/external-secrets-manager.ee.test.ts index 68aa52efc2..eef6792095 100644 --- a/packages/cli/src/external-secrets.ee/__tests__/external-secrets-manager.ee.test.ts +++ b/packages/cli/src/external-secrets.ee/__tests__/external-secrets-manager.ee.test.ts @@ -352,10 +352,10 @@ describe('External Secrets Manager', () => { expect(manager.getSecretNames('dummy')).toEqual(['test1', 'test2']); }); - test('should return undefined when provider does not exist', async () => { + test('should return an empty array when provider does not exist', async () => { await manager.init(); - expect(manager.getSecretNames('nonexistent')).toBeUndefined(); + expect(manager.getSecretNames('nonexistent')).toBeEmptyArray(); }); }); diff --git a/packages/cli/src/external-secrets.ee/external-secrets-manager.ee.ts b/packages/cli/src/external-secrets.ee/external-secrets-manager.ee.ts index 2d48891e29..8b775ee480 100644 --- a/packages/cli/src/external-secrets.ee/external-secrets-manager.ee.ts +++ b/packages/cli/src/external-secrets.ee/external-secrets-manager.ee.ts @@ -2,7 +2,7 @@ import { Logger } from '@n8n/backend-common'; import { SettingsRepository } from '@n8n/db'; import { OnPubSubEvent, OnShutdown } from '@n8n/decorators'; import { Service } from '@n8n/di'; -import { Cipher } from 'n8n-core'; +import { Cipher, type IExternalSecretsManager } from 'n8n-core'; import { jsonParse, type IDataObject, ensureError, UnexpectedError } from 'n8n-workflow'; import { EventService } from '@/events/event.service'; @@ -19,7 +19,7 @@ import { ExternalSecretsConfig } from './external-secrets.config'; import type { ExternalSecretsSettings, SecretsProvider, SecretsProviderSettings } from './types'; @Service() -export class ExternalSecretsManager { +export class ExternalSecretsManager implements IExternalSecretsManager { private providers: Record = {}; private initializingPromise?: Promise; @@ -211,7 +211,7 @@ export class ExternalSecretsManager { return provider in this.providers; } - getProviderNames(): string[] | undefined { + getProviderNames(): string[] { return Object.keys(this.providers); } @@ -223,8 +223,8 @@ export class ExternalSecretsManager { return this.getProvider(provider)?.hasSecret(name) ?? false; } - getSecretNames(provider: string): string[] | undefined { - return this.getProvider(provider)?.getSecretNames(); + getSecretNames(provider: string): string[] { + return this.getProvider(provider)?.getSecretNames() ?? []; } getAllSecretNames(): Record { diff --git a/packages/cli/src/scaling/__tests__/job-processor.service.test.ts b/packages/cli/src/scaling/__tests__/job-processor.service.test.ts index 6648c4dab2..710d60a615 100644 --- a/packages/cli/src/scaling/__tests__/job-processor.service.test.ts +++ b/packages/cli/src/scaling/__tests__/job-processor.service.test.ts @@ -3,6 +3,7 @@ import type { IExecutionResponse } from '@n8n/db'; import type { ExecutionRepository } from '@n8n/db'; import { mock } from 'jest-mock-extended'; import type { WorkflowExecute as ActualWorkflowExecute } from 'n8n-core'; +import { ExternalSecretsProxy } from 'n8n-core'; import { mockInstance } from 'n8n-core/test/utils'; import type { IPinData, ITaskData, IWorkflowExecuteAdditionalData } from 'n8n-workflow'; import { Workflow, type IRunExecutionData, type WorkflowExecuteMode } from 'n8n-workflow'; @@ -11,7 +12,6 @@ import { CredentialsHelper } from '@/credentials-helper'; import { VariablesService } from '@/environments.ee/variables/variables.service.ee'; import { ExternalHooks } from '@/external-hooks'; import type { ManualExecutionService } from '@/manual-execution.service'; -import { SecretsHelper } from '@/secrets-helpers.ee'; import { WorkflowStatisticsService } from '@/services/workflow-statistics.service'; import * as WorkflowExecuteAdditionalData from '@/workflow-execute-additional-data'; import { WorkflowStaticDataService } from '@/workflows/workflow-static-data.service'; @@ -23,7 +23,7 @@ mockInstance(VariablesService, { getAllCached: jest.fn().mockResolvedValue([]), }); mockInstance(CredentialsHelper); -mockInstance(SecretsHelper); +mockInstance(ExternalSecretsProxy); mockInstance(WorkflowStaticDataService); mockInstance(WorkflowStatisticsService); mockInstance(ExternalHooks); diff --git a/packages/cli/src/secrets-helpers.ee.ts b/packages/cli/src/secrets-helpers.ee.ts deleted file mode 100644 index 7390bf8795..0000000000 --- a/packages/cli/src/secrets-helpers.ee.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Service } from '@n8n/di'; -import type { SecretsHelpersBase } from 'n8n-workflow'; - -import { ExternalSecretsManager } from './external-secrets.ee/external-secrets-manager.ee'; - -@Service() -export class SecretsHelper implements SecretsHelpersBase { - constructor(private service: ExternalSecretsManager) {} - - async update() { - if (!this.service.initialized) { - await this.service.init(); - } - await this.service.updateSecrets(); - } - - async waitForInit() { - if (!this.service.initialized) { - await this.service.init(); - } - } - - getSecret(provider: string, name: string) { - return this.service.getSecret(provider, name); - } - - hasSecret(provider: string, name: string): boolean { - return this.service.hasSecret(provider, name); - } - - hasProvider(provider: string): boolean { - return this.service.hasProvider(provider); - } - - listProviders(): string[] { - return this.service.getProviderNames() ?? []; - } - - listSecrets(provider: string): string[] { - return this.service.getSecretNames(provider) ?? []; - } -} diff --git a/packages/cli/src/workflow-execute-additional-data.ts b/packages/cli/src/workflow-execute-additional-data.ts index 16f06a1f34..78406361f8 100644 --- a/packages/cli/src/workflow-execute-additional-data.ts +++ b/packages/cli/src/workflow-execute-additional-data.ts @@ -7,7 +7,7 @@ import { Logger } from '@n8n/backend-common'; import { GlobalConfig } from '@n8n/config'; import { ExecutionRepository, WorkflowRepository } from '@n8n/db'; import { Container } from '@n8n/di'; -import { WorkflowExecute } from 'n8n-core'; +import { ExternalSecretsProxy, WorkflowExecute } from 'n8n-core'; import { UnexpectedError, Workflow } from 'n8n-workflow'; import type { IDataObject, @@ -46,7 +46,6 @@ import { import type { UpdateExecutionPayload } from '@/interfaces'; import { NodeTypes } from '@/node-types'; import { Push } from '@/push'; -import { SecretsHelper } from '@/secrets-helpers.ee'; import { UrlService } from '@/services/url.service'; import { TaskRequester } from '@/task-runners/task-managers/task-requester'; import { findSubworkflowStart } from '@/utils'; @@ -388,7 +387,7 @@ export async function getBase( userId, setExecutionStatus, variables, - secretsHelpers: Container.get(SecretsHelper), + externalSecretsProxy: Container.get(ExternalSecretsProxy), async startRunnerTask( additionalData: IWorkflowExecuteAdditionalData, jobType: string, diff --git a/packages/cli/test/integration/active-workflow-manager.test.ts b/packages/cli/test/integration/active-workflow-manager.test.ts index 5e5e27a31a..8f987db32e 100644 --- a/packages/cli/test/integration/active-workflow-manager.test.ts +++ b/packages/cli/test/integration/active-workflow-manager.test.ts @@ -2,7 +2,7 @@ import type { WebhookEntity } from '@n8n/db'; import { WorkflowRepository } from '@n8n/db'; import { Container } from '@n8n/di'; import { mock } from 'jest-mock-extended'; -import { InstanceSettings } from 'n8n-core'; +import { InstanceSettings, ExternalSecretsProxy } from 'n8n-core'; import { FormTrigger } from 'n8n-nodes-base/nodes/Form/FormTrigger.node'; import { ScheduleTrigger } from 'n8n-nodes-base/nodes/Schedule/ScheduleTrigger.node'; import { NodeApiError, Workflow } from 'n8n-workflow'; @@ -19,7 +19,6 @@ import { ExecutionService } from '@/executions/execution.service'; import { ExternalHooks } from '@/external-hooks'; import { NodeTypes } from '@/node-types'; import { Push } from '@/push'; -import { SecretsHelper } from '@/secrets-helpers.ee'; import * as WebhookHelpers from '@/webhooks/webhook-helpers'; import { WebhookService } from '@/webhooks/webhook.service'; import * as AdditionalData from '@/workflow-execute-additional-data'; @@ -33,7 +32,7 @@ import { mockInstance } from '../shared/mocking'; mockInstance(ActiveExecutions); mockInstance(Push); -mockInstance(SecretsHelper); +mockInstance(ExternalSecretsProxy); mockInstance(ExecutionService); mockInstance(WorkflowService); diff --git a/packages/core/src/execution-engine/__tests__/execution-lifecycle-hooks.test.ts b/packages/core/src/execution-engine/__tests__/execution-lifecycle-hooks.test.ts index c255182e32..8c51baebb3 100644 --- a/packages/core/src/execution-engine/__tests__/execution-lifecycle-hooks.test.ts +++ b/packages/core/src/execution-engine/__tests__/execution-lifecycle-hooks.test.ts @@ -13,7 +13,7 @@ import type { import type { ExecutionLifecycleHookName, - ExecutionLifecyleHookHandlers, + ExecutionLifecycleHookHandlers, } from '../execution-lifecycle-hooks'; import { ExecutionLifecycleHooks } from '../execution-lifecycle-hooks'; @@ -46,12 +46,14 @@ describe('ExecutionLifecycleHooks', () => { describe('addHandler()', () => { const hooksHandlers = mock<{ - [K in keyof ExecutionLifecyleHookHandlers]: ExecutionLifecyleHookHandlers[K][number]; + [K in keyof ExecutionLifecycleHookHandlers]: ExecutionLifecycleHookHandlers[K][number]; }>(); const testCases: Array<{ hook: ExecutionLifecycleHookName; - args: Parameters; + args: Parameters< + ExecutionLifecycleHookHandlers[keyof ExecutionLifecycleHookHandlers][number] + >; }> = [ { hook: 'nodeExecuteBefore', args: ['testNode', mock()] }, { diff --git a/packages/core/src/execution-engine/__tests__/external-secrets-proxy.test.ts b/packages/core/src/execution-engine/__tests__/external-secrets-proxy.test.ts new file mode 100644 index 0000000000..5c1ad4fcd9 --- /dev/null +++ b/packages/core/src/execution-engine/__tests__/external-secrets-proxy.test.ts @@ -0,0 +1,121 @@ +import { mock } from 'jest-mock-extended'; + +import { ExternalSecretsProxy, type IExternalSecretsManager } from '../external-secrets-proxy'; + +describe('ExternalSecretsProxy', () => { + let proxy: ExternalSecretsProxy; + const manager = mock(); + + beforeEach(() => { + jest.resetAllMocks(); + proxy = new ExternalSecretsProxy(); + }); + + describe('update', () => { + it('should update secrets when manager is set', async () => { + manager.updateSecrets.mockResolvedValue(); + proxy.setManager(manager); + + await proxy.update(); + + expect(manager.updateSecrets).toHaveBeenCalledTimes(1); + }); + + it('should not throw when updating without a manager', async () => { + await expect(proxy.update()).resolves.not.toThrow(); + }); + }); + + describe('getSecret', () => { + it('should get secret from manager', () => { + const secretValue = { key: 'value' }; + manager.getSecret.mockReturnValue(secretValue); + proxy.setManager(manager); + + const result = proxy.getSecret('aws', 'api-key'); + + expect(manager.getSecret).toHaveBeenCalledWith('aws', 'api-key'); + expect(result).toBe(secretValue); + }); + + it('should return undefined when getting secret without a manager', () => { + const result = proxy.getSecret('aws', 'api-key'); + + expect(result).toBeUndefined(); + }); + }); + + describe('hasSecret', () => { + it('should check if secret exists', () => { + manager.hasSecret.mockReturnValue(true); + proxy.setManager(manager); + + const result = proxy.hasSecret('aws', 'api-key'); + + expect(manager.hasSecret).toHaveBeenCalledWith('aws', 'api-key'); + expect(result).toBe(true); + }); + + it('should return false when checking secret without a manager', () => { + const result = proxy.hasSecret('aws', 'api-key'); + + expect(result).toBe(false); + }); + }); + + describe('hasProvider', () => { + it('should check if provider exists', () => { + manager.hasProvider.mockReturnValue(true); + proxy.setManager(manager); + + const result = proxy.hasProvider('aws'); + + expect(manager.hasProvider).toHaveBeenCalledWith('aws'); + expect(result).toBe(true); + }); + + it('should return false when checking provider without a manager', () => { + const result = proxy.hasProvider('aws'); + + expect(result).toBe(false); + }); + }); + + describe('listProviders', () => { + it('should list providers', () => { + const providers = ['aws', 'gcp', 'azure']; + manager.getProviderNames.mockReturnValue(providers); + proxy.setManager(manager); + + const result = proxy.listProviders(); + + expect(manager.getProviderNames).toHaveBeenCalledTimes(1); + expect(result).toEqual(providers); + }); + + it('should return empty array when listing providers without a manager', () => { + const result = proxy.listProviders(); + + expect(result).toEqual([]); + }); + }); + + describe('listSecrets', () => { + it('should list secrets for a provider', () => { + const secrets = ['api-key', 'api-secret', 'token']; + manager.getSecretNames.mockReturnValue(secrets); + proxy.setManager(manager); + + const result = proxy.listSecrets('aws'); + + expect(manager.getSecretNames).toHaveBeenCalledWith('aws'); + expect(result).toEqual(secrets); + }); + + it('should return empty array when listing secrets without a manager', () => { + const result = proxy.listSecrets('aws'); + + expect(result).toEqual([]); + }); + }); +}); diff --git a/packages/core/src/execution-engine/execution-lifecycle-hooks.ts b/packages/core/src/execution-engine/execution-lifecycle-hooks.ts index a7793c1ca9..8a72f01bfc 100644 --- a/packages/core/src/execution-engine/execution-lifecycle-hooks.ts +++ b/packages/core/src/execution-engine/execution-lifecycle-hooks.ts @@ -11,7 +11,7 @@ import type { WorkflowExecuteMode, } from 'n8n-workflow'; -export type ExecutionLifecyleHookHandlers = { +export type ExecutionLifecycleHookHandlers = { nodeExecuteBefore: Array< ( this: ExecutionLifecycleHooks, @@ -56,7 +56,7 @@ s */ >; }; -export type ExecutionLifecycleHookName = keyof ExecutionLifecyleHookHandlers; +export type ExecutionLifecycleHookName = keyof ExecutionLifecycleHookHandlers; /** * Contains hooks that trigger at specific events in an execution's lifecycle. Every hook has an array of callbacks to run. @@ -77,7 +77,7 @@ export type ExecutionLifecycleHookName = keyof ExecutionLifecyleHookHandlers; * ``` */ export class ExecutionLifecycleHooks { - readonly handlers: ExecutionLifecyleHookHandlers = { + readonly handlers: ExecutionLifecycleHookHandlers = { nodeExecuteAfter: [], nodeExecuteBefore: [], nodeFetchedData: [], @@ -92,18 +92,18 @@ export class ExecutionLifecycleHooks { readonly workflowData: IWorkflowBase, ) {} - addHandler( + addHandler( hookName: Hook, - ...handlers: Array + ...handlers: Array ): void { // @ts-expect-error FIX THIS this.handlers[hookName].push(...handlers); } async runHook< - Hook extends keyof ExecutionLifecyleHookHandlers, + Hook extends keyof ExecutionLifecycleHookHandlers, Params extends unknown[] = Parameters< - Exclude[number] + Exclude[number] >, >(hookName: Hook, parameters: Params) { const hooks = this.handlers[hookName]; @@ -116,9 +116,3 @@ export class ExecutionLifecycleHooks { } } } - -declare module 'n8n-workflow' { - interface IWorkflowExecuteAdditionalData { - hooks?: ExecutionLifecycleHooks; - } -} diff --git a/packages/core/src/execution-engine/external-secrets-proxy.ts b/packages/core/src/execution-engine/external-secrets-proxy.ts new file mode 100644 index 0000000000..a6752f2097 --- /dev/null +++ b/packages/core/src/execution-engine/external-secrets-proxy.ts @@ -0,0 +1,43 @@ +import { Service } from '@n8n/di'; + +export interface IExternalSecretsManager { + updateSecrets(): Promise; + hasSecret(provider: string, name: string): boolean; + getSecret(provider: string, name: string): unknown; + getSecretNames(provider: string): string[]; + hasProvider(provider: string): boolean; + getProviderNames(): string[]; +} + +@Service() +export class ExternalSecretsProxy { + private manager?: IExternalSecretsManager; + + setManager(manager: IExternalSecretsManager) { + this.manager = manager; + } + + async update() { + await this.manager?.updateSecrets(); + } + + getSecret(provider: string, name: string) { + return this.manager?.getSecret(provider, name); + } + + hasSecret(provider: string, name: string): boolean { + return !!this.manager && this.manager.hasSecret(provider, name); + } + + hasProvider(provider: string): boolean { + return !!this.manager && this.manager.hasProvider(provider); + } + + listProviders(): string[] { + return this.manager?.getProviderNames() ?? []; + } + + listSecrets(provider: string): string[] { + return this.manager?.getSecretNames(provider) ?? []; + } +} diff --git a/packages/core/src/execution-engine/index.ts b/packages/core/src/execution-engine/index.ts index bb8adfb34c..34a31194f2 100644 --- a/packages/core/src/execution-engine/index.ts +++ b/packages/core/src/execution-engine/index.ts @@ -1,3 +1,13 @@ +import type { ExecutionLifecycleHooks } from './execution-lifecycle-hooks'; +import type { ExternalSecretsProxy } from './external-secrets-proxy'; + +declare module 'n8n-workflow' { + interface IWorkflowExecuteAdditionalData { + hooks?: ExecutionLifecycleHooks; + externalSecretsProxy: ExternalSecretsProxy; + } +} + export * from './active-workflows'; export * from './interfaces'; export * from './routing-node'; @@ -6,3 +16,4 @@ export * from './partial-execution-utils'; export * from './node-execution-context/utils/execution-metadata'; export * from './workflow-execute'; export { ExecutionLifecycleHooks } from './execution-lifecycle-hooks'; +export { ExternalSecretsProxy, type IExternalSecretsManager } from './external-secrets-proxy'; diff --git a/packages/core/src/execution-engine/node-execution-context/utils/__tests__/get-additional-keys.test.ts b/packages/core/src/execution-engine/node-execution-context/utils/__tests__/get-additional-keys.test.ts index b78de39a6d..21b96ac9bd 100644 --- a/packages/core/src/execution-engine/node-execution-context/utils/__tests__/get-additional-keys.test.ts +++ b/packages/core/src/execution-engine/node-execution-context/utils/__tests__/get-additional-keys.test.ts @@ -1,24 +1,20 @@ import { mock } from 'jest-mock-extended'; import { LoggerProxy } from 'n8n-workflow'; -import type { - IDataObject, - IRunExecutionData, - IWorkflowExecuteAdditionalData, - SecretsHelpersBase, -} from 'n8n-workflow'; +import type { IDataObject, IRunExecutionData, IWorkflowExecuteAdditionalData } from 'n8n-workflow'; import { PLACEHOLDER_EMPTY_EXECUTION_ID } from '@/constants'; +import type { ExternalSecretsProxy } from '@/execution-engine/external-secrets-proxy'; import { getAdditionalKeys } from '../get-additional-keys'; describe('getAdditionalKeys', () => { - const secretsHelpers = mock(); + const externalSecretsProxy = mock(); const additionalData = mock({ executionId: '123', webhookWaitingBaseUrl: 'https://webhook.test', formWaitingBaseUrl: 'https://form.test', variables: { testVar: 'value' }, - secretsHelpers, + externalSecretsProxy, }); const runExecutionData = mock({ @@ -30,11 +26,11 @@ describe('getAdditionalKeys', () => { beforeAll(() => { LoggerProxy.init(mock()); - secretsHelpers.hasProvider.mockReturnValue(true); - secretsHelpers.hasSecret.mockReturnValue(true); - secretsHelpers.getSecret.mockReturnValue('secret-value'); - secretsHelpers.listSecrets.mockReturnValue(['secret1']); - secretsHelpers.listProviders.mockReturnValue(['provider1']); + externalSecretsProxy.hasProvider.mockReturnValue(true); + externalSecretsProxy.hasSecret.mockReturnValue(true); + externalSecretsProxy.getSecret.mockReturnValue('secret-value'); + externalSecretsProxy.listSecrets.mockReturnValue(['secret1']); + externalSecretsProxy.listProviders.mockReturnValue(['provider1']); }); it('should use placeholder execution ID when none provided', () => { diff --git a/packages/core/src/execution-engine/node-execution-context/utils/get-secrets-proxy.ts b/packages/core/src/execution-engine/node-execution-context/utils/get-secrets-proxy.ts index d5693dd8f1..2014b30dde 100644 --- a/packages/core/src/execution-engine/node-execution-context/utils/get-secrets-proxy.ts +++ b/packages/core/src/execution-engine/node-execution-context/utils/get-secrets-proxy.ts @@ -23,7 +23,7 @@ function buildSecretsValueProxy(value: IDataObject): unknown { } export function getSecretsProxy(additionalData: IWorkflowExecuteAdditionalData): IDataObject { - const secretsHelpers = additionalData.secretsHelpers; + const { externalSecretsProxy } = additionalData; return new Proxy( {}, { @@ -31,7 +31,7 @@ export function getSecretsProxy(additionalData: IWorkflowExecuteAdditionalData): if (typeof providerName !== 'string') { return {}; } - if (secretsHelpers.hasProvider(providerName)) { + if (externalSecretsProxy.hasProvider(providerName)) { return new Proxy( {}, { @@ -39,13 +39,13 @@ export function getSecretsProxy(additionalData: IWorkflowExecuteAdditionalData): if (typeof secretName !== 'string') { return; } - if (!secretsHelpers.hasSecret(providerName, secretName)) { + if (!externalSecretsProxy.hasSecret(providerName, secretName)) { throw new ExpressionError('Could not load secrets', { description: 'The credential in use tries to use secret from an external store that could not be found', }); } - const retValue = secretsHelpers.getSecret(providerName, secretName); + const retValue = externalSecretsProxy.getSecret(providerName, secretName); if (typeof retValue === 'object' && retValue !== null) { return buildSecretsValueProxy(retValue as IDataObject); } @@ -55,7 +55,7 @@ export function getSecretsProxy(additionalData: IWorkflowExecuteAdditionalData): return false; }, ownKeys() { - return secretsHelpers.listSecrets(providerName); + return externalSecretsProxy.listSecrets(providerName); }, }, ); @@ -69,7 +69,7 @@ export function getSecretsProxy(additionalData: IWorkflowExecuteAdditionalData): return false; }, ownKeys() { - return secretsHelpers.listProviders(); + return externalSecretsProxy.listProviders(); }, }, ); diff --git a/packages/workflow/src/interfaces.ts b/packages/workflow/src/interfaces.ts index 9832586d46..0cd7cd6f43 100644 --- a/packages/workflow/src/interfaces.ts +++ b/packages/workflow/src/interfaces.ts @@ -2374,7 +2374,6 @@ export interface IWorkflowExecuteAdditionalData { executionTimeoutTimestamp?: number; userId?: string; variables: IDataObject; - secretsHelpers: SecretsHelpersBase; logAiEvent: (eventName: AiEvent, payload: AiEventPayload) => void; parentCallbackManager?: CallbackManager; startRunnerTask( @@ -2870,17 +2869,6 @@ export interface ICheckProcessedContextData { export type N8nAIProviderType = 'openai' | 'unknown'; -export interface SecretsHelpersBase { - update(): Promise; - waitForInit(): Promise; - - getSecret(provider: string, name: string): unknown; - hasSecret(provider: string, name: string): boolean; - hasProvider(provider: string): boolean; - listProviders(): string[]; - listSecrets(provider: string): string[]; -} - export type Functionality = 'regular' | 'configuration-node' | 'pairedItem'; export type CallbackManager = CallbackManagerLC;