prodlint 0.9.1 → 0.9.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/README.md +38 -6
- package/dist/cli.js +237 -119
- package/dist/index.d.ts +13 -7
- package/dist/index.js +252 -107
- package/dist/mcp.js +61 -111
- package/package.json +1 -1
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,
|
|
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",
|
|
4529
|
-
return make("robots_txt", "robots.txt", "robots.txt exists and is accessible",
|
|
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", "
|
|
4533
|
-
const agents = ["GPTBot", "ChatGPT-User", "ClaudeBot", "Claude-Web", "Google-Extended", "PerplexityBot", "Bytespider", "CCBot", "anthropic-ai", "cohere-ai"
|
|
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", "
|
|
4536
|
-
if (found.length < 3) return make("robots_ai_directives", "AI robots.txt Directives", "AI bot user-agents in robots.txt", 15, "critical", "
|
|
4537
|
-
return make("robots_ai_directives", "AI robots.txt Directives", "AI bot user-agents in robots.txt", 15, "critical", "
|
|
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",
|
|
4543
|
-
return make("content_usage", "Content-Usage (IETF aipref)", "Content-Usage header or directives",
|
|
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",
|
|
4547
|
-
const lines = ctx.llmsTxt.split("\n").filter((l) => l.trim().length > 0
|
|
4548
|
-
if (lines.length < 3) return make("llms_txt", "llms.txt", "LLM-optimized site summary at /llms.txt",
|
|
4549
|
-
return make("llms_txt", "llms.txt", "LLM-optimized site summary at /llms.txt",
|
|
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",
|
|
4555
|
-
return make("tdmrep", "TDMRep (W3C)", "Text & data mining reservation file or header",
|
|
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",
|
|
4559
|
-
return make("ai_disclosure", "AI-Disclosure Header", "Declares AI involvement in content generation",
|
|
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",
|
|
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",
|
|
4566
|
-
return make("agent_card", "A2A AgentCard", "/.well-known/agent-card.json for agent discovery",
|
|
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",
|
|
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",
|
|
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",
|
|
4575
|
-
return make("ai_txt", "ai.txt", "AI training permissions per Spawning spec",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
4590
|
-
return make("structured_data", "Structured Data", "JSON-LD or Schema.org markup",
|
|
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",
|
|
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",
|
|
4597
|
-
if (passed < 3) return make("opengraph", "OpenGraph & Meta", "OG tags and meta description",
|
|
4598
|
-
|
|
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",
|
|
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",
|
|
4605
|
-
if (count === 0) return make("sitemap", "sitemap.xml", "Sitemap exists and is valid",
|
|
4606
|
-
return make("sitemap", "sitemap.xml", "Sitemap exists and is valid",
|
|
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",
|
|
4613
|
-
if (hasDirectory) return make("http_signatures", "HTTP Message Signatures (RFC 9421)", "Agent identity verification via cryptographic signatures",
|
|
4614
|
-
return make("http_signatures", "HTTP Message Signatures (RFC 9421)", "Agent identity verification via cryptographic signatures",
|
|
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",
|
|
4618
|
-
if (ctx.loadTimeMs > 5e3) return make("page_speed", "Page Load Time", "Response time for AI agent interactions",
|
|
4619
|
-
if (ctx.loadTimeMs > 2e3) return make("page_speed", "Page Load Time", "Response time for AI agent interactions",
|
|
4620
|
-
return make("page_speed", "Page Load Time", "Response time for AI agent interactions",
|
|
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
|
|
4681
|
+
const r = await fetch(url, { signal: c.signal, headers: { "User-Agent": "Prodlint-WebScanner/1.0" }, redirect: "follow" });
|
|
4734
4682
|
clearTimeout(t);
|
|
4735
|
-
if (
|
|
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
|
|
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
|
|
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
|
-
|
|
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))
|
|
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
|
-
|
|
4865
|
-
|
|
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`);
|