feat(realtime): remote-control consent signaling + verified-email gate (LC-183 server) #227

Merged
nrupard merged 1 commit from feat/lc-183-remote-control-handshake into main 2026-05-26 21:57:28 +02:00
Owner

Server half of the LC-181 remote-control handshake (subtask LC-183). Adds the consent signaling + the security gate; a forged client frame cannot bypass it because the gate runs on the relay.

Changes

  • New RemoteControlSignal ChatEvent (kind = request/grant/deny/revoke, no input payload - the input stream is a separate WebRTC data channel, LC-184) + the remote_control_signal ClientFrame. Dedicated variant rather than overloading CallSignal (per LC-182: CallSignal's relay special-cases glare/voice).
  • relay_control_signal mirrors relay_call_signal's DM-room + membership + peer resolution, then enforces the gate before forwarding to the peer only.
  • Gate (remote_control_allowed, fail-closed): both peers email-verified AND neither blocked. Verification is build-defined per the LC-182 decision:
    • standalone: users.email_verified_at IS NOT NULL (the #[cfg(standalone)] verification flow sets it).
    • saas: an authenticated account is verified - saas never populates that column, so a column check would deny everyone. Chose path (a); one cfg'd helper (remote_control_email_verified) to flip if saas later grows its own check.
  • WS send task renders the signal to the recipient as the #lc-control-bus OOB fragment (mirrors #lc-call-bus); render_event None-arm updated for exhaustiveness.

Tests

Gate unit tests (routes::ws::tests): two verified+unblocked allowed; blocked denied either direction (both builds); unverified denied either direction (standalone-only, since saas verification is implicit). just test + just test-saas both green (106 each); clippy/fmt clean both modes.

Out of scope (next slice)

The client consent UI - the "Request control" affordance, the grant/deny prompt, and the #lc-control-bus consumer - is browser-iterated and pairs with the LC-184 data channel. This PR is the security-critical, fully-tested server foundation.

Server half of the LC-181 remote-control handshake (subtask LC-183). Adds the consent signaling + the security gate; a forged client frame cannot bypass it because the gate runs on the relay. ## Changes - New `RemoteControlSignal` `ChatEvent` (kind = request/grant/deny/revoke, **no input payload** - the input stream is a separate WebRTC data channel, LC-184) + the `remote_control_signal` `ClientFrame`. Dedicated variant rather than overloading `CallSignal` (per LC-182: CallSignal's relay special-cases glare/voice). - `relay_control_signal` mirrors `relay_call_signal`'s DM-room + membership + peer resolution, then enforces the gate before forwarding to the peer only. - **Gate** (`remote_control_allowed`, fail-closed): both peers email-verified AND neither blocked. Verification is build-defined per the LC-182 decision: - **standalone**: `users.email_verified_at IS NOT NULL` (the `#[cfg(standalone)]` verification flow sets it). - **saas**: an authenticated account is verified - saas never populates that column, so a column check would deny everyone. Chose path (a); one cfg'd helper (`remote_control_email_verified`) to flip if saas later grows its own check. - WS send task renders the signal to the recipient as the `#lc-control-bus` OOB fragment (mirrors `#lc-call-bus`); `render_event` None-arm updated for exhaustiveness. ## Tests Gate unit tests (`routes::ws::tests`): two verified+unblocked allowed; blocked denied either direction (both builds); unverified denied either direction (standalone-only, since saas verification is implicit). `just test` + `just test-saas` both green (106 each); clippy/fmt clean both modes. ## Out of scope (next slice) The client consent UI - the "Request control" affordance, the grant/deny prompt, and the `#lc-control-bus` consumer - is browser-iterated and pairs with the LC-184 data channel. This PR is the security-critical, fully-tested server foundation.
feat(realtime): remote-control consent signaling + verified-email gate (LC-183, server)
All checks were successful
check-secrets / Nosey parker (push) Successful in 3s
check-secrets / TruffleHog (push) Successful in 4s
check-secrets / Kingfisher (push) Successful in 5s
check-secrets / TruffleHog (pull_request) Successful in 6s
check-secrets / Nosey parker (pull_request) Successful in 7s
check-secrets / Kingfisher (pull_request) Successful in 10s
Check / clippy + fmt + tests (pull_request) Successful in 3m9s
Create release / Create release from merged PR (pull_request) Has been skipped
847152cc57
Server half of the LC-181 remote-control handshake. Adds the request/grant/deny/revoke signaling between the two members of a DM call, gated server-side so a forged client frame can't bypass it.

- New RemoteControlSignal ChatEvent (kind = request/grant/deny/revoke; no input payload - the input stream is a separate data channel, LC-184) and the `remote_control_signal` ClientFrame. Dedicated variant rather than overloading CallSignal, per the LC-182 design (CallSignal's relay special-cases glare/voice).
- relay_control_signal mirrors relay_call_signal's DM-room + membership + peer resolution, then enforces the gate before forwarding to the peer.
- Gate (remote_control_allowed, fail-closed): both peers email-verified AND neither blocked. Verification is build-defined per the LC-182 decision: standalone reads users.email_verified_at (the #[cfg(standalone)] verification flow sets it); saas treats an authenticated account as verified (saas never populates that column, so a column check would deny everyone - chose path (a)). One cfg'd helper to flip if saas later grows its own check.
- WS send task renders RemoteControlSignal to the recipient as the #lc-control-bus OOB fragment (mirrors #lc-call-bus); render_event None-arm updated for exhaustiveness.
- Unit tests on the gate: two verified+unblocked allowed; blocked denied either way (both builds); unverified denied either way (standalone-only, since saas verification is implicit).

Out of scope (next slice): the client consent UI - the "Request control" affordance, the grant/deny prompt, and the #lc-control-bus consumer - which is browser-iterated and pairs with the LC-184 data channel. This server half is the security-critical, fully-tested foundation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
nrupard deleted branch feat/lc-183-remote-control-handshake 2026-05-26 21:57:29 +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!227
No description provided.