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/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
+ };