antigravity-claude-proxy 2.2.0 → 2.2.2
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.
- package/package.json +1 -1
- package/src/account-manager/credentials.js +175 -43
- package/src/account-manager/index.js +2 -1
- package/src/account-manager/onboarding.js +5 -10
- package/src/account-manager/strategies/index.js +2 -8
- package/src/auth/oauth.js +37 -3
- package/src/cli/accounts.js +7 -11
- package/src/cloudcode/model-api.js +1 -1
- package/src/constants.js +8 -0
- package/src/index.js +4 -1
- package/src/webui/index.js +2 -1
package/package.json
CHANGED
|
@@ -11,11 +11,47 @@ import {
|
|
|
11
11
|
LOAD_CODE_ASSIST_HEADERS,
|
|
12
12
|
DEFAULT_PROJECT_ID
|
|
13
13
|
} from '../constants.js';
|
|
14
|
-
import { refreshAccessToken } from '../auth/oauth.js';
|
|
14
|
+
import { refreshAccessToken, parseRefreshParts, formatRefreshParts } from '../auth/oauth.js';
|
|
15
15
|
import { getAuthStatus } from '../auth/database.js';
|
|
16
16
|
import { logger } from '../utils/logger.js';
|
|
17
17
|
import { isNetworkError } from '../utils/helpers.js';
|
|
18
18
|
import { onboardUser, getDefaultTierId } from './onboarding.js';
|
|
19
|
+
import { parseTierId } from '../cloudcode/model-api.js';
|
|
20
|
+
|
|
21
|
+
// Track accounts currently fetching subscription to avoid duplicate calls
|
|
22
|
+
const subscriptionFetchInProgress = new Set();
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Fetch subscription tier and save it (blocking)
|
|
26
|
+
* Used when we have a cached project but missing subscription data
|
|
27
|
+
*
|
|
28
|
+
* @param {string} token - OAuth access token
|
|
29
|
+
* @param {Object} account - Account object
|
|
30
|
+
* @param {Function} [onSave] - Callback to save account changes
|
|
31
|
+
*/
|
|
32
|
+
async function fetchAndSaveSubscription(token, account, onSave) {
|
|
33
|
+
// Avoid duplicate fetches for the same account
|
|
34
|
+
if (subscriptionFetchInProgress.has(account.email)) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
subscriptionFetchInProgress.add(account.email);
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
// Call discoverProject just to get subscription info
|
|
41
|
+
const { subscription } = await discoverProject(token, account.projectId);
|
|
42
|
+
if (subscription && subscription.tier !== 'unknown') {
|
|
43
|
+
account.subscription = subscription;
|
|
44
|
+
if (onSave) {
|
|
45
|
+
await onSave();
|
|
46
|
+
}
|
|
47
|
+
logger.info(`[AccountManager] Updated subscription tier for ${account.email}: ${subscription.tier}`);
|
|
48
|
+
}
|
|
49
|
+
} catch (e) {
|
|
50
|
+
logger.debug(`[AccountManager] Subscription fetch failed for ${account.email}: ${e.message}`);
|
|
51
|
+
} finally {
|
|
52
|
+
subscriptionFetchInProgress.delete(account.email);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
19
55
|
|
|
20
56
|
/**
|
|
21
57
|
* Get OAuth token for an account
|
|
@@ -82,27 +118,92 @@ export async function getTokenForAccount(account, tokenCache, onInvalid, onSave)
|
|
|
82
118
|
|
|
83
119
|
/**
|
|
84
120
|
* Get project ID for an account
|
|
121
|
+
* Aligned with opencode-antigravity-auth: parses refresh token for stored project IDs
|
|
85
122
|
*
|
|
86
123
|
* @param {Object} account - Account object
|
|
87
124
|
* @param {string} token - OAuth access token
|
|
88
125
|
* @param {Map} projectCache - Project cache map
|
|
126
|
+
* @param {Function} [onSave] - Callback to save account changes
|
|
89
127
|
* @returns {Promise<string>} Project ID
|
|
90
128
|
*/
|
|
91
|
-
export async function getProjectForAccount(account, token, projectCache) {
|
|
129
|
+
export async function getProjectForAccount(account, token, projectCache, onSave = null) {
|
|
92
130
|
// Check cache first
|
|
93
131
|
const cached = projectCache.get(account.email);
|
|
94
132
|
if (cached) {
|
|
95
133
|
return cached;
|
|
96
134
|
}
|
|
97
135
|
|
|
98
|
-
//
|
|
136
|
+
// Parse refresh token to get stored project IDs (aligned with opencode-antigravity-auth)
|
|
137
|
+
const parts = account.refreshToken ? parseRefreshParts(account.refreshToken) : { refreshToken: null, projectId: undefined, managedProjectId: undefined };
|
|
138
|
+
|
|
139
|
+
// If we have a managedProjectId in the refresh token, use it
|
|
140
|
+
if (parts.managedProjectId) {
|
|
141
|
+
projectCache.set(account.email, parts.managedProjectId);
|
|
142
|
+
// If subscription is missing/unknown, fetch it now (blocking)
|
|
143
|
+
if (!account.subscription || account.subscription.tier === 'unknown') {
|
|
144
|
+
await fetchAndSaveSubscription(token, account, onSave);
|
|
145
|
+
}
|
|
146
|
+
return parts.managedProjectId;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Legacy: check account.projectId for backward compatibility
|
|
99
150
|
if (account.projectId) {
|
|
100
151
|
projectCache.set(account.email, account.projectId);
|
|
152
|
+
// If subscription is missing/unknown, fetch it now (blocking)
|
|
153
|
+
if (!account.subscription || account.subscription.tier === 'unknown') {
|
|
154
|
+
await fetchAndSaveSubscription(token, account, onSave);
|
|
155
|
+
}
|
|
101
156
|
return account.projectId;
|
|
102
157
|
}
|
|
103
158
|
|
|
104
|
-
// Discover project
|
|
105
|
-
|
|
159
|
+
// Discover managed project, passing projectId for metadata.duetProject
|
|
160
|
+
// Reference: opencode-antigravity-auth - discoverProject handles fallback internally
|
|
161
|
+
const { project, subscription } = await discoverProject(token, parts.projectId);
|
|
162
|
+
|
|
163
|
+
// Store managedProjectId back in refresh token (if we got a real project)
|
|
164
|
+
if (project && project !== DEFAULT_PROJECT_ID) {
|
|
165
|
+
let needsSave = false;
|
|
166
|
+
|
|
167
|
+
if (account.refreshToken) {
|
|
168
|
+
// OAuth accounts: encode in refresh token
|
|
169
|
+
account.refreshToken = formatRefreshParts({
|
|
170
|
+
refreshToken: parts.refreshToken,
|
|
171
|
+
projectId: parts.projectId,
|
|
172
|
+
managedProjectId: project,
|
|
173
|
+
});
|
|
174
|
+
needsSave = true;
|
|
175
|
+
} else if (account.source === 'database' || account.source === 'manual') {
|
|
176
|
+
// Database/manual accounts: store in projectId field
|
|
177
|
+
account.projectId = project;
|
|
178
|
+
needsSave = true;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Save subscription tier if discovered
|
|
182
|
+
if (subscription) {
|
|
183
|
+
account.subscription = subscription;
|
|
184
|
+
needsSave = true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Trigger save to persist the updated project and subscription
|
|
188
|
+
if (needsSave && onSave) {
|
|
189
|
+
try {
|
|
190
|
+
await onSave();
|
|
191
|
+
} catch (e) {
|
|
192
|
+
logger.warn(`[AccountManager] Failed to save updated project: ${e.message}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
} else if (subscription) {
|
|
196
|
+
// Even if no project discovered, save subscription if we got it
|
|
197
|
+
account.subscription = subscription;
|
|
198
|
+
if (onSave) {
|
|
199
|
+
try {
|
|
200
|
+
await onSave();
|
|
201
|
+
} catch (e) {
|
|
202
|
+
logger.warn(`[AccountManager] Failed to save subscription: ${e.message}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
106
207
|
projectCache.set(account.email, project);
|
|
107
208
|
return project;
|
|
108
209
|
}
|
|
@@ -111,13 +212,23 @@ export async function getProjectForAccount(account, token, projectCache) {
|
|
|
111
212
|
* Discover project ID via Cloud Code API
|
|
112
213
|
*
|
|
113
214
|
* @param {string} token - OAuth access token
|
|
114
|
-
* @
|
|
215
|
+
* @param {string} [projectId] - Optional project ID from refresh token (for metadata.duetProject)
|
|
216
|
+
* @returns {Promise<{project: string, subscription: {tier: string, projectId: string|null, detectedAt: string}|null}>} Project and subscription info
|
|
115
217
|
*/
|
|
116
|
-
export async function discoverProject(token) {
|
|
218
|
+
export async function discoverProject(token, projectId = undefined) {
|
|
117
219
|
let lastError = null;
|
|
118
220
|
let gotSuccessfulResponse = false;
|
|
119
221
|
let loadCodeAssistData = null;
|
|
120
222
|
|
|
223
|
+
const metadata = {
|
|
224
|
+
ideType: 'IDE_UNSPECIFIED',
|
|
225
|
+
platform: 'PLATFORM_UNSPECIFIED',
|
|
226
|
+
pluginType: 'GEMINI'
|
|
227
|
+
};
|
|
228
|
+
if (projectId) {
|
|
229
|
+
metadata.duetProject = projectId;
|
|
230
|
+
}
|
|
231
|
+
|
|
121
232
|
for (const endpoint of LOAD_CODE_ASSIST_ENDPOINTS) {
|
|
122
233
|
try {
|
|
123
234
|
const response = await fetch(`${endpoint}/v1internal:loadCodeAssist`, {
|
|
@@ -127,14 +238,7 @@ export async function discoverProject(token) {
|
|
|
127
238
|
'Content-Type': 'application/json',
|
|
128
239
|
...LOAD_CODE_ASSIST_HEADERS
|
|
129
240
|
},
|
|
130
|
-
body: JSON.stringify({
|
|
131
|
-
metadata: {
|
|
132
|
-
ideType: 'IDE_UNSPECIFIED',
|
|
133
|
-
platform: 'PLATFORM_UNSPECIFIED',
|
|
134
|
-
pluginType: 'GEMINI',
|
|
135
|
-
duetProject: DEFAULT_PROJECT_ID
|
|
136
|
-
}
|
|
137
|
-
})
|
|
241
|
+
body: JSON.stringify({ metadata })
|
|
138
242
|
});
|
|
139
243
|
|
|
140
244
|
if (!response.ok) {
|
|
@@ -150,13 +254,16 @@ export async function discoverProject(token) {
|
|
|
150
254
|
|
|
151
255
|
logger.debug(`[AccountManager] loadCodeAssist response from ${endpoint}:`, JSON.stringify(data));
|
|
152
256
|
|
|
257
|
+
// Extract subscription tier from response
|
|
258
|
+
const subscription = extractSubscriptionFromResponse(data);
|
|
259
|
+
|
|
153
260
|
if (typeof data.cloudaicompanionProject === 'string') {
|
|
154
261
|
logger.success(`[AccountManager] Discovered project: ${data.cloudaicompanionProject}`);
|
|
155
|
-
return data.cloudaicompanionProject;
|
|
262
|
+
return { project: data.cloudaicompanionProject, subscription };
|
|
156
263
|
}
|
|
157
264
|
if (data.cloudaicompanionProject?.id) {
|
|
158
265
|
logger.success(`[AccountManager] Discovered project: ${data.cloudaicompanionProject.id}`);
|
|
159
|
-
return data.cloudaicompanionProject.id;
|
|
266
|
+
return { project: data.cloudaicompanionProject.id, subscription };
|
|
160
267
|
}
|
|
161
268
|
|
|
162
269
|
// No project found - log tier data and try to onboard the user
|
|
@@ -171,47 +278,72 @@ export async function discoverProject(token) {
|
|
|
171
278
|
|
|
172
279
|
// If we got a successful response but no project, try onboarding
|
|
173
280
|
if (gotSuccessfulResponse && loadCodeAssistData) {
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
tierId = loadCodeAssistData.paidTier.id;
|
|
180
|
-
tierSource = 'paidTier';
|
|
181
|
-
} else if (loadCodeAssistData.currentTier?.id) {
|
|
182
|
-
tierId = loadCodeAssistData.currentTier.id;
|
|
183
|
-
tierSource = 'currentTier';
|
|
184
|
-
} else {
|
|
185
|
-
tierId = getDefaultTierId(loadCodeAssistData.allowedTiers);
|
|
186
|
-
tierSource = 'allowedTiers';
|
|
187
|
-
}
|
|
281
|
+
// Only use allowedTiers for onboarding (matching opencode-antigravity-auth and oauth.js)
|
|
282
|
+
// Note: paidTier (g1-pro-tier, g1-ultra-tier) is NOT valid for onboardUser API
|
|
283
|
+
// The paidTier is used for subscription detection only, not for onboarding
|
|
284
|
+
const tierId = getDefaultTierId(loadCodeAssistData.allowedTiers) || 'free-tier';
|
|
285
|
+
logger.info(`[AccountManager] Onboarding user with tier: ${tierId}`);
|
|
188
286
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
// Check if this is a free tier (raw API values contain 'free')
|
|
193
|
-
const isFree = tierId.toLowerCase().includes('free');
|
|
194
|
-
|
|
195
|
-
// For non-free tiers, pass DEFAULT_PROJECT_ID as the GCP project
|
|
196
|
-
// The API requires a project for paid tier onboarding
|
|
287
|
+
// Pass projectId for metadata.duetProject (without fallback, matching reference)
|
|
288
|
+
// Reference: opencode-antigravity-auth passes parts.projectId (not fallback) to onboardManagedProject
|
|
197
289
|
const onboardedProject = await onboardUser(
|
|
198
290
|
token,
|
|
199
291
|
tierId,
|
|
200
|
-
|
|
292
|
+
projectId // Original projectId without fallback
|
|
201
293
|
);
|
|
202
294
|
if (onboardedProject) {
|
|
203
295
|
logger.success(`[AccountManager] Successfully onboarded, project: ${onboardedProject}`);
|
|
204
|
-
|
|
296
|
+
const subscription = extractSubscriptionFromResponse(loadCodeAssistData);
|
|
297
|
+
return { project: onboardedProject, subscription };
|
|
205
298
|
}
|
|
206
299
|
|
|
207
|
-
logger.warn(`[AccountManager] Onboarding failed
|
|
300
|
+
logger.warn(`[AccountManager] Onboarding failed - account may not work correctly`);
|
|
208
301
|
}
|
|
209
302
|
|
|
210
303
|
// Only warn if all endpoints failed with errors (not just missing project)
|
|
211
304
|
if (!gotSuccessfulResponse) {
|
|
212
305
|
logger.warn(`[AccountManager] loadCodeAssist failed for all endpoints: ${lastError}`);
|
|
213
306
|
}
|
|
214
|
-
|
|
307
|
+
|
|
308
|
+
// Fallback: use projectId if available, otherwise use default
|
|
309
|
+
// Reference: opencode-antigravity-auth/src/plugin/project.ts
|
|
310
|
+
if (projectId) {
|
|
311
|
+
return { project: projectId, subscription: null };
|
|
312
|
+
}
|
|
313
|
+
return { project: DEFAULT_PROJECT_ID, subscription: null };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Extract subscription tier from loadCodeAssist response
|
|
318
|
+
*
|
|
319
|
+
* @param {Object} data - loadCodeAssist response data
|
|
320
|
+
* @returns {{tier: string, projectId: string|null, detectedAt: string}|null} Subscription info
|
|
321
|
+
*/
|
|
322
|
+
function extractSubscriptionFromResponse(data) {
|
|
323
|
+
if (!data) return null;
|
|
324
|
+
|
|
325
|
+
// Priority: paidTier > currentTier (consistent with model-api.js)
|
|
326
|
+
let tier = 'free';
|
|
327
|
+
let cloudProject = null;
|
|
328
|
+
|
|
329
|
+
if (data.paidTier?.id) {
|
|
330
|
+
tier = parseTierId(data.paidTier.id);
|
|
331
|
+
} else if (data.currentTier?.id) {
|
|
332
|
+
tier = parseTierId(data.currentTier.id);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Get project ID
|
|
336
|
+
if (typeof data.cloudaicompanionProject === 'string') {
|
|
337
|
+
cloudProject = data.cloudaicompanionProject;
|
|
338
|
+
} else if (data.cloudaicompanionProject?.id) {
|
|
339
|
+
cloudProject = data.cloudaicompanionProject.id;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
tier,
|
|
344
|
+
projectId: cloudProject,
|
|
345
|
+
detectedAt: new Date().toISOString()
|
|
346
|
+
};
|
|
215
347
|
}
|
|
216
348
|
|
|
217
349
|
/**
|
|
@@ -297,7 +297,8 @@ export class AccountManager {
|
|
|
297
297
|
* @returns {Promise<string>} Project ID
|
|
298
298
|
*/
|
|
299
299
|
async getProjectForAccount(account, token) {
|
|
300
|
-
|
|
300
|
+
// Pass onSave callback to persist managedProjectId in refresh token
|
|
301
|
+
return fetchProject(account, token, this.#projectCache, () => this.saveToDisk());
|
|
301
302
|
}
|
|
302
303
|
|
|
303
304
|
/**
|
|
@@ -43,7 +43,7 @@ export function getDefaultTierId(allowedTiers) {
|
|
|
43
43
|
* @param {number} [delayMs=5000] - Delay between polling attempts
|
|
44
44
|
* @returns {Promise<string|null>} Managed project ID or null if failed
|
|
45
45
|
*/
|
|
46
|
-
export async function onboardUser(token, tierId, projectId =
|
|
46
|
+
export async function onboardUser(token, tierId, projectId = undefined, maxAttempts = 10, delayMs = 5000) {
|
|
47
47
|
const metadata = {
|
|
48
48
|
ideType: 'IDE_UNSPECIFIED',
|
|
49
49
|
platform: 'PLATFORM_UNSPECIFIED',
|
|
@@ -58,16 +58,11 @@ export async function onboardUser(token, tierId, projectId = null, maxAttempts =
|
|
|
58
58
|
tierId,
|
|
59
59
|
metadata
|
|
60
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)
|
|
61
64
|
|
|
62
|
-
|
|
63
|
-
const isFree = tierId.toLowerCase().includes('free');
|
|
64
|
-
|
|
65
|
-
// Non-free tiers require a cloudaicompanionProject
|
|
66
|
-
if (!isFree && projectId) {
|
|
67
|
-
requestBody.cloudaicompanionProject = projectId;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
logger.debug(`[Onboarding] Starting onboard with tierId: ${tierId}, projectId: ${projectId}, isFree: ${isFree}`);
|
|
65
|
+
logger.debug(`[Onboarding] Starting onboard with tierId: ${tierId}, projectId: ${projectId}`);
|
|
71
66
|
|
|
72
67
|
for (const endpoint of ONBOARD_USER_ENDPOINTS) {
|
|
73
68
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
@@ -10,20 +10,14 @@ import { HybridStrategy } from './hybrid-strategy.js';
|
|
|
10
10
|
import { logger } from '../../utils/logger.js';
|
|
11
11
|
import {
|
|
12
12
|
SELECTION_STRATEGIES,
|
|
13
|
-
DEFAULT_SELECTION_STRATEGY
|
|
13
|
+
DEFAULT_SELECTION_STRATEGY,
|
|
14
|
+
STRATEGY_LABELS
|
|
14
15
|
} from '../../constants.js';
|
|
15
16
|
|
|
16
17
|
// Re-export strategy constants for convenience
|
|
17
18
|
export const STRATEGY_NAMES = SELECTION_STRATEGIES;
|
|
18
19
|
export const DEFAULT_STRATEGY = DEFAULT_SELECTION_STRATEGY;
|
|
19
20
|
|
|
20
|
-
// Strategy display labels
|
|
21
|
-
export const STRATEGY_LABELS = {
|
|
22
|
-
'sticky': 'Sticky (Cache Optimized)',
|
|
23
|
-
'round-robin': 'Round Robin (Load Balanced)',
|
|
24
|
-
'hybrid': 'Hybrid (Smart Distribution)'
|
|
25
|
-
};
|
|
26
|
-
|
|
27
21
|
/**
|
|
28
22
|
* Create a strategy instance
|
|
29
23
|
* @param {string} strategyName - Name of the strategy ('sticky', 'round-robin', 'hybrid')
|
package/src/auth/oauth.js
CHANGED
|
@@ -17,6 +17,34 @@ import {
|
|
|
17
17
|
import { logger } from '../utils/logger.js';
|
|
18
18
|
import { onboardUser, getDefaultTierId } from '../account-manager/onboarding.js';
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Parse refresh token parts (aligned with opencode-antigravity-auth)
|
|
22
|
+
* Format: refreshToken|projectId|managedProjectId
|
|
23
|
+
*
|
|
24
|
+
* @param {string} refresh - Composite refresh token string
|
|
25
|
+
* @returns {{refreshToken: string, projectId: string|undefined, managedProjectId: string|undefined}}
|
|
26
|
+
*/
|
|
27
|
+
export function parseRefreshParts(refresh) {
|
|
28
|
+
const [refreshToken = '', projectId = '', managedProjectId = ''] = (refresh ?? '').split('|');
|
|
29
|
+
return {
|
|
30
|
+
refreshToken,
|
|
31
|
+
projectId: projectId || undefined,
|
|
32
|
+
managedProjectId: managedProjectId || undefined,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Format refresh token parts back into composite string
|
|
38
|
+
*
|
|
39
|
+
* @param {{refreshToken: string, projectId?: string|undefined, managedProjectId?: string|undefined}} parts
|
|
40
|
+
* @returns {string} Composite refresh token
|
|
41
|
+
*/
|
|
42
|
+
export function formatRefreshParts(parts) {
|
|
43
|
+
const projectSegment = parts.projectId ?? '';
|
|
44
|
+
const base = `${parts.refreshToken}|${projectSegment}`;
|
|
45
|
+
return parts.managedProjectId ? `${base}|${parts.managedProjectId}` : base;
|
|
46
|
+
}
|
|
47
|
+
|
|
20
48
|
/**
|
|
21
49
|
* Generate PKCE code verifier and challenge
|
|
22
50
|
*/
|
|
@@ -267,11 +295,15 @@ export async function exchangeCode(code, verifier) {
|
|
|
267
295
|
|
|
268
296
|
/**
|
|
269
297
|
* Refresh access token using refresh token
|
|
298
|
+
* Handles composite refresh tokens (refreshToken|projectId|managedProjectId)
|
|
270
299
|
*
|
|
271
|
-
* @param {string}
|
|
300
|
+
* @param {string} compositeRefresh - OAuth refresh token (may be composite)
|
|
272
301
|
* @returns {Promise<{accessToken: string, expiresIn: number}>} New access token
|
|
273
302
|
*/
|
|
274
|
-
export async function refreshAccessToken(
|
|
303
|
+
export async function refreshAccessToken(compositeRefresh) {
|
|
304
|
+
// Parse the composite refresh token to extract the actual OAuth token
|
|
305
|
+
const parts = parseRefreshParts(compositeRefresh);
|
|
306
|
+
|
|
275
307
|
const response = await fetch(OAUTH_CONFIG.tokenUrl, {
|
|
276
308
|
method: 'POST',
|
|
277
309
|
headers: {
|
|
@@ -280,7 +312,7 @@ export async function refreshAccessToken(refreshToken) {
|
|
|
280
312
|
body: new URLSearchParams({
|
|
281
313
|
client_id: OAUTH_CONFIG.clientId,
|
|
282
314
|
client_secret: OAUTH_CONFIG.clientSecret,
|
|
283
|
-
refresh_token: refreshToken,
|
|
315
|
+
refresh_token: parts.refreshToken, // Use the actual OAuth token
|
|
284
316
|
grant_type: 'refresh_token'
|
|
285
317
|
})
|
|
286
318
|
});
|
|
@@ -408,6 +440,8 @@ export async function completeOAuthFlow(code, verifier) {
|
|
|
408
440
|
}
|
|
409
441
|
|
|
410
442
|
export default {
|
|
443
|
+
parseRefreshParts,
|
|
444
|
+
formatRefreshParts,
|
|
411
445
|
getAuthorizationUrl,
|
|
412
446
|
extractCodeFromInput,
|
|
413
447
|
startCallbackServer,
|
package/src/cli/accounts.js
CHANGED
|
@@ -95,7 +95,7 @@ function openBrowser(url) {
|
|
|
95
95
|
args = [url];
|
|
96
96
|
} else if (platform === 'win32') {
|
|
97
97
|
command = 'cmd';
|
|
98
|
-
args = ['/c', 'start', '', url];
|
|
98
|
+
args = ['/c', 'start', '', url.replace(/&/g, '^&')];
|
|
99
99
|
} else {
|
|
100
100
|
command = 'xdg-open';
|
|
101
101
|
args = [url];
|
|
@@ -210,20 +210,18 @@ async function addAccount(existingAccounts) {
|
|
|
210
210
|
if (existing) {
|
|
211
211
|
console.log(`\n⚠ Account ${result.email} already exists. Updating tokens.`);
|
|
212
212
|
existing.refreshToken = result.refreshToken;
|
|
213
|
-
|
|
213
|
+
// Note: projectId will be discovered and stored in refresh token on first use
|
|
214
214
|
existing.addedAt = new Date().toISOString();
|
|
215
215
|
return null; // Don't add duplicate
|
|
216
216
|
}
|
|
217
217
|
|
|
218
218
|
console.log(`\n✓ Successfully authenticated: ${result.email}`);
|
|
219
|
-
|
|
220
|
-
console.log(` Project ID: ${result.projectId}`);
|
|
221
|
-
}
|
|
219
|
+
console.log(' Project will be discovered on first API request.');
|
|
222
220
|
|
|
223
221
|
return {
|
|
224
222
|
email: result.email,
|
|
225
223
|
refreshToken: result.refreshToken,
|
|
226
|
-
|
|
224
|
+
// Note: projectId stored in refresh token, not as separate field
|
|
227
225
|
addedAt: new Date().toISOString(),
|
|
228
226
|
modelRateLimits: {}
|
|
229
227
|
};
|
|
@@ -267,20 +265,18 @@ async function addAccountNoBrowser(existingAccounts, rl) {
|
|
|
267
265
|
if (existing) {
|
|
268
266
|
console.log(`\n⚠ Account ${result.email} already exists. Updating tokens.`);
|
|
269
267
|
existing.refreshToken = result.refreshToken;
|
|
270
|
-
|
|
268
|
+
// Note: projectId will be discovered and stored in refresh token on first use
|
|
271
269
|
existing.addedAt = new Date().toISOString();
|
|
272
270
|
return null; // Don't add duplicate
|
|
273
271
|
}
|
|
274
272
|
|
|
275
273
|
console.log(`\n✓ Successfully authenticated: ${result.email}`);
|
|
276
|
-
|
|
277
|
-
console.log(` Project ID: ${result.projectId}`);
|
|
278
|
-
}
|
|
274
|
+
console.log(' Project will be discovered on first API request.');
|
|
279
275
|
|
|
280
276
|
return {
|
|
281
277
|
email: result.email,
|
|
282
278
|
refreshToken: result.refreshToken,
|
|
283
|
-
|
|
279
|
+
// Note: projectId stored in refresh token, not as separate field
|
|
284
280
|
addedAt: new Date().toISOString(),
|
|
285
281
|
modelRateLimits: {}
|
|
286
282
|
};
|
|
@@ -128,7 +128,7 @@ export async function getModelQuotas(token, projectId = null) {
|
|
|
128
128
|
* @param {string} tierId - The tier ID from the API
|
|
129
129
|
* @returns {'free' | 'pro' | 'ultra' | 'unknown'} The subscription tier
|
|
130
130
|
*/
|
|
131
|
-
function parseTierId(tierId) {
|
|
131
|
+
export function parseTierId(tierId) {
|
|
132
132
|
if (!tierId) return 'unknown';
|
|
133
133
|
const lower = tierId.toLowerCase();
|
|
134
134
|
|
package/src/constants.js
CHANGED
|
@@ -121,6 +121,13 @@ export const MIN_SIGNATURE_LENGTH = 50; // Minimum valid thinking signature leng
|
|
|
121
121
|
export const SELECTION_STRATEGIES = ['sticky', 'round-robin', 'hybrid'];
|
|
122
122
|
export const DEFAULT_SELECTION_STRATEGY = 'hybrid';
|
|
123
123
|
|
|
124
|
+
// Strategy display labels
|
|
125
|
+
export const STRATEGY_LABELS = {
|
|
126
|
+
'sticky': 'Sticky (Cache Optimized)',
|
|
127
|
+
'round-robin': 'Round Robin (Load Balanced)',
|
|
128
|
+
'hybrid': 'Hybrid (Smart Distribution)'
|
|
129
|
+
};
|
|
130
|
+
|
|
124
131
|
// Gemini-specific limits
|
|
125
132
|
export const GEMINI_MAX_OUTPUT_TOKENS = 16384;
|
|
126
133
|
|
|
@@ -263,6 +270,7 @@ export default {
|
|
|
263
270
|
isThinkingModel,
|
|
264
271
|
OAUTH_CONFIG,
|
|
265
272
|
OAUTH_REDIRECT_URI,
|
|
273
|
+
STRATEGY_LABELS,
|
|
266
274
|
MODEL_FALLBACK_MAP,
|
|
267
275
|
TEST_MODELS,
|
|
268
276
|
DEFAULT_PRESETS,
|
package/src/index.js
CHANGED
|
@@ -60,8 +60,11 @@ const server = app.listen(PORT, () => {
|
|
|
60
60
|
const align4 = (text) => text + ' '.repeat(Math.max(0, 58 - text.length));
|
|
61
61
|
|
|
62
62
|
// Build Control section dynamically
|
|
63
|
+
const strategyOptions = `(${STRATEGY_NAMES.join('/')})`;
|
|
64
|
+
const strategyLine2 = ' ' + strategyOptions;
|
|
63
65
|
let controlSection = '║ Control: ║\n';
|
|
64
|
-
controlSection += '║ --strategy=<s> Set selection strategy
|
|
66
|
+
controlSection += '║ --strategy=<s> Set account selection strategy ║\n';
|
|
67
|
+
controlSection += `${border} ${align(strategyLine2)}${border}\n`;
|
|
65
68
|
if (!isDebug) {
|
|
66
69
|
controlSection += '║ --debug Enable debug logging ║\n';
|
|
67
70
|
}
|
package/src/webui/index.js
CHANGED
|
@@ -668,10 +668,11 @@ export function mountWebUI(app, dirname, accountManager) {
|
|
|
668
668
|
const accountData = await completeOAuthFlow(code, verifier);
|
|
669
669
|
|
|
670
670
|
// Add or update the account
|
|
671
|
+
// Note: Don't set projectId here - it will be discovered and stored
|
|
672
|
+
// in the refresh token via getProjectForAccount() on first use
|
|
671
673
|
await addAccount({
|
|
672
674
|
email: accountData.email,
|
|
673
675
|
refreshToken: accountData.refreshToken,
|
|
674
|
-
projectId: accountData.projectId,
|
|
675
676
|
source: 'oauth'
|
|
676
677
|
});
|
|
677
678
|
|