@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/sandbox.ts
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { Container, getContainer } from "@cloudflare/containers";
|
|
2
|
+
import { HttpClient } from "./client";
|
|
3
|
+
import { isLocalhostPattern } from "./request-handler";
|
|
4
|
+
|
|
5
|
+
export function getSandbox(ns: DurableObjectNamespace<Sandbox>, id: string) {
|
|
6
|
+
const stub = getContainer(ns, id);
|
|
7
|
+
|
|
8
|
+
// Store the name on first access
|
|
9
|
+
stub.setSandboxName?.(id);
|
|
10
|
+
|
|
11
|
+
return stub;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class Sandbox<Env = unknown> extends Container<Env> {
|
|
15
|
+
sleepAfter = "3m"; // Sleep the sandbox if no requests are made in this timeframe
|
|
16
|
+
client: HttpClient;
|
|
17
|
+
private workerHostname: string | null = null;
|
|
18
|
+
private sandboxName: string | null = null;
|
|
19
|
+
|
|
20
|
+
constructor(ctx: DurableObjectState, env: Env) {
|
|
21
|
+
super(ctx, env);
|
|
22
|
+
this.client = new HttpClient({
|
|
23
|
+
onCommandComplete: (success, exitCode, _stdout, _stderr, command, _args) => {
|
|
24
|
+
console.log(
|
|
25
|
+
`[Container] Command completed: ${command}, Success: ${success}, Exit code: ${exitCode}`
|
|
26
|
+
);
|
|
27
|
+
},
|
|
28
|
+
onCommandStart: (command, args) => {
|
|
29
|
+
console.log(
|
|
30
|
+
`[Container] Command started: ${command} ${args.join(" ")}`
|
|
31
|
+
);
|
|
32
|
+
},
|
|
33
|
+
onError: (error, _command, _args) => {
|
|
34
|
+
console.error(`[Container] Command error: ${error}`);
|
|
35
|
+
},
|
|
36
|
+
onOutput: (stream, data, _command) => {
|
|
37
|
+
console.log(`[Container] [${stream}] ${data}`);
|
|
38
|
+
},
|
|
39
|
+
port: 3000, // Control plane port
|
|
40
|
+
stub: this,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Load the sandbox name from storage on initialization
|
|
44
|
+
this.ctx.blockConcurrencyWhile(async () => {
|
|
45
|
+
this.sandboxName = await this.ctx.storage.get<string>('sandboxName') || null;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// RPC method to set the sandbox name
|
|
50
|
+
async setSandboxName(name: string): Promise<void> {
|
|
51
|
+
if (!this.sandboxName) {
|
|
52
|
+
this.sandboxName = name;
|
|
53
|
+
await this.ctx.storage.put('sandboxName', name);
|
|
54
|
+
console.log(`[Sandbox] Stored sandbox name via RPC: ${name}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
override onStart() {
|
|
59
|
+
console.log("Sandbox successfully started");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
override onStop() {
|
|
63
|
+
console.log("Sandbox successfully shut down");
|
|
64
|
+
if (this.client) {
|
|
65
|
+
this.client.clearSession();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
override onError(error: unknown) {
|
|
70
|
+
console.log("Sandbox error:", error);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Override fetch to capture the hostname and route to appropriate ports
|
|
74
|
+
override async fetch(request: Request): Promise<Response> {
|
|
75
|
+
const url = new URL(request.url);
|
|
76
|
+
|
|
77
|
+
// Capture the hostname from the first request
|
|
78
|
+
if (!this.workerHostname) {
|
|
79
|
+
this.workerHostname = url.hostname;
|
|
80
|
+
console.log(`[Sandbox] Captured hostname: ${this.workerHostname}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Capture and store the sandbox name from the header if present
|
|
84
|
+
if (!this.sandboxName && request.headers.has('X-Sandbox-Name')) {
|
|
85
|
+
const name = request.headers.get('X-Sandbox-Name')!;
|
|
86
|
+
this.sandboxName = name;
|
|
87
|
+
await this.ctx.storage.put('sandboxName', name);
|
|
88
|
+
console.log(`[Sandbox] Stored sandbox name: ${this.sandboxName}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Determine which port to route to
|
|
92
|
+
const port = this.determinePort(url);
|
|
93
|
+
|
|
94
|
+
// Route to the appropriate port
|
|
95
|
+
return await this.containerFetch(request, port);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private determinePort(url: URL): number {
|
|
99
|
+
// Extract port from proxy requests (e.g., /proxy/8080/*)
|
|
100
|
+
const proxyMatch = url.pathname.match(/^\/proxy\/(\d+)/);
|
|
101
|
+
if (proxyMatch) {
|
|
102
|
+
return parseInt(proxyMatch[1]);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// All other requests go to control plane on port 3000
|
|
106
|
+
// This includes /api/* endpoints and any other control requests
|
|
107
|
+
return 3000;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async exec(command: string, args: string[], options?: { stream?: boolean; background?: boolean }) {
|
|
111
|
+
if (options?.stream) {
|
|
112
|
+
return this.client.executeStream(command, args, undefined, options?.background);
|
|
113
|
+
}
|
|
114
|
+
return this.client.execute(command, args, undefined, options?.background);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async gitCheckout(
|
|
118
|
+
repoUrl: string,
|
|
119
|
+
options: { branch?: string; targetDir?: string; stream?: boolean }
|
|
120
|
+
) {
|
|
121
|
+
if (options?.stream) {
|
|
122
|
+
return this.client.gitCheckoutStream(
|
|
123
|
+
repoUrl,
|
|
124
|
+
options.branch,
|
|
125
|
+
options.targetDir
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
return this.client.gitCheckout(repoUrl, options.branch, options.targetDir);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async mkdir(
|
|
132
|
+
path: string,
|
|
133
|
+
options: { recursive?: boolean; stream?: boolean } = {}
|
|
134
|
+
) {
|
|
135
|
+
if (options?.stream) {
|
|
136
|
+
return this.client.mkdirStream(path, options.recursive);
|
|
137
|
+
}
|
|
138
|
+
return this.client.mkdir(path, options.recursive);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async writeFile(
|
|
142
|
+
path: string,
|
|
143
|
+
content: string,
|
|
144
|
+
options: { encoding?: string; stream?: boolean } = {}
|
|
145
|
+
) {
|
|
146
|
+
if (options?.stream) {
|
|
147
|
+
return this.client.writeFileStream(path, content, options.encoding);
|
|
148
|
+
}
|
|
149
|
+
return this.client.writeFile(path, content, options.encoding);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async deleteFile(path: string, options: { stream?: boolean } = {}) {
|
|
153
|
+
if (options?.stream) {
|
|
154
|
+
return this.client.deleteFileStream(path);
|
|
155
|
+
}
|
|
156
|
+
return this.client.deleteFile(path);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async renameFile(
|
|
160
|
+
oldPath: string,
|
|
161
|
+
newPath: string,
|
|
162
|
+
options: { stream?: boolean } = {}
|
|
163
|
+
) {
|
|
164
|
+
if (options?.stream) {
|
|
165
|
+
return this.client.renameFileStream(oldPath, newPath);
|
|
166
|
+
}
|
|
167
|
+
return this.client.renameFile(oldPath, newPath);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async moveFile(
|
|
171
|
+
sourcePath: string,
|
|
172
|
+
destinationPath: string,
|
|
173
|
+
options: { stream?: boolean } = {}
|
|
174
|
+
) {
|
|
175
|
+
if (options?.stream) {
|
|
176
|
+
return this.client.moveFileStream(sourcePath, destinationPath);
|
|
177
|
+
}
|
|
178
|
+
return this.client.moveFile(sourcePath, destinationPath);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async readFile(
|
|
182
|
+
path: string,
|
|
183
|
+
options: { encoding?: string; stream?: boolean } = {}
|
|
184
|
+
) {
|
|
185
|
+
if (options?.stream) {
|
|
186
|
+
return this.client.readFileStream(path, options.encoding);
|
|
187
|
+
}
|
|
188
|
+
return this.client.readFile(path, options.encoding);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async exposePort(port: number, options?: { name?: string }) {
|
|
192
|
+
await this.client.exposePort(port, options?.name);
|
|
193
|
+
|
|
194
|
+
// We need the sandbox name to construct preview URLs
|
|
195
|
+
if (!this.sandboxName) {
|
|
196
|
+
throw new Error('Sandbox name not available. Ensure sandbox is accessed through getSandbox()');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const hostname = this.getHostname();
|
|
200
|
+
const url = this.constructPreviewUrl(port, this.sandboxName, hostname);
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
url,
|
|
204
|
+
port,
|
|
205
|
+
name: options?.name,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async unexposePort(port: number) {
|
|
210
|
+
await this.client.unexposePort(port);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async getExposedPorts() {
|
|
214
|
+
const response = await this.client.getExposedPorts();
|
|
215
|
+
|
|
216
|
+
// We need the sandbox name to construct preview URLs
|
|
217
|
+
if (!this.sandboxName) {
|
|
218
|
+
throw new Error('Sandbox name not available. Ensure sandbox is accessed through getSandbox()');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const hostname = this.getHostname();
|
|
222
|
+
|
|
223
|
+
return response.ports.map(port => ({
|
|
224
|
+
url: this.constructPreviewUrl(port.port, this.sandboxName!, hostname),
|
|
225
|
+
port: port.port,
|
|
226
|
+
name: port.name,
|
|
227
|
+
exposedAt: port.exposedAt,
|
|
228
|
+
}));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private getHostname(): string {
|
|
232
|
+
// Use the captured hostname or fall back to localhost for development
|
|
233
|
+
return this.workerHostname || "localhost:8787";
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private constructPreviewUrl(port: number, sandboxId: string, hostname: string): string {
|
|
237
|
+
// Check if this is a localhost pattern
|
|
238
|
+
const isLocalhost = isLocalhostPattern(hostname);
|
|
239
|
+
|
|
240
|
+
if (isLocalhost) {
|
|
241
|
+
// For local development, we need to use a different approach
|
|
242
|
+
// Since subdomains don't work with localhost, we'll use the base URL
|
|
243
|
+
// with a note that the user needs to handle routing differently
|
|
244
|
+
return `http://${hostname}/preview/${port}/${sandboxId}`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// For all other domains (workers.dev, custom domains, etc.)
|
|
248
|
+
// Use subdomain-based routing pattern
|
|
249
|
+
const protocol = hostname.includes(":") ? "http" : "https";
|
|
250
|
+
return `${protocol}://${port}-${sandboxId}.${hostname}`;
|
|
251
|
+
}
|
|
252
|
+
}
|