@vercel/sandbox 0.0.21 → 0.0.23

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
@@ -24,6 +24,12 @@ class Sandbox {
24
24
  get status() {
25
25
  return this.sandbox.status;
26
26
  }
27
+ /**
28
+ * The timeout of the sandbox in milliseconds.
29
+ */
30
+ get timeout() {
31
+ return this.sandbox.timeout;
32
+ }
27
33
  /**
28
34
  * Allow to get a list of sandboxes for a team narrowed to the given params.
29
35
  * It returns both the sandboxes and the pagination metadata to allow getting
@@ -35,7 +41,10 @@ class Sandbox {
35
41
  teamId: credentials.teamId,
36
42
  token: credentials.token,
37
43
  });
38
- return client.listSandboxes(params);
44
+ return client.listSandboxes({
45
+ ...params,
46
+ signal: params.signal,
47
+ });
39
48
  }
40
49
  /**
41
50
  * Create a new sandbox.
@@ -57,6 +66,7 @@ class Sandbox {
57
66
  timeout: params?.timeout,
58
67
  resources: params?.resources,
59
68
  runtime: params?.runtime,
69
+ signal: params?.signal,
60
70
  ...privateParams,
61
71
  });
62
72
  return new Sandbox({
@@ -79,6 +89,7 @@ class Sandbox {
79
89
  });
80
90
  const sandbox = await client.getSandbox({
81
91
  sandboxId: params.sandboxId,
92
+ signal: params.signal,
82
93
  });
83
94
  return new Sandbox({
84
95
  client,
@@ -102,12 +113,15 @@ class Sandbox {
102
113
  * Get a previously run command by its ID.
103
114
  *
104
115
  * @param cmdId - ID of the command to retrieve
116
+ * @param opts - Optional parameters.
117
+ * @param opts.signal - An AbortSignal to cancel the operation.
105
118
  * @returns A {@link Command} instance representing the command
106
119
  */
107
- async getCommand(cmdId) {
120
+ async getCommand(cmdId, opts) {
108
121
  const command = await this.client.getCommand({
109
122
  sandboxId: this.sandbox.id,
110
123
  cmdId,
124
+ signal: opts?.signal,
111
125
  });
112
126
  return new command_1.Command({
113
127
  client: this.client,
@@ -115,9 +129,9 @@ class Sandbox {
115
129
  cmd: command.json.command,
116
130
  });
117
131
  }
118
- async runCommand(commandOrParams, args) {
132
+ async runCommand(commandOrParams, args, opts) {
119
133
  return typeof commandOrParams === "string"
120
- ? this._runCommand({ cmd: commandOrParams, args })
134
+ ? this._runCommand({ cmd: commandOrParams, args, signal: opts?.signal })
121
135
  : this._runCommand(commandOrParams);
122
136
  }
123
137
  /**
@@ -135,6 +149,7 @@ class Sandbox {
135
149
  cwd: params.cwd,
136
150
  env: params.env ?? {},
137
151
  sudo: params.sudo ?? false,
152
+ signal: params.signal,
138
153
  });
139
154
  const command = new command_1.Command({
140
155
  client: this.client,
@@ -143,7 +158,7 @@ class Sandbox {
143
158
  });
144
159
  if (params.stdout || params.stderr) {
145
160
  (async () => {
146
- for await (const log of command.logs()) {
161
+ for await (const log of command.logs({ signal: params.signal })) {
147
162
  if (log.stream === "stdout") {
148
163
  params.stdout?.write(log.data);
149
164
  }
@@ -153,30 +168,36 @@ class Sandbox {
153
168
  }
154
169
  })();
155
170
  }
156
- return params.detached ? command : command.wait();
171
+ return params.detached ? command : command.wait({ signal: params.signal });
157
172
  }
158
173
  /**
159
174
  * Create a directory in the filesystem of this sandbox.
160
175
  *
161
176
  * @param path - Path of the directory to create
177
+ * @param opts - Optional parameters.
178
+ * @param opts.signal - An AbortSignal to cancel the operation.
162
179
  */
163
- async mkDir(path) {
180
+ async mkDir(path, opts) {
164
181
  await this.client.mkDir({
165
182
  sandboxId: this.sandbox.id,
166
183
  path: path,
184
+ signal: opts?.signal,
167
185
  });
168
186
  }
169
187
  /**
170
188
  * Read a file from the filesystem of this sandbox.
171
189
  *
172
190
  * @param file - File to read, with path and optional cwd
191
+ * @param opts - Optional parameters.
192
+ * @param opts.signal - An AbortSignal to cancel the operation.
173
193
  * @returns A promise that resolves to a ReadableStream containing the file contents
174
194
  */
175
- async readFile(file) {
195
+ async readFile(file, opts) {
176
196
  return this.client.readFile({
177
197
  sandboxId: this.sandbox.id,
178
198
  path: file.path,
179
199
  cwd: file.cwd,
200
+ signal: opts?.signal,
180
201
  });
181
202
  }
182
203
  /**
@@ -185,14 +206,17 @@ class Sandbox {
185
206
  * Writes files using the `vercel-sandbox` user.
186
207
  *
187
208
  * @param files - Array of files with path and stream/buffer contents
209
+ * @param opts - Optional parameters.
210
+ * @param opts.signal - An AbortSignal to cancel the operation.
188
211
  * @returns A promise that resolves when the files are written
189
212
  */
190
- async writeFiles(files) {
213
+ async writeFiles(files, opts) {
191
214
  return this.client.writeFiles({
192
215
  sandboxId: this.sandbox.id,
193
216
  cwd: this.sandbox.cwd,
194
217
  extractDir: "/",
195
218
  files: files,
219
+ signal: opts?.signal,
196
220
  });
197
221
  }
198
222
  /**
@@ -214,12 +238,35 @@ class Sandbox {
214
238
  /**
215
239
  * Stop the sandbox.
216
240
  *
241
+ * @param opts - Optional parameters.
242
+ * @param opts.signal - An AbortSignal to cancel the operation.
217
243
  * @returns A promise that resolves when the sandbox is stopped
218
244
  */
219
- async stop() {
245
+ async stop(opts) {
220
246
  await this.client.stopSandbox({
221
247
  sandboxId: this.sandbox.id,
248
+ signal: opts?.signal,
249
+ });
250
+ }
251
+ /**
252
+ * Extend the timeout of the sandbox by the specified duration.
253
+ *
254
+ * This allows you to extend the lifetime of a sandbox up until the maximum
255
+ * execution timeout for your plan.
256
+ *
257
+ * @param duration - The duration in milliseconds to extend the timeout by
258
+ * @param opts - Optional parameters.
259
+ * @param opts.signal - An AbortSignal to cancel the operation.
260
+ * @returns A promise that resolves when the timeout is extended
261
+ */
262
+ async extendTimeout(duration, opts) {
263
+ const response = await this.client.extendTimeout({
264
+ sandboxId: this.sandbox.id,
265
+ duration,
266
+ signal: opts?.signal,
222
267
  });
268
+ // Update the internal sandbox metadata with the new timeout value
269
+ this.sandbox = response.json.sandbox;
223
270
  }
224
271
  }
225
272
  exports.Sandbox = Sandbox;
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.0.21";
1
+ export declare const VERSION = "0.0.23";
package/dist/version.js CHANGED
@@ -2,4 +2,4 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.VERSION = void 0;
4
4
  // Autogenerated by inject-version.ts
5
- exports.VERSION = "0.0.21";
5
+ exports.VERSION = "0.0.23";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/sandbox",
3
- "version": "0.0.21",
3
+ "version": "0.0.23",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -12,6 +12,7 @@ import {
12
12
  EmptyResponse,
13
13
  LogLine,
14
14
  SandboxesResponse,
15
+ ExtendTimeoutResponse,
15
16
  } from "./validators";
16
17
  import { APIError } from "./api-error";
17
18
  import { FileWriter } from "./file-writer";
@@ -71,10 +72,12 @@ export class APIClient extends BaseClient {
71
72
  });
72
73
  }
73
74
 
74
- async getSandbox(params: { sandboxId: string }) {
75
+ async getSandbox(params: { sandboxId: string; signal?: AbortSignal }) {
75
76
  return parseOrThrow(
76
77
  SandboxAndRoutesResponse,
77
- await this.request(`/v1/sandboxes/${params.sandboxId}`),
78
+ await this.request(`/v1/sandboxes/${params.sandboxId}`, {
79
+ signal: params.signal,
80
+ }),
78
81
  );
79
82
  }
80
83
 
@@ -95,6 +98,7 @@ export class APIClient extends BaseClient {
95
98
  timeout?: number;
96
99
  resources?: { vcpus: number };
97
100
  runtime?: "node22" | "python3.13" | (string & {});
101
+ signal?: AbortSignal;
98
102
  }>,
99
103
  ) {
100
104
  const privateParams = getPrivateParams(params);
@@ -111,6 +115,7 @@ export class APIClient extends BaseClient {
111
115
  runtime: params.runtime,
112
116
  ...privateParams,
113
117
  }),
118
+ signal: params.signal,
114
119
  }),
115
120
  );
116
121
  }
@@ -122,6 +127,7 @@ export class APIClient extends BaseClient {
122
127
  args: string[];
123
128
  env: Record<string, string>;
124
129
  sudo: boolean;
130
+ signal?: AbortSignal;
125
131
  }) {
126
132
  return parseOrThrow(
127
133
  CommandResponse,
@@ -134,6 +140,7 @@ export class APIClient extends BaseClient {
134
140
  env: params.env,
135
141
  sudo: params.sudo,
136
142
  }),
143
+ signal: params.signal,
137
144
  }),
