Skip to content

Fix tagged native auth callback handoff - #5742

Merged
lawrencecchen merged 4 commits into
mainfrom
feat-safari-login-authscheme
Jun 10, 2026
Merged

lawrencecchen merged 4 commits into
mainfrom
feat-safari-login-authscheme

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 10, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • register tagged dev auth callback schemes and have native auth use the matching cmux-dev-<tag> callback
  • print the CLI fallback sign-in URL and expose auth.sign_in_url over the control socket
  • replace the client after-sign-in page with a localized server route and add a nonce-gated native sign-in wrapper
  • tighten native callback state handling, stale attempt handling, timeout behavior, and auth debug redaction

Mechanism

The normal macOS popup path stays on ASWebAuthenticationSession. Tagged dev builds now register cmux-dev-<tag> in Info.plist and pass that same scheme through the native auth URL. The web handoff enters /handler/native-sign-in, mints an httpOnly handoff cookie, and only auto-opens the native callback when the after-sign-in route sees both the expected stateful callback and matching handoff nonce. Direct/manual fallback links remain click-only.

Verification

  • /Users/lawrence/fun/cmuxterm-hq/skills/autoreview/scripts/autoreview --mode local
  • /Users/lawrence/fun/cmuxterm-hq/skills/autoreview/scripts/autoreview --mode branch --base origin/main
  • swift test in Packages/CmuxAuthRuntime
  • bunx tsc --noEmit in web
  • bash -n scripts/reload.sh && git diff --check
  • xcodebuild -project cmux.xcodeproj -scheme cmux-unit -configuration Debug -destination "platform=macOS" -derivedDataPath /tmp/cmux-safauth-cli-test -only-testing:cmuxTests/AuthEnvironmentTests test
  • CMUX_PORT=4577 CMUX_PORT_RANGE=1 CMUX_PORT_END=4577 ./scripts/reload.sh --tag safauth
  • verified built Info.plist includes cmux-dev-safauth and CMUX_AUTH_CALLBACK_SCHEME=cmux-dev-safauth
  • probed /handler/native-sign-in on http://localhost:4577 and confirmed the handoff cookie + Stack redirect

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


Note

High Risk
Touches authentication token handoff, callback validation, and web/native redirect security; regressions could block sign-in or accept wrong callbacks.

Overview
Tagged dev native auth now uses per-tag callback schemes (cmux-dev-<tag>): AuthEnvironment derives them from CMUX_TAG, scripts/reload.sh wires Info.plist URL schemes and env, and sign-in URLs embed cmux_auth_state on the native return URL.

Browser sign-in flow is hardened in HostBrowserSignInFlow: per-attempt cmux_auth_state, manualSignInURL for CLI/socket fallback, stricter external callback routing (state match, stale/sign-out rejection), non-auth ASWebAuthenticationSession completions that wait for a real callback, guarded continuation resumes, and abandoned-attempt timeouts without killing in-flight token validation. Diagnostics add structured logging and broader redaction (including cmux_auth_state).

Control surface: new socket method auth.sign_in_url; CLI cmux auth login prints a fallback sign-in URL before opening the popup.

Web handoff: sign-in enters /handler/native-sign-in (httpOnly handoff nonce) instead of Stack sign-in directly; after-sign-in moves to a localized server route.ts that only auto-opens native when scheme, state, and nonce align; manual fallback stays click-only. i18n strings added for the return page.

Tests expand HostBrowserSignInFlow, AuthDebugLog, AuthEnvironment, and execution-policy coverage.

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


Summary by cubic

Fixes native auth handoff for tagged dev builds and hardens the browser sign-in flow. Tagged schemes now round-trip reliably, and Safari/ASWebAuthenticationSession callbacks are state-checked and nonce-gated.

  • New Features

    • Tagged dev builds register and use cmux-dev-<tag>; scripts/reload.sh writes the Info.plist URL scheme and sets CMUX_AUTH_CALLBACK_SCHEME/CMUX_TAG.
    • Added /handler/native-sign-in wrapper and a server /handler/after-sign-in route with an httpOnly handoff cookie and cmux_auth_handoff nonce; auto-open only when state and nonce verify.
    • Exposed auth.sign_in_url on the control socket; the CLI prints a fallback sign-in URL before opening the popup.
    • HostBrowserSignInFlow generates a stateful manualSignInURL, embeds cmux_auth_state in native return URLs, and gates external callbacks by state.
    • Improved diagnostics: more detailed ASWebAuthenticationSession/runtime logs, optional CMUX_DEBUG_LOG path(s), and broader redaction (covers encoded cmux_auth_state and token values).
  • Bug Fixes

    • Strict callback routing: accepts only the active cmux_auth_state; rejects stale/mismatched callbacks and late callbacks after sign-out.
    • Handles non-auth completions from ASWebAuthenticationSession, waits for external callbacks, prevents double resumes, and times out abandoned attempts without canceling in-flight validation.
    • Web after-sign-in route only allows native return URLs for trusted schemes (cmux, cmux-nightly, and local cmux-dev[-tag]) to prevent unintended launches.
    • Expanded tests across runtime and app: tagged scheme/env selection, state/nonce handoff, attempt timeout, mismatch rejection, and external fallback.

