@cloudflare/sandbox 0.0.7 → 0.0.9

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/src/client.ts CHANGED
@@ -1,9 +1,10 @@
1
- import type { DurableObject } from "cloudflare:workers";
2
1
  import type { Sandbox } from "./index";
3
2
 
4
3
  interface ExecuteRequest {
5
4
  command: string;
6
5
  args?: string[];
6
+ sessionId?: string;
7
+ background?: boolean;
7
8
  }
8
9
 
9
10
  export interface ExecuteResponse {
@@ -139,6 +140,37 @@ export interface MoveFileResponse {
139
140
  timestamp: string;
140
141
  }
141
142
 
143
+ interface PreviewInfo {
144
+ url: string;
145
+ port: number;
146
+ name?: string;
147
+ }
148
+
149
+ interface ExposedPort extends PreviewInfo {
150
+ exposedAt: string;
151
+ timestamp: string;
152
+ }
153
+
154
+ interface ExposePortResponse {
155
+ success: boolean;
156
+ port: number;
157
+ name?: string;
158
+ exposedAt: string;
159
+ timestamp: string;
160
+ }
161
+
162
+ interface UnexposePortResponse {
163
+ success: boolean;
164
+ port: number;
165
+ timestamp: string;
166
+ }
167
+
168
+ interface GetExposedPortsResponse {
169
+ ports: ExposedPort[];
170
+ count: number;
171
+ timestamp: string;
172
+ }
173
+
142
174
  interface PingResponse {
143
175
  message: string;
144
176
  timestamp: string;
@@ -276,13 +308,13 @@ export class HttpClient {
276
308
 
277
309
  getOnCommandComplete():
278
310
  | ((
279
- success: boolean,
280
- exitCode: number,
281
- stdout: string,
282
- stderr: string,
283
- command: string,
284
- args: string[]
285
- ) => void)
311
+ success: boolean,
312
+ exitCode: number,
313
+ stdout: string,
314
+ stderr: string,
315
+ command: string,
316
+ args: string[]
317
+ ) => void)
286
318
  | undefined {
287
319
  return this.options.onCommandComplete;
288
320
  }
@@ -339,7 +371,8 @@ export class HttpClient {
339
371
  async execute(
340
372
  command: string,
341
373
  args: string[] = [],
342
- sessionId?: string
374
+ sessionId?: string,
375
+ background: boolean = false,
343
376
  ): Promise<ExecuteResponse> {
344
377
  try {
345
378
  const targetSessionId = sessionId || this.sessionId;
@@ -348,8 +381,9 @@ export class HttpClient {
348
381
  body: JSON.stringify({
349
382
  args,
350
383
  command,
384
+ background,
351
385
  sessionId: targetSessionId,
352
- }),
386
+ } as ExecuteRequest),
353
387
  headers: {
354
388
  "Content-Type": "application/json",
355
389
  },
@@ -395,7 +429,8 @@ export class HttpClient {
395
429
  async executeStream(
396
430
  command: string,
397
431
  args: string[] = [],
398
- sessionId?: string
432
+ sessionId?: string,
433
+ background: boolean = false
399
434
  ): Promise<void> {
400
435
  try {
401
436
  const targetSessionId = sessionId || this.sessionId;
@@ -404,6 +439,7 @@ export class HttpClient {
404
439
  body: JSON.stringify({
405
440
  args,
406
441
  command,
442
+ background,
407
443
  sessionId: targetSessionId,
408
444
  }),
409
445
  headers: {
@@ -451,8 +487,7 @@ export class HttpClient {
451
487
  switch (event.type) {
452
488
  case "command_start":
453
489
  console.log(
454
- `[HTTP Client] Command started: ${
455
- event.command
490
+ `[HTTP Client] Command started: ${event.command
456
491
  } ${event.args?.join(" ")}`
457
492
  );
458
493
  this.options.onCommandStart?.(
@@ -533,7 +568,7 @@ export class HttpClient {
533
568
  repoUrl,
534
569
  sessionId: targetSessionId,
535
570
  targetDir,
536
- }),
571
+ } as GitCheckoutRequest),
537
572
  headers: {
538
573
  "Content-Type": "application/json",
539
574
  },
@@ -624,8 +659,7 @@ export class HttpClient {
624
659
  switch (event.type) {
625
660
  case "command_start":
626
661
  console.log(
627
- `[HTTP Client] Git checkout started: ${
628
- event.command
662
+ `[HTTP Client] Git checkout started: ${event.command
629
663
  } ${event.args?.join(" ")}`
630
664
  );
631
665
  this.options.onCommandStart?.(
@@ -704,7 +738,7 @@ export class HttpClient {
704
738
  path,
705
739
  recursive,
706
740
  sessionId: targetSessionId,
707
- }),
741
+ } as MkdirRequest),
708
742
  headers: {
709
743
  "Content-Type": "application/json",
710
744
  },
@@ -745,7 +779,7 @@ export class HttpClient {
745
779
  path,
746
780
  recursive,
747
781
  sessionId: targetSessionId,
748
- }),
782
+ } as MkdirRequest),
749
783
  headers: {
750
784
  "Content-Type": "application/json",
751
785
  },
@@ -791,8 +825,7 @@ export class HttpClient {
791
825
  switch (event.type) {
792
826
  case "command_start":
793
827
  console.log(
794
- `[HTTP Client] Mkdir started: ${
795
- event.command
828
+ `[HTTP Client] Mkdir started: ${event.command
796
829
  } ${event.args?.join(" ")}`
797
830
  );
798
831
  this.options.onCommandStart?.(
@@ -871,7 +904,7 @@ export class HttpClient {
871
904
  encoding,
872
905
  path,
873
906
  sessionId: targetSessionId,
874
- }),
907
+ } as WriteFileRequest),
875
908
  headers: {
876
909
  "Content-Type": "application/json",
877
910
  },
@@ -914,7 +947,7 @@ export class HttpClient {
914
947
  encoding,
915
948
  path,
916
949
  sessionId: targetSessionId,
917
- }),
950
+ } as WriteFileRequest),
918
951
  headers: {
919
952
  "Content-Type": "application/json",
920
953
  },
@@ -1037,7 +1070,7 @@ export class HttpClient {
1037
1070
  encoding,
1038
1071
  path,
1039
1072
  sessionId: targetSessionId,
1040
- }),
1073
+ } as ReadFileRequest),
1041
1074
  headers: {
1042
1075
  "Content-Type": "application/json",
1043
1076
  },
@@ -1078,7 +1111,7 @@ export class HttpClient {
1078
1111
  encoding,
1079
1112
  path,
1080
1113
  sessionId: targetSessionId,
1081
- }),
1114
+ } as ReadFileRequest),
1082
1115
  headers: {
1083
1116
  "Content-Type": "application/json",
1084
1117
  },
@@ -1133,10 +1166,8 @@ export class HttpClient {
1133
1166
 
1134
1167
  case "command_complete":
1135
1168
  console.log(
1136
- `[HTTP Client] Read file completed: ${
1137
- event.path
1138
- }, Success: ${event.success}, Content length: ${
1139
- event.content?.length || 0
1169
+ `[HTTP Client] Read file completed: ${event.path
1170
+ }, Success: ${event.success}, Content length: ${event.content?.length || 0
1140
1171
  }`
1141
1172
  );
1142
1173
  this.options.onCommandComplete?.(
@@ -1193,7 +1224,7 @@ export class HttpClient {
1193
1224
  body: JSON.stringify({
1194
1225
  path,
1195
1226
  sessionId: targetSessionId,
1196
- }),
1227
+ } as DeleteFileRequest),
1197
1228
  headers: {
1198
1229
  "Content-Type": "application/json",
1199
1230
  },
@@ -1229,7 +1260,7 @@ export class HttpClient {
1229
1260
  body: JSON.stringify({
1230
1261
  path,
1231
1262
  sessionId: targetSessionId,
1232
- }),
1263
+ } as DeleteFileRequest),
1233
1264
  headers: {
1234
1265
  "Content-Type": "application/json",
1235
1266
  },
@@ -1339,7 +1370,7 @@ export class HttpClient {
1339
1370
  newPath,
1340
1371
  oldPath,
1341
1372
  sessionId: targetSessionId,
1342
- }),
1373
+ } as RenameFileRequest),
1343
1374
  headers: {
1344
1375
  "Content-Type": "application/json",
1345
1376
  },
@@ -1380,7 +1411,7 @@ export class HttpClient {
1380
1411
  newPath,
1381
1412
  oldPath,
1382
1413
  sessionId: targetSessionId,
1383
- }),
1414
+ } as RenameFileRequest),
1384
1415
  headers: {
1385
1416
  "Content-Type": "application/json",
1386
1417
  },
@@ -1493,7 +1524,7 @@ export class HttpClient {
1493
1524
  destinationPath,
1494
1525
  sessionId: targetSessionId,
1495
1526
  sourcePath,
1496
- }),
1527
+ } as MoveFileRequest),
1497
1528
  headers: {
1498
1529
  "Content-Type": "application/json",
1499
1530
  },
