opencode-antigravity-auth 1.0.7 → 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.
- package/README.md +29 -7
- package/dist/src/plugin/accounts.d.ts +37 -0
- package/dist/src/plugin/accounts.d.ts.map +1 -0
- package/dist/src/plugin/accounts.js +190 -0
- package/dist/src/plugin/accounts.js.map +1 -0
- package/dist/src/plugin/accounts.test.d.ts +2 -0
- package/dist/src/plugin/accounts.test.d.ts.map +1 -0
- package/dist/src/plugin/accounts.test.js +139 -0
- package/dist/src/plugin/accounts.test.js.map +1 -0
- package/dist/src/plugin/cli.d.ts +4 -0
- package/dist/src/plugin/cli.d.ts.map +1 -1
- package/dist/src/plugin/cli.js +14 -0
- package/dist/src/plugin/cli.js.map +1 -1
- package/dist/src/plugin/project.d.ts +2 -2
- package/dist/src/plugin/project.d.ts.map +1 -1
- package/dist/src/plugin/project.js +1 -5
- package/dist/src/plugin/project.js.map +1 -1
- package/dist/src/plugin/storage.d.ts +24 -0
- package/dist/src/plugin/storage.d.ts.map +1 -0
- package/dist/src/plugin/storage.js +47 -0
- package/dist/src/plugin/storage.js.map +1 -0
- package/dist/src/plugin/token.d.ts +13 -0
- package/dist/src/plugin/token.d.ts.map +1 -1
- package/dist/src/plugin/token.js +29 -30
- package/dist/src/plugin/token.js.map +1 -1
- package/dist/src/plugin/token.test.js +19 -11
- package/dist/src/plugin/token.test.js.map +1 -1
- package/dist/src/plugin/types.d.ts +30 -6
- package/dist/src/plugin/types.d.ts.map +1 -1
- package/dist/src/plugin.d.ts.map +1 -1
- package/dist/src/plugin.js +449 -124
- package/dist/src/plugin.js.map +1 -1
- package/package.json +1 -1
package/dist/src/plugin.js
CHANGED
|
@@ -1,13 +1,162 @@
|
|
|
1
1
|
import { exec } from "node:child_process";
|
|
2
|
-
import { ANTIGRAVITY_ENDPOINT_FALLBACKS, ANTIGRAVITY_PROVIDER_ID
|
|
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
|
-
|
|
43
|
-
if (
|
|
44
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
214
|
+
await accountManager.saveToDisk();
|
|
60
215
|
}
|
|
61
216
|
catch (error) {
|
|
62
|
-
|
|
217
|
+
console.error("[opencode-antigravity-auth] Failed to persist rotation state:", error);
|
|
63
218
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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
|
|
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
|
|
143
|
-
|
|
482
|
+
catch {
|
|
483
|
+
listener = null;
|
|
144
484
|
}
|
|
145
485
|
}
|
|
146
|
-
const authorization = await authorizeAntigravity(
|
|
147
|
-
|
|
486
|
+
const authorization = await authorizeAntigravity(projectId);
|
|
487
|
+
const fallbackState = getStateFromAuthorizationUrl(authorization.url);
|
|
148
488
|
if (!isHeadless) {
|
|
149
|
-
|
|
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
|
|
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
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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
|
|
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: "
|
|
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 (
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
},
|