opencode-antigravity-auth 1.0.6 → 1.1.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 (41) hide show
  1. package/README.md +29 -7
  2. package/dist/src/plugin/accounts.d.ts +37 -0
  3. package/dist/src/plugin/accounts.d.ts.map +1 -0
  4. package/dist/src/plugin/accounts.js +190 -0
  5. package/dist/src/plugin/accounts.js.map +1 -0
  6. package/dist/src/plugin/accounts.test.d.ts +2 -0
  7. package/dist/src/plugin/accounts.test.d.ts.map +1 -0
  8. package/dist/src/plugin/accounts.test.js +139 -0
  9. package/dist/src/plugin/accounts.test.js.map +1 -0
  10. package/dist/src/plugin/cli.d.ts +4 -0
  11. package/dist/src/plugin/cli.d.ts.map +1 -1
  12. package/dist/src/plugin/cli.js +14 -0
  13. package/dist/src/plugin/cli.js.map +1 -1
  14. package/dist/src/plugin/project.d.ts +2 -2
  15. package/dist/src/plugin/project.d.ts.map +1 -1
  16. package/dist/src/plugin/project.js +1 -5
  17. package/dist/src/plugin/project.js.map +1 -1
  18. package/dist/src/plugin/request-helpers.d.ts +1 -0
  19. package/dist/src/plugin/request-helpers.d.ts.map +1 -1
  20. package/dist/src/plugin/request-helpers.js +44 -18
  21. package/dist/src/plugin/request-helpers.js.map +1 -1
  22. package/dist/src/plugin/request.d.ts +2 -0
  23. package/dist/src/plugin/request.d.ts.map +1 -1
  24. package/dist/src/plugin/request.js +150 -14
  25. package/dist/src/plugin/request.js.map +1 -1
  26. package/dist/src/plugin/storage.d.ts +24 -0
  27. package/dist/src/plugin/storage.d.ts.map +1 -0
  28. package/dist/src/plugin/storage.js +47 -0
  29. package/dist/src/plugin/storage.js.map +1 -0
  30. package/dist/src/plugin/token.d.ts +13 -0
  31. package/dist/src/plugin/token.d.ts.map +1 -1
  32. package/dist/src/plugin/token.js +29 -30
  33. package/dist/src/plugin/token.js.map +1 -1
  34. package/dist/src/plugin/token.test.js +19 -11
  35. package/dist/src/plugin/token.test.js.map +1 -1
  36. package/dist/src/plugin/types.d.ts +30 -6
  37. package/dist/src/plugin/types.d.ts.map +1 -1
  38. package/dist/src/plugin.d.ts.map +1 -1
  39. package/dist/src/plugin.js +449 -124
  40. package/dist/src/plugin.js.map +1 -1
  41. package/package.json +1 -1
@@ -1,13 +1,162 @@
1
1
  import { exec } from "node:child_process";
2
- import { ANTIGRAVITY_ENDPOINT_FALLBACKS, ANTIGRAVITY_PROVIDER_ID, ANTIGRAVITY_REDIRECT_URI, } from "./constants";
2
+ import { ANTIGRAVITY_ENDPOINT_FALLBACKS, ANTIGRAVITY_PROVIDER_ID } from "./constants";
3
3
  import { authorizeAntigravity, exchangeAntigravity } from "./antigravity/oauth";
4
- import { accessTokenExpired, isOAuthAuth } from "./plugin/auth";
5
- import { promptProjectId } from "./plugin/cli";
4
+ import { accessTokenExpired, isOAuthAuth, parseRefreshParts } from "./plugin/auth";
5
+ import { promptAddAnotherAccount, promptProjectId } from "./plugin/cli";
6
6
  import { ensureProjectContext } from "./plugin/project";
7
7
  import { startAntigravityDebugRequest } from "./plugin/debug";
8
8
  import { isGenerativeLanguageRequest, prepareAntigravityRequest, transformAntigravityResponse, } from "./plugin/request";
