Skip to content

Releases: ndycode/codex-multi-auth

v2.3.0-beta.3

11 Jun 08:17

Choose a tag to compare

v2.3.0-beta.3 Pre-release
Pre-release

Runtime Rotation

Bugfixes

  • Fixed stream forwarding stalling indefinitely for slow clients. forwardStreamingResponse now checks the return value of res.write() and awaits drain before reading the next upstream chunk, preventing unbounded in-process buffering when the client socket falls behind.

Improvements

  • Converted the two startup guards in startRuntimeRotationProxy from bare Error throws to CodexValidationError with machine-readable field/expected/context metadata. Error messages are byte-identical; callers can now branch on instanceof CodexValidationError and stable field names instead of message text (audit §4.3, #586).

Storage

Bugfixes

  • Fixed multi-tier account deduplication. deduplicateAccountsByIdentity now runs fixpoint iteration: a single pass was not enough when a newest-wins merge could install an account that itself duplicated an earlier survivor through a different identity tier (e.g. an email-tier merge installs an account whose accountId + refreshToken already matches an earlier entry). The wrapper now loops until the array is stable; every pass strictly shrinks it by at least one entry, so it terminates in at most accounts.length passes.
  • Added vi.restoreAllMocks() to storage.test.ts afterEach to prevent a failing test's leaked fs spy from cascading into every subsequent storage test in the same worker.

Improvements

  • Migrated the last two hand-rolled retry loops to the shared withRetry helper in lib/fs-retry.ts: the temp→final account-save rename (storage.ts) and the config env-path CAS loop (config.ts). Inter-attempt delay schedules are unchanged; only the wasted trailing sleep after a final failure is removed.
  • Converted savePluginConfig's "unreadable config file" abort from a bare Error to a typed StorageError with code: "UNREADABLE" and the file path. Callers can now branch on instanceof StorageError instead of message text (#588, audit §4.3).

Security

Improvements

  • All atomic write helpers now use crypto.randomBytes instead of Math.random() for staging-path nonces, preventing a local attacker from predicting the next staging path (#517).

Code Quality

Improvements

  • Removed 852 lines of dead code: seven orphaned modules with no live importers deleted (#554, #558).
  • Pruned unused exports and types flagged by knip; added knip.jsonc config for ongoing dead-code analysis (#555, #556, #557).
  • isRetryableStorageWriteError, copyDashboardSettingValue, mergeDashboardSettingsForKeys, and DEFAULT_STATUSLINE_FIELDS exported from their respective modules for direct test access.
  • Synced plugin manifest and AGENTS.md package-version claim to v2.3.0-beta.3.

Testing

Improvements

  • 20 new direct test suites covering: login-oauth, login-menu actions/flow/data, persist-selected, health-check, forecast-report-shared, settings write-queue, rotation selection/state/token-refresh, auth-menu builder, model-fallback property, write-queue property, rate-limit helpers, usage-ledger redaction, settings preview builders, and settings-hub shared helpers.
  • Property-based test suite for deduplicateAccountsByIdentity: covers order-independence and convergence across all permutations using fast-check.
  • shouldRetryFileOperation, fs-retry, and temp-path covered with new unit suites.

Notes

  • Prerelease published under the beta dist-tag (npm i -g codex-multi-auth@beta).
  • The #509 sequential drain-first feature and all fixes from 2.3.0-beta.2 are included.

v2.3.0-beta.2

10 Jun 17:43

Choose a tag to compare

v2.3.0-beta.2 Pre-release
Pre-release

Runtime Rotation

Bugfixes

  • Fixed the sequential drain-first pointer advancing during a within-request fallback in legacy routing mode. persistRuntimeActiveAccount called markSwitchedLocked unconditionally when routingMutex !== "enabled", including when schedulingStrategy: "sequential" was set; a transient token-bucket fallback to a secondary account would permanently move the primary and break the #509 drain-first invariant. The same schedulingStrategy !== "sequential" guard already present in the enabled-mutex branch now applies to the legacy branch too.
  • Fixed ensureFreshAccessToken forwarding an expired access token when commitRefreshedAuthOnce returned null (account not found in storage after persist). The fallback used updatedAccount.access, which was the original stale token, causing a downstream 401 and incorrectly triggering invalidation-cooldown logic. The function now uses refreshResult.access (the just-issued token) on the null-commit path while preserving the committed account's stored token on the success path (required for the dedup-commit case where two concurrent callers share one commit).

Storage

Bugfixes

  • Fixed normalizeFlaggedStorage silently dropping workspaces, currentWorkspaceIndex, accessToken, and expiresAt from flagged accounts. The function built a hardcoded field list that omitted these fields, defeating the Zod schema's .passthrough() intent; multi-workspace accounts permanently lost their workspace list and active-workspace index after a flag→restore round-trip. All four fields are now preserved.
  • Fixed refreshQuotaCacheForMenu wiping all other accounts' quota data when a transient disk failure occurred during cache reload. The rebase-merge block had a dead catch because loadQuotaCache() never throws — it returns empty maps on any read failure. When the persisted cache came back empty, only this run's probed entries were written back, discarding every other account's still-valid quota data. The empty-load case is now detected explicitly and falls back to the full in-memory snapshot.

Security

Hardening

  • All atomic write helpers (storage.ts, recovery/storage.ts, quota-cache.ts, unified-settings.ts, and twelve other call sites) now use crypto.randomBytes for staging-path nonces instead of Math.random(), preventing a local attacker who can observe one staged path from predicting the next one (#517).
  • All GitHub Actions workflow steps are pinned to exact commit SHAs rather than floating version tags, hardening against supply-chain tag drift (#519).
  • Upstream response headers matching x-codex-multi-auth-account-* are now blocked by prefix match rather than an allowlist; a future header added under that namespace is blocked by default rather than leaking until someone extends a list (#546).
  • Fixed withTimeout rejecting after calling onTimeout: cancelling a stream reader in onTimeout can settle the raced promise with a clean done: true, and a settlement enqueued ahead of the rejection would win the race, turning a stall into a silent success. Reject is now issued before onTimeout (#546).

Refactoring

Improvements

  • Decomposed lib/codex-manager.ts across four phases: login control loop, OAuth machinery, command registry, and formatter modules extracted into lib/codex-manager/ submodules. The file shrinks from 2,266 lines to 690 lines (-82%) (#525, #535, #540, #547).
  • Decomposed lib/runtime-rotation-proxy.ts across two phases: rate-limit decision logic, stream-failover runtime, and rotation-proxy closure state extracted into lib/request/ and lib/runtime/ submodules (#532, #548).
  • Eliminated all detected import cycles; eslint-plugin-import-x no-cycle rule is now enforced in CI so regressions are caught at commit time (#541).
  • Replaced local isRecord guards in runtime-current-account.ts, codex-manager/commands/rotation.ts, and refresh-lease.ts with the canonical lib/utils.ts version. The local copies accepted arrays (typeof v === "object" && v !== null); the canonical version correctly rejects them (#544, #545).

CLI & Tests

Improvements

  • Added shared cli-test-fixtures mock factory infrastructure; all manager test suites now use a consistent set of factories, removing per-suite boilerplate (#537, #539, #550).
  • Pinned the machine-readable JSON output contract (status, report, doctor, forecast, why-selected) with snapshot tests that catch shape regressions (#533).

Build & Packaging

Improvements

  • Stripped JS source maps from the published package; declaration maps are kept for go-to-definition. Reduces published size (#527).
  • Pinned @types/node to the supported runtime floor major to prevent silent type drift from newer Node API additions (#528).
  • Raised engines.node to >=18.17.0, reflecting the actual tested minimum and aligning with undici@6 and the node18-smoke CI job (#518).
  • config/schema/config.schema.json is now generated from the Zod schema with a byte-exact drift-guard test; the schema and its source of truth can never silently diverge (#536).

Notes

  • Prerelease published under the beta dist-tag (npm i -g codex-multi-auth@beta).
  • The #509 sequential drain-first feature from 2.3.0-beta.0 is unchanged; the pointer-corruption bug above is now fixed.

v2.3.0-beta.1

07 Jun 09:29
d8306d5

Choose a tag to compare

v2.3.0-beta.1 Pre-release
Pre-release

Account Login

Bugfixes

  • Stopped codex-multi-auth login from always printing Added account when a
    same-email / different-workspace login folded onto an existing saved entry.
    The CLI login path used local copies of resolveAccountSelection and
    persistAccountPool in lib/codex-manager.ts that had drifted from the
    workspace-aware versions in lib/runtime/ (added for #491 and used only by
    the runtime proxy); the CLI copies never persisted workspaces and
    unconditionally reported Added account. The login flow now reports the real
    outcome — Added account, Updated existing account, or
    Rebound workspace for existing account — based on whether the write
    inserted a new entry, refreshed an existing one, or surfaced a
    previously-untracked workspace (issue #512).
  • Persisted token-derived workspaces on the saved account so
    codex-multi-auth workspace <account> is usable after a same-email
    multi-workspace login. Rows no longer save with workspaces: null;
    per-workspace enabled/disabledAt state is preserved across re-logins, and
    the explicit login --org <id> binding now tracks workspaces too (previously
    the override path returned before workspace discovery).
  • Classified the first workspace-aware re-login of a pre-#491 account (one with
    no tracked workspaces yet) as Updated existing account rather than
    Rebound workspace, so quiet first-time enrichment is not mislabeled as a
    rebind.

Manual sign-in

  • Surfaced real validation errors from codex-multi-auth login --manual
    instead of reporting every failure as Cancelled.. The manual callback
    reader returned null for a genuine user cancel, a callback URL missing the
    code/state parameter, and an OAuth state mismatch alike, and the caller
    treated all three as a cancellation. A new pure classifier
    (classifyManualCallbackInput) distinguishes code / cancelled /
    invalid / state-mismatch; invalid and state-mismatch now exit non-zero
    with a specific, actionable message (callbackInvalid /
    callbackStateMismatch), while a genuine cancellation is unchanged
    (issue #512 follow-up).

Release Hygiene

Tests

  • Extracted the account-pool fold (dedup → insert/update/rebound →
    workspace-tracking → active-index) into pure helpers
    (applyAccountPoolResults, buildInsertedAccount, buildUpdatedAccount,
    mergeAccountWorkspaces, resolveCurrentWorkspaceIndex) and covered them with
    unit tests, including an end-to-end reproduction of the same-email
    multi-workspace scenario driven through the real findMatchingAccountIndex
    dedup strategy.
  • Added CLI regressions: login --org <id> persists workspace tracking, a
    mismatched manual-callback state exits non-zero with the state-mismatch
    message, and a malformed manual-callback URL exits non-zero with the
    callbackInvalid message — none of which persist an account.
  • Full classifier coverage for the manual-callback contract, including the
    reporter's "pasted a localhost callback URL but still saw Cancelled" case.

Notes

  • Prerelease published under the beta dist-tag
    (npm i -g codex-multi-auth@beta). This is a bugfix beta on the 2.3.0 line;
    the #509 sequential drain-first feature from 2.3.0-beta.0 is unchanged.

v2.3.0-beta.0

04 Jun 13:06

Choose a tag to compare

v2.3.0-beta.0 Pre-release
Pre-release

Runtime Rotation

Features

  • Sequential / drain-first account scheduling (opt-in). A new
    schedulingStrategy setting (CODEX_AUTH_SCHEDULING_STRATEGY /
    schedulingStrategy, default hybrid) adds a sequential mode that drains one
    account fully before moving to the next, instead of spreading load across the
    pool. Set it to sequential to keep every new request on the current active
    account until that account is exhausted (rate-limited, cooling down, circuit-open,
    or disabled), then advance to the next usable account. When an earlier account's
    quota window recovers it reclaims the active slot on the next forward scan, so the
    pool's quota windows stagger over time rather than resetting together — longer
    uninterrupted sessions across a multi-account pool (issue #509).
  • Default behavior is unchanged. hybrid remains the default and keeps the
    existing weighted health/token/freshness selection plus per-session affinity.
    sequential is fully opt-in; pools that do not set it behave exactly as before.
  • Manual pin still wins. A switch <n> pin overrides scheduling in both modes.
    In sequential mode per-session affinity is intentionally bypassed — every
    request follows the single active account, not a per-chat sticky account.

Release Hygiene

Tests

  • Selector coverage for the drain-first path: sticky-while-usable, advance-on-
    exhaustion, wrap-to-recovered-earlier-account, returns-null when the whole pool is
    exhausted, cooldown/circuit-open/disabled failover, per-family cursor isolation,
    and the policy-blocked-anchor guard.
  • Proxy-level coverage: affinity is ignored, manual pin takes precedence, the active
    pointer advances only on true exhaustion (not on a transient attempted-this-request
    skip), and the mode survives the routing-mutex select+commit path without double-
    advancing the cursor.
  • Config coverage for schedulingStrategy: default, explicit value, env override in
    both directions, and invalid env/persisted values falling back safely.

Notes

  • Prerelease published under the beta dist-tag
    (npm i -g codex-multi-auth@beta). Whether drain-first delivers longer
    uninterrupted sessions depends on each pool's real quota-window timing, which is
    why this ships as a beta for validation on real accounts before a stable cut.

v2.2.2

03 Jun 16:14

Choose a tag to compare

Runtime Rotation

Bugfixes

  • Cleared an account's persisted runtime skip reason on its next successful request, so a reason with no time-based expiry (notably token-exhausted) no longer lingers in accountSkipReasons and keeps the forecast reporting a working account as unavailable.
  • Added recordRuntimeAccountRecovery(index) on the proxy success path: it removes the account's entry from accountSkipReasons and lastPoolExhaustionSkipReasons, is a no-op when nothing is recorded, and ignores non-integer or negative indices.

Quota & Forecast

Bugfixes

  • Stopped forecast --live marking working accounts as unavailable from a stale runtime overlay that persisted a skip reason after its window expired but was never cleared on a subsequent successful request.
  • Ignored stale time-bounded overlay reasons by cross-referencing disk state: rate-limited when getRateLimitResetTimeForFamily finds no active reset (including model-scoped keys like codex:5h), and cooling-down:... when coolingDownUntil is absent or elapsed.
  • Left non-time-bounded reasons (circuit-open, token-exhausted, policy-blocked) applied as-is by the forecast; these are cleared at the source on a successful request instead (see Runtime Rotation).
  • Aligned doctor's forecast-runtime-alignment check, which shares the forecast evaluation, so the same stale state no longer raises a spurious warning.

v2.2.1

03 Jun 12:36

Choose a tag to compare

Launcher

Bugfixes

  • Rewrote mcodex as a Node bin (scripts/mcodex.js); the old bash script shipped as a Windows bin died with HCS_E_SERVICE_NOT_AVAILABLE when a WSL stub shadowed git-bash. Zero bash dependency on the default path; tmux/watch invoked as argv arrays; graceful degrade when absent.
  • Canonicalized the direct-run gate (realpath) so the launcher runs through an npm-created symlink bin.
  • Relayed SIGTERM/SIGINT to the spawned child so it is never orphaned.

Auth

Bugfixes

  • Isolated OAuth concurrent-login state in per-call closures instead of the shared http.Server instance, so parallel logins can't cross-bind callback code/state.
  • Serialized the local-client-token store's read-modify-write through its write queue and widened the rename retry set to EBUSY/EPERM/EAGAIN/ENOTEMPTY/EACCES; debounced lastUsedAt writes on the bearer-verify hot path.

Runtime Rotation

Bugfixes

  • Closed the routingMutex="enabled" selection race: selection and cursor commit now run in one reentrant mutex acquisition. Legacy mode unchanged.
  • Bucketed a model-less /codex/responses request into the codex family (CURRENT_CODEX_MODEL) instead of the general gpt-5.5 family, keeping rotation/cooldown/budget accounting correct.
  • Stripped inbound cookie / proxy-authorization on both egress paths; short-circuited per-request storage re-reads on unchanged mtime/size; check auth before path/method (401 before 404).

Storage

Bugfixes

  • Fixed a storage-transaction deadlock: flagged-storage recovery (backup restore + legacy-file migration) inside a held lock re-acquired the global mutex and wedged all later saves. Lock ownership is now tracked so recovery persists without re-locking.
  • Preserved pinnedAccountIndex/affinityGeneration through the combined transaction clone so a doctor restore no longer erases the manual pin.
  • Serialized the env-path config save under a cross-process file lock (owner token + compare-before-unlink) plus retried stat and mtime compare-and-swap.
  • Created secret directories 0o700; floored fractional indices and coerced NaN in clampIndex.

Quota & Forecast

Improvements

  • gpt-5.5 is now the default live/quota probe model, legacy fallback chain (gpt-5.4gpt-5.3-codexgpt-5.2-codexgpt-5-codex) preserved in one shared QUOTA_PROBE_MODEL_CHAIN.
  • Codex-unavailable accounts are labeled "signed in" not "working"; live check gained Codex available / signed in only / need re-login counters (! instead of ).

Bugfixes

  • Classified the normalized "model not currently available for this ChatGPT account" wording as an entitlement block across the probe/forecast/report/check surfaces.
  • Excluded policy-blocked and token-exhausted accounts from forecast recommendations.
  • Fixed status-tone precedence so a failed live check carrying a quota percentage renders red, not green.

CLI

Bugfixes

  • Hardened --model parsing on every parser (best, forecast, report, fix, integrations, models): a flag-like or whitespace-only value after --model/-m/--model= is rejected, not consumed.
  • Rejected non-integer workspace/switch indices instead of truncating; rejected a flag-like budget check key.
  • Shipped .codex-plugin/plugin.json in the package, enforced by the pack-budget check via an exact-file requirement.
  • Read the capability matrix under the entitlement key (matching the write path); clamped out-of-range quota percentages; made capability-policy eviction LRU.

v2.2.0

02 Jun 13:09
29c8b4f

Choose a tag to compare

Launcher

Improvements

  • New mcodex launcher (#500): launches Codex with a cached status line (model, reasoning effort, cwd, active account, quota usage, plan, cache age) printed before startup. mcodex --tmux runs inside tmux with mouse scrollback; --tmux --live-accounts adds a live codex-multi-auth list pane; --monitor is monitor-only.
  • The status line reads local cache/observability/account state and never calls OpenAI on launch; refreshes quota in the background only when stale (default 10 min, CODEX_MULTI_AUTH_STATUS_QUOTA_REFRESH_INTERVAL_MS), behind a lock. Resolves the per-project pool when perProjectAccounts is on and Codex CLI sync is off. Toggle with CODEX_MULTI_AUTH_STATUSLINE=0.

Hardening

  • Validated MCODEX_MONITOR_INTERVAL / MCODEX_TMUX_HISTORY_LIMIT as numeric before interpolating into watch/tmux commands (no shell injection).
  • --monitor / --live-accounts fail fast when watch is missing instead of spawning a broken pane; status path resolves ~ correctly on Windows and never blocks the event loop.

Runtime Rotation

Bugfixes

  • Bound the rotation proxy and local bridge loopback-only with no opt-out, and stopped forwarding inbound credentials (authorization, x-api-key, cookie, proxy-authorization) upstream; IPv6 loopback normalized for bind + base URL (#499).
  • Stripped inbound cookie / proxy-authorization on both egress paths and bounded the proxy's upstream error-body read (#503).

Storage

Bugfixes

  • SHA-256-verified cached Codex instructions: a tampered or unverified-legacy cache is never fast-path served and never drives a conditional 304 (#499).
  • Validated stored ids before building filesystem paths, quarantining unsafe ids (../poison) or non-numeric time.created; rejected NUL-byte paths in resolvePath (#499, #503).
  • Made the account store atomic + self-healing (checksummed WAL + temp-and-rename) and retried the transient-lock taxonomy (EBUSY/EPERM/ENOTEMPTY/EACCES/EAGAIN) across all writes (#499).
  • Persisted runtime-observability.json owner-only (0o600 / dir 0o700) on POSIX; removed Atomics.wait sleeps from the config-load and logger retry paths (#499, #503).

Quota & Forecast

Bugfixes

  • Detected an unsupported Codex model from the upstream error detail shape (not just the nested error envelope), surfacing a friendly "Codex unavailable" note across best/forecast/report/live-check instead of leaking raw upstream text (#501/#502).
  • Kept a genuine transient failure surfacing as the real error, not masked behind the friendly note (#501/#502).
  • Bounded prompt and release-metadata fetches by connect+body timeouts that cancel a stalled body (#499).

CLI

Improvements

  • status / list gained --json with a stable shape whether or not accounts are configured (#499).

Bugfixes

  • Required a strict integer for switch <index> (no silent float truncation) (#503).
  • Masked tokens/emails across log, debug-bundle, and status sinks; the debug bundle redacts the home prefix (Windows case/separator-insensitive), strips config credentials, and masks the account id (#499).

Dependencies

Bugfixes

v2.1.13-beta.1

31 May 02:35
33e4b22

Choose a tag to compare

v2.1.13-beta.1 Pre-release
Pre-release

Accounts

Bugfixes

  • Account workspaces and currentWorkspaceIndex now survive a load
    round-trip. The strict V3 storage schema (AccountMetadataV3Schema) did not
    declare these fields, so Zod stripped them on every read; login captured the
    workspaces and wrote them to disk, but the next load wiped them. This is the
    root cause of workspace labels appearing empty after login and of two
    same-email accounts being indistinguishable.
  • formatAccountLabel surfaces the active workspace name, so same-email accounts
    in different workspaces stay distinguishable, for example
    Account 1 ([Personal Plus], user@gmail.com, id:g-AAAA). All prior label
    formats are preserved when no workspace is tracked.

Features

  • status / list now list every workspace beneath an account that tracks more
    than one, marking the active workspace and flagging disabled ones.
  • New workspace <account> [workspace] command: with only an account index it
    lists that account's workspaces; with a workspace index it sets the active
    workspace and persists it.
  • login --org <org_id> (and --org=<id>) binds a login to a specific
    workspace/org. It reuses the CODEX_AUTH_ACCOUNT_ID override that every login
    resolver already honors, scoped to the invocation and restored afterward, so a
    second workspace can be registered on demand instead of always resolving to the
    default org.

Release Hygiene

Tests

  • Schema round-trip preservation and a workspace-load regression that locks the
    fix in place.
  • Label disambiguation coverage and formatWorkspaceLines output coverage
    (active marker, disabled annotation, indentation).
  • Nine workspace-command cases (list, switch, persistence, already-active
    no-op, disabled rejection, out-of-range and non-numeric indices).
  • login --org argument parsing and missing-value handling.

Notes

  • The issue #486 503 root cause (doctor reports all green, runtime still returns 503) is not fixed by this prerelease.
  • Whether a real two-workspace login returns both orgs in one token or one at a time is unconfirmed. Both paths are supported (the workspace command for the former, login --org for the latter); a sanitized accounts.json from a real two-workspace account would confirm which path users hit.

v2.1.12

25 May 14:47

Choose a tag to compare

Runtime Rotation

Bugfixes

  • Mirrored trusted [hooks.state.*] TOML blocks from the original config.toml into runtime shadow configs when the hook key points at the user's hooks.json.
  • Preserved CRLF line endings and full hook-state table bodies while rewriting hook keys to the shadow hooks.json path.
  • Kept unrelated hook-state entries, alternate hook files, and existing shadow entries unchanged.

Release Hygiene

Tests

  • Added regression coverage for literal TOML keys, CRLF preservation, table-like content inside strings and arrays, alternate hook paths, long hook keys, shadow hooks.json visibility, and concurrent runtime shadow launches.
  • Linked duplicated TOML block scanner helpers in production and test code to reduce future drift.

v2.1.10

11 May 07:39

Choose a tag to compare

Runtime Rotation

Bugfixes

  • Runtime pool exhaustion on no-account now resets volatile runtime trackers, reloads accounts from disk once, and retries selection even when stale in-memory circuit breakers are open. This prevents a long-lived proxy from staying wedged after disk state has recovered.
  • Runtime proxy shutdown now flushes only the active account manager after a stale-state reload, avoiding stale-vs-fresh manager save races during close.
  • Pinned-account hard-fail behavior remains preserved: unavailable pinned accounts still return the explicit pinned-account error instead of silently rotating.

Diagnostics

Improvements

  • Pool-exhausted 503 responses include per-account runtime skip reasons and a targeted recovery hint.
  • report and doctor now include persisted quota-cache inputs when computing forecast readiness, so quota-exhausted cache state cannot be reported as ready by those diagnostics.
  • doctor keeps the forecast-runtime-alignment warning for disk/runtime divergence.
  • rotation reset-runtime --json no longer clears runtime diagnostics if the app bind restart fails, preserving evidence needed for follow-up diagnosis.

Release Hygiene

Improvements

  • The npm package now includes only runtime/install scripts instead of internal benchmark, audit, and maintenance scripts.
  • Production dependency patch lines were refreshed (undici and zod) while keeping the published package on the current Node 18-compatible runtime line.
  • Release index labels were corrected so older release-note rows are not shown as current stable.