@sandbox-engine/sdk 0.1.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.
package/README.md ADDED
@@ -0,0 +1,168 @@
1
+ # @sandbox-engine/sdk
2
+
3
+ SDK for creating and managing isolated sandbox environments. Each sandbox is an isolated Docker container with its own filesystem, process namespace, and network — run untrusted code safely.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @sandbox-engine/sdk
9
+ # or
10
+ pnpm add @sandbox-engine/sdk
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```ts
16
+ import { Sandbox } from '@sandbox-engine/sdk'
17
+
18
+ const sandbox = await Sandbox.create({
19
+ serverUrl: 'https://your-sandbox-server.com',
20
+ apiKey: 'your-api-key',
21
+ })
22
+
23
+ const result = await sandbox.exec('node', { args: ['-e', 'console.log(1 + 1)'] })
24
+ console.log(result.stdout) // "2\n"
25
+
26
+ await sandbox.close()
27
+ ```
28
+
29
+ ## Configuration via environment variables
30
+
31
+ Instead of passing `serverUrl` and `apiKey` every time, set environment variables:
32
+
33
+ ```bash
34
+ SANDBOX_SERVER_URL=https://your-sandbox-server.com
35
+ SANDBOX_API_KEY=your-api-key
36
+ ```
37
+
38
+ ```ts
39
+ // No options needed — reads from env
40
+ const sandbox = await Sandbox.create()
41
+ ```
42
+
43
+ ## Custom environments (Dockerfile)
44
+
45
+ Define exactly what's installed in your sandbox using a Dockerfile snippet:
46
+
47
+ ```ts
48
+ const sandbox = await Sandbox.create({
49
+ dockerfile: `
50
+ RUN npm install -g @anthropic-ai/claude-code typescript
51
+ RUN pip install numpy pandas matplotlib
52
+ `,
53
+ })
54
+
55
+ // First call: builds a Docker image (~seconds to minutes)
56
+ // Subsequent calls with same Dockerfile: instant (cached by content hash)
57
+ ```
58
+
59
+ You don't need a `FROM` — the base sandbox image is automatically prepended.
60
+
61
+ ## API
62
+
63
+ ### `Sandbox.create(opts?)`
64
+
65
+ Creates and starts a new sandbox. Returns when the sandbox is ready.
66
+
67
+ | Option | Type | Default | Description |
68
+ |--------|------|---------|-------------|
69
+ | `serverUrl` | `string` | `$SANDBOX_SERVER_URL` or `localhost:4000` | Sandbox server URL |
70
+ | `apiKey` | `string` | `$SANDBOX_API_KEY` or `dev-api-key` | API key |
71
+ | `template` | `string` | `'base'` | Pre-built Docker image tag |
72
+ | `dockerfile` | `string` | — | Custom Dockerfile content |
73
+ | `timeout` | `number` | `300000` | TTL in ms before auto-destroy |
74
+
75
+ ---
76
+
77
+ ### `sandbox.exec(cmd, opts?)`
78
+
79
+ Execute a command and wait for it to finish.
80
+
81
+ ```ts
82
+ const result = await sandbox.exec('python3', {
83
+ args: ['script.py'],
84
+ cwd: '/app',
85
+ env: { PYTHONPATH: '/app/lib' },
86
+ timeout: 30_000,
87
+ })
88
+ console.log(result.stdout) // string
89
+ console.log(result.stderr) // string
90
+ console.log(result.exitCode) // number
91
+ ```
92
+
93
+ #### Streaming exec
94
+
95
+ ```ts
96
+ const proc = await sandbox.exec('npm install', { stream: true })
97
+ proc.on('stdout', (data) => process.stdout.write(data))
98
+ proc.on('stderr', (data) => process.stderr.write(data))
99
+ const { exitCode } = await proc.wait()
100
+ ```
101
+
102
+ ---
103
+
104
+ ### `sandbox.fs`
105
+
106
+ ```ts
107
+ // Write a file
108
+ await sandbox.fs.write('/app/index.js', 'console.log("hello")')
109
+
110
+ // Read a file
111
+ const content = await sandbox.fs.read('/app/index.js')
112
+
113
+ // List a directory
114
+ const files = await sandbox.fs.list('/app')
115
+ // [{ name: 'index.js', path: '/app/index.js', isFile: true, isDirectory: false }]
116
+
117
+ // Delete a file or directory
118
+ await sandbox.fs.delete('/app/tmp')
119
+ ```
120
+
121
+ ---
122
+
123
+ ### `sandbox.terminal()`
124
+
125
+ Opens an interactive PTY terminal session.
126
+
127
+ ```ts
128
+ const term = await sandbox.terminal()
129
+
130
+ term.on('data', (data) => process.stdout.write(data))
131
+ term.write('ls -la\n')
132
+ term.resize(120, 40) // cols, rows
133
+ term.close()
134
+ ```
135
+
136
+ ---
137
+
138
+ ### `sandbox.ports.list()`
139
+
140
+ Returns host port mappings for ports exposed by the sandbox (3000–3004).
141
+
142
+ ```ts
143
+ const ports = await sandbox.ports.list()
144
+ // [{ containerPort: 3000, hostPort: 15234, url: 'http://host:15234' }]
145
+ ```
146
+
147
+ ---
148
+
149
+ ### `sandbox.close()`
150
+
151
+ Destroys the sandbox and frees all resources. Always call this when done.
152
+
153
+ ```ts
154
+ await sandbox.close()
155
+ ```
156
+
157
+ ## Server setup
158
+
159
+ The SDK connects to a **sandbox-engine server** you deploy. See the [server repository](https://github.com/your-org/sandbox-engine) for deployment instructions.
160
+
161
+ ### Quick deploy (Ubuntu VPS)
162
+
163
+ ```bash
164
+ # On your VPS
165
+ git clone https://github.com/your-org/sandbox-engine /opt/sandbox-engine
166
+ cd /opt/sandbox-engine
167
+ bash scripts/setup-vps.sh
168
+ ```
@@ -0,0 +1,118 @@
1
+ import WebSocket from 'ws';
2
+ import { EventEmitter } from 'node:events';
3
+
4
+ declare class HttpClient {
5
+ private readonly baseUrl;
6
+ private readonly apiKey;
7
+ constructor(baseUrl: string, apiKey: string);
8
+ private headers;
9
+ get<T>(path: string, query?: Record<string, string>): Promise<T>;
10
+ post<T>(path: string, body?: unknown, timeoutMs?: number): Promise<T>;
11
+ delete(path: string, query?: Record<string, string>): Promise<void>;
12
+ openWebSocket(path: string): WebSocket;
13
+ }
14
+
15
+ interface SandboxOptions {
16
+ serverUrl?: string;
17
+ apiKey?: string;
18
+ /**
19
+ * Name of a pre-built Docker image to use as the sandbox environment.
20
+ * Defaults to 'base' (sandbox-base:latest).
21
+ */
22
+ template?: string;
23
+ /**
24
+ * Custom Dockerfile content. The server will build a Docker image from it
25
+ * (using sandbox-base:latest as the base if no FROM is specified) and cache
26
+ * it by content hash. Subsequent calls with the same Dockerfile skip the build.
27
+ *
28
+ * @example
29
+ * dockerfile: `RUN npm install -g @anthropic-ai/claude-code`
30
+ */
31
+ dockerfile?: string;
32
+ timeout?: number;
33
+ }
34
+ interface ExecOptions {
35
+ args?: string[];
36
+ cwd?: string;
37
+ env?: Record<string, string>;
38
+ timeout?: number;
39
+ stream?: boolean;
40
+ }
41
+ interface ExecResult {
42
+ stdout: string;
43
+ stderr: string;
44
+ exitCode: number;
45
+ }
46
+ interface FileEntry {
47
+ name: string;
48
+ path: string;
49
+ isDirectory: boolean;
50
+ isFile: boolean;
51
+ }
52
+ interface PortMapping {
53
+ containerPort: number;
54
+ hostPort: number;
55
+ url: string;
56
+ }
57
+ type WsMessageType = 'stdout' | 'stderr' | 'exit' | 'error' | 'input' | 'resize';
58
+ interface WsMessage {
59
+ type: WsMessageType;
60
+ data?: string;
61
+ exitCode?: number;
62
+ cols?: number;
63
+ rows?: number;
64
+ }
65
+
66
+ declare class Filesystem {
67
+ private readonly sandboxId;
68
+ private readonly client;
69
+ constructor(sandboxId: string, client: HttpClient);
70
+ read(filePath: string): Promise<string>;
71
+ write(filePath: string, content: string): Promise<void>;
72
+ list(dirPath: string): Promise<FileEntry[]>;
73
+ delete(filePath: string): Promise<void>;
74
+ }
75
+
76
+ declare class Process extends EventEmitter {
77
+ private ws;
78
+ constructor(ws: WebSocket);
79
+ kill(): void;
80
+ wait(): Promise<ExecResult>;
81
+ }
82
+
83
+ declare class Terminal extends EventEmitter {
84
+ private ws;
85
+ constructor(ws: WebSocket);
86
+ write(data: string): void;
87
+ resize(cols: number, rows: number): void;
88
+ close(): void;
89
+ }
90
+
91
+ interface SandboxApiResponse {
92
+ id: string;
93
+ status: string;
94
+ template: string;
95
+ createdAt: string;
96
+ expiresAt: string;
97
+ agentUrl: string;
98
+ ports: Record<string, number>;
99
+ }
100
+ declare class Sandbox {
101
+ private readonly client;
102
+ readonly id: string;
103
+ readonly fs: Filesystem;
104
+ private constructor();
105
+ static create(opts?: SandboxOptions): Promise<Sandbox>;
106
+ exec(cmd: string, opts?: ExecOptions): Promise<ExecResult>;
107
+ exec(cmd: string, opts: ExecOptions & {
108
+ stream: true;
109
+ }): Promise<Process>;
110
+ terminal(): Promise<Terminal>;
111
+ readonly ports: {
112
+ list: () => Promise<PortMapping[]>;
113
+ };
114
+ info(): Promise<SandboxApiResponse>;
115
+ close(): Promise<void>;
116
+ }
117
+
118
+ export { type ExecOptions, type ExecResult, type FileEntry, Filesystem, type PortMapping, Process, Sandbox, type SandboxOptions, Terminal, type WsMessage, type WsMessageType };
@@ -0,0 +1,118 @@
1
+ import WebSocket from 'ws';
2
+ import { EventEmitter } from 'node:events';
3
+
4
+ declare class HttpClient {
5
+ private readonly baseUrl;
6
+ private readonly apiKey;
7
+ constructor(baseUrl: string, apiKey: string);
8
+ private headers;
9
+ get<T>(path: string, query?: Record<string, string>): Promise<T>;
10
+ post<T>(path: string, body?: unknown, timeoutMs?: number): Promise<T>;
11
+ delete(path: string, query?: Record<string, string>): Promise<void>;
12
+ openWebSocket(path: string): WebSocket;
13
+ }
14
+
15
+ interface SandboxOptions {
16
+ serverUrl?: string;
17
+ apiKey?: string;
18
+ /**
19
+ * Name of a pre-built Docker image to use as the sandbox environment.
20
+ * Defaults to 'base' (sandbox-base:latest).
21
+ */
22
+ template?: string;
23
+ /**
24
+ * Custom Dockerfile content. The server will build a Docker image from it
25
+ * (using sandbox-base:latest as the base if no FROM is specified) and cache
26
+ * it by content hash. Subsequent calls with the same Dockerfile skip the build.
27
+ *
28
+ * @example
29
+ * dockerfile: `RUN npm install -g @anthropic-ai/claude-code`
30
+ */
31
+ dockerfile?: string;
32
+ timeout?: number;
33
+ }
34
+ interface ExecOptions {
35
+ args?: string[];
36
+ cwd?: string;
37
+ env?: Record<string, string>;
38
+ timeout?: number;
39
+ stream?: boolean;
40
+ }
41
+ interface ExecResult {
42
+ stdout: string;
43
+ stderr: string;
44
+ exitCode: number;
45
+ }
46
+ interface FileEntry {
47
+ name: string;
48
+ path: string;
49
+ isDirectory: boolean;
50
+ isFile: boolean;
51
+ }
52
+ interface PortMapping {
53
+ containerPort: number;
54
+ hostPort: number;
55
+ url: string;
56
+ }
57
+ type WsMessageType = 'stdout' | 'stderr' | 'exit' | 'error' | 'input' | 'resize';
58
+ interface WsMessage {
59
+ type: WsMessageType;
60
+ data?: string;
61
+ exitCode?: number;
62
+ cols?: number;
63
+ rows?: number;
64
+ }
65
+
66
+ declare class Filesystem {
67
+ private readonly sandboxId;
68
+ private readonly client;
69
+ constructor(sandboxId: string, client: HttpClient);
70
+ read(filePath: string): Promise<string>;
71
+ write(filePath: string, content: string): Promise<void>;
72
+ list(dirPath: string): Promise<FileEntry[]>;
73
+ delete(filePath: string): Promise<void>;
74
+ }
75
+
76
+ declare class Process extends EventEmitter {
77
+ private ws;
78
+ constructor(ws: WebSocket);
79
+ kill(): void;
80
+ wait(): Promise<ExecResult>;
81
+ }
82
+
83
+ declare class Terminal extends EventEmitter {
84
+ private ws;
85
+ constructor(ws: WebSocket);
86
+ write(data: string): void;
87
+ resize(cols: number, rows: number): void;
88
+ close(): void;
89
+ }
90
+
91
+ interface SandboxApiResponse {
92
+ id: string;
93
+ status: string;
94
+ template: string;
95
+ createdAt: string;
96
+ expiresAt: string;
97
+ agentUrl: string;
98
+ ports: Record<string, number>;
99
+ }
100
+ declare class Sandbox {
101
+ private readonly client;
102
+ readonly id: string;
103
+ readonly fs: Filesystem;
104
+ private constructor();
105
+ static create(opts?: SandboxOptions): Promise<Sandbox>;
106
+ exec(cmd: string, opts?: ExecOptions): Promise<ExecResult>;
107
+ exec(cmd: string, opts: ExecOptions & {
108
+ stream: true;
109
+ }): Promise<Process>;
110
+ terminal(): Promise<Terminal>;
111
+ readonly ports: {
112
+ list: () => Promise<PortMapping[]>;
113
+ };
114
+ info(): Promise<SandboxApiResponse>;
115
+ close(): Promise<void>;
116
+ }
117
+
118
+ export { type ExecOptions, type ExecResult, type FileEntry, Filesystem, type PortMapping, Process, Sandbox, type SandboxOptions, Terminal, type WsMessage, type WsMessageType };
package/dist/index.js ADDED
@@ -0,0 +1,295 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ Filesystem: () => Filesystem,
34
+ Process: () => Process,
35
+ Sandbox: () => Sandbox,
36
+ Terminal: () => Terminal
37
+ });
38
+ module.exports = __toCommonJS(index_exports);
39
+
40
+ // src/client.ts
41
+ var import_ws = __toESM(require("ws"));
42
+ var HttpClient = class {
43
+ constructor(baseUrl, apiKey) {
44
+ this.baseUrl = baseUrl;
45
+ this.apiKey = apiKey;
46
+ }
47
+ baseUrl;
48
+ apiKey;
49
+ headers() {
50
+ return {
51
+ "content-type": "application/json",
52
+ authorization: `Bearer ${this.apiKey}`
53
+ };
54
+ }
55
+ async get(path, query) {
56
+ const qs = query ? "?" + new URLSearchParams(query).toString() : "";
57
+ const res = await fetch(`${this.baseUrl}${path}${qs}`, {
58
+ headers: this.headers(),
59
+ signal: AbortSignal.timeout(3e4)
60
+ });
61
+ if (!res.ok) {
62
+ const body = await res.text();
63
+ throw new Error(`GET ${path} failed (${res.status}): ${body}`);
64
+ }
65
+ return res.json();
66
+ }
67
+ async post(path, body, timeoutMs = 3e5) {
68
+ const res = await fetch(`${this.baseUrl}${path}`, {
69
+ method: "POST",
70
+ headers: this.headers(),
71
+ body: JSON.stringify(body ?? {}),
72
+ signal: AbortSignal.timeout(timeoutMs)
73
+ });
74
+ if (!res.ok) {
75
+ const text = await res.text();
76
+ throw new Error(`POST ${path} failed (${res.status}): ${text}`);
77
+ }
78
+ if (res.status === 204) return void 0;
79
+ return res.json();
80
+ }
81
+ async delete(path, query) {
82
+ const qs = query ? "?" + new URLSearchParams(query).toString() : "";
83
+ const res = await fetch(`${this.baseUrl}${path}${qs}`, {
84
+ method: "DELETE",
85
+ headers: this.headers(),
86
+ signal: AbortSignal.timeout(3e4)
87
+ });
88
+ if (!res.ok && res.status !== 404) {
89
+ const text = await res.text();
90
+ throw new Error(`DELETE ${path} failed (${res.status}): ${text}`);
91
+ }
92
+ }
93
+ openWebSocket(path) {
94
+ const wsUrl = this.baseUrl.replace(/^http/, "ws") + path;
95
+ return new import_ws.default(wsUrl, {
96
+ headers: { authorization: `Bearer ${this.apiKey}` }
97
+ });
98
+ }
99
+ };
100
+
101
+ // src/filesystem.ts
102
+ var Filesystem = class {
103
+ constructor(sandboxId, client) {
104
+ this.sandboxId = sandboxId;
105
+ this.client = client;
106
+ }
107
+ sandboxId;
108
+ client;
109
+ async read(filePath) {
110
+ const res = await this.client.get(
111
+ `/api/sandboxes/${this.sandboxId}/files`,
112
+ { path: filePath }
113
+ );
114
+ return res.content;
115
+ }
116
+ async write(filePath, content) {
117
+ await this.client.post(`/api/sandboxes/${this.sandboxId}/files`, {
118
+ path: filePath,
119
+ content
120
+ });
121
+ }
122
+ async list(dirPath) {
123
+ const res = await this.client.get(
124
+ `/api/sandboxes/${this.sandboxId}/files/list`,
125
+ { path: dirPath }
126
+ );
127
+ return res.files;
128
+ }
129
+ async delete(filePath) {
130
+ await this.client.delete(`/api/sandboxes/${this.sandboxId}/files`, { path: filePath });
131
+ }
132
+ };
133
+
134
+ // src/process.ts
135
+ var import_node_events = require("events");
136
+ var Process = class extends import_node_events.EventEmitter {
137
+ ws;
138
+ constructor(ws) {
139
+ super();
140
+ this.ws = ws;
141
+ ws.on("message", (raw) => {
142
+ try {
143
+ const msg = JSON.parse(raw.toString());
144
+ if (msg.type === "stdout") this.emit("stdout", msg.data ?? "");
145
+ else if (msg.type === "stderr") this.emit("stderr", msg.data ?? "");
146
+ else if (msg.type === "exit") this.emit("exit", msg.exitCode ?? 0);
147
+ else if (msg.type === "error") this.emit("error", new Error(msg.data));
148
+ } catch {
149
+ }
150
+ });
151
+ ws.on("error", (err) => this.emit("error", err));
152
+ ws.on("close", () => this.emit("close"));
153
+ }
154
+ kill() {
155
+ this.ws.close();
156
+ }
157
+ wait() {
158
+ return new Promise((resolve, reject) => {
159
+ let stdout = "";
160
+ let stderr = "";
161
+ let exitCode = 0;
162
+ this.on("stdout", (data) => {
163
+ stdout += data;
164
+ });
165
+ this.on("stderr", (data) => {
166
+ stderr += data;
167
+ });
168
+ this.on("exit", (code) => {
169
+ exitCode = code;
170
+ resolve({ stdout, stderr, exitCode });
171
+ });
172
+ this.on("error", reject);
173
+ });
174
+ }
175
+ };
176
+
177
+ // src/terminal.ts
178
+ var import_node_events2 = require("events");
179
+ var Terminal = class extends import_node_events2.EventEmitter {
180
+ ws;
181
+ constructor(ws) {
182
+ super();
183
+ this.ws = ws;
184
+ ws.on("message", (raw) => {
185
+ try {
186
+ const msg = JSON.parse(raw.toString());
187
+ if (msg.type === "stdout") this.emit("data", msg.data ?? "");
188
+ else if (msg.type === "exit") this.emit("exit", msg.exitCode ?? 0);
189
+ } catch {
190
+ }
191
+ });
192
+ ws.on("error", (err) => this.emit("error", err));
193
+ ws.on("close", () => this.emit("close"));
194
+ }
195
+ write(data) {
196
+ if (this.ws.readyState === this.ws.OPEN) {
197
+ const msg = { type: "input", data };
198
+ this.ws.send(JSON.stringify(msg));
199
+ }
200
+ }
201
+ resize(cols, rows) {
202
+ if (this.ws.readyState === this.ws.OPEN) {
203
+ const msg = { type: "resize", cols, rows };
204
+ this.ws.send(JSON.stringify(msg));
205
+ }
206
+ }
207
+ close() {
208
+ this.ws.close();
209
+ }
210
+ };
211
+
212
+ // src/sandbox.ts
213
+ var Sandbox = class _Sandbox {
214
+ constructor(client, id) {
215
+ this.client = client;
216
+ this.id = id;
217
+ this.fs = new Filesystem(id, client);
218
+ }
219
+ client;
220
+ id;
221
+ fs;
222
+ static async create(opts = {}) {
223
+ const {
224
+ serverUrl = process.env.SANDBOX_SERVER_URL ?? "http://localhost:4000",
225
+ apiKey = process.env.SANDBOX_API_KEY ?? "dev-api-key",
226
+ template,
227
+ dockerfile,
228
+ timeout
229
+ } = opts;
230
+ const client = new HttpClient(serverUrl, apiKey);
231
+ const data = await client.post("/api/sandboxes", {
232
+ template,
233
+ dockerfile,
234
+ timeout
235
+ });
236
+ return new _Sandbox(client, data.id);
237
+ }
238
+ async exec(cmd, opts = {}) {
239
+ const { args, cwd, env, timeout, stream } = opts;
240
+ if (stream) {
241
+ const ws = this.client.openWebSocket(`/api/sandboxes/${this.id}/exec/stream`);
242
+ const proc = new Process(ws);
243
+ await new Promise((resolve, reject) => {
244
+ ws.once("open", () => {
245
+ const req = {
246
+ type: "input",
247
+ cmd,
248
+ args,
249
+ cwd
250
+ };
251
+ ws.send(JSON.stringify(req));
252
+ resolve();
253
+ });
254
+ ws.once("error", reject);
255
+ });
256
+ return proc;
257
+ }
258
+ return this.client.post(`/api/sandboxes/${this.id}/exec`, {
259
+ cmd,
260
+ args,
261
+ cwd,
262
+ env,
263
+ timeout
264
+ });
265
+ }
266
+ async terminal() {
267
+ const ws = this.client.openWebSocket(`/api/sandboxes/${this.id}/terminal`);
268
+ await new Promise((resolve, reject) => {
269
+ ws.once("open", resolve);
270
+ ws.once("error", reject);
271
+ });
272
+ return new Terminal(ws);
273
+ }
274
+ ports = {
275
+ list: async () => {
276
+ const res = await this.client.get(
277
+ `/api/sandboxes/${this.id}/ports`
278
+ );
279
+ return res.ports;
280
+ }
281
+ };
282
+ async info() {
283
+ return this.client.get(`/api/sandboxes/${this.id}`);
284
+ }
285
+ async close() {
286
+ await this.client.delete(`/api/sandboxes/${this.id}`);
287
+ }
288
+ };
289
+ // Annotate the CommonJS export names for ESM import in node:
290
+ 0 && (module.exports = {
291
+ Filesystem,
292
+ Process,
293
+ Sandbox,
294
+ Terminal
295
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,255 @@
1
+ // src/client.ts
2
+ import WebSocket from "ws";
3
+ var HttpClient = class {
4
+ constructor(baseUrl, apiKey) {
5
+ this.baseUrl = baseUrl;
6
+ this.apiKey = apiKey;
7
+ }
8
+ baseUrl;
9
+ apiKey;
10
+ headers() {
11
+ return {
12
+ "content-type": "application/json",
13
+ authorization: `Bearer ${this.apiKey}`
14
+ };
15
+ }
16
+ async get(path, query) {
17
+ const qs = query ? "?" + new URLSearchParams(query).toString() : "";
18
+ const res = await fetch(`${this.baseUrl}${path}${qs}`, {
19
+ headers: this.headers(),
20
+ signal: AbortSignal.timeout(3e4)
21
+ });
22
+ if (!res.ok) {
23
+ const body = await res.text();
24
+ throw new Error(`GET ${path} failed (${res.status}): ${body}`);
25
+ }
26
+ return res.json();
27
+ }
28
+ async post(path, body, timeoutMs = 3e5) {
29
+ const res = await fetch(`${this.baseUrl}${path}`, {
30
+ method: "POST",
31
+ headers: this.headers(),
32
+ body: JSON.stringify(body ?? {}),
33
+ signal: AbortSignal.timeout(timeoutMs)
34
+ });
35
+ if (!res.ok) {
36
+ const text = await res.text();
37
+ throw new Error(`POST ${path} failed (${res.status}): ${text}`);
38
+ }
39
+ if (res.status === 204) return void 0;
40
+ return res.json();
41
+ }
42
+ async delete(path, query) {
43
+ const qs = query ? "?" + new URLSearchParams(query).toString() : "";
44
+ const res = await fetch(`${this.baseUrl}${path}${qs}`, {
45
+ method: "DELETE",
46
+ headers: this.headers(),
47
+ signal: AbortSignal.timeout(3e4)
48
+ });
49
+ if (!res.ok && res.status !== 404) {
50
+ const text = await res.text();
51
+ throw new Error(`DELETE ${path} failed (${res.status}): ${text}`);
52
+ }
53
+ }
54
+ openWebSocket(path) {
55
+ const wsUrl = this.baseUrl.replace(/^http/, "ws") + path;
56
+ return new WebSocket(wsUrl, {
57
+ headers: { authorization: `Bearer ${this.apiKey}` }
58
+ });
59
+ }
60
+ };
61
+
62
+ // src/filesystem.ts
63
+ var Filesystem = class {
64
+ constructor(sandboxId, client) {
65
+ this.sandboxId = sandboxId;
66
+ this.client = client;
67
+ }
68
+ sandboxId;
69
+ client;
70
+ async read(filePath) {
71
+ const res = await this.client.get(
72
+ `/api/sandboxes/${this.sandboxId}/files`,
73
+ { path: filePath }
74
+ );
75
+ return res.content;
76
+ }
77
+ async write(filePath, content) {
78
+ await this.client.post(`/api/sandboxes/${this.sandboxId}/files`, {
79
+ path: filePath,
80
+ content
81
+ });
82
+ }
83
+ async list(dirPath) {
84
+ const res = await this.client.get(
85
+ `/api/sandboxes/${this.sandboxId}/files/list`,
86
+ { path: dirPath }
87
+ );
88
+ return res.files;
89
+ }
90
+ async delete(filePath) {
91
+ await this.client.delete(`/api/sandboxes/${this.sandboxId}/files`, { path: filePath });
92
+ }
93
+ };
94
+
95
+ // src/process.ts
96
+ import { EventEmitter } from "events";
97
+ var Process = class extends EventEmitter {
98
+ ws;
99
+ constructor(ws) {
100
+ super();
101
+ this.ws = ws;
102
+ ws.on("message", (raw) => {
103
+ try {
104
+ const msg = JSON.parse(raw.toString());
105
+ if (msg.type === "stdout") this.emit("stdout", msg.data ?? "");
106
+ else if (msg.type === "stderr") this.emit("stderr", msg.data ?? "");
107
+ else if (msg.type === "exit") this.emit("exit", msg.exitCode ?? 0);
108
+ else if (msg.type === "error") this.emit("error", new Error(msg.data));
109
+ } catch {
110
+ }
111
+ });
112
+ ws.on("error", (err) => this.emit("error", err));
113
+ ws.on("close", () => this.emit("close"));
114
+ }
115
+ kill() {
116
+ this.ws.close();
117
+ }
118
+ wait() {
119
+ return new Promise((resolve, reject) => {
120
+ let stdout = "";
121
+ let stderr = "";
122
+ let exitCode = 0;
123
+ this.on("stdout", (data) => {
124
+ stdout += data;
125
+ });
126
+ this.on("stderr", (data) => {
127
+ stderr += data;
128
+ });
129
+ this.on("exit", (code) => {
130
+ exitCode = code;
131
+ resolve({ stdout, stderr, exitCode });
132
+ });
133
+ this.on("error", reject);
134
+ });
135
+ }
136
+ };
137
+
138
+ // src/terminal.ts
139
+ import { EventEmitter as EventEmitter2 } from "events";
140
+ var Terminal = class extends EventEmitter2 {
141
+ ws;
142
+ constructor(ws) {
143
+ super();
144
+ this.ws = ws;
145
+ ws.on("message", (raw) => {
146
+ try {
147
+ const msg = JSON.parse(raw.toString());
148
+ if (msg.type === "stdout") this.emit("data", msg.data ?? "");
149
+ else if (msg.type === "exit") this.emit("exit", msg.exitCode ?? 0);
150
+ } catch {
151
+ }
152
+ });
153
+ ws.on("error", (err) => this.emit("error", err));
154
+ ws.on("close", () => this.emit("close"));
155
+ }
156
+ write(data) {
157
+ if (this.ws.readyState === this.ws.OPEN) {
158
+ const msg = { type: "input", data };
159
+ this.ws.send(JSON.stringify(msg));
160
+ }
161
+ }
162
+ resize(cols, rows) {
163
+ if (this.ws.readyState === this.ws.OPEN) {
164
+ const msg = { type: "resize", cols, rows };
165
+ this.ws.send(JSON.stringify(msg));
166
+ }
167
+ }
168
+ close() {
169
+ this.ws.close();
170
+ }
171
+ };
172
+
173
+ // src/sandbox.ts
174
+ var Sandbox = class _Sandbox {
175
+ constructor(client, id) {
176
+ this.client = client;
177
+ this.id = id;
178
+ this.fs = new Filesystem(id, client);
179
+ }
180
+ client;
181
+ id;
182
+ fs;
183
+ static async create(opts = {}) {
184
+ const {
185
+ serverUrl = process.env.SANDBOX_SERVER_URL ?? "http://localhost:4000",
186
+ apiKey = process.env.SANDBOX_API_KEY ?? "dev-api-key",
187
+ template,
188
+ dockerfile,
189
+ timeout
190
+ } = opts;
191
+ const client = new HttpClient(serverUrl, apiKey);
192
+ const data = await client.post("/api/sandboxes", {
193
+ template,
194
+ dockerfile,
195
+ timeout
196
+ });
197
+ return new _Sandbox(client, data.id);
198
+ }
199
+ async exec(cmd, opts = {}) {
200
+ const { args, cwd, env, timeout, stream } = opts;
201
+ if (stream) {
202
+ const ws = this.client.openWebSocket(`/api/sandboxes/${this.id}/exec/stream`);
203
+ const proc = new Process(ws);
204
+ await new Promise((resolve, reject) => {
205
+ ws.once("open", () => {
206
+ const req = {
207
+ type: "input",
208
+ cmd,
209
+ args,
210
+ cwd
211
+ };
212
+ ws.send(JSON.stringify(req));
213
+ resolve();
214
+ });
215
+ ws.once("error", reject);
216
+ });
217
+ return proc;
218
+ }
219
+ return this.client.post(`/api/sandboxes/${this.id}/exec`, {
220
+ cmd,
221
+ args,
222
+ cwd,
223
+ env,
224
+ timeout
225
+ });
226
+ }
227
+ async terminal() {
228
+ const ws = this.client.openWebSocket(`/api/sandboxes/${this.id}/terminal`);
229
+ await new Promise((resolve, reject) => {
230
+ ws.once("open", resolve);
231
+ ws.once("error", reject);
232
+ });
233
+ return new Terminal(ws);
234
+ }
235
+ ports = {
236
+ list: async () => {
237
+ const res = await this.client.get(
238
+ `/api/sandboxes/${this.id}/ports`
239
+ );
240
+ return res.ports;
241
+ }
242
+ };
243
+ async info() {
244
+ return this.client.get(`/api/sandboxes/${this.id}`);
245
+ }
246
+ async close() {
247
+ await this.client.delete(`/api/sandboxes/${this.id}`);
248
+ }
249
+ };
250
+ export {
251
+ Filesystem,
252
+ Process,
253
+ Sandbox,
254
+ Terminal
255
+ };
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@sandbox-engine/sdk",
3
+ "version": "0.1.0",
4
+ "description": "SDK for creating and managing isolated sandbox environments",
5
+ "keywords": ["sandbox", "docker", "code-execution", "isolated", "e2b"],
6
+ "license": "MIT",
7
+ "main": "dist/index.js",
8
+ "module": "dist/index.mjs",
9
+ "types": "dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "import": {
13
+ "types": "./dist/index.d.mts",
14
+ "default": "./dist/index.mjs"
15
+ },
16
+ "require": {
17
+ "types": "./dist/index.d.ts",
18
+ "default": "./dist/index.js"
19
+ }
20
+ }
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "README.md"
25
+ ],
26
+ "scripts": {
27
+ "build": "tsup",
28
+ "dev": "tsup --watch",
29
+ "test": "vitest run",
30
+ "prepublishOnly": "pnpm build"
31
+ },
32
+ "dependencies": {
33
+ "ws": "^8.17.0"
34
+ },
35
+ "devDependencies": {
36
+ "typescript": "^5.4.5",
37
+ "tsup": "^8.0.2",
38
+ "vitest": "^1.5.0",
39
+ "@types/node": "^20.12.7",
40
+ "@types/ws": "^8.5.10"
41
+ },
42
+ "engines": {
43
+ "node": ">=18.0.0"
44
+ }
45
+ }