prodlint 0.7.2 → 0.8.1

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
@@ -715,7 +715,8 @@ var SECRET_PATTERNS = [
715
715
  { name: "AWS access key", pattern: /AKIA[0-9A-Z]{16}/ },
716
716
  { name: "AWS secret key", pattern: /(?:aws_secret_access_key|AWS_SECRET)\s*[=:]\s*['"]?[A-Za-z0-9/+=]{40}['"]?/ },
717
717
  { name: "Supabase service role key", pattern: /eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\.[A-Za-z0-9_-]{50,}\.[A-Za-z0-9_-]{20,}/ },
718
- { name: "OpenAI API key", pattern: /sk-[a-zA-Z0-9]{20,}T3BlbkFJ[a-zA-Z0-9]{20,}/ },
718
+ { name: "OpenAI API key (legacy)", pattern: /sk-[a-zA-Z0-9]{20,}T3BlbkFJ[a-zA-Z0-9]{20,}/ },
719
+ { name: "OpenAI API key", pattern: /sk-proj-[a-zA-Z0-9_\-]{20,}/ },
719
720
  { name: "GitHub token", pattern: /gh[ps]_[A-Za-z0-9_]{36,}/ },
720
721
  { name: "GitHub fine-grained token", pattern: /github_pat_[A-Za-z0-9_]{22,}/ },
721
722
  { name: "Generic API key assignment", pattern: /(?:api_key|apikey|api_secret|secret_key|private_key)\s*[=:]\s*['"][a-zA-Z0-9_\-]{20,}['"]/ },
@@ -2254,7 +2255,7 @@ var codebaseConsistencyRule = {
2254
2255
  // src/rules/dead-exports.ts
2255
2256
  function isEntryPoint(relativePath) {
2256
2257
  const name = relativePath.split("/").pop() ?? "";
2257
- return /^(page|layout|loading|error|not-found|route|middleware|instrumentation)\.(tsx?|jsx?)$/.test(name) || /^index\.(tsx?|jsx?)$/.test(name) || name === "global-error.tsx" || name === "global-error.jsx";
2258
+ return /^(page|layout|loading|error|not-found|route|middleware|instrumentation|opengraph-image|twitter-image|icon|apple-icon|sitemap|robots|manifest)\.(tsx?|jsx?)$/.test(name) || /^index\.(tsx?|jsx?)$/.test(name) || name === "global-error.tsx" || name === "global-error.jsx";
2258
2259
  }
2259
2260
  var THRESHOLD = 5;
2260
2261
  var deadExportsRule = {
@@ -2277,8 +2278,31 @@ var deadExportsRule = {
2277
2278
  const importedFiles = /* @__PURE__ */ new Set();
2278
2279
  for (const file of sourceFiles) {
2279
2280
  if (isEntryPoint(file.relativePath)) continue;
2281
+ let inTemplateLiteral = false;
2280
2282
  for (let i = 0; i < file.lines.length; i++) {
2281
2283
  const line = file.lines[i];
2284
+ let backtickCount = 0;
2285
+ for (let j = 0; j < line.length; j++) {
2286
+ if (line[j] === "\\") {
2287
+ j++;
2288
+ continue;
2289
+ }
2290
+ if (line[j] === "`") backtickCount++;
2291
+ }
2292
+ if (backtickCount % 2 === 1) inTemplateLiteral = !inTemplateLiteral;
2293
+ if (inTemplateLiteral && backtickCount % 2 === 0) continue;
2294
+ const exportIdx = line.indexOf("export");
2295
+ if (exportIdx >= 0) {
2296
+ let inStr = false;
2297
+ for (let j = 0; j < exportIdx; j++) {
2298
+ if (line[j] === "\\") {
2299
+ j++;
2300
+ continue;
2301
+ }
2302
+ if (line[j] === "'" || line[j] === '"' || line[j] === "`") inStr = !inStr;
2303
+ }
2304
+ if (inStr) continue;
2305
+ }
2282
2306
  let match;
2283
2307
  const namedRe = /export\s+(?:async\s+)?(?:function|const|let|class|enum)\s+(\w+)/g;
2284
2308
  while ((match = namedRe.exec(line)) !== null) {
@@ -3847,6 +3871,8 @@ var hydrationMismatchRule = {
3847
3871
  if (isClientComponent(file.content)) return [];
3848
3872
  if (!/(?:^|\/)(?:src\/)?app\//.test(file.relativePath)) return [];
3849
3873
  if (/route\.[jt]sx?$/.test(file.relativePath)) return [];
3874
+ if (/(?:^|\/)(?:src\/)?app\/(?:lib|utils|helpers|server|actions)\//.test(file.relativePath)) return [];
3875
+ if (/\.[jt]s$/.test(file.relativePath) && !/<[A-Z]/.test(file.content)) return [];
3850
3876
  const findings = [];
3851
3877
  let useEffectRanges = [];
3852
3878
  if (file.ast) {
@@ -4385,8 +4411,249 @@ async function scan(options) {
4385
4411
  summary
4386
4412
  };
4387
4413
  }
4414
+
4415
+ // src/web-scanner/checks.ts
4416
+ function make(id, name, description, maxPoints, severity, status, details) {
4417
+ return {
4418
+ id,
4419
+ name,
4420
+ description,
4421
+ status,
4422
+ severity,
4423
+ details,
4424
+ points: status === "pass" ? maxPoints : status === "warn" ? Math.floor(maxPoints / 2) : 0,
4425
+ maxPoints
4426
+ };
4427
+ }
4428
+ function checkRobotsTxt(ctx) {
4429
+ if (!ctx.robotsTxt) return make("robots_txt", "robots.txt", "robots.txt exists and is accessible", 5, "medium", "fail", "No robots.txt found.");
4430
+ return make("robots_txt", "robots.txt", "robots.txt exists and is accessible", 5, "medium", "pass", "robots.txt found.");
4431
+ }
4432
+ function checkRobotsAiDirectives(ctx) {
4433
+ 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.");
4434
+ const agents = ["GPTBot", "ChatGPT-User", "ClaudeBot", "Claude-Web", "Google-Extended", "PerplexityBot", "Bytespider", "CCBot", "anthropic-ai", "cohere-ai"];
4435
+ const found = agents.filter((a) => ctx.robotsTxt.toLowerCase().includes(a.toLowerCase()));
4436
+ 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.");
4437
+ 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(", ")}.`);
4438
+ 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(", ")}.`);
4439
+ }
4440
+ function checkContentUsage(ctx) {
4441
+ const hasHeader = ctx.headers["content-usage"] != null;
4442
+ const hasInRobots = ctx.robotsTxt?.toLowerCase().includes("content-usage") ?? false;
4443
+ if (!hasHeader && !hasInRobots) return make("content_usage", "Content-Usage (IETF aipref)", "Content-Usage header or directives", 10, "high", "fail", "No Content-Usage directives found.");
4444
+ 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.");
4445
+ }
4446
+ function checkLlmsTxt(ctx) {
4447
+ if (!ctx.llmsTxt) return make("llms_txt", "llms.txt", "LLM-optimized site summary at /llms.txt", 10, "high", "fail", "No llms.txt found.");
4448
+ const lines = ctx.llmsTxt.split("\n").filter((l) => l.trim().length > 0);
4449
+ 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.");
4450
+ return make("llms_txt", "llms.txt", "LLM-optimized site summary at /llms.txt", 10, "high", "pass", `llms.txt found with ${lines.length} lines.`);
4451
+ }
4452
+ function checkTdmRep(ctx) {
4453
+ const hasWK = ctx.tdmRep != null;
4454
+ const hasHeader = ctx.headers["tdm-reservation"] != null;
4455
+ if (!hasWK && !hasHeader) return make("tdmrep", "TDMRep (W3C)", "Text & data mining reservation file or header", 8, "medium", "fail", "No TDMRep configuration found.");
4456
+ 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"]}`);
4457
+ }
4458
+ function checkAiDisclosure(ctx) {
4459
+ 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.");
4460
+ return make("ai_disclosure", "AI-Disclosure Header", "Declares AI involvement in content generation", 5, "low", "pass", `Header: ${ctx.headers["ai-disclosure"]}`);
4461
+ }
4462
+ function checkAgentCard(ctx) {
4463
+ if (!ctx.agentCard) return make("agent_card", "A2A AgentCard", "/.well-known/agent-card.json for agent discovery", 10, "high", "fail", "No A2A AgentCard found.");
4464
+ try {
4465
+ const card = JSON.parse(ctx.agentCard);
4466
+ 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.");
4467
+ 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).`);
4468
+ } catch {
4469
+ return make("agent_card", "A2A AgentCard", "/.well-known/agent-card.json for agent discovery", 10, "high", "warn", "AgentCard found but contains invalid JSON.");
4470
+ }
4471
+ }
4472
+ function checkAiTxt(ctx) {
4473
+ 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.");
4474
+ const lines = ctx.aiTxt.split("\n").filter((l) => l.trim().length > 0 && !l.trim().startsWith("#"));
4475
+ 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.");
4476
+ return make("ai_txt", "ai.txt", "AI training permissions per Spawning spec", 5, "medium", "pass", `ai.txt found with ${lines.length} directive(s).`);
4477
+ }
4478
+ function checkWebMCP(ctx) {
4479
+ if (!ctx.html) return make("webmcp", "WebMCP Tools", "Chrome 146+ WebMCP tool registration", 10, "high", "info", "Could not check for WebMCP tools.");
4480
+ const hasToolname = /toolname=/i.test(ctx.html);
4481
+ const hasModelContext = /navigator\.modelContext/i.test(ctx.html) || /registerTool/i.test(ctx.html);
4482
+ if (!hasToolname && !hasModelContext) return make("webmcp", "WebMCP Tools", "Chrome 146+ WebMCP tool registration", 10, "high", "fail", "No WebMCP tools detected.");
4483
+ const count = (ctx.html.match(/toolname=/gi) || []).length;
4484
+ return make("webmcp", "WebMCP Tools", "Chrome 146+ WebMCP tool registration", 10, "high", "pass", hasToolname ? `${count} form(s) with toolname attributes.` : "registerTool() usage detected.");
4485
+ }
4486
+ function checkStructuredData(ctx) {
4487
+ if (!ctx.html) return make("structured_data", "Structured Data", "JSON-LD or Schema.org markup", 10, "medium", "info", "Could not fetch page HTML.");
4488
+ const jsonLd = ctx.html.match(/<script[^>]*type=["']application\/ld\+json["'][^>]*>/gi);
4489
+ const hasMicrodata = ctx.html.includes("itemscope") && ctx.html.includes("itemtype");
4490
+ if (!jsonLd && !hasMicrodata) return make("structured_data", "Structured Data", "JSON-LD or Schema.org markup", 10, "medium", "fail", "No structured data found.");
4491
+ 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).`);
4492
+ }
4493
+ function checkOpenGraph(ctx) {
4494
+ if (!ctx.html) return make("opengraph", "OpenGraph & Meta", "OG tags and meta description", 7, "low", "info", "Could not fetch HTML.");
4495
+ 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)];
4496
+ const passed = checks.filter(Boolean).length;
4497
+ if (passed === 0) return make("opengraph", "OpenGraph & Meta", "OG tags and meta description", 7, "low", "fail", "No OpenGraph tags or meta description.");
4498
+ if (passed < 3) return make("opengraph", "OpenGraph & Meta", "OG tags and meta description", 7, "low", "warn", `Found ${passed}/4 meta tags.`);
4499
+ return make("opengraph", "OpenGraph & Meta", "OG tags and meta description", 7, "low", "pass", `All ${passed} key meta tags present.`);
4500
+ }
4501
+ function checkSitemap(ctx) {
4502
+ if (!ctx.sitemapXml) return make("sitemap", "sitemap.xml", "Sitemap exists and is valid", 5, "medium", "fail", "No sitemap.xml found.");
4503
+ const count = Math.max((ctx.sitemapXml.match(/<url>/gi) || []).length, (ctx.sitemapXml.match(/<loc>/gi) || []).length);
4504
+ if (count === 0 && /<sitemapindex/i.test(ctx.sitemapXml)) return make("sitemap", "sitemap.xml", "Sitemap exists and is valid", 5, "medium", "pass", "Sitemap index found.");
4505
+ if (count === 0) return make("sitemap", "sitemap.xml", "Sitemap exists and is valid", 5, "medium", "warn", "sitemap.xml found but appears empty.");
4506
+ return make("sitemap", "sitemap.xml", "Sitemap exists and is valid", 5, "medium", "pass", `sitemap.xml found with ${count} URL(s).`);
4507
+ }
4508
+ function checkHttpSignatures(ctx) {
4509
+ const hasDirectory = ctx.httpSigDirectory != null;
4510
+ const hasSignatureHeader = ctx.headers["signature"] != null || ctx.headers["signature-input"] != null;
4511
+ const hasSignatureAgent = ctx.headers["signature-agent"] != null;
4512
+ 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.");
4513
+ 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.");
4514
+ 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.");
4515
+ }
4516
+ function checkPageSpeed(ctx) {
4517
+ if (ctx.loadTimeMs == null) return make("page_speed", "Page Load Time", "Response time for AI agent interactions", 5, "low", "info", "Could not measure.");
4518
+ 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.`);
4519
+ 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.`);
4520
+ return make("page_speed", "Page Load Time", "Response time for AI agent interactions", 5, "low", "pass", `${(ctx.loadTimeMs / 1e3).toFixed(1)}s.`);
4521
+ }
4522
+ var allChecks = [
4523
+ checkRobotsTxt,
4524
+ checkRobotsAiDirectives,
4525
+ checkContentUsage,
4526
+ checkLlmsTxt,
4527
+ checkAiTxt,
4528
+ checkTdmRep,
4529
+ checkAiDisclosure,
4530
+ checkAgentCard,
4531
+ checkWebMCP,
4532
+ checkHttpSignatures,
4533
+ checkStructuredData,
4534
+ checkOpenGraph,
4535
+ checkSitemap,
4536
+ checkPageSpeed
4537
+ ];
4538
+
4539
+ // src/web-scanner/index.ts
4540
+ function getGrade(score) {
4541
+ if (score >= 80) return "A";
4542
+ if (score >= 60) return "B";
4543
+ if (score >= 40) return "C";
4544
+ if (score >= 20) return "D";
4545
+ return "F";
4546
+ }
4547
+ function getDomain(url) {
4548
+ try {
4549
+ return new URL(url).hostname;
4550
+ } catch {
4551
+ return url;
4552
+ }
4553
+ }
4554
+ var PRIVATE_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "0.0.0.0", "[::1]", "metadata.google.internal"]);
4555
+ function isPrivateHost(hostname) {
4556
+ if (PRIVATE_HOSTNAMES.has(hostname.toLowerCase())) return true;
4557
+ const h = hostname.replace(/^\[|\]$/g, "").toLowerCase();
4558
+ if (h === "::1" || h.startsWith("fe80:") || h.startsWith("fc") || h.startsWith("fd") || h === "::") return true;
4559
+ const ipv4 = hostname.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
4560
+ if (ipv4) {
4561
+ const [, a, b] = ipv4.map(Number);
4562
+ if (a === 10 || a === 127 || a === 0) return true;
4563
+ if (a === 172 && b >= 16 && b <= 31) return true;
4564
+ if (a === 192 && b === 168) return true;
4565
+ if (a === 169 && b === 254) return true;
4566
+ if (a === 100 && b >= 64 && b <= 127) return true;
4567
+ if (a === 198 && (b === 18 || b === 19)) return true;
4568
+ }
4569
+ return false;
4570
+ }
4571
+ function validateRedirectUrl(responseUrl) {
4572
+ try {
4573
+ const parsed = new URL(responseUrl);
4574
+ return !isPrivateHost(parsed.hostname);
4575
+ } catch {
4576
+ return false;
4577
+ }
4578
+ }
4579
+ async function fetchText(url, timeout = 8e3) {
4580
+ try {
4581
+ const c = new AbortController();
4582
+ const t = setTimeout(() => c.abort(), timeout);
4583
+ const r = await fetch(url, { signal: c.signal, headers: { "User-Agent": "Prodlint-WebScanner/1.0" }, redirect: "follow" });
4584
+ clearTimeout(t);
4585
+ if (r.url && !validateRedirectUrl(r.url)) return null;
4586
+ return r.ok ? await r.text() : null;
4587
+ } catch {
4588
+ return null;
4589
+ }
4590
+ }
4591
+ async function fetchHeaders(url, timeout = 8e3) {
4592
+ try {
4593
+ const c = new AbortController();
4594
+ const t = setTimeout(() => c.abort(), timeout);
4595
+ const r = await fetch(url, { signal: c.signal, headers: { "User-Agent": "Prodlint-WebScanner/1.0" }, redirect: "follow" });
4596
+ clearTimeout(t);
4597
+ if (r.url && !validateRedirectUrl(r.url)) return {};
4598
+ const h = {};
4599
+ r.headers.forEach((v, k) => {
4600
+ h[k.toLowerCase()] = v;
4601
+ });
4602
+ return h;
4603
+ } catch {
4604
+ return {};
4605
+ }
4606
+ }
4607
+ async function fetchWithTiming(url, timeout = 15e3) {
4608
+ try {
4609
+ const c = new AbortController();
4610
+ const t = setTimeout(() => c.abort(), timeout);
4611
+ const start = Date.now();
4612
+ const r = await fetch(url, { signal: c.signal, headers: { "User-Agent": "Prodlint-WebScanner/1.0" }, redirect: "follow" });
4613
+ const html = await r.text();
4614
+ clearTimeout(t);
4615
+ if (r.url && !validateRedirectUrl(r.url)) return { html: null, loadTimeMs: null };
4616
+ return r.ok ? { html, loadTimeMs: Date.now() - start } : { html: null, loadTimeMs: null };
4617
+ } catch {
4618
+ return { html: null, loadTimeMs: null };
4619
+ }
4620
+ }
4621
+ async function runWebScan(targetUrl) {
4622
+ const domain = getDomain(targetUrl);
4623
+ const [robotsTxt, llmsTxt, aiTxt, tdmRep, agentCard, sitemapXml, httpSigDirectory, headers, pageData] = await Promise.all([
4624
+ fetchText(`${targetUrl}/robots.txt`),
4625
+ fetchText(`${targetUrl}/llms.txt`),
4626
+ fetchText(`${targetUrl}/ai.txt`),
4627
+ fetchText(`${targetUrl}/.well-known/tdmrep.json`),
4628
+ fetchText(`${targetUrl}/.well-known/agent-card.json`),
4629
+ fetchText(`${targetUrl}/sitemap.xml`),
4630
+ fetchText(`${targetUrl}/.well-known/http-message-signatures-directory`),
4631
+ fetchHeaders(targetUrl),
4632
+ fetchWithTiming(targetUrl)
4633
+ ]);
4634
+ const ctx = { url: targetUrl, domain, robotsTxt, llmsTxt, aiTxt, tdmRep, agentCard, sitemapXml, httpSigDirectory, headers, html: pageData.html, loadTimeMs: pageData.loadTimeMs };
4635
+ const checks = allChecks.map((fn) => fn(ctx));
4636
+ const totalPoints = checks.reduce((s, c) => s + c.points, 0);
4637
+ const maxPoints = checks.reduce((s, c) => s + c.maxPoints, 0);
4638
+ const overallScore = maxPoints > 0 ? Math.round(totalPoints / maxPoints * 100) : 0;
4639
+ return {
4640
+ url: targetUrl,
4641
+ domain,
4642
+ scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
4643
+ overallScore,
4644
+ grade: getGrade(overallScore),
4645
+ checks,
4646
+ summary: {
4647
+ passed: checks.filter((c) => c.status === "pass").length,
4648
+ failed: checks.filter((c) => c.status === "fail").length,
4649
+ warnings: checks.filter((c) => c.status === "warn").length,
4650
+ totalChecks: checks.length
4651
+ }
4652
+ };
4653
+ }
4388
4654
  export {
4389
4655
  rules,
4656
+ runWebScan,
4390
4657
  scan
4391
4658
  };
4392
4659
  //# sourceMappingURL=index.js.map