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

@boxd-sh/sdk

v0.2.1

Published

TypeScript SDK for the boxd cloud VM platform

Downloads

1,903

Readme

@boxd-sh/sdk

TypeScript SDK for the boxd cloud machine platform.

Promise-only, ESM-only. Runs on Node 20+, Bun, and Deno.

Install

npm install @boxd-sh/sdk
# or: bun add @boxd-sh/sdk

Quick start

import { Boxd } from "@boxd-sh/sdk";

const boxd = new Boxd({ apiKey: "bxd_..." });

const machine = await boxd.machines.create({ name: "my-machine" });
await boxd.machines.waitUntilReady(machine.id);

const result = await boxd.machines.exec(machine.id, { command: ["echo", "hello"] });
console.log(result.stdout);

await boxd.machines.delete(machine.id);
await boxd.close();

The client is the only stateful object. Everything else is a namespace of flat methods that return plain data — a Machine is a record of fields, not a handle, and every operation takes the machine id as its first argument.

Client construction

new Boxd();                                        // production
new Boxd({ apiKey: "bxd_..." });
new Boxd({ baseURL: "https://boxd.example.com" }); // any other cluster

| Option | Env var | Default | |---|---|---| | apiKey | BOXD_API_KEY | — | | token | BOXD_TOKEN | — | | baseURL | BOXD_BASE_URL (or the deprecated BOXD_API_URL) | http://boxd.sh:9443 | | timeout | — | 60000 ms | | maxRetries | — | 2 |

baseURL accepts an optional scheme that controls TLS:

| Value | Transport | |---|---| | http://host:port | plaintext | | https://host:port | TLS | | bare host:port | TLS, except localhost / 127.* |

Failed connections are retried up to maxRetries times with exponential backoff. Timeouts are never retried — the request may already have been applied — and authentication failures are never retried either.

Authentication

new Boxd({ apiKey: "bxd_..." });   // recommended
new Boxd({ token: "..." });        // a token you already hold
new Boxd();                        // BOXD_API_KEY, then BOXD_TOKEN

Credentials are resolved in this order, first match wins:

  1. an explicit token
  2. an explicit apiKey
  3. BOXD_TOKEN
  4. BOXD_API_KEY
  5. automatic, inside a boxd machine (below)

If none of those produce a credential, the first request throws AuthenticationError.

An API key is exchanged for a short-lived session token and kept fresh for you. Revoking a key takes effect immediately, so if a key is revoked mid-run the next call fails with AuthenticationError rather than retrying.

Inside a boxd machine

Running inside a boxd machine, new Boxd() authenticates automatically — no API key, no configuration:

const boxd = new Boxd();
const mine = await boxd.machines.list();

The client also points itself at the cluster the surrounding machine belongs to, so the same code runs unchanged wherever it is deployed. Passing an explicit baseURL or credential still wins.

Limits inside a shared machine. Automatic credentials act within the organization that owns the machine. From a shared machine you can manage the organization's shared machines, but you cannot read env vars or secrets, and you cannot reach machines that are private to another member. Use an API key for those.

Machines

const machine = await boxd.machines.create({ name: "my-machine" });
const one     = await boxd.machines.get(machine.id);   // id or name
const all     = await boxd.machines.list();            // Machine[]
await boxd.machines.list({ org: "acme" });             // one organization's machines
await boxd.machines.list({ allContexts: true });       // every org you belong to
await boxd.machines.delete(machine.id);

await boxd.machines.start(id);
await boxd.machines.stop(id);
await boxd.machines.reboot(id);
await boxd.machines.pause(id);        // suspend to RAM → { suspendUs: number }
await boxd.machines.resume(id);       // { resumeUs: number }
await boxd.machines.hibernate(id);    // suspend to disk
await boxd.machines.wake(id);

