npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

fc-sandbox-sdk

v0.3.0

Published

TypeScript SDK for the fc-spawn microVM sandbox control plane.

Readme

fc-sandbox-sdk

TypeScript SDK for the fc-spawn microVM sandbox control plane — spawn Firecracker VMs, run commands, move files, and manage networks.

v0.2 is a redesign: createSandbox() returns a stateful Sandbox handle instead of a raw response, errors are a typed hierarchy, and the transport retries transient failures automatically.

Install

npm install fc-sandbox-sdk

Requires Node 20+ (or any runtime with global fetch, ReadableStream and AbortSignal.any — Bun, Deno, modern edge runtimes).

Quick start

import { FcClient } from "fc-sandbox-sdk";

const fc = new FcClient({ apiKey: process.env.FC_API_KEY });

const sandbox = await fc.createSandbox({
  shape: "s-1vcpu-256mb",
  rootfs: "devbox:1",
});

try {
  const { result } = await sandbox.runCommand("node", ["--version"]);
  console.log(result.stdout); // "v20.x.x"
} finally {
  await sandbox.destroy();
}

createSandbox blocks until the sandbox reaches running.

Configuration

Every option is optional. apiKey and baseUrl fall back to the FC_API_KEY and FC_BASE_URL environment variables. apiKey is sent as X-Api-Key. Auth is required for control-plane calls: provide either apiKey or authHeaders.

const fc = new FcClient({
  apiKey: "sk-...",                 // or env FC_API_KEY
  baseUrl: "https://fc-spawn...",   // or env FC_BASE_URL
  timeoutMs: 30_000,                // per-request deadline (default 60s)
  retry: { maxRetries: 2, baseDelayMs: 500, maxDelayMs: 30_000 },
  headers: { "x-team": "platform" },// merged into every request
});

Use authHeaders when the SDK is talking to your own API/proxy and your app auth is not an fc-spawn API key:

const fc = new FcClient({
  baseUrl: "https://api.your-app.com/fc",
  authHeaders: {
    Authorization: `Bearer ${sessionToken}`,
    "X-Workspace-Id": workspaceId,
  },
});

apiKey and authHeaders are mutually exclusive.

// Zero-config: reads FC_API_KEY + FC_BASE_URL from the environment.
const fc = new FcClient();

Every method takes a final options argument for per-call overrides:

await fc.whoami({ timeoutMs: 5_000, retry: false, signal: ac.signal });

Creating sandboxes

const sandbox = await fc.createSandbox({
  shape: "s-1vcpu-256mb",          // required — see fc.listShapes()
  rootfs: "devbox:1",              // catalog name or template id/name
  name: "build-worker",            // optional; auto-generated if omitted
  envs: { NODE_ENV: "production" },// injected into every command
  egress: ["pypi.org", "1.1.1.1"], // allowlist; omit for allow-all
  disk_mib: 20480,                 // overlay disk; 0 = shape default
  ingress_enabled: true,           // enable public HTTP ingress
  node_selector: { region: "nyc1" },// optional; keys must match host labels
});

Return immediately instead of waiting for running:

const sandbox = await fc.createSandbox({ shape: "s-1vcpu-256mb" }, { wait: false });
console.log(sandbox.status); // "creating" — or "running" if the spawn already finished
await sandbox.waitUntilRunning({ timeoutMs: 60_000 });

Skip the client object entirely for one-off scripts — Sandbox.create constructs the client for you:

import { Sandbox } from "fc-sandbox-sdk";

const sandbox = await Sandbox.create(
  { shape: "s-1vcpu-256mb", ingress_enabled: true },
  { apiKey: process.env.FC_API_KEY },
);

Connecting to existing sandboxes

const sandbox = await fc.getSandbox("sb_01K...");
const byIp = await fc.getSandboxByIP("10.0.0.2");

const running = await fc.listSandboxes({ status: "running", limit: 100 });
for (const sbx of running) {
  console.log(sbx.id, sbx.status, sbx.ip);
}

Sandbox.connect is the client-less analogue of getSandbox:

const sandbox = await Sandbox.connect("sb_01K...", { apiKey: process.env.FC_API_KEY });

The Sandbox handle

sandbox.id;       // "sb_01K..."
sandbox.status;   // "running" | "paused" | "creating" | ...
sandbox.ip;       // "10.0.0.2"
sandbox.name;     // "build-worker"
sandbox.data;     // the full SandboxView projection

await sandbox.refresh(); // re-fetch the projection in place

Running commands

// Buffered — resolves when the command exits.
const { result, exec_ms } = await sandbox.runCommand("bash", ["-lc", "ls -la /"]);
console.log(result.stdout, result.stderr, result.exit_code);

// A non-zero exit code is a normal result, not a thrown error.
const check = await sandbox.runCommand("test", ["-f", "/etc/hosts"]);
if (check.result.exit_code !== 0) console.log("missing");

