@vercel/sandbox 0.0.5 → 0.0.6

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 (59) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-typecheck.log +1 -1
  3. package/CHANGELOG.md +8 -0
  4. package/README.md +1 -0
  5. package/dist/{client/client.d.ts → api-client/api-client.d.ts} +30 -17
  6. package/dist/{client/client.js → api-client/api-client.js} +49 -44
  7. package/dist/{client → api-client}/base-client.d.ts +1 -1
  8. package/dist/{client → api-client}/base-client.js +3 -3
  9. package/dist/api-client/index.d.ts +1 -0
  10. package/dist/api-client/index.js +5 -0
  11. package/dist/{client → api-client}/validators.d.ts +20 -1
  12. package/dist/{client → api-client}/validators.js +9 -2
  13. package/dist/command.d.ts +127 -0
  14. package/dist/command.js +137 -0
  15. package/dist/index.d.ts +2 -1
  16. package/dist/index.js +6 -5
  17. package/dist/sandbox.d.ts +201 -0
  18. package/dist/sandbox.js +174 -0
  19. package/dist/utils/decode-base64-url.d.ts +7 -0
  20. package/dist/utils/decode-base64-url.js +12 -0
  21. package/dist/utils/get-credentials.d.ts +26 -0
  22. package/dist/utils/get-credentials.js +84 -0
  23. package/dist/utils/get-vercel-oidc-token.d.ts +6 -0
  24. package/dist/utils/get-vercel-oidc-token.js +21 -0
  25. package/dist/version.d.ts +1 -1
  26. package/dist/version.js +1 -1
  27. package/package.json +10 -3
  28. package/src/api-client/api-client.ts +225 -0
  29. package/src/{client → api-client}/base-client.ts +1 -1
  30. package/src/api-client/index.ts +1 -0
  31. package/src/{client → api-client}/validators.ts +9 -1
  32. package/src/command.test.ts +51 -0
  33. package/src/command.ts +176 -0
  34. package/src/index.ts +2 -1
  35. package/src/sandbox.ts +309 -0
  36. package/src/utils/decode-base64-url.ts +14 -0
  37. package/src/utils/get-credentials.ts +113 -0
  38. package/src/utils/get-vercel-oidc-token.ts +31 -0
  39. package/src/version.ts +1 -1
  40. package/tsconfig.json +2 -1
  41. package/typedoc.json +7 -1
  42. package/vitest.config.ts +8 -0
  43. package/vitest.setup.ts +4 -0
  44. package/dist/create-sandbox.d.ts +0 -219
  45. package/dist/create-sandbox.js +0 -231
  46. package/dist/utils/deferred-generator.d.ts +0 -5
  47. package/dist/utils/deferred-generator.js +0 -32
  48. package/dist/utils/deferred.d.ts +0 -6
  49. package/dist/utils/deferred.js +0 -12
  50. package/src/client/client.ts +0 -188
  51. package/src/create-sandbox.ts +0 -330
  52. package/src/utils/deferred-generator.ts +0 -38
  53. package/src/utils/deferred.ts +0 -12
  54. /package/dist/{client → api-client}/api-error.d.ts +0 -0
  55. /package/dist/{client → api-client}/api-error.js +0 -0
  56. /package/dist/{client → api-client}/with-retry.d.ts +0 -0
  57. /package/dist/{client → api-client}/with-retry.js +0 -0
  58. /package/src/{client → api-client}/api-error.ts +0 -0
  59. /package/src/{client → api-client}/with-retry.ts +0 -0