138
145
  );
139
146
  }
@@ -142,44 +149,58 @@ export class APIClient extends BaseClient {
142
149
  sandboxId: string;
143
150
  cmdId: string;
144
151
  wait: true;
152
+ signal?: AbortSignal;
145
153
  }): Promise<Parsed<z.infer<typeof CommandFinishedResponse>>>;
146
154
  async getCommand(params: {
147
155
  sandboxId: string;
148
156
  cmdId: string;
149
157
  wait?: boolean;
158
+ signal?: AbortSignal;
150
159
  }): Promise<Parsed<z.infer<typeof CommandResponse>>>;
151
160
  async getCommand(params: {
152
161
  sandboxId: string;
153
162
  cmdId: string;
154
163
  wait?: boolean;
164
+ signal?: AbortSignal;
155
165
  }) {
156
166
  return params.wait
157
167
  ? parseOrThrow(
158
168
  CommandFinishedResponse,
159
169
  await this.request(
160
170
  `/v1/sandboxes/${params.sandboxId}/cmd/${params.cmdId}`,
161
- { query: { wait: "true" } },
171
+ { signal: params.signal, query: { wait: "true" } },
162
172
  ),
163
173
  )
164
174
  : parseOrThrow(
165
175
  CommandResponse,
166
176
  await this.request(
167
177
  `/v1/sandboxes/${params.sandboxId}/cmd/${params.cmdId}`,
178
+ { signal: params.signal },
168
179
  ),
169
180
  );
170
181
  }
