prodlint 0.8.1 → 0.9.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/mcp.js CHANGED
@@ -5,7 +5,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
6
  import { z } from "zod";
7
7
  import { stat as stat2 } from "fs/promises";
8
- import { resolve as resolve4 } from "path";
8
+ import { resolve as resolve4, sep as sep2 } from "path";
9
9
 
10
10
  // src/utils/file-walker.ts
11
11
  import fg from "fast-glob";
@@ -719,8 +719,8 @@ function summarizeFindings(findings) {
719
719
 
720
720
  // src/rules/secrets.ts
721
721
  var SECRET_PATTERNS = [
722
- { name: "Stripe secret key", pattern: /sk_live_[a-zA-Z0-9]{20,}/ },
723
- { name: "Stripe test key", pattern: /sk_test_[a-zA-Z0-9]{20,}/ },
722
+ { name: "Stripe secret key", pattern: /sk_live_[a-zA-Z0-9]{8,}/ },
723
+ { name: "Stripe test key", pattern: /sk_test_[a-zA-Z0-9]{8,}/ },
724
724
  { name: "AWS access key", pattern: /AKIA[0-9A-Z]{16}/ },
725
725
  { name: "AWS secret key", pattern: /(?:aws_secret_access_key|AWS_SECRET)\s*[=:]\s*['"]?[A-Za-z0-9/+=]{40}['"]?/ },
726
726
  { name: "Supabase service role key", pattern: /eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\.[A-Za-z0-9_-]{50,}\.[A-Za-z0-9_-]{20,}/ },
@@ -753,7 +753,8 @@ var secretsRule = {
753
753
  column: match.index + 1,
754
754
  message: `Hardcoded ${name} detected`,
755
755
  severity: "critical",
756
- category: "security"
756
+ category: "security",
757
+ fix: "Move the secret to an environment variable and access via process.env.SECRET_NAME"
757
758
  });
758
759
  }
759
760
  }
@@ -837,7 +838,8 @@ var hallucinatedImportsRule = {
837
838
  column: 1,
838
839
  message: `Package "${pkgName}" is imported but not in package.json`,
839
840
  severity: isNonProd ? "warning" : "critical",
840
- category: "reliability"
841
+ category: "reliability",
842
+ fix: "Verify the package exists on npm. The AI may have invented this package name."
841
843
  });
842
844
  }
843
845
  return findings;
@@ -865,7 +867,8 @@ var hallucinatedImportsRule = {
865
867
  column: match.index + 1,
866
868
  message: `Package "${pkgName}" is imported but not in package.json`,
867
869
  severity: isNonProd ? "warning" : "critical",
868
- category: "reliability"
870
+ category: "reliability",
871
+ fix: "Verify the package exists on npm. The AI may have invented this package name."
869
872
  });
870
873
  }
871
874
  }
@@ -948,7 +951,8 @@ var authChecksRule = {
948
951
  column: 1,
949
952
  message,
950
953
  severity,
951
- category: "security"
954
+ category: "security",
955
+ fix: "Add authentication check: const session = await auth(); if (!session) return new Response('Unauthorized', { status: 401 })"
952
956
  }];
953
957
  }
954
958
  };
@@ -987,7 +991,8 @@ var envExposureRule = {
987
991
  column: 1,
988
992
  message: ".env is not listed in .gitignore \u2014 secrets may be committed",
989
993
  severity: "critical",
990
- category: "security"
994
+ category: "security",
995
+ fix: "Add .env to .gitignore to prevent committing secrets"
991
996
  });
992
997
  }
993
998
  return findings;
@@ -1009,7 +1014,8 @@ var envExposureRule = {
1009
1014
  column: match.index + 1,
1010
1015
  message: isSensitive ? `Sensitive server env var "${envName}" used in client component` : `Server env var "${envName}" used in client component (will be undefined at runtime)`,
1011
1016
  severity: isSensitive ? "critical" : "warning",
1012
- category: "security"
1017
+ category: "security",
1018
+ fix: "Move to server-only file or use NEXT_PUBLIC_ prefix only for non-sensitive values"
1013
1019
  });
1014
1020
  }
1015
1021
  }
@@ -1047,7 +1053,8 @@ var errorHandlingRule = {
1047
1053
  column: 1,
1048
1054
  message: "API route handler has no try/catch block",
1049
1055
  severity: "warning",
1050
- category: "reliability"
1056
+ category: "reliability",
1057
+ fix: "Wrap the handler body in try/catch and return appropriate error responses"
1051
1058
  }];
1052
1059
  }
1053
1060
  };
@@ -1105,7 +1112,8 @@ var inputValidationRule = {
1105
1112
  column: 1,
1106
1113
  message: "Request body accessed without validation (consider using Zod, Yup, or similar)",
1107
1114
  severity: "warning",
1108
- category: "security"
1115
+ category: "security",
1116
+ fix: "Validate input with Zod or a similar schema library before using it"
1109
1117
  }];
1110
1118
  }
1111
1119
  };
@@ -1162,12 +1170,22 @@ var rateLimitingRule = {
1162
1170
  column: 1,
1163
1171
  message: "No rate limiting \u2014 anyone could spam this endpoint and run up your API costs",
1164
1172
  severity: "info",
1165
- category: "security"
1173
+ category: "security",
1174
+ fix: "Add rate limiting: import { Ratelimit } from '@upstash/ratelimit' or use express-rate-limit"
1166
1175
  }];
1167
1176
  }
1168
1177
  };
1169
1178
 
1170
1179
  // src/rules/cors-config.ts
