@vercel/sandbox 0.0.1

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.
@@ -0,0 +1,175 @@
1
+ import { SandboxClient } from "./client/client";
2
+ import { Readable } from "stream";
3
+ /**
4
+ * Create a new instance of the SandboxSDK.
5
+ *
6
+ * ````
7
+ * const teamId = process.env.VERCEL_TEAM_ID
8
+ * const token = process.env.VERCEL_TOKEN
9
+ * const sdk = new SandboxSDK({ teamId: teamId!, token: token! });
10
+ * ````
11
+ *
12
+ * @see {@link createSandbox} to start a sandbox.
13
+ */
14
+ export declare class SandboxSDK {
15
+ client: SandboxClient;
16
+ constructor({ teamId, token }: {
17
+ teamId: string;
18
+ token: string;
19
+ });
20
+ /**
21
+ * Create a new Sandbox with the given resources, source code, and ports.
22
+ *
23
+ * Upon start, the sandbox will perform a `git clone` of the repository given.
24
+ * This repo needs to be public.
25
+ *
26
+ * @param params
27
+ * @returns a
28
+ */
29
+ createSandbox(params: {
30
+ source: {
31
+ type: "git";
32
+ url: string;
33
+ };
34
+ ports: number[];
35
+ timeout?: number;
36
+ }): Promise<Sandbox>;
37
+ /** @hidden */
38
+ getSandbox({ routes, sandboxId, }: {
39
+ routes: {
40
+ subdomain: string;
41
+ port: number;
42
+ }[];
43
+ sandboxId: string;
44
+ }): Promise<Sandbox>;
45
+ }
46
+ /**
47
+ * A Sandbox is an isolated Linux MicroVM that you can your experiments on.
48
+ *
49
+ * @see {@link SandboxSDK.createSandbox} to construct a Sandbox.
50
+ * @hideconstructor
51
+ */
52
+ export declare class Sandbox {
53
+ private client;
54
+ /** @hidden */
55
+ routes: {
56
+ subdomain: string;
57
+ port: number;
58
+ }[];
59
+ /**
60
+ * The ID of this sandbox.
61
+ */
62
+ sandboxId: string;
63
+ constructor({ client, routes, sandboxId, }: {
64
+ client: SandboxClient;
65
+ routes: {
66
+ subdomain: string;
67
+ port: number;
68
+ }[];
69
+ sandboxId: string;
70
+ });
71
+ /**
72
+ * Start executing a command in this sandbox.
73
+ * @param command
74
+ * @param args
75
+ * @returns
76
+ */
77
+ runCommand(command: string, args?: string[]): Promise<Command>;
78
+ /**
79
+ * Write files to the filesystem of this sandbox.
80
+ */
81
+ writeFiles(files: {
82
+ path: string;
83
+ stream: Readable | Buffer;
84
+ }[]): Promise<void>;
85
+ /**
86
+ * Get the public domain of a port of this sandbox.
87
+ *
88
+ * E.g. `2grza2l7imxe.vercel.run`
89
+ */
90
+ domain(p: number): string;
91
+ }
92
+ /**
93
+ * A command executed in a Sandbox.
94
+ *
95
+ * You can {@link wait} on commands to access their {@link exitCode}, and
96
+ * iterate over their output with {@link logs}.
97
+ *
98
+ * @see {@link Sandbox.runCommand} to start a command.
99
+ *
100
+ * @hideconstructor
101
+ */
102
+ export declare class Command {
103
+ private client;
104
+ private sandboxId;
105
+ /**
106
+ * ID of the command execution.
107
+ */
108
+ cmdId: string;
109
+ /**
110
+ * The exit code of the command, if available. This is set after
111
+ * {@link wait} has returned.
112
+ */
113
+ exitCode: number | null;
114
+ constructor({ client, sandboxId, cmdId, }: {
115
+ client: SandboxClient;
116
+ sandboxId: string;
117
+ cmdId: string;
118
+ });
119
+ /**
120
+ * Iterate over the output of this command.
121
+ *
122
+ * ```
123
+ * for await (const log of cmd.logs()) {
124
+ * if (log.stream === "stdout") {
125
+ * process.stdout.write(log.data);
126
+ * } else {
127
+ * process.stderr.write(log.data);
128
+ * }
129
+ * }
130
+ * ```
131
+ *
132
+ * @see {@link Command.stdout}, {@link Command.stderr}, and {@link Command.output}
133
+ * to access output as a string.
134
+ */
135
+ logs(): AsyncGenerator<{
136
+ data: string;
137
+ stream: "stdout" | "stderr";
138
+ }, void, void>;
139
+ /**
140
+ * Wait for a command to exit and populate it's exit code.
141
+ *
142
+ * ```
143
+ * await cmd.wait()
144
+ * if (cmd.exitCode != 0) {
145
+ * console.error("Something went wrong...")
146
+ * }
147
+ * ````
148
+ */
149
+ wait(): Promise<this>;
150
+ /**
151
+ * Print command logs to stdout/stderr
152
+ */
153
+ printLogs(): Promise<void>;
154
+ /**
155
+ * Get the output of `stdout` or `stderr` as a string.
156
+ *
157
+ * NOTE: This may error with string conversion errors if the command does
158
+ * not ouptut valid unicode.
159
+ */
160
+ output(stream?: "stdout" | "stderr" | "both"): Promise<string>;
161
+ /**
162
+ * Get the output of `stdout` as a string.
163
+ *
164
+ * NOTE: This may error with string conversion errors if the command does
165
+ * not ouptut valid unicode.
166
+ */
167
+ stdout(): Promise<string>;
168
+ /**
169
+ * Get the output of `stderr` as a string.
170
+ *
171
+ * NOTE: This may error with string conversion errors if the command does
172
+ * not ouptut valid unicode.
173
+ */
174
+ stderr(): Promise<string>;
175
+ }
@@ -0,0 +1,212 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Command = exports.Sandbox = exports.SandboxSDK = void 0;
4
+ const client_1 = require("./client/client");
5
+ /**
6
+ * Create a new instance of the SandboxSDK.
7
+ *
8
+ * ````
9
+ * const teamId = process.env.VERCEL_TEAM_ID
10
+ * const token = process.env.VERCEL_TOKEN
11
+ * const sdk = new SandboxSDK({ teamId: teamId!, token: token! });
12
+ * ````
13
+ *
14
+ * @see {@link createSandbox} to start a sandbox.
15
+ */
16
+ class SandboxSDK {
17
+ constructor({ teamId, token }) {
18
+ this.client = new client_1.SandboxClient({ teamId, token });
19
+ }
20
+ /**
21
+ * Create a new Sandbox with the given resources, source code, and ports.
22
+ *
23
+ * Upon start, the sandbox will perform a `git clone` of the repository given.
24
+ * This repo needs to be public.
25
+ *
26
+ * @param params
27
+ * @returns a
28
+ */
29
+ async createSandbox(params) {
30
+ const { client } = this;
31
+ const sandbox = await client.createSandbox({
32
+ source: params.source,
33
+ ports: params.ports,
34
+ timeout: params.timeout,
35
+ });
36
+ return new Sandbox({
37
+ client,
38
+ sandboxId: sandbox.json.sandboxId,
39
+ routes: sandbox.json.routes,
40
+ });
41
+ }
42
+ /** @hidden */
43
+ async getSandbox({ routes, sandboxId, }) {
44
+ return new Sandbox({
45
+ client: this.client,
46
+ sandboxId: sandboxId,
47
+ routes: routes,
48
+ });
49
+ }
50
+ }
51
+ exports.SandboxSDK = SandboxSDK;
52
+ /**
53
+ * A Sandbox is an isolated Linux MicroVM that you can your experiments on.
54
+ *
55
+ * @see {@link SandboxSDK.createSandbox} to construct a Sandbox.
56
+ * @hideconstructor
57
+ */
58
+ class Sandbox {
59
+ constructor({ client, routes, sandboxId, }) {
60
+ this.client = client;
61
+ this.routes = routes;
62
+ this.sandboxId = sandboxId;
63
+ }
64
+ /**
65
+ * Start executing a command in this sandbox.
66
+ * @param command
67
+ * @param args
68
+ * @returns
69
+ */
70
+ async runCommand(command, args = []) {
71
+ const commandResponse = await this.client.runCommand({
72
+ sandboxId: this.sandboxId,
73
+ command,
74
+ args,
75
+ });
76
+ return new Command({
77
+ client: this.client,
78
+ sandboxId: this.sandboxId,
79
+ cmdId: commandResponse.json.cmdId,
80
+ });
81
+ }
82
+ /**
83
+ * Write files to the filesystem of this sandbox.
84
+ */
85
+ async writeFiles(files) {
86
+ return this.client.writeFiles({
87
+ sandboxId: this.sandboxId,
88
+ files: files,
89
+ });
90
+ }
91
+ /**
92
+ * Get the public domain of a port of this sandbox.
93
+ *
94
+ * E.g. `2grza2l7imxe.vercel.run`
95
+ */
96
+ domain(p) {
97
+ const route = this.routes.find(({ port }) => port == p);
98
+ if (route) {
99
+ return `https://${route.subdomain}.vercel.run`;
100
+ }
101
+ else {
102
+ throw new Error(`No route for port ${p}`);
103
+ }
104
+ }
105
+ }
106
+ exports.Sandbox = Sandbox;
107
+ /**
108
+ * A command executed in a Sandbox.
109
+ *
110
+ * You can {@link wait} on commands to access their {@link exitCode}, and
111
+ * iterate over their output with {@link logs}.
112
+ *
113
+ * @see {@link Sandbox.runCommand} to start a command.
114
+ *
115
+ * @hideconstructor
116
+ */
117
+ class Command {
118
+ constructor({ client, sandboxId, cmdId, }) {
119
+ this.client = client;
120
+ this.sandboxId = sandboxId;
121
+ this.cmdId = cmdId;
122
+ this.exitCode = null;
123
+ }
124
+ /**
125
+ * Iterate over the output of this command.
126
+ *
127
+ * ```
128
+ * for await (const log of cmd.logs()) {
129
+ * if (log.stream === "stdout") {
130
+ * process.stdout.write(log.data);
131
+ * } else {
132
+ * process.stderr.write(log.data);
133
+ * }
134
+ * }
135
+ * ```
136
+ *
137
+ * @see {@link Command.stdout}, {@link Command.stderr}, and {@link Command.output}
138
+ * to access output as a string.
139
+ */
140
+ logs() {
141
+ return this.client.getLogs({
142
+ sandboxId: this.sandboxId,
143
+ cmdId: this.cmdId,
144
+ });
145
+ }
146
+ /**
147
+ * Wait for a command to exit and populate it's exit code.
148
+ *
149
+ * ```
150
+ * await cmd.wait()
151
+ * if (cmd.exitCode != 0) {
152
+ * console.error("Something went wrong...")
153
+ * }
154
+ * ````
155
+ */
156
+ async wait() {
157
+ const command = await this.client.getCommand({
158
+ sandboxId: this.sandboxId,
159
+ cmdId: this.cmdId,
160
+ wait: true,
161
+ });
162
+ this.exitCode = command.json.exitCode;
163
+ return this;
164
+ }
165
+ /**
166
+ * Print command logs to stdout/stderr
167
+ */
168
+ async printLogs() {
169
+ for await (const log of this.logs()) {
170
+ if (log.stream === "stdout") {
171
+ process.stdout.write(log.data);
172
+ }
173
+ else {
174
+ process.stderr.write(log.data);
175
+ }
176
+ }
177
+ }
178
+ /**
179
+ * Get the output of `stdout` or `stderr` as a string.
180
+ *
181
+ * NOTE: This may error with string conversion errors if the command does
182
+ * not ouptut valid unicode.
183
+ */
184
+ async output(stream = "both") {
185
+ let data = "";
186
+ for await (const log of this.logs()) {
187
+ if (log.stream === stream) {
188
+ data += log.data;
189
+ }
190
+ }
191
+ return data;
192
+ }
193
+ /**
194
+ * Get the output of `stdout` as a string.
195
+ *
196
+ * NOTE: This may error with string conversion errors if the command does
197
+ * not ouptut valid unicode.
198
+ */
199
+ async stdout() {
200
+ return this.output("stdout");
201
+ }
202
+ /**
203
+ * Get the output of `stderr` as a string.
204
+ *
205
+ * NOTE: This may error with string conversion errors if the command does
206
+ * not ouptut valid unicode.
207
+ */
208
+ async stderr() {
209
+ return this.output("stderr");
210
+ }
211
+ }
212
+ exports.Command = Command;
@@ -0,0 +1,2 @@
1
+ export { SandboxSDK, Sandbox, Command } from "./create-sandbox";
2
+ export { SandboxClient } from "./client/client";
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SandboxClient = exports.Command = exports.Sandbox = exports.SandboxSDK = void 0;
4
+ var create_sandbox_1 = require("./create-sandbox");
5
+ Object.defineProperty(exports, "SandboxSDK", { enumerable: true, get: function () { return create_sandbox_1.SandboxSDK; } });
6
+ Object.defineProperty(exports, "Sandbox", { enumerable: true, get: function () { return create_sandbox_1.Sandbox; } });
7
+ Object.defineProperty(exports, "Command", { enumerable: true, get: function () { return create_sandbox_1.Command; } });
8
+ var client_1 = require("./client/client");
9
+ Object.defineProperty(exports, "SandboxClient", { enumerable: true, get: function () { return client_1.SandboxClient; } });
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Returns an array from the given item. If the item is an array it will be
3
+ * returned as a it is, otherwise it will be returned as a single item array.
4
+ * If the item is undefined or null an empty array will be returned.
5
+ *
6
+ * @param item The item to convert to an array.
7
+ * @returns An array.
8
+ */
9
+ export declare function array<T>(item?: null | T | T[]): T[];
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.array = array;
4
+ /**
5
+ * Returns an array from the given item. If the item is an array it will be
6
+ * returned as a it is, otherwise it will be returned as a single item array.
7
+ * If the item is undefined or null an empty array will be returned.
8
+ *
9
+ * @param item The item to convert to an array.
10
+ * @returns An array.
11
+ */
12
+ function array(item) {
13
+ return item !== undefined && item !== null
14
+ ? Array.isArray(item)
15
+ ? item
16
+ : [item]
17
+ : [];
18
+ }
@@ -0,0 +1,5 @@
1
+ export interface DeferredGenerator<T, R> {
2
+ generator(): AsyncGenerator<T, R, void>;
3
+ next: (value: IteratorResult<T | Promise<T>>) => void;
4
+ }
5
+ export declare function createDeferredGenerator<T, R>(): DeferredGenerator<T, R>;
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createDeferredGenerator = createDeferredGenerator;
4
+ const deferred_1 = require("./deferred");
5
+ function createDeferredGenerator() {
6
+ const deferreds = [new deferred_1.Deferred()];
7
+ let currentIndex = 0;
8
+ const next = (value) => {
9
+ const currentDeferred = deferreds[currentIndex];
10
+ if (!value.done) {
11
+ deferreds.push(new deferred_1.Deferred());
12
+ currentIndex++;
13
+ }
14
+ currentDeferred.resolve(value);
15
+ };
16
+ function generator() {
17
+ let currentIndex = 0;
18
+ return (async function* () {
19
+ while (true) {
20
+ const result = await deferreds[currentIndex].promise;
21
+ if (result.done)
22
+ return result.value;
23
+ yield result.value;
24
+ currentIndex++;
25
+ }
26
+ })();
27
+ }
28
+ return {
29
+ generator,
30
+ next,
31
+ };
32
+ }
@@ -0,0 +1,6 @@
1
+ export declare class Deferred<T> {
2
+ promise: Promise<T>;
3
+ resolve: (value: T | PromiseLike<T>) => void;
4
+ reject: (reason?: any) => void;
5
+ constructor();
6
+ }
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Deferred = void 0;
4
+ class Deferred {
5
+ constructor() {
6
+ this.promise = new Promise((res, rej) => {
7
+ this.resolve = res;
8
+ this.reject = rej;
9
+ });
10
+ }
11
+ }
12
+ exports.Deferred = Deferred;
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@vercel/sandbox",
3
+ "version": "0.0.1",
4
+ "description": "",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "keywords": [],
8
+ "author": "",
9
+ "license": "ISC",
10
+ "dependencies": {
11
+ "async-retry": "1.3.3",
12
+ "form-data": "3.0.0",
13
+ "jsonlines": "0.1.1",
14
+ "node-fetch": "2.6.11",
15
+ "zod": "3.24.4"
16
+ },
17
+ "devDependencies": {
18
+ "@types/async-retry": "1.4.9",
19
+ "@types/jsonlines": "0.1.5",
20
+ "@types/node": "22.15.12",
21
+ "@types/node-fetch": "2.6.12",
22
+ "typedoc": "^0.28.4",
23
+ "typescript": "5.8.3"
24
+ },
25
+ "scripts": {
26
+ "build": "tsc",
27
+ "typedoc": "typedoc",
28
+ "typecheck": "tsc --noEmit"
29
+ }
30
+ }
@@ -0,0 +1,26 @@
1
+ import type { Response } from "node-fetch";
2
+
3
+ interface Options<ErrorData> {
4
+ message?: string;
5
+ json?: ErrorData;
6
+ text?: string;
7
+ }
8
+
9
+ export class APIError<ErrorData> extends Error {
10
+ public response: Response;
11
+ public message: string;
12
+ public json?: ErrorData;
13
+ public text?: string;
14
+
15
+ constructor(response: Response, options?: Options<ErrorData>) {
16
+ super(response.statusText);
17
+ if (Error.captureStackTrace) {
18
+ Error.captureStackTrace(this, APIError);
19
+ }
20
+
21
+ this.response = response;
22
+ this.message = options?.message ?? "";
23
+ this.json = options?.json;
24
+ this.text = options?.text;
25
+ }
26
+ }
@@ -0,0 +1,144 @@
1
+ import type { Options as RetryOptions } from "async-retry";
2
+ import { APIError } from "./api-error";
3
+ import { ZodType } from "zod";
4
+ import { array } from "../utils/array";
5
+ import { withRetry, type RequestOptions } from "./with-retry";
6
+ import nodeFetch, { type Response, type RequestInit } from "node-fetch";
7
+
8
+ export interface RequestParams extends RequestInit {
9
+ headers?: Record<string, string>;
10
+ method?: string;
11
+ onRetry?(error: any, options: RequestOptions): void;
12
+ query?: Record<string, number | string | null | undefined | string[]>;
13
+ retry?: Partial<RetryOptions>;
14
+ }
15
+
16
+ /**
17
+ * A base API client that provides a convenience wrapper for fetching where
18
+ * we can pass query parameters as an object, support retries, debugging
19
+ * and automatic authorization.
20
+ */
21
+ export class APIClient {
22
+ protected token?: string;
23
+ private fetch: ReturnType<typeof withRetry<RequestInit>>;
24
+ private debug: boolean;
25
+ private host: string;
26
+
27
+ constructor(params: { debug?: boolean; host: string; token?: string }) {
28
+ this.fetch = withRetry(nodeFetch);
29
+ this.host = params.host;
30
+ this.debug = params.debug ?? process.env.DEBUG_FETCH === "true";
31
+ this.token = params.token;
32
+ }
33
+
34
+ protected async request(path: string, opts?: RequestParams) {
35
+ const url = new URL(path, this.host);
36
+ if (opts?.query) {
37
+ for (const [key, value] of Object.entries(opts.query)) {
38
+ array(value).forEach((value) => {
39
+ url.searchParams.append(key, value.toString());
40
+ });
41
+ }
42
+ }
43
+
44
+ const start = Date.now();
45
+ const response = await this.fetch(url.toString(), {
46
+ ...opts,
47
+ body: opts?.body,
48
+ method: opts?.method || "GET",
49
+ headers: this.token
50
+ ? { Authorization: `Bearer ${this.token}`, ...opts?.headers }
51
+ : opts?.headers,
52
+ });
53
+
54
+ if (this.debug) {
55
+ const duration = Date.now() - start;
56
+ console.log(`[API] ${url} (${response.status}) ${duration}ms`);
57
+ if (response.status === 429) {
58
+ const retry = parseInt(response.headers.get("Retry-After") ?? "", 10);
59
+ const hours = Math.floor(retry / 60 / 60);
60
+ const minutes = Math.floor(retry / 60) % 60;
61
+ const seconds = retry % 60;
62
+ console.warn(
63
+ `[API] ${url} Rate Limited, Retry After ${hours}h ${minutes}m ${seconds}s`,
64
+ );
65
+ }
66
+ }
67
+
68
+ return response;
69
+ }
70
+ }
71
+
72
+ export interface Parsed<Data> {
73
+ response: Response;
74
+ text: string;
75
+ json: Data;
76
+ }
77
+
78
+ /**
79
+ * Allows to read the response text and parse it as JSON casting to the given
80
+ * type. If the response is not ok or cannot be parsed it will return error.
81
+ *
82
+ * @param response Response to parse.
83
+ * @returns Parsed response or error.
84
+ */
85
+ export async function parse<Data, ErrorData>(
86
+ validator: ZodType<Data>,
87
+ response: Response,
88
+ ): Promise<Parsed<Data> | APIError<ErrorData>> {
89
+ const text = await response.text().catch((err) => {
90
+ return new APIError<ErrorData>(response, {
91
+ message: `Can't read response text: ${String(err)}`,
92
+ });
93
+ });
94
+
95
+ if (typeof text !== "string") {
96
+ return text;
97
+ }
98
+
99
+ let json: Data | ErrorData;
100
+
101
+ try {
102
+ json = JSON.parse(text || "{}");
103
+ } catch (error) {
104
+ return new APIError<ErrorData>(response, {
105
+ message: `Can't parse JSON: ${String(error)}`,
106
+ text,
107
+ });
108
+ }
109
+
110
+ if (!response.ok) {
111
+ return new APIError<ErrorData>(response, {
112
+ message: `Status code ${response.status} is not ok`,
113
+ json: json as ErrorData,
114
+ text,
115
+ });
116
+ }
117
+
118
+ const validated = validator.safeParse(json);
119
+ if (!validated.success) {
120
+ return new APIError<ErrorData>(response, {
121
+ message: `Response JSON is not valid: ${validated.error}`,
122
+ json: json as ErrorData,
123
+ text,
124
+ });
125
+ }
126
+
127
+ return {
128
+ json: validated.data,
129
+ response,
130
+ text,
131
+ };
132
+ }
133
+
134
+ export async function parseOrThrow<Data, ErrorData>(
135
+ validator: ZodType<Data>,
136
+ response: Response,
137
+ ): Promise<Parsed<Data>> {
138
+ const result = await parse<Data, ErrorData>(validator, response);
139
+ if (result instanceof APIError) {
140
+ throw result;
141
+ }
142
+
143
+ return result;
144
+ }