@cortexkit/opencode-anthropic-auth 1.1.0 → 1.1.1

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.
Files changed (2) hide show
  1. package/dist/index.js +101 -15
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
- import { authorize, buildClaudeQuotaSummary, buildFallbackQuotaSummaries, CACHE_1H_COMMAND_NAME, CLAUDE_DUMP_COMMAND_NAME, CLAUDE_FAST_COMMAND_NAME, CLAUDE_QUOTAS_COMMAND_NAME, ClaudeOAuthRefreshError, exchange, executeCache1hCommand, executeDumpCommand, executeFastModeCommand, FallbackAccountManager, fetchOAuthQuotaSnapshot, getCache1hMode, getCache1hPersistentMode, getQuotaCheckIntervalMs, getQuotaNextRefreshAt, getRelayConfig, isCache1hEnabled, isCache1hPersistentlyEnabled, isDumpPersistentlyEnabled, isFastModeEnabled, isFastModePersistentlyEnabled, isFastModeSupportedModel, loadAccounts, log, parseCache1hCommandAction, parseDumpCommandAction, parseFastModeCommandAction, quotaSnapshotPassesPolicy, refreshClaudeOAuthToken, resolveClaudeCodeIdentity, sendViaRelay, setCache1hPersistentEnabled, setCache1hPersistentMode, setCache1hState, setDumpEnabled, setDumpPersistentEnabled, setFastModeEnabled, setFastModePersistentEnabled, shouldFallbackStatus, } from '@cortexkit/anthropic-auth-core';
1
+ import { randomUUID } from 'node:crypto';
2
+ import { authorize, buildClaudeQuotaSummary, buildFallbackQuotaSummaries, buildRefreshOperationError, CACHE_1H_COMMAND_NAME, CLAUDE_DUMP_COMMAND_NAME, CLAUDE_FAST_COMMAND_NAME, CLAUDE_QUOTAS_COMMAND_NAME, ClaudeOAuthRefreshError, exchange, executeCache1hCommand, executeDumpCommand, executeFastModeCommand, FallbackAccountManager, fetchOAuthQuotaSnapshot, formatRefreshBackoffMessage, getCache1hMode, getCache1hPersistentMode, getQuotaCheckIntervalMs, getQuotaNextRefreshAt, getRelayConfig, hashRefreshToken, isCache1hEnabled, isCache1hPersistentlyEnabled, isDumpPersistentlyEnabled, isFastModeEnabled, isFastModePersistentlyEnabled, isFastModeSupportedModel, loadAccounts, log, parseCache1hCommandAction, parseDumpCommandAction, parseFastModeCommandAction, quotaSnapshotPassesPolicy, refreshBackoffActive, refreshClaudeOAuthToken, resolveClaudeCodeIdentity, saveAccounts, sendViaRelay, setCache1hPersistentEnabled, setCache1hPersistentMode, setCache1hState, setDumpEnabled, setDumpPersistentEnabled, setFastModeEnabled, setFastModePersistentEnabled, shouldFallbackStatus, } from '@cortexkit/anthropic-auth-core';
2
3
  import { resolvePromptContext } from "./prompt-context.js";
3
4
  import { addFastModeBetaHeader, createStrippedStream, isInsecure, mergeHeaders, rewriteRequestBody, rewriteUrl, setOAuthHeaders, } from "./transform.js";
4
5
  const HANDLED_SENTINEL = '__OPENCODE_ANTHROPIC_AUTH_COMMAND_HANDLED__';