1180
+ function hasCredentialsNearby(lines, startLine, commentMap) {
1181
+ const end = Math.min(lines.length, startLine + 8);
1182
+ for (let j = startLine; j < end; j++) {
1183
+ if (commentMap[j]) continue;
1184
+ if (/credentials\s*:\s*true/.test(lines[j])) return true;
1185
+ if (/Access-Control-Allow-Credentials['"]\s*[,:]\s*['"]true['"]/.test(lines[j])) return true;
1186
+ }
1187
+ return false;
1188
+ }
1171
1189
  var corsConfigRule = {
1172
1190
  id: "cors-config",
1173
1191
  name: "Permissive CORS",
@@ -1181,14 +1199,16 @@ var corsConfigRule = {
1181
1199
  if (isCommentLine(file.lines, i, file.commentMap)) continue;
1182
1200
  const line = file.lines[i];
1183
1201
  if (/['"]Access-Control-Allow-Origin['"]\s*[,:]\s*['"]\*['"]/.test(line)) {
1202
+ const withCreds = hasCredentialsNearby(file.lines, i, file.commentMap);
1184
1203
  findings.push({
1185
1204
  ruleId: "cors-config",
1186
1205
  file: file.relativePath,
1187
1206
  line: i + 1,
1188
1207
  column: line.indexOf("Access-Control") + 1,
1189
- message: 'Access-Control-Allow-Origin set to "*" allows any domain',
1190
- severity: "warning",
1191
- category: "security"
1208
+ message: withCreds ? "CORS wildcard with credentials allows any site to make authenticated requests" : 'Access-Control-Allow-Origin set to "*" allows any domain',
1209
+ severity: withCreds ? "critical" : "warning",
1210
+ category: "security",
1211
+ fix: withCreds ? "Never use credentials with wildcard origin. Set origin to specific trusted domains." : "Restrict origin to specific domains: origin: ['https://yourdomain.com']"
1192
1212
  });
1193
1213
  }
1194
1214
  if (/cors\(\s*\)/.test(line)) {
@@ -1199,18 +1219,21 @@ var corsConfigRule = {
1199
1219
  column: line.indexOf("cors(") + 1,
1200
1220
  message: "cors() called without config allows all origins",
1201
1221
  severity: "warning",
1202
- category: "security"
1222
+ category: "security",
1223
+ fix: "Pass explicit options: cors({ origin: 'https://yourdomain.com' })"
1203
1224
  });
1204
1225
  }
1205
1226
  if (/origin\s*:\s*['"]\*['"]/.test(line)) {
1227
+ const withCreds = hasCredentialsNearby(file.lines, i, file.commentMap);
1206
1228
  findings.push({
1207
1229
  ruleId: "cors-config",
1208
1230
  file: file.relativePath,
1209
1231
  line: i + 1,
1210
1232
  column: line.indexOf("origin") + 1,
1211
- message: 'CORS origin set to "*" allows any domain',
1212
- severity: "warning",
1213
- category: "security"
1233
+ message: withCreds ? "CORS wildcard with credentials allows any site to make authenticated requests" : 'CORS origin set to "*" allows any domain',
1234
+ severity: withCreds ? "critical" : "warning",
1235
+ category: "security",
1236
+ fix: withCreds ? "Never use credentials with wildcard origin. Set origin to specific trusted domains." : "Restrict origin to specific domains: origin: ['https://yourdomain.com']"
1214
1237
  });
1215
1238
  }
1216
1239
  if (/origin\s*:\s*true/.test(line)) {
@@ -1221,7 +1244,8 @@ var corsConfigRule = {
1221
1244
  column: line.indexOf("origin") + 1,
1222
1245
  message: "CORS origin set to true mirrors any requesting origin",
1223
1246
  severity: "warning",
1224
- category: "security"
1247
+ category: "security",
1248
+ fix: "Set origin to specific domains instead of reflecting the request origin"
1225
1249
  });
1226
1250
  }
1227
1251
  }
@@ -1230,7 +1254,7 @@ var corsConfigRule = {
1230
1254
  };
1231
1255
 
1232
1256
  // src/rules/ai-smells.ts
1233
- var CONSOLE_LOG_THRESHOLD = 5;
1257
+ var CONSOLE_LOG_THRESHOLD = 3;
1234
1258
  var ANY_TYPE_THRESHOLD = 5;
1235
1259
  var COMMENTED_CODE_THRESHOLD = 3;
1236
1260
  var aiSmellsRule = {
@@ -1264,7 +1288,8 @@ var aiSmellsRule = {
1264
1288
  column: line.indexOf(todoMatch[1]) + 1,
1265
1289
  message: `${todoMatch[1]} comment: ${todoMatch[2].trim() || "(no description)"}`,
1266
1290
  severity: "info",
1267
- category: "ai-quality"
1291
+ category: "ai-quality",
1292
+ fix: "Resolve the TODO/FIXME before shipping to production"
1268
1293
  });
1269
1294
  }
1270
1295
  const commentContent = trimmed.slice(2).trim();
@@ -1279,7 +1304,8 @@ var aiSmellsRule = {
1279
1304
  column: 1,
1280
1305
  message: `${COMMENTED_CODE_THRESHOLD}+ consecutive lines of commented-out code`,
1281
1306
  severity: "info",
1282
- category: "ai-quality"
1307
+ category: "ai-quality",
1308
+ fix: "Remove commented-out code \u2014 use version control to recover old code if needed"
1283
1309
  });
1284
1310
  }
1285
1311
  } else {
@@ -1296,7 +1322,8 @@ var aiSmellsRule = {
1296
1322
  column: 1,
1297
1323
  message: 'Placeholder "not implemented" function',
1298
1324
  severity: "warning",
1299
- category: "ai-quality"
1325
+ category: "ai-quality",
1326
+ fix: "Replace with a production-ready implementation or remove the function"
1300
1327
  });
1301
1328
  }
1302
1329
  if (/console\.log\s*\(/.test(line)) {
@@ -1314,7 +1341,8 @@ var aiSmellsRule = {
1314
1341
  column: 1,
1315
1342
  message: `${consoleLogCount} console.log statements (consider a proper logger)`,
1316
1343
  severity: "warning",
1317
- category: "ai-quality"
1344
+ category: "ai-quality",
1345
+ fix: "Replace console.log with a structured logger (e.g., pino, winston) or remove debug logs"
1318
1346
  });
1319
1347
  }
1320
1348
  if (anyTypeCount > ANY_TYPE_THRESHOLD) {
@@ -1325,7 +1353,8 @@ var aiSmellsRule = {
1325
1353
  column: 1,
1326
1354
  message: `${anyTypeCount} uses of "any" type (consider proper typing)`,
1327
1355
  severity: "warning",
1328
- category: "ai-quality"
1356
+ category: "ai-quality",
1357
+ fix: 'Replace "any" with specific types or use "unknown" with type narrowing'
1329
1358
  });
