opencode-gemini-auth 1.3.6 → 1.3.7

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 CHANGED
@@ -37,7 +37,7 @@ Add the plugin to your Opencode configuration file
37
37
  - A browser window will open for you to approve the access.
38
38
  - The plugin spins up a temporary local server to capture the callback.
39
39
  - If the local server fails (e.g., port in use or headless environment),
40
- you can manually copy/paste the callback URL as instructed.
40
+ you can manually paste the callback URL or just the authorization code.
41
41
 
42
42
  Once authenticated, Opencode will use your Google account for Gemini requests.
43
43
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opencode-gemini-auth",
3
3
  "module": "index.ts",
4
- "version": "1.3.6",
4
+ "version": "1.3.7",
5
5
  "author": "jenslys",
6
6
  "repository": "https://github.com/jenslys/opencode-gemini-auth",
7
7
  "files": [
@@ -11,7 +11,7 @@
11
11
  "license": "MIT",
12
12
  "type": "module",
13
13
  "devDependencies": {
14
- "@opencode-ai/plugin": "^0.15.31",
14
+ "@opencode-ai/plugin": "^1.0.203",
15
15
  "@types/bun": "latest"
16
16
  },
17
17
  "peerDependencies": {
@@ -107,53 +107,24 @@ export async function exchangeGemini(
107
107
  try {
108
108
  const { verifier } = decodeState(state);
109
109
 
110
- const tokenResponse = await fetch("https://oauth2.googleapis.com/token", {
111
- method: "POST",
112
- headers: {
113
- "Content-Type": "application/x-www-form-urlencoded",
114
- },
115
- body: new URLSearchParams({
116
- client_id: GEMINI_CLIENT_ID,
117
- client_secret: GEMINI_CLIENT_SECRET,
118
- code,
119
- grant_type: "authorization_code",
120
- redirect_uri: GEMINI_REDIRECT_URI,
121
- code_verifier: verifier,
122
- }),
123
- });
124
-
125
- if (!tokenResponse.ok) {
126
- const errorText = await tokenResponse.text();
127
- return { type: "failed", error: errorText };
128
- }
129
-
130
- const tokenPayload = (await tokenResponse.json()) as GeminiTokenResponse;
131
-
132
- const userInfoResponse = await fetch(
133
- "https://www.googleapis.com/oauth2/v1/userinfo?alt=json",
134
- {
135
- headers: {
136
- Authorization: `Bearer ${tokenPayload.access_token}`,
137
- },
138
- },
139
- );
140
-
141
- const userInfo = userInfoResponse.ok
142
- ? ((await userInfoResponse.json()) as GeminiUserInfo)
143
- : {};
144
-
145
- const refreshToken = tokenPayload.refresh_token;
146
- if (!refreshToken) {
147
- return { type: "failed", error: "Missing refresh token in response" };
148
- }
149
-
110
+ return await exchangeGeminiWithVerifierInternal(code, verifier);
111
+ } catch (error) {
150
112
  return {
151
- type: "success",
152
- refresh: refreshToken,
153
- access: tokenPayload.access_token,
154
- expires: Date.now() + tokenPayload.expires_in * 1000,
155
- email: userInfo.email,
113
+ type: "failed",
114
+ error: error instanceof Error ? error.message : "Unknown error",
156
115
  };
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Exchange an authorization code using a known PKCE verifier.
121
+ */
122
+ export async function exchangeGeminiWithVerifier(
123
+ code: string,
124
+ verifier: string,
125
+ ): Promise<GeminiTokenExchangeResult> {
126
+ try {
127
+ return await exchangeGeminiWithVerifierInternal(code, verifier);
157
128
  } catch (error) {
158
129
  return {
159
130
  type: "failed",
@@ -161,3 +132,56 @@ export async function exchangeGemini(
161
132
  };
162
133
  }
163
134
  }
135
+
136
+ async function exchangeGeminiWithVerifierInternal(
137
+ code: string,
138
+ verifier: string,
139
+ ): Promise<GeminiTokenExchangeResult> {
140
+ const tokenResponse = await fetch("https://oauth2.googleapis.com/token", {
141
+ method: "POST",
142
+ headers: {
143
+ "Content-Type": "application/x-www-form-urlencoded",
144
+ },
145
+ body: new URLSearchParams({
146
+ client_id: GEMINI_CLIENT_ID,
147
+ client_secret: GEMINI_CLIENT_SECRET,
148
+ code,
149
+ grant_type: "authorization_code",
150
+ redirect_uri: GEMINI_REDIRECT_URI,
151
+ code_verifier: verifier,
152
+ }),
153
+ });
154
+
155
+ if (!tokenResponse.ok) {
156
+ const errorText = await tokenResponse.text();
157
+ return { type: "failed", error: errorText };
158
+ }
159
+
160
+ const tokenPayload = (await tokenResponse.json()) as GeminiTokenResponse;
161
+
162
+ const userInfoResponse = await fetch(
163
+ "https://www.googleapis.com/oauth2/v1/userinfo?alt=json",
164
+ {
165
+ headers: {
166
+ Authorization: `Bearer ${tokenPayload.access_token}`,
167
+ },
168
+ },
169
+ );
170
+
171
+ const userInfo = userInfoResponse.ok
172
+ ? ((await userInfoResponse.json()) as GeminiUserInfo)
173
+ : {};
174
+
175
+ const refreshToken = tokenPayload.refresh_token;
176
+ if (!refreshToken) {
177
+ return { type: "failed", error: "Missing refresh token in response" };
178
+ }
179
+
180
+ return {
181
+ type: "success",
182
+ refresh: refreshToken,
183
+ access: tokenPayload.access_token,
184
+ expires: Date.now() + tokenPayload.expires_in * 1000,
185
+ email: userInfo.email,
186
+ };
187
+ }
package/src/plugin.ts CHANGED
@@ -1,7 +1,11 @@
1
1
  import { spawn } from "node:child_process";
2
2
 
3
3
  import { GEMINI_PROVIDER_ID, GEMINI_REDIRECT_URI } from "./constants";
4
- import { authorizeGemini, exchangeGemini } from "./gemini/oauth";
4
+ import {
5
+ authorizeGemini,
6
+ exchangeGemini,
7
+ exchangeGeminiWithVerifier,
8
+ } from "./gemini/oauth";
5
9
  import type { GeminiTokenExchangeResult } from "./gemini/oauth";
6
10
  import { accessTokenExpired, isOAuthAuth } from "./plugin/auth";
7
11
  import { ensureProjectContext } from "./plugin/project";
@@ -146,16 +150,18 @@ export const GeminiCLIOAuthPlugin = async (
146
150
  } catch (error) {
147
151
  if (error instanceof Error) {
148
152
  console.log(
149
- `Warning: Couldn't start the local callback listener (${error.message}). You'll need to paste the callback URL.`,
153
+ `Warning: Couldn't start the local callback listener (${error.message}). You'll need to paste the callback URL or authorization code.`,
150
154
  );
151
155
  } else {
152
156
  console.log(
153
- "Warning: Couldn't start the local callback listener. You'll need to paste the callback URL.",
157
+ "Warning: Couldn't start the local callback listener. You'll need to paste the callback URL or authorization code.",
154
158
  );
155
159
  }
156
160
  }
157
161
  } else {
158
- console.log("Headless environment detected. You'll need to paste the callback URL.");
162
+ console.log(
163
+ "Headless environment detected. You'll need to paste the callback URL or authorization code.",
164
+ );
159
165
  }
160
166
 
161
167
  const authorization = await authorizeGemini();
@@ -201,22 +207,24 @@ export const GeminiCLIOAuthPlugin = async (
201
207
  return {
202
208
  url: authorization.url,
203
209
  instructions:
204
- "Complete OAuth in your browser, then paste the full redirected URL (e.g., http://localhost:8085/oauth2callback?code=...&state=...)",
210
+ "Complete OAuth in your browser, then paste the full redirected URL (e.g., http://localhost:8085/oauth2callback?code=...&state=...) or just the authorization code.",
205
211
  method: "code",
206
212
  callback: async (callbackUrl: string): Promise<GeminiTokenExchangeResult> => {
207
213
  try {
208
- const url = new URL(callbackUrl);
209
- const code = url.searchParams.get("code");
210
- const state = url.searchParams.get("state");
214
+ const { code, state } = parseOAuthCallbackInput(callbackUrl);
211
215
 
212
- if (!code || !state) {
216
+ if (!code) {
213
217
  return {
214
218
  type: "failed",
215
- error: "Missing code or state in callback URL",
219
+ error: "Missing authorization code in callback input",
216
220
  };
217
221
  }
218
222
 
219
- return exchangeGemini(code, state);
223
+ if (state) {
224
+ return exchangeGemini(code, state);
225
+ }
226
+
227
+ return exchangeGeminiWithVerifier(code, authorization.verifier);
220
228
  } catch (error) {
221
229
  return {
222
230
  type: "failed",
@@ -249,6 +257,37 @@ function toUrlString(value: RequestInfo): string {
249
257
  return value.toString();
250
258
  }
251
259
 
260
+ function parseOAuthCallbackInput(input: string): { code?: string; state?: string } {
261
+ const trimmed = input.trim();
262
+ if (!trimmed) {
263
+ return {};
264
+ }
265
+
266
+ if (/^https?:\/\//i.test(trimmed)) {
267
+ try {
268
+ const url = new URL(trimmed);
269
+ return {
270
+ code: url.searchParams.get("code") || undefined,
271
+ state: url.searchParams.get("state") || undefined,
272
+ };
273
+ } catch {
274
+ return {};
275
+ }
276
+ }
277
+
278
+ const candidate = trimmed.startsWith("?") ? trimmed.slice(1) : trimmed;
279
+ if (candidate.includes("=")) {
280
+ const params = new URLSearchParams(candidate);
281
+ const code = params.get("code") || undefined;
282
+ const state = params.get("state") || undefined;
283
+ if (code || state) {
284
+ return { code, state };
285
+ }
286
+ }
287
+
288
+ return { code: trimmed };
289
+ }
290
+
252
291
  function openBrowserUrl(url: string): void {
253
292
  try {
254
293
  // Best-effort: don't block auth flow if spawning fails.