@@ -1534,7 +1565,7 @@ export class HttpClient {
1534
1565
  destinationPath,
1535
1566
  sessionId: targetSessionId,
1536
1567
  sourcePath,
1537
- }),
1568
+ } as MoveFileRequest),
1538
1569
  headers: {
1539
1570
  "Content-Type": "application/json",
1540
1571
  },
@@ -1637,6 +1668,104 @@ export class HttpClient {
1637
1668
  }
1638
1669
  }
1639
1670
 
1671
+ async exposePort(port: number, name?: string): Promise<ExposePortResponse> {
1672
+ try {
1673
+ const response = await this.doFetch(`/api/expose-port`, {
1674
+ body: JSON.stringify({
1675
+ port,
1676
+ name,
1677
+ }),
1678
+ headers: {
1679
+ "Content-Type": "application/json",
1680
+ },
1681
+ method: "POST",
1682
+ });
1683
+
1684
+ if (!response.ok) {
1685
+ const errorData = (await response.json().catch(() => ({}))) as {
1686
+ error?: string;
1687
+ };
1688
+ console.log(errorData);
1689
+ throw new Error(
1690
+ errorData.error || `HTTP error! status: ${response.status}`
1691
+ );
1692
+ }
1693
+
1694
+ const data: ExposePortResponse = await response.json();
1695
+ console.log(
1696
+ `[HTTP Client] Port exposed: ${port}${name ? ` (${name})` : ""}, Success: ${data.success}`
1697
+ );
1698
+
1699
+ return data;
1700
+ } catch (error) {
1701
+ console.error("[HTTP Client] Error exposing port:", error);
1702
+ throw error;
1703
+ }
1704
+ }
1705
+
1706
+ async unexposePort(port: number): Promise<UnexposePortResponse> {
1707
+ try {
1708
+ const response = await this.doFetch(`/api/unexpose-port`, {
1709
+ body: JSON.stringify({
1710
+ port,
1711
+ }),
1712
+ headers: {
1713
+ "Content-Type": "application/json",
1714
+ },
1715
+ method: "DELETE",
1716
+ });
1717
+
1718
+ if (!response.ok) {
1719
+ const errorData = (await response.json().catch(() => ({}))) as {
1720
+ error?: string;
1721
+ };
1722
+ throw new Error(
1723
+ errorData.error || `HTTP error! status: ${response.status}`
1724
+ );
1725
+ }
1726
+
1727
+ const data: UnexposePortResponse = await response.json();
1728
+ console.log(
1729
+ `[HTTP Client] Port unexposed: ${port}, Success: ${data.success}`
1730
+ );
1731
+
1732
+ return data;
1733
+ } catch (error) {
1734
+ console.error("[HTTP Client] Error unexposing port:", error);
1735
+ throw error;
1736
+ }
1737
+ }
1738
+
1739
+ async getExposedPorts(): Promise<GetExposedPortsResponse> {
1740
+ try {
1741
+ const response = await this.doFetch(`/api/exposed-ports`, {
1742
+ headers: {
1743
+ "Content-Type": "application/json",
1744
+ },
1745
+ method: "GET",
1746
+ });
1747
+
1748
+ if (!response.ok) {
1749
+ const errorData = (await response.json().catch(() => ({}))) as {
1750
+ error?: string;
1751
+ };
1752
+ throw new Error(
1753
+ errorData.error || `HTTP error! status: ${response.status}`
1754
+ );
1755
+ }
1756
+
1757
+ const data: GetExposedPortsResponse = await response.json();
1758
+ console.log(
1759
+ `[HTTP Client] Got ${data.count} exposed ports`
1760
+ );
1761
+
1762
+ return data;
1763
+ } catch (error) {
1764
+ console.error("[HTTP Client] Error getting exposed ports:", error);
1765
+ throw error;
1766
+ }
1767
+ }
1768
+
1640
1769
  async ping(): Promise<string> {
1641
1770
  try {
1642
1771
  const response = await this.doFetch(`/api/ping`, {
package/src/index.ts CHANGED
@@ -1,136 +1,14 @@
1
- import { Container, getContainer } from "@cloudflare/containers";
2
- import { HttpClient } from "./client";
3
-
4
- export function getSandbox(ns: DurableObjectNamespace<Sandbox>, id: string) {
5
- return getContainer(ns, id);
6
- }
7
-
8
- export class Sandbox<Env = unknown> extends Container<Env> {
9
- defaultPort = 3000; // The default port for the container to listen on
10
- sleepAfter = "3m"; // Sleep the sandbox if no requests are made in this timeframe
11
- client: HttpClient;
12
-
13
- constructor(ctx: DurableObjectState, env: Env) {
14
- super(ctx, env);
15
- this.client = new HttpClient({
16
- onCommandComplete: (success, exitCode, stdout, stderr, command, args) => {
17
- console.log(
18
- `[Container] Command completed: ${command}, Success: ${success}, Exit code: ${exitCode}`
19
- );
20
- },
21
- onCommandStart: (command, args) => {
22
- console.log(
23
- `[Container] Command started: ${command} ${args.join(" ")}`
24
- );
25
- },
26
- onError: (error, command, args) => {
27
- console.error(`[Container] Command error: ${error}`);
28
- },
29
- onOutput: (stream, data, command) => {
30
- console.log(`[Container] [${stream}] ${data}`);
31
- },
32
- port: this.defaultPort,
33
- stub: this,
34
- });
35
- }
36
-
37
- envVars = {
38
- MESSAGE: "I was passed in via the Sandbox class!",
39
- };
40
-
41
- override onStart() {
42
- console.log("Sandbox successfully started");
43
- }
44
-
45
- override onStop() {
46
- console.log("Sandbox successfully shut down");
47
- if (this.client) {
48
- this.client.clearSession();
49
- }
50
- }
51
-
52
- override onError(error: unknown) {
53
- console.log("Sandbox error:", error);
54
- }
55
-
56
- async exec(command: string, args: string[], options?: { stream?: boolean }) {
57
- if (options?.stream) {
58
- return this.client.executeStream(command, args);
59
- }
60
- return this.client.execute(command, args);
61
- }
62
-
63
- async gitCheckout(
64
- repoUrl: string,
65
- options: { branch?: string; targetDir?: string; stream?: boolean }
66
- ) {
67
- if (options?.stream) {
68
- return this.client.gitCheckoutStream(
69
- repoUrl,
70
- options.branch,
71
- options.targetDir
72
- );
73
- }
74
- return this.client.gitCheckout(repoUrl, options.branch, options.targetDir);
75
- }
76
-
77
- async mkdir(
78
- path: string,
79
- options: { recursive?: boolean; stream?: boolean } = {}
80
- ) {
81
- if (options?.stream) {
82
- return this.client.mkdirStream(path, options.recursive);
83
- }
84
- return this.client.mkdir(path, options.recursive);
85
- }
86
-
87
- async writeFile(
88
- path: string,
89
- content: string,
90
- options: { encoding?: string; stream?: boolean } = {}
91
- ) {
92
- if (options?.stream) {
93
- return this.client.writeFileStream(path, content, options.encoding);
94
- }
95
- return this.client.writeFile(path, content, options.encoding);
96
- }
97
-
98
- async deleteFile(path: string, options: { stream?: boolean } = {}) {
99
- if (options?.stream) {
100
- return this.client.deleteFileStream(path);
101
- }
102
- return this.client.deleteFile(path);
103
- }
104
-
105
- async renameFile(
106
- oldPath: string,
107
- newPath: string,
108
- options: { stream?: boolean } = {}
109
- ) {
110
- if (options?.stream) {
111
- return this.client.renameFileStream(oldPath, newPath);
112
- }
113
- return this.client.renameFile(oldPath, newPath);
114
- }
115
-
116
- async moveFile(
117
- sourcePath: string,
118
- destinationPath: string,
119
- options: { stream?: boolean } = {}
120
- ) {
121
- if (options?.stream) {
122
- return this.client.moveFileStream(sourcePath, destinationPath);
123
- }
124
- return this.client.moveFile(sourcePath, destinationPath);
125
- }
126
-
127
- async readFile(
128
- path: string,
129
- options: { encoding?: string; stream?: boolean } = {}
130
- ) {
131
- if (options?.stream) {
132
- return this.client.readFileStream(path, options.encoding);
133
- }
134
- return this.client.readFile(path, options.encoding);
135
- }
136
- }
1
+ // Export types from client
2
+ export type {
3
+ DeleteFileResponse, ExecuteResponse,
4
+ GitCheckoutResponse,
5
+ MkdirResponse, MoveFileResponse,
6
+ ReadFileResponse, RenameFileResponse, WriteFileResponse
7
+ } from "./client";
8
+
9
+ // Re-export request handler utilities
10
+ export {
11
+ proxyToSandbox, type RouteInfo, type SandboxEnv
12
+ } from './request-handler';
13
+
14
+ export { getSandbox, Sandbox } from "./sandbox";
@@ -0,0 +1,95 @@
1
+ import { getSandbox, type Sandbox } from "./sandbox";
2
+
3
+ export interface SandboxEnv {
4
+ Sandbox: DurableObjectNamespace<Sandbox>;
5
+ }
6
+
7
+ export interface RouteInfo {
8
+ port: number;
9
+ sandboxId: string;
10
+ path: string;
11
+ }
12
+
13
+ export async function proxyToSandbox<E extends SandboxEnv>(
14
+ request: Request,
15
+ env: E
16
+ ): Promise<Response | null> {
17
+ try {
18
+ const url = new URL(request.url);
19
+ const routeInfo = extractSandboxRoute(url);
20
+
21
+ if (!routeInfo) {
22
+ return null; // Not a request to an exposed container port
23
+ }
24
+
25
+ const { sandboxId, port, path } = routeInfo;
26
+ const sandbox = getSandbox(env.Sandbox, sandboxId);
27
+
28
+ // Build proxy request with proper headers
29
+ let proxyUrl: string;
30
+
31
+ // Route based on the target port
32
+ if (port !== 3000) {
33
+ // Route directly to user's service on the specified port
34
+ proxyUrl = `http://localhost:${port}${path}${url.search}`;
35
+ } else {
36
+ // Port 3000 is our control plane - route normally
37
+ proxyUrl = `http://localhost:3000${path}${url.search}`;
38
+ }
39
+
40
+ const proxyRequest = new Request(proxyUrl, {
41
+ method: request.method,
42
+ headers: {
43
+ ...Object.fromEntries(request.headers),
44
+ 'X-Original-URL': request.url,
45
+ 'X-Forwarded-Host': url.hostname,
46
+ 'X-Forwarded-Proto': url.protocol.replace(':', ''),
47
+ 'X-Sandbox-Name': sandboxId, // Pass the friendly name
48
+ },
49
+ body: request.body,
50
+ });
51
+
52
+ return sandbox.containerFetch(proxyRequest, port);
53
+ } catch (error) {
54
+ console.error('[Sandbox] Proxy routing error:', error);
55
+ return new Response('Proxy routing error', { status: 500 });
56
+ }
57
+ }
58
+
59
+ function extractSandboxRoute(url: URL): RouteInfo | null {
60
+ // Production: subdomain pattern {port}-{sandboxId}.{domain}
61
+ const subdomainMatch = url.hostname.match(/^(\d+)-([a-zA-Z0-9-]+)\./);
62
+ if (subdomainMatch) {
63
+ return {
64
+ port: parseInt(subdomainMatch[1]),
65
+ sandboxId: subdomainMatch[2],
66
+ path: url.pathname,
67
+ };
68
+ }
69
+
70
+ // Development: path pattern /preview/{port}/{sandboxId}/*
71
+ if (isLocalhostPattern(url.hostname)) {
72
+ const pathMatch = url.pathname.match(/^\/preview\/(\d+)\/([^/]+)(\/.*)?$/);
73
+ if (pathMatch) {
74
+ return {
75
+ port: parseInt(pathMatch[1]),
76
+ sandboxId: pathMatch[2],
77
+ path: pathMatch[3] || "/",
78
+ };
79
+ }
80
+ }
81
+
82
+ return null;
83
+ }
84
+
85
+ export function isLocalhostPattern(hostname: string): boolean {
86
+ const hostPart = hostname.split(":")[0];
87
+ return (
88
+ hostPart === "localhost" ||
89
+ hostPart === "127.0.0.1" ||
90
+ hostPart === "::1" ||
91
+ hostPart === "[::1]" ||
92
+ hostPart === "0.0.0.0"
93
+ );
94
+ }
95
+