Thanks to visit codestin.com
Credit goes to github.com

Skip to content

fix(auth): make --device-auth login work end-to-end - #478

Merged
ndycode merged 3 commits into
mainfrom
fix/issue-477-device-auth-keepalive
May 10, 2026
Merged

ndycode merged 3 commits into
mainfrom
fix/issue-477-device-auth-keepalive

Conversation

@ndycode

@ndycode ndycode commented May 10, 2026

Copy link
Copy Markdown
Owner

Summary

  • Fix --device-auth crash from "Detected unsettled top-level await" by adding a keepAlive option to the device-auth flow that prevents polling timers from being unref'd in CLI context.
  • Skip the Accounts Dashboard menu when an explicit transport flag (--device-auth, --manual, --no-browser) is provided, so the requested mode runs even with existing accounts.
  • Exit cleanly after declining "Add another?" when an explicit transport mode was used (previously looped back into a fresh sign-in).

Closes #477

Files Changed

  • lib/auth/device-auth.ts adds opt-in keepAlive plumbing in getSleep and sleepWithAbort. Default behavior preserved for library consumers.
  • lib/codex-manager.ts passes keepAlive: true from the single CLI call site, gates the dashboard with explicitSignInMode, and returns 0 from the add-another decline branch when in explicit mode.
  • test/device-auth.test.ts adds 3 unit tests for the keepAlive plumbing.
  • test/codex-manager-cli.test.ts adds 2 integration tests for dashboard bypass with existing accounts (one for --device-auth, one for --manual).

Risk Notes

  • keepAlive defaults to false, so existing library consumers of runDeviceAuthFlow keep the previous unref'd-timer behavior.
  • Dashboard skip only triggers when the user explicitly passes a transport flag. Bare cx auth login with existing accounts still opens the dashboard.
  • Manage-account refresh, repair commands, and interactive promptOAuthSignInMode were audited: none reach runDeviceAuthFlow so keepAlive exposure is contained to the documented CLI entry point.
  • AbortSignal path was checked: when keepAlive: true and the signal aborts, clearTimeout runs before reject, so the ref is released and the process can exit.

Test plan

  • npm run typecheck clean
  • npm test 3979/3979 passing across 267 files in 112s
  • Local manual verification: codex-multi-auth login --device-auth prints device code, stays alive past the previous crash point, completes auth, and exits cleanly after n to "Add another?"
  • Local manual verification with existing account pool: cx auth login --device-auth bypasses the dashboard and runs device-auth directly

note: greptile review for oc-chatgpt-multi-auth. cite files like lib/foo.ts:123. confirm regression tests + windows concurrency/token redaction coverage.

Greptile Summary

this pr fixes --device-auth login end-to-end by adding a keepAlive option that prevents polling timers from being unref'd in the cli context, skipping the accounts dashboard when an explicit transport flag is passed, and returning cleanly from all three exit edges (cancel, add-another decline, max-accounts cap).

  • lib/auth/device-auth.ts adds keepAlive?: boolean to DeviceAuthFlowOptions; both getSleep and sleepWithAbort gate unrefTimer behind !options.keepAlive, with the abort path correctly calling clearTimeout before rejecting to release the handle.
  • lib/codex-manager.ts sets keepAlive: true at the single cli call site, derives explicitSignInMode from loginOptions.deviceAuth || loginOptions.manual (which correctly covers --no-browser since that flag maps to manual: true in parseAuthLoginArgs), and guards all three inner-loop exit edges with if (explicitSignInMode) return 0 — including the previously unguarded MAX_ACCOUNTS break.
  • five new integration/unit tests cover keepAlive timer behaviour, dashboard bypass for --device-auth and --manual, clean cancel exit, and the max-accounts cap branch.

Confidence Score: 5/5

safe to merge — the change is self-contained, defaults are preserved for library consumers, and all three inner-loop exit edges in explicit mode are correctly guarded.

keepAlive defaults to false, so no existing library consumer behaviour changes. the explicitSignInMode guard is derived correctly from parsed options (covering --no-browser via loginOptions.manual). the previously unguarded MAX_ACCOUNTS break is fixed in this diff and covered by a new integration test. the abort path clears the referenced timer before rejecting, so no process-hang risk. no concurrency or windows filesystem concerns introduced.

no files require special attention.

Important Files Changed

