@kamel-ahmed/proxy-claude 1.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 (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +622 -0
  3. package/bin/cli.js +124 -0
  4. package/package.json +80 -0
  5. package/public/app.js +228 -0
  6. package/public/css/src/input.css +523 -0
  7. package/public/css/style.css +1 -0
  8. package/public/favicon.svg +10 -0
  9. package/public/index.html +381 -0
  10. package/public/js/components/account-manager.js +245 -0
  11. package/public/js/components/claude-config.js +420 -0
  12. package/public/js/components/dashboard/charts.js +589 -0
  13. package/public/js/components/dashboard/filters.js +362 -0
  14. package/public/js/components/dashboard/stats.js +110 -0
  15. package/public/js/components/dashboard.js +236 -0
  16. package/public/js/components/logs-viewer.js +100 -0
  17. package/public/js/components/models.js +36 -0
  18. package/public/js/components/server-config.js +349 -0
  19. package/public/js/config/constants.js +102 -0
  20. package/public/js/data-store.js +386 -0
  21. package/public/js/settings-store.js +58 -0
  22. package/public/js/store.js +78 -0
  23. package/public/js/translations/en.js +351 -0
  24. package/public/js/translations/id.js +396 -0
  25. package/public/js/translations/pt.js +287 -0
  26. package/public/js/translations/tr.js +342 -0
  27. package/public/js/translations/zh.js +357 -0
  28. package/public/js/utils/account-actions.js +189 -0
  29. package/public/js/utils/error-handler.js +96 -0
  30. package/public/js/utils/model-config.js +42 -0
  31. package/public/js/utils/validators.js +77 -0
  32. package/public/js/utils.js +69 -0
  33. package/public/views/accounts.html +329 -0
  34. package/public/views/dashboard.html +484 -0
  35. package/public/views/logs.html +97 -0
  36. package/public/views/models.html +331 -0
  37. package/public/views/settings.html +1329 -0
  38. package/src/account-manager/credentials.js +243 -0
  39. package/src/account-manager/index.js +380 -0
  40. package/src/account-manager/onboarding.js +117 -0
  41. package/src/account-manager/rate-limits.js +237 -0
  42. package/src/account-manager/storage.js +136 -0
  43. package/src/account-manager/strategies/base-strategy.js +104 -0
  44. package/src/account-manager/strategies/hybrid-strategy.js +195 -0
  45. package/src/account-manager/strategies/index.js +79 -0
  46. package/src/account-manager/strategies/round-robin-strategy.js +76 -0
  47. package/src/account-manager/strategies/sticky-strategy.js +138 -0
  48. package/src/account-manager/strategies/trackers/health-tracker.js +162 -0
  49. package/src/account-manager/strategies/trackers/index.js +8 -0
  50. package/src/account-manager/strategies/trackers/token-bucket-tracker.js +121 -0
  51. package/src/auth/database.js +169 -0
  52. package/src/auth/oauth.js +419 -0
  53. package/src/auth/token-extractor.js +117 -0
  54. package/src/cli/accounts.js +512 -0
  55. package/src/cli/refresh.js +201 -0
  56. package/src/cli/setup.js +338 -0
  57. package/src/cloudcode/index.js +29 -0
  58. package/src/cloudcode/message-handler.js +386 -0
  59. package/src/cloudcode/model-api.js +248 -0
  60. package/src/cloudcode/rate-limit-parser.js +181 -0
  61. package/src/cloudcode/request-builder.js +93 -0
  62. package/src/cloudcode/session-manager.js +47 -0
  63. package/src/cloudcode/sse-parser.js +121 -0
  64. package/src/cloudcode/sse-streamer.js +293 -0
  65. package/src/cloudcode/streaming-handler.js +492 -0
  66. package/src/config.js +107 -0
  67. package/src/constants.js +278 -0
  68. package/src/errors.js +238 -0
  69. package/src/fallback-config.js +29 -0
  70. package/src/format/content-converter.js +193 -0
  71. package/src/format/index.js +20 -0
  72. package/src/format/request-converter.js +248 -0
  73. package/src/format/response-converter.js +120 -0
  74. package/src/format/schema-sanitizer.js +673 -0
  75. package/src/format/signature-cache.js +88 -0
  76. package/src/format/thinking-utils.js +558 -0
  77. package/src/index.js +146 -0
  78. package/src/modules/usage-stats.js +205 -0
  79. package/src/server.js +861 -0
  80. package/src/utils/claude-config.js +245 -0
  81. package/src/utils/helpers.js +51 -0
  82. package/src/utils/logger.js +142 -0
  83. package/src/utils/native-module-helper.js +162 -0
  84. package/src/webui/index.js +707 -0
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Hybrid Strategy
3
+ *
4
+ * Smart selection based on health score, token bucket, and LRU freshness.
5
+ * Combines multiple signals for optimal account distribution.
6
+ *
7
+ * Scoring formula:
8
+ * score = (Health × 2) + ((Tokens / MaxTokens × 100) × 5) + (LRU × 0.1)
9
+ *
10
+ * Filters accounts that are:
11
+ * - Not rate-limited
12
+ * - Not invalid or disabled
13
+ * - Health score >= minUsable
14
+ * - Has tokens available
15
+ */
16
+
17
+ import { BaseStrategy } from './base-strategy.js';
18
+ import { HealthTracker, TokenBucketTracker } from './trackers/index.js';
19
+ import { logger } from '../../utils/logger.js';
20
+
21
+ // Default weights for scoring
22
+ const DEFAULT_WEIGHTS = {
23
+ health: 2,
24
+ tokens: 5,
25
+ lru: 0.1
26
+ };
27
+
28
+ export class HybridStrategy extends BaseStrategy {
29
+ #healthTracker;
30
+ #tokenBucketTracker;
31
+ #weights;
32
+
33
+ /**
34
+ * Create a new HybridStrategy
35
+ * @param {Object} config - Strategy configuration
36
+ * @param {Object} [config.healthScore] - Health tracker configuration
37
+ * @param {Object} [config.tokenBucket] - Token bucket configuration
38
+ * @param {Object} [config.weights] - Scoring weights
39
+ */
40
+ constructor(config = {}) {
41
+ super(config);
42
+ this.#healthTracker = new HealthTracker(config.healthScore || {});
43
+ this.#tokenBucketTracker = new TokenBucketTracker(config.tokenBucket || {});
44
+ this.#weights = { ...DEFAULT_WEIGHTS, ...config.weights };
45
+ }
46
+
47
+ /**
48
+ * Select an account based on combined health, tokens, and LRU score
49
+ *
50
+ * @param {Array} accounts - Array of account objects
51
+ * @param {string} modelId - The model ID for the request
52
+ * @param {Object} options - Additional options
53
+ * @returns {SelectionResult} The selected account and index
54
+ */
55
+ selectAccount(accounts, modelId, options = {}) {
56
+ const { onSave } = options;
57
+
58
+ if (accounts.length === 0) {
59
+ return { account: null, index: 0, waitMs: 0 };
60
+ }
61
+
62
+ // Get candidates that pass all filters
63
+ const candidates = this.#getCandidates(accounts, modelId);
64
+
65
+ if (candidates.length === 0) {
66
+ logger.debug('[HybridStrategy] No candidates available');
67
+ return { account: null, index: 0, waitMs: 0 };
68
+ }
69
+
70
+ // Score and sort candidates
71
+ const scored = candidates.map(({ account, index }) => ({
72
+ account,
73
+ index,
74
+ score: this.#calculateScore(account)
75
+ }));
76
+
77
+ scored.sort((a, b) => b.score - a.score);
78
+
79
+ // Select the best candidate
80
+ const best = scored[0];
81
+ best.account.lastUsed = Date.now();
82
+
83
+ // Consume a token from the bucket
84
+ this.#tokenBucketTracker.consume(best.account.email);
85
+
86
+ if (onSave) onSave();
87
+
88
+ const position = best.index + 1;
89
+ const total = accounts.length;
90
+ logger.info(`[HybridStrategy] Using account: ${best.account.email} (${position}/${total}, score: ${best.score.toFixed(1)})`);
91
+
92
+ return { account: best.account, index: best.index, waitMs: 0 };
93
+ }
94
+
95
+ /**
96
+ * Called after a successful request
97
+ */
98
+ onSuccess(account, modelId) {
99
+ if (account && account.email) {
100
+ this.#healthTracker.recordSuccess(account.email);
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Called when a request is rate-limited
106
+ */
107
+ onRateLimit(account, modelId) {
108
+ if (account && account.email) {
109
+ this.#healthTracker.recordRateLimit(account.email);
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Called when a request fails
115
+ */
116
+ onFailure(account, modelId) {
117
+ if (account && account.email) {
118
+ this.#healthTracker.recordFailure(account.email);
119
+ // Refund the token since the request didn't complete
120
+ this.#tokenBucketTracker.refund(account.email);
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Get candidates that pass all filters
126
+ * @private
127
+ */
128
+ #getCandidates(accounts, modelId) {
129
+ return accounts
130
+ .map((account, index) => ({ account, index }))
131
+ .filter(({ account }) => {
132
+ // Basic usability check
133
+ if (!this.isAccountUsable(account, modelId)) {
134
+ return false;
135
+ }
136
+
137
+ // Health score check
138
+ if (!this.#healthTracker.isUsable(account.email)) {
139
+ return false;
140
+ }
141
+
142
+ // Token availability check
143
+ if (!this.#tokenBucketTracker.hasTokens(account.email)) {
144
+ return false;
145
+ }
146
+
147
+ return true;
148
+ });
149
+ }
150
+
151
+ /**
152
+ * Calculate the combined score for an account
153
+ * @private
154
+ */
155
+ #calculateScore(account) {
156
+ const email = account.email;
157
+
158
+ // Health component (0-100 scaled by weight)
159
+ const health = this.#healthTracker.getScore(email);
160
+ const healthComponent = health * this.#weights.health;
161
+
162
+ // Token component (0-100 scaled by weight)
163
+ const tokens = this.#tokenBucketTracker.getTokens(email);
164
+ const maxTokens = this.#tokenBucketTracker.getMaxTokens();
165
+ const tokenRatio = tokens / maxTokens;
166
+ const tokenComponent = (tokenRatio * 100) * this.#weights.tokens;
167
+
168
+ // LRU component (older = higher score)
169
+ // Use time since last use, capped at 1 hour for scoring
170
+ const lastUsed = account.lastUsed || 0;
171
+ const timeSinceLastUse = Math.min(Date.now() - lastUsed, 3600000); // Cap at 1 hour
172
+ const lruMinutes = timeSinceLastUse / 60000;
173
+ const lruComponent = lruMinutes * this.#weights.lru;
174
+
175
+ return healthComponent + tokenComponent + lruComponent;
176
+ }
177
+
178
+ /**
179
+ * Get the health tracker (for testing/debugging)
180
+ * @returns {HealthTracker} The health tracker instance
181
+ */
182
+ getHealthTracker() {
183
+ return this.#healthTracker;
184
+ }
185
+
186
+ /**
187
+ * Get the token bucket tracker (for testing/debugging)
188
+ * @returns {TokenBucketTracker} The token bucket tracker instance
189
+ */
190
+ getTokenBucketTracker() {
191
+ return this.#tokenBucketTracker;
192
+ }
193
+ }
194
+
195
+ export default HybridStrategy;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Strategy Factory
3
+ *
4
+ * Creates and exports account selection strategy instances.
5
+ */
6
+
7
+ import { StickyStrategy } from './sticky-strategy.js';
8
+ import { RoundRobinStrategy } from './round-robin-strategy.js';
9
+ import { HybridStrategy } from './hybrid-strategy.js';
10
+ import { logger } from '../../utils/logger.js';
11
+ import {
12
+ SELECTION_STRATEGIES,
13
+ DEFAULT_SELECTION_STRATEGY,
14
+ STRATEGY_LABELS
15
+ } from '../../constants.js';
16
+
17
+ // Re-export strategy constants for convenience
18
+ export const STRATEGY_NAMES = SELECTION_STRATEGIES;
19
+ export const DEFAULT_STRATEGY = DEFAULT_SELECTION_STRATEGY;
20
+
21
+ /**
22
+ * Create a strategy instance
23
+ * @param {string} strategyName - Name of the strategy ('sticky', 'round-robin', 'hybrid')
24
+ * @param {Object} config - Strategy configuration
25
+ * @returns {BaseStrategy} The strategy instance
26
+ */
27
+ export function createStrategy(strategyName, config = {}) {
28
+ const name = (strategyName || DEFAULT_STRATEGY).toLowerCase();
29
+
30
+ switch (name) {
31
+ case 'sticky':
32
+ logger.debug('[Strategy] Creating StickyStrategy');
33
+ return new StickyStrategy(config);
34
+
35
+ case 'round-robin':
36
+ case 'roundrobin':
37
+ logger.debug('[Strategy] Creating RoundRobinStrategy');
38
+ return new RoundRobinStrategy(config);
39
+
40
+ case 'hybrid':
41
+ logger.debug('[Strategy] Creating HybridStrategy');
42
+ return new HybridStrategy(config);
43
+
44
+ default:
45
+ logger.warn(`[Strategy] Unknown strategy "${strategyName}", falling back to ${DEFAULT_STRATEGY}`);
46
+ return new HybridStrategy(config);
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Check if a strategy name is valid
52
+ * @param {string} name - Strategy name to check
53
+ * @returns {boolean} True if valid
54
+ */
55
+ export function isValidStrategy(name) {
56
+ if (!name) return false;
57
+ const lower = name.toLowerCase();
58
+ return STRATEGY_NAMES.includes(lower) || lower === 'roundrobin';
59
+ }
60
+
61
+ /**
62
+ * Get the display label for a strategy
63
+ * @param {string} name - Strategy name
64
+ * @returns {string} Display label
65
+ */
66
+ export function getStrategyLabel(name) {
67
+ const lower = (name || DEFAULT_STRATEGY).toLowerCase();
68
+ if (lower === 'roundrobin') return STRATEGY_LABELS['round-robin'];
69
+ return STRATEGY_LABELS[lower] || STRATEGY_LABELS[DEFAULT_STRATEGY];
70
+ }
71
+
72
+ // Re-export strategies for direct use
73
+ export { StickyStrategy } from './sticky-strategy.js';
74
+ export { RoundRobinStrategy } from './round-robin-strategy.js';
75
+ export { HybridStrategy } from './hybrid-strategy.js';
76
+ export { BaseStrategy } from './base-strategy.js';
77
+
78
+ // Re-export trackers
79
+ export { HealthTracker, TokenBucketTracker } from './trackers/index.js';
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Round-Robin Strategy
3
+ *
4
+ * Rotates to the next account on every request for maximum throughput.
5
+ * Does not maintain cache continuity but maximizes concurrent requests.
6
+ */
7
+
8
+ import { BaseStrategy } from './base-strategy.js';
9
+ import { logger } from '../../utils/logger.js';
10
+
11
+ export class RoundRobinStrategy extends BaseStrategy {
12
+ #cursor = 0; // Tracks current position in rotation
13
+
14
+ /**
15
+ * Create a new RoundRobinStrategy
16
+ * @param {Object} config - Strategy configuration
17
+ */
18
+ constructor(config = {}) {
19
+ super(config);
20
+ }
21
+
22
+ /**
23
+ * Select the next available account in rotation
24
+ *
25
+ * @param {Array} accounts - Array of account objects
26
+ * @param {string} modelId - The model ID for the request
27
+ * @param {Object} options - Additional options
28
+ * @returns {SelectionResult} The selected account and index
29
+ */
30
+ selectAccount(accounts, modelId, options = {}) {
31
+ const { onSave } = options;
32
+
33
+ if (accounts.length === 0) {
34
+ return { account: null, index: 0, waitMs: 0 };
35
+ }
36
+
37
+ // Clamp cursor to valid range
38
+ if (this.#cursor >= accounts.length) {
39
+ this.#cursor = 0;
40
+ }
41
+
42
+ // Start from the next position after the cursor
43
+ const startIndex = (this.#cursor + 1) % accounts.length;
44
+
45
+ // Try each account starting from startIndex
46
+ for (let i = 0; i < accounts.length; i++) {
47
+ const idx = (startIndex + i) % accounts.length;
48
+ const account = accounts[idx];
49
+
50
+ if (this.isAccountUsable(account, modelId)) {
51
+ account.lastUsed = Date.now();
52
+ this.#cursor = idx;
53
+
54
+ if (onSave) onSave();
55
+
56
+ const position = idx + 1;
57
+ const total = accounts.length;
58
+ logger.info(`[RoundRobinStrategy] Using account: ${account.email} (${position}/${total})`);
59
+
60
+ return { account, index: idx, waitMs: 0 };
61
+ }
62
+ }
63
+
64
+ // No usable accounts found
65
+ return { account: null, index: this.#cursor, waitMs: 0 };
66
+ }
67
+
68
+ /**
69
+ * Reset the cursor position
70
+ */
71
+ resetCursor() {
72
+ this.#cursor = 0;
73
+ }
74
+ }
75
+
76
+ export default RoundRobinStrategy;
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Sticky Strategy
3
+ *
4
+ * Keeps using the same account until it becomes unavailable (rate-limited or invalid).
5
+ * Best for prompt caching as it maintains cache continuity across requests.
6
+ */
7
+
8
+ import { BaseStrategy } from './base-strategy.js';
9
+ import { logger } from '../../utils/logger.js';
10
+ import { formatDuration } from '../../utils/helpers.js';
11
+ import { MAX_WAIT_BEFORE_ERROR_MS } from '../../constants.js';
12
+
13
+ export class StickyStrategy extends BaseStrategy {
14
+ /**
15
+ * Create a new StickyStrategy
16
+ * @param {Object} config - Strategy configuration
17
+ */
18
+ constructor(config = {}) {
19
+ super(config);
20
+ }
21
+
22
+ /**
23
+ * Select an account with sticky preference
24
+ * Prefers the current account for cache continuity, only switches when:
25
+ * - Current account is rate-limited for > 2 minutes
26
+ * - Current account is invalid
27
+ * - Current account is disabled
28
+ *
29
+ * @param {Array} accounts - Array of account objects
30
+ * @param {string} modelId - The model ID for the request
31
+ * @param {Object} options - Additional options
32
+ * @returns {SelectionResult} The selected account and index
33
+ */
34
+ selectAccount(accounts, modelId, options = {}) {
35
+ const { currentIndex = 0, onSave } = options;
36
+
37
+ if (accounts.length === 0) {
38
+ return { account: null, index: currentIndex, waitMs: 0 };
39
+ }
40
+
41
+ // Clamp index to valid range
42
+ let index = currentIndex >= accounts.length ? 0 : currentIndex;
43
+ const currentAccount = accounts[index];
44
+
45
+ // Check if current account is usable
46
+ if (this.isAccountUsable(currentAccount, modelId)) {
47
+ currentAccount.lastUsed = Date.now();
48
+ if (onSave) onSave();
49
+ return { account: currentAccount, index, waitMs: 0 };
50
+ }
51
+
52
+ // Current account is not usable - check if others are available
53
+ const usableAccounts = this.getUsableAccounts(accounts, modelId);
54
+
55
+ if (usableAccounts.length > 0) {
56
+ // Found a free account - switch immediately
57
+ const { account: nextAccount, index: nextIndex } = this.#pickNext(
58
+ accounts,
59
+ index,
60
+ modelId,
61
+ onSave
62
+ );
63
+ if (nextAccount) {
64
+ logger.info(`[StickyStrategy] Switched to new account (failover): ${nextAccount.email}`);
65
+ return { account: nextAccount, index: nextIndex, waitMs: 0 };
66
+ }
67
+ }
68
+
69
+ // No other accounts available - check if we should wait for current
70
+ const waitInfo = this.#shouldWaitForAccount(currentAccount, modelId);
71
+ if (waitInfo.shouldWait) {
72
+ logger.info(`[StickyStrategy] Waiting ${formatDuration(waitInfo.waitMs)} for sticky account: ${currentAccount.email}`);
73
+ return { account: null, index, waitMs: waitInfo.waitMs };
74
+ }
75
+
76
+ // Current account unavailable for too long, try to find any other
77
+ const { account: nextAccount, index: nextIndex } = this.#pickNext(
78
+ accounts,
79
+ index,
80
+ modelId,
81
+ onSave
82
+ );
83
+
84
+ return { account: nextAccount, index: nextIndex, waitMs: 0 };
85
+ }
86
+
87
+ /**
88
+ * Pick the next available account starting from after the current index
89
+ * @private
90
+ */
91
+ #pickNext(accounts, currentIndex, modelId, onSave) {
92
+ for (let i = 1; i <= accounts.length; i++) {
93
+ const idx = (currentIndex + i) % accounts.length;
94
+ const account = accounts[idx];
95
+
96
+ if (this.isAccountUsable(account, modelId)) {
97
+ account.lastUsed = Date.now();
98
+ if (onSave) onSave();
99
+
100
+ const position = idx + 1;
101
+ const total = accounts.length;
102
+ logger.info(`[StickyStrategy] Using account: ${account.email} (${position}/${total})`);
103
+
104
+ return { account, index: idx };
105
+ }
106
+ }
107
+
108
+ return { account: null, index: currentIndex };
109
+ }
110
+
111
+ /**
112
+ * Check if we should wait for an account's rate limit to reset
113
+ * @private
114
+ */
115
+ #shouldWaitForAccount(account, modelId) {
116
+ if (!account || account.isInvalid || account.enabled === false) {
117
+ return { shouldWait: false, waitMs: 0 };
118
+ }
119
+
120
+ let waitMs = 0;
121
+
122
+ if (modelId && account.modelRateLimits && account.modelRateLimits[modelId]) {
123
+ const limit = account.modelRateLimits[modelId];
124
+ if (limit.isRateLimited && limit.resetTime) {
125
+ waitMs = limit.resetTime - Date.now();
126
+ }
127
+ }
128
+
129
+ // Wait if within threshold
130
+ if (waitMs > 0 && waitMs <= MAX_WAIT_BEFORE_ERROR_MS) {
131
+ return { shouldWait: true, waitMs };
132
+ }
133
+
134
+ return { shouldWait: false, waitMs: 0 };
135
+ }
136
+ }
137
+
138
+ export default StickyStrategy;
@@ -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-antigravity-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: 2, // Passive recovery rate
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,8 @@
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';