1330
1359
  }
1331
1360
  return findings;
@@ -1367,7 +1396,8 @@ var unsafeHtmlRule = {
1367
1396
  column: file.lines[line - 1].indexOf("dangerouslySetInnerHTML") + 1,
1368
1397
  message: "dangerouslySetInnerHTML is an XSS risk \u2014 sanitize with DOMPurify or similar",
1369
1398
  severity: "critical",
1370
- category: "security"
1399
+ category: "security",
1400
+ fix: "Sanitize HTML with DOMPurify before rendering: DOMPurify.sanitize(html)"
1371
1401
  });
1372
1402
  }
1373
1403
  }
@@ -1393,7 +1423,8 @@ var unsafeHtmlRule = {
1393
1423
  column: file.lines[line - 1].indexOf("dangerouslySetInnerHTML") + 1,
1394
1424
  message: "dangerouslySetInnerHTML is an XSS risk \u2014 sanitize with DOMPurify or similar",
1395
1425
  severity: "critical",
1396
- category: "security"
1426
+ category: "security",
1427
+ fix: "Sanitize HTML with DOMPurify before rendering: DOMPurify.sanitize(html)"
1397
1428
  });
1398
1429
  }
1399
1430
  }
@@ -1408,7 +1439,8 @@ var unsafeHtmlRule = {
1408
1439
  column: file.lines[line - 1].indexOf(".innerHTML") + 1,
1409
1440
  message: "Direct innerHTML assignment is an XSS risk",
1410
1441
  severity: "critical",
1411
- category: "security"
1442
+ category: "security",
1443
+ fix: "Sanitize HTML with DOMPurify before rendering: DOMPurify.sanitize(html)"
1412
1444
  });
1413
1445
  }
1414
1446
  }
@@ -1436,7 +1468,8 @@ var unsafeHtmlRule = {
1436
1468
  column: line.indexOf("dangerouslySetInnerHTML") + 1,
1437
1469
  message: "dangerouslySetInnerHTML is an XSS risk \u2014 sanitize with DOMPurify or similar",
1438
1470
  severity: "critical",
1439
- category: "security"
1471
+ category: "security",
1472
+ fix: "Sanitize HTML with DOMPurify before rendering: DOMPurify.sanitize(html)"
1440
1473
  });
1441
1474
  }
1442
1475
  if (/\w\.innerHTML\s*=/.test(line)) {
@@ -1447,7 +1480,8 @@ var unsafeHtmlRule = {
1447
1480
  column: line.indexOf(".innerHTML") + 1,
1448
1481
  message: "Direct innerHTML assignment is an XSS risk",
1449
1482
  severity: "critical",
1450
- category: "security"
1483
+ category: "security",
1484
+ fix: "Sanitize HTML with DOMPurify before rendering: DOMPurify.sanitize(html)"
1451
1485
  });
1452
1486
  }
1453
1487
  }
@@ -1509,7 +1543,8 @@ var sqlInjectionRule = {
1509
1543
  column: 1,
1510
1544
  message,
1511
1545
  severity,
1512
- category: "security"
1546
+ category: "security",
1547
+ fix: "Use parameterized queries: db.query('SELECT * FROM users WHERE id = $1', [id])"
1513
1548
  });
1514
1549
  break;
1515
1550
  }
@@ -1557,7 +1592,8 @@ var placeholderContentRule = {
1557
1592
  column: match.index + 1,
1558
1593
  message: label,
1559
1594
  severity: "info",
1560
- category: "ai-quality"
1595
+ category: "ai-quality",
1596
+ fix: "Replace placeholder content with real production values before deploying"
1561
1597
  });
1562
1598
  break;
1563
1599
  }
@@ -1603,7 +1639,8 @@ var staleFallbackRule = {
1603
1639
  column: match.index + 1,
1604
1640
  message: `${label} \u2014 use environment variable instead`,
1605
1641
  severity: "warning",
1606
- category: "ai-quality"
1642
+ category: "ai-quality",
1643
+ fix: "Replace hardcoded URL with an environment variable: process.env.DATABASE_URL or similar"
1607
1644
  });
1608
1645
  break;
1609
1646
  }
@@ -1649,7 +1686,8 @@ var hallucinatedApiRule = {
1649
1686
  column: match.index + 1,
1650
1687
  message: fix,
1651
1688
  severity: "warning",
1652
- category: "ai-quality"
1689
+ category: "ai-quality",
1690
+ fix
1653
1691
  });
1654
1692
  }
1655
1693
  }
@@ -1731,7 +1769,8 @@ var openRedirectRule = {
1731
1769
  column: col,
1732
1770
  message: "User input in redirect \u2014 validate against an allowlist to prevent open redirect",
1733
1771
  severity: "warning",
1734
- category: "security"
1772
+ category: "security",
1773
+ fix: "Validate the redirect URL against an allowlist of trusted domains"
1735
1774
  });
1736
1775
  return;
1737
1776
  }
@@ -1743,7 +1782,8 @@ var openRedirectRule = {
1743
1782
  column: col,
1744
1783
  message: "Possible user input in redirect \u2014 verify the URL is validated before use",
1745
1784
  severity: "warning",
1746
- category: "security"
1785
+ category: "security",
1786
+ fix: "Validate the redirect URL against an allowlist of trusted domains"
1747
1787
  });
1748
1788
  }
1749
1789
  });
@@ -1764,7 +1804,8 @@ var openRedirectRule = {
1764
1804
  column: match.index + 1,
1765
1805
  message: "User input in redirect \u2014 validate against an allowlist to prevent open redirect",
1766
1806
  severity: "warning",
1767
- category: "security"
1807
+ category: "security",
1808
+ fix: "Validate the redirect URL against an allowlist of trusted domains"
1768
1809
  });
1769
1810
  break;
1770
1811
  }
@@ -1780,7 +1821,8 @@ var openRedirectRule = {
1780
1821
  column: match.index + 1,
1781
1822
  message: "Possible user input in redirect \u2014 verify the URL is validated before use",
1782
1823
  severity: "warning",
1783
- category: "security"
1824
+ category: "security",
1825
+ fix: "Validate the redirect URL against an allowlist of trusted domains"
1784
1826
  });
