# 01 — TS gateway (`tools/trap-sandbox/`) > Sub-plan of [trap-sandbox-gateway-spec](./trap-sandbox-gateway-spec.md). Implements the > **Protocol** and **Handshake** contracts defined there — do not redefine frame shapes or error > codes here; reference the spec. **Build first** (M1): no Lua dependency. ## Scope The TypeScript service: Fastify app on port 4444, the `trapos-cloud-gateway` module (protocol/registry/WS), the opencode probe, the auth seam, env/config, package wiring, and TS **unit** tests. Integration/e2e tests live in [04-integration-tooling](./04-integration-tooling.md). ## Task checklist - [ ] Scaffold `tools/trap-sandbox/` (ESM, `tsc` build, `tsx` dev/test, `node:test`, typescript-eslint flat config — copy `mcp-bridge`'s `tsconfig.json`/`eslint.config.js`). - [ ] `package.json`: deps + devDeps + scripts (see below). - [ ] `src/auth.ts` — `resolveAccount(secret)`. - [ ] `src/opencode.ts` — `createOpencodeProbe(config)`. - [ ] `src/modules/trapos-cloud-gateway/protocol.ts` — envelope/error types + parse/validate. - [ ] `src/modules/trapos-cloud-gateway/registry.ts` — `GatewayRegistry`. - [ ] `src/modules/trapos-cloud-gateway/index.ts` — Fastify plugin: `GET /health` (open) + `GET /gateway` (ws). - [ ] `src/app.ts` — `createApp(config)` returning `{ app, registry }`. - [ ] `src/index.ts` — env config, listen, `EADDRINUSE` clear message + exit. - [ ] TS unit tests (`test/`): protocol, registry, auth/hello, probe. - [ ] Gate: `npm test`, `npm run lint`/`check` clean. ## Layout ESM, `tsc` build, `tsx` dev/test, `node:test`, typescript-eslint flat config (copy mcp-bridge's `tsconfig.json`/`eslint.config.js`). Fastify `^5` + `@fastify/websocket` `^11`; `engines.node>=20`. ``` src/ ├── index.ts # env config; build app; listen on SANDBOX_PORT; clear EADDRINUSE message + exit ├── app.ts # createApp(config): Fastify + GatewayRegistry; register @fastify/websocket + module; returns { app, registry } ├── auth.ts # resolveAccount(secret): "local" | null (constant seam; null = unauthorized) ├── opencode.ts # createOpencodeProbe(config): probe(): { ok, detail } via @opencode-ai/sdk └── modules/trapos-cloud-gateway/ ├── index.ts # Fastify plugin: GET /health (open) + GET /gateway (ws) ├── protocol.ts # envelope/error types + parse/validate (4 events, hello, empty-payload→{}) └── registry.ts # GatewayRegistry keyed by (accountId,traposId); newest-wins; request()+timeout; pending cleanup on close test/ # TS unit (protocol, registry, auth/hello, probe) with FakeSocket + fake fetch ``` ## Behavior - **WS `/gateway`** (`{ websocket: true }`): no upgrade auth (auth is the hello frame). `maxPayload` ~1MB. `socket.on("message")` parses a frame inside try/catch → dispatch: pre-hello only `hello` is honored; post-hello handle `computer_request` (`health`, else `unknown_type`) and `computer_response` (resolve pending `gateway_request` by messageId). `close`/`error` → unregister + reject that connection's pendings. (Handshake steps & error codes: see spec **Handshake**.) - **GatewayRegistry**: `register`/`unregister`/`has`/`count`; `request(accountId, traposId, type, payload, timeoutMs=600000)` sends `gateway_request`, races matching `computer_response` vs timeout. Keyed by `(accountId, traposId)`, newest-wins. - **opencode probe**: `createOpencodeClient({ baseUrl: OPENCODE_URL, headers:{ Authorization: basic ("opencode", OPENCODE_SERVER_PASSWORD) }, throwOnError:false })` then `config.get()` with `AbortSignal.timeout(5000)`, wrapped in try/catch. `ok = response 2xx`; `detail` = url + status or error. No `OPENCODE_URL` → `{ ok:false, detail:"OPENCODE_URL not set" }` without a call. - **auth**: `resolveAccount(secret)` → `"local" | null` (constant seam; `null` = unauthorized). ## Env (`index.ts`) `SANDBOX_HOST`(`0.0.0.0`), `SANDBOX_PORT`(`4444`), `SANDBOX_PASSWORD`(unset⇒ auth off), `SANDBOX_REQUEST_TIMEOUT_MS`(`600000`), `OPENCODE_URL`(`http://127.0.0.1:4242`), `OPENCODE_SERVER_PASSWORD`, `OPENCODE_USERNAME`(`opencode`). ## package.json - deps: `fastify`, `@fastify/websocket`, `@opencode-ai/sdk` (move from devDeps). - devDeps: copy mcp-bridge (`typescript`, `tsx`, `@eslint/js`, `typescript-eslint`, `@types/node`, `globals`, `eslint`, `@types/ws`). - scripts: copy mcp-bridge **plus `"lint":"npm run eslint"`**. ## TS unit tests (`test/`) Protocol parse/validate (4 events, hello, `ok`/`error`, empty-payload→`{}`), registry (duplicate newest-wins per account, messageId correlation, timeout, pending cleanup on close), `resolveAccount`, opencode probe (fake fetch/server: ok / 401 / timeout). Use `FakeSocket` + fake fetch. ## Verification gate `npm test` + `npm run lint`/`check` clean in `tools/trap-sandbox`; service starts and listens on 4444.