oc-codex-multi-auth 6.3.1 → 6.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAKH,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,qBAAqB,CAAC;AAsM/D;;;;;;;;;;;;;;;GAeG;AAEH,eAAO,MAAM,iBAAiB,EAAE,MAojH/B,CAAC;AAEF,eAAO,MAAM,gBAAgB,QAAoB,CAAC;AAElD,eAAe,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAKH,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,qBAAqB,CAAC;AAwM/D;;;;;;;;;;;;;;;GAeG;AAEH,eAAO,MAAM,iBAAiB,EAAE,MAyqH/B,CAAC;AAEF,eAAO,MAAM,gBAAgB,QAAoB,CAAC;AAElD,eAAe,iBAAiB,CAAC"}
package/dist/index.js CHANGED
@@ -41,8 +41,8 @@ import { AccountManager, extractAccountEmail, extractAccountId, formatAccountLab
41
41
  import { resolveDisplayEmail } from "./lib/account-display.js";
42
42
  import { CodexAuthError } from "./lib/errors.js";
43
43
  import { getStoragePath, loadAccounts, saveAccounts, withAccountStorageTransaction, clearAccounts, setStoragePath, loadFlaggedAccounts, saveFlaggedAccounts, withFlaggedAccountStorageTransaction, clearFlaggedAccounts, getWorkspaceIdentityKey, StorageError, formatStorageErrorHint, } from "./lib/storage.js";
44
- import { createCodexHeaders, extractRequestUrl, handleErrorResponse, handleSuccessResponse, isDeactivatedWorkspaceError, getUnsupportedCodexModelInfo, resolveUnsupportedCodexFallbackModel, refreshAndUpdateToken, rewriteUrlForCodex, shouldRefreshToken, transformRequestForCodex, } from "./lib/request/fetch-helpers.js";
45
- import { createDeactivatedWorkspaceError, DEACTIVATED_WORKSPACE_ERROR_CODE, isDeactivatedWorkspaceErrorMessage, } from "./lib/error-sentinels.js";
44
+ import { createCodexHeaders, extractRequestUrl, handleErrorResponse, handleSuccessResponse, isDeactivatedWorkspaceError, isInvalidatedAuthTokenError, getUnsupportedCodexModelInfo, resolveUnsupportedCodexFallbackModel, refreshAndUpdateToken, rewriteUrlForCodex, shouldRefreshToken, transformRequestForCodex, } from "./lib/request/fetch-helpers.js";
45
+ import { createDeactivatedWorkspaceError, DEACTIVATED_WORKSPACE_ERROR_CODE, isDeactivatedWorkspaceErrorMessage, isInvalidatedAuthTokenMessage, } from "./lib/error-sentinels.js";
46
46
  import { applyFastSessionDefaults, upsertBackendModelIdentityMessage, } from "./lib/request/request-transformer.js";
47
47
  import { getRateLimitBackoff, RATE_LIMIT_SHORT_RETRY_THRESHOLD_MS, resetRateLimitBackoff, } from "./lib/request/rate-limit-backoff.js";
48
48
  import { isEmptyResponse } from "./lib/request/response-handler.js";
@@ -1861,6 +1861,63 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1861
1861
  }
1862
1862
  break;
1863
1863
  }
