oc-auth-switcher 0.1.1 → 0.2.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 +17 -3
- package/dist/cli.js +130 -24
- package/dist/index.js +94 -85
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ This plugin registers as the Anthropic auth provider for OpenCode. It uses [`@ex
|
|
|
8
8
|
|
|
9
9
|
1. **Multi-account rotation** — maintains a pool of OAuth accounts and selects the best one on each request
|
|
10
10
|
2. **Real-time metric capture** — reads Anthropic's rate-limit response headers on every API call (no manual pinging needed)
|
|
11
|
-
3. **Automatic failover** — when any utilization metric exceeds the threshold (default
|
|
11
|
+
3. **Automatic failover** — when any utilization metric exceeds the threshold (default 95%), the next request automatically uses a different account
|
|
12
12
|
|
|
13
13
|
## Setup
|
|
14
14
|
|
|
@@ -58,8 +58,8 @@ oc-auth-switcher <command> [options]
|
|
|
58
58
|
## Configuration
|
|
59
59
|
|
|
60
60
|
```bash
|
|
61
|
-
# Set uniform threshold (default:
|
|
62
|
-
oc-auth-switcher config --threshold 0.
|
|
61
|
+
# Set uniform threshold (default: 95%)
|
|
62
|
+
oc-auth-switcher config --threshold 0.95
|
|
63
63
|
|
|
64
64
|
# Set per-metric thresholds (5h, 7d, 7d-sonnet)
|
|
65
65
|
oc-auth-switcher config --thresholds 90,80,70
|
|
@@ -89,6 +89,20 @@ Both files use atomic writes with `.bak` fallback for crash safety.
|
|
|
89
89
|
- Auth failures trigger a 1-hour cooldown per account
|
|
90
90
|
- Token refresh is handled automatically with retry and fallback to other accounts
|
|
91
91
|
|
|
92
|
+
## Running the CLI
|
|
93
|
+
|
|
94
|
+
When installed via npm (as part of the OpenCode plugin):
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
npx oc-auth-switcher <command>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
From the project directory (development):
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
bun dist/cli.js <command>
|
|
104
|
+
```
|
|
105
|
+
|
|
92
106
|
## Building from Source
|
|
93
107
|
|
|
94
108
|
```bash
|
package/dist/cli.js
CHANGED
|
@@ -11,9 +11,26 @@ import os from "os";
|
|
|
11
11
|
var configDir = path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "opencode");
|
|
12
12
|
var ACCOUNTS_FILE = path.join(configDir, "auth-switcher-accounts.json");
|
|
13
13
|
var STATE_FILE = path.join(configDir, "auth-switcher-state.json");
|
|
14
|
-
var DEFAULT_THRESHOLD = 0.
|
|
14
|
+
var DEFAULT_THRESHOLD = 0.95;
|
|
15
15
|
var DEFAULT_CHECK_INTERVAL = 60 * 60 * 1000;
|
|
16
|
-
var AUTH_FAILURE_COOLDOWN =
|
|
16
|
+
var AUTH_FAILURE_COOLDOWN = 10 * 60 * 1000;
|
|
17
|
+
|
|
18
|
+
// node_modules/@ex-machina/opencode-anthropic-auth/dist/constants.js
|
|
19
|
+
var CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
|
20
|
+
var AUTHORIZE_URLS = {
|
|
21
|
+
console: "https://platform.claude.com/oauth/authorize",
|
|
22
|
+
max: "https://claude.ai/oauth/authorize"
|
|
23
|
+
};
|
|
24
|
+
var CODE_CALLBACK_URL = "https://platform.claude.com/oauth/code/callback";
|
|
25
|
+
var TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
|
|
26
|
+
var OAUTH_SCOPES = [
|
|
27
|
+
"org:create_api_key",
|
|
28
|
+
"user:profile",
|
|
29
|
+
"user:inference",
|
|
30
|
+
"user:sessions:claude_code",
|
|
31
|
+
"user:mcp_servers",
|
|
32
|
+
"user:file_upload"
|
|
33
|
+
];
|
|
17
34
|
|
|
18
35
|
// src/accounts.ts
|
|
19
36
|
function normalizeAccount(raw) {
|
|
@@ -86,6 +103,49 @@ function removeAccount(name) {
|
|
|
86
103
|
saveAccounts(data);
|
|
87
104
|
return data;
|
|
88
105
|
}
|
|
106
|
+
function updateAccountTokens(name, access, refresh, expires) {
|
|
107
|
+
const data = loadAccounts();
|
|
108
|
+
const account = data.accounts.find((a) => a.name === name);
|
|
109
|
+
if (account) {
|
|
110
|
+
account.access = access;
|
|
111
|
+
account.refresh = refresh;
|
|
112
|
+
account.expires = expires;
|
|
113
|
+
saveAccounts(data);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function refreshAccountToken(refreshTokenValue) {
|
|
117
|
+
try {
|
|
118
|
+
const response = await fetch(TOKEN_URL, {
|
|
119
|
+
method: "POST",
|
|
120
|
+
headers: {
|
|
121
|
+
"Content-Type": "application/json",
|
|
122
|
+
Accept: "application/json, text/plain, */*",
|
|
123
|
+
"User-Agent": "axios/1.13.6"
|
|
124
|
+
},
|
|
125
|
+
body: JSON.stringify({
|
|
126
|
+
grant_type: "refresh_token",
|
|
127
|
+
refresh_token: refreshTokenValue,
|
|
128
|
+
client_id: CLIENT_ID
|
|
129
|
+
})
|
|
130
|
+
});
|
|
131
|
+
if (!response.ok) {
|
|
132
|
+
const body = await response.text().catch(() => "");
|
|
133
|
+
return { ok: false, error: `HTTP ${response.status}: ${body}` };
|
|
134
|
+
}
|
|
135
|
+
const json = await response.json();
|
|
136
|
+
return {
|
|
137
|
+
ok: true,
|
|
138
|
+
access: json.access_token,
|
|
139
|
+
refresh: json.refresh_token,
|
|
140
|
+
expires: Date.now() + json.expires_in * 1000
|
|
141
|
+
};
|
|
142
|
+
} catch (err) {
|
|
143
|
+
return {
|
|
144
|
+
ok: false,
|
|
145
|
+
error: err instanceof Error ? err.message : String(err)
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
89
149
|
|
|
90
150
|
// src/state.ts
|
|
91
151
|
var EMPTY_METRIC = { utilization: 0, reset: 0, status: "" };
|
|
@@ -167,23 +227,6 @@ function clearAuthFailure(state, accountName) {
|
|
|
167
227
|
delete state.authFailures[accountName];
|
|
168
228
|
}
|
|
169
229
|
|
|
170
|
-
// node_modules/@ex-machina/opencode-anthropic-auth/dist/constants.js
|
|
171
|
-
var CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
|
172
|
-
var AUTHORIZE_URLS = {
|
|
173
|
-
console: "https://platform.claude.com/oauth/authorize",
|
|
174
|
-
max: "https://claude.ai/oauth/authorize"
|
|
175
|
-
};
|
|
176
|
-
var CODE_CALLBACK_URL = "https://platform.claude.com/oauth/code/callback";
|
|
177
|
-
var TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
|
|
178
|
-
var OAUTH_SCOPES = [
|
|
179
|
-
"org:create_api_key",
|
|
180
|
-
"user:profile",
|
|
181
|
-
"user:inference",
|
|
182
|
-
"user:sessions:claude_code",
|
|
183
|
-
"user:mcp_servers",
|
|
184
|
-
"user:file_upload"
|
|
185
|
-
];
|
|
186
|
-
|
|
187
230
|
// node_modules/@ex-machina/opencode-anthropic-auth/dist/pkce.js
|
|
188
231
|
function base64UrlEncode(bytes) {
|
|
189
232
|
let bin = "";
|
|
@@ -294,6 +337,7 @@ async function exchange(input, verifier, redirectUri, expectedState) {
|
|
|
294
337
|
}
|
|
295
338
|
|
|
296
339
|
// src/cli.ts
|
|
340
|
+
import { spawn } from "child_process";
|
|
297
341
|
var RESET = "\x1B[0m";
|
|
298
342
|
var BOLD = "\x1B[1m";
|
|
299
343
|
var DIM = "\x1B[2m";
|
|
@@ -315,6 +359,36 @@ function progressBar(value, threshold, width = 30) {
|
|
|
315
359
|
const label = `${(value * 100).toFixed(1)}%`;
|
|
316
360
|
return `${bar} ${color}${label}${RESET}`;
|
|
317
361
|
}
|
|
362
|
+
function tryCopy(cmd, args, text) {
|
|
363
|
+
return new Promise((resolve) => {
|
|
364
|
+
try {
|
|
365
|
+
const child = spawn(cmd, args);
|
|
366
|
+
child.on("error", () => resolve(false));
|
|
367
|
+
child.on("close", (code) => resolve(code === 0));
|
|
368
|
+
child.stdin.on("error", () => resolve(false));
|
|
369
|
+
child.stdin.end(text);
|
|
370
|
+
} catch {
|
|
371
|
+
resolve(false);
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
async function copyToClipboard(text) {
|
|
376
|
+
switch (process.platform) {
|
|
377
|
+
case "darwin":
|
|
378
|
+
return tryCopy("pbcopy", [], text);
|
|
379
|
+
case "win32":
|
|
380
|
+
return tryCopy("clip", [], text);
|
|
381
|
+
default: {
|
|
382
|
+
if (await tryCopy("wl-copy", [], text))
|
|
383
|
+
return true;
|
|
384
|
+
if (await tryCopy("xclip", ["-selection", "clipboard"], text))
|
|
385
|
+
return true;
|
|
386
|
+
if (await tryCopy("xsel", ["--clipboard", "--input"], text))
|
|
387
|
+
return true;
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
318
392
|
async function prompt(question) {
|
|
319
393
|
process.stdout.write(question);
|
|
320
394
|
const buf = [];
|
|
@@ -349,6 +423,10 @@ ${BOLD}Starting OAuth flow for account: ${CYAN}${name}${RESET}
|
|
|
349
423
|
console.log(`${BOLD}Open this URL in your browser:${RESET}
|
|
350
424
|
`);
|
|
351
425
|
console.log(` ${BLUE}${authResult.url}${RESET}
|
|
426
|
+
`);
|
|
427
|
+
const copied = await copyToClipboard(authResult.url);
|
|
428
|
+
if (copied)
|
|
429
|
+
console.log(`${DIM}(URL copied to clipboard)${RESET}
|
|
352
430
|
`);
|
|
353
431
|
console.log(`${DIM}After authorizing, paste the callback URL or code below:${RESET}
|
|
354
432
|
`);
|
|
@@ -406,6 +484,10 @@ ${BOLD}Re-authenticating account: ${CYAN}${name}${RESET}
|
|
|
406
484
|
console.log(`${BOLD}Open this URL in your browser:${RESET}
|
|
407
485
|
`);
|
|
408
486
|
console.log(` ${BLUE}${authResult.url}${RESET}
|
|
487
|
+
`);
|
|
488
|
+
const copied = await copyToClipboard(authResult.url);
|
|
489
|
+
if (copied)
|
|
490
|
+
console.log(`${DIM}(URL copied to clipboard)${RESET}
|
|
409
491
|
`);
|
|
410
492
|
const callbackInput = await prompt(`${CYAN}Callback: ${RESET}`);
|
|
411
493
|
const exchangeResult = await exchange(callbackInput, authResult.verifier, authResult.redirectUri, authResult.state);
|
|
@@ -424,11 +506,27 @@ ${RED}Re-authentication failed${RESET}`);
|
|
|
424
506
|
console.log(`
|
|
425
507
|
${GREEN}Account "${name}" re-authenticated successfully.${RESET}`);
|
|
426
508
|
}
|
|
427
|
-
function
|
|
509
|
+
async function refreshExpiredAccounts(data, state) {
|
|
510
|
+
for (const account of data.accounts) {
|
|
511
|
+
if (account.access && account.expires > Date.now())
|
|
512
|
+
continue;
|
|
513
|
+
const result = await refreshAccountToken(account.refresh);
|
|
514
|
+
if (result.ok && result.access && result.refresh && result.expires) {
|
|
515
|
+
account.access = result.access;
|
|
516
|
+
account.refresh = result.refresh;
|
|
517
|
+
account.expires = result.expires;
|
|
518
|
+
updateAccountTokens(account.name, result.access, result.refresh, result.expires);
|
|
519
|
+
clearAuthFailure(state, account.name);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
async function cmdUsage(args) {
|
|
428
524
|
const watch = args.includes("--watch") || args.includes("-w");
|
|
429
|
-
const showUsage = () => {
|
|
525
|
+
const showUsage = async () => {
|
|
430
526
|
const data = loadAccounts();
|
|
431
527
|
const state = loadState();
|
|
528
|
+
await refreshExpiredAccounts(data, state);
|
|
529
|
+
saveState(state);
|
|
432
530
|
resolveStaleMetrics(state);
|
|
433
531
|
ensureAccountsInState(state, data.accounts.map((a) => a.name));
|
|
434
532
|
const thresholds = getThresholds(state.config);
|
|
@@ -470,11 +568,19 @@ ${BOLD}${CYAN}=== Auth Switcher Usage Dashboard ===${RESET}
|
|
|
470
568
|
console.log();
|
|
471
569
|
}
|
|
472
570
|
};
|
|
473
|
-
showUsage();
|
|
571
|
+
await showUsage();
|
|
474
572
|
if (watch) {
|
|
475
573
|
console.log(`${DIM}Refreshing every 5 seconds. Press Ctrl+C to stop.${RESET}
|
|
476
574
|
`);
|
|
477
|
-
|
|
575
|
+
let running = false;
|
|
576
|
+
setInterval(() => {
|
|
577
|
+
if (running)
|
|
578
|
+
return;
|
|
579
|
+
running = true;
|
|
580
|
+
showUsage().catch((err) => console.error(`${RED}Refresh error:${RESET}`, err)).finally(() => {
|
|
581
|
+
running = false;
|
|
582
|
+
});
|
|
583
|
+
}, 5000);
|
|
478
584
|
new Promise(() => {});
|
|
479
585
|
}
|
|
480
586
|
}
|
|
@@ -654,7 +760,7 @@ async function main() {
|
|
|
654
760
|
break;
|
|
655
761
|
case "usage":
|
|
656
762
|
case "u":
|
|
657
|
-
cmdUsage(commandArgs);
|
|
763
|
+
await cmdUsage(commandArgs);
|
|
658
764
|
break;
|
|
659
765
|
case "config":
|
|
660
766
|
case "c":
|
package/dist/index.js
CHANGED
|
@@ -413,9 +413,9 @@ import os from "node:os";
|
|
|
413
413
|
var configDir = path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "opencode");
|
|
414
414
|
var ACCOUNTS_FILE = path.join(configDir, "auth-switcher-accounts.json");
|
|
415
415
|
var STATE_FILE = path.join(configDir, "auth-switcher-state.json");
|
|
416
|
-
var DEFAULT_THRESHOLD = 0.
|
|
416
|
+
var DEFAULT_THRESHOLD = 0.95;
|
|
417
417
|
var DEFAULT_CHECK_INTERVAL = 60 * 60 * 1000;
|
|
418
|
-
var AUTH_FAILURE_COOLDOWN =
|
|
418
|
+
var AUTH_FAILURE_COOLDOWN = 10 * 60 * 1000;
|
|
419
419
|
|
|
420
420
|
// src/accounts.ts
|
|
421
421
|
function normalizeAccount(raw) {
|
|
@@ -481,6 +481,39 @@ function updateAccountTokens(name, access, refresh, expires) {
|
|
|
481
481
|
saveAccounts(data);
|
|
482
482
|
}
|
|
483
483
|
}
|
|
484
|
+
async function refreshAccountToken(refreshTokenValue) {
|
|
485
|
+
try {
|
|
486
|
+
const response = await fetch(TOKEN_URL, {
|
|
487
|
+
method: "POST",
|
|
488
|
+
headers: {
|
|
489
|
+
"Content-Type": "application/json",
|
|
490
|
+
Accept: "application/json, text/plain, */*",
|
|
491
|
+
"User-Agent": "axios/1.13.6"
|
|
492
|
+
},
|
|
493
|
+
body: JSON.stringify({
|
|
494
|
+
grant_type: "refresh_token",
|
|
495
|
+
refresh_token: refreshTokenValue,
|
|
496
|
+
client_id: CLIENT_ID
|
|
497
|
+
})
|
|
498
|
+
});
|
|
499
|
+
if (!response.ok) {
|
|
500
|
+
const body = await response.text().catch(() => "");
|
|
501
|
+
return { ok: false, error: `HTTP ${response.status}: ${body}` };
|
|
502
|
+
}
|
|
503
|
+
const json = await response.json();
|
|
504
|
+
return {
|
|
505
|
+
ok: true,
|
|
506
|
+
access: json.access_token,
|
|
507
|
+
refresh: json.refresh_token,
|
|
508
|
+
expires: Date.now() + json.expires_in * 1000
|
|
509
|
+
};
|
|
510
|
+
} catch (err) {
|
|
511
|
+
return {
|
|
512
|
+
ok: false,
|
|
513
|
+
error: err instanceof Error ? err.message : String(err)
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
}
|
|
484
517
|
|
|
485
518
|
// src/state.ts
|
|
486
519
|
var EMPTY_METRIC = { utilization: 0, reset: 0, status: "" };
|
|
@@ -659,10 +692,44 @@ function getEarliestReset(usage) {
|
|
|
659
692
|
].filter((r) => r > 0);
|
|
660
693
|
return resets.length > 0 ? Math.min(...resets) * 1000 : 0;
|
|
661
694
|
}
|
|
695
|
+
function purgeExpiredCooldowns(state) {
|
|
696
|
+
const now = Date.now();
|
|
697
|
+
for (const name of Object.keys(state.authFailures)) {
|
|
698
|
+
if (state.authFailures[name] <= now) {
|
|
699
|
+
delete state.authFailures[name];
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
function findBestAvailable(candidates, state, exclude) {
|
|
704
|
+
for (const acct of candidates) {
|
|
705
|
+
if (exclude.has(acct.name))
|
|
706
|
+
continue;
|
|
707
|
+
if (isTemporarilyUnavailable(state, acct.name))
|
|
708
|
+
continue;
|
|
709
|
+
if (!isOverThreshold(state.usage[acct.name], state)) {
|
|
710
|
+
return acct;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
let best = null;
|
|
714
|
+
let bestScore = Infinity;
|
|
715
|
+
for (const acct of candidates) {
|
|
716
|
+
if (exclude.has(acct.name))
|
|
717
|
+
continue;
|
|
718
|
+
if (isTemporarilyUnavailable(state, acct.name))
|
|
719
|
+
continue;
|
|
720
|
+
const score = getUtilizationScore(state.usage[acct.name], state);
|
|
721
|
+
if (score < bestScore) {
|
|
722
|
+
bestScore = score;
|
|
723
|
+
best = acct;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return best;
|
|
727
|
+
}
|
|
662
728
|
function selectAccount(accounts, state) {
|
|
663
729
|
if (accounts.length === 0) {
|
|
664
730
|
throw new Error("No accounts available");
|
|
665
731
|
}
|
|
732
|
+
purgeExpiredCooldowns(state);
|
|
666
733
|
if (accounts.length === 1) {
|
|
667
734
|
return { account: accounts[0], switched: false };
|
|
668
735
|
}
|
|
@@ -690,40 +757,39 @@ function selectAccount(accounts, state) {
|
|
|
690
757
|
if (isPrimary) {
|
|
691
758
|
if (isOverThreshold(primaryUsage, state)) {
|
|
692
759
|
const exceededMetric = getExceededMetric(primaryUsage, state);
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
continue;
|
|
696
|
-
const fbUsage = state.usage[fb.name];
|
|
697
|
-
if (!isOverThreshold(fbUsage, state)) {
|
|
698
|
-
return {
|
|
699
|
-
account: fb,
|
|
700
|
-
switched: true,
|
|
701
|
-
reason: `Primary exceeded ${exceededMetric} threshold — switching to ${fb.name}`
|
|
702
|
-
};
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
let bestFb = null;
|
|
706
|
-
let bestScore = Infinity;
|
|
707
|
-
for (const fb of fallbacks) {
|
|
708
|
-
if (isTemporarilyUnavailable(state, fb.name))
|
|
709
|
-
continue;
|
|
710
|
-
const score = getUtilizationScore(state.usage[fb.name], state);
|
|
711
|
-
if (score < bestScore) {
|
|
712
|
-
bestScore = score;
|
|
713
|
-
bestFb = fb;
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
if (bestFb) {
|
|
760
|
+
const best = findBestAvailable(fallbacks, state, new Set);
|
|
761
|
+
if (best) {
|
|
717
762
|
return {
|
|
718
|
-
account:
|
|
763
|
+
account: best,
|
|
719
764
|
switched: true,
|
|
720
|
-
reason: `Primary exceeded threshold
|
|
765
|
+
reason: `Primary exceeded ${exceededMetric} threshold — switching to ${best.name}`
|
|
721
766
|
};
|
|
722
767
|
}
|
|
723
768
|
return { account: primary, switched: false };
|
|
724
769
|
}
|
|
725
770
|
return { account: primary, switched: false };
|
|
726
771
|
}
|
|
772
|
+
const currentOverThreshold = isOverThreshold(currentUsage, state);
|
|
773
|
+
const currentInCooldown = isTemporarilyUnavailable(state, current.name);
|
|
774
|
+
if (currentOverThreshold || currentInCooldown) {
|
|
775
|
+
const reason = currentInCooldown ? `${current.name} in auth-failure cooldown` : `${current.name} exceeded threshold`;
|
|
776
|
+
if (!isOverThreshold(primaryUsage, state) && !isTemporarilyUnavailable(state, primary.name)) {
|
|
777
|
+
return {
|
|
778
|
+
account: primary,
|
|
779
|
+
switched: true,
|
|
780
|
+
reason: `${reason} — switching back to primary`
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
const best = findBestAvailable(accounts, state, new Set([current.name]));
|
|
784
|
+
if (best && best.name !== current.name) {
|
|
785
|
+
return {
|
|
786
|
+
account: best,
|
|
787
|
+
switched: true,
|
|
788
|
+
reason: `${reason} — switching to ${best.name}`
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
return { account: current, switched: false };
|
|
792
|
+
}
|
|
727
793
|
const now = Date.now();
|
|
728
794
|
const checkInterval = state.config.checkInterval;
|
|
729
795
|
const earliestReset = getEarliestReset(primaryUsage);
|
|
@@ -739,30 +805,6 @@ function selectAccount(accounts, state) {
|
|
|
739
805
|
};
|
|
740
806
|
}
|
|
741
807
|
}
|
|
742
|
-
if (!isTemporarilyUnavailable(state, current.name)) {
|
|
743
|
-
return { account: current, switched: false };
|
|
744
|
-
}
|
|
745
|
-
for (const fb of fallbacks) {
|
|
746
|
-
if (fb.name === current.name)
|
|
747
|
-
continue;
|
|
748
|
-
if (isTemporarilyUnavailable(state, fb.name))
|
|
749
|
-
continue;
|
|
750
|
-
const fbUsage = state.usage[fb.name];
|
|
751
|
-
if (!isOverThreshold(fbUsage, state)) {
|
|
752
|
-
return {
|
|
753
|
-
account: fb,
|
|
754
|
-
switched: true,
|
|
755
|
-
reason: `Current account ${current.name} in cooldown — switching to ${fb.name}`
|
|
756
|
-
};
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
if (!isTemporarilyUnavailable(state, primary.name)) {
|
|
760
|
-
return {
|
|
761
|
-
account: primary,
|
|
762
|
-
switched: true,
|
|
763
|
-
reason: "All fallbacks unavailable — falling back to primary"
|
|
764
|
-
};
|
|
765
|
-
}
|
|
766
808
|
return { account: current, switched: false };
|
|
767
809
|
}
|
|
768
810
|
function markAuthFailure(state, accountName) {
|
|
@@ -773,39 +815,6 @@ function clearAuthFailure(state, accountName) {
|
|
|
773
815
|
}
|
|
774
816
|
|
|
775
817
|
// src/index.ts
|
|
776
|
-
async function refreshAccountToken(refreshTokenValue) {
|
|
777
|
-
try {
|
|
778
|
-
const response = await fetch(TOKEN_URL, {
|
|
779
|
-
method: "POST",
|
|
780
|
-
headers: {
|
|
781
|
-
"Content-Type": "application/json",
|
|
782
|
-
Accept: "application/json, text/plain, */*",
|
|
783
|
-
"User-Agent": "axios/1.13.6"
|
|
784
|
-
},
|
|
785
|
-
body: JSON.stringify({
|
|
786
|
-
grant_type: "refresh_token",
|
|
787
|
-
refresh_token: refreshTokenValue,
|
|
788
|
-
client_id: CLIENT_ID
|
|
789
|
-
})
|
|
790
|
-
});
|
|
791
|
-
if (!response.ok) {
|
|
792
|
-
const body = await response.text().catch(() => "");
|
|
793
|
-
return { ok: false, error: `HTTP ${response.status}: ${body}` };
|
|
794
|
-
}
|
|
795
|
-
const json = await response.json();
|
|
796
|
-
return {
|
|
797
|
-
ok: true,
|
|
798
|
-
access: json.access_token,
|
|
799
|
-
refresh: json.refresh_token,
|
|
800
|
-
expires: Date.now() + json.expires_in * 1000
|
|
801
|
-
};
|
|
802
|
-
} catch (err) {
|
|
803
|
-
return {
|
|
804
|
-
ok: false,
|
|
805
|
-
error: err instanceof Error ? err.message : String(err)
|
|
806
|
-
};
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
818
|
var AuthSwitcherPlugin = async ({ client }) => {
|
|
810
819
|
return {
|
|
811
820
|
auth: {
|