const fork = await boxd.machines.fork(id, { name: "fork-1" });
await boxd.machines.fork(id, { shared: true, config: { vcpu: 8 } });
await boxd.machines.share(id);        // visible to the whole org
await boxd.machines.unshare(id);

const renamed = await boxd.machines.rename(id, "new-name");   // the new name; reboots
await boxd.machines.setAutoSuspendTimeout(id, 300);    // seconds; 0 disables
await boxd.machines.setAutoHibernateTimeout(id, 3600);

await boxd.machines.waitUntilReady(id);       // running *and* exec works
await boxd.machines.waitUntilReady(id, { timeout: 180_000, pollInterval: 1_000 }); // ms
const suggested = await boxd.machines.suggestName();

list() returns a plain array. A fork inherits the source's sizing for anything config leaves unset, and is private to you unless you pass shared: true.

Creating

await boxd.machines.create({
  name: "builder",
  image: "ubuntu:24.04",
  env: { API_URL: "https://example.com" },
  cmd: ["/usr/local/bin/start"],
  restartPolicy: "always",           // "always" | "never"
  config: {
    vcpu: 2,
    memory: "8G",                    // "8G", "512M", or a byte count
    disk: "100G",
    autoSuspendTimeout: 300,         // seconds; 0 disables
    autoDestroyTimeout: 0,
    ssh: true,                       // give the machine an SSH port
    proxies: [{ name: "api", port: 3000 }],   // `port` optional — detected if omitted
    volumes: [{ diskId: "d_...", mountPath: "/data", readOnly: false }],
  },
});

// Every option is optional — this boots the cluster's default image.
await boxd.machines.create();

// In an organization. `shared` makes it visible to every member.
await boxd.machines.create({ org: "acme", shared: true });

// From a snapshot instead of an image.
await boxd.machines.create({ fromSnapshot: "golden", name: "from-golden" });

Restoring a snapshot replays the machine as it was captured, so fromSnapshot goes with name, org and config — the options that describe a fresh machine are a compile error beside it.

Renaming reboots the machine, so it is its own call. Every other setting is readable straight off Machine.

The Machine record

interface Machine {
  id: string;
  name: string;
  status: MachineStatus;   // "pending" | "starting" | "running" | "suspended" |
                           // "hibernated" | "stopped" | "failed" | "destroyed" | "migrating"
  imageRef: string;
  restartPolicy: string | null;
  createdAt: Date | null;          // null when no creation time is on record

  resources: {                     // what the machine actually got, not what
    vcpu: number;                  // you asked for — always concrete
    memoryBytes: number;
    diskBytes: number;
  };

  org: { id: string; name: string } | null;   // null = your personal quota
  shared: boolean;                 // shared with that org, or private to you

  access: {
    sshPort: number | null;        // null until allocated
    domain: string;
    url: string;                   // `https://<name>.<domain>`
  };

  idle: {                          // seconds; 0 = that timer is disabled
    suspendAfter: number;
    hibernateAfter: number;
    destroyAfter: number;
  };

  source: MachineSource | null;    // null = booted from an image
  hibernatedAt: Date | null;       // null = not hibernated
  lastConnectedAt: Date | null;    // null = never connected
  bootTimeMs: number | null;       // last boot duration; null = never booted
}

interface MachineSource {
  kind: "fork" | "snapshot";
  name: string;                    // source machine, or snapshot name
  version: number | null;          // snapshots only; a fork has no version
  id: string | null;               // provenance — may not resolve
}

null always means "not set": a port that was never allocated, a boot that never happened, an org you do not have. Where 0 is a real answer — a disabled idle timer — it stays 0.

org is the org the machine belongs to and is billed to; shared says whether your teammates can see it. A private machine can still be org-billed, so org set with shared: false is normal, not a contradiction.

source.id points at the machine or snapshot this one came from. It is a record of where the machine came from, not a live link — it may not resolve, and a lookup that finds nothing is normal.

MACHINE_STATUSES is exported as an array of every status this release knows, alongside isKnownMachineStatus(s) to check one:

