antigravity-claude-proxy 2.7.1 → 2.7.3
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/README.md +0 -1
- package/package.json +1 -1
- package/public/css/style.css +1 -1
- package/public/index.html +0 -2
- package/public/js/components/account-manager.js +9 -121
- package/public/js/components/server-config.js +32 -0
- package/public/js/config/constants.js +4 -0
- package/public/views/accounts.html +0 -149
- package/public/views/settings.html +58 -0
- package/src/account-manager/credentials.js +2 -2
- package/src/account-manager/index.js +25 -87
- package/src/account-manager/onboarding.js +2 -2
- package/src/account-manager/rate-limits.js +27 -1
- package/src/account-manager/storage.js +9 -16
- package/src/auth/oauth.js +5 -4
- package/src/cli/accounts.js +2 -4
- package/src/cloudcode/message-handler.js +37 -4
- package/src/cloudcode/model-api.js +3 -2
- package/src/cloudcode/rate-limit-state.js +41 -0
- package/src/cloudcode/request-builder.js +2 -7
- package/src/cloudcode/streaming-handler.js +41 -8
- package/src/config.js +2 -0
- package/src/constants.js +1 -4
- package/src/errors.js +33 -0
- package/src/server.js +0 -2
- package/src/utils/helpers.js +18 -0
- package/src/webui/index.js +16 -73
- package/src/utils/fingerprint.js +0 -133
package/public/index.html
CHANGED
|
@@ -9,8 +9,6 @@
|
|
|
9
9
|
|
|
10
10
|
<!-- Libraries -->
|
|
11
11
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
12
|
-
<!-- Alpine.js Plugins -->
|
|
13
|
-
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/collapse@3.x.x/dist/cdn.min.js"></script>
|
|
14
12
|
<!-- Alpine.js must be deferred so stores register their listeners first -->
|
|
15
13
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
|
16
14
|
|
|
@@ -118,6 +118,15 @@ window.Components.accountManager = () => ({
|
|
|
118
118
|
|
|
119
119
|
async fixAccount(email) {
|
|
120
120
|
const store = Alpine.store('global');
|
|
121
|
+
const dataStore = Alpine.store('data');
|
|
122
|
+
// If the account has a verification URL (403 VALIDATION_REQUIRED), open it directly
|
|
123
|
+
const account = (dataStore.accounts || []).find(a => a.email === email);
|
|
124
|
+
if (account?.verifyUrl) {
|
|
125
|
+
window.open(account.verifyUrl, '_blank');
|
|
126
|
+
store.showToast(store.t('verifyThenRefresh') || 'After completing verification, click the ↻ Refresh button to re-enable this account', 'info', 10000);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// Otherwise fall back to OAuth re-auth
|
|
121
130
|
store.showToast(store.t('reauthenticating', { email: Redact.email(email) }), 'info');
|
|
122
131
|
const password = store.webuiPassword;
|
|
123
132
|
try {
|
|
@@ -203,127 +212,6 @@ window.Components.accountManager = () => ({
|
|
|
203
212
|
newModelThreshold: 10
|
|
204
213
|
},
|
|
205
214
|
|
|
206
|
-
// Fingerprint Modal
|
|
207
|
-
fingerprintModal: {
|
|
208
|
-
email: '',
|
|
209
|
-
current: null,
|
|
210
|
-
history: [],
|
|
211
|
-
loading: false,
|
|
212
|
-
regenerating: false,
|
|
213
|
-
restoring: false
|
|
214
|
-
},
|
|
215
|
-
|
|
216
|
-
async openFingerprintModal(account) {
|
|
217
|
-
const store = Alpine.store('global');
|
|
218
|
-
this.fingerprintModal = {
|
|
219
|
-
email: account.email,
|
|
220
|
-
current: null,
|
|
221
|
-
history: [],
|
|
222
|
-
loading: true,
|
|
223
|
-
regenerating: false,
|
|
224
|
-
restoring: false
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
document.getElementById('fingerprint_modal').showModal();
|
|
228
|
-
|
|
229
|
-
try {
|
|
230
|
-
const { response, newPassword } = await window.utils.request(
|
|
231
|
-
`/api/accounts/${encodeURIComponent(account.email)}/fingerprint`,
|
|
232
|
-
{},
|
|
233
|
-
store.webuiPassword
|
|
234
|
-
);
|
|
235
|
-
if (newPassword) store.webuiPassword = newPassword;
|
|
236
|
-
|
|
237
|
-
const data = await response.json();
|
|
238
|
-
if (data.status === 'ok') {
|
|
239
|
-
this.fingerprintModal.current = data.fingerprint;
|
|
240
|
-
this.fingerprintModal.history = data.history;
|
|
241
|
-
} else {
|
|
242
|
-
store.showToast(data.error || 'Failed to fetch fingerprint', 'error');
|
|
243
|
-
}
|
|
244
|
-
} catch (e) {
|
|
245
|
-
store.showToast('Error fetching fingerprint: ' + e.message, 'error');
|
|
246
|
-
} finally {
|
|
247
|
-
this.fingerprintModal.loading = false;
|
|
248
|
-
}
|
|
249
|
-
},
|
|
250
|
-
|
|
251
|
-
async regenerateFingerprint() {
|
|
252
|
-
const store = Alpine.store('global');
|
|
253
|
-
const email = this.fingerprintModal.email;
|
|
254
|
-
this.fingerprintModal.regenerating = true;
|
|
255
|
-
|
|
256
|
-
try {
|
|
257
|
-
const { response, newPassword } = await window.utils.request(
|
|
258
|
-
`/api/accounts/${encodeURIComponent(email)}/fingerprint/regenerate`,
|
|
259
|
-
{ method: 'POST' },
|
|
260
|
-
store.webuiPassword
|
|
261
|
-
);
|
|
262
|
-
if (newPassword) store.webuiPassword = newPassword;
|
|
263
|
-
|
|
264
|
-
const data = await response.json();
|
|
265
|
-
if (data.status === 'ok') {
|
|
266
|
-
store.showToast('Fingerprint regenerated', 'success');
|
|
267
|
-
// Refresh modal data
|
|
268
|
-
this.fingerprintModal.current = data.fingerprint;
|
|
269
|
-
// Fetch history again to show the old one moved to history
|
|
270
|
-
await this.refreshFingerprintData(email);
|
|
271
|
-
} else {
|
|
272
|
-
throw new Error(data.error || 'Failed to regenerate');
|
|
273
|
-
}
|
|
274
|
-
} catch (e) {
|
|
275
|
-
store.showToast('Regeneration failed: ' + e.message, 'error');
|
|
276
|
-
} finally {
|
|
277
|
-
this.fingerprintModal.regenerating = false;
|
|
278
|
-
}
|
|
279
|
-
},
|
|
280
|
-
|
|
281
|
-
async restoreFingerprint(index) {
|
|
282
|
-
const store = Alpine.store('global');
|
|
283
|
-
const email = this.fingerprintModal.email;
|
|
284
|
-
this.fingerprintModal.restoring = true;
|
|
285
|
-
|
|
286
|
-
try {
|
|
287
|
-
const { response, newPassword } = await window.utils.request(
|
|
288
|
-
`/api/accounts/${encodeURIComponent(email)}/fingerprint/restore`,
|
|
289
|
-
{
|
|
290
|
-
method: 'POST',
|
|
291
|
-
headers: { 'Content-Type': 'application/json' },
|
|
292
|
-
body: JSON.stringify({ index })
|
|
293
|
-
},
|
|
294
|
-
store.webuiPassword
|
|
295
|
-
);
|
|
296
|
-
if (newPassword) store.webuiPassword = newPassword;
|
|
297
|
-
|
|
298
|
-
const data = await response.json();
|
|
299
|
-
if (data.status === 'ok') {
|
|
300
|
-
store.showToast('Fingerprint restored', 'success');
|
|
301
|
-
this.fingerprintModal.current = data.fingerprint;
|
|
302
|
-
await this.refreshFingerprintData(email);
|
|
303
|
-
} else {
|
|
304
|
-
throw new Error(data.error || 'Failed to restore');
|
|
305
|
-
}
|
|
306
|
-
} catch (e) {
|
|
307
|
-
store.showToast('Restore failed: ' + e.message, 'error');
|
|
308
|
-
} finally {
|
|
309
|
-
this.fingerprintModal.restoring = false;
|
|
310
|
-
}
|
|
311
|
-
},
|
|
312
|
-
|
|
313
|
-
async refreshFingerprintData(email) {
|
|
314
|
-
const store = Alpine.store('global');
|
|
315
|
-
const { response, newPassword } = await window.utils.request(
|
|
316
|
-
`/api/accounts/${encodeURIComponent(email)}/fingerprint`,
|
|
317
|
-
{},
|
|
318
|
-
store.webuiPassword
|
|
319
|
-
);
|
|
320
|
-
if (newPassword) store.webuiPassword = newPassword;
|
|
321
|
-
const data = await response.json();
|
|
322
|
-
if (data.status === 'ok') {
|
|
323
|
-
this.fingerprintModal.history = data.history;
|
|
324
|
-
}
|
|
325
|
-
},
|
|
326
|
-
|
|
327
215
|
openThresholdModal(account) {
|
|
328
216
|
this.thresholdDialog = {
|
|
329
217
|
email: account.email,
|
|
@@ -323,6 +323,38 @@ window.Components.serverConfig = () => ({
|
|
|
323
323
|
}, window.AppConstants.INTERVALS.CONFIG_DEBOUNCE);
|
|
324
324
|
},
|
|
325
325
|
|
|
326
|
+
async toggleRequestThrottling(enabled) {
|
|
327
|
+
const store = Alpine.store('global');
|
|
328
|
+
const previousValue = this.serverConfig.requestThrottlingEnabled;
|
|
329
|
+
this.serverConfig.requestThrottlingEnabled = enabled;
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
const { response, newPassword } = await window.utils.request('/api/config', {
|
|
333
|
+
method: 'POST',
|
|
334
|
+
headers: { 'Content-Type': 'application/json' },
|
|
335
|
+
body: JSON.stringify({ requestThrottlingEnabled: enabled })
|
|
336
|
+
}, store.webuiPassword);
|
|
337
|
+
|
|
338
|
+
if (newPassword) store.webuiPassword = newPassword;
|
|
339
|
+
|
|
340
|
+
const data = await response.json();
|
|
341
|
+
if (data.status === 'ok') {
|
|
342
|
+
store.showToast(`Request Throttling ${enabled ? 'enabled' : 'disabled'}`, 'success');
|
|
343
|
+
} else {
|
|
344
|
+
throw new Error(data.error || 'Failed to update');
|
|
345
|
+
}
|
|
346
|
+
} catch (e) {
|
|
347
|
+
this.serverConfig.requestThrottlingEnabled = previousValue;
|
|
348
|
+
store.showToast('Failed to update Request Throttling: ' + e.message, 'error');
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
|
|
352
|
+
toggleRequestDelayMs(value) {
|
|
353
|
+
const { REQUEST_DELAY_MIN, REQUEST_DELAY_MAX } = window.AppConstants.VALIDATION;
|
|
354
|
+
this.saveConfigField('requestDelayMs', value, 'Request Delay',
|
|
355
|
+
(v) => window.Validators.validateRange(v, REQUEST_DELAY_MIN, REQUEST_DELAY_MAX, 'Request Delay'));
|
|
356
|
+
},
|
|
357
|
+
|
|
326
358
|
toggleMaxAccounts(value) {
|
|
327
359
|
const { MAX_ACCOUNTS_MIN, MAX_ACCOUNTS_MAX } = window.AppConstants.VALIDATION;
|
|
328
360
|
this.saveConfigField('maxAccounts', value, 'Max Accounts',
|
|
@@ -96,6 +96,10 @@ window.AppConstants.VALIDATION = {
|
|
|
96
96
|
GLOBAL_QUOTA_THRESHOLD_MIN: 0,
|
|
97
97
|
GLOBAL_QUOTA_THRESHOLD_MAX: 99,
|
|
98
98
|
|
|
99
|
+
// Request delay (100 - 5000ms)
|
|
100
|
+
REQUEST_DELAY_MIN: 100,
|
|
101
|
+
REQUEST_DELAY_MAX: 5000,
|
|
102
|
+
|
|
99
103
|
// Switch account delay (1s - 60s)
|
|
100
104
|
SWITCH_ACCOUNT_DELAY_MIN: 1000,
|
|
101
105
|
SWITCH_ACCOUNT_DELAY_MAX: 60000,
|
|
@@ -88,7 +88,6 @@
|
|
|
88
88
|
<th class="pl-6 py-3 text-left text-[10px] font-bold text-gray-500 uppercase tracking-wider w-16" x-text="$store.global.t('enabled')">Enabled</th>
|
|
89
89
|
<th class="py-3 text-left text-[10px] font-bold text-gray-500 uppercase tracking-wider flex-1 min-w-[200px]" x-text="$store.global.t('accountEmail')">Account (Email)</th>
|
|
90
90
|
<th class="py-3 text-left text-[10px] font-bold text-gray-500 uppercase tracking-wider w-20" x-text="$store.global.t('source')">Source</th>
|
|
91
|
-
<th class="py-3 text-left text-[10px] font-bold text-gray-500 uppercase tracking-wider w-24">Fingerprint</th>
|
|
92
91
|
<th class="py-3 text-left text-[10px] font-bold text-gray-500 uppercase tracking-wider w-16" x-text="$store.global.t('tier')">Tier</th>
|
|
93
92
|
<th class="py-3 text-left text-[10px] font-bold text-gray-500 uppercase tracking-wider w-32" x-text="$store.global.t('quota')">Quota</th>
|
|
94
93
|
<th class="py-3 text-left text-[10px] font-bold text-gray-500 uppercase tracking-wider w-24" x-text="$store.global.t('health')">Health</th>
|
|
@@ -152,15 +151,6 @@
|
|
|
152
151
|
x-text="acc.source || 'oauth'">
|
|
153
152
|
</span>
|
|
154
153
|
</td>
|
|
155
|
-
<td class="py-4">
|
|
156
|
-
<button class="p-2 rounded-lg hover:bg-space-800 text-gray-500 hover:text-neon-cyan transition-colors"
|
|
157
|
-
@click="openFingerprintModal(acc)"
|
|
158
|
-
:title="'View Fingerprint for ' + formatEmail(acc.email)">
|
|
159
|
-
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
160
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 11c0 3.517-1.009 6.799-2.753 9.571m-3.44-2.04l.054-.09A13.916 13.916 0 008 11a4 4 0 118 0c0 1.017-.07 2.019-.203 3m-2.118 6.844A21.88 21.88 0 0015.171 17m3.839 1.132c.645-2.266.99-4.659.99-7.132A8 8 0 008 4.07M3 15.364c.64-1.319 1-2.8 1-4.364 0-1.457.2-2.873.571-4.241m2.823 10.36c.682-1.365 1.178-2.791 1.406-4.117m8.406 5.828a5.74 5.74 0 00-1.552-1.433" />
|
|
161
|
-
</svg>
|
|
162
|
-
</button>
|
|
163
|
-
</td>
|
|
164
154
|
<td class="py-4">
|
|
165
155
|
<span :class="{
|
|
166
156
|
'status-pill-ultra': acc.subscription?.tier === 'ultra',
|
|
@@ -647,143 +637,4 @@
|
|
|
647
637
|
<button>close</button>
|
|
648
638
|
</form>
|
|
649
639
|
</dialog>
|
|
650
|
-
|
|
651
|
-
<!-- Fingerprint Details Modal -->
|
|
652
|
-
<dialog id="fingerprint_modal" class="modal backdrop-blur-sm">
|
|
653
|
-
<div class="modal-box max-w-2xl w-full bg-space-900 border border-space-border text-gray-300 shadow-2xl p-6 relative">
|
|
654
|
-
<!-- Close Button (Top Right) -->
|
|
655
|
-
<form method="dialog" class="absolute top-4 right-4">
|
|
656
|
-
<button class="btn btn-sm btn-circle btn-ghost text-gray-500 hover:text-white">
|
|
657
|
-
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
658
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
659
|
-
</svg>
|
|
660
|
-
</button>
|
|
661
|
-
</form>
|
|
662
|
-
|
|
663
|
-
<h3 class="font-bold text-xl text-white mb-2 flex items-center gap-2">
|
|
664
|
-
<svg class="w-6 h-6 text-neon-cyan" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
665
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 11c0 3.517-1.009 6.799-2.753 9.571m-3.44-2.04l.054-.09A13.916 13.916 0 008 11a4 4 0 118 0c0 1.017-.07 2.019-.203 3m-2.118 6.844A21.88 21.88 0 0015.171 17m3.839 1.132c.645-2.266.99-4.659.99-7.132A8 8 0 008 4.07M3 15.364c.64-1.319 1-2.8 1-4.364 0-1.457.2-2.873.571-4.241m2.823 10.36c.682-1.365 1.178-2.791 1.406-4.117m8.406 5.828a5.74 5.74 0 00-1.552-1.433" />
|
|
666
|
-
</svg>
|
|
667
|
-
<span>Device Fingerprint</span>
|
|
668
|
-
</h3>
|
|
669
|
-
<p class="text-sm text-gray-500 font-mono mb-6" x-text="Redact.email(fingerprintModal.email)"></p>
|
|
670
|
-
|
|
671
|
-
<div x-show="fingerprintModal.loading" class="flex justify-center py-8">
|
|
672
|
-
<span class="loading loading-spinner text-neon-cyan"></span>
|
|
673
|
-
</div>
|
|
674
|
-
|
|
675
|
-
<div x-show="!fingerprintModal.loading && fingerprintModal.current">
|
|
676
|
-
<!-- Current Fingerprint -->
|
|
677
|
-
<div class="bg-space-800/50 border border-space-border/30 rounded-lg p-4 mb-6 relative overflow-hidden">
|
|
678
|
-
<div class="absolute top-0 right-0 p-2 opacity-10">
|
|
679
|
-
<svg class="w-24 h-24 text-neon-cyan" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
680
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 11c0 3.517-1.009 6.799-2.753 9.571m-3.44-2.04l.054-.09A13.916 13.916 0 008 11a4 4 0 118 0c0 1.017-.07 2.019-.203 3m-2.118 6.844A21.88 21.88 0 0015.171 17m3.839 1.132c.645-2.266.99-4.659.99-7.132A8 8 0 008 4.07M3 15.364c.64-1.319 1-2.8 1-4.364 0-1.457.2-2.873.571-4.241m2.823 10.36c.682-1.365 1.178-2.791 1.406-4.117m8.406 5.828a5.74 5.74 0 00-1.552-1.433" />
|
|
681
|
-
</svg>
|
|
682
|
-
</div>
|
|
683
|
-
|
|
684
|
-
<div class="grid grid-cols-1 gap-3 relative z-10">
|
|
685
|
-
<div>
|
|
686
|
-
<span class="text-[10px] uppercase text-gray-500 font-bold tracking-wider">Device ID</span>
|
|
687
|
-
<div class="font-mono text-sm text-neon-cyan break-all" x-text="fingerprintModal.current?.deviceId"></div>
|
|
688
|
-
</div>
|
|
689
|
-
<div>
|
|
690
|
-
<span class="text-[10px] uppercase text-gray-500 font-bold tracking-wider">User Agent</span>
|
|
691
|
-
<div class="font-mono text-xs text-gray-300 break-words bg-space-900/50 p-2 rounded border border-space-border/20 w-fit max-w-full"
|
|
692
|
-
x-text="fingerprintModal.current?.userAgent"></div>
|
|
693
|
-
</div>
|
|
694
|
-
<div class="grid grid-cols-2 gap-4">
|
|
695
|
-
<div>
|
|
696
|
-
<span class="text-[10px] uppercase text-gray-500 font-bold tracking-wider">Platform</span>
|
|
697
|
-
<div class="font-mono text-xs text-gray-300"
|
|
698
|
-
x-text="fingerprintModal.current?.clientMetadata?.platform || 'UNK'"></div>
|
|
699
|
-
</div>
|
|
700
|
-
<div>
|
|
701
|
-
<span class="text-[10px] uppercase text-gray-500 font-bold tracking-wider">Created</span>
|
|
702
|
-
<div class="font-mono text-xs text-gray-400"
|
|
703
|
-
x-text="new Date(fingerprintModal.current?.createdAt).toLocaleString()"></div>
|
|
704
|
-
</div>
|
|
705
|
-
</div>
|
|
706
|
-
</div>
|
|
707
|
-
|
|
708
|
-
<!-- Advanced Details (Collapsible) -->
|
|
709
|
-
<div class="mt-4 pt-4 border-t border-space-border/20" x-data="{ expanded: false }">
|
|
710
|
-
<button @click="expanded = !expanded"
|
|
711
|
-
class="flex items-center gap-2 text-[10px] uppercase text-gray-500 font-bold tracking-wider hover:text-neon-cyan transition-colors w-full">
|
|
712
|
-
<svg class="w-3 h-3 transition-transform" :class="{ 'rotate-90': expanded }" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
713
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
|
714
|
-
</svg>
|
|
715
|
-
Advanced Details
|
|
716
|
-
</button>
|
|
717
|
-
|
|
718
|
-
<div x-show="expanded" x-collapse class="mt-3 grid grid-cols-1 gap-3 relative z-10">
|
|
719
|
-
<div>
|
|
720
|
-
<span class="text-[10px] uppercase text-gray-500 font-bold tracking-wider">API Client</span>
|
|
721
|
-
<div class="font-mono text-xs text-gray-300 break-words" x-text="fingerprintModal.current?.apiClient"></div>
|
|
722
|
-
</div>
|
|
723
|
-
<div>
|
|
724
|
-
<span class="text-[10px] uppercase text-gray-500 font-bold tracking-wider">Quota User</span>
|
|
725
|
-
<div class="font-mono text-xs text-gray-300 break-all" x-text="fingerprintModal.current?.quotaUser"></div>
|
|
726
|
-
</div>
|
|
727
|
-
<div>
|
|
728
|
-
<span class="text-[10px] uppercase text-gray-500 font-bold tracking-wider">Session Token</span>
|
|
729
|
-
<div class="font-mono text-xs text-gray-300 break-all" x-text="fingerprintModal.current?.sessionToken"></div>
|
|
730
|
-
</div>
|
|
731
|
-
<div>
|
|
732
|
-
<span class="text-[10px] uppercase text-gray-500 font-bold tracking-wider">Metadata</span>
|
|
733
|
-
<div class="font-mono text-[10px] text-gray-400 bg-space-900/50 p-2 rounded border border-space-border/20 mt-1">
|
|
734
|
-
<div class="grid grid-cols-2 gap-x-4 gap-y-1">
|
|
735
|
-
<div><span class="text-gray-600">OS Version:</span> <span class="text-gray-300" x-text="fingerprintModal.current?.clientMetadata?.osVersion"></span></div>
|
|
736
|
-
<div><span class="text-gray-600">Arch:</span> <span class="text-gray-300" x-text="fingerprintModal.current?.clientMetadata?.arch"></span></div>
|
|
737
|
-
<div><span class="text-gray-600">IDE:</span> <span class="text-gray-300" x-text="fingerprintModal.current?.clientMetadata?.ideType"></span></div>
|
|
738
|
-
<div><span class="text-gray-600">Plugin:</span> <span class="text-gray-300" x-text="fingerprintModal.current?.clientMetadata?.pluginType"></span></div>
|
|
739
|
-
</div>
|
|
740
|
-
</div>
|
|
741
|
-
</div>
|
|
742
|
-
</div>
|
|
743
|
-
</div>
|
|
744
|
-
</div>
|
|
745
|
-
|
|
746
|
-
<!-- Actions -->
|
|
747
|
-
<div class="flex justify-end mb-6">
|
|
748
|
-
<button class="btn btn-sm btn-outline border-neon-cyan text-neon-cyan hover:bg-neon-cyan hover:text-black gap-2"
|
|
749
|
-
@click="regenerateFingerprint()"
|
|
750
|
-
:disabled="fingerprintModal.regenerating">
|
|
751
|
-
<svg class="w-4 h-4" :class="{ 'animate-spin': fingerprintModal.regenerating }" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
752
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
|
753
|
-
</svg>
|
|
754
|
-
<span x-text="fingerprintModal.regenerating ? 'Regenerating...' : 'Regenerate Fingerprint'"></span>
|
|
755
|
-
</button>
|
|
756
|
-
</div>
|
|
757
|
-
|
|
758
|
-
<!-- History -->
|
|
759
|
-
<div x-show="fingerprintModal.history.length > 0">
|
|
760
|
-
<h4 class="text-sm font-bold text-gray-400 mb-3 uppercase tracking-wider">History</h4>
|
|
761
|
-
<div class="space-y-2 max-h-40 overflow-y-auto custom-scrollbar">
|
|
762
|
-
<template x-for="(entry, index) in fingerprintModal.history" :key="entry.timestamp">
|
|
763
|
-
<div class="flex items-center justify-between p-3 bg-space-800/30 border border-space-border/20 rounded hover:bg-space-800/50 transition-colors">
|
|
764
|
-
<div class="flex-1 min-w-0 mr-4">
|
|
765
|
-
<div class="flex items-center gap-2 mb-1">
|
|
766
|
-
<span class="text-xs font-mono text-gray-400 truncate" x-text="entry.fingerprint.deviceId"></span>
|
|
767
|
-
<span class="text-[10px] px-1.5 py-0.5 rounded bg-space-700 text-gray-500" x-text="entry.reason"></span>
|
|
768
|
-
</div>
|
|
769
|
-
<div class="text-[10px] text-gray-600 font-mono" x-text="new Date(entry.timestamp).toLocaleString()"></div>
|
|
770
|
-
</div>
|
|
771
|
-
<button class="btn btn-xs btn-ghost text-gray-500 hover:text-white"
|
|
772
|
-
@click="restoreFingerprint(index)"
|
|
773
|
-
:disabled="fingerprintModal.restoring"
|
|
774
|
-
title="Restore this fingerprint">
|
|
775
|
-
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
776
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6" />
|
|
777
|
-
</svg>
|
|
778
|
-
</button>
|
|
779
|
-
</div>
|
|
780
|
-
</template>
|
|
781
|
-
</div>
|
|
782
|
-
</div>
|
|
783
|
-
</div>
|
|
784
|
-
</div>
|
|
785
|
-
<form method="dialog" class="modal-backdrop">
|
|
786
|
-
<button>close</button>
|
|
787
|
-
</form>
|
|
788
|
-
</dialog>
|
|
789
640
|
</div>
|
|
@@ -1750,6 +1750,64 @@
|
|
|
1750
1750
|
</div>
|
|
1751
1751
|
</div>
|
|
1752
1752
|
|
|
1753
|
+
<!-- Request Throttling -->
|
|
1754
|
+
<div class="space-y-4 pt-2 border-t border-space-border/10">
|
|
1755
|
+
<div class="flex items-center gap-2 mb-2">
|
|
1756
|
+
<span class="text-[10px] text-gray-500 font-bold uppercase tracking-widest">Request Throttling</span>
|
|
1757
|
+
<span class="px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wide rounded bg-amber-500/20 text-amber-400 border border-amber-500/40">Experimental</span>
|
|
1758
|
+
</div>
|
|
1759
|
+
|
|
1760
|
+
<!-- Request Throttling Card -->
|
|
1761
|
+
<div class="form-control bg-space-900/50 rounded-lg border transition-all duration-300 hover:border-amber-500/50"
|
|
1762
|
+
:class="serverConfig.requestThrottlingEnabled ? 'border-amber-500 bg-amber-500/10 shadow-[0_0_25px_rgba(245,158,11,0.2)] ring-1 ring-amber-500/30' : 'border-space-border/50'">
|
|
1763
|
+
|
|
1764
|
+
<!-- Toggle Row -->
|
|
1765
|
+
<div class="flex items-center justify-between p-4">
|
|
1766
|
+
<div class="flex flex-col gap-1">
|
|
1767
|
+
<span class="label-text font-medium transition-colors"
|
|
1768
|
+
:class="serverConfig.requestThrottlingEnabled ? 'text-amber-300' : 'text-gray-300'">Enable Request Throttling</span>
|
|
1769
|
+
<span class="text-xs transition-colors"
|
|
1770
|
+
:class="serverConfig.requestThrottlingEnabled ? 'text-amber-100/50' : 'text-gray-500'">Add delay before each Google API request to avoid rate limits.</span>
|
|
1771
|
+
</div>
|
|
1772
|
+
<label class="relative inline-flex items-center cursor-pointer">
|
|
1773
|
+
<input type="checkbox" class="sr-only peer"
|
|
1774
|
+
:checked="serverConfig.requestThrottlingEnabled"
|
|
1775
|
+
@change="toggleRequestThrottling($event.target.checked)"
|
|
1776
|
+
aria-label="Request throttling toggle">
|
|
1777
|
+
<div
|
|
1778
|
+
class="w-9 h-5 bg-space-800 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-gray-600 after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-neon-green peer-checked:after:bg-white">
|
|
1779
|
+
</div>
|
|
1780
|
+
</label>
|
|
1781
|
+
</div>
|
|
1782
|
+
|
|
1783
|
+
<!-- Delay Slider (only shown when enabled) -->
|
|
1784
|
+
<div x-show="serverConfig.requestThrottlingEnabled" x-transition x-collapse>
|
|
1785
|
+
<div class="border-t border-amber-500/20 px-4 pb-4 pt-3">
|
|
1786
|
+
<label class="flex justify-between items-center mb-2">
|
|
1787
|
+
<span class="text-xs text-amber-200/70">Delay per Request</span>
|
|
1788
|
+
<span class="font-mono text-amber-400 text-xs font-semibold"
|
|
1789
|
+
x-text="(serverConfig.requestDelayMs || 200) + 'ms'"></span>
|
|
1790
|
+
</label>
|
|
1791
|
+
<div class="flex gap-3 items-center">
|
|
1792
|
+
<input type="range" min="100" max="5000" step="100"
|
|
1793
|
+
class="custom-range custom-range-yellow flex-1"
|
|
1794
|
+
:value="serverConfig.requestDelayMs || 200"
|
|
1795
|
+
:style="`background-size: ${((serverConfig.requestDelayMs || 200) - 100) / 49}% 100%`"
|
|
1796
|
+
@input="toggleRequestDelayMs($event.target.value)"
|
|
1797
|
+
aria-label="Request delay slider">
|
|
1798
|
+
<input type="number" min="100" max="5000" step="100"
|
|
1799
|
+
class="input input-xs input-bordered w-20 bg-space-800/50 border-amber-500/30 text-white font-mono text-center"
|
|
1800
|
+
:value="serverConfig.requestDelayMs || 200"
|
|
1801
|
+
@change="toggleRequestDelayMs($event.target.value)"
|
|
1802
|
+
aria-label="Request delay value">
|
|
1803
|
+
</div>
|
|
1804
|
+
<p class="text-[9px] text-amber-100/40 mt-2 leading-tight">
|
|
1805
|
+
Milliseconds to wait before each API call (token refresh, quota check, messages, etc).</p>
|
|
1806
|
+
</div>
|
|
1807
|
+
</div>
|
|
1808
|
+
</div>
|
|
1809
|
+
</div>
|
|
1810
|
+
|
|
1753
1811
|
<!-- Error Handling Tuning -->
|
|
1754
1812
|
<div class="space-y-4 pt-2 border-t border-space-border/10">
|
|
1755
1813
|
<div class="flex items-center gap-2 mb-2">
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
import { refreshAccessToken, parseRefreshParts, formatRefreshParts } from '../auth/oauth.js';
|
|
16
16
|
import { getAuthStatus } from '../auth/database.js';
|
|
17
17
|
import { logger } from '../utils/logger.js';
|
|
18
|
-
import { isNetworkError } from '../utils/helpers.js';
|
|
18
|
+
import { isNetworkError, throttledFetch } from '../utils/helpers.js';
|
|
19
19
|
import { onboardUser, getDefaultTierId } from './onboarding.js';
|
|
20
20
|
import { parseTierId } from '../cloudcode/model-api.js';
|
|
21
21
|
|
|
@@ -228,7 +228,7 @@ export async function discoverProject(token, projectId = undefined) {
|
|
|
228
228
|
|
|
229
229
|
for (const endpoint of LOAD_CODE_ASSIST_ENDPOINTS) {
|
|
230
230
|
try {
|
|
231
|
-
const response = await
|
|
231
|
+
const response = await throttledFetch(`${endpoint}/v1internal:loadCodeAssist`, {
|
|
232
232
|
method: 'POST',
|
|
233
233
|
headers: {
|
|
234
234
|
'Authorization': `Bearer ${token}`,
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
resetAllRateLimits as resetLimits,
|
|
16
16
|
markRateLimited as markLimited,
|
|
17
17
|
markInvalid as markAccountInvalid,
|
|
18
|
+
clearInvalid as clearAccountInvalid,
|
|
18
19
|
getMinWaitTimeMs as getMinWait,
|
|
19
20
|
getRateLimitInfo as getLimitInfo,
|
|
20
21
|
getConsecutiveFailures as getFailures,
|
|
@@ -34,7 +35,6 @@ import {
|
|
|
34
35
|
} from './credentials.js';
|
|
35
36
|
import { createStrategy, getStrategyLabel, DEFAULT_STRATEGY } from './strategies/index.js';
|
|
36
37
|
import { logger } from '../utils/logger.js';
|
|
37
|
-
import { generateFingerprint, MAX_FINGERPRINT_HISTORY } from '../utils/fingerprint.js';
|
|
38
38
|
|
|
39
39
|
export class AccountManager {
|
|
40
40
|
#accounts = [];
|
|
@@ -138,6 +138,16 @@ export class AccountManager {
|
|
|
138
138
|
return getInvalid(this.#accounts);
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Check if all enabled accounts are invalid (need user intervention).
|
|
143
|
+
* Unlike rate limits, invalid accounts won't self-recover — waiting is pointless.
|
|
144
|
+
* @returns {boolean} True if every enabled account is invalid
|
|
145
|
+
*/
|
|
146
|
+
isAllAccountsInvalid() {
|
|
147
|
+
const enabled = this.#accounts.filter(a => a.enabled !== false);
|
|
148
|
+
return enabled.length > 0 && enabled.every(a => a.isInvalid);
|
|
149
|
+
}
|
|
150
|
+
|
|
141
151
|
/**
|
|
142
152
|
* Clear expired rate limits
|
|
143
153
|
* @returns {number} Number of rate limits cleared
|
|
@@ -283,9 +293,19 @@ export class AccountManager {
|
|
|
283
293
|
* Mark an account as invalid (credentials need re-authentication)
|
|
284
294
|
* @param {string} email - Email of the account to mark
|
|
285
295
|
* @param {string} reason - Reason for marking as invalid
|
|
296
|
+
* @param {string|null} verifyUrl - Optional verification URL (for 403 VALIDATION_REQUIRED)
|
|
297
|
+
*/
|
|
298
|
+
markInvalid(email, reason = 'Unknown error', verifyUrl = null) {
|
|
299
|
+
markAccountInvalid(this.#accounts, email, reason, verifyUrl);
|
|
300
|
+
this.saveToDisk();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Clear invalid status for an account (after user completes verification)
|
|
305
|
+
* @param {string} email - Email of the account to clear
|
|
286
306
|
*/
|
|
287
|
-
|
|
288
|
-
|
|
307
|
+
clearInvalid(email) {
|
|
308
|
+
clearAccountInvalid(this.#accounts, email);
|
|
289
309
|
this.saveToDisk();
|
|
290
310
|
}
|
|
291
311
|
|
|
@@ -434,11 +454,11 @@ export class AccountManager {
|
|
|
434
454
|
modelRateLimits: a.modelRateLimits || {},
|
|
435
455
|
isInvalid: a.isInvalid || false,
|
|
436
456
|
invalidReason: a.invalidReason || null,
|
|
457
|
+
verifyUrl: a.verifyUrl || null,
|
|
437
458
|
lastUsed: a.lastUsed,
|
|
438
459
|
// Include quota threshold settings
|
|
439
460
|
quotaThreshold: a.quotaThreshold,
|
|
440
|
-
modelQuotaThresholds: a.modelQuotaThresholds || {}
|
|
441
|
-
hasFingerprint: !!a.fingerprint
|
|
461
|
+
modelQuotaThresholds: a.modelQuotaThresholds || {}
|
|
442
462
|
}))
|
|
443
463
|
};
|
|
444
464
|
}
|
|
@@ -503,88 +523,6 @@ export class AccountManager {
|
|
|
503
523
|
getAllAccounts() {
|
|
504
524
|
return this.#accounts;
|
|
505
525
|
}
|
|
506
|
-
|
|
507
|
-
/**
|
|
508
|
-
* Regenerate fingerprint for an account
|
|
509
|
-
* @param {string} email - Email of the account
|
|
510
|
-
* @returns {Object|null} New fingerprint or null if account not found
|
|
511
|
-
*/
|
|
512
|
-
regenerateFingerprint(email) {
|
|
513
|
-
const account = this.#accounts.find(a => a.email === email);
|
|
514
|
-
if (!account) return null;
|
|
515
|
-
|
|
516
|
-
if (account.fingerprint) {
|
|
517
|
-
const historyEntry = {
|
|
518
|
-
fingerprint: account.fingerprint,
|
|
519
|
-
timestamp: Date.now(),
|
|
520
|
-
reason: 'regenerated'
|
|
521
|
-
};
|
|
522
|
-
|
|
523
|
-
if (!account.fingerprintHistory) {
|
|
524
|
-
account.fingerprintHistory = [];
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
account.fingerprintHistory.unshift(historyEntry);
|
|
528
|
-
if (account.fingerprintHistory.length > MAX_FINGERPRINT_HISTORY) {
|
|
529
|
-
account.fingerprintHistory = account.fingerprintHistory.slice(0, MAX_FINGERPRINT_HISTORY);
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
account.fingerprint = generateFingerprint();
|
|
534
|
-
this.saveToDisk();
|
|
535
|
-
return account.fingerprint;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
/**
|
|
539
|
-
* Restore fingerprint from history
|
|
540
|
-
* @param {string} email - Email of the account
|
|
541
|
-
* @param {number} historyIndex - Index in history array (0 is most recent)
|
|
542
|
-
* @returns {Object|null} Restored fingerprint or null
|
|
543
|
-
*/
|
|
544
|
-
restoreFingerprint(email, historyIndex) {
|
|
545
|
-
const account = this.#accounts.find(a => a.email === email);
|
|
546
|
-
if (!account || !account.fingerprintHistory || !account.fingerprintHistory[historyIndex]) {
|
|
547
|
-
return null;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
const restoredEntry = account.fingerprintHistory[historyIndex];
|
|
551
|
-
|
|
552
|
-
// Save current to history before restoring
|
|
553
|
-
if (account.fingerprint) {
|
|
554
|
-
const historyEntry = {
|
|
555
|
-
fingerprint: account.fingerprint,
|
|
556
|
-
timestamp: Date.now(),
|
|
557
|
-
reason: 'restored'
|
|
558
|
-
};
|
|
559
|
-
// account.fingerprintHistory is guaranteed to exist if we are here
|
|
560
|
-
account.fingerprintHistory.unshift(historyEntry);
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
// Remove the restored entry from history (shifted by 1 if we just unshifted)
|
|
564
|
-
const removeIndex = account.fingerprint ? historyIndex + 1 : historyIndex;
|
|
565
|
-
account.fingerprintHistory.splice(removeIndex, 1);
|
|
566
|
-
|
|
567
|
-
// Restore
|
|
568
|
-
account.fingerprint = { ...restoredEntry.fingerprint, createdAt: Date.now() };
|
|
569
|
-
|
|
570
|
-
// Trim history again (since we added one)
|
|
571
|
-
if (account.fingerprintHistory.length > MAX_FINGERPRINT_HISTORY) {
|
|
572
|
-
account.fingerprintHistory = account.fingerprintHistory.slice(0, MAX_FINGERPRINT_HISTORY);
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
this.saveToDisk();
|
|
576
|
-
return account.fingerprint;
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
/**
|
|
580
|
-
* Get fingerprint history
|
|
581
|
-
* @param {string} email - Email of the account
|
|
582
|
-
* @returns {Array} Array of fingerprint history entries
|
|
583
|
-
*/
|
|
584
|
-
getFingerprintHistory(email) {
|
|
585
|
-
const account = this.#accounts.find(a => a.email === email);
|
|
586
|
-
return account ? (account.fingerprintHistory || []) : [];
|
|
587
|
-
}
|
|
588
526
|
}
|
|
589
527
|
|
|
590
528
|
// Re-export CooldownReason for use by handlers
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
CLIENT_METADATA
|
|
11
11
|
} from '../constants.js';
|
|
12
12
|
import { logger } from '../utils/logger.js';
|
|
13
|
-
import { sleep } from '../utils/helpers.js';
|
|
13
|
+
import { sleep, throttledFetch } from '../utils/helpers.js';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Get the default tier ID from allowed tiers list
|
|
@@ -64,7 +64,7 @@ export async function onboardUser(token, tierId, projectId = undefined, maxAttem
|
|
|
64
64
|
for (const endpoint of ONBOARD_USER_ENDPOINTS) {
|
|
65
65
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
66
66
|
try {
|
|
67
|
-
const response = await
|
|
67
|
+
const response = await throttledFetch(`${endpoint}/v1internal:onboardUser`, {
|
|
68
68
|
method: 'POST',
|
|
69
69
|
headers: {
|
|
70
70
|
'Authorization': `Bearer ${token}`,
|