auditai-scan 0.1.0 → 0.2.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.
Files changed (2) hide show
  1. package/dist/auditai-scan.mjs +994 -269
  2. package/package.json +1 -1
@@ -173,8 +173,8 @@ function formatScanText(r) {
173
173
  }
174
174
 
175
175
  // packages/scanner/src/scan.ts
176
- import { existsSync, readFileSync as readFileSync2 } from "node:fs";
177
- import { join as join3 } from "node:path";
176
+ import { existsSync, readFileSync as readFileSync3 } from "node:fs";
177
+ import { join as join4 } from "node:path";
178
178
 
179
179
  // packages/graph/src/graph.ts
180
180
  var SecurityGraph = class {
@@ -286,7 +286,8 @@ function buildGraph(model) {
286
286
  filters: q.filters,
287
287
  payload: q.payload,
288
288
  text: q.text,
289
- table: q.table
289
+ table: q.table,
290
+ ...q.via && q.via.length > 0 ? { via: q.via } : {}
290
291
  };
291
292
  const qn = g.addNode({
292
293
  id: `query:${q.location.file}:${q.location.line}:${i}`,
@@ -367,6 +368,8 @@ function isIgnored(rel, patterns) {
367
368
  function discoverFiles(root, extraSqlDirs = [], ignoreGlobs = []) {
368
369
  const source = [];
369
370
  const sql = [];
371
+ const manifests = [];
372
+ const tsconfigs = [];
370
373
  const ignore = ignoreGlobs.map(globToRegExp);
371
374
  const walk2 = (dir, sqlOnly = false) => {
372
375
  let entries;
@@ -396,14 +399,18 @@ function discoverFiles(root, extraSqlDirs = [], ignoreGlobs = []) {
396
399
  continue;
397
400
  }
398
401
  if (sqlOnly || name.endsWith(".d.ts")) continue;
399
- if (SOURCE_EXT.test(name)) source.push(rel);
402
+ if (name === "package.json") manifests.push(rel);
403
+ else if (/^tsconfig(\..+)?\.json$/.test(name)) tsconfigs.push(rel);
404
+ else if (SOURCE_EXT.test(name)) source.push(rel);
400
405
  }
401
406
  };
402
407
  walk2(root);
403
408
  for (const extra of extraSqlDirs) walk2(resolve(root, extra), true);
404
409
  source.sort();
405
410
  sql.sort();
406
- return { source, sql };
411
+ manifests.sort();
412
+ tsconfigs.sort();
413
+ return { source, sql, manifests, tsconfigs };
407
414
  }
408
415
 
409
416
  // packages/parser/src/model.ts
@@ -486,21 +493,80 @@ function hasExportModifier(node) {
486
493
  const mods = ts.canHaveModifiers(node) ? ts.getModifiers(node) : void 0;
487
494
  return mods?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
488
495
  }
489
- function exportedFunctions(sf) {
496
+ function functionOfInitializer(init, sf) {
497
+ const u = unwrap(init);
498
+ if (ts.isArrowFunction(u) || ts.isFunctionExpression(u)) return { fn: u };
499
+ if (ts.isCallExpression(u)) {
500
+ for (const a of u.arguments) {
501
+ const ua = unwrap(a);
502
+ if (ts.isArrowFunction(ua) || ts.isFunctionExpression(ua)) {
503
+ return { fn: ua, wrapper: u.expression.getText(sf).replace(/\s+/g, "") };
504
+ }
505
+ }
506
+ }
507
+ return null;
508
+ }
509
+ function topLevelFunctions(sf) {
490
510
  const out = [];
491
511
  for (const stmt of sf.statements) {
492
- if (ts.isFunctionDeclaration(stmt) && stmt.name && hasExportModifier(stmt)) {
493
- out.push({ name: stmt.name.text, fn: stmt, node: stmt });
494
- } else if (ts.isVariableStatement(stmt) && hasExportModifier(stmt)) {
512
+ if (ts.isFunctionDeclaration(stmt) && stmt.name) {
513
+ out.push({ name: stmt.name.text, fn: stmt, node: stmt, exported: hasExportModifier(stmt) });
514
+ } else if (ts.isVariableStatement(stmt)) {
515
+ const exported = hasExportModifier(stmt);
495
516
  for (const d of stmt.declarationList.declarations) {
496
- if (ts.isIdentifier(d.name) && d.initializer) {
497
- const init = unwrap(d.initializer);
498
- if (ts.isArrowFunction(init) || ts.isFunctionExpression(init)) {
499
- out.push({ name: d.name.text, fn: init, node: d });
517
+ if (!ts.isIdentifier(d.name) || !d.initializer) continue;
518
+ const f = functionOfInitializer(d.initializer, sf);
519
+ if (!f) continue;
520
+ out.push(
521
+ f.wrapper === void 0 ? { name: d.name.text, fn: f.fn, node: d, exported } : { name: d.name.text, fn: f.fn, node: d, exported, wrapper: f.wrapper }
522
+ );
523
+ }
524
+ }
525
+ }
526
+ return out;
527
+ }
528
+ function exportedFunctions(sf) {
529
+ return topLevelFunctions(sf).filter((f) => f.exported);
530
+ }
531
+ function classesIn(sf) {
532
+ const out = [];
533
+ for (const stmt of sf.statements) {
534
+ if (!ts.isClassDeclaration(stmt) || !stmt.name) continue;
535
+ const ctorParams = [];
536
+ const propFromParam = /* @__PURE__ */ new Map();
537
+ const methods = /* @__PURE__ */ new Map();
538
+ for (const member of stmt.members) {
539
+ if (ts.isConstructorDeclaration(member)) {
540
+ member.parameters.forEach((p, i) => {
541
+ const name = ts.isIdentifier(p.name) ? p.name.text : `arg${i}`;
542
+ ctorParams.push(name);
543
+ const mods = ts.getModifiers(p) ?? [];
544
+ if (mods.some(
545
+ (m) => m.kind === ts.SyntaxKind.PrivateKeyword || m.kind === ts.SyntaxKind.PublicKeyword || m.kind === ts.SyntaxKind.ProtectedKeyword || m.kind === ts.SyntaxKind.ReadonlyKeyword
546
+ )) {
547
+ propFromParam.set(name, i);
548
+ }
549
+ });
550
+ if (member.body) {
551
+ for (const bin of collect(member.body, ts.isBinaryExpression)) {
552
+ if (bin.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isPropertyAccessExpression(bin.left) && bin.left.expression.kind === ts.SyntaxKind.ThisKeyword && ts.isIdentifier(bin.right)) {
553
+ const idx = ctorParams.indexOf(bin.right.text);
554
+ if (idx >= 0) propFromParam.set(bin.left.name.text, idx);
555
+ }
500
556
  }
501
557
  }
558
+ } else if (ts.isMethodDeclaration(member) && ts.isIdentifier(member.name)) {
559
+ methods.set(member.name.text, member);
502
560
  }
503
561
  }
562
+ out.push({
563
+ name: stmt.name.text,
564
+ node: stmt,
565
+ exported: hasExportModifier(stmt),
566
+ ctorParams,
567
+ propFromParam,
568
+ methods
569
+ });
504
570
  }
505
571
  return out;
506
572
  }
@@ -546,6 +612,11 @@ function enclosingStatement(node) {
546
612
 
547
613
  // packages/parser/src/nextjs.ts
548
614
  var ROUTE_FILE = /^(?:(.*?)\/)?(?:src\/)?app\/(.*?)\/?route\.(ts|tsx|js|jsx|mjs)$/;
615
+ var PAGE_FILE = /^(?:(.*?)\/)?(?:src\/)?app\/(.*?)\/?page\.(tsx|ts|jsx|js)$/;
616
+ function routePath(dir) {
617
+ const parts = dir.split("/").filter((p) => p.length > 0 && !p.startsWith("(") && !p.startsWith("@"));
618
+ return `/${parts.join("/")}`;
619
+ }
549
620
  function appRootOf(rel) {
550
621
  const m = /^(?:(.*?)\/)?(?:src\/)?app\//.exec(rel);
551
622
  if (!m) return null;
@@ -555,9 +626,31 @@ function appRootOf(rel) {
555
626
  function routeFromFile(rel) {
556
627
  const m = ROUTE_FILE.exec(rel);
557
628
  if (!m) return null;
558
- const dir = m[2] ?? "";
559
- const parts = dir.split("/").filter((p) => p.length > 0 && !p.startsWith("(") && !p.startsWith("@"));
560
- return `/${parts.join("/")}`;
629
+ return routePath(m[2] ?? "");
630
+ }
631
+ function pageFromFile(rel) {
632
+ const m = PAGE_FILE.exec(rel);
633
+ if (!m) return null;
634
+ return routePath(m[2] ?? "");
635
+ }
636
+ function pageHandlerIn(sf) {
637
+ for (const stmt of sf.statements) {
638
+ if (ts2.isFunctionDeclaration(stmt)) {
639
+ const mods = ts2.getModifiers(stmt) ?? [];
640
+ if (mods.some((m) => m.kind === ts2.SyntaxKind.DefaultKeyword)) {
641
+ return { name: stmt.name?.text ?? "Page", fn: stmt, node: stmt, exported: true };
642
+ }
643
+ } else if (ts2.isExportAssignment(stmt) && !stmt.isExportEquals) {
644
+ const e = unwrap(stmt.expression);
645
+ if (ts2.isIdentifier(e)) {
646
+ const f = topLevelFunctions(sf).find((t) => t.name === e.text);
647
+ if (f) return { ...f, exported: true };
648
+ } else if (ts2.isArrowFunction(e) || ts2.isFunctionExpression(e)) {
649
+ return { name: "Page", fn: e, node: stmt, exported: true };
650
+ }
651
+ }
652
+ }
653
+ return null;
561
654
  }
562
655
  function routeHandlersIn(sf) {
563
656
  const out = [];
@@ -582,9 +675,172 @@ function isClientComponentFile(sf) {
582
675
  }
583
676
 
584
677
  // packages/parser/src/parse-project.ts
678
+ import { readFileSync as readFileSync2 } from "node:fs";
679
+ import { join as join3, resolve as resolve2 } from "node:path";
680
+ import ts5 from "typescript";
681
+
682
+ // packages/parser/src/resolve.ts
585
683
  import { readFileSync } from "node:fs";
586
- import { join as join2, posix, resolve as resolve2 } from "node:path";
587
- import ts4 from "typescript";
684
+ import { join as join2, posix } from "node:path";
685
+ import ts3 from "typescript";
686
+ var EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs"];
687
+ var CONDITIONS = ["import", "default", "require", "node", "types", "module"];
688
+ function dirOf(rel) {
689
+ const d = posix.dirname(rel);
690
+ return d === "." ? "" : d;
691
+ }
692
+ function under(dir, rel) {
693
+ return posix.normalize(dir ? posix.join(dir, rel) : rel).replace(/^\.\//, "");
694
+ }
695
+ function matchPattern(pattern, spec) {
696
+ const i = pattern.indexOf("*");
697
+ if (i < 0) return pattern === spec ? "" : null;
698
+ const pre = pattern.slice(0, i);
699
+ const suf = pattern.slice(i + 1);
700
+ if (spec.length >= pre.length + suf.length && spec.startsWith(pre) && spec.endsWith(suf)) {
701
+ return spec.slice(pre.length, spec.length - suf.length);
702
+ }
703
+ return null;
704
+ }
705
+ function pickCondition(v) {
706
+ if (typeof v === "string") return [v];
707
+ if (Array.isArray(v)) return v.flatMap(pickCondition);
708
+ if (v && typeof v === "object") {
709
+ const o = v;
710
+ for (const c of CONDITIONS) {
711
+ if (c in o) {
712
+ const r = pickCondition(o[c]);
713
+ if (r.length > 0) return r;
714
+ }
715
+ }
716
+ for (const val of Object.values(o)) {
717
+ const r = pickCondition(val);
718
+ if (r.length > 0) return r;
719
+ }
720
+ }
721
+ return [];
722
+ }
723
+ function resolveExports(exports, subpath) {
724
+ if (exports === null || exports === void 0) return [];
725
+ if (typeof exports === "string") return subpath === "." ? [exports] : [];
726
+ if (Array.isArray(exports)) return exports.flatMap((e) => resolveExports(e, subpath));
727
+ if (typeof exports !== "object") return [];
728
+ const obj = exports;
729
+ const keys = Object.keys(obj);
730
+ if (!keys.some((k) => k.startsWith("."))) return subpath === "." ? pickCondition(obj) : [];
731
+ const exact = obj[subpath];
732
+ if (exact !== void 0) return pickCondition(exact);
733
+ for (const k of keys) {
734
+ if (!k.includes("*")) continue;
735
+ const m = matchPattern(k, subpath);
736
+ if (m === null) continue;
737
+ return pickCondition(obj[k]).map((v) => v.replace("*", m));
738
+ }
739
+ return [];
740
+ }
741
+ var Resolver = class {
742
+ constructor(root, files, manifests, tsconfigs, warnings) {
743
+ this.files = files;
744
+ for (const rel of manifests) {
745
+ try {
746
+ const j = JSON.parse(readFileSync(join2(root, rel), "utf8"));
747
+ if (typeof j.name !== "string") continue;
748
+ const main2 = [j.main, j.module, j.types].find((v) => typeof v === "string");
749
+ this.packages.push({
750
+ name: j.name,
751
+ dir: dirOf(rel),
752
+ exports: j.exports ?? null,
753
+ main: main2 ?? null
754
+ });
755
+ } catch (e) {
756
+ warnings.push(`could not read ${rel}: ${e instanceof Error ? e.message : String(e)}`);
757
+ }
758
+ }
759
+ this.packages.sort((a, b) => b.name.length - a.name.length);
760
+ for (const rel of tsconfigs) {
761
+ const read = ts3.readConfigFile(join2(root, rel), (p) => readFileSync(p, "utf8"));
762
+ const config = read.config;
763
+ if (read.error || !config) continue;
764
+ const co = config.compilerOptions ?? {};
765
+ const paths = co.paths;
766
+ if (!paths || typeof paths !== "object") continue;
767
+ const dir = dirOf(rel);
768
+ const base = typeof co.baseUrl === "string" ? under(dir, co.baseUrl).replace(/^\.$/, "") : dir;
769
+ const mappings = [];
770
+ for (const [pattern, targets] of Object.entries(paths)) {
771
+ if (!Array.isArray(targets)) continue;
772
+ mappings.push({
773
+ pattern,
774
+ targets: targets.filter((t) => typeof t === "string")
775
+ });
776
+ }
777
+ this.tsconfigs.push({ dir, base, paths: mappings });
778
+ }
779
+ this.tsconfigs.sort((a, b) => b.dir.length - a.dir.length);
780
+ }
781
+ packages = [];
782
+ tsconfigs = [];
783
+ cache = /* @__PURE__ */ new Map();
784
+ /** Project-relative path of the module `spec` refers to from `fromRel`, or null when it is outside the project. */
785
+ resolve(spec, fromRel) {
786
+ const key = `${fromRel}\0${spec}`;
787
+ const cached = this.cache.get(key);
788
+ if (cached !== void 0) return cached;
789
+ let hit = null;
790
+ for (const base of this.candidates(spec, fromRel)) {
791
+ hit = this.firstExisting(base);
792
+ if (hit) break;
793
+ }
794
+ this.cache.set(key, hit);
795
+ return hit;
796
+ }
797
+ candidates(spec, fromRel) {
798
+ const out = [];
799
+ if (spec.startsWith(".")) {
800
+ out.push(posix.normalize(posix.join(posix.dirname(fromRel), spec)));
801
+ return out;
802
+ }
803
+ const cfg = this.tsconfigs.find((c) => c.dir === "" || fromRel.startsWith(`${c.dir}/`));
804
+ if (cfg) {
805
+ for (const { pattern, targets } of cfg.paths) {
806
+ const m = matchPattern(pattern, spec);
807
+ if (m === null) continue;
808
+ for (const t of targets) out.push(under(cfg.base, t.replace("*", m)));
809
+ }
810
+ }
811
+ if (spec.startsWith("@/") || spec.startsWith("~/")) {
812
+ const rest = spec.slice(2);
813
+ const appRoot = appRootOf(fromRel);
814
+ if (appRoot) out.push(posix.join(appRoot, rest), posix.join(appRoot, "src", rest));
815
+ out.push(rest, `src/${rest}`);
816
+ }
817
+ const pkg = this.packages.find((p) => spec === p.name || spec.startsWith(`${p.name}/`));
818
+ if (pkg) {
819
+ const sub = spec === pkg.name ? "." : `.${spec.slice(pkg.name.length)}`;
820
+ for (const t of resolveExports(pkg.exports, sub)) out.push(under(pkg.dir, t));
821
+ if (sub === ".") {
822
+ if (pkg.main) out.push(under(pkg.dir, pkg.main));
823
+ out.push(under(pkg.dir, "src/index"), under(pkg.dir, "index"));
824
+ } else {
825
+ out.push(under(pkg.dir, sub), under(pkg.dir, `src/${sub.slice(2)}`));
826
+ }
827
+ }
828
+ return out;
829
+ }
830
+ firstExisting(base) {
831
+ const b = base.replace(/^\.\//, "");
832
+ const stems = [b];
833
+ const m = /\.(js|jsx|mjs|cjs)$/.exec(b);
834
+ if (m) stems.push(b.slice(0, -m[0].length));
835
+ for (const s of stems) {
836
+ if (this.files.has(s)) return s;
837
+ for (const ext of EXTENSIONS) if (this.files.has(s + ext)) return s + ext;
838
+ for (const ext of EXTENSIONS)
839
+ if (this.files.has(`${s}/index${ext}`)) return `${s}/index${ext}`;
840
+ }
841
+ return null;
842
+ }
843
+ };
588
844
 
589
845
  // packages/parser/src/rls.ts
590
846
  var CONSTRAINT_WORDS = /* @__PURE__ */ new Set([
@@ -629,7 +885,7 @@ function splitTopLevel(text) {
629
885
  if (cur.trim()) out.push(cur);
630
886
  return out.map((s) => s.trim()).filter((s) => s.length > 0);
631
887
  }
632
- var IDENT = String.raw`(?:public\.)?"?([A-Za-z_][A-Za-z0-9_]*)"?`;
888
+ var IDENT = String.raw`(?:"?public"?\.)?"?([A-Za-z_][A-Za-z0-9_]*)"?`;
633
889
  var CREATE_TABLE = new RegExp(
634
890
  String.raw`^create\s+table\s+(?:if\s+not\s+exists\s+)?${IDENT}\s*\(`,
635
891
  "i"
@@ -717,22 +973,22 @@ function parseSqlForRls(rel, text, into) {
717
973
  }
718
974
 
719
975
  // packages/parser/src/supabase.ts
720
- import ts3 from "typescript";
976
+ import ts4 from "typescript";
721
977
  var CREATE_CLIENT_CALLEES = /^(createClient|createServerClient|createBrowserClient)$/;
722
978
  function isCreateClientCall(call, sf) {
723
979
  const callee = call.expression;
724
- if (ts3.isIdentifier(callee)) return CREATE_CLIENT_CALLEES.test(callee.text);
725
- if (ts3.isPropertyAccessExpression(callee)) return CREATE_CLIENT_CALLEES.test(callee.name.text);
980
+ if (ts4.isIdentifier(callee)) return CREATE_CLIENT_CALLEES.test(callee.text);
981
+ if (ts4.isPropertyAccessExpression(callee)) return CREATE_CLIENT_CALLEES.test(callee.name.text);
726
982
  return CREATE_CLIENT_CALLEES.test(callee.getText(sf));
727
983
  }
728
984
  function resolveArgText(expr, sf) {
729
985
  const u = unwrap(expr);
730
- if (!ts3.isIdentifier(u)) return expr.getText(sf);
986
+ if (!ts4.isIdentifier(u)) return expr.getText(sf);
731
987
  let scope = expr.parent;
732
988
  while (scope) {
733
- if (ts3.isFunctionLike(scope) || ts3.isBlock(scope) || ts3.isSourceFile(scope)) {
734
- const decl = collect(scope, ts3.isVariableDeclaration).find(
735
- (d) => ts3.isIdentifier(d.name) && d.name.text === u.text && d.initializer
989
+ if (ts4.isFunctionLike(scope) || ts4.isBlock(scope) || ts4.isSourceFile(scope)) {
990
+ const decl = collect(scope, ts4.isVariableDeclaration).find(
991
+ (d) => ts4.isIdentifier(d.name) && d.name.text === u.text && d.initializer
736
992
  );
737
993
  if (decl?.initializer) return `${u.text} = ${decl.initializer.getText(sf)}`;
738
994
  }
@@ -751,13 +1007,13 @@ function classifyCreateClientCall(call, sf) {
751
1007
  evidence: `${callee}() from @supabase/ssr acts as the signed-in user; RLS applies`
752
1008
  };
753
1009
  }
754
- if (/SERVICE_ROLE|service_role|SECRET_KEY|SB_SECRET|sb_secret/i.test(keyArg)) {
1010
+ if (/SERVICE_?ROLE|SECRET_KEY|SB_SECRET/i.test(keyArg)) {
755
1011
  return {
756
1012
  kind: "service_role",
757
1013
  evidence: `key ${keyArg} is a service-role secret; RLS is bypassed`
758
1014
  };
759
1015
  }
760
- const anonKey = /ANON|PUBLISHABLE|anon|publishable/.test(keyArg);
1016
+ const anonKey = /ANON|PUBLISHABLE/i.test(keyArg);
761
1017
  if (anonKey && /Authorization|headers/i.test(optArg)) {
762
1018
  return {
763
1019
  kind: "user_scoped",
@@ -773,39 +1029,101 @@ function classifyCreateClientCall(call, sf) {
773
1029
  return { kind: "unknown", evidence: `could not classify key argument ${keyArg || "(none)"}` };
774
1030
  }
775
1031
  var AUTH_CALL = /\.auth\.(getUser|getSession|getClaims)\s*\(/;
1032
+ function hasModifier(node, kind) {
1033
+ const mods = ts4.canHaveModifiers(node) ? ts4.getModifiers(node) : void 0;
1034
+ return mods?.some((m) => m.kind === kind) ?? false;
1035
+ }
776
1036
  function analyzeModule(rel, sf) {
777
1037
  const imports = /* @__PURE__ */ new Map();
1038
+ const reexports = [];
1039
+ const exportedNames = /* @__PURE__ */ new Set();
1040
+ let defaultExport = null;
1041
+ for (const stmt of sf.statements) {
1042
+ if (ts4.isImportDeclaration(stmt) && ts4.isStringLiteral(stmt.moduleSpecifier)) {
1043
+ const spec = stmt.moduleSpecifier.text;
1044
+ const clause = stmt.importClause;
1045
+ if (!clause) continue;
1046
+ if (clause.name) imports.set(clause.name.text, { spec, imported: "default" });
1047
+ const nb = clause.namedBindings;
1048
+ if (nb && ts4.isNamedImports(nb)) {
1049
+ for (const el of nb.elements) {
1050
+ imports.set(el.name.text, { spec, imported: (el.propertyName ?? el.name).text });
1051
+ }
1052
+ }
1053
+ if (nb && ts4.isNamespaceImport(nb)) imports.set(nb.name.text, { spec, imported: "*" });
1054
+ } else if (ts4.isExportDeclaration(stmt)) {
1055
+ const spec = stmt.moduleSpecifier && ts4.isStringLiteral(stmt.moduleSpecifier) ? stmt.moduleSpecifier.text : null;
1056
+ const clause = stmt.exportClause;
1057
+ if (spec && !clause) reexports.push({ star: true, spec });
1058
+ else if (clause && ts4.isNamedExports(clause)) {
1059
+ for (const el of clause.elements) {
1060
+ const name = (el.propertyName ?? el.name).text;
1061
+ if (spec) reexports.push({ star: false, name, alias: el.name.text, spec });
1062
+ else exportedNames.add(name);
1063
+ }
1064
+ }
1065
+ } else if (ts4.isExportAssignment(stmt) && !stmt.isExportEquals) {
1066
+ const e = unwrap(stmt.expression);
1067
+ if (ts4.isIdentifier(e)) defaultExport = e.text;
1068
+ } else if (ts4.isFunctionDeclaration(stmt) && stmt.name && hasModifier(stmt, ts4.SyntaxKind.DefaultKeyword)) {
1069
+ defaultExport = stmt.name.text;
1070
+ }
1071
+ }
1072
+ const functions = /* @__PURE__ */ new Map();
1073
+ for (const f of topLevelFunctions(sf)) {
1074
+ functions.set(f.name, exportedNames.has(f.name) ? { ...f, exported: true } : f);
1075
+ }
1076
+ const classes = /* @__PURE__ */ new Map();
1077
+ for (const c of classesIn(sf)) {
1078
+ classes.set(c.name, exportedNames.has(c.name) ? { ...c, exported: true } : c);
1079
+ }
1080
+ const moduleVars = /* @__PURE__ */ new Map();
778
1081
  for (const stmt of sf.statements) {
779
- if (!ts3.isImportDeclaration(stmt) || !ts3.isStringLiteral(stmt.moduleSpecifier)) continue;
780
- const spec = stmt.moduleSpecifier.text;
781
- const clause = stmt.importClause;
782
- if (!clause) continue;
783
- if (clause.name) imports.set(clause.name.text, spec);
784
- const nb = clause.namedBindings;
785
- if (nb && ts3.isNamedImports(nb)) for (const el of nb.elements) imports.set(el.name.text, spec);
786
- if (nb && ts3.isNamespaceImport(nb)) imports.set(nb.name.text, spec);
1082
+ if (!ts4.isVariableStatement(stmt)) continue;
1083
+ const exported = hasModifier(stmt, ts4.SyntaxKind.ExportKeyword);
1084
+ for (const d of stmt.declarationList.declarations) {
1085
+ if (!ts4.isIdentifier(d.name) || !d.initializer || functions.has(d.name.text)) continue;
1086
+ const init = unwrap(d.initializer);
1087
+ if (!ts4.isCallExpression(init) && !ts4.isNewExpression(init)) continue;
1088
+ moduleVars.set(d.name.text, {
1089
+ name: d.name.text,
1090
+ init,
1091
+ exported: exported || exportedNames.has(d.name.text),
1092
+ node: d
1093
+ });
1094
+ }
787
1095
  }
788
1096
  const clientFactories = [];
789
1097
  const authHelpers = [];
790
- for (const ex of exportedFunctions(sf)) {
791
- const text = ex.fn.getText(sf);
792
- const location = { file: rel, line: lineOf(sf, ex.node) };
1098
+ for (const f of functions.values()) {
1099
+ const text = f.fn.getText(sf);
1100
+ const location = { file: rel, line: lineOf(sf, f.node) };
793
1101
  if (AUTH_CALL.test(text)) {
794
1102
  authHelpers.push({
795
- name: ex.name,
1103
+ name: f.name,
796
1104
  location,
797
1105
  evidence: "calls supabase auth.getUser/getSession/getClaims"
798
1106
  });
799
1107
  continue;
800
1108
  }
801
- const creates = collect(ex.fn, ts3.isCallExpression).filter((c) => isCreateClientCall(c, sf));
1109
+ const creates = collect(f.fn, ts4.isCallExpression).filter((c) => isCreateClientCall(c, sf));
802
1110
  const first = creates[0];
803
1111
  if (first) {
804
1112
  const { kind, evidence } = classifyCreateClientCall(first, sf);
805
- clientFactories.push({ name: ex.name, kind, location, evidence });
1113
+ clientFactories.push({ name: f.name, kind, location, evidence });
806
1114
  }
807
1115
  }
808
- return { file: rel, clientFactories, authHelpers, imports };
1116
+ return {
1117
+ file: rel,
1118
+ clientFactories,
1119
+ authHelpers,
1120
+ imports,
1121
+ functions,
1122
+ classes,
1123
+ moduleVars,
1124
+ reexports,
1125
+ defaultExport
1126
+ };
809
1127
  }
810
1128
 
811
1129
  // packages/parser/src/parse-project.ts
@@ -831,151 +1149,411 @@ var FILTER_METHODS = /* @__PURE__ */ new Set([
831
1149
  var WRITE_OPERATIONS = /* @__PURE__ */ new Set(["insert", "update", "upsert"]);
832
1150
  var OPERATIONS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "upsert"]);
833
1151
  var PUBLIC_SECRET_ENV = /process\.env\.NEXT_PUBLIC_[A-Z0-9_]*(?:SERVICE_ROLE|SECRET)[A-Z0-9_]*/g;
834
- function resolveImport(spec, fromRel, files) {
835
- let base;
836
- const bases = [];
837
- if (spec.startsWith("@/") || spec.startsWith("~/")) {
838
- base = spec.slice(2);
839
- const appRoot = appRootOf(fromRel);
840
- if (appRoot) bases.push(posix.join(appRoot, base), posix.join(appRoot, "src", base));
841
- bases.push(base, `src/${base}`);
842
- } else if (spec.startsWith(".")) {
843
- base = posix.normalize(posix.join(posix.dirname(fromRel), spec));
844
- bases.push(base);
845
- } else return null;
846
- for (const b of bases) {
847
- for (const c of [
848
- `${b}.ts`,
849
- `${b}.tsx`,
850
- `${b}.js`,
851
- `${b}.jsx`,
852
- `${b}/index.ts`,
853
- `${b}/index.tsx`,
854
- b
855
- ]) {
856
- if (files.has(c)) return c;
1152
+ var MAX_DEPTH = 3;
1153
+ var WRAPPER_AUTH = /enhance(Action|RouteHandler)|auth|protected|session|guard|require|withUser/i;
1154
+ var REQUEST_NAME = /^(req|request)$/;
1155
+ var NO_ARG = { client: null, instance: null, tainted: false, isRequest: false };
1156
+ function ownFunctionSym(facts, name, fn) {
1157
+ const factory = facts.clientFactories.find((c) => c.name === name);
1158
+ if (factory) return { kind: "factory", factory };
1159
+ const helper = facts.authHelpers.find((a) => a.name === name);
1160
+ if (helper) return { kind: "auth", helper };
1161
+ return { kind: "function", name, fn, facts };
1162
+ }
1163
+ function exportedSym(p, tf, name, depth) {
1164
+ if (depth > 5) return null;
1165
+ const f = tf.functions.get(name);
1166
+ if (f?.exported) return ownFunctionSym(tf, name, f.fn);
1167
+ const c = tf.classes.get(name);
1168
+ if (c?.exported) return { kind: "class", cls: c, facts: tf };
1169
+ const v = tf.moduleVars.get(name);
1170
+ if (v?.exported) return { kind: "var", name, init: v.init, facts: tf };
1171
+ if (name === "default" && tf.defaultExport) {
1172
+ const local = tf.defaultExport;
1173
+ const lf = tf.functions.get(local);
1174
+ if (lf) return ownFunctionSym(tf, local, lf.fn);
1175
+ const lc = tf.classes.get(local);
1176
+ if (lc) return { kind: "class", cls: lc, facts: tf };
1177
+ const lv = tf.moduleVars.get(local);
1178
+ if (lv) return { kind: "var", name: local, init: lv.init, facts: tf };
1179
+ }
1180
+ for (const r of tf.reexports) {
1181
+ const target = p.resolver.resolve(r.spec, tf.file);
1182
+ const t = target ? p.registry.get(target) : void 0;
1183
+ if (!t) continue;
1184
+ if (r.star) {
1185
+ const s = exportedSym(p, t, name, depth + 1);
1186
+ if (s) return s;
1187
+ } else if (r.alias === name) {
1188
+ const s = exportedSym(p, t, r.name, depth + 1);
1189
+ if (s) return s;
857
1190
  }
858
1191
  }
859
1192
  return null;
860
1193
  }
861
- function analyzeHandler(ctx) {
862
- const { rel, sf, fn, facts, registry, files } = ctx;
863
- const loc2 = (n) => ({ file: rel, line: lineOf(sf, n) });
864
- const factories = /* @__PURE__ */ new Map();
865
- const authHelpers = /* @__PURE__ */ new Map();
866
- for (const [local, spec] of facts.imports) {
867
- const target = resolveImport(spec, rel, files);
868
- const tf = target ? registry.get(target) : void 0;
1194
+ function scopeOf(p, facts) {
1195
+ const cached = p.scopes.get(facts.file);
1196
+ if (cached) return cached;
1197
+ const scope = /* @__PURE__ */ new Map();
1198
+ p.scopes.set(facts.file, scope);
1199
+ for (const [name, f] of facts.functions) scope.set(name, ownFunctionSym(facts, name, f.fn));
1200
+ for (const [name, c] of facts.classes) scope.set(name, { kind: "class", cls: c, facts });
1201
+ for (const [name, v] of facts.moduleVars) {
1202
+ scope.set(name, { kind: "var", name, init: v.init, facts });
1203
+ }
1204
+ for (const [local, ref] of facts.imports) {
1205
+ const target = p.resolver.resolve(ref.spec, facts.file);
1206
+ const tf = target ? p.registry.get(target) : void 0;
869
1207
  if (tf) {
870
- const cf = tf.clientFactories.find((c) => c.name === local);
871
- if (cf) factories.set(local, cf);
872
- const ah = tf.authHelpers.find((a) => a.name === local);
873
- if (ah) authHelpers.set(local, ah);
874
- } else if (spec.startsWith(".") || spec.startsWith("@/") || spec.startsWith("~/")) {
875
- let matched = false;
876
- for (const f of registry.values()) {
877
- const cf = f.clientFactories.find((c) => c.name === local);
878
- if (cf) {
879
- factories.set(local, cf);
880
- matched = true;
881
- }
882
- const ah = f.authHelpers.find((a) => a.name === local);
883
- if (ah) {
884
- authHelpers.set(local, ah);
885
- matched = true;
886
- }
1208
+ if (ref.imported === "*") scope.set(local, { kind: "namespace", facts: tf });
1209
+ else {
1210
+ const sym = exportedSym(p, tf, ref.imported, 0);
1211
+ if (sym) scope.set(local, sym);
1212
+ }
1213
+ continue;
1214
+ }
1215
+ if (!/^[.~]|^@\//.test(ref.spec)) continue;
1216
+ for (const f of p.registry.values()) {
1217
+ const fn = f.functions.get(ref.imported);
1218
+ if (!fn?.exported) continue;
1219
+ const sym = ownFunctionSym(f, ref.imported, fn.fn);
1220
+ if (sym.kind === "factory" || sym.kind === "auth") {
1221
+ scope.set(local, sym);
1222
+ p.warnings.push(
1223
+ `unresolved import "${ref.spec}" in ${facts.file}; matched "${ref.imported}" by name`
1224
+ );
1225
+ break;
1226
+ }
1227
+ }
1228
+ }
1229
+ return scope;
1230
+ }
1231
+ function symOfCallee(p, callee, scope) {
1232
+ if (ts5.isIdentifier(callee)) return scope.get(callee.text);
1233
+ if (ts5.isPropertyAccessExpression(callee) && ts5.isIdentifier(callee.expression)) {
1234
+ const ns = scope.get(callee.expression.text);
1235
+ if (ns?.kind === "namespace") return exportedSym(p, ns.facts, callee.name.text, 0) ?? void 0;
1236
+ }
1237
+ return void 0;
1238
+ }
1239
+ function returnedExpressions(fn) {
1240
+ if (!fn.body) return [];
1241
+ if (!ts5.isBlock(fn.body)) return [fn.body];
1242
+ return collect(fn.body, ts5.isReturnStatement).map((r) => r.expression).filter((e) => e !== void 0);
1243
+ }
1244
+ function factoryOfFunction(p, sym, depth) {
1245
+ const key = `${sym.facts.file}#${sym.name}`;
1246
+ if (p.factoryOfFn.has(key)) return p.factoryOfFn.get(key) ?? null;
1247
+ p.factoryOfFn.set(key, null);
1248
+ const sf = p.sources.get(sym.facts.file);
1249
+ if (!sf || depth > 2) return null;
1250
+ const scope = scopeOf(p, sym.facts);
1251
+ let found = null;
1252
+ for (const ret of returnedExpressions(sym.fn)) {
1253
+ const u = unwrap(ret);
1254
+ if (!ts5.isCallExpression(u)) continue;
1255
+ found = classifyCall(p, u, sf, scope, null, depth + 1);
1256
+ if (found) break;
1257
+ }
1258
+ p.factoryOfFn.set(key, found);
1259
+ return found;
1260
+ }
1261
+ function classifyCall(p, call, sf, scope, _frame, depth) {
1262
+ const sym = symOfCallee(p, call.expression, scope);
1263
+ if (sym?.kind === "factory") {
1264
+ return { kind: sym.factory.kind, name: sym.factory.name, location: sym.factory.location };
1265
+ }
1266
+ if (sym?.kind === "function") {
1267
+ const b = factoryOfFunction(p, sym, depth);
1268
+ if (b) return b;
1269
+ }
1270
+ if (isCreateClientCall(call, sf)) {
1271
+ const c = classifyCreateClientCall(call, sf);
1272
+ return {
1273
+ kind: c.kind,
1274
+ name: call.expression.getText(sf),
1275
+ location: { file: sf.fileName, line: lineOf(sf, call) }
1276
+ };
1277
+ }
1278
+ return null;
1279
+ }
1280
+ function instanceOfCall(p, call, frame, scope) {
1281
+ const sym = symOfCallee(p, call.expression, scope);
1282
+ if (sym?.kind !== "function") return null;
1283
+ const fscope = scopeOf(p, sym.facts);
1284
+ const params = sym.fn.parameters.map((pp) => ts5.isIdentifier(pp.name) ? pp.name.text : null);
1285
+ for (const ret of returnedExpressions(sym.fn)) {
1286
+ const u = unwrap(ret);
1287
+ if (!ts5.isNewExpression(u) || !ts5.isIdentifier(u.expression)) continue;
1288
+ const cs = fscope.get(u.expression.text);
1289
+ if (cs?.kind !== "class") continue;
1290
+ const ctorArgs = (u.arguments ?? []).map((a) => {
1291
+ const ua = unwrap(a);
1292
+ if (ts5.isIdentifier(ua)) {
1293
+ const j = params.indexOf(ua.text);
1294
+ if (j >= 0 && frame) return argBinding(p, call.arguments[j], frame);
1295
+ }
1296
+ return NO_ARG;
1297
+ });
1298
+ return { cls: cs.cls, facts: cs.facts, ctorArgs };
1299
+ }
1300
+ return null;
1301
+ }
1302
+ function varBinding(p, sym) {
1303
+ const key = `${sym.facts.file}#${sym.name}`;
1304
+ const cached = p.varBindings.get(key);
1305
+ if (cached) return cached;
1306
+ p.varBindings.set(key, NO_ARG);
1307
+ const sf = p.sources.get(sym.facts.file);
1308
+ if (!sf) return NO_ARG;
1309
+ const scope = scopeOf(p, sym.facts);
1310
+ const init = unwrap(sym.init);
1311
+ let out = NO_ARG;
1312
+ if (ts5.isCallExpression(init)) {
1313
+ const found = classifyCall(p, init, sf, scope, null, 0);
1314
+ const client = found ? {
1315
+ kind: found.kind,
1316
+ name: sym.name,
1317
+ location: { file: sym.facts.file, line: lineOf(sf, init) }
1318
+ } : null;
1319
+ const instance = client ? null : instanceOfCall(p, init, null, scope);
1320
+ out = { client, instance, tainted: false, isRequest: false };
1321
+ } else if (ts5.isNewExpression(init) && ts5.isIdentifier(init.expression)) {
1322
+ const cs = scope.get(init.expression.text);
1323
+ if (cs?.kind === "class") {
1324
+ out = {
1325
+ client: null,
1326
+ instance: { cls: cs.cls, facts: cs.facts, ctorArgs: [] },
1327
+ tainted: false,
1328
+ isRequest: false
1329
+ };
1330
+ }
1331
+ }
1332
+ p.varBindings.set(key, out);
1333
+ return out;
1334
+ }
1335
+ function isQueryChain(call) {
1336
+ return flattenChain(call).segments.some((s) => s.name === "from" || s.name === "rpc");
1337
+ }
1338
+ function derivedIn(frame, e) {
1339
+ for (const id of identifiersIn(e)) if (frame.inputNames.has(id)) return true;
1340
+ return frame.depth === 0 && /^(params|body|query|searchParams)\b/.test(e.getText(frame.sf));
1341
+ }
1342
+ function argBinding(p, arg, frame) {
1343
+ if (!arg) return NO_ARG;
1344
+ const u = unwrap(arg);
1345
+ const scope = scopeOf(p, frame.facts);
1346
+ let client = null;
1347
+ let instance = null;
1348
+ let isRequest = false;
1349
+ if (ts5.isIdentifier(u)) {
1350
+ client = frame.clients.get(u.text) ?? null;
1351
+ instance = frame.instances.get(u.text) ?? null;
1352
+ isRequest = frame.reqNames.has(u.text);
1353
+ if (!client && !instance) {
1354
+ const sym = scope.get(u.text);
1355
+ if (sym?.kind === "var") {
1356
+ const vb = varBinding(p, sym);
1357
+ client = vb.client;
1358
+ instance = vb.instance;
887
1359
  }
888
- if (matched)
889
- ctx.warnings.push(`unresolved import "${spec}" in ${rel}; matched "${local}" by name`);
1360
+ }
1361
+ } else if (ts5.isPropertyAccessExpression(u) && u.expression.kind === ts5.SyntaxKind.ThisKeyword) {
1362
+ const tp = frame.thisProps.get(u.name.text);
1363
+ if (tp) {
1364
+ client = tp.client;
1365
+ instance = tp.instance;
1366
+ isRequest = tp.isRequest;
1367
+ }
1368
+ } else if (ts5.isCallExpression(u)) {
1369
+ client = classifyCall(p, u, frame.sf, scope, frame, 0);
1370
+ if (!client) instance = instanceOfCall(p, u, frame, scope);
1371
+ } else if (ts5.isNewExpression(u) && ts5.isIdentifier(u.expression)) {
1372
+ const cs = scope.get(u.expression.text);
1373
+ if (cs?.kind === "class") {
1374
+ instance = {
1375
+ cls: cs.cls,
1376
+ facts: cs.facts,
1377
+ ctorArgs: (u.arguments ?? []).map((a) => argBinding(p, a, frame))
1378
+ };
890
1379
  }
891
1380
  }
892
- for (const cf of facts.clientFactories) factories.set(cf.name, cf);
893
- for (const ah of facts.authHelpers) authHelpers.set(ah.name, ah);
1381
+ return { client, instance, tainted: derivedIn(frame, arg), isRequest };
1382
+ }
1383
+ function methodTarget(inst, method) {
1384
+ const m = inst.cls.methods.get(method);
1385
+ if (!m) return null;
1386
+ const thisProps = /* @__PURE__ */ new Map();
1387
+ for (const [prop, idx] of inst.cls.propFromParam)
1388
+ thisProps.set(prop, inst.ctorArgs[idx] ?? NO_ARG);
1389
+ return { fn: m, facts: inst.facts, name: `${inst.cls.name}.${method}`, cls: inst.cls, thisProps };
1390
+ }
1391
+ function callTarget(p, call, frame, scope) {
1392
+ const callee = call.expression;
1393
+ if (ts5.isIdentifier(callee)) {
1394
+ const sym = scope.get(callee.text);
1395
+ if (sym?.kind === "function") {
1396
+ return { fn: sym.fn, facts: sym.facts, name: sym.name, cls: null, thisProps: /* @__PURE__ */ new Map() };
1397
+ }
1398
+ return null;
1399
+ }
1400
+ if (!ts5.isPropertyAccessExpression(callee)) return null;
1401
+ const obj = callee.expression;
1402
+ const method = callee.name.text;
1403
+ if (ts5.isIdentifier(obj)) {
1404
+ const inst = frame.instances.get(obj.text);
1405
+ if (inst) return methodTarget(inst, method);
1406
+ const sym = scope.get(obj.text);
1407
+ if (sym?.kind === "namespace") {
1408
+ const s = exportedSym(p, sym.facts, method, 0);
1409
+ if (s?.kind === "function") {
1410
+ return { fn: s.fn, facts: s.facts, name: s.name, cls: null, thisProps: /* @__PURE__ */ new Map() };
1411
+ }
1412
+ } else if (sym?.kind === "var") {
1413
+ const vb = varBinding(p, sym);
1414
+ if (vb.instance) return methodTarget(vb.instance, method);
1415
+ }
1416
+ return null;
1417
+ }
1418
+ if (obj.kind === ts5.SyntaxKind.ThisKeyword && frame.cls) {
1419
+ const m = frame.cls.methods.get(method);
1420
+ if (!m) return null;
1421
+ return {
1422
+ fn: m,
1423
+ facts: frame.facts,
1424
+ name: `${frame.cls.name}.${method}`,
1425
+ cls: frame.cls,
1426
+ thisProps: frame.thisProps
1427
+ };
1428
+ }
1429
+ if (ts5.isPropertyAccessExpression(obj) && obj.expression.kind === ts5.SyntaxKind.ThisKeyword) {
1430
+ const tp = frame.thisProps.get(obj.name.text);
1431
+ if (tp?.instance) return methodTarget(tp.instance, method);
1432
+ }
1433
+ return null;
1434
+ }
1435
+ function analyzeFrame(p, frame, acc) {
1436
+ const { rel, sf, fn } = frame;
1437
+ const scope = scopeOf(p, frame.facts);
1438
+ const loc2 = (n) => ({ file: rel, line: lineOf(sf, n) });
894
1439
  const body = fn.body ?? fn;
895
- const firstParam = fn.parameters[0];
896
- const reqName = firstParam && ts4.isIdentifier(firstParam.name) ? firstParam.name.text : null;
897
- const inputs = [];
898
- const inputNames = /* @__PURE__ */ new Set(["params", "searchParams"]);
1440
+ for (const [name, sym] of scope) {
1441
+ if (sym.kind !== "var" || frame.clients.has(name) || frame.instances.has(name)) continue;
1442
+ const vb = varBinding(p, sym);
1443
+ if (vb.client) frame.clients.set(name, vb.client);
1444
+ if (vb.instance) frame.instances.set(name, vb.instance);
1445
+ }
899
1446
  const addInput = (kind, name, n, bind) => {
900
- if (!inputs.some((i) => i.kind === kind && i.name === name))
901
- inputs.push({ kind, name, location: loc2(n) });
902
- if (bind) inputNames.add(name);
1447
+ if (!acc.inputs.some((i) => i.kind === kind && i.name === name)) {
1448
+ acc.inputs.push({ kind, name, location: loc2(n) });
1449
+ }
1450
+ if (bind) frame.inputNames.add(name);
1451
+ };
1452
+ const isRequestCall = (text) => {
1453
+ if (!/\.(json|formData|text)\(\)$/.test(text)) return false;
1454
+ for (const r of frame.reqNames) if (text.startsWith(`${r}.`)) return true;
1455
+ return frame.reqNames.size === 0 && /^(req|request)\./.test(text);
903
1456
  };
904
- if (ctx.kind === "server_action") {
905
- for (const p of fn.parameters)
906
- for (const nm of boundNames(p.name)) addInput("action_arg", nm, p, true);
907
- }
908
- walk(body, (n) => {
909
- if (ts4.isVariableDeclaration(n) && n.initializer) {
910
- const init = unwrap(n.initializer);
911
- const text = init.getText(sf);
1457
+ for (const decl of collect(body, ts5.isVariableDeclaration)) {
1458
+ if (!decl.initializer) continue;
1459
+ const init = unwrap(decl.initializer);
1460
+ const names = boundNames(decl.name);
1461
+ const text = init.getText(sf);
1462
+ if (frame.depth === 0) {
912
1463
  if (/^(params|context\.params|ctx\.params|props\.params)$/.test(text)) {
913
- for (const nm of boundNames(n.name)) addInput("route_param", nm, n, true);
914
- } else if (ts4.isCallExpression(init) && /\.(json|formData|text)\(\)$/.test(text) && (reqName ? text.startsWith(`${reqName}.`) : /^(req|request)\./.test(text))) {
915
- for (const nm of boundNames(n.name)) addInput("body", nm, n, true);
916
- } else if (/searchParams\.get\(|\.searchParams$|^new URL\(/.test(text)) {
917
- for (const nm of boundNames(n.name)) addInput("query", nm, n, true);
918
- } else if (/headers\.get\(/.test(text)) {
919
- for (const nm of boundNames(n.name)) addInput("header", nm, n, true);
920
- } else if (ts4.isIdentifier(init) && inputNames.has(init.text)) {
921
- for (const nm of boundNames(n.name)) addInput("body", nm, n, true);
1464
+ for (const nm of names) addInput("route_param", nm, decl, true);
1465
+ continue;
1466
+ }
1467
+ if (ts5.isCallExpression(init) && isRequestCall(text)) {
1468
+ for (const nm of names) addInput("body", nm, decl, true);
1469
+ continue;
1470
+ }
1471
+ if (/searchParams\.get\(|\.searchParams$|^new URL\(/.test(text)) {
1472
+ for (const nm of names) addInput("query", nm, decl, true);
1473
+ continue;
1474
+ }
1475
+ if (/headers\.get\(/.test(text)) {
1476
+ for (const nm of names) addInput("header", nm, decl, true);
1477
+ continue;
922
1478
  }
923
1479
  }
924
- if (ts4.isPropertyAccessExpression(n) && ts4.isIdentifier(n.expression) && n.expression.text === "params") {
925
- addInput("route_param", n.name.text, n, false);
1480
+ if (ts5.isCallExpression(init)) {
1481
+ const client = classifyCall(p, init, sf, scope, frame, 0);
1482
+ if (client && ts5.isIdentifier(decl.name)) {
1483
+ frame.clients.set(decl.name.text, client);
1484
+ continue;
1485
+ }
1486
+ const inst = instanceOfCall(p, init, frame, scope);
1487
+ if (inst && ts5.isIdentifier(decl.name)) {
1488
+ frame.instances.set(decl.name.text, inst);
1489
+ continue;
1490
+ }
1491
+ if (isQueryChain(init)) continue;
1492
+ if (returnsIdentity(p, init, sf, scope)) continue;
1493
+ const args = init.arguments.map((a) => argBinding(p, a, frame));
1494
+ if (args.some((a) => a.tainted || a.isRequest)) {
1495
+ for (const nm of names) frame.inputNames.add(nm);
1496
+ }
1497
+ } else if (ts5.isNewExpression(init) && ts5.isIdentifier(init.expression)) {
1498
+ const cs = scope.get(init.expression.text);
1499
+ if (cs?.kind === "class" && ts5.isIdentifier(decl.name)) {
1500
+ frame.instances.set(decl.name.text, {
1501
+ cls: cs.cls,
1502
+ facts: cs.facts,
1503
+ ctorArgs: (init.arguments ?? []).map((a) => argBinding(p, a, frame))
1504
+ });
1505
+ }
1506
+ } else if (ts5.isIdentifier(init)) {
1507
+ const c = frame.clients.get(init.text);
1508
+ if (c && ts5.isIdentifier(decl.name)) frame.clients.set(decl.name.text, c);
1509
+ const i = frame.instances.get(init.text);
1510
+ if (i && ts5.isIdentifier(decl.name)) frame.instances.set(decl.name.text, i);
1511
+ if (frame.inputNames.has(init.text)) {
1512
+ for (const nm of names) {
1513
+ if (frame.depth === 0) addInput("body", nm, decl, true);
1514
+ else frame.inputNames.add(nm);
1515
+ }
1516
+ }
1517
+ } else if (!isChainWithQuery(init) && derivedIn(frame, init)) {
1518
+ for (const nm of names) frame.inputNames.add(nm);
926
1519
  }
927
- return void 0;
928
- });
929
- const derived = (e) => {
930
- for (const id of identifiersIn(e)) if (inputNames.has(id)) return true;
931
- return /^(params|body|query|searchParams)\b/.test(e.getText(sf));
932
- };
1520
+ }
1521
+ if (frame.depth === 0) {
1522
+ walk(body, (n) => {
1523
+ if (ts5.isPropertyAccessExpression(n) && ts5.isIdentifier(n.expression) && n.expression.text === "params") {
1524
+ addInput("route_param", n.name.text, n, false);
1525
+ }
1526
+ return void 0;
1527
+ });
1528
+ }
933
1529
  const isWholeInput = (e) => {
934
1530
  const u = unwrap(e);
935
- if (ts4.isIdentifier(u)) return inputNames.has(u.text);
936
- if (ts4.isObjectLiteralExpression(u)) {
1531
+ if (ts5.isIdentifier(u)) return frame.inputNames.has(u.text);
1532
+ if (ts5.isObjectLiteralExpression(u)) {
937
1533
  return u.properties.some(
938
- (p) => ts4.isSpreadAssignment(p) && ts4.isIdentifier(unwrap(p.expression)) && inputNames.has(unwrap(p.expression).text)
1534
+ (pr) => ts5.isSpreadAssignment(pr) && ts5.isIdentifier(unwrap(pr.expression)) && frame.inputNames.has(unwrap(pr.expression).text)
939
1535
  );
940
1536
  }
941
1537
  return false;
942
1538
  };
943
- const classifyClientCall = (call) => {
944
- if (ts4.isIdentifier(call.expression)) {
945
- const f = factories.get(call.expression.text);
946
- if (f) return { kind: f.kind, name: f.name, location: f.location };
947
- }
948
- if (isCreateClientCall(call, sf)) {
949
- const c = classifyCreateClientCall(call, sf);
950
- return { kind: c.kind, name: call.expression.getText(sf), location: loc2(call) };
951
- }
952
- return null;
953
- };
954
- const clients = /* @__PURE__ */ new Map();
955
- const authChecks = [];
956
- for (const call of collect(body, ts4.isCallExpression)) {
1539
+ for (const call of collect(body, ts5.isCallExpression)) {
957
1540
  const calleeText = call.expression.getText(sf);
958
- if (/\.auth\.(getUser|getSession|getClaims)$/.test(calleeText)) authChecks.push(loc2(call));
959
- else if (ts4.isIdentifier(call.expression) && authHelpers.has(call.expression.text))
960
- authChecks.push(loc2(call));
961
- }
962
- for (const decl of collect(body, ts4.isVariableDeclaration)) {
963
- if (!decl.initializer || !ts4.isIdentifier(decl.name)) continue;
964
- const init = unwrap(decl.initializer);
965
- if (!ts4.isCallExpression(init)) continue;
966
- const binding = classifyClientCall(init);
967
- if (binding) clients.set(decl.name.text, binding);
1541
+ if (/\.auth\.(getUser|getSession|getClaims)$/.test(calleeText)) acc.authChecks.push(loc2(call));
1542
+ else if (symOfCallee(p, call.expression, scope)?.kind === "auth")
1543
+ acc.authChecks.push(loc2(call));
968
1544
  }
969
- const metadataAccesses = [];
970
- for (const pa of collect(body, ts4.isPropertyAccessExpression)) {
1545
+ for (const pa of collect(body, ts5.isPropertyAccessExpression)) {
971
1546
  const inner = pa.expression;
972
- if (ts4.isPropertyAccessExpression(inner) && (inner.name.text === "user_metadata" || inner.name.text === "app_metadata")) {
973
- metadataAccesses.push({ path: pa.getText(sf), bucket: inner.name.text, location: loc2(pa) });
1547
+ if (ts5.isPropertyAccessExpression(inner) && (inner.name.text === "user_metadata" || inner.name.text === "app_metadata")) {
1548
+ acc.metadataAccesses.push({
1549
+ path: pa.getText(sf),
1550
+ bucket: inner.name.text,
1551
+ location: loc2(pa)
1552
+ });
974
1553
  }
975
1554
  }
976
- const queries = [];
977
1555
  const seen = /* @__PURE__ */ new Set();
978
- for (const call of collect(body, ts4.isCallExpression)) {
1556
+ for (const call of collect(body, ts5.isCallExpression)) {
979
1557
  if (!isChainTail(call)) continue;
980
1558
  const chain = flattenChain(call);
981
1559
  const fromIdx = chain.segments.findIndex((s) => s.name === "from");
@@ -985,21 +1563,18 @@ function analyzeHandler(ctx) {
985
1563
  if (anchorIdx < 0 || !anchor) continue;
986
1564
  if (seen.has(anchor.node.pos)) continue;
987
1565
  seen.add(anchor.node.pos);
988
- let client = "unknown";
1566
+ let binding = null;
989
1567
  let clientName = null;
990
- let clientLocation = null;
991
1568
  const root = unwrap(chain.root);
992
- let binding = null;
993
- if (ts4.isIdentifier(root)) {
994
- binding = clients.get(root.text) ?? null;
1569
+ if (ts5.isIdentifier(root)) {
1570
+ binding = frame.clients.get(root.text) ?? null;
995
1571
  if (!binding) clientName = root.text;
996
- } else if (ts4.isCallExpression(root)) {
997
- binding = classifyClientCall(root);
998
- }
999
- if (binding) {
1000
- client = binding.kind;
1001
- clientName = binding.name;
1002
- clientLocation = binding.location;
1572
+ } else if (ts5.isCallExpression(root)) {
1573
+ binding = classifyCall(p, root, sf, scope, frame, 0);
1574
+ if (!binding) clientName = root.expression.getText(sf);
1575
+ } else if (ts5.isPropertyAccessExpression(root) && root.expression.kind === ts5.SyntaxKind.ThisKeyword) {
1576
+ binding = frame.thisProps.get(root.name.text)?.client ?? null;
1577
+ if (!binding) clientName = root.getText(sf);
1003
1578
  }
1004
1579
  const after = chain.segments.slice(anchorIdx + 1);
1005
1580
  let operation = fromIdx >= 0 ? "unknown" : "rpc";
@@ -1011,7 +1586,7 @@ function analyzeHandler(ctx) {
1011
1586
  if (WRITE_OPERATIONS.has(operation) && arg) {
1012
1587
  payload = {
1013
1588
  text: arg.getText(sf).replace(/\s+/g, " ").slice(0, 200),
1014
- inputDerived: derived(arg),
1589
+ inputDerived: derivedIn(frame, arg),
1015
1590
  wholeInput: isWholeInput(arg)
1016
1591
  };
1017
1592
  }
@@ -1022,14 +1597,14 @@ function analyzeHandler(ctx) {
1022
1597
  for (const s of after) {
1023
1598
  if (!FILTER_METHODS.has(s.name)) continue;
1024
1599
  const first = s.args[0];
1025
- if (s.name === "match" && first && ts4.isObjectLiteralExpression(first)) {
1026
- for (const p of first.properties) {
1027
- if (ts4.isPropertyAssignment(p)) {
1600
+ if (s.name === "match" && first && ts5.isObjectLiteralExpression(first)) {
1601
+ for (const pr of first.properties) {
1602
+ if (ts5.isPropertyAssignment(pr)) {
1028
1603
  filters.push({
1029
1604
  method: "match",
1030
- column: p.name.getText(sf).replace(/['"]/g, ""),
1031
- valueText: p.initializer.getText(sf),
1032
- inputDerived: derived(p.initializer)
1605
+ column: pr.name.getText(sf).replace(/['"]/g, ""),
1606
+ valueText: pr.initializer.getText(sf),
1607
+ inputDerived: derivedIn(frame, pr.initializer)
1033
1608
  });
1034
1609
  }
1035
1610
  }
@@ -1040,38 +1615,137 @@ function analyzeHandler(ctx) {
1040
1615
  method: s.name,
1041
1616
  column: stringLiteralValue(first),
1042
1617
  valueText: val ? val.getText(sf) : "",
1043
- inputDerived: val ? derived(val) : false
1618
+ inputDerived: val ? derivedIn(frame, val) : false
1044
1619
  });
1045
1620
  }
1046
- queries.push({
1621
+ const query = {
1047
1622
  table: stringLiteralValue(anchor.args[0]) ?? "(dynamic)",
1048
1623
  operation,
1049
- client,
1050
- clientName,
1051
- clientLocation,
1624
+ client: binding?.kind ?? "unknown",
1625
+ clientName: binding?.name ?? clientName,
1626
+ clientLocation: binding?.location ?? null,
1052
1627
  filters,
1053
1628
  payload,
1054
1629
  location: loc2(anchor.node),
1055
1630
  text: call.getText(sf).replace(/\s+/g, " ").slice(0, 200)
1631
+ };
1632
+ if (frame.via.length > 0) query.via = frame.via;
1633
+ acc.queries.push(query);
1634
+ }
1635
+ if (frame.depth >= MAX_DEPTH) return;
1636
+ for (const call of collect(body, ts5.isCallExpression)) {
1637
+ const target = callTarget(p, call, frame, scope);
1638
+ if (!target) continue;
1639
+ const tsf = p.sources.get(target.facts.file);
1640
+ if (!tsf) continue;
1641
+ const child = {
1642
+ rel: target.facts.file,
1643
+ sf: tsf,
1644
+ facts: target.facts,
1645
+ fn: target.fn,
1646
+ depth: frame.depth + 1,
1647
+ via: [...frame.via, `${target.name} (${target.facts.file}:${lineOf(tsf, target.fn)})`],
1648
+ inputNames: /* @__PURE__ */ new Set(),
1649
+ reqNames: /* @__PURE__ */ new Set(),
1650
+ clients: /* @__PURE__ */ new Map(),
1651
+ instances: /* @__PURE__ */ new Map(),
1652
+ cls: target.cls,
1653
+ thisProps: target.thisProps
1654
+ };
1655
+ target.fn.parameters.forEach((param, i) => {
1656
+ const ab = argBinding(p, call.arguments[i], frame);
1657
+ const names = boundNames(param.name);
1658
+ const head = names[0];
1659
+ if (ab.client && head !== void 0 && ts5.isIdentifier(param.name)) {
1660
+ child.clients.set(head, ab.client);
1661
+ }
1662
+ if (ab.instance && head !== void 0 && ts5.isIdentifier(param.name)) {
1663
+ child.instances.set(head, ab.instance);
1664
+ }
1665
+ for (const nm of names) {
1666
+ if (ab.isRequest) child.reqNames.add(nm);
1667
+ if (ab.tainted) child.inputNames.add(nm);
1668
+ }
1056
1669
  });
1670
+ const signature = [
1671
+ ...[...child.clients].map(([n, c]) => `${n}=${c.kind}`),
1672
+ ...[...child.inputNames].map((n) => `${n}!`),
1673
+ ...[...child.reqNames].map((n) => `${n}?`),
1674
+ ...[...child.thisProps].map(([n, b]) => `this.${n}=${b.client?.kind ?? "-"}`)
1675
+ ].sort();
1676
+ const key = `${target.facts.file}#${target.name}#${signature.join(",")}`;
1677
+ if (acc.visited.has(key)) continue;
1678
+ acc.visited.add(key);
1679
+ analyzeFrame(p, child, acc);
1680
+ }
1681
+ }
1682
+ function isChainWithQuery(e) {
1683
+ return ts5.isCallExpression(e) && isQueryChain(e);
1684
+ }
1685
+ var IDENTITY_CALLEE = /\.auth\.|user|session|claims|auth|principal|viewer/i;
1686
+ function returnsIdentity(p, call, sf, scope) {
1687
+ if (symOfCallee(p, call.expression, scope)?.kind === "auth") return true;
1688
+ return IDENTITY_CALLEE.test(call.expression.getText(sf));
1689
+ }
1690
+ function analyzeHandler(p, h) {
1691
+ const { rel, sf, fn } = h;
1692
+ const loc2 = (n) => ({ file: rel, line: lineOf(sf, n) });
1693
+ const acc = {
1694
+ inputs: [],
1695
+ authChecks: [],
1696
+ queries: [],
1697
+ metadataAccesses: [],
1698
+ visited: /* @__PURE__ */ new Set()
1699
+ };
1700
+ const frame = {
1701
+ rel,
1702
+ sf,
1703
+ facts: h.facts,
1704
+ fn,
1705
+ depth: 0,
1706
+ via: [],
1707
+ inputNames: /* @__PURE__ */ new Set(["params", "searchParams"]),
1708
+ reqNames: /* @__PURE__ */ new Set(),
1709
+ clients: /* @__PURE__ */ new Map(),
1710
+ instances: /* @__PURE__ */ new Map(),
1711
+ cls: null,
1712
+ thisProps: /* @__PURE__ */ new Map()
1713
+ };
1714
+ const first = fn.parameters[0];
1715
+ if (h.kind === "route" && first) {
1716
+ if (ts5.isIdentifier(first.name)) frame.reqNames.add(first.name.text);
1717
+ else for (const nm of boundNames(first.name)) if (REQUEST_NAME.test(nm)) frame.reqNames.add(nm);
1718
+ }
1719
+ if (h.kind === "server_action") {
1720
+ const params = h.wrapper ? fn.parameters.slice(0, 1) : fn.parameters;
1721
+ for (const prm of params) {
1722
+ for (const nm of boundNames(prm.name)) {
1723
+ if (!acc.inputs.some((i) => i.kind === "action_arg" && i.name === nm)) {
1724
+ acc.inputs.push({ kind: "action_arg", name: nm, location: loc2(prm) });
1725
+ }
1726
+ frame.inputNames.add(nm);
1727
+ }
1728
+ }
1057
1729
  }
1058
- const entry = ctx.kind === "route" ? `${ctx.method} ${ctx.route}` : `server action ${ctx.route}`;
1059
- const stmt = enclosingStatement(ctx.node);
1730
+ if (h.wrapper && WRAPPER_AUTH.test(h.wrapper)) acc.authChecks.push(loc2(h.node));
1731
+ analyzeFrame(p, frame, acc);
1732
+ const entry = h.kind === "route" ? `${h.method} ${h.route}` : h.kind === "page" ? `PAGE ${h.route}` : `server action ${h.route}`;
1733
+ const stmt = enclosingStatement(h.node);
1060
1734
  const ignores = parseIgnoreDirectives(sf, stmt.getFullStart()).map((d) => ({
1061
1735
  ruleId: d.ruleId,
1062
1736
  reason: d.reason,
1063
1737
  location: { file: rel, line: d.line }
1064
1738
  }));
1065
1739
  return {
1066
- kind: ctx.kind,
1067
- route: ctx.route,
1068
- method: ctx.method,
1740
+ kind: h.kind,
1741
+ route: h.route,
1742
+ method: h.method,
1069
1743
  entry,
1070
- location: loc2(ctx.node),
1071
- inputs,
1072
- authChecks,
1073
- queries,
1074
- metadataAccesses,
1744
+ location: loc2(h.node),
1745
+ inputs: acc.inputs,
1746
+ authChecks: acc.authChecks,
1747
+ queries: acc.queries,
1748
+ metadataAccesses: acc.metadataAccesses,
1075
1749
  ignores
1076
1750
  };
1077
1751
  }
@@ -1087,7 +1761,7 @@ function findExposures(rel, sf) {
1087
1761
  });
1088
1762
  }
1089
1763
  if (isClientComponentFile(sf)) {
1090
- for (const call of collect(sf, ts4.isCallExpression)) {
1764
+ for (const call of collect(sf, ts5.isCallExpression)) {
1091
1765
  if (!isCreateClientCall(call, sf)) continue;
1092
1766
  const c = classifyCreateClientCall(call, sf);
1093
1767
  if (c.kind === "service_role") {
@@ -1106,12 +1780,16 @@ function findExposures(rel, sf) {
1106
1780
  }
1107
1781
  function parseProject(rootInput, opts = {}) {
1108
1782
  const root = resolve2(rootInput);
1109
- const { source, sql } = discoverFiles(root, opts.sqlDirs ?? [], opts.ignore ?? []);
1783
+ const { source, sql, manifests, tsconfigs } = discoverFiles(
1784
+ root,
1785
+ opts.sqlDirs ?? [],
1786
+ opts.ignore ?? []
1787
+ );
1110
1788
  const warnings = [];
1111
1789
  const sources = /* @__PURE__ */ new Map();
1112
1790
  for (const rel of source) {
1113
1791
  try {
1114
- sources.set(rel, parseSource(rel, readFileSync(join2(root, rel), "utf8")));
1792
+ sources.set(rel, parseSource(rel, readFileSync2(join3(root, rel), "utf8")));
1115
1793
  } catch (e) {
1116
1794
  warnings.push(`could not read ${rel}: ${e instanceof Error ? e.message : String(e)}`);
1117
1795
  }
@@ -1121,12 +1799,20 @@ function parseProject(rootInput, opts = {}) {
1121
1799
  const tables = /* @__PURE__ */ new Map();
1122
1800
  for (const rel of sql) {
1123
1801
  try {
1124
- parseSqlForRls(rel, readFileSync(resolve2(root, rel), "utf8"), tables);
1802
+ parseSqlForRls(rel, readFileSync2(resolve2(root, rel), "utf8"), tables);
1125
1803
  } catch (e) {
1126
1804
  warnings.push(`could not read ${rel}: ${e instanceof Error ? e.message : String(e)}`);
1127
1805
  }
1128
1806
  }
1129
- const files = new Set(source);
1807
+ const project = {
1808
+ sources,
1809
+ registry,
1810
+ resolver: new Resolver(root, new Set(source), manifests, tsconfigs, warnings),
1811
+ scopes: /* @__PURE__ */ new Map(),
1812
+ factoryOfFn: /* @__PURE__ */ new Map(),
1813
+ varBindings: /* @__PURE__ */ new Map(),
1814
+ warnings
1815
+ };
1130
1816
  const routes = [];
1131
1817
  const exposures = [];
1132
1818
  const fileIgnores = {};
@@ -1139,34 +1825,56 @@ function parseProject(rootInput, opts = {}) {
1139
1825
  if (top.length > 0) fileIgnores[rel] = top;
1140
1826
  exposures.push(...findExposures(rel, sf));
1141
1827
  const facts = registry.get(rel) ?? analyzeModule(rel, sf);
1142
- const common = { rel, sf, facts, registry, files, warnings };
1143
1828
  const route = routeFromFile(rel);
1144
1829
  if (route) {
1145
1830
  for (const { method, exported } of routeHandlersIn(sf)) {
1146
1831
  routes.push(
1147
- analyzeHandler({
1148
- ...common,
1832
+ analyzeHandler(project, {
1833
+ rel,
1834
+ sf,
1835
+ facts,
1149
1836
  kind: "route",
1150
1837
  route,
1151
1838
  method,
1152
1839
  fn: exported.fn,
1153
- node: exported.node
1840
+ node: exported.node,
1841
+ wrapper: exported.wrapper
1154
1842
  })
1155
1843
  );
1156
1844
  }
1157
1845
  } else if (isServerActionFile(sf)) {
1158
1846
  for (const ex of exportedFunctions(sf)) {
1159
1847
  routes.push(
1160
- analyzeHandler({
1161
- ...common,
1848
+ analyzeHandler(project, {
1849
+ rel,
1850
+ sf,
1851
+ facts,
1162
1852
  kind: "server_action",
1163
1853
  route: ex.name,
1164
1854
  method: "ACTION",
1165
1855
  fn: ex.fn,
1166
- node: ex.node
1856
+ node: ex.node,
1857
+ wrapper: ex.wrapper
1167
1858
  })
1168
1859
  );
1169
1860
  }
1861
+ } else {
1862
+ const page = pageFromFile(rel);
1863
+ const handler = page !== null && !isClientComponentFile(sf) ? pageHandlerIn(sf) : null;
1864
+ if (page !== null && handler) {
1865
+ const analysed = analyzeHandler(project, {
1866
+ rel,
1867
+ sf,
1868
+ facts,
1869
+ kind: "page",
1870
+ route: page,
1871
+ method: "PAGE",
1872
+ fn: handler.fn,
1873
+ node: handler.node,
1874
+ wrapper: handler.wrapper
1875
+ });
1876
+ if (analysed.queries.length > 0 || analysed.inputs.length > 0) routes.push(analysed);
1877
+ }
1170
1878
  }
1171
1879
  }
1172
1880
  routes.sort((a, b) => a.entry.localeCompare(b.entry));
@@ -1241,6 +1949,9 @@ function queryViews(ctx, handler) {
1241
1949
  function locations(...refs) {
1242
1950
  return refs.filter((r) => r !== void 0);
1243
1951
  }
1952
+ function viaNote(q) {
1953
+ return q.via && q.via.length > 0 ? ` Reached through ${q.via.join(" -> ")}.` : "";
1954
+ }
1244
1955
  function rlsNote(t, tableName) {
1245
1956
  if (!t?.known) return `public.${tableName} was not found in migrations; RLS state unknown.`;
1246
1957
  if (!t.rlsEnabled) return `RLS is disabled on public.${tableName}.`;
@@ -1292,7 +2003,7 @@ var serviceRoleObjectAccessWithoutTenantScope = {
1292
2003
  const evidence = [
1293
2004
  {
1294
2005
  kind: "rule",
1295
- summary: `${q.operation} on public.${tableName} filtered by user-controlled "${idFilter.column}" through a service-role client, with no tenant/owner scoping. ${authNote} ${rlsNote(v.tableData, tableName)}`,
2006
+ summary: `${q.operation} on public.${tableName} filtered by user-controlled "${idFilter.column}" through a service-role client, with no tenant/owner scoping. ${authNote} ${rlsNote(v.tableData, tableName)}${viaNote(q)}`,
1296
2007
  locations: locations(h.handler.location, v.query.location, v.client?.location),
1297
2008
  data: {
1298
2009
  deterministic: false,
@@ -1326,46 +2037,75 @@ var tableWithoutRls = {
1326
2037
  confidence: 0.9,
1327
2038
  cwe: ["CWE-284", "CWE-862"],
1328
2039
  evaluate(ctx) {
1329
- const out = [];
1330
- const seen = /* @__PURE__ */ new Set();
2040
+ const groups = /* @__PURE__ */ new Map();
1331
2041
  for (const h of handlerViews(ctx)) {
1332
2042
  for (const v of queryViews(ctx, h.handler)) {
1333
2043
  const c = v.clientData;
1334
2044
  if (c?.kind !== "anon" && c?.kind !== "user_scoped") continue;
1335
2045
  const t = v.tableData;
1336
2046
  if (!t?.known || t.rlsEnabled) continue;
1337
- const key = `${t.table}:${h.data.entry}`;
1338
- if (seen.has(key)) continue;
1339
- seen.add(key);
1340
- const path = [
1341
- "HTTP request",
1342
- h.data.entry,
1343
- `${c.name} (${c.kind}, RLS would apply)`,
1344
- `public.${t.table} (RLS disabled)`
1345
- ];
1346
- out.push(
1347
- finding(ctx, this, {
1348
- title: `Table "${t.table}" is exposed without RLS`,
1349
- entrypoints: [h.data.entry],
1350
- sources: h.inputs.map((i) => `${i.kind}:${i.name}`),
1351
- sinks: [`supabase.${v.data.operation}:public.${t.table}`],
1352
- path,
1353
- evidence: [
1354
- {
1355
- kind: "rule",
1356
- summary: `public.${t.table} has no "enable row level security" in migrations but is queried with a ${c.kind} client. Anyone holding the public anon key can read every row directly through PostgREST.`,
1357
- locations: locations(v.query.location, v.table?.location),
1358
- data: { deterministic: true, ruleId: this.id }
1359
- },
1360
- { kind: "trace", summary: path.join(" -> ") }
1361
- ]
1362
- })
1363
- );
2047
+ const g = group(groups, t.table, () => ({
2048
+ path: [
2049
+ "HTTP request",
2050
+ h.data.entry,
2051
+ `${c.name} (${c.kind}, RLS would apply)`,
2052
+ `public.${t.table} (RLS disabled)`
2053
+ ],
2054
+ summary: `public.${t.table} has no "enable row level security" in migrations but is queried with a ${c.kind} client. Anyone holding the public anon key can read every row directly through PostgREST.`,
2055
+ title: `Table "${t.table}" is exposed without RLS`,
2056
+ data: { deterministic: true, ruleId: this.id },
2057
+ tail: locations(v.table?.location)
2058
+ }));
2059
+ addReach(g, h, v, `supabase.${v.data.operation}:public.${t.table}`);
1364
2060
  }
1365
2061
  }
1366
- return out;
2062
+ return emitGroups(ctx, this, groups);
1367
2063
  }
1368
2064
  };
2065
+ function group(groups, key, init) {
2066
+ let g = groups.get(key);
2067
+ if (!g) {
2068
+ g = { ...init(), entrypoints: [], sources: /* @__PURE__ */ new Set(), sinks: /* @__PURE__ */ new Set(), queryLocations: [] };
2069
+ groups.set(key, g);
2070
+ }
2071
+ return g;
2072
+ }
2073
+ function addReach(g, h, v, sink) {
2074
+ if (!g.entrypoints.includes(h.data.entry)) g.entrypoints.push(h.data.entry);
2075
+ for (const i of h.inputs) g.sources.add(`${i.kind}:${i.name}`);
2076
+ g.sinks.add(sink);
2077
+ if (v.query.location && !g.queryLocations.some((l) => sameRef(l, v.query.location))) {
2078
+ g.queryLocations.push(v.query.location);
2079
+ }
2080
+ }
2081
+ function sameRef(a, b) {
2082
+ return b !== void 0 && a.file === b.file && a.line === b.line;
2083
+ }
2084
+ function emitGroups(ctx, rule, groups) {
2085
+ const out = [];
2086
+ for (const g of groups.values()) {
2087
+ const reached = g.entrypoints.length > 1 ? ` Reached from ${g.entrypoints.length} entry points.` : "";
2088
+ out.push(
2089
+ finding(ctx, rule, {
2090
+ title: g.title,
2091
+ entrypoints: g.entrypoints,
2092
+ sources: [...g.sources],
2093
+ sinks: [...g.sinks],
2094
+ path: g.path,
2095
+ evidence: [
2096
+ {
2097
+ kind: "rule",
2098
+ summary: g.summary + reached,
2099
+ locations: [...g.queryLocations, ...g.tail],
2100
+ data: g.data
2101
+ },
2102
+ { kind: "trace", summary: g.path.join(" -> ") }
2103
+ ]
2104
+ })
2105
+ );
2106
+ }
2107
+ return out;
2108
+ }
1369
2109
  var rlsPolicyWithoutCallerPredicate = {
1370
2110
  id: "supabase.rls-policy-without-caller-predicate",
1371
2111
  title: "RLS policy grants rows without a caller predicate",
@@ -1374,8 +2114,7 @@ var rlsPolicyWithoutCallerPredicate = {
1374
2114
  confidence: 0.85,
1375
2115
  cwe: ["CWE-863", "CWE-284"],
1376
2116
  evaluate(ctx) {
1377
- const out = [];
1378
- const seen = /* @__PURE__ */ new Set();
2117
+ const groups = /* @__PURE__ */ new Map();
1379
2118
  for (const h of handlerViews(ctx)) {
1380
2119
  for (const v of queryViews(ctx, h.handler)) {
1381
2120
  const c = v.clientData;
@@ -1388,37 +2127,23 @@ var rlsPolicyWithoutCallerPredicate = {
1388
2127
  if (p.command !== "all" && p.command !== op) continue;
1389
2128
  const expr = op === "insert" ? p.check : p.using;
1390
2129
  if (expr === null || policyScopesToCaller(expr)) continue;
1391
- const key = `${t.table}:${p.name}:${h.data.entry}`;
1392
- if (seen.has(key)) continue;
1393
- seen.add(key);
1394
- const path = [
1395
- "HTTP request",
1396
- h.data.entry,
1397
- `${c.name} (${c.kind})`,
1398
- `public.${t.table} policy "${p.name}" ${op === "insert" ? "with check" : "using"} (${expr})`
1399
- ];
1400
- out.push(
1401
- finding(ctx, this, {
1402
- title: `RLS policy "${p.name}" on "${t.table}" does not scope rows to the caller`,
1403
- entrypoints: [h.data.entry],
1404
- sources: h.inputs.map((i) => `${i.kind}:${i.name}`),
1405
- sinks: [`supabase.${op}:public.${t.table}`],
1406
- path,
1407
- evidence: [
1408
- {
1409
- kind: "rule",
1410
- summary: `Policy "${p.name}" for ${p.command} on public.${t.table} uses (${expr}). The table has a scope column (${t.columns.filter(isScopeColumn).join(", ")}) but the policy never compares it to auth.uid() or the caller's tenant, so RLS lets every ${p.roles.join("/") || "authenticated"} user through.`,
1411
- locations: locations(v.query.location, p.location),
1412
- data: { deterministic: false, ruleId: this.id, policy: p.name }
1413
- },
1414
- { kind: "trace", summary: path.join(" -> ") }
1415
- ]
1416
- })
1417
- );
2130
+ const g = group(groups, `${t.table}:${p.name}:${p.command}`, () => ({
2131
+ path: [
2132
+ "HTTP request",
2133
+ h.data.entry,
2134
+ `${c.name} (${c.kind})`,
2135
+ `public.${t.table} policy "${p.name}" ${op === "insert" ? "with check" : "using"} (${expr})`
2136
+ ],
2137
+ summary: `Policy "${p.name}" for ${p.command} on public.${t.table} uses (${expr}). The table has a scope column (${t.columns.filter(isScopeColumn).join(", ")}) but the policy never compares it to auth.uid() or the caller's tenant, so RLS lets every ${p.roles.join("/") || "authenticated"} user through.`,
2138
+ title: `RLS policy "${p.name}" on "${t.table}" does not scope rows to the caller`,
2139
+ data: { deterministic: false, ruleId: this.id, policy: p.name },
2140
+ tail: locations(p.location)
2141
+ }));
2142
+ addReach(g, h, v, `supabase.${op}:public.${t.table}`);
1418
2143
  }
1419
2144
  }
1420
2145
  }
1421
- return out;
2146
+ return emitGroups(ctx, this, groups);
1422
2147
  }
1423
2148
  };
1424
2149
  var userControlledTenantScope = {
@@ -1698,10 +2423,10 @@ var defaultRules = [...supabaseAuthorizationPack];
1698
2423
 
1699
2424
  // packages/scanner/src/scan.ts
1700
2425
  function readAuditConfig(root) {
1701
- const p = join3(root, "audit.config.json");
2426
+ const p = join4(root, "audit.config.json");
1702
2427
  if (!existsSync(p)) return {};
1703
2428
  try {
1704
- const raw = JSON.parse(readFileSync2(p, "utf8"));
2429
+ const raw = JSON.parse(readFileSync3(p, "utf8"));
1705
2430
  return {
1706
2431
  ...Array.isArray(raw.ignore) ? { ignore: raw.ignore.filter((x) => typeof x === "string") } : {},
1707
2432
  ...Array.isArray(raw.migrations) ? { migrations: raw.migrations.filter((x) => typeof x === "string") } : {}