first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,75 @@
import { readFile } from 'node:fs/promises';
import { randomUUID } from 'node:crypto';
import ivm from 'isolated-vm';
import type { IDataObject } from 'n8n-workflow';
// Singleton recreated only after resetSandboxCache() (tests) or isolate disposal.
let sandboxIsolate: ivm.Isolate | null = null;
let sandboxContext: ivm.Context | null = null;
/** Disposes the cached isolate. Exposed for tests only. */
export function resetSandboxCache(): void {
sandboxContext = null;
if (sandboxIsolate && !sandboxIsolate.isDisposed) {
sandboxIsolate.dispose();
}
sandboxIsolate = null;
}
/** Returns a cached isolated-vm context with alasql pre-loaded. Creates it on first call. */
export async function loadAlaSqlSandbox(): Promise<ivm.Context> {
if (sandboxContext && sandboxIsolate && !sandboxIsolate.isDisposed) {
return sandboxContext;
}
sandboxIsolate = new ivm.Isolate({ memoryLimit: 64 }); // 64 MB hard limit
sandboxContext = await sandboxIsolate.createContext();
// Browser bundle only no Node.js fs/require handlers inside the isolate.
// eslint-disable-next-line @typescript-eslint/no-require-imports
const alasqlBundlePath = require.resolve('alasql/dist/alasql.min.js');
await sandboxContext.eval(await readFile(alasqlBundlePath, 'utf-8'));
await sandboxContext.eval('Object.freeze(alasql.fn)');
return sandboxContext;
}
/**
* Runs a SQL query against plain-object table data inside the isolated-vm sandbox.
* Only JSON-serialisable values cross the isolate boundary.
*/
export async function runAlaSqlInSandbox(
context: ivm.Context,
tableData: unknown[][],
query: string,
): Promise<IDataObject[]> {
// UUID per invocation so concurrent calls sharing the singleton context
// don't collide on alasql.databases.
const dbId = randomUUID();
// Double-serialization: outer JSON.stringify produces a JSON string, inner produces a JSON literal
// embedded in the script source. Inside the isolate, JSON.parse reconstructs the plain array.
// This ensures data enters the isolate as a parsed JSON literal, never as live objects.
const script = `(function() {
const __rows = JSON.parse(${JSON.stringify(JSON.stringify(tableData))});
const __db = new alasql.Database(${JSON.stringify(dbId)});
try {
for (let i = 0; i < __rows.length; i++) {
__db.exec('CREATE TABLE input' + (i + 1));
__db.tables['input' + (i + 1)].data = __rows[i];
}
return JSON.stringify(__db.exec(${JSON.stringify(query)}));
} finally {
delete alasql.databases[${JSON.stringify(dbId)}];
}
})()`;
const resultJson = (await context.eval(script, { timeout: 5000, copy: true })) as string;
try {
return JSON.parse(resultJson) as IDataObject[];
} catch (e) {
throw new Error(`Failed to parse SQL result: ${(e as Error).message}`);
}
}