opencode-anthropic-multi-account 0.2.4 → 0.2.5

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/dist/index.js CHANGED
@@ -2194,9 +2194,100 @@ var TOOL_PREFIX = ANTHROPIC_OAUTH_ADAPTER.toolPrefix;
2194
2194
  var ACCOUNTS_FILENAME2 = ANTHROPIC_OAUTH_ADAPTER.accountStorageFilename;
2195
2195
  var PLAN_LABELS = ANTHROPIC_OAUTH_ADAPTER.planLabels;
2196
2196
  var TOKEN_EXPIRY_BUFFER_MS = 6e4;
2197
+ var TOKEN_REFRESH_TIMEOUT_MS = 3e4;
2197
2198
 
2198
2199
  // src/pi-ai-adapter.ts
2199
- import { loginAnthropic, refreshAnthropicToken } from "@mariozechner/pi-ai/oauth";
2200
+ import { AsyncLocalStorage } from "node:async_hooks";
2201
+ import * as piAiOauth from "@mariozechner/pi-ai/oauth";
2202
+
2203
+ // src/token-node-request.ts
2204
+ import * as childProcess from "node:child_process";
2205
+ function buildNodeTokenRequestScript() {
2206
+ return `
2207
+ const https = require("node:https");
2208
+ const endpoint = process.env.ANTHROPIC_REFRESH_ENDPOINT;
2209
+ const timeoutMs = Number(process.env.ANTHROPIC_REFRESH_TIMEOUT_MS || "30000");
2210
+ const payload = process.env.ANTHROPIC_REFRESH_REQUEST_BODY || "";
2211
+
2212
+ function printSuccess(body) {
2213
+ console.log(JSON.stringify({ ok: true, body }));
2214
+ }
2215
+
2216
+ function printFailure(error) {
2217
+ console.log(JSON.stringify({ ok: false, ...error }));
2218
+ }
2219
+
2220
+ const request = https.request(endpoint, {
2221
+ method: "POST",
2222
+ headers: {
2223
+ "Content-Type": "application/json",
2224
+ Accept: "application/json",
2225
+ "Content-Length": Buffer.byteLength(payload).toString(),
2226
+ },
2227
+ }, (response) => {
2228
+ let body = "";
2229
+ response.setEncoding("utf8");
2230
+ response.on("data", (chunk) => {
2231
+ body += chunk;
2232
+ });
2233
+ response.on("end", () => {
2234
+ const status = response.statusCode ?? 0;
2235
+ if (status < 200 || status >= 300) {
2236
+ printFailure({ status, body });
2237
+ return;
2238
+ }
2239
+
2240
+ printSuccess(body);
2241
+ });
2242
+ });
2243
+
2244
+ request.setTimeout(timeoutMs, () => {
2245
+ request.destroy(new Error("Request timed out after " + timeoutMs + "ms"));
2246
+ });
2247
+
2248
+ request.on("error", (error) => {
2249
+ printFailure({ error: error instanceof Error ? error.name + ": " + error.message : String(error) });
2250
+ });
2251
+
2252
+ request.write(payload);
2253
+ request.end();
2254
+ `;
2255
+ }
2256
+ async function defaultRunNodeTokenRequest(options) {
2257
+ const script = buildNodeTokenRequestScript();
2258
+ return await new Promise((resolve, reject) => {
2259
+ childProcess.execFile(
2260
+ options.executable,
2261
+ ["-e", script],
2262
+ {
2263
+ timeout: options.timeoutMs + 1e3,
2264
+ maxBuffer: 1024 * 1024,
2265
+ env: {
2266
+ ...process.env,
2267
+ ANTHROPIC_REFRESH_ENDPOINT: options.endpoint,
2268
+ ANTHROPIC_REFRESH_REQUEST_BODY: options.body,
2269
+ ANTHROPIC_REFRESH_TIMEOUT_MS: String(options.timeoutMs)
2270
+ }
2271
+ },
2272
+ (error, stdout, stderr) => {
2273
+ const trimmedStdout = stdout.trim();
2274
+ if (error) {
2275
+ reject(new Error(stderr.trim() || error.message));
2276
+ return;
2277
+ }
2278
+ if (!trimmedStdout) {
2279
+ reject(new Error("Empty response from Node refresh helper"));
2280
+ return;
2281
+ }
2282
+ resolve(trimmedStdout);
2283
+ }
2284
+ );
2285
+ });
2286
+ }
2287
+ var nodeTokenRequestRunner = defaultRunNodeTokenRequest;
2288
+ async function runNodeTokenRequest(options) {
2289
+ return await nodeTokenRequestRunner(options);
2290
+ }
2200
2291
 
