/** * User Onboarding * * Handles provisioning of managed projects for accounts that don't have one. */ import { ONBOARD_USER_ENDPOINTS, ANTIGRAVITY_HEADERS, CLIENT_METADATA } from '../constants.js'; import { logger } from '../utils/logger.js'; import { sleep, throttledFetch } from '../utils/helpers.js'; /** * Get the default tier ID from allowed tiers list * * @param {Array} allowedTiers - List of allowed tiers from loadCodeAssist * @returns {string|undefined} Default tier ID */ export function getDefaultTierId(allowedTiers) { if (!allowedTiers || allowedTiers.length === 0) { return undefined; } // Find the tier marked as default for (const tier of allowedTiers) { if (tier?.isDefault) { return tier.id; } } // Fall back to first tier return allowedTiers[0]?.id; } /** * Onboard a user to get a managed project * * @param {string} token - OAuth access token * @param {string} tierId - Tier ID (raw API value, e.g., 'free-tier', 'standard-tier', 'g1-pro-tier') * @param {string} [projectId] - Optional GCP project ID (required for non-free tiers) * @param {number} [maxAttempts=10] - Maximum polling attempts * @param {number} [delayMs=5000] - Delay between polling attempts * @returns {Promise} Managed project ID or null if failed */ export async function onboardUser(token, tierId, projectId = undefined, maxAttempts = 10, delayMs = 5000) { const metadata = { ...CLIENT_METADATA }; if (projectId) { metadata.duetProject = projectId; } const requestBody = { tierId, metadata }; // Note: Do NOT add cloudaicompanionProject to requestBody // Reference implementation only sets metadata.duetProject, not the body field // Adding cloudaicompanionProject causes 400 errors for auto-provisioned tiers (g1-pro, g1-ultra) logger.debug(`[Onboarding] Starting onboard with tierId: ${tierId}, projectId: ${projectId}`); for (const endpoint of ONBOARD_USER_ENDPOINTS) { for (let attempt = 0; attempt < maxAttempts; attempt++) { try { const response = await throttledFetch(`${endpoint}/v1internal:onboardUser`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', ...ANTIGRAVITY_HEADERS }, body: JSON.stringify(requestBody) }); if (!response.ok) { const errorText = await response.text(); logger.warn(`[Onboarding] onboardUser failed at ${endpoint}: ${response.status} - ${errorText}`); break; // Try next endpoint } const data = await response.json(); logger.debug(`[Onboarding] onboardUser response (attempt ${attempt + 1}):`, JSON.stringify(data)); // Check if onboarding is complete const managedProjectId = data.response?.cloudaicompanionProject?.id; if (data.done && managedProjectId) { return managedProjectId; } if (data.done && projectId) { return projectId; } // Not done yet, wait and retry if (attempt < maxAttempts - 1) { logger.debug(`[Onboarding] onboardUser not complete, waiting ${delayMs}ms...`); await sleep(delayMs); } } catch (error) { logger.warn(`[Onboarding] onboardUser error at ${endpoint}:`, error.message); break; // Try next endpoint } } } logger.warn(`[Onboarding] All onboarding attempts failed for tierId: ${tierId}`); return null; }