@treesap/sandbox 0.2.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 (66) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/README.md +495 -0
  3. package/dist/api-server.d.ts +41 -0
  4. package/dist/api-server.d.ts.map +1 -0
  5. package/dist/api-server.js +536 -0
  6. package/dist/api-server.js.map +1 -0
  7. package/dist/auth-middleware.d.ts +31 -0
  8. package/dist/auth-middleware.d.ts.map +1 -0
  9. package/dist/auth-middleware.js +35 -0
  10. package/dist/auth-middleware.js.map +1 -0
  11. package/dist/cli.d.ts +3 -0
  12. package/dist/cli.d.ts.map +1 -0
  13. package/dist/cli.js +65 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/client.d.ts +137 -0
  16. package/dist/client.d.ts.map +1 -0
  17. package/dist/client.js +412 -0
  18. package/dist/client.js.map +1 -0
  19. package/dist/file-service.d.ts +94 -0
  20. package/dist/file-service.d.ts.map +1 -0
  21. package/dist/file-service.js +203 -0
  22. package/dist/file-service.js.map +1 -0
  23. package/dist/http-exposure-service.d.ts +71 -0
  24. package/dist/http-exposure-service.d.ts.map +1 -0
  25. package/dist/http-exposure-service.js +172 -0
  26. package/dist/http-exposure-service.js.map +1 -0
  27. package/dist/index.d.ts +59 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +66 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/sandbox-manager.d.ts +76 -0
  32. package/dist/sandbox-manager.d.ts.map +1 -0
  33. package/dist/sandbox-manager.js +161 -0
  34. package/dist/sandbox-manager.js.map +1 -0
  35. package/dist/sandbox.d.ts +118 -0
  36. package/dist/sandbox.d.ts.map +1 -0
  37. package/dist/sandbox.js +303 -0
  38. package/dist/sandbox.js.map +1 -0
  39. package/dist/server.d.ts +7 -0
  40. package/dist/server.d.ts.map +1 -0
  41. package/dist/server.js +240 -0
  42. package/dist/server.js.map +1 -0
  43. package/dist/stream-service.d.ts +35 -0
  44. package/dist/stream-service.d.ts.map +1 -0
  45. package/dist/stream-service.js +136 -0
  46. package/dist/stream-service.js.map +1 -0
  47. package/dist/terminal.d.ts +46 -0
  48. package/dist/terminal.d.ts.map +1 -0
  49. package/dist/terminal.js +264 -0
  50. package/dist/terminal.js.map +1 -0
  51. package/dist/websocket.d.ts +48 -0
  52. package/dist/websocket.d.ts.map +1 -0
  53. package/dist/websocket.js +332 -0
  54. package/dist/websocket.js.map +1 -0
  55. package/package.json +59 -0
  56. package/src/api-server.ts +658 -0
  57. package/src/auth-middleware.ts +65 -0
  58. package/src/cli.ts +71 -0
  59. package/src/client.ts +537 -0
  60. package/src/file-service.ts +273 -0
  61. package/src/http-exposure-service.ts +232 -0
  62. package/src/index.ts +101 -0
  63. package/src/sandbox-manager.ts +202 -0
  64. package/src/sandbox.ts +396 -0
  65. package/src/stream-service.ts +174 -0
  66. package/tsconfig.json +37 -0