1785
1827
  break;
1786
1828
  }
@@ -1818,7 +1860,8 @@ var noSyncFsRule = {
1818
1860
  column: match.index + 1,
1819
1861
  message: `Synchronous ${fnName}() blocks the event loop \u2014 use async alternative`,
1820
1862
  severity,
1821
- category: "performance"
1863
+ category: "performance",
1864
+ fix: `Use the async version: fs.promises.${fnName.replace("Sync", "")}() instead of ${fnName}()`
1822
1865
  });
1823
1866
  }
1824
1867
  }
@@ -1876,7 +1919,8 @@ var noNPlusOneRule = {
1876
1919
  column: match.index + 1,
1877
1920
  message: "Database/fetch call inside loop \u2014 potential N+1 query, consider batching",
1878
1921
  severity: "warning",
1879
- category: "performance"
1922
+ category: "performance",
1923
+ fix: "Use eager loading (include/join) or batch the queries outside the loop"
1880
1924
  });
1881
1925
  break;
1882
1926
  }
@@ -1912,7 +1956,8 @@ var noDynamicImportLoopRule = {
1912
1956
  column: match.index + 1,
1913
1957
  message: "Dynamic import() inside loop \u2014 move import outside or use Promise.all",
1914
1958
  severity: "warning",
1915
- category: "performance"
1959
+ category: "performance",
1960
+ fix: "Move the dynamic import outside the loop and call it once"
1916
1961
  });
1917
1962
  }
1918
1963
  }
@@ -1944,7 +1989,8 @@ var noUnboundedQueryRule = {
1944
1989
  column: line.indexOf(".findMany") + 1,
1945
1990
  message: ".findMany() without take/limit \u2014 query may return unbounded results",
1946
1991
  severity: "warning",
1947
- category: "performance"
1992
+ category: "performance",
1993
+ fix: "Add a LIMIT clause or use pagination to prevent unbounded result sets"
1948
1994
  });
1949
1995
  continue;
1950
1996
  }
@@ -1958,7 +2004,8 @@ var noUnboundedQueryRule = {
1958
2004
  column: line.indexOf(".findMany") + 1,
1959
2005
  message: ".findMany() without take \u2014 add pagination or limit",
1960
2006
  severity: "warning",
1961
- category: "performance"
2007
+ category: "performance",
2008
+ fix: "Add a LIMIT clause or use pagination to prevent unbounded result sets"
1962
2009
  });
1963
2010
  }
1964
2011
  continue;
@@ -1974,7 +2021,8 @@ var noUnboundedQueryRule = {
1974
2021
  column: line.indexOf(".select") + 1,
1975
2022
  message: ".select('*') without .limit() or filter \u2014 add pagination or a where clause",
1976
2023
  severity: "warning",
1977
- category: "performance"
2024
+ category: "performance",
2025
+ fix: "Add a LIMIT clause or use pagination to prevent unbounded result sets"
1978
2026
  });
1979
2027
  }
1980
2028
  }
@@ -2053,7 +2101,8 @@ var unhandledPromiseRule = {
2053
2101
  column: col ? col.index + 1 : 1,
2054
2102
  message: "Async call without await, return, or assignment \u2014 promise result is lost",
2055
2103
  severity: "warning",
2056
- category: "reliability"
2104
+ category: "reliability",
2105
+ fix: "Add .catch() handler or use try/catch with await"
2057
2106
  });
2058
2107
  }
2059
2108
  return findings;
@@ -2085,7 +2134,8 @@ var missingLoadingStateRule = {
2085
2134
  column: 1,
2086
2135
  message: "useEffect with fetch but no loading/pending state \u2014 users see empty content during load",
2087
2136
  severity: "info",
2088
- category: "reliability"
2137
+ category: "reliability",
2138
+ fix: "Add a loading state: const [loading, setLoading] = useState(true) and show a spinner/skeleton while loading"
2089
2139
  }];
2090
2140
  }
2091
2141
  }
@@ -2116,7 +2166,8 @@ var missingErrorBoundaryRule = {
2116
2166
  column: 1,
2117
2167
  message: `Layout without error.tsx \u2014 errors in ${dir} will bubble up to parent error boundary`,
2118
2168
  severity: "info",
2119
- category: "reliability"
2169
+ category: "reliability",
2170
+ fix: "Add an error.tsx file in the same route segment to catch rendering errors"
2120
2171
  }];
2121
2172
  }
2122
2173
  };
@@ -2254,7 +2305,8 @@ var codebaseConsistencyRule = {
2254
2305
  column: 1,
2255
2306
  message: `${dim.label}: ${consistency}% consistent \u2014 dominant: ${dominant} (${dominantCount} files), minority: ${minorities.join(", ")}`,
2256
2307
  severity: consistency < 60 ? "warning" : "info",
2257
- category: "ai-quality"
2308
+ category: "ai-quality",
2309
+ fix: "Standardize on one pattern across the codebase for consistency"
2258
2310
  });
2259
2311
  }
2260
2312
  return findings;
@@ -2383,7 +2435,8 @@ var deadExportsRule = {
2383
2435
  column: 1,
2384
2436
  message: `${totalDead} exported symbols never imported \u2014 dead exports pollute AI context. Top files: ${topFiles.join(", ")}`,
2385
2437
  severity: totalDead > 20 ? "warning" : "info",
2386
- category: "ai-quality"
2438
+ category: "ai-quality",
2439
+ fix: "Remove the unused export or add a consumer"
2387
2440
  });
2388
2441
  }
2389
2442
  return findings;
@@ -2447,7 +2500,8 @@ var shallowCatchRule = {
2447
2500
  column: file.lines[catchLine].indexOf("catch") + 1,
2448
2501
  message: score === 0 ? "Empty catch block \u2014 errors are silently swallowed" : `Decorative error handler: ${label}`,
2449
2502
  severity: score === 0 ? "warning" : "info",
2450
- category: "reliability"
2503
+ category: "reliability",
2504
+ fix: "Log the error with context and either re-throw, return an error response, or recover gracefully"
2451
2505
  });
2452
2506
  }
2453
2507
  });
