claude-codex-proxy 1.0.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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +227 -0
  3. package/bin/cli.js +113 -0
  4. package/docs/ACCOUNTS.md +202 -0
  5. package/docs/API.md +244 -0
  6. package/docs/ARCHITECTURE.md +119 -0
  7. package/docs/CLAUDE_INTEGRATION.md +163 -0
  8. package/docs/OAUTH.md +83 -0
  9. package/docs/OPENCLAW.md +33 -0
  10. package/docs/legal.md +9 -0
  11. package/images/demo-screenshot.png +0 -0
  12. package/images/f757093f-507b-4453-994e-f8275f8b07a9.png +0 -0
  13. package/package.json +56 -0
  14. package/public/css/style.css +791 -0
  15. package/public/index.html +838 -0
  16. package/public/js/app.js +619 -0
  17. package/src/account-manager.js +526 -0
  18. package/src/account-rotation/index.js +93 -0
  19. package/src/account-rotation/rate-limits.js +293 -0
  20. package/src/account-rotation/strategies/base-strategy.js +48 -0
  21. package/src/account-rotation/strategies/index.js +31 -0
  22. package/src/account-rotation/strategies/round-robin-strategy.js +42 -0
  23. package/src/account-rotation/strategies/sticky-strategy.js +97 -0
  24. package/src/claude-config.js +154 -0
  25. package/src/cli/accounts.js +551 -0
  26. package/src/direct-api.js +214 -0
  27. package/src/format-converter.js +563 -0
  28. package/src/index.js +45 -0
  29. package/src/middleware/credentials.js +116 -0
  30. package/src/middleware/sse.js +130 -0
  31. package/src/model-api.js +189 -0
  32. package/src/model-mapper.js +88 -0
  33. package/src/oauth.js +623 -0
  34. package/src/response-streamer.js +469 -0
  35. package/src/routes/accounts-route.js +296 -0
  36. package/src/routes/api-routes.js +102 -0
  37. package/src/routes/chat-route.js +239 -0
  38. package/src/routes/claude-config-route.js +114 -0
  39. package/src/routes/logs-route.js +43 -0
  40. package/src/routes/messages-route.js +164 -0
  41. package/src/routes/models-route.js +115 -0
  42. package/src/routes/settings-route.js +154 -0
  43. package/src/server-settings.js +70 -0
  44. package/src/server.js +57 -0
  45. package/src/signature-cache.js +106 -0
  46. package/src/thinking-utils.js +312 -0
  47. package/src/utils/logger.js +170 -0
