diff --git a/src/content/docs/sandbox/api/ports.mdx b/src/content/docs/sandbox/api/ports.mdx index 53e789c109ac55a..861aede7a3757fd 100644 --- a/src/content/docs/sandbox/api/ports.mdx +++ b/src/content/docs/sandbox/api/ports.mdx @@ -17,7 +17,7 @@ Expose services running in your sandbox via public preview URLs. See [Preview UR ### `exposePort()` -Expose a port and get a preview URL. +Expose a port and get a preview URL for accessing services running in the sandbox. ```ts const response = await sandbox.exposePort(port: number, options: ExposePortOptions): Promise @@ -27,28 +27,42 @@ const response = await sandbox.exposePort(port: number, options: ExposePortOptio - `port` - Port number to expose (1024-65535) - `options`: - - `hostname` - Your Worker's domain name (e.g., `'example.com'`). Required to construct preview URLs with wildcard subdomains like `https://8080-sandbox-abc123.example.com`. Cannot be a `.workers.dev` domain as it doesn't support wildcard DNS patterns. + - `hostname` - Your Worker's domain name (e.g., `'example.com'`). Required to construct preview URLs with wildcard subdomains like `https://8080-sandbox-abc123token.example.com`. Cannot be a `.workers.dev` domain as it doesn't support wildcard DNS patterns. - `name` - Friendly name for the port (optional) + - `token` - Custom token for the preview URL (optional). Must be 1-16 characters containing only lowercase letters (a-z), numbers (0-9), hyphens (-), and underscores (_). If not provided, a random 16-character token is generated automatically. **Returns**: `Promise` with `port`, `url` (preview URL), `name` -``` +```ts // Extract hostname from request const { hostname } = new URL(request.url); +// Basic usage with auto-generated token await sandbox.startProcess('python -m http.server 8000'); const exposed = await sandbox.exposePort(8000, { hostname }); -console.log('Available at:', exposed.exposedAt); -// https://8000-abc123.yourdomain.com +console.log('Available at:', exposed.url); +// https://8000-sandbox-id-abc123random4567.yourdomain.com -// Multiple services with names +// With custom token for stable URLs across deployments await sandbox.startProcess('node api.js'); -const api = await sandbox.exposePort(3000, { hostname, name: 'api' }); +const api = await sandbox.exposePort(3000, { + hostname, + name: 'api', + token: 'prod-api-v1' // URL stays same across restarts +}); +console.log('Stable API URL:', api.url); +// https://3000-sandbox-id-prod-api-v1.yourdomain.com + +// Multiple services with custom tokens await sandbox.startProcess('npm run dev'); -const frontend = await sandbox.exposePort(5173, { hostname, name: 'frontend' }); +const frontend = await sandbox.exposePort(5173, { + hostname, + name: 'frontend', + token: 'dev-ui' +}); ``` @@ -56,6 +70,82 @@ const frontend = await sandbox.exposePort(5173, { hostname, name: 'frontend' }); When using `wrangler dev`, you must add `EXPOSE` directives to your Dockerfile for each port. See [Expose Services guide](/sandbox/guides/expose-services/#local-development) for details. ::: +## Custom Tokens for Stable URLs + +Custom tokens enable consistent preview URLs across container restarts and deployments. This is useful for: + +- **Production environments** - Share stable URLs with users or teams +- **Development workflows** - Maintain bookmarks and integrations +- **CI/CD pipelines** - Reference consistent URLs in tests or deployment scripts + +**Token Requirements:** +- 1-16 characters in length +- Only lowercase letters (a-z), numbers (0-9), hyphens (-), and underscores (_) +- Must be unique per sandbox (cannot reuse tokens across different ports) + + +```ts +// Production API with stable URL +const { url } = await sandbox.exposePort(8080, { + hostname: 'api.example.com', + token: 'v1-stable' // Always the same URL +}); + +// Error: Token collision prevention +await sandbox.exposePort(8081, { hostname, token: 'v1-stable' }); +// Throws: Token 'v1-stable' is already in use by port 8080 + +// Success: Re-exposing same port with same token (idempotent) +await sandbox.exposePort(8080, { hostname, token: 'v1-stable' }); +// Works - same port, same token +``` + + +### `validatePortToken()` + +Validate if a token is authorized to access a specific exposed port. Useful for custom authentication or routing logic. + +```ts +const isValid = await sandbox.validatePortToken(port: number, token: string): Promise +``` + +**Parameters**: + +- `port` - Port number to check +- `token` - Token to validate + +**Returns**: `Promise` - `true` if token is valid for the port, `false` otherwise + + +```ts +// Custom validation in your Worker +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + + // Extract token from custom header or query param + const customToken = request.headers.get('x-access-token'); + + if (customToken) { + const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); + const isValid = await sandbox.validatePortToken(8080, customToken); + + if (!isValid) { + return new Response('Invalid token', { status: 403 }); + } + } + + // Handle preview URL routing + const proxyResponse = await proxyToSandbox(request, env); + if (proxyResponse) return proxyResponse; + + // Your application routes + return new Response('Not found', { status: 404 }); + } +}; +``` + + ### `unexposePort()` Remove an exposed port and close its preview URL. @@ -137,4 +227,4 @@ export default { ## Related resources - [Preview URLs concept](/sandbox/concepts/preview-urls/) - How preview URLs work -- [Commands API](/sandbox/api/commands/) - Start background processes +- [Commands API](/sandbox/api/commands/) - Start background processes \ No newline at end of file diff --git a/src/content/docs/sandbox/concepts/preview-urls.mdx b/src/content/docs/sandbox/concepts/preview-urls.mdx index 1d98a3b8e41d48c..e67e4cb9481f176 100644 --- a/src/content/docs/sandbox/concepts/preview-urls.mdx +++ b/src/content/docs/sandbox/concepts/preview-urls.mdx @@ -18,21 +18,56 @@ const { hostname } = new URL(request.url); await sandbox.startProcess("python -m http.server 8000"); const exposed = await sandbox.exposePort(8000, { hostname }); -console.log(exposed.exposedAt); -// Production: https://8000-abc123.yourdomain.com +console.log(exposed.url); +// Production: https://8000-sandbox-id-abc123random4567.yourdomain.com // Local dev: http://localhost:8787/... ``` ## URL Format -**Production**: `https://{port}-{sandbox-id}.yourdomain.com` +**Production**: `https://{port}-{sandbox-id}-{token}.yourdomain.com` -- Port 8080: `https://8080-abc123.yourdomain.com` -- Port 3000: `https://3000-abc123.yourdomain.com` +- With auto-generated token: `https://8080-abc123-random16chars12.yourdomain.com` +- With custom token: `https://8080-abc123-my-api-v1.yourdomain.com` **Local development**: `http://localhost:8787/...` -Preview URLs remain stable while a port is exposed and can be shared during that time. However, if you unexpose and re-expose a port, a new random token is generated and the URL changes. For persistent URLs, keep ports exposed for the duration you need them accessible. +## Token Types + +### Auto-generated tokens (default) + +When no custom token is specified, a random 16-character token is generated: + +```typescript +const exposed = await sandbox.exposePort(8000, { hostname }); +// https://8000-sandbox-id-abc123random4567.yourdomain.com +``` + +URLs with auto-generated tokens change when you unexpose and re-expose a port. + +### Custom tokens for stable URLs + +For production deployments or shared URLs, specify a custom token to maintain consistency across container restarts: + +```typescript +const stable = await sandbox.exposePort(8000, { + hostname, + token: 'api-v1' +}); +// https://8000-sandbox-id-api-v1.yourdomain.com +// Same URL every time ✓ +``` + +**Token requirements:** +- 1-16 characters long +- Lowercase letters (a-z), numbers (0-9), hyphens (-), and underscores (_) only +- Must be unique within each sandbox + +**Use cases for custom tokens:** +- Production APIs with stable endpoints +- Sharing demo URLs with external users +- Documentation with consistent examples +- Integration testing with predictable URLs ## ID Case Sensitivity @@ -45,7 +80,7 @@ Preview URLs extract the sandbox ID from the hostname to route requests. Since h const sandbox = getSandbox(env.Sandbox, 'MyProject-123'); // Durable Object ID: "MyProject-123" await sandbox.exposePort(8080, { hostname }); -// Preview URL: 8000-myproject-123.yourdomain.com +// Preview URL: 8080-myproject-123-token123.yourdomain.com // Routes to: "myproject-123" (different DO - doesn't exist!) ``` @@ -56,7 +91,7 @@ const sandbox = getSandbox(env.Sandbox, 'MyProject-123', { normalizeId: true }); // Durable Object ID: "myproject-123" (lowercased) -// Preview URL: 8000-myproject-123.yourdomain.com +// Preview URL: 8080-myproject-123-token123.yourdomain.com // Routes to: "myproject-123" (same DO - works!) ``` @@ -101,9 +136,9 @@ await sandbox.startProcess("node admin.js"); // Port 3001 const api = await sandbox.exposePort(3000, { hostname, name: "api" }); const admin = await sandbox.exposePort(3001, { hostname, name: "admin" }); -// Each gets its own URL: -// https://3000-abc123.yourdomain.com -// https://3001-abc123.yourdomain.com +// Each gets its own URL with unique tokens: +// https://3000-abc123-random16chars01.yourdomain.com +// https://3001-abc123-random16chars02.yourdomain.com ``` ## What Works @@ -131,10 +166,10 @@ const { hostname } = new URL(request.url); // Start a WebSocket server await sandbox.startProcess("bun run ws-server.ts 8080"); -const { exposedAt } = await sandbox.exposePort(8080, { hostname }); +const { url } = await sandbox.exposePort(8080, { hostname }); // Clients connect using WebSocket protocol -// Browser: new WebSocket('wss://8080-abc123.yourdomain.com') +// Browser: new WebSocket('wss://8080-abc123-token123.yourdomain.com') // Your Worker routes automatically export default { @@ -155,9 +190,10 @@ Preview URLs are publicly accessible by default, but require a valid access toke **Built-in security**: -- **Token-based access** - Each exposed port gets a unique token in the URL (for example, `https://8080-sandbox-abc123token.yourdomain.com`) +- **Token-based access** - Each exposed port gets a unique token in the URL (for example, `https://8080-sandbox-abc123token456.yourdomain.com`) - **HTTPS in production** - All traffic is encrypted with automatic TLS -- **Unpredictable URLs** - Tokens are randomly generated and difficult to guess +- **Unpredictable URLs** - Auto-generated tokens are randomly generated and difficult to guess +- **Token collision prevention** - Custom tokens are validated to ensure uniqueness within each sandbox **Add application-level authentication**: diff --git a/src/content/docs/sandbox/guides/expose-services.mdx b/src/content/docs/sandbox/guides/expose-services.mdx index ee82fb05d96dbe8..13ab454c7ec7c20 100644 --- a/src/content/docs/sandbox/guides/expose-services.mdx +++ b/src/content/docs/sandbox/guides/expose-services.mdx @@ -88,6 +88,45 @@ This lowercases the ID during creation so it matches preview URL routing. Withou See [Sandbox options](/sandbox/configuration/sandbox-options/#normalizeid) for details. ::: +## Stable URLs with custom tokens + +For production deployments or when sharing URLs with users, use custom tokens to maintain consistent preview URLs across container restarts: + + +``` +// Extract hostname from request +const { hostname } = new URL(request.url); + +// Without custom token - URL changes on restart +const exposed = await sandbox.exposePort(8080, { hostname }); +// https://8080-sandbox-id-random16chars12.yourdomain.com + +// With custom token - URL stays the same across restarts +const stable = await sandbox.exposePort(8080, { + hostname, + token: 'api-v1' +}); +// https://8080-sandbox-id-api-v1.yourdomain.com +// Same URL after container restart ✓ + +return Response.json({ + 'Temporary URL (changes on restart)': exposed.url, + 'Stable URL (consistent)': stable.url +}); +``` + + +**Token requirements:** +- 1-16 characters long +- Lowercase letters (a-z), numbers (0-9), hyphens (-), and underscores (_) only +- Must be unique within each sandbox + +**Use cases:** +- Production APIs with stable endpoints +- Sharing demo URLs with external users +- Integration testing with predictable URLs +- Documentation with consistent examples + ## Name your exposed ports When exposing multiple ports, use names to stay organized: @@ -97,15 +136,23 @@ When exposing multiple ports, use names to stay organized: // Extract hostname from request const { hostname } = new URL(request.url); -// Start and expose API server +// Start and expose API server with stable token await sandbox.startProcess('node api.js', { env: { PORT: '8080' } }); await new Promise(resolve => setTimeout(resolve, 2000)); -const api = await sandbox.exposePort(8080, { hostname, name: 'api' }); +const api = await sandbox.exposePort(8080, { + hostname, + name: 'api', + token: 'api-prod' +}); -// Start and expose frontend +// Start and expose frontend with stable token await sandbox.startProcess('npm run dev', { env: { PORT: '5173' } }); await new Promise(resolve => setTimeout(resolve, 2000)); -const frontend = await sandbox.exposePort(5173, { hostname, name: 'frontend' }); +const frontend = await sandbox.exposePort(5173, { + hostname, + name: 'frontend', + token: 'web-app' +}); console.log('Services:'); console.log('- API:', api.exposedAt); @@ -330,10 +377,10 @@ See [Sandbox options - normalizeId](/sandbox/configuration/sandbox-options/#norm ## Preview URL Format -**Production**: `https://{port}-{sandbox-id}.yourdomain.com` +**Production**: `https://{port}-{sandbox-id}-{token}.yourdomain.com` -- Port 8080: `https://8080-abc123.yourdomain.com` -- Port 5173: `https://5173-abc123.yourdomain.com` +- Auto-generated token: `https://8080-abc123-random16chars12.yourdomain.com` +- Custom token: `https://8080-abc123-my-api-v1.yourdomain.com` **Local development**: `http://localhost:8787/...`