Filename Overview
lib/auth/device-auth.ts adds keepAlive option to DeviceAuthFlowOptions; both getSleep and sleepWithAbort correctly gate unrefTimer behind !options.keepAlive; abort path clears the timer before rejecting, so no dangling handle when aborting with keepAlive enabled.
lib/codex-manager.ts hardcodes keepAlive: true at the single device-auth cli call site; derives explicitSignInMode correctly (covers --no-browser via loginOptions.manual); all three inner-loop early-exit edges now have if (explicitSignInMode) return 0 guards, including the previously unguarded MAX_ACCOUNTS break.
test/device-auth.test.ts adds four targeted unit tests for keepAlive: no-unref with keepAlive, unref preserved without keepAlive, sleepWithAbort with signal and keepAlive, and abort-clears-timer with keepAlive; guard assertion on setTimeoutCount prevents vacuous-pass regression.
test/codex-manager-cli.test.ts adds five integration tests: dashboard bypass for --device-auth and --manual with existing accounts, clean cancel exit, and MAX_ACCOUNTS cap exit; fetch call-count assertions guard against re-entering the flow.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["cx auth login [--device-auth | --manual | --no-browser]"] --> B{explicitSignInMode?}
    B -- yes --> D[skip dashboard]
    B -- no --> C{existing accounts?}
    C -- yes --> E[show accounts dashboard]
    E --> F{menu result}
    F -- add --> D
    F -- cancel --> Z[return 0]
    D --> G[runSignInFlow]
    G -- device --> H["runDeviceAuthFlow(keepAlive: true)"]
    G -- manual/browser --> I[runOAuthFlow]
    H --> J{result}
    I --> J
    J -- cancelled & explicitSignInMode --> Z
    J -- success --> K[persistAccountPool]
    K --> L{count >= MAX_ACCOUNTS?}
    L -- yes & explicitSignInMode --> Z
    L -- yes & !explicit --> M[break → loginFlow restart]
    L -- no --> N[promptAddAnotherAccount]
    N -- false & explicitSignInMode --> Z
    N -- false & !explicit --> M
    N -- true --> G
Loading

Comments Outside Diff (1)

  1. lib/codex-manager.ts, line 3179-3184 (link)

    P1 MAX_ACCOUNTS break re-enters device-auth in explicit mode — the bare break exits only the inner sign-in while(true) loop; the outer loginFlow: while (true) then restarts, skips the dashboard because explicitSignInMode is still true, and immediately kicks off another device-auth session without user interaction. the same return 0 guard applied at the addAnother = false branch three lines below should also be applied here: if (explicitSignInMode) { return 0; } before the break.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: lib/codex-manager.ts
    Line: 3179-3184
    
    Comment:
    **MAX_ACCOUNTS `break` re-enters device-auth in explicit mode** — the bare `break` exits only the inner sign-in `while(true)` loop; the outer `loginFlow: while (true)` then restarts, skips the dashboard because `explicitSignInMode` is still `true`, and immediately kicks off another device-auth session without user interaction. the same `return 0` guard applied at the `addAnother = false` branch three lines below should also be applied here: `if (explicitSignInMode) { return 0; }` before the `break`.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Codex

Reviews (3): Last reviewed commit: "fix(auth): exit cleanly at MAX_ACCOUNTS ..." | Re-trigger Greptile

Closes #477

The CLI invocation of runDeviceAuthFlow was crashing with "Detected
unsettled top-level await" because the polling sleep timer is unref'd,
which is correct for library/background callers but causes Node to exit
before the user can complete browser authorization when called from
top-level await in scripts/codex-multi-auth.js.

Additionally, when the user already had accounts, login --device-auth
was being swallowed by the Accounts Dashboard menu instead of running
the requested transport.

Changes:
- lib/auth/device-auth.ts: add keepAlive option to DeviceAuthFlowOptions.
  When true, getSleep and sleepWithAbort skip unrefTimer so the polling
  sleep keeps the event loop alive. Default behavior unchanged so
  library consumers retain unref'd timers.
- lib/codex-manager.ts: pass keepAlive: true from runSignInFlow (the
  only CLI caller of runDeviceAuthFlow).
- lib/codex-manager.ts: when --device-auth, --manual, or --no-browser
  is explicitly provided, skip the Accounts Dashboard menu so the
  requested transport runs even if accounts already exist. After a
  successful add, declining "Add another?" exits cleanly instead of
  falling back into a fresh sign-in loop.

Tests:
- test/device-auth.test.ts: 3 unit tests covering keepAlive suppressing
  unref in getSleep, default behavior preserved, and keepAlive
  suppressing unref in sleepWithAbort.
