@cortexkit/anthropic-auth-core 1.2.2 → 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.
- package/dist/accounts.d.ts +1 -0
- package/dist/accounts.js +110 -31
- package/dist/auth.d.ts +3 -1
- package/dist/auth.js +19 -2
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/package.json +1 -1
package/dist/accounts.d.ts
CHANGED
|
@@ -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();
|
|
@@ -380,9 +383,17 @@ export function buildRefreshOperationError(input) {
|
|
|
380
383
|
? (input.previous.retryCount ?? 0)
|
|
381
384
|
: 0;
|
|
382
385
|
const retryCount = previousRetryCount + 1;
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
+
let delay;
|
|
387
|
+
if (input.error instanceof ClaudeOAuthRefreshError &&
|
|
388
|
+
input.error.retryAfter) {
|
|
389
|
+
delay = input.error.retryAfter * 1000;
|
|
390
|
+
}
|
|
391
|
+
else if (isTransientRefreshError(input.error)) {
|
|
392
|
+
delay = Math.min(MAX_REFRESH_RETRY_DELAY_MS, MIN_REFRESH_RETRY_DELAY_MS * 2 ** Math.min(retryCount - 1, 6));
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
delay = NON_TRANSIENT_REFRESH_RETRY_DELAY_MS;
|
|
396
|
+
}
|
|
386
397
|
return {
|
|
387
398
|
message: formatErrorMessage(input.error),
|
|
388
399
|
checkedAt: input.now,
|
|
@@ -526,6 +537,12 @@ function recordQuotaRefreshError(account, error, now) {
|
|
|
526
537
|
recordRefreshError(account, error, now);
|
|
527
538
|
}
|
|
528
539
|
}
|
|
540
|
+
function fallbackRefreshLockName(accountId) {
|
|
541
|
+
return `fallback-oauth-refresh-${createHash('sha256')
|
|
542
|
+
.update(accountId)
|
|
543
|
+
.digest('hex')
|
|
544
|
+
.slice(0, 16)}`;
|
|
545
|
+
}
|
|
529
546
|
export class FallbackAccountManager {
|
|
530
547
|
now;
|
|
531
548
|
fetchImpl;
|
|
@@ -741,43 +758,105 @@ export class FallbackAccountManager {
|
|
|
741
758
|
updateStoredAccount(storage, refreshed);
|
|
742
759
|
return refreshed;
|
|
743
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
|
+
}
|
|
744
793
|
async refreshAccountNow(account, storage, options) {
|
|
745
|
-
|
|
746
|
-
|
|
794
|
+
let latestStorage = await this.load();
|
|
795
|
+
let latestAccount = latestStorage?.accounts.find((candidate) => candidate.id === account.id);
|
|
747
796
|
if (latestAccount &&
|
|
748
797
|
!options.force &&
|
|
749
798
|
!tokenNeedsRefresh(latestAccount, latestStorage, this.now())) {
|
|
750
799
|
updateStoredAccount(storage, latestAccount);
|
|
751
800
|
return latestAccount;
|
|
752
801
|
}
|
|
753
|
-
|
|
754
|
-
const
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
expiresInMs: sourceAccount.expires
|
|
759
|
-
? sourceAccount.expires - this.now()
|
|
760
|
-
: undefined,
|
|
761
|
-
});
|
|
762
|
-
const refreshed = await refreshClaudeOAuthToken({
|
|
763
|
-
refreshToken,
|
|
764
|
-
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,
|
|
765
807
|
now: this.now,
|
|
766
808
|
});
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
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
|
+
}
|
|
781
860
|
}
|
|
782
861
|
async refreshAccountQuota(account, storage) {
|
|
783
862
|
let target = account;
|
package/dist/auth.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export declare class ClaudeOAuthRefreshError extends Error {
|
|
2
2
|
readonly status: number;
|
|
3
3
|
readonly body: string;
|
|
4
|
-
|
|
4
|
+
/** Parsed Retry-After value in seconds, if the server provided one. */
|
|
5
|
+
readonly retryAfter: number | undefined;
|
|
6
|
+
constructor(status: number, body: string, retryAfterHeader?: string | null);
|
|
5
7
|
}
|
|
6
8
|
export type ClaudeOAuthRefreshResult = {
|
|
7
9
|
access: string;
|
package/dist/auth.js
CHANGED
|
@@ -1,13 +1,29 @@
|
|
|
1
1
|
import { AUTHORIZE_URLS, CLIENT_ID, CODE_CALLBACK_URL, OAUTH_SCOPES, TOKEN_URL, } from "./constants.js";
|
|
2
2
|
import { generatePKCE } from "./pkce.js";
|
|
3
|
+
function parseRetryAfterHeader(value) {
|
|
4
|
+
if (!value)
|
|
5
|
+
return undefined;
|
|
6
|
+
const seconds = Number(value);
|
|
7
|
+
if (Number.isFinite(seconds) && seconds > 0)
|
|
8
|
+
return Math.ceil(seconds);
|
|
9
|
+
const date = Date.parse(value);
|
|
10
|
+
if (Number.isFinite(date)) {
|
|
11
|
+
const delta = Math.ceil((date - Date.now()) / 1000);
|
|
12
|
+
return delta > 0 ? delta : undefined;
|
|
13
|
+
}
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
3
16
|
export class ClaudeOAuthRefreshError extends Error {
|
|
4
17
|
status;
|
|
5
18
|
body;
|
|
6
|
-
|
|
19
|
+
/** Parsed Retry-After value in seconds, if the server provided one. */
|
|
20
|
+
retryAfter;
|
|
21
|
+
constructor(status, body, retryAfterHeader) {
|
|
7
22
|
super(`Claude OAuth refresh failed: ${status} — ${body}`);
|
|
8
23
|
this.status = status;
|
|
9
24
|
this.body = body;
|
|
10
25
|
this.name = 'ClaudeOAuthRefreshError';
|
|
26
|
+
this.retryAfter = parseRetryAfterHeader(retryAfterHeader ?? undefined);
|
|
11
27
|
}
|
|
12
28
|
}
|
|
13
29
|
function isTransientNetworkError(error) {
|
|
@@ -35,6 +51,7 @@ export async function refreshClaudeOAuthToken(input) {
|
|
|
35
51
|
headers: {
|
|
36
52
|
'Content-Type': 'application/json',
|
|
37
53
|
Accept: 'application/json',
|
|
54
|
+
'User-Agent': 'axios/1.13.6',
|
|
38
55
|
},
|
|
39
56
|
body: JSON.stringify({
|
|
40
57
|
grant_type: 'refresh_token',
|
|
@@ -48,7 +65,7 @@ export async function refreshClaudeOAuthToken(input) {
|
|
|
48
65
|
continue;
|
|
49
66
|
}
|
|
50
67
|
const body = await response.text().catch(() => '');
|
|
51
|
-
throw new ClaudeOAuthRefreshError(response.status, body);
|
|
68
|
+
throw new ClaudeOAuthRefreshError(response.status, body, response.headers.get('retry-after'));
|
|
52
69
|
}
|
|
53
70
|
const json = (await response.json());
|
|
54
71
|
const refreshedAt = input.now?.() ?? Date.now();
|
package/dist/constants.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export declare const AUTHORIZE_URLS: {
|
|
|
4
4
|
readonly max: "https://claude.ai/oauth/authorize";
|
|
5
5
|
};
|
|
6
6
|
export declare const CODE_CALLBACK_URL = "https://platform.claude.com/oauth/code/callback";
|
|
7
|
-
export declare const TOKEN_URL = "https://
|
|
7
|
+
export declare const TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
|
|
8
8
|
export declare const OAUTH_SCOPES: string[];
|
|
9
9
|
export declare const TOOL_PREFIX = "mcp_";
|
|
10
10
|
export declare const REQUIRED_BETAS: string[];
|
package/dist/constants.js
CHANGED
|
@@ -4,7 +4,7 @@ export const AUTHORIZE_URLS = {
|
|
|
4
4
|
max: 'https://claude.ai/oauth/authorize',
|
|
5
5
|
};
|
|
6
6
|
export const CODE_CALLBACK_URL = 'https://platform.claude.com/oauth/code/callback';
|
|
7
|
-
export const TOKEN_URL = 'https://
|
|
7
|
+
export const TOKEN_URL = 'https://platform.claude.com/v1/oauth/token';
|
|
8
8
|
export const OAUTH_SCOPES = [
|
|
9
9
|
'org:create_api_key',
|
|
10
10
|
'user:profile',
|