๐Ÿ“ฆ n8n-io / n8n

๐Ÿ“„ execution-context.ts ยท 189 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189import z, { type ZodType } from 'zod/v4';

import { jsonParse } from './utils';

const CredentialContextSchemaV1 = z.object({
	version: z.literal(1),
	/**
	 * Identity token/value used for credential resolution
	 * Could be JWT, API key, session token, user ID, etc.
	 */
	identity: z.string(),

	/**
	 * Optional metadata for credential resolution
	 */
	metadata: z.record(z.string(), z.unknown()).optional(),
});

export type ICredentialContextV1 = z.output<typeof CredentialContextSchemaV1>;

export const CredentialContextSchema = z
	.discriminatedUnion('version', [CredentialContextSchemaV1])
	.meta({
		title: 'ICredentialContext',
	});

/**
 * Decrypted structure of credentials field
 * Never stored in this form - always encrypted in IExecutionContext
 */
export type ICredentialContext = z.output<typeof CredentialContextSchema>;

const WorkflowExecuteModeSchema = z.union([
	z.literal('cli'),
	z.literal('error'),
	z.literal('integrated'),
	z.literal('internal'),
	z.literal('manual'),
	z.literal('retry'),
	z.literal('trigger'),
	z.literal('webhook'),
	z.literal('evaluation'),
	z.literal('chat'),
]);

export type WorkflowExecuteModeValues = z.infer<typeof WorkflowExecuteModeSchema>;

const ExecutionContextSchemaV1 = z.object({
	version: z.literal(1),
	/**
	 * When the context was established (Unix timestamp in milliseconds)
	 */
	establishedAt: z.number(),

	/**
	 * The mode in which the workflow is being executed
	 */
	source: WorkflowExecuteModeSchema,

	/**
	 * Optional node where execution started
	 */
	triggerNode: z
		.object({
			name: z.string(),
			type: z.string(),
		})
		.optional(),

	/**
	 * Optional ID of the parent execution, if this is set this
	 * execution context inherited from the mentioned parent execution context.
	 */
	parentExecutionId: z.string().optional(),

	/**
	 * Encrypted credential context for dynamic credential resolution
	 * Always encrypted when stored, decrypted on-demand by credential resolver
	 * @see ICredentialContext for decrypted structure
	 */
	credentials: z.string().optional().meta({
		description:
			'Encrypted credential context for dynamic credential resolution Always encrypted when stored, decrypted on-demand by credential resolver @see ICredentialContext for decrypted structure',
	}),
});

export type IExecutionContextV1 = z.output<typeof ExecutionContextSchemaV1>;

export const ExecutionContextSchema = z
	.discriminatedUnion('version', [ExecutionContextSchemaV1])
	.meta({
		title: 'IExecutionContext',
	});

/**
 * Execution context carries per-execution metadata throughout workflow lifecycle
 * Established at execution start and propagated to sub-workflows/error workflows
 */
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;
};

export const safeParse = <T extends ZodType>(value: string | object, schema: T) => {
	const typeName = schema.meta()?.title ?? 'Object';
	try {
		const normalizedObject = typeof value === 'string' ? jsonParse(value) : value;
		const parseResult = schema.safeParse(normalizedObject);
		if (parseResult.error) {
			throw parseResult.error;
		}
		// here we could implement a mgiration policy for migrating old execution context versions to newer ones
		return parseResult.data;
	} catch (error) {
		throw new Error(`Failed to parse to valid ${typeName}`, {
			cause: error,
		});
	}
};

/**
 * Safely parses an execution context from an
 * @param obj
 * @returns
 */
export const toExecutionContext = (value: string | object): IExecutionContext => {
	// here we could implement a mgiration policy for migrating old execution context versions to newer ones
	return safeParse(value, ExecutionContextSchema);
};

/**
 * Safely parses a credential context from either an object or a string to an
 * ICredentialContext. This can be used to safely parse a decrypted context for
 * example.
 * @param value The object or string to be parsed
 * @returns ICredentialContext
 * @throws Error in case parsing fails for any reason
 */
export const toCredentialContext = (value: string | object): ICredentialContext => {
	// here we could implement a mgiration policy for migrating old credential context versions to newer ones
	return safeParse(value, CredentialContextSchema);
};