@cloudflare/sandbox 0.1.1 → 0.1.3
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 +14 -0
- package/Dockerfile +4 -5
- package/README.md +562 -0
- package/dist/{chunk-ISFOIYQC.js → chunk-YZRU4CKO.js} +3 -1
- package/dist/chunk-YZRU4CKO.js.map +1 -0
- package/dist/{client-Da-mLX4p.d.ts → client-BAuciZya.d.ts} +1 -0
- package/dist/client.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/request-handler.d.ts +1 -1
- package/dist/request-handler.js +1 -1
- package/dist/sandbox.d.ts +1 -1
- package/dist/sandbox.js +1 -1
- package/package.json +4 -3
- package/src/sandbox.ts +1 -0
- package/dist/chunk-ISFOIYQC.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @cloudflare/sandbox
|
|
2
2
|
|
|
3
|
+
## 0.1.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#32](https://github.com/cloudflare/sandbox-sdk/pull/32) [`1a42464`](https://github.com/cloudflare/sandbox-sdk/commit/1a4246479369c5d0160705caf192aa1816540d52) Thanks [@ghostwriternr](https://github.com/ghostwriternr)! - Bring back package README
|
|
8
|
+
|
|
9
|
+
## 0.1.2
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- [#30](https://github.com/cloudflare/sandbox-sdk/pull/30) [`30e5c25`](https://github.com/cloudflare/sandbox-sdk/commit/30e5c25cf7d4b07f9049724206c531e2d5d29d5c) Thanks [@ghostwriternr](https://github.com/ghostwriternr)! - Remove actions timeout
|
|
14
|
+
|
|
15
|
+
- [#29](https://github.com/cloudflare/sandbox-sdk/pull/29) [`d78508f`](https://github.com/cloudflare/sandbox-sdk/commit/d78508f7287a59e0423edd2999c2c83e9e34ccfd) Thanks [@ghostwriternr](https://github.com/ghostwriternr)! - Create multi-platform Docker image and switch to Cloudflare official repo
|
|
16
|
+
|
|
3
17
|
## 0.1.1
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/Dockerfile
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# Sandbox base image with development tools, Python, Node.js, and Bun
|
|
2
|
+
FROM oven/bun:latest AS bun-source
|
|
2
3
|
FROM ubuntu:22.04
|
|
3
4
|
|
|
4
5
|
# Prevent interactive prompts during package installation
|
|
@@ -50,11 +51,9 @@ RUN apt-get update && apt-get install -y ca-certificates curl gnupg \
|
|
|
50
51
|
&& apt-get install -y nodejs \
|
|
51
52
|
&& rm -rf /var/lib/apt/lists/*
|
|
52
53
|
|
|
53
|
-
# Install Bun
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
&& mv /root/.bun/bin/bunx /usr/local/bin/bunx \
|
|
57
|
-
&& rm -rf /root/.bun
|
|
54
|
+
# Install Bun from official image (avoids architecture compatibility issues)
|
|
55
|
+
COPY --from=bun-source /usr/local/bin/bun /usr/local/bin/bun
|
|
56
|
+
COPY --from=bun-source /usr/local/bin/bunx /usr/local/bin/bunx
|
|
58
57
|
|
|
59
58
|
# Install global npm packages as root
|
|
60
59
|
RUN npm install -g yarn pnpm
|
package/README.md
ADDED
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<h1>📦 Cloudflare Sandbox SDK</h1>
|
|
3
|
+
<h3><strong>Run sandboxed code environments on Cloudflare's edge network</strong></h3>
|
|
4
|
+
<p>
|
|
5
|
+
<a href="https://www.npmjs.com/package/@cloudflare/sandbox"><img src="https://img.shields.io/npm/v/@cloudflare/sandbox.svg" alt="npm version"></a>
|
|
6
|
+
<a href="https://github.com/cloudflare/sandbox-sdk"><img src="https://img.shields.io/badge/status-experimental-orange.svg" alt="status"></a>
|
|
7
|
+
</p>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
|
11
|
+
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
|
12
|
+
|
|
13
|
+
- [✨ Overview](#overview)
|
|
14
|
+
- [🎯 Features](#features)
|
|
15
|
+
- [🚀 Quick Start](#quick-start)
|
|
16
|
+
- [Installation](#installation)
|
|
17
|
+
- [Basic Setup](#basic-setup)
|
|
18
|
+
- [📚 API Reference](#api-reference)
|
|
19
|
+
- [Core Methods](#core-methods)
|
|
20
|
+
- [🌐 Port Forwarding](#port-forwarding)
|
|
21
|
+
- [Utility Methods](#utility-methods)
|
|
22
|
+
- [💡 Examples](#examples)
|
|
23
|
+
- [Run a Node.js App](#run-a-nodejs-app)
|
|
24
|
+
- [Build and Test Code](#build-and-test-code)
|
|
25
|
+
- [Interactive Development Environment](#interactive-development-environment)
|
|
26
|
+
- [Expose Services with Preview URLs](#expose-services-with-preview-urls)
|
|
27
|
+
- [🏗️ Architecture](#architecture)
|
|
28
|
+
- [🛠️ Advanced Usage](#advanced-usage)
|
|
29
|
+
- [AsyncIterable Streaming Support](#asynciterable-streaming-support)
|
|
30
|
+
- [Session Management](#session-management)
|
|
31
|
+
- [🔍 Debugging](#debugging)
|
|
32
|
+
- [🚧 Known Limitations](#known-limitations)
|
|
33
|
+
- [🤝 Contributing](#contributing)
|
|
34
|
+
- [📄 License](#license)
|
|
35
|
+
- [🙌 Acknowledgments](#acknowledgments)
|
|
36
|
+
|
|
37
|
+
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
|
38
|
+
|
|
39
|
+
<h2 id="overview">✨ Overview</h2>
|
|
40
|
+
|
|
41
|
+
The Cloudflare Sandbox SDK enables you to run isolated code environments directly on Cloudflare's edge network using Durable Objects and the Cloudflare Containers. Execute commands, manage files, run services, and expose them via public URLs - all within secure, sandboxed containers.
|
|
42
|
+
|
|
43
|
+
<h2 id="features">🎯 Features</h2>
|
|
44
|
+
|
|
45
|
+
- **🔒 Secure Isolation**: Each sandbox runs in its own container with full process isolation
|
|
46
|
+
- **⚡ Edge-Native**: Runs on Cloudflare's global network for low latency worldwide
|
|
47
|
+
- **📁 File System Access**: Read, write, and manage files within the sandbox
|
|
48
|
+
- **🔧 Command Execution**: Run any command or process inside the container
|
|
49
|
+
- **🌐 Preview URLs**: Expose services running in your sandbox via public URLs
|
|
50
|
+
- **🔄 Git Integration**: Clone repositories directly into sandboxes
|
|
51
|
+
- **🚀 Streaming Support**: Real-time output streaming for long-running commands
|
|
52
|
+
- **🎮 Session Management**: Maintain state across multiple operations
|
|
53
|
+
|
|
54
|
+
<h2 id="quick-start">🚀 Quick Start</h2>
|
|
55
|
+
|
|
56
|
+
### Installation
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npm install @cloudflare/sandbox
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Basic Setup
|
|
63
|
+
|
|
64
|
+
1. **Create a Dockerfile** (temporary requirement, will be removed in future releases):
|
|
65
|
+
|
|
66
|
+
```dockerfile
|
|
67
|
+
FROM docker.io/cloudflare/sandbox:0.1.3
|
|
68
|
+
|
|
69
|
+
EXPOSE 3000
|
|
70
|
+
|
|
71
|
+
# Run the same command as the original image
|
|
72
|
+
CMD ["bun", "index.ts"]
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
2. **Configure wrangler.json**:
|
|
76
|
+
|
|
77
|
+
> **NOTE**: In an upcoming release, this step will be removed entirely and you can reference a single Docker image published by us directly in your wrangler configuration below.
|
|
78
|
+
|
|
79
|
+
```jsonc
|
|
80
|
+
{
|
|
81
|
+
// ...
|
|
82
|
+
"containers": [
|
|
83
|
+
{
|
|
84
|
+
"class_name": "Sandbox",
|
|
85
|
+
"image": "./Dockerfile",
|
|
86
|
+
"max_instances": 1
|
|
87
|
+
}
|
|
88
|
+
],
|
|
89
|
+
"durable_objects": {
|
|
90
|
+
"bindings": [
|
|
91
|
+
{
|
|
92
|
+
"class_name": "Sandbox",
|
|
93
|
+
"name": "Sandbox"
|
|
94
|
+
}
|
|
95
|
+
]
|
|
96
|
+
},
|
|
97
|
+
"migrations": [
|
|
98
|
+
{
|
|
99
|
+
"new_sqlite_classes": ["Sandbox"],
|
|
100
|
+
"tag": "v1"
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
3. **Create your Worker**:
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
import { getSandbox } from "@cloudflare/sandbox";
|
|
110
|
+
|
|
111
|
+
// Export the Sandbox class in your Worker
|
|
112
|
+
export { Sandbox } from "@cloudflare/sandbox";
|
|
113
|
+
|
|
114
|
+
export default {
|
|
115
|
+
async fetch(request: Request, env: Env) {
|
|
116
|
+
const sandbox = getSandbox(env.Sandbox, "my-sandbox");
|
|
117
|
+
|
|
118
|
+
// Execute a command
|
|
119
|
+
const result = await sandbox.exec("echo 'Hello from the edge!'");
|
|
120
|
+
return new Response(result.stdout);
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
<h2 id="api-reference">📚 API Reference</h2>
|
|
126
|
+
|
|
127
|
+
### Core Methods
|
|
128
|
+
|
|
129
|
+
#### Command Execution
|
|
130
|
+
|
|
131
|
+
**`exec(command, options?)`** - Enhanced command execution that always returns results
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
// Simple execution
|
|
135
|
+
const result = await sandbox.exec("npm install express");
|
|
136
|
+
console.log(result.stdout, result.exitCode);
|
|
137
|
+
|
|
138
|
+
// With streaming callbacks
|
|
139
|
+
const result = await sandbox.exec("npm run build", {
|
|
140
|
+
stream: true,
|
|
141
|
+
onOutput: (stream, data) => console.log(`[${stream}] ${data}`)
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**`execStream(command, options?)`** - Dedicated streaming method returning SSE stream
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
import { parseSSEStream, type ExecEvent } from '@cloudflare/sandbox';
|
|
149
|
+
|
|
150
|
+
const stream = await sandbox.execStream("npm run test");
|
|
151
|
+
for await (const event of parseSSEStream<ExecEvent>(stream)) {
|
|
152
|
+
switch (event.type) {
|
|
153
|
+
case 'stdout':
|
|
154
|
+
console.log(`Test output: ${event.data}`);
|
|
155
|
+
break;
|
|
156
|
+
case 'complete':
|
|
157
|
+
console.log(`Tests ${event.exitCode === 0 ? 'passed' : 'failed'}`);
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**`startProcess(command, options?)`** - Start background processes with lifecycle management
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
const process = await sandbox.startProcess("node server.js");
|
|
167
|
+
console.log(`Started process ${process.id} with PID ${process.pid}`);
|
|
168
|
+
|
|
169
|
+
// Monitor the process
|
|
170
|
+
const logStream = await sandbox.streamProcessLogs(process.id);
|
|
171
|
+
for await (const log of parseSSEStream<LogEvent>(logStream)) {
|
|
172
|
+
console.log(`Server: ${log.data}`);
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
#### `writeFile(path, content, options?)`
|
|
177
|
+
|
|
178
|
+
Write content to a file.
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
await sandbox.writeFile("/app.js", "console.log('Hello!');");
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
#### `readFile(path, options?)`
|
|
185
|
+
|
|
186
|
+
Read a file from the sandbox.
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
const file = await sandbox.readFile("/package.json");
|
|
190
|
+
console.log(file.content);
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
#### `gitCheckout(repoUrl, options?)`
|
|
194
|
+
|
|
195
|
+
Clone a git repository.
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
await sandbox.gitCheckout("https://github.com/user/repo", {
|
|
199
|
+
branch: "main",
|
|
200
|
+
targetDir: "my-project",
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
#### `setEnvVars(envVars)`
|
|
205
|
+
|
|
206
|
+
Set environment variables dynamically in the sandbox.
|
|
207
|
+
|
|
208
|
+
> **Important**: This method must be called immediately after `getSandbox()` and before any other operations. Once a sandbox instance starts up, environment variables cannot be changed
|
|
209
|
+
for that instance.
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
const sandbox = getSandbox(env.Sandbox, "my-sandbox");
|
|
213
|
+
|
|
214
|
+
// Set environment variables FIRST, before any other operations
|
|
215
|
+
await sandbox.setEnvVars({
|
|
216
|
+
NODE_ENV: "production",
|
|
217
|
+
API_KEY: "your-api-key",
|
|
218
|
+
DATABASE_URL: "postgresql://localhost:5432/mydb"
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Now you can run commands - environment variables are available
|
|
222
|
+
const result = await sandbox.exec("echo $NODE_ENV");
|
|
223
|
+
console.log(result.stdout); // "production"
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
#### Process Management
|
|
227
|
+
|
|
228
|
+
- `listProcesses()` - List all running processes
|
|
229
|
+
- `getProcess(id)` - Get detailed process status
|
|
230
|
+
- `killProcess(id, signal?)` - Terminate specific processes
|
|
231
|
+
- `killAllProcesses()` - Kill all processes
|
|
232
|
+
- `streamProcessLogs(id, options?)` - Stream logs from running processes
|
|
233
|
+
- `getProcessLogs(id)` - Get accumulated process output
|
|
234
|
+
|
|
235
|
+
#### File System Methods
|
|
236
|
+
|
|
237
|
+
- `writeFile(path, content, options?)` - Write content to a file
|
|
238
|
+
- `readFile(path, options?)` - Read a file from the sandbox
|
|
239
|
+
- `mkdir(path, options?)` - Create a directory
|
|
240
|
+
- `deleteFile(path)` - Delete a file
|
|
241
|
+
- `renameFile(oldPath, newPath)` - Rename a file
|
|
242
|
+
- `moveFile(sourcePath, destinationPath)` - Move a file
|
|
243
|
+
- `gitCheckout(repoUrl, options?)` - Clone git repositories
|
|
244
|
+
|
|
245
|
+
#### Network Methods
|
|
246
|
+
|
|
247
|
+
- `exposePort(port, options?)` - Expose a port and get a public URL
|
|
248
|
+
- `unexposePort(port)` - Remove port exposure
|
|
249
|
+
- `getExposedPorts()` - List all exposed ports with their URLs
|
|
250
|
+
|
|
251
|
+
<h2 id="port-forwarding">🌐 Port Forwarding</h2>
|
|
252
|
+
|
|
253
|
+
The SDK automatically handles preview URL routing for exposed ports. Just add one line to your worker:
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
import { proxyToSandbox, getSandbox } from "@cloudflare/sandbox";
|
|
257
|
+
|
|
258
|
+
export default {
|
|
259
|
+
async fetch(request, env) {
|
|
260
|
+
// Route requests to exposed container ports via their preview URLs
|
|
261
|
+
const proxyResponse = await proxyToSandbox(request, env);
|
|
262
|
+
if (proxyResponse) return proxyResponse;
|
|
263
|
+
|
|
264
|
+
// Your custom routes here
|
|
265
|
+
// ...
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
When you expose a port, the SDK returns a preview URL that automatically routes to your service:
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
const preview = await sandbox.exposePort(3000);
|
|
274
|
+
console.log(preview.url); // https://3000-sandbox-id.your-worker.dev
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
The SDK handles:
|
|
278
|
+
|
|
279
|
+
- Subdomain routing (`3000-sandbox-id.domain.com`) for both production and local development
|
|
280
|
+
- All localhost variants (127.0.0.1, ::1, etc.)
|
|
281
|
+
- Request forwarding with proper headers
|
|
282
|
+
|
|
283
|
+
> **Important for Local Development**: When developing locally with `wrangler dev`, you must explicitly expose ports in your Dockerfile using the `EXPOSE` instruction. This is **only required for local development** - in production, all container ports are automatically accessible.
|
|
284
|
+
|
|
285
|
+
```dockerfile
|
|
286
|
+
# In your Dockerfile (only needed for local dev)
|
|
287
|
+
FROM oven/bun:latest
|
|
288
|
+
|
|
289
|
+
# Expose the ports you'll be using
|
|
290
|
+
EXPOSE 3000 # For a web server
|
|
291
|
+
EXPOSE 8080 # For an API server
|
|
292
|
+
EXPOSE 3001 # For any additional services
|
|
293
|
+
|
|
294
|
+
# Your container setup...
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Without the `EXPOSE` instruction in local development, you'll see this error:
|
|
298
|
+
|
|
299
|
+
```
|
|
300
|
+
connect(): Connection refused: container port not found. Make sure you exposed the port in your container definition.
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
For more details, see the [Cloudflare Containers local development guide](https://developers.cloudflare.com/containers/local-dev/#exposing-ports).
|
|
304
|
+
|
|
305
|
+
### Utility Methods
|
|
306
|
+
|
|
307
|
+
- `ping()` - Health check for the sandbox
|
|
308
|
+
- `containerFetch(request)` - Direct container communication
|
|
309
|
+
|
|
310
|
+
<h2 id="examples">💡 Examples</h2>
|
|
311
|
+
|
|
312
|
+
### Run a Node.js App
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
const sandbox = getSandbox(env.Sandbox, "node-app");
|
|
316
|
+
|
|
317
|
+
// Write a simple Express server
|
|
318
|
+
await sandbox.writeFile(
|
|
319
|
+
"/app.js",
|
|
320
|
+
`
|
|
321
|
+
const express = require('express');
|
|
322
|
+
const app = express();
|
|
323
|
+
|
|
324
|
+
app.get('/', (req, res) => {
|
|
325
|
+
res.json({ message: 'Hello from Cloudflare!' });
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
app.listen(3000);
|
|
329
|
+
`
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
// Install dependencies and start the server
|
|
333
|
+
await sandbox.exec("npm init -y");
|
|
334
|
+
await sandbox.exec("npm install express");
|
|
335
|
+
const server = await sandbox.startProcess("node app.js");
|
|
336
|
+
|
|
337
|
+
// Expose it to the internet
|
|
338
|
+
const preview = await sandbox.exposePort(3000);
|
|
339
|
+
console.log(`API available at: ${preview.url}`);
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Build and Test Code
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
const sandbox = getSandbox(env.Sandbox, "test-env");
|
|
346
|
+
|
|
347
|
+
// Clone a repository
|
|
348
|
+
await sandbox.gitCheckout("https://github.com/user/project");
|
|
349
|
+
|
|
350
|
+
// Run tests
|
|
351
|
+
const testResult = await sandbox.exec("npm test");
|
|
352
|
+
|
|
353
|
+
// Build the project
|
|
354
|
+
const buildResult = await sandbox.exec("npm run build");
|
|
355
|
+
|
|
356
|
+
return new Response(
|
|
357
|
+
JSON.stringify({
|
|
358
|
+
tests: testResult.exitCode === 0 ? "passed" : "failed",
|
|
359
|
+
build: buildResult.exitCode === 0 ? "success" : "failed",
|
|
360
|
+
output: testResult.stdout,
|
|
361
|
+
})
|
|
362
|
+
);
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### Interactive Development Environment
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
// Create a development sandbox with hot reload
|
|
369
|
+
const sandbox = getSandbox(env.Sandbox, "dev-env");
|
|
370
|
+
|
|
371
|
+
// Set up the project
|
|
372
|
+
await sandbox.gitCheckout("https://github.com/user/my-app");
|
|
373
|
+
await sandbox.exec("npm install");
|
|
374
|
+
|
|
375
|
+
// Start dev server
|
|
376
|
+
const devServer = await sandbox.startProcess("npm run dev");
|
|
377
|
+
|
|
378
|
+
// Expose the dev server
|
|
379
|
+
const preview = await sandbox.exposePort(3000, { name: "dev-server" });
|
|
380
|
+
|
|
381
|
+
// Make changes and see them live!
|
|
382
|
+
await sandbox.writeFile("/src/App.jsx", updatedCode);
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Expose Services with Preview URLs
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
// Create and start a web server
|
|
389
|
+
await sandbox.writeFile(
|
|
390
|
+
"/server.js",
|
|
391
|
+
`Bun.serve({
|
|
392
|
+
port: 8080,
|
|
393
|
+
fetch(req) {
|
|
394
|
+
return new Response("Hello from sandbox!");
|
|
395
|
+
}
|
|
396
|
+
});`
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
const server = await sandbox.startProcess("bun run /server.js");
|
|
400
|
+
|
|
401
|
+
// Expose the port - returns a public URL
|
|
402
|
+
const preview = await sandbox.exposePort(8080);
|
|
403
|
+
console.log(`Service available at: ${preview.url}`);
|
|
404
|
+
|
|
405
|
+
// Note: Your Worker needs to handle preview URL routing.
|
|
406
|
+
// See the example in examples/basic/src/index.ts for the routing implementation.
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
<h2 id="architecture">🏗️ Architecture</h2>
|
|
410
|
+
|
|
411
|
+
The SDK leverages Cloudflare's infrastructure:
|
|
412
|
+
|
|
413
|
+
- **Durable Objects**: Manages sandbox lifecycle and state
|
|
414
|
+
- **Containers**: Provides isolated execution environments
|
|
415
|
+
- **Workers**: Handles HTTP routing and API interface
|
|
416
|
+
- **Edge Network**: Enables global distribution and low latency
|
|
417
|
+
|
|
418
|
+
<h2 id="advanced-usage">🛠️ Advanced Usage</h2>
|
|
419
|
+
|
|
420
|
+
### AsyncIterable Streaming Support
|
|
421
|
+
|
|
422
|
+
The SDK provides powerful streaming capabilities with typed AsyncIterable support:
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
import { parseSSEStream, type ExecEvent } from '@cloudflare/sandbox';
|
|
426
|
+
|
|
427
|
+
// Stream command execution
|
|
428
|
+
const stream = await sandbox.execStream('npm run build');
|
|
429
|
+
for await (const event of parseSSEStream<ExecEvent>(stream)) {
|
|
430
|
+
switch (event.type) {
|
|
431
|
+
case 'start':
|
|
432
|
+
console.log(`Build started: ${event.command}`);
|
|
433
|
+
break;
|
|
434
|
+
case 'stdout':
|
|
435
|
+
console.log(`Build: ${event.data}`);
|
|
436
|
+
break;
|
|
437
|
+
case 'complete':
|
|
438
|
+
console.log(`Exit code: ${event.exitCode}`);
|
|
439
|
+
break;
|
|
440
|
+
case 'error':
|
|
441
|
+
console.error(`Error: ${event.error}`);
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
#### Streaming Utilities
|
|
448
|
+
|
|
449
|
+
The SDK exports utilities for working with Server-Sent Event streams:
|
|
450
|
+
|
|
451
|
+
- **`parseSSEStream<T>(stream)`** - Convert ReadableStream to typed AsyncIterable
|
|
452
|
+
- **`responseToAsyncIterable<T>(response)`** - Convert SSE Response to AsyncIterable
|
|
453
|
+
- **`asyncIterableToSSEStream<T>(iterable)`** - Convert AsyncIterable back to SSE stream
|
|
454
|
+
|
|
455
|
+
#### Advanced Streaming Examples
|
|
456
|
+
|
|
457
|
+
**CI/CD Build System:**
|
|
458
|
+
```typescript
|
|
459
|
+
export async function runBuild(env: Env, buildId: string) {
|
|
460
|
+
const sandbox = getSandbox(env.SANDBOX, buildId);
|
|
461
|
+
const stream = await sandbox.execStream('npm run build');
|
|
462
|
+
|
|
463
|
+
for await (const event of parseSSEStream<ExecEvent>(stream)) {
|
|
464
|
+
switch (event.type) {
|
|
465
|
+
case 'start':
|
|
466
|
+
await env.BUILDS.put(buildId, { status: 'running' });
|
|
467
|
+
break;
|
|
468
|
+
case 'complete':
|
|
469
|
+
await env.BUILDS.put(buildId, {
|
|
470
|
+
status: event.exitCode === 0 ? 'success' : 'failed',
|
|
471
|
+
exitCode: event.exitCode
|
|
472
|
+
});
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
**System Monitoring:**
|
|
480
|
+
```typescript
|
|
481
|
+
const monitor = await sandbox.startProcess('tail -f /var/log/system.log');
|
|
482
|
+
const logStream = await sandbox.streamProcessLogs(monitor.id);
|
|
483
|
+
|
|
484
|
+
for await (const log of parseSSEStream<LogEvent>(logStream)) {
|
|
485
|
+
if (log.type === 'stdout' && log.data.includes('ERROR')) {
|
|
486
|
+
await env.ALERTS.send({
|
|
487
|
+
severity: 'high',
|
|
488
|
+
message: log.data,
|
|
489
|
+
timestamp: log.timestamp
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
### Session Management
|
|
496
|
+
|
|
497
|
+
Maintain context across commands:
|
|
498
|
+
|
|
499
|
+
```typescript
|
|
500
|
+
const sessionId = crypto.randomUUID();
|
|
501
|
+
|
|
502
|
+
// Commands in the same session share working directory
|
|
503
|
+
await sandbox.exec("cd /app", { sessionId });
|
|
504
|
+
await sandbox.exec("npm install", { sessionId });
|
|
505
|
+
const app = await sandbox.startProcess("npm start", { sessionId });
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
<h2 id="debugging">🔍 Debugging</h2>
|
|
509
|
+
|
|
510
|
+
Enable verbose logging:
|
|
511
|
+
|
|
512
|
+
```typescript
|
|
513
|
+
const sandbox = getSandbox(env.Sandbox, "debug-sandbox");
|
|
514
|
+
sandbox.client.onCommandStart = (cmd, args) =>
|
|
515
|
+
console.log(`Starting: ${cmd} ${args.join(" ")}`);
|
|
516
|
+
sandbox.client.onOutput = (stream, data) => console.log(`[${stream}] ${data}`);
|
|
517
|
+
sandbox.client.onCommandComplete = (success, code) =>
|
|
518
|
+
console.log(`Completed: ${success} (${code})`);
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
<h2 id="known-limitations">🚧 Known Limitations</h2>
|
|
522
|
+
|
|
523
|
+
- Maximum container runtime is limited by Durable Object constraints
|
|
524
|
+
- WebSocket support for preview URLs coming soon
|
|
525
|
+
- Some system calls may be restricted in the container environment
|
|
526
|
+
|
|
527
|
+
<h2 id="contributing">🤝 Contributing</h2>
|
|
528
|
+
|
|
529
|
+
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
|
|
530
|
+
|
|
531
|
+
```bash
|
|
532
|
+
# Clone the repo
|
|
533
|
+
git clone https://github.com/cloudflare/sandbox-sdk
|
|
534
|
+
cd sandbox-sdk
|
|
535
|
+
|
|
536
|
+
# Install dependencies
|
|
537
|
+
npm install
|
|
538
|
+
|
|
539
|
+
# Run tests
|
|
540
|
+
npm test
|
|
541
|
+
|
|
542
|
+
# Build the project
|
|
543
|
+
npm run build
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
<h2 id="license">📄 License</h2>
|
|
547
|
+
|
|
548
|
+
[MIT License](LICENSE)
|
|
549
|
+
|
|
550
|
+
<h2 id="acknowledgments">🙌 Acknowledgments</h2>
|
|
551
|
+
|
|
552
|
+
Built with ❤️ by the Cloudflare team. Special thanks to all early adopters and contributors.
|
|
553
|
+
|
|
554
|
+
---
|
|
555
|
+
|
|
556
|
+
<div align="center">
|
|
557
|
+
<p>
|
|
558
|
+
<a href="https://github.com/cloudflare/sandbox-sdk/issues">Issues</a> •
|
|
559
|
+
<a href="https://discord.gg/cloudflaredev">Discord</a> •
|
|
560
|
+
<a href="https://twitter.com/CloudflareDev">Twitter</a>
|
|
561
|
+
</p>
|
|
562
|
+
</div>
|
|
@@ -121,6 +121,8 @@ function getSandbox(ns, id) {
|
|
|
121
121
|
return stub;
|
|
122
122
|
}
|
|
123
123
|
var Sandbox = class extends Container {
|
|
124
|
+
defaultPort = 3e3;
|
|
125
|
+
// Default port for the container's Bun server
|
|
124
126
|
sleepAfter = "3m";
|
|
125
127
|
// Sleep the sandbox if no requests are made in this timeframe
|
|
126
128
|
client;
|
|
@@ -582,4 +584,4 @@ export {
|
|
|
582
584
|
proxyToSandbox,
|
|
583
585
|
isLocalhostPattern
|
|
584
586
|
};
|
|
585
|
-
//# sourceMappingURL=chunk-
|
|
587
|
+
//# sourceMappingURL=chunk-YZRU4CKO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/sandbox.ts","../src/request-handler.ts"],"sourcesContent":["import { Container, getContainer } from \"@cloudflare/containers\";\nimport { HttpClient } from \"./client\";\nimport { isLocalhostPattern } from \"./request-handler\";\nimport {\n logSecurityEvent,\n SecurityError,\n sanitizeSandboxId,\n validatePort\n} from \"./security\";\nimport type {\n ExecOptions,\n ExecResult,\n ISandbox,\n Process,\n ProcessOptions,\n ProcessStatus,\n StreamOptions\n} from \"./types\";\nimport {\n ProcessNotFoundError,\n SandboxError\n} from \"./types\";\n\nexport function getSandbox(ns: DurableObjectNamespace<Sandbox>, id: string) {\n const stub = getContainer(ns, id);\n\n // Store the name on first access\n stub.setSandboxName?.(id);\n\n return stub;\n}\n\nexport class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {\n defaultPort = 3000; // Default port for the container's Bun server\n sleepAfter = \"3m\"; // Sleep the sandbox if no requests are made in this timeframe\n client: HttpClient;\n private sandboxName: string | null = null;\n\n constructor(ctx: DurableObjectState, env: Env) {\n super(ctx, env);\n this.client = new HttpClient({\n onCommandComplete: (success, exitCode, _stdout, _stderr, command) => {\n console.log(\n `[Container] Command completed: ${command}, Success: ${success}, Exit code: ${exitCode}`\n );\n },\n onCommandStart: (command) => {\n console.log(\n `[Container] Command started: ${command}`\n );\n },\n onError: (error, _command) => {\n console.error(`[Container] Command error: ${error}`);\n },\n onOutput: (stream, data, _command) => {\n console.log(`[Container] [${stream}] ${data}`);\n },\n port: 3000, // Control plane port\n stub: this,\n });\n\n // Load the sandbox name from storage on initialization\n this.ctx.blockConcurrencyWhile(async () => {\n this.sandboxName = await this.ctx.storage.get<string>('sandboxName') || null;\n });\n }\n\n // RPC method to set the sandbox name\n async setSandboxName(name: string): Promise<void> {\n if (!this.sandboxName) {\n this.sandboxName = name;\n await this.ctx.storage.put('sandboxName', name);\n console.log(`[Sandbox] Stored sandbox name via RPC: ${name}`);\n }\n }\n\n // RPC method to set environment variables\n async setEnvVars(envVars: Record<string, string>): Promise<void> {\n this.envVars = { ...this.envVars, ...envVars };\n console.log(`[Sandbox] Updated environment variables`);\n }\n\n override onStart() {\n console.log(\"Sandbox successfully started\");\n }\n\n override onStop() {\n console.log(\"Sandbox successfully shut down\");\n if (this.client) {\n this.client.clearSession();\n }\n }\n\n override onError(error: unknown) {\n console.log(\"Sandbox error:\", error);\n }\n\n // Override fetch to route internal container requests to appropriate ports\n override async fetch(request: Request): Promise<Response> {\n const url = new URL(request.url);\n\n // Capture and store the sandbox name from the header if present\n if (!this.sandboxName && request.headers.has('X-Sandbox-Name')) {\n const name = request.headers.get('X-Sandbox-Name')!;\n this.sandboxName = name;\n await this.ctx.storage.put('sandboxName', name);\n console.log(`[Sandbox] Stored sandbox name: ${this.sandboxName}`);\n }\n\n // Determine which port to route to\n const port = this.determinePort(url);\n\n // Route to the appropriate port\n return await this.containerFetch(request, port);\n }\n\n private determinePort(url: URL): number {\n // Extract port from proxy requests (e.g., /proxy/8080/*)\n const proxyMatch = url.pathname.match(/^\\/proxy\\/(\\d+)/);\n if (proxyMatch) {\n return parseInt(proxyMatch[1]);\n }\n\n // All other requests go to control plane on port 3000\n // This includes /api/* endpoints and any other control requests\n return 3000;\n }\n\n // Enhanced exec method - always returns ExecResult with optional streaming\n // This replaces the old exec method to match ISandbox interface\n async exec(command: string, options?: ExecOptions): Promise<ExecResult> {\n const startTime = Date.now();\n const timestamp = new Date().toISOString();\n\n // Handle timeout\n let timeoutId: NodeJS.Timeout | undefined;\n\n try {\n // Handle cancellation\n if (options?.signal?.aborted) {\n throw new Error('Operation was aborted');\n }\n\n let result: ExecResult;\n\n if (options?.stream && options?.onOutput) {\n // Streaming with callbacks - we need to collect the final result\n result = await this.executeWithStreaming(command, options, startTime, timestamp);\n } else {\n // Regular execution\n const response = await this.client.execute(\n command,\n options?.sessionId\n );\n\n const duration = Date.now() - startTime;\n result = this.mapExecuteResponseToExecResult(response, duration, options?.sessionId);\n }\n\n // Call completion callback if provided\n if (options?.onComplete) {\n options.onComplete(result);\n }\n\n return result;\n } catch (error) {\n if (options?.onError && error instanceof Error) {\n options.onError(error);\n }\n throw error;\n } finally {\n if (timeoutId) {\n clearTimeout(timeoutId);\n }\n }\n }\n\n private async executeWithStreaming(\n command: string,\n options: ExecOptions,\n startTime: number,\n timestamp: string\n ): Promise<ExecResult> {\n let stdout = '';\n let stderr = '';\n\n try {\n const stream = await this.client.executeCommandStream(command, options.sessionId);\n const { parseSSEStream } = await import('./sse-parser');\n\n for await (const event of parseSSEStream<import('./types').ExecEvent>(stream)) {\n // Check for cancellation\n if (options.signal?.aborted) {\n throw new Error('Operation was aborted');\n }\n\n switch (event.type) {\n case 'stdout':\n case 'stderr':\n if (event.data) {\n // Update accumulated output\n if (event.type === 'stdout') stdout += event.data;\n if (event.type === 'stderr') stderr += event.data;\n\n // Call user's callback\n if (options.onOutput) {\n options.onOutput(event.type, event.data);\n }\n }\n break;\n\n case 'complete': {\n // Use result from complete event if available\n const duration = Date.now() - startTime;\n return event.result || {\n success: event.exitCode === 0,\n exitCode: event.exitCode || 0,\n stdout,\n stderr,\n command,\n duration,\n timestamp,\n sessionId: options.sessionId\n };\n }\n\n case 'error':\n throw new Error(event.error || 'Command execution failed');\n }\n }\n\n // If we get here without a complete event, something went wrong\n throw new Error('Stream ended without completion event');\n\n } catch (error) {\n if (options.signal?.aborted) {\n throw new Error('Operation was aborted');\n }\n throw error;\n }\n }\n\n private mapExecuteResponseToExecResult(\n response: import('./client').ExecuteResponse,\n duration: number,\n sessionId?: string\n ): ExecResult {\n return {\n success: response.success,\n exitCode: response.exitCode,\n stdout: response.stdout,\n stderr: response.stderr,\n command: response.command,\n duration,\n timestamp: response.timestamp,\n sessionId\n };\n }\n\n\n // Background process management\n async startProcess(command: string, options?: ProcessOptions): Promise<Process> {\n // Use the new HttpClient method to start the process\n try {\n const response = await this.client.startProcess(command, {\n processId: options?.processId,\n sessionId: options?.sessionId,\n timeout: options?.timeout,\n env: options?.env,\n cwd: options?.cwd,\n encoding: options?.encoding,\n autoCleanup: options?.autoCleanup\n });\n\n const process = response.process;\n const processObj: Process = {\n id: process.id,\n pid: process.pid,\n command: process.command,\n status: process.status as ProcessStatus,\n startTime: new Date(process.startTime),\n endTime: undefined,\n exitCode: undefined,\n sessionId: process.sessionId,\n\n async kill(): Promise<void> {\n throw new Error('Method will be replaced');\n },\n async getStatus(): Promise<ProcessStatus> {\n throw new Error('Method will be replaced');\n },\n async getLogs(): Promise<{ stdout: string; stderr: string }> {\n throw new Error('Method will be replaced');\n }\n };\n\n // Bind context properly\n processObj.kill = async (signal?: string) => {\n await this.killProcess(process.id, signal);\n };\n\n processObj.getStatus = async () => {\n const current = await this.getProcess(process.id);\n return current?.status || 'error';\n };\n\n processObj.getLogs = async () => {\n const logs = await this.getProcessLogs(process.id);\n return { stdout: logs.stdout, stderr: logs.stderr };\n };\n\n // Call onStart callback if provided\n if (options?.onStart) {\n options.onStart(processObj);\n }\n\n return processObj;\n\n } catch (error) {\n if (options?.onError && error instanceof Error) {\n options.onError(error);\n }\n\n throw error;\n }\n }\n\n async listProcesses(): Promise<Process[]> {\n const response = await this.client.listProcesses();\n\n return response.processes.map(processData => ({\n id: processData.id,\n pid: processData.pid,\n command: processData.command,\n status: processData.status,\n startTime: new Date(processData.startTime),\n endTime: processData.endTime ? new Date(processData.endTime) : undefined,\n exitCode: processData.exitCode,\n sessionId: processData.sessionId,\n\n kill: async (signal?: string) => {\n await this.killProcess(processData.id, signal);\n },\n\n getStatus: async () => {\n const current = await this.getProcess(processData.id);\n return current?.status || 'error';\n },\n\n getLogs: async () => {\n const logs = await this.getProcessLogs(processData.id);\n return { stdout: logs.stdout, stderr: logs.stderr };\n }\n }));\n }\n\n async getProcess(id: string): Promise<Process | null> {\n const response = await this.client.getProcess(id);\n if (!response.process) {\n return null;\n }\n\n const processData = response.process;\n return {\n id: processData.id,\n pid: processData.pid,\n command: processData.command,\n status: processData.status,\n startTime: new Date(processData.startTime),\n endTime: processData.endTime ? new Date(processData.endTime) : undefined,\n exitCode: processData.exitCode,\n sessionId: processData.sessionId,\n\n kill: async (signal?: string) => {\n await this.killProcess(processData.id, signal);\n },\n\n getStatus: async () => {\n const current = await this.getProcess(processData.id);\n return current?.status || 'error';\n },\n\n getLogs: async () => {\n const logs = await this.getProcessLogs(processData.id);\n return { stdout: logs.stdout, stderr: logs.stderr };\n }\n };\n }\n\n async killProcess(id: string, _signal?: string): Promise<void> {\n try {\n // Note: signal parameter is not currently supported by the HttpClient implementation\n await this.client.killProcess(id);\n } catch (error) {\n if (error instanceof Error && error.message.includes('Process not found')) {\n throw new ProcessNotFoundError(id);\n }\n throw new SandboxError(\n `Failed to kill process ${id}: ${error instanceof Error ? error.message : 'Unknown error'}`,\n 'KILL_PROCESS_FAILED'\n );\n }\n }\n\n async killAllProcesses(): Promise<number> {\n const response = await this.client.killAllProcesses();\n return response.killedCount;\n }\n\n async cleanupCompletedProcesses(): Promise<number> {\n // For now, this would need to be implemented as a container endpoint\n // as we no longer maintain local process storage\n // We'll return 0 as a placeholder until the container endpoint is added\n return 0;\n }\n\n async getProcessLogs(id: string): Promise<{ stdout: string; stderr: string }> {\n try {\n const response = await this.client.getProcessLogs(id);\n return {\n stdout: response.stdout,\n stderr: response.stderr\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes('Process not found')) {\n throw new ProcessNotFoundError(id);\n }\n throw error;\n }\n }\n\n\n // Streaming methods - return ReadableStream for RPC compatibility\n async execStream(command: string, options?: StreamOptions): Promise<ReadableStream<Uint8Array>> {\n // Check for cancellation\n if (options?.signal?.aborted) {\n throw new Error('Operation was aborted');\n }\n\n // Get the stream from HttpClient (need to add this method)\n const stream = await this.client.executeCommandStream(command, options?.sessionId);\n\n // Return the ReadableStream directly - can be converted to AsyncIterable by consumers\n return stream;\n }\n\n async streamProcessLogs(processId: string, options?: { signal?: AbortSignal }): Promise<ReadableStream<Uint8Array>> {\n // Check for cancellation\n if (options?.signal?.aborted) {\n throw new Error('Operation was aborted');\n }\n\n // Get the stream from HttpClient\n const stream = await this.client.streamProcessLogs(processId);\n\n // Return the ReadableStream directly - can be converted to AsyncIterable by consumers\n return stream;\n }\n\n async gitCheckout(\n repoUrl: string,\n options: { branch?: string; targetDir?: string }\n ) {\n return this.client.gitCheckout(repoUrl, options.branch, options.targetDir);\n }\n\n async mkdir(\n path: string,\n options: { recursive?: boolean } = {}\n ) {\n return this.client.mkdir(path, options.recursive);\n }\n\n async writeFile(\n path: string,\n content: string,\n options: { encoding?: string } = {}\n ) {\n return this.client.writeFile(path, content, options.encoding);\n }\n\n async deleteFile(path: string) {\n return this.client.deleteFile(path);\n }\n\n async renameFile(\n oldPath: string,\n newPath: string\n ) {\n return this.client.renameFile(oldPath, newPath);\n }\n\n async moveFile(\n sourcePath: string,\n destinationPath: string\n ) {\n return this.client.moveFile(sourcePath, destinationPath);\n }\n\n async readFile(\n path: string,\n options: { encoding?: string } = {}\n ) {\n return this.client.readFile(path, options.encoding);\n }\n\n async exposePort(port: number, options: { name?: string; hostname: string }) {\n await this.client.exposePort(port, options?.name);\n\n // We need the sandbox name to construct preview URLs\n if (!this.sandboxName) {\n throw new Error('Sandbox name not available. Ensure sandbox is accessed through getSandbox()');\n }\n\n const url = this.constructPreviewUrl(port, this.sandboxName, options.hostname);\n\n return {\n url,\n port,\n name: options?.name,\n };\n }\n\n async unexposePort(port: number) {\n if (!validatePort(port)) {\n logSecurityEvent('INVALID_PORT_UNEXPOSE', {\n port\n }, 'high');\n throw new SecurityError(`Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`);\n }\n\n await this.client.unexposePort(port);\n\n logSecurityEvent('PORT_UNEXPOSED', {\n port\n }, 'low');\n }\n\n async getExposedPorts(hostname: string) {\n const response = await this.client.getExposedPorts();\n\n // We need the sandbox name to construct preview URLs\n if (!this.sandboxName) {\n throw new Error('Sandbox name not available. Ensure sandbox is accessed through getSandbox()');\n }\n\n return response.ports.map(port => ({\n url: this.constructPreviewUrl(port.port, this.sandboxName!, hostname),\n port: port.port,\n name: port.name,\n exposedAt: port.exposedAt,\n }));\n }\n\n\n private constructPreviewUrl(port: number, sandboxId: string, hostname: string): string {\n if (!validatePort(port)) {\n logSecurityEvent('INVALID_PORT_REJECTED', {\n port,\n sandboxId,\n hostname\n }, 'high');\n throw new SecurityError(`Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`);\n }\n\n let sanitizedSandboxId: string;\n try {\n sanitizedSandboxId = sanitizeSandboxId(sandboxId);\n } catch (error) {\n logSecurityEvent('INVALID_SANDBOX_ID_REJECTED', {\n sandboxId,\n port,\n hostname,\n error: error instanceof Error ? error.message : 'Unknown error'\n }, 'high');\n throw error;\n }\n\n const isLocalhost = isLocalhostPattern(hostname);\n\n if (isLocalhost) {\n // Unified subdomain approach for localhost (RFC 6761)\n const [host, portStr] = hostname.split(':');\n const mainPort = portStr || '80';\n\n // Use URL constructor for safe URL building\n try {\n const baseUrl = new URL(`http://${host}:${mainPort}`);\n // Construct subdomain safely\n const subdomainHost = `${port}-${sanitizedSandboxId}.${host}`;\n baseUrl.hostname = subdomainHost;\n\n const finalUrl = baseUrl.toString();\n\n logSecurityEvent('PREVIEW_URL_CONSTRUCTED', {\n port,\n sandboxId: sanitizedSandboxId,\n hostname,\n resultUrl: finalUrl,\n environment: 'localhost'\n }, 'low');\n\n return finalUrl;\n } catch (error) {\n logSecurityEvent('URL_CONSTRUCTION_FAILED', {\n port,\n sandboxId: sanitizedSandboxId,\n hostname,\n error: error instanceof Error ? error.message : 'Unknown error'\n }, 'high');\n throw new SecurityError(`Failed to construct preview URL: ${error instanceof Error ? error.message : 'Unknown error'}`);\n }\n }\n\n // Production subdomain logic - enforce HTTPS\n try {\n // Always use HTTPS for production (non-localhost)\n const protocol = \"https\";\n const baseUrl = new URL(`${protocol}://${hostname}`);\n\n // Construct subdomain safely\n const subdomainHost = `${port}-${sanitizedSandboxId}.${hostname}`;\n baseUrl.hostname = subdomainHost;\n\n const finalUrl = baseUrl.toString();\n\n logSecurityEvent('PREVIEW_URL_CONSTRUCTED', {\n port,\n sandboxId: sanitizedSandboxId,\n hostname,\n resultUrl: finalUrl,\n environment: 'production'\n }, 'low');\n\n return finalUrl;\n } catch (error) {\n logSecurityEvent('URL_CONSTRUCTION_FAILED', {\n port,\n sandboxId: sanitizedSandboxId,\n hostname,\n error: error instanceof Error ? error.message : 'Unknown error'\n }, 'high');\n throw new SecurityError(`Failed to construct preview URL: ${error instanceof Error ? error.message : 'Unknown error'}`);\n }\n }\n}\n","import { getSandbox, type Sandbox } from \"./sandbox\";\nimport {\n logSecurityEvent,\n sanitizeSandboxId,\n validatePort\n} from \"./security\";\n\nexport interface SandboxEnv {\n Sandbox: DurableObjectNamespace<Sandbox>;\n}\n\nexport interface RouteInfo {\n port: number;\n sandboxId: string;\n path: string;\n}\n\nexport async function proxyToSandbox<E extends SandboxEnv>(\n request: Request,\n env: E\n): Promise<Response | null> {\n try {\n const url = new URL(request.url);\n const routeInfo = extractSandboxRoute(url);\n\n if (!routeInfo) {\n return null; // Not a request to an exposed container port\n }\n\n const { sandboxId, port, path } = routeInfo;\n const sandbox = getSandbox(env.Sandbox, sandboxId);\n\n // Build proxy request with proper headers\n let proxyUrl: string;\n\n // Route based on the target port\n if (port !== 3000) {\n // Route directly to user's service on the specified port\n proxyUrl = `http://localhost:${port}${path}${url.search}`;\n } else {\n // Port 3000 is our control plane - route normally\n proxyUrl = `http://localhost:3000${path}${url.search}`;\n }\n\n const proxyRequest = new Request(proxyUrl, {\n method: request.method,\n headers: {\n ...Object.fromEntries(request.headers),\n 'X-Original-URL': request.url,\n 'X-Forwarded-Host': url.hostname,\n 'X-Forwarded-Proto': url.protocol.replace(':', ''),\n 'X-Sandbox-Name': sandboxId, // Pass the friendly name\n },\n body: request.body,\n });\n\n return sandbox.containerFetch(proxyRequest, port);\n } catch (error) {\n console.error('[Sandbox] Proxy routing error:', error);\n return new Response('Proxy routing error', { status: 500 });\n }\n}\n\nfunction extractSandboxRoute(url: URL): RouteInfo | null {\n // Parse subdomain pattern: port-sandboxId.domain\n const subdomainMatch = url.hostname.match(/^(\\d{4,5})-([^.-][^.]*[^.-]|[^.-])\\.(.+)$/);\n\n if (!subdomainMatch) {\n // Log malformed subdomain attempts\n if (url.hostname.includes('-') && url.hostname.includes('.')) {\n logSecurityEvent('MALFORMED_SUBDOMAIN_ATTEMPT', {\n hostname: url.hostname,\n url: url.toString()\n }, 'medium');\n }\n return null;\n }\n\n const portStr = subdomainMatch[1];\n const sandboxId = subdomainMatch[2];\n const domain = subdomainMatch[3];\n\n const port = parseInt(portStr, 10);\n if (!validatePort(port)) {\n logSecurityEvent('INVALID_PORT_IN_SUBDOMAIN', {\n port,\n portStr,\n sandboxId,\n hostname: url.hostname,\n url: url.toString()\n }, 'high');\n return null;\n }\n\n let sanitizedSandboxId: string;\n try {\n sanitizedSandboxId = sanitizeSandboxId(sandboxId);\n } catch (error) {\n logSecurityEvent('INVALID_SANDBOX_ID_IN_SUBDOMAIN', {\n sandboxId,\n port,\n hostname: url.hostname,\n url: url.toString(),\n error: error instanceof Error ? error.message : 'Unknown error'\n }, 'high');\n return null;\n }\n\n // DNS subdomain length limit is 63 characters\n if (sandboxId.length > 63) {\n logSecurityEvent('SANDBOX_ID_LENGTH_VIOLATION', {\n sandboxId,\n length: sandboxId.length,\n port,\n hostname: url.hostname\n }, 'medium');\n return null;\n }\n\n logSecurityEvent('SANDBOX_ROUTE_EXTRACTED', {\n port,\n sandboxId: sanitizedSandboxId,\n domain,\n path: url.pathname || \"/\",\n hostname: url.hostname\n }, 'low');\n\n return {\n port,\n sandboxId: sanitizedSandboxId,\n path: url.pathname || \"/\",\n };\n}\n\nexport function isLocalhostPattern(hostname: string): boolean {\n const hostPart = hostname.split(\":\")[0];\n return (\n hostPart === \"localhost\" ||\n hostPart === \"127.0.0.1\" ||\n hostPart === \"::1\" ||\n hostPart === \"[::1]\" ||\n hostPart === \"0.0.0.0\"\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -3,6 +3,7 @@ import { ISandbox, ExecOptions, ExecResult, ProcessOptions, Process, StreamOptio
|
|
|
3
3
|
|
|
4
4
|
declare function getSandbox(ns: DurableObjectNamespace<Sandbox>, id: string): DurableObjectStub<Sandbox<unknown>>;
|
|
5
5
|
declare class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
6
|
+
defaultPort: number;
|
|
6
7
|
sleepAfter: string;
|
|
7
8
|
client: HttpClient;
|
|
8
9
|
private sandboxName;
|
package/dist/client.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { D as DeleteFileResponse, E as ExecuteResponse, G as GitCheckoutResponse, H as HttpClient, M as MkdirResponse, a as MoveFileResponse, R as ReadFileResponse, b as RenameFileResponse, W as WriteFileResponse } from './client-
|
|
1
|
+
export { D as DeleteFileResponse, E as ExecuteResponse, G as GitCheckoutResponse, H as HttpClient, M as MkdirResponse, a as MoveFileResponse, R as ReadFileResponse, b as RenameFileResponse, W as WriteFileResponse } from './client-BAuciZya.js';
|
|
2
2
|
import './types.js';
|
|
3
3
|
import '@cloudflare/containers';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { D as DeleteFileResponse, E as ExecuteResponse, G as GitCheckoutResponse, M as MkdirResponse, a as MoveFileResponse, R as ReadFileResponse, b as RenameFileResponse, S as Sandbox, W as WriteFileResponse, g as getSandbox } from './client-
|
|
1
|
+
export { D as DeleteFileResponse, E as ExecuteResponse, G as GitCheckoutResponse, M as MkdirResponse, a as MoveFileResponse, R as ReadFileResponse, b as RenameFileResponse, S as Sandbox, W as WriteFileResponse, g as getSandbox } from './client-BAuciZya.js';
|
|
2
2
|
export { RouteInfo, SandboxEnv, proxyToSandbox } from './request-handler.js';
|
|
3
3
|
export { asyncIterableToSSEStream, parseSSEStream, responseToAsyncIterable } from './sse-parser.js';
|
|
4
4
|
export { ExecEvent, LogEvent } from './types.js';
|
package/dist/index.js
CHANGED
package/dist/request-handler.js
CHANGED
package/dist/sandbox.d.ts
CHANGED
package/dist/sandbox.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudflare/sandbox",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/cloudflare/sandbox-sdk"
|
|
@@ -18,8 +18,9 @@
|
|
|
18
18
|
],
|
|
19
19
|
"scripts": {
|
|
20
20
|
"build": "rm -rf dist && tsup src/*.ts --outDir dist --dts --sourcemap --format esm",
|
|
21
|
-
"docker:
|
|
22
|
-
"docker:publish": "docker
|
|
21
|
+
"docker:local": "docker build . -t cloudflare/sandbox-test:$npm_package_version",
|
|
22
|
+
"docker:publish": "docker buildx build --platform linux/amd64,linux/arm64 -t cloudflare/sandbox:$npm_package_version --push .",
|
|
23
|
+
"docker:publish:beta": "docker buildx build --platform linux/amd64,linux/arm64 -t cloudflare/sandbox:$npm_package_version-beta --push ."
|
|
23
24
|
},
|
|
24
25
|
"exports": {
|
|
25
26
|
".": {
|
package/src/sandbox.ts
CHANGED
|
@@ -31,6 +31,7 @@ export function getSandbox(ns: DurableObjectNamespace<Sandbox>, id: string) {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
34
|
+
defaultPort = 3000; // Default port for the container's Bun server
|
|
34
35
|
sleepAfter = "3m"; // Sleep the sandbox if no requests are made in this timeframe
|
|
35
36
|
client: HttpClient;
|
|
36
37
|
private sandboxName: string | null = null;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/sandbox.ts","../src/request-handler.ts"],"sourcesContent":["import { Container, getContainer } from \"@cloudflare/containers\";\nimport { HttpClient } from \"./client\";\nimport { isLocalhostPattern } from \"./request-handler\";\nimport {\n logSecurityEvent,\n SecurityError,\n sanitizeSandboxId,\n validatePort\n} from \"./security\";\nimport type {\n ExecOptions,\n ExecResult,\n ISandbox,\n Process,\n ProcessOptions,\n ProcessStatus,\n StreamOptions\n} from \"./types\";\nimport {\n ProcessNotFoundError,\n SandboxError\n} from \"./types\";\n\nexport function getSandbox(ns: DurableObjectNamespace<Sandbox>, id: string) {\n const stub = getContainer(ns, id);\n\n // Store the name on first access\n stub.setSandboxName?.(id);\n\n return stub;\n}\n\nexport class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {\n sleepAfter = \"3m\"; // Sleep the sandbox if no requests are made in this timeframe\n client: HttpClient;\n private sandboxName: string | null = null;\n\n constructor(ctx: DurableObjectState, env: Env) {\n super(ctx, env);\n this.client = new HttpClient({\n onCommandComplete: (success, exitCode, _stdout, _stderr, command) => {\n console.log(\n `[Container] Command completed: ${command}, Success: ${success}, Exit code: ${exitCode}`\n );\n },\n onCommandStart: (command) => {\n console.log(\n `[Container] Command started: ${command}`\n );\n },\n onError: (error, _command) => {\n console.error(`[Container] Command error: ${error}`);\n },\n onOutput: (stream, data, _command) => {\n console.log(`[Container] [${stream}] ${data}`);\n },\n port: 3000, // Control plane port\n stub: this,\n });\n\n // Load the sandbox name from storage on initialization\n this.ctx.blockConcurrencyWhile(async () => {\n this.sandboxName = await this.ctx.storage.get<string>('sandboxName') || null;\n });\n }\n\n // RPC method to set the sandbox name\n async setSandboxName(name: string): Promise<void> {\n if (!this.sandboxName) {\n this.sandboxName = name;\n await this.ctx.storage.put('sandboxName', name);\n console.log(`[Sandbox] Stored sandbox name via RPC: ${name}`);\n }\n }\n\n // RPC method to set environment variables\n async setEnvVars(envVars: Record<string, string>): Promise<void> {\n this.envVars = { ...this.envVars, ...envVars };\n console.log(`[Sandbox] Updated environment variables`);\n }\n\n override onStart() {\n console.log(\"Sandbox successfully started\");\n }\n\n override onStop() {\n console.log(\"Sandbox successfully shut down\");\n if (this.client) {\n this.client.clearSession();\n }\n }\n\n override onError(error: unknown) {\n console.log(\"Sandbox error:\", error);\n }\n\n // Override fetch to route internal container requests to appropriate ports\n override async fetch(request: Request): Promise<Response> {\n const url = new URL(request.url);\n\n // Capture and store the sandbox name from the header if present\n if (!this.sandboxName && request.headers.has('X-Sandbox-Name')) {\n const name = request.headers.get('X-Sandbox-Name')!;\n this.sandboxName = name;\n await this.ctx.storage.put('sandboxName', name);\n console.log(`[Sandbox] Stored sandbox name: ${this.sandboxName}`);\n }\n\n // Determine which port to route to\n const port = this.determinePort(url);\n\n // Route to the appropriate port\n return await this.containerFetch(request, port);\n }\n\n private determinePort(url: URL): number {\n // Extract port from proxy requests (e.g., /proxy/8080/*)\n const proxyMatch = url.pathname.match(/^\\/proxy\\/(\\d+)/);\n if (proxyMatch) {\n return parseInt(proxyMatch[1]);\n }\n\n // All other requests go to control plane on port 3000\n // This includes /api/* endpoints and any other control requests\n return 3000;\n }\n\n // Enhanced exec method - always returns ExecResult with optional streaming\n // This replaces the old exec method to match ISandbox interface\n async exec(command: string, options?: ExecOptions): Promise<ExecResult> {\n const startTime = Date.now();\n const timestamp = new Date().toISOString();\n\n // Handle timeout\n let timeoutId: NodeJS.Timeout | undefined;\n\n try {\n // Handle cancellation\n if (options?.signal?.aborted) {\n throw new Error('Operation was aborted');\n }\n\n let result: ExecResult;\n\n if (options?.stream && options?.onOutput) {\n // Streaming with callbacks - we need to collect the final result\n result = await this.executeWithStreaming(command, options, startTime, timestamp);\n } else {\n // Regular execution\n const response = await this.client.execute(\n command,\n options?.sessionId\n );\n\n const duration = Date.now() - startTime;\n result = this.mapExecuteResponseToExecResult(response, duration, options?.sessionId);\n }\n\n // Call completion callback if provided\n if (options?.onComplete) {\n options.onComplete(result);\n }\n\n return result;\n } catch (error) {\n if (options?.onError && error instanceof Error) {\n options.onError(error);\n }\n throw error;\n } finally {\n if (timeoutId) {\n clearTimeout(timeoutId);\n }\n }\n }\n\n private async executeWithStreaming(\n command: string,\n options: ExecOptions,\n startTime: number,\n timestamp: string\n ): Promise<ExecResult> {\n let stdout = '';\n let stderr = '';\n\n try {\n const stream = await this.client.executeCommandStream(command, options.sessionId);\n const { parseSSEStream } = await import('./sse-parser');\n\n for await (const event of parseSSEStream<import('./types').ExecEvent>(stream)) {\n // Check for cancellation\n if (options.signal?.aborted) {\n throw new Error('Operation was aborted');\n }\n\n switch (event.type) {\n case 'stdout':\n case 'stderr':\n if (event.data) {\n // Update accumulated output\n if (event.type === 'stdout') stdout += event.data;\n if (event.type === 'stderr') stderr += event.data;\n\n // Call user's callback\n if (options.onOutput) {\n options.onOutput(event.type, event.data);\n }\n }\n break;\n\n case 'complete': {\n // Use result from complete event if available\n const duration = Date.now() - startTime;\n return event.result || {\n success: event.exitCode === 0,\n exitCode: event.exitCode || 0,\n stdout,\n stderr,\n command,\n duration,\n timestamp,\n sessionId: options.sessionId\n };\n }\n\n case 'error':\n throw new Error(event.error || 'Command execution failed');\n }\n }\n\n // If we get here without a complete event, something went wrong\n throw new Error('Stream ended without completion event');\n\n } catch (error) {\n if (options.signal?.aborted) {\n throw new Error('Operation was aborted');\n }\n throw error;\n }\n }\n\n private mapExecuteResponseToExecResult(\n response: import('./client').ExecuteResponse,\n duration: number,\n sessionId?: string\n ): ExecResult {\n return {\n success: response.success,\n exitCode: response.exitCode,\n stdout: response.stdout,\n stderr: response.stderr,\n command: response.command,\n duration,\n timestamp: response.timestamp,\n sessionId\n };\n }\n\n\n // Background process management\n async startProcess(command: string, options?: ProcessOptions): Promise<Process> {\n // Use the new HttpClient method to start the process\n try {\n const response = await this.client.startProcess(command, {\n processId: options?.processId,\n sessionId: options?.sessionId,\n timeout: options?.timeout,\n env: options?.env,\n cwd: options?.cwd,\n encoding: options?.encoding,\n autoCleanup: options?.autoCleanup\n });\n\n const process = response.process;\n const processObj: Process = {\n id: process.id,\n pid: process.pid,\n command: process.command,\n status: process.status as ProcessStatus,\n startTime: new Date(process.startTime),\n endTime: undefined,\n exitCode: undefined,\n sessionId: process.sessionId,\n\n async kill(): Promise<void> {\n throw new Error('Method will be replaced');\n },\n async getStatus(): Promise<ProcessStatus> {\n throw new Error('Method will be replaced');\n },\n async getLogs(): Promise<{ stdout: string; stderr: string }> {\n throw new Error('Method will be replaced');\n }\n };\n\n // Bind context properly\n processObj.kill = async (signal?: string) => {\n await this.killProcess(process.id, signal);\n };\n\n processObj.getStatus = async () => {\n const current = await this.getProcess(process.id);\n return current?.status || 'error';\n };\n\n processObj.getLogs = async () => {\n const logs = await this.getProcessLogs(process.id);\n return { stdout: logs.stdout, stderr: logs.stderr };\n };\n\n // Call onStart callback if provided\n if (options?.onStart) {\n options.onStart(processObj);\n }\n\n return processObj;\n\n } catch (error) {\n if (options?.onError && error instanceof Error) {\n options.onError(error);\n }\n\n throw error;\n }\n }\n\n async listProcesses(): Promise<Process[]> {\n const response = await this.client.listProcesses();\n\n return response.processes.map(processData => ({\n id: processData.id,\n pid: processData.pid,\n command: processData.command,\n status: processData.status,\n startTime: new Date(processData.startTime),\n endTime: processData.endTime ? new Date(processData.endTime) : undefined,\n exitCode: processData.exitCode,\n sessionId: processData.sessionId,\n\n kill: async (signal?: string) => {\n await this.killProcess(processData.id, signal);\n },\n\n getStatus: async () => {\n const current = await this.getProcess(processData.id);\n return current?.status || 'error';\n },\n\n getLogs: async () => {\n const logs = await this.getProcessLogs(processData.id);\n return { stdout: logs.stdout, stderr: logs.stderr };\n }\n }));\n }\n\n async getProcess(id: string): Promise<Process | null> {\n const response = await this.client.getProcess(id);\n if (!response.process) {\n return null;\n }\n\n const processData = response.process;\n return {\n id: processData.id,\n pid: processData.pid,\n command: processData.command,\n status: processData.status,\n startTime: new Date(processData.startTime),\n endTime: processData.endTime ? new Date(processData.endTime) : undefined,\n exitCode: processData.exitCode,\n sessionId: processData.sessionId,\n\n kill: async (signal?: string) => {\n await this.killProcess(processData.id, signal);\n },\n\n getStatus: async () => {\n const current = await this.getProcess(processData.id);\n return current?.status || 'error';\n },\n\n getLogs: async () => {\n const logs = await this.getProcessLogs(processData.id);\n return { stdout: logs.stdout, stderr: logs.stderr };\n }\n };\n }\n\n async killProcess(id: string, _signal?: string): Promise<void> {\n try {\n // Note: signal parameter is not currently supported by the HttpClient implementation\n await this.client.killProcess(id);\n } catch (error) {\n if (error instanceof Error && error.message.includes('Process not found')) {\n throw new ProcessNotFoundError(id);\n }\n throw new SandboxError(\n `Failed to kill process ${id}: ${error instanceof Error ? error.message : 'Unknown error'}`,\n 'KILL_PROCESS_FAILED'\n );\n }\n }\n\n async killAllProcesses(): Promise<number> {\n const response = await this.client.killAllProcesses();\n return response.killedCount;\n }\n\n async cleanupCompletedProcesses(): Promise<number> {\n // For now, this would need to be implemented as a container endpoint\n // as we no longer maintain local process storage\n // We'll return 0 as a placeholder until the container endpoint is added\n return 0;\n }\n\n async getProcessLogs(id: string): Promise<{ stdout: string; stderr: string }> {\n try {\n const response = await this.client.getProcessLogs(id);\n return {\n stdout: response.stdout,\n stderr: response.stderr\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes('Process not found')) {\n throw new ProcessNotFoundError(id);\n }\n throw error;\n }\n }\n\n\n // Streaming methods - return ReadableStream for RPC compatibility\n async execStream(command: string, options?: StreamOptions): Promise<ReadableStream<Uint8Array>> {\n // Check for cancellation\n if (options?.signal?.aborted) {\n throw new Error('Operation was aborted');\n }\n\n // Get the stream from HttpClient (need to add this method)\n const stream = await this.client.executeCommandStream(command, options?.sessionId);\n\n // Return the ReadableStream directly - can be converted to AsyncIterable by consumers\n return stream;\n }\n\n async streamProcessLogs(processId: string, options?: { signal?: AbortSignal }): Promise<ReadableStream<Uint8Array>> {\n // Check for cancellation\n if (options?.signal?.aborted) {\n throw new Error('Operation was aborted');\n }\n\n // Get the stream from HttpClient\n const stream = await this.client.streamProcessLogs(processId);\n\n // Return the ReadableStream directly - can be converted to AsyncIterable by consumers\n return stream;\n }\n\n async gitCheckout(\n repoUrl: string,\n options: { branch?: string; targetDir?: string }\n ) {\n return this.client.gitCheckout(repoUrl, options.branch, options.targetDir);\n }\n\n async mkdir(\n path: string,\n options: { recursive?: boolean } = {}\n ) {\n return this.client.mkdir(path, options.recursive);\n }\n\n async writeFile(\n path: string,\n content: string,\n options: { encoding?: string } = {}\n ) {\n return this.client.writeFile(path, content, options.encoding);\n }\n\n async deleteFile(path: string) {\n return this.client.deleteFile(path);\n }\n\n async renameFile(\n oldPath: string,\n newPath: string\n ) {\n return this.client.renameFile(oldPath, newPath);\n }\n\n async moveFile(\n sourcePath: string,\n destinationPath: string\n ) {\n return this.client.moveFile(sourcePath, destinationPath);\n }\n\n async readFile(\n path: string,\n options: { encoding?: string } = {}\n ) {\n return this.client.readFile(path, options.encoding);\n }\n\n async exposePort(port: number, options: { name?: string; hostname: string }) {\n await this.client.exposePort(port, options?.name);\n\n // We need the sandbox name to construct preview URLs\n if (!this.sandboxName) {\n throw new Error('Sandbox name not available. Ensure sandbox is accessed through getSandbox()');\n }\n\n const url = this.constructPreviewUrl(port, this.sandboxName, options.hostname);\n\n return {\n url,\n port,\n name: options?.name,\n };\n }\n\n async unexposePort(port: number) {\n if (!validatePort(port)) {\n logSecurityEvent('INVALID_PORT_UNEXPOSE', {\n port\n }, 'high');\n throw new SecurityError(`Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`);\n }\n\n await this.client.unexposePort(port);\n\n logSecurityEvent('PORT_UNEXPOSED', {\n port\n }, 'low');\n }\n\n async getExposedPorts(hostname: string) {\n const response = await this.client.getExposedPorts();\n\n // We need the sandbox name to construct preview URLs\n if (!this.sandboxName) {\n throw new Error('Sandbox name not available. Ensure sandbox is accessed through getSandbox()');\n }\n\n return response.ports.map(port => ({\n url: this.constructPreviewUrl(port.port, this.sandboxName!, hostname),\n port: port.port,\n name: port.name,\n exposedAt: port.exposedAt,\n }));\n }\n\n\n private constructPreviewUrl(port: number, sandboxId: string, hostname: string): string {\n if (!validatePort(port)) {\n logSecurityEvent('INVALID_PORT_REJECTED', {\n port,\n sandboxId,\n hostname\n }, 'high');\n throw new SecurityError(`Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`);\n }\n\n let sanitizedSandboxId: string;\n try {\n sanitizedSandboxId = sanitizeSandboxId(sandboxId);\n } catch (error) {\n logSecurityEvent('INVALID_SANDBOX_ID_REJECTED', {\n sandboxId,\n port,\n hostname,\n error: error instanceof Error ? error.message : 'Unknown error'\n }, 'high');\n throw error;\n }\n\n const isLocalhost = isLocalhostPattern(hostname);\n\n if (isLocalhost) {\n // Unified subdomain approach for localhost (RFC 6761)\n const [host, portStr] = hostname.split(':');\n const mainPort = portStr || '80';\n\n // Use URL constructor for safe URL building\n try {\n const baseUrl = new URL(`http://${host}:${mainPort}`);\n // Construct subdomain safely\n const subdomainHost = `${port}-${sanitizedSandboxId}.${host}`;\n baseUrl.hostname = subdomainHost;\n\n const finalUrl = baseUrl.toString();\n\n logSecurityEvent('PREVIEW_URL_CONSTRUCTED', {\n port,\n sandboxId: sanitizedSandboxId,\n hostname,\n resultUrl: finalUrl,\n environment: 'localhost'\n }, 'low');\n\n return finalUrl;\n } catch (error) {\n logSecurityEvent('URL_CONSTRUCTION_FAILED', {\n port,\n sandboxId: sanitizedSandboxId,\n hostname,\n error: error instanceof Error ? error.message : 'Unknown error'\n }, 'high');\n throw new SecurityError(`Failed to construct preview URL: ${error instanceof Error ? error.message : 'Unknown error'}`);\n }\n }\n\n // Production subdomain logic - enforce HTTPS\n try {\n // Always use HTTPS for production (non-localhost)\n const protocol = \"https\";\n const baseUrl = new URL(`${protocol}://${hostname}`);\n\n // Construct subdomain safely\n const subdomainHost = `${port}-${sanitizedSandboxId}.${hostname}`;\n baseUrl.hostname = subdomainHost;\n\n const finalUrl = baseUrl.toString();\n\n logSecurityEvent('PREVIEW_URL_CONSTRUCTED', {\n port,\n sandboxId: sanitizedSandboxId,\n hostname,\n resultUrl: finalUrl,\n environment: 'production'\n }, 'low');\n\n return finalUrl;\n } catch (error) {\n logSecurityEvent('URL_CONSTRUCTION_FAILED', {\n port,\n sandboxId: sanitizedSandboxId,\n hostname,\n error: error instanceof Error ? error.message : 'Unknown error'\n }, 'high');\n throw new SecurityError(`Failed to construct preview URL: ${error instanceof Error ? error.message : 'Unknown error'}`);\n }\n }\n}\n","import { getSandbox, type Sandbox } from \"./sandbox\";\nimport {\n logSecurityEvent,\n sanitizeSandboxId,\n validatePort\n} from \"./security\";\n\nexport interface SandboxEnv {\n Sandbox: DurableObjectNamespace<Sandbox>;\n}\n\nexport interface RouteInfo {\n port: number;\n sandboxId: string;\n path: string;\n}\n\nexport async function proxyToSandbox<E extends SandboxEnv>(\n request: Request,\n env: E\n): Promise<Response | null> {\n try {\n const url = new URL(request.url);\n const routeInfo = extractSandboxRoute(url);\n\n if (!routeInfo) {\n return null; // Not a request to an exposed container port\n }\n\n const { sandboxId, port, path } = routeInfo;\n const sandbox = getSandbox(env.Sandbox, sandboxId);\n\n // Build proxy request with proper headers\n let proxyUrl: string;\n\n // Route based on the target port\n if (port !== 3000) {\n // Route directly to user's service on the specified port\n proxyUrl = `http://localhost:${port}${path}${url.search}`;\n } else {\n // Port 3000 is our control plane - route normally\n proxyUrl = `http://localhost:3000${path}${url.search}`;\n }\n\n const proxyRequest = new Request(proxyUrl, {\n method: request.method,\n headers: {\n ...Object.fromEntries(request.headers),\n 'X-Original-URL': request.url,\n 'X-Forwarded-Host': url.hostname,\n 'X-Forwarded-Proto': url.protocol.replace(':', ''),\n 'X-Sandbox-Name': sandboxId, // Pass the friendly name\n },\n body: request.body,\n });\n\n return sandbox.containerFetch(proxyRequest, port);\n } catch (error) {\n console.error('[Sandbox] Proxy routing error:', error);\n return new Response('Proxy routing error', { status: 500 });\n }\n}\n\nfunction extractSandboxRoute(url: URL): RouteInfo | null {\n // Parse subdomain pattern: port-sandboxId.domain\n const subdomainMatch = url.hostname.match(/^(\\d{4,5})-([^.-][^.]*[^.-]|[^.-])\\.(.+)$/);\n\n if (!subdomainMatch) {\n // Log malformed subdomain attempts\n if (url.hostname.includes('-') && url.hostname.includes('.')) {\n logSecurityEvent('MALFORMED_SUBDOMAIN_ATTEMPT', {\n hostname: url.hostname,\n url: url.toString()\n }, 'medium');\n }\n return null;\n }\n\n const portStr = subdomainMatch[1];\n const sandboxId = subdomainMatch[2];\n const domain = subdomainMatch[3];\n\n const port = parseInt(portStr, 10);\n if (!validatePort(port)) {\n logSecurityEvent('INVALID_PORT_IN_SUBDOMAIN', {\n port,\n portStr,\n sandboxId,\n hostname: url.hostname,\n url: url.toString()\n }, 'high');\n return null;\n }\n\n let sanitizedSandboxId: string;\n try {\n sanitizedSandboxId = sanitizeSandboxId(sandboxId);\n } catch (error) {\n logSecurityEvent('INVALID_SANDBOX_ID_IN_SUBDOMAIN', {\n sandboxId,\n port,\n hostname: url.hostname,\n url: url.toString(),\n error: error instanceof Error ? error.message : 'Unknown error'\n }, 'high');\n return null;\n }\n\n // DNS subdomain length limit is 63 characters\n if (sandboxId.length > 63) {\n logSecurityEvent('SANDBOX_ID_LENGTH_VIOLATION', {\n sandboxId,\n length: sandboxId.length,\n port,\n hostname: url.hostname\n }, 'medium');\n return null;\n }\n\n logSecurityEvent('SANDBOX_ROUTE_EXTRACTED', {\n port,\n sandboxId: sanitizedSandboxId,\n domain,\n path: url.pathname || \"/\",\n hostname: url.hostname\n }, 'low');\n\n return {\n port,\n sandboxId: sanitizedSandboxId,\n path: url.pathname || \"/\",\n };\n}\n\nexport function isLocalhostPattern(hostname: string): boolean {\n const hostPart = hostname.split(\":\")[0];\n return (\n hostPart === \"localhost\" ||\n hostPart === \"127.0.0.1\" ||\n hostPart === \"::1\" ||\n hostPart === \"[::1]\" ||\n hostPart === \"0.0.0.0\"\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;AAAA,SAAS,WAAW,oBAAoB;;;ACiBxC,eAAsB,eACpB,SACA,KAC0B;AAC1B,MAAI;AACF,UAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;AAC/B,UAAM,YAAY,oBAAoB,GAAG;AAEzC,QAAI,CAAC,WAAW;AACd,aAAO;AAAA,IACT;AAEA,UAAM,EAAE,WAAW,MAAM,KAAK,IAAI;AAClC,UAAM,UAAU,WAAW,IAAI,SAAS,SAAS;AAGjD,QAAI;AAGJ,QAAI,SAAS,KAAM;AAEjB,iBAAW,oBAAoB,IAAI,GAAG,IAAI,GAAG,IAAI,MAAM;AAAA,IACzD,OAAO;AAEL,iBAAW,wBAAwB,IAAI,GAAG,IAAI,MAAM;AAAA,IACtD;AAEA,UAAM,eAAe,IAAI,QAAQ,UAAU;AAAA,MACzC,QAAQ,QAAQ;AAAA,MAChB,SAAS;AAAA,QACP,GAAG,OAAO,YAAY,QAAQ,OAAO;AAAA,QACrC,kBAAkB,QAAQ;AAAA,QAC1B,oBAAoB,IAAI;AAAA,QACxB,qBAAqB,IAAI,SAAS,QAAQ,KAAK,EAAE;AAAA,QACjD,kBAAkB;AAAA;AAAA,MACpB;AAAA,MACA,MAAM,QAAQ;AAAA,IAChB,CAAC;AAED,WAAO,QAAQ,eAAe,cAAc,IAAI;AAAA,EAClD,SAAS,OAAO;AACd,YAAQ,MAAM,kCAAkC,KAAK;AACrD,WAAO,IAAI,SAAS,uBAAuB,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC5D;AACF;AAEA,SAAS,oBAAoB,KAA4B;AAEvD,QAAM,iBAAiB,IAAI,SAAS,MAAM,2CAA2C;AAErF,MAAI,CAAC,gBAAgB;AAEnB,QAAI,IAAI,SAAS,SAAS,GAAG,KAAK,IAAI,SAAS,SAAS,GAAG,GAAG;AAC5D,uBAAiB,+BAA+B;AAAA,QAC9C,UAAU,IAAI;AAAA,QACd,KAAK,IAAI,SAAS;AAAA,MACpB,GAAG,QAAQ;AAAA,IACb;AACA,WAAO;AAAA,EACT;AAEA,QAAM,UAAU,eAAe,CAAC;AAChC,QAAM,YAAY,eAAe,CAAC;AAClC,QAAM,SAAS,eAAe,CAAC;AAE/B,QAAM,OAAO,SAAS,SAAS,EAAE;AACjC,MAAI,CAAC,aAAa,IAAI,GAAG;AACvB,qBAAiB,6BAA6B;AAAA,MAC5C;AAAA,MACA;AAAA,MACA;AAAA,MACA,UAAU,IAAI;AAAA,MACd,KAAK,IAAI,SAAS;AAAA,IACpB,GAAG,MAAM;AACT,WAAO;AAAA,EACT;AAEA,MAAI;AACJ,MAAI;AACF,yBAAqB,kBAAkB,SAAS;AAAA,EAClD,SAAS,OAAO;AACd,qBAAiB,mCAAmC;AAAA,MAClD;AAAA,MACA;AAAA,MACA,UAAU,IAAI;AAAA,MACd,KAAK,IAAI,SAAS;AAAA,MAClB,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,IAClD,GAAG,MAAM;AACT,WAAO;AAAA,EACT;AAGA,MAAI,UAAU,SAAS,IAAI;AACzB,qBAAiB,+BAA+B;AAAA,MAC9C;AAAA,MACA,QAAQ,UAAU;AAAA,MAClB;AAAA,MACA,UAAU,IAAI;AAAA,IAChB,GAAG,QAAQ;AACX,WAAO;AAAA,EACT;AAEA,mBAAiB,2BAA2B;AAAA,IAC1C;AAAA,IACA,WAAW;AAAA,IACX;AAAA,IACA,MAAM,IAAI,YAAY;AAAA,IACtB,UAAU,IAAI;AAAA,EAChB,GAAG,KAAK;AAER,SAAO;AAAA,IACL;AAAA,IACA,WAAW;AAAA,IACX,MAAM,IAAI,YAAY;AAAA,EACxB;AACF;AAEO,SAAS,mBAAmB,UAA2B;AAC5D,QAAM,WAAW,SAAS,MAAM,GAAG,EAAE,CAAC;AACtC,SACE,aAAa,eACb,aAAa,eACb,aAAa,SACb,aAAa,WACb,aAAa;AAEjB;;;ADxHO,SAAS,WAAW,IAAqC,IAAY;AAC1E,QAAM,OAAO,aAAa,IAAI,EAAE;AAGhC,OAAK,iBAAiB,EAAE;AAExB,SAAO;AACT;AAEO,IAAM,UAAN,cAAqC,UAAmC;AAAA,EAC7E,aAAa;AAAA;AAAA,EACb;AAAA,EACQ,cAA6B;AAAA,EAErC,YAAY,KAAyB,KAAU;AAC7C,UAAM,KAAK,GAAG;AACd,SAAK,SAAS,IAAI,WAAW;AAAA,MAC3B,mBAAmB,CAAC,SAAS,UAAU,SAAS,SAAS,YAAY;AACnE,gBAAQ;AAAA,UACN,kCAAkC,OAAO,cAAc,OAAO,gBAAgB,QAAQ;AAAA,QACxF;AAAA,MACF;AAAA,MACA,gBAAgB,CAAC,YAAY;AAC3B,gBAAQ;AAAA,UACN,gCAAgC,OAAO;AAAA,QACzC;AAAA,MACF;AAAA,MACA,SAAS,CAAC,OAAO,aAAa;AAC5B,gBAAQ,MAAM,8BAA8B,KAAK,EAAE;AAAA,MACrD;AAAA,MACA,UAAU,CAAC,QAAQ,MAAM,aAAa;AACpC,gBAAQ,IAAI,gBAAgB,MAAM,KAAK,IAAI,EAAE;AAAA,MAC/C;AAAA,MACA,MAAM;AAAA;AAAA,MACN,MAAM;AAAA,IACR,CAAC;AAGD,SAAK,IAAI,sBAAsB,YAAY;AACzC,WAAK,cAAc,MAAM,KAAK,IAAI,QAAQ,IAAY,aAAa,KAAK;AAAA,IAC1E,CAAC;AAAA,EACH;AAAA;AAAA,EAGA,MAAM,eAAe,MAA6B;AAChD,QAAI,CAAC,KAAK,aAAa;AACrB,WAAK,cAAc;AACnB,YAAM,KAAK,IAAI,QAAQ,IAAI,eAAe,IAAI;AAC9C,cAAQ,IAAI,0CAA0C,IAAI,EAAE;AAAA,IAC9D;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,WAAW,SAAgD;AAC/D,SAAK,UAAU,EAAE,GAAG,KAAK,SAAS,GAAG,QAAQ;AAC7C,YAAQ,IAAI,yCAAyC;AAAA,EACvD;AAAA,EAES,UAAU;AACjB,YAAQ,IAAI,8BAA8B;AAAA,EAC5C;AAAA,EAES,SAAS;AAChB,YAAQ,IAAI,gCAAgC;AAC5C,QAAI,KAAK,QAAQ;AACf,WAAK,OAAO,aAAa;AAAA,IAC3B;AAAA,EACF;AAAA,EAES,QAAQ,OAAgB;AAC/B,YAAQ,IAAI,kBAAkB,KAAK;AAAA,EACrC;AAAA;AAAA,EAGA,MAAe,MAAM,SAAqC;AACxD,UAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;AAG/B,QAAI,CAAC,KAAK,eAAe,QAAQ,QAAQ,IAAI,gBAAgB,GAAG;AAC9D,YAAM,OAAO,QAAQ,QAAQ,IAAI,gBAAgB;AACjD,WAAK,cAAc;AACnB,YAAM,KAAK,IAAI,QAAQ,IAAI,eAAe,IAAI;AAC9C,cAAQ,IAAI,kCAAkC,KAAK,WAAW,EAAE;AAAA,IAClE;AAGA,UAAM,OAAO,KAAK,cAAc,GAAG;AAGnC,WAAO,MAAM,KAAK,eAAe,SAAS,IAAI;AAAA,EAChD;AAAA,EAEQ,cAAc,KAAkB;AAEtC,UAAM,aAAa,IAAI,SAAS,MAAM,iBAAiB;AACvD,QAAI,YAAY;AACd,aAAO,SAAS,WAAW,CAAC,CAAC;AAAA,IAC/B;AAIA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA,EAIA,MAAM,KAAK,SAAiB,SAA4C;AACtE,UAAM,YAAY,KAAK,IAAI;AAC3B,UAAM,aAAY,oBAAI,KAAK,GAAE,YAAY;AAGzC,QAAI;AAEJ,QAAI;AAEF,UAAI,SAAS,QAAQ,SAAS;AAC5B,cAAM,IAAI,MAAM,uBAAuB;AAAA,MACzC;AAEA,UAAI;AAEJ,UAAI,SAAS,UAAU,SAAS,UAAU;AAExC,iBAAS,MAAM,KAAK,qBAAqB,SAAS,SAAS,WAAW,SAAS;AAAA,MACjF,OAAO;AAEL,cAAM,WAAW,MAAM,KAAK,OAAO;AAAA,UACjC;AAAA,UACA,SAAS;AAAA,QACX;AAEA,cAAM,WAAW,KAAK,IAAI,IAAI;AAC9B,iBAAS,KAAK,+BAA+B,UAAU,UAAU,SAAS,SAAS;AAAA,MACrF;AAGA,UAAI,SAAS,YAAY;AACvB,gBAAQ,WAAW,MAAM;AAAA,MAC3B;AAEA,aAAO;AAAA,IACT,SAAS,OAAO;AACd,UAAI,SAAS,WAAW,iBAAiB,OAAO;AAC9C,gBAAQ,QAAQ,KAAK;AAAA,MACvB;AACA,YAAM;AAAA,IACR,UAAE;AACA,UAAI,WAAW;AACb,qBAAa,SAAS;AAAA,MACxB;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAc,qBACZ,SACA,SACA,WACA,WACqB;AACrB,QAAI,SAAS;AACb,QAAI,SAAS;AAEb,QAAI;AACF,YAAM,SAAS,MAAM,KAAK,OAAO,qBAAqB,SAAS,QAAQ,SAAS;AAChF,YAAM,EAAE,eAAe,IAAI,MAAM,OAAO,iBAAc;AAEtD,uBAAiB,SAAS,eAA4C,MAAM,GAAG;AAE7E,YAAI,QAAQ,QAAQ,SAAS;AAC3B,gBAAM,IAAI,MAAM,uBAAuB;AAAA,QACzC;AAEA,gBAAQ,MAAM,MAAM;AAAA,UAClB,KAAK;AAAA,UACL,KAAK;AACH,gBAAI,MAAM,MAAM;AAEd,kBAAI,MAAM,SAAS,SAAU,WAAU,MAAM;AAC7C,kBAAI,MAAM,SAAS,SAAU,WAAU,MAAM;AAG7C,kBAAI,QAAQ,UAAU;AACpB,wBAAQ,SAAS,MAAM,MAAM,MAAM,IAAI;AAAA,cACzC;AAAA,YACF;AACA;AAAA,UAEF,KAAK,YAAY;AAEf,kBAAM,WAAW,KAAK,IAAI,IAAI;AAC9B,mBAAO,MAAM,UAAU;AAAA,cACrB,SAAS,MAAM,aAAa;AAAA,cAC5B,UAAU,MAAM,YAAY;AAAA,cAC5B;AAAA,cACA;AAAA,cACA;AAAA,cACA;AAAA,cACA;AAAA,cACA,WAAW,QAAQ;AAAA,YACrB;AAAA,UACF;AAAA,UAEA,KAAK;AACH,kBAAM,IAAI,MAAM,MAAM,SAAS,0BAA0B;AAAA,QAC7D;AAAA,MACF;AAGA,YAAM,IAAI,MAAM,uCAAuC;AAAA,IAEzD,SAAS,OAAO;AACd,UAAI,QAAQ,QAAQ,SAAS;AAC3B,cAAM,IAAI,MAAM,uBAAuB;AAAA,MACzC;AACA,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEQ,+BACN,UACA,UACA,WACY;AACZ,WAAO;AAAA,MACL,SAAS,SAAS;AAAA,MAClB,UAAU,SAAS;AAAA,MACnB,QAAQ,SAAS;AAAA,MACjB,QAAQ,SAAS;AAAA,MACjB,SAAS,SAAS;AAAA,MAClB;AAAA,MACA,WAAW,SAAS;AAAA,MACpB;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAIA,MAAM,aAAa,SAAiB,SAA4C;AAE9E,QAAI;AACF,YAAM,WAAW,MAAM,KAAK,OAAO,aAAa,SAAS;AAAA,QACvD,WAAW,SAAS;AAAA,QACpB,WAAW,SAAS;AAAA,QACpB,SAAS,SAAS;AAAA,QAClB,KAAK,SAAS;AAAA,QACd,KAAK,SAAS;AAAA,QACd,UAAU,SAAS;AAAA,QACnB,aAAa,SAAS;AAAA,MACxB,CAAC;AAED,YAAM,UAAU,SAAS;AACzB,YAAM,aAAsB;AAAA,QAC1B,IAAI,QAAQ;AAAA,QACZ,KAAK,QAAQ;AAAA,QACb,SAAS,QAAQ;AAAA,QACjB,QAAQ,QAAQ;AAAA,QAChB,WAAW,IAAI,KAAK,QAAQ,SAAS;AAAA,QACrC,SAAS;AAAA,QACT,UAAU;AAAA,QACV,WAAW,QAAQ;AAAA,QAEnB,MAAM,OAAsB;AAC1B,gBAAM,IAAI,MAAM,yBAAyB;AAAA,QAC3C;AAAA,QACA,MAAM,YAAoC;AACxC,gBAAM,IAAI,MAAM,yBAAyB;AAAA,QAC3C;AAAA,QACA,MAAM,UAAuD;AAC3D,gBAAM,IAAI,MAAM,yBAAyB;AAAA,QAC3C;AAAA,MACF;AAGA,iBAAW,OAAO,OAAO,WAAoB;AAC3C,cAAM,KAAK,YAAY,QAAQ,IAAI,MAAM;AAAA,MAC3C;AAEA,iBAAW,YAAY,YAAY;AACjC,cAAM,UAAU,MAAM,KAAK,WAAW,QAAQ,EAAE;AAChD,eAAO,SAAS,UAAU;AAAA,MAC5B;AAEA,iBAAW,UAAU,YAAY;AAC/B,cAAM,OAAO,MAAM,KAAK,eAAe,QAAQ,EAAE;AACjD,eAAO,EAAE,QAAQ,KAAK,QAAQ,QAAQ,KAAK,OAAO;AAAA,MACpD;AAGA,UAAI,SAAS,SAAS;AACpB,gBAAQ,QAAQ,UAAU;AAAA,MAC5B;AAEA,aAAO;AAAA,IAET,SAAS,OAAO;AACd,UAAI,SAAS,WAAW,iBAAiB,OAAO;AAC9C,gBAAQ,QAAQ,KAAK;AAAA,MACvB;AAEA,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEA,MAAM,gBAAoC;AACxC,UAAM,WAAW,MAAM,KAAK,OAAO,cAAc;AAEjD,WAAO,SAAS,UAAU,IAAI,kBAAgB;AAAA,MAC5C,IAAI,YAAY;AAAA,MAChB,KAAK,YAAY;AAAA,MACjB,SAAS,YAAY;AAAA,MACrB,QAAQ,YAAY;AAAA,MACpB,WAAW,IAAI,KAAK,YAAY,SAAS;AAAA,MACzC,SAAS,YAAY,UAAU,IAAI,KAAK,YAAY,OAAO,IAAI;AAAA,MAC/D,UAAU,YAAY;AAAA,MACtB,WAAW,YAAY;AAAA,MAEvB,MAAM,OAAO,WAAoB;AAC/B,cAAM,KAAK,YAAY,YAAY,IAAI,MAAM;AAAA,MAC/C;AAAA,MAEA,WAAW,YAAY;AACrB,cAAM,UAAU,MAAM,KAAK,WAAW,YAAY,EAAE;AACpD,eAAO,SAAS,UAAU;AAAA,MAC5B;AAAA,MAEA,SAAS,YAAY;AACnB,cAAM,OAAO,MAAM,KAAK,eAAe,YAAY,EAAE;AACrD,eAAO,EAAE,QAAQ,KAAK,QAAQ,QAAQ,KAAK,OAAO;AAAA,MACpD;AAAA,IACF,EAAE;AAAA,EACJ;AAAA,EAEA,MAAM,WAAW,IAAqC;AACpD,UAAM,WAAW,MAAM,KAAK,OAAO,WAAW,EAAE;AAChD,QAAI,CAAC,SAAS,SAAS;AACrB,aAAO;AAAA,IACT;AAEA,UAAM,cAAc,SAAS;AAC7B,WAAO;AAAA,MACL,IAAI,YAAY;AAAA,MAChB,KAAK,YAAY;AAAA,MACjB,SAAS,YAAY;AAAA,MACrB,QAAQ,YAAY;AAAA,MACpB,WAAW,IAAI,KAAK,YAAY,SAAS;AAAA,MACzC,SAAS,YAAY,UAAU,IAAI,KAAK,YAAY,OAAO,IAAI;AAAA,MAC/D,UAAU,YAAY;AAAA,MACtB,WAAW,YAAY;AAAA,MAEvB,MAAM,OAAO,WAAoB;AAC/B,cAAM,KAAK,YAAY,YAAY,IAAI,MAAM;AAAA,MAC/C;AAAA,MAEA,WAAW,YAAY;AACrB,cAAM,UAAU,MAAM,KAAK,WAAW,YAAY,EAAE;AACpD,eAAO,SAAS,UAAU;AAAA,MAC5B;AAAA,MAEA,SAAS,YAAY;AACnB,cAAM,OAAO,MAAM,KAAK,eAAe,YAAY,EAAE;AACrD,eAAO,EAAE,QAAQ,KAAK,QAAQ,QAAQ,KAAK,OAAO;AAAA,MACpD;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,YAAY,IAAY,SAAiC;AAC7D,QAAI;AAEF,YAAM,KAAK,OAAO,YAAY,EAAE;AAAA,IAClC,SAAS,OAAO;AACd,UAAI,iBAAiB,SAAS,MAAM,QAAQ,SAAS,mBAAmB,GAAG;AACzE,cAAM,IAAI,qBAAqB,EAAE;AAAA,MACnC;AACA,YAAM,IAAI;AAAA,QACR,0BAA0B,EAAE,KAAK,iBAAiB,QAAQ,MAAM,UAAU,eAAe;AAAA,QACzF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,mBAAoC;AACxC,UAAM,WAAW,MAAM,KAAK,OAAO,iBAAiB;AACpD,WAAO,SAAS;AAAA,EAClB;AAAA,EAEA,MAAM,4BAA6C;AAIjD,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,eAAe,IAAyD;AAC5E,QAAI;AACF,YAAM,WAAW,MAAM,KAAK,OAAO,eAAe,EAAE;AACpD,aAAO;AAAA,QACL,QAAQ,SAAS;AAAA,QACjB,QAAQ,SAAS;AAAA,MACnB;AAAA,IACF,SAAS,OAAO;AACd,UAAI,iBAAiB,SAAS,MAAM,QAAQ,SAAS,mBAAmB,GAAG;AACzE,cAAM,IAAI,qBAAqB,EAAE;AAAA,MACnC;AACA,YAAM;AAAA,IACR;AAAA,EACF;AAAA;AAAA,EAIA,MAAM,WAAW,SAAiB,SAA8D;AAE9F,QAAI,SAAS,QAAQ,SAAS;AAC5B,YAAM,IAAI,MAAM,uBAAuB;AAAA,IACzC;AAGA,UAAM,SAAS,MAAM,KAAK,OAAO,qBAAqB,SAAS,SAAS,SAAS;AAGjF,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,kBAAkB,WAAmB,SAAyE;AAElH,QAAI,SAAS,QAAQ,SAAS;AAC5B,YAAM,IAAI,MAAM,uBAAuB;AAAA,IACzC;AAGA,UAAM,SAAS,MAAM,KAAK,OAAO,kBAAkB,SAAS;AAG5D,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,YACJ,SACA,SACA;AACA,WAAO,KAAK,OAAO,YAAY,SAAS,QAAQ,QAAQ,QAAQ,SAAS;AAAA,EAC3E;AAAA,EAEA,MAAM,MACJ,MACA,UAAmC,CAAC,GACpC;AACA,WAAO,KAAK,OAAO,MAAM,MAAM,QAAQ,SAAS;AAAA,EAClD;AAAA,EAEA,MAAM,UACJ,MACA,SACA,UAAiC,CAAC,GAClC;AACA,WAAO,KAAK,OAAO,UAAU,MAAM,SAAS,QAAQ,QAAQ;AAAA,EAC9D;AAAA,EAEA,MAAM,WAAW,MAAc;AAC7B,WAAO,KAAK,OAAO,WAAW,IAAI;AAAA,EACpC;AAAA,EAEA,MAAM,WACJ,SACA,SACA;AACA,WAAO,KAAK,OAAO,WAAW,SAAS,OAAO;AAAA,EAChD;AAAA,EAEA,MAAM,SACJ,YACA,iBACA;AACA,WAAO,KAAK,OAAO,SAAS,YAAY,eAAe;AAAA,EACzD;AAAA,EAEA,MAAM,SACJ,MACA,UAAiC,CAAC,GAClC;AACA,WAAO,KAAK,OAAO,SAAS,MAAM,QAAQ,QAAQ;AAAA,EACpD;AAAA,EAEA,MAAM,WAAW,MAAc,SAA8C;AAC3E,UAAM,KAAK,OAAO,WAAW,MAAM,SAAS,IAAI;AAGhD,QAAI,CAAC,KAAK,aAAa;AACrB,YAAM,IAAI,MAAM,6EAA6E;AAAA,IAC/F;AAEA,UAAM,MAAM,KAAK,oBAAoB,MAAM,KAAK,aAAa,QAAQ,QAAQ;AAE7E,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA,MAAM,SAAS;AAAA,IACjB;AAAA,EACF;AAAA,EAEA,MAAM,aAAa,MAAc;AAC/B,QAAI,CAAC,aAAa,IAAI,GAAG;AACvB,uBAAiB,yBAAyB;AAAA,QACxC;AAAA,MACF,GAAG,MAAM;AACT,YAAM,IAAI,cAAc,wBAAwB,IAAI,gDAAgD;AAAA,IACtG;AAEA,UAAM,KAAK,OAAO,aAAa,IAAI;AAEnC,qBAAiB,kBAAkB;AAAA,MACjC;AAAA,IACF,GAAG,KAAK;AAAA,EACV;AAAA,EAEA,MAAM,gBAAgB,UAAkB;AACtC,UAAM,WAAW,MAAM,KAAK,OAAO,gBAAgB;AAGnD,QAAI,CAAC,KAAK,aAAa;AACrB,YAAM,IAAI,MAAM,6EAA6E;AAAA,IAC/F;AAEA,WAAO,SAAS,MAAM,IAAI,WAAS;AAAA,MACjC,KAAK,KAAK,oBAAoB,KAAK,MAAM,KAAK,aAAc,QAAQ;AAAA,MACpE,MAAM,KAAK;AAAA,MACX,MAAM,KAAK;AAAA,MACX,WAAW,KAAK;AAAA,IAClB,EAAE;AAAA,EACJ;AAAA,EAGQ,oBAAoB,MAAc,WAAmB,UAA0B;AACrF,QAAI,CAAC,aAAa,IAAI,GAAG;AACvB,uBAAiB,yBAAyB;AAAA,QACxC;AAAA,QACA;AAAA,QACA;AAAA,MACF,GAAG,MAAM;AACT,YAAM,IAAI,cAAc,wBAAwB,IAAI,gDAAgD;AAAA,IACtG;AAEA,QAAI;AACJ,QAAI;AACF,2BAAqB,kBAAkB,SAAS;AAAA,IAClD,SAAS,OAAO;AACd,uBAAiB,+BAA+B;AAAA,QAC9C;AAAA,QACA;AAAA,QACA;AAAA,QACA,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,MAClD,GAAG,MAAM;AACT,YAAM;AAAA,IACR;AAEA,UAAM,cAAc,mBAAmB,QAAQ;AAE/C,QAAI,aAAa;AAEf,YAAM,CAAC,MAAM,OAAO,IAAI,SAAS,MAAM,GAAG;AAC1C,YAAM,WAAW,WAAW;AAG5B,UAAI;AACF,cAAM,UAAU,IAAI,IAAI,UAAU,IAAI,IAAI,QAAQ,EAAE;AAEpD,cAAM,gBAAgB,GAAG,IAAI,IAAI,kBAAkB,IAAI,IAAI;AAC3D,gBAAQ,WAAW;AAEnB,cAAM,WAAW,QAAQ,SAAS;AAElC,yBAAiB,2BAA2B;AAAA,UAC1C;AAAA,UACA,WAAW;AAAA,UACX;AAAA,UACA,WAAW;AAAA,UACX,aAAa;AAAA,QACf,GAAG,KAAK;AAER,eAAO;AAAA,MACT,SAAS,OAAO;AACd,yBAAiB,2BAA2B;AAAA,UAC1C;AAAA,UACA,WAAW;AAAA,UACX;AAAA,UACA,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,QAClD,GAAG,MAAM;AACT,cAAM,IAAI,cAAc,oCAAoC,iBAAiB,QAAQ,MAAM,UAAU,eAAe,EAAE;AAAA,MACxH;AAAA,IACF;AAGA,QAAI;AAEF,YAAM,WAAW;AACjB,YAAM,UAAU,IAAI,IAAI,GAAG,QAAQ,MAAM,QAAQ,EAAE;AAGnD,YAAM,gBAAgB,GAAG,IAAI,IAAI,kBAAkB,IAAI,QAAQ;AAC/D,cAAQ,WAAW;AAEnB,YAAM,WAAW,QAAQ,SAAS;AAElC,uBAAiB,2BAA2B;AAAA,QAC1C;AAAA,QACA,WAAW;AAAA,QACX;AAAA,QACA,WAAW;AAAA,QACX,aAAa;AAAA,MACf,GAAG,KAAK;AAER,aAAO;AAAA,IACT,SAAS,OAAO;AACd,uBAAiB,2BAA2B;AAAA,QAC1C;AAAA,QACA,WAAW;AAAA,QACX;AAAA,QACA,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,MAClD,GAAG,MAAM;AACT,YAAM,IAAI,cAAc,oCAAoC,iBAAiB,QAAQ,MAAM,UAAU,eAAe,EAAE;AAAA,IACxH;AAAA,EACF;AACF;","names":[]}
|