From ca3069f52886a5c92cfadbe36d6ee6083bafbb56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Castro?= Date: Thu, 27 Aug 2026 08:46:38 +0000 Subject: [PATCH] feat(core): Wire credentialDecrypt host point (no-changelog) (#36962) Co-authored-by: Claude Sonnet 5 --- .../src/__tests__/credentials-helper.test.ts | 149 ++++++++++++++++++ .../workflow-execute-additional-data.test.ts | 50 +++++- .../src/__tests__/workflow-helpers.test.ts | 17 +- packages/cli/src/credentials-helper.ts | 11 ++ .../src/workflow-execute-additional-data.ts | 12 ++ .../__tests__/hook-context.test.ts | 17 ++ .../__tests__/load-options-context.test.ts | 2 +- .../__tests__/poll-context.test.ts | 18 +++ .../__tests__/trigger-context.test.ts | 18 +++ .../__tests__/webhook-context.test.ts | 18 +++ .../node-execution-context/hook-context.ts | 8 +- .../load-options-context.ts | 8 +- .../node-execution-context/poll-context.ts | 7 +- .../node-execution-context/trigger-context.ts | 7 +- .../node-execution-context/webhook-context.ts | 6 +- 15 files changed, 335 insertions(+), 13 deletions(-) diff --git a/packages/cli/src/__tests__/credentials-helper.test.ts b/packages/cli/src/__tests__/credentials-helper.test.ts index 4cd2f9cd07d..bdaab556a89 100644 --- a/packages/cli/src/__tests__/credentials-helper.test.ts +++ b/packages/cli/src/__tests__/credentials-helper.test.ts @@ -19,6 +19,7 @@ import type { IAuthenticateGeneric, ICredentialDataDecryptedObject, ICredentialType, + IExecuteData, IHttpRequestHelper, IHttpRequestOptions, INode, @@ -50,6 +51,7 @@ import type { CredentialsOverwrites } from '@/credentials-overwrites'; import { CredentialNotFoundError } from '@/errors/credential-not-found.error'; import type { LoadNodesAndCredentials } from '@/load-nodes-and-credentials'; import type { ExternalSecretsConfig } from '@/modules/external-secrets.ee/external-secrets.config'; +import type { PolicyEnforcementService } from '@/policy/policy-enforcement.service'; import type { AiGatewayService } from '@/services/ai-gateway.service'; describe('CredentialsHelper', () => { @@ -60,6 +62,7 @@ describe('CredentialsHelper', () => { const licenseState = mock(); const externalSecretsConfig = mock(); const mockLogger = mock(); + const policyEnforcementService = mock(); // Use a real instance of DynamicCredentialsProxy so setResolverProvider works const dynamicCredentialProxy = new DynamicCredentialsProxy(mockLogger); @@ -81,6 +84,7 @@ describe('CredentialsHelper', () => { licenseState, externalSecretsConfig, mock(), + policyEnforcementService, ); describe('getCredentials', () => { @@ -179,6 +183,7 @@ describe('CredentialsHelper', () => { licenseState, externalSecretsConfig, mock(), + policyEnforcementService, ); const result = await helper.applyDefaultsAndOverwrites( @@ -232,6 +237,7 @@ describe('CredentialsHelper', () => { licenseState, externalSecretsConfig, mock(), + policyEnforcementService, ); await expect( @@ -275,6 +281,7 @@ describe('CredentialsHelper', () => { licenseState, externalSecretsConfig, mock(), + policyEnforcementService, ); const externalSecretsProxy = mock>(); @@ -354,6 +361,7 @@ describe('CredentialsHelper', () => { licenseState, externalSecretsConfig, mock(), + policyEnforcementService, ); // Region left at its default: neither `region` nor `url` was persisted. @@ -413,6 +421,7 @@ describe('CredentialsHelper', () => { licenseState, externalSecretsConfig, mock(), + policyEnforcementService, ); const result = await helper.applyDefaultsAndOverwrites( @@ -455,6 +464,7 @@ describe('CredentialsHelper', () => { licenseState, externalSecretsConfig, mock(), + policyEnforcementService, ); const registerType = (credentialType: ICredentialType) => @@ -1243,6 +1253,7 @@ describe('CredentialsHelper', () => { licenseState, externalSecretsConfig, aiGatewayService, + policyEnforcementService, ); const syntheticCred = { apiKey: 'mock-jwt', host: 'http://gateway/v1/gateway/google' }; @@ -1288,6 +1299,7 @@ describe('CredentialsHelper', () => { licenseState, externalSecretsConfig, aiGatewayService, + policyEnforcementService, ); const syntheticCred = { apiKey: 'mock-jwt', host: 'http://gateway/v1/gateway/google' }; @@ -1335,6 +1347,7 @@ describe('CredentialsHelper', () => { licenseState, externalSecretsConfig, aiGatewayService, + policyEnforcementService, ); const syntheticCred = { @@ -1570,6 +1583,7 @@ describe('CredentialsHelper', () => { licenseState, externalSecretsConfig, mock(), + policyEnforcementService, ); const result = await helperWithoutProvider.getDecrypted( @@ -1830,6 +1844,7 @@ describe('CredentialsHelper', () => { mock(), mock(), mock(), + policyEnforcementService, ); // The loader sets the class's `supportedNodes` to short names (e.g. "restrictedConsumer"); @@ -2384,4 +2399,138 @@ describe('CredentialsHelper', () => { expect(result).toMatchObject({ accessToken: 'NEW_TOKEN' }); }); }); + + describe('getDecrypted - credentialDecrypt policy enforcement', () => { + const nodeCredentials: INodeCredentialsDetails = { + id: 'cred-policy', + name: 'Policy Test Credential', + }; + + const credentialEntity = { + id: 'cred-policy', + name: 'Policy Test Credential', + type: 'testApi', + data: cipher.encrypt({ apiKey: 'test' }), + isResolvable: false, + usageScope: 'project', + } as CredentialsEntity; + + let helper: CredentialsHelper; + + beforeEach(() => { + vi.clearAllMocks(); + credentialsRepository.findOneByOrFail.mockResolvedValue(credentialEntity); + policyEnforcementService.enforceCredentialDecrypt.mockResolvedValue(mock()); + helper = new CredentialsHelper( + new CredentialTypes(mockNodesAndCredentials), + mock(), + credentialsRepository, + dynamicCredentialProxy, + secretsProviderRepository, + licenseState, + externalSecretsConfig, + mock(), + policyEnforcementService, + ); + }); + + test('calls enforceCredentialDecrypt with the credential, consumer and project context', async () => { + const executeData = { + node: { + name: 'Slack1', + type: 'n8n-nodes-base.slack', + typeVersion: 1, + position: [0, 0], + parameters: {}, + }, + data: {}, + source: null, + } as IExecuteData; + + const additionalData = mock({ projectId: 'proj-1' }); + + await helper.getDecrypted( + additionalData, + nodeCredentials, + 'testApi', + 'manual', + executeData, + true, + ); + + expect(policyEnforcementService.enforceCredentialDecrypt).toHaveBeenCalledExactlyOnceWith({ + credentialType: 'testApi', + credentialId: 'cred-policy', + consumer: { nodeType: 'n8n-nodes-base.slack' }, + projectId: 'proj-1', + }); + }); + + test('passes a null consumer when no node is asking, e.g. a credential test', async () => { + const additionalData = mock({ projectId: undefined }); + + await helper.getDecrypted( + additionalData, + nodeCredentials, + 'testApi', + 'manual', + undefined, + true, + ); + + expect(policyEnforcementService.enforceCredentialDecrypt).toHaveBeenCalledExactlyOnceWith( + expect.objectContaining({ consumer: null, projectId: null }), + ); + }); + + test('resolves the credential before enforcing the policy check', async () => { + const callOrder: string[] = []; + credentialsRepository.findOneByOrFail.mockImplementation(async () => { + callOrder.push('findOneByOrFail'); + return credentialEntity; + }); + policyEnforcementService.enforceCredentialDecrypt.mockImplementation(async () => { + callOrder.push('enforceCredentialDecrypt'); + return await mock(); + }); + + await helper.getDecrypted( + mock(), + nodeCredentials, + 'testApi', + 'manual', + undefined, + true, + ); + + expect(callOrder).toEqual(['findOneByOrFail', 'enforceCredentialDecrypt']); + }); + + test('blocks decryption when the policy check throws', async () => { + const violation = new Error('blocked by policy'); + policyEnforcementService.enforceCredentialDecrypt.mockRejectedValueOnce(violation); + + await expect( + helper.getDecrypted( + mock(), + nodeCredentials, + 'testApi', + 'manual', + ), + ).rejects.toThrow(violation); + }); + + test('decryption behavior is unchanged when the policy check clears', async () => { + const result = await helper.getDecrypted( + mock(), + nodeCredentials, + 'testApi', + 'manual', + undefined, + true, + ); + + expect(result).toEqual({ apiKey: 'test' }); + }); + }); }); 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 9a1781087fb..ebb431cd9af 100644 --- a/packages/cli/src/__tests__/workflow-execute-additional-data.test.ts +++ b/packages/cli/src/__tests__/workflow-execute-additional-data.test.ts @@ -271,7 +271,9 @@ describe('WorkflowExecuteAdditionalData', () => { mock({ loadedWorkflowData: undefined, doNotWaitToFinish: false }), ); - expect(getVariablesSpy).toHaveBeenCalledWith(workflowId, undefined); + // getBase backfills projectId from the workflow owner (mocked to + // 'project-id-1' in this describe's beforeEach) before calling getVariables. + expect(getVariablesSpy).toHaveBeenCalledWith(workflowId, 'project-id-1'); }); describe('credential permission check routing', () => { @@ -1330,6 +1332,52 @@ describe('WorkflowExecuteAdditionalData', () => { expect(additionalData.workflowSettings).toBe(workflowSettings); }); + + describe('projectId resolution', () => { + const ownershipService = mockInstance(OwnershipService); + + beforeEach(() => { + ownershipService.getWorkflowProjectCached.mockReset(); + // Both this and the executeWorkflow/executeAgent describes call + // mockInstance(OwnershipService), which each Container.set a fresh mock. + // Re-bind ours so the source resolves it. + Container.set(OwnershipService, ownershipService); + }); + + it('backfills projectId from the workflow owner when missing', async () => { + ownershipService.getWorkflowProjectCached.mockResolvedValue( + mock({ id: 'owning-project-1' }), + ); + + const additionalData = await getBase({ workflowId: 'workflow-1' }); + + expect(ownershipService.getWorkflowProjectCached).toHaveBeenCalledWith('workflow-1'); + expect(additionalData.projectId).toBe('owning-project-1'); + }); + + it('keeps the given projectId untouched when already present', async () => { + const additionalData = await getBase({ + workflowId: 'workflow-1', + projectId: 'given-project', + }); + + expect(ownershipService.getWorkflowProjectCached).not.toHaveBeenCalled(); + expect(additionalData.projectId).toBe('given-project'); + }); + + it('leaves projectId unset when workflowId is missing', async () => { + const additionalData = await getBase(); + + expect(ownershipService.getWorkflowProjectCached).not.toHaveBeenCalled(); + expect(additionalData.projectId).toBeUndefined(); + }); + + it('rejects when the workflow has no resolvable owning project', async () => { + ownershipService.getWorkflowProjectCached.mockRejectedValue(new Error('not found')); + + await expect(getBase({ workflowId: 'workflow-1' })).rejects.toThrow('not found'); + }); + }); }); describe('executeAgent', () => { diff --git a/packages/cli/src/__tests__/workflow-helpers.test.ts b/packages/cli/src/__tests__/workflow-helpers.test.ts index 24eb55e777b..2e29118e60f 100644 --- a/packages/cli/src/__tests__/workflow-helpers.test.ts +++ b/packages/cli/src/__tests__/workflow-helpers.test.ts @@ -35,6 +35,11 @@ import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { mock } from 'vitest-mock-extended'; describe('workflow-helpers', () => { + const ownershipService = mockInstance(OwnershipService); + ownershipService.getWorkflowProjectCached.mockResolvedValue( + mock({ id: '1', name: 'project' }), + ); + beforeAll(() => { mockInstance(VariablesService, { async getAllCached() { @@ -64,12 +69,6 @@ describe('workflow-helpers', () => { ] as Variables[]; }, }); - - mockInstance(OwnershipService, { - async getWorkflowProjectCached(_workflowId: string) { - return { id: '1', name: 'project' } as unknown as Project; - }, - }); }); describe('getVariables', () => { @@ -97,6 +96,12 @@ describe('workflow-helpers', () => { const variables = await getVariables(undefined, '1'); expect(variables.VAR2).toBe('value1Project'); }); + + it('should reject when the owning project cannot be resolved', async () => { + ownershipService.getWorkflowProjectCached.mockRejectedValueOnce(new Error('not found')); + + await expect(getVariables('1')).rejects.toThrow('not found'); + }); }); }); diff --git a/packages/cli/src/credentials-helper.ts b/packages/cli/src/credentials-helper.ts index a6de29a0041..aa893790728 100644 --- a/packages/cli/src/credentials-helper.ts +++ b/packages/cli/src/credentials-helper.ts @@ -46,6 +46,7 @@ import { MANAGED_OAUTH_PINNED_FIELDS, } from '@/oauth/dcr-managed-fields'; import { ExternalSecretsConfig } from '@/modules/external-secrets.ee/external-secrets.config'; +import { PolicyEnforcementService } from '@/policy/policy-enforcement.service'; import { AiGatewayService } from '@/services/ai-gateway.service'; import { RESPONSE_ERROR_MESSAGES } from './constants'; @@ -100,6 +101,7 @@ export class CredentialsHelper extends ICredentialsHelper { private readonly licenseState: LicenseState, private readonly externalSecretsConfig: ExternalSecretsConfig, private readonly aiGatewayService: AiGatewayService, + private readonly policyEnforcementService: PolicyEnforcementService, ) { super(); } @@ -527,6 +529,15 @@ export class CredentialsHelper extends ICredentialsHelper { } const credentialsEntity = await this.getCredentialsEntity(nodeCredentials, type); + + // Validate against the executing project's policy before any decryption happens. + await this.policyEnforcementService.enforceCredentialDecrypt({ + credentialType: type, + credentialId: credentialsEntity.id, + consumer: executeData ? { nodeType: executeData.node.type } : null, + projectId: additionalData.projectId ?? null, + }); + const credentials = new Credentials( { id: credentialsEntity.id, name: credentialsEntity.name }, credentialsEntity.type, diff --git a/packages/cli/src/workflow-execute-additional-data.ts b/packages/cli/src/workflow-execute-additional-data.ts index 0d56df4dc61..8c3e11f8208 100644 --- a/packages/cli/src/workflow-execute-additional-data.ts +++ b/packages/cli/src/workflow-execute-additional-data.ts @@ -789,6 +789,18 @@ export async function getBase({ const globalConfig = Container.get(GlobalConfig); + // Trigger-fired, webhook, and worker-queued executions build additionalData without + // a `projectId`. Resolve it from the workflow's owning project so every downstream + // consumer (e.g. policy enforcement) sees the executing project, same as + // `executeAgent` already does locally for its own use. Left unguarded on purpose, + // matching `getVariables`'s own pre-existing lookup below: an unresolvable owner + // project fails execution setup, it isn't silently tolerated. + if (!projectId && workflowId) { + const { OwnershipService } = await import('@/services/ownership.service.js'); + const project = await Container.get(OwnershipService).getWorkflowProjectCached(workflowId); + projectId = project?.id; + } + const variables = await WorkflowHelpers.getVariables(workflowId, projectId); const eventService = Container.get(EventService); diff --git a/packages/core/src/execution-engine/node-execution-context/__tests__/hook-context.test.ts b/packages/core/src/execution-engine/node-execution-context/__tests__/hook-context.test.ts index c5ad74794de..211582ea0ad 100644 --- a/packages/core/src/execution-engine/node-execution-context/__tests__/hook-context.test.ts +++ b/packages/core/src/execution-engine/node-execution-context/__tests__/hook-context.test.ts @@ -100,6 +100,23 @@ describe('HookContext', () => { expect(credentials).toEqual({ secret: 'token' }); }); + + it('should surface the node to the credentials helper', async () => { + credentialsHelper.getDecrypted.mockResolvedValue({ secret: 'token' }); + credentialsHelper.isCredentialUsableByNode.mockReturnValue(true); + + await hookContext.getCredentials(testCredentialType); + + expect(credentialsHelper.getDecrypted).toHaveBeenCalledWith( + additionalData, + expect.anything(), + testCredentialType, + mode, + expect.objectContaining({ node }), + false, + undefined, + ); + }); }); describe('getNodeParameter', () => { diff --git a/packages/core/src/execution-engine/node-execution-context/__tests__/load-options-context.test.ts b/packages/core/src/execution-engine/node-execution-context/__tests__/load-options-context.test.ts index d5f2231bd38..575e7ff4785 100644 --- a/packages/core/src/execution-engine/node-execution-context/__tests__/load-options-context.test.ts +++ b/packages/core/src/execution-engine/node-execution-context/__tests__/load-options-context.test.ts @@ -95,7 +95,7 @@ describe('LoadOptionsContext', () => { expect.anything(), testCredentialType, 'internal', - undefined, + expect.objectContaining({ node }), false, undefined, ); diff --git a/packages/core/src/execution-engine/node-execution-context/__tests__/poll-context.test.ts b/packages/core/src/execution-engine/node-execution-context/__tests__/poll-context.test.ts index 9e8187bf722..4f7be8158ec 100644 --- a/packages/core/src/execution-engine/node-execution-context/__tests__/poll-context.test.ts +++ b/packages/core/src/execution-engine/node-execution-context/__tests__/poll-context.test.ts @@ -76,6 +76,24 @@ describe('PollContext', () => { expect(credentials).toEqual({ secret: 'token' }); }); + + it('should surface the node to the credentials helper', async () => { + nodeTypes.getByNameAndVersion.mockReturnValue(nodeType); + credentialsHelper.getDecrypted.mockResolvedValue({ secret: 'token' }); + credentialsHelper.isCredentialUsableByNode.mockReturnValue(true); + + await pollContext.getCredentials(testCredentialType); + + expect(credentialsHelper.getDecrypted).toHaveBeenCalledWith( + additionalData, + expect.anything(), + testCredentialType, + mode, + expect.objectContaining({ node }), + false, + undefined, + ); + }); }); describe('getNodeParameter', () => { diff --git a/packages/core/src/execution-engine/node-execution-context/__tests__/trigger-context.test.ts b/packages/core/src/execution-engine/node-execution-context/__tests__/trigger-context.test.ts index f23290d6a8f..5a70bdcf62f 100644 --- a/packages/core/src/execution-engine/node-execution-context/__tests__/trigger-context.test.ts +++ b/packages/core/src/execution-engine/node-execution-context/__tests__/trigger-context.test.ts @@ -76,6 +76,24 @@ describe('TriggerContext', () => { expect(credentials).toEqual({ secret: 'token' }); }); + + it('should surface the node to the credentials helper', async () => { + nodeTypes.getByNameAndVersion.mockReturnValue(nodeType); + credentialsHelper.getDecrypted.mockResolvedValue({ secret: 'token' }); + credentialsHelper.isCredentialUsableByNode.mockReturnValue(true); + + await triggerContext.getCredentials(testCredentialType); + + expect(credentialsHelper.getDecrypted).toHaveBeenCalledWith( + additionalData, + expect.anything(), + testCredentialType, + mode, + expect.objectContaining({ node }), + false, + undefined, + ); + }); }); describe('getNodeParameter', () => { diff --git a/packages/core/src/execution-engine/node-execution-context/__tests__/webhook-context.test.ts b/packages/core/src/execution-engine/node-execution-context/__tests__/webhook-context.test.ts index 140cf5adbbb..065bc91b10d 100644 --- a/packages/core/src/execution-engine/node-execution-context/__tests__/webhook-context.test.ts +++ b/packages/core/src/execution-engine/node-execution-context/__tests__/webhook-context.test.ts @@ -140,6 +140,24 @@ describe('WebhookContext', () => { expect(credentials).toEqual({ secret: 'token' }); }); + + it('should surface the node to the credentials helper', async () => { + nodeTypes.getByNameAndVersion.mockReturnValue(nodeType); + credentialsHelper.getDecrypted.mockResolvedValue({ secret: 'token' }); + credentialsHelper.isCredentialUsableByNode.mockReturnValue(true); + + await webhookContext.getCredentials(testCredentialType); + + expect(credentialsHelper.getDecrypted).toHaveBeenCalledWith( + additionalData, + expect.anything(), + testCredentialType, + mode, + expect.objectContaining({ node }), + false, + undefined, + ); + }); }); describe('getBodyData', () => { diff --git a/packages/core/src/execution-engine/node-execution-context/hook-context.ts b/packages/core/src/execution-engine/node-execution-context/hook-context.ts index 40bc5c12dee..6b769559a6f 100644 --- a/packages/core/src/execution-engine/node-execution-context/hook-context.ts +++ b/packages/core/src/execution-engine/node-execution-context/hook-context.ts @@ -1,6 +1,7 @@ import { UnexpectedError } from 'n8n-workflow'; import type { ICredentialDataDecryptedObject, + IExecuteData, INode, IHookFunctions, IWorkflowExecuteAdditionalData, @@ -36,7 +37,12 @@ export class HookContext extends NodeExecutionContext implements IHookFunctions } async getCredentials(type: string) { - return await this._getCredentials(type); + // No real task run backs a webhook-registration hook, so this only exists to + // surface `node` to the credentials helper (e.g. for policy checks) — `data`/ + // `source` are unused. + const executeData: IExecuteData = { data: {}, node: this.node, source: null }; + + return await this._getCredentials(type, executeData); } getNodeWebhookUrl(name: WebhookType): string | undefined { diff --git a/packages/core/src/execution-engine/node-execution-context/load-options-context.ts b/packages/core/src/execution-engine/node-execution-context/load-options-context.ts index ca3b4a726f0..9a33534f1f3 100644 --- a/packages/core/src/execution-engine/node-execution-context/load-options-context.ts +++ b/packages/core/src/execution-engine/node-execution-context/load-options-context.ts @@ -1,6 +1,7 @@ import get from 'lodash/get'; import type { ICredentialDataDecryptedObject, + IExecuteData, IGetNodeParameterOptions, INode, ILoadOptionsFunctions, @@ -44,7 +45,12 @@ export class LoadOptionsContext extends NodeExecutionContext implements ILoadOpt } async getCredentials(type: string) { - return await this._getCredentials(type); + // No real task run backs design-time parameter loading, so this only exists to + // surface `node` to the credentials helper (e.g. for policy checks) — `data`/`source` + // are unused. + const executeData: IExecuteData = { data: {}, node: this.node, source: null }; + + return await this._getCredentials(type, executeData); } getCurrentNodeParameter( diff --git a/packages/core/src/execution-engine/node-execution-context/poll-context.ts b/packages/core/src/execution-engine/node-execution-context/poll-context.ts index 2b53a588437..3da7fe95f2b 100644 --- a/packages/core/src/execution-engine/node-execution-context/poll-context.ts +++ b/packages/core/src/execution-engine/node-execution-context/poll-context.ts @@ -2,6 +2,7 @@ import { createDeferredPromise } from '@n8n/utils/promise/deferred-promise'; import type { ICredentialDataDecryptedObject, IDataObject, + IExecuteData, INode, IPollFunctions, IWorkflowExecuteAdditionalData, @@ -66,6 +67,10 @@ export class PollContext extends NodeExecutionContext implements IPollFunctions } async getCredentials(type: string) { - return await this._getCredentials(type); + // No real task run backs a poll, so this only exists to surface `node` to + // the credentials helper (e.g. for policy checks) — `data`/`source` are unused. + const executeData: IExecuteData = { data: {}, node: this.node, source: null }; + + return await this._getCredentials(type, executeData); } } diff --git a/packages/core/src/execution-engine/node-execution-context/trigger-context.ts b/packages/core/src/execution-engine/node-execution-context/trigger-context.ts index 010290d928c..d6cd0783c8e 100644 --- a/packages/core/src/execution-engine/node-execution-context/trigger-context.ts +++ b/packages/core/src/execution-engine/node-execution-context/trigger-context.ts @@ -1,6 +1,7 @@ import { createDeferredPromise } from '@n8n/utils/promise/deferred-promise'; import type { ICredentialDataDecryptedObject, + IExecuteData, INode, ITriggerFunctions, IWorkflowExecuteAdditionalData, @@ -65,6 +66,10 @@ export class TriggerContext extends NodeExecutionContext implements ITriggerFunc } async getCredentials(type: string) { - return await this._getCredentials(type); + // No real task run backs a trigger, so this only exists to surface `node` to + // the credentials helper (e.g. for policy checks) — `data`/`source` are unused. + const executeData: IExecuteData = { data: {}, node: this.node, source: null }; + + return await this._getCredentials(type, executeData); } } diff --git a/packages/core/src/execution-engine/node-execution-context/webhook-context.ts b/packages/core/src/execution-engine/node-execution-context/webhook-context.ts index a5fc2e11c6f..6202becbe24 100644 --- a/packages/core/src/execution-engine/node-execution-context/webhook-context.ts +++ b/packages/core/src/execution-engine/node-execution-context/webhook-context.ts @@ -99,7 +99,11 @@ export class WebhookContext extends NodeExecutionContext implements IWebhookFunc } async getCredentials(type: string) { - return await this._getCredentials(type); + // No real task run backs a webhook call, so this only exists to surface `node` + // to the credentials helper (e.g. for policy checks) — `data`/`source` are unused. + const executeData: IExecuteData = { data: {}, node: this.node, source: null }; + + return await this._getCredentials(type, executeData); } getBodyData() {