prodlint 0.8.1 → 0.9.0

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";
@@ -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
  }
@@ -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
  }
@@ -3477,7 +3537,8 @@ var evalInjectionRule = {
3477
3537
  column: match.index + 1,
3478
3538
  message: msg,
3479
3539
  severity: "critical",
3480
- category: "security"
3540
+ category: "security",
3541
+ fix: "Replace eval/Function constructor with a safe alternative like JSON.parse() or a sandboxed interpreter"
3481
3542
  });
3482
3543
  break;
3483
3544
  }
@@ -4274,44 +4335,57 @@ var clientSideAuthOnlyRule = {
4274
4335
  };
4275
4336
 
4276
4337
  // src/rules/missing-abort-controller.ts
4277
- var FETCH_CALL = /\bfetch\s*\(/;
4338
+ var HTTP_CALL = /\b(fetch|axios)\s*[.(]/;
4278
4339
  var HAS_TIMEOUT = [
4279
4340
  /AbortController/,
4280
4341
  /abort/i,
4281
4342
  /signal\s*:/,
4282
- /timeout/i,
4283
- /setTimeout.*abort/s
4343
+ /timeout\s*:/,
4344
+ /timeout\s*=/,
4345
+ /setTimeout.*abort/s,
4346
+ /axios\.create\s*\([^)]*timeout/s
4284
4347
  ];
4348
+ var BACKGROUND_PATTERN = /\b(setInterval|cron|schedule|createWorker|bullmq|agenda|queue\.process)\b/;
4349
+ function isBackgroundFile(file) {
4350
+ if (BACKGROUND_PATTERN.test(file.content)) return true;
4351
+ const p = file.relativePath.toLowerCase();
4352
+ return /\b(cron|jobs?|workers?|queues?|tasks?|background)\b/.test(p);
4353
+ }
4285
4354
  var missingAbortControllerRule = {
4286
4355
  id: "missing-abort-controller",
4287
4356
  name: "Missing Abort Controller",
4288
- description: "Detects fetch calls in API routes without timeout or AbortController \u2014 requests can hang indefinitely",
4357
+ description: "Detects fetch/axios calls without timeout or AbortController \u2014 requests can hang indefinitely",
4289
4358
  category: "performance",
4290
4359
  severity: "info",
4291
4360
  fileExtensions: ["ts", "tsx", "js", "jsx"],
4292
4361
  check(file, _project) {
4293
4362
  if (isTestFile(file.relativePath)) return [];
4294
- if (!isApiRoute(file.relativePath) && !/['"]use server['"]/.test(file.content)) return [];
4295
- if (!FETCH_CALL.test(file.content)) return [];
4363
+ const isRelevant = isApiRoute(file.relativePath) || /['"]use server['"]/.test(file.content) || isBackgroundFile(file);
4364
+ if (!isRelevant) return [];
4365
+ if (!HTTP_CALL.test(file.content)) return [];
4296
4366
  const hasTimeout = HAS_TIMEOUT.some((p) => p.test(file.content));
4297
4367
  if (hasTimeout) return [];
4298
4368
  let reportLine = 1;
4369
+ let matchedCall = "fetch";
4299
4370
  for (let i = 0; i < file.lines.length; i++) {
4300
4371
  if (isCommentLine(file.lines, i, file.commentMap)) continue;
4301
- if (FETCH_CALL.test(file.lines[i])) {
4372
+ const m = file.lines[i].match(HTTP_CALL);
4373
+ if (m) {
4302
4374
  reportLine = i + 1;
4375
+ matchedCall = m[1];
4303
4376
  break;
4304
4377
  }
4305
4378
  }
4379
+ const isFetch = matchedCall === "fetch";
4306
4380
  return [{
4307
4381
  ruleId: "missing-abort-controller",
4308
4382
  file: file.relativePath,
4309
4383
  line: reportLine,
4310
4384
  column: 1,
4311
- message: "fetch() without timeout or AbortController \u2014 request will hang indefinitely if the upstream server doesn't respond",
4385
+ message: `${matchedCall}() without timeout or AbortController \u2014 request will hang indefinitely if the upstream server doesn't respond`,
4312
4386
  severity: "info",
4313
4387
  category: "performance",
4314
- fix: "Add a timeout: const controller = new AbortController(); setTimeout(() => controller.abort(), 10000); fetch(url, { signal: controller.signal })"
4388
+ 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
4389
  }];
4316
4390
  }
4317
4391
  };
@@ -4684,7 +4758,9 @@ server.tool(
4684
4758
  async ({ path, ignore }) => {
4685
4759
  const resolved = resolve4(path);
4686
4760
  const cwd = process.cwd();
4687
- if (!resolved.startsWith(cwd)) {
4761
+ const normalizedResolved = process.platform === "win32" ? resolved.toLowerCase() : resolved;
4762
+ const normalizedCwd = process.platform === "win32" ? cwd.toLowerCase() : cwd;
4763
+ if (normalizedResolved !== normalizedCwd && !normalizedResolved.startsWith(normalizedCwd + sep2)) {
4688
4764
  return {
4689
4765
  content: [{ type: "text", text: `Error: Path must be within the current working directory (${cwd})` }],
4690
4766
  isError: true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prodlint",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "Production readiness for vibe-coded apps — know your AI code is ready to ship",
5
5
  "license": "MIT",
6
6
  "author": "prodlint contributors",