# DOX — packages/browser-plugin/src/server

Files in this directory. One row per source file. See change: add-browser-relay.

| File | Purpose |
|------|---------|
| `index.ts` | Server entry `registerPlugin` — composition root. Builds ONE `AuditRing`, ONE `RelayManager` (deps: unredacted `ctx.getPluginConfig`, lazy `ctx.fastify.server.address().port`, real `canOpenChrome()`/`listChromeProfiles()`, `onStatusChange → status.broadcastNow`), ONE `BrowserRelayStatus`, then mounts WS routes + gateway handlers + REST routes. `PI_BROWSER_RELAY_FAKE=1` seeds the `Fake` instance (env-only, never config). `ctx.onShutdown` disposes the status timer + `manager.closeAll`. Plugin-disable teardown needs no hook: `teardownPlugin` closes tracked sockets 1001 → `RelayInstance` finalizes → manager entry dropped. Tasks 2.1, 2.10b, 2.11, 3.6. |
| `audit.ts` | `AuditRing` (cap `AUDIT_CAPACITY`=500) + `AuditKind`. Entries `{ts, profileDirectory, instanceId, kind, detail}`; `detail` string-only so a payload/guid/token cannot leak (coerced, never stored as object). `auditSeq` is a monotonic COUNTER, not `entries.length` — the client's audit-refresh signal. `list(profile?)` newest-first. `setOnAppend(fn)` observes every append (status broadcaster coalesces off it; observer throw swallowed). Task 2.6 / E15, E16. |
| `capability.ts` | `canOpenChrome(deps)` = `computeSystemOpen()` (shared) AND Chrome user-data dir exists; `chromeUserDataDir(platform, env)`; `buildChromeOpenCommand` → macOS `open -na "Google Chrome" --args --profile-directory=<dir> <url>` (plain `open <url>` would ignore the profile and hit the default browser). Task 2.10. |
| `connect.ts` | `PLAYWRIGHT_EXTENSION_ID` (`mmlmfjhmonkocbjadbfplnigmagldckm`), `buildConnectUrl` (connect.html + `mcpRelayUrl=ws://127.0.0.1:<port>/ws/browser-ext/<guid>` + `protocolVersion=2`, `token` ONLY when `zeroDialog`), `openChromeProfile` (detached spawn via shared `platform/exec`). Token mismatch/Reject/no-answer are deliberately indistinguishable → one 504. Task 2.8 / E18. |
| `profiles.ts` | `listChromeProfiles(deps)` reads `Local State → profile.info_cache`, keys rows by `profileDirectory` (labels duplicate), `installed` via vendored `isExtensionInstalledInProfile`. Missing/corrupt/empty cache → one synthetic `Default` row + `warning` naming the path (200, not an error). Task 2.7 / E17, X9. |
| `status.ts` | `BrowserRelayStatus` — `browser_relay_status` composer + the three relay gateway handlers. `message()` maps manager instances (`statusState()` + `tabList()`) + `auditSeq`. `broadcastNow()` immediate (instance/tab change, via manager `onStatusChange`), `schedule()` coalesced ≤1 per `STATUS_COALESCE_MS`=500 on audit append (via `audit.setOnAppend`). `registerHandlers(ctx)` owns `browser_relay_subscribe|unsubscribe|input`, keyed `{instanceId, tabId}` — malformed/unknown refs dropped + audited (`browser_relay_*` detail), socket close = `unsubscribeAll` on every live instance (`_trackClose` once per socket). Frames bypass this module (per-socket in the tap). Tasks 2.11, 3.4, 3.6 / E22, P3, X12. |
| `routes.ts` | Six `/api/browser/*` routes on `ctx.fastify`: `GET status` (`{enabled, canOpenChrome}`), `GET profiles` (rows keyed by `profileDirectory` + `installed`/`hasToken` + `instances[].tabs[]`; optional `?profileDirectory=`; carries the synthetic-`Default` `warning`), `POST connect` (`{profileDirectory}` → 200 `{cdpUrl, instanceId}` / 400 / 403 disabled / 409 not-installed|busy / 503 / 504), `POST disconnect?instanceId=` (400 missing / 404 unknown / 403 disabled), `GET audit?profile=` (newest-first), `PUT enabled` (kill switch; 400 non-boolean). Reads always answer; WRITES 403 while disabled; PUT is the way back on. TOKEN SAFETY: `getPluginConfig()` is unredacted in-plugin, so a row carries ONLY `hasToken` — never the config. Task 2.9. |
| `ws-routes.ts` | `registerBrowserWsRoutes(ctx, manager)` registers both relay scopes via `ctx.registerWsRoute` during activation. `browser-ext` (`/ws/browser-ext/<guid>`, `admitOrigins=[CHROME_EXTENSION_ORIGIN]` pinned exact), `browser-cdp` (`/ws/browser-cdp/<guid>`, empty `admitOrigins` + handler refuses ANY `Origin` header). `handleUpgrade` extracts guid → `manager.resolve` (404 unknown/expired/malformed/nested), completes handshake via `WebSocketServer({noServer:true})`, then `meta.trackSocket` + `manager.attachExtension|attachCdp`. Core gates (host/origin/local-peer/ticket) run BEFORE delegation — group 1. `RelayAttacher` narrow seam. Task 2.5 / E10. |
| `relay/` | Relay + tap machinery — see `relay/AGENTS.md` and `relay/vendor/AGENTS.md` (never-edit rule, shims, refresh policy). |