@@ -0,0 +1,225 @@
1
+ import FormData from "form-data";
2
+ import {
3
+ BaseClient,
4
+ parseOrThrow,
5
+ type Parsed,
6
+ type RequestParams,
7
+ } from "./base-client";
8
+ import {
9
+ Command,
10
+ CommandFinished,
11
+ CreatedCommand,
12
+ CreatedSandbox,
13
+ LogLine,
14
+ StoppedSandbox,
15
+ WrittenFile,
16
+ } from "./validators";
17
+ import { Readable } from "stream";
18
+ import { APIError } from "./api-error";
19
+ import { LRUCache } from "lru-cache";
20
+ import { VERSION } from "../version";
21
+ import { z } from "zod";
22
+ import jsonlines from "jsonlines";
23
+ import os from "os";
24
+ import ms from "ms";
25
+
26
+ /**
27
+ * Allows to track the logs hits for a command un a to maximum of items and
28
+ * TTL so that we don't incur in memory leaks in a log running process.
29
+ */
30
+ const logHits = new LRUCache<string, boolean>({
31
+ ttl: ms("45m"),
32
+ max: 1000,
33
+ });
34
+
35
+ export class APIClient extends BaseClient {
36
+ private teamId: string;
37
+
38
+ constructor(params: { host?: string; teamId: string; token: string }) {
39
+ super({
40
+ host: params.host ?? "https://api.vercel.com",
41
+ token: params.token,
42
+ debug: false,
43
+ });
44
+
45
+ this.teamId = params.teamId;
46
+ }
47
+
48
+ protected async request(path: string, params?: RequestParams) {
49
+ return super.request(path, {
50
+ ...params,
51
+ query: { teamId: this.teamId, ...params?.query },
52
+ headers: {
53
+ "content-type": "application/json",
54
+ "user-agent": `vercel/sandbox/${VERSION} (Node.js/${process.version}; ${os.platform()}/${os.arch()})`,
55
+ ...params?.headers,
56
+ },
57
+ });
58
+ }
59
+
60
+ async createSandbox(params: {
61
+ ports?: number[];
62
+ projectId: string;
63
+ source?: { type: "git"; url: string } | { type: "tarball"; url: string };
64
+ timeout?: number;
65
+ resources?: { cores: number; memory: number };
66
+ }) {
67
+ return parseOrThrow(
68
+ CreatedSandbox,
69
+ await this.request("/v1/sandboxes", {
70
+ method: "POST",
71
+ body: JSON.stringify({
72
+ projectId: params.projectId,
73
+ ports: params.ports,
74
+ source: params.source,
75
+ timeout: params.timeout,
76
+ resources: params.resources,
77
+ }),
78
+ }),
79
+ );
80
+ }
81
+
82
+ async runCommand(params: {
83
+ sandboxId: string;
84
+ cwd?: string;
85
+ command: string;
86
+ args: string[];
87
+ env: Record<string, string>;
88
+ }) {
89
+ return parseOrThrow(
90
+ CreatedCommand,
91
+ await this.request(`/v1/sandboxes/${params.sandboxId}/cmd`, {
92
+ method: "POST",
93
+ body: JSON.stringify({
94
+ command: params.command,
95
+ args: params.args,
96
+ cwd: params.cwd,
97
+ env: params.env,
98
+ }),
99
+ }),
100
+ );
101
+ }
102
+
103
+ async getCommand(params: {
104
+ sandboxId: string;
105
+ cmdId: string;
106
+ wait: true;
107
+ }): Promise<Parsed<z.infer<typeof CommandFinished>>>;
108
+ async getCommand(params: {
109
+ sandboxId: string;
110
+ cmdId: string;
111
+ wait?: boolean;
112
+ }): Promise<Parsed<z.infer<typeof Command>>>;
113
+ async getCommand(params: {
114
+ sandboxId: string;
115
+ cmdId: string;
116
+ wait?: boolean;
117
+ }) {
118
+ return params.wait
119
+ ? parseOrThrow(
120
+ CommandFinished,
121
+ await this.request(
122
+ `/v1/sandboxes/${params.sandboxId}/cmd/${params.cmdId}`,
123
+ { query: { wait: "true" } },
124
+ ),
125
+ )
126
+ : parseOrThrow(
127
+ Command,
128
+ await this.request(
129
+ `/v1/sandboxes/${params.sandboxId}/cmd/${params.cmdId}`,
130
+ ),
131
+ );
132
+ }
133
+
134
+ async mkDir(params: { sandboxId: string; path: string; cwd?: string }) {
135
+ return parseOrThrow(
136
+ WrittenFile,
137
+ await this.request(`/v1/sandboxes/${params.sandboxId}/fs/mkdir`, {
138
+ method: "POST",
139
+ body: JSON.stringify({ path: params.path, cwd: params.cwd }),
140
+ }),
141
+ );
142
+ }
143
+
144
+ async writeFiles(params: {
145
+ sandboxId: string;
146
+ files: { path: string; stream: Readable | Buffer }[];
147
+ }) {
148
+ const formData = new FormData();
149
+
150
+ for (const file of params.files) {
151
+ formData.append(file.path, file.stream, file.path);
152
+ }
153
+
154
+ await parseOrThrow(
155
+ WrittenFile,
156
+ await this.request(`/v1/sandboxes/${params.sandboxId}/fs/write`, {
157
+ method: "POST",
158
+ headers: { ...formData.getHeaders() },
159
+ body: formData,
160
+ }),
161
+ );
162
+ }
163
+
164
+ async readFile(params: {
165
+ sandboxId: string;
166
+ path: string;
167
+ cwd?: string;
168
+ }): Promise<NodeJS.ReadableStream | null> {
169
+ const response = await this.request(
170
+ `/v1/sandboxes/${params.sandboxId}/fs/read`,
171
+ {
172
+ method: "POST",
173
+ body: JSON.stringify({ path: params.path, cwd: params.cwd }),
174
+ },
175
+ );
176
+
177
+ if (response.status === 404) {
178
+ return null;
179
+ }
180
+
181
+ return response.body;
182
+ }
183
+
184
+ async *getLogs(params: {
185
+ sandboxId: string;
186
+ cmdId: string;
187
+ }): AsyncIterable<z.infer<typeof LogLine>> {
188
+ const url = `/v1/sandboxes/${params.sandboxId}/cmd/${params.cmdId}/logs`;
189
+ const response = await this.request(url, { method: "GET" });
190
+ if (response.headers.get("content-type") !== "application/x-ndjson") {
191
+ throw new APIError(response, {
192
+ message: "Expected a stream of logs",
193
+ });
194
+ }
195
+
196
+ /**
197
+ * Currently, once we consume logs in the backend we cannot read them
198
+ * again. This logic writes a warning when the endpoint for a command
199
+ * logs is consumed more than once to alert the user about this.
200
+ *
201
+ * This is a temporary solution, we should be able to handle this in
202
+ * the backend in the future.
203
+ */
204
+ if (logHits.get(url)) {
205
+ console.warn(
206
+ `Multiple consumers for logs of command \`${params.cmdId}\`. This may lead to unexpected behavior.`,
207
+ );
208
+ }
209
+
210
+ logHits.set(url, true);
211
+ for await (const chunk of response.body.pipe(jsonlines.parse())) {
212
+ yield LogLine.parse(chunk);
213
+ }
214
+ }
215
+
216
+ async stopSandbox(params: {
217
+ sandboxId: string;
218
+ }): Promise<Parsed<z.infer<typeof StoppedSandbox>>> {
219
+ const url = `/v1/sandboxes/${params.sandboxId}/stop`;
220
+ return parseOrThrow(
221
+ StoppedSandbox,
222
+ await this.request(url, { method: "POST" }),
223
+ );
224
+ }
225
+ }
@@ -18,7 +18,7 @@ export interface RequestParams extends RequestInit {
18
18
  * we can pass query parameters as an object, support retries, debugging
19
19
  * and automatic authorization.
20
20
  */
21
- export class APIClient {
21
+ export class BaseClient {
22
22
  protected token?: string;
23
23
  private fetch: ReturnType<typeof withRetry<RequestInit>>;
24
24
  private debug: boolean;
@@ -0,0 +1 @@
1
+ export { APIClient } from "./api-client";
@@ -17,12 +17,20 @@ export const Command = z.object({
17
17
  name: z.string(),
18
18
  });
19
19
 
20
- export const FinishedCommand = z.object({
20
+ export const CommandFinished = z.object({
21
+ args: z.array(z.string()),
21
22
  cmdId: z.string(),
23
+ cwd: z.string(),
24
+ exitCode: z.number(),
25
+ name: z.string(),
22
26
  });
23
27
 
24
28
  export const WrittenFile = z.object({});
25
29
 
30
+ export const StoppedSandbox = z.object({
31
+ sandboxId: z.string(),
32
+ });
33
+
26
34
  export const LogLine = z.object({
27
35
  stream: z.enum(["stdout", "stderr"]),
28
36
  data: z.string(),
@@ -0,0 +1,51 @@
1
+ import { it, expect, vi } from "vitest";
2
+ import { Sandbox } from "./sandbox";
3
+
4
+ it("warns when there is more than one logs consumer", async () => {
5
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
6
+ const stdoutSpy = vi
7
+ .spyOn(process.stdout, "write")
8
+ .mockImplementation(() => true);
9
+
10
+ const sandbox = await Sandbox.create({
11
+ projectId: process.env.VERCEL_PROJECT_ID!,
12
+ teamId: process.env.VERCEL_TEAM_ID!,
13
+ token: process.env.VERCEL_TOKEN!,
14
+ });
15
+
16
+ const cmd = await sandbox.runCommand({
17
+ cmd: "echo",
18
+ args: ["Hello World!"],
19
+ stdout: process.stdout,
20
+ });
21
+
22
+ expect(await cmd.stdout()).toEqual("");
23
+ expect(stdoutSpy).toHaveBeenCalledWith("Hello World!\n");
24
+ expect(warnSpy).toHaveBeenCalledWith(
25
+ expect.stringMatching(
26
+ /Multiple consumers for logs of command `[^`]+`\.\sThis may lead to unexpected behavior\./,
27
+ ),
28
+ );
29
+
30
+ await sandbox.stop();
31
+ });
32
+
33
+ it("does not warn when there is only one logs consumer", async () => {
34
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
35
+
36
+ const sandbox = await Sandbox.create({
37
+ projectId: process.env.VERCEL_PROJECT_ID!,
38
+ teamId: process.env.VERCEL_TEAM_ID!,
39
+ token: process.env.VERCEL_TOKEN!,
40
+ });
41
+
42
+ const cmd = await sandbox.runCommand({
43
+ cmd: "echo",
44
+ args: ["Hello World!"],
45
+ });
46
+
47
+ expect(await cmd.stdout()).toEqual("Hello World!\n");
48
+ expect(warnSpy).not.toHaveBeenCalled();
49
+
50
+ await sandbox.stop();
51
+ });
package/src/command.ts ADDED
@@ -0,0 +1,176 @@
1
+ import { APIClient } from "./api-client";
2
+
3
+ /**
4
+ * A command executed in a Sandbox.
5
+ *
6
+ * You can {@link wait} on commands to access their {@link CommandFinished.exitCode}, and
7
+ * iterate over their output with {@link logs}.
8
+ *
9
+ * @see {@link Sandbox.runCommand} to start a command.
10
+ *
11
+ * @hideconstructor
12
+ */
13
+ export class Command {
14
+ /**
15
+ * @internal
16
+ * @private
17
+ */
18
+ protected client: APIClient;
19
+
20
+ /**
21
+ * ID of the sandbox this command is running in.
22
+ */
23
+ private sandboxId: string;
24
+
25
+ /**
26
+ * ID of the command execution.
27
+ */
28
+ public cmdId: string;
29
+
30
+ /**
31
+ * @param params - Object containing the client, sandbox ID, and command ID.
32
+ * @param params.client - API client used to interact with the backend.
33
+ * @param params.sandboxId - The ID of the sandbox where the command is running.
34
+ * @param params.cmdId - The ID of the command execution.
35
+ */
36
+ constructor({
37
+ client,
38
+ sandboxId,
39
+ cmdId,
40
+ }: {
41
+ client: APIClient;
42
+ sandboxId: string;
43
+ cmdId: string;
44
+ }) {
45
+ this.client = client;
46
+ this.sandboxId = sandboxId;
47
+ this.cmdId = cmdId;
48
+ }
49
+
50
+ /**
51
+ * Iterate over the output of this command.
52
+ *
53
+ * ```
54
+ * for await (const log of cmd.logs()) {
55
+ * if (log.stream === "stdout") {
56
+ * process.stdout.write(log.data);
57
+ * } else {
58
+ * process.stderr.write(log.data);
59
+ * }
60
+ * }
61
+ * ```
62
+ *
63
+ * @returns An async iterable of log entries from the command output.
64
+ *
65
+ * @see {@link Command.stdout}, {@link Command.stderr}, and {@link Command.output}
66
+ * to access output as a string.
67
+ */
68
+ logs() {
69
+ return this.client.getLogs({
70
+ sandboxId: this.sandboxId,
71
+ cmdId: this.cmdId,
72
+ });
73
+ }
74
+
75
+ /**
76
+ * Wait for a command to exit and populate its exit code.
77
+ *
78
+ * ```
79
+ * await cmd.wait()
80
+ * if (cmd.exitCode != 0) {
81
+ * console.error("Something went wrong...")
82
+ * }
83
+ * ```
84
+ *
85
+ * @returns A {@link CommandFinished} instance with populated exit code.
86
+ */
87
+ async wait() {
88
+ const command = await this.client.getCommand({
89
+ sandboxId: this.sandboxId,
90
+ cmdId: this.cmdId,
91
+ wait: true,
92
+ });
93
+
94
+ return new CommandFinished({
95
+ client: this.client,
96
+ sandboxId: this.sandboxId,
97
+ cmdId: command.json.cmdId,
98
+ exitCode: command.json.exitCode,
99
+ });
100
+ }
101
+
102
+ /**
103
+ * Get the output of `stdout`, `stderr`, or both as a string.
104
+ *
105
+ * NOTE: This may throw string conversion errors if the command does
106
+ * not output valid Unicode.
107
+ *
108
+ * @param stream - The output stream to read: "stdout", "stderr", or "both".
109
+ * @returns The output of the specified stream(s) as a string.
110
+ */
111
+ async output(stream: "stdout" | "stderr" | "both" = "both") {
112
+ let data = "";
113
+ for await (const log of this.logs()) {
114
+ if (stream === "both" || log.stream === stream) {
115
+ data += log.data;
116
+ }
117
+ }
118
+ return data;
119
+ }
120
+
121
+ /**
122
+ * Get the output of `stdout` as a string.
123
+ *
124
+ * NOTE: This may throw string conversion errors if the command does
125
+ * not output valid Unicode.
126
+ *
127
+ * @returns The standard output of the command.
128
+ */
129
+ async stdout() {
130
+ return this.output("stdout");
131
+ }
132
+
133
+ /**
134
+ * Get the output of `stderr` as a string.
135
+ *
136
+ * NOTE: This may throw string conversion errors if the command does
137
+ * not output valid Unicode.
138
+ *
139
+ * @returns The standard error output of the command.
140
+ */
141
+ async stderr() {
142
+ return this.output("stderr");
143
+ }
144
+ }
145
+
146
+ /**
147
+ * A command that has finished executing.
148
+ *
149
+ * Contains the exit code of the command.
150
+ *
151
+ * @hideconstructor
152
+ */
153
+ export class CommandFinished extends Command {
154
+ /**
155
+ * The exit code of the command, if available. This is set after
156
+ * {@link wait} has returned.
157
+ */
158
+ public exitCode: number;
159
+
160
+ /**
161
+ * @param params - Object containing client, sandbox ID, command ID, and exit code.
162
+ * @param params.client - API client used to interact with the backend.
163
+ * @param params.sandboxId - The ID of the sandbox where the command ran.
164
+ * @param params.cmdId - The ID of the command execution.
165
+ * @param params.exitCode - The exit code of the completed command.
166
+ */
167
+ constructor(params: {
168
+ client: APIClient;
169
+ sandboxId: string;
170
+ cmdId: string;
171
+ exitCode: number;
172
+ }) {
173
+ super({ ...params });
174
+ this.exitCode = params.exitCode;
175
+ }
176
+ }
package/src/index.ts CHANGED
@@ -1 +1,2 @@
1
- export { SDK, Sandbox, Command } from "./create-sandbox";
1
+ export { Sandbox } from "./sandbox";
2
+ export { Command, CommandFinished } from "./command";