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