fix(auth): make --device-auth login work end-to-end - #478
Conversation
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.
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
📝 WalkthroughWalkthroughThe PR fixes a process-exit issue when using ChangesDevice Auth Timer Lifecycle & CLI Flow
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested labels
review notes
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. 📋 Issue PlannerBuilt with CodeRabbit's Coding Plans for faster development and fewer bugs. View plan used: ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
✨ Simplify code
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. Comment |
There was a problem hiding this comment.
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 winexplicit transport cancel currently loops into a fresh sign-in attempt.
with dashboard bypass in
lib/codex-manager.ts:2800-2804, thiscontinue loginFlowatlib/codex-manager.ts:3138-3144re-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:1for: existing accounts + explicit--manual/--device-auth+ cancel => exits with0and 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
📒 Files selected for processing (4)
lib/auth/device-auth.tslib/codex-manager.tstest/codex-manager-cli.test.tstest/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.tslib/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.tstest/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, andlib/auth/device-auth.ts:154-156keep 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-3188correctly avoids dropping back into a new login cycle when explicit transport flags were used.
2793-2797:--no-browsercorrectly maps tologinOptions.manualand feedsexplicitSignInMode.the concern in this review is unfounded.
parseAuthLoginArgsinlib/codex-manager/help.ts:69-70explicitly maps both--manualand--no-browsertooptions.manual = true. the condition atlib/codex-manager.ts:2797(loginOptions.deviceAuth || loginOptions.manual) correctly captures this because--no-browsergets baked intologinOptions.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:4869andtest/codex-manager-cli.test.ts:4943correctly pin the dashboard bypass behavior by assertingpromptLoginModeMockis not called, and they verify the login path still persists a new account.
- 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.
- 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.
Summary
--device-authcrash from "Detected unsettled top-level await" by adding akeepAliveoption to the device-auth flow that prevents polling timers from being unref'd in CLI context.--device-auth,--manual,--no-browser) is provided, so the requested mode runs even with existing accounts.Closes #477
Files Changed
lib/auth/device-auth.tsadds opt-inkeepAliveplumbing ingetSleepandsleepWithAbort. Default behavior preserved for library consumers.lib/codex-manager.tspasseskeepAlive: truefrom the single CLI call site, gates the dashboard withexplicitSignInMode, and returns 0 from the add-another decline branch when in explicit mode.test/device-auth.test.tsadds 3 unit tests for thekeepAliveplumbing.test/codex-manager-cli.test.tsadds 2 integration tests for dashboard bypass with existing accounts (one for--device-auth, one for--manual).Risk Notes
keepAlivedefaults to false, so existing library consumers ofrunDeviceAuthFlowkeep the previous unref'd-timer behavior.cx auth loginwith existing accounts still opens the dashboard.promptOAuthSignInModewere audited: none reachrunDeviceAuthFlowsokeepAliveexposure is contained to the documented CLI entry point.keepAlive: trueand the signal aborts,clearTimeoutruns before reject, so the ref is released and the process can exit.Test plan
npm run typecheckcleannpm test3979/3979 passing across 267 files in 112scodex-multi-auth login --device-authprints device code, stays alive past the previous crash point, completes auth, and exits cleanly afternto "Add another?"cx auth login --device-authbypasses the dashboard and runs device-auth directlynote: 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-authlogin end-to-end by adding akeepAliveoption 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.tsaddskeepAlive?: booleantoDeviceAuthFlowOptions; bothgetSleepandsleepWithAbortgateunrefTimerbehind!options.keepAlive, with the abort path correctly callingclearTimeoutbefore rejecting to release the handle.lib/codex-manager.tssetskeepAlive: trueat the single cli call site, derivesexplicitSignInModefromloginOptions.deviceAuth || loginOptions.manual(which correctly covers--no-browsersince that flag maps tomanual: trueinparseAuthLoginArgs), and guards all three inner-loop exit edges withif (explicitSignInMode) return 0— including the previously unguardedMAX_ACCOUNTSbreak.--device-authand--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
explicitSignInModeguard is derived correctly from parsed options (covering--no-browservialoginOptions.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
keepAliveoption toDeviceAuthFlowOptions; bothgetSleepandsleepWithAbortcorrectly gateunrefTimerbehind!options.keepAlive; abort path clears the timer before rejecting, so no dangling handle when aborting with keepAlive enabled.keepAlive: trueat the single device-auth cli call site; derivesexplicitSignInModecorrectly (covers--no-browservialoginOptions.manual); all three inner-loop early-exit edges now haveif (explicitSignInMode) return 0guards, including the previously unguarded MAX_ACCOUNTS break.--device-authand--manualwith 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 --> GComments Outside Diff (1)
lib/codex-manager.ts, line 3179-3184 (link)breakre-enters device-auth in explicit mode — the barebreakexits only the inner sign-inwhile(true)loop; the outerloginFlow: while (true)then restarts, skips the dashboard becauseexplicitSignInModeis stilltrue, and immediately kicks off another device-auth session without user interaction. the samereturn 0guard applied at theaddAnother = falsebranch three lines below should also be applied here:if (explicitSignInMode) { return 0; }before thebreak.Prompt To Fix With AI
Reviews (3): Last reviewed commit: "fix(auth): exit cleanly at MAX_ACCOUNTS ..." | Re-trigger Greptile