@n8n/expression-runtime 0.22.1 → 0.23.1

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 (109) hide show
  1. package/dist/cjs/bridge/bridge-messages.d.ts +8 -8
  2. package/dist/cjs/bridge/bridge-messages.js +234 -244
  3. package/dist/cjs/bridge/bridge-messages.js.map +1 -1
  4. package/dist/cjs/bridge/isolated-vm-bridge.d.ts.map +1 -1
  5. package/dist/cjs/bridge/isolated-vm-bridge.js +644 -654
  6. package/dist/cjs/bridge/isolated-vm-bridge.js.map +1 -1
  7. package/dist/cjs/build.tsbuildinfo +1 -1
  8. package/dist/cjs/evaluator/error-classification.js +15 -25
  9. package/dist/cjs/evaluator/error-classification.js.map +1 -1
  10. package/dist/cjs/evaluator/expression-evaluator.d.ts.map +1 -1
  11. package/dist/cjs/evaluator/expression-evaluator.js +152 -162
  12. package/dist/cjs/evaluator/expression-evaluator.js.map +1 -1
  13. package/dist/cjs/evaluator/lru-cache.d.ts.map +1 -1
  14. package/dist/cjs/evaluator/lru-cache.js +35 -45
  15. package/dist/cjs/evaluator/lru-cache.js.map +1 -1
  16. package/dist/cjs/extensions/array-extensions.js +589 -599
  17. package/dist/cjs/extensions/array-extensions.js.map +1 -1
  18. package/dist/cjs/extensions/boolean-extensions.js +38 -48
  19. package/dist/cjs/extensions/boolean-extensions.js.map +1 -1
  20. package/dist/cjs/extensions/date-extensions.js +500 -510
  21. package/dist/cjs/extensions/date-extensions.js.map +1 -1
  22. package/dist/cjs/extensions/expression-extension-error.d.ts.map +1 -1
  23. package/dist/cjs/extensions/expression-extension-error.js +12 -22
  24. package/dist/cjs/extensions/expression-extension-error.js.map +1 -1
  25. package/dist/cjs/extensions/extend.js +162 -172
  26. package/dist/cjs/extensions/extend.js.map +1 -1
  27. package/dist/cjs/extensions/extensions.js +2 -12
  28. package/dist/cjs/extensions/function-extensions.d.ts +10 -18
  29. package/dist/cjs/extensions/function-extensions.d.ts.map +1 -1
  30. package/dist/cjs/extensions/function-extensions.js +78 -88
  31. package/dist/cjs/extensions/function-extensions.js.map +1 -1
  32. package/dist/cjs/extensions/number-extensions.js +225 -235
  33. package/dist/cjs/extensions/number-extensions.js.map +1 -1
  34. package/dist/cjs/extensions/object-extensions.js +286 -296
  35. package/dist/cjs/extensions/object-extensions.js.map +1 -1
  36. package/dist/cjs/extensions/string-extensions.d.ts +1 -1
  37. package/dist/cjs/extensions/string-extensions.d.ts.map +1 -1
  38. package/dist/cjs/extensions/string-extensions.js +758 -768
  39. package/dist/cjs/extensions/string-extensions.js.map +1 -1
  40. package/dist/cjs/extensions/utils.d.ts.map +1 -1
  41. package/dist/cjs/extensions/utils.js +28 -38
  42. package/dist/cjs/extensions/utils.js.map +1 -1
  43. package/dist/cjs/index.js +32 -42
  44. package/dist/cjs/index.js.map +1 -1
  45. package/dist/cjs/observability/metrics.d.ts +27 -27
  46. package/dist/cjs/observability/metrics.d.ts.map +1 -1
  47. package/dist/cjs/observability/metrics.js +59 -69
  48. package/dist/cjs/observability/metrics.js.map +1 -1
  49. package/dist/cjs/observability/noop-provider.js +28 -38
  50. package/dist/cjs/observability/noop-provider.js.map +1 -1
  51. package/dist/cjs/pool/idle-scaling-pool.d.ts.map +1 -1
  52. package/dist/cjs/pool/idle-scaling-pool.js +134 -144
  53. package/dist/cjs/pool/idle-scaling-pool.js.map +1 -1
  54. package/dist/cjs/pool/isolate-pool.d.ts.map +1 -1
  55. package/dist/cjs/pool/isolate-pool.js +101 -111
  56. package/dist/cjs/pool/isolate-pool.js.map +1 -1
  57. package/dist/cjs/runtime/context.js +418 -428
  58. package/dist/cjs/runtime/context.js.map +1 -1
  59. package/dist/cjs/runtime/index.js +26 -36
  60. package/dist/cjs/runtime/index.js.map +1 -1
  61. package/dist/cjs/runtime/jmespath.js +72 -82
  62. package/dist/cjs/runtime/jmespath.js.map +1 -1
  63. package/dist/cjs/runtime/lazy-proxy.js +238 -248
  64. package/dist/cjs/runtime/lazy-proxy.js.map +1 -1
  65. package/dist/cjs/runtime/safe-globals.d.ts.map +1 -1
  66. package/dist/cjs/runtime/safe-globals.js +134 -144
  67. package/dist/cjs/runtime/safe-globals.js.map +1 -1
  68. package/dist/cjs/runtime/serialize.js +54 -64
  69. package/dist/cjs/runtime/serialize.js.map +1 -1
  70. package/dist/cjs/runtime/utils.js +20 -30
  71. package/dist/cjs/runtime/utils.js.map +1 -1
  72. package/dist/cjs/types/bridge.js +15 -25
  73. package/dist/cjs/types/bridge.js.map +1 -1
  74. package/dist/cjs/types/evaluator.d.ts.map +1 -1
  75. package/dist/cjs/types/evaluator.js +35 -45
  76. package/dist/cjs/types/evaluator.js.map +1 -1
  77. package/dist/cjs/types/index.js +14 -24
  78. package/dist/cjs/types/index.js.map +1 -1
  79. package/dist/cjs/types/runtime.d.ts.map +1 -1
  80. package/dist/cjs/types/runtime.js +27 -37
  81. package/dist/cjs/types/runtime.js.map +1 -1
  82. package/dist/esm/bridge/bridge-messages.d.ts +8 -8
  83. package/dist/esm/bridge/isolated-vm-bridge.d.ts.map +1 -1
  84. package/dist/esm/bridge/isolated-vm-bridge.js.map +1 -1
  85. package/dist/esm/build.tsbuildinfo +1 -1
  86. package/dist/esm/evaluator/expression-evaluator.d.ts.map +1 -1
  87. package/dist/esm/evaluator/lru-cache.d.ts.map +1 -1
  88. package/dist/esm/evaluator/lru-cache.js.map +1 -1
  89. package/dist/esm/extensions/expression-extension-error.d.ts.map +1 -1
  90. package/dist/esm/extensions/function-extensions.d.ts +10 -18
  91. package/dist/esm/extensions/function-extensions.d.ts.map +1 -1
  92. package/dist/esm/extensions/number-extensions.js.map +1 -1
  93. package/dist/esm/extensions/string-extensions.d.ts +1 -1
  94. package/dist/esm/extensions/string-extensions.d.ts.map +1 -1
  95. package/dist/esm/extensions/string-extensions.js.map +1 -1
  96. package/dist/esm/extensions/utils.d.ts.map +1 -1
  97. package/dist/esm/observability/metrics.d.ts +27 -27
  98. package/dist/esm/observability/metrics.d.ts.map +1 -1
  99. package/dist/esm/pool/idle-scaling-pool.d.ts.map +1 -1
  100. package/dist/esm/pool/idle-scaling-pool.js.map +1 -1
  101. package/dist/esm/pool/isolate-pool.d.ts.map +1 -1
  102. package/dist/esm/pool/isolate-pool.js.map +1 -1
  103. package/dist/esm/runtime/lazy-proxy.js.map +1 -1
  104. package/dist/esm/runtime/safe-globals.d.ts.map +1 -1
  105. package/dist/esm/types/evaluator.d.ts.map +1 -1
  106. package/dist/esm/types/evaluator.js.map +1 -1
  107. package/dist/esm/types/runtime.d.ts.map +1 -1
  108. package/dist/esm/types/runtime.js.map +1 -1
  109. package/package.json +7 -6
@@ -1,3 +1,4 @@
1
+ "use strict";
1
2
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
2
3
  if (k2 === undefined) k2 = k;
3
4
  var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -31,233 +32,223 @@ var __importStar = (this && this.__importStar) || (function () {
31
32
  return result;
32
33
  };
33
34
  })();
