@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.
- package/CHANGELOG.md +16 -0
- package/Dockerfile +73 -9
- package/container_src/handler/exec.ts +337 -0
- package/container_src/handler/file.ts +844 -0
- package/container_src/handler/git.ts +182 -0
- package/container_src/handler/ports.ts +314 -0
- package/container_src/handler/process.ts +640 -0
- package/container_src/index.ts +102 -2647
- package/container_src/types.ts +103 -0
- package/dist/chunk-6THNBO4S.js +46 -0
- package/dist/chunk-6THNBO4S.js.map +1 -0
- package/dist/chunk-6UAWTJ5S.js +85 -0
- package/dist/chunk-6UAWTJ5S.js.map +1 -0
- package/dist/chunk-G4XT4SP7.js +638 -0
- package/dist/chunk-G4XT4SP7.js.map +1 -0
- package/dist/chunk-ISFOIYQC.js +585 -0
- package/dist/chunk-ISFOIYQC.js.map +1 -0
- package/dist/chunk-NNGBXDMY.js +89 -0
- package/dist/chunk-NNGBXDMY.js.map +1 -0
- package/dist/client-Da-mLX4p.d.ts +210 -0
- package/dist/client.d.ts +2 -1
- package/dist/client.js +3 -37
- package/dist/index.d.ts +5 -200
- package/dist/index.js +17 -106
- package/dist/index.js.map +1 -1
- package/dist/request-handler.d.ts +16 -0
- package/dist/request-handler.js +12 -0
- package/dist/request-handler.js.map +1 -0
- package/dist/sandbox.d.ts +3 -0
- package/dist/sandbox.js +12 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/security.d.ts +30 -0
- package/dist/security.js +13 -0
- package/dist/security.js.map +1 -0
- package/dist/sse-parser.d.ts +28 -0
- package/dist/sse-parser.js +11 -0
- package/dist/sse-parser.js.map +1 -0
- package/dist/types.d.ts +284 -0
- package/dist/types.js +19 -0
- package/dist/types.js.map +1 -0
- package/package.json +2 -7
- package/src/client.ts +320 -1242
- package/src/index.ts +20 -136
- package/src/request-handler.ts +144 -0
- package/src/sandbox.ts +645 -0
- package/src/security.ts +113 -0
- package/src/sse-parser.ts +147 -0
- package/src/types.ts +386 -0
- package/README.md +0 -65
- package/dist/chunk-7WZJ3TRE.js +0 -1364
- package/dist/chunk-7WZJ3TRE.js.map +0 -1
- package/tests/client.example.ts +0 -308
- package/tests/connection-test.ts +0 -81
- package/tests/simple-test.ts +0 -81
- package/tests/test1.ts +0 -281
- 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
|
-
#
|
|
1
|
+
# Sandbox base image with development tools, Python, Node.js, and Bun
|
|
2
|
+
FROM ubuntu:22.04
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
#
|
|
8
|
-
RUN
|
|
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
|
-
|
|
11
|
-
|
|
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
|
+
}
|