prodlint 0.9.5 → 0.10.2

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/index.js CHANGED
@@ -516,6 +516,33 @@ async function collectWorkspaceDependencies(root, patterns) {
516
516
  }
517
517
  return deps;
518
518
  }
519
+ async function collectNestedPackageDependencies(root) {
520
+ const deps = /* @__PURE__ */ new Set();
521
+ try {
522
+ const pkgFiles = await fg(["**/package.json"], {
523
+ cwd: root,
524
+ absolute: false,
525
+ ignore: ["**/node_modules/**", "package.json"]
526
+ });
527
+ for (const pkgFile of pkgFiles) {
528
+ try {
529
+ const raw = await readFile(resolve(root, pkgFile), "utf-8");
530
+ const pkg = JSON.parse(raw);
531
+ if (pkg.name) deps.add(pkg.name);
532
+ for (const key of ["dependencies", "devDependencies", "peerDependencies"]) {
533
+ if (pkg[key] && typeof pkg[key] === "object") {
534
+ for (const dep of Object.keys(pkg[key])) {
535
+ deps.add(dep);
536
+ }
537
+ }
538
+ }
539
+ } catch {
540
+ }
541
+ }
542
+ } catch {
543
+ }
544
+ return deps;
545
+ }
519
546
  async function buildProjectContext(root, files) {
520
547
  let packageJson = null;
521
548
  let declaredDependencies = /* @__PURE__ */ new Set();
@@ -543,6 +570,10 @@ async function buildProjectContext(root, files) {
543
570
  declaredDependencies.add(dep);
544
571
  }
545
572
  }