import { MACHINE_STATUSES, isKnownMachineStatus } from "@boxd-sh/sdk";

Exec

One-shot exec collects the output:

const r = await boxd.machines.exec(id, { command: ["python", "script.py"] });
r.stdout;    // string
r.stderr;    // string — populated for non-PTY execs
r.exitCode;  // number
r.success;   // boolean

await boxd.machines.exec(id, {
  command: ["sh", "-c", "echo $FOO"],
  env: { FOO: "bar" },
  timeout: 30_000,        // milliseconds
});

// Under a PTY, stderr merges into stdout and `stderr` comes back empty.
await boxd.machines.exec(id, { command: "top -b -n1", tty: true, cols: 120, rows: 40 });

command takes argv — shell-quoted for you — or a ready-made command line as a string. timeout gives up on the call; whatever it started inside the machine may well still be running.

Interactive and PTY sessions use a stream handle, the one stateful object besides the client:

const stream = boxd.machines.streamExec(id, { command: "bash", tty: true });

stream.write("echo hello\n");
stream.end();          // half-close stdin — the process sees EOF
for await (const chunk of stream) process.stdout.write(chunk);
const code = await stream.wait();   // resolves with the exit code
stream.exitCode;       // number | undefined — set once the process is done
stream.close();        // cancel the session; safe to call twice

streamExec hands the stream back synchronously and buffers whatever you write until the session is live. Iterating the stream yields stdout as Uint8Array chunks.

Without tty, stderr arrives separately on stream.stderr — useful when a tool's progress goes to stderr and its answer to stdout. With tty, the terminal layer merges the two onto stdout, as terminals do.

For TUI apps, pass the initial geometry and forward resizes:

const stream = boxd.machines.streamExec(id, {
  command: "vim",
  tty: true,
  cols: process.stdout.columns,
  rows: process.stdout.rows,
});
process.stdout.on("resize", () =>
  stream.resize(process.stdout.columns, process.stdout.rows),
);

Headless one-shots that read stdin (jq, cat) hang waiting for input. Pass closeStdin: true to send EOF immediately. It is rejected together with tty, where stdin must stay open.

Logs

for await (const chunk of boxd.machines.logs(id)) process.stdout.write(chunk);
for await (const chunk of boxd.machines.logs(id, { follow: true })) { /* ... */ }

Files, ports, proxies, checkpoints

// Files — large uploads are chunked for you.
const written = await boxd.machines.files.upload(id, "/app/file.txt", "text content");
await boxd.machines.files.upload(id, "/app/file.bin", new Uint8Array([1, 2, 3]));
await boxd.machines.files.upload(id, "/app/app.py", { fromPath: "local.py" });
const bytes = await boxd.machines.files.download(id, "/app/output.json");  // Uint8Array

upload returns the number of bytes the machine confirmed it wrote.

// Raw TCP/UDP forwards (max 3 per machine).
const fwd = await boxd.machines.ports.expose(id, 5432);                  // tcp by default
await boxd.machines.ports.expose(id, 5353, { protocol: "udp" });         // "tcp" | "udp" | "both"
fwd.dns; fwd.publicPort; fwd.machinePort; fwd.protocol;
fwd.machineId; fwd.machineName;
await boxd.machines.ports.list(id);        // one machine's forwards
await boxd.machines.ports.list();          // every forward you own
await boxd.machines.ports.unexpose(id, 5432);   // echoes back the forward it removed

Connect on dns:publicPort. Re-exposing a machine port keeps its public port and just updates the protocol set; "both" shares one public port across TCP and UDP.

