| |
| |
| |
| |
| |
|
|
| import { |
| UserTierId, |
| IneligibleTierReasonCode, |
| type ClientMetadata, |
| type GeminiUserTier, |
| type IneligibleTier, |
| type LoadCodeAssistResponse, |
| type OnboardUserRequest, |
| } from './types.js'; |
| import { CodeAssistServer, type HttpOptions } from './server.js'; |
| import type { AuthClient } from 'google-auth-library'; |
| import { ChangeAuthRequestedError } from '../utils/errors.js'; |
| import { ValidationRequiredError } from '../utils/googleQuotaErrors.js'; |
| import { debugLogger } from '../utils/debugLogger.js'; |
| import { createCache, type CacheService } from '../utils/cache.js'; |
| import type { Config } from '../config/config.js'; |
| import { |
| logOnboardingStart, |
| logOnboardingSuccess, |
| OnboardingStartEvent, |
| OnboardingSuccessEvent, |
| } from '../telemetry/index.js'; |
|
|
| export class ProjectIdRequiredError extends Error { |
| constructor() { |
| super( |
| 'This account requires setting the GOOGLE_CLOUD_PROJECT or GOOGLE_CLOUD_PROJECT_ID env var. See https://goo.gle/gemini-cli-auth-docs#workspace-gca', |
| ); |
| this.name = 'ProjectIdRequiredError'; |
| } |
| } |
|
|
| export class InvalidNumericProjectIdError extends Error { |
| constructor(projectId: string) { |
| super( |
| `Invalid Google Cloud Project ID: "${projectId}". The GOOGLE_CLOUD_PROJECT (or GOOGLE_CLOUD_PROJECT_ID) environment variable must be set to your string-based Project ID (e.g., "my-project-123"), not your numeric Project Number. Please update your environment variables.`, |
| ); |
| this.name = 'InvalidNumericProjectIdError'; |
| } |
| } |
|
|
| |
| |
| |
| |
| export class ValidationCancelledError extends Error { |
| constructor() { |
| super('User cancelled account validation'); |
| this.name = 'ValidationCancelledError'; |
| } |
| } |
|
|
| export class IneligibleTierError extends Error { |
| readonly ineligibleTiers: IneligibleTier[]; |
|
|
| constructor(ineligibleTiers: IneligibleTier[]) { |
| const reasons = ineligibleTiers.map((t) => t.reasonMessage).join(', '); |
| super(reasons); |
| this.name = 'IneligibleTierError'; |
| this.ineligibleTiers = ineligibleTiers; |
| } |
| } |
|
|
| export interface UserData { |
| projectId: string; |
| userTier: UserTierId; |
| userTierName?: string; |
| paidTier?: GeminiUserTier; |
| hasOnboardedPreviously?: boolean; |
| } |
|
|
| |
| |
| |
| let userDataCache = createCache< |
| AuthClient, |
| CacheService<string | undefined, Promise<UserData>> |
| >({ |
| storage: 'weakmap', |
| }); |
|
|
| |
| |
| |
| |
| export function resetUserDataCacheForTesting() { |
| userDataCache = createCache< |
| AuthClient, |
| CacheService<string | undefined, Promise<UserData>> |
| >({ |
| storage: 'weakmap', |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export async function setupUser( |
| client: AuthClient, |
| config: Config, |
| httpOptions: HttpOptions = {}, |
| ): Promise<UserData> { |
| const projectId = |
| process.env['GOOGLE_CLOUD_PROJECT'] || |
| process.env['GOOGLE_CLOUD_PROJECT_ID'] || |
| undefined; |
|
|
| if (projectId && /^\d+$/.test(projectId)) { |
| throw new InvalidNumericProjectIdError(projectId); |
| } |
|
|
| const projectCache = userDataCache.getOrCreate(client, () => |
| createCache<string | undefined, Promise<UserData>>({ |
| storage: 'map', |
| defaultTtl: 30000, |
| }), |
| ); |
|
|
| return projectCache.getOrCreate(projectId, () => |
| _doSetupUser(client, projectId, config, httpOptions), |
| ); |
| } |
|
|
| |
| |
| |
| async function _doSetupUser( |
| client: AuthClient, |
| projectId: string | undefined, |
| config: Config, |
| httpOptions: HttpOptions = {}, |
| ): Promise<UserData> { |
| const caServer = new CodeAssistServer( |
| client, |
| projectId, |
| httpOptions, |
| '', |
| undefined, |
| undefined, |
| ); |
| const coreClientMetadata: ClientMetadata = { |
| ideType: 'IDE_UNSPECIFIED', |
| platform: 'PLATFORM_UNSPECIFIED', |
| pluginType: 'GEMINI', |
| }; |
|
|
| const validationHandler = config.getValidationHandler(); |
|
|
| let loadRes: LoadCodeAssistResponse; |
| while (true) { |
| loadRes = await caServer.loadCodeAssist({ |
| cloudaicompanionProject: projectId, |
| metadata: { |
| ...coreClientMetadata, |
| duetProject: projectId, |
| }, |
| }); |
|
|
| try { |
| validateLoadCodeAssistResponse(loadRes); |
| break; |
| } catch (e) { |
| if (e instanceof ValidationRequiredError && validationHandler) { |
| const intent = await validationHandler( |
| e.validationLink, |
| e.validationDescription, |
| ); |
| if (intent === 'verify') { |
| continue; |
| } |
| if (intent === 'change_auth') { |
| throw new ChangeAuthRequestedError(); |
| } |
| throw new ValidationCancelledError(); |
| } |
| throw e; |
| } |
| } |
|
|
| if (loadRes.currentTier) { |
| if (!loadRes.paidTier?.id && !loadRes.currentTier.id) { |
| debugLogger.warn( |
| 'Warning: Code Assist API did not return a user tier ID. Defaulting to STANDARD tier.', |
| ); |
| } |
|
|
| if (!loadRes.cloudaicompanionProject) { |
| if (projectId) { |
| return { |
| projectId, |
| userTier: |
| loadRes.paidTier?.id ?? |
| loadRes.currentTier.id ?? |
| UserTierId.STANDARD, |
| userTierName: loadRes.paidTier?.name ?? loadRes.currentTier.name, |
| paidTier: loadRes.paidTier ?? undefined, |
| hasOnboardedPreviously: |
| loadRes.currentTier.hasOnboardedPreviously ?? true, |
| }; |
| } |
|
|
| |
| throwIneligibleOrProjectIdError(loadRes); |
| } |
| return { |
| projectId: loadRes.cloudaicompanionProject, |
| userTier: |
| loadRes.paidTier?.id ?? loadRes.currentTier.id ?? UserTierId.STANDARD, |
| userTierName: loadRes.paidTier?.name ?? loadRes.currentTier.name, |
| paidTier: loadRes.paidTier ?? undefined, |
| hasOnboardedPreviously: |
| loadRes.currentTier.hasOnboardedPreviously ?? true, |
| }; |
| } |
|
|
| const tier = getOnboardTier(loadRes); |
|
|
| if (!tier.id) { |
| debugLogger.warn( |
| 'Warning: Code Assist API did not return an onboarding tier ID. Defaulting to STANDARD tier.', |
| ); |
| } |
|
|
| let onboardReq: OnboardUserRequest; |
| if (tier.id === UserTierId.FREE) { |
| |
| onboardReq = { |
| tierId: tier.id, |
| cloudaicompanionProject: undefined, |
| metadata: coreClientMetadata, |
| }; |
| } else { |
| onboardReq = { |
| tierId: tier.id, |
| cloudaicompanionProject: projectId, |
| metadata: { |
| ...coreClientMetadata, |
| duetProject: projectId, |
| }, |
| }; |
| } |
|
|
| logOnboardingStart(config, new OnboardingStartEvent()); |
| const onboardingStartTime = Date.now(); |
|
|
| let lroRes = await caServer.onboardUser(onboardReq); |
| if (!lroRes.done && lroRes.name) { |
| const operationName = lroRes.name; |
| while (!lroRes.done) { |
| await new Promise((f) => setTimeout(f, 5000)); |
| lroRes = await caServer.getOperation(operationName); |
| } |
| } |
|
|
| logOnboardingSuccess( |
| config, |
| new OnboardingSuccessEvent(tier.name, Date.now() - onboardingStartTime), |
| ); |
|
|
| if (!lroRes.response?.cloudaicompanionProject?.id) { |
| if (projectId) { |
| return { |
| projectId, |
| userTier: tier.id ?? UserTierId.STANDARD, |
| userTierName: tier.name, |
| hasOnboardedPreviously: tier.hasOnboardedPreviously ?? false, |
| }; |
| } |
|
|
| throwIneligibleOrProjectIdError(loadRes); |
| } |
|
|
| return { |
| projectId: lroRes.response.cloudaicompanionProject.id, |
| userTier: tier.id ?? UserTierId.STANDARD, |
| userTierName: tier.name, |
| hasOnboardedPreviously: tier.hasOnboardedPreviously ?? false, |
| }; |
| } |
|
|
| function throwIneligibleOrProjectIdError(res: LoadCodeAssistResponse): never { |
| if (res.ineligibleTiers && res.ineligibleTiers.length > 0) { |
| throw new IneligibleTierError(res.ineligibleTiers); |
| } |
| throw new ProjectIdRequiredError(); |
| } |
|
|
| function getOnboardTier(res: LoadCodeAssistResponse): GeminiUserTier { |
| for (const tier of res.allowedTiers || []) { |
| if (tier.isDefault) { |
| return tier; |
| } |
| } |
| return { |
| name: '', |
| description: '', |
| id: UserTierId.LEGACY, |
| userDefinedCloudaicompanionProject: true, |
| }; |
| } |
|
|
| function validateLoadCodeAssistResponse(res: LoadCodeAssistResponse): void { |
| if (!res) { |
| throw new Error('LoadCodeAssist returned empty response'); |
| } |
| if ( |
| !res.currentTier && |
| res.ineligibleTiers && |
| res.ineligibleTiers.length > 0 |
| ) { |
| const validationTier = res.ineligibleTiers.find( |
| (t) => |
| t.validationUrl && |
| t.reasonCode === IneligibleTierReasonCode.VALIDATION_REQUIRED, |
| ); |
| const validationUrl = validationTier?.validationUrl; |
| if (validationTier && validationUrl) { |
| throw new ValidationRequiredError( |
| `Account validation required: ${validationTier.reasonMessage}`, |
| undefined, |
| validationUrl, |
| validationTier.reasonMessage, |
| ); |
| } |
| } |
| } |
|
|