171
182
 
172
- async mkDir(params: { sandboxId: string; path: string; cwd?: string }) {
183
+ async mkDir(params: {
184
+ sandboxId: string;
185
+ path: string;
186
+ cwd?: string;
187
+ signal?: AbortSignal;
188
+ }) {
173
189
  return parseOrThrow(
174
190
  EmptyResponse,
175
191
  await this.request(`/v1/sandboxes/${params.sandboxId}/fs/mkdir`, {
176
192
  method: "POST",
177
193
  body: JSON.stringify({ path: params.path, cwd: params.cwd }),
194
+ signal: params.signal,
178
195
  }),
179
196
  );
180
197
  }
181
198
 
182
- getFileWriter(params: { sandboxId: string; extractDir: string }) {
199
+ getFileWriter(params: {
200
+ sandboxId: string;
201
+ extractDir: string;
202
+ signal?: AbortSignal;
203
+ }) {
183
204
  const writer = new FileWriter();
184
205
  return {
185
206
  response: (async () => {
@@ -190,6 +211,7 @@ export class APIClient extends BaseClient {
190
211
  "x-cwd": params.extractDir,
191
212
  },
192
213
  body: await consumeReadable(writer.readable),
214
+ signal: params.signal,
193
215
  });
194
216
  })(),
195
217
  writer,
@@ -217,6 +239,7 @@ export class APIClient extends BaseClient {
217
239
  * @example 1540095775951
218
240
  */
219
241
  until?: number | Date;
242
+ signal?: AbortSignal;
220
243
  }) {
221
244
  return parseOrThrow(
222
245
  SandboxesResponse,
@@ -234,6 +257,7 @@ export class APIClient extends BaseClient {
234
257
  : params.until?.getTime(),
235
258
  },
236
259
  method: "GET",
260
+ signal: params.signal,
237
261
  }),
238
262
  );
239
263
  }
