import z from 'zod'; import { Config, Env } from '../decorators'; const expressionEngineSchema = z.enum(['legacy', 'vm', 'quickjs']); /** * The engines the editor can run in a browser. `vm` is absent on purpose: isolated-vm * is a native module, so the enum rejects it here instead of letting the editor accept * the value and silently keep the legacy evaluator. */ const frontendExpressionEngineSchema = z.enum(['legacy', 'quickjs']); @Config export class ExpressionEngineConfig { /** * Which expression engine to use. * - `vm` (default) runs expressions in a V8 isolate (isolated-vm). * - `legacy` runs expressions without isolation. Less secure and soon to be deprecated. * - `quickjs` runs expressions in a QuickJS WASM sandbox. Experimental. */ @Env('N8N_EXPRESSION_ENGINE', expressionEngineSchema) engine: 'legacy' | 'vm' | 'quickjs' = 'vm'; /** * Which expression engine the editor uses in the browser. Independent of `engine`: * the backend can evaluate with `vm` while the editor evaluates with `quickjs`. * - `legacy` (default) runs expressions without isolation. Soon to be deprecated. * - `quickjs` runs expressions in a QuickJS WASM sandbox. */ @Env('N8N_EXPRESSION_ENGINE_FRONTEND', frontendExpressionEngineSchema) frontendEngine: 'legacy' | 'quickjs' = 'legacy'; /** Number of V8 isolates ready in the pool. */ @Env('N8N_EXPRESSION_ENGINE_POOL_SIZE') poolSize: number = 1; /** Max number of AST-transformed expressions to cache. */ @Env('N8N_EXPRESSION_ENGINE_MAX_CODE_CACHE_SIZE') maxCodeCacheSize: number = 1024; /** * Execution timeout in milliseconds for each expression evaluation in the VM bridge. * The isolate accepts only a positive 32-bit integer: it rejects a fractional value * outright, and leaves the evaluation effectively unbounded for zero or a negative one. */ @Env('N8N_EXPRESSION_ENGINE_TIMEOUT', z.number({ coerce: true }).int().positive()) bridgeTimeout: number = 5000; /** * Memory limit in MB for the V8 isolate used by the VM bridge. * The isolate requires at least 8MB, and a negative value leaves it without a limit. * It tolerates a fractional value by truncating it, but whole megabytes are the only * meaningful unit here, so this rejects a fraction rather than silently truncating. */ @Env('N8N_EXPRESSION_ENGINE_MEMORY_LIMIT', z.number({ coerce: true }).int().min(8)) bridgeMemoryLimit: number = 128; /** * Whether to emit observability signals (metrics, traces, logs) for the VM evaluator. * Only takes effect when `engine === 'vm'` or `engine === 'quickjs'`; legacy mode never * emits expression metrics regardless of this setting. */ @Env('N8N_EXPRESSION_ENGINE_OBSERVABILITY_ENABLED') observabilityEnabled: boolean = true; /** * Whether n8n sends OpenTelemetry spans for expression evaluations when `N8N_OTEL_ENABLED` * is true. n8n creates a span for each evaluation that fails or takes longer than * `N8N_EXPRESSION_ENGINE_SLOW_EVAL_THRESHOLD_MS`, and samples the rest at * `N8N_EXPRESSION_ENGINE_TRACES_SAMPLE_RATE`. */ @Env('N8N_EXPRESSION_ENGINE_TRACES_ENABLED') tracesEnabled: boolean = false; /** Threshold in ms above which an evaluation is considered "slow" and gets a span. */ @Env('N8N_EXPRESSION_ENGINE_SLOW_EVAL_THRESHOLD_MS', z.number({ coerce: true }).positive()) slowEvaluationThresholdMs: number = 50; /** Head-based sampling rate (0.0–1.0) for healthy-path spans. Slow and erroring expressions always emit. */ @Env('N8N_EXPRESSION_ENGINE_TRACES_SAMPLE_RATE', z.number({ coerce: true }).min(0).max(1)) tracesSampleRate: number = 0.0; /** If set, scale the pool to 0 warm isolates after this many seconds with no acquire. */ @Env('N8N_EXPRESSION_ENGINE_IDLE_TIMEOUT') idleTimeout?: number; /** * Whether a production webhook request may skip acquiring an isolate when its * trigger provably evaluates no expression during the webhook phase. Off * acquires one for every request. * * TODO(native-evaluation rollout, CAT-4699): remove through a deprecation entry. */ @Env('N8N_EXPRESSION_ENGINE_ALLOW_WEBHOOK_ISOLATE_SKIP') allowWebhookIsolateSkip: boolean = true; /** * Experimental: lazy isolate acquisition. Acquire calls only open a scope, * and an isolate is created on the first expression that actually needs the * engine. Scopes that evaluate no expression never consume an isolate. When * the pool is exhausted, the isolate is built synchronously on the request * path (see the `expression.pool.cold_start_sync` metrics). */ @Env('N8N_EXPRESSION_ENGINE_LAZY_ACQUIRE') lazyAcquire: boolean = false; /** * Experimental: reuse the V8 compile cache for the expression runtime * bundle. The first bundle compile produces V8 cached data, and every * later isolate build consumes it, skipping re-parsing the bundle. V8 * validates the cached data and silently recompiles when it is stale. * Only affects the `vm` engine. */ @Env('N8N_EXPRESSION_ENGINE_COMPILE_CACHE') compileCache: boolean = false; /** * Experimental: fast native evaluation. Simple expressions are evaluated * directly by n8n instead of the expression engine, so they never need an * isolate. Every other expression is evaluated by the engine as before. * Applies to every engine. */ @Env('N8N_EXPRESSION_ENGINE_NATIVE_EVALUATION') nativeEvaluation: boolean = false; }