# WebX Managed Proxies

WebX can **lease** an upstream proxy for a session by geography instead of the
caller supplying credentials — and, uniquely, records the **observed exit
country** so the session receipt proves *where the browser actually egressed*,
not merely where it was asked to.

## What it adds over an external proxy

WebX already accepts an external `--proxy-server` and answers authenticated
`407` challenges. The managed layer adds, on top of that:

- A **`ProxyProvider`** abstraction (default: an operator-supplied static pool;
  a residential vendor drops in behind the same trait).
- **Geo-routing** — select a pool entry by `country` / `state` / `city`.
- **Sticky egress** — a `stickyKey` maps a caller to a stable IP across leases.
- **Bandwidth metering** — real wire bytes (`proxyBytes`) counted from the
  browser's CDP `Network` events.
- **Egress verification** — an out-of-band IP-geo lookup *through the leased
  proxy* records the actual exit country.
- A **proxy receipt** on the session: provider, requested vs observed country,
  a one-way lease hash, bytes, and rotation policy.

Credentials live only in the pool config (a secret) and the leased launch args —
never in logs, session metadata, or the receipt (which exposes a hashed
`leaseHash`, never the username/password).

## Enabling it

Managed proxies are **opt-in** and advertised only when a pool is configured;
otherwise a managed request fails honestly (`400 managed_proxy_unavailable`).

| Env var | Purpose |
| --- | --- |
| `WEBX_PROXY_POOL` | Inline JSON array of pool entries (legacy static pool). |
| `WEBX_PROXY_POOL_FILE` | Path to the same JSON (legacy static pool). |
| `WEBX_PROXY_INVENTORY_FILE` | Operator-managed JSON inventory file that can refresh without restart. |
| `WEBX_PROXY_INVENTORY_URLS` | Comma-separated signed HTTPS inventory endpoints. |
| `WEBX_PROXY_INVENTORY_SIGNING_SECRET` | HMAC secret used to verify signed HTTPS inventory payloads. |
| `WEBX_PROXY_GEO_CHECK_URL` | IP-geo endpoint used to verify the observed exit country (e.g. `https://ipinfo.io/json`). Unset → `observedCountry` is honestly `null`. |

A pool/inventory entry:

```json
[
  {
    "server": "http://gateway.vendor:8080",
    "username": "u", "password": "p",
    "country": "US", "state": "CA", "city": "SAN_FRANCISCO",
    "provider": "vendor-a",
    "pool": "residential-login",
    "region": "us-east-1",
    "healthCheckUrl": "https://inventory.vendor.example/health",
    "credentialVersion": "2026-08-03"
  }
]
```

## Requesting a managed proxy

Use the Browserbase-compatible `proxies` field on session create. `true` leases
any managed egress; an object selects a geography:

```jsonc
// POST /v1/sessions
{
  "proxies": [
    {
      "type": "webx",
      "geolocation": { "country": "US", "state": "CA", "city": "SAN_FRANCISCO" },
      "rotation": "per_session",   // per_session (default) | per_request | on_failure
      "stickyKey": "login-flow-7"  // optional: stable egress for this caller
    }
  ]
}
```

An external proxy is still supported and applied verbatim:

```jsonc
{ "proxies": [{ "type": "external", "server": "http://host:8080",
                "username": "u", "password": "p" }] }
```

## The proxy receipt

The create/get response carries a `proxy` object (present only for a managed
lease):

```jsonc
{
  "proxyBytes": 148223,
  "proxy": {
    "provider": "vendor-a",
    "requestedGeo": "US-CA",
    "observedCountry": "US",   // verified THROUGH the proxy; null if unverified
    "leaseHash": "1654bddb4ea2f043",
    "bytes": 148223,
    "rotations": 0,
    "rotationPolicy": "per_session"
  }
}
```

`observedCountry` is the evidence: WebX fetched an IP-geo endpoint through the
leased proxy and recorded the real exit country. It is **never fabricated** — a
failed or unconfigured check leaves it `null` rather than echoing the request.

## Behavior & limits (MVP)

- The static pool honors **per-session** rotation (a session keeps its egress
  IP); `per_request` / `on_failure` are recorded on the receipt for a vendor
  adapter that implements them, and treated as per-session by the static pool.
- Inventory can refresh from operator files or signed HTTPS endpoints without a
  restart. Sticky callers keep their existing lease hash while the same entry
  remains present; new credentials or new pools are picked up on the next lease.
- Entries whose explicit `healthCheckUrl` fails are quarantined temporarily and
  excluded from leasing; requests fail closed if every matching entry is
  quarantined.
- Transport-failure health scoring can quarantine endpoints in-process; geo
  mismatch quarantines immediately. Identity-preserving failover prefers the
  minimum identity-distance healthy route and **fails closed** when none exists
  within the safe distance budget (country changes are never silent).
- Optional inventory fields: `proxyType` (`datacenter`|`isp`|`residential`|
  `mobile`), `maxSessions`, `asn`, and price hints (`pricePerSession` /
  `pricePerGb` / `pricePerMinute`) for cost-aware scheduling when budgets carry
  `network_usd`.
- A request whose geography/pool no live entry satisfies is **rejected** (`400
  managed_proxy_unavailable`) — never silently downgraded to a different region.
- `proxyBytes` is metered from the browser's proxied `Network.loadingFinished`
  wire bytes; it accrues only while the authenticated-proxy pump is attached
  (i.e. for authenticated pool entries, the normal case).
- The lease is released — and its receipt cleared — on session teardown
  (explicit release, timeout, or graceful shutdown).
- Qualification without `WEBX_PROXY_GEO_CHECK_URL` returns honest
  `qualified: false` / unevaluable — never fabricates exit geography.

## Session identity coherence

Browser fingerprint, managed lease, geography, DNS mode, and persistence policy
compose into a single `SessionIdentity` (`WEBX_IDENTITY_POLICY=permissive|coherent|strict`).
Default is **permissive** (backward compatible). Coherent/strict fail closed on
high-value contradictions (timezone vs country, mobile proxy + desktop UA,
context country jumps, Chrome major vs `Browser.getVersion` when observed).
Persistent contexts store a secret-free `webx-network-identity.json` so reuse
rebinds a compatible network family beside cookies.
