@n8n/expression-runtime 0.7.0 → 0.9.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/dist/bundle/runtime.esm.js +17 -17
- package/dist/bundle/runtime.esm.js.map +4 -4
- package/dist/bundle/runtime.iife.js +17 -17
- package/dist/bundle/runtime.iife.js.map +4 -4
- package/dist/cjs/bridge/isolated-vm-bridge.d.ts +48 -34
- package/dist/cjs/bridge/isolated-vm-bridge.d.ts.map +1 -1
- package/dist/cjs/bridge/isolated-vm-bridge.js +285 -236
- package/dist/cjs/bridge/isolated-vm-bridge.js.map +1 -1
- package/dist/cjs/build.tsbuildinfo +1 -1
- package/dist/cjs/evaluator/expression-evaluator.d.ts +8 -1
- package/dist/cjs/evaluator/expression-evaluator.d.ts.map +1 -1
- package/dist/cjs/evaluator/expression-evaluator.js +62 -6
- package/dist/cjs/evaluator/expression-evaluator.js.map +1 -1
- package/dist/cjs/extensions/boolean-extensions.d.ts +1 -1
- package/dist/cjs/extensions/object-extensions.d.ts.map +1 -1
- package/dist/cjs/extensions/object-extensions.js +8 -4
- package/dist/cjs/extensions/object-extensions.js.map +1 -1
- package/dist/cjs/index.d.ts +2 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +4 -2
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/pool/isolate-pool.d.ts +35 -0
- package/dist/cjs/pool/isolate-pool.d.ts.map +1 -0
- package/dist/cjs/pool/isolate-pool.js +123 -0
- package/dist/cjs/pool/isolate-pool.js.map +1 -0
- package/dist/cjs/runtime/context.d.ts +18 -0
- package/dist/cjs/runtime/context.d.ts.map +1 -0
- package/dist/cjs/runtime/{reset.js → context.js} +95 -100
- package/dist/cjs/runtime/context.js.map +1 -0
- package/dist/cjs/runtime/index.d.ts +1 -19
- package/dist/cjs/runtime/index.d.ts.map +1 -1
- package/dist/cjs/runtime/index.js +3 -5
- package/dist/cjs/runtime/index.js.map +1 -1
- package/dist/cjs/runtime/lazy-proxy.d.ts +28 -3
- package/dist/cjs/runtime/lazy-proxy.d.ts.map +1 -1
- package/dist/cjs/runtime/lazy-proxy.js +84 -16
- package/dist/cjs/runtime/lazy-proxy.js.map +1 -1
- package/dist/cjs/types/bridge.d.ts +12 -7
- package/dist/cjs/types/bridge.d.ts.map +1 -1
- package/dist/cjs/types/bridge.js +7 -1
- package/dist/cjs/types/bridge.js.map +1 -1
- package/dist/cjs/types/evaluator.d.ts +24 -14
- package/dist/cjs/types/evaluator.d.ts.map +1 -1
- package/dist/cjs/types/evaluator.js.map +1 -1
- package/dist/cjs/types/index.d.ts +1 -1
- package/dist/cjs/types/index.d.ts.map +1 -1
- package/dist/esm/bridge/isolated-vm-bridge.d.ts +48 -34
- package/dist/esm/bridge/isolated-vm-bridge.d.ts.map +1 -1
- package/dist/esm/bridge/isolated-vm-bridge.js +285 -236
- package/dist/esm/bridge/isolated-vm-bridge.js.map +1 -1
- package/dist/esm/build.tsbuildinfo +1 -1
- package/dist/esm/evaluator/expression-evaluator.d.ts +8 -1
- package/dist/esm/evaluator/expression-evaluator.d.ts.map +1 -1
- package/dist/esm/evaluator/expression-evaluator.js +61 -5
- package/dist/esm/evaluator/expression-evaluator.js.map +1 -1
- package/dist/esm/extensions/object-extensions.d.ts.map +1 -1
- package/dist/esm/extensions/object-extensions.js +8 -4
- package/dist/esm/extensions/object-extensions.js.map +1 -1
- package/dist/esm/index.d.ts +2 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/pool/isolate-pool.d.ts +35 -0
- package/dist/esm/pool/isolate-pool.d.ts.map +1 -0
- package/dist/esm/pool/isolate-pool.js +107 -0
- package/dist/esm/pool/isolate-pool.js.map +1 -0
- package/dist/esm/runtime/context.d.ts +18 -0
- package/dist/esm/runtime/context.d.ts.map +1 -0
- package/dist/esm/runtime/{reset.js → context.js} +95 -100
- package/dist/esm/runtime/context.js.map +1 -0
- package/dist/esm/runtime/index.d.ts +1 -19
- package/dist/esm/runtime/index.d.ts.map +1 -1
- package/dist/esm/runtime/index.js +2 -4
- package/dist/esm/runtime/index.js.map +1 -1
- package/dist/esm/runtime/lazy-proxy.d.ts +28 -3
- package/dist/esm/runtime/lazy-proxy.d.ts.map +1 -1
- package/dist/esm/runtime/lazy-proxy.js +83 -16
- package/dist/esm/runtime/lazy-proxy.js.map +1 -1
- package/dist/esm/types/bridge.d.ts +12 -7
- package/dist/esm/types/bridge.d.ts.map +1 -1
- package/dist/esm/types/bridge.js +7 -1
- package/dist/esm/types/bridge.js.map +1 -1
- package/dist/esm/types/evaluator.d.ts +24 -14
- package/dist/esm/types/evaluator.d.ts.map +1 -1
- package/dist/esm/types/evaluator.js.map +1 -1
- package/dist/esm/types/index.d.ts +1 -1
- package/dist/esm/types/index.d.ts.map +1 -1
- package/package.json +10 -7
- package/dist/cjs/runtime/reset.d.ts +0 -16
- package/dist/cjs/runtime/reset.d.ts.map +0 -1
- package/dist/cjs/runtime/reset.js.map +0 -1
- package/dist/esm/runtime/reset.d.ts +0 -16
- package/dist/esm/runtime/reset.d.ts.map +0 -1
- package/dist/esm/runtime/reset.js.map +0 -1
|
@@ -10,6 +10,33 @@ function getIvm() {
|
|
|
10
10
|
return _ivm;
|
|
11
11
|
}
|
|
12
12
|
const BUNDLE_RELATIVE_PATH = path.join('dist', 'bundle', 'runtime.iife.js');
|
|
13
|
+
/** Check if a value is an error sentinel returned by serializeError. */
|
|
14
|
+
function isErrorSentinel(value) {
|
|
15
|
+
return (typeof value === 'object' &&
|
|
16
|
+
value !== null &&
|
|
17
|
+
value.__isError === true);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Serialize an error into a transferable metadata object.
|
|
21
|
+
*
|
|
22
|
+
* Host-side callbacks (getValueAtPath, etc.) catch errors and return this
|
|
23
|
+
* sentinel instead of letting the error cross the isolate boundary (which
|
|
24
|
+
* strips custom class identity and properties). The isolate-side proxy
|
|
25
|
+
* detects __isError and reconstructs a proper Error to throw.
|
|
26
|
+
*/
|
|
27
|
+
function serializeError(err) {
|
|
28
|
+
if (err instanceof Error) {
|
|
29
|
+
const extra = Object.fromEntries(Object.entries(err).filter(([key]) => key !== 'name' && key !== 'message' && key !== 'stack'));
|
|
30
|
+
return {
|
|
31
|
+
__isError: true,
|
|
32
|
+
name: err.name,
|
|
33
|
+
message: err.message,
|
|
34
|
+
stack: err.stack,
|
|
35
|
+
extra,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return { __isError: true, name: 'Error', message: String(err), extra: {} };
|
|
39
|
+
}
|
|
13
40
|
/**
|
|
14
41
|
* Read the runtime IIFE bundle by walking up from `__dirname` until
|
|
15
42
|
* `dist/bundle/runtime.iife.js` is found.
|
|
@@ -46,23 +73,13 @@ export class IsolatedVmBridge {
|
|
|
46
73
|
initialized = false;
|
|
47
74
|
disposed = false;
|
|
48
75
|
config;
|
|
49
|
-
|
|
50
|
-
// Maps expression code -> compiled ivm.Script
|
|
51
|
-
scriptCache = new Map();
|
|
52
|
-
// Active ivm.Reference callbacks — released before each re-registration
|
|
53
|
-
// to prevent reference accumulation across execute() calls
|
|
54
|
-
valueAtPathRef;
|
|
55
|
-
arrayElementRef;
|
|
56
|
-
callFunctionRef;
|
|
57
|
-
// Pre-resolved reference to resetDataProxies() inside the isolate.
|
|
58
|
-
// Using applySync on a stored reference avoids the per-call
|
|
59
|
-
// ScriptCompiler::Compile() cost that evalSync incurs.
|
|
60
|
-
resetDataProxiesRef;
|
|
76
|
+
logger;
|
|
61
77
|
constructor(config = {}) {
|
|
62
78
|
this.config = {
|
|
63
79
|
...DEFAULT_BRIDGE_CONFIG,
|
|
64
80
|
...config,
|
|
65
81
|
};
|
|
82
|
+
this.logger = this.config.logger;
|
|
66
83
|
// Create isolate with memory limit
|
|
67
84
|
// Note: memoryLimit is in MB
|
|
68
85
|
this.isolate = new (getIvm().Isolate)({ memoryLimit: this.config.memoryLimit });
|
|
@@ -96,14 +113,8 @@ export class IsolatedVmBridge {
|
|
|
96
113
|
await this.verifyProxySystem();
|
|
97
114
|
// Inject E() error handler needed by tournament-generated try-catch code
|
|
98
115
|
await this.injectErrorHandler();
|
|
99
|
-
// Store a reference to resetDataProxies for efficient per-call invocation
|
|
100
|
-
this.resetDataProxiesRef = await this.context.global.get('resetDataProxies', {
|
|
101
|
-
reference: true,
|
|
102
|
-
});
|
|
103
116
|
this.initialized = true;
|
|
104
|
-
|
|
105
|
-
console.log('[IsolatedVmBridge] Initialized successfully');
|
|
106
|
-
}
|
|
117
|
+
this.logger.info('[IsolatedVmBridge] Initialized successfully');
|
|
107
118
|
}
|
|
108
119
|
/**
|
|
109
120
|
* Load runtime bundle into the isolate.
|
|
@@ -112,7 +123,7 @@ export class IsolatedVmBridge {
|
|
|
112
123
|
* - DateTime, extend, extendOptional (expression engine globals)
|
|
113
124
|
* - SafeObject and SafeError wrappers
|
|
114
125
|
* - createDeepLazyProxy function
|
|
115
|
-
* -
|
|
126
|
+
* - buildContext function
|
|
116
127
|
*
|
|
117
128
|
* @private
|
|
118
129
|
* @throws {Error} If context not initialized or bundle loading fails
|
|
@@ -125,20 +136,16 @@ export class IsolatedVmBridge {
|
|
|
125
136
|
// Load runtime bundle (includes vendor libraries + proxy system)
|
|
126
137
|
const runtimeBundle = await readRuntimeBundle();
|
|
127
138
|
// Evaluate bundle in isolate context
|
|
128
|
-
// This makes all exported globals available (DateTime, extend, extendOptional, SafeObject, SafeError, createDeepLazyProxy,
|
|
139
|
+
// This makes all exported globals available (DateTime, extend, extendOptional, SafeObject, SafeError, createDeepLazyProxy, buildContext)
|
|
129
140
|
await this.context.eval(runtimeBundle);
|
|
130
|
-
|
|
131
|
-
console.log('[IsolatedVmBridge] Runtime bundle loaded');
|
|
132
|
-
}
|
|
141
|
+
this.logger.info('[IsolatedVmBridge] Runtime bundle loaded');
|
|
133
142
|
// Verify vendor libraries loaded correctly
|
|
134
143
|
const hasDateTime = await this.context.eval('typeof DateTime !== "undefined"');
|
|
135
144
|
const hasExtend = await this.context.eval('typeof extend !== "undefined"');
|
|
136
145
|
if (!hasDateTime || !hasExtend) {
|
|
137
146
|
throw new Error(`Library verification failed: DateTime=${hasDateTime}, extend=${hasExtend}`);
|
|
138
147
|
}
|
|
139
|
-
|
|
140
|
-
console.log('[IsolatedVmBridge] Vendor libraries verified successfully');
|
|
141
|
-
}
|
|
148
|
+
this.logger.info('[IsolatedVmBridge] Vendor libraries verified successfully');
|
|
142
149
|
}
|
|
143
150
|
catch (error) {
|
|
144
151
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -161,19 +168,16 @@ export class IsolatedVmBridge {
|
|
|
161
168
|
try {
|
|
162
169
|
// Verify proxy system components loaded correctly
|
|
163
170
|
const hasProxyCreator = await this.context.eval('typeof createDeepLazyProxy !== "undefined"');
|
|
164
|
-
const hasData = await this.context.eval('typeof __data !== "undefined"');
|
|
165
171
|
const hasSafeObject = await this.context.eval('typeof SafeObject !== "undefined"');
|
|
166
172
|
const hasSafeError = await this.context.eval('typeof SafeError !== "undefined"');
|
|
167
|
-
const
|
|
168
|
-
if (!hasProxyCreator || !
|
|
173
|
+
const hasBuildContext = await this.context.eval('typeof buildContext !== "undefined"');
|
|
174
|
+
if (!hasProxyCreator || !hasSafeObject || !hasSafeError || !hasBuildContext) {
|
|
169
175
|
throw new Error(`Proxy system verification failed: ` +
|
|
170
|
-
`createDeepLazyProxy=${hasProxyCreator},
|
|
176
|
+
`createDeepLazyProxy=${hasProxyCreator}, ` +
|
|
171
177
|
`SafeObject=${hasSafeObject}, SafeError=${hasSafeError}, ` +
|
|
172
|
-
`
|
|
173
|
-
}
|
|
174
|
-
if (this.config.debug) {
|
|
175
|
-
console.log('[IsolatedVmBridge] Proxy system verified successfully');
|
|
178
|
+
`buildContext=${hasBuildContext}`);
|
|
176
179
|
}
|
|
180
|
+
this.logger.info('[IsolatedVmBridge] Proxy system verified successfully');
|
|
177
181
|
}
|
|
178
182
|
catch (error) {
|
|
179
183
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -183,11 +187,22 @@ export class IsolatedVmBridge {
|
|
|
183
187
|
/**
|
|
184
188
|
* Inject the E() error handler into the isolate context.
|
|
185
189
|
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
* -
|
|
190
|
-
*
|
|
190
|
+
* There are two exception-handling layers inside the isolate:
|
|
191
|
+
*
|
|
192
|
+
* 1. **Inner layer (this handler, `E()`)** — Tournament wraps each
|
|
193
|
+
* expression with try-catch that calls `E(error, this)`. This handler
|
|
194
|
+
* must match the legacy engine's behavior (set in expression.ts via
|
|
195
|
+
* setErrorHandler):
|
|
196
|
+
* - Re-throw ExpressionError / ExpressionExtensionError
|
|
197
|
+
* - Swallow everything else (TypeErrors, generic Errors, etc.)
|
|
198
|
+
*
|
|
199
|
+
* 2. **Outer layer (`wrappedCode` try-catch in `execute()`)** — Catches
|
|
200
|
+
* anything that escaped `E()` (e.g. re-thrown ExpressionErrors) and
|
|
201
|
+
* serializes it into a sentinel object so the host can reconstruct it.
|
|
202
|
+
*
|
|
203
|
+
* Inside the isolate, errors from host callbacks arrive as sentinel
|
|
204
|
+
* objects ({ __isError, name, message, ... }) rather than class instances,
|
|
205
|
+
* so we match by name instead of instanceof.
|
|
191
206
|
*
|
|
192
207
|
* @private
|
|
193
208
|
* @throws {Error} If context not initialized
|
|
@@ -199,208 +214,203 @@ export class IsolatedVmBridge {
|
|
|
199
214
|
await this.context.eval(`
|
|
200
215
|
if (typeof E === 'undefined') {
|
|
201
216
|
globalThis.E = function(error, _context) {
|
|
202
|
-
// Re-throw
|
|
203
|
-
|
|
217
|
+
// Re-throw ExpressionError / ExpressionExtensionError to match
|
|
218
|
+
// the legacy handler in expression.ts. Errors from host callbacks
|
|
219
|
+
// arrive as sentinels (not class instances), so check by name.
|
|
220
|
+
const name = error?.name;
|
|
221
|
+
if (name === 'ExpressionError' || name === 'ExpressionExtensionError') {
|
|
204
222
|
throw error;
|
|
205
223
|
}
|
|
206
|
-
// Swallow
|
|
207
|
-
|
|
208
|
-
return undefined;
|
|
209
|
-
}
|
|
210
|
-
throw error;
|
|
224
|
+
// Swallow everything else (TypeErrors, generic Errors, etc.)
|
|
225
|
+
return undefined;
|
|
211
226
|
};
|
|
212
227
|
}
|
|
213
228
|
`);
|
|
214
|
-
|
|
215
|
-
console.log('[IsolatedVmBridge] Error handler injected successfully');
|
|
216
|
-
}
|
|
229
|
+
this.logger.info('[IsolatedVmBridge] Error handler injected successfully');
|
|
217
230
|
}
|
|
218
231
|
/**
|
|
219
|
-
*
|
|
232
|
+
* Create an ivm.Reference callback for getting value/metadata at a path.
|
|
220
233
|
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
* 2. Initialize fresh workflow data references
|
|
224
|
-
* 3. Expose workflow properties to globalThis
|
|
225
|
-
*
|
|
226
|
-
* The reset function runs in the isolate and calls back to the host
|
|
227
|
-
* via ivm.Reference callbacks to fetch workflow data.
|
|
234
|
+
* Used by createDeepLazyProxy when accessing properties. Returns metadata
|
|
235
|
+
* markers for functions, arrays, and objects, or the primitive value directly.
|
|
228
236
|
*
|
|
237
|
+
* @param data - Current workflow data to use for callback responses
|
|
229
238
|
* @private
|
|
230
|
-
* @throws {Error} If context not initialized or reset fails
|
|
231
239
|
*/
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
240
|
+
createGetValueAtPathRef(data) {
|
|
241
|
+
return new (getIvm().Reference)((path) => {
|
|
242
|
+
try {
|
|
243
|
+
// Navigate to value
|
|
244
|
+
// Special-case: paths starting with ['$item', index] call data.$item(index)
|
|
245
|
+
// to get the sub-proxy for that item, then continue navigating the rest.
|
|
246
|
+
let value = data;
|
|
247
|
+
let startIndex = 0;
|
|
248
|
+
const itemFn = data.$item;
|
|
249
|
+
if (path.length >= 2 && path[0] === '$item' && typeof itemFn === 'function') {
|
|
250
|
+
const itemIndex = parseInt(path[1], 10);
|
|
251
|
+
if (!isNaN(itemIndex)) {
|
|
252
|
+
value = itemFn(itemIndex);
|
|
253
|
+
startIndex = 2;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
const dollarFn = data.$;
|
|
258
|
+
if (path.length >= 2 && path[0] === '$' && typeof dollarFn === 'function') {
|
|
259
|
+
value = dollarFn(path[1]);
|
|
260
|
+
startIndex = 2;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
for (let i = startIndex; i < path.length; i++) {
|
|
264
|
+
value = value?.[path[i]];
|
|
265
|
+
if (value === undefined || value === null) {
|
|
266
|
+
return value;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// Handle functions - return metadata marker
|
|
270
|
+
if (typeof value === 'function') {
|
|
271
|
+
const fnString = value.toString();
|
|
272
|
+
// Block native functions for security
|
|
273
|
+
if (fnString.includes('[native code]')) {
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
return { __isFunction: true, __name: path[path.length - 1] };
|
|
277
|
+
}
|
|
278
|
+
// Handle arrays - always lazy, only transfer length
|
|
279
|
+
if (Array.isArray(value)) {
|
|
280
|
+
return {
|
|
281
|
+
__isArray: true,
|
|
282
|
+
__length: value.length,
|
|
283
|
+
__data: null,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
// Handle objects - return metadata with keys
|
|
287
|
+
if (value !== null && typeof value === 'object') {
|
|
288
|
+
return {
|
|
289
|
+
__isObject: true,
|
|
290
|
+
__keys: Object.keys(value),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
// Primitive value
|
|
294
|
+
return value;
|
|
242
295
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
296
|
+
catch (err) {
|
|
297
|
+
return serializeError(err);
|
|
298
|
+
}
|
|
299
|
+
});
|
|
248
300
|
}
|
|
249
301
|
/**
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
* Creates three ivm.Reference callbacks that the runtime bundle uses
|
|
253
|
-
* to fetch data from the host process:
|
|
302
|
+
* Create an ivm.Reference callback for getting array elements at an index.
|
|
254
303
|
*
|
|
255
|
-
*
|
|
256
|
-
* - __getArrayElement: Returns individual array elements
|
|
257
|
-
* - __callFunctionAtPath: Executes functions in host context
|
|
258
|
-
*
|
|
259
|
-
* These callbacks are called synchronously from isolate proxy traps.
|
|
304
|
+
* Used by array proxy when accessing numeric indices.
|
|
260
305
|
*
|
|
261
306
|
* @param data - Current workflow data to use for callback responses
|
|
262
307
|
* @private
|
|
263
308
|
*/
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const itemIndex = parseInt(path[1], 10);
|
|
279
|
-
if (!isNaN(itemIndex)) {
|
|
280
|
-
value = itemFn(itemIndex);
|
|
281
|
-
startIndex = 2;
|
|
309
|
+
createGetArrayElementRef(data) {
|
|
310
|
+
return new (getIvm().Reference)((path, index) => {
|
|
311
|
+
try {
|
|
312
|
+
// Navigate to array
|
|
313
|
+
// Special-case: paths starting with ['$item', index] call data.$item(index)
|
|
314
|
+
let arr = data;
|
|
315
|
+
let startIndex = 0;
|
|
316
|
+
const itemFn = data.$item;
|
|
317
|
+
if (path.length >= 2 && path[0] === '$item' && typeof itemFn === 'function') {
|
|
318
|
+
const itemIndex = parseInt(path[1], 10);
|
|
319
|
+
if (!isNaN(itemIndex)) {
|
|
320
|
+
arr = itemFn(itemIndex);
|
|
321
|
+
startIndex = 2;
|
|
322
|
+
}
|
|
282
323
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
324
|
+
else {
|
|
325
|
+
const dollarFn = data.$;
|
|
326
|
+
if (path.length >= 2 && path[0] === '$' && typeof dollarFn === 'function') {
|
|
327
|
+
arr = dollarFn(path[1]);
|
|
328
|
+
startIndex = 2;
|
|
329
|
+
}
|
|
288
330
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
331
|
+
for (let i = startIndex; i < path.length; i++) {
|
|
332
|
+
arr = arr?.[path[i]];
|
|
333
|
+
if (arr === undefined || arr === null) {
|
|
334
|
+
return undefined;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (!Array.isArray(arr)) {
|
|
295
338
|
return undefined;
|
|
296
339
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
340
|
+
const element = arr[index];
|
|
341
|
+
// If element is object/array, return metadata
|
|
342
|
+
if (element !== null && typeof element === 'object') {
|
|
343
|
+
if (Array.isArray(element)) {
|
|
344
|
+
return {
|
|
345
|
+
__isArray: true,
|
|
346
|
+
__length: element.length,
|
|
347
|
+
__data: null,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
return {
|
|
351
|
+
__isObject: true,
|
|
352
|
+
__keys: Object.keys(element),
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
// Primitive element
|
|
356
|
+
return element;
|
|
306
357
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
return {
|
|
310
|
-
__isObject: true,
|
|
311
|
-
__keys: Object.keys(value),
|
|
312
|
-
};
|
|
358
|
+
catch (err) {
|
|
359
|
+
return serializeError(err);
|
|
313
360
|
}
|
|
314
|
-
// Primitive value
|
|
315
|
-
return value;
|
|
316
361
|
});
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Create an ivm.Reference callback for calling functions at a path.
|
|
365
|
+
*
|
|
366
|
+
* Used when expressions invoke functions from workflow data.
|
|
367
|
+
*
|
|
368
|
+
* @param data - Current workflow data to use for callback responses
|
|
369
|
+
* @private
|
|
370
|
+
*/
|
|
371
|
+
createCallFunctionAtPathRef(data) {
|
|
372
|
+
return new (getIvm().Reference)((path, ...args) => {
|
|
373
|
+
try {
|
|
374
|
+
// Navigate to function, tracking parent to preserve `this` context
|
|
375
|
+
let fn = data;
|
|
376
|
+
let parent = undefined;
|
|
377
|
+
let startIndex = 0;
|
|
378
|
+
const dollarFn = data.$;
|
|
379
|
+
if (path.length >= 2 && path[0] === '$' && typeof dollarFn === 'function') {
|
|
380
|
+
fn = dollarFn(path[1]);
|
|
329
381
|
startIndex = 2;
|
|
330
382
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
if (arr === undefined || arr === null) {
|
|
335
|
-
return undefined;
|
|
383
|
+
for (let i = startIndex; i < path.length; i++) {
|
|
384
|
+
parent = fn;
|
|
385
|
+
fn = fn?.[path[i]];
|
|
336
386
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
return undefined;
|
|
340
|
-
}
|
|
341
|
-
const element = arr[index];
|
|
342
|
-
// If element is object/array, return metadata
|
|
343
|
-
if (element !== null && typeof element === 'object') {
|
|
344
|
-
if (Array.isArray(element)) {
|
|
345
|
-
return {
|
|
346
|
-
__isArray: true,
|
|
347
|
-
__length: element.length,
|
|
348
|
-
__data: null,
|
|
349
|
-
};
|
|
387
|
+
if (typeof fn !== 'function') {
|
|
388
|
+
throw new Error(`${path.join('.')} is not a function`);
|
|
350
389
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
return element;
|
|
358
|
-
});
|
|
359
|
-
// Callback 3: Call function at path with arguments
|
|
360
|
-
// Used when expressions invoke functions from workflow data
|
|
361
|
-
const callFunctionAtPath = new (getIvm().Reference)((path, ...args) => {
|
|
362
|
-
// Navigate to function, tracking parent to preserve `this` context
|
|
363
|
-
let fn = data;
|
|
364
|
-
let parent = undefined;
|
|
365
|
-
for (const key of path) {
|
|
366
|
-
parent = fn;
|
|
367
|
-
fn = fn?.[key];
|
|
368
|
-
}
|
|
369
|
-
if (typeof fn !== 'function') {
|
|
370
|
-
throw new Error(`${path.join('.')} is not a function`);
|
|
390
|
+
// Block native functions for security (same check as getValueAtPath)
|
|
391
|
+
if (fn.toString().includes('[native code]')) {
|
|
392
|
+
throw new Error(`${path.join('.')} is a native function and cannot be called`);
|
|
393
|
+
}
|
|
394
|
+
// Execute function with parent as `this` to preserve method context
|
|
395
|
+
return fn.call(parent, ...args);
|
|
371
396
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
throw new Error(`${path.join('.')} is a native function and cannot be called`);
|
|
397
|
+
catch (err) {
|
|
398
|
+
return serializeError(err);
|
|
375
399
|
}
|
|
376
|
-
// Execute function with parent as `this` to preserve method context
|
|
377
|
-
return fn.call(parent, ...args);
|
|
378
400
|
});
|
|
379
|
-
// Release previous references before replacing to avoid accumulation
|
|
380
|
-
this.valueAtPathRef?.release();
|
|
381
|
-
this.arrayElementRef?.release();
|
|
382
|
-
this.callFunctionRef?.release();
|
|
383
|
-
// Store references so they can be released on the next call or on dispose()
|
|
384
|
-
this.valueAtPathRef = getValueAtPath;
|
|
385
|
-
this.arrayElementRef = getArrayElement;
|
|
386
|
-
this.callFunctionRef = callFunctionAtPath;
|
|
387
|
-
// Register all callbacks in isolate global context
|
|
388
|
-
this.context.global.setSync('__getValueAtPath', getValueAtPath);
|
|
389
|
-
this.context.global.setSync('__getArrayElement', getArrayElement);
|
|
390
|
-
this.context.global.setSync('__callFunctionAtPath', callFunctionAtPath);
|
|
391
|
-
if (this.config.debug) {
|
|
392
|
-
console.log('[IsolatedVmBridge] Callbacks registered successfully');
|
|
393
|
-
}
|
|
394
401
|
}
|
|
395
402
|
/**
|
|
396
403
|
* Execute JavaScript code in the isolated context.
|
|
397
404
|
*
|
|
398
405
|
* Flow:
|
|
399
|
-
* 1.
|
|
400
|
-
* 2.
|
|
401
|
-
*
|
|
402
|
-
*
|
|
403
|
-
*
|
|
406
|
+
* 1. Create three ivm.Reference callbacks scoped to the current data
|
|
407
|
+
* 2. Use evalClosureSync to run the code in a closure where $0/$1/$2
|
|
408
|
+
* are the callback references — no global mutable state
|
|
409
|
+
* 3. buildContext() inside the isolate creates a fresh evaluation context
|
|
410
|
+
* from the closure-scoped references
|
|
411
|
+
*
|
|
412
|
+
* Each call gets its own closure, so nested and concurrent evaluations
|
|
413
|
+
* cannot interfere with each other.
|
|
404
414
|
*
|
|
405
415
|
* @param code - JavaScript expression to evaluate
|
|
406
416
|
* @param data - Workflow data (e.g., { $json: {...}, $runIndex: 0 })
|
|
@@ -411,35 +421,58 @@ export class IsolatedVmBridge {
|
|
|
411
421
|
if (!this.initialized || !this.context) {
|
|
412
422
|
throw new Error('Bridge not initialized. Call initialize() first.');
|
|
413
423
|
}
|
|
424
|
+
const getValueAtPath = this.createGetValueAtPathRef(data);
|
|
425
|
+
const getArrayElement = this.createGetArrayElementRef(data);
|
|
426
|
+
const callFunctionAtPath = this.createCallFunctionAtPathRef(data);
|
|
414
427
|
try {
|
|
415
|
-
|
|
416
|
-
this.
|
|
417
|
-
// Step 2: Reset proxies for this evaluation
|
|
418
|
-
// This initializes $json, $binary, etc. as lazy proxies
|
|
419
|
-
this.resetDataProxies(options?.timezone);
|
|
420
|
-
// Step 3: Wrap transformed code so 'this' === __data in the isolate.
|
|
428
|
+
const timezone = options?.timezone ? JSON.stringify(options.timezone) : 'undefined';
|
|
429
|
+
// Wrap transformed code so 'this' === the closure-scoped context.
|
|
421
430
|
// Tournament generates: this.$json.email, this.$items(), etc.
|
|
422
|
-
//
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
431
|
+
// buildContext() creates a fresh context with lazy proxies from the
|
|
432
|
+
// closure-scoped callback references ($0, $1, $2) — no globals touched.
|
|
433
|
+
// The outer try-catch serializes errors into a sentinel object and returns
|
|
434
|
+
// it as the result. Errors from host callbacks arrive as sentinels already
|
|
435
|
+
// (via serializeError), so we pass them through. This avoids a round-trip
|
|
436
|
+
// callback and keeps Error reconstruction on the host side only.
|
|
437
|
+
const wrappedCode = `
|
|
438
|
+
var __ctx = buildContext($0, $1, $2, ${timezone});
|
|
439
|
+
try {
|
|
440
|
+
var __result = (function() {
|
|
441
|
+
${code}
|
|
442
|
+
}).call(__ctx);
|
|
443
|
+
return __prepareForTransfer(__result);
|
|
444
|
+
} catch(e) {
|
|
445
|
+
if (e && e.__isError) return e;
|
|
446
|
+
if (e == null) return { __isError: true, name: "Error", message: String(e), stack: "", extra: {} };
|
|
447
|
+
var extra = {};
|
|
448
|
+
for (var k in e) {
|
|
449
|
+
if (Object.prototype.hasOwnProperty.call(e, k) && k !== "name" && k !== "message" && k !== "stack") extra[k] = e[k];
|
|
450
|
+
}
|
|
451
|
+
return {
|
|
452
|
+
__isError: true,
|
|
453
|
+
name: e.name || "Error",
|
|
454
|
+
message: e.message || "",
|
|
455
|
+
stack: e.stack || "",
|
|
456
|
+
extra: extra
|
|
457
|
+
};
|
|
458
|
+
}`;
|
|
459
|
+
const result = this.context.evalClosureSync(wrappedCode, [getValueAtPath, getArrayElement, callFunctionAtPath], { result: { copy: true }, timeout: this.config.timeout });
|
|
460
|
+
if (isErrorSentinel(result)) {
|
|
461
|
+
throw this.reconstructError(result);
|
|
439
462
|
}
|
|
463
|
+
this.logger.debug('[IsolatedVmBridge] Expression executed successfully');
|
|
440
464
|
return result;
|
|
441
465
|
}
|
|
442
466
|
catch (error) {
|
|
467
|
+
// Re-throw reconstructed errors as-is.
|
|
468
|
+
// Note: TypeError is intentionally NOT included here — the isolate's
|
|
469
|
+
// E() handler swallows TypeErrors (failed attack attempts return undefined),
|
|
470
|
+
// so TypeErrors from host callbacks should also go through the generic
|
|
471
|
+
// wrapping for consistent behavior.
|
|
472
|
+
if (error instanceof Error &&
|
|
473
|
+
(error.name === 'ExpressionError' || error.name === 'ExpressionExtensionError')) {
|
|
474
|
+
throw error;
|
|
475
|
+
}
|
|
443
476
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
444
477
|
if (errorMessage.includes('Script execution timed out')) {
|
|
445
478
|
throw new TimeoutError(`Expression timed out after ${this.config.timeout}ms`, {});
|
|
@@ -449,6 +482,29 @@ export class IsolatedVmBridge {
|
|
|
449
482
|
}
|
|
450
483
|
throw new Error(`Expression evaluation failed: ${errorMessage}`);
|
|
451
484
|
}
|
|
485
|
+
finally {
|
|
486
|
+
getValueAtPath.release();
|
|
487
|
+
getArrayElement.release();
|
|
488
|
+
callFunctionAtPath.release();
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Reconstruct an error from serialized isolate data.
|
|
493
|
+
*
|
|
494
|
+
* Maps error names back to their host-side classes and restores
|
|
495
|
+
* custom properties that would otherwise be lost crossing the boundary.
|
|
496
|
+
*/
|
|
497
|
+
reconstructError(data) {
|
|
498
|
+
const error = new Error(data.message);
|
|
499
|
+
error.name = data.name || 'Error';
|
|
500
|
+
if (data.stack) {
|
|
501
|
+
error.stack = data.stack;
|
|
502
|
+
}
|
|
503
|
+
// Restore custom properties transferred via copy: true
|
|
504
|
+
if (data.extra) {
|
|
505
|
+
Object.assign(error, data.extra);
|
|
506
|
+
}
|
|
507
|
+
return error;
|
|
452
508
|
}
|
|
453
509
|
/**
|
|
454
510
|
* Dispose of the isolate and free resources.
|
|
@@ -463,16 +519,9 @@ export class IsolatedVmBridge {
|
|
|
463
519
|
if (!this.isolate.isDisposed) {
|
|
464
520
|
this.isolate.dispose();
|
|
465
521
|
}
|
|
466
|
-
// Release callback references
|
|
467
|
-
this.valueAtPathRef?.release();
|
|
468
|
-
this.arrayElementRef?.release();
|
|
469
|
-
this.callFunctionRef?.release();
|
|
470
522
|
this.disposed = true;
|
|
471
523
|
this.initialized = false;
|
|
472
|
-
this.
|
|
473
|
-
if (this.config.debug) {
|
|
474
|
-
console.log('[IsolatedVmBridge] Disposed');
|
|
475
|
-
}
|
|
524
|
+
this.logger.info('[IsolatedVmBridge] Disposed');
|
|
476
525
|
}
|
|
477
526
|
/**
|
|
478
527
|
* Check if the bridge has been disposed.
|
|
@@ -480,7 +529,7 @@ export class IsolatedVmBridge {
|
|
|
480
529
|
* @returns true if disposed, false otherwise
|
|
481
530
|
*/
|
|
482
531
|
isDisposed() {
|
|
483
|
-
return this.disposed;
|
|
532
|
+
return this.disposed || this.isolate.isDisposed;
|
|
484
533
|
}
|
|
485
534
|
}
|
|
486
535
|
//# sourceMappingURL=isolated-vm-bridge.js.map
|