@@ -2515,7 +2569,8 @@ var shallowCatchRule = {
2515
2569
  column: file.lines[i].indexOf("catch") + 1,
2516
2570
  message: score === 0 ? "Empty catch block \u2014 errors are silently swallowed" : `Decorative error handler: ${label}`,
2517
2571
  severity: score === 0 ? "warning" : "info",
2518
- category: "reliability"
2572
+ category: "reliability",
2573
+ fix: "Log the error with context and either re-throw, return an error response, or recover gracefully"
2519
2574
  });
2520
2575
  }
2521
2576
  i = bodyEnd;
@@ -2567,7 +2622,8 @@ var comprehensionDebtRule = {
2567
2622
  column: 1,
2568
2623
  message: `${fnName}() has ${params} parameters (max ${MAX_PARAMS}) \u2014 hard to call correctly`,
2569
2624
  severity: "info",
2570
- category: "ai-quality"
2625
+ category: "ai-quality",
2626
+ fix: "Group related parameters into an options object"
2571
2627
  });
2572
2628
  }
2573
2629
  }
@@ -2594,7 +2650,8 @@ var comprehensionDebtRule = {
2594
2650
  column: 1,
2595
2651
  message: `${fnName}() is ${length} lines long (max ${MAX_FUNCTION_LENGTH}) \u2014 consider splitting`,
2596
2652
  severity: "info",
2597
- category: "ai-quality"
2653
+ category: "ai-quality",
2654
+ fix: "Break this into smaller, focused functions with clear names"
2598
2655
  });
2599
2656
  }
2600
2657
  if (maxDepthInFn > MAX_NESTING_DEPTH) {
@@ -2605,7 +2662,8 @@ var comprehensionDebtRule = {
2605
2662
  column: 1,
2606
2663
  message: `${fnName}() has nesting depth ${maxDepthInFn} (max ${MAX_NESTING_DEPTH}) \u2014 flatten with early returns`,
2607
2664
  severity: "info",
2608
- category: "ai-quality"
2665
+ category: "ai-quality",
2666
+ fix: "Reduce nesting with early returns, guard clauses, or extracted helper functions"
2609
2667
  });
2610
2668
  }
2611
2669
  inFunction = false;
@@ -2712,7 +2770,8 @@ var phantomDependencyRule = {
2712
2770
  column: 1,
2713
2771
  message: `"${name}" is a commonly hallucinated package name \u2014 verify it exists and is the correct package`,
2714
2772
  severity: "warning",
2715
- category: "security"
2773
+ category: "security",
2774
+ fix: "Add the missing package to dependencies in package.json, or remove the import if the package is hallucinated"
2716
2775
  });
2717
2776
  }
2718
2777
  for (const pattern of SUSPICIOUS_PATTERNS) {
@@ -2724,7 +2783,8 @@ var phantomDependencyRule = {
2724
2783
  column: 1,
2725
2784
  message: `"${name}" has a suspicious package name pattern \u2014 verify it's legitimate`,
2726
2785
  severity: "info",
2727
- category: "security"
2786
+ category: "security",
2787
+ fix: "Verify the package on npm \u2014 suspicious names may indicate typosquatting or hallucination"
2728
2788
  });
2729
2789
  break;
2730
2790
  }
