feat(realtime): typed WS topics (enclave/user/admin) + subscribe-time auth (LC-160) #211

Merged
nrupard merged 1 commit from feat/lc-160-typed-ws-topics into main 2026-05-26 16:08:59 +02:00
Owner

What

LC-160 (follows the LC-156 declarative-subscribe foundation): add a typed WebSocket topic channel so non-room surfaces can receive live updates, with subscribe-time authorization.

How

The hub only keyed subscriptions by room_id. Rather than rewrite every broadcast_to_room call site, this adds a parallel typed-topic channel, leaving room fan-out untouched (current behavior unchanged, risk contained):

  • Hub: new topics: DashMap<String, HashSet<ConnId>> alongside rooms, with subscribe_topic / broadcast_to_topic and disconnect cleanup mirroring the room path.
  • WS: new subscribe_topic client frame. Each subscription is authorized per kind before joining the set - admin -> admin role, enclave:{id} -> membership (new db::enclave::is_enclave_member), user:{id} -> must be the caller's own id; unknown/malformed denied. Centralizes subscription auth (the audit flagged some surfaces lacking it).
  • live.js: data-lc-live-topic="enclave:5" | "user:abc" | "admin" sends the frame on socket open and on content swap, alongside data-lc-live-room.

Scope

broadcast_to_topic has no consumers yet - LC-161 wires the stale surfaces (enclave / invitations / admin / settings / saved / inbox / activity) onto these topics. This PR is the transport + auth layer.

Test

is_enclave_member (member / non-member / added-member) - the load-bearing enclave-topic auth check. just check / just test / just test-saas green (the routes_uploads flake is pre-existing and nondeterministic; no upload or hub-room code changed). WS-frame and hub-topic fan-out have no existing unit harness in this repo (consistent with the untested room-subscribe frame); the auth predicate is covered at the db level, matching how room-subscribe auth (is_room_member) is tested.

Part of the LC-156 epic / LC-159 post-audit story.

🤖 Generated with Claude Code

## What LC-160 (follows the LC-156 declarative-subscribe foundation): add a typed WebSocket topic channel so non-room surfaces can receive live updates, with subscribe-time authorization. ## How The hub only keyed subscriptions by `room_id`. Rather than rewrite every `broadcast_to_room` call site, this adds a **parallel** typed-topic channel, leaving room fan-out untouched (current behavior unchanged, risk contained): - **Hub**: new `topics: DashMap<String, HashSet<ConnId>>` alongside `rooms`, with `subscribe_topic` / `broadcast_to_topic` and disconnect cleanup mirroring the room path. - **WS**: new `subscribe_topic` client frame. Each subscription is authorized per kind before joining the set - `admin` -> admin role, `enclave:{id}` -> membership (new `db::enclave::is_enclave_member`), `user:{id}` -> must be the caller's own id; unknown/malformed denied. Centralizes subscription auth (the audit flagged some surfaces lacking it). - **live.js**: `data-lc-live-topic="enclave:5" | "user:abc" | "admin"` sends the frame on socket open and on content swap, alongside `data-lc-live-room`. ## Scope `broadcast_to_topic` has no consumers yet - LC-161 wires the stale surfaces (enclave / invitations / admin / settings / saved / inbox / activity) onto these topics. This PR is the transport + auth layer. ## Test `is_enclave_member` (member / non-member / added-member) - the load-bearing enclave-topic auth check. `just check` / `just test` / `just test-saas` green (the `routes_uploads` flake is pre-existing and nondeterministic; no upload or hub-room code changed). WS-frame and hub-topic fan-out have no existing unit harness in this repo (consistent with the untested room-subscribe frame); the auth predicate is covered at the db level, matching how room-subscribe auth (`is_room_member`) is tested. Part of the LC-156 epic / LC-159 post-audit story. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(realtime): typed WS topics (enclave/user/admin) + subscribe-time auth (LC-160)
All checks were successful
check-secrets / Kingfisher (push) Successful in 4s
check-secrets / Nosey parker (push) Successful in 4s
check-secrets / TruffleHog (push) Successful in 4s
check-secrets / TruffleHog (pull_request) Successful in 6s
check-secrets / Nosey parker (pull_request) Successful in 7s
check-secrets / Kingfisher (pull_request) Successful in 9s
Check / clippy + fmt + tests (pull_request) Successful in 3m6s
Create release / Create release from merged PR (pull_request) Has been skipped
beb9728755
Builds on the LC-156 declarative subscribe foundation. The hub keyed subscriptions only by room_id, so non-room surfaces had no live channel. Adds a parallel typed-topic channel without touching the existing room fan-out (so current behavior is unchanged and risk is contained):

- Hub: a new `topics: DashMap<String, HashSet<ConnId>>` alongside `rooms`, with subscribe_topic / broadcast_to_topic and disconnect cleanup mirroring the room path.
- WS: a new `subscribe_topic` client frame. Each subscription is authorized per kind before the connection joins the topic's set - `admin` requires the admin role, `enclave:{id}` requires membership (new db::enclave::is_enclave_member), `user:{id}` must be the caller's own id; unknown/malformed topics are denied. Centralizes subscription authorization, which the audit flagged as missing on some surfaces.
- live.js: `data-lc-live-topic="enclave:5" | "user:abc" | "admin"` sends the subscribe_topic frame on socket open and on content swap, alongside the existing data-lc-live-room.

broadcast_to_topic has no consumers yet; LC-161 wires the stale surfaces (enclave/invitations/admin/settings/saved/inbox/activity) onto these topics. Test: is_enclave_member (member/non-member/added-member), the load-bearing enclave-topic auth check.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
nrupard deleted branch feat/lc-160-typed-ws-topics 2026-05-26 16:08:59 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!211
No description provided.