JSPM

@cloudflare/sandbox

0.0.0-cecde0a
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 208742
  • Score
    100M100P100Q158405F
  • License ISC

A sandboxed environment for running commands

Package Exports

  • @cloudflare/sandbox

Readme

@cloudflare/sandbox

⚠️ Experimental - This library is currently experimental and we're actively seeking feedback. Please try it out and let us know what you think!

A library to spin up a sandboxed environment.

First, setup your wrangler.json to use the sandbox:

{
  // ...
  "containers": [
    {
      "class_name": "Sandbox",
      "image": "./node_modules/@cloudflare/sandbox/Dockerfile",
      "name": "sandbox"
    }
  ],
  "durable_objects": {
    "bindings": [
      {
        "class_name": "Sandbox",
        "name": "Sandbox"
      }
    ]
  },
  "migrations": [
    {
      "new_sqlite_classes": ["Sandbox"],
      "tag": "v1"
    }
  ]
}

Then, export the Sandbox class in your worker:

export { Sandbox } from "@cloudflare/sandbox";

You can then use the Sandbox class in your worker:

import { getSandbox } from "@cloudflare/sandbox";

export default {
  async fetch(request: Request, env: Env) {
    const sandbox = getSandbox(env.Sandbox, "my-sandbox");
    const result = await sandbox.exec("ls -la");
    return Response.json(result);
  },
};

Core Methods

Command Execution

  • exec(command: string, options?: ExecOptions): Execute a command and return the complete result.
  • execStream(command: string, options?: StreamOptions): Execute a command with real-time streaming (returns ReadableStream).

Process Management

  • startProcess(command: string, options?: ProcessOptions): Start a background process.
  • listProcesses(): List all running processes.
  • getProcess(id: string): Get details of a specific process.
  • killProcess(id: string, signal?: string): Kill a specific process.
  • killAllProcesses(): Kill all running processes.
  • streamProcessLogs(processId: string, options?: { signal?: AbortSignal }): Stream logs from a running process (returns ReadableStream).

File Operations

  • gitCheckout(repoUrl: string, options: { branch?: string; targetDir?: string }): Checkout a git repository.
  • mkdir(path: string, options?: { recursive?: boolean }): Create a directory.
  • writeFile(path: string, content: string, options?: { encoding?: string }): Write content to a file.
  • readFile(path: string, options?: { encoding?: string }): Read content from a file.
  • deleteFile(path: string): Delete a file.
  • renameFile(oldPath: string, newPath: string): Rename a file.
  • moveFile(sourcePath: string, destinationPath: string): Move a file.

Port Management

  • exposePort(port: number, options: { name?: string; hostname: string }): Expose a port for external access.
  • unexposePort(port: number): Unexpose a previously exposed port.
  • getExposedPorts(hostname: string): List all exposed ports with their preview URLs.

Beautiful AsyncIterable Streaming APIs ✨

The SDK provides streaming methods that return ReadableStream for RPC compatibility, along with a parseSSEStream utility to convert them to typed AsyncIterables:

Stream Command Output

import { parseSSEStream, type ExecEvent } from '@cloudflare/sandbox';

// Get the stream and convert to AsyncIterable
const stream = await sandbox.execStream('npm run build');
for await (const event of parseSSEStream<ExecEvent>(stream)) {
  switch (event.type) {
    case 'start':
      console.log(`Build started: ${event.command}`);
      break;
    case 'stdout':
      console.log(`[OUT] ${event.data}`);
      break;
    case 'stderr':
      console.error(`[ERR] ${event.data}`);
      break;
    case 'complete':
      console.log(`Build finished with exit code: ${event.exitCode}`);
      break;
    case 'error':
      console.error(`Build error: ${event.error}`);
      break;
  }
}

Stream Process Logs

import { parseSSEStream, type LogEvent } from '@cloudflare/sandbox';

// Monitor background process logs
const webServer = await sandbox.startProcess('node server.js');

const logStream = await sandbox.streamProcessLogs(webServer.id);
for await (const log of parseSSEStream<LogEvent>(logStream)) {
  if (log.type === 'stdout') {
    console.log(`Server: ${log.data}`);
  } else if (log.type === 'stderr' && log.data.includes('ERROR')) {
    // React to errors
    await handleError(log);
  } else if (log.type === 'exit') {
    console.log(`Server exited with code: ${log.exitCode}`);
    break;
  }
}

Why parseSSEStream?

The streaming methods return ReadableStream<Uint8Array> to ensure compatibility across Durable Object RPC boundaries. The parseSSEStream utility converts these streams into typed AsyncIterables, giving you the best of both worlds:

  • RPC Compatibility: ReadableStream can be serialized across process boundaries
  • Beautiful APIs: AsyncIterable provides clean for await syntax with typed events
  • Type Safety: Full TypeScript support with ExecEvent and LogEvent types

Advanced Examples

CI/CD Build System
import { parseSSEStream, type ExecEvent } from '@cloudflare/sandbox';

export async function runBuild(env: Env, buildId: string) {
  const sandbox = getSandbox(env.Sandbox, buildId);
  const buildLog: string[] = [];

  try {
    const stream = await sandbox.execStream('npm run build');
    for await (const event of parseSSEStream<ExecEvent>(stream)) {
      buildLog.push(`[${event.type}] ${event.data || ''}`);

      if (event.type === 'complete') {
        await env.BUILDS.put(buildId, {
          status: event.exitCode === 0 ? 'success' : 'failed',
          exitCode: event.exitCode,
          logs: buildLog.join('\n'),
          duration: Date.now() - new Date(event.timestamp).getTime()
        });
      }
    }
  } catch (error) {
    await env.BUILDS.put(buildId, {
      status: 'error',
      error: error.message,
      logs: buildLog.join('\n')
    });
  }
}
System Monitoring
import { parseSSEStream, type LogEvent } from '@cloudflare/sandbox';