// HTTPS routes — an id or a name works, like everywhere else.
const route = await boxd.machines.proxies.create(machine.name, "api", 3000);
route.name; route.port;
const routes = await boxd.machines.proxies.list(machine.name);
routes[0].name;        // string | null — null on the machine's default route
routes[0].domain;      // the hostname this route answers on
routes[0].port;        // number — where traffic actually goes
routes[0].portMode;    // "locked" (you pinned it) | "auto" (detected for you)
routes[0].isDefault;
routes[0].machineId; routes[0].machineName;
await boxd.machines.proxies.setPort(machine.name, 3001, { name: "api" });
await boxd.machines.proxies.setPort(machine.name, "auto");   // default route, auto-detected
await boxd.machines.proxies.delete(machine.name, "api");

create answers as soon as the route is accepted, so it confirms the subdomain and the port it was pointed at; list() reports the full domain and the resolved port.

// Checkpoints — restore a machine in place.
const cp = await boxd.machines.checkpoints.create(id, "before-upgrade");
cp.id; cp.name; cp.status;
const saved = await boxd.machines.checkpoints.list(id);
saved[0].sizeBytes;
saved[0].createdAt;    // Date
saved[0].createdBy;    // string | null
saved[0].available;    // restorable right now
await boxd.machines.checkpoints.restore(id, "before-upgrade");
await boxd.machines.checkpoints.delete(id, "before-upgrade");

The machine must be running to take a checkpoint. status is "pending" until the artifact lands, then "ready" (or "failed"); restore wants one that is "ready" and available. Checkpoints belong to their machine and go away with it.

Env vars and secrets

Two namespaces with identical methods. The difference is what comes back: an env var has a readable value, a secret does not — secret values are write-only and never leave the server.

await boxd.env.set("API_URL", "https://example.com", { scope: "all" });
await boxd.env.list();       // [{ name, scope, value }]
await boxd.env.list({ org: "acme" });
await boxd.env.delete("API_URL", { scope: "all" });

await boxd.secrets.set("STRIPE_KEY", "sk_live_...", { scope: "private" });
await boxd.secrets.list();   // [{ name, scope }] — no values
await boxd.secrets.delete("STRIPE_KEY", { scope: "private" });

scope defaults to "shared" on set and delete; list takes org only and reports every scope.

set, delete and move each return the server's human-readable confirmation of what it did.

Scope decides which machines a value reaches:

| Scope | Applies to | |---|---| | private | only your own machines in that organization | | shared | the organization's shared machines | | all | every machine in the organization |

move changes the scope. It needs from as well as to, because the same name can exist in several scopes at once:

await boxd.secrets.move("STRIPE_KEY", { from: "private", to: "all" });

It is a move between two places, not a field update, so calling it twice fails the second time. Env vars and secrets share one name space per scope, so an existing env var can block a secret moving into that scope, and vice versa.

Pass { org: "acme" } to any of these to work in an organization instead of your personal scope.

Snapshots and disks

const snap = await boxd.snapshots.create(id, "golden");
snap.id; snap.name; snap.version; snap.status;
await boxd.snapshots.get("golden");                 // by name or id
await boxd.snapshots.list();
await boxd.snapshots.delete("golden");
await boxd.snapshots.list({ org: "acme" });         // `org` works on get/list/delete too

const disk = await boxd.disks.create("data", "10G");   // bytes or a human string
disk.id; disk.name; disk.sizeBytes; disk.status;
await boxd.disks.attach(disk.id, id, "/mnt/data");
await boxd.disks.attach(disk.id, id, "/mnt/data", { readOnly: true });
await boxd.disks.detach(disk.id, id);
await boxd.disks.list();
await boxd.disks.delete(disk.id);

The machine must be running to snapshot it, and create answers before the artifact lands — status is "pending" until it does. Snapshots stay inside one organization.

A disk is always created writable; read-only is chosen per attachment. A disk can be attached to only one machine at a time.

create confirms only what the server can answer immediately; the full records come back from get and list:

interface Snapshot {
  id: string;
  name: string;
  version: number | null;   // latest ready version; null = nothing captured yet
  status: ArtifactStatus;   // "pending" | "ready" | "failed"
  sizeBytes: number;
  createdAt: Date | null;   // the first capture — it stays put
  updatedAt: Date | null;   // the most recent capture
  vcpu: number;             // the sizing the machine was captured at
  memoryBytes: number;
  useCount: number;         // machines restored from it so far
}