1864
+ // A 401 token-invalidated response means the access token presented for
1865
+ // THIS account was rejected by the backend even though the proactive
1866
+ // refresh above either ran or judged the token still fresh. Without an
1867
+ // explicit handler the 401 fell straight through to `return errorResponse`
1868
+ // below, so persisted family routing kept pinning every request to the dead
1869
+ // account slot instead of failing over (issue #171). Treat it as an
1870
+ // account-health failure: cool the refresh-token group down (or remove it
1871
+ // past the failure threshold) and rotate to the next healthy account.
1872
+ //
1873
+ // Note: 401s intentionally do NOT feed the circuit breaker — the breaker
1874
+ // guards against upstream faults (network / 5xx), not client-side auth
1875
+ // decisions (see the 5xx handler above).
1876
+ if (isInvalidatedAuthTokenError(errorBody, response.status)) {
1877
+ const accountLabel = formatAccountLabel(account, account.index, {
1878
+ maskEmail: maskEmailEnabled,
1879
+ });
1880
+ accountManager.refundToken(account, modelFamily, model);
1881
+ accountManager.recordFailure(account, modelFamily, model);
1882
+ account.lastSwitchReason = "rotation";
1883
+ runtimeMetrics.failedRequests++;
1884
+ runtimeMetrics.accountRotations++;
1885
+ runtimeMetrics.lastError = `Auth token invalidated on ${accountLabel}`;
1886
+ runtimeMetrics.lastErrorCategory = "auth-invalidated";
1887
+ const failures = await accountManager.incrementAuthFailures(account);
1888
+ if (failures >= ACCOUNT_LIMITS.MAX_AUTH_FAILURES_BEFORE_REMOVAL) {
1889
+ const removedCount = accountManager.removeAccountsWithSameRefreshToken(account);
1890
+ if (removedCount > 0) {
1891
+ accountManager.saveToDiskDebounced();
1892
+ await showToast(removedCount > 1
1893
+ ? `Removed ${removedCount} accounts (same refresh token) after ${failures} auth-token failures. Run 'opencode auth login' to re-add.`
1894
+ : `Removed ${accountLabel} after ${failures} auth-token failures. Run 'opencode auth login' to re-add.`, "error", { duration: toastDurationMs * 2 });
1895
+ // Indices shift after removal; restart traversal with a fresh
1896
+ // attempted set so no healthy account is skipped.
1897
+ attempted.clear();
1898
+ accountCount = accountManager.getAccountCount();
1899
+ break;
1900
+ }
1901
+ logWarn(`[${PLUGIN_NAME}] Expected grouped account removal after auth-token invalidation, but removed ${removedCount}.`);
1902
+ }
1903
+ // Below the removal threshold (or grouped removal was a no-op): cool the
1904
+ // account's whole refresh-token group down so selection skips it, then
1905
+ // rotate to the next healthy account instead of returning the 401.
1906
+ const cooledCount = accountManager.markAccountsWithRefreshTokenCoolingDown(account.refreshToken, ACCOUNT_LIMITS.AUTH_FAILURE_COOLDOWN_MS, "auth-failure");
1907
+ if (cooledCount <= 0) {
1908
+ accountManager.markAccountCoolingDown(account, ACCOUNT_LIMITS.AUTH_FAILURE_COOLDOWN_MS, "auth-failure");
1909
+ }
1910
+ accountManager.saveToDiskDebounced();
1911
+ logWarn(accountCount > 1
1912
+ ? `Auth token invalidated for account ${account.index + 1}. Cooling down and rotating to next account.`
1913
+ : `Auth token invalidated for account ${account.index + 1}. Cooling down; no other account available.`);
1914
+ if (accountCount > 1 &&
1915
+ accountManager.shouldShowAccountToast(account.index, rateLimitToastDebounceMs)) {
1916
+ await showToast(`Account ${account.index + 1} sign-in expired. Switching accounts.`, "warning", { duration: toastDurationMs });
1917
+ accountManager.markToastShown(account.index);
1918
+ }
1919
+ break;
1920
+ }
1864
1921
  runtimeMetrics.failedRequests++;
1865
1922
  runtimeMetrics.lastError = `HTTP ${response.status}`;
1866
1923
  runtimeMetrics.lastErrorCategory = "http";
@@ -1927,6 +1984,12 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1927
1984
  }
1928
1985
  }
1929
1986
  accountManager.recordSuccess(account, modelFamily, model);
1987
+ // A successful request proves the account's credentials are good
1988
+ // again, so reset the auth-failure counter that a prior 401
1989
+ // token-invalidated response (or refresh failure) may have bumped.
1990
+ // Otherwise stale counts could accumulate across requests and
1991
+ // eventually remove a now-healthy account.
1992
+ accountManager.clearAuthFailures(account);
1930
1993
  // RC-8: closes a half-open gate or prunes the failure window so a
1931
1994
  // sequence of successes keeps the breaker healthy.
1932
1995
  circuitBreaker.recordSuccess();
@@ -2248,6 +2311,15 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2248
2311
  if (isDeactivatedWorkspaceError(errorBody, response.status)) {
2249
2312
  throw createDeactivatedWorkspaceError();
2250
2313
  }
2314
+ // A 401 here proves the token is invalid even when the body
2315
+ // carries only a generic "Unauthorized" string. The status is
2316
+ // dropped once we throw a plain Error, so normalize to the
2317
+ // canonical message the catch below matches — otherwise a
2318
+ // non-specific 401 would leave a dead routing slot unflagged
2319
+ // for codex-doctor --fix (issue #171).
2320
+ if (isInvalidatedAuthTokenError(errorBody, response.status)) {
2321
+ throw new Error("Your authentication token has been invalidated. Please try signing in again.");
2322
+ }
2251
2323
  throw new Error(message);
2252
2324
  }
2253
2325
  lastError = new Error("Codex response did not include quota headers");
@@ -2450,6 +2522,21 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2450
2522
  removeFromActive.add(getWorkspaceIdentityKey(account));
2451
2523
  flaggedChanged = true;
2452
2524
  }
2525
+ else if (isInvalidatedAuthTokenMessage(message)) {
2526
+ // The cached access token probed OK locally but the backend
2527
+ // rejected it (401 invalidated). Surface it so `codex-doctor
2528
+ // --fix` repairs the active routing instead of leaving a dead
2529
+ // slot selected (issue #171).
2530
+ const flaggedRecord = {
2531
+ ...account,
2532
+ flaggedAt: Date.now(),
2533
+ flaggedReason: "token-invalid",
2534
+ lastError: message,
2535
+ };
2536
+ flaggedUpdates.set(getWorkspaceIdentityKey(flaggedRecord), flaggedRecord);
2537
+ removeFromActive.add(getWorkspaceIdentityKey(account));
2538
+ flaggedChanged = true;
2539
+ }
2453
2540
  console.log(`[${i + 1}/${total}] ${label}: ERROR (${message.slice(0, 160)})`);
2454
2541
  }
2455
2542
  }