oc-auth-switcher 0.1.2 → 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 +3 -3
- package/dist/cli.js +129 -23
- package/dist/index.js +34 -34
- 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
|
package/dist/cli.js
CHANGED
|
@@ -11,10 +11,27 @@ 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
16
|
var AUTH_FAILURE_COOLDOWN = 10 * 60 * 1000;
|
|
17
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
|
+
];
|
|
34
|
+
|
|
18
35
|
// src/accounts.ts
|
|
19
36
|
function normalizeAccount(raw) {
|
|
20
37
|
const name = raw.name || "unnamed";
|
|
@@ -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,7 +413,7 @@ 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
418
|
var AUTH_FAILURE_COOLDOWN = 10 * 60 * 1000;
|
|
419
419
|
|
|
@@ -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: "" };
|
|
@@ -782,39 +815,6 @@ function clearAuthFailure(state, accountName) {
|
|
|
782
815
|
}
|
|
783
816
|
|
|
784
817
|
// src/index.ts
|
|
785
|
-
async function refreshAccountToken(refreshTokenValue) {
|
|
786
|
-
try {
|
|
787
|
-
const response = await fetch(TOKEN_URL, {
|
|
788
|
-
method: "POST",
|
|
789
|
-
headers: {
|
|
790
|
-
"Content-Type": "application/json",
|
|
791
|
-
Accept: "application/json, text/plain, */*",
|
|
792
|
-
"User-Agent": "axios/1.13.6"
|
|
793
|
-
},
|
|
794
|
-
body: JSON.stringify({
|
|
795
|
-
grant_type: "refresh_token",
|
|
796
|
-
refresh_token: refreshTokenValue,
|
|
797
|
-
client_id: CLIENT_ID
|
|
798
|
-
})
|
|
799
|
-
});
|
|
800
|
-
if (!response.ok) {
|
|
801
|
-
const body = await response.text().catch(() => "");
|
|
802
|
-
return { ok: false, error: `HTTP ${response.status}: ${body}` };
|
|
803
|
-
}
|
|
804
|
-
const json = await response.json();
|
|
805
|
-
return {
|
|
806
|
-
ok: true,
|
|
807
|
-
access: json.access_token,
|
|
808
|
-
refresh: json.refresh_token,
|
|
809
|
-
expires: Date.now() + json.expires_in * 1000
|
|
810
|
-
};
|
|
811
|
-
} catch (err) {
|
|
812
|
-
return {
|
|
813
|
-
ok: false,
|
|
814
|
-
error: err instanceof Error ? err.message : String(err)
|
|
815
|
-
};
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
818
|
var AuthSwitcherPlugin = async ({ client }) => {
|
|
819
819
|
return {
|
|
820
820
|
auth: {
|