/** * Gemini CLI OAuth flow (Google Cloud Code Assist) * Standard Gemini models only (gemini-2.0-flash, gemini-2.5-*) */ import { getGeminiCliHeaders } from "@oh-my-pi/pi-catalog/wire/gemini-headers"; import { $env } from "@oh-my-pi/pi-utils"; import * as AIError from "../../error"; import { runGoogleOAuthLogin } from "./google-oauth-shared"; import type { OAuthController, OAuthCredentials } from "./types"; const decode = (s: string) => atob(s); const CLIENT_ID = decode( "NjgxMjU1ODA5Mzk1LW9vOGZ0Mm9wcmRybnA5ZTNhcWY2YXYzaG1kaWIxMzVqLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29t", ); const CLIENT_SECRET = decode("R09DU1BYLTR1SGdNUG0tMW83U2stZ2VWNkN1NWNsWEZzeGw="); const CALLBACK_PORT = 8085; const CALLBACK_PATH = "/oauth2callback"; const SCOPES = [ "https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile", ]; const AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth"; const TOKEN_URL = "https://oauth2.googleapis.com/token"; const CODE_ASSIST_ENDPOINT = "https://cloudcode-pa.googleapis.com"; interface LoadCodeAssistPayload { cloudaicompanionProject?: string; currentTier?: { id?: string }; allowedTiers?: Array<{ id?: string; isDefault?: boolean }>; } interface LongRunningOperationResponse { name?: string; done?: boolean; response?: { cloudaicompanionProject?: { id?: string }; }; } const TIER_FREE = "free-tier"; const TIER_LEGACY = "legacy-tier"; const TIER_STANDARD = "standard-tier"; interface GoogleRpcErrorResponse { error?: { details?: Array<{ reason?: string }>; }; } function getDefaultTier(allowedTiers?: Array<{ id?: string; isDefault?: boolean }>): { id?: string } { if (!allowedTiers || allowedTiers.length === 0) return { id: TIER_LEGACY }; const defaultTier = allowedTiers.find(t => t.isDefault); return defaultTier ?? { id: TIER_LEGACY }; } function isVpcScAffectedUser(payload: unknown): boolean { if (!payload || typeof payload !== "object") return false; if (!("error" in payload)) return false; const error = (payload as GoogleRpcErrorResponse).error; if (!error?.details || !Array.isArray(error.details)) return false; return error.details.some(detail => detail.reason === "SECURITY_POLICY_VIOLATED"); } async function pollOperation( operationName: string, headers: Record, onProgress?: (message: string) => void, ): Promise { let attempt = 0; while (true) { if (attempt > 0) { onProgress?.(`Waiting for project provisioning (attempt ${attempt + 1})...`); await Bun.sleep(5000); } const response = await fetch(`${CODE_ASSIST_ENDPOINT}/v1internal/${operationName}`, { method: "GET", headers, }); if (!response.ok) { throw new AIError.OAuthError(`Failed to poll operation: ${response.status} ${response.statusText}`, { kind: "polling", provider: "google-gemini-cli", status: response.status, }); } const data = (await response.json()) as LongRunningOperationResponse; if (data.done) { return data; } attempt += 1; } } async function discoverProject(accessToken: string, onProgress?: (message: string) => void): Promise { const envProjectId = $env.GOOGLE_CLOUD_PROJECT || $env.GOOGLE_CLOUD_PROJECT_ID; const headers = { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", ...getGeminiCliHeaders(), }; onProgress?.("Checking for existing Cloud Code Assist project..."); const loadResponse = await fetch(`${CODE_ASSIST_ENDPOINT}/v1internal:loadCodeAssist`, { method: "POST", headers, body: JSON.stringify({ cloudaicompanionProject: envProjectId, metadata: { ideType: "IDE_UNSPECIFIED", platform: "PLATFORM_UNSPECIFIED", pluginType: "GEMINI", duetProject: envProjectId, }, }), }); let data: LoadCodeAssistPayload; if (!loadResponse.ok) { let errorPayload: unknown; try { errorPayload = await loadResponse.clone().json(); } catch { errorPayload = undefined; } if (isVpcScAffectedUser(errorPayload)) { data = { currentTier: { id: TIER_STANDARD } }; } else { const errorText = await loadResponse.text(); throw new AIError.OAuthError( `loadCodeAssist failed: ${loadResponse.status} ${loadResponse.statusText}: ${errorText}`, { kind: "discovery", provider: "google-gemini-cli", status: loadResponse.status }, ); } } else { data = (await loadResponse.json()) as LoadCodeAssistPayload; } if (data.currentTier) { if (data.cloudaicompanionProject) { return data.cloudaicompanionProject; } if (envProjectId) { return envProjectId; } throw new AIError.OAuthError( "This account requires setting the GOOGLE_CLOUD_PROJECT or GOOGLE_CLOUD_PROJECT_ID environment variable. " + "See https://goo.gle/gemini-cli-auth-docs#workspace-gca", { kind: "configuration", provider: "google-gemini-cli" }, ); } const tier = getDefaultTier(data.allowedTiers); const tierId = tier?.id ?? TIER_FREE; if (tierId !== TIER_FREE && !envProjectId) { throw new AIError.OAuthError( "This account requires setting the GOOGLE_CLOUD_PROJECT or GOOGLE_CLOUD_PROJECT_ID environment variable. " + "See https://goo.gle/gemini-cli-auth-docs#workspace-gca", { kind: "configuration", provider: "google-gemini-cli" }, ); } onProgress?.("Provisioning Cloud Code Assist project (this may take a moment)..."); const onboardBody: Record = { tierId, metadata: { ideType: "IDE_UNSPECIFIED", platform: "PLATFORM_UNSPECIFIED", pluginType: "GEMINI", }, }; if (tierId !== TIER_FREE && envProjectId) { onboardBody.cloudaicompanionProject = envProjectId; (onboardBody.metadata as Record).duetProject = envProjectId; } const onboardResponse = await fetch(`${CODE_ASSIST_ENDPOINT}/v1internal:onboardUser`, { method: "POST", headers, body: JSON.stringify(onboardBody), }); if (!onboardResponse.ok) { const errorText = await onboardResponse.text(); throw new AIError.OAuthError( `onboardUser failed: ${onboardResponse.status} ${onboardResponse.statusText}: ${errorText}`, { kind: "provisioning", provider: "google-gemini-cli", status: onboardResponse.status }, ); } let lroData = (await onboardResponse.json()) as LongRunningOperationResponse; if (!lroData.done && lroData.name) { lroData = await pollOperation(lroData.name, headers, onProgress); } const projectId = lroData.response?.cloudaicompanionProject?.id; if (projectId) { return projectId; } if (envProjectId) { return envProjectId; } throw new AIError.OAuthError( "Could not discover or provision a Google Cloud project. " + "Try setting the GOOGLE_CLOUD_PROJECT or GOOGLE_CLOUD_PROJECT_ID environment variable. " + "See https://goo.gle/gemini-cli-auth-docs#workspace-gca", { kind: "validation", provider: "google-gemini-cli" }, ); } export async function loginGeminiCli(ctrl: OAuthController): Promise { return runGoogleOAuthLogin(ctrl, { clientId: CLIENT_ID, clientSecret: CLIENT_SECRET, authUrl: AUTH_URL, tokenUrl: TOKEN_URL, scopes: SCOPES, callbackPort: CALLBACK_PORT, callbackPath: CALLBACK_PATH, discoverProject, }); } /** * Refresh Google Cloud Code Assist token */ export async function refreshGoogleCloudToken(refreshToken: string, projectId: string): Promise { const response = await fetch(TOKEN_URL, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ client_id: CLIENT_ID, client_secret: CLIENT_SECRET, refresh_token: refreshToken, grant_type: "refresh_token", }), }); if (!response.ok) { const error = await response.text(); throw new AIError.OAuthError(`Google Cloud token refresh failed: ${error}`, { kind: "token-refresh", provider: "google-gemini-cli", }); } const data = (await response.json()) as { access_token: string; expires_in: number; refresh_token?: string; }; return { refresh: data.refresh_token || refreshToken, access: data.access_token, expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000, projectId, }; }