@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
@@ -0,0 +1,658 @@
1
+ import { Hono } from 'hono';
2
+ import { serve } from '@hono/node-server';
3
+ import { SandboxManager } from './sandbox-manager';
4
+ import { FileService } from './file-service';
5
+ import { StreamService } from './stream-service';
6
+ import { stream } from 'hono/streaming';
7
+ import { Readable } from 'stream';
8
+ import { createAuthMiddleware, parseApiKeysFromEnv } from './auth-middleware';
9
+ import { HttpExposureService } from './http-exposure-service';
10
+
11
+ export interface ServerConfig {
12
+ port?: number;
13
+ host?: string;
14
+ basePath?: string;
15
+ maxSandboxes?: number;
16
+ cors?: boolean;
17
+ /**
18
+ * API keys for authentication. If empty or not provided, authentication is disabled.
19
+ * Can also be set via SANDBOX_API_KEYS environment variable (comma-separated).
20
+ */
21
+ apiKeys?: string[];
22
+ /**
23
+ * HTTP exposure configuration for Caddy integration
24
+ */
25
+ httpExposure?: {
26
+ caddyAdminUrl?: string;
27
+ baseDomain?: string;
28
+ protocol?: 'http' | 'https';
29
+ upstreamHost?: string;
30
+ };
31
+ }
32
+
33
+ /**
34
+ * Create and configure the API server
35
+ */
36
+ export function createServer(config: ServerConfig = {}) {
37
+ const app = new Hono();
38
+ const manager = new SandboxManager({
39
+ basePath: config.basePath,
40
+ maxSandboxes: config.maxSandboxes,
41
+ });
42
+ const httpExposure = new HttpExposureService(config.httpExposure || {});
43
+
44
+ // CORS middleware if enabled
45
+ if (config.cors) {
46
+ app.use('*', async (c, next) => {
47
+ await next();
48
+ c.header('Access-Control-Allow-Origin', '*');
49
+ c.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
50
+ c.header('Access-Control-Allow-Headers', 'Content-Type, X-API-Key');
51
+ });
52
+ }
53
+
54
+ // API key authentication middleware
55
+ const apiKeys = config.apiKeys || parseApiKeysFromEnv();
56
+ if (apiKeys.length > 0) {
57
+ app.use('*', createAuthMiddleware({
58
+ apiKeys,
59
+ excludePaths: ['/'], // Health check doesn't require auth
60
+ }));
61
+ }
62
+
63
+ // Health check
64
+ app.get('/', (c) => {
65
+ return c.json({
66
+ status: 'ok',
67
+ service: 'TreeSap Sandbox',
68
+ version: '1.0.0',
69
+ stats: manager.getStats(),
70
+ });
71
+ });
72
+
73
+ // ============================================================================
74
+ // Sandbox Management Endpoints
75
+ // ============================================================================
76
+
77
+ /**
78
+ * Create a new sandbox
79
+ * POST /sandbox
80
+ */
81
+ app.post('/sandbox', async (c) => {
82
+ try {
83
+ const body = await c.req.json().catch(() => ({}));
84
+ const sandbox = await manager.createSandbox(body);
85
+
86
+ return c.json({
87
+ id: sandbox.id,
88
+ workDir: sandbox.workDir,
89
+ createdAt: sandbox.createdAt,
90
+ }, 201);
91
+ } catch (error: any) {
92
+ return c.json({ error: error.message }, 400);
93
+ }
94
+ });
95
+
96
+ /**
97
+ * List all sandboxes
98
+ * GET /sandbox
99
+ */
100
+ app.get('/sandbox', (c) => {
101
+ const sandboxes = manager.listSandboxes();
102
+ return c.json({ sandboxes });
103
+ });
104
+
105
+ /**
106
+ * Get sandbox info
107
+ * GET /sandbox/:id
108
+ */
109
+ app.get('/sandbox/:id', (c) => {
110
+ const id = c.req.param('id');
111
+ const sandbox = manager.getSandbox(id);
112
+
113
+ if (!sandbox) {
114
+ return c.json({ error: 'Sandbox not found' }, 404);
115
+ }
116
+
117
+ return c.json(sandbox.getStatus());
118
+ });
119
+
120
+ /**
121
+ * Destroy a sandbox
122
+ * DELETE /sandbox/:id
123
+ */
124
+ app.delete('/sandbox/:id', async (c) => {
125
+ try {
126
+ const id = c.req.param('id');
127
+ const cleanup = c.req.query('cleanup') === 'true';
128
+
129
+ await manager.destroySandbox(id, { cleanup });
130
+ return c.json({ success: true });
131
+ } catch (error: any) {
132
+ return c.json({ error: error.message }, 404);
133
+ }
134
+ });
135
+
136
+ // ============================================================================
137
+ // Command Execution Endpoints
138
+ // ============================================================================
139
+
140
+ /**
141
+ * Execute a command
142
+ * POST /sandbox/:id/exec
143
+ */
144
+ app.post('/sandbox/:id/exec', async (c) => {
145
+ try {
146
+ const id = c.req.param('id');
147
+ const sandbox = manager.getSandbox(id);
148
+
149
+ if (!sandbox) {
150
+ return c.json({ error: 'Sandbox not found' }, 404);
151
+ }
152
+
153
+ const body = await c.req.json();
154
+ const { command, timeout, cwd, env } = body;
155
+
156
+ if (!command) {
157
+ return c.json({ error: 'Command is required' }, 400);
158
+ }
159
+
160
+ const result = await sandbox.exec(command, { timeout, cwd, env });
161
+ return c.json(result);
162
+ } catch (error: any) {
163
+ return c.json({ error: error.message }, 500);
164
+ }
165
+ });
166
+
167
+ /**
168
+ * Execute a command with streaming output
169
+ * GET /sandbox/:id/exec-stream?command=...
170
+ */
171
+ app.get('/sandbox/:id/exec-stream', (c) => {
172
+ const id = c.req.param('id');
173
+ const sandbox = manager.getSandbox(id);
174
+
175
+ if (!sandbox) {
176
+ return c.json({ error: 'Sandbox not found' }, 404);
177
+ }
178
+
179
+ const command = c.req.query('command');
180
+ if (!command) {
181
+ return c.json({ error: 'Command is required' }, 400);
182
+ }
183
+
184
+ const timeout = c.req.query('timeout');
185
+ const options = timeout ? { timeout: parseInt(timeout) } : {};
186
+
187
+ return stream(c, async (stream) => {
188
+ const execStream = StreamService.createExecStream(sandbox, command, options);
189
+
190
+ // Set SSE headers
191
+ c.header('Content-Type', 'text/event-stream');
192
+ c.header('Cache-Control', 'no-cache');
193
+ c.header('Connection', 'keep-alive');
194
+
195
+ // Pipe the readable stream to the response
196
+ for await (const chunk of execStream) {
197
+ await stream.write(chunk);
198
+ }
199
+ });
200
+ });
201
+
202
+ // ============================================================================
203
+ // Process Management Endpoints
204
+ // ============================================================================
205
+
206
+ /**
207
+ * Start a background process
208
+ * POST /sandbox/:id/process
209
+ */
210
+ app.post('/sandbox/:id/process', async (c) => {
211
+ try {
212
+ const id = c.req.param('id');
213
+ const sandbox = manager.getSandbox(id);
214
+
215
+ if (!sandbox) {
216
+ return c.json({ error: 'Sandbox not found' }, 404);
217
+ }
218
+
219
+ const body = await c.req.json();
220
+ const { command, cwd, env } = body;
221
+
222
+ if (!command) {
223
+ return c.json({ error: 'Command is required' }, 400);
224
+ }
225
+
226
+ const processInfo = await sandbox.startProcess(command, { cwd, env });
227
+ return c.json(processInfo, 201);
228
+ } catch (error: any) {
229
+ return c.json({ error: error.message }, 500);
230
+ }
231
+ });
232
+
233
+ /**
234
+ * List all processes in a sandbox
235
+ * GET /sandbox/:id/process
236
+ */
237
+ app.get('/sandbox/:id/process', (c) => {
238
+ const id = c.req.param('id');
239
+ const sandbox = manager.getSandbox(id);
240
+
241
+ if (!sandbox) {
242
+ return c.json({ error: 'Sandbox not found' }, 404);
243
+ }
244
+
245
+ const processes = sandbox.listProcesses();
246
+ return c.json({ processes });
247
+ });
248
+
249
+ /**
250
+ * Get process info
251
+ * GET /sandbox/:id/process/:processId
252
+ */
253
+ app.get('/sandbox/:id/process/:processId', (c) => {
254
+ const id = c.req.param('id');
255
+ const processId = c.req.param('processId');
256
+ const sandbox = manager.getSandbox(id);
257
+
258
+ if (!sandbox) {
259
+ return c.json({ error: 'Sandbox not found' }, 404);
260
+ }
261
+
262
+ const process = sandbox.getProcess(processId);
263
+ if (!process) {
264
+ return c.json({ error: 'Process not found' }, 404);
265
+ }
266
+
267
+ return c.json(process);
268
+ });
269
+
270
+ /**
271
+ * Kill a process
272
+ * DELETE /sandbox/:id/process/:processId
273
+ */
274
+ app.delete('/sandbox/:id/process/:processId', async (c) => {
275
+ try {
276
+ const id = c.req.param('id');
277
+ const processId = c.req.param('processId');
278
+ const sandbox = manager.getSandbox(id);
279
+
280
+ if (!sandbox) {
281
+ return c.json({ error: 'Sandbox not found' }, 404);
282
+ }
283
+
284
+ const signal = c.req.query('signal') || 'SIGTERM';
285
+ await sandbox.killProcess(processId, signal);
286
+
287
+ return c.json({ success: true });
288
+ } catch (error: any) {
289
+ return c.json({ error: error.message }, 404);
290
+ }
291
+ });
292
+
293
+ /**
294
+ * Stream process logs
295
+ * GET /sandbox/:id/process/:processId/logs
296
+ */
297
+ app.get('/sandbox/:id/process/:processId/logs', (c) => {
298
+ const id = c.req.param('id');
299
+ const processId = c.req.param('processId');
300
+ const sandbox = manager.getSandbox(id);
301
+
302
+ if (!sandbox) {
303
+ return c.json({ error: 'Sandbox not found' }, 404);
304
+ }
305
+
306
+ return stream(c, async (stream) => {
307
+ const logStream = StreamService.createProcessLogStream(sandbox, processId);
308
+
309
+ c.header('Content-Type', 'text/event-stream');
310
+ c.header('Cache-Control', 'no-cache');
311
+ c.header('Connection', 'keep-alive');
312
+
313
+ for await (const chunk of logStream) {
314
+ await stream.write(chunk);
315
+ }
316
+ });
317
+ });
318
+
319
+ // ============================================================================
320
+ // File Operations Endpoints
321
+ // ============================================================================
322
+
323
+ /**
324
+ * List files in a directory
325
+ * GET /sandbox/:id/files?path=...&recursive=true
326
+ */
327
+ app.get('/sandbox/:id/files', async (c) => {
328
+ try {
329
+ const id = c.req.param('id');
330
+ const sandbox = manager.getSandbox(id);
331
+
332
+ if (!sandbox) {
333
+ return c.json({ error: 'Sandbox not found' }, 404);
334
+ }
335
+
336
+ const fileService = new FileService(sandbox.workDir);
337
+ const dirPath = c.req.query('path') || '.';
338
+ const recursive = c.req.query('recursive') === 'true';
339
+ const pattern = c.req.query('pattern');
340
+ const includeHidden = c.req.query('hidden') === 'true';
341
+
342
+ const files = await fileService.listFiles(dirPath, {
343
+ recursive,
344
+ pattern,
345
+ includeHidden,
346
+ });
347
+
348
+ return c.json({ files });
349
+ } catch (error: any) {
350
+ return c.json({ error: error.message }, 500);
351
+ }
352
+ });
353
+
354
+ /**
355
+ * Read a file
356
+ * GET /sandbox/:id/files/*
357
+ */
358
+ app.get('/sandbox/:id/files/*', async (c) => {
359
+ try {
360
+ const id = c.req.param('id');
361
+ const sandbox = manager.getSandbox(id);
362
+
363
+ if (!sandbox) {
364
+ return c.json({ error: 'Sandbox not found' }, 404);
365
+ }
366
+
367
+ // Get the file path from the wildcard
368
+ const fullPath = c.req.path;
369
+ const filePath = fullPath.replace(`/sandbox/${id}/files/`, '');
370
+
371
+ if (!filePath) {
372
+ return c.json({ error: 'File path is required' }, 400);
373
+ }
374
+
375
+ const fileService = new FileService(sandbox.workDir);
376
+ const raw = c.req.query('raw') === 'true';
377
+
378
+ if (raw) {
379
+ // Return raw file as stream
380
+ const stream = fileService.createReadStream(filePath);
381
+ return c.body(stream as any);
382
+ } else {
383
+ // Return file content as JSON
384
+ const content = await fileService.readFile(filePath);
385
+ return c.json({ content });
386
+ }
387
+ } catch (error: any) {
388
+ return c.json({ error: error.message }, 500);
389
+ }
390
+ });
391
+
392
+ /**
393
+ * Write a file
394
+ * POST /sandbox/:id/files/*
395
+ */
396
+ app.post('/sandbox/:id/files/*', async (c) => {
397
+ try {
398
+ const id = c.req.param('id');
399
+ const sandbox = manager.getSandbox(id);
400
+
401
+ if (!sandbox) {
402
+ return c.json({ error: 'Sandbox not found' }, 404);
403
+ }
404
+
405
+ const fullPath = c.req.path;
406
+ const filePath = fullPath.replace(`/sandbox/${id}/files/`, '');
407
+
408
+ if (!filePath) {
409
+ return c.json({ error: 'File path is required' }, 400);
410
+ }
411
+
412
+ const body = await c.req.json();
413
+ const { content } = body;
414
+
415
+ if (content === undefined) {
416
+ return c.json({ error: 'Content is required' }, 400);
417
+ }
418
+
419
+ const fileService = new FileService(sandbox.workDir);
420
+ await fileService.writeFile(filePath, content, { createDirs: true });
421
+
422
+ return c.json({ success: true, path: filePath });
423
+ } catch (error: any) {
424
+ return c.json({ error: error.message }, 500);
425
+ }
426
+ });
427
+
428
+ /**
429
+ * Delete a file
430
+ * DELETE /sandbox/:id/files/*
431
+ */
432
+ app.delete('/sandbox/:id/files/*', async (c) => {
433
+ try {
434
+ const id = c.req.param('id');
435
+ const sandbox = manager.getSandbox(id);
436
+
437
+ if (!sandbox) {
438
+ return c.json({ error: 'Sandbox not found' }, 404);
439
+ }
440
+
441
+ const fullPath = c.req.path;
442
+ const filePath = fullPath.replace(`/sandbox/${id}/files/`, '');
443
+
444
+ if (!filePath) {
445
+ return c.json({ error: 'File path is required' }, 400);
446
+ }
447
+
448
+ const fileService = new FileService(sandbox.workDir);
449
+ const recursive = c.req.query('recursive') === 'true';
450
+
451
+ await fileService.deleteFile(filePath, { recursive });
452
+
453
+ return c.json({ success: true });
454
+ } catch (error: any) {
455
+ return c.json({ error: error.message }, 500);
456
+ }
457
+ });
458
+
459
+ // ============================================================================
460
+ // Environment Variable Endpoints
461
+ // ============================================================================
462
+
463
+ /**
464
+ * Set environment variables
465
+ * POST /sandbox/:id/env
466
+ * Body: { variables: { KEY: "value", ... } }
467
+ */
468
+ app.post('/sandbox/:id/env', async (c) => {
469
+ try {
470
+ const id = c.req.param('id');
471
+ const sandbox = manager.getSandbox(id);
472
+
473
+ if (!sandbox) {
474
+ return c.json({ error: 'Sandbox not found' }, 404);
475
+ }
476
+
477
+ const body = await c.req.json();
478
+ const { variables } = body;
479
+
480
+ if (!variables || typeof variables !== 'object') {
481
+ return c.json({ error: 'Variables object is required' }, 400);
482
+ }
483
+
484
+ sandbox.setEnvBatch(variables);
485
+
486
+ return c.json({
487
+ success: true,
488
+ count: Object.keys(variables).length,
489
+ });
490
+ } catch (error: any) {
491
+ return c.json({ error: error.message }, 500);
492
+ }
493
+ });
494
+
495
+ /**
496
+ * Get environment variable names (not values for security)
497
+ * GET /sandbox/:id/env
498
+ */
499
+ app.get('/sandbox/:id/env', (c) => {
500
+ const id = c.req.param('id');
501
+ const sandbox = manager.getSandbox(id);
502
+
503
+ if (!sandbox) {
504
+ return c.json({ error: 'Sandbox not found' }, 404);
505
+ }
506
+
507
+ const keys = sandbox.getEnvKeys();
508
+ return c.json({ variables: keys, count: keys.length });
509
+ });
510
+
511
+ /**
512
+ * Unset an environment variable
513
+ * DELETE /sandbox/:id/env/:key
514
+ */
515
+ app.delete('/sandbox/:id/env/:key', (c) => {
516
+ try {
517
+ const id = c.req.param('id');
518
+ const key = c.req.param('key');
519
+ const sandbox = manager.getSandbox(id);
520
+
521
+ if (!sandbox) {
522
+ return c.json({ error: 'Sandbox not found' }, 404);
523
+ }
524
+
525
+ const removed = sandbox.unsetEnv(key);
526
+
527
+ if (!removed) {
528
+ return c.json({ error: 'Environment variable not found' }, 404);
529
+ }
530
+
531
+ return c.json({ success: true, key });
532
+ } catch (error: any) {
533
+ return c.json({ error: error.message }, 500);
534
+ }
535
+ });
536
+
537
+ // ============================================================================
538
+ // HTTP Exposure Endpoints
539
+ // ============================================================================
540
+
541
+ /**
542
+ * Expose a sandbox port via HTTP
543
+ * POST /sandbox/:id/expose
544
+ * Body: { port: 3000 }
545
+ */
546
+ app.post('/sandbox/:id/expose', async (c) => {
547
+ try {
548
+ const id = c.req.param('id');
549
+ const sandbox = manager.getSandbox(id);
550
+
551
+ if (!sandbox) {
552
+ return c.json({ error: 'Sandbox not found' }, 404);
553
+ }
554
+
555
+ const body = await c.req.json();
556
+ const { port } = body;
557
+
558
+ if (!port || typeof port !== 'number') {
559
+ return c.json({ error: 'Port number is required' }, 400);
560
+ }
561
+
562
+ const url = await httpExposure.expose(id, port);
563
+
564
+ return c.json({ url, port, sandboxId: id });
565
+ } catch (error: any) {
566
+ return c.json({ error: error.message }, 500);
567
+ }
568
+ });
569
+
570
+ /**
571
+ * Get all exposed endpoints for a sandbox
572
+ * GET /sandbox/:id/expose
573
+ */
574
+ app.get('/sandbox/:id/expose', (c) => {
575
+ const id = c.req.param('id');
576
+ const sandbox = manager.getSandbox(id);
577
+
578
+ if (!sandbox) {
579
+ return c.json({ error: 'Sandbox not found' }, 404);
580
+ }
581
+
582
+ const exposures = httpExposure.getExposures(id);
583
+ return c.json({ exposures });
584
+ });
585
+
586
+ /**
587
+ * Remove HTTP exposure for a sandbox port
588
+ * DELETE /sandbox/:id/expose?port=3000
589
+ */
590
+ app.delete('/sandbox/:id/expose', async (c) => {
591
+ try {
592
+ const id = c.req.param('id');
593
+ const sandbox = manager.getSandbox(id);
594
+
595
+ if (!sandbox) {
596
+ return c.json({ error: 'Sandbox not found' }, 404);
597
+ }
598
+
599
+ const portStr = c.req.query('port');
600
+ const port = portStr ? parseInt(portStr) : undefined;
601
+
602
+ await httpExposure.unexpose(id, port);
603
+
604
+ return c.json({ success: true });
605
+ } catch (error: any) {
606
+ return c.json({ error: error.message }, 500);
607
+ }
608
+ });
609
+
610
+ // Cleanup on process exit
611
+ process.on('SIGINT', async () => {
612
+ console.log('\nShutting down...');
613
+ console.log('Cleaning up sandboxes...');
614
+ await manager.shutdown({ cleanup: true });
615
+ console.log('✅ Shutdown complete');
616
+ process.exit(0);
617
+ });
618
+
619
+ process.on('SIGTERM', async () => {
620
+ console.log('\nShutting down...');
621
+ console.log('Cleaning up sandboxes...');
622
+ await manager.shutdown({ cleanup: true });
623
+ console.log('✅ Shutdown complete');
624
+ process.exit(0);
625
+ });
626
+
627
+ return { app, manager, httpExposure };
628
+ }
629
+
630
+ /**
631
+ * Start the sandbox server
632
+ */
633
+ export async function startServer(config: ServerConfig = {}) {
634
+ const port = config.port || 3000;
635
+ const host = config.host || '0.0.0.0';
636
+
637
+ const { app, manager } = createServer(config);
638
+
639
+ console.log(`🌳 TreeSap Sandbox Server starting...`);
640
+ console.log(`📁 Base path: ${manager.getStats().basePath}`);
641
+ console.log(`🚀 Server listening on http://${host}:${port}`);
642
+
643
+ const server = serve({
644
+ fetch: app.fetch,
645
+ port,
646
+ hostname: host,
647
+ });
648
+
649
+ return { server, app, manager };
650
+ }
651
+
652
+ // Start server if this file is run directly
653
+ if (import.meta.url === `file://${process.argv[1]}`) {
654
+ startServer().catch((error) => {
655
+ console.error('Failed to start server:', error);
656
+ process.exit(1);
657
+ });
658
+ }
@@ -0,0 +1,65 @@
1
+ import { Context, Next } from 'hono';
2
+
3
+ export interface AuthConfig {
4
+ /**
5
+ * List of valid API keys
6
+ */
7
+ apiKeys: string[];
8
+
9
+ /**
10
+ * Header name to check for API key (default: 'X-API-Key')
11
+ */
12
+ headerName?: string;
13
+
14
+ /**
15
+ * Paths to exclude from authentication (default: ['/'])
16
+ */
17
+ excludePaths?: string[];
18
+ }
19
+
20
+ /**
21
+ * Create authentication middleware for API key validation
22
+ */
23
+ export function createAuthMiddleware(config: AuthConfig) {
24
+ const headerName = config.headerName || 'X-API-Key';
25
+ const excludePaths = config.excludePaths || ['/'];
26
+
27
+ return async (c: Context, next: Next) => {
28
+ // Skip auth for excluded paths (exact match)
29
+ if (excludePaths.includes(c.req.path)) {
30
+ return next();
31
+ }
32
+
33
+ // Get API key from header
34
+ const apiKey = c.req.header(headerName);
35
+
36
+ if (!apiKey) {
37
+ return c.json(
38
+ { error: 'API key required', message: `Missing ${headerName} header` },
39
+ 401
40
+ );
41
+ }
42
+
43
+ if (!config.apiKeys.includes(apiKey)) {
44
+ return c.json(
45
+ { error: 'Invalid API key', message: 'The provided API key is not valid' },
46
+ 403
47
+ );
48
+ }
49
+
50
+ // API key is valid, continue to the next middleware/handler
51
+ return next();
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Parse API keys from environment variable
57
+ * Supports comma-separated list of keys
58
+ */
59
+ export function parseApiKeysFromEnv(envVar: string = 'SANDBOX_API_KEYS'): string[] {
60
+ const keysString = process.env[envVar] || '';
61
+ return keysString
62
+ .split(',')
63
+ .map((key) => key.trim())
64
+ .filter((key) => key.length > 0);
65
+ }