oc-codex-multi-auth 6.3.2 → 6.3.4

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;AAyM/D;;;;;;;;;;;;;;;GAeG;AAEH,eAAO,MAAM,iBAAiB,EAAE,MA8qH/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, createAbortError, 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";
@@ -1299,9 +1299,14 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1299
1299
  return false;
1300
1300
  };
1301
1301
  const abortSignal = requestInit?.signal ?? init?.signal ?? null;
1302
+ // Surface caller-cancellation during retry/backoff waits as a proper
1303
+ // AbortError (name set) carrying the caller abort reason, mirroring the
1304
+ // fetch path (Aborted by user) and lib/codex-usage.ts isAbortError. A bare
1305
+ // new Error("Aborted") was opaque and dropped abortSignal.reason (#176).
1306
+ const abortError = () => createAbortError(abortSignal);
1302
1307
  const sleep = (ms) => new Promise((resolve, reject) => {
1303
1308
  if (abortSignal?.aborted) {
1304
- reject(new Error("Aborted"));
1309
+ reject(abortError());
1305
1310
  return;
1306
1311
  }
1307
1312
  const timeout = setTimeout(() => {
@@ -1310,7 +1315,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1310
1315
  }, ms);
1311
1316
  const onAbort = () => {
1312
1317
  cleanup();
1313
- reject(new Error("Aborted"));
1318
+ reject(abortError());
1314
1319
  };
1315
1320
  const cleanup = () => {
1316
1321
  clearTimeout(timeout);
@@ -1323,7 +1328,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1323
1328
  const endTime = startTime + totalMs;
1324
1329
  while (Date.now() < endTime) {
1325
1330
  if (abortSignal?.aborted) {
1326
- throw new Error("Aborted");
1331
+ throw abortError();
1327
1332
  }
1328
1333
  const remaining = Math.max(0, endTime - Date.now());
1329
1334
  const waitLabel = formatWaitTime(remaining);
@@ -1861,6 +1866,63 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1861
1866
  }
1862
1867
  break;
1863
1868
  }
1869
+ // A 401 token-invalidated response means the access token presented for
1870
+ // THIS account was rejected by the backend even though the proactive
1871
+ // refresh above either ran or judged the token still fresh. Without an
1872
+ // explicit handler the 401 fell straight through to `return errorResponse`
1873
+ // below, so persisted family routing kept pinning every request to the dead
1874
+ // account slot instead of failing over (issue #171). Treat it as an
1875
+ // account-health failure: cool the refresh-token group down (or remove it
1876
+ // past the failure threshold) and rotate to the next healthy account.
1877
+ //
1878
+ // Note: 401s intentionally do NOT feed the circuit breaker — the breaker
1879
+ // guards against upstream faults (network / 5xx), not client-side auth
1880
+ // decisions (see the 5xx handler above).
1881
+ if (isInvalidatedAuthTokenError(errorBody, response.status)) {
1882
+ const accountLabel = formatAccountLabel(account, account.index, {
1883
+ maskEmail: maskEmailEnabled,
1884
+ });
1885
+ accountManager.refundToken(account, modelFamily, model);
1886
+ accountManager.recordFailure(account, modelFamily, model);
1887
+ account.lastSwitchReason = "rotation";
1888
+ runtimeMetrics.failedRequests++;
1889
+ runtimeMetrics.accountRotations++;
1890
+ runtimeMetrics.lastError = `Auth token invalidated on ${accountLabel}`;
1891
+ runtimeMetrics.lastErrorCategory = "auth-invalidated";
1892
+ const failures = await accountManager.incrementAuthFailures(account);
1893
+ if (failures >= ACCOUNT_LIMITS.MAX_AUTH_FAILURES_BEFORE_REMOVAL) {
1894
+ const removedCount = accountManager.removeAccountsWithSameRefreshToken(account);
1895
+ if (removedCount > 0) {
1896
+ accountManager.saveToDiskDebounced();
1897
+ await showToast(removedCount > 1
1898
+ ? `Removed ${removedCount} accounts (same refresh token) after ${failures} auth-token failures. Run 'opencode auth login' to re-add.`
1899
+ : `Removed ${accountLabel} after ${failures} auth-token failures. Run 'opencode auth login' to re-add.`, "error", { duration: toastDurationMs * 2 });
1900
+ // Indices shift after removal; restart traversal with a fresh
1901
+ // attempted set so no healthy account is skipped.
1902
+ attempted.clear();
1903
+ accountCount = accountManager.getAccountCount();
1904
+ break;
1905
+ }
1906
+ logWarn(`[${PLUGIN_NAME}] Expected grouped account removal after auth-token invalidation, but removed ${removedCount}.`);
1907
+ }
1908
+ // Below the removal threshold (or grouped removal was a no-op): cool the
1909
+ // account's whole refresh-token group down so selection skips it, then
1910
+ // rotate to the next healthy account instead of returning the 401.
1911
+ const cooledCount = accountManager.markAccountsWithRefreshTokenCoolingDown(account.refreshToken, ACCOUNT_LIMITS.AUTH_FAILURE_COOLDOWN_MS, "auth-failure");
1912
+ if (cooledCount <= 0) {
1913
+ accountManager.markAccountCoolingDown(account, ACCOUNT_LIMITS.AUTH_FAILURE_COOLDOWN_MS, "auth-failure");
1914
+ }
1915
+ accountManager.saveToDiskDebounced();
1916
+ logWarn(accountCount > 1
1917
+ ? `Auth token invalidated for account ${account.index + 1}. Cooling down and rotating to next account.`
1918
+ : `Auth token invalidated for account ${account.index + 1}. Cooling down; no other account available.`);
1919
+ if (accountCount > 1 &&
1920
+ accountManager.shouldShowAccountToast(account.index, rateLimitToastDebounceMs)) {
1921
+ await showToast(`Account ${account.index + 1} sign-in expired. Switching accounts.`, "warning", { duration: toastDurationMs });
1922
+ accountManager.markToastShown(account.index);
1923
+ }
1924
+ break;
1925
+ }
1864
1926
  runtimeMetrics.failedRequests++;
1865
1927
  runtimeMetrics.lastError = `HTTP ${response.status}`;
1866
1928
  runtimeMetrics.lastErrorCategory = "http";
@@ -1927,6 +1989,12 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1927
1989
  }
1928
1990
  }
