Skip to content

Fix Settings sign-in by using the default browser#4974

Open
austinywang wants to merge 13 commits into
mainfrom
issue-3617-signin-fix
Open

Fix Settings sign-in by using the default browser#4974
austinywang wants to merge 13 commits into
mainfrom
issue-3617-signin-fix

Conversation

@austinywang
Copy link
Copy Markdown
Contributor

@austinywang austinywang commented May 29, 2026

Closes #3617.

Summary

This consolidates the durable pieces of the existing issue-3617 work and changes the Settings sign-in architecture from an embedded ASWebAuthenticationSession sheet to the system default browser plus cmux's existing native auth callback.

  • Settings > Account > Sign In now opens AuthEnvironment.signInURL() through NSWorkspace, so Arc/Chrome/Firefox/etc. are honored instead of always using Safari's AuthenticationServices web stack.
  • The native callback URL includes a per-attempt state; stale callbacks from an older or signed-out attempt are ignored instead of reauthenticating unexpectedly.
  • The app no longer registers itself as an http/https URL handler, matching the LaunchServices loop fix from Drop vestigial http/https URL handler so ASWebAuthenticationSession works against http://localhost #3650.
  • The after-sign-in handler now requires both a non-empty refresh token and a non-empty decoded access token before emitting a native handoff, matching the defensive web-side fix from Require valid accessToken for native app handoff to prevent auth crashes #3634.
  • The browser handoff page automatically attempts the cmux*://auth-callback return while keeping the manual Return to cmux link as a fallback.
  • Settings now surfaces localized sign-in errors instead of only writing auth failures to logs.

Why this architecture

I weighed the two options called out in the issue:

This PR chooses the system-browser architecture. It also keeps the LaunchServices and token-shape fixes because they are still valid defense-in-depth for callback reliability.

Related work credited

Paid-user reproduction folded in

A paid user reported Arc as their macOS default browser, but cmux Settings sign-in opened Safari and then auto-closed before sign-in. They completed sign-in only by capturing the auth URL from LaunchServices logs and pasting it into Arc manually. This PR makes that workaround the normal path: cmux opens the auth URL in the default browser directly, then completes via the native callback.

Regression coverage

The first commit adds a failing Swift Testing regression for the new invariant: AuthManager.beginSignIn() must hand the generated sign-in URL to the injected browser opener. That fails on the old embedded-session implementation and passes with this change.

This PR also adds Bun coverage for the web native-handoff guard: native schemes are recognized, callback targets are coerced to auth-callback, callback state is preserved, and empty access tokens do not emit native handoffs.

Verification

Local static checks only, per repo instruction not to run local tests or builds:

  • python3 scripts/normalize-pbxproj.py --check cmux.xcodeproj/project.pbxproj
  • python3 -m json.tool Resources/Localizable.xcstrings >/dev/null
  • git diff --check

I did not run ./scripts/reload.sh, xcodebuild, or local tests. I did not use cloud-mac because real sign-in completion needs live credentials; the fix is verified by code-path ownership and regression coverage rather than a credentialed recording.


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag @codesmith with what you need. Autofix is disabled.


Note

High Risk
Changes authentication UX and callback validation (token handoff, state matching, URL handler registration); mistakes could block sign-in or accept wrong callbacks.

Overview
Settings account sign-in no longer uses an embedded ASWebAuthenticationSession sheet. It opens the Stack sign-in URL in the system default browser (via NSWorkspace), then completes when the app receives a cmux*://auth-callback deep link.

Each attempt gets a unique state on the callback URL; AuthManager only accepts callbacks that match the active attempt, and ignores stale, sign-out-cancelled, or stateless callbacks. Sign-in can time out (default 5 minutes) with a new localized “Sign in timed out” message shown in Settings.

Plumbing: Info.plist drops http/https URL-handler registration so LaunchServices is less likely to route auth URLs back into cmux. The web after-sign-in handler only deep-links when both tokens are present, normalizes native return URLs to auth-callback, preserves state, and auto-redirects via OpenNativeClient.

Tests: Swift tests for external-browser sign-in and timeout/stale callbacks; Bun tests for native handoff helpers; existing sign-out race tests updated for state.

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


Summary by cubic

