@cloudflare/sandbox 0.0.9 → 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 +10 -0
  2. package/Dockerfile +1 -14
  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 +82 -2973
  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 +3 -1
  24. package/dist/index.js +13 -3
  25. package/dist/request-handler.d.ts +2 -1
  26. package/dist/request-handler.js +4 -2
  27. package/dist/sandbox.d.ts +2 -1
  28. package/dist/sandbox.js +4 -2
  29. package/dist/security.d.ts +30 -0
  30. package/dist/security.js +13 -0
  31. package/dist/security.js.map +1 -0
  32. package/dist/sse-parser.d.ts +28 -0
  33. package/dist/sse-parser.js +11 -0
  34. package/dist/sse-parser.js.map +1 -0
  35. package/dist/types.d.ts +284 -0
  36. package/dist/types.js +19 -0
  37. package/dist/types.js.map +1 -0
  38. package/package.json +2 -7
  39. package/src/client.ts +235 -1286
  40. package/src/index.ts +6 -0
  41. package/src/request-handler.ts +69 -20
  42. package/src/sandbox.ts +463 -70
  43. package/src/security.ts +113 -0
  44. package/src/sse-parser.ts +147 -0
  45. package/src/types.ts +386 -0
  46. package/README.md +0 -65
  47. package/dist/chunk-4J5LQCCN.js +0 -1446
  48. package/dist/chunk-4J5LQCCN.js.map +0 -1
  49. package/dist/chunk-5SZ3RVJZ.js +0 -250
  50. package/dist/chunk-5SZ3RVJZ.js.map +0 -1
  51. package/dist/client-BuVjqV00.d.ts +0 -247
  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
