@cloudflare/sandbox 0.0.0-af082ab → 0.0.0-b61841c

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 (105) hide show
  1. package/CHANGELOG.md +63 -6
  2. package/Dockerfile +91 -51
  3. package/README.md +88 -825
  4. package/dist/chunk-BFVUNTP4.js +104 -0
  5. package/dist/chunk-BFVUNTP4.js.map +1 -0
  6. package/dist/chunk-EKSWCBCA.js +86 -0
  7. package/dist/chunk-EKSWCBCA.js.map +1 -0
  8. package/dist/chunk-JXZMAU2C.js +559 -0
  9. package/dist/chunk-JXZMAU2C.js.map +1 -0
  10. package/dist/chunk-QHRFHK6X.js +7 -0
  11. package/dist/chunk-QHRFHK6X.js.map +1 -0
  12. package/dist/chunk-SFCV5YTY.js +2456 -0
  13. package/dist/chunk-SFCV5YTY.js.map +1 -0
  14. package/dist/chunk-Z532A7QC.js +78 -0
  15. package/dist/chunk-Z532A7QC.js.map +1 -0
  16. package/dist/file-stream.d.ts +43 -0
  17. package/dist/file-stream.js +9 -0
  18. package/dist/file-stream.js.map +1 -0
  19. package/dist/index.d.ts +9 -0
  20. package/dist/index.js +67 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/interpreter.d.ts +33 -0
  23. package/dist/interpreter.js +8 -0
  24. package/dist/interpreter.js.map +1 -0
  25. package/dist/request-handler.d.ts +18 -0
  26. package/dist/request-handler.js +13 -0
  27. package/dist/request-handler.js.map +1 -0
  28. package/dist/sandbox-DWQVgVTY.d.ts +603 -0
  29. package/dist/sandbox.d.ts +4 -0
  30. package/dist/sandbox.js +13 -0
  31. package/dist/sandbox.js.map +1 -0
  32. package/dist/security.d.ts +31 -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/version.d.ts +8 -0
  39. package/dist/version.js +7 -0
  40. package/dist/version.js.map +1 -0
  41. package/package.json +12 -4
  42. package/src/clients/base-client.ts +280 -0
  43. package/src/clients/command-client.ts +115 -0
  44. package/src/clients/file-client.ts +295 -0
  45. package/src/clients/git-client.ts +92 -0
  46. package/src/clients/index.ts +64 -0
  47. package/src/{interpreter-client.ts → clients/interpreter-client.ts} +148 -171
  48. package/src/clients/port-client.ts +105 -0
  49. package/src/clients/process-client.ts +177 -0
  50. package/src/clients/sandbox-client.ts +41 -0
  51. package/src/clients/types.ts +84 -0
  52. package/src/clients/utility-client.ts +119 -0
  53. package/src/errors/adapter.ts +180 -0
  54. package/src/errors/classes.ts +469 -0
  55. package/src/errors/index.ts +105 -0
  56. package/src/file-stream.ts +119 -117
  57. package/src/index.ts +81 -69
  58. package/src/interpreter.ts +17 -8
  59. package/src/request-handler.ts +80 -44
  60. package/src/sandbox.ts +794 -537
  61. package/src/security.ts +14 -23
  62. package/src/sse-parser.ts +4 -8
  63. package/src/version.ts +6 -0
  64. package/startup.sh +3 -0
  65. package/tests/base-client.test.ts +328 -0
  66. package/tests/command-client.test.ts +407 -0
  67. package/tests/file-client.test.ts +719 -0
  68. package/tests/file-stream.test.ts +306 -0
  69. package/tests/get-sandbox.test.ts +110 -0
  70. package/tests/git-client.test.ts +328 -0
  71. package/tests/port-client.test.ts +301 -0
  72. package/tests/process-client.test.ts +658 -0
  73. package/tests/request-handler.test.ts +240 -0
  74. package/tests/sandbox.test.ts +554 -0
  75. package/tests/sse-parser.test.ts +290 -0
  76. package/tests/utility-client.test.ts +332 -0
  77. package/tests/version.test.ts +16 -0
  78. package/tests/wrangler.jsonc +35 -0
  79. package/tsconfig.json +9 -1
  80. package/vitest.config.ts +31 -0
  81. package/container_src/bun.lock +0 -76
  82. package/container_src/circuit-breaker.ts +0 -121
  83. package/container_src/control-process.ts +0 -784
  84. package/container_src/handler/exec.ts +0 -185
  85. package/container_src/handler/file.ts +0 -457
  86. package/container_src/handler/git.ts +0 -130
  87. package/container_src/handler/ports.ts +0 -314
  88. package/container_src/handler/process.ts +0 -568
  89. package/container_src/handler/session.ts +0 -92
  90. package/container_src/index.ts +0 -601
  91. package/container_src/interpreter-service.ts +0 -276
  92. package/container_src/isolation.ts +0 -1213
  93. package/container_src/mime-processor.ts +0 -255
  94. package/container_src/package.json +0 -18
  95. package/container_src/runtime/executors/javascript/node_executor.ts +0 -123
  96. package/container_src/runtime/executors/python/ipython_executor.py +0 -338
  97. package/container_src/runtime/executors/typescript/ts_executor.ts +0 -138
  98. package/container_src/runtime/process-pool.ts +0 -464
  99. package/container_src/shell-escape.ts +0 -42
  100. package/container_src/startup.sh +0 -11
  101. package/container_src/types.ts +0 -131
  102. package/src/client.ts +0 -1048
  103. package/src/errors.ts +0 -219
  104. package/src/interpreter-types.ts +0 -390
  105. package/src/types.ts +0 -571