9
- import { refreshAccessToken } from "./plugin/token";
9
+ import { AntigravityTokenRefreshError, refreshAccessToken } from "./plugin/token";
10
10
  import { startOAuthListener } from "./plugin/server";
11
+ import { loadAccounts, saveAccounts } from "./plugin/storage";
12
+ import { AccountManager } from "./plugin/accounts";
13
+ const MAX_OAUTH_ACCOUNTS = 10;
14
+ async function openBrowser(url) {
15
+ try {
16
+ if (process.platform === "darwin") {
17
+ exec(`open "${url}"`);
18
+ return;
19
+ }
20
+ if (process.platform === "win32") {
21
+ exec(`start "${url}"`);
22
+ return;
23
+ }
24
+ exec(`xdg-open "${url}"`);
25
+ }
26
+ catch {
27
+ // ignore
28
+ }
29
+ }
30
+ async function promptOAuthCallbackValue(message) {
31
+ const { createInterface } = await import("node:readline/promises");
32
+ const { stdin, stdout } = await import("node:process");
33
+ const rl = createInterface({ input: stdin, output: stdout });
34
+ try {
35
+ return (await rl.question(message)).trim();
36
+ }
37
+ finally {
38
+ rl.close();
39
+ }
40
+ }
41
+ function getStateFromAuthorizationUrl(authorizationUrl) {
42
+ try {
43
+ return new URL(authorizationUrl).searchParams.get("state") ?? "";
44
+ }
45
+ catch {
46
+ return "";
47
+ }
48
+ }
49
+ function extractOAuthCallbackParams(url) {
50
+ const code = url.searchParams.get("code");
51
+ const state = url.searchParams.get("state");
52
+ if (!code || !state) {
53
+ return null;
54
+ }
55
+ return { code, state };
56
+ }
57
+ function parseOAuthCallbackInput(value, fallbackState) {
58
+ const trimmed = value.trim();
59
+ if (!trimmed) {
60
+ return { error: "Missing authorization code" };
61
+ }
62
+ try {
63
+ const url = new URL(trimmed);
64
+ const code = url.searchParams.get("code");
65
+ const state = url.searchParams.get("state") ?? fallbackState;
66
+ if (!code) {
67
+ return { error: "Missing code in callback URL" };
68
+ }
69
+ if (!state) {
70
+ return { error: "Missing state in callback URL" };
71
+ }
72
+ return { code, state };
73
+ }
74
+ catch {
75
+ if (!fallbackState) {
76
+ return { error: "Missing state. Paste the full redirect URL instead of only the code." };
77
+ }
78
+ return { code: trimmed, state: fallbackState };
79
+ }
80
+ }
81
+ function clampInt(value, min, max) {
82
+ if (!Number.isFinite(value)) {
83
+ return min;
84
+ }
85
+ return Math.min(max, Math.max(min, Math.floor(value)));
86
+ }
87
+ async function persistAccountPool(results) {
88
+ if (results.length === 0) {
89
+ return;
90
+ }
91
+ const now = Date.now();
92
+ const stored = await loadAccounts();
93
+ const accounts = stored?.accounts ? [...stored.accounts] : [];
94
+ const indexByRefreshToken = new Map();
95
+ for (let i = 0; i < accounts.length; i++) {
96
+ const token = accounts[i]?.refreshToken;
97
+ if (token) {
98
+ indexByRefreshToken.set(token, i);
99
+ }
100
+ }
101
+ for (const result of results) {
102
+ const parts = parseRefreshParts(result.refresh);
103
+ if (!parts.refreshToken) {
104
+ continue;
105
+ }
106
+ const existingIndex = indexByRefreshToken.get(parts.refreshToken);
107
+ if (existingIndex === undefined) {
108
+ indexByRefreshToken.set(parts.refreshToken, accounts.length);
109
+ accounts.push({
110
+ email: result.email,
111
+ refreshToken: parts.refreshToken,
112
+ projectId: parts.projectId,
113
+ managedProjectId: parts.managedProjectId,
114
+ addedAt: now,
115
+ lastUsed: now,
116
+ isRateLimited: false,
117
+ rateLimitResetTime: 0,
118
+ });
119
+ continue;
120
+ }
121
+ const existing = accounts[existingIndex];
122
+ if (!existing) {
123
+ continue;
124
+ }
125
+ accounts[existingIndex] = {
126
+ ...existing,
127
+ email: result.email ?? existing.email,
128
+ projectId: parts.projectId ?? existing.projectId,
129
+ managedProjectId: parts.managedProjectId ?? existing.managedProjectId,
130
+ lastUsed: now,
131
+ };
132
+ }
133
+ if (accounts.length === 0) {
134
+ return;
135
+ }
136
+ const activeIndex = typeof stored?.activeIndex === "number" && Number.isFinite(stored.activeIndex) ? stored.activeIndex : 0;
137
+ await saveAccounts({
138
+ version: 1,
139
+ accounts,
140
+ activeIndex: clampInt(activeIndex, 0, accounts.length - 1),
141
+ });
142
+ }
143
+ function retryAfterMsFromResponse(response) {
144
+ const retryAfterMsHeader = response.headers.get("retry-after-ms");
145
+ if (retryAfterMsHeader) {
146
+ const parsed = Number.parseInt(retryAfterMsHeader, 10);
147
+ if (!Number.isNaN(parsed) && parsed > 0) {
148
+ return parsed;
149
+ }
150
+ }
151
+ const retryAfterHeader = response.headers.get("retry-after");
152
+ if (retryAfterHeader) {
153
+ const parsed = Number.parseInt(retryAfterHeader, 10);
154
+ if (!Number.isNaN(parsed) && parsed > 0) {
155
+ return parsed * 1000;
156
+ }
157
+ }
158
+ return 60_000;
159
+ }
11
160
  /**
12
161
  * Creates an Antigravity OAuth plugin for a specific provider ID.
13
162
  */