@@ -0,0 +1,182 @@
1
+ import { spawn } from "node:child_process";
2
+ import { randomBytes } from "node:crypto";
3
+ import type { GitCheckoutRequest, SessionData } from "../types";
4
+
5
+ function executeGitCheckout(
6
+ sessions: Map<string, SessionData>,
7
+ repoUrl: string,
8
+ branch: string,
9
+ targetDir: string,
10
+ sessionId?: string
11
+ ): Promise<{
12
+ success: boolean;
13
+ stdout: string;
14
+ stderr: string;
15
+ exitCode: number;
16
+ }> {
17
+ return new Promise((resolve, reject) => {
18
+ // First, clone the repository
19
+ const cloneChild = spawn(
20
+ "git",
21
+ ["clone", "-b", branch, repoUrl, targetDir],
22
+ {
23
+ shell: true,
24
+ stdio: ["pipe", "pipe", "pipe"],
25
+ }
26
+ );
27
+
28
+ // Store the process reference for cleanup if sessionId is provided
29
+ if (sessionId && sessions.has(sessionId)) {
30
+ const session = sessions.get(sessionId)!;
31
+ session.activeProcess = cloneChild;
32
+ }
33
+
34
+ let stdout = "";
35
+ let stderr = "";
36
+
37
+ cloneChild.stdout?.on("data", (data) => {
38
+ stdout += data.toString();
39
+ });
40
+
41
+ cloneChild.stderr?.on("data", (data) => {
42
+ stderr += data.toString();
43
+ });
44
+
45
+ cloneChild.on("close", (code) => {
46
+ // Clear the active process reference
47
+ if (sessionId && sessions.has(sessionId)) {
48
+ const session = sessions.get(sessionId)!;
49
+ session.activeProcess = null;
50
+ }
51
+
52
+ if (code === 0) {
53
+ console.log(
54
+ `[Server] Repository cloned successfully: ${repoUrl} to ${targetDir}`
55
+ );
56
+ resolve({
57
+ exitCode: code || 0,
58
+ stderr,
59
+ stdout,
60
+ success: true,
61
+ });
62
+ } else {
63
+ console.error(
64
+ `[Server] Failed to clone repository: ${repoUrl}, Exit code: ${code}`
65
+ );
66
+ resolve({
67
+ exitCode: code || 1,
68
+ stderr,
69
+ stdout,
70
+ success: false,
71
+ });
72
+ }
73
+ });
74
+
75
+ cloneChild.on("error", (error) => {
76
+ // Clear the active process reference
77
+ if (sessionId && sessions.has(sessionId)) {
78
+ const session = sessions.get(sessionId)!;
79
+ session.activeProcess = null;
80
+ }
81
+
82
+ console.error(`[Server] Error cloning repository: ${repoUrl}`, error);
83
+ reject(error);
84
+ });
85
+ });
86
+ }
87
+
88
+ export async function handleGitCheckoutRequest(
89
+ sessions: Map<string, SessionData>,
90
+ req: Request,
91
+ corsHeaders: Record<string, string>
92
+ ): Promise<Response> {
93
+ try {
94
+ const body = (await req.json()) as GitCheckoutRequest;
95
+ const { repoUrl, branch = "main", targetDir, sessionId } = body;
96
+
97
+ if (!repoUrl || typeof repoUrl !== "string") {
98
+ return new Response(
99
+ JSON.stringify({
100
+ error: "Repository URL is required and must be a string",
101
+ }),
102
+ {
103
+ headers: {
104
+ "Content-Type": "application/json",
105
+ ...corsHeaders,
106
+ },
107
+ status: 400,
108
+ }
109
+ );
110
+ }
111
+
112
+ // Validate repository URL format
113
+ const urlPattern =
114
+ /^(https?:\/\/|git@|ssh:\/\/).*\.git$|^https?:\/\/.*\/.*$/;
115
+ if (!urlPattern.test(repoUrl)) {
116
+ return new Response(
117
+ JSON.stringify({
118
+ error: "Invalid repository URL format",
119
+ }),
120
+ {
121
+ headers: {
122
+ "Content-Type": "application/json",
123
+ ...corsHeaders,
124
+ },
125
+ status: 400,
126
+ }
127
+ );
128
+ }
129
+
130
+ // Generate target directory if not provided using cryptographically secure randomness
131
+ const checkoutDir =
132
+ targetDir ||
133
+ `repo_${Date.now()}_${randomBytes(6).toString('hex')}`;
134
+
135
+ console.log(
136
+ `[Server] Checking out repository: ${repoUrl} to ${checkoutDir}`
137
+ );
138
+
139
+ const result = await executeGitCheckout(
140
+ sessions,
141
+ repoUrl,
142
+ branch,
143
+ checkoutDir,
144
+ sessionId
145
+ );
146
+
147
+ return new Response(
148
+ JSON.stringify({
149
+ branch,
150
+ exitCode: result.exitCode,
151
+ repoUrl,
152
+ stderr: result.stderr,
153
+ stdout: result.stdout,
154
+ success: result.success,
155
+ targetDir: checkoutDir,
156
+ timestamp: new Date().toISOString(),
157
+ }),
158
+ {
159
+ headers: {
160
+ "Content-Type": "application/json",
161
+ ...corsHeaders,
162
+ },
163
+ }
164
+ );
165
+ } catch (error) {
166
+ console.error("[Server] Error in handleGitCheckoutRequest:", error);
167
+ return new Response(
168
+ JSON.stringify({
169
+ error: "Failed to checkout repository",
170
+ message: error instanceof Error ? error.message : "Unknown error",
171
+ }),
172
+ {
173
+ headers: {
174
+ "Content-Type": "application/json",
175
+ ...corsHeaders,
176
+ },
177
+ status: 500,
178
+ }
179
+ );
180
+ }
181
+ }
182
+
@@ -0,0 +1,314 @@
1
+ import type { ExposePortRequest, UnexposePortRequest } from "../types";
2
+
3
+ export async function handleExposePortRequest(
4
+ exposedPorts: Map<number, { name?: string; exposedAt: Date }>,
5
+ req: Request,
6
+ corsHeaders: Record<string, string>
7
+ ): Promise<Response> {
8
+ try {
9
+ const body = (await req.json()) as ExposePortRequest;
10
+ const { port, name } = body;
11
+
12
+ if (!port || typeof port !== "number") {
13
+ return new Response(
14
+ JSON.stringify({
15
+ error: "Port is required and must be a number",
16
+ }),
17
+ {
18
+ headers: {
19
+ "Content-Type": "application/json",
20
+ ...corsHeaders,
21
+ },
22
+ status: 400,
23
+ }
24
+ );
25
+ }
26
+
27
+ // Validate port range
28
+ if (port < 1 || port > 65535) {
29
+ return new Response(
30
+ JSON.stringify({
31
+ error: "Port must be between 1 and 65535",
32
+ }),
33
+ {
34
+ headers: {
35
+ "Content-Type": "application/json",
36
+ ...corsHeaders,
37
+ },
38
+ status: 400,
39
+ }
40
+ );
41
+ }
42
+
43
+ // Store the exposed port
44
+ exposedPorts.set(port, { name, exposedAt: new Date() });
45
+
46
+ console.log(`[Server] Exposed port: ${port}${name ? ` (${name})` : ""}`);
47
+
48
+ return new Response(
49
+ JSON.stringify({
50
+ port,
51
+ name,
52
+ exposedAt: new Date().toISOString(),
53
+ success: true,
54
+ timestamp: new Date().toISOString(),
55
+ }),
56
+ {
57
+ headers: {
58
+ "Content-Type": "application/json",
59
+ ...corsHeaders,
60
+ },
61
+ }
62
+ );
63
+ } catch (error) {
64
+ console.error("[Server] Error in handleExposePortRequest:", error);
65
+ return new Response(
66
+ JSON.stringify({
67
+ error: "Failed to expose port",
68
+ message: error instanceof Error ? error.message : "Unknown error",
69
+ }),
70
+ {
71
+ headers: {
72
+ "Content-Type": "application/json",
73
+ ...corsHeaders,
74
+ },
75
+ status: 500,
76
+ }
77
+ );
78
+ }
79
+ }
80
+
81
+ export async function handleUnexposePortRequest(
82
+ exposedPorts: Map<number, { name?: string; exposedAt: Date }>,
83
+ req: Request,
84
+ corsHeaders: Record<string, string>
85
+ ): Promise<Response> {
86
+ try {
87
+ const body = (await req.json()) as UnexposePortRequest;
88
+ const { port } = body;
89
+
90
+ if (!port || typeof port !== "number") {
91
+ return new Response(
92
+ JSON.stringify({
93
+ error: "Port is required and must be a number",
94
+ }),
95
+ {
96
+ headers: {
97
+ "Content-Type": "application/json",
98
+ ...corsHeaders,
99
+ },
100
+ status: 400,
101
+ }
102
+ );
103
+ }
104
+
105
+ // Check if port is exposed
106
+ if (!exposedPorts.has(port)) {
107
+ return new Response(
108
+ JSON.stringify({
109
+ error: "Port is not exposed",
110
+ }),
111
+ {
112
+ headers: {
113
+ "Content-Type": "application/json",
114
+ ...corsHeaders,
115
+ },
116
+ status: 404,
117
+ }
118
+ );
119
+ }
120
+
121
+ // Remove the exposed port
122
+ exposedPorts.delete(port);
123
+
124
+ console.log(`[Server] Unexposed port: ${port}`);
125
+
126
+ return new Response(
127
+ JSON.stringify({
128
+ port,
129
+ success: true,
130
+ timestamp: new Date().toISOString(),
131
+ }),
132
+ {
133
+ headers: {
134
+ "Content-Type": "application/json",
135
+ ...corsHeaders,
136
+ },
137
+ }
138
+ );
139
+ } catch (error) {
140
+ console.error("[Server] Error in handleUnexposePortRequest:", error);
141
+ return new Response(
142
+ JSON.stringify({
143
+ error: "Failed to unexpose port",
144
+ message: error instanceof Error ? error.message : "Unknown error",
145
+ }),
146
+ {
147
+ headers: {
148
+ "Content-Type": "application/json",
149
+ ...corsHeaders,
150
+ },
151
+ status: 500,
152
+ }
153
+ );
154
+ }
155
+ }
156
+
157
+ export async function handleGetExposedPortsRequest(
158
+ exposedPorts: Map<number, { name?: string; exposedAt: Date }>,
159
+ req: Request,
160
+ corsHeaders: Record<string, string>
161
+ ): Promise<Response> {
162
+ try {
163
+ const ports = Array.from(exposedPorts.entries()).map(([port, info]) => ({
164
+ port,
165
+ name: info.name,
166
+ exposedAt: info.exposedAt.toISOString(),
167
+ }));
168
+
169
+ return new Response(
170
+ JSON.stringify({
171
+ ports,
172
+ count: ports.length,
173
+ timestamp: new Date().toISOString(),
174
+ }),
175
+ {
176
+ headers: {
177
+ "Content-Type": "application/json",
178
+ ...corsHeaders,
179
+ },
180
+ }
181
+ );
182
+ } catch (error) {
183
+ console.error("[Server] Error in handleGetExposedPortsRequest:", error);
184
+ return new Response(
185
+ JSON.stringify({
186
+ error: "Failed to get exposed ports",
187
+ message: error instanceof Error ? error.message : "Unknown error",
188
+ }),
189
+ {
190
+ headers: {
191
+ "Content-Type": "application/json",
192
+ ...corsHeaders,
193
+ },
194
+ status: 500,
195
+ }
196
+ );
197
+ }
198
+ }
199
+
200
+ export async function handleProxyRequest(
201
+ exposedPorts: Map<number, { name?: string; exposedAt: Date }>,
202
+ req: Request,
203
+ corsHeaders: Record<string, string>
204
+ ): Promise<Response> {
205
+ try {
206
+ const url = new URL(req.url);
207
+ const pathParts = url.pathname.split("/");
208
+
209
+ // Extract port from path like /proxy/3000/...
210
+ if (pathParts.length < 3) {
211
+ return new Response(
212
+ JSON.stringify({
213
+ error: "Invalid proxy path",
214
+ }),
215
+ {
216
+ headers: {
217
+ "Content-Type": "application/json",
218
+ ...corsHeaders,
219
+ },
220
+ status: 400,
221
+ }
222
+ );
223
+ }
224
+
225
+ const port = parseInt(pathParts[2]);
226
+ if (!port || Number.isNaN(port)) {
227
+ return new Response(
228
+ JSON.stringify({
229
+ error: "Invalid port in proxy path",
230
+ }),
231
+ {
232
+ headers: {
233
+ "Content-Type": "application/json",
234
+ ...corsHeaders,
235
+ },
236
+ status: 400,
237
+ }
238
+ );
239
+ }
240
+
241
+ // Check if port is exposed
242
+ if (!exposedPorts.has(port)) {
243
+ return new Response(
244
+ JSON.stringify({
245
+ error: `Port ${port} is not exposed`,
246
+ }),
247
+ {
248
+ headers: {
249
+ "Content-Type": "application/json",
250
+ ...corsHeaders,
251
+ },
252
+ status: 404,
253
+ }
254
+ );
255
+ }
256
+
257
+ // Construct the target URL
258
+ const targetPath = `/${pathParts.slice(3).join("/")}`;
259
+ // Use 127.0.0.1 instead of localhost for more reliable container networking
260
+ const targetUrl = `http://127.0.0.1:${port}${targetPath}${url.search}`;
261
+
262
+ console.log(`[Server] Proxying request to: ${targetUrl}`);
263
+ console.log(`[Server] Method: ${req.method}, Port: ${port}, Path: ${targetPath}`);
264
+
265
+ try {
266
+ // Forward the request to the target port
267
+ const targetResponse = await fetch(targetUrl, {
268
+ method: req.method,
269
+ headers: req.headers,
270
+ body: req.body,
271
+ });
272
+
273
+ // Return the response from the target
274
+ return new Response(targetResponse.body, {
275
+ status: targetResponse.status,
276
+ statusText: targetResponse.statusText,
277
+ headers: {
278
+ ...Object.fromEntries(targetResponse.headers.entries()),
279
+ ...corsHeaders,
280
+ },
281
+ });
282
+ } catch (fetchError) {
283
+ console.error(`[Server] Error proxying to port ${port}:`, fetchError);
284
+ return new Response(
285
+ JSON.stringify({
286
+ error: `Service on port ${port} is not responding`,
287
+ message: fetchError instanceof Error ? fetchError.message : "Unknown error",
288
+ }),
289
+ {
290
+ headers: {
291
+ "Content-Type": "application/json",
292
+ ...corsHeaders,
293
+ },
294
+ status: 502,
295
+ }
296
+ );
297
+ }
298
+ } catch (error) {
299
+ console.error("[Server] Error in handleProxyRequest:", error);
300
+ return new Response(
301
+ JSON.stringify({
302
+ error: "Failed to proxy request",
303
+ message: error instanceof Error ? error.message : "Unknown error",
304
+ }),
305
+ {
306
+ headers: {
307
+ "Content-Type": "application/json",
308
+ ...corsHeaders,
309
+ },
310
+ status: 500,
311
+ }
312
+ );
313
+ }
314
+ }