@@ -1,6 +1,7 @@
1
+ import { createLogger, type LogContext, TraceContext } from "@repo/shared";
2
+ import { switchPort } from "@cloudflare/containers";
1
3
  import { getSandbox, type Sandbox } from "./sandbox";
2
4
  import {
3
- logSecurityEvent,
4
5
  sanitizeSandboxId,
5
6
  validatePort
6
7
  } from "./security";
@@ -13,12 +14,21 @@ export interface RouteInfo {
13
14
  port: number;
14
15
  sandboxId: string;
15
16
  path: string;
17
+ token: string;
16
18
  }
17
19
 
18
20
  export async function proxyToSandbox<E extends SandboxEnv>(
19
21
  request: Request,
20
22
  env: E
21
23
  ): Promise<Response | null> {
24
+ // Create logger context for this request
25
+ const traceId = TraceContext.fromHeaders(request.headers) || TraceContext.generate();
26
+ const logger = createLogger({
27
+ component: 'sandbox-do',
28
+ traceId,
29
+ operation: 'proxy'
30
+ });
31
+
22
32
  try {
23
33
  const url = new URL(request.url);
24
34
  const routeInfo = extractSandboxRoute(url);
@@ -27,9 +37,48 @@ export async function proxyToSandbox<E extends SandboxEnv>(
27
37
  return null; // Not a request to an exposed container port
28
38
  }
29
39
 
30
- const { sandboxId, port, path } = routeInfo;
40
+ const { sandboxId, port, path, token } = routeInfo;
31
41
  const sandbox = getSandbox(env.Sandbox, sandboxId);
32
42
 
43
+ // Critical security check: Validate token (mandatory for all user ports)
44
+ // Skip check for control plane port 3000
45
+ if (port !== 3000) {
46
+ // Validate the token matches the port
47
+ const isValidToken = await sandbox.validatePortToken(port, token);
48
+ if (!isValidToken) {
49
+ logger.warn('Invalid token access blocked', {
50
+ port,
51
+ sandboxId,
52
+ path,
53
+ hostname: url.hostname,
54
+ url: request.url,
55
+ method: request.method,
56
+ userAgent: request.headers.get('User-Agent') || 'unknown'
57
+ });
58
+
59
+ return new Response(
60
+ JSON.stringify({
61
+ error: `Access denied: Invalid token or port not exposed`,
62
+ code: 'INVALID_TOKEN'
63
+ }),
64
+ {
65
+ status: 404,
66
+ headers: {
67
+ 'Content-Type': 'application/json'
68
+ }
69
+ }
70
+ );
71
+ }
72
+ }
73
+
74
+ // Detect WebSocket upgrade request
75
+ const upgradeHeader = request.headers.get('Upgrade');
76
+ if (upgradeHeader?.toLowerCase() === 'websocket') {
77
+ // WebSocket path: Must use fetch() not containerFetch()
78
+ // This bypasses JSRPC serialization boundary which cannot handle WebSocket upgrades
79
+ return await sandbox.fetch(switchPort(request, port));
80
+ }
81
+
33
82
  // Build proxy request with proper headers
34
83
  let proxyUrl: string;
35
84
 
@@ -52,43 +101,33 @@ export async function proxyToSandbox<E extends SandboxEnv>(
52
101
  'X-Sandbox-Name': sandboxId, // Pass the friendly name
53
102
  },
54
103
  body: request.body,
104
+ // @ts-expect-error - duplex required for body streaming in modern runtimes
105
+ duplex: 'half',
55
106
  });
56
107
 
