@vercel/sandbox 0.0.12 → 0.0.14
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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +14 -0
- package/README.md +77 -2
- package/dist/api-client/api-client.d.ts +1 -0
- package/dist/api-client/api-client.js +1 -0
- package/dist/command.d.ts +3 -0
- package/dist/command.js +7 -0
- package/dist/sandbox.d.ts +4 -0
- package/dist/sandbox.js +1 -0
- package/dist/utils/get-credentials.js +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -1
- package/src/api-client/api-client.ts +2 -0
- package/src/command.test.ts +33 -5
- package/src/command.ts +11 -0
- package/src/sandbox.test.ts +3 -7
- package/src/sandbox.ts +5 -0
- package/src/utils/get-credentials.ts +2 -2
- package/src/version.ts +1 -1
- package/vercel.json +9 -0
- package/dist/utils/get-vercel-oidc-token.d.ts +0 -6
- package/dist/utils/get-vercel-oidc-token.js +0 -21
- package/src/utils/get-vercel-oidc-token.ts +0 -31
package/.turbo/turbo-build.log
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @vercel/sandbox
|
|
2
2
|
|
|
3
|
+
## 0.0.14
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Use `@vercel/oidc` for authentication ([#87](https://github.com/vercel/sandbox-sdk/pull/87))
|
|
8
|
+
|
|
9
|
+
- Expose more data in `Command` ([#85](https://github.com/vercel/sandbox-sdk/pull/85))
|
|
10
|
+
|
|
11
|
+
## 0.0.13
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- Add sudo support to running commands ([#72](https://github.com/vercel/sandbox-sdk/pull/72))
|
|
16
|
+
|
|
3
17
|
## 0.0.12
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -35,7 +35,52 @@ vercel env pull
|
|
|
35
35
|
|
|
36
36
|
Now create `next-dev.ts`:
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
```ts
|
|
39
|
+
import ms from "ms";
|
|
40
|
+
import { Sandbox } from "@vercel/sandbox";
|
|
41
|
+
import { setTimeout } from "timers/promises";
|
|
42
|
+
import { spawn } from "child_process";
|
|
43
|
+
|
|
44
|
+
async function main() {
|
|
45
|
+
const sandbox = await Sandbox.create({
|
|
46
|
+
source: {
|
|
47
|
+
url: "https://github.com/vercel/sandbox-example-next.git",
|
|
48
|
+
type: "git",
|
|
49
|
+
},
|
|
50
|
+
resources: { vcpus: 4 },
|
|
51
|
+
timeout: ms("5m"),
|
|
52
|
+
ports: [3000],
|
|
53
|
+
runtime: "node22",
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
console.log(`Installing dependencies...`);
|
|
57
|
+
const install = await sandbox.runCommand({
|
|
58
|
+
cmd: "npm",
|
|
59
|
+
args: ["install", "--loglevel", "info"],
|
|
60
|
+
stderr: process.stderr,
|
|
61
|
+
stdout: process.stdout,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (install.exitCode != 0) {
|
|
65
|
+
console.log("installing packages failed");
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
console.log(`Starting the development server...`);
|
|
70
|
+
await sandbox.runCommand({
|
|
71
|
+
cmd: "npm",
|
|
72
|
+
args: ["run", "dev"],
|
|
73
|
+
stderr: process.stderr,
|
|
74
|
+
stdout: process.stdout,
|
|
75
|
+
detached: true,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
await setTimeout(500);
|
|
79
|
+
spawn("open", [sandbox.domain(3000)]);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
main().catch(console.error);
|
|
83
|
+
```
|
|
39
84
|
|
|
40
85
|
Run it like this:
|
|
41
86
|
|
|
@@ -89,6 +134,7 @@ const sandbox = await Sandbox.create({
|
|
|
89
134
|
type: "git",
|
|
90
135
|
},
|
|
91
136
|
resources: { vcpus: 4 },
|
|
137
|
+
// Defaults to 5 minutes. The maximum is 45 minutes.
|
|
92
138
|
timeout: ms("5m"),
|
|
93
139
|
ports: [3000],
|
|
94
140
|
runtime: "node22",
|
|
@@ -97,8 +143,9 @@ const sandbox = await Sandbox.create({
|
|
|
97
143
|
|
|
98
144
|
## Limitations
|
|
99
145
|
|
|
100
|
-
- `sudo` is not available. User code is executed as the `vercel-sandbox` user.
|
|
101
146
|
- Max resources: 8 vCPUs. You will get 2048 MB of memory per vCPU.
|
|
147
|
+
- Sandboxes have a maximum runtime duration of 45 minutes, with a default of 5
|
|
148
|
+
minutes. This can be configured using the `timeout` option of `Sandbox.create()`.
|
|
102
149
|
|
|
103
150
|
## System
|
|
104
151
|
|
|
@@ -131,5 +178,33 @@ zstd
|
|
|
131
178
|
- User code is executed as the `vercel-sandbox` user.
|
|
132
179
|
- `/vercel/sandbox` is writable.
|
|
133
180
|
|
|
181
|
+
## Sudo access
|
|
182
|
+
|
|
183
|
+
The `node22` and `python3.13` images allow users to run commands as root. This
|
|
184
|
+
can be used to install packages and system tools:
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
import { Sandbox } from "@vercel/sandbox";
|
|
188
|
+
|
|
189
|
+
const sandbox = await Sandbox.create();
|
|
190
|
+
await sandbox.runCommand({
|
|
191
|
+
cmd: "dnf",
|
|
192
|
+
args: ["install", "-y", "golang"],
|
|
193
|
+
sudo: true,
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Sandbox runs sudo in the following configuration:
|
|
198
|
+
|
|
199
|
+
- `HOME` is set to `/root` – Executed commands will source root's configuration
|
|
200
|
+
files (e.g. `.gitconfig`, `.bashrc`, etc).
|
|
201
|
+
- Environment variables are not reset before executing the command.
|
|
202
|
+
- `PATH` is left unchanged – sudo won’t change the value of PATH, so local or
|
|
203
|
+
project-specific binaries will still be found.
|
|
204
|
+
|
|
205
|
+
Both these images are based on Amazon Linux 2023. The full package list is
|
|
206
|
+
available [here](https://docs.aws.amazon.com/linux/al2023/release-notes/all-packages-AL2023.7.html).
|
|
207
|
+
|
|
134
208
|
[create-token]: https://vercel.com/account/settings/tokens
|
|
135
209
|
[hive]: https://vercel.com/blog/a-deep-dive-into-hive-vercels-builds-infrastructure
|
|
210
|
+
[al-2023-packages]: https://docs.aws.amazon.com/linux/al2023/release-notes/all-packages-AL2023.7.html
|
package/dist/command.d.ts
CHANGED
|
@@ -24,10 +24,13 @@ export declare class Command {
|
|
|
24
24
|
* Data for the command execution.
|
|
25
25
|
*/
|
|
26
26
|
private cmd;
|
|
27
|
+
exitCode: number | null;
|
|
27
28
|
/**
|
|
28
29
|
* ID of the command execution.
|
|
29
30
|
*/
|
|
30
31
|
get cmdId(): string;
|
|
32
|
+
get cwd(): string;
|
|
33
|
+
get startedAt(): number;
|
|
31
34
|
/**
|
|
32
35
|
* @param params - Object containing the client, sandbox ID, and command ID.
|
|
33
36
|
* @param params.client - API client used to interact with the backend.
|
package/dist/command.js
CHANGED
|
@@ -19,6 +19,12 @@ class Command {
|
|
|
19
19
|
get cmdId() {
|
|
20
20
|
return this.cmd.id;
|
|
21
21
|
}
|
|
22
|
+
get cwd() {
|
|
23
|
+
return this.cmd.cwd;
|
|
24
|
+
}
|
|
25
|
+
get startedAt() {
|
|
26
|
+
return this.cmd.startedAt;
|
|
27
|
+
}
|
|
22
28
|
/**
|
|
23
29
|
* @param params - Object containing the client, sandbox ID, and command ID.
|
|
24
30
|
* @param params.client - API client used to interact with the backend.
|
|
@@ -29,6 +35,7 @@ class Command {
|
|
|
29
35
|
this.client = client;
|
|
30
36
|
this.sandboxId = sandboxId;
|
|
31
37
|
this.cmd = cmd;
|
|
38
|
+
this.exitCode = cmd.exitCode ?? null;
|
|
32
39
|
}
|
|
33
40
|
/**
|
|
34
41
|
* Iterate over the output of this command.
|
package/dist/sandbox.d.ts
CHANGED
|
@@ -78,6 +78,10 @@ interface RunCommandParams {
|
|
|
78
78
|
* Environment variables to set for this command
|
|
79
79
|
*/
|
|
80
80
|
env?: Record<string, string>;
|
|
81
|
+
/**
|
|
82
|
+
* If true, execute this command with root privileges. Defaults to false.
|
|
83
|
+
*/
|
|
84
|
+
sudo?: boolean;
|
|
81
85
|
/**
|
|
82
86
|
* If true, the command will return without waiting for `exitCode`
|
|
83
87
|
*/
|
package/dist/sandbox.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.getCredentials = getCredentials;
|
|
4
|
-
const
|
|
4
|
+
const oidc_1 = require("@vercel/oidc");
|
|
5
5
|
const decode_base64_url_1 = require("./decode-base64-url");
|
|
6
6
|
const zod_1 = require("zod");
|
|
7
7
|
/**
|
|
@@ -19,7 +19,7 @@ function getCredentials(params) {
|
|
|
19
19
|
if (credentials) {
|
|
20
20
|
return credentials;
|
|
21
21
|
}
|
|
22
|
-
const oidcToken = (0,
|
|
22
|
+
const oidcToken = (0, oidc_1.getVercelOidcTokenSync)();
|
|
23
23
|
if (oidcToken) {
|
|
24
24
|
return getCredentialsFromOIDCToken(oidcToken);
|
|
25
25
|
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.0.
|
|
1
|
+
export declare const VERSION = "0.0.14";
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vercel/sandbox",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.14",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"author": "",
|
|
10
10
|
"license": "ISC",
|
|
11
11
|
"dependencies": {
|
|
12
|
+
"@vercel/oidc": "^2.0.0",
|
|
12
13
|
"async-retry": "1.3.3",
|
|
13
14
|
"form-data": "3.0.0",
|
|
14
15
|
"jsonlines": "0.1.1",
|
|
@@ -102,6 +102,7 @@ export class APIClient extends BaseClient {
|
|
|
102
102
|
command: string;
|
|
103
103
|
args: string[];
|
|
104
104
|
env: Record<string, string>;
|
|
105
|
+
sudo: boolean;
|
|
105
106
|
}) {
|
|
106
107
|
return parseOrThrow(
|
|
107
108
|
CommandResponse,
|
|
@@ -112,6 +113,7 @@ export class APIClient extends BaseClient {
|
|
|
112
113
|
args: params.args,
|
|
113
114
|
cwd: params.cwd,
|
|
114
115
|
env: params.env,
|
|
116
|
+
sudo: params.sudo,
|
|
115
117
|
}),
|
|
116
118
|
}),
|
|
117
119
|
);
|
package/src/command.test.ts
CHANGED
|
@@ -4,11 +4,7 @@ import { Sandbox } from "./sandbox";
|
|
|
4
4
|
let sandbox: Sandbox;
|
|
5
5
|
|
|
6
6
|
beforeEach(async () => {
|
|
7
|
-
sandbox = await Sandbox.create(
|
|
8
|
-
projectId: process.env.VERCEL_PROJECT_ID!,
|
|
9
|
-
teamId: process.env.VERCEL_TEAM_ID!,
|
|
10
|
-
token: process.env.VERCEL_TOKEN!,
|
|
11
|
-
});
|
|
7
|
+
sandbox = await Sandbox.create();
|
|
12
8
|
});
|
|
13
9
|
|
|
14
10
|
afterEach(async () => {
|
|
@@ -72,3 +68,35 @@ it("Kills a command with a SIGTERM", async () => {
|
|
|
72
68
|
const result = await cmd.wait();
|
|
73
69
|
expect(result.exitCode).toBe(143); // 128 + 15
|
|
74
70
|
});
|
|
71
|
+
|
|
72
|
+
it("can execute commands with sudo", async () => {
|
|
73
|
+
const cmd = await sandbox.runCommand({
|
|
74
|
+
cmd: "env",
|
|
75
|
+
sudo: true,
|
|
76
|
+
env: {
|
|
77
|
+
FOO: "bar",
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
expect(cmd.exitCode).toBe(0);
|
|
82
|
+
|
|
83
|
+
const output = await cmd.stdout();
|
|
84
|
+
expect(output).toContain("FOO=bar\n");
|
|
85
|
+
expect(output).toContain("USER=root\n");
|
|
86
|
+
expect(output).toContain("SUDO_USER=vercel-sandbox\n");
|
|
87
|
+
|
|
88
|
+
// The actual path contains more, but this is enough
|
|
89
|
+
// to verify that we do not reset it to a default path.
|
|
90
|
+
expect(output).toContain("PATH=/vercel/bin");
|
|
91
|
+
|
|
92
|
+
const dnf = await sandbox.runCommand({
|
|
93
|
+
cmd: "dnf",
|
|
94
|
+
args: ["install", "-y", "golang"],
|
|
95
|
+
sudo: true,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
expect(dnf.exitCode).toBe(0);
|
|
99
|
+
|
|
100
|
+
const which = await sandbox.runCommand("which", ["go"]);
|
|
101
|
+
expect(which.output()).resolves.toContain("/usr/bin/go");
|
|
102
|
+
});
|
package/src/command.ts
CHANGED
|
@@ -28,6 +28,8 @@ export class Command {
|
|
|
28
28
|
*/
|
|
29
29
|
private cmd: CommandData;
|
|
30
30
|
|
|
31
|
+
public exitCode: number | null;
|
|
32
|
+
|
|
31
33
|
/**
|
|
32
34
|
* ID of the command execution.
|
|
33
35
|
*/
|
|
@@ -35,6 +37,14 @@ export class Command {
|
|
|
35
37
|
return this.cmd.id;
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
get cwd() {
|
|
41
|
+
return this.cmd.cwd;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get startedAt() {
|
|
45
|
+
return this.cmd.startedAt;
|
|
46
|
+
}
|
|
47
|
+
|
|
38
48
|
/**
|
|
39
49
|
* @param params - Object containing the client, sandbox ID, and command ID.
|
|
40
50
|
* @param params.client - API client used to interact with the backend.
|
|
@@ -53,6 +63,7 @@ export class Command {
|
|
|
53
63
|
this.client = client;
|
|
54
64
|
this.sandboxId = sandboxId;
|
|
55
65
|
this.cmd = cmd;
|
|
66
|
+
this.exitCode = cmd.exitCode ?? null;
|
|
56
67
|
}
|
|
57
68
|
|
|
58
69
|
/**
|
package/src/sandbox.test.ts
CHANGED
|
@@ -5,11 +5,7 @@ import { Sandbox } from "./sandbox";
|
|
|
5
5
|
let sandbox: Sandbox;
|
|
6
6
|
|
|
7
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
|
-
});
|
|
8
|
+
sandbox = await Sandbox.create();
|
|
13
9
|
});
|
|
14
10
|
|
|
15
11
|
afterEach(async () => {
|
|
@@ -18,8 +14,8 @@ afterEach(async () => {
|
|
|
18
14
|
|
|
19
15
|
it("allows to write files and then read them", async () => {
|
|
20
16
|
await sandbox.writeFiles([
|
|
21
|
-
{ path: "hello1.txt",
|
|
22
|
-
{ path: "hello2.txt",
|
|
17
|
+
{ path: "hello1.txt", content: Buffer.from("Hello 1") },
|
|
18
|
+
{ path: "hello2.txt", content: Buffer.from("Hello 2") },
|
|
23
19
|
]);
|
|
24
20
|
|
|
25
21
|
const content1 = await sandbox.readFile({ path: "hello1.txt" });
|
package/src/sandbox.ts
CHANGED
|
@@ -80,6 +80,10 @@ interface RunCommandParams {
|
|
|
80
80
|
* Environment variables to set for this command
|
|
81
81
|
*/
|
|
82
82
|
env?: Record<string, string>;
|
|
83
|
+
/**
|
|
84
|
+
* If true, execute this command with root privileges. Defaults to false.
|
|
85
|
+
*/
|
|
86
|
+
sudo?: boolean;
|
|
83
87
|
/**
|
|
84
88
|
* If true, the command will return without waiting for `exitCode`
|
|
85
89
|
*/
|
|
@@ -268,6 +272,7 @@ export class Sandbox {
|
|
|
268
272
|
args: params.args ?? [],
|
|
269
273
|
cwd: params.cwd,
|
|
270
274
|
env: params.env ?? {},
|
|
275
|
+
sudo: params.sudo ?? false,
|
|
271
276
|
});
|
|
272
277
|
|
|
273
278
|
const command = new Command({
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getVercelOidcTokenSync } from "@vercel/oidc";
|
|
2
2
|
import { decodeBase64Url } from "./decode-base64-url";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
|
|
@@ -34,7 +34,7 @@ export function getCredentials<T>(params?: T | Credentials): Credentials {
|
|
|
34
34
|
return credentials;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
const oidcToken =
|
|
37
|
+
const oidcToken = getVercelOidcTokenSync();
|
|
38
38
|
if (oidcToken) {
|
|
39
39
|
return getCredentialsFromOIDCToken(oidcToken);
|
|
40
40
|
}
|
package/src/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Autogenerated by inject-version.ts
|
|
2
|
-
export const VERSION = "0.0.
|
|
2
|
+
export const VERSION = "0.0.14";
|
package/vercel.json
ADDED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* This function is implemented in @vercel/functions but it is asynchronous.
|
|
3
|
-
* In order to keep it synchronous, we are implementing it here as well.
|
|
4
|
-
* Ideally we should remove this and use the one from @vercel/functions.
|
|
5
|
-
*/
|
|
6
|
-
export declare function getVercelOidcToken(): string | undefined;
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getVercelOidcToken = getVercelOidcToken;
|
|
4
|
-
/**
|
|
5
|
-
* This function is implemented in @vercel/functions but it is asynchronous.
|
|
6
|
-
* In order to keep it synchronous, we are implementing it here as well.
|
|
7
|
-
* Ideally we should remove this and use the one from @vercel/functions.
|
|
8
|
-
*/
|
|
9
|
-
function getVercelOidcToken() {
|
|
10
|
-
const token = getContext().headers?.["x-vercel-oidc-token"] ??
|
|
11
|
-
process.env.VERCEL_OIDC_TOKEN;
|
|
12
|
-
if (!token && process.env.NODE_ENV === "production") {
|
|
13
|
-
throw new Error(`The 'x-vercel-oidc-token' header is missing from the request. Do you have the OIDC option enabled in the Vercel project settings?`);
|
|
14
|
-
}
|
|
15
|
-
return token;
|
|
16
|
-
}
|
|
17
|
-
const SYMBOL_FOR_REQ_CONTEXT = Symbol.for("@vercel/request-context");
|
|
18
|
-
function getContext() {
|
|
19
|
-
const fromSymbol = globalThis;
|
|
20
|
-
return fromSymbol[SYMBOL_FOR_REQ_CONTEXT]?.get?.() ?? {};
|
|
21
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* This function is implemented in @vercel/functions but it is asynchronous.
|
|
3
|
-
* In order to keep it synchronous, we are implementing it here as well.
|
|
4
|
-
* Ideally we should remove this and use the one from @vercel/functions.
|
|
5
|
-
*/
|
|
6
|
-
export function getVercelOidcToken(): string | undefined {
|
|
7
|
-
const token =
|
|
8
|
-
getContext().headers?.["x-vercel-oidc-token"] ??
|
|
9
|
-
process.env.VERCEL_OIDC_TOKEN;
|
|
10
|
-
|
|
11
|
-
if (!token && process.env.NODE_ENV === "production") {
|
|
12
|
-
throw new Error(
|
|
13
|
-
`The 'x-vercel-oidc-token' header is missing from the request. Do you have the OIDC option enabled in the Vercel project settings?`,
|
|
14
|
-
);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
return token;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const SYMBOL_FOR_REQ_CONTEXT = Symbol.for("@vercel/request-context");
|
|
21
|
-
|
|
22
|
-
interface Context {
|
|
23
|
-
headers?: Record<string, string>;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function getContext(): Context {
|
|
27
|
-
const fromSymbol: typeof globalThis & {
|
|
28
|
-
[SYMBOL_FOR_REQ_CONTEXT]?: { get?: () => Context };
|
|
29
|
-
} = globalThis;
|
|
30
|
-
return fromSymbol[SYMBOL_FOR_REQ_CONTEXT]?.get?.() ?? {};
|
|
31
|
-
}
|