@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/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
+ }