Skip to content

feat(core): Add runtime bundle and extension utilities#2036

Open
everettbu wants to merge 6 commits into
masterfrom
feat/expression-runtime-pr2-runtime-bundle
Open

feat(core): Add runtime bundle and extension utilities#2036
everettbu wants to merge 6 commits into
masterfrom
feat/expression-runtime-pr2-runtime-bundle

Conversation

@everettbu

@everettbu everettbu commented Feb 21, 2026

Copy link
Copy Markdown

Mirror of n8n-io/n8n#26077
Original author: despairblue


Context

n8n expressions today run via tournament (an AST transformer) inside the main Node.js process. The goal of this PR series is to make expression evaluation available inside isolated-vm — a V8 isolate that is memory-isolated from the host process. This gives us a proper security boundary: a malicious expression cannot access Node.js APIs, the filesystem, or the host's memory.

The series is being landed incrementally:

PR What it adds
#26047 ✅ Package scaffold: public types and architecture docs
This PR The runtime bundle that runs inside the isolate
PR 3 (upcoming) IsolatedVmBridge — creates the isolate, loads the bundle, and exposes workflow data to it via callbacks
PR 4 (upcoming) ExpressionEvaluator — the public API that drives the bridge, plus integration tests

What this PR adds

When an expression is evaluated inside an isolate, the isolate has no access to the host process — it cannot require() anything or call Node.js APIs directly. Every piece of code it needs must be bundled into a single self-contained script and injected at isolate startup.

This PR produces that script.

esbuild.config.js builds src/runtime/index.ts and all its imports into two bundle formats:

  • dist/bundle/runtime.iife.js — an IIFE loaded into the isolate as a string via isolated-vm
  • dist/bundle/runtime.esm.js — an ESM module for future Web Worker support

src/runtime/ — the code that runs inside the isolate, split into four focused modules:

  • safe-globals.tsSafeObject and SafeError proxies that block dangerous methods (prototype pollution, stack trace manipulation) from expression code; ExpressionError for tournament-generated error handlers; __sanitize() for dynamic property access (guards against obj["__proto__"] etc.)
  • lazy-proxy.tscreateDeepLazyProxy(), a Proxy that loads workflow data on demand across the isolate boundary. When an expression accesses $json.name, the proxy calls an ivm.Reference callback set by the bridge (PR 3) to fetch just that value — nothing is copied up front.
  • reset.tsresetDataProxies(), called by the bridge before each expression evaluation to clear proxy caches and re-expose $json, $binary, $input, etc. to globalThis.
  • index.ts — entry point: declares global types, exposes extend/extendOptional and DateTime for tournament-transformed expressions, and wires the above modules to globalThis.

src/extensions/ — see the duplication note below.

