@n8n/expression-runtime 0.24.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -62
- package/dist/cjs/build.tsbuildinfo +1 -1
- package/dist/esm/build.tsbuildinfo +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -4,18 +4,16 @@ Secure, isolated expression evaluation runtime for n8n workflows.
|
|
|
4
4
|
|
|
5
5
|
## Status
|
|
6
6
|
|
|
7
|
-
**
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
- ✅
|
|
11
|
-
- ✅
|
|
12
|
-
- ✅
|
|
13
|
-
- ✅
|
|
14
|
-
- ✅
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
Coming in later PRs:
|
|
18
|
-
- 🚧 Workflow integration behind `N8N_EXPRESSION_ENGINE=vm` flag (PR 5)
|
|
7
|
+
**Shipped — the `vm` engine is n8n's default expression engine.**
|
|
8
|
+
|
|
9
|
+
- ✅ TypeScript interfaces and architecture design
|
|
10
|
+
- ✅ Runtime bundle: extension functions, deep lazy proxy system
|
|
11
|
+
- ✅ `IsolatedVmBridge`: V8 isolate management via `isolated-vm`
|
|
12
|
+
- ✅ `ExpressionEvaluator`: tournament integration, expression code caching, isolate pooling
|
|
13
|
+
- ✅ Workflow integration — default engine; `N8N_EXPRESSION_ENGINE=legacy` opts out
|
|
14
|
+
- ✅ Observability (metrics, traces, logs) wired up in `packages/cli`
|
|
15
|
+
|
|
16
|
+
Coming later:
|
|
19
17
|
- 🚧 Web Worker support (Phase 2+)
|
|
20
18
|
- 🚧 Performance optimizations (Phase 3)
|
|
21
19
|
|
|
@@ -34,7 +32,7 @@ Future support (Phase 2+):
|
|
|
34
32
|
|
|
35
33
|
- 🔒 **Secure**: Expressions run in isolated V8 contexts with memory limits (128MB) and timeouts (5s)
|
|
36
34
|
- 🚀 **Performant**: Lazy data loading via proxies, script compilation caching, and expression code caching
|
|
37
|
-
- 📊 **Observable**: Built-in metrics, traces, and logs support
|
|
35
|
+
- 📊 **Observable**: Built-in metrics, traces, and logs support via `ObservabilityProvider`
|
|
38
36
|
- 🌐 **Universal**: Works in Node.js backend (browsers and task runners in Phase 2+)
|
|
39
37
|
- 🛡️ **AST Security**: Tournament AST hooks (`ThisSanitizer`, `PrototypeSanitizer`, `DollarSignValidator`) validate expressions before execution
|
|
40
38
|
|
|
@@ -61,32 +59,33 @@ pnpm add @n8n/expression-runtime
|
|
|
61
59
|
```typescript
|
|
62
60
|
import { ExpressionEvaluator, IsolatedVmBridge } from '@n8n/expression-runtime';
|
|
63
61
|
|
|
64
|
-
// Create bridge
|
|
65
|
-
const bridge = new IsolatedVmBridge({
|
|
66
|
-
memoryLimit: 128,
|
|
67
|
-
timeout: 5000,
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
// Create evaluator
|
|
62
|
+
// Create evaluator with a bridge factory (bridges are pooled)
|
|
71
63
|
const evaluator = new ExpressionEvaluator({
|
|
72
|
-
|
|
64
|
+
createBridge: () => new IsolatedVmBridge({ memoryLimit: 128, timeout: 5000 }),
|
|
65
|
+
maxCodeCacheSize: 1024,
|
|
73
66
|
});
|
|
74
67
|
|
|
75
68
|
// Initialize
|
|
76
69
|
await evaluator.initialize();
|
|
77
70
|
|
|
78
|
-
//
|
|
71
|
+
// Acquire an isolate for a caller, evaluate, release
|
|
72
|
+
const caller = {};
|
|
73
|
+
await evaluator.acquire(caller);
|
|
74
|
+
|
|
79
75
|
const result = evaluator.evaluate(
|
|
80
76
|
'{{ $json.user.email }}',
|
|
81
77
|
{
|
|
82
78
|
$json: {
|
|
83
79
|
user: { email: 'test@example.com' }
|
|
84
80
|
}
|
|
85
|
-
}
|
|
81
|
+
},
|
|
82
|
+
caller,
|
|
86
83
|
);
|
|
87
84
|
|
|
88
85
|
console.log(result); // "test@example.com"
|
|
89
86
|
|
|
87
|
+
await evaluator.release(caller);
|
|
88
|
+
|
|
90
89
|
// Clean up
|
|
91
90
|
await evaluator.dispose();
|
|
92
91
|
```
|
|
@@ -103,9 +102,9 @@ import {
|
|
|
103
102
|
DollarSignValidator,
|
|
104
103
|
} from 'n8n-workflow/expression-sandboxing';
|
|
105
104
|
|
|
106
|
-
const bridge = new IsolatedVmBridge({ timeout: 5000 });
|
|
107
105
|
const evaluator = new ExpressionEvaluator({
|
|
108
|
-
|
|
106
|
+
createBridge: () => new IsolatedVmBridge({ timeout: 5000 }),
|
|
107
|
+
maxCodeCacheSize: 1024,
|
|
109
108
|
hooks: {
|
|
110
109
|
before: [ThisSanitizer],
|
|
111
110
|
after: [PrototypeSanitizer, DollarSignValidator],
|
|
@@ -117,22 +116,19 @@ await evaluator.initialize();
|
|
|
117
116
|
|
|
118
117
|
When `hooks` is omitted the evaluator still runs tournament transformation (template parsing, `this` binding) but without AST security validation — suitable for development and testing.
|
|
119
118
|
|
|
120
|
-
### With Observability
|
|
119
|
+
### With Observability
|
|
121
120
|
|
|
122
|
-
|
|
123
|
-
import { OpenTelemetryProvider } from '@n8n/expression-runtime/observability';
|
|
124
|
-
|
|
125
|
-
const observability = new OpenTelemetryProvider({
|
|
126
|
-
serviceName: 'n8n-expressions',
|
|
127
|
-
});
|
|
121
|
+
Pass an `ObservabilityProvider` implementation to emit metrics, traces, and logs for evaluations:
|
|
128
122
|
|
|
123
|
+
```typescript
|
|
129
124
|
const evaluator = new ExpressionEvaluator({
|
|
130
|
-
|
|
125
|
+
createBridge: () => new IsolatedVmBridge({ timeout: 5000 }),
|
|
126
|
+
maxCodeCacheSize: 1024,
|
|
131
127
|
observability,
|
|
132
128
|
});
|
|
133
129
|
```
|
|
134
130
|
|
|
135
|
-
|
|
131
|
+
This package defines the `ObservabilityProvider` interface; the production implementation lives in `packages/cli/src/expression-observability/expression-observability.provider.ts` and is wired up during backend startup. It is controlled via the `N8N_EXPRESSION_ENGINE_OBSERVABILITY_*` and `N8N_EXPRESSION_ENGINE_TRACES_*` environment variables (see below).
|
|
136
132
|
|
|
137
133
|
## API
|
|
138
134
|
|
|
@@ -144,7 +140,9 @@ Main class for expression evaluation.
|
|
|
144
140
|
class ExpressionEvaluator {
|
|
145
141
|
constructor(config: EvaluatorConfig);
|
|
146
142
|
initialize(): Promise<void>;
|
|
147
|
-
|
|
143
|
+
acquire(owner: object): Promise<boolean>;
|
|
144
|
+
evaluate(expression: string, data: WorkflowData, caller: object, options?: EvaluateOptions): unknown;
|
|
145
|
+
release(owner: object): Promise<void>;
|
|
148
146
|
dispose(): Promise<void>;
|
|
149
147
|
isDisposed(): boolean;
|
|
150
148
|
}
|
|
@@ -179,38 +177,41 @@ interface RuntimeBridge {
|
|
|
179
177
|
|
|
180
178
|
```typescript
|
|
181
179
|
interface EvaluatorConfig {
|
|
182
|
-
|
|
183
|
-
|
|
180
|
+
createBridge: () => RuntimeBridge; // required - factory, bridges are pooled
|
|
181
|
+
maxCodeCacheSize: number; // required - LRU size for tournament-transformed code
|
|
182
|
+
observability?: ObservabilityProvider; // optional - metrics/traces/logs provider
|
|
184
183
|
hooks?: TournamentHooks; // optional - AST security hooks for tournament
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
memoryLimit?: number; // Default: 128 MB
|
|
189
|
-
timeout?: number; // Default: 5000 ms
|
|
190
|
-
debug?: boolean; // Default: false
|
|
184
|
+
poolSize?: number; // optional - pre-warmed bridges, default 1
|
|
185
|
+
idleTimeoutMs?: number; // optional - scale pool to 0 after idle period
|
|
186
|
+
logger?: Logger; // optional - falls back to no-op
|
|
191
187
|
}
|
|
192
188
|
```
|
|
193
189
|
|
|
194
|
-
## Environment Variables
|
|
190
|
+
## Environment Variables
|
|
191
|
+
|
|
192
|
+
In n8n, the evaluator is configured via `ExpressionEngineConfig` (`@n8n/config`):
|
|
195
193
|
|
|
196
194
|
```bash
|
|
197
|
-
#
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
#
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
195
|
+
# Engine selection ('vm' is the default; 'legacy' opts out of isolation)
|
|
196
|
+
N8N_EXPRESSION_ENGINE=vm
|
|
197
|
+
|
|
198
|
+
# Isolate pool and code cache
|
|
199
|
+
N8N_EXPRESSION_ENGINE_POOL_SIZE=1
|
|
200
|
+
N8N_EXPRESSION_ENGINE_MAX_CODE_CACHE_SIZE=1024
|
|
201
|
+
N8N_EXPRESSION_ENGINE_IDLE_TIMEOUT= # seconds; unset = pool never scales to 0
|
|
202
|
+
|
|
203
|
+
# Bridge limits
|
|
204
|
+
N8N_EXPRESSION_ENGINE_TIMEOUT=5000 # ms
|
|
205
|
+
N8N_EXPRESSION_ENGINE_MEMORY_LIMIT=128 # MB
|
|
206
|
+
|
|
207
|
+
# Observability
|
|
208
|
+
N8N_EXPRESSION_ENGINE_OBSERVABILITY_ENABLED=true
|
|
209
|
+
N8N_EXPRESSION_ENGINE_TRACES_ENABLED=true
|
|
210
|
+
N8N_EXPRESSION_ENGINE_SLOW_EVAL_THRESHOLD_MS=50
|
|
211
|
+
N8N_EXPRESSION_ENGINE_TRACES_SAMPLE_RATE=0.0
|
|
211
212
|
```
|
|
212
213
|
|
|
213
|
-
|
|
214
|
+
See `packages/@n8n/config/src/configs/expression-engine.config.ts` for the authoritative list and defaults.
|
|
214
215
|
|
|
215
216
|
## Development
|
|
216
217
|
|
|
@@ -243,13 +244,18 @@ import { ExpressionEvaluator, IsolatedVmBridge } from '@n8n/expression-runtime';
|
|
|
243
244
|
|
|
244
245
|
describe('ExpressionEvaluator', () => {
|
|
245
246
|
it('evaluates simple expression', async () => {
|
|
246
|
-
const
|
|
247
|
-
|
|
247
|
+
const evaluator = new ExpressionEvaluator({
|
|
248
|
+
createBridge: () => new IsolatedVmBridge({ timeout: 5000 }),
|
|
249
|
+
maxCodeCacheSize: 1024,
|
|
250
|
+
});
|
|
248
251
|
|
|
249
252
|
await evaluator.initialize();
|
|
250
253
|
|
|
251
|
-
const
|
|
254
|
+
const caller = {};
|
|
255
|
+
await evaluator.acquire(caller);
|
|
256
|
+
const result = evaluator.evaluate('{{ $json.value }}', { $json: { value: 42 } }, caller);
|
|
252
257
|
expect(result).toBe(42);
|
|
258
|
+
await evaluator.release(caller);
|
|
253
259
|
|
|
254
260
|
await evaluator.dispose();
|
|
255
261
|
});
|