import { Logger } from '@n8n/backend-common'; import { Service } from '@n8n/di'; import { IExecuteData, IExecutionContext, INodeExecutionData, PlaintextExecutionContext, toCredentialContext, toExecutionContextEstablishmentHookParameter, Workflow, } from 'n8n-workflow'; import { Cipher } from '@/encryption'; import { deepMerge } from '@/utils/deep-merge'; import { ExecutionContextHookRegistry } from './execution-context-hook-registry.service'; @Service() export class ExecutionContextService { constructor( private readonly logger: Logger, private readonly executionContextHookRegistry: ExecutionContextHookRegistry, private readonly cipher: Cipher, ) {} decryptExecutionContext(context: IExecutionContext): PlaintextExecutionContext { let credentials = undefined; if (context.credentials) { const decrypted = this.cipher.decrypt(context.credentials); credentials = toCredentialContext(decrypted); } return { ...context, credentials, }; } encryptExecutionContext(context: PlaintextExecutionContext): IExecutionContext { let credentials = undefined; if (context.credentials) { credentials = this.cipher.encrypt(context.credentials); } return { ...context, credentials, }; } mergeExecutionContexts( baseContext: PlaintextExecutionContext, contextToMerge: Partial, ): PlaintextExecutionContext { return deepMerge(baseContext, contextToMerge); } // startItem is mutated to reflect any changes to trigger items made by the hooks async augmentExecutionContextWithHooks( workflow: Workflow, startItem: IExecuteData, contextToAugment: IExecutionContext, ): Promise<{ context: IExecutionContext; triggerItems: INodeExecutionData[] | null; }> { // Main input data is an array of items, each item represents an event that triggers the workflow execution // The 'main' selector selects the input name of the nodes, and the 0 index represents the runIndex, // 0 being the first run of this node in the workflow. let currentTriggerItems = startItem.data['main'][0]; const contextEstablishmentHookParameters = { ...(workflow.getNode(startItem.node.name)?.parameters ?? {}), ...startItem.node.parameters, }; const startNodeParametersResult = toExecutionContextEstablishmentHookParameter( contextEstablishmentHookParameters, ); if (!startNodeParametersResult || startNodeParametersResult.error) { if (startNodeParametersResult?.error) { this.logger.warn( `Failed to parse execution context establishment hook parameters for node ${startItem.node.name}: ${startNodeParametersResult.error.message}`, ); } // no execution establishment hooks found, we just return the original context return { context: contextToAugment, triggerItems: currentTriggerItems, }; } // startNodeParameters will hold the parameters of the start node // this can be the settings for the different hooks to be executed // for example to extract the bearer token from the start node data. const startNodeParameters = startNodeParametersResult.data; // decrypt the context to work with plaintext data let context = this.decryptExecutionContext(contextToAugment); // based on startNodeParameters, startNodeType and currentTriggerItems we can now // iterate over the different hooks to extract specific data for the runtime context for (const hookParameters of startNodeParameters.contextEstablishmentHooks.hooks) { const hook = this.executionContextHookRegistry.getHookByName(hookParameters.hookName); if (!hook) { this.logger.warn( `Execution context establishment hook ${hookParameters.hookName} not found, skipping this hook`, ); continue; } try { // call the hook to let it modify the context and/or the main input data const result = await hook.execute({ triggerNode: startItem.node, workflow, triggerItems: currentTriggerItems, context, options: hookParameters, }); if (result.triggerItems !== undefined) { // Update trigger items in case they were modified by the hook currentTriggerItems = result.triggerItems; } if (result.contextUpdate) { // Merge any returned context fields into the execution context context = this.mergeExecutionContexts(context, result.contextUpdate); } } catch (error) { this.logger.warn( `Failed to execute context establishment hook ${hookParameters.hookName}`, { error }, ); if (!hookParameters.isAllowedToFail) { // If the hook is not allowed to fail, rethrow the error throw error; } } } return { context: this.encryptExecutionContext(context), triggerItems: currentTriggerItems, }; } }