packages/workflow/src/extensions/*.ts — a 7-line comment prepended to each file documenting the mirror.

Why is src/extensions/ a copy of packages/workflow/src/extensions/?

n8n expressions support method calls like $json.name.toUpperCase() or [1,2,3].sum(). The non-native ones (sum, toDateTime, etc.) are provided by the extension framework in packages/workflow/src/extensions/. The isolate needs these too — but it cannot import them from the host, because all isolate code must be pre-bundled.

The natural fix would be to import them from @n8n`/expression-runtime` and have esbuild bundle them in. The problem is that @n8n/expression-runtime is Vite-aliased to an empty stub for browser builds, to exclude the isolated-vm native addon. Vite analyses dynamic imports statically — even a IS_FRONTEND-guarded import('@n8n/expression-runtime') in packages/workflow would cause the browser build to pull in the stub, meaning the extension functions would be missing at runtime in the editor.

The workaround is to keep a verbatim copy of the extension files inside @n8n`/expression-runtime/src/extensions/`, which esbuild bundles directly into the IIFE. Each mirrored file in `packages/workflow/src/extensions/` has a comment explaining this and pointing to the long-term fix: split @n8n/expression-runtime into a browser-safe subpath (extension utilities only, no isolated-vm) and a node-only subpath (the bridge and evaluator, Vite-stubbed). At that point, packages/workflow can import the extensions directly from the package and the duplication goes away.

Why no tests in this PR?

src/extensions/ — already covered by the existing test suite in packages/workflow/src/extensions/. Duplicating it would mean keeping two suites in sync.

src/runtime/ — the interesting behaviour (createDeepLazyProxy, resetDataProxies) is the cross-isolate data transfer: the proxy calls ivm.Reference callbacks set by the bridge, which live in the host process. That transfer cannot happen without an actual isolate. Unit tests with mocked callbacks would only test that a mock was called — they would not exercise the serialisation, the copy: true transfer semantics, or the metadata protocol (__isArray, __isObject, __isFunction) that the bridge and runtime agree on.

The integration tests that cover the full path — bridge creates isolate → loads runtime bundle → sets callbacks → expression evaluates and produces the correct result — are planned for PR 4, once both the bridge and the evaluator exist.

Verification

cd packages/`@n8n`/expression-runtime
node esbuild.config.js   # produces dist/bundle/runtime.iife.js and runtime.esm.js
pnpm typecheck           # passes

Related Linear tickets, Github issues, and Community forum posts

Review / Merge checklist

  • PR title and summary are descriptive. (conventions)
  • Docs updated or follow-up ticket created.
  • Tests included. (deferred to PR 4 — see note above)
  • PR Labeled with release/backport (if the PR is an urgent fix that needs to be backported)

…ties

- Add esbuild.config.js to bundle src/runtime/index.ts as both IIFE
  (for isolated-vm) and ESM (for Web Workers) into dist/bundle/
- Add src/runtime/index.ts: deep lazy proxy system, SafeObject/SafeError
  wrappers, resetDataProxies(), and library globals for the isolate
- Add src/extensions/: full copy of the n8n-workflow extension utilities
  (array, boolean, date, number, object, string, expression-extension-error,
  extend, extensions, utils) bundled into the runtime IIFE
- Prepend 7-line mirror comment to packages/workflow/src/extensions/*.ts
  documenting the intentional duplication and the path to eliminate it
- Restore build:runtime script in package.json

The runtime bundle is self-contained (lodash and luxon are inlined by
esbuild) and provides the expression evaluation environment loaded into
each isolated-vm context by the bridge (added in PR 3).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@everettbu everettbu added core Enhancement outside /nodes-base and /editor-ui n8n team Authored by the n8n team labels Feb 21, 2026
…d modules

- safe-globals.ts: SafeObject, SafeError, ExpressionError, __sanitize
- lazy-proxy.ts: createDeepLazyProxy
- reset.ts: resetDataProxies
- index.ts: global declarations, library setup, wires everything to globalThis

esbuild bundles all modules regardless; the split is purely for readability.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@everettbu everettbu changed the title feat(@n8n/expression-runtime): Add runtime bundle and extension utilities feat(core): Add runtime bundle and extension utilities Feb 21, 2026
…d globals

Remove unused imports (Duration, Interval, lodash) and drop the
corresponding globalThis assignments for _, Duration, Interval, and
luxon namespace — these are not exposed by the current expression engine.
Update comments in index.ts and reset.ts to match.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@everettbu
everettbu marked this pull request as ready for review February 21, 2026 16:42
shortstacked and others added 3 commits February 23, 2026 11:10
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…ure docs

- Remove getDataSync() reference from security section of ARCHITECTURE.md;
  data access from the isolate uses ivm.Reference callbacks, not a method
  on RuntimeBridge
- Replace NodeVmBridge mentions (does not exist) with accurate references
  to IsolatedVmBridge integration tests
- Remove small-array threshold from deep-lazy-proxy.md test coverage list;
  all arrays are always lazy-loaded via __getArrayElement callbacks
- Remove stale manual-test.example.ts reference (file does not exist)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@everettbu
everettbu force-pushed the feat/expression-runtime-pr2-runtime-bundle branch from 7f24fdf to 576b06b Compare February 23, 2026 12:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core Enhancement outside /nodes-base and /editor-ui n8n team Authored by the n8n team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants