Files
stack/apps/backend/src/lib/js-execution.tsx
T

262 lines
9.6 KiB
TypeScript

import { traceSpan } from '@/utils/telemetry';
import { runAsynchronouslyAndWaitUntil } from '@/utils/background-tasks';
import { getEnvVariable, getNodeEnvironment } from '@hexclave/shared/dist/utils/env';
import { HexclaveAssertionError, captureError } from '@hexclave/shared/dist/utils/errors';
import { Result } from '@hexclave/shared/dist/utils/results';
import { Sandbox } from '@vercel/sandbox';
import { Freestyle as FreestyleClient } from 'freestyle';
export type ExecuteJavascriptOptions = {
nodeModules?: Record<string, string>,
};
export type ExecuteResult =
| { status: "ok", data: unknown }
| { status: "error", error: { message: string, stack?: string, cause?: unknown } };
type JsEngine = {
name: string,
execute: (code: string, options: ExecuteJavascriptOptions) => Promise<ExecuteResult>,
};
function createFreestyleEngine(): JsEngine {
return {
name: 'freestyle',
execute: async (code: string, options: ExecuteJavascriptOptions): Promise<ExecuteResult> => {
const apiKey = getEnvVariable("STACK_FREESTYLE_API_KEY");
let baseUrl = getEnvVariable("STACK_FREESTYLE_API_ENDPOINT", "") || undefined;
if (apiKey === "mock_stack_freestyle_key") {
if (!["development", "test"].includes(getNodeEnvironment())) {
throw new HexclaveAssertionError("Mock Freestyle key used in production; please set the STACK_FREESTYLE_API_KEY environment variable.");
}
if (!baseUrl) {
const prefix = getEnvVariable("NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX", "81");
baseUrl = `http://localhost:${prefix}22`;
}
}
const freestyle = new FreestyleClient({
apiKey,
baseUrl,
});
const response = await freestyle.serverless.runs.create({
code,
nodeModules: options.nodeModules ?? {},
});
if (response.result === undefined) {
throw new HexclaveAssertionError("Freestyle execution returned undefined result", { response, innerCode: code, innerOptions: options });
}
return response.result as ExecuteResult;
},
};
}
function createVercelSandboxEngine(): JsEngine {
return {
name: 'vercel-sandbox',
execute: async (code: string, options: ExecuteJavascriptOptions): Promise<ExecuteResult> => {
const teamId = getEnvVariable("STACK_VERCEL_SANDBOX_TEAM_ID");
const projectId = getEnvVariable("STACK_VERCEL_SANDBOX_PROJECT_ID");
const token = getEnvVariable("STACK_VERCEL_SANDBOX_TOKEN");
const sandbox = await Sandbox.create({
resources: { vcpus: 2 },
timeout: 30000,
runtime: 'node24' as const,
teamId: teamId || undefined,
projectId: projectId || undefined,
token: token || undefined,
});
try {
if (options.nodeModules && Object.keys(options.nodeModules).length > 0) {
const packages = Object.entries(options.nodeModules)
.map(([name, version]) => `${name}@${version}`);
const installResult = await sandbox.runCommand('npm', ['install', '--no-save', ...packages]);
if (installResult.exitCode !== 0) {
throw new HexclaveAssertionError("Failed to install packages in Vercel Sandbox", { exitCode: installResult.exitCode, innerCode: code, innerOptions: options });
}
}
const resultPath = '/vercel/sandbox/result.json';
const runnerScript = `
import { writeFileSync } from 'fs';
import fn from './code.mjs';
const result = await fn();
writeFileSync(${JSON.stringify(resultPath)}, JSON.stringify(result));
`;
await sandbox.writeFiles([
{ path: '/vercel/sandbox/code.mjs', content: Buffer.from(code, 'utf-8') },
{ path: '/vercel/sandbox/runner.mjs', content: Buffer.from(runnerScript, 'utf-8') },
]);
const runResult = await sandbox.runCommand('node', ['/vercel/sandbox/runner.mjs']);
if (runResult.exitCode !== 0) {
throw new HexclaveAssertionError("Vercel Sandbox runner exited with non-zero code", { innerCode: code, innerOptions: options, exitCode: runResult.exitCode });
}
const resultBuffer = await sandbox.readFileToBuffer({ path: resultPath });
if (resultBuffer === null) {
throw new HexclaveAssertionError("Result file not found in Vercel Sandbox", { resultPath, innerCode: code, innerOptions: options });
}
const resultJson = resultBuffer.toString();
try {
return JSON.parse(resultJson);
} catch (e: any) {
throw new HexclaveAssertionError("Failed to parse result from Vercel Sandbox", { resultJson, cause: e, innerCode: code, innerOptions: options });
}
} finally {
await sandbox.stop();
}
},
};
}
const engineMap = new Map<string, JsEngine>([
['freestyle', createFreestyleEngine()],
['vercel-sandbox', createVercelSandboxEngine()],
]);
/**
* Executes the given code with the given options. Returns the result of the code execution
* if it is JSON-serializable. Has undefined behavior if it is not JSON-serializable or if
* the code throws an error.
*/
export async function executeJavascript(code: string, options: ExecuteJavascriptOptions = {}): Promise<ExecuteResult> {
return await traceSpan({
description: 'js-execution.executeJavascript',
attributes: {
'js-execution.code.length': code.length.toString(),
'js-execution.nodeModules.count': options.nodeModules ? Object.keys(options.nodeModules).length.toString() : '0',
}
}, async () => {
if (getEnvVariable("STACK_VERCEL_SANDBOX_TOKEN") != "vercel_sandbox_disabled_for_local_development") {
const shouldSanityTest = Math.random() < 0.05;
if (shouldSanityTest) {
runAsynchronouslyAndWaitUntil(runSanityTest(code, options));
}
return await runWithFallback(code, options);
} else {
if (getNodeEnvironment().includes("prod")) {
throw new HexclaveAssertionError("STACK_VERCEL_SANDBOX_TOKEN is set to the disabled sentinel value in production. Please configure a real Vercel Sandbox token.");
}
return await runWithoutFallback(code, options);
}
});
}
/**
* Compare two execution results for sanity test equality.
* For error results, we only compare status and message (not stack traces,
* which differ between execution environments).
*/
function areResultsEqual(a: ExecuteResult, b: ExecuteResult): boolean {
if (a.status !== b.status) return false;
if (a.status === 'ok' && b.status === 'ok') {
return JSON.stringify(a.data) === JSON.stringify(b.data);
}
if (a.status === 'error' && b.status === 'error') {
return a.error.message === b.error.message;
}
return false;
}
async function runSanityTest(code: string, options: ExecuteJavascriptOptions) {
const results: Array<{ engine: string, result: unknown }> = [];
const failures: Array<{ engine: string, error: unknown }> = [];
for (const [name, engine] of engineMap) {
try {
const result = await engine.execute(code, options);
results.push({ engine: name, result });
} catch (error) {
failures.push({ engine: name, error });
}
}
if (failures.length > 0) {
captureError("js-execution-sanity-test-failures", new HexclaveAssertionError(
`JS execution sanity test: ${failures.length} engine(s) failed`,
{ failures, successfulEngines: results.map(r => r.engine), innerCode: code, innerOptions: options }
));
}
if (results.length < 2) {
return;
}
const referenceResult = results[0].result as ExecuteResult;
const allEqual = results.every(r => areResultsEqual(r.result as ExecuteResult, referenceResult));
if (!allEqual) {
captureError("js-execution-sanity-test-mismatch", new HexclaveAssertionError(
"JS execution sanity test: engines returned different results",
{ results, innerCode: code, innerOptions: options }
));
}
}
async function runWithFallback(code: string, options: ExecuteJavascriptOptions): Promise<ExecuteResult> {
const freestyleEngine = engineMap.get("freestyle")!;
const vercelSandboxEngine = engineMap.get("vercel-sandbox")!;
const maxAttempts = 2;
const retryResult = await Result.retry(
async () => {
try {
const result = await freestyleEngine.execute(code, options);
return Result.ok(result);
} catch (error) {
return Result.error(error);
}
},
maxAttempts,
{ exponentialDelayBase: 500 }
);
if (retryResult.status === 'ok') {
return retryResult.data;
}
captureError(`js-execution-freestyle-failed`, new HexclaveAssertionError(
`JS execution freestyle engine failed, falling back to vercel sandbox engine`,
{ cause: retryResult.error, innerCode: code, innerOptions: options }
));
try {
const result = await vercelSandboxEngine.execute(code, options);
return result;
} catch (error){
captureError(`js-execution-vercel-sandbox-failed`, new HexclaveAssertionError(
`JS execution vercel sandbox engine failed after fallback from freestyle engine`,
{ cause: error, innerCode: code, innerOptions: options }
));
throw new HexclaveAssertionError("Vercel Sandbox service unavailable", { cause: error, innerCode: code, innerOptions: options });
}
}
async function runWithoutFallback(code: string, options: ExecuteJavascriptOptions): Promise<ExecuteResult> {
const freestyleEngine = engineMap.get("freestyle")!;
try {
const result = await freestyleEngine.execute(code, options);
return result;
} catch (error) {
throw new HexclaveAssertionError("Freestyle rendering service unavailable when running without fallback", { cause: error, innerCode: code, innerOptions: options });
}
}