@cloudflare/sandbox 0.0.0-ee8c772 → 0.0.0-f5fcd52

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,258 @@
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
+ // RPC method to set environment variables
59
+ async setEnvVars(envVars: Record<string, string>): Promise<void> {
60
+ this.envVars = { ...this.envVars, ...envVars };
61
+ console.log(`[Sandbox] Updated environment variables`);
62
+ }
63
+
64
+ override onStart() {
65
+ console.log("Sandbox successfully started");
66
+ }
67
+
68
+ override onStop() {
69
+ console.log("Sandbox successfully shut down");
70
+ if (this.client) {
71
+ this.client.clearSession();
72
+ }
73
+ }
74
+
75
+ override onError(error: unknown) {
76
+ console.log("Sandbox error:", error);
77
+ }
78
+
79
+ // Override fetch to capture the hostname and route to appropriate ports
80
+ override async fetch(request: Request): Promise<Response> {
81
+ const url = new URL(request.url);
82
+
83
+ // Capture the hostname from the first request
84
+ if (!this.workerHostname) {
85
+ this.workerHostname = url.hostname;
86
+ console.log(`[Sandbox] Captured hostname: ${this.workerHostname}`);
87
+ }
88
+
89
+ // Capture and store the sandbox name from the header if present
90
+ if (!this.sandboxName && request.headers.has('X-Sandbox-Name')) {
91
+ const name = request.headers.get('X-Sandbox-Name')!;
92
+ this.sandboxName = name;
93
+ await this.ctx.storage.put('sandboxName', name);
94
+ console.log(`[Sandbox] Stored sandbox name: ${this.sandboxName}`);
95
+ }
96
+
97
+ // Determine which port to route to
98
+ const port = this.determinePort(url);
99
+
100
+ // Route to the appropriate port
101
+ return await this.containerFetch(request, port);
102
+ }
103
+
104
+ private determinePort(url: URL): number {
105
+ // Extract port from proxy requests (e.g., /proxy/8080/*)
106
+ const proxyMatch = url.pathname.match(/^\/proxy\/(\d+)/);
107
+ if (proxyMatch) {
108
+ return parseInt(proxyMatch[1]);
109
+ }
110
+
111
+ // All other requests go to control plane on port 3000
112
+ // This includes /api/* endpoints and any other control requests
113
+ return 3000;
114
+ }
115
+
116
+ async exec(command: string, args: string[], options?: { stream?: boolean; background?: boolean }) {
117
+ if (options?.stream) {
118
+ return this.client.executeStream(command, args, undefined, options?.background);
119
+ }
120
+ return this.client.execute(command, args, undefined, options?.background);
121
+ }
122
+
123
+ async gitCheckout(
124
+ repoUrl: string,
125
+ options: { branch?: string; targetDir?: string; stream?: boolean }
126
+ ) {
127
+ if (options?.stream) {
128
+ return this.client.gitCheckoutStream(
129
+ repoUrl,
130
+ options.branch,
131
+ options.targetDir
132
+ );
133
+ }
134
+ return this.client.gitCheckout(repoUrl, options.branch, options.targetDir);
135
+ }
136
+
137
+ async mkdir(
138
+ path: string,
139
+ options: { recursive?: boolean; stream?: boolean } = {}
140
+ ) {
141
+ if (options?.stream) {
142
+ return this.client.mkdirStream(path, options.recursive);
143
+ }
144
+ return this.client.mkdir(path, options.recursive);
145
+ }
146
+
147
+ async writeFile(
148
+ path: string,
149
+ content: string,
150
+ options: { encoding?: string; stream?: boolean } = {}
151
+ ) {
152
+ if (options?.stream) {
153
+ return this.client.writeFileStream(path, content, options.encoding);
154
+ }
155
+ return this.client.writeFile(path, content, options.encoding);
156
+ }
157
+
158
+ async deleteFile(path: string, options: { stream?: boolean } = {}) {
159
+ if (options?.stream) {
160
+ return this.client.deleteFileStream(path);
161
+ }
162
+ return this.client.deleteFile(path);
163
+ }
164
+
165
+ async renameFile(
166
+ oldPath: string,
167
+ newPath: string,
168
+ options: { stream?: boolean } = {}
169
+ ) {
170
+ if (options?.stream) {
171
+ return this.client.renameFileStream(oldPath, newPath);
172
+ }
173
+ return this.client.renameFile(oldPath, newPath);
174
+ }
175
+
176
+ async moveFile(
177
+ sourcePath: string,
178
+ destinationPath: string,
179
+ options: { stream?: boolean } = {}
180
+ ) {
181
+ if (options?.stream) {
182
+ return this.client.moveFileStream(sourcePath, destinationPath);
183
+ }
184
+ return this.client.moveFile(sourcePath, destinationPath);
185
+ }
186
+
187
+ async readFile(
188
+ path: string,
189
+ options: { encoding?: string; stream?: boolean } = {}
190
+ ) {
191
+ if (options?.stream) {
192
+ return this.client.readFileStream(path, options.encoding);
193
+ }
194
+ return this.client.readFile(path, options.encoding);
195
+ }
196
+
197
+ async exposePort(port: number, options?: { name?: string }) {
198
+ await this.client.exposePort(port, options?.name);
199
+
200
+ // We need the sandbox name to construct preview URLs
201
+ if (!this.sandboxName) {
202
+ throw new Error('Sandbox name not available. Ensure sandbox is accessed through getSandbox()');
203
+ }
204
+
205
+ const hostname = this.getHostname();
206
+ const url = this.constructPreviewUrl(port, this.sandboxName, hostname);
207
+
208
+ return {
209
+ url,
210
+ port,
211
+ name: options?.name,
212
+ };
213
+ }
214
+
215
+ async unexposePort(port: number) {
216
+ await this.client.unexposePort(port);
217
+ }
218
+
219
+ async getExposedPorts() {
220
+ const response = await this.client.getExposedPorts();
221
+
222
+ // We need the sandbox name to construct preview URLs
223
+ if (!this.sandboxName) {
224
+ throw new Error('Sandbox name not available. Ensure sandbox is accessed through getSandbox()');
225
+ }
226
+
227
+ const hostname = this.getHostname();
228
+
229
+ return response.ports.map(port => ({
230
+ url: this.constructPreviewUrl(port.port, this.sandboxName!, hostname),
231
+ port: port.port,
232
+ name: port.name,
233
+ exposedAt: port.exposedAt,
234
+ }));
235
+ }
236
+
237
+ private getHostname(): string {
238
+ // Use the captured hostname or fall back to localhost for development
239
+ return this.workerHostname || "localhost:8787";
240
+ }
241
+
242
+ private constructPreviewUrl(port: number, sandboxId: string, hostname: string): string {
243
+ // Check if this is a localhost pattern
244
+ const isLocalhost = isLocalhostPattern(hostname);
245
+
246
+ if (isLocalhost) {
247
+ // For local development, we need to use a different approach
248
+ // Since subdomains don't work with localhost, we'll use the base URL
249
+ // with a note that the user needs to handle routing differently
250
+ return `http://${hostname}/preview/${port}/${sandboxId}`;
251
+ }
252
+
253
+ // For all other domains (workers.dev, custom domains, etc.)
254
+ // Use subdomain-based routing pattern
255
+ const protocol = hostname.includes(":") ? "http" : "https";
256
+ return `${protocol}://${port}-${sandboxId}.${hostname}`;
257
+ }
258
+ }