Cubicle

TypeScript SDK

Install @cubicle/sdk (ESM only, Node.js 22 or later), configure it with CB_SDK_API_URL and CB_SDK_API_KEY or client options, address Boxes by ID, ORG/NAME or name, read BoxInfo records with boxes.get and boxes.list, act through Box and BoxRef handles from create and ref, and create, run commands in and delete Boxes with a service account API key.

@cubicle/sdk is the TypeScript client for the Cubicle HTTP API. It creates, starts, stops, hibernates and deletes Boxes, runs commands in them, takes and restores snapshots, and follows operations, all with a service account API key. Calls that change a Box wait until the change has taken effect, so when await returns, the Box is ready, stopped, hibernated or gone.

Install

npm install @cubicle/sdk

Requirements

  • Node.js 22 or later. Other runtimes are untested.
  • ES modules. The package is ESM only, so import it from an ES module, such as a .mts file or a project with "type": "module".
  • No runtime dependencies. The SDK uses fetch, AbortSignal, crypto.randomUUID() and other web-standard APIs, and no Node built-ins.
  • No local state. Every request carries the API key. Nothing is written to disk, and there is no session to renew.
  • A service account API key (cbk_…), with write scope for any call that changes a Box or runs a command. See Sign in and API keys and Run CI jobs in Boxes.
  • A grant on each Box you run commands in, and a grant or the owner or admin role on each Box you start, stop, hibernate, delete, snapshot or restore. A service account gets a grant on every Box it creates; see Box grants.

Keep API keys out of browsers

An API key acts with all of its service account's access. Use the SDK in server-side code, CI jobs and agents. Never ship a key in code that runs in a browser.

Configure the client

new Cubicle() takes its API URL and API key from the first place that sets each one:

  1. The apiUrl and apiKey options passed to new Cubicle({ … }).
  2. The CB_SDK_API_URL and CB_SDK_API_KEY environment variables, where the runtime has process.env.

If either is still missing, the constructor throws a CubicleError with code missing_api_url or missing_api_key. The constructor sends no request, so a wrong key shows up on the first call, as a CubicleApiError with code unauthenticated.

export CB_SDK_API_URL=https://api.cubicle.sh
export CB_SDK_API_KEY=cbk_…
import { Cubicle } from "@cubicle/sdk"

const cubicle = new Cubicle() // reads CB_SDK_API_URL and CB_SDK_API_KEY

Pass apiKey when the key comes from somewhere else, such as a secret manager. Options win over the environment.

Client options

OptionEnv varDefaultMeaning
apiUrlCB_SDK_API_URLNone: requiredThe API URL, https://api.cubicle.sh. It must use http or https.
apiKeyCB_SDK_API_KEYNone: requiredA service account API key, cbk_….
org—The API key's organizationOrganization name or ID for boxes.create, boxes.list, listBases and listSnapshots. It also narrows bare Box names.
fetch—The global fetchA replacement fetch, for proxies or tests.
requestTimeoutMs—30000 (30 s)Timeout for each attempt of a request, from 1 to 2^31 − 1 ms.
maxRetries—3Retries for each request after a rate limit, a 502, 503 or 504, a connection failure or a request timeout, from 0 to 100. Waits do not use it.
pollIntervalMs—1000 (1 s)Delay between one wait's polls, from 1 to 2^31 − 1 ms.
maxPollsPerSecond—5The most polls a second across all of the client's waits, from 0.001 to 1000.

The constructor throws a CubicleError with code invalid_option when a value is out of range. A wait's timeoutMs that is negative or not a number rejects with invalid_option too, when the wait begins, after the request was sent; see Change the timeout. Values the API checks, such as a Box name, a size or an exec timeoutMs under 100 ms, reject with a CubicleApiError with code invalid_request.

Retries are described in Errors and retries and polling in Waiting. Every option is listed in CubicleOptions.

Use one client per process and share it, because its waits share one poll budget. Separate clients share nothing in the SDK, but every API key of one service account still spends the same rate limit.

The default organization

An API key belongs to one organization, so you rarely need to name one. Calls that need an organization (boxes.create, listBases and listSnapshots) take it from the first of:

  1. The call's own org: params.org for boxes.create, or the org option of listBases and listSnapshots.
  2. The client's org option.
  3. The API key's organization. The client reads it from GET /v1/me on first use and remembers it. A failed lookup is tried again on the next call.

If none applies, which happens only with a credential that is not an API key and does not belong to exactly one organization, the call throws a CubicleError with code org_required.

