@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.
- package/CHANGELOG.md +23 -0
- package/README.md +15 -4
- package/package.json +1 -1
- package/src/cli/router-module.js +190 -64
- package/src/node/config-workflows.js +3 -1
- package/src/runtime/config.js +19 -15
- package/src/runtime/handler/provider-call.js +135 -105
- package/src/runtime/subscription-auth.js +200 -94
- package/src/runtime/subscription-constants.js +32 -0
- package/src/runtime/subscription-provider.js +156 -10
|
@@ -1,13 +1,67 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* OAuth authentication for
|
|
3
|
-
*
|
|
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 {
|
|
10
|
-
|
|
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
|
|
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
|
|
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,
|
|
164
|
+
if (isTokenExpired(tokens, config.tokenRefreshBufferMs)) {
|
|
96
165
|
if (!tokens.refreshToken) {
|
|
97
|
-
return null;
|
|
166
|
+
return null;
|
|
98
167
|
}
|
|
99
168
|
|
|
100
169
|
try {
|
|
101
|
-
const newTokens = await refreshAccessToken(tokens.refreshToken
|
|
102
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
162
|
-
const
|
|
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(
|
|
241
|
+
|
|
242
|
+
const authUrl = new URL(config.authorizeUrl);
|
|
168
243
|
authUrl.searchParams.set('response_type', 'code');
|
|
169
|
-
authUrl.searchParams.set('client_id',
|
|
244
|
+
authUrl.searchParams.set('client_id', config.clientId);
|
|
170
245
|
authUrl.searchParams.set('redirect_uri', redirectUri);
|
|
171
|
-
authUrl.searchParams.set('scope',
|
|
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 (
|
|
176
|
-
for (const [key, value] of Object.entries(
|
|
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 !==
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
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
|
-
|
|
255
|
-
|
|
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:
|
|
262
|
-
scope:
|
|
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(
|
|
374
|
+
await new Promise((resolve) => setTimeout(resolve, interval * 1000));
|
|
287
375
|
|
|
288
376
|
try {
|
|
289
|
-
const tokenResponse = await fetch(
|
|
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:
|
|
385
|
+
client_id: config.clientId
|
|
298
386
|
})
|
|
299
387
|
});
|
|
300
388
|
|
|
301
389
|
if (tokenResponse.ok) {
|
|
302
390
|
const tokenData = await tokenResponse.json();
|
|
303
|
-
|
|
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;
|
|
397
|
+
continue;
|
|
317
398
|
}
|
|
318
399
|
if (errorData.error === 'slow_down') {
|
|
319
|
-
await new Promise(
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
*/
|