Skip to content

fix(server): a revoked or expired session's preview stream closes - #32245

Open
Jacksondr5 wants to merge 1 commit into
pingdotgg:mainfrom
Jacksondr5:fix/preview-stream-closes-on-revoke
Open

Jacksondr5 wants to merge 1 commit into
pingdotgg:mainfrom
Jacksondr5:fix/preview-stream-closes-on-revoke

Conversation

@Jacksondr5

Copy link
Copy Markdown
Contributor

Problem

When a session is revoked in Settings › Sharing & access, or expires, its RPC socket closes at once (since #17370). The preview stream socket (/api/preview-stream/ws) checks the session only when it opens, and never again. T3's own client closes its preview socket when its RPC socket drops, so the normal UI hides this, but the server doesn't enforce it: a client that keeps its preview socket open after the revoke keeps receiving the server Browser tab's frames and, with the preview-operate scope, keeps sending input, until it disconnects. Reconnecting is refused, since new requests are checked.

Change

ServerBrowserStream now races the viewer against SessionStore.awaitInvalidation(session.sessionId, session.proofKeyThumbprint), the same pattern ws.ts uses. When the session is invalidated, the stream and input fibers are interrupted, the viewer's finalizers run, and the socket closes. A DPoP session renewed with the same key keeps its stream, as on the RPC socket.

Scope and approval

A very small, focused fix for an obvious bug: it applies #17370's own pattern to the one socket it missed. No contract or client change.

Related: open #24881 (CLI revoke goes through the server) is complementary. It makes t3 auth session revoke reach the running server's open sockets. This makes the preview stream one of the sockets that closes.

Verification

  • Added two cases to ServerBrowserStream.test.ts: an established viewer, read-only and with operate scope plus a running input, closes when its session is invalidated. They check that the viewer detaches, the running input is interrupted, the socket closes, and no frame arrives after invalidation.
  • On main at d5fc29f without the source change, both new cases fail: the socket never closes and the test times out. With the change, ServerBrowserStream.test.ts (13 tests) and SessionStore.test.ts pass.
  • apps/server typecheck, lint and format pass on the changed files.
  • Checked live on isolated dev servers (web), on main at d5fc29fe5c and on this branch, with a second paired session (standard grant) watching a server Browser tab. To see past the client's own cleanup, the second session also held a preview socket of its own open:
    • On main, after the revoke that socket stayed open for 105 s until stopped by hand, kept receiving frames (247 to 1,509), and still took control of the tab and typed text that appeared in the owner's view.
    • On this branch, it closed 18 ms after the revoke (code 1001), with no frames after, and input sent after the revoke did nothing.
    • In both, the app's own preview socket closed right after its RPC socket, about 0.4 s after the revoke.

Claude Opus 5.5, an AI agent, on Jackson's behalf.

🤖 Generated with Claude Code

The preview stream socket authenticated its session only when it opened,
so a revoked or expired device kept receiving frames and, with the
preview-operate scope, kept sending input until it disconnected. Race the
viewer against SessionStore.awaitInvalidation, as the RPC socket does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:unvouched Author is not yet trusted in the VOUCHED list. size:XS 0-9 changed lines (additions + deletions). labels Nov 7, 2026
@macroscopeapp

macroscopeapp Bot commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved at 7c6aff6

Macroscope's review found this PR approvable — Preview streams and their input handling now stop when their session is revoked or expires, using the existing session-invalidation mechanism. The change is limited to connection lifetime and preserves existing authorization and renewal rules.

You can add or adjust custom eligibility rules. Learn more.

@coderabbitai

coderabbitai Bot commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

Explain Change →

No actionable comments were generated in the recent review.

Walkthrough

The WebSocket handler now races its stream against invalidation of the authenticated session and proof-key thumbprint. When invalidation completes, the stream ends. Tests verify viewer detachment, socket closure, input interruption in operate scope, and no later frames.

Changes: WebSocket session invalidation

Layer / File(s) Summary
Session invalidation termination
apps/server/src/preview/ServerBrowserStream.ts, apps/server/src/preview/ServerBrowserStream.test.ts
The handler monitors authenticated-session invalidation and ends the stream when it occurs. Tests cover viewer detachment, socket closure, interruption of running input in operate scope, and no subsequent frames.

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant makeHandler
  participant SessionStore
  Browser->>makeHandler: Establish WebSocket
  makeHandler->>SessionStore: Await invalidation for session ID and proof-key thumbprint
  SessionStore-->>makeHandler: Invalidation completes
  makeHandler-->>Browser: End stream with an empty response
Loading

Suggested labels: bugfix

Suggested reviewers: juliusmarminge

Merge Risk: Minimal · up to 7c6af

Preview streams terminate when their session is invalidated. No material issue remains that needs resolution before merge.

Pre-merge checks: Passed 4

Check name Status Explanation
Title check Passed The title clearly states the main change: closing preview streams when sessions are revoked or expire.
Description check Passed The description covers the problem, change, scope, and verification. It also identifies the agent model and harness.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Finishing Touches

Generate unit tests (beta)

  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Recent review info

Run configuration

Commits

Reviewing files that changed from the base of the PR and between d5fc29f and 7c6aff6.

Files selected for processing: 2

  • apps/server/src/preview/ServerBrowserStream.test.ts
  • apps/server/src/preview/ServerBrowserStream.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

Wladefant

This comment was marked as spam.

@t3dotgg
t3dotgg dismissed Wladefant’s stale review November 8, 2026 23:30

Dismissed: unsolicited automated review from a blocked account.

@maria-rcks

maria-rcks commented Nov 15, 2026 •

Copy link
Copy Markdown
Collaborator

Note

Written by gpt-6.1-sol on behalf of Maria

Verified in a real dev environment

Verified unchanged head 7c6aff64765d51b3cb40d1ac996671b5c4d2f199 on a disposable Linux VM, after reproducing on github/main at 48a383ca6fea84bf31b71433886b783364857eb3.

Ran vp i and a real dev server on each checkout, sequentially. Used repository Playwright with /usr/bin/google-chrome. Created the project and thread through the web UI, and ran real Codex gpt-6.1-sol turns to create/start the page. No mocks, seeded database, or replay provider.

The page had a live clock and editable note. Paired separate operating and read-only browser sessions through the running server. Kept independent, cookie-authenticated preview sockets open so normal client cleanup could not hide the bug. Acknowledged the actual JPEG frames and sent the real preview input protocol.

  • Main: revocation returned {"revoked":true}, but the established operating socket stayed OPEN, received 23 more frames during the observation window, and typed AFTER REVOKE into the owner's live browser page. A new connection with the revoked session was refused.
  • PR: revocation closed the established operating socket with code 1001; post-revoke input could not be sent and reconnect was refused. The note stayed before revoke. The last frame arrived before the revoke HTTP response; the socket then closed.
  • The unrevoked read-only viewer continued receiving frames. Its input attempts did nothing. Revoking that read-only session also closed its established socket with code 1001.
  • The owner regained control and typed through the ordinary web canvas after revocation. Also clicked Revoke in Settings > Sharing & access for a normal paired web client and observed its preview disconnect.

Short runtime transcript:

main  revoke HTTP 200 -> socket OPEN -> +23 frames -> AFTER REVOKE input visible
PR    revoke HTTP 200 -> socket CLOSED (1001) -> input blocked -> reconnect refused
PR    other viewer keeps streaming; read-only input ignored; owner input works
PR    revoke read-only session -> socket CLOSED (1001)
blacksmith: ServerBrowserStream.test.ts 13/13; SessionStore.test.ts 22/22

No source changes or push needed. Existing CI is green on the reviewed head. The two focused suites were rerun through blacksmith and all 35 tests passed.

main: a revoked viewer still types AFTER REVOKE

pr: revocation releases control and prevents the revoked input

Not verified

Natural session expiry and same-key DPoP renewal were not exercised live. Native iOS/Android, Electron, macOS/Windows, relay/tunnel connections, and the separate CLI revocation path were not exercised. This is development-runtime verification, not an installed-release acceptance.

@juliusmarminge juliusmarminge added the takeover:in-progress A takeover lead is working on this PR label Nov 15, 2026
@juliusmarminge

Copy link
Copy Markdown
Member

The automated pipeline stopped on this pull request and will not merge it:

someone else pushed to fix/preview-stream-closes-on-revoke (head is now 7c6aff6); the pipeline does not build on commits it did not review

It stays open for a person to finish or close.

@juliusmarminge juliusmarminge added takeover:blocked Takeover blocked; reason in the PR body and removed takeover:in-progress A takeover lead is working on this PR labels Nov 15, 2026
@Jacksondr5
Jacksondr5 force-pushed the fix/preview-stream-closes-on-revoke branch from 8464f1e to 7c6aff6 Compare November 15, 2026 06:01

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XS 0-9 changed lines (additions + deletions). takeover:blocked Takeover blocked; reason in the PR body vouch:unvouched Author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants