prodlint 0.9.1 → 0.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp.js CHANGED
@@ -4511,113 +4511,111 @@ async function scan(options) {
4511
4511
  }
4512
4512
 
4513
4513
  // src/web-scanner/checks.ts
4514
- function make(id, name, description, maxPoints, severity, maturity, status, details) {
4514
+ function make(id, name, description, maxPoints, severity, status, details) {
4515
4515
  return {
4516
4516
  id,
4517
4517
  name,
4518
4518
  description,
4519
4519
  status,
4520
4520
  severity,
4521
- maturity,
4522
4521
  details,
4523
4522
  points: status === "pass" ? maxPoints : status === "warn" ? Math.floor(maxPoints / 2) : 0,
4524
4523
  maxPoints
4525
4524
  };
4526
4525
  }
4527
4526
  function checkRobotsTxt(ctx) {
4528
- if (!ctx.robotsTxt) return make("robots_txt", "robots.txt", "robots.txt exists and is accessible", 7, "medium", "established", "fail", "No robots.txt found.");
4529
- return make("robots_txt", "robots.txt", "robots.txt exists and is accessible", 7, "medium", "established", "pass", "robots.txt found.");
4527
+ if (!ctx.robotsTxt) return make("robots_txt", "robots.txt", "robots.txt exists and is accessible", 5, "medium", "fail", "No robots.txt found.");
4528
+ return make("robots_txt", "robots.txt", "robots.txt exists and is accessible", 5, "medium", "pass", "robots.txt found.");
4530
4529
  }
4531
4530
  function checkRobotsAiDirectives(ctx) {
4532
- if (!ctx.robotsTxt) return make("robots_ai_directives", "AI robots.txt Directives", "AI bot user-agents in robots.txt", 15, "critical", "established", "fail", "No robots.txt found. AI bots have no guidance.");
4533
- const agents = ["GPTBot", "ChatGPT-User", "ClaudeBot", "Claude-Web", "Google-Extended", "PerplexityBot", "Bytespider", "CCBot", "anthropic-ai", "cohere-ai", "Applebot-Extended", "Grok"];
4531
+ if (!ctx.robotsTxt) return make("robots_ai_directives", "AI robots.txt Directives", "AI bot user-agents in robots.txt", 15, "critical", "fail", "No robots.txt found. AI bots have no guidance.");
4532
+ const agents = ["GPTBot", "ChatGPT-User", "ClaudeBot", "Claude-Web", "Google-Extended", "PerplexityBot", "Bytespider", "CCBot", "anthropic-ai", "cohere-ai"];
4534
4533
  const found = agents.filter((a) => ctx.robotsTxt.toLowerCase().includes(a.toLowerCase()));
4535
- if (found.length === 0) return make("robots_ai_directives", "AI robots.txt Directives", "AI bot user-agents in robots.txt", 15, "critical", "established", "fail", "No AI-specific user-agent directives found.");
4536
- if (found.length < 3) return make("robots_ai_directives", "AI robots.txt Directives", "AI bot user-agents in robots.txt", 15, "critical", "established", "warn", `Found ${found.length} AI bot directive(s): ${found.join(", ")}.`);
4537
- return make("robots_ai_directives", "AI robots.txt Directives", "AI bot user-agents in robots.txt", 15, "critical", "established", "pass", `Found ${found.length} AI bot directive(s): ${found.join(", ")}.`);
4534
+ if (found.length === 0) return make("robots_ai_directives", "AI robots.txt Directives", "AI bot user-agents in robots.txt", 15, "critical", "fail", "No AI-specific user-agent directives found.");
4535
+ if (found.length < 3) return make("robots_ai_directives", "AI robots.txt Directives", "AI bot user-agents in robots.txt", 15, "critical", "warn", `Found ${found.length} AI bot directive(s): ${found.join(", ")}.`);
4536
+ return make("robots_ai_directives", "AI robots.txt Directives", "AI bot user-agents in robots.txt", 15, "critical", "pass", `Found ${found.length} AI bot directive(s): ${found.join(", ")}.`);
4538
4537
  }
4539
4538
  function checkContentUsage(ctx) {
4540
4539
  const hasHeader = ctx.headers["content-usage"] != null;
4541
4540
  const hasInRobots = ctx.robotsTxt?.toLowerCase().includes("content-usage") ?? false;
4542
- if (!hasHeader && !hasInRobots) return make("content_usage", "Content-Usage (IETF aipref)", "Content-Usage header or directives", 5, "high", "emerging", "fail", "No Content-Usage directives found.");
4543
- return make("content_usage", "Content-Usage (IETF aipref)", "Content-Usage header or directives", 5, "high", "emerging", "pass", hasHeader ? `Header: ${ctx.headers["content-usage"]}` : "Found in robots.txt.");
4541
+ if (!hasHeader && !hasInRobots) return make("content_usage", "Content-Usage (IETF aipref)", "Content-Usage header or directives", 10, "high", "fail", "No Content-Usage directives found.");
4542
+ return make("content_usage", "Content-Usage (IETF aipref)", "Content-Usage header or directives", 10, "high", "pass", hasHeader ? `Header: ${ctx.headers["content-usage"]}` : "Found in robots.txt.");
4544
4543
  }
4545
4544
  function checkLlmsTxt(ctx) {
4546
- if (!ctx.llmsTxt) return make("llms_txt", "llms.txt", "LLM-optimized site summary at /llms.txt", 6, "high", "emerging", "fail", "No llms.txt found.");
4547
- const lines = ctx.llmsTxt.split("\n").filter((l) => l.trim().length > 0 && !l.trim().startsWith("#"));
4548
- if (lines.length < 3) return make("llms_txt", "llms.txt", "LLM-optimized site summary at /llms.txt", 6, "high", "emerging", "warn", "llms.txt found but appears minimal.");
4549
- return make("llms_txt", "llms.txt", "LLM-optimized site summary at /llms.txt", 6, "high", "emerging", "pass", `llms.txt found with ${lines.length} content lines.`);
4545
+ if (!ctx.llmsTxt) return make("llms_txt", "llms.txt", "LLM-optimized site summary at /llms.txt", 10, "high", "fail", "No llms.txt found.");
4546
+ const lines = ctx.llmsTxt.split("\n").filter((l) => l.trim().length > 0);
4547
+ if (lines.length < 3) return make("llms_txt", "llms.txt", "LLM-optimized site summary at /llms.txt", 10, "high", "warn", "llms.txt found but appears minimal.");
4548
+ return make("llms_txt", "llms.txt", "LLM-optimized site summary at /llms.txt", 10, "high", "pass", `llms.txt found with ${lines.length} lines.`);
4550
4549
  }
4551
4550
  function checkTdmRep(ctx) {
4552
4551
  const hasWK = ctx.tdmRep != null;
4553
4552
  const hasHeader = ctx.headers["tdm-reservation"] != null;
4554
- if (!hasWK && !hasHeader) return make("tdmrep", "TDMRep (W3C)", "Text & data mining reservation file or header", 4, "medium", "emerging", "fail", "No TDMRep configuration found.");
4555
- return make("tdmrep", "TDMRep (W3C)", "Text & data mining reservation file or header", 4, "medium", "emerging", "pass", hasWK ? "/.well-known/tdmrep.json found." : `TDM-Reservation header: ${ctx.headers["tdm-reservation"]}`);
4553
+ if (!hasWK && !hasHeader) return make("tdmrep", "TDMRep (W3C)", "Text & data mining reservation file or header", 8, "medium", "fail", "No TDMRep configuration found.");
4554
+ return make("tdmrep", "TDMRep (W3C)", "Text & data mining reservation file or header", 8, "medium", "pass", hasWK ? "/.well-known/tdmrep.json found." : `TDM-Reservation header: ${ctx.headers["tdm-reservation"]}`);
4556
4555
  }
4557
4556
  function checkAiDisclosure(ctx) {
4558
- if (!ctx.headers["ai-disclosure"]) return make("ai_disclosure", "AI-Disclosure Header", "Declares AI involvement in content generation", 2, "low", "emerging", "fail", "No AI-Disclosure header found.");
4559
- return make("ai_disclosure", "AI-Disclosure Header", "Declares AI involvement in content generation", 2, "low", "emerging", "pass", `Header: ${ctx.headers["ai-disclosure"]}`);
4557
+ if (!ctx.headers["ai-disclosure"]) return make("ai_disclosure", "AI-Disclosure Header", "Declares AI involvement in content generation", 5, "low", "fail", "No AI-Disclosure header found.");
4558
+ return make("ai_disclosure", "AI-Disclosure Header", "Declares AI involvement in content generation", 5, "low", "pass", `Header: ${ctx.headers["ai-disclosure"]}`);
4560
4559
  }
4561
4560
  function checkAgentCard(ctx) {
4562
- if (!ctx.agentCard) return make("agent_card", "A2A AgentCard", "/.well-known/agent-card.json for agent discovery", 5, "high", "emerging", "fail", "No A2A AgentCard found.");
4561
+ if (!ctx.agentCard) return make("agent_card", "A2A AgentCard", "/.well-known/agent-card.json for agent discovery", 10, "high", "fail", "No A2A AgentCard found.");
4563
4562
  try {
4564
4563
  const card = JSON.parse(ctx.agentCard);
4565
- if (!card.name || !Array.isArray(card.skills) || card.skills.length === 0) return make("agent_card", "A2A AgentCard", "/.well-known/agent-card.json for agent discovery", 5, "high", "emerging", "warn", "AgentCard found but missing name or skills.");
4566
- return make("agent_card", "A2A AgentCard", "/.well-known/agent-card.json for agent discovery", 5, "high", "emerging", "pass", `AgentCard found with ${card.skills.length} skill(s).`);
4564
+ if (!card.name || !Array.isArray(card.skills) || card.skills.length === 0) return make("agent_card", "A2A AgentCard", "/.well-known/agent-card.json for agent discovery", 10, "high", "warn", "AgentCard found but missing name or skills.");
4565
+ return make("agent_card", "A2A AgentCard", "/.well-known/agent-card.json for agent discovery", 10, "high", "pass", `AgentCard found with ${card.skills.length} skill(s).`);
4567
4566
  } catch {
4568
- return make("agent_card", "A2A AgentCard", "/.well-known/agent-card.json for agent discovery", 5, "high", "emerging", "warn", "AgentCard found but contains invalid JSON.");
4567
+ return make("agent_card", "A2A AgentCard", "/.well-known/agent-card.json for agent discovery", 10, "high", "warn", "AgentCard found but contains invalid JSON.");
4569
4568
  }
4570
4569
  }
4571
4570
  function checkAiTxt(ctx) {
4572
- if (!ctx.aiTxt) return make("ai_txt", "ai.txt", "AI training permissions per Spawning spec", 3, "medium", "emerging", "fail", "No ai.txt found at site root.");
4571
+ if (!ctx.aiTxt) return make("ai_txt", "ai.txt", "AI training permissions per Spawning spec", 5, "medium", "fail", "No ai.txt found at site root.");
4573
4572
  const lines = ctx.aiTxt.split("\n").filter((l) => l.trim().length > 0 && !l.trim().startsWith("#"));
4574
- if (lines.length < 2) return make("ai_txt", "ai.txt", "AI training permissions per Spawning spec", 3, "medium", "emerging", "warn", "ai.txt found but appears minimal.");
4575
- return make("ai_txt", "ai.txt", "AI training permissions per Spawning spec", 3, "medium", "emerging", "pass", `ai.txt found with ${lines.length} directive(s).`);
4573
+ if (lines.length < 2) return make("ai_txt", "ai.txt", "AI training permissions per Spawning spec", 5, "medium", "warn", "ai.txt found but appears minimal.");
4574
+ return make("ai_txt", "ai.txt", "AI training permissions per Spawning spec", 5, "medium", "pass", `ai.txt found with ${lines.length} directive(s).`);
4576
4575
  }
4577
4576
  function checkWebMCP(ctx) {
4578
- if (!ctx.html) return make("webmcp", "WebMCP Tools", "Chrome 146+ WebMCP tool registration", 5, "high", "emerging", "info", "Could not check for WebMCP tools.");
4577
+ if (!ctx.html) return make("webmcp", "WebMCP Tools", "Chrome 146+ WebMCP tool registration", 10, "high", "info", "Could not check for WebMCP tools.");
4579
4578
  const hasToolname = /toolname=/i.test(ctx.html);
4580
4579
  const hasModelContext = /navigator\.modelContext/i.test(ctx.html) || /registerTool/i.test(ctx.html);
4581
- if (!hasToolname && !hasModelContext) return make("webmcp", "WebMCP Tools", "Chrome 146+ WebMCP tool registration", 5, "high", "emerging", "fail", "No WebMCP tools detected.");
4580
+ if (!hasToolname && !hasModelContext) return make("webmcp", "WebMCP Tools", "Chrome 146+ WebMCP tool registration", 10, "high", "fail", "No WebMCP tools detected.");
4582
4581
  const count = (ctx.html.match(/toolname=/gi) || []).length;
4583
- return make("webmcp", "WebMCP Tools", "Chrome 146+ WebMCP tool registration", 5, "high", "emerging", "pass", hasToolname ? `${count} form(s) with toolname attributes.` : "registerTool() usage detected.");
4582
+ return make("webmcp", "WebMCP Tools", "Chrome 146+ WebMCP tool registration", 10, "high", "pass", hasToolname ? `${count} form(s) with toolname attributes.` : "registerTool() usage detected.");
4584
4583
  }
4585
4584
  function checkStructuredData(ctx) {
4586
- if (!ctx.html) return make("structured_data", "Structured Data", "JSON-LD or Schema.org markup", 12, "medium", "established", "info", "Could not fetch page HTML.");
4585
+ if (!ctx.html) return make("structured_data", "Structured Data", "JSON-LD or Schema.org markup", 10, "medium", "info", "Could not fetch page HTML.");
4587
4586
  const jsonLd = ctx.html.match(/<script[^>]*type=["']application\/ld\+json["'][^>]*>/gi);
4588
4587
  const hasMicrodata = ctx.html.includes("itemscope") && ctx.html.includes("itemtype");
4589
- if (!jsonLd && !hasMicrodata) return make("structured_data", "Structured Data", "JSON-LD or Schema.org markup", 12, "medium", "established", "fail", "No structured data found.");
4590
- return make("structured_data", "Structured Data", "JSON-LD or Schema.org markup", 12, "medium", "established", "pass", `Found ${(jsonLd?.length || 0) + (hasMicrodata ? 1 : 0)} structured data block(s).`);
4588
+ if (!jsonLd && !hasMicrodata) return make("structured_data", "Structured Data", "JSON-LD or Schema.org markup", 10, "medium", "fail", "No structured data found.");
4589
+ return make("structured_data", "Structured Data", "JSON-LD or Schema.org markup", 10, "medium", "pass", `Found ${(jsonLd?.length || 0) + (hasMicrodata ? 1 : 0)} structured data block(s).`);
4591
4590
  }
4592
4591
  function checkOpenGraph(ctx) {
4593
- if (!ctx.html) return make("opengraph", "OpenGraph & Meta", "OG tags and meta description", 10, "low", "established", "info", "Could not fetch HTML.");
4592
+ if (!ctx.html) return make("opengraph", "OpenGraph & Meta", "OG tags and meta description", 7, "low", "info", "Could not fetch HTML.");
4594
4593
  const checks = [/og:title/i.test(ctx.html), /og:description/i.test(ctx.html), /og:image/i.test(ctx.html), /name=["']description["']/i.test(ctx.html)];
4595
4594
  const passed = checks.filter(Boolean).length;
4596
- if (passed === 0) return make("opengraph", "OpenGraph & Meta", "OG tags and meta description", 10, "low", "established", "fail", "No OpenGraph tags or meta description.");
4597
- if (passed < 3) return make("opengraph", "OpenGraph & Meta", "OG tags and meta description", 10, "low", "established", "warn", `Found ${passed}/4 meta tags.`);
4598
- if (passed < 4) return make("opengraph", "OpenGraph & Meta", "OG tags and meta description", 10, "low", "established", "pass", `Found ${passed}/4 key meta tags.`);
4599
- return make("opengraph", "OpenGraph & Meta", "OG tags and meta description", 10, "low", "established", "pass", "All 4 key meta tags present.");
4595
+ if (passed === 0) return make("opengraph", "OpenGraph & Meta", "OG tags and meta description", 7, "low", "fail", "No OpenGraph tags or meta description.");
4596
+ if (passed < 3) return make("opengraph", "OpenGraph & Meta", "OG tags and meta description", 7, "low", "warn", `Found ${passed}/4 meta tags.`);
4597
+ return make("opengraph", "OpenGraph & Meta", "OG tags and meta description", 7, "low", "pass", `All ${passed} key meta tags present.`);
4600
4598
  }
4601
4599
  function checkSitemap(ctx) {
4602
- if (!ctx.sitemapXml) return make("sitemap", "sitemap.xml", "Sitemap exists and is valid", 10, "medium", "established", "fail", "No sitemap.xml found.");
4600
+ if (!ctx.sitemapXml) return make("sitemap", "sitemap.xml", "Sitemap exists and is valid", 5, "medium", "fail", "No sitemap.xml found.");
4603
4601
  const count = Math.max((ctx.sitemapXml.match(/<url>/gi) || []).length, (ctx.sitemapXml.match(/<loc>/gi) || []).length);
4604
- if (count === 0 && /<sitemapindex/i.test(ctx.sitemapXml)) return make("sitemap", "sitemap.xml", "Sitemap exists and is valid", 10, "medium", "established", "pass", "Sitemap index found.");
4605
- if (count === 0) return make("sitemap", "sitemap.xml", "Sitemap exists and is valid", 10, "medium", "established", "warn", "sitemap.xml found but appears empty.");
4606
- return make("sitemap", "sitemap.xml", "Sitemap exists and is valid", 10, "medium", "established", "pass", `sitemap.xml found with ${count} URL(s).`);
4602
+ if (count === 0 && /<sitemapindex/i.test(ctx.sitemapXml)) return make("sitemap", "sitemap.xml", "Sitemap exists and is valid", 5, "medium", "pass", "Sitemap index found.");
4603
+ if (count === 0) return make("sitemap", "sitemap.xml", "Sitemap exists and is valid", 5, "medium", "warn", "sitemap.xml found but appears empty.");
4604
+ return make("sitemap", "sitemap.xml", "Sitemap exists and is valid", 5, "medium", "pass", `sitemap.xml found with ${count} URL(s).`);
4607
4605
  }
4608
4606
  function checkHttpSignatures(ctx) {
4609
4607
  const hasDirectory = ctx.httpSigDirectory != null;
4610
4608
  const hasSignatureHeader = ctx.headers["signature"] != null || ctx.headers["signature-input"] != null;
4611
4609
  const hasSignatureAgent = ctx.headers["signature-agent"] != null;
4612
- if (!hasDirectory && !hasSignatureHeader && !hasSignatureAgent) return make("http_signatures", "HTTP Message Signatures (RFC 9421)", "Agent identity verification via cryptographic signatures", 3, "medium", "emerging", "fail", "No HTTP signature support detected.");
4613
- if (hasDirectory) return make("http_signatures", "HTTP Message Signatures (RFC 9421)", "Agent identity verification via cryptographic signatures", 3, "medium", "emerging", "pass", "/.well-known/http-message-signatures-directory found.");
4614
- return make("http_signatures", "HTTP Message Signatures (RFC 9421)", "Agent identity verification via cryptographic signatures", 3, "medium", "emerging", "pass", hasSignatureAgent ? "Signature-Agent header detected." : "Signature/Signature-Input headers detected.");
4610
+ if (!hasDirectory && !hasSignatureHeader && !hasSignatureAgent) return make("http_signatures", "HTTP Message Signatures (RFC 9421)", "Agent identity verification via cryptographic signatures", 5, "medium", "fail", "No HTTP signature support detected.");
4611
+ if (hasDirectory) return make("http_signatures", "HTTP Message Signatures (RFC 9421)", "Agent identity verification via cryptographic signatures", 5, "medium", "pass", "/.well-known/http-message-signatures-directory found.");
4612
+ return make("http_signatures", "HTTP Message Signatures (RFC 9421)", "Agent identity verification via cryptographic signatures", 5, "medium", "pass", hasSignatureAgent ? "Signature-Agent header detected." : "Signature/Signature-Input headers detected.");
4615
4613
  }
4616
4614
  function checkPageSpeed(ctx) {
4617
- if (ctx.loadTimeMs == null) return make("page_speed", "Page Load Time", "Response time for AI agent interactions", 8, "low", "established", "info", "Could not measure.");
4618
- if (ctx.loadTimeMs > 5e3) return make("page_speed", "Page Load Time", "Response time for AI agent interactions", 8, "low", "established", "fail", `${(ctx.loadTimeMs / 1e3).toFixed(1)}s \u2014 agents may time out.`);
4619
- if (ctx.loadTimeMs > 2e3) return make("page_speed", "Page Load Time", "Response time for AI agent interactions", 8, "low", "established", "warn", `${(ctx.loadTimeMs / 1e3).toFixed(1)}s \u2014 consider optimizing.`);
4620
- return make("page_speed", "Page Load Time", "Response time for AI agent interactions", 8, "low", "established", "pass", `${(ctx.loadTimeMs / 1e3).toFixed(1)}s.`);
4615
+ if (ctx.loadTimeMs == null) return make("page_speed", "Page Load Time", "Response time for AI agent interactions", 5, "low", "info", "Could not measure.");
4616
+ if (ctx.loadTimeMs > 5e3) return make("page_speed", "Page Load Time", "Response time for AI agent interactions", 5, "low", "fail", `${(ctx.loadTimeMs / 1e3).toFixed(1)}s \u2014 agents may time out.`);
4617
+ if (ctx.loadTimeMs > 2e3) return make("page_speed", "Page Load Time", "Response time for AI agent interactions", 5, "low", "warn", `${(ctx.loadTimeMs / 1e3).toFixed(1)}s \u2014 consider optimizing.`);
4618
+ return make("page_speed", "Page Load Time", "Response time for AI agent interactions", 5, "low", "pass", `${(ctx.loadTimeMs / 1e3).toFixed(1)}s.`);
4621
4619
  }
4622
4620
  var allChecks = [
4623
4621
  checkRobotsTxt,
@@ -4656,29 +4654,6 @@ function isPrivateHost(hostname) {
4656
4654
  if (PRIVATE_HOSTNAMES.has(hostname.toLowerCase())) return true;
4657
4655
  const h = hostname.replace(/^\[|\]$/g, "").toLowerCase();
4658
4656
  if (h === "::1" || h.startsWith("fe80:") || h.startsWith("fc") || h.startsWith("fd") || h === "::") return true;
4659
- const v4mapped = h.match(/^(?:::ffff:)(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
4660
- if (v4mapped) {
4661
- const [, a, b] = v4mapped.map(Number);
4662
- if (a === 10 || a === 127 || a === 0) return true;
4663
- if (a === 172 && b >= 16 && b <= 31) return true;
4664
- if (a === 192 && b === 168) return true;
4665
- if (a === 169 && b === 254) return true;
4666
- return false;
4667
- }
4668
- if (/^(0[xX][0-9a-fA-F]+|0[0-7]+)(\.|$)/.test(hostname)) return true;
4669
- if (/^\d+$/.test(hostname)) {
4670
- const dec = parseInt(hostname, 10);
4671
- if (dec >= 0 && dec <= 4294967295) {
4672
- const a = dec >>> 24 & 255;
4673
- const b = dec >>> 16 & 255;
4674
- if (a === 10 || a === 127 || a === 0) return true;
4675
- if (a === 172 && b >= 16 && b <= 31) return true;
4676
- if (a === 192 && b === 168) return true;
4677
- if (a === 169 && b === 254) return true;
4678
- if (a === 100 && b >= 64 && b <= 127) return true;
4679
- if (a === 198 && (b === 18 || b === 19)) return true;
4680
- }
4681
- }
4682
4657
  const ipv4 = hostname.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
4683
4658
  if (ipv4) {
4684
4659
  const [, a, b] = ipv4.map(Number);
@@ -4691,49 +4666,22 @@ function isPrivateHost(hostname) {
4691
4666
  }
4692
4667
  return false;
4693
4668
  }
4694
- function isAllowedProtocol(url) {
4695
- try {
4696
- const p = new URL(url).protocol;
4697
- return p === "https:" || p === "http:";
4698
- } catch {
4699
- return false;
4700
- }
4701
- }
4702
4669
  function validateRedirectUrl(responseUrl) {
4703
4670
  try {
4704
4671
  const parsed = new URL(responseUrl);
4705
- if (!isAllowedProtocol(responseUrl)) return false;
4706
4672
  return !isPrivateHost(parsed.hostname);
4707
4673
  } catch {
4708
4674
  return false;
4709
4675
  }
4710
4676
  }
4711
- var MAX_REDIRECTS = 5;
4712
- async function safeFetch(url, timeout, signal) {
4713
- let current = url;
4714
- for (let i = 0; i < MAX_REDIRECTS; i++) {
4715
- const r = await fetch(current, { signal, headers: { "User-Agent": "Prodlint-WebScanner/1.0" }, redirect: "manual" });
4716
- const status = r.status;
4717
- if (status >= 300 && status < 400) {
4718
- const location = r.headers.get("location");
4719
- if (!location) return null;
4720
- const resolved = new URL(location, current).toString();
4721
- if (!validateRedirectUrl(resolved)) return null;
4722
- current = resolved;
4723
- continue;
4724
- }
4725
- return r;
4726
- }
4727
- return null;
4728
- }
4729
4677
  async function fetchText(url, timeout = 8e3) {
4730
4678
  try {
4731
4679
  const c = new AbortController();
4732
4680
  const t = setTimeout(() => c.abort(), timeout);
4733
- const r = await safeFetch(url, timeout, c.signal);
4681
+ const r = await fetch(url, { signal: c.signal, headers: { "User-Agent": "Prodlint-WebScanner/1.0" }, redirect: "follow" });
4734
4682
  clearTimeout(t);
4735
- if (!r || !r.ok) return null;
4736
- return await r.text();
4683
+ if (r.url && !validateRedirectUrl(r.url)) return null;
4684
+ return r.ok ? await r.text() : null;
4737
4685
  } catch {
4738
4686
  return null;
4739
4687
  }
@@ -4742,9 +4690,9 @@ async function fetchHeaders(url, timeout = 8e3) {
4742
4690
  try {
4743
4691
  const c = new AbortController();
4744
4692
  const t = setTimeout(() => c.abort(), timeout);
4745
- const r = await safeFetch(url, timeout, c.signal);
4693
+ const r = await fetch(url, { signal: c.signal, headers: { "User-Agent": "Prodlint-WebScanner/1.0" }, redirect: "follow" });
4746
4694
  clearTimeout(t);
4747
- if (!r) return {};
4695
+ if (r.url && !validateRedirectUrl(r.url)) return {};
4748
4696
  const h = {};
4749
4697
  r.headers.forEach((v, k) => {
4750
4698
  h[k.toLowerCase()] = v;
@@ -4759,18 +4707,20 @@ async function fetchWithTiming(url, timeout = 15e3) {
4759
4707
  const c = new AbortController();
4760
4708
  const t = setTimeout(() => c.abort(), timeout);
4761
4709
  const start = Date.now();
4762
- const r = await safeFetch(url, timeout, c.signal);
4763
- clearTimeout(t);
4764
- if (!r || !r.ok) return { html: null, loadTimeMs: null };
4710
+ const r = await fetch(url, { signal: c.signal, headers: { "User-Agent": "Prodlint-WebScanner/1.0" }, redirect: "follow" });
4765
4711
  const html = await r.text();
4766
- return { html, loadTimeMs: Date.now() - start };
4712
+ clearTimeout(t);
4713
+ if (r.url && !validateRedirectUrl(r.url)) return { html: null, loadTimeMs: null };
4714
+ return r.ok ? { html, loadTimeMs: Date.now() - start } : { html: null, loadTimeMs: null };
4767
4715
  } catch {
4768
4716
  return { html: null, loadTimeMs: null };
4769
4717
  }
4770
4718
  }
4771
4719
  function normalizeUrl(input) {
4772
4720
  let url = input.trim();
4773
- if (!/^https?:\/\//i.test(url)) url = `https://${url}`;
4721
+ if (!/^https?:\/\//i.test(url)) {
4722
+ url = `https://${url}`;
4723
+ }
4774
4724
  const parsed = new URL(url);
4775
4725
  return parsed.origin;
4776
4726
  }
@@ -4861,9 +4811,9 @@ server.tool(
4861
4811
  if (result.findings.length > 0) {
4862
4812
  summary.push("", "### Findings");
4863
4813
  for (const f of result.findings.slice(0, 30)) {
4864
- summary.push(
4865
- `- **[${f.severity}]** \`${f.ruleId}\` ${f.file}:${f.line} \u2014 ${f.message}`
4866
- );
4814
+ const line = `- **[${f.severity}]** \`${f.ruleId}\` ${f.file}:${f.line} \u2014 ${f.message}`;
4815
+ summary.push(f.fix ? `${line}
4816
+ - _Fix: ${f.fix}_` : line);
4867
4817
  }
4868
4818
  if (result.findings.length > 30) {
4869
4819
  summary.push(`- ...and ${result.findings.length - 30} more findings`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prodlint",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "description": "Production readiness for vibe-coded apps — know your AI code is ready to ship",
5
5
  "license": "MIT",
6
6
  "author": "prodlint contributors",
@@ -40,22 +40,26 @@
40
40
  "test": "vitest run",
41
41
  "test:watch": "vitest",
42
42
  "lint": "node dist/cli.js .",
43
+ "audit:supply-chain": "node scripts/supply-chain-audit.cjs",
43
44
  "prepublishOnly": "npm run build"
44
45
  },
45
46
  "dependencies": {
46
- "@babel/parser": "^7.29.0",
47
- "@babel/types": "^7.29.0",
48
- "@modelcontextprotocol/sdk": "^1.26.0",
49
- "@rollup/rollup-win32-x64-msvc": "^4.59.0",
47
+ "@babel/parser": "^7.29.7",
48
+ "@babel/types": "^7.29.7",
49
+ "@modelcontextprotocol/sdk": "^1.29.0",
50
50
  "fast-glob": "^3.3.3",
51
51
  "picocolors": "^1.1.1",
52
- "zod": "^4.3.6"
52
+ "zod": "^4.4.3"
53
53
  },
54
54
  "devDependencies": {
55
- "@types/node": "^25.2.3",
55
+ "@rollup/rollup-win32-x64-msvc": "^4.62.2",
56
+ "@types/node": "^26.0.1",
56
57
  "tsup": "^8.4.0",
57
- "typescript": "^5.7.0",
58
- "vitest": "^3.0.0"
58
+ "typescript": "^5.9.3",
59
+ "vitest": "^3.2.6"
60
+ },
61
+ "overrides": {
62
+ "esbuild": "^0.28.1"
59
63
  },
60
64
  "engines": {
61
65
  "node": ">=18"