commons-proxy 2.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 (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +757 -0
  3. package/bin/cli.js +146 -0
  4. package/package.json +97 -0
  5. package/public/Complaint Details.pdf +0 -0
  6. package/public/Cyber Crime Portal.pdf +0 -0
  7. package/public/app.js +229 -0
  8. package/public/css/src/input.css +523 -0
  9. package/public/css/style.css +1 -0
  10. package/public/favicon.png +0 -0
  11. package/public/index.html +549 -0
  12. package/public/js/components/account-manager.js +356 -0
  13. package/public/js/components/add-account-modal.js +414 -0
  14. package/public/js/components/claude-config.js +420 -0
  15. package/public/js/components/dashboard/charts.js +605 -0
  16. package/public/js/components/dashboard/filters.js +362 -0
  17. package/public/js/components/dashboard/stats.js +110 -0
  18. package/public/js/components/dashboard.js +236 -0
  19. package/public/js/components/logs-viewer.js +100 -0
  20. package/public/js/components/models.js +36 -0
  21. package/public/js/components/server-config.js +349 -0
  22. package/public/js/config/constants.js +102 -0
  23. package/public/js/data-store.js +375 -0
  24. package/public/js/settings-store.js +58 -0
  25. package/public/js/store.js +99 -0
  26. package/public/js/translations/en.js +367 -0
  27. package/public/js/translations/id.js +412 -0
  28. package/public/js/translations/pt.js +308 -0
  29. package/public/js/translations/tr.js +358 -0
  30. package/public/js/translations/zh.js +373 -0
  31. package/public/js/utils/account-actions.js +189 -0
  32. package/public/js/utils/error-handler.js +96 -0
  33. package/public/js/utils/model-config.js +42 -0
  34. package/public/js/utils/ui-logger.js +143 -0
  35. package/public/js/utils/validators.js +77 -0
  36. package/public/js/utils.js +69 -0
  37. package/public/proxy-server-64.png +0 -0
  38. package/public/views/accounts.html +361 -0
  39. package/public/views/dashboard.html +484 -0
  40. package/public/views/logs.html +97 -0
  41. package/public/views/models.html +331 -0
  42. package/public/views/settings.html +1327 -0
  43. package/src/account-manager/credentials.js +378 -0
  44. package/src/account-manager/index.js +462 -0
  45. package/src/account-manager/onboarding.js +112 -0
  46. package/src/account-manager/rate-limits.js +369 -0
  47. package/src/account-manager/storage.js +160 -0
  48. package/src/account-manager/strategies/base-strategy.js +109 -0
  49. package/src/account-manager/strategies/hybrid-strategy.js +339 -0
  50. package/src/account-manager/strategies/index.js +79 -0
  51. package/src/account-manager/strategies/round-robin-strategy.js +76 -0
  52. package/src/account-manager/strategies/sticky-strategy.js +138 -0
  53. package/src/account-manager/strategies/trackers/health-tracker.js +162 -0
  54. package/src/account-manager/strategies/trackers/index.js +9 -0
  55. package/src/account-manager/strategies/trackers/quota-tracker.js +120 -0
  56. package/src/account-manager/strategies/trackers/token-bucket-tracker.js +155 -0
  57. package/src/auth/database.js +169 -0
  58. package/src/auth/oauth.js +548 -0
  59. package/src/auth/token-extractor.js +117 -0
  60. package/src/cli/accounts.js +648 -0
  61. package/src/cloudcode/index.js +29 -0
  62. package/src/cloudcode/message-handler.js +510 -0
  63. package/src/cloudcode/model-api.js +248 -0
  64. package/src/cloudcode/rate-limit-parser.js +235 -0
  65. package/src/cloudcode/request-builder.js +93 -0
  66. package/src/cloudcode/session-manager.js +47 -0
  67. package/src/cloudcode/sse-parser.js +121 -0
  68. package/src/cloudcode/sse-streamer.js +293 -0
  69. package/src/cloudcode/streaming-handler.js +615 -0
  70. package/src/config.js +125 -0
  71. package/src/constants.js +407 -0
  72. package/src/errors.js +242 -0
  73. package/src/fallback-config.js +29 -0
  74. package/src/format/content-converter.js +193 -0
  75. package/src/format/index.js +20 -0
  76. package/src/format/request-converter.js +255 -0
  77. package/src/format/response-converter.js +120 -0
  78. package/src/format/schema-sanitizer.js +673 -0
  79. package/src/format/signature-cache.js +88 -0
  80. package/src/format/thinking-utils.js +648 -0
  81. package/src/index.js +148 -0
  82. package/src/modules/usage-stats.js +205 -0
  83. package/src/providers/anthropic-provider.js +258 -0
  84. package/src/providers/base-provider.js +157 -0
  85. package/src/providers/cloudcode.js +94 -0
  86. package/src/providers/copilot.js +399 -0
  87. package/src/providers/github-provider.js +287 -0
  88. package/src/providers/google-provider.js +192 -0
  89. package/src/providers/index.js +211 -0
  90. package/src/providers/openai-compatible.js +265 -0
  91. package/src/providers/openai-provider.js +271 -0
  92. package/src/providers/openrouter-provider.js +325 -0
  93. package/src/providers/setup.js +83 -0
  94. package/src/server.js +870 -0
  95. package/src/utils/claude-config.js +245 -0
  96. package/src/utils/helpers.js +51 -0
  97. package/src/utils/logger.js +142 -0
  98. package/src/utils/native-module-helper.js +162 -0
  99. package/src/webui/index.js +1134 -0
@@ -0,0 +1,548 @@
1
+ /**
2
+ * Google OAuth with PKCE for Antigravity
3
+ *
4
+ * Implements the same OAuth flow as opencode-cloudcode-auth
5
+ * to obtain refresh tokens for multiple Google accounts.
6
+ * Uses a local callback server to automatically capture the auth code.
7
+ */
8
+
9
+ import crypto from 'crypto';
10
+ import http from 'http';
11
+ import {
12
+ ANTIGRAVITY_ENDPOINT_FALLBACKS,
13
+ LOAD_CODE_ASSIST_HEADERS,
14
+ OAUTH_CONFIG,
15
+ OAUTH_REDIRECT_URI
16
+ } from '../constants.js';
17
+ import { logger } from '../utils/logger.js';
18
+ import { onboardUser, getDefaultTierId } from '../account-manager/onboarding.js';
19
+
20
+ /**
21
+ * Parse refresh token parts (aligned with opencode-cloudcode-auth)
22
+ * Format: refreshToken|projectId|managedProjectId
23
+ *
24
+ * @param {string} refresh - Composite refresh token string
25
+ * @returns {{refreshToken: string, projectId: string|undefined, managedProjectId: string|undefined}}
26
+ */
27
+ export function parseRefreshParts(refresh) {
28
+ const [refreshToken = '', projectId = '', managedProjectId = ''] = (refresh ?? '').split('|');
29
+ return {
30
+ refreshToken,
31
+ projectId: projectId || undefined,
32
+ managedProjectId: managedProjectId || undefined,
33
+ };
34
+ }
35
+
36
+ /**
37
+ * Format refresh token parts back into composite string
38
+ *
39
+ * @param {{refreshToken: string, projectId?: string|undefined, managedProjectId?: string|undefined}} parts
40
+ * @returns {string} Composite refresh token
41
+ */
42
+ export function formatRefreshParts(parts) {
43
+ const projectSegment = parts.projectId ?? '';
44
+ const base = `${parts.refreshToken}|${projectSegment}`;
45
+ return parts.managedProjectId ? `${base}|${parts.managedProjectId}` : base;
46
+ }
47
+
48
+ /**
49
+ * Generate PKCE code verifier and challenge
50
+ */
51
+ function generatePKCE() {
52
+ const verifier = crypto.randomBytes(32).toString('base64url');
53
+ const challenge = crypto
54
+ .createHash('sha256')
55
+ .update(verifier)
56
+ .digest('base64url');
57
+ return { verifier, challenge };
58
+ }
59
+
60
+ /**
61
+ * Generate authorization URL for Google OAuth
62
+ * Returns the URL and the PKCE verifier (needed for token exchange)
63
+ *
64
+ * @param {string} [customRedirectUri] - Optional custom redirect URI (e.g. for WebUI)
65
+ * @returns {{url: string, verifier: string, state: string}} Auth URL and PKCE data
66
+ */
67
+ export function getAuthorizationUrl(customRedirectUri = null) {
68
+ const { verifier, challenge } = generatePKCE();
69
+ const state = crypto.randomBytes(16).toString('hex');
70
+
71
+ const params = new URLSearchParams({
72
+ client_id: OAUTH_CONFIG.clientId,
73
+ redirect_uri: customRedirectUri || OAUTH_REDIRECT_URI,
74
+ response_type: 'code',
75
+ scope: OAUTH_CONFIG.scopes.join(' '),
76
+ access_type: 'offline',
77
+ prompt: 'consent',
78
+ code_challenge: challenge,
79
+ code_challenge_method: 'S256',
80
+ state: state
81
+ });
82
+
83
+ return {
84
+ url: `${OAUTH_CONFIG.authUrl}?${params.toString()}`,
85
+ verifier,
86
+ state
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Extract authorization code and state from user input.
92
+ * User can paste either:
93
+ * - Full callback URL: http://localhost:51121/oauth-callback?code=xxx&state=xxx
94
+ * - Just the code parameter: 4/0xxx...
95
+ *
96
+ * @param {string} input - User input (URL or code)
97
+ * @returns {{code: string, state: string|null}} Extracted code and optional state
98
+ */
99
+ export function extractCodeFromInput(input) {
100
+ if (!input || typeof input !== 'string') {
101
+ throw new Error('No input provided');
102
+ }
103
+
104
+ const trimmed = input.trim();
105
+
106
+ // Check if it looks like a URL
107
+ if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
108
+ try {
109
+ const url = new URL(trimmed);
110
+ const code = url.searchParams.get('code');
111
+ const state = url.searchParams.get('state');
112
+ const error = url.searchParams.get('error');
113
+
114
+ if (error) {
115
+ throw new Error(`OAuth error: ${error}`);
116
+ }
117
+
118
+ if (!code) {
119
+ throw new Error('No authorization code found in URL');
120
+ }
121
+
122
+ return { code, state };
123
+ } catch (e) {
124
+ if (e.message.includes('OAuth error') || e.message.includes('No authorization code')) {
125
+ throw e;
126
+ }
127
+ throw new Error('Invalid URL format');
128
+ }
129
+ }
130
+
131
+ // Assume it's a raw code
132
+ // Google auth codes typically start with "4/" and are long
133
+ if (trimmed.length < 10) {
134
+ throw new Error('Input is too short to be a valid authorization code');
135
+ }
136
+
137
+ return { code: trimmed, state: null };
138
+ }
139
+
140
+ /**
141
+ * Attempt to bind server to a specific port
142
+ * @param {http.Server} server - HTTP server instance
143
+ * @param {number} port - Port to bind to
144
+ * @returns {Promise<number>} Resolves with port on success, rejects on error
145
+ */
146
+ function tryBindPort(server, port) {
147
+ return new Promise((resolve, reject) => {
148
+ const onError = (err) => {
149
+ server.removeListener('listening', onSuccess);
150
+ reject(err);
151
+ };
152
+ const onSuccess = () => {
153
+ server.removeListener('error', onError);
154
+ resolve(port);
155
+ };
156
+ server.once('error', onError);
157
+ server.once('listening', onSuccess);
158
+ server.listen(port);
159
+ });
160
+ }
161
+
162
+ /**
163
+ * Start a local server to receive the OAuth callback
164
+ * Implements automatic port fallback for Windows compatibility (issue #176)
165
+ * Returns an object with a promise and an abort function
166
+ *
167
+ * @param {string} expectedState - Expected state parameter for CSRF protection
168
+ * @param {number} timeoutMs - Timeout in milliseconds (default 120000)
169
+ * @returns {{promise: Promise<string>, abort: Function, getPort: Function}} Object with promise, abort, and getPort functions
170
+ */
171
+ export function startCallbackServer(expectedState, timeoutMs = 120000) {
172
+ let server = null;
173
+ let timeoutId = null;
174
+ let isAborted = false;
175
+ let actualPort = OAUTH_CONFIG.callbackPort;
176
+
177
+ const promise = new Promise(async (resolve, reject) => {
178
+ // Build list of ports to try: primary + fallbacks
179
+ const portsToTry = [OAUTH_CONFIG.callbackPort, ...(OAUTH_CONFIG.callbackFallbackPorts || [])];
180
+ const errors = [];
181
+
182
+ server = http.createServer((req, res) => {
183
+ const url = new URL(req.url, `http://localhost:${actualPort}`);
184
+
185
+ if (url.pathname !== '/oauth-callback') {
186
+ res.writeHead(404);
187
+ res.end('Not found');
188
+ return;
189
+ }
190
+
191
+ const code = url.searchParams.get('code');
192
+ const state = url.searchParams.get('state');
193
+ const error = url.searchParams.get('error');
194
+
195
+ if (error) {
196
+ res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
197
+ res.end(`
198
+ <html>
199
+ <head><meta charset="UTF-8"><title>Authentication Failed</title></head>
200
+ <body style="font-family: system-ui; padding: 40px; text-align: center;">
201
+ <h1 style="color: #dc3545;">❌ Authentication Failed</h1>
202
+ <p>Error: ${error}</p>
203
+ <p>You can close this window.</p>
204
+ </body>
205
+ </html>
206
+ `);
207
+ server.close();
208
+ reject(new Error(`OAuth error: ${error}`));
209
+ return;
210
+ }
211
+
212
+ if (state !== expectedState) {
213
+ res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
214
+ res.end(`
215
+ <html>
216
+ <head><meta charset="UTF-8"><title>Authentication Failed</title></head>
217
+ <body style="font-family: system-ui; padding: 40px; text-align: center;">
218
+ <h1 style="color: #dc3545;">❌ Authentication Failed</h1>
219
+ <p>State mismatch - possible CSRF attack.</p>
220
+ <p>You can close this window.</p>
221
+ </body>
222
+ </html>
223
+ `);
224
+ server.close();
225
+ reject(new Error('State mismatch'));
226
+ return;
227
+ }
228
+
229
+ if (!code) {
230
+ res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
231
+ res.end(`
232
+ <html>
233
+ <head><meta charset="UTF-8"><title>Authentication Failed</title></head>
234
+ <body style="font-family: system-ui; padding: 40px; text-align: center;">
235
+ <h1 style="color: #dc3545;">❌ Authentication Failed</h1>
236
+ <p>No authorization code received.</p>
237
+ <p>You can close this window.</p>
238
+ </body>
239
+ </html>
240
+ `);
241
+ server.close();
242
+ reject(new Error('No authorization code'));
243
+ return;
244
+ }
245
+
246
+ // Success!
247
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
248
+ res.end(`
249
+ <html>
250
+ <head><meta charset="UTF-8"><title>Authentication Successful</title></head>
251
+ <body style="font-family: system-ui; padding: 40px; text-align: center;">
252
+ <h1 style="color: #28a745;">✅ Authentication Successful!</h1>
253
+ <p>You can close this window and return to the terminal.</p>
254
+ <script>setTimeout(() => window.close(), 2000);</script>
255
+ </body>
256
+ </html>
257
+ `);
258
+
259
+ server.close();
260
+ resolve(code);
261
+ });
262
+
263
+ // Try ports with fallback logic (issue #176 - Windows EACCES fix)
264
+ let boundSuccessfully = false;
265
+ for (const port of portsToTry) {
266
+ try {
267
+ await tryBindPort(server, port);
268
+ actualPort = port;
269
+ boundSuccessfully = true;
270
+
271
+ if (port !== OAUTH_CONFIG.callbackPort) {
272
+ logger.warn(`[OAuth] Primary port ${OAUTH_CONFIG.callbackPort} unavailable, using fallback port ${port}`);
273
+ } else {
274
+ logger.info(`[OAuth] Callback server listening on port ${port}`);
275
+ }
276
+ break;
277
+ } catch (err) {
278
+ const errMsg = err.code === 'EACCES'
279
+ ? `Permission denied on port ${port}`
280
+ : err.code === 'EADDRINUSE'
281
+ ? `Port ${port} already in use`
282
+ : `Failed to bind port ${port}: ${err.message}`;
283
+ errors.push(errMsg);
284
+ logger.warn(`[OAuth] ${errMsg}`);
285
+ }
286
+ }
287
+
288
+ if (!boundSuccessfully) {
289
+ // All ports failed - provide helpful error message
290
+ const isWindows = process.platform === 'win32';
291
+ let errorMsg = `Failed to start OAuth callback server.\nTried ports: ${portsToTry.join(', ')}\n\nErrors:\n${errors.join('\n')}`;
292
+
293
+ if (isWindows) {
294
+ errorMsg += `\n
295
+ ================== WINDOWS TROUBLESHOOTING ==================
296
+ The default port range may be reserved by Hyper-V/WSL2/Docker.
297
+
298
+ Option 1: Use a custom port
299
+ Set OAUTH_CALLBACK_PORT=3456 in your environment or .env file
300
+
301
+ Option 2: Reset Windows NAT (run as Administrator)
302
+ net stop winnat && net start winnat
303
+
304
+ Option 3: Check reserved port ranges
305
+ netsh interface ipv4 show excludedportrange protocol=tcp
306
+
307
+ Option 4: Exclude port from reservation (run as Administrator)
308
+ netsh int ipv4 add excludedportrange protocol=tcp startport=51121 numberofports=1
309
+ ==============================================================`;
310
+ } else {
311
+ errorMsg += `\n\nTry setting a custom port: OAUTH_CALLBACK_PORT=3456`;
312
+ }
313
+
314
+ reject(new Error(errorMsg));
315
+ return;
316
+ }
317
+
318
+ // Timeout after specified duration
319
+ timeoutId = setTimeout(() => {
320
+ if (!isAborted) {
321
+ server.close();
322
+ reject(new Error('OAuth callback timeout - no response received'));
323
+ }
324
+ }, timeoutMs);
325
+ });
326
+
327
+ // Abort function to clean up server when manual completion happens
328
+ const abort = () => {
329
+ if (isAborted) return;
330
+ isAborted = true;
331
+ if (timeoutId) {
332
+ clearTimeout(timeoutId);
333
+ }
334
+ if (server) {
335
+ server.close();
336
+ logger.info('[OAuth] Callback server aborted (manual completion)');
337
+ }
338
+ };
339
+
340
+ // Get actual port (useful when fallback is used)
341
+ const getPort = () => actualPort;
342
+
343
+ return { promise, abort, getPort };
344
+ }
345
+
346
+ /**
347
+ * Exchange authorization code for tokens
348
+ *
349
+ * @param {string} code - Authorization code from OAuth callback
350
+ * @param {string} verifier - PKCE code verifier
351
+ * @returns {Promise<{accessToken: string, refreshToken: string, expiresIn: number}>} OAuth tokens
352
+ */
353
+ export async function exchangeCode(code, verifier) {
354
+ const response = await fetch(OAUTH_CONFIG.tokenUrl, {
355
+ method: 'POST',
356
+ headers: {
357
+ 'Content-Type': 'application/x-www-form-urlencoded'
358
+ },
359
+ body: new URLSearchParams({
360
+ client_id: OAUTH_CONFIG.clientId,
361
+ client_secret: OAUTH_CONFIG.clientSecret,
362
+ code: code,
363
+ code_verifier: verifier,
364
+ grant_type: 'authorization_code',
365
+ redirect_uri: OAUTH_REDIRECT_URI
366
+ })
367
+ });
368
+
369
+ if (!response.ok) {
370
+ const error = await response.text();
371
+ logger.error(`[OAuth] Token exchange failed: ${response.status} ${error}`);
372
+ throw new Error(`Token exchange failed: ${error}`);
373
+ }
374
+
375
+ const tokens = await response.json();
376
+
377
+ if (!tokens.access_token) {
378
+ logger.error('[OAuth] No access token in response:', tokens);
379
+ throw new Error('No access token received');
380
+ }
381
+
382
+ logger.info(`[OAuth] Token exchange successful, access_token length: ${tokens.access_token?.length}`);
383
+
384
+ return {
385
+ accessToken: tokens.access_token,
386
+ refreshToken: tokens.refresh_token,
387
+ expiresIn: tokens.expires_in
388
+ };
389
+ }
390
+
391
+ /**
392
+ * Refresh access token using refresh token
393
+ * Handles composite refresh tokens (refreshToken|projectId|managedProjectId)
394
+ *
395
+ * @param {string} compositeRefresh - OAuth refresh token (may be composite)
396
+ * @returns {Promise<{accessToken: string, expiresIn: number}>} New access token
397
+ */
398
+ export async function refreshAccessToken(compositeRefresh) {
399
+ // Parse the composite refresh token to extract the actual OAuth token
400
+ const parts = parseRefreshParts(compositeRefresh);
401
+
402
+ const response = await fetch(OAUTH_CONFIG.tokenUrl, {
403
+ method: 'POST',
404
+ headers: {
405
+ 'Content-Type': 'application/x-www-form-urlencoded'
406
+ },
407
+ body: new URLSearchParams({
408
+ client_id: OAUTH_CONFIG.clientId,
409
+ client_secret: OAUTH_CONFIG.clientSecret,
410
+ refresh_token: parts.refreshToken, // Use the actual OAuth token
411
+ grant_type: 'refresh_token'
412
+ })
413
+ });
414
+
415
+ if (!response.ok) {
416
+ const error = await response.text();
417
+ throw new Error(`Token refresh failed: ${error}`);
418
+ }
419
+
420
+ const tokens = await response.json();
421
+ return {
422
+ accessToken: tokens.access_token,
423
+ expiresIn: tokens.expires_in
424
+ };
425
+ }
426
+
427
+ /**
428
+ * Get user email from access token
429
+ *
430
+ * @param {string} accessToken - OAuth access token
431
+ * @returns {Promise<string>} User's email address
432
+ */
433
+ export async function getUserEmail(accessToken) {
434
+ const response = await fetch(OAUTH_CONFIG.userInfoUrl, {
435
+ headers: {
436
+ 'Authorization': `Bearer ${accessToken}`
437
+ }
438
+ });
439
+
440
+ if (!response.ok) {
441
+ const errorText = await response.text();
442
+ logger.error(`[OAuth] getUserEmail failed: ${response.status} ${errorText}`);
443
+ throw new Error(`Failed to get user info: ${response.status}`);
444
+ }
445
+
446
+ const userInfo = await response.json();
447
+ return userInfo.email;
448
+ }
449
+
450
+ /**
451
+ * Discover project ID for the authenticated user
452
+ *
453
+ * @param {string} accessToken - OAuth access token
454
+ * @returns {Promise<string|null>} Project ID or null if not found
455
+ */
456
+ export async function discoverProjectId(accessToken) {
457
+ let loadCodeAssistData = null;
458
+
459
+ for (const endpoint of ANTIGRAVITY_ENDPOINT_FALLBACKS) {
460
+ try {
461
+ const response = await fetch(`${endpoint}/v1internal:loadCodeAssist`, {
462
+ method: 'POST',
463
+ headers: {
464
+ 'Authorization': `Bearer ${accessToken}`,
465
+ 'Content-Type': 'application/json',
466
+ ...LOAD_CODE_ASSIST_HEADERS
467
+ },
468
+ body: JSON.stringify({
469
+ metadata: {
470
+ ideType: 'IDE_UNSPECIFIED',
471
+ platform: 'PLATFORM_UNSPECIFIED',
472
+ pluginType: 'GEMINI'
473
+ }
474
+ })
475
+ });
476
+
477
+ if (!response.ok) continue;
478
+
479
+ const data = await response.json();
480
+ loadCodeAssistData = data;
481
+
482
+ if (typeof data.cloudaicompanionProject === 'string') {
483
+ return data.cloudaicompanionProject;
484
+ }
485
+ if (data.cloudaicompanionProject?.id) {
486
+ return data.cloudaicompanionProject.id;
487
+ }
488
+
489
+ // No project found - try to onboard
490
+ logger.info('[OAuth] No project in loadCodeAssist response, attempting onboardUser...');
491
+ break;
492
+ } catch (error) {
493
+ logger.warn(`[OAuth] Project discovery failed at ${endpoint}:`, error.message);
494
+ }
495
+ }
496
+
497
+ // Try onboarding if we got a response but no project
498
+ if (loadCodeAssistData) {
499
+ const tierId = getDefaultTierId(loadCodeAssistData.allowedTiers) || 'FREE';
500
+ logger.info(`[OAuth] Onboarding user with tier: ${tierId}`);
501
+
502
+ const onboardedProject = await onboardUser(accessToken, tierId);
503
+ if (onboardedProject) {
504
+ logger.success(`[OAuth] Successfully onboarded, project: ${onboardedProject}`);
505
+ return onboardedProject;
506
+ }
507
+ }
508
+
509
+ return null;
510
+ }
511
+
512
+ /**
513
+ * Complete OAuth flow: exchange code and get all account info
514
+ *
515
+ * @param {string} code - Authorization code from OAuth callback
516
+ * @param {string} verifier - PKCE code verifier
517
+ * @returns {Promise<{email: string, refreshToken: string, accessToken: string, projectId: string|null}>} Complete account info
518
+ */
519
+ export async function completeOAuthFlow(code, verifier) {
520
+ // Exchange code for tokens
521
+ const tokens = await exchangeCode(code, verifier);
522
+
523
+ // Get user email
524
+ const email = await getUserEmail(tokens.accessToken);
525
+
526
+ // Discover project ID
527
+ const projectId = await discoverProjectId(tokens.accessToken);
528
+
529
+ return {
530
+ email,
531
+ refreshToken: tokens.refreshToken,
532
+ accessToken: tokens.accessToken,
533
+ projectId
534
+ };
535
+ }
536
+
537
+ export default {
538
+ parseRefreshParts,
539
+ formatRefreshParts,
540
+ getAuthorizationUrl,
541
+ extractCodeFromInput,
542
+ startCallbackServer,
543
+ exchangeCode,
544
+ refreshAccessToken,
545
+ getUserEmail,
546
+ discoverProjectId,
547
+ completeOAuthFlow
548
+ };
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Token Extractor Module
3
+ * Extracts OAuth tokens from Cloud Code IDE's SQLite database
4
+ *
5
+ * The database is automatically updated by the IDE when tokens refresh,
6
+ * so this approach doesn't require any manual intervention.
7
+ */
8
+
9
+ import {
10
+ TOKEN_REFRESH_INTERVAL_MS,
11
+ CLOUDCODE_AUTH_PORT
12
+ } from '../constants.js';
13
+ import { getAuthStatus } from './database.js';
14
+ import { logger } from '../utils/logger.js';
15
+
16
+ // Cache for the extracted token
17
+ let cachedToken = null;
18
+ let tokenExtractedAt = null;
19
+
20
+ /**
21
+ * Extract the chat params from Cloud Code IDE's HTML page (fallback method)
22
+ */
23
+ async function extractChatParams() {
24
+ try {
25
+ const response = await fetch(`http://127.0.0.1:${CLOUDCODE_AUTH_PORT}/`);
26
+ const html = await response.text();
27
+
28
+ // Find the base64-encoded chatParams in the HTML
29
+ const match = html.match(/window\.chatParams\s*=\s*'([^']+)'/);
30
+ if (!match) {
31
+ throw new Error('Could not find chatParams in Cloud Code IDE page');
32
+ }
33
+
34
+ // Decode base64
35
+ const base64Data = match[1];
36
+ const jsonString = Buffer.from(base64Data, 'base64').toString('utf-8');
37
+ const config = JSON.parse(jsonString);
38
+
39
+ return config;
40
+ } catch (error) {
41
+ if (error.code === 'ECONNREFUSED') {
42
+ throw new Error(
43
+ `Cannot connect to Cloud Code IDE on port ${CLOUDCODE_AUTH_PORT}. ` +
44
+ 'Make sure the IDE is running.'
45
+ );
46
+ }
47
+ throw error;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Get fresh token data - tries DB first, falls back to HTML page
53
+ */
54
+ async function getTokenData() {
55
+ // Try database first (preferred - always has fresh token)
56
+ try {
57
+ const dbData = getAuthStatus();
58
+ if (dbData?.apiKey) {
59
+ logger.info('[Token] Got fresh token from SQLite database');
60
+ return dbData;
61
+ }
62
+ } catch (err) {
63
+ logger.warn('[Token] DB extraction failed, trying HTML page...');
64
+ }
65
+
66
+ // Fallback to HTML page
67
+ try {
68
+ const pageData = await extractChatParams();
69
+ if (pageData?.apiKey) {
70
+ logger.warn('[Token] Got token from HTML page (may be stale)');
71
+ return pageData;
72
+ }
73
+ } catch (err) {
74
+ logger.warn(`[Token] HTML page extraction failed: ${err.message}`);
75
+ }
76
+
77
+ throw new Error(
78
+ 'Could not extract token from Cloud Code IDE. ' +
79
+ 'Make sure the IDE is running and you are logged in.'
80
+ );
81
+ }
82
+
83
+ /**
84
+ * Check if the cached token needs refresh
85
+ */
86
+ function needsRefresh() {
87
+ if (!cachedToken || !tokenExtractedAt) {
88
+ return true;
89
+ }
90
+ return Date.now() - tokenExtractedAt > TOKEN_REFRESH_INTERVAL_MS;
91
+ }
92
+
93
+ /**
94
+ * Get the current OAuth token (with caching)
95
+ */
96
+ export async function getToken() {
97
+ if (needsRefresh()) {
98
+ const data = await getTokenData();
99
+ cachedToken = data.apiKey;
100
+ tokenExtractedAt = Date.now();
101
+ }
102
+ return cachedToken;
103
+ }
104
+
105
+ /**
106
+ * Force refresh the token (useful if requests start failing)
107
+ */
108
+ export async function forceRefresh() {
109
+ cachedToken = null;
110
+ tokenExtractedAt = null;
111
+ return getToken();
112
+ }
113
+
114
+ export default {
115
+ getToken,
116
+ forceRefresh
117
+ };