@absolutejs/isolated-jsc

中文文档
0.15.0发布于 2026/11/03BSL-1.1TypeScriptES Module构建来源已验证近 30 天下载 3052

JavaScriptCore-native sandbox for Bun — heap-isolated execution for untrusted code, isolated-vm-shaped API

原始使用文档
文档目录17 节
  1. Why this exists
  2. Quick answers
  3. What ships today (v0.12.2)
  4. Bun sandboxing decision guide
  5. What it ISN'T (honest limits per backend)
  6. Installation note for the FFI backend
  7. API
  8. Fetch handlers
  9. Errors
  10. Install
  11. Doctor
  12. Examples
  13. Benchmarks
  14. Tests
  15. Release checks
  16. Related
  17. License

@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 libJavaScriptCore directly via bun: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 via JSGlobalContextSetEvalEnabled. Available on macOS (system framework) + Linux with libjavascriptcoregtk-4.1-0 or libjavascriptcoregtk-6.0-1 installed.

  • Worker backend (fallback) — one Bun Worker per 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 Isolate runs 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 via Worker.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 with MemoryLimitError.
  • Hardened sandbox by default (T2.1, new in 0.1). Host-capability globals — fetch, Bun, process, Worker, WebSocket, host postMessage / addEventListener, navigator, storage, … — are stripped from the sandbox. User code can't reach them via bare lookup, globalThis.X, this.X, or direct eval('X') (on the Worker backend, indirect eval and dynamic import() still reach host authority unless the runtime can lock the isolate down; see SECURITY.md and requireLockdown). 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 via harden: false for trusted code, or expose specific capabilities via unsafelyExposeGlobals: ['fetch'].
  • Host-callable References. Expose host functions to the isolate. Worker backend: calls always round-trip via async message-passing (use await on the isolate side). FFI backend (0.3+): sync host fns return their value directly through a per-Reference JSCallback. 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 by Script.run's timeout.
  • ExternalCopy for marking large host-side values for cheap pass-through.
  • Optional bounded console capture. onConsole routes isolate console.log calls back to the host. maxConsoleEntries and maxConsoleBytes bound 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, and createContext({ seed, checkpoint }) restores versioned checkpoints from context.checkpoint({ maxBytes, include, exclude }). Checkpoints carry schemaVersion, backend, byte length, included/skipped counts, and skipped-key reasons. context.checkpointWithReceipt() and isolate.createContextWithReceipt() produce schema-v1 CheckpointReceipts — operation: "create" | "restore", executionId, durationMs, aggregated skippedReasons, and optional purpose/tenant/policy labels 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 (HttpError with .statusCode, etc.) survives. instanceof doesn't work across the boundary; use .name / .code checks.
  • 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. Plain run() still returns the bare value.
  • Execution receipts. script.runWithReceipt(), callable.callWithReceipt(), runIsolated(..., { withReceipt: true }), and runner receipt modes return local audit records with schemaVersion: 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 / redactAuditOutput hooks, and each tool can override them before audit events hit logs or receipts.
  • Bounded capability audit buffers. createCapabilityAuditBuffer({ maxEvents }) gives apps a receipt-ready onAudit sink with explicit truncation metadata, avoiding unbounded in-memory audit arrays when sandbox code spams host tools.
  • Capability output limits. Set maxOutputBytes per capability or defaultMaxOutputBytes on a broker to reject oversized host-tool outputs with CapabilityError before they return to sandbox code.
  • Result size limits. Pass maxResultBytes in run options to reject oversized successful outputs with ResultSizeError before application code accepts them.
  • Console output limits. Pass maxConsoleEntries / maxConsoleBytes when creating an isolate to drop excess captured console events and surface overflow flags in receipts.
  • Backend observability. isolate.backend reports the resolved backend ("ffi" or "worker"), runWithMetrics() / callWithMetrics() include metrics.backend, and isolated-jsc doctor --json emits 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 a recipe with recommended result limits, console limits, audit buffer caps, broker caps, and runner pool settings. Helper builders such as policyAuditOptions(policy), policyBrokerOptions(policy), policyConsoleOptions(policy), policyRunOptions(policy), and policyRunnerOptions(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. Use runner.run() for fresh-context source execution, runner.precompile() to warm callables, runner.call() to compile once per key/name and call repeatedly, and runner.stats() to inspect pool/cache size.

Bun sandboxing decision guide

Use caseChooseWhy
Local development, demos, CI smoke tests, or platforms without libJSCbackend: "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 JavaScriptCorebackend: "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 JavaScriptCoreWorker fallback behind a process/container boundaryWorker 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 powersFFI plus Reference / capability brokerKeep the sandbox global small and expose only validated, audited host tools.
High-value secrets, arbitrary third-party code, or network-adjacent workloadsFFI plus process, container, uid, seccomp, and network egress policyJavaScript isolation is one layer; OS isolation still owns blast-radius control.
Trusted app code that only needs cancellation or tenant-level poolingWorker or FFI poolSecurity 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)