2201
2292
  // src/config.ts
2202
2293
  initCoreConfig("claude-multiauth.json");
@@ -2378,19 +2469,140 @@ function fromPiAiCredentials(creds) {
2378
2469
  expiresAt: creds.expires
2379
2470
  };
2380
2471
  }
2472
+ var ANTHROPIC_REFRESH_ENDPOINT = "https://platform.claude.com/v1/oauth/token";
2473
+ var REFRESH_NODE_EXECUTABLE = process.env.OPENCODE_REFRESH_NODE_EXECUTABLE || "node";
2474
+ var tokenProxyContext = new AsyncLocalStorage();
2475
+ var tokenProxyInstalled = false;
2476
+ var tokenProxyOriginalFetch = null;
2477
+ var refreshEndpointUrl = new URL(ANTHROPIC_REFRESH_ENDPOINT);
2478
+ function buildRefreshRequestError(details) {
2479
+ return new Error(`Anthropic token refresh request failed. url=${ANTHROPIC_REFRESH_ENDPOINT}; details=${details}`);
2480
+ }
2481
+ function buildRefreshInvalidJsonError(body, details) {
2482
+ return new Error(`Anthropic token refresh returned invalid JSON. url=${ANTHROPIC_REFRESH_ENDPOINT}; body=${body}; details=${details}`);
2483
+ }
2484
+ function getRequestUrlString(input) {
2485
+ if (typeof input === "string") return input;
2486
+ if (input instanceof URL) return input.toString();
2487
+ return input.url;
2488
+ }
2489
+ function isAnthropicTokenEndpoint(input) {
2490
+ const rawUrl = getRequestUrlString(input);
2491
+ try {
2492
+ const url = new URL(rawUrl);
2493
+ return url.origin === refreshEndpointUrl.origin && url.pathname === refreshEndpointUrl.pathname;
2494
+ } catch {
2495
+ return rawUrl === ANTHROPIC_REFRESH_ENDPOINT;
2496
+ }
2497
+ }
2498
+ function getRequestBodySource(input, init) {
2499
+ if (init?.body !== void 0) {
2500
+ return init.body;
2501
+ }
2502
+ if (input instanceof Request) {
2503
+ return input.body;
2504
+ }
2505
+ return void 0;
2506
+ }
2507
+ function stringifyBinaryBody(body) {
2508
+ if (body instanceof ArrayBuffer) {
2509
+ return Buffer.from(body).toString("utf8");
2510
+ }
2511
+ return Buffer.from(body.buffer, body.byteOffset, body.byteLength).toString("utf8");
2512
+ }
2513
+ async function getRequestBody(input, init) {
2514
+ const body = getRequestBodySource(input, init);
2515
+ if (typeof body === "string") return body;
2516
+ if (body instanceof URLSearchParams) return body.toString();
2517
+ if (body instanceof Uint8Array || body instanceof ArrayBuffer || ArrayBuffer.isView(body)) {
2518
+ return stringifyBinaryBody(body);
2519
+ }
2520
+ if (typeof Blob !== "undefined" && body instanceof Blob) return await body.text();
2521
+ if (body instanceof ReadableStream) return await new Response(body).text();
2522
+ if (input instanceof Request && init?.body === void 0) return await input.clone().text();
2523
+ if (body == null) return "";
2524
+ throw buildRefreshRequestError(`Unsupported token request body type: ${Object.prototype.toString.call(body)}`);
2525
+ }
2526
+ function getRequestMethod(input, init) {
2527
+ return init?.method ?? (input instanceof Request ? input.method : "GET");
2528
+ }
2529
+ function shouldProxyTokenRequest(input) {
2530
+ return tokenProxyContext.getStore() === true && isAnthropicTokenEndpoint(input);
2531
+ }
2532
+ async function postAnthropicTokenViaNode(body) {
2533
+ let output;
2534
+ try {
2535
+ output = await runNodeTokenRequest({
2536
+ body,
2537
+ endpoint: ANTHROPIC_REFRESH_ENDPOINT,
2538
+ executable: REFRESH_NODE_EXECUTABLE,
2539
+ timeoutMs: TOKEN_REFRESH_TIMEOUT_MS
2540
+ });
2541
+ } catch (error) {
2542
+ const details = error instanceof Error ? error.message : String(error);
2543
+ throw buildRefreshRequestError(details);
2544
+ }
2545
+ let parsed;
2546
+ try {
2547
+ parsed = JSON.parse(output);
2548
+ } catch (error) {
2549
+ const details = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
2550
+ throw buildRefreshInvalidJsonError(output, details);
2551
+ }
2552
+ const result = parsed;
2553
+ if (!result.ok) {
2554
+ if (result.error) {
2555
+ throw buildRefreshRequestError(result.error);
2556
+ }
2557
+ throw buildRefreshRequestError(`Error: HTTP request failed. status=${result.status ?? 0}; url=${ANTHROPIC_REFRESH_ENDPOINT}; body=${result.body ?? ""}`);
2558
+ }
2559
+ return new Response(result.body ?? "", {
2560
+ status: 200,
2561
+ headers: {
2562
+ "content-type": "application/json"
2563
+ }
2564
+ });
2565
+ }
2566
+ function createAnthropicTokenProxyFetch(originalFetch) {
2567
+ return (async (input, init) => {
2568
+ if (!shouldProxyTokenRequest(input)) {
2569
+ return originalFetch(input, init);
2570
+ }
2571
+ const method = getRequestMethod(input, init).toUpperCase();
2572
+ if (method !== "POST") {
2573
+ throw buildRefreshRequestError(`Unsupported token endpoint method: ${method}`);
2574
+ }
2575
+ return await postAnthropicTokenViaNode(await getRequestBody(input, init));
2576
+ });
2577
+ }
2578
+ function ensureAnthropicTokenProxyFetchInstalled() {
2579
+ if (tokenProxyInstalled) return;
2580
+ tokenProxyOriginalFetch = globalThis.fetch;
2581
+ globalThis.fetch = createAnthropicTokenProxyFetch(tokenProxyOriginalFetch);
2582
+ tokenProxyInstalled = true;
2583
+ }
2584
+ async function withAnthropicTokenProxyFetch(operation) {
2585
+ ensureAnthropicTokenProxyFetchInstalled();
2586
+ return await tokenProxyContext.run(true, operation);
2587
+ }
2588
+ async function fetchProfileWithSingleRetry(accessToken) {
2589
+ let profileResult = await fetchProfile(accessToken);
2590
+ if (profileResult.ok) {
2591
+ return profileResult;
2592
+ }
2593
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
2594
+ profileResult = await fetchProfile(accessToken);
2595
+ return profileResult;
2596
+ }
2381
2597
  async function loginWithPiAi(callbacks) {
2382
- const piCreds = await loginAnthropic({
2598
+ const piCreds = await withAnthropicTokenProxyFetch(() => piAiOauth.loginAnthropic({
2383
2599
  onAuth: callbacks.onAuth,
2384
2600
  onPrompt: callbacks.onPrompt,
2385
2601
  onProgress: callbacks.onProgress,
2386
2602
  onManualCodeInput: callbacks.onManualCodeInput
2387
- });
2603
+ }));
2388
2604
  const base = fromPiAiCredentials(piCreds);
2389
- let profileResult = await fetchProfile(piCreds.access);
2390
- if (!profileResult.ok) {
2391
- await new Promise((r) => setTimeout(r, 1e3));
2392
- profileResult = await fetchProfile(piCreds.access);
2393
- }
2605
+ const profileResult = await fetchProfileWithSingleRetry(piCreds.access);
2394
2606
  const profileData = profileResult.ok ? profileResult.data : void 0;
2395
2607
  return {
2396
2608
  ...base,
@@ -2401,7 +2613,7 @@ async function loginWithPiAi(callbacks) {
2401
2613
  };
2402
2614
  }
2403
2615
  async function refreshWithPiAi(currentRefreshToken) {
2404
- const piCreds = await refreshAnthropicToken(currentRefreshToken);
2616
+ const piCreds = await withAnthropicTokenProxyFetch(() => piAiOauth.refreshAnthropicToken(currentRefreshToken));
2405
2617
  return {
2406
2618
  accessToken: piCreds.access,
2407
2619
  refreshToken: piCreds.refresh,
@@ -2412,6 +2624,13 @@ var PI_AI_ADAPTER_SERVICE = ANTHROPIC_OAUTH_ADAPTER.serviceLogName;
2412
2624
 
2413
2625
  // src/token.ts
2414
2626
  var PERMANENT_FAILURE_HTTP_STATUSES = /* @__PURE__ */ new Set([400, 401, 403]);
2627
+ var PERMANENT_FAILURE_MESSAGE_PATTERNS = [
2628
+ /\binvalid_grant\b/i,
2629
+ /\binvalid_scope\b/i,
2630
+ /\bunauthorized_client\b/i,
2631
+ /\brefresh token\b.*\b(invalid|expired|revoked|no longer valid)\b/i,
2632
+ /\bauth(?:entication)?(?:[_\s-]+)?invalid\b/i
2633
+ ];
2415
2634
  var refreshMutexByAccountId = /* @__PURE__ */ new Map();
2416
2635
  function isTokenExpired(account) {
2417
2636
  if (!account.accessToken || !account.expiresAt) return true;
@@ -2428,7 +2647,9 @@ async function refreshToken(currentRefreshToken, accountId, client) {
2428
2647
  } catch (error) {
2429
2648
  const message = error instanceof Error ? error.message : String(error);
2430
2649
  const statusMatch = message.match(/\b(400|401|403)\b/);
2431
- const isPermanent = statusMatch !== null && PERMANENT_FAILURE_HTTP_STATUSES.has(Number(statusMatch[1]));
2650
+ const hasPermanentStatus = statusMatch !== null && PERMANENT_FAILURE_HTTP_STATUSES.has(Number(statusMatch[1]));
2651
+ const hasPermanentMessage = PERMANENT_FAILURE_MESSAGE_PATTERNS.some((pattern) => pattern.test(message));
2652
+ const isPermanent = hasPermanentStatus || hasPermanentMessage;
2432
2653
  await client.app.log({
2433
2654
  body: {
2434
2655
  service: ANTHROPIC_OAUTH_ADAPTER.serviceLogName,
@@ -3039,7 +3260,14 @@ async function checkAccountQuota(manager, account, client) {
3039
3260
  }
3040
3261
  const refreshResult = await manager.ensureValidToken(account.uuid, client);
3041
3262
  if (!refreshResult.ok) {
3042
- printQuotaError(account, account.isAuthDisabled ? `${account.authDisabledReason ?? "Auth disabled"} (refresh failed)` : "Failed to refresh token");
3263
+ await manager.markAuthFailure(account.uuid, refreshResult);
3264
+ await manager.refresh();
3265
+ const updatedAccount = manager.getAccounts().find((candidate) => candidate.uuid === account.uuid);
3266
+ if (!updatedAccount) {
3267
+ printQuotaError(account, refreshResult.permanent ? "Refresh failed permanently; account removed" : "Failed to refresh token");
3268
+ return;
3269
+ }
3270
+ printQuotaError(updatedAccount, updatedAccount.isAuthDisabled ? `${updatedAccount.authDisabledReason ?? "Auth disabled"} (refresh failed)` : "Failed to refresh token");
3043
3271
  return;
3044
3272
  }
3045
3273
  await manager.refresh();
@@ -11,6 +11,8 @@ export interface LoginWithPiAiCallbacks {
11
11
  onProgress?: (message: string) => void;
12
12
  onManualCodeInput?: () => Promise<string>;
13
13
  }
14
+ export declare function withAnthropicTokenProxyFetch<T>(operation: () => Promise<T>): Promise<T>;
15
+ export declare function resetAnthropicTokenProxyStateForTest(): void;
14
16
  export declare function loginWithPiAi(callbacks: LoginWithPiAiCallbacks): Promise<Partial<StoredAccount>>;
15
17
  export declare function refreshWithPiAi(currentRefreshToken: string): Promise<CredentialRefreshPatch>;
16
18
  export declare const PI_AI_ADAPTER_SERVICE: string;
@@ -0,0 +1,10 @@
1
+ export interface NodeTokenRequestOptions {
2
+ body: string;
3
+ endpoint: string;
4
+ executable: string;
5
+ timeoutMs: number;
6
+ }
7
+ type NodeTokenRequestRunner = (options: NodeTokenRequestOptions) => Promise<string>;
8
+ export declare function runNodeTokenRequest(options: NodeTokenRequestOptions): Promise<string>;
9
+ export declare function setNodeTokenRequestRunnerForTest(runner: NodeTokenRequestRunner | null): void;
10
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-anthropic-multi-account",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "OpenCode plugin for Anthropic multi-account management with automatic rate limit switching",
5
5
  "main": "./dist/index.js",
6
6
  "type": "module",
@@ -39,7 +39,7 @@
39
39
  "directory": "packages/anthropic-multi-account"
40
40
  },
41
41
  "dependencies": {
42
- "opencode-multi-account-core": "^0.2.4",
42
+ "opencode-multi-account-core": "^0.2.5",
43
43
  "@mariozechner/pi-ai": "^0.61.0",
44
44
  "valibot": "^1.2.0"
45
45
  },