package/src/oauth.js ADDED
@@ -0,0 +1,623 @@
1
+ /**
2
+ * OpenAI/ChatGPT OAuth Module
3
+ * Handles OAuth 2.0 with PKCE for ChatGPT authentication
4
+ */
5
+
6
+ import crypto from 'crypto';
7
+ import http from 'http';
8
+ import { exec } from 'child_process';
9
+ import { promisify } from 'util';
10
+
11
+ const execAsync = promisify(exec);
12
+
13
+ // OpenAI OAuth Configuration (from Codex app)
14
+ const OAUTH_CONFIG = {
15
+ clientId: 'app_EMoamEEZ73f0CkXaXp7hrann',
16
+ authUrl: 'https://auth.openai.com/oauth/authorize',
17
+ tokenUrl: 'https://auth.openai.com/oauth/token',
18
+ logoutUrl: 'https://auth.openai.com/logout',
19
+ userInfoUrl: 'https://api.openai.com/v1/me',
20
+ scopes: ['openid', 'profile', 'email', 'offline_access'],
21
+ callbackPort: 1455,
22
+ callbackFallbackPorts: [1456, 1457, 1458, 1459, 1460],
23
+ callbackPath: '/auth/callback'
24
+ };
25
+
26
+ // Store PKCE verifiers temporarily (in production, use proper session storage)
27
+ const pkceStore = new Map();
28
+
29
+ /**
30
+ * Generate PKCE code verifier and challenge
31
+ * @returns {{verifier: string, challenge: string}}
32
+ */
33
+ function generatePKCE() {
34
+ const verifier = crypto.randomBytes(32).toString('base64url');
35
+ const challenge = crypto
36
+ .createHash('sha256')
37
+ .update(verifier)
38
+ .digest('base64url');
39
+ return { verifier, challenge };
40
+ }
41
+
42
+ /**
43
+ * Generate random state for CSRF protection
44
+ * @returns {string}
45
+ */
46
+ function generateState() {
47
+ return crypto.randomBytes(16).toString('hex');
48
+ }
49
+
50
+ /**
51
+ * Decode JWT token without verification (for extracting claims)
52
+ * @param {string} token - JWT token
53
+ * @returns {object} Decoded payload
54
+ */
55
+ function decodeJWT(token) {
56
+ try {
57
+ const parts = token.split('.');
58
+ if (parts.length !== 3) return null;
59
+ const payload = Buffer.from(parts[1], 'base64').toString('utf8');
60
+ return JSON.parse(payload);
61
+ } catch (e) {
62
+ return null;
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Extract account info from access token
68
+ * @param {string} accessToken - JWT access token
69
+ * @returns {{accountId: string, planType: string, userId: string, email: string}}
70
+ */
71
+ function extractAccountInfo(accessToken) {
72
+ const payload = decodeJWT(accessToken);
73
+ if (!payload) return null;
74
+
75
+ const authInfo = payload['https://api.openai.com/auth'] || {};
76
+ const profileInfo = payload['https://api.openai.com/profile'] || {};
77
+
78
+ return {
79
+ accountId: authInfo.chatgpt_account_id || null,
80
+ planType: authInfo.chatgpt_plan_type || 'free',
81
+ userId: authInfo.chatgpt_user_id || payload.sub || null,
82
+ email: profileInfo.email || payload.email || null,
83
+ expiresAt: payload.exp ? payload.exp * 1000 : null
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Get authorization URL for OAuth flow
89
+ * @param {string} verifier - PKCE code verifier
90
+ * @param {string} state - CSRF state
91
+ * @param {number} port - Callback server port
92
+ * @returns {string} Authorization URL
93
+ */
94
+ function getAuthorizationUrl(verifier, state, port) {
95
+ const { challenge } = generatePKCEFromVerifier(verifier);
96
+ const redirectUri = `http://localhost:${port}${OAUTH_CONFIG.callbackPath}`;
97
+
98
+ pkceStore.set(state, { verifier, port, createdAt: Date.now() });
99
+
100
+ // Clean up old entries
101
+ for (const [key, value] of pkceStore.entries()) {
102
+ if (Date.now() - value.createdAt > 5 * 60 * 1000) {
103
+ pkceStore.delete(key);
104
+ }
105
+ }
106
+
107
+ const params = new URLSearchParams({
108
+ response_type: 'code',
109
+ client_id: OAUTH_CONFIG.clientId,
110
+ redirect_uri: redirectUri,
111
+ scope: OAUTH_CONFIG.scopes.join(' '),
112
+ code_challenge: challenge,
113
+ code_challenge_method: 'S256',
114
+ state: state,
115
+ id_token_add_organizations: 'true',
116
+ codex_cli_simplified_flow: 'true',
117
+ originator: 'codex_cli_rs',
118
+ prompt: 'login', // Force login screen for multi-account support
119
+ max_age: '0' // Force re-authentication
120
+ });
121
+
122
+ const url = `${OAUTH_CONFIG.authUrl}?${params.toString()}`;
123
+ console.log(`[OAuth] Generated Authorization URL: ${url}`);
124
+ return url;
125
+ }
126
+
127
+ /**
128
+ * Modern Success/Error templates for better UX
129
+ */
130
+ function getSuccessHtml(message) {
131
+ return `
132
+ <!DOCTYPE html>
133
+ <html>
134
+ <head>
135
+ <meta charset="UTF-8">
136
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
137
+ <title>Authentication Successful</title>
138
+ <style>
139
+ body { font-family: 'Inter', system-ui, -apple-system, sans-serif; background: #0f172a; color: #f8fafc; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; }
140
+ .card { background: #1e293b; padding: 3rem; border-radius: 1rem; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5); text-align: center; max-width: 400px; border: 1px solid #334155; }
141
+ .icon { font-size: 4rem; margin-bottom: 1.5rem; display: block; }
142
+ h1 { margin: 0 0 1rem; color: #10b981; font-weight: 700; }
143
+ p { color: #94a3b8; line-height: 1.6; font-size: 1.1rem; }
144
+ .footer { margin-top: 2rem; font-size: 0.9rem; color: #64748b; }
145
+ </style>
146
+ </head>
147
+ <body>
148
+ <div class="card">
149
+ <span class="icon">✅</span>
150
+ <h1>Success!</h1>
151
+ <p>\${message}</p>
152
+ <div class="footer">You can close this window and return to the app.</div>
153
+ </div>
154
+ <script>
155
+ if (window.opener) {
156
+ window.opener.postMessage({ type: 'oauth-success' }, '*');
157
+ }
158
+ setTimeout(() => window.close(), 3000);
159
+ </script>
160
+ </body>
161
+ </html>
162
+ `;
163
+ }
164
+
165
+ function getErrorHtml(error) {
166
+ return `
167
+ <!DOCTYPE html>
168
+ <html>
169
+ <head>
170
+ <meta charset="UTF-8">
171
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
172
+ <title>Authentication Failed</title>
173
+ <style>
174
+ body { font-family: 'Inter', system-ui, -apple-system, sans-serif; background: #0f172a; color: #f8fafc; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; }
175
+ .card { background: #1e293b; padding: 3rem; border-radius: 1rem; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5); text-align: center; max-width: 400px; border: 1px solid #334155; }
176
+ .icon { font-size: 4rem; margin-bottom: 1.5rem; display: block; }
177
+ h1 { margin: 0 0 1rem; color: #ef4444; font-weight: 700; }
178
+ p { color: #94a3b8; line-height: 1.6; font-size: 1.1rem; }
179
+ </style>
180
+ </head>
181
+ <body>
182
+ <div class="card">
183
+ <span class="icon">❌</span>
184
+ <h1>Failed</h1>
185
+ <p>Authentication could not be completed.</p>
186
+ <div style="background: rgba(239, 68, 68, 0.1); padding: 1rem; border-radius: 0.5rem; color: #fca5a5; margin-top: 1rem; font-family: monospace; font-size: 0.9rem;">
187
+ \${error}
188
+ </div>
189
+ <p style="margin-top: 1.5rem; font-size: 0.9rem;">Please close this window and try again.</p>
190
+ </div>
191
+ </body>
192
+ </html>
193
+ `;
194
+ }
195
+
196
+ function getLogoutThenAuthUrl(verifier, state, port) {
197
+ const authUrl = getAuthorizationUrl(verifier, state, port);
198
+ // Note: auth.openai.com/logout doesn't always support 'continue' reliably for all users
199
+ // prompt=login in getAuthorizationUrl is the preferred way now.
200
+ return authUrl;
201
+ }
202
+
203
+ /**
204
+ * Generate challenge from verifier
205
+ * @param {string} verifier - PKCE code verifier
206
+ * @returns {{challenge: string}}
207
+ */
208
+ function generatePKCEFromVerifier(verifier) {
209
+ const challenge = crypto
210
+ .createHash('sha256')
211
+ .update(verifier)
212
+ .digest('base64url');
213
+ return { challenge };
214
+ }
215
+
216
+ /**
217
+ * Get stored PKCE data for a state
218
+ * @param {string} state - OAuth state
219
+ * @returns {{verifier: string, port: number}|null}
220
+ */
221
+ function getPKCEData(state) {
222
+ return pkceStore.get(state) || null;
223
+ }
224
+
225
+ /**
226
+ * Attempt to bind server to a specific port
227
+ * @param {http.Server} server - HTTP server instance
228
+ * @param {number} port - Port to bind to
229
+ * @param {string} host - Host to bind to
230
+ * @returns {Promise<number>} Resolves with port on success, rejects on error
231
+ */
232
+ function tryBindPort(server, port, host = '0.0.0.0') {
233
+ return new Promise((resolve, reject) => {
234
+ const onError = (err) => {
235
+ server.removeListener('listening', onSuccess);
236
+ reject(err);
237
+ };
238
+ const onSuccess = () => {
239
+ server.removeListener('error', onError);
240
+ resolve(port);
241
+ };
242
+ server.once('error', onError);
243
+ server.once('listening', onSuccess);
244
+ server.listen(port, host);
245
+ });
246
+ }
247
+
248
+ /**
249
+ * Start local callback server with port fallback and abort support
250
+ * @param {string} expectedState - Expected state for validation
251
+ * @param {number} timeoutMs - Timeout in milliseconds
252
+ * @returns {{promise: Promise<string>, abort: Function, getPort: Function}}
253
+ */
254
+ function startCallbackServer(expectedState, timeoutMs = 120000) {
255
+ let server = null;
256
+ let timeoutId = null;
257
+ let isAborted = false;
258
+ let actualPort = OAUTH_CONFIG.callbackPort;
259
+ const host = process.env.HOST || '0.0.0.0';
260
+
261
+ const promise = new Promise(async (resolve, reject) => {
262
+ const portsToTry = [OAUTH_CONFIG.callbackPort, ...(OAUTH_CONFIG.callbackFallbackPorts || [])];
263
+ const errors = [];
264
+
265
+ server = http.createServer((req, res) => {
266
+ const url = new URL(req.url, `http://${host === '0.0.0.0' ? 'localhost' : host}:${actualPort}`);
267
+ console.log(`[OAuth] Received request: ${req.method} ${req.url}`);
268
+
269
+ if (url.pathname !== OAUTH_CONFIG.callbackPath && url.pathname !== '/success') {
270
+ res.writeHead(404);
271
+ res.end('Not found');
272
+ return;
273
+ }
274
+
275
+ const code = url.searchParams.get('code');
276
+ const state = url.searchParams.get('state');
277
+ const error = url.searchParams.get('error');
278
+ const idToken = url.searchParams.get('id_token');
279
+
280
+ if (error) {
281
+ console.error(`[OAuth] Error in callback: ${error}`);
282
+ res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
283
+ res.end(getErrorHtml(error));
284
+ server.close();
285
+ reject(new Error(`OAuth error: ${error}`));
286
+ return;
287
+ }
288
+
289
+ if (code) {
290
+ console.log('[OAuth] Got authorization code');
291
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
292
+ res.end(getSuccessHtml('Authentication Successful! You can close this window.'));
293
+
294
+ setTimeout(() => {
295
+ server.close();
296
+ clearTimeout(timeoutId);
297
+ resolve(code);
298
+ }, 1000);
299
+ return;
300
+ }
301
+
302
+ if (url.pathname === '/success' || idToken) {
303
+ console.log('[OAuth] At success page');
304
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
305
+ res.end(getSuccessHtml('Login Successful!'));
306
+ return;
307
+ }
308
+
309
+ res.writeHead(400);
310
+ res.end('Waiting for authorization code...');
311
+ });
312
+
313
+ // Try ports with fallback logic (Windows EACCES fix)
314
+ let boundSuccessfully = false;
315
+ for (const port of portsToTry) {
316
+ try {
317
+ await tryBindPort(server, port, host);
318
+ actualPort = port;
319
+ boundSuccessfully = true;
320
+
321
+ if (port !== OAUTH_CONFIG.callbackPort) {
322
+ console.log(`[OAuth] Primary port ${OAUTH_CONFIG.callbackPort} unavailable, using fallback port ${port}`);
323
+ } else {
324
+ console.log(`[OAuth] Callback server listening on ${host}:${port}`);
325
+ }
326
+ break;
327
+ } catch (err) {
328
+ const errMsg = err.code === 'EACCES'
329
+ ? `Permission denied on port ${port}`
330
+ : err.code === 'EADDRINUSE'
331
+ ? `Port ${port} already in use`
332
+ : `Failed to bind port ${port}: ${err.message}`;
333
+ errors.push(errMsg);
334
+ console.log(`[OAuth] ${errMsg}`);
335
+ }
336
+ }
337
+
338
+ if (!boundSuccessfully) {
339
+ const isWindows = process.platform === 'win32';
340
+ let errorMsg = `Failed to start OAuth callback server.\nTried ports: ${portsToTry.join(', ')}\n\nErrors:\n${errors.join('\n')}`;
341
+
342
+ if (isWindows) {
343
+ errorMsg += `\n
344
+ ================== WINDOWS TROUBLESHOOTING ==================
345
+ The default port range may be reserved by Hyper-V/WSL2/Docker.
346
+
347
+ Option 1: Use a custom port
348
+ Set OAUTH_CALLBACK_PORT=3456 in your environment
349
+
350
+ Option 2: Reset Windows NAT (run as Administrator)
351
+ net stop winnat && net start winnat
352
+
353
+ Option 3: Check reserved port ranges
354
+ netsh interface ipv4 show excludedportrange protocol=tcp
355
+ ==============================================================`;
356
+ } else {
357
+ errorMsg += `\n\nTry setting a custom port via environment variable.`;
358
+ }
359
+
360
+ reject(new Error(errorMsg));
361
+ return;
362
+ }
363
+
364
+ timeoutId = setTimeout(() => {
365
+ if (!isAborted) {
366
+ server.close();
367
+ reject(new Error('OAuth callback timeout - no response received'));
368
+ }
369
+ }, timeoutMs);
370
+ });
371
+
372
+ const abort = () => {
373
+ if (isAborted) return;
374
+ isAborted = true;
375
+ if (timeoutId) {
376
+ clearTimeout(timeoutId);
377
+ }
378
+ if (server) {
379
+ server.close();
380
+ console.log('[OAuth] Callback server aborted (manual completion)');
381
+ }
382
+ };
383
+
384
+ const getPort = () => actualPort;
385
+
386
+ return { promise, abort, getPort };
387
+ }
388
+
389
+ /**
390
+ * Exchange authorization code for tokens
391
+ * @param {string} code - Authorization code
392
+ * @param {string} verifier - PKCE code verifier
393
+ * @param {number} port - Callback port used
394
+ * @returns {Promise<{accessToken: string, refreshToken: string, idToken: string, expiresIn: number}>}
395
+ */
396
+ async function exchangeCodeForTokens(code, verifier, port) {
397
+ const redirectUri = `http://localhost:${port}${OAUTH_CONFIG.callbackPath}`;
398
+
399
+ const response = await fetch(OAUTH_CONFIG.tokenUrl, {
400
+ method: 'POST',
401
+ headers: {
402
+ 'Content-Type': 'application/x-www-form-urlencoded'
403
+ },
404
+ body: new URLSearchParams({
405
+ grant_type: 'authorization_code',
406
+ code: code,
407
+ redirect_uri: redirectUri,
408
+ client_id: OAUTH_CONFIG.clientId,
409
+ code_verifier: verifier
410
+ })
411
+ });
412
+
413
+ if (!response.ok) {
414
+ const error = await response.text();
415
+ throw new Error(`Token exchange failed: ${response.status} - ${error}`);
416
+ }
417
+
418
+ const tokens = await response.json();
419
+
420
+ if (!tokens.access_token) {
421
+ throw new Error('No access token in response');
422
+ }
423
+
424
+ return {
425
+ accessToken: tokens.access_token,
426
+ refreshToken: tokens.refresh_token,
427
+ idToken: tokens.id_token,
428
+ expiresIn: tokens.expires_in
429
+ };
430
+ }
431
+
432
+ /**
433
+ * Refresh access token using refresh token
434
+ * @param {string} refreshToken - OAuth refresh token
435
+ * @returns {Promise<{accessToken: string, refreshToken: string, expiresIn: number}>}
436
+ */
437
+ async function refreshAccessToken(refreshToken) {
438
+ const response = await fetch(OAUTH_CONFIG.tokenUrl, {
439
+ method: 'POST',
440
+ headers: {
441
+ 'Content-Type': 'application/x-www-form-urlencoded'
442
+ },
443
+ body: new URLSearchParams({
444
+ grant_type: 'refresh_token',
445
+ refresh_token: refreshToken,
446
+ client_id: OAUTH_CONFIG.clientId
447
+ })
448
+ });
449
+
450
+ if (!response.ok) {
451
+ const error = await response.text();
452
+ throw new Error(`Token refresh failed: ${response.status} - ${error}`);
453
+ }
454
+
455
+ const tokens = await response.json();
456
+
457
+ return {
458
+ accessToken: tokens.access_token,
459
+ refreshToken: tokens.refresh_token || refreshToken,
460
+ idToken: tokens.id_token,
461
+ expiresIn: tokens.expires_in
462
+ };
463
+ }
464
+
465
+ /**
466
+ * Open URL in default browser
467
+ * @param {string} url - URL to open
468
+ */
469
+ async function openBrowser(url) {
470
+ const platform = process.platform;
471
+
472
+ try {
473
+ if (platform === 'darwin') {
474
+ await execAsync(`open "${url}"`);
475
+ } else if (platform === 'win32') {
476
+ await execAsync(`start "" "${url}"`);
477
+ } else {
478
+ await execAsync(`xdg-open "${url}"`);
479
+ }
480
+ } catch (e) {
481
+ console.log(`[OAuth] Could not open browser automatically. Please visit:\n${url}`);
482
+ }
483
+ }
484
+
485
+ /**
486
+ * Complete OAuth flow - returns full account info
487
+ * @param {number} [customPort] - Optional custom port for callback
488
+ * @returns {Promise<{email: string, accountId: string, planType: string, accessToken: string, refreshToken: string}>}
489
+ */
490
+ async function performOAuthFlow(customPort) {
491
+ const port = customPort || OAUTH_CONFIG.callbackPort;
492
+ const { verifier } = generatePKCE();
493
+ const state = generateState();
494
+
495
+ // Get authorization URL
496
+ const authUrl = getAuthorizationUrl(verifier, state, port);
497
+
498
+ // Start callback server
499
+ const { promise: callbackPromise, server } = startCallbackServer(port, state);
500
+
501
+ console.log(`\n[OAuth] Starting authentication flow...`);
502
+ console.log(`[OAuth] Callback URL: http://localhost:${port}${OAUTH_CONFIG.callbackPath}`);
503
+
504
+ // Open browser
505
+ await openBrowser(authUrl);
506
+
507
+ console.log(`\n[OAuth] Waiting for authentication...`);
508
+ console.log(`[OAuth] If browser didn't open, visit:\n${authUrl}\n`);
509
+
510
+ // Wait for callback
511
+ const code = await callbackPromise;
512
+ console.log(`[OAuth] Received authorization code`);
513
+
514
+ // Exchange code for tokens
515
+ console.log(`[OAuth] Exchanging code for tokens...`);
516
+ const tokens = await exchangeCodeForTokens(code, verifier, port);
517
+ console.log(`[OAuth] Token exchange successful`);
518
+
519
+ // Extract account info from access token
520
+ const accountInfo = extractAccountInfo(tokens.accessToken);
521
+
522
+ return {
523
+ email: accountInfo?.email || 'unknown',
524
+ accountId: accountInfo?.accountId,
525
+ planType: accountInfo?.planType || 'free',
526
+ accessToken: tokens.accessToken,
527
+ refreshToken: tokens.refreshToken,
528
+ idToken: tokens.idToken,
529
+ expiresAt: accountInfo?.expiresAt || (Date.now() + tokens.expiresIn * 1000)
530
+ };
531
+ }
532
+
533
+ /**
534
+ * Handle OAuth callback from web flow
535
+ * @param {string} code - Authorization code
536
+ * @param {string} state - OAuth state
537
+ * @returns {Promise<{email: string, accountId: string, planType: string, accessToken: string, refreshToken: string}>}
538
+ */
539
+ async function handleOAuthCallback(code, state) {
540
+ const pkceData = getPKCEData(state);
541
+ if (!pkceData) {
542
+ throw new Error('Invalid or expired OAuth state');
543
+ }
544
+
545
+ const tokens = await exchangeCodeForTokens(code, pkceData.verifier, pkceData.port);
546
+ const accountInfo = extractAccountInfo(tokens.accessToken);
547
+
548
+ // Clean up
549
+ pkceStore.delete(state);
550
+
551
+ return {
552
+ email: accountInfo?.email || 'unknown',
553
+ accountId: accountInfo?.accountId,
554
+ planType: accountInfo?.planType || 'free',
555
+ accessToken: tokens.accessToken,
556
+ refreshToken: tokens.refreshToken,
557
+ idToken: tokens.idToken,
558
+ expiresAt: accountInfo?.expiresAt || (Date.now() + tokens.expiresIn * 1000)
559
+ };
560
+ }
561
+
562
+ export function extractCodeFromInput(input) {
563
+ if (!input || typeof input !== 'string') {
564
+ throw new Error('No input provided');
565
+ }
566
+
567
+ const trimmed = input.trim();
568
+
569
+ if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
570
+ try {
571
+ const url = new URL(trimmed);
572
+ const code = url.searchParams.get('code');
573
+ const state = url.searchParams.get('state');
574
+ const error = url.searchParams.get('error');
575
+
576
+ if (error) {
577
+ throw new Error(`OAuth error: ${error}`);
578
+ }
579
+
580
+ if (!code) {
581
+ throw new Error('No authorization code found in URL');
582
+ }
583
+
584
+ return { code, state };
585
+ } catch (e) {
586
+ if (e.message.includes('OAuth error') || e.message.includes('No authorization code')) {
587
+ throw e;
588
+ }
589
+ throw new Error('Invalid URL format');
590
+ }
591
+ }
592
+
593
+ if (trimmed.length < 10) {
594
+ throw new Error('Input is too short to be a valid authorization code');
595
+ }
596
+
597
+ return { code: trimmed, state: null };
598
+ }
599
+
600
+ export {
601
+ OAUTH_CONFIG,
602
+ generatePKCE,
603
+ generateState,
604
+ decodeJWT,
605
+ extractAccountInfo,
606
+ getAuthorizationUrl,
607
+ getLogoutThenAuthUrl,
608
+ startCallbackServer,
609
+ exchangeCodeForTokens,
610
+ refreshAccessToken,
611
+ openBrowser,
612
+ performOAuthFlow,
613
+ handleOAuthCallback,
614
+ getPKCEData
615
+ };
616
+
617
+ export default {
618
+ performOAuthFlow,
619
+ handleOAuthCallback,
620
+ refreshAccessToken,
621
+ extractAccountInfo,
622
+ extractCodeFromInput
623
+ };