oc-auth-switcher 0.7.1 → 0.8.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 +1 -1
- package/dist/cli.js +57 -39
- package/dist/index.js +163 -71
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ oc-auth-switcher <command> [options]
|
|
|
47
47
|
|---------|-------------|
|
|
48
48
|
| `add [name]` | Add a new account via OAuth |
|
|
49
49
|
| `reauth <name>` | Re-authenticate an existing account |
|
|
50
|
-
| `usage [--watch]` | Show utilization dashboard with progress bars |
|
|
50
|
+
| `usage [--watch]` | Show utilization dashboard with progress bars and reset countdowns |
|
|
51
51
|
| `config [options]` | View/modify thresholds |
|
|
52
52
|
| `switch <name>` | Set the active account |
|
|
53
53
|
| `status` | Show current active account and rotation state |
|
package/dist/cli.js
CHANGED
|
@@ -21,6 +21,14 @@ var METRIC_MODEL_FAMILY = {
|
|
|
21
21
|
weekly7dSonnet: "sonnet",
|
|
22
22
|
weekly7dFable: "fable"
|
|
23
23
|
};
|
|
24
|
+
var UNIFIED_RATE_LIMIT_PREFIX = "anthropic-ratelimit-unified-";
|
|
25
|
+
var QUOTA_WINDOW_SCOPE_PATTERN = /^\d+[hmd](?:_[a-z0-9.\-]+)?$/i;
|
|
26
|
+
function quotaWindowScope(prefix) {
|
|
27
|
+
if (!prefix)
|
|
28
|
+
return;
|
|
29
|
+
const scope = prefix.toLowerCase().startsWith(UNIFIED_RATE_LIMIT_PREFIX) ? prefix.slice(UNIFIED_RATE_LIMIT_PREFIX.length) : prefix;
|
|
30
|
+
return QUOTA_WINDOW_SCOPE_PATTERN.test(scope) ? scope : undefined;
|
|
31
|
+
}
|
|
24
32
|
|
|
25
33
|
// node_modules/@ex-machina/opencode-anthropic-auth/dist/constants.js
|
|
26
34
|
var CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
|
@@ -40,6 +48,11 @@ var OAUTH_SCOPES = [
|
|
|
40
48
|
];
|
|
41
49
|
|
|
42
50
|
// src/accounts.ts
|
|
51
|
+
function isAccessTokenFresh(account, nowMs = Date.now()) {
|
|
52
|
+
if (account.expires == null || Number.isNaN(account.expires))
|
|
53
|
+
return true;
|
|
54
|
+
return !!account.access && account.expires > nowMs;
|
|
55
|
+
}
|
|
43
56
|
function normalizeAccount(raw) {
|
|
44
57
|
const name = raw.name || "unnamed";
|
|
45
58
|
const access = raw.access || raw.accessToken || "";
|
|
@@ -217,17 +230,20 @@ function normalizeState(raw) {
|
|
|
217
230
|
weekly7dSonnet: threshold.weekly7dSonnet ?? DEFAULT_THRESHOLD,
|
|
218
231
|
weekly7dFable: threshold.weekly7dFable ?? DEFAULT_THRESHOLD
|
|
219
232
|
} : threshold ?? defaults.config.threshold;
|
|
220
|
-
const usage = Object.fromEntries(Object.entries(raw.usage ?? {}).map(([name, accountUsage]) =>
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
233
|
+
const usage = Object.fromEntries(Object.entries(raw.usage ?? {}).map(([name, accountUsage]) => {
|
|
234
|
+
const rejected = { ...EMPTY_METRIC, ...accountUsage?.rejected };
|
|
235
|
+
return [
|
|
236
|
+
name,
|
|
237
|
+
{
|
|
238
|
+
session5h: { ...EMPTY_METRIC, ...accountUsage?.session5h },
|
|
239
|
+
weekly7d: { ...EMPTY_METRIC, ...accountUsage?.weekly7d },
|
|
240
|
+
weekly7dSonnet: { ...EMPTY_METRIC, ...accountUsage?.weekly7dSonnet },
|
|
241
|
+
weekly7dFable: { ...EMPTY_METRIC, ...accountUsage?.weekly7dFable },
|
|
242
|
+
rejected: rejected.status?.toLowerCase() === "rejected" && !quotaWindowScope(rejected.prefix) ? { ...EMPTY_METRIC } : rejected,
|
|
243
|
+
timestamp: accountUsage?.timestamp
|
|
244
|
+
}
|
|
245
|
+
];
|
|
246
|
+
}));
|
|
231
247
|
return {
|
|
232
248
|
currentAccount: raw.currentAccount ?? defaults.currentAccount,
|
|
233
249
|
requestCount: raw.requestCount ?? defaults.requestCount,
|
|
@@ -238,8 +254,8 @@ function normalizeState(raw) {
|
|
|
238
254
|
authFailures: raw.authFailures ?? defaults.authFailures
|
|
239
255
|
};
|
|
240
256
|
}
|
|
241
|
-
function loadState() {
|
|
242
|
-
return normalizeState(safeReadJSON(
|
|
257
|
+
function loadState(stateFile = STATE_FILE) {
|
|
258
|
+
return normalizeState(safeReadJSON(stateFile, {}));
|
|
243
259
|
}
|
|
244
260
|
function saveState(state) {
|
|
245
261
|
return safeWriteJSON(STATE_FILE, state);
|
|
@@ -360,7 +376,7 @@ function purgeExpiredCooldowns(state) {
|
|
|
360
376
|
}
|
|
361
377
|
}
|
|
362
378
|
}
|
|
363
|
-
function
|
|
379
|
+
function pickByScore(candidates, state, exclude, modelFamily, predicate) {
|
|
364
380
|
let best = null;
|
|
365
381
|
let bestScore = Infinity;
|
|
366
382
|
for (const acct of candidates) {
|
|
@@ -368,22 +384,7 @@ function findBestAvailable(candidates, state, exclude, modelFamily) {
|
|
|
368
384
|
continue;
|
|
369
385
|
if (isTemporarilyUnavailable(state, acct.name))
|
|
370
386
|
continue;
|
|
371
|
-
if (!
|
|
372
|
-
const score = getUtilizationScore(state.usage[acct.name], state, modelFamily);
|
|
373
|
-
if (!best || score < bestScore) {
|
|
374
|
-
bestScore = score;
|
|
375
|
-
best = acct;
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
if (best)
|
|
380
|
-
return best;
|
|
381
|
-
best = null;
|
|
382
|
-
bestScore = Infinity;
|
|
383
|
-
for (const acct of candidates) {
|
|
384
|
-
if (exclude.has(acct.name))
|
|
385
|
-
continue;
|
|
386
|
-
if (isTemporarilyUnavailable(state, acct.name))
|
|
387
|
+
if (!predicate(acct))
|
|
387
388
|
continue;
|
|
388
389
|
const score = getUtilizationScore(state.usage[acct.name], state, modelFamily);
|
|
389
390
|
if (!best || score < bestScore) {
|
|
@@ -393,6 +394,12 @@ function findBestAvailable(candidates, state, exclude, modelFamily) {
|
|
|
393
394
|
}
|
|
394
395
|
return best;
|
|
395
396
|
}
|
|
397
|
+
function findBestAvailable(candidates, state, exclude, modelFamily) {
|
|
398
|
+
const underThreshold = (acct) => !isOverThreshold(state.usage[acct.name], state, modelFamily);
|
|
399
|
+
const fresh = (acct) => isAccessTokenFresh(acct);
|
|
400
|
+
const stale = (acct) => !isAccessTokenFresh(acct);
|
|
401
|
+
return pickByScore(candidates, state, exclude, modelFamily, (acct) => underThreshold(acct) && fresh(acct)) ?? pickByScore(candidates, state, exclude, modelFamily, (acct) => underThreshold(acct) && stale(acct)) ?? pickByScore(candidates, state, exclude, modelFamily, fresh) ?? pickByScore(candidates, state, exclude, modelFamily, stale);
|
|
402
|
+
}
|
|
396
403
|
function selectAccount(accounts, state, model) {
|
|
397
404
|
if (accounts.length === 0) {
|
|
398
405
|
throw new Error("No accounts available");
|
|
@@ -461,6 +468,10 @@ function formatRelativeDuration(targetMs, nowMs = Date.now()) {
|
|
|
461
468
|
parts.push(`${Math.max(1, Math.round(remaining))}s`);
|
|
462
469
|
return difference >= 0 ? `in ${parts.join(" ")}` : `${parts.join(" ")} ago`;
|
|
463
470
|
}
|
|
471
|
+
function formatResetTime(reset, nowMs = Date.now()) {
|
|
472
|
+
const resetMs = reset * 1000;
|
|
473
|
+
return `${new Date(resetMs).toLocaleString()} (${formatRelativeDuration(resetMs, nowMs)})`;
|
|
474
|
+
}
|
|
464
475
|
function displayRejectionPrefix(prefix) {
|
|
465
476
|
return prefix?.replace(/^anthropic-ratelimit-unified-/i, "");
|
|
466
477
|
}
|
|
@@ -677,6 +688,11 @@ function progressBar(value, threshold, width = 30) {
|
|
|
677
688
|
const label = `${(value * 100).toFixed(1)}%`;
|
|
678
689
|
return `${bar} ${color}${label}${RESET}`;
|
|
679
690
|
}
|
|
691
|
+
function resetSuffix(metric, now) {
|
|
692
|
+
if (!metric.reset || metric.reset * 1000 <= now)
|
|
693
|
+
return "";
|
|
694
|
+
return ` ${DIM}resets ${formatRelativeDuration(metric.reset * 1000, now)}${RESET}`;
|
|
695
|
+
}
|
|
680
696
|
function tryCopy(cmd, args, text) {
|
|
681
697
|
return new Promise((resolve) => {
|
|
682
698
|
try {
|
|
@@ -865,17 +881,20 @@ ${BOLD}${CYAN}=== Auth Switcher Usage Dashboard ===${RESET}
|
|
|
865
881
|
}
|
|
866
882
|
for (const account of data.accounts) {
|
|
867
883
|
const usage = state.usage[account.name];
|
|
884
|
+
const now = Date.now();
|
|
868
885
|
const isActive = account.name === state.currentAccount;
|
|
869
|
-
const isCooling = !!state.authFailures[account.name] && state.authFailures[account.name] >
|
|
886
|
+
const isCooling = !!state.authFailures[account.name] && state.authFailures[account.name] > now;
|
|
870
887
|
const tag = isActive ? `${GREEN} [ACTIVE]${RESET}` : isCooling ? `${RED} [COOLDOWN]${RESET}` : "";
|
|
871
888
|
console.log(` ${BOLD}${account.name}${RESET}${tag}`);
|
|
872
889
|
if (usage) {
|
|
873
|
-
console.log(` 5h session: ${progressBar(usage.session5h.utilization, thresholds.session5h)}`);
|
|
874
|
-
console.log(` 7d weekly: ${progressBar(usage.weekly7d.utilization, thresholds.weekly7d)}`);
|
|
875
|
-
console.log(` 7d sonnet: ${progressBar(usage.weekly7dSonnet.utilization, thresholds.weekly7dSonnet)}`);
|
|
876
|
-
console.log(` 7d fable: ${progressBar(usage.weekly7dFable.utilization, thresholds.weekly7dFable)}`);
|
|
877
|
-
|
|
878
|
-
|
|
890
|
+
console.log(` 5h session: ${progressBar(usage.session5h.utilization, thresholds.session5h)}${resetSuffix(usage.session5h, now)}`);
|
|
891
|
+
console.log(` 7d weekly: ${progressBar(usage.weekly7d.utilization, thresholds.weekly7d)}${resetSuffix(usage.weekly7d, now)}`);
|
|
892
|
+
console.log(` 7d sonnet: ${progressBar(usage.weekly7dSonnet.utilization, thresholds.weekly7dSonnet)}${resetSuffix(usage.weekly7dSonnet, now)}`);
|
|
893
|
+
console.log(` 7d fable: ${progressBar(usage.weekly7dFable.utilization, thresholds.weekly7dFable)}${resetSuffix(usage.weekly7dFable, now)}`);
|
|
894
|
+
const rejection = deriveAccountHealth(account, usage, thresholds, state.authFailures[account.name], now).reasons.find((reason) => reason.kind === "rejection");
|
|
895
|
+
if (rejection) {
|
|
896
|
+
const reset = rejection.reset ? ` \u2014 resets ${formatResetTime(rejection.reset, now)}` : "";
|
|
897
|
+
console.log(` ${RED}Rate limit status: ${rejection.message}${reset}${RESET}`);
|
|
879
898
|
}
|
|
880
899
|
if (usage.timestamp) {
|
|
881
900
|
console.log(` ${DIM}Last updated: ${usage.timestamp}${RESET}`);
|
|
@@ -1067,8 +1086,7 @@ ${BOLD}${CYAN}=== Auth Switcher Status ===${RESET}
|
|
|
1067
1086
|
console.log(` ${RED}Reason: ${reason.message}${RESET}`);
|
|
1068
1087
|
}
|
|
1069
1088
|
if (health.earliestReset) {
|
|
1070
|
-
|
|
1071
|
-
console.log(` Reset: ${new Date(resetMs).toLocaleString()} (${formatRelativeDuration(resetMs, now)})`);
|
|
1089
|
+
console.log(` Reset: ${formatResetTime(health.earliestReset, now)}`);
|
|
1072
1090
|
}
|
|
1073
1091
|
const tokenColor = health.reasons.some((reason) => reason.kind === "token") ? RED : DIM;
|
|
1074
1092
|
console.log(` ${tokenColor}Token: ${health.tokenExpiry}${RESET}`);
|
package/dist/index.js
CHANGED
|
@@ -423,8 +423,21 @@ var METRIC_MODEL_FAMILY = {
|
|
|
423
423
|
weekly7dSonnet: "sonnet",
|
|
424
424
|
weekly7dFable: "fable"
|
|
425
425
|
};
|
|
426
|
+
var UNIFIED_RATE_LIMIT_PREFIX = "anthropic-ratelimit-unified-";
|
|
427
|
+
var QUOTA_WINDOW_SCOPE_PATTERN = /^\d+[hmd](?:_[a-z0-9.\-]+)?$/i;
|
|
428
|
+
function quotaWindowScope(prefix) {
|
|
429
|
+
if (!prefix)
|
|
430
|
+
return;
|
|
431
|
+
const scope = prefix.toLowerCase().startsWith(UNIFIED_RATE_LIMIT_PREFIX) ? prefix.slice(UNIFIED_RATE_LIMIT_PREFIX.length) : prefix;
|
|
432
|
+
return QUOTA_WINDOW_SCOPE_PATTERN.test(scope) ? scope : undefined;
|
|
433
|
+
}
|
|
426
434
|
|
|
427
435
|
// src/accounts.ts
|
|
436
|
+
function isAccessTokenFresh(account, nowMs = Date.now()) {
|
|
437
|
+
if (account.expires == null || Number.isNaN(account.expires))
|
|
438
|
+
return true;
|
|
439
|
+
return !!account.access && account.expires > nowMs;
|
|
440
|
+
}
|
|
428
441
|
function normalizeAccount(raw) {
|
|
429
442
|
const name = raw.name || "unnamed";
|
|
430
443
|
const access = raw.access || raw.accessToken || "";
|
|
@@ -585,17 +598,20 @@ function normalizeState(raw) {
|
|
|
585
598
|
weekly7dSonnet: threshold.weekly7dSonnet ?? DEFAULT_THRESHOLD,
|
|
586
599
|
weekly7dFable: threshold.weekly7dFable ?? DEFAULT_THRESHOLD
|
|
587
600
|
} : threshold ?? defaults.config.threshold;
|
|
588
|
-
const usage = Object.fromEntries(Object.entries(raw.usage ?? {}).map(([name, accountUsage]) =>
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
601
|
+
const usage = Object.fromEntries(Object.entries(raw.usage ?? {}).map(([name, accountUsage]) => {
|
|
602
|
+
const rejected = { ...EMPTY_METRIC, ...accountUsage?.rejected };
|
|
603
|
+
return [
|
|
604
|
+
name,
|
|
605
|
+
{
|
|
606
|
+
session5h: { ...EMPTY_METRIC, ...accountUsage?.session5h },
|
|
607
|
+
weekly7d: { ...EMPTY_METRIC, ...accountUsage?.weekly7d },
|
|
608
|
+
weekly7dSonnet: { ...EMPTY_METRIC, ...accountUsage?.weekly7dSonnet },
|
|
609
|
+
weekly7dFable: { ...EMPTY_METRIC, ...accountUsage?.weekly7dFable },
|
|
610
|
+
rejected: rejected.status?.toLowerCase() === "rejected" && !quotaWindowScope(rejected.prefix) ? { ...EMPTY_METRIC } : rejected,
|
|
611
|
+
timestamp: accountUsage?.timestamp
|
|
612
|
+
}
|
|
613
|
+
];
|
|
614
|
+
}));
|
|
599
615
|
return {
|
|
600
616
|
currentAccount: raw.currentAccount ?? defaults.currentAccount,
|
|
601
617
|
requestCount: raw.requestCount ?? defaults.requestCount,
|
|
@@ -606,8 +622,8 @@ function normalizeState(raw) {
|
|
|
606
622
|
authFailures: raw.authFailures ?? defaults.authFailures
|
|
607
623
|
};
|
|
608
624
|
}
|
|
609
|
-
function loadState() {
|
|
610
|
-
return normalizeState(safeReadJSON(
|
|
625
|
+
function loadState(stateFile = STATE_FILE) {
|
|
626
|
+
return normalizeState(safeReadJSON(stateFile, {}));
|
|
611
627
|
}
|
|
612
628
|
function saveState(state) {
|
|
613
629
|
return safeWriteJSON(STATE_FILE, state);
|
|
@@ -727,7 +743,8 @@ function updateUsageFromHeaders(state, accountName, headers) {
|
|
|
727
743
|
}
|
|
728
744
|
for (const [headerName, headerValue] of headers.entries()) {
|
|
729
745
|
const normalizedName = headerName.toLowerCase();
|
|
730
|
-
|
|
746
|
+
const statusMatch = normalizedName.match(/^anthropic-ratelimit-unified-(.+)-status$/);
|
|
747
|
+
if (!statusMatch || !quotaWindowScope(statusMatch[1]) || headerValue.toLowerCase() !== "rejected") {
|
|
731
748
|
continue;
|
|
732
749
|
}
|
|
733
750
|
const prefix = normalizedName.slice(0, -"-status".length);
|
|
@@ -820,7 +837,7 @@ function purgeExpiredCooldowns(state) {
|
|
|
820
837
|
}
|
|
821
838
|
}
|
|
822
839
|
}
|
|
823
|
-
function
|
|
840
|
+
function pickByScore(candidates, state, exclude, modelFamily, predicate) {
|
|
824
841
|
let best = null;
|
|
825
842
|
let bestScore = Infinity;
|
|
826
843
|
for (const acct of candidates) {
|
|
@@ -828,22 +845,7 @@ function findBestAvailable(candidates, state, exclude, modelFamily) {
|
|
|
828
845
|
continue;
|
|
829
846
|
if (isTemporarilyUnavailable(state, acct.name))
|
|
830
847
|
continue;
|
|
831
|
-
if (!
|
|
832
|
-
const score = getUtilizationScore(state.usage[acct.name], state, modelFamily);
|
|
833
|
-
if (!best || score < bestScore) {
|
|
834
|
-
bestScore = score;
|
|
835
|
-
best = acct;
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
if (best)
|
|
840
|
-
return best;
|
|
841
|
-
best = null;
|
|
842
|
-
bestScore = Infinity;
|
|
843
|
-
for (const acct of candidates) {
|
|
844
|
-
if (exclude.has(acct.name))
|
|
845
|
-
continue;
|
|
846
|
-
if (isTemporarilyUnavailable(state, acct.name))
|
|
848
|
+
if (!predicate(acct))
|
|
847
849
|
continue;
|
|
848
850
|
const score = getUtilizationScore(state.usage[acct.name], state, modelFamily);
|
|
849
851
|
if (!best || score < bestScore) {
|
|
@@ -853,6 +855,12 @@ function findBestAvailable(candidates, state, exclude, modelFamily) {
|
|
|
853
855
|
}
|
|
854
856
|
return best;
|
|
855
857
|
}
|
|
858
|
+
function findBestAvailable(candidates, state, exclude, modelFamily) {
|
|
859
|
+
const underThreshold = (acct) => !isOverThreshold(state.usage[acct.name], state, modelFamily);
|
|
860
|
+
const fresh = (acct) => isAccessTokenFresh(acct);
|
|
861
|
+
const stale = (acct) => !isAccessTokenFresh(acct);
|
|
862
|
+
return pickByScore(candidates, state, exclude, modelFamily, (acct) => underThreshold(acct) && fresh(acct)) ?? pickByScore(candidates, state, exclude, modelFamily, (acct) => underThreshold(acct) && stale(acct)) ?? pickByScore(candidates, state, exclude, modelFamily, fresh) ?? pickByScore(candidates, state, exclude, modelFamily, stale);
|
|
863
|
+
}
|
|
856
864
|
function selectAccount(accounts, state, model) {
|
|
857
865
|
if (accounts.length === 0) {
|
|
858
866
|
throw new Error("No accounts available");
|
|
@@ -894,6 +902,96 @@ function clearAuthFailure(state, accountName) {
|
|
|
894
902
|
delete state.authFailures[accountName];
|
|
895
903
|
}
|
|
896
904
|
|
|
905
|
+
// src/retry.ts
|
|
906
|
+
var RATE_LIMIT_STATUSES = new Set([429]);
|
|
907
|
+
function isReplayableBody(body) {
|
|
908
|
+
return body == null || typeof body === "string";
|
|
909
|
+
}
|
|
910
|
+
function isRateLimitStatus(status) {
|
|
911
|
+
return RATE_LIMIT_STATUSES.has(status);
|
|
912
|
+
}
|
|
913
|
+
function finalizeRequestAccounting(state, accountName, response) {
|
|
914
|
+
updateUsageFromHeaders(state, accountName, response.headers);
|
|
915
|
+
if (response.ok) {
|
|
916
|
+
clearAuthFailure(state, accountName);
|
|
917
|
+
state.requestCount = (state.requestCount || 0) + 1;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
async function retryAcrossAccounts(initial, account, options) {
|
|
921
|
+
const { accounts, state, model, bodyReplayable, attemptedAccounts, send, log } = options;
|
|
922
|
+
attemptedAccounts.add(account.name);
|
|
923
|
+
let response = initial;
|
|
924
|
+
let current = account;
|
|
925
|
+
let authRetryUsed = false;
|
|
926
|
+
const maxFetches = Math.max(accounts.length, 1) + 1;
|
|
927
|
+
let fetches = 1;
|
|
928
|
+
while (fetches < maxFetches) {
|
|
929
|
+
if (!bodyReplayable)
|
|
930
|
+
break;
|
|
931
|
+
if (isRateLimitStatus(response.status)) {
|
|
932
|
+
updateUsageFromHeaders(state, current.name, response.headers);
|
|
933
|
+
attemptedAccounts.add(current.name);
|
|
934
|
+
const available = accounts.filter((candidate) => !attemptedAccounts.has(candidate.name));
|
|
935
|
+
if (available.length === 0)
|
|
936
|
+
break;
|
|
937
|
+
const next = selectAccount(available, state, model).account;
|
|
938
|
+
if (next.name === current.name || attemptedAccounts.has(next.name))
|
|
939
|
+
break;
|
|
940
|
+
await log(`${current.name} rate-limited (HTTP ${response.status}) — retrying on ${next.name}`);
|
|
941
|
+
const prepared = await ensureFreshToken(next, state);
|
|
942
|
+
if (!prepared) {
|
|
943
|
+
attemptedAccounts.add(next.name);
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
current = prepared;
|
|
947
|
+
response = await send(current);
|
|
948
|
+
fetches += 1;
|
|
949
|
+
attemptedAccounts.add(current.name);
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
if ((response.status === 401 || response.status === 403) && !authRetryUsed) {
|
|
953
|
+
const errorBody = await response.clone().text().catch(() => "");
|
|
954
|
+
const isScopeError = errorBody.includes("scope") || errorBody.includes("unauthorized") || errorBody.includes("invalid");
|
|
955
|
+
if (!isScopeError)
|
|
956
|
+
break;
|
|
957
|
+
markAuthFailure(state, current.name);
|
|
958
|
+
attemptedAccounts.add(current.name);
|
|
959
|
+
authRetryUsed = true;
|
|
960
|
+
const available = accounts.filter((candidate) => !attemptedAccounts.has(candidate.name) && (!state.authFailures[candidate.name] || state.authFailures[candidate.name] <= Date.now()));
|
|
961
|
+
if (available.length === 0)
|
|
962
|
+
break;
|
|
963
|
+
const next = selectAccount(available, state, model).account;
|
|
964
|
+
if (next.name === current.name)
|
|
965
|
+
break;
|
|
966
|
+
await log(`${current.name} auth failure (HTTP ${response.status}) — retrying on ${next.name}`);
|
|
967
|
+
const prepared = await ensureFreshToken(next, state);
|
|
968
|
+
if (!prepared)
|
|
969
|
+
break;
|
|
970
|
+
current = prepared;
|
|
971
|
+
response = await send(current);
|
|
972
|
+
fetches += 1;
|
|
973
|
+
attemptedAccounts.add(current.name);
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
break;
|
|
977
|
+
}
|
|
978
|
+
return { response, account: current };
|
|
979
|
+
}
|
|
980
|
+
async function ensureFreshToken(account, state) {
|
|
981
|
+
if (isAccessTokenFresh(account))
|
|
982
|
+
return account;
|
|
983
|
+
const result = await refreshAccountToken(account.refresh);
|
|
984
|
+
if (result.ok && result.access && result.refresh && result.expires) {
|
|
985
|
+
account.access = result.access;
|
|
986
|
+
account.refresh = result.refresh;
|
|
987
|
+
account.expires = result.expires;
|
|
988
|
+
await updateAccountTokens(account.name, result.access, result.refresh, result.expires);
|
|
989
|
+
return account;
|
|
990
|
+
}
|
|
991
|
+
markAuthFailure(state, account.name);
|
|
992
|
+
return null;
|
|
993
|
+
}
|
|
994
|
+
|
|
897
995
|
// src/index.ts
|
|
898
996
|
function selectionSnapshot(state) {
|
|
899
997
|
return {
|
|
@@ -1002,7 +1100,7 @@ var AuthSwitcherPlugin = async ({ client }) => {
|
|
|
1002
1100
|
const attemptedAccounts = new Set;
|
|
1003
1101
|
while (true) {
|
|
1004
1102
|
attemptedAccounts.add(account.name);
|
|
1005
|
-
if (!account
|
|
1103
|
+
if (!isAccessTokenFresh(account)) {
|
|
1006
1104
|
const result = await refreshAccountToken(account.refresh);
|
|
1007
1105
|
if (result.ok && result.access && result.refresh && result.expires) {
|
|
1008
1106
|
account.access = result.access;
|
|
@@ -1031,6 +1129,19 @@ var AuthSwitcherPlugin = async ({ client }) => {
|
|
|
1031
1129
|
body = rewriteRequestBody(body);
|
|
1032
1130
|
}
|
|
1033
1131
|
const rewritten = rewriteUrl(input);
|
|
1132
|
+
const bodyReplayable = isReplayableBody(init?.body);
|
|
1133
|
+
const sendWithAccount = async (acct) => {
|
|
1134
|
+
const headers = mergeHeaders(input, init);
|
|
1135
|
+
setOAuthHeaders(headers, acct.access);
|
|
1136
|
+
return fetch(rewritten.input, {
|
|
1137
|
+
...init,
|
|
1138
|
+
body,
|
|
1139
|
+
headers,
|
|
1140
|
+
...isInsecure() && {
|
|
1141
|
+
tls: { rejectUnauthorized: false }
|
|
1142
|
+
}
|
|
1143
|
+
});
|
|
1144
|
+
};
|
|
1034
1145
|
const response = await fetch(rewritten.input, {
|
|
1035
1146
|
...init,
|
|
1036
1147
|
body,
|
|
@@ -1039,48 +1150,29 @@ var AuthSwitcherPlugin = async ({ client }) => {
|
|
|
1039
1150
|
tls: { rejectUnauthorized: false }
|
|
1040
1151
|
}
|
|
1041
1152
|
});
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
if (result.ok && result.access && result.refresh && result.expires) {
|
|
1056
|
-
next.access = result.access;
|
|
1057
|
-
next.refresh = result.refresh;
|
|
1058
|
-
next.expires = result.expires;
|
|
1059
|
-
await updateAccountTokens(next.name, result.access, result.refresh, result.expires);
|
|
1060
|
-
}
|
|
1153
|
+
const retried = await retryAcrossAccounts(response, account, {
|
|
1154
|
+
accounts,
|
|
1155
|
+
state,
|
|
1156
|
+
model,
|
|
1157
|
+
bodyReplayable,
|
|
1158
|
+
attemptedAccounts,
|
|
1159
|
+
send: sendWithAccount,
|
|
1160
|
+
log: async (message) => {
|
|
1161
|
+
await client.app.log({
|
|
1162
|
+
body: {
|
|
1163
|
+
service: "oc-auth-switcher",
|
|
1164
|
+
level: "info",
|
|
1165
|
+
message
|
|
1061
1166
|
}
|
|
1062
|
-
|
|
1063
|
-
setOAuthHeaders(retryHeaders, next.access);
|
|
1064
|
-
const retryResponse = await fetch(rewritten.input, {
|
|
1065
|
-
...init,
|
|
1066
|
-
body,
|
|
1067
|
-
headers: retryHeaders,
|
|
1068
|
-
...isInsecure() && {
|
|
1069
|
-
tls: { rejectUnauthorized: false }
|
|
1070
|
-
}
|
|
1071
|
-
});
|
|
1072
|
-
updateUsageFromHeaders(state, next.name, retryResponse.headers);
|
|
1073
|
-
clearAuthFailure(state, next.name);
|
|
1074
|
-
await saveRequestState(state, initiallyLoadedSelection);
|
|
1075
|
-
return createStrippedStream(retryResponse);
|
|
1076
|
-
}
|
|
1167
|
+
}).catch(() => {});
|
|
1077
1168
|
}
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1169
|
+
});
|
|
1170
|
+
account = retried.account;
|
|
1171
|
+
state.currentAccount = account.name;
|
|
1172
|
+
const finalResponse = retried.response;
|
|
1173
|
+
finalizeRequestAccounting(state, account.name, finalResponse);
|
|
1082
1174
|
await saveRequestState(state, initiallyLoadedSelection);
|
|
1083
|
-
return createStrippedStream(
|
|
1175
|
+
return createStrippedStream(finalResponse);
|
|
1084
1176
|
}
|
|
1085
1177
|
};
|
|
1086
1178
|
},
|