@aerol-ai/aerolvm-sdk 0.1.6 → 0.1.8

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/dist/MicroVM.d.ts CHANGED
@@ -1,10 +1,18 @@
1
+ import { type APIVersion } from "./internal/client.js";
1
2
  import { Sandbox } from "./Sandbox.js";
2
- import type { CreateOptions, CreateSessionOptions, ExecStreamHandle, ExecStreamOptions, HealthStatus, Lifecycle, MountSpecRedacted, ResizeOptions, Session, SessionAttachHandle, SessionAttachOptions } from "./types.js";
3
+ import type { CreateOptions, CreateSessionOptions, ExecStreamHandle, ExecStreamOptions, HealthStatus, Lifecycle, MountSpecRedacted, ResizeOptions, SandboxSnapshot, Session, SessionAttachHandle, SessionAttachOptions } from "./types.js";
3
4
  type FetchLike = typeof fetch;
4
5
  export interface MicroVMConfig {
5
6
  patToken?: string;
6
7
  apiUrl?: string;
7
8
  fetch?: FetchLike;
9
+ /**
10
+ * Wire version of the sandbox daemon API to call. Defaults to the SDK's
11
+ * pinned default ("v1" today). The SDK package version and the API wire
12
+ * version evolve independently — you can pin one without affecting the
13
+ * other.
14
+ */
15
+ apiVersion?: APIVersion;
8
16
  }
9
17
  export declare class MicroVM {
10
18
  readonly apiUrl: string;
@@ -16,6 +24,7 @@ export declare class MicroVM {
16
24
  get(id: string): Promise<Sandbox>;
17
25
  start(id: string): Promise<Sandbox>;
18
26
  stop(id: string): Promise<Sandbox>;
27
+ createSnapshot(id: string, name: string): Promise<SandboxSnapshot>;
19
28
  destroy(id: string): Promise<void>;
20
29
  resize(id: string, options: ResizeOptions): Promise<Sandbox>;
21
30
  updateLifecycle(id: string, lifecycle: Lifecycle): Promise<Sandbox>;
package/dist/MicroVM.js CHANGED
@@ -18,6 +18,7 @@ export class MicroVM {
18
18
  baseURL: apiUrl,
19
19
  patToken,
20
20
  fetch: config.fetch,
21
+ apiVersion: config.apiVersion,
21
22
  });
22
23
  }
