@vercel/sandbox 1.7.1 → 2.0.0-beta.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/dist/sandbox.js CHANGED
@@ -1,74 +1,153 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Sandbox = void 0;
4
- const promises_1 = require("stream/promises");
5
- const fs_1 = require("fs");
6
- const promises_2 = require("fs/promises");
7
- const path_1 = require("path");
8
4
  const api_client_1 = require("./api-client");
9
- const command_1 = require("./command");
5
+ const api_error_1 = require("./api-client/api-error");
10
6
  const get_credentials_1 = require("./utils/get-credentials");
11
7
  const types_1 = require("./utils/types");
12
- const snapshot_1 = require("./snapshot");
13
- const consume_readable_1 = require("./utils/consume-readable");
14
- const convert_sandbox_1 = require("./utils/convert-sandbox");
8
+ const session_1 = require("./session");
9
+ const network_policy_1 = require("./utils/network-policy");
10
+ const promises_1 = require("node:timers/promises");
11
+ function isSandboxStoppedError(err) {
12
+ return err instanceof api_error_1.APIError && err.response.status === 410;
13
+ }
14
+ function isSandboxStoppingError(err) {
15
+ return (err instanceof api_error_1.APIError &&
16
+ err.response.status === 422 &&
17
+ err.json?.error?.code === "sandbox_stopping");
18
+ }
15
19
  /**
16
- * A Sandbox is an isolated Linux MicroVM to run commands in.
17
- *
20
+ * A Sandbox is a persistent, isolated Linux MicroVMs to run commands in.
18
21
  * Use {@link Sandbox.create} or {@link Sandbox.get} to construct.
19
22
  * @hideconstructor
20
23
  */
