@cloudflare/sandbox 0.0.0-eb0ea62 → 0.0.0-f106fda
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/CHANGELOG.md +24 -0
- package/Dockerfile +86 -9
- package/container_src/index.ts +436 -84
- package/package.json +5 -3
- package/src/client.ts +197 -37
- package/src/index.ts +14 -129
- package/src/request-handler.ts +95 -0
- package/src/sandbox.ts +252 -0
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;
|
|
@@ -203,10 +235,41 @@ export class HttpClient {
|
|
|
203
235
|
path: string,
|
|
204
236
|
options?: RequestInit
|
|
205
237
|
): Promise<Response> {
|
|
206
|
-
|
|
207
|
-
|
|
238
|
+
const url = this.options.stub
|
|
239
|
+
? `http://localhost:${this.options.port}${path}`
|
|
240
|
+
: `${this.baseUrl}${path}`;
|
|
241
|
+
const method = options?.method || "GET";
|
|
242
|
+
|
|
243
|
+
console.log(`[HTTP Client] Making ${method} request to ${url}`);
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
let response: Response;
|
|
247
|
+
|
|
248
|
+
if (this.options.stub) {
|
|
249
|
+
response = await this.options.stub.containerFetch(
|
|
250
|
+
url,
|
|
251
|
+
options,
|
|
252
|
+
this.options.port
|
|
253
|
+
);
|
|
254
|
+
} else {
|
|
255
|
+
response = await fetch(url, options);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
console.log(
|
|
259
|
+
`[HTTP Client] Response: ${response.status} ${response.statusText}`
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
if (!response.ok) {
|
|
263
|
+
console.error(
|
|
264
|
+
`[HTTP Client] Request failed: ${method} ${url} - ${response.status} ${response.statusText}`
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return response;
|
|
269
|
+
} catch (error) {
|
|
270
|
+
console.error(`[HTTP Client] Request error: ${method} ${url}`, error);
|
|
271
|
+
throw error;
|
|
208
272
|
}
|
|
209
|
-
return fetch(this.baseUrl + path, options);
|
|
210
273
|
}
|
|
211
274
|
// Public methods to set event handlers
|
|
212
275
|
setOnOutput(
|
|
@@ -245,13 +308,13 @@ export class HttpClient {
|
|
|
245
308
|
|
|
246
309
|
getOnCommandComplete():
|
|
247
310
|
| ((
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
311
|
+
success: boolean,
|
|
312
|
+
exitCode: number,
|
|
313
|
+
stdout: string,
|
|
314
|
+
stderr: string,
|
|
315
|
+
command: string,
|
|
316
|
+
args: string[]
|
|
317
|
+
) => void)
|
|
255
318
|
| undefined {
|
|
256
319
|
return this.options.onCommandComplete;
|
|
257
320
|
}
|
|
@@ -308,7 +371,8 @@ export class HttpClient {
|
|
|
308
371
|
async execute(
|
|
309
372
|
command: string,
|
|
310
373
|
args: string[] = [],
|
|
311
|
-
sessionId?: string
|
|
374
|
+
sessionId?: string,
|
|
375
|
+
background: boolean = false,
|
|
312
376
|
): Promise<ExecuteResponse> {
|
|
313
377
|
try {
|
|
314
378
|
const targetSessionId = sessionId || this.sessionId;
|
|
@@ -317,8 +381,9 @@ export class HttpClient {
|
|
|
317
381
|
body: JSON.stringify({
|
|
318
382
|
args,
|
|
319
383
|
command,
|
|
384
|
+
background,
|
|
320
385
|
sessionId: targetSessionId,
|
|
321
|
-
}),
|
|
386
|
+
} as ExecuteRequest),
|
|
322
387
|
headers: {
|
|
323
388
|
"Content-Type": "application/json",
|
|
324
389
|
},
|
|
@@ -364,7 +429,8 @@ export class HttpClient {
|
|
|
364
429
|
async executeStream(
|
|
365
430
|
command: string,
|
|
366
431
|
args: string[] = [],
|
|
367
|
-
sessionId?: string
|
|
432
|
+
sessionId?: string,
|
|
433
|
+
background: boolean = false
|
|
368
434
|
): Promise<void> {
|
|
369
435
|
try {
|
|
370
436
|
const targetSessionId = sessionId || this.sessionId;
|
|
@@ -373,6 +439,7 @@ export class HttpClient {
|
|
|
373
439
|
body: JSON.stringify({
|
|
374
440
|
args,
|
|
375
441
|
command,
|
|
442
|
+
background,
|
|
376
443
|
sessionId: targetSessionId,
|
|
377
444
|
}),
|
|
378
445
|
headers: {
|
|
@@ -420,8 +487,7 @@ export class HttpClient {
|
|
|
420
487
|
switch (event.type) {
|
|
421
488
|
case "command_start":
|
|
422
489
|
console.log(
|
|
423
|
-
`[HTTP Client] Command started: ${
|
|
424
|
-
event.command
|
|
490
|
+
`[HTTP Client] Command started: ${event.command
|
|
425
491
|
} ${event.args?.join(" ")}`
|
|
426
492
|
);
|
|
427
493
|
this.options.onCommandStart?.(
|
|
@@ -502,7 +568,7 @@ export class HttpClient {
|
|
|
502
568
|
repoUrl,
|
|
503
569
|
sessionId: targetSessionId,
|
|
504
570
|
targetDir,
|
|
505
|
-
}),
|
|
571
|
+
} as GitCheckoutRequest),
|
|
506
572
|
headers: {
|
|
507
573
|
"Content-Type": "application/json",
|
|
508
574
|
},
|
|
@@ -593,8 +659,7 @@ export class HttpClient {
|
|
|
593
659
|
switch (event.type) {
|
|
594
660
|
case "command_start":
|
|
595
661
|
console.log(
|
|
596
|
-
`[HTTP Client] Git checkout started: ${
|
|
597
|
-
event.command
|
|
662
|
+
`[HTTP Client] Git checkout started: ${event.command
|
|
598
663
|
} ${event.args?.join(" ")}`
|
|
599
664
|
);
|
|
600
665
|
this.options.onCommandStart?.(
|
|
@@ -673,7 +738,7 @@ export class HttpClient {
|
|
|
673
738
|
path,
|
|
674
739
|
recursive,
|
|
675
740
|
sessionId: targetSessionId,
|
|
676
|
-
}),
|
|
741
|
+
} as MkdirRequest),
|
|
677
742
|
headers: {
|
|
678
743
|
"Content-Type": "application/json",
|
|
679
744
|
},
|
|
@@ -714,7 +779,7 @@ export class HttpClient {
|
|
|
714
779
|
path,
|
|
715
780
|
recursive,
|
|
716
781
|
sessionId: targetSessionId,
|
|
717
|
-
}),
|
|
782
|
+
} as MkdirRequest),
|
|
718
783
|
headers: {
|
|
719
784
|
"Content-Type": "application/json",
|
|
720
785
|
},
|
|
@@ -760,8 +825,7 @@ export class HttpClient {
|
|
|
760
825
|
switch (event.type) {
|
|
761
826
|
case "command_start":
|
|
762
827
|
console.log(
|
|
763
|
-
`[HTTP Client] Mkdir started: ${
|
|
764
|
-
event.command
|
|
828
|
+
`[HTTP Client] Mkdir started: ${event.command
|
|
765
829
|
} ${event.args?.join(" ")}`
|
|
766
830
|
);
|
|
767
831
|
this.options.onCommandStart?.(
|
|
@@ -840,7 +904,7 @@ export class HttpClient {
|
|
|
840
904
|
encoding,
|
|
841
905
|
path,
|
|
842
906
|
sessionId: targetSessionId,
|
|
843
|
-
}),
|
|
907
|
+
} as WriteFileRequest),
|
|
844
908
|
headers: {
|
|
845
909
|
"Content-Type": "application/json",
|
|
846
910
|
},
|
|
@@ -883,7 +947,7 @@ export class HttpClient {
|
|
|
883
947
|
encoding,
|
|
884
948
|
path,
|
|
885
949
|
sessionId: targetSessionId,
|
|
886
|
-
}),
|
|
950
|
+
} as WriteFileRequest),
|
|
887
951
|
headers: {
|
|
888
952
|
"Content-Type": "application/json",
|
|
889
953
|
},
|
|
@@ -1006,7 +1070,7 @@ export class HttpClient {
|
|
|
1006
1070
|
encoding,
|
|
1007
1071
|
path,
|
|
1008
1072
|
sessionId: targetSessionId,
|
|
1009
|
-
}),
|
|
1073
|
+
} as ReadFileRequest),
|
|
1010
1074
|
headers: {
|
|
1011
1075
|
"Content-Type": "application/json",
|
|
1012
1076
|
},
|
|
@@ -1047,7 +1111,7 @@ export class HttpClient {
|
|
|
1047
1111
|
encoding,
|
|
1048
1112
|
path,
|
|
1049
1113
|
sessionId: targetSessionId,
|
|
1050
|
-
}),
|
|
1114
|
+
} as ReadFileRequest),
|
|
1051
1115
|
headers: {
|
|
1052
1116
|
"Content-Type": "application/json",
|
|
1053
1117
|
},
|
|
@@ -1102,10 +1166,8 @@ export class HttpClient {
|
|
|
1102
1166
|
|
|
1103
1167
|
case "command_complete":
|
|
1104
1168
|
console.log(
|
|
1105
|
-
`[HTTP Client] Read file completed: ${
|
|
1106
|
-
|
|
1107
|
-
}, Success: ${event.success}, Content length: ${
|
|
1108
|
-
event.content?.length || 0
|
|
1169
|
+
`[HTTP Client] Read file completed: ${event.path
|
|
1170
|
+
}, Success: ${event.success}, Content length: ${event.content?.length || 0
|
|
1109
1171
|
}`
|
|
1110
1172
|
);
|
|
1111
1173
|
this.options.onCommandComplete?.(
|
|
@@ -1162,7 +1224,7 @@ export class HttpClient {
|
|
|
1162
1224
|
body: JSON.stringify({
|
|
1163
1225
|
path,
|
|
1164
1226
|
sessionId: targetSessionId,
|
|
1165
|
-
}),
|
|
1227
|
+
} as DeleteFileRequest),
|
|
1166
1228
|
headers: {
|
|
1167
1229
|
"Content-Type": "application/json",
|
|
1168
1230
|
},
|
|
@@ -1198,7 +1260,7 @@ export class HttpClient {
|
|
|
1198
1260
|
body: JSON.stringify({
|
|
1199
1261
|
path,
|
|
1200
1262
|
sessionId: targetSessionId,
|
|
1201
|
-
}),
|
|
1263
|
+
} as DeleteFileRequest),
|
|
1202
1264
|
headers: {
|
|
1203
1265
|
"Content-Type": "application/json",
|
|
1204
1266
|
},
|
|
@@ -1308,7 +1370,7 @@ export class HttpClient {
|
|
|
1308
1370
|
newPath,
|
|
1309
1371
|
oldPath,
|
|
1310
1372
|
sessionId: targetSessionId,
|
|
1311
|
-
}),
|
|
1373
|
+
} as RenameFileRequest),
|
|
1312
1374
|
headers: {
|
|
1313
1375
|
"Content-Type": "application/json",
|
|
1314
1376
|
},
|
|
@@ -1349,7 +1411,7 @@ export class HttpClient {
|
|
|
1349
1411
|
newPath,
|
|
1350
1412
|
oldPath,
|
|
1351
1413
|
sessionId: targetSessionId,
|
|
1352
|
-
}),
|
|
1414
|
+
} as RenameFileRequest),
|
|
1353
1415
|
headers: {
|
|
1354
1416
|
"Content-Type": "application/json",
|
|
1355
1417
|
},
|
|
@@ -1462,7 +1524,7 @@ export class HttpClient {
|
|
|
1462
1524
|
destinationPath,
|
|
1463
1525
|
sessionId: targetSessionId,
|
|
1464
1526
|
sourcePath,
|
|
1465
|
-
}),
|
|
1527
|
+
} as MoveFileRequest),
|
|
1466
1528
|
headers: {
|
|
1467
1529
|
"Content-Type": "application/json",
|
|
1468
1530
|
},
|
|
@@ -1503,7 +1565,7 @@ export class HttpClient {
|
|
|
1503
1565
|
destinationPath,
|
|
1504
1566
|
sessionId: targetSessionId,
|
|
1505
1567
|
sourcePath,
|
|
1506
|
-
}),
|
|
1568
|
+
} as MoveFileRequest),
|
|
1507
1569
|
headers: {
|
|
1508
1570
|
"Content-Type": "application/json",
|
|
1509
1571
|
},
|
|
@@ -1606,6 +1668,104 @@ export class HttpClient {
|
|
|
1606
1668
|
}
|
|
1607
1669
|
}
|
|
1608
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
|
+
|
|
1609
1769
|
async ping(): Promise<string> {
|
|
1610
1770
|
try {
|
|
1611
1771
|
const response = await this.doFetch(`/api/ping`, {
|
package/src/index.ts
CHANGED
|
@@ -1,129 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
`[Container] Command completed: ${command}, Success: ${success}, Exit code: ${exitCode}`
|
|
16
|
-
);
|
|
17
|
-
},
|
|
18
|
-
onCommandStart: (command, args) => {
|
|
19
|
-
console.log(`[Container] Command started: ${command} ${args.join(" ")}`);
|
|
20
|
-
},
|
|
21
|
-
onError: (error, command, args) => {
|
|
22
|
-
console.error(`[Container] Command error: ${error}`);
|
|
23
|
-
},
|
|
24
|
-
onOutput: (stream, data, command) => {
|
|
25
|
-
console.log(`[Container] [${stream}] ${data}`);
|
|
26
|
-
},
|
|
27
|
-
port: this.defaultPort,
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
envVars = {
|
|
31
|
-
MESSAGE: "I was passed in via the Sandbox class!",
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
override onStart() {
|
|
35
|
-
console.log("Sandbox successfully started");
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
override onStop() {
|
|
39
|
-
console.log("Sandbox successfully shut down");
|
|
40
|
-
if (this.client) {
|
|
41
|
-
this.client.clearSession();
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
override onError(error: unknown) {
|
|
46
|
-
console.log("Sandbox error:", error);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async exec(command: string, args: string[], options?: { stream?: boolean }) {
|
|
50
|
-
if (options?.stream) {
|
|
51
|
-
return this.client.executeStream(command, args);
|
|
52
|
-
}
|
|
53
|
-
return this.client.execute(command, args);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async gitCheckout(
|
|
57
|
-
repoUrl: string,
|
|
58
|
-
options: { branch?: string; targetDir?: string; stream?: boolean }
|
|
59
|
-
) {
|
|
60
|
-
if (options?.stream) {
|
|
61
|
-
return this.client.gitCheckoutStream(
|
|
62
|
-
repoUrl,
|
|
63
|
-
options.branch,
|
|
64
|
-
options.targetDir
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
return this.client.gitCheckout(repoUrl, options.branch, options.targetDir);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async mkdir(
|
|
71
|
-
path: string,
|
|
72
|
-
options: { recursive?: boolean; stream?: boolean }
|
|
73
|
-
) {
|
|
74
|
-
if (options?.stream) {
|
|
75
|
-
return this.client.mkdirStream(path, options.recursive);
|
|
76
|
-
}
|
|
77
|
-
return this.client.mkdir(path, options.recursive);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async writeFile(
|
|
81
|
-
path: string,
|
|
82
|
-
content: string,
|
|
83
|
-
options: { encoding?: string; stream?: boolean }
|
|
84
|
-
) {
|
|
85
|
-
if (options?.stream) {
|
|
86
|
-
return this.client.writeFileStream(path, content, options.encoding);
|
|
87
|
-
}
|
|
88
|
-
return this.client.writeFile(path, content, options.encoding);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
async deleteFile(path: string, options: { stream?: boolean }) {
|
|
92
|
-
if (options?.stream) {
|
|
93
|
-
return this.client.deleteFileStream(path);
|
|
94
|
-
}
|
|
95
|
-
return this.client.deleteFile(path);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
async renameFile(
|
|
99
|
-
oldPath: string,
|
|
100
|
-
newPath: string,
|
|
101
|
-
options: { stream?: boolean }
|
|
102
|
-
) {
|
|
103
|
-
if (options?.stream) {
|
|
104
|
-
return this.client.renameFileStream(oldPath, newPath);
|
|
105
|
-
}
|
|
106
|
-
return this.client.renameFile(oldPath, newPath);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
async moveFile(
|
|
110
|
-
sourcePath: string,
|
|
111
|
-
destinationPath: string,
|
|
112
|
-
options: { stream?: boolean }
|
|
113
|
-
) {
|
|
114
|
-
if (options?.stream) {
|
|
115
|
-
return this.client.moveFileStream(sourcePath, destinationPath);
|
|
116
|
-
}
|
|
117
|
-
return this.client.moveFile(sourcePath, destinationPath);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
async readFile(
|
|
121
|
-
path: string,
|
|
122
|
-
options: { encoding?: string; stream?: boolean }
|
|
123
|
-
) {
|
|
124
|
-
if (options?.stream) {
|
|
125
|
-
return this.client.readFileStream(path, options.encoding);
|
|
126
|
-
}
|
|
127
|
-
return this.client.readFile(path, options.encoding);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
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
|
+
|