@cloudflare/sandbox 0.0.0-cecde0a → 0.0.0-d55b0f4

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 CHANGED
@@ -1,5 +1,31 @@
1
1
  # @cloudflare/sandbox
2
2
 
3
+ ## 0.1.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [#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
8
+
9
+ - [#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
10
+
11
+ ## 0.1.1
12
+
13
+ ### Patch Changes
14
+
15
+ - [`157dde9`](https://github.com/cloudflare/sandbox-sdk/commit/157dde9b1f23e9bb6f3e9c3f0514b639a8813897) Thanks [@threepointone](https://github.com/threepointone)! - update deps
16
+
17
+ - [`a04f6b6`](https://github.com/cloudflare/sandbox-sdk/commit/a04f6b6c0b2ef9e3ce0851b53769f1c10d8c6de6) Thanks [@threepointone](https://github.com/threepointone)! - trigger a build with updated deps
18
+
19
+ ## 0.1.0
20
+
21
+ ### Minor Changes
22
+
23
+ - [#24](https://github.com/cloudflare/sandbox-sdk/pull/24) [`cecde0a`](https://github.com/cloudflare/sandbox-sdk/commit/cecde0a7530a87deffd8562fb8b01d66ee80ee19) Thanks [@ghostwriternr](https://github.com/ghostwriternr)! - Redesign command execution API
24
+
25
+ ### Patch Changes
26
+
27
+ - [#22](https://github.com/cloudflare/sandbox-sdk/pull/22) [`f5fcd52`](https://github.com/cloudflare/sandbox-sdk/commit/f5fcd52025d1f7958a374e69d75e3fc590275f3f) Thanks [@ghostwriternr](https://github.com/ghostwriternr)! - Allow setting env variables dynamically and remove command restrictions
28
+
3
29
  ## 0.0.9
4
30
 
5
31
  ### 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 using the official installation script
54
- RUN curl -fsSL https://bun.sh/install | bash \
55
- && mv /root/.bun/bin/bun /usr/local/bin/bun \
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/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@cloudflare/sandbox",
3
- "version": "0.0.0-cecde0a",
3
+ "version": "0.0.0-d55b0f4",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/cloudflare/sandbox-sdk"
7
7
  },
8
8
  "description": "A sandboxed environment for running commands",
9
9
  "dependencies": {
10
- "@cloudflare/containers": "^0.0.24"
10
+ "@cloudflare/containers": "^0.0.25"
11
11
  },
12
12
  "tags": [
13
13
  "sandbox",
@@ -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:build": "docker build -t ghostwriternr/cloudflare-sandbox:$npm_package_version .",
22
- "docker:publish": "docker push docker.io/ghostwriternr/cloudflare-sandbox:$npm_package_version"
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;
package/tsconfig.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "extends": "../../tsconfig.json"
2
+ "extends": "../../tsconfig.base.json"
3
3
  }
package/README.md DELETED
@@ -1,365 +0,0 @@
1
- ## @cloudflare/sandbox
2
-
3
- > **⚠️ Experimental** - This library is currently experimental and we're actively seeking feedback. Please try it out and let us know what you think!
4
-
5
- A library to spin up a sandboxed environment.
6
-
7
- First, setup your wrangler.json to use the sandbox:
8
-
9
- ```jsonc
10
- {
11
- // ...
12
- "containers": [
13
- {
14
- "class_name": "Sandbox",
15
- "image": "./node_modules/@cloudflare/sandbox/Dockerfile",
16
- "name": "sandbox"
17
- }
18
- ],
19
- "durable_objects": {
20
- "bindings": [
21
- {
22
- "class_name": "Sandbox",
23
- "name": "Sandbox"
24
- }
25
- ]
26
- },
27
- "migrations": [
28
- {
29
- "new_sqlite_classes": ["Sandbox"],
30
- "tag": "v1"
31
- }
32
- ]
33
- }
34
- ```
35
-
36
- Then, export the Sandbox class in your worker:
37
-
38
- ```ts
39
- export { Sandbox } from "@cloudflare/sandbox";
40
- ```
41
-
42
- You can then use the Sandbox class in your worker:
43
-
44
- ```ts
45
- import { getSandbox } from "@cloudflare/sandbox";
46
-
47
- export default {
48
- async fetch(request: Request, env: Env) {
49
- const sandbox = getSandbox(env.Sandbox, "my-sandbox");
50
- const result = await sandbox.exec("ls -la");
51
- return Response.json(result);
52
- },
53
- };
54
- ```
55
-
56
- ### Core Methods
57
-
58
- #### Command Execution
59
- - `exec(command: string, options?: ExecOptions)`: Execute a command and return the complete result.
60
- - `execStream(command: string, options?: StreamOptions)`: Execute a command with real-time streaming (returns ReadableStream).
61
-
62
- #### Process Management
63
- - `startProcess(command: string, options?: ProcessOptions)`: Start a background process.
64
- - `listProcesses()`: List all running processes.
65
- - `getProcess(id: string)`: Get details of a specific process.
66
- - `killProcess(id: string, signal?: string)`: Kill a specific process.
67
- - `killAllProcesses()`: Kill all running processes.
68
- - `streamProcessLogs(processId: string, options?: { signal?: AbortSignal })`: Stream logs from a running process (returns ReadableStream).
69
-
70
- #### File Operations
71
- - `gitCheckout(repoUrl: string, options: { branch?: string; targetDir?: string })`: Checkout a git repository.
72
- - `mkdir(path: string, options?: { recursive?: boolean })`: Create a directory.
73
- - `writeFile(path: string, content: string, options?: { encoding?: string })`: Write content to a file.
74
- - `readFile(path: string, options?: { encoding?: string })`: Read content from a file.
75
- - `deleteFile(path: string)`: Delete a file.
76
- - `renameFile(oldPath: string, newPath: string)`: Rename a file.
77
- - `moveFile(sourcePath: string, destinationPath: string)`: Move a file.
78
-
79
- #### Port Management
80
- - `exposePort(port: number, options: { name?: string; hostname: string })`: Expose a port for external access.
81
- - `unexposePort(port: number)`: Unexpose a previously exposed port.
82
- - `getExposedPorts(hostname: string)`: List all exposed ports with their preview URLs.
83
-
84
- ### Beautiful AsyncIterable Streaming APIs ✨
85
-
86
- The SDK provides streaming methods that return `ReadableStream` for RPC compatibility, along with a `parseSSEStream` utility to convert them to typed AsyncIterables:
87
-
88
- #### Stream Command Output
89
- ```typescript
90
- import { parseSSEStream, type ExecEvent } from '@cloudflare/sandbox';
91
-
92
- // Get the stream and convert to AsyncIterable
93
- const stream = await sandbox.execStream('npm run build');
94
- for await (const event of parseSSEStream<ExecEvent>(stream)) {
95
- switch (event.type) {
96
- case 'start':
97
- console.log(`Build started: ${event.command}`);
98
- break;
99
- case 'stdout':
100
- console.log(`[OUT] ${event.data}`);
101
- break;
102
- case 'stderr':
103
- console.error(`[ERR] ${event.data}`);
104
- break;
105
- case 'complete':
106
- console.log(`Build finished with exit code: ${event.exitCode}`);
107
- break;
108
- case 'error':
109
- console.error(`Build error: ${event.error}`);
110
- break;
111
- }
112
- }
113
- ```
114
-
115
- #### Stream Process Logs
116
- ```typescript
117
- import { parseSSEStream, type LogEvent } from '@cloudflare/sandbox';
118
-
119
- // Monitor background process logs
120
- const webServer = await sandbox.startProcess('node server.js');
121
-
122
- const logStream = await sandbox.streamProcessLogs(webServer.id);
123
- for await (const log of parseSSEStream<LogEvent>(logStream)) {
124
- if (log.type === 'stdout') {
125
- console.log(`Server: ${log.data}`);
126
- } else if (log.type === 'stderr' && log.data.includes('ERROR')) {
127
- // React to errors
128
- await handleError(log);
129
- } else if (log.type === 'exit') {
130
- console.log(`Server exited with code: ${log.exitCode}`);
131
- break;
132
- }
133
- }
134
- ```
135
-
136
- #### Why parseSSEStream?
137
-
138
- The streaming methods return `ReadableStream<Uint8Array>` to ensure compatibility across Durable Object RPC boundaries. The `parseSSEStream` utility converts these streams into typed AsyncIterables, giving you the best of both worlds:
139
-
140
- - **RPC Compatibility**: ReadableStream can be serialized across process boundaries
141
- - **Beautiful APIs**: AsyncIterable provides clean `for await` syntax with typed events
142
- - **Type Safety**: Full TypeScript support with `ExecEvent` and `LogEvent` types
143
-
144
- #### Advanced Examples
145
-
146
- ##### CI/CD Build System
147
- ```typescript
148
- import { parseSSEStream, type ExecEvent } from '@cloudflare/sandbox';
149
-
150
- export async function runBuild(env: Env, buildId: string) {
151
- const sandbox = getSandbox(env.Sandbox, buildId);
152
- const buildLog: string[] = [];
153
-
154
- try {
155
- const stream = await sandbox.execStream('npm run build');
156
- for await (const event of parseSSEStream<ExecEvent>(stream)) {
157
- buildLog.push(`[${event.type}] ${event.data || ''}`);
158
-
159
- if (event.type === 'complete') {
160
- await env.BUILDS.put(buildId, {
161
- status: event.exitCode === 0 ? 'success' : 'failed',
162
- exitCode: event.exitCode,
163
- logs: buildLog.join('\n'),
164
- duration: Date.now() - new Date(event.timestamp).getTime()
165
- });
166
- }
167
- }
168
- } catch (error) {
169
- await env.BUILDS.put(buildId, {
170
- status: 'error',
171
- error: error.message,
172
- logs: buildLog.join('\n')
173
- });
174
- }
175
- }
176
- ```
177
-
178
- ##### System Monitoring
179
- ```typescript
180
- import { parseSSEStream, type LogEvent } from '@cloudflare/sandbox';
181
-
182
- export default {
183
- async scheduled(controller: ScheduledController, env: Env) {
184
- const sandbox = getSandbox(env.Sandbox, 'monitor');
185
-
186
- // Monitor system logs
187
- const monitor = await sandbox.startProcess('journalctl -f');
188
-
189
- const logStream = await sandbox.streamProcessLogs(monitor.id);
190
- for await (const log of parseSSEStream<LogEvent>(logStream)) {
191
- if (log.type === 'stdout') {
192
- // Check for critical errors
193
- if (log.data.includes('CRITICAL')) {
194
- await env.ALERTS.send({
195
- severity: 'critical',
196
- message: log.data,
197
- timestamp: log.timestamp
198
- });
199
- }
200
-
201
- // Store logs
202
- await env.LOGS.put(`${log.timestamp}-${monitor.id}`, log.data);
203
- }
204
- }
205
- }
206
- }
207
- ```
208
-
209
- ##### Streaming to Frontend via SSE
210
- ```typescript
211
- // Worker endpoint that streams to frontend
212
- app.get('/api/build/:id/stream', async (req, env) => {
213
- const sandbox = getSandbox(env.Sandbox, req.params.id);
214
- const encoder = new TextEncoder();
215
-
216
- return new Response(
217
- new ReadableStream({
218
- async start(controller) {
219
- try {
220
- for await (const event of sandbox.execStream('npm run build')) {
221
- // Forward events to frontend as SSE
222
- const sseEvent = `data: ${JSON.stringify(event)}\n\n`;
223
- controller.enqueue(encoder.encode(sseEvent));
224
- }
225
- } catch (error) {
226
- const errorEvent = `data: ${JSON.stringify({
227
- type: 'error',
228
- error: error.message
229
- })}\n\n`;
230
- controller.enqueue(encoder.encode(errorEvent));
231
- } finally {
232
- controller.close();
233
- }
234
- }
235
- }),
236
- {
237
- headers: {
238
- 'Content-Type': 'text/event-stream',
239
- 'Cache-Control': 'no-cache'
240
- }
241
- }
242
- );
243
- });
244
- ```
245
-
246
- ### Streaming Utilities
247
-
248
- The SDK exports additional utilities for working with SSE streams:
249
-
250
- #### `parseSSEStream`
251
- Converts a `ReadableStream<Uint8Array>` (from SSE endpoints) into a typed `AsyncIterable<T>`. This is the primary utility for consuming streams from the SDK.
252
-
253
- ```typescript
254
- import { parseSSEStream, type ExecEvent } from '@cloudflare/sandbox';
255
-
256
- const stream = await sandbox.execStream('npm build');
257
- for await (const event of parseSSEStream<ExecEvent>(stream)) {
258
- console.log(event);
259
- }
260
- ```
261
-
262
- #### `responseToAsyncIterable`
263
- Converts a `Response` object with SSE content directly to `AsyncIterable<T>`. Useful when fetching from external SSE endpoints.
264
-
265
- ```typescript
266
- import { responseToAsyncIterable, type LogEvent } from '@cloudflare/sandbox';
267
-
268
- // Fetch from an external SSE endpoint
269
- const response = await fetch('https://api.example.com/logs/stream', {
270
- headers: { 'Accept': 'text/event-stream' }
271
- });
272
-
273
- // Convert Response to typed AsyncIterable
274
- for await (const event of responseToAsyncIterable<LogEvent>(response)) {
275
- console.log(`[${event.type}] ${event.data}`);
276
- }
277
- ```
278
-
279
- #### `asyncIterableToSSEStream`
280
- Converts an `AsyncIterable<T>` into an SSE-formatted `ReadableStream<Uint8Array>`. Perfect for Worker endpoints that need to transform or filter events before sending to clients.
281
-
282
- ```typescript
283
- import { getSandbox, parseSSEStream, asyncIterableToSSEStream, type LogEvent } from '@cloudflare/sandbox';
284
-
285
- export async function handleFilteredLogs(request: Request, env: Env) {
286
- const sandbox = getSandbox(env.SANDBOX);
287
-
288
- // Custom async generator that filters logs
289
- async function* filterLogs() {
290
- const stream = await sandbox.streamProcessLogs('web-server');
291
-
292
- for await (const log of parseSSEStream<LogEvent>(stream)) {
293
- // Only forward error logs to the client
294
- if (log.type === 'stderr' || log.data.includes('ERROR')) {
295
- yield log;
296
- }
297
- }
298
- }
299
-
300
- // Convert filtered AsyncIterable back to SSE stream for the response
301
- const sseStream = asyncIterableToSSEStream(filterLogs());
302
-
303
- return new Response(sseStream, {
304
- headers: {
305
- 'Content-Type': 'text/event-stream',
306
- 'Cache-Control': 'no-cache',
307
- }
308
- });
309
- }
310
- ```
311
-
312
- **Advanced Example - Merging Multiple Streams:**
313
- ```typescript
314
- async function* mergeBuilds(env: Env) {
315
- const sandbox1 = getSandbox(env.SANDBOX1);
316
- const sandbox2 = getSandbox(env.SANDBOX2);
317
-
318
- // Start builds in parallel
319
- const [stream1, stream2] = await Promise.all([
320
- sandbox1.execStream('npm run build:frontend'),
321
- sandbox2.execStream('npm run build:backend')
322
- ]);
323
-
324
- // Parse and merge events
325
- const frontend = parseSSEStream<ExecEvent>(stream1);
326
- const backend = parseSSEStream<ExecEvent>(stream2);
327
-
328
- // Merge with source identification
329
- for await (const event of frontend) {
330
- yield { ...event, source: 'frontend' };
331
- }
332
- for await (const event of backend) {
333
- yield { ...event, source: 'backend' };
334
- }
335
- }
336
-
337
- // Convert merged stream to SSE for client
338
- const mergedSSE = asyncIterableToSSEStream(mergeBuilds(env));
339
- ```
340
-
341
- ### Cancellation Support
342
-
343
- Both streaming methods support cancellation via AbortSignal:
344
-
345
- ```typescript
346
- const controller = new AbortController();
347
-
348
- // Cancel after 30 seconds
349
- setTimeout(() => controller.abort(), 30000);
350
-
351
- try {
352
- for await (const event of sandbox.execStream('long-running-task', {
353
- signal: controller.signal
354
- })) {
355
- // Process events
356
- if (shouldCancel(event)) {
357
- controller.abort();
358
- }
359
- }
360
- } catch (error) {
361
- if (error.message.includes('aborted')) {
362
- console.log('Operation cancelled');
363
- }
364
- }
365
- ```