Settings sign-in now opens in the default browser and completes via cmux*://auth-callback. Each attempt uses a unique state, times out after 5 minutes with a localized error shown in Settings, ignores stale/stateless/cancelled callbacks, and scopes loading to the active attempt (fixes #3617).

  • Bug Fixes
    • Replace ASWebAuthenticationSession with system-browser opening via NSWorkspace of AuthEnvironment.signInURL(callbackURL:); per-attempt state via AuthEnvironment.authCallbackURL(state:); add a 5‑minute timeout and surface lastSignInError in Settings; loading is tied to the active attempt and clears on timeout/settle.
    • Validate callbacks strictly: require a matching active state; ignore stateless callbacks, stale callbacks (including after timeout), and callbacks from sign‑out–cancelled attempts.
    • Remove http/https URL handler from Info.plist and defensively avoid launching cmux as the browser to prevent LaunchServices loops.
    • Web after‑sign‑in only deep‑links when both tokens are present, coerces native return targets to .../auth-callback, preserves state, and OpenNativeClient auto‑redirects; tests cover the external‑browser opener, timeouts, stale/stateless/cancelled callbacks, and native handoff.

Written for commit d3a5769. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes

    • Sign-in now opens in the user’s default browser, matches callbacks to the active sign-in attempt, and times out with a user-facing "sign in timed out" message.
  • UI

    • Settings shows the last sign-in error beneath the account row when present.
  • Localization

    • Added localized "sign in timed out" translations for many locales.
  • New Features

    • Improved native-app handoff and callback handling for after-sign-in flows.
  • Tests

    • Added tests for external browser sign-in and native handoff logic.

Review Change Stack

@vercel
Copy link
Copy Markdown

vercel Bot commented May 29, 2026

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

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment May 29, 2026 7:00am
cmux-staging Building Building Preview, Comment May 29, 2026 7:00am

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 29, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Browser sign-in now uses per-attempt state, surfaces localized sign-in errors, adds a sign-in timeout, centralizes Settings button logic and error display, removes web URL scheme registration, extracts native handoff helpers, refactors after-sign-in logic, adds client-side native redirect, and introduces tests and project updates.

Changes

Auth error surface and state tracking