@@ -3234,6 +3294,7 @@ var useClientOveruseRule = {
3234
3294
  // src/rules/env-fallback-secret.ts
3235
3295
  var SENSITIVE_ENV = /process\.env\.(JWT_SECRET|SECRET_KEY|AUTH_SECRET|SESSION_SECRET|ENCRYPTION_KEY|API_SECRET|PRIVATE_KEY|SIGNING_KEY|NEXTAUTH_SECRET|TOKEN_SECRET|APP_SECRET|COOKIE_SECRET|HASH_SECRET)\s*(?:\|\||&&|\?\?)\s*['"`]/i;
3236
3296
  var ENV_FALLBACK = /process\.env\.\w*(SECRET|KEY|PASSWORD|TOKEN)\w*\s*(?:\|\||\?\?)\s*['"`]/i;
3297
+ var CONN_STRING_FALLBACK = /process\.env\.\w+\s*(?:\|\||\?\?)\s*['"`](?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp|mssql):\/\//i;
3237
3298
  var envFallbackSecretRule = {
3238
3299
  id: "env-fallback-secret",
3239
3300
  name: "Secret with Fallback Value",
@@ -3261,6 +3322,20 @@ var envFallbackSecretRule = {
3261
3322
  });
3262
3323
  continue;
3263
3324
  }
3325
+ const connMatch = CONN_STRING_FALLBACK.exec(line);
3326
+ if (connMatch) {
3327
+ findings.push({
3328
+ ruleId: "env-fallback-secret",
3329
+ file: file.relativePath,
3330
+ line: i + 1,
3331
+ column: connMatch.index + 1,
3332
+ message: "Connection string with credentials used as fallback \u2014 hardcoded DB/service URL becomes the production connection when env var is missing",
3333
+ severity: "warning",
3334
+ category: "security",
3335
+ fix: "Fail fast when required env vars are missing instead of falling back to a default value"
3336
+ });
3337
+ continue;
3338
+ }
3264
3339
  const genericMatch = ENV_FALLBACK.exec(line);
3265
3340
  if (genericMatch && !isConfigFile(file.relativePath)) {
3266
3341
  findings.push({
@@ -3477,7 +3552,8 @@ var evalInjectionRule = {
3477
3552
  column: match.index + 1,
3478
3553
  message: msg,
3479
3554
  severity: "critical",
3480
- category: "security"
3555
+ category: "security",
3556
+ fix: "Replace eval/Function constructor with a safe alternative like JSON.parse() or a sandboxed interpreter"
3481
3557
  });
3482
3558
  break;
3483
3559
  }
@@ -4274,44 +4350,57 @@ var clientSideAuthOnlyRule = {
4274
4350
  };
4275
4351
 
4276
4352
  // src/rules/missing-abort-controller.ts
4277
- var FETCH_CALL = /\bfetch\s*\(/;
4353
+ var HTTP_CALL = /\b(fetch|axios)\s*[.(]/;
4278
4354
  var HAS_TIMEOUT = [
4279
4355
  /AbortController/,
4280
4356
  /abort/i,
4281
4357
  /signal\s*:/,
4282
- /timeout/i,
4283
- /setTimeout.*abort/s
4358
+ /timeout\s*:/,
4359
+ /timeout\s*=/,
4360
+ /setTimeout.*abort/s,
4361
+ /axios\.create\s*\([^)]*timeout/s
4284
4362
  ];
4363
+ var BACKGROUND_PATTERN = /\b(setInterval|cron|schedule|createWorker|bullmq|agenda|queue\.process)\b/;
4364
+ function isBackgroundFile(file) {
4365
+ if (BACKGROUND_PATTERN.test(file.content)) return true;
4366
+ const p = file.relativePath.toLowerCase();
4367
+ return /\b(cron|jobs?|workers?|queues?|tasks?|background)\b/.test(p);
4368
+ }
4285
4369
  var missingAbortControllerRule = {
4286
4370
  id: "missing-abort-controller",
4287
4371
  name: "Missing Abort Controller",
4288
- description: "Detects fetch calls in API routes without timeout or AbortController \u2014 requests can hang indefinitely",
4372
+ description: "Detects fetch/axios calls without timeout or AbortController \u2014 requests can hang indefinitely",
4289
4373
  category: "performance",
4290
4374
  severity: "info",
4291
4375
  fileExtensions: ["ts", "tsx", "js", "jsx"],
4292
4376
  check(file, _project) {
4293
4377
  if (isTestFile(file.relativePath)) return [];
4294
- if (!isApiRoute(file.relativePath) && !/['"]use server['"]/.test(file.content)) return [];
4295
- if (!FETCH_CALL.test(file.content)) return [];
4378
+ const isRelevant = isApiRoute(file.relativePath) || /['"]use server['"]/.test(file.content) || isBackgroundFile(file);
4379
+ if (!isRelevant) return [];
4380
+ if (!HTTP_CALL.test(file.content)) return [];
4296
4381
  const hasTimeout = HAS_TIMEOUT.some((p) => p.test(file.content));
4297
4382
  if (hasTimeout) return [];
4298
4383
  let reportLine = 1;
4384
+ let matchedCall = "fetch";
4299
4385
  for (let i = 0; i < file.lines.length; i++) {
4300
4386
  if (isCommentLine(file.lines, i, file.commentMap)) continue;
4301
- if (FETCH_CALL.test(file.lines[i])) {
4387
+ const m = file.lines[i].match(HTTP_CALL);
4388
+ if (m) {
4302
4389
  reportLine = i + 1;
4390
+ matchedCall = m[1];
4303
4391
  break;
4304
4392
  }
4305
4393
  }
4394
+ const isFetch = matchedCall === "fetch";
4306
4395
  return [{
4307
4396
  ruleId: "missing-abort-controller",
4308
4397
  file: file.relativePath,
4309
4398
  line: reportLine,
4310
4399
  column: 1,
4311
- message: "fetch() without timeout or AbortController \u2014 request will hang indefinitely if the upstream server doesn't respond",
4400
+ message: `${matchedCall}() without timeout or AbortController \u2014 request will hang indefinitely if the upstream server doesn't respond`,
4312
4401
  severity: "info",
4313
4402
  category: "performance",
4314
- fix: "Add a timeout: const controller = new AbortController(); setTimeout(() => controller.abort(), 10000); fetch(url, { signal: controller.signal })"
4403
+ fix: isFetch ? "Add a timeout: const controller = new AbortController(); setTimeout(() => controller.abort(), 10000); fetch(url, { signal: controller.signal })" : "Add a timeout: axios.get(url, { timeout: 10000 }) or configure in axios.create({ timeout: 10000 })"
4315
4404
  }];
4316
4405
  }
4317
4406
  };
@@ -4422,111 +4511,113 @@ async function scan(options) {
4422
4511
  }
4423
4512
 
4424
4513
  // src/web-scanner/checks.ts
4425
- function make(id, name, description, maxPoints, severity, status, details) {
4514
+ function make(id, name, description, maxPoints, severity, maturity, status, details) {
4426
4515
  return {
4427
4516
  id,
4428
4517
  name,
4429
4518
  description,
4430
4519
  status,
4431
4520
  severity,
4521
+ maturity,
4432
4522
  details,
4433
4523
  points: status === "pass" ? maxPoints : status === "warn" ? Math.floor(maxPoints / 2) : 0,
4434
4524
  maxPoints
4435
4525
  };
4436
4526
  }
4437
4527
  function checkRobotsTxt(ctx) {
4438
- if (!ctx.robotsTxt) return make("robots_txt", "robots.txt", "robots.txt exists and is accessible", 5, "medium", "fail", "No robots.txt found.");
4439
- return make("robots_txt", "robots.txt", "robots.txt exists and is accessible", 5, "medium", "pass", "robots.txt found.");
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.");
4440
4530
  }
4441
4531
  function checkRobotsAiDirectives(ctx) {
4442
- 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.");
4443
- const agents = ["GPTBot", "ChatGPT-User", "ClaudeBot", "Claude-Web", "Google-Extended", "PerplexityBot", "Bytespider", "CCBot", "anthropic-ai", "cohere-ai"];
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"];
4444
4534
  const found = agents.filter((a) => ctx.robotsTxt.toLowerCase().includes(a.toLowerCase()));
4445
- 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.");
4446
- 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(", ")}.`);
4447
- 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(", ")}.`);
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(", ")}.`);
4448
4538
  }
4449
4539
  function checkContentUsage(ctx) {
4450
4540
  const hasHeader = ctx.headers["content-usage"] != null;
4451
4541
  const hasInRobots = ctx.robotsTxt?.toLowerCase().includes("content-usage") ?? false;
4452
- if (!hasHeader && !hasInRobots) return make("content_usage", "Content-Usage (IETF aipref)", "Content-Usage header or directives", 10, "high", "fail", "No Content-Usage directives found.");
4453
- 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.");
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.");
4454
4544
  }
4455
4545
  function checkLlmsTxt(ctx) {
4456
- if (!ctx.llmsTxt) return make("llms_txt", "llms.txt", "LLM-optimized site summary at /llms.txt", 10, "high", "fail", "No llms.txt found.");
4457
- const lines = ctx.llmsTxt.split("\n").filter((l) => l.trim().length > 0);
4458
- 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.");
4459
- return make("llms_txt", "llms.txt", "LLM-optimized site summary at /llms.txt", 10, "high", "pass", `llms.txt found with ${lines.length} lines.`);
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.`);
4460
4550
  }
4461
4551
  function checkTdmRep(ctx) {
4462
4552
  const hasWK = ctx.tdmRep != null;
4463
4553
  const hasHeader = ctx.headers["tdm-reservation"] != null;
4464
- if (!hasWK && !hasHeader) return make("tdmrep", "TDMRep (W3C)", "Text & data mining reservation file or header", 8, "medium", "fail", "No TDMRep configuration found.");
4465
- 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"]}`);
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"]}`);
4466
4556
  }
4467
4557
  function checkAiDisclosure(ctx) {
4468
- 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.");
4469
- return make("ai_disclosure", "AI-Disclosure Header", "Declares AI involvement in content generation", 5, "low", "pass", `Header: ${ctx.headers["ai-disclosure"]}`);
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"]}`);
4470
4560
  }
4471
4561
  function checkAgentCard(ctx) {
4472
- if (!ctx.agentCard) return make("agent_card", "A2A AgentCard", "/.well-known/agent-card.json for agent discovery", 10, "high", "fail", "No A2A AgentCard found.");
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.");
4473
4563
  try {
4474
4564
  const card = JSON.parse(ctx.agentCard);
4475
- 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.");
4476
- 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).`);
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).`);
4477
4567
  } catch {
4478
- return make("agent_card", "A2A AgentCard", "/.well-known/agent-card.json for agent discovery", 10, "high", "warn", "AgentCard found but contains invalid JSON.");
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.");
4479
4569
  }
4480
4570
  }
4481
4571
  function checkAiTxt(ctx) {
4482
- 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.");
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.");
4483
4573
  const lines = ctx.aiTxt.split("\n").filter((l) => l.trim().length > 0 && !l.trim().startsWith("#"));
4484
- 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.");
4485
- return make("ai_txt", "ai.txt", "AI training permissions per Spawning spec", 5, "medium", "pass", `ai.txt found with ${lines.length} directive(s).`);
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).`);
4486
4576
  }
4487
4577
  function checkWebMCP(ctx) {
4488
- if (!ctx.html) return make("webmcp", "WebMCP Tools", "Chrome 146+ WebMCP tool registration", 10, "high", "info", "Could not check for WebMCP tools.");
4578
+ if (!ctx.html) return make("webmcp", "WebMCP Tools", "Chrome 146+ WebMCP tool registration", 5, "high", "emerging", "info", "Could not check for WebMCP tools.");
4489
4579
  const hasToolname = /toolname=/i.test(ctx.html);
4490
4580
  const hasModelContext = /navigator\.modelContext/i.test(ctx.html) || /registerTool/i.test(ctx.html);
4491
- if (!hasToolname && !hasModelContext) return make("webmcp", "WebMCP Tools", "Chrome 146+ WebMCP tool registration", 10, "high", "fail", "No WebMCP tools detected.");
4581
+ if (!hasToolname && !hasModelContext) return make("webmcp", "WebMCP Tools", "Chrome 146+ WebMCP tool registration", 5, "high", "emerging", "fail", "No WebMCP tools detected.");
4492
4582
  const count = (ctx.html.match(/toolname=/gi) || []).length;
4493
- return make("webmcp", "WebMCP Tools", "Chrome 146+ WebMCP tool registration", 10, "high", "pass", hasToolname ? `${count} form(s) with toolname attributes.` : "registerTool() usage detected.");
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.");
4494
4584
  }
4495
4585
  function checkStructuredData(ctx) {
4496
- if (!ctx.html) return make("structured_data", "Structured Data", "JSON-LD or Schema.org markup", 10, "medium", "info", "Could not fetch page HTML.");
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.");
4497
4587
  const jsonLd = ctx.html.match(/<script[^>]*type=["']application\/ld\+json["'][^>]*>/gi);
4498
4588
  const hasMicrodata = ctx.html.includes("itemscope") && ctx.html.includes("itemtype");
4499
- if (!jsonLd && !hasMicrodata) return make("structured_data", "Structured Data", "JSON-LD or Schema.org markup", 10, "medium", "fail", "No structured data found.");
4500
- 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).`);
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).`);
4501
4591
  }
4502
4592
  function checkOpenGraph(ctx) {
4503
- if (!ctx.html) return make("opengraph", "OpenGraph & Meta", "OG tags and meta description", 7, "low", "info", "Could not fetch HTML.");
4593
+ if (!ctx.html) return make("opengraph", "OpenGraph & Meta", "OG tags and meta description", 10, "low", "established", "info", "Could not fetch HTML.");
4504
4594
  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)];
4505
4595
  const passed = checks.filter(Boolean).length;
4506
- if (passed === 0) return make("opengraph", "OpenGraph & Meta", "OG tags and meta description", 7, "low", "fail", "No OpenGraph tags or meta description.");
4507
- if (passed < 3) return make("opengraph", "OpenGraph & Meta", "OG tags and meta description", 7, "low", "warn", `Found ${passed}/4 meta tags.`);
4508
- return make("opengraph", "OpenGraph & Meta", "OG tags and meta description", 7, "low", "pass", `All ${passed} key meta tags present.`);
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.");
4509
4600
  }
4510
4601
  function checkSitemap(ctx) {
4511
- if (!ctx.sitemapXml) return make("sitemap", "sitemap.xml", "Sitemap exists and is valid", 5, "medium", "fail", "No sitemap.xml found.");
4602
+ if (!ctx.sitemapXml) return make("sitemap", "sitemap.xml", "Sitemap exists and is valid", 10, "medium", "established", "fail", "No sitemap.xml found.");
4512
4603
  const count = Math.max((ctx.sitemapXml.match(/<url>/gi) || []).length, (ctx.sitemapXml.match(/<loc>/gi) || []).length);
4513
- if (count === 0 && /<sitemapindex/i.test(ctx.sitemapXml)) return make("sitemap", "sitemap.xml", "Sitemap exists and is valid", 5, "medium", "pass", "Sitemap index found.");
4514
- if (count === 0) return make("sitemap", "sitemap.xml", "Sitemap exists and is valid", 5, "medium", "warn", "sitemap.xml found but appears empty.");
4515
- return make("sitemap", "sitemap.xml", "Sitemap exists and is valid", 5, "medium", "pass", `sitemap.xml found with ${count} URL(s).`);
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).`);
4516
4607
  }
4517
4608
  function checkHttpSignatures(ctx) {
4518
4609
  const hasDirectory = ctx.httpSigDirectory != null;
4519
4610
  const hasSignatureHeader = ctx.headers["signature"] != null || ctx.headers["signature-input"] != null;
4520
4611
  const hasSignatureAgent = ctx.headers["signature-agent"] != null;
4521
- 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.");
4522
- 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.");
4523
- 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.");
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.");
4524
4615
  }
4525
4616
  function checkPageSpeed(ctx) {
4526
- if (ctx.loadTimeMs == null) return make("page_speed", "Page Load Time", "Response time for AI agent interactions", 5, "low", "info", "Could not measure.");
4527
- 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.`);
4528
- 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.`);
4529
- return make("page_speed", "Page Load Time", "Response time for AI agent interactions", 5, "low", "pass", `${(ctx.loadTimeMs / 1e3).toFixed(1)}s.`);
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.`);
4530
4621
  }
4531
4622
  var allChecks = [
4532
4623
  checkRobotsTxt,
@@ -4565,6 +4656,29 @@ function isPrivateHost(hostname) {
4565
4656
  if (PRIVATE_HOSTNAMES.has(hostname.toLowerCase())) return true;
4566
4657
  const h = hostname.replace(/^\[|\]$/g, "").toLowerCase();
4567
4658
  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
+ }
4568
4682
  const ipv4 = hostname.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
4569
4683
  if (ipv4) {
4570
4684
  const [, a, b] = ipv4.map(Number);
@@ -4577,22 +4691,49 @@ function isPrivateHost(hostname) {
4577
4691
  }
4578
4692
  return false;
4579
4693
  }
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
+ }
4580
4702
  function validateRedirectUrl(responseUrl) {
4581
4703
  try {
4582
4704
  const parsed = new URL(responseUrl);
4705
+ if (!isAllowedProtocol(responseUrl)) return false;
4583
4706
  return !isPrivateHost(parsed.hostname);
4584
4707
  } catch {
4585
4708
  return false;
4586
4709
  }
4587
4710
  }
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
+ }
4588
4729
  async function fetchText(url, timeout = 8e3) {
4589
4730
  try {
4590
4731
  const c = new AbortController();
4591
4732
  const t = setTimeout(() => c.abort(), timeout);
4592
- const r = await fetch(url, { signal: c.signal, headers: { "User-Agent": "Prodlint-WebScanner/1.0" }, redirect: "follow" });
4733
+ const r = await safeFetch(url, timeout, c.signal);
4593
4734
  clearTimeout(t);
4594
- if (r.url && !validateRedirectUrl(r.url)) return null;
4595
- return r.ok ? await r.text() : null;
4735
+ if (!r || !r.ok) return null;
4736
+ return await r.text();
4596
4737
  } catch {
4597
4738
  return null;
4598
4739
  }
@@ -4601,9 +4742,9 @@ async function fetchHeaders(url, timeout = 8e3) {
4601
4742
  try {
4602
4743
  const c = new AbortController();
4603
4744
  const t = setTimeout(() => c.abort(), timeout);
4604
- const r = await fetch(url, { signal: c.signal, headers: { "User-Agent": "Prodlint-WebScanner/1.0" }, redirect: "follow" });
4745
+ const r = await safeFetch(url, timeout, c.signal);
4605
4746
  clearTimeout(t);
4606
- if (r.url && !validateRedirectUrl(r.url)) return {};
4747
+ if (!r) return {};
4607
4748
  const h = {};
4608
4749
  r.headers.forEach((v, k) => {
4609
4750
  h[k.toLowerCase()] = v;
@@ -4618,20 +4759,18 @@ async function fetchWithTiming(url, timeout = 15e3) {
4618
4759
  const c = new AbortController();
4619
4760
  const t = setTimeout(() => c.abort(), timeout);
4620
4761
  const start = Date.now();
4621
- const r = await fetch(url, { signal: c.signal, headers: { "User-Agent": "Prodlint-WebScanner/1.0" }, redirect: "follow" });
4622
- const html = await r.text();
4762
+ const r = await safeFetch(url, timeout, c.signal);
4623
4763
  clearTimeout(t);
4624
- if (r.url && !validateRedirectUrl(r.url)) return { html: null, loadTimeMs: null };
4625
- return r.ok ? { html, loadTimeMs: Date.now() - start } : { html: null, loadTimeMs: null };
4764
+ if (!r || !r.ok) return { html: null, loadTimeMs: null };
4765
+ const html = await r.text();
4766
+ return { html, loadTimeMs: Date.now() - start };
4626
4767
  } catch {
4627
4768
  return { html: null, loadTimeMs: null };
4628
4769
  }
4629
4770
  }
4630
4771
  function normalizeUrl(input) {
4631
4772
  let url = input.trim();
4632
- if (!/^https?:\/\//i.test(url)) {
4633
- url = `https://${url}`;
4634
- }
4773
+ if (!/^https?:\/\//i.test(url)) url = `https://${url}`;
4635
4774
  const parsed = new URL(url);
4636
4775
  return parsed.origin;
4637
4776
  }
@@ -4684,7 +4823,9 @@ server.tool(
4684
4823
  async ({ path, ignore }) => {
4685
4824
  const resolved = resolve4(path);
4686
4825
  const cwd = process.cwd();
4687
- if (!resolved.startsWith(cwd)) {
4826
+ const normalizedResolved = process.platform === "win32" ? resolved.toLowerCase() : resolved;
4827
+ const normalizedCwd = process.platform === "win32" ? cwd.toLowerCase() : cwd;
4828
+ if (normalizedResolved !== normalizedCwd && !normalizedResolved.startsWith(normalizedCwd + sep2)) {
4688
4829
  return {
4689
4830
  content: [{ type: "text", text: `Error: Path must be within the current working directory (${cwd})` }],
4690
4831
  isError: true