antigravity-claude-proxy 1.1.5 → 1.2.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.
- package/package.json +6 -6
- package/src/account-manager/credentials.js +171 -0
- package/src/account-manager/index.js +293 -0
- package/src/account-manager/rate-limits.js +157 -0
- package/src/account-manager/selection.js +169 -0
- package/src/account-manager/storage.js +128 -0
- package/src/{oauth.js → auth/oauth.js} +8 -7
- package/src/{token-extractor.js → auth/token-extractor.js} +7 -6
- package/src/{accounts-cli.js → cli/accounts.js} +11 -11
- package/src/cloudcode/index.js +28 -0
- package/src/cloudcode/message-handler.js +209 -0
- package/src/cloudcode/model-api.js +97 -0
- package/src/cloudcode/rate-limit-parser.js +181 -0
- package/src/cloudcode/request-builder.js +68 -0
- package/src/cloudcode/session-manager.js +47 -0
- package/src/cloudcode/sse-parser.js +116 -0
- package/src/cloudcode/sse-streamer.js +285 -0
- package/src/cloudcode/streaming-handler.js +199 -0
- package/src/format/content-converter.js +2 -1
- package/src/format/request-converter.js +9 -7
- package/src/format/thinking-utils.js +8 -7
- package/src/index.js +37 -4
- package/src/server.js +28 -17
- package/src/utils/logger.js +114 -0
- package/src/account-manager.js +0 -623
- package/src/cloudcode-client.js +0 -1044
- /package/src/{db → auth}/database.js +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "antigravity-claude-proxy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Proxy server to use Antigravity's Claude models with Claude Code CLI",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
"scripts": {
|
|
15
15
|
"start": "node src/index.js",
|
|
16
16
|
"dev": "node --watch src/index.js",
|
|
17
|
-
"accounts": "node src/accounts
|
|
18
|
-
"accounts:add": "node src/accounts
|
|
19
|
-
"accounts:list": "node src/accounts
|
|
20
|
-
"accounts:remove": "node src/accounts
|
|
21
|
-
"accounts:verify": "node src/accounts
|
|
17
|
+
"accounts": "node src/cli/accounts.js",
|
|
18
|
+
"accounts:add": "node src/cli/accounts.js add",
|
|
19
|
+
"accounts:list": "node src/cli/accounts.js list",
|
|
20
|
+
"accounts:remove": "node src/cli/accounts.js remove",
|
|
21
|
+
"accounts:verify": "node src/cli/accounts.js verify",
|
|
22
22
|
"test": "node tests/run-all.cjs",
|
|
23
23
|
"test:signatures": "node tests/test-thinking-signatures.cjs",
|
|
24
24
|
"test:multiturn": "node tests/test-multiturn-thinking-tools.cjs",
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credentials Management
|
|
3
|
+
*
|
|
4
|
+
* Handles OAuth token handling and project discovery.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
ANTIGRAVITY_DB_PATH,
|
|
9
|
+
TOKEN_REFRESH_INTERVAL_MS,
|
|
10
|
+
ANTIGRAVITY_ENDPOINT_FALLBACKS,
|
|
11
|
+
ANTIGRAVITY_HEADERS,
|
|
12
|
+
DEFAULT_PROJECT_ID
|
|
13
|
+
} from '../constants.js';
|
|
14
|
+
import { refreshAccessToken } from '../auth/oauth.js';
|
|
15
|
+
import { getAuthStatus } from '../auth/database.js';
|
|
16
|
+
import { logger } from '../utils/logger.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get OAuth token for an account
|
|
20
|
+
*
|
|
21
|
+
* @param {Object} account - Account object with email and credentials
|
|
22
|
+
* @param {Map} tokenCache - Token cache map
|
|
23
|
+
* @param {Function} onInvalid - Callback when account is invalid (email, reason)
|
|
24
|
+
* @param {Function} onSave - Callback to save changes
|
|
25
|
+
* @returns {Promise<string>} OAuth access token
|
|
26
|
+
* @throws {Error} If token refresh fails
|
|
27
|
+
*/
|
|
28
|
+
export async function getTokenForAccount(account, tokenCache, onInvalid, onSave) {
|
|
29
|
+
// Check cache first
|
|
30
|
+
const cached = tokenCache.get(account.email);
|
|
31
|
+
if (cached && (Date.now() - cached.extractedAt) < TOKEN_REFRESH_INTERVAL_MS) {
|
|
32
|
+
return cached.token;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Get fresh token based on source
|
|
36
|
+
let token;
|
|
37
|
+
|
|
38
|
+
if (account.source === 'oauth' && account.refreshToken) {
|
|
39
|
+
// OAuth account - use refresh token to get new access token
|
|
40
|
+
try {
|
|
41
|
+
const tokens = await refreshAccessToken(account.refreshToken);
|
|
42
|
+
token = tokens.accessToken;
|
|
43
|
+
// Clear invalid flag on success
|
|
44
|
+
if (account.isInvalid) {
|
|
45
|
+
account.isInvalid = false;
|
|
46
|
+
account.invalidReason = null;
|
|
47
|
+
if (onSave) await onSave();
|
|
48
|
+
}
|
|
49
|
+
logger.success(`[AccountManager] Refreshed OAuth token for: ${account.email}`);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
logger.error(`[AccountManager] Failed to refresh token for ${account.email}:`, error.message);
|
|
52
|
+
// Mark account as invalid (credentials need re-auth)
|
|
53
|
+
if (onInvalid) onInvalid(account.email, error.message);
|
|
54
|
+
throw new Error(`AUTH_INVALID: ${account.email}: ${error.message}`);
|
|
55
|
+
}
|
|
56
|
+
} else if (account.source === 'manual' && account.apiKey) {
|
|
57
|
+
token = account.apiKey;
|
|
58
|
+
} else {
|
|
59
|
+
// Extract from database
|
|
60
|
+
const dbPath = account.dbPath || ANTIGRAVITY_DB_PATH;
|
|
61
|
+
const authData = getAuthStatus(dbPath);
|
|
62
|
+
token = authData.apiKey;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Cache the token
|
|
66
|
+
tokenCache.set(account.email, {
|
|
67
|
+
token,
|
|
68
|
+
extractedAt: Date.now()
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return token;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get project ID for an account
|
|
76
|
+
*
|
|
77
|
+
* @param {Object} account - Account object
|
|
78
|
+
* @param {string} token - OAuth access token
|
|
79
|
+
* @param {Map} projectCache - Project cache map
|
|
80
|
+
* @returns {Promise<string>} Project ID
|
|
81
|
+
*/
|
|
82
|
+
export async function getProjectForAccount(account, token, projectCache) {
|
|
83
|
+
// Check cache first
|
|
84
|
+
const cached = projectCache.get(account.email);
|
|
85
|
+
if (cached) {
|
|
86
|
+
return cached;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// OAuth or manual accounts may have projectId specified
|
|
90
|
+
if (account.projectId) {
|
|
91
|
+
projectCache.set(account.email, account.projectId);
|
|
92
|
+
return account.projectId;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Discover project via loadCodeAssist API
|
|
96
|
+
const project = await discoverProject(token);
|
|
97
|
+
projectCache.set(account.email, project);
|
|
98
|
+
return project;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Discover project ID via Cloud Code API
|
|
103
|
+
*
|
|
104
|
+
* @param {string} token - OAuth access token
|
|
105
|
+
* @returns {Promise<string>} Project ID
|
|
106
|
+
*/
|
|
107
|
+
export async function discoverProject(token) {
|
|
108
|
+
for (const endpoint of ANTIGRAVITY_ENDPOINT_FALLBACKS) {
|
|
109
|
+
try {
|
|
110
|
+
const response = await fetch(`${endpoint}/v1internal:loadCodeAssist`, {
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers: {
|
|
113
|
+
'Authorization': `Bearer ${token}`,
|
|
114
|
+
'Content-Type': 'application/json',
|
|
115
|
+
...ANTIGRAVITY_HEADERS
|
|
116
|
+
},
|
|
117
|
+
body: JSON.stringify({
|
|
118
|
+
metadata: {
|
|
119
|
+
ideType: 'IDE_UNSPECIFIED',
|
|
120
|
+
platform: 'PLATFORM_UNSPECIFIED',
|
|
121
|
+
pluginType: 'GEMINI'
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (!response.ok) continue;
|
|
127
|
+
|
|
128
|
+
const data = await response.json();
|
|
129
|
+
|
|
130
|
+
if (typeof data.cloudaicompanionProject === 'string') {
|
|
131
|
+
return data.cloudaicompanionProject;
|
|
132
|
+
}
|
|
133
|
+
if (data.cloudaicompanionProject?.id) {
|
|
134
|
+
return data.cloudaicompanionProject.id;
|
|
135
|
+
}
|
|
136
|
+
} catch (error) {
|
|
137
|
+
logger.warn(`[AccountManager] Project discovery failed at ${endpoint}:`, error.message);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
logger.info(`[AccountManager] Using default project: ${DEFAULT_PROJECT_ID}`);
|
|
142
|
+
return DEFAULT_PROJECT_ID;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Clear project cache for an account
|
|
147
|
+
*
|
|
148
|
+
* @param {Map} projectCache - Project cache map
|
|
149
|
+
* @param {string|null} email - Email to clear cache for, or null to clear all
|
|
150
|
+
*/
|
|
151
|
+
export function clearProjectCache(projectCache, email = null) {
|
|
152
|
+
if (email) {
|
|
153
|
+
projectCache.delete(email);
|
|
154
|
+
} else {
|
|
155
|
+
projectCache.clear();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Clear token cache for an account
|
|
161
|
+
*
|
|
162
|
+
* @param {Map} tokenCache - Token cache map
|
|
163
|
+
* @param {string|null} email - Email to clear cache for, or null to clear all
|
|
164
|
+
*/
|
|
165
|
+
export function clearTokenCache(tokenCache, email = null) {
|
|
166
|
+
if (email) {
|
|
167
|
+
tokenCache.delete(email);
|
|
168
|
+
} else {
|
|
169
|
+
tokenCache.clear();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Account Manager
|
|
3
|
+
* Manages multiple Antigravity accounts with sticky selection,
|
|
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
|
+
} from './rate-limits.js';
|
|
19
|
+
import {
|
|
20
|
+
getTokenForAccount as fetchToken,
|
|
21
|
+
getProjectForAccount as fetchProject,
|
|
22
|
+
clearProjectCache as clearProject,
|
|
23
|
+
clearTokenCache as clearToken
|
|
24
|
+
} from './credentials.js';
|
|
25
|
+
import {
|
|
26
|
+
pickNext as selectNext,
|
|
27
|
+
getCurrentStickyAccount as getSticky,
|
|
28
|
+
shouldWaitForCurrentAccount as shouldWait,
|
|
29
|
+
pickStickyAccount as selectSticky
|
|
30
|
+
} from './selection.js';
|
|
31
|
+
import { logger } from '../utils/logger.js';
|
|
32
|
+
|
|
33
|
+
export class AccountManager {
|
|
34
|
+
#accounts = [];
|
|
35
|
+
#currentIndex = 0;
|
|
36
|
+
#configPath;
|
|
37
|
+
#settings = {};
|
|
38
|
+
#initialized = false;
|
|
39
|
+
|
|
40
|
+
// Per-account caches
|
|
41
|
+
#tokenCache = new Map(); // email -> { token, extractedAt }
|
|
42
|
+
#projectCache = new Map(); // email -> projectId
|
|
43
|
+
|
|
44
|
+
constructor(configPath = ACCOUNT_CONFIG_PATH) {
|
|
45
|
+
this.#configPath = configPath;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Initialize the account manager by loading config
|
|
50
|
+
*/
|
|
51
|
+
async initialize() {
|
|
52
|
+
if (this.#initialized) return;
|
|
53
|
+
|
|
54
|
+
const { accounts, settings, activeIndex } = await loadAccounts(this.#configPath);
|
|
55
|
+
|
|
56
|
+
this.#accounts = accounts;
|
|
57
|
+
this.#settings = settings;
|
|
58
|
+
this.#currentIndex = activeIndex;
|
|
59
|
+
|
|
60
|
+
// If config exists but has no accounts, fall back to Antigravity database
|
|
61
|
+
if (this.#accounts.length === 0) {
|
|
62
|
+
logger.warn('[AccountManager] No accounts in config. Falling back to Antigravity database');
|
|
63
|
+
const { accounts: defaultAccounts, tokenCache } = loadDefaultAccount();
|
|
64
|
+
this.#accounts = defaultAccounts;
|
|
65
|
+
this.#tokenCache = tokenCache;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Clear any expired rate limits
|
|
69
|
+
this.clearExpiredLimits();
|
|
70
|
+
|
|
71
|
+
this.#initialized = true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get the number of accounts
|
|
76
|
+
* @returns {number} Number of configured accounts
|
|
77
|
+
*/
|
|
78
|
+
getAccountCount() {
|
|
79
|
+
return this.#accounts.length;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Check if all accounts are rate-limited
|
|
84
|
+
* @returns {boolean} True if all accounts are rate-limited
|
|
85
|
+
*/
|
|
86
|
+
isAllRateLimited() {
|
|
87
|
+
return checkAllRateLimited(this.#accounts);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get list of available (non-rate-limited, non-invalid) accounts
|
|
92
|
+
* @returns {Array<Object>} Array of available account objects
|
|
93
|
+
*/
|
|
94
|
+
getAvailableAccounts() {
|
|
95
|
+
return getAvailable(this.#accounts);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get list of invalid accounts
|
|
100
|
+
* @returns {Array<Object>} Array of invalid account objects
|
|
101
|
+
*/
|
|
102
|
+
getInvalidAccounts() {
|
|
103
|
+
return getInvalid(this.#accounts);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Clear expired rate limits
|
|
108
|
+
* @returns {number} Number of rate limits cleared
|
|
109
|
+
*/
|
|
110
|
+
clearExpiredLimits() {
|
|
111
|
+
const cleared = clearLimits(this.#accounts);
|
|
112
|
+
if (cleared > 0) {
|
|
113
|
+
this.saveToDisk();
|
|
114
|
+
}
|
|
115
|
+
return cleared;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Clear all rate limits to force a fresh check
|
|
120
|
+
* (Optimistic retry strategy)
|
|
121
|
+
* @returns {void}
|
|
122
|
+
*/
|
|
123
|
+
resetAllRateLimits() {
|
|
124
|
+
resetLimits(this.#accounts);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Pick the next available account (fallback when current is unavailable).
|
|
129
|
+
* Sets activeIndex to the selected account's index.
|
|
130
|
+
* @returns {Object|null} The next available account or null if none available
|
|
131
|
+
*/
|
|
132
|
+
pickNext() {
|
|
133
|
+
const { account, newIndex } = selectNext(this.#accounts, this.#currentIndex, () => this.saveToDisk());
|
|
134
|
+
this.#currentIndex = newIndex;
|
|
135
|
+
return account;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get the current account without advancing the index (sticky selection).
|
|
140
|
+
* Used for cache continuity - sticks to the same account until rate-limited.
|
|
141
|
+
* @returns {Object|null} The current account or null if unavailable/rate-limited
|
|
142
|
+
*/
|
|
143
|
+
getCurrentStickyAccount() {
|
|
144
|
+
const { account, newIndex } = getSticky(this.#accounts, this.#currentIndex, () => this.saveToDisk());
|
|
145
|
+
this.#currentIndex = newIndex;
|
|
146
|
+
return account;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Check if we should wait for the current account's rate limit to reset.
|
|
151
|
+
* Used for sticky account selection - wait if rate limit is short (≤ threshold).
|
|
152
|
+
* @returns {{shouldWait: boolean, waitMs: number, account: Object|null}}
|
|
153
|
+
*/
|
|
154
|
+
shouldWaitForCurrentAccount() {
|
|
155
|
+
return shouldWait(this.#accounts, this.#currentIndex);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Pick an account with sticky selection preference.
|
|
160
|
+
* Prefers the current account for cache continuity, only switches when:
|
|
161
|
+
* - Current account is rate-limited for > 2 minutes
|
|
162
|
+
* - Current account is invalid
|
|
163
|
+
* @returns {{account: Object|null, waitMs: number}} Account to use and optional wait time
|
|
164
|
+
*/
|
|
165
|
+
pickStickyAccount() {
|
|
166
|
+
const { account, waitMs, newIndex } = selectSticky(this.#accounts, this.#currentIndex, () => this.saveToDisk());
|
|
167
|
+
this.#currentIndex = newIndex;
|
|
168
|
+
return { account, waitMs };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Mark an account as rate-limited
|
|
173
|
+
* @param {string} email - Email of the account to mark
|
|
174
|
+
* @param {number|null} resetMs - Time in ms until rate limit resets (optional)
|
|
175
|
+
*/
|
|
176
|
+
markRateLimited(email, resetMs = null) {
|
|
177
|
+
markLimited(this.#accounts, email, resetMs, this.#settings);
|
|
178
|
+
this.saveToDisk();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Mark an account as invalid (credentials need re-authentication)
|
|
183
|
+
* @param {string} email - Email of the account to mark
|
|
184
|
+
* @param {string} reason - Reason for marking as invalid
|
|
185
|
+
*/
|
|
186
|
+
markInvalid(email, reason = 'Unknown error') {
|
|
187
|
+
markAccountInvalid(this.#accounts, email, reason);
|
|
188
|
+
this.saveToDisk();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get the minimum wait time until any account becomes available
|
|
193
|
+
* @returns {number} Wait time in milliseconds
|
|
194
|
+
*/
|
|
195
|
+
getMinWaitTimeMs() {
|
|
196
|
+
return getMinWait(this.#accounts);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Get OAuth token for an account
|
|
201
|
+
* @param {Object} account - Account object with email and credentials
|
|
202
|
+
* @returns {Promise<string>} OAuth access token
|
|
203
|
+
* @throws {Error} If token refresh fails
|
|
204
|
+
*/
|
|
205
|
+
async getTokenForAccount(account) {
|
|
206
|
+
return fetchToken(
|
|
207
|
+
account,
|
|
208
|
+
this.#tokenCache,
|
|
209
|
+
(email, reason) => this.markInvalid(email, reason),
|
|
210
|
+
() => this.saveToDisk()
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Get project ID for an account
|
|
216
|
+
* @param {Object} account - Account object
|
|
217
|
+
* @param {string} token - OAuth access token
|
|
218
|
+
* @returns {Promise<string>} Project ID
|
|
219
|
+
*/
|
|
220
|
+
async getProjectForAccount(account, token) {
|
|
221
|
+
return fetchProject(account, token, this.#projectCache);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Clear project cache for an account (useful on auth errors)
|
|
226
|
+
* @param {string|null} email - Email to clear cache for, or null to clear all
|
|
227
|
+
*/
|
|
228
|
+
clearProjectCache(email = null) {
|
|
229
|
+
clearProject(this.#projectCache, email);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Clear token cache for an account (useful on auth errors)
|
|
234
|
+
* @param {string|null} email - Email to clear cache for, or null to clear all
|
|
235
|
+
*/
|
|
236
|
+
clearTokenCache(email = null) {
|
|
237
|
+
clearToken(this.#tokenCache, email);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Save current state to disk (async)
|
|
242
|
+
* @returns {Promise<void>}
|
|
243
|
+
*/
|
|
244
|
+
async saveToDisk() {
|
|
245
|
+
await saveAccounts(this.#configPath, this.#accounts, this.#settings, this.#currentIndex);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get status object for logging/API
|
|
250
|
+
* @returns {{accounts: Array, settings: Object}} Status object with accounts and settings
|
|
251
|
+
*/
|
|
252
|
+
getStatus() {
|
|
253
|
+
const available = this.getAvailableAccounts();
|
|
254
|
+
const rateLimited = this.#accounts.filter(a => a.isRateLimited);
|
|
255
|
+
const invalid = this.getInvalidAccounts();
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
total: this.#accounts.length,
|
|
259
|
+
available: available.length,
|
|
260
|
+
rateLimited: rateLimited.length,
|
|
261
|
+
invalid: invalid.length,
|
|
262
|
+
summary: `${this.#accounts.length} total, ${available.length} available, ${rateLimited.length} rate-limited, ${invalid.length} invalid`,
|
|
263
|
+
accounts: this.#accounts.map(a => ({
|
|
264
|
+
email: a.email,
|
|
265
|
+
source: a.source,
|
|
266
|
+
isRateLimited: a.isRateLimited,
|
|
267
|
+
rateLimitResetTime: a.rateLimitResetTime,
|
|
268
|
+
isInvalid: a.isInvalid || false,
|
|
269
|
+
invalidReason: a.invalidReason || null,
|
|
270
|
+
lastUsed: a.lastUsed
|
|
271
|
+
}))
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Get settings
|
|
277
|
+
* @returns {Object} Current settings object
|
|
278
|
+
*/
|
|
279
|
+
getSettings() {
|
|
280
|
+
return { ...this.#settings };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Get all accounts (internal use for quota fetching)
|
|
285
|
+
* Returns the full account objects including credentials
|
|
286
|
+
* @returns {Array<Object>} Array of account objects
|
|
287
|
+
*/
|
|
288
|
+
getAllAccounts() {
|
|
289
|
+
return this.#accounts;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export default AccountManager;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate Limit Management
|
|
3
|
+
*
|
|
4
|
+
* Handles rate limit tracking and state management for accounts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { DEFAULT_COOLDOWN_MS } from '../constants.js';
|
|
8
|
+
import { formatDuration } from '../utils/helpers.js';
|
|
9
|
+
import { logger } from '../utils/logger.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Check if all accounts are rate-limited
|
|
13
|
+
*
|
|
14
|
+
* @param {Array} accounts - Array of account objects
|
|
15
|
+
* @returns {boolean} True if all accounts are rate-limited
|
|
16
|
+
*/
|
|
17
|
+
export function isAllRateLimited(accounts) {
|
|
18
|
+
if (accounts.length === 0) return true;
|
|
19
|
+
return accounts.every(acc => acc.isRateLimited);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get list of available (non-rate-limited, non-invalid) accounts
|
|
24
|
+
*
|
|
25
|
+
* @param {Array} accounts - Array of account objects
|
|
26
|
+
* @returns {Array} Array of available account objects
|
|
27
|
+
*/
|
|
28
|
+
export function getAvailableAccounts(accounts) {
|
|
29
|
+
return accounts.filter(acc => !acc.isRateLimited && !acc.isInvalid);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get list of invalid accounts
|
|
34
|
+
*
|
|
35
|
+
* @param {Array} accounts - Array of account objects
|
|
36
|
+
* @returns {Array} Array of invalid account objects
|
|
37
|
+
*/
|
|
38
|
+
export function getInvalidAccounts(accounts) {
|
|
39
|
+
return accounts.filter(acc => acc.isInvalid);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Clear expired rate limits
|
|
44
|
+
*
|
|
45
|
+
* @param {Array} accounts - Array of account objects
|
|
46
|
+
* @returns {number} Number of rate limits cleared
|
|
47
|
+
*/
|
|
48
|
+
export function clearExpiredLimits(accounts) {
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
let cleared = 0;
|
|
51
|
+
|
|
52
|
+
for (const account of accounts) {
|
|
53
|
+
if (account.isRateLimited && account.rateLimitResetTime && account.rateLimitResetTime <= now) {
|
|
54
|
+
account.rateLimitResetTime = null;
|
|
55
|
+
cleared++;
|
|
56
|
+
logger.success(`[AccountManager] Rate limit expired for: ${account.email}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return cleared;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Clear all rate limits to force a fresh check (optimistic retry strategy)
|
|
65
|
+
*
|
|
66
|
+
* @param {Array} accounts - Array of account objects
|
|
67
|
+
*/
|
|
68
|
+
export function resetAllRateLimits(accounts) {
|
|
69
|
+
for (const account of accounts) {
|
|
70
|
+
account.isRateLimited = false;
|
|
71
|
+
account.rateLimitResetTime = null;
|
|
72
|
+
}
|
|
73
|
+
logger.warn('[AccountManager] Reset all rate limits for optimistic retry');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Mark an account as rate-limited
|
|
78
|
+
*
|
|
79
|
+
* @param {Array} accounts - Array of account objects
|
|
80
|
+
* @param {string} email - Email of the account to mark
|
|
81
|
+
* @param {number|null} resetMs - Time in ms until rate limit resets (optional)
|
|
82
|
+
* @param {Object} settings - Settings object with cooldownDurationMs
|
|
83
|
+
* @returns {boolean} True if account was found and marked
|
|
84
|
+
*/
|
|
85
|
+
export function markRateLimited(accounts, email, resetMs = null, settings = {}) {
|
|
86
|
+
const account = accounts.find(a => a.email === email);
|
|
87
|
+
if (!account) return false;
|
|
88
|
+
|
|
89
|
+
account.isRateLimited = true;
|
|
90
|
+
const cooldownMs = resetMs || settings.cooldownDurationMs || DEFAULT_COOLDOWN_MS;
|
|
91
|
+
account.rateLimitResetTime = Date.now() + cooldownMs;
|
|
92
|
+
|
|
93
|
+
logger.warn(
|
|
94
|
+
`[AccountManager] Rate limited: ${email}. Available in ${formatDuration(cooldownMs)}`
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Mark an account as invalid (credentials need re-authentication)
|
|
102
|
+
*
|
|
103
|
+
* @param {Array} accounts - Array of account objects
|
|
104
|
+
* @param {string} email - Email of the account to mark
|
|
105
|
+
* @param {string} reason - Reason for marking as invalid
|
|
106
|
+
* @returns {boolean} True if account was found and marked
|
|
107
|
+
*/
|
|
108
|
+
export function markInvalid(accounts, email, reason = 'Unknown error') {
|
|
109
|
+
const account = accounts.find(a => a.email === email);
|
|
110
|
+
if (!account) return false;
|
|
111
|
+
|
|
112
|
+
account.isInvalid = true;
|
|
113
|
+
account.invalidReason = reason;
|
|
114
|
+
account.invalidAt = Date.now();
|
|
115
|
+
|
|
116
|
+
logger.error(
|
|
117
|
+
`[AccountManager] ⚠ Account INVALID: ${email}`
|
|
118
|
+
);
|
|
119
|
+
logger.error(
|
|
120
|
+
`[AccountManager] Reason: ${reason}`
|
|
121
|
+
);
|
|
122
|
+
logger.error(
|
|
123
|
+
`[AccountManager] Run 'npm run accounts' to re-authenticate this account`
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get the minimum wait time until any account becomes available
|
|
131
|
+
*
|
|
132
|
+
* @param {Array} accounts - Array of account objects
|
|
133
|
+
* @returns {number} Wait time in milliseconds
|
|
134
|
+
*/
|
|
135
|
+
export function getMinWaitTimeMs(accounts) {
|
|
136
|
+
if (!isAllRateLimited(accounts)) return 0;
|
|
137
|
+
|
|
138
|
+
const now = Date.now();
|
|
139
|
+
let minWait = Infinity;
|
|
140
|
+
let soonestAccount = null;
|
|
141
|
+
|
|
142
|
+
for (const account of accounts) {
|
|
143
|
+
if (account.rateLimitResetTime) {
|
|
144
|
+
const wait = account.rateLimitResetTime - now;
|
|
145
|
+
if (wait > 0 && wait < minWait) {
|
|
146
|
+
minWait = wait;
|
|
147
|
+
soonestAccount = account;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (soonestAccount) {
|
|
153
|
+
logger.info(`[AccountManager] Shortest wait: ${formatDuration(minWait)} (account: ${soonestAccount.email})`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return minWait === Infinity ? DEFAULT_COOLDOWN_MS : minWait;
|
|
157
|
+
}
|