@@ -19,6 +168,15 @@ export const createAntigravityPlugin = (providerId) => async ({ client }) => ({
19
168
  if (!isOAuthAuth(auth)) {
20
169
  return null;
21
170
  }
171
+ const accountManager = await AccountManager.loadFromDisk(auth);
172
+ if (accountManager.getAccountCount() > 0) {
173
+ try {
174
+ await accountManager.saveToDisk();
175
+ }
176
+ catch (error) {
177
+ console.error("[opencode-antigravity-auth] Failed to persist initial account pool:", error);
178
+ }
179
+ }
22
180
  if (provider.models) {
23
181
  for (const model of Object.values(provider.models)) {
24
182
  if (model) {
@@ -39,89 +197,170 @@ export const createAntigravityPlugin = (providerId) => async ({ client }) => ({
39
197
  if (!isOAuthAuth(latestAuth)) {
40
198
  return fetch(input, init);
41
199
  }
42
- let authRecord = latestAuth;
43
- if (accessTokenExpired(authRecord)) {
44
- const refreshed = await refreshAccessToken(authRecord, client, providerId);
45
- if (!refreshed) {
46
- return fetch(input, init);
47
- }
48
- authRecord = refreshed;
49
- }
50
- const accessToken = authRecord.access;
51
- if (!accessToken) {
52
- return fetch(input, init);
200
+ const accountCount = accountManager.getAccountCount();
201
+ if (accountCount === 0) {
202
+ throw new Error("No Antigravity accounts configured. Run `opencode auth login`.");
53
203
  }
54
- /**
55
- * Ensures we have a usable project context for the current auth snapshot.
56
- */
57
- async function resolveProjectContext() {
204
+ let lastFailure = null;
205
+ let lastError = null;
206
+ accountLoop: for (let attempt = 0; attempt < accountCount; attempt++) {
207
+ const account = accountManager.pickNext();
208
+ if (!account) {
209
+ const waitMs = accountManager.getMinWaitTimeMs();
210
+ const waitSec = Math.max(1, Math.ceil(waitMs / 1000));
211
+ throw new Error(`All ${accountManager.getAccountCount()} account(s) are rate-limited. Retry in ${waitSec}s or add more accounts via 'opencode auth login'.`);
212
+ }
58
213
  try {
59
- return await ensureProjectContext(authRecord, client, providerId);
214
+ await accountManager.saveToDisk();
60
215
  }
61
216
  catch (error) {
62
- throw error;
217
+ console.error("[opencode-antigravity-auth] Failed to persist rotation state:", error);
63
218
  }
64
- }
65
- const projectContext = await resolveProjectContext();
66
- // Endpoint fallback logic: try daily → autopush → prod
67
- let lastError = null;
68
- let lastResponse = null;
69
- for (let i = 0; i < ANTIGRAVITY_ENDPOINT_FALLBACKS.length; i++) {
70
- const currentEndpoint = ANTIGRAVITY_ENDPOINT_FALLBACKS[i];
71
- try {
72
- const { request, init: transformedInit, streaming, requestedModel, effectiveModel, projectId: usedProjectId, endpoint: usedEndpoint, toolDebugMissing, toolDebugSummary, toolDebugPayload, } = prepareAntigravityRequest(input, init, accessToken, projectContext.effectiveProjectId, currentEndpoint);
73
- const originalUrl = toUrlString(input);
74
- const resolvedUrl = toUrlString(request);
75
- const debugContext = startAntigravityDebugRequest({
76
- originalUrl,
77
- resolvedUrl,
78
- method: transformedInit.method,
79
- headers: transformedInit.headers,
80
- body: transformedInit.body,
81
- streaming,
82
- projectId: projectContext.effectiveProjectId,
83
- });
84
- const response = await fetch(request, transformedInit);
85
- // Check if we should retry with next endpoint
86
- const shouldRetry = (response.status === 403 || // Forbidden
87
- response.status === 404 || // Not Found
88
- response.status === 429 || // Rate Limit
89
- response.status >= 500 // Server errors
90
- );
91
- if (shouldRetry && i < ANTIGRAVITY_ENDPOINT_FALLBACKS.length - 1) {
92
- // Try next endpoint
93
- lastResponse = response;
219
+ let authRecord = accountManager.toAuthDetails(account);
220
+ if (accessTokenExpired(authRecord)) {
221
+ try {
222
+ const refreshed = await refreshAccessToken(authRecord, client, providerId);
223
+ if (!refreshed) {
224
+ lastError = new Error("Antigravity token refresh failed");
225
+ continue;
226
+ }
227
+ accountManager.updateFromAuth(account, refreshed);
228
+ authRecord = refreshed;
229
+ try {
230
+ await accountManager.saveToDisk();
231
+ }
232
+ catch (error) {
233
+ console.error("[opencode-antigravity-auth] Failed to persist refreshed auth:", error);
234
+ }
235
+ }
236
+ catch (error) {
237
+ if (error instanceof AntigravityTokenRefreshError && error.code === "invalid_grant") {
238
+ const removed = accountManager.removeAccount(account);
239
+ if (removed) {
240
+ console.warn("[opencode-antigravity-auth] Removed revoked account from pool. Reauthenticate it via `opencode auth login` to add it back.");
241
+ try {
242
+ await accountManager.saveToDisk();
243
+ }
244
+ catch (persistError) {
245
+ console.error("[opencode-antigravity-auth] Failed to persist revoked account removal:", persistError);
246
+ }
247
+ }
248
+ if (accountManager.getAccountCount() === 0) {
249
+ try {
250
+ await client.auth.set({
251
+ path: { id: providerId },
252
+ body: { type: "oauth", refresh: "" },
253
+ });
254
+ }
255
+ catch (storeError) {
256
+ console.error("Failed to clear stored Antigravity OAuth credentials:", storeError);
257
+ }
258
+ throw new Error("All Antigravity accounts have invalid refresh tokens. Run `opencode auth login` and reauthenticate.");
259
+ }
260
+ lastError = error;
261
+ continue;
262
+ }
263
+ lastError = error instanceof Error ? error : new Error(String(error));
94
264
  continue;
95
265
  }
96
- // Success or final attempt - return transformed response
97
- return transformAntigravityResponse(response, streaming, debugContext, requestedModel, usedProjectId, usedEndpoint, effectiveModel, toolDebugMissing, toolDebugSummary, toolDebugPayload);
266
+ }
267
+ const accessToken = authRecord.access;
268
+ if (!accessToken) {
269
+ lastError = new Error("Missing access token");
270
+ continue;
271
+ }
272
+ let projectContext;
273
+ try {
274
+ projectContext = await ensureProjectContext(authRecord);
98
275
  }
99
276
  catch (error) {
100
- // Network error or other exception
101
- if (i < ANTIGRAVITY_ENDPOINT_FALLBACKS.length - 1) {
277
+ lastError = error instanceof Error ? error : new Error(String(error));
278
+ continue;
279
+ }
280
+ if (projectContext.auth !== authRecord) {
281
+ accountManager.updateFromAuth(account, projectContext.auth);
282
+ authRecord = projectContext.auth;
283
+ try {
284
+ await accountManager.saveToDisk();
285
+ }
286
+ catch (error) {
287
+ console.error("[opencode-antigravity-auth] Failed to persist project context:", error);
288
+ }
289
+ }
290
+ for (let i = 0; i < ANTIGRAVITY_ENDPOINT_FALLBACKS.length; i++) {
291
+ const currentEndpoint = ANTIGRAVITY_ENDPOINT_FALLBACKS[i];
292
+ try {
293
+ const prepared = prepareAntigravityRequest(input, init, accessToken, projectContext.effectiveProjectId, currentEndpoint);
294
+ const originalUrl = toUrlString(input);
295
+ const resolvedUrl = toUrlString(prepared.request);
296
+ const debugContext = startAntigravityDebugRequest({
297
+ originalUrl,
298
+ resolvedUrl,
299
+ method: prepared.init.method,
300
+ headers: prepared.init.headers,
301
+ body: prepared.init.body,
302
+ streaming: prepared.streaming,
303
+ projectId: projectContext.effectiveProjectId,
304
+ });
305
+ const response = await fetch(prepared.request, prepared.init);
306
+ if (response.status === 429 && accountManager.getAccountCount() > 1) {
307
+ const retryAfterMs = retryAfterMsFromResponse(response);
308
+ accountManager.markRateLimited(account, retryAfterMs);
309
+ try {
310
+ await accountManager.saveToDisk();
311
+ }
312
+ catch (error) {
313
+ console.error("[opencode-antigravity-auth] Failed to persist rate-limit state:", error);
314
+ }
315
+ lastFailure = {
316
+ response,
317
+ streaming: prepared.streaming,
318
+ debugContext,
319
+ requestedModel: prepared.requestedModel,
320
+ projectId: prepared.projectId,
321
+ endpoint: prepared.endpoint,
322
+ effectiveModel: prepared.effectiveModel,
323
+ toolDebugMissing: prepared.toolDebugMissing,
324
+ toolDebugSummary: prepared.toolDebugSummary,
325
+ toolDebugPayload: prepared.toolDebugPayload,
326
+ };
327
+ continue accountLoop;
328
+ }
329
+ const shouldRetry = (response.status === 403 ||
330
+ response.status === 404 ||
331
+ (response.status === 429 && accountManager.getAccountCount() <= 1) ||
332
+ response.status >= 500);
333
+ if (shouldRetry && i < ANTIGRAVITY_ENDPOINT_FALLBACKS.length - 1) {
334
+ lastFailure = {
335
+ response,
336
+ streaming: prepared.streaming,
337
+ debugContext,
338
+ requestedModel: prepared.requestedModel,
339
+ projectId: prepared.projectId,
340
+ endpoint: prepared.endpoint,
341
+ effectiveModel: prepared.effectiveModel,
342
+ toolDebugMissing: prepared.toolDebugMissing,
343
+ toolDebugSummary: prepared.toolDebugSummary,
344
+ toolDebugPayload: prepared.toolDebugPayload,
345
+ };
346
+ continue;
347
+ }
348
+ return transformAntigravityResponse(response, prepared.streaming, debugContext, prepared.requestedModel, prepared.projectId, prepared.endpoint, prepared.effectiveModel, prepared.toolDebugMissing, prepared.toolDebugSummary, prepared.toolDebugPayload);
349
+ }
350
+ catch (error) {
351
+ if (i < ANTIGRAVITY_ENDPOINT_FALLBACKS.length - 1) {
352
+ lastError = error instanceof Error ? error : new Error(String(error));
353
+ continue;
354
+ }
102
355
  lastError = error instanceof Error ? error : new Error(String(error));
103
- continue;
356
+ continue accountLoop;
104
357
  }
105
- // Final attempt failed, throw the error
106
- throw error;
107
358
  }
108
359
  }
109
- // If we get here, all endpoints failed
110
- if (lastResponse) {
111
- // Return the last response even if it was an error
112
- const { streaming, requestedModel, effectiveModel, projectId: usedProjectId, endpoint: usedEndpoint, toolDebugMissing, toolDebugSummary, toolDebugPayload, } = prepareAntigravityRequest(input, init, accessToken, projectContext.effectiveProjectId, ANTIGRAVITY_ENDPOINT_FALLBACKS[ANTIGRAVITY_ENDPOINT_FALLBACKS.length - 1]);
113
- const debugContext = startAntigravityDebugRequest({
114
- originalUrl: toUrlString(input),
115
- resolvedUrl: toUrlString(input),
116
- method: init?.method,
117
- headers: init?.headers,
118
- body: init?.body,
119
- streaming,
120
- projectId: projectContext.effectiveProjectId,
121
- });
122
- return transformAntigravityResponse(lastResponse, streaming, debugContext, requestedModel, usedProjectId, usedEndpoint, effectiveModel, toolDebugMissing, toolDebugSummary, toolDebugPayload);
360
+ if (lastFailure) {
361
+ return transformAntigravityResponse(lastFailure.response, lastFailure.streaming, lastFailure.debugContext, lastFailure.requestedModel, lastFailure.projectId, lastFailure.endpoint, lastFailure.effectiveModel, lastFailure.toolDebugMissing, lastFailure.toolDebugSummary, lastFailure.toolDebugPayload);
123
362
  }
124
- throw lastError || new Error("All Antigravity endpoints failed");
363
+ throw lastError || new Error("All Antigravity accounts failed");
125
364
  },
126
365
  };
127
366
  },
@@ -129,59 +368,148 @@ export const createAntigravityPlugin = (providerId) => async ({ client }) => ({
129
368
  {
130
369
  label: "OAuth with Google (Antigravity)",
131
370
  type: "oauth",
132
- authorize: async () => {
371
+ authorize: async (inputs) => {
133
372
  const isHeadless = !!(process.env.SSH_CONNECTION ||
134
373
  process.env.SSH_CLIENT ||
135
374
  process.env.SSH_TTY ||
136
375
  process.env.OPENCODE_HEADLESS);
376
+ // CLI flow (`opencode auth login`) passes an inputs object.
377
+ if (inputs) {
378
+ const accounts = [];
379
+ while (accounts.length < MAX_OAUTH_ACCOUNTS) {
380
+ console.log(`\n=== Antigravity OAuth (Account ${accounts.length + 1}) ===`);
381
+ const projectId = await promptProjectId();
382
+ const result = await (async () => {
383
+ let listener = null;
384
+ if (!isHeadless) {
385
+ try {
386
+ listener = await startOAuthListener();
387
+ }
388
+ catch {
389
+ listener = null;
390
+ }
391
+ }
392
+ const authorization = await authorizeAntigravity(projectId);
393
+ const fallbackState = getStateFromAuthorizationUrl(authorization.url);
394
+ console.log("\nOAuth URL:\n" + authorization.url + "\n");
395
+ if (!isHeadless) {
396
+ await openBrowser(authorization.url);
397
+ }
398
+ if (listener) {
399
+ try {
400
+ const callbackUrl = await listener.waitForCallback();
401
+ const params = extractOAuthCallbackParams(callbackUrl);
402
+ if (!params) {
403
+ return { type: "failed", error: "Missing code or state in callback URL" };
404
+ }
405
+ return exchangeAntigravity(params.code, params.state);
406
+ }
407
+ catch (error) {
408
+ return {
409
+ type: "failed",
410
+ error: error instanceof Error ? error.message : "Unknown error",
411
+ };
412
+ }
413
+ finally {
414
+ try {
415
+ await listener.close();
416
+ }
417
+ catch {
418
+ // ignore
419
+ }
420
+ }
421
+ }
422
+ console.log("1. Open the URL below in your browser and complete Google sign-in.");
423
+ console.log("2. After approving, copy the full redirected localhost URL from the address bar.");
424
+ console.log("3. Paste it back here.");
425
+ const callbackInput = await promptOAuthCallbackValue("Paste the redirect URL (or just the code) here: ");
426
+ const params = parseOAuthCallbackInput(callbackInput, fallbackState);
427
+ if ("error" in params) {
428
+ return { type: "failed", error: params.error };
429
+ }
430
+ return exchangeAntigravity(params.code, params.state);
431
+ })();
432
+ if (result.type === "failed") {
433
+ if (accounts.length === 0) {
434
+ return {
435
+ url: "",
436
+ instructions: `Authentication failed: ${result.error}`,
437
+ method: "auto",
438
+ callback: async () => result,
439
+ };
440
+ }
441
+ console.warn(`[opencode-antigravity-auth] Skipping failed account ${accounts.length + 1}: ${result.error}`);
442
+ break;
443
+ }
444
+ accounts.push(result);
445
+ try {
446
+ await persistAccountPool([result]);
447
+ }
448
+ catch {
449
+ // ignore
450
+ }
451
+ if (accounts.length >= MAX_OAUTH_ACCOUNTS) {
452
+ break;
453
+ }
454
+ const addAnother = await promptAddAnotherAccount(accounts.length);
455
+ if (!addAnother) {
456
+ break;
457
+ }
458
+ }
459
+ const primary = accounts[0];
460
+ if (!primary) {
461
+ return {
462
+ url: "",
463
+ instructions: "Authentication cancelled",
464
+ method: "auto",
465
+ callback: async () => ({ type: "failed", error: "Authentication cancelled" }),
466
+ };
467
+ }
468
+ return {
469
+ url: "",
470
+ instructions: `Multi-account setup complete (${accounts.length} account(s)).`,
471
+ method: "auto",
472
+ callback: async () => primary,
473
+ };
474
+ }
475
+ // TUI flow (`/connect`) does not support per-account prompts yet.
476
+ const projectId = "";
137
477
  let listener = null;
138
478
  if (!isHeadless) {
139
479
  try {
140
480
  listener = await startOAuthListener();
141
481
  }
142
- catch (error) {
143
- console.log("\nWarning: Couldn't start the local callback listener. Falling back to manual copy/paste.");
482
+ catch {
483
+ listener = null;
144
484
  }
145
485
  }
146
- const authorization = await authorizeAntigravity("");
147
- // Try to open the browser automatically
486
+ const authorization = await authorizeAntigravity(projectId);
487
+ const fallbackState = getStateFromAuthorizationUrl(authorization.url);
148
488
  if (!isHeadless) {
149
- try {
150
- if (process.platform === "darwin") {
151
- exec(`open "${authorization.url}"`);
152
- }
153
- else if (process.platform === "win32") {
154
- exec(`start "${authorization.url}"`);
155
- }
156
- else {
157
- exec(`xdg-open "${authorization.url}"`);
158
- }
159
- }
160
- catch (e) {
161
- console.log("Could not open browser automatically. Please Copy/Paste the URL below.");
162
- }
489
+ await openBrowser(authorization.url);
163
490
  }
164
491
  if (listener) {
165
- const { host } = new URL(ANTIGRAVITY_REDIRECT_URI);
166
492
  return {
167
493
  url: authorization.url,
168
- instructions: "Complete the sign-in flow in your browser. We'll automatically detect the redirect back to localhost.",
494
+ instructions: "Complete sign-in in your browser. We'll automatically detect the redirect back to localhost.",
169
495
  method: "auto",
170
496
  callback: async () => {
171
497
  try {
172
- // We know listener is not null here because we checked 'if (listener)'
173
- // But TS might need a check or non-null assertion if not inferable.
174
- // Since we are in the if (listener) block, it is safe.
175
498
  const callbackUrl = await listener.waitForCallback();
176
- const code = callbackUrl.searchParams.get("code");
177
- const state = callbackUrl.searchParams.get("state");
178
- if (!code || !state) {
179
- return {
180
- type: "failed",
181
- error: "Missing code or state in callback URL",
182
- };
499
+ const params = extractOAuthCallbackParams(callbackUrl);
500
+ if (!params) {
501
+ return { type: "failed", error: "Missing code or state in callback URL" };
183
502
  }
184
- return await exchangeAntigravity(code, state);
503
+ const result = await exchangeAntigravity(params.code, params.state);
504
+ if (result.type === "success") {
505
+ try {
506
+ await persistAccountPool([result]);
507
+ }
508
+ catch {
509
+ // ignore
510
+ }
511
+ }
512
+ return result;
185
513
  }
186
514
  catch (error) {
187
515
  return {
@@ -191,9 +519,10 @@ export const createAntigravityPlugin = (providerId) => async ({ client }) => ({
191
519
  }
192
520
  finally {
193
521
  try {
194
- await listener?.close();
522
+ await listener.close();
195
523
  }
196
524
  catch {
525
+ // ignore
197
526
  }
198
527
  }
199
528
  },
@@ -201,27 +530,23 @@ export const createAntigravityPlugin = (providerId) => async ({ client }) => ({
201
530
  }
202
531
  return {
203
532
  url: authorization.url,
204
- instructions: "Paste the full redirected URL (e.g., http://localhost:8085/oauth2callback?code=...): ",
533
+ instructions: "Visit the URL above, complete OAuth, then paste either the full redirect URL or the authorization code.",
205
534
  method: "code",
206
- callback: async (callbackUrl) => {
207
- try {
208
- const url = new URL(callbackUrl);
209
- const code = url.searchParams.get("code");
210
- const state = url.searchParams.get("state");
211
- if (!code || !state) {
212
- return {
213
- type: "failed",
214
- error: "Missing code or state in callback URL",
215
- };
216
- }
217
- return exchangeAntigravity(code, state);
535
+ callback: async (codeInput) => {
536
+ const params = parseOAuthCallbackInput(codeInput, fallbackState);
537
+ if ("error" in params) {
538
+ return { type: "failed", error: params.error };
218
539
  }
219
- catch (error) {
220
- return {
221
- type: "failed",
222
- error: error instanceof Error ? error.message : "Unknown error",
223
- };
540
+ const result = await exchangeAntigravity(params.code, params.state);
541
+ if (result.type === "success") {
542
+ try {
543
+ await persistAccountPool([result]);
544
+ }
545
+ catch {
546
+ // ignore
547
+ }
224
548
  }
549
+ return result;
225
550
  },
226
551
  };
227
552
  },