23
24
  async create(options) {
@@ -40,6 +41,9 @@ export class MicroVM {
40
41
  const sandbox = await this.client.stop(id);
41
42
  return this.wrap(sandbox.toJSON());
42
43
  }
44
+ async createSnapshot(id, name) {
45
+ return this.client.createSnapshot(id, name);
46
+ }
43
47
  async destroy(id) {
44
48
  await this.client.destroy(id);
45
49
  }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export { MicroVM } from "./MicroVM.js";
2
2
  export { Sandbox } from "./Sandbox.js";
3
3
  export type { MicroVMConfig } from "./MicroVM.js";
4
- export type { BinaryLike, CreateOptions, CreateSessionOptions, ExecRequest, ExecResult, ExecExitInfo, ExposedPort, HealthStatus, Lifecycle, MountSpec, MountSpecRedacted, MountType, RegistryAuth, ResizeOptions, Sandbox as SandboxData, SandboxStatus, Session, SessionAttachHandle, SessionAttachOptions, SessionStatus, UpdateLifecycleOptions, } from "./types.js";
4
+ export type { BinaryLike, CreateOptions, CreateSessionOptions, ExecRequest, ExecResult, ExecExitInfo, ExposedPort, ExposePortOptions, ExposeProtocol, ExposeResult, HealthStatus, Lifecycle, MountSpec, MountSpecRedacted, MountType, RegistryAuth, ResizeOptions, Sandbox as SandboxData, SandboxSnapshot, SandboxStatus, Session, SessionAttachHandle, SessionAttachOptions, SessionStatus, UpdateLifecycleOptions, } from "./types.js";
@@ -0,0 +1 @@
1
+ export declare const PATH_PREFIX = "/v1";
@@ -0,0 +1,7 @@
1
+ // v1 path-prefix constant. The TypeScript SDK uses this to build URLs against
2
+ // the v1 surface of the sandbox daemon. When v2 lands, a sibling
3
+ // internal/api/v2/paths.ts will export its own PATH_PREFIX, and the client
4
+ // constructor's apiVersion option selects which one to use.
5
+ //
6
+ // Keep this in sync with pkg/api/v1/dto.go::PathPrefix on the server.
7
+ export const PATH_PREFIX = "/v1";
@@ -1,20 +1,43 @@
1
- import type { BinaryLike, CreateOptions, CreateSessionOptions, ExecRequest, ExecResult, ExecStreamHandle, ExecStreamOptions, ExposedPort, HealthStatus, Lifecycle, MountSpecRedacted, ResizeOptions, Sandbox, Session, SessionAttachHandle, SessionAttachOptions } from "../types.js";
1
+ import type { BinaryLike, CreateOptions, CreateSessionOptions, ExecRequest, ExecResult, ExecStreamHandle, ExecStreamOptions, ExposedPort, ExposePortOptions, ExposeResult, HealthStatus, Lifecycle, MountSpecRedacted, NetworkUsage, SetNetworkLimitsOptions, ResizeOptions, Sandbox, SandboxSnapshot, Session, SessionAttachHandle, SessionAttachOptions } from "../types.js";
2
2
  type FetchLike = typeof fetch;
3
+ /**
4
+ * Wire version of the sandbox daemon API this client speaks. The SDK and the
5
+ * server version independently — bumping the SDK package does not move the
6
+ * wire version. Today only "v1" exists; "v2" will be added when a wire-level
7
+ * breaking change is needed on the server.
8
+ */
9
+ export type APIVersion = "v1";
3
10
  export interface APIClientConfig {
4
11
  baseURL: string;
5
12
  patToken?: string;
6
13
  fetch?: FetchLike;
14
+ /**
15
+ * Wire version to call. Defaults to "v1". Pin this if you need a specific
16
+ * version — otherwise the SDK will track its own default, which may move in
17
+ * a future major SDK release.
18
+ */
19
+ apiVersion?: APIVersion;
7
20
  }
8
21
  export declare class APIClient {
9
22
  readonly baseURL: string;
23
+ readonly apiVersion: APIVersion;
10
24
  private readonly patToken;
11
25
  private readonly fetchFn;
26
+ private readonly versionPrefix;
12
27
  constructor(config: APIClientConfig);
28
+ /**
29
+ * Build a versioned API path. Pass the suffix beginning with "/" (e.g.
30
+ * "/sandboxes") and the active version's prefix is prepended. Use this for
31
+ * every versioned API call so v2 (when it lands) can be selected by the
32
+ * apiVersion option without touching call sites.
33
+ */
34
+ private versioned;
13
35
  create(options: CreateOptions): Promise<SandboxResource>;
14
36
  list(): Promise<SandboxResource[]>;
15
37
  get(id: string): Promise<SandboxResource>;
16
38
  start(id: string): Promise<SandboxResource>;
17
39
  stop(id: string): Promise<SandboxResource>;
40
+ createSnapshot(id: string, name: string): Promise<SandboxSnapshot>;
18
41
  destroy(id: string): Promise<void>;
19
42
  resize(id: string, options: ResizeOptions): Promise<SandboxResource>;
20
43
  updateLifecycle(id: string, lifecycle: Lifecycle): Promise<SandboxResource>;
@@ -31,24 +54,26 @@ export declare class APIClient {
31
54
  attachSession(id: string, sessionID: string, options?: SessionAttachOptions): SessionAttachHandle;
32
55
  uploadFile(id: string, targetPath: string, data: BinaryLike): Promise<void>;
33
56
  downloadFile(id: string, targetPath: string): Promise<Uint8Array>;
34
- exposePort(id: string, port: number): Promise<string>;
35
- /**
36
- * Publish a raw TCP port through caddy-l4. The returned URL is in
37
- * `tcp://<host>:<port>` form and works directly with `psql`, `redis-cli`,
38
- * `mysql`, `mongosh`, and any other native-protocol client.
39
- */
40
- exposeTCPPort(id: string, port: number): Promise<string>;
41
57
  /**
42
- * Publish a TCP port behind the shared TLS-SNI multiplexer. Returns
43
- * `tls://<id>-<port>.<domain>:<l4-port>`. Requires the daemon to have a
44
- * domain configured AND `SB_L4_TLS_LISTEN` set.
58
+ * Publish a sandbox container port. The chosen wire protocol is selected by
59
+ * `options.protocol` (defaults to `"http"`):
60
+ *
61
+ * - `"http"`: Caddy HTTP reverse proxy at `https://<id>-<port>.<domain>`.
62
+ * - `"tcp"`: raw caddy-l4 listener on a parent-host port — pair with
63
+ * native protocol clients (psql, redis-cli, mysql, mongosh).
64
+ * - `"tls"`: caddy-l4 TLS-SNI route on the shared listener. Requires the
65
+ * daemon to have a domain configured AND `SB_L4_TLS_LISTEN` set.
66
+ *
67
+ * Returns a discriminated result keyed on `protocol`. Only the `"tcp"`
68
+ * variant carries `host` / `hostPort` — everything else is in `url`.
45
69
  */
46
- exposeTLSPort(id: string, port: number): Promise<string>;
47
- private exposePortWithProtocol;
70
+ exposePort(id: string, port: number, options?: ExposePortOptions): Promise<ExposeResult>;
48
71
  unexposePort(id: string, port: number): Promise<void>;
49
72
  reconcile(): Promise<void>;
50
73
  health(): Promise<HealthStatus>;
51
74
  mounts(id: string): Promise<MountSpecRedacted[]>;
75
+ getNetworkUsage(id: string): Promise<NetworkUsage>;
76
+ setNetworkLimits(id: string, options: SetNetworkLimitsOptions): Promise<NetworkUsage>;
52
77
  private wrap;
53
78
  private doJSON;
54
79
  private doBytes;
@@ -94,15 +119,16 @@ export declare class SandboxResource implements Sandbox {
94
119
  attachSession(sessionID: string, options?: SessionAttachOptions): SessionAttachHandle;
95
120
  uploadFile(targetPath: string, data: BinaryLike): Promise<void>;
96
121
  downloadFile(targetPath: string): Promise<Uint8Array>;
97
- exposePort(port: number): Promise<string>;
98
- exposeTCPPort(port: number): Promise<string>;
99
- exposeTLSPort(port: number): Promise<string>;
122
+ exposePort(port: number, options?: ExposePortOptions): Promise<ExposeResult>;
100
123
  unexposePort(port: number): Promise<void>;
124
+ createSnapshot(name: string): Promise<SandboxSnapshot>;
101
125
  start(): Promise<this>;
102
126
  stop(): Promise<this>;
103
127
  destroy(): Promise<void>;
104
128
  resize(options: ResizeOptions): Promise<this>;
105
129
  updateLifecycle(lifecycle: Lifecycle): Promise<this>;
130
+ getNetworkUsage(): Promise<NetworkUsage>;
131
+ setNetworkLimits(options: SetNetworkLimitsOptions): Promise<NetworkUsage>;
106
132
  toJSON(): Sandbox;
107
133
  private apply;
108
134
  }
@@ -1,135 +1,159 @@
1
1
  import { basename } from "node:path";
2
+ import { PATH_PREFIX as V1_PATH_PREFIX } from "./api/v1/paths.js";
3
+ const DEFAULT_API_VERSION = "v1";
4
+ const PATH_PREFIXES = {
5
+ v1: V1_PATH_PREFIX,
6
+ };
2
7
  export class APIClient {
3
8
  baseURL;
9
+ apiVersion;
4
10
  patToken;
5
11
  fetchFn;
12
+ versionPrefix;
6
13
  constructor(config) {
7
14
  this.baseURL = config.baseURL.replace(/\/+$/, "");
8
15
  this.patToken = config.patToken ?? "";
9
16
  this.fetchFn = config.fetch ?? fetch;
17
+ this.apiVersion = config.apiVersion ?? DEFAULT_API_VERSION;
18
+ this.versionPrefix = PATH_PREFIXES[this.apiVersion];
19
+ }
20
+ /**
21
+ * Build a versioned API path. Pass the suffix beginning with "/" (e.g.
22
+ * "/sandboxes") and the active version's prefix is prepended. Use this for
23
+ * every versioned API call so v2 (when it lands) can be selected by the
24
+ * apiVersion option without touching call sites.
25
+ */
26
+ versioned(suffix) {
27
+ return `${this.versionPrefix}${suffix}`;
10
28
  }
11
29
  async create(options) {
12
- const response = await this.doJSON("POST", "/v1/sandboxes", toApiCreateOptions(options));
30
+ const response = await this.doJSON("POST", this.versioned("/sandboxes"), toApiCreateOptions(options));
13
31
  return new SandboxResource(this, fromApiCreateSandboxResponse(response));
14
32
  }
15
33
  async list() {
16
- const response = await this.doJSON("GET", "/v1/sandboxes");
34
+ const response = await this.doJSON("GET", this.versioned("/sandboxes"));
17
35
  return response.map((item) => this.wrap(item));
18
36
  }
19
37
  async get(id) {
20
- const response = await this.doJSON("GET", `/v1/sandboxes/${id}`);
38
+ const response = await this.doJSON("GET", `${this.versionPrefix}/sandboxes/${id}`);
21
39
  return this.wrap(response);
22
40
  }
23
41
  async start(id) {
24
- const response = await this.doJSON("POST", `/v1/sandboxes/${id}/start`);
42
+ const response = await this.doJSON("POST", `${this.versionPrefix}/sandboxes/${id}/start`);
25
43
  return this.wrap(response);
26
44
  }
27
45
  async stop(id) {
28
- const response = await this.doJSON("POST", `/v1/sandboxes/${id}/stop`);
46
+ const response = await this.doJSON("POST", `${this.versionPrefix}/sandboxes/${id}/stop`);
29
47
  return this.wrap(response);
30
48
  }
49
+ async createSnapshot(id, name) {
50
+ const response = await this.doJSON("POST", `${this.versionPrefix}/sandboxes/${id}/snapshot`, { name });
51
+ return fromApiSandboxSnapshot(response);
52
+ }
31
53
  async destroy(id) {
32
- await this.doJSON("DELETE", `/v1/sandboxes/${id}`);
54
+ await this.doJSON("DELETE", `${this.versionPrefix}/sandboxes/${id}`);
33
55
  }
34
56
  async resize(id, options) {
35
- const response = await this.doJSON("POST", `/v1/sandboxes/${id}/resize`, toApiResizeOptions(options));
57
+ const response = await this.doJSON("POST", `${this.versionPrefix}/sandboxes/${id}/resize`, toApiResizeOptions(options));
36
58
  return this.wrap(response);
37
59
  }
38
60
  async updateLifecycle(id, lifecycle) {
39
- const response = await this.doJSON("PUT", `/v1/sandboxes/${id}/lifecycle`, toApiLifecycle(lifecycle));
61
+ const response = await this.doJSON("PUT", `${this.versionPrefix}/sandboxes/${id}/lifecycle`, toApiLifecycle(lifecycle));
40
62
  return this.wrap(response);
41
63
  }
42
64
  async exec(id, request) {
43
- const response = await this.doJSON("POST", `/v1/sandboxes/${id}/toolbox/process/execute`, toApiExecRequest(request));
65
+ const response = await this.doJSON("POST", `${this.versionPrefix}/sandboxes/${id}/toolbox/process/execute`, toApiExecRequest(request));
44
66
  return fromApiExecResult(response);
45
67
  }
46
68
  execStream(id, options) {
47
- return openExecStream(this.baseURL, this.patToken, id, options);
69
+ return openExecStream(this.baseURL, this.versionPrefix, this.patToken, id, options);
48
70
  }
49
71
  async createSession(id, options) {
50
- const response = await this.doJSON("POST", `/v1/sandboxes/${id}/sessions`, toApiCreateSessionOptions(options));
72
+ const response = await this.doJSON("POST", `${this.versionPrefix}/sandboxes/${id}/sessions`, toApiCreateSessionOptions(options));
51
73
  return fromApiSession(response);
52
74
  }
53
75
  async listSessions(id) {
54
- const response = await this.doJSON("GET", `/v1/sandboxes/${id}/sessions`);
76
+ const response = await this.doJSON("GET", `${this.versionPrefix}/sandboxes/${id}/sessions`);
55
77
  return response.sessions.map(fromApiSession);
56
78
  }
57
79
  async getSession(id, sessionID) {
58
- const response = await this.doJSON("GET", `/v1/sandboxes/${id}/sessions/${sessionID}`);
80
+ const response = await this.doJSON("GET", `${this.versionPrefix}/sandboxes/${id}/sessions/${sessionID}`);
59
81
  return fromApiSession(response);
60
82
  }
61
83
  async deleteSession(id, sessionID) {
62
- await this.doJSON("DELETE", `/v1/sandboxes/${id}/sessions/${sessionID}`);
84
+ await this.doJSON("DELETE", `${this.versionPrefix}/sandboxes/${id}/sessions/${sessionID}`);
63
85
  }
64
86
  async signalSession(id, sessionID, signal) {
65
- await this.doJSON("POST", `/v1/sandboxes/${id}/sessions/${sessionID}/signal`, { signal });
87
+ await this.doJSON("POST", `${this.versionPrefix}/sandboxes/${id}/sessions/${sessionID}/signal`, { signal });
66
88
  }
67
89
  async resizeSession(id, sessionID, cols, rows) {
68
- await this.doJSON("POST", `/v1/sandboxes/${id}/sessions/${sessionID}/resize`, { cols, rows });
90
+ await this.doJSON("POST", `${this.versionPrefix}/sandboxes/${id}/sessions/${sessionID}/resize`, { cols, rows });
69
91
  }
70
92
  async sessionLog(id, sessionID) {
71
- return this.doBytes(`/v1/sandboxes/${id}/sessions/${sessionID}/log`);
93
+ return this.doBytes(`${this.versionPrefix}/sandboxes/${id}/sessions/${sessionID}/log`);
72
94
  }
73
95
  async sessionRecording(id, sessionID) {
74
- return this.doBytes(`/v1/sandboxes/${id}/sessions/${sessionID}/recording`);
96
+ return this.doBytes(`${this.versionPrefix}/sandboxes/${id}/sessions/${sessionID}/recording`);
75
97
  }
76
98
  attachSession(id, sessionID, options = {}) {
77
- return openSessionAttach(this.baseURL, this.patToken, id, sessionID, options);
99
+ return openSessionAttach(this.baseURL, this.versionPrefix, this.patToken, id, sessionID, options);
78
100
  }
79
101
  async uploadFile(id, targetPath, data) {
80
102
  const form = new FormData();
81
103
  form.set("path", targetPath);
82
104
  form.set("file", toBlob(data), basename(targetPath));
83
- const response = await this.request("POST", `/v1/sandboxes/${id}/toolbox/files/upload`, { body: form });
105
+ const response = await this.request("POST", `${this.versionPrefix}/sandboxes/${id}/toolbox/files/upload`, { body: form });
84
106
  if (!response.ok) {
85
107
  throw await decodeError(response);
86
108
  }
87
109
  }
88
110
  async downloadFile(id, targetPath) {
89
- const response = await this.request("GET", `/v1/sandboxes/${id}/toolbox/files/download?path=${encodeURIComponent(targetPath)}`);
111
+ const response = await this.request("GET", `${this.versionPrefix}/sandboxes/${id}/toolbox/files/download?path=${encodeURIComponent(targetPath)}`);
90
112
  if (!response.ok) {
91
113
  throw await decodeError(response);
92
114
  }
93
115
  return new Uint8Array(await response.arrayBuffer());
94
116
  }
95
- async exposePort(id, port) {
96
- return this.exposePortWithProtocol(id, port);
97
- }
98
- /**
99
- * Publish a raw TCP port through caddy-l4. The returned URL is in
100
- * `tcp://<host>:<port>` form and works directly with `psql`, `redis-cli`,
101
- * `mysql`, `mongosh`, and any other native-protocol client.
102
- */
103
- async exposeTCPPort(id, port) {
104
- return this.exposePortWithProtocol(id, port, "tcp");
105
- }
106
117
  /**
107
- * Publish a TCP port behind the shared TLS-SNI multiplexer. Returns
108
- * `tls://<id>-<port>.<domain>:<l4-port>`. Requires the daemon to have a
109
- * domain configured AND `SB_L4_TLS_LISTEN` set.
118
+ * Publish a sandbox container port. The chosen wire protocol is selected by
119
+ * `options.protocol` (defaults to `"http"`):
120
+ *
121
+ * - `"http"`: Caddy HTTP reverse proxy at `https://<id>-<port>.<domain>`.
122
+ * - `"tcp"`: raw caddy-l4 listener on a parent-host port — pair with
123
+ * native protocol clients (psql, redis-cli, mysql, mongosh).
124
+ * - `"tls"`: caddy-l4 TLS-SNI route on the shared listener. Requires the
125
+ * daemon to have a domain configured AND `SB_L4_TLS_LISTEN` set.
126
+ *
127
+ * Returns a discriminated result keyed on `protocol`. Only the `"tcp"`
128
+ * variant carries `host` / `hostPort` — everything else is in `url`.
110
129
  */
111
- async exposeTLSPort(id, port) {
112
- return this.exposePortWithProtocol(id, port, "tls");
113
- }
114
- async exposePortWithProtocol(id, port, protocol) {
115
- const body = protocol ? { protocol } : undefined;
116
- const response = await this.doJSON("POST", `/v1/sandboxes/${id}/ports/${port}`, body);
117
- return response.public_url;
130
+ async exposePort(id, port, options = {}) {
131
+ const body = options.protocol ? { protocol: options.protocol } : undefined;
132
+ const response = await this.doJSON("POST", `${this.versionPrefix}/sandboxes/${id}/ports/${port}`, body);
133
+ return fromApiExposePortResponse(response);
118
134
  }
119
135
  async unexposePort(id, port) {
120
- await this.doJSON("DELETE", `/v1/sandboxes/${id}/ports/${port}`);
136
+ await this.doJSON("DELETE", `${this.versionPrefix}/sandboxes/${id}/ports/${port}`);
121
137
  }
122
138
  async reconcile() {
123
- await this.doJSON("POST", "/v1/admin/reconcile");
139
+ await this.doJSON("POST", this.versioned("/admin/reconcile"));
124
140
  }
125
141
  async health() {
126
142
  const response = await this.doJSON("GET", "/health");
127
143
  return fromApiHealthStatus(response);
128
144
  }
129
145
  async mounts(id) {
130
- const response = await this.doJSON("GET", `/v1/sandboxes/${id}/mounts`);
146
+ const response = await this.doJSON("GET", `${this.versionPrefix}/sandboxes/${id}/mounts`);
131
147
  return response.mounts.map(fromApiMountSpecRedacted);
132
148
  }
149
+ async getNetworkUsage(id) {
150
+ const response = await this.doJSON("GET", `${this.versionPrefix}/sandboxes/${id}/network/usage`);
151
+ return fromApiNetworkUsage(response);
152
+ }
153
+ async setNetworkLimits(id, options) {
154
+ const response = await this.doJSON("PATCH", `${this.versionPrefix}/sandboxes/${id}/network/limits`, toApiSetNetworkLimitsOptions(options));
155
+ return fromApiNetworkUsage(response);
156
+ }
133
157
  wrap(sandbox) {
134
158
  return new SandboxResource(this, fromApiSandbox(sandbox));
135
159
  }
@@ -219,18 +243,15 @@ export class SandboxResource {
219
243
  async downloadFile(targetPath) {
220
244
  return this.client.downloadFile(this.id, targetPath);
221
245
  }
222
- async exposePort(port) {
223
- return this.client.exposePort(this.id, port);
224
- }
225
- async exposeTCPPort(port) {
226
- return this.client.exposeTCPPort(this.id, port);
227
- }
228
- async exposeTLSPort(port) {
229
- return this.client.exposeTLSPort(this.id, port);
246
+ async exposePort(port, options) {
247
+ return this.client.exposePort(this.id, port, options);
230
248
  }
231
249
  async unexposePort(port) {
232
250
  await this.client.unexposePort(this.id, port);
233
251
  }
252
+ async createSnapshot(name) {
253
+ return this.client.createSnapshot(this.id, name);
254
+ }
234
255
  async start() {
235
256
  const updated = await this.client.start(this.id);
236
257
  this.apply(updated.toJSON());
@@ -254,6 +275,12 @@ export class SandboxResource {
254
275
  this.apply(updated.toJSON());
255
276
  return this;
256
277
  }
278
+ async getNetworkUsage() {
279
+ return this.client.getNetworkUsage(this.id);
280
+ }
281
+ async setNetworkLimits(options) {
282
+ return this.client.setNetworkLimits(this.id, options);
283
+ }
257
284
  toJSON() {
258
285
  return cloneSandbox(this);
259
286
  }
@@ -270,6 +297,8 @@ function toApiCreateOptions(options) {
270
297
  env: options.env,
271
298
  os_user: options.osUser,
272
299
  network_block_all: options.networkBlockAll,
300
+ network_bytes_in_limit: options.networkBytesInLimit,
301
+ network_bytes_out_limit: options.networkBytesOutLimit,
273
302
  registry: options.registry,
274
303
  container_command: options.containerCommand,
275
304
  mounts: options.mounts?.map(toApiMountSpec),
@@ -353,6 +382,15 @@ function fromApiCreateSandboxResponse(response) {
353
382
  sshPrivateKey: response.ssh_private_key,
354
383
  };
355
384
  }
385
+ function fromApiSandboxSnapshot(snapshot) {
386
+ return {
387
+ name: snapshot.name,
388
+ image: snapshot.image,
389
+ imageID: snapshot.image_id,
390
+ sourceSandboxID: snapshot.source_sandbox_id,
391
+ createdAt: snapshot.created_at,
392
+ };
393
+ }
356
394
  function fromApiSession(session) {
357
395
  return {
358
396
  id: session.id,
@@ -379,6 +417,21 @@ function fromApiExposedPort(port) {
379
417
  createdAt: port.created_at,
380
418
  };
381
419
  }
420
+ function fromApiExposePortResponse(response) {
421
+ switch (response.protocol) {
422
+ case "tcp":
423
+ return {
424
+ protocol: "tcp",
425
+ url: response.public_url,
426
+ host: response.host ?? "",
427
+ hostPort: response.host_port ?? 0,
428
+ };
429
+ case "tls":
430
+ return { protocol: "tls", url: response.public_url };
431
+ case "http":
432
+ return { protocol: "http", url: response.public_url };
433
+ }
434
+ }
382
435
  function fromApiExecResult(result) {
383
436
  return {
384
437
  stdout: result.stdout,
@@ -417,6 +470,24 @@ function fromApiMountSpecRedacted(mount) {
417
470
  hasCredentials: mount.has_credentials,
418
471
  };
419
472
  }
473
+ function fromApiNetworkUsage(usage) {
474
+ return {
475
+ sandboxID: usage.sandbox_id,
476
+ bytesIn: usage.bytes_in,
477
+ bytesOut: usage.bytes_out,
478
+ bytesInLimit: usage.bytes_in_limit,
479
+ bytesOutLimit: usage.bytes_out_limit,
480
+ quotaExceeded: usage.quota_exceeded,
481
+ quotaExceededAt: usage.quota_exceeded_at ?? undefined,
482
+ lastSampledAt: usage.last_sampled_at ?? undefined,
483
+ };
484
+ }
485
+ function toApiSetNetworkLimitsOptions(options) {
486
+ return {
487
+ network_bytes_in_limit: options.networkBytesInLimit,
488
+ network_bytes_out_limit: options.networkBytesOutLimit,
489
+ };
490
+ }
420
491
  function fromApiLifecycle(lifecycle) {
421
492
  const result = {};
422
493
  if (lifecycle?.stop_if_idle_for !== undefined) {
@@ -503,8 +574,8 @@ function toWebSocketBinaryFrame(data) {
503
574
  frame.set(bytes);
504
575
  return frame;
505
576
  }
506
- function openExecStream(baseURL, patToken, sandboxID, options) {
507
- const wsURL = baseURL.replace(/^http/, "ws") + `/v1/sandboxes/${encodeURIComponent(sandboxID)}/toolbox/process/exec/stream`;
577
+ function openExecStream(baseURL, versionPrefix, patToken, sandboxID, options) {
578
+ const wsURL = baseURL.replace(/^http/, "ws") + `${versionPrefix}/sandboxes/${encodeURIComponent(sandboxID)}/toolbox/process/exec/stream`;
508
579
  const WS = globalThis.WebSocket;
509
580
  if (!WS) {
510
581
  throw new Error("WebSocket is not available in this runtime — Node 22+ or a browser is required");
@@ -609,8 +680,8 @@ function openExecStream(baseURL, patToken, sandboxID, options) {
609
680
  done,
610
681
  };
611
682
  }
612
- function openSessionAttach(baseURL, patToken, sandboxID, sessionID, options) {
613
- const wsURL = baseURL.replace(/^http/, "ws") + `/v1/sandboxes/${encodeURIComponent(sandboxID)}/sessions/${encodeURIComponent(sessionID)}/attach`;
683
+ function openSessionAttach(baseURL, versionPrefix, patToken, sandboxID, sessionID, options) {
684
+ const wsURL = baseURL.replace(/^http/, "ws") + `${versionPrefix}/sandboxes/${encodeURIComponent(sandboxID)}/sessions/${encodeURIComponent(sessionID)}/attach`;
614
685
  const WS = globalThis.WebSocket;
615
686
  if (!WS) {
616
687
  throw new Error("WebSocket is not available in this runtime — Node 22+ or a browser is required");
package/dist/types.d.ts CHANGED
@@ -69,6 +69,19 @@ export interface CreateOptions {
69
69
  env?: Record<string, string>;
70
70
  osUser?: string;
71
71
  networkBlockAll?: boolean;
72
+ /**
73
+ * Cap on bytes the sandbox may receive from outside the container before its
74
+ * ingress is dropped via per-IP iptables rule. `0` (default) is unlimited.
75
+ * Limits can be raised or lifted at runtime via `setNetworkLimits`.
76
+ */
77
+ networkBytesInLimit?: number;
78
+ /**
79
+ * Cap on bytes the sandbox may send to outside the container before its
80
+ * egress is dropped. `0` (default) is unlimited. The block reuses the same
81
+ * iptables row as `networkBlockAll`; clearing the quota does not lift an
82
+ * operator-set blanket egress block.
83
+ */
84
+ networkBytesOutLimit?: number;
72
85
  registry?: RegistryAuth;
73
86
  containerCommand?: string[];
74
87
  mounts?: MountSpec[];
@@ -129,6 +142,42 @@ export interface ExposedPort {
129
142
  publicURL: string;
130
143
  createdAt: string;
131
144
  }
145
+ /**
146
+ * Wire protocol an exposure publishes through:
147
+ * - "http": Caddy HTTP reverse proxy at https://<id>-<port>.<domain>.
148
+ * - "tcp": raw caddy-l4 listener on a parent-host port.
149
+ * - "tls": caddy-l4 TLS-SNI route on the shared :443 listener.
150
+ */
151
+ export type ExposeProtocol = "http" | "tcp" | "tls";
152
+ export interface ExposePortOptions {
153
+ /** Defaults to "http" when omitted. */
154
+ protocol?: ExposeProtocol;
155
+ }
156
+ export interface SandboxSnapshot {
157
+ name: string;
158
+ image: string;
159
+ imageID?: string;
160
+ sourceSandboxID: string;
161
+ createdAt: string;
162
+ }
163
+ /**
164
+ * Discriminated result from `exposePort`. Branch on `protocol` to access the
165
+ * fields that are meaningful for the chosen wire protocol — only the raw-TCP
166
+ * variant carries `host` and `hostPort`, which are what native protocol
167
+ * clients (psql, redis-cli, mysql, mongosh, …) need to dial.
168
+ */
169
+ export type ExposeResult = {
170
+ protocol: "http";
171
+ url: string;
172
+ } | {
173
+ protocol: "tcp";
174
+ url: string;
175
+ host: string;
176
+ hostPort: number;
177
+ } | {
178
+ protocol: "tls";
179
+ url: string;
180
+ };
132
181
  export interface Sandbox {
133
182
  id: string;
134
183
  image: string;
@@ -160,6 +209,29 @@ export interface Sandbox {
160
209
  /** GPU configuration this sandbox was created with. Absent means no GPU. */
161
210
  gpus?: GPUOptions;
162
211
  }
212
+ /**
213
+ * Per-sandbox network byte counters and the configured caps that drive the
214
+ * quota enforcer. `bytesIn` is traffic the container received (ingress);
215
+ * `bytesOut` is traffic the container sent (egress). A `*Limit` of `0` means
216
+ * unlimited. `quotaExceeded` flips true the first time the meter crosses
217
+ * either configured cap and stays true until the operator raises the limit.
218
+ */
219
+ export interface NetworkUsage {
220
+ sandboxID: string;
221
+ bytesIn: number;
222
+ bytesOut: number;
223
+ bytesInLimit: number;
224
+ bytesOutLimit: number;
225
+ quotaExceeded: boolean;
226
+ quotaExceededAt?: string;
227
+ /** Absent until the netstats poller has produced at least one sample. */
228
+ lastSampledAt?: string;
229
+ }
230
+ export interface SetNetworkLimitsOptions {
231
+ /** Omit (undefined) to leave unchanged. `0` means unlimited. */
232
+ networkBytesInLimit?: number;
233
+ networkBytesOutLimit?: number;
234
+ }
163
235
  export interface ExecRequest {
164
236
  command: string;
165
237
  workDir?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aerol-ai/aerolvm-sdk",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -13,11 +13,12 @@
13
13
  },
14
14
  "scripts": {
15
15
  "build": "tsc -p tsconfig.json",
16
+ "example:browser-isolation:src": "npx tsx src/examples/browserIsolation.ts",
16
17
  "example:create": "npm run build && node dist/examples/createSandbox.js",
17
18
  "example:create:src": "npx tsx src/examples/createSandbox.ts",
18
19
  "example:delete": "npm run build && node dist/examples/deleteSandbox.js",
19
20
  "example:delete:src": "npx tsx src/examples/deleteSandbox.ts",
20
- "test": "tsc -p tsconfig.test.json --outDir dist-test && node --test dist-test/*.test.js"
21
+ "test": "tsc -p tsconfig.test.json --outDir dist-test && node --test dist-test/*.test.js dist-test/internal/*.test.js"
21
22
  },
22
23
  "devDependencies": {
23
24
  "@types/node": "^25.6.2",