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.
- package/LICENSE +21 -0
- package/README.md +757 -0
- package/bin/cli.js +146 -0
- package/package.json +97 -0
- package/public/Complaint Details.pdf +0 -0
- package/public/Cyber Crime Portal.pdf +0 -0
- package/public/app.js +229 -0
- package/public/css/src/input.css +523 -0
- package/public/css/style.css +1 -0
- package/public/favicon.png +0 -0
- package/public/index.html +549 -0
- package/public/js/components/account-manager.js +356 -0
- package/public/js/components/add-account-modal.js +414 -0
- package/public/js/components/claude-config.js +420 -0
- package/public/js/components/dashboard/charts.js +605 -0
- package/public/js/components/dashboard/filters.js +362 -0
- package/public/js/components/dashboard/stats.js +110 -0
- package/public/js/components/dashboard.js +236 -0
- package/public/js/components/logs-viewer.js +100 -0
- package/public/js/components/models.js +36 -0
- package/public/js/components/server-config.js +349 -0
- package/public/js/config/constants.js +102 -0
- package/public/js/data-store.js +375 -0
- package/public/js/settings-store.js +58 -0
- package/public/js/store.js +99 -0
- package/public/js/translations/en.js +367 -0
- package/public/js/translations/id.js +412 -0
- package/public/js/translations/pt.js +308 -0
- package/public/js/translations/tr.js +358 -0
- package/public/js/translations/zh.js +373 -0
- package/public/js/utils/account-actions.js +189 -0
- package/public/js/utils/error-handler.js +96 -0
- package/public/js/utils/model-config.js +42 -0
- package/public/js/utils/ui-logger.js +143 -0
- package/public/js/utils/validators.js +77 -0
- package/public/js/utils.js +69 -0
- package/public/proxy-server-64.png +0 -0
- package/public/views/accounts.html +361 -0
- package/public/views/dashboard.html +484 -0
- package/public/views/logs.html +97 -0
- package/public/views/models.html +331 -0
- package/public/views/settings.html +1327 -0
- package/src/account-manager/credentials.js +378 -0
- package/src/account-manager/index.js +462 -0
- package/src/account-manager/onboarding.js +112 -0
- package/src/account-manager/rate-limits.js +369 -0
- package/src/account-manager/storage.js +160 -0
- package/src/account-manager/strategies/base-strategy.js +109 -0
- package/src/account-manager/strategies/hybrid-strategy.js +339 -0
- package/src/account-manager/strategies/index.js +79 -0
- package/src/account-manager/strategies/round-robin-strategy.js +76 -0
- package/src/account-manager/strategies/sticky-strategy.js +138 -0
- package/src/account-manager/strategies/trackers/health-tracker.js +162 -0
- package/src/account-manager/strategies/trackers/index.js +9 -0
- package/src/account-manager/strategies/trackers/quota-tracker.js +120 -0
- package/src/account-manager/strategies/trackers/token-bucket-tracker.js +155 -0
- package/src/auth/database.js +169 -0
- package/src/auth/oauth.js +548 -0
- package/src/auth/token-extractor.js +117 -0
- package/src/cli/accounts.js +648 -0
- package/src/cloudcode/index.js +29 -0
- package/src/cloudcode/message-handler.js +510 -0
- package/src/cloudcode/model-api.js +248 -0
- package/src/cloudcode/rate-limit-parser.js +235 -0
- package/src/cloudcode/request-builder.js +93 -0
- package/src/cloudcode/session-manager.js +47 -0
- package/src/cloudcode/sse-parser.js +121 -0
- package/src/cloudcode/sse-streamer.js +293 -0
- package/src/cloudcode/streaming-handler.js +615 -0
- package/src/config.js +125 -0
- package/src/constants.js +407 -0
- package/src/errors.js +242 -0
- package/src/fallback-config.js +29 -0
- package/src/format/content-converter.js +193 -0
- package/src/format/index.js +20 -0
- package/src/format/request-converter.js +255 -0
- package/src/format/response-converter.js +120 -0
- package/src/format/schema-sanitizer.js +673 -0
- package/src/format/signature-cache.js +88 -0
- package/src/format/thinking-utils.js +648 -0
- package/src/index.js +148 -0
- package/src/modules/usage-stats.js +205 -0
- package/src/providers/anthropic-provider.js +258 -0
- package/src/providers/base-provider.js +157 -0
- package/src/providers/cloudcode.js +94 -0
- package/src/providers/copilot.js +399 -0
- package/src/providers/github-provider.js +287 -0
- package/src/providers/google-provider.js +192 -0
- package/src/providers/index.js +211 -0
- package/src/providers/openai-compatible.js +265 -0
- package/src/providers/openai-provider.js +271 -0
- package/src/providers/openrouter-provider.js +325 -0
- package/src/providers/setup.js +83 -0
- package/src/server.js +870 -0
- package/src/utils/claude-config.js +245 -0
- package/src/utils/helpers.js +51 -0
- package/src/utils/logger.js +142 -0
- package/src/utils/native-module-helper.js +162 -0
- 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
|
+
};
|