Skip to content

fix(auth): client-side auth timeout + reconnect recovery#61

Merged
vxcozy merged 1 commit into
mainfrom
fix/in-tui-auth-polish
Apr 18, 2026
Merged

fix(auth): client-side auth timeout + reconnect recovery#61
vxcozy merged 1 commit into
mainfrom
fix/in-tui-auth-polish

Conversation

@vxcozy
Copy link
Copy Markdown
Owner

@vxcozy vxcozy commented Apr 18, 2026

Summary

  • PickerState::tick_auth_deadline trips the in-TUI Spotify auth flow after 5m30s when the daemon dies mid-flow and never emits AuthCompleted / AuthFailed. 30s longer than the daemon's own 5m ceiling so the daemon's specific AuthFailed { reason: "timeout" } wins the happy-path-with-delay race.
  • ReconnectingSession now fires a reconnect notice into a channel the render loop drains each tick. On reconnect the client clears any in-flight auth state with a benign "daemon reconnected — retry if needed" banner and re-issues Verb::ReadConfig so the Settings tab resyncs with the new daemon process.
  • Softer pending-state copy points SSH/headless users at clitunes auth until the upstream URL-capture is available.

Why

PR #60 landed in-TUI Spotify sign-in with two known gaps flagged during pride-gate review (CLI-93):

  1. Client had no deadline and no reconnect recovery. If the daemon crashed mid-flow the auth_in_progress flag survived forever; if the control socket reconnected into a fresh daemon, the client remained convinced a flow was still running.
  2. SSH/headless fallback was dead data. Event::AuthStarted { url } was always None because librespot-oauth 0.8 doesn't expose the authorize URL through its public API.

Gap 1 is this PR. Gap 2 turned out to be structurally impossible to shadow from outside the crate — OAuthClient::set_auth_url generates both the CSRF state and the PKCE code_challenge with private randomness, so any URL we'd rebuild ourselves would pair a different state + challenge with the pkce_verifier librespot's listener is waiting to exchange. The user's browser redirect would fail PKCE validation. The task's stop-and-report clause applied. Added a TODO(librespot-oauth) pointing at the upstream dev branch and routed SSH/headless users to the sibling clitunes auth CLI in the meantime, where the URL can be printed directly to the user's terminal.

Test plan

  • cargo fmt --check clean
  • cargo clippy --workspace --all-targets -- -D warnings clean
  • cargo test --workspace --all-features — full suite passes (509 + 4 new picker-state tests, 1 new paint test, existing auth-event tests untouched).
  • tick_auth_deadline_trips_after_timeout — deterministic time-driven unstick verified with AUTH_CLIENT_TIMEOUT + 1s.
  • daemon_reconnect_clears_pending_auth_with_benign_noteauth_in_progress flips off with the expected reason; auth_started_at resets.
  • Manual: built clitunes debug binary successfully; rebinding the render-loop config compiles through the main TUI and pane entrypoints.

PR #60 left the in-TUI Spotify auth flow vulnerable to two stuck-state
bugs: if the daemon crashed mid-flow the `auth_in_progress` flag
survived indefinitely, and a control-socket reconnect would leave the
client convinced a flow was still running against a daemon that had
no memory of it. Fix both:

- `PickerState::tick_auth_deadline` trips after 5m30s — longer than
  the daemon's own 5m ceiling so the daemon's `AuthFailed { "timeout" }`
  normally wins with its specific reason, and this is the last-resort
  unstick when the daemon is simply gone. Called every render tick.
- `ReconnectingSession` fires a reconnect notice via a sender the TUI
  installs on startup. The render loop drains the channel each tick,
  calls `handle_daemon_reconnected` (which clears any pending auth
  flag with a benign "daemon reconnected — retry if needed" banner),
  and re-issues `Verb::ReadConfig` so the Settings tab resyncs with
  the new daemon's actual auth state.

Fix 1 from the original scope — capturing the OAuth authorize URL so
the TUI can render it for SSH/headless users — turned out to be
structurally impossible from outside librespot-oauth. Both the CSRF
state and the PKCE challenge are generated inside
`OAuthClient::set_auth_url` with no public hook. Any URL we'd build
ourselves would pair a different state + challenge with the verifier
librespot's listener is waiting to exchange — the user's browser
redirect would then fail PKCE validation. The task's stop-and-report
clause applies. Added a TODO in auth.rs pointing at the upstream fix
(expose the URL through the public API) and softened the pending-row
message to point headless users at the sibling `clitunes auth` CLI,
which runs in their terminal and can print the URL directly.

Tests:
- tick_auth_deadline: noop when idle, noop before timeout, trips
  after timeout with a client-side reason, and doesn't double-fail.
- handle_daemon_reconnected: clears pending flag + surfaces benign
  note; noop when not in progress.
- set_auth_completed/failed clear the deadline clock.
- `a` press populates `auth_started_at`.
- paint: pending state renders the new "run clitunes auth" fallback.

Local-CI triple clean: `cargo fmt --check`, `cargo clippy --workspace
--all-targets -- -D warnings`, `cargo test --workspace --all-features`.

Closes clitunes-lsn / CLI-93
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@vxcozy vxcozy merged commit 2fb2c44 into main Apr 18, 2026
12 checks passed
@vxcozy vxcozy deleted the fix/in-tui-auth-polish branch April 18, 2026 19:36
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