v6.16.0
https://www.npmjs.com/package/oc-codex-multi-auth/v/6.16.0
npx oc-codex-multi-auth@6.16.0 installAdded
A second browser login method, and a default browser login that no longer gives up (#244). opencode auth login now offers four OAuth methods:
Codex OAuth (ChatGPT Plus/Pro)— opens your default browserCodex OAuth (Open URL Manually)— new; binds the listener first, prints the URL, launches nothingCodex OAuth (Device Code)Codex OAuth (Manual URL Paste)
The new method suits SSH with -L 1455:localhost:1455: it prints the authorization URL once port 1455 is listening, you open it in any browser, and sign-in completes through the callback.
The default method changed too. When the browser cannot be launched, the login is no longer cancelled: the URL is printed and the listener keeps waiting, so opening that URL by hand still finishes the login. Before, a failed launch closed the listener and returned a failure, and the URL was never printed. On a Linux or WSL host with no xdg-open on PATH that was every login.
The ChatGPT plan tier is reported per account (#243). Free, Plus, Pro, Business, Business Premium, shown by codex-list and codex-status:
# Label Plan Status
--- ------------------------- ------------------ --------------------
1 Account 1 (us***@…) Pro active
It is read from the access token at login and re-read on every token refresh, so a Plus → Pro upgrade shows up without re-authenticating. codex-limits and the TUI quota pane read the plan live from the usage endpoint and now name it the same way, so one seat no longer reads Business on one surface and team on another. An unrecognized plan is reported as it arrived rather than renamed.
Fixed
A replaced account manager could delete accounts (#242). Disposing an AccountManager left its queued debounced save armed, and that save takes account membership from the dead manager's snapshot wholesale, so it deleted every account the successor had since loaded or added.
Cancelling the timer was not the answer either. The only save a cancel can still reach is one armed after the caller's flush, and that is the rate-limit block an in-flight request just recorded; dropping it hands an exhausted account straight back to rotation for another 429. A disposed manager now writes through a volatile-only merge: membership, credentials and active-index routing come from disk, and only rate-limit blocks, cooldowns and last-used stamps merge in. That also covers a save already in flight when disposal ran, which no clearTimeout could reach.
A quota alert could describe a quota no account had (#241). The 5-hour and weekly lines took the highest remaining percentage and the earliest reset from different accounts. With 60% left on one account resetting at 02:00 and an exhausted account resetting at 22:30, the line read:
5h: 60% | resets 22:30
and the reader waited for relief belonging to the exhausted account. The percentage and the reset printed beside it come from one account again. When a different account recovers sooner, that reset is appended under its own label instead of replacing the paired one:
5h: 10% | resets 02:00 | another account resets 22:30
A generated account label leaked the email past maskEmail (#243). Accounts were labelled <email> id:<suffix>, and every display surface prints the label verbatim while masking only the email field. With masking on, a line read:
Account 1 (user@example.com id:c487c4, us***@example.com, id:c487c4)
No label is generated from a ChatGPT credential now. The email and account id are stored as their own fields and rendered from there, so masking applies and each identity appears once. A login clears a stale label left behind by an API-platform organization name; a label you set with codex-label is kept.
Quarantining an account dropped its plan (#243). The flagged-account normalizer rebuilds records field by field and did not carry planType, so an account restored from quarantine reported an unknown plan until the next login.
A bare authorization code was accepted with no state check (#244). The manual URL paste flow stopped comparing the OAuth state when the input classified as a raw code, which removed the only binding the plugin performs between the pasted value and the login attempt in progress. The full callback URL, including its state, is required again for every input.
noBrowser was ignored (#244). A caller passing noBrowser=true or no-browser=true entered the multi-account browser loop, tried to launch a browser, and bound port 1455 — the opposite of what the input asks for on a headless host. It selects the paste flow again.
A hostile plan_type could inject lines into terminal output. An unrecognized plan slug was rendered as it arrived, control characters included, and plan_type is read off /wham/usage, which is cast without validation and which OPENAI_BASE_URL lets a user put an arbitrary gateway in front of. A value of pro\nPlan: Enterprise split a codex-list row in two and the smuggled half read as a line the tool had emitted; an ESC byte reached the same place. Control characters are dropped and the rendered plan is bounded to 32 characters, which alters no real plan name.
Internal
Two test suites bind the real OAuth callback port 1455, and Vitest schedules files in parallel, so they raced and whichever lost failed an assertion the other file's existence had broken. They take a cross-worker lock now (#244).