Layer / File(s) Summary
Callback state & callback URL construction
Sources/Auth/AuthCallbackRouter.swift, Sources/Auth/AuthEnvironment.swift
Adds optional state to callback payload, extracts/trim state from callback URLs, and builds callback URLs with optional state query parameter.
AuthManager: attempt state, errors, and callback handling
Sources/Auth/AuthManager.swift
Tracks per-attempt browser sign-in state, adds lastSignInError and signInTimedOut, refactors beginSignIn/callback handling to validate state, and adds diagnostics/timestamp helpers.
Settings UI and localization
Sources/cmuxApp.swift, Resources/Localizable.xcstrings, Resources/Info.plist, CHANGELOG.md
Displays localized last-sign-in error under the Sign In button, centralizes button disabled logic, adds settings.account.error.signInTimedOut translations, removes the web URL scheme from Info.plist, and documents the fix in CHANGELOG.
Tests and project registration
cmuxTests/*, cmux.xcodeproj/project.pbxproj
Adds browser sign-in initiation and timeout/staleness tests, in-memory test token store and client stub, and registers the new test in the Xcode project.

Native handoff module and web refactor

Layer / File(s) Summary
Native handoff utilities
web/app/handler/after-sign-in/native-handoff.ts
New helpers to detect native return schemes, convert return-to URLs into auth-callback deep links (preserving state), and gate emission on token presence.
After-sign-in handler refactor
web/app/handler/after-sign-in/page.tsx
Replaces inline native-scheme logic with helpers, changes buildNativeHref to return null on missing/invalid inputs, rebuilds access cookie payload, and emits native handoff only when helpers agree.
OpenNativeClient auto-redirect
web/app/handler/after-sign-in/OpenNativeClient.tsx
Adds useEffect redirect (window.location.assign) to perform client-side deep-link navigation when href changes.
Native handoff tests
web/tests/native-handoff.test.ts
Adds Bun tests covering scheme detection, callback conversion (state preserved), and token-presence gating for handoff emission.

Estimated code review effort:
🎯 4 (Complex) | ⏱️ ~45 minutes

Poem:

"A rabbit tapped the code with care,
Hopped a state into the air,
Callbacks matched and errors shown,
Native links now find their home,
🍃 hop, sign-in — all is fair."


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (2 errors, 1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Cmux Swift Blocking Runtime ❌ Error PR introduces Task.sleep in production code (Sources/Auth/AuthManager.swift) for timeouts, violating swift-blocking-runtime.md rule which treats Task.sleep as default failure. Replace Task.sleep with proper async timing abstractions: use DispatchSourceTimer, Timer, or real callback signals instead of sleep in scheduleBrowserSignInTimeout and waitForSignInSettled.
Cmux Architecture Rethink ❌ Error PR introduces Task.sleep timeout + multiple mutable flags (activeBrowserSignInAttemptID/State) papering over shared-state race between external browser and callback handler from different contexts. Use unified state machine actor for browser signin lifecycle instead of Task.sleep delays and scattered mutable ID/state guard flags.
Docstring Coverage ⚠️ Warning Docstring coverage is 8.62% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Cmux Swift @Concurrent ❓ Inconclusive No result was produced after verification. Marking as INCONCLUSIVE. Re-run the check or adjust instructions to produce a final result.
✅ Passed checks (14 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: Settings sign-in now uses the default browser instead of ASWebAuthenticationSession.
Linked Issues check ✅ Passed The PR successfully addresses all coding requirements from issue #3617: surfaces user-visible sign-in errors via lastSignInError and userFacingSignInErrorMessage, implements per-attempt state matching to ignore stale callbacks, removes http/https URL handler registration, opens sign-in in default browser, adds auto-redirect to native deep link, and includes regression test coverage.
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #3617 and its related issues (#3650, #3634, #3624, #1078): auth flow refactoring, error surfacing, state management, URL handler removal, web handoff logic, and regression tests. No unrelated changes detected.
Cmux Swift Actor Isolation ✅ Passed No Swift 6 actor isolation violations. @Published property correctly on @MainActor class; Sendable struct with proper types; service protocols properly scoped.
Cmux No Hacky Sleeps ✅ Passed PR introduces no sleep/timer/polling patterns in TypeScript/JavaScript production code; uses proper event-driven patterns (React useEffect, async/await, pure functions).
Cmux Algorithmic Complexity ✅ Passed All changes are O(1) state management or simple URL parsing with fixed-size components. No nested scans, per-target rescans, or unbounded collection rebuilds in production code.
Cmux Swift Concurrency ✅ Passed PR introduces no legacy async patterns. New @Published property in AuthManager is for SwiftUI boundary (allowed). Tasks properly stored, not fire-and-forget. Tests clean.
Cmux Swift File And Package Boundaries ✅ Passed PR satisfies Swift file-boundary rules: no oversized new files, AuthManager +150 lines (below 250 threshold), no mixed responsibilities, focused bug-fix refactor appropriate for app target.
Cmux Swift Logging ✅ Passed Auth-related Swift files contain no NSLog, print, debugPrint, or dump statements added in the diff, complying with swift-logging.md rules.
Cmux User-Facing Error Privacy ✅ Passed All user-facing error messages comply with privacy rule: new timeout message is generic, and no raw errors, tokens, credentials, or sensitive details leak to users.
Cmux Full Internationalization ✅ Passed PR complies with i18n rules: Swift errors use String(localized:); new xcstrings entry has all 20 locales; web logic is non-UI; pre-existing web strings unchanged.
Cmux Swiftui State Layout ✅ Passed Touches existing legacy ObservableObject (AuthManager since #3027), adding @Published incidentally. No render-time mutations, no GeometryReader, no new SwiftUI state patterns.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR removes ASWebAuthenticationSession sheet and adds error display as Text element in existing Settings view; no new NSWindow/NSPanel/NSWindowController or SwiftUI Window/WindowGroup created.
Description check ✅ Passed The PR description is comprehensive, well-structured, and addresses all template sections with substantial detail.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-3617-signin-fix

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Sources/Auth/AuthManager.swift (1)

743-743: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Swift 6 concurrency warning: main actor isolated property accessed from nonisolated context.

The pipeline flagged a Swift 6 concurrency warning: logTimestampFormatter is main actor-isolated but referenced from the nonisolated static func authLog. While not blocking, this may cause issues when strict concurrency checking is enabled.

🔧 Possible fix

Mark logTimestampFormatter as nonisolated:

-private static let logTimestampFormatter: ISO8601DateFormatter = {
+private nonisolated static let logTimestampFormatter: ISO8601DateFormatter = {
     let f = ISO8601DateFormatter()
     f.formatOptions = [.withInternetDateTime]
     return f
 }()

ISO8601DateFormatter is thread-safe, so this annotation is safe.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Auth/AuthManager.swift` at line 743, The static authLog function
references the main-actor isolated property logTimestampFormatter causing a
Swift 6 concurrency warning; mark the static property logTimestampFormatter as
nonisolated (or otherwise annotate it to be safely accessible from nonisolated
contexts) so authLog can call Self.logTimestampFormatter.string(from:) without
main-actor isolation conflicts; ensure the change is applied to the declaration
of logTimestampFormatter (not authLog) and keep the formatter as an
ISO8601DateFormatter since it is thread-safe.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CHANGELOG.md`:
- Around line 5-9: Update the Unreleased changelog entry under "Fixed" to remove
implementation details like "auth callback" and "native auth handoffs" and
instead describe user-visible behavior: state that Account sign-in now opens in
the user's default browser (replacing the embedded Safari sheet) and that
sign-in errors (e.g., timeouts) are now surfaced to users with localized error
messages; keep the entry concise and product-focused while referencing the
existing bullet "Open Settings > Account sign-in" to replace the current
technical phrasing.

In `@Sources/Auth/AuthManager.swift`:
- Around line 437-440: The defer block currently uses "guard let
browserSignInAttemptID else { return }" which is illegal because defer cannot
transfer control; change it to a conditional binding: inside the defer use "if
let browserSignInAttemptID = browserSignInAttemptID {
finishBrowserSignInAttempt(browserSignInAttemptID) }" (i.e., replace the guard
with an if-let and call finishBrowserSignInAttempt(browserSignInAttemptID) only
when non-nil).

In `@Sources/cmuxApp.swift`:
- Around line 8437-8439: The buttonIsDisabled computed property currently only
disables the button during session restore or when authenticated+loading,
leaving it enabled during sign-in loading and allowing duplicate beginSignIn()
calls; change the condition to disable whenever any auth operation is in-flight
by using authManager.isRestoringSession || authManager.isLoading (i.e., remove
the dependency on isAuthenticated) so the UI prevents additional beginSignIn()
invocations while loading.
- Around line 8388-8392: The code is rendering raw
lastSignInError?.localizedDescription which may expose internal/upstream
details; update the UI to use a sanitized, user-facing message provided by
AuthManager (e.g., expose a computed property like userFacingMessage or
userFacingErrorMessage on AuthManager) instead of localizedDescription; if such
a property doesn't exist, add a mapping in AuthManager that converts known error
types to localized, user-safe strings and returns a generic fallback for unknown
errors, then change the SwiftUI view to display authManager.userFacingMessage
(with the same font/color) rather than lastSignInError?.localizedDescription.

In `@web/app/handler/after-sign-in/page.tsx`:
- Around line 130-137: The isNativeReturnScheme(nativeReturnTo) check is
redundant because nativeCallbackHref is null for non-native schemes; remove that
guard from the if condition and simplify the block to rely on
shouldEmitNativeHandoff({ refreshToken, accessToken }) && nativeCallbackHref,
keeping the existing buildNativeHref(nativeCallbackHref, refreshToken,
accessCookie) call and the OpenNativeClient return path (functions/identifiers
to locate: shouldEmitNativeHandoff, isNativeReturnScheme, nativeCallbackHref,
buildNativeHref, OpenNativeClient).
- Around line 149-161: Remove the redundant fallback block that computes
fallbackReturnTo via nativeAuthCallbackForReturnTo and the subsequent if that
calls buildNativeHref and returns <OpenNativeClient ... />; the same
nativeAuthCallbackForReturnTo value is already used in the primary native
handoff path guarded by shouldEmitNativeHandoff and buildNativeHref, so delete
the fallbackReturnTo variable and the entire fallback if/return (references:
fallbackReturnTo, nativeAuthCallbackForReturnTo, shouldEmitNativeHandoff,
buildNativeHref, OpenNativeClient, accessCookie) to clarify control flow and
avoid dead code.

---

Outside diff comments:
In `@Sources/Auth/AuthManager.swift`:
- Line 743: The static authLog function references the main-actor isolated
property logTimestampFormatter causing a Swift 6 concurrency warning; mark the
static property logTimestampFormatter as nonisolated (or otherwise annotate it
to be safely accessible from nonisolated contexts) so authLog can call
Self.logTimestampFormatter.string(from:) without main-actor isolation conflicts;
ensure the change is applied to the declaration of logTimestampFormatter (not
authLog) and keep the formatter as an ISO8601DateFormatter since it is
thread-safe.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: fcab30d0-21b1-478e-b0b3-183c4852a5a0

📥 Commits

Reviewing files that changed from the base of the PR and between 8aa6aef and c92e4b9.

📒 Files selected for processing (13)
  • CHANGELOG.md
  • Resources/Info.plist
  • Resources/Localizable.xcstrings
  • Sources/Auth/AuthCallbackRouter.swift
  • Sources/Auth/AuthEnvironment.swift
  • Sources/Auth/AuthManager.swift
  • Sources/cmuxApp.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/AuthManagerExternalBrowserSignInTests.swift
  • web/app/handler/after-sign-in/OpenNativeClient.tsx
  • web/app/handler/after-sign-in/native-handoff.ts
  • web/app/handler/after-sign-in/page.tsx
  • web/tests/native-handoff.test.ts
💤 Files with no reviewable changes (1)
  • Resources/Info.plist

Comment thread CHANGELOG.md
Comment thread Sources/Auth/AuthManager.swift
Comment thread Sources/cmuxApp.swift Outdated
Comment thread Sources/cmuxApp.swift
Comment thread web/app/handler/after-sign-in/page.tsx
Comment thread web/app/handler/after-sign-in/page.tsx Outdated
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 29, 2026

Greptile Summary

This PR replaces the ASWebAuthenticationSession embedded sheet with a system-browser sign-in flow for the Settings Account row. beginSignIn() now opens AuthEnvironment.signInURL(callbackURL:) via NSWorkspace, with a per-attempt UUID state embedded in the native callback URL so only the matching callback is accepted.

  • AuthManager gains activeBrowserSignInAttemptState / signOutCancelledBrowserSignInAttemptState to enforce per-attempt callback validation, a 5-minute scheduleBrowserSignInTimeout, and a lastSignInError: AuthManagerError? published property surfaced in Account settings.
  • Info.plist drops the http/https URL-scheme registration to prevent LaunchServices loops, and the web after-sign-in page is updated to coerce native return targets to auth-callback, preserve state, gate native handoff on both tokens, and auto-redirect via window.location.assign.

Confidence Score: 5/5

Safe to merge — the state-matching guard, sign-out cancellation tracking, and timeout teardown are all correctly coordinated on the main actor with no observable double-fire or stale-callback path.

The core callback-validation logic (per-attempt UUID state, sign-out cancellation recording, nil-active-state early return) covers all the edge cases identified in the linked issues and previous PR threads. The finishBrowserSignInAttempt idempotency guard and the isLoading check in beginSignInAndAwait prevent double-timeout side effects even when the internal browserSignInTimeoutTask and the waitForSignInSettled sleep race. Translations are complete across all 20 locales. Tests cover the injected-opener contract, timeout, stale-callback, and stateless-callback paths.

No files require special attention.

Important Files Changed

Filename Overview
Sources/Auth/AuthManager.swift Core architectural change: replaces ASWebAuthenticationSession with NSWorkspace browser opener plus per-attempt state tracking; timeout, sign-out cancellation, and stale-callback guards are all handled correctly on @mainactor
Sources/Auth/AuthCallbackRouter.swift Adds state field to CMUXAuthCallbackPayload and exposes callbackState(from:) for validating per-attempt state; logic is clean and symmetric with the new AuthEnvironment.authCallbackURL
Sources/Auth/AuthEnvironment.swift Adds authCallbackURL(state:) to stamp per-attempt state onto the native callback URL, and makes signInURL(callbackURL:) accept an injectable URL to carry that state; straightforward, no issues
Sources/cmuxApp.swift AuthSettingsRow wraps the existing HStack in a VStack and adds a conditional error Text for userFacingSignInErrorMessage; @Published/ObservableObject invalidation is correct
web/app/handler/after-sign-in/page.tsx Native handoff now uses shouldEmitNativeHandoff (decoded accessToken guard) and nativeAuthCallbackForReturnTo (scheme+state normalization); fallback dead-code block removed; logic is sound
web/app/handler/after-sign-in/native-handoff.ts New module cleanly separates native-scheme detection, callback URL coercion (preserving state), and handoff guard; well-tested in the companion test file
Resources/Localizable.xcstrings Adds settings.account.error.signInTimedOut with translations for all 20 supported locales (ar, bs, da, de, en, es, fr, it, ja, km, ko, nb, pl, pt-BR, ru, th, tr, uk, zh-Hans, zh-Hant)
Resources/Info.plist Removes the http/https CFBundleURLSchemes entry that caused LaunchServices to route web URLs back into cmux, closing the self-loop; native cmux:// scheme registration is preserved
cmuxTests/AuthManagerExternalBrowserSignInTests.swift New regression suite covers: URL opener injection, timeout (timeout=0 + 20ms wait), stale callback after timeout, and stateless callback without active attempt; all assertions are specific and deterministic
web/tests/native-handoff.test.ts Bun tests cover isNativeReturnScheme, nativeAuthCallbackForReturnTo (coercion + state preservation + null cases), and shouldEmitNativeHandoff edge cases; complete and correct

Sequence Diagram

sequenceDiagram
    participant User
    participant Settings as AuthSettingsRow
    participant AM as AuthManager (MainActor)
    participant Browser as System Browser
    participant Web as after-sign-in page
    participant App as cmux URL Handler

    User->>Settings: tap Sign In
    Settings->>AM: beginSignIn(timeout: 300)
    AM->>AM: generate UUID state
    AM->>AM: startBrowserSignInAttempt(state: UUID)
    AM->>AM: scheduleBrowserSignInTimeout(300s)
    AM->>Browser: "NSWorkspace.open(signInURL?state=UUID)"
    AM-->>Settings: "isLoading = true"

    Browser->>Web: /handler/sign-in → /handler/after-sign-in
    Web->>Web: nativeAuthCallbackForReturnTo(nativeReturnTo)
    Web->>Web: "shouldEmitNativeHandoff({refreshToken, accessToken})"
    Web->>Web: buildNativeHref(callbackHref, refreshToken, accessCookie)
    Web->>Browser: "window.location.assign(cmux://auth-callback?...&state=UUID)"

    Browser->>App: "open cmux://auth-callback?stack_refresh=...&state=UUID"
    App->>AM: handleCallbackURL(url)
    AM->>AM: "callbackState == activeBrowserSignInAttemptState?"
    alt state matches
        AM->>AM: tokenStore.seed + refreshSession
        AM->>AM: "didCompleteBrowserSignIn = true, lastSignInError = nil"
        AM->>AM: "finishBrowserSignInAttempt (isLoading = false)"
        AM-->>Settings: "isAuthenticated = true"
    else stale / cancelled / no active attempt
        AM->>AM: log + return (no token seeded)
    end

    opt timeout fires (300s, no callback)
        AM->>AM: "lastSignInError = .signInTimedOut"
        AM->>AM: "finishBrowserSignInAttempt (isLoading = false)"
        AM-->>Settings: userFacingSignInErrorMessage shown
    end
Loading

Reviews (6): Last reviewed commit: "fix: keep auth loading scoped to browser..." | Re-trigger Greptile

Comment on lines 120943 to 120977
}
}
},
"settings.account.error.signInTimedOut": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Sign in timed out. Try again."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "サインインがタイムアウトしました。もう一度お試しください。"
}
},
"uk": {
"stringUnit": {
"state": "translated",
"value": "Час очікування входу минув. Спробуйте ще раз."
}
},
"ko": {
"stringUnit": {
"state": "translated",
"value": "로그인 시간이 초과되었습니다. 다시 시도하세요."
}
}
}
},
"remote.state.connected.vmNoProxy": {
"extractionState": "manual",
"localizations": {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Missing translations for 16 of 20 supported locales

The new settings.account.error.signInTimedOut string is only translated into en, ja, uk, and ko. The catalog already supports ar, bs, da, de, es, fr, it, km, nb, pl, pt-BR, ru, th, tr, zh-Hans, and zh-Hant — users on any of those locales will see the raw English fallback "Sign in timed out. Try again." when the timeout fires. The settings.account.error.missingRefreshToken and settings.account.error.invalidCallback strings added in companion PRs each carry all 20 translations; this one is the odd one out.

Rule Used: Flag production user-facing text that is not fully... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +226 to +231
authLog("auth.browserSignIn begin url=\(Self.redactedURLDescription(signInURL))")
urlOpener(signInURL)
guard isCurrentBrowserSignInAttempt(attemptID) else {
return
}
session.presentationContextProvider = AuthPresentationContext.shared
session.prefersEphemeralWebBrowserSession = false
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 The guard isCurrentBrowserSignInAttempt(attemptID) check after urlOpener(signInURL) is unreachable dead code. urlOpener is a synchronous (URL) -> Void closure; because beginSignIn() runs on the main actor, no other actor-isolated code can execute during the call, so activeBrowserSignInAttemptID cannot change while urlOpener is running. Whether the guard passes or fails, there is no following work — the function ends in both branches. Remove it to avoid misleading future readers.

Suggested change
authLog("auth.browserSignIn begin url=\(Self.redactedURLDescription(signInURL))")
urlOpener(signInURL)
guard isCurrentBrowserSignInAttempt(attemptID) else {
return
}
session.presentationContextProvider = AuthPresentationContext.shared
session.prefersEphemeralWebBrowserSession = false
}
authLog("auth.browserSignIn begin url=\(Self.redactedURLDescription(signInURL))")
urlOpener(signInURL)
}

Comment thread web/app/handler/after-sign-in/page.tsx Outdated
Comment on lines +149 to +159
// Fallback: try native app only when we can preserve the requested native scheme.
const fallbackReturnTo = nativeAuthCallbackForReturnTo(nativeReturnTo);
if (
shouldEmitNativeHandoff({ refreshToken, accessToken }) &&
fallbackReturnTo
) {
const fallback = buildNativeHref(
fallbackReturnTo,
refreshToken,
accessCookie,
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Fallback native-handoff block is unreachable

The fallback's conditions — shouldEmitNativeHandoff({ refreshToken, accessToken }) && fallbackReturnTo — are identical to the reduced conditions of the primary block above (lines 130–137). When both tokens are present and nativeReturnTo is a native scheme, the primary block's buildNativeHref call will always succeed (because accessCookie is guaranteed non-empty by the JSON.stringify([refreshToken, accessToken]) assignment on line 117 when both tokens are truthy). So the primary block always returns when its conditions hold, and the fallback's conditions can never be independently true. This block is dead code and can be removed.

Comment thread Sources/Auth/AuthManager.swift Outdated
Comment thread web/app/handler/after-sign-in/page.tsx Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
cmuxTests/AuthManagerExternalBrowserSignInTests.swift (1)

31-33: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Consider moving signOut() to cleanup rather than test body.

Calling signOut() at Line 33 before the assertions (Lines 35-45) is unusual ordering. If this is cleanup, use defer or a test teardown phase; if it's part of the test flow, add assertions to verify sign-out behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmuxTests/AuthManagerExternalBrowserSignInTests.swift` around lines 31 - 33,
The call to manager.signOut() is placed before the post-beginSignIn assertions,
which mixes cleanup with test verification; either move the signOut() call into
test teardown (use defer { try? await manager.signOut() } or the test class
tearDown/async tearDown) to ensure assertions about manager.isLoading and
related state run unmodified, or keep signOut() in the flow but add explicit
assertions verifying sign-out behavior (e.g., that isAuthenticated == false)
after calling manager.signOut(); locate the calls around manager.beginSignIn(),
manager.isLoading and manager.signOut() in
AuthManagerExternalBrowserSignInTests.swift to implement the change.
Sources/Auth/AuthManager.swift (1)

33-37: ⚠️ Potential issue | 🟠 Major

Add missing localized strings for settings.account.error.signInTimedOut for all supported locales.

Resources/Localizable.xcstrings only provides this key for en, ja, ko, and uk; missing: ar, bs, da, de, es, fr, it, km, no, pl, pt-BR, ru, th, tr, zh-CN, zh-TW.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Auth/AuthManager.swift` around lines 33 - 37, The localized string
key "settings.account.error.signInTimedOut" used in the AuthManager.swift case
.signInTimedOut is only present for en/ja/ko/uk; add this key with appropriate
translated values to Resources/Localizable.xcstrings for the missing locales
(ar, bs, da, de, es, fr, it, km, no, pl, pt-BR, ru, th, tr, zh-CN, zh-TW).
Ensure each entry uses the same key exactly
("settings.account.error.signInTimedOut") and follows the existing .xcstrings
format and context/notes conventions so String(localized:) resolves correctly at
runtime. Validate by building and testing that String(localized:
"settings.account.error.signInTimedOut", defaultValue: ...) returns the
localized text for each locale.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@cmuxTests/AuthManagerExternalBrowserSignInTests.swift`:
- Around line 31-33: The call to manager.signOut() is placed before the
post-beginSignIn assertions, which mixes cleanup with test verification; either
move the signOut() call into test teardown (use defer { try? await
manager.signOut() } or the test class tearDown/async tearDown) to ensure
assertions about manager.isLoading and related state run unmodified, or keep
signOut() in the flow but add explicit assertions verifying sign-out behavior
(e.g., that isAuthenticated == false) after calling manager.signOut(); locate
the calls around manager.beginSignIn(), manager.isLoading and manager.signOut()
in AuthManagerExternalBrowserSignInTests.swift to implement the change.

In `@Sources/Auth/AuthManager.swift`:
- Around line 33-37: The localized string key
"settings.account.error.signInTimedOut" used in the AuthManager.swift case
.signInTimedOut is only present for en/ja/ko/uk; add this key with appropriate
translated values to Resources/Localizable.xcstrings for the missing locales
(ar, bs, da, de, es, fr, it, km, no, pl, pt-BR, ru, th, tr, zh-CN, zh-TW).
Ensure each entry uses the same key exactly
("settings.account.error.signInTimedOut") and follows the existing .xcstrings
format and context/notes conventions so String(localized:) resolves correctly at
runtime. Validate by building and testing that String(localized:
"settings.account.error.signInTimedOut", defaultValue: ...) returns the
localized text for each locale.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 860c0204-7f7e-41e7-ab88-0c56e0db8691

📥 Commits

Reviewing files that changed from the base of the PR and between c92e4b9 and b53968a.

📒 Files selected for processing (2)
  • Sources/Auth/AuthManager.swift
  • cmuxTests/AuthManagerExternalBrowserSignInTests.swift

Comment thread Sources/Auth/AuthManager.swift Outdated
Comment on lines +421 to +425
if let activeState = activeBrowserSignInAttemptState,
callbackState != activeState {
authLog("auth.browserSignIn ignored stale callback state=\(callbackState ?? "nil")")
return
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 State-bearing callbacks bypass staleness enforcement after an attempt has concluded. The current guard only fires when activeBrowserSignInAttemptState is non-nil — if the sign-in attempt already finished (or timed out) and then the user signed out without a pending attempt, cancelBrowserSignInForSignOut records nothing, both activeBrowserSignInAttemptState and signOutCancelledBrowserSignInAttemptState are nil, and a stale callback with the original state UUID re-authenticates the user. Replacing the if let opt-in with an unconditional guard equality check closes the gap: nil == nil still passes legacy stateless callbacks while "uuid-old" == nil is rejected.

Suggested change
if let activeState = activeBrowserSignInAttemptState,
callbackState != activeState {
authLog("auth.browserSignIn ignored stale callback state=\(callbackState ?? "nil")")
return
}
guard callbackState == activeBrowserSignInAttemptState else {
authLog("auth.browserSignIn ignored stale callback state=\(callbackState ?? "nil") activeState=\(activeBrowserSignInAttemptState ?? "nil")")
return
}

Comment thread Sources/Auth/AuthManager.swift
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Sources/Auth/AuthManager.swift (1)

431-434: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Require active state to be non-nil before matching.

The guard callbackState == activeBrowserSignInAttemptState passes when both are nil, allowing a callback (possibly from a pre-state-tracking flow or malformed URL) to be processed when no sign-in attempt is active. The intent is to ignore callbacks that don't match an active attempt's state.

🔧 Proposed fix
-    guard callbackState == activeBrowserSignInAttemptState else {
+    guard let activeState = activeBrowserSignInAttemptState,
+          callbackState == activeState else {
         authLog("auth.browserSignIn ignored stale callback state=\(callbackState ?? "nil") activeState=\(activeBrowserSignInAttemptState ?? "nil")")
         return
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Auth/AuthManager.swift` around lines 431 - 434, The guard currently
allows both callbackState and activeBrowserSignInAttemptState to be nil and thus
proceeds; update the condition in the AuthManager logic so that it requires an
activeBrowserSignInAttemptState to be non-nil before matching (e.g., check
activeBrowserSignInAttemptState != nil && callbackState ==
activeBrowserSignInAttemptState) so stale or unsolicited callbacks are ignored;
locate the check that uses callbackState and activeBrowserSignInAttemptState in
AuthManager.swift and modify that guard accordingly, keeping the existing
authLog and return behavior when the guard fails.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@Sources/Auth/AuthManager.swift`:
- Around line 431-434: The guard currently allows both callbackState and
activeBrowserSignInAttemptState to be nil and thus proceeds; update the
condition in the AuthManager logic so that it requires an
activeBrowserSignInAttemptState to be non-nil before matching (e.g., check
activeBrowserSignInAttemptState != nil && callbackState ==
activeBrowserSignInAttemptState) so stale or unsolicited callbacks are ignored;
locate the check that uses callbackState and activeBrowserSignInAttemptState in
AuthManager.swift and modify that guard accordingly, keeping the existing
authLog and return behavior when the guard fails.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d2f4bfa7-ddca-4fb2-8312-4dbb93902087

📥 Commits

Reviewing files that changed from the base of the PR and between 0906d87 and 6775e2c.

📒 Files selected for processing (1)
  • Sources/Auth/AuthManager.swift

Comment thread Sources/Auth/AuthManager.swift
Comment thread Sources/Auth/AuthManager.swift
Copy link
Copy Markdown

@cursor cursor Bot left a comment

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 and found 1 potential issue.

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 386039a. Configure here.

Comment thread Sources/Auth/AuthManager.swift
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.

Settings → Sign In… opens auth sheet that closes immediately (no page rendered)

1 participant