prodlint 0.10.0 → 0.10.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/index.js CHANGED
@@ -2481,15 +2481,16 @@ var deadExportsRule = {
2481
2481
  };
2482
2482
 
2483
2483
  // src/rules/shallow-catch.ts
2484
- function scoreCatchBody(bodyLines) {
2485
- if (bodyLines.length === 0 || bodyLines.every((l) => l.trim() === "")) {
2486
- return { score: 0, label: "empty catch" };
2487
- }
2488
- const body = bodyLines.join("\n");
2484
+ function reportsError(value, param) {
2485
+ if (/error/i.test(value) || /\berr|Err|status|Response|json/.test(value)) return true;
2486
+ return param !== void 0 && new RegExp(`(?<![\\w$])${param.replace(/\$/g, "\\$")}(?![\\w$])`).test(value);
2487
+ }
2488
+ function scoreCatchBody({ text: body, empty, returned, param }) {
2489
+ if (empty) return { score: 0, label: "empty catch" };
2489
2490
  let score = 0;
2490
2491
  if (/console\.(log|warn|error|info)\s*\(/.test(body)) score = 1;
2491
2492
  if ((/console\.(error|warn)\s*\(/.test(body) || /\b(logger|log)\.(error|warn)\s*\(/.test(body)) && /\b(err|error|e)\b/.test(body)) score = 2;
2492
- if (/\bthrow\b/.test(body) || /\breturn\b.*(?:error|err|status|Response|NextResponse|json)/.test(body) || /\bset\w*Error\s*\(/.test(body) || /\bres\.\w+\s*\(/.test(body) || // Toast notifications (react-toastify, sonner, shadcn)
2493
+ if (/\bthrow\b/.test(body) || returned.some((value) => reportsError(value, param)) || /\bset\w*Error\s*\(/.test(body) || /\bres\.\w+\s*\(/.test(body) || // Toast notifications (react-toastify, sonner, shadcn)
2493
2494
  /\btoast\.(error|warn|warning)\s*\(/.test(body) || /\btoast\s*\(\s*\{/.test(body) || // Error monitoring (Sentry, etc.)
2494
2495
  /\b(Sentry\.)?captureException\s*\(/.test(body) || /\b(Sentry\.)?captureMessage\s*\(/.test(body) || // Structured loggers
2495
2496
  /\b(logger|log)\.(error|fatal)\s*\(/.test(body) || // Error handler utilities
@@ -2499,13 +2500,56 @@ function scoreCatchBody(bodyLines) {
2499
2500
  score = 3;
2500
2501
  }
2501
2502
  const labels = {
2502
- 0: "empty catch",
2503
+ 0: "catch swallows the error",
2503
2504
  1: "catch only logs (no recovery or propagation)",
2504
2505
  2: "catch logs error but does not propagate or recover",
2505
2506
  3: "catch handles error properly"
2506
2507
  };
2507
2508
  return { score, label: labels[score] };
2508
2509
  }
2510
+ function catchFinding(file, catchLine, body) {
2511
+ const { score, label } = scoreCatchBody(body);
2512
+ if (score > 1) return null;
2513
+ return {
2514
+ ruleId: "shallow-catch",
2515
+ file: file.relativePath,
2516
+ line: catchLine + 1,
2517
+ column: file.lines[catchLine].indexOf("catch") + 1,
2518
+ message: body.empty ? "Empty catch block \u2014 errors are silently swallowed" : score === 0 ? "Catch block swallows the error \u2014 it isn't logged, rethrown or returned" : `Decorative error handler: ${label}`,
2519
+ severity: score === 0 ? "warning" : "info",
2520
+ category: "reliability",
2521
+ fix: "Log the error with context and either re-throw, return an error response, or recover gracefully"
2522
+ };
2523
+ }
2524
+ var NON_CODE = /\/\/.*|\/\*[\s\S]*?\*\/|'(?:[^'\\\n]|\\[\s\S])*'|"(?:[^"\\\n]|\\[\s\S])*"|`(?:[^`\\]|\\[\s\S])*`/g;
2525
+ function blankOut(source, strings) {
2526
+ const blank = (s) => s.replace(/[^\n]/g, " ");
2527
+ return source.replace(NON_CODE, (m) => m[0] === "/" ? blank(m) : strings ? m[0] + blank(m.slice(1, -1)) + m[0] : m);
2528
+ }
2529
+ function closingBrace(code, open) {
2530
+ let depth = 0;
2531
+ for (let i = open; i < code.length; i++) {
2532
+ if (code[i] === "{") depth++;
2533
+ else if (code[i] === "}" && --depth === 0) return i;
2534
+ }
2535
+ return -1;
2536
+ }
2537
+ function returnedValues(code, text) {
2538
+ const values = [];
2539
+ for (const m of code.matchAll(/(?<![\w$.])return\b/g)) {
2540
+ const start = m.index + "return".length;
2541
+ let end = start;
2542
+ for (let depth = 0; end < code.length; end++) {
2543
+ const ch = code[end];
2544
+ if (ch === "(" || ch === "[" || ch === "{") depth++;
2545
+ else if (ch === ")" || ch === "]" || ch === "}") {
2546
+ if (depth-- === 0) break;
2547
+ } else if (depth === 0 && (ch === ";" || ch === "\n")) break;
2548
+ }
2549
+ values.push(text.slice(start, end));
2550
+ }
2551
+ return values;
2552
+ }
2509
2553
  var shallowCatchRule = {
2510
2554
  id: "shallow-catch",
2511
2555
  name: "Shallow Error Handler",
@@ -2522,95 +2566,45 @@ var shallowCatchRule = {
2522
2566
  walkAST(file.ast.program, (node) => {
2523
2567
  if (node.type !== "CatchClause") return;
2524
2568
  if (!node.loc) return;
2525
- const catchLine = node.loc.start.line - 1;
2569
+ const catchLine2 = node.loc.start.line - 1;
2526
2570
  const body = node.body;
2527
- if (!body || !body.loc) return;
2528
- const bodyStart = body.loc.start.line - 1;
2529
- const bodyEnd = body.loc.end.line - 1;
2530
- const bodyLines = bodyStart === bodyEnd ? [file.lines[bodyStart]] : file.lines.slice(bodyStart + 1, bodyEnd);
2531
- const { score, label } = scoreCatchBody(bodyLines);
2532
- if (score <= 1) {
2533
- findings.push({
2534
- ruleId: "shallow-catch",
2535
- file: file.relativePath,
2536
- line: catchLine + 1,
2537
- column: file.lines[catchLine].indexOf("catch") + 1,
2538
- message: score === 0 ? "Empty catch block \u2014 errors are silently swallowed" : `Decorative error handler: ${label}`,
2539
- severity: score === 0 ? "warning" : "info",
2540
- category: "reliability",
2541
- fix: "Log the error with context and either re-throw, return an error response, or recover gracefully"
2542
- });
2543
- }
2571
+ if (body?.start == null || body.end == null) return;
2572
+ const returned = [];
2573
+ walkAST(body, (n) => {
2574
+ if (n.type === "ReturnStatement" && n.argument?.start != null && n.argument.end != null) {
2575
+ returned.push(file.content.slice(n.argument.start, n.argument.end));
2576
+ }
2577
+ });
2578
+ const finding = catchFinding(file, catchLine2, {
2579
+ text: file.content.slice(body.start + 1, body.end - 1),
2580
+ empty: body.body.length === 0,
2581
+ returned,
2582
+ param: node.param?.type === "Identifier" ? node.param.name : void 0
2583
+ });
2584
+ if (finding) findings.push(finding);
2544
2585
  });
2545
2586
  return findings;
2546
2587
  } catch {
2547
2588
  }
2548
2589
  }
2549
- for (let i = 0; i < file.lines.length; i++) {
2550
- if (isCommentLine(file.lines, i, file.commentMap)) continue;
2551
- const trimmed = file.lines[i].trim();
2552
- if (!/\bcatch\s*\(/.test(trimmed) && !/\bcatch\s*\{/.test(trimmed)) continue;
2553
- let braceStart = -1;
2554
- for (let j = i; j < Math.min(i + 3, file.lines.length); j++) {
2555
- if (file.lines[j].includes("{")) {
2556
- braceStart = j;
2557
- break;
2558
- }
2559
- }
2560
- if (braceStart === -1) continue;
2561
- let depth = 0;
2562
- let bodyEnd = braceStart;
2563
- let inSingle = false;
2564
- let inDouble = false;
2565
- let inTemplate = false;
2566
- for (let j = braceStart; j < file.lines.length; j++) {
2567
- const line = file.lines[j];
2568
- const startPos = j === braceStart ? line.indexOf("{") : 0;
2569
- for (let k = startPos; k < line.length; k++) {
2570
- const ch = line[k];
2571
- const prev = k > 0 ? line[k - 1] : "";
2572
- const escaped = prev === "\\" && (k < 2 || line[k - 2] !== "\\");
2573
- if (!escaped) {
2574
- if (ch === "'" && !inDouble && !inTemplate) {
2575
- inSingle = !inSingle;
2576
- continue;
2577
- }
2578
- if (ch === '"' && !inSingle && !inTemplate) {
2579
- inDouble = !inDouble;
2580
- continue;
2581
- }
2582
- if (ch === "`" && !inSingle && !inDouble) {
2583
- inTemplate = !inTemplate;
2584
- continue;
2585
- }
2586
- }
2587
- if (inSingle || inDouble || inTemplate) continue;
2588
- if (ch === "{") depth++;
2589
- if (ch === "}") {
2590
- depth--;
2591
- if (depth === 0) {
2592
- bodyEnd = j;
2593
- break;
2594
- }
2595
- }
2596
- }
2597
- if (depth === 0) break;
2598
- }
2599
- const bodyLines = file.lines.slice(braceStart + 1, bodyEnd);
2600
- const { score, label } = scoreCatchBody(bodyLines);
2601
- if (score <= 1) {
2602
- findings.push({
2603
- ruleId: "shallow-catch",
2604
- file: file.relativePath,
2605
- line: i + 1,
2606
- column: file.lines[i].indexOf("catch") + 1,
2607
- message: score === 0 ? "Empty catch block \u2014 errors are silently swallowed" : `Decorative error handler: ${label}`,
2608
- severity: score === 0 ? "warning" : "info",
2609
- category: "reliability",
2610
- fix: "Log the error with context and either re-throw, return an error response, or recover gracefully"
2611
- });
2612
- }
2613
- i = bodyEnd;
2590
+ const source = file.lines.join("\n");
2591
+ const code = blankOut(source, true);
2592
+ const noComments = blankOut(source, false);
2593
+ const catchRe = /(?<![\w$.])catch\s*(?:\(([^)]*)\)\s*)?\{/g;
2594
+ let catchLine = 0;
2595
+ let scanned = 0;
2596
+ for (let m = catchRe.exec(code); m; m = catchRe.exec(code)) {
2597
+ for (; scanned < m.index; scanned++) if (code[scanned] === "\n") catchLine++;
2598
+ const open = m.index + m[0].length - 1;
2599
+ const close = closingBrace(code, open);
2600
+ if (close === -1) continue;
2601
+ const finding = catchFinding(file, catchLine, {
2602
+ text: source.slice(open + 1, close),
2603
+ empty: code.slice(open + 1, close).trim() === "",
2604
+ returned: returnedValues(code.slice(open + 1, close), noComments.slice(open + 1, close)),
2605
+ param: m[1]?.match(/^\s*([A-Za-z_$][\w$]*)/)?.[1]
2606
+ });
2607
+ if (finding) findings.push(finding);
2614
2608
  }
2615
2609
  return findings;
2616
2610
  }
@@ -4740,6 +4734,14 @@ var STATUS_ICONS = {
4740
4734
  info: pc.blue
4741
4735
  };
4742
4736
 
4737
+ // src/web-scanner/index.ts
4738
+ import { lookup as dnsLookup } from "dns";
4739
+ import { lookup as dnsLookupAll } from "dns/promises";
4740
+ import { request as httpRequest } from "http";
4741
+ import { request as httpsRequest } from "https";
4742
+ import { BlockList, isIP } from "net";
4743
+ import { createBrotliDecompress, createGunzip, createInflate } from "zlib";
4744
+
4743
4745
  // src/web-scanner/checks.ts
4744
4746
  function make(id, name, description, maxPoints, severity, status, details) {
4745
4747
  return {
@@ -4879,76 +4881,153 @@ function getDomain(url) {
4879
4881
  return url;
4880
4882
  }
4881
4883
  }
4882
- var PRIVATE_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "0.0.0.0", "[::1]", "metadata.google.internal"]);
4883
- function isPrivateHost(hostname) {
4884
- if (PRIVATE_HOSTNAMES.has(hostname.toLowerCase())) return true;
4885
- const h = hostname.replace(/^\[|\]$/g, "").toLowerCase();
4886
- if (h === "::1" || h.startsWith("fe80:") || h.startsWith("fc") || h.startsWith("fd") || h === "::") return true;
4887
- const ipv4 = hostname.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
4888
- if (ipv4) {
4889
- const [, a, b] = ipv4.map(Number);
4890
- if (a === 10 || a === 127 || a === 0) return true;
4891
- if (a === 172 && b >= 16 && b <= 31) return true;
4892
- if (a === 192 && b === 168) return true;
4893
- if (a === 169 && b === 254) return true;
4894
- if (a === 100 && b >= 64 && b <= 127) return true;
4895
- if (a === 198 && (b === 18 || b === 19)) return true;
4896
- }
4897
- return false;
4884
+ var PRIVATE_RANGES = new BlockList();
4885
+ for (const [network, prefix] of [
4886
+ ["0.0.0.0", 8],
4887
+ ["10.0.0.0", 8],
4888
+ ["100.64.0.0", 10],
4889
+ ["127.0.0.0", 8],
4890
+ ["169.254.0.0", 16],
4891
+ ["172.16.0.0", 12],
4892
+ ["192.0.0.0", 24],
4893
+ ["192.168.0.0", 16],
4894
+ ["198.18.0.0", 15],
4895
+ ["224.0.0.0", 3]
4896
+ ]) PRIVATE_RANGES.addSubnet(network, prefix, "ipv4");
4897
+ for (const [network, prefix] of [
4898
+ ["::", 96],
4899
+ ["64:ff9b:1::", 48],
4900
+ ["2001::", 32],
4901
+ ["fc00::", 7],
4902
+ ["fe80::", 10],
4903
+ ["fec0::", 10],
4904
+ ["ff00::", 8]
4905
+ ]) PRIVATE_RANGES.addSubnet(network, prefix, "ipv6");
4906
+ function ipv6Groups(address) {
4907
+ const quad = address.match(/^(.*:)(\d+)\.(\d+)\.(\d+)\.(\d+)$/);
4908
+ const hex = quad ? `${quad[1]}${(+quad[2] << 8 | +quad[3]).toString(16)}:${(+quad[4] << 8 | +quad[5]).toString(16)}` : address;
4909
+ const [head, tail] = hex.split("::");
4910
+ const parse2 = (part) => part ? part.split(":").map((g) => parseInt(g, 16)) : [];
4911
+ const front = parse2(head);
4912
+ const back = parse2(tail);
4913
+ return tail === void 0 ? front : [...front, ...Array(8 - front.length - back.length).fill(0), ...back];
4898
4914
  }
4899
- function validateRedirectUrl(responseUrl) {
4900
- try {
4901
- const parsed = new URL(responseUrl);
4902
- return !isPrivateHost(parsed.hostname);
4903
- } catch {
4904
- return false;
4905
- }
4915
+ function embeddedIPv4(address) {
4916
+ const g = ipv6Groups(address);
4917
+ const quad = (hi, lo) => `${hi >> 8}.${hi & 255}.${lo >> 8}.${lo & 255}`;
4918
+ const zero = (from, to) => g.slice(from, to).every((x) => x === 0);
4919
+ if (zero(0, 5) && g[5] === 65535) return quad(g[6], g[7]);
4920
+ if (g[0] === 100 && g[1] === 65435 && zero(2, 6)) return quad(g[6], g[7]);
4921
+ if (g[0] === 8194) return quad(g[1], g[2]);
4922
+ return null;
4906
4923
  }
4907
- async function fetchText(url, timeout = 8e3) {
4908
- try {
4909
- const c = new AbortController();
4910
- const t = setTimeout(() => c.abort(), timeout);
4911
- const r = await fetch(url, { signal: c.signal, headers: { "User-Agent": "Prodlint-WebScanner/1.0" }, redirect: "follow" });
4912
- clearTimeout(t);
4913
- if (r.url && !validateRedirectUrl(r.url)) return null;
4914
- return r.ok ? await r.text() : null;
4915
- } catch {
4916
- return null;
4924
+ function isPrivateAddress(address) {
4925
+ const ip = address.replace(/^\[|\]$/g, "").replace(/%.*$/, "");
4926
+ const family = isIP(ip);
4927
+ if (family === 4) return PRIVATE_RANGES.check(ip, "ipv4");
4928
+ if (family !== 6) return false;
4929
+ const v4 = embeddedIPv4(ip);
4930
+ return v4 !== null && PRIVATE_RANGES.check(v4, "ipv4") || PRIVATE_RANGES.check(ip, "ipv6");
4931
+ }
4932
+ function isPrivateHost(hostname) {
4933
+ const host = hostname.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.+$/, "");
4934
+ if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".internal")) return true;
4935
+ return isPrivateAddress(host);
4936
+ }
4937
+ var guardedLookup = (hostname, options, callback) => {
4938
+ dnsLookup(hostname, { ...options, all: true }, (err, addresses) => {
4939
+ if (err) return callback(err, "");
4940
+ const blocked = addresses.find((a) => isPrivateAddress(a.address));
4941
+ if (blocked) {
4942
+ return callback(Object.assign(new Error(`${hostname} resolves to private address ${blocked.address}`), { code: "EPRIVATEADDR" }), "");
4943
+ }
4944
+ if (options.all) return callback(null, addresses);
4945
+ callback(null, addresses[0].address, addresses[0].family);
4946
+ });
4947
+ };
4948
+ var USER_AGENT = "Prodlint-WebScanner/1.0";
4949
+ var MAX_REDIRECTS = 5;
4950
+ var MAX_BODY_BYTES = 5 * 1024 * 1024;
4951
+ function flattenHeaders(res) {
4952
+ const headers = {};
4953
+ for (const [key, value] of Object.entries(res.headers)) {
4954
+ if (value !== void 0) headers[key] = Array.isArray(value) ? value.join(", ") : value;
4917
4955
  }
4956
+ return headers;
4918
4957
  }
4919
- async function fetchHeaders(url, timeout = 8e3) {
4920
- try {
4921
- const c = new AbortController();
4922
- const t = setTimeout(() => c.abort(), timeout);
4923
- const r = await fetch(url, { signal: c.signal, headers: { "User-Agent": "Prodlint-WebScanner/1.0" }, redirect: "follow" });
4924
- clearTimeout(t);
4925
- if (r.url && !validateRedirectUrl(r.url)) return {};
4926
- const h = {};
4927
- r.headers.forEach((v, k) => {
4928
- h[k.toLowerCase()] = v;
4958
+ function readBody(res) {
4959
+ const encoding = String(res.headers["content-encoding"] ?? "").trim().toLowerCase();
4960
+ const decoder = encoding === "gzip" || encoding === "x-gzip" ? createGunzip() : encoding === "deflate" ? createInflate() : encoding === "br" ? createBrotliDecompress() : null;
4961
+ const stream = decoder ? res.pipe(decoder) : res;
4962
+ return new Promise((resolve4, reject) => {
4963
+ const chunks = [];
4964
+ let size = 0;
4965
+ const done = () => resolve4(Buffer.concat(chunks).toString("utf8"));
4966
+ stream.on("data", (chunk) => {
4967
+ if (size >= MAX_BODY_BYTES) return;
4968
+ chunks.push(chunk.subarray(0, MAX_BODY_BYTES - size));
4969
+ size += chunk.length;
4970
+ if (size >= MAX_BODY_BYTES) {
4971
+ done();
4972
+ res.destroy();
4973
+ decoder?.destroy();
4974
+ }
4929
4975
  });
4930
- return h;
4931
- } catch {
4932
- return {};
4933
- }
4976
+ stream.on("end", done);
4977
+ stream.on("error", reject);
4978
+ res.on("error", reject);
4979
+ });
4934
4980
  }
4935
- async function fetchWithTiming(url, timeout = 15e3) {
4936
- try {
4937
- const c = new AbortController();
4938
- const t = setTimeout(() => c.abort(), timeout);
4939
- const start = Date.now();
4940
- const r = await fetch(url, { signal: c.signal, headers: { "User-Agent": "Prodlint-WebScanner/1.0" }, redirect: "follow" });
4941
- const html = await r.text();
4942
- clearTimeout(t);
4943
- if (r.url && !validateRedirectUrl(r.url)) return { html: null, loadTimeMs: null };
4944
- return r.ok ? { html, loadTimeMs: Date.now() - start } : { html: null, loadTimeMs: null };
4945
- } catch {
4946
- return { html: null, loadTimeMs: null };
4947
- }
4981
+ function guardedGet(url, timeoutMs, lookup = guardedLookup) {
4982
+ return new Promise((resolve4) => {
4983
+ let current;
4984
+ const finish = (result) => {
4985
+ clearTimeout(timer);
4986
+ current?.destroy();
4987
+ resolve4(result);
4988
+ };
4989
+ const timer = setTimeout(() => finish(null), timeoutMs);
4990
+ const attempt = (target, redirectsLeft) => {
4991
+ if (target.protocol !== "http:" && target.protocol !== "https:" || isPrivateHost(target.hostname)) return finish(null);
4992
+ const send = target.protocol === "https:" ? httpsRequest : httpRequest;
4993
+ current = send(target, { agent: false, lookup, headers: { "User-Agent": USER_AGENT, "Accept-Encoding": "gzip, deflate, br" } }, (res) => {
4994
+ const status = res.statusCode ?? 0;
4995
+ const location = res.headers.location;
4996
+ if (status >= 300 && status < 400 && location) {
4997
+ res.resume();
4998
+ let next;
4999
+ try {
5000
+ next = new URL(location, target);
5001
+ } catch {
5002
+ return finish(null);
5003
+ }
5004
+ return redirectsLeft > 0 ? attempt(next, redirectsLeft - 1) : finish(null);
5005
+ }
5006
+ readBody(res).then((body) => finish({ status, headers: flattenHeaders(res), body }), () => finish(null));
5007
+ });
5008
+ current.on("error", () => finish(null));
5009
+ current.end();
5010
+ };
5011
+ try {
5012
+ attempt(new URL(url), MAX_REDIRECTS);
5013
+ } catch {
5014
+ finish(null);
5015
+ }
5016
+ });
5017
+ }
5018
+ async function fetchText(url, timeout = 8e3) {
5019
+ const r = await guardedGet(url, timeout);
5020
+ return r && r.status >= 200 && r.status < 300 ? r.body : null;
5021
+ }
5022
+ async function fetchPage(url, timeout = 15e3) {
5023
+ const start = Date.now();
5024
+ const r = await guardedGet(url, timeout);
5025
+ const ok = r !== null && r.status >= 200 && r.status < 300;
5026
+ return { headers: r?.headers ?? {}, html: ok ? r.body : null, loadTimeMs: ok ? Date.now() - start : null };
4948
5027
  }
4949
5028
  async function runWebScan(targetUrl) {
4950
5029
  const domain = getDomain(targetUrl);
4951
- const [robotsTxt, llmsTxt, aiTxt, tdmRep, agentCard, sitemapXml, httpSigDirectory, headers, pageData] = await Promise.all([
5030
+ const [robotsTxt, llmsTxt, aiTxt, tdmRep, agentCard, sitemapXml, httpSigDirectory, page] = await Promise.all([
4952
5031
  fetchText(`${targetUrl}/robots.txt`),
4953
5032
  fetchText(`${targetUrl}/llms.txt`),
4954
5033
  fetchText(`${targetUrl}/ai.txt`),
@@ -4956,10 +5035,9 @@ async function runWebScan(targetUrl) {
4956
5035
  fetchText(`${targetUrl}/.well-known/agent-card.json`),
4957
5036
  fetchText(`${targetUrl}/sitemap.xml`),
4958
5037
  fetchText(`${targetUrl}/.well-known/http-message-signatures-directory`),
4959
- fetchHeaders(targetUrl),
4960
- fetchWithTiming(targetUrl)
5038
+ fetchPage(targetUrl)
4961
5039
  ]);
4962
- const ctx = { url: targetUrl, domain, robotsTxt, llmsTxt, aiTxt, tdmRep, agentCard, sitemapXml, httpSigDirectory, headers, html: pageData.html, loadTimeMs: pageData.loadTimeMs };
5040
+ const ctx = { url: targetUrl, domain, robotsTxt, llmsTxt, aiTxt, tdmRep, agentCard, sitemapXml, httpSigDirectory, ...page };
4963
5041
  const checks = allChecks.map((fn) => fn(ctx));
4964
5042
  const totalPoints = checks.reduce((s, c) => s + c.points, 0);
4965
5043
  const maxPoints = checks.reduce((s, c) => s + c.maxPoints, 0);