interface Disk {
  id: string;
  name: string;
  sizeBytes: number;
  status: DiskStatus;       // "creating" | "ready" | "destroyed" — attach once "ready"
  createdAt: Date | null;
  attachments: {
    machineId: string;
    machineName: string;
    mountPath: string;
    mountMode: "ro" | "rw";
  }[];
}

Organizations

const orgs = await boxd.orgs.list();   // Org[]
orgs[0].id;
orgs[0].name;        // display label — it can repeat across organizations
orgs[0].slug;        // the organization's unique key
orgs[0].isAdmin;     // you administer it
orgs[0].isDefault;   // where your personal machines are billed

Anywhere a call takes org, it accepts an organization's name or id.

Credentials and account

const key = await boxd.apiKeys.create({
  name: "ci",
  org: "acme",                    // the organization the key is fenced to
  kind: "member",                 // "member" (default) acts as you within that org;
                                  // "org" is a userless service credential,
                                  // limited to the org's shared fleet, org admin only
  expiresIn: 60 * 60 * 24 * 30,   // seconds; omit or 0 for no expiry
});
key.id;
key.apiKey;      // shown once — store it now
key.expiresAt;   // Date | null

const keys = await boxd.apiKeys.list();
keys[0].name; keys[0].keyPrefix; keys[0].createdAt;
keys[0].lastUsedAt;   // Date | null — null = never used
keys[0].expiresAt;    // Date | null — null = no expiry
keys[0].org; keys[0].kind;   // "member" | "org"
await boxd.apiKeys.delete(key.id);

const me = await boxd.account.get();
me.userId;
me.displayName;          // string | null — falls back to `userId`
me.sshKeyFingerprints;   // string[]
me.billing.subscriptionStatus;   // "active", "trialing", … | null
me.billing.pastDueSince;         // Date | null
me.billing.maxVms;               // effective quota
me.billing.vcpu; me.billing.memoryBytes;

await boxd.account.linkSshKey({ pubkey: "ssh-ed25519 AAAA..." });
await boxd.account.linkSshKey({
  pubkey: "ssh-ed25519 AAAA...",
  deviceId: "laptop",     // one key kept per device — re-linking replaces it
  label: "MacBook Pro",   // shown wherever the device is listed
});

const cfg = await boxd.account.config();
cfg.defaultImage; cfg.zone;

Every key is fenced to exactly one organization. Deleting one takes effect immediately.

Errors

import { NotFoundError } from "@boxd-sh/sdk";

try {
  await boxd.machines.get("nope");
} catch (e) {
  if (e instanceof NotFoundError) { /* ... */ }
}

Everything thrown extends BoxdError:

| Class | Meaning | |---|---| | AuthenticationError | the credential was rejected, or none was found | | PermissionDeniedError | authenticated, but not allowed | | NotFoundError | no such resource | | ConflictError | already exists, or the resource is in the wrong state | | RateLimitError | quota or rate limit reached | | APIStatusError | any other error returned by the server | | APIConnectionError | the request never reached the server |

Every error carries grpcCode, the numeric status code, for finer-grained handling.

Async disposal

Node 20+, Bun, and Deno support TC39 explicit resource management:

{
  await using boxd = new Boxd({ apiKey: "bxd_..." });
  await boxd.machines.list();
  // boxd.close() runs at scope exit
}

Version notifications

The SDK prints a one-time console.warn on stderr when a newer release is available:

A new version of @boxd-sh/sdk is available (v0.2.0, you have v0.1.9). Update with:
  npm install @boxd-sh/sdk@latest

It fires at most once per process and never causes a request to fail. The installed version is also exported:

import { VERSION } from "@boxd-sh/sdk";