@vercel/sandbox 1.2.0 → 1.3.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.
Files changed (65) hide show
  1. package/README.md +5 -0
  2. package/dist/api-client/api-client.js +7 -0
  3. package/dist/api-client/api-client.js.map +1 -1
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.js +3 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/utils/get-credentials.js +1 -1
  8. package/dist/version.d.ts +1 -1
  9. package/dist/version.js +1 -1
  10. package/package.json +6 -1
  11. package/.turbo/turbo-build.log +0 -4
  12. package/.turbo/turbo-test.log +0 -24
  13. package/.turbo/turbo-typecheck.log +0 -4
  14. package/CHANGELOG.md +0 -277
  15. package/__mocks__/picocolors.ts +0 -13
  16. package/scripts/inject-version.ts +0 -11
  17. package/src/api-client/api-client.test.ts +0 -228
  18. package/src/api-client/api-client.ts +0 -592
  19. package/src/api-client/api-error.ts +0 -46
  20. package/src/api-client/base-client.ts +0 -171
  21. package/src/api-client/file-writer.ts +0 -90
  22. package/src/api-client/index.ts +0 -2
  23. package/src/api-client/validators.ts +0 -146
  24. package/src/api-client/with-retry.ts +0 -131
  25. package/src/auth/api.ts +0 -31
  26. package/src/auth/error.ts +0 -8
  27. package/src/auth/file.ts +0 -69
  28. package/src/auth/index.ts +0 -9
  29. package/src/auth/infer-scope.test.ts +0 -178
  30. package/src/auth/linked-project.test.ts +0 -86
  31. package/src/auth/linked-project.ts +0 -40
  32. package/src/auth/oauth.ts +0 -333
  33. package/src/auth/poll-for-token.ts +0 -89
  34. package/src/auth/project.ts +0 -92
  35. package/src/auth/zod.ts +0 -16
  36. package/src/command.test.ts +0 -103
  37. package/src/command.ts +0 -287
  38. package/src/constants.ts +0 -1
  39. package/src/index.ts +0 -4
  40. package/src/sandbox.test.ts +0 -171
  41. package/src/sandbox.ts +0 -677
  42. package/src/snapshot.ts +0 -110
  43. package/src/utils/array.ts +0 -15
  44. package/src/utils/consume-readable.ts +0 -12
  45. package/src/utils/decode-base64-url.ts +0 -14
  46. package/src/utils/dev-credentials.test.ts +0 -217
  47. package/src/utils/dev-credentials.ts +0 -196
  48. package/src/utils/get-credentials.test.ts +0 -20
  49. package/src/utils/get-credentials.ts +0 -183
  50. package/src/utils/jwt-expiry.test.ts +0 -125
  51. package/src/utils/jwt-expiry.ts +0 -105
  52. package/src/utils/log.ts +0 -20
  53. package/src/utils/normalizePath.test.ts +0 -114
  54. package/src/utils/normalizePath.ts +0 -33
  55. package/src/utils/resolveSignal.ts +0 -24
  56. package/src/utils/types.test.js +0 -7
  57. package/src/utils/types.ts +0 -23
  58. package/src/version.ts +0 -2
  59. package/test-utils/mock-response.ts +0 -12
  60. package/tsconfig.json +0 -16
  61. package/turbo.json +0 -9
  62. package/typedoc.json +0 -13
  63. package/vercel.json +0 -9
  64. package/vitest.config.ts +0 -9
  65. package/vitest.setup.ts +0 -4
