Metadata-Version: 2.5
Name: browserious
Version: 0.2.1
Summary: Browserious command-line client and MCP server
License-Expression: MIT
Requires-Python: >=3.11
Requires-Dist: fastmcp<3,>=2.13
Requires-Dist: httpx>=0.27
Requires-Dist: playwright>=1.40
Requires-Dist: pywin32>=311; sys_platform == 'win32'
Requires-Dist: websockets>=13.0
Description-Content-Type: text/markdown

# Browserious - Browser API Wrapper

Self-hosted, API-controlled browser-as-a-service for automation, testing, and web scraping.

[![Tests](https://github.com/muzhig/browserious/actions/workflows/ci.yml/badge.svg)](https://github.com/muzhig/browserious/actions/workflows/ci.yml)
[![Python](https://img.shields.io/badge/python-3.11+-blue.svg)](https://python.org)
[![Docker](https://img.shields.io/badge/docker-required-blue.svg)](https://docker.com)

## Overview

Browserious provides a REST API + WebSocket interface to control Chromium browsers running in Docker containers. Each browser session runs in an isolated container with:

- **Persistent Profiles** - Maintain cookies, localStorage, and session state
- **Proxy Support** - Configure HTTP/HTTPS/SOCKS5 proxies per session
- **Extensions** - Load custom Chrome extensions
- **Request Interception** - Block, redirect, or mock network requests
- **Remote Viewing** - Watch browser sessions via VNC
- **CDP Access** - Direct Chrome DevTools Protocol control

**Primary Use Cases:**
- Browser automation and E2E testing
- Web scraping and data extraction
- Remote browser service

## Quick Start

### Prerequisites

- Docker and Docker Compose
- Python 3.11+ (for development)

### Installation

```bash
# Clone repository
git clone https://github.com/muzhig/browserious.git
cd browserious

# Generate API key
export API_KEYS=$(openssl rand -base64 32)

# Start service
docker-compose up -d

# Verify
curl -H "X-API-Key: $API_KEYS" http://localhost:8100/sessions
```

### Create Your First Session

```bash
# Create browser session
SESSION_ID=$(curl -X POST http://localhost:8100/sessions \
  -H "X-API-Key: $API_KEYS" \
  -H "Content-Type: application/json" \
  -d '{"timeout": 600}' \
  | jq -r '.session_id')

# Navigate to a page
curl -X POST http://localhost:8100/sessions/$SESSION_ID/pages/page-0/goto \
  -H "X-API-Key: $API_KEYS" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com"}'

# Take screenshot
curl -H "X-API-Key: $API_KEYS" \
  http://localhost:8100/sessions/$SESSION_ID/pages/page-0/screenshot \
  > screenshot.png

# Watch in browser (VNC)
# Open the web_vnc_url from the session response, e.g.:
# http://localhost:8100/vnc.html?session=$SESSION_ID

# Cleanup
curl -X DELETE http://localhost:8100/sessions/$SESSION_ID \
  -H "X-API-Key: $API_KEYS"
```

## Architecture

```
┌─────────────────────────────────────────────────────────┐
│  API Container (browserious-api)                         │
│  ┌───────────────────────────────────────────────────┐  │
│  │  FastAPI Application :8100                        │  │
│  │  - REST API + WebSocket                           │  │
│  │  - VNC WebSocket proxy /sessions/{id}/vnc         │  │
│  │  - Web VNC viewer /vnc.html?session={id}          │  │
│  │  - Docker API (creates/manages browser nodes)     │  │
│  └───────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────┘
           │
           │ Docker Network: browserious
           ▼
┌─────────────────────────────────────────────────────────┐
│  Browser Node Containers (one per session)              │
│                                                         │
│  ┌─────────────────┐    ┌─────────────────┐            │
│  │ session-abc123  │    │ session-xyz789  │   ...      │
│  │                 │    │                 │            │
│  │  Xvfb :99       │    │  Xvfb :99       │            │
│  │  x11vnc :5900   │    │  x11vnc :5900   │            │
│  │  Chromium       │    │  Chromium       │            │
│  │  CDP :9222      │    │  CDP :9222      │            │
│  └─────────────────┘    └─────────────────┘            │
│                                                         │
│  (No port conflicts - each node uses same internal     │
│   ports, addressed by container hostname/IP)           │
└─────────────────────────────────────────────────────────┘
```

## Features

### ✅ Session Management
- Create isolated browser sessions
- Configure proxy, viewport, user agent
- Persistent profiles with cookie/storage management
- Automatic timeout and cleanup
- Restart-safe session cleanup with durable retry state and ownership-scoped orphan reporting

### ✅ Page Operations
- Navigate, click, type, fill forms
- Multiple selector strategies (CSS, XPath, Playwright, JS)
- Wait for elements (visibility, loading states)
- Execute JavaScript in page context
- Screenshots (full page or viewport)
- HTML content extraction

### ✅ Request Interception
- Block requests (ads, trackers, images)
- Redirect URLs (mock APIs, override media)
- Override responses (status, headers, body)
- Inspect and capture requests

### ✅ Extensions
- Upload custom Chrome extensions
- Load extensions per session
- Support for .zip and .crx formats

### ✅ Remote Access
- VNC server with web client (noVNC)
- Per-session password protection
- Real-time browser viewing

### ✅ CDP WebSocket
- Direct Chrome DevTools Protocol access
- Compatible with Puppeteer/Playwright
- Full browser control for advanced use cases

## Documentation

| Document | Description |
|----------|-------------|
| [PRD](docs/PRD-main.md) | Product Requirements Document - features, use cases, MVP scope |
| [ADR](docs/ADR.md) | Architecture Decision Records - key technical decisions and rationale |
| [API](docs/API.md) | Complete API Reference - endpoints, examples, error codes |
| [EXAMPLES](docs/EXAMPLES.md) | Code examples - form interaction, content extraction, visual debugging |
| [SECURITY](docs/SECURITY.md) | Security Analysis - threat model, hardening, best practices |
| [Project Brief](docs/project-brief.md) | Original project specification |

## Browserious CLI and MCP

The `browserious` wheel contains the complete CLI, shared OAuth client, and optional MCP compatibility adapter. The CLI is the primary interface.

### Setup

Install:

```bash
pip install browserious
browserious login
browserious whoami
browserious session create
```

For an isolated run use `uvx browserious --help`. Pin a release for reproducible automation; because `uvx` caches environments, add `--refresh` when it must resolve the requested release again:

```bash
uvx --refresh --from browserious==0.2.0 browserious --version
```

`browserious login` uses device authorization and stores rotating OAuth credentials. Use `--no-browser` on a headless host. CLI/MCP browser commands use authenticated shared REST operations and share server-owned sessions by ID without a process-local registry. The local tunnel owner alone requests a short-lived tunnel capability. Run `browserious --help` and `browserious GROUP --help` for the installed wheel's normative command inventory; [the API guide](docs/API.md#browserious-cli) lists all command groups, output and exit semantics, tunnels, and MCP parity.

The CLI defaults to human output; `--json` or `--output json` emits compact JSON. Errors go to stderr. Secrets are recursively redacted unless `--show-secrets` is used on one of the four bounded commands that permit it (`session create`, `file get`, `profile get`, `profile export`). Exit codes classify success (`0`), local validation (`2`), auth (`3`), forbidden (`4`), not found (`5`), conflict (`6`), rate limit (`7`), transport/server (`8`), other HTTP (`9`), and interrupted foreground tunnel (`130`).

Open a foreground localhost tunnel with:

```bash
browserious tunnel open SESSION_ID --local-port 3000
```

The command owns its WebSocket until interrupted. `--detach` transfers ownership to the per-user daemon; inspect it with `browserious tunnel list` and stop it with `browserious daemon stop`. Detached tunnels require Unix-domain sockets, while foreground tunnels remain available on unsupported daemon platforms.

### Local MCP compatibility

Run MCP explicitly from the same wheel with `browserious mcp`. It uses the CLI credential store, refresh lifecycle, capability client, operations, scopes, and revocation path. A pinned local host configuration is:

```json
{
  "mcpServers": {
    "browserious": {
      "command": "uvx",
      "args": ["--refresh", "--from", "browserious==0.2.0", "browserious", "mcp"],
      "env": {
        "BROWSERIOUS_API_URL": "https://api.browserious.com"
      }
    }
  }
}
```

Credential precedence is `--access-token`, `BROWSERIOUS_ACCESS_TOKEN`, legacy `BROWSERIOUS_API_KEY`, then the stored login. Normal interactive setup uses device login rather than pasted `br_live_*` tokens. The 0.2.0 compatibility release contains exactly two deprecated, transitional bridges: `python -m browserious_mcp`, and zero-argument `browserious` when both standard streams are non-TTY. Interactive zero-argument invocation shows CLI help. Eventual MCP retirement removes the verb, bridges, `browserious_mcp` module, MCP dependencies, parity tests, and MCP docs from this wheel; it does not create or migrate to a second package. No later removal version is promised.

### Optional remote MCP

The same bundled adapter is also available over stateless Streamable HTTP at `https://api.browserious.com/mcp`. Remote clients discover the shared Browserious authorization server, dynamically register as public clients, and complete authorization-code authentication with S256 PKCE. They request the existing `browser:read browser:write tunnel` scopes and use the same rotating `br_at_*` and `br_rt_*` token families as other Browserious clients.

Discovery starts from the `401` challenge's RFC 9728 `resource_metadata` URL, `https://api.browserious.com/.well-known/oauth-protected-resource/mcp`. That document identifies the resource and authorization server; the client then reads `https://api.browserious.com/.well-known/oauth-authorization-server`, registers at the advertised endpoint, and uses the advertised authorization/token/revocation endpoints with the exact redirect URI and S256 verifier.

Claude Code can add the remote endpoint with:

```bash
claude mcp add --transport http browserious https://api.browserious.com/mcp
```

Claude Code configuration was sanitized and probed against this URL. Cursor and Windsurf onboarding remains manual-only; no completed client-specific OAuth flow is claimed. If a client cannot complete remote OAuth, use local stdio. Remote and stdio enumerate the same tools and call the same operations. Remote `open_tunnel` and `close_tunnel` fail safely because a service-host process cannot own a client-local port; use the CLI or local stdio MCP for tunnels.

Public DNS is Cloudflare Anycast/proxied, and requests traverse Cloudflare → Caddy → FastAPI. The local `192.168.1.3` address is an intentional host/VPN shortcut. Node callbacks send the explicit `Browserious-Node/0.2.0` service User-Agent to avoid Cloudflare error 1010. This topology makes no unverified streaming timeout promise.

### Available Tools

The wheel currently exposes 23 MCP tools. Runtime enumeration and the installed-wheel parity check are normative, not this descriptive count. Every tool has a CLI equivalent:

| Tool | Description |
|------|-------------|
| `create_session` | Create browser session, returns session_id and URLs |
| `delete_session` | Close and cleanup session |
| `list_sessions` | List all active sessions |
| `create_page` | Open new tab, optionally navigate to URL |
| `page_goto` | Navigate to URL |
| `page_click` | Click element (CSS, XPath, text, role, JS selectors) |
| `page_type` | Type text with keyboard events |
| `page_fill` | Fill input instantly |
| `page_eval` | Execute JavaScript, return result |
| `page_wait_for_selector` | Wait for element state |
| `page_content` | Get full HTML |
| `page_screenshot` | Capture page, return a reference plus metadata (opt into bytes with `inline`) |
| `display_screenshot` | Capture X11 display with cursor, same reference-first contract |
| `play_input` | Hardware-level mouse/keyboard via X11 |
| `get_cursor_position` | Get current cursor screen coordinates |
| `convert_coordinate` | Convert a viewport/page point to screen space, and return the offset for local batch conversion |
| `click_element` | Click an element with hardware input, resolved by selector — no calibration needed |
| `element_at_point` | Identify the element at viewport coordinates |
| `captcha_detect` | Detect CAPTCHA regions |
| `open_tunnel` | Open a client-owned localhost tunnel (local transports only) |
| `close_tunnel` | Close a client-owned localhost tunnel (local transports only) |
| `list_files` | List stored files |
| `get_file` | Return owner-scoped metadata and a redacted-by-default presigned download URL |
| `delete_file` | Delete a stored file |

### Screenshot handling

CLI screenshot commands write image bytes to `--output-file` or `browserious-output.bin`.

MCP screenshot tools return a reference plus metadata instead of an inline image, so a large capture
cannot flood a caller's context:

```json
{"session_id": "sess-1", "page_id": "page-0", "format": "png", "size_bytes": 454931,
 "width": 1920, "height": 4200, "path": "/tmp/browserious-screenshots/sess-1-page-0.png",
 "path_scope": "mcp-client-host", "file_id": null, "retrievable": true, "inline": false,
 "hint": "Local stdio MCP: ...", "warnings": []}
```

`inline=true` returns FastMCP image content, but only below `BROWSERIOUS_MCP_INLINE_MAX_BYTES`
(default 1 MiB; a value that is not a byte count is ignored); above it the tool raises a structured
error that still carries the reference. `path` resolves on the MCP server's host — the caller's own
machine over local stdio (`BROWSERIOUS_SCREENSHOT_DIR` overrides the directory), and `null` over
remote MCP, where `get_file(file_id)` is the way to the bytes. Each capture target reuses one stable
path, and the directory keeps only the most recent captures, so a verification loop cannot fill the
disk. When no path and no `file_id` can be produced, the tool still returns the reference with
`retrievable: false` and a `hint` naming the prerequisites, rather than discarding a capture that
already succeeded. Secret-bearing JSON fields remain redacted by default.

**Fonts are required for meaningful screenshots.** A browser container without fonts renders
invisible text, producing a structurally correct and completely worthless capture; installing
`fonts-dejavu-core` and `fontconfig` grew a real text-heavy capture roughly 15x. This is a property
of whichever environment renders the page — Browserious' own node image inherits fonts from its
Playwright base, so the risk is a browser container you build or run yourself. The signal is
relative, not absolute: a correct but sparse page (a login form, an empty state) is within ~1.2x of
a fontless full-page render in bytes per pixel, so size alone cannot diagnose it. Compare
`size_bytes` for the *same* page before and after installing fonts.

### Coordinate System

Hardware input (`play_input`) addresses the **X11 display**, not the browser viewport. You do not
have to derive that offset yourself — the server does it.

To click an element, use `click_element`: it resolves the selector, converts the geometry, and plays
an eased move + press/release, all server-side.

```bash
browserious input click-element sess-1 "textarea[name='q']"
```

To convert points yourself — for drags, custom playlists, or batching — use `input coordinate`. It
returns the converted point **and** the `offset`, so N targets cost one call:

```bash
browserious input coordinate sess-1 640 400
```

```json
{"x": 650, "y": 497, "offset": {"x": 10, "y": 97}, "on_screen": true,
 "geometry": {"screen_x": 10, "screen_y": 10, "inner_width": 1919, "inner_height": 992,
              "chrome_height": 87, "device_pixel_ratio": 1, "...": "..."}}
```

Then convert further points locally as `screen = offset + viewport_point`. `on_screen` reports
whether a point is actually clickable — inside both the viewport and the display.

The offset derives from window geometry as `(screenX + chromeWidth // 2, screenY + chromeHeight)`,
which measures `(10, 97)` on a default 1920x1080 node. Deriving it by hand is discouraged: the
endpoint refuses rather than guessing when it cannot calibrate (e.g. `devicePixelRatio != 1`),
whereas hand-rolled arithmetic silently mis-clicks.

## API Examples

### Web Scraping with Proxy

```python
import requests

API_KEY = "your-api-key"
BASE_URL = "http://localhost:8100"
headers = {"X-API-Key": API_KEY}

# Create session with proxy
session = requests.post(
    f"{BASE_URL}/sessions",
    headers=headers,
    json={
        "profile_id": "scraper-001",
        "proxy": {"server": "http://proxy.example.com:8080"},
        "timeout": 3600
    }
).json()
session_id = session["session_id"]

# Block images to speed up scraping
requests.post(
    f"{BASE_URL}/sessions/{session_id}/routes",
    headers=headers,
    json={
        "pattern": "**/*.{png,jpg,jpeg,gif}",
        "action": {"type": "block"}
    }
)

# Navigate and extract data
requests.post(
    f"{BASE_URL}/sessions/{session_id}/pages/page-0/goto",
    headers=headers,
    json={"url": "https://example.com"}
)

data = requests.post(
    f"{BASE_URL}/sessions/{session_id}/pages/page-0/eval",
    headers=headers,
    json={"script": "return document.querySelectorAll('.item').length"}
).json()

print(f"Found {data['result']} items")

# Cleanup
requests.delete(f"{BASE_URL}/sessions/{session_id}", headers=headers)
```

### E2E Testing with Mocked API

```python
# Create test session
session = requests.post(
    f"{BASE_URL}/sessions",
    headers=headers,
    json={"profile_id": "test-user"}
).json()
session_id = session["session_id"]

# Mock API response
requests.post(
    f"{BASE_URL}/sessions/{session_id}/routes",
    headers=headers,
    json={
        "pattern": "**/api/user",
        "action": {
            "type": "override",
            "status": 200,
            "body": '{"id": 123, "name": "Test User"}'
        }
    }
)

# Run test
requests.post(
    f"{BASE_URL}/sessions/{session_id}/pages/page-0/goto",
    headers=headers,
    json={"url": "https://app.example.com"}
)

# Interact with page
requests.post(
    f"{BASE_URL}/sessions/{session_id}/pages/page-0/click",
    headers=headers,
    json={"selector": {"strategy": "css", "value": "#login-button"}}
)

# Capture screenshot on failure
screenshot = requests.get(
    f"{BASE_URL}/sessions/{session_id}/pages/page-0/screenshot",
    headers=headers,
    params={"full_page": True}
).content

with open("test-failure.png", "wb") as f:
    f.write(screenshot)
```

### Using Puppeteer via CDP

```javascript
const puppeteer = require('puppeteer-core');

// Connect to existing session via CDP
const browser = await puppeteer.connect({
  browserWSEndpoint: 'ws://localhost:8100/sessions/{session_id}/cdp',
  headers: {Authorization: `Bearer ${process.env.BROWSERIOUS_ACCESS_TOKEN}`}
});

const page = await browser.newPage();
await page.goto('https://example.com');

const title = await page.title();
console.log('Page title:', title);

await page.screenshot({path: 'screenshot.png'});
```

## Configuration

### Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `API_KEYS` | - | Comma-separated API keys (required) |
| `PROFILES_DIR` | `/data/profiles` | Profile storage directory |
| `EXTENSIONS_DIR` | `/data/extensions` | Extensions storage directory |
| `MAX_SESSIONS` | 10 | Maximum concurrent sessions |
| `DEFAULT_TIMEOUT` | 3600 | Default session timeout (seconds) |
| `RESOLUTION` | `1920x1080x24` | Virtual display resolution |

#### Browser download capture

Off by default. When enabled, files the browser downloads are uploaded to file
storage as `file_type=download` and are then listed by `GET /files?file_type=download`
and the `list_files` MCP tool.

| Variable | Default | Description |
|----------|---------|-------------|
| `DOWNLOAD_CAPTURE_ENABLED` | `false` | Capture browser downloads into file storage |
| `DOWNLOADS_DIR` | `/data/downloads` | Download directory as the **API** sees it |
| `HOST_DOWNLOADS_DIR` | `` | Same directory as the **Docker host** sees it; required when the API itself runs in a container |
| `DOWNLOAD_MAX_BYTES` | `536870912` | Largest download stored (512 MiB) |
| `DOWNLOAD_DRAIN_TIMEOUT` | `30` | Seconds a closing session waits for downloads to finish |
| `DOWNLOAD_UPLOAD_TIMEOUT` | `30` | Seconds a closing session waits for those uploads to reach storage |
| `DOWNLOAD_STALL_TIMEOUT` | `5` | Seconds without progress after which a download is treated as hung |
| `DOWNLOAD_MAX_CONCURRENT_CAPTURES` | `4` | Concurrent uploads per session |

Prerequisites, all checked and logged at startup:

- `FILE_STORAGE_BACKEND` must not be `none`.
- `DOWNLOADS_DIR` must exist and be writable by the API process.
- The API and the browser container must resolve the directory to the **same bytes**.
  That holds for a local (BYOM) Docker backend with the volume below. It does **not**
  hold for the Fly backend, which cannot take host bind mounts — capture is skipped
  there, with a warning.
- Sessions authenticated with a legacy `X-API-Key` have no tenant to own the stored
  file, so capture is skipped for them, with a warning.

### Docker Compose

```yaml
services:
  api:
    build: .
    container_name: browserious-api
    ports:
      - "8100:8100"   # API + VNC viewer
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./profiles:/data/profiles
      - ./extensions:/data/extensions
      - ./downloads:/data/downloads
    environment:
      - API_KEYS=${API_KEYS}
      - API_HOST=localhost
      - API_PORT=8100
      - PROFILES_DIR=/data/profiles
      - EXTENSIONS_DIR=/data/extensions
      - DOWNLOADS_DIR=/data/downloads
      - HOST_PROFILES_DIR=${HOST_PROFILES_DIR}
      - HOST_EXTENSIONS_DIR=${HOST_EXTENSIONS_DIR}
      - HOST_DOWNLOADS_DIR=${HOST_DOWNLOADS_DIR}
      - DOCKER_NETWORK=browserious
      - DOCKER_IMAGE=browserious-node:latest
      - MAX_SESSIONS=10
      - DEFAULT_TIMEOUT=3600
    networks:
      - browserious
    restart: unless-stopped

networks:
  browserious:
    name: browserious
    driver: bridge
```

**Note:** The API container requires access to Docker socket to spawn browser node containers. Set `HOST_PROFILES_DIR`, `HOST_EXTENSIONS_DIR` and `HOST_DOWNLOADS_DIR` to the absolute paths on your host machine for volume mounts to work correctly in browser nodes. These are the paths the Docker daemon resolves, not the paths inside the API container; getting them wrong makes downloads land somewhere the API cannot read.

## Development

### Setup

```bash
# Clone repository
git clone https://github.com/muzhig/browserious.git
cd browserious

# Install dependencies
pip install -r requirements.txt
pip install -r requirements-dev.txt

# Install Playwright browsers
playwright install chromium

# Run tests
pytest

# Run locally (without Docker)
export API_KEYS=test-key
export DISPLAY=:99
Xvfb :99 -screen 0 1920x1080x24 &
python -m src.server
```

`src.server` enables Uvicorn proxy-header processing only for peers in
`FORWARDED_ALLOW_IPS`, an explicit comma-separated IP/CIDR allowlist. It defaults
to `127.0.0.1`, matching the supported local Caddy deployment. Keep that default
for direct API exposure; when another ingress connects to the container, set the
variable to that ingress network only. Wildcard trust is rejected.

### Rebuilding After Code Changes

Different parts of the codebase require different rebuild steps:

| Changed | Rebuild Command | Notes |
|---------|-----------------|-------|
| `src/*.py` | `docker-compose build api` | Python API code |
| `node/api/*.go` | `docker build -t browserious-node:latest ./node` | Go node API (input simulation) |
| `node/Dockerfile` | `docker build -t browserious-node:latest ./node` | Node container config |
| `node/entrypoint.sh` | `docker build -t browserious-node:latest ./node` | Node startup script |
| `Dockerfile` | `docker-compose build api` | API container config |
| `docker-compose.yml` | `docker-compose up -d` | Just restart, no rebuild |

**Quick reference:**

```bash
# After changing Python code (src/)
docker-compose build api && docker-compose up -d

# After changing Go code (node/api/)
docker build -t browserious-node:latest ./node

# After changing both
docker build -t browserious-node:latest ./node && docker-compose build api && docker-compose up -d

# Full rebuild from scratch
docker build -t browserious-node:latest ./node && docker-compose build --no-cache && docker-compose up -d
```

**Note:** Existing browser sessions use the node image that was current when they were created. New sessions will use the updated image.

### Project Structure

```
browserious/
├── docs/               # Documentation
│   ├── PRD.md
│   ├── ADR.md
│   ├── API.md
│   └── SECURITY.md
├── src/                # Source code
│   ├── main.py
│   ├── browser_manager.py
│   ├── page_operations.py
│   ├── route_manager.py
│   └── ...
├── tests/              # Test suite
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
└── README.md
```

## Deployment

### Production Checklist

- [ ] Generate strong API keys (`openssl rand -base64 32`)
- [ ] Set `JWT_SECRET` to 32+ random bytes (`openssl rand -base64 32`) — required outside `dev`/`testing`, the API refuses to start without it
- [ ] Enable HTTPS (nginx/Caddy reverse proxy)
- [ ] Configure firewall (restrict API/VNC to trusted IPs)
- [ ] Set resource limits (CPU, memory)
- [ ] Enable profile encryption (LUKS or application-level)
- [ ] Set up monitoring and alerting
- [ ] Configure backups
- [ ] Review [Security Documentation](docs/SECURITY.md)

### Horizontal Scaling

Run multiple instances with external load balancer:

```bash
# Instance 1
docker-compose up -d

# Instance 2 (different ports)
docker-compose -f docker-compose.instance2.yml up -d

# Load balancer (nginx)
upstream browserapi {
    server instance1:8000;
    server instance2:8000;
}
```

See [ADR-002](docs/ADR.md#adr-002-manual-orchestration-vs-built-in-scaling) for scaling strategy.

## Security

Browserious provides multiple security layers:
- API key authentication
- Docker container isolation
- Chromium sandbox
- Per-session VNC passwords
- Resource limits

**Important:** This service exposes significant attack surface. Review [SECURITY.md](docs/SECURITY.md) before production deployment.

### Quick Security Wins

1. **HTTPS Only**: Use TLS for API (nginx reverse proxy)
2. **Network Isolation**: Firewall rules to restrict access
3. **Strong API Keys**: Generate with `openssl rand -base64 32`
4. **Resource Limits**: Prevent DoS via docker-compose limits
5. **Profile Encryption**: Encrypt `/data/profiles` volume

## Troubleshooting

### Common Issues

**Session creation fails**
- Check Docker is running: `docker ps`
- Check logs: `docker-compose logs`
- Verify shm_size: `docker inspect | grep ShmSize`

**VNC not showing browser**
- Ensure you're using the web VNC URL: `http://localhost:8100/vnc.html?session={session_id}`
- Check that the session exists: `curl http://localhost:8100/sessions/{session_id}`
- Verify the browser node container is running: `docker ps | grep session-`

**Browser crashes**
- Increase shm_size in docker-compose.yml (recommended: 2g)
- Check memory limits
- Review container logs

**Profile corruption**
- Avoid concurrent access to same profile_id
- Use unique profile IDs: `user-{id}-{session-num}`
- See [ADR-003](docs/ADR.md#adr-003-shared-profile-concurrent-access)

## Roadmap

### MVP (Current)
- [x] Session management
- [x] Page operations (all selector strategies)
- [x] Request interception
- [x] Profile management
- [x] Extension support
- [x] VNC access
- [x] CDP WebSocket

### Post-MVP
- [ ] Metrics and monitoring (`/metrics`, `/health`)
- [ ] Session event webhooks
- [ ] Redis-based session registry (multi-instance)
- [ ] Session recording (Playwright trace)
- [ ] Firefox support
- [ ] Mobile device emulation

## Contributing

Contributions welcome! Please:

1. Fork the repository
2. Create feature branch (`git checkout -b feature/amazing-feature`)
3. Make changes following [code style guidelines](CONTRIBUTING.md#code-style-guidelines)
4. Run tests: `pytest tests/ -v`
5. Commit changes (`git commit -m 'Add amazing feature'`)
6. Push to branch (`git push origin feature/amazing-feature`)
7. Open Pull Request

### Quick Setup for Contributors

```bash
git clone https://github.com/muzhig/browserious.git
cd browserious
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt -r requirements-dev.txt
playwright install chromium
pytest tests/ -v
```

See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines.

## Acknowledgments

- [Playwright](https://playwright.dev/) - Browser automation framework
- [FastAPI](https://fastapi.tiangolo.com/) - Modern Python web framework
- [noVNC](https://github.com/novnc/noVNC) - HTML5 VNC client
- [x11vnc](https://github.com/LibVNC/x11vnc) - VNC server

## Support

- **Issues**: [GitHub Issues](https://github.com/muzhig/browserious/issues)
- **Documentation**: [docs/](docs/)
- **Security**: Report vulnerabilities to security@example.com

---

**Built for automation, testing, and scraping workflows.**