34
- (function (factory) {
35
- if (typeof module === "object" && typeof module.exports === "object") {
36
- var v = factory(require, exports);
37
- if (v !== undefined) module.exports = v;
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.IsolatedVmBridge = void 0;
37
+ const promises_1 = require("node:fs/promises");
38
+ const path = __importStar(require("node:path"));
39
+ const types_1 = require("../types");
40
+ const bridge_messages_1 = require("./bridge-messages");
41
+ let _ivm = null;
42
+ function getIvm() {
43
+ if (!_ivm) {
44
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
45
+ _ivm = require('isolated-vm');
38
46
  }
39
- else if (typeof define === "function" && define.amd) {
40
- define(["require", "exports", "node:fs/promises", "node:path", "../types", "./bridge-messages"], factory);
47
+ return _ivm;
48
+ }
49
+ const BUNDLE_RELATIVE_PATH = path.join('dist', 'bundle', 'runtime.iife.js');
50
+ /** Check if a value is an error sentinel returned by serializeError. */
51
+ function isErrorSentinel(value) {
52
+ return (typeof value === 'object' &&
53
+ value !== null &&
54
+ value.__isError === true);
55
+ }
56
+ /**
57
+ * Serialize an error into a transferable metadata object.
58
+ *
59
+ * Host-side callbacks (getValueAtPath, etc.) catch errors and return this
60
+ * sentinel instead of letting the error cross the isolate boundary (which
61
+ * strips custom class identity and properties). The isolate-side proxy
62
+ * detects __isError and reconstructs a proper Error to throw.
63
+ */
64
+ function serializeError(err) {
65
+ if (err instanceof Error) {
66
+ const extra = Object.fromEntries(Object.entries(err).filter(([key]) => key !== 'name' && key !== 'message' && key !== 'stack'));
67
+ return {
68
+ __isError: true,
69
+ name: err.name,
70
+ message: err.message,
71
+ stack: err.stack,
72
+ extra,
73
+ };
41
74
  }
42
- })(function (require, exports) {
43
- "use strict";
44
- Object.defineProperty(exports, "__esModule", { value: true });
45
- exports.IsolatedVmBridge = void 0;
46
- const promises_1 = require("node:fs/promises");
47
- const path = __importStar(require("node:path"));
48
- const types_1 = require("../types");
49
- const bridge_messages_1 = require("./bridge-messages");
50
- let _ivm = null;
51
- function getIvm() {
52
- if (!_ivm) {
53
- // eslint-disable-next-line @typescript-eslint/no-require-imports
54
- _ivm = require('isolated-vm');
75
+ return { __isError: true, name: 'Error', message: String(err), extra: {} };
76
+ }
77
+ /**
78
+ * Read the runtime IIFE bundle by walking up from `__dirname` until
79
+ * `dist/bundle/runtime.iife.js` is found.
80
+ *
81
+ * This works regardless of where the compiled output lives:
82
+ * - `src/bridge/` (vitest running against source)
83
+ * - `dist/cjs/bridge/` (CJS build)
84
+ */
85
+ async function readRuntimeBundle() {
86
+ let dir = __dirname;
87
+ while (dir !== path.dirname(dir)) {
88
+ try {
89
+ return await (0, promises_1.readFile)(path.join(dir, BUNDLE_RELATIVE_PATH), 'utf-8');
55
90
  }
56
- return _ivm;
91
+ catch { }
92
+ dir = path.dirname(dir);
57
93
  }
58
- const BUNDLE_RELATIVE_PATH = path.join('dist', 'bundle', 'runtime.iife.js');
59
- /** Check if a value is an error sentinel returned by serializeError. */
60
- function isErrorSentinel(value) {
61
- return (typeof value === 'object' &&
62
- value !== null &&
63
- value.__isError === true);
94
+ throw new Error(`Could not find runtime bundle (${BUNDLE_RELATIVE_PATH}) in any parent of ${__dirname}`);
95
+ }
96
+ /**
97
+ * IsolatedVmBridge - Runtime bridge using isolated-vm for secure expression evaluation.
98
+ *
99
+ * This bridge creates a V8 isolate with:
100
+ * - Hard memory limit (128MB default)
101
+ * - No access to Node.js APIs
102
+ * - Timeout enforcement
103
+ * - Complete isolation from host process
104
+ *
105
+ * Context reuse pattern: Create isolate/context once, reset state between evaluations.
106
+ */
107
+ class IsolatedVmBridge {
108
+ isolate;
109
+ context;
110
+ initialized = false;
111
+ disposed = false;
112
+ config;
113
+ logger;
114
+ constructor(config = {}) {
115
+ this.config = {
116
+ ...types_1.DEFAULT_BRIDGE_CONFIG,
117
+ ...config,
118
+ };
119
+ this.logger = this.config.logger;
120
+ // Create isolate with memory limit
121
+ // Note: memoryLimit is in MB
122
+ this.isolate = new (getIvm().Isolate)({ memoryLimit: this.config.memoryLimit });
64
123
  }
65
124
  /**
66
- * Serialize an error into a transferable metadata object.
125
+ * Initialize the isolate and create execution context.
126
+ *
127
+ * Steps:
128
+ * 1. Create context
129
+ * 2. Set up basic globals (global reference)
130
+ * 3. Load runtime bundle (DateTime, extend, proxy system)
131
+ * 4. Verify proxy system
67
132
  *
68
- * Host-side callbacks (getValueAtPath, etc.) catch errors and return this
69
- * sentinel instead of letting the error cross the isolate boundary (which
70
- * strips custom class identity and properties). The isolate-side proxy
71
- * detects __isError and reconstructs a proper Error to throw.
133
+ * Must be called before execute().
72
134
  */
73
- function serializeError(err) {
74
- if (err instanceof Error) {
75
- const extra = Object.fromEntries(Object.entries(err).filter(([key]) => key !== 'name' && key !== 'message' && key !== 'stack'));
76
- return {
77
- __isError: true,
78
- name: err.name,
79
- message: err.message,
80
- stack: err.stack,
81
- extra,
82
- };
135
+ async initialize() {
136
+ if (this.initialized) {
137
+ return;
83
138
  }
84
- return { __isError: true, name: 'Error', message: String(err), extra: {} };
139
+ // Create context in the isolate
140
+ this.context = await this.isolate.createContext();
141
+ // Set up basic globals
142
+ // jail is a reference to the context's global object
143
+ const jail = this.context.global;
144
+ // Set 'global' to reference itself (pattern from POC)
145
+ // This allows code in isolate to access 'global.something'
146
+ await jail.set('global', jail.derefInto());
147
+ // Load runtime bundle (DateTime, extend, SafeObject, proxy system)
148
+ await this.loadVendorLibraries();
149
+ // Verify proxy system loaded correctly
150
+ await this.verifyProxySystem();
151
+ // Inject E() error handler needed by tournament-generated try-catch code
152
+ await this.injectErrorHandler();
153
+ this.initialized = true;
154
+ this.logger.debug('[IsolatedVmBridge] Initialized successfully');
85
155
  }
86
156
  /**
87
- * Read the runtime IIFE bundle by walking up from `__dirname` until
88
- * `dist/bundle/runtime.iife.js` is found.
157
+ * Load runtime bundle into the isolate.
89
158
  *
90
- * This works regardless of where the compiled output lives:
91
- * - `src/bridge/` (vitest running against source)
92
- * - `dist/cjs/bridge/` (CJS build)
159
+ * The runtime bundle includes:
160
+ * - DateTime, extend, extendOptional (expression engine globals)
161
+ * - SafeObject and SafeError wrappers
162
+ * - createDeepLazyProxy function
163
+ * - buildContext function
164
+ *
165
+ * @private
166
+ * @throws {Error} If context not initialized or bundle loading fails
93
167
  */
94
- async function readRuntimeBundle() {
95
- let dir = __dirname;
96
- while (dir !== path.dirname(dir)) {
97
- try {
98
- return await (0, promises_1.readFile)(path.join(dir, BUNDLE_RELATIVE_PATH), 'utf-8');
168
+ async loadVendorLibraries() {
169
+ if (!this.context) {
170
+ throw new Error('Context not initialized');
171
+ }
172
+ try {
173
+ // Load runtime bundle (includes vendor libraries + proxy system)
174
+ const runtimeBundle = await readRuntimeBundle();
175
+ // Evaluate bundle in isolate context
176
+ // This makes all exported globals available (DateTime, extend, extendOptional, SafeObject, SafeError, createDeepLazyProxy, buildContext)
177
+ await this.context.eval(runtimeBundle);
178
+ this.logger.debug('[IsolatedVmBridge] Runtime bundle loaded');
179
+ // Verify vendor libraries loaded correctly
180
+ const hasDateTime = await this.context.eval('typeof DateTime !== "undefined"');
181
+ const hasExtend = await this.context.eval('typeof extend !== "undefined"');
182
+ if (!hasDateTime || !hasExtend) {
183
+ throw new Error(`Library verification failed: DateTime=${hasDateTime}, extend=${hasExtend}`);
99
184
  }
100
- catch { }
101
- dir = path.dirname(dir);
185
+ this.logger.debug('[IsolatedVmBridge] Vendor libraries verified successfully');
186
+ }
187
+ catch (error) {
188
+ const errorMessage = error instanceof Error ? error.message : String(error);
189
+ throw new Error(`Failed to load runtime bundle: ${errorMessage}`);
102
190
  }
103
- throw new Error(`Could not find runtime bundle (${BUNDLE_RELATIVE_PATH}) in any parent of ${__dirname}`);
104
191
  }
105
192
  /**
106
- * IsolatedVmBridge - Runtime bridge using isolated-vm for secure expression evaluation.
193
+ * Verify the proxy system loaded correctly.
107
194
  *
108
- * This bridge creates a V8 isolate with:
109
- * - Hard memory limit (128MB default)
110
- * - No access to Node.js APIs
111
- * - Timeout enforcement
112
- * - Complete isolation from host process
195
+ * The proxy system is loaded as part of the runtime bundle in loadVendorLibraries().
196
+ * This method verifies all required components are available.
113
197
  *
114
- * Context reuse pattern: Create isolate/context once, reset state between evaluations.
198
+ * @private
199
+ * @throws {Error} If context not initialized or proxy system verification fails
115
200
  */
116
- class IsolatedVmBridge {
117
- isolate;
118
- context;
119
- initialized = false;
120
- disposed = false;
121
- config;
122
- logger;
123
- constructor(config = {}) {
124
- this.config = {
125
- ...types_1.DEFAULT_BRIDGE_CONFIG,
126
- ...config,
127
- };
128
- this.logger = this.config.logger;
129
- // Create isolate with memory limit
130
- // Note: memoryLimit is in MB
131
- this.isolate = new (getIvm().Isolate)({ memoryLimit: this.config.memoryLimit });
201
+ async verifyProxySystem() {
202
+ if (!this.context) {
203
+ throw new Error('Context not initialized');
132
204
  }
133
- /**
134
- * Initialize the isolate and create execution context.
135
- *
136
- * Steps:
137
- * 1. Create context
138
- * 2. Set up basic globals (global reference)
139
- * 3. Load runtime bundle (DateTime, extend, proxy system)
140
- * 4. Verify proxy system
141
- *
142
- * Must be called before execute().
143
- */
144
- async initialize() {
145
- if (this.initialized) {
146
- return;
205
+ try {
206
+ // Verify proxy system components loaded correctly
207
+ const hasProxyCreator = await this.context.eval('typeof createDeepLazyProxy !== "undefined"');
208
+ const hasSafeObject = await this.context.eval('typeof SafeObject !== "undefined"');
209
+ const hasSafeError = await this.context.eval('typeof SafeError !== "undefined"');
210
+ const hasBuildContext = await this.context.eval('typeof buildContext !== "undefined"');
211
+ if (!hasProxyCreator || !hasSafeObject || !hasSafeError || !hasBuildContext) {
212
+ throw new Error(`Proxy system verification failed: ` +
213
+ `createDeepLazyProxy=${hasProxyCreator}, ` +
214
+ `SafeObject=${hasSafeObject}, SafeError=${hasSafeError}, ` +
215
+ `buildContext=${hasBuildContext}`);
147
216
  }
148
- // Create context in the isolate
149
- this.context = await this.isolate.createContext();
150
- // Set up basic globals
151
- // jail is a reference to the context's global object
152
- const jail = this.context.global;
153
- // Set 'global' to reference itself (pattern from POC)
154
- // This allows code in isolate to access 'global.something'
155
- await jail.set('global', jail.derefInto());
156
- // Load runtime bundle (DateTime, extend, SafeObject, proxy system)
157
- await this.loadVendorLibraries();
158
- // Verify proxy system loaded correctly
159
- await this.verifyProxySystem();
160
- // Inject E() error handler needed by tournament-generated try-catch code
161
- await this.injectErrorHandler();
162
- this.initialized = true;
163
- this.logger.debug('[IsolatedVmBridge] Initialized successfully');
217
+ this.logger.debug('[IsolatedVmBridge] Proxy system verified successfully');
164
218
  }
165
- /**
166
- * Load runtime bundle into the isolate.
167
- *
168
- * The runtime bundle includes:
169
- * - DateTime, extend, extendOptional (expression engine globals)
170
- * - SafeObject and SafeError wrappers
171
- * - createDeepLazyProxy function
172
- * - buildContext function
173
- *
174
- * @private
175
- * @throws {Error} If context not initialized or bundle loading fails
176
- */
177
- async loadVendorLibraries() {
178
- if (!this.context) {
179
- throw new Error('Context not initialized');
180
- }
181
- try {
182
- // Load runtime bundle (includes vendor libraries + proxy system)
183
- const runtimeBundle = await readRuntimeBundle();
184
- // Evaluate bundle in isolate context
185
- // This makes all exported globals available (DateTime, extend, extendOptional, SafeObject, SafeError, createDeepLazyProxy, buildContext)
186
- await this.context.eval(runtimeBundle);
187
- this.logger.debug('[IsolatedVmBridge] Runtime bundle loaded');
188
- // Verify vendor libraries loaded correctly
189
- const hasDateTime = await this.context.eval('typeof DateTime !== "undefined"');
190
- const hasExtend = await this.context.eval('typeof extend !== "undefined"');
191
- if (!hasDateTime || !hasExtend) {
192
- throw new Error(`Library verification failed: DateTime=${hasDateTime}, extend=${hasExtend}`);
193
- }
194
- this.logger.debug('[IsolatedVmBridge] Vendor libraries verified successfully');
195
- }
196
- catch (error) {
197
- const errorMessage = error instanceof Error ? error.message : String(error);
198
- throw new Error(`Failed to load runtime bundle: ${errorMessage}`);
199
- }
219
+ catch (error) {
220
+ const errorMessage = error instanceof Error ? error.message : String(error);
221
+ throw new Error(`Failed to verify proxy system: ${errorMessage}`);
200
222
  }
201
- /**
202
- * Verify the proxy system loaded correctly.
203
- *
204
- * The proxy system is loaded as part of the runtime bundle in loadVendorLibraries().
205
- * This method verifies all required components are available.
206
- *
207
- * @private
208
- * @throws {Error} If context not initialized or proxy system verification fails
209
- */
210
- async verifyProxySystem() {
211
- if (!this.context) {
212
- throw new Error('Context not initialized');
213
- }
214
- try {
215
- // Verify proxy system components loaded correctly
216
- const hasProxyCreator = await this.context.eval('typeof createDeepLazyProxy !== "undefined"');
217
- const hasSafeObject = await this.context.eval('typeof SafeObject !== "undefined"');
218
- const hasSafeError = await this.context.eval('typeof SafeError !== "undefined"');
219
- const hasBuildContext = await this.context.eval('typeof buildContext !== "undefined"');
220
- if (!hasProxyCreator || !hasSafeObject || !hasSafeError || !hasBuildContext) {
221
- throw new Error(`Proxy system verification failed: ` +
222
- `createDeepLazyProxy=${hasProxyCreator}, ` +
223
- `SafeObject=${hasSafeObject}, SafeError=${hasSafeError}, ` +
224
- `buildContext=${hasBuildContext}`);
225
- }
226
- this.logger.debug('[IsolatedVmBridge] Proxy system verified successfully');
227
- }
228
- catch (error) {
229
- const errorMessage = error instanceof Error ? error.message : String(error);
230
- throw new Error(`Failed to verify proxy system: ${errorMessage}`);
231
- }
223
+ }
224
+ /**
225
+ * Inject the E() error handler into the isolate context.
226
+ *
227
+ * There are two exception-handling layers inside the isolate:
228
+ *
229
+ * 1. **Inner layer (this handler, `E()`)** — Tournament wraps each
230
+ * expression with try-catch that calls `E(error, this)`. This handler
231
+ * must match the legacy engine's behavior (set in expression.ts via
232
+ * setErrorHandler):
233
+ * - Re-throw ExpressionError / ExpressionExtensionError
234
+ * - Swallow everything else (TypeErrors, generic Errors, etc.)
235
+ *
236
+ * 2. **Outer layer (`wrappedCode` try-catch in `execute()`)** — Catches
237
+ * anything that escaped `E()` (e.g. re-thrown ExpressionErrors) and
238
+ * serializes it into a sentinel object so the host can reconstruct it.
239
+ *
240
+ * Inside the isolate, errors from host callbacks arrive as sentinel
241
+ * objects ({ __isError, name, message, ... }) rather than class instances,
242
+ * so we match by name instead of instanceof.
243
+ *
244
+ * @private
245
+ * @throws {Error} If context not initialized
246
+ */
247
+ async injectErrorHandler() {
248
+ if (!this.context) {
249
+ throw new Error('Context not initialized');
232
250
  }
233
- /**
234
- * Inject the E() error handler into the isolate context.
235
- *
236
- * There are two exception-handling layers inside the isolate:
237
- *
238
- * 1. **Inner layer (this handler, `E()`)** — Tournament wraps each
239
- * expression with try-catch that calls `E(error, this)`. This handler
240
- * must match the legacy engine's behavior (set in expression.ts via
241
- * setErrorHandler):
242
- * - Re-throw ExpressionError / ExpressionExtensionError
243
- * - Swallow everything else (TypeErrors, generic Errors, etc.)
244
- *
245
- * 2. **Outer layer (`wrappedCode` try-catch in `execute()`)** — Catches
246
- * anything that escaped `E()` (e.g. re-thrown ExpressionErrors) and
247
- * serializes it into a sentinel object so the host can reconstruct it.
248
- *
249
- * Inside the isolate, errors from host callbacks arrive as sentinel
250
- * objects ({ __isError, name, message, ... }) rather than class instances,
251
- * so we match by name instead of instanceof.
252
- *
253
- * @private
254
- * @throws {Error} If context not initialized
255
- */
256
- async injectErrorHandler() {
257
- if (!this.context) {
258
- throw new Error('Context not initialized');
259
- }
260
- await this.context.eval(`
251
+ await this.context.eval(`
261
252
  if (typeof E === 'undefined') {
262
253
  globalThis.E = function(error, _context) {
263
254
  // Re-throw ExpressionError / ExpressionExtensionError to match
@@ -272,406 +263,406 @@ var __importStar = (this && this.__importStar) || (function () {
272
263
  };
273
264
  }
274
265
  `);
275
- this.logger.debug('[IsolatedVmBridge] Error handler injected successfully');
276
- }
277
- /**
278
- * Create an ivm.Reference callback for getting value/metadata at a path.
279
- *
280
- * Used by createDeepLazyProxy when accessing properties. Returns metadata
281
- * markers for arrays and objects, or the primitive value directly.
282
- *
283
- * Function-typed values are returned as `undefined` — every callable on
284
- * the host data surface (`$('Foo').first()`, `$items()`, `$fromAI()`,
285
- * `$evaluateExpression()`, `$getPairedItem()`) is wired in-isolate via
286
- * the typed-RPC dispatcher (`callHost`). No expression form should
287
- * reach a function through this path.
288
- *
289
- * @param data - Current workflow data to use for callback responses
290
- * @private
291
- */
292
- createGetValueAtPathRef(data) {
293
- return new (getIvm().Reference)((path) => {
294
- try {
295
- // Navigate to value
296
- // Special-case: paths starting with ['$item', index] call data.$item(index)
297
- // to get the sub-proxy for that item, then continue navigating the rest.
298
- let value = data;
299
- let startIndex = 0;
300
- const itemFn = data.$item;
301
- if (path.length >= 2 && path[0] === '$item' && typeof itemFn === 'function') {
302
- const itemIndex = parseInt(path[1], 10);
303
- if (!isNaN(itemIndex)) {
304
- value = itemFn(itemIndex);
305
- startIndex = 2;
306
- }
307
- }
308
- else {
309
- const dollarFn = data.$;
310
- if (path.length >= 2 && path[0] === '$' && typeof dollarFn === 'function') {
311
- value = dollarFn(path[1]);
312
- startIndex = 2;
313
- }
314
- }
315
- for (let i = startIndex; i < path.length; i++) {
316
- value = value?.[path[i]];
317
- if (value === undefined || value === null) {
318
- return value;
319
- }
320
- }
321
- // Functions are not reachable via the lazy-proxy data path —
322
- // every callable on the host data surface routes through the
323
- // typed-RPC dispatcher. Return undefined so any residual
324
- // access surfaces as missing rather than as a stale metadata
325
- // marker the runtime no longer knows how to interpret.
326
- if (typeof value === 'function') {
327
- return undefined;
266
+ this.logger.debug('[IsolatedVmBridge] Error handler injected successfully');
267
+ }
268
+ /**
269
+ * Create an ivm.Reference callback for getting value/metadata at a path.
270
+ *
271
+ * Used by createDeepLazyProxy when accessing properties. Returns metadata
272
+ * markers for arrays and objects, or the primitive value directly.
273
+ *
274
+ * Function-typed values are returned as `undefined` — every callable on
275
+ * the host data surface (`$('Foo').first()`, `$items()`, `$fromAI()`,
276
+ * `$evaluateExpression()`, `$getPairedItem()`) is wired in-isolate via
277
+ * the typed-RPC dispatcher (`callHost`). No expression form should
278
+ * reach a function through this path.
279
+ *
280
+ * @param data - Current workflow data to use for callback responses
281
+ * @private
282
+ */
283
+ createGetValueAtPathRef(data) {
284
+ return new (getIvm().Reference)((path) => {
285
+ try {
286
+ // Navigate to value
287
+ // Special-case: paths starting with ['$item', index] call data.$item(index)
288
+ // to get the sub-proxy for that item, then continue navigating the rest.
289
+ let value = data;
290
+ let startIndex = 0;
291
+ const itemFn = data.$item;
292
+ if (path.length >= 2 && path[0] === '$item' && typeof itemFn === 'function') {
293
+ const itemIndex = parseInt(path[1], 10);
294
+ if (!isNaN(itemIndex)) {
295
+ value = itemFn(itemIndex);
296
+ startIndex = 2;
328
297
  }
329
- // Handle arrays - always lazy, only transfer length
330
- if (Array.isArray(value)) {
331
- return {
332
- __isArray: true,
333
- __length: value.length,
334
- __data: null,
335
- };
298
+ }
299
+ else {
300
+ const dollarFn = data.$;
301
+ if (path.length >= 2 && path[0] === '$' && typeof dollarFn === 'function') {
302
+ value = dollarFn(path[1]);
303
+ startIndex = 2;
336
304
  }
337
- // Dates have no enumerable own keys; pass through instead of
338
- // marshaling as an empty object.
339
- if (value instanceof Date) {
305
+ }
306
+ for (let i = startIndex; i < path.length; i++) {
307
+ value = value?.[path[i]];
308
+ if (value === undefined || value === null) {
340
309
  return value;
341
310
  }
342
- // Handle objects - return metadata with keys
343
- if (value !== null && typeof value === 'object') {
344
- return {
345
- __isObject: true,
346
- __keys: Object.keys(value),
347
- };
348
- }
349
- // Primitive value
311
+ }
312
+ // Functions are not reachable via the lazy-proxy data path —
313
+ // every callable on the host data surface routes through the
314
+ // typed-RPC dispatcher. Return undefined so any residual
315
+ // access surfaces as missing rather than as a stale metadata
316
+ // marker the runtime no longer knows how to interpret.
317
+ if (typeof value === 'function') {
318
+ return undefined;
319
+ }
320
+ // Handle arrays - always lazy, only transfer length
321
+ if (Array.isArray(value)) {
322
+ return {
323
+ __isArray: true,
324
+ __length: value.length,
325
+ __data: null,
326
+ };
327
+ }
328
+ // Dates have no enumerable own keys; pass through instead of
329
+ // marshaling as an empty object.
330
+ if (value instanceof Date) {
350
331
  return value;
351
332
  }
352
- catch (err) {
353
- return serializeError(err);
333
+ // Handle objects - return metadata with keys
334
+ if (value !== null && typeof value === 'object') {
335
+ return {
336
+ __isObject: true,
337
+ __keys: Object.keys(value),
338
+ };
354
339
  }
355
- });
356
- }
357
- /**
358
- * Create an ivm.Reference callback for getting array elements at an index.
359
- *
360
- * Used by array proxy when accessing numeric indices.
361
- *
362
- * @param data - Current workflow data to use for callback responses
363
- * @private
364
- */
365
- createGetArrayElementRef(data) {
366
- return new (getIvm().Reference)((path, index) => {
367
- try {
368
- // Navigate to array
369
- // Special-case: paths starting with ['$item', index] call data.$item(index)
370
- let arr = data;
371
- let startIndex = 0;
372
- const itemFn = data.$item;
373
- if (path.length >= 2 && path[0] === '$item' && typeof itemFn === 'function') {
374
- const itemIndex = parseInt(path[1], 10);
375
- if (!isNaN(itemIndex)) {
376
- arr = itemFn(itemIndex);
377
- startIndex = 2;
378
- }
379
- }
380
- else {
381
- const dollarFn = data.$;
382
- if (path.length >= 2 && path[0] === '$' && typeof dollarFn === 'function') {
383
- arr = dollarFn(path[1]);
384
- startIndex = 2;
385
- }
386
- }
387
- for (let i = startIndex; i < path.length; i++) {
388
- arr = arr?.[path[i]];
389
- if (arr === undefined || arr === null) {
390
- return undefined;
391
- }
392
- }
393
- if (!Array.isArray(arr)) {
394
- return undefined;
340
+ // Primitive value
341
+ return value;
342
+ }
343
+ catch (err) {
344
+ return serializeError(err);
345
+ }
346
+ });
347
+ }
348
+ /**
349
+ * Create an ivm.Reference callback for getting array elements at an index.
350
+ *
351
+ * Used by array proxy when accessing numeric indices.
352
+ *
353
+ * @param data - Current workflow data to use for callback responses
354
+ * @private
355
+ */
356
+ createGetArrayElementRef(data) {
357
+ return new (getIvm().Reference)((path, index) => {
358
+ try {
359
+ // Navigate to array
360
+ // Special-case: paths starting with ['$item', index] call data.$item(index)
361
+ let arr = data;
362
+ let startIndex = 0;
363
+ const itemFn = data.$item;
364
+ if (path.length >= 2 && path[0] === '$item' && typeof itemFn === 'function') {
365
+ const itemIndex = parseInt(path[1], 10);
366
+ if (!isNaN(itemIndex)) {
367
+ arr = itemFn(itemIndex);
368
+ startIndex = 2;
395
369
  }
396
- // Only genuine array indices are reachable; anything else (e.g.
397
- // 'constructor', '__lookupGetter__') would read off the prototype
398
- // chain and could leak a host function reference across the boundary.
399
- if (!Number.isInteger(index) || index < 0) {
400
- return undefined;
370
+ }
371
+ else {
372
+ const dollarFn = data.$;
373
+ if (path.length >= 2 && path[0] === '$' && typeof dollarFn === 'function') {
374
+ arr = dollarFn(path[1]);
375
+ startIndex = 2;
401
376
  }
402
- const element = arr[index];
403
- // Functions are never reachable through the data surface — mirror the
404
- // guard in getValueAtPath so a host callable can't cross the boundary.
405
- if (typeof element === 'function') {
377
+ }
378
+ for (let i = startIndex; i < path.length; i++) {
379
+ arr = arr?.[path[i]];
380
+ if (arr === undefined || arr === null) {
406
381
  return undefined;
407
382
  }
408
- // Dates have no enumerable own keys; pass through instead of
409
- // marshaling as an empty object.
410
- if (element instanceof Date) {
411
- return element;
412
- }
413
- // If element is object/array, return metadata
414
- if (element !== null && typeof element === 'object') {
415
- if (Array.isArray(element)) {
416
- return {
417
- __isArray: true,
418
- __length: element.length,
419
- __data: null,
420
- };
421
- }
383
+ }
384
+ if (!Array.isArray(arr)) {
385
+ return undefined;
386
+ }
387
+ // Only genuine array indices are reachable; anything else (e.g.
388
+ // 'constructor', '__lookupGetter__') would read off the prototype
389
+ // chain and could leak a host function reference across the boundary.
390
+ if (!Number.isInteger(index) || index < 0) {
391
+ return undefined;
392
+ }
393
+ const element = arr[index];
394
+ // Functions are never reachable through the data surface — mirror the
395
+ // guard in getValueAtPath so a host callable can't cross the boundary.
396
+ if (typeof element === 'function') {
397
+ return undefined;
398
+ }
399
+ // Dates have no enumerable own keys; pass through instead of
400
+ // marshaling as an empty object.
401
+ if (element instanceof Date) {
402
+ return element;
403
+ }
404
+ // If element is object/array, return metadata
405
+ if (element !== null && typeof element === 'object') {
406
+ if (Array.isArray(element)) {
422
407
  return {
423
- __isObject: true,
424
- __keys: Object.keys(element),
408
+ __isArray: true,
409
+ __length: element.length,
410
+ __data: null,
425
411
  };
426
412
  }
427
- // Primitive element
428
- return element;
413
+ return {
414
+ __isObject: true,
415
+ __keys: Object.keys(element),
416
+ };
429
417
  }
430
- catch (err) {
431
- return serializeError(err);
432
- }
433
- });
434
- }
435
- /**
436
- * Create the single typed-RPC dispatcher.
437
- *
438
- * The isolate sends one envelope per typed RPC invocation:
439
- * `callHost({ type: 'getNodeFirst', nodeName, branchIndex?, runIndex? })`
440
- *
441
- * Inputs cross a trust boundary, so the dispatcher parses every envelope
442
- * with the host-side zod schema (`bridgeMessageSchema`) before any
443
- * dispatch happens. Anything that deviates from the declared shape —
444
- * unknown `type`, missing required fields, extra unexpected fields,
445
- * wrong field types — fails the parse and an error sentinel is returned
446
- * to the caller.
447
- *
448
- * After parsing, `switch (msg.type)` dispatches to a private handler with
449
- * a fully narrowed message type. The operation set is exactly the cases
450
- * in this switch; the `type` field selects a static branch in source,
451
- * not a property lookup on a runtime object.
452
- *
453
- * @param data - Current workflow data
454
- * @private
455
- */
456
- createCallHostRef(data) {
457
- return new (getIvm().Reference)((rawMsg) => {
458
- try {
459
- const msg = bridge_messages_1.bridgeMessageSchema.parse(rawMsg);
460
- switch (msg.type) {
461
- case 'getNodeFirst':
462
- return this.handleGetNodeFirst(msg, data);
463
- case 'getNodeLast':
464
- return this.handleGetNodeLast(msg, data);
465
- case 'getNodeAll':
466
- return this.handleGetNodeAll(msg, data);
467
- case 'getInputFirst':
468
- return this.handleGetInputFirst(data);
469
- case 'getInputLast':
470
- return this.handleGetInputLast(data);
471
- case 'getInputAll':
472
- return this.handleGetInputAll(data);
473
- case 'getItems':
474
- return this.handleGetItems(msg, data);
475
- case 'fromAi':
476
- return this.handleFromAi(msg, data);
477
- case 'getNodePairedItem':
478
- return this.handleGetNodePairedItem(msg, data);
479
- case 'getNodeItemMatching':
480
- return this.handleGetNodeItemMatching(msg, data);
481
- case 'getNodeItem':
482
- return this.handleGetNodeItem(msg, data);
483
- case 'evaluateExpression':
484
- return this.handleEvaluateExpression(msg, data);
485
- case 'getPairedItem':
486
- return this.handleGetPairedItem(msg, data);
487
- default: {
488
- // Unreachable at runtime — zod rejects unknown `type` values
489
- // before the switch. The `never` assignment is the compile-time
490
- // guard: a new schema added to `bridgeMessageSchema` without a
491
- // matching case here becomes a type error.
492
- const exhaustive = msg;
493
- void exhaustive;
494
- throw new Error('Unhandled bridge message');
495
- }
418
+ // Primitive element
419
+ return element;
420
+ }
421
+ catch (err) {
422
+ return serializeError(err);
423
+ }
424
+ });
425
+ }
426
+ /**
427
+ * Create the single typed-RPC dispatcher.
428
+ *
429
+ * The isolate sends one envelope per typed RPC invocation:
430
+ * `callHost({ type: 'getNodeFirst', nodeName, branchIndex?, runIndex? })`
431
+ *
432
+ * Inputs cross a trust boundary, so the dispatcher parses every envelope
433
+ * with the host-side zod schema (`bridgeMessageSchema`) before any
434
+ * dispatch happens. Anything that deviates from the declared shape —
435
+ * unknown `type`, missing required fields, extra unexpected fields,
436
+ * wrong field types fails the parse and an error sentinel is returned
437
+ * to the caller.
438
+ *
439
+ * After parsing, `switch (msg.type)` dispatches to a private handler with
440
+ * a fully narrowed message type. The operation set is exactly the cases
441
+ * in this switch; the `type` field selects a static branch in source,
442
+ * not a property lookup on a runtime object.
443
+ *
444
+ * @param data - Current workflow data
445
+ * @private
446
+ */
447
+ createCallHostRef(data) {
448
+ return new (getIvm().Reference)((rawMsg) => {
449
+ try {
450
+ const msg = bridge_messages_1.bridgeMessageSchema.parse(rawMsg);
451
+ switch (msg.type) {
452
+ case 'getNodeFirst':
453
+ return this.handleGetNodeFirst(msg, data);
454
+ case 'getNodeLast':
455
+ return this.handleGetNodeLast(msg, data);
456
+ case 'getNodeAll':
457
+ return this.handleGetNodeAll(msg, data);
458
+ case 'getInputFirst':
459
+ return this.handleGetInputFirst(data);
460
+ case 'getInputLast':
461
+ return this.handleGetInputLast(data);
462
+ case 'getInputAll':
463
+ return this.handleGetInputAll(data);
464
+ case 'getItems':
465
+ return this.handleGetItems(msg, data);
466
+ case 'fromAi':
467
+ return this.handleFromAi(msg, data);
468
+ case 'getNodePairedItem':
469
+ return this.handleGetNodePairedItem(msg, data);
470
+ case 'getNodeItemMatching':
471
+ return this.handleGetNodeItemMatching(msg, data);
472
+ case 'getNodeItem':
473
+ return this.handleGetNodeItem(msg, data);
474
+ case 'evaluateExpression':
475
+ return this.handleEvaluateExpression(msg, data);
476
+ case 'getPairedItem':
477
+ return this.handleGetPairedItem(msg, data);
478
+ default: {
479
+ // Unreachable at runtime zod rejects unknown `type` values
480
+ // before the switch. The `never` assignment is the compile-time
481
+ // guard: a new schema added to `bridgeMessageSchema` without a
482
+ // matching case here becomes a type error.
483
+ const exhaustive = msg;
484
+ void exhaustive;
485
+ throw new Error('Unhandled bridge message');
496
486
  }
497
487
  }
498
- catch (err) {
499
- return serializeError(err);
500
- }
501
- });
502
- }
503
- /**
504
- * Handlers for the `$('Foo').{first,last,all}` typed RPCs.
505
- *
506
- * Each handler reads a fixed literal property name off the host-side node
507
- * proxy — the isolate cannot influence which property is dereferenced.
508
- * Eliminating `data.$` as a host-callable entirely would require reaching
509
- * the `WorkflowDataProxy` internals (e.g. `getNodeExecutionOrPinnedData`)
510
- * rather than the public `$()` API; that's a follow-up.
511
- *
512
- * `data.$` is a host-wired function (`WorkflowDataProxy`'s `$`). If it
513
- * ever isn't, optional chaining short-circuits to `undefined` — the same
514
- * observable result the runtime's `E()` handler produces from any thrown
515
- * error here.
516
- *
517
- * @private
518
- */
519
- handleGetNodeFirst(msg, data) {
520
- return data.$?.(msg.nodeName)?.first?.(msg.branchIndex, msg.runIndex);
521
- }
522
- handleGetNodeLast(msg, data) {
523
- return data.$?.(msg.nodeName)?.last?.(msg.branchIndex, msg.runIndex);
524
- }
525
- handleGetNodeAll(msg, data) {
526
- return data.$?.(msg.nodeName)?.all?.(msg.branchIndex, msg.runIndex);
527
- }
528
- /**
529
- * Handlers for the `$input.{first,last,all}` typed RPCs.
530
- *
531
- * Each reads a fixed literal property name off `data.$input` (the host's
532
- * `WorkflowDataProxy` input proxy). The host enforces zero arguments on
533
- * these methods — the schemas have no fields besides `type`, so the
534
- * isolate cannot pass anything that would trigger the "should have no
535
- * arguments" error path on the host side.
536
- *
537
- * @private
538
- */
539
- handleGetInputFirst(data) {
540
- return data.$input?.first?.();
541
- }
542
- handleGetInputLast(data) {
543
- return data.$input?.last?.();
544
- }
545
- handleGetInputAll(data) {
546
- return data.$input?.all?.();
547
- }
548
- /**
549
- * Handler for `$items(nodeName?, outputIndex?, runIndex?)` — the
550
- * global accessor for a node's execution data. Reads the literal
551
- * `$items` property off `data` (host-wired by `WorkflowDataProxy`)
552
- * and forwards the validated args verbatim. The host applies its own
553
- * defaults when fields are `undefined`.
554
- *
555
- * @private
556
- */
557
- handleGetItems(msg, data) {
558
- return data.$items?.(msg.nodeName, msg.outputIndex, msg.runIndex);
559
- }
560
- /**
561
- * Handler for `$fromAI(name, description?, type?, defaultValue?)` and its
562
- * `$fromAi` / `$fromai` aliases. Reads the literal `$fromAI` property
563
- * off `data` (host-wired) and forwards the args. The host validates
564
- * `name` (required + regex) and applies its own resolution / fallback
565
- * logic, so empty / invalid names surface as the host's structured
566
- * `ExpressionError` rather than a generic zod parse error.
567
- *
568
- * Note: `msg.valueType` maps to the host's third positional parameter
569
- * (`_type` in `WorkflowDataProxy.handleFromAi`). The bridge protocol
570
- * renames it to avoid collision with the `type` discriminator on the
571
- * envelope — the host parameter currently goes unused, but if it ever
572
- * gains a name (`type`), this mapping should stay explicit.
573
- *
574
- * @private
575
- */
576
- handleFromAi(msg, data) {
577
- return data.$fromAI?.(msg.name, msg.description, msg.valueType, msg.defaultValue);
578
- }
579
- /**
580
- * Handlers for the `$('Foo').pairedItem(itemIndex?)` / `.itemMatching(...)` /
581
- * `.item` cluster. Three separate typed RPCs, each reading exactly one
582
- * literal property off the host node proxy.
583
- *
584
- * The split is load-bearing: the host's `pairedItemMethod` closure
585
- * captures which property name the proxy `get` trap saw, and uses
586
- * that to pick the right error message (e.g. "Missing item index for
587
- * .itemMatching()") and to decide between method-call vs getter
588
- * semantics for `.item`. Reading the matching property here lets
589
- * those host-side branches fire exactly as they do in the legacy
590
- * engine; no in-isolate validation needed.
591
- *
592
- * @private
593
- */
594
- handleGetNodePairedItem(msg, data) {
595
- return data.$?.(msg.nodeName)?.pairedItem?.(msg.itemIndex);
596
- }
597
- handleGetNodeItemMatching(msg, data) {
598
- return data.$?.(msg.nodeName)?.itemMatching?.(msg.itemIndex);
599
- }
600
- handleGetNodeItem(msg, data) {
601
- // `.item` is a host getter — accessing it invokes the resolver and
602
- // returns the value immediately. Optional chaining only short-
603
- // circuits on null/undefined; the getter still fires on access.
604
- return data.$?.(msg.nodeName)?.item;
605
- }
606
- /**
607
- * Handler for `$evaluateExpression(expression, itemIndex?)`. Forwards
608
- * the string to the host's nested-evaluation helper, which re-enters
609
- * the expression engine on the inner expression. Under the VM engine
610
- * this round-trips through the bridge again on a fresh evaluation
611
- * cycle, which is the same shape the legacy engine supports.
612
- *
613
- * @private
614
- */
615
- handleEvaluateExpression(msg, data) {
616
- return data.$evaluateExpression?.(msg.expression, msg.itemIndex);
617
- }
618
- /**
619
- * Handler for `$getPairedItem(destinationNodeName, incomingSourceData,
620
- * initialPairedItem)`. Forwards directly to the host binding, which
621
- * walks the paired-item ancestry chain back to the named upstream node
622
- * and returns the matching execution item.
623
- *
624
- * The two trailing host parameters — `usedMethodName` and
625
- * `nodeBeforeLast` — are deliberately not part of the wire protocol:
626
- * the host's default for `usedMethodName` is already `$getPairedItem`,
627
- * and `nodeBeforeLast` is an internal recursion argument the host sets
628
- * during traversal.
629
- *
630
- * @private
631
- */
632
- handleGetPairedItem(msg, data) {
633
- return data.$getPairedItem?.(msg.destinationNodeName, msg.incomingSourceData, msg.initialPairedItem);
634
- }
635
- /**
636
- * Execute JavaScript code in the isolated context.
637
- *
638
- * Flow:
639
- * 1. Create three ivm.Reference callbacks scoped to the current data:
640
- * `getValueAtPath`, `getArrayElement`, `callHost`.
641
- * 2. Use evalClosureSync to run the code in a closure where `$0`/`$1`/`$2`
642
- * are the callback references — no global mutable state.
643
- * 3. buildContext() inside the isolate creates a fresh evaluation context
644
- * from the closure-scoped references.
645
- *
646
- * Each call gets its own closure, so nested and concurrent evaluations
647
- * cannot interfere with each other.
648
- *
649
- * @param code - JavaScript expression to evaluate
650
- * @param data - Workflow data (e.g., { $json: {...}, $runIndex: 0 })
651
- * @returns Result of the expression
652
- * @throws {Error} If bridge not initialized or execution fails
653
- */
654
- execute(code, data, options) {
655
- if (!this.initialized || !this.context) {
656
- throw new Error('Bridge not initialized. Call initialize() first.');
657
488
  }
658
- const getValueAtPath = this.createGetValueAtPathRef(data);
659
- const getArrayElement = this.createGetArrayElementRef(data);
660
- const callHost = this.createCallHostRef(data);
661
- try {
662
- const timezone = options?.timezone ? JSON.stringify(options.timezone) : 'undefined';
663
- // Wrap transformed code so 'this' === the closure-scoped context.
664
- // Tournament generates: this.$json.email, this.$items(), etc.
665
- // buildContext() creates a fresh context with lazy proxies from the
666
- // closure-scoped callback references no globals touched. The bundle
667
- // is passed as a single object so adding typed RPCs doesn't churn the
668
- // evalClosureSync signature; new operations land as new schemas in
669
- // bridge-messages.ts and new cases in the callHost dispatcher.
670
- // The outer try-catch serializes errors into a sentinel object and returns
671
- // it as the result. Errors from host callbacks arrive as sentinels already
672
- // (via serializeError), so we pass them through. This avoids a round-trip
673
- // callback and keeps Error reconstruction on the host side only.
674
- const wrappedCode = `
489
+ catch (err) {
490
+ return serializeError(err);
491
+ }
492
+ });
493
+ }
494
+ /**
495
+ * Handlers for the `$('Foo').{first,last,all}` typed RPCs.
496
+ *
497
+ * Each handler reads a fixed literal property name off the host-side node
498
+ * proxy the isolate cannot influence which property is dereferenced.
499
+ * Eliminating `data.$` as a host-callable entirely would require reaching
500
+ * the `WorkflowDataProxy` internals (e.g. `getNodeExecutionOrPinnedData`)
501
+ * rather than the public `$()` API; that's a follow-up.
502
+ *
503
+ * `data.$` is a host-wired function (`WorkflowDataProxy`'s `$`). If it
504
+ * ever isn't, optional chaining short-circuits to `undefined` the same
505
+ * observable result the runtime's `E()` handler produces from any thrown
506
+ * error here.
507
+ *
508
+ * @private
509
+ */
510
+ handleGetNodeFirst(msg, data) {
511
+ return data.$?.(msg.nodeName)?.first?.(msg.branchIndex, msg.runIndex);
512
+ }
513
+ handleGetNodeLast(msg, data) {
514
+ return data.$?.(msg.nodeName)?.last?.(msg.branchIndex, msg.runIndex);
515
+ }
516
+ handleGetNodeAll(msg, data) {
517
+ return data.$?.(msg.nodeName)?.all?.(msg.branchIndex, msg.runIndex);
518
+ }
519
+ /**
520
+ * Handlers for the `$input.{first,last,all}` typed RPCs.
521
+ *
522
+ * Each reads a fixed literal property name off `data.$input` (the host's
523
+ * `WorkflowDataProxy` input proxy). The host enforces zero arguments on
524
+ * these methods — the schemas have no fields besides `type`, so the
525
+ * isolate cannot pass anything that would trigger the "should have no
526
+ * arguments" error path on the host side.
527
+ *
528
+ * @private
529
+ */
530
+ handleGetInputFirst(data) {
531
+ return data.$input?.first?.();
532
+ }
533
+ handleGetInputLast(data) {
534
+ return data.$input?.last?.();
535
+ }
536
+ handleGetInputAll(data) {
537
+ return data.$input?.all?.();
538
+ }
539
+ /**
540
+ * Handler for `$items(nodeName?, outputIndex?, runIndex?)` — the
541
+ * global accessor for a node's execution data. Reads the literal
542
+ * `$items` property off `data` (host-wired by `WorkflowDataProxy`)
543
+ * and forwards the validated args verbatim. The host applies its own
544
+ * defaults when fields are `undefined`.
545
+ *
546
+ * @private
547
+ */
548
+ handleGetItems(msg, data) {
549
+ return data.$items?.(msg.nodeName, msg.outputIndex, msg.runIndex);
550
+ }
551
+ /**
552
+ * Handler for `$fromAI(name, description?, type?, defaultValue?)` and its
553
+ * `$fromAi` / `$fromai` aliases. Reads the literal `$fromAI` property
554
+ * off `data` (host-wired) and forwards the args. The host validates
555
+ * `name` (required + regex) and applies its own resolution / fallback
556
+ * logic, so empty / invalid names surface as the host's structured
557
+ * `ExpressionError` rather than a generic zod parse error.
558
+ *
559
+ * Note: `msg.valueType` maps to the host's third positional parameter
560
+ * (`_type` in `WorkflowDataProxy.handleFromAi`). The bridge protocol
561
+ * renames it to avoid collision with the `type` discriminator on the
562
+ * envelope — the host parameter currently goes unused, but if it ever
563
+ * gains a name (`type`), this mapping should stay explicit.
564
+ *
565
+ * @private
566
+ */
567
+ handleFromAi(msg, data) {
568
+ return data.$fromAI?.(msg.name, msg.description, msg.valueType, msg.defaultValue);
569
+ }
570
+ /**
571
+ * Handlers for the `$('Foo').pairedItem(itemIndex?)` / `.itemMatching(...)` /
572
+ * `.item` cluster. Three separate typed RPCs, each reading exactly one
573
+ * literal property off the host node proxy.
574
+ *
575
+ * The split is load-bearing: the host's `pairedItemMethod` closure
576
+ * captures which property name the proxy `get` trap saw, and uses
577
+ * that to pick the right error message (e.g. "Missing item index for
578
+ * .itemMatching()") and to decide between method-call vs getter
579
+ * semantics for `.item`. Reading the matching property here lets
580
+ * those host-side branches fire exactly as they do in the legacy
581
+ * engine; no in-isolate validation needed.
582
+ *
583
+ * @private
584
+ */
585
+ handleGetNodePairedItem(msg, data) {
586
+ return data.$?.(msg.nodeName)?.pairedItem?.(msg.itemIndex);
587
+ }
588
+ handleGetNodeItemMatching(msg, data) {
589
+ return data.$?.(msg.nodeName)?.itemMatching?.(msg.itemIndex);
590
+ }
591
+ handleGetNodeItem(msg, data) {
592
+ // `.item` is a host getter — accessing it invokes the resolver and
593
+ // returns the value immediately. Optional chaining only short-
594
+ // circuits on null/undefined; the getter still fires on access.
595
+ return data.$?.(msg.nodeName)?.item;
596
+ }
597
+ /**
598
+ * Handler for `$evaluateExpression(expression, itemIndex?)`. Forwards
599
+ * the string to the host's nested-evaluation helper, which re-enters
600
+ * the expression engine on the inner expression. Under the VM engine
601
+ * this round-trips through the bridge again on a fresh evaluation
602
+ * cycle, which is the same shape the legacy engine supports.
603
+ *
604
+ * @private
605
+ */
606
+ handleEvaluateExpression(msg, data) {
607
+ return data.$evaluateExpression?.(msg.expression, msg.itemIndex);
608
+ }
609
+ /**
610
+ * Handler for `$getPairedItem(destinationNodeName, incomingSourceData,
611
+ * initialPairedItem)`. Forwards directly to the host binding, which
612
+ * walks the paired-item ancestry chain back to the named upstream node
613
+ * and returns the matching execution item.
614
+ *
615
+ * The two trailing host parameters — `usedMethodName` and
616
+ * `nodeBeforeLast` — are deliberately not part of the wire protocol:
617
+ * the host's default for `usedMethodName` is already `$getPairedItem`,
618
+ * and `nodeBeforeLast` is an internal recursion argument the host sets
619
+ * during traversal.
620
+ *
621
+ * @private
622
+ */
623
+ handleGetPairedItem(msg, data) {
624
+ return data.$getPairedItem?.(msg.destinationNodeName, msg.incomingSourceData, msg.initialPairedItem);
625
+ }
626
+ /**
627
+ * Execute JavaScript code in the isolated context.
628
+ *
629
+ * Flow:
630
+ * 1. Create three ivm.Reference callbacks scoped to the current data:
631
+ * `getValueAtPath`, `getArrayElement`, `callHost`.
632
+ * 2. Use evalClosureSync to run the code in a closure where `$0`/`$1`/`$2`
633
+ * are the callback references — no global mutable state.
634
+ * 3. buildContext() inside the isolate creates a fresh evaluation context
635
+ * from the closure-scoped references.
636
+ *
637
+ * Each call gets its own closure, so nested and concurrent evaluations
638
+ * cannot interfere with each other.
639
+ *
640
+ * @param code - JavaScript expression to evaluate
641
+ * @param data - Workflow data (e.g., { $json: {...}, $runIndex: 0 })
642
+ * @returns Result of the expression
643
+ * @throws {Error} If bridge not initialized or execution fails
644
+ */
645
+ execute(code, data, options) {
646
+ if (!this.initialized || !this.context) {
647
+ throw new Error('Bridge not initialized. Call initialize() first.');
648
+ }
649
+ const getValueAtPath = this.createGetValueAtPathRef(data);
650
+ const getArrayElement = this.createGetArrayElementRef(data);
651
+ const callHost = this.createCallHostRef(data);
652
+ try {
653
+ const timezone = options?.timezone ? JSON.stringify(options.timezone) : 'undefined';
654
+ // Wrap transformed code so 'this' === the closure-scoped context.
655
+ // Tournament generates: this.$json.email, this.$items(), etc.
656
+ // buildContext() creates a fresh context with lazy proxies from the
657
+ // closure-scoped callback references — no globals touched. The bundle
658
+ // is passed as a single object so adding typed RPCs doesn't churn the
659
+ // evalClosureSync signature; new operations land as new schemas in
660
+ // bridge-messages.ts and new cases in the callHost dispatcher.
661
+ // The outer try-catch serializes errors into a sentinel object and returns
662
+ // it as the result. Errors from host callbacks arrive as sentinels already
663
+ // (via serializeError), so we pass them through. This avoids a round-trip
664
+ // callback and keeps Error reconstruction on the host side only.
665
+ const wrappedCode = `
675
666
  var __ctx = buildContext({
676
667
  getValueAtPath: $0,
677
668
  getArrayElement: $1,
@@ -697,82 +688,81 @@ try {
697
688
  extra: extra
698
689
  };
699
690
  }`;
700
- const result = this.context.evalClosureSync(wrappedCode, [getValueAtPath, getArrayElement, callHost], { result: { copy: true }, timeout: this.config.timeout });
701
- if (isErrorSentinel(result)) {
702
- throw this.reconstructError(result);
703
- }
704
- this.logger.debug('[IsolatedVmBridge] Expression executed successfully');
705
- return result;
706
- }
707
- catch (error) {
708
- // Re-throw reconstructed errors as-is.
709
- // Note: TypeError is intentionally NOT included here — the isolate's
710
- // E() handler swallows TypeErrors (failed attack attempts return undefined),
711
- // so TypeErrors from host callbacks should also go through the generic
712
- // wrapping for consistent behavior.
713
- if (error instanceof Error &&
714
- (error.name === 'ExpressionError' || error.name === 'ExpressionExtensionError')) {
715
- throw error;
716
- }
717
- const errorMessage = error instanceof Error ? error.message : String(error);
718
- if (errorMessage.includes('Script execution timed out')) {
719
- throw new types_1.TimeoutError(`Expression timed out after ${this.config.timeout}ms`, {});
720
- }
721
- if (errorMessage.includes('memory limit')) {
722
- throw new types_1.MemoryLimitError(`Expression exceeded memory limit of ${this.config.memoryLimit}MB`, {});
723
- }
724
- throw new Error(`Expression evaluation failed: ${errorMessage}`);
725
- }
726
- finally {
727
- getValueAtPath.release();
728
- getArrayElement.release();
729
- callHost.release();
691
+ const result = this.context.evalClosureSync(wrappedCode, [getValueAtPath, getArrayElement, callHost], { result: { copy: true }, timeout: this.config.timeout });
692
+ if (isErrorSentinel(result)) {
693
+ throw this.reconstructError(result);
730
694
  }
695
+ this.logger.debug('[IsolatedVmBridge] Expression executed successfully');
696
+ return result;
731
697
  }
732
- /**
733
- * Reconstruct an error from serialized isolate data.
734
- *
735
- * Maps error names back to their host-side classes and restores
736
- * custom properties that would otherwise be lost crossing the boundary.
737
- */
738
- reconstructError(data) {
739
- const error = new Error(data.message);
740
- error.name = data.name || 'Error';
741
- if (data.stack) {
742
- error.stack = data.stack;
698
+ catch (error) {
699
+ // Re-throw reconstructed errors as-is.
700
+ // Note: TypeError is intentionally NOT included here — the isolate's
701
+ // E() handler swallows TypeErrors (failed attack attempts return undefined),
702
+ // so TypeErrors from host callbacks should also go through the generic
703
+ // wrapping for consistent behavior.
704
+ if (error instanceof Error &&
705
+ (error.name === 'ExpressionError' || error.name === 'ExpressionExtensionError')) {
706
+ throw error;
743
707
  }
744
- // Restore custom properties transferred via copy: true
745
- if (data.extra) {
746
- Object.assign(error, data.extra);
708
+ const errorMessage = error instanceof Error ? error.message : String(error);
709
+ if (errorMessage.includes('Script execution timed out')) {
710
+ throw new types_1.TimeoutError(`Expression timed out after ${this.config.timeout}ms`, {});
747
711
  }
748
- return error;
749
- }
750
- /**
751
- * Dispose of the isolate and free resources.
752
- *
753
- * After disposal, the bridge cannot be used again.
754
- */
755
- async dispose() {
756
- if (this.disposed) {
757
- return;
758
- }
759
- // Dispose isolate (this also disposes all contexts, references, etc.)
760
- if (!this.isolate.isDisposed) {
761
- this.isolate.dispose();
712
+ if (errorMessage.includes('memory limit')) {
713
+ throw new types_1.MemoryLimitError(`Expression exceeded memory limit of ${this.config.memoryLimit}MB`, {});
762
714
  }
763
- this.disposed = true;
764
- this.initialized = false;
765
- this.logger.debug('[IsolatedVmBridge] Disposed');
715
+ throw new Error(`Expression evaluation failed: ${errorMessage}`);
766
716
  }
767
- /**
768
- * Check if the bridge has been disposed.
769
- *
770
- * @returns true if disposed, false otherwise
771
- */
772
- isDisposed() {
773
- return this.disposed || this.isolate.isDisposed;
717
+ finally {
718
+ getValueAtPath.release();
719
+ getArrayElement.release();
720
+ callHost.release();
774
721
  }
775
722
  }
776
- exports.IsolatedVmBridge = IsolatedVmBridge;
777
- });
723
+ /**
724
+ * Reconstruct an error from serialized isolate data.
725
+ *
726
+ * Maps error names back to their host-side classes and restores
727
+ * custom properties that would otherwise be lost crossing the boundary.
728
+ */
729
+ reconstructError(data) {
730
+ const error = new Error(data.message);
731
+ error.name = data.name || 'Error';
732
+ if (data.stack) {
733
+ error.stack = data.stack;
734
+ }
735
+ // Restore custom properties transferred via copy: true
736
+ if (data.extra) {
737
+ Object.assign(error, data.extra);
738
+ }
739
+ return error;
740
+ }
741
+ /**
742
+ * Dispose of the isolate and free resources.
743
+ *
744
+ * After disposal, the bridge cannot be used again.
745
+ */
746
+ async dispose() {
747
+ if (this.disposed) {
748
+ return;
749
+ }
750
+ // Dispose isolate (this also disposes all contexts, references, etc.)
751
+ if (!this.isolate.isDisposed) {
752
+ this.isolate.dispose();
753
+ }
754
+ this.disposed = true;
755
+ this.initialized = false;
756
+ this.logger.debug('[IsolatedVmBridge] Disposed');
757
+ }
758
+ /**
759
+ * Check if the bridge has been disposed.
760
+ *
761
+ * @returns true if disposed, false otherwise
762
+ */
763
+ isDisposed() {
764
+ return this.disposed || this.isolate.isDisposed;
765
+ }
766
+ }
767
+ exports.IsolatedVmBridge = IsolatedVmBridge;
778
768
  //# sourceMappingURL=isolated-vm-bridge.js.map