21
24
  class Sandbox {
22
25
  /**
23
- * Unique ID of this sandbox.
26
+ * The name of this sandbox.
24
27
  */
25
- get sandboxId() {
26
- return this.sandbox.id;
28
+ get name() {
29
+ return this.namedSandbox.name;
27
30
  }
28
- get interactivePort() {
29
- return this.sandbox.interactivePort ?? undefined;
31
+ /**
32
+ * Routes from ports to subdomains.
33
+ * @hidden
34
+ */
35
+ get routes() {
36
+ return this.session.routes;
30
37
  }
31
38
  /**
32
- * The status of the sandbox.
39
+ * Whether this sandbox snapshots on shutdown.
33
40
  */
34
- get status() {
35
- return this.sandbox.status;
41
+ get snapshotOnShutdown() {
42
+ return this.namedSandbox.snapshotOnShutdown;
43
+ }
44
+ /**
45
+ * The region this sandbox runs in.
46
+ */
47
+ get region() {
48
+ return this.namedSandbox.region;
49
+ }
50
+ /**
51
+ * Number of virtual CPUs allocated.
52
+ */
53
+ get vcpus() {
54
+ return this.namedSandbox.vcpus;
55
+ }
56
+ /**
57
+ * Memory allocated in MB.
58
+ */
59
+ get memory() {
60
+ return this.namedSandbox.memory;
61
+ }
62
+ /** Runtime identifier (e.g. "node24", "python3.13"). */
63
+ get runtime() {
64
+ return this.namedSandbox.runtime;
36
65
  }
37
66
  /**
38
- * The creation date of the sandbox.
67
+ * Cumulative egress bytes across all sessions.
68
+ */
69
+ get totalEgressBytes() {
70
+ return this.namedSandbox.totalEgressBytes;
71
+ }
72
+ /**
73
+ * Cumulative ingress bytes across all sessions.
74
+ */
75
+ get totalIngressBytes() {
76
+ return this.namedSandbox.totalIngressBytes;
77
+ }
78
+ /**
79
+ * Cumulative active CPU duration in milliseconds across all sessions.
80
+ */
81
+ get totalActiveCpuDurationMs() {
82
+ return this.namedSandbox.totalActiveCpuDurationMs;
83
+ }
84
+ /**
85
+ * Cumulative wall-clock duration in milliseconds across all sessions.
86
+ */
87
+ get totalDurationMs() {
88
+ return this.namedSandbox.totalDurationMs;
89
+ }
90
+ /**
91
+ * When this sandbox was last updated.
92
+ */
93
+ get updatedAt() {
94
+ return new Date(this.namedSandbox.updatedAt);
95
+ }
96
+ /**
97
+ * When this sandbox was created.
39
98
  */
40
99
  get createdAt() {
41
- return new Date(this.sandbox.createdAt);
100
+ return new Date(this.namedSandbox.createdAt);
101
+ }
102
+ /**
103
+ * Interactive port.
104
+ */
105
+ get interactivePort() {
106
+ return this.session.interactivePort;
42
107
  }
43
108
  /**
44
- * The timeout of the sandbox in milliseconds.
109
+ * The status of the current session.
110
+ */
111
+ get status() {
112
+ return this.session.status;
113
+ }
114
+ /**
115
+ * The default timeout of this sandbox in milliseconds.
45
116
  */
46
117
  get timeout() {
47
- return this.sandbox.timeout;
118
+ return this.namedSandbox.timeout;
48
119
  }
49
120
  /**
50
- * The network policy of the sandbox.
121
+ * The default network policy of this sandbox.
51
122
  */
52
123
  get networkPolicy() {
53
- return this.sandbox.networkPolicy;
124
+ return this.namedSandbox.networkPolicy
125
+ ? (0, network_policy_1.fromAPINetworkPolicy)(this.namedSandbox.networkPolicy)
126
+ : undefined;
54
127
  }
55
128
  /**
56
- * If the sandbox was created from a snapshot, the ID of that snapshot.
129
+ * If the session was created from a snapshot, the ID of that snapshot.
57
130
  */
58
131
  get sourceSnapshotId() {
59
- return this.sandbox.sourceSnapshotId;
132
+ return this.session.sourceSnapshotId;
60
133
  }
61
134
  /**
62
- * The amount of CPU used by the sandbox. Only reported once the VM is stopped.
135
+ * The current snapshot ID of this sandbox, if any.
136
+ */
137
+ get currentSnapshotId() {
138
+ return this.namedSandbox.currentSnapshotId;
139
+ }
140
+ /**
141
+ * The amount of CPU used by the session. Only reported once the VM is stopped.
63
142
  */
64
143
  get activeCpuUsageMs() {
65
- return this.sandbox.activeCpuDurationMs;
144
+ return this.session.activeCpuUsageMs;
66
145
  }
67
146
  /**
68
- * The amount of network data used by the sandbox. Only reported once the VM is stopped.
147
+ * The amount of network data used by the session. Only reported once the VM is stopped.
69
148
  */
70
149
  get networkTransfer() {
71
- return this.sandbox.networkTransfer;
150
+ return this.session.networkTransfer;
72
151
  }
73
152
  /**
74
153
  * Allow to get a list of sandboxes for a team narrowed to the given params.
@@ -82,7 +161,7 @@ class Sandbox {
82
161
  token: credentials.token,
83
162
  fetch: params?.fetch,
84
163
  });
85
- return client.listSandboxes({
164
+ return client.listNamedSandboxes({
86
165
  ...credentials,
87
166
  ...params,
88
167
  });
@@ -107,7 +186,7 @@ class Sandbox {
107
186
  fetch: params?.fetch,
108
187
  });
109
188
  const privateParams = (0, types_1.getPrivateParams)(params);
110
- const sandbox = await client.createSandbox({
189
+ const response = await client.createSandbox({
111
190
  source: params?.source,
112
191
  projectId: credentials.projectId,
113
192
  ports: params?.ports ?? [],
@@ -115,17 +194,22 @@ class Sandbox {
115
194
  resources: params?.resources,
116
195
  runtime: params && "runtime" in params ? params?.runtime : undefined,
117
196
  networkPolicy: params?.networkPolicy,
197
+ env: params?.env,
118
198
  signal: params?.signal,
199
+ name: params?.name,
200
+ snapshotOnShutdown: params?.snapshotOnShutdown,
119
201
  ...privateParams,
120
202
  });
121
203
  return new DisposableSandbox({
122
204
  client,
123
- sandbox: sandbox.json.sandbox,
124
- routes: sandbox.json.routes,
205
+ session: response.json.sandbox,
206
+ namedSandbox: response.json.namedSandbox,
207
+ routes: response.json.routes,
208
+ projectId: credentials.projectId,
125
209
  });
126
210
  }
127
211
  /**
128
- * Retrieve an existing sandbox.
212
+ * Retrieve an existing sandbox and resume its session.
129
213
  *
130
214
  * @param params - Get parameters and optional credentials.
131
215
  * @returns A promise resolving to the {@link Sandbox}.
@@ -137,47 +221,93 @@ class Sandbox {
137
221
  token: credentials.token,
138
222
  fetch: params.fetch,
139
223
  });
140
- const privateParams = (0, types_1.getPrivateParams)(params);
141
- const sandbox = await client.getSandbox({
142
- sandboxId: params.sandboxId,
224
+ const response = await client.getNamedSandbox({
225
+ name: params.name,
226
+ projectId: credentials.projectId,
227
+ resume: params.resume,
143
228
  signal: params.signal,
144
- ...privateParams,
145
229
  });
146
230
  return new Sandbox({
147
231
  client,
148
- sandbox: sandbox.json.sandbox,
149
- routes: sandbox.json.routes,
232
+ session: response.json.sandbox,
233
+ namedSandbox: response.json.namedSandbox,
234
+ routes: response.json.routes,
235
+ projectId: credentials.projectId,
150
236
  });
151
237
  }
152
- constructor({ client, routes, sandbox, }) {
238
+ constructor({ client, routes, session, namedSandbox, projectId, }) {
153
239
  this.client = client;
154
- this.routes = routes;
155
- this.sandbox = (0, convert_sandbox_1.convertSandbox)(sandbox);
240
+ this.session = new session_1.Session({ client, routes, session });
241
+ this.namedSandbox = namedSandbox;
242
+ this.projectId = projectId;
156
243
  }
157
244
  /**
158
- * Get a previously run command by its ID.
245
+ * Get the current session (the running VM) for this sandbox.
159
246
  *
160
- * @param cmdId - ID of the command to retrieve
161
- * @param opts - Optional parameters.
162
- * @param opts.signal - An AbortSignal to cancel the operation.
163
- * @returns A {@link Command} instance representing the command
247
+ * @returns The {@link Session} instance.
164
248
  */
165
- async getCommand(cmdId, opts) {
166
- const command = await this.client.getCommand({
167
- sandboxId: this.sandbox.id,
168
- cmdId,
169
- signal: opts?.signal,
249
+ currentSession() {
250
+ return this.session;
251
+ }
252
+ /**
253
+ * Resume this sandbox by creating a new session via `getNamedSandbox`.
254
+ */
255
+ async resume(signal) {
256
+ const response = await this.client.getNamedSandbox({
257
+ name: this.namedSandbox.name,
258
+ projectId: this.projectId,
259
+ resume: true,
260
+ signal,
170
261
  });
171
- return new command_1.Command({
262
+ this.session = new session_1.Session({
172
263
  client: this.client,
173
- sandboxId: this.sandbox.id,
174
- cmd: command.json.command,
264
+ routes: response.json.routes,
265
+ session: response.json.sandbox,
175
266
  });
176
267
  }
268
+ /**
269
+ * Poll until the current session reaches a terminal state, then resume.
270
+ */
271
+ async waitForStopAndResume(signal) {
272
+ const pollingInterval = 500;
273
+ let status = this.session.status;
274
+ while (status === "stopping" || status === "snapshotting") {
275
+ await (0, promises_1.setTimeout)(pollingInterval, undefined, { signal });
276
+ const poll = await this.client.getSandbox({
277
+ sandboxId: this.session.sessionId,
278
+ signal,
279
+ });
280
+ this.session = new session_1.Session({
281
+ client: this.client,
282
+ routes: poll.json.routes,
283
+ session: poll.json.sandbox,
284
+ });
285
+ status = poll.json.sandbox.status;
286
+ }
287
+ await this.resume(signal);
288
+ }
289
+ /**
290
+ * Execute `fn`, and if the session is stopped/stopping, resume and retry.
291
+ */
292
+ async withResume(fn, signal) {
293
+ try {
294
+ return await fn();
295
+ }
296
+ catch (err) {
297
+ if (isSandboxStoppedError(err)) {
298
+ await this.resume(signal);
299
+ return fn();
300
+ }
301
+ if (isSandboxStoppingError(err)) {
302
+ await this.waitForStopAndResume(signal);
303
+ return fn();
304
+ }
305
+ throw err;
306
+ }
307
+ }
177
308
  async runCommand(commandOrParams, args, opts) {
178
- return typeof commandOrParams === "string"
179
- ? this._runCommand({ cmd: commandOrParams, args, signal: opts?.signal })
180
- : this._runCommand(commandOrParams);
309
+ const signal = typeof commandOrParams === "string" ? opts?.signal : commandOrParams.signal;
310
+ return this.withResume(() => this.session.runCommand(commandOrParams, args, opts), signal);
181
311
  }
182
312
  /**
183
313
  * Internal helper to start a command in the sandbox.
@@ -186,71 +316,8 @@ class Sandbox {
186
316
  * @returns A {@link Command} or {@link CommandFinished}, depending on `detached`.
187
317
  * @internal
188
318
  */
189
- async _runCommand(params) {
190
- const wait = params.detached ? false : true;
191
- const getLogs = (command) => {
192
- if (params.stdout || params.stderr) {
193
- (async () => {
194
- try {
195
- for await (const log of command.logs({ signal: params.signal })) {
196
- if (log.stream === "stdout") {
197
- params.stdout?.write(log.data);
198
- }
199
- else if (log.stream === "stderr") {
200
- params.stderr?.write(log.data);
201
- }
202
- }
203
- }
204
- catch (err) {
205
- if (params.signal?.aborted) {
206
- return;
207
- }
208
- throw err;
209
- }
210
- })();
211
- }
212
- };
213
- if (wait) {
214
- const commandStream = await this.client.runCommand({
215
- sandboxId: this.sandbox.id,
216
- command: params.cmd,
217
- args: params.args ?? [],
218
- cwd: params.cwd,
219
- env: params.env ?? {},
220
- sudo: params.sudo ?? false,
221
- wait: true,
222
- signal: params.signal,
223
- });
224
- const command = new command_1.Command({
225
- client: this.client,
226
- sandboxId: this.sandbox.id,
227
- cmd: commandStream.command,
228
- });
229
- getLogs(command);
230
- const finished = await commandStream.finished;
231
- return new command_1.CommandFinished({
232
- client: this.client,
233
- sandboxId: this.sandbox.id,
234
- cmd: finished,
235
- exitCode: finished.exitCode ?? 0,
236
- });
237
- }
238
- const commandResponse = await this.client.runCommand({
239
- sandboxId: this.sandbox.id,
240
- command: params.cmd,
241
- args: params.args ?? [],
242
- cwd: params.cwd,
243
- env: params.env ?? {},
244
- sudo: params.sudo ?? false,
245
- signal: params.signal,
246
- });
247
- const command = new command_1.Command({
248
- client: this.client,
249
- sandboxId: this.sandbox.id,
250
- cmd: commandResponse.json.command,
251
- });
252
- getLogs(command);
253
- return command;
319
+ async getCommand(cmdId, opts) {
320
+ return this.withResume(() => this.session.getCommand(cmdId, opts), opts?.signal);
254
321
  }
255
322
  /**
256
323
  * Create a directory in the filesystem of this sandbox.
@@ -260,11 +327,7 @@ class Sandbox {
260
327
  * @param opts.signal - An AbortSignal to cancel the operation.
261
328
  */
262
329
  async mkDir(path, opts) {
263
- await this.client.mkDir({
264
- sandboxId: this.sandbox.id,
265
- path: path,
266
- signal: opts?.signal,
267
- });
330
+ return this.withResume(() => this.session.mkDir(path, opts), opts?.signal);
268
331
  }
269
332
  /**
270
333
  * Read a file from the filesystem of this sandbox as a stream.
@@ -275,12 +338,7 @@ class Sandbox {
275
338
  * @returns A promise that resolves to a ReadableStream containing the file contents, or null if file not found
276
339
  */
277
340
  async readFile(file, opts) {
278
- return this.client.readFile({
279
- sandboxId: this.sandbox.id,
280
- path: file.path,
281
- cwd: file.cwd,
282
- signal: opts?.signal,
283
- });
341
+ return this.withResume(() => this.session.readFile(file, opts), opts?.signal);
284
342
  }
285
343
  /**
286
344
  * Read a file from the filesystem of this sandbox as a Buffer.
@@ -291,16 +349,7 @@ class Sandbox {
291
349
  * @returns A promise that resolves to the file contents as a Buffer, or null if file not found
292
350
  */
293
351
  async readFileToBuffer(file, opts) {
294
- const stream = await this.client.readFile({
295
- sandboxId: this.sandbox.id,
296
- path: file.path,
297
- cwd: file.cwd,
298
- signal: opts?.signal,
299
- });
300
- if (stream === null) {
301
- return null;
302
- }
303
- return (0, consume_readable_1.consumeReadable)(stream);
352
+ return this.withResume(() => this.session.readFileToBuffer(file, opts), opts?.signal);
304
353
  }
305
354
  /**
306
355
  * Download a file from the sandbox to the local filesystem.
@@ -313,34 +362,7 @@ class Sandbox {
313
362
  * @returns The absolute path to the written file, or null if the source file was not found
314
363
  */
315
364
  async downloadFile(src, dst, opts) {
316
- if (!src?.path) {
317
- throw new Error("downloadFile: source path is required");
318
- }
319
- if (!dst?.path) {
320
- throw new Error("downloadFile: destination path is required");
321
- }
322
- const stream = await this.client.readFile({
323
- sandboxId: this.sandbox.id,
324
- path: src.path,
325
- cwd: src.cwd,
326
- signal: opts?.signal,
327
- });
328
- if (stream === null) {
329
- return null;
330
- }
331
- try {
332
- const dstPath = (0, path_1.resolve)(dst.cwd ?? "", dst.path);
333
- if (opts?.mkdirRecursive) {
334
- await (0, promises_2.mkdir)((0, path_1.dirname)(dstPath), { recursive: true });
335
- }
336
- await (0, promises_1.pipeline)(stream, (0, fs_1.createWriteStream)(dstPath), {
337
- signal: opts?.signal,
338
- });
339
- return dstPath;
340
- }
341
- finally {
342
- stream.destroy();
343
- }
365
+ return this.withResume(() => this.session.downloadFile(src, dst, opts), opts?.signal);
344
366
  }
345
367
  /**
346
368
  * Write files to the filesystem of this sandbox.
@@ -353,13 +375,7 @@ class Sandbox {
353
375
  * @returns A promise that resolves when the files are written
354
376
  */
355
377
  async writeFiles(files, opts) {
356
- return this.client.writeFiles({
357
- sandboxId: this.sandbox.id,
358
- cwd: this.sandbox.cwd,
359
- extractDir: "/",
360
- files: files,
361
- signal: opts?.signal,
362
- });
378
+ return this.withResume(() => this.session.writeFiles(files, opts), opts?.signal);
363
379
  }
364
380
  /**
365
381
  * Get the public domain of a port of this sandbox.
@@ -369,13 +385,7 @@ class Sandbox {
369
385
  * @throws If the port has no associated route
370
386
  */
371
387
  domain(p) {
372
- const route = this.routes.find(({ port }) => port == p);
373
- if (route) {
374
- return `https://${route.subdomain}.vercel.run`;
375
- }
376
- else {
377
- throw new Error(`No route for port ${p}`);
378
- }
388
+ return this.session.domain(p);
379
389
  }
380
390
  /**
381
391
  * Stop the sandbox.
@@ -386,13 +396,7 @@ class Sandbox {
386
396
  * @returns The sandbox metadata at the time the stop was acknowledged, or after fully stopped if `blocking` is true.
387
397
  */
388
398
  async stop(opts) {
389
- const response = await this.client.stopSandbox({
390
- sandboxId: this.sandbox.id,
391
- signal: opts?.signal,
392
- blocking: opts?.blocking,
393
- });
394
- this.sandbox = (0, convert_sandbox_1.convertSandbox)(response.json.sandbox);
395
- return this.sandbox;
399
+ return this.session.stop(opts);
396
400
  }
397
401
  /**
398
402
  * Update the network policy for this sandbox.
@@ -426,14 +430,7 @@ class Sandbox {
426
430
  * await sandbox.updateNetworkPolicy("deny-all");
427
431
  */
428
432
  async updateNetworkPolicy(networkPolicy, opts) {
429
- const response = await this.client.updateNetworkPolicy({
430
- sandboxId: this.sandbox.id,
431
- networkPolicy: networkPolicy,
432
- signal: opts?.signal,
433
- });
434
- // Update the internal sandbox metadata with the new timeout value
435
- this.sandbox = (0, convert_sandbox_1.convertSandbox)(response.json.sandbox);
436
- return this.sandbox.networkPolicy;
433
+ return this.withResume(() => this.session.updateNetworkPolicy(networkPolicy, opts), opts?.signal);
437
434
  }
438
435
  /**
439
436
  * Extend the timeout of the sandbox by the specified duration.
@@ -452,13 +449,7 @@ class Sandbox {
452
449
  * await sandbox.extendTimeout(ms('5m'));
453
450
  */
454
451
  async extendTimeout(duration, opts) {
455
- const response = await this.client.extendTimeout({
456
- sandboxId: this.sandbox.id,
457
- duration,
458
- signal: opts?.signal,
459
- });
460
- // Update the internal sandbox metadata with the new timeout value
461
- this.sandbox = (0, convert_sandbox_1.convertSandbox)(response.json.sandbox);
452
+ return this.withResume(() => this.session.extendTimeout(duration, opts), opts?.signal);
462
453
  }
463
454
  /**
464
455
  * Create a snapshot from this currently running sandbox. New sandboxes can
@@ -472,15 +463,70 @@ class Sandbox {
472
463
  * @returns A promise that resolves to the Snapshot instance
473
464
  */
474
465
  async snapshot(opts) {
475
- const response = await this.client.createSnapshot({
476
- sandboxId: this.sandbox.id,
477
- expiration: opts?.expiration,
466
+ return this.withResume(() => this.session.snapshot(opts), opts?.signal);
467
+ }
468
+ /**
469
+ * Update the named sandbox configuration. Only provided fields are modified.
470
+ *
471
+ * @param params - Fields to update.
472
+ * @param opts - Optional abort signal.
473
+ */
474
+ async update(params, opts) {
475
+ const response = await this.client.updateNamedSandbox({
476
+ name: this.namedSandbox.name,
477
+ projectId: this.projectId,
478
+ resources: params.resources,
479
+ runtime: params.runtime,
480
+ timeout: params.timeout,
481
+ networkPolicy: params.networkPolicy,
478
482
  signal: opts?.signal,
479
483
  });
480
- this.sandbox = (0, convert_sandbox_1.convertSandbox)(response.json.sandbox);
481
- return new snapshot_1.Snapshot({
482
- client: this.client,
483
- snapshot: response.json.snapshot,
484
+ this.namedSandbox = response.json.namedSandbox;
485
+ }
486
+ /**
487
+ * Delete this named sandbox.
488
+ *
489
+ * After deletion the instance becomes inert — all further API calls will
490
+ * throw immediately.
491
+ */
492
+ async delete(opts) {
493
+ await this.client.deleteNamedSandbox({
494
+ name: this.namedSandbox.name,
495
+ projectId: this.projectId,
496
+ preserveSnapshots: opts?.preserveSnapshots,
497
+ signal: opts?.signal,
498
+ });
499
+ }
500
+ /**
501
+ * List sessions (VMs) that have been created for this named sandbox.
502
+ *
503
+ * @param params - Optional pagination parameters.
504
+ * @returns The list of sessions and pagination metadata.
505
+ */
506
+ async listSessions(params) {
507
+ return this.client.listSandboxes({
508
+ projectId: this.projectId,
509
+ name: this.namedSandbox.name,
510
+ limit: params?.limit,
511
+ since: params?.since,
512
+ until: params?.until,
513
+ signal: params?.signal,
514
+ });
515
+ }
516
+ /**
517
+ * List snapshots that belong to this named sandbox.
518
+ *
519
+ * @param params - Optional pagination parameters.
520
+ * @returns The list of snapshots and pagination metadata.
521
+ */
522
+ async listSnapshots(params) {
523
+ return this.client.listSnapshots({
524
+ projectId: this.projectId,
525
+ name: this.namedSandbox.name,
526
+ limit: params?.limit,
527
+ since: params?.since,
528
+ until: params?.until,
529
+ signal: params?.signal,
484
530
  });
485
531
  }
486
532
  }