@cloudflare/sandbox 0.0.9 → 0.1.1
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 +18 -0
- package/Dockerfile +1 -14
- 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 +82 -2973
- 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 +3 -1
- package/dist/index.js +13 -3
- package/dist/request-handler.d.ts +2 -1
- package/dist/request-handler.js +4 -2
- package/dist/sandbox.d.ts +2 -1
- package/dist/sandbox.js +4 -2
- 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 +235 -1286
- package/src/index.ts +6 -0
- package/src/request-handler.ts +69 -20
- package/src/sandbox.ts +463 -70
- package/src/security.ts +113 -0
- package/src/sse-parser.ts +147 -0
- package/src/types.ts +386 -0
- package/tsconfig.json +1 -1
- package/README.md +0 -65
- package/dist/chunk-4J5LQCCN.js +0 -1446
- package/dist/chunk-4J5LQCCN.js.map +0 -1
- package/dist/chunk-5SZ3RVJZ.js +0 -250
- package/dist/chunk-5SZ3RVJZ.js.map +0 -1
- package/dist/client-BuVjqV00.d.ts +0 -247
- 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/src/security.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security utilities for URL construction and input validation
|
|
3
|
+
*
|
|
4
|
+
* This module contains critical security functions to prevent:
|
|
5
|
+
* - URL injection attacks
|
|
6
|
+
* - SSRF (Server-Side Request Forgery) attacks
|
|
7
|
+
* - DNS rebinding attacks
|
|
8
|
+
* - Host header injection
|
|
9
|
+
* - Open redirect vulnerabilities
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export class SecurityError extends Error {
|
|
13
|
+
constructor(message: string, public readonly code?: string) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = 'SecurityError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Validates port numbers for sandbox services
|
|
21
|
+
* Only allows non-system ports to prevent conflicts and security issues
|
|
22
|
+
*/
|
|
23
|
+
export function validatePort(port: number): boolean {
|
|
24
|
+
// Must be a valid integer
|
|
25
|
+
if (!Number.isInteger(port)) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Only allow non-system ports (1024-65535)
|
|
30
|
+
if (port < 1024 || port > 65535) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Exclude ports reserved by our system
|
|
35
|
+
const reservedPorts = [
|
|
36
|
+
3000, // Control plane port
|
|
37
|
+
8787, // Common wrangler dev port
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
if (reservedPorts.includes(port)) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Sanitizes and validates sandbox IDs for DNS compliance and security
|
|
49
|
+
* Only enforces critical requirements - allows maximum developer flexibility
|
|
50
|
+
*/
|
|
51
|
+
export function sanitizeSandboxId(id: string): string {
|
|
52
|
+
// Basic validation: not empty, reasonable length limit (DNS subdomain limit is 63 chars)
|
|
53
|
+
if (!id || id.length > 63) {
|
|
54
|
+
throw new SecurityError(
|
|
55
|
+
'Sandbox ID must be 1-63 characters long.',
|
|
56
|
+
'INVALID_SANDBOX_ID_LENGTH'
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// DNS compliance: cannot start or end with hyphens (RFC requirement)
|
|
61
|
+
if (id.startsWith('-') || id.endsWith('-')) {
|
|
62
|
+
throw new SecurityError(
|
|
63
|
+
'Sandbox ID cannot start or end with hyphens (DNS requirement).',
|
|
64
|
+
'INVALID_SANDBOX_ID_HYPHENS'
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Prevent reserved names that cause technical conflicts
|
|
69
|
+
const reservedNames = [
|
|
70
|
+
'www', 'api', 'admin', 'root', 'system',
|
|
71
|
+
'cloudflare', 'workers'
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
const lowerCaseId = id.toLowerCase();
|
|
75
|
+
if (reservedNames.includes(lowerCaseId)) {
|
|
76
|
+
throw new SecurityError(
|
|
77
|
+
`Reserved sandbox ID '${id}' is not allowed.`,
|
|
78
|
+
'RESERVED_SANDBOX_ID'
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return id;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Logs security events for monitoring
|
|
88
|
+
*/
|
|
89
|
+
export function logSecurityEvent(
|
|
90
|
+
event: string,
|
|
91
|
+
details: Record<string, any>,
|
|
92
|
+
severity: 'low' | 'medium' | 'high' | 'critical' = 'medium'
|
|
93
|
+
): void {
|
|
94
|
+
const logEntry = {
|
|
95
|
+
timestamp: new Date().toISOString(),
|
|
96
|
+
event,
|
|
97
|
+
severity,
|
|
98
|
+
...details
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
switch (severity) {
|
|
102
|
+
case 'critical':
|
|
103
|
+
case 'high':
|
|
104
|
+
console.error(`[SECURITY:${severity.toUpperCase()}] ${event}:`, JSON.stringify(logEntry));
|
|
105
|
+
break;
|
|
106
|
+
case 'medium':
|
|
107
|
+
console.warn(`[SECURITY:${severity.toUpperCase()}] ${event}:`, JSON.stringify(logEntry));
|
|
108
|
+
break;
|
|
109
|
+
case 'low':
|
|
110
|
+
console.info(`[SECURITY:${severity.toUpperCase()}] ${event}:`, JSON.stringify(logEntry));
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-Sent Events (SSE) parser for streaming responses
|
|
3
|
+
* Converts ReadableStream<Uint8Array> to typed AsyncIterable<T>
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parse a ReadableStream of SSE events into typed AsyncIterable
|
|
8
|
+
* @param stream - The ReadableStream from fetch response
|
|
9
|
+
* @param signal - Optional AbortSignal for cancellation
|
|
10
|
+
*/
|
|
11
|
+
export async function* parseSSEStream<T>(
|
|
12
|
+
stream: ReadableStream<Uint8Array>,
|
|
13
|
+
signal?: AbortSignal
|
|
14
|
+
): AsyncIterable<T> {
|
|
15
|
+
const reader = stream.getReader();
|
|
16
|
+
const decoder = new TextDecoder();
|
|
17
|
+
let buffer = '';
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
while (true) {
|
|
21
|
+
// Check for cancellation
|
|
22
|
+
if (signal?.aborted) {
|
|
23
|
+
throw new Error('Operation was aborted');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const { done, value } = await reader.read();
|
|
27
|
+
if (done) break;
|
|
28
|
+
|
|
29
|
+
// Decode chunk and add to buffer
|
|
30
|
+
buffer += decoder.decode(value, { stream: true });
|
|
31
|
+
|
|
32
|
+
// Process complete SSE events in buffer
|
|
33
|
+
const lines = buffer.split('\n');
|
|
34
|
+
|
|
35
|
+
// Keep the last incomplete line in buffer
|
|
36
|
+
buffer = lines.pop() || '';
|
|
37
|
+
|
|
38
|
+
for (const line of lines) {
|
|
39
|
+
// Skip empty lines
|
|
40
|
+
if (line.trim() === '') continue;
|
|
41
|
+
|
|
42
|
+
// Process SSE data lines
|
|
43
|
+
if (line.startsWith('data: ')) {
|
|
44
|
+
const data = line.substring(6);
|
|
45
|
+
|
|
46
|
+
// Skip [DONE] markers or empty data
|
|
47
|
+
if (data === '[DONE]' || data.trim() === '') continue;
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const event = JSON.parse(data) as T;
|
|
51
|
+
yield event;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
// Log parsing errors but continue processing
|
|
54
|
+
console.error('Failed to parse SSE event:', data, error);
|
|
55
|
+
// Optionally yield an error event
|
|
56
|
+
// yield { type: 'error', data: `Parse error: ${error.message}` } as T;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Handle other SSE fields if needed (event:, id:, retry:)
|
|
60
|
+
// For now, we only care about data: lines
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Process any remaining data in buffer
|
|
65
|
+
if (buffer.trim() && buffer.startsWith('data: ')) {
|
|
66
|
+
const data = buffer.substring(6);
|
|
67
|
+
if (data !== '[DONE]' && data.trim()) {
|
|
68
|
+
try {
|
|
69
|
+
const event = JSON.parse(data) as T;
|
|
70
|
+
yield event;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error('Failed to parse final SSE event:', data, error);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} finally {
|
|
77
|
+
// Clean up resources
|
|
78
|
+
reader.releaseLock();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Helper to convert a Response with SSE stream directly to AsyncIterable
|
|
85
|
+
* @param response - Response object with SSE stream
|
|
86
|
+
* @param signal - Optional AbortSignal for cancellation
|
|
87
|
+
*/
|
|
88
|
+
export async function* responseToAsyncIterable<T>(
|
|
89
|
+
response: Response,
|
|
90
|
+
signal?: AbortSignal
|
|
91
|
+
): AsyncIterable<T> {
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
throw new Error(`Response not ok: ${response.status} ${response.statusText}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!response.body) {
|
|
97
|
+
throw new Error('No response body');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
yield* parseSSEStream<T>(response.body, signal);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Create an SSE-formatted ReadableStream from an AsyncIterable
|
|
105
|
+
* (Useful for Worker endpoints that need to forward AsyncIterable as SSE)
|
|
106
|
+
* @param events - AsyncIterable of events
|
|
107
|
+
* @param options - Stream options
|
|
108
|
+
*/
|
|
109
|
+
export function asyncIterableToSSEStream<T>(
|
|
110
|
+
events: AsyncIterable<T>,
|
|
111
|
+
options?: {
|
|
112
|
+
signal?: AbortSignal;
|
|
113
|
+
serialize?: (event: T) => string;
|
|
114
|
+
}
|
|
115
|
+
): ReadableStream<Uint8Array> {
|
|
116
|
+
const encoder = new TextEncoder();
|
|
117
|
+
const serialize = options?.serialize || JSON.stringify;
|
|
118
|
+
|
|
119
|
+
return new ReadableStream({
|
|
120
|
+
async start(controller) {
|
|
121
|
+
try {
|
|
122
|
+
for await (const event of events) {
|
|
123
|
+
if (options?.signal?.aborted) {
|
|
124
|
+
controller.error(new Error('Operation was aborted'));
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const data = serialize(event);
|
|
129
|
+
const sseEvent = `data: ${data}\n\n`;
|
|
130
|
+
controller.enqueue(encoder.encode(sseEvent));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Send completion marker
|
|
134
|
+
controller.enqueue(encoder.encode('data: [DONE]\n\n'));
|
|
135
|
+
} catch (error) {
|
|
136
|
+
controller.error(error);
|
|
137
|
+
} finally {
|
|
138
|
+
controller.close();
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
cancel() {
|
|
143
|
+
// Handle stream cancellation
|
|
144
|
+
console.log('SSE stream cancelled');
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
// Core Types
|
|
2
|
+
|
|
3
|
+
export interface BaseExecOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Session ID for grouping related commands
|
|
6
|
+
*/
|
|
7
|
+
sessionId?: string;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Maximum execution time in milliseconds
|
|
11
|
+
*/
|
|
12
|
+
timeout?: number;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Environment variables for the command
|
|
16
|
+
*/
|
|
17
|
+
env?: Record<string, string>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Working directory for command execution
|
|
21
|
+
*/
|
|
22
|
+
cwd?: string;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Text encoding for output (default: 'utf8')
|
|
26
|
+
*/
|
|
27
|
+
encoding?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ExecOptions extends BaseExecOptions {
|
|
31
|
+
/**
|
|
32
|
+
* Enable real-time output streaming via callbacks
|
|
33
|
+
*/
|
|
34
|
+
stream?: boolean;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Callback for real-time output data
|
|
38
|
+
*/
|
|
39
|
+
onOutput?: (stream: 'stdout' | 'stderr', data: string) => void;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Callback when command completes (only when stream: true)
|
|
43
|
+
*/
|
|
44
|
+
onComplete?: (result: ExecResult) => void;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Callback for execution errors
|
|
48
|
+
*/
|
|
49
|
+
onError?: (error: Error) => void;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* AbortSignal for cancelling execution
|
|
53
|
+
*/
|
|
54
|
+
signal?: AbortSignal;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface ExecResult {
|
|
58
|
+
/**
|
|
59
|
+
* Whether the command succeeded (exitCode === 0)
|
|
60
|
+
*/
|
|
61
|
+
success: boolean;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Process exit code
|
|
65
|
+
*/
|
|
66
|
+
exitCode: number;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Standard output content
|
|
70
|
+
*/
|
|
71
|
+
stdout: string;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Standard error content
|
|
75
|
+
*/
|
|
76
|
+
stderr: string;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Command that was executed
|
|
80
|
+
*/
|
|
81
|
+
command: string;
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Execution duration in milliseconds
|
|
86
|
+
*/
|
|
87
|
+
duration: number;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* ISO timestamp when command started
|
|
91
|
+
*/
|
|
92
|
+
timestamp: string;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Session ID if provided
|
|
96
|
+
*/
|
|
97
|
+
sessionId?: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Background Process Types
|
|
101
|
+
|
|
102
|
+
export interface ProcessOptions extends BaseExecOptions {
|
|
103
|
+
/**
|
|
104
|
+
* Custom process ID for later reference
|
|
105
|
+
* If not provided, a UUID will be generated
|
|
106
|
+
*/
|
|
107
|
+
processId?: string;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Automatically cleanup process record after exit (default: true)
|
|
111
|
+
*/
|
|
112
|
+
autoCleanup?: boolean;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Callback when process exits
|
|
116
|
+
*/
|
|
117
|
+
onExit?: (code: number | null) => void;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Callback for real-time output (background processes)
|
|
121
|
+
*/
|
|
122
|
+
onOutput?: (stream: 'stdout' | 'stderr', data: string) => void;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Callback when process starts successfully
|
|
126
|
+
*/
|
|
127
|
+
onStart?: (process: Process) => void;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Callback for process errors
|
|
131
|
+
*/
|
|
132
|
+
onError?: (error: Error) => void;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export type ProcessStatus =
|
|
136
|
+
| 'starting' // Process is being initialized
|
|
137
|
+
| 'running' // Process is actively running
|
|
138
|
+
| 'completed' // Process exited successfully (code 0)
|
|
139
|
+
| 'failed' // Process exited with non-zero code
|
|
140
|
+
| 'killed' // Process was terminated by signal
|
|
141
|
+
| 'error'; // Process failed to start or encountered error
|
|
142
|
+
|
|
143
|
+
export interface Process {
|
|
144
|
+
/**
|
|
145
|
+
* Unique process identifier
|
|
146
|
+
*/
|
|
147
|
+
readonly id: string;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* System process ID (if available and running)
|
|
151
|
+
*/
|
|
152
|
+
readonly pid?: number;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Command that was executed
|
|
156
|
+
*/
|
|
157
|
+
readonly command: string;
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Current process status
|
|
162
|
+
*/
|
|
163
|
+
readonly status: ProcessStatus;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* When the process was started
|
|
167
|
+
*/
|
|
168
|
+
readonly startTime: Date;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* When the process ended (if completed)
|
|
172
|
+
*/
|
|
173
|
+
readonly endTime?: Date;
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Process exit code (if completed)
|
|
177
|
+
*/
|
|
178
|
+
readonly exitCode?: number;
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Session ID if provided
|
|
182
|
+
*/
|
|
183
|
+
readonly sessionId?: string;
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Kill the process
|
|
187
|
+
*/
|
|
188
|
+
kill(signal?: string): Promise<void>;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Get current process status (refreshed)
|
|
192
|
+
*/
|
|
193
|
+
getStatus(): Promise<ProcessStatus>;
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Get accumulated logs
|
|
197
|
+
*/
|
|
198
|
+
getLogs(): Promise<{ stdout: string; stderr: string }>;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Streaming Types
|
|
202
|
+
|
|
203
|
+
export interface ExecEvent {
|
|
204
|
+
type: 'start' | 'stdout' | 'stderr' | 'complete' | 'error';
|
|
205
|
+
timestamp: string;
|
|
206
|
+
data?: string;
|
|
207
|
+
command?: string;
|
|
208
|
+
exitCode?: number;
|
|
209
|
+
result?: ExecResult;
|
|
210
|
+
error?: string; // Changed to string for serialization
|
|
211
|
+
sessionId?: string;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export interface LogEvent {
|
|
215
|
+
type: 'stdout' | 'stderr' | 'exit' | 'error';
|
|
216
|
+
timestamp: string;
|
|
217
|
+
data: string;
|
|
218
|
+
processId: string;
|
|
219
|
+
sessionId?: string;
|
|
220
|
+
exitCode?: number; // For 'exit' events
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export interface StreamOptions extends BaseExecOptions {
|
|
224
|
+
/**
|
|
225
|
+
* Buffer size for streaming output
|
|
226
|
+
*/
|
|
227
|
+
bufferSize?: number;
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* AbortSignal for cancelling stream
|
|
231
|
+
*/
|
|
232
|
+
signal?: AbortSignal;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Error Types
|
|
236
|
+
|
|
237
|
+
export class SandboxError extends Error {
|
|
238
|
+
constructor(message: string, public code?: string) {
|
|
239
|
+
super(message);
|
|
240
|
+
this.name = 'SandboxError';
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export class ProcessNotFoundError extends SandboxError {
|
|
245
|
+
constructor(processId: string) {
|
|
246
|
+
super(`Process not found: ${processId}`, 'PROCESS_NOT_FOUND');
|
|
247
|
+
this.name = 'ProcessNotFoundError';
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export class ProcessAlreadyExistsError extends SandboxError {
|
|
252
|
+
constructor(processId: string) {
|
|
253
|
+
super(`Process already exists: ${processId}`, 'PROCESS_EXISTS');
|
|
254
|
+
this.name = 'ProcessAlreadyExistsError';
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export class ExecutionTimeoutError extends SandboxError {
|
|
259
|
+
constructor(timeout: number) {
|
|
260
|
+
super(`Execution timed out after ${timeout}ms`, 'EXECUTION_TIMEOUT');
|
|
261
|
+
this.name = 'ExecutionTimeoutError';
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Internal Container Types
|
|
266
|
+
|
|
267
|
+
export interface ProcessRecord {
|
|
268
|
+
id: string;
|
|
269
|
+
pid?: number;
|
|
270
|
+
command: string;
|
|
271
|
+
status: ProcessStatus;
|
|
272
|
+
startTime: Date;
|
|
273
|
+
endTime?: Date;
|
|
274
|
+
exitCode?: number;
|
|
275
|
+
sessionId?: string;
|
|
276
|
+
|
|
277
|
+
// Internal fields
|
|
278
|
+
childProcess?: any; // Node.js ChildProcess
|
|
279
|
+
stdout: string; // Accumulated output (ephemeral)
|
|
280
|
+
stderr: string; // Accumulated output (ephemeral)
|
|
281
|
+
|
|
282
|
+
// Streaming
|
|
283
|
+
outputListeners: Set<(stream: 'stdout' | 'stderr', data: string) => void>;
|
|
284
|
+
statusListeners: Set<(status: ProcessStatus) => void>;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Container Request/Response Types
|
|
288
|
+
|
|
289
|
+
export interface StartProcessRequest {
|
|
290
|
+
command: string;
|
|
291
|
+
options?: {
|
|
292
|
+
processId?: string;
|
|
293
|
+
sessionId?: string;
|
|
294
|
+
timeout?: number;
|
|
295
|
+
env?: Record<string, string>;
|
|
296
|
+
cwd?: string;
|
|
297
|
+
encoding?: string;
|
|
298
|
+
autoCleanup?: boolean;
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export interface StartProcessResponse {
|
|
303
|
+
process: {
|
|
304
|
+
id: string;
|
|
305
|
+
pid?: number;
|
|
306
|
+
command: string;
|
|
307
|
+
status: ProcessStatus;
|
|
308
|
+
startTime: string;
|
|
309
|
+
sessionId?: string;
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export interface ListProcessesResponse {
|
|
314
|
+
processes: Array<{
|
|
315
|
+
id: string;
|
|
316
|
+
pid?: number;
|
|
317
|
+
command: string;
|
|
318
|
+
status: ProcessStatus;
|
|
319
|
+
startTime: string;
|
|
320
|
+
endTime?: string;
|
|
321
|
+
exitCode?: number;
|
|
322
|
+
sessionId?: string;
|
|
323
|
+
}>;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export interface GetProcessResponse {
|
|
327
|
+
process: {
|
|
328
|
+
id: string;
|
|
329
|
+
pid?: number;
|
|
330
|
+
command: string;
|
|
331
|
+
status: ProcessStatus;
|
|
332
|
+
startTime: string;
|
|
333
|
+
endTime?: string;
|
|
334
|
+
exitCode?: number;
|
|
335
|
+
sessionId?: string;
|
|
336
|
+
} | null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export interface GetProcessLogsResponse {
|
|
340
|
+
stdout: string;
|
|
341
|
+
stderr: string;
|
|
342
|
+
processId: string;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Main Sandbox Interface
|
|
346
|
+
|
|
347
|
+
export interface ISandbox {
|
|
348
|
+
// Enhanced execution API
|
|
349
|
+
exec(command: string, options?: ExecOptions): Promise<ExecResult>;
|
|
350
|
+
|
|
351
|
+
// Background process management
|
|
352
|
+
startProcess(command: string, options?: ProcessOptions): Promise<Process>;
|
|
353
|
+
listProcesses(): Promise<Process[]>;
|
|
354
|
+
getProcess(id: string): Promise<Process | null>;
|
|
355
|
+
killProcess(id: string, signal?: string): Promise<void>;
|
|
356
|
+
killAllProcesses(): Promise<number>;
|
|
357
|
+
|
|
358
|
+
// Advanced streaming - returns ReadableStream that can be converted to AsyncIterable
|
|
359
|
+
execStream(command: string, options?: StreamOptions): Promise<ReadableStream<Uint8Array>>;
|
|
360
|
+
streamProcessLogs(processId: string, options?: { signal?: AbortSignal }): Promise<ReadableStream<Uint8Array>>;
|
|
361
|
+
|
|
362
|
+
// Utility methods
|
|
363
|
+
cleanupCompletedProcesses(): Promise<number>;
|
|
364
|
+
getProcessLogs(id: string): Promise<{ stdout: string; stderr: string }>;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Type Guards
|
|
368
|
+
|
|
369
|
+
export function isExecResult(value: any): value is ExecResult {
|
|
370
|
+
return value &&
|
|
371
|
+
typeof value.success === 'boolean' &&
|
|
372
|
+
typeof value.exitCode === 'number' &&
|
|
373
|
+
typeof value.stdout === 'string' &&
|
|
374
|
+
typeof value.stderr === 'string';
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function isProcess(value: any): value is Process {
|
|
378
|
+
return value &&
|
|
379
|
+
typeof value.id === 'string' &&
|
|
380
|
+
typeof value.command === 'string' &&
|
|
381
|
+
typeof value.status === 'string';
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function isProcessStatus(value: string): value is ProcessStatus {
|
|
385
|
+
return ['starting', 'running', 'completed', 'failed', 'killed', 'error'].includes(value);
|
|
386
|
+
}
|
package/tsconfig.json
CHANGED
package/README.md
DELETED
|
@@ -1,65 +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
|
-
return sandbox.exec("ls", ["-la"]);
|
|
51
|
-
},
|
|
52
|
-
};
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
### Methods:
|
|
56
|
-
|
|
57
|
-
- `exec(command: string, args: string[], options?: { stream?: boolean })`: Execute a command in the sandbox.
|
|
58
|
-
- `gitCheckout(repoUrl: string, options: { branch?: string; targetDir?: string; stream?: boolean })`: Checkout a git repository in the sandbox.
|
|
59
|
-
- `mkdir(path: string, options: { recursive?: boolean; stream?: boolean })`: Create a directory in the sandbox.
|
|
60
|
-
- `writeFile(path: string, content: string, options: { encoding?: string; stream?: boolean })`: Write content to a file in the sandbox.
|
|
61
|
-
- `readFile(path: string, options: { encoding?: string; stream?: boolean })`: Read content from a file in the sandbox.
|
|
62
|
-
- `deleteFile(path: string, options?: { stream?: boolean })`: Delete a file from the sandbox.
|
|
63
|
-
- `renameFile(oldPath: string, newPath: string, options?: { stream?: boolean })`: Rename a file in the sandbox.
|
|
64
|
-
- `moveFile(sourcePath: string, destinationPath: string, options?: { stream?: boolean })`: Move a file from one location to another in the sandbox.
|
|
65
|
-
- `ping()`: Ping the sandbox.
|