commons-proxy 2.0.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.
Files changed (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +757 -0
  3. package/bin/cli.js +146 -0
  4. package/package.json +97 -0
  5. package/public/Complaint Details.pdf +0 -0
  6. package/public/Cyber Crime Portal.pdf +0 -0
  7. package/public/app.js +229 -0
  8. package/public/css/src/input.css +523 -0
  9. package/public/css/style.css +1 -0
  10. package/public/favicon.png +0 -0
  11. package/public/index.html +549 -0
  12. package/public/js/components/account-manager.js +356 -0
  13. package/public/js/components/add-account-modal.js +414 -0
  14. package/public/js/components/claude-config.js +420 -0
  15. package/public/js/components/dashboard/charts.js +605 -0
  16. package/public/js/components/dashboard/filters.js +362 -0
  17. package/public/js/components/dashboard/stats.js +110 -0
  18. package/public/js/components/dashboard.js +236 -0
  19. package/public/js/components/logs-viewer.js +100 -0
  20. package/public/js/components/models.js +36 -0
  21. package/public/js/components/server-config.js +349 -0
  22. package/public/js/config/constants.js +102 -0
  23. package/public/js/data-store.js +375 -0
  24. package/public/js/settings-store.js +58 -0
  25. package/public/js/store.js +99 -0
  26. package/public/js/translations/en.js +367 -0
  27. package/public/js/translations/id.js +412 -0
  28. package/public/js/translations/pt.js +308 -0
  29. package/public/js/translations/tr.js +358 -0
  30. package/public/js/translations/zh.js +373 -0
  31. package/public/js/utils/account-actions.js +189 -0
  32. package/public/js/utils/error-handler.js +96 -0
  33. package/public/js/utils/model-config.js +42 -0
  34. package/public/js/utils/ui-logger.js +143 -0
  35. package/public/js/utils/validators.js +77 -0
  36. package/public/js/utils.js +69 -0
  37. package/public/proxy-server-64.png +0 -0
  38. package/public/views/accounts.html +361 -0
  39. package/public/views/dashboard.html +484 -0
  40. package/public/views/logs.html +97 -0
  41. package/public/views/models.html +331 -0
  42. package/public/views/settings.html +1327 -0
  43. package/src/account-manager/credentials.js +378 -0
  44. package/src/account-manager/index.js +462 -0
  45. package/src/account-manager/onboarding.js +112 -0
  46. package/src/account-manager/rate-limits.js +369 -0
  47. package/src/account-manager/storage.js +160 -0
  48. package/src/account-manager/strategies/base-strategy.js +109 -0
  49. package/src/account-manager/strategies/hybrid-strategy.js +339 -0
  50. package/src/account-manager/strategies/index.js +79 -0
  51. package/src/account-manager/strategies/round-robin-strategy.js +76 -0
  52. package/src/account-manager/strategies/sticky-strategy.js +138 -0
  53. package/src/account-manager/strategies/trackers/health-tracker.js +162 -0
  54. package/src/account-manager/strategies/trackers/index.js +9 -0
  55. package/src/account-manager/strategies/trackers/quota-tracker.js +120 -0
  56. package/src/account-manager/strategies/trackers/token-bucket-tracker.js +155 -0
  57. package/src/auth/database.js +169 -0
  58. package/src/auth/oauth.js +548 -0
  59. package/src/auth/token-extractor.js +117 -0
  60. package/src/cli/accounts.js +648 -0
  61. package/src/cloudcode/index.js +29 -0
  62. package/src/cloudcode/message-handler.js +510 -0
  63. package/src/cloudcode/model-api.js +248 -0
  64. package/src/cloudcode/rate-limit-parser.js +235 -0
  65. package/src/cloudcode/request-builder.js +93 -0
  66. package/src/cloudcode/session-manager.js +47 -0
  67. package/src/cloudcode/sse-parser.js +121 -0
  68. package/src/cloudcode/sse-streamer.js +293 -0
  69. package/src/cloudcode/streaming-handler.js +615 -0
  70. package/src/config.js +125 -0
  71. package/src/constants.js +407 -0
  72. package/src/errors.js +242 -0
  73. package/src/fallback-config.js +29 -0
  74. package/src/format/content-converter.js +193 -0
  75. package/src/format/index.js +20 -0
  76. package/src/format/request-converter.js +255 -0
  77. package/src/format/response-converter.js +120 -0
  78. package/src/format/schema-sanitizer.js +673 -0
  79. package/src/format/signature-cache.js +88 -0
  80. package/src/format/thinking-utils.js +648 -0
  81. package/src/index.js +148 -0
  82. package/src/modules/usage-stats.js +205 -0
  83. package/src/providers/anthropic-provider.js +258 -0
  84. package/src/providers/base-provider.js +157 -0
  85. package/src/providers/cloudcode.js +94 -0
  86. package/src/providers/copilot.js +399 -0
  87. package/src/providers/github-provider.js +287 -0
  88. package/src/providers/google-provider.js +192 -0
  89. package/src/providers/index.js +211 -0
  90. package/src/providers/openai-compatible.js +265 -0
  91. package/src/providers/openai-provider.js +271 -0
  92. package/src/providers/openrouter-provider.js +325 -0
  93. package/src/providers/setup.js +83 -0
  94. package/src/server.js +870 -0
  95. package/src/utils/claude-config.js +245 -0
  96. package/src/utils/helpers.js +51 -0
  97. package/src/utils/logger.js +142 -0
  98. package/src/utils/native-module-helper.js +162 -0
  99. package/src/webui/index.js +1134 -0
@@ -0,0 +1,462 @@
1
+ /**
2
+ * Account Manager
3
+ * Manages multiple Antigravity accounts with configurable selection strategies,
4
+ * automatic failover, and smart cooldown for rate-limited accounts.
5
+ */
6
+
7
+ import { ACCOUNT_CONFIG_PATH } from '../constants.js';
8
+ import { loadAccounts, loadDefaultAccount, saveAccounts } from './storage.js';
9
+ import {
10
+ isAllRateLimited as checkAllRateLimited,
11
+ getAvailableAccounts as getAvailable,
12
+ getInvalidAccounts as getInvalid,
13
+ clearExpiredLimits as clearLimits,
14
+ resetAllRateLimits as resetLimits,
15
+ markRateLimited as markLimited,
16
+ markInvalid as markAccountInvalid,
17
+ getMinWaitTimeMs as getMinWait,
18
+ getRateLimitInfo as getLimitInfo,
19
+ getConsecutiveFailures as getFailures,
20
+ resetConsecutiveFailures as resetFailures,
21
+ incrementConsecutiveFailures as incrementFailures,
22
+ markAccountCoolingDown as markCoolingDown,
23
+ isAccountCoolingDown as checkCoolingDown,
24
+ clearAccountCooldown as clearCooldown,
25
+ getCooldownRemaining as getCooldownMs,
26
+ CooldownReason
27
+ } from './rate-limits.js';
28
+ import {
29
+ getTokenForAccount as fetchToken,
30
+ getProjectForAccount as fetchProject,
31
+ clearProjectCache as clearProject,
32
+ clearTokenCache as clearToken
33
+ } from './credentials.js';
34
+ import { createStrategy, getStrategyLabel, DEFAULT_STRATEGY } from './strategies/index.js';
35
+ import { logger } from '../utils/logger.js';
36
+ import { config } from '../config.js';
37
+
38
+ export class AccountManager {
39
+ #accounts = [];
40
+ #currentIndex = 0;
41
+ #configPath;
42
+ #settings = {};
43
+ #initialized = false;
44
+ #strategy = null;
45
+ #strategyName = DEFAULT_STRATEGY;
46
+
47
+ // Per-account caches
48
+ #tokenCache = new Map(); // email -> { token, extractedAt }
49
+ #projectCache = new Map(); // email -> projectId
50
+
51
+ constructor(configPath = ACCOUNT_CONFIG_PATH, strategyName = null) {
52
+ this.#configPath = configPath;
53
+ // Strategy name can be set at construction or later via initialize
54
+ if (strategyName) {
55
+ this.#strategyName = strategyName;
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Initialize the account manager by loading config
61
+ * @param {string} [strategyOverride] - Override strategy name (from CLI flag or env var)
62
+ */
63
+ async initialize(strategyOverride = null) {
64
+ if (this.#initialized) return;
65
+
66
+ const { accounts, settings, activeIndex } = await loadAccounts(this.#configPath);
67
+
68
+ this.#accounts = accounts;
69
+ this.#settings = settings;
70
+ this.#currentIndex = activeIndex;
71
+
72
+ // If config exists but has no accounts, fall back to Antigravity database
73
+ if (this.#accounts.length === 0) {
74
+ logger.warn('[AccountManager] No accounts in config. Falling back to Antigravity database');
75
+ const { accounts: defaultAccounts, tokenCache } = loadDefaultAccount();
76
+ this.#accounts = defaultAccounts;
77
+ this.#tokenCache = tokenCache;
78
+ }
79
+
80
+ // Determine strategy: CLI override > env var > config file > default
81
+ const configStrategy = config?.accountSelection?.strategy;
82
+ const envStrategy = process.env.ACCOUNT_STRATEGY;
83
+ this.#strategyName = strategyOverride || envStrategy || configStrategy || this.#strategyName;
84
+
85
+ // Create the strategy instance
86
+ const strategyConfig = config?.accountSelection || {};
87
+ this.#strategy = createStrategy(this.#strategyName, strategyConfig);
88
+ logger.info(`[AccountManager] Using ${getStrategyLabel(this.#strategyName)} selection strategy`);
89
+
90
+ // Clear any expired rate limits
91
+ this.clearExpiredLimits();
92
+
93
+ this.#initialized = true;
94
+ }
95
+
96
+ /**
97
+ * Reload accounts from disk (force re-initialization)
98
+ * Useful when accounts.json is modified externally (e.g., by WebUI)
99
+ */
100
+ async reload() {
101
+ this.#initialized = false;
102
+ await this.initialize();
103
+ logger.info('[AccountManager] Accounts reloaded from disk');
104
+ }
105
+
106
+ /**
107
+ * Get the number of accounts
108
+ * @returns {number} Number of configured accounts
109
+ */
110
+ getAccountCount() {
111
+ return this.#accounts.length;
112
+ }
113
+
114
+ /**
115
+ * Check if all accounts are rate-limited
116
+ * @param {string} [modelId] - Optional model ID
117
+ * @returns {boolean} True if all accounts are rate-limited
118
+ */
119
+ isAllRateLimited(modelId = null) {
120
+ return checkAllRateLimited(this.#accounts, modelId);
121
+ }
122
+
123
+ /**
124
+ * Get list of available (non-rate-limited, non-invalid) accounts
125
+ * @param {string} [modelId] - Optional model ID
126
+ * @returns {Array<Object>} Array of available account objects
127
+ */
128
+ getAvailableAccounts(modelId = null) {
129
+ return getAvailable(this.#accounts, modelId);
130
+ }
131
+
132
+ /**
133
+ * Get list of invalid accounts
134
+ * @returns {Array<Object>} Array of invalid account objects
135
+ */
136
+ getInvalidAccounts() {
137
+ return getInvalid(this.#accounts);
138
+ }
139
+
140
+ /**
141
+ * Clear expired rate limits
142
+ * @returns {number} Number of rate limits cleared
143
+ */
144
+ clearExpiredLimits() {
145
+ const cleared = clearLimits(this.#accounts);
146
+ if (cleared > 0) {
147
+ this.saveToDisk();
148
+ }
149
+ return cleared;
150
+ }
151
+
152
+ /**
153
+ * Clear all rate limits to force a fresh check
154
+ * (Optimistic retry strategy)
155
+ * @returns {void}
156
+ */
157
+ resetAllRateLimits() {
158
+ resetLimits(this.#accounts);
159
+ }
160
+
161
+ /**
162
+ * Select an account using the configured strategy.
163
+ * This is the main method to use for account selection.
164
+ * @param {string} [modelId] - Model ID for the request
165
+ * @param {Object} [options] - Additional options
166
+ * @param {string} [options.sessionId] - Session ID for cache continuity
167
+ * @returns {{account: Object|null, waitMs: number}} Account to use and optional wait time
168
+ */
169
+ selectAccount(modelId = null, options = {}) {
170
+ if (!this.#strategy) {
171
+ throw new Error('AccountManager not initialized. Call initialize() first.');
172
+ }
173
+
174
+ const result = this.#strategy.selectAccount(this.#accounts, modelId, {
175
+ currentIndex: this.#currentIndex,
176
+ onSave: () => this.saveToDisk(),
177
+ ...options
178
+ });
179
+
180
+ this.#currentIndex = result.index;
181
+ return { account: result.account, waitMs: result.waitMs || 0 };
182
+ }
183
+
184
+ /**
185
+ * Notify the strategy of a successful request
186
+ * @param {Object} account - The account that was used
187
+ * @param {string} modelId - The model ID that was used
188
+ */
189
+ notifySuccess(account, modelId) {
190
+ if (this.#strategy) {
191
+ this.#strategy.onSuccess(account, modelId);
192
+ }
193
+ // Reset consecutive failures on success (matches opencode-cloudcode-auth)
194
+ if (account?.email) {
195
+ resetFailures(this.#accounts, account.email);
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Notify the strategy of a rate limit
201
+ * @param {Object} account - The account that was rate-limited
202
+ * @param {string} modelId - The model ID that was rate-limited
203
+ */
204
+ notifyRateLimit(account, modelId) {
205
+ if (this.#strategy) {
206
+ this.#strategy.onRateLimit(account, modelId);
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Notify the strategy of a failure
212
+ * @param {Object} account - The account that failed
213
+ * @param {string} modelId - The model ID that failed
214
+ */
215
+ notifyFailure(account, modelId) {
216
+ if (this.#strategy) {
217
+ this.#strategy.onFailure(account, modelId);
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Get the consecutive failure count for an account
223
+ * Used for progressive backoff calculation
224
+ * @param {string} email - Account email
225
+ * @returns {number} Number of consecutive failures
226
+ */
227
+ getConsecutiveFailures(email) {
228
+ return getFailures(this.#accounts, email);
229
+ }
230
+
231
+ /**
232
+ * Increment the consecutive failure count without marking as rate limited
233
+ * Used for quick retries to track failures while staying on same account
234
+ * @param {string} email - Account email
235
+ * @returns {number} New consecutive failure count
236
+ */
237
+ incrementConsecutiveFailures(email) {
238
+ return incrementFailures(this.#accounts, email);
239
+ }
240
+
241
+ /**
242
+ * Get the current strategy name
243
+ * @returns {string} Strategy name
244
+ */
245
+ getStrategyName() {
246
+ return this.#strategyName;
247
+ }
248
+
249
+ /**
250
+ * Get the strategy display label
251
+ * @returns {string} Strategy display label
252
+ */
253
+ getStrategyLabel() {
254
+ return getStrategyLabel(this.#strategyName);
255
+ }
256
+
257
+ /**
258
+ * Get the health tracker from the current strategy (if available)
259
+ * Used by handlers for consecutive failure tracking
260
+ * Only available when using hybrid strategy
261
+ * @returns {Object|null} Health tracker instance or null if not available
262
+ */
263
+ getHealthTracker() {
264
+ if (this.#strategy && typeof this.#strategy.getHealthTracker === 'function') {
265
+ return this.#strategy.getHealthTracker();
266
+ }
267
+ return null;
268
+ }
269
+
270
+ /**
271
+ * Mark an account as rate-limited
272
+ * @param {string} email - Email of the account to mark
273
+ * @param {number|null} resetMs - Time in ms until rate limit resets (optional)
274
+ * @param {string} [modelId] - Optional model ID to mark specific limit
275
+ */
276
+ markRateLimited(email, resetMs = null, modelId = null) {
277
+ markLimited(this.#accounts, email, resetMs, modelId);
278
+ this.saveToDisk();
279
+ }
280
+
281
+ /**
282
+ * Mark an account as invalid (credentials need re-authentication)
283
+ * @param {string} email - Email of the account to mark
284
+ * @param {string} reason - Reason for marking as invalid
285
+ */
286
+ markInvalid(email, reason = 'Unknown error') {
287
+ markAccountInvalid(this.#accounts, email, reason);
288
+ this.saveToDisk();
289
+ }
290
+
291
+ /**
292
+ * Get the minimum wait time until any account becomes available
293
+ * @param {string} [modelId] - Optional model ID
294
+ * @returns {number} Wait time in milliseconds
295
+ */
296
+ getMinWaitTimeMs(modelId = null) {
297
+ return getMinWait(this.#accounts, modelId);
298
+ }
299
+
300
+ /**
301
+ * Get rate limit info for a specific account and model
302
+ * @param {string} email - Email of the account
303
+ * @param {string} modelId - Model ID to check
304
+ * @returns {{isRateLimited: boolean, actualResetMs: number|null, waitMs: number}} Rate limit info
305
+ */
306
+ getRateLimitInfo(email, modelId) {
307
+ return getLimitInfo(this.#accounts, email, modelId);
308
+ }
309
+
310
+ // ============================================================================
311
+ // Cooldown Methods (matches opencode-cloudcode-auth)
312
+ // ============================================================================
313
+
314
+ /**
315
+ * Mark an account as cooling down for a specified duration
316
+ * Used for temporary backoff separate from rate limits
317
+ * @param {string} email - Email of the account
318
+ * @param {number} cooldownMs - Duration of cooldown in milliseconds
319
+ * @param {string} [reason] - Reason for the cooldown (use CooldownReason constants)
320
+ */
321
+ markAccountCoolingDown(email, cooldownMs, reason = CooldownReason.RATE_LIMIT) {
322
+ markCoolingDown(this.#accounts, email, cooldownMs, reason);
323
+ }
324
+
325
+ /**
326
+ * Check if an account is currently cooling down
327
+ * @param {string} email - Email of the account
328
+ * @returns {boolean} True if account is cooling down
329
+ */
330
+ isAccountCoolingDown(email) {
331
+ const account = this.#accounts.find(a => a.email === email);
332
+ return account ? checkCoolingDown(account) : false;
333
+ }
334
+
335
+ /**
336
+ * Clear the cooldown for an account
337
+ * @param {string} email - Email of the account
338
+ */
339
+ clearAccountCooldown(email) {
340
+ const account = this.#accounts.find(a => a.email === email);
341
+ if (account) {
342
+ clearCooldown(account);
343
+ }
344
+ }
345
+
346
+ /**
347
+ * Get time remaining until cooldown expires for an account
348
+ * @param {string} email - Email of the account
349
+ * @returns {number} Milliseconds until cooldown expires, 0 if not cooling down
350
+ */
351
+ getCooldownRemaining(email) {
352
+ const account = this.#accounts.find(a => a.email === email);
353
+ return account ? getCooldownMs(account) : 0;
354
+ }
355
+
356
+ /**
357
+ * Get OAuth token for an account
358
+ * @param {Object} account - Account object with email and credentials
359
+ * @returns {Promise<string>} OAuth access token
360
+ * @throws {Error} If token refresh fails
361
+ */
362
+ async getTokenForAccount(account) {
363
+ return fetchToken(
364
+ account,
365
+ this.#tokenCache,
366
+ (email, reason) => this.markInvalid(email, reason),
367
+ () => this.saveToDisk()
368
+ );
369
+ }
370
+
371
+ /**
372
+ * Get project ID for an account
373
+ * @param {Object} account - Account object
374
+ * @param {string} token - OAuth access token
375
+ * @returns {Promise<string>} Project ID
376
+ */
377
+ async getProjectForAccount(account, token) {
378
+ // Pass onSave callback to persist managedProjectId in refresh token
379
+ return fetchProject(account, token, this.#projectCache, () => this.saveToDisk());
380
+ }
381
+
382
+ /**
383
+ * Clear project cache for an account (useful on auth errors)
384
+ * @param {string|null} email - Email to clear cache for, or null to clear all
385
+ */
386
+ clearProjectCache(email = null) {
387
+ clearProject(this.#projectCache, email);
388
+ }
389
+
390
+ /**
391
+ * Clear token cache for an account (useful on auth errors)
392
+ * @param {string|null} email - Email to clear cache for, or null to clear all
393
+ */
394
+ clearTokenCache(email = null) {
395
+ clearToken(this.#tokenCache, email);
396
+ }
397
+
398
+ /**
399
+ * Save current state to disk (async)
400
+ * @returns {Promise<void>}
401
+ */
402
+ async saveToDisk() {
403
+ await saveAccounts(this.#configPath, this.#accounts, this.#settings, this.#currentIndex);
404
+ }
405
+
406
+ /**
407
+ * Get status object for logging/API
408
+ * @returns {{accounts: Array, settings: Object}} Status object with accounts and settings
409
+ */
410
+ getStatus() {
411
+ const available = this.getAvailableAccounts();
412
+ const invalid = this.getInvalidAccounts();
413
+
414
+ // Count accounts that have any active model-specific rate limits
415
+ const rateLimited = this.#accounts.filter(a => {
416
+ if (!a.modelRateLimits) return false;
417
+ return Object.values(a.modelRateLimits).some(
418
+ limit => limit.isRateLimited && limit.resetTime > Date.now()
419
+ );
420
+ });
421
+
422
+ return {
423
+ total: this.#accounts.length,
424
+ available: available.length,
425
+ rateLimited: rateLimited.length,
426
+ invalid: invalid.length,
427
+ summary: `${this.#accounts.length} total, ${available.length} available, ${rateLimited.length} rate-limited, ${invalid.length} invalid`,
428
+ accounts: this.#accounts.map(a => ({
429
+ email: a.email,
430
+ source: a.source,
431
+ enabled: a.enabled !== false, // Default to true if undefined
432
+ projectId: a.projectId || null,
433
+ modelRateLimits: a.modelRateLimits || {},
434
+ isInvalid: a.isInvalid || false,
435
+ invalidReason: a.invalidReason || null,
436
+ lastUsed: a.lastUsed
437
+ }))
438
+ };
439
+ }
440
+
441
+ /**
442
+ * Get settings
443
+ * @returns {Object} Current settings object
444
+ */
445
+ getSettings() {
446
+ return { ...this.#settings };
447
+ }
448
+
449
+ /**
450
+ * Get all accounts (internal use for quota fetching)
451
+ * Returns the full account objects including credentials
452
+ * @returns {Array<Object>} Array of account objects
453
+ */
454
+ getAllAccounts() {
455
+ return this.#accounts;
456
+ }
457
+ }
458
+
459
+ // Re-export CooldownReason for use by handlers
460
+ export { CooldownReason };
461
+
462
+ export default AccountManager;
@@ -0,0 +1,112 @@
1
+ /**
2
+ * User Onboarding
3
+ *
4
+ * Handles provisioning of managed projects for accounts that don't have one.
5
+ */
6
+
7
+ import {
8
+ ONBOARD_USER_ENDPOINTS,
9
+ ANTIGRAVITY_HEADERS
10
+ } from '../constants.js';
11
+ import { logger } from '../utils/logger.js';
12
+ import { sleep } from '../utils/helpers.js';
13
+
14
+ /**
15
+ * Get the default tier ID from allowed tiers list
16
+ *
17
+ * @param {Array} allowedTiers - List of allowed tiers from loadCodeAssist
18
+ * @returns {string|undefined} Default tier ID
19
+ */
20
+ export function getDefaultTierId(allowedTiers) {
21
+ if (!allowedTiers || allowedTiers.length === 0) {
22
+ return undefined;
23
+ }
24
+
25
+ // Find the tier marked as default
26
+ for (const tier of allowedTiers) {
27
+ if (tier?.isDefault) {
28
+ return tier.id;
29
+ }
30
+ }
31
+
32
+ // Fall back to first tier
33
+ return allowedTiers[0]?.id;
34
+ }
35
+
36
+ /**
37
+ * Onboard a user to get a managed project
38
+ *
39
+ * @param {string} token - OAuth access token
40
+ * @param {string} tierId - Tier ID (raw API value, e.g., 'free-tier', 'standard-tier', 'g1-pro-tier')
41
+ * @param {string} [projectId] - Optional GCP project ID (required for non-free tiers)
42
+ * @param {number} [maxAttempts=10] - Maximum polling attempts
43
+ * @param {number} [delayMs=5000] - Delay between polling attempts
44
+ * @returns {Promise<string|null>} Managed project ID or null if failed
45
+ */
46
+ export async function onboardUser(token, tierId, projectId = undefined, maxAttempts = 10, delayMs = 5000) {
47
+ const metadata = {
48
+ ideType: 'IDE_UNSPECIFIED',
49
+ platform: 'PLATFORM_UNSPECIFIED',
50
+ pluginType: 'GEMINI'
51
+ };
52
+
53
+ if (projectId) {
54
+ metadata.duetProject = projectId;
55
+ }
56
+
57
+ const requestBody = {
58
+ tierId,
59
+ metadata
60
+ };
61
+ // Note: Do NOT add cloudaicompanionProject to requestBody
62
+ // Reference implementation only sets metadata.duetProject, not the body field
63
+ // Adding cloudaicompanionProject causes 400 errors for auto-provisioned tiers (g1-pro, g1-ultra)
64
+
65
+ logger.debug(`[Onboarding] Starting onboard with tierId: ${tierId}, projectId: ${projectId}`);
66
+
67
+ for (const endpoint of ONBOARD_USER_ENDPOINTS) {
68
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
69
+ try {
70
+ const response = await fetch(`${endpoint}/v1internal:onboardUser`, {
71
+ method: 'POST',
72
+ headers: {
73
+ 'Authorization': `Bearer ${token}`,
74
+ 'Content-Type': 'application/json',
75
+ ...ANTIGRAVITY_HEADERS
76
+ },
77
+ body: JSON.stringify(requestBody)
78
+ });
79
+
80
+ if (!response.ok) {
81
+ const errorText = await response.text();
82
+ logger.warn(`[Onboarding] onboardUser failed at ${endpoint}: ${response.status} - ${errorText}`);
83
+ break; // Try next endpoint
84
+ }
85
+
86
+ const data = await response.json();
87
+ logger.debug(`[Onboarding] onboardUser response (attempt ${attempt + 1}):`, JSON.stringify(data));
88
+
89
+ // Check if onboarding is complete
90
+ const managedProjectId = data.response?.cloudaicompanionProject?.id;
91
+ if (data.done && managedProjectId) {
92
+ return managedProjectId;
93
+ }
94
+ if (data.done && projectId) {
95
+ return projectId;
96
+ }
97
+
98
+ // Not done yet, wait and retry
99
+ if (attempt < maxAttempts - 1) {
100
+ logger.debug(`[Onboarding] onboardUser not complete, waiting ${delayMs}ms...`);
101
+ await sleep(delayMs);
102
+ }
103
+ } catch (error) {
104
+ logger.warn(`[Onboarding] onboardUser error at ${endpoint}:`, error.message);
105
+ break; // Try next endpoint
106
+ }
107
+ }
108
+ }
109
+
110
+ logger.warn(`[Onboarding] All onboarding attempts failed for tierId: ${tierId}`);
111
+ return null;
112
+ }