@oh-my-pi/pi-ai 6.7.67 → 6.8.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/README.md +31 -31
- package/package.json +2 -1
- package/src/cli.ts +114 -52
- package/src/providers/anthropic.ts +215 -166
- package/src/providers/google-gemini-cli.ts +4 -20
- package/src/providers/openai-codex/response-handler.ts +4 -43
- package/src/providers/openai-codex-responses.ts +2 -2
- package/src/storage.ts +185 -0
- package/src/utils/event-stream.ts +3 -3
- package/src/utils/oauth/anthropic.ts +70 -88
- package/src/utils/oauth/callback-server.ts +245 -0
- package/src/utils/oauth/cursor.ts +1 -5
- package/src/utils/oauth/github-copilot.ts +1 -23
- package/src/utils/oauth/google-antigravity.ts +73 -263
- package/src/utils/oauth/google-gemini-cli.ts +73 -281
- package/src/utils/oauth/oauth.html +199 -0
- package/src/utils/oauth/openai-codex.ts +131 -318
- package/src/utils/oauth/pkce.ts +1 -1
- package/src/utils/oauth/types.ts +8 -0
|
@@ -1,21 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Gemini CLI OAuth flow (Google Cloud Code Assist)
|
|
3
3
|
* Standard Gemini models only (gemini-2.0-flash, gemini-2.5-*)
|
|
4
|
-
*
|
|
5
|
-
* NOTE: This module uses Node.js http.createServer for the OAuth callback.
|
|
6
|
-
* It is only intended for CLI use, not browser environments.
|
|
7
4
|
*/
|
|
8
5
|
|
|
9
|
-
import
|
|
6
|
+
import { OAuthCallbackFlow } from "./callback-server";
|
|
10
7
|
import { generatePKCE } from "./pkce";
|
|
11
|
-
import type { OAuthCredentials } from "./types";
|
|
8
|
+
import type { OAuthController, OAuthCredentials } from "./types";
|
|
12
9
|
|
|
13
10
|
const decode = (s: string) => atob(s);
|
|
14
11
|
const CLIENT_ID = decode(
|
|
15
12
|
"NjgxMjU1ODA5Mzk1LW9vOGZ0Mm9wcmRybnA5ZTNhcWY2YXYzaG1kaWIxMzVqLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29t",
|
|
16
13
|
);
|
|
17
14
|
const CLIENT_SECRET = decode("R09DU1BYLTR1SGdNUG0tMW83U2stZ2VWNkN1NWNsWEZzeGw=");
|
|
18
|
-
const
|
|
15
|
+
const CALLBACK_PORT = 8085;
|
|
16
|
+
const CALLBACK_PATH = "/oauth2callback";
|
|
19
17
|
const SCOPES = [
|
|
20
18
|
"https://www.googleapis.com/auth/cloud-platform",
|
|
21
19
|
"https://www.googleapis.com/auth/userinfo.email",
|
|
@@ -25,106 +23,12 @@ const AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
|
25
23
|
const TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
26
24
|
const CODE_ASSIST_ENDPOINT = "https://cloudcode-pa.googleapis.com";
|
|
27
25
|
|
|
28
|
-
type CallbackServerInfo = {
|
|
29
|
-
server: Server;
|
|
30
|
-
cancelWait: () => void;
|
|
31
|
-
waitForCode: () => Promise<{ code: string; state: string } | null>;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Start a local HTTP server to receive the OAuth callback
|
|
36
|
-
*/
|
|
37
|
-
async function startCallbackServer(): Promise<CallbackServerInfo> {
|
|
38
|
-
const { createServer } = await import("http");
|
|
39
|
-
|
|
40
|
-
return new Promise((resolve, reject) => {
|
|
41
|
-
let result: { code: string; state: string } | null = null;
|
|
42
|
-
let cancelled = false;
|
|
43
|
-
|
|
44
|
-
const server = createServer((req, res) => {
|
|
45
|
-
const url = new URL(req.url || "", `http://localhost:8085`);
|
|
46
|
-
|
|
47
|
-
if (url.pathname === "/oauth2callback") {
|
|
48
|
-
const code = url.searchParams.get("code");
|
|
49
|
-
const state = url.searchParams.get("state");
|
|
50
|
-
const error = url.searchParams.get("error");
|
|
51
|
-
|
|
52
|
-
if (error) {
|
|
53
|
-
res.writeHead(400, { "Content-Type": "text/html" });
|
|
54
|
-
res.end(
|
|
55
|
-
`<html><body><h1>Authentication Failed</h1><p>Error: ${error}</p><p>You can close this window.</p></body></html>`,
|
|
56
|
-
);
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (code && state) {
|
|
61
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
62
|
-
res.end(
|
|
63
|
-
`<html><body><h1>Authentication Successful</h1><p>You can close this window and return to the terminal.</p></body></html>`,
|
|
64
|
-
);
|
|
65
|
-
result = { code, state };
|
|
66
|
-
} else {
|
|
67
|
-
res.writeHead(400, { "Content-Type": "text/html" });
|
|
68
|
-
res.end(
|
|
69
|
-
`<html><body><h1>Authentication Failed</h1><p>Missing code or state parameter.</p></body></html>`,
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
} else {
|
|
73
|
-
res.writeHead(404);
|
|
74
|
-
res.end();
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
server.on("error", (err) => {
|
|
79
|
-
reject(err);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
server.listen(8085, "127.0.0.1", () => {
|
|
83
|
-
resolve({
|
|
84
|
-
server,
|
|
85
|
-
cancelWait: () => {
|
|
86
|
-
cancelled = true;
|
|
87
|
-
},
|
|
88
|
-
waitForCode: async () => {
|
|
89
|
-
const sleep = () => new Promise((r) => setTimeout(r, 100));
|
|
90
|
-
while (!result && !cancelled) {
|
|
91
|
-
await sleep();
|
|
92
|
-
}
|
|
93
|
-
return result;
|
|
94
|
-
},
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Parse redirect URL to extract code and state
|
|
102
|
-
*/
|
|
103
|
-
function parseRedirectUrl(input: string): { code?: string; state?: string } {
|
|
104
|
-
const value = input.trim();
|
|
105
|
-
if (!value) return {};
|
|
106
|
-
|
|
107
|
-
try {
|
|
108
|
-
const url = new URL(value);
|
|
109
|
-
return {
|
|
110
|
-
code: url.searchParams.get("code") ?? undefined,
|
|
111
|
-
state: url.searchParams.get("state") ?? undefined,
|
|
112
|
-
};
|
|
113
|
-
} catch {
|
|
114
|
-
// Not a URL, return empty
|
|
115
|
-
return {};
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
26
|
interface LoadCodeAssistPayload {
|
|
120
27
|
cloudaicompanionProject?: string;
|
|
121
28
|
currentTier?: { id?: string };
|
|
122
29
|
allowedTiers?: Array<{ id?: string; isDefault?: boolean }>;
|
|
123
30
|
}
|
|
124
31
|
|
|
125
|
-
/**
|
|
126
|
-
* Long-running operation response from onboardUser
|
|
127
|
-
*/
|
|
128
32
|
interface LongRunningOperationResponse {
|
|
129
33
|
name?: string;
|
|
130
34
|
done?: boolean;
|
|
@@ -133,7 +37,6 @@ interface LongRunningOperationResponse {
|
|
|
133
37
|
};
|
|
134
38
|
}
|
|
135
39
|
|
|
136
|
-
// Tier IDs as used by the Cloud Code API
|
|
137
40
|
const TIER_FREE = "free-tier";
|
|
138
41
|
const TIER_LEGACY = "legacy-tier";
|
|
139
42
|
const TIER_STANDARD = "standard-tier";
|
|
@@ -144,16 +47,6 @@ interface GoogleRpcErrorResponse {
|
|
|
144
47
|
};
|
|
145
48
|
}
|
|
146
49
|
|
|
147
|
-
/**
|
|
148
|
-
* Wait helper for onboarding retries
|
|
149
|
-
*/
|
|
150
|
-
function wait(ms: number): Promise<void> {
|
|
151
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Get default tier from allowed tiers
|
|
156
|
-
*/
|
|
157
50
|
function getDefaultTier(allowedTiers?: Array<{ id?: string; isDefault?: boolean }>): { id?: string } {
|
|
158
51
|
if (!allowedTiers || allowedTiers.length === 0) return { id: TIER_LEGACY };
|
|
159
52
|
const defaultTier = allowedTiers.find((t) => t.isDefault);
|
|
@@ -168,9 +61,6 @@ function isVpcScAffectedUser(payload: unknown): boolean {
|
|
|
168
61
|
return error.details.some((detail) => detail.reason === "SECURITY_POLICY_VIOLATED");
|
|
169
62
|
}
|
|
170
63
|
|
|
171
|
-
/**
|
|
172
|
-
* Poll a long-running operation until completion
|
|
173
|
-
*/
|
|
174
64
|
async function pollOperation(
|
|
175
65
|
operationName: string,
|
|
176
66
|
headers: Record<string, string>,
|
|
@@ -180,7 +70,7 @@ async function pollOperation(
|
|
|
180
70
|
while (true) {
|
|
181
71
|
if (attempt > 0) {
|
|
182
72
|
onProgress?.(`Waiting for project provisioning (attempt ${attempt + 1})...`);
|
|
183
|
-
await
|
|
73
|
+
await Bun.sleep(5000);
|
|
184
74
|
}
|
|
185
75
|
|
|
186
76
|
const response = await fetch(`${CODE_ASSIST_ENDPOINT}/v1internal/${operationName}`, {
|
|
@@ -201,11 +91,7 @@ async function pollOperation(
|
|
|
201
91
|
}
|
|
202
92
|
}
|
|
203
93
|
|
|
204
|
-
/**
|
|
205
|
-
* Discover or provision a Google Cloud project for the user
|
|
206
|
-
*/
|
|
207
94
|
async function discoverProject(accessToken: string, onProgress?: (message: string) => void): Promise<string> {
|
|
208
|
-
// Check for user-provided project ID via environment variable
|
|
209
95
|
const envProjectId = process.env.GOOGLE_CLOUD_PROJECT || process.env.GOOGLE_CLOUD_PROJECT_ID;
|
|
210
96
|
|
|
211
97
|
const headers = {
|
|
@@ -215,7 +101,6 @@ async function discoverProject(accessToken: string, onProgress?: (message: strin
|
|
|
215
101
|
"X-Goog-Api-Client": "gl-node/22.17.0",
|
|
216
102
|
};
|
|
217
103
|
|
|
218
|
-
// Try to load existing project via loadCodeAssist
|
|
219
104
|
onProgress?.("Checking for existing Cloud Code Assist project...");
|
|
220
105
|
const loadResponse = await fetch(`${CODE_ASSIST_ENDPOINT}/v1internal:loadCodeAssist`, {
|
|
221
106
|
method: "POST",
|
|
@@ -251,12 +136,10 @@ async function discoverProject(accessToken: string, onProgress?: (message: strin
|
|
|
251
136
|
data = (await loadResponse.json()) as LoadCodeAssistPayload;
|
|
252
137
|
}
|
|
253
138
|
|
|
254
|
-
// If user already has a current tier and project, use it
|
|
255
139
|
if (data.currentTier) {
|
|
256
140
|
if (data.cloudaicompanionProject) {
|
|
257
141
|
return data.cloudaicompanionProject;
|
|
258
142
|
}
|
|
259
|
-
// User has a tier but no managed project - they need to provide one via env var
|
|
260
143
|
if (envProjectId) {
|
|
261
144
|
return envProjectId;
|
|
262
145
|
}
|
|
@@ -266,7 +149,6 @@ async function discoverProject(accessToken: string, onProgress?: (message: strin
|
|
|
266
149
|
);
|
|
267
150
|
}
|
|
268
151
|
|
|
269
|
-
// User needs to be onboarded - get the default tier
|
|
270
152
|
const tier = getDefaultTier(data.allowedTiers);
|
|
271
153
|
const tierId = tier?.id ?? TIER_FREE;
|
|
272
154
|
|
|
@@ -279,8 +161,6 @@ async function discoverProject(accessToken: string, onProgress?: (message: strin
|
|
|
279
161
|
|
|
280
162
|
onProgress?.("Provisioning Cloud Code Assist project (this may take a moment)...");
|
|
281
163
|
|
|
282
|
-
// Build onboard request - for free tier, don't include project ID (Google provisions one)
|
|
283
|
-
// For other tiers, include the user's project ID if available
|
|
284
164
|
const onboardBody: Record<string, unknown> = {
|
|
285
165
|
tierId,
|
|
286
166
|
metadata: {
|
|
@@ -295,7 +175,6 @@ async function discoverProject(accessToken: string, onProgress?: (message: strin
|
|
|
295
175
|
(onboardBody.metadata as Record<string, unknown>).duetProject = envProjectId;
|
|
296
176
|
}
|
|
297
177
|
|
|
298
|
-
// Start onboarding - this returns a long-running operation
|
|
299
178
|
const onboardResponse = await fetch(`${CODE_ASSIST_ENDPOINT}/v1internal:onboardUser`, {
|
|
300
179
|
method: "POST",
|
|
301
180
|
headers,
|
|
@@ -309,18 +188,15 @@ async function discoverProject(accessToken: string, onProgress?: (message: strin
|
|
|
309
188
|
|
|
310
189
|
let lroData = (await onboardResponse.json()) as LongRunningOperationResponse;
|
|
311
190
|
|
|
312
|
-
// If the operation isn't done yet, poll until completion
|
|
313
191
|
if (!lroData.done && lroData.name) {
|
|
314
192
|
lroData = await pollOperation(lroData.name, headers, onProgress);
|
|
315
193
|
}
|
|
316
194
|
|
|
317
|
-
// Try to get project ID from the response
|
|
318
195
|
const projectId = lroData.response?.cloudaicompanionProject?.id;
|
|
319
196
|
if (projectId) {
|
|
320
197
|
return projectId;
|
|
321
198
|
}
|
|
322
199
|
|
|
323
|
-
// If no project ID from onboarding, fall back to env var
|
|
324
200
|
if (envProjectId) {
|
|
325
201
|
return envProjectId;
|
|
326
202
|
}
|
|
@@ -332,9 +208,6 @@ async function discoverProject(accessToken: string, onProgress?: (message: strin
|
|
|
332
208
|
);
|
|
333
209
|
}
|
|
334
210
|
|
|
335
|
-
/**
|
|
336
|
-
* Get user email from the access token
|
|
337
|
-
*/
|
|
338
211
|
async function getUserEmail(accessToken: string): Promise<string | undefined> {
|
|
339
212
|
try {
|
|
340
213
|
const response = await fetch("https://www.googleapis.com/oauth2/v1/userinfo?alt=json", {
|
|
@@ -353,165 +226,51 @@ async function getUserEmail(accessToken: string): Promise<string | undefined> {
|
|
|
353
226
|
return undefined;
|
|
354
227
|
}
|
|
355
228
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
export async function refreshGoogleCloudToken(refreshToken: string, projectId: string): Promise<OAuthCredentials> {
|
|
360
|
-
const response = await fetch(TOKEN_URL, {
|
|
361
|
-
method: "POST",
|
|
362
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
363
|
-
body: new URLSearchParams({
|
|
364
|
-
client_id: CLIENT_ID,
|
|
365
|
-
client_secret: CLIENT_SECRET,
|
|
366
|
-
refresh_token: refreshToken,
|
|
367
|
-
grant_type: "refresh_token",
|
|
368
|
-
}),
|
|
369
|
-
});
|
|
229
|
+
class GeminiCliOAuthFlow extends OAuthCallbackFlow {
|
|
230
|
+
private verifier: string = "";
|
|
231
|
+
private challenge: string = "";
|
|
370
232
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
throw new Error(`Google Cloud token refresh failed: ${error}`);
|
|
233
|
+
constructor(ctrl: OAuthController) {
|
|
234
|
+
super(ctrl, CALLBACK_PORT, CALLBACK_PATH);
|
|
374
235
|
}
|
|
375
236
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
refresh: data.refresh_token || refreshToken,
|
|
384
|
-
access: data.access_token,
|
|
385
|
-
expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,
|
|
386
|
-
projectId,
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
/**
|
|
391
|
-
* Login with Gemini CLI (Google Cloud Code Assist) OAuth
|
|
392
|
-
*
|
|
393
|
-
* @param onAuth - Callback with URL and optional instructions
|
|
394
|
-
* @param onProgress - Optional progress callback
|
|
395
|
-
* @param onManualCodeInput - Optional promise that resolves with user-pasted redirect URL.
|
|
396
|
-
* Races with browser callback - whichever completes first wins.
|
|
397
|
-
*/
|
|
398
|
-
export async function loginGeminiCli(
|
|
399
|
-
onAuth: (info: { url: string; instructions?: string }) => void,
|
|
400
|
-
onProgress?: (message: string) => void,
|
|
401
|
-
onManualCodeInput?: () => Promise<string>,
|
|
402
|
-
): Promise<OAuthCredentials> {
|
|
403
|
-
const { verifier, challenge } = await generatePKCE();
|
|
404
|
-
|
|
405
|
-
// Start local server for callback
|
|
406
|
-
onProgress?.("Starting local server for OAuth callback...");
|
|
407
|
-
const server = await startCallbackServer();
|
|
237
|
+
protected async generateAuthUrl(
|
|
238
|
+
state: string,
|
|
239
|
+
redirectUri: string,
|
|
240
|
+
): Promise<{ url: string; instructions?: string }> {
|
|
241
|
+
const pkce = await generatePKCE();
|
|
242
|
+
this.verifier = pkce.verifier;
|
|
243
|
+
this.challenge = pkce.challenge;
|
|
408
244
|
|
|
409
|
-
let code: string | undefined;
|
|
410
|
-
|
|
411
|
-
try {
|
|
412
|
-
// Build authorization URL
|
|
413
245
|
const authParams = new URLSearchParams({
|
|
414
246
|
client_id: CLIENT_ID,
|
|
415
247
|
response_type: "code",
|
|
416
|
-
redirect_uri:
|
|
248
|
+
redirect_uri: redirectUri,
|
|
417
249
|
scope: SCOPES.join(" "),
|
|
418
|
-
code_challenge: challenge,
|
|
250
|
+
code_challenge: this.challenge,
|
|
419
251
|
code_challenge_method: "S256",
|
|
420
|
-
state
|
|
252
|
+
state,
|
|
421
253
|
access_type: "offline",
|
|
422
254
|
prompt: "consent",
|
|
423
255
|
});
|
|
424
256
|
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
onAuth({
|
|
429
|
-
url: authUrl,
|
|
430
|
-
instructions: "Complete the sign-in in your browser.",
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
// Wait for the callback, racing with manual input if provided
|
|
434
|
-
onProgress?.("Waiting for OAuth callback...");
|
|
435
|
-
|
|
436
|
-
if (onManualCodeInput) {
|
|
437
|
-
// Race between browser callback and manual input
|
|
438
|
-
let manualInput: string | undefined;
|
|
439
|
-
let manualError: Error | undefined;
|
|
440
|
-
const manualPromise = onManualCodeInput()
|
|
441
|
-
.then((input) => {
|
|
442
|
-
manualInput = input;
|
|
443
|
-
server.cancelWait();
|
|
444
|
-
})
|
|
445
|
-
.catch((err) => {
|
|
446
|
-
manualError = err instanceof Error ? err : new Error(String(err));
|
|
447
|
-
server.cancelWait();
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
const result = await server.waitForCode();
|
|
451
|
-
|
|
452
|
-
// If manual input was cancelled, throw that error
|
|
453
|
-
if (manualError) {
|
|
454
|
-
throw manualError;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
if (result?.code) {
|
|
458
|
-
// Browser callback won - verify state
|
|
459
|
-
if (result.state !== verifier) {
|
|
460
|
-
throw new Error("OAuth state mismatch - possible CSRF attack");
|
|
461
|
-
}
|
|
462
|
-
code = result.code;
|
|
463
|
-
} else if (manualInput) {
|
|
464
|
-
// Manual input won
|
|
465
|
-
const parsed = parseRedirectUrl(manualInput);
|
|
466
|
-
if (parsed.state && parsed.state !== verifier) {
|
|
467
|
-
throw new Error("OAuth state mismatch - possible CSRF attack");
|
|
468
|
-
}
|
|
469
|
-
code = parsed.code;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
// If still no code, wait for manual promise and try that
|
|
473
|
-
if (!code) {
|
|
474
|
-
await manualPromise;
|
|
475
|
-
if (manualError) {
|
|
476
|
-
throw manualError;
|
|
477
|
-
}
|
|
478
|
-
if (manualInput) {
|
|
479
|
-
const parsed = parseRedirectUrl(manualInput);
|
|
480
|
-
if (parsed.state && parsed.state !== verifier) {
|
|
481
|
-
throw new Error("OAuth state mismatch - possible CSRF attack");
|
|
482
|
-
}
|
|
483
|
-
code = parsed.code;
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
} else {
|
|
487
|
-
// Original flow: just wait for callback
|
|
488
|
-
const result = await server.waitForCode();
|
|
489
|
-
if (result?.code) {
|
|
490
|
-
if (result.state !== verifier) {
|
|
491
|
-
throw new Error("OAuth state mismatch - possible CSRF attack");
|
|
492
|
-
}
|
|
493
|
-
code = result.code;
|
|
494
|
-
}
|
|
495
|
-
}
|
|
257
|
+
const url = `${AUTH_URL}?${authParams.toString()}`;
|
|
258
|
+
return { url, instructions: "Complete the sign-in in your browser." };
|
|
259
|
+
}
|
|
496
260
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
}
|
|
261
|
+
protected async exchangeToken(code: string, _state: string, redirectUri: string): Promise<OAuthCredentials> {
|
|
262
|
+
this.ctrl.onProgress?.("Exchanging authorization code for tokens...");
|
|
500
263
|
|
|
501
|
-
// Exchange code for tokens
|
|
502
|
-
onProgress?.("Exchanging authorization code for tokens...");
|
|
503
264
|
const tokenResponse = await fetch(TOKEN_URL, {
|
|
504
265
|
method: "POST",
|
|
505
|
-
headers: {
|
|
506
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
507
|
-
},
|
|
266
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
508
267
|
body: new URLSearchParams({
|
|
509
268
|
client_id: CLIENT_ID,
|
|
510
269
|
client_secret: CLIENT_SECRET,
|
|
511
270
|
code,
|
|
512
271
|
grant_type: "authorization_code",
|
|
513
|
-
redirect_uri:
|
|
514
|
-
code_verifier: verifier,
|
|
272
|
+
redirect_uri: redirectUri,
|
|
273
|
+
code_verifier: this.verifier,
|
|
515
274
|
}),
|
|
516
275
|
});
|
|
517
276
|
|
|
@@ -530,26 +289,59 @@ export async function loginGeminiCli(
|
|
|
530
289
|
throw new Error("No refresh token received. Please try again.");
|
|
531
290
|
}
|
|
532
291
|
|
|
533
|
-
|
|
534
|
-
onProgress?.("Getting user info...");
|
|
292
|
+
this.ctrl.onProgress?.("Getting user info...");
|
|
535
293
|
const email = await getUserEmail(tokenData.access_token);
|
|
536
294
|
|
|
537
|
-
|
|
538
|
-
const projectId = await discoverProject(tokenData.access_token, onProgress);
|
|
539
|
-
|
|
540
|
-
// Calculate expiry time (current time + expires_in seconds - 5 min buffer)
|
|
541
|
-
const expiresAt = Date.now() + tokenData.expires_in * 1000 - 5 * 60 * 1000;
|
|
295
|
+
const projectId = await discoverProject(tokenData.access_token, this.ctrl.onProgress);
|
|
542
296
|
|
|
543
|
-
|
|
297
|
+
return {
|
|
544
298
|
refresh: tokenData.refresh_token,
|
|
545
299
|
access: tokenData.access_token,
|
|
546
|
-
expires:
|
|
300
|
+
expires: Date.now() + tokenData.expires_in * 1000 - 5 * 60 * 1000,
|
|
547
301
|
projectId,
|
|
548
302
|
email,
|
|
549
303
|
};
|
|
304
|
+
}
|
|
305
|
+
}
|
|
550
306
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
307
|
+
/**
|
|
308
|
+
* Login with Gemini CLI (Google Cloud Code Assist) OAuth
|
|
309
|
+
*/
|
|
310
|
+
export async function loginGeminiCli(ctrl: OAuthController): Promise<OAuthCredentials> {
|
|
311
|
+
const flow = new GeminiCliOAuthFlow(ctrl);
|
|
312
|
+
return flow.login();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Refresh Google Cloud Code Assist token
|
|
317
|
+
*/
|
|
318
|
+
export async function refreshGoogleCloudToken(refreshToken: string, projectId: string): Promise<OAuthCredentials> {
|
|
319
|
+
const response = await fetch(TOKEN_URL, {
|
|
320
|
+
method: "POST",
|
|
321
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
322
|
+
body: new URLSearchParams({
|
|
323
|
+
client_id: CLIENT_ID,
|
|
324
|
+
client_secret: CLIENT_SECRET,
|
|
325
|
+
refresh_token: refreshToken,
|
|
326
|
+
grant_type: "refresh_token",
|
|
327
|
+
}),
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
if (!response.ok) {
|
|
331
|
+
const error = await response.text();
|
|
332
|
+
throw new Error(`Google Cloud token refresh failed: ${error}`);
|
|
554
333
|
}
|
|
334
|
+
|
|
335
|
+
const data = (await response.json()) as {
|
|
336
|
+
access_token: string;
|
|
337
|
+
expires_in: number;
|
|
338
|
+
refresh_token?: string;
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
refresh: data.refresh_token || refreshToken,
|
|
343
|
+
access: data.access_token,
|
|
344
|
+
expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,
|
|
345
|
+
projectId,
|
|
346
|
+
};
|
|
555
347
|
}
|