@lizard-build/sdk 0.1.58 → 0.1.60

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/README.md CHANGED
@@ -61,7 +61,7 @@ if (result.exitCode !== 0) throw new Error(result.stderr)
61
61
 
62
62
  ## Desktop (computer use)
63
63
 
64
- The `desktop` template runs XFCE and Chromium you can watch in a browser and an agent can drive:
64
+ The `desktop` template runs an Openbox desktop with Chromium, streamed to a browser over WebRTC (with noVNC as the fallback). You can watch it, and an agent can drive it:
65
65
 
66
66
  ```ts
67
67
  import { Sandbox } from '@lizard-build/sdk'
@@ -108,13 +108,14 @@ const sandbox = await CodeSandbox.create({ projectId: 'proj_123' });
108
108
  try {
109
109
  await sandbox.runCode('x = 21');
110
110
  const run = await sandbox.runCode('x * 2');
111
- console.log(run.results[0].data); // "42"
111
+ const out = run.results[0];
112
+ if (out?.type === 'result') console.log(out.data); // "42"
112
113
  } finally {
113
114
  await sandbox.kill();
114
115
  }
115
116
  ```
116
117
 
117
- `language: 'bash'` (and `'javascript'` where `node` is installed; the `interpreter` template does not ship it) runs each call as a fresh process with no shared state.
118
+ `language: 'bash'` and `language: 'javascript'` (the `interpreter` template ships Node 22) run each call as a fresh process with no shared state.
118
119
 
119
120
  ## Configuration
120
121
 
@@ -124,7 +125,7 @@ Pass `timeoutMs` to `create()` to set the sandbox lifetime. The default is five
124
125
 
125
126
  ## Runtime limits
126
127
 