package/src/cli.ts ADDED
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { startServer } from './api-server';
4
+
5
+ /**
6
+ * CLI entry point for TreeSap Sandbox
7
+ */
8
+
9
+ // Parse command line arguments
10
+ const args = process.argv.slice(2);
11
+ const config: any = {};
12
+
13
+ for (let i = 0; i < args.length; i++) {
14
+ const arg = args[i];
15
+
16
+ switch (arg) {
17
+ case '--port':
18
+ case '-p':
19
+ config.port = parseInt(args[++i]);
20
+ break;
21
+ case '--host':
22
+ case '-h':
23
+ config.host = args[++i];
24
+ break;
25
+ case '--base-path':
26
+ case '-b':
27
+ config.basePath = args[++i];
28
+ break;
29
+ case '--max-sandboxes':
30
+ case '-m':
31
+ config.maxSandboxes = parseInt(args[++i]);
32
+ break;
33
+ case '--cors':
34
+ config.cors = true;
35
+ break;
36
+ case '--help':
37
+ printHelp();
38
+ process.exit(0);
39
+ default:
40
+ console.error(`Unknown argument: ${arg}`);
41
+ printHelp();
42
+ process.exit(1);
43
+ }
44
+ }
45
+
46
+ function printHelp() {
47
+ console.log(`
48
+ TreeSap Sandbox Server
49
+
50
+ Usage: treesap-sandbox [options]
51
+
52
+ Options:
53
+ -p, --port <port> Port to listen on (default: 3000)
54
+ -h, --host <host> Host to bind to (default: 0.0.0.0)
55
+ -b, --base-path <path> Base path for sandbox folders (default: ./.sandboxes)
56
+ -m, --max-sandboxes <num> Maximum number of sandboxes (default: 100)
57
+ --cors Enable CORS (default: false)
58
+ --help Show this help message
59
+
60
+ Examples:
61
+ treesap-sandbox --port 8080
62
+ treesap-sandbox --base-path /tmp/sandboxes --cors
63
+ treesap-sandbox -p 3000 -m 50
64
+ `);
65
+ }
66
+
67
+ // Start the server
68
+ startServer(config).catch((error) => {
69
+ console.error('Failed to start server:', error);
70
+ process.exit(1);
71
+ });
package/src/client.ts ADDED
@@ -0,0 +1,537 @@
1
+ import { ExecuteResponse, ProcessInfo, ExecOptions } from './sandbox';
2
+ import { FileInfo, ListFilesOptions } from './file-service';
3
+ import { ExecEvent, LogEvent } from './stream-service';
4
+
5
+ export interface SandboxClientConfig {
6
+ baseUrl: string;
7
+ sandboxId?: string;
8
+ apiKey?: string;
9
+ }
10
+
11
+ export interface CreateSandboxOptions {
12
+ apiKey?: string;
13
+ env?: Record<string, string>;
14
+ timeout?: number;
15
+ }
16
+
17
+ export interface CreateSandboxResponse {
18
+ id: string;
19
+ workDir: string;
20
+ createdAt: number;
21
+ }
22
+
23
+ /**
24
+ * Client library for interacting with TreeSap Sandbox API
25
+ */
26
+ export class SandboxClient {
27
+ private baseUrl: string;
28
+ private apiKey?: string;
29
+ public readonly id: string;
30
+
31
+ constructor(config: SandboxClientConfig) {
32
+ this.baseUrl = config.baseUrl.replace(/\/$/, ''); // Remove trailing slash
33
+ this.id = config.sandboxId || '';
34
+ this.apiKey = config.apiKey;
35
+ }
36
+
37
+ /**
38
+ * Get headers for API requests (includes API key if configured)
39
+ */
40
+ private getHeaders(contentType?: string): Record<string, string> {
41
+ const headers: Record<string, string> = {};
42
+ if (contentType) {
43
+ headers['Content-Type'] = contentType;
44
+ }
45
+ if (this.apiKey) {
46
+ headers['X-API-Key'] = this.apiKey;
47
+ }
48
+ return headers;
49
+ }
50
+
51
+ /**
52
+ * Create a new sandbox instance
53
+ */
54
+ static async create(baseUrl: string, options: CreateSandboxOptions = {}): Promise<SandboxClient> {
55
+ const url = `${baseUrl.replace(/\/$/, '')}/sandbox`;
56
+
57
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' };
58
+ if (options.apiKey) {
59
+ headers['X-API-Key'] = options.apiKey;
60
+ }
61
+
62
+ const response = await fetch(url, {
63
+ method: 'POST',
64
+ headers,
65
+ body: JSON.stringify({ env: options.env, timeout: options.timeout }),
66
+ });
67
+
68
+ if (!response.ok) {
69
+ const error = await response.json() as any;
70
+ throw new Error(error.error || 'Failed to create sandbox');
71
+ }
72
+
73
+ const data = await response.json() as CreateSandboxResponse;
74
+
75
+ return new SandboxClient({
76
+ baseUrl,
77
+ sandboxId: data.id,
78
+ apiKey: options.apiKey,
79
+ });
80
+ }
81
+
82
+ /**
83
+ * Get existing sandbox by ID
84
+ */
85
+ static fromId(baseUrl: string, sandboxId: string, apiKey?: string): SandboxClient {
86
+ return new SandboxClient({ baseUrl, sandboxId, apiKey });
87
+ }
88
+
89
+ // ============================================================================
90
+ // Command Execution
91
+ // ============================================================================
92
+
93
+ /**
94
+ * Execute a command and return the complete result
95
+ */
96
+ async exec(command: string, options: ExecOptions = {}): Promise<ExecuteResponse> {
97
+ const url = `${this.baseUrl}/sandbox/${this.id}/exec`;
98
+
99
+ const response = await fetch(url, {
100
+ method: 'POST',
101
+ headers: this.getHeaders('application/json'),
102
+ body: JSON.stringify({ command, ...options }),
103
+ });
104
+
105
+ if (!response.ok) {
106
+ const error = await response.json() as any;
107
+ throw new Error(error.error || 'Failed to execute command');
108
+ }
109
+
110
+ return await response.json() as ExecuteResponse;
111
+ }
112
+
113
+ /**
114
+ * Execute a command and return a streaming response
115
+ */
116
+ async execStream(command: string, options: ExecOptions = {}): Promise<ReadableStream> {
117
+ const params = new URLSearchParams({ command });
118
+ if (options.timeout) {
119
+ params.append('timeout', options.timeout.toString());
120
+ }
121
+
122
+ const url = `${this.baseUrl}/sandbox/${this.id}/exec-stream?${params}`;
123
+
124
+ const response = await fetch(url, { headers: this.getHeaders() });
125
+
126
+ if (!response.ok) {
127
+ const error = await response.json() as any;
128
+ throw new Error(error.error || 'Failed to execute command');
129
+ }
130
+
131
+ if (!response.body) {
132
+ throw new Error('No response body');
133
+ }
134
+
135
+ return response.body;
136
+ }
137
+
138
+ // ============================================================================
139
+ // Process Management
140
+ // ============================================================================
141
+
142
+ /**
143
+ * Start a long-running background process
144
+ */
145
+ async startProcess(command: string, options: ExecOptions = {}): Promise<ProcessInfo> {
146
+ const url = `${this.baseUrl}/sandbox/${this.id}/process`;
147
+
148
+ const response = await fetch(url, {
149
+ method: 'POST',
150
+ headers: this.getHeaders('application/json'),
151
+ body: JSON.stringify({ command, ...options }),
152
+ });
153
+
154
+ if (!response.ok) {
155
+ const error = await response.json() as any;
156
+ throw new Error(error.error || 'Failed to start process');
157
+ }
158
+
159
+ return await response.json() as ProcessInfo;
160
+ }
161
+
162
+ /**
163
+ * List all processes
164
+ */
165
+ async listProcesses(): Promise<ProcessInfo[]> {
166
+ const url = `${this.baseUrl}/sandbox/${this.id}/process`;
167
+
168
+ const response = await fetch(url, { headers: this.getHeaders() });
169
+
170
+ if (!response.ok) {
171
+ const error = await response.json() as any;
172
+ throw new Error(error.error || 'Failed to list processes');
173
+ }
174
+
175
+ const data = await response.json() as any;
176
+ return data.processes as ProcessInfo[];
177
+ }
178
+
179
+ /**
180
+ * Get process information
181
+ */
182
+ async getProcess(processId: string): Promise<ProcessInfo> {
183
+ const url = `${this.baseUrl}/sandbox/${this.id}/process/${processId}`;
184
+
185
+ const response = await fetch(url, { headers: this.getHeaders() });
186
+
187
+ if (!response.ok) {
188
+ const error = await response.json() as any;
189
+ throw new Error(error.error || 'Failed to get process');
190
+ }
191
+
192
+ return await response.json() as ProcessInfo;
193
+ }
194
+
195
+ /**
196
+ * Kill a process
197
+ */
198
+ async killProcess(processId: string, signal: string = 'SIGTERM'): Promise<void> {
199
+ const url = `${this.baseUrl}/sandbox/${this.id}/process/${processId}?signal=${signal}`;
200
+
201
+ const response = await fetch(url, { method: 'DELETE', headers: this.getHeaders() });
202
+
203
+ if (!response.ok) {
204
+ const error = await response.json() as any;
205
+ throw new Error(error.error || 'Failed to kill process');
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Kill all processes
211
+ */
212
+ async killAllProcesses(): Promise<void> {
213
+ const processes = await this.listProcesses();
214
+ const runningProcesses = processes.filter((p) => p.status === 'running');
215
+
216
+ await Promise.all(
217
+ runningProcesses.map((p) => this.killProcess(p.id).catch(() => {}))
218
+ );
219
+ }
220
+
221
+ /**
222
+ * Stream process logs
223
+ */
224
+ async streamProcessLogs(processId: string): Promise<ReadableStream> {
225
+ const url = `${this.baseUrl}/sandbox/${this.id}/process/${processId}/logs`;
226
+
227
+ const response = await fetch(url, { headers: this.getHeaders() });
228
+
229
+ if (!response.ok) {
230
+ const error = await response.json() as any;
231
+ throw new Error(error.error || 'Failed to stream logs');
232
+ }
233
+
234
+ if (!response.body) {
235
+ throw new Error('No response body');
236
+ }
237
+
238
+ return response.body;
239
+ }
240
+
241
+ /**
242
+ * Get accumulated process logs (utility method)
243
+ */
244
+ async getProcessLogs(processId: string): Promise<string> {
245
+ const stream = await this.streamProcessLogs(processId);
246
+ const logs: string[] = [];
247
+
248
+ for await (const event of parseSSEStream<LogEvent>(stream)) {
249
+ if (event.data) {
250
+ logs.push(event.data);
251
+ }
252
+ }
253
+
254
+ return logs.join('');
255
+ }
256
+
257
+ // ============================================================================
258
+ // File Operations
259
+ // ============================================================================
260
+
261
+ /**
262
+ * List files in a directory
263
+ */
264
+ async listFiles(path: string = '.', options: ListFilesOptions = {}): Promise<FileInfo[]> {
265
+ const params = new URLSearchParams({ path });
266
+
267
+ if (options.recursive) {
268
+ params.append('recursive', 'true');
269
+ }
270
+ if (options.pattern) {
271
+ params.append('pattern', options.pattern);
272
+ }
273
+ if (options.includeHidden) {
274
+ params.append('hidden', 'true');
275
+ }
276
+
277
+ const url = `${this.baseUrl}/sandbox/${this.id}/files?${params}`;
278
+
279
+ const response = await fetch(url, { headers: this.getHeaders() });
280
+
281
+ if (!response.ok) {
282
+ const error = await response.json() as any;
283
+ throw new Error(error.error || 'Failed to list files');
284
+ }
285
+
286
+ const data = await response.json() as any;
287
+ return data.files as FileInfo[];
288
+ }
289
+
290
+ /**
291
+ * Read a file's contents
292
+ */
293
+ async readFile(path: string): Promise<string> {
294
+ const url = `${this.baseUrl}/sandbox/${this.id}/files/${path}`;
295
+
296
+ const response = await fetch(url, { headers: this.getHeaders() });
297
+
298
+ if (!response.ok) {
299
+ const error = await response.json() as any;
300
+ throw new Error(error.error || 'Failed to read file');
301
+ }
302
+
303
+ const data = await response.json() as any;
304
+ return data.content as string;
305
+ }
306
+
307
+ /**
308
+ * Write content to a file
309
+ */
310
+ async writeFile(path: string, content: string): Promise<void> {
311
+ const url = `${this.baseUrl}/sandbox/${this.id}/files/${path}`;
312
+
313
+ const response = await fetch(url, {
314
+ method: 'POST',
315
+ headers: this.getHeaders('application/json'),
316
+ body: JSON.stringify({ content }),
317
+ });
318
+
319
+ if (!response.ok) {
320
+ const error = await response.json() as any;
321
+ throw new Error(error.error || 'Failed to write file');
322
+ }
323
+ }
324
+
325
+ /**
326
+ * Delete a file or directory
327
+ */
328
+ async deleteFile(path: string, options: { recursive?: boolean } = {}): Promise<void> {
329
+ const params = new URLSearchParams();
330
+ if (options.recursive) {
331
+ params.append('recursive', 'true');
332
+ }
333
+
334
+ const url = `${this.baseUrl}/sandbox/${this.id}/files/${path}?${params}`;
335
+
336
+ const response = await fetch(url, { method: 'DELETE', headers: this.getHeaders() });
337
+
338
+ if (!response.ok) {
339
+ const error = await response.json() as any;
340
+ throw new Error(error.error || 'Failed to delete file');
341
+ }
342
+ }
343
+
344
+ // ============================================================================
345
+ // Environment Variable Management
346
+ // ============================================================================
347
+
348
+ /**
349
+ * Set environment variables in the sandbox
350
+ */
351
+ async setEnv(variables: Record<string, string>): Promise<void> {
352
+ const url = `${this.baseUrl}/sandbox/${this.id}/env`;
353
+
354
+ const response = await fetch(url, {
355
+ method: 'POST',
356
+ headers: this.getHeaders('application/json'),
357
+ body: JSON.stringify({ variables }),
358
+ });
359
+
360
+ if (!response.ok) {
361
+ const error = await response.json() as any;
362
+ throw new Error(error.error || 'Failed to set environment variables');
363
+ }
364
+ }
365
+
366
+ /**
367
+ * Get list of environment variable names (not values for security)
368
+ */
369
+ async listEnv(): Promise<string[]> {
370
+ const url = `${this.baseUrl}/sandbox/${this.id}/env`;
371
+
372
+ const response = await fetch(url, { headers: this.getHeaders() });
373
+
374
+ if (!response.ok) {
375
+ const error = await response.json() as any;
376
+ throw new Error(error.error || 'Failed to list environment variables');
377
+ }
378
+
379
+ const data = await response.json() as any;
380
+ return data.variables as string[];
381
+ }
382
+
383
+ /**
384
+ * Unset (remove) an environment variable
385
+ */
386
+ async unsetEnv(key: string): Promise<void> {
387
+ const url = `${this.baseUrl}/sandbox/${this.id}/env/${encodeURIComponent(key)}`;
388
+
389
+ const response = await fetch(url, { method: 'DELETE', headers: this.getHeaders() });
390
+
391
+ if (!response.ok) {
392
+ const error = await response.json() as any;
393
+ throw new Error(error.error || 'Failed to unset environment variable');
394
+ }
395
+ }
396
+
397
+ // ============================================================================
398
+ // HTTP Exposure
399
+ // ============================================================================
400
+
401
+ /**
402
+ * Expose a sandbox port via HTTP and return the public URL
403
+ */
404
+ async exposeHttp(port: number): Promise<string> {
405
+ const url = `${this.baseUrl}/sandbox/${this.id}/expose`;
406
+
407
+ const response = await fetch(url, {
408
+ method: 'POST',
409
+ headers: this.getHeaders('application/json'),
410
+ body: JSON.stringify({ port }),
411
+ });
412
+
413
+ if (!response.ok) {
414
+ const error = await response.json() as any;
415
+ throw new Error(error.error || 'Failed to expose HTTP port');
416
+ }
417
+
418
+ const data = await response.json() as any;
419
+ return data.url as string;
420
+ }
421
+
422
+ /**
423
+ * Get list of exposed HTTP endpoints
424
+ */
425
+ async listExposures(): Promise<Array<{ sandboxId: string; port: number; publicUrl: string }>> {
426
+ const url = `${this.baseUrl}/sandbox/${this.id}/expose`;
427
+
428
+ const response = await fetch(url, { headers: this.getHeaders() });
429
+
430
+ if (!response.ok) {
431
+ const error = await response.json() as any;
432
+ throw new Error(error.error || 'Failed to list exposures');
433
+ }
434
+
435
+ const data = await response.json() as any;
436
+ return data.exposures || [];
437
+ }
438
+
439
+ /**
440
+ * Remove HTTP exposure for a specific port (or all if port is undefined)
441
+ */
442
+ async unexposeHttp(port?: number): Promise<void> {
443
+ const params = port ? `?port=${port}` : '';
444
+ const url = `${this.baseUrl}/sandbox/${this.id}/expose${params}`;
445
+
446
+ const response = await fetch(url, { method: 'DELETE', headers: this.getHeaders() });
447
+
448
+ if (!response.ok) {
449
+ const error = await response.json() as any;
450
+ throw new Error(error.error || 'Failed to unexpose HTTP');
451
+ }
452
+ }
453
+
454
+ // ============================================================================
455
+ // Sandbox Management
456
+ // ============================================================================
457
+
458
+ /**
459
+ * Get sandbox status
460
+ */
461
+ async getStatus() {
462
+ const url = `${this.baseUrl}/sandbox/${this.id}`;
463
+
464
+ const response = await fetch(url, { headers: this.getHeaders() });
465
+
466
+ if (!response.ok) {
467
+ const error = await response.json() as any;
468
+ throw new Error(error.error || 'Failed to get status');
469
+ }
470
+
471
+ return await response.json() as any;
472
+ }
473
+
474
+ /**
475
+ * Destroy the sandbox
476
+ */
477
+ async destroy(options: { cleanup?: boolean } = {}): Promise<void> {
478
+ const params = new URLSearchParams();
479
+ if (options.cleanup) {
480
+ params.append('cleanup', 'true');
481
+ }
482
+
483
+ const url = `${this.baseUrl}/sandbox/${this.id}?${params}`;
484
+
485
+ const response = await fetch(url, { method: 'DELETE', headers: this.getHeaders() });
486
+
487
+ if (!response.ok) {
488
+ const error = await response.json() as any;
489
+ throw new Error(error.error || 'Failed to destroy sandbox');
490
+ }
491
+ }
492
+ }
493
+
494
+ /**
495
+ * Parse Server-Sent Events stream
496
+ * Utility function for consuming SSE streams from exec and logs
497
+ */
498
+ export async function* parseSSEStream<T = ExecEvent | LogEvent>(
499
+ stream: ReadableStream
500
+ ): AsyncGenerator<T> {
501
+ const reader = stream.getReader();
502
+ const decoder = new TextDecoder();
503
+ let buffer = '';
504
+
505
+ try {
506
+ while (true) {
507
+ const { done, value } = await reader.read();
508
+
509
+ if (done) {
510
+ break;
511
+ }
512
+
513
+ buffer += decoder.decode(value, { stream: true });
514
+
515
+ // Split by double newline (SSE event delimiter)
516
+ const events = buffer.split('\n\n');
517
+ buffer = events.pop() || '';
518
+
519
+ for (const event of events) {
520
+ if (event.trim()) {
521
+ // Parse SSE format (data: {json})
522
+ const match = event.match(/^data: (.+)$/m);
523
+ if (match) {
524
+ try {
525
+ const data = JSON.parse(match[1]);
526
+ yield data as T;
527
+ } catch {
528
+ // Ignore parse errors
529
+ }
530
+ }
531
+ }
532
+ }
533
+ }
534
+ } finally {
535
+ reader.releaseLock();
536
+ }
537
+ }