57
- return sandbox.containerFetch(proxyRequest, port);
108
+ return await sandbox.containerFetch(proxyRequest, port);
58
109
  } catch (error) {
59
- console.error('[Sandbox] Proxy routing error:', error);
110
+ logger.error('Proxy routing error', error instanceof Error ? error : new Error(String(error)));
60
111
  return new Response('Proxy routing error', { status: 500 });
61
112
  }
62
113
  }
63
114
 
64
115
  function extractSandboxRoute(url: URL): RouteInfo | null {
65
- // Parse subdomain pattern: port-sandboxId.domain
66
- const subdomainMatch = url.hostname.match(/^(\d{4,5})-([^.-][^.]*[^.-]|[^.-])\.(.+)$/);
116
+ // Parse subdomain pattern: port-sandboxId-token.domain (tokens mandatory)
117
+ // Token is always exactly 16 chars (generated by generatePortToken)
118
+ const subdomainMatch = url.hostname.match(/^(\d{4,5})-([^.-][^.]*?[^.-]|[^.-])-([a-z0-9_-]{16})\.(.+)$/);
67
119
 
68
120
  if (!subdomainMatch) {
69
- // Log malformed subdomain attempts
70
- if (url.hostname.includes('-') && url.hostname.includes('.')) {
71
- logSecurityEvent('MALFORMED_SUBDOMAIN_ATTEMPT', {
72
- hostname: url.hostname,
73
- url: url.toString()
74
- }, 'medium');
75
- }
76
121
  return null;
77
122
  }
78
123
 
79
124
  const portStr = subdomainMatch[1];
80
125
  const sandboxId = subdomainMatch[2];
81
- const domain = subdomainMatch[3];
126
+ const token = subdomainMatch[3]; // Mandatory token
127
+ const domain = subdomainMatch[4];
82
128
 
83
129
  const port = parseInt(portStr, 10);
84
130
  if (!validatePort(port)) {
85
- logSecurityEvent('INVALID_PORT_IN_SUBDOMAIN', {
86
- port,
87
- portStr,
88
- sandboxId,
89
- hostname: url.hostname,
90
- url: url.toString()
91
- }, 'high');
92
131
  return null;
93
132
  }
94
133
 
@@ -96,49 +135,46 @@ function extractSandboxRoute(url: URL): RouteInfo | null {
96
135
  try {
97
136
  sanitizedSandboxId = sanitizeSandboxId(sandboxId);
98
137
  } catch (error) {
99
- logSecurityEvent('INVALID_SANDBOX_ID_IN_SUBDOMAIN', {
100
- sandboxId,
101
- port,
102
- hostname: url.hostname,
103
- url: url.toString(),
104
- error: error instanceof Error ? error.message : 'Unknown error'
105
- }, 'high');
106
138
  return null;
107
139
  }
108
140
 
109
141
  // DNS subdomain length limit is 63 characters
110
142
  if (sandboxId.length > 63) {
111
- logSecurityEvent('SANDBOX_ID_LENGTH_VIOLATION', {
112
- sandboxId,
113
- length: sandboxId.length,
114
- port,
115
- hostname: url.hostname
116
- }, 'medium');
117
143
  return null;
118
144
  }
119
145
 
120
- logSecurityEvent('SANDBOX_ROUTE_EXTRACTED', {
121
- port,
122
- sandboxId: sanitizedSandboxId,
123
- domain,
124
- path: url.pathname || "/",
125
- hostname: url.hostname
126
- }, 'low');
127
-
128
146
  return {
129
147
  port,
130
148
  sandboxId: sanitizedSandboxId,
131
149
  path: url.pathname || "/",
150
+ token,
132
151
  };
133
152
  }
134
153
 
135
154
  export function isLocalhostPattern(hostname: string): boolean {
155
+ // Handle IPv6 addresses in brackets (with or without port)
156
+ if (hostname.startsWith('[')) {
157
+ if (hostname.includes(']:')) {
158
+ // [::1]:port format
159
+ const ipv6Part = hostname.substring(0, hostname.indexOf(']:') + 1);
160
+ return ipv6Part === '[::1]';
161
+ } else {
162
+ // [::1] format without port
163
+ return hostname === '[::1]';
164
+ }
165
+ }
166
+
167
+ // Handle bare IPv6 without brackets
168
+ if (hostname === '::1') {
169
+ return true;
170
+ }
171
+
172
+ // For IPv4 and regular hostnames, split on colon to remove port
136
173
  const hostPart = hostname.split(":")[0];
174
+
137
175
  return (
138
176
  hostPart === "localhost" ||
139
177
  hostPart === "127.0.0.1" ||
140
- hostPart === "::1" ||
141
- hostPart === "[::1]" ||
142
178
  hostPart === "0.0.0.0"
143
179
  );
144
180
  }