export default {
  async scheduled(controller: ScheduledController, env: Env) {
    const sandbox = getSandbox(env.Sandbox, 'monitor');

    // Monitor system logs
    const monitor = await sandbox.startProcess('journalctl -f');

    const logStream = await sandbox.streamProcessLogs(monitor.id);
    for await (const log of parseSSEStream<LogEvent>(logStream)) {
      if (log.type === 'stdout') {
        // Check for critical errors
        if (log.data.includes('CRITICAL')) {
          await env.ALERTS.send({
            severity: 'critical',
            message: log.data,
            timestamp: log.timestamp
          });
        }

        // Store logs
        await env.LOGS.put(`${log.timestamp}-${monitor.id}`, log.data);
      }
    }
  }
}
Streaming to Frontend via SSE
// Worker endpoint that streams to frontend
app.get('/api/build/:id/stream', async (req, env) => {
  const sandbox = getSandbox(env.Sandbox, req.params.id);
  const encoder = new TextEncoder();

  return new Response(
    new ReadableStream({
      async start(controller) {
        try {
          for await (const event of sandbox.execStream('npm run build')) {
            // Forward events to frontend as SSE
            const sseEvent = `data: ${JSON.stringify(event)}\n\n`;
            controller.enqueue(encoder.encode(sseEvent));
          }
        } catch (error) {
          const errorEvent = `data: ${JSON.stringify({
            type: 'error',
            error: error.message
          })}\n\n`;
          controller.enqueue(encoder.encode(errorEvent));
        } finally {
          controller.close();
        }
      }
    }),
    {
      headers: {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache'
      }
    }
  );
});

Streaming Utilities

The SDK exports additional utilities for working with SSE streams:

parseSSEStream

Converts a ReadableStream<Uint8Array> (from SSE endpoints) into a typed AsyncIterable<T>. This is the primary utility for consuming streams from the SDK.

import { parseSSEStream, type ExecEvent } from '@cloudflare/sandbox';

const stream = await sandbox.execStream('npm build');
for await (const event of parseSSEStream<ExecEvent>(stream)) {
  console.log(event);
}

responseToAsyncIterable

Converts a Response object with SSE content directly to AsyncIterable<T>. Useful when fetching from external SSE endpoints.

import { responseToAsyncIterable, type LogEvent } from '@cloudflare/sandbox';

// Fetch from an external SSE endpoint
const response = await fetch('https://api.example.com/logs/stream', {
  headers: { 'Accept': 'text/event-stream' }
});

// Convert Response to typed AsyncIterable
for await (const event of responseToAsyncIterable<LogEvent>(response)) {
  console.log(`[${event.type}] ${event.data}`);
}

asyncIterableToSSEStream

Converts an AsyncIterable<T> into an SSE-formatted ReadableStream<Uint8Array>. Perfect for Worker endpoints that need to transform or filter events before sending to clients.

import { getSandbox, parseSSEStream, asyncIterableToSSEStream, type LogEvent } from '@cloudflare/sandbox';

export async function handleFilteredLogs(request: Request, env: Env) {
  const sandbox = getSandbox(env.SANDBOX);
  
  // Custom async generator that filters logs
  async function* filterLogs() {
    const stream = await sandbox.streamProcessLogs('web-server');
    
    for await (const log of parseSSEStream<LogEvent>(stream)) {
      // Only forward error logs to the client
      if (log.type === 'stderr' || log.data.includes('ERROR')) {
        yield log;
      }
    }
  }

  // Convert filtered AsyncIterable back to SSE stream for the response
  const sseStream = asyncIterableToSSEStream(filterLogs());
  
  return new Response(sseStream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
    }
  });
}

Advanced Example - Merging Multiple Streams:

async function* mergeBuilds(env: Env) {
  const sandbox1 = getSandbox(env.SANDBOX1);
  const sandbox2 = getSandbox(env.SANDBOX2);
  
  // Start builds in parallel
  const [stream1, stream2] = await Promise.all([
    sandbox1.execStream('npm run build:frontend'),
    sandbox2.execStream('npm run build:backend')
  ]);
  
  // Parse and merge events
  const frontend = parseSSEStream<ExecEvent>(stream1);
  const backend = parseSSEStream<ExecEvent>(stream2);
  
  // Merge with source identification
  for await (const event of frontend) {
    yield { ...event, source: 'frontend' };
  }
  for await (const event of backend) {
    yield { ...event, source: 'backend' };
  }
}

// Convert merged stream to SSE for client
const mergedSSE = asyncIterableToSSEStream(mergeBuilds(env));

Cancellation Support

Both streaming methods support cancellation via AbortSignal:

const controller = new AbortController();

// Cancel after 30 seconds
setTimeout(() => controller.abort(), 30000);

try {
  for await (const event of sandbox.execStream('long-running-task', {
    signal: controller.signal
  })) {
    // Process events
    if (shouldCancel(event)) {
      controller.abort();
    }
  }
} catch (error) {
  if (error.message.includes('aborted')) {
    console.log('Operation cancelled');
  }
}