brave-real-browser-mcp-server 2.28.1 โ 2.29.1
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 +127 -20
- package/dist/browser-manager.js +344 -0
- package/dist/handlers/advanced-tools.js +863 -170
- package/dist/handlers/navigation-handlers.js +185 -16
- package/dist/handlers/tool-executor.js +201 -0
- package/dist/tool-definitions.js +104 -6
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -107,6 +107,115 @@ npm run dev
|
|
|
107
107
|
|
|
108
108
|
---
|
|
109
109
|
|
|
110
|
+
## ๐ New in v2.28.1 - Advanced Enhancements
|
|
111
|
+
|
|
112
|
+
### ๐ Advanced Navigation
|
|
113
|
+
| Option | Description |
|
|
114
|
+
|--------|-------------|
|
|
115
|
+
| `blockResources` | Block images, fonts, CSS for faster loading |
|
|
116
|
+
| `customHeaders` | Set custom HTTP headers |
|
|
117
|
+
| `referrer` | Custom referrer URL |
|
|
118
|
+
| `waitForSelector` | Wait for specific element after load |
|
|
119
|
+
| `waitForContent` | Wait for specific text content |
|
|
120
|
+
| `scrollToBottom` | Auto-scroll for lazy loading |
|
|
121
|
+
| `randomDelay` | Human-like delay (100-500ms) |
|
|
122
|
+
| `bypassCSP` | Bypass Content Security Policy |
|
|
123
|
+
|
|
124
|
+
**Example:**
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
"url": "https://example.com",
|
|
128
|
+
"blockResources": ["image", "font"],
|
|
129
|
+
"waitForSelector": ".main-content",
|
|
130
|
+
"scrollToBottom": true,
|
|
131
|
+
"randomDelay": true
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### ๐ Parallel Scraping (js_scrape)
|
|
136
|
+
| Option | Description |
|
|
137
|
+
|--------|-------------|
|
|
138
|
+
| `urls` | Array of multiple URLs to scrape |
|
|
139
|
+
| `concurrency` | Max concurrent scrapes (1-10) |
|
|
140
|
+
| `continueOnError` | Continue even if some URLs fail |
|
|
141
|
+
| `delayBetween` | Delay between scrapes (ms) |
|
|
142
|
+
|
|
143
|
+
**Example:**
|
|
144
|
+
```json
|
|
145
|
+
{
|
|
146
|
+
"urls": ["https://site1.com", "https://site2.com", "https://site3.com"],
|
|
147
|
+
"concurrency": 3,
|
|
148
|
+
"extractSelector": "article"
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### ๐ Auto-Pagination (link_harvester)
|
|
153
|
+
| Option | Description |
|
|
154
|
+
|--------|-------------|
|
|
155
|
+
| `followPagination` | Auto-follow pagination links |
|
|
156
|
+
| `maxPages` | Maximum pages to scrape (1-20) |
|
|
157
|
+
| `paginationSelector` | Custom next page selector |
|
|
158
|
+
| `delayBetweenPages` | Delay between pages (ms) |
|
|
159
|
+
|
|
160
|
+
**Example:**
|
|
161
|
+
```json
|
|
162
|
+
{
|
|
163
|
+
"followPagination": true,
|
|
164
|
+
"maxPages": 10,
|
|
165
|
+
"filter": "movie"
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### ๐ API Interceptor (network_recorder)
|
|
170
|
+
| Option | Description |
|
|
171
|
+
|--------|-------------|
|
|
172
|
+
| `interceptMode` | `record`, `intercept`, or `mock` |
|
|
173
|
+
| `blockPatterns` | URL patterns to block |
|
|
174
|
+
| `mockResponses` | Fake responses for URLs |
|
|
175
|
+
| `modifyHeaders` | Modify request headers |
|
|
176
|
+
| `capturePayloads` | Capture POST/PUT bodies |
|
|
177
|
+
|
|
178
|
+
**Example:**
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"interceptMode": "intercept",
|
|
182
|
+
"blockPatterns": ["ads", "tracking"],
|
|
183
|
+
"capturePayloads": true
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### ๐ก๏ธ Anti-Detection Enhancements
|
|
188
|
+
|
|
189
|
+
| Feature | Description |
|
|
190
|
+
|---------|-------------|
|
|
191
|
+
| **Fingerprint Randomizer** | Canvas, Audio, Hardware randomization |
|
|
192
|
+
| **Human Behavior Simulation** | Natural mouse/scroll patterns |
|
|
193
|
+
| **WebGL Spoofing** | Random GPU from 8 configs (Intel, NVIDIA, AMD) |
|
|
194
|
+
| **Proxy Rotation** | round-robin, random, least-used, on-error strategies |
|
|
195
|
+
| **Rate Limiter** | Per-second/minute limits, domain-specific |
|
|
196
|
+
| **Error Auto-Recovery** | Smart retry with different strategies |
|
|
197
|
+
|
|
198
|
+
### ๐ Internal Systems
|
|
199
|
+
|
|
200
|
+
```javascript
|
|
201
|
+
// Rate Limiter
|
|
202
|
+
initRateLimiter({ requestsPerSecond: 5, requestsPerMinute: 100 });
|
|
203
|
+
setDomainRateLimit('api.example.com', 2); // 2 req/sec for this domain
|
|
204
|
+
|
|
205
|
+
// Proxy Rotation
|
|
206
|
+
initProxyRotation(['proxy1:8080', 'proxy2:8080'], 'round-robin');
|
|
207
|
+
rotateProxy('error'); // Rotate on error
|
|
208
|
+
|
|
209
|
+
// Error Recovery
|
|
210
|
+
executeWithRecovery(operation, {
|
|
211
|
+
toolName: 'navigate',
|
|
212
|
+
page: pageInstance,
|
|
213
|
+
restartBrowser: () => browser.restart()
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
110
219
|
## ๐ Unified MCP+LSP+SSE Ecosystem
|
|
111
220
|
|
|
112
221
|
**เคเค command เคธเฅ เคธเคฌ active:**
|
|
@@ -117,7 +226,7 @@ npm run dev
|
|
|
117
226
|
|
|
118
227
|
### Output:
|
|
119
228
|
```
|
|
120
|
-
๐ฆ Brave Real Browser - Unified Server v2.
|
|
229
|
+
๐ฆ Brave Real Browser - Unified Server v2.28.1
|
|
121
230
|
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
122
231
|
๐ก HTTP Server: http://localhost:3000
|
|
123
232
|
|
|
@@ -185,14 +294,14 @@ brave-real-launcher
|
|
|
185
294
|
### Navigation
|
|
186
295
|
| Tool | Description |
|
|
187
296
|
|------|-------------|
|
|
188
|
-
| `navigate` | Navigate to URL |
|
|
297
|
+
| `navigate` | Navigate to URL with advanced options (resource blocking, custom headers, auto-scroll) |
|
|
189
298
|
| `wait` | Wait for conditions |
|
|
190
299
|
|
|
191
300
|
### Content
|
|
192
301
|
| Tool | Description |
|
|
193
302
|
|------|-------------|
|
|
194
303
|
| `get_content` | Get page HTML/text |
|
|
195
|
-
| `find_element` | Find by selector/text/AI |
|
|
304
|
+
| `find_element` | Find by selector/text/AI with batch mode |
|
|
196
305
|
| `save_content_as_markdown` | Save as markdown file |
|
|
197
306
|
|
|
198
307
|
### Interaction
|
|
@@ -204,34 +313,32 @@ brave-real-launcher
|
|
|
204
313
|
| `random_scroll` | Natural scrolling |
|
|
205
314
|
| `solve_captcha` | Solve CAPTCHAs |
|
|
206
315
|
|
|
207
|
-
### Media
|
|
316
|
+
### Media & Streaming
|
|
317
|
+
| Tool | Description |
|
|
318
|
+
|------|-------------|
|
|
319
|
+
| `stream_extractor` | Extract streams with multi-quality selector, VidSrc/Filemoon/StreamWish support |
|
|
320
|
+
| `player_api_hook` | Hook into JWPlayer, Video.js, HLS.js, Plyr, Vidstack, DASH.js |
|
|
321
|
+
| `iframe_handler` | Deep iframe scraping with video source extraction |
|
|
322
|
+
|
|
323
|
+
### Scraping
|
|
208
324
|
| Tool | Description |
|
|
209
325
|
|------|-------------|
|
|
210
|
-
| `
|
|
211
|
-
| `
|
|
212
|
-
| `
|
|
326
|
+
| `js_scrape` | JavaScript-rendered scraping with parallel URL support |
|
|
327
|
+
| `link_harvester` | Harvest links with auto-pagination |
|
|
328
|
+
| `network_recorder` | Record traffic with API interception |
|
|
329
|
+
| `extract_json` | Extract embedded JSON with AES decryption |
|
|
330
|
+
| `search_regex` | Regex search like regex101.com |
|
|
213
331
|
|
|
214
332
|
### Advanced
|
|
215
333
|
| Tool | Description |
|
|
216
334
|
|------|-------------|
|
|
217
|
-
| `search_content` | Search patterns |
|
|
218
|
-
| `extract_json` | Extract embedded JSON |
|
|
219
335
|
| `scrape_meta_tags` | Meta/OG tags |
|
|
220
|
-
| `deep_analysis` | Full page analysis |
|
|
221
|
-
| `network_recorder` | Record traffic |
|
|
222
|
-
| `api_finder` | Discover APIs |
|
|
223
|
-
| `ajax_content_waiter` | Wait for AJAX |
|
|
224
|
-
| `link_harvester` | Harvest links |
|
|
225
|
-
| `batch_element_scraper` | Batch scrape |
|
|
226
|
-
| `extract_schema` | Schema.org data |
|
|
227
|
-
| `element_screenshot` | Screenshot element |
|
|
228
|
-
| `breadcrumb_navigator` | Navigate breadcrumbs |
|
|
336
|
+
| `deep_analysis` | Full page analysis with screenshot |
|
|
229
337
|
| `redirect_tracer` | Trace redirects |
|
|
230
338
|
| `progress_tracker` | Track progress |
|
|
231
339
|
| `cookie_manager` | Manage cookies |
|
|
232
340
|
| `file_downloader` | Download files |
|
|
233
|
-
| `
|
|
234
|
-
| `popup_handler` | Handle popups |
|
|
341
|
+
| `execute_js` | Run custom JavaScript |
|
|
235
342
|
|
|
236
343
|
---
|
|
237
344
|
|
package/dist/browser-manager.js
CHANGED
|
@@ -36,6 +36,350 @@ function loadEnvFile() {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
loadEnvFile();
|
|
39
|
+
// Global proxy rotation state
|
|
40
|
+
let proxyRotation = {
|
|
41
|
+
enabled: false,
|
|
42
|
+
proxies: [],
|
|
43
|
+
rotationStrategy: 'round-robin',
|
|
44
|
+
rotateAfterRequests: 50,
|
|
45
|
+
rotateOnBlock: true,
|
|
46
|
+
currentIndex: 0,
|
|
47
|
+
requestCount: 0,
|
|
48
|
+
proxyStats: new Map()
|
|
49
|
+
};
|
|
50
|
+
// Initialize proxy rotation
|
|
51
|
+
export function initProxyRotation(proxies, strategy = 'round-robin') {
|
|
52
|
+
const parsedProxies = proxies.map(p => {
|
|
53
|
+
if (typeof p === 'string') {
|
|
54
|
+
// Parse string format: protocol://user:pass@host:port or host:port
|
|
55
|
+
const match = p.match(/^(?:(https?|socks5):\/\/)?(?:([^:]+):([^@]+)@)?([^:]+):(\d+)$/);
|
|
56
|
+
if (match) {
|
|
57
|
+
return {
|
|
58
|
+
protocol: match[1] || 'http',
|
|
59
|
+
username: match[2],
|
|
60
|
+
password: match[3],
|
|
61
|
+
host: match[4],
|
|
62
|
+
port: parseInt(match[5], 10)
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// Simple host:port
|
|
66
|
+
const [host, port] = p.split(':');
|
|
67
|
+
return { host, port: parseInt(port, 10), protocol: 'http' };
|
|
68
|
+
}
|
|
69
|
+
return p;
|
|
70
|
+
});
|
|
71
|
+
proxyRotation = {
|
|
72
|
+
enabled: true,
|
|
73
|
+
proxies: parsedProxies,
|
|
74
|
+
rotationStrategy: strategy,
|
|
75
|
+
rotateAfterRequests: 50,
|
|
76
|
+
rotateOnBlock: true,
|
|
77
|
+
currentIndex: 0,
|
|
78
|
+
requestCount: 0,
|
|
79
|
+
proxyStats: new Map()
|
|
80
|
+
};
|
|
81
|
+
// Initialize stats for each proxy
|
|
82
|
+
parsedProxies.forEach(p => {
|
|
83
|
+
const key = `${p.host}:${p.port}`;
|
|
84
|
+
proxyRotation.proxyStats.set(key, { uses: 0, failures: 0, lastUsed: 0 });
|
|
85
|
+
});
|
|
86
|
+
console.log(`[ProxyRotation] Initialized with ${parsedProxies.length} proxies, strategy: ${strategy}`);
|
|
87
|
+
}
|
|
88
|
+
// Get current proxy
|
|
89
|
+
export function getCurrentProxy() {
|
|
90
|
+
if (!proxyRotation.enabled || proxyRotation.proxies.length === 0) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
return proxyRotation.proxies[proxyRotation.currentIndex];
|
|
94
|
+
}
|
|
95
|
+
// Get proxy as URL string
|
|
96
|
+
export function getCurrentProxyUrl() {
|
|
97
|
+
const proxy = getCurrentProxy();
|
|
98
|
+
if (!proxy)
|
|
99
|
+
return null;
|
|
100
|
+
let url = `${proxy.protocol || 'http'}://`;
|
|
101
|
+
if (proxy.username && proxy.password) {
|
|
102
|
+
url += `${proxy.username}:${proxy.password}@`;
|
|
103
|
+
}
|
|
104
|
+
url += `${proxy.host}:${proxy.port}`;
|
|
105
|
+
return url;
|
|
106
|
+
}
|
|
107
|
+
// Rotate to next proxy
|
|
108
|
+
export function rotateProxy(reason = 'scheduled') {
|
|
109
|
+
if (!proxyRotation.enabled || proxyRotation.proxies.length === 0) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
const prevProxy = getCurrentProxy();
|
|
113
|
+
const prevKey = prevProxy ? `${prevProxy.host}:${prevProxy.port}` : '';
|
|
114
|
+
switch (proxyRotation.rotationStrategy) {
|
|
115
|
+
case 'round-robin':
|
|
116
|
+
proxyRotation.currentIndex = (proxyRotation.currentIndex + 1) % proxyRotation.proxies.length;
|
|
117
|
+
break;
|
|
118
|
+
case 'random':
|
|
119
|
+
proxyRotation.currentIndex = Math.floor(Math.random() * proxyRotation.proxies.length);
|
|
120
|
+
break;
|
|
121
|
+
case 'least-used':
|
|
122
|
+
let minUses = Infinity;
|
|
123
|
+
let minIndex = 0;
|
|
124
|
+
proxyRotation.proxies.forEach((p, i) => {
|
|
125
|
+
const key = `${p.host}:${p.port}`;
|
|
126
|
+
const stats = proxyRotation.proxyStats.get(key);
|
|
127
|
+
if (stats && stats.uses < minUses) {
|
|
128
|
+
minUses = stats.uses;
|
|
129
|
+
minIndex = i;
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
proxyRotation.currentIndex = minIndex;
|
|
133
|
+
break;
|
|
134
|
+
case 'on-error':
|
|
135
|
+
// Only rotate on error/blocked, otherwise keep current
|
|
136
|
+
if (reason === 'error' || reason === 'blocked') {
|
|
137
|
+
proxyRotation.currentIndex = (proxyRotation.currentIndex + 1) % proxyRotation.proxies.length;
|
|
138
|
+
}
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
proxyRotation.requestCount = 0;
|
|
142
|
+
const newProxy = getCurrentProxy();
|
|
143
|
+
const newKey = newProxy ? `${newProxy.host}:${newProxy.port}` : '';
|
|
144
|
+
// Update stats
|
|
145
|
+
if (newKey && proxyRotation.proxyStats.has(newKey)) {
|
|
146
|
+
const stats = proxyRotation.proxyStats.get(newKey);
|
|
147
|
+
stats.uses++;
|
|
148
|
+
stats.lastUsed = Date.now();
|
|
149
|
+
if (reason === 'error' || reason === 'blocked') {
|
|
150
|
+
const prevStats = proxyRotation.proxyStats.get(prevKey);
|
|
151
|
+
if (prevStats)
|
|
152
|
+
prevStats.failures++;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
console.log(`[ProxyRotation] Rotated proxy (${reason}): ${prevKey} -> ${newKey}`);
|
|
156
|
+
return newProxy;
|
|
157
|
+
}
|
|
158
|
+
// Check if proxy should be rotated
|
|
159
|
+
export function shouldRotateProxy() {
|
|
160
|
+
if (!proxyRotation.enabled)
|
|
161
|
+
return false;
|
|
162
|
+
proxyRotation.requestCount++;
|
|
163
|
+
if (proxyRotation.rotateAfterRequests &&
|
|
164
|
+
proxyRotation.requestCount >= proxyRotation.rotateAfterRequests) {
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
// Mark current proxy as blocked/failed
|
|
170
|
+
export function markProxyFailed(blocked = false) {
|
|
171
|
+
if (!proxyRotation.enabled)
|
|
172
|
+
return;
|
|
173
|
+
const proxy = getCurrentProxy();
|
|
174
|
+
if (proxy) {
|
|
175
|
+
const key = `${proxy.host}:${proxy.port}`;
|
|
176
|
+
const stats = proxyRotation.proxyStats.get(key);
|
|
177
|
+
if (stats) {
|
|
178
|
+
stats.failures++;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (proxyRotation.rotateOnBlock && blocked) {
|
|
182
|
+
rotateProxy('blocked');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// Get proxy stats
|
|
186
|
+
export function getProxyStats() {
|
|
187
|
+
return {
|
|
188
|
+
enabled: proxyRotation.enabled,
|
|
189
|
+
currentProxy: getCurrentProxyUrl(),
|
|
190
|
+
totalProxies: proxyRotation.proxies.length,
|
|
191
|
+
currentIndex: proxyRotation.currentIndex,
|
|
192
|
+
requestCount: proxyRotation.requestCount,
|
|
193
|
+
strategy: proxyRotation.rotationStrategy,
|
|
194
|
+
stats: Object.fromEntries(proxyRotation.proxyStats)
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
// Global rate limiter configuration
|
|
198
|
+
let rateLimiterConfig = {
|
|
199
|
+
enabled: false,
|
|
200
|
+
requestsPerSecond: 5,
|
|
201
|
+
requestsPerMinute: 100,
|
|
202
|
+
burstLimit: 10,
|
|
203
|
+
domainLimits: new Map(),
|
|
204
|
+
globalCooldown: 100,
|
|
205
|
+
retryAfter: 5000
|
|
206
|
+
};
|
|
207
|
+
// Global rate limiter state
|
|
208
|
+
let rateLimiterState = {
|
|
209
|
+
lastRequestTime: 0,
|
|
210
|
+
requestsInSecond: 0,
|
|
211
|
+
requestsInMinute: 0,
|
|
212
|
+
secondWindowStart: Date.now(),
|
|
213
|
+
minuteWindowStart: Date.now(),
|
|
214
|
+
domainRequestTimes: new Map(),
|
|
215
|
+
isThrottled: false,
|
|
216
|
+
throttleUntil: 0
|
|
217
|
+
};
|
|
218
|
+
/**
|
|
219
|
+
* Initialize rate limiter with custom configuration
|
|
220
|
+
*/
|
|
221
|
+
export function initRateLimiter(config = {}) {
|
|
222
|
+
rateLimiterConfig = {
|
|
223
|
+
...rateLimiterConfig,
|
|
224
|
+
enabled: true,
|
|
225
|
+
...config
|
|
226
|
+
};
|
|
227
|
+
// Reset state
|
|
228
|
+
rateLimiterState = {
|
|
229
|
+
lastRequestTime: 0,
|
|
230
|
+
requestsInSecond: 0,
|
|
231
|
+
requestsInMinute: 0,
|
|
232
|
+
secondWindowStart: Date.now(),
|
|
233
|
+
minuteWindowStart: Date.now(),
|
|
234
|
+
domainRequestTimes: new Map(),
|
|
235
|
+
isThrottled: false,
|
|
236
|
+
throttleUntil: 0
|
|
237
|
+
};
|
|
238
|
+
console.log(`[RateLimiter] Initialized: ${rateLimiterConfig.requestsPerSecond}/sec, ${rateLimiterConfig.requestsPerMinute}/min`);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Set rate limit for a specific domain
|
|
242
|
+
*/
|
|
243
|
+
export function setDomainRateLimit(domain, requestsPerSecond, requestsPerMinute) {
|
|
244
|
+
rateLimiterConfig.domainLimits.set(domain, {
|
|
245
|
+
requestsPerSecond,
|
|
246
|
+
requestsPerMinute: requestsPerMinute || requestsPerSecond * 60
|
|
247
|
+
});
|
|
248
|
+
console.log(`[RateLimiter] Domain limit set for ${domain}: ${requestsPerSecond}/sec`);
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Get the delay needed before making a request
|
|
252
|
+
* Returns 0 if request can be made immediately
|
|
253
|
+
*/
|
|
254
|
+
export function getRateLimitDelay(url) {
|
|
255
|
+
if (!rateLimiterConfig.enabled)
|
|
256
|
+
return 0;
|
|
257
|
+
const now = Date.now();
|
|
258
|
+
// Check if we're in a throttle period
|
|
259
|
+
if (rateLimiterState.isThrottled && now < rateLimiterState.throttleUntil) {
|
|
260
|
+
return rateLimiterState.throttleUntil - now;
|
|
261
|
+
}
|
|
262
|
+
else if (rateLimiterState.isThrottled) {
|
|
263
|
+
rateLimiterState.isThrottled = false;
|
|
264
|
+
}
|
|
265
|
+
// Update sliding windows
|
|
266
|
+
if (now - rateLimiterState.secondWindowStart >= 1000) {
|
|
267
|
+
rateLimiterState.requestsInSecond = 0;
|
|
268
|
+
rateLimiterState.secondWindowStart = now;
|
|
269
|
+
}
|
|
270
|
+
if (now - rateLimiterState.minuteWindowStart >= 60000) {
|
|
271
|
+
rateLimiterState.requestsInMinute = 0;
|
|
272
|
+
rateLimiterState.minuteWindowStart = now;
|
|
273
|
+
}
|
|
274
|
+
// Check global limits
|
|
275
|
+
if (rateLimiterState.requestsInSecond >= rateLimiterConfig.requestsPerSecond) {
|
|
276
|
+
return 1000 - (now - rateLimiterState.secondWindowStart);
|
|
277
|
+
}
|
|
278
|
+
if (rateLimiterState.requestsInMinute >= rateLimiterConfig.requestsPerMinute) {
|
|
279
|
+
return 60000 - (now - rateLimiterState.minuteWindowStart);
|
|
280
|
+
}
|
|
281
|
+
// Check global cooldown
|
|
282
|
+
const timeSinceLastRequest = now - rateLimiterState.lastRequestTime;
|
|
283
|
+
if (timeSinceLastRequest < rateLimiterConfig.globalCooldown) {
|
|
284
|
+
return rateLimiterConfig.globalCooldown - timeSinceLastRequest;
|
|
285
|
+
}
|
|
286
|
+
// Check domain-specific limits
|
|
287
|
+
if (url) {
|
|
288
|
+
try {
|
|
289
|
+
const domain = new URL(url).hostname;
|
|
290
|
+
const domainLimit = rateLimiterConfig.domainLimits.get(domain);
|
|
291
|
+
if (domainLimit) {
|
|
292
|
+
const domainTimes = rateLimiterState.domainRequestTimes.get(domain) || [];
|
|
293
|
+
// Clean up old timestamps
|
|
294
|
+
const recentTimes = domainTimes.filter(t => now - t < 60000);
|
|
295
|
+
rateLimiterState.domainRequestTimes.set(domain, recentTimes);
|
|
296
|
+
// Check domain limits
|
|
297
|
+
const lastSecondRequests = recentTimes.filter(t => now - t < 1000).length;
|
|
298
|
+
if (lastSecondRequests >= domainLimit.requestsPerSecond) {
|
|
299
|
+
const oldestInSecond = recentTimes.filter(t => now - t < 1000)[0];
|
|
300
|
+
return oldestInSecond ? 1000 - (now - oldestInSecond) : 1000;
|
|
301
|
+
}
|
|
302
|
+
const lastMinuteRequests = recentTimes.length;
|
|
303
|
+
if (lastMinuteRequests >= domainLimit.requestsPerMinute) {
|
|
304
|
+
const oldestInMinute = recentTimes[0];
|
|
305
|
+
return oldestInMinute ? 60000 - (now - oldestInMinute) : 60000;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
// Invalid URL, ignore domain limits
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return 0;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Wait for rate limit if needed, then record the request
|
|
317
|
+
*/
|
|
318
|
+
export async function waitForRateLimit(url) {
|
|
319
|
+
const delay = getRateLimitDelay(url);
|
|
320
|
+
if (delay > 0) {
|
|
321
|
+
console.log(`[RateLimiter] Waiting ${delay}ms before request${url ? ` to ${new URL(url).hostname}` : ''}`);
|
|
322
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
323
|
+
}
|
|
324
|
+
// Record the request
|
|
325
|
+
recordRequest(url);
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Record a request for rate limiting tracking
|
|
329
|
+
*/
|
|
330
|
+
export function recordRequest(url) {
|
|
331
|
+
if (!rateLimiterConfig.enabled)
|
|
332
|
+
return;
|
|
333
|
+
const now = Date.now();
|
|
334
|
+
rateLimiterState.lastRequestTime = now;
|
|
335
|
+
rateLimiterState.requestsInSecond++;
|
|
336
|
+
rateLimiterState.requestsInMinute++;
|
|
337
|
+
// Record domain-specific request
|
|
338
|
+
if (url) {
|
|
339
|
+
try {
|
|
340
|
+
const domain = new URL(url).hostname;
|
|
341
|
+
const domainTimes = rateLimiterState.domainRequestTimes.get(domain) || [];
|
|
342
|
+
domainTimes.push(now);
|
|
343
|
+
rateLimiterState.domainRequestTimes.set(domain, domainTimes);
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
// Invalid URL
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Signal that a rate limit response (429) was received
|
|
352
|
+
*/
|
|
353
|
+
export function onRateLimitHit(retryAfterMs) {
|
|
354
|
+
const delay = retryAfterMs || rateLimiterConfig.retryAfter;
|
|
355
|
+
rateLimiterState.isThrottled = true;
|
|
356
|
+
rateLimiterState.throttleUntil = Date.now() + delay;
|
|
357
|
+
console.log(`[RateLimiter] Rate limit hit! Throttling for ${delay}ms`);
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Get current rate limiter stats
|
|
361
|
+
*/
|
|
362
|
+
export function getRateLimiterStats() {
|
|
363
|
+
const now = Date.now();
|
|
364
|
+
return {
|
|
365
|
+
enabled: rateLimiterConfig.enabled,
|
|
366
|
+
requestsInSecond: rateLimiterState.requestsInSecond,
|
|
367
|
+
requestsInMinute: rateLimiterState.requestsInMinute,
|
|
368
|
+
limitsPerSecond: rateLimiterConfig.requestsPerSecond,
|
|
369
|
+
limitsPerMinute: rateLimiterConfig.requestsPerMinute,
|
|
370
|
+
isThrottled: rateLimiterState.isThrottled,
|
|
371
|
+
throttleRemaining: rateLimiterState.isThrottled ? Math.max(0, rateLimiterState.throttleUntil - now) : 0,
|
|
372
|
+
domainLimits: Object.fromEntries(rateLimiterConfig.domainLimits),
|
|
373
|
+
cooldown: rateLimiterConfig.globalCooldown
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Disable rate limiter
|
|
378
|
+
*/
|
|
379
|
+
export function disableRateLimiter() {
|
|
380
|
+
rateLimiterConfig.enabled = false;
|
|
381
|
+
console.log('[RateLimiter] Disabled');
|
|
382
|
+
}
|
|
39
383
|
// Browser error categorization
|
|
40
384
|
export var BrowserErrorType;
|
|
41
385
|
(function (BrowserErrorType) {
|