auditai-scan 0.4.0 → 0.6.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 +1431 -415
  2. package/package.json +1 -1
@@ -78,20 +78,20 @@ var IllegalTransitionError = class extends Error {
78
78
  to;
79
79
  name = "IllegalTransitionError";
80
80
  };
81
- function transition(finding3, to, opts = {}) {
82
- if (!canTransition(finding3.status, to)) {
83
- throw new IllegalTransitionError(finding3.id, finding3.status, to);
81
+ function transition(finding4, to, opts = {}) {
82
+ if (!canTransition(finding4.status, to)) {
83
+ throw new IllegalTransitionError(finding4.id, finding4.status, to);
84
84
  }
85
85
  const requiresEvidence = to === "confirmed" || to === "verified";
86
86
  if (requiresEvidence && !opts.evidence) {
87
- throw new Error(`Finding ${finding3.id}: transition to ${to} requires evidence`);
87
+ throw new Error(`Finding ${finding4.id}: transition to ${to} requires evidence`);
88
88
  }
89
- if (to === "verified" && !isVerificationPassing(finding3.verification)) {
90
- throw new Error(`Finding ${finding3.id}: cannot mark verified without a passing verification`);
89
+ if (to === "verified" && !isVerificationPassing(finding4.verification)) {
90
+ throw new Error(`Finding ${finding4.id}: cannot mark verified without a passing verification`);
91
91
  }
92
- const evidence = opts.evidence ? [...finding3.evidence, opts.evidence] : finding3.evidence;
92
+ const evidence = opts.evidence ? [...finding4.evidence, opts.evidence] : finding4.evidence;
93
93
  return {
94
- ...finding3,
94
+ ...finding4,
95
95
  status: to,
96
96
  evidence,
97
97
  updatedAt: opts.now ?? (/* @__PURE__ */ new Date()).toISOString()
@@ -100,8 +100,8 @@ function transition(finding3, to, opts = {}) {
100
100
  function isVerificationPassing(v) {
101
101
  return v !== void 0 && v.securityTestBefore === "failed" && v.securityTestAfter === "passed" && (v.existingTests === "passed" || v.existingTests === "skipped") && v.rescan === "passed";
102
102
  }
103
- function isDeterministic(finding3) {
104
- return finding3.evidence.some((e) => e.kind === "rule" && e.data?.deterministic === true);
103
+ function isDeterministic(finding4) {
104
+ return finding4.evidence.some((e) => e.kind === "rule" && e.data?.deterministic === true);
105
105
  }
106
106
  var DEFAULT_BLOCKING_POLICY = {
107
107
  minConfidence: 0.8,
@@ -109,14 +109,14 @@ var DEFAULT_BLOCKING_POLICY = {
109
109
  blockDeterministicCritical: true
110
110
  };
111
111
  var CONFIRMED_OR_LATER = ["confirmed", "fix_proposed", "fix_applied"];
112
- function isBlocking(finding3, policy = DEFAULT_BLOCKING_POLICY) {
113
- if (finding3.status === "suppressed") return false;
114
- if (finding3.status === "verified") return true;
115
- if (finding3.confidence < policy.minConfidence) return false;
116
- if (CONFIRMED_OR_LATER.includes(finding3.status) && policy.blockOnConfirmedSeverities.includes(finding3.severity)) {
112
+ function isBlocking(finding4, policy = DEFAULT_BLOCKING_POLICY) {
113
+ if (finding4.status === "suppressed") return false;
114
+ if (finding4.status === "verified") return true;
115
+ if (finding4.confidence < policy.minConfidence) return false;
116
+ if (CONFIRMED_OR_LATER.includes(finding4.status) && policy.blockOnConfirmedSeverities.includes(finding4.severity)) {
117
117
  return true;
118
118
  }
119
- if (policy.blockDeterministicCritical && finding3.severity === "critical" && isDeterministic(finding3) && finding3.status !== "unverified") {
119
+ if (policy.blockDeterministicCritical && finding4.severity === "critical" && isDeterministic(finding4) && finding4.status !== "unverified") {
120
120
  return true;
121
121
  }
122
122
  return false;
@@ -158,9 +158,15 @@ function formatScanText(r) {
158
158
  const s = r.summary;
159
159
  const out = [
160
160
  `Audit AI scan ${displayRoot(s.root)}`,
161
- `Files ${s.files} \xB7 Routes ${s.routes} \xB7 Supabase queries ${s.queries} \xB7 Tables with RLS ${s.tablesWithRls}/${s.tablesKnown} \xB7 Rules ${s.rules}`,
162
- ""
161
+ `Files ${s.files} \xB7 Routes ${s.routes} \xB7 Supabase queries ${s.queries} \xB7 Tables with RLS ${s.tablesWithRls}/${s.tablesKnown} \xB7 Rules ${s.rules}`
163
162
  ];
163
+ if (s.publicTables.length > 0) {
164
+ const n = r.findings.filter((f) => f.evidence.some((e) => e.data?.publicTables)).length;
165
+ out.push(
166
+ `Declared public in audit.config.json: ${s.publicTables.join(", ")} (${n} read-only finding${n === 1 ? "" : "s"} suppressed by the declaration; write paths are never covered)`
167
+ );
168
+ }
169
+ out.push("");
164
170
  if (r.findings.length === 0) {
165
171
  out.push(
166
172
  `No findings. ${s.routes} route${s.routes === 1 ? "" : "s"} and ${s.queries} quer${s.queries === 1 ? "y" : "ies"} checked.`
@@ -181,6 +187,9 @@ function formatScanText(r) {
181
187
  `;
182
188
  }
183
189
 
190
+ // packages/scanner/src/scan.ts
191
+ import { statSync as statSync2 } from "node:fs";
192
+
184
193
  // packages/graph/src/graph.ts
185
194
  var SecurityGraph = class {
186
195
  nodes = /* @__PURE__ */ new Map();
@@ -255,7 +264,8 @@ function buildGraph(model) {
255
264
  method: h.method,
256
265
  route: h.route,
257
266
  inputs: h.inputs,
258
- metadataAccesses: h.metadataAccesses
267
+ metadataAccesses: h.metadataAccesses,
268
+ roleChecks: h.roleChecks ?? []
259
269
  };
260
270
  const handler = g.addNode({
261
271
  id: `handler:${h.location.file}:${h.location.line}`,
@@ -295,7 +305,8 @@ function buildGraph(model) {
295
305
  table: q.table,
296
306
  ...q.via && q.via.length > 0 ? { via: q.via } : {},
297
307
  ...q.storage ? { storage: q.storage } : {},
298
- ...q.guard ? { guard: q.guard } : {}
308
+ ...q.guard ? { guard: q.guard } : {},
309
+ ...q.ownerChecks ? { ownerChecks: q.ownerChecks } : {}
299
310
  };
300
311
  const qn = g.addNode({
301
312
  // Per handler: the same helper query reached from two entry points carries different
@@ -865,7 +876,7 @@ function isClientComponentFile(sf) {
865
876
  // packages/parser/src/parse-project.ts
866
877
  import { readFileSync as readFileSync2 } from "node:fs";
867
878
  import { join as join3, resolve as resolve2 } from "node:path";
868
- import ts10 from "typescript";
879
+ import ts11 from "typescript";
869
880
 
870
881
  // packages/parser/src/auth-evidence.ts
871
882
  import ts3 from "typescript";
@@ -1180,6 +1191,36 @@ function missingRowExit(tail, segments) {
1180
1191
  }
1181
1192
  return null;
1182
1193
  }
1194
+ var INEQUALITY = /* @__PURE__ */ new Set([
1195
+ ts4.SyntaxKind.ExclamationEqualsEqualsToken,
1196
+ ts4.SyntaxKind.ExclamationEqualsToken
1197
+ ]);
1198
+ function rowComparisons(tail) {
1199
+ const { parent } = outerOf(tail);
1200
+ if (!parent || !ts4.isVariableDeclaration(parent)) return [];
1201
+ const { data } = resultNames(parent.name);
1202
+ const out = [];
1203
+ for (const s of ifsAfter(parent)) {
1204
+ const exit = exitKind(s.thenStatement);
1205
+ if (!exit) continue;
1206
+ walk(s.expression, (n) => {
1207
+ if (!ts4.isBinaryExpression(n) || !INEQUALITY.has(n.operatorToken.kind)) return void 0;
1208
+ for (const [a, b] of [
1209
+ [n.left, n.right],
1210
+ [n.right, n.left]
1211
+ ]) {
1212
+ const ua = unwrap(a);
1213
+ if (!ts4.isPropertyAccessExpression(ua)) continue;
1214
+ const r = rootName(ua.expression);
1215
+ if (r === null || !data.has(r)) continue;
1216
+ out.push({ column: ua.name.text, value: b, exit });
1217
+ break;
1218
+ }
1219
+ return void 0;
1220
+ });
1221
+ }
1222
+ return out;
1223
+ }
1183
1224
  function callResultChecked(call) {
1184
1225
  const { node, parent } = outerOf(call);
1185
1226
  if (!parent) return false;
@@ -1452,10 +1493,17 @@ var Resolver = class {
1452
1493
  }
1453
1494
  this.packages.sort((a, b) => b.name.length - a.name.length);
1454
1495
  for (const rel of tsconfigs) {
1455
- const read = ts6.readConfigFile(join2(root, rel), (p) => readFileSync(p, "utf8"));
1456
- const config = read.config;
1457
- if (read.error || !config) continue;
1458
- const co = config.compilerOptions ?? {};
1496
+ let config;
1497
+ try {
1498
+ const read = ts6.readConfigFile(join2(root, rel), (p) => readFileSync(p, "utf8"));
1499
+ if (read.error) continue;
1500
+ config = read.config;
1501
+ } catch (e) {
1502
+ warnings.push(`could not read ${rel}: ${e instanceof Error ? e.message : String(e)}`);
1503
+ continue;
1504
+ }
1505
+ if (!config || typeof config !== "object") continue;
1506
+ const co = config.compilerOptions && typeof config.compilerOptions === "object" ? config.compilerOptions : {};
1459
1507
  const paths = co.paths;
1460
1508
  if (!paths || typeof paths !== "object") continue;
1461
1509
  const dir = dirOf(rel);
@@ -1556,7 +1604,7 @@ function isIdentPart(code) {
1556
1604
  function isDigit(code) {
1557
1605
  return code >= 48 && code <= 57;
1558
1606
  }
1559
- function readQuoted(text, from, quote, backslashEscapes) {
1607
+ function readQuoted(text, from, quote2, backslashEscapes) {
1560
1608
  let value = "";
1561
1609
  let j = from + 1;
1562
1610
  while (j < text.length) {
@@ -1567,9 +1615,9 @@ function readQuoted(text, from, quote, backslashEscapes) {
1567
1615
  j += 2;
1568
1616
  continue;
1569
1617
  }
1570
- if (c === quote) {
1571
- if (text.charAt(j + 1) === quote) {
1572
- value += quote;
1618
+ if (c === quote2) {
1619
+ if (text.charAt(j + 1) === quote2) {
1620
+ value += quote2;
1573
1621
  j += 2;
1574
1622
  continue;
1575
1623
  }
@@ -2091,6 +2139,213 @@ function parseTableConstraint(tokens) {
2091
2139
  return { kind: "other", name };
2092
2140
  }
2093
2141
 
2142
+ // packages/parser/src/sql-do-loops.ts
2143
+ function placeholders(fmt) {
2144
+ const out = [];
2145
+ let next = 0;
2146
+ for (let i = 0; i < fmt.length; i += 1) {
2147
+ if (fmt.charAt(i) !== "%") continue;
2148
+ const rest = fmt.slice(i + 1);
2149
+ if (rest.startsWith("%")) {
2150
+ out.push({ kind: "s", arg: -1, start: i, end: i + 2 });
2151
+ i += 1;
2152
+ continue;
2153
+ }
2154
+ const m = /^(?:(\d+)\$)?([IsL])/.exec(rest);
2155
+ if (!m) return null;
2156
+ const arg = m[1] !== void 0 ? Number(m[1]) - 1 : next;
2157
+ next = arg + 1;
2158
+ out.push({ kind: m[2], arg, start: i, end: i + 1 + m[0].length });
2159
+ i += m[0].length;
2160
+ }
2161
+ return out;
2162
+ }
2163
+ function quoteIdent(v) {
2164
+ return /^[a-z_][a-z0-9_]*$/.test(v) ? v : `"${v.replace(/"/g, '""')}"`;
2165
+ }
2166
+ function render(fmt, args) {
2167
+ const ph = placeholders(fmt);
2168
+ if (ph === null) return null;
2169
+ let out = "";
2170
+ let from = 0;
2171
+ for (const p of ph) {
2172
+ out += fmt.slice(from, p.start);
2173
+ from = p.end;
2174
+ if (p.arg < 0) {
2175
+ out += "%";
2176
+ continue;
2177
+ }
2178
+ const v = args[p.arg];
2179
+ if (v === void 0) return null;
2180
+ out += p.kind === "I" ? quoteIdent(v) : p.kind === "L" ? `'${v.replace(/'/g, "''")}'` : v;
2181
+ }
2182
+ return out + fmt.slice(from);
2183
+ }
2184
+ function literalArg(part, loopVar, element) {
2185
+ const first = part[0];
2186
+ if (!first) return null;
2187
+ if (part.length === 1 && first.kind === "string") return first.value;
2188
+ if (loopVar === null || element === null) return null;
2189
+ const name = identOf(first);
2190
+ if (name === null || name.toLowerCase() !== loopVar) return null;
2191
+ if (part.length === 1) return element;
2192
+ if (part.length === 3 && isPunct(part[1], ".") && identOf(part[2]) !== null) return element;
2193
+ return null;
2194
+ }
2195
+ function executedSql(tk, loopVar, element) {
2196
+ if (!isWord(tk[0], "execute")) return null;
2197
+ const a = tk[1];
2198
+ if (a?.kind === "string" && tk.length === 2) return a.value;
2199
+ if (!isWord(a, "format") || !isPunct(tk[2], "(")) return null;
2200
+ const parts = splitTopLevelTokens(groupInner(tk, 2));
2201
+ const fmtTok = parts[0]?.[0];
2202
+ if (!fmtTok || parts[0]?.length !== 1 || fmtTok.kind !== "string") return null;
2203
+ const args = [];
2204
+ for (const part of parts.slice(1)) {
2205
+ const v = literalArg(part, loopVar, element);
2206
+ if (v === null) return null;
2207
+ args.push(v);
2208
+ }
2209
+ return render(fmtTok.value, args);
2210
+ }
2211
+ function literalElements(expr) {
2212
+ const arrayAt = expr.findIndex((t, i) => isWord(t, "array") && isPunct(expr[i + 1], "["));
2213
+ if (arrayAt >= 0) {
2214
+ const out = [];
2215
+ for (const part of splitTopLevelTokens(groupInner(expr, arrayAt + 1))) {
2216
+ const t = part[0];
2217
+ if (!t || part.length !== 1 || t.kind !== "string") return null;
2218
+ out.push(t.value);
2219
+ }
2220
+ return out;
2221
+ }
2222
+ const valuesAt = expr.findIndex((t) => isWord(t, "values"));
2223
+ if (valuesAt >= 0) {
2224
+ const out = [];
2225
+ let rows = expr.slice(valuesAt + 1);
2226
+ const close = rows.findIndex((t, i) => isPunct(t, ")") && depthAt(rows, i) < 0);
2227
+ if (close >= 0) rows = rows.slice(0, close);
2228
+ for (const part of splitTopLevelTokens(rows)) {
2229
+ if (!isPunct(part[0], "(")) return null;
2230
+ const inner = groupInner(part, 0);
2231
+ const t = inner[0];
2232
+ if (!t || inner.length !== 1 || t.kind !== "string") return null;
2233
+ out.push(t.value);
2234
+ }
2235
+ return out;
2236
+ }
2237
+ return null;
2238
+ }
2239
+ function depthAt(tokens, i) {
2240
+ let depth = 0;
2241
+ for (let j = 0; j < i; j += 1) {
2242
+ if (isPunct(tokens[j], "(") || isPunct(tokens[j], "[")) depth += 1;
2243
+ else if (isPunct(tokens[j], ")") || isPunct(tokens[j], "]")) depth -= 1;
2244
+ }
2245
+ return depth;
2246
+ }
2247
+ function loopHeader(tk, declared) {
2248
+ const foreach = isWord(tk[0], "foreach");
2249
+ if (!foreach && !isWord(tk[0], "for")) return null;
2250
+ const variable = identOf(tk[1])?.toLowerCase();
2251
+ if (variable === void 0 || !isWord(tk[2], "in")) return null;
2252
+ const loopAt = findWord(tk, 3, "loop");
2253
+ if (loopAt < 0) return null;
2254
+ let from = 3;
2255
+ if (foreach && isWord(tk[from], "array")) from += 1;
2256
+ if (isWord(tk[from], "reverse")) return { variable, elements: null, rest: [] };
2257
+ const expr = tk.slice(from, loopAt);
2258
+ const rest = tk.slice(loopAt + 1);
2259
+ const name = expr.length === 1 ? identOf(expr[0])?.toLowerCase() : void 0;
2260
+ if (name !== void 0) return { variable, elements: declared.get(name) ?? null, rest };
2261
+ const elements = expr.some((t) => t.kind === "op" && t.raw === "..") ? null : literalElements(expr);
2262
+ return { variable, elements, rest };
2263
+ }
2264
+ function declaredList(tk) {
2265
+ let i = 0;
2266
+ if (isWord(tk[i], "declare")) i += 1;
2267
+ const name = identOf(tk[i])?.toLowerCase();
2268
+ if (name === void 0) return null;
2269
+ const assign = tk.findIndex(
2270
+ (t, j) => j > i && (t.kind === "op" && t.raw === ":" && tk[j + 1]?.raw === "=" || isWord(t, "default"))
2271
+ );
2272
+ if (assign < 0) return null;
2273
+ const elements = literalElements(tk.slice(assign + (isWord(tk[assign], "default") ? 1 : 2)));
2274
+ return elements === null ? null : [name, elements];
2275
+ }
2276
+ function stripBegin(tk) {
2277
+ let i = 0;
2278
+ while (isWord(tk[i], "begin")) i += 1;
2279
+ return tk.slice(i);
2280
+ }
2281
+ function conditionalDelta(tk) {
2282
+ if (isWord(tk[0], "if") && findWord(tk, 1, "then") >= 0 || isWord(tk[0], "case")) return 1;
2283
+ if (isWord(tk[0], "end") && (isWord(tk[1], "if") || isWord(tk[1], "case"))) return -1;
2284
+ return 0;
2285
+ }
2286
+ function expandDoBlock(stmt) {
2287
+ const body = stmt.tokens.find((t) => t.kind === "string");
2288
+ const out = { statements: [], dynamic: false };
2289
+ if (!body || !isWord(stmt.tokens[0], "do")) return out;
2290
+ const inner = splitSqlStatements(body.value).map((s) => stripBegin(s.tokens));
2291
+ const emit = (sql) => {
2292
+ if (sql === null) {
2293
+ out.dynamic = true;
2294
+ return;
2295
+ }
2296
+ for (const s of splitSqlStatements(sql)) out.statements.push({ ...s, line: stmt.line });
2297
+ };
2298
+ const declared = /* @__PURE__ */ new Map();
2299
+ for (const tk of inner) {
2300
+ const d = declaredList(tk);
2301
+ if (d) declared.set(d[0], d[1]);
2302
+ }
2303
+ let conditional = 0;
2304
+ let loop = null;
2305
+ const bodyStatements = [];
2306
+ for (const tk of inner) {
2307
+ if (tk.length === 0) continue;
2308
+ if (loop) {
2309
+ if (isWord(tk[0], "end") && isWord(tk[1], "loop")) {
2310
+ if (loop.elements === null) out.dynamic = true;
2311
+ else runLoop(loop, bodyStatements, emit);
2312
+ loop = null;
2313
+ bodyStatements.length = 0;
2314
+ continue;
2315
+ }
2316
+ if (loopHeader(tk, declared)) {
2317
+ loop.elements = null;
2318
+ }
2319
+ bodyStatements.push(tk);
2320
+ continue;
2321
+ }
2322
+ const header = loopHeader(tk, declared);
2323
+ if (header) {
2324
+ loop = header;
2325
+ if (conditional > 0) loop.elements = null;
2326
+ if (header.rest.length > 0) bodyStatements.push(header.rest);
2327
+ continue;
2328
+ }
2329
+ conditional = Math.max(0, conditional + conditionalDelta(tk));
2330
+ if (isWord(tk[0], "execute")) emit(conditional > 0 ? null : executedSql(tk, null, null));
2331
+ }
2332
+ if (loop) out.dynamic = true;
2333
+ return out;
2334
+ }
2335
+ function runLoop(loop, body, emit) {
2336
+ const elements = loop.elements ?? [];
2337
+ let conditional = 0;
2338
+ for (const tk of body) {
2339
+ conditional = Math.max(0, conditional + conditionalDelta(tk));
2340
+ if (!isWord(tk[0], "execute")) continue;
2341
+ if (conditional > 0) {
2342
+ emit(null);
2343
+ continue;
2344
+ }
2345
+ for (const element of elements) emit(executedSql(tk, loop.variable, element));
2346
+ }
2347
+ }
2348
+
2094
2349
  // packages/parser/src/sql-functions.ts
2095
2350
  function newFunctionRegistry() {
2096
2351
  return {
@@ -2548,7 +2803,8 @@ function schemaStateFor(tables) {
2548
2803
  uniqueIndexes: /* @__PURE__ */ new Map(),
2549
2804
  enums: /* @__PURE__ */ new Map(),
2550
2805
  functions: newFunctionRegistry(),
2551
- buckets: newBucketRegistry()
2806
+ buckets: newBucketRegistry(),
2807
+ warnings: []
2552
2808
  };
2553
2809
  STATES.set(tables, state);
2554
2810
  }
@@ -3022,7 +3278,8 @@ function finishSchema(state) {
3022
3278
  // fromEntries defines own properties, so a hostile type name like "__proto__" stays a plain key.
3023
3279
  enums: Object.fromEntries([...state.enums].map(([k, v]) => [k, [...v]])),
3024
3280
  sqlFunctions: finishFunctions(state.functions),
3025
- storageBuckets: finishBuckets(state.buckets)
3281
+ storageBuckets: finishBuckets(state.buckets),
3282
+ warnings: [...state.warnings]
3026
3283
  };
3027
3284
  }
3028
3285
 
@@ -3045,6 +3302,10 @@ var CREATE_POLICY = new RegExp(
3045
3302
  String.raw`^create\s+policy\s+("([^"]+)"|\S+)\s+on\s+${QUALIFIED}`,
3046
3303
  "i"
3047
3304
  );
3305
+ var DROP_POLICY = new RegExp(
3306
+ String.raw`^drop\s+policy\s+(?:if\s+exists\s+)?("([^"]+)"|\S+)\s+on\s+${QUALIFIED}`,
3307
+ "i"
3308
+ );
3048
3309
  function isAppliedSqlFile(rel) {
3049
3310
  return !/(?:^|\/)supabase\/migrations\/[^/]+\/.+\.sql$/i.test(rel.split("\\").join("/"));
3050
3311
  }
@@ -3052,48 +3313,166 @@ function parseSqlForRls(rel, text, into) {
3052
3313
  if (!isAppliedSqlFile(rel)) return;
3053
3314
  const state = schemaStateFor(into);
3054
3315
  for (const stmt of splitSqlStatements(text)) {
3055
- const cp = CREATE_POLICY.exec(stmt.text);
3056
- if (!cp?.[1] || !cp[4]) {
3057
- applySchemaStatement(state, stmt, rel);
3058
- continue;
3059
- }
3060
- const t = ensureTable(
3061
- state,
3062
- qualifiedKey({ schema: cp[3] ?? null, name: cp[4] }),
3063
- rel,
3064
- stmt.line
3065
- );
3066
- const name = cp[2] ?? cp[1];
3067
- const rest = stmt.text.slice(cp[0].length);
3068
- const cmdMatch = /\bfor\s+(select|insert|update|delete|all)\b/i.exec(rest);
3069
- const command = cmdMatch?.[1]?.toLowerCase() ?? "all";
3070
- const rolesMatch = /\bto\s+([A-Za-z_][A-Za-z0-9_]*(?:\s*,\s*[A-Za-z_][A-Za-z0-9_]*)*)/i.exec(
3071
- rest
3072
- );
3073
- const roles = rolesMatch?.[1] ? rolesMatch[1].split(/\s*,\s*/).map((r) => r.toLowerCase()) : [];
3074
- let using = null;
3075
- let check = null;
3076
- const u = /\busing\s*\(/i.exec(rest);
3077
- if (u) using = balanced(rest, u.index + u[0].length - 1)?.inner.trim() ?? null;
3078
- const c = /\bwith\s+check\s*\(/i.exec(rest);
3079
- if (c) check = balanced(rest, c.index + c[0].length - 1)?.inner.trim() ?? null;
3080
- t.policies.push(name);
3081
- t.policyDetails.push({
3082
- name,
3083
- command,
3084
- roles,
3085
- using,
3086
- check,
3087
- location: { file: rel, line: stmt.line }
3088
- });
3316
+ applyStatement(state, stmt, rel);
3317
+ if (!isWord(stmt.tokens[0], "do")) continue;
3318
+ const expanded = expandDoBlock(stmt);
3319
+ for (const s of expanded.statements) applyStatement(state, s, rel);
3320
+ if (expanded.dynamic) warnDynamicSql(state, rel);
3321
+ }
3322
+ }
3323
+ function warnDynamicSql(state, rel) {
3324
+ const w = `${rel}: a DO block runs dynamic SQL (a loop over a query, or an EXECUTE that is conditional or built from expressions); RLS, policies and privileges it sets are not seen`;
3325
+ if (!state.warnings.includes(w)) state.warnings.push(w);
3326
+ }
3327
+ var ROLE_NAME = '(?:"[A-Za-z_][A-Za-z0-9_]*"|[A-Za-z_][A-Za-z0-9_]*)';
3328
+ var ROLES = new RegExp(`\\bto\\s+(${ROLE_NAME}(?:\\s*,\\s*${ROLE_NAME})*)`, "i");
3329
+ function headEnd(rest) {
3330
+ const u = /\busing\s*\(/i.exec(rest);
3331
+ const c = /\bwith\s+check\s*\(/i.exec(rest);
3332
+ const ends = [u?.index, c?.index].filter((i) => i !== void 0);
3333
+ return ends.length === 0 ? rest.length : Math.min(...ends);
3334
+ }
3335
+ function applyStatement(state, stmt, rel) {
3336
+ const cp = CREATE_POLICY.exec(stmt.text);
3337
+ if (!cp?.[1] || !cp[4]) {
3338
+ dropPolicy(state, stmt) || applySchemaStatement(state, stmt, rel);
3339
+ return;
3089
3340
  }
3341
+ const t = ensureTable(
3342
+ state,
3343
+ qualifiedKey({ schema: cp[3] ?? null, name: cp[4] }),
3344
+ rel,
3345
+ stmt.line
3346
+ );
3347
+ const name = cp[2] ?? cp[1];
3348
+ const rest = stmt.text.slice(cp[0].length);
3349
+ const cmdMatch = /\bfor\s+(select|insert|update|delete|all)\b/i.exec(rest);
3350
+ const command = cmdMatch?.[1]?.toLowerCase() ?? "all";
3351
+ const head = rest.slice(0, headEnd(rest));
3352
+ const rolesMatch = ROLES.exec(head);
3353
+ const roles = rolesMatch?.[1] ? rolesMatch[1].split(/\s*,\s*/).map((r) => r.replace(/"/g, "").toLowerCase()) : [];
3354
+ let using = null;
3355
+ let check = null;
3356
+ const u = /\busing\s*\(/i.exec(rest);
3357
+ if (u) using = balanced(rest, u.index + u[0].length - 1)?.inner.trim() ?? null;
3358
+ const c = /\bwith\s+check\s*\(/i.exec(rest);
3359
+ if (c) check = balanced(rest, c.index + c[0].length - 1)?.inner.trim() ?? null;
3360
+ t.policies.push(name);
3361
+ t.policyDetails.push({
3362
+ name,
3363
+ command,
3364
+ roles,
3365
+ using,
3366
+ check,
3367
+ location: { file: rel, line: stmt.line }
3368
+ });
3369
+ }
3370
+ function dropPolicy(state, stmt) {
3371
+ const dp = DROP_POLICY.exec(stmt.text);
3372
+ if (!dp?.[1] || !dp[4]) return false;
3373
+ const name = dp[2] ?? dp[1].replace(/^"|"$/g, "");
3374
+ const t = state.tables.get(qualifiedKey({ schema: dp[3] ?? null, name: dp[4] }));
3375
+ if (t) {
3376
+ t.policies = t.policies.filter((p) => p !== name);
3377
+ t.policyDetails = t.policyDetails.filter((p) => p.name !== name);
3378
+ }
3379
+ return true;
3090
3380
  }
3091
3381
  function sqlSchemaFor(into) {
3092
3382
  return finishSchema(schemaStateFor(into));
3093
3383
  }
3094
3384
 
3095
- // packages/parser/src/storage.ts
3385
+ // packages/parser/src/role-gates.ts
3096
3386
  import ts7 from "typescript";
3387
+ var ROLE_PROPERTY = /^(role|roles|app_metadata|is_?admin|is_?superuser|permissions?|claims?)$/i;
3388
+ var EMAIL_PROPERTY = /^email$/i;
3389
+ function propertyPath(e) {
3390
+ const u = unwrap(e);
3391
+ if (ts7.isIdentifier(u)) return [u.text];
3392
+ if (ts7.isPropertyAccessExpression(u)) {
3393
+ const base = propertyPath(u.expression);
3394
+ return base === null ? null : [...base, u.name.text];
3395
+ }
3396
+ return null;
3397
+ }
3398
+ function isLiteral(e) {
3399
+ const u = unwrap(e);
3400
+ return ts7.isStringLiteralLike(u) || ts7.isNumericLiteral(u) || ts7.isArrayLiteralExpression(u) || u.kind === ts7.SyntaxKind.TrueKeyword || u.kind === ts7.SyntaxKind.FalseKeyword || u.kind === ts7.SyntaxKind.NullKeyword;
3401
+ }
3402
+ function sessionClaim(e, sessionNames) {
3403
+ const path = propertyPath(e);
3404
+ const root = path?.[0];
3405
+ const last = path?.[path.length - 1];
3406
+ if (!path || path.length < 2 || root === void 0 || last === void 0) return null;
3407
+ if (!sessionNames.has(root)) return null;
3408
+ if (path.some((p) => p === "user_metadata")) return null;
3409
+ if (path.some((p) => ROLE_PROPERTY.test(p))) return { source: path.join("."), kind: "role" };
3410
+ if (EMAIL_PROPERTY.test(last)) return { source: path.join("."), kind: "email" };
3411
+ return null;
3412
+ }
3413
+ function claimIn(cond, sessionNames) {
3414
+ let found = null;
3415
+ const visit = (n) => {
3416
+ if (found !== null) return;
3417
+ if (ts7.isCallExpression(n)) {
3418
+ for (const a of n.arguments) {
3419
+ const c = sessionClaim(a, sessionNames);
3420
+ if (c) {
3421
+ found = c.source;
3422
+ return;
3423
+ }
3424
+ }
3425
+ } else if (ts7.isBinaryExpression(n)) {
3426
+ for (const [a, b] of [
3427
+ [n.left, n.right],
3428
+ [n.right, n.left]
3429
+ ]) {
3430
+ const c = sessionClaim(a, sessionNames);
3431
+ if (!c) continue;
3432
+ if (c.kind === "role" || isLiteral(b) || envNamesIn(b).length > 0) {
3433
+ found = c.source;
3434
+ return;
3435
+ }
3436
+ }
3437
+ } else if (ts7.isPrefixUnaryExpression(n) || ts7.isPropertyAccessExpression(n)) {
3438
+ const target = ts7.isPrefixUnaryExpression(n) ? n.operand : n;
3439
+ const c = sessionClaim(target, sessionNames);
3440
+ if (c?.kind === "role" && !ts7.isCallExpression(n.parent)) {
3441
+ found = c.source;
3442
+ return;
3443
+ }
3444
+ }
3445
+ n.forEachChild(visit);
3446
+ };
3447
+ visit(cond);
3448
+ return found;
3449
+ }
3450
+ function sessionNamesIn(body, isSessionCall) {
3451
+ const out = /* @__PURE__ */ new Set();
3452
+ walkOwn(body, (n) => {
3453
+ if (!ts7.isVariableDeclaration(n) || !n.initializer) return;
3454
+ const init = unwrap(n.initializer);
3455
+ if (ts7.isCallExpression(init) && isSessionCall(init)) {
3456
+ for (const nm of boundNames(n.name)) out.add(nm);
3457
+ }
3458
+ });
3459
+ return out;
3460
+ }
3461
+ function roleGatesIn(body, sessionNames) {
3462
+ const out = [];
3463
+ if (sessionNames.size === 0) return out;
3464
+ walkOwn(body, (n) => {
3465
+ if (!ts7.isIfStatement(n)) return;
3466
+ const exit = exitKind(n.thenStatement);
3467
+ if (!exit) return;
3468
+ const source = claimIn(n.expression, sessionNames);
3469
+ if (source !== null) out.push({ node: n, source, exit });
3470
+ });
3471
+ return out;
3472
+ }
3473
+
3474
+ // packages/parser/src/storage.ts
3475
+ import ts8 from "typescript";
3097
3476
  var STORAGE_OPS = {
3098
3477
  download: "select",
3099
3478
  list: "select",
@@ -3109,14 +3488,14 @@ var STORAGE_OPS = {
3109
3488
  var TWO_PATHS = /* @__PURE__ */ new Set(["move", "copy"]);
3110
3489
  function isStorageRoot(e) {
3111
3490
  const u = unwrap(e);
3112
- return ts7.isPropertyAccessExpression(u) && u.name.text === "storage";
3491
+ return ts8.isPropertyAccessExpression(u) && u.name.text === "storage";
3113
3492
  }
3114
3493
  function storageBindingsIn(body) {
3115
3494
  const out = /* @__PURE__ */ new Map();
3116
- for (const decl of collect(body, ts7.isVariableDeclaration)) {
3117
- if (!decl.initializer || !ts7.isIdentifier(decl.name)) continue;
3495
+ for (const decl of collect(body, ts8.isVariableDeclaration)) {
3496
+ if (!decl.initializer || !ts8.isIdentifier(decl.name)) continue;
3118
3497
  const init = unwrap(decl.initializer);
3119
- if (!ts7.isCallExpression(init) || !ts7.isPropertyAccessExpression(init.expression)) continue;
3498
+ if (!ts8.isCallExpression(init) || !ts8.isPropertyAccessExpression(init.expression)) continue;
3120
3499
  const callee = init.expression;
3121
3500
  if (callee.name.text !== "from" || !isStorageRoot(callee.expression)) continue;
3122
3501
  const root = unwrap(callee.expression);
@@ -3132,7 +3511,7 @@ function storageCallOf(chain, bound) {
3132
3511
  const r = unwrap(root);
3133
3512
  return { clientRoot: r.expression, bucketArg: first.args[0], op: known(second) };
3134
3513
  }
3135
- if (ts7.isIdentifier(root) && first) {
3514
+ if (ts8.isIdentifier(root) && first) {
3136
3515
  const b = bound.get(root.text);
3137
3516
  if (b) return { clientRoot: b.clientRoot, bucketArg: b.bucketArg, op: known(first) };
3138
3517
  }
@@ -3143,9 +3522,9 @@ function bucketName(arg, sf) {
3143
3522
  const u = unwrap(arg);
3144
3523
  const lit = stringLiteralValue(u);
3145
3524
  if (lit !== null) return lit;
3146
- if (!ts7.isIdentifier(u)) return null;
3147
- for (const d of collect(sf, ts7.isVariableDeclaration)) {
3148
- if (!ts7.isIdentifier(d.name) || d.name.text !== u.text || !d.initializer) continue;
3525
+ if (!ts8.isIdentifier(u)) return null;
3526
+ for (const d of collect(sf, ts8.isVariableDeclaration)) {
3527
+ if (!ts8.isIdentifier(d.name) || d.name.text !== u.text || !d.initializer) continue;
3149
3528
  const v = stringLiteralValue(unwrap(d.initializer));
3150
3529
  if (v !== null) return v;
3151
3530
  }
@@ -3158,20 +3537,20 @@ var INPUT_OBJECT = /^(params|searchParams|body|query|headers|cookies|formData)$/
3158
3537
  var CallerScope = class {
3159
3538
  constructor(body, inputNames) {
3160
3539
  this.inputNames = inputNames;
3161
- for (const decl of collect(body, ts7.isVariableDeclaration)) {
3162
- if (!decl.initializer || !ts7.isIdentifier(decl.name)) continue;
3540
+ for (const decl of collect(body, ts8.isVariableDeclaration)) {
3541
+ if (!decl.initializer || !ts8.isIdentifier(decl.name)) continue;
3163
3542
  if (this.refersToCaller(decl.initializer)) this.scopedVars.add(decl.name.text);
3164
3543
  }
3165
- for (const call of collect(body, ts7.isCallExpression)) {
3544
+ for (const call of collect(body, ts8.isCallExpression)) {
3166
3545
  const callee = call.expression;
3167
- if (!ts7.isPropertyAccessExpression(callee) || callee.name.text !== "startsWith") continue;
3546
+ if (!ts8.isPropertyAccessExpression(callee) || callee.name.text !== "startsWith") continue;
3168
3547
  const target = unwrap(callee.expression);
3169
3548
  const arg = call.arguments[0];
3170
- if (ts7.isIdentifier(target) && arg && this.refersToCaller(arg)) this.guarded.add(target.text);
3549
+ if (ts8.isIdentifier(target) && arg && this.refersToCaller(arg)) this.guarded.add(target.text);
3171
3550
  }
3172
- for (const bin of collect(body, ts7.isBinaryExpression)) {
3551
+ for (const bin of collect(body, ts8.isBinaryExpression)) {
3173
3552
  const k = bin.operatorToken.kind;
3174
- if (k !== ts7.SyntaxKind.EqualsEqualsEqualsToken && k !== ts7.SyntaxKind.ExclamationEqualsEqualsToken && k !== ts7.SyntaxKind.EqualsEqualsToken && k !== ts7.SyntaxKind.ExclamationEqualsToken) {
3553
+ if (k !== ts8.SyntaxKind.EqualsEqualsEqualsToken && k !== ts8.SyntaxKind.ExclamationEqualsEqualsToken && k !== ts8.SyntaxKind.EqualsEqualsToken && k !== ts8.SyntaxKind.ExclamationEqualsToken) {
3175
3554
  continue;
3176
3555
  }
3177
3556
  for (const [side, other] of [
@@ -3191,13 +3570,13 @@ var CallerScope = class {
3191
3570
  let hit = false;
3192
3571
  walk(e, (n) => {
3193
3572
  if (hit) return false;
3194
- if (ts7.isPropertyAccessExpression(n) && this.isIdentityAccess(n)) {
3573
+ if (ts8.isPropertyAccessExpression(n) && this.isIdentityAccess(n)) {
3195
3574
  hit = true;
3196
3575
  return false;
3197
3576
  }
3198
- if (ts7.isIdentifier(n)) {
3577
+ if (ts8.isIdentifier(n)) {
3199
3578
  const parent = n.parent;
3200
- if (parent && ts7.isPropertyAccessExpression(parent) && parent.name === n) return void 0;
3579
+ if (parent && ts8.isPropertyAccessExpression(parent) && parent.name === n) return void 0;
3201
3580
  if (this.scopedVars.has(n.text)) hit = true;
3202
3581
  else if (IDENTITY_VAR.test(n.text) && !this.inputNames.has(n.text)) hit = true;
3203
3582
  }
@@ -3208,17 +3587,17 @@ var CallerScope = class {
3208
3587
  /** A path argument is covered when it embeds the caller's id or is an identifier checked against it. */
3209
3588
  covers(e) {
3210
3589
  const u = unwrap(e);
3211
- return this.refersToCaller(u) || ts7.isIdentifier(u) && this.guarded.has(u.text);
3590
+ return this.refersToCaller(u) || ts8.isIdentifier(u) && this.guarded.has(u.text);
3212
3591
  }
3213
3592
  isIdentityAccess(pa) {
3214
3593
  const field = pa.name.text;
3215
3594
  const names = [];
3216
3595
  let base = unwrap(pa.expression);
3217
- while (ts7.isPropertyAccessExpression(base)) {
3596
+ while (ts8.isPropertyAccessExpression(base)) {
3218
3597
  names.push(base.name.text);
3219
3598
  base = unwrap(base.expression);
3220
3599
  }
3221
- if (!ts7.isIdentifier(base) || this.inputNames.has(base.text)) return false;
3600
+ if (!ts8.isIdentifier(base) || this.inputNames.has(base.text)) return false;
3222
3601
  names.push(base.text);
3223
3602
  if (names.some((n) => INPUT_OBJECT.test(n))) return false;
3224
3603
  if (OWNER_FIELD.test(field)) return true;
@@ -3227,20 +3606,20 @@ var CallerScope = class {
3227
3606
  };
3228
3607
  function splitHead(e) {
3229
3608
  const u = unwrap(e);
3230
- if (!ts7.isElementAccessExpression(u)) return null;
3609
+ if (!ts8.isElementAccessExpression(u)) return null;
3231
3610
  const idx = u.argumentExpression;
3232
- if (!ts7.isNumericLiteral(idx) || idx.text !== "0") return null;
3611
+ if (!ts8.isNumericLiteral(idx) || idx.text !== "0") return null;
3233
3612
  const call = unwrap(u.expression);
3234
- if (!ts7.isCallExpression(call) || !ts7.isPropertyAccessExpression(call.expression)) return null;
3613
+ if (!ts8.isCallExpression(call) || !ts8.isPropertyAccessExpression(call.expression)) return null;
3235
3614
  if (call.expression.name.text !== "split") return null;
3236
3615
  const target = unwrap(call.expression.expression);
3237
- return ts7.isIdentifier(target) ? target.text : null;
3616
+ return ts8.isIdentifier(target) ? target.text : null;
3238
3617
  }
3239
3618
  function storageAccessOf(op, bucket, sf, scope, derived) {
3240
3619
  const pathArgs = op.args.slice(0, TWO_PATHS.has(op.name) ? 2 : 1);
3241
3620
  const parts = pathArgs.flatMap((a) => {
3242
3621
  const u = unwrap(a);
3243
- return ts7.isArrayLiteralExpression(u) ? [...u.elements] : [a];
3622
+ return ts8.isArrayLiteralExpression(u) ? [...u.elements] : [a];
3244
3623
  });
3245
3624
  const tainted = parts.filter((p) => derived(p));
3246
3625
  const judged = tainted.length > 0 ? tainted : parts;
@@ -3254,22 +3633,22 @@ function storageAccessOf(op, bucket, sf, scope, derived) {
3254
3633
  }
3255
3634
 
3256
3635
  // packages/parser/src/supabase.ts
3257
- import ts8 from "typescript";
3636
+ import ts9 from "typescript";
3258
3637
  var CREATE_CLIENT_CALLEES = /^(createClient|createServerClient|createBrowserClient)$/;
3259
3638
  function isCreateClientCall(call, sf) {
3260
3639
  const callee = call.expression;
3261
- if (ts8.isIdentifier(callee)) return CREATE_CLIENT_CALLEES.test(callee.text);
3262
- if (ts8.isPropertyAccessExpression(callee)) return CREATE_CLIENT_CALLEES.test(callee.name.text);
3640
+ if (ts9.isIdentifier(callee)) return CREATE_CLIENT_CALLEES.test(callee.text);
3641
+ if (ts9.isPropertyAccessExpression(callee)) return CREATE_CLIENT_CALLEES.test(callee.name.text);
3263
3642
  return CREATE_CLIENT_CALLEES.test(callee.getText(sf));
3264
3643
  }
3265
3644
  function resolveArgText(expr, sf) {
3266
3645
  const u = unwrap(expr);
3267
- if (!ts8.isIdentifier(u)) return expr.getText(sf);
3646
+ if (!ts9.isIdentifier(u)) return expr.getText(sf);
3268
3647
  let scope = expr.parent;
3269
3648
  while (scope) {
3270
- if (ts8.isFunctionLike(scope) || ts8.isBlock(scope) || ts8.isSourceFile(scope)) {
3271
- const decl = collect(scope, ts8.isVariableDeclaration).find(
3272
- (d) => ts8.isIdentifier(d.name) && d.name.text === u.text && d.initializer
3649
+ if (ts9.isFunctionLike(scope) || ts9.isBlock(scope) || ts9.isSourceFile(scope)) {
3650
+ const decl = collect(scope, ts9.isVariableDeclaration).find(
3651
+ (d) => ts9.isIdentifier(d.name) && d.name.text === u.text && d.initializer
3273
3652
  );
3274
3653
  if (decl?.initializer) return `${u.text} = ${decl.initializer.getText(sf)}`;
3275
3654
  }
@@ -3311,7 +3690,7 @@ function classifyCreateClientCall(call, sf) {
3311
3690
  }
3312
3691
  var AUTH_CALL = /\.auth\.(getUser|getSession|getClaims)\s*\(/;
3313
3692
  function hasModifier(node, kind) {
3314
- const mods = ts8.canHaveModifiers(node) ? ts8.getModifiers(node) : void 0;
3693
+ const mods = ts9.canHaveModifiers(node) ? ts9.getModifiers(node) : void 0;
3315
3694
  return mods?.some((m) => m.kind === kind) ?? false;
3316
3695
  }
3317
3696
  function analyzeModule(rel, sf) {
@@ -3320,33 +3699,33 @@ function analyzeModule(rel, sf) {
3320
3699
  const exportedNames = /* @__PURE__ */ new Set();
3321
3700
  let defaultExport = null;
3322
3701
  for (const stmt of sf.statements) {
3323
- if (ts8.isImportDeclaration(stmt) && ts8.isStringLiteral(stmt.moduleSpecifier)) {
3702
+ if (ts9.isImportDeclaration(stmt) && ts9.isStringLiteral(stmt.moduleSpecifier)) {
3324
3703
  const spec = stmt.moduleSpecifier.text;
3325
3704
  const clause = stmt.importClause;
3326
3705
  if (!clause) continue;
3327
3706
  if (clause.name) imports.set(clause.name.text, { spec, imported: "default" });
3328
3707
  const nb = clause.namedBindings;
3329
- if (nb && ts8.isNamedImports(nb)) {
3708
+ if (nb && ts9.isNamedImports(nb)) {
3330
3709
  for (const el of nb.elements) {
3331
3710
  imports.set(el.name.text, { spec, imported: (el.propertyName ?? el.name).text });
3332
3711
  }
3333
3712
  }
3334
- if (nb && ts8.isNamespaceImport(nb)) imports.set(nb.name.text, { spec, imported: "*" });
3335
- } else if (ts8.isExportDeclaration(stmt)) {
3336
- const spec = stmt.moduleSpecifier && ts8.isStringLiteral(stmt.moduleSpecifier) ? stmt.moduleSpecifier.text : null;
3713
+ if (nb && ts9.isNamespaceImport(nb)) imports.set(nb.name.text, { spec, imported: "*" });
3714
+ } else if (ts9.isExportDeclaration(stmt)) {
3715
+ const spec = stmt.moduleSpecifier && ts9.isStringLiteral(stmt.moduleSpecifier) ? stmt.moduleSpecifier.text : null;
3337
3716
  const clause = stmt.exportClause;
3338
3717
  if (spec && !clause) reexports.push({ star: true, spec });
3339
- else if (clause && ts8.isNamedExports(clause)) {
3718
+ else if (clause && ts9.isNamedExports(clause)) {
3340
3719
  for (const el of clause.elements) {
3341
3720
  const name = (el.propertyName ?? el.name).text;
3342
3721
  if (spec) reexports.push({ star: false, name, alias: el.name.text, spec });
3343
3722
  else exportedNames.add(name);
3344
3723
  }
3345
3724
  }
3346
- } else if (ts8.isExportAssignment(stmt) && !stmt.isExportEquals) {
3725
+ } else if (ts9.isExportAssignment(stmt) && !stmt.isExportEquals) {
3347
3726
  const e = unwrap(stmt.expression);
3348
- if (ts8.isIdentifier(e)) defaultExport = e.text;
3349
- } else if (ts8.isFunctionDeclaration(stmt) && stmt.name && hasModifier(stmt, ts8.SyntaxKind.DefaultKeyword)) {
3727
+ if (ts9.isIdentifier(e)) defaultExport = e.text;
3728
+ } else if (ts9.isFunctionDeclaration(stmt) && stmt.name && hasModifier(stmt, ts9.SyntaxKind.DefaultKeyword)) {
3350
3729
  defaultExport = stmt.name.text;
3351
3730
  }
3352
3731
  }
@@ -3361,15 +3740,15 @@ function analyzeModule(rel, sf) {
3361
3740
  const moduleVars = /* @__PURE__ */ new Map();
3362
3741
  const drizzleTables = /* @__PURE__ */ new Map();
3363
3742
  for (const stmt of sf.statements) {
3364
- if (!ts8.isVariableStatement(stmt)) continue;
3365
- const exported = hasModifier(stmt, ts8.SyntaxKind.ExportKeyword);
3743
+ if (!ts9.isVariableStatement(stmt)) continue;
3744
+ const exported = hasModifier(stmt, ts9.SyntaxKind.ExportKeyword);
3366
3745
  for (const d of stmt.declarationList.declarations) {
3367
- if (!ts8.isIdentifier(d.name) || !d.initializer || functions.has(d.name.text)) continue;
3746
+ if (!ts9.isIdentifier(d.name) || !d.initializer || functions.has(d.name.text)) continue;
3368
3747
  const init = clientCreatingOperand(d.initializer);
3369
- if (!ts8.isCallExpression(init) && !ts8.isNewExpression(init)) continue;
3370
- if (ts8.isCallExpression(init)) {
3748
+ if (!ts9.isCallExpression(init) && !ts9.isNewExpression(init)) continue;
3749
+ if (ts9.isCallExpression(init)) {
3371
3750
  const callee = init.expression;
3372
- const fn = ts8.isIdentifier(callee) ? callee.text : ts8.isPropertyAccessExpression(callee) ? callee.name.text : "";
3751
+ const fn = ts9.isIdentifier(callee) ? callee.text : ts9.isPropertyAccessExpression(callee) ? callee.name.text : "";
3373
3752
  const tableName = stringLiteralValue(init.arguments[0]);
3374
3753
  if ((DRIZZLE_TABLE_FNS.has(fn) || fn === "table") && tableName !== null) {
3375
3754
  drizzleTables.set(d.name.text, tableName);
@@ -3392,8 +3771,8 @@ function analyzeModule(rel, sf) {
3392
3771
  );
3393
3772
  if (nextAuthLocal.size > 0) {
3394
3773
  for (const stmt of sf.statements) {
3395
- if (!ts8.isVariableStatement(stmt)) continue;
3396
- const exported = hasModifier(stmt, ts8.SyntaxKind.ExportKeyword);
3774
+ if (!ts9.isVariableStatement(stmt)) continue;
3775
+ const exported = hasModifier(stmt, ts9.SyntaxKind.ExportKeyword);
3397
3776
  for (const name of nextAuthSessionNames(stmt, nextAuthLocal)) {
3398
3777
  const helper = {
3399
3778
  name,
@@ -3416,7 +3795,7 @@ function analyzeModule(rel, sf) {
3416
3795
  });
3417
3796
  continue;
3418
3797
  }
3419
- const creates = collect(f.fn, ts8.isCallExpression).filter((c) => isCreateClientCall(c, sf));
3798
+ const creates = collect(f.fn, ts9.isCallExpression).filter((c) => isCreateClientCall(c, sf));
3420
3799
  const first = creates[0];
3421
3800
  if (first) {
3422
3801
  const { kind, evidence } = classifyCreateClientCall(first, sf);
@@ -3439,7 +3818,7 @@ function analyzeModule(rel, sf) {
3439
3818
  }
3440
3819
 
3441
3820
  // packages/parser/src/whole-input.ts
3442
- import ts9 from "typescript";
3821
+ import ts10 from "typescript";
3443
3822
  var ELEMENT_PRESERVING = /* @__PURE__ */ new Set([
3444
3823
  "filter",
3445
3824
  "slice",
@@ -3455,35 +3834,35 @@ var ELEMENT_PRESERVING = /* @__PURE__ */ new Set([
3455
3834
  ]);
3456
3835
  var RESHAPING = /* @__PURE__ */ new Set(["map", "flatMap"]);
3457
3836
  var LOGICAL = /* @__PURE__ */ new Set([
3458
- ts9.SyntaxKind.QuestionQuestionToken,
3459
- ts9.SyntaxKind.BarBarToken,
3460
- ts9.SyntaxKind.AmpersandAmpersandToken
3837
+ ts10.SyntaxKind.QuestionQuestionToken,
3838
+ ts10.SyntaxKind.BarBarToken,
3839
+ ts10.SyntaxKind.AmpersandAmpersandToken
3461
3840
  ]);
3462
3841
  function isWholeInput(e, cx) {
3463
3842
  const u = unwrap(e);
3464
- if (ts9.isIdentifier(u)) return cx.wholeName(u.text);
3465
- if (ts9.isPropertyAccessExpression(u) || ts9.isElementAccessExpression(u)) {
3843
+ if (ts10.isIdentifier(u)) return cx.wholeName(u.text);
3844
+ if (ts10.isPropertyAccessExpression(u) || ts10.isElementAccessExpression(u)) {
3466
3845
  return isWholeInput(u.expression, cx);
3467
3846
  }
3468
- if (ts9.isObjectLiteralExpression(u)) {
3469
- return u.properties.some((pr) => ts9.isSpreadAssignment(pr) && isWholeInput(pr.expression, cx));
3847
+ if (ts10.isObjectLiteralExpression(u)) {
3848
+ return u.properties.some((pr) => ts10.isSpreadAssignment(pr) && isWholeInput(pr.expression, cx));
3470
3849
  }
3471
- if (ts9.isArrayLiteralExpression(u)) {
3472
- return u.elements.some((el) => isWholeInput(ts9.isSpreadElement(el) ? el.expression : el, cx));
3850
+ if (ts10.isArrayLiteralExpression(u)) {
3851
+ return u.elements.some((el) => isWholeInput(ts10.isSpreadElement(el) ? el.expression : el, cx));
3473
3852
  }
3474
- if (ts9.isConditionalExpression(u)) {
3853
+ if (ts10.isConditionalExpression(u)) {
3475
3854
  return isWholeInput(u.whenTrue, cx) || isWholeInput(u.whenFalse, cx);
3476
3855
  }
3477
- if (ts9.isBinaryExpression(u) && LOGICAL.has(u.operatorToken.kind)) {
3856
+ if (ts10.isBinaryExpression(u) && LOGICAL.has(u.operatorToken.kind)) {
3478
3857
  return isWholeInput(u.left, cx) || isWholeInput(u.right, cx);
3479
3858
  }
3480
- if (ts9.isCallExpression(u)) return isWholeCall(u, cx);
3859
+ if (ts10.isCallExpression(u)) return isWholeCall(u, cx);
3481
3860
  return false;
3482
3861
  }
3483
3862
  function isWholeCall(call, cx) {
3484
3863
  if (cx.requestBody(call)) return true;
3485
3864
  const callee = call.expression;
3486
- if (ts9.isPropertyAccessExpression(callee)) {
3865
+ if (ts10.isPropertyAccessExpression(callee)) {
3487
3866
  const method = callee.name.text;
3488
3867
  const receiverWhole = isWholeInput(callee.expression, cx);
3489
3868
  if (RESHAPING.has(method)) return callbackKeepsWhole(call.arguments[0], receiverWhole, cx);
@@ -3491,13 +3870,13 @@ function isWholeCall(call, cx) {
3491
3870
  }
3492
3871
  return call.arguments.some((a) => {
3493
3872
  const ua = unwrap(a);
3494
- return isWholeInput(a, cx) || ts9.isIdentifier(ua) && cx.requestName(ua.text);
3873
+ return isWholeInput(a, cx) || ts10.isIdentifier(ua) && cx.requestName(ua.text);
3495
3874
  });
3496
3875
  }
3497
3876
  function callbackKeepsWhole(cb, receiverWhole, cx) {
3498
3877
  if (!cb) return false;
3499
3878
  const f = unwrap(cb);
3500
- if (!ts9.isArrowFunction(f) && !ts9.isFunctionExpression(f)) return receiverWhole;
3879
+ if (!ts10.isArrowFunction(f) && !ts10.isFunctionExpression(f)) return receiverWhole;
3501
3880
  const element = f.parameters[0];
3502
3881
  const elementNames = new Set(receiverWhole && element ? boundNames(element.name) : []);
3503
3882
  const inner = {
@@ -3508,19 +3887,19 @@ function callbackKeepsWhole(cb, receiverWhole, cx) {
3508
3887
  return ownReturns(f).some((r) => isWholeInput(r, inner));
3509
3888
  }
3510
3889
  function propertyKey(name) {
3511
- if (ts9.isIdentifier(name) || ts9.isStringLiteral(name)) return name.text;
3890
+ if (ts10.isIdentifier(name) || ts10.isStringLiteral(name)) return name.text;
3512
3891
  return null;
3513
3892
  }
3514
3893
  function wholeParamNames(param, arg, cx) {
3515
3894
  if (!arg) return [];
3516
3895
  const u = unwrap(arg);
3517
- if (ts9.isObjectBindingPattern(param) && ts9.isObjectLiteralExpression(u) && !u.properties.some(ts9.isSpreadAssignment)) {
3896
+ if (ts10.isObjectBindingPattern(param) && ts10.isObjectLiteralExpression(u) && !u.properties.some(ts10.isSpreadAssignment)) {
3518
3897
  const out = [];
3519
3898
  for (const el of param.elements) {
3520
3899
  if (el.dotDotDotToken) continue;
3521
3900
  const key = propertyKey(el.propertyName ?? el.name);
3522
3901
  const prop = u.properties.find((pr) => pr.name && propertyKey(pr.name) === key);
3523
- const value = prop && ts9.isPropertyAssignment(prop) ? prop.initializer : prop && ts9.isShorthandPropertyAssignment(prop) ? prop.name : void 0;
3902
+ const value = prop && ts10.isPropertyAssignment(prop) ? prop.initializer : prop && ts10.isShorthandPropertyAssignment(prop) ? prop.name : void 0;
3524
3903
  if (value && isWholeInput(value, cx)) out.push(...boundNames(el.name));
3525
3904
  }
3526
3905
  return out;
@@ -3654,8 +4033,8 @@ function scopeOf(p, facts) {
3654
4033
  return scope;
3655
4034
  }
3656
4035
  function symOfCallee(p, callee, scope) {
3657
- if (ts10.isIdentifier(callee)) return scope.get(callee.text);
3658
- if (ts10.isPropertyAccessExpression(callee) && ts10.isIdentifier(callee.expression)) {
4036
+ if (ts11.isIdentifier(callee)) return scope.get(callee.text);
4037
+ if (ts11.isPropertyAccessExpression(callee) && ts11.isIdentifier(callee.expression)) {
3659
4038
  const ns = scope.get(callee.expression.text);
3660
4039
  if (ns?.kind === "namespace") return exportedSym(p, ns.facts, callee.name.text, 0) ?? void 0;
3661
4040
  }
@@ -3663,8 +4042,8 @@ function symOfCallee(p, callee, scope) {
3663
4042
  }
3664
4043
  function returnedExpressions(fn) {
3665
4044
  if (!fn.body) return [];
3666
- if (!ts10.isBlock(fn.body)) return [fn.body];
3667
- return collect(fn.body, ts10.isReturnStatement).map((r) => r.expression).filter((e) => e !== void 0);
4045
+ if (!ts11.isBlock(fn.body)) return [fn.body];
4046
+ return collect(fn.body, ts11.isReturnStatement).map((r) => r.expression).filter((e) => e !== void 0);
3668
4047
  }
3669
4048
  function factoryOfFunction(p, sym, depth) {
3670
4049
  const key = `${sym.facts.file}#${sym.name}`;
@@ -3676,7 +4055,7 @@ function factoryOfFunction(p, sym, depth) {
3676
4055
  let found = null;
3677
4056
  for (const ret of returnedExpressions(sym.fn)) {
3678
4057
  const u = unwrap(ret);
3679
- if (!ts10.isCallExpression(u)) continue;
4058
+ if (!ts11.isCallExpression(u)) continue;
3680
4059
  found = classifyCall(p, u, sf, scope, null, depth + 1);
3681
4060
  if (found) break;
3682
4061
  }
@@ -3713,7 +4092,7 @@ function classifyCall(p, call, sf, scope, frame, depth) {
3713
4092
  }
3714
4093
  function prismaExtensionBase(p, call, sf, scope, frame, depth) {
3715
4094
  const callee = call.expression;
3716
- if (!ts10.isPropertyAccessExpression(callee) || callee.name.text !== "$extends") return null;
4095
+ if (!ts11.isPropertyAccessExpression(callee) || callee.name.text !== "$extends") return null;
3717
4096
  if (depth > 5) return null;
3718
4097
  const base = unwrap(callee.expression);
3719
4098
  if (isPrismaNew(base)) {
@@ -3723,8 +4102,8 @@ function prismaExtensionBase(p, call, sf, scope, frame, depth) {
3723
4102
  location: { file: sf.fileName, line: lineOf(sf, call) }
3724
4103
  };
3725
4104
  }
3726
- if (ts10.isCallExpression(base)) return classifyCall(p, base, sf, scope, frame, depth + 1);
3727
- if (!ts10.isIdentifier(base)) return null;
4105
+ if (ts11.isCallExpression(base)) return classifyCall(p, base, sf, scope, frame, depth + 1);
4106
+ if (!ts11.isIdentifier(base)) return null;
3728
4107
  const bound = frame?.clients.get(base.text);
3729
4108
  if (bound) return bound;
3730
4109
  const sym = scope.get(base.text);
@@ -3734,15 +4113,15 @@ function instanceOfCall(p, call, frame, scope) {
3734
4113
  const sym = symOfCallee(p, call.expression, scope);
3735
4114
  if (sym?.kind !== "function") return null;
3736
4115
  const fscope = scopeOf(p, sym.facts);
3737
- const params = sym.fn.parameters.map((pp) => ts10.isIdentifier(pp.name) ? pp.name.text : null);
4116
+ const params = sym.fn.parameters.map((pp) => ts11.isIdentifier(pp.name) ? pp.name.text : null);
3738
4117
  for (const ret of returnedExpressions(sym.fn)) {
3739
4118
  const u = unwrap(ret);
3740
- if (!ts10.isNewExpression(u) || !ts10.isIdentifier(u.expression)) continue;
4119
+ if (!ts11.isNewExpression(u) || !ts11.isIdentifier(u.expression)) continue;
3741
4120
  const cs = fscope.get(u.expression.text);
3742
4121
  if (cs?.kind !== "class") continue;
3743
4122
  const ctorArgs = (u.arguments ?? []).map((a) => {
3744
4123
  const ua = unwrap(a);
3745
- if (ts10.isIdentifier(ua)) {
4124
+ if (ts11.isIdentifier(ua)) {
3746
4125
  const j = params.indexOf(ua.text);
3747
4126
  if (j >= 0 && frame) return argBinding(p, call.arguments[j], frame);
3748
4127
  }
@@ -3774,7 +4153,7 @@ function varBinding(p, sym) {
3774
4153
  whole: false,
3775
4154
  isRequest: false
3776
4155
  };
3777
- } else if (ts10.isCallExpression(init)) {
4156
+ } else if (ts11.isCallExpression(init)) {
3778
4157
  const found = classifyCall(p, init, sf, scope, null, 0);
3779
4158
  const client = found ? {
3780
4159
  kind: found.kind,
@@ -3783,7 +4162,7 @@ function varBinding(p, sym) {
3783
4162
  } : null;
3784
4163
  const instance = client ? null : instanceOfCall(p, init, null, scope);
3785
4164
  out = { client, instance, tainted: false, whole: false, isRequest: false };
3786
- } else if (ts10.isNewExpression(init) && ts10.isIdentifier(init.expression)) {
4165
+ } else if (ts11.isNewExpression(init) && ts11.isIdentifier(init.expression)) {
3787
4166
  const cs = scope.get(init.expression.text);
3788
4167
  if (cs?.kind === "class") {
3789
4168
  out = {
@@ -3809,15 +4188,15 @@ function usesInput(frame, e) {
3809
4188
  let hit = false;
3810
4189
  walk(e, (n) => {
3811
4190
  if (hit) return false;
3812
- if (ts10.isCallExpression(n) && handsOverRequest(frame, n)) {
4191
+ if (ts11.isCallExpression(n) && handsOverRequest(frame, n)) {
3813
4192
  hit = true;
3814
4193
  return false;
3815
4194
  }
3816
- if (!ts10.isIdentifier(n)) return void 0;
4195
+ if (!ts11.isIdentifier(n)) return void 0;
3817
4196
  const parent = n.parent;
3818
- if (parent && ts10.isPropertyAccessExpression(parent) && parent.name === n) return void 0;
3819
- if (parent && ts10.isPropertyAssignment(parent) && parent.name === n) return void 0;
3820
- const member = parent && ts10.isPropertyAccessExpression(parent) && parent.expression === n;
4197
+ if (parent && ts11.isPropertyAccessExpression(parent) && parent.name === n) return void 0;
4198
+ if (parent && ts11.isPropertyAssignment(parent) && parent.name === n) return void 0;
4199
+ const member = parent && ts11.isPropertyAccessExpression(parent) && parent.expression === n;
3821
4200
  if (frame.inputNames.has(n.text)) hit = true;
3822
4201
  else if (member && frame.reqNames.has(n.text) && REQUEST_MEMBER.test(parent.name.text)) {
3823
4202
  hit = true;
@@ -3836,46 +4215,46 @@ function handsOverRequest(frame, call) {
3836
4215
  if (IDENTITY_CALLEE.test(callee) || REQUEST_CLIENT_CALLEE.test(callee)) return false;
3837
4216
  return call.arguments.some((a) => {
3838
4217
  const u = unwrap(a);
3839
- return ts10.isIdentifier(u) && frame.reqNames.has(u.text);
4218
+ return ts11.isIdentifier(u) && frame.reqNames.has(u.text);
3840
4219
  });
3841
4220
  }
3842
4221
  function propertyKeyOf(name) {
3843
- if (name && (ts10.isIdentifier(name) || ts10.isStringLiteral(name))) return name.text;
4222
+ if (name && (ts11.isIdentifier(name) || ts11.isStringLiteral(name))) return name.text;
3844
4223
  return null;
3845
4224
  }
3846
4225
  function taintedProperties(frame, lit) {
3847
4226
  const out = /* @__PURE__ */ new Set();
3848
4227
  for (const pr of lit.properties) {
3849
- if (ts10.isSpreadAssignment(pr)) {
4228
+ if (ts11.isSpreadAssignment(pr)) {
3850
4229
  if (derivedIn(frame, pr.expression)) return null;
3851
4230
  continue;
3852
4231
  }
3853
4232
  const key = propertyKeyOf(pr.name);
3854
4233
  if (key === null) continue;
3855
- if (ts10.isPropertyAssignment(pr) && derivedIn(frame, pr.initializer)) out.add(key);
3856
- else if (ts10.isShorthandPropertyAssignment(pr) && derivedIn(frame, pr.name)) out.add(key);
4234
+ if (ts11.isPropertyAssignment(pr) && derivedIn(frame, pr.initializer)) out.add(key);
4235
+ else if (ts11.isShorthandPropertyAssignment(pr) && derivedIn(frame, pr.name)) out.add(key);
3857
4236
  }
3858
4237
  return out;
3859
4238
  }
3860
4239
  function bindParamTaint(child, param, arg, frame) {
3861
4240
  const u = arg ? unwrap(arg) : void 0;
3862
4241
  let props = null;
3863
- if (u && ts10.isObjectLiteralExpression(u)) props = taintedProperties(frame, u);
3864
- else if (u && ts10.isIdentifier(u) && !frame.inputNames.has(u.text)) {
4242
+ if (u && ts11.isObjectLiteralExpression(u)) props = taintedProperties(frame, u);
4243
+ else if (u && ts11.isIdentifier(u) && !frame.inputNames.has(u.text)) {
3865
4244
  props = frame.partialInputs.get(u.text) ?? null;
3866
4245
  }
3867
4246
  if (props === null) {
3868
4247
  for (const nm of boundNames(param)) child.inputNames.add(nm);
3869
4248
  return;
3870
4249
  }
3871
- if (ts10.isIdentifier(param)) {
4250
+ if (ts11.isIdentifier(param)) {
3872
4251
  child.partialInputs.set(param.text, new Set(props));
3873
4252
  return;
3874
4253
  }
3875
4254
  bindPatternFrom(child, param, props);
3876
4255
  }
3877
4256
  function bindPatternFrom(child, pattern, props) {
3878
- if (!ts10.isObjectBindingPattern(pattern)) {
4257
+ if (!ts11.isObjectBindingPattern(pattern)) {
3879
4258
  if (props.size > 0) for (const nm of boundNames(pattern)) child.inputNames.add(nm);
3880
4259
  return;
3881
4260
  }
@@ -3888,11 +4267,11 @@ function bindPatternFrom(child, pattern, props) {
3888
4267
  }
3889
4268
  function isRequestBodyCall(frame, call) {
3890
4269
  const callee = call.expression;
3891
- if (!ts10.isPropertyAccessExpression(callee) || !/^(json|formData|text)$/.test(callee.name.text)) {
4270
+ if (!ts11.isPropertyAccessExpression(callee) || !/^(json|formData|text)$/.test(callee.name.text)) {
3892
4271
  return false;
3893
4272
  }
3894
4273
  const recv = unwrap(callee.expression);
3895
- if (!ts10.isIdentifier(recv)) return false;
4274
+ if (!ts11.isIdentifier(recv)) return false;
3896
4275
  if (frame.reqNames.has(recv.text)) return true;
3897
4276
  return frame.depth === 0 && frame.reqNames.size === 0 && REQUEST_NAME.test(recv.text);
3898
4277
  }
@@ -3905,7 +4284,7 @@ function wholeContext(frame) {
3905
4284
  }
3906
4285
  function receiverTainted(frame, call) {
3907
4286
  const callee = call.expression;
3908
- if (!ts10.isPropertyAccessExpression(callee) && !ts10.isElementAccessExpression(callee)) return false;
4287
+ if (!ts11.isPropertyAccessExpression(callee) && !ts11.isElementAccessExpression(callee)) return false;
3909
4288
  return derivedIn(frame, callee);
3910
4289
  }
3911
4290
  function argBinding(p, arg, frame) {
@@ -3915,7 +4294,7 @@ function argBinding(p, arg, frame) {
3915
4294
  let client = null;
3916
4295
  let instance = null;
3917
4296
  let isRequest = false;
3918
- if (ts10.isIdentifier(u)) {
4297
+ if (ts11.isIdentifier(u)) {
3919
4298
  client = frame.clients.get(u.text) ?? null;
3920
4299
  instance = frame.instances.get(u.text) ?? null;
3921
4300
  isRequest = frame.reqNames.has(u.text);
@@ -3927,14 +4306,14 @@ function argBinding(p, arg, frame) {
3927
4306
  instance = vb.instance;
3928
4307
  }
3929
4308
  }
3930
- } else if (ts10.isPropertyAccessExpression(u) && u.expression.kind === ts10.SyntaxKind.ThisKeyword) {
4309
+ } else if (ts11.isPropertyAccessExpression(u) && u.expression.kind === ts11.SyntaxKind.ThisKeyword) {
3931
4310
  const tp = frame.thisProps.get(u.name.text);
3932
4311
  if (tp) {
3933
4312
  client = tp.client;
3934
4313
  instance = tp.instance;
3935
4314
  isRequest = tp.isRequest;
3936
4315
  }
3937
- } else if (ts10.isCallExpression(u)) {
4316
+ } else if (ts11.isCallExpression(u)) {
3938
4317
  client = classifyCall(p, u, frame.sf, scope, frame, 0);
3939
4318
  if (!client) instance = instanceOfCall(p, u, frame, scope);
3940
4319
  } else if (isPrismaNew(u)) {
@@ -3943,7 +4322,7 @@ function argBinding(p, arg, frame) {
3943
4322
  name: "PrismaClient",
3944
4323
  location: { file: frame.rel, line: lineOf(frame.sf, u) }
3945
4324
  };
3946
- } else if (ts10.isNewExpression(u) && ts10.isIdentifier(u.expression)) {
4325
+ } else if (ts11.isNewExpression(u) && ts11.isIdentifier(u.expression)) {
3947
4326
  const cs = scope.get(u.expression.text);
3948
4327
  if (cs?.kind === "class") {
3949
4328
  instance = {
@@ -3971,7 +4350,7 @@ function methodTarget(inst, method) {
3971
4350
  }
3972
4351
  function callTarget(p, call, frame, scope) {
3973
4352
  const callee = call.expression;
3974
- if (ts10.isIdentifier(callee)) {
4353
+ if (ts11.isIdentifier(callee)) {
3975
4354
  const sym = scope.get(callee.text);
3976
4355
  if (sym?.kind === "function") {
3977
4356
  return { fn: sym.fn, facts: sym.facts, name: sym.name, cls: null, thisProps: /* @__PURE__ */ new Map() };
@@ -3987,10 +4366,10 @@ function callTarget(p, call, frame, scope) {
3987
4366
  }
3988
4367
  return null;
3989
4368
  }
3990
- if (!ts10.isPropertyAccessExpression(callee)) return null;
4369
+ if (!ts11.isPropertyAccessExpression(callee)) return null;
3991
4370
  const obj = callee.expression;
3992
4371
  const method = callee.name.text;
3993
- if (ts10.isIdentifier(obj)) {
4372
+ if (ts11.isIdentifier(obj)) {
3994
4373
  const inst = frame.instances.get(obj.text);
3995
4374
  if (inst) return methodTarget(inst, method);
3996
4375
  const sym = scope.get(obj.text);
@@ -4008,7 +4387,7 @@ function callTarget(p, call, frame, scope) {
4008
4387
  }
4009
4388
  return null;
4010
4389
  }
4011
- if (obj.kind === ts10.SyntaxKind.ThisKeyword && frame.cls) {
4390
+ if (obj.kind === ts11.SyntaxKind.ThisKeyword && frame.cls) {
4012
4391
  const m = frame.cls.methods.get(method);
4013
4392
  if (!m) return null;
4014
4393
  return {
@@ -4019,13 +4398,13 @@ function callTarget(p, call, frame, scope) {
4019
4398
  thisProps: frame.thisProps
4020
4399
  };
4021
4400
  }
4022
- if (ts10.isPropertyAccessExpression(obj) && obj.expression.kind === ts10.SyntaxKind.ThisKeyword) {
4401
+ if (ts11.isPropertyAccessExpression(obj) && obj.expression.kind === ts11.SyntaxKind.ThisKeyword) {
4023
4402
  const tp = frame.thisProps.get(obj.name.text);
4024
4403
  if (tp?.instance) return methodTarget(tp.instance, method);
4025
4404
  }
4026
4405
  return null;
4027
4406
  }
4028
- function analyzeFrame(p, frame, acc) {
4407
+ function bindDeclarations(p, frame, acc) {
4029
4408
  const { rel, sf, fn } = frame;
4030
4409
  const scope = scopeOf(p, frame.facts);
4031
4410
  const loc2 = (n) => ({ file: rel, line: lineOf(sf, n) });
@@ -4057,24 +4436,24 @@ function analyzeFrame(p, frame, acc) {
4057
4436
  for (const nm of names) addInput(kind, nm, decl, true);
4058
4437
  bindInput(names, true);
4059
4438
  };
4060
- for (const decl of collect(body, ts10.isVariableDeclaration)) {
4439
+ for (const decl of collect(body, ts11.isVariableDeclaration)) {
4061
4440
  if (!decl.initializer) continue;
4062
4441
  const init = clientCreatingOperand(decl.initializer);
4063
4442
  const names = boundNames(decl.name);
4064
4443
  const text = init.getText(sf);
4065
4444
  aliasLocal(frame, decl.name, init);
4066
- const client = ts10.isCallExpression(init) ? classifyCall(p, init, sf, scope, frame, 0) : null;
4067
- if (client && ts10.isIdentifier(decl.name)) {
4445
+ const client = ts11.isCallExpression(init) ? classifyCall(p, init, sf, scope, frame, 0) : null;
4446
+ if (client && ts11.isIdentifier(decl.name)) {
4068
4447
  frame.clients.set(decl.name.text, client);
4069
4448
  continue;
4070
4449
  }
4071
- const rowsOfQuery = ts10.isCallExpression(init) && (isQueryChain(init) || isDbChain(init, frame));
4450
+ const rowsOfQuery = ts11.isCallExpression(init) && (isQueryChain(init) || isDbChain(init, frame));
4072
4451
  if (frame.depth === 0 && !rowsOfQuery) {
4073
4452
  if (/^(params|context\.params|ctx\.params|props\.params)$/.test(text)) {
4074
4453
  handlerInput("route_param", names, decl);
4075
4454
  continue;
4076
4455
  }
4077
- if (ts10.isCallExpression(init) && isRequestCall(text)) {
4456
+ if (ts11.isCallExpression(init) && isRequestCall(text)) {
4078
4457
  handlerInput("body", names, decl);
4079
4458
  continue;
4080
4459
  }
@@ -4087,9 +4466,9 @@ function analyzeFrame(p, frame, acc) {
4087
4466
  continue;
4088
4467
  }
4089
4468
  }
4090
- if (ts10.isCallExpression(init)) {
4469
+ if (ts11.isCallExpression(init)) {
4091
4470
  const inst = instanceOfCall(p, init, frame, scope);
4092
- if (inst && ts10.isIdentifier(decl.name)) {
4471
+ if (inst && ts11.isIdentifier(decl.name)) {
4093
4472
  frame.instances.set(decl.name.text, inst);
4094
4473
  continue;
4095
4474
  }
@@ -4097,34 +4476,41 @@ function analyzeFrame(p, frame, acc) {
4097
4476
  if (returnsIdentity(p, init, scope)) continue;
4098
4477
  const args = init.arguments.map((a) => argBinding(p, a, frame));
4099
4478
  if (args.some((a) => a.tainted || a.isRequest) || receiverTainted(frame, init)) {
4100
- bindInput(names, isWholeInput(init, wholeContext(frame)));
4479
+ const rt = returnTaint(p, init, frame, scope, 0);
4480
+ if (rt === null || rt.tainted && rt.props === null) {
4481
+ bindInput(names, isWholeInput(init, wholeContext(frame)));
4482
+ } else if (rt.tainted && rt.props !== null) {
4483
+ if (ts11.isIdentifier(decl.name))
4484
+ frame.partialInputs.set(decl.name.text, new Set(rt.props));
4485
+ else bindPatternFrom(frame, decl.name, rt.props);
4486
+ }
4101
4487
  }
4102
- } else if (isPrismaNew(init) && ts10.isIdentifier(decl.name)) {
4488
+ } else if (isPrismaNew(init) && ts11.isIdentifier(decl.name)) {
4103
4489
  frame.clients.set(decl.name.text, {
4104
4490
  kind: "direct_db",
4105
4491
  name: decl.name.text,
4106
4492
  location: loc2(init)
4107
4493
  });
4108
- } else if (ts10.isNewExpression(init) && ts10.isIdentifier(init.expression) && scope.get(init.expression.text)?.kind === "class") {
4494
+ } else if (ts11.isNewExpression(init) && ts11.isIdentifier(init.expression) && scope.get(init.expression.text)?.kind === "class") {
4109
4495
  const cs = scope.get(init.expression.text);
4110
- if (cs?.kind === "class" && ts10.isIdentifier(decl.name)) {
4496
+ if (cs?.kind === "class" && ts11.isIdentifier(decl.name)) {
4111
4497
  frame.instances.set(decl.name.text, {
4112
4498
  cls: cs.cls,
4113
4499
  facts: cs.facts,
4114
4500
  ctorArgs: (init.arguments ?? []).map((a) => argBinding(p, a, frame))
4115
4501
  });
4116
4502
  }
4117
- } else if (ts10.isIdentifier(init)) {
4503
+ } else if (ts11.isIdentifier(init)) {
4118
4504
  const c = frame.clients.get(init.text);
4119
- if (c && ts10.isIdentifier(decl.name)) frame.clients.set(decl.name.text, c);
4505
+ if (c && ts11.isIdentifier(decl.name)) frame.clients.set(decl.name.text, c);
4120
4506
  const i = frame.instances.get(init.text);
4121
- if (i && ts10.isIdentifier(decl.name)) frame.instances.set(decl.name.text, i);
4507
+ if (i && ts11.isIdentifier(decl.name)) frame.instances.set(decl.name.text, i);
4122
4508
  if (frame.inputNames.has(init.text)) {
4123
4509
  if (frame.depth === 0) for (const nm of names) addInput("body", nm, decl, true);
4124
4510
  bindInput(names, frame.wholeNames.has(init.text));
4125
4511
  } else {
4126
4512
  const partial = frame.partialInputs.get(init.text);
4127
- if (partial && ts10.isIdentifier(decl.name)) {
4513
+ if (partial && ts11.isIdentifier(decl.name)) {
4128
4514
  frame.partialInputs.set(decl.name.text, new Set(partial));
4129
4515
  } else if (partial) bindPatternFrom(frame, decl.name, partial);
4130
4516
  }
@@ -4134,34 +4520,47 @@ function analyzeFrame(p, frame, acc) {
4134
4520
  }
4135
4521
  if (frame.depth === 0) {
4136
4522
  walk(body, (n) => {
4137
- if (ts10.isPropertyAccessExpression(n) && ts10.isIdentifier(n.expression) && n.expression.text === "params") {
4523
+ if (ts11.isPropertyAccessExpression(n) && ts11.isIdentifier(n.expression) && n.expression.text === "params") {
4138
4524
  addInput("route_param", n.name.text, n, false);
4139
4525
  }
4140
4526
  return void 0;
4141
4527
  });
4142
4528
  }
4143
- for (const call of collect(body, ts10.isCallExpression)) {
4529
+ }
4530
+ function analyzeFrame(p, frame, acc) {
4531
+ const { rel, sf, fn } = frame;
4532
+ const scope = scopeOf(p, frame.facts);
4533
+ const loc2 = (n) => ({ file: rel, line: lineOf(sf, n) });
4534
+ const body = fn.body ?? fn;
4535
+ bindDeclarations(p, frame, acc);
4536
+ for (const call of collect(body, ts11.isCallExpression)) {
4144
4537
  const callee = call.expression;
4145
- if (!ts10.isPropertyAccessExpression(callee) || !/^\$?transaction$/.test(callee.name.text))
4538
+ if (!ts11.isPropertyAccessExpression(callee) || !/^\$?transaction$/.test(callee.name.text))
4146
4539
  continue;
4147
- if (!ts10.isIdentifier(callee.expression)) continue;
4540
+ if (!ts11.isIdentifier(callee.expression)) continue;
4148
4541
  const outer = frame.clients.get(callee.expression.text);
4149
- const fn2 = call.arguments.map((a) => unwrap(a)).find((a) => ts10.isArrowFunction(a) || ts10.isFunctionExpression(a));
4150
- const param = fn2 && (ts10.isArrowFunction(fn2) || ts10.isFunctionExpression(fn2)) ? fn2.parameters[0] : void 0;
4151
- if (outer && param && ts10.isIdentifier(param.name)) frame.clients.set(param.name.text, outer);
4152
- }
4153
- for (const call of collect(body, ts10.isCallExpression)) {
4154
- const calleeText = call.expression.getText(sf);
4155
- if (/\.auth\.(getUser|getSession|getClaims)$/.test(calleeText) || symOfCallee(p, call.expression, scope)?.kind === "auth") {
4156
- acc.authChecks.push({ ...loc2(call), kind: "session" });
4157
- }
4542
+ const fn2 = call.arguments.map((a) => unwrap(a)).find((a) => ts11.isArrowFunction(a) || ts11.isFunctionExpression(a));
4543
+ const param = fn2 && (ts11.isArrowFunction(fn2) || ts11.isFunctionExpression(fn2)) ? fn2.parameters[0] : void 0;
4544
+ if (outer && param && ts11.isIdentifier(param.name)) frame.clients.set(param.name.text, outer);
4545
+ }
4546
+ const isSessionCall = (call) => /\.auth\.(getUser|getSession|getClaims)$/.test(call.expression.getText(sf)) || symOfCallee(p, call.expression, scope)?.kind === "auth";
4547
+ for (const call of collect(body, ts11.isCallExpression)) {
4548
+ if (isSessionCall(call)) acc.authChecks.push({ ...loc2(call), kind: "session" });
4549
+ }
4550
+ for (const gate of roleGatesIn(body, sessionNamesIn(body, isSessionCall))) {
4551
+ if (gate.exit === "return" && !frame.exitPropagates) continue;
4552
+ acc.roleChecks.push({
4553
+ ...loc2(gate.node),
4554
+ source: gate.source,
4555
+ text: gate.node.expression.getText(sf).replace(/\s+/g, " ").slice(0, 160)
4556
+ });
4158
4557
  }
4159
4558
  for (const check of secretChecksIn(fn, sf)) {
4160
4559
  acc.authChecks.push({ ...loc2(check.node), kind: "secret" });
4161
4560
  }
4162
- for (const pa of collect(body, ts10.isPropertyAccessExpression)) {
4561
+ for (const pa of collect(body, ts11.isPropertyAccessExpression)) {
4163
4562
  const inner = pa.expression;
4164
- if (ts10.isPropertyAccessExpression(inner) && (inner.name.text === "user_metadata" || inner.name.text === "app_metadata")) {
4563
+ if (ts11.isPropertyAccessExpression(inner) && (inner.name.text === "user_metadata" || inner.name.text === "app_metadata")) {
4165
4564
  acc.metadataAccesses.push({
4166
4565
  path: pa.getText(sf),
4167
4566
  bucket: inner.name.text,
@@ -4173,15 +4572,15 @@ function analyzeFrame(p, frame, acc) {
4173
4572
  const seen = /* @__PURE__ */ new Set();
4174
4573
  const clientOf = (root) => {
4175
4574
  const u = unwrap(root);
4176
- if (ts10.isIdentifier(u)) {
4575
+ if (ts11.isIdentifier(u)) {
4177
4576
  const b = frame.clients.get(u.text) ?? null;
4178
4577
  return { binding: b, name: b ? null : u.text };
4179
4578
  }
4180
- if (ts10.isCallExpression(u)) {
4579
+ if (ts11.isCallExpression(u)) {
4181
4580
  const b = classifyCall(p, u, sf, scope, frame, 0);
4182
4581
  return { binding: b, name: b ? null : u.expression.getText(sf) };
4183
4582
  }
4184
- if (ts10.isPropertyAccessExpression(u) && u.expression.kind === ts10.SyntaxKind.ThisKeyword) {
4583
+ if (ts11.isPropertyAccessExpression(u) && u.expression.kind === ts11.SyntaxKind.ThisKeyword) {
4185
4584
  const b = frame.thisProps.get(u.name.text)?.client ?? null;
4186
4585
  return { binding: b, name: b ? null : u.getText(sf) };
4187
4586
  }
@@ -4190,11 +4589,11 @@ function analyzeFrame(p, frame, acc) {
4190
4589
  const tableSym = (e) => {
4191
4590
  if (!e) return null;
4192
4591
  const u = unwrap(e);
4193
- if (ts10.isIdentifier(u)) {
4592
+ if (ts11.isIdentifier(u)) {
4194
4593
  const sym = scope.get(u.text);
4195
4594
  return sym?.kind === "table" ? sym.table : null;
4196
4595
  }
4197
- if (ts10.isPropertyAccessExpression(u) && ts10.isIdentifier(u.expression)) {
4596
+ if (ts11.isPropertyAccessExpression(u) && ts11.isIdentifier(u.expression)) {
4198
4597
  const ns = scope.get(u.expression.text);
4199
4598
  if (ns?.kind === "namespace") {
4200
4599
  const sym = exportedSym(p, ns.facts, u.name.text, 0);
@@ -4251,7 +4650,46 @@ function analyzeFrame(p, frame, acc) {
4251
4650
  if (frame.via.length > 0) query.via = frame.via;
4252
4651
  acc.queries.push(query);
4253
4652
  };
4254
- for (const call of collect(body, ts10.isCallExpression)) {
4653
+ const supabaseFilters = (segments) => {
4654
+ const filters = [];
4655
+ for (const s of segments) {
4656
+ if (!FILTER_METHODS.has(s.name)) continue;
4657
+ const firstArg = s.args[0];
4658
+ if (s.name === "match" && firstArg && ts11.isObjectLiteralExpression(firstArg)) {
4659
+ for (const pr of firstArg.properties) {
4660
+ if (ts11.isPropertyAssignment(pr)) {
4661
+ const f2 = {
4662
+ method: "match",
4663
+ column: pr.name.getText(sf).replace(/['"]/g, ""),
4664
+ valueText: pr.initializer.getText(sf),
4665
+ inputDerived: derivedIn(frame, pr.initializer)
4666
+ };
4667
+ filters.push(valued(f2, pr.initializer));
4668
+ }
4669
+ }
4670
+ continue;
4671
+ }
4672
+ const val = s.args[1];
4673
+ const f = {
4674
+ method: s.name,
4675
+ column: stringLiteralValue(firstArg),
4676
+ valueText: val ? val.getText(sf) : "",
4677
+ inputDerived: val ? derivedIn(frame, val) : false
4678
+ };
4679
+ filters.push(valued(f, val));
4680
+ }
4681
+ return filters;
4682
+ };
4683
+ const builders = /* @__PURE__ */ new Map();
4684
+ const bindBuilder = (tail, query) => {
4685
+ const { node, parent } = outerOf(tail);
4686
+ if (parent && ts11.isVariableDeclaration(parent) && ts11.isIdentifier(parent.name)) {
4687
+ builders.set(parent.name.text, query);
4688
+ } else if (parent && ts11.isBinaryExpression(parent) && parent.operatorToken.kind === ts11.SyntaxKind.EqualsToken && parent.right === node && ts11.isIdentifier(parent.left)) {
4689
+ builders.set(parent.left.text, query);
4690
+ }
4691
+ };
4692
+ for (const call of collect(body, ts11.isCallExpression)) {
4255
4693
  if (!isChainTail(call)) continue;
4256
4694
  const chain = flattenChain(call);
4257
4695
  const storageCall = storageCallOf(chain, storageHandles);
@@ -4260,6 +4698,18 @@ function analyzeFrame(p, frame, acc) {
4260
4698
  continue;
4261
4699
  }
4262
4700
  const root = unwrap(chain.root);
4701
+ const builder = ts11.isIdentifier(root) ? builders.get(root.text) : void 0;
4702
+ if (builder) {
4703
+ const condition = enclosingCondition(call, body);
4704
+ if (!condition || !derivedIn(frame, condition)) {
4705
+ const note = condition ? ` (when ${condition.getText(sf).replace(/\s+/g, " ")})` : "";
4706
+ for (const f of supabaseFilters(chain.segments)) {
4707
+ builder.filters.push(note ? { ...f, valueText: `${f.valueText}${note}` } : f);
4708
+ }
4709
+ }
4710
+ bindBuilder(call, builder);
4711
+ continue;
4712
+ }
4263
4713
  const first = chain.segments[0];
4264
4714
  const fromIdx = chain.segments.findIndex((s) => s.name === "from");
4265
4715
  const rpcIdx = chain.segments.findIndex((s) => s.name === "rpc");
@@ -4282,33 +4732,7 @@ function analyzeFrame(p, frame, acc) {
4282
4732
  break;
4283
4733
  }
4284
4734
  }
4285
- const filters = [];
4286
- for (const s of after) {
4287
- if (!FILTER_METHODS.has(s.name)) continue;
4288
- const firstArg = s.args[0];
4289
- if (s.name === "match" && firstArg && ts10.isObjectLiteralExpression(firstArg)) {
4290
- for (const pr of firstArg.properties) {
4291
- if (ts10.isPropertyAssignment(pr)) {
4292
- const f2 = {
4293
- method: "match",
4294
- column: pr.name.getText(sf).replace(/['"]/g, ""),
4295
- valueText: pr.initializer.getText(sf),
4296
- inputDerived: derivedIn(frame, pr.initializer)
4297
- };
4298
- filters.push(valued(f2, pr.initializer));
4299
- }
4300
- }
4301
- continue;
4302
- }
4303
- const val = s.args[1];
4304
- const f = {
4305
- method: s.name,
4306
- column: stringLiteralValue(firstArg),
4307
- valueText: val ? val.getText(sf) : "",
4308
- inputDerived: val ? derivedIn(frame, val) : false
4309
- };
4310
- filters.push(valued(f, val));
4311
- }
4735
+ const filters = supabaseFilters(after);
4312
4736
  parsed = {
4313
4737
  anchor: anchor.node,
4314
4738
  table: fromTable ?? stringLiteralValue(anchor.args[0]) ?? "(dynamic)",
@@ -4337,7 +4761,7 @@ function analyzeFrame(p, frame, acc) {
4337
4761
  payload: payloadOf(payloadSeg?.args[0]),
4338
4762
  clientRoot: chain.root
4339
4763
  };
4340
- } else if (first && DRIZZLE_QUERY_API.has(first.name) && ts10.isPropertyAccessExpression(root) && ts10.isPropertyAccessExpression(root.expression) && root.expression.name.text === "query") {
4764
+ } else if (first && DRIZZLE_QUERY_API.has(first.name) && ts11.isPropertyAccessExpression(root) && ts11.isPropertyAccessExpression(root.expression) && root.expression.name.text === "query") {
4341
4765
  const key = root.name.text;
4342
4766
  parsed = {
4343
4767
  anchor: first.node,
@@ -4349,7 +4773,7 @@ function analyzeFrame(p, frame, acc) {
4349
4773
  payload: null,
4350
4774
  clientRoot: root.expression.expression
4351
4775
  };
4352
- } else if (first && PRISMA_OPS[first.name] && ts10.isPropertyAccessExpression(root) && ts10.isIdentifier(root.expression) && clientOf(root.expression).binding?.kind === "direct_db") {
4776
+ } else if (first && PRISMA_OPS[first.name] && ts11.isPropertyAccessExpression(root) && ts11.isIdentifier(root.expression) && clientOf(root.expression).binding?.kind === "direct_db") {
4353
4777
  const accessor = root.name.text;
4354
4778
  const arg = first.args[0];
4355
4779
  parsed = {
@@ -4382,6 +4806,13 @@ function analyzeFrame(p, frame, acc) {
4382
4806
  call,
4383
4807
  chain.segments.map((s) => s.name)
4384
4808
  );
4809
+ const checks = rowComparisons(call).filter((c) => c.exit === "throw" || frame.exitPropagates).map((c) => ({
4810
+ method: "compare",
4811
+ column: c.column,
4812
+ valueText: c.value.getText(sf).replace(/\s+/g, " "),
4813
+ inputDerived: derivedIn(frame, c.value)
4814
+ }));
4815
+ if (checks.length > 0) query.ownerChecks = checks;
4385
4816
  acc.reads.push({
4386
4817
  query,
4387
4818
  keys: query.filters.map((f) => {
@@ -4389,76 +4820,151 @@ function analyzeFrame(p, frame, acc) {
4389
4820
  return v ? valueKey(frame, v) : null;
4390
4821
  }),
4391
4822
  order: [...frame.pathPos, parsed.anchor.getStart(sf)],
4392
- exits: rowExit === "throw" || rowExit === "return" && frame.exitPropagates
4823
+ exits: rowExit === "throw" || rowExit === "return" && frame.exitPropagates,
4824
+ checks
4393
4825
  });
4826
+ bindBuilder(call, query);
4394
4827
  }
4395
4828
  if (query.filters.some((f) => f.inputDerived && isCredentialColumn(f.column))) {
4396
4829
  acc.authChecks.push({ ...query.location, kind: "credential" });
4397
4830
  }
4398
4831
  }
4399
4832
  if (frame.depth >= MAX_DEPTH) return;
4400
- for (const call of collect(body, ts10.isCallExpression)) {
4833
+ for (const call of collect(body, ts11.isCallExpression)) {
4401
4834
  const target = callTarget(p, call, frame, scope);
4402
4835
  if (!target) continue;
4403
- const tsf = p.sources.get(target.facts.file);
4404
- if (!tsf) continue;
4405
- const child = {
4406
- rel: target.facts.file,
4407
- sf: tsf,
4408
- facts: target.facts,
4409
- fn: target.fn,
4410
- depth: frame.depth + 1,
4411
- via: [...frame.via, `${target.name} (${target.facts.file}:${lineOf(tsf, target.fn)})`],
4412
- inputNames: /* @__PURE__ */ new Set(),
4413
- wholeNames: /* @__PURE__ */ new Set(),
4414
- partialInputs: /* @__PURE__ */ new Map(),
4415
- reqNames: /* @__PURE__ */ new Set(),
4416
- clients: /* @__PURE__ */ new Map(),
4417
- instances: /* @__PURE__ */ new Map(),
4418
- cls: target.cls,
4419
- thisProps: target.thisProps,
4420
- key: `${target.facts.file}#${target.name}@${call.getStart(frame.sf)}`,
4421
- aliases: /* @__PURE__ */ new Map(),
4422
- pathPos: [...frame.pathPos, call.getStart(frame.sf)],
4423
- exitPropagates: frame.exitPropagates && callResultChecked(call)
4424
- };
4425
- const cx = wholeContext(frame);
4426
- target.fn.parameters.forEach((param, i) => {
4427
- const arg = call.arguments[i];
4428
- const ab = argBinding(p, arg, frame);
4429
- const names = boundNames(param.name);
4430
- const head = names[0];
4431
- if (ab.client && head !== void 0 && ts10.isIdentifier(param.name)) {
4432
- child.clients.set(head, ab.client);
4433
- }
4434
- if (ab.instance && head !== void 0 && ts10.isIdentifier(param.name)) {
4435
- child.instances.set(head, ab.instance);
4436
- }
4437
- for (const nm of names) if (ab.isRequest) child.reqNames.add(nm);
4438
- if (ab.tainted) {
4439
- bindParamTaint(child, param.name, arg, frame);
4440
- for (const nm of wholeParamNames(param.name, arg, cx)) {
4441
- child.inputNames.add(nm);
4442
- child.wholeNames.add(nm);
4443
- }
4444
- aliasParam(child, param.name, arg, frame);
4445
- }
4446
- });
4447
- const signature = [
4448
- ...[...child.clients].map(([n, c]) => `${n}=${c.kind}`),
4449
- ...[...child.inputNames].map((n) => `${n}!${child.wholeNames.has(n) ? "*" : ""}`),
4450
- ...[...child.partialInputs].map(([n, s]) => `${n}.{${[...s].sort().join("|")}}`),
4451
- ...[...child.reqNames].map((n) => `${n}?`),
4452
- ...[...child.thisProps].map(([n, b]) => `this.${n}=${b.client?.kind ?? "-"}`),
4453
- ...[...child.aliases].map(([n, k]) => `${n}~${k}`),
4454
- `exit=${child.exitPropagates}`
4455
- ].sort();
4456
- const key = `${target.facts.file}#${target.name}#${signature.join(",")}`;
4836
+ const child = childFrame(p, call, target, frame);
4837
+ if (!child) continue;
4838
+ const key = `${target.facts.file}#${target.name}#${frameSignature(child)}`;
4457
4839
  if (acc.visited.has(key)) continue;
4458
4840
  acc.visited.add(key);
4459
4841
  analyzeFrame(p, child, acc);
4460
4842
  }
4461
4843
  }
4844
+ function childFrame(p, call, target, frame) {
4845
+ const tsf = p.sources.get(target.facts.file);
4846
+ if (!tsf) return null;
4847
+ const child = {
4848
+ rel: target.facts.file,
4849
+ sf: tsf,
4850
+ facts: target.facts,
4851
+ fn: target.fn,
4852
+ depth: frame.depth + 1,
4853
+ via: [...frame.via, `${target.name} (${target.facts.file}:${lineOf(tsf, target.fn)})`],
4854
+ inputNames: /* @__PURE__ */ new Set(),
4855
+ wholeNames: /* @__PURE__ */ new Set(),
4856
+ partialInputs: /* @__PURE__ */ new Map(),
4857
+ reqNames: /* @__PURE__ */ new Set(),
4858
+ clients: /* @__PURE__ */ new Map(),
4859
+ instances: /* @__PURE__ */ new Map(),
4860
+ cls: target.cls,
4861
+ thisProps: target.thisProps,
4862
+ key: `${target.facts.file}#${target.name}@${call.getStart(frame.sf)}`,
4863
+ aliases: /* @__PURE__ */ new Map(),
4864
+ pathPos: [...frame.pathPos, call.getStart(frame.sf)],
4865
+ exitPropagates: frame.exitPropagates && callResultChecked(call)
4866
+ };
4867
+ const cx = wholeContext(frame);
4868
+ target.fn.parameters.forEach((param, i) => {
4869
+ const arg = call.arguments[i];
4870
+ const ab = argBinding(p, arg, frame);
4871
+ const names = boundNames(param.name);
4872
+ const head = names[0];
4873
+ if (ab.client && head !== void 0 && ts11.isIdentifier(param.name)) {
4874
+ child.clients.set(head, ab.client);
4875
+ }
4876
+ if (ab.instance && head !== void 0 && ts11.isIdentifier(param.name)) {
4877
+ child.instances.set(head, ab.instance);
4878
+ }
4879
+ for (const nm of names) if (ab.isRequest) child.reqNames.add(nm);
4880
+ if (ab.tainted) {
4881
+ bindParamTaint(child, param.name, arg, frame);
4882
+ for (const nm of wholeParamNames(param.name, arg, cx)) {
4883
+ child.inputNames.add(nm);
4884
+ child.wholeNames.add(nm);
4885
+ }
4886
+ aliasParam(child, param.name, arg, frame);
4887
+ }
4888
+ });
4889
+ return child;
4890
+ }
4891
+ function frameSignature(child) {
4892
+ return [
4893
+ ...[...child.clients].map(([n, c]) => `${n}=${c.kind}`),
4894
+ ...[...child.inputNames].map((n) => `${n}!${child.wholeNames.has(n) ? "*" : ""}`),
4895
+ ...[...child.partialInputs].map(([n, s]) => `${n}.{${[...s].sort().join("|")}}`),
4896
+ ...[...child.reqNames].map((n) => `${n}?`),
4897
+ ...[...child.thisProps].map(([n, b]) => `this.${n}=${b.client?.kind ?? "-"}`),
4898
+ ...[...child.aliases].map(([n, k]) => `${n}~${k}`),
4899
+ `exit=${child.exitPropagates}`
4900
+ ].sort().join(",");
4901
+ }
4902
+ var CLEAN = { tainted: false, props: null };
4903
+ var TAINTED = { tainted: true, props: null };
4904
+ var MAX_RETURN_DEPTH = 2;
4905
+ function branchesOf(e) {
4906
+ const u = unwrap(e);
4907
+ if (ts11.isConditionalExpression(u)) return [...branchesOf(u.whenTrue), ...branchesOf(u.whenFalse)];
4908
+ if (ts11.isBinaryExpression(u) && (u.operatorToken.kind === ts11.SyntaxKind.QuestionQuestionToken || u.operatorToken.kind === ts11.SyntaxKind.BarBarToken || u.operatorToken.kind === ts11.SyntaxKind.AmpersandAmpersandToken)) {
4909
+ return [...branchesOf(u.left), ...branchesOf(u.right)];
4910
+ }
4911
+ return [u];
4912
+ }
4913
+ function isLiteralValue(e) {
4914
+ return ts11.isStringLiteralLike(e) || ts11.isNumericLiteral(e) || e.kind === ts11.SyntaxKind.NullKeyword || e.kind === ts11.SyntaxKind.TrueKeyword || e.kind === ts11.SyntaxKind.FalseKeyword || ts11.isIdentifier(e) && e.text === "undefined";
4915
+ }
4916
+ function mergeTaint(a, b) {
4917
+ if (!a.tainted) return b;
4918
+ if (!b.tainted) return a;
4919
+ if (a.props === null || b.props === null) return TAINTED;
4920
+ return { tainted: true, props: /* @__PURE__ */ new Set([...a.props, ...b.props]) };
4921
+ }
4922
+ function leafTaint(p, leaf, child, scope, depth) {
4923
+ if (isLiteralValue(leaf)) return CLEAN;
4924
+ if (ts11.isObjectLiteralExpression(leaf)) {
4925
+ const props = taintedProperties(child, leaf);
4926
+ if (props === null) return TAINTED;
4927
+ return props.size === 0 ? CLEAN : { tainted: true, props };
4928
+ }
4929
+ if (ts11.isCallExpression(leaf)) {
4930
+ if (isChainWithQuery(leaf) || isDbChain(leaf, child)) return CLEAN;
4931
+ if (returnsIdentity(p, leaf, scope)) return CLEAN;
4932
+ const nested = returnTaint(p, leaf, child, scope, depth + 1);
4933
+ if (nested !== null) return nested;
4934
+ }
4935
+ return derivedIn(child, leaf) ? TAINTED : CLEAN;
4936
+ }
4937
+ function returnTaint(p, call, frame, scope, depth) {
4938
+ if (depth > MAX_RETURN_DEPTH) return null;
4939
+ const target = callTarget(p, call, frame, scope);
4940
+ if (!target) return null;
4941
+ const child = childFrame(p, call, target, frame);
4942
+ if (!child) return null;
4943
+ const key = `${target.facts.file}#${target.name}#${frameSignature(child)}`;
4944
+ const cached = p.returnTaints.get(key);
4945
+ if (cached !== void 0) return cached;
4946
+ p.returnTaints.set(key, null);
4947
+ const scratch = {
4948
+ inputs: [],
4949
+ authChecks: [],
4950
+ roleChecks: [],
4951
+ queries: [],
4952
+ metadataAccesses: [],
4953
+ visited: /* @__PURE__ */ new Set(),
4954
+ reads: []
4955
+ };
4956
+ bindDeclarations(p, child, scratch);
4957
+ const childScope = scopeOf(p, child.facts);
4958
+ let out = CLEAN;
4959
+ for (const ret of ownReturns(target.fn)) {
4960
+ for (const leaf of branchesOf(ret)) {
4961
+ out = mergeTaint(out, leafTaint(p, leaf, child, childScope, depth));
4962
+ if (out.tainted && out.props === null) break;
4963
+ }
4964
+ }
4965
+ p.returnTaints.set(key, out);
4966
+ return out;
4967
+ }
4462
4968
  function pushQuery(acc, q) {
4463
4969
  const same = acc.queries.find(
4464
4970
  (x) => x.location.file === q.location.file && x.location.line === q.location.line && x.text === q.text && x.client === q.client && x.clientName === q.clientName && x.filters.length === q.filters.length
@@ -4479,8 +4985,8 @@ function pushQuery(acc, q) {
4479
4985
  }
4480
4986
  function dottedPath(e) {
4481
4987
  const u = unwrap(e);
4482
- if (ts10.isIdentifier(u)) return u.text;
4483
- if (ts10.isPropertyAccessExpression(u)) {
4988
+ if (ts11.isIdentifier(u)) return u.text;
4989
+ if (ts11.isPropertyAccessExpression(u)) {
4484
4990
  const base = dottedPath(u.expression);
4485
4991
  return base === null ? null : `${base}.${u.name.text}`;
4486
4992
  }
@@ -4499,14 +5005,14 @@ function valueKey(frame, e) {
4499
5005
  function aliasLocal(frame, name, init) {
4500
5006
  const key = valueKey(frame, init);
4501
5007
  if (key === null) return;
4502
- if (ts10.isIdentifier(name)) {
5008
+ if (ts11.isIdentifier(name)) {
4503
5009
  frame.aliases.set(name.text, key);
4504
5010
  return;
4505
5011
  }
4506
- if (!ts10.isObjectBindingPattern(name)) return;
5012
+ if (!ts11.isObjectBindingPattern(name)) return;
4507
5013
  for (const el of name.elements) {
4508
5014
  const k = propertyKeyOf(el.propertyName ?? el.name);
4509
- if (k !== null && !el.dotDotDotToken && ts10.isIdentifier(el.name)) {
5015
+ if (k !== null && !el.dotDotDotToken && ts11.isIdentifier(el.name)) {
4510
5016
  frame.aliases.set(el.name.text, `${key}.${k}`);
4511
5017
  }
4512
5018
  }
@@ -4514,51 +5020,102 @@ function aliasLocal(frame, name, init) {
4514
5020
  function aliasParam(child, param, arg, frame) {
4515
5021
  if (!arg) return;
4516
5022
  const u = unwrap(arg);
4517
- if (ts10.isIdentifier(param)) {
5023
+ if (ts11.isIdentifier(param)) {
4518
5024
  const key = valueKey(frame, arg);
4519
5025
  if (key !== null) child.aliases.set(param.text, key);
4520
- else if (ts10.isObjectLiteralExpression(u)) {
5026
+ else if (ts11.isObjectLiteralExpression(u)) {
4521
5027
  for (const pr of u.properties) {
4522
5028
  const k = propertyKeyOf(pr.name);
4523
- const v = ts10.isPropertyAssignment(pr) ? pr.initializer : ts10.isShorthandPropertyAssignment(pr) ? pr.name : void 0;
5029
+ const v = ts11.isPropertyAssignment(pr) ? pr.initializer : ts11.isShorthandPropertyAssignment(pr) ? pr.name : void 0;
4524
5030
  const vk = k !== null && v ? valueKey(frame, v) : null;
4525
5031
  if (k !== null && vk !== null) child.aliases.set(`${param.text}.${k}`, vk);
4526
5032
  }
4527
5033
  }
4528
5034
  return;
4529
5035
  }
4530
- if (!ts10.isObjectBindingPattern(param)) return;
5036
+ if (!ts11.isObjectBindingPattern(param)) return;
4531
5037
  const base = valueKey(frame, arg);
4532
5038
  for (const el of param.elements) {
4533
5039
  const k = propertyKeyOf(el.propertyName ?? el.name);
4534
- if (k === null || el.dotDotDotToken || !ts10.isIdentifier(el.name)) continue;
5040
+ if (k === null || el.dotDotDotToken || !ts11.isIdentifier(el.name)) continue;
4535
5041
  if (base !== null) {
4536
5042
  child.aliases.set(el.name.text, `${base}.${k}`);
4537
5043
  continue;
4538
5044
  }
4539
- if (!ts10.isObjectLiteralExpression(u)) continue;
5045
+ if (!ts11.isObjectLiteralExpression(u)) continue;
4540
5046
  const pr = u.properties.find((x) => propertyKeyOf(x.name) === k);
4541
- const v = pr && ts10.isPropertyAssignment(pr) ? pr.initializer : pr && ts10.isShorthandPropertyAssignment(pr) ? pr.name : void 0;
5047
+ const v = pr && ts11.isPropertyAssignment(pr) ? pr.initializer : pr && ts11.isShorthandPropertyAssignment(pr) ? pr.name : void 0;
4542
5048
  const vk = v ? valueKey(frame, v) : null;
4543
5049
  if (vk !== null) child.aliases.set(el.name.text, vk);
4544
5050
  }
4545
5051
  }
4546
- function linkGuards(reads) {
4547
- const col = (c) => (c ?? "").toLowerCase().replace(/_/g, "");
5052
+ function enclosingCondition(node, body) {
5053
+ let child = node;
5054
+ let cur = node.parent;
5055
+ while (cur && cur !== body && !isFunctionLikeNode(cur)) {
5056
+ if (ts11.isIfStatement(cur) && cur.expression !== child) return cur.expression;
5057
+ child = cur;
5058
+ cur = cur.parent;
5059
+ }
5060
+ return null;
5061
+ }
5062
+ var normalizeColumn = (c) => (c ?? "").toLowerCase().replace(/_/g, "");
5063
+ function singular(table) {
5064
+ if (table.endsWith("ies")) return `${table.slice(0, -3)}y`;
5065
+ if (table.endsWith("ses") || table.endsWith("xes")) return table.slice(0, -2);
5066
+ return table.endsWith("s") ? table.slice(0, -1) : table;
5067
+ }
5068
+ function columnNamesTable(column, table) {
5069
+ const c = normalizeColumn(column);
5070
+ if (!c.endsWith("id") || c === "id") return false;
5071
+ const stem = c.slice(0, -2);
5072
+ const t = normalizeColumn(table);
5073
+ return stem === t || stem === singular(t);
5074
+ }
5075
+ function guardMatch(q, g, tables) {
5076
+ const qTable = q.query.table.toLowerCase();
5077
+ const gTable = g.query.table.toLowerCase();
5078
+ const info = tables.get(qTable);
5079
+ for (let i = 0; i < q.query.filters.length; i += 1) {
5080
+ const f = q.query.filters[i];
5081
+ const k = q.keys[i];
5082
+ if (!f?.inputDerived || !f.column || k === null || k === void 0) continue;
5083
+ const j = g.keys.indexOf(k);
5084
+ const gf = j >= 0 ? g.query.filters[j] : void 0;
5085
+ if (!gf) continue;
5086
+ if (qTable === gTable) {
5087
+ if (normalizeColumn(gf.column) === normalizeColumn(f.column))
5088
+ return { read: g, column: f.column };
5089
+ continue;
5090
+ }
5091
+ const ref = info?.columnInfo?.find((c) => c.name === f.column?.toLowerCase())?.references;
5092
+ if (ref && ref.table === gTable && normalizeColumn(ref.column) === normalizeColumn(gf.column)) {
5093
+ return {
5094
+ read: g,
5095
+ column: f.column,
5096
+ parent: { table: g.query.table, column: gf.column ?? "id", how: "foreign key" }
5097
+ };
5098
+ }
5099
+ if (normalizeColumn(gf.column) === "id" && columnNamesTable(f.column, gTable)) {
5100
+ return {
5101
+ read: g,
5102
+ column: f.column,
5103
+ parent: { table: g.query.table, column: gf.column ?? "id", how: "column name" }
5104
+ };
5105
+ }
5106
+ }
5107
+ return null;
5108
+ }
5109
+ function linkGuards(reads, tables) {
4548
5110
  for (const q of reads) {
4549
5111
  let best = null;
4550
5112
  for (const g2 of reads) {
4551
5113
  if (g2 === q || !["select", "unknown"].includes(g2.query.operation)) continue;
4552
- if (g2.query.table.toLowerCase() !== q.query.table.toLowerCase()) continue;
4553
5114
  if (compareOrder(g2.order, q.order) >= 0) continue;
4554
- const shared = q.query.filters.findIndex((f, i) => {
4555
- const k = q.keys[i];
4556
- if (!f.inputDerived || k === null || k === void 0) return false;
4557
- return g2.query.filters.some((gf, j) => g2.keys[j] === k && col(gf.column) === col(f.column));
4558
- });
4559
- const column = q.query.filters[shared]?.column;
4560
- if (shared < 0 || !column) continue;
4561
- if (!best || g2.exits && !best.read.exits) best = { read: g2, column };
5115
+ const m = guardMatch(q, g2, tables);
5116
+ if (!m) continue;
5117
+ const stops = (r) => r.exits || r.checks.length > 0;
5118
+ if (!best || stops(m.read) && !stops(best.read)) best = m;
4562
5119
  }
4563
5120
  if (!best) continue;
4564
5121
  const g = best.read.query;
@@ -4569,20 +5126,23 @@ function linkGuards(reads) {
4569
5126
  clientName: g.clientName,
4570
5127
  filters: g.filters,
4571
5128
  column: best.column,
4572
- exitsWhenMissing: best.read.exits,
5129
+ // A row that fails the comparison stops the entry point too; a missing one fails it as well.
5130
+ exitsWhenMissing: best.read.exits || best.read.checks.length > 0,
4573
5131
  text: g.text,
4574
- ...g.via ? { via: g.via } : {}
5132
+ ...g.via ? { via: g.via } : {},
5133
+ ...best.read.checks.length > 0 ? { checks: best.read.checks } : {},
5134
+ ...best.parent ? { parent: best.parent } : {}
4575
5135
  };
4576
5136
  }
4577
5137
  }
4578
5138
  function isChainWithQuery(e) {
4579
- return ts10.isCallExpression(e) && isQueryChain(e);
5139
+ return ts11.isCallExpression(e) && isQueryChain(e);
4580
5140
  }
4581
5141
  function isDbChain(call, frame) {
4582
5142
  const root = unwrap(flattenChain(call).root);
4583
5143
  let base = root;
4584
- while (ts10.isPropertyAccessExpression(base)) base = base.expression;
4585
- return ts10.isIdentifier(base) && frame.clients.has(base.text);
5144
+ while (ts11.isPropertyAccessExpression(base)) base = base.expression;
5145
+ return ts11.isIdentifier(base) && frame.clients.has(base.text);
4586
5146
  }
4587
5147
  var IDENTITY_CALLEE = /\.auth\.|user|session|claims|auth|principal|viewer/i;
4588
5148
  function returnsIdentity(p, call, scope) {
@@ -4591,10 +5151,10 @@ function returnsIdentity(p, call, scope) {
4591
5151
  }
4592
5152
  function calleePath(e) {
4593
5153
  const u = unwrap(e);
4594
- if (ts10.isIdentifier(u)) return u.text;
4595
- if (ts10.isPropertyAccessExpression(u)) return `${calleePath(u.expression)}.${u.name.text}`;
4596
- if (ts10.isCallExpression(u)) return `${calleePath(u.expression)}()`;
4597
- if (ts10.isElementAccessExpression(u)) return `${calleePath(u.expression)}[]`;
5154
+ if (ts11.isIdentifier(u)) return u.text;
5155
+ if (ts11.isPropertyAccessExpression(u)) return `${calleePath(u.expression)}.${u.name.text}`;
5156
+ if (ts11.isCallExpression(u)) return `${calleePath(u.expression)}()`;
5157
+ if (ts11.isElementAccessExpression(u)) return `${calleePath(u.expression)}[]`;
4598
5158
  return "";
4599
5159
  }
4600
5160
  function analyzeHandler(p, h) {
@@ -4603,6 +5163,7 @@ function analyzeHandler(p, h) {
4603
5163
  const acc = {
4604
5164
  inputs: [],
4605
5165
  authChecks: [],
5166
+ roleChecks: [],
4606
5167
  queries: [],
4607
5168
  metadataAccesses: [],
4608
5169
  visited: /* @__PURE__ */ new Set(),
@@ -4632,7 +5193,7 @@ function analyzeHandler(p, h) {
4632
5193
  };
4633
5194
  const first = fn.parameters[0];
4634
5195
  if (h.kind === "route" && first) {
4635
- if (ts10.isIdentifier(first.name)) frame.reqNames.add(first.name.text);
5196
+ if (ts11.isIdentifier(first.name)) frame.reqNames.add(first.name.text);
4636
5197
  else for (const nm of boundNames(first.name)) if (REQUEST_NAME.test(nm)) frame.reqNames.add(nm);
4637
5198
  }
4638
5199
  if (h.kind === "server_action") {
@@ -4651,7 +5212,7 @@ function analyzeHandler(p, h) {
4651
5212
  acc.authChecks.push({ ...loc2(h.node), kind: "session" });
4652
5213
  }
4653
5214
  analyzeFrame(p, frame, acc);
4654
- linkGuards(acc.reads);
5215
+ linkGuards(acc.reads, p.tables);
4655
5216
  const entry = h.kind === "route" ? `${h.method} ${h.route}` : h.kind === "page" ? `PAGE ${h.route}` : `server action ${h.route}`;
4656
5217
  const stmt = enclosingStatement(h.node);
4657
5218
  const ignores = parseIgnoreDirectives(sf, stmt.getFullStart()).map((d) => ({
@@ -4669,19 +5230,20 @@ function analyzeHandler(p, h) {
4669
5230
  authChecks: acc.authChecks,
4670
5231
  queries: acc.queries,
4671
5232
  metadataAccesses: acc.metadataAccesses,
4672
- ignores
5233
+ ignores,
5234
+ roleChecks: acc.roleChecks
4673
5235
  };
4674
5236
  }
4675
5237
  function publicSecretEnvReads(sf) {
4676
- const isProcessEnv = (e) => ts10.isPropertyAccessExpression(e) && e.name.text === "env" && ts10.isIdentifier(e.expression) && e.expression.text === "process";
5238
+ const isProcessEnv = (e) => ts11.isPropertyAccessExpression(e) && e.name.text === "env" && ts11.isIdentifier(e.expression) && e.expression.text === "process";
4677
5239
  const out = [];
4678
5240
  const visit = (n) => {
4679
- if (ts10.isPropertyAccessExpression(n) && isProcessEnv(n.expression)) {
5241
+ if (ts11.isPropertyAccessExpression(n) && isProcessEnv(n.expression)) {
4680
5242
  if (PUBLIC_SECRET_ENV.test(n.name.text)) out.push({ name: n.name.text, node: n });
4681
- } else if (ts10.isElementAccessExpression(n) && isProcessEnv(n.expression) && ts10.isStringLiteralLike(n.argumentExpression) && PUBLIC_SECRET_ENV.test(n.argumentExpression.text)) {
5243
+ } else if (ts11.isElementAccessExpression(n) && isProcessEnv(n.expression) && ts11.isStringLiteralLike(n.argumentExpression) && PUBLIC_SECRET_ENV.test(n.argumentExpression.text)) {
4682
5244
  out.push({ name: n.argumentExpression.text, node: n });
4683
5245
  }
4684
- ts10.forEachChild(n, visit);
5246
+ ts11.forEachChild(n, visit);
4685
5247
  };
4686
5248
  visit(sf);
4687
5249
  return out;
@@ -4696,7 +5258,7 @@ function findExposures(rel, sf) {
4696
5258
  });
4697
5259
  }
4698
5260
  if (isClientComponentFile(sf)) {
4699
- for (const call of collect(sf, ts10.isCallExpression)) {
5261
+ for (const call of collect(sf, ts11.isCallExpression)) {
4700
5262
  if (!isCreateClientCall(call, sf)) continue;
4701
5263
  const c = classifyCreateClientCall(call, sf);
4702
5264
  if (c.kind === "service_role") {
@@ -4727,7 +5289,14 @@ function parseProject(rootInput, opts = {}) {
4727
5289
  }
4728
5290
  }
4729
5291
  const registry = /* @__PURE__ */ new Map();
4730
- for (const [rel, sf] of sources) registry.set(rel, analyzeModule(rel, sf));
5292
+ for (const [rel, sf] of sources) {
5293
+ try {
5294
+ registry.set(rel, analyzeModule(rel, sf));
5295
+ } catch (e) {
5296
+ sources.delete(rel);
5297
+ warnings.push(`could not analyse ${rel}: ${e instanceof Error ? e.message : String(e)}`);
5298
+ }
5299
+ }
4731
5300
  const tables = /* @__PURE__ */ new Map();
4732
5301
  for (const rel of sql) {
4733
5302
  try {
@@ -4759,12 +5328,46 @@ function parseProject(rootInput, opts = {}) {
4759
5328
  varBindings: /* @__PURE__ */ new Map(),
4760
5329
  drizzleTablesByExport,
4761
5330
  prismaModels,
5331
+ returnTaints: /* @__PURE__ */ new Map(),
5332
+ tables,
4762
5333
  warnings
4763
5334
  };
4764
5335
  const routes = [];
4765
5336
  const exposures = [];
4766
5337
  const fileIgnores = {};
4767
5338
  for (const [rel, sf] of sources) {
5339
+ try {
5340
+ analyzeFile(project, rel, sf, registry.get(rel) ?? analyzeModule(rel, sf), {
5341
+ routes,
5342
+ exposures,
5343
+ fileIgnores
5344
+ });
5345
+ } catch (e) {
5346
+ warnings.push(`could not analyse ${rel}: ${e instanceof Error ? e.message : String(e)}`);
5347
+ }
5348
+ }
5349
+ routes.sort((a, b) => a.entry.localeCompare(b.entry));
5350
+ const all = [...registry.values()];
5351
+ const schema = sqlSchemaFor(tables);
5352
+ warnings.push(...schema.warnings);
5353
+ return {
5354
+ root,
5355
+ files: [...source, ...sql],
5356
+ routes,
5357
+ clientFactories: all.flatMap((f) => f.clientFactories),
5358
+ authHelpers: all.flatMap((f) => f.authHelpers),
5359
+ tables: [...tables.values()],
5360
+ exposures,
5361
+ fileIgnores,
5362
+ warnings,
5363
+ enums: schema.enums,
5364
+ sqlFunctions: schema.sqlFunctions,
5365
+ storageBuckets: schema.storageBuckets
5366
+ };
5367
+ }
5368
+ function analyzeFile(project, rel, sf, facts, out) {
5369
+ const { routes, exposures, fileIgnores } = out;
5370
+ {
4768
5371
  const top = parseIgnoreDirectives(sf, 0).map((d) => ({
4769
5372
  ruleId: d.ruleId,
4770
5373
  reason: d.reason,
@@ -4772,7 +5375,6 @@ function parseProject(rootInput, opts = {}) {
4772
5375
  }));
4773
5376
  if (top.length > 0) fileIgnores[rel] = top;
4774
5377
  exposures.push(...findExposures(rel, sf));
4775
- const facts = registry.get(rel) ?? analyzeModule(rel, sf);
4776
5378
  const route = routeFromFile(rel);
4777
5379
  if (route) {
4778
5380
  for (const { method, exported } of routeHandlersIn(sf)) {
@@ -4825,23 +5427,6 @@ function parseProject(rootInput, opts = {}) {
4825
5427
  }
4826
5428
  }
4827
5429
  }
4828
- routes.sort((a, b) => a.entry.localeCompare(b.entry));
4829
- const all = [...registry.values()];
4830
- const schema = sqlSchemaFor(tables);
4831
- return {
4832
- root,
4833
- files: [...source, ...sql],
4834
- routes,
4835
- clientFactories: all.flatMap((f) => f.clientFactories),
4836
- authHelpers: all.flatMap((f) => f.authHelpers),
4837
- tables: [...tables.values()],
4838
- exposures,
4839
- fileIgnores,
4840
- warnings,
4841
- enums: schema.enums,
4842
- sqlFunctions: schema.sqlFunctions,
4843
- storageBuckets: schema.storageBuckets
4844
- };
4845
5430
  }
4846
5431
 
4847
5432
  // packages/rules/src/packs/sql-functions.ts
@@ -4885,16 +5470,16 @@ var SCOPE_COLUMNS = /* @__PURE__ */ new Set([
4885
5470
  "author_id",
4886
5471
  "profile_id"
4887
5472
  ]);
4888
- function normalizeColumn(column) {
5473
+ function normalizeColumn2(column) {
4889
5474
  return column.toLowerCase().replace(/_/g, "");
4890
5475
  }
4891
- var SCOPE_KEYS = new Set([...SCOPE_COLUMNS].map(normalizeColumn));
5476
+ var SCOPE_KEYS = new Set([...SCOPE_COLUMNS].map(normalizeColumn2));
4892
5477
  function isScopeColumn(column) {
4893
- return column !== null && SCOPE_KEYS.has(normalizeColumn(column));
5478
+ return column !== null && SCOPE_KEYS.has(normalizeColumn2(column));
4894
5479
  }
4895
5480
  function isObjectIdColumn(column) {
4896
5481
  if (column === null || isScopeColumn(column)) return false;
4897
- const c = normalizeColumn(column);
5482
+ const c = normalizeColumn2(column);
4898
5483
  return c === "id" || c === "uuid" || c === "slug" || c.endsWith("id");
4899
5484
  }
4900
5485
  function bypassesRls(kind) {
@@ -4925,7 +5510,8 @@ function handlerViews(ctx) {
4925
5510
  data,
4926
5511
  inputs: data.inputs ?? [],
4927
5512
  authenticated: kinds.length > 0,
4928
- operatorOnly: kinds.length > 0 && kinds.every((k) => k === "secret")
5513
+ operatorOnly: kinds.length > 0 && kinds.every((k) => k === "secret"),
5514
+ roleChecks: data.roleChecks ?? []
4929
5515
  };
4930
5516
  });
4931
5517
  }
@@ -4955,12 +5541,12 @@ function rlsNote(t, tableName, kind) {
4955
5541
  const bypass = kind === "direct_db" ? "a direct database connection (Drizzle/Prisma) does not go through PostgREST, so it does not apply" : "the service role bypasses it";
4956
5542
  return `RLS is enabled on public.${tableName} with ${t.policies.length} polic${t.policies.length === 1 ? "y" : "ies"}, but ${bypass}.`;
4957
5543
  }
4958
- function finding(ctx, rule, partial) {
5544
+ function finding(ctx, rule, partial, severity) {
4959
5545
  return {
4960
5546
  id: ctx.nextId(),
4961
5547
  ruleId: rule.id,
4962
5548
  status: "likely",
4963
- severity: rule.severity,
5549
+ severity: severity ?? rule.severity,
4964
5550
  confidence: rule.confidence,
4965
5551
  cwe: rule.cwe,
4966
5552
  createdAt: ctx.now,
@@ -4968,6 +5554,39 @@ function finding(ctx, rule, partial) {
4968
5554
  ...partial
4969
5555
  };
4970
5556
  }
5557
+ function anonReadPolicy(t) {
5558
+ if (!t?.known || !t.rlsEnabled) return void 0;
5559
+ return t.policyDetails.find(
5560
+ (p) => (p.command === "select" || p.command === "all") && (p.using ?? "").replace(/[\s()]/g, "").toLowerCase() === "true" && (p.roles.length === 0 || p.roles.some((r) => r === "anon" || r === "public"))
5561
+ );
5562
+ }
5563
+ function publicReadNote(p, table) {
5564
+ return ` public.${table} is readable by the anon role through RLS policy "${p.name}" (${p.location.file}:${p.location.line}), so this read leaks nothing beyond what the anon key already returns; the repository may not have intended that policy, so the finding stays at medium.`;
5565
+ }
5566
+ function adminOnly(ctx, h, t) {
5567
+ const check = h.roleChecks[0];
5568
+ if (!check || !t?.known || !singleTenantTable(ctx, t.table)) return void 0;
5569
+ return check;
5570
+ }
5571
+ function singleTenantTable(ctx, table) {
5572
+ const info = ctx.model.tables.find((x) => x.table === table.toLowerCase());
5573
+ if (!info || info.columns.some((c) => isScopeColumn(c))) return false;
5574
+ for (const col of info.columnInfo ?? []) {
5575
+ if (!col.references || col.nullable) continue;
5576
+ const parent = ctx.model.tables.find((x) => x.table === col.references?.table);
5577
+ if (parent?.columns.some((c) => isScopeColumn(c))) return false;
5578
+ }
5579
+ return true;
5580
+ }
5581
+ function adminOnlyNote(check, table) {
5582
+ return ` Admin-only: the handler stops unless ${check.source} passes the role check at ${check.file}:${check.line} (${check.text}), and public.${table} has no tenant or owner column, so the row is shared site content rather than a tenant's; verify the admin check cannot be self-granted.`;
5583
+ }
5584
+ function tableDataOf(ctx, table) {
5585
+ return ctx.graph.nodes.get(`table:${table}`)?.data;
5586
+ }
5587
+ function callerCheck(checks) {
5588
+ return checks?.find((c) => isScopeColumn(c.column) && !c.inputDerived);
5589
+ }
4971
5590
  function guardTiesRowToCaller(guard, table, callerFns) {
4972
5591
  const callerFilter = guard.filters.find((f) => isScopeColumn(f.column) && !f.inputDerived);
4973
5592
  if (callerFilter) {
@@ -4977,6 +5596,14 @@ function guardTiesRowToCaller(guard, table, callerFns) {
4977
5596
  why: ""
4978
5597
  };
4979
5598
  }
5599
+ const compared = callerCheck(guard.checks);
5600
+ if (compared) {
5601
+ return {
5602
+ tied: true,
5603
+ how: `its ${compared.column} compared with ${compared.valueText} in code, stopping otherwise`,
5604
+ why: ""
5605
+ };
5606
+ }
4980
5607
  if (guard.client !== "user_scoped") {
4981
5608
  return {
4982
5609
  tied: false,
@@ -5017,12 +5644,19 @@ var serviceRoleObjectAccessWithoutTenantScope = {
5017
5644
  const filters = q.filters;
5018
5645
  const idFilter = filters.find((f) => f.inputDerived && isObjectIdColumn(f.column));
5019
5646
  if (!idFilter || filters.some((f) => isScopeColumn(f.column))) continue;
5647
+ if (q.operation === "select" && callerCheck(q.ownerChecks)) continue;
5020
5648
  const guard = q.guard;
5021
- const tied = guard ? guardTiesRowToCaller(guard, v.tableData, callerFns) : null;
5649
+ const guardTable = guard?.parent ? tableDataOf(ctx, guard.table) : v.tableData;
5650
+ const tied = guard ? guardTiesRowToCaller(guard, guardTable, callerFns) : null;
5022
5651
  if (guard && tied?.tied && guard.exitsWhenMissing) continue;
5023
- const guardNote = guard && tied && (tied.tied || guard.client === "user_scoped") ? ` An earlier read of the same "${guard.column}" at ${guard.location.file}:${guard.location.line} (${tied.how}) could be an ownership check, but ${tied.tied ? "the entry point does not stop when it finds no row" : tied.why}.` : "";
5652
+ const guardWhat = guard?.parent ? `the parent row ${guard.parent.table}.${guard.parent.column} (${q.table}.${guard.column} refers to it by ${guard.parent.how})` : `the same "${guard?.column}"`;
5653
+ const guardNote = guard && tied && (tied.tied || guard.client === "user_scoped") ? ` An earlier read of ${guardWhat} at ${guard.location.file}:${guard.location.line} (${tied.how}) could be an ownership check, but ${tied.tied ? "the entry point does not stop when it finds no row" : tied.why}.` : "";
5024
5654
  const tableName = v.tableData?.table ?? q.table;
5025
5655
  const authNote = h.authenticated ? "The handler authenticates the caller but never checks that the row belongs to them." : "The handler does not authenticate the caller at all.";
5656
+ const admin = adminOnly(ctx, h, v.tableData);
5657
+ const anonPolicy = q.operation === "select" ? anonReadPolicy(v.tableData) : void 0;
5658
+ const downgrade = admin || anonPolicy ? "medium" : void 0;
5659
+ const downgradeNote = (admin ? adminOnlyNote(admin, tableName) : "") + (anonPolicy ? publicReadNote(anonPolicy, tableName) : "");
5026
5660
  const path = [
5027
5661
  h.data.kind === "server_action" ? "Server action call" : "HTTP request",
5028
5662
  h.data.entry,
@@ -5034,26 +5668,40 @@ var serviceRoleObjectAccessWithoutTenantScope = {
5034
5668
  const evidence = [
5035
5669
  {
5036
5670
  kind: "rule",
5037
- summary: `${q.operation} on public.${tableName} filtered by user-controlled "${idFilter.column}" through a ${clientNoun(v.clientData)}, with no tenant/owner scoping. ${authNote} ${rlsNote(v.tableData, tableName, v.clientData?.kind)}${viaNote(q)}${guardNote}`,
5038
- locations: locations(h.handler.location, v.query.location, v.client?.location),
5671
+ summary: `${q.operation} on public.${tableName} filtered by user-controlled "${idFilter.column}" through a ${clientNoun(v.clientData)}, with no tenant/owner scoping. ${authNote} ${rlsNote(v.tableData, tableName, v.clientData?.kind)}${viaNote(q)}${guardNote}${downgradeNote}`,
5672
+ locations: locations(
5673
+ h.handler.location,
5674
+ v.query.location,
5675
+ v.client?.location,
5676
+ admin ? { file: admin.file, line: admin.line } : void 0,
5677
+ anonPolicy?.location
5678
+ ),
5039
5679
  data: {
5040
5680
  deterministic: false,
5041
5681
  ruleId: this.id,
5042
5682
  authenticated: h.authenticated,
5043
- query: q.text
5683
+ query: q.text,
5684
+ ...admin ? { adminOnly: true, roleCheck: admin.source } : {},
5685
+ ...anonPolicy ? { anonReadPolicy: anonPolicy.name } : {}
5044
5686
  }
5045
5687
  },
5046
5688
  { kind: "trace", summary: path.join(" -> ") }
5047
5689
  ];
5690
+ const who = admin ? "Admin-only" : h.authenticated ? "Cross-tenant" : "Unauthenticated";
5048
5691
  out.push(
5049
- finding(ctx, this, {
5050
- title: `${h.authenticated ? "Cross-tenant" : "Unauthenticated"} ${q.operation} on "${tableName}" via ${clientNoun(v.clientData)}`,
5051
- entrypoints: [h.data.entry],
5052
- sources: h.inputs.map((i) => `${i.kind}:${i.name}`),
5053
- sinks: [`supabase.${q.operation}:public.${tableName}`],
5054
- path,
5055
- evidence
5056
- })
5692
+ finding(
5693
+ ctx,
5694
+ this,
5695
+ {
5696
+ title: `${who} ${q.operation} on "${tableName}" via ${clientNoun(v.clientData)}${admin ? " (verify the admin check)" : ""}`,
5697
+ entrypoints: [h.data.entry],
5698
+ sources: h.inputs.map((i) => `${i.kind}:${i.name}`),
5699
+ sinks: [`supabase.${q.operation}:public.${tableName}`],
5700
+ path,
5701
+ evidence
5702
+ },
5703
+ downgrade
5704
+ )
5057
5705
  );
5058
5706
  }
5059
5707
  }
@@ -5170,7 +5818,13 @@ var rlsPolicyWithoutCallerPredicate = {
5170
5818
  ],
5171
5819
  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.`,
5172
5820
  title: `RLS policy "${p.name}" on "${t.table}" does not scope rows to the caller`,
5173
- data: { deterministic: false, ruleId: this.id, policy: p.name },
5821
+ data: {
5822
+ deterministic: false,
5823
+ ruleId: this.id,
5824
+ policy: p.name,
5825
+ command: p.command,
5826
+ table: t.table
5827
+ },
5174
5828
  tail: locations(p.location)
5175
5829
  }));
5176
5830
  addReach(g, h, v, `supabase.${op}:public.${t.table}`);
@@ -5331,6 +5985,14 @@ var serviceRoleQueryWithoutAuthentication = {
5331
5985
  const v = views[0];
5332
5986
  if (!v) continue;
5333
5987
  const tables = [...new Set(views.map((x) => x.tableData?.table ?? x.data.table))];
5988
+ const anonPolicies = views.map(
5989
+ (x) => x.data.operation === "select" ? anonReadPolicy(x.tableData) : void 0
5990
+ );
5991
+ const allPublicReads = anonPolicies.every((x) => x !== void 0);
5992
+ const publicNote = allPublicReads ? views.map((x, i) => {
5993
+ const pol = anonPolicies[i];
5994
+ return pol ? publicReadNote(pol, x.tableData?.table ?? x.data.table) : "";
5995
+ }).join("") : "";
5334
5996
  const path = [
5335
5997
  h.data.kind === "server_action" ? "Server action call" : "HTTP request",
5336
5998
  h.data.entry,
@@ -5339,24 +6001,37 @@ var serviceRoleQueryWithoutAuthentication = {
5339
6001
  `public.${tables.join(", public.")}`
5340
6002
  ];
5341
6003
  out.push(
5342
- finding(ctx, this, {
5343
- title: `Unauthenticated ${views.some((v2) => v2.clientData?.kind === "direct_db") ? "database" : "service-role"} access to "${tables.join('", "')}" in ${h.data.entry}`,
5344
- entrypoints: [h.data.entry],
5345
- sources: h.inputs.map((i) => `${i.kind}:${i.name}`),
5346
- sinks: views.map(
5347
- (x) => `supabase.${x.data.operation}:public.${x.tableData?.table ?? x.data.table}`
5348
- ),
5349
- path,
5350
- evidence: [
5351
- {
5352
- kind: "rule",
5353
- summary: `${h.data.entry} runs ${views.length} privileged quer${views.length === 1 ? "y" : "ies"} (${tables.join(", ")}; RLS does not protect them) and establishes no caller: no auth.getUser/getSession/getClaims call, no session from an auth library, no auth helper, no comparison of a request credential with a server secret (cron secret, API key, signature) and no API-key lookup was found, directly or in the helpers it calls.`,
5354
- locations: locations(h.handler.location, ...views.map((x) => x.query.location)),
5355
- data: { deterministic: false, ruleId: this.id }
5356
- },
5357
- { kind: "trace", summary: path.join(" -> ") }
5358
- ]
5359
- })
6004
+ finding(
6005
+ ctx,
6006
+ this,
6007
+ {
6008
+ title: `Unauthenticated ${views.some((v2) => v2.clientData?.kind === "direct_db") ? "database" : "service-role"} access to "${tables.join('", "')}" in ${h.data.entry}`,
6009
+ entrypoints: [h.data.entry],
6010
+ sources: h.inputs.map((i) => `${i.kind}:${i.name}`),
6011
+ sinks: views.map(
6012
+ (x) => `supabase.${x.data.operation}:public.${x.tableData?.table ?? x.data.table}`
6013
+ ),
6014
+ path,
6015
+ evidence: [
6016
+ {
6017
+ kind: "rule",
6018
+ summary: `${h.data.entry} runs ${views.length} privileged quer${views.length === 1 ? "y" : "ies"} (${tables.join(", ")}; RLS does not protect them) and establishes no caller: no auth.getUser/getSession/getClaims call, no session from an auth library, no auth helper, no comparison of a request credential with a server secret (cron secret, API key, signature) and no API-key lookup was found, directly or in the helpers it calls.${publicNote}`,
6019
+ locations: locations(
6020
+ h.handler.location,
6021
+ ...views.map((x) => x.query.location),
6022
+ ...anonPolicies.map((x) => x?.location)
6023
+ ),
6024
+ data: {
6025
+ deterministic: false,
6026
+ ruleId: this.id,
6027
+ ...allPublicReads ? { anonReadPolicies: anonPolicies.map((x) => x?.name ?? "") } : {}
6028
+ }
6029
+ },
6030
+ { kind: "trace", summary: path.join(" -> ") }
6031
+ ]
6032
+ },
6033
+ allPublicReads ? "medium" : void 0
6034
+ )
5360
6035
  );
5361
6036
  }
5362
6037
  return out;
@@ -5421,6 +6096,253 @@ var supabaseAuthorizationPack = [
5421
6096
  rlsPolicyWithoutCallerPredicate
5422
6097
  ];
5423
6098
 
6099
+ // packages/rules/src/packs/supabase-sql-policies.ts
6100
+ var DATA_API = "Supabase Data API (PostgREST)";
6101
+ function locations2(...refs) {
6102
+ const out = [];
6103
+ for (const r of refs) {
6104
+ if (r && !out.some((o) => o.file === r.file && o.line === r.line)) out.push(r);
6105
+ }
6106
+ return out;
6107
+ }
6108
+ function finding2(ctx, rule, body, severity) {
6109
+ return {
6110
+ id: ctx.nextId(),
6111
+ ruleId: rule.id,
6112
+ status: "likely",
6113
+ severity: severity ?? rule.severity,
6114
+ confidence: rule.confidence,
6115
+ cwe: rule.cwe,
6116
+ createdAt: ctx.now,
6117
+ updatedAt: ctx.now,
6118
+ ...body
6119
+ };
6120
+ }
6121
+ function publicTables(ctx) {
6122
+ return ctx.model.tables.filter((t) => !t.table.includes("."));
6123
+ }
6124
+ function commandLabel(p) {
6125
+ return p.command === "all" ? "for all commands" : `for ${p.command}`;
6126
+ }
6127
+ function roleLabel(p) {
6128
+ return p.roles.length === 0 ? "public (no TO clause)" : p.roles.join(", ");
6129
+ }
6130
+ function quote(expr, max = 200) {
6131
+ if (expr === null) return "(none)";
6132
+ const one = expr.replace(/\s+/g, " ").trim();
6133
+ return one.length > max ? `${one.slice(0, max - 1)}\u2026` : one;
6134
+ }
6135
+ var USER_METADATA = /\buser_metadata\b/i;
6136
+ var RAW_USER_META = /\braw_user_meta_data\b/i;
6137
+ var JWT_SOURCE = /auth\s*\.\s*jwt\s*\(|request\.jwt\.claim/i;
6138
+ var AUTH_USERS = /\bauth\s*\.\s*users\b/i;
6139
+ function readsSelfWrittenMetadata(expr) {
6140
+ if (USER_METADATA.test(expr) && JWT_SOURCE.test(expr)) return true;
6141
+ return RAW_USER_META.test(expr) && AUTH_USERS.test(expr);
6142
+ }
6143
+ var rlsPolicyTrustsUserMetadata = {
6144
+ id: "supabase.rls-policy-trusts-user-metadata",
6145
+ title: "RLS policy reads user_metadata from the JWT",
6146
+ description: "A policy decides access with a claim under user_metadata (or raw_user_meta_data). Any signed-in user can write user_metadata with updateUser({ data }), and the claim lands in their next JWT without validation, so the policy grants itself. Move the claim to app_metadata, which only the service role writes, or read a roles table joined on auth.uid().",
6147
+ severity: "critical",
6148
+ confidence: 0.9,
6149
+ cwe: ["CWE-602", "CWE-863"],
6150
+ evaluate(ctx) {
6151
+ const out = [];
6152
+ for (const t of publicTables(ctx)) {
6153
+ for (const p of t.policyDetails) {
6154
+ const where2 = [];
6155
+ if (p.using !== null && readsSelfWrittenMetadata(p.using)) where2.push(["USING", p.using]);
6156
+ if (p.check !== null && readsSelfWrittenMetadata(p.check))
6157
+ where2.push(["WITH CHECK", p.check]);
6158
+ if (where2.length === 0) continue;
6159
+ const clause = where2.map(([kind, expr]) => `${kind} ${quote(expr)}`).join(" / ");
6160
+ const evidence = [
6161
+ {
6162
+ kind: "rule",
6163
+ summary: `Policy "${p.name}" on public.${t.table} (${commandLabel(p)}, to ${roleLabel(p)}) decides access from user_metadata: ${clause}. A signed-in user sets user_metadata themselves with supabase.auth.updateUser({ data: { ... } }); the value is copied into their next access token without any check, so the caller can grant themselves whatever this predicate asks for. app_metadata cannot be written this way, and a roles table joined on auth.uid() cannot either.`,
6164
+ locations: locations2(p.location, t.location),
6165
+ // No `deterministic: true`: a critical deterministic finding blocks the GitHub
6166
+ // check, and this rule has not been seen on a real repository yet (zero hits on the
6167
+ // 26-repository corpus of 13 Sept 2026). It blocks once measured, not before.
6168
+ data: {
6169
+ ruleId: this.id,
6170
+ table: t.table,
6171
+ policy: p.name,
6172
+ command: p.command
6173
+ }
6174
+ },
6175
+ {
6176
+ kind: "trace",
6177
+ summary: [
6178
+ "Any signed-in user",
6179
+ 'auth.updateUser({ data: { role: "admin" } })',
6180
+ "the claim is signed into the next JWT",
6181
+ `policy "${p.name}" reads it from user_metadata`,
6182
+ `public.${t.table} (${commandLabel(p)})`
6183
+ ].join(" -> ")
6184
+ }
6185
+ ];
6186
+ out.push(
6187
+ finding2(ctx, this, {
6188
+ title: `Policy "${p.name}" on "${t.table}" trusts user_metadata`,
6189
+ entrypoints: [DATA_API],
6190
+ sources: ["jwt:user_metadata"],
6191
+ sinks: [`supabase.policy:public.${t.table}`],
6192
+ path: [
6193
+ "Any signed-in user",
6194
+ "user_metadata written by the user",
6195
+ `policy "${p.name}"`,
6196
+ `public.${t.table}`
6197
+ ],
6198
+ evidence
6199
+ })
6200
+ );
6201
+ }
6202
+ }
6203
+ return out;
6204
+ }
6205
+ };
6206
+ var policiesWithoutRlsEnabled = {
6207
+ id: "supabase.policies-without-rls-enabled",
6208
+ title: "Policies exist but RLS is never enabled on the table",
6209
+ description: "A migration writes policies for a table but never runs `alter table ... enable row level security`, so the policies have no effect and the table stays fully readable and writable through the Data API. Every reviewer who sees the policies assumes the table is protected. Supabase's own lint (0007) reports the same shape.",
6210
+ severity: "high",
6211
+ confidence: 0.9,
6212
+ cwe: ["CWE-284"],
6213
+ evaluate(ctx) {
6214
+ const out = [];
6215
+ for (const t of publicTables(ctx)) {
6216
+ if (t.rlsEnabled || t.policyDetails.length === 0) continue;
6217
+ const names = t.policyDetails.map((p) => `"${p.name}" (${commandLabel(p)})`).join(", ");
6218
+ const evidence = [
6219
+ {
6220
+ kind: "rule",
6221
+ summary: `public.${t.table} has ${t.policyDetails.length} ${t.policyDetails.length === 1 ? "policy" : "policies"} \u2014 ${names} \u2014 and no "alter table public.${t.table} enable row level security" anywhere in the migrations. Policies only apply to a table with RLS on, so every one of them is dead and the table is fully readable and writable by anyone holding the public anon key. The policies say what the intent was, which is what makes this a defect rather than a choice.`,
6222
+ locations: locations2(t.location, t.policyDetails[0]?.location),
6223
+ data: {
6224
+ deterministic: true,
6225
+ ruleId: this.id,
6226
+ table: t.table,
6227
+ policies: t.policyDetails.map((p) => p.name)
6228
+ }
6229
+ },
6230
+ {
6231
+ kind: "trace",
6232
+ summary: [
6233
+ "Anyone with the public anon key",
6234
+ "PostgREST",
6235
+ `public.${t.table} (RLS never enabled)`,
6236
+ `${t.policyDetails.length} policies that never run`
6237
+ ].join(" -> ")
6238
+ }
6239
+ ];
6240
+ out.push(
6241
+ finding2(ctx, this, {
6242
+ title: `Table "${t.table}" has policies but RLS is off`,
6243
+ entrypoints: [DATA_API],
6244
+ sources: ["anon-key"],
6245
+ sinks: [`supabase.select:public.${t.table}`],
6246
+ path: [
6247
+ "Anyone with the public anon key",
6248
+ "PostgREST",
6249
+ `public.${t.table} (RLS off, policies inert)`
6250
+ ],
6251
+ evidence
6252
+ })
6253
+ );
6254
+ }
6255
+ return out;
6256
+ }
6257
+ };
6258
+ function isTautology(expr) {
6259
+ if (expr === null) return false;
6260
+ return /^\(*\s*true\s*\)*$/i.test(expr.trim());
6261
+ }
6262
+ var ANON_ROLES = /* @__PURE__ */ new Set(["anon", "public"]);
6263
+ function reachableByAnon(p) {
6264
+ if (p.roles.length === 0) return true;
6265
+ return p.roles.some((r) => ANON_ROLES.has(r.toLowerCase().replace(/^"|"$/g, "")));
6266
+ }
6267
+ var WRITE_COMMANDS = /* @__PURE__ */ new Set(["insert", "update", "delete", "all"]);
6268
+ var anonWritePolicy = {
6269
+ id: "supabase.anon-write-policy",
6270
+ title: "Write policy open to anon or every role",
6271
+ description: "An insert, update or delete policy targets anon (or omits the TO clause, which means PUBLIC) and decides with `true`. Anyone holding the public anon key writes the table directly through PostgREST, including rows that belong to signed-in users, whether or not the application has a form for it.",
6272
+ severity: "high",
6273
+ confidence: 0.85,
6274
+ cwe: ["CWE-284", "CWE-862"],
6275
+ evaluate(ctx) {
6276
+ const out = [];
6277
+ for (const t of publicTables(ctx)) {
6278
+ if (!t.rlsEnabled) continue;
6279
+ for (const p of t.policyDetails) {
6280
+ if (!WRITE_COMMANDS.has(p.command) || !reachableByAnon(p)) continue;
6281
+ const decides = p.command === "insert" ? [["WITH CHECK", p.check]] : p.command === "all" ? [
6282
+ ["USING", p.using],
6283
+ ["WITH CHECK", p.check]
6284
+ ] : [["USING", p.using]];
6285
+ const open = decides.filter(([, expr]) => isTautology(expr));
6286
+ if (open.length === 0) continue;
6287
+ const insertOnly = p.command === "insert";
6288
+ const severity = insertOnly ? "medium" : "high";
6289
+ const clause = open.map(([kind, expr]) => `${kind} ${quote(expr)}`).join(" / ");
6290
+ const verbs = p.command === "all" ? "insert, update and delete" : `${p.command} rows in`;
6291
+ const evidence = [
6292
+ {
6293
+ kind: "rule",
6294
+ summary: `Policy "${p.name}" on public.${t.table} is ${commandLabel(p)} to ${roleLabel(p)} and decides with a tautology: ${clause}. Anyone holding the public anon key can ${verbs} public.${t.table} straight through PostgREST, without going through this application.${insertOnly ? " An insert-only policy is how deliberate public forms are written, so this is reported as medium: check that the table is meant to accept rows from strangers and that a rate limit and a validation trigger exist." : " Rows that belong to signed-in users can be changed or deleted by a stranger."}`,
6295
+ locations: locations2(p.location, t.location),
6296
+ data: {
6297
+ deterministic: true,
6298
+ ruleId: this.id,
6299
+ table: t.table,
6300
+ policy: p.name,
6301
+ command: p.command,
6302
+ roles: p.roles
6303
+ }
6304
+ },
6305
+ {
6306
+ kind: "trace",
6307
+ summary: [
6308
+ "Anyone with the public anon key",
6309
+ "PostgREST",
6310
+ `policy "${p.name}" (${commandLabel(p)}, to ${roleLabel(p)}, ${clause})`,
6311
+ `public.${t.table}`
6312
+ ].join(" -> ")
6313
+ }
6314
+ ];
6315
+ out.push(
6316
+ finding2(
6317
+ ctx,
6318
+ this,
6319
+ {
6320
+ title: `Policy "${p.name}" lets anyone ${p.command === "all" ? "write" : p.command} "${t.table}"`,
6321
+ entrypoints: [DATA_API],
6322
+ sources: ["anon-key"],
6323
+ sinks: [`supabase.${p.command === "all" ? "insert" : p.command}:public.${t.table}`],
6324
+ path: [
6325
+ "Anyone with the public anon key",
6326
+ "PostgREST",
6327
+ `policy "${p.name}" (${commandLabel(p)}, to ${roleLabel(p)})`,
6328
+ `public.${t.table}`
6329
+ ],
6330
+ evidence
6331
+ },
6332
+ severity
6333
+ )
6334
+ );
6335
+ }
6336
+ }
6337
+ return out;
6338
+ }
6339
+ };
6340
+ var supabaseSqlPoliciesPack = [
6341
+ rlsPolicyTrustsUserMetadata,
6342
+ policiesWithoutRlsEnabled,
6343
+ anonWritePolicy
6344
+ ];
6345
+
5424
6346
  // packages/rules/src/packs/supabase-storage-rpc.ts
5425
6347
  function reaches(ctx) {
5426
6348
  const out = [];
@@ -5443,7 +6365,7 @@ function reaches(ctx) {
5443
6365
  }
5444
6366
  return out;
5445
6367
  }
5446
- function locations2(...refs) {
6368
+ function locations3(...refs) {
5447
6369
  const out = [];
5448
6370
  for (const r of refs) {
5449
6371
  if (r && !out.some((o) => o.file === r.file && o.line === r.line)) out.push(r);
@@ -5453,7 +6375,7 @@ function locations2(...refs) {
5453
6375
  function unique(values) {
5454
6376
  return [...new Set(values)];
5455
6377
  }
5456
- function finding2(ctx, rule, body, severity) {
6378
+ function finding3(ctx, rule, body, severity) {
5457
6379
  return {
5458
6380
  id: ctx.nextId(),
5459
6381
  ruleId: rule.id,
@@ -5509,7 +6431,7 @@ var storageObjectAccessWithoutOwnerScope = {
5509
6431
  {
5510
6432
  kind: "rule",
5511
6433
  summary: `storage.from(${s.bucket === null ? "\u2026" : `"${s.bucket}"`}).${s.op}(${s.pathText}) runs through ${client}, a service-role client, so storage policies on storage.objects do not apply. The object path comes from the request and is neither prefixed with nor checked against the caller's user id, so any signed-in user can ${verb} any object in ${bucketLabel(s.bucket)}, including other users' files. The handler authenticates the caller but never ties the path to them.${via}`,
5512
- locations: locations2(r.handler.location, r.query.location, r.client?.location),
6434
+ locations: locations3(r.handler.location, r.query.location, r.client?.location),
5513
6435
  data: {
5514
6436
  deterministic: false,
5515
6437
  ruleId: this.id,
@@ -5522,7 +6444,7 @@ var storageObjectAccessWithoutOwnerScope = {
5522
6444
  { kind: "trace", summary: path.join(" -> ") }
5523
6445
  ];
5524
6446
  out.push(
5525
- finding2(ctx, this, {
6447
+ finding3(ctx, this, {
5526
6448
  title: `Cross-user storage ${s.op} in ${bucketLabel(s.bucket)} via service-role client`,
5527
6449
  entrypoints: [r.handlerData.entry],
5528
6450
  sources: r.inputs.map((i) => `${i.kind}:${i.name}`),
@@ -5629,7 +6551,7 @@ var storagePolicyWithoutOwnerCheck = {
5629
6551
  ];
5630
6552
  const reachNote = entries.length > 0 ? ` The app reaches the bucket from ${entries.join(", ")} with a client that relies on this policy.` : "";
5631
6553
  out.push(
5632
- finding2(ctx, this, {
6554
+ finding3(ctx, this, {
5633
6555
  title: `Storage policy "${p.name}" lets ${who} ${verb} every object in ${buckets.length > 0 ? `bucket ${where2}` : "every bucket"}`,
5634
6556
  entrypoints: [...entries, storageEntry],
5635
6557
  sources: unique([
@@ -5642,7 +6564,7 @@ var storagePolicyWithoutOwnerCheck = {
5642
6564
  {
5643
6565
  kind: "rule",
5644
6566
  summary: `Policy "${p.name}" for ${p.command} on storage.objects ${clause} (${expr}) checks only the bucket: no auth.uid(), no storage.foldername(name) ownership, no owner column. So ${whoLong} can ${verb} every object in ${where2}, including other users' files, straight through the Storage API.${privateNote}${reachNote} Scope it to the owner, e.g. bucket_id = '${buckets[0] ?? "<bucket>"}' and (storage.foldername(name))[1] = (select auth.uid())::text.`,
5645
- locations: locations2(p.location, ...reached.map((r) => r.query.location)),
6567
+ locations: locations3(p.location, ...reached.map((r) => r.query.location)),
5646
6568
  data: { deterministic: false, ruleId: this.id, policy: p.name, buckets }
5647
6569
  },
5648
6570
  { kind: "trace", summary: path.join(" -> ") }
@@ -5690,7 +6612,7 @@ var securityDefinerFunctionWithoutCallerCheck = {
5690
6612
  "no auth.uid() / auth.jwt() check"
5691
6613
  ];
5692
6614
  out.push(
5693
- finding2(
6615
+ finding3(
5694
6616
  ctx,
5695
6617
  this,
5696
6618
  {
@@ -5706,7 +6628,7 @@ var securityDefinerFunctionWithoutCallerCheck = {
5706
6628
  {
5707
6629
  kind: "rule",
5708
6630
  summary: `public.${fn.name}() is SECURITY DEFINER: it runs with the rights of its owner and Row Level Security does not apply inside it. Its body never reads the caller's identity (auth.uid(), auth.jwt(), auth.email() or the request JWT), so whatever it returns or changes is available to every role that can execute it: ${roles.join(", ")}. ${who} at ${endpoint}.${callNote} Filter by auth.uid() inside the function, make it SECURITY INVOKER, or revoke EXECUTE from public, anon and authenticated.`,
5709
- locations: locations2(
6631
+ locations: locations3(
5710
6632
  fn.location,
5711
6633
  ...sites.flatMap((s) => [s.handler.location, s.query.location])
5712
6634
  ),
@@ -5751,8 +6673,40 @@ function runRules(rules, model, graph, opts = {}) {
5751
6673
  }
5752
6674
  }
5753
6675
  findings = applySuppressions(findings, model, now);
6676
+ findings = applyPublicTables(findings, opts.publicTables ?? [], now);
5754
6677
  return findings;
5755
6678
  }
6679
+ var PUBLIC_TABLE_RULES = /* @__PURE__ */ new Set([
6680
+ "supabase.service-role-query-without-authentication",
6681
+ "supabase.rls-policy-without-caller-predicate"
6682
+ ]);
6683
+ var READ_SINK = /^supabase\.select:public\.(.+)$/;
6684
+ function applyPublicTables(findings, publicTables2, now) {
6685
+ if (publicTables2.length === 0) return findings;
6686
+ const declared = new Set(publicTables2.map((t) => t.toLowerCase()));
6687
+ return findings.map((f) => {
6688
+ if (f.status === "suppressed" || !PUBLIC_TABLE_RULES.has(f.ruleId)) return f;
6689
+ if (f.sinks.length === 0) return f;
6690
+ const tables = [];
6691
+ for (const sink of f.sinks) {
6692
+ const table = READ_SINK.exec(sink)?.[1];
6693
+ if (table === void 0 || !declared.has(table.toLowerCase())) return f;
6694
+ if (!tables.includes(table)) tables.push(table);
6695
+ }
6696
+ const command = f.evidence[0]?.data?.command;
6697
+ if (f.ruleId === "supabase.rls-policy-without-caller-predicate" && command !== "select") {
6698
+ return f;
6699
+ }
6700
+ return transition(f, "suppressed", {
6701
+ evidence: {
6702
+ kind: "rule",
6703
+ summary: `Suppressed: declared public in audit.config.json (publicTables: ${tables.join(", ")}). Every query of this finding only reads ${tables.map((t) => `public.${t}`).join(", ")}; the declaration never covers insert, update, delete or rpc paths, nor the object-access and mass-assignment rules.`,
6704
+ data: { suppressed: true, ruleId: f.ruleId, publicTables: tables }
6705
+ },
6706
+ now
6707
+ });
6708
+ });
6709
+ }
5756
6710
  function applySuppressions(findings, model, now) {
5757
6711
  const byEntry = new Map(model.routes.map((h) => [h.entry, h.ignores]));
5758
6712
  return findings.map((f) => {
@@ -5777,12 +6731,15 @@ function applySuppressions(findings, model, now) {
5777
6731
  // packages/rules/src/index.ts
5778
6732
  var defaultRules = [
5779
6733
  ...supabaseAuthorizationPack,
5780
- ...supabaseStorageRpcPack
6734
+ ...supabaseStorageRpcPack,
6735
+ ...supabaseSqlPoliciesPack
5781
6736
  ];
5782
6737
 
5783
6738
  // packages/scanner/src/config.ts
5784
6739
  import { existsSync, readFileSync as readFileSync3, realpathSync, statSync } from "node:fs";
5785
6740
  import { isAbsolute, join as join4, resolve as resolve3, sep as sep2 } from "node:path";
6741
+ var MAX_PUBLIC_TABLES = 64;
6742
+ var TABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
5786
6743
  var MAX_REPO_MIGRATION_DIRS = 16;
5787
6744
  function shown2(value) {
5788
6745
  return JSON.stringify(value.length > 120 ? `${value.slice(0, 120)}...` : value);
@@ -5808,14 +6765,42 @@ function loadAuditConfig(root) {
5808
6765
  const obj = raw;
5809
6766
  const ignore = strings(obj.ignore);
5810
6767
  const migrations = strings(obj.migrations);
6768
+ const publicTables2 = publicTableNames(obj.publicTables);
5811
6769
  return {
5812
6770
  config: {
5813
6771
  ...ignore ? { ignore } : {},
5814
- ...migrations ? { migrations } : {}
6772
+ ...migrations ? { migrations } : {},
6773
+ ...publicTables2.tables ? { publicTables: publicTables2.tables } : {}
5815
6774
  },
5816
- warnings: []
6775
+ warnings: publicTables2.warnings
5817
6776
  };
5818
6777
  }
6778
+ function publicTableNames(value) {
6779
+ const warnings = [];
6780
+ if (value === void 0) return { warnings };
6781
+ if (!Array.isArray(value)) {
6782
+ return { warnings: ["audit.config.json: publicTables ignored (not an array of table names)"] };
6783
+ }
6784
+ const tables = [];
6785
+ for (const entry of value) {
6786
+ if (typeof entry !== "string" || !TABLE_NAME.test(entry.trim())) {
6787
+ warnings.push(
6788
+ `audit.config.json: ignored publicTables entry ${shown2(String(entry))} (not a table name)`
6789
+ );
6790
+ continue;
6791
+ }
6792
+ const name = entry.trim().toLowerCase();
6793
+ if (tables.includes(name)) continue;
6794
+ if (tables.length >= MAX_PUBLIC_TABLES) {
6795
+ warnings.push(
6796
+ `audit.config.json: ignored publicTables entry ${shown2(entry)} (at most ${MAX_PUBLIC_TABLES} tables)`
6797
+ );
6798
+ continue;
6799
+ }
6800
+ tables.push(name);
6801
+ }
6802
+ return { tables, warnings };
6803
+ }
5819
6804
  function inside(base, p) {
5820
6805
  return p === base || p.startsWith(base.endsWith(sep2) ? base : `${base}${sep2}`);
5821
6806
  }
@@ -5881,7 +6866,28 @@ function repoMigrationDirs(root, entries) {
5881
6866
  }
5882
6867
 
5883
6868
  // packages/scanner/src/scan.ts
5884
- function summarize(model, rules) {
6869
+ var ScanError = class extends Error {
6870
+ constructor(code, path, message) {
6871
+ super(message);
6872
+ this.code = code;
6873
+ this.path = path;
6874
+ }
6875
+ code;
6876
+ path;
6877
+ name = "ScanError";
6878
+ };
6879
+ function ensureDirectory(path) {
6880
+ let isDirectory;
6881
+ try {
6882
+ isDirectory = statSync2(path).isDirectory();
6883
+ } catch (e) {
6884
+ const code = errorCode2(e);
6885
+ const why = code === "ENOENT" ? "no such file or directory" : code;
6886
+ throw new ScanError("path_not_found", path, `${path} is not a directory (${why})`);
6887
+ }
6888
+ if (!isDirectory) throw new ScanError("path_not_found", path, `${path} is not a directory`);
6889
+ }
6890
+ function summarize(model, rules, publicTables2 = []) {
5885
6891
  const apiTables = model.tables.filter((t) => !t.table.includes("."));
5886
6892
  return {
5887
6893
  root: model.root,
@@ -5891,10 +6897,12 @@ function summarize(model, rules) {
5891
6897
  tablesKnown: apiTables.length,
5892
6898
  tablesWithRls: apiTables.filter((t) => t.rlsEnabled).length,
5893
6899
  rules,
5894
- warnings: model.warnings
6900
+ warnings: model.warnings,
6901
+ publicTables: [...publicTables2]
5895
6902
  };
5896
6903
  }
5897
6904
  function runScan(path, opts = {}) {
6905
+ ensureDirectory(path);
5898
6906
  const cfg = loadAuditConfig(path);
5899
6907
  const repoDirs = repoMigrationDirs(path, cfg.config.migrations ?? []);
5900
6908
  const sqlDirs = [...repoDirs.dirs, ...opts.sqlDirs ?? []];
@@ -5904,10 +6912,11 @@ function runScan(path, opts = {}) {
5904
6912
  ...ignore.globs.length > 0 ? { ignore: ignore.globs } : {}
5905
6913
  });
5906
6914
  const graph = buildGraph(model);
5907
- const runOpts = opts.now === void 0 ? {} : { now: opts.now };
6915
+ const publicTables2 = cfg.config.publicTables ?? [];
6916
+ const runOpts = { ...opts.now === void 0 ? {} : { now: opts.now }, publicTables: publicTables2 };
5908
6917
  const findings = runRules(defaultRules, model, graph, runOpts);
5909
6918
  const coverage = summarizeCoverage(findings);
5910
- const summary = summarize(model, defaultRules.length);
6919
+ const summary = summarize(model, defaultRules.length, publicTables2);
5911
6920
  return {
5912
6921
  summary: {
5913
6922
  ...summary,
@@ -5941,6 +6950,8 @@ Options:
5941
6950
  --migrations <dir> extra directory with Supabase migration SQL (repeatable)
5942
6951
  -h, --help show this help
5943
6952
 
6953
+ Exit code: 0 clean, 1 a finding reached --fail-on, 2 usage error or <path> is not a directory
6954
+
5944
6955
  Config: <path>/audit.config.json { "ignore": ["evals/**"], "migrations": ["supabase/migrations"] }
5945
6956
  Suppress a finding: // auditai:ignore <ruleId|*> -- reason (above the handler or at the top of a file)
5946
6957
  `;
@@ -5976,10 +6987,15 @@ function main(argv) {
5976
6987
  return 2;
5977
6988
  }
5978
6989
  const migrations = values.migrations.map((m) => resolve4(m));
5979
- const result = runScan(
5980
- positionals[0] ?? ".",
5981
- migrations.length > 0 ? { sqlDirs: migrations } : {}
5982
- );
6990
+ let result;
6991
+ try {
6992
+ result = runScan(positionals[0] ?? ".", migrations.length > 0 ? { sqlDirs: migrations } : {});
6993
+ } catch (e) {
6994
+ if (!(e instanceof ScanError)) throw e;
6995
+ process.stderr.write(`error: ${e.message}
6996
+ `);
6997
+ return 2;
6998
+ }
5983
6999
  process.stdout.write(
5984
7000
  values.json ? `${JSON.stringify(result, null, 2)}
5985
7001
  ` : formatScanText(result)