// Environment variables are set per-sandbox at create time via `envs` and are
// injected into every command. The control plane does not support per-command
// stdin or env overrides — set env when creating or forking the sandbox.
const box = await fc.createSandbox({
  shape: "s-1vcpu-256mb",
  envs: { LOG_LEVEL: "info" },
});
const logged = await box.runCommand("printenv", ["LOG_LEVEL"]);

Streaming output yields a discriminated union — switch on event.type:

for await (const event of sandbox.streamCommand("bash", [
  "-lc",
  "for i in 1 2 3; do echo line $i; sleep 1; done",
])) {
  switch (event.type) {
    case "stdout":
      process.stdout.write(event.data);
      break;
    case "stderr":
      process.stderr.write(event.data);
      break;
    case "exit":
      console.log("exited", event.exitCode);
      break;
    case "error":
      console.error("agent error:", event.message);
      break;
    case "heartbeat":
      break;
  }
}

Files

// Upload — accepts any BodyInit (string, Uint8Array, Blob, stream).
await sandbox.files.upload("/tmp/note.txt", "hello");
await sandbox.files.upload("/tmp/data.bin", new Uint8Array([1, 2, 3]));

// Download — returns an ArrayBuffer.
const bytes = await sandbox.files.download("/tmp/note.txt");
console.log(new TextDecoder().decode(bytes));

Lifecycle

await sandbox.pause();
await sandbox.waitUntilPaused();

await sandbox.resume();
await sandbox.waitUntilRunning();

const clone = await sandbox.fork();                  // clone a paused sandbox
const clone2 = await sandbox.fork({ start_paused: true });

await sandbox.resize(20480);                         // grow the overlay disk
await sandbox.setIngress(true);                      // toggle HTTP ingress

const { destroyed } = await sandbox.destroy();       // destroyed sandbox id — async

pause, resume and fork are asynchronous on the server. The waitUntil* helpers poll with adaptive backoff and throw FcTimeoutError if the budget runs out:

await sandbox.waitUntilRunning({ timeoutMs: 90_000 });
await sandbox.waitUntilDestroyed();

A fork/snapshot workflow:

const base = await fc.createSandbox({ shape: "s-1vcpu-256mb" });
await base.runCommand("bash", ["-lc", "apt-get install -y ripgrep"]);
await base.pause();
await base.waitUntilPaused();

// Fan out independent copies of the prepared sandbox.
const workers = await Promise.all([base.fork(), base.fork(), base.fork()]);

Preview URLs

const sandbox = await fc.createSandbox({
  shape: "s-1vcpu-256mb",
  ingress_enabled: true,
});
// Redirect the background process's stdio so the buffered runCommand can
// return — otherwise it waits for the inherited stdout pipe to close.
await sandbox.runCommand("bash", ["-lc", "python3 -m http.server 8080 >/dev/null 2>&1 &"]);
await sandbox.waitForPortReady(8080); // block until something listens
console.log(sandbox.previewUrl(8080)); // https://<id>-8080.<domain>

previewUrl is only available on sandboxes created with ingress_enabled: true.

waitForPortReady(port, options?) opens a /dev/tcp probe inside the VM until the port accepts a connection. Defaults: 30 s budget, 200 ms poll interval, host 127.0.0.1. Throws FcTimeoutError if the port stays closed. Requires a rootfs with bash and GNU timeout (both present in the fc-spawn default rootfs).

Egress and bandwidth

await sandbox.setEgress(["github.com", "registry.npmjs.org"]);
await sandbox.setEgress(null); // null / [] = allow all
console.log(await sandbox.getEgress());

const bw = await sandbox.getBandwidth();
console.log(bw.used_bytes, bw.remaining_bytes, bw.capped);
await sandbox.rechargeBandwidth(10 * 1024 * 1024 * 1024); // +10 GiB

Networks

const network = await fc.networks.create({ name: "backend" });

await sandbox.attachNetwork(network.id);
await otherSandbox.attachNetwork(network.id);
// sandboxes now reach each other by name across the overlay

await fc.networks.get(network.id);   // includes members
await fc.networks.list();
await sandbox.detachNetwork(network.id);
await fc.networks.delete(network.id);

Templates

Build a custom rootfs from a Dockerfile:

const template = await fc.templates.create({
  name: "rg-base",
  dockerfile:
    "FROM bhautikchudasama/fc-base:debian-1\n" +
    "RUN apt-get update && apt-get install -y ripgrep",
});

// Follow the build log until it finishes. Pass a generous timeoutMs — a build
// can outlast the default 60s per-request deadline.
for await (const event of fc.templates.followLogs(template.id, { timeoutMs: 600_000 })) {
  if (event.line) console.log(event.line);
  if (event.final) console.log("build", event.status);
}

// Or fetch the log as plain text after the fact.
console.log(await fc.templates.logs(template.id));

const ready = await fc.templates.get(template.id);
if (ready.status === "ready") {
  await fc.createSandbox({ shape: "s-1vcpu-256mb", rootfs: "rg-base" });
}

