oc-codex-multi-auth 6.1.10 → 6.3.0
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/README.md +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +192 -27
- package/dist/index.js.map +1 -1
- package/dist/lib/account-display.d.ts +31 -0
- package/dist/lib/account-display.d.ts.map +1 -0
- package/dist/lib/account-display.js +46 -0
- package/dist/lib/account-display.js.map +1 -0
- package/dist/lib/accounts/recovery.d.ts +13 -0
- package/dist/lib/accounts/recovery.d.ts.map +1 -1
- package/dist/lib/accounts/recovery.js +32 -0
- package/dist/lib/accounts/recovery.js.map +1 -1
- package/dist/lib/accounts/state.d.ts +6 -0
- package/dist/lib/accounts/state.d.ts.map +1 -1
- package/dist/lib/accounts/state.js +37 -1
- package/dist/lib/accounts/state.js.map +1 -1
- package/dist/lib/accounts.d.ts +4 -1
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/accounts.js +6 -2
- package/dist/lib/accounts.js.map +1 -1
- package/dist/lib/cli.d.ts +1 -0
- package/dist/lib/cli.d.ts.map +1 -1
- package/dist/lib/cli.js +9 -6
- package/dist/lib/cli.js.map +1 -1
- package/dist/lib/codex-usage.d.ts +49 -0
- package/dist/lib/codex-usage.d.ts.map +1 -1
- package/dist/lib/codex-usage.js +96 -16
- package/dist/lib/codex-usage.js.map +1 -1
- package/dist/lib/config.d.ts +2 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +8 -0
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/errors.d.ts +11 -0
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +4 -0
- package/dist/lib/errors.js.map +1 -1
- package/dist/lib/logger.d.ts.map +1 -1
- package/dist/lib/logger.js +14 -1
- package/dist/lib/logger.js.map +1 -1
- package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
- package/dist/lib/request/fetch-helpers.js +37 -16
- package/dist/lib/request/fetch-helpers.js.map +1 -1
- package/dist/lib/request/rate-limit-backoff.d.ts +7 -0
- package/dist/lib/request/rate-limit-backoff.d.ts.map +1 -1
- package/dist/lib/request/rate-limit-backoff.js +24 -0
- package/dist/lib/request/rate-limit-backoff.js.map +1 -1
- package/dist/lib/rotation.d.ts +23 -0
- package/dist/lib/rotation.d.ts.map +1 -1
- package/dist/lib/rotation.js +47 -0
- package/dist/lib/rotation.js.map +1 -1
- package/dist/lib/schemas.d.ts +2 -0
- package/dist/lib/schemas.d.ts.map +1 -1
- package/dist/lib/schemas.js +2 -0
- package/dist/lib/schemas.js.map +1 -1
- package/dist/lib/storage/flagged.d.ts.map +1 -1
- package/dist/lib/storage/flagged.js +48 -2
- package/dist/lib/storage/flagged.js.map +1 -1
- package/dist/lib/storage/keychain.d.ts +14 -0
- package/dist/lib/storage/keychain.d.ts.map +1 -1
- package/dist/lib/storage/keychain.js +42 -0
- package/dist/lib/storage/keychain.js.map +1 -1
- package/dist/lib/storage/load-save.d.ts.map +1 -1
- package/dist/lib/storage/load-save.js +22 -4
- package/dist/lib/storage/load-save.js.map +1 -1
- package/dist/lib/tools/codex-dashboard.d.ts.map +1 -1
- package/dist/lib/tools/codex-dashboard.js +4 -3
- package/dist/lib/tools/codex-dashboard.js.map +1 -1
- package/dist/lib/tools/codex-diff.d.ts.map +1 -1
- package/dist/lib/tools/codex-diff.js +68 -8
- package/dist/lib/tools/codex-diff.js.map +1 -1
- package/dist/lib/tools/codex-health.d.ts.map +1 -1
- package/dist/lib/tools/codex-health.js +6 -4
- package/dist/lib/tools/codex-health.js.map +1 -1
- package/dist/lib/tools/codex-label.d.ts.map +1 -1
- package/dist/lib/tools/codex-label.js +3 -2
- package/dist/lib/tools/codex-label.js.map +1 -1
- package/dist/lib/tools/codex-limits.d.ts +12 -0
- package/dist/lib/tools/codex-limits.d.ts.map +1 -1
- package/dist/lib/tools/codex-limits.js +50 -7
- package/dist/lib/tools/codex-limits.js.map +1 -1
- package/dist/lib/tools/codex-list.d.ts.map +1 -1
- package/dist/lib/tools/codex-list.js +4 -3
- package/dist/lib/tools/codex-list.js.map +1 -1
- package/dist/lib/tools/codex-note.d.ts.map +1 -1
- package/dist/lib/tools/codex-note.js +3 -2
- package/dist/lib/tools/codex-note.js.map +1 -1
- package/dist/lib/tools/codex-refresh.d.ts.map +1 -1
- package/dist/lib/tools/codex-refresh.js +3 -2
- package/dist/lib/tools/codex-refresh.js.map +1 -1
- package/dist/lib/tools/codex-remove.d.ts.map +1 -1
- package/dist/lib/tools/codex-remove.js +9 -6
- package/dist/lib/tools/codex-remove.js.map +1 -1
- package/dist/lib/tools/codex-status.d.ts.map +1 -1
- package/dist/lib/tools/codex-status.js +4 -3
- package/dist/lib/tools/codex-status.js.map +1 -1
- package/dist/lib/tools/codex-switch.d.ts.map +1 -1
- package/dist/lib/tools/codex-switch.js +4 -3
- package/dist/lib/tools/codex-switch.js.map +1 -1
- package/dist/lib/tools/codex-tag.d.ts.map +1 -1
- package/dist/lib/tools/codex-tag.js +3 -2
- package/dist/lib/tools/codex-tag.js.map +1 -1
- package/dist/lib/tools/index.d.ts +4 -1
- package/dist/lib/tools/index.d.ts.map +1 -1
- package/dist/lib/tools/index.js.map +1 -1
- package/dist/lib/tui-status.d.ts +4 -1
- package/dist/lib/tui-status.d.ts.map +1 -1
- package/dist/lib/tui-status.js +22 -13
- package/dist/lib/tui-status.js.map +1 -1
- package/dist/lib/ui/auth-menu.d.ts +4 -1
- package/dist/lib/ui/auth-menu.d.ts.map +1 -1
- package/dist/lib/ui/auth-menu.js +10 -7
- package/dist/lib/ui/auth-menu.js.map +1 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +14 -5
- package/dist/tui.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -249,6 +249,8 @@ Selected runtime/environment overrides:
|
|
|
249
249
|
| `CODEX_TUI_V2=0/1` | Disable/enable codex-style tool output |
|
|
250
250
|
| `CODEX_TUI_COLOR_PROFILE=truecolor|ansi256|ansi16` | Force terminal color profile |
|
|
251
251
|
| `CODEX_TUI_GLYPHS=ascii|unicode|auto` | Force terminal glyph style |
|
|
252
|
+
| `CODEX_TUI_MASK_EMAIL=0/1` | Hide account email in TUI quota status only |
|
|
253
|
+
| `CODEX_TUI_MASK_EMAIL_DETAILS=0/1` | Also hide account email in quota details when prompt masking is enabled |
|
|
252
254
|
| `CODEX_AUTH_PER_PROJECT_ACCOUNTS=0/1` | Disable/enable per-project account pools |
|
|
253
255
|
| `CODEX_AUTH_AUTO_UPDATE=0/1` | Disable/enable daily npm update check and cache refresh |
|
|
254
256
|
| `CODEX_AUTH_UNSUPPORTED_MODEL_POLICY=strict|fallback` | Control unsupported-model retry behavior |
|
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;AAsM/D;;;;;;;;;;;;;;;GAeG;AAEH,eAAO,MAAM,iBAAiB,EAAE,MAojH/B,CAAC;AAEF,eAAO,MAAM,gBAAgB,QAAoB,CAAC;AAElD,eAAe,iBAAiB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -32,13 +32,15 @@ import { queuedRefresh } from "./lib/refresh-queue.js";
|
|
|
32
32
|
import { openBrowserUrl } from "./lib/auth/browser.js";
|
|
33
33
|
import { startLocalOAuthServer } from "./lib/auth/server.js";
|
|
34
34
|
import { promptAddAnotherAccount, promptLoginMode } from "./lib/cli.js";
|
|
35
|
-
import { getCodexMode, getRequestTransformMode, getFastSession, getFastSessionStrategy, getFastSessionMaxInputItems, getRetryProfile, getRetryBudgetOverrides, getRateLimitToastDebounceMs, getRetryAllAccountsMaxRetries, getRetryAllAccountsMaxWaitMs, getRetryAllAccountsRateLimited, getFallbackToGpt52OnUnsupportedGpt53, getUnsupportedCodexPolicy, getUnsupportedCodexFallbackChain, getTokenRefreshSkewMs, getSessionRecovery, getAutoResume, getAutoUpdate, getToastDurationMs, getPerProjectAccounts, getEmptyResponseMaxRetries, getEmptyResponseRetryDelayMs, getPidOffsetEnabled, getFetchTimeoutMs, getStreamStallTimeoutMs, getCodexTuiV2, getCodexTuiColorProfile, getCodexTuiGlyphMode, getBeginnerSafeMode, loadPluginConfig, } from "./lib/config.js";
|
|
35
|
+
import { getCodexMode, getRequestTransformMode, getFastSession, getFastSessionStrategy, getFastSessionMaxInputItems, getRetryProfile, getRetryBudgetOverrides, getRateLimitToastDebounceMs, getRetryAllAccountsMaxRetries, getRetryAllAccountsMaxWaitMs, getRetryAllAccountsRateLimited, getFallbackToGpt52OnUnsupportedGpt53, getUnsupportedCodexPolicy, getUnsupportedCodexFallbackChain, getTokenRefreshSkewMs, getSessionRecovery, getAutoResume, getAutoUpdate, getToastDurationMs, getPerProjectAccounts, getEmptyResponseMaxRetries, getEmptyResponseRetryDelayMs, getPidOffsetEnabled, getFetchTimeoutMs, getStreamStallTimeoutMs, getCodexTuiV2, getCodexTuiColorProfile, getCodexTuiGlyphMode, getBeginnerSafeMode, getCodexTuiMaskEmail, loadPluginConfig, } from "./lib/config.js";
|
|
36
36
|
import { AUTH_LABELS, CODEX_BASE_URL, DUMMY_API_KEY, LOG_STAGES, PLUGIN_NAME, PROVIDER_ID, ACCOUNT_LIMITS, } from "./lib/constants.js";
|
|
37
37
|
import { initLogger, logRequest, logDebug, logInfo, logWarn, logError, setCorrelationId, clearCorrelationId, } from "./lib/logger.js";
|
|
38
38
|
import { checkAndNotify } from "./lib/auto-update-checker.js";
|
|
39
39
|
import { handleContextOverflow } from "./lib/context-overflow.js";
|
|
40
40
|
import { AccountManager, extractAccountEmail, extractAccountId, formatAccountLabel, formatWaitTime, sanitizeEmail, shouldUpdateAccountIdFromToken, resolveRequestAccountId, parseRateLimitReason, lookupCodexCliTokensByEmail, } from "./lib/accounts.js";
|
|
41
|
-
import {
|
|
41
|
+
import { resolveDisplayEmail } from "./lib/account-display.js";
|
|
42
|
+
import { CodexAuthError } from "./lib/errors.js";
|
|
43
|
+
import { getStoragePath, loadAccounts, saveAccounts, withAccountStorageTransaction, clearAccounts, setStoragePath, loadFlaggedAccounts, saveFlaggedAccounts, withFlaggedAccountStorageTransaction, clearFlaggedAccounts, getWorkspaceIdentityKey, StorageError, formatStorageErrorHint, } from "./lib/storage.js";
|
|
42
44
|
import { createCodexHeaders, extractRequestUrl, handleErrorResponse, handleSuccessResponse, isDeactivatedWorkspaceError, getUnsupportedCodexModelInfo, resolveUnsupportedCodexFallbackModel, refreshAndUpdateToken, rewriteUrlForCodex, shouldRefreshToken, transformRequestForCodex, } from "./lib/request/fetch-helpers.js";
|
|
43
45
|
import { createDeactivatedWorkspaceError, DEACTIVATED_WORKSPACE_ERROR_CODE, isDeactivatedWorkspaceErrorMessage, } from "./lib/error-sentinels.js";
|
|
44
46
|
import { applyFastSessionDefaults, upsertBackendModelIdentityMessage, } from "./lib/request/request-transformer.js";
|
|
@@ -399,15 +401,22 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
399
401
|
if (accountsToHydrate.length === 0)
|
|
400
402
|
return storage;
|
|
401
403
|
let changed = false;
|
|
404
|
+
// Record hydrated field updates keyed by the account's ORIGINAL
|
|
405
|
+
// refresh token (captured before the network call) so we can
|
|
406
|
+
// re-apply them onto a freshly-loaded snapshot inside a storage
|
|
407
|
+
// transaction — avoiding a lost-update race with concurrent saves.
|
|
408
|
+
const hydrationUpdates = new Map();
|
|
402
409
|
// process in chunks of 3 to avoid auth0 rate limits (429) on startup
|
|
403
410
|
const chunkSize = 3;
|
|
404
411
|
for (let i = 0; i < accountsToHydrate.length; i += chunkSize) {
|
|
405
412
|
const chunk = accountsToHydrate.slice(i, i + chunkSize);
|
|
406
413
|
await Promise.all(chunk.map(async (account) => {
|
|
414
|
+
const originalRefreshToken = account.refreshToken;
|
|
407
415
|
try {
|
|
408
416
|
const refreshed = await queuedRefresh(account.refreshToken);
|
|
409
417
|
if (refreshed.type !== "success")
|
|
410
418
|
return;
|
|
419
|
+
const update = hydrationUpdates.get(originalRefreshToken) ?? {};
|
|
411
420
|
const id = extractAccountId(refreshed.access);
|
|
412
421
|
const email = sanitizeEmail(extractAccountEmail(refreshed.access, refreshed.idToken));
|
|
413
422
|
if (id &&
|
|
@@ -415,28 +424,38 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
415
424
|
shouldUpdateAccountIdFromToken(account.accountIdSource, account.accountId)) {
|
|
416
425
|
account.accountId = id;
|
|
417
426
|
account.accountIdSource = "token";
|
|
427
|
+
update.accountId = id;
|
|
428
|
+
update.accountIdSource = "token";
|
|
418
429
|
changed = true;
|
|
419
430
|
}
|
|
420
431
|
if (email && email !== account.email) {
|
|
421
432
|
account.email = email;
|
|
433
|
+
update.email = email;
|
|
422
434
|
changed = true;
|
|
423
435
|
}
|
|
424
436
|
if (refreshed.access && refreshed.access !== account.accessToken) {
|
|
425
437
|
account.accessToken = refreshed.access;
|
|
438
|
+
update.accessToken = refreshed.access;
|
|
426
439
|
changed = true;
|
|
427
440
|
}
|
|
428
441
|
if (typeof refreshed.expires === "number" && refreshed.expires !== account.expiresAt) {
|
|
429
442
|
account.expiresAt = refreshed.expires;
|
|
443
|
+
update.expiresAt = refreshed.expires;
|
|
430
444
|
changed = true;
|
|
431
445
|
}
|
|
432
446
|
if (refreshed.scope && refreshed.scope !== account.oauthScope) {
|
|
433
447
|
account.oauthScope = refreshed.scope;
|
|
448
|
+
update.oauthScope = refreshed.scope;
|
|
434
449
|
changed = true;
|
|
435
450
|
}
|
|
436
451
|
if (refreshed.refresh && refreshed.refresh !== account.refreshToken) {
|
|
437
452
|
account.refreshToken = refreshed.refresh;
|
|
453
|
+
update.refreshToken = refreshed.refresh;
|
|
438
454
|
changed = true;
|
|
439
455
|
}
|
|
456
|
+
if (Object.keys(update).length > 0) {
|
|
457
|
+
hydrationUpdates.set(originalRefreshToken, update);
|
|
458
|
+
}
|
|
440
459
|
}
|
|
441
460
|
catch {
|
|
442
461
|
logWarn(`[${PLUGIN_NAME}] Failed to hydrate email for account`);
|
|
@@ -444,8 +463,37 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
444
463
|
}));
|
|
445
464
|
}
|
|
446
465
|
if (changed) {
|
|
466
|
+
// Persist under the storage lock against a fresh snapshot so a
|
|
467
|
+
// concurrent save during the (potentially multi-second) hydration
|
|
468
|
+
// network loop is not clobbered. Match accounts by their original
|
|
469
|
+
// refresh token.
|
|
470
|
+
await withAccountStorageTransaction(async (current, persist) => {
|
|
471
|
+
if (!current)
|
|
472
|
+
return;
|
|
473
|
+
for (const acc of current.accounts) {
|
|
474
|
+
const update = hydrationUpdates.get(acc.refreshToken);
|
|
475
|
+
if (!update)
|
|
476
|
+
continue;
|
|
477
|
+
if (update.accountId !== undefined)
|
|
478
|
+
acc.accountId = update.accountId;
|
|
479
|
+
if (update.accountIdSource !== undefined)
|
|
480
|
+
acc.accountIdSource = update.accountIdSource;
|
|
481
|
+
if (update.email !== undefined)
|
|
482
|
+
acc.email = update.email;
|
|
483
|
+
if (update.accessToken !== undefined)
|
|
484
|
+
acc.accessToken = update.accessToken;
|
|
485
|
+
if (update.expiresAt !== undefined)
|
|
486
|
+
acc.expiresAt = update.expiresAt;
|
|
487
|
+
if (update.oauthScope !== undefined)
|
|
488
|
+
acc.oauthScope = update.oauthScope;
|
|
489
|
+
// Apply the rotated refresh token LAST so the map key
|
|
490
|
+
// (original token) still matches above.
|
|
491
|
+
if (update.refreshToken !== undefined)
|
|
492
|
+
acc.refreshToken = update.refreshToken;
|
|
493
|
+
}
|
|
494
|
+
await persist(current);
|
|
495
|
+
});
|
|
447
496
|
storage.accounts = accountsCopy;
|
|
448
|
-
await saveAccounts(storage);
|
|
449
497
|
}
|
|
450
498
|
return storage;
|
|
451
499
|
};
|
|
@@ -487,6 +535,9 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
487
535
|
const resolveUiRuntime = () => {
|
|
488
536
|
return applyUiRuntimeFromConfig(loadPluginConfig());
|
|
489
537
|
};
|
|
538
|
+
const resolveMaskEmail = () => {
|
|
539
|
+
return getCodexTuiMaskEmail(loadPluginConfig());
|
|
540
|
+
};
|
|
490
541
|
const getStatusMarker = (ui, status) => {
|
|
491
542
|
if (!ui.v2Enabled) {
|
|
492
543
|
if (status === "ok")
|
|
@@ -509,8 +560,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
509
560
|
return normalized;
|
|
510
561
|
return `${normalized.slice(0, 8)}...${normalized.slice(-6)}`;
|
|
511
562
|
};
|
|
512
|
-
const formatCommandAccountLabel = (account, index) => {
|
|
513
|
-
const email = account?.email
|
|
563
|
+
const formatCommandAccountLabel = (account, index, options = {}) => {
|
|
564
|
+
const email = resolveDisplayEmail(account?.email, options.maskEmail ?? false);
|
|
514
565
|
const workspace = account?.accountLabel?.trim();
|
|
515
566
|
const accountId = formatAccountIdForDisplay(account?.accountId);
|
|
516
567
|
const tags = Array.isArray(account?.accountTags)
|
|
@@ -555,8 +606,9 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
555
606
|
return null;
|
|
556
607
|
try {
|
|
557
608
|
const { select } = await import("./lib/ui/select.js");
|
|
609
|
+
const maskEmail = resolveMaskEmail();
|
|
558
610
|
const selected = await select(storage.accounts.map((account, index) => ({
|
|
559
|
-
label: formatCommandAccountLabel(account, index),
|
|
611
|
+
label: formatCommandAccountLabel(account, index, { maskEmail }),
|
|
560
612
|
value: index,
|
|
561
613
|
})), {
|
|
562
614
|
message: title,
|
|
@@ -819,8 +871,23 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
819
871
|
}
|
|
820
872
|
};
|
|
821
873
|
const invalidateAccountManagerCache = () => {
|
|
874
|
+
// Dispose the outgoing manager so we don't leak its shutdown handler
|
|
875
|
+
// into the global cleanup queue or leave a pending debounce timer
|
|
876
|
+
// pointing at a stale instance. Flush first (best-effort, in the
|
|
877
|
+
// background) so any queued debounced save is not silently dropped.
|
|
878
|
+
const previous = cachedAccountManager;
|
|
822
879
|
cachedAccountManager = null;
|
|
823
880
|
accountManagerPromise = null;
|
|
881
|
+
if (previous) {
|
|
882
|
+
void previous
|
|
883
|
+
.flushPendingSave()
|
|
884
|
+
.catch((error) => {
|
|
885
|
+
logWarn(`Failed to flush pending save while invalidating account manager: ${error instanceof Error ? error.message : String(error)}`);
|
|
886
|
+
})
|
|
887
|
+
.finally(() => {
|
|
888
|
+
previous.disposeShutdownHandler();
|
|
889
|
+
});
|
|
890
|
+
}
|
|
824
891
|
};
|
|
825
892
|
const persistAuthenticatedSelections = async (results, replaceAll) => {
|
|
826
893
|
try {
|
|
@@ -919,6 +986,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
919
986
|
resolveUiRuntime,
|
|
920
987
|
getStatusMarker,
|
|
921
988
|
formatCommandAccountLabel,
|
|
989
|
+
resolveMaskEmail,
|
|
922
990
|
normalizeAccountTags,
|
|
923
991
|
supportsInteractiveMenus,
|
|
924
992
|
promptAccountIndexSelection,
|
|
@@ -1027,6 +1095,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1027
1095
|
const fastSessionMaxInputItems = getFastSessionMaxInputItems(pluginConfig);
|
|
1028
1096
|
const beginnerSafeMode = getBeginnerSafeMode(pluginConfig);
|
|
1029
1097
|
beginnerSafeModeEnabled = beginnerSafeMode;
|
|
1098
|
+
const maskEmailEnabled = getCodexTuiMaskEmail(pluginConfig);
|
|
1030
1099
|
const retryProfile = beginnerSafeMode
|
|
1031
1100
|
? "conservative"
|
|
1032
1101
|
: getRetryProfile(pluginConfig);
|
|
@@ -1345,8 +1414,24 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1345
1414
|
runtimeMetrics.accountRotations++;
|
|
1346
1415
|
runtimeMetrics.lastError = err?.message ?? String(err);
|
|
1347
1416
|
runtimeMetrics.lastErrorCategory = "auth-refresh";
|
|
1417
|
+
// Transient refresh failures (network blip / upstream 5xx) must NOT
|
|
1418
|
+
// count toward permanent account removal — the credentials are still
|
|
1419
|
+
// valid and a flaky network or outage would otherwise silently delete
|
|
1420
|
+
// them. Cool the account down briefly and rotate instead.
|
|
1421
|
+
const isTransientRefreshFailure = err instanceof CodexAuthError && err.retryable === true;
|
|
1422
|
+
if (isTransientRefreshFailure) {
|
|
1423
|
+
const cooledCount = accountManager.markAccountsWithRefreshTokenCoolingDown(account.refreshToken, ACCOUNT_LIMITS.AUTH_FAILURE_COOLDOWN_MS, "auth-failure");
|
|
1424
|
+
if (cooledCount <= 0) {
|
|
1425
|
+
accountManager.markAccountCoolingDown(account, ACCOUNT_LIMITS.AUTH_FAILURE_COOLDOWN_MS, "auth-failure");
|
|
1426
|
+
}
|
|
1427
|
+
accountManager.saveToDiskDebounced();
|
|
1428
|
+
logWarn(`[${PLUGIN_NAME}] Transient auth refresh failure for account ${account.index + 1} (${err.refreshFailureReason ?? "unknown"}${err.statusCode ? ` ${err.statusCode}` : ""}); cooling down without counting toward removal.`);
|
|
1429
|
+
continue;
|
|
1430
|
+
}
|
|
1348
1431
|
const failures = await accountManager.incrementAuthFailures(account);
|
|
1349
|
-
const accountLabel = formatAccountLabel(account, account.index
|
|
1432
|
+
const accountLabel = formatAccountLabel(account, account.index, {
|
|
1433
|
+
maskEmail: maskEmailEnabled,
|
|
1434
|
+
});
|
|
1350
1435
|
if (failures >= ACCOUNT_LIMITS.MAX_AUTH_FAILURES_BEFORE_REMOVAL) {
|
|
1351
1436
|
const removedCount = accountManager.removeAccountsWithSameRefreshToken(account);
|
|
1352
1437
|
if (removedCount <= 0) {
|
|
@@ -1392,7 +1477,9 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1392
1477
|
accountManager.saveToDiskDebounced();
|
|
1393
1478
|
if (accountCount > 1 &&
|
|
1394
1479
|
accountManager.shouldShowAccountToast(account.index, rateLimitToastDebounceMs)) {
|
|
1395
|
-
const accountLabel = formatAccountLabel(account, account.index
|
|
1480
|
+
const accountLabel = formatAccountLabel(account, account.index, {
|
|
1481
|
+
maskEmail: maskEmailEnabled,
|
|
1482
|
+
});
|
|
1396
1483
|
await showToast(`Using ${accountLabel} (${account.index + 1}/${accountCount})`, "info");
|
|
1397
1484
|
accountManager.markToastShown(account.index);
|
|
1398
1485
|
}
|
|
@@ -1410,7 +1497,11 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1410
1497
|
`Local token bucket depleted for account ${account.index + 1} (${modelFamily}${model ? `:${model}` : ""})`;
|
|
1411
1498
|
runtimeMetrics.lastErrorCategory = "rate-limit-local";
|
|
1412
1499
|
logWarn(`Skipping account ${account.index + 1}: local token bucket depleted for ${modelFamily}${model ? `:${model}` : ""}`);
|
|
1413
|
-
|
|
1500
|
+
// Skip THIS account and rotate to the next one. `account.index` is
|
|
1501
|
+
// already in `attempted` (added above), so the traversal loop guard
|
|
1502
|
+
// prevents reselecting it and terminates once all are exhausted.
|
|
1503
|
+
// Using `break` here would abandon every other healthy account.
|
|
1504
|
+
continue;
|
|
1414
1505
|
}
|
|
1415
1506
|
// RC-8: per-(account, family) circuit-breaker key. The breaker gates
|
|
1416
1507
|
// upstream calls so that repeated failures short-circuit to the
|
|
@@ -1525,7 +1616,9 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1525
1616
|
});
|
|
1526
1617
|
const workspaceDeactivated = isDeactivatedWorkspaceError(errorBody, response.status);
|
|
1527
1618
|
if (workspaceDeactivated) {
|
|
1528
|
-
const accountLabel = formatAccountLabel(account, account.index
|
|
1619
|
+
const accountLabel = formatAccountLabel(account, account.index, {
|
|
1620
|
+
maskEmail: maskEmailEnabled,
|
|
1621
|
+
});
|
|
1529
1622
|
accountManager.refundToken(account, modelFamily, model);
|
|
1530
1623
|
accountManager.recordFailure(account, modelFamily, model);
|
|
1531
1624
|
account.lastSwitchReason = "rotation";
|
|
@@ -1552,12 +1645,13 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1552
1645
|
catch (flagError) {
|
|
1553
1646
|
logWarn(`Failed to persist deactivated workspace flag for ${accountLabel}: ${flagError instanceof Error ? flagError.message : String(flagError)}`);
|
|
1554
1647
|
}
|
|
1555
|
-
//
|
|
1556
|
-
//
|
|
1557
|
-
//
|
|
1558
|
-
//
|
|
1559
|
-
//
|
|
1560
|
-
|
|
1648
|
+
// Remove ONLY the deactivated workspace, scoped by workspace
|
|
1649
|
+
// identity (org/account id). A single multi-org OAuth login
|
|
1650
|
+
// produces sibling accounts that share one refresh token but are
|
|
1651
|
+
// independently valid; removing all refresh-token siblings here
|
|
1652
|
+
// would silently drop still-valid workspaces from rotation. The
|
|
1653
|
+
// refresh token itself is still good, so siblings must survive.
|
|
1654
|
+
const removedCount = accountManager.removeAccountsByWorkspaceIdentity(account);
|
|
1561
1655
|
if (removedCount > 0) {
|
|
1562
1656
|
accountManager.saveToDiskDebounced();
|
|
1563
1657
|
restartAccountTraversalAfterWorkspaceDeactivation = true;
|
|
@@ -1774,9 +1868,32 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1774
1868
|
}
|
|
1775
1869
|
resetRateLimitBackoff(account.index, quotaKey);
|
|
1776
1870
|
runtimeMetrics.cumulativeLatencyMs += fetchLatencyMs;
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1871
|
+
let successResponse;
|
|
1872
|
+
try {
|
|
1873
|
+
successResponse = await handleSuccessResponse(response, isStreaming, {
|
|
1874
|
+
streamStallTimeoutMs,
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
catch (streamError) {
|
|
1878
|
+
// A stream stall or SSE-parse failure happened AFTER a token was
|
|
1879
|
+
// consumed (line ~token-consume above). Without this catch the
|
|
1880
|
+
// exception escaped both loops, leaking the consumed token and
|
|
1881
|
+
// skipping account rotation. Refund, mark the breaker/account
|
|
1882
|
+
// failed, and rotate to the next account.
|
|
1883
|
+
accountManager.refundToken(account, modelFamily, model);
|
|
1884
|
+
accountManager.recordFailure(account, modelFamily, model);
|
|
1885
|
+
circuitBreaker.recordFailure();
|
|
1886
|
+
account.lastSwitchReason = "rotation";
|
|
1887
|
+
runtimeMetrics.failedRequests++;
|
|
1888
|
+
runtimeMetrics.accountRotations++;
|
|
1889
|
+
runtimeMetrics.lastError =
|
|
1890
|
+
streamError instanceof Error ? streamError.message : String(streamError);
|
|
1891
|
+
runtimeMetrics.lastErrorCategory = "stream";
|
|
1892
|
+
logWarn(`Stream/response handling failed for account ${account.index + 1}: ${runtimeMetrics.lastError}. Rotating.`);
|
|
1893
|
+
// Account for the server-class retry budget, then rotate.
|
|
1894
|
+
consumeRetryBudget("server", "Stream/response handling failure");
|
|
1895
|
+
break;
|
|
1896
|
+
}
|
|
1780
1897
|
if (!successResponse.ok) {
|
|
1781
1898
|
runtimeMetrics.failedRequests++;
|
|
1782
1899
|
runtimeMetrics.lastError = `HTTP ${successResponse.status}`;
|
|
@@ -1795,10 +1912,12 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1795
1912
|
runtimeMetrics.emptyResponseRetries++;
|
|
1796
1913
|
logWarn(`Empty response received (attempt ${emptyResponseRetries}/${emptyResponseMaxRetries}). Retrying...`);
|
|
1797
1914
|
await showToast(`Empty response. Retrying (${emptyResponseRetries}/${emptyResponseMaxRetries})...`, "warning", { duration: toastDurationMs });
|
|
1798
|
-
accountManager.refundToken(account, modelFamily, model);
|
|
1799
|
-
accountManager.recordFailure(account, modelFamily, model);
|
|
1800
1915
|
await sleep(addJitter(emptyResponseRetryDelayMs, 0.2));
|
|
1801
|
-
|
|
1916
|
+
// Re-issue against the SAME account by re-entering the inner
|
|
1917
|
+
// request loop. Using `break` here exited to account rotation,
|
|
1918
|
+
// which is a no-op for single-account pools and surfaced a
|
|
1919
|
+
// misleading "all accounts failed" 503 instead of retrying.
|
|
1920
|
+
continue;
|
|
1802
1921
|
}
|
|
1803
1922
|
logWarn(`Empty response after ${emptyResponseMaxRetries} retries. Returning as-is.`);
|
|
1804
1923
|
}
|
|
@@ -2165,12 +2284,15 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2165
2284
|
let ok = 0;
|
|
2166
2285
|
let disabled = 0;
|
|
2167
2286
|
let errors = 0;
|
|
2287
|
+
const maskEmailEnabled = getCodexTuiMaskEmail(loadPluginConfig());
|
|
2168
2288
|
console.log(`\nChecking ${deepProbe ? "full account health" : "quotas"} for all accounts...\n`);
|
|
2169
2289
|
for (let i = 0; i < total; i += 1) {
|
|
2170
2290
|
const account = workingStorage.accounts[i];
|
|
2171
2291
|
if (!account)
|
|
2172
2292
|
continue;
|
|
2173
|
-
const label = account.
|
|
2293
|
+
const label = account.accountLabel?.trim() ||
|
|
2294
|
+
resolveDisplayEmail(account.email, maskEmailEnabled) ||
|
|
2295
|
+
`Account ${i + 1}`;
|
|
2174
2296
|
if (account.enabled === false) {
|
|
2175
2297
|
disabled += 1;
|
|
2176
2298
|
console.log(`[${i + 1}/${total}] ${label}: DISABLED`);
|
|
@@ -2343,7 +2465,45 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2343
2465
|
storageChanged = true;
|
|
2344
2466
|
}
|
|
2345
2467
|
if (storageChanged) {
|
|
2346
|
-
|
|
2468
|
+
// Persist under the storage lock against a fresh snapshot so
|
|
2469
|
+
// concurrent saves during the (long) health-check network loop
|
|
2470
|
+
// are not clobbered. Re-apply this run's per-account quota/state
|
|
2471
|
+
// updates by workspace identity and re-apply removals.
|
|
2472
|
+
const workingByIdentity = new Map(workingStorage.accounts.map((account) => [
|
|
2473
|
+
getWorkspaceIdentityKey(account),
|
|
2474
|
+
account,
|
|
2475
|
+
]));
|
|
2476
|
+
await withAccountStorageTransaction(async (current, persist) => {
|
|
2477
|
+
if (!current) {
|
|
2478
|
+
// No on-disk state to merge into; fall back to the working copy.
|
|
2479
|
+
await persist(workingStorage);
|
|
2480
|
+
return;
|
|
2481
|
+
}
|
|
2482
|
+
const merged = [];
|
|
2483
|
+
for (const acc of current.accounts) {
|
|
2484
|
+
const identity = getWorkspaceIdentityKey(acc);
|
|
2485
|
+
if (removeFromActive.has(identity))
|
|
2486
|
+
continue;
|
|
2487
|
+
const updated = workingByIdentity.get(identity);
|
|
2488
|
+
if (updated) {
|
|
2489
|
+
// Carry forward ONLY the token/identity fields this health
|
|
2490
|
+
// check refreshes. Labels, tags, notes, and rate-limit /
|
|
2491
|
+
// cooldown state stay as the fresh snapshot has them so a
|
|
2492
|
+
// concurrent edit during the network loop is not reverted.
|
|
2493
|
+
acc.accountId = updated.accountId;
|
|
2494
|
+
acc.accountIdSource = updated.accountIdSource;
|
|
2495
|
+
acc.refreshToken = updated.refreshToken;
|
|
2496
|
+
acc.accessToken = updated.accessToken;
|
|
2497
|
+
acc.expiresAt = updated.expiresAt;
|
|
2498
|
+
acc.oauthScope = updated.oauthScope;
|
|
2499
|
+
acc.email = updated.email;
|
|
2500
|
+
}
|
|
2501
|
+
merged.push(acc);
|
|
2502
|
+
}
|
|
2503
|
+
current.accounts = merged;
|
|
2504
|
+
clampActiveIndices(current);
|
|
2505
|
+
await persist(current);
|
|
2506
|
+
});
|
|
2347
2507
|
invalidateAccountManagerCache();
|
|
2348
2508
|
}
|
|
2349
2509
|
if (flaggedChanged) {
|
|
@@ -2372,13 +2532,16 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2372
2532
|
return;
|
|
2373
2533
|
}
|
|
2374
2534
|
console.log("\nVerifying flagged accounts...\n");
|
|
2535
|
+
const maskEmailEnabled = getCodexTuiMaskEmail(loadPluginConfig());
|
|
2375
2536
|
const remaining = [];
|
|
2376
2537
|
const restored = [];
|
|
2377
2538
|
for (let i = 0; i < flaggedStorage.accounts.length; i += 1) {
|
|
2378
2539
|
const flagged = flaggedStorage.accounts[i];
|
|
2379
2540
|
if (!flagged)
|
|
2380
2541
|
continue;
|
|
2381
|
-
const label = flagged.
|
|
2542
|
+
const label = flagged.accountLabel?.trim() ||
|
|
2543
|
+
resolveDisplayEmail(flagged.email, maskEmailEnabled) ||
|
|
2544
|
+
`Flagged ${i + 1}`;
|
|
2382
2545
|
if (flagged.flaggedReason === "workspace-deactivated") {
|
|
2383
2546
|
console.log(`[${i + 1}/${flaggedStorage.accounts.length}] ${label}: STILL FLAGGED (workspace deactivated)`);
|
|
2384
2547
|
remaining.push(flagged);
|
|
@@ -2494,8 +2657,10 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2494
2657
|
enabled: account.enabled !== false,
|
|
2495
2658
|
};
|
|
2496
2659
|
});
|
|
2660
|
+
const maskEmailEnabled = getCodexTuiMaskEmail(loadPluginConfig());
|
|
2497
2661
|
const menuResult = await promptLoginMode(existingAccounts, {
|
|
2498
2662
|
flaggedCount: flaggedStorage.accounts.length,
|
|
2663
|
+
maskEmail: maskEmailEnabled,
|
|
2499
2664
|
});
|
|
2500
2665
|
if (menuResult.mode === "cancel") {
|
|
2501
2666
|
return {
|
|
@@ -2531,7 +2696,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2531
2696
|
accounts: flaggedStorage.accounts.filter((flagged) => !matchesWorkspaceIdentity(flagged, getWorkspaceIdentityKey(target))),
|
|
2532
2697
|
});
|
|
2533
2698
|
invalidateAccountManagerCache();
|
|
2534
|
-
console.log(`\nDeleted ${target.email ?? `Account ${menuResult.deleteAccountIndex + 1}`}.\n`);
|
|
2699
|
+
console.log(`\nDeleted ${resolveDisplayEmail(target.email, maskEmailEnabled) ?? `Account ${menuResult.deleteAccountIndex + 1}`}.\n`);
|
|
2535
2700
|
}
|
|
2536
2701
|
continue;
|
|
2537
2702
|
}
|
|
@@ -2541,7 +2706,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2541
2706
|
target.enabled = target.enabled === false ? true : false;
|
|
2542
2707
|
await saveAccounts(workingStorage);
|
|
2543
2708
|
invalidateAccountManagerCache();
|
|
2544
|
-
console.log(`\n${target.email ?? `Account ${menuResult.toggleAccountIndex + 1}`} ${target.enabled === false ? "disabled" : "enabled"}.\n`);
|
|
2709
|
+
console.log(`\n${resolveDisplayEmail(target.email, maskEmailEnabled) ?? `Account ${menuResult.toggleAccountIndex + 1}`} ${target.enabled === false ? "disabled" : "enabled"}.\n`);
|
|
2545
2710
|
}
|
|
2546
2711
|
continue;
|
|
2547
2712
|
}
|