boxes.list() with no org, on a client with no org option, lists every organization the caller can see; for an API key, that is its own organization. Bare Box names are narrowed to the client's org, or else to the API key's organization.

What the client offers

A Cubicle client has these members:

MemberWhat it doesReference
cubicle.boxes.*Create, read, list, start, stop, hibernate and delete Boxes; run commands; snapshot and restore; wait for a state; list operations.Boxes
cubicle.operations.*get(id) and wait(id) for exec, snapshot and restore operations.Operations
cubicle.me()The caller's identity, organizations and API key.Me
cubicle.listBases()Base images, and whether a healthy Node has each one. Free capacity is not considered.CreationBase
cubicle.listSnapshots()Completed snapshots you may create Boxes from.CreationSnapshot

Boxes and handles

Each cubicle.boxes method that acts on one Box takes a Box reference as its first argument: the Box's ID, ORG/NAME, or a bare NAME, narrowed as in The default organization. The SDK URL-encodes the reference, so acme/job-1 is sent as acme%2Fjob-1. How each form resolves is in Box references. A deleted Box's name can be reused at once, so store box.id when you need to reach the same Box later.

CallReturnsNetwork requestUse it to
cubicle.boxes.get(ref)BoxInfo recordGET /v1/boxes/{box}Read the Box's current state. It never changes the Box.
cubicle.boxes.list()BoxInfo recordsGET /v1/orgs/{org}/boxes, or GET /v1/boxes without an organizationList Boxes. Boxes being deleted stay listed, with deletion: "pending", until they are gone.
cubicle.boxes.ref("acme/job-1")BoxRef handleNoneAct on a Box by reference. A missing Box is reported by the handle's first call.
cubicle.boxes.ref(info)Box handleNoneTurn a record from get or list into a handle with id, name and org.
cubicle.boxes.create(…)Box handlePOST /v1/orgs/{org}/boxes, then pollsCreate a Box and get a handle for it.
  • A record, BoxInfo, is the Box as the API reported it at that moment. It does not update itself. Its fields are explained in The Box object.
  • A handle, BoxRef or Box, holds a reference, not a record. Its methods are info(), start(), stop(), hibernate(), delete(), exec(), waitUntil(), waitUntilReady(), snapshot(), listSnapshots(), restore() and listOperations(): the cubicle.boxes methods bound to one Box, so box.stop() and cubicle.boxes.stop(box.id) do the same thing. info() calls get() for its Box.
  • A Box also has id, name and org, and addresses its Box by ID. A BoxRef has only ref, the reference it sends.
const info = await cubicle.boxes.get("acme/job-1")
console.log(info.state, info.ssh.ready) // => "running" true

const job = cubicle.boxes.ref("acme/job-1") // no request yet
await job.stop()

const handle = cubicle.boxes.ref(info) // a Box, addressed by ID
console.log(handle.id, handle.name) // => "8d0c6a8e-…" "job-1"

What each method does, waits for and rejects with is in the Boxes reference. The tasks, with examples, are in Run commands, Snapshot and restore a Box and Stop, hibernate and wake a Box.

Example: create, run a command, delete

This program creates a Box, runs a command in it, and deletes the Box even when the command fails.

import { Cubicle } from "@cubicle/sdk"

const cubicle = new Cubicle()

// Waits until the Box is ready for commands, up to 5 minutes.
const box = await cubicle.boxes.create({ name: "job-1" })

try {
  const result = await box.exec(["uname", "-a"])
  console.log(result.exitCode, result.stdout)
  // => 0 "Linux box-job-1 …"
} finally {
  // Waits until the Box's Node has removed it.
  await box.delete()
}

It uses top-level await, so run it as an ES module: for example, save it as job.mts and run npx tsx job.mts.

Box names are unique in an organization, so running it again while job-1 still exists rejects with conflict. Omit name to get a generated one.

Not in the SDK yet

  • Streaming exec output, stdin and terminals. exec returns the output when the command ends.
  • File upload and download. See Move files into and out of a Box.
  • Network connections to a Box. The SDK cannot open one, and there are no port forwards, proxies or public URLs. To reach a service in a Box from code, run the client inside the Box with exec, for example against localhost.
  • SSH. Service accounts cannot register SSH keys; use cubicle ssh with your own sign-in.
  • Fork, resize, rename and reboot.

Everything Cubicle does not offer yet is in Limits and defaults.

On this page