PathFFI backendWorker backend
fetch(url) (bare)undefinedundefined
Bun.spawn(...) (bare)undefinedundefined
globalThis.fetch / globalThis.Bunundefinedundefined
this.Bunundefinedundefined
eval('Bun') (direct)blocks (eval disabled per-context)undefined
(0, eval)('Bun') (indirect)blockslocked down: gone; stock Bun: reachable
new Function('return Bun')()blockslocked down: gone; stock Bun: reachable
import('node:child_process') / import('bun:ffi')no module loaderlocked down: rejects; stock Bun: reachable, even without eval (see SECURITY.md)
Math.PI / JSON.stringify / Promise / crypto.randomUUIDreachablereachable
URL / TextEncoder / WebSocketnot present (JSC API only, no Web APIs)reachable (Bun Worker exposes Web APIs)
Cold JSC heap~300 KB~1 MB
Timeout behaviourTerminationException thrown into script; isolate keeps runningisolate dies, must respawn
Memory capwatchdog-polled heapCapacity; terminates on overagepolled 50 ms; terminates whole isolate
Sync host fns via Referencesupported (direct return)always wrapped in await
Async (Promise) host fns via Referencesupported (0.4+) — pumped to settlement, bounded by timeoutsupported via await
Prototype isolation across contextsone isolate per tenantone 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' throws JscLibraryNotFoundError.

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();

Fetch handlers

Run a Web-standard app (export default { fetch(request, env, ctx) }, the shape Workers, Bun.serve and Deno use) in an isolate and call it with host Requests. Worker backend only.

import {
  bundleFetchHandler,
  createFetchHandler,
  createIsolate,
  Reference,
} from "@absolutejs/isolated-jsc";

const source = await bundleFetchHandler("./app.ts"); // one CommonJS bundle, Web APIs only
const isolate = await createIsolate({
  backend: "worker",
  requireLockdown: true,
});
const handler = await createFetchHandler(
  await isolate.createContext(),
  source,
  {
    env: { region: "nyc1", lookup: new Reference(async (id) => db.user(id)) },
  },
);

const response = await handler.fetch(
  new Request("https://app.example/users/7"),
);
const { metrics } = await handler.fetchWithMetrics(request); // cpuMs is thread CPU time
  • env holds data and Reference capabilities (each an async function calling back to the host).
  • ctx.state persists on the context between requests; context.checkpoint({ include: ["state"] }) captures it for hibernation.
  • ctx.waitUntil(promise) lets work finish after the response.
  • Bodies are buffered and capped at 1 MiB each way (maxRequestBytes, maxResponseBytes); FetchBodyTooLargeError says which side overflowed.
  • bundleFetchHandler refuses what an isolate cannot provide (node: and bun: imports, Node built-ins with no installed package, native addons) and lists every such import in a FetchHandlerBundleError. Bundling can run the app's macros, so run it where you would run the app's own build.
  • An Elysia 2 app runs through it unchanged: export default { fetch: (request) => app.handle(request) }.

Errors

  • FetchBodyTooLargeError — a fetch handler's request or response body exceeded its limit.
  • FetchHandlerBundleError — an app imports Bun or Node APIs or a native addon.
  • TimeoutError — wall-clock budget elapsed.
  • MemoryLimitError — heap exceeded the configured cap.
  • ResultSizeError — successful output exceeded maxResultBytes.
  • IsolateDisposedError — operation on a disposed isolate.
  • CompileError — syntax error in script source.

Install

bun add @absolutejs/isolated-jsc

Requires Bun ≥ 1.3.

Doctor

bunx @absolutejs/isolated-jsc
bunx @absolutejs/isolated-jsc --json
# or, from this repo:
bun src/doctor.ts

The 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:checkpoint

The 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:proof

This 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 test

The 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:release

The 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.

  • @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].

版本与发布记录

@absolutejs/isolated-jsc 共发布 54 个版本,首次发布于 2026/05/27;过去 12 个月发布了 54 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.15.0latest2026/11/034 天前
0.14.22026/11/034 天前
0.14.12026/11/034 天前
0.14.02026/11/034 天前
0.13.12026/11/034 天前
0.13.02026/11/034 天前

关于 @absolutejs/isolated-jsc 的常见问题

@absolutejs/isolated-jsc 是什么?

JavaScriptCore-native sandbox for Bun — heap-isolated execution for untrusted code, isolated-vm-shaped API

如何安装 @absolutejs/isolated-jsc?

在项目目录运行 npm install @absolutejs/isolated-jsc,或使用 pnpm add / yarn add / bun add。它还提供命令行工具 isolated-jsc、isolated-jsc-doctor,可以用 npx isolated-jsc 直接运行。

@absolutejs/isolated-jsc 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@absolutejs/isolated-jsc 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@absolutejs/isolated-jsc 可以商用吗?

@absolutejs/isolated-jsc 使用 BSL-1.1 许可。声明的许可为 BSL-1.1,请阅读许可原文确认使用条件。以上为概括,不构成法律意见,请以许可原文为准。

@absolutejs/isolated-jsc 的下载量有多少?

根据 npm 官方下载统计,@absolutejs/isolated-jsc 在 2026/10/05 – 2026/11/03 这 30 天内被下载 3,052 次,日均约 102 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@absolutejs/isolated-jsc 还在维护吗?

最新版本 0.15.0 发布于 2026/11/03(4 天前);至今共发布 54 个版本,其中过去 12 个月发布了 54 个;首次发布于 2026/05/27。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。