opencode-gemini-oauth 1.0.0 → 1.1.1

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 (2) hide show
  1. package/dist/index.js +154 -271
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
- // src/oauth.ts
1
+ // src/index.ts
2
2
  import { createServer } from "http";
3
3
  import { randomBytes, createHash } from "crypto";
4
- import { URL as URL2, URLSearchParams as URLSearchParams2 } from "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/storage.ts
34
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
35
- import { join } from "path";
36
- import { homedir } from "os";
37
- function getStoragePath() {
38
- const configDir = process.env.APPDATA || (process.platform === "darwin" ? join(homedir(), "Library", "Application Support") : join(homedir(), ".config"));
39
- const opencodeDir = join(configDir, "opencode");
40
- if (!existsSync(opencodeDir)) {
41
- mkdirSync(opencodeDir, { recursive: true });
42
- }
43
- return join(opencodeDir, STORAGE_FILE);
44
- }
45
- function loadStorage() {
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 URLSearchParams2({
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 URLSearchParams2({
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 getUserInfo(accessToken) {
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: account.refreshToken,
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
- expiresAt
113
+ expiresIn: data.expires_in
315
114
  };
316
115
  }
317
- async function getValidAccessToken() {
318
- const account = getActiveAccount();
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
- let accessToken;
370
- try {
371
- accessToken = await getValidAccessToken();
372
- } catch (error) {
373
- throw new Error(
374
- `Failed to get access token: ${error instanceof Error ? error.message : "Unknown error"}`
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
- // src/index.ts
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 (_auth, _provider) => {
422
- const account = getActiveAccount();
423
- if (!account) {
424
- return {
425
- apiKey: ""
426
- };
213
+ loader: async (getAuth) => {
214
+ const auth = await getAuth();
215
+ if (!auth || auth.type !== "oauth") {
216
+ return {};
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 { authUrl, waitForCallback, server } = await startOAuthFlow();
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 account = await waitForCallback();
446
- return {
447
- type: "success",
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" };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-gemini-oauth",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "OpenCode plugin for Google Gemini OAuth authentication (Antigravity)",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",