- test/codex-manager-cli.test.ts: 2 integration tests verifying
  --device-auth and --manual bypass the dashboard when accounts already
  exist and exit cleanly after declining add-another.
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@coderabbitai

coderabbitai Bot commented May 10, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

The PR fixes a process-exit issue when using --device-auth by adding a keepAlive timer option to prevent Node's event loop from terminating during polling, passing this flag in the device auth sign-in flow, and adjusting dashboard prompt logic to bypass it when explicit auth modes are selected with existing accounts.

Changes

Device Auth Timer Lifecycle & CLI Flow

Layer / File(s) Summary
Timer Management Data Shape
lib/auth/device-auth.ts:54-59
DeviceAuthFlowOptions adds optional keepAlive?: boolean field to control timer unref() behavior.
Core Timer Implementation
lib/auth/device-auth.ts:80-82, lib/auth/device-auth.ts:154-156
getSleep and sleepWithAbort conditionally skip unref() when keepAlive: true, keeping polling timers referenced to the event loop.
CLI Device Auth Integration
lib/codex-manager.ts:2034-2037
Device sign-in flow passes keepAlive: true into runDeviceAuthFlow(...) to prevent premature exit during browser auth step.
Dashboard Bypass Control Flow
lib/codex-manager.ts:2793-2804, lib/codex-manager.ts:3180-3187
Detects explicit sign-in modes (--device-auth or --manual); skips dashboard menu and returns immediately when mode was explicit and user declines adding another account.
Timer Lifecycle & Dashboard Bypass Tests
test/device-auth.test.ts:671-754, test/codex-manager-cli.test.ts:4869-5004
New test suites verify keepAlive suppresses unref() calls during polling and with abort signals; CLI tests confirm dashboard is bypassed when explicit flags are used with existing accounts and new account is persisted.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

bug


review notes

  • the keepalive mechanism is sound: timer lifecycle control is isolated to lib/auth/device-auth.ts:80-82 and lib/auth/device-auth.ts:154-156. verify both getSleep and sleepWithAbort paths are exercised in tests (they are, via test/device-auth.test.ts:671-754).
  • the dashboard bypass logic at lib/codex-manager.ts:3180-3187 returns 0 immediately when explicitSignInMode is true and addAnotherAccount is declined. confirm this doesn't skip cleanup or leave dangling promises, especially on windows where process termination timing can be tight.
  • test/codex-manager-cli.test.ts:4869-5004 stubs the device auth and manual flows but doesn't test concurrency scenarios (e.g., rapid successive --device-auth calls or signal interruption during polling). these edge cases may emerge in field use.
  • the keepAlive flag defaults to undefined, which is falsy, so existing callers continue unref'ing timers. only the new device sign-in flow at lib/codex-manager.ts:2034-2037 explicitly enables it. confirm no other auth paths need this (manual, normal oauth flows).
  • consider whether the dashboard "add another account" prompt should be suppressible via the same explicit-mode flag in future, or if that's out of scope here.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title follows conventional commits format with type 'fix', scope 'auth', and summary ≤72 chars in lowercase imperative.
Linked Issues check ✅ Passed Code changes directly address #477: keepAlive prevents premature exit, dashboard bypass respects explicit flags, and end-to-end device-auth flow now works.
Out of Scope Changes check ✅ Passed All changes align with #477 objectives: keepAlive plumbing, explicit-flag gating, and dashboard bypass logic. No extraneous refactors or unrelated feature additions.
Description check ✅ Passed PR description comprehensively covers the fix, includes risk analysis, test plan with manual verification results, and traces the explicitSignInMode guard across all three exit edges.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

📋 Issue Planner

Built with CodeRabbit's Coding Plans for faster development and fewer bugs.

View plan used: #477

✨ 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 fix/issue-477-device-auth-keepalive
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fix/issue-477-device-auth-keepalive

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.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (1)
lib/codex-manager.ts (1)

3138-3144: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

explicit transport cancel currently loops into a fresh sign-in attempt.

with dashboard bypass in lib/codex-manager.ts:2800-2804, this continue loginFlow at lib/codex-manager.ts:3138-3144 re-enters auth again on cancellation for explicit modes instead of exiting. this can trap users in repeated prompts; on windows shells this is especially rough when cancel behavior is inconsistent across terminals.

