Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/hot-pans-warn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudflare/sandbox": patch
---

Move .connect to .wsConnect within DO stub
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ tests/e2e/test-worker/wrangler.jsonc
# Allow config files and scripts
!scripts/**/*.js
!examples/**/*.js
# Allow generated types in examples
!examples/**/*.d.ts
# Allow source in dist directories
!dist/**
!node_modules/**
7 changes: 1 addition & 6 deletions examples/claude-code/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getSandbox, type Sandbox } from '@cloudflare/sandbox';
import { getSandbox } from '@cloudflare/sandbox';

interface CmdOutput {
success: boolean;
Expand All @@ -8,11 +8,6 @@ interface CmdOutput {
// helper to read the outputs from `.exec` results
const getOutput = (res: CmdOutput) => (res.success ? res.stdout : res.stderr);

type Env = {
Sandbox: DurableObjectNamespace<Sandbox>;
ANTHROPIC_API_KEY: string;
};

const EXTRA_SYSTEM =
'You are an automatic feature-implementer/bug-fixer.' +
'You apply all necessary changes to achieve the user request. You must ensure you DO NOT commit the changes, ' +
Expand Down
8,383 changes: 8,383 additions & 0 deletions examples/claude-code/worker-configuration.d.ts

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions examples/code-interpreter/.dev.vars.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
CLOUDFLARE_ACCOUNT_ID=xxxx
CLOUDFLARE_API_KEY=xxxx
6 changes: 1 addition & 5 deletions examples/minimal/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import { getSandbox, type Sandbox } from '@cloudflare/sandbox';
import { getSandbox } from '@cloudflare/sandbox';

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

type Env = {
Sandbox: DurableObjectNamespace<Sandbox>;
};

export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
Expand Down
8,376 changes: 8,376 additions & 0 deletions examples/minimal/worker-configuration.d.ts

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/sandbox/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export {
SandboxClient,
UtilityClient
} from './clients';
export { connect, getSandbox, Sandbox } from './sandbox';
export { getSandbox, Sandbox } from './sandbox';

// Legacy types are now imported from the new client architecture

Expand Down
58 changes: 23 additions & 35 deletions packages/sandbox/src/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export function getSandbox(
id: string,
options?: SandboxOptions
): Sandbox {
const stub = getContainer(ns, id) as any as Sandbox;
const stub = getContainer(ns, id) as unknown as Sandbox;

// Store the name on first access
stub.setSandboxName?.(id);
Expand All @@ -49,41 +49,24 @@ export function getSandbox(
stub.setKeepAlive(options.keepAlive);
}

return stub;
return Object.assign(stub, {
wsConnect: connect(stub)
});
}

/**
* Connect an incoming WebSocket request to a specific port inside the container.
*
* Note: This is a standalone function (not a Sandbox method) because WebSocket
* connections cannot be serialized over Durable Object RPC.
*
* @param sandbox - The Sandbox instance to route the request through
* @param request - The incoming WebSocket upgrade request
* @param port - The port number to connect to (1024-65535)
* @returns The WebSocket upgrade response
* @throws {SecurityError} - If port is invalid or in restricted range
*
* @example
* const sandbox = getSandbox(env.Sandbox, 'sandbox-id');
* if (request.headers.get('Upgrade')?.toLowerCase() === 'websocket') {
* return await connect(sandbox, request, 8080);
* }
*/
export async function connect(
sandbox: Sandbox,
request: Request,
port: number
): Promise<Response> {
// Validate port before routing
if (!validatePort(port)) {
throw new SecurityError(
`Invalid or restricted port: ${port}. Ports must be in range 1024-65535 and not reserved.`
);
}

const portSwitchedRequest = switchPort(request, port);
return await sandbox.fetch(portSwitchedRequest);
export function connect(
stub: { fetch: (request: Request) => Promise<Response> }
) {
return async (request: Request, port: number) => {
// Validate port before routing
if (!validatePort(port)) {
throw new SecurityError(
`Invalid or restricted port: ${port}. Ports must be in range 1024-65535 and not reserved.`
);
}
const portSwitchedRequest = switchPort(request, port);
return await stub.fetch(portSwitchedRequest);
};
}

export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
Expand All @@ -100,7 +83,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
private logger: ReturnType<typeof createLogger>;
private keepAliveEnabled: boolean = false;

constructor(ctx: DurableObject['ctx'], env: Env) {
constructor(ctx: DurableObjectState<{}>, env: Env) {
super(ctx, env);

const envObj = env as any;
Expand Down Expand Up @@ -360,6 +343,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
});
}

