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 +1 -1
- package/package.json +2 -2
- package/src/gemini/oauth.ts +69 -45
- package/src/plugin.ts +50 -11
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
|
|
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.
|
|
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.
|
|
14
|
+
"@opencode-ai/plugin": "^1.0.203",
|
|
15
15
|
"@types/bun": "latest"
|
|
16
16
|
},
|
|
17
17
|
"peerDependencies": {
|
package/src/gemini/oauth.ts
CHANGED
|
@@ -107,53 +107,24 @@ export async function exchangeGemini(
|
|
|
107
107
|
try {
|
|
108
108
|
const { verifier } = decodeState(state);
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
|
|
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: "
|
|
152
|
-
|
|
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 {
|
|
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(
|
|
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
|
|
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
|
|
216
|
+
if (!code) {
|
|
213
217
|
return {
|
|
214
218
|
type: "failed",
|
|
215
|
-
error: "Missing code
|
|
219
|
+
error: "Missing authorization code in callback input",
|
|
216
220
|
};
|
|
217
221
|
}
|
|
218
222
|
|
|
219
|
-
|
|
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.
|