opencode-gemini-oauth 1.0.0 → 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/dist/index.js +154 -271
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// src/
|
|
1
|
+
// src/index.ts
|
|
2
2
|
import { createServer } from "http";
|
|
3
3
|
import { randomBytes, createHash } from "crypto";
|
|
4
|
-
import { URL
|
|
4
|
+
import { URL, URLSearchParams } from "url";
|
|
5
5
|
|
|
6
6
|
// src/constants.ts
|
|
7
7
|
var OAUTH_CONFIG = {
|
|
@@ -27,64 +27,21 @@ var CODE_ASSIST_HEADERS = {
|
|
|
27
27
|
"User-Agent": "antigravity/1.11.5",
|
|
28
28
|
"X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1"
|
|
29
29
|
};
|
|
30
|
-
var STORAGE_FILE = "gemini-oauth-accounts.json";
|
|
31
30
|
var TOKEN_REFRESH_BUFFER_MS = 60 * 1e3;
|
|
32
31
|
|
|
33
|
-
// src/
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const path = getStoragePath();
|
|
47
|
-
if (!existsSync(path)) {
|
|
48
|
-
return { version: 1, accounts: [], activeIndex: 0 };
|
|
49
|
-
}
|
|
50
|
-
try {
|
|
51
|
-
const data = JSON.parse(readFileSync(path, "utf-8"));
|
|
52
|
-
return {
|
|
53
|
-
version: 1,
|
|
54
|
-
accounts: data.accounts || [],
|
|
55
|
-
activeIndex: data.activeIndex || 0
|
|
56
|
-
};
|
|
57
|
-
} catch {
|
|
58
|
-
return { version: 1, accounts: [], activeIndex: 0 };
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
function saveStorage(data) {
|
|
62
|
-
const path = getStoragePath();
|
|
63
|
-
writeFileSync(path, JSON.stringify(data, null, 2), "utf-8");
|
|
64
|
-
}
|
|
65
|
-
function getActiveAccount() {
|
|
66
|
-
const storage = loadStorage();
|
|
67
|
-
if (storage.accounts.length === 0) {
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
return storage.accounts[storage.activeIndex] || storage.accounts[0] || null;
|
|
71
|
-
}
|
|
72
|
-
function addOrUpdateAccount(account) {
|
|
73
|
-
const storage = loadStorage();
|
|
74
|
-
const existingIndex = storage.accounts.findIndex(
|
|
75
|
-
(a) => a.email === account.email
|
|
76
|
-
);
|
|
77
|
-
if (existingIndex >= 0) {
|
|
78
|
-
storage.accounts[existingIndex] = account;
|
|
79
|
-
storage.activeIndex = existingIndex;
|
|
80
|
-
} else {
|
|
81
|
-
storage.accounts.push(account);
|
|
82
|
-
storage.activeIndex = storage.accounts.length - 1;
|
|
83
|
-
}
|
|
84
|
-
saveStorage(storage);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// src/oauth.ts
|
|
32
|
+
// src/index.ts
|
|
33
|
+
var MODEL_ALIASES = {
|
|
34
|
+
"gemini-3-pro-preview": "gemini-3-pro-high",
|
|
35
|
+
"gemini-3-flash-preview": "gemini-3-flash",
|
|
36
|
+
"gemini-2.5-pro": "gemini-2.5-pro-exp-03-25",
|
|
37
|
+
"gemini-2.5-flash": "gemini-2.5-flash",
|
|
38
|
+
"gemini-2.5-flash-lite": "gemini-2.5-flash-lite-001",
|
|
39
|
+
// Claude models via Antigravity
|
|
40
|
+
"gemini-claude-sonnet-4-5": "claude-sonnet-4-5",
|
|
41
|
+
"gemini-claude-sonnet-4-5-thinking": "claude-sonnet-4-5-thinking",
|
|
42
|
+
"gemini-claude-opus-4-5": "claude-opus-4-5",
|
|
43
|
+
"gemini-claude-opus-4-5-thinking": "claude-opus-4-5-thinking"
|
|
44
|
+
};
|
|
88
45
|
function generatePKCE() {
|
|
89
46
|
const verifier = randomBytes(32).toString("base64url").replace(/[^a-zA-Z0-9]/g, "").substring(0, 43);
|
|
90
47
|
const challenge = createHash("sha256").update(verifier).digest("base64url").replace(/[^a-zA-Z0-9\-_]/g, "");
|
|
@@ -94,7 +51,7 @@ function generateState() {
|
|
|
94
51
|
return randomBytes(16).toString("hex");
|
|
95
52
|
}
|
|
96
53
|
function buildAuthUrl(state, codeChallenge) {
|
|
97
|
-
const params = new
|
|
54
|
+
const params = new URLSearchParams({
|
|
98
55
|
client_id: OAUTH_CONFIG.clientId,
|
|
99
56
|
redirect_uri: OAUTH_CONFIG.redirectUri,
|
|
100
57
|
response_type: "code",
|
|
@@ -113,7 +70,7 @@ async function exchangeCodeForTokens(code, codeVerifier) {
|
|
|
113
70
|
headers: {
|
|
114
71
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
115
72
|
},
|
|
116
|
-
body: new
|
|
73
|
+
body: new URLSearchParams({
|
|
117
74
|
client_id: OAUTH_CONFIG.clientId,
|
|
118
75
|
client_secret: OAUTH_CONFIG.clientSecret,
|
|
119
76
|
code,
|
|
@@ -133,158 +90,7 @@ async function exchangeCodeForTokens(code, codeVerifier) {
|
|
|
133
90
|
expiresIn: data.expires_in
|
|
134
91
|
};
|
|
135
92
|
}
|
|
136
|
-
async function
|
|
137
|
-
const response = await fetch(
|
|
138
|
-
"https://www.googleapis.com/oauth2/v2/userinfo",
|
|
139
|
-
{
|
|
140
|
-
headers: {
|
|
141
|
-
Authorization: `Bearer ${accessToken}`
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
);
|
|
145
|
-
if (!response.ok) {
|
|
146
|
-
throw new Error("Failed to get user info");
|
|
147
|
-
}
|
|
148
|
-
const data = await response.json();
|
|
149
|
-
return { email: data.email, name: data.name };
|
|
150
|
-
}
|
|
151
|
-
async function getProjectId(accessToken) {
|
|
152
|
-
try {
|
|
153
|
-
const response = await fetch(
|
|
154
|
-
"https://cloudcode-pa.googleapis.com/v1/userWorkspace",
|
|
155
|
-
{
|
|
156
|
-
headers: {
|
|
157
|
-
Authorization: `Bearer ${accessToken}`,
|
|
158
|
-
"X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1"
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
);
|
|
162
|
-
if (response.ok) {
|
|
163
|
-
const data = await response.json();
|
|
164
|
-
return data.managedProjectId;
|
|
165
|
-
}
|
|
166
|
-
} catch {
|
|
167
|
-
}
|
|
168
|
-
return void 0;
|
|
169
|
-
}
|
|
170
|
-
async function startOAuthFlow() {
|
|
171
|
-
const { verifier, challenge } = generatePKCE();
|
|
172
|
-
const state = generateState();
|
|
173
|
-
const authUrl = buildAuthUrl(state, challenge);
|
|
174
|
-
return new Promise((resolveSetup, rejectSetup) => {
|
|
175
|
-
let callbackResolve;
|
|
176
|
-
let callbackReject;
|
|
177
|
-
const callbackPromise = new Promise((resolve, reject) => {
|
|
178
|
-
callbackResolve = resolve;
|
|
179
|
-
callbackReject = reject;
|
|
180
|
-
});
|
|
181
|
-
const server = createServer(async (req, res) => {
|
|
182
|
-
try {
|
|
183
|
-
const url = new URL2(req.url || "/", `http://localhost:${OAUTH_CONFIG.port}`);
|
|
184
|
-
if (url.pathname === "/oauth-callback") {
|
|
185
|
-
const code = url.searchParams.get("code");
|
|
186
|
-
const returnedState = url.searchParams.get("state");
|
|
187
|
-
const error = url.searchParams.get("error");
|
|
188
|
-
if (error) {
|
|
189
|
-
res.writeHead(400, { "Content-Type": "text/html" });
|
|
190
|
-
res.end(`
|
|
191
|
-
<html>
|
|
192
|
-
<body style="font-family: system-ui; padding: 40px; text-align: center;">
|
|
193
|
-
<h1 style="color: #dc2626;">Authentication Failed</h1>
|
|
194
|
-
<p>Error: ${error}</p>
|
|
195
|
-
<p>You can close this window.</p>
|
|
196
|
-
</body>
|
|
197
|
-
</html>
|
|
198
|
-
`);
|
|
199
|
-
callbackReject(new Error(`OAuth error: ${error}`));
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
if (!code || returnedState !== state) {
|
|
203
|
-
res.writeHead(400, { "Content-Type": "text/html" });
|
|
204
|
-
res.end(`
|
|
205
|
-
<html>
|
|
206
|
-
<body style="font-family: system-ui; padding: 40px; text-align: center;">
|
|
207
|
-
<h1 style="color: #dc2626;">Invalid Response</h1>
|
|
208
|
-
<p>Missing code or invalid state.</p>
|
|
209
|
-
<p>You can close this window.</p>
|
|
210
|
-
</body>
|
|
211
|
-
</html>
|
|
212
|
-
`);
|
|
213
|
-
callbackReject(new Error("Invalid OAuth response"));
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
const tokens = await exchangeCodeForTokens(code, verifier);
|
|
217
|
-
const userInfo = await getUserInfo(tokens.accessToken);
|
|
218
|
-
const projectId = await getProjectId(tokens.accessToken);
|
|
219
|
-
const account = {
|
|
220
|
-
email: userInfo.email,
|
|
221
|
-
refreshToken: tokens.refreshToken,
|
|
222
|
-
accessToken: tokens.accessToken,
|
|
223
|
-
expiresAt: Date.now() + tokens.expiresIn * 1e3,
|
|
224
|
-
projectId,
|
|
225
|
-
managedProjectId: projectId
|
|
226
|
-
};
|
|
227
|
-
addOrUpdateAccount(account);
|
|
228
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
229
|
-
res.end(`
|
|
230
|
-
<html>
|
|
231
|
-
<body style="font-family: system-ui; padding: 40px; text-align: center;">
|
|
232
|
-
<h1 style="color: #16a34a;">Authentication Successful!</h1>
|
|
233
|
-
<p>Logged in as: <strong>${userInfo.email}</strong></p>
|
|
234
|
-
<p>You can close this window and return to OpenCode.</p>
|
|
235
|
-
<script>setTimeout(() => window.close(), 2000);</script>
|
|
236
|
-
</body>
|
|
237
|
-
</html>
|
|
238
|
-
`);
|
|
239
|
-
callbackResolve(account);
|
|
240
|
-
} else {
|
|
241
|
-
res.writeHead(404);
|
|
242
|
-
res.end("Not found");
|
|
243
|
-
}
|
|
244
|
-
} catch (err) {
|
|
245
|
-
res.writeHead(500, { "Content-Type": "text/html" });
|
|
246
|
-
res.end(`
|
|
247
|
-
<html>
|
|
248
|
-
<body style="font-family: system-ui; padding: 40px; text-align: center;">
|
|
249
|
-
<h1 style="color: #dc2626;">Error</h1>
|
|
250
|
-
<p>${err instanceof Error ? err.message : "Unknown error"}</p>
|
|
251
|
-
<p>You can close this window.</p>
|
|
252
|
-
</body>
|
|
253
|
-
</html>
|
|
254
|
-
`);
|
|
255
|
-
callbackReject(err instanceof Error ? err : new Error("Unknown error"));
|
|
256
|
-
}
|
|
257
|
-
});
|
|
258
|
-
server.listen(OAUTH_CONFIG.port, async () => {
|
|
259
|
-
try {
|
|
260
|
-
const open = (await import("open")).default;
|
|
261
|
-
await open(authUrl);
|
|
262
|
-
} catch {
|
|
263
|
-
}
|
|
264
|
-
resolveSetup({
|
|
265
|
-
authUrl,
|
|
266
|
-
server,
|
|
267
|
-
waitForCallback: () => callbackPromise
|
|
268
|
-
});
|
|
269
|
-
});
|
|
270
|
-
server.on("error", (err) => {
|
|
271
|
-
rejectSetup(new Error(`Failed to start OAuth server: ${err.message}`));
|
|
272
|
-
});
|
|
273
|
-
setTimeout(() => {
|
|
274
|
-
server.close();
|
|
275
|
-
callbackReject(new Error("OAuth flow timed out"));
|
|
276
|
-
}, 5 * 60 * 1e3);
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// src/token.ts
|
|
281
|
-
function isTokenExpired(account) {
|
|
282
|
-
if (!account.accessToken || !account.accessTokenExpiry) {
|
|
283
|
-
return true;
|
|
284
|
-
}
|
|
285
|
-
return Date.now() >= account.accessTokenExpiry - TOKEN_REFRESH_BUFFER_MS;
|
|
286
|
-
}
|
|
287
|
-
async function refreshAccessToken(account) {
|
|
93
|
+
async function refreshAccessToken(refreshToken) {
|
|
288
94
|
const response = await fetch(OAUTH_CONFIG.tokenUrl, {
|
|
289
95
|
method: "POST",
|
|
290
96
|
headers: {
|
|
@@ -293,7 +99,7 @@ async function refreshAccessToken(account) {
|
|
|
293
99
|
body: new URLSearchParams({
|
|
294
100
|
client_id: OAUTH_CONFIG.clientId,
|
|
295
101
|
client_secret: OAUTH_CONFIG.clientSecret,
|
|
296
|
-
refresh_token:
|
|
102
|
+
refresh_token: refreshToken,
|
|
297
103
|
grant_type: "refresh_token"
|
|
298
104
|
}).toString()
|
|
299
105
|
});
|
|
@@ -302,43 +108,14 @@ async function refreshAccessToken(account) {
|
|
|
302
108
|
throw new Error(`Token refresh failed: ${error}`);
|
|
303
109
|
}
|
|
304
110
|
const data = await response.json();
|
|
305
|
-
const expiresAt = Date.now() + data.expires_in * 1e3;
|
|
306
|
-
const updatedAccount = {
|
|
307
|
-
...account,
|
|
308
|
-
accessToken: data.access_token,
|
|
309
|
-
accessTokenExpiry: expiresAt
|
|
310
|
-
};
|
|
311
|
-
addOrUpdateAccount(updatedAccount);
|
|
312
111
|
return {
|
|
313
112
|
accessToken: data.access_token,
|
|
314
|
-
|
|
113
|
+
expiresIn: data.expires_in
|
|
315
114
|
};
|
|
316
115
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
if (!account) {
|
|
320
|
-
throw new Error("No authenticated account. Please run OAuth login first.");
|
|
321
|
-
}
|
|
322
|
-
if (isTokenExpired(account)) {
|
|
323
|
-
const { accessToken } = await refreshAccessToken(account);
|
|
324
|
-
return accessToken;
|
|
325
|
-
}
|
|
326
|
-
return account.accessToken;
|
|
116
|
+
function isTokenExpired(expires) {
|
|
117
|
+
return Date.now() >= expires - TOKEN_REFRESH_BUFFER_MS;
|
|
327
118
|
}
|
|
328
|
-
|
|
329
|
-
// src/fetch-wrapper.ts
|
|
330
|
-
var MODEL_ALIASES = {
|
|
331
|
-
"gemini-3-pro-preview": "gemini-3-pro-high",
|
|
332
|
-
"gemini-3-flash-preview": "gemini-3-flash",
|
|
333
|
-
"gemini-2.5-pro": "gemini-2.5-pro-exp-03-25",
|
|
334
|
-
"gemini-2.5-flash": "gemini-2.5-flash",
|
|
335
|
-
"gemini-2.5-flash-lite": "gemini-2.5-flash-lite-001",
|
|
336
|
-
// Claude models via Antigravity
|
|
337
|
-
"gemini-claude-sonnet-4-5": "claude-sonnet-4-5",
|
|
338
|
-
"gemini-claude-sonnet-4-5-thinking": "claude-sonnet-4-5-thinking",
|
|
339
|
-
"gemini-claude-opus-4-5": "claude-opus-4-5",
|
|
340
|
-
"gemini-claude-opus-4-5-thinking": "claude-opus-4-5-thinking"
|
|
341
|
-
};
|
|
342
119
|
function extractAction(url) {
|
|
343
120
|
const match = url.match(/:(\w+)(?:\?|$)/);
|
|
344
121
|
return match ? match[1] : null;
|
|
@@ -355,7 +132,7 @@ function transformRequestBody(body) {
|
|
|
355
132
|
return body;
|
|
356
133
|
}
|
|
357
134
|
}
|
|
358
|
-
function createAntigravityFetch() {
|
|
135
|
+
function createAntigravityFetch(getAuth, client) {
|
|
359
136
|
let currentEndpointIndex = 0;
|
|
360
137
|
const antigravityFetch = async (input, init) => {
|
|
361
138
|
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
@@ -366,13 +143,29 @@ function createAntigravityFetch() {
|
|
|
366
143
|
if (!action) {
|
|
367
144
|
return fetch(input, init);
|
|
368
145
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
146
|
+
const auth = await getAuth();
|
|
147
|
+
if (!auth || auth.type !== "oauth") {
|
|
148
|
+
throw new Error("No OAuth authentication. Please run: opencode auth login");
|
|
149
|
+
}
|
|
150
|
+
let accessToken = auth.access;
|
|
151
|
+
if (isTokenExpired(auth.expires)) {
|
|
152
|
+
try {
|
|
153
|
+
const refreshed = await refreshAccessToken(auth.refresh);
|
|
154
|
+
accessToken = refreshed.accessToken;
|
|
155
|
+
await client.auth.set({
|
|
156
|
+
path: { id: "google" },
|
|
157
|
+
body: {
|
|
158
|
+
type: "oauth",
|
|
159
|
+
access: refreshed.accessToken,
|
|
160
|
+
expires: Date.now() + refreshed.expiresIn * 1e3,
|
|
161
|
+
refresh: auth.refresh
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
} catch (error) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`Token refresh failed: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
376
169
|
}
|
|
377
170
|
let body = init?.body;
|
|
378
171
|
if (typeof body === "string") {
|
|
@@ -413,21 +206,18 @@ function createAntigravityFetch() {
|
|
|
413
206
|
};
|
|
414
207
|
return antigravityFetch;
|
|
415
208
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
var GeminiOAuthPlugin = async (_input) => {
|
|
209
|
+
var GeminiOAuthPlugin = async (input) => {
|
|
210
|
+
const { client } = input;
|
|
419
211
|
const authHook = {
|
|
420
212
|
provider: "google",
|
|
421
|
-
loader: async (
|
|
422
|
-
const
|
|
423
|
-
if (!
|
|
424
|
-
return {
|
|
425
|
-
apiKey: ""
|
|
426
|
-
};
|
|
213
|
+
loader: async (getAuth) => {
|
|
214
|
+
const auth = await getAuth();
|
|
215
|
+
if (!auth || auth.type !== "oauth") {
|
|
216
|
+
return { apiKey: "" };
|
|
427
217
|
}
|
|
428
218
|
return {
|
|
429
219
|
apiKey: "",
|
|
430
|
-
fetch: createAntigravityFetch()
|
|
220
|
+
fetch: createAntigravityFetch(getAuth, client)
|
|
431
221
|
};
|
|
432
222
|
},
|
|
433
223
|
methods: [
|
|
@@ -435,20 +225,113 @@ var GeminiOAuthPlugin = async (_input) => {
|
|
|
435
225
|
type: "oauth",
|
|
436
226
|
label: "OAuth with Google (Gemini Pro)",
|
|
437
227
|
authorize: async () => {
|
|
438
|
-
const {
|
|
228
|
+
const { verifier, challenge } = generatePKCE();
|
|
229
|
+
const state = generateState();
|
|
230
|
+
const authUrl = buildAuthUrl(state, challenge);
|
|
231
|
+
let server;
|
|
232
|
+
let callbackResolve;
|
|
233
|
+
let callbackReject;
|
|
234
|
+
const callbackPromise = new Promise((resolve, reject) => {
|
|
235
|
+
callbackResolve = resolve;
|
|
236
|
+
callbackReject = reject;
|
|
237
|
+
});
|
|
238
|
+
server = createServer(async (req, res) => {
|
|
239
|
+
try {
|
|
240
|
+
const url = new URL(
|
|
241
|
+
req.url || "/",
|
|
242
|
+
`http://localhost:${OAUTH_CONFIG.port}`
|
|
243
|
+
);
|
|
244
|
+
if (url.pathname === "/oauth-callback") {
|
|
245
|
+
const code = url.searchParams.get("code");
|
|
246
|
+
const returnedState = url.searchParams.get("state");
|
|
247
|
+
const error = url.searchParams.get("error");
|
|
248
|
+
if (error) {
|
|
249
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
250
|
+
res.end(`
|
|
251
|
+
<html>
|
|
252
|
+
<body style="font-family: system-ui; padding: 40px; text-align: center;">
|
|
253
|
+
<h1 style="color: #dc2626;">Authentication Failed</h1>
|
|
254
|
+
<p>Error: ${error}</p>
|
|
255
|
+
<p>You can close this window.</p>
|
|
256
|
+
</body>
|
|
257
|
+
</html>
|
|
258
|
+
`);
|
|
259
|
+
callbackReject(new Error(`OAuth error: ${error}`));
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (!code || returnedState !== state) {
|
|
263
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
264
|
+
res.end(`
|
|
265
|
+
<html>
|
|
266
|
+
<body style="font-family: system-ui; padding: 40px; text-align: center;">
|
|
267
|
+
<h1 style="color: #dc2626;">Invalid Response</h1>
|
|
268
|
+
<p>Missing code or invalid state.</p>
|
|
269
|
+
<p>You can close this window.</p>
|
|
270
|
+
</body>
|
|
271
|
+
</html>
|
|
272
|
+
`);
|
|
273
|
+
callbackReject(new Error("Invalid OAuth response"));
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const tokens = await exchangeCodeForTokens(code, verifier);
|
|
277
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
278
|
+
res.end(`
|
|
279
|
+
<html>
|
|
280
|
+
<body style="font-family: system-ui; padding: 40px; text-align: center;">
|
|
281
|
+
<h1 style="color: #16a34a;">Authentication Successful!</h1>
|
|
282
|
+
<p>You can close this window and return to OpenCode.</p>
|
|
283
|
+
<script>setTimeout(() => window.close(), 2000);</script>
|
|
284
|
+
</body>
|
|
285
|
+
</html>
|
|
286
|
+
`);
|
|
287
|
+
callbackResolve({
|
|
288
|
+
type: "success",
|
|
289
|
+
refresh: tokens.refreshToken,
|
|
290
|
+
access: tokens.accessToken,
|
|
291
|
+
expires: Date.now() + tokens.expiresIn * 1e3
|
|
292
|
+
});
|
|
293
|
+
} else {
|
|
294
|
+
res.writeHead(404);
|
|
295
|
+
res.end("Not found");
|
|
296
|
+
}
|
|
297
|
+
} catch (err) {
|
|
298
|
+
res.writeHead(500, { "Content-Type": "text/html" });
|
|
299
|
+
res.end(`
|
|
300
|
+
<html>
|
|
301
|
+
<body style="font-family: system-ui; padding: 40px; text-align: center;">
|
|
302
|
+
<h1 style="color: #dc2626;">Error</h1>
|
|
303
|
+
<p>${err instanceof Error ? err.message : "Unknown error"}</p>
|
|
304
|
+
<p>You can close this window.</p>
|
|
305
|
+
</body>
|
|
306
|
+
</html>
|
|
307
|
+
`);
|
|
308
|
+
callbackReject(
|
|
309
|
+
err instanceof Error ? err : new Error("Unknown error")
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
await new Promise((resolve, reject) => {
|
|
314
|
+
server.listen(OAUTH_CONFIG.port, () => resolve());
|
|
315
|
+
server.on("error", reject);
|
|
316
|
+
});
|
|
317
|
+
try {
|
|
318
|
+
const open = (await import("open")).default;
|
|
319
|
+
await open(authUrl);
|
|
320
|
+
} catch {
|
|
321
|
+
}
|
|
322
|
+
const timeout = setTimeout(() => {
|
|
323
|
+
server.close();
|
|
324
|
+
callbackReject(new Error("OAuth flow timed out"));
|
|
325
|
+
}, 5 * 60 * 1e3);
|
|
439
326
|
return {
|
|
440
327
|
url: authUrl,
|
|
441
328
|
instructions: "Opening browser for Google authentication. Please sign in with your Google AI Pro account.",
|
|
442
329
|
method: "auto",
|
|
443
330
|
callback: async () => {
|
|
444
331
|
try {
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
refresh: account.refreshToken,
|
|
449
|
-
access: account.accessToken || "",
|
|
450
|
-
expires: account.expiresAt || Date.now() + 3600 * 1e3
|
|
451
|
-
};
|
|
332
|
+
const result = await callbackPromise;
|
|
333
|
+
clearTimeout(timeout);
|
|
334
|
+
return result;
|
|
452
335
|
} catch (error) {
|
|
453
336
|
console.error("OAuth failed:", error);
|
|
454
337
|
return { type: "failed" };
|