127
- Each sandbox is a Firecracker microVM with its own kernel. Use volumes mounted at `/workspace` to keep files after a sandbox ends. `pause()`/`resume()` keep memory and running processes; `snapshot()` is ready in about 2 s and `Sandbox.restore()` starts a copy in about 0.4 s; `fork()` clones a running sandbox. File watching and sandbox log streaming are not available on Firecracker sandboxes yet, volumes cannot be resized yet, and a sandbox with a volume attached cannot be forked. See the [snapshot guide](../../docs/sandboxes.md#pause-resume-snapshots-and-fork).
128
+ Each sandbox is a Firecracker microVM with its own kernel. Use volumes mounted at `/workspace` to keep files after a sandbox ends. `pause()`/`resume()` keep memory and running processes; `snapshot()` is ready in about 2 s and `Sandbox.restore()` starts a copy in about 0.4 s; `fork()` clones a running sandbox. File watching and sandbox log streaming are not available on Firecracker sandboxes yet. Volumes can grow but not shrink, and a sandbox with a volume attached cannot be forked. See the [snapshot guide](../../docs/sandboxes.md#pause-resume-snapshots-and-fork).
128
129
 
129
130
  `getHost()` returns a hostname without `https://`; the port is private, so send `sandbox.accessToken` as the `X-Lizard-Access-Token` header (or use `exposePort()`, which also returns a browser `url` carrying the token). File writes accept text or bytes (binary is sent exactly); `readBytes()` supports binary downloads.
130
131
 
package/dist/index.d.mts CHANGED
@@ -23,9 +23,10 @@ declare class PlatformClient {
23
23
  readonly config: ConnectionConfig;
24
24
  constructor(opts: ConnectionOpts);
25
25
  private json;
26
- request<T>(method: string, path: string, body?: unknown): Promise<T>;
26
+ /** `timeoutMs` overrides the connection's requestTimeoutMs for a call known to be slow, never shortening it. */
27
+ request<T>(method: string, path: string, body?: unknown, timeoutMs?: number): Promise<T>;
27
28
  get<T>(path: string): Promise<T>;
28
- post<T>(path: string, body?: unknown): Promise<T>;
29
+ post<T>(path: string, body?: unknown, timeoutMs?: number): Promise<T>;
29
30
  put<T>(path: string, body: unknown): Promise<T>;
30
31
  patch<T>(path: string, body: unknown): Promise<T>;
31
32
  delete<T = void>(path: string, body?: unknown): Promise<T>;
@@ -144,7 +145,10 @@ declare class ProjectsAPI {
144
145
  }): Promise<Project[]>;
145
146
  /** Get a project by ID. */
146
147
  get(id: string): Promise<Project>;
147
- /** Create a new project. */
148
+ /**
149
+ * Create a new project. A scoped key can create one only in a workspace it
150
+ * covers; anywhere else it gets HTTP 403 with `code: 'PROJECT_OUT_OF_SCOPE'`.
151
+ */
148
152
  create(opts: CreateProjectOpts): Promise<Project>;
149
153
  /** Update a project's name. */
150
154
  update(id: string, opts: {
@@ -670,7 +674,7 @@ declare class Watcher {
670
674
  * State of a sandbox's desktop, and the URLs that stream it.
671
675
  *
672
676
  * **Both URLs are credentials.** The port is published on a public hostname, and each
673
- * URL carries the stream token and a VNC password. Anyone holding `url` can see and
677
+ * URL carries the stream token and a password. Anyone holding `url` can see and
674
678
  * control the desktop; anyone holding `viewOnlyUrl` can watch it. Do not log them or
675
679
  * put them anywhere you would not put an API key.
676
680
  */
@@ -679,9 +683,9 @@ interface DesktopInfo {
679
683
  /** Screen width in pixels, or `null` when the desktop has never been started. */
680
684
  width: number | null;
681
685
  height: number | null;
682
- /** noVNC page with full control (mouse and keyboard). Treat like a credential. */
686
+ /** Viewer page with full control (mouse and keyboard): WebRTC, with noVNC as the fallback. Treat like a credential. */
683
687
  url: string;
684
- /** noVNC page that can only watch; enforced by the VNC server, not the page. */
688
+ /** Viewer page that can only watch; enforced by the stream server in the sandbox, not the page. */
685
689
  viewOnlyUrl: string;
686
690
  }
687
691
  interface DesktopStartOpts {
@@ -701,8 +705,9 @@ interface ClickOpts {
701
705
  /**
702
706
  * A graphical desktop in the sandbox, for computer-use agents and for people to watch.
703
707
  *
704
- * Only the `desktop` template has one (Xvfb + XFCE + Chromium on `DISPLAY=:1`); on
705
- * any other template the calls fail with `code === 'DESKTOP_NOT_SUPPORTED'`.
708
+ * Only the `desktop` template has one (Xvfb + Openbox + Chromium on `DISPLAY=:1`,
709
+ * streamed over WebRTC with noVNC as the fallback); on any other template the calls
710
+ * fail with `code === 'DESKTOP_NOT_SUPPORTED'`.
706
711
  *
707
712
  * Input and screenshots run inside the sandbox with `xdotool` and `scrot`. Every
708
713
  * argument goes to the sandbox as an argv array and is never parsed by a shell, so
@@ -791,9 +796,9 @@ interface SandboxInfo {
791
796
  /** The sandbox runtime: `'firecracker'` (a microVM), `'container'`, or `null` when unknown. */
792
797
  runtime?: 'firecracker' | 'container' | null;
793
798
  pauseError?: string | null;
794
- /** Machine size (`'small' | 'medium' | 'large'`); may be `null` for a sandbox created from a private snapshot. */
799
+ /** Machine size (`'small' | 'medium' | 'large'`); may be `null` for a sandbox billed by measured usage, such as one restored from a container snapshot. */
795
800
  size?: 'small' | 'medium' | 'large' | null;
796
- /** Flat hourly price in USD for `size`, billed per second while running; may be `null` when billed by measured usage (private snapshot). */
801
+ /** Flat hourly price in USD for `size`, billed per second while running; may be `null` when billed by measured usage (a container snapshot). */
797
802
  pricePerHour?: number | null;
798
803
  cpus?: number;
799
804
  memoryMb?: number;
@@ -825,8 +830,8 @@ interface SandboxOpts extends ConnectionOpts {
825
830
  projectId?: string;
826
831
  template?: string;
827
832
  /**
828
- * Create from a private saved snapshot. The sandbox runs on the machine the
829
- * snapshot was captured on and is billed by measured usage; `size` is ignored.
833
+ * Create from a private saved snapshot. The sandbox runs at the source sandbox's
834
+ * size and is billed flat at that size; `size` is ignored.
830
835
  */
831
836
  snapshotId?: string;
832
837
  /**
@@ -1153,9 +1158,10 @@ declare class Sandbox extends SandboxClient {
1153
1158
  */
1154
1159
  exposePort(port: number, opts?: ConnectionOpts): Promise<ExposedPort>;
1155
1160
  /**
1156
- * Clone this running sandbox `count` times (1-10). Each fork is a copy of the
1161
+ * Clone this running sandbox `count` times (1-100). Each fork is a copy of the
1157
1162
  * microVM at this instant -- memory, running processes and files -- and is
1158
- * billed like its source. The source keeps running.
1163
+ * billed like its source. The source keeps running. Each copy counts as a
1164
+ * sandbox start, so the account's running-sandbox limit is the real cap.
1159
1165
  *
1160
1166
  * Returns one entry per requested fork, in order: `{ sandbox, info }`, or
1161
1167
  * `{ error }` for a copy that could not be made (e.g. the account's sandbox
@@ -1175,9 +1181,10 @@ declare class Sandbox extends SandboxClient {
1175
1181
  }): Promise<ForkResult<this>[]>;
1176
1182
  /**
1177
1183
  * Save this sandbox -- memory, running processes and files -- as a private
1178
- * snapshot. A Firecracker snapshot is `ready` as soon as this returns (about 2 s);
1179
- * start copies of it with {@link Sandbox.restore} (about 0.4 s each). The source
1180
- * keeps running. `poolSize` applies to container sandboxes only.
1184
+ * snapshot. A Firecracker snapshot is `ready` as soon as this returns: about 2 s, up to
1185
+ * a few minutes for a sandbox that filled most of its memory (its memory is uploaded
1186
+ * first), so the call waits up to 11 minutes. Start copies of it with {@link Sandbox.restore}
1187
+ * (about 0.4 s each). The source keeps running. `poolSize` applies to container sandboxes only.
1181
1188
  */
1182
1189
  snapshot(name?: string, opts?: SnapshotOpts): Promise<SandboxSnapshot>;
1183
1190
  unexpose(port: number): Promise<void>;
@@ -1418,14 +1425,22 @@ declare class WorkspacesAPI {
1418
1425
  * project. A full key returns every workspace the account belongs to.
1419
1426
  */
1420
1427
  list(): Promise<Workspace[]>;
1421
- /** Create a workspace. The caller becomes its owner. */
1428
+ /**
1429
+ * Create a workspace. The caller becomes its owner.
1430
+ *
1431
+ * Needs a key with no scopes: a scoped key gets HTTP 403 with
1432
+ * `code: 'ACCOUNT_SCOPE_REQUIRED'`.
1433
+ */
1422
1434
  create(opts: CreateWorkspaceOpts): Promise<Workspace>;
1423
1435
  /**
1424
1436
  * Delete a workspace.
1425
1437
  *
1426
- * Empty-only by default — the server refuses while any project or sandbox remains,
1427
- * so this cannot quietly destroy a user's work. Pass `{ force: true }` to delete a
1428
- * workspace and everything in it; that is irreversible.
1438
+ * Empty-only by default — the server refuses while any project, sandbox or volume
1439
+ * remains, so this cannot quietly destroy a user's work. `{ force: true }` deletes
1440
+ * the workspace with its apps, addons and projects; that is irreversible. It does not
1441
+ * delete sandboxes, sandbox volumes or snapshots: a running sandbox keeps running with
1442
+ * no workspace left to find it in. Kill the workspace's sandboxes and delete its
1443
+ * volumes and snapshots first.
1429
1444
  */
1430
1445
  delete(id: string, opts?: {
1431
1446
  force?: boolean;
@@ -1840,9 +1855,9 @@ declare class Volume {
1840
1855
  /**
1841
1856
  * Resize a volume, by name or by id, to `sizeGb`.
1842
1857
  *
1843
- * **Firecracker volumes cannot be resized yet**: volumes created for the default
1844
- * Firecracker sandboxes throw `LizardError` (400) with `code: 'volume_not_resizable'`.
1845
- * Create the volume at the size you need, or copy the data to a new, larger one.
1858
+ * A Firecracker volume (the default) can only grow. The change is in place and
1859
+ * online: a sandbox with the volume attached keeps running and sees the new size.
1860
+ * A smaller size throws `LizardError` (400) with `code: 'volume_shrink_unsupported'`.
1846
1861
  *
1847
1862
  * On a container volume the change is in place and online: the size is a quota,
1848
1863
  * so no data is copied and a mounted sandbox sees the new size immediately. Both
@@ -1850,16 +1865,17 @@ declare class Volume {
1850
1865
  * new size free — otherwise it throws `ConflictError` with
1851
1866
  * `code: 'volume_too_full_to_shrink'`.
1852
1867
  *
1853
- * Other failures carry a `code` too: `invalid_volume_size`, `volume_not_resizable`,
1868
+ * Other failures carry a `code` too: `invalid_volume_size`, `volume_too_large`,
1869
+ * `volume_not_resizable` (an app's volume, not a sandbox volume),
1854
1870
  * `volume_resize_in_progress`, `volume_not_provisioned`,
1855
1871
  * `volume_capacity_unavailable`, and `volume_resize_timeout` (a `TimeoutError`;
1856
1872
  * the size is left unchanged).
1857
1873
  *
1858
- * @returns The volume's updated info, including `sizeEnforced`.
1874
+ * @returns The volume's updated info, including `sizeEnforced` for a container volume.
1859
1875
  */
1860
1876
  static resize(projectId: string, nameOrId: string, sizeGb: number, opts?: ConnectionOpts): Promise<VolumeInfo>;
1861
1877
  getInfo(projectId: string): Promise<VolumeInfo>;
1862
- /** Resize this volume — see the static {@link Volume.resize} (not supported for Firecracker volumes yet). */
1878
+ /** Resize this volume — see the static {@link Volume.resize} (a Firecracker volume can only grow). */
1863
1879
  resize(projectId: string, sizeGb: number): Promise<VolumeInfo>;
1864
1880
  delete(projectId: string): Promise<void>;
1865
1881
  }
@@ -2152,7 +2168,7 @@ interface CreateContextOpts {
2152
2168
  * Limits, honestly:
2153
2169
  * - `javascript` (node) and `bash` run each call as a fresh process: no state
2154
2170
  * carries over and `results` stays empty. `node` must be installed in the
2155
- * template; the `interpreter` template does not ship it.
2171
+ * template; the `interpreter` template ships Node 22.
2156
2172
  * - On a template without Jupyter (e.g. `base`), Python also runs as a fresh
2157
2173
  * process per call.
2158
2174
  * - `stdout`/`stderr` are delivered in the chunks the kernel or process
@@ -2166,7 +2182,8 @@ interface CreateContextOpts {
2166
2182
  * const sandbox = await CodeSandbox.create({ project: 'my-project' })
2167
2183
  * await sandbox.runCode('x = 21')
2168
2184
  * const run = await sandbox.runCode('x * 2')
2169
- * console.log(run.results[0].data) // "42"
2185
+ * const out = run.results[0]
2186
+ * if (out?.type === 'result') console.log(out.data) // "42"
2170
2187
  * await sandbox.kill()
2171
2188
  * ```
2172
2189
  */
package/dist/index.d.ts CHANGED
@@ -23,9 +23,10 @@ declare class PlatformClient {
23
23
  readonly config: ConnectionConfig;
24
24
  constructor(opts: ConnectionOpts);
25
25
  private json;
26
- request<T>(method: string, path: string, body?: unknown): Promise<T>;
26
+ /** `timeoutMs` overrides the connection's requestTimeoutMs for a call known to be slow, never shortening it. */
27
+ request<T>(method: string, path: string, body?: unknown, timeoutMs?: number): Promise<T>;
27
28
  get<T>(path: string): Promise<T>;
28
- post<T>(path: string, body?: unknown): Promise<T>;
29
+ post<T>(path: string, body?: unknown, timeoutMs?: number): Promise<T>;
29
30
  put<T>(path: string, body: unknown): Promise<T>;
30
31
  patch<T>(path: string, body: unknown): Promise<T>;
31
32
  delete<T = void>(path: string, body?: unknown): Promise<T>;
@@ -144,7 +145,10 @@ declare class ProjectsAPI {
144
145
  }): Promise<Project[]>;
145
146
  /** Get a project by ID. */
146
147
  get(id: string): Promise<Project>;
147
- /** Create a new project. */
148
+ /**
149
+ * Create a new project. A scoped key can create one only in a workspace it
150
+ * covers; anywhere else it gets HTTP 403 with `code: 'PROJECT_OUT_OF_SCOPE'`.
151
+ */
148
152
  create(opts: CreateProjectOpts): Promise<Project>;
149
153
  /** Update a project's name. */
150
154
  update(id: string, opts: {
@@ -670,7 +674,7 @@ declare class Watcher {
670
674
  * State of a sandbox's desktop, and the URLs that stream it.
671
675
  *
672
676
  * **Both URLs are credentials.** The port is published on a public hostname, and each
673
- * URL carries the stream token and a VNC password. Anyone holding `url` can see and
677
+ * URL carries the stream token and a password. Anyone holding `url` can see and
674
678
  * control the desktop; anyone holding `viewOnlyUrl` can watch it. Do not log them or
675
679
  * put them anywhere you would not put an API key.
676
680
  */
@@ -679,9 +683,9 @@ interface DesktopInfo {
679
683
  /** Screen width in pixels, or `null` when the desktop has never been started. */
680
684
  width: number | null;
681
685
  height: number | null;
682
- /** noVNC page with full control (mouse and keyboard). Treat like a credential. */
686
+ /** Viewer page with full control (mouse and keyboard): WebRTC, with noVNC as the fallback. Treat like a credential. */
683
687
  url: string;
684
- /** noVNC page that can only watch; enforced by the VNC server, not the page. */
688
+ /** Viewer page that can only watch; enforced by the stream server in the sandbox, not the page. */
685
689
  viewOnlyUrl: string;
686
690
  }
687
691
  interface DesktopStartOpts {
@@ -701,8 +705,9 @@ interface ClickOpts {
701
705
  /**
702
706
  * A graphical desktop in the sandbox, for computer-use agents and for people to watch.
703
707
  *
704
- * Only the `desktop` template has one (Xvfb + XFCE + Chromium on `DISPLAY=:1`); on
705
- * any other template the calls fail with `code === 'DESKTOP_NOT_SUPPORTED'`.
708
+ * Only the `desktop` template has one (Xvfb + Openbox + Chromium on `DISPLAY=:1`,
709
+ * streamed over WebRTC with noVNC as the fallback); on any other template the calls
710
+ * fail with `code === 'DESKTOP_NOT_SUPPORTED'`.
706
711
  *
707
712
  * Input and screenshots run inside the sandbox with `xdotool` and `scrot`. Every
708
713
  * argument goes to the sandbox as an argv array and is never parsed by a shell, so
@@ -791,9 +796,9 @@ interface SandboxInfo {
791
796
  /** The sandbox runtime: `'firecracker'` (a microVM), `'container'`, or `null` when unknown. */
792
797
  runtime?: 'firecracker' | 'container' | null;
793
798
  pauseError?: string | null;
794
- /** Machine size (`'small' | 'medium' | 'large'`); may be `null` for a sandbox created from a private snapshot. */
799
+ /** Machine size (`'small' | 'medium' | 'large'`); may be `null` for a sandbox billed by measured usage, such as one restored from a container snapshot. */
795
800
  size?: 'small' | 'medium' | 'large' | null;
796
- /** Flat hourly price in USD for `size`, billed per second while running; may be `null` when billed by measured usage (private snapshot). */
801
+ /** Flat hourly price in USD for `size`, billed per second while running; may be `null` when billed by measured usage (a container snapshot). */
797
802
  pricePerHour?: number | null;
798
803
  cpus?: number;
799
804
  memoryMb?: number;
@@ -825,8 +830,8 @@ interface SandboxOpts extends ConnectionOpts {
825
830
  projectId?: string;
826
831
  template?: string;
827
832
  /**
828
- * Create from a private saved snapshot. The sandbox runs on the machine the
829
- * snapshot was captured on and is billed by measured usage; `size` is ignored.
833
+ * Create from a private saved snapshot. The sandbox runs at the source sandbox's
834
+ * size and is billed flat at that size; `size` is ignored.
830
835
  */
831
836
  snapshotId?: string;
832
837
  /**
@@ -1153,9 +1158,10 @@ declare class Sandbox extends SandboxClient {
1153
1158
  */
1154
1159
  exposePort(port: number, opts?: ConnectionOpts): Promise<ExposedPort>;
1155
1160
  /**
1156
- * Clone this running sandbox `count` times (1-10). Each fork is a copy of the
1161
+ * Clone this running sandbox `count` times (1-100). Each fork is a copy of the
1157
1162
  * microVM at this instant -- memory, running processes and files -- and is
1158
- * billed like its source. The source keeps running.
1163
+ * billed like its source. The source keeps running. Each copy counts as a
1164
+ * sandbox start, so the account's running-sandbox limit is the real cap.
1159
1165
  *
1160
1166
  * Returns one entry per requested fork, in order: `{ sandbox, info }`, or
1161
1167
  * `{ error }` for a copy that could not be made (e.g. the account's sandbox
@@ -1175,9 +1181,10 @@ declare class Sandbox extends SandboxClient {
1175
1181
  }): Promise<ForkResult<this>[]>;
1176
1182
  /**
1177
1183
  * Save this sandbox -- memory, running processes and files -- as a private
1178
- * snapshot. A Firecracker snapshot is `ready` as soon as this returns (about 2 s);
1179
- * start copies of it with {@link Sandbox.restore} (about 0.4 s each). The source
1180
- * keeps running. `poolSize` applies to container sandboxes only.
1184
+ * snapshot. A Firecracker snapshot is `ready` as soon as this returns: about 2 s, up to
1185
+ * a few minutes for a sandbox that filled most of its memory (its memory is uploaded
1186
+ * first), so the call waits up to 11 minutes. Start copies of it with {@link Sandbox.restore}
1187
+ * (about 0.4 s each). The source keeps running. `poolSize` applies to container sandboxes only.
1181
1188
  */
1182
1189
  snapshot(name?: string, opts?: SnapshotOpts): Promise<SandboxSnapshot>;
1183
1190
  unexpose(port: number): Promise<void>;
@@ -1418,14 +1425,22 @@ declare class WorkspacesAPI {
1418
1425
  * project. A full key returns every workspace the account belongs to.
1419
1426
  */
1420
1427
  list(): Promise<Workspace[]>;
1421
- /** Create a workspace. The caller becomes its owner. */
1428
+ /**
1429
+ * Create a workspace. The caller becomes its owner.
1430
+ *
1431
+ * Needs a key with no scopes: a scoped key gets HTTP 403 with
1432
+ * `code: 'ACCOUNT_SCOPE_REQUIRED'`.
1433
+ */
1422
1434
  create(opts: CreateWorkspaceOpts): Promise<Workspace>;
1423
1435
  /**
1424
1436
  * Delete a workspace.
1425
1437
  *
1426
- * Empty-only by default — the server refuses while any project or sandbox remains,
1427
- * so this cannot quietly destroy a user's work. Pass `{ force: true }` to delete a
1428
- * workspace and everything in it; that is irreversible.
1438
+ * Empty-only by default — the server refuses while any project, sandbox or volume
1439
+ * remains, so this cannot quietly destroy a user's work. `{ force: true }` deletes
1440
+ * the workspace with its apps, addons and projects; that is irreversible. It does not
1441
+ * delete sandboxes, sandbox volumes or snapshots: a running sandbox keeps running with
1442
+ * no workspace left to find it in. Kill the workspace's sandboxes and delete its
1443
+ * volumes and snapshots first.
1429
1444
  */
1430
1445
  delete(id: string, opts?: {
1431
1446
  force?: boolean;
@@ -1840,9 +1855,9 @@ declare class Volume {
1840
1855
  /**
1841
1856
  * Resize a volume, by name or by id, to `sizeGb`.
1842
1857
  *
1843
- * **Firecracker volumes cannot be resized yet**: volumes created for the default
1844
- * Firecracker sandboxes throw `LizardError` (400) with `code: 'volume_not_resizable'`.
1845
- * Create the volume at the size you need, or copy the data to a new, larger one.
1858
+ * A Firecracker volume (the default) can only grow. The change is in place and
1859
+ * online: a sandbox with the volume attached keeps running and sees the new size.
1860
+ * A smaller size throws `LizardError` (400) with `code: 'volume_shrink_unsupported'`.
1846
1861
  *
1847
1862
  * On a container volume the change is in place and online: the size is a quota,
1848
1863
  * so no data is copied and a mounted sandbox sees the new size immediately. Both
@@ -1850,16 +1865,17 @@ declare class Volume {
1850
1865
  * new size free — otherwise it throws `ConflictError` with
1851
1866
  * `code: 'volume_too_full_to_shrink'`.
1852
1867
  *
1853
- * Other failures carry a `code` too: `invalid_volume_size`, `volume_not_resizable`,
1868
+ * Other failures carry a `code` too: `invalid_volume_size`, `volume_too_large`,
1869
+ * `volume_not_resizable` (an app's volume, not a sandbox volume),
1854
1870
  * `volume_resize_in_progress`, `volume_not_provisioned`,
1855
1871
  * `volume_capacity_unavailable`, and `volume_resize_timeout` (a `TimeoutError`;
1856
1872
  * the size is left unchanged).
1857
1873
  *
1858
- * @returns The volume's updated info, including `sizeEnforced`.
1874
+ * @returns The volume's updated info, including `sizeEnforced` for a container volume.
1859
1875
  */
1860
1876
  static resize(projectId: string, nameOrId: string, sizeGb: number, opts?: ConnectionOpts): Promise<VolumeInfo>;
1861
1877
  getInfo(projectId: string): Promise<VolumeInfo>;
1862
- /** Resize this volume — see the static {@link Volume.resize} (not supported for Firecracker volumes yet). */
1878
+ /** Resize this volume — see the static {@link Volume.resize} (a Firecracker volume can only grow). */
1863
1879
  resize(projectId: string, sizeGb: number): Promise<VolumeInfo>;
1864
1880
  delete(projectId: string): Promise<void>;
1865
1881
  }
@@ -2152,7 +2168,7 @@ interface CreateContextOpts {
2152
2168
  * Limits, honestly:
2153
2169
  * - `javascript` (node) and `bash` run each call as a fresh process: no state
2154
2170
  * carries over and `results` stays empty. `node` must be installed in the
2155
- * template; the `interpreter` template does not ship it.
2171
+ * template; the `interpreter` template ships Node 22.
2156
2172
  * - On a template without Jupyter (e.g. `base`), Python also runs as a fresh
2157
2173
  * process per call.
2158
2174
  * - `stdout`/`stderr` are delivered in the chunks the kernel or process
@@ -2166,7 +2182,8 @@ interface CreateContextOpts {
2166
2182
  * const sandbox = await CodeSandbox.create({ project: 'my-project' })
2167
2183
  * await sandbox.runCode('x = 21')
2168
2184
  * const run = await sandbox.runCode('x * 2')
2169
- * console.log(run.results[0].data) // "42"
2185
+ * const out = run.results[0]
2186
+ * if (out?.type === 'result') console.log(out.data) // "42"
2170
2187
  * await sandbox.kill()
2171
2188
  * ```
2172
2189
  */
package/dist/index.js CHANGED
@@ -176,19 +176,20 @@ var PlatformClient = class {
176
176
  const text = await res.text();
177
177
  return text ? JSON.parse(text) : void 0;
178
178
  }
179
- request(method, path, body) {
179
+ /** `timeoutMs` overrides the connection's requestTimeoutMs for a call known to be slow, never shortening it. */
180
+ request(method, path, body, timeoutMs) {
180
181
  return fetch(`${this.config.apiUrl}${path}`, {
181
182
  method,
182
183
  headers: this.config.headers,
183
184
  body: body === void 0 ? void 0 : JSON.stringify(body),
184
- signal: AbortSignal.timeout(this.config.requestTimeoutMs)
185
+ signal: AbortSignal.timeout(Math.max(this.config.requestTimeoutMs, timeoutMs ?? 0))
185
186
  }).then((res) => this.json(res));
186
187
  }
187
188
  get(path) {
188
189
  return this.request("GET", path);
189
190
  }
190
- post(path, body) {
191
- return this.request("POST", path, body);
191
+ post(path, body, timeoutMs) {
192
+ return this.request("POST", path, body, timeoutMs);
192
193
  }
193
194
  put(path, body) {
194
195
  return this.request("PUT", path, body);
@@ -1126,6 +1127,7 @@ var SandboxClient = class {
1126
1127
  };
1127
1128
 
1128
1129
  // src/sandbox/index.ts
1130
+ var SNAPSHOT_TIMEOUT_MS = 11 * 6e4;
1129
1131
  var Sandbox = class extends SandboxClient {
1130
1132
  constructor(opts) {
1131
1133
  super();
@@ -1256,9 +1258,10 @@ var Sandbox = class extends SandboxClient {
1256
1258
  return exposed;
1257
1259
  }
1258
1260
  /**
1259
- * Clone this running sandbox `count` times (1-10). Each fork is a copy of the
1261
+ * Clone this running sandbox `count` times (1-100). Each fork is a copy of the
1260
1262
  * microVM at this instant -- memory, running processes and files -- and is
1261
- * billed like its source. The source keeps running.
1263
+ * billed like its source. The source keeps running. Each copy counts as a
1264
+ * sandbox start, so the account's running-sandbox limit is the real cap.
1262
1265
  *
1263
1266
  * Returns one entry per requested fork, in order: `{ sandbox, info }`, or
1264
1267
  * `{ error }` for a copy that could not be made (e.g. the account's sandbox
@@ -1286,12 +1289,13 @@ var Sandbox = class extends SandboxClient {
1286
1289
  }
1287
1290
  /**
1288
1291
  * Save this sandbox -- memory, running processes and files -- as a private
1289
- * snapshot. A Firecracker snapshot is `ready` as soon as this returns (about 2 s);
1290
- * start copies of it with {@link Sandbox.restore} (about 0.4 s each). The source
1291
- * keeps running. `poolSize` applies to container sandboxes only.
1292
+ * snapshot. A Firecracker snapshot is `ready` as soon as this returns: about 2 s, up to
1293
+ * a few minutes for a sandbox that filled most of its memory (its memory is uploaded
1294
+ * first), so the call waits up to 11 minutes. Start copies of it with {@link Sandbox.restore}
1295
+ * (about 0.4 s each). The source keeps running. `poolSize` applies to container sandboxes only.
1292
1296
  */
1293
1297
  snapshot(name = `Snapshot ${this.sandboxId}`, opts = {}) {
1294
- return new PlatformClient(this.resolveOpts()).post(`/api/sandboxes/${this.sandboxId}/snapshot`, { name, poolSize: opts.poolSize ?? 5, scope: opts.scope });
1298
+ return new PlatformClient(this.resolveOpts()).post(`/api/sandboxes/${this.sandboxId}/snapshot`, { name, poolSize: opts.poolSize ?? 5, scope: opts.scope }, SNAPSHOT_TIMEOUT_MS);
1295
1299
  }
1296
1300
  unexpose(port) {
1297
1301
  return new PlatformClient(this.resolveOpts()).delete(`/api/sandboxes/${this.sandboxId}/expose/${port}`);
@@ -1378,7 +1382,10 @@ var ProjectsAPI = class {
1378
1382
  get(id) {
1379
1383
  return this.client.get(`/api/projects/${id}`);
1380
1384
  }
1381
- /** Create a new project. */
1385
+ /**
1386
+ * Create a new project. A scoped key can create one only in a workspace it
1387
+ * covers; anywhere else it gets HTTP 403 with `code: 'PROJECT_OUT_OF_SCOPE'`.
1388
+ */
1382
1389
  create(opts) {
1383
1390
  return this.client.post("/api/projects", opts);
1384
1391
  }
@@ -1647,16 +1654,24 @@ var WorkspacesAPI = class {
1647
1654
  list() {
1648
1655
  return this.client.get("/api/workspaces");
1649
1656
  }
1650
- /** Create a workspace. The caller becomes its owner. */
1657
+ /**
1658
+ * Create a workspace. The caller becomes its owner.
1659
+ *
1660
+ * Needs a key with no scopes: a scoped key gets HTTP 403 with
1661
+ * `code: 'ACCOUNT_SCOPE_REQUIRED'`.
1662
+ */
1651
1663
  create(opts) {
1652
1664
  return this.client.post("/api/workspaces", opts);
1653
1665
  }
1654
1666
  /**
1655
1667
  * Delete a workspace.
1656
1668
  *
1657
- * Empty-only by default — the server refuses while any project or sandbox remains,
1658
- * so this cannot quietly destroy a user's work. Pass `{ force: true }` to delete a
1659
- * workspace and everything in it; that is irreversible.
1669
+ * Empty-only by default — the server refuses while any project, sandbox or volume
1670
+ * remains, so this cannot quietly destroy a user's work. `{ force: true }` deletes
1671
+ * the workspace with its apps, addons and projects; that is irreversible. It does not
1672
+ * delete sandboxes, sandbox volumes or snapshots: a running sandbox keeps running with
1673
+ * no workspace left to find it in. Kill the workspace's sandboxes and delete its
1674
+ * volumes and snapshots first.
1660
1675
  */
1661
1676
  delete(id, opts) {
1662
1677
  const qs = opts?.force ? "" : "?requireEmpty=true";
@@ -1850,9 +1865,9 @@ var Volume = class _Volume {
1850
1865
  /**
1851
1866
  * Resize a volume, by name or by id, to `sizeGb`.
1852
1867
  *
1853
- * **Firecracker volumes cannot be resized yet**: volumes created for the default
1854
- * Firecracker sandboxes throw `LizardError` (400) with `code: 'volume_not_resizable'`.
1855
- * Create the volume at the size you need, or copy the data to a new, larger one.
1868
+ * A Firecracker volume (the default) can only grow. The change is in place and
1869
+ * online: a sandbox with the volume attached keeps running and sees the new size.
1870
+ * A smaller size throws `LizardError` (400) with `code: 'volume_shrink_unsupported'`.
1856
1871
  *
1857
1872
  * On a container volume the change is in place and online: the size is a quota,
1858
1873
  * so no data is copied and a mounted sandbox sees the new size immediately. Both
@@ -1860,12 +1875,13 @@ var Volume = class _Volume {
1860
1875
  * new size free — otherwise it throws `ConflictError` with
1861
1876
  * `code: 'volume_too_full_to_shrink'`.
1862
1877
  *
1863
- * Other failures carry a `code` too: `invalid_volume_size`, `volume_not_resizable`,
1878
+ * Other failures carry a `code` too: `invalid_volume_size`, `volume_too_large`,
1879
+ * `volume_not_resizable` (an app's volume, not a sandbox volume),
1864
1880
  * `volume_resize_in_progress`, `volume_not_provisioned`,
1865
1881
  * `volume_capacity_unavailable`, and `volume_resize_timeout` (a `TimeoutError`;
1866
1882
  * the size is left unchanged).
1867
1883
  *
1868
- * @returns The volume's updated info, including `sizeEnforced`.
1884
+ * @returns The volume's updated info, including `sizeEnforced` for a container volume.
1869
1885
  */
1870
1886
  static async resize(projectId, nameOrId, sizeGb, opts) {
1871
1887
  const config = new ConnectionConfig(opts);
@@ -1884,7 +1900,7 @@ var Volume = class _Volume {
1884
1900
  if (!res.ok) await handleApiError(res);
1885
1901
  return res.json();
1886
1902
  }
1887
- /** Resize this volume — see the static {@link Volume.resize} (not supported for Firecracker volumes yet). */
1903
+ /** Resize this volume — see the static {@link Volume.resize} (a Firecracker volume can only grow). */
1888
1904
  async resize(projectId, sizeGb) {
1889
1905
  return _Volume.resize(projectId, this.volumeId, sizeGb, {
1890
1906
  apiKey: this.config.apiKey,
package/dist/index.mjs CHANGED
@@ -123,19 +123,20 @@ var PlatformClient = class {
123
123
  const text = await res.text();
124
124
  return text ? JSON.parse(text) : void 0;
125
125
  }
126
- request(method, path, body) {
126
+ /** `timeoutMs` overrides the connection's requestTimeoutMs for a call known to be slow, never shortening it. */
127
+ request(method, path, body, timeoutMs) {
127
128
  return fetch(`${this.config.apiUrl}${path}`, {
128
129
  method,
129
130
  headers: this.config.headers,
130
131
  body: body === void 0 ? void 0 : JSON.stringify(body),
131
- signal: AbortSignal.timeout(this.config.requestTimeoutMs)
132
+ signal: AbortSignal.timeout(Math.max(this.config.requestTimeoutMs, timeoutMs ?? 0))
132
133
  }).then((res) => this.json(res));
133
134
  }
134
135
  get(path) {
135
136
  return this.request("GET", path);
136
137
  }
137
- post(path, body) {
138
- return this.request("POST", path, body);
138
+ post(path, body, timeoutMs) {
139
+ return this.request("POST", path, body, timeoutMs);
139
140
  }
140
141
  put(path, body) {
141
142
  return this.request("PUT", path, body);
@@ -1073,6 +1074,7 @@ var SandboxClient = class {
1073
1074
  };
1074
1075
 
1075
1076
  // src/sandbox/index.ts
1077
+ var SNAPSHOT_TIMEOUT_MS = 11 * 6e4;
1076
1078
  var Sandbox = class extends SandboxClient {
1077
1079
  constructor(opts) {
1078
1080
  super();
@@ -1203,9 +1205,10 @@ var Sandbox = class extends SandboxClient {
1203
1205
  return exposed;
1204
1206
  }
1205
1207
  /**
1206
- * Clone this running sandbox `count` times (1-10). Each fork is a copy of the
1208
+ * Clone this running sandbox `count` times (1-100). Each fork is a copy of the
1207
1209
  * microVM at this instant -- memory, running processes and files -- and is
1208
- * billed like its source. The source keeps running.
1210
+ * billed like its source. The source keeps running. Each copy counts as a
1211
+ * sandbox start, so the account's running-sandbox limit is the real cap.
1209
1212
  *
1210
1213
  * Returns one entry per requested fork, in order: `{ sandbox, info }`, or
1211
1214
  * `{ error }` for a copy that could not be made (e.g. the account's sandbox
@@ -1233,12 +1236,13 @@ var Sandbox = class extends SandboxClient {
1233
1236
  }
1234
1237
  /**
1235
1238
  * Save this sandbox -- memory, running processes and files -- as a private
1236
- * snapshot. A Firecracker snapshot is `ready` as soon as this returns (about 2 s);
1237
- * start copies of it with {@link Sandbox.restore} (about 0.4 s each). The source
1238
- * keeps running. `poolSize` applies to container sandboxes only.
1239
+ * snapshot. A Firecracker snapshot is `ready` as soon as this returns: about 2 s, up to
1240
+ * a few minutes for a sandbox that filled most of its memory (its memory is uploaded
1241
+ * first), so the call waits up to 11 minutes. Start copies of it with {@link Sandbox.restore}
1242
+ * (about 0.4 s each). The source keeps running. `poolSize` applies to container sandboxes only.
1239
1243
  */
1240
1244
  snapshot(name = `Snapshot ${this.sandboxId}`, opts = {}) {
1241
- return new PlatformClient(this.resolveOpts()).post(`/api/sandboxes/${this.sandboxId}/snapshot`, { name, poolSize: opts.poolSize ?? 5, scope: opts.scope });
1245
+ return new PlatformClient(this.resolveOpts()).post(`/api/sandboxes/${this.sandboxId}/snapshot`, { name, poolSize: opts.poolSize ?? 5, scope: opts.scope }, SNAPSHOT_TIMEOUT_MS);
1242
1246
  }
1243
1247
  unexpose(port) {
1244
1248
  return new PlatformClient(this.resolveOpts()).delete(`/api/sandboxes/${this.sandboxId}/expose/${port}`);
@@ -1325,7 +1329,10 @@ var ProjectsAPI = class {
1325
1329
  get(id) {
1326
1330
  return this.client.get(`/api/projects/${id}`);
1327
1331
  }
1328
- /** Create a new project. */
1332
+ /**
1333
+ * Create a new project. A scoped key can create one only in a workspace it
1334
+ * covers; anywhere else it gets HTTP 403 with `code: 'PROJECT_OUT_OF_SCOPE'`.
1335
+ */
1329
1336
  create(opts) {
1330
1337
  return this.client.post("/api/projects", opts);
1331
1338
  }
@@ -1594,16 +1601,24 @@ var WorkspacesAPI = class {
1594
1601
  list() {
1595
1602
  return this.client.get("/api/workspaces");
1596
1603
  }
1597
- /** Create a workspace. The caller becomes its owner. */
1604
+ /**
1605
+ * Create a workspace. The caller becomes its owner.
1606
+ *
1607
+ * Needs a key with no scopes: a scoped key gets HTTP 403 with
1608
+ * `code: 'ACCOUNT_SCOPE_REQUIRED'`.
1609
+ */
1598
1610
  create(opts) {
1599
1611
  return this.client.post("/api/workspaces", opts);
1600
1612
  }
1601
1613
  /**
1602
1614
  * Delete a workspace.
1603
1615
  *
1604
- * Empty-only by default — the server refuses while any project or sandbox remains,
1605
- * so this cannot quietly destroy a user's work. Pass `{ force: true }` to delete a
1606
- * workspace and everything in it; that is irreversible.
1616
+ * Empty-only by default — the server refuses while any project, sandbox or volume
1617
+ * remains, so this cannot quietly destroy a user's work. `{ force: true }` deletes
1618
+ * the workspace with its apps, addons and projects; that is irreversible. It does not
1619
+ * delete sandboxes, sandbox volumes or snapshots: a running sandbox keeps running with
1620
+ * no workspace left to find it in. Kill the workspace's sandboxes and delete its
1621
+ * volumes and snapshots first.
1607
1622
  */
1608
1623
  delete(id, opts) {
1609
1624
  const qs = opts?.force ? "" : "?requireEmpty=true";
@@ -1797,9 +1812,9 @@ var Volume = class _Volume {
1797
1812
  /**
1798
1813
  * Resize a volume, by name or by id, to `sizeGb`.
1799
1814
  *
1800
- * **Firecracker volumes cannot be resized yet**: volumes created for the default
1801
- * Firecracker sandboxes throw `LizardError` (400) with `code: 'volume_not_resizable'`.
1802
- * Create the volume at the size you need, or copy the data to a new, larger one.
1815
+ * A Firecracker volume (the default) can only grow. The change is in place and
1816
+ * online: a sandbox with the volume attached keeps running and sees the new size.
1817
+ * A smaller size throws `LizardError` (400) with `code: 'volume_shrink_unsupported'`.
1803
1818
  *
1804
1819
  * On a container volume the change is in place and online: the size is a quota,
1805
1820
  * so no data is copied and a mounted sandbox sees the new size immediately. Both
@@ -1807,12 +1822,13 @@ var Volume = class _Volume {
1807
1822
  * new size free — otherwise it throws `ConflictError` with
1808
1823
  * `code: 'volume_too_full_to_shrink'`.
1809
1824
  *
1810
- * Other failures carry a `code` too: `invalid_volume_size`, `volume_not_resizable`,
1825
+ * Other failures carry a `code` too: `invalid_volume_size`, `volume_too_large`,
1826
+ * `volume_not_resizable` (an app's volume, not a sandbox volume),
1811
1827
  * `volume_resize_in_progress`, `volume_not_provisioned`,
1812
1828
  * `volume_capacity_unavailable`, and `volume_resize_timeout` (a `TimeoutError`;
1813
1829
  * the size is left unchanged).
1814
1830
  *
1815
- * @returns The volume's updated info, including `sizeEnforced`.
1831
+ * @returns The volume's updated info, including `sizeEnforced` for a container volume.
1816
1832
  */
1817
1833
  static async resize(projectId, nameOrId, sizeGb, opts) {
1818
1834
  const config = new ConnectionConfig(opts);
@@ -1831,7 +1847,7 @@ var Volume = class _Volume {
1831
1847
  if (!res.ok) await handleApiError(res);
1832
1848
  return res.json();
1833
1849
  }
1834
- /** Resize this volume — see the static {@link Volume.resize} (not supported for Firecracker volumes yet). */
1850
+ /** Resize this volume — see the static {@link Volume.resize} (a Firecracker volume can only grow). */
1835
1851
  async resize(projectId, sizeGb) {
1836
1852
  return _Volume.resize(projectId, this.volumeId, sizeGb, {
1837
1853
  apiKey: this.config.apiKey,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lizard-build/sdk",
3
- "version": "0.1.58",
3
+ "version": "0.1.60",
4
4
  "description": "TypeScript SDK for Lizard: Firecracker microVM sandboxes, code execution and cloud apps.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",