/** * Bootstrap Antigravity OAuth accounts from environment variables. */ import { parseRefreshParts, formatRefreshParts } from '../auth/oauth.js'; import { loadAccounts, saveAccounts } from '../account-manager/storage.js'; import { logger } from '../utils/logger.js'; function envValue(env, key) { const value = env[key]; return typeof value === 'string' ? value.trim() : ''; } function isTruthy(value) { return ['1', 'true', 'yes', 'on'].includes(String(value || '').trim().toLowerCase()); } function normalizeTier(value) { const tier = String(value || '').trim().toLowerCase(); if (tier.includes('ultra')) return 'ultra'; if (tier.includes('pro') || tier.includes('premium') || tier.includes('standard')) return 'pro'; if (tier.includes('free')) return 'free'; return 'unknown'; } function findCredentialIndexes(env) { const indexes = new Set(); for (const key of Object.keys(env)) { const match = key.match(/^ANTIGRAVITY_(\d+)_REFRESH_TOKEN$/); if (match && envValue(env, key)) indexes.add(Number(match[1])); } if (indexes.size === 0 && envValue(env, 'ANTIGRAVITY_REFRESH_TOKEN')) { indexes.add(0); } return [...indexes].sort((a, b) => a - b); } export function collectEnvAccounts(env = process.env) { const accounts = []; for (const index of findCredentialIndexes(env)) { const prefix = index === 0 ? 'ANTIGRAVITY' : `ANTIGRAVITY_${index}`; const email = envValue(env, `${prefix}_EMAIL`); const rawRefreshToken = envValue(env, `${prefix}_REFRESH_TOKEN`); const projectId = envValue(env, `${prefix}_PROJECT_ID`); const tier = normalizeTier(envValue(env, `${prefix}_TIER`)); if (!email) { throw new Error(`${prefix}_EMAIL is required when ${prefix}_REFRESH_TOKEN is set`); } if (!rawRefreshToken) continue; const existingParts = parseRefreshParts(rawRefreshToken); const resolvedProjectId = projectId || existingParts.managedProjectId || existingParts.projectId; const refreshToken = formatRefreshParts({ refreshToken: existingParts.refreshToken, projectId: existingParts.projectId || resolvedProjectId, managedProjectId: existingParts.managedProjectId || resolvedProjectId }); accounts.push({ email, source: 'oauth', enabled: true, refreshToken, projectId: resolvedProjectId || undefined, addedAt: new Date().toISOString(), isInvalid: false, invalidReason: null, verifyUrl: null, modelRateLimits: {}, lastUsed: null, subscription: { tier, projectId: resolvedProjectId || null, detectedAt: tier !== 'unknown' || resolvedProjectId ? Date.now() : null }, quota: { models: {}, lastChecked: null }, modelQuotaThresholds: {} }); } return accounts; } export async function bootstrapEnvAccounts(configPath, env = process.env) { const envAccounts = collectEnvAccounts(env); if (envAccounts.length === 0) { return { changed: false, count: 0 }; } const force = isTruthy(env.FORCE_ENV_ACCOUNTS); const { accounts: existingAccounts, settings, activeIndex } = await loadAccounts(configPath); const merged = force ? [] : [...existingAccounts]; for (const envAccount of envAccounts) { const existingIndex = merged.findIndex(account => account.email === envAccount.email); if (existingIndex === -1) { merged.push(envAccount); } else { merged[existingIndex] = { ...merged[existingIndex], ...envAccount, addedAt: merged[existingIndex].addedAt || envAccount.addedAt, modelRateLimits: force ? {} : (merged[existingIndex].modelRateLimits || {}), lastUsed: force ? null : (merged[existingIndex].lastUsed || null), quota: force ? envAccount.quota : (merged[existingIndex].quota || envAccount.quota) }; } } await saveAccounts(configPath, merged, settings, Math.min(activeIndex, Math.max(0, merged.length - 1))); // saveAccounts logs and swallows I/O errors, so verify persistence explicitly. const { accounts: persistedAccounts } = await loadAccounts(configPath); const persistedEmails = new Set(persistedAccounts.map(account => account.email)); const missingEmails = envAccounts .map(account => account.email) .filter(email => !persistedEmails.has(email)); if (missingEmails.length > 0) { throw new Error(`Failed to persist environment accounts: ${missingEmails.join(', ')}`); } logger.info(`[Bootstrap] Loaded ${envAccounts.length} Antigravity account(s) from environment${force ? ' (authoritative)' : ''}`); return { changed: true, count: envAccounts.length }; }