1929
1991
  accountManager.recordSuccess(account, modelFamily, model);
1992
+ // A successful request proves the account's credentials are good
1993
+ // again, so reset the auth-failure counter that a prior 401
1994
+ // token-invalidated response (or refresh failure) may have bumped.
1995
+ // Otherwise stale counts could accumulate across requests and
1996
+ // eventually remove a now-healthy account.
1997
+ accountManager.clearAuthFailures(account);
1930
1998
  // RC-8: closes a half-open gate or prunes the failure window so a
1931
1999
  // sequence of successes keeps the breaker healthy.
1932
2000
  circuitBreaker.recordSuccess();
@@ -2248,6 +2316,15 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2248
2316
  if (isDeactivatedWorkspaceError(errorBody, response.status)) {
2249
2317
  throw createDeactivatedWorkspaceError();
2250
2318
  }
2319
+ // A 401 here proves the token is invalid even when the body
2320
+ // carries only a generic "Unauthorized" string. The status is
2321
+ // dropped once we throw a plain Error, so normalize to the
2322
+ // canonical message the catch below matches — otherwise a
2323
+ // non-specific 401 would leave a dead routing slot unflagged
2324
+ // for codex-doctor --fix (issue #171).
2325
+ if (isInvalidatedAuthTokenError(errorBody, response.status)) {
2326
+ throw new Error("Your authentication token has been invalidated. Please try signing in again.");
2327
+ }
2251
2328
  throw new Error(message);
2252
2329
  }
2253
2330
  lastError = new Error("Codex response did not include quota headers");
@@ -2450,6 +2527,21 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2450
2527
  removeFromActive.add(getWorkspaceIdentityKey(account));
2451
2528
  flaggedChanged = true;
2452
2529
  }
2530
+ else if (isInvalidatedAuthTokenMessage(message)) {
2531
+ // The cached access token probed OK locally but the backend
2532
+ // rejected it (401 invalidated). Surface it so `codex-doctor
2533
+ // --fix` repairs the active routing instead of leaving a dead
2534
+ // slot selected (issue #171).
2535
+ const flaggedRecord = {
2536
+ ...account,
2537
+ flaggedAt: Date.now(),
2538
+ flaggedReason: "token-invalid",
2539
+ lastError: message,
2540
+ };
2541
+ flaggedUpdates.set(getWorkspaceIdentityKey(flaggedRecord), flaggedRecord);
2542
+ removeFromActive.add(getWorkspaceIdentityKey(account));
2543
+ flaggedChanged = true;
2544
+ }
2453
2545
  console.log(`[${i + 1}/${total}] ${label}: ERROR (${message.slice(0, 160)})`);
2454
2546
  }
2455
2547
  }