@cortexkit/anthropic-auth-core 1.2.3 → 1.2.4

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.
@@ -153,6 +153,7 @@ export declare class FallbackAccountManager {
153
153
  refreshAccount(account: OAuthAccount, storage: AccountStorage, options?: {
154
154
  force?: boolean;
155
155
  }): Promise<OAuthAccount>;
156
+ private waitForConcurrentFallbackRefresh;
156
157
  private refreshAccountNow;
157
158
  refreshAccountQuota(account: OAuthAccount, storage: AccountStorage): Promise<OAuthAccount>;
158
159
  }
package/dist/accounts.js CHANGED
@@ -22,6 +22,9 @@ const DEFAULT_MINIMUM_REMAINING = {
22
22
  const DEFAULT_FAIL_CLOSED_ON_UNKNOWN_QUOTA = true;
23
23
  const BACKGROUND_TICK_MS = 60_000;
24
24
  const BACKGROUND_TICK_JITTER_MS = 60_000;
25
+ const FALLBACK_REFRESH_LOCK_TTL_MS = 2 * 60_000;
26
+ const FALLBACK_REFRESH_JOIN_WAIT_MS = 10_000;
27
+ const FALLBACK_REFRESH_JOIN_POLL_MS = 100;
25
28
  function getConfigDir() {
26
29
  if (process.env.OPENCODE_CONFIG_DIR?.trim()) {
27
30
  return process.env.OPENCODE_CONFIG_DIR.trim();
@@ -534,6 +537,12 @@ function recordQuotaRefreshError(account, error, now) {
534
537
  recordRefreshError(account, error, now);
535
538
  }
536
539
  }
540
+ function fallbackRefreshLockName(accountId) {
541
+ return `fallback-oauth-refresh-${createHash('sha256')
542
+ .update(accountId)
543
+ .digest('hex')
544
+ .slice(0, 16)}`;
545
+ }
537
546
  export class FallbackAccountManager {
538
547
  now;
539
548
  fetchImpl;
@@ -749,43 +758,105 @@ export class FallbackAccountManager {
749
758
  updateStoredAccount(storage, refreshed);
750
759
  return refreshed;
751
760
  }
761
+ async waitForConcurrentFallbackRefresh(account, storage, previous, options) {
762
+ const deadline = Date.now() + FALLBACK_REFRESH_JOIN_WAIT_MS;
763
+ while (Date.now() < deadline) {
764
+ await new Promise((resolve) => setTimeout(resolve, FALLBACK_REFRESH_JOIN_POLL_MS));
765
+ const latestStorage = await this.load();
766
+ const latestAccount = latestStorage?.accounts.find((candidate) => candidate.id === account.id);
767
+ if (!latestAccount)
768
+ continue;
769
+ const changed = latestAccount.access !== previous.access ||
770
+ latestAccount.refresh !== previous.refresh ||
771
+ (latestAccount.expires ?? 0) > (previous.expires ?? 0) + 60_000;
772
+ if (changed &&
773
+ (options.force ||
774
+ !tokenNeedsRefresh(latestAccount, latestStorage, this.now()))) {
775
+ updateStoredAccount(storage, latestAccount);
776
+ log('[refresh] fallback oauth joined concurrent refresh', {
777
+ accountId: latestAccount.id,
778
+ expiresInMs: latestAccount.expires
779
+ ? latestAccount.expires - this.now()
780
+ : undefined,
781
+ });
782
+ return latestAccount;
783
+ }
784
+ const refreshError = latestAccount.lastRefreshError;
785
+ if (refreshError &&
786
+ refreshBackoffActive(refreshError, latestAccount.refresh, this.now())) {
787
+ updateStoredAccount(storage, latestAccount);
788
+ throw new Error(formatRefreshBackoffMessage(refreshError, this.now()));
789
+ }
790
+ }
791
+ return null;
792
+ }
752
793
  async refreshAccountNow(account, storage, options) {
753
- const latestStorage = await this.load();
754
- const latestAccount = latestStorage?.accounts.find((candidate) => candidate.id === account.id);
794
+ let latestStorage = await this.load();
795
+ let latestAccount = latestStorage?.accounts.find((candidate) => candidate.id === account.id);
755
796
  if (latestAccount &&
756
797
  !options.force &&
757
798
  !tokenNeedsRefresh(latestAccount, latestStorage, this.now())) {
758
799
  updateStoredAccount(storage, latestAccount);
759
800
  return latestAccount;
760
801
  }
761
- const sourceAccount = latestAccount ?? account;
762
- const refreshToken = sourceAccount.refresh;
763
- log('[refresh] fallback oauth refresh request start', {
764
- accountId: sourceAccount.id,
765
- force: options.force === true,
766
- expiresInMs: sourceAccount.expires
767
- ? sourceAccount.expires - this.now()
768
- : undefined,
769
- });
770
- const refreshed = await refreshClaudeOAuthToken({
771
- refreshToken,
772
- fetchImpl: this.fetchImpl,
802
+ let sourceAccount = latestAccount ?? account;
803
+ const fileLock = await acquireRefreshFileLock({
804
+ name: fallbackRefreshLockName(sourceAccount.id),
805
+ ttlMs: FALLBACK_REFRESH_LOCK_TTL_MS,
806
+ path: this.configPath,
773
807
  now: this.now,
774
808
  });
775
- sourceAccount.access = refreshed.access;
776
- sourceAccount.refresh = refreshed.refresh;
777
- sourceAccount.expires = refreshed.expires;
778
- sourceAccount.lastRefreshedAt =
779
- refreshed.expires - refreshed.expiresIn * 1000;
780
- sourceAccount.lastRefreshError = undefined;
781
- updateStoredAccount(storage, sourceAccount);
782
- log('[refresh] fallback oauth refresh succeeded', {
783
- accountId: sourceAccount.id,
784
- expiresInMs: sourceAccount.expires
785
- ? sourceAccount.expires - this.now()
786
- : undefined,
787
- });
788
- return sourceAccount;
809
+ if (!fileLock) {
810
+ log('[refresh] fallback oauth refresh skipped file lock', {
811
+ accountId: sourceAccount.id,
812
+ });
813
+ const concurrent = await this.waitForConcurrentFallbackRefresh(account, storage, sourceAccount, options);
814
+ if (concurrent)
815
+ return concurrent;
816
+ throw new Error('Fallback OAuth refresh is already in progress');
817
+ }
818
+ try {
819
+ latestStorage = await this.load();
820
+ latestAccount = latestStorage?.accounts.find((candidate) => candidate.id === account.id);
821
+ if (latestAccount &&
822
+ !options.force &&
823
+ !tokenNeedsRefresh(latestAccount, latestStorage, this.now())) {
824
+ updateStoredAccount(storage, latestAccount);
825
+ return latestAccount;
826
+ }
827
+ sourceAccount = latestAccount ?? sourceAccount;
828
+ const refreshToken = sourceAccount.refresh;
829
+ log('[refresh] fallback oauth refresh request start', {
830
+ accountId: sourceAccount.id,
831
+ force: options.force === true,
832
+ expiresInMs: sourceAccount.expires
833
+ ? sourceAccount.expires - this.now()
834
+ : undefined,
835
+ });
836
+ const refreshed = await refreshClaudeOAuthToken({
837
+ refreshToken,
838
+ fetchImpl: this.fetchImpl,
839
+ now: this.now,
840
+ });
841
+ sourceAccount.access = refreshed.access;
842
+ sourceAccount.refresh = refreshed.refresh;
843
+ sourceAccount.expires = refreshed.expires;
844
+ sourceAccount.lastRefreshedAt =
845
+ refreshed.expires - refreshed.expiresIn * 1000;
846
+ sourceAccount.lastRefreshError = undefined;
847
+ updateStoredAccount(storage, sourceAccount);
848
+ await this.save(storage);
849
+ log('[refresh] fallback oauth refresh succeeded', {
850
+ accountId: sourceAccount.id,
851
+ expiresInMs: sourceAccount.expires
852
+ ? sourceAccount.expires - this.now()
853
+ : undefined,
854
+ });
855
+ return sourceAccount;
856
+ }
857
+ finally {
858
+ await fileLock.release();
859
+ }
789
860
  }
790
861
  async refreshAccountQuota(account, storage) {
791
862
  let target = account;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/anthropic-auth-core",
3
- "version": "1.2.3",
3
+ "version": "1.2.4",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",