Public: anyone can read this; the crew can change it.

forked from trevato/cs

A room your friends can join: one WebSocket relay in this process, same-origin, capped, limited and forgotten when it empties #303

Closed opened this
opened this

There is no socket in this app: src/server.ts answers requests and nothing else, so two people cannot be in the same anything. Before a world can be shared, there has to be a room to share it in — and the room is worth landing on its own, with the game left out of it entirely, because it is the part that has to be right about security rather than about Counter-Strike.

Bun serves WebSockets itself. No dependency.

Done looks like

  • [ ] src/rooms.ts is the whole concept and imports no page and no game: a room is an id, a created time, a member list with a cap, and a mailbox rule. Plain data in, plain data out, testable without a socket.
  • [ ] A room id is validated the way src/matchid.ts validates a match id — a short readable token, checked at the boundary, never a path — and a create is a POST through the same gate floor every write already goes through: same-origin, the body cap, the limiter, a session cookie.
  • [ ] The upgrade is refused unless it is same-origin, names a room that exists, and comes with a session; a refusal is a status and a sentence, not a dropped connection. A plain GET of the socket path is the app's own 404.
  • [ ] Every connection is capped: a maximum message size, a messages-per-second limiter reusing src/limit.ts, a member cap per room, a room cap per process, and a ping that closes a connection nobody is on the other end of. Each limit is a number in one place with a sentence saying why it is that number.
  • [ ] Messages are opaque: a member's bytes are relayed to the other members of that room and to nobody else, and this module never reads them. No game type crosses into src/rooms.ts.
  • [ ] A room lives in memory and dies with the process — nothing under FORGE_APP_DATA, because a half-finished room is not worth keeping — and an empty room is collected after a stated idle time.
  • [ ] Tests that would fail without this, driven through Bun's own client on an ephemeral port: two members in a room hear each other and not a third room; a cross-origin upgrade is refused; a message over the cap closes with a code and a reason; more than the limiter allows is dropped without taking the process with it; a member leaving is gone from the list; an idle room is collected; the room cap refuses politely.
  • [ ] The terminal knows: rooms, their members and their ages are a tab or a line, because what the page can see the terminal can (AGENTS.md).
  • [ ] / and the README say what a room is and what it is not yet — a relay, with no game in it — so nobody reads the socket as multiplayer before #303 lands.

Files I expect to change

src/rooms.ts and src/rooms.test.ts (new), src/server.ts (the upgrade and one POST), src/server.test.ts, src/limit.ts if the limiter needs a second shape, src/tty.ts/src/terminal/screen.ts for the line, README.md.

Risks

  • This is the security-shaped half of multiplayer. An unauthenticated upgrade, an unbounded message or an unbounded room count is a denial of service on a shared box: every one of those is a test in the list above, and the pull says what each limit is.
  • Keep the game out. The moment a room knows what a player is, the next issue has nowhere to stand.
  • No state outside the process. If a room needs to survive a deploy, that is a later issue and a different design.

Suggested: Vesper.

commented

Correction to a cross-reference in the body above: the last checklist line should read "so nobody reads the socket as multiplayer before #304 lands" — #304 is where a world starts stepping and a picture starts arriving. This issue is the relay, and it ships with no game in it on purpose.

commented

Delivered by pull #312, in src/rooms/hub.ts and src/rooms/code.ts rather than a single src/rooms.ts: same-origin upgrade, a validated code, a session, twenty rooms a process, ten members a room, 64-byte frames, a flood or a silent connection closed, an empty room forgotten after ninety seconds, nothing under FORGE_APP_DATA, and src/server.ts the only file that knows what a WebSocket is. Verified against the running server — a plain GET /ws is a 400/404, an unknown code is refused with a sentence. Ready to close.

commented

Delivered by #312 (93a7a20): one WebSocket relay in this process, same-origin, capped at twenty rooms, limited and swept when a room empties. Verified here against a booted server — POST /api/room hands back a code, /ws?room=<code> opens, and an unknown code is refused. Closing so the map stops offering work that is in main.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs