Skip to content

Secure shared-session access and harden P2P UX - #5

Merged
icanvardar merged 7 commits into
mainfrom
dev
Jul 27, 2026
Merged

Secure shared-session access and harden P2P UX#5
icanvardar merged 7 commits into
mainfrom
dev

Conversation

@icanvardar

@icanvardar icanvardar commented Jul 24, 2026

Copy link
Copy Markdown
Member

Summary

  • Gate non-owner relay access with a per-session share code, rate-limit guessing, revoke access on unshare, protect local loopback WebSockets with a 256-bit token, and refresh Convex JWTs for long-running hosts.
  • Enable direct WebRTC by default with relay fallback, queue early ICE candidates, recover transient disconnects, switch promptly back to relay when P2P fails, and keep a live P2P session running if signaling disconnects.
  • Add a non-disruptive terminal-title HUD for host/viewer role, session and active transport; show context controls when Ctrl+\\ is armed without reserving a row that would break full-screen apps.
  • Document security, HUD and transport behavior, add detailed image TODOs throughout Mintlify, and fix dark-theme contrast.

Security model

  • A session is owner-only until shared.
  • A non-owner needs both the session id and share code; the backend stores only the code hash.
  • Relay tickets are short-lived, single-use, session-scoped and rechecked on consumption.
  • Local attach requires a per-session token readable only from the owner's protected registry.
  • P2P signaling remains behind authenticated relay tickets and authoritative peer ids.

Test plan

  • bun run format:check
  • bun run check-types
  • bun run lint
  • bunx turbo run test (76 tests, including real PTY e2e outside sandbox)
  • mint validate
  • mint broken-links
  • mint a11y
  • Dev smoke: share, join as owner without code, join as another user with code, reject missing/wrong code, then unshare and verify re-attach is denied.
  • Dev transport: confirm title changes relay to p2p, then force WRAPPER_P2P=0 and confirm relay-only mode.

Note

High Risk
Changes authentication, authorization, and default transport behavior for shared sessions; incorrect gating or P2P fallback could block joins or expose loopback attach without the token.

Overview
This PR tightens who can join a shared session and improves how relay sessions feel in the terminal.

Access control: Sharing now generates a human-readable share code; the backend stores only a hash via session:setShareCode. Owners still join relay sessions without a code; everyone else must use wrapper attach --relay --id … --code …. Non-owner ticket requests are rate limited, and unshare clears the hash to revoke access. Local attach requires a per-session ?token= on the loopback WebSocket (token lives in the owner’s registry). Long-running hosts auto-refresh Convex JWTs so heartbeats and share calls do not die after token expiry.

Transport: WebRTC P2P is on by default (WRAPPER_P2P=0 opts out). Negotiation queues early ICE candidates, tolerates transient disconnects, falls back to relay when P2P fails, and can keep a session on P2P if signaling drops while the data channel stays up.

UX: A terminal-title HUD shows role, short session id, and transport (local / relay / p2p / …), with armed Ctrl+\ controls—no reserved screen row. Docs and Mintlify pages are updated accordingly (plus image TODOs).

Reviewed by Cursor Bugbot for commit f4dab33. Bugbot is set up for automated code reviews on this repo. Configure here.

Sharing now generates a secret code (only its hash is stored). The owner can
join their own session from any device without a code, but any other viewer must
present the matching code to get a viewer ticket, and non-owner attempts are rate
limited per user and session to stop guessing. Unsharing clears the code and
revokes outstanding access. The host prints the code and join command on share;
`wrapper attach` gains a `--code` option.
P2P now defaults on so relay sessions get a direct low-latency data channel
automatically, with the relay as the fallback. Set WRAPPER_P2P=0 (or false/off)
to force the relay path. Documented the peer-IP exposure tradeoff in the env
example.
… P2P default

Explain the capability-based access model (owner plus share code), the shared
behaviors (shared control, multi-viewer, consensus resize, single host), the
--code join flow, and the on-by-default P2P transport across the READMEs and the
docs site. No em dashes.
@vercel

vercel Bot commented Jul 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
wrapper-web Ready Ready Preview, Comment Jul 27, 2026 11:26am

await backend.client
.mutation(sessionHeartbeatRef, { sessionId, shared: false, port: server.port })
.catch(() => {});
await backend.client.mutation(setShareCodeRef, { sessionId }).catch(() => {});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Relay failure leaves orphan share hash

Medium Severity

When setShareCode succeeds but relay setup then fails (for reasons other than a Pro plan requirement), the backend's share code isn't cleared. This leaves the session in an inconsistent state: the host sees a local-only share and doesn't receive the code, but the backend believes it's shared. Recovery requires a manual unshare/share cycle.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit afb203c. Configure here.

} else {
for (const line of lines) log.info(line);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heartbeat clears shared after setShareCode

High Severity

During relay share, setShareCode marks the session shared in Convex while the host’s local shared flag stays false until commitShared() runs. The periodic heartbeat still sends shared: false, which overwrites backend shared but leaves shareCodeHash set, so non-owners with a valid --code are rejected until the next heartbeat restores shared: true.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit afb203c. Configure here.

A shell-host minted one Convex JWT at startup and reused it forever, so sessions
that outlived the token started failing every heartbeat, share, and close call
with an expired-token error. The host now re-mints the JWT from the stored
session token before it expires (and reactively on an auth-expiry error), and
stops the refresh on shutdown.
The loopback WS server upgraded any connection with no auth, so on a shared
machine another local user could attach to a shell by reaching 127.0.0.1. The
host now generates a 256-bit token, stores it only in the 0600 registry, and
requires it as ?token= on every local connection. attach reads it from the
registry, and it is redacted from logs. The token is optional so headless tests
are unaffected.
Keep role, session, and active transport visible in the terminal title without
reserving a row that would break full-screen apps. Reveal context controls when
the prefix is armed, repaint after shell title changes, and make P2P failures
switch back to the relay promptly while preserving a live direct channel if the
signaling socket closes.
Document the discoverable controls, transport state and hardened fallback path,
add detailed non-rendered image TODOs where visual assets will help, and fix the
docs dark-theme contrast so the accessibility check passes.
@icanvardar icanvardar changed the title Share-code session access, default P2P, and docs Secure shared-session access and harden P2P UX Jul 27, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f4dab33. Configure here.

.catch(() => {});
// Clears both `shared` and the stored code hash, revoking any
// outstanding viewer access immediately.
void backend.client.mutation(setShareCodeRef, { sessionId }).catch(() => {});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unshare mutation not awaited

Medium Severity

Unshare clears local shared and stops the relay bridge but fires setShareCodeRef with void and swallows errors. Backend sharing and the code hash can stay active until a later heartbeat or not at all if the mutation fails.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f4dab33. Configure here.

@icanvardar
icanvardar merged commit befee7a into main Jul 27, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant