Skip to main content

TypeScript SDK

The Orunbase TypeScript SDK (@saas/sdk) is a typed, contract-driven client for the control-plane API. It has zero runtime dependencies, uses only Web Platform primitives (fetch, Headers, URL, Web Crypto), and every request and response type is generated from the same contracts the API workers validate against — the console and the orun-cloud CLI are both built on it.

note

The SDK ships inside the sourceplane/orun-cloud repository as the private workspace package @saas/sdk; it is not yet published to the public npm registry. Inside the monorepo (or a fork), add it as a workspace dependency.

Install and instantiate

// package.json of a consumer
{
"dependencies": {
"@saas/sdk": "workspace:*"
}
}
import { OrunCloud } from "@saas/sdk";

const client = new OrunCloud({
baseUrl: "https://api.orunbase.com",
auth: { kind: "bearer", token: process.env["ORUN_CLOUD_TOKEN"]! },
});

baseUrl points at the edge API (trailing slashes are stripped). The bearer token is any credential the API accepts — a workspace API key or a user session token; see Authentication. Client options:

OptionTypeNotes
baseUrlstringRequired. Base URL of the edge API, e.g. https://api.orunbase.com.
auth{ kind: "bearer", token } | { kind: "session", cookie }Optional; sent on every request when present.
defaultHeadersRecord<string, string>Merged into every request; per-request headers win on conflict.
fetchtypeof fetchCustom fetch implementation. Defaults to the platform global.

Runtime support

The same source runs unmodified on every runtime with a WHATWG fetch and Web Crypto on the global — there are no node:* imports anywhere in the package.

RuntimeStatus
Node ≥ 20Tier 1 — native fetch / Web Crypto
Browsers (modern)Tier 1
Cloudflare WorkersTier 1 — pure Web Platform
BunTier 1

On platforms without a global fetch, inject one via new OrunCloud({ fetch }) — also the seam for tests and for wrapping requests with retries (see No built-in retries).

Resource namespaces

Every API surface is reachable as client.<resource>:

NamespaceCovers
workspacesWorkspaces — the public vocabulary. Same ids and surface as organizations, served via the /v1/workspaces alias.
organizationsThe canonical /v1/organizations spelling. Fully supported; new code should prefer workspaces.
reposProjects, under the canonical name (a project is a git repo). Same surface as projects.
projectsDeprecated alias of repos — retained for one minor. list, get, create, archive, restore.
environmentsEnvironments under a project.
membershipsMembers, invitations, role updates.
teamsTeams, team members, role grants, effective access.
apiKeysWorkspace API keys (create, list, revoke).
webhooksWebhook endpoints, subscriptions, delivery attempts, secret rotation, replay.
meteringUsage recording, batch ingest, summaries, quota checks, quota violations.
billingPlans, customer, invoices, entitlements, checkout, portal, plan changes.
eventsThe audit log — list, page, iterate, NDJSON export.
securityEventsActor-scoped account security events.
configSettings (typed valueType/visibility on create and update), feature flags, secret metadata (listSecretMetadata takes an allScopes option — organization scope only — mapping to ?scope=all).
notificationsEmail notifications and per-user preferences.
authLogin, session, profile, OAuth provider discovery.
cliSessionsCLI device sessions and grant approval/denial.
integrationsGitHub integration: connections, repo links, deliveries, token minting.
stateState plane: workspace links, catalog, runs, content-addressed object reads.
transportThe underlying HTTP Transport — exposed for advanced extension.

Contract types (PublicProject, CreateOrganizationRequest, ERROR_CODES, …) are re-exported from @saas/sdk, so consumers never import @saas/contracts directly.

Per-request options

Every resource method accepts a final RequestOptions argument:

FieldTypeNotes
idempotencyKeystringSent as Idempotency-Key. Caller-owned — the SDK never auto-generates one.
signalAbortSignalForwarded to the underlying fetch for cancellation/timeouts.
requestIdstringSent as x-request-id. Auto-generated (req_<uuid>) when omitted.
headersRecord<string, string>Per-request header overrides (last write wins).
tip

Pass idempotencyKey on every create so a failed call can be retried with the same key — the edge replay store guarantees no double-create within the 24-hour replay window. See Idempotency.

Typed errors

Every non-2xx response throws a subclass of OrunCloudError. Branch on the class or on error.code:

ClasscodeStatus
BadRequestErrorbad_request400
UnauthenticatedErrorunauthenticated401
ForbiddenErrorforbidden403
NotFoundErrornot_found404
UnsupportedErrorunsupported405 / 415
ConflictErrorconflict409
PreconditionFailedErrorprecondition_failed412
ValidationErrorvalidation_failed422
RateLimitErrorrate_limited429
InternalErrorinternal_error500+
OrunCloudError (base)anyany

Every error carries code, status, requestId, details, the raw envelope, and (when synthesized from a real response) the original Response for raw header access. Two subclasses add structure:

  • ValidationError.fields — field-level violations as Record<string, string[]>, decoded from details.fields.
  • RateLimitErrorretryAfterSeconds (from Retry-After, falling back to details.retryAfterSeconds), scope ("org" | "identity"), and windows decoded from the X-RateLimit-{Limit,Remaining,Reset}-{org,identity} headers, with orgWindow / identityWindow convenience accessors.

Unknown error codes (forward compatibility — e.g. a future quota_exceeded) decode to the base OrunCloudError with the raw envelope preserved; non-JSON 5xx bodies (gateway HTML, empty body) decode to InternalError with message: "HTTP <status>". See Errors for the wire envelope.

Pagination and iteration

List endpoints are cursor-paginated: the success envelope's meta.cursor is the continuation token, and null means the end. The audit surface ships all three consumption styles:

// One page + the cursor (for paginated UIs):
const { entries, cursor } = await client.events.listAuditEntriesPage("org_1", {
by: "org",
category: "billing",
limit: 50,
});

// Every entry across every page, lazily:
for await (const entry of client.events.iterAuditEntries("org_1", { by: "org" })) {
console.log(entry.eventType);
}

// NDJSON export — one JSON line per entry:
for await (const line of client.events.exportAuditEntriesNdjson("org_1", { by: "org" })) {
process.stdout.write(line);
}

The iterator reads meta.cursor through the transport's envelope-aware path and carries two loop guards: a hard cap of AUDIT_ITERATOR_MAX_PAGES (1000) page reads, and a repeated-cursor check that throws rather than looping forever. securityEvents.listPage and webhooks.listDeliveryAttemptsPage follow the same page-plus-cursor pattern. See Pagination.

No built-in retries

The SDK deliberately ships no retry logic: retries interact with idempotency, rate-limit backoff, and abort semantics in ways the caller should own. The transport seam is left clean so wrapping it is trivial:

import { OrunCloud, RateLimitError, InternalError } from "@saas/sdk";

async function withRetry<T>(fn: () => Promise<T>, attempts = 3): Promise<T> {
for (let i = 0; ; i++) {
try {
return await fn();
} catch (err) {
const retryable =
err instanceof RateLimitError || err instanceof InternalError;
if (!retryable || i >= attempts - 1) throw err;
const delay =
err instanceof RateLimitError && err.retryAfterSeconds !== null
? err.retryAfterSeconds * 1000
: 2 ** i * 1000;
await new Promise((r) => setTimeout(r, delay));
}
}
}

// Safe because the same idempotency key replays, never re-executes:
const key = crypto.randomUUID();
const { project } = await withRetry(() =>
client.repos.create("org_1", { name: "web-app" }, { idempotencyKey: key }),
);

Honor RateLimitError.retryAfterSeconds before retrying 429s — see Rate limits.

Worked examples

List projects in a workspace

const { projects } = await client.repos.list("org_1");
for (const p of projects) {
console.log(`${p.id} ${p.name} ${p.status}`);
}

Create a project with an idempotency key

const { project } = await client.repos.create(
"org_1",
{ name: "Web app" },
{ idempotencyKey: crypto.randomUUID() },
);
console.log(project.id);

If the call fails mid-flight, retry with the same key — the edge replay store returns the original result instead of creating a duplicate.

Handle validation failures field by field

import { ValidationError, RateLimitError } from "@saas/sdk";

try {
await client.workspaces.create({ name: "" });
} catch (err) {
if (err instanceof ValidationError) {
for (const [field, problems] of Object.entries(err.fields)) {
console.warn(`${field}: ${problems.join("; ")}`);
}
} else if (err instanceof RateLimitError) {
console.warn(`back off ${err.retryAfterSeconds}s (scope=${err.scope})`);
} else {
throw err;
}
}

Cancel a slow request

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

const { organizations } = await client.workspaces.list({
signal: controller.signal,
});