@khanglvm/llm-router 1.2.0 → 1.3.0

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.
@@ -1,13 +1,67 @@
1
1
  /**
2
- * OAuth authentication for ChatGPT Codex subscription providers.
3
- * Implements browser-based login and device code flow.
2
+ * OAuth authentication for subscription providers.
3
+ * Supports ChatGPT Codex and Claude Code OAuth flows.
4
4
  */
5
5
 
6
6
  import http from 'node:http';
7
7
  import crypto from 'node:crypto';
8
8
  import { spawn } from 'node:child_process';
9
- import { CODEX_OAUTH_CONFIG } from './subscription-constants.js';
10
- import { saveTokens, loadTokens, isTokenExpired, deleteTokens, listTokenProfiles as listTokenProfilesFromStore } from './subscription-tokens.js';
9
+ import {
10
+ CODEX_OAUTH_CONFIG,
11
+ CLAUDE_CODE_OAUTH_CONFIG
12
+ } from './subscription-constants.js';
13
+ import {
14
+ saveTokens,
15
+ loadTokens,
16
+ isTokenExpired,
17
+ deleteTokens,
18
+ listTokenProfiles as listTokenProfilesFromStore
19
+ } from './subscription-tokens.js';
20
+
21
+ const SUBSCRIPTION_TYPE_CHATGPT_CODEX = 'chatgpt-codex';
22
+ const SUBSCRIPTION_TYPE_CLAUDE_CODE = 'claude-code';
23
+ const CLAUDE_PROFILE_PREFIX = `${SUBSCRIPTION_TYPE_CLAUDE_CODE}__`;
24
+
25
+ function normalizeSubscriptionType(value, { allowEmpty = false } = {}) {
26
+ const normalized = String(value || '').trim().toLowerCase();
27
+ if (!normalized) {
28
+ return allowEmpty ? '' : SUBSCRIPTION_TYPE_CHATGPT_CODEX;
29
+ }
30
+ if (normalized === SUBSCRIPTION_TYPE_CHATGPT_CODEX) return SUBSCRIPTION_TYPE_CHATGPT_CODEX;
31
+ if (normalized === SUBSCRIPTION_TYPE_CLAUDE_CODE) return SUBSCRIPTION_TYPE_CLAUDE_CODE;
32
+ throw new Error(`Unsupported subscription type '${value}'.`);
33
+ }
34
+
35
+ function resolveOAuthConfig(subscriptionType) {
36
+ const normalized = normalizeSubscriptionType(subscriptionType);
37
+ if (normalized === SUBSCRIPTION_TYPE_CLAUDE_CODE) {
38
+ return CLAUDE_CODE_OAUTH_CONFIG;
39
+ }
40
+ return CODEX_OAUTH_CONFIG;
41
+ }
42
+
43
+ function toTokenProfileKey(profileId, subscriptionType) {
44
+ const normalizedProfile = String(profileId || 'default').trim() || 'default';
45
+ const normalizedType = normalizeSubscriptionType(subscriptionType);
46
+ if (normalizedType === SUBSCRIPTION_TYPE_CLAUDE_CODE) {
47
+ return `${CLAUDE_PROFILE_PREFIX}${normalizedProfile}`;
48
+ }
49
+ return normalizedProfile;
50
+ }
51
+
52
+ function fromTokenProfileKey(profileKey) {
53
+ const value = String(profileKey || '').trim();
54
+ if (value.startsWith(CLAUDE_PROFILE_PREFIX)) {
55
+ return {
56
+ profileId: value.slice(CLAUDE_PROFILE_PREFIX.length) || 'default',
57
+ subscriptionType: SUBSCRIPTION_TYPE_CLAUDE_CODE
58
+ };
59
+ }
60
+ return {
61
+ profileId: value || 'default',
62
+ subscriptionType: SUBSCRIPTION_TYPE_CHATGPT_CODEX
63
+ };
64
+ }
11
65
 
12
66
  /**
13
67
  * Generate PKCE code verifier and challenge.
@@ -48,22 +102,40 @@ function tryOpenBrowser(url) {
48
102
  }
49
103
  }
50
104
 
105
+ function normalizeTokenData(data, fallbackRefreshToken = undefined) {
106
+ return {
107
+ accessToken: data.access_token,
108
+ refreshToken: data.refresh_token || fallbackRefreshToken,
109
+ expiresAt: Date.now() + (Number(data.expires_in || 0) * 1000),
110
+ tokenType: data.token_type || 'Bearer',
111
+ scope: data.scope
112
+ };
113
+ }
114
+
51
115
  /**
52
116
  * Refresh an access token using refresh token.
53
117
  * @param {string} refreshToken - OAuth refresh token
118
+ * @param {Object} [options] - Options
119
+ * @param {string} [options.subscriptionType] - Subscription type
54
120
  * @returns {Promise<Object>} New token data
55
121
  */
