@vercel/sandbox 0.0.9 → 0.0.10

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.
@@ -1,4 +1,4 @@
1
1
 
2
- > @vercel/sandbox@0.0.9 build /home/runner/work/sandbox-sdk/sandbox-sdk/packages/sandbox
2
+ > @vercel/sandbox@0.0.10 build /home/runner/work/sandbox-sdk/sandbox-sdk/packages/sandbox
3
3
  > tsc
4
4
 
@@ -1,4 +1,4 @@
1
1
 
2
- > @vercel/sandbox@0.0.9 typecheck /home/runner/work/sandbox-sdk/sandbox-sdk/packages/sandbox
2
+ > @vercel/sandbox@0.0.10 typecheck /home/runner/work/sandbox-sdk/sandbox-sdk/packages/sandbox
3
3
  > tsc --noEmit
4
4
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @vercel/sandbox
2
2
 
3
+ ## 0.0.10
4
+
5
+ ### Patch Changes
6
+
7
+ - Add `readFile` and fix a bug writing files to a Sandbox ([#54](https://github.com/vercel/sandbox-sdk/pull/54))
8
+
9
+ - Remove unused `routes` parameter from getSandbox ([#59](https://github.com/vercel/sandbox-sdk/pull/59))
10
+
3
11
  ## 0.0.9
4
12
 
5
13
  ### Patch Changes
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  Vercel Sandbox allows you to run arbitrary code in isolated, ephemeral Linux
2
- VMs. This product is in private beta.
2
+ VMs. This product is in [beta](https://vercel.com/docs/release-phases#beta).
3
3
 
4
4
  ## What is a sandbox?
5
5
 
@@ -9,14 +9,9 @@ infrastructure][hive] that powers 1M+ builds a day at Vercel.
9
9
 
10
10
  ## Getting started
11
11
 
12
- Vercel Sandbox is in private beta. These examples will not work unless these
13
- APIs are enabled for your team.
12
+ Vercel Sandbox is in [beta](https://vercel.com/docs/release-phases#beta).
14
13
 
15
- - Go to your team settings, and copy the team ID.
16
- - Go to a project's settings, and copy the project ID.
17
- - Go to your Vercel account settings and [create a token][create-token]. Make
18
- sure it is scoped to the team ID from the previous step.
19
- - Create a new project:
14
+ To get started, create a new project:
20
15
 
21
16
  ```sh
22
17
  mkdir sandbox-test
@@ -26,6 +21,18 @@ pnpm add @vercel/sandbox ms
26
21
  pnpm add -D @types/ms @types/node
27
22
  ```
28
23
 
24
+ Link it to Vercel:
25
+
26
+ ```sh
27
+ vercel link
28
+ ```
29
+
30
+ Pull its environment variables:
31
+
32
+ ```sh
33
+ vercel env pull
34
+ ```
35
+
29
36
  Now create `next-dev.ts`:
30
37
 
31
38
  {@includeCode ../cli/src/example-next.ts}
@@ -33,7 +40,7 @@ Now create `next-dev.ts`:
33
40
  Run it like this:
34
41
 
35
42
  ```sh
36
- VERCEL_TEAM_ID=<team_id> VERCEL_TOKEN=<token> VERCEL_PROJECT_ID=<project_id> node --experimental-strip-types ./next-dev.ts
43
+ node --env-file .env.local --experimental-strip-types ./next-dev.ts
37
44
  ```
38
45
 
39
46
  This will:
@@ -45,12 +52,53 @@ This will:
45
52
 
46
53
  All while streaming logs to your local terminal.
47
54
 
55
+ ## Authentication
56
+
57
+ ### Vercel OIDC token
58
+
59
+ The SDK uses Vercel OIDC tokens to authenticate whenever available. This is the
60
+ most straightforward and recommended way to authenticate.
61
+
62
+ When developing locally, you can download a development token to `.env.local`
63
+ using `vercel env pull`. After 12 hours the development token expires, meaning
64
+ you will have to call `vercel env pull` again.
65
+
66
+ In production, Vercel manages token expiration for you.
67
+
68
+ ### Access token
69
+
70
+ If you want to use the SDK from an environment where `VERCEL_OIDC_TOKEN` is
71
+ unavailable, you can also authenticate using an access token:
72
+
73
+ - Go to your team settings, and copy the team ID.
74
+ - Go to a project's settings, and copy the project ID.
75
+ - Go to your Vercel account settings and [create a token][create-token]. Make
76
+ sure it is scoped to the team ID from the previous step.
77
+
78
+ Set your team ID, project ID, and token to the environment variables
79
+ `VERCEL_TEAM_ID`, `VERCEL_PROJECT_ID`, and `VERCEL_TOKEN`. Then pass these to
80
+ the `create` method:
81
+
82
+ ```ts
83
+ const sandbox = await Sandbox.create({
84
+ teamId: process.env.VERCEL_TEAM_ID!,
85
+ projectId: process.env.VERCEL_PROJECT_ID!,
86
+ token: process.env.VERCEL_TOKEN!,
87
+ source: {
88
+ url: "https://github.com/vercel/sandbox-example-next.git",
89
+ type: "git",
90
+ },
91
+ resources: { vcpus: 4 },
92
+ timeout: ms("5m"),
93
+ ports: [3000],
94
+ runtime: "node22",
95
+ });
96
+ ```
97
+
48
98
  ## Limitations
49
99
 
50
- - Sandbox only supports cloning public repositories at the moment.
51
100
  - `sudo` is not available. User code is executed as the `vercel-sandbox` user.
52
101
  - Max resources: 8 vCPUs. You will get 2048 MB of memory per vCPU.
53
- - All APIs of the SDK are subject to change. **We make no stability promises.**
54
102
 
55
103
  ## System
56
104
 
@@ -78,7 +126,8 @@ whois
78
126
  zstd
79
127
  ```
80
128
 
81
- - The `node22` system ships a Node 22 runtime under `/vercel/runtimes/node22`.
129
+ - The `node22` image ships a Node 22 runtime under `/vercel/runtimes/node22`.
130
+ - The `python3.13` image ships a Python 3.13 runtime under `/vercel/runtimes/python`.
82
131
  - User code is executed as the `vercel-sandbox` user.
83
132
  - `/vercel/sandbox` is writable.
84
133
 
@@ -43,6 +43,8 @@ export declare class APIClient extends BaseClient {
43
43
  url: string;
44
44
  depth?: number;
45
45
  revision?: string;
46
+ username?: string;
47
+ password?: string;
46
48
  } | {
47
49
  type: "tarball";
48
50
  url: string;
@@ -10,6 +10,7 @@ const api_error_1 = require("./api-error");
10
10
  const file_writer_1 = require("./file-writer");
11
11
  const lru_cache_1 = require("lru-cache");
12
12
  const version_1 = require("../version");
13
+ const consume_readable_1 = require("../utils/consume-readable");
13
14
  const jsonlines_1 = __importDefault(require("jsonlines"));
14
15
  const os_1 = __importDefault(require("os"));
15
16
  const ms_1 = __importDefault(require("ms"));
@@ -82,11 +83,13 @@ class APIClient extends base_client_1.BaseClient {
82
83
  getFileWriter(params) {
83
84
  const writer = new file_writer_1.FileWriter();
84
85
  return {
85
- response: this.request(`/v1/sandboxes/${params.sandboxId}/fs/write`, {
86
- method: "POST",
87
- headers: { "content-type": "application/gzip" },
88
- body: writer.readable,
89
- }),
86
+ response: (async () => {
87
+ return this.request(`/v1/sandboxes/${params.sandboxId}/fs/write`, {
88
+ method: "POST",
89
+ headers: { "content-type": "application/gzip" },
90
+ body: await (0, consume_readable_1.consumeReadable)(writer.readable),
91
+ });
92
+ })(),
90
93
  writer,
91
94
  };
92
95
  }
@@ -97,7 +100,7 @@ class APIClient extends base_client_1.BaseClient {
97
100
  for (const file of params.files) {
98
101
  await writer.addFile({ name: file.path, content: file.stream });
99
102
  }
100
- await writer.end();
103
+ writer.end();
101
104
  await (0, base_client_1.parseOrThrow)(validators_1.EmptyResponse, await response);
102
105
  }
103
106
  async readFile(params) {
package/dist/sandbox.d.ts CHANGED
@@ -19,6 +19,13 @@ export interface CreateSandboxParams {
19
19
  url: string;
20
20
  depth?: number;
21
21
  revision?: string;
22
+ } | {
23
+ type: "git";
24
+ url: string;
25
+ username: string;
26
+ password: string;
27
+ depth?: number;
28
+ revision?: string;
22
29
  } | {
23
30
  type: "tarball";
24
31
  url: string;
@@ -48,13 +55,6 @@ export interface CreateSandboxParams {
48
55
  }
49
56
  /** @inline */
50
57
  interface GetSandboxParams {
51
- /**
52
- * Port-to-subdomain route mappings.
53
- */
54
- routes: Array<{
55
- subdomain: string;
56
- port: number;
57
- }>;
58
58
  /**
59
59
  * Unique identifier of the sandbox.
60
60
  */
@@ -183,6 +183,16 @@ export declare class Sandbox {
183
183
  * @param path - Path of the directory to create
184
184
  */
185
185
  mkDir(path: string): Promise<void>;
186
+ /**
187
+ * Read a file from the filesystem of this sandbox.
188
+ *
189
+ * @param file - File to read, with path and optional cwd
190
+ * @returns A promise that resolves to a ReadableStream containing the file contents
191
+ */
192
+ readFile(file: {
193
+ path: string;
194
+ cwd?: string;
195
+ }): Promise<NodeJS.ReadableStream | null>;
186
196
  /**
187
197
  * Write files to the filesystem of this sandbox.
188
198
  *
package/dist/sandbox.js CHANGED
@@ -143,6 +143,19 @@ class Sandbox {
143
143
  path: path,
144
144
  });
145
145
  }
146
+ /**
147
+ * Read a file from the filesystem of this sandbox.
148
+ *
149
+ * @param file - File to read, with path and optional cwd
150
+ * @returns A promise that resolves to a ReadableStream containing the file contents
151
+ */
152
+ async readFile(file) {
153
+ return this.client.readFile({
154
+ sandboxId: this.sandbox.id,
155
+ path: file.path,
156
+ cwd: file.cwd,
157
+ });
158
+ }
146
159
  /**
147
160
  * Write files to the filesystem of this sandbox.
148
161
  *
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Consumes a readable entirely concatenating all content in a single Buffer
3
+ * @param readable A Readable stream
4
+ */
5
+ export declare function consumeReadable(readable: NodeJS.ReadableStream): Promise<Buffer<ArrayBufferLike>>;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.consumeReadable = consumeReadable;
4
+ /**
5
+ * Consumes a readable entirely concatenating all content in a single Buffer
6
+ * @param readable A Readable stream
7
+ */
8
+ function consumeReadable(readable) {
9
+ return new Promise((resolve, reject) => {
10
+ const chunks = [];
11
+ readable.on("error", (err) => reject(err));
12
+ readable.on("data", (chunk) => chunks.push(chunk));
13
+ readable.on("end", () => resolve(Buffer.concat(chunks)));
14
+ });
15
+ }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.0.9";
1
+ export declare const VERSION = "0.0.10";
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.9";
5
+ exports.VERSION = "0.0.10";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/sandbox",
3
- "version": "0.0.9",
3
+ "version": "0.0.10",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -16,6 +16,7 @@ import { APIError } from "./api-error";
16
16
  import { FileWriter } from "./file-writer";
17
17
  import { LRUCache } from "lru-cache";
18
18
  import { VERSION } from "../version";
19
+ import { consumeReadable } from "../utils/consume-readable";
19
20
  import { z } from "zod";
20
21
  import jsonlines from "jsonlines";
21
22
  import os from "os";
@@ -66,7 +67,14 @@ export class APIClient extends BaseClient {
66
67
  ports?: number[];
67
68
  projectId: string;
68
69
  source?:
69
- | { type: "git"; url: string; depth?: number; revision?: string }
70
+ | {
71
+ type: "git";
72
+ url: string;
73
+ depth?: number;
74
+ revision?: string;
75
+ username?: string;
76
+ password?: string;
77
+ }
70
78
  | { type: "tarball"; url: string };
71
79
  timeout?: number;
72
80
  resources?: { vcpus: number };
@@ -153,11 +161,13 @@ export class APIClient extends BaseClient {
153
161
  getFileWriter(params: { sandboxId: string }) {
154
162
  const writer = new FileWriter();
155
163
  return {
156
- response: this.request(`/v1/sandboxes/${params.sandboxId}/fs/write`, {
157
- method: "POST",
158
- headers: { "content-type": "application/gzip" },
159
- body: writer.readable,
160
- }),
164
+ response: (async () => {
165
+ return this.request(`/v1/sandboxes/${params.sandboxId}/fs/write`, {
166
+ method: "POST",
167
+ headers: { "content-type": "application/gzip" },
168
+ body: await consumeReadable(writer.readable),
169
+ });
170
+ })(),
161
171
  writer,
162
172
  };
163
173
  }
@@ -174,7 +184,7 @@ export class APIClient extends BaseClient {
174
184
  await writer.addFile({ name: file.path, content: file.stream });
175
185
  }
176
186
 
177
- await writer.end();
187
+ writer.end();
178
188
  await parseOrThrow(EmptyResponse, await response);
179
189
  }
180
190
 
@@ -0,0 +1,29 @@
1
+ import { it, beforeEach, afterEach, expect } from "vitest";
2
+ import { consumeReadable } from "./utils/consume-readable";
3
+ import { Sandbox } from "./sandbox";
4
+
5
+ let sandbox: Sandbox;
6
+
7
+ beforeEach(async () => {
8
+ sandbox = await Sandbox.create({
9
+ projectId: process.env.VERCEL_PROJECT_ID!,
10
+ teamId: process.env.VERCEL_TEAM_ID!,
11
+ token: process.env.VERCEL_TOKEN!,
12
+ });
13
+ });
14
+
15
+ afterEach(async () => {
16
+ await sandbox.stop();
17
+ });
18
+
19
+ it("allows to write files and then read them", async () => {
20
+ await sandbox.writeFiles([
21
+ { path: "hello1.txt", stream: Buffer.from("Hello 1") },
22
+ { path: "hello2.txt", stream: Buffer.from("Hello 2") },
23
+ ]);
24
+
25
+ const content1 = await sandbox.readFile({ path: "hello1.txt" });
26
+ const content2 = await sandbox.readFile({ path: "hello2.txt" });
27
+ expect((await consumeReadable(content1!)).toString()).toBe("Hello 1");
28
+ expect((await consumeReadable(content2!)).toString()).toBe("Hello 2");
29
+ });
package/src/sandbox.ts CHANGED
@@ -16,7 +16,20 @@ export interface CreateSandboxParams {
16
16
  * - `revision`: Clones and checks out a specific commit, branch, or tag
17
17
  */
18
18
  source?:
19
- | { type: "git"; url: string; depth?: number; revision?: string }
19
+ | {
20
+ type: "git";
21
+ url: string;
22
+ depth?: number;
23
+ revision?: string;
24
+ }
25
+ | {
26
+ type: "git";
27
+ url: string;
28
+ username: string;
29
+ password: string;
30
+ depth?: number;
31
+ revision?: string;
32
+ }
20
33
  | { type: "tarball"; url: string };
21
34
  /**
22
35
  * Array of port numbers to expose from the sandbox.
@@ -43,10 +56,6 @@ export interface CreateSandboxParams {
43
56
 
44
57
  /** @inline */
45
58
  interface GetSandboxParams {
46
- /**
47
- * Port-to-subdomain route mappings.
48
- */
49
- routes: Array<{ subdomain: string; port: number }>;
50
59
  /**
51
60
  * Unique identifier of the sandbox.
52
61
  */
@@ -294,6 +303,20 @@ export class Sandbox {
294
303
  });
295
304
  }
296
305
 
306
+ /**
307
+ * Read a file from the filesystem of this sandbox.
308
+ *
309
+ * @param file - File to read, with path and optional cwd
310
+ * @returns A promise that resolves to a ReadableStream containing the file contents
311
+ */
312
+ async readFile(file: { path: string; cwd?: string }) {
313
+ return this.client.readFile({
314
+ sandboxId: this.sandbox.id,
315
+ path: file.path,
316
+ cwd: file.cwd,
317
+ });
318
+ }
319
+
297
320
  /**
298
321
  * Write files to the filesystem of this sandbox.
299
322
  *
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Consumes a readable entirely concatenating all content in a single Buffer
3
+ * @param readable A Readable stream
4
+ */
5
+ export function consumeReadable(readable: NodeJS.ReadableStream) {
6
+ return new Promise<Buffer>((resolve, reject) => {
7
+ const chunks: Buffer[] = [];
8
+ readable.on("error", (err) => reject(err));
9
+ readable.on("data", (chunk) => chunks.push(chunk));
10
+ readable.on("end", () => resolve(Buffer.concat(chunks)));
11
+ });
12
+ }
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // Autogenerated by inject-version.ts
2
- export const VERSION = "0.0.9";
2
+ export const VERSION = "0.0.10";