@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.
Files changed (94) hide show
  1. package/dist/bundle/runtime.esm.js +17 -17
  2. package/dist/bundle/runtime.esm.js.map +4 -4
  3. package/dist/bundle/runtime.iife.js +17 -17
  4. package/dist/bundle/runtime.iife.js.map +4 -4
  5. package/dist/cjs/bridge/isolated-vm-bridge.d.ts +48 -34
  6. package/dist/cjs/bridge/isolated-vm-bridge.d.ts.map +1 -1
  7. package/dist/cjs/bridge/isolated-vm-bridge.js +285 -236
  8. package/dist/cjs/bridge/isolated-vm-bridge.js.map +1 -1
  9. package/dist/cjs/build.tsbuildinfo +1 -1
  10. package/dist/cjs/evaluator/expression-evaluator.d.ts +8 -1
  11. package/dist/cjs/evaluator/expression-evaluator.d.ts.map +1 -1
  12. package/dist/cjs/evaluator/expression-evaluator.js +62 -6
  13. package/dist/cjs/evaluator/expression-evaluator.js.map +1 -1
  14. package/dist/cjs/extensions/boolean-extensions.d.ts +1 -1
  15. package/dist/cjs/extensions/object-extensions.d.ts.map +1 -1
  16. package/dist/cjs/extensions/object-extensions.js +8 -4
  17. package/dist/cjs/extensions/object-extensions.js.map +1 -1
  18. package/dist/cjs/index.d.ts +2 -1
  19. package/dist/cjs/index.d.ts.map +1 -1
  20. package/dist/cjs/index.js +4 -2
  21. package/dist/cjs/index.js.map +1 -1
  22. package/dist/cjs/pool/isolate-pool.d.ts +35 -0
  23. package/dist/cjs/pool/isolate-pool.d.ts.map +1 -0
  24. package/dist/cjs/pool/isolate-pool.js +123 -0
  25. package/dist/cjs/pool/isolate-pool.js.map +1 -0
  26. package/dist/cjs/runtime/context.d.ts +18 -0
  27. package/dist/cjs/runtime/context.d.ts.map +1 -0
  28. package/dist/cjs/runtime/{reset.js → context.js} +95 -100
  29. package/dist/cjs/runtime/context.js.map +1 -0
  30. package/dist/cjs/runtime/index.d.ts +1 -19
  31. package/dist/cjs/runtime/index.d.ts.map +1 -1
  32. package/dist/cjs/runtime/index.js +3 -5
  33. package/dist/cjs/runtime/index.js.map +1 -1
  34. package/dist/cjs/runtime/lazy-proxy.d.ts +28 -3
  35. package/dist/cjs/runtime/lazy-proxy.d.ts.map +1 -1
  36. package/dist/cjs/runtime/lazy-proxy.js +84 -16
  37. package/dist/cjs/runtime/lazy-proxy.js.map +1 -1
  38. package/dist/cjs/types/bridge.d.ts +12 -7
  39. package/dist/cjs/types/bridge.d.ts.map +1 -1
  40. package/dist/cjs/types/bridge.js +7 -1
  41. package/dist/cjs/types/bridge.js.map +1 -1
  42. package/dist/cjs/types/evaluator.d.ts +24 -14
  43. package/dist/cjs/types/evaluator.d.ts.map +1 -1
  44. package/dist/cjs/types/evaluator.js.map +1 -1
  45. package/dist/cjs/types/index.d.ts +1 -1
  46. package/dist/cjs/types/index.d.ts.map +1 -1
  47. package/dist/esm/bridge/isolated-vm-bridge.d.ts +48 -34
  48. package/dist/esm/bridge/isolated-vm-bridge.d.ts.map +1 -1
  49. package/dist/esm/bridge/isolated-vm-bridge.js +285 -236
  50. package/dist/esm/bridge/isolated-vm-bridge.js.map +1 -1
  51. package/dist/esm/build.tsbuildinfo +1 -1
  52. package/dist/esm/evaluator/expression-evaluator.d.ts +8 -1
  53. package/dist/esm/evaluator/expression-evaluator.d.ts.map +1 -1
  54. package/dist/esm/evaluator/expression-evaluator.js +61 -5
  55. package/dist/esm/evaluator/expression-evaluator.js.map +1 -1
  56. package/dist/esm/extensions/object-extensions.d.ts.map +1 -1
  57. package/dist/esm/extensions/object-extensions.js +8 -4
  58. package/dist/esm/extensions/object-extensions.js.map +1 -1
  59. package/dist/esm/index.d.ts +2 -1
  60. package/dist/esm/index.d.ts.map +1 -1
  61. package/dist/esm/index.js +1 -0
  62. package/dist/esm/index.js.map +1 -1
  63. package/dist/esm/pool/isolate-pool.d.ts +35 -0
  64. package/dist/esm/pool/isolate-pool.d.ts.map +1 -0
  65. package/dist/esm/pool/isolate-pool.js +107 -0
  66. package/dist/esm/pool/isolate-pool.js.map +1 -0
  67. package/dist/esm/runtime/context.d.ts +18 -0
  68. package/dist/esm/runtime/context.d.ts.map +1 -0
  69. package/dist/esm/runtime/{reset.js → context.js} +95 -100
  70. package/dist/esm/runtime/context.js.map +1 -0
  71. package/dist/esm/runtime/index.d.ts +1 -19
  72. package/dist/esm/runtime/index.d.ts.map +1 -1
  73. package/dist/esm/runtime/index.js +2 -4
  74. package/dist/esm/runtime/index.js.map +1 -1
  75. package/dist/esm/runtime/lazy-proxy.d.ts +28 -3
  76. package/dist/esm/runtime/lazy-proxy.d.ts.map +1 -1
  77. package/dist/esm/runtime/lazy-proxy.js +83 -16
  78. package/dist/esm/runtime/lazy-proxy.js.map +1 -1
  79. package/dist/esm/types/bridge.d.ts +12 -7
  80. package/dist/esm/types/bridge.d.ts.map +1 -1
  81. package/dist/esm/types/bridge.js +7 -1
  82. package/dist/esm/types/bridge.js.map +1 -1
  83. package/dist/esm/types/evaluator.d.ts +24 -14
  84. package/dist/esm/types/evaluator.d.ts.map +1 -1
  85. package/dist/esm/types/evaluator.js.map +1 -1
  86. package/dist/esm/types/index.d.ts +1 -1
  87. package/dist/esm/types/index.d.ts.map +1 -1
  88. package/package.json +10 -7
  89. package/dist/cjs/runtime/reset.d.ts +0 -16
  90. package/dist/cjs/runtime/reset.d.ts.map +0 -1
  91. package/dist/cjs/runtime/reset.js.map +0 -1
  92. package/dist/esm/runtime/reset.d.ts +0 -16
  93. package/dist/esm/runtime/reset.d.ts.map +0 -1
  94. 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