@@ -273,7 +274,20 @@ export const AnthropicAuthPlugin = async (ctx) => {
273
274
  refreshPromise = (async () => {
274
275
  const maxRetries = 2;
275
276
  const baseDelayMs = 500;
277
+ let leaseId = null;
278
+ let leaseTokenHash = null;
279
+ async function updateMainRefreshState(update) {
280
+ const storage = (await loadAccounts()) ?? {
281
+ version: 1,
282
+ main: { type: 'opencode', provider: 'anthropic' },
283
+ accounts: [],
284
+ };
285
+ storage.refresh = storage.refresh ?? {};
286
+ update(storage);
287
+ await saveAccounts(storage);
288
+ }
276
289
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
290
+ let freshAuth = null;
277
291
  try {
278
292
  if (attempt > 0) {
279
293
  const delay = baseDelayMs * 2 ** (attempt - 1);
@@ -282,24 +296,43 @@ export const AnthropicAuthPlugin = async (ctx) => {
282
296
  // Re-read auth to get the latest refresh token.
283
297
  // The outer `auth` snapshot may be stale if tokens
284
298
  // were rotated since the fetch() call was made.
285
- const freshAuth = await getAuth();
299
+ freshAuth = await getAuth();
286
300
  if (!freshAuth.refresh) {
287
301
  throw new Error('Token refresh failed: missing refresh token');
288
302
  }
289
- let refreshed;
290
- try {
291
- refreshed = await refreshClaudeOAuthToken({
292
- refreshToken: freshAuth.refresh,
293
- });
303
+ const storage = await loadAccounts();
304
+ const refreshTokenHash = hashRefreshToken(freshAuth.refresh);
305
+ const mainError = storage?.refresh?.mainLastRefreshError;
306
+ if (mainError &&
307
+ refreshBackoffActive(mainError, freshAuth.refresh, Date.now())) {
308
+ throw new Error(formatRefreshBackoffMessage(mainError, Date.now()));
294
309
  }
295
- catch (error) {
296
- if (error instanceof ClaudeOAuthRefreshError &&
297
- error.status >= 500 &&
298
- attempt < maxRetries) {
299
- continue;
300
- }
301
- throw error;
310
+ if (storage?.refresh?.mainRefreshLeaseUntil &&
311
+ storage.refresh.mainRefreshLeaseUntil > Date.now() &&
312
+ storage.refresh.mainRefreshLeaseTokenHash ===
313
+ refreshTokenHash) {
314
+ throw new Error('Claude OAuth refresh is already in progress');
315
+ }
316
+ leaseId = randomUUID();
317
+ leaseTokenHash = refreshTokenHash;
318
+ await updateMainRefreshState((nextStorage) => {
319
+ nextStorage.refresh = nextStorage.refresh ?? {};
320
+ nextStorage.refresh.mainRefreshLeaseId =
321
+ leaseId ?? undefined;
322
+ nextStorage.refresh.mainRefreshLeaseUntil =
323
+ Date.now() + 2 * 60_000;
324
+ nextStorage.refresh.mainRefreshLeaseTokenHash =
325
+ refreshTokenHash;
326
+ });
327
+ const latestLease = await loadAccounts();
328
+ if (latestLease?.refresh?.mainRefreshLeaseId !== leaseId ||
329
+ latestLease.refresh.mainRefreshLeaseTokenHash !==
330
+ refreshTokenHash) {
331
+ throw new Error('Claude OAuth refresh is already in progress');
302
332
  }
333
+ const refreshed = await refreshClaudeOAuthToken({
334
+ refreshToken: freshAuth.refresh,
335
+ });
303
336
  // biome-ignore lint/suspicious/noExplicitAny: SDK types don't expose auth.set
304
337
  await client.auth.set({
305
338
  path: {
@@ -312,6 +345,16 @@ export const AnthropicAuthPlugin = async (ctx) => {
312
345
  expires: refreshed.expires,
313
346
  },
314
347
  });
348
+ await updateMainRefreshState((storage) => {
349
+ if (!storage?.refresh)
350
+ return;
351
+ storage.refresh.mainLastRefreshError = undefined;
352
+ if (storage.refresh.mainRefreshLeaseId === leaseId) {
353
+ storage.refresh.mainRefreshLeaseId = undefined;
354
+ storage.refresh.mainRefreshLeaseUntil = undefined;
355
+ storage.refresh.mainRefreshLeaseTokenHash = undefined;
356
+ }
357
+ });
315
358
  return refreshed.access;
316
359
  }
317
360
  catch (error) {
@@ -322,11 +365,43 @@ export const AnthropicAuthPlugin = async (ctx) => {
322
365
  error.code === 'ECONNREFUSED' ||
323
366
  error.code === 'ETIMEDOUT' ||
324
367
  error.code === 'UND_ERR_CONNECT_TIMEOUT')));
325
- if (attempt < maxRetries && isNetworkError) {
368
+ if (attempt < maxRetries &&
369
+ (isNetworkError ||
370
+ (error instanceof ClaudeOAuthRefreshError &&
371
+ error.status >= 500))) {
326
372
  continue;
327
373
  }
374
+ const failedRefreshToken = freshAuth?.refresh;
375
+ if (failedRefreshToken &&
376
+ error instanceof ClaudeOAuthRefreshError) {
377
+ await updateMainRefreshState((storage) => {
378
+ storage.refresh = storage.refresh ?? {};
379
+ storage.refresh.mainLastRefreshError =
380
+ buildRefreshOperationError({
381
+ error,
382
+ now: Date.now(),
383
+ refreshToken: failedRefreshToken,
384
+ previous: storage.refresh.mainLastRefreshError,
385
+ });
386
+ });
387
+ }
328
388
  throw error;
329
389
  }
390
+ finally {
391
+ if (leaseId) {
392
+ await updateMainRefreshState((storage) => {
393
+ if (!storage?.refresh)
394
+ return;
395
+ if (storage.refresh.mainRefreshLeaseId === leaseId &&
396
+ storage.refresh.mainRefreshLeaseTokenHash ===
397
+ leaseTokenHash) {
398
+ storage.refresh.mainRefreshLeaseId = undefined;
399
+ storage.refresh.mainRefreshLeaseUntil = undefined;
400
+ storage.refresh.mainRefreshLeaseTokenHash = undefined;
401
+ }
402
+ }).catch(() => { });
403
+ }
404
+ }
330
405
  }
331
406
  // Unreachable — each iteration either returns or throws.
332
407
  // Kept as a TypeScript exhaustiveness guard.
@@ -356,6 +431,17 @@ export const AnthropicAuthPlugin = async (ctx) => {
356
431
  mainRefreshBeforeExpiryMs(storage)) {
357
432
  return;
358
433
  }
434
+ if (latestAuth.refresh &&
435
+ refreshBackoffActive(storage?.refresh?.mainLastRefreshError, latestAuth.refresh, Date.now())) {
436
+ return;
437
+ }
438
+ if (latestAuth.refresh &&
439
+ storage?.refresh?.mainRefreshLeaseUntil &&
440
+ storage.refresh.mainRefreshLeaseUntil > Date.now() &&
441
+ storage.refresh.mainRefreshLeaseTokenHash ===
442
+ hashRefreshToken(latestAuth.refresh)) {
443
+ return;
444
+ }
359
445
  await refreshMainAccessToken();
360
446
  log('[refresh] opencode main oauth refreshed in background', {
361
447
  expires: latestAuth.expires,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/opencode-anthropic-auth",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -40,6 +40,6 @@
40
40
  "@opencode-ai/plugin": "*"
41
41
  },
42
42
  "dependencies": {
43
- "@cortexkit/anthropic-auth-core": "1.1.0"
43
+ "@cortexkit/anthropic-auth-core": "1.1.1"
44
44
  }
45
45
  }