oc-auth-switcher 0.1.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/LICENSE +21 -0
- package/README.md +109 -0
- package/dist/cli.js +689 -0
- package/dist/index.js +1018 -0
- package/package.json +45 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1018 @@
|
|
|
1
|
+
// node_modules/@ex-machina/opencode-anthropic-auth/dist/cch.js
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
|
|
4
|
+
// node_modules/@ex-machina/opencode-anthropic-auth/dist/constants.js
|
|
5
|
+
var CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
|
6
|
+
var AUTHORIZE_URLS = {
|
|
7
|
+
console: "https://platform.claude.com/oauth/authorize",
|
|
8
|
+
max: "https://claude.ai/oauth/authorize"
|
|
9
|
+
};
|
|
10
|
+
var CODE_CALLBACK_URL = "https://platform.claude.com/oauth/code/callback";
|
|
11
|
+
var TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
|
|
12
|
+
var OAUTH_SCOPES = [
|
|
13
|
+
"org:create_api_key",
|
|
14
|
+
"user:profile",
|
|
15
|
+
"user:inference",
|
|
16
|
+
"user:sessions:claude_code",
|
|
17
|
+
"user:mcp_servers",
|
|
18
|
+
"user:file_upload"
|
|
19
|
+
];
|
|
20
|
+
var TOOL_PREFIX = "mcp_";
|
|
21
|
+
var REQUIRED_BETAS = [
|
|
22
|
+
"oauth-2025-04-20",
|
|
23
|
+
"interleaved-thinking-2025-05-14"
|
|
24
|
+
];
|
|
25
|
+
var OPENCODE_IDENTITY_PREFIX = "You are OpenCode";
|
|
26
|
+
var CLAUDE_CODE_IDENTITY = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
|
|
27
|
+
var CCH_SALT = "59cf53e54c78";
|
|
28
|
+
var CCH_POSITIONS = [4, 7, 20];
|
|
29
|
+
var CLAUDE_CODE_VERSION = "2.1.87";
|
|
30
|
+
var CLAUDE_CODE_ENTRYPOINT = "sdk-cli";
|
|
31
|
+
var USER_AGENT = "claude-cli/2.1.87 (external, cli)";
|
|
32
|
+
var PARAGRAPH_REMOVAL_ANCHORS = [
|
|
33
|
+
"github.com/anomalyco/opencode",
|
|
34
|
+
"opencode.ai/docs"
|
|
35
|
+
];
|
|
36
|
+
var TEXT_REPLACEMENTS = [
|
|
37
|
+
{ match: "if OpenCode honestly", replacement: "if the assistant honestly" },
|
|
38
|
+
{
|
|
39
|
+
match: "Here is some useful information about the environment you are running in:",
|
|
40
|
+
replacement: "Environment context you are running in:"
|
|
41
|
+
}
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
// node_modules/@ex-machina/opencode-anthropic-auth/dist/cch.js
|
|
45
|
+
function extractFirstUserMessageText(messages) {
|
|
46
|
+
const userMsg = messages.find((message) => message.role === "user");
|
|
47
|
+
if (!userMsg)
|
|
48
|
+
return "";
|
|
49
|
+
const { content } = userMsg;
|
|
50
|
+
if (typeof content === "string")
|
|
51
|
+
return content;
|
|
52
|
+
if (Array.isArray(content)) {
|
|
53
|
+
const textBlock = content.find((block) => block.type === "text");
|
|
54
|
+
if (textBlock?.text)
|
|
55
|
+
return textBlock.text;
|
|
56
|
+
}
|
|
57
|
+
return "";
|
|
58
|
+
}
|
|
59
|
+
function computeCCH(messageText) {
|
|
60
|
+
return createHash("sha256").update(messageText).digest("hex").slice(0, 5);
|
|
61
|
+
}
|
|
62
|
+
function computeVersionSuffix(messageText, version = CLAUDE_CODE_VERSION) {
|
|
63
|
+
const chars = CCH_POSITIONS.map((index) => messageText[index] || "0").join("");
|
|
64
|
+
return createHash("sha256").update(`${CCH_SALT}${chars}${version}`).digest("hex").slice(0, 3);
|
|
65
|
+
}
|
|
66
|
+
function buildBillingHeaderValue(messages, version = CLAUDE_CODE_VERSION, entrypoint) {
|
|
67
|
+
const text = extractFirstUserMessageText(messages);
|
|
68
|
+
const suffix = computeVersionSuffix(text, version);
|
|
69
|
+
const cch = computeCCH(text);
|
|
70
|
+
return "x-anthropic-billing-header: " + `cc_version=${version}.${suffix}; ` + `cc_entrypoint=${entrypoint}; ` + `cch=${cch};`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// node_modules/@ex-machina/opencode-anthropic-auth/dist/transform.js
|
|
74
|
+
function prefixName(name) {
|
|
75
|
+
return `${TOOL_PREFIX}${name.charAt(0).toUpperCase()}${name.slice(1)}`;
|
|
76
|
+
}
|
|
77
|
+
function unprefixName(name) {
|
|
78
|
+
if (name === "StructuredOutput") {
|
|
79
|
+
return name;
|
|
80
|
+
}
|
|
81
|
+
return `${name.charAt(0).toLowerCase()}${name.slice(1)}`;
|
|
82
|
+
}
|
|
83
|
+
function mergeHeaders(input, init) {
|
|
84
|
+
const headers = new Headers;
|
|
85
|
+
if (input instanceof Request) {
|
|
86
|
+
input.headers.forEach((value, key) => {
|
|
87
|
+
headers.set(key, value);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
const initHeaders = init?.headers;
|
|
91
|
+
if (initHeaders) {
|
|
92
|
+
if (initHeaders instanceof Headers) {
|
|
93
|
+
initHeaders.forEach((value, key) => {
|
|
94
|
+
headers.set(key, value);
|
|
95
|
+
});
|
|
96
|
+
} else if (Array.isArray(initHeaders)) {
|
|
97
|
+
for (const entry of initHeaders) {
|
|
98
|
+
const [key, value] = entry;
|
|
99
|
+
if (typeof value !== "undefined") {
|
|
100
|
+
headers.set(key, String(value));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
for (const [key, value] of Object.entries(initHeaders)) {
|
|
105
|
+
if (typeof value !== "undefined") {
|
|
106
|
+
headers.set(key, String(value));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return headers;
|
|
112
|
+
}
|
|
113
|
+
function mergeBetaHeaders(headers) {
|
|
114
|
+
const incomingBeta = headers.get("anthropic-beta") || "";
|
|
115
|
+
const incomingBetasList = incomingBeta.split(",").map((b) => b.trim()).filter(Boolean);
|
|
116
|
+
return [...new Set([...REQUIRED_BETAS, ...incomingBetasList])].join(",");
|
|
117
|
+
}
|
|
118
|
+
function setOAuthHeaders(headers, accessToken) {
|
|
119
|
+
headers.set("authorization", `Bearer ${accessToken}`);
|
|
120
|
+
headers.set("anthropic-beta", mergeBetaHeaders(headers));
|
|
121
|
+
headers.set("user-agent", USER_AGENT);
|
|
122
|
+
headers.delete("x-api-key");
|
|
123
|
+
return headers;
|
|
124
|
+
}
|
|
125
|
+
function prefixToolNames(parsed) {
|
|
126
|
+
if (parsed.tools && Array.isArray(parsed.tools)) {
|
|
127
|
+
parsed.tools = parsed.tools.map((tool) => ({
|
|
128
|
+
...tool,
|
|
129
|
+
name: tool.name ? prefixName(tool.name) : tool.name
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
if (parsed.messages && Array.isArray(parsed.messages)) {
|
|
133
|
+
parsed.messages = parsed.messages.map((msg) => {
|
|
134
|
+
if (msg.content && Array.isArray(msg.content)) {
|
|
135
|
+
msg.content = msg.content.map((block) => {
|
|
136
|
+
if (block.type === "tool_use" && block.name) {
|
|
137
|
+
return { ...block, name: prefixName(block.name) };
|
|
138
|
+
}
|
|
139
|
+
return block;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return msg;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return JSON.stringify(parsed);
|
|
146
|
+
}
|
|
147
|
+
function stripToolPrefix(text) {
|
|
148
|
+
return text.replace(/"name"\s*:\s*"mcp_([^"]+)"/g, (_match, name) => `"name": "${unprefixName(name)}"`);
|
|
149
|
+
}
|
|
150
|
+
function isInsecure() {
|
|
151
|
+
if (!process.env.ANTHROPIC_BASE_URL?.trim())
|
|
152
|
+
return false;
|
|
153
|
+
const raw = process.env.ANTHROPIC_INSECURE?.trim();
|
|
154
|
+
return raw === "1" || raw === "true";
|
|
155
|
+
}
|
|
156
|
+
function resolveBaseUrl() {
|
|
157
|
+
const raw = process.env.ANTHROPIC_BASE_URL?.trim();
|
|
158
|
+
if (!raw)
|
|
159
|
+
return null;
|
|
160
|
+
try {
|
|
161
|
+
const baseUrl = new URL(raw);
|
|
162
|
+
if (baseUrl.protocol !== "http:" && baseUrl.protocol !== "https:" || baseUrl.username || baseUrl.password) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
return baseUrl;
|
|
166
|
+
} catch {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function rewriteUrl(input) {
|
|
171
|
+
let requestUrl = null;
|
|
172
|
+
try {
|
|
173
|
+
if (typeof input === "string" || input instanceof URL) {
|
|
174
|
+
requestUrl = new URL(input.toString());
|
|
175
|
+
} else if (input instanceof Request) {
|
|
176
|
+
requestUrl = new URL(input.url);
|
|
177
|
+
}
|
|
178
|
+
} catch {
|
|
179
|
+
requestUrl = null;
|
|
180
|
+
}
|
|
181
|
+
if (!requestUrl)
|
|
182
|
+
return { input, url: null };
|
|
183
|
+
const originalHref = requestUrl.href;
|
|
184
|
+
const baseUrl = resolveBaseUrl();
|
|
185
|
+
if (baseUrl) {
|
|
186
|
+
requestUrl.protocol = baseUrl.protocol;
|
|
187
|
+
requestUrl.host = baseUrl.host;
|
|
188
|
+
}
|
|
189
|
+
if (requestUrl.pathname === "/v1/messages" && !requestUrl.searchParams.has("beta")) {
|
|
190
|
+
requestUrl.searchParams.set("beta", "true");
|
|
191
|
+
}
|
|
192
|
+
if (requestUrl.href === originalHref) {
|
|
193
|
+
return { input, url: requestUrl };
|
|
194
|
+
}
|
|
195
|
+
const newInput = input instanceof Request ? new Request(requestUrl.toString(), input) : requestUrl;
|
|
196
|
+
return { input: newInput, url: requestUrl };
|
|
197
|
+
}
|
|
198
|
+
function sanitizeSystemText(text) {
|
|
199
|
+
const paragraphs = text.split(/\n\n+/);
|
|
200
|
+
const filtered = paragraphs.filter((paragraph) => {
|
|
201
|
+
if (paragraph.includes(OPENCODE_IDENTITY_PREFIX)) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
for (const anchor of PARAGRAPH_REMOVAL_ANCHORS) {
|
|
205
|
+
if (paragraph.includes(anchor))
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
return true;
|
|
209
|
+
});
|
|
210
|
+
let result = filtered.join(`
|
|
211
|
+
|
|
212
|
+
`);
|
|
213
|
+
for (const rule of TEXT_REPLACEMENTS) {
|
|
214
|
+
result = result.replace(rule.match, rule.replacement);
|
|
215
|
+
}
|
|
216
|
+
return result.trim();
|
|
217
|
+
}
|
|
218
|
+
function isRecord(value) {
|
|
219
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
220
|
+
}
|
|
221
|
+
function prependClaudeCodeIdentity(system) {
|
|
222
|
+
const identityBlock = {
|
|
223
|
+
type: "text",
|
|
224
|
+
text: CLAUDE_CODE_IDENTITY
|
|
225
|
+
};
|
|
226
|
+
if (system == null)
|
|
227
|
+
return [identityBlock];
|
|
228
|
+
if (typeof system === "string") {
|
|
229
|
+
const sanitized2 = sanitizeSystemText(system);
|
|
230
|
+
if (sanitized2 === CLAUDE_CODE_IDENTITY)
|
|
231
|
+
return [identityBlock];
|
|
232
|
+
return [identityBlock, { type: "text", text: sanitized2 }];
|
|
233
|
+
}
|
|
234
|
+
if (isRecord(system)) {
|
|
235
|
+
const type = typeof system.type === "string" ? system.type : "text";
|
|
236
|
+
const text = typeof system.text === "string" ? system.text : "";
|
|
237
|
+
return [identityBlock, { ...system, type, text: sanitizeSystemText(text) }];
|
|
238
|
+
}
|
|
239
|
+
if (!Array.isArray(system))
|
|
240
|
+
return [identityBlock];
|
|
241
|
+
const sanitized = system.map((item) => {
|
|
242
|
+
if (typeof item === "string") {
|
|
243
|
+
return { type: "text", text: sanitizeSystemText(item) };
|
|
244
|
+
}
|
|
245
|
+
if (isRecord(item) && item.type === "text" && typeof item.text === "string") {
|
|
246
|
+
return {
|
|
247
|
+
...item,
|
|
248
|
+
type: "text",
|
|
249
|
+
text: sanitizeSystemText(item.text)
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
return { type: "text", text: String(item) };
|
|
253
|
+
});
|
|
254
|
+
if (sanitized[0]?.text === CLAUDE_CODE_IDENTITY) {
|
|
255
|
+
return sanitized;
|
|
256
|
+
}
|
|
257
|
+
return [identityBlock, ...sanitized];
|
|
258
|
+
}
|
|
259
|
+
function rewriteRequestBody(body) {
|
|
260
|
+
try {
|
|
261
|
+
const parsed = JSON.parse(body);
|
|
262
|
+
const billingHeader = Array.isArray(parsed.messages) && parsed.messages.some((message) => message.role === "user") ? buildBillingHeaderValue(parsed.messages, undefined, CLAUDE_CODE_ENTRYPOINT) : null;
|
|
263
|
+
parsed.system = prependClaudeCodeIdentity(parsed.system);
|
|
264
|
+
if (billingHeader && Array.isArray(parsed.system)) {
|
|
265
|
+
parsed.system.unshift({ type: "text", text: billingHeader });
|
|
266
|
+
}
|
|
267
|
+
return prefixToolNames(parsed);
|
|
268
|
+
} catch {
|
|
269
|
+
return body;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function createStrippedStream(response) {
|
|
273
|
+
if (!response.body)
|
|
274
|
+
return response;
|
|
275
|
+
const reader = response.body.getReader();
|
|
276
|
+
const decoder = new TextDecoder;
|
|
277
|
+
const encoder = new TextEncoder;
|
|
278
|
+
const stream = new ReadableStream({
|
|
279
|
+
async pull(controller) {
|
|
280
|
+
const { done, value } = await reader.read();
|
|
281
|
+
if (done) {
|
|
282
|
+
controller.close();
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
let text = decoder.decode(value, { stream: true });
|
|
286
|
+
text = stripToolPrefix(text);
|
|
287
|
+
controller.enqueue(encoder.encode(text));
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
return new Response(stream, {
|
|
291
|
+
status: response.status,
|
|
292
|
+
statusText: response.statusText,
|
|
293
|
+
headers: response.headers
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// node_modules/@ex-machina/opencode-anthropic-auth/dist/pkce.js
|
|
298
|
+
function base64UrlEncode(bytes) {
|
|
299
|
+
let bin = "";
|
|
300
|
+
for (const byte of bytes)
|
|
301
|
+
bin += String.fromCharCode(byte);
|
|
302
|
+
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
303
|
+
}
|
|
304
|
+
async function generatePKCE() {
|
|
305
|
+
const buf = new Uint8Array(64);
|
|
306
|
+
crypto.getRandomValues(buf);
|
|
307
|
+
const verifier = base64UrlEncode(buf);
|
|
308
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
|
|
309
|
+
return {
|
|
310
|
+
verifier,
|
|
311
|
+
challenge: base64UrlEncode(new Uint8Array(digest)),
|
|
312
|
+
method: "S256"
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// node_modules/@ex-machina/opencode-anthropic-auth/dist/auth.js
|
|
317
|
+
function generateState() {
|
|
318
|
+
return crypto.randomUUID().replace(/-/g, "");
|
|
319
|
+
}
|
|
320
|
+
function parseCallbackInput(input) {
|
|
321
|
+
const trimmed = input.trim();
|
|
322
|
+
try {
|
|
323
|
+
const url = new URL(trimmed);
|
|
324
|
+
const code2 = url.searchParams.get("code");
|
|
325
|
+
const state2 = url.searchParams.get("state");
|
|
326
|
+
if (code2 && state2) {
|
|
327
|
+
return { code: code2, state: state2 };
|
|
328
|
+
}
|
|
329
|
+
} catch {}
|
|
330
|
+
const hashSplits = trimmed.split("#");
|
|
331
|
+
if (hashSplits.length === 2 && hashSplits[0] && hashSplits[1]) {
|
|
332
|
+
return { code: hashSplits[0], state: hashSplits[1] };
|
|
333
|
+
}
|
|
334
|
+
const params = new URLSearchParams(trimmed);
|
|
335
|
+
const code = params.get("code");
|
|
336
|
+
const state = params.get("state");
|
|
337
|
+
if (code && state) {
|
|
338
|
+
return { code, state };
|
|
339
|
+
}
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
async function exchangeCode(callback, verifier, redirectUri) {
|
|
343
|
+
const result = await fetch(TOKEN_URL, {
|
|
344
|
+
method: "POST",
|
|
345
|
+
headers: {
|
|
346
|
+
"Content-Type": "application/json",
|
|
347
|
+
Accept: "application/json, text/plain, */*",
|
|
348
|
+
"User-Agent": "axios/1.13.6"
|
|
349
|
+
},
|
|
350
|
+
body: JSON.stringify({
|
|
351
|
+
code: callback.code,
|
|
352
|
+
state: callback.state,
|
|
353
|
+
grant_type: "authorization_code",
|
|
354
|
+
client_id: CLIENT_ID,
|
|
355
|
+
redirect_uri: redirectUri,
|
|
356
|
+
code_verifier: verifier
|
|
357
|
+
})
|
|
358
|
+
});
|
|
359
|
+
if (!result.ok) {
|
|
360
|
+
return {
|
|
361
|
+
type: "failed"
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
const json = await result.json();
|
|
365
|
+
return {
|
|
366
|
+
type: "success",
|
|
367
|
+
refresh: json.refresh_token,
|
|
368
|
+
access: json.access_token,
|
|
369
|
+
expires: Date.now() + json.expires_in * 1000
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
async function authorize(mode) {
|
|
373
|
+
const pkce = await generatePKCE();
|
|
374
|
+
const state = generateState();
|
|
375
|
+
const url = new URL(AUTHORIZE_URLS[mode], import.meta.url);
|
|
376
|
+
url.searchParams.set("code", "true");
|
|
377
|
+
url.searchParams.set("client_id", CLIENT_ID);
|
|
378
|
+
url.searchParams.set("response_type", "code");
|
|
379
|
+
url.searchParams.set("redirect_uri", CODE_CALLBACK_URL);
|
|
380
|
+
url.searchParams.set("scope", OAUTH_SCOPES.join(" "));
|
|
381
|
+
url.searchParams.set("code_challenge", pkce.challenge);
|
|
382
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
383
|
+
url.searchParams.set("state", state);
|
|
384
|
+
return {
|
|
385
|
+
url: url.toString(),
|
|
386
|
+
redirectUri: CODE_CALLBACK_URL,
|
|
387
|
+
state,
|
|
388
|
+
verifier: pkce.verifier
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
async function exchange(input, verifier, redirectUri, expectedState) {
|
|
392
|
+
const callback = parseCallbackInput(input);
|
|
393
|
+
if (!callback) {
|
|
394
|
+
return {
|
|
395
|
+
type: "failed"
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
if (expectedState && callback.state !== expectedState) {
|
|
399
|
+
return {
|
|
400
|
+
type: "failed"
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
return exchangeCode(callback, verifier, redirectUri);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// src/accounts.ts
|
|
407
|
+
import fs from "node:fs";
|
|
408
|
+
import path2 from "node:path";
|
|
409
|
+
|
|
410
|
+
// src/constants.ts
|
|
411
|
+
import path from "node:path";
|
|
412
|
+
import os from "node:os";
|
|
413
|
+
var configDir = path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "opencode");
|
|
414
|
+
var ACCOUNTS_FILE = path.join(configDir, "auth-switcher-accounts.json");
|
|
415
|
+
var STATE_FILE = path.join(configDir, "auth-switcher-state.json");
|
|
416
|
+
var DEFAULT_THRESHOLD = 0.9;
|
|
417
|
+
var DEFAULT_CHECK_INTERVAL = 60 * 60 * 1000;
|
|
418
|
+
var AUTH_FAILURE_COOLDOWN = 60 * 60 * 1000;
|
|
419
|
+
|
|
420
|
+
// src/accounts.ts
|
|
421
|
+
function normalizeAccount(raw) {
|
|
422
|
+
const name = raw.name || "unnamed";
|
|
423
|
+
const access = raw.access || raw.accessToken || "";
|
|
424
|
+
const refresh = raw.refresh || raw.refreshToken || "";
|
|
425
|
+
let expires = 0;
|
|
426
|
+
const rawExpires = raw.expires ?? raw.expiresAt;
|
|
427
|
+
if (typeof rawExpires === "number") {
|
|
428
|
+
expires = rawExpires;
|
|
429
|
+
} else if (typeof rawExpires === "string") {
|
|
430
|
+
const parsed = Date.parse(rawExpires);
|
|
431
|
+
if (!isNaN(parsed))
|
|
432
|
+
expires = parsed;
|
|
433
|
+
}
|
|
434
|
+
return { name, access, refresh, expires };
|
|
435
|
+
}
|
|
436
|
+
function ensureDir(filePath) {
|
|
437
|
+
const dir = path2.dirname(filePath);
|
|
438
|
+
if (!fs.existsSync(dir)) {
|
|
439
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
function safeReadJSON(filePath, fallback) {
|
|
443
|
+
for (const p of [filePath, filePath + ".bak"]) {
|
|
444
|
+
try {
|
|
445
|
+
const raw = fs.readFileSync(p, "utf-8");
|
|
446
|
+
return JSON.parse(raw);
|
|
447
|
+
} catch {}
|
|
448
|
+
}
|
|
449
|
+
return fallback;
|
|
450
|
+
}
|
|
451
|
+
function safeWriteJSON(filePath, data) {
|
|
452
|
+
ensureDir(filePath);
|
|
453
|
+
const content = JSON.stringify(data, null, 2);
|
|
454
|
+
const tmpPath = filePath + ".tmp";
|
|
455
|
+
const bakPath = filePath + ".bak";
|
|
456
|
+
if (fs.existsSync(filePath)) {
|
|
457
|
+
try {
|
|
458
|
+
fs.copyFileSync(filePath, bakPath);
|
|
459
|
+
} catch {}
|
|
460
|
+
}
|
|
461
|
+
fs.writeFileSync(tmpPath, content, { mode: 384 });
|
|
462
|
+
fs.renameSync(tmpPath, filePath);
|
|
463
|
+
}
|
|
464
|
+
function loadAccounts() {
|
|
465
|
+
const raw = safeReadJSON(ACCOUNTS_FILE, {
|
|
466
|
+
accounts: []
|
|
467
|
+
});
|
|
468
|
+
const accounts = (raw.accounts || []).map((a) => normalizeAccount(a));
|
|
469
|
+
return { accounts };
|
|
470
|
+
}
|
|
471
|
+
function saveAccounts(data) {
|
|
472
|
+
safeWriteJSON(ACCOUNTS_FILE, data);
|
|
473
|
+
}
|
|
474
|
+
function updateAccountTokens(name, access, refresh, expires) {
|
|
475
|
+
const data = loadAccounts();
|
|
476
|
+
const account = data.accounts.find((a) => a.name === name);
|
|
477
|
+
if (account) {
|
|
478
|
+
account.access = access;
|
|
479
|
+
account.refresh = refresh;
|
|
480
|
+
account.expires = expires;
|
|
481
|
+
saveAccounts(data);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// src/state.ts
|
|
486
|
+
var EMPTY_METRIC = { utilization: 0, reset: 0, status: "" };
|
|
487
|
+
var EMPTY_USAGE = {
|
|
488
|
+
session5h: { ...EMPTY_METRIC },
|
|
489
|
+
weekly7d: { ...EMPTY_METRIC },
|
|
490
|
+
weekly7dSonnet: { ...EMPTY_METRIC }
|
|
491
|
+
};
|
|
492
|
+
function defaultState() {
|
|
493
|
+
return {
|
|
494
|
+
currentAccount: null,
|
|
495
|
+
lastRotationCheck: 0,
|
|
496
|
+
requestCount: 0,
|
|
497
|
+
config: {
|
|
498
|
+
threshold: DEFAULT_THRESHOLD,
|
|
499
|
+
checkInterval: DEFAULT_CHECK_INTERVAL
|
|
500
|
+
},
|
|
501
|
+
usage: {},
|
|
502
|
+
authFailures: {}
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
function loadState() {
|
|
506
|
+
const raw = safeReadJSON(STATE_FILE, {});
|
|
507
|
+
const defaults = defaultState();
|
|
508
|
+
return {
|
|
509
|
+
currentAccount: raw.currentAccount ?? defaults.currentAccount,
|
|
510
|
+
lastRotationCheck: raw.lastRotationCheck ?? defaults.lastRotationCheck,
|
|
511
|
+
requestCount: raw.requestCount ?? defaults.requestCount,
|
|
512
|
+
config: {
|
|
513
|
+
threshold: raw.config?.threshold ?? defaults.config.threshold,
|
|
514
|
+
checkInterval: raw.config?.checkInterval ?? defaults.config.checkInterval
|
|
515
|
+
},
|
|
516
|
+
usage: raw.usage ?? defaults.usage,
|
|
517
|
+
authFailures: raw.authFailures ?? defaults.authFailures
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
function saveState(state) {
|
|
521
|
+
safeWriteJSON(STATE_FILE, state);
|
|
522
|
+
}
|
|
523
|
+
function getThresholds(config) {
|
|
524
|
+
if (typeof config.threshold === "number") {
|
|
525
|
+
return {
|
|
526
|
+
session5h: config.threshold,
|
|
527
|
+
weekly7d: config.threshold,
|
|
528
|
+
weekly7dSonnet: config.threshold
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
return config.threshold;
|
|
532
|
+
}
|
|
533
|
+
function resolveStaleMetrics(state) {
|
|
534
|
+
const now = Date.now() / 1000;
|
|
535
|
+
for (const accountName of Object.keys(state.usage)) {
|
|
536
|
+
const usage = state.usage[accountName];
|
|
537
|
+
const metrics = ["session5h", "weekly7d", "weekly7dSonnet"];
|
|
538
|
+
for (const key of metrics) {
|
|
539
|
+
const metric = usage[key];
|
|
540
|
+
if (metric.reset > 0 && metric.reset <= now) {
|
|
541
|
+
metric.utilization = 0;
|
|
542
|
+
metric.reset = 0;
|
|
543
|
+
metric.status = "";
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
function ensureAccountsInState(state, accountNames) {
|
|
549
|
+
for (const name of accountNames) {
|
|
550
|
+
if (!state.usage[name]) {
|
|
551
|
+
state.usage[name] = {
|
|
552
|
+
session5h: { ...EMPTY_METRIC },
|
|
553
|
+
weekly7d: { ...EMPTY_METRIC },
|
|
554
|
+
weekly7dSonnet: { ...EMPTY_METRIC }
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
function updateUsageFromHeaders(state, accountName, headers) {
|
|
560
|
+
if (!state.usage[accountName]) {
|
|
561
|
+
state.usage[accountName] = {
|
|
562
|
+
session5h: { ...EMPTY_METRIC },
|
|
563
|
+
weekly7d: { ...EMPTY_METRIC },
|
|
564
|
+
weekly7dSonnet: { ...EMPTY_METRIC }
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
const usage = state.usage[accountName];
|
|
568
|
+
let updated = false;
|
|
569
|
+
const metricFamilies = [
|
|
570
|
+
{
|
|
571
|
+
key: "session5h",
|
|
572
|
+
prefix: "anthropic-ratelimit-unified-5h"
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
key: "weekly7d",
|
|
576
|
+
prefix: "anthropic-ratelimit-unified-7d"
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
key: "weekly7dSonnet",
|
|
580
|
+
prefix: "anthropic-ratelimit-unified-7d_sonnet"
|
|
581
|
+
}
|
|
582
|
+
];
|
|
583
|
+
for (const { key, prefix } of metricFamilies) {
|
|
584
|
+
const utilHeader = headers.get(`${prefix}-utilization`);
|
|
585
|
+
const resetHeader = headers.get(`${prefix}-reset`);
|
|
586
|
+
const statusHeader = headers.get(`${prefix}-status`);
|
|
587
|
+
if (utilHeader !== null || resetHeader !== null || statusHeader !== null) {
|
|
588
|
+
if (utilHeader !== null) {
|
|
589
|
+
const val = parseFloat(utilHeader);
|
|
590
|
+
if (!isNaN(val)) {
|
|
591
|
+
usage[key].utilization = val;
|
|
592
|
+
updated = true;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
if (resetHeader !== null) {
|
|
596
|
+
const val = Number(resetHeader);
|
|
597
|
+
if (!isNaN(val)) {
|
|
598
|
+
usage[key].reset = val;
|
|
599
|
+
} else {
|
|
600
|
+
const parsed = Date.parse(resetHeader);
|
|
601
|
+
if (!isNaN(parsed)) {
|
|
602
|
+
usage[key].reset = parsed / 1000;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
if (statusHeader !== null) {
|
|
607
|
+
usage[key].status = statusHeader;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
if (updated) {
|
|
612
|
+
usage.timestamp = new Date().toISOString();
|
|
613
|
+
}
|
|
614
|
+
return updated;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// src/rotation.ts
|
|
618
|
+
function isTemporarilyUnavailable(state, accountName) {
|
|
619
|
+
const cooldownUntil = state.authFailures[accountName];
|
|
620
|
+
if (!cooldownUntil)
|
|
621
|
+
return false;
|
|
622
|
+
if (Date.now() > cooldownUntil) {
|
|
623
|
+
delete state.authFailures[accountName];
|
|
624
|
+
return false;
|
|
625
|
+
}
|
|
626
|
+
return true;
|
|
627
|
+
}
|
|
628
|
+
function isOverThreshold(usage, state) {
|
|
629
|
+
if (!usage)
|
|
630
|
+
return false;
|
|
631
|
+
const thresholds = getThresholds(state.config);
|
|
632
|
+
return usage.session5h.utilization > thresholds.session5h || usage.weekly7d.utilization > thresholds.weekly7d || usage.weekly7dSonnet.utilization > thresholds.weekly7dSonnet;
|
|
633
|
+
}
|
|
634
|
+
function getUtilizationScore(usage, state) {
|
|
635
|
+
if (!usage)
|
|
636
|
+
return 0;
|
|
637
|
+
const thresholds = getThresholds(state.config);
|
|
638
|
+
return Math.max(usage.session5h.utilization / thresholds.session5h, usage.weekly7d.utilization / thresholds.weekly7d, usage.weekly7dSonnet.utilization / thresholds.weekly7dSonnet);
|
|
639
|
+
}
|
|
640
|
+
function getExceededMetric(usage, state) {
|
|
641
|
+
if (!usage)
|
|
642
|
+
return null;
|
|
643
|
+
const thresholds = getThresholds(state.config);
|
|
644
|
+
const metrics = [
|
|
645
|
+
{ name: "session5h", util: usage.session5h.utilization, thresh: thresholds.session5h },
|
|
646
|
+
{ name: "weekly7d", util: usage.weekly7d.utilization, thresh: thresholds.weekly7d },
|
|
647
|
+
{ name: "weekly7dSonnet", util: usage.weekly7dSonnet.utilization, thresh: thresholds.weekly7dSonnet }
|
|
648
|
+
];
|
|
649
|
+
const exceeded = metrics.filter((m) => m.util > m.thresh).sort((a, b) => b.util / b.thresh - a.util / a.thresh);
|
|
650
|
+
return exceeded.length > 0 ? exceeded[0].name : null;
|
|
651
|
+
}
|
|
652
|
+
function getEarliestReset(usage) {
|
|
653
|
+
if (!usage)
|
|
654
|
+
return 0;
|
|
655
|
+
const resets = [
|
|
656
|
+
usage.session5h.reset,
|
|
657
|
+
usage.weekly7d.reset,
|
|
658
|
+
usage.weekly7dSonnet.reset
|
|
659
|
+
].filter((r) => r > 0);
|
|
660
|
+
return resets.length > 0 ? Math.min(...resets) * 1000 : 0;
|
|
661
|
+
}
|
|
662
|
+
function selectAccount(accounts, state) {
|
|
663
|
+
if (accounts.length === 0) {
|
|
664
|
+
throw new Error("No accounts available");
|
|
665
|
+
}
|
|
666
|
+
if (accounts.length === 1) {
|
|
667
|
+
return { account: accounts[0], switched: false };
|
|
668
|
+
}
|
|
669
|
+
const primary = accounts[0];
|
|
670
|
+
const fallbacks = accounts.slice(1);
|
|
671
|
+
if (!state.currentAccount) {
|
|
672
|
+
return {
|
|
673
|
+
account: primary,
|
|
674
|
+
switched: true,
|
|
675
|
+
reason: "Initial selection — using primary account"
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
const currentIdx = accounts.findIndex((a) => a.name === state.currentAccount);
|
|
679
|
+
if (currentIdx < 0) {
|
|
680
|
+
return {
|
|
681
|
+
account: primary,
|
|
682
|
+
switched: true,
|
|
683
|
+
reason: "Current account no longer in pool — switching to primary"
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
const current = accounts[currentIdx];
|
|
687
|
+
const currentUsage = state.usage[current.name];
|
|
688
|
+
const primaryUsage = state.usage[primary.name];
|
|
689
|
+
const isPrimary = current.name === primary.name;
|
|
690
|
+
if (isPrimary) {
|
|
691
|
+
if (isOverThreshold(primaryUsage, state)) {
|
|
692
|
+
const exceededMetric = getExceededMetric(primaryUsage, state);
|
|
693
|
+
for (const fb of fallbacks) {
|
|
694
|
+
if (isTemporarilyUnavailable(state, fb.name))
|
|
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) {
|
|
717
|
+
return {
|
|
718
|
+
account: bestFb,
|
|
719
|
+
switched: true,
|
|
720
|
+
reason: `Primary exceeded threshold, all fallbacks busy — using least loaded: ${bestFb.name}`
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
return { account: primary, switched: false };
|
|
724
|
+
}
|
|
725
|
+
return { account: primary, switched: false };
|
|
726
|
+
}
|
|
727
|
+
const now = Date.now();
|
|
728
|
+
const checkInterval = state.config.checkInterval;
|
|
729
|
+
const earliestReset = getEarliestReset(primaryUsage);
|
|
730
|
+
const timeSinceLastCheck = now - state.lastRotationCheck;
|
|
731
|
+
const shouldCheckPrimary = earliestReset > 0 && earliestReset <= now || timeSinceLastCheck >= checkInterval;
|
|
732
|
+
if (shouldCheckPrimary) {
|
|
733
|
+
state.lastRotationCheck = now;
|
|
734
|
+
if (!isOverThreshold(primaryUsage, state) && !isTemporarilyUnavailable(state, primary.name)) {
|
|
735
|
+
return {
|
|
736
|
+
account: primary,
|
|
737
|
+
switched: true,
|
|
738
|
+
reason: "Primary has recovered — switching back"
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
}
|
|
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
|
+
return { account: current, switched: false };
|
|
767
|
+
}
|
|
768
|
+
function markAuthFailure(state, accountName) {
|
|
769
|
+
state.authFailures[accountName] = Date.now() + AUTH_FAILURE_COOLDOWN;
|
|
770
|
+
}
|
|
771
|
+
function clearAuthFailure(state, accountName) {
|
|
772
|
+
delete state.authFailures[accountName];
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// 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
|
+
var AuthSwitcherPlugin = async ({ client }) => {
|
|
810
|
+
return {
|
|
811
|
+
auth: {
|
|
812
|
+
provider: "anthropic",
|
|
813
|
+
async loader(getAuth, provider) {
|
|
814
|
+
const auth = await getAuth();
|
|
815
|
+
if (auth.type !== "oauth")
|
|
816
|
+
return {};
|
|
817
|
+
for (const model of Object.values(provider.models)) {
|
|
818
|
+
model.cost = {
|
|
819
|
+
input: 0,
|
|
820
|
+
output: 0,
|
|
821
|
+
cache: { read: 0, write: 0 }
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
let refreshPromise = null;
|
|
825
|
+
return {
|
|
826
|
+
apiKey: "",
|
|
827
|
+
async fetch(input, init) {
|
|
828
|
+
const { accounts } = loadAccounts();
|
|
829
|
+
const state = loadState();
|
|
830
|
+
if (accounts.length === 0) {
|
|
831
|
+
const auth2 = await getAuth();
|
|
832
|
+
if (auth2.type !== "oauth")
|
|
833
|
+
return fetch(input, init);
|
|
834
|
+
if (!auth2.access || !auth2.expires || auth2.expires < Date.now()) {
|
|
835
|
+
if (!refreshPromise) {
|
|
836
|
+
refreshPromise = (async () => {
|
|
837
|
+
const freshAuth = await getAuth();
|
|
838
|
+
const result = await refreshAccountToken(freshAuth.refresh);
|
|
839
|
+
if (!result.ok)
|
|
840
|
+
throw new Error(result.error);
|
|
841
|
+
await client.auth.set({
|
|
842
|
+
path: { id: "anthropic" },
|
|
843
|
+
body: {
|
|
844
|
+
type: "oauth",
|
|
845
|
+
refresh: result.refresh,
|
|
846
|
+
access: result.access,
|
|
847
|
+
expires: result.expires
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
return result.access;
|
|
851
|
+
})().finally(() => {
|
|
852
|
+
refreshPromise = null;
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
auth2.access = await refreshPromise;
|
|
856
|
+
}
|
|
857
|
+
const requestHeaders2 = mergeHeaders(input, init);
|
|
858
|
+
setOAuthHeaders(requestHeaders2, auth2.access);
|
|
859
|
+
let body2 = init?.body;
|
|
860
|
+
if (body2 && typeof body2 === "string") {
|
|
861
|
+
body2 = rewriteRequestBody(body2);
|
|
862
|
+
}
|
|
863
|
+
const rewritten2 = rewriteUrl(input);
|
|
864
|
+
const response2 = await fetch(rewritten2.input, {
|
|
865
|
+
...init,
|
|
866
|
+
body: body2,
|
|
867
|
+
headers: requestHeaders2,
|
|
868
|
+
...isInsecure() && {
|
|
869
|
+
tls: { rejectUnauthorized: false }
|
|
870
|
+
}
|
|
871
|
+
});
|
|
872
|
+
return createStrippedStream(response2);
|
|
873
|
+
}
|
|
874
|
+
ensureAccountsInState(state, accounts.map((a) => a.name));
|
|
875
|
+
resolveStaleMetrics(state);
|
|
876
|
+
const selection = selectAccount(accounts, state);
|
|
877
|
+
let account = selection.account;
|
|
878
|
+
if (selection.switched) {
|
|
879
|
+
console.log(`[oc-auth-switcher] ${selection.reason}`);
|
|
880
|
+
}
|
|
881
|
+
state.currentAccount = account.name;
|
|
882
|
+
const attemptedAccounts = new Set;
|
|
883
|
+
while (true) {
|
|
884
|
+
attemptedAccounts.add(account.name);
|
|
885
|
+
if (!account.access || account.expires <= Date.now()) {
|
|
886
|
+
const result = await refreshAccountToken(account.refresh);
|
|
887
|
+
if (result.ok && result.access && result.refresh && result.expires) {
|
|
888
|
+
account.access = result.access;
|
|
889
|
+
account.refresh = result.refresh;
|
|
890
|
+
account.expires = result.expires;
|
|
891
|
+
updateAccountTokens(account.name, result.access, result.refresh, result.expires);
|
|
892
|
+
} else {
|
|
893
|
+
markAuthFailure(state, account.name);
|
|
894
|
+
const next = accounts.find((a) => !attemptedAccounts.has(a.name) && !state.authFailures[a.name]);
|
|
895
|
+
if (!next) {
|
|
896
|
+
throw new Error(`[oc-auth-switcher] All accounts failed token refresh`);
|
|
897
|
+
}
|
|
898
|
+
account = next;
|
|
899
|
+
state.currentAccount = account.name;
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
break;
|
|
904
|
+
}
|
|
905
|
+
const requestHeaders = mergeHeaders(input, init);
|
|
906
|
+
setOAuthHeaders(requestHeaders, account.access);
|
|
907
|
+
let body = init?.body;
|
|
908
|
+
if (body && typeof body === "string") {
|
|
909
|
+
body = rewriteRequestBody(body);
|
|
910
|
+
}
|
|
911
|
+
const rewritten = rewriteUrl(input);
|
|
912
|
+
const response = await fetch(rewritten.input, {
|
|
913
|
+
...init,
|
|
914
|
+
body,
|
|
915
|
+
headers: requestHeaders,
|
|
916
|
+
...isInsecure() && {
|
|
917
|
+
tls: { rejectUnauthorized: false }
|
|
918
|
+
}
|
|
919
|
+
});
|
|
920
|
+
if ((response.status === 401 || response.status === 403) && !attemptedAccounts.has("__retried__")) {
|
|
921
|
+
const errorBody = await response.clone().text().catch(() => "");
|
|
922
|
+
const isScopeError = errorBody.includes("scope") || errorBody.includes("unauthorized") || errorBody.includes("invalid");
|
|
923
|
+
if (isScopeError) {
|
|
924
|
+
markAuthFailure(state, account.name);
|
|
925
|
+
const next = accounts.find((a) => !attemptedAccounts.has(a.name) && (!state.authFailures[a.name] || state.authFailures[a.name] <= Date.now()));
|
|
926
|
+
if (next) {
|
|
927
|
+
attemptedAccounts.add("__retried__");
|
|
928
|
+
account = next;
|
|
929
|
+
state.currentAccount = next.name;
|
|
930
|
+
if (!next.access || next.expires <= Date.now()) {
|
|
931
|
+
const result = await refreshAccountToken(next.refresh);
|
|
932
|
+
if (result.ok && result.access && result.refresh && result.expires) {
|
|
933
|
+
next.access = result.access;
|
|
934
|
+
next.refresh = result.refresh;
|
|
935
|
+
next.expires = result.expires;
|
|
936
|
+
updateAccountTokens(next.name, result.access, result.refresh, result.expires);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
const retryHeaders = mergeHeaders(input, init);
|
|
940
|
+
setOAuthHeaders(retryHeaders, next.access);
|
|
941
|
+
const retryResponse = await fetch(rewritten.input, {
|
|
942
|
+
...init,
|
|
943
|
+
body,
|
|
944
|
+
headers: retryHeaders,
|
|
945
|
+
...isInsecure() && {
|
|
946
|
+
tls: { rejectUnauthorized: false }
|
|
947
|
+
}
|
|
948
|
+
});
|
|
949
|
+
updateUsageFromHeaders(state, next.name, retryResponse.headers);
|
|
950
|
+
clearAuthFailure(state, next.name);
|
|
951
|
+
saveState(state);
|
|
952
|
+
return createStrippedStream(retryResponse);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
updateUsageFromHeaders(state, account.name, response.headers);
|
|
957
|
+
clearAuthFailure(state, account.name);
|
|
958
|
+
state.requestCount = (state.requestCount || 0) + 1;
|
|
959
|
+
saveState(state);
|
|
960
|
+
return createStrippedStream(response);
|
|
961
|
+
}
|
|
962
|
+
};
|
|
963
|
+
},
|
|
964
|
+
methods: [
|
|
965
|
+
{
|
|
966
|
+
label: "Claude Pro/Max",
|
|
967
|
+
type: "oauth",
|
|
968
|
+
authorize: async () => {
|
|
969
|
+
const result = await authorize("max");
|
|
970
|
+
return {
|
|
971
|
+
url: result.url,
|
|
972
|
+
instructions: "Paste the authorization code here:",
|
|
973
|
+
method: "code",
|
|
974
|
+
callback: async (code) => {
|
|
975
|
+
return exchange(code, result.verifier, result.redirectUri, result.state);
|
|
976
|
+
}
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
},
|
|
980
|
+
{
|
|
981
|
+
label: "Create an API Key",
|
|
982
|
+
type: "oauth",
|
|
983
|
+
authorize: async () => {
|
|
984
|
+
const result = await authorize("console");
|
|
985
|
+
return {
|
|
986
|
+
url: result.url,
|
|
987
|
+
instructions: "Paste the authorization code here:",
|
|
988
|
+
method: "code",
|
|
989
|
+
callback: async (code) => {
|
|
990
|
+
const credentials = await exchange(code, result.verifier, result.redirectUri, result.state);
|
|
991
|
+
if (credentials.type === "failed")
|
|
992
|
+
return credentials;
|
|
993
|
+
const apiKey = await fetch("https://api.anthropic.com/api/oauth/claude_cli/create_api_key", {
|
|
994
|
+
method: "POST",
|
|
995
|
+
headers: {
|
|
996
|
+
"Content-Type": "application/json",
|
|
997
|
+
authorization: `Bearer ${credentials.access}`
|
|
998
|
+
}
|
|
999
|
+
}).then((r) => r.json());
|
|
1000
|
+
return { type: "success", key: apiKey.raw_key };
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
},
|
|
1005
|
+
{
|
|
1006
|
+
provider: "anthropic",
|
|
1007
|
+
label: "Manually enter API Key",
|
|
1008
|
+
type: "api"
|
|
1009
|
+
}
|
|
1010
|
+
]
|
|
1011
|
+
}
|
|
1012
|
+
};
|
|
1013
|
+
};
|
|
1014
|
+
var src_default = AuthSwitcherPlugin;
|
|
1015
|
+
export {
|
|
1016
|
+
src_default as default,
|
|
1017
|
+
AuthSwitcherPlugin
|
|
1018
|
+
};
|