@vercel/sandbox 0.0.5 → 0.0.7

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 (58) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-typecheck.log +1 -1
  3. package/CHANGELOG.md +14 -0
  4. package/README.md +2 -1
  5. package/dist/{client/client.d.ts → api-client/api-client.d.ts} +29 -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 +171 -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/{client/client.ts → api-client/api-client.ts} +93 -56
  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 +306 -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/create-sandbox.ts +0 -330
  51. package/src/utils/deferred-generator.ts +0 -38
  52. package/src/utils/deferred.ts +0 -12
  53. /package/dist/{client → api-client}/api-error.d.ts +0 -0
  54. /package/dist/{client → api-client}/api-error.js +0 -0
  55. /package/dist/{client → api-client}/with-retry.d.ts +0 -0
  56. /package/dist/{client → api-client}/with-retry.js +0 -0
  57. /package/src/{client → api-client}/api-error.ts +0 -0
  58. /package/src/{client → api-client}/with-retry.ts +0 -0
@@ -1,21 +1,38 @@
1
1
  import FormData from "form-data";
2
- import { APIClient, parseOrThrow, type RequestParams } from "./base-client";
2
+ import {
3
+ BaseClient,
4
+ parseOrThrow,
5
+ type Parsed,
6
+ type RequestParams,
7
+ } from "./base-client";
3
8
  import {
4
9
  Command,
10
+ CommandFinished,
5
11
  CreatedCommand,
6
12
  CreatedSandbox,
7
13
  LogLine,
14
+ StoppedSandbox,
8
15
  WrittenFile,
9
16
  } from "./validators";
10
17
  import { Readable } from "stream";
11
18
  import { APIError } from "./api-error";
12
- import { createDeferredGenerator } from "../utils/deferred-generator";
19
+ import { LRUCache } from "lru-cache";
13
20
  import { VERSION } from "../version";
14
21
  import { z } from "zod";
15
22
  import jsonlines from "jsonlines";
16
23
  import os from "os";
17
-
18
- export class SandboxClient extends APIClient {
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 {
19
36
  private teamId: string;
20
37
 
21
38
  constructor(params: { host?: string; teamId: string; token: string }) {
@@ -41,10 +58,11 @@ export class SandboxClient extends APIClient {
41
58
  }
42
59
 
43
60
  async createSandbox(params: {
44
- ports: number[];
61
+ ports?: number[];
45
62
  projectId: string;
46
- source: { type: "git"; url: string };
63
+ source?: { type: "git"; url: string } | { type: "tarball"; url: string };
47
64
  timeout?: number;
65
+ resources?: { vcpus: number };
48
66
  }) {
49
67
  return parseOrThrow(
50
68
  CreatedSandbox,
@@ -55,6 +73,7 @@ export class SandboxClient extends APIClient {
55
73
  ports: params.ports,
56
74
  source: params.source,
57
75
  timeout: params.timeout,
76
+ resources: params.resources,
58
77
  }),
59
78
  }),
60
79
  );
@@ -81,17 +100,44 @@ export class SandboxClient extends APIClient {
81
100
  );
82
101
  }
83
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>>>;
84
113
  async getCommand(params: {
85
114
  sandboxId: string;
86
115
  cmdId: string;
87
116
  wait?: boolean;
88
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 }) {
89
135
  return parseOrThrow(
90
- Command,
91
- await this.request(
92
- `/v1/sandboxes/${params.sandboxId}/cmd/${params.cmdId}`,
93
- { query: { wait: params.wait ? "true" : undefined } },
94
- ),
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
+ }),
95
141
  );
96
142
  }
97
143
 
@@ -135,54 +181,45 @@ export class SandboxClient extends APIClient {
135
181
  return response.body;
136
182
  }
137
183
 
138
- getLogs(params: { sandboxId: string; cmdId: string }) {
139
- const deferred = createDeferredGenerator<z.infer<typeof LogLine>, void>();
140
-
141
- (async () => {
142
- const response = await this.request(
143
- `/v1/sandboxes/${params.sandboxId}/cmd/${params.cmdId}/logs`,
144
- { method: "GET" },
145
- );
146
-
147
- if (response.headers.get("content-type") !== "application/x-ndjson") {
148
- throw new APIError(response, {
149
- message: "Expected a stream of logs",
150
- });
151
- }
152
-
153
- const parser = jsonlines.parse();
154
- response.body.pipe(parser);
155
-
156
- parser.on("data", (data) => {
157
- const parsed = LogLine.safeParse(data);
158
- if (parsed.success) {
159
- deferred.next({
160
- value: parsed.data,
161
- done: false,
162
- });
163
- } else {
164
- deferred.next({
165
- value: Promise.reject(parsed.error),
166
- done: false,
167
- });
168
- }
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",
169
193
  });
194
+ }
170
195
 
171
- parser.on("error", (err) => {
172
- deferred.next({
173
- value: Promise.reject(err),
174
- done: false,
175
- });
176
- });
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
+ }
177
209
 
178
- parser.on("end", () => {
179
- deferred.next({
180
- value: undefined,
181
- done: true,
182
- });
183
- });
184
- })();
210
+ logHits.set(url, true);
211
+ for await (const chunk of response.body.pipe(jsonlines.parse())) {
212
+ yield LogLine.parse(chunk);
213
+ }
214
+ }
185
215
 
186
- return deferred.generator();
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
+ );
187
224
  }
188
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";