mirror of
https://github.com/n8n-io/n8n.git
synced 2025-11-20 17:46:34 +00:00
chore(core): Interface definition for context establishment hooks (#22073)
This commit is contained in:
parent
4232093bb9
commit
dcea7a9d5f
@ -12,6 +12,7 @@
|
||||
"lint:fix": "eslint . --fix",
|
||||
"watch": "tsc -p tsconfig.build.json --watch",
|
||||
"test": "jest",
|
||||
"test:unit": "jest",
|
||||
"test:dev": "jest --watch"
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
|
||||
@ -0,0 +1,148 @@
|
||||
import { Container } from '@n8n/di';
|
||||
|
||||
import type {
|
||||
ContextEstablishmentOptions,
|
||||
ContextEstablishmentResult,
|
||||
IContextEstablishmentHook,
|
||||
} from '../context-establishment-hook';
|
||||
import {
|
||||
ContextEstablishmentHookMetadata,
|
||||
ContextEstablishmentHook,
|
||||
} from '../context-establishment-hook-metadata';
|
||||
|
||||
describe('@ContextEstablishmentHook decorator', () => {
|
||||
let hookMetadata: ContextEstablishmentHookMetadata;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
hookMetadata = new ContextEstablishmentHookMetadata();
|
||||
Container.set(ContextEstablishmentHookMetadata, hookMetadata);
|
||||
});
|
||||
|
||||
it('should register hook in ContextEstablishmentHookMetadata', () => {
|
||||
@ContextEstablishmentHook()
|
||||
class TestHook implements IContextEstablishmentHook {
|
||||
hookDescription = { name: 'test.hook' };
|
||||
async execute(_options: ContextEstablishmentOptions): Promise<ContextEstablishmentResult> {
|
||||
return {};
|
||||
}
|
||||
isApplicableToTriggerNode(_nodeType: string): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const registeredHooks = hookMetadata.getClasses();
|
||||
|
||||
expect(registeredHooks).toContain(TestHook);
|
||||
expect(registeredHooks).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should register multiple hooks', () => {
|
||||
@ContextEstablishmentHook()
|
||||
class FirstHook implements IContextEstablishmentHook {
|
||||
hookDescription = { name: 'first.hook' };
|
||||
async execute(_options: ContextEstablishmentOptions): Promise<ContextEstablishmentResult> {
|
||||
return {};
|
||||
}
|
||||
isApplicableToTriggerNode(_nodeType: string): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@ContextEstablishmentHook()
|
||||
class SecondHook implements IContextEstablishmentHook {
|
||||
hookDescription = { name: 'second.hook' };
|
||||
async execute(_options: ContextEstablishmentOptions): Promise<ContextEstablishmentResult> {
|
||||
return {};
|
||||
}
|
||||
isApplicableToTriggerNode(_nodeType: string): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@ContextEstablishmentHook()
|
||||
class ThirdHook implements IContextEstablishmentHook {
|
||||
hookDescription = { name: 'third.hook' };
|
||||
async execute(_options: ContextEstablishmentOptions): Promise<ContextEstablishmentResult> {
|
||||
return {};
|
||||
}
|
||||
isApplicableToTriggerNode(_nodeType: string): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const registeredHooks = hookMetadata.getClasses();
|
||||
|
||||
expect(registeredHooks).toContain(FirstHook);
|
||||
expect(registeredHooks).toContain(SecondHook);
|
||||
expect(registeredHooks).toContain(ThirdHook);
|
||||
expect(registeredHooks).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should apply Service decorator', () => {
|
||||
@ContextEstablishmentHook()
|
||||
class TestHook implements IContextEstablishmentHook {
|
||||
hookDescription = { name: 'test.hook' };
|
||||
async execute(_options: ContextEstablishmentOptions): Promise<ContextEstablishmentResult> {
|
||||
return {};
|
||||
}
|
||||
isApplicableToTriggerNode(_nodeType: string): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
expect(Container.has(TestHook)).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow instantiation of registered hooks with accessible hookDescription', () => {
|
||||
@ContextEstablishmentHook()
|
||||
class TestHook implements IContextEstablishmentHook {
|
||||
hookDescription = { name: 'credentials.bearerToken' };
|
||||
async execute(_options: ContextEstablishmentOptions): Promise<ContextEstablishmentResult> {
|
||||
return {};
|
||||
}
|
||||
isApplicableToTriggerNode(_nodeType: string): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const hookInstance = Container.get(TestHook);
|
||||
|
||||
expect(hookInstance).toBeInstanceOf(TestHook);
|
||||
expect(hookInstance.hookDescription).toEqual({ name: 'credentials.bearerToken' });
|
||||
expect(hookInstance.hookDescription.name).toBe('credentials.bearerToken');
|
||||
});
|
||||
|
||||
it('should register hooks with different description names', () => {
|
||||
@ContextEstablishmentHook()
|
||||
class BearerTokenHook implements IContextEstablishmentHook {
|
||||
hookDescription = { name: 'credentials.bearerToken' };
|
||||
async execute(_options: ContextEstablishmentOptions): Promise<ContextEstablishmentResult> {
|
||||
return {};
|
||||
}
|
||||
isApplicableToTriggerNode(_nodeType: string): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@ContextEstablishmentHook()
|
||||
class ApiKeyHook implements IContextEstablishmentHook {
|
||||
hookDescription = { name: 'credentials.apiKey' };
|
||||
async execute(_options: ContextEstablishmentOptions): Promise<ContextEstablishmentResult> {
|
||||
return {};
|
||||
}
|
||||
isApplicableToTriggerNode(_nodeType: string): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const registeredHooks = hookMetadata.getClasses();
|
||||
const bearerTokenHook = Container.get(BearerTokenHook);
|
||||
const apiKeyHook = Container.get(ApiKeyHook);
|
||||
|
||||
expect(registeredHooks).toHaveLength(2);
|
||||
expect(bearerTokenHook.hookDescription.name).toBe('credentials.bearerToken');
|
||||
expect(apiKeyHook.hookDescription.name).toBe('credentials.apiKey');
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,200 @@
|
||||
import { Container, Service } from '@n8n/di';
|
||||
|
||||
import { ContextEstablishmentHookClass } from './context-establishment-hook';
|
||||
|
||||
/**
|
||||
* Registry entry for a context establishment hook.
|
||||
*
|
||||
* This is a lightweight wrapper around the hook class constructor that can be
|
||||
* extended in the future to include additional metadata if needed (e.g., module
|
||||
* source, registration timestamp, feature flags, license flags).
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
type ContextEstablishmentHookEntry = {
|
||||
/** The hook class constructor for DI container instantiation */
|
||||
class: ContextEstablishmentHookClass;
|
||||
};
|
||||
|
||||
/**
|
||||
* Low-level metadata registry for context establishment hooks.
|
||||
*
|
||||
* This service acts as a simple collection of registered hook classes that gets
|
||||
* populated automatically by the @ContextEstablishmentHook decorator at module
|
||||
* load time. It serves as the foundation for the higher-level Hook Registry.
|
||||
*
|
||||
* **Architecture:**
|
||||
* ```
|
||||
* Decorator → ContextEstablishmentHookMetadata → Hook Registry → Execution Engine
|
||||
* (registration) (collection) (discovery) (execution)
|
||||
* ```
|
||||
*
|
||||
* @see ContextEstablishmentHook decorator for automatic registration
|
||||
* @see IContextEstablishmentHook for hook interface
|
||||
*/
|
||||
@Service()
|
||||
export class ContextEstablishmentHookMetadata {
|
||||
/**
|
||||
* Internal collection of registered hook classes.
|
||||
*
|
||||
* Uses Set for efficient deduplication (though duplicate registration
|
||||
* should not occur with proper decorator usage).
|
||||
*/
|
||||
private readonly contextEstablishmentHooks: Set<ContextEstablishmentHookEntry> = new Set();
|
||||
|
||||
/**
|
||||
* Registers a hook class in the metadata collection.
|
||||
*
|
||||
* Called automatically by the @ContextEstablishmentHook decorator during
|
||||
* module loading. Should not be called directly by application code.
|
||||
*
|
||||
* **Note:** This method does not validate uniqueness or check for naming
|
||||
* conflicts. Validation happens later in the Hook Registry.
|
||||
*
|
||||
* @param hookEntry - The hook class entry to register
|
||||
*
|
||||
* @internal Called by decorator only
|
||||
*/
|
||||
register(hookEntry: ContextEstablishmentHookEntry) {
|
||||
this.contextEstablishmentHooks.add(hookEntry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all registered hook entries.
|
||||
*
|
||||
* Returns an array of [index, entry] tuples compatible with Set.entries().
|
||||
* Primarily used for debugging or low-level iteration.
|
||||
*
|
||||
* **Prefer getClasses()** for most use cases as it returns just the classes.
|
||||
*
|
||||
* @returns Array of [index, entry] tuples from the internal Set
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const entries = metadata.getEntries();
|
||||
* for (const [index, entry] of entries) {
|
||||
* console.log(`Hook ${index}:`, entry.class.name);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
getEntries() {
|
||||
return [...this.contextEstablishmentHooks.entries()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all registered hook classes.
|
||||
*
|
||||
* This is the primary method used by the Hook Registry to obtain hook classes
|
||||
* for instantiation and indexing. Returns just the class constructors without
|
||||
* the wrapper entry objects.
|
||||
*
|
||||
* **Usage pattern:**
|
||||
* ```typescript
|
||||
* const classes = metadata.getClasses();
|
||||
* const hooks = classes.map(HookClass => Container.get(HookClass));
|
||||
* const hooksByName = new Map(hooks.map(h => [h.hookDescription.name, h]));
|
||||
* ```
|
||||
*
|
||||
* @returns Array of hook class constructors ready for DI instantiation
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @Service()
|
||||
* export class HookRegistry {
|
||||
* constructor(
|
||||
* private metadata: ContextEstablishmentHookMetadata,
|
||||
* private container: Container
|
||||
* ) {
|
||||
* const hookClasses = metadata.getClasses();
|
||||
* this.hooks = hookClasses.map(cls => container.get(cls));
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
getClasses() {
|
||||
return [...this.contextEstablishmentHooks.values()].map((entry) => entry.class);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class decorator for context establishment hooks.
|
||||
*
|
||||
* This decorator performs two critical functions:
|
||||
* 1. **Registers** the hook class in ContextEstablishmentHookMetadata for discovery
|
||||
* 2. **Enables DI** by applying @Service() to make the hook injectable
|
||||
*
|
||||
* The decorator executes at module load time (when the class is defined), ensuring
|
||||
* all hooks are registered before the application starts. This enables automatic
|
||||
* discovery without manual registration code.
|
||||
*
|
||||
* **Registration flow:**
|
||||
* ```
|
||||
* @ContextEstablishmentHook() // 1. Decorator executes
|
||||
* export class BearerTokenHook // 2. Class is defined
|
||||
* ↓
|
||||
* ContextEstablishmentHookMetadata // 3. Hook class registered in metadata
|
||||
* ↓
|
||||
* @Service() // 4. DI container registration
|
||||
* ↓
|
||||
* Hook is discoverable & injectable // 5. Ready for use
|
||||
* ```
|
||||
*
|
||||
* **Design pattern:**
|
||||
* This follows the declarative registration pattern used throughout n8n for
|
||||
* extensibility (similar to node registration). Hooks self-register without
|
||||
* requiring central registration files or manual imports.
|
||||
*
|
||||
* **Requirements:**
|
||||
* - Decorated class MUST implement IContextEstablishmentHook
|
||||
* - Decorated class MUST have a hookDescription property with unique name
|
||||
*
|
||||
* **Important notes:**
|
||||
* - No decorator parameters needed (hook metadata lives on hook instance)
|
||||
* - Hooks are registered as singletons via @Service()
|
||||
* - Registration happens eagerly at module load, not lazily
|
||||
* - Duplicate decoration of the same class is safe (Set deduplicates)
|
||||
*
|
||||
* @see IContextEstablishmentHook for interface requirements
|
||||
* @see ContextEstablishmentHookMetadata for underlying registry
|
||||
* @see HookDescription for hook metadata structure
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Basic hook registration:
|
||||
* @ContextEstablishmentHook()
|
||||
* export class BearerTokenHook implements IContextEstablishmentHook {
|
||||
* hookDescription = {
|
||||
* name: 'credentials.bearerToken'
|
||||
* };
|
||||
*
|
||||
* async execute(options: ContextEstablishmentOptions) {
|
||||
* // Extract bearer token from Authorization header
|
||||
* const token = this.extractToken(options.triggerItem);
|
||||
* return {
|
||||
* triggerItem: this.removeAuthHeader(options.triggerItem),
|
||||
* contextUpdate: {
|
||||
* credentials: { version: 1, identity: token }
|
||||
* }
|
||||
* };
|
||||
* }
|
||||
*
|
||||
* isApplicableToTriggerNode(nodeType: string) {
|
||||
* return nodeType === 'n8n-nodes-base.webhook';
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @returns A class decorator function that registers and enables DI for the hook
|
||||
*/
|
||||
export const ContextEstablishmentHook =
|
||||
<T extends ContextEstablishmentHookClass>() =>
|
||||
(target: T) => {
|
||||
// Register hook class in metadata for discovery by Hook Registry
|
||||
Container.get(ContextEstablishmentHookMetadata).register({
|
||||
class: target,
|
||||
});
|
||||
|
||||
// Enable dependency injection for the hook class
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return Service()(target);
|
||||
};
|
||||
@ -0,0 +1,328 @@
|
||||
import type { Constructable } from '@n8n/di';
|
||||
import type {
|
||||
INode,
|
||||
INodeExecutionData,
|
||||
PlaintextExecutionContext,
|
||||
IWorkflowBase,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
/**
|
||||
* Input parameters passed to a context establishment hook during execution.
|
||||
*
|
||||
* Hooks receive the current workflow state and extract information from
|
||||
* trigger items to build the execution context (e.g., credentials, environment).
|
||||
* All hooks work with plaintext (decrypted) context for runtime operations.
|
||||
*
|
||||
* @see IContextEstablishmentHook
|
||||
* @see PlaintextExecutionContext
|
||||
*/
|
||||
export type ContextEstablishmentOptions = {
|
||||
/** The trigger node that initiated the workflow execution */
|
||||
triggerNode: INode;
|
||||
|
||||
/** The complete workflow definition */
|
||||
workflow: IWorkflowBase;
|
||||
|
||||
/**
|
||||
* Trigger items from the workflow execution start.
|
||||
* This array represents items as modified by previous hooks in the chain.
|
||||
* Hooks can extract data from these items and optionally modify them
|
||||
* (e.g., removing sensitive headers before storage).
|
||||
*/
|
||||
triggerItems: INodeExecutionData[];
|
||||
|
||||
/**
|
||||
* The plaintext execution context built so far.
|
||||
* Includes base context plus results from any previously executed hooks.
|
||||
* Contains decrypted credential data for runtime operations.
|
||||
*
|
||||
* @see PlaintextExecutionContext for security considerations
|
||||
*/
|
||||
context: PlaintextExecutionContext;
|
||||
|
||||
/**
|
||||
* Hook-specific configuration provided by the trigger node.
|
||||
* Structure varies per hook type (e.g., { removeFromItem: true } for bearer token hook).
|
||||
*/
|
||||
options?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Result returned by a context establishment hook after execution.
|
||||
*
|
||||
* Hooks can modify trigger items (e.g., remove sensitive headers) and
|
||||
* contribute partial context updates that get merged into the execution context.
|
||||
* All context data is in plaintext form during hook execution.
|
||||
*
|
||||
* @see IContextEstablishmentHook
|
||||
* @see PlaintextExecutionContext
|
||||
*/
|
||||
export type ContextEstablishmentResult = {
|
||||
/**
|
||||
* The potentially modified trigger items.
|
||||
* If undefined, the original trigger items are preserved unchanged.
|
||||
*
|
||||
* Common use case: Removing sensitive data (e.g., Authorization headers)
|
||||
* before storing items in execution history.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Remove Authorization header from trigger items
|
||||
* const modifiedItems = options.triggerItems.map(item => ({
|
||||
* ...item,
|
||||
* json: {
|
||||
* ...item.json,
|
||||
* headers: {
|
||||
* ...item.json.headers,
|
||||
* authorization: undefined
|
||||
* }
|
||||
* }
|
||||
* }));
|
||||
* return { triggerItems: modifiedItems, contextUpdate: { ... } };
|
||||
* ```
|
||||
*/
|
||||
triggerItems?: INodeExecutionData[];
|
||||
|
||||
/**
|
||||
* Partial context update to merge into the execution context.
|
||||
* If undefined, no context updates are applied.
|
||||
*
|
||||
* Contains only this hook's contributions (e.g., credentials data).
|
||||
* Multiple hooks' updates are merged sequentially during execution.
|
||||
* Context data is in plaintext form and will be encrypted before persistence.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Add credential context from bearer token
|
||||
* return {
|
||||
* triggerItems: modifiedItems,
|
||||
* contextUpdate: {
|
||||
* credentials: {
|
||||
* version: 1,
|
||||
* identity: extractedToken,
|
||||
* metadata: { source: 'bearer-token' }
|
||||
* }
|
||||
* }
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
contextUpdate?: Partial<PlaintextExecutionContext>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Metadata describing a context establishment hook.
|
||||
*
|
||||
* This object carries self-describing information about the hook that enables
|
||||
* runtime discovery, lookup, and instantiation. Each hook instance serves as
|
||||
* the single source of truth for its own metadata.
|
||||
*
|
||||
* **Design rationale:**
|
||||
* - Hook instances are self-describing (no external configuration files)
|
||||
* - Name lookup happens at runtime via Registry, not during registration
|
||||
* - Description can be extended without changing decorator or registry internals
|
||||
* - Supports future features like versioning, schema validation, and categorization
|
||||
*
|
||||
* **Future extensions** may include:
|
||||
* - `version?: string` - Semantic version of hook implementation for compatibility checks
|
||||
* - `configSchema?: ZodSchema` - Validation schema for hook-specific options
|
||||
* - `tags?: string[]` - Categorization tags for grouping and filtering
|
||||
* - `applicableTriggers?: string[]` - Cached list of compatible trigger node types
|
||||
* - `deprecated?: boolean | string` - Deprecation status and migration guidance
|
||||
*
|
||||
* @see IContextEstablishmentHook.hookDescription
|
||||
* @see ContextEstablishmentHookMetadata for registration mechanism
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @ContextEstablishmentHook()
|
||||
* export class BearerTokenHook implements IContextEstablishmentHook {
|
||||
* hookDescription = {
|
||||
* name: 'credentials.bearerToken'
|
||||
* };
|
||||
*
|
||||
* // ... hook implementation
|
||||
* }
|
||||
*
|
||||
* ```
|
||||
*/
|
||||
export type HookDescription = {
|
||||
/**
|
||||
* Unique identifier for this hook type.
|
||||
*
|
||||
* Used by the Hook Registry (to be implemented) to index and retrieve
|
||||
* hook instances at runtime. Must be unique across all registered hooks.
|
||||
*
|
||||
* **Naming convention**: Use namespaced names like 'credentials.bearerToken'
|
||||
* or 'envVars.tenantConfig' to organize hooks by domain and avoid collisions.
|
||||
*
|
||||
* **Usage contexts:**
|
||||
* - Trigger node configuration specifies hooks by name
|
||||
* - Hook Registry uses name as lookup key
|
||||
* - UI displays localized names via i18n (e.g., `hooks.${name}.displayName`)
|
||||
* - Logging and debugging references hooks by name
|
||||
* - Error messages include hook name for troubleshooting
|
||||
*
|
||||
* **Versioning**: Future hook versions can use naming like 'credentials.bearerToken.v2'
|
||||
* if breaking changes are needed, though this is not required initially.
|
||||
*
|
||||
* @example 'credentials.bearerToken'
|
||||
* @example 'credentials.apiKey'
|
||||
* @example 'envVars.tenantConfig'
|
||||
* @example 'audit.requestMetadata'
|
||||
*/
|
||||
name: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Interface for context establishment hooks that extract data from trigger
|
||||
* items and extend the execution context during workflow initialization.
|
||||
*
|
||||
* @see ContextEstablishmentOptions - Input parameters
|
||||
* @see ContextEstablishmentResult - Output structure
|
||||
* @see PlaintextExecutionContext - Runtime context type with decrypted data
|
||||
*/
|
||||
export interface IContextEstablishmentHook {
|
||||
/**
|
||||
* Self-describing metadata for this hook instance.
|
||||
*
|
||||
* Provides the unique name and future metadata used by the Hook Registry
|
||||
* for discovery, lookup, and validation. This property makes each hook
|
||||
* instance self-contained and discoverable without external configuration.
|
||||
*
|
||||
* @see HookDescription for detailed metadata structure and future extensions
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @ContextEstablishmentHook()
|
||||
* export class BearerTokenHook implements IContextEstablishmentHook {
|
||||
* hookDescription = {
|
||||
* name: 'credentials.bearerToken'
|
||||
* };
|
||||
*
|
||||
* async execute(options: ContextEstablishmentOptions) {
|
||||
* // Hook implementation
|
||||
* }
|
||||
*
|
||||
* isApplicableToTriggerNode(nodeType: string) {
|
||||
* return nodeType === 'n8n-nodes-base.webhook';
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
hookDescription: HookDescription;
|
||||
/**
|
||||
* Executes the hook to extract context data from trigger information.
|
||||
*
|
||||
* **Implementation requirements:**
|
||||
* 1. Extract relevant data from trigger items (headers, body, query params, etc.)
|
||||
* 2. Optionally modify trigger items to remove sensitive data
|
||||
* 3. Return partial context updates to merge into execution context
|
||||
* 4. Throw errors for unrecoverable failures (stops workflow execution)
|
||||
*
|
||||
* **Execution order:**
|
||||
* Hooks execute sequentially in the order configured by the trigger node.
|
||||
* Each hook receives:
|
||||
* - Trigger items as modified by all previous hooks
|
||||
* - Context with updates from all previous hooks
|
||||
*
|
||||
* **Context handling:**
|
||||
* - Input context is plaintext (PlaintextExecutionContext) for runtime operations
|
||||
* - Output updates are plaintext and will be encrypted before persistence
|
||||
* - Never log or expose plaintext context outside hook execution
|
||||
*
|
||||
* **Error handling:**
|
||||
* - Throw errors if required data is missing (e.g., expected header not found)
|
||||
* - Use descriptive error messages for debugging
|
||||
* - Errors stop workflow execution (fail-fast approach)
|
||||
*
|
||||
* @param options - Input parameters including trigger node, workflow, items, and current context
|
||||
* @returns Promise resolving to modified trigger items and context updates
|
||||
* @throws Error if hook execution fails (stops workflow execution)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* async execute(options: ContextEstablishmentOptions): Promise<ContextEstablishmentResult> {
|
||||
* const removeHeader = options.options?.removeFromItem ?? true;
|
||||
*
|
||||
* // Extract data
|
||||
* const token = this.extractToken(options.triggerItems);
|
||||
* if (!token) {
|
||||
* throw new Error('Bearer token not found in Authorization header');
|
||||
* }
|
||||
*
|
||||
* // Optionally modify items
|
||||
* const modifiedItems = removeHeader
|
||||
* ? this.removeAuthHeader(options.triggerItems)
|
||||
* : undefined;
|
||||
*
|
||||
* // Return context update
|
||||
* return {
|
||||
* triggerItems: modifiedItems,
|
||||
* contextUpdate: {
|
||||
* credentials: { version: 1, identity: token }
|
||||
* }
|
||||
* };
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
execute(options: ContextEstablishmentOptions): Promise<ContextEstablishmentResult>;
|
||||
|
||||
/**
|
||||
* Method to determine if this hook is applicable to a specific trigger node type.
|
||||
*
|
||||
* **Use cases:**
|
||||
* - **UI filtering**: Show only relevant hooks for a trigger type in node configuration
|
||||
* - **Validation**: Prevent incompatible hook configurations at save time
|
||||
* - **Auto-suggestion**: Suggest applicable hooks based on trigger node selection
|
||||
* - **Documentation**: Generate trigger-specific hook documentation
|
||||
*
|
||||
* **Implementation notes:**
|
||||
* - Return true if the hook can extract meaningful data from this trigger type
|
||||
* - Consider transport layer (HTTP, AMQP, manual, etc.)
|
||||
* - Multiple triggers can share the same hook (e.g., webhook and form trigger both support bearer tokens)
|
||||
*
|
||||
* @param nodeType - The node type identifier (e.g., 'n8n-nodes-base.webhook')
|
||||
* @returns true if this hook can be used with the given trigger node type
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Hook only works with HTTP-based triggers
|
||||
* isApplicableToTriggerNode(nodeType: string): boolean {
|
||||
* return [
|
||||
* 'n8n-nodes-base.webhook',
|
||||
* 'n8n-nodes-base.formTrigger',
|
||||
* 'n8n-nodes-base.httpRequest'
|
||||
* ].includes(nodeType);
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Hook works with any trigger that has HTTP headers
|
||||
* isApplicableToTriggerNode(nodeType: string): boolean {
|
||||
* return nodeType.includes('webhook') || nodeType.includes('http');
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
isApplicableToTriggerNode(nodeType: string): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type representing the constructor/class of a context establishment hook.
|
||||
*
|
||||
* Used by the dependency injection container to register and instantiate
|
||||
* hook classes at runtime. Works with the @ContextEstablishmentHook decorator.
|
||||
*
|
||||
* @see IContextEstablishmentHook
|
||||
* @see ContextEstablishmentHook decorator in './index.ts'
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Container } from '@n8n/di';
|
||||
* import type { ContextEstablishmentHookClass } from './context-establishment-hook';
|
||||
*
|
||||
* const HookClass: ContextEstablishmentHookClass = BearerTokenHook;
|
||||
* const hookInstance = Container.get(HookClass);
|
||||
* ```
|
||||
*/
|
||||
export type ContextEstablishmentHookClass = Constructable<IContextEstablishmentHook>;
|
||||
@ -0,0 +1,5 @@
|
||||
export {
|
||||
ContextEstablishmentHookMetadata,
|
||||
ContextEstablishmentHook,
|
||||
} from './context-establishment-hook-metadata';
|
||||
export type * from './context-establishment-hook';
|
||||
@ -88,6 +88,55 @@ export const ExecutionContextSchema = z
|
||||
*/
|
||||
export type IExecutionContext = z.output<typeof ExecutionContextSchema>;
|
||||
|
||||
/**
|
||||
* Runtime representation of execution context with decrypted credential data.
|
||||
*
|
||||
* This type is identical to IExecutionContext except the `credentials` field
|
||||
* contains the decrypted ICredentialContext object instead of an encrypted string.
|
||||
*
|
||||
* **Usage contexts:**
|
||||
* - Hook execution: Hooks work with plaintext context to extract/merge credential data
|
||||
* - Credential resolution: Resolvers need decrypted identity tokens
|
||||
* - Internal processing: Runtime operations that need access to credential context
|
||||
*
|
||||
* **Security notes:**
|
||||
* - Never persist this type to database - use IExecutionContext with encrypted credentials
|
||||
* - Never expose in API responses or logs
|
||||
* - Only exists in-memory during workflow execution
|
||||
* - Should be cleared from memory after use
|
||||
*
|
||||
* **Lifecycle:**
|
||||
* 1. Load IExecutionContext from storage (credentials encrypted)
|
||||
* 2. Decrypt credentials field → PlaintextExecutionContext (runtime only)
|
||||
* 3. Use for hook execution, credential resolution, etc.
|
||||
* 4. Encrypt credentials → IExecutionContext before persistence
|
||||
*
|
||||
* @see IExecutionContext - Persisted form with encrypted credentials
|
||||
* @see ICredentialContext - Decrypted credential structure
|
||||
* @see IExecutionContextUpdate - Partial updates during hook execution
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // During hook execution:
|
||||
* const plaintextContext: PlaintextExecutionContext = {
|
||||
* ...context,
|
||||
* credentials: decryptCredentials(context.credentials) // Decrypt for runtime use
|
||||
* };
|
||||
*
|
||||
* // Hook can now access plaintext credential data
|
||||
* const identity = plaintextContext.credentials?.identity;
|
||||
*
|
||||
* // Before storage, re-encrypt:
|
||||
* const storableContext: IExecutionContext = {
|
||||
* ...plaintextContext,
|
||||
* credentials: encryptCredentials(plaintextContext.credentials)
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
export type PlaintextExecutionContext = Omit<IExecutionContext, 'credentials'> & {
|
||||
credentials?: ICredentialContext;
|
||||
};
|
||||
|
||||
const safeParse = <T extends ZodType>(value: string | object, schema: T) => {
|
||||
const typeName = schema.meta()?.title ?? 'Object';
|
||||
try {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user