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/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
- return stripped.length > MAX_LOG_STRING_LENGTH ? `${stripped.slice(0, MAX_LOG_STRING_LENGTH)}...` : stripped;
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
- var DEFAULT_TTL_MS, DEFAULT_TTL_SECONDS, DEFAULT_MAX_ENTRIES, TTLCache, INFLIGHT, IN_MEMORY_CACHE;
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 response = await fetch(url, {
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 fetch(url, {
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
- if (opts?.secondaryDoh?.endpoint) {
502
- const bvDns = await fetchDohResponse(
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 checkScopedRateLimitWithCoordinator(ip, "tools", MINUTE_LIMIT, HOUR_LIMIT, quotaCoordinator);
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
- logError("[rate-limiter] quota coordinator error, falling back to KV/in-memory");
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 checkScopedRateLimitWithCoordinator(ip, "control", CONTROL_PLANE_MINUTE_LIMIT, CONTROL_PLANE_HOUR_LIMIT, quotaCoordinator);
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
- logError("[rate-limiter] quota coordinator control-plane error, falling back to KV/in-memory");
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 checkToolDailyRateLimitWithCoordinator(principalId, toolName, limit, quotaCoordinator);
1550
+ const coordinated = await quotaCoordinatorBreaker.call(
1551
+ () => checkToolDailyRateLimitWithCoordinator(principalId, toolName, limit, quotaCoordinator)
1552
+ );
1379
1553
  if (coordinated) return coordinated;
1380
- } catch {
1381
- logError("[rate-limiter] quota coordinator tool quota error, falling back to KV/in-memory");
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 checkGlobalDailyLimitWithCoordinator(limit, quotaCoordinator);
1572
+ const coordinated = await quotaCoordinatorBreaker.call(
1573
+ () => checkGlobalDailyLimitWithCoordinator(limit, quotaCoordinator)
1574
+ );
1397
1575
  if (coordinated) return coordinated;
1398
- } catch {
1399
- logError("[rate-limiter] quota coordinator global cap error, falling back to KV/in-memory");
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
- const baseResult = await checkDNSSEC(
2506
- domain,
2507
- makeQueryDNS4(dnsOptions),
2508
- {
2509
- timeout: dnsOptions?.timeoutMs ?? 5e3,
2510
- rawQueryDNS: async (d, type, dnssecFlag) => {
2511
- const resp = await queryDns(d, type, dnssecFlag ?? false, dnsOptions);
2512
- return { AD: resp.AD, Answer: resp.Answer };
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
- 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");
2517
- if (dnssecAbsent) {
2518
- return baseResult;
2519
- }
2520
- const [dnskeyResult, dsResult] = await Promise.allSettled([
2521
- queryDnsRecords(domain, "DNSKEY", dnsOptions),
2522
- queryDnsRecords(domain, "DS", dnsOptions)
2523
- ]);
2524
- const hasDnskey = dnskeyResult.status === "fulfilled" && dnskeyResult.value.length > 0;
2525
- const hasDs = dsResult.status === "fulfilled" && dsResult.value.length > 0;
2526
- const dnssecSource = hasDnskey && hasDs ? "domain_configured" : "tld_inherited";
2527
- if (dnssecSource === "tld_inherited") {
2528
- const inheritedFinding = createFinding(
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 inherited from TLD",
2742
+ "DNSSEC configured by domain owner",
2531
2743
  "info",
2532
- `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.`,
2533
- { dnssecSource: "tld_inherited" }
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", [...baseResult.findings, inheritedFinding]);
2536
- }
2537
- if (baseResult.findings.length > 0) {
2538
- const [first, ...rest] = baseResult.findings;
2539
- const tagged = { ...first, metadata: { ...first.metadata ?? {}, dnssecSource: "domain_configured" } };
2540
- return buildCheckResult("dnssec", [tagged, ...rest]);
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
- return buildCheckResult("lookalikes", [
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. This check generates many DNS queries \u2014 try again shortly, as partial results are cached."
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: null,
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 findings = [createFinding(category, `${category.toUpperCase()} check error`, "high", `Check failed: ${safeMessage}`)];
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: "error" };
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 interactive = isInteractiveClient(runtimeOptions?.clientType);
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 (!interactive) {
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, "An unexpected error occurred");
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
- if (options.validateSession && method !== "initialize" && !method.startsWith("notifications/")) {
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.2.2";
11812
+ var SERVER_VERSION = "2.3.5";
11522
11813
 
11523
11814
  // src/stdio.ts
11524
11815
  function buildNotInitializedError(id) {