fix(desktop): re-mint terminal JWT on every reconnect

SA
4mo ago
+66-17

Problem

Remote-workspace terminals fail to attach while everything else (files, PRs, git, ports) works. Relay logs show /hosts/<org>:<machine>/terminal/<id>?token=… returning HTTP 401 in ~0–1 ms at the relay edge, at a ~71% rate across multiple users' hosts (128× 401 vs 10× 200 on a live sample). The same host gets both 200 and 401 → a per-token rejection, not a per-host/version issue.

Root cause

The terminal WebSocket URL has a 1-hour better-auth JWT baked into it at React render time (useWorkspaceWsUrl → getWsToken()), and the desktop transport stores that full URL in transport.currentUrl. Every reconnect path — reconnectNow, scheduleReconnect, handleResume — redials transport.currentUrl verbatim, never re-minting the token. Once the JWT passes its exp, the relay (validating via better-auth JWKS) rejects the attach with 401. tRPC/filesystem calls are unaffected because they mint a fresh token per request.

The module-level token refresh (setJwt every 50 min) updates a plain variable that doesn't trigger a re-render, so the baked URL never recomputes.

Trigger: the sleep/wake + focus/visibility auto-reconnect machinery added in #5135 (and hardened in #5137) redials aggressively using the token-baked URL, converting a previously-latent staleness bug into a fleet-wide 401 storm. Surfacing via the recent canary / 1.12.6 builds. The relay validator and JWT signing config are unchanged — this is not a server/secret problem.

Fix

Stop persisting the token in currentUrl; mint it fresh per dial:

  • terminal-ws-transport.ts: store a token-less base URL (stripUrlToken) and append a freshly-minted token at each dial — both the WS open and the fly-affinity pre-flight — via a new optional getToken provider on connect().
  • terminal-runtime-registry.ts: thread getToken through connect()/reconnect(); compare against the stripped URL so a token refresh alone doesn't tear down a live shell.
  • TerminalPane.tsx: pass getWsToken (from useWorkspaceClient()) as the provider.

This mirrors what web's TerminalConnection.buildUrl() and desktop's tRPC headers already do (fresh token per request).

Verification

  • bun test terminal-ws-transport.test.ts → 7 pass
  • biome check → clean
  • Typecheck: no errors from the changed files (a pre-existing duplicate-@codemirror dedup error in CodeEditor.tsx is unrelated and reproduces on main without this change).

Immediate mitigation (no release needed)

Affected users can fully quit & reopen the desktop app (or reopen the terminal) to force a fresh token.


Open in Stage

Summary by cubic

Re-mint the terminal JWT on every reconnect in the desktop app to stop 401s after token expiry. Store a token‑less base URL and add a fresh token for each dial (including pre-flight).

  • Bug Fixes
    • Store a token-less currentUrl; append a fresh token per (re)dial in terminal-ws-transport (WS open and relay-affinity pre-flight).
    • Add optional getToken to connect(); thread through terminal-runtime-registry so reconnects use new tokens.
    • Compare against stripped URLs to avoid tearing down a live shell when only the token changes.
    • Pass getWsToken from useWorkspaceClient() in TerminalPane (@superset/workspace-client).

Summary by CodeRabbit

  • New Features

    • Terminal connections now refresh authentication tokens automatically during reconnects.
    • Reconnects are smarter and can reuse existing sessions when only the token changes.
  • Bug Fixes

    • Reduced unnecessary terminal disconnects caused by token updates.
    • Improved WebSocket reconnection reliability for terminal sessions.