@platinum-dev/sdk 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kortix AI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # @platinum-dev/sdk
2
+
3
+ Typed TypeScript client for [Platinum](https://github.com/kortix-ai/platinum) —
4
+ hardware-isolated sandbox microVMs (one Cloud Hypervisor VM per sandbox, sub-second
5
+ boots via a warm pool).
6
+
7
+ > **Not on npm yet.** Until the first publish, install from source:
8
+ > `git clone` the repo and import `packages/sdk-ts/src/index.ts` directly
9
+ > (Bun and ts-aware bundlers consume it as-is), or `bun run build` and import `dist/`.
10
+
11
+ ## Quickstart
12
+
13
+ ```ts
14
+ import { Platinum } from '@platinum-dev/sdk';
15
+
16
+ const dn = new Platinum({
17
+ token: process.env.PT_TOKEN!, // API key (pt_live_…) — mint in the dashboard
18
+ url: process.env.PT_API_URL, // e.g. https://api.platinum.dev
19
+ });
20
+
21
+ // Create → running in one round-trip (server holds the POST until ready).
22
+ const sbx = await dn.sandboxes.create(
23
+ { template: 'pt-base', env: { GREETING: 'hi' } },
24
+ { waitForRunning: true },
25
+ );
26
+
27
+ const r = await sbx.exec(['uname', '-a']);
28
+ console.log(r.exit_code, r.stdout);
29
+
30
+ await sbx.delete();
31
+ ```
32
+
33
+ Expose a port in the same call (no second round-trip):
34
+
35
+ ```ts
36
+ const sbx = await dn.sandboxes.create(
37
+ { template: 'pt-base', expose: [{ port: 8000, public: true }] },
38
+ { waitForRunning: true },
39
+ );
40
+ console.log(sbx.exposedUrl(8000)); // reachable preview URL
41
+ ```
42
+
43
+ Build a custom image inline (cache-hit on repeat, built on first use):
44
+
45
+ ```ts
46
+ import { Template, Platinum } from '@platinum-dev/sdk';
47
+
48
+ const image = Template.fromPythonImage('3.12-slim').pipInstall(['fastapi', 'uvicorn']).workdir('/app');
49
+ const sbx = await dn.sandboxes.create({ image }, { waitForRunning: true, waitTimeoutMs: 600_000 });
50
+ ```
51
+
52
+ ## Configuration
53
+
54
+ | Option / env | Default | Meaning |
55
+ |---|---|---|
56
+ | `token` / `PT_TOKEN` | — (required) | API key `pt_live_…`, org-scoped bearer token |
57
+ | `url` / `PT_API_URL` | `http://127.0.0.1:3000` | Control-plane URL |
58
+ | `timeoutMs` | `60000` | Per-request timeout (file transfer calls override per call) |
59
+
60
+ Server-side runtimes only (Node ≥ 20, Bun) — never ship an API key to a browser.
61
+
62
+ ## What's wrapped today
63
+
64
+ | Area | Methods |
65
+ |---|---|
66
+ | Sandboxes | `create` (inline image, inline expose, server-side wait) · `get` · `list` · `delete` · `rename` |
67
+ | Lifecycle | `pause` · `resume` · `fork` · `snapshot` · `archive` · `backup` · `waitRunning` · `refresh` |
68
+ | Execution | `exec(argv)` · `sh(script)` · `runCode(code, {lang})` — python/bash/node/sh |
69
+ | Networking | `expose(port)` · `unexpose(port)` · `exposedUrl(port)` |
70
+ | Files (vsock) | `files.read/write/delete/list/stat/mkdir` — small files anywhere in the guest FS |
71
+ | Share (virtio-fs) | `share.get/put/list/stat/mkdir/delete/info` — bulk transfer, works while paused |
72
+ | Platform | `templates.list` · `Template` builder · `regions.list` · `me()` · `health.check` |
73
+
74
+ Not wrapped yet (REST endpoints exist; wrappers are on the roadmap): `stop`/`start`,
75
+ `clone`, `restore`, snapshot listing/deletion, `resize`, `migrate`, ssh-keys,
76
+ egress policy, `files find/grep/replace/watch`, metrics, usage, interactive
77
+ terminal (WebSocket), volumes, API-key management, webhooks, audit logs.
78
+
79
+ ## Limits worth knowing (honest edition)
80
+
81
+ - **`files.write` bodies must stay ≤ 16 MiB** — larger writes currently truncate and
82
+ fail through the vsock path. Use `share.put` for anything big (verified fine to
83
+ hundreds of MiB) and for bulk trees.
84
+ - **`exec`/`runCode` are buffered**, not streaming: output arrives after the command
85
+ exits. Default timeout 30 s — pass `timeoutMs` for longer runs. `runCode` source is
86
+ capped at 1 MiB.
87
+ - **The default `pt-base` template is busybox-based** — no git, pip, node, or even the
88
+ `httpd` applet inside (`nc` and `wget` are available). Use an inline `image`/template
89
+ built from a real distro image when you need tooling.
90
+ - **Background processes are reaped when their `exec` call returns.** To leave a server
91
+ running (e.g. before hitting an exposed port), daemonize it:
92
+ `setsid sh -c '<server loop>' >/dev/null 2>&1 < /dev/null &` — see
93
+ [`examples/03-expose-service.ts`](./examples/03-expose-service.ts).
94
+ - Errors throw `PlatinumError` with `.status` (HTTP status; `0` for non-HTTP failures —
95
+ a finer-grained error taxonomy is planned pre-1.0).
96
+
97
+ ## Examples
98
+
99
+ Runnable scripts in [`examples/`](./examples): create→exec→delete, files vs share
100
+ (including the 16 MiB boundary), expose + fetch a live service.
101
+
102
+ ```sh
103
+ PT_API_URL=… PT_TOKEN=… bun examples/01-create-exec-delete.ts
104
+ ```
105
+
106
+ ## License
107
+
108
+ MIT
@@ -0,0 +1,340 @@
1
+ export interface PlatinumOptions {
2
+ apiUrl?: string;
3
+ url?: string;
4
+ token: string;
5
+ fetch?: typeof fetch;
6
+ timeoutMs?: number;
7
+ }
8
+ export type SandboxType = 'persistent' | 'ephemeral';
9
+ export type ExecLang = 'python' | 'bash' | 'node' | 'sh';
10
+ /** Inline declarative image spec — alternative to `template`. Mirror of
11
+ * `POST /v1/templates/from-spec` body, minus `name`/`version` (Platinum
12
+ * derives `inline-<hash>` deterministically). Identical specs across any
13
+ * caller dedupe to one cached template. */
14
+ export interface SandboxImageSpec {
15
+ base_image: string;
16
+ steps?: Array<Record<string, unknown>>;
17
+ entrypoint?: string;
18
+ ready_cmd?: string;
19
+ default_cpu?: number;
20
+ default_ram_mb?: number;
21
+ default_disk_gb?: number;
22
+ size_mb?: number;
23
+ }
24
+ export interface SandboxSpec {
25
+ /** Optional human-friendly label. If omitted, the sandbox id is used as the
26
+ * display name. Must be unique among your org's active sandboxes (reusable
27
+ * once a sandbox is destroyed); a clash returns 409. */
28
+ name?: string;
29
+ /** Pre-built template — name or `tpl_<ULID>`. Mutually exclusive with `image`. */
30
+ template?: string;
31
+ /** Inline image — either a `Template` builder instance OR a raw `SandboxImageSpec`.
32
+ * Platinum hashes the spec, cache-hits a prior build or materializes a new
33
+ * template on-demand, then boots the sandbox in one call. With
34
+ * `waitForRunning: true`, the create resolves when the VM is ready. */
35
+ image?: SandboxImageSpec | {
36
+ toImageSpec(): Record<string, unknown>;
37
+ };
38
+ /** persistent (default) | ephemeral (auto-stop 15m, archive 7d, delete 30d). */
39
+ type?: SandboxType;
40
+ /** Pin to a specific region (must have at least one ready host). */
41
+ region?: string;
42
+ cpu?: number;
43
+ ram_mb?: number;
44
+ disk_gb?: number;
45
+ gpus?: number;
46
+ /** Env vars — written to /etc/environment on boot AND set on the running guest. */
47
+ env?: Record<string, string>;
48
+ /** Alias of `env` for E2B compatibility. Both are merged. */
49
+ envVars?: Record<string, string>;
50
+ /** Pre-existing volume IDs to attach at create-time. */
51
+ volume_ids?: string[];
52
+ /** OpenSSH public keys, one per line — written to /root/.ssh/authorized_keys. */
53
+ ssh_keys?: string[];
54
+ /** Default language for `runCode` calls. */
55
+ language?: ExecLang;
56
+ /** Auto-lifecycle overrides — 0 = disabled. Defaults set by `type`. */
57
+ auto_stop_minutes?: number;
58
+ auto_archive_days?: number;
59
+ auto_delete_days?: number;
60
+ /** Arbitrary metadata — surfaced on GET /v1/sandboxes/:id. */
61
+ metadata?: Record<string, unknown>;
62
+ /** Inline expose: same as POST /v1/sandboxes/:id/expose but folded into
63
+ * the create response, saving one round-trip. With `waitForRunning`,
64
+ * the spawn → reachable URL is a single Atlantic RTT. Max 8 entries. */
65
+ expose?: Array<{
66
+ port: number;
67
+ public?: boolean;
68
+ ttl_seconds?: number;
69
+ }>;
70
+ }
71
+ export interface SandboxState {
72
+ id: string;
73
+ /** Display label, or null when none was set (fall back to `id`). */
74
+ name: string | null;
75
+ orgId: string;
76
+ templateId: string;
77
+ hostId: string | null;
78
+ state: 'pending' | 'provisioning' | 'starting' | 'running' | 'stopping' | 'stopped' | 'resuming' | 'archived' | 'failed-provision' | 'failed-start' | 'lost' | 'deleted';
79
+ type?: SandboxType;
80
+ region: string | null;
81
+ cpu: number;
82
+ ramMb: number;
83
+ diskGb: number;
84
+ vsockCid: number | null;
85
+ internalIp: string | null;
86
+ exposedPorts: number[];
87
+ metadata: Record<string, unknown>;
88
+ errorMessage: string | null;
89
+ autoStopMinutes?: number;
90
+ autoArchiveDays?: number;
91
+ autoDeleteDays?: number;
92
+ lastActivityAt?: string | null;
93
+ createdAt: string;
94
+ startedAt: string | null;
95
+ stoppedAt: string | null;
96
+ deletedAt: string | null;
97
+ }
98
+ export interface ExecResult {
99
+ stdout: string;
100
+ stderr: string;
101
+ exit_code: number;
102
+ error?: string;
103
+ }
104
+ export interface ExposeResult {
105
+ url: string;
106
+ port: number;
107
+ sandbox_id: string;
108
+ token?: string;
109
+ public: boolean;
110
+ }
111
+ export interface SnapshotResult {
112
+ id: string;
113
+ size_bytes: number;
114
+ took_ms: number;
115
+ signed?: boolean;
116
+ }
117
+ export interface ForkResult {
118
+ id: string;
119
+ parent_id: string;
120
+ state: string;
121
+ }
122
+ export interface ShareEntry {
123
+ name: string;
124
+ size: number;
125
+ is_dir: boolean;
126
+ mode: string;
127
+ mtime_ms: number;
128
+ }
129
+ export interface ShareListResult {
130
+ entries: ShareEntry[];
131
+ path: string;
132
+ }
133
+ export interface ShareStatResult {
134
+ size: number;
135
+ is_dir: boolean;
136
+ mode: string;
137
+ mtime_ms: number;
138
+ }
139
+ export interface SharePutResult {
140
+ path: string;
141
+ bytes: number;
142
+ }
143
+ export interface ShareInfo {
144
+ guest_mount: string;
145
+ sandbox_id: string;
146
+ fs_type: 'virtio-fs';
147
+ }
148
+ export interface RegionInfo {
149
+ region: string;
150
+ hosts: number;
151
+ }
152
+ declare class PlatinumError extends Error {
153
+ readonly status: number;
154
+ readonly body: unknown;
155
+ constructor(status: number, body: unknown, message: string);
156
+ }
157
+ export { PlatinumError };
158
+ export declare class Platinum {
159
+ readonly apiUrl: string;
160
+ readonly token: string;
161
+ private readonly _fetch;
162
+ readonly timeoutMs: number;
163
+ constructor(opts: PlatinumOptions);
164
+ request<T = unknown>(method: string, path: string, body?: unknown, opts?: {
165
+ timeoutMs?: number;
166
+ rawBody?: Uint8Array | string | ArrayBuffer | Blob;
167
+ rawResponse?: boolean;
168
+ query?: Record<string, string | number | boolean | undefined>;
169
+ }): Promise<T>;
170
+ readonly sandboxes: {
171
+ /**
172
+ * Create a sandbox. Pass `template` (pre-built name/id) or `image`
173
+ * (inline spec — either a `Template` builder or raw `SandboxImageSpec`).
174
+ *
175
+ * With `waitForRunning: true` (or `waitTimeoutMs` set), the server holds
176
+ * the POST open until the sandbox reaches `running` — single Atlantic
177
+ * RTT, no client-side polling. Inline-image creates pay the build cost
178
+ * the first time and cache-hit thereafter, so bump the timeout when you
179
+ * expect a fresh build (default 60s; recommend 600000 for inline).
180
+ */
181
+ create: (spec: SandboxSpec, opts?: {
182
+ waitForRunning?: boolean;
183
+ waitTimeoutMs?: number;
184
+ }) => Promise<Sandbox>;
185
+ get: (id: string) => Promise<SandboxState>;
186
+ list: (opts?: {
187
+ limit?: number;
188
+ offset?: number;
189
+ state?: string;
190
+ }) => Promise<{
191
+ rows: SandboxState[];
192
+ total: number;
193
+ has_more: boolean;
194
+ }>;
195
+ delete: (id: string) => Promise<void>;
196
+ /** Set or clear a sandbox's display label by id. Pass null/'' to clear it.
197
+ * Throws PlatinumError(409) if another active sandbox already uses the name. */
198
+ rename: (id: string, name: string | null) => Promise<SandboxState>;
199
+ };
200
+ readonly templates: {
201
+ list: () => Promise<Array<{
202
+ id: string;
203
+ name: string;
204
+ version: string;
205
+ state: string;
206
+ defaultCpu?: number;
207
+ defaultRamMb?: number;
208
+ defaultDiskGb?: number;
209
+ region?: string | null;
210
+ }>>;
211
+ };
212
+ readonly regions: {
213
+ /** GET /v1/regions — regions with at least one ready host. */
214
+ list: () => Promise<RegionInfo[]>;
215
+ };
216
+ readonly health: {
217
+ check: () => Promise<{
218
+ status: string;
219
+ ts: string;
220
+ }>;
221
+ };
222
+ /** Identity + role of the current bearer. */
223
+ me(): Promise<{
224
+ orgId: string;
225
+ role: 'admin' | 'org';
226
+ userId?: string;
227
+ orgRole?: 'owner' | 'admin' | 'member';
228
+ }>;
229
+ }
230
+ export interface SandboxInit {
231
+ state?: string;
232
+ name?: string | null;
233
+ hostId?: string | null;
234
+ warm?: boolean;
235
+ /** Ports the create() call already exposed via the inline `expose:` field
236
+ * on the spawn request. Skips a follow-up POST /v1/sandboxes/:id/expose. */
237
+ exposed?: Array<{
238
+ port: number;
239
+ url: string;
240
+ token?: string;
241
+ public: boolean;
242
+ }>;
243
+ }
244
+ export declare class Sandbox {
245
+ readonly client: Platinum;
246
+ readonly id: string;
247
+ /** True if the create() returned a warm-pool VM (claim, not spawn). */
248
+ readonly warm: boolean;
249
+ /** Display label set at create (or via .rename()), or null if none. */
250
+ name: string | null;
251
+ /** Host that runs this sandbox — may change on cross-host resume. */
252
+ hostId: string | null;
253
+ /** Ports that came pre-exposed in the create response (inline `expose:`).
254
+ * Populated only when spec.expose was set AND state reached `running`. */
255
+ readonly exposed: ReadonlyArray<{
256
+ port: number;
257
+ url: string;
258
+ token?: string;
259
+ public: boolean;
260
+ }>;
261
+ constructor(client: Platinum, id: string, init?: SandboxInit);
262
+ /** Convenience: URL of a port from the inline expose, or null if it
263
+ * wasn't pre-exposed. For ports added after create, call .expose(). */
264
+ exposedUrl(port: number): string | null;
265
+ refresh(): Promise<SandboxState>;
266
+ /** Block until the sandbox reaches state='running'. */
267
+ waitRunning(opts?: {
268
+ timeoutMs?: number;
269
+ intervalMs?: number;
270
+ }): Promise<SandboxState>;
271
+ delete(): Promise<void>;
272
+ pause(): Promise<void>;
273
+ resume(): Promise<void>;
274
+ fork(): Promise<ForkResult>;
275
+ snapshot(opts?: {
276
+ name?: string;
277
+ }): Promise<SnapshotResult>;
278
+ archive(): Promise<void>;
279
+ /** Set or clear this sandbox's display label. Pass null/'' to clear it (the
280
+ * id is shown again). Throws PlatinumError(409) if another active sandbox in
281
+ * the org already uses the name. Returns the updated sandbox state. */
282
+ rename(name: string | null): Promise<SandboxState>;
283
+ backup(): Promise<{
284
+ id: string;
285
+ backup_state: string;
286
+ }>;
287
+ exec(cmd: string[], opts?: {
288
+ timeoutMs?: number;
289
+ }): Promise<ExecResult>;
290
+ /** Wrap a shell script — sugar for exec(['sh','-c', script]). */
291
+ sh(script: string, opts?: {
292
+ timeoutMs?: number;
293
+ }): Promise<ExecResult>;
294
+ /** POST /v1/sandboxes/:id/run-code — run a snippet in a given language. */
295
+ runCode(code: string, opts?: {
296
+ lang?: ExecLang;
297
+ timeoutMs?: number;
298
+ }): Promise<{
299
+ stdout: string;
300
+ stderr: string;
301
+ exit_code: number;
302
+ }>;
303
+ expose(port: number): Promise<ExposeResult>;
304
+ unexpose(port: number): Promise<void>;
305
+ readonly files: {
306
+ read: (path: string) => Promise<Uint8Array>;
307
+ write: (path: string, body: Uint8Array | string | ArrayBuffer | Blob) => Promise<void>;
308
+ delete: (path: string) => Promise<void>;
309
+ list: (path: string) => Promise<Array<{
310
+ name: string;
311
+ size: number;
312
+ mode: number;
313
+ is_dir: boolean;
314
+ }>>;
315
+ stat: (path: string) => Promise<{
316
+ size: number;
317
+ mode: string;
318
+ mtime_ms: number;
319
+ }>;
320
+ mkdir: (path: string) => Promise<void>;
321
+ };
322
+ readonly share: {
323
+ info: () => Promise<ShareInfo>;
324
+ list: (path?: string) => Promise<ShareListResult>;
325
+ stat: (path: string) => Promise<ShareStatResult>;
326
+ get: (path: string, opts?: {
327
+ timeoutMs?: number;
328
+ }) => Promise<Uint8Array>;
329
+ put: (path: string, body: Uint8Array | string | ArrayBuffer | Blob, opts?: {
330
+ timeoutMs?: number;
331
+ }) => Promise<SharePutResult>;
332
+ mkdir: (path: string) => Promise<void>;
333
+ delete: (path: string, opts?: {
334
+ recursive?: boolean;
335
+ }) => Promise<void>;
336
+ };
337
+ }
338
+ export { Template } from './template';
339
+ export type { BuildStep, BuildOpts, BuildResult } from './template';
340
+ export type { components as Schemas, paths as ApiPaths, operations as ApiOperations } from './generated/schema';
package/dist/index.js ADDED
@@ -0,0 +1,362 @@
1
+ // src/template.ts
2
+ import { readFileSync } from "node:fs";
3
+
4
+ class Template {
5
+ base;
6
+ steps = [];
7
+ _entrypoint;
8
+ _readyCmd;
9
+ constructor(base) {
10
+ this.base = base;
11
+ }
12
+ static fromImage(ref) {
13
+ return new Template(ref);
14
+ }
15
+ static fromPythonImage(version) {
16
+ return new Template(`python:${version}`);
17
+ }
18
+ static fromNodeImage(version) {
19
+ return new Template(`node:${version}`);
20
+ }
21
+ static fromBunImage(version) {
22
+ return new Template(`oven/bun:${version}`);
23
+ }
24
+ static fromUbuntuImage(version) {
25
+ return new Template(`ubuntu:${version}`);
26
+ }
27
+ static fromDebianImage(version) {
28
+ return new Template(`debian:${version}`);
29
+ }
30
+ static fromAlpineImage(version) {
31
+ return new Template(`alpine:${version}`);
32
+ }
33
+ env(key, value) {
34
+ this.steps.push({ op: "env", key, value });
35
+ return this;
36
+ }
37
+ workdir(path) {
38
+ this.steps.push({ op: "workdir", path });
39
+ return this;
40
+ }
41
+ user(user) {
42
+ this.steps.push({ op: "user", user });
43
+ return this;
44
+ }
45
+ runCmd(cmd) {
46
+ this.steps.push({ op: "run", cmd });
47
+ return this;
48
+ }
49
+ pipInstall(packages) {
50
+ this.steps.push({ op: "pip", packages });
51
+ return this;
52
+ }
53
+ npmInstall(packages) {
54
+ this.steps.push({ op: "npm", packages });
55
+ return this;
56
+ }
57
+ aptInstall(packages) {
58
+ this.steps.push({ op: "apt", packages });
59
+ return this;
60
+ }
61
+ copy(src, dst, mode) {
62
+ const raw = typeof src === "string" ? readFileSync(src) : src;
63
+ if (raw.length > 256 * 1024)
64
+ throw new Error("copy(): file too large for inline (>256 KiB). Use a separate file PUT after build.");
65
+ this.steps.push({ op: "copy", content_b64: raw.toString("base64"), dst, mode });
66
+ return this;
67
+ }
68
+ entrypoint(cmd) {
69
+ this._entrypoint = cmd;
70
+ return this;
71
+ }
72
+ readyCmd(cmd) {
73
+ this._readyCmd = cmd;
74
+ return this;
75
+ }
76
+ toSpec(opts) {
77
+ return {
78
+ name: opts.name,
79
+ version: opts.version ?? "1.0.0",
80
+ base_image: this.base,
81
+ steps: this.steps,
82
+ entrypoint: this._entrypoint,
83
+ ready_cmd: this._readyCmd,
84
+ default_cpu: opts.default_cpu ?? 1,
85
+ default_ram_mb: opts.default_ram_mb ?? 512,
86
+ default_disk_gb: opts.default_disk_gb ?? 2,
87
+ size_mb: opts.size_mb ?? 1024
88
+ };
89
+ }
90
+ toImageSpec(opts) {
91
+ return {
92
+ base_image: this.base,
93
+ steps: this.steps,
94
+ entrypoint: this._entrypoint,
95
+ ready_cmd: this._readyCmd,
96
+ default_cpu: opts?.default_cpu,
97
+ default_ram_mb: opts?.default_ram_mb,
98
+ default_disk_gb: opts?.default_disk_gb,
99
+ size_mb: opts?.size_mb
100
+ };
101
+ }
102
+ async build(dn, opts) {
103
+ const r = await fetch(`${dn.url}/v1/templates/from-spec`, {
104
+ method: "POST",
105
+ headers: { Authorization: "Bearer " + dn.token, "Content-Type": "application/json" },
106
+ body: JSON.stringify(this.toSpec(opts))
107
+ });
108
+ const j = await r.json();
109
+ if (!r.ok)
110
+ throw new Error(`build queue: ${r.status} ${j.error || ""}`);
111
+ const id = j.id;
112
+ const waitMs = opts.wait_ms ?? 5 * 60000;
113
+ if (waitMs === 0)
114
+ return { id, name: opts.name, state: "building" };
115
+ const deadline = Date.now() + waitMs;
116
+ while (Date.now() < deadline) {
117
+ const list = await fetch(`${dn.url}/v1/templates`, { headers: { Authorization: "Bearer " + dn.token } });
118
+ const all = await list.json();
119
+ const me = all.find((t) => t.id === id);
120
+ if (me && (me.state === "ready" || me.state === "failed")) {
121
+ return { id, name: opts.name, state: me.state, build_logs: me.buildLogs ?? me.build_logs, content_hash: me.contentHash ?? me.content_hash };
122
+ }
123
+ await new Promise((res) => setTimeout(res, 3000));
124
+ }
125
+ return { id, name: opts.name, state: "building" };
126
+ }
127
+ }
128
+
129
+ // src/index.ts
130
+ class PlatinumError extends Error {
131
+ status;
132
+ body;
133
+ constructor(status, body, message) {
134
+ super(message);
135
+ this.status = status;
136
+ this.body = body;
137
+ this.name = "PlatinumError";
138
+ }
139
+ }
140
+ class Platinum {
141
+ apiUrl;
142
+ token;
143
+ _fetch;
144
+ timeoutMs;
145
+ constructor(opts) {
146
+ this.apiUrl = (opts.apiUrl ?? opts.url ?? (typeof process !== "undefined" ? process.env?.PT_API_URL : "") ?? "http://127.0.0.1:3000").replace(/\/+$/, "");
147
+ this.token = opts.token;
148
+ this._fetch = opts.fetch ?? fetch;
149
+ this.timeoutMs = opts.timeoutMs ?? 60000;
150
+ }
151
+ async request(method, path, body, opts) {
152
+ const ctl = new AbortController;
153
+ const timer = setTimeout(() => ctl.abort(), opts?.timeoutMs ?? this.timeoutMs);
154
+ try {
155
+ const q = opts?.query ? "?" + Object.entries(opts.query).filter(([, v]) => v !== undefined && v !== "").map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join("&") : "";
156
+ const headers = { authorization: `Bearer ${this.token}` };
157
+ let payload;
158
+ if (opts?.rawBody !== undefined) {
159
+ payload = opts.rawBody;
160
+ } else if (body !== undefined) {
161
+ headers["content-type"] = "application/json";
162
+ payload = JSON.stringify(body);
163
+ }
164
+ const res = await this._fetch(this.apiUrl + path + q, { method, headers, body: payload, signal: ctl.signal });
165
+ if (opts?.rawResponse)
166
+ return res;
167
+ const text = await res.text();
168
+ const parsed = text ? safeParse(text) : null;
169
+ if (!res.ok) {
170
+ throw new PlatinumError(res.status, parsed ?? text, `${method} ${path} → ${res.status}`);
171
+ }
172
+ return parsed;
173
+ } finally {
174
+ clearTimeout(timer);
175
+ }
176
+ }
177
+ sandboxes = {
178
+ create: async (spec, opts) => {
179
+ const body = { ...spec };
180
+ if (spec.image && typeof spec.image.toImageSpec === "function") {
181
+ body.image = spec.image.toImageSpec();
182
+ }
183
+ const wait = opts?.waitForRunning || opts?.waitTimeoutMs;
184
+ const query = wait ? { wait_for_state: "running", wait_timeout_ms: opts?.waitTimeoutMs ?? 60000 } : undefined;
185
+ const rpcMs = wait ? (opts?.waitTimeoutMs ?? 60000) + 30000 : this.timeoutMs;
186
+ const created = await this.request("POST", "/v1/sandboxes", body, { query, timeoutMs: rpcMs });
187
+ return new Sandbox(this, created.id, {
188
+ state: created.state,
189
+ name: created.name ?? null,
190
+ hostId: created.host_id,
191
+ warm: created.warm,
192
+ exposed: created.exposed
193
+ });
194
+ },
195
+ get: async (id) => this.request("GET", `/v1/sandboxes/${id}`),
196
+ list: async (opts) => {
197
+ return this.request("GET", "/v1/sandboxes", undefined, {
198
+ query: { paginated: "true", limit: opts?.limit ?? 50, offset: opts?.offset ?? 0, state: opts?.state }
199
+ });
200
+ },
201
+ delete: async (id) => {
202
+ await this.request("DELETE", `/v1/sandboxes/${id}`);
203
+ },
204
+ rename: async (id, name) => this.request("PATCH", `/v1/sandboxes/${id}`, { name })
205
+ };
206
+ templates = {
207
+ list: async () => this.request("GET", "/v1/templates")
208
+ };
209
+ regions = {
210
+ list: async () => this.request("GET", "/v1/regions")
211
+ };
212
+ health = {
213
+ check: async () => this.request("GET", "/health")
214
+ };
215
+ async me() {
216
+ return this.request("GET", "/v1/me");
217
+ }
218
+ }
219
+
220
+ class Sandbox {
221
+ client;
222
+ id;
223
+ warm;
224
+ name;
225
+ hostId;
226
+ exposed;
227
+ constructor(client, id, init = {}) {
228
+ this.client = client;
229
+ this.id = id;
230
+ this.warm = !!init.warm;
231
+ this.name = init.name ?? null;
232
+ this.hostId = init.hostId ?? null;
233
+ this.exposed = init.exposed ?? [];
234
+ }
235
+ exposedUrl(port) {
236
+ return this.exposed.find((e) => e.port === port)?.url ?? null;
237
+ }
238
+ async refresh() {
239
+ const s = await this.client.sandboxes.get(this.id);
240
+ this.hostId = s.hostId;
241
+ this.name = s.name ?? null;
242
+ return s;
243
+ }
244
+ async waitRunning(opts) {
245
+ const timeoutMs = opts?.timeoutMs ?? 30000;
246
+ const intervalMs = opts?.intervalMs ?? 250;
247
+ const deadline = Date.now() + timeoutMs;
248
+ let last;
249
+ while (Date.now() < deadline) {
250
+ last = await this.refresh();
251
+ if (last.state === "running")
252
+ return last;
253
+ if (last.state.startsWith("failed") || last.state === "lost" || last.state === "deleted") {
254
+ throw new PlatinumError(0, last, `sandbox ${this.id} entered terminal state: ${last.state}`);
255
+ }
256
+ await sleep(intervalMs);
257
+ }
258
+ throw new PlatinumError(0, last, `timeout waiting for running (last: ${last.state})`);
259
+ }
260
+ async delete() {
261
+ await this.client.sandboxes.delete(this.id);
262
+ }
263
+ async pause() {
264
+ await this.client.request("POST", `/v1/sandboxes/${this.id}/pause`, {});
265
+ }
266
+ async resume() {
267
+ await this.client.request("POST", `/v1/sandboxes/${this.id}/resume`, {});
268
+ }
269
+ async fork() {
270
+ return this.client.request("POST", `/v1/sandboxes/${this.id}/fork`, {});
271
+ }
272
+ async snapshot(opts) {
273
+ return this.client.request("POST", `/v1/sandboxes/${this.id}/snapshot`, opts ?? {});
274
+ }
275
+ async archive() {
276
+ await this.client.request("POST", `/v1/sandboxes/${this.id}/archive`, {});
277
+ }
278
+ async rename(name) {
279
+ const s = await this.client.request("PATCH", `/v1/sandboxes/${this.id}`, { name });
280
+ this.name = s.name ?? null;
281
+ return s;
282
+ }
283
+ async backup() {
284
+ return this.client.request("POST", `/v1/sandboxes/${this.id}/backup`, {});
285
+ }
286
+ async exec(cmd, opts) {
287
+ const r = await this.client.request("POST", `/v1/sandboxes/${this.id}/exec`, { cmd, timeout_ms: opts?.timeoutMs ?? 30000 }, { timeoutMs: (opts?.timeoutMs ?? 30000) + 5000 });
288
+ if (r.error)
289
+ throw new PlatinumError(0, r, r.error);
290
+ return r.result;
291
+ }
292
+ async sh(script, opts) {
293
+ return this.exec(["sh", "-c", script], opts);
294
+ }
295
+ async runCode(code, opts) {
296
+ return this.client.request("POST", `/v1/sandboxes/${this.id}/run-code`, { code, lang: opts?.lang ?? "python", timeout_ms: opts?.timeoutMs ?? 30000 });
297
+ }
298
+ async expose(port) {
299
+ return this.client.request("POST", `/v1/sandboxes/${this.id}/expose`, { port });
300
+ }
301
+ async unexpose(port) {
302
+ await this.client.request("DELETE", `/v1/sandboxes/${this.id}/expose/${port}`);
303
+ }
304
+ files = {
305
+ read: async (path) => {
306
+ const res = await this.client.request("GET", `/v1/sandboxes/${this.id}/files`, undefined, { query: { path }, rawResponse: true });
307
+ if (!res.ok)
308
+ throw new PlatinumError(res.status, await res.text(), `read ${path}: ${res.status}`);
309
+ return new Uint8Array(await res.arrayBuffer());
310
+ },
311
+ write: async (path, body) => {
312
+ await this.client.request("PUT", `/v1/sandboxes/${this.id}/files`, undefined, { query: { path }, rawBody: body });
313
+ },
314
+ delete: async (path) => {
315
+ await this.client.request("DELETE", `/v1/sandboxes/${this.id}/files`, undefined, { query: { path } });
316
+ },
317
+ list: async (path) => {
318
+ const r = await this.client.request("GET", `/v1/sandboxes/${this.id}/files/list`, undefined, { query: { path } });
319
+ if (r && r.ok === false)
320
+ throw new PlatinumError(0, r, r.error || `files.list ${path} failed`);
321
+ return r?.entries ?? [];
322
+ },
323
+ stat: async (path) => this.client.request("GET", `/v1/sandboxes/${this.id}/files/stat`, undefined, { query: { path } }),
324
+ mkdir: async (path) => {
325
+ await this.client.request("POST", `/v1/sandboxes/${this.id}/files/mkdir`, undefined, { query: { path } });
326
+ }
327
+ };
328
+ share = {
329
+ info: async () => this.client.request("GET", `/v1/sandboxes/${this.id}/share/info`),
330
+ list: async (path = "") => this.client.request("GET", `/v1/sandboxes/${this.id}/share/list`, undefined, { query: { path } }),
331
+ stat: async (path) => this.client.request("GET", `/v1/sandboxes/${this.id}/share/stat`, undefined, { query: { path } }),
332
+ get: async (path, opts) => {
333
+ const res = await this.client.request("GET", `/v1/sandboxes/${this.id}/share/get`, undefined, { query: { path }, rawResponse: true, timeoutMs: opts?.timeoutMs ?? 5 * 60000 });
334
+ if (!res.ok)
335
+ throw new PlatinumError(res.status, await res.text(), `share get ${path}: ${res.status}`);
336
+ return new Uint8Array(await res.arrayBuffer());
337
+ },
338
+ put: async (path, body, opts) => this.client.request("POST", `/v1/sandboxes/${this.id}/share/put`, undefined, { query: { path }, rawBody: body, timeoutMs: opts?.timeoutMs ?? 5 * 60000 }),
339
+ mkdir: async (path) => {
340
+ await this.client.request("POST", `/v1/sandboxes/${this.id}/share/mkdir`, undefined, { query: { path } });
341
+ },
342
+ delete: async (path, opts) => {
343
+ await this.client.request("DELETE", `/v1/sandboxes/${this.id}/share/delete`, undefined, { query: { path, recursive: opts?.recursive ? "1" : undefined } });
344
+ }
345
+ };
346
+ }
347
+ function sleep(ms) {
348
+ return new Promise((r) => setTimeout(r, ms));
349
+ }
350
+ function safeParse(s) {
351
+ try {
352
+ return JSON.parse(s);
353
+ } catch {
354
+ return;
355
+ }
356
+ }
357
+ export {
358
+ Template,
359
+ Sandbox,
360
+ PlatinumError,
361
+ Platinum
362
+ };
@@ -0,0 +1,91 @@
1
+ export type BuildStep = {
2
+ op: 'env';
3
+ key: string;
4
+ value: string;
5
+ } | {
6
+ op: 'workdir';
7
+ path: string;
8
+ } | {
9
+ op: 'user';
10
+ user: string;
11
+ } | {
12
+ op: 'run';
13
+ cmd: string;
14
+ } | {
15
+ op: 'pip';
16
+ packages: string[];
17
+ } | {
18
+ op: 'npm';
19
+ packages: string[];
20
+ } | {
21
+ op: 'apt';
22
+ packages: string[];
23
+ } | {
24
+ op: 'copy';
25
+ content_b64: string;
26
+ dst: string;
27
+ mode?: string;
28
+ };
29
+ export interface BuildOpts {
30
+ name: string;
31
+ version?: string;
32
+ default_cpu?: number;
33
+ default_ram_mb?: number;
34
+ default_disk_gb?: number;
35
+ size_mb?: number;
36
+ /** Polls the template list until state != "building" — set 0 to fire-and-forget. */
37
+ wait_ms?: number;
38
+ }
39
+ export interface BuildResult {
40
+ id: string;
41
+ name: string;
42
+ state: 'building' | 'ready' | 'failed';
43
+ build_logs?: string;
44
+ content_hash?: string;
45
+ }
46
+ interface PlatinumLike {
47
+ url: string;
48
+ token: string;
49
+ }
50
+ export declare class Template {
51
+ private base;
52
+ private steps;
53
+ private _entrypoint?;
54
+ private _readyCmd?;
55
+ private constructor();
56
+ static fromImage(ref: string): Template;
57
+ static fromPythonImage(version: string): Template;
58
+ static fromNodeImage(version: string): Template;
59
+ static fromBunImage(version: string): Template;
60
+ static fromUbuntuImage(version: string): Template;
61
+ static fromDebianImage(version: string): Template;
62
+ static fromAlpineImage(version: string): Template;
63
+ env(key: string, value: string): this;
64
+ workdir(path: string): this;
65
+ user(user: string): this;
66
+ runCmd(cmd: string): this;
67
+ pipInstall(packages: string[]): this;
68
+ npmInstall(packages: string[]): this;
69
+ aptInstall(packages: string[]): this;
70
+ /** Inline copy. For small files only (<256 KiB). Pass a path to read it from disk. */
71
+ copy(src: string | Buffer, dst: string, mode?: string): this;
72
+ entrypoint(cmd: string): this;
73
+ /** Probe that runs inside the guest after entrypoint launches. Sandbox reports ready
74
+ * only when this exits 0. Mirrors E2B's `setReadyCmd`. */
75
+ readyCmd(cmd: string): this;
76
+ toSpec(opts: BuildOpts): Record<string, unknown>;
77
+ /**
78
+ * Inline-image shape for `POST /v1/sandboxes`. Skips the explicit `name`/
79
+ * `version` — Platinum derives a deterministic `inline-<hash>` template
80
+ * name from the hashed spec, so identical specs from any caller dedupe to
81
+ * one row. Pass this directly via `sandboxes.create({ image: tpl, ... })`.
82
+ */
83
+ toImageSpec(opts?: {
84
+ default_cpu?: number;
85
+ default_ram_mb?: number;
86
+ default_disk_gb?: number;
87
+ size_mb?: number;
88
+ }): Record<string, unknown>;
89
+ build(dn: PlatinumLike, opts: BuildOpts): Promise<BuildResult>;
90
+ }
91
+ export {};
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@platinum-dev/sdk",
3
+ "publishConfig": { "access": "public" },
4
+ "version": "0.2.0",
5
+ "description": "Platinum TypeScript SDK — typed client for hardware-isolated sandbox microVMs.",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "dist/index.js",
9
+ "module": "dist/index.js",
10
+ "types": "dist/index.d.ts",
11
+ "files": ["dist", "README.md", "LICENSE"],
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js"
16
+ }
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/kortix-ai/platinum.git",
21
+ "directory": "packages/sdk-ts"
22
+ },
23
+ "scripts": {
24
+ "build": "bun build src/index.ts --outdir dist --target node --format esm && bun x tsc --emitDeclarationOnly --declaration --outDir dist src/index.ts",
25
+ "gen": "openapi-typescript ../../apps/api/openapi.codegen.json -o src/generated/schema.d.ts",
26
+ "test": "bun test"
27
+ },
28
+ "devDependencies": {
29
+ "@types/bun": "latest",
30
+ "openapi-typescript": "7.13.0",
31
+ "typescript": "^5.7.2"
32
+ }
33
+ }