wsConnect(request: Request, port: number): Promise<Response> {
// Dummy implementation that will be overridden by the stub
throw new Error('Not implemented here to avoid RPC serialization issues');
}

private determinePort(url: URL): number {
// Extract port from proxy requests (e.g., /proxy/8080/*)
const proxyMatch = url.pathname.match(/^\/proxy\/(\d+)/);
Expand Down
24 changes: 14 additions & 10 deletions packages/sandbox/tests/sandbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ vi.mock('@cloudflare/containers', () => {

describe('Sandbox - Automatic Session Management', () => {
let sandbox: Sandbox;
let mockCtx: Partial<DurableObjectState>;
let mockCtx: Partial<DurableObjectState<{}>>;
let mockEnv: any;

beforeEach(async () => {
Expand Down Expand Up @@ -82,13 +82,17 @@ describe('Sandbox - Automatic Session Management', () => {
mockEnv = {};

// Create Sandbox instance - SandboxClient is created internally
sandbox = new Sandbox(mockCtx as DurableObjectState, mockEnv);
const stub = new Sandbox(mockCtx, mockEnv);

// Wait for blockConcurrencyWhile to complete
await vi.waitFor(() => {
expect(mockCtx.blockConcurrencyWhile).toHaveBeenCalled();
});

sandbox = Object.assign(stub, {
wsConnect: connect(stub)
});

// Now spy on the client methods that we need for testing
vi.spyOn(sandbox.client.utils, 'createSession').mockResolvedValue({
success: true,
Expand Down Expand Up @@ -621,7 +625,7 @@ describe('Sandbox - Automatic Session Management', () => {
});
});

describe('connect() function', () => {
describe('wsConnect() method', () => {
it('should route WebSocket request through switchPort to sandbox.fetch', async () => {
const { switchPort } = await import('@cloudflare/containers');
const switchPortMock = vi.mocked(switchPort);
Expand All @@ -634,7 +638,7 @@ describe('Sandbox - Automatic Session Management', () => {
});

const fetchSpy = vi.spyOn(sandbox, 'fetch');
const response = await connect(sandbox, request, 8080);
const response = await sandbox.wsConnect(request, 8080);

// Verify switchPort was called with correct port
expect(switchPortMock).toHaveBeenCalledWith(request, 8080);
Expand All @@ -653,21 +657,21 @@ describe('Sandbox - Automatic Session Management', () => {
});

// Invalid port values
await expect(connect(sandbox, request, -1)).rejects.toThrow(
await expect(sandbox.wsConnect(request, -1)).rejects.toThrow(
'Invalid or restricted port'
);
await expect(connect(sandbox, request, 0)).rejects.toThrow(
await expect(sandbox.wsConnect(request, 0)).rejects.toThrow(
'Invalid or restricted port'
);
await expect(connect(sandbox, request, 70000)).rejects.toThrow(
await expect(sandbox.wsConnect(request, 70000)).rejects.toThrow(
'Invalid or restricted port'
);

// Privileged ports
await expect(connect(sandbox, request, 80)).rejects.toThrow(
await expect(sandbox.wsConnect(request, 80)).rejects.toThrow(
'Invalid or restricted port'
);
await expect(connect(sandbox, request, 443)).rejects.toThrow(
await expect(sandbox.wsConnect(request, 443)).rejects.toThrow(
'Invalid or restricted port'
);
});
Expand All @@ -685,7 +689,7 @@ describe('Sandbox - Automatic Session Management', () => {
);

const fetchSpy = vi.spyOn(sandbox, 'fetch');
await connect(sandbox, request, 8080);
await sandbox.wsConnect(request, 8080);

const calledRequest = fetchSpy.mock.calls[0][0];

Expand Down
3 changes: 3 additions & 0 deletions packages/shared/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,9 @@ export interface ISandbox {
): Promise<ReadableStream>;
listCodeContexts(): Promise<CodeContext[]>;
deleteCodeContext(contextId: string): Promise<void>;

// WebSocket connection
wsConnect(request: Request, port: number): Promise<Response>;
}

// Type guards for runtime validation
Expand Down
15 changes: 5 additions & 10 deletions tests/e2e/test-worker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,7 @@
* Exposes SDK methods via HTTP endpoints for E2E testing.
* Supports both default sessions (implicit) and explicit sessions via X-Session-Id header.
*/
import {
Sandbox,
getSandbox,
proxyToSandbox,
connect
} from '@cloudflare/sandbox';
import { Sandbox, getSandbox, proxyToSandbox } from '@cloudflare/sandbox';
export { Sandbox };

interface Env {
Expand Down Expand Up @@ -44,7 +39,7 @@ export default {

const sandbox = getSandbox(env.Sandbox, sandboxId, {
keepAlive
}) as Sandbox<Env>;
});

// Get session ID from header (optional)
// If provided, retrieve the session fresh from the Sandbox DO on each request
Expand Down Expand Up @@ -229,13 +224,13 @@ console.log('Terminal server on port ' + port);
const upgradeHeader = request.headers.get('Upgrade');
if (upgradeHeader?.toLowerCase() === 'websocket') {
if (url.pathname === '/ws/echo') {
return await connect(sandbox, request, 8080);
return await sandbox.wsConnect(request, 8080);
}
if (url.pathname === '/ws/code') {
return await connect(sandbox, request, 8081);
return await sandbox.wsConnect(request, 8081);
}
if (url.pathname === '/ws/terminal') {
return await connect(sandbox, request, 8082);
return await sandbox.wsConnect(request, 8082);
}
}

Expand Down
6 changes: 3 additions & 3 deletions tests/e2e/websocket-connect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import {
} from './helpers/test-fixtures';

/**
* WebSocket connect() Integration Tests
* WebSocket wsConnect() Integration Tests
*
* Tests the connect() method for routing WebSocket requests to container services.
* Tests the wsConnect() method for routing WebSocket requests to container services.
* Focuses on transport-level functionality, not application-level server implementations.
*/
describe('WebSocket Connections', () => {
Expand Down Expand Up @@ -50,7 +50,7 @@ describe('WebSocket Connections', () => {
// Wait for server to be ready (generous timeout for first startup)
await new Promise((resolve) => setTimeout(resolve, 2000));

// Connect via WebSocket using connect() routing
// Connect via WebSocket using wsConnect() routing
const wsUrl = workerUrl.replace(/^http/, 'ws') + '/ws/echo';
const ws = new WebSocket(wsUrl, {
headers: {
Expand Down
9 changes: 2 additions & 7 deletions tests/integration/app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3814,7 +3814,7 @@ function WebSocketTab() {
<div className="websocket-header">
<h3>WebSocket Echo Server</h3>
<p>
Test the <code>connect()</code> method by sending messages to a
Test the <code>wsConnect()</code> method by sending messages to a
WebSocket echo server running in the sandbox.
</p>
</div>
Expand Down Expand Up @@ -3922,19 +3922,14 @@ function WebSocketTab() {
</li>
<li>
<strong>Connect:</strong> Uses{' '}
<code>connect(sandbox, request, 8080)</code> to route WebSocket to
<code>sandbox.wsConnect(request, 8080)</code> to route WebSocket to
the server
</li>
<li>
<strong>Echo:</strong> Any message you send will be echoed back by
the server
</li>
</ol>
<p>
This demonstrates the <code>connect()</code> method, which is
syntactic sugar for <code>fetch(switchPort())</code> to make WebSocket
routing clear and simple.
</p>
</div>
</div>
);
Expand Down
3 changes: 1 addition & 2 deletions tests/integration/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {
getSandbox,
proxyToSandbox,
connect,
type Sandbox
} from '@cloudflare/sandbox';
import { codeExamples } from '../shared/examples';
Expand Down Expand Up @@ -106,7 +105,7 @@ export default {
sandboxId
) as unknown as Sandbox<unknown>;
// Route WebSocket connection to echo server on port 8080
return await connect(sandbox, request, 8080);
return await sandbox.wsConnect(request, 8080);
}

const sandbox = getUserSandbox(
Expand Down
Loading