@cloudflare/sandbox 0.0.8 → 0.1.0

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.
Files changed (56) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/Dockerfile +73 -9
  3. package/container_src/handler/exec.ts +337 -0
  4. package/container_src/handler/file.ts +844 -0
  5. package/container_src/handler/git.ts +182 -0
  6. package/container_src/handler/ports.ts +314 -0
  7. package/container_src/handler/process.ts +640 -0
  8. package/container_src/index.ts +102 -2647
  9. package/container_src/types.ts +103 -0
  10. package/dist/chunk-6THNBO4S.js +46 -0
  11. package/dist/chunk-6THNBO4S.js.map +1 -0
  12. package/dist/chunk-6UAWTJ5S.js +85 -0
  13. package/dist/chunk-6UAWTJ5S.js.map +1 -0
  14. package/dist/chunk-G4XT4SP7.js +638 -0
  15. package/dist/chunk-G4XT4SP7.js.map +1 -0
  16. package/dist/chunk-ISFOIYQC.js +585 -0
  17. package/dist/chunk-ISFOIYQC.js.map +1 -0
  18. package/dist/chunk-NNGBXDMY.js +89 -0
  19. package/dist/chunk-NNGBXDMY.js.map +1 -0
  20. package/dist/client-Da-mLX4p.d.ts +210 -0
  21. package/dist/client.d.ts +2 -1
  22. package/dist/client.js +3 -37
  23. package/dist/index.d.ts +5 -200
  24. package/dist/index.js +17 -106
  25. package/dist/index.js.map +1 -1
  26. package/dist/request-handler.d.ts +16 -0
  27. package/dist/request-handler.js +12 -0
  28. package/dist/request-handler.js.map +1 -0
  29. package/dist/sandbox.d.ts +3 -0
  30. package/dist/sandbox.js +12 -0
  31. package/dist/sandbox.js.map +1 -0
  32. package/dist/security.d.ts +30 -0
  33. package/dist/security.js +13 -0
  34. package/dist/security.js.map +1 -0
  35. package/dist/sse-parser.d.ts +28 -0
  36. package/dist/sse-parser.js +11 -0
  37. package/dist/sse-parser.js.map +1 -0
  38. package/dist/types.d.ts +284 -0
  39. package/dist/types.js +19 -0
  40. package/dist/types.js.map +1 -0
  41. package/package.json +2 -7
  42. package/src/client.ts +320 -1242
  43. package/src/index.ts +20 -136
  44. package/src/request-handler.ts +144 -0
  45. package/src/sandbox.ts +645 -0
  46. package/src/security.ts +113 -0
  47. package/src/sse-parser.ts +147 -0
  48. package/src/types.ts +386 -0
  49. package/README.md +0 -65
  50. package/dist/chunk-7WZJ3TRE.js +0 -1364
  51. package/dist/chunk-7WZJ3TRE.js.map +0 -1
  52. package/tests/client.example.ts +0 -308
  53. package/tests/connection-test.ts +0 -81
  54. package/tests/simple-test.ts +0 -81
  55. package/tests/test1.ts +0 -281
  56. package/tests/test2.ts +0 -929
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # @cloudflare/sandbox
2
2
 
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#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
8
+
9
+ ### Patch Changes
10
+
11
+ - [#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
12
+
13
+ ## 0.0.9
14
+
15
+ ### Patch Changes
16
+
17
+ - [#20](https://github.com/cloudflare/sandbox-sdk/pull/20) [`f106fda`](https://github.com/cloudflare/sandbox-sdk/commit/f106fdac98e7ef35677326290d45cbf3af88982c) Thanks [@ghostwriternr](https://github.com/ghostwriternr)! - add preview URLs and dynamic port forwarding
18
+
3
19
  ## 0.0.8
4
20
 
5
21
  ### Patch Changes
package/Dockerfile CHANGED
@@ -1,16 +1,80 @@
1
- # syntax=docker/dockerfile:1
1
+ # Sandbox base image with development tools, Python, Node.js, and Bun
2
+ FROM ubuntu:22.04
2
3
 
3
- FROM oven/bun:latest
4
- # Set destination for COPY
4
+ # Prevent interactive prompts during package installation
5
+ ENV DEBIAN_FRONTEND=noninteractive
6
+
7
+ # Install essential system packages and development tools
8
+ RUN apt-get update && apt-get install -y \
9
+ # Basic utilities
10
+ curl \
11
+ wget \
12
+ git \
13
+ unzip \
14
+ zip \
15
+ # Process management
16
+ procps \
17
+ htop \
18
+ # Build tools
19
+ build-essential \
20
+ pkg-config \
21
+ # Network tools
22
+ net-tools \
23
+ iputils-ping \
24
+ dnsutils \
25
+ # Text processing
26
+ jq \
27
+ vim \
28
+ nano \
29
+ # Python dependencies
30
+ python3.11 \
31
+ python3.11-dev \
32
+ python3-pip \
33
+ # Other useful tools
34
+ sudo \
35
+ ca-certificates \
36
+ gnupg \
37
+ lsb-release \
38
+ && rm -rf /var/lib/apt/lists/*
39
+
40
+ # Set Python 3.11 as default python3
41
+ RUN update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.11 1
42
+
43
+ # Install Node.js 22 LTS
44
+ # Using the official NodeSource repository setup script
45
+ RUN apt-get update && apt-get install -y ca-certificates curl gnupg \
46
+ && mkdir -p /etc/apt/keyrings \
47
+ && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
48
+ && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list \
49
+ && apt-get update \
50
+ && apt-get install -y nodejs \
51
+ && rm -rf /var/lib/apt/lists/*
52
+
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
58
+
59
+ # Install global npm packages as root
60
+ RUN npm install -g yarn pnpm
61
+
62
+ # Set up working directory
5
63
  WORKDIR /app
6
64
 
7
- # Install git
8
- RUN apt-get update && apt-get install -y git
65
+ # Verify installations
66
+ RUN python3 --version && \
67
+ node --version && \
68
+ npm --version && \
69
+ bun --version && \
70
+ yarn --version && \
71
+ pnpm --version
9
72
 
10
- COPY container_src/* ./
11
- # RUN bun install
73
+ # Copy container source files
74
+ COPY container_src/ ./
12
75
 
76
+ # Expose the application port
13
77
  EXPOSE 3000
14
- # Run
15
- CMD ["bun", "index.ts"]
16
78
 
79
+ # Run the application
80
+ CMD ["bun", "index.ts"]
@@ -0,0 +1,337 @@
1
+ import { type SpawnOptions, spawn } from "node:child_process";
2
+ import type { ExecuteRequest, SessionData } from "../types";
3
+
4
+ function executeCommand(
5
+ sessions: Map<string, SessionData>,
6
+ command: string,
7
+ sessionId?: string,
8
+ background?: boolean
9
+ ): Promise<{
10
+ success: boolean;
11
+ stdout: string;
12
+ stderr: string;
13
+ exitCode: number;
14
+ }> {
15
+ return new Promise((resolve, reject) => {
16
+ const spawnOptions: SpawnOptions = {
17
+ shell: true,
18
+ stdio: ["pipe", "pipe", "pipe"] as const,
19
+ detached: background || false,
20
+ };
21
+
22
+ const child = spawn(command, spawnOptions);
23
+
24
+ // Store the process reference for cleanup if sessionId is provided
25
+ if (sessionId && sessions.has(sessionId)) {
26
+ const session = sessions.get(sessionId)!;
27
+ session.activeProcess = child;
28
+ }
29
+
30
+ let stdout = "";
31
+ let stderr = "";
32
+
33
+ child.stdout?.on("data", (data) => {
34
+ stdout += data.toString();
35
+ });
36
+
37
+ child.stderr?.on("data", (data) => {
38
+ stderr += data.toString();
39
+ });
40
+
41
+ if (background) {
42
+ // For background processes, unref and return quickly
43
+ child.unref();
44
+
45
+ // Collect initial output for 100ms then return
46
+ setTimeout(() => {
47
+ resolve({
48
+ exitCode: 0, // Process is still running
49
+ stderr,
50
+ stdout,
51
+ success: true,
52
+ });
53
+ }, 100);
54
+
55
+ // Still handle errors
56
+ child.on("error", (error) => {
57
+ console.error(`[Server] Background process error: ${command}`, error);
58
+ // Don't reject since we might have already resolved
59
+ });
60
+ } else {
61
+ // Normal synchronous execution
62
+ child.on("close", (code) => {
63
+ // Clear the active process reference
64
+ if (sessionId && sessions.has(sessionId)) {
65
+ const session = sessions.get(sessionId)!;
66
+ session.activeProcess = null;
67
+ }
68
+
69
+ console.log(`[Server] Command completed: ${command}, Exit code: ${code}`);
70
+
71
+ resolve({
72
+ exitCode: code || 0,
73
+ stderr,
74
+ stdout,
75
+ success: code === 0,
76
+ });
77
+ });
78
+
79
+ child.on("error", (error) => {
80
+ // Clear the active process reference
81
+ if (sessionId && sessions.has(sessionId)) {
82
+ const session = sessions.get(sessionId)!;
83
+ session.activeProcess = null;
84
+ }
85
+
86
+ reject(error);
87
+ });
88
+ }
89
+ });
90
+ }
91
+
92
+ export async function handleExecuteRequest(
93
+ sessions: Map<string, SessionData>,
94
+ req: Request,
95
+ corsHeaders: Record<string, string>
96
+ ): Promise<Response> {
97
+ try {
98
+ const body = (await req.json()) as ExecuteRequest;
99
+ const { command, sessionId, background } = body;
100
+
101
+ if (!command || typeof command !== "string") {
102
+ return new Response(
103
+ JSON.stringify({
104
+ error: "Command is required and must be a string",
105
+ }),
106
+ {
107
+ headers: {
108
+ "Content-Type": "application/json",
109
+ ...corsHeaders,
110
+ },
111
+ status: 400,
112
+ }
113
+ );
114
+ }
115
+
116
+ console.log(`[Server] Executing command: ${command}`);
117
+
118
+ const result = await executeCommand(sessions, command, sessionId, background);
119
+
120
+ return new Response(
121
+ JSON.stringify({
122
+ command,
123
+ exitCode: result.exitCode,
124
+ stderr: result.stderr,
125
+ stdout: result.stdout,
126
+ success: result.success,
127
+ timestamp: new Date().toISOString(),
128
+ }),
129
+ {
130
+ headers: {
131
+ "Content-Type": "application/json",
132
+ ...corsHeaders,
133
+ },
134
+ }
135
+ );
136
+ } catch (error) {
137
+ console.error("[Server] Error in handleExecuteRequest:", error);
138
+ return new Response(
139
+ JSON.stringify({
140
+ error: "Failed to execute command",
141
+ message: error instanceof Error ? error.message : "Unknown error",
142
+ }),
143
+ {
144
+ headers: {
145
+ "Content-Type": "application/json",
146
+ ...corsHeaders,
147
+ },
148
+ status: 500,
149
+ }
150
+ );
151
+ }
152
+ }
153
+
154
+ export async function handleStreamingExecuteRequest(
155
+ sessions: Map<string, SessionData>,
156
+ req: Request,
157
+ corsHeaders: Record<string, string>
158
+ ): Promise<Response> {
159
+ try {
160
+ const body = (await req.json()) as ExecuteRequest;
161
+ const { command, sessionId, background } = body;
162
+
163
+ if (!command || typeof command !== "string") {
164
+ return new Response(
165
+ JSON.stringify({
166
+ error: "Command is required and must be a string",
167
+ }),
168
+ {
169
+ headers: {
170
+ "Content-Type": "application/json",
171
+ ...corsHeaders,
172
+ },
173
+ status: 400,
174
+ }
175
+ );
176
+ }
177
+
178
+ console.log(
179
+ `[Server] Executing streaming command: ${command}`
180
+ );
181
+
182
+ const stream = new ReadableStream({
183
+ start(controller) {
184
+ const spawnOptions: SpawnOptions = {
185
+ shell: true,
186
+ stdio: ["pipe", "pipe", "pipe"] as const,
187
+ detached: background || false,
188
+ };
189
+
190
+ const child = spawn(command, spawnOptions);
191
+
192
+ // Store the process reference for cleanup if sessionId is provided
193
+ if (sessionId && sessions.has(sessionId)) {
194
+ const session = sessions.get(sessionId)!;
195
+ session.activeProcess = child;
196
+ }
197
+
198
+ // For background processes, unref to prevent blocking
199
+ if (background) {
200
+ child.unref();
201
+ }
202
+
203
+ let stdout = "";
204
+ let stderr = "";
205
+
206
+ // Send command start event
207
+ controller.enqueue(
208
+ new TextEncoder().encode(
209
+ `data: ${JSON.stringify({
210
+ type: "start",
211
+ timestamp: new Date().toISOString(),
212
+ command,
213
+ background: background || false,
214
+ })}\n\n`
215
+ )
216
+ );
217
+
218
+ child.stdout?.on("data", (data) => {
219
+ const output = data.toString();
220
+ stdout += output;
221
+
222
+ // Send real-time output
223
+ controller.enqueue(
224
+ new TextEncoder().encode(
225
+ `data: ${JSON.stringify({
226
+ type: "stdout",
227
+ timestamp: new Date().toISOString(),
228
+ data: output,
229
+ command,
230
+ })}\n\n`
231
+ )
232
+ );
233
+ });
234
+
235
+ child.stderr?.on("data", (data) => {
236
+ const output = data.toString();
237
+ stderr += output;
238
+
239
+ // Send real-time error output
240
+ controller.enqueue(
241
+ new TextEncoder().encode(
242
+ `data: ${JSON.stringify({
243
+ type: "stderr",
244
+ timestamp: new Date().toISOString(),
245
+ data: output,
246
+ command,
247
+ })}\n\n`
248
+ )
249
+ );
250
+ });
251
+
252
+ child.on("close", (code) => {
253
+ // Clear the active process reference
254
+ if (sessionId && sessions.has(sessionId)) {
255
+ const session = sessions.get(sessionId)!;
256
+ session.activeProcess = null;
257
+ }
258
+
259
+ console.log(
260
+ `[Server] Command completed: ${command}, Exit code: ${code}`
261
+ );
262
+
263
+ // Send command completion event
264
+ controller.enqueue(
265
+ new TextEncoder().encode(
266
+ `data: ${JSON.stringify({
267
+ type: "complete",
268
+ timestamp: new Date().toISOString(),
269
+ command,
270
+ exitCode: code,
271
+ result: {
272
+ success: code === 0,
273
+ exitCode: code,
274
+ stdout,
275
+ stderr,
276
+ command,
277
+ timestamp: new Date().toISOString(),
278
+ },
279
+ })}\n\n`
280
+ )
281
+ );
282
+
283
+ // For non-background processes, close the stream
284
+ // For background processes with streaming, the stream stays open
285
+ if (!background) {
286
+ controller.close();
287
+ }
288
+ });
289
+
290
+ child.on("error", (error) => {
291
+ // Clear the active process reference
292
+ if (sessionId && sessions.has(sessionId)) {
293
+ const session = sessions.get(sessionId)!;
294
+ session.activeProcess = null;
295
+ }
296
+
297
+ controller.enqueue(
298
+ new TextEncoder().encode(
299
+ `data: ${JSON.stringify({
300
+ type: "error",
301
+ timestamp: new Date().toISOString(),
302
+ error: error.message,
303
+ command,
304
+ })}\n\n`
305
+ )
306
+ );
307
+
308
+ controller.close();
309
+ });
310
+ },
311
+ });
312
+
313
+ return new Response(stream, {
314
+ headers: {
315
+ "Cache-Control": "no-cache",
316
+ Connection: "keep-alive",
317
+ "Content-Type": "text/event-stream",
318
+ ...corsHeaders,
319
+ },
320
+ });
321
+ } catch (error) {
322
+ console.error("[Server] Error in handleStreamingExecuteRequest:", error);
323
+ return new Response(
324
+ JSON.stringify({
325
+ error: "Failed to execute streaming command",
326
+ message: error instanceof Error ? error.message : "Unknown error",
327
+ }),
328
+ {
329
+ headers: {
330
+ "Content-Type": "application/json",
331
+ ...corsHeaders,
332
+ },
333
+ status: 500,
334
+ }
335
+ );
336
+ }
337
+ }