await fc.templates.list();
await fc.templates.delete(template.id);

Catalog and identity

await fc.listShapes();   // Shape[] — { id, vcpu, mem_mib, default_disk_mib }
await fc.listRootfs();   // { rootfs, default, entries }
await fc.listHosts();    // HostPublic[]
await fc.whoami();       // { user_id, stats }
await fc.healthz();      // { up }
await fc.readyz();       // { ready, reason? } — does not throw on 503

Errors

Non-2xx responses throw a typed error. Every one extends FcError; HTTP errors also extend FcApiError and carry the request context needed to file a useful support ticket — statusCode, endpoint, method, requestId, resourceId, and the parsed JSend envelope.

import { FcNotFoundError, FcRateLimitError, FcValidationError } from "fc-sandbox-sdk";

try {
  await fc.createSandbox({ shape: "does-not-exist" });
} catch (err) {
  if (err instanceof FcValidationError) {
    console.error("bad request:", err.envelope?.data);
  } else if (err instanceof FcNotFoundError) {
    console.error(`not found at ${err.method} ${err.endpoint} (req ${err.requestId})`);
  } else if (err instanceof FcRateLimitError) {
    console.error("retry after", err.retryAfterSeconds, "s");
  } else {
    throw err;
  }
}

Every FcApiError exposes:

  • statusCode — HTTP status as a number.
  • endpoint — request pathname (no host, no query). Stable enough to bucket errors in dashboards.
  • method — HTTP verb.
  • requestId — server-issued id (X-Request-Id or X-Fc-Request-Id) for cross-referencing with the control plane's logs.
  • resourceId — sandbox / template / network / disk id parsed from the path, when present.
  • code — the stable machine-readable code from envelope.data.code, when present.

| Error | Cause | | --- | --- | | FcAuthError | 401 — missing / invalid API key | | FcPermissionError | 403 | | FcNotFoundError | 404 | | FcValidationError | 400 / 409 / 422 | | FcRateLimitError | 429 — exposes retryAfterSeconds | | FcServerError | 5xx | | FcConnectionError | network failure, no response | | FcTimeoutError | request or waitUntil* deadline exceeded |

Observability

The client takes optional lifecycle hooks. Wire them into OpenTelemetry, your structured logger, or a metrics sink — the SDK does not pull any runtime dependency for this.

const fc = new FcClient({
  apiKey: process.env.FC_API_KEY,
  hooks: {
    onRequest: (ctx) => log.debug("→", ctx.method, ctx.url, `try ${ctx.attempt}`),
    onResponse: (ctx) =>
      log.debug("←", ctx.status, `${ctx.durationMs.toFixed(0)}ms`, ctx.requestId),
    onRetry: (ctx) => log.warn("retry", ctx.reason, "in", ctx.delayMs, "ms"),
  },
});

Hook context is pre-redacted: Authorization, X-Api-Key, X-Auth-Token, Cookie, Proxy-Authorization, X-Csrf-Token, and common credential query params never reach a hook payload. A throw inside a hook is caught and warned — a flaky observer will not crash the request.

onRetry.reason is one of "network" (the fetch threw), "rate-limit" (the server set Retry-After), or "status" (a retryable 4xx/5xx without a Retry-After).

Streaming requests (Sandbox.streamCommand, TemplatesApi.followLogs) take a separate transport path and do not fire hooks — they aren't retried and live for the lifetime of their for await loop. Wrap that loop yourself if you need per-stream tracing.

Retries and timeouts

The transport retries transient failures with exponential backoff and jitter, and honors the Retry-After header. Idempotent methods retry on network errors and 408/500/502/503/504; non-idempotent methods retry only on 429/503, where the server demonstrably did not act.

const fc = new FcClient({ retry: { maxRetries: 4, baseDelayMs: 250 } });

await fc.whoami({ retry: false });               // disable for one call
await fc.createSandbox(req, { timeoutMs: 120_000 });

Cancellation

Every method accepts an AbortSignal:

const ac = new AbortController();
setTimeout(() => ac.abort(), 5_000);

const sandbox = await fc.createSandbox(
  { shape: "s-1vcpu-256mb" },
  { signal: ac.signal },
);

Escape hatch

fc.http exposes the low-level transport (request, requestRaw, stream) for endpoints the SDK does not model:

const data = await fc.http.request("GET", "/v1/some/new/endpoint");

Docs

Design

The handle model, typed-error hierarchy and retry policy were benchmarked against seven other sandbox / compute SDKs (E2B, Daytona, ComputeSDK, Modal, Cloudflare, CodeSandbox, Vercel). See docs/explanation/sdk-analysis.md for the full competitive analysis — what each does well and badly, which ideas this SDK borrowed, and where it leads.

Publishing

npm whoami
npm version patch
npm run publish:dry
npm run publish:npm
git push --follow-tags

prepublishOnly runs the test and typecheck gates before a real publish. If publish fails with E401, the local npm token is invalid — run npm login --registry=https://registry.npmjs.org/ and retry.