prodlint 0.10.4 → 0.10.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/action.yml +1 -1
- package/dist/cli.js +584 -78
- package/dist/index.d.ts +4 -0
- package/dist/index.js +584 -78
- package/dist/mcp.js +583 -77
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -81,7 +81,8 @@ function isLineSuppressed(lines, lineIndex, ruleId) {
|
|
|
81
81
|
return false;
|
|
82
82
|
}
|
|
83
83
|
function isTestFile(relativePath) {
|
|
84
|
-
return /\.(test|spec)\.[jt]sx?$/.test(relativePath) ||
|
|
84
|
+
return /\.(test|spec)\.[jt]sx?$/.test(relativePath) || // Jest's __tests__ and __mocks__, jscodeshift's __testfixtures__
|
|
85
|
+
/(?:^|\/)__(?:tests|testfixtures|fixtures|mocks)__\//.test(relativePath) || /(?:^|\/)tests?\//.test(relativePath) || /(?:^|\/)fixtures?\//.test(relativePath) || /(?:^|\/)mocks?\//.test(relativePath);
|
|
85
86
|
}
|
|
86
87
|
function isScriptFile(relativePath) {
|
|
87
88
|
if (/(?:^|\/)scripts?\//.test(relativePath)) return true;
|
|
@@ -395,6 +396,263 @@ var DEPENDENCY_TO_FRAMEWORK = {
|
|
|
395
396
|
var SQL_SAFE_ORMS = /* @__PURE__ */ new Set(["prisma", "drizzle", "knex", "typeorm", "sequelize"]);
|
|
396
397
|
var RATE_LIMIT_FRAMEWORKS = /* @__PURE__ */ new Set(["upstash-ratelimit", "express-rate-limit", "rate-limiter-flexible"]);
|
|
397
398
|
|
|
399
|
+
// src/utils/middleware-matcher.ts
|
|
400
|
+
function matcherExcludesApi(content, fileName) {
|
|
401
|
+
const sources = matcherSources(content, fileName);
|
|
402
|
+
if (!sources || sources.length === 0) return false;
|
|
403
|
+
return sources.every((source) => {
|
|
404
|
+
const matcher = compile(source);
|
|
405
|
+
return matcher !== null && !matcher.patterns.some(namesApi) && !apiPaths(matcher.literals).some((path) => matcher.regexp.test(path));
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
function matcherExcludesPages(content, fileName) {
|
|
409
|
+
const sources = matcherSources(content, fileName);
|
|
410
|
+
if (!sources || sources.length === 0) return false;
|
|
411
|
+
return sources.every((source) => {
|
|
412
|
+
const matcher = compile(source);
|
|
413
|
+
return matcher !== null && underApi(matcher.tokens);
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
function underApi(tokens) {
|
|
417
|
+
const [head, ...rest] = tokens;
|
|
418
|
+
if (typeof head !== "string") return false;
|
|
419
|
+
if (head.startsWith("/api/")) return true;
|
|
420
|
+
if (head !== "/api") return false;
|
|
421
|
+
for (const token of rest) {
|
|
422
|
+
const start = typeof token === "string" ? token : token.pattern ? prefixOf(token) : token.prefix + token.suffix;
|
|
423
|
+
if (!start.startsWith("/")) return false;
|
|
424
|
+
if (typeof token === "string" || token.modifier !== "?" && token.modifier !== "*") return true;
|
|
425
|
+
}
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
428
|
+
function apiPaths(literals) {
|
|
429
|
+
const paths = ["/api/x", "/api/x/y"];
|
|
430
|
+
for (const literal2 of literals) {
|
|
431
|
+
for (const head of ["", "/api", "/api/x"]) {
|
|
432
|
+
for (const tail of ["", "/x"]) paths.push(head + literal2 + tail);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return paths.filter((path) => path.startsWith("/api/"));
|
|
436
|
+
}
|
|
437
|
+
function matcherSources(content, fileName) {
|
|
438
|
+
const ast = parseFile(content, fileName);
|
|
439
|
+
if (!ast) return null;
|
|
440
|
+
for (const st of ast.program.body) {
|
|
441
|
+
if (st.type !== "ExportNamedDeclaration" || st.declaration?.type !== "VariableDeclaration") continue;
|
|
442
|
+
if (st.declaration.kind !== "const") continue;
|
|
443
|
+
for (const d of st.declaration.declarations) {
|
|
444
|
+
if (d.id.type !== "Identifier" || d.id.name !== "config" || !d.init) continue;
|
|
445
|
+
const config = unwrap(d.init);
|
|
446
|
+
if (config.type !== "ObjectExpression") return null;
|
|
447
|
+
if (config.properties.some((p) => p.type !== "ObjectProperty")) return null;
|
|
448
|
+
const matcher = property(config, "matcher");
|
|
449
|
+
if (!matcher) return void 0;
|
|
450
|
+
const values = matcher.type === "ArrayExpression" ? matcher.elements : [matcher];
|
|
451
|
+
const sources = [];
|
|
452
|
+
for (const value of values) {
|
|
453
|
+
const element = value ? unwrap(value) : null;
|
|
454
|
+
const source = element?.type === "ObjectExpression" ? property(element, "source") : element;
|
|
455
|
+
const text = source ? literal(source) : null;
|
|
456
|
+
if (text === null) return null;
|
|
457
|
+
sources.push(text);
|
|
458
|
+
}
|
|
459
|
+
return sources;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return void 0;
|
|
463
|
+
}
|
|
464
|
+
function unwrap(node) {
|
|
465
|
+
while (node.type === "TSSatisfiesExpression" || node.type === "TSAsExpression") node = node.expression;
|
|
466
|
+
return node;
|
|
467
|
+
}
|
|
468
|
+
function property(object, name) {
|
|
469
|
+
if (object.type !== "ObjectExpression") return void 0;
|
|
470
|
+
let value;
|
|
471
|
+
for (const p of object.properties) {
|
|
472
|
+
if (p.type !== "ObjectProperty" || p.computed) continue;
|
|
473
|
+
const key = p.key.type === "Identifier" ? p.key.name : p.key.type === "StringLiteral" ? p.key.value : null;
|
|
474
|
+
if (key === name) value = unwrap(p.value);
|
|
475
|
+
}
|
|
476
|
+
return value;
|
|
477
|
+
}
|
|
478
|
+
function literal(node) {
|
|
479
|
+
if (node.type === "StringLiteral") return node.value;
|
|
480
|
+
if (node.type === "TemplateLiteral" && node.expressions.length === 0) return node.quasis[0].value.cooked ?? null;
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
function namesApi(pattern) {
|
|
484
|
+
let kept = "";
|
|
485
|
+
const negative = [];
|
|
486
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
487
|
+
const c = pattern[i];
|
|
488
|
+
const skipping = negative.includes(true);
|
|
489
|
+
if (c === "\\") {
|
|
490
|
+
if (!skipping) kept += pattern.slice(i, i + 2);
|
|
491
|
+
i++;
|
|
492
|
+
} else if (c === "(") {
|
|
493
|
+
negative.push(pattern.startsWith("(?!", i) || pattern.startsWith("(?<!", i));
|
|
494
|
+
if (!skipping) kept += c;
|
|
495
|
+
} else if (c === ")") {
|
|
496
|
+
negative.pop();
|
|
497
|
+
if (!negative.includes(true)) kept += c;
|
|
498
|
+
} else if (!skipping) {
|
|
499
|
+
kept += c;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return /\bapi\b/i.test(kept);
|
|
503
|
+
}
|
|
504
|
+
var DELIMITER = "/#?";
|
|
505
|
+
var PREFIXES = "./";
|
|
506
|
+
var escape = (s) => s.replace(/([.+*?=^!:${}()[\]|/\\])/g, "\\$1");
|
|
507
|
+
var prefixOf = (token) => (token.modifier === "*" || token.modifier === "+") && !token.prefix && !token.suffix ? "/" : token.prefix;
|
|
508
|
+
function lex(source) {
|
|
509
|
+
const tokens = [];
|
|
510
|
+
let i = 0;
|
|
511
|
+
while (i < source.length) {
|
|
512
|
+
const c = source[i];
|
|
513
|
+
if (c === "*" || c === "+" || c === "?") {
|
|
514
|
+
tokens.push({ type: "MODIFIER", value: source[i++] });
|
|
515
|
+
} else if (c === "\\") {
|
|
516
|
+
tokens.push({ type: "ESCAPED_CHAR", value: source[i + 1] });
|
|
517
|
+
i += 2;
|
|
518
|
+
} else if (c === "{" || c === "}") {
|
|
519
|
+
tokens.push({ type: c === "{" ? "OPEN" : "CLOSE", value: source[i++] });
|
|
520
|
+
} else if (c === ":") {
|
|
521
|
+
const name = /^\w+/.exec(source.slice(i + 1))?.[0];
|
|
522
|
+
if (!name) throw new TypeError(`Missing parameter name at ${i}`);
|
|
523
|
+
tokens.push({ type: "NAME", value: name });
|
|
524
|
+
i += 1 + name.length;
|
|
525
|
+
} else if (c === "(") {
|
|
526
|
+
let depth = 1;
|
|
527
|
+
let pattern = "";
|
|
528
|
+
let j = i + 1;
|
|
529
|
+
if (source[j] === "?") throw new TypeError(`Pattern cannot start with "?" at ${j}`);
|
|
530
|
+
while (j < source.length) {
|
|
531
|
+
if (source[j] === "\\") {
|
|
532
|
+
pattern += source[j++] + source[j++];
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
if (source[j] === ")") {
|
|
536
|
+
if (--depth === 0) {
|
|
537
|
+
j++;
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
} else if (source[j] === "(") {
|
|
541
|
+
depth++;
|
|
542
|
+
if (source[j + 1] !== "?") throw new TypeError(`Capturing groups are not allowed at ${j}`);
|
|
543
|
+
}
|
|
544
|
+
pattern += source[j++];
|
|
545
|
+
}
|
|
546
|
+
if (depth) throw new TypeError(`Unbalanced pattern at ${i}`);
|
|
547
|
+
if (!pattern) throw new TypeError(`Missing pattern at ${i}`);
|
|
548
|
+
tokens.push({ type: "PATTERN", value: pattern });
|
|
549
|
+
i = j;
|
|
550
|
+
} else {
|
|
551
|
+
tokens.push({ type: "CHAR", value: source[i++] });
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
tokens.push({ type: "END", value: "" });
|
|
555
|
+
return tokens;
|
|
556
|
+
}
|
|
557
|
+
function parse2(source) {
|
|
558
|
+
const tokens = lex(source);
|
|
559
|
+
const result = [];
|
|
560
|
+
let i = 0;
|
|
561
|
+
let path = "";
|
|
562
|
+
const tryConsume = (type) => i < tokens.length && tokens[i].type === type ? tokens[i++].value : void 0;
|
|
563
|
+
const mustConsume = (type) => {
|
|
564
|
+
const value = tryConsume(type);
|
|
565
|
+
if (value === void 0) throw new TypeError(`Unexpected ${tokens[i].type}, expected ${type}`);
|
|
566
|
+
return value;
|
|
567
|
+
};
|
|
568
|
+
const consumeText = () => {
|
|
569
|
+
let text = "";
|
|
570
|
+
let value;
|
|
571
|
+
while (value = tryConsume("CHAR") || tryConsume("ESCAPED_CHAR")) text += value;
|
|
572
|
+
return text;
|
|
573
|
+
};
|
|
574
|
+
const safePattern = (prefix) => {
|
|
575
|
+
const prev = result[result.length - 1];
|
|
576
|
+
const prevText = prefix || (typeof prev === "string" ? prev : "");
|
|
577
|
+
if (prev && !prevText) throw new TypeError("Must have text between two parameters");
|
|
578
|
+
if (!prevText || [...DELIMITER].some((c) => prevText.includes(c))) return `[^${escape(DELIMITER)}]+?`;
|
|
579
|
+
return `(?:(?!${escape(prevText)})[^${escape(DELIMITER)}])+?`;
|
|
580
|
+
};
|
|
581
|
+
while (i < tokens.length) {
|
|
582
|
+
const char = tryConsume("CHAR");
|
|
583
|
+
const name = tryConsume("NAME");
|
|
584
|
+
const pattern = tryConsume("PATTERN");
|
|
585
|
+
if (name || pattern) {
|
|
586
|
+
let prefix = char || "";
|
|
587
|
+
if (!PREFIXES.includes(prefix)) {
|
|
588
|
+
path += prefix;
|
|
589
|
+
prefix = "";
|
|
590
|
+
}
|
|
591
|
+
if (path) {
|
|
592
|
+
result.push(path);
|
|
593
|
+
path = "";
|
|
594
|
+
}
|
|
595
|
+
result.push({ prefix, suffix: "", pattern: pattern || safePattern(prefix), modifier: tryConsume("MODIFIER") || "" });
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
const value = char || tryConsume("ESCAPED_CHAR");
|
|
599
|
+
if (value) {
|
|
600
|
+
path += value;
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
if (path) {
|
|
604
|
+
result.push(path);
|
|
605
|
+
path = "";
|
|
606
|
+
}
|
|
607
|
+
if (tryConsume("OPEN")) {
|
|
608
|
+
const prefix = consumeText();
|
|
609
|
+
const name2 = tryConsume("NAME") || "";
|
|
610
|
+
const pattern2 = tryConsume("PATTERN") || "";
|
|
611
|
+
const suffix = consumeText();
|
|
612
|
+
mustConsume("CLOSE");
|
|
613
|
+
result.push({ prefix, suffix, pattern: name2 && !pattern2 ? safePattern(prefix) : pattern2, modifier: tryConsume("MODIFIER") || "" });
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
mustConsume("END");
|
|
617
|
+
}
|
|
618
|
+
return result;
|
|
619
|
+
}
|
|
620
|
+
function compile(source) {
|
|
621
|
+
if (!source.startsWith("/")) return null;
|
|
622
|
+
try {
|
|
623
|
+
const tokens = parse2(source);
|
|
624
|
+
let route = "^";
|
|
625
|
+
for (const token of tokens) {
|
|
626
|
+
if (typeof token === "string") {
|
|
627
|
+
route += escape(token);
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
if (!token.pattern) {
|
|
631
|
+
route += `(?:${escape(token.prefix)}${escape(token.suffix)})${token.modifier}`;
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
const repeat = token.modifier === "*" || token.modifier === "+";
|
|
635
|
+
const prefix = escape(prefixOf(token));
|
|
636
|
+
const suffix = escape(token.suffix);
|
|
637
|
+
if (!prefix && !suffix) {
|
|
638
|
+
route += `(${token.pattern})${token.modifier}`;
|
|
639
|
+
} else if (repeat) {
|
|
640
|
+
route += `(?:${prefix}((?:${token.pattern})(?:${suffix}${prefix}(?:${token.pattern}))*)${suffix})${token.modifier === "*" ? "?" : ""}`;
|
|
641
|
+
} else {
|
|
642
|
+
route += `(?:${prefix}(${token.pattern})${suffix})${token.modifier}`;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return {
|
|
646
|
+
regexp: new RegExp(`${route}[${escape(DELIMITER)}]?$`),
|
|
647
|
+
tokens,
|
|
648
|
+
literals: tokens.filter((token) => typeof token === "string"),
|
|
649
|
+
patterns: tokens.flatMap((token) => typeof token === "string" ? [] : [token.pattern])
|
|
650
|
+
};
|
|
651
|
+
} catch {
|
|
652
|
+
return null;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
398
656
|
// src/utils/file-walker.ts
|
|
399
657
|
var DEFAULT_IGNORES = [
|
|
400
658
|
"**/node_modules/**",
|
|
@@ -424,6 +682,37 @@ var SCAN_EXTENSIONS = [
|
|
|
424
682
|
];
|
|
425
683
|
var AST_EXTENSIONS = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "mjs", "cjs"]);
|
|
426
684
|
var MAX_FILE_SIZE = 1024 * 1024;
|
|
685
|
+
var MIDDLEWARE_FILES = [
|
|
686
|
+
"middleware.ts",
|
|
687
|
+
"middleware.js",
|
|
688
|
+
"src/middleware.ts",
|
|
689
|
+
"src/middleware.js",
|
|
690
|
+
"proxy.ts",
|
|
691
|
+
"proxy.js",
|
|
692
|
+
"src/proxy.ts",
|
|
693
|
+
"src/proxy.js"
|
|
694
|
+
];
|
|
695
|
+
var MIDDLEWARE_AUTH_PATTERNS = [
|
|
696
|
+
/getSession/i,
|
|
697
|
+
/getUser/i,
|
|
698
|
+
/auth\(\)/,
|
|
699
|
+
/withAuth/i,
|
|
700
|
+
/clerkMiddleware/i,
|
|
701
|
+
/authMiddleware/i,
|
|
702
|
+
/NextAuth/i,
|
|
703
|
+
/supabase.*auth/i,
|
|
704
|
+
/createMiddlewareClient/i,
|
|
705
|
+
/getToken/i,
|
|
706
|
+
/verifyToken/i,
|
|
707
|
+
/jwt/i,
|
|
708
|
+
/updateSession/i,
|
|
709
|
+
// Auth.js: export { auth as proxy } from '@/auth', export default auth((req) => …)
|
|
710
|
+
/\bauth\s+as\s+(?:proxy|middleware|default)\b/,
|
|
711
|
+
/\bauth\(/,
|
|
712
|
+
// NextAuth v4: export { default } from 'next-auth/middleware'
|
|
713
|
+
/['"]next-auth\/middleware['"]/
|
|
714
|
+
];
|
|
715
|
+
var MIDDLEWARE_RATE_LIMIT_PATTERNS = [/rateLimit/i, /throttle/i];
|
|
427
716
|
async function walkFiles(root, extraIgnores = []) {
|
|
428
717
|
const patterns = SCAN_EXTENSIONS.map((ext) => `**/*.${ext}`);
|
|
429
718
|
patterns.push("**/.env", "**/.env.*");
|
|
@@ -562,6 +851,8 @@ async function buildProjectContext(root, files) {
|
|
|
562
851
|
let declaredDependencies = /* @__PURE__ */ new Set();
|
|
563
852
|
let tsconfigPaths = /* @__PURE__ */ new Set();
|
|
564
853
|
let hasAuthMiddleware = false;
|
|
854
|
+
let authMiddlewareExcludesApi = false;
|
|
855
|
+
let authMiddlewareExcludesPages = false;
|
|
565
856
|
let hasRateLimiting = false;
|
|
566
857
|
const detectedFrameworks = /* @__PURE__ */ new Set();
|
|
567
858
|
let gitignoreContent = null;
|
|
@@ -613,45 +904,19 @@ async function buildProjectContext(root, files) {
|
|
|
613
904
|
}
|
|
614
905
|
} catch {
|
|
615
906
|
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
/getUser/i,
|
|
623
|
-
/auth\(\)/,
|
|
624
|
-
/withAuth/i,
|
|
625
|
-
/clerkMiddleware/i,
|
|
626
|
-
/authMiddleware/i,
|
|
627
|
-
/NextAuth/i,
|
|
628
|
-
/supabase.*auth/i,
|
|
629
|
-
/createMiddlewareClient/i,
|
|
630
|
-
/getToken/i,
|
|
631
|
-
/verifyToken/i,
|
|
632
|
-
/jwt/i,
|
|
633
|
-
/updateSession/i
|
|
634
|
-
];
|
|
635
|
-
if (authPatterns.some((p) => p.test(content))) {
|
|
636
|
-
hasAuthMiddleware = true;
|
|
637
|
-
break;
|
|
638
|
-
}
|
|
639
|
-
} catch {
|
|
640
|
-
}
|
|
907
|
+
for (const name of MIDDLEWARE_FILES) {
|
|
908
|
+
let content;
|
|
909
|
+
try {
|
|
910
|
+
content = await readFile(resolve(root, name), "utf-8");
|
|
911
|
+
} catch {
|
|
912
|
+
continue;
|
|
641
913
|
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
try {
|
|
647
|
-
const content = await readFile(resolve(root, name), "utf-8");
|
|
648
|
-
if (/rateLimit/i.test(content) || /throttle/i.test(content)) {
|
|
649
|
-
hasRateLimiting = true;
|
|
650
|
-
break;
|
|
651
|
-
}
|
|
652
|
-
} catch {
|
|
653
|
-
}
|
|
914
|
+
if (MIDDLEWARE_AUTH_PATTERNS.some((p) => p.test(content))) {
|
|
915
|
+
authMiddlewareExcludesApi = (!hasAuthMiddleware || authMiddlewareExcludesApi) && matcherExcludesApi(content, name);
|
|
916
|
+
authMiddlewareExcludesPages = (!hasAuthMiddleware || authMiddlewareExcludesPages) && matcherExcludesPages(content, name);
|
|
917
|
+
hasAuthMiddleware = true;
|
|
654
918
|
}
|
|
919
|
+
if (MIDDLEWARE_RATE_LIMIT_PATTERNS.some((p) => p.test(content))) hasRateLimiting = true;
|
|
655
920
|
}
|
|
656
921
|
try {
|
|
657
922
|
gitignoreContent = await readFile(resolve(root, ".gitignore"), "utf-8");
|
|
@@ -664,6 +929,8 @@ async function buildProjectContext(root, files) {
|
|
|
664
929
|
declaredDependencies,
|
|
665
930
|
tsconfigPaths,
|
|
666
931
|
hasAuthMiddleware,
|
|
932
|
+
authMiddlewareExcludesApi,
|
|
933
|
+
authMiddlewareExcludesPages,
|
|
667
934
|
hasRateLimiting,
|
|
668
935
|
detectedFrameworks,
|
|
669
936
|
gitignoreContent,
|
|
@@ -981,6 +1248,15 @@ var AUTH_PATTERNS = [
|
|
|
981
1248
|
var MUTATION_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
982
1249
|
var HANDLER_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"]);
|
|
983
1250
|
var TRPC_ROUTE = /['"]@trpc\/server\/adapters\//;
|
|
1251
|
+
function servedUnderApi(relativePath) {
|
|
1252
|
+
const pages = /(?:^|\/)pages\/api\/(.+)$/.exec(relativePath);
|
|
1253
|
+
if (pages) return !/^index\.\w+$/.test(pages[1]);
|
|
1254
|
+
const segments = relativePath.split("/");
|
|
1255
|
+
const app = segments.indexOf("app");
|
|
1256
|
+
if (app === -1) return false;
|
|
1257
|
+
const url = segments.slice(app + 1, -1).filter((s) => !/^\(.*\)$/.test(s) && !s.startsWith("@"));
|
|
1258
|
+
return url[0] === "api" && url.length > 1;
|
|
1259
|
+
}
|
|
984
1260
|
var AUTH_WORD = /^(?:auth(?!or(?:s|ed|ing|ship)?$)|session)/i;
|
|
985
1261
|
var NO_AUTH_WORD = /^(?:without|no|non|skip|bypass|optional|public|anon|anonymous|guest)$/i;
|
|
986
1262
|
function words(name) {
|
|
@@ -1189,8 +1465,10 @@ var authChecksRule = {
|
|
|
1189
1465
|
const handlers = file.ast ? handlersFromAst(file.ast.program) : handlersFromText(file);
|
|
1190
1466
|
const exposed = handlers.filter((h) => !h.authed);
|
|
1191
1467
|
if (handlers.length > 0 && exposed.length === 0) return [];
|
|
1468
|
+
const middlewareSkipsRoute = project.hasAuthMiddleware && project.authMiddlewareExcludesApi === true && servedUnderApi(file.relativePath);
|
|
1469
|
+
const middlewareAuth = project.hasAuthMiddleware && !middlewareSkipsRoute;
|
|
1192
1470
|
let severity;
|
|
1193
|
-
if (
|
|
1471
|
+
if (middlewareAuth) {
|
|
1194
1472
|
severity = "info";
|
|
1195
1473
|
} else if (exposed.some((h) => h.local && MUTATION_METHODS.has(h.method)) && !TRPC_ROUTE.test(file.content)) {
|
|
1196
1474
|
severity = "critical";
|
|
@@ -1198,7 +1476,7 @@ var authChecksRule = {
|
|
|
1198
1476
|
severity = "info";
|
|
1199
1477
|
}
|
|
1200
1478
|
const handlerLine = exposed[0]?.line ?? 1;
|
|
1201
|
-
const message =
|
|
1479
|
+
const message = middlewareAuth ? "API route has no inline auth check (middleware auth detected \u2014 verify coverage)" : middlewareSkipsRoute ? "API route has no authentication check (middleware auth detected, but its matcher excludes /api)" : "API route has no authentication check";
|
|
1202
1480
|
return [{
|
|
1203
1481
|
ruleId: "auth-checks",
|
|
1204
1482
|
file: file.relativePath,
|
|
@@ -2602,16 +2880,190 @@ var codebaseConsistencyRule = {
|
|
|
2602
2880
|
};
|
|
2603
2881
|
|
|
2604
2882
|
// src/rules/dead-exports.ts
|
|
2605
|
-
function isEntryPoint(relativePath) {
|
|
2883
|
+
function isEntryPoint(relativePath, appRoots, routeModules) {
|
|
2606
2884
|
const name = relativePath.split("/").pop() ?? "";
|
|
2607
|
-
return /^(page|layout|loading|error|not-found|route|
|
|
2885
|
+
return /^(page|layout|loading|error|not-found|route|opengraph-image|twitter-image|icon|apple-icon|sitemap|robots|manifest)\.(tsx?|jsx?)$/.test(name) || isBarrel(relativePath) || name === "global-error.tsx" || name === "global-error.jsx" || // SvelteKit's route files: +page.ts, +page.server.ts, +layout.ts, +server.ts
|
|
2886
|
+
/^\+(page|layout|server)(\.server)?\.(ts|js)$/.test(name) || // Storybook's stories: it reads a CSF file's default export as the meta, and each named
|
|
2887
|
+
// export as a story
|
|
2888
|
+
/\.stories\.(tsx?|jsx?|mjs)$/.test(name) || isNextRootFile(relativePath, appRoots) || isPagesRoute(relativePath, appRoots) || routeModules.has(relativePath);
|
|
2889
|
+
}
|
|
2890
|
+
function isBarrel(relativePath) {
|
|
2891
|
+
return /(?:^|\/)index\.(tsx?|jsx?)$/.test(relativePath);
|
|
2892
|
+
}
|
|
2893
|
+
function isAppFolder(dir, appRoots) {
|
|
2894
|
+
return appRoots.has(dir) || appRoots.has(dir.replace(/(?:^|\/)src$/, ""));
|
|
2895
|
+
}
|
|
2896
|
+
function isNextRootFile(relativePath, appRoots) {
|
|
2897
|
+
const match = /^(?:(.*)\/)?(?:middleware|proxy|instrumentation|instrumentation-client)\.(tsx?|jsx?)$/.exec(relativePath);
|
|
2898
|
+
return match !== null && isAppFolder(match[1] ?? "", appRoots);
|
|
2899
|
+
}
|
|
2900
|
+
function isPagesRoute(relativePath, appRoots) {
|
|
2901
|
+
if (!/\.(tsx?|jsx?)$/.test(relativePath)) return false;
|
|
2902
|
+
const parts = relativePath.split("/");
|
|
2903
|
+
return parts.some((part, i) => part === "pages" && isAppFolder(parts.slice(0, i).join("/"), appRoots));
|
|
2904
|
+
}
|
|
2905
|
+
function findAppRoots(paths) {
|
|
2906
|
+
const roots = /* @__PURE__ */ new Set([""]);
|
|
2907
|
+
for (const path of paths) {
|
|
2908
|
+
const match = /^(?:(.*)\/)?package\.json$/.exec(path);
|
|
2909
|
+
if (match) roots.add(match[1] ?? "");
|
|
2910
|
+
}
|
|
2911
|
+
return roots;
|
|
2912
|
+
}
|
|
2913
|
+
function findRouteModules(files, paths, appRoots) {
|
|
2914
|
+
const appFolders = [];
|
|
2915
|
+
for (const path of paths) {
|
|
2916
|
+
const match = /^(?:(.*)\/)?app\/root\.(tsx?|jsx?)$/.exec(path);
|
|
2917
|
+
if (match && appRoots.has(match[1] ?? "")) appFolders.push(path.slice(0, path.lastIndexOf("/")));
|
|
2918
|
+
}
|
|
2919
|
+
const modules = /* @__PURE__ */ new Set();
|
|
2920
|
+
for (const { relativePath, content } of files) {
|
|
2921
|
+
const folder = appFolders.find((f) => relativePath.startsWith(`${f}/`));
|
|
2922
|
+
if (folder === void 0) continue;
|
|
2923
|
+
const path = relativePath.slice(folder.length + 1);
|
|
2924
|
+
if (/^(?:root|entry\.server|routes\/.+)\.(tsx?|jsx?)$/.test(path)) {
|
|
2925
|
+
modules.add(relativePath);
|
|
2926
|
+
} else if (/^routes\.(?:[cm]?[jt]s|[jt]sx)$/.test(path)) {
|
|
2927
|
+
for (const match of content.matchAll(/['"`]([^'"`\n]+\.(?:tsx?|jsx?))['"`]/g)) {
|
|
2928
|
+
const module = normalizePath(`${folder}/${match[1]}`);
|
|
2929
|
+
if (module !== null) modules.add(module);
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
return modules;
|
|
2608
2934
|
}
|
|
2609
2935
|
var IMPORT_NAMED = /\bimport\s*(?:type\s*)?(?:[\w$]+\s*,\s*)?\{([^}]*)\}\s*from\s*['"]([^'"]+)['"]/g;
|
|
2610
2936
|
var IMPORT_DEFAULT = /\bimport\s+([\w$]+)\s*(?:,\s*\{[^}]*\}\s*)?from\s*['"]([^'"]+)['"]/g;
|
|
2611
2937
|
var IMPORT_NAMESPACE = /\bimport\s+(?:[\w$]+\s*,\s*)?\*\s*as\s+[\w$]+\s+from\s*['"]([^'"]+)['"]/g;
|
|
2612
|
-
var
|
|
2613
|
-
|
|
2614
|
-
|
|
2938
|
+
var DYNAMIC_IMPORT = /(\btypeof\s+)?(?:\{([^{}]*)\}\s*=\s*await\s+)?\bimport\s*\(\s*['"`]([^'"`]+)['"`]\s*\)(?:\s*\.\s*([\w$]+))?/g;
|
|
2939
|
+
var REQUIRE = /(?:\{([^{}]*)\}\s*=\s*)?\brequire\s*\(\s*['"]([^'"]+)['"]\s*\)(?:\s*\.\s*([\w$]+))?/g;
|
|
2940
|
+
var REEXPORT_NAMED = /\bexport\s*(?:type\s*)?\{([^}]*)\}\s*from\s*['"]([^'"]+)['"]/g;
|
|
2941
|
+
var REEXPORT_ALL = /\bexport\s*\*\s*(?:as\s+([\w$]+)\s*)?from\s*['"]([^'"]+)['"]/g;
|
|
2942
|
+
var MODULE_EXTENSIONS = ["ts", "tsx", "js", "jsx", "mjs", "cjs"];
|
|
2943
|
+
var MODULE_EXTENSION = /\.(?:[cm]?[jt]s|[jt]sx)$/;
|
|
2944
|
+
function parseSpecifiers(list) {
|
|
2945
|
+
return list.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").split(",").map((s) => s.trim().replace(/^type\s+/, "")).filter(Boolean).map((s) => {
|
|
2946
|
+
const [name, alias] = s.split(/\s+as\s+/).map((part) => part.trim());
|
|
2947
|
+
return [name, alias ?? name];
|
|
2948
|
+
});
|
|
2949
|
+
}
|
|
2950
|
+
function normalizePath(path) {
|
|
2951
|
+
const parts = [];
|
|
2952
|
+
for (const part of path.split("/")) {
|
|
2953
|
+
if (part === "..") {
|
|
2954
|
+
if (parts.length === 0) return null;
|
|
2955
|
+
parts.pop();
|
|
2956
|
+
} else if (part && part !== ".") {
|
|
2957
|
+
parts.push(part);
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
return parts.join("/");
|
|
2961
|
+
}
|
|
2962
|
+
var EXTERNAL = { files: [], folders: [] };
|
|
2963
|
+
var UNKNOWN = { files: [], folders: [""] };
|
|
2964
|
+
function isAsset(source) {
|
|
2965
|
+
const name = source.slice(source.lastIndexOf("/") + 1);
|
|
2966
|
+
return /^[^.].*\.\w+$/.test(name) && !MODULE_EXTENSION.test(name);
|
|
2967
|
+
}
|
|
2968
|
+
function stringsIn(value) {
|
|
2969
|
+
if (typeof value === "string") return [value];
|
|
2970
|
+
if (Array.isArray(value)) return value.flatMap(stringsIn);
|
|
2971
|
+
if (value && typeof value === "object") return Object.values(value).flatMap(stringsIn);
|
|
2972
|
+
return [];
|
|
2973
|
+
}
|
|
2974
|
+
function createResolver(files, project) {
|
|
2975
|
+
const modules = new Set(files.filter((f) => MODULE_EXTENSIONS.includes(f.ext)).map((f) => f.relativePath));
|
|
2976
|
+
const byName = /* @__PURE__ */ new Map();
|
|
2977
|
+
for (const path of modules) {
|
|
2978
|
+
const key = path.replace(/\.\w+$/, "").replace(/(?:^|\/)index$/, "");
|
|
2979
|
+
const name = key.slice(key.lastIndexOf("/") + 1);
|
|
2980
|
+
const list = byName.get(name);
|
|
2981
|
+
if (list) list.push({ path, key });
|
|
2982
|
+
else byName.set(name, [{ path, key }]);
|
|
2983
|
+
}
|
|
2984
|
+
const manifests = [];
|
|
2985
|
+
for (const file of files) {
|
|
2986
|
+
if (!/(?:^|\/)package\.json$/.test(file.relativePath)) continue;
|
|
2987
|
+
try {
|
|
2988
|
+
const json = JSON.parse(file.content);
|
|
2989
|
+
if (json && typeof json === "object") manifests.push({ folder: file.relativePath.replace(/\/?package\.json$/, ""), json });
|
|
2990
|
+
} catch {
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
const packages = /* @__PURE__ */ new Map();
|
|
2994
|
+
for (const { folder, json } of manifests) {
|
|
2995
|
+
if (typeof json.name === "string" && json.name) packages.set(json.name, [...packages.get(json.name) ?? [], folder]);
|
|
2996
|
+
}
|
|
2997
|
+
const findModule = (path) => {
|
|
2998
|
+
const stem = path.replace(/\.(?:[cm]?js|jsx)$/, "");
|
|
2999
|
+
const index = path ? `${path}/index` : "index";
|
|
3000
|
+
return [
|
|
3001
|
+
path,
|
|
3002
|
+
// TypeScript's ESM style names the compiled file: ./format.js for format.ts
|
|
3003
|
+
...stem === path ? [] : [`${stem}.ts`, `${stem}.tsx`],
|
|
3004
|
+
...MODULE_EXTENSIONS.map((ext) => `${path}.${ext}`),
|
|
3005
|
+
...MODULE_EXTENSIONS.map((ext) => `${index}.${ext}`)
|
|
3006
|
+
].find((candidate) => modules.has(candidate));
|
|
3007
|
+
};
|
|
3008
|
+
const packageEntries = /* @__PURE__ */ new Set();
|
|
3009
|
+
for (const { folder, json } of manifests) {
|
|
3010
|
+
if (json.private === true) continue;
|
|
3011
|
+
for (const target of stringsIn([json.main, json.module, json.types, json.typings, json.exports, json.bin])) {
|
|
3012
|
+
const isSource = /\.(?:[cm]?ts|tsx)$/.test(target) && !/\.d\.[cm]?ts$/.test(target);
|
|
3013
|
+
const path = isSource || target.includes("*") ? null : normalizePath(`${folder}/${target}`);
|
|
3014
|
+
if (path === null) continue;
|
|
3015
|
+
const stem = path.replace(/\.d\.ts$/, "").replace(MODULE_EXTENSION, "");
|
|
3016
|
+
const entry = findModule(path) ?? findModule(stem) ?? findModule(stem.replace(/(^|\/)(?:dist|build|lib|out|esm|cjs)\//, "$1src/"));
|
|
3017
|
+
if (entry) packageEntries.add(entry);
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
const resolveRelative = (path) => {
|
|
3021
|
+
const normalized = normalizePath(path);
|
|
3022
|
+
if (normalized?.includes("${")) {
|
|
3023
|
+
const parts = normalized.split(/\$\{[^}]*\}/).map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
3024
|
+
const pattern = new RegExp(`^${parts.join(".*")}(?:\\.\\w+)?$`);
|
|
3025
|
+
return { files: [...modules].filter((m) => pattern.test(m)), folders: [] };
|
|
3026
|
+
}
|
|
3027
|
+
const module = normalized === null ? void 0 : findModule(normalized);
|
|
3028
|
+
if (module) return { files: [module], folders: [] };
|
|
3029
|
+
return isAsset(path) ? EXTERNAL : UNKNOWN;
|
|
3030
|
+
};
|
|
3031
|
+
const resolveBare = (source) => {
|
|
3032
|
+
if (/^[a-z][\w+.-]*:/i.test(source)) return EXTERNAL;
|
|
3033
|
+
const name = source.startsWith("@") ? source.split("/").slice(0, 2).join("/") : source.split("/")[0];
|
|
3034
|
+
const folders = packages.get(name);
|
|
3035
|
+
if (folders) {
|
|
3036
|
+
const subpath = source.slice(name.length + 1);
|
|
3037
|
+
const found = folders.map((folder) => {
|
|
3038
|
+
const base = folder ? `${folder}/` : "";
|
|
3039
|
+
return subpath ? findModule(base + subpath) ?? findModule(`${base}src/${subpath}`) : findModule(folder) ?? findModule(`${base}src`);
|
|
3040
|
+
});
|
|
3041
|
+
return { files: found.filter((f) => f !== void 0), folders };
|
|
3042
|
+
}
|
|
3043
|
+
if (NODE_BUILTINS.has(name) || project.declaredDependencies.has(name)) return EXTERNAL;
|
|
3044
|
+
const segments = source.replace(MODULE_EXTENSION, "").split("/").filter(Boolean);
|
|
3045
|
+
const named = byName.get(segments[segments.length - 1] ?? "") ?? [];
|
|
3046
|
+
for (let i = 0; i < segments.length; i++) {
|
|
3047
|
+
const path = segments.slice(i).join("/");
|
|
3048
|
+
const matches = named.filter((m) => m.key === path || m.key.endsWith(`/${path}`));
|
|
3049
|
+
if (matches.length > 0) return i <= 1 ? { files: matches.map((m) => m.path), folders: [] } : UNKNOWN;
|
|
3050
|
+
}
|
|
3051
|
+
return isAsset(source) ? EXTERNAL : UNKNOWN;
|
|
3052
|
+
};
|
|
3053
|
+
const cache = /* @__PURE__ */ new Map();
|
|
3054
|
+
const resolve4 = (source, from) => {
|
|
3055
|
+
const clean = source.replace(/\?.*$/, "");
|
|
3056
|
+
const relative = /^\.\.?(?:\/|$)/.test(clean);
|
|
3057
|
+
const dir = relative ? from.slice(0, from.lastIndexOf("/") + 1) : "";
|
|
3058
|
+
const key = `${dir}\0${clean}`;
|
|
3059
|
+
let target = cache.get(key);
|
|
3060
|
+
if (!target) {
|
|
3061
|
+
target = relative ? resolveRelative(dir + clean) : resolveBare(clean);
|
|
3062
|
+
cache.set(key, target);
|
|
3063
|
+
}
|
|
3064
|
+
return target;
|
|
3065
|
+
};
|
|
3066
|
+
return { resolve: resolve4, packageEntries };
|
|
2615
3067
|
}
|
|
2616
3068
|
var THRESHOLD = 5;
|
|
2617
3069
|
var deadExportsRule = {
|
|
@@ -2624,17 +3076,16 @@ var deadExportsRule = {
|
|
|
2624
3076
|
check() {
|
|
2625
3077
|
return [];
|
|
2626
3078
|
},
|
|
2627
|
-
checkProject(files,
|
|
3079
|
+
checkProject(files, project) {
|
|
2628
3080
|
const sourceFiles = files.filter(
|
|
2629
3081
|
(f) => ["ts", "tsx", "js", "jsx"].includes(f.ext) && !isTestFile(f.relativePath) && !isScriptFile(f.relativePath)
|
|
2630
3082
|
);
|
|
3083
|
+
const appRoots = findAppRoots(project.allFiles);
|
|
3084
|
+
const routeModules = findRouteModules(files, project.allFiles, appRoots);
|
|
3085
|
+
const { resolve: resolve4, packageEntries } = createResolver(files, project);
|
|
2631
3086
|
const exports = /* @__PURE__ */ new Map();
|
|
2632
|
-
const imports = /* @__PURE__ */ new Map();
|
|
2633
|
-
const allImportedSymbols = /* @__PURE__ */ new Set();
|
|
2634
|
-
const importedFiles = /* @__PURE__ */ new Set();
|
|
2635
|
-
const namespaceImported = /* @__PURE__ */ new Set();
|
|
2636
3087
|
for (const file of sourceFiles) {
|
|
2637
|
-
if (isEntryPoint(file.relativePath)) continue;
|
|
3088
|
+
if (isEntryPoint(file.relativePath, appRoots, routeModules) || packageEntries.has(file.relativePath)) continue;
|
|
2638
3089
|
let inTemplateLiteral = false;
|
|
2639
3090
|
for (let i = 0; i < file.lines.length; i++) {
|
|
2640
3091
|
const line = file.lines[i];
|
|
@@ -2675,38 +3126,92 @@ var deadExportsRule = {
|
|
|
2675
3126
|
}
|
|
2676
3127
|
}
|
|
2677
3128
|
}
|
|
2678
|
-
const
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
3129
|
+
const contents = new Map(files.map((f) => [f.relativePath, f.content]));
|
|
3130
|
+
const used = /* @__PURE__ */ new Set();
|
|
3131
|
+
const usedInFolder = /* @__PURE__ */ new Map();
|
|
3132
|
+
const reexports = /* @__PURE__ */ new Map();
|
|
3133
|
+
const reexportsOf = (file) => {
|
|
3134
|
+
let list = reexports.get(file);
|
|
3135
|
+
if (!list) {
|
|
3136
|
+
list = [];
|
|
3137
|
+
const content = contents.get(file) ?? "";
|
|
3138
|
+
for (const match of content.matchAll(REEXPORT_NAMED)) {
|
|
3139
|
+
for (const [imported, exported] of parseSpecifiers(match[1])) list.push({ source: match[2], imported, exported });
|
|
3140
|
+
}
|
|
3141
|
+
for (const match of content.matchAll(REEXPORT_ALL)) {
|
|
3142
|
+
list.push(match[1] ? { source: match[2], imported: "*", exported: match[1] } : { source: match[2], imported: "", exported: "*" });
|
|
3143
|
+
}
|
|
3144
|
+
reexports.set(file, list);
|
|
3145
|
+
}
|
|
3146
|
+
return list;
|
|
3147
|
+
};
|
|
3148
|
+
const use = (target, name) => {
|
|
3149
|
+
if (name !== "*" && name !== "default") {
|
|
3150
|
+
for (const folder of target.folders) {
|
|
3151
|
+
const names = usedInFolder.get(folder) ?? /* @__PURE__ */ new Set();
|
|
3152
|
+
names.add(name);
|
|
3153
|
+
usedInFolder.set(folder, names);
|
|
3154
|
+
}
|
|
3155
|
+
}
|
|
3156
|
+
for (const file of target.files) {
|
|
3157
|
+
const key = `${file}::${name}`;
|
|
3158
|
+
if (used.has(key)) continue;
|
|
3159
|
+
used.add(key);
|
|
3160
|
+
for (const { source, imported, exported } of reexportsOf(file)) {
|
|
3161
|
+
if (exported === "*") {
|
|
3162
|
+
if (name !== "default") use(resolve4(source, file), name);
|
|
3163
|
+
} else if (name === "*" || name === exported) {
|
|
3164
|
+
use(resolve4(source, file), imported);
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
};
|
|
3169
|
+
const useLoaded = (target, destructured, member, typeOnly) => {
|
|
3170
|
+
if (member && !["then", "catch", "finally"].includes(member)) {
|
|
3171
|
+
use(target, member);
|
|
3172
|
+
} else if (typeOnly) {
|
|
3173
|
+
return;
|
|
3174
|
+
} else if (destructured !== void 0 && !destructured.includes("...")) {
|
|
3175
|
+
for (const name of destructured.split(",").map((s) => s.split(/[:=]/)[0].trim()).filter(Boolean)) use(target, name);
|
|
3176
|
+
} else {
|
|
3177
|
+
use(target, "*");
|
|
2685
3178
|
}
|
|
2686
3179
|
};
|
|
2687
3180
|
for (const file of files) {
|
|
3181
|
+
const from = file.relativePath;
|
|
2688
3182
|
for (const match of file.content.matchAll(IMPORT_NAMED)) {
|
|
2689
|
-
const
|
|
2690
|
-
for (const sym of symbols) addImport(sym, match[2]);
|
|
3183
|
+
for (const [name] of parseSpecifiers(match[1])) use(resolve4(match[2], from), name);
|
|
2691
3184
|
}
|
|
2692
3185
|
for (const match of file.content.matchAll(IMPORT_DEFAULT)) {
|
|
2693
|
-
|
|
3186
|
+
const target = resolve4(match[2], from);
|
|
3187
|
+
use(target, "default");
|
|
3188
|
+
use(target, match[1]);
|
|
2694
3189
|
}
|
|
2695
|
-
for (const match of file.content.matchAll(IMPORT_NAMESPACE))
|
|
2696
|
-
|
|
3190
|
+
for (const match of file.content.matchAll(IMPORT_NAMESPACE)) use(resolve4(match[1], from), "*");
|
|
3191
|
+
for (const match of file.content.matchAll(DYNAMIC_IMPORT)) {
|
|
3192
|
+
const [, typeOnly, destructured, source, member] = match;
|
|
3193
|
+
useLoaded(resolve4(source, from), destructured, member, Boolean(typeOnly));
|
|
2697
3194
|
}
|
|
2698
|
-
for (const match of file.content.matchAll(
|
|
2699
|
-
|
|
3195
|
+
for (const match of file.content.matchAll(REQUIRE)) {
|
|
3196
|
+
const [, destructured, source, member] = match;
|
|
3197
|
+
useLoaded(resolve4(source, from), destructured, member, false);
|
|
2700
3198
|
}
|
|
2701
|
-
|
|
3199
|
+
if (isEntryPoint(from, appRoots, routeModules) && !isBarrel(from)) use({ files: [from], folders: [] }, "*");
|
|
3200
|
+
}
|
|
3201
|
+
for (const entry of packageEntries) use({ files: [entry], folders: [] }, "*");
|
|
3202
|
+
const usedInAFolderOf = (file, name) => {
|
|
3203
|
+
let folder = file;
|
|
3204
|
+
while (folder) {
|
|
3205
|
+
folder = folder.slice(0, Math.max(folder.lastIndexOf("/"), 0));
|
|
3206
|
+
if (usedInFolder.get(folder)?.has(name)) return true;
|
|
3207
|
+
}
|
|
3208
|
+
return false;
|
|
3209
|
+
};
|
|
2702
3210
|
const deadByFile = /* @__PURE__ */ new Map();
|
|
2703
3211
|
for (const [key, loc] of exports) {
|
|
2704
3212
|
const symbolName = key.split("::")[1];
|
|
2705
|
-
const
|
|
2706
|
-
if (
|
|
2707
|
-
const importSet = imports.get(exportFileBasename);
|
|
2708
|
-
const isImported = importSet?.has(symbolName) ?? false;
|
|
2709
|
-
if (!isImported && !allImportedSymbols.has(symbolName)) {
|
|
3213
|
+
const isUsed = used.has(key) || used.has(`${loc.file}::*`) || usedInAFolderOf(loc.file, symbolName);
|
|
3214
|
+
if (!isUsed) {
|
|
2710
3215
|
deadByFile.set(loc.file, (deadByFile.get(loc.file) ?? 0) + 1);
|
|
2711
3216
|
}
|
|
2712
3217
|
}
|
|
@@ -3820,7 +4325,6 @@ var serverActionAuthRule = {
|
|
|
3820
4325
|
check(file, project) {
|
|
3821
4326
|
if (isTestFile(file.relativePath)) return [];
|
|
3822
4327
|
if (!USE_SERVER3.test(file.content)) return [];
|
|
3823
|
-
if (project.hasAuthMiddleware) return [];
|
|
3824
4328
|
for (const p of PUBLIC_ACTION_NAMES) {
|
|
3825
4329
|
if (p.test(file.relativePath)) return [];
|
|
3826
4330
|
}
|
|
@@ -3835,15 +4339,17 @@ var serverActionAuthRule = {
|
|
|
3835
4339
|
break;
|
|
3836
4340
|
}
|
|
3837
4341
|
}
|
|
4342
|
+
const middlewareSkipsPages = project.hasAuthMiddleware && project.authMiddlewareExcludesPages === true;
|
|
4343
|
+
const middleware = project.hasAuthMiddleware && !middlewareSkipsPages;
|
|
3838
4344
|
return [{
|
|
3839
4345
|
ruleId: "server-action-auth",
|
|
3840
4346
|
file: file.relativePath,
|
|
3841
4347
|
line: reportLine,
|
|
3842
4348
|
column: 1,
|
|
3843
|
-
message: "Server action performs mutations without any authentication check \u2014 anyone can call this action",
|
|
3844
|
-
severity: "warning",
|
|
4349
|
+
message: middleware ? "Server action performs mutations without an inline auth check (middleware auth detected \u2014 verify coverage)" : middlewareSkipsPages ? "Server action performs mutations without any authentication check (middleware auth detected, but its matcher doesn't cover pages)" : "Server action performs mutations without any authentication check \u2014 anyone can call this action",
|
|
4350
|
+
severity: middleware ? "info" : "warning",
|
|
3845
4351
|
category: "security",
|
|
3846
|
-
fix: 'Add auth check: const session = await auth(); if (!session) throw new Error("Unauthorized")'
|
|
4352
|
+
fix: middleware ? 'Check the user in the action too, not only in middleware: const session = await auth(); if (!session) throw new Error("Unauthorized")' : 'Add auth check: const session = await auth(); if (!session) throw new Error("Unauthorized")'
|
|
3847
4353
|
}];
|
|
3848
4354
|
}
|
|
3849
4355
|
};
|
|
@@ -5209,9 +5715,9 @@ function ipv6Groups(address) {
|
|
|
5209
5715
|
const quad = address.match(/^(.*:)(\d+)\.(\d+)\.(\d+)\.(\d+)$/);
|
|
5210
5716
|
const hex = quad ? `${quad[1]}${(+quad[2] << 8 | +quad[3]).toString(16)}:${(+quad[4] << 8 | +quad[5]).toString(16)}` : address;
|
|
5211
5717
|
const [head, tail] = hex.split("::");
|
|
5212
|
-
const
|
|
5213
|
-
const front =
|
|
5214
|
-
const back =
|
|
5718
|
+
const parse3 = (part) => part ? part.split(":").map((g) => parseInt(g, 16)) : [];
|
|
5719
|
+
const front = parse3(head);
|
|
5720
|
+
const back = parse3(tail);
|
|
5215
5721
|
return tail === void 0 ? front : [...front, ...Array(8 - front.length - back.length).fill(0), ...back];
|
|
5216
5722
|
}
|
|
5217
5723
|
function embeddedIPv4(address) {
|