- // Script compilation cache for performance
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
- if (this.config.debug) {
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
- * - __data object initialization
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, resetDataProxies, __data)
184
+ // This makes all exported globals available (DateTime, extend, extendOptional, SafeObject, SafeError, createDeepLazyProxy, buildContext)
174
185
  await this.context.eval(runtimeBundle);
175
- if (this.config.debug) {
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
- if (this.config.debug) {
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 hasResetFunction = await this.context.eval('typeof resetDataProxies !== "undefined"');
213
- if (!hasProxyCreator || !hasData || !hasSafeObject || !hasSafeError || !hasResetFunction) {
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}, __data=${hasData}, ` +
221
+ `createDeepLazyProxy=${hasProxyCreator}, ` +
216
222
  `SafeObject=${hasSafeObject}, SafeError=${hasSafeError}, ` +
217
- `resetDataProxies=${hasResetFunction}`);
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
- * Tournament wraps expressions with try-catch that calls E(error, this).
232
- * This handler:
233
- * - Re-throws security violations from __sanitize
234
- * - Swallows TypeErrors (failed attack attempts return undefined)
235
- * - Re-throws all other errors
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 security violations from __sanitize
248
- if (error && error.message && error.message.includes('due to security concerns')) {
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 TypeErrors (failed attack attempts return undefined)
252
- if (error instanceof TypeError) {
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
- if (this.config.debug) {
260
- console.log('[IsolatedVmBridge] Error handler injected successfully');
261
- }
274
+ this.logger.info('[IsolatedVmBridge] Error handler injected successfully');
262
275
  }
263
276
  /**
264
- * Reset data proxies in the isolate context.
277
+ * Create an ivm.Reference callback for getting value/metadata at a path.
265
278
  *
266
- * This method should be called before each execute() to:
267
- * 1. Clear proxy caches from previous evaluations
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
- resetDataProxies(timezone) {
278
- if (!this.resetDataProxiesRef) {
279
- throw new Error('Context not initialized');
280
- }
281
- try {
282
- this.resetDataProxiesRef.applySync(null, [timezone ?? null], {
283
- arguments: { copy: true },
284
- });
285
- if (this.config.debug) {
286
- console.log('[IsolatedVmBridge] Data proxies reset successfully');
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
- catch (error) {
290
- const errorMessage = error instanceof Error ? error.message : String(error);
291
- throw new Error(`Failed to reset data proxies: ${errorMessage}`);
292
- }
341
+ catch (err) {
342
+ return serializeError(err);
343
+ }
344
+ });
293
345
  }
294
346
  /**
295
- * Register callback functions for cross-isolate communication.
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
- * - __getValueAtPath: Returns metadata or primitive for a property path
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
- registerCallbacks(data) {
310
- if (!this.context) {
311
- throw new Error('Context not initialized');
312
- }
313
- // Callback 1: Get value/metadata at path
314
- // Used by createDeepLazyProxy when accessing properties
315
- const getValueAtPath = new (getIvm().Reference)((path) => {
316
- // Navigate to value
317
- // Special-case: paths starting with ['$item', index] call data.$item(index)
318
- // to get the sub-proxy for that item, then continue navigating the rest.
319
- let value = data;
320
- let startIndex = 0;
321
- const itemFn = data.$item;
322
- if (path.length >= 2 && path[0] === '$item' && typeof itemFn === 'function') {
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
- for (let i = startIndex; i < path.length; i++) {
330
- value = value?.[path[i]];
331
- if (value === undefined || value === null) {
332
- return value;
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
- // Handle functions - return metadata marker
336
- if (typeof value === 'function') {
337
- const fnString = value.toString();
338
- // Block native functions for security
339
- if (fnString.includes('[native code]')) {
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
- return { __isFunction: true, __name: path[path.length - 1] };
343
- }
344
- // Handle arrays - always lazy, only transfer length
345
- if (Array.isArray(value)) {
346
- return {
347
- __isArray: true,
348
- __length: value.length,
349
- __data: null,
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
- // Handle objects - return metadata with keys
353
- if (value !== null && typeof value === 'object') {
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
- // Callback 2: Get array element at index
363
- // Used by array proxy when accessing numeric indices
364
- const getArrayElement = new (getIvm().Reference)((path, index) => {
365
- // Navigate to array
366
- // Special-case: paths starting with ['$item', index] call data.$item(index)
367
- let arr = data;
368
- let startIndex = 0;
369
- const itemFn = data.$item;
370
- if (path.length >= 2 && path[0] === '$item' && typeof itemFn === 'function') {
371
- const itemIndex = parseInt(path[1], 10);
372
- if (!isNaN(itemIndex)) {
373
- arr = itemFn(itemIndex);
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
- for (let i = startIndex; i < path.length; i++) {
378
- arr = arr?.[path[i]];
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
- if (!Array.isArray(arr)) {
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
- return {
397
- __isObject: true,
398
- __keys: Object.keys(element),
399
- };
400
- }
401
- // Primitive element
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
- // Block native functions for security (same check as getValueAtPath)
418
- if (fn.toString().includes('[native code]')) {
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. Register callbacks as ivm.Reference for cross-isolate communication
445
- * 2. Call resetDataProxies() to initialize workflow data proxies
446
- * 3. Compile script (with caching for performance)
447
- * 4. Execute with timeout enforcement
448
- * 5. Return result (copied from isolate)
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
- // Step 1: Register callbacks with current data context
461
- this.registerCallbacks(data);
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
- // __data has $json, $items, etc. as lazy proxies (set in resetDataProxies).
468
- const wrappedCode = `(function() { var __result = (function() {\n${code}\n}).call(__data); return __prepareForTransfer(__result); })()`;
469
- let script = this.scriptCache.get(code);
470
- if (!script) {
471
- script = this.isolate.compileScriptSync(wrappedCode);
472
- this.scriptCache.set(code, script);
473
- if (this.config.debug) {
474
- console.log('[IsolatedVmBridge] Script compiled and cached');
475
- }
476
- }
477
- // Step 4: Execute with timeout and copy result back
478
- const result = script.runSync(this.context, {
479
- timeout: this.config.timeout,
480
- copy: true,
481
- });
482
- if (this.config.debug) {
483
- console.log('[IsolatedVmBridge] Expression executed successfully');
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.scriptCache.clear();
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;