Files in `__tests__/`:

| File | Purpose |
|------|---------|
| `audit.test.ts` | E15 (cap/drop-oldest across 499/500/501/600, `auditSeq` strictly increasing past the cap, newest-first, secret-free JSON) + E16 (detail is URL/method/kind, non-string coerced). |
| `capability.test.ts` | 2.10 verify: false on `PI_DASHBOARD_SYSTEM_OPEN=0` / missing dir / display-less Linux; true on override+dir; per-OS user-data dir; D4 argv incl. a space-bearing directory as ONE element. |
| `connect.test.ts` | E18: pinned extension host, `protocolVersion=2`, loopback `mcpRelayUrl`, `token` only with `zeroDialog`, argv elements. |
| `fake-relay-instance.test.ts` | 2.10b / E28: one tab, `connected`; immediate frame then ≥5 frames/s under fake timers; refuses a foreign tab; input echoed to the audit; malformed input → `denied`; silent after `close`. |
| `index.test.ts` | Wiring: both WS scopes + three handlers + REST routes mounted once; `onShutdown` registered; `PI_BROWSER_RELAY_FAKE` unset → no `Fake` row, set → one `Fake` row with a tab. |
| `profiles.test.ts` | E17 (3 profiles incl. duplicate labels + one email, `installed` exactly once, label fallback) + X9 (dir absent / `{not json` / empty cache → one `Default` row + warning naming the path; synthetic row still checks `Default/Extensions`). |
| `relay-manager.test.ts` | E9 (guid regex BVA, resolve, claim-once, second ext socket 1000), E19 (409 `not-installed` / `busy`), E20 (disconnect param), E21 (kill switch), E28 (`seedFake`), X1 (504 + guid dropped), X10 (503), the both-paths 2.2b decision (`allowMultipleInstancesPerProfile`), and the 2.11 open + connect-latency log lines. |
| `routes.test.ts` | Task 2.9. Fastify `inject`: status payload; profiles keyed rows + `hasToken` (token never in the body) + `?profileDirectory=` + `warning`; connect 200/400/403/409(×2)/503/504; disconnect 200/400/404/403; audit newest-first + `?profile=`; `PUT enabled` persists + kill switch + 400 non-boolean. |
| `status.test.ts` | E22 (composed instances/tabs 5+9, no `guid`/`token` keys, `auditSeq`), P3 (100 audit appends → exactly 1 broadcast per 500 ms ending on the last seq; re-arm after a batch; `broadcastNow` cancels a pending emit), 3.4 devtools `reason` passthrough, 3.6 (handler registration, valid/unknown/malformed refs, remote-address forwarding, close → `unsubscribeAll` on every instance). |
| `vendor-integrity.test.ts` | Scenario X14/7.57. SHA-256 every file under `relay/vendor/playwright-core/` against `vendor-hashes.json` (set-equality: no edits, no additions); NOTICE carries upstream commit. Also pins shim contracts: `CDPRelayServer` constructs inert, `start()` rejects `not supported — transport is supplied by relay-instance.ts`; registry shim throws on launch; `ManualPromise` real (resolve/reject/isDone). node env. |
| `ws-routes.test.ts` | Task 2.5 / E10. Real `http.Server` + `ws` pair: scope/prefix/admitOrigins registration shape; ext live guid → 101 + `attachExtension` + `trackSocket`; ext unknown/empty/malformed/nested guid → 404; cdp `Origin`-bearing → 403 even for live guid; cdp header-less live guid → 101 + `attachCdp`; cdp unclaimed-but-live → 101 (client may arrive first). Core gates NOT re-simulated — see `packages/server/src/__tests__/plugin-ws-route.test.ts`. |
| `vendor-hashes.json` | Recorded hashes + `upstreamCommit` (`d1ead3ecca23182f2d06d761c28e3d4edafb6595`, 2026-09-11). Regenerate on refresh — see the embedded `$comment` recipe. |