@@ -243,10 +267,12 @@ export class APIClient extends BaseClient {
243
267
  cwd: string;
244
268
  files: { path: string; content: Buffer }[];
245
269
  extractDir: string;
270
+ signal?: AbortSignal;
246
271
  }) {
247
272
  const { writer, response } = this.getFileWriter({
248
273
  sandboxId: params.sandboxId,
249
274
  extractDir: params.extractDir,
275
+ signal: params.signal,
250
276
  });
251
277
 
252
278
  for (const file of params.files) {
@@ -268,12 +294,14 @@ export class APIClient extends BaseClient {
268
294
  sandboxId: string;
269
295
  path: string;
270
296
  cwd?: string;
297
+ signal?: AbortSignal;
271
298
  }): Promise<NodeJS.ReadableStream | null> {
272
299
  const response = await this.request(
273
300
  `/v1/sandboxes/${params.sandboxId}/fs/read`,
274
301
  {
275
302
  method: "POST",
276
303
  body: JSON.stringify({ path: params.path, cwd: params.cwd }),
304
+ signal: params.signal,
277
305
  },
278
306
  );
279
307
 
@@ -292,6 +320,7 @@ export class APIClient extends BaseClient {
292
320
  sandboxId: string;
293
321
  commandId: string;
294
322
  signal: number;
323
+ abortSignal?: AbortSignal;
295
324
  }) {
296
325
  return parseOrThrow(
297
326
  CommandResponse,
@@ -300,6 +329,7 @@ export class APIClient extends BaseClient {
300
329
  {
301
330
  method: "POST",
302
331
  body: JSON.stringify({ signal: params.signal }),
332
+ signal: params.abortSignal,
303
333
  },
304
334
  ),
305
335
  );
@@ -356,11 +386,28 @@ export class APIClient extends BaseClient {
356
386
 
357
387
  async stopSandbox(params: {
358
388
  sandboxId: string;
389
+ signal?: AbortSignal;
359
390
  }): Promise<Parsed<z.infer<typeof SandboxResponse>>> {
360
391
  const url = `/v1/sandboxes/${params.sandboxId}/stop`;
361
392
  return parseOrThrow(
362
393
  SandboxResponse,
363
- await this.request(url, { method: "POST" }),
394
+ await this.request(url, { method: "POST", signal: params.signal }),
395
+ );
396
+ }
397
+
398
+ async extendTimeout(params: {
399
+ sandboxId: string;
400
+ duration: number;
401
+ signal?: AbortSignal;
402
+ }): Promise<Parsed<z.infer<typeof ExtendTimeoutResponse>>> {
403
+ const url = `/v1/sandboxes/${params.sandboxId}/extend-timeout`;
404
+ return parseOrThrow(
405
+ ExtendTimeoutResponse,
406
+ await this.request(url, {
407
+ method: "POST",
408
+ body: JSON.stringify({ duration: params.duration }),
409
+ signal: params.signal,
410
+ }),
364
411
  );
365
412
  }
366
413
  }
@@ -55,6 +55,7 @@ export class BaseClient {
55
55
  : opts?.headers,
56
56
  // @ts-expect-error Node.js' and undici's Agent have different types
57
57
  dispatcher: this.agent,
58
+ signal: opts?.signal,
58
59
  });
59
60
 
60
61
  if (this.debug) {
@@ -89,3 +89,7 @@ export const SandboxesResponse = z.object({
89
89
  sandboxes: z.array(Sandbox),
90
90
  pagination: Pagination,
91
91
  });
92
+
93
+ export const ExtendTimeoutResponse = z.object({
94
+ sandbox: Sandbox,
95
+ });
package/src/command.ts CHANGED
@@ -112,13 +112,18 @@ export class Command {
112
112
  * }
113
113
  * ```
114
114
  *
115
+ * @param params - Optional parameters.
116
+ * @param params.signal - An AbortSignal to cancel waiting.
115
117
  * @returns A {@link CommandFinished} instance with populated exit code.
116
118
  */
117
- async wait() {
119
+ async wait(params?: { signal?: AbortSignal }) {
120
+ params?.signal?.throwIfAborted();
121
+
118
122
  const command = await this.client.getCommand({
119
123
  sandboxId: this.sandboxId,
120
124
  cmdId: this.cmd.id,
121
125
  wait: true,
126
+ signal: params?.signal,
122
127
  });
123
128
 
124
129
  return new CommandFinished({
@@ -136,11 +141,16 @@ export class Command {
136
141
  * not output valid Unicode.
137
142
  *
138
143
  * @param stream - The output stream to read: "stdout", "stderr", or "both".
144
+ * @param opts - Optional parameters.
145
+ * @param opts.signal - An AbortSignal to cancel output streaming.
139
146
  * @returns The output of the specified stream(s) as a string.
140
147
  */
141
- async output(stream: "stdout" | "stderr" | "both" = "both") {
148
+ async output(
149
+ stream: "stdout" | "stderr" | "both" = "both",
150
+ opts?: { signal?: AbortSignal },
151
+ ) {
142
152
  let data = "";
143
- for await (const log of this.logs()) {
153
+ for await (const log of this.logs({ signal: opts?.signal })) {
144
154
  if (stream === "both" || log.stream === stream) {
145
155
  data += log.data;
146
156
  }
@@ -154,10 +164,12 @@ export class Command {
154
164
  * NOTE: This may throw string conversion errors if the command does
155
165
  * not output valid Unicode.
156
166
  *
167
+ * @param opts - Optional parameters.
168
+ * @param opts.signal - An AbortSignal to cancel output streaming.
157
169
  * @returns The standard output of the command.
158
170
  */
159
- async stdout() {
160
- return this.output("stdout");
171
+ async stdout(opts?: { signal?: AbortSignal }) {
172
+ return this.output("stdout", opts);
161
173
  }
162
174
 
163
175
  /**
@@ -166,24 +178,28 @@ export class Command {
166
178
  * NOTE: This may throw string conversion errors if the command does
167
179
  * not output valid Unicode.
168
180
  *
181
+ * @param opts - Optional parameters.
182
+ * @param opts.signal - An AbortSignal to cancel output streaming.
169
183
  * @returns The standard error output of the command.
170
184
  */
171
- async stderr() {
172
- return this.output("stderr");
185
+ async stderr(opts?: { signal?: AbortSignal }) {
186
+ return this.output("stderr", opts);
173
187
  }
174
188
 
175
189
  /**
176
190
  * Kill a running command in a sandbox.
177
191
  *
178
- * @param params - commandId and the signal to send the running process.
179
- * Defaults to SIGTERM.
192
+ * @param signal - The signal to send the running process. Defaults to SIGTERM.
193
+ * @param opts - Optional parameters.
194
+ * @param opts.abortSignal - An AbortSignal to cancel the kill operation.
180
195
  * @returns Promise<void>.
181
196
  */
182
- async kill(signal?: Signal) {
197
+ async kill(signal?: Signal, opts?: { abortSignal?: AbortSignal }) {
183
198
  await this.client.killCommand({
184
199
  sandboxId: this.sandboxId,
185
200
  commandId: this.cmd.id,
186
201
  signal: resolveSignal(signal ?? "SIGTERM"),
202
+ abortSignal: opts?.abortSignal,
187
203
  });
188
204
  }
189
205
  }
@@ -1,6 +1,8 @@
1
1
  import { it, beforeEach, afterEach, expect } from "vitest";
2
2
  import { consumeReadable } from "./utils/consume-readable";
3
3
  import { Sandbox } from "./sandbox";
4
+ import { APIError } from "./api-client/api-error";
5
+ import ms from "ms";
4
6
 
5
7
  const PORTS = [3000, 4000];
6
8
  let sandbox: Sandbox;
@@ -64,3 +66,26 @@ for (const port of ports) {
64
66
 
65
67
  await server.kill();
66
68
  });
69
+
70
+ it("allows extending the sandbox timeout", async () => {
71
+ const originalTimeout = sandbox.timeout;
72
+ const extensionDuration = ms("5m");
73
+
74
+ await sandbox.extendTimeout(extensionDuration);
75
+ expect(sandbox.timeout).toEqual(originalTimeout + extensionDuration);
76
+ });
77
+
78
+ it("raises an error when the timeout cannot be updated", async () => {
79
+ try {
80
+ await sandbox.extendTimeout(ms("5d"));
81
+ expect.fail("Expected extendTimeout to throw an error");
82
+ } catch (error) {
83
+ expect(error).toBeInstanceOf(APIError);
84
+ expect(error).toMatchObject({
85
+ response: { status: 400 },
86
+ json: {
87
+ error: { code: "sandbox_timeout_invalid" },
88
+ },
89
+ });
90
+ }
91
+ });