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.
- package/LICENSE +21 -0
- package/README.md +757 -0
- package/bin/cli.js +146 -0
- package/package.json +97 -0
- package/public/Complaint Details.pdf +0 -0
- package/public/Cyber Crime Portal.pdf +0 -0
- package/public/app.js +229 -0
- package/public/css/src/input.css +523 -0
- package/public/css/style.css +1 -0
- package/public/favicon.png +0 -0
- package/public/index.html +549 -0
- package/public/js/components/account-manager.js +356 -0
- package/public/js/components/add-account-modal.js +414 -0
- package/public/js/components/claude-config.js +420 -0
- package/public/js/components/dashboard/charts.js +605 -0
- package/public/js/components/dashboard/filters.js +362 -0
- package/public/js/components/dashboard/stats.js +110 -0
- package/public/js/components/dashboard.js +236 -0
- package/public/js/components/logs-viewer.js +100 -0
- package/public/js/components/models.js +36 -0
- package/public/js/components/server-config.js +349 -0
- package/public/js/config/constants.js +102 -0
- package/public/js/data-store.js +375 -0
- package/public/js/settings-store.js +58 -0
- package/public/js/store.js +99 -0
- package/public/js/translations/en.js +367 -0
- package/public/js/translations/id.js +412 -0
- package/public/js/translations/pt.js +308 -0
- package/public/js/translations/tr.js +358 -0
- package/public/js/translations/zh.js +373 -0
- package/public/js/utils/account-actions.js +189 -0
- package/public/js/utils/error-handler.js +96 -0
- package/public/js/utils/model-config.js +42 -0
- package/public/js/utils/ui-logger.js +143 -0
- package/public/js/utils/validators.js +77 -0
- package/public/js/utils.js +69 -0
- package/public/proxy-server-64.png +0 -0
- package/public/views/accounts.html +361 -0
- package/public/views/dashboard.html +484 -0
- package/public/views/logs.html +97 -0
- package/public/views/models.html +331 -0
- package/public/views/settings.html +1327 -0
- package/src/account-manager/credentials.js +378 -0
- package/src/account-manager/index.js +462 -0
- package/src/account-manager/onboarding.js +112 -0
- package/src/account-manager/rate-limits.js +369 -0
- package/src/account-manager/storage.js +160 -0
- package/src/account-manager/strategies/base-strategy.js +109 -0
- package/src/account-manager/strategies/hybrid-strategy.js +339 -0
- package/src/account-manager/strategies/index.js +79 -0
- package/src/account-manager/strategies/round-robin-strategy.js +76 -0
- package/src/account-manager/strategies/sticky-strategy.js +138 -0
- package/src/account-manager/strategies/trackers/health-tracker.js +162 -0
- package/src/account-manager/strategies/trackers/index.js +9 -0
- package/src/account-manager/strategies/trackers/quota-tracker.js +120 -0
- package/src/account-manager/strategies/trackers/token-bucket-tracker.js +155 -0
- package/src/auth/database.js +169 -0
- package/src/auth/oauth.js +548 -0
- package/src/auth/token-extractor.js +117 -0
- package/src/cli/accounts.js +648 -0
- package/src/cloudcode/index.js +29 -0
- package/src/cloudcode/message-handler.js +510 -0
- package/src/cloudcode/model-api.js +248 -0
- package/src/cloudcode/rate-limit-parser.js +235 -0
- package/src/cloudcode/request-builder.js +93 -0
- package/src/cloudcode/session-manager.js +47 -0
- package/src/cloudcode/sse-parser.js +121 -0
- package/src/cloudcode/sse-streamer.js +293 -0
- package/src/cloudcode/streaming-handler.js +615 -0
- package/src/config.js +125 -0
- package/src/constants.js +407 -0
- package/src/errors.js +242 -0
- package/src/fallback-config.js +29 -0
- package/src/format/content-converter.js +193 -0
- package/src/format/index.js +20 -0
- package/src/format/request-converter.js +255 -0
- package/src/format/response-converter.js +120 -0
- package/src/format/schema-sanitizer.js +673 -0
- package/src/format/signature-cache.js +88 -0
- package/src/format/thinking-utils.js +648 -0
- package/src/index.js +148 -0
- package/src/modules/usage-stats.js +205 -0
- package/src/providers/anthropic-provider.js +258 -0
- package/src/providers/base-provider.js +157 -0
- package/src/providers/cloudcode.js +94 -0
- package/src/providers/copilot.js +399 -0
- package/src/providers/github-provider.js +287 -0
- package/src/providers/google-provider.js +192 -0
- package/src/providers/index.js +211 -0
- package/src/providers/openai-compatible.js +265 -0
- package/src/providers/openai-provider.js +271 -0
- package/src/providers/openrouter-provider.js +325 -0
- package/src/providers/setup.js +83 -0
- package/src/server.js +870 -0
- package/src/utils/claude-config.js +245 -0
- package/src/utils/helpers.js +51 -0
- package/src/utils/logger.js +142 -0
- package/src/utils/native-module-helper.js +162 -0
- package/src/webui/index.js +1134 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health Tracker
|
|
3
|
+
*
|
|
4
|
+
* Tracks per-account health scores to prioritize healthy accounts.
|
|
5
|
+
* Scores increase on success and decrease on failures/rate limits.
|
|
6
|
+
* Passive recovery over time helps accounts recover from temporary issues.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Default configuration (matches opencode-cloudcode-auth)
|
|
10
|
+
const DEFAULT_CONFIG = {
|
|
11
|
+
initial: 70, // Starting score for new accounts
|
|
12
|
+
successReward: 1, // Points on successful request
|
|
13
|
+
rateLimitPenalty: -10, // Points on rate limit
|
|
14
|
+
failurePenalty: -20, // Points on other failures
|
|
15
|
+
recoveryPerHour: 10, // Passive recovery rate (increased from 2 for faster recovery)
|
|
16
|
+
minUsable: 50, // Minimum score to be selected
|
|
17
|
+
maxScore: 100 // Maximum score cap
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export class HealthTracker {
|
|
21
|
+
#scores = new Map(); // email -> { score, lastUpdated, consecutiveFailures }
|
|
22
|
+
#config;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Create a new HealthTracker
|
|
26
|
+
* @param {Object} config - Health score configuration
|
|
27
|
+
*/
|
|
28
|
+
constructor(config = {}) {
|
|
29
|
+
this.#config = { ...DEFAULT_CONFIG, ...config };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get the health score for an account
|
|
34
|
+
* @param {string} email - Account email
|
|
35
|
+
* @returns {number} Current health score (with passive recovery applied)
|
|
36
|
+
*/
|
|
37
|
+
getScore(email) {
|
|
38
|
+
const record = this.#scores.get(email);
|
|
39
|
+
if (!record) {
|
|
40
|
+
return this.#config.initial;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Apply passive recovery based on time elapsed
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
const hoursElapsed = (now - record.lastUpdated) / (1000 * 60 * 60);
|
|
46
|
+
const recovery = hoursElapsed * this.#config.recoveryPerHour;
|
|
47
|
+
const recoveredScore = Math.min(
|
|
48
|
+
this.#config.maxScore,
|
|
49
|
+
record.score + recovery
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
return recoveredScore;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Record a successful request for an account
|
|
57
|
+
* @param {string} email - Account email
|
|
58
|
+
*/
|
|
59
|
+
recordSuccess(email) {
|
|
60
|
+
const currentScore = this.getScore(email);
|
|
61
|
+
const newScore = Math.min(
|
|
62
|
+
this.#config.maxScore,
|
|
63
|
+
currentScore + this.#config.successReward
|
|
64
|
+
);
|
|
65
|
+
this.#scores.set(email, {
|
|
66
|
+
score: newScore,
|
|
67
|
+
lastUpdated: Date.now(),
|
|
68
|
+
consecutiveFailures: 0 // Reset on success
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Record a rate limit for an account
|
|
74
|
+
* @param {string} email - Account email
|
|
75
|
+
*/
|
|
76
|
+
recordRateLimit(email) {
|
|
77
|
+
const record = this.#scores.get(email);
|
|
78
|
+
const currentScore = this.getScore(email);
|
|
79
|
+
const newScore = Math.max(
|
|
80
|
+
0,
|
|
81
|
+
currentScore + this.#config.rateLimitPenalty
|
|
82
|
+
);
|
|
83
|
+
this.#scores.set(email, {
|
|
84
|
+
score: newScore,
|
|
85
|
+
lastUpdated: Date.now(),
|
|
86
|
+
consecutiveFailures: (record?.consecutiveFailures ?? 0) + 1
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Record a failure for an account
|
|
92
|
+
* @param {string} email - Account email
|
|
93
|
+
*/
|
|
94
|
+
recordFailure(email) {
|
|
95
|
+
const record = this.#scores.get(email);
|
|
96
|
+
const currentScore = this.getScore(email);
|
|
97
|
+
const newScore = Math.max(
|
|
98
|
+
0,
|
|
99
|
+
currentScore + this.#config.failurePenalty
|
|
100
|
+
);
|
|
101
|
+
this.#scores.set(email, {
|
|
102
|
+
score: newScore,
|
|
103
|
+
lastUpdated: Date.now(),
|
|
104
|
+
consecutiveFailures: (record?.consecutiveFailures ?? 0) + 1
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Check if an account is usable based on health score
|
|
110
|
+
* @param {string} email - Account email
|
|
111
|
+
* @returns {boolean} True if account health score is above minimum threshold
|
|
112
|
+
*/
|
|
113
|
+
isUsable(email) {
|
|
114
|
+
return this.getScore(email) >= this.#config.minUsable;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get the minimum usable score threshold
|
|
119
|
+
* @returns {number} Minimum score for an account to be usable
|
|
120
|
+
*/
|
|
121
|
+
getMinUsable() {
|
|
122
|
+
return this.#config.minUsable;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get the maximum score cap
|
|
127
|
+
* @returns {number} Maximum health score
|
|
128
|
+
*/
|
|
129
|
+
getMaxScore() {
|
|
130
|
+
return this.#config.maxScore;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Reset the score for an account (e.g., after re-authentication)
|
|
135
|
+
* @param {string} email - Account email
|
|
136
|
+
*/
|
|
137
|
+
reset(email) {
|
|
138
|
+
this.#scores.set(email, {
|
|
139
|
+
score: this.#config.initial,
|
|
140
|
+
lastUpdated: Date.now(),
|
|
141
|
+
consecutiveFailures: 0
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get the consecutive failure count for an account
|
|
147
|
+
* @param {string} email - Account email
|
|
148
|
+
* @returns {number} Number of consecutive failures
|
|
149
|
+
*/
|
|
150
|
+
getConsecutiveFailures(email) {
|
|
151
|
+
return this.#scores.get(email)?.consecutiveFailures ?? 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Clear all tracked scores
|
|
156
|
+
*/
|
|
157
|
+
clear() {
|
|
158
|
+
this.#scores.clear();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export default HealthTracker;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trackers Index
|
|
3
|
+
*
|
|
4
|
+
* Exports all tracker classes for account selection strategies.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { HealthTracker } from './health-tracker.js';
|
|
8
|
+
export { TokenBucketTracker } from './token-bucket-tracker.js';
|
|
9
|
+
export { QuotaTracker } from './quota-tracker.js';
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quota Tracker
|
|
3
|
+
*
|
|
4
|
+
* Tracks per-account quota levels to prioritize accounts with available quota.
|
|
5
|
+
* Uses quota data from account.quota.models[modelId].remainingFraction.
|
|
6
|
+
* Accounts below critical threshold are excluded from selection.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Default configuration
|
|
10
|
+
const DEFAULT_CONFIG = {
|
|
11
|
+
lowThreshold: 0.10, // 10% - reduce score
|
|
12
|
+
criticalThreshold: 0.05, // 5% - exclude from candidates
|
|
13
|
+
staleMs: 300000, // 5 min - max age of quota data to trust
|
|
14
|
+
unknownScore: 50 // Score for accounts with unknown quota
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export class QuotaTracker {
|
|
18
|
+
#config;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Create a new QuotaTracker
|
|
22
|
+
* @param {Object} config - Quota tracker configuration
|
|
23
|
+
*/
|
|
24
|
+
constructor(config = {}) {
|
|
25
|
+
this.#config = { ...DEFAULT_CONFIG, ...config };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get the quota fraction for an account and model
|
|
30
|
+
* @param {Object} account - Account object
|
|
31
|
+
* @param {string} modelId - Model ID to check
|
|
32
|
+
* @returns {number|null} Remaining fraction (0-1) or null if unknown
|
|
33
|
+
*/
|
|
34
|
+
getQuotaFraction(account, modelId) {
|
|
35
|
+
if (!account?.quota?.models?.[modelId]) return null;
|
|
36
|
+
const fraction = account.quota.models[modelId].remainingFraction;
|
|
37
|
+
return typeof fraction === 'number' ? fraction : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check if quota data is fresh enough to be trusted
|
|
42
|
+
* @param {Object} account - Account object
|
|
43
|
+
* @returns {boolean} True if quota data is fresh
|
|
44
|
+
*/
|
|
45
|
+
isQuotaFresh(account) {
|
|
46
|
+
if (!account?.quota?.lastChecked) return false;
|
|
47
|
+
return (Date.now() - account.quota.lastChecked) < this.#config.staleMs;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Check if an account has critically low quota for a model
|
|
52
|
+
* @param {Object} account - Account object
|
|
53
|
+
* @param {string} modelId - Model ID to check
|
|
54
|
+
* @returns {boolean} True if quota is at or below critical threshold
|
|
55
|
+
*/
|
|
56
|
+
isQuotaCritical(account, modelId) {
|
|
57
|
+
const fraction = this.getQuotaFraction(account, modelId);
|
|
58
|
+
// Unknown quota = not critical (assume OK)
|
|
59
|
+
if (fraction === null) return false;
|
|
60
|
+
// Only apply critical check if data is fresh
|
|
61
|
+
if (!this.isQuotaFresh(account)) return false;
|
|
62
|
+
return fraction <= this.#config.criticalThreshold;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Check if an account has low (but not critical) quota for a model
|
|
67
|
+
* @param {Object} account - Account object
|
|
68
|
+
* @param {string} modelId - Model ID to check
|
|
69
|
+
* @returns {boolean} True if quota is below low threshold but above critical
|
|
70
|
+
*/
|
|
71
|
+
isQuotaLow(account, modelId) {
|
|
72
|
+
const fraction = this.getQuotaFraction(account, modelId);
|
|
73
|
+
if (fraction === null) return false;
|
|
74
|
+
return fraction <= this.#config.lowThreshold && fraction > this.#config.criticalThreshold;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get a score (0-100) for an account based on quota
|
|
79
|
+
* Higher score = more quota available
|
|
80
|
+
* @param {Object} account - Account object
|
|
81
|
+
* @param {string} modelId - Model ID to check
|
|
82
|
+
* @returns {number} Score from 0-100
|
|
83
|
+
*/
|
|
84
|
+
getScore(account, modelId) {
|
|
85
|
+
const fraction = this.getQuotaFraction(account, modelId);
|
|
86
|
+
|
|
87
|
+
// Unknown quota = middle score
|
|
88
|
+
if (fraction === null) {
|
|
89
|
+
return this.#config.unknownScore;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Convert fraction (0-1) to score (0-100)
|
|
93
|
+
let score = fraction * 100;
|
|
94
|
+
|
|
95
|
+
// Apply small penalty for stale data (reduce confidence)
|
|
96
|
+
if (!this.isQuotaFresh(account)) {
|
|
97
|
+
score *= 0.9; // 10% penalty for stale data
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return score;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get the critical threshold
|
|
105
|
+
* @returns {number} Critical threshold (0-1)
|
|
106
|
+
*/
|
|
107
|
+
getCriticalThreshold() {
|
|
108
|
+
return this.#config.criticalThreshold;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get the low threshold
|
|
113
|
+
* @returns {number} Low threshold (0-1)
|
|
114
|
+
*/
|
|
115
|
+
getLowThreshold() {
|
|
116
|
+
return this.#config.lowThreshold;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export default QuotaTracker;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token Bucket Tracker
|
|
3
|
+
*
|
|
4
|
+
* Client-side rate limiting using the token bucket algorithm.
|
|
5
|
+
* Each account has a bucket of tokens that regenerate over time.
|
|
6
|
+
* Requests consume tokens; accounts without tokens are deprioritized.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Default configuration (matches opencode-cloudcode-auth)
|
|
10
|
+
const DEFAULT_CONFIG = {
|
|
11
|
+
maxTokens: 50, // Maximum token capacity
|
|
12
|
+
tokensPerMinute: 6, // Regeneration rate
|
|
13
|
+
initialTokens: 50 // Starting tokens
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export class TokenBucketTracker {
|
|
17
|
+
#buckets = new Map(); // email -> { tokens, lastUpdated }
|
|
18
|
+
#config;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Create a new TokenBucketTracker
|
|
22
|
+
* @param {Object} config - Token bucket configuration
|
|
23
|
+
*/
|
|
24
|
+
constructor(config = {}) {
|
|
25
|
+
this.#config = { ...DEFAULT_CONFIG, ...config };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get the current token count for an account
|
|
30
|
+
* @param {string} email - Account email
|
|
31
|
+
* @returns {number} Current token count (with regeneration applied)
|
|
32
|
+
*/
|
|
33
|
+
getTokens(email) {
|
|
34
|
+
const bucket = this.#buckets.get(email);
|
|
35
|
+
if (!bucket) {
|
|
36
|
+
return this.#config.initialTokens;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Apply token regeneration based on time elapsed
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
const minutesElapsed = (now - bucket.lastUpdated) / (1000 * 60);
|
|
42
|
+
const regenerated = minutesElapsed * this.#config.tokensPerMinute;
|
|
43
|
+
const currentTokens = Math.min(
|
|
44
|
+
this.#config.maxTokens,
|
|
45
|
+
bucket.tokens + regenerated
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
return currentTokens;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check if an account has tokens available
|
|
53
|
+
* @param {string} email - Account email
|
|
54
|
+
* @returns {boolean} True if account has at least 1 token
|
|
55
|
+
*/
|
|
56
|
+
hasTokens(email) {
|
|
57
|
+
return this.getTokens(email) >= 1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Consume a token from an account's bucket
|
|
62
|
+
* @param {string} email - Account email
|
|
63
|
+
* @returns {boolean} True if token was consumed, false if no tokens available
|
|
64
|
+
*/
|
|
65
|
+
consume(email) {
|
|
66
|
+
const currentTokens = this.getTokens(email);
|
|
67
|
+
if (currentTokens < 1) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
this.#buckets.set(email, {
|
|
72
|
+
tokens: currentTokens - 1,
|
|
73
|
+
lastUpdated: Date.now()
|
|
74
|
+
});
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Refund a token to an account's bucket (e.g., on request failure before processing)
|
|
80
|
+
* @param {string} email - Account email
|
|
81
|
+
*/
|
|
82
|
+
refund(email) {
|
|
83
|
+
const currentTokens = this.getTokens(email);
|
|
84
|
+
const newTokens = Math.min(
|
|
85
|
+
this.#config.maxTokens,
|
|
86
|
+
currentTokens + 1
|
|
87
|
+
);
|
|
88
|
+
this.#buckets.set(email, {
|
|
89
|
+
tokens: newTokens,
|
|
90
|
+
lastUpdated: Date.now()
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get the maximum token capacity
|
|
96
|
+
* @returns {number} Maximum tokens per bucket
|
|
97
|
+
*/
|
|
98
|
+
getMaxTokens() {
|
|
99
|
+
return this.#config.maxTokens;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Reset the bucket for an account
|
|
104
|
+
* @param {string} email - Account email
|
|
105
|
+
*/
|
|
106
|
+
reset(email) {
|
|
107
|
+
this.#buckets.set(email, {
|
|
108
|
+
tokens: this.#config.initialTokens,
|
|
109
|
+
lastUpdated: Date.now()
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Clear all tracked buckets
|
|
115
|
+
*/
|
|
116
|
+
clear() {
|
|
117
|
+
this.#buckets.clear();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get time in milliseconds until next token is available for an account
|
|
122
|
+
* @param {string} email - Account email
|
|
123
|
+
* @returns {number} Milliseconds until next token, 0 if tokens available now
|
|
124
|
+
*/
|
|
125
|
+
getTimeUntilNextToken(email) {
|
|
126
|
+
const currentTokens = this.getTokens(email);
|
|
127
|
+
if (currentTokens >= 1) {
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Calculate time to regenerate 1 token
|
|
132
|
+
const tokensNeeded = 1 - currentTokens;
|
|
133
|
+
const minutesNeeded = tokensNeeded / this.#config.tokensPerMinute;
|
|
134
|
+
return Math.ceil(minutesNeeded * 60 * 1000);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get the minimum time until any account in the list has a token
|
|
139
|
+
* @param {Array<string>} emails - List of account emails
|
|
140
|
+
* @returns {number} Minimum milliseconds until any account has a token
|
|
141
|
+
*/
|
|
142
|
+
getMinTimeUntilToken(emails) {
|
|
143
|
+
if (emails.length === 0) return 0;
|
|
144
|
+
|
|
145
|
+
let minWait = Infinity;
|
|
146
|
+
for (const email of emails) {
|
|
147
|
+
const wait = this.getTimeUntilNextToken(email);
|
|
148
|
+
if (wait === 0) return 0;
|
|
149
|
+
minWait = Math.min(minWait, wait);
|
|
150
|
+
}
|
|
151
|
+
return minWait === Infinity ? 0 : minWait;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export default TokenBucketTracker;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite Database Access Module
|
|
3
|
+
* Provides cross-platform database operations for Cloud Code IDE state.
|
|
4
|
+
*
|
|
5
|
+
* Uses better-sqlite3 for:
|
|
6
|
+
* - Windows compatibility (no CLI dependency)
|
|
7
|
+
* - Native performance
|
|
8
|
+
* - Synchronous API (simple error handling)
|
|
9
|
+
*
|
|
10
|
+
* Includes auto-rebuild capability for handling Node.js version updates
|
|
11
|
+
* that cause native module incompatibility.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createRequire } from 'module';
|
|
15
|
+
import { CLOUDCODE_DB_PATH } from '../constants.js';
|
|
16
|
+
import { isModuleVersionError, attemptAutoRebuild, clearRequireCache } from '../utils/native-module-helper.js';
|
|
17
|
+
import { logger } from '../utils/logger.js';
|
|
18
|
+
import { NativeModuleError } from '../errors.js';
|
|
19
|
+
|
|
20
|
+
const require = createRequire(import.meta.url);
|
|
21
|
+
|
|
22
|
+
// Lazy-loaded Database constructor
|
|
23
|
+
let Database = null;
|
|
24
|
+
let moduleLoadError = null;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Load the better-sqlite3 module with auto-rebuild on version mismatch
|
|
28
|
+
* Uses synchronous require to maintain API compatibility
|
|
29
|
+
* @returns {Function} The Database constructor
|
|
30
|
+
* @throws {Error} If module cannot be loaded even after rebuild
|
|
31
|
+
*/
|
|
32
|
+
function loadDatabaseModule() {
|
|
33
|
+
// Return cached module if already loaded
|
|
34
|
+
if (Database) return Database;
|
|
35
|
+
|
|
36
|
+
// Re-throw cached error if previous load failed permanently
|
|
37
|
+
if (moduleLoadError) throw moduleLoadError;
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
Database = require('better-sqlite3');
|
|
41
|
+
return Database;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (isModuleVersionError(error)) {
|
|
44
|
+
logger.warn('[Database] Native module version mismatch detected');
|
|
45
|
+
|
|
46
|
+
if (attemptAutoRebuild(error)) {
|
|
47
|
+
// Clear require cache and retry
|
|
48
|
+
try {
|
|
49
|
+
const resolvedPath = require.resolve('better-sqlite3');
|
|
50
|
+
// Clear the module and all its dependencies from cache
|
|
51
|
+
clearRequireCache(resolvedPath, require.cache);
|
|
52
|
+
|
|
53
|
+
Database = require('better-sqlite3');
|
|
54
|
+
logger.success('[Database] Module reloaded successfully after rebuild');
|
|
55
|
+
return Database;
|
|
56
|
+
} catch (retryError) {
|
|
57
|
+
// Rebuild succeeded but reload failed - user needs to restart
|
|
58
|
+
moduleLoadError = new NativeModuleError(
|
|
59
|
+
'Native module rebuild completed. Please restart the server to apply the fix.',
|
|
60
|
+
true, // rebuildSucceeded
|
|
61
|
+
true // restartRequired
|
|
62
|
+
);
|
|
63
|
+
logger.info('[Database] Rebuild succeeded - server restart required');
|
|
64
|
+
throw moduleLoadError;
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
moduleLoadError = new NativeModuleError(
|
|
68
|
+
'Failed to auto-rebuild native module. Please run manually:\n' +
|
|
69
|
+
' npm rebuild better-sqlite3\n' +
|
|
70
|
+
'Or if using npx, find the package location in the error and run:\n' +
|
|
71
|
+
' cd /path/to/better-sqlite3 && npm rebuild',
|
|
72
|
+
false, // rebuildSucceeded
|
|
73
|
+
false // restartRequired
|
|
74
|
+
);
|
|
75
|
+
throw moduleLoadError;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Non-version-mismatch error, just throw it
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Query Cloud Code IDE database for authentication status
|
|
86
|
+
* @param {string} [dbPath] - Optional custom database path
|
|
87
|
+
* @returns {Object} Parsed auth data with apiKey, email, name, etc.
|
|
88
|
+
* @throws {Error} If database doesn't exist, query fails, or no auth status found
|
|
89
|
+
*/
|
|
90
|
+
export function getAuthStatus(dbPath = CLOUDCODE_DB_PATH) {
|
|
91
|
+
const Db = loadDatabaseModule();
|
|
92
|
+
let db;
|
|
93
|
+
try {
|
|
94
|
+
// Open database in read-only mode
|
|
95
|
+
db = new Db(dbPath, {
|
|
96
|
+
readonly: true,
|
|
97
|
+
fileMustExist: true
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Prepare and execute query
|
|
101
|
+
const stmt = db.prepare(
|
|
102
|
+
"SELECT value FROM ItemTable WHERE key = 'cloudcodeAuthStatus'"
|
|
103
|
+
);
|
|
104
|
+
const row = stmt.get();
|
|
105
|
+
|
|
106
|
+
if (!row || !row.value) {
|
|
107
|
+
throw new Error('No auth status found in database');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Parse JSON value
|
|
111
|
+
const authData = JSON.parse(row.value);
|
|
112
|
+
|
|
113
|
+
if (!authData.apiKey) {
|
|
114
|
+
throw new Error('Auth data missing apiKey field');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return authData;
|
|
118
|
+
} catch (error) {
|
|
119
|
+
// Enhance error messages for common issues
|
|
120
|
+
if (error.code === 'SQLITE_CANTOPEN') {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`Database not found at ${dbPath}. ` +
|
|
123
|
+
'Make sure the Cloud Code IDE is installed and you are logged in.'
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
// Re-throw with context if not already our error
|
|
127
|
+
if (error.message.includes('No auth status') || error.message.includes('missing apiKey')) {
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
// Re-throw native module errors from loadDatabaseModule without wrapping
|
|
131
|
+
if (error instanceof NativeModuleError) {
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
throw new Error(`Failed to read Cloud Code IDE database: ${error.message}`);
|
|
135
|
+
} finally {
|
|
136
|
+
// Always close database connection
|
|
137
|
+
if (db) {
|
|
138
|
+
db.close();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Check if database exists and is accessible
|
|
145
|
+
* @param {string} [dbPath] - Optional custom database path
|
|
146
|
+
* @returns {boolean} True if database exists and can be opened
|
|
147
|
+
*/
|
|
148
|
+
export function isDatabaseAccessible(dbPath = CLOUDCODE_DB_PATH) {
|
|
149
|
+
let db;
|
|
150
|
+
try {
|
|
151
|
+
const Db = loadDatabaseModule();
|
|
152
|
+
db = new Db(dbPath, {
|
|
153
|
+
readonly: true,
|
|
154
|
+
fileMustExist: true
|
|
155
|
+
});
|
|
156
|
+
return true;
|
|
157
|
+
} catch {
|
|
158
|
+
return false;
|
|
159
|
+
} finally {
|
|
160
|
+
if (db) {
|
|
161
|
+
db.close();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export default {
|
|
167
|
+
getAuthStatus,
|
|
168
|
+
isDatabaseAccessible
|
|
169
|
+
};
|