Skip to content

TUI Supabase auth flow allows overlap and can hang indefinitely #35

Description

@jumski

Summary

src/tui/dialog.tsx launches the Supabase OAuth flow as fire-and-forget work with no in-flight guard, no cancellation, and no timeout around the authorize() / callback() network calls.

This allows:

  • overlapping auth attempts in a single session
  • background auth work to continue after the dialog is dismissed
  • retries that start a second flow while the first is still running
  • dialogs that can hang forever if the callback path never resolves

Evidence

Fire-and-forget auth start

src/tui/dialog.tsx

  • 178-186: startOAuth = () => runAuthFlow(...)
  • 195: idle state onConfirm: startOAuth

No in-flight state is tracked outside the dialog UI state itself.

Dismiss only closes UI, not the network/auth flow

src/tui/dialog.tsx

  • 139-145: closeDialog() sets lifecycle.closed = true and calls props.onClose()
  • 147-175: setState() stops UI updates after lifecycle.closed

This prevents stale UI changes, but it does not cancel the underlying runAuthFlow() promise or the host OAuth calls it is awaiting.

Retry can overlap a still-running attempt

src/tui/dialog.tsx

  • 224-234: error dialog onConfirm does await startOAuth() again

If a previous attempt is still running in the background, retry starts another flow.

No timeout around host OAuth calls

src/tui/dialog.tsx

  • 65-68: waits on provider.oauth.authorize(...)
  • 97-100: waits on provider.oauth.callback(...)

Neither call has a timeout or abort path.

Reproduction

Overlap case

  1. Run /supabase.
  2. Confirm the dialog and let it reach "Waiting for authorization...".
  3. Dismiss the dialog before completing the browser callback.
  4. Re-open /supabase and start auth again.
  5. Complete one or both browser flows.

Expected

At most one Supabase auth flow should be active per dialog/session/store, and retry should either cancel or await the existing attempt.

Actual

Multiple auth flows can overlap. The first one can continue running after the dialog is gone.

Hang case

  1. Run /supabase.
  2. Start auth.
  3. Make authorize() or callback() stall indefinitely (e.g. broken server path, unreachable host endpoint, unresolved callback).

Expected

User gets a bounded timeout and a recoverable retry path.

Actual

The dialog can remain stuck waiting indefinitely.

Impact

  • Confusing UX with multiple browser tabs or competing flows.
  • Last-finishing auth flow wins, even if it is not the one the user intended.
  • Hanging auth requests have no bounded recovery path.

Suggested Fix

Recommended approach:

  1. Track a single in-flight auth promise for the dialog lifecycle.
  2. Block re-entry while one flow is active, or convert retry into "cancel then restart".
  3. Add timeout wrappers around authorize() and callback().
  4. If possible, thread cancellation/abort through the flow so dismiss actually cancels the work instead of only hiding the UI.

Acceptance Criteria

  • User cannot start a second Supabase auth flow while one is already active.
  • Dismiss/retry semantics are explicit and deterministic.
  • authorize() and callback() fail with a clear timeout instead of hanging forever.
  • Regression coverage exists for dismiss-then-retry overlap.

Notes

This is mostly a reliability/UX issue, but it can indirectly affect correctness because multiple flows can race to update the same local auth store.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpriority:p1High-priority next wavestatus:triagedReviewed and ranked

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions