import { type WranglerConfig, type WorkerManifestInput } from './workers.cjs'; /** The hosted control plane: what a client talks to when neither the options * nor PLATINUM_API_URL / PT_API_URL name another one. */ export declare const DEFAULT_API_URL = "https://api.platinum.dev"; export interface PlatinumOptions { apiUrl?: string; url?: string; token?: string; fetch?: typeof fetch; timeoutMs?: number; } export type SandboxType = 'persistent' | 'ephemeral'; export type ExecLang = 'python' | 'bash' | 'node' | 'sh'; /** Options for the low-level {@link Platinum.request}. */ export interface RequestOptions { timeoutMs?: number; rawBody?: Uint8Array | string | ArrayBuffer | Blob | ReadableStream | FormData; rawResponse?: boolean; query?: Record; headers?: Record; signal?: AbortSignal; } /** Inline declarative image spec — alternative to `template`. Mirror of * `POST /v1/templates/from-spec` body, minus `name`/`version` (Platinum * derives `inline-` deterministically). Identical specs across any * caller dedupe to one cached template. */ export interface SandboxImageSpec { base_image: string; steps?: Array>; entrypoint?: string; ready_cmd?: string; default_cpu?: number; default_ram_mb?: number; default_disk_gb?: number; size_mb?: number; } export interface SandboxSpec { /** Optional human-friendly label. If omitted, the sandbox id is used as the * display name. Must be unique among your org's active sandboxes (reusable * once a sandbox is destroyed); a clash returns 409. */ name?: string; /** Pre-built template — name or `tpl_`. Mutually exclusive with `image`. */ template?: string; /** Inline image — either a `Template` builder instance OR a raw `SandboxImageSpec`. * Platinum hashes the spec, cache-hits a prior build or materializes a new * template on-demand, then boots the sandbox in one call. With * `waitForRunning: true`, the create resolves when the VM is ready. */ image?: SandboxImageSpec | { toImageSpec(): Record; }; /** persistent (default) | ephemeral (auto-stop 15m, archive 7d, delete 30d). */ type?: SandboxType; /** Pin to a specific region (must have at least one ready host). */ region?: string; /** Execution backend — one of GET /v1/runtimes. Omit for microvm. See docs/runtimes.md. */ runtime?: string; /** A cell's Worker: its folder under the org's storage prefix (workers/). Only with runtime 'cell'. */ worker?: string; cpu?: number; ram_mb?: number; disk_gb?: number; gpus?: number; /** Env vars — written to /etc/environment on boot AND set on the running guest. */ env?: Record; /** Alias of `env` for E2B compatibility. Both are merged. */ envVars?: Record; /** OpenSSH public keys, one per line — written to /root/.ssh/authorized_keys. */ ssh_keys?: string[]; /** Default language for `runCode` calls. */ language?: ExecLang; /** Auto-lifecycle overrides — 0 = disabled. Defaults set by `type`. */ auto_stop_minutes?: number; auto_archive_days?: number; auto_delete_days?: number; /** Whether an inbound request to an exposed port wakes the sandbox while it * is stopped or archived. Default true. false = only `start()`/`resume()` * bring it back — for callers that track the sandbox's state themselves. */ auto_resume?: boolean; /** Automatic recovery-backup interval in minutes. 0 disables; omitted = 60. */ backup_interval_min?: number; /** Parent sandbox. Omit for a top-level sandbox; set it and this sandbox is * a child of that one, listable via `sandboxes.children(parentId)`. The * parent must be live and in the same organization. */ parent_id?: string; /** Arbitrary metadata — surfaced on GET /v1/sandboxes/:id. */ metadata?: Record; /** Inline expose: same as POST /v1/sandboxes/:id/expose but folded into * the create response, saving one round-trip. With `waitForRunning`, * the spawn → reachable URL is a single Atlantic RTT. Max 8 entries. */ expose?: Array<{ port: number; public?: boolean; ttl_seconds?: number; }>; /** Outbound internet access. Default true. A falsy value is exactly * `network: { denyOut: ['0.0.0.0/0'] }`. Applied to the VM's interface * BEFORE it boots, so a sandbox created with `false` never had a route out. */ allowInternetAccess?: boolean; /** Fine-grained egress: allow/deny lists and per-host request transforms. */ network?: NetworkConfig; /** Org secrets this sandbox may spend, attached before it boots. Max 16. * A bare string is a secret id or name (`secrets: ['search-api']`). * * The egress the credential needs is DERIVED: each secret's allowed * hostnames and the header it lands in are added to this sandbox's policy * and applied to the interface before first boot, so the box is never up * with the destination reachable and the credential absent. An entry that * cannot be resolved fails the create — a sandbox that exists but cannot * spend what it was created with is the failure this exists to kill. * * Nothing lands in the guest: no env var, no file, no placeholder. Read * back what is attached with `sbx.secrets.list()`. */ secrets?: SecretAttach[]; /** Volumes to mount before first boot, keyed by guest mount path (max 8): * * volumes: { '/data': vol, '/home': vol.subdir(`/users/${uid}`), '/ref': 'shared-ref' } * * A value is a `Volume`, a `vol.subdir(p).readOnly()` spec, or a volume * name or id. The response's sandbox carries the mounts as `volume_mounts`. */ volumes?: Record; } /** `0.0.0.0/0` — every destination. Prefer the selector form * (`denyOut: ({ allTraffic }) => [allTraffic]`), which reads better and cannot * be typo'd; this constant stays for callers that want the literal. */ export declare const ALL_TRAFFIC = "0.0.0.0/0"; /** One per-host rule. `transform.headers` is injected into matching outbound * HTTP/HTTPS requests by the egress proxy. */ export interface NetworkRule { transform?: { headers?: Record; }; } /** Context handed to the selector callbacks. */ export interface NetworkSelectorCtx { allTraffic: typeof ALL_TRAFFIC; rules: Map; } type NetworkList = string[] | ((ctx: NetworkSelectorCtx) => string[]); /** * Per-sandbox egress policy. * * Entries in `allowOut` may be IPs, CIDRs, hostnames or `*.wildcards`; * `denyOut` takes addresses only. **Allow always wins over deny.** Using a * hostname requires denying everything else, so a domain list cannot read like * a restriction while leaving egress open. * * // only google.com is reachable * { allowOut: ['google.com'], denyOut: ({ allTraffic }) => [allTraffic] } * * // inject a header on calls to one host, everything else untouched * { rules: { 'api.example.com': [{ transform: { headers: { 'X-H': 'v' } } }] } } * * Registering a rule grants no egress by itself — reference the host in * `allowOut` too (`allowOut: ({ rules }) => [...rules.keys()]`). * * Domain matching reads the TLS SNI on :443 and the Host header on :80. Other * ports are filtered by address only. Header injection into HTTPS terminates * TLS at the host for THAT host alone, using a certificate authority generated * per sandbox and trusted only inside it; every other connection is passed * through untouched. */ export interface NetworkConfig { allowOut?: NetworkList; denyOut?: NetworkList; rules?: Record | Map; /** Also settable at the top level of the create options. */ allowInternetAccess?: boolean; } /** Resolve the selector callbacks and Map form into the plain JSON the API * takes. Exported for the SDK's own tests; callers never need it. */ export declare function serializeNetwork(net: NetworkConfig | undefined): Record | undefined; /** Read-back shape of GET /:id/network. * * The top level is the policy IN FORCE, which since org-secret bindings can * include entries the control plane derived. `owned` and `managed` split it by * author — see their own docs for why that split matters if you ever POST a * policy back. */ export interface NetworkView { allow_internet_access: boolean; allow_out: string[]; deny_out: string[]; rules: Record; proxy: boolean; applied_at?: string; /** Your own policy — exactly what a POST to /:id/network replaces, in the * vocabulary it accepts. * * HYDRATE A FORM FROM THIS, NOT FROM THE TOP LEVEL. The top level carries * the CP-derived hostnames and the `` marker standing in for a * credential's header value; POSTing those back would freeze a derived * destination into your own allowlist (so unbinding the secret would leave * it reachable forever) and send a marker the API refuses. Round-tripping * `owned` is a no-op by construction. * * Optional only because a control plane older than this field omits it. */ owned?: { allow_internet_access: boolean; allow_out: string[]; deny_out: string[]; rules: Record; }; /** What the control plane added on your behalf, so a UI can say "this host is * reachable BECAUSE a secret is bound" instead of showing rules the user did * not write and cannot delete. READ-ONLY: no endpoint accepts it. Change it * by attaching or detaching a secret. */ managed?: { allow_out: string[]; rules: Record; reason: string; /** Distinct credentials the applied policy is carrying (not rule count). */ bindings: number; }; /** Present when the sandbox's host is too old to enforce hostname rules or * transforms; the address part of the policy is in force and the rest is * denied. */ warning?: string; } /** What the proxy does when the credential's own bytes appear in a response * on the way back to the guest. `block` (the default) kills the response; * `redact` replaces the bytes, which leaks their length. Both are tripwires * for an origin that echoes by accident, not defences against a hostile one. */ export type SecretOnEcho = 'block' | 'redact'; export interface SecretSpec { /** Org-unique label: lowercase, starts with a letter, ≤64 chars. */ name: string; /** The credential. Sent once, on this call, and never returned by anything. */ value: string; description?: string; /** EXACT hostnames the value may be spent against. No wildcards — `*.x.com` * is refused client-side and server-side. Required and non-empty: a secret * with no destination can never be spent. */ allow: string[]; /** Request headers the proxy may write the value into (e.g. `authorization`). * Required and non-empty. Header names only — there is no body or query * substitution. */ headers: string[]; on_echo?: SecretOnEcho; } /** PATCH body. `value` is deliberately absent: a new value is a rotation * (`rotate()`), which keeps the placeholder stable. */ export interface SecretPatch { /** Rename. Org-unique, same rules as `SecretSpec.name`. Metadata only — a * rename does not reseal the value or bump the generation. */ name?: string; description?: string; allow?: string[]; headers?: string[]; on_echo?: SecretOnEcho; } /** Metadata for one secret. There is no `value` field on this type, on the * wire, or anywhere else. */ export interface Secret { id: string; name: string; description: string | null; /** Version counter. A rotation or a routing edit bumps it; never reused. */ current_gen: number; allow: string[]; headers: string[]; on_echo: SecretOnEcho; created_at: string | null; created_by: string | null; last_used_at: string | null; } /** GET /v1/secrets/:id — metadata plus this secret's live bindings. */ export interface SecretDetail extends Secret { bindings: SecretBinding[]; } /** "This sandbox may spend this secret." The binding's placeholder is not * part of this shape — the API never returns it and nothing outside the * control plane and the host proxy has a use for it. */ export interface SecretBinding { id: string; sandbox_id: string; secret_id: string; /** The key this binding is addressed by — `detach(alias)` takes it. */ alias: string; /** The same value as `alias`, always. The spelling that shipped first; both * are returned by every endpoint and neither is going away. */ env_name: string; /** Request header this binding lands in, once the derive has placed it. * null when the host has not resolved one yet. */ header: string | null; created_at: string | null; revoked_at: string | null; /** Set by `unbind()` when the revoke SUCCEEDED but the host refused the * updated egress policy. The credential is gone either way; the dead * transform rule that stays behind makes every request from that sandbox to * the allowed hostname fail CLOSED (503) until the policy is re-applied. * Surface it — an unbind that returns this is not a clean success. */ warning?: string; } export interface SecretPage { items: Secret[]; /** Total live secrets in the org, when the API reports one. */ total: number | null; /** Pass back as `cursor` for the next page; null on the last page. */ cursor: string | null; } export interface SecretDeleted { id: string; name: string; deleted: boolean; /** True when the ciphertext was erased, not merely flagged deleted. */ value_erased: boolean; } /** One requested attachment. A bare string is the common case and means "this * secret, under its own name, in whichever header it permits" — it is read as * a secret id (`sec_…`) OR a secret name. * * The object form exists for the two things that cannot be defaulted: a second * key for the same secret (`alias`), and an explicit destination when two * secrets compete for one header on one host (`header`). */ export type SecretAttach = string | { /** Secret id (`sec_…`) or name. */ secret: string; /** Key this binding is addressed by. Defaults to the secret's own name. * `^[A-Za-z_][A-Za-z0-9_-]{0,63}$` — and NOT an env var: nothing of this * name exists in the guest. */ alias?: string; /** Which of the secret's own `headers[]` the value is written into. Must be * one of them; anything else is refused (the ciphertext does not authorize * a header outside that list). */ header?: string; }; /** Is this credential in force on the datapath, on its way, or not going to be? * * `armed` the host has the credential and the sandbox is up. The only * value that means "spend it". * `arming` consistent, but the datapath has not caught up (still booting, * or the ~15 s lease cycle has not run). Retry; do not * reconfigure. * `unavailable` waiting will not help: the secret was deleted, the header it * needs is taken, or this sandbox's egress policy cannot carry a * credential at all. Something has to change first. */ export type SecretBindingState = 'armed' | 'arming' | 'unavailable'; /** One credential attached to one sandbox. No value, no placeholder, ever — * not on this type, not on the wire. */ export interface AttachedSecret { binding_id: string; secret_id: string; /** null when the secret was deleted out from under the binding. */ secret_name: string | null; alias: string; /** Always equal to `alias` (see SecretBinding). */ env_name: string; /** Exact hostnames this credential may be spent against. */ hosts: string[]; /** Header the derive actually placed it in — the outcome, not the request. */ header: string | null; on_echo: SecretOnEcho | null; gen: number | null; state: SecretBindingState; } /** GET/PUT/DELETE `/v1/sandboxes/:id/secrets`, and the `secrets` block folded * into a create that carried `secrets:`. */ export interface SandboxSecrets { sandbox_id: string; /** What the control plane wants the host to carry. */ epoch: number; /** What the host last leased. `leased_epoch >= epoch` on a running box is * what makes a binding `armed`. */ leased_epoch: number; /** Worst case over `secrets[]`. No bindings is `armed`: the datapath carries * exactly what it should, which is nothing. */ state: SecretBindingState; /** Present when this sandbox's egress policy cannot carry a credential at * all — each entry names a rule the policy is missing. */ policy_blocked?: string[]; /** What the bindings ADD to this sandbox's egress. None of it is stored: it * is recomputed from the bindings on every push, so a hostname reachable * BECAUSE of a credential is visible as such. */ implied_egress: { domains: string[]; /** host → the header names written on requests to it. */ rules: Record; proxy: boolean; }; secrets: AttachedSecret[]; /** `detach()` only, and only when the revoke landed but the host refused the * re-pushed egress policy — same meaning as `SecretBinding.warning`. */ warning?: string; } /** Normalize one attach entry into the wire shape. A string passes straight * through (the API reads it as an id or a name); an object is rebuilt key by * key so a stray field cannot ride along into the request. */ export declare function serializeSecretAttach(entry: SecretAttach): unknown; /** Same explicit-projection rule as toSecret(): nothing the client did not ask * for reaches the caller. There is no value and no placeholder in this * response — that is a property of the endpoint, and this keeps it a property * of the client too. */ export declare function toSandboxSecrets(raw: unknown): SandboxSecrets; /** PATCH /v1/sandboxes/:id body. Fields left out are unchanged. */ export interface SandboxUpdate { /** Display label; null or '' clears it. */ name?: string | null; /** Whether an inbound request to an exposed port wakes the sandbox while stopped. */ auto_resume?: boolean; } export interface SandboxState { id: string; /** Display label, or null when none was set (fall back to `id`). */ name: string | null; orgId: string; templateId: string; hostId: string | null; state: 'pending' | 'provisioning' | 'starting' | 'running' | 'stopping' | 'stopped' | 'resuming' | 'restoring' | 'archiving' | 'archived' | 'unarchiving' | 'failed-provision' | 'failed-start' | 'lost' | 'deleted'; type?: SandboxType; region: string | null; /** The API origin that owns this sandbox (its region's control plane), e.g. * `https://us-east.api.platinum.dev`. Handles returned by `create()` and * `connect()` pin themselves to it so a per-sandbox call never crosses regions. * Absent or equal to the client's URL on a single-region deployment. */ api_url?: string; /** Execution backend — one of GET /v1/runtimes. Every sandbox created before runtimes existed is 'microvm'. */ runtime?: string; /** A cell's Worker folder under the org prefix; null/absent = the org root. */ worker?: string | null; cpu: number; ramMb: number; diskGb: number; vsockCid: number | null; internalIp: string | null; exposedPorts: number[]; metadata: Record; errorMessage: string | null; autoStopMinutes?: number; autoArchiveDays?: number; autoDeleteDays?: number; /** Whether an inbound request to an exposed port wakes it while stopped. */ autoResume?: boolean; backupIntervalMin?: number; lastActivityAt?: string | null; createdAt: string; startedAt: string | null; stoppedAt: string | null; deletedAt: string | null; /** Public region + datacenter labels the API derives per row. */ datacenter?: string | null; /** True once the in-VM agent has answered on vsock (exec/files are usable). */ agent_ready?: boolean; templateName?: string | null; } /** Result of exec/sh/runCode. stdout/stderr are ALWAYS strings — the in-VM * agent omits empty fields on the wire and the SDK normalizes them back. */ export declare class ExecResult { readonly stdout: string; readonly stderr: string; readonly exit_code: number; readonly error?: string | undefined; /** runCode only — wall time measured by the API. */ readonly duration_ms?: number | undefined; /** runCode only — the language that actually ran (after server-side fallback). */ readonly lang?: string | undefined; /** The deadline the API actually applied, in ms. Only sent when it differs * from the one asked for; see `MAX_EXEC_TIMEOUT_MS`. */ readonly timeout_ms?: number | undefined; /** True when `timeout_ms` above is a clamped-down version of what this call * asked for. Absent (undefined) whenever the request was honoured as sent. */ readonly timeout_ms_clamped?: boolean | undefined; constructor(stdout: string, stderr: string, exit_code: number, error?: string | undefined, /** runCode only — wall time measured by the API. */ duration_ms?: number | undefined, /** runCode only — the language that actually ran (after server-side fallback). */ lang?: string | undefined, /** The deadline the API actually applied, in ms. Only sent when it differs * from the one asked for; see `MAX_EXEC_TIMEOUT_MS`. */ timeout_ms?: number | undefined, /** True when `timeout_ms` above is a clamped-down version of what this call * asked for. Absent (undefined) whenever the request was honoured as sent. */ timeout_ms_clamped?: boolean | undefined); /** Throw if exit_code != 0 (analogous to subprocess.check_output). */ check(): this; } export interface ExposeResult { url: string; port: number; sandbox_id: string; token?: string; public: boolean; } export interface SnapshotResult { id: string; /** Present on the synchronous path; a slow snapshot returns 202 with * state:'in_progress' instead — poll listSnapshots() for the row. */ size_bytes?: number; took_ms?: number; signed?: boolean; state?: 'in_progress'; detail?: string; } export interface ForkResult { id: string; forked_from: string; snapshot_id: string; state: string; snapshot_size_bytes?: number; snapshot_took_ms?: number; } export interface CloneResult { id: string; cloned_from: string; snapshot_id: string; state: string; } export interface SandboxSnapshotRow { id: string; sandboxId: string; sizeBytes?: number; createdAt?: string; [k: string]: unknown; } export interface SandboxMetrics { ts: string; state: string; cpu_count: number; /** The sandbox's configured (guest) RAM. This is the SPEC value — it is the * denominator for what you asked for, NOT for `mem_used_mb`. */ mem_total_mb: number; disk_total_gb: number; gpus?: number; created_at: string; started_at: string | null; last_activity_at?: string | null; age_seconds: number; /** null until the first heartbeat sample lands (~30 s after start) or when stale. */ cpu_pct: number | null; /** Host cgroup `memory.current` for the VM's scope, in MiB — the memory the * hypervisor process is charged for, which INCLUDES host page cache from the * guest's disk I/O. It is therefore routinely larger than what the guest * reports for itself, and can exceed `mem_total_mb`. * * Divide by `mem_limit_mb`, not by `mem_total_mb`: those two are the same * accounting domain and the kernel guarantees the ratio stays ≤ 100%. */ mem_used_mb: number | null; /** Host cgroup `memory.max` for the same scope, in MiB — the ceiling the * kernel throttles and OOM-kills against. null when the sandbox's host agent * predates this field. */ mem_limit_mb: number | null; disk_used_mb: number | null; } /** One event from files.watch(). 'ready' fires once the server has armed the * baseline (safe to create files from then on); 'change' carries the changed * path; 'heartbeat' ticks every poll interval. */ export type WatchEvent = { event: 'ready' | 'heartbeat'; data: string; } | { event: 'change'; data: { ts: number; type: 'file' | 'dir' | 'link' | 'other'; path: string; }; }; export interface RegionInfo { region: string; label?: string; /** True if the region has GPU capacity. Host/DC counts are not exposed. */ gpuAvailable?: boolean; } export { PlatinumError, ValidationError, AuthenticationError, ForbiddenError, NotFoundError, ConflictError, RateLimitError, ServerError, PlatinumTimeoutError, PlatinumConnectionError, AtomicReplaceUnsupportedError, VolumeNotFoundError, VolumeExistsError, VolumeBusyError, QuotaExceededError, ReadOnlyMountError, CommitFailedError, } from './errors.cjs'; export type { VolumeHolder } from './errors.cjs'; import { SandboxProcesses } from './process.cjs'; import { Volumes, SandboxVolumes, type VolumeMountInput } from './volumes.cjs'; export { Volume, Volumes, VolumeListing, VolumeMountSpec, VolumeUploader, SandboxVolume, SandboxVolumes, VOLUME_BLOCK_SIZE, VOLUME_MAX_PUT_BYTES, } from './volumes.cjs'; export type { VolumeData, SyncMode, VolumeFileEntry, VolumeStat, VolumeOverwrite, VolumeSkip, CommitResult, VolumeCommit, VolumeMount, VolumeTag, VolumeLimits, VolumePage, VolumeHistoryPage, VolumeWriteResult, VolumePublish, VolumeWaitResult, VolumeUploadResult, PullResult, VolumeMountInput, VolumeReadOptions, VolumeWriteOptions, UploadFileOptions, VolumeUploadOptions, VolumeUploadInfo, VolumeFilePage, VolumeWriteData, } from './volumes.cjs'; export { SandboxProcesses, ProcessHandle, isManagedProcessesUnsupported } from './process.cjs'; export type { ManagedProcess, ProcessLogs, ProcessStreamEvent, ProcessSignal, ProcessSignalName, ProcessStartSpec, ProcessStartOptions, ProcessCallOptions, ProcessLogsOptions, ProcessStreamOptions, ProcessWaitOptions, ProcessStdinResult, } from './process.cjs'; export declare class Platinum { readonly apiUrl: string; readonly token: string; private readonly _fetch; readonly timeoutMs: number; constructor(opts?: PlatinumOptions); /** * This client, addressed at another API origin. Same token, same fetch, same * timeouts; only `apiUrl` differs. Sandbox handles use it to pin themselves to * the control plane that owns the sandbox (`api_url` on the sandbox object), so * every call made through the handle (lifecycle, exec, files, processes, the * terminal) goes straight to that region. Returns `this` unchanged for a * missing or identical origin, which is every single-region deployment — no * behaviour change there. * * A real client, not `Object.create(this)`: the namespaces (`sandboxes`, * `volumes`, `templates`, …) are fields whose functions are bound to the * client that built them, so a prototype view still sent refresh, waitState * and delete to the global endpoint. One client per origin, shared by every * handle in that region. */ forOrigin(origin?: string | null): Platinum; private readonly _origins; request(method: string, path: string, body?: unknown, opts?: RequestOptions): Promise; private send; /** Workers: deploy a built module to the folder cells created with `worker: ` boot from. No storage credentials. */ readonly workers: { /** GET /v1/workers — every worker in the org with its active version and cell count. */ list: () => Promise<{ workers: Worker[]; }>; /** GET /v1/workers/:name — the active version and the cells serving it. */ get: (name: string) => Promise; /** POST /v1/workers/:name/versions — upload a built module + manifest; the version is content-addressed. */ deployVersion: (name: string, input: { bundle: Uint8Array | Blob; manifest: WorkerManifestInput; }) => Promise; /** POST /v1/workers/:name/activate — point the worker at a version; cells named in the reply need a restart on celld 0.3.0. */ activate: (name: string, version: string) => Promise; /** Build the manifest from wrangler config, upload, activate. */ deploy: (name: string, input: { wrangler: WranglerConfig; bundle: Uint8Array | Blob; }) => Promise; }; readonly sandboxes: { /** * Create a sandbox. Pass `template` (pre-built name/id) or `image` * (inline spec — either a `Template` builder or raw `SandboxImageSpec`). * * With `waitForRunning: true` (or `waitTimeoutMs` set), the server holds * the POST open until the sandbox reaches `running` — single Atlantic * RTT, no client-side polling. Inline-image creates pay the build cost * the first time and cache-hit thereafter, so bump the timeout when you * expect a fresh build (default 60s; recommend 600000 for inline). * Note: inline-image builds run in-handler for the full timeout, but the * post-boot STATE wait is clamped to 60s server-side — check the returned * state (or waitRunning()) if your boot can exceed that. */ create: (spec: SandboxSpec, opts?: { waitForRunning?: boolean; waitTimeoutMs?: number; signal?: AbortSignal; }) => Promise; get: (id: string) => Promise; /** Fetch an existing sandbox by id and return a hydrated handle (name, * hostId populated; verifies the sandbox exists — 404 → NotFoundError). */ connect: (id: string) => Promise; list: (opts?: { limit?: number; offset?: number; state?: string; runtime?: string; worker?: string; parent_id?: string; rootsOnly?: boolean; }) => Promise<{ rows: SandboxState[]; total: number; has_more: boolean; }>; /** GET /v1/sandboxes/:id/children — the direct children of one sandbox. * Throws NotFoundError(404) for a sandbox you don't own, rather than * returning an empty list. */ children: (id: string) => Promise; delete: (id: string) => Promise; /** Set or clear a sandbox's display label by id. Pass null/'' to clear it. * Throws ConflictError(409) if another active sandbox already uses the name. */ rename: (id: string, name: string | null) => Promise; /** PATCH /v1/sandboxes/:id — change mutable fields by id. Fields left out * are unchanged. `auto_resume: false` stops inbound requests from waking * the sandbox while it is stopped; only start()/resume() bring it back. */ update: (id: string, patch: SandboxUpdate) => Promise; /** Iterate ALL sandboxes (auto-pagination over list()): * for await (const s of dn.sandboxes.iterate({ state: 'running' })) ... */ iterate: (opts?: { state?: string; pageSize?: number; }) => AsyncGenerator; }; private _iterateSandboxes; /** Volumes: named, versioned filesystems you mount into sandboxes or reach * directly with `vol.files.*`. See `Volume`. */ readonly volumes: Volumes; readonly templates: { list: () => Promise>; /** GET /v1/templates/:id — one template incl. state/build fields * (build internals are stripped unless you own it). */ get: (id: string) => Promise>; /** DELETE /v1/templates/:id — refuses while a build is in flight or a * live sandbox still uses the template (409). */ delete: (id: string) => Promise; }; /** Org webhooks — lifecycle events POSTed to your endpoint, HMAC-signed * (x-pt-signature: sha256= over the JSON body). */ readonly webhooks: { /** POST /v1/webhooks — subscribe a URL. `events` defaults to ['*']; * `secret` is generated when omitted and returned ONCE. */ create: (spec: { url: string; secret?: string; events?: string[]; }) => Promise<{ id: string; url: string; events: string[]; secret_returned_once: string; }>; list: (opts?: { limit?: number; offset?: number; }) => Promise>>; /** PATCH /v1/webhooks/:id — pause/resume (enabled), change url or events. */ update: (id: string, patch: { enabled?: boolean; url?: string; events?: string[]; }) => Promise>; delete: (id: string) => Promise; /** POST /v1/webhooks/:id/test — synthetic `ping` through the real * delivery path (HMAC + reachability). Poll deliveries() for the result. */ test: (id: string) => Promise<{ delivery_id: string; status: string; }>; deliveries: (id: string, opts?: { status?: "pending" | "success" | "failed" | "dead_letter"; limit?: number; offset?: number; }) => Promise<{ rows: Array>; total: number; has_more: boolean; }>; /** POST /v1/webhooks/:id/deliveries/:dlvId/retry — retry a failed or * dead-lettered delivery now, with a fresh attempt budget. */ retryDelivery: (id: string, deliveryId: string) => Promise>; }; /** * Org secrets — a credential the sandbox can spend but never holds. * * The value goes in once and is never returned by any call at any scope. * The host's egress proxy writes it into the headers named in `headers`, * over HTTPS only, for the exact hostnames in `allow`. Plain HTTP is * refused, not passed through unarmed. * * const s = await client.secrets.create({ * name: 'search-api', value: 'Bearer ' + process.env.SEARCH_API_KEY!, * allow: ['api.example.com'], headers: ['authorization'], * }); * // attach at create — the egress it needs is derived and applied * // before the VM boots: * const sbx = await client.sandboxes.create({ template: 'pt-base', secrets: [s.name] }); * // …or afterwards, declaratively: await sbx.secrets.set([s.name]); * * Store the FINISHED header value, scheme prefix and all: the proxy writes * `value` into the header verbatim. `authorization` wants `Bearer sk_…`; * `x-api-key` wants the bare key. * * `allow` IS the security boundary: a sandbox can spend the credential * against any host on that list, so only list hosts that consume it. See * the secrets guide in the docs before widening one. * * Scopes: `secrets:read` on the reads, `secrets:write` on create/rotate/ * delete, `secrets:use` on bind/unbind. Editing `allow`/`headers`/`on_echo` * needs write AND use — widening an allowlist is disclosure. */ readonly secrets: { /** POST /v1/secrets — store a credential. Returns metadata, never the value. */ create: (spec: SecretSpec) => Promise; /** GET /v1/secrets — one cursor page. Pass the returned `cursor` back for * the next one; a null cursor is the last page. */ list: (opts?: { limit?: number; cursor?: string; }) => Promise; /** GET /v1/secrets/:id — metadata plus this secret's live bindings. */ get: (id: string) => Promise; /** PATCH /v1/secrets/:id — rename, edit the description, or change the * routing decision. Editing `allow`/`headers`/`on_echo` reseals the value * at a new generation and needs `secrets:use` on top of `secrets:write`; * `name`/`description` are plain metadata and need neither. */ update: (id: string, patch: SecretPatch) => Promise; /** POST /v1/secrets/:id/versions — store a new value at gen+1. Bindings and * their placeholders are untouched, so every bound sandbox picks the new * value up without being recreated. */ rotate: (id: string, value: string) => Promise; /** DELETE /v1/secrets/:id — revoke every binding and erase the ciphertext. */ delete: (id: string) => Promise; /** POST /v1/secrets/bindings — let one sandbox spend one secret. `secret` * is the secret's id or its name. Needs `secrets:use`. * * Imperative, and keyed on a binding id you then have to keep. For a set * you can re-send — a deploy, a redeploy — prefer `sbx.secrets.set()`, * which diffs against what is attached and leaves unchanged entries alone. * * await client.secrets.bind({ sandbox_id: sbx.id, secret: 'search-api', alias: 'search-api' }); */ bind: (spec: { sandbox_id: string; secret: string; /** Key this binding is addressed by. Give one of `alias` / `env_name`. */ alias?: string; /** @deprecated The pre-`alias` spelling of the same field. Still accepted * everywhere, permanently — but new code should say `alias`. */ env_name?: string; }) => Promise; /** DELETE /v1/secrets/bindings/:id — revoke one binding. The host zeroizes * the credential on its next lease. * * CHECK `.warning` ON THE RESULT. It is set when the revoke landed but the * host rejected the re-pushed egress policy: the sandbox's requests to the * allowed hostname then fail closed until the policy is re-applied * (`sandboxes.setEgressPolicy` / a restart). A truthy `warning` means the * unbind was NOT a clean success. */ unbind: (bindingId: string) => Promise; }; readonly regions: { /** GET /v1/regions — regions with at least one ready host. */ list: () => Promise; }; readonly health: { check: () => Promise<{ status: string; ts: string; }>; }; /** Identity + role of the current bearer. */ me(): Promise<{ orgId: string; role: 'admin' | 'org'; userId?: string; orgRole?: 'owner' | 'admin' | 'member'; }>; } export interface SandboxInit { state?: string; name?: string | null; hostId?: string | null; warm?: boolean; /** Ports the create() call already exposed via the inline `expose:` field * on the spawn request. Skips a follow-up POST /v1/sandboxes/:id/expose. */ exposed?: Array<{ port: number; url: string; token?: string; public: boolean; }>; } /** * The longest an `exec` / `sh` / `runCode` call can actually run. * * This is the EDGE's budget, not a product choice. The hosted API sits behind a * reverse proxy whose `response_header_timeout` is 90 s, itself under the CDN's * ~100 s origin ceiling. A request the origin has not answered by then has its * response replaced with an HTML error page, so a typed client gets markup and * no `code`. Measured against the hosted API: a 30 s command returned in 30.2 s, * a 110 s command died at 90.2 s with an HTML 504. * * A larger `timeoutMs` is not refused — the API accepts it and clamps the * command's deadline to this, then reports the clamp on the result * (`timeout_ms` / `timeout_ms_clamped`). Nothing that runs inside the budget * changes. Past it you get a normal timed-out `ExecResult` instead of an edge * error page. * * Work that genuinely needs longer belongs on `sbx.process.*` — a managed * process is started in one short request, outlives it, and is polled for * output. */ export declare const MAX_EXEC_TIMEOUT_MS = 85000; export declare class Sandbox { readonly client: Platinum; readonly id: string; /** True if the create() returned a warm-pool VM (claim, not spawn). */ readonly warm: boolean; /** Display label set at create (or via .rename()), or null if none. */ name: string | null; /** Host that runs this sandbox — may change on cross-host resume. */ hostId: string | null; /** Ports currently exposed on this handle. Seeded from the create response's * inline `expose:` and kept in sync as you call `.expose()` / `.unexpose()`. */ private _exposed; get exposed(): ReadonlyArray<{ port: number; url: string; token?: string; public: boolean; }>; /** Managed processes on this sandbox — start something that keeps running * after the call returns, then read its output by absolute offset: * * const p = await sbx.process.start({ cmd: ['npm','run','dev'], cwd: '/app' }); * for await (const ev of p.stream()) … * await sbx.process.list(); * const again = await sbx.process.get(p.handle); // reconnect later * * Unlike `exec()`, nothing is killed when the RPC returns. Assigned in the * constructor body rather than as a field initializer: parameter properties * (`client`, `id`) are not assigned until the body runs, so an initializer * would capture undefined for both. */ readonly process: SandboxProcesses; /** Volumes mounted on this sandbox: * * await sbx.volumes.attach('/data', vol); * const r = await sbx.volumes.get('/data').commit(); // durable on return * await sbx.volumes.get('/data').pull(); // take others' commits */ readonly volumes: SandboxVolumes; constructor(client: Platinum, id: string, init?: SandboxInit); /** Convenience: URL of an exposed port — whether from the inline `expose:` at * create or a later `.expose()` call — or null if that port isn't exposed. */ exposedUrl(port: number): string | null; refresh(): Promise; /** Block until the sandbox reaches state='running'. */ waitRunning(opts?: { timeoutMs?: number; intervalMs?: number; }): Promise; /** Block until the sandbox reaches `target` (e.g. 'stopped' after .stop() * or .pause()). Throws PlatinumError on a terminal failure state and * PlatinumTimeoutError when the deadline expires. Always polls at least * once, even with timeoutMs <= 0. */ waitState(target: SandboxState['state'] | 'paused', opts?: { timeoutMs?: number; intervalMs?: number; }): Promise; delete(): Promise; /** Pause the sandbox (alias of stop(): memory and disk are kept) and return * once it has settled. A paused sandbox reports state `stopped`; bring it * back with resume() or start(). */ pause(opts?: { waitTimeoutMs?: number; }): Promise; resume(): Promise; fork(): Promise; /** Take a DISK snapshot. Optional scrub flags wipe secrets before the disk * image is taken (useful when the snapshot will be shared). * * Snapshots capture disk state only. RAM and running processes are NOT * captured, so `clone()`/`restore()` cold-boot the snapshot disk and tmpfs * paths like /tmp come back empty. Memory state is preserved only across * `stop()`/`start()` (and their `pause()`/`resume()` aliases). * * Even on that memory-preserving path, a nested hypervisor running inside * the sandbox (qemu/KVM, an Android emulator) must be cold-restarted after * a resume: its guest CPU state is not part of what gets restored, and * process-bound transports such as ADB will not recover on their own. */ snapshot(opts?: { scrub_env?: string[]; clear_tmp?: boolean; clear_ssh_keys?: boolean; }): Promise; archive(): Promise; /** Set or clear this sandbox's display label. Pass null/'' to clear it (the * id is shown again). Throws PlatinumError(409) if another active sandbox in * the org already uses the name. Returns the updated sandbox state. */ rename(name: string | null): Promise; /** Merge environment variables into this sandbox. A null value REMOVES that * key; keys you do not mention are left untouched. * * APPLIES ON THE NEXT COLD BOOT, not immediately: a running guest keeps the * environment it was exec'd with, exactly like `docker run -e`. Call this, * then stop and start the sandbox, for the new value to take effect. * * Use it to rotate a credential the sandbox holds. Without it a sandbox's * environment was fixed at create for its whole life, so a rotated token * could never reach the guest: every cold boot re-seeded /etc/pt-env from * the create-time values and the guest came back on the old secret. * * Values are never echoed back — the response carries key names only. */ setEnv(env: Record): Promise<{ id: string; env_var_keys: string[]; applies: string; detail: string; }>; /** Whether an inbound request to an exposed port may wake this sandbox while * it is stopped or archived. Off = only start()/resume() bring it back. */ setAutoResume(enabled: boolean): Promise; backup(): Promise<{ id: string; backup_state: string; }>; /** Alias of delete() — the name most sandbox users reach for first. */ kill(): Promise; /** POST /:id/stop — stop the VM, keep identity + disk (restart later with * .start()). `waitForStopped` holds the response server-side until the host * confirms (no client polling; server caps the hold at 60 s). */ stop(opts?: { waitForStopped?: boolean; waitTimeoutMs?: number; }): Promise<{ id: string; state: string; }>; /** POST /:id/start — start a stopped/archived sandbox (also retries a * transiently failed resume). Same wait semantics as .stop(). */ start(opts?: { waitForRunning?: boolean; waitTimeoutMs?: number; }): Promise<{ id: string; state: string; host_id?: string; }>; /** POST /:id/resize — change cpu/ram_mb/disk_gb of a STOPPED sandbox and * cold-boot it at the new size. Disk survives; in-memory state does not. * disk_gb can only grow. */ resize(spec: { cpu?: number; ram_mb?: number; disk_gb?: number; }): Promise<{ id: string; state: string; cpu: number; ram_mb: number; disk_gb: number; host_id?: string; }>; /** POST /:id/clone — boot a NEW sandbox from one of this sandbox's * snapshots (latest when snapshotId is omitted). The source keeps running. */ clone(opts?: { snapshotId?: string; }): Promise; /** GET /:id/snapshots — snapshots taken of this sandbox (newest first). */ listSnapshots(): Promise; /** DELETE /:id/snapshots/:snapId — free the snapshot row + on-host blob. */ deleteSnapshot(snapshotId: string): Promise<{ id: string; deleted: boolean; }>; /** POST /:id/restore — roll THIS sandbox back to a snapshot (latest when * omitted). Returns state:'resuming'; waitRunning() to block until up. */ restore(opts?: { snapshotId?: string; }): Promise<{ id: string; snapshot_id: string; state: string; }>; /** POST /:id/restore-from-backup — rebuild this sandbox from its off-host * backup (409 when no backup exists). */ restoreFromBackup(): Promise<{ id: string; state: string; host_id?: string; prev_host_id?: string | null; }>; /** Run a command. Pass argv (`['ls','-la']`, direct execve) or a plain * string (`'echo hi && ls'`, wrapped in sh -c by the API). */ exec(cmd: string | string[], opts?: { timeoutMs?: number; signal?: AbortSignal; }): Promise; /** Wrap a shell script — sugar for exec(['sh','-c', script]). */ sh(script: string, opts?: { timeoutMs?: number; signal?: AbortSignal; }): Promise; /** POST /v1/sandboxes/:id/run-code — run a snippet. When `lang` is omitted * the sandbox's create-time `language` applies (server default: python). */ runCode(code: string, opts?: { lang?: ExecLang; timeoutMs?: number; signal?: AbortSignal; }): Promise; /** POST /:id/expose — publish a port through the edge proxy. Default is a * private HMAC-token URL; `public: true` skips the token. `ttlSeconds` * bounds how long the signed URL stays valid. */ expose(port: number, opts?: { public?: boolean; ttlSeconds?: number; }): Promise; unexpose(port: number): Promise; /** POST /:id/expose/revoke — kill ONE leaked preview token without tearing * down the port (other clients' tokens keep working). */ revokeExposeToken(token: string, reason?: string): Promise<{ ok: boolean; }>; /** POST /:id/egress-policy — outbound network policy (nftables on the host). * mode names the LIST's role, not the default verdict: * 'allow' → listed CIDRs are BLOCKED, everything else allowed (deny-list); * 'deny' → listed CIDRs are ALLOWED, everything else blocked (allow-list). * Kill all egress: {mode:'deny', cidrs:[]} or {mode:'allow', cidrs:['0.0.0.0/0']}. */ setEgressPolicy(policy: { mode: 'allow' | 'deny'; cidrs: string[]; }): Promise<{ ok: boolean; mode: string; cidrs: string[]; }>; /** POST /:id/network — replace this sandbox's egress policy in place, no * restart. * * REPLACE, not merge: the argument is the complete new policy, and * `updateNetwork({})` clears every rule. Merging would make "lock this down" * depend on whatever was set before, which is the wrong default for a * security control. * * await sbx.updateNetwork({ allowInternetAccess: false }); * await sbx.updateNetwork({ * denyOut: ({ allTraffic }) => [allTraffic], * allowOut: ['api.example.com'], * }); */ updateNetwork(net?: NetworkConfig): Promise; /** GET /:id/network — the policy currently in force. */ getNetwork(): Promise; /** POST /:id/ssh-keys — replace /root/.ssh/authorized_keys with these keys. */ addSshKeys(keys: string[]): Promise<{ ok: boolean; count: number; }>; readonly secrets: { /** GET /:id/secrets — what this box can spend, and whether it is live yet. * * Descriptors only: no value, no placeholder, not even a redaction marker * standing in for one. Readable with a `sandbox:` key, so an agent * INSIDE the box can find out whether its credential is armed and for * which host — the alternative is debugging a 503 with no information. * * const s = await sbx.secrets.list(); * if (s.state !== 'armed') … // 'arming' → retry; 'unavailable' → fix something */ list: () => Promise; /** PUT /:id/secrets — make the attached set be EXACTLY this. * * Declarative, so re-sending an unchanged set is genuinely a no-op: the * diff is computed on (secret, alias, header) server-side, in one * transaction, and an entry that did not change keeps its row rather than * being re-minted — which would disarm and re-arm every credential on the * box on every deploy. `set([])` detaches everything. * * await sbx.secrets.set(['search-api', { secret: 'openai', header: 'authorization' }]); * * Needs `secrets:use`; a `sandbox:` key cannot do this, which is what * stops a box arming itself. */ set: (secrets: readonly SecretAttach[]) => Promise; /** DELETE /:id/secrets/:alias — drop one, by the key you gave it. * * CHECK `.warning` ON THE RESULT, exactly as with `secrets.unbind()`: the * revoke always lands, but a host that refuses the re-pushed egress policy * leaves this sandbox's requests to that hostname failing CLOSED. */ detach: (alias: string) => Promise; }; /** GET /:id/metrics — allocated resources + live cpu/mem/disk utilisation * (live values are null until the first heartbeat sample, ~30 s after start). */ metrics(): Promise; /** GET /:id/usage — cumulative billed usage (cpu-hours, ram-gb-hours, cost) * plus live utilisation; `sinceMs` bounds the window (epoch millis). */ usage(opts?: { sinceMs?: number; }): Promise>; readonly files: { read: (path: string, opts?: { timeoutMs?: number; signal?: AbortSignal; }) => Promise; /** GET /:id/files (ranged) — read only the byte window [offset, offset+length) * instead of the whole file. Returns the bytes, the file's total size, and * whether the window reached EOF. `read()` (whole file) is unchanged; use this * to page through large files without transferring them in full. */ readRange: (path: string, opts: { offset: number; length: number; timeoutMs?: number; signal?: AbortSignal; }) => Promise<{ data: Uint8Array; totalBytes: number; eof: boolean; }>; /** PUT /:id/files — write `body` to `path`. * * `atomic: true` goes to a DIFFERENT endpoint (`PUT /:id/files/atomic`), * where the guest streams the bytes into a sibling temp file, fsyncs it and * renames it over the destination. A concurrent reader then sees the whole * old file or the whole new one, never the zero-length window a plain write * opens, and two writers to one path cannot tear each other's bytes. * * The separate endpoint IS the guarantee, not an implementation detail: a * query flag would be ignored by a control plane that predates it, which * would then do the in-place write and answer 200 — and nothing the caller * checked afterwards could undo it. A missing route is a 404, surfaced here * as `AtomicReplaceUnsupportedError`. A control plane that HAS the route * but a sandbox whose in-VM agent predates the guest verb throws * `ConflictError` with code `atomic_write_unsupported` — rebuild that * template. * * It is not the default because it swaps the inode: a symlink at `path` * becomes a regular file, hard links keep the old bytes, and the old * file's ownership and mode are not inherited. */ write: (path: string, body: Uint8Array | string | ArrayBuffer | Blob, opts?: { atomic?: boolean; timeoutMs?: number; signal?: AbortSignal; }) => Promise; /** `write(path, body, { atomic: true })`, named for what it guarantees. * * Not to be confused with `replace(path, find, replace)` below, which is an * in-place `sed -E` find/replace across many files. */ replaceFile: (path: string, body: Uint8Array | string | ArrayBuffer | Blob, opts?: { timeoutMs?: number; signal?: AbortSignal; }) => Promise; delete: (path: string, opts?: { recurse?: boolean; }) => Promise; /** stat() that answers false instead of throwing NotFoundError. */ exists: (path: string) => Promise; /** GET /:id/files/find — filenames under `path` matching a glob * (whatever `find -name` accepts). Returns matched paths. */ find: (path: string, pattern: string, opts?: { max?: number; }) => Promise; /** GET /:id/files/grep — recursive content search. Returns one * `file:line:content` string per match. */ grep: (path: string, pattern: string, opts?: { max?: number; }) => Promise; /** POST /:id/files/replace — in-place sed -E find/replace across files * under `path` (glob restricts which files, default '*'). */ replace: (path: string, find: string, replace: string, opts?: { glob?: string; }) => Promise<{ ok: boolean; stdout: string; stderr: string; }>; /** GET /:id/files/watch — stream file-change events under `path` as an * async iterator. Yields {event, data} where event is * 'ready' | 'change' | 'heartbeat'; 'change' data is * {ts, type: 'file'|'dir'|'link'|'other', path}. Ends after * maxSeconds (default 300). Break out of the loop to stop early. * * for await (const ev of sbx.files.watch('/workspace', { maxSeconds: 60 })) { * if (ev.event === 'change') console.log(ev.data.path); * } */ watch: (path: string, opts?: { intervalMs?: number; maxSeconds?: number; signal?: AbortSignal; }) => AsyncGenerator; list: (path: string) => Promise>; stat: (path: string) => Promise<{ size: number; mode: number; mtime?: number; is_dir?: boolean; ok?: boolean; exit_code?: number; }>; mkdir: (path: string) => Promise; }; /** SSE reader behind files.watch(). */ private _watchFiles; } export { Template } from './template.cjs'; export type { BuildStep, BuildOpts, BuildResult } from './template.cjs'; export type { components as Schemas, paths as ApiPaths, operations as ApiOperations } from './generated/schema.cjs'; export { manifestFromWrangler, parseJsonc } from './workers.cjs'; export type { WranglerConfig, WorkerManifestInput } from './workers.cjs'; export type Worker = { name: string; script_name: string | null; version: string | null; prefix?: string | null; cells: number | Array<{ id: string; state: string; }>; }; export type WorkerVersion = { name: string; script_name: string; version: string; prefix: string; bytes: number; }; export type WorkerActivation = { name: string; script_name: string; version: string; cells: Array<{ id: string; state: string; }>; restart_required: boolean; };