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.
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +97 -5
- package/dist/index.js.map +1 -1
- package/dist/lib/accounts/stale-state.d.ts +139 -0
- package/dist/lib/accounts/stale-state.d.ts.map +1 -0
- package/dist/lib/accounts/stale-state.js +192 -0
- package/dist/lib/accounts/stale-state.js.map +1 -0
- package/dist/lib/auth/login-runner.d.ts.map +1 -1
- package/dist/lib/auth/login-runner.js +5 -1
- package/dist/lib/auth/login-runner.js.map +1 -1
- package/dist/lib/error-sentinels.d.ts +26 -0
- package/dist/lib/error-sentinels.d.ts.map +1 -1
- package/dist/lib/error-sentinels.js +30 -0
- package/dist/lib/error-sentinels.js.map +1 -1
- package/dist/lib/request/fetch-helpers.d.ts +26 -0
- package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
- package/dist/lib/request/fetch-helpers.js +79 -1
- package/dist/lib/request/fetch-helpers.js.map +1 -1
- package/dist/lib/storage/identity.d.ts +1 -0
- package/dist/lib/storage/identity.d.ts.map +1 -1
- package/dist/lib/storage/identity.js +45 -2
- package/dist/lib/storage/identity.js.map +1 -1
- package/dist/lib/tools/codex-doctor.d.ts.map +1 -1
- package/dist/lib/tools/codex-doctor.js +110 -2
- package/dist/lib/tools/codex-doctor.js.map +1 -1
- package/dist/lib/tools/codex-health.d.ts.map +1 -1
- package/dist/lib/tools/codex-health.js +21 -0
- package/dist/lib/tools/codex-health.js.map +1 -1
- package/package.json +7 -4
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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(
|
|
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(
|
|
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
|
|
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
|
}
|