Skip to content

feat: direct browser↔gateway xterm via short-lived HMAC tickets#862

Merged
harshithmullapudi merged 1 commit into
mainfrom
feat/xterm-direct-ticket
May 18, 2026
Merged

feat: direct browser↔gateway xterm via short-lived HMAC tickets#862
harshithmullapudi merged 1 commit into
mainfrom
feat/xterm-direct-ticket

Conversation

@harshithmullapudi

Copy link
Copy Markdown
Member

Summary

  • Browser opens the xterm WebSocket directly to the gateway when supported, skipping the webapp WS proxy hop and reducing latency.
  • Webapp signs a 5-minute HMAC ticket scoped to one PTY (key = sha256(rawSecurityKey) = the gateway's existing securityKeyHash); gateway verifies on the WS upgrade. No new secret introduced, no new on-disk state.
  • Older gateways (no directXterm capability in manifest) and http-only baseUrls transparently fall back to the existing proxy path. No coordinated rollout needed.

How it works

Browser flow:
  browser → webapp /xterm-ticket   (cookie auth)
         ← { wsUrl }                # direct: wss://gw/…?ticket=…
                                    # proxy:  /api/v1/.../xterm
  browser → wsUrl                   # direct: gateway verifies ticket
                                    # proxy:  webapp proxies as before

CLI / programmatic flow:
  client → gateway WS               # Bearer auth, unchanged

Two ticket-issuing endpoints, one per existing proxy path:

Endpoint Caller has Body
POST /api/v1/gateways/:gatewayId/xterm-ticket gatewayId + a session_id (gateway PTY id) { session_id }
POST /api/v1/coding-sessions/:codingSessionId/xterm-ticket only a CORE DB row id {} (webapp resolves)

Both return either { mode: "direct", wsUrl, expiresAt } or { mode: "proxy", wsUrl }.

Why this design

  • No replay-prevention (no jti dedup map). Matches what code-server / ttyd / Cloud Shell do: a replayed ticket only re-attaches to a PTY the attacker would already need the session_id for. Key rotation is the escape hatch.
  • Gateway never gets a URL-issuing endpoint — only the webapp has the raw key needed to sign. Keeps the "hash-only on disk" property of the gateway.
  • Ticket-auth is only accepted on /api/coding/coding_xterm_session; every other gateway route still requires Authorization: Bearer.

Files

Gateway (packages/)

  • gateway-protocol/src/manifest.ts — adds capabilities.directXterm?: boolean
  • cli/src/server/api/auth.tsverifyTicket() + ticket-eligible path whitelist in the auth hook
  • cli/src/server/api/manifest-builder.ts — advertises directXterm when coding slot is on

Webapp (apps/webapp/)

  • app/routes/api.v1.gateways.$gatewayId.xterm-ticket.tsx — gateway-direct ticket
  • app/routes/api.v1.coding-sessions.$codingSessionId.xterm-ticket.tsx — CodingSession-bound ticket
  • app/lib/xterm-ws.client.ts — browser helper that calls the ticket endpoint and resolves relative proxy URLs to wss://${location.host}…
  • app/components/coding/gateway-terminal.tsx, app/components/gateway/xterm-pane.tsx, app/routes/home.gateways.$gatewayId.terminal.tsx — wire UI to the new helper

Test plan

  • New webapp + new gateway over HTTPS → terminal opens with direct WS (DevTools network: WS connects to gateway host, not webapp host); xterm input/output round-trips correctly.
  • New webapp + new gateway over plain HTTP → falls back to proxy (no mixed-content errors); terminal still works.
  • New webapp + old gateway (no directXterm capability in manifest) → falls back to proxy path; terminal still works.
  • CLI / programmatic clients hitting /api/coding/coding_xterm_session with Authorization: Bearer … → still authenticated as before.
  • Per-gateway Terminal tab (/home/gateways/:id/terminal) — open, type, exit; "Start new shell" reopens cleanly.
  • Per-task coding session (/home/tasks/:id/coding/:sessionId) — open, resume session, new session.
  • Ticket TTL — leave the page idle for >5 min then reopen; new ticket is fetched on remount.
  • Invalid ticket (e.g. tamper with ?ticket= query) → gateway returns 401 on WS upgrade.

🤖 Generated with Claude Code

Browser now opens the xterm WebSocket straight to the gateway when
supported, skipping the webapp WS proxy hop. Webapp signs a 5-minute
ticket scoped to one PTY (HMAC over the gateway's existing
securityKeyHash); gateway verifies it on the upgrade. Older gateways
and http-only baseUrls transparently fall back to the existing proxy
path via the new xterm-ticket endpoints, so no coordinated rollout
is needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@harshithmullapudi
harshithmullapudi merged commit d86cfac into main May 18, 2026
1 check 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