@namespacelabs/sdk
v1.5.2
Published
TypeScript SDK for Namespace Cloud APIs
Readme
Namespace TypeScript SDK
TypeScript/JavaScript SDK for Namespace Cloud APIs, providing authentication, client management, and type-safe API access.
Features
- Modular Authentication: Multiple token loading strategies (user tokens, workload tokens, environment-based)
- Token Management: Automatic token caching and refresh
- Type Safety: Full TypeScript support with generated types
- Dual Module Support: Works with both ESM and CommonJS
- Flexible Client Creation: Support for both bearer tokens and TokenSource instances
Installation
npm install @namespacelabs/sdk @connectrpc/connect @connectrpc/connect-node @bufbuild/protobufQuick Start
Basic Authentication
import { loadUserToken } from "@namespacelabs/sdk/auth";
import { createRegionTransport, createClient } from "@namespacelabs/sdk/api";
// Load token from user configuration
const tokenSource = await loadUserToken();
// Create transport with token source
const transport = createRegionTransport("us", { tokenSource });
// Use with your service clients...Using Bearer Tokens
import { bearerAuthInterceptor } from "@namespacelabs/sdk/api";
const transport = createRegionTransport("us", {
token: "your-bearer-token",
});Authentication
The SDK provides multiple ways to load authentication tokens, following patterns from the Go SDK:
Token Loading Functions
Each function returns a TokenSource (or a promise of one). Call and await
them to load eagerly, or pass them uncalled wherever a token source is
accepted (e.g. tokenSource: loadUserToken) — the SDK invokes and awaits them
internally on first use, in the same style as AWS SDK credential providers.
loadDefaults()
Automatically detects and loads the appropriate token based on context:
- Checks
NSC_TOKEN_FILEenvironment variable - Tries
/var/run/nsc/token.json(workload token) - Falls back to user token from config directory
import { loadDefaults } from "@namespacelabs/sdk/auth";
const tokenSource = await loadDefaults();
const token = await tokenSource.issueToken(5 * 60 * 1000); // 5 minutesloadUserToken()
Loads token from user's local configuration:
- macOS:
~/Library/Application Support/ns/token.json - Linux:
~/.config/ns/token.json - Windows:
%APPDATA%/ns/token.json
import { loadUserToken } from "@namespacelabs/sdk/auth";
const tokenSource = await loadUserToken();loadWorkloadToken()
Loads token from workload environment:
- Checks
NSC_TOKEN_FILEenvironment variable - Falls back to
/var/run/nsc/token.json
import { loadWorkloadToken } from "@namespacelabs/sdk/auth";
const tokenSource = await loadWorkloadToken();fromBearerToken(token)
Creates a TokenSource from a bearer token string:
import { fromBearerToken } from "@namespacelabs/sdk/auth";
const tokenSource = fromBearerToken("nsct_...");Token Source Interface
All token loading functions return a TokenSource that implements:
interface TokenSource {
issueToken(minDuration: number, force?: boolean): Promise<string>;
}minDuration: Minimum duration (in milliseconds) the token should be validforce: Force token refresh even if cached token is valid
Token Caching
The SDK automatically caches tokens to minimize token refresh requests:
- Cached tokens are stored in
token.cachenext to the source token file - Cache is validated against tenant ID and expiration time
- Cache files are created with secure permissions (0600)
Token Claims
Extract and validate JWT token claims:
import { extractClaims, isTokenExpired, getTenantId } from "@namespacelabs/sdk/auth";
const claims = extractClaims(token);
console.log(claims.tenant_id);
const expired = isTokenExpired(claims);
const tenantId = getTenantId(token);Devboxes
The Devbox client exposes product-level resources for devboxes, blueprints, and images. A created or fetched devbox is an operational handle: starting it, establishing authenticated connections, and reusing those connections happen automatically.
import { createDevboxClient } from "@namespacelabs/sdk";
// Also available from the subpath: "@namespacelabs/sdk/devbox".
// With no options, authentication defaults to the workload token when
// running in a Namespace workload, falling back to the local user token.
const client = createDevboxClient();
// Or pass an explicit token source (invoked and awaited internally
// on first use):
// createDevboxClient({ tokenSource: loadUserToken })
const blueprint = await client.blueprints.create("typescript", {
image: "builtin:default",
size: "m",
environment: { NODE_ENV: "development" },
});
const devbox = await client.devboxes.create({
name: "my-devbox",
blueprint: blueprint.name,
});
// Structured argv: arguments are passed literally and do not expand in a shell.
const result = await devbox.exec(["node", "--version"]);
// Shell syntax, without allocating a TTY.
await devbox.shell("npm install && npm test", {
cwd: "/workspace",
});
// Services start immediately by default and continue running independently.
await devbox.services.create({
name: "web",
command: "node",
args: [
"-e",
"require('node:http').createServer((_request, response) => response.end('Hello!')).listen(3000)",
],
});
// Commands run through the devbox agent, which retains each command and its
// output for later inspection (`devbox logs`). Relative `cwd` paths resolve
// against the devbox workspace directory. When `cwd` is omitted, commands run
// in the devbox default directory; if the devbox checks out a repository, that
// directory only exists once the checkout completes, so pass an explicit `cwd`
// when running commands immediately after creation.
await devbox.fs.upload("./package.json", "/workspace/package.json");
await devbox.fs.download("/workspace/results.json", "./results.json");
await devbox.fs.copy("/workspace/results.json", "/workspace/results-copy.json");
const text = await devbox.fs.read("/var/log/app.log");
const bytes = await devbox.fs.read("/var/log/app.log", {
format: "bytes",
offset: 1024,
length: 64 * 1024,
});
const stream = await devbox.fs.read("/data/output.bin", { format: "stream" });
for await (const chunk of stream) process.stdout.write(chunk);
// PTY sessions are explicit and separate from shell execution.
const terminal = await devbox.terminal.open({ columns: 120, rows: 40 });
terminal.onData((data) => process.stdout.write(data));
terminal.write("pwd\n");
terminal.close();
client.close();Asynchronous execution
devbox.executions.start() and devbox.executions.startShell() return an execution handle as soon as the
agent accepts the command, without waiting for it to exit. Unlike foreground
exec()/shell(), these commands continue when the client disconnects.
const client = createDevboxClient({ connectionTimeoutMs: 90_000 });
const devbox = await client.devboxes.get("my-devbox");
const execution = await devbox.executions.start(["bash", "-lc", "sleep 3; echo done"], {
cwd: "/tmp",
env: { CI: "true" },
timeoutMs: 10_000,
});
const id = execution.id; // Save this together with the devbox ID.
client.close();
const otherClient = createDevboxClient();
const sameDevbox = await otherClient.devboxes.get(devbox.id);
const reattached = await sameDevbox.executions.get(id);
const status = await reattached.status(); // running | completed | missing
const result = await reattached.wait({ timeoutMs: 30_000, maxOutputBytes: 1024 * 1024 });
console.log(result.exitCode, result.stdout, result.stderr, result.error);
// Discover retained commands, including commands started by other clients:
const executions = await sameDevbox.executions.list();
for (const entry of executions) console.log(entry.id, await entry.status());
// Alternatively, consume retained and live bytes without collecting output:
for await (const chunk of reattached.logs({ timeoutMs: 30_000 })) {
process.stdout.write(chunk.stdout);
process.stderr.write(chunk.stderr);
if (chunk.result) console.log("exit", chunk.result.exitCode, chunk.result.error);
}
otherClient.close();executions.startShell(script, options)uses the devbox's configured shell, with an optionalshelloverride. Start options supportcwd,env, and initialstdin(string or bytes), just like foreground execution. Output callbacksonStdoutandonStderrbelong onwait(), not on the start call.- Only
completedstatus has an exit code and completion time. An exit code of zero in agent metadata does not imply completion. Nonzero exits and command-start failures resolve normally withexitCodeand optionalerror. executions.get()rejects withExecutionNotFoundErrorfor an absent ID; an existing handle'sstatus()returnsmissingif it is no longer retained.stop(),logs(), andwait()reject withExecutionNotFoundErrorfor missing executions. Transport/authentication failures reject, never masquerade asmissing.executions.list()returns handles for all command executions retained by the current agent, running and completed, sorted by start time. Boot operations are excluded. There is no pagination or durable history. A listed execution may be evicted before its status or logs are read. Connection failures reject rather than returning an empty list.- Each
wait()/logs()call has its own reader, including concurrent calls. Every reader replays from the beginning of retained output, then follows live output until a final result. Repeated waits invoke callbacks again; nothing is cached in the handle. Streams do not automatically reconnect. There is no cursor, so reopening after a failure may duplicate output: exactly-once delivery across reconnects is not guaranteed. Decode UTF-8 incrementally when consuming byte chunks, since a character can span chunks. wait()captures at most 10 MiB of combined stdout/stderr by default. Set a finite, non-negative integermaxOutputBytesto change the limit. Exceeding it rejects withExecutionOutputLimitError; uselogs()for large output. Streaming does not collect output and applies transport backpressure.- Client
connectionTimeoutMsbounds connection establishment. Start-calltimeoutMscovers connection acquisition plus the start RPC, not command runtime.wait()/logs()timeoutMsstarts after connection acquisition; their signal also cancels acquisition.status(),executions.get(), andexecutions.list(), as well asstop(), use one operation timeout including acquisition. Start options never carry over to later reads. Read timeouts reject withDevboxTimeoutError. - Aborting, timing out, breaking iteration, exceeding the output limit, or closing a client only stops reading an asynchronous execution. None of these operations kills the remote command. If StartExec fails after being sent, the command may already exist. The SDK never retries it automatically; blindly retrying may launch a duplicate.
- Execution IDs are agent-local, and log retention is not indefinite. Do not rely on IDs or output surviving VM replacement. Looking up an execution is connection-backed and may activate a stopped devbox, but cannot restore an execution lost with its previous agent.
To terminate an execution, stop() defaults to graceful termination:
await execution.stop(); // Send SIGTERM; equivalent to { mode: "graceful" }.
// If needed, a separate request escalates to SIGKILL:
await execution.stop({ mode: "force", timeoutMs: 5_000 });
const result = await execution.wait({ timeoutMs: 10_000 });stop() acknowledges the request, not completion; status(), logs(), or wait()
provide the final result. Graceful stop allows cleanup and never escalates
automatically. Forced stop sends SIGKILL without cleanup. Repeating the same or
weaker mode, or stopping an already completed execution, is a no-op. Cleanup can
exit successfully or with its own nonzero code. Signal termination reports exit
code -1 and agent error detail; ExecResult.signal remains null because the
protocol does not provide a structured signal field.
Stop targets the original process group only until its leader exits; detached or
surviving descendants are not managed. Canceling or timing out the stop RPC does
not undo a request already accepted by the agent. The SDK does not retry it
automatically. Agents without StopExec reject with ConnectError code
Unimplemented; there is no shell-based fallback. Arbitrary signals, incremental
stdin, and stdin-close still require backend changes.
Run the opt-in integration tests against an authenticated Linux devbox with
bash, cat, head, ps, and sleep installed (no devbox is created or deleted):
SDK_TEST_DEVBOX=my-devbox npm run test:devboxCheckout configuration
For direct creation (without a blueprint), omit versionControl and repository
to inherit the tenant's default repository configuration. Pass versionControl
to explicitly configure checkout; an empty object or empty repositories list
disables checkout entirely.
To create a scratch devbox without checking out any repository:
const scratch = await client.devboxes.create({
name: "scratch",
imageName: "builtin:base",
versionControl: {},
});
const checkout = await client.devboxes.create({
name: "checkout",
versionControl: {
repositories: [
{
repository: "https://github.com/namespacelabs/typescript-sdk",
ref: "main", // Optional branch, tag, or commit SHA; defaults to the default branch.
},
{ repository: "https://github.com/namespacelabs/docs" },
],
},
});Each of the up to 20 repository entries can select its own branch, tag, or
commit with ref, and its own "git-clone" or "git-snapshot-unpack"
checkout method. Blueprint definitions accept the same versionControl field.
Returned Devbox information and blueprints expose checkout configuration through
versionControl.repositories.
The existing top-level repository option remains supported. Setting it to a
repository URL checks out that repository; an empty string inherits defaults
and does not disable checkout. repository and versionControl cannot be
combined, even when either is empty. Neither can be used with a blueprint.
Invalid combinations are rejected by TypeScript and throw TypeError before
any RPC.
upload() and download() transfer one file. copy() operates inside the
devbox and accepts { recursive: true } for directories. read() returns
UTF-8 text by default; pass format: "bytes" for a Uint8Array or
format: "stream" for a ReadableStream. Its offset and length options
are byte-based. The existing readFile() returns the complete file as a
Uint8Array. All operations accept AbortSignal and timeout options.
Devboxes with a graphical display — macOS devboxes — expose screen access through devbox.display, backed by VNC. Methods reject with DevboxDisplayUnavailableError when the devbox has no display (for example, Linux devboxes):
import { writeFile } from "node:fs/promises";
import { DevboxDisplayUnavailableError } from "@namespacelabs/sdk";
const macos = await client.devboxes.create({
name: "my-mac",
os: "macos",
size: "m",
});
try {
const screenshot = await macos.display.screenshot();
await writeFile("screen.png", screenshot.png);
// Click at framebuffer coordinates (origin top-left).
await macos.display.click(100, 200);
await macos.display.click(100, 200, { button: "right" });
} catch (error) {
if (error instanceof DevboxDisplayUnavailableError) {
// This devbox has no display.
}
throw error;
}Like other connection-backed operations, using devbox.display on a stopped devbox activates it first, and the underlying VNC session is cached and reused across calls.
The VNC client behind devbox.display is also available standalone as @namespacelabs/sdk/vnc (openVnc): a minimal RFB 3.8 client over websockets with Apple Remote Desktop authentication, raw encoding, and PNG screenshots — no native dependencies and no Namespace-specific behavior.
Images can be registered from an existing image reference, listed, inspected, optimized for a site, and deleted:
const image = await client.images.register({
name: "node-22",
ref: "node:22",
});
// Image optimization defaults to iad when no site is specified.
await client.images.optimize(image.name);API Clients
The SDK provides high-level client factories for each Namespace Cloud API:
Available APIs
- Compute (
@namespacelabs/sdk/api/compute) - Instance management, regional - IAM (
@namespacelabs/sdk/api/iam) - Tenant and token management, global - Builds (
@namespacelabs/sdk/api/builds) - Container image builds, regional - Storage (
@namespacelabs/sdk/api/storage) - Artifact storage, regional - Registry (
@namespacelabs/sdk/api/registry) - Container registry, global - Vault (
@namespacelabs/sdk/api/vault) - Secrets management, regional - Devboxes (
@namespacelabs/sdk/devbox) - Devboxes, blueprints, and images
Using API Clients
Each API provides a client factory function:
import { loadUserToken } from "@namespacelabs/sdk/auth";
import { createComputeClient } from "@namespacelabs/sdk/api/compute";
import { createIAMClient } from "@namespacelabs/sdk/api/iam";
const tokenSource = await loadUserToken();
// Create a compute client for US region
const computeClient = createComputeClient({
region: "us",
tokenSource,
});
// Use the client
const instances = await computeClient.compute.listInstances({
tenantId: "your-tenant-id",
});
// Create an IAM client (global)
const iamClient = createIAMClient({ tokenSource });
// Use the IAM client
const tenants = await iamClient.tenants.listTenants({});Creating a macOS Instance
The macOS instance example uses the Compute API to create a macOS Tahoe instance on Apple Silicon with 6 vCPUs and 14 GiB RAM, waits for it to be ready, and runs uname -a in the guest. It prints stdout and stderr and exits with the command's exit code.
Run it from a checkout of this repository with Node.js 22:
npm ci
npm run build
nsc login
npx tsx examples/compute/create-macos-instance.tscreateComputeClient() defaults to the US region and loads authentication lazily using loadDefaults(): a workload token inside Namespace, or your local user token from nsc login. You can also set NSC_TOKEN_FILE to a token file. Your workspace must have macOS capacity available.
The example creates a real, billable instance in the US region and leaves it running for inspection. It prints the instance URL and an nsc destroy <instance-id> command; a 30-minute deadline limits its lifetime even if the script fails while waiting. To select another macOS version, change the macos.version shape selector using the available selectors.
Creating Transports
The SDK provides transport creation utilities for different API endpoints:
Regional Transport
For regional APIs (Compute, Builds, Storage, Vault):
import { createRegionTransport } from "@namespacelabs/sdk/api";
// With token source
const transport = createRegionTransport("us", {
tokenSource: await loadUserToken(),
});
// With custom base URL
const transport = createRegionTransport("us", {
tokenSource,
baseUrl: "https://custom.api.endpoint.com",
});Global Transport
For global APIs (IAM, Registry):
import { createGlobalTransport } from "@namespacelabs/sdk/api";
const transport = createGlobalTransport({
tokenSource: await loadUserToken(),
});Interceptors
The SDK provides an interceptor for adding authentication to requests:
bearerAuthInterceptor(tokenSource, minDuration?)
Dynamically fetches tokens for each request from a TokenSource:
import { bearerAuthInterceptor } from "@namespacelabs/sdk/api";
import { loadUserToken } from "@namespacelabs/sdk/auth";
import { createConnectTransport } from "@connectrpc/connect-node";
const tokenSource = await loadUserToken();
const transport = createConnectTransport({
baseUrl: "https://api.example.com",
interceptors: [
bearerAuthInterceptor(tokenSource, 5 * 60 * 1000), // 5 minutes
],
});The interceptor automatically refreshes tokens as needed based on the minDuration parameter (default: 5 minutes).
Project Structure
typescript-sdk/
├── src/
│ ├── auth/ # Authentication module
│ │ ├── types.ts # Core types and interfaces
│ │ ├── claims.ts # JWT claims extraction
│ │ ├── token.ts # Token loading and management
│ │ └── index.ts # Module exports
│ ├── api/ # API client module
│ │ ├── clients.ts # Transport creation utilities
│ │ ├── interceptors.ts # Auth interceptors
│ │ └── index.ts # Module exports
│ └── index.ts # Main SDK export
├── dist/ # Compiled output (CJS, ESM, types)
├── examples/ # Example usage
├── package.json
├── tsconfig.json
└── README.mdModule Exports
The SDK provides multiple export paths:
@namespacelabs/sdk- Main entry point (re-exports auth, API clients, and Devboxes)@namespacelabs/sdk/auth- Authentication module only@namespacelabs/sdk/api- API client utilities only@namespacelabs/sdk/devbox- Devbox product API
Each export path supports both ESM and CommonJS:
// ESM
import { loadUserToken } from "@namespacelabs/sdk/auth";
// CommonJS
const { loadUserToken } = require("@namespacelabs/sdk/auth");Environment Variables
NSC_TOKEN_FILE- Override default token file locationXDG_CONFIG_HOME- Linux config directory (defaults to~/.config)APPDATA- Windows config directory
Error Handling
The SDK provides specific error types:
import { NotLoggedInError } from "@namespacelabs/sdk/auth";
try {
const tokenSource = await loadUserToken();
} catch (error) {
if (error instanceof NotLoggedInError) {
console.error("Please run `nsc login` first");
}
}TypeScript Support
The SDK is written in TypeScript and provides full type definitions:
import type { TokenSource, TokenClaims, CachedToken } from "@namespacelabs/sdk/auth";
import type { CreateRegionTransportOpts, CreateClientOpts } from "@namespacelabs/sdk/api";Development
Building
npm run buildThis generates:
- CommonJS output in
dist/cjs/ - ES Module output in
dist/esm/ - Type declarations in
dist/types/
Clean
npm run cleanComparison with Go SDK
This TypeScript SDK follows similar patterns to the Go integrations/auth package:
| Go SDK | TypeScript SDK |
|--------|----------------|
| auth.LoadDefaults() | loadDefaults() |
| auth.LoadUserToken() | loadUserToken() |
| auth.LoadWorkloadToken() | loadWorkloadToken() |
| api.TokenSource | TokenSource interface |
| auth.ExtractClaims() | extractClaims() |
| Bearer token via gRPC metadata | Bearer token via HTTP Authorization header |
License
Apache-2.0
Contributing
Contributions are welcome! Please open issues or pull requests on GitHub.
