@cortexkit/anthropic-auth-core 1.2.1 → 1.2.3
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.js +47 -17
- 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.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
-
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { ClaudeOAuthRefreshError, refreshClaudeOAuthToken } from "./auth.js";
|
|
@@ -21,6 +21,7 @@ const DEFAULT_MINIMUM_REMAINING = {
|
|
|
21
21
|
};
|
|
22
22
|
const DEFAULT_FAIL_CLOSED_ON_UNKNOWN_QUOTA = true;
|
|
23
23
|
const BACKGROUND_TICK_MS = 60_000;
|
|
24
|
+
const BACKGROUND_TICK_JITTER_MS = 60_000;
|
|
24
25
|
function getConfigDir() {
|
|
25
26
|
if (process.env.OPENCODE_CONFIG_DIR?.trim()) {
|
|
26
27
|
return process.env.OPENCODE_CONFIG_DIR.trim();
|
|
@@ -162,46 +163,64 @@ export async function saveAccounts(storage, path = getAccountStoragePath()) {
|
|
|
162
163
|
}
|
|
163
164
|
export async function acquireRefreshFileLock(options) {
|
|
164
165
|
const accountPath = options.path ?? getAccountStoragePath();
|
|
165
|
-
const
|
|
166
|
-
const
|
|
166
|
+
const lockPath = `${accountPath}.${options.name}.lock`;
|
|
167
|
+
const legacyOwnerPath = join(lockPath, 'owner.json');
|
|
167
168
|
const ownerId = randomUUID();
|
|
168
169
|
const now = options.now ?? Date.now;
|
|
170
|
+
async function readOwner() {
|
|
171
|
+
try {
|
|
172
|
+
return JSON.parse(await readFile(lockPath, 'utf8'));
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
const code = error.code;
|
|
176
|
+
if (code !== 'EISDIR')
|
|
177
|
+
throw error;
|
|
178
|
+
return JSON.parse(await readFile(legacyOwnerPath, 'utf8'));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
169
181
|
async function tryAcquire() {
|
|
170
182
|
try {
|
|
171
|
-
await
|
|
172
|
-
await writeFile(ownerPath, `${JSON.stringify({ ownerId, expiresAt: now() + options.ttlMs })}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
183
|
+
await writeFile(lockPath, `${JSON.stringify({ ownerId, expiresAt: now() + options.ttlMs })}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
173
184
|
return true;
|
|
174
185
|
}
|
|
175
186
|
catch (error) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
187
|
+
const code = error.code;
|
|
188
|
+
if (code === 'EEXIST' || code === 'EISDIR')
|
|
189
|
+
return false;
|
|
190
|
+
throw error;
|
|
179
191
|
}
|
|
180
192
|
}
|
|
181
193
|
if (!(await tryAcquire())) {
|
|
182
194
|
try {
|
|
183
|
-
const owner =
|
|
195
|
+
const owner = await readOwner();
|
|
184
196
|
if (Number(owner?.expiresAt) > now())
|
|
185
197
|
return null;
|
|
186
198
|
}
|
|
187
199
|
catch {
|
|
188
|
-
|
|
200
|
+
try {
|
|
201
|
+
const current = await stat(lockPath);
|
|
202
|
+
if (current.mtimeMs + options.ttlMs > Date.now())
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
189
208
|
}
|
|
190
|
-
await rm(
|
|
209
|
+
await rm(lockPath, { recursive: true, force: true }).catch(() => { });
|
|
191
210
|
if (!(await tryAcquire()))
|
|
192
211
|
return null;
|
|
193
212
|
}
|
|
194
213
|
return {
|
|
195
214
|
release: async () => {
|
|
196
215
|
try {
|
|
197
|
-
const owner =
|
|
216
|
+
const owner = await readOwner();
|
|
198
217
|
if (owner?.ownerId !== ownerId)
|
|
199
218
|
return;
|
|
200
219
|
}
|
|
201
220
|
catch {
|
|
202
221
|
return;
|
|
203
222
|
}
|
|
204
|
-
await rm(
|
|
223
|
+
await rm(lockPath, { recursive: true, force: true }).catch(() => { });
|
|
205
224
|
},
|
|
206
225
|
};
|
|
207
226
|
}
|
|
@@ -327,6 +346,9 @@ function quotaEnabled(storage) {
|
|
|
327
346
|
function refreshEnabled(storage) {
|
|
328
347
|
return storage?.refresh?.enabled !== false;
|
|
329
348
|
}
|
|
349
|
+
function jitterMs(maxMs) {
|
|
350
|
+
return Math.floor(Math.random() * Math.max(0, maxMs));
|
|
351
|
+
}
|
|
330
352
|
function refreshBeforeExpiryMs(storage) {
|
|
331
353
|
const minutes = storage?.refresh?.refreshBeforeExpiryMinutes ??
|
|
332
354
|
DEFAULT_REFRESH_BEFORE_EXPIRY_MINUTES;
|
|
@@ -358,9 +380,17 @@ export function buildRefreshOperationError(input) {
|
|
|
358
380
|
? (input.previous.retryCount ?? 0)
|
|
359
381
|
: 0;
|
|
360
382
|
const retryCount = previousRetryCount + 1;
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
383
|
+
let delay;
|
|
384
|
+
if (input.error instanceof ClaudeOAuthRefreshError &&
|
|
385
|
+
input.error.retryAfter) {
|
|
386
|
+
delay = input.error.retryAfter * 1000;
|
|
387
|
+
}
|
|
388
|
+
else if (isTransientRefreshError(input.error)) {
|
|
389
|
+
delay = Math.min(MAX_REFRESH_RETRY_DELAY_MS, MIN_REFRESH_RETRY_DELAY_MS * 2 ** Math.min(retryCount - 1, 6));
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
delay = NON_TRANSIENT_REFRESH_RETRY_DELAY_MS;
|
|
393
|
+
}
|
|
364
394
|
return {
|
|
365
395
|
message: formatErrorMessage(input.error),
|
|
366
396
|
checkedAt: input.now,
|
|
@@ -531,7 +561,7 @@ export class FallbackAccountManager {
|
|
|
531
561
|
if (!this.refreshTimer) {
|
|
532
562
|
this.refreshTimer = setInterval(() => {
|
|
533
563
|
void run().catch(() => { });
|
|
534
|
-
}, BACKGROUND_TICK_MS);
|
|
564
|
+
}, BACKGROUND_TICK_MS + jitterMs(BACKGROUND_TICK_JITTER_MS));
|
|
535
565
|
if ('unref' in this.refreshTimer)
|
|
536
566
|
this.refreshTimer.unref();
|
|
537
567
|
}
|
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',
|