blackveil-dns 2.2.2 → 2.3.5
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 +27 -19
- package/dist/index.d.ts +32 -1
- package/dist/index.js +225 -119
- package/dist/index.js.map +1 -1
- package/dist/stdio.js +389 -98
- package/dist/stdio.js.map +1 -1
- package/package.json +1 -1
package/dist/stdio.js
CHANGED
|
@@ -20,35 +20,39 @@ function isSensitiveKey(key) {
|
|
|
20
20
|
function isPlainObject(value) {
|
|
21
21
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
22
22
|
}
|
|
23
|
-
function sanitizeString(value) {
|
|
23
|
+
function sanitizeString(value, maxLength = MAX_LOG_STRING_LENGTH) {
|
|
24
24
|
const stripped = value.replace(/[\x00-\x08\x0a-\x1f\x7f]/g, " ");
|
|
25
|
-
|
|
25
|
+
if (stripped.length <= maxLength) return stripped;
|
|
26
|
+
const half = Math.floor((maxLength - 5) / 2);
|
|
27
|
+
return `${stripped.slice(0, half)} ... ${stripped.slice(-half)}`;
|
|
26
28
|
}
|
|
27
|
-
function sanitizeLogValue(value, key) {
|
|
29
|
+
function sanitizeLogValue(value, key, maxLength) {
|
|
28
30
|
if (key && isSensitiveKey(key)) {
|
|
29
31
|
return REDACTED;
|
|
30
32
|
}
|
|
31
33
|
if (typeof value === "string") {
|
|
32
|
-
return sanitizeString(value);
|
|
34
|
+
return sanitizeString(value, maxLength);
|
|
33
35
|
}
|
|
34
36
|
if (Array.isArray(value)) {
|
|
35
|
-
return value.map((item) => sanitizeLogValue(item));
|
|
37
|
+
return value.map((item) => sanitizeLogValue(item, void 0, maxLength));
|
|
36
38
|
}
|
|
37
39
|
if (isPlainObject(value)) {
|
|
38
40
|
const sanitized = {};
|
|
39
41
|
for (const [entryKey, entryValue] of Object.entries(value)) {
|
|
40
|
-
sanitized[entryKey] = sanitizeLogValue(entryValue, entryKey);
|
|
42
|
+
sanitized[entryKey] = sanitizeLogValue(entryValue, entryKey, maxLength);
|
|
41
43
|
}
|
|
42
44
|
return sanitized;
|
|
43
45
|
}
|
|
44
46
|
return value;
|
|
45
47
|
}
|
|
46
48
|
function logEvent(event) {
|
|
49
|
+
const isError = event.severity === "error";
|
|
50
|
+
const maxLen = isError ? MAX_ERROR_STRING_LENGTH : MAX_LOG_STRING_LENGTH;
|
|
47
51
|
const log = {
|
|
48
52
|
...event,
|
|
49
53
|
timestamp: event.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
|
|
50
|
-
details: sanitizeLogValue(event.details),
|
|
51
|
-
error: typeof event.error === "string" ? sanitizeString(event.error) : event.error
|
|
54
|
+
details: sanitizeLogValue(event.details, void 0, maxLen),
|
|
55
|
+
error: typeof event.error === "string" ? sanitizeString(event.error, maxLen) : event.error
|
|
52
56
|
};
|
|
53
57
|
console.log(JSON.stringify(log));
|
|
54
58
|
}
|
|
@@ -60,17 +64,18 @@ function logError(error2, context) {
|
|
|
60
64
|
...context
|
|
61
65
|
});
|
|
62
66
|
}
|
|
63
|
-
var REDACTED, MAX_LOG_STRING_LENGTH, SENSITIVE_KEY_PATTERN;
|
|
67
|
+
var REDACTED, MAX_LOG_STRING_LENGTH, MAX_ERROR_STRING_LENGTH, SENSITIVE_KEY_PATTERN;
|
|
64
68
|
var init_log = __esm({
|
|
65
69
|
"src/lib/log.ts"() {
|
|
66
70
|
REDACTED = "[redacted]";
|
|
67
71
|
MAX_LOG_STRING_LENGTH = 256;
|
|
72
|
+
MAX_ERROR_STRING_LENGTH = 1024;
|
|
68
73
|
SENSITIVE_KEY_PATTERN = /(^ip$|authorization|mcp-session-id|session|token|api[-_]?key|secret|password|cookie|rawbody)/i;
|
|
69
74
|
}
|
|
70
75
|
});
|
|
71
76
|
|
|
72
77
|
// src/lib/config.ts
|
|
73
|
-
var BLOCKED_SUFFIXES, BLOCKED_HOSTS, BLOCKED_IP_PATTERNS, BLOCKED_DNS_REBINDING, MAX_DOMAIN_LENGTH, MAX_LABEL_LENGTH, LABEL_REGEX, HTTPS_TIMEOUT_MS, DNS_TIMEOUT_MS, DNS_RETRIES, DOH_EDGE_CACHE_TTL, INFLIGHT_CLEANUP_MS, DNS_RETRY_BASE_DELAY_MS, DNS_CONFIRM_WITH_SECONDARY_ON_EMPTY, GLOBAL_DAILY_TOOL_LIMIT, TIER_DAILY_LIMITS, TIER_TOOL_DAILY_LIMITS, FREE_TOOL_DAILY_LIMITS;
|
|
78
|
+
var BLOCKED_SUFFIXES, BLOCKED_HOSTS, BLOCKED_IP_PATTERNS, BLOCKED_DNS_REBINDING, MAX_DOMAIN_LENGTH, MAX_LABEL_LENGTH, LABEL_REGEX, HTTPS_TIMEOUT_MS, DNS_TIMEOUT_MS, DNS_RETRIES, DOH_EDGE_CACHE_TTL, INFLIGHT_CLEANUP_MS, DNS_RETRY_BASE_DELAY_MS, DNS_CONFIRM_WITH_SECONDARY_ON_EMPTY, GLOBAL_DAILY_TOOL_LIMIT, TIER_DAILY_LIMITS, TIER_TOOL_DAILY_LIMITS, FREE_TOOL_DAILY_LIMITS, TIER_CONCURRENT_LIMITS;
|
|
74
79
|
var init_config = __esm({
|
|
75
80
|
"src/lib/config.ts"() {
|
|
76
81
|
BLOCKED_SUFFIXES = [
|
|
@@ -192,6 +197,14 @@ var init_config = __esm({
|
|
|
192
197
|
map_compliance: 75,
|
|
193
198
|
simulate_attack_paths: 75
|
|
194
199
|
};
|
|
200
|
+
TIER_CONCURRENT_LIMITS = {
|
|
201
|
+
free: 3,
|
|
202
|
+
agent: 5,
|
|
203
|
+
developer: 10,
|
|
204
|
+
enterprise: 25,
|
|
205
|
+
partner: 50,
|
|
206
|
+
owner: Infinity
|
|
207
|
+
};
|
|
195
208
|
}
|
|
196
209
|
});
|
|
197
210
|
|
|
@@ -238,10 +251,37 @@ async function runWithCache(key, run, kv, ttlSeconds, skipCache) {
|
|
|
238
251
|
}
|
|
239
252
|
const existing = INFLIGHT.get(key);
|
|
240
253
|
if (existing) return existing;
|
|
254
|
+
if (kv && !skipCache) {
|
|
255
|
+
const sentinelKey = `${key}:computing`;
|
|
256
|
+
try {
|
|
257
|
+
const sentinel = await kv.get(sentinelKey);
|
|
258
|
+
if (sentinel) {
|
|
259
|
+
const polled = await pollForResult(key, kv);
|
|
260
|
+
if (polled !== void 0) return polled;
|
|
261
|
+
} else {
|
|
262
|
+
await kv.put(sentinelKey, String(Date.now()), { expirationTtl: SENTINEL_TTL_SECONDS });
|
|
263
|
+
}
|
|
264
|
+
} catch {
|
|
265
|
+
}
|
|
266
|
+
}
|
|
241
267
|
const cleanup = setTimeout(() => INFLIGHT.delete(key), INFLIGHT_CLEANUP_MS);
|
|
242
268
|
const promise = run().then(async (result) => {
|
|
243
269
|
await cacheSet(key, result, kv, ttlSeconds);
|
|
270
|
+
if (kv) {
|
|
271
|
+
try {
|
|
272
|
+
await kv.delete(`${key}:computing`);
|
|
273
|
+
} catch {
|
|
274
|
+
}
|
|
275
|
+
}
|
|
244
276
|
return result;
|
|
277
|
+
}).catch(async (err) => {
|
|
278
|
+
if (kv) {
|
|
279
|
+
try {
|
|
280
|
+
await kv.delete(`${key}:computing`);
|
|
281
|
+
} catch {
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
throw err;
|
|
245
285
|
}).finally(() => {
|
|
246
286
|
clearTimeout(cleanup);
|
|
247
287
|
INFLIGHT.delete(key);
|
|
@@ -249,7 +289,19 @@ async function runWithCache(key, run, kv, ttlSeconds, skipCache) {
|
|
|
249
289
|
INFLIGHT.set(key, promise);
|
|
250
290
|
return promise;
|
|
251
291
|
}
|
|
252
|
-
|
|
292
|
+
async function pollForResult(key, kv) {
|
|
293
|
+
for (const delay of POLL_DELAYS_MS) {
|
|
294
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
295
|
+
try {
|
|
296
|
+
const val = await kv.get(key, "json");
|
|
297
|
+
if (val !== null) return val;
|
|
298
|
+
} catch {
|
|
299
|
+
return void 0;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return void 0;
|
|
303
|
+
}
|
|
304
|
+
var DEFAULT_TTL_MS, DEFAULT_TTL_SECONDS, DEFAULT_MAX_ENTRIES, TTLCache, INFLIGHT, IN_MEMORY_CACHE, SENTINEL_TTL_SECONDS, POLL_DELAYS_MS;
|
|
253
305
|
var init_cache = __esm({
|
|
254
306
|
"src/lib/cache.ts"() {
|
|
255
307
|
init_config();
|
|
@@ -341,6 +393,8 @@ var init_cache = __esm({
|
|
|
341
393
|
ttlMs: DEFAULT_TTL_MS,
|
|
342
394
|
maxEntries: DEFAULT_MAX_ENTRIES
|
|
343
395
|
});
|
|
396
|
+
SENTINEL_TTL_SECONDS = 10;
|
|
397
|
+
POLL_DELAYS_MS = [250, 500, 750];
|
|
344
398
|
}
|
|
345
399
|
});
|
|
346
400
|
|
|
@@ -426,18 +480,25 @@ async function fetchDohResponse(url, timeoutMs, opts) {
|
|
|
426
480
|
try {
|
|
427
481
|
const headers = { Accept: "application/dns-json" };
|
|
428
482
|
if (opts?.token) headers["X-BV-Token"] = opts.token;
|
|
429
|
-
const
|
|
483
|
+
const doFetch = () => fetch(url, {
|
|
430
484
|
method: "GET",
|
|
431
485
|
headers,
|
|
432
486
|
signal: AbortSignal.timeout(timeoutMs),
|
|
433
487
|
...opts?.useEdgeCache ? { cf: { cacheTtl: DOH_EDGE_CACHE_TTL, cacheEverything: true } } : {}
|
|
434
488
|
});
|
|
489
|
+
const response = opts?.semaphore ? await opts.semaphore.run(doFetch) : await doFetch();
|
|
435
490
|
if (!response.ok) return null;
|
|
436
491
|
const data = await response.json();
|
|
437
492
|
const parsed = DohResponseSchema.safeParse(data);
|
|
438
493
|
if (!parsed.success) return null;
|
|
439
494
|
return parsed.data;
|
|
440
|
-
} catch {
|
|
495
|
+
} catch (err) {
|
|
496
|
+
const isTimeout = err instanceof DOMException && err.name === "TimeoutError";
|
|
497
|
+
logError(isTimeout ? "DNS fetch timeout" : "DNS fetch failed", {
|
|
498
|
+
severity: "warn",
|
|
499
|
+
category: "dns-transport",
|
|
500
|
+
details: { url: url.replace(/name=[^&]+/, "name=<domain>"), errorType: isTimeout ? "timeout" : "network" }
|
|
501
|
+
});
|
|
441
502
|
return null;
|
|
442
503
|
}
|
|
443
504
|
}
|
|
@@ -460,11 +521,13 @@ async function queryDnsUncached(domain, type, dnssecCheck = false, opts) {
|
|
|
460
521
|
const timeoutMs = opts?.timeoutMs ?? DNS_TIMEOUT_MS;
|
|
461
522
|
const retries = opts?.retries ?? DNS_RETRIES;
|
|
462
523
|
const confirmWithSecondaryOnEmpty = opts?.confirmWithSecondaryOnEmpty ?? DNS_CONFIRM_WITH_SECONDARY_ON_EMPTY;
|
|
524
|
+
const sem = opts?.dnsSemaphore;
|
|
463
525
|
const url = buildDohUrl(DOH_ENDPOINT, domain, type, dnssecCheck);
|
|
526
|
+
const guardedFetch = (input, init) => sem ? sem.run(() => fetch(input, init)) : fetch(input, init);
|
|
464
527
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
465
528
|
let response;
|
|
466
529
|
try {
|
|
467
|
-
response = await
|
|
530
|
+
response = await guardedFetch(url, {
|
|
468
531
|
method: "GET",
|
|
469
532
|
headers: { Accept: "application/dns-json" },
|
|
470
533
|
signal: AbortSignal.timeout(timeoutMs),
|
|
@@ -498,35 +561,51 @@ async function queryDnsUncached(domain, type, dnssecCheck = false, opts) {
|
|
|
498
561
|
}
|
|
499
562
|
const data = validated.data;
|
|
500
563
|
if (confirmWithSecondaryOnEmpty && !opts?.skipSecondaryConfirmation && !hasTypedAnswers(data, type)) {
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
buildDohUrl(opts.secondaryDoh.endpoint, domain, type, dnssecCheck),
|
|
504
|
-
timeoutMs,
|
|
505
|
-
{ token: opts.secondaryDoh.token }
|
|
506
|
-
);
|
|
507
|
-
if (bvDns && hasTypedAnswers(bvDns, type)) {
|
|
508
|
-
return bvDns;
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
const google = await fetchDohResponse(
|
|
512
|
-
buildDohUrl(GOOGLE_DOH_ENDPOINT, domain, type, dnssecCheck),
|
|
513
|
-
timeoutMs,
|
|
514
|
-
{ useEdgeCache: true }
|
|
515
|
-
);
|
|
516
|
-
if (google && hasTypedAnswers(google, type)) {
|
|
517
|
-
return google;
|
|
518
|
-
}
|
|
564
|
+
const secondaryResult = await confirmWithSecondaryResolvers(domain, type, dnssecCheck, timeoutMs, sem, opts);
|
|
565
|
+
if (secondaryResult) return secondaryResult;
|
|
519
566
|
}
|
|
520
567
|
return data;
|
|
521
568
|
}
|
|
522
569
|
throw new DnsQueryError("DNS query failed after retries", domain, type);
|
|
523
570
|
}
|
|
571
|
+
async function confirmWithSecondaryResolvers(domain, type, dnssecCheck, timeoutMs, sem, opts) {
|
|
572
|
+
const hasBvDns = !!opts?.secondaryDoh?.endpoint;
|
|
573
|
+
const googleUrl = buildDohUrl(GOOGLE_DOH_ENDPOINT, domain, type, dnssecCheck);
|
|
574
|
+
if (hasBvDns) {
|
|
575
|
+
const bvDnsUrl = buildDohUrl(opts.secondaryDoh.endpoint, domain, type, dnssecCheck);
|
|
576
|
+
const candidates = [
|
|
577
|
+
fetchDohResponse(bvDnsUrl, timeoutMs, { token: opts.secondaryDoh.token, semaphore: sem }),
|
|
578
|
+
fetchDohResponse(googleUrl, timeoutMs, { useEdgeCache: true, semaphore: sem })
|
|
579
|
+
];
|
|
580
|
+
const results = await Promise.allSettled(candidates);
|
|
581
|
+
for (const r of results) {
|
|
582
|
+
if (r.status === "fulfilled" && r.value && hasTypedAnswers(r.value, type)) {
|
|
583
|
+
return r.value;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
const allFailed = results.every((r) => r.status === "rejected" || !r.value);
|
|
587
|
+
if (allFailed) {
|
|
588
|
+
logError("All secondary DNS resolvers failed", {
|
|
589
|
+
severity: "warn",
|
|
590
|
+
category: "dns-transport",
|
|
591
|
+
details: { domain, type }
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
const google = await fetchDohResponse(googleUrl, timeoutMs, { useEdgeCache: true, semaphore: sem });
|
|
597
|
+
if (google && hasTypedAnswers(google, type)) {
|
|
598
|
+
return google;
|
|
599
|
+
}
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
524
602
|
var DOH_ENDPOINT, GOOGLE_DOH_ENDPOINT, DnsQueryError;
|
|
525
603
|
var init_dns_transport = __esm({
|
|
526
604
|
"src/lib/dns-transport.ts"() {
|
|
527
605
|
init_config();
|
|
528
606
|
init_dns_types();
|
|
529
607
|
init_dns();
|
|
608
|
+
init_log();
|
|
530
609
|
DOH_ENDPOINT = "https://cloudflare-dns.com/dns-query";
|
|
531
610
|
GOOGLE_DOH_ENDPOINT = "https://dns.google/resolve";
|
|
532
611
|
DnsQueryError = class extends Error {
|
|
@@ -911,7 +990,16 @@ async function checkMx(domain, options, dnsOptions) {
|
|
|
911
990
|
);
|
|
912
991
|
}
|
|
913
992
|
}
|
|
914
|
-
} catch {
|
|
993
|
+
} catch (err) {
|
|
994
|
+
logEvent({
|
|
995
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
996
|
+
severity: "warn",
|
|
997
|
+
category: "provider-detection",
|
|
998
|
+
domain,
|
|
999
|
+
tool: "check_mx",
|
|
1000
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1001
|
+
details: { phase: "provider_detection" }
|
|
1002
|
+
});
|
|
915
1003
|
}
|
|
916
1004
|
return { ...baseResult, findings };
|
|
917
1005
|
}
|
|
@@ -919,6 +1007,7 @@ var init_check_mx = __esm({
|
|
|
919
1007
|
"src/tools/check-mx.ts"() {
|
|
920
1008
|
init_dns2();
|
|
921
1009
|
init_provider_signatures();
|
|
1010
|
+
init_log();
|
|
922
1011
|
}
|
|
923
1012
|
});
|
|
924
1013
|
|
|
@@ -1190,6 +1279,76 @@ async function checkSessionCreateRateLimitWithCoordinator(ip, limit, windowMs, n
|
|
|
1190
1279
|
});
|
|
1191
1280
|
}
|
|
1192
1281
|
|
|
1282
|
+
// src/lib/circuit-breaker.ts
|
|
1283
|
+
var CircuitBreakerOpen = class extends Error {
|
|
1284
|
+
constructor(name) {
|
|
1285
|
+
super(`Circuit breaker '${name}' is OPEN \u2014 call rejected`);
|
|
1286
|
+
this.name = "CircuitBreakerOpen";
|
|
1287
|
+
}
|
|
1288
|
+
};
|
|
1289
|
+
var CircuitBreaker = class {
|
|
1290
|
+
_state = "CLOSED";
|
|
1291
|
+
_failureCount = 0;
|
|
1292
|
+
_lastFailureAt = 0;
|
|
1293
|
+
config;
|
|
1294
|
+
constructor(config) {
|
|
1295
|
+
this.config = config;
|
|
1296
|
+
}
|
|
1297
|
+
/** Current state (evaluates HALF_OPEN transition lazily). */
|
|
1298
|
+
get state() {
|
|
1299
|
+
if (this._state === "OPEN" && Date.now() - this._lastFailureAt >= this.config.cooldownMs) {
|
|
1300
|
+
this._state = "HALF_OPEN";
|
|
1301
|
+
}
|
|
1302
|
+
return this._state;
|
|
1303
|
+
}
|
|
1304
|
+
get failureCount() {
|
|
1305
|
+
return this._failureCount;
|
|
1306
|
+
}
|
|
1307
|
+
/** Execute a function through the circuit breaker. Throws CircuitBreakerOpen if OPEN. */
|
|
1308
|
+
async call(fn) {
|
|
1309
|
+
const currentState = this.state;
|
|
1310
|
+
if (currentState === "OPEN") {
|
|
1311
|
+
throw new CircuitBreakerOpen(this.config.name);
|
|
1312
|
+
}
|
|
1313
|
+
try {
|
|
1314
|
+
const result = await fn();
|
|
1315
|
+
this.onSuccess();
|
|
1316
|
+
return result;
|
|
1317
|
+
} catch (err) {
|
|
1318
|
+
this.onFailure();
|
|
1319
|
+
throw err;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
/** Execute with a fallback value returned when the circuit is OPEN. */
|
|
1323
|
+
async callWithFallback(fn, fallback) {
|
|
1324
|
+
try {
|
|
1325
|
+
return await this.call(fn);
|
|
1326
|
+
} catch (err) {
|
|
1327
|
+
if (err instanceof CircuitBreakerOpen) {
|
|
1328
|
+
return fallback;
|
|
1329
|
+
}
|
|
1330
|
+
throw err;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
/** Reset to CLOSED state. */
|
|
1334
|
+
reset() {
|
|
1335
|
+
this._state = "CLOSED";
|
|
1336
|
+
this._failureCount = 0;
|
|
1337
|
+
this._lastFailureAt = 0;
|
|
1338
|
+
}
|
|
1339
|
+
onSuccess() {
|
|
1340
|
+
this._failureCount = 0;
|
|
1341
|
+
this._state = "CLOSED";
|
|
1342
|
+
}
|
|
1343
|
+
onFailure() {
|
|
1344
|
+
this._failureCount++;
|
|
1345
|
+
this._lastFailureAt = Date.now();
|
|
1346
|
+
if (this._failureCount >= this.config.failureThreshold) {
|
|
1347
|
+
this._state = "OPEN";
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
};
|
|
1351
|
+
|
|
1193
1352
|
// src/lib/rate-limiter.ts
|
|
1194
1353
|
init_log();
|
|
1195
1354
|
var MINUTE_LIMIT = 50;
|
|
@@ -1200,6 +1359,11 @@ var MINUTE_MS2 = 6e4;
|
|
|
1200
1359
|
var HOUR_MS2 = 36e5;
|
|
1201
1360
|
var DAY_MS2 = 864e5;
|
|
1202
1361
|
var KV_IP_LOCK_TAILS = /* @__PURE__ */ new Map();
|
|
1362
|
+
var quotaCoordinatorBreaker = new CircuitBreaker({
|
|
1363
|
+
name: "QuotaCoordinator",
|
|
1364
|
+
failureThreshold: 3,
|
|
1365
|
+
cooldownMs: 6e4
|
|
1366
|
+
});
|
|
1203
1367
|
function checkRateLimitInMemory(ip) {
|
|
1204
1368
|
return checkScopedRateLimitInMemory(ip, "tools", MINUTE_LIMIT, HOUR_LIMIT);
|
|
1205
1369
|
}
|
|
@@ -1339,10 +1503,14 @@ async function checkGlobalDailyLimitKV(limit, kv) {
|
|
|
1339
1503
|
async function checkRateLimit(ip, kv, quotaCoordinator) {
|
|
1340
1504
|
if (quotaCoordinator) {
|
|
1341
1505
|
try {
|
|
1342
|
-
const coordinated = await
|
|
1506
|
+
const coordinated = await quotaCoordinatorBreaker.call(
|
|
1507
|
+
() => checkScopedRateLimitWithCoordinator(ip, "tools", MINUTE_LIMIT, HOUR_LIMIT, quotaCoordinator)
|
|
1508
|
+
);
|
|
1343
1509
|
if (coordinated) return coordinated;
|
|
1344
|
-
} catch {
|
|
1345
|
-
|
|
1510
|
+
} catch (err) {
|
|
1511
|
+
if (!(err instanceof CircuitBreakerOpen)) {
|
|
1512
|
+
logError("[rate-limiter] quota coordinator error, falling back to KV/in-memory");
|
|
1513
|
+
}
|
|
1346
1514
|
}
|
|
1347
1515
|
}
|
|
1348
1516
|
if (kv) {
|
|
@@ -1357,10 +1525,14 @@ async function checkRateLimit(ip, kv, quotaCoordinator) {
|
|
|
1357
1525
|
async function checkControlPlaneRateLimit(ip, kv, quotaCoordinator) {
|
|
1358
1526
|
if (quotaCoordinator) {
|
|
1359
1527
|
try {
|
|
1360
|
-
const coordinated = await
|
|
1528
|
+
const coordinated = await quotaCoordinatorBreaker.call(
|
|
1529
|
+
() => checkScopedRateLimitWithCoordinator(ip, "control", CONTROL_PLANE_MINUTE_LIMIT, CONTROL_PLANE_HOUR_LIMIT, quotaCoordinator)
|
|
1530
|
+
);
|
|
1361
1531
|
if (coordinated) return coordinated;
|
|
1362
|
-
} catch {
|
|
1363
|
-
|
|
1532
|
+
} catch (err) {
|
|
1533
|
+
if (!(err instanceof CircuitBreakerOpen)) {
|
|
1534
|
+
logError("[rate-limiter] quota coordinator control-plane error, falling back to KV/in-memory");
|
|
1535
|
+
}
|
|
1364
1536
|
}
|
|
1365
1537
|
}
|
|
1366
1538
|
if (kv) {
|
|
@@ -1375,10 +1547,14 @@ async function checkControlPlaneRateLimit(ip, kv, quotaCoordinator) {
|
|
|
1375
1547
|
async function checkToolDailyRateLimit(principalId, toolName, limit, kv, quotaCoordinator) {
|
|
1376
1548
|
if (quotaCoordinator) {
|
|
1377
1549
|
try {
|
|
1378
|
-
const coordinated = await
|
|
1550
|
+
const coordinated = await quotaCoordinatorBreaker.call(
|
|
1551
|
+
() => checkToolDailyRateLimitWithCoordinator(principalId, toolName, limit, quotaCoordinator)
|
|
1552
|
+
);
|
|
1379
1553
|
if (coordinated) return coordinated;
|
|
1380
|
-
} catch {
|
|
1381
|
-
|
|
1554
|
+
} catch (err) {
|
|
1555
|
+
if (!(err instanceof CircuitBreakerOpen)) {
|
|
1556
|
+
logError("[rate-limiter] quota coordinator tool quota error, falling back to KV/in-memory");
|
|
1557
|
+
}
|
|
1382
1558
|
}
|
|
1383
1559
|
}
|
|
1384
1560
|
if (kv) {
|
|
@@ -1393,10 +1569,14 @@ async function checkToolDailyRateLimit(principalId, toolName, limit, kv, quotaCo
|
|
|
1393
1569
|
async function checkGlobalDailyLimit(limit, kv, quotaCoordinator) {
|
|
1394
1570
|
if (quotaCoordinator) {
|
|
1395
1571
|
try {
|
|
1396
|
-
const coordinated = await
|
|
1572
|
+
const coordinated = await quotaCoordinatorBreaker.call(
|
|
1573
|
+
() => checkGlobalDailyLimitWithCoordinator(limit, quotaCoordinator)
|
|
1574
|
+
);
|
|
1397
1575
|
if (coordinated) return coordinated;
|
|
1398
|
-
} catch {
|
|
1399
|
-
|
|
1576
|
+
} catch (err) {
|
|
1577
|
+
if (!(err instanceof CircuitBreakerOpen)) {
|
|
1578
|
+
logError("[rate-limiter] quota coordinator global cap error, falling back to KV/in-memory");
|
|
1579
|
+
}
|
|
1400
1580
|
}
|
|
1401
1581
|
}
|
|
1402
1582
|
if (kv) {
|
|
@@ -1408,6 +1588,23 @@ async function checkGlobalDailyLimit(limit, kv, quotaCoordinator) {
|
|
|
1408
1588
|
}
|
|
1409
1589
|
return checkGlobalDailyLimitInMemory(limit);
|
|
1410
1590
|
}
|
|
1591
|
+
var activeConcurrency = /* @__PURE__ */ new Map();
|
|
1592
|
+
function acquireConcurrencySlot(principalId, limit) {
|
|
1593
|
+
const current = activeConcurrency.get(principalId) ?? 0;
|
|
1594
|
+
if (current >= limit) {
|
|
1595
|
+
return { allowed: false, retryAfterMs: 1e3, active: current, limit };
|
|
1596
|
+
}
|
|
1597
|
+
activeConcurrency.set(principalId, current + 1);
|
|
1598
|
+
return { allowed: true, active: current + 1, limit };
|
|
1599
|
+
}
|
|
1600
|
+
function releaseConcurrencySlot(principalId) {
|
|
1601
|
+
const current = activeConcurrency.get(principalId) ?? 0;
|
|
1602
|
+
if (current <= 1) {
|
|
1603
|
+
activeConcurrency.delete(principalId);
|
|
1604
|
+
} else {
|
|
1605
|
+
activeConcurrency.set(principalId, current - 1);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1411
1608
|
|
|
1412
1609
|
// src/mcp/execute.ts
|
|
1413
1610
|
init_log();
|
|
@@ -1775,7 +1972,7 @@ async function validateSessionRequest(sessionId, sessionStore, id, message) {
|
|
|
1775
1972
|
if (!await validateSession(sessionId, sessionStore)) {
|
|
1776
1973
|
return {
|
|
1777
1974
|
status: 404,
|
|
1778
|
-
payload: jsonRpcError(id, JSON_RPC_ERRORS.INVALID_REQUEST, "Not Found: session expired or terminated")
|
|
1975
|
+
payload: jsonRpcError(id, JSON_RPC_ERRORS.INVALID_REQUEST, "Not Found: session expired or terminated. Send a new initialize request to start a fresh session.")
|
|
1779
1976
|
};
|
|
1780
1977
|
}
|
|
1781
1978
|
return void 0;
|
|
@@ -2502,51 +2699,66 @@ function makeQueryDNS4(dnsOptions) {
|
|
|
2502
2699
|
};
|
|
2503
2700
|
}
|
|
2504
2701
|
async function checkDnssec2(domain, dnsOptions) {
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2702
|
+
try {
|
|
2703
|
+
const baseResult = await checkDNSSEC(
|
|
2704
|
+
domain,
|
|
2705
|
+
makeQueryDNS4(dnsOptions),
|
|
2706
|
+
{
|
|
2707
|
+
timeout: dnsOptions?.timeoutMs ?? 5e3,
|
|
2708
|
+
rawQueryDNS: async (d, type, dnssecFlag) => {
|
|
2709
|
+
const resp = await queryDns(d, type, dnssecFlag ?? false, dnsOptions);
|
|
2710
|
+
return { AD: resp.AD, Answer: resp.Answer };
|
|
2711
|
+
}
|
|
2513
2712
|
}
|
|
2713
|
+
);
|
|
2714
|
+
const dnssecAbsent = baseResult.findings.some((f) => f.title === "DNSSEC not enabled") || baseResult.findings.some((f) => f.title === "DNSSEC check failed") || baseResult.findings.some((f) => f.title === "DNSSEC chain of trust incomplete") || baseResult.findings.some((f) => f.title === "DNSSEC validation failing");
|
|
2715
|
+
if (dnssecAbsent) {
|
|
2716
|
+
return baseResult;
|
|
2514
2717
|
}
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2718
|
+
const [dnskeyResult, dsResult] = await Promise.allSettled([
|
|
2719
|
+
queryDnsRecords(domain, "DNSKEY", dnsOptions),
|
|
2720
|
+
queryDnsRecords(domain, "DS", dnsOptions)
|
|
2721
|
+
]);
|
|
2722
|
+
const hasDnskey = dnskeyResult.status === "fulfilled" && dnskeyResult.value.length > 0;
|
|
2723
|
+
const hasDs = dsResult.status === "fulfilled" && dsResult.value.length > 0;
|
|
2724
|
+
const dnssecSource = hasDnskey && hasDs ? "domain_configured" : "tld_inherited";
|
|
2725
|
+
if (dnssecSource === "tld_inherited") {
|
|
2726
|
+
const inheritedFinding = createFinding(
|
|
2727
|
+
"dnssec",
|
|
2728
|
+
"DNSSEC inherited from TLD",
|
|
2729
|
+
"info",
|
|
2730
|
+
`DNSSEC validation passes but ${domain} does not have its own DNSKEY or DS records. DNSSEC protection is inherited from the TLD registry, not configured by the domain owner.`,
|
|
2731
|
+
{ dnssecSource: "tld_inherited" }
|
|
2732
|
+
);
|
|
2733
|
+
return buildCheckResult("dnssec", [...baseResult.findings, inheritedFinding]);
|
|
2734
|
+
}
|
|
2735
|
+
if (baseResult.findings.length > 0) {
|
|
2736
|
+
const [first, ...rest] = baseResult.findings;
|
|
2737
|
+
const tagged = { ...first, metadata: { ...first.metadata ?? {}, dnssecSource: "domain_configured" } };
|
|
2738
|
+
return buildCheckResult("dnssec", [tagged, ...rest]);
|
|
2739
|
+
}
|
|
2740
|
+
const configuredFinding = createFinding(
|
|
2529
2741
|
"dnssec",
|
|
2530
|
-
"DNSSEC
|
|
2742
|
+
"DNSSEC configured by domain owner",
|
|
2531
2743
|
"info",
|
|
2532
|
-
|
|
2533
|
-
{ dnssecSource: "
|
|
2744
|
+
`${domain} has DNSKEY and DS records \u2014 DNSSEC is explicitly configured by the domain owner.`,
|
|
2745
|
+
{ dnssecSource: "domain_configured" }
|
|
2534
2746
|
);
|
|
2535
|
-
return buildCheckResult("dnssec", [
|
|
2536
|
-
}
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2747
|
+
return buildCheckResult("dnssec", [configuredFinding]);
|
|
2748
|
+
} catch (err) {
|
|
2749
|
+
if (err instanceof DnsQueryError) {
|
|
2750
|
+
return buildCheckResult("dnssec", [
|
|
2751
|
+
createFinding(
|
|
2752
|
+
"dnssec",
|
|
2753
|
+
"DNSSEC check could not complete",
|
|
2754
|
+
"info",
|
|
2755
|
+
`Unable to verify DNSSEC for ${domain} \u2014 DNS query failed: ${err.message}`,
|
|
2756
|
+
{ checkStatus: "error" }
|
|
2757
|
+
)
|
|
2758
|
+
]);
|
|
2759
|
+
}
|
|
2760
|
+
throw err;
|
|
2541
2761
|
}
|
|
2542
|
-
const configuredFinding = createFinding(
|
|
2543
|
-
"dnssec",
|
|
2544
|
-
"DNSSEC configured by domain owner",
|
|
2545
|
-
"info",
|
|
2546
|
-
`${domain} has DNSKEY and DS records \u2014 DNSSEC is explicitly configured by the domain owner.`,
|
|
2547
|
-
{ dnssecSource: "domain_configured" }
|
|
2548
|
-
);
|
|
2549
|
-
return buildCheckResult("dnssec", [configuredFinding]);
|
|
2550
2762
|
}
|
|
2551
2763
|
|
|
2552
2764
|
// src/tools/check-ssl.ts
|
|
@@ -2896,14 +3108,16 @@ async function checkLookalikes(domain) {
|
|
|
2896
3108
|
checkLookalikesCore(domain),
|
|
2897
3109
|
new Promise((_, reject) => setTimeout(() => reject(new Error("Lookalike check timed out")), LOOKALIKE_TIMEOUT_MS))
|
|
2898
3110
|
]).catch(() => {
|
|
2899
|
-
|
|
3111
|
+
const result = buildCheckResult("lookalikes", [
|
|
2900
3112
|
createFinding(
|
|
2901
3113
|
"lookalikes",
|
|
2902
3114
|
"Lookalike check incomplete",
|
|
2903
3115
|
"info",
|
|
2904
|
-
"Lookalike check did not complete
|
|
3116
|
+
"Lookalike check did not complete within the time limit. Results may be incomplete \u2014 try again shortly."
|
|
2905
3117
|
)
|
|
2906
3118
|
]);
|
|
3119
|
+
result.partial = true;
|
|
3120
|
+
return result;
|
|
2907
3121
|
});
|
|
2908
3122
|
}
|
|
2909
3123
|
async function checkLookalikesCore(domain) {
|
|
@@ -3378,6 +3592,7 @@ async function checkShadowDomains(domain, dnsOptions) {
|
|
|
3378
3592
|
const dnsOpts = { ...dnsOptions, skipSecondaryConfirmation: true };
|
|
3379
3593
|
let primaryMx = [];
|
|
3380
3594
|
let primaryNs = [];
|
|
3595
|
+
let primaryDnsUnavailable = false;
|
|
3381
3596
|
try {
|
|
3382
3597
|
const [mxResult, nsResult] = await Promise.allSettled([
|
|
3383
3598
|
queryMxRecords(domain, dnsOpts),
|
|
@@ -3385,7 +3600,21 @@ async function checkShadowDomains(domain, dnsOptions) {
|
|
|
3385
3600
|
]);
|
|
3386
3601
|
primaryMx = mxResult.status === "fulfilled" ? mxResult.value.map((r) => r.exchange) : [];
|
|
3387
3602
|
primaryNs = nsResult.status === "fulfilled" ? nsResult.value : [];
|
|
3603
|
+
if (mxResult.status === "rejected" && nsResult.status === "rejected") {
|
|
3604
|
+
primaryDnsUnavailable = true;
|
|
3605
|
+
}
|
|
3388
3606
|
} catch {
|
|
3607
|
+
primaryDnsUnavailable = true;
|
|
3608
|
+
}
|
|
3609
|
+
if (primaryDnsUnavailable) {
|
|
3610
|
+
findings.push(
|
|
3611
|
+
createFinding(
|
|
3612
|
+
"shadow_domains",
|
|
3613
|
+
"Primary domain DNS unavailable",
|
|
3614
|
+
"info",
|
|
3615
|
+
`DNS queries for ${domain} failed \u2014 shadow domain analysis may be incomplete.`
|
|
3616
|
+
)
|
|
3617
|
+
);
|
|
3389
3618
|
}
|
|
3390
3619
|
const registeredVariants = await filterByNsExistence2(variants, dnsOpts);
|
|
3391
3620
|
for (const variant of variants) {
|
|
@@ -4862,6 +5091,9 @@ function adjustForNoSendDomain(results) {
|
|
|
4862
5091
|
});
|
|
4863
5092
|
}
|
|
4864
5093
|
|
|
5094
|
+
// src/tools/scan-domain.ts
|
|
5095
|
+
init_log();
|
|
5096
|
+
|
|
4865
5097
|
// src/tools/scan/maturity-staging.ts
|
|
4866
5098
|
function capMaturityStage(maturity, score) {
|
|
4867
5099
|
if (score < 50 && maturity.stage > 2) {
|
|
@@ -6055,7 +6287,12 @@ async function scanDomain(domain, kv, runtimeOptions) {
|
|
|
6055
6287
|
})();
|
|
6056
6288
|
if (runtimeOptions.waitUntil) runtimeOptions.waitUntil(telemetryPromise);
|
|
6057
6289
|
}
|
|
6058
|
-
} catch {
|
|
6290
|
+
} catch (postProcessError) {
|
|
6291
|
+
logError(postProcessError instanceof Error ? postProcessError : String(postProcessError), {
|
|
6292
|
+
category: "scan-domain",
|
|
6293
|
+
domain,
|
|
6294
|
+
details: { phase: "post-processing", checksCompleted: checkResults.length }
|
|
6295
|
+
});
|
|
6059
6296
|
if (degradedStatuses.size > 0) {
|
|
6060
6297
|
checkResults = checkResults.map((r) => {
|
|
6061
6298
|
const status = degradedStatuses.get(r.category);
|
|
@@ -6083,7 +6320,7 @@ async function scanDomain(domain, kv, runtimeOptions) {
|
|
|
6083
6320
|
context: fallbackContext,
|
|
6084
6321
|
cached: false,
|
|
6085
6322
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6086
|
-
scoringNote:
|
|
6323
|
+
scoringNote: "Post-processing encountered an error; results may be approximate",
|
|
6087
6324
|
adaptiveWeightDeltas: null,
|
|
6088
6325
|
interactionEffects: []
|
|
6089
6326
|
};
|
|
@@ -6156,11 +6393,16 @@ async function safeCheck(category, fn) {
|
|
|
6156
6393
|
return result;
|
|
6157
6394
|
} catch (err) {
|
|
6158
6395
|
const rawMessage = err instanceof Error ? err.message : "Check failed";
|
|
6396
|
+
const isTimeout = rawMessage === "Check timed out";
|
|
6159
6397
|
const SAFE_PREFIXES = ["DNS query", "Check timed out", "Check failed", "Connection", "timeout"];
|
|
6160
6398
|
const safeMessage = SAFE_PREFIXES.some((p) => rawMessage.startsWith(p)) ? rawMessage : "Check failed";
|
|
6161
|
-
const
|
|
6399
|
+
const severity = isTimeout ? "low" : "high";
|
|
6400
|
+
const title = isTimeout ? `${category.toUpperCase()} check timed out` : `${category.toUpperCase()} check error`;
|
|
6401
|
+
const detail = isTimeout ? `Check did not complete within the ${PER_CHECK_TIMEOUT_MS / 1e3}s per-check time limit. Try running this check individually.` : `Check failed: ${safeMessage}`;
|
|
6402
|
+
const checkStatus = isTimeout ? "timeout" : "error";
|
|
6403
|
+
const findings = [createFinding(category, title, severity, detail)];
|
|
6162
6404
|
const result = buildCheckResult(category, findings);
|
|
6163
|
-
return { ...result, score: 0, checkStatus
|
|
6405
|
+
return { ...result, score: 0, checkStatus };
|
|
6164
6406
|
}
|
|
6165
6407
|
}
|
|
6166
6408
|
|
|
@@ -9931,13 +10173,19 @@ async function handleToolsCall(params, scanCacheKV, runtimeOptions) {
|
|
|
9931
10173
|
}
|
|
9932
10174
|
const validDomain = domain ?? "";
|
|
9933
10175
|
const effectiveFormat = resolveFormat(validatedArgs, runtimeOptions?.clientType);
|
|
9934
|
-
const
|
|
10176
|
+
const _interactive = isInteractiveClient(runtimeOptions?.clientType);
|
|
9935
10177
|
const executeDispatch = async () => {
|
|
9936
10178
|
const registeredTool = TOOL_REGISTRY[name];
|
|
9937
10179
|
if (registeredTool) {
|
|
9938
10180
|
const checkName = registeredTool.cacheKey(validatedArgs);
|
|
9939
10181
|
const cacheKey = `cache:${validDomain}:check:${checkName}`;
|
|
9940
10182
|
const result = await runWithCache(cacheKey, () => registeredTool.execute(validDomain, validatedArgs, runtimeOptions), scanCacheKV, registeredTool.cacheTtlSeconds);
|
|
10183
|
+
if (result.partial && scanCacheKV) {
|
|
10184
|
+
try {
|
|
10185
|
+
await scanCacheKV.delete(cacheKey);
|
|
10186
|
+
} catch {
|
|
10187
|
+
}
|
|
10188
|
+
}
|
|
9941
10189
|
runtimeOptions?.resultCapture?.(result);
|
|
9942
10190
|
logResult = result.passed ? "pass" : "fail";
|
|
9943
10191
|
logDetails = result;
|
|
@@ -9977,7 +10225,7 @@ async function handleToolsCall(params, scanCacheKV, runtimeOptions) {
|
|
|
9977
10225
|
authTier: runtimeOptions?.authTier
|
|
9978
10226
|
});
|
|
9979
10227
|
const content = [mcpText(formatScanReport(result, effectiveFormat))];
|
|
9980
|
-
if (
|
|
10228
|
+
if (effectiveFormat === "full") {
|
|
9981
10229
|
const structured = buildStructuredScanResult(result);
|
|
9982
10230
|
content.push(mcpText(`<!-- STRUCTURED_RESULT
|
|
9983
10231
|
${JSON.stringify(structured)}
|
|
@@ -10412,7 +10660,7 @@ STRUCTURED_RESULT -->`));
|
|
|
10412
10660
|
clientType: runtimeOptions?.clientType,
|
|
10413
10661
|
authTier: runtimeOptions?.authTier
|
|
10414
10662
|
});
|
|
10415
|
-
return buildToolErrorResult(`Unknown tool: ${name}
|
|
10663
|
+
return buildToolErrorResult(`Unknown tool: ${name}. Call tools/list to see all 44 available tools.`);
|
|
10416
10664
|
}
|
|
10417
10665
|
};
|
|
10418
10666
|
return await Promise.race([
|
|
@@ -10437,7 +10685,7 @@ STRUCTURED_RESULT -->`));
|
|
|
10437
10685
|
isError: true
|
|
10438
10686
|
};
|
|
10439
10687
|
}
|
|
10440
|
-
const message = sanitizeErrorMessage(err,
|
|
10688
|
+
const message = sanitizeErrorMessage(err, `An unexpected error occurred while running ${name}. Retry the request \u2014 transient DNS failures are common.`);
|
|
10441
10689
|
logToolFailure({
|
|
10442
10690
|
toolName: name,
|
|
10443
10691
|
durationMs: Date.now() - startTime,
|
|
@@ -11289,6 +11537,7 @@ async function executeMcpRequest(options) {
|
|
|
11289
11537
|
};
|
|
11290
11538
|
}
|
|
11291
11539
|
}
|
|
11540
|
+
let concurrencyPrincipalId;
|
|
11292
11541
|
if (method !== "tools/call") {
|
|
11293
11542
|
const controlPlaneLimited = await buildControlPlaneRateLimitResponse(
|
|
11294
11543
|
options.ip,
|
|
@@ -11312,12 +11561,13 @@ async function executeMcpRequest(options) {
|
|
|
11312
11561
|
}
|
|
11313
11562
|
}
|
|
11314
11563
|
let sessionRevived = false;
|
|
11315
|
-
|
|
11564
|
+
const isSessionlessProtocolMethod = options.isAuthenticated && (method === "tools/list" || method === "resources/list" || method === "prompts/list" || method === "prompts/get" || method === "ping");
|
|
11565
|
+
if (options.validateSession && method !== "initialize" && !method.startsWith("notifications/") && !isSessionlessProtocolMethod) {
|
|
11316
11566
|
const sessionError = await validateSessionRequest(
|
|
11317
11567
|
options.sessionId,
|
|
11318
11568
|
options.sessionStore,
|
|
11319
11569
|
id,
|
|
11320
|
-
options.sessionErrorMessage ?? "Bad Request: missing session"
|
|
11570
|
+
options.sessionErrorMessage ?? "Bad Request: missing session. Send an initialize request first to create a session."
|
|
11321
11571
|
);
|
|
11322
11572
|
if (sessionError) {
|
|
11323
11573
|
const canRecoverExpiredSession = sessionError.status === 404 && typeof options.sessionId === "string" && options.sessionId.length > 0;
|
|
@@ -11365,6 +11615,43 @@ async function executeMcpRequest(options) {
|
|
|
11365
11615
|
emitRequestAnalytics(options, method, "ok", false);
|
|
11366
11616
|
return { kind: "notification" };
|
|
11367
11617
|
}
|
|
11618
|
+
if (method === "tools/call") {
|
|
11619
|
+
const tier = options.tierAuthResult?.authenticated && options.tierAuthResult.tier ? options.tierAuthResult.tier : "free";
|
|
11620
|
+
const concurrencyLimit = TIER_CONCURRENT_LIMITS[tier];
|
|
11621
|
+
concurrencyPrincipalId = options.tierAuthResult?.authenticated && options.tierAuthResult.keyHash ? options.tierAuthResult.keyHash : options.ip;
|
|
11622
|
+
if (concurrencyLimit !== Infinity) {
|
|
11623
|
+
const concurrencyResult = acquireConcurrencySlot(concurrencyPrincipalId, concurrencyLimit);
|
|
11624
|
+
if (!concurrencyResult.allowed) {
|
|
11625
|
+
const concurrencyHeaders = {
|
|
11626
|
+
...rateHeaders,
|
|
11627
|
+
"retry-after": String(Math.ceil((concurrencyResult.retryAfterMs ?? 1e3) / 1e3))
|
|
11628
|
+
};
|
|
11629
|
+
options.analytics?.emitRateLimitEvent({
|
|
11630
|
+
limitType: "concurrency",
|
|
11631
|
+
toolName: "n/a",
|
|
11632
|
+
limit: concurrencyLimit,
|
|
11633
|
+
remaining: 0,
|
|
11634
|
+
country: options.country,
|
|
11635
|
+
authTier: options.authTier ?? tier
|
|
11636
|
+
});
|
|
11637
|
+
emitRequestAnalytics(options, method, "error", true);
|
|
11638
|
+
return {
|
|
11639
|
+
kind: "response",
|
|
11640
|
+
payload: jsonRpcError(
|
|
11641
|
+
id,
|
|
11642
|
+
JSON_RPC_ERRORS.RATE_LIMITED,
|
|
11643
|
+
`Rate limit exceeded. ${tier} tier is limited to ${concurrencyLimit} concurrent requests.`
|
|
11644
|
+
),
|
|
11645
|
+
headers: concurrencyHeaders,
|
|
11646
|
+
httpStatus: 200,
|
|
11647
|
+
useErrorEnvelope: true,
|
|
11648
|
+
eventId
|
|
11649
|
+
};
|
|
11650
|
+
}
|
|
11651
|
+
} else {
|
|
11652
|
+
concurrencyPrincipalId = void 0;
|
|
11653
|
+
}
|
|
11654
|
+
}
|
|
11368
11655
|
if (options.allowStreaming && method === "tools/call" && options.responseTransport === "sse" && acceptsSSE(options.accept)) {
|
|
11369
11656
|
const dispatchPromise = dispatchMcpMethod({
|
|
11370
11657
|
id,
|
|
@@ -11414,6 +11701,8 @@ async function executeMcpRequest(options) {
|
|
|
11414
11701
|
const hasJsonRpcError = typeof dispatchResult.payload === "object" && dispatchResult.payload !== null && "error" in dispatchResult.payload;
|
|
11415
11702
|
emitRequestAnalytics(options, method, hasJsonRpcError ? "error" : "ok", hasJsonRpcError);
|
|
11416
11703
|
return dispatchResult.payload;
|
|
11704
|
+
}).finally(() => {
|
|
11705
|
+
if (concurrencyPrincipalId) releaseConcurrencySlot(concurrencyPrincipalId);
|
|
11417
11706
|
});
|
|
11418
11707
|
return {
|
|
11419
11708
|
kind: "response",
|
|
@@ -11514,11 +11803,13 @@ async function executeMcpRequest(options) {
|
|
|
11514
11803
|
useErrorEnvelope: true,
|
|
11515
11804
|
eventId
|
|
11516
11805
|
};
|
|
11806
|
+
} finally {
|
|
11807
|
+
if (concurrencyPrincipalId) releaseConcurrencySlot(concurrencyPrincipalId);
|
|
11517
11808
|
}
|
|
11518
11809
|
}
|
|
11519
11810
|
|
|
11520
11811
|
// src/lib/server-version.ts
|
|
11521
|
-
var SERVER_VERSION = "2.
|
|
11812
|
+
var SERVER_VERSION = "2.3.5";
|
|
11522
11813
|
|
|
11523
11814
|
// src/stdio.ts
|
|
11524
11815
|
function buildNotInitializedError(id) {
|