573
+ const nestedDeps = await collectNestedPackageDependencies(root);
574
+ for (const dep of nestedDeps) {
575
+ declaredDependencies.add(dep);
576
+ }
546
577
  for (const dep of declaredDependencies) {
547
578
  const framework = DEPENDENCY_TO_FRAMEWORK[dep];
548
579
  if (framework) {
@@ -4709,6 +4740,14 @@ var STATUS_ICONS = {
4709
4740
  info: pc.blue
4710
4741
  };
4711
4742
 
4743
+ // src/web-scanner/index.ts
4744
+ import { lookup as dnsLookup } from "dns";
4745
+ import { lookup as dnsLookupAll } from "dns/promises";
4746
+ import { request as httpRequest } from "http";
4747
+ import { request as httpsRequest } from "https";
4748
+ import { BlockList, isIP } from "net";
4749
+ import { createBrotliDecompress, createGunzip, createInflate } from "zlib";
4750
+
4712
4751
  // src/web-scanner/checks.ts
4713
4752
  function make(id, name, description, maxPoints, severity, status, details) {
4714
4753
  return {
@@ -4848,76 +4887,153 @@ function getDomain(url) {
4848
4887
  return url;
4849
4888
  }
4850
4889
  }
4851
- var PRIVATE_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "0.0.0.0", "[::1]", "metadata.google.internal"]);
4852
- function isPrivateHost(hostname) {
4853
- if (PRIVATE_HOSTNAMES.has(hostname.toLowerCase())) return true;
4854
- const h = hostname.replace(/^\[|\]$/g, "").toLowerCase();
4855
- if (h === "::1" || h.startsWith("fe80:") || h.startsWith("fc") || h.startsWith("fd") || h === "::") return true;
4856
- const ipv4 = hostname.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
4857
- if (ipv4) {
4858
- const [, a, b] = ipv4.map(Number);
4859
- if (a === 10 || a === 127 || a === 0) return true;
4860
- if (a === 172 && b >= 16 && b <= 31) return true;
4861
- if (a === 192 && b === 168) return true;
4862
- if (a === 169 && b === 254) return true;
4863
- if (a === 100 && b >= 64 && b <= 127) return true;
4864
- if (a === 198 && (b === 18 || b === 19)) return true;
4865
- }
4866
- return false;
4890
+ var PRIVATE_RANGES = new BlockList();
4891
+ for (const [network, prefix] of [
4892
+ ["0.0.0.0", 8],
4893
+ ["10.0.0.0", 8],
4894
+ ["100.64.0.0", 10],
4895
+ ["127.0.0.0", 8],
4896
+ ["169.254.0.0", 16],
4897
+ ["172.16.0.0", 12],
4898
+ ["192.0.0.0", 24],
4899
+ ["192.168.0.0", 16],
4900
+ ["198.18.0.0", 15],
4901
+ ["224.0.0.0", 3]
4902
+ ]) PRIVATE_RANGES.addSubnet(network, prefix, "ipv4");
4903
+ for (const [network, prefix] of [
4904
+ ["::", 96],
4905
+ ["64:ff9b:1::", 48],
4906
+ ["2001::", 32],
4907
+ ["fc00::", 7],
4908
+ ["fe80::", 10],
4909
+ ["fec0::", 10],
4910
+ ["ff00::", 8]
4911
+ ]) PRIVATE_RANGES.addSubnet(network, prefix, "ipv6");
4912
+ function ipv6Groups(address) {
4913
+ const quad = address.match(/^(.*:)(\d+)\.(\d+)\.(\d+)\.(\d+)$/);
4914
+ const hex = quad ? `${quad[1]}${(+quad[2] << 8 | +quad[3]).toString(16)}:${(+quad[4] << 8 | +quad[5]).toString(16)}` : address;
4915
+ const [head, tail] = hex.split("::");
4916
+ const parse2 = (part) => part ? part.split(":").map((g) => parseInt(g, 16)) : [];
4917
+ const front = parse2(head);
4918
+ const back = parse2(tail);
4919
+ return tail === void 0 ? front : [...front, ...Array(8 - front.length - back.length).fill(0), ...back];
4867
4920
  }
4868
- function validateRedirectUrl(responseUrl) {
4869
- try {
4870
- const parsed = new URL(responseUrl);
4871
- return !isPrivateHost(parsed.hostname);
4872
- } catch {
4873
- return false;
4874
- }
4921
+ function embeddedIPv4(address) {
4922
+ const g = ipv6Groups(address);
4923
+ const quad = (hi, lo) => `${hi >> 8}.${hi & 255}.${lo >> 8}.${lo & 255}`;
4924
+ const zero = (from, to) => g.slice(from, to).every((x) => x === 0);
4925
+ if (zero(0, 5) && g[5] === 65535) return quad(g[6], g[7]);
4926
+ if (g[0] === 100 && g[1] === 65435 && zero(2, 6)) return quad(g[6], g[7]);
4927
+ if (g[0] === 8194) return quad(g[1], g[2]);
4928
+ return null;
4875
4929
  }
4876
- async function fetchText(url, timeout = 8e3) {
4877
- try {
4878
- const c = new AbortController();
4879
- const t = setTimeout(() => c.abort(), timeout);
4880
- const r = await fetch(url, { signal: c.signal, headers: { "User-Agent": "Prodlint-WebScanner/1.0" }, redirect: "follow" });
4881
- clearTimeout(t);
4882
- if (r.url && !validateRedirectUrl(r.url)) return null;
4883
- return r.ok ? await r.text() : null;
4884
- } catch {
4885
- return null;
4930
+ function isPrivateAddress(address) {
4931
+ const ip = address.replace(/^\[|\]$/g, "").replace(/%.*$/, "");
4932
+ const family = isIP(ip);
4933
+ if (family === 4) return PRIVATE_RANGES.check(ip, "ipv4");
4934
+ if (family !== 6) return false;
4935
+ const v4 = embeddedIPv4(ip);
4936
+ return v4 !== null && PRIVATE_RANGES.check(v4, "ipv4") || PRIVATE_RANGES.check(ip, "ipv6");
4937
+ }
4938
+ function isPrivateHost(hostname) {
4939
+ const host = hostname.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.+$/, "");
4940
+ if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".internal")) return true;
4941
+ return isPrivateAddress(host);
4942
+ }
4943
+ var guardedLookup = (hostname, options, callback) => {
4944
+ dnsLookup(hostname, { ...options, all: true }, (err, addresses) => {
4945
+ if (err) return callback(err, "");
4946
+ const blocked = addresses.find((a) => isPrivateAddress(a.address));
4947
+ if (blocked) {
4948
+ return callback(Object.assign(new Error(`${hostname} resolves to private address ${blocked.address}`), { code: "EPRIVATEADDR" }), "");
4949
+ }
4950
+ if (options.all) return callback(null, addresses);
4951
+ callback(null, addresses[0].address, addresses[0].family);
4952
+ });
4953
+ };
4954
+ var USER_AGENT = "Prodlint-WebScanner/1.0";
4955
+ var MAX_REDIRECTS = 5;
4956
+ var MAX_BODY_BYTES = 5 * 1024 * 1024;
4957
+ function flattenHeaders(res) {
4958
+ const headers = {};
4959
+ for (const [key, value] of Object.entries(res.headers)) {
4960
+ if (value !== void 0) headers[key] = Array.isArray(value) ? value.join(", ") : value;
4886
4961
  }
4962
+ return headers;
4887
4963
  }
4888
- async function fetchHeaders(url, timeout = 8e3) {
4889
- try {
4890
- const c = new AbortController();
4891
- const t = setTimeout(() => c.abort(), timeout);
4892
- const r = await fetch(url, { signal: c.signal, headers: { "User-Agent": "Prodlint-WebScanner/1.0" }, redirect: "follow" });
4893
- clearTimeout(t);
4894
- if (r.url && !validateRedirectUrl(r.url)) return {};
4895
- const h = {};
4896
- r.headers.forEach((v, k) => {
4897
- h[k.toLowerCase()] = v;
4964
+ function readBody(res) {
4965
+ const encoding = String(res.headers["content-encoding"] ?? "").trim().toLowerCase();
4966
+ const decoder = encoding === "gzip" || encoding === "x-gzip" ? createGunzip() : encoding === "deflate" ? createInflate() : encoding === "br" ? createBrotliDecompress() : null;
4967
+ const stream = decoder ? res.pipe(decoder) : res;
4968
+ return new Promise((resolve4, reject) => {
4969
+ const chunks = [];
4970
+ let size = 0;
4971
+ const done = () => resolve4(Buffer.concat(chunks).toString("utf8"));
4972
+ stream.on("data", (chunk) => {
4973
+ if (size >= MAX_BODY_BYTES) return;
4974
+ chunks.push(chunk.subarray(0, MAX_BODY_BYTES - size));
4975
+ size += chunk.length;
4976
+ if (size >= MAX_BODY_BYTES) {
4977
+ done();
4978
+ res.destroy();
4979
+ decoder?.destroy();
4980
+ }
4898
4981
  });
4899
- return h;
4900
- } catch {
4901
- return {};
4902
- }
4982
+ stream.on("end", done);
4983
+ stream.on("error", reject);
4984
+ res.on("error", reject);
4985
+ });
4903
4986
  }
4904
- async function fetchWithTiming(url, timeout = 15e3) {
4905
- try {
4906
- const c = new AbortController();
4907
- const t = setTimeout(() => c.abort(), timeout);
4908
- const start = Date.now();
4909
- const r = await fetch(url, { signal: c.signal, headers: { "User-Agent": "Prodlint-WebScanner/1.0" }, redirect: "follow" });
4910
- const html = await r.text();
4911
- clearTimeout(t);
4912
- if (r.url && !validateRedirectUrl(r.url)) return { html: null, loadTimeMs: null };
4913
- return r.ok ? { html, loadTimeMs: Date.now() - start } : { html: null, loadTimeMs: null };
4914
- } catch {
4915
- return { html: null, loadTimeMs: null };
4916
- }
4987
+ function guardedGet(url, timeoutMs, lookup = guardedLookup) {
4988
+ return new Promise((resolve4) => {
4989
+ let current;
4990
+ const finish = (result) => {
4991
+ clearTimeout(timer);
4992
+ current?.destroy();
4993
+ resolve4(result);
4994
+ };
4995
+ const timer = setTimeout(() => finish(null), timeoutMs);
4996
+ const attempt = (target, redirectsLeft) => {
4997
+ if (target.protocol !== "http:" && target.protocol !== "https:" || isPrivateHost(target.hostname)) return finish(null);
4998
+ const send = target.protocol === "https:" ? httpsRequest : httpRequest;
4999
+ current = send(target, { agent: false, lookup, headers: { "User-Agent": USER_AGENT, "Accept-Encoding": "gzip, deflate, br" } }, (res) => {
5000
+ const status = res.statusCode ?? 0;
5001
+ const location = res.headers.location;
5002
+ if (status >= 300 && status < 400 && location) {
5003
+ res.resume();
5004
+ let next;
5005
+ try {
5006
+ next = new URL(location, target);
5007
+ } catch {
5008
+ return finish(null);
5009
+ }
5010
+ return redirectsLeft > 0 ? attempt(next, redirectsLeft - 1) : finish(null);
5011
+ }
5012
+ readBody(res).then((body) => finish({ status, headers: flattenHeaders(res), body }), () => finish(null));
5013
+ });
5014
+ current.on("error", () => finish(null));
5015
+ current.end();
5016
+ };
5017
+ try {
5018
+ attempt(new URL(url), MAX_REDIRECTS);
5019
+ } catch {
5020
+ finish(null);
5021
+ }
5022
+ });
5023
+ }
5024
+ async function fetchText(url, timeout = 8e3) {
5025
+ const r = await guardedGet(url, timeout);
5026
+ return r && r.status >= 200 && r.status < 300 ? r.body : null;
5027
+ }
5028
+ async function fetchPage(url, timeout = 15e3) {
5029
+ const start = Date.now();
5030
+ const r = await guardedGet(url, timeout);
5031
+ const ok = r !== null && r.status >= 200 && r.status < 300;
5032
+ return { headers: r?.headers ?? {}, html: ok ? r.body : null, loadTimeMs: ok ? Date.now() - start : null };
4917
5033
  }
4918
5034
  async function runWebScan(targetUrl) {
4919
5035
  const domain = getDomain(targetUrl);
4920
- const [robotsTxt, llmsTxt, aiTxt, tdmRep, agentCard, sitemapXml, httpSigDirectory, headers, pageData] = await Promise.all([
5036
+ const [robotsTxt, llmsTxt, aiTxt, tdmRep, agentCard, sitemapXml, httpSigDirectory, page] = await Promise.all([
4921
5037
  fetchText(`${targetUrl}/robots.txt`),
4922
5038
  fetchText(`${targetUrl}/llms.txt`),
4923
5039
  fetchText(`${targetUrl}/ai.txt`),
@@ -4925,10 +5041,9 @@ async function runWebScan(targetUrl) {
4925
5041
  fetchText(`${targetUrl}/.well-known/agent-card.json`),
4926
5042
  fetchText(`${targetUrl}/sitemap.xml`),
4927
5043
  fetchText(`${targetUrl}/.well-known/http-message-signatures-directory`),
4928
- fetchHeaders(targetUrl),
4929
- fetchWithTiming(targetUrl)
5044
+ fetchPage(targetUrl)
4930
5045
  ]);
4931
- const ctx = { url: targetUrl, domain, robotsTxt, llmsTxt, aiTxt, tdmRep, agentCard, sitemapXml, httpSigDirectory, headers, html: pageData.html, loadTimeMs: pageData.loadTimeMs };
5046
+ const ctx = { url: targetUrl, domain, robotsTxt, llmsTxt, aiTxt, tdmRep, agentCard, sitemapXml, httpSigDirectory, ...page };
4932
5047
  const checks = allChecks.map((fn) => fn(ctx));
4933
5048
  const totalPoints = checks.reduce((s, c) => s + c.points, 0);
4934
5049
  const maxPoints = checks.reduce((s, c) => s + c.maxPoints, 0);
package/dist/mcp.js CHANGED
@@ -525,6 +525,33 @@ async function collectWorkspaceDependencies(root, patterns) {
525
525
  }
526
526
  return deps;
527
527
  }
528
+ async function collectNestedPackageDependencies(root) {
529
+ const deps = /* @__PURE__ */ new Set();
530
+ try {
531
+ const pkgFiles = await fg(["**/package.json"], {
532
+ cwd: root,
533
+ absolute: false,
534
+ ignore: ["**/node_modules/**", "package.json"]
535
+ });
536
+ for (const pkgFile of pkgFiles) {
537
+ try {
538
+ const raw = await readFile(resolve(root, pkgFile), "utf-8");
539
+ const pkg = JSON.parse(raw);
540
+ if (pkg.name) deps.add(pkg.name);
541
+ for (const key of ["dependencies", "devDependencies", "peerDependencies"]) {
542
+ if (pkg[key] && typeof pkg[key] === "object") {
543
+ for (const dep of Object.keys(pkg[key])) {
544
+ deps.add(dep);
545
+ }
546
+ }
547
+ }
548
+ } catch {
549
+ }
550
+ }
551
+ } catch {
552
+ }
553
+ return deps;
554
+ }
528
555
  async function buildProjectContext(root, files) {
529
556
  let packageJson = null;
530
557
  let declaredDependencies = /* @__PURE__ */ new Set();
@@ -552,6 +579,10 @@ async function buildProjectContext(root, files) {
552
579
  declaredDependencies.add(dep);
553
580
  }
554
581
  }
582
+ const nestedDeps = await collectNestedPackageDependencies(root);
583
+ for (const dep of nestedDeps) {
584
+ declaredDependencies.add(dep);
585
+ }
555
586
  for (const dep of declaredDependencies) {
556
587
  const framework = DEPENDENCY_TO_FRAMEWORK[dep];
557
588
  if (framework) {
@@ -4525,6 +4556,14 @@ async function scan(options) {
4525
4556
  };
4526
4557
  }
4527
4558
 
4559
+ // src/web-scanner/index.ts
4560
+ import { lookup as dnsLookup } from "dns";
4561
+ import { lookup as dnsLookupAll } from "dns/promises";
4562
+ import { request as httpRequest } from "http";
4563
+ import { request as httpsRequest } from "https";
4564
+ import { BlockList, isIP } from "net";
4565
+ import { createBrotliDecompress, createGunzip, createInflate } from "zlib";
4566
+
4528
4567
  // src/web-scanner/checks.ts
4529
4568
  function make(id, name, description, maxPoints, severity, status, details) {
4530
4569
  return {
@@ -4664,72 +4703,157 @@ function getDomain(url) {
4664
4703
  return url;
4665
4704
  }
4666
4705
  }
4667
- var PRIVATE_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "0.0.0.0", "[::1]", "metadata.google.internal"]);
4706
+ var PRIVATE_RANGES = new BlockList();
4707
+ for (const [network, prefix] of [
4708
+ ["0.0.0.0", 8],
4709
+ ["10.0.0.0", 8],
4710
+ ["100.64.0.0", 10],
4711
+ ["127.0.0.0", 8],
4712
+ ["169.254.0.0", 16],
4713
+ ["172.16.0.0", 12],
4714
+ ["192.0.0.0", 24],
4715
+ ["192.168.0.0", 16],
4716
+ ["198.18.0.0", 15],
4717
+ ["224.0.0.0", 3]
4718
+ ]) PRIVATE_RANGES.addSubnet(network, prefix, "ipv4");
4719
+ for (const [network, prefix] of [
4720
+ ["::", 96],
4721
+ ["64:ff9b:1::", 48],
4722
+ ["2001::", 32],
4723
+ ["fc00::", 7],
4724
+ ["fe80::", 10],
4725
+ ["fec0::", 10],
4726
+ ["ff00::", 8]
4727
+ ]) PRIVATE_RANGES.addSubnet(network, prefix, "ipv6");
4728
+ function ipv6Groups(address) {
4729
+ const quad = address.match(/^(.*:)(\d+)\.(\d+)\.(\d+)\.(\d+)$/);
4730
+ const hex = quad ? `${quad[1]}${(+quad[2] << 8 | +quad[3]).toString(16)}:${(+quad[4] << 8 | +quad[5]).toString(16)}` : address;
4731
+ const [head, tail] = hex.split("::");
4732
+ const parse2 = (part) => part ? part.split(":").map((g) => parseInt(g, 16)) : [];
4733
+ const front = parse2(head);
4734
+ const back = parse2(tail);
4735
+ return tail === void 0 ? front : [...front, ...Array(8 - front.length - back.length).fill(0), ...back];
4736
+ }
4737
+ function embeddedIPv4(address) {
4738
+ const g = ipv6Groups(address);
4739
+ const quad = (hi, lo) => `${hi >> 8}.${hi & 255}.${lo >> 8}.${lo & 255}`;
4740
+ const zero = (from, to) => g.slice(from, to).every((x) => x === 0);
4741
+ if (zero(0, 5) && g[5] === 65535) return quad(g[6], g[7]);
4742
+ if (g[0] === 100 && g[1] === 65435 && zero(2, 6)) return quad(g[6], g[7]);
4743
+ if (g[0] === 8194) return quad(g[1], g[2]);
4744
+ return null;
4745
+ }
4746
+ function isPrivateAddress(address) {
4747
+ const ip = address.replace(/^\[|\]$/g, "").replace(/%.*$/, "");
4748
+ const family = isIP(ip);
4749
+ if (family === 4) return PRIVATE_RANGES.check(ip, "ipv4");
4750
+ if (family !== 6) return false;
4751
+ const v4 = embeddedIPv4(ip);
4752
+ return v4 !== null && PRIVATE_RANGES.check(v4, "ipv4") || PRIVATE_RANGES.check(ip, "ipv6");
4753
+ }
4668
4754
  function isPrivateHost(hostname) {
4669
- if (PRIVATE_HOSTNAMES.has(hostname.toLowerCase())) return true;
4670
- const h = hostname.replace(/^\[|\]$/g, "").toLowerCase();
4671
- if (h === "::1" || h.startsWith("fe80:") || h.startsWith("fc") || h.startsWith("fd") || h === "::") return true;
4672
- const ipv4 = hostname.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
4673
- if (ipv4) {
4674
- const [, a, b] = ipv4.map(Number);
4675
- if (a === 10 || a === 127 || a === 0) return true;
4676
- if (a === 172 && b >= 16 && b <= 31) return true;
4677
- if (a === 192 && b === 168) return true;
4678
- if (a === 169 && b === 254) return true;
4679
- if (a === 100 && b >= 64 && b <= 127) return true;
4680
- if (a === 198 && (b === 18 || b === 19)) return true;
4681
- }
4682
- return false;
4755
+ const host = hostname.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.+$/, "");
4756
+ if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".internal")) return true;
4757
+ return isPrivateAddress(host);
4683
4758
  }
4684
- function validateRedirectUrl(responseUrl) {
4759
+ async function resolvesToPrivateAddress(hostname) {
4685
4760
  try {
4686
- const parsed = new URL(responseUrl);
4687
- return !isPrivateHost(parsed.hostname);
4761
+ const addresses = await dnsLookupAll(hostname.replace(/^\[|\]$/g, ""), { all: true });
4762
+ return addresses.some((a) => isPrivateAddress(a.address));
4688
4763
  } catch {
4689
4764
  return false;
4690
4765
  }
4691
4766
  }
4692
- async function fetchText(url, timeout = 8e3) {
4693
- try {
4694
- const c = new AbortController();
4695
- const t = setTimeout(() => c.abort(), timeout);
4696
- const r = await fetch(url, { signal: c.signal, headers: { "User-Agent": "Prodlint-WebScanner/1.0" }, redirect: "follow" });
4697
- clearTimeout(t);
4698
- if (r.url && !validateRedirectUrl(r.url)) return null;
4699
- return r.ok ? await r.text() : null;
4700
- } catch {
4701
- return null;
4767
+ var guardedLookup = (hostname, options, callback) => {
4768
+ dnsLookup(hostname, { ...options, all: true }, (err, addresses) => {
4769
+ if (err) return callback(err, "");
4770
+ const blocked = addresses.find((a) => isPrivateAddress(a.address));
4771
+ if (blocked) {
4772
+ return callback(Object.assign(new Error(`${hostname} resolves to private address ${blocked.address}`), { code: "EPRIVATEADDR" }), "");
4773
+ }
4774
+ if (options.all) return callback(null, addresses);
4775
+ callback(null, addresses[0].address, addresses[0].family);
4776
+ });
4777
+ };
4778
+ var USER_AGENT = "Prodlint-WebScanner/1.0";
4779
+ var MAX_REDIRECTS = 5;
4780
+ var MAX_BODY_BYTES = 5 * 1024 * 1024;
4781
+ function flattenHeaders(res) {
4782
+ const headers = {};
4783
+ for (const [key, value] of Object.entries(res.headers)) {
4784
+ if (value !== void 0) headers[key] = Array.isArray(value) ? value.join(", ") : value;
4702
4785
  }
4786
+ return headers;
4703
4787
  }
4704
- async function fetchHeaders(url, timeout = 8e3) {
4705
- try {
4706
- const c = new AbortController();
4707
- const t = setTimeout(() => c.abort(), timeout);
4708
- const r = await fetch(url, { signal: c.signal, headers: { "User-Agent": "Prodlint-WebScanner/1.0" }, redirect: "follow" });
4709
- clearTimeout(t);
4710
- if (r.url && !validateRedirectUrl(r.url)) return {};
4711
- const h = {};
4712
- r.headers.forEach((v, k) => {
4713
- h[k.toLowerCase()] = v;
4788
+ function readBody(res) {
4789
+ const encoding = String(res.headers["content-encoding"] ?? "").trim().toLowerCase();
4790
+ const decoder = encoding === "gzip" || encoding === "x-gzip" ? createGunzip() : encoding === "deflate" ? createInflate() : encoding === "br" ? createBrotliDecompress() : null;
4791
+ const stream = decoder ? res.pipe(decoder) : res;
4792
+ return new Promise((resolve5, reject) => {
4793
+ const chunks = [];
4794
+ let size = 0;
4795
+ const done = () => resolve5(Buffer.concat(chunks).toString("utf8"));
4796
+ stream.on("data", (chunk) => {
4797
+ if (size >= MAX_BODY_BYTES) return;
4798
+ chunks.push(chunk.subarray(0, MAX_BODY_BYTES - size));
4799
+ size += chunk.length;
4800
+ if (size >= MAX_BODY_BYTES) {
4801
+ done();
4802
+ res.destroy();
4803
+ decoder?.destroy();
4804
+ }
4714
4805
  });
4715
- return h;
4716
- } catch {
4717
- return {};
4718
- }
4806
+ stream.on("end", done);
4807
+ stream.on("error", reject);
4808
+ res.on("error", reject);
4809
+ });
4719
4810
  }
4720
- async function fetchWithTiming(url, timeout = 15e3) {
4721
- try {
4722
- const c = new AbortController();
4723
- const t = setTimeout(() => c.abort(), timeout);
4724
- const start = Date.now();
4725
- const r = await fetch(url, { signal: c.signal, headers: { "User-Agent": "Prodlint-WebScanner/1.0" }, redirect: "follow" });
4726
- const html = await r.text();
4727
- clearTimeout(t);
4728
- if (r.url && !validateRedirectUrl(r.url)) return { html: null, loadTimeMs: null };
4729
- return r.ok ? { html, loadTimeMs: Date.now() - start } : { html: null, loadTimeMs: null };
4730
- } catch {
4731
- return { html: null, loadTimeMs: null };
4732
- }
4811
+ function guardedGet(url, timeoutMs, lookup = guardedLookup) {
4812
+ return new Promise((resolve5) => {
4813
+ let current;
4814
+ const finish = (result) => {
4815
+ clearTimeout(timer);
4816
+ current?.destroy();
4817
+ resolve5(result);
4818
+ };
4819
+ const timer = setTimeout(() => finish(null), timeoutMs);
4820
+ const attempt = (target, redirectsLeft) => {
4821
+ if (target.protocol !== "http:" && target.protocol !== "https:" || isPrivateHost(target.hostname)) return finish(null);
4822
+ const send = target.protocol === "https:" ? httpsRequest : httpRequest;
4823
+ current = send(target, { agent: false, lookup, headers: { "User-Agent": USER_AGENT, "Accept-Encoding": "gzip, deflate, br" } }, (res) => {
4824
+ const status = res.statusCode ?? 0;
4825
+ const location = res.headers.location;
4826
+ if (status >= 300 && status < 400 && location) {
4827
+ res.resume();
4828
+ let next;
4829
+ try {
4830
+ next = new URL(location, target);
4831
+ } catch {
4832
+ return finish(null);
4833
+ }
4834
+ return redirectsLeft > 0 ? attempt(next, redirectsLeft - 1) : finish(null);
4835
+ }
4836
+ readBody(res).then((body) => finish({ status, headers: flattenHeaders(res), body }), () => finish(null));
4837
+ });
4838
+ current.on("error", () => finish(null));
4839
+ current.end();
4840
+ };
4841
+ try {
4842
+ attempt(new URL(url), MAX_REDIRECTS);
4843
+ } catch {
4844
+ finish(null);
4845
+ }
4846
+ });
4847
+ }
4848
+ async function fetchText(url, timeout = 8e3) {
4849
+ const r = await guardedGet(url, timeout);
4850
+ return r && r.status >= 200 && r.status < 300 ? r.body : null;
4851
+ }
4852
+ async function fetchPage(url, timeout = 15e3) {
4853
+ const start = Date.now();
4854
+ const r = await guardedGet(url, timeout);
4855
+ const ok = r !== null && r.status >= 200 && r.status < 300;
4856
+ return { headers: r?.headers ?? {}, html: ok ? r.body : null, loadTimeMs: ok ? Date.now() - start : null };
4733
4857
  }
4734
4858
  function normalizeUrl(input) {
4735
4859
  let url = input.trim();
@@ -4741,7 +4865,7 @@ function normalizeUrl(input) {
4741
4865
  }
4742
4866
  async function runWebScan(targetUrl) {
4743
4867
  const domain = getDomain(targetUrl);
4744
- const [robotsTxt, llmsTxt, aiTxt, tdmRep, agentCard, sitemapXml, httpSigDirectory, headers, pageData] = await Promise.all([
4868
+ const [robotsTxt, llmsTxt, aiTxt, tdmRep, agentCard, sitemapXml, httpSigDirectory, page] = await Promise.all([
4745
4869
  fetchText(`${targetUrl}/robots.txt`),
4746
4870
  fetchText(`${targetUrl}/llms.txt`),
4747
4871
  fetchText(`${targetUrl}/ai.txt`),
@@ -4749,10 +4873,9 @@ async function runWebScan(targetUrl) {
4749
4873
  fetchText(`${targetUrl}/.well-known/agent-card.json`),
4750
4874
  fetchText(`${targetUrl}/sitemap.xml`),
4751
4875
  fetchText(`${targetUrl}/.well-known/http-message-signatures-directory`),
4752
- fetchHeaders(targetUrl),
4753
- fetchWithTiming(targetUrl)
4876
+ fetchPage(targetUrl)
4754
4877
  ]);
4755
- const ctx = { url: targetUrl, domain, robotsTxt, llmsTxt, aiTxt, tdmRep, agentCard, sitemapXml, httpSigDirectory, headers, html: pageData.html, loadTimeMs: pageData.loadTimeMs };
4878
+ const ctx = { url: targetUrl, domain, robotsTxt, llmsTxt, aiTxt, tdmRep, agentCard, sitemapXml, httpSigDirectory, ...page };
4756
4879
  const checks = allChecks.map((fn) => fn(ctx));
4757
4880
  const totalPoints = checks.reduce((s, c) => s + c.points, 0);
4758
4881
  const maxPoints = checks.reduce((s, c) => s + c.maxPoints, 0);
@@ -4854,7 +4977,7 @@ server.tool(
4854
4977
  };
4855
4978
  }
4856
4979
  const hostname = new URL(normalizedUrl).hostname;
4857
- if (isPrivateHost(hostname)) {
4980
+ if (isPrivateHost(hostname) || await resolvesToPrivateAddress(hostname)) {
4858
4981
  return {
4859
4982
  content: [{ type: "text", text: "Error: Cannot scan private or internal hosts." }],
4860
4983
  isError: true