antigravity-claude-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.
- package/LICENSE +21 -0
- package/README.md +289 -0
- package/bin/cli.js +109 -0
- package/package.json +54 -0
- package/src/account-manager.js +633 -0
- package/src/accounts-cli.js +437 -0
- package/src/cloudcode-client.js +1018 -0
- package/src/constants.js +164 -0
- package/src/errors.js +159 -0
- package/src/format-converter.js +731 -0
- package/src/index.js +40 -0
- package/src/oauth.js +346 -0
- package/src/server.js +517 -0
- package/src/token-extractor.js +146 -0
- package/src/utils/helpers.js +33 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Antigravity Claude Proxy
|
|
3
|
+
* Entry point - starts the proxy server
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import app from './server.js';
|
|
7
|
+
import { DEFAULT_PORT } from './constants.js';
|
|
8
|
+
|
|
9
|
+
const PORT = process.env.PORT || DEFAULT_PORT;
|
|
10
|
+
|
|
11
|
+
app.listen(PORT, () => {
|
|
12
|
+
console.log(`
|
|
13
|
+
╔══════════════════════════════════════════════════════════════╗
|
|
14
|
+
║ Antigravity Claude Proxy Server ║
|
|
15
|
+
╠══════════════════════════════════════════════════════════════╣
|
|
16
|
+
║ ║
|
|
17
|
+
║ Server running at: http://localhost:${PORT} ║
|
|
18
|
+
║ ║
|
|
19
|
+
║ Endpoints: ║
|
|
20
|
+
║ POST /v1/messages - Anthropic Messages API ║
|
|
21
|
+
║ GET /v1/models - List available models ║
|
|
22
|
+
║ GET /health - Health check ║
|
|
23
|
+
║ GET /account-limits - Account status & quotas ║
|
|
24
|
+
║ POST /refresh-token - Force token refresh ║
|
|
25
|
+
║ ║
|
|
26
|
+
║ Usage with Claude Code: ║
|
|
27
|
+
║ export ANTHROPIC_BASE_URL=http://localhost:${PORT} ║
|
|
28
|
+
║ export ANTHROPIC_API_KEY=dummy ║
|
|
29
|
+
║ claude ║
|
|
30
|
+
║ ║
|
|
31
|
+
║ Add Google accounts: ║
|
|
32
|
+
║ npm run accounts ║
|
|
33
|
+
║ ║
|
|
34
|
+
║ Prerequisites (if no accounts configured): ║
|
|
35
|
+
║ - Antigravity must be running ║
|
|
36
|
+
║ - Have a chat panel open in Antigravity ║
|
|
37
|
+
║ ║
|
|
38
|
+
╚══════════════════════════════════════════════════════════════╝
|
|
39
|
+
`);
|
|
40
|
+
});
|
package/src/oauth.js
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google OAuth with PKCE for Antigravity
|
|
3
|
+
*
|
|
4
|
+
* Implements the same OAuth flow as opencode-antigravity-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
|
+
ANTIGRAVITY_HEADERS,
|
|
14
|
+
OAUTH_CONFIG,
|
|
15
|
+
OAUTH_REDIRECT_URI
|
|
16
|
+
} from './constants.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Generate PKCE code verifier and challenge
|
|
20
|
+
*/
|
|
21
|
+
function generatePKCE() {
|
|
22
|
+
const verifier = crypto.randomBytes(32).toString('base64url');
|
|
23
|
+
const challenge = crypto
|
|
24
|
+
.createHash('sha256')
|
|
25
|
+
.update(verifier)
|
|
26
|
+
.digest('base64url');
|
|
27
|
+
return { verifier, challenge };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Generate authorization URL for Google OAuth
|
|
32
|
+
* Returns the URL and the PKCE verifier (needed for token exchange)
|
|
33
|
+
*
|
|
34
|
+
* @returns {{url: string, verifier: string, state: string}} Auth URL and PKCE data
|
|
35
|
+
*/
|
|
36
|
+
export function getAuthorizationUrl() {
|
|
37
|
+
const { verifier, challenge } = generatePKCE();
|
|
38
|
+
const state = crypto.randomBytes(16).toString('hex');
|
|
39
|
+
|
|
40
|
+
const params = new URLSearchParams({
|
|
41
|
+
client_id: OAUTH_CONFIG.clientId,
|
|
42
|
+
redirect_uri: OAUTH_REDIRECT_URI,
|
|
43
|
+
response_type: 'code',
|
|
44
|
+
scope: OAUTH_CONFIG.scopes.join(' '),
|
|
45
|
+
access_type: 'offline',
|
|
46
|
+
prompt: 'consent',
|
|
47
|
+
code_challenge: challenge,
|
|
48
|
+
code_challenge_method: 'S256',
|
|
49
|
+
state: state
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
url: `${OAUTH_CONFIG.authUrl}?${params.toString()}`,
|
|
54
|
+
verifier,
|
|
55
|
+
state
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Start a local server to receive the OAuth callback
|
|
61
|
+
* Returns a promise that resolves with the authorization code
|
|
62
|
+
*
|
|
63
|
+
* @param {string} expectedState - Expected state parameter for CSRF protection
|
|
64
|
+
* @param {number} timeoutMs - Timeout in milliseconds (default 120000)
|
|
65
|
+
* @returns {Promise<string>} Authorization code from OAuth callback
|
|
66
|
+
*/
|
|
67
|
+
export function startCallbackServer(expectedState, timeoutMs = 120000) {
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
const server = http.createServer((req, res) => {
|
|
70
|
+
const url = new URL(req.url, `http://localhost:${OAUTH_CONFIG.callbackPort}`);
|
|
71
|
+
|
|
72
|
+
if (url.pathname !== '/oauth-callback') {
|
|
73
|
+
res.writeHead(404);
|
|
74
|
+
res.end('Not found');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const code = url.searchParams.get('code');
|
|
79
|
+
const state = url.searchParams.get('state');
|
|
80
|
+
const error = url.searchParams.get('error');
|
|
81
|
+
|
|
82
|
+
if (error) {
|
|
83
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
84
|
+
res.end(`
|
|
85
|
+
<html>
|
|
86
|
+
<head><title>Authentication Failed</title></head>
|
|
87
|
+
<body style="font-family: system-ui; padding: 40px; text-align: center;">
|
|
88
|
+
<h1 style="color: #dc3545;">❌ Authentication Failed</h1>
|
|
89
|
+
<p>Error: ${error}</p>
|
|
90
|
+
<p>You can close this window.</p>
|
|
91
|
+
</body>
|
|
92
|
+
</html>
|
|
93
|
+
`);
|
|
94
|
+
server.close();
|
|
95
|
+
reject(new Error(`OAuth error: ${error}`));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (state !== expectedState) {
|
|
100
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
101
|
+
res.end(`
|
|
102
|
+
<html>
|
|
103
|
+
<head><title>Authentication Failed</title></head>
|
|
104
|
+
<body style="font-family: system-ui; padding: 40px; text-align: center;">
|
|
105
|
+
<h1 style="color: #dc3545;">❌ Authentication Failed</h1>
|
|
106
|
+
<p>State mismatch - possible CSRF attack.</p>
|
|
107
|
+
<p>You can close this window.</p>
|
|
108
|
+
</body>
|
|
109
|
+
</html>
|
|
110
|
+
`);
|
|
111
|
+
server.close();
|
|
112
|
+
reject(new Error('State mismatch'));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!code) {
|
|
117
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
118
|
+
res.end(`
|
|
119
|
+
<html>
|
|
120
|
+
<head><title>Authentication Failed</title></head>
|
|
121
|
+
<body style="font-family: system-ui; padding: 40px; text-align: center;">
|
|
122
|
+
<h1 style="color: #dc3545;">❌ Authentication Failed</h1>
|
|
123
|
+
<p>No authorization code received.</p>
|
|
124
|
+
<p>You can close this window.</p>
|
|
125
|
+
</body>
|
|
126
|
+
</html>
|
|
127
|
+
`);
|
|
128
|
+
server.close();
|
|
129
|
+
reject(new Error('No authorization code'));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Success!
|
|
134
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
135
|
+
res.end(`
|
|
136
|
+
<html>
|
|
137
|
+
<head><title>Authentication Successful</title></head>
|
|
138
|
+
<body style="font-family: system-ui; padding: 40px; text-align: center;">
|
|
139
|
+
<h1 style="color: #28a745;">✅ Authentication Successful!</h1>
|
|
140
|
+
<p>You can close this window and return to the terminal.</p>
|
|
141
|
+
<script>setTimeout(() => window.close(), 2000);</script>
|
|
142
|
+
</body>
|
|
143
|
+
</html>
|
|
144
|
+
`);
|
|
145
|
+
|
|
146
|
+
server.close();
|
|
147
|
+
resolve(code);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
server.on('error', (err) => {
|
|
151
|
+
if (err.code === 'EADDRINUSE') {
|
|
152
|
+
reject(new Error(`Port ${OAUTH_CONFIG.callbackPort} is already in use. Close any other OAuth flows and try again.`));
|
|
153
|
+
} else {
|
|
154
|
+
reject(err);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
server.listen(OAUTH_CONFIG.callbackPort, () => {
|
|
159
|
+
console.log(`[OAuth] Callback server listening on port ${OAUTH_CONFIG.callbackPort}`);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Timeout after specified duration
|
|
163
|
+
setTimeout(() => {
|
|
164
|
+
server.close();
|
|
165
|
+
reject(new Error('OAuth callback timeout - no response received'));
|
|
166
|
+
}, timeoutMs);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Exchange authorization code for tokens
|
|
172
|
+
*
|
|
173
|
+
* @param {string} code - Authorization code from OAuth callback
|
|
174
|
+
* @param {string} verifier - PKCE code verifier
|
|
175
|
+
* @returns {Promise<{accessToken: string, refreshToken: string, expiresIn: number}>} OAuth tokens
|
|
176
|
+
*/
|
|
177
|
+
export async function exchangeCode(code, verifier) {
|
|
178
|
+
const response = await fetch(OAUTH_CONFIG.tokenUrl, {
|
|
179
|
+
method: 'POST',
|
|
180
|
+
headers: {
|
|
181
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
182
|
+
},
|
|
183
|
+
body: new URLSearchParams({
|
|
184
|
+
client_id: OAUTH_CONFIG.clientId,
|
|
185
|
+
client_secret: OAUTH_CONFIG.clientSecret,
|
|
186
|
+
code: code,
|
|
187
|
+
code_verifier: verifier,
|
|
188
|
+
grant_type: 'authorization_code',
|
|
189
|
+
redirect_uri: OAUTH_REDIRECT_URI
|
|
190
|
+
})
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
if (!response.ok) {
|
|
194
|
+
const error = await response.text();
|
|
195
|
+
console.error('[OAuth] Token exchange failed:', response.status, error);
|
|
196
|
+
throw new Error(`Token exchange failed: ${error}`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const tokens = await response.json();
|
|
200
|
+
|
|
201
|
+
if (!tokens.access_token) {
|
|
202
|
+
console.error('[OAuth] No access token in response:', tokens);
|
|
203
|
+
throw new Error('No access token received');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
console.log('[OAuth] Token exchange successful, access_token length:', tokens.access_token?.length);
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
accessToken: tokens.access_token,
|
|
210
|
+
refreshToken: tokens.refresh_token,
|
|
211
|
+
expiresIn: tokens.expires_in
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Refresh access token using refresh token
|
|
217
|
+
*
|
|
218
|
+
* @param {string} refreshToken - OAuth refresh token
|
|
219
|
+
* @returns {Promise<{accessToken: string, expiresIn: number}>} New access token
|
|
220
|
+
*/
|
|
221
|
+
export async function refreshAccessToken(refreshToken) {
|
|
222
|
+
const response = await fetch(OAUTH_CONFIG.tokenUrl, {
|
|
223
|
+
method: 'POST',
|
|
224
|
+
headers: {
|
|
225
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
226
|
+
},
|
|
227
|
+
body: new URLSearchParams({
|
|
228
|
+
client_id: OAUTH_CONFIG.clientId,
|
|
229
|
+
client_secret: OAUTH_CONFIG.clientSecret,
|
|
230
|
+
refresh_token: refreshToken,
|
|
231
|
+
grant_type: 'refresh_token'
|
|
232
|
+
})
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
if (!response.ok) {
|
|
236
|
+
const error = await response.text();
|
|
237
|
+
throw new Error(`Token refresh failed: ${error}`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const tokens = await response.json();
|
|
241
|
+
return {
|
|
242
|
+
accessToken: tokens.access_token,
|
|
243
|
+
expiresIn: tokens.expires_in
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Get user email from access token
|
|
249
|
+
*
|
|
250
|
+
* @param {string} accessToken - OAuth access token
|
|
251
|
+
* @returns {Promise<string>} User's email address
|
|
252
|
+
*/
|
|
253
|
+
export async function getUserEmail(accessToken) {
|
|
254
|
+
const response = await fetch(OAUTH_CONFIG.userInfoUrl, {
|
|
255
|
+
headers: {
|
|
256
|
+
'Authorization': `Bearer ${accessToken}`
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
if (!response.ok) {
|
|
261
|
+
const errorText = await response.text();
|
|
262
|
+
console.error('[OAuth] getUserEmail failed:', response.status, errorText);
|
|
263
|
+
throw new Error(`Failed to get user info: ${response.status}`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const userInfo = await response.json();
|
|
267
|
+
return userInfo.email;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Discover project ID for the authenticated user
|
|
272
|
+
*
|
|
273
|
+
* @param {string} accessToken - OAuth access token
|
|
274
|
+
* @returns {Promise<string|null>} Project ID or null if not found
|
|
275
|
+
*/
|
|
276
|
+
export async function discoverProjectId(accessToken) {
|
|
277
|
+
for (const endpoint of ANTIGRAVITY_ENDPOINT_FALLBACKS) {
|
|
278
|
+
try {
|
|
279
|
+
const response = await fetch(`${endpoint}/v1internal:loadCodeAssist`, {
|
|
280
|
+
method: 'POST',
|
|
281
|
+
headers: {
|
|
282
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
283
|
+
'Content-Type': 'application/json',
|
|
284
|
+
...ANTIGRAVITY_HEADERS
|
|
285
|
+
},
|
|
286
|
+
body: JSON.stringify({
|
|
287
|
+
metadata: {
|
|
288
|
+
ideType: 'IDE_UNSPECIFIED',
|
|
289
|
+
platform: 'PLATFORM_UNSPECIFIED',
|
|
290
|
+
pluginType: 'GEMINI'
|
|
291
|
+
}
|
|
292
|
+
})
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
if (!response.ok) continue;
|
|
296
|
+
|
|
297
|
+
const data = await response.json();
|
|
298
|
+
|
|
299
|
+
if (typeof data.cloudaicompanionProject === 'string') {
|
|
300
|
+
return data.cloudaicompanionProject;
|
|
301
|
+
}
|
|
302
|
+
if (data.cloudaicompanionProject?.id) {
|
|
303
|
+
return data.cloudaicompanionProject.id;
|
|
304
|
+
}
|
|
305
|
+
} catch (error) {
|
|
306
|
+
console.log(`[OAuth] Project discovery failed at ${endpoint}:`, error.message);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Complete OAuth flow: exchange code and get all account info
|
|
315
|
+
*
|
|
316
|
+
* @param {string} code - Authorization code from OAuth callback
|
|
317
|
+
* @param {string} verifier - PKCE code verifier
|
|
318
|
+
* @returns {Promise<{email: string, refreshToken: string, accessToken: string, projectId: string|null}>} Complete account info
|
|
319
|
+
*/
|
|
320
|
+
export async function completeOAuthFlow(code, verifier) {
|
|
321
|
+
// Exchange code for tokens
|
|
322
|
+
const tokens = await exchangeCode(code, verifier);
|
|
323
|
+
|
|
324
|
+
// Get user email
|
|
325
|
+
const email = await getUserEmail(tokens.accessToken);
|
|
326
|
+
|
|
327
|
+
// Discover project ID
|
|
328
|
+
const projectId = await discoverProjectId(tokens.accessToken);
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
email,
|
|
332
|
+
refreshToken: tokens.refreshToken,
|
|
333
|
+
accessToken: tokens.accessToken,
|
|
334
|
+
projectId
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export default {
|
|
339
|
+
getAuthorizationUrl,
|
|
340
|
+
startCallbackServer,
|
|
341
|
+
exchangeCode,
|
|
342
|
+
refreshAccessToken,
|
|
343
|
+
getUserEmail,
|
|
344
|
+
discoverProjectId,
|
|
345
|
+
completeOAuthFlow
|
|
346
|
+
};
|