56
- export async function refreshAccessToken(refreshToken) {
57
- const response = await fetch(CODEX_OAUTH_CONFIG.tokenUrl, {
122
+ export async function refreshAccessToken(refreshToken, options = {}) {
123
+ const config = resolveOAuthConfig(options.subscriptionType);
124
+ const body = {
125
+ grant_type: 'refresh_token',
126
+ refresh_token: refreshToken,
127
+ client_id: config.clientId
128
+ };
129
+ if (typeof config.scopes === 'string' && config.scopes.trim()) {
130
+ body.scope = config.scopes;
131
+ }
132
+
133
+ const response = await fetch(config.tokenUrl, {
58
134
  method: 'POST',
59
135
  headers: {
60
136
  'Content-Type': 'application/json'
61
137
  },
62
- body: JSON.stringify({
63
- grant_type: 'refresh_token',
64
- refresh_token: refreshToken,
65
- client_id: CODEX_OAUTH_CONFIG.clientId
66
- })
138
+ body: JSON.stringify(body)
67
139
  });
68
140
 
69
141
  if (!response.ok) {
@@ -72,38 +144,36 @@ export async function refreshAccessToken(refreshToken) {
72
144
  }
73
145
 
74
146
  const data = await response.json();
75
-
76
- return {
77
- accessToken: data.access_token,
78
- refreshToken: data.refresh_token || refreshToken,
79
- expiresAt: Date.now() + (data.expires_in * 1000),
80
- tokenType: data.token_type || 'Bearer',
81
- scope: data.scope
82
- };
147
+ return normalizeTokenData(data, refreshToken);
83
148
  }
84
149
 
85
150
  /**
86
151
  * Get valid access token for a profile, refreshing if needed.
87
152
  * @param {string} profileId - Provider profile ID
153
+ * @param {Object} [options] - Options
154
+ * @param {string} [options.subscriptionType] - Subscription type
88
155
  * @returns {Promise<string|null>} Valid access token or null
89
156
  */
90
- export async function getValidAccessToken(profileId) {
91
- const tokens = await loadTokens(profileId);
157
+ export async function getValidAccessToken(profileId, options = {}) {
158
+ const config = resolveOAuthConfig(options.subscriptionType);
159
+ const tokenProfileKey = toTokenProfileKey(profileId, options.subscriptionType);
160
+ const tokens = await loadTokens(tokenProfileKey);
92
161
  if (!tokens) return null;
93
162
 
94
163
  // Check if token needs refresh
95
- if (isTokenExpired(tokens, CODEX_OAUTH_CONFIG.tokenRefreshBufferMs)) {
164
+ if (isTokenExpired(tokens, config.tokenRefreshBufferMs)) {
96
165
  if (!tokens.refreshToken) {
97
- return null; // Cannot refresh without refresh token
166
+ return null;
98
167
  }
99
168
 
100
169
  try {
101
- const newTokens = await refreshAccessToken(tokens.refreshToken);
102
- await saveTokens(profileId, newTokens);
170
+ const newTokens = await refreshAccessToken(tokens.refreshToken, {
171
+ subscriptionType: options.subscriptionType
172
+ });
173
+ await saveTokens(tokenProfileKey, newTokens);
103
174
  return newTokens.accessToken;
104
175
  } catch {
105
- // Refresh failed, tokens are invalid
106
- await deleteTokens(profileId);
176
+ await deleteTokens(tokenProfileKey);
107
177
  return null;
108
178
  }
109
179
  }
@@ -116,21 +186,30 @@ export async function getValidAccessToken(profileId) {
116
186
  * @param {string} code - Authorization code
117
187
  * @param {string} codeVerifier - PKCE code verifier
118
188
  * @param {string} redirectUri - Redirect URI used in auth request
189
+ * @param {Object} [options] - Options
190
+ * @param {string} [options.subscriptionType] - Subscription type
191
+ * @param {string} [options.state] - OAuth state
119
192
  * @returns {Promise<Object>} Token data
120
193
  */
121
- async function exchangeCodeForTokens(code, codeVerifier, redirectUri) {
122
- const response = await fetch(CODEX_OAUTH_CONFIG.tokenUrl, {
194
+ async function exchangeCodeForTokens(code, codeVerifier, redirectUri, options = {}) {
195
+ const config = resolveOAuthConfig(options.subscriptionType);
196
+ const body = {
197
+ grant_type: 'authorization_code',
198
+ code,
199
+ code_verifier: codeVerifier,
200
+ redirect_uri: redirectUri,
201
+ client_id: config.clientId
202
+ };
203
+ if (typeof options.state === 'string' && options.state.trim()) {
204
+ body.state = options.state.trim();
205
+ }
206
+
207
+ const response = await fetch(config.tokenUrl, {
123
208
  method: 'POST',
124
209
  headers: {
125
210
  'Content-Type': 'application/json'
126
211
  },
127
- body: JSON.stringify({
128
- grant_type: 'authorization_code',
129
- code,
130
- code_verifier: codeVerifier,
131
- redirect_uri: redirectUri,
132
- client_id: CODEX_OAUTH_CONFIG.clientId
133
- })
212
+ body: JSON.stringify(body)
134
213
  });
135
214
 
136
215
  if (!response.ok) {
@@ -139,41 +218,37 @@ async function exchangeCodeForTokens(code, codeVerifier, redirectUri) {
139
218
  }
140
219
 
141
220
  const data = await response.json();
142
-
143
- return {
144
- accessToken: data.access_token,
145
- refreshToken: data.refresh_token,
146
- expiresAt: Date.now() + (data.expires_in * 1000),
147
- tokenType: data.token_type || 'Bearer',
148
- scope: data.scope
149
- };
221
+ return normalizeTokenData(data);
150
222
  }
151
223
 
152
224
  /**
153
225
  * Start browser-based OAuth login.
154
226
  * @param {string} profileId - Provider profile ID
155
227
  * @param {Object} [options] - Options
228
+ * @param {string} [options.subscriptionType] - Subscription type
156
229
  * @param {number} [options.port] - Callback server port
157
230
  * @param {function} [options.onUrl] - Callback when auth URL is ready
158
231
  * @returns {Promise<boolean>} Success status
159
232
  */
160
233
  export async function loginWithBrowser(profileId, options = {}) {
161
- const port = options.port || CODEX_OAUTH_CONFIG.callbackPort;
162
- const redirectUri = `http://localhost:${port}${CODEX_OAUTH_CONFIG.callbackPath}`;
163
-
234
+ const config = resolveOAuthConfig(options.subscriptionType);
235
+ const tokenProfileKey = toTokenProfileKey(profileId, options.subscriptionType);
236
+ const port = options.port || config.callbackPort;
237
+ const redirectUri = `http://localhost:${port}${config.callbackPath}`;
238
+
164
239
  const pkce = generatePKCE();
165
240
  const state = generateState();
166
-
167
- const authUrl = new URL(CODEX_OAUTH_CONFIG.authorizeUrl);
241
+
242
+ const authUrl = new URL(config.authorizeUrl);
168
243
  authUrl.searchParams.set('response_type', 'code');
169
- authUrl.searchParams.set('client_id', CODEX_OAUTH_CONFIG.clientId);
244
+ authUrl.searchParams.set('client_id', config.clientId);
170
245
  authUrl.searchParams.set('redirect_uri', redirectUri);
171
- authUrl.searchParams.set('scope', CODEX_OAUTH_CONFIG.scopes);
246
+ authUrl.searchParams.set('scope', config.scopes);
172
247
  authUrl.searchParams.set('state', state);
173
248
  authUrl.searchParams.set('code_challenge', pkce.challenge);
174
249
  authUrl.searchParams.set('code_challenge_method', 'S256');
175
- if (CODEX_OAUTH_CONFIG.authorizeParams && typeof CODEX_OAUTH_CONFIG.authorizeParams === 'object') {
176
- for (const [key, value] of Object.entries(CODEX_OAUTH_CONFIG.authorizeParams)) {
250
+ if (config.authorizeParams && typeof config.authorizeParams === 'object') {
251
+ for (const [key, value] of Object.entries(config.authorizeParams)) {
177
252
  if (value !== undefined && value !== null && String(value).trim() !== '') {
178
253
  authUrl.searchParams.set(key, String(value));
179
254
  }
@@ -181,11 +256,19 @@ export async function loginWithBrowser(profileId, options = {}) {
181
256
  }
182
257
 
183
258
  return new Promise((resolve, reject) => {
259
+ let completed = false;
260
+ const finish = (fn) => {
261
+ if (completed) return;
262
+ completed = true;
263
+ clearTimeout(timeout);
264
+ fn();
265
+ };
266
+
184
267
  const server = http.createServer(async (req, res) => {
185
268
  try {
186
269
  const url = new URL(req.url, `http://localhost:${port}`);
187
-
188
- if (url.pathname !== CODEX_OAUTH_CONFIG.callbackPath) {
270
+
271
+ if (url.pathname !== config.callbackPath) {
189
272
  res.writeHead(404);
190
273
  res.end('Not found');
191
274
  return;
@@ -199,7 +282,7 @@ export async function loginWithBrowser(profileId, options = {}) {
199
282
  res.writeHead(400, { 'Content-Type': 'text/html' });
200
283
  res.end(`<h1>Authentication failed</h1><p>${error}</p>`);
201
284
  server.close();
202
- reject(new Error(`OAuth error: ${error}`));
285
+ finish(() => reject(new Error(`OAuth error: ${error}`)));
203
286
  return;
204
287
  }
205
288
 
@@ -207,24 +290,25 @@ export async function loginWithBrowser(profileId, options = {}) {
207
290
  res.writeHead(400, { 'Content-Type': 'text/html' });
208
291
  res.end('<h1>Invalid callback</h1><p>Missing or invalid state/code</p>');
209
292
  server.close();
210
- reject(new Error('Invalid OAuth callback'));
293
+ finish(() => reject(new Error('Invalid OAuth callback')));
211
294
  return;
212
295
  }
213
296
 
214
- // Exchange code for tokens
215
- const tokens = await exchangeCodeForTokens(code, pkce.verifier, redirectUri);
216
- await saveTokens(profileId, tokens);
297
+ const tokens = await exchangeCodeForTokens(code, pkce.verifier, redirectUri, {
298
+ subscriptionType: options.subscriptionType,
299
+ state
300
+ });
301
+ await saveTokens(tokenProfileKey, tokens);
217
302
 
218
303
  res.writeHead(200, { 'Content-Type': 'text/html' });
219
304
  res.end('<h1>Success!</h1><p>You can close this window and return to the terminal.</p>');
220
-
221
305
  server.close();
222
- resolve(true);
306
+ finish(() => resolve(true));
223
307
  } catch (err) {
224
308
  res.writeHead(500, { 'Content-Type': 'text/html' });
225
309
  res.end(`<h1>Error</h1><p>${err.message}</p>`);
226
310
  server.close();
227
- reject(err);
311
+ finish(() => reject(err));
228
312
  }
229
313
  });
230
314
 
@@ -236,10 +320,9 @@ export async function loginWithBrowser(profileId, options = {}) {
236
320
  }
237
321
  });
238
322
 
239
- // Timeout after 5 minutes
240
- setTimeout(() => {
323
+ const timeout = setTimeout(() => {
241
324
  server.close();
242
- reject(new Error('Login timed out after 5 minutes'));
325
+ finish(() => reject(new Error('Login timed out after 5 minutes')));
243
326
  }, 5 * 60 * 1000);
244
327
  });
245
328
  }
@@ -247,19 +330,26 @@ export async function loginWithBrowser(profileId, options = {}) {
247
330
  /**
248
331
  * Start device code OAuth login (for headless environments).
249
332
  * @param {string} profileId - Provider profile ID
333
+ * @param {Object} [options] - Options
334
+ * @param {string} [options.subscriptionType] - Subscription type
250
335
  * @param {function} [options.onCode] - Callback when device code is ready
251
336
  * @returns {Promise<boolean>} Success status
252
337
  */
253
338
  export async function loginWithDeviceCode(profileId, options = {}) {
254
- // Request device code
255
- const response = await fetch(CODEX_OAUTH_CONFIG.deviceCodeUrl, {
339
+ const config = resolveOAuthConfig(options.subscriptionType);
340
+ if (!config.deviceCodeUrl) {
341
+ throw new Error(`Device code OAuth flow is not supported for subscription type '${normalizeSubscriptionType(options.subscriptionType)}'.`);
342
+ }
343
+
344
+ const tokenProfileKey = toTokenProfileKey(profileId, options.subscriptionType);
345
+ const response = await fetch(config.deviceCodeUrl, {
256
346
  method: 'POST',
257
347
  headers: {
258
348
  'Content-Type': 'application/x-www-form-urlencoded'
259
349
  },
260
350
  body: new URLSearchParams({
261
- client_id: CODEX_OAUTH_CONFIG.clientId,
262
- scope: CODEX_OAUTH_CONFIG.scopes
351
+ client_id: config.clientId,
352
+ scope: config.scopes
263
353
  }).toString()
264
354
  });
265
355
 
@@ -275,18 +365,16 @@ export async function loginWithDeviceCode(profileId, options = {}) {
275
365
  const expiresIn = data.expires_in;
276
366
  const interval = data.interval || 5;
277
367
 
278
- // Notify user
279
368
  if (options.onCode) {
280
369
  options.onCode({ userCode, verificationUri, expiresIn });
281
370
  }
282
371
 
283
- // Poll for token
284
372
  const startTime = Date.now();
285
373
  while (Date.now() - startTime < expiresIn * 1000) {
286
- await new Promise(r => setTimeout(r, interval * 1000));
374
+ await new Promise((resolve) => setTimeout(resolve, interval * 1000));
287
375
 
288
376
  try {
289
- const tokenResponse = await fetch(CODEX_OAUTH_CONFIG.tokenUrl, {
377
+ const tokenResponse = await fetch(config.tokenUrl, {
290
378
  method: 'POST',
291
379
  headers: {
292
380
  'Content-Type': 'application/json'
@@ -294,29 +382,22 @@ export async function loginWithDeviceCode(profileId, options = {}) {
294
382
  body: JSON.stringify({
295
383
  grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
296
384
  device_code: deviceCode,
297
- client_id: CODEX_OAUTH_CONFIG.clientId
385
+ client_id: config.clientId
298
386
  })
299
387
  });
300
388
 
301
389
  if (tokenResponse.ok) {
302
390
  const tokenData = await tokenResponse.json();
303
- const tokens = {
304
- accessToken: tokenData.access_token,
305
- refreshToken: tokenData.refresh_token,
306
- expiresAt: Date.now() + (tokenData.expires_in * 1000),
307
- tokenType: tokenData.token_type || 'Bearer',
308
- scope: tokenData.scope
309
- };
310
- await saveTokens(profileId, tokens);
391
+ await saveTokens(tokenProfileKey, normalizeTokenData(tokenData));
311
392
  return true;
312
393
  }
313
394
 
314
395
  const errorData = await tokenResponse.json();
315
396
  if (errorData.error === 'authorization_pending') {
316
- continue; // Keep polling
397
+ continue;
317
398
  }
318
399
  if (errorData.error === 'slow_down') {
319
- await new Promise(r => setTimeout(r, interval * 1000));
400
+ await new Promise((resolve) => setTimeout(resolve, interval * 1000));
320
401
  continue;
321
402
  }
322
403
  throw new Error(`Token polling error: ${errorData.error}`);
@@ -334,32 +415,41 @@ export async function loginWithDeviceCode(profileId, options = {}) {
334
415
  /**
335
416
  * Logout (delete tokens) for a profile.
336
417
  * @param {string} profileId - Provider profile ID
418
+ * @param {Object} [options] - Options
419
+ * @param {string} [options.subscriptionType] - Subscription type
337
420
  */
338
- export async function logout(profileId) {
339
- await deleteTokens(profileId);
421
+ export async function logout(profileId, options = {}) {
422
+ const tokenProfileKey = toTokenProfileKey(profileId, options.subscriptionType);
423
+ await deleteTokens(tokenProfileKey);
340
424
  }
341
425
 
342
426
  /**
343
427
  * Check authentication status for a profile.
344
428
  * @param {string} profileId - Provider profile ID
429
+ * @param {Object} [options] - Options
430
+ * @param {string} [options.subscriptionType] - Subscription type
345
431
  * @returns {Promise<Object>} Status object
346
432
  */
347
- export async function getAuthStatus(profileId) {
348
- const tokens = await loadTokens(profileId);
349
-
433
+ export async function getAuthStatus(profileId, options = {}) {
434
+ const config = resolveOAuthConfig(options.subscriptionType);
435
+ const tokenProfileKey = toTokenProfileKey(profileId, options.subscriptionType);
436
+ const tokens = await loadTokens(tokenProfileKey);
437
+
350
438
  if (!tokens) {
351
439
  return {
352
440
  authenticated: false,
353
441
  profileId,
442
+ subscriptionType: normalizeSubscriptionType(options.subscriptionType),
354
443
  reason: 'No tokens found'
355
444
  };
356
445
  }
357
446
 
358
- const expired = isTokenExpired(tokens, CODEX_OAUTH_CONFIG.tokenRefreshBufferMs);
359
-
447
+ const expired = isTokenExpired(tokens, config.tokenRefreshBufferMs);
448
+
360
449
  return {
361
450
  authenticated: !expired,
362
451
  profileId,
452
+ subscriptionType: normalizeSubscriptionType(options.subscriptionType),
363
453
  expiresAt: tokens.expiresAt,
364
454
  expiresAtIso: new Date(tokens.expiresAt).toISOString(),
365
455
  expired,
@@ -369,8 +459,24 @@ export async function getAuthStatus(profileId) {
369
459
 
370
460
  /**
371
461
  * List all token profiles with stored subscription credentials.
462
+ * When subscriptionType is provided, returns only profiles for that type.
463
+ * @param {Object} [options] - Options
464
+ * @param {string} [options.subscriptionType] - Subscription type filter
372
465
  * @returns {Promise<string[]>} Profile IDs
373
466
  */
374
- export async function listTokenProfiles() {
375
- return listTokenProfilesFromStore();
467
+ export async function listTokenProfiles(options = {}) {
468
+ const requestedType = normalizeSubscriptionType(options.subscriptionType, { allowEmpty: true });
469
+ const profileKeys = await listTokenProfilesFromStore();
470
+ const visibleProfiles = [];
471
+ const seen = new Set();
472
+
473
+ for (const profileKey of profileKeys) {
474
+ const parsed = fromTokenProfileKey(profileKey);
475
+ if (requestedType && parsed.subscriptionType !== requestedType) continue;
476
+ if (seen.has(parsed.profileId)) continue;
477
+ seen.add(parsed.profileId);
478
+ visibleProfiles.push(parsed.profileId);
479
+ }
480
+
481
+ return visibleProfiles;
376
482
  }
@@ -9,6 +9,17 @@ export const CODEX_SUBSCRIPTION_MODELS = Object.freeze([
9
9
  'gpt-5.1-codex-mini'
10
10
  ]);
11
11
 
12
+ /**
13
+ * Hardcoded Claude Code subscription models.
14
+ * These defaults mirror current Claude Code model naming.
15
+ * Users can still customize the final saved model list.
16
+ */
17
+ export const CLAUDE_CODE_SUBSCRIPTION_MODELS = Object.freeze([
18
+ 'claude-sonnet-4-6',
19
+ 'claude-opus-4-6',
20
+ 'claude-haiku-4-5'
21
+ ]);
22
+
12
23
  /**
13
24
  * OAuth configuration for ChatGPT Codex subscription.
14
25
  */
@@ -28,6 +39,27 @@ export const CODEX_OAUTH_CONFIG = Object.freeze({
28
39
  tokenRefreshBufferMs: 5 * 60 * 1000 // 5 minutes before expiration
29
40
  });
30
41
 
42
+ /**
43
+ * OAuth configuration for Claude Code subscription.
44
+ * Values align with the current Claude Code CLI runtime.
45
+ */
46
+ export const CLAUDE_CODE_OAUTH_CONFIG = Object.freeze({
47
+ authorizeUrl: 'https://claude.ai/oauth/authorize',
48
+ tokenUrl: 'https://platform.claude.com/v1/oauth/token',
49
+ callbackPort: 1456,
50
+ callbackPath: '/callback',
51
+ manualRedirectUrl: 'https://platform.claude.com/oauth/code/callback',
52
+ scopes: 'user:profile user:inference user:sessions:claude_code user:mcp_servers',
53
+ clientId: '9d1c250a-e61b-44d9-88ed-5944d1962f5e',
54
+ authorizeParams: Object.freeze({
55
+ code: 'true'
56
+ }),
57
+ oauthBeta: 'oauth-2025-04-20',
58
+ apiBaseUrl: 'https://api.anthropic.com',
59
+ messagesPath: '/v1/messages?beta=true',
60
+ tokenRefreshBufferMs: 5 * 60 * 1000
61
+ });
62
+
31
63
  /**
32
64
  * Token storage directory relative to home.
33
65
  */