@@ -1,89 +0,0 @@
1
- import { setTimeout } from "node:timers/promises";
2
- import { updateAuthConfig } from "./file";
3
- import { DeviceAuthorizationRequest, isOAuthError, OAuth } from "./oauth";
4
-
5
- export type PollTokenItem =
6
- | { _tag: "Timeout"; newInterval: number }
7
- | { _tag: "SlowDown"; newInterval: number }
8
- | { _tag: "Error"; error: Error }
9
- | {
10
- _tag: "Response";
11
- response: { text(): Promise<string> };
12
- };
13
-
14
- export async function* pollForToken({
15
- request,
16
- oauth,
17
- }: {
18
- request: DeviceAuthorizationRequest;
19
- oauth: OAuth;
20
- }): AsyncGenerator<PollTokenItem, void, void> {
21
- const controller = new AbortController();
22
- try {
23
- let intervalMs = request.interval * 1000;
24
- while (Date.now() < request.expiresAt) {
25
- const [tokenResponseError, tokenResponse] =
26
- await oauth.deviceAccessTokenRequest(request.device_code);
27
-
28
- if (tokenResponseError) {
29
- // 2x backoff on connection timeouts per spec https://datatracker.ietf.org/doc/html/rfc8628#section-3.5
30
- if (tokenResponseError.message.includes("timeout")) {
31
- intervalMs *= 2;
32
- yield { _tag: "Timeout" as const, newInterval: intervalMs };
33
- await setTimeout(intervalMs, { signal: controller.signal });
34
- continue;
35
- }
36
- yield { _tag: "Error" as const, error: tokenResponseError };
37
- return;
38
- }
39
-
40
- yield {
41
- _tag: "Response" as const,
42
- response: tokenResponse.clone() as { text(): Promise<string> },
43
- };
44
-
45
- const [tokensError, tokens] =
46
- await oauth.processTokenResponse(tokenResponse);
47
-
48
- if (isOAuthError(tokensError)) {
49
- const { code } = tokensError;
50
- switch (code) {
51
- case "authorization_pending":
52
- await setTimeout(intervalMs, { signal: controller.signal });
53
- continue;
54
- case "slow_down":
55
- intervalMs += 5 * 1000;
56
- yield { _tag: "SlowDown" as const, newInterval: intervalMs };
57
- await setTimeout(intervalMs, { signal: controller.signal });
58
- continue;
59
- default:
60
- yield { _tag: "Error", error: tokensError.cause };
61
- return;
62
- }
63
- }
64
-
65
- if (tokensError) {
66
- yield { _tag: "Error", error: tokensError };
67
- return;
68
- }
69
-
70
- updateAuthConfig({
71
- token: tokens.access_token,
72
- expiresAt: new Date(Date.now() + tokens.expires_in * 1000),
73
- refreshToken: tokens.refresh_token,
74
- });
75
-
76
- return;
77
- }
78
-
79
- yield {
80
- _tag: "Error" as const,
81
- error: new Error(
82
- "Timed out waiting for authentication. Please try again.",
83
- ),
84
- };
85
- return;
86
- } finally {
87
- controller.abort();
88
- }
89
- }
@@ -1,92 +0,0 @@
1
- import { z } from "zod";
2
- import { fetchApi } from "./api";
3
- import { NotOk } from "./error";
4
- import { readLinkedProject } from "./linked-project";
5
-
6
- const TeamsSchema = z.object({
7
- teams: z
8
- .array(
9
- z.object({
10
- slug: z.string(),
11
- }),
12
- )
13
- .min(1, `No teams found. Please create a team first.`),
14
- });
15
-
16
- const DEFAULT_PROJECT_NAME = "vercel-sandbox-default-project";
17
-
18
- /**
19
- * Resolves the team and project scope for sandbox operations.
20
- *
21
- * First checks for a locally linked project in `.vercel/project.json`.
22
- * If found, uses the `projectId` and `orgId` from there.
23
- *
24
- * Otherwise, if `teamId` is not provided, selects the first available team for the account.
25
- * Ensures a default project exists within the team, creating it if necessary.
26
- *
27
- * @param opts.token - Vercel API authentication token.
28
- * @param opts.teamId - Optional team slug. If omitted, the first team is selected.
29
- * @param opts.cwd - Optional directory to search for `.vercel/project.json`. Defaults to `process.cwd()`.
30
- * @returns The resolved scope with `projectId`, `teamId`, and whether the project was `created`.
31
- *
32
- * @throws {NotOk} If the API returns an error other than 404 when checking the project.
33
- * @throws {ZodError} If no teams exist for the account.
34
- *
35
- * @example
36
- * ```ts
37
- * const scope = await inferScope({ token: "vercel_..." });
38
- * // => { projectId: "vercel-sandbox-default-project", teamId: "my-team", created: false }
39
- * ```
40
- */
41
- export async function inferScope(opts: {
42
- token: string;
43
- teamId?: string;
44
- cwd?: string;
45
- }): Promise<{ projectId: string; teamId: string; created: boolean }> {
46
- const linkedProject = await readLinkedProject(opts.cwd ?? process.cwd());
47
- if (linkedProject) {
48
- return { ...linkedProject, created: false };
49
- }
50
-
51
- const teamId = opts.teamId ?? (await selectTeam(opts.token));
52
-
53
- let created = false;
54
- try {
55
- await fetchApi({
56
- token: opts.token,
57
- endpoint: `/v2/projects/${encodeURIComponent(DEFAULT_PROJECT_NAME)}?slug=${encodeURIComponent(teamId)}`,
58
- });
59
- } catch (e) {
60
- if (!(e instanceof NotOk) || e.response.statusCode !== 404) {
61
- throw e;
62
- }
63
-
64
- await fetchApi({
65
- token: opts.token,
66
- endpoint: `/v11/projects?slug=${encodeURIComponent(teamId)}`,
67
- method: "POST",
68
- body: JSON.stringify({
69
- name: DEFAULT_PROJECT_NAME,
70
- }),
71
- });
72
- created = true;
73
- }
74
-
75
- return { projectId: DEFAULT_PROJECT_NAME, teamId, created };
76
- }
77
-
78
- /**
79
- * Selects a team for the current token by querying the Teams API and
80
- * returning the slug of the first team in the result set.
81
- *
82
- * @param token - Authentication token used to call the Vercel API.
83
- * @returns A promise that resolves to the first team's slug.
84
- */
85
- export async function selectTeam(token: string) {
86
- const {
87
- teams: [team],
88
- } = await fetchApi({ token, endpoint: "/v2/teams?limit=1" }).then(
89
- TeamsSchema.parse,
90
- );
91
- return team.slug;
92
- }
package/src/auth/zod.ts DELETED
@@ -1,16 +0,0 @@
1
- import { z } from "zod";
2
-
3
- /**
4
- * A Zod codec that serializes and deserializes JSON strings.
5
- */
6
- export const json = z.string().transform((jsonString: string, ctx): unknown => {
7
- try {
8
- return JSON.parse(jsonString);
9
- } catch (err: any) {
10
- ctx.addIssue({
11
- code: z.ZodIssueCode.custom,
12
- message: `Invalid JSON: ${err.message}`,
13
- });
14
- return z.NEVER;
15
- }
16
- });
@@ -1,103 +0,0 @@
1
- import { expect, it, vi, beforeEach, afterEach, describe } from "vitest";
2
- import { Sandbox } from "./sandbox";
3
-
4
- describe.skipIf(process.env.RUN_INTEGRATION_TESTS !== "1")("Command", () => {
5
- let sandbox: Sandbox;
6
-
7
- beforeEach(async () => {
8
- sandbox = await Sandbox.create();
9
- });
10
-
11
- afterEach(async () => {
12
- await sandbox.stop();
13
- });
14
-
15
- it("supports more than one logs consumer", async () => {
16
- const stdoutSpy = vi
17
- .spyOn(process.stdout, "write")
18
- .mockImplementation(() => true);
19
-
20
- const cmd = await sandbox.runCommand({
21
- cmd: "echo",
22
- args: ["Hello World!"],
23
- stdout: process.stdout,
24
- });
25
-
26
- expect(await cmd.stdout()).toEqual("Hello World!\n");
27
- expect(stdoutSpy).toHaveBeenCalledWith("Hello World!\n");
28
- });
29
-
30
- it("does not warn when there is only one logs consumer", async () => {
31
- const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
32
-
33
- const cmd = await sandbox.runCommand({
34
- cmd: "echo",
35
- args: ["Hello World!"],
36
- });
37
-
38
- expect(await cmd.stdout()).toEqual("Hello World!\n");
39
- expect(warnSpy).not.toHaveBeenCalled();
40
- });
41
-
42
- it("Kills a command with a SIGINT", async () => {
43
- const cmd = await sandbox.runCommand({
44
- cmd: "sleep",
45
- args: ["200000"],
46
- detached: true,
47
- });
48
-
49
- await cmd.kill("SIGINT");
50
- const result = await cmd.wait();
51
- expect(result.exitCode).toBe(130); // 128 + 2
52
- });
53
-
54
- it("Kills a command with a SIGTERM", async () => {
55
- const cmd = await sandbox.runCommand({
56
- cmd: "sleep",
57
- args: ["200000"],
58
- detached: true,
59
- });
60
-
61
- await cmd.kill("SIGTERM");
62
-
63
- const result = await cmd.wait();
64
- expect(result.exitCode).toBe(143); // 128 + 15
65
- });
66
-
67
- it("can execute commands with sudo", async () => {
68
- const cmd = await sandbox.runCommand({
69
- cmd: "env",
70
- sudo: true,
71
- env: {
72
- FOO: "bar",
73
- },
74
- });
75
-
76
- expect(cmd.exitCode).toBe(0);
77
-
78
- const output = await cmd.stdout();
79
- expect(output).toContain("FOO=bar\n");
80
- expect(output).toContain("USER=root\n");
81
- expect(output).toContain("SUDO_USER=vercel-sandbox\n");
82
-
83
- const pathLine = output
84
- .split("\n")
85
- .find((line) => line.startsWith("PATH="));
86
- expect(pathLine).toBeDefined();
87
-
88
- const pathSegments = pathLine!.slice(5).split(":");
89
- expect(pathSegments).toContain("/vercel/bin");
90
- expect(pathSegments).toContain("/vercel/runtimes/node22/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(await which.output()).toContain("/usr/bin/go");
102
- });
103
- });
package/src/command.ts DELETED
@@ -1,287 +0,0 @@
1
- import { APIClient, type CommandData } from "./api-client";
2
- import { Signal, resolveSignal } from "./utils/resolveSignal";
3
-
4
- /**
5
- * A command executed in a Sandbox.
6
- *
7
- * For detached commands, you can {@link wait} to get a {@link CommandFinished} instance
8
- * with the populated exit code. For non-detached commands, {@link Sandbox.runCommand}
9
- * automatically waits and returns a {@link CommandFinished} instance.
10
- *
11
- * You can iterate over command output with {@link logs}.
12
- *
13
- * @see {@link Sandbox.runCommand} to start a command.
14
- *
15
- * @hideconstructor
16
- */
17
- export class Command {
18
- /**
19
- * @internal
20
- * @private
21
- */
22
- protected client: APIClient;
23
-
24
- /**
25
- * ID of the sandbox this command is running in.
26
- */
27
- private sandboxId: string;
28
-
29
- /**
30
- * Data for the command execution.
31
- */
32
- private cmd: CommandData;
33
-
34
- public exitCode: number | null;
35
-
36
- private outputCache: Promise<{
37
- stdout: string;
38
- stderr: string;
39
- both: string;
40
- }> | null = null;
41
-
42
- /**
43
- * ID of the command execution.
44
- */
45
- get cmdId() {
46
- return this.cmd.id;
47
- }
48
-
49
- get cwd() {
50
- return this.cmd.cwd;
51
- }
52
-
53
- get startedAt() {
54
- return this.cmd.startedAt;
55
- }
56
-
57
- /**
58
- * @param params - Object containing the client, sandbox ID, and command ID.
59
- * @param params.client - API client used to interact with the backend.
60
- * @param params.sandboxId - The ID of the sandbox where the command is running.
61
- * @param params.cmdId - The ID of the command execution.
62
- */
63
- constructor({
64
- client,
65
- sandboxId,
66
- cmd,
67
- }: {
68
- client: APIClient;
69
- sandboxId: string;
70
- cmd: CommandData;
71
- }) {
72
- this.client = client;
73
- this.sandboxId = sandboxId;
74
- this.cmd = cmd;
75
- this.exitCode = cmd.exitCode ?? null;
76
- }
77
-
78
- /**
79
- * Iterate over the output of this command.
80
- *
81
- * ```
82
- * for await (const log of cmd.logs()) {
83
- * if (log.stream === "stdout") {
84
- * process.stdout.write(log.data);
85
- * } else {
86
- * process.stderr.write(log.data);
87
- * }
88
- * }
89
- * ```
90
- *
91
- * @param opts - Optional parameters.
92
- * @param opts.signal - An AbortSignal to cancel log streaming.
93
- * @returns An async iterable of log entries from the command output.
94
- *
95
- * @see {@link Command.stdout}, {@link Command.stderr}, and {@link Command.output}
96
- * to access output as a string.
97
- */
98
- logs(opts?: { signal?: AbortSignal }) {
99
- return this.client.getLogs({
100
- sandboxId: this.sandboxId,
101
- cmdId: this.cmd.id,
102
- signal: opts?.signal,
103
- });
104
- }
105
-
106
- /**
107
- * Wait for a command to exit and populate its exit code.
108
- *
109
- * This method is useful for detached commands where you need to wait
110
- * for completion. For non-detached commands, {@link Sandbox.runCommand}
111
- * automatically waits and returns a {@link CommandFinished} instance.
112
- *
113
- * ```
114
- * const detachedCmd = await sandbox.runCommand({ cmd: 'sleep', args: ['5'], detached: true });
115
- * const result = await detachedCmd.wait();
116
- * if (result.exitCode !== 0) {
117
- * console.error("Something went wrong...")
118
- * }
119
- * ```
120
- *
121
- * @param params - Optional parameters.
122
- * @param params.signal - An AbortSignal to cancel waiting.
123
- * @returns A {@link CommandFinished} instance with populated exit code.
124
- */
125
- async wait(params?: { signal?: AbortSignal }) {
126
- params?.signal?.throwIfAborted();
127
-
128
- const command = await this.client.getCommand({
129
- sandboxId: this.sandboxId,
130
- cmdId: this.cmd.id,
131
- wait: true,
132
- signal: params?.signal,
133
- });
134
-
135
- return new CommandFinished({
136
- client: this.client,
137
- sandboxId: this.sandboxId,
138
- cmd: command.json.command,
139
- exitCode: command.json.command.exitCode,
140
- });
141
- }
142
-
143
- /**
144
- * Get cached output, fetching logs only once and reusing for concurrent calls.
145
- * This prevents race conditions when stdout() and stderr() are called in parallel.
146
- */
147
- private async getCachedOutput(opts?: { signal?: AbortSignal }): Promise<{
148
- stdout: string;
149
- stderr: string;
150
- both: string;
151
- }> {
152
- if (!this.outputCache) {
153
- this.outputCache = (async () => {
154
- try {
155
- let stdout = "";
156
- let stderr = "";
157
- let both = "";
158
- for await (const log of this.logs({ signal: opts?.signal })) {
159
- both += log.data;
160
- if (log.stream === "stdout") {
161
- stdout += log.data;
162
- } else {
163
- stderr += log.data;
164
- }
165
- }
166
- return { stdout, stderr, both };
167
- } catch (err) {
168
- // Clear the promise so future calls can retry
169
- this.outputCache = null;
170
- throw err;
171
- }
172
- })();
173
- }
174
-
175
- return this.outputCache;
176
- }
177
-
178
- /**
179
- * Get the output of `stdout`, `stderr`, or both as a string.
180
- *
181
- * NOTE: This may throw string conversion errors if the command does
182
- * not output valid Unicode.
183
- *
184
- * @param stream - The output stream to read: "stdout", "stderr", or "both".
185
- * @param opts - Optional parameters.
186
- * @param opts.signal - An AbortSignal to cancel output streaming.
187
- * @returns The output of the specified stream(s) as a string.
188
- */
189
- async output(
190
- stream: "stdout" | "stderr" | "both" = "both",
191
- opts?: { signal?: AbortSignal },
192
- ) {
193
- const cached = await this.getCachedOutput(opts);
194
- return cached[stream];
195
- }
196
-
197
- /**
198
- * Get the output of `stdout` as a string.
199
- *
200
- * NOTE: This may throw string conversion errors if the command does
201
- * not output valid Unicode.
202
- *
203
- * @param opts - Optional parameters.
204
- * @param opts.signal - An AbortSignal to cancel output streaming.
205
- * @returns The standard output of the command.
206
- */
207
- async stdout(opts?: { signal?: AbortSignal }) {
208
- return this.output("stdout", opts);
209
- }
210
-
211
- /**
212
- * Get the output of `stderr` as a string.
213
- *
214
- * NOTE: This may throw string conversion errors if the command does
215
- * not output valid Unicode.
216
- *
217
- * @param opts - Optional parameters.
218
- * @param opts.signal - An AbortSignal to cancel output streaming.
219
- * @returns The standard error output of the command.
220
- */
221
- async stderr(opts?: { signal?: AbortSignal }) {
222
- return this.output("stderr", opts);
223
- }
224
-
225
- /**
226
- * Kill a running command in a sandbox.
227
- *
228
- * @param signal - The signal to send the running process. Defaults to SIGTERM.
229
- * @param opts - Optional parameters.
230
- * @param opts.abortSignal - An AbortSignal to cancel the kill operation.
231
- * @returns Promise<void>.
232
- */
233
- async kill(signal?: Signal, opts?: { abortSignal?: AbortSignal }) {
234
- await this.client.killCommand({
235
- sandboxId: this.sandboxId,
236
- commandId: this.cmd.id,
237
- signal: resolveSignal(signal ?? "SIGTERM"),
238
- abortSignal: opts?.abortSignal,
239
- });
240
- }
241
- }
242
-
243
- /**
244
- * A command that has finished executing.
245
- *
246
- * The exit code is immediately available and populated upon creation.
247
- * Unlike {@link Command}, you don't need to call wait() - the command
248
- * has already completed execution.
249
- *
250
- * @hideconstructor
251
- */
252
- export class CommandFinished extends Command {
253
- /**
254
- * The exit code of the command. This is always populated for
255
- * CommandFinished instances.
256
- */
257
- public exitCode: number;
258
-
259
- /**
260
- * @param params - Object containing client, sandbox ID, command ID, and exit code.
261
- * @param params.client - API client used to interact with the backend.
262
- * @param params.sandboxId - The ID of the sandbox where the command ran.
263
- * @param params.cmdId - The ID of the command execution.
264
- * @param params.exitCode - The exit code of the completed command.
265
- */
266
- constructor(params: {
267
- client: APIClient;
268
- sandboxId: string;
269
- cmd: CommandData;
270
- exitCode: number;
271
- }) {
272
- super({ ...params });
273
- this.exitCode = params.exitCode;
274
- }
275
-
276
- /**
277
- * The wait method is not needed for CommandFinished instances since
278
- * the command has already completed and exitCode is populated.
279
- *
280
- * @deprecated This method is redundant for CommandFinished instances.
281
- * The exitCode is already available.
282
- * @returns This CommandFinished instance.
283
- */
284
- async wait(): Promise<CommandFinished> {
285
- return this;
286
- }
287
- }
package/src/constants.ts DELETED
@@ -1 +0,0 @@
1
- export type RUNTIMES = "node24" | "node22" | "python3.13";
package/src/index.ts DELETED
@@ -1,4 +0,0 @@
1
- export { Sandbox } from "./sandbox";
2
- export { Snapshot } from "./snapshot";
3
- export { Command, CommandFinished } from "./command";
4
- export { StreamError } from "./api-client/api-error";