opencode-gemini-auth 1.1.3 → 1.1.5

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opencode-gemini-auth",
3
3
  "module": "index.ts",
4
- "version": "1.1.3",
4
+ "version": "1.1.5",
5
5
  "author": "jenslys",
6
6
  "repository": "https://github.com/jenslys/opencode-gemini-auth",
7
7
  "files": [
@@ -162,6 +162,38 @@ export async function transformGeminiResponse(
162
162
  try {
163
163
  const text = await response.text();
164
164
  const headers = new Headers(response.headers);
165
+
166
+ // Extract retry timing from Google's structured error response
167
+ // Google returns retry timing in error.details[].retryDelay: "55.846891726s"
168
+ if (!response.ok && text) {
169
+ try {
170
+ const errorBody = JSON.parse(text);
171
+ if (errorBody?.error?.details && Array.isArray(errorBody.error.details)) {
172
+ // Look for RetryInfo type
173
+ const retryInfo = errorBody.error.details.find(
174
+ (detail: any) => detail['@type'] === 'type.googleapis.com/google.rpc.RetryInfo'
175
+ );
176
+
177
+ if (retryInfo?.retryDelay) {
178
+ // Parse "55.846891726s" format
179
+ const match = retryInfo.retryDelay.match(/^([\d.]+)s$/);
180
+ if (match && match[1]) {
181
+ const retrySeconds = parseFloat(match[1]);
182
+ if (!isNaN(retrySeconds) && retrySeconds > 0) {
183
+ // Add both formats for compatibility
184
+ const retryAfterSec = Math.ceil(retrySeconds).toString();
185
+ const retryAfterMs = Math.ceil(retrySeconds * 1000).toString();
186
+ headers.set('Retry-After', retryAfterSec);
187
+ headers.set('retry-after-ms', retryAfterMs);
188
+ }
189
+ }
190
+ }
191
+ }
192
+ } catch (parseError) {
193
+ // If JSON parsing fails, continue without retry headers
194
+ }
195
+ }
196
+
165
197
  const init = {
166
198
  status: response.status,
167
199
  statusText: response.statusText,
@@ -139,14 +139,13 @@ export async function refreshAccessToken(
139
139
  storeCachedAuth(updatedAuth);
140
140
  invalidateProjectContextCache(auth.refresh);
141
141
 
142
- const refreshTokenRotated =
143
- typeof payload.refresh_token === "string" && payload.refresh_token !== parts.refreshToken;
144
-
145
- if (refreshTokenRotated) {
142
+ try {
146
143
  await client.auth.set({
147
144
  path: { id: GEMINI_PROVIDER_ID },
148
145
  body: updatedAuth,
149
146
  });
147
+ } catch (storeError) {
148
+ console.error("Failed to persist refreshed Gemini OAuth credentials:", storeError);
150
149
  }
151
150
 
152
151
  return updatedAuth;
package/src/plugin.ts CHANGED
@@ -115,29 +115,47 @@ export const GeminiCLIOAuthPlugin = async (
115
115
  authorize: async () => {
116
116
  console.log("\n=== Google Gemini OAuth Setup ===");
117
117
 
118
+ // Detect headless/SSH environment
119
+ const isHeadless = !!(
120
+ process.env.SSH_CONNECTION ||
121
+ process.env.SSH_CLIENT ||
122
+ process.env.SSH_TTY ||
123
+ process.env.OPENCODE_HEADLESS
124
+ );
125
+
118
126
  let listener: OAuthListener | null = null;
119
- try {
120
- listener = await startOAuthListener();
121
- const { host } = new URL(GEMINI_REDIRECT_URI);
122
- console.log("1. You'll be asked to sign in to your Google account and grant permission.");
123
- console.log(
124
- `2. We'll automatically capture the browser redirect on http://${host}. No need to paste anything back here.`,
125
- );
126
- console.log("3. Once you see the 'Authentication complete' page in your browser, return to this terminal.");
127
- } catch (error) {
127
+ if (!isHeadless) {
128
+ try {
129
+ listener = await startOAuthListener();
130
+ const { host } = new URL(GEMINI_REDIRECT_URI);
131
+ console.log("1. You'll be asked to sign in to your Google account and grant permission.");
132
+ console.log(
133
+ `2. We'll automatically capture the browser redirect on http://${host}. No need to paste anything back here.`,
134
+ );
135
+ console.log("3. Once you see the 'Authentication complete' page in your browser, return to this terminal.");
136
+ } catch (error) {
137
+ console.log("1. You'll be asked to sign in to your Google account and grant permission.");
138
+ console.log("2. After you approve, the browser will try to redirect to a 'localhost' page.");
139
+ console.log(
140
+ "3. This page will show an error like 'This site can't be reached'. This is perfectly normal and means it worked!",
141
+ );
142
+ console.log(
143
+ "4. Once you see that error, copy the entire URL from the address bar, paste it back here, and press Enter.",
144
+ );
145
+ if (error instanceof Error) {
146
+ console.log(`\nWarning: Couldn't start the local callback listener (${error.message}). Falling back to manual copy/paste.`);
147
+ } else {
148
+ console.log("\nWarning: Couldn't start the local callback listener. Falling back to manual copy/paste.");
149
+ }
150
+ }
151
+ } else {
152
+ console.log("Headless environment detected. Using manual OAuth flow.");
128
153
  console.log("1. You'll be asked to sign in to your Google account and grant permission.");
129
- console.log("2. After you approve, the browser will try to redirect to a 'localhost' page.");
154
+ console.log("2. After you approve, the browser will redirect to a 'localhost' URL.");
130
155
  console.log(
131
- "3. This page will show an error like 'This site can’t be reached'. This is perfectly normal and means it worked!",
156
+ "3. Copy the ENTIRE URL from your browser's address bar (it will look like: http://localhost:8085/oauth2callback?code=...&state=...)",
132
157
  );
133
- console.log(
134
- "4. Once you see that error, copy the entire URL from the address bar, paste it back here, and press Enter.",
135
- );
136
- if (error instanceof Error) {
137
- console.log(`\nWarning: Couldn't start the local callback listener (${error.message}). Falling back to manual copy/paste.`);
138
- } else {
139
- console.log("\nWarning: Couldn't start the local callback listener. Falling back to manual copy/paste.");
140
- }
158
+ console.log("4. Paste the URL back here and press Enter.");
141
159
  }
142
160
  console.log("\n");
143
161