proposed fix
 			if (tokenResult.type !== "success") {
 				if (isOAuthCancellation(tokenResult)) {
+					if (explicitSignInMode) {
+						return 0;
+					}
 					if (existingCount > 0) {
 						console.log(
 							stylePromptText(UI_COPY.oauth.cancelledBackToMenu, "muted"),
 						);
 						continue loginFlow;
 					}

please add a vitest regression in test/codex-manager-cli.test.ts:1 for: existing accounts + explicit --manual/--device-auth + cancel => exits with 0 and no second sign-in prompt.

As per coding guidelines, lib/**: "verify every change cites affected tests (vitest)" and "focus on ... concurrency."

🤖 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 `@lib/codex-manager.ts` around lines 3138 - 3144, When
isOAuthCancellation(tokenResult) is true and existingCount > 0 inside the
loginFlow loop, do not continue back into authentication; instead break
out/return so explicit transports (manual/device-auth) exit immediately rather
than re-entering loginFlow — replace the continue loginFlow path that currently
causes a second sign-in prompt with an early exit path (e.g., return/close flow)
and ensure any cleanup runs; add a vitest regression in
codex-manager-cli.test.ts that simulates existing accounts + explicit
--manual/--device-auth cancel and asserts process exits with code 0 and that no
second sign-in prompt is shown (targeting the isOAuthCancellation/tokenResult
handling and existingCount behavior).
🤖 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 `@test/device-auth.test.ts`:
- Around line 676-688: The installSetTimeoutSpy currently only mocks
globalThis.setTimeout causing failures when lib/auth/device-auth.ts's abort path
(with keepAlive: true) calls clearTimeout; update installSetTimeoutSpy to also
mock globalThis.clearTimeout (e.g., provide a noop/mock implementation that
safely accepts the timer handle) so abort logic can call clearTimeout without
throwing or invoking the real timer cleanup; keep the setTimeout spy behavior
and ensure both mocks are restored by the test framework as needed.
- Around line 740-753: The test currently never triggers the AbortSignal; change
it to abort during polling so sleepWithAbort runs: use installSetTimeoutSpy()
and pollingFetchMock() that delays the polling response, start
runDeviceAuthFlow({ log: vi.fn(), signal: controller.signal, keepAlive: true }),
then trigger controller.abort() while the flow is waiting (e.g., after a
microtask/tick) and assert the flow rejects/returns an abort error and that
spy.unrefCount() is 0; reference the AbortController, runDeviceAuthFlow,
pollingFetchMock, installSetTimeoutSpy and verify the abort path clears timers
when keepAlive is true.
- Around line 717-728: The test currently only checks spy.unrefCount() and may
pass if timers were never scheduled; after calling runDeviceAuthFlow (which uses
pollingFetchMock and the spy from installSetTimeoutSpy), assert that the spy
actually recorded timer scheduling by adding an assertion such as
expect(spy.callCount()).toBeGreaterThan(0) (or the equivalent method provided by
installSetTimeoutSpy) immediately after the runDeviceAuthFlow result check to
ensure setTimeout was invoked before checking unrefCount().

---

Outside diff comments:
In `@lib/codex-manager.ts`:
- Around line 3138-3144: When isOAuthCancellation(tokenResult) is true and
existingCount > 0 inside the loginFlow loop, do not continue back into
authentication; instead break out/return so explicit transports
(manual/device-auth) exit immediately rather than re-entering loginFlow —
replace the continue loginFlow path that currently causes a second sign-in
prompt with an early exit path (e.g., return/close flow) and ensure any cleanup
runs; add a vitest regression in codex-manager-cli.test.ts that simulates
existing accounts + explicit --manual/--device-auth cancel and asserts process
exits with code 0 and that no second sign-in prompt is shown (targeting the
isOAuthCancellation/tokenResult handling and existingCount behavior).
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1ebd5c9e-64f5-4ed7-8268-5679b2227c5a

📥 Commits

Reviewing files that changed from the base of the PR and between 1671299 and 95de758.

📒 Files selected for processing (4)
  • lib/auth/device-auth.ts
  • lib/codex-manager.ts
  • test/codex-manager-cli.test.ts
  • test/device-auth.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Greptile Review
🧰 Additional context used
📓 Path-based instructions (2)
lib/**

⚙️ CodeRabbit configuration file

focus on auth rotation, windows filesystem IO, and concurrency. verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios. check for logging that leaks tokens or emails.

Files:

  • lib/auth/device-auth.ts
  • lib/codex-manager.ts
test/**

⚙️ CodeRabbit configuration file

tests must stay deterministic and use vitest. demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior. reject changes that mock real secrets or skip assertions.

Files:

  • test/device-auth.test.ts
  • test/codex-manager-cli.test.ts
🔇 Additional comments (4)
lib/auth/device-auth.ts (1)

54-59: keepalive gating is correctly scoped to cli callers.

lib/auth/device-auth.ts:54-59, lib/auth/device-auth.ts:80-83, and lib/auth/device-auth.ts:154-156 keep default unref behavior for library/background use while allowing foreground cli flows to stay alive. abort cleanup is still correct.

Also applies to: 80-83, 154-156

lib/codex-manager.ts (2)

3180-3188: good fix for explicit-mode add-another exit.

lib/codex-manager.ts:3180-3188 correctly avoids dropping back into a new login cycle when explicit transport flags were used.


2793-2797: --no-browser correctly maps to loginOptions.manual and feeds explicitSignInMode.

the concern in this review is unfounded. parseAuthLoginArgs in lib/codex-manager/help.ts:69-70 explicitly maps both --manual and --no-browser to options.manual = true. the condition at lib/codex-manager.ts:2797 (loginOptions.deviceAuth || loginOptions.manual) correctly captures this because --no-browser gets baked into loginOptions.manual. dashboard bypass triggers as documented.

test/codex-manager-cli.test.ts (1)

4869-5004: solid regression coverage for explicit login modes with existing accounts.

this is a good addition. test/codex-manager-cli.test.ts:4869 and test/codex-manager-cli.test.ts:4943 correctly pin the dashboard bypass behavior by asserting promptLoginModeMock is not called, and they verify the login path still persists a new account.

Comment thread test/device-auth.test.ts Outdated
Comment thread test/device-auth.test.ts
Comment thread test/device-auth.test.ts
ndycode added 2 commits May 10, 2026 22:33
- lib/codex-manager.ts: when an OAuth cancellation occurs in explicit
  transport mode (--device-auth/--manual/--no-browser), exit cleanly
  instead of falling back into loginFlow (which would re-enter the
  same transport that was just cancelled now that the dashboard is
  bypassed).
- test/device-auth.test.ts: extend installSetTimeoutSpy to also mock
  clearTimeout, expose setTimeoutCount and clearTimeoutCount, and add
  an autoFire option for tests that need a pending timer. Existing
  keepAlive tests now assert setTimeoutCount > 0 to guard against
  vacuous passes if the polling sleep is ever skipped. Replace the
  signal-but-no-abort test with a regression that actually aborts
  mid-poll and asserts the timer is cleared so the CLI can exit.
- test/codex-manager-cli.test.ts: regression test for explicit
  --device-auth + existing accounts + cancellation -> exitCode 0,
  no second sign-in prompt, dashboard not invoked.
When --device-auth/--manual/--no-browser pushed the pool to
ACCOUNT_LIMITS.MAX_ACCOUNTS, the inner add-account loop printed the cap
message and broke out, but the outer loginFlow loop then re-entered the
explicit-mode branch (dashboard bypassed) and silently started another
sign-in session despite the cap. Mirror the addAnother=false guard so
explicit modes return 0 once the cap is reached.

Tests: regression in codex-manager-cli.test.ts seeding 19 accounts and
asserting that --device-auth completes the 20th add, prints the cap
message, exits 0, and does not invoke promptAddAnotherAccount or call
fetch a second time.
@ndycode
ndycode merged commit 38ac285 into main May 10, 2026
2 checks passed
ndycode added a commit that referenced this pull request May 10, 2026
- lib/codex-manager.ts: when an OAuth cancellation occurs in explicit
  transport mode (--device-auth/--manual/--no-browser), exit cleanly
  instead of falling back into loginFlow (which would re-enter the
  same transport that was just cancelled now that the dashboard is
  bypassed).
- test/device-auth.test.ts: extend installSetTimeoutSpy to also mock
  clearTimeout, expose setTimeoutCount and clearTimeoutCount, and add
  an autoFire option for tests that need a pending timer. Existing
  keepAlive tests now assert setTimeoutCount > 0 to guard against
  vacuous passes if the polling sleep is ever skipped. Replace the
  signal-but-no-abort test with a regression that actually aborts
  mid-poll and asserts the timer is cleared so the CLI can exit.
- test/codex-manager-cli.test.ts: regression test for explicit
  --device-auth + existing accounts + cancellation -> exitCode 0,
  no second sign-in prompt, dashboard not invoked.
@ndycode
ndycode deleted the fix/issue-477-device-auth-keepalive branch May 10, 2026 15:15
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.

[bug] --device-auth

1 participant