Written for commit 1f1cb27. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Fallback sign-in URL in CLI when primary flow unavailable
    • Enhanced native app authentication handoff with improved state tracking
    • New post-sign-in interface with localized messaging (20+ languages) and auto-redirect capability
  • Improvements

    • Stronger callback validation and timeout handling to prevent stale requests
    • Improved authentication state management for more reliable flows
  • Tests

    • Expanded authentication and debug logging test coverage

@vercel

vercel Bot commented Jun 10, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 10, 2026 2:28am
cmux-staging Building Building Preview, Comment Jun 10, 2026 2:28am

@coderabbitai

coderabbitai Bot commented Jun 10, 2026 •

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 758c00be-38a8-485e-91ec-b8cfa10f6784

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR implements a secure native-to-web sign-in handoff mechanism with per-attempt callback state validation, refactored web routing, and enhanced debugging. The core change threads callback state through HostBrowserSignInFlow to prevent stale callbacks from completing newer attempts; integrates a new /handler/native-sign-in route to control handoff entry and an upgraded /handler/after-sign-in route to manage native/web redirects and token refreshing; adds CLI support for retrieving manual sign-in URLs; and expands logging and token redaction coverage.

Changes

Native Sign-In Handoff Flow

Layer / File(s) Summary
Callback State Management & Attempt Lifecycle
Packages/CmuxAuthRuntime/Sources/CmuxAuthRuntime/BrowserSignIn/HostBrowserSignInFlow.swift, Packages/CmuxAuthRuntime/Tests/CmuxAuthRuntimeTests/HostBrowserSignInFlowTests.swift
HostBrowserSignInFlow now tracks per-attempt callback state and active continuations, validates inbound callbacks against the active attempt's state, schedules/cancels per-attempt timeouts, and prevents stale callbacks from completing newer attempts. Tests expanded to cover state validation, stale/late callback rejection, timeout handling, and state-machine edge cases.
Auth Environment & Callback Scheme Configuration
Sources/Auth/AuthEnvironment.swift, cmuxTests/AuthEnvironmentTests.swift, Sources/Auth/MacAuthComposition.swift
AuthEnvironment.signInURL() now accepts an optional callback state parameter and embeds it in the native callback as cmux_auth_state; callback scheme generation refactored to support DEBUG tag-based schemes (cmux-dev-<tag>) via new overloads; tests validate debug vs. release behaviors and state embedding.
Web Route: Native Sign-In Handoff
web/app/handler/native-sign-in/route.ts
New route controls native-to-web handoff: validates same-origin requests pointing to /handler/after-sign-in, conditionally sets an auto-open nonce cookie based on request origin, and redirects to /handler/sign-in with the validated return target.
Web Route: After Sign-In Handler
web/app/handler/after-sign-in/route.ts
Replaced prior page.tsx implementation with a new TypeScript route that handles native token handoff (with optional auto-open via meta refresh/script when nonce verified), supports Stack token refresh, includes localized UI fallback messages, and manages web-only redirects for after_auth_return_to paths.
CLI & Socket: Manual Sign-In URL Access
CLI/cmux.swift, Sources/TerminalController.swift, Packages/CmuxControlSocket/Sources/CmuxControlSocket/Wire/ControlCommandExecutionPolicy.swift
Added auth.sign_in_url request handling: CLI prints a fallback sign-in URL before opening browser; TerminalController returns manualSignInURL with embedded state; ControlCommandExecutionPolicy routes the request to socket-worker thread.
Enhanced Logging & Token Redaction
Packages/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Diagnostics/AuthDebugLog.swift, Packages/CmuxAuthRuntime/Tests/CmuxAuthRuntimeTests/AuthDebugLogTests.swift, Sources/AppDelegate.swift, Packages/CmuxAuthRuntime/Sources/CmuxAuthRuntime/BrowserSignIn/ASWebBrowserAuthSessionFactory.swift
AuthDebugLog refactored to support multiple configurable debug log paths and expanded regex redaction for URL-encoded token parameters; AppDelegate adds DEBUG instrumentation for URL route counts; ASWebBrowserAuthSessionFactory logs session creation and completion details.
Localization: Post-Sign-In Messages
web/messages/{ar,bs,da,de,en,es,fr,it,ja,km,ko,no,pl,pt-BR,ru,th,tr,uk,zh-CN,zh-TW}.json
Added afterSignIn message blocks with localized title, body, and button text for the post-login screen across 20 language files.
Build Script: URL Scheme Configuration
scripts/reload.sh
Extended to set up custom URL schemes in staged app Info.plist, compute tag-specific callback schemes for dev builds, persist debug log paths, and emit tag-scoped "Dev web command" output.

🎯 3 (Moderate) | ⏱️ ~25 minutes


  • Ari4ka

