@absolutejs/isolated-jsc
v0.12.5
Published
JavaScriptCore-native sandbox for Bun — heap-isolated execution for untrusted code, isolated-vm-shaped API
Maintainers
Readme
@absolutejs/isolated-jsc
JavaScriptCore-native sandbox for Bun. Heap-isolated execution for untrusted code, with an
isolated-vm-shaped API.
import { createIsolate, Reference } from "@absolutejs/isolated-jsc";
const isolate = await createIsolate({ memoryLimit: 64 });
const context = await isolate.createContext();
await context.setGlobal(
"log",
new Reference((msg) => console.log("[tenant]", msg)),
);
const script = await isolate.compileScript('await log("hello"); 1 + 1');
const result = await script.run(context, { timeout: 500 });
// result === 2
await isolate.dispose();Why this exists
Bun has no equivalent to Node's isolated-vm. The Node library is V8-specific — it links against V8's HasCustomHostObject ABI symbol — and Bun uses JavaScriptCore, not V8. So bun install isolated-vm succeeds, then import fails with undefined symbol: HasCustomHostObject.
This leaves an entire category of applications stranded on Node:
- AI agent code execution. Anthropic's
code_execution, the OpenAI Code Interpreter, every "run this LLM-generated snippet" tool — these all need a heap-isolated runtime with hard resource limits. (oven-sh/bun#25929) - Multi-tenant scripting. Cloudflare-Workers-style "your customer wrote some JS, run it scoped to their account."
- Build-time evaluation of untrusted plugins. (oven-sh/bun#23653)
- General sandboxing permissions. (oven-sh/bun#6617)
@absolutejs/isolated-jsc fills that gap. See BUN_POSITIONING.md for the Bun-focused strategic frame, ISSUES_WILL_CLOSE.md for the upstream issues this library closes, and UPSTREAM_ISSUES.md for the upstream Bun bugs this library works around (with cleanup instructions for when each is fixed). See MIGRATING_FROM_ISOLATED_VM.md for the Node isolated-vm to Bun migration path. See SECURITY.md for the threat model and hardening guidance. See SNAPSHOT_RESEARCH.md for the JavaScriptCore checkpoint research. See CHANGELOG.md for release notes.
Quick answers
Why not Node isolated-vm? It is the right shape for Node/V8, but Bun is JavaScriptCore. isolated-jsc ports the isolate-shaped API to Bun/JSC instead of trying to load a V8 addon.
Why not just use Bun Workers? Workers are the portable substrate and the fallback backend. This package adds the sandbox product layer: hardened globals, heap limits, timeouts, metrics, error fidelity, TypeScript helpers, pools, and explicit host capability brokers.
When should I require FFI? Require backend: "ffi" for hostile-code production paths on macOS or Linux where JavaScriptCore is available. Use backend: "auto" for portable defaults, demos, and CI. Add process/container/uid/network boundaries whenever a sandbox escape would expose meaningful host secrets.
What ships today (v0.12.2)
@absolutejs/isolated-jsc runs on two interchangeable backends behind one API:
FFI backend (default when libJSC is reachable) — talks to
libJavaScriptCoredirectly viabun:ffi. Cold heap ~300 KB, CPU timeouts use JSC's interrupt-driven watchdog (the isolate keeps running after a TimeoutError), and(0, eval)('X')/new Function('return X')()are blocked entirely viaJSGlobalContextSetEvalEnabled. Available on macOS (system framework) + Linux withlibjavascriptcoregtk-4.1-0orlibjavascriptcoregtk-6.0-1installed.Worker backend (fallback) — one Bun
Workerper isolate. Its worker-local JSC heap starts around 1 MB on Bun 1.3.x; the full worker process footprint is larger. Timeout terminates the whole isolate. Always available (no system dependency). Default on Windows and any Linux without libJSC.
The two share every public type. Pick explicitly with createIsolate({ backend: 'ffi' | 'worker' | 'auto' }), or let 'auto' (the default) probe for libJSC and fall back to Worker. Both backends:
- Heap isolation. Each
Isolateruns in its own Bun Worker → its own JSC VM → its own GC heap. No memory sharing with the host or with peer isolates. isolated-vm-shaped API.Isolate,Context,Script,Reference,ExternalCopy. Port-friendly.- Wall-clock timeouts.
script.run(context, { timeout: 500 })— millisecond accuracy, enforced from the host viaWorker.terminate(). v1 trade-off: timeout terminates the entire isolate (pool at the app layer if you need to recycle). - Memory limits. Soft cap polled every 50 ms via the worker-local
bun:jsc.heapSize(). Host and peer-worker allocation is excluded. Breach posts a fatal and self-terminates; the host rejects pending ops withMemoryLimitError. - Hardened sandbox by default (T2.1, new in 0.1). Host-capability globals —
fetch,Bun,process,Worker,WebSocket, hostpostMessage/addEventListener,navigator, storage, … — are stripped from the sandbox. User code can't reach them via bare lookup,globalThis.X,this.X, or directeval('X'). Pure JS built-ins (Math, JSON, Promise, the typed-array suite) and safe Web primitives (URL, TextEncoder, Web Crypto, setTimeout, console) stay reachable. Opt out per-isolate viaharden: falsefor trusted code, or expose specific capabilities viaunsafelyExposeGlobals: ['fetch']. - Host-callable
References. Expose host functions to the isolate. Worker backend: calls always round-trip via async message-passing (useawaiton the isolate side). FFI backend (0.3+): sync host fns return their value directly through a per-ReferenceJSCallback. Async (Promise-returning) host fns work too on FFI (0.4+) — the runner alternately yields to Bun's event loop and drains JSC's microtask queue until the promise settles, bounded byScript.run'stimeout. ExternalCopyfor marking large host-side values for cheap pass-through.- Optional bounded console capture.
onConsoleroutes isolateconsole.logcalls back to the host.maxConsoleEntriesandmaxConsoleBytesbound forwarded logs, and receipts report overflow. - First-class isolate pool (T2.2, new in 0.1).
createIsolatePool({ isolate, maxSize, idleMs, recycleAfter })returns a keyed pool — lazy spawn per key, reuse across calls, LRU eviction at cap, transparent re-spawn after isolate self-termination, configurable post-N-call recycle to bound JSC heap creep. Replaces the bespoke per-tenant lookup-or-spawn map every consumer rolls. - Context seed + data checkpoint (T2.3, new in 0.1; expanded in 0.8.19; receipts added in 0.8.21).
createContext({ seed, snapshot })still restores old plain snapshots, andcreateContext({ seed, checkpoint })restores versioned checkpoints fromcontext.checkpoint({ maxBytes, include, exclude }). Checkpoints carryschemaVersion, backend, byte length, included/skipped counts, and skipped-key reasons.context.checkpointWithReceipt()andisolate.createContextWithReceipt()produce schema-v1CheckpointReceipts —operation: "create" | "restore",executionId,durationMs, aggregatedskippedReasons, and optionalpurpose/tenant/policylabels for audit workflows. This is a data checkpoint, not a JSC heap pause/resume image; see SNAPSHOT_RESEARCH.md. - Error fidelity (T2.4, new in 0.1). Errors thrown inside the isolate round-trip with
error.cause(recursively) and enumerable own properties intact. Custom Error subclasses' instance data (HttpErrorwith.statusCode, etc.) survives.instanceofdoesn't work across the boundary; use.name/.codechecks. - Per-run telemetry (T2.4, new in 0.1).
script.runWithMetrics(ctx, opts)returns{ result, metrics: { backend, cpuMs, heapBytes } }for billing / dashboards / per-call monitoring. Plainrun()still returns the bare value. - Execution receipts.
script.runWithReceipt(),callable.callWithReceipt(),runIsolated(..., { withReceipt: true }), and runner receipt modes return local audit records withschemaVersion: 1, execution id, backend, policy, resource settings, timing, output size, bounded capability-call summaries, and dropped-event counts. - Capability audit redaction. Brokers support default
redactAuditInput/redactAuditOutputhooks, and each tool can override them before audit events hit logs or receipts. - Bounded capability audit buffers.
createCapabilityAuditBuffer({ maxEvents })gives apps a receipt-readyonAuditsink with explicit truncation metadata, avoiding unbounded in-memory audit arrays when sandbox code spams host tools. - Capability output limits. Set
maxOutputBytesper capability ordefaultMaxOutputByteson a broker to reject oversized host-tool outputs withCapabilityErrorbefore they return to sandbox code. - Result size limits. Pass
maxResultBytesin run options to reject oversized successful outputs withResultSizeErrorbefore application code accepts them. - Console output limits. Pass
maxConsoleEntries/maxConsoleByteswhen creating an isolate to drop excess captured console events and surface overflow flags in receipts. - Backend observability.
isolate.backendreports the resolved backend ("ffi"or"worker"),runWithMetrics()/callWithMetrics()includemetrics.backend, andisolated-jsc doctor --jsonemits machine-readable backend probe details. - Policy recipes.
createIsolate({ policy: "ai-tool" | "tenant-script" | "plugin" | "trusted" })applies standardized isolate/run defaults.resolveIsolatePolicy(name, overrides?)returns the same policy object plus arecipewith recommended result limits, console limits, audit buffer caps, broker caps, and runner pool settings. Helper builders such aspolicyAuditOptions(policy),policyBrokerOptions(policy),policyConsoleOptions(policy),policyRunOptions(policy), andpolicyRunnerOptions(policy)turn those recommendations into copy-safe options for the surrounding APIs. - One-shot execution.
runIsolated(source, { policy, globals, context, run, withMetrics })covers request/response paths that do not need to manage isolate lifecycle directly. - Reusable runners.
createIsolatedRunner({ policy, globals, pool })reuses isolates by key for hot tenant/session/conversation paths. Userunner.run()for fresh-context source execution,runner.precompile()to warm callables,runner.call()to compile once per key/name and call repeatedly, andrunner.stats()to inspect pool/cache size.
Bun sandboxing decision guide
| Use case | Choose | Why |
| -------------------------------------------------------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Local development, demos, CI smoke tests, or platforms without libJSC | backend: "auto" | Uses FFI when available and Worker when it is not, so the same code runs everywhere. |
| Production untrusted code on macOS or Linux where you can install JavaScriptCore | backend: "ffi" | Lowest cold heap, interrupt-driven CPU timeouts, isolate survives timeouts, and eval / Function-constructor residuals are closed. |
| Production untrusted code on Windows or hosts without system JavaScriptCore | Worker fallback behind a process/container boundary | Worker keeps heap isolation and resource caps, but hostile workloads should not share host secrets or broad network/file permissions. |
| User plugins that need explicit host powers | FFI plus Reference / capability broker | Keep the sandbox global small and expose only validated, audited host tools. |
| High-value secrets, arbitrary third-party code, or network-adjacent workloads | FFI plus process, container, uid, seccomp, and network egress policy | JavaScript isolation is one layer; OS isolation still owns blast-radius control. |
| Trusted app code that only needs cancellation or tenant-level pooling | Worker or FFI pool | Security posture matters less; optimize for deployment reach and operational simplicity. |
Rule of thumb: use backend: "ffi" for hostile-code production paths, backend: "auto" for portable defaults, and add OS/process isolation whenever sandbox escape would expose meaningful host secrets.
What it ISN'T (honest limits per backend)
| Path | FFI backend | Worker backend |
| -------------------------------------------------------------- | ------------------------------------------------------------------ | --------------------------------------- |
| fetch(url) (bare) | undefined | undefined |
| Bun.spawn(...) (bare) | undefined | undefined |
| globalThis.fetch / globalThis.Bun | undefined | undefined |
| this.Bun | undefined | undefined |
| eval('Bun') (direct) | blocks (eval disabled per-context) | undefined |
| (0, eval)('Bun') (indirect) | blocks | reachable — documented residual |
| new Function('return Bun')() | blocks | reachable — documented residual |
| Math.PI / JSON.stringify / Promise / crypto.randomUUID | reachable | reachable |
| URL / TextEncoder / WebSocket | not present (JSC API only, no Web APIs) | reachable (Bun Worker exposes Web APIs) |
| Cold JSC heap | ~300 KB | ~1 MB |
| Timeout behaviour | TerminationException thrown into script; isolate keeps running | isolate dies, must respawn |
| Memory cap | watchdog-polled heapCapacity; terminates on overage | polled 50 ms; terminates whole isolate |
| Sync host fns via Reference | supported (direct return) | always wrapped in await |
| Async (Promise) host fns via Reference | supported (0.4+) — pumped to settlement, bounded by timeout | supported via await |
| Prototype isolation across contexts | one isolate per tenant | one isolate per tenant |
Installation note for the FFI backend
- macOS: zero install. JavaScriptCore is a system framework at
/System/Library/Frameworks/JavaScriptCore.framework. - Linux (Debian / Ubuntu):
sudo apt install libjavascriptcoregtk-4.1-0(8 MB / 31 MB installed). - Linux (Fedora):
sudo dnf install webkit2gtk4.1. - Linux with Playwright already installed: we accept Playwright's bundled
libjavascriptcoregtk-6.0.so.1— no extra install. - Windows: not supported (no system JSC, no realistic distribution path).
createIsolate({ backend: 'auto' })falls back to Worker automatically;backend: 'ffi'throwsJscLibraryNotFoundError.
API
import {
createCapabilityAuditBuffer,
createCapabilityBroker,
defineCapabilityTool,
compileTypeScriptCallable,
compileTypeScriptCallableFile,
compileTypeScriptFile,
createIsolate,
createIsolatedRunner,
policyAuditOptions,
policyBrokerOptions,
policyConsoleOptions,
policyRunnerOptions,
resolveIsolatePolicy,
runIsolated,
runIsolatedFile,
Reference,
ExternalCopy,
validateContextCheckpoint,
type Isolate,
type Context,
type Script,
} from "@absolutejs/isolated-jsc";
// Create an isolate (one per untrusted tenant).
const isolate: Isolate = await createIsolate({
memoryLimit: 256, // MB; default 256
bootstrap: "var foo = 1", // optional — runs once in the worker
maxConsoleEntries: 100,
maxConsoleBytes: 64_000,
onConsole: (level, args) => console.log(`[iso/${level}]`, ...args),
});
// One or more contexts (fresh global scopes) per isolate.
const context: Context = await isolate.createContext();
// Expose a host function callable from inside the isolate.
const dbQuery = new Reference(async (sql: string) => {
return await myDb.query(sql); // host-side call, tenant-scoped
});
await context.setGlobal("db", dbQuery);
// Compile + run.
const script: Script = await isolate.compileScript(`
(async () => {
const rows = await db('SELECT * FROM users');
return rows.length;
})()
`);
const count = await script.run(context, { timeout: 500 });
// count === number of rows
// TypeScript source: transpiled with Bun before execution in the isolate.
const handler = await compileTypeScriptCallable(
context,
"async (name: string): Promise<string> => name.trim().toUpperCase()",
);
await handler.call([" alex "]); // "ALEX"
// Real files work too. Keep generics, types, and complex tenant/plugin code in
// normal `.ts`, `.tsx`, `.js`, or `.jsx` files instead of string literals.
const typedScript = await compileTypeScriptFile(
isolate,
"./tenant-code/score.ts",
);
await typedScript.run(context);
// Callable files use a default export:
//
// type Box<T> = { value: T };
// export default function unwrap<T>(box: Box<T>): T {
// return box.value;
// }
//
const unwrap = await compileTypeScriptCallableFile(
context,
"./tenant-code/unwrap.ts",
);
await unwrap.call([{ value: "typed file" }]); // "typed file"
// Explicit data checkpoints for turn/session handoff. These capture
// structured-cloneable own globals only, not a JSC heap image.
const checkpoint = await context.checkpoint({
exclude: ["scratch"],
maxBytes: 64 * 1024,
});
const persisted = JSON.stringify(checkpoint);
const restoredCheckpoint = JSON.parse(persisted);
validateContextCheckpoint(restoredCheckpoint);
const resumed = await isolate.createContext({
checkpoint: restoredCheckpoint,
seed: "this.double = (n) => n * 2",
});
// checkpoint.schemaVersion === 1
// checkpoint.skipped === [{ key, reason, bytes? }, ...]
// Versioned checkpoint receipts for operational evidence. Mirrors the
// ExecutionReceipt shape: schemaVersion:1, durationMs, startedAt/endedAt,
// executionId, plus operation:"create"|"restore" and aggregated
// skippedReasons counts. Pass purpose/tenant for audit-log workflows;
// errors rethrow with `.receipt` attached.
const audited = await context.checkpointWithReceipt({
exclude: ["scratch"],
maxBytes: 64 * 1024,
purpose: "turn-handoff",
tenant: "tenant-a",
});
// audited.receipt.operation === "create"
// audited.receipt.skippedReasons === { excluded, notClonable, overMaxBytes }
const restored = await isolate.createContextWithReceipt({
checkpoint: audited.checkpoint,
purpose: "turn-handoff",
tenant: "tenant-a",
});
// restored.receipt.operation === "restore"
// restored.receipt.sourceBackend === "ffi" | "worker"
type TenantContext = { id: string };
type LookupOrderInput = { id: string };
type Order = {
cardLast4: string;
customerEmail: string;
id: string;
status: string;
};
const policy = resolveIsolatePolicy("ai-tool", {
maxResultBytes: 32 * 1024,
timeout: 750,
});
const audit = createCapabilityAuditBuffer<TenantContext>({
...policyAuditOptions(policy),
});
const redactEmail = (email: string) => {
const [name, domain] = email.split("@");
return domain === undefined
? "[email redacted]"
: `${name.slice(0, 2)}***@${domain}`;
};
const broker = createCapabilityBroker(
{
lookupOrder: defineCapabilityTool<
LookupOrderInput,
Order | null,
TenantContext
>({
description: "Read one order by id for the current tenant",
risk: "read-only",
input: { name: "LookupOrderInput" },
output: "Order | null",
maxOutputBytes: 16_384,
redactAuditInput: (input) => ({ id: (input as { id?: unknown }).id }),
redactAuditOutput: (output) => {
if (output === null) return null;
const order = output as Order;
return {
cardLast4: order.cardLast4,
customerEmail: redactEmail(order.customerEmail),
id: order.id,
status: order.status,
};
},
timeoutMs: 250,
validateInput: (input) => {
if (input === null || typeof input !== "object") {
throw new Error("lookupOrder input must be an object");
}
const id = (input as { id?: unknown }).id;
if (typeof id !== "string") {
throw new Error("lookupOrder input requires a string id");
}
return { id };
},
handler: async ({ id }, tenant) => await lookupOrder(tenant.id, id),
}),
chargeCard: defineCapabilityTool<
{ cardToken: string; orderId: string },
{ authorizationId: string; processorTraceId: string },
TenantContext
>({
description: "Charge an opaque card token for one tenant order",
input: "ChargeInput",
output: "ChargeResult",
risk: "write",
redactAuditInput: (input) => ({
cardToken: "[token redacted]",
orderId: (input as { orderId?: unknown }).orderId,
}),
redactAuditOutput: (output) => ({
authorizationId: (output as { authorizationId?: unknown })
.authorizationId,
processorTraceId: "[trace redacted]",
}),
validateInput: (input) => input as { cardToken: string; orderId: string },
handler: async (input, tenant) => await chargeCard(tenant.id, input),
}),
},
{
context: { id: "tenant_123" },
...policyBrokerOptions(policy),
onAudit: audit.onAudit,
redactAuditInput: () => "[input redacted by default]",
redactAuditOutput: () => "[output redacted by default]",
},
);
// Host-side direct calls are typed from the tool map.
const order = await broker.call("lookupOrder", { id: "ord_123" });
// ^? Order | null
// Reviewable capability manifest for docs, audits, and agent/tool UIs.
broker.manifest();
// [{
// name: "lookupOrder",
// schemaVersion: 1,
// description: "Read one order by id for the current tenant",
// risk: "read-only",
// input: { name: "LookupOrderInput" },
// output: "Order | null",
// maxOutputBytes: 16384,
// timeoutMs: 250,
// hasInputValidator: true,
// hasOutputValidator: false,
// redactsInput: true,
// redactsOutput: true
// }]
// Audit events and receipts retain the operational proof, not raw secrets:
// order ids stay visible
// emails become ad***@example.com
// card tokens become [token redacted]
// processor trace ids become [trace redacted]
// Sandbox calls still use an untrusted-code-safe Reference.
await context.setGlobal("tools", broker.reference);
// Policy recipes standardize isolate construction, default timeout, default
// result-size limits, and recommended audit/broker/runner settings.
const policyIsolate = await createIsolate({
policy,
...policyConsoleOptions(policy),
});
const policyContext = await policyIsolate.createContext();
const policyScript = await policyIsolate.compileScript("1 + 1");
await policyScript.run(policyContext);
await policyIsolate.dispose();
// One-shot execution for request/response paths.
const oneShot = await runIsolated<number>("input.n * 2", {
policy,
globals: { input: { n: 21 } },
});
// oneShot === 42
const oneShotFile = await runIsolatedFile<number>("./tenant-code/score.ts", {
policy,
globals: { input: { n: 21 } },
});
// oneShotFile === 42
const measured = await runIsolated<number>("input.n + 1", {
policy: "tenant-script",
globals: { input: { n: 41 } },
withMetrics: true,
});
// measured.result === 42; measured.metrics.backend === "ffi" | "worker"
const receipted = await runIsolated<number>("input.n + 1", {
policy: "tenant-script",
globals: { input: { n: 41 } },
run: {
...audit.receiptOptions(),
executionId: "exec_123",
purpose: "ai-tool-call",
tenant: "tenant_123",
},
withReceipt: true,
});
// receipted.result === 42
// receipted.receipt.status === "success"
// receipted.receipt.backend === "ffi" | "worker"
// Reusable runner for repeated request/response paths.
const runner = createIsolatedRunner({
...policyRunnerOptions(policy),
policy,
globals: { tools: broker.reference },
});
const tenantResult = await runner.run<number>("input.n * 2", {
key: "tenant_123",
globals: { input: { n: 21 } },
});
// tenantResult === 42
const tenantFileResult = await runner.runFile<number>(
"./tenant-code/score.ts",
{
key: "tenant_123",
globals: { input: { n: 21 } },
},
);
// tenantFileResult === 42
await runner.precompile(
"scoreFormula",
"(input) => input.base * 2 + input.bonus",
{ key: "tenant_123" },
);
const callableResult = await runner.call<number>(
"scoreFormula",
"(input) => input.base * 2 + input.bonus",
[{ base: 20, bonus: 2 }],
{ key: "tenant_123" },
);
// callableResult === 42
await runner.precompileFile("unwrap", "./tenant-code/unwrap.ts", {
key: "tenant_123",
});
const callableFileResult = await runner.callFile<string>(
"unwrap",
"./tenant-code/unwrap.ts",
[{ value: "typed file" }],
{ key: "tenant_123" },
);
// callableFileResult === "typed file"
// runner.stats() === { poolSize: 1, callableCacheSize: 1, callablesByKey: { tenant_123: 1 } }
await runner.dispose();
await isolate.dispose();Errors
TimeoutError— wall-clock budget elapsed.MemoryLimitError— heap exceeded the configured cap.ResultSizeError— successful output exceededmaxResultBytes.IsolateDisposedError— operation on a disposed isolate.CompileError— syntax error in script source.
Install
bun add @absolutejs/isolated-jscRequires Bun ≥ 1.3.
Doctor
bunx @absolutejs/isolated-jsc
bunx @absolutejs/isolated-jsc --json
# or, from this repo:
bun src/doctor.tsThe doctor prints Bun/platform details, FFI backend availability, JavaScriptCore flavor/path when found, checked library paths when missing, and the install hint for the current platform. Pass --json for machine-readable CI or deployment checks.
Examples
bun run example:agent-tool
bun run example:checkpointThe agent-tool example combines TypeScript callable compilation, a capability broker, tenant context, tool timeouts, audit events, and per-call metrics. The checkpoint example shows persisted checkpoint JSON, restore validation, and seed-based resume.
Benchmarks
bun run bench:proofThis writes BENCHMARKS.md with local measurements for the FFI backend, Worker backend, Bun process-spawn baseline, and Node isolated-vm when that optional native package is installed in the Node environment.
Tests
bun install
bun testThe test suite covers compile/run, contexts, Reference call-through, ExternalCopy, timeout, memory caps, dispose idempotency, pool behavior, TypeScript helpers, capability brokers, FFI behavior, host-reachability documentation, and hostile-tenant stress cases.
Release checks
bun run check:releaseThe release check runs the package typecheck, build, unit tests, agent-tool example, and the browser smoke in the sibling ../examples/isolated-jsc app. Keep the absolutejs/examples repo checked out next to this package repo when cutting releases so the demo path is covered before publish.
Related
@absolutejs/sync— reactive sync engine. Will eventually use this library to sandbox per-tenant mutation handlers in the hosted PaaS.- Bun docs:
bun:jsc— the JSC primitives this library builds on. isolated-vm— the V8/Node equivalent this library mirrors (we owe Andrew Laverdet the API shape).
License
Business Source License 1.1 — production use is free except offering it as a competing hosted sandbox/isolate service (see the Additional Use Grant in LICENSE). Converts to Apache 2.0 on May 29, 2030. Commercial licensing available — contact [email protected].
