From dcea7a9d5f41dd239091940845f2712187982ef0 Mon Sep 17 00:00:00 2001 From: Andreas Fitzek Date: Thu, 20 Nov 2025 12:26:02 +0100 Subject: [PATCH] chore(core): Interface definition for context establishment hooks (#22073) --- packages/@n8n/decorators/package.json | 1 + .../context-establishment-hook.test.ts | 148 ++++++++ .../context-establishment-hook-metadata.ts | 200 +++++++++++ .../context-establishment-hook.ts | 328 ++++++++++++++++++ .../src/context-establishment/index.ts | 5 + packages/workflow/src/execution-context.ts | 49 +++ 6 files changed, 731 insertions(+) create mode 100644 packages/@n8n/decorators/src/context-establishment/__tests__/context-establishment-hook.test.ts create mode 100644 packages/@n8n/decorators/src/context-establishment/context-establishment-hook-metadata.ts create mode 100644 packages/@n8n/decorators/src/context-establishment/context-establishment-hook.ts create mode 100644 packages/@n8n/decorators/src/context-establishment/index.ts diff --git a/packages/@n8n/decorators/package.json b/packages/@n8n/decorators/package.json index 89cff5978ce..e45fc61d28c 100644 --- a/packages/@n8n/decorators/package.json +++ b/packages/@n8n/decorators/package.json @@ -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", diff --git a/packages/@n8n/decorators/src/context-establishment/__tests__/context-establishment-hook.test.ts b/packages/@n8n/decorators/src/context-establishment/__tests__/context-establishment-hook.test.ts new file mode 100644 index 00000000000..7da6738799f --- /dev/null +++ b/packages/@n8n/decorators/src/context-establishment/__tests__/context-establishment-hook.test.ts @@ -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 { + 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 { + return {}; + } + isApplicableToTriggerNode(_nodeType: string): boolean { + return true; + } + } + + @ContextEstablishmentHook() + class SecondHook implements IContextEstablishmentHook { + hookDescription = { name: 'second.hook' }; + async execute(_options: ContextEstablishmentOptions): Promise { + return {}; + } + isApplicableToTriggerNode(_nodeType: string): boolean { + return true; + } + } + + @ContextEstablishmentHook() + class ThirdHook implements IContextEstablishmentHook { + hookDescription = { name: 'third.hook' }; + async execute(_options: ContextEstablishmentOptions): Promise { + 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 { + 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 { + 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 { + return {}; + } + isApplicableToTriggerNode(_nodeType: string): boolean { + return true; + } + } + + @ContextEstablishmentHook() + class ApiKeyHook implements IContextEstablishmentHook { + hookDescription = { name: 'credentials.apiKey' }; + async execute(_options: ContextEstablishmentOptions): Promise { + 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'); + }); +}); diff --git a/packages/@n8n/decorators/src/context-establishment/context-establishment-hook-metadata.ts b/packages/@n8n/decorators/src/context-establishment/context-establishment-hook-metadata.ts new file mode 100644 index 00000000000..e26dfb1e749 --- /dev/null +++ b/packages/@n8n/decorators/src/context-establishment/context-establishment-hook-metadata.ts @@ -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 = 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 = + () => + (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); + }; diff --git a/packages/@n8n/decorators/src/context-establishment/context-establishment-hook.ts b/packages/@n8n/decorators/src/context-establishment/context-establishment-hook.ts new file mode 100644 index 00000000000..15efaa1ef87 --- /dev/null +++ b/packages/@n8n/decorators/src/context-establishment/context-establishment-hook.ts @@ -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; +}; + +/** + * 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; +}; + +/** + * 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 { + * 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; + + /** + * 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; diff --git a/packages/@n8n/decorators/src/context-establishment/index.ts b/packages/@n8n/decorators/src/context-establishment/index.ts new file mode 100644 index 00000000000..7ea4a597362 --- /dev/null +++ b/packages/@n8n/decorators/src/context-establishment/index.ts @@ -0,0 +1,5 @@ +export { + ContextEstablishmentHookMetadata, + ContextEstablishmentHook, +} from './context-establishment-hook-metadata'; +export type * from './context-establishment-hook'; diff --git a/packages/workflow/src/execution-context.ts b/packages/workflow/src/execution-context.ts index 14950ee8248..b91453bc18e 100644 --- a/packages/workflow/src/execution-context.ts +++ b/packages/workflow/src/execution-context.ts @@ -88,6 +88,55 @@ export const ExecutionContextSchema = z */ export type IExecutionContext = z.output; +/** + * 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 & { + credentials?: ICredentialContext; +}; + const safeParse = (value: string | object, schema: T) => { const typeName = schema.meta()?.title ?? 'Object'; try {