🐰 A state machine hops secure and true,
Through callbacks verified, both old and new,
Native clients now hand off with grace,
While tokens redact in the debug trace.
Web routes align, and locales shine bright—
Sign-in handoff done just right! ✨


Important

Pre-merge checks failed

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

❌ Failed checks (2 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Cache Substitution Correctness ❌ Error manualSignInURL generates fresh state on every read, overwriting the cache. Multiple reads before startAttempt() causes stale cache where the wrong state is used, rejecting user-clicked links. Change line 82 from let state = makeCallbackState() to let state = pendingManualCallbackState ?? makeCallbackState() to reuse cached state.
Cmux Full Internationalization ❌ Error New CLI auth login/logout text lacks String(localized:defaultValue:) per i18n rules for Swift command text. Web UI properly localized across all 20 locales. Update CLI/cmux.swift auth login/logout messages to use String(localized:defaultValue:) with entries in Resources/Localizable.xcstrings for all supported locales.
Docstring Coverage ⚠️ Warning Docstring coverage is 11.63% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (18 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix tagged native auth callback handoff' directly addresses the main objective of the PR: fixing authentication callback handling for tagged dev builds with per-tag schemes.
Description check ✅ Passed The description comprehensively covers all template sections: a detailed Summary explaining what changed and why (tagged schemes, fallback URLs, server routes, state/nonce handling), Testing section with multiple verification steps performed, and a Checklist confirming testing and resolution of reviews.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed HostBrowserSignInFlow and ASWebBrowserAuthSessionFactory properly marked @MainActor, AuthDebugLog is pure Sendable, AuthEnvironment is static-only, and cross-actor calls properly guarded.
Cmux Swift Blocking Runtime ✅ Passed No blocking primitives in auth production code. Uses clock.sleep() (non-blocking structured concurrency) for timeouts with virtual clock injection support.
Cmux Expensive Synchronous Load ✅ Passed No expensive synchronous loaders added to main actor or socket handlers; new auth.sign_in_url handler performs only in-memory URL/string construction without disk I/O or sysctl calls.
Cmux No Hacky Sleeps ✅ Passed PR introduces no new sleep/timer/polling patterns in TypeScript/JavaScript/shell. New handlers and functions contain no timing primitives. Existing sleeps are pre-existing, not introduced or worsened.
Cmux Algorithmic Complexity ✅ Passed All algorithmic patterns found are acceptable: fixed-size collections (≤2 debug paths, ≤20 locales), small URL query params, 3-item environ var arrays, with early short-circuit returns where needed.
Cmux Swift Concurrency ✅ Passed PR introduces no legacy async patterns. New Task (activeAttemptTimeoutTask) is properly stored, cancelled, and lifecycle-managed; no DispatchQueue.global(), Combine, or fire-and-forget Tasks found.
Cmux Swift @Concurrent ✅ Passed All public async methods in HostBrowserSignInFlow pre-existed; PR modified internal implementation without changing isolation/execution cost/call sites, which the rule allows for existing functions.
Cmux Swift File And Package Boundaries ✅ Passed HostBrowserSignInFlow (412 lines) is existing file with single responsibility; feature logic in packages; app-target files appropriate; no boundary violations.
Cmux Swift Logging ✅ Passed All new logging complies with swift-logging.md: auth code uses proper os.log Logger via AuthDebugLog; all app logging is DEBUG-guarded; CLI print() allowed; tokens redacted; no secrets exposed.
Cmux User-Facing Error Privacy ✅ Passed User-facing outputs comply with privacy rules: CLI prints product messages and web URLs without secrets, web renders localized text, debug logging is gated and redacted.
Cmux Swiftui State Layout ✅ Passed PR contains no SwiftUI View changes; only model/controller class HostBrowserSignInFlow uses modern @Observable pattern (correct approach), web/CLI/AppKit code unchanged.
Cmux Architecture Rethink ✅ Passed Proper architecture: explicit per-attempt state with clear invariants (attemptID, callbackState guards), Clock injection for testability, single @MainActor owner, no timing repairs or split lifecycle.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed No Swift changes add or materially change standalone cmux-owned windows. ASWebAuthenticationSession creation and presentation are delegated to unchanged factory/anchor classes.
Cmux Source Artifacts ✅ Passed All changed files in the PR are legitimate source code, tests, configs, and localization catalogs; no artifacts (logs, caches, temp folders, build output, DerivedData) were added to source control.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-safari-login-authscheme

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.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

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 3 potential issues.

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 5782c5c. Configure here.

}

function requestIsSecure(): boolean {
return process.env.NODE_ENV === "production";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Handoff cookie secure mismatch

Low Severity

The native handoff cookie is set with secure tied to the request scheme in native-sign-in, but cleared in after-sign-in using NODE_ENV === "production". On HTTPS dev hosts, the clear cookie may not match the original Secure flag, so the nonce cookie can outlive auto-open.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5782c5c. Configure here.

@greptile-apps

greptile-apps Bot commented Jun 10, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR hardens native auth callback routing for tagged dev builds by registering per-tag cmux-dev-<tag> URL schemes and threading the scheme through the sign-in URL, the web handoff (/handler/native-sign-in), and the after-sign-in route. It also introduces stateful callback matching (cmux_auth_state), a nonce-gated auto-open mechanism, attempt timeouts, and auth.sign_in_url as a CLI/socket fallback.

  • Tagged dev scheme round-trip: reload.sh patches Info.plist and sets CMUX_TAG/CMUX_AUTH_CALLBACK_SCHEME; AuthEnvironment.callbackScheme derives cmux-dev-<tag> from the env; the new native-sign-in route mints a short-lived httpOnly nonce, and after-sign-in only auto-opens the deep link when state and nonce both verify.
  • Callback hardening in HostBrowserSignInFlow: each attempt now carries an opaque cmux_auth_state UUID; external callbacks are accepted only when they match activeCallbackState; a browserAttemptTimeout cancels abandoned sessions; resumeActiveSessionContinuation prevents double-resume; and manualSignInURL exposes a stateful fallback URL via the new auth.sign_in_url socket command.
  • Diagnostics: structured debug-log messages with wider token redaction (including cmux_auth_state and double-encoded variants) are added throughout the auth flow.

Confidence Score: 4/5

The core auth callback routing is well-structured — state matching, nonce gating, scheme allowlisting, and double-resume prevention are all implemented correctly. The two findings are scoped to edge cases: a cookie-attribute mismatch visible only in HTTPS non-production deployments, and a side-channel state coupling that can produce a stale fallback URL if Settings login fires between two rapid CLI socket calls.

The new auth flow is thoughtfully tested and the security-critical pieces (nonce verification, state matching, allowed-scheme gating) are correct. The requestIsSecure() inconsistency and the sanitizedCallbackSchemeTag collision are real but low-blast-radius issues that do not affect the primary happy path.

web/app/handler/after-sign-in/route.ts (cookie secure-flag inconsistency), Sources/Auth/AuthEnvironment.swift (tag scheme collision), and HostBrowserSignInFlow.swift (side-effectful computed property ordering contract).

Important Files Changed

Filename Overview
web/app/handler/after-sign-in/route.ts Replaces the React page with a server route that reads Stack cookies, builds the native callback URL with tokens, and conditionally auto-opens the deep link via a nonce-checked meta-refresh. Minor requestIsSecure() inconsistency with the sibling route for cookie secure attribute.
web/app/handler/native-sign-in/route.ts New server route that validates same-origin after-sign-in return URL, mints an httpOnly handoff nonce (10 min TTL), and redirects to Stack sign-in. Fetch-metadata guard and same-origin URL validation are correct.
Packages/CmuxAuthRuntime/Sources/CmuxAuthRuntime/BrowserSignIn/HostBrowserSignInFlow.swift Significant hardening: per-attempt cmux_auth_state, continuation-based popup/external-callback routing, browserAttemptTimeout via injected Clock, double-resume guard, and stateful manualSignInURL computed property. The side-effectful computed property creates an ordering dependency with startAttempt().
Sources/Auth/AuthEnvironment.swift Adds CMUX_TAG-based scheme derivation and sanitizedCallbackSchemeTag for tagged dev builds. Scheme sanitization squashes non-alphanumeric characters to -, meaning different raw tags can produce the same sanitized scheme.
Sources/TerminalController.swift Adds auth.sign_in_url socket command that reads manualSignInURL via the existing v2MainSync bridge and returns the URL. Consistent with existing patterns for auth socket commands.
Packages/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Diagnostics/AuthDebugLog.swift Extends token redaction to cover cmux_auth_state and double-encoded variants; adds multi-path debug log support via CMUX_DEBUG_LOG env. All OSLog calls use privacy: .public with pre-redacted messages.
scripts/reload.sh Registers cmux-dev-<tag> URL scheme in Info.plist and sets CMUX_TAG/CMUX_AUTH_CALLBACK_SCHEME in both the plist LSEnvironment and the launch environment for tagged dev builds.
Sources/AppDelegate.swift Adds #if DEBUG-guarded structured log calls in application(_:open:) using AuthDebugLog, logging only URL scheme/host/query-key summaries (no values). Correct redaction via existing AuthDebugLog machinery.

Sequence Diagram

sequenceDiagram
    participant CLI
    participant App as Native App (MainActor)
    participant NativeSignIn as /handler/native-sign-in
    participant Stack as Stack /handler/sign-in
    participant AfterSignIn as /handler/after-sign-in

    CLI->>App: auth.sign_in_url (socket)
    App->>App: manualSignInURL generates state, sets pendingManualCallbackState
    App-->>CLI: url with cmux_auth_state embedded
    CLI->>CLI: print fallback URL

    CLI->>App: auth.begin_sign_in (socket)
    App->>App: startAttempt() consumes pendingManualCallbackState as activeCallbackState
    App->>App: scheduleAttemptTimeout 10 min
    App->>NativeSignIn: ASWebAuthenticationSession GET
    NativeSignIn->>NativeSignIn: "canSetAutoHandoff=true, nonce=UUID"
    NativeSignIn->>NativeSignIn: append cmux_auth_handoff nonce to after-sign-in URL
    NativeSignIn-->>App: 302 to Stack sign-in, Set-Cookie handoff nonce httpOnly
    App->>Stack: User authenticates

    alt Popup path
        Stack-->>App: 302 to after-sign-in with native_app_return_to and nonce
        App->>AfterSignIn: GET with handoff cookie
        AfterSignIn->>AfterSignIn: verifiedAutoOpen cookie matches nonce
        AfterSignIn-->>App: meta-refresh to cmux-dev-tag callback URL with tokens
        App->>App: handleCallbackURL state matches activeCallbackState, completeCallback
    else Fallback path
        CLI->>NativeSignIn: user opens fallback URL manually
        NativeSignIn-->>AfterSignIn: same nonce flow
        AfterSignIn-->>App: URL scheme callback, state matches activeCallbackState
    end

    App->>App: cancelAttemptTimeout, finishAttempt
    App-->>CLI: auth.begin_sign_in signed in
Loading

Comments Outside Diff (1)

  1. Sources/Auth/AuthEnvironment.swift, line 89-102 (link)

    P2 Tag sanitization allows scheme collisions

    sanitizedCallbackSchemeTag replaces every run of non-alphanumeric characters with a single hyphen, so distinct raw tags like "foo_bar" and "foo bar" (or "foo.bar") all normalize to "foo-bar". Two developers running reload.sh --tag foo_bar and --tag foo bar on the same machine would register the same cmux-dev-foo-bar URL scheme and their ASWebAuthenticationSession callbacks would be routed unpredictably. A doc-comment on the function (or a logged warning when sanitizedCallbackSchemeTag(tag) != tag.lowercased()) would help surface the collision risk at tag-registration time.

Reviews (1): Last reviewed commit: "Update Swift file length budget for auth..." | Re-trigger Greptile

Comment on lines +270 to +272
function requestIsSecure(): boolean {
return process.env.NODE_ENV === "production";
}

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 requestIsSecure() helper uses NODE_ENV === "production" to set the secure flag when clearing the handoff cookie, but the sibling native-sign-in route sets the same cookie using request.nextUrl.protocol === "https:". In an HTTPS staging or preview environment where NODE_ENV is not "production", the cookie would have been written with secure: true (by native-sign-in) but the deletion attempt here would use secure: false, causing browsers to silently ignore the Set-Cookie header and leaving a stale nonce in the browser until it naturally expires (10 min).

Suggested change
function requestIsSecure(): boolean {
return process.env.NODE_ENV === "production";
}
function requestIsSecure(request: NextRequest): boolean {
return request.nextUrl.protocol === "https:";
}

Comment on lines +79 to +85
/// The hosted sign-in URL for manual fallback when the browser handoff does
/// not return to the native app.
public var manualSignInURL: URL {
let state = makeCallbackState()
pendingManualCallbackState = state
return makeSignInURL(state)
}

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 Side-effectful computed property creates fragile ordering coupling

manualSignInURL writes pendingManualCallbackState as a side effect, which is then consumed by the very next startAttempt() call. The invariant holds for the CLI's sequential auth.sign_in_url → auth.begin_sign_in path, but breaks if beginSignIn() fires between those two socket calls (e.g., the user clicks the Settings login button). In that case, startAttempt() consumes state-N for the Settings popup, the subsequent auth.begin_sign_in call generates a fresh state-N+1 for the CLI popup, and the already-printed fallback URL (holding state-N) will be rejected as a state mismatch when the user tries to use it. Consider making the state transfer explicit by having the caller own the UUID and pass it through both the URL generator and the sign-in call, rather than leaving it in shared mutable side-channel state.

Rule Used: Flag Swift fixes that patch symptoms while leaving... (source)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 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 `@cmuxTests/AuthEnvironmentTests.swift`:
- Around line 40-45: The test signInURLEntersNativeWrapper currently only checks
that the query contains "after_auth_return_to="; update it to parse the URL's
query items (use URLComponents(url: _, resolvingAgainstBaseURL:
false).queryItems) find the "after_auth_return_to" value, URL-decode it and
parse it as a nested URLComponents, and then assert that the nested queryItems
include "native_app_return_to" and "cmux_auth_state" with the expected value
("state-1"); reference AuthEnvironment.signInURL(callbackState:) and the test
signInURLEntersNativeWrapper to locate the code to change.

In
`@Packages/CmuxAuthRuntime/Sources/CmuxAuthRuntime/BrowserSignIn/HostBrowserSignInFlow.swift`:
- Around line 81-84: manualSignInURL currently creates a new callback state
every read and overwrites pendingManualCallbackState, invalidating earlier URLs;
change the getter so it first checks if pendingManualCallbackState already
exists and, if so, returns makeSignInURL(existingState), otherwise generate a
new state via makeCallbackState(), assign it to pendingManualCallbackState, and
then return makeSignInURL(state), keeping pendingManualCallbackState stable
until it is consumed (e.g., by the existing consumption code that reads/clears
pendingManualCallbackState).

In `@scripts/reload.sh`:
- Around line 322-327: The helper set_plist_url_scheme currently only tries Set
:CFBundleURLTypes:1:CFBundleURLSchemes:0 and swallows failures; change it to try
a robust sequence of updates (attempt index 1, then index 0, then scan
CFBundleURLTypes entries to locate the first CFBundleURLSchemes array and set
its 0th element) and if all attempts fail emit a clear error to stderr and exit
non‑zero instead of swallowing the PlistBuddy error; update the function name
set_plist_url_scheme to return failure on error and ensure callers handle that
non‑zero exit so the reload fails fast when the auth URL scheme rewrite does not
land.

In `@web/app/handler/after-sign-in/route.ts`:
- Around line 26-29: The host-parsing in isLocalRequest incorrectly splits on
":" so bracketed IPv6 hosts like "[::1]:3000" become "[" and fail the local
check; update isLocalRequest to robustly extract the hostname rather than using
simple split — for example, detect and strip an IPv6 bracketed address (remove
surrounding "[" and "]" and any trailing ":port"), or use a URL/hostname parser
to get request host, then compare the resulting hostname (e.g., "::1",
"127.0.0.1", "localhost") against the local list; ensure the change targets the
isLocalRequest function and preserves the existing comparisons.
- Around line 80-101: The decodeAccessCookie and decodeRefreshCookie functions
call decodeURIComponent on untrusted cookie strings which can throw URIError for
malformed percent-escapes; wrap the decodeURIComponent call in a try/catch (or
perform a safeDecode helper used by both functions) so that if
decodeURIComponent throws you fall back to the raw value instead of propagating
the error, preserving existing behavior (return accessToken/refreshToken or
undefined) and preventing a 500 from malformed cookies.
🪄 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: aa671515-1a32-46ee-82c8-0a095321b5ab

📥 Commits

Reviewing files that changed from the base of the PR and between 919d2d4 and 5782c5c.

📒 Files selected for processing (39)
  • CLI/cmux.swift
  • Packages/CmuxAuthRuntime/Sources/CmuxAuthRuntime/BrowserSignIn/ASWebBrowserAuthSessionFactory.swift
  • Packages/CmuxAuthRuntime/Sources/CmuxAuthRuntime/BrowserSignIn/HostBrowserSignInFlow.swift
  • Packages/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Diagnostics/AuthDebugLog.swift
  • Packages/CmuxAuthRuntime/Tests/CmuxAuthRuntimeTests/AuthDebugLogTests.swift
  • Packages/CmuxAuthRuntime/Tests/CmuxAuthRuntimeTests/HostBrowserSignInFlowTests.swift
  • Packages/CmuxControlSocket/Sources/CmuxControlSocket/Wire/ControlCommandExecutionPolicy.swift
  • Packages/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandExecutionPolicyTests.swift
  • Sources/AppDelegate.swift
  • Sources/Auth/AuthEnvironment.swift
  • Sources/Auth/MacAuthComposition.swift
  • Sources/TerminalController.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/AuthEnvironmentTests.swift
  • scripts/reload.sh
  • web/app/handler/after-sign-in/OpenNativeClient.tsx
  • web/app/handler/after-sign-in/page.tsx
  • web/app/handler/after-sign-in/route.ts
  • web/app/handler/native-sign-in/route.ts
  • web/messages/ar.json
  • web/messages/bs.json
  • web/messages/da.json
  • web/messages/de.json
  • web/messages/en.json
  • web/messages/es.json
  • web/messages/fr.json
  • web/messages/it.json
  • web/messages/ja.json
  • web/messages/km.json
  • web/messages/ko.json
  • web/messages/no.json
  • web/messages/pl.json
  • web/messages/pt-BR.json
  • web/messages/ru.json
  • web/messages/th.json
  • web/messages/tr.json
  • web/messages/uk.json
  • web/messages/zh-CN.json
  • web/messages/zh-TW.json
💤 Files with no reviewable changes (2)
  • web/app/handler/after-sign-in/OpenNativeClient.tsx
  • web/app/handler/after-sign-in/page.tsx

Comment on lines +40 to +45
@Test("sign-in URL enters native wrapper")
func signInURLEntersNativeWrapper() {
let url = AuthEnvironment.signInURL(callbackState: "state-1")
#expect(url.path == "/handler/native-sign-in")
#expect(url.query?.contains("after_auth_return_to=") == true)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Strengthen nested callback assertions in the wrapper URL test.

The current check can pass even if the nested after_auth_return_to payload is malformed. Parse query items and assert the nested URL contains the expected native_app_return_to (and cmux_auth_state) contract.

Suggested test hardening
 `@Test`("sign-in URL enters native wrapper")
 func signInURLEntersNativeWrapper() {
     let url = AuthEnvironment.signInURL(callbackState: "state-1")
     `#expect`(url.path == "/handler/native-sign-in")
-    `#expect`(url.query?.contains("after_auth_return_to=") == true)
+    let outer = URLComponents(url: url, resolvingAgainstBaseURL: false)
+    let afterAuthReturnTo = outer?.queryItems?.first(where: { $0.name == "after_auth_return_to" })?.value
+    `#expect`(afterAuthReturnTo != nil)
+
+    let afterSignInURL = URL(string: afterAuthReturnTo!)
+    let afterSignIn = URLComponents(url: afterSignInURL!, resolvingAgainstBaseURL: false)
+    let nativeAppReturnTo = afterSignIn?.queryItems?.first(where: { $0.name == "native_app_return_to" })?.value
+    `#expect`(nativeAppReturnTo != nil)
+    `#expect`(nativeAppReturnTo?.contains("cmux_auth_state=state-1") == true)
 }
🤖 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/AuthEnvironmentTests.swift` around lines 40 - 45, The test
signInURLEntersNativeWrapper currently only checks that the query contains
"after_auth_return_to="; update it to parse the URL's query items (use
URLComponents(url: _, resolvingAgainstBaseURL: false).queryItems) find the
"after_auth_return_to" value, URL-decode it and parse it as a nested
URLComponents, and then assert that the nested queryItems include
"native_app_return_to" and "cmux_auth_state" with the expected value
("state-1"); reference AuthEnvironment.signInURL(callbackState:) and the test
signInURLEntersNativeWrapper to locate the code to change.

Comment on lines +81 to +84
public var manualSignInURL: URL {
let state = makeCallbackState()
pendingManualCallbackState = state
return makeSignInURL(state)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make manualSignInURL stable for the current pending attempt.

Line 82 generates a fresh callback state on every read and overwrites pendingManualCallbackState. Any caller that touches this property twice before Line 195 consumes it will invalidate the first URL, so the earlier link's callback is rejected as stale.

Suggested fix
 public var manualSignInURL: URL {
-    let state = makeCallbackState()
-    pendingManualCallbackState = state
-    return makeSignInURL(state)
+    let state = pendingManualCallbackState ?? makeCallbackState()
+    pendingManualCallbackState = state
+    return makeSignInURL(state)
 }
🤖 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
`@Packages/CmuxAuthRuntime/Sources/CmuxAuthRuntime/BrowserSignIn/HostBrowserSignInFlow.swift`
around lines 81 - 84, manualSignInURL currently creates a new callback state
every read and overwrites pendingManualCallbackState, invalidating earlier URLs;
change the getter so it first checks if pendingManualCallbackState already
exists and, if so, returns makeSignInURL(existingState), otherwise generate a
new state via makeCallbackState(), assign it to pendingManualCallbackState, and
then return makeSignInURL(state), keeping pendingManualCallbackState stable
until it is consumed (e.g., by the existing consumption code that reads/clears
pendingManualCallbackState).

Comment thread scripts/reload.sh
Comment on lines +322 to +327
set_plist_url_scheme() {
local plist="$1"
local scheme="$2"
/usr/libexec/PlistBuddy -c "Set :CFBundleURLTypes:1:CFBundleURLSchemes:0 \"${scheme}\"" "$plist" 2>/dev/null \
|| true
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail fast if the auth URL scheme rewrite does not land.

This helper only tries CFBundleURLTypes:1:CFBundleURLSchemes:0 and then swallows any PlistBuddy failure. If that slot ever moves or is missing, the staged app stays registered for the old callback scheme while Lines 913-923 and web/app/handler/after-sign-in/route.ts now require cmux-dev-${TAG_SLUG}. The reload still reports success, but tagged native auth callbacks no longer reopen the app.

🩹 Suggested fix
 set_plist_url_scheme() {
   local plist="$1"
   local scheme="$2"
-  /usr/libexec/PlistBuddy -c "Set :CFBundleURLTypes:1:CFBundleURLSchemes:0 \"${scheme}\"" "$plist" 2>/dev/null \
-    || true
+  if ! /usr/libexec/PlistBuddy -c "Set :CFBundleURLTypes:1:CFBundleURLSchemes:0 \"${scheme}\"" "$plist" 2>/dev/null; then
+    echo "error: failed to update auth callback URL scheme in $plist" >&2
+    return 1
+  fi
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
set_plist_url_scheme() {
local plist="$1"
local scheme="$2"
/usr/libexec/PlistBuddy -c "Set :CFBundleURLTypes:1:CFBundleURLSchemes:0 \"${scheme}\"" "$plist" 2>/dev/null \
|| true
}
set_plist_url_scheme() {
local plist="$1"
local scheme="$2"
if ! /usr/libexec/PlistBuddy -c "Set :CFBundleURLTypes:1:CFBundleURLSchemes:0 \"${scheme}\"" "$plist" 2>/dev/null; then
echo "error: failed to update auth callback URL scheme in $plist" >&2
return 1
fi
}
🤖 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 `@scripts/reload.sh` around lines 322 - 327, The helper set_plist_url_scheme
currently only tries Set :CFBundleURLTypes:1:CFBundleURLSchemes:0 and swallows
failures; change it to try a robust sequence of updates (attempt index 1, then
index 0, then scan CFBundleURLTypes entries to locate the first
CFBundleURLSchemes array and set its 0th element) and if all attempts fail emit
a clear error to stderr and exit non‑zero instead of swallowing the PlistBuddy
error; update the function name set_plist_url_scheme to return failure on error
and ensure callers handle that non‑zero exit so the reload fails fast when the
auth URL scheme rewrite does not land.

Comment on lines +26 to +29
function isLocalRequest(request: NextRequest): boolean {
const hostHeader = request.headers.get("host");
const host = (hostHeader?.split(":")[0] ?? request.nextUrl.hostname).toLowerCase();
return host === "localhost" || host === "127.0.0.1" || host === "::1";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

[::1] never passes the local-request check.

Line 28 splits the Host header on :, so a loopback IPv6 host like [::1]:3000 becomes "[". That makes isLocalRequest() false and rejects cmux-dev / tagged callback schemes on a local dev server reached via IPv6.

🤖 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 `@web/app/handler/after-sign-in/route.ts` around lines 26 - 29, The
host-parsing in isLocalRequest incorrectly splits on ":" so bracketed IPv6 hosts
like "[::1]:3000" become "[" and fail the local check; update isLocalRequest to
robustly extract the hostname rather than using simple split — for example,
detect and strip an IPv6 bracketed address (remove surrounding "[" and "]" and
any trailing ":port"), or use a URL/hostname parser to get request host, then
compare the resulting hostname (e.g., "::1", "127.0.0.1", "localhost") against
the local list; ensure the change targets the isLocalRequest function and
preserves the existing comparisons.

Comment on lines +80 to +101
function decodeAccessCookie(value: string | undefined): { refreshToken?: string; accessToken?: string } {
if (!value) return {};
const decoded = value.includes("%") ? decodeURIComponent(value) : value;
if (!decoded.startsWith("[")) return { accessToken: decoded };
try {
const arr = JSON.parse(decoded) as unknown[];
if (Array.isArray(arr) && arr.length >= 2) {
return { refreshToken: arr[0] as string, accessToken: arr[1] as string };
}
} catch {}
return {};
}

function decodeRefreshCookie(value: string | undefined): string | undefined {
if (!value) return undefined;
const decoded = value.includes("%") ? decodeURIComponent(value) : value;
if (!decoded.startsWith("{")) return decoded;
try {
const obj = JSON.parse(decoded) as Record<string, unknown>;
if (typeof obj.refresh_token === "string") return obj.refresh_token;
} catch {}
return undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard malformed cookie decoding.

Lines 82 and 95 call decodeURIComponent on raw cookie values. Those cookies are request-controlled input; a stray % or truncated escape throws URIError and turns /handler/after-sign-in into a 500 during sign-in completion.

Suggested fix
+function safeDecodeCookie(value: string): string | null {
+  try {
+    return value.includes("%") ? decodeURIComponent(value) : value;
+  } catch {
+    return null;
+  }
+}
+
 function decodeAccessCookie(value: string | undefined): { refreshToken?: string; accessToken?: string } {
   if (!value) return {};
-  const decoded = value.includes("%") ? decodeURIComponent(value) : value;
+  const decoded = safeDecodeCookie(value);
+  if (decoded === null) return {};
   if (!decoded.startsWith("[")) return { accessToken: decoded };
   try {
     const arr = JSON.parse(decoded) as unknown[];
@@
 function decodeRefreshCookie(value: string | undefined): string | undefined {
   if (!value) return undefined;
-  const decoded = value.includes("%") ? decodeURIComponent(value) : value;
+  const decoded = safeDecodeCookie(value);
+  if (decoded === null) return undefined;
   if (!decoded.startsWith("{")) return decoded;
   try {
🤖 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 `@web/app/handler/after-sign-in/route.ts` around lines 80 - 101, The
decodeAccessCookie and decodeRefreshCookie functions call decodeURIComponent on
untrusted cookie strings which can throw URIError for malformed percent-escapes;
wrap the decodeURIComponent call in a try/catch (or perform a safeDecode helper
used by both functions) so that if decodeURIComponent throws you fall back to
the raw value instead of propagating the error, preserving existing behavior
(return accessToken/refreshToken or undefined) and preventing a 500 from
malformed cookies.

@lawrencecchen
lawrencecchen merged commit 028f5dc into main Jun 10, 2026
30 of 35 checks passed
@lawrencecchen
lawrencecchen deleted the feat-safari-login-authscheme branch June 10, 2026 02:18
@lawrencecchen
lawrencecchen restored the feat-safari-login-authscheme branch July 18, 2026 10:18

This branch was successfully deployed

1 active deployment
Preview – cmux — 1f1cb272 Deployed Jun 10, 2026 by vercel[bot]
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