auditai-scan 0.4.0 → 0.5.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 +1132 -386
  2. package/package.json +1 -1
@@ -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);
@@ -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
 
@@ -3052,48 +3309,148 @@ function parseSqlForRls(rel, text, into) {
3052
3309
  if (!isAppliedSqlFile(rel)) return;
3053
3310
  const state = schemaStateFor(into);
3054
3311
  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
- });
3312
+ applyStatement(state, stmt, rel);
3313
+ if (!isWord(stmt.tokens[0], "do")) continue;
3314
+ const expanded = expandDoBlock(stmt);
3315
+ for (const s of expanded.statements) applyStatement(state, s, rel);
3316
+ if (expanded.dynamic) warnDynamicSql(state, rel);
3317
+ }
3318
+ }
3319
+ function warnDynamicSql(state, rel) {
3320
+ 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`;
3321
+ if (!state.warnings.includes(w)) state.warnings.push(w);
3322
+ }
3323
+ function applyStatement(state, stmt, rel) {
3324
+ const cp = CREATE_POLICY.exec(stmt.text);
3325
+ if (!cp?.[1] || !cp[4]) {
3326
+ applySchemaStatement(state, stmt, rel);
3327
+ return;
3089
3328
  }
3329
+ const t = ensureTable(
3330
+ state,
3331
+ qualifiedKey({ schema: cp[3] ?? null, name: cp[4] }),
3332
+ rel,
3333
+ stmt.line
3334
+ );
3335
+ const name = cp[2] ?? cp[1];
3336
+ const rest = stmt.text.slice(cp[0].length);
3337
+ const cmdMatch = /\bfor\s+(select|insert|update|delete|all)\b/i.exec(rest);
3338
+ const command = cmdMatch?.[1]?.toLowerCase() ?? "all";
3339
+ const rolesMatch = /\bto\s+([A-Za-z_][A-Za-z0-9_]*(?:\s*,\s*[A-Za-z_][A-Za-z0-9_]*)*)/i.exec(
3340
+ rest
3341
+ );
3342
+ const roles = rolesMatch?.[1] ? rolesMatch[1].split(/\s*,\s*/).map((r) => r.toLowerCase()) : [];
3343
+ let using = null;
3344
+ let check = null;
3345
+ const u = /\busing\s*\(/i.exec(rest);
3346
+ if (u) using = balanced(rest, u.index + u[0].length - 1)?.inner.trim() ?? null;
3347
+ const c = /\bwith\s+check\s*\(/i.exec(rest);
3348
+ if (c) check = balanced(rest, c.index + c[0].length - 1)?.inner.trim() ?? null;
3349
+ t.policies.push(name);
3350
+ t.policyDetails.push({
3351
+ name,
3352
+ command,
3353
+ roles,
3354
+ using,
3355
+ check,
3356
+ location: { file: rel, line: stmt.line }
3357
+ });
3090
3358
  }
3091
3359
  function sqlSchemaFor(into) {
3092
3360
  return finishSchema(schemaStateFor(into));
3093
3361
  }
3094
3362
 
3095
- // packages/parser/src/storage.ts
3363
+ // packages/parser/src/role-gates.ts
3096
3364
  import ts7 from "typescript";
3365
+ var ROLE_PROPERTY = /^(role|roles|app_metadata|is_?admin|is_?superuser|permissions?|claims?)$/i;
3366
+ var EMAIL_PROPERTY = /^email$/i;
3367
+ function propertyPath(e) {
3368
+ const u = unwrap(e);
3369
+ if (ts7.isIdentifier(u)) return [u.text];
3370
+ if (ts7.isPropertyAccessExpression(u)) {
3371
+ const base = propertyPath(u.expression);
3372
+ return base === null ? null : [...base, u.name.text];
3373
+ }
3374
+ return null;
3375
+ }
3376
+ function isLiteral(e) {
3377
+ const u = unwrap(e);
3378
+ 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;
3379
+ }
3380
+ function sessionClaim(e, sessionNames) {
3381
+ const path = propertyPath(e);
3382
+ const root = path?.[0];
3383
+ const last = path?.[path.length - 1];
3384
+ if (!path || path.length < 2 || root === void 0 || last === void 0) return null;
3385
+ if (!sessionNames.has(root)) return null;
3386
+ if (path.some((p) => p === "user_metadata")) return null;
3387
+ if (path.some((p) => ROLE_PROPERTY.test(p))) return { source: path.join("."), kind: "role" };
3388
+ if (EMAIL_PROPERTY.test(last)) return { source: path.join("."), kind: "email" };
3389
+ return null;
3390
+ }
3391
+ function claimIn(cond, sessionNames) {
3392
+ let found = null;
3393
+ const visit = (n) => {
3394
+ if (found !== null) return;
3395
+ if (ts7.isCallExpression(n)) {
3396
+ for (const a of n.arguments) {
3397
+ const c = sessionClaim(a, sessionNames);
3398
+ if (c) {
3399
+ found = c.source;
3400
+ return;
3401
+ }
3402
+ }
3403
+ } else if (ts7.isBinaryExpression(n)) {
3404
+ for (const [a, b] of [
3405
+ [n.left, n.right],
3406
+ [n.right, n.left]
3407
+ ]) {
3408
+ const c = sessionClaim(a, sessionNames);
3409
+ if (!c) continue;
3410
+ if (c.kind === "role" || isLiteral(b) || envNamesIn(b).length > 0) {
3411
+ found = c.source;
3412
+ return;
3413
+ }
3414
+ }
3415
+ } else if (ts7.isPrefixUnaryExpression(n) || ts7.isPropertyAccessExpression(n)) {
3416
+ const target = ts7.isPrefixUnaryExpression(n) ? n.operand : n;
3417
+ const c = sessionClaim(target, sessionNames);
3418
+ if (c?.kind === "role" && !ts7.isCallExpression(n.parent)) {
3419
+ found = c.source;
3420
+ return;
3421
+ }
3422
+ }
3423
+ n.forEachChild(visit);
3424
+ };
3425
+ visit(cond);
3426
+ return found;
3427
+ }
3428
+ function sessionNamesIn(body, isSessionCall) {
3429
+ const out = /* @__PURE__ */ new Set();
3430
+ walkOwn(body, (n) => {
3431
+ if (!ts7.isVariableDeclaration(n) || !n.initializer) return;
3432
+ const init = unwrap(n.initializer);
3433
+ if (ts7.isCallExpression(init) && isSessionCall(init)) {
3434
+ for (const nm of boundNames(n.name)) out.add(nm);
3435
+ }
3436
+ });
3437
+ return out;
3438
+ }
3439
+ function roleGatesIn(body, sessionNames) {
3440
+ const out = [];
3441
+ if (sessionNames.size === 0) return out;
3442
+ walkOwn(body, (n) => {
3443
+ if (!ts7.isIfStatement(n)) return;
3444
+ const exit = exitKind(n.thenStatement);
3445
+ if (!exit) return;
3446
+ const source = claimIn(n.expression, sessionNames);
3447
+ if (source !== null) out.push({ node: n, source, exit });
3448
+ });
3449
+ return out;
3450
+ }
3451
+
3452
+ // packages/parser/src/storage.ts
3453
+ import ts8 from "typescript";
3097
3454
  var STORAGE_OPS = {
3098
3455
  download: "select",
3099
3456
  list: "select",
@@ -3109,14 +3466,14 @@ var STORAGE_OPS = {
3109
3466
  var TWO_PATHS = /* @__PURE__ */ new Set(["move", "copy"]);
3110
3467
  function isStorageRoot(e) {
3111
3468
  const u = unwrap(e);
3112
- return ts7.isPropertyAccessExpression(u) && u.name.text === "storage";
3469
+ return ts8.isPropertyAccessExpression(u) && u.name.text === "storage";
3113
3470
  }
3114
3471
  function storageBindingsIn(body) {
3115
3472
  const out = /* @__PURE__ */ new Map();
3116
- for (const decl of collect(body, ts7.isVariableDeclaration)) {
3117
- if (!decl.initializer || !ts7.isIdentifier(decl.name)) continue;
3473
+ for (const decl of collect(body, ts8.isVariableDeclaration)) {
3474
+ if (!decl.initializer || !ts8.isIdentifier(decl.name)) continue;
3118
3475
  const init = unwrap(decl.initializer);
3119
- if (!ts7.isCallExpression(init) || !ts7.isPropertyAccessExpression(init.expression)) continue;
3476
+ if (!ts8.isCallExpression(init) || !ts8.isPropertyAccessExpression(init.expression)) continue;
3120
3477
  const callee = init.expression;
3121
3478
  if (callee.name.text !== "from" || !isStorageRoot(callee.expression)) continue;
3122
3479
  const root = unwrap(callee.expression);
@@ -3132,7 +3489,7 @@ function storageCallOf(chain, bound) {
3132
3489
  const r = unwrap(root);
3133
3490
  return { clientRoot: r.expression, bucketArg: first.args[0], op: known(second) };
3134
3491
  }
3135
- if (ts7.isIdentifier(root) && first) {
3492
+ if (ts8.isIdentifier(root) && first) {
3136
3493
  const b = bound.get(root.text);
3137
3494
  if (b) return { clientRoot: b.clientRoot, bucketArg: b.bucketArg, op: known(first) };
3138
3495
  }
@@ -3143,9 +3500,9 @@ function bucketName(arg, sf) {
3143
3500
  const u = unwrap(arg);
3144
3501
  const lit = stringLiteralValue(u);
3145
3502
  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;
3503
+ if (!ts8.isIdentifier(u)) return null;
3504
+ for (const d of collect(sf, ts8.isVariableDeclaration)) {
3505
+ if (!ts8.isIdentifier(d.name) || d.name.text !== u.text || !d.initializer) continue;
3149
3506
  const v = stringLiteralValue(unwrap(d.initializer));
3150
3507
  if (v !== null) return v;
3151
3508
  }
@@ -3158,20 +3515,20 @@ var INPUT_OBJECT = /^(params|searchParams|body|query|headers|cookies|formData)$/
3158
3515
  var CallerScope = class {
3159
3516
  constructor(body, inputNames) {
3160
3517
  this.inputNames = inputNames;
3161
- for (const decl of collect(body, ts7.isVariableDeclaration)) {
3162
- if (!decl.initializer || !ts7.isIdentifier(decl.name)) continue;
3518
+ for (const decl of collect(body, ts8.isVariableDeclaration)) {
3519
+ if (!decl.initializer || !ts8.isIdentifier(decl.name)) continue;
3163
3520
  if (this.refersToCaller(decl.initializer)) this.scopedVars.add(decl.name.text);
3164
3521
  }
3165
- for (const call of collect(body, ts7.isCallExpression)) {
3522
+ for (const call of collect(body, ts8.isCallExpression)) {
3166
3523
  const callee = call.expression;
3167
- if (!ts7.isPropertyAccessExpression(callee) || callee.name.text !== "startsWith") continue;
3524
+ if (!ts8.isPropertyAccessExpression(callee) || callee.name.text !== "startsWith") continue;
3168
3525
  const target = unwrap(callee.expression);
3169
3526
  const arg = call.arguments[0];
3170
- if (ts7.isIdentifier(target) && arg && this.refersToCaller(arg)) this.guarded.add(target.text);
3527
+ if (ts8.isIdentifier(target) && arg && this.refersToCaller(arg)) this.guarded.add(target.text);
3171
3528
  }
3172
- for (const bin of collect(body, ts7.isBinaryExpression)) {
3529
+ for (const bin of collect(body, ts8.isBinaryExpression)) {
3173
3530
  const k = bin.operatorToken.kind;
3174
- if (k !== ts7.SyntaxKind.EqualsEqualsEqualsToken && k !== ts7.SyntaxKind.ExclamationEqualsEqualsToken && k !== ts7.SyntaxKind.EqualsEqualsToken && k !== ts7.SyntaxKind.ExclamationEqualsToken) {
3531
+ if (k !== ts8.SyntaxKind.EqualsEqualsEqualsToken && k !== ts8.SyntaxKind.ExclamationEqualsEqualsToken && k !== ts8.SyntaxKind.EqualsEqualsToken && k !== ts8.SyntaxKind.ExclamationEqualsToken) {
3175
3532
  continue;
3176
3533
  }
3177
3534
  for (const [side, other] of [
@@ -3191,13 +3548,13 @@ var CallerScope = class {
3191
3548
  let hit = false;
3192
3549
  walk(e, (n) => {
3193
3550
  if (hit) return false;
3194
- if (ts7.isPropertyAccessExpression(n) && this.isIdentityAccess(n)) {
3551
+ if (ts8.isPropertyAccessExpression(n) && this.isIdentityAccess(n)) {
3195
3552
  hit = true;
3196
3553
  return false;
3197
3554
  }
3198
- if (ts7.isIdentifier(n)) {
3555
+ if (ts8.isIdentifier(n)) {
3199
3556
  const parent = n.parent;
3200
- if (parent && ts7.isPropertyAccessExpression(parent) && parent.name === n) return void 0;
3557
+ if (parent && ts8.isPropertyAccessExpression(parent) && parent.name === n) return void 0;
3201
3558
  if (this.scopedVars.has(n.text)) hit = true;
3202
3559
  else if (IDENTITY_VAR.test(n.text) && !this.inputNames.has(n.text)) hit = true;
3203
3560
  }
@@ -3208,17 +3565,17 @@ var CallerScope = class {
3208
3565
  /** A path argument is covered when it embeds the caller's id or is an identifier checked against it. */
3209
3566
  covers(e) {
3210
3567
  const u = unwrap(e);
3211
- return this.refersToCaller(u) || ts7.isIdentifier(u) && this.guarded.has(u.text);
3568
+ return this.refersToCaller(u) || ts8.isIdentifier(u) && this.guarded.has(u.text);
3212
3569
  }
3213
3570
  isIdentityAccess(pa) {
3214
3571
  const field = pa.name.text;
3215
3572
  const names = [];
3216
3573
  let base = unwrap(pa.expression);
3217
- while (ts7.isPropertyAccessExpression(base)) {
3574
+ while (ts8.isPropertyAccessExpression(base)) {
3218
3575
  names.push(base.name.text);
3219
3576
  base = unwrap(base.expression);
3220
3577
  }
3221
- if (!ts7.isIdentifier(base) || this.inputNames.has(base.text)) return false;
3578
+ if (!ts8.isIdentifier(base) || this.inputNames.has(base.text)) return false;
3222
3579
  names.push(base.text);
3223
3580
  if (names.some((n) => INPUT_OBJECT.test(n))) return false;
3224
3581
  if (OWNER_FIELD.test(field)) return true;
@@ -3227,20 +3584,20 @@ var CallerScope = class {
3227
3584
  };
3228
3585
  function splitHead(e) {
3229
3586
  const u = unwrap(e);
3230
- if (!ts7.isElementAccessExpression(u)) return null;
3587
+ if (!ts8.isElementAccessExpression(u)) return null;
3231
3588
  const idx = u.argumentExpression;
3232
- if (!ts7.isNumericLiteral(idx) || idx.text !== "0") return null;
3589
+ if (!ts8.isNumericLiteral(idx) || idx.text !== "0") return null;
3233
3590
  const call = unwrap(u.expression);
3234
- if (!ts7.isCallExpression(call) || !ts7.isPropertyAccessExpression(call.expression)) return null;
3591
+ if (!ts8.isCallExpression(call) || !ts8.isPropertyAccessExpression(call.expression)) return null;
3235
3592
  if (call.expression.name.text !== "split") return null;
3236
3593
  const target = unwrap(call.expression.expression);
3237
- return ts7.isIdentifier(target) ? target.text : null;
3594
+ return ts8.isIdentifier(target) ? target.text : null;
3238
3595
  }
3239
3596
  function storageAccessOf(op, bucket, sf, scope, derived) {
3240
3597
  const pathArgs = op.args.slice(0, TWO_PATHS.has(op.name) ? 2 : 1);
3241
3598
  const parts = pathArgs.flatMap((a) => {
3242
3599
  const u = unwrap(a);
3243
- return ts7.isArrayLiteralExpression(u) ? [...u.elements] : [a];
3600
+ return ts8.isArrayLiteralExpression(u) ? [...u.elements] : [a];
3244
3601
  });
3245
3602
  const tainted = parts.filter((p) => derived(p));
3246
3603
  const judged = tainted.length > 0 ? tainted : parts;
@@ -3254,22 +3611,22 @@ function storageAccessOf(op, bucket, sf, scope, derived) {
3254
3611
  }
3255
3612
 
3256
3613
  // packages/parser/src/supabase.ts
3257
- import ts8 from "typescript";
3614
+ import ts9 from "typescript";
3258
3615
  var CREATE_CLIENT_CALLEES = /^(createClient|createServerClient|createBrowserClient)$/;
3259
3616
  function isCreateClientCall(call, sf) {
3260
3617
  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);
3618
+ if (ts9.isIdentifier(callee)) return CREATE_CLIENT_CALLEES.test(callee.text);
3619
+ if (ts9.isPropertyAccessExpression(callee)) return CREATE_CLIENT_CALLEES.test(callee.name.text);
3263
3620
  return CREATE_CLIENT_CALLEES.test(callee.getText(sf));
3264
3621
  }
3265
3622
  function resolveArgText(expr, sf) {
3266
3623
  const u = unwrap(expr);
3267
- if (!ts8.isIdentifier(u)) return expr.getText(sf);
3624
+ if (!ts9.isIdentifier(u)) return expr.getText(sf);
3268
3625
  let scope = expr.parent;
3269
3626
  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
3627
+ if (ts9.isFunctionLike(scope) || ts9.isBlock(scope) || ts9.isSourceFile(scope)) {
3628
+ const decl = collect(scope, ts9.isVariableDeclaration).find(
3629
+ (d) => ts9.isIdentifier(d.name) && d.name.text === u.text && d.initializer
3273
3630
  );
3274
3631
  if (decl?.initializer) return `${u.text} = ${decl.initializer.getText(sf)}`;
3275
3632
  }
@@ -3311,7 +3668,7 @@ function classifyCreateClientCall(call, sf) {
3311
3668
  }
3312
3669
  var AUTH_CALL = /\.auth\.(getUser|getSession|getClaims)\s*\(/;
3313
3670
  function hasModifier(node, kind) {
3314
- const mods = ts8.canHaveModifiers(node) ? ts8.getModifiers(node) : void 0;
3671
+ const mods = ts9.canHaveModifiers(node) ? ts9.getModifiers(node) : void 0;
3315
3672
  return mods?.some((m) => m.kind === kind) ?? false;
3316
3673
  }
3317
3674
  function analyzeModule(rel, sf) {
@@ -3320,33 +3677,33 @@ function analyzeModule(rel, sf) {
3320
3677
  const exportedNames = /* @__PURE__ */ new Set();
3321
3678
  let defaultExport = null;
3322
3679
  for (const stmt of sf.statements) {
3323
- if (ts8.isImportDeclaration(stmt) && ts8.isStringLiteral(stmt.moduleSpecifier)) {
3680
+ if (ts9.isImportDeclaration(stmt) && ts9.isStringLiteral(stmt.moduleSpecifier)) {
3324
3681
  const spec = stmt.moduleSpecifier.text;
3325
3682
  const clause = stmt.importClause;
3326
3683
  if (!clause) continue;
3327
3684
  if (clause.name) imports.set(clause.name.text, { spec, imported: "default" });
3328
3685
  const nb = clause.namedBindings;
3329
- if (nb && ts8.isNamedImports(nb)) {
3686
+ if (nb && ts9.isNamedImports(nb)) {
3330
3687
  for (const el of nb.elements) {
3331
3688
  imports.set(el.name.text, { spec, imported: (el.propertyName ?? el.name).text });
3332
3689
  }
3333
3690
  }
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;
3691
+ if (nb && ts9.isNamespaceImport(nb)) imports.set(nb.name.text, { spec, imported: "*" });
3692
+ } else if (ts9.isExportDeclaration(stmt)) {
3693
+ const spec = stmt.moduleSpecifier && ts9.isStringLiteral(stmt.moduleSpecifier) ? stmt.moduleSpecifier.text : null;
3337
3694
  const clause = stmt.exportClause;
3338
3695
  if (spec && !clause) reexports.push({ star: true, spec });
3339
- else if (clause && ts8.isNamedExports(clause)) {
3696
+ else if (clause && ts9.isNamedExports(clause)) {
3340
3697
  for (const el of clause.elements) {
3341
3698
  const name = (el.propertyName ?? el.name).text;
3342
3699
  if (spec) reexports.push({ star: false, name, alias: el.name.text, spec });
3343
3700
  else exportedNames.add(name);
3344
3701
  }
3345
3702
  }
3346
- } else if (ts8.isExportAssignment(stmt) && !stmt.isExportEquals) {
3703
+ } else if (ts9.isExportAssignment(stmt) && !stmt.isExportEquals) {
3347
3704
  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)) {
3705
+ if (ts9.isIdentifier(e)) defaultExport = e.text;
3706
+ } else if (ts9.isFunctionDeclaration(stmt) && stmt.name && hasModifier(stmt, ts9.SyntaxKind.DefaultKeyword)) {
3350
3707
  defaultExport = stmt.name.text;
3351
3708
  }
3352
3709
  }
@@ -3361,15 +3718,15 @@ function analyzeModule(rel, sf) {
3361
3718
  const moduleVars = /* @__PURE__ */ new Map();
3362
3719
  const drizzleTables = /* @__PURE__ */ new Map();
3363
3720
  for (const stmt of sf.statements) {
3364
- if (!ts8.isVariableStatement(stmt)) continue;
3365
- const exported = hasModifier(stmt, ts8.SyntaxKind.ExportKeyword);
3721
+ if (!ts9.isVariableStatement(stmt)) continue;
3722
+ const exported = hasModifier(stmt, ts9.SyntaxKind.ExportKeyword);
3366
3723
  for (const d of stmt.declarationList.declarations) {
3367
- if (!ts8.isIdentifier(d.name) || !d.initializer || functions.has(d.name.text)) continue;
3724
+ if (!ts9.isIdentifier(d.name) || !d.initializer || functions.has(d.name.text)) continue;
3368
3725
  const init = clientCreatingOperand(d.initializer);
3369
- if (!ts8.isCallExpression(init) && !ts8.isNewExpression(init)) continue;
3370
- if (ts8.isCallExpression(init)) {
3726
+ if (!ts9.isCallExpression(init) && !ts9.isNewExpression(init)) continue;
3727
+ if (ts9.isCallExpression(init)) {
3371
3728
  const callee = init.expression;
3372
- const fn = ts8.isIdentifier(callee) ? callee.text : ts8.isPropertyAccessExpression(callee) ? callee.name.text : "";
3729
+ const fn = ts9.isIdentifier(callee) ? callee.text : ts9.isPropertyAccessExpression(callee) ? callee.name.text : "";
3373
3730
  const tableName = stringLiteralValue(init.arguments[0]);
3374
3731
  if ((DRIZZLE_TABLE_FNS.has(fn) || fn === "table") && tableName !== null) {
3375
3732
  drizzleTables.set(d.name.text, tableName);
@@ -3392,8 +3749,8 @@ function analyzeModule(rel, sf) {
3392
3749
  );
3393
3750
  if (nextAuthLocal.size > 0) {
3394
3751
  for (const stmt of sf.statements) {
3395
- if (!ts8.isVariableStatement(stmt)) continue;
3396
- const exported = hasModifier(stmt, ts8.SyntaxKind.ExportKeyword);
3752
+ if (!ts9.isVariableStatement(stmt)) continue;
3753
+ const exported = hasModifier(stmt, ts9.SyntaxKind.ExportKeyword);
3397
3754
  for (const name of nextAuthSessionNames(stmt, nextAuthLocal)) {
3398
3755
  const helper = {
3399
3756
  name,
@@ -3416,7 +3773,7 @@ function analyzeModule(rel, sf) {
3416
3773
  });
3417
3774
  continue;
3418
3775
  }
3419
- const creates = collect(f.fn, ts8.isCallExpression).filter((c) => isCreateClientCall(c, sf));
3776
+ const creates = collect(f.fn, ts9.isCallExpression).filter((c) => isCreateClientCall(c, sf));
3420
3777
  const first = creates[0];
3421
3778
  if (first) {
3422
3779
  const { kind, evidence } = classifyCreateClientCall(first, sf);
@@ -3439,7 +3796,7 @@ function analyzeModule(rel, sf) {
3439
3796
  }
3440
3797
 
3441
3798
  // packages/parser/src/whole-input.ts
3442
- import ts9 from "typescript";
3799
+ import ts10 from "typescript";
3443
3800
  var ELEMENT_PRESERVING = /* @__PURE__ */ new Set([
3444
3801
  "filter",
3445
3802
  "slice",
@@ -3455,35 +3812,35 @@ var ELEMENT_PRESERVING = /* @__PURE__ */ new Set([
3455
3812
  ]);
3456
3813
  var RESHAPING = /* @__PURE__ */ new Set(["map", "flatMap"]);
3457
3814
  var LOGICAL = /* @__PURE__ */ new Set([
3458
- ts9.SyntaxKind.QuestionQuestionToken,
3459
- ts9.SyntaxKind.BarBarToken,
3460
- ts9.SyntaxKind.AmpersandAmpersandToken
3815
+ ts10.SyntaxKind.QuestionQuestionToken,
3816
+ ts10.SyntaxKind.BarBarToken,
3817
+ ts10.SyntaxKind.AmpersandAmpersandToken
3461
3818
  ]);
3462
3819
  function isWholeInput(e, cx) {
3463
3820
  const u = unwrap(e);
3464
- if (ts9.isIdentifier(u)) return cx.wholeName(u.text);
3465
- if (ts9.isPropertyAccessExpression(u) || ts9.isElementAccessExpression(u)) {
3821
+ if (ts10.isIdentifier(u)) return cx.wholeName(u.text);
3822
+ if (ts10.isPropertyAccessExpression(u) || ts10.isElementAccessExpression(u)) {
3466
3823
  return isWholeInput(u.expression, cx);
3467
3824
  }
3468
- if (ts9.isObjectLiteralExpression(u)) {
3469
- return u.properties.some((pr) => ts9.isSpreadAssignment(pr) && isWholeInput(pr.expression, cx));
3825
+ if (ts10.isObjectLiteralExpression(u)) {
3826
+ return u.properties.some((pr) => ts10.isSpreadAssignment(pr) && isWholeInput(pr.expression, cx));
3470
3827
  }
3471
- if (ts9.isArrayLiteralExpression(u)) {
3472
- return u.elements.some((el) => isWholeInput(ts9.isSpreadElement(el) ? el.expression : el, cx));
3828
+ if (ts10.isArrayLiteralExpression(u)) {
3829
+ return u.elements.some((el) => isWholeInput(ts10.isSpreadElement(el) ? el.expression : el, cx));
3473
3830
  }
3474
- if (ts9.isConditionalExpression(u)) {
3831
+ if (ts10.isConditionalExpression(u)) {
3475
3832
  return isWholeInput(u.whenTrue, cx) || isWholeInput(u.whenFalse, cx);
3476
3833
  }
3477
- if (ts9.isBinaryExpression(u) && LOGICAL.has(u.operatorToken.kind)) {
3834
+ if (ts10.isBinaryExpression(u) && LOGICAL.has(u.operatorToken.kind)) {
3478
3835
  return isWholeInput(u.left, cx) || isWholeInput(u.right, cx);
3479
3836
  }
3480
- if (ts9.isCallExpression(u)) return isWholeCall(u, cx);
3837
+ if (ts10.isCallExpression(u)) return isWholeCall(u, cx);
3481
3838
  return false;
3482
3839
  }
3483
3840
  function isWholeCall(call, cx) {
3484
3841
  if (cx.requestBody(call)) return true;
3485
3842
  const callee = call.expression;
3486
- if (ts9.isPropertyAccessExpression(callee)) {
3843
+ if (ts10.isPropertyAccessExpression(callee)) {
3487
3844
  const method = callee.name.text;
3488
3845
  const receiverWhole = isWholeInput(callee.expression, cx);
3489
3846
  if (RESHAPING.has(method)) return callbackKeepsWhole(call.arguments[0], receiverWhole, cx);
@@ -3491,13 +3848,13 @@ function isWholeCall(call, cx) {
3491
3848
  }
3492
3849
  return call.arguments.some((a) => {
3493
3850
  const ua = unwrap(a);
3494
- return isWholeInput(a, cx) || ts9.isIdentifier(ua) && cx.requestName(ua.text);
3851
+ return isWholeInput(a, cx) || ts10.isIdentifier(ua) && cx.requestName(ua.text);
3495
3852
  });
3496
3853
  }
3497
3854
  function callbackKeepsWhole(cb, receiverWhole, cx) {
3498
3855
  if (!cb) return false;
3499
3856
  const f = unwrap(cb);
3500
- if (!ts9.isArrowFunction(f) && !ts9.isFunctionExpression(f)) return receiverWhole;
3857
+ if (!ts10.isArrowFunction(f) && !ts10.isFunctionExpression(f)) return receiverWhole;
3501
3858
  const element = f.parameters[0];
3502
3859
  const elementNames = new Set(receiverWhole && element ? boundNames(element.name) : []);
3503
3860
  const inner = {
@@ -3508,19 +3865,19 @@ function callbackKeepsWhole(cb, receiverWhole, cx) {
3508
3865
  return ownReturns(f).some((r) => isWholeInput(r, inner));
3509
3866
  }
3510
3867
  function propertyKey(name) {
3511
- if (ts9.isIdentifier(name) || ts9.isStringLiteral(name)) return name.text;
3868
+ if (ts10.isIdentifier(name) || ts10.isStringLiteral(name)) return name.text;
3512
3869
  return null;
3513
3870
  }
3514
3871
  function wholeParamNames(param, arg, cx) {
3515
3872
  if (!arg) return [];
3516
3873
  const u = unwrap(arg);
3517
- if (ts9.isObjectBindingPattern(param) && ts9.isObjectLiteralExpression(u) && !u.properties.some(ts9.isSpreadAssignment)) {
3874
+ if (ts10.isObjectBindingPattern(param) && ts10.isObjectLiteralExpression(u) && !u.properties.some(ts10.isSpreadAssignment)) {
3518
3875
  const out = [];
3519
3876
  for (const el of param.elements) {
3520
3877
  if (el.dotDotDotToken) continue;
3521
3878
  const key = propertyKey(el.propertyName ?? el.name);
3522
3879
  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;
3880
+ const value = prop && ts10.isPropertyAssignment(prop) ? prop.initializer : prop && ts10.isShorthandPropertyAssignment(prop) ? prop.name : void 0;
3524
3881
  if (value && isWholeInput(value, cx)) out.push(...boundNames(el.name));
3525
3882
  }
3526
3883
  return out;
@@ -3654,8 +4011,8 @@ function scopeOf(p, facts) {
3654
4011
  return scope;
3655
4012
  }
3656
4013
  function symOfCallee(p, callee, scope) {
3657
- if (ts10.isIdentifier(callee)) return scope.get(callee.text);
3658
- if (ts10.isPropertyAccessExpression(callee) && ts10.isIdentifier(callee.expression)) {
4014
+ if (ts11.isIdentifier(callee)) return scope.get(callee.text);
4015
+ if (ts11.isPropertyAccessExpression(callee) && ts11.isIdentifier(callee.expression)) {
3659
4016
  const ns = scope.get(callee.expression.text);
3660
4017
  if (ns?.kind === "namespace") return exportedSym(p, ns.facts, callee.name.text, 0) ?? void 0;
3661
4018
  }
@@ -3663,8 +4020,8 @@ function symOfCallee(p, callee, scope) {
3663
4020
  }
3664
4021
  function returnedExpressions(fn) {
3665
4022
  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);
4023
+ if (!ts11.isBlock(fn.body)) return [fn.body];
4024
+ return collect(fn.body, ts11.isReturnStatement).map((r) => r.expression).filter((e) => e !== void 0);
3668
4025
  }
3669
4026
  function factoryOfFunction(p, sym, depth) {
3670
4027
  const key = `${sym.facts.file}#${sym.name}`;
@@ -3676,7 +4033,7 @@ function factoryOfFunction(p, sym, depth) {
3676
4033
  let found = null;
3677
4034
  for (const ret of returnedExpressions(sym.fn)) {
3678
4035
  const u = unwrap(ret);
3679
- if (!ts10.isCallExpression(u)) continue;
4036
+ if (!ts11.isCallExpression(u)) continue;
3680
4037
  found = classifyCall(p, u, sf, scope, null, depth + 1);
3681
4038
  if (found) break;
3682
4039
  }
@@ -3713,7 +4070,7 @@ function classifyCall(p, call, sf, scope, frame, depth) {
3713
4070
  }
3714
4071
  function prismaExtensionBase(p, call, sf, scope, frame, depth) {
3715
4072
  const callee = call.expression;
3716
- if (!ts10.isPropertyAccessExpression(callee) || callee.name.text !== "$extends") return null;
4073
+ if (!ts11.isPropertyAccessExpression(callee) || callee.name.text !== "$extends") return null;
3717
4074
  if (depth > 5) return null;
3718
4075
  const base = unwrap(callee.expression);
3719
4076
  if (isPrismaNew(base)) {
@@ -3723,8 +4080,8 @@ function prismaExtensionBase(p, call, sf, scope, frame, depth) {
3723
4080
  location: { file: sf.fileName, line: lineOf(sf, call) }
3724
4081
  };
3725
4082
  }
3726
- if (ts10.isCallExpression(base)) return classifyCall(p, base, sf, scope, frame, depth + 1);
3727
- if (!ts10.isIdentifier(base)) return null;
4083
+ if (ts11.isCallExpression(base)) return classifyCall(p, base, sf, scope, frame, depth + 1);
4084
+ if (!ts11.isIdentifier(base)) return null;
3728
4085
  const bound = frame?.clients.get(base.text);
3729
4086
  if (bound) return bound;
3730
4087
  const sym = scope.get(base.text);
@@ -3734,15 +4091,15 @@ function instanceOfCall(p, call, frame, scope) {
3734
4091
  const sym = symOfCallee(p, call.expression, scope);
3735
4092
  if (sym?.kind !== "function") return null;
3736
4093
  const fscope = scopeOf(p, sym.facts);
3737
- const params = sym.fn.parameters.map((pp) => ts10.isIdentifier(pp.name) ? pp.name.text : null);
4094
+ const params = sym.fn.parameters.map((pp) => ts11.isIdentifier(pp.name) ? pp.name.text : null);
3738
4095
  for (const ret of returnedExpressions(sym.fn)) {
3739
4096
  const u = unwrap(ret);
3740
- if (!ts10.isNewExpression(u) || !ts10.isIdentifier(u.expression)) continue;
4097
+ if (!ts11.isNewExpression(u) || !ts11.isIdentifier(u.expression)) continue;
3741
4098
  const cs = fscope.get(u.expression.text);
3742
4099
  if (cs?.kind !== "class") continue;
3743
4100
  const ctorArgs = (u.arguments ?? []).map((a) => {
3744
4101
  const ua = unwrap(a);
3745
- if (ts10.isIdentifier(ua)) {
4102
+ if (ts11.isIdentifier(ua)) {
3746
4103
  const j = params.indexOf(ua.text);
3747
4104
  if (j >= 0 && frame) return argBinding(p, call.arguments[j], frame);
3748
4105
  }
@@ -3774,7 +4131,7 @@ function varBinding(p, sym) {
3774
4131
  whole: false,
3775
4132
  isRequest: false
3776
4133
  };
3777
- } else if (ts10.isCallExpression(init)) {
4134
+ } else if (ts11.isCallExpression(init)) {
3778
4135
  const found = classifyCall(p, init, sf, scope, null, 0);
3779
4136
  const client = found ? {
3780
4137
  kind: found.kind,
@@ -3783,7 +4140,7 @@ function varBinding(p, sym) {
3783
4140
  } : null;
3784
4141
  const instance = client ? null : instanceOfCall(p, init, null, scope);
3785
4142
  out = { client, instance, tainted: false, whole: false, isRequest: false };
3786
- } else if (ts10.isNewExpression(init) && ts10.isIdentifier(init.expression)) {
4143
+ } else if (ts11.isNewExpression(init) && ts11.isIdentifier(init.expression)) {
3787
4144
  const cs = scope.get(init.expression.text);
3788
4145
  if (cs?.kind === "class") {
3789
4146
  out = {
@@ -3809,15 +4166,15 @@ function usesInput(frame, e) {
3809
4166
  let hit = false;
3810
4167
  walk(e, (n) => {
3811
4168
  if (hit) return false;
3812
- if (ts10.isCallExpression(n) && handsOverRequest(frame, n)) {
4169
+ if (ts11.isCallExpression(n) && handsOverRequest(frame, n)) {
3813
4170
  hit = true;
3814
4171
  return false;
3815
4172
  }
3816
- if (!ts10.isIdentifier(n)) return void 0;
4173
+ if (!ts11.isIdentifier(n)) return void 0;
3817
4174
  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;
4175
+ if (parent && ts11.isPropertyAccessExpression(parent) && parent.name === n) return void 0;
4176
+ if (parent && ts11.isPropertyAssignment(parent) && parent.name === n) return void 0;
4177
+ const member = parent && ts11.isPropertyAccessExpression(parent) && parent.expression === n;
3821
4178
  if (frame.inputNames.has(n.text)) hit = true;
3822
4179
  else if (member && frame.reqNames.has(n.text) && REQUEST_MEMBER.test(parent.name.text)) {
3823
4180
  hit = true;
@@ -3836,46 +4193,46 @@ function handsOverRequest(frame, call) {
3836
4193
  if (IDENTITY_CALLEE.test(callee) || REQUEST_CLIENT_CALLEE.test(callee)) return false;
3837
4194
  return call.arguments.some((a) => {
3838
4195
  const u = unwrap(a);
3839
- return ts10.isIdentifier(u) && frame.reqNames.has(u.text);
4196
+ return ts11.isIdentifier(u) && frame.reqNames.has(u.text);
3840
4197
  });
3841
4198
  }
3842
4199
  function propertyKeyOf(name) {
3843
- if (name && (ts10.isIdentifier(name) || ts10.isStringLiteral(name))) return name.text;
4200
+ if (name && (ts11.isIdentifier(name) || ts11.isStringLiteral(name))) return name.text;
3844
4201
  return null;
3845
4202
  }
3846
4203
  function taintedProperties(frame, lit) {
3847
4204
  const out = /* @__PURE__ */ new Set();
3848
4205
  for (const pr of lit.properties) {
3849
- if (ts10.isSpreadAssignment(pr)) {
4206
+ if (ts11.isSpreadAssignment(pr)) {
3850
4207
  if (derivedIn(frame, pr.expression)) return null;
3851
4208
  continue;
3852
4209
  }
3853
4210
  const key = propertyKeyOf(pr.name);
3854
4211
  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);
4212
+ if (ts11.isPropertyAssignment(pr) && derivedIn(frame, pr.initializer)) out.add(key);
4213
+ else if (ts11.isShorthandPropertyAssignment(pr) && derivedIn(frame, pr.name)) out.add(key);
3857
4214
  }
3858
4215
  return out;
3859
4216
  }
3860
4217
  function bindParamTaint(child, param, arg, frame) {
3861
4218
  const u = arg ? unwrap(arg) : void 0;
3862
4219
  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)) {
4220
+ if (u && ts11.isObjectLiteralExpression(u)) props = taintedProperties(frame, u);
4221
+ else if (u && ts11.isIdentifier(u) && !frame.inputNames.has(u.text)) {
3865
4222
  props = frame.partialInputs.get(u.text) ?? null;
3866
4223
  }
3867
4224
  if (props === null) {
3868
4225
  for (const nm of boundNames(param)) child.inputNames.add(nm);
3869
4226
  return;
3870
4227
  }
3871
- if (ts10.isIdentifier(param)) {
4228
+ if (ts11.isIdentifier(param)) {
3872
4229
  child.partialInputs.set(param.text, new Set(props));
3873
4230
  return;
3874
4231
  }
3875
4232
  bindPatternFrom(child, param, props);
3876
4233
  }
3877
4234
  function bindPatternFrom(child, pattern, props) {
3878
- if (!ts10.isObjectBindingPattern(pattern)) {
4235
+ if (!ts11.isObjectBindingPattern(pattern)) {
3879
4236
  if (props.size > 0) for (const nm of boundNames(pattern)) child.inputNames.add(nm);
3880
4237
  return;
3881
4238
  }
@@ -3888,11 +4245,11 @@ function bindPatternFrom(child, pattern, props) {
3888
4245
  }
3889
4246
  function isRequestBodyCall(frame, call) {
3890
4247
  const callee = call.expression;
3891
- if (!ts10.isPropertyAccessExpression(callee) || !/^(json|formData|text)$/.test(callee.name.text)) {
4248
+ if (!ts11.isPropertyAccessExpression(callee) || !/^(json|formData|text)$/.test(callee.name.text)) {
3892
4249
  return false;
3893
4250
  }
3894
4251
  const recv = unwrap(callee.expression);
3895
- if (!ts10.isIdentifier(recv)) return false;
4252
+ if (!ts11.isIdentifier(recv)) return false;
3896
4253
  if (frame.reqNames.has(recv.text)) return true;
3897
4254
  return frame.depth === 0 && frame.reqNames.size === 0 && REQUEST_NAME.test(recv.text);
3898
4255
  }
@@ -3905,7 +4262,7 @@ function wholeContext(frame) {
3905
4262
  }
3906
4263
  function receiverTainted(frame, call) {
3907
4264
  const callee = call.expression;
3908
- if (!ts10.isPropertyAccessExpression(callee) && !ts10.isElementAccessExpression(callee)) return false;
4265
+ if (!ts11.isPropertyAccessExpression(callee) && !ts11.isElementAccessExpression(callee)) return false;
3909
4266
  return derivedIn(frame, callee);
3910
4267
  }
3911
4268
  function argBinding(p, arg, frame) {
@@ -3915,7 +4272,7 @@ function argBinding(p, arg, frame) {
3915
4272
  let client = null;
3916
4273
  let instance = null;
3917
4274
  let isRequest = false;
3918
- if (ts10.isIdentifier(u)) {
4275
+ if (ts11.isIdentifier(u)) {
3919
4276
  client = frame.clients.get(u.text) ?? null;
3920
4277
  instance = frame.instances.get(u.text) ?? null;
3921
4278
  isRequest = frame.reqNames.has(u.text);
@@ -3927,14 +4284,14 @@ function argBinding(p, arg, frame) {
3927
4284
  instance = vb.instance;
3928
4285
  }
3929
4286
  }
3930
- } else if (ts10.isPropertyAccessExpression(u) && u.expression.kind === ts10.SyntaxKind.ThisKeyword) {
4287
+ } else if (ts11.isPropertyAccessExpression(u) && u.expression.kind === ts11.SyntaxKind.ThisKeyword) {
3931
4288
  const tp = frame.thisProps.get(u.name.text);
3932
4289
  if (tp) {
3933
4290
  client = tp.client;
3934
4291
  instance = tp.instance;
3935
4292
  isRequest = tp.isRequest;
3936
4293
  }
3937
- } else if (ts10.isCallExpression(u)) {
4294
+ } else if (ts11.isCallExpression(u)) {
3938
4295
  client = classifyCall(p, u, frame.sf, scope, frame, 0);
3939
4296
  if (!client) instance = instanceOfCall(p, u, frame, scope);
3940
4297
  } else if (isPrismaNew(u)) {
@@ -3943,7 +4300,7 @@ function argBinding(p, arg, frame) {
3943
4300
  name: "PrismaClient",
3944
4301
  location: { file: frame.rel, line: lineOf(frame.sf, u) }
3945
4302
  };
3946
- } else if (ts10.isNewExpression(u) && ts10.isIdentifier(u.expression)) {
4303
+ } else if (ts11.isNewExpression(u) && ts11.isIdentifier(u.expression)) {
3947
4304
  const cs = scope.get(u.expression.text);
3948
4305
  if (cs?.kind === "class") {
3949
4306
  instance = {
@@ -3971,7 +4328,7 @@ function methodTarget(inst, method) {
3971
4328
  }
3972
4329
  function callTarget(p, call, frame, scope) {
3973
4330
  const callee = call.expression;
3974
- if (ts10.isIdentifier(callee)) {
4331
+ if (ts11.isIdentifier(callee)) {
3975
4332
  const sym = scope.get(callee.text);
3976
4333
  if (sym?.kind === "function") {
3977
4334
  return { fn: sym.fn, facts: sym.facts, name: sym.name, cls: null, thisProps: /* @__PURE__ */ new Map() };
@@ -3987,10 +4344,10 @@ function callTarget(p, call, frame, scope) {
3987
4344
  }
3988
4345
  return null;
3989
4346
  }
3990
- if (!ts10.isPropertyAccessExpression(callee)) return null;
4347
+ if (!ts11.isPropertyAccessExpression(callee)) return null;
3991
4348
  const obj = callee.expression;
3992
4349
  const method = callee.name.text;
3993
- if (ts10.isIdentifier(obj)) {
4350
+ if (ts11.isIdentifier(obj)) {
3994
4351
  const inst = frame.instances.get(obj.text);
3995
4352
  if (inst) return methodTarget(inst, method);
3996
4353
  const sym = scope.get(obj.text);
@@ -4008,7 +4365,7 @@ function callTarget(p, call, frame, scope) {
4008
4365
  }
4009
4366
  return null;
4010
4367
  }
4011
- if (obj.kind === ts10.SyntaxKind.ThisKeyword && frame.cls) {
4368
+ if (obj.kind === ts11.SyntaxKind.ThisKeyword && frame.cls) {
4012
4369
  const m = frame.cls.methods.get(method);
4013
4370
  if (!m) return null;
4014
4371
  return {
@@ -4019,13 +4376,13 @@ function callTarget(p, call, frame, scope) {
4019
4376
  thisProps: frame.thisProps
4020
4377
  };
4021
4378
  }
4022
- if (ts10.isPropertyAccessExpression(obj) && obj.expression.kind === ts10.SyntaxKind.ThisKeyword) {
4379
+ if (ts11.isPropertyAccessExpression(obj) && obj.expression.kind === ts11.SyntaxKind.ThisKeyword) {
4023
4380
  const tp = frame.thisProps.get(obj.name.text);
4024
4381
  if (tp?.instance) return methodTarget(tp.instance, method);
4025
4382
  }
4026
4383
  return null;
4027
4384
  }
4028
- function analyzeFrame(p, frame, acc) {
4385
+ function bindDeclarations(p, frame, acc) {
4029
4386
  const { rel, sf, fn } = frame;
4030
4387
  const scope = scopeOf(p, frame.facts);
4031
4388
  const loc2 = (n) => ({ file: rel, line: lineOf(sf, n) });
@@ -4057,24 +4414,24 @@ function analyzeFrame(p, frame, acc) {
4057
4414
  for (const nm of names) addInput(kind, nm, decl, true);
4058
4415
  bindInput(names, true);
4059
4416
  };
4060
- for (const decl of collect(body, ts10.isVariableDeclaration)) {
4417
+ for (const decl of collect(body, ts11.isVariableDeclaration)) {
4061
4418
  if (!decl.initializer) continue;
4062
4419
  const init = clientCreatingOperand(decl.initializer);
4063
4420
  const names = boundNames(decl.name);
4064
4421
  const text = init.getText(sf);
4065
4422
  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)) {
4423
+ const client = ts11.isCallExpression(init) ? classifyCall(p, init, sf, scope, frame, 0) : null;
4424
+ if (client && ts11.isIdentifier(decl.name)) {
4068
4425
  frame.clients.set(decl.name.text, client);
4069
4426
  continue;
4070
4427
  }
4071
- const rowsOfQuery = ts10.isCallExpression(init) && (isQueryChain(init) || isDbChain(init, frame));
4428
+ const rowsOfQuery = ts11.isCallExpression(init) && (isQueryChain(init) || isDbChain(init, frame));
4072
4429
  if (frame.depth === 0 && !rowsOfQuery) {
4073
4430
  if (/^(params|context\.params|ctx\.params|props\.params)$/.test(text)) {
4074
4431
  handlerInput("route_param", names, decl);
4075
4432
  continue;
4076
4433
  }
4077
- if (ts10.isCallExpression(init) && isRequestCall(text)) {
4434
+ if (ts11.isCallExpression(init) && isRequestCall(text)) {
4078
4435
  handlerInput("body", names, decl);
4079
4436
  continue;
4080
4437
  }
@@ -4087,9 +4444,9 @@ function analyzeFrame(p, frame, acc) {
4087
4444
  continue;
4088
4445
  }
4089
4446
  }
4090
- if (ts10.isCallExpression(init)) {
4447
+ if (ts11.isCallExpression(init)) {
4091
4448
  const inst = instanceOfCall(p, init, frame, scope);
4092
- if (inst && ts10.isIdentifier(decl.name)) {
4449
+ if (inst && ts11.isIdentifier(decl.name)) {
4093
4450
  frame.instances.set(decl.name.text, inst);
4094
4451
  continue;
4095
4452
  }
@@ -4097,34 +4454,41 @@ function analyzeFrame(p, frame, acc) {
4097
4454
  if (returnsIdentity(p, init, scope)) continue;
4098
4455
  const args = init.arguments.map((a) => argBinding(p, a, frame));
4099
4456
  if (args.some((a) => a.tainted || a.isRequest) || receiverTainted(frame, init)) {
4100
- bindInput(names, isWholeInput(init, wholeContext(frame)));
4457
+ const rt = returnTaint(p, init, frame, scope, 0);
4458
+ if (rt === null || rt.tainted && rt.props === null) {
4459
+ bindInput(names, isWholeInput(init, wholeContext(frame)));
4460
+ } else if (rt.tainted && rt.props !== null) {
4461
+ if (ts11.isIdentifier(decl.name))
4462
+ frame.partialInputs.set(decl.name.text, new Set(rt.props));
4463
+ else bindPatternFrom(frame, decl.name, rt.props);
4464
+ }
4101
4465
  }
4102
- } else if (isPrismaNew(init) && ts10.isIdentifier(decl.name)) {
4466
+ } else if (isPrismaNew(init) && ts11.isIdentifier(decl.name)) {
4103
4467
  frame.clients.set(decl.name.text, {
4104
4468
  kind: "direct_db",
4105
4469
  name: decl.name.text,
4106
4470
  location: loc2(init)
4107
4471
  });
4108
- } else if (ts10.isNewExpression(init) && ts10.isIdentifier(init.expression) && scope.get(init.expression.text)?.kind === "class") {
4472
+ } else if (ts11.isNewExpression(init) && ts11.isIdentifier(init.expression) && scope.get(init.expression.text)?.kind === "class") {
4109
4473
  const cs = scope.get(init.expression.text);
4110
- if (cs?.kind === "class" && ts10.isIdentifier(decl.name)) {
4474
+ if (cs?.kind === "class" && ts11.isIdentifier(decl.name)) {
4111
4475
  frame.instances.set(decl.name.text, {
4112
4476
  cls: cs.cls,
4113
4477
  facts: cs.facts,
4114
4478
  ctorArgs: (init.arguments ?? []).map((a) => argBinding(p, a, frame))
4115
4479
  });
4116
4480
  }
4117
- } else if (ts10.isIdentifier(init)) {
4481
+ } else if (ts11.isIdentifier(init)) {
4118
4482
  const c = frame.clients.get(init.text);
4119
- if (c && ts10.isIdentifier(decl.name)) frame.clients.set(decl.name.text, c);
4483
+ if (c && ts11.isIdentifier(decl.name)) frame.clients.set(decl.name.text, c);
4120
4484
  const i = frame.instances.get(init.text);
4121
- if (i && ts10.isIdentifier(decl.name)) frame.instances.set(decl.name.text, i);
4485
+ if (i && ts11.isIdentifier(decl.name)) frame.instances.set(decl.name.text, i);
4122
4486
  if (frame.inputNames.has(init.text)) {
4123
4487
  if (frame.depth === 0) for (const nm of names) addInput("body", nm, decl, true);
4124
4488
  bindInput(names, frame.wholeNames.has(init.text));
4125
4489
  } else {
4126
4490
  const partial = frame.partialInputs.get(init.text);
4127
- if (partial && ts10.isIdentifier(decl.name)) {
4491
+ if (partial && ts11.isIdentifier(decl.name)) {
4128
4492
  frame.partialInputs.set(decl.name.text, new Set(partial));
4129
4493
  } else if (partial) bindPatternFrom(frame, decl.name, partial);
4130
4494
  }
@@ -4134,34 +4498,47 @@ function analyzeFrame(p, frame, acc) {
4134
4498
  }
4135
4499
  if (frame.depth === 0) {
4136
4500
  walk(body, (n) => {
4137
- if (ts10.isPropertyAccessExpression(n) && ts10.isIdentifier(n.expression) && n.expression.text === "params") {
4501
+ if (ts11.isPropertyAccessExpression(n) && ts11.isIdentifier(n.expression) && n.expression.text === "params") {
4138
4502
  addInput("route_param", n.name.text, n, false);
4139
4503
  }
4140
4504
  return void 0;
4141
4505
  });
4142
4506
  }
4143
- for (const call of collect(body, ts10.isCallExpression)) {
4507
+ }
4508
+ function analyzeFrame(p, frame, acc) {
4509
+ const { rel, sf, fn } = frame;
4510
+ const scope = scopeOf(p, frame.facts);
4511
+ const loc2 = (n) => ({ file: rel, line: lineOf(sf, n) });
4512
+ const body = fn.body ?? fn;
4513
+ bindDeclarations(p, frame, acc);
4514
+ for (const call of collect(body, ts11.isCallExpression)) {
4144
4515
  const callee = call.expression;
4145
- if (!ts10.isPropertyAccessExpression(callee) || !/^\$?transaction$/.test(callee.name.text))
4516
+ if (!ts11.isPropertyAccessExpression(callee) || !/^\$?transaction$/.test(callee.name.text))
4146
4517
  continue;
4147
- if (!ts10.isIdentifier(callee.expression)) continue;
4518
+ if (!ts11.isIdentifier(callee.expression)) continue;
4148
4519
  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
- }
4520
+ const fn2 = call.arguments.map((a) => unwrap(a)).find((a) => ts11.isArrowFunction(a) || ts11.isFunctionExpression(a));
4521
+ const param = fn2 && (ts11.isArrowFunction(fn2) || ts11.isFunctionExpression(fn2)) ? fn2.parameters[0] : void 0;
4522
+ if (outer && param && ts11.isIdentifier(param.name)) frame.clients.set(param.name.text, outer);
4523
+ }
4524
+ const isSessionCall = (call) => /\.auth\.(getUser|getSession|getClaims)$/.test(call.expression.getText(sf)) || symOfCallee(p, call.expression, scope)?.kind === "auth";
4525
+ for (const call of collect(body, ts11.isCallExpression)) {
4526
+ if (isSessionCall(call)) acc.authChecks.push({ ...loc2(call), kind: "session" });
4527
+ }
4528
+ for (const gate of roleGatesIn(body, sessionNamesIn(body, isSessionCall))) {
4529
+ if (gate.exit === "return" && !frame.exitPropagates) continue;
4530
+ acc.roleChecks.push({
4531
+ ...loc2(gate.node),
4532
+ source: gate.source,
4533
+ text: gate.node.expression.getText(sf).replace(/\s+/g, " ").slice(0, 160)
4534
+ });
4158
4535
  }
4159
4536
  for (const check of secretChecksIn(fn, sf)) {
4160
4537
  acc.authChecks.push({ ...loc2(check.node), kind: "secret" });
4161
4538
  }
4162
- for (const pa of collect(body, ts10.isPropertyAccessExpression)) {
4539
+ for (const pa of collect(body, ts11.isPropertyAccessExpression)) {
4163
4540
  const inner = pa.expression;
4164
- if (ts10.isPropertyAccessExpression(inner) && (inner.name.text === "user_metadata" || inner.name.text === "app_metadata")) {
4541
+ if (ts11.isPropertyAccessExpression(inner) && (inner.name.text === "user_metadata" || inner.name.text === "app_metadata")) {
4165
4542
  acc.metadataAccesses.push({
4166
4543
  path: pa.getText(sf),
4167
4544
  bucket: inner.name.text,
@@ -4173,15 +4550,15 @@ function analyzeFrame(p, frame, acc) {
4173
4550
  const seen = /* @__PURE__ */ new Set();
4174
4551
  const clientOf = (root) => {
4175
4552
  const u = unwrap(root);
4176
- if (ts10.isIdentifier(u)) {
4553
+ if (ts11.isIdentifier(u)) {
4177
4554
  const b = frame.clients.get(u.text) ?? null;
4178
4555
  return { binding: b, name: b ? null : u.text };
4179
4556
  }
4180
- if (ts10.isCallExpression(u)) {
4557
+ if (ts11.isCallExpression(u)) {
4181
4558
  const b = classifyCall(p, u, sf, scope, frame, 0);
4182
4559
  return { binding: b, name: b ? null : u.expression.getText(sf) };
4183
4560
  }
4184
- if (ts10.isPropertyAccessExpression(u) && u.expression.kind === ts10.SyntaxKind.ThisKeyword) {
4561
+ if (ts11.isPropertyAccessExpression(u) && u.expression.kind === ts11.SyntaxKind.ThisKeyword) {
4185
4562
  const b = frame.thisProps.get(u.name.text)?.client ?? null;
4186
4563
  return { binding: b, name: b ? null : u.getText(sf) };
4187
4564
  }
@@ -4190,11 +4567,11 @@ function analyzeFrame(p, frame, acc) {
4190
4567
  const tableSym = (e) => {
4191
4568
  if (!e) return null;
4192
4569
  const u = unwrap(e);
4193
- if (ts10.isIdentifier(u)) {
4570
+ if (ts11.isIdentifier(u)) {
4194
4571
  const sym = scope.get(u.text);
4195
4572
  return sym?.kind === "table" ? sym.table : null;
4196
4573
  }
4197
- if (ts10.isPropertyAccessExpression(u) && ts10.isIdentifier(u.expression)) {
4574
+ if (ts11.isPropertyAccessExpression(u) && ts11.isIdentifier(u.expression)) {
4198
4575
  const ns = scope.get(u.expression.text);
4199
4576
  if (ns?.kind === "namespace") {
4200
4577
  const sym = exportedSym(p, ns.facts, u.name.text, 0);
@@ -4251,7 +4628,46 @@ function analyzeFrame(p, frame, acc) {
4251
4628
  if (frame.via.length > 0) query.via = frame.via;
4252
4629
  acc.queries.push(query);
4253
4630
  };
4254
- for (const call of collect(body, ts10.isCallExpression)) {
4631
+ const supabaseFilters = (segments) => {
4632
+ const filters = [];
4633
+ for (const s of segments) {
4634
+ if (!FILTER_METHODS.has(s.name)) continue;
4635
+ const firstArg = s.args[0];
4636
+ if (s.name === "match" && firstArg && ts11.isObjectLiteralExpression(firstArg)) {
4637
+ for (const pr of firstArg.properties) {
4638
+ if (ts11.isPropertyAssignment(pr)) {
4639
+ const f2 = {
4640
+ method: "match",
4641
+ column: pr.name.getText(sf).replace(/['"]/g, ""),
4642
+ valueText: pr.initializer.getText(sf),
4643
+ inputDerived: derivedIn(frame, pr.initializer)
4644
+ };
4645
+ filters.push(valued(f2, pr.initializer));
4646
+ }
4647
+ }
4648
+ continue;
4649
+ }
4650
+ const val = s.args[1];
4651
+ const f = {
4652
+ method: s.name,
4653
+ column: stringLiteralValue(firstArg),
4654
+ valueText: val ? val.getText(sf) : "",
4655
+ inputDerived: val ? derivedIn(frame, val) : false
4656
+ };
4657
+ filters.push(valued(f, val));
4658
+ }
4659
+ return filters;
4660
+ };
4661
+ const builders = /* @__PURE__ */ new Map();
4662
+ const bindBuilder = (tail, query) => {
4663
+ const { node, parent } = outerOf(tail);
4664
+ if (parent && ts11.isVariableDeclaration(parent) && ts11.isIdentifier(parent.name)) {
4665
+ builders.set(parent.name.text, query);
4666
+ } else if (parent && ts11.isBinaryExpression(parent) && parent.operatorToken.kind === ts11.SyntaxKind.EqualsToken && parent.right === node && ts11.isIdentifier(parent.left)) {
4667
+ builders.set(parent.left.text, query);
4668
+ }
4669
+ };
4670
+ for (const call of collect(body, ts11.isCallExpression)) {
4255
4671
  if (!isChainTail(call)) continue;
4256
4672
  const chain = flattenChain(call);
4257
4673
  const storageCall = storageCallOf(chain, storageHandles);
@@ -4260,6 +4676,18 @@ function analyzeFrame(p, frame, acc) {
4260
4676
  continue;
4261
4677
  }
4262
4678
  const root = unwrap(chain.root);
4679
+ const builder = ts11.isIdentifier(root) ? builders.get(root.text) : void 0;
4680
+ if (builder) {
4681
+ const condition = enclosingCondition(call, body);
4682
+ if (!condition || !derivedIn(frame, condition)) {
4683
+ const note = condition ? ` (when ${condition.getText(sf).replace(/\s+/g, " ")})` : "";
4684
+ for (const f of supabaseFilters(chain.segments)) {
4685
+ builder.filters.push(note ? { ...f, valueText: `${f.valueText}${note}` } : f);
4686
+ }
4687
+ }
4688
+ bindBuilder(call, builder);
4689
+ continue;
4690
+ }
4263
4691
  const first = chain.segments[0];
4264
4692
  const fromIdx = chain.segments.findIndex((s) => s.name === "from");
4265
4693
  const rpcIdx = chain.segments.findIndex((s) => s.name === "rpc");
@@ -4282,33 +4710,7 @@ function analyzeFrame(p, frame, acc) {
4282
4710
  break;
4283
4711
  }
4284
4712
  }
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
- }
4713
+ const filters = supabaseFilters(after);
4312
4714
  parsed = {
4313
4715
  anchor: anchor.node,
4314
4716
  table: fromTable ?? stringLiteralValue(anchor.args[0]) ?? "(dynamic)",
@@ -4337,7 +4739,7 @@ function analyzeFrame(p, frame, acc) {
4337
4739
  payload: payloadOf(payloadSeg?.args[0]),
4338
4740
  clientRoot: chain.root
4339
4741
  };
4340
- } else if (first && DRIZZLE_QUERY_API.has(first.name) && ts10.isPropertyAccessExpression(root) && ts10.isPropertyAccessExpression(root.expression) && root.expression.name.text === "query") {
4742
+ } else if (first && DRIZZLE_QUERY_API.has(first.name) && ts11.isPropertyAccessExpression(root) && ts11.isPropertyAccessExpression(root.expression) && root.expression.name.text === "query") {
4341
4743
  const key = root.name.text;
4342
4744
  parsed = {
4343
4745
  anchor: first.node,
@@ -4349,7 +4751,7 @@ function analyzeFrame(p, frame, acc) {
4349
4751
  payload: null,
4350
4752
  clientRoot: root.expression.expression
4351
4753
  };
4352
- } else if (first && PRISMA_OPS[first.name] && ts10.isPropertyAccessExpression(root) && ts10.isIdentifier(root.expression) && clientOf(root.expression).binding?.kind === "direct_db") {
4754
+ } else if (first && PRISMA_OPS[first.name] && ts11.isPropertyAccessExpression(root) && ts11.isIdentifier(root.expression) && clientOf(root.expression).binding?.kind === "direct_db") {
4353
4755
  const accessor = root.name.text;
4354
4756
  const arg = first.args[0];
4355
4757
  parsed = {
@@ -4382,6 +4784,13 @@ function analyzeFrame(p, frame, acc) {
4382
4784
  call,
4383
4785
  chain.segments.map((s) => s.name)
4384
4786
  );
4787
+ const checks = rowComparisons(call).filter((c) => c.exit === "throw" || frame.exitPropagates).map((c) => ({
4788
+ method: "compare",
4789
+ column: c.column,
4790
+ valueText: c.value.getText(sf).replace(/\s+/g, " "),
4791
+ inputDerived: derivedIn(frame, c.value)
4792
+ }));
4793
+ if (checks.length > 0) query.ownerChecks = checks;
4385
4794
  acc.reads.push({
4386
4795
  query,
4387
4796
  keys: query.filters.map((f) => {
@@ -4389,76 +4798,151 @@ function analyzeFrame(p, frame, acc) {
4389
4798
  return v ? valueKey(frame, v) : null;
4390
4799
  }),
4391
4800
  order: [...frame.pathPos, parsed.anchor.getStart(sf)],
4392
- exits: rowExit === "throw" || rowExit === "return" && frame.exitPropagates
4801
+ exits: rowExit === "throw" || rowExit === "return" && frame.exitPropagates,
4802
+ checks
4393
4803
  });
4804
+ bindBuilder(call, query);
4394
4805
  }
4395
4806
  if (query.filters.some((f) => f.inputDerived && isCredentialColumn(f.column))) {
4396
4807
  acc.authChecks.push({ ...query.location, kind: "credential" });
4397
4808
  }
4398
4809
  }
4399
4810
  if (frame.depth >= MAX_DEPTH) return;
4400
- for (const call of collect(body, ts10.isCallExpression)) {
4811
+ for (const call of collect(body, ts11.isCallExpression)) {
4401
4812
  const target = callTarget(p, call, frame, scope);
4402
4813
  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(",")}`;
4814
+ const child = childFrame(p, call, target, frame);
4815
+ if (!child) continue;
4816
+ const key = `${target.facts.file}#${target.name}#${frameSignature(child)}`;
4457
4817
  if (acc.visited.has(key)) continue;
4458
4818
  acc.visited.add(key);
4459
4819
  analyzeFrame(p, child, acc);
4460
4820
  }
4461
4821
  }
4822
+ function childFrame(p, call, target, frame) {
4823
+ const tsf = p.sources.get(target.facts.file);
4824
+ if (!tsf) return null;
4825
+ const child = {
4826
+ rel: target.facts.file,
4827
+ sf: tsf,
4828
+ facts: target.facts,
4829
+ fn: target.fn,
4830
+ depth: frame.depth + 1,
4831
+ via: [...frame.via, `${target.name} (${target.facts.file}:${lineOf(tsf, target.fn)})`],
4832
+ inputNames: /* @__PURE__ */ new Set(),
4833
+ wholeNames: /* @__PURE__ */ new Set(),
4834
+ partialInputs: /* @__PURE__ */ new Map(),
4835
+ reqNames: /* @__PURE__ */ new Set(),
4836
+ clients: /* @__PURE__ */ new Map(),
4837
+ instances: /* @__PURE__ */ new Map(),
4838
+ cls: target.cls,
4839
+ thisProps: target.thisProps,
4840
+ key: `${target.facts.file}#${target.name}@${call.getStart(frame.sf)}`,
4841
+ aliases: /* @__PURE__ */ new Map(),
4842
+ pathPos: [...frame.pathPos, call.getStart(frame.sf)],
4843
+ exitPropagates: frame.exitPropagates && callResultChecked(call)
4844
+ };
4845
+ const cx = wholeContext(frame);
4846
+ target.fn.parameters.forEach((param, i) => {
4847
+ const arg = call.arguments[i];
4848
+ const ab = argBinding(p, arg, frame);
4849
+ const names = boundNames(param.name);
4850
+ const head = names[0];
4851
+ if (ab.client && head !== void 0 && ts11.isIdentifier(param.name)) {
4852
+ child.clients.set(head, ab.client);
4853
+ }
4854
+ if (ab.instance && head !== void 0 && ts11.isIdentifier(param.name)) {
4855
+ child.instances.set(head, ab.instance);
4856
+ }
4857
+ for (const nm of names) if (ab.isRequest) child.reqNames.add(nm);
4858
+ if (ab.tainted) {
4859
+ bindParamTaint(child, param.name, arg, frame);
4860
+ for (const nm of wholeParamNames(param.name, arg, cx)) {
4861
+ child.inputNames.add(nm);
4862
+ child.wholeNames.add(nm);
4863
+ }
4864
+ aliasParam(child, param.name, arg, frame);
4865
+ }
4866
+ });
4867
+ return child;
4868
+ }
4869
+ function frameSignature(child) {
4870
+ return [
4871
+ ...[...child.clients].map(([n, c]) => `${n}=${c.kind}`),
4872
+ ...[...child.inputNames].map((n) => `${n}!${child.wholeNames.has(n) ? "*" : ""}`),
4873
+ ...[...child.partialInputs].map(([n, s]) => `${n}.{${[...s].sort().join("|")}}`),
4874
+ ...[...child.reqNames].map((n) => `${n}?`),
4875
+ ...[...child.thisProps].map(([n, b]) => `this.${n}=${b.client?.kind ?? "-"}`),
4876
+ ...[...child.aliases].map(([n, k]) => `${n}~${k}`),
4877
+ `exit=${child.exitPropagates}`
4878
+ ].sort().join(",");
4879
+ }
4880
+ var CLEAN = { tainted: false, props: null };
4881
+ var TAINTED = { tainted: true, props: null };
4882
+ var MAX_RETURN_DEPTH = 2;
4883
+ function branchesOf(e) {
4884
+ const u = unwrap(e);
4885
+ if (ts11.isConditionalExpression(u)) return [...branchesOf(u.whenTrue), ...branchesOf(u.whenFalse)];
4886
+ if (ts11.isBinaryExpression(u) && (u.operatorToken.kind === ts11.SyntaxKind.QuestionQuestionToken || u.operatorToken.kind === ts11.SyntaxKind.BarBarToken || u.operatorToken.kind === ts11.SyntaxKind.AmpersandAmpersandToken)) {
4887
+ return [...branchesOf(u.left), ...branchesOf(u.right)];
4888
+ }
4889
+ return [u];
4890
+ }
4891
+ function isLiteralValue(e) {
4892
+ 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";
4893
+ }
4894
+ function mergeTaint(a, b) {
4895
+ if (!a.tainted) return b;
4896
+ if (!b.tainted) return a;
4897
+ if (a.props === null || b.props === null) return TAINTED;
4898
+ return { tainted: true, props: /* @__PURE__ */ new Set([...a.props, ...b.props]) };
4899
+ }
4900
+ function leafTaint(p, leaf, child, scope, depth) {
4901
+ if (isLiteralValue(leaf)) return CLEAN;
4902
+ if (ts11.isObjectLiteralExpression(leaf)) {
4903
+ const props = taintedProperties(child, leaf);
4904
+ if (props === null) return TAINTED;
4905
+ return props.size === 0 ? CLEAN : { tainted: true, props };
4906
+ }
4907
+ if (ts11.isCallExpression(leaf)) {
4908
+ if (isChainWithQuery(leaf) || isDbChain(leaf, child)) return CLEAN;
4909
+ if (returnsIdentity(p, leaf, scope)) return CLEAN;
4910
+ const nested = returnTaint(p, leaf, child, scope, depth + 1);
4911
+ if (nested !== null) return nested;
4912
+ }
4913
+ return derivedIn(child, leaf) ? TAINTED : CLEAN;
4914
+ }
4915
+ function returnTaint(p, call, frame, scope, depth) {
4916
+ if (depth > MAX_RETURN_DEPTH) return null;
4917
+ const target = callTarget(p, call, frame, scope);
4918
+ if (!target) return null;
4919
+ const child = childFrame(p, call, target, frame);
4920
+ if (!child) return null;
4921
+ const key = `${target.facts.file}#${target.name}#${frameSignature(child)}`;
4922
+ const cached = p.returnTaints.get(key);
4923
+ if (cached !== void 0) return cached;
4924
+ p.returnTaints.set(key, null);
4925
+ const scratch = {
4926
+ inputs: [],
4927
+ authChecks: [],
4928
+ roleChecks: [],
4929
+ queries: [],
4930
+ metadataAccesses: [],
4931
+ visited: /* @__PURE__ */ new Set(),
4932
+ reads: []
4933
+ };
4934
+ bindDeclarations(p, child, scratch);
4935
+ const childScope = scopeOf(p, child.facts);
4936
+ let out = CLEAN;
4937
+ for (const ret of ownReturns(target.fn)) {
4938
+ for (const leaf of branchesOf(ret)) {
4939
+ out = mergeTaint(out, leafTaint(p, leaf, child, childScope, depth));
4940
+ if (out.tainted && out.props === null) break;
4941
+ }
4942
+ }
4943
+ p.returnTaints.set(key, out);
4944
+ return out;
4945
+ }
4462
4946
  function pushQuery(acc, q) {
4463
4947
  const same = acc.queries.find(
4464
4948
  (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 +4963,8 @@ function pushQuery(acc, q) {
4479
4963
  }
4480
4964
  function dottedPath(e) {
4481
4965
  const u = unwrap(e);
4482
- if (ts10.isIdentifier(u)) return u.text;
4483
- if (ts10.isPropertyAccessExpression(u)) {
4966
+ if (ts11.isIdentifier(u)) return u.text;
4967
+ if (ts11.isPropertyAccessExpression(u)) {
4484
4968
  const base = dottedPath(u.expression);
4485
4969
  return base === null ? null : `${base}.${u.name.text}`;
4486
4970
  }
@@ -4499,14 +4983,14 @@ function valueKey(frame, e) {
4499
4983
  function aliasLocal(frame, name, init) {
4500
4984
  const key = valueKey(frame, init);
4501
4985
  if (key === null) return;
4502
- if (ts10.isIdentifier(name)) {
4986
+ if (ts11.isIdentifier(name)) {
4503
4987
  frame.aliases.set(name.text, key);
4504
4988
  return;
4505
4989
  }
4506
- if (!ts10.isObjectBindingPattern(name)) return;
4990
+ if (!ts11.isObjectBindingPattern(name)) return;
4507
4991
  for (const el of name.elements) {
4508
4992
  const k = propertyKeyOf(el.propertyName ?? el.name);
4509
- if (k !== null && !el.dotDotDotToken && ts10.isIdentifier(el.name)) {
4993
+ if (k !== null && !el.dotDotDotToken && ts11.isIdentifier(el.name)) {
4510
4994
  frame.aliases.set(el.name.text, `${key}.${k}`);
4511
4995
  }
4512
4996
  }
@@ -4514,51 +4998,102 @@ function aliasLocal(frame, name, init) {
4514
4998
  function aliasParam(child, param, arg, frame) {
4515
4999
  if (!arg) return;
4516
5000
  const u = unwrap(arg);
4517
- if (ts10.isIdentifier(param)) {
5001
+ if (ts11.isIdentifier(param)) {
4518
5002
  const key = valueKey(frame, arg);
4519
5003
  if (key !== null) child.aliases.set(param.text, key);
4520
- else if (ts10.isObjectLiteralExpression(u)) {
5004
+ else if (ts11.isObjectLiteralExpression(u)) {
4521
5005
  for (const pr of u.properties) {
4522
5006
  const k = propertyKeyOf(pr.name);
4523
- const v = ts10.isPropertyAssignment(pr) ? pr.initializer : ts10.isShorthandPropertyAssignment(pr) ? pr.name : void 0;
5007
+ const v = ts11.isPropertyAssignment(pr) ? pr.initializer : ts11.isShorthandPropertyAssignment(pr) ? pr.name : void 0;
4524
5008
  const vk = k !== null && v ? valueKey(frame, v) : null;
4525
5009
  if (k !== null && vk !== null) child.aliases.set(`${param.text}.${k}`, vk);
4526
5010
  }
4527
5011
  }
4528
5012
  return;
4529
5013
  }
4530
- if (!ts10.isObjectBindingPattern(param)) return;
5014
+ if (!ts11.isObjectBindingPattern(param)) return;
4531
5015
  const base = valueKey(frame, arg);
4532
5016
  for (const el of param.elements) {
4533
5017
  const k = propertyKeyOf(el.propertyName ?? el.name);
4534
- if (k === null || el.dotDotDotToken || !ts10.isIdentifier(el.name)) continue;
5018
+ if (k === null || el.dotDotDotToken || !ts11.isIdentifier(el.name)) continue;
4535
5019
  if (base !== null) {
4536
5020
  child.aliases.set(el.name.text, `${base}.${k}`);
4537
5021
  continue;
4538
5022
  }
4539
- if (!ts10.isObjectLiteralExpression(u)) continue;
5023
+ if (!ts11.isObjectLiteralExpression(u)) continue;
4540
5024
  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;
5025
+ const v = pr && ts11.isPropertyAssignment(pr) ? pr.initializer : pr && ts11.isShorthandPropertyAssignment(pr) ? pr.name : void 0;
4542
5026
  const vk = v ? valueKey(frame, v) : null;
4543
5027
  if (vk !== null) child.aliases.set(el.name.text, vk);
4544
5028
  }
4545
5029
  }
4546
- function linkGuards(reads) {
4547
- const col = (c) => (c ?? "").toLowerCase().replace(/_/g, "");
5030
+ function enclosingCondition(node, body) {
5031
+ let child = node;
5032
+ let cur = node.parent;
5033
+ while (cur && cur !== body && !isFunctionLikeNode(cur)) {
5034
+ if (ts11.isIfStatement(cur) && cur.expression !== child) return cur.expression;
5035
+ child = cur;
5036
+ cur = cur.parent;
5037
+ }
5038
+ return null;
5039
+ }
5040
+ var normalizeColumn = (c) => (c ?? "").toLowerCase().replace(/_/g, "");
5041
+ function singular(table) {
5042
+ if (table.endsWith("ies")) return `${table.slice(0, -3)}y`;
5043
+ if (table.endsWith("ses") || table.endsWith("xes")) return table.slice(0, -2);
5044
+ return table.endsWith("s") ? table.slice(0, -1) : table;
5045
+ }
5046
+ function columnNamesTable(column, table) {
5047
+ const c = normalizeColumn(column);
5048
+ if (!c.endsWith("id") || c === "id") return false;
5049
+ const stem = c.slice(0, -2);
5050
+ const t = normalizeColumn(table);
5051
+ return stem === t || stem === singular(t);
5052
+ }
5053
+ function guardMatch(q, g, tables) {
5054
+ const qTable = q.query.table.toLowerCase();
5055
+ const gTable = g.query.table.toLowerCase();
5056
+ const info = tables.get(qTable);
5057
+ for (let i = 0; i < q.query.filters.length; i += 1) {
5058
+ const f = q.query.filters[i];
5059
+ const k = q.keys[i];
5060
+ if (!f?.inputDerived || !f.column || k === null || k === void 0) continue;
5061
+ const j = g.keys.indexOf(k);
5062
+ const gf = j >= 0 ? g.query.filters[j] : void 0;
5063
+ if (!gf) continue;
5064
+ if (qTable === gTable) {
5065
+ if (normalizeColumn(gf.column) === normalizeColumn(f.column))
5066
+ return { read: g, column: f.column };
5067
+ continue;
5068
+ }
5069
+ const ref = info?.columnInfo?.find((c) => c.name === f.column?.toLowerCase())?.references;
5070
+ if (ref && ref.table === gTable && normalizeColumn(ref.column) === normalizeColumn(gf.column)) {
5071
+ return {
5072
+ read: g,
5073
+ column: f.column,
5074
+ parent: { table: g.query.table, column: gf.column ?? "id", how: "foreign key" }
5075
+ };
5076
+ }
5077
+ if (normalizeColumn(gf.column) === "id" && columnNamesTable(f.column, gTable)) {
5078
+ return {
5079
+ read: g,
5080
+ column: f.column,
5081
+ parent: { table: g.query.table, column: gf.column ?? "id", how: "column name" }
5082
+ };
5083
+ }
5084
+ }
5085
+ return null;
5086
+ }
5087
+ function linkGuards(reads, tables) {
4548
5088
  for (const q of reads) {
4549
5089
  let best = null;
4550
5090
  for (const g2 of reads) {
4551
5091
  if (g2 === q || !["select", "unknown"].includes(g2.query.operation)) continue;
4552
- if (g2.query.table.toLowerCase() !== q.query.table.toLowerCase()) continue;
4553
5092
  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 };
5093
+ const m = guardMatch(q, g2, tables);
5094
+ if (!m) continue;
5095
+ const stops = (r) => r.exits || r.checks.length > 0;
5096
+ if (!best || stops(m.read) && !stops(best.read)) best = m;
4562
5097
  }
4563
5098
  if (!best) continue;
4564
5099
  const g = best.read.query;
@@ -4569,20 +5104,23 @@ function linkGuards(reads) {
4569
5104
  clientName: g.clientName,
4570
5105
  filters: g.filters,
4571
5106
  column: best.column,
4572
- exitsWhenMissing: best.read.exits,
5107
+ // A row that fails the comparison stops the entry point too; a missing one fails it as well.
5108
+ exitsWhenMissing: best.read.exits || best.read.checks.length > 0,
4573
5109
  text: g.text,
4574
- ...g.via ? { via: g.via } : {}
5110
+ ...g.via ? { via: g.via } : {},
5111
+ ...best.read.checks.length > 0 ? { checks: best.read.checks } : {},
5112
+ ...best.parent ? { parent: best.parent } : {}
4575
5113
  };
4576
5114
  }
4577
5115
  }
4578
5116
  function isChainWithQuery(e) {
4579
- return ts10.isCallExpression(e) && isQueryChain(e);
5117
+ return ts11.isCallExpression(e) && isQueryChain(e);
4580
5118
  }
4581
5119
  function isDbChain(call, frame) {
4582
5120
  const root = unwrap(flattenChain(call).root);
4583
5121
  let base = root;
4584
- while (ts10.isPropertyAccessExpression(base)) base = base.expression;
4585
- return ts10.isIdentifier(base) && frame.clients.has(base.text);
5122
+ while (ts11.isPropertyAccessExpression(base)) base = base.expression;
5123
+ return ts11.isIdentifier(base) && frame.clients.has(base.text);
4586
5124
  }
4587
5125
  var IDENTITY_CALLEE = /\.auth\.|user|session|claims|auth|principal|viewer/i;
4588
5126
  function returnsIdentity(p, call, scope) {
@@ -4591,10 +5129,10 @@ function returnsIdentity(p, call, scope) {
4591
5129
  }
4592
5130
  function calleePath(e) {
4593
5131
  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)}[]`;
5132
+ if (ts11.isIdentifier(u)) return u.text;
5133
+ if (ts11.isPropertyAccessExpression(u)) return `${calleePath(u.expression)}.${u.name.text}`;
5134
+ if (ts11.isCallExpression(u)) return `${calleePath(u.expression)}()`;
5135
+ if (ts11.isElementAccessExpression(u)) return `${calleePath(u.expression)}[]`;
4598
5136
  return "";
4599
5137
  }
4600
5138
  function analyzeHandler(p, h) {
@@ -4603,6 +5141,7 @@ function analyzeHandler(p, h) {
4603
5141
  const acc = {
4604
5142
  inputs: [],
4605
5143
  authChecks: [],
5144
+ roleChecks: [],
4606
5145
  queries: [],
4607
5146
  metadataAccesses: [],
4608
5147
  visited: /* @__PURE__ */ new Set(),
@@ -4632,7 +5171,7 @@ function analyzeHandler(p, h) {
4632
5171
  };
4633
5172
  const first = fn.parameters[0];
4634
5173
  if (h.kind === "route" && first) {
4635
- if (ts10.isIdentifier(first.name)) frame.reqNames.add(first.name.text);
5174
+ if (ts11.isIdentifier(first.name)) frame.reqNames.add(first.name.text);
4636
5175
  else for (const nm of boundNames(first.name)) if (REQUEST_NAME.test(nm)) frame.reqNames.add(nm);
4637
5176
  }
4638
5177
  if (h.kind === "server_action") {
@@ -4651,7 +5190,7 @@ function analyzeHandler(p, h) {
4651
5190
  acc.authChecks.push({ ...loc2(h.node), kind: "session" });
4652
5191
  }
4653
5192
  analyzeFrame(p, frame, acc);
4654
- linkGuards(acc.reads);
5193
+ linkGuards(acc.reads, p.tables);
4655
5194
  const entry = h.kind === "route" ? `${h.method} ${h.route}` : h.kind === "page" ? `PAGE ${h.route}` : `server action ${h.route}`;
4656
5195
  const stmt = enclosingStatement(h.node);
4657
5196
  const ignores = parseIgnoreDirectives(sf, stmt.getFullStart()).map((d) => ({
@@ -4669,19 +5208,20 @@ function analyzeHandler(p, h) {
4669
5208
  authChecks: acc.authChecks,
4670
5209
  queries: acc.queries,
4671
5210
  metadataAccesses: acc.metadataAccesses,
4672
- ignores
5211
+ ignores,
5212
+ roleChecks: acc.roleChecks
4673
5213
  };
4674
5214
  }
4675
5215
  function publicSecretEnvReads(sf) {
4676
- const isProcessEnv = (e) => ts10.isPropertyAccessExpression(e) && e.name.text === "env" && ts10.isIdentifier(e.expression) && e.expression.text === "process";
5216
+ const isProcessEnv = (e) => ts11.isPropertyAccessExpression(e) && e.name.text === "env" && ts11.isIdentifier(e.expression) && e.expression.text === "process";
4677
5217
  const out = [];
4678
5218
  const visit = (n) => {
4679
- if (ts10.isPropertyAccessExpression(n) && isProcessEnv(n.expression)) {
5219
+ if (ts11.isPropertyAccessExpression(n) && isProcessEnv(n.expression)) {
4680
5220
  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)) {
5221
+ } else if (ts11.isElementAccessExpression(n) && isProcessEnv(n.expression) && ts11.isStringLiteralLike(n.argumentExpression) && PUBLIC_SECRET_ENV.test(n.argumentExpression.text)) {
4682
5222
  out.push({ name: n.argumentExpression.text, node: n });
4683
5223
  }
4684
- ts10.forEachChild(n, visit);
5224
+ ts11.forEachChild(n, visit);
4685
5225
  };
4686
5226
  visit(sf);
4687
5227
  return out;
@@ -4696,7 +5236,7 @@ function findExposures(rel, sf) {
4696
5236
  });
4697
5237
  }
4698
5238
  if (isClientComponentFile(sf)) {
4699
- for (const call of collect(sf, ts10.isCallExpression)) {
5239
+ for (const call of collect(sf, ts11.isCallExpression)) {
4700
5240
  if (!isCreateClientCall(call, sf)) continue;
4701
5241
  const c = classifyCreateClientCall(call, sf);
4702
5242
  if (c.kind === "service_role") {
@@ -4727,7 +5267,14 @@ function parseProject(rootInput, opts = {}) {
4727
5267
  }
4728
5268
  }
4729
5269
  const registry = /* @__PURE__ */ new Map();
4730
- for (const [rel, sf] of sources) registry.set(rel, analyzeModule(rel, sf));
5270
+ for (const [rel, sf] of sources) {
5271
+ try {
5272
+ registry.set(rel, analyzeModule(rel, sf));
5273
+ } catch (e) {
5274
+ sources.delete(rel);
5275
+ warnings.push(`could not analyse ${rel}: ${e instanceof Error ? e.message : String(e)}`);
5276
+ }
5277
+ }
4731
5278
  const tables = /* @__PURE__ */ new Map();
4732
5279
  for (const rel of sql) {
4733
5280
  try {
@@ -4759,12 +5306,46 @@ function parseProject(rootInput, opts = {}) {
4759
5306
  varBindings: /* @__PURE__ */ new Map(),
4760
5307
  drizzleTablesByExport,
4761
5308
  prismaModels,
5309
+ returnTaints: /* @__PURE__ */ new Map(),
5310
+ tables,
4762
5311
  warnings
4763
5312
  };
4764
5313
  const routes = [];
4765
5314
  const exposures = [];
4766
5315
  const fileIgnores = {};
4767
5316
  for (const [rel, sf] of sources) {
5317
+ try {
5318
+ analyzeFile(project, rel, sf, registry.get(rel) ?? analyzeModule(rel, sf), {
5319
+ routes,
5320
+ exposures,
5321
+ fileIgnores
5322
+ });
5323
+ } catch (e) {
5324
+ warnings.push(`could not analyse ${rel}: ${e instanceof Error ? e.message : String(e)}`);
5325
+ }
5326
+ }
5327
+ routes.sort((a, b) => a.entry.localeCompare(b.entry));
5328
+ const all = [...registry.values()];
5329
+ const schema = sqlSchemaFor(tables);
5330
+ warnings.push(...schema.warnings);
5331
+ return {
5332
+ root,
5333
+ files: [...source, ...sql],
5334
+ routes,
5335
+ clientFactories: all.flatMap((f) => f.clientFactories),
5336
+ authHelpers: all.flatMap((f) => f.authHelpers),
5337
+ tables: [...tables.values()],
5338
+ exposures,
5339
+ fileIgnores,
5340
+ warnings,
5341
+ enums: schema.enums,
5342
+ sqlFunctions: schema.sqlFunctions,
5343
+ storageBuckets: schema.storageBuckets
5344
+ };
5345
+ }
5346
+ function analyzeFile(project, rel, sf, facts, out) {
5347
+ const { routes, exposures, fileIgnores } = out;
5348
+ {
4768
5349
  const top = parseIgnoreDirectives(sf, 0).map((d) => ({
4769
5350
  ruleId: d.ruleId,
4770
5351
  reason: d.reason,
@@ -4772,7 +5353,6 @@ function parseProject(rootInput, opts = {}) {
4772
5353
  }));
4773
5354
  if (top.length > 0) fileIgnores[rel] = top;
4774
5355
  exposures.push(...findExposures(rel, sf));
4775
- const facts = registry.get(rel) ?? analyzeModule(rel, sf);
4776
5356
  const route = routeFromFile(rel);
4777
5357
  if (route) {
4778
5358
  for (const { method, exported } of routeHandlersIn(sf)) {
@@ -4825,23 +5405,6 @@ function parseProject(rootInput, opts = {}) {
4825
5405
  }
4826
5406
  }
4827
5407
  }
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
5408
  }
4846
5409
 
4847
5410
  // packages/rules/src/packs/sql-functions.ts
@@ -4885,16 +5448,16 @@ var SCOPE_COLUMNS = /* @__PURE__ */ new Set([
4885
5448
  "author_id",
4886
5449
  "profile_id"
4887
5450
  ]);
4888
- function normalizeColumn(column) {
5451
+ function normalizeColumn2(column) {
4889
5452
  return column.toLowerCase().replace(/_/g, "");
4890
5453
  }
4891
- var SCOPE_KEYS = new Set([...SCOPE_COLUMNS].map(normalizeColumn));
5454
+ var SCOPE_KEYS = new Set([...SCOPE_COLUMNS].map(normalizeColumn2));
4892
5455
  function isScopeColumn(column) {
4893
- return column !== null && SCOPE_KEYS.has(normalizeColumn(column));
5456
+ return column !== null && SCOPE_KEYS.has(normalizeColumn2(column));
4894
5457
  }
4895
5458
  function isObjectIdColumn(column) {
4896
5459
  if (column === null || isScopeColumn(column)) return false;
4897
- const c = normalizeColumn(column);
5460
+ const c = normalizeColumn2(column);
4898
5461
  return c === "id" || c === "uuid" || c === "slug" || c.endsWith("id");
4899
5462
  }
4900
5463
  function bypassesRls(kind) {
@@ -4925,7 +5488,8 @@ function handlerViews(ctx) {
4925
5488
  data,
4926
5489
  inputs: data.inputs ?? [],
4927
5490
  authenticated: kinds.length > 0,
4928
- operatorOnly: kinds.length > 0 && kinds.every((k) => k === "secret")
5491
+ operatorOnly: kinds.length > 0 && kinds.every((k) => k === "secret"),
5492
+ roleChecks: data.roleChecks ?? []
4929
5493
  };
4930
5494
  });
4931
5495
  }
@@ -4955,12 +5519,12 @@ function rlsNote(t, tableName, kind) {
4955
5519
  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
5520
  return `RLS is enabled on public.${tableName} with ${t.policies.length} polic${t.policies.length === 1 ? "y" : "ies"}, but ${bypass}.`;
4957
5521
  }
4958
- function finding(ctx, rule, partial) {
5522
+ function finding(ctx, rule, partial, severity) {
4959
5523
  return {
4960
5524
  id: ctx.nextId(),
4961
5525
  ruleId: rule.id,
4962
5526
  status: "likely",
4963
- severity: rule.severity,
5527
+ severity: severity ?? rule.severity,
4964
5528
  confidence: rule.confidence,
4965
5529
  cwe: rule.cwe,
4966
5530
  createdAt: ctx.now,
@@ -4968,6 +5532,39 @@ function finding(ctx, rule, partial) {
4968
5532
  ...partial
4969
5533
  };
4970
5534
  }
5535
+ function anonReadPolicy(t) {
5536
+ if (!t?.known || !t.rlsEnabled) return void 0;
5537
+ return t.policyDetails.find(
5538
+ (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"))
5539
+ );
5540
+ }
5541
+ function publicReadNote(p, table) {
5542
+ 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.`;
5543
+ }
5544
+ function adminOnly(ctx, h, t) {
5545
+ const check = h.roleChecks[0];
5546
+ if (!check || !t?.known || !singleTenantTable(ctx, t.table)) return void 0;
5547
+ return check;
5548
+ }
5549
+ function singleTenantTable(ctx, table) {
5550
+ const info = ctx.model.tables.find((x) => x.table === table.toLowerCase());
5551
+ if (!info || info.columns.some((c) => isScopeColumn(c))) return false;
5552
+ for (const col of info.columnInfo ?? []) {
5553
+ if (!col.references || col.nullable) continue;
5554
+ const parent = ctx.model.tables.find((x) => x.table === col.references?.table);
5555
+ if (parent?.columns.some((c) => isScopeColumn(c))) return false;
5556
+ }
5557
+ return true;
5558
+ }
5559
+ function adminOnlyNote(check, table) {
5560
+ 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.`;
5561
+ }
5562
+ function tableDataOf(ctx, table) {
5563
+ return ctx.graph.nodes.get(`table:${table}`)?.data;
5564
+ }
5565
+ function callerCheck(checks) {
5566
+ return checks?.find((c) => isScopeColumn(c.column) && !c.inputDerived);
5567
+ }
4971
5568
  function guardTiesRowToCaller(guard, table, callerFns) {
4972
5569
  const callerFilter = guard.filters.find((f) => isScopeColumn(f.column) && !f.inputDerived);
4973
5570
  if (callerFilter) {
@@ -4977,6 +5574,14 @@ function guardTiesRowToCaller(guard, table, callerFns) {
4977
5574
  why: ""
4978
5575
  };
4979
5576
  }
5577
+ const compared = callerCheck(guard.checks);
5578
+ if (compared) {
5579
+ return {
5580
+ tied: true,
5581
+ how: `its ${compared.column} compared with ${compared.valueText} in code, stopping otherwise`,
5582
+ why: ""
5583
+ };
5584
+ }
4980
5585
  if (guard.client !== "user_scoped") {
4981
5586
  return {
4982
5587
  tied: false,
@@ -5017,12 +5622,19 @@ var serviceRoleObjectAccessWithoutTenantScope = {
5017
5622
  const filters = q.filters;
5018
5623
  const idFilter = filters.find((f) => f.inputDerived && isObjectIdColumn(f.column));
5019
5624
  if (!idFilter || filters.some((f) => isScopeColumn(f.column))) continue;
5625
+ if (q.operation === "select" && callerCheck(q.ownerChecks)) continue;
5020
5626
  const guard = q.guard;
5021
- const tied = guard ? guardTiesRowToCaller(guard, v.tableData, callerFns) : null;
5627
+ const guardTable = guard?.parent ? tableDataOf(ctx, guard.table) : v.tableData;
5628
+ const tied = guard ? guardTiesRowToCaller(guard, guardTable, callerFns) : null;
5022
5629
  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}.` : "";
5630
+ 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}"`;
5631
+ 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
5632
  const tableName = v.tableData?.table ?? q.table;
5025
5633
  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.";
5634
+ const admin = adminOnly(ctx, h, v.tableData);
5635
+ const anonPolicy = q.operation === "select" ? anonReadPolicy(v.tableData) : void 0;
5636
+ const downgrade = admin || anonPolicy ? "medium" : void 0;
5637
+ const downgradeNote = (admin ? adminOnlyNote(admin, tableName) : "") + (anonPolicy ? publicReadNote(anonPolicy, tableName) : "");
5026
5638
  const path = [
5027
5639
  h.data.kind === "server_action" ? "Server action call" : "HTTP request",
5028
5640
  h.data.entry,
@@ -5034,26 +5646,40 @@ var serviceRoleObjectAccessWithoutTenantScope = {
5034
5646
  const evidence = [
5035
5647
  {
5036
5648
  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),
5649
+ 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}`,
5650
+ locations: locations(
5651
+ h.handler.location,
5652
+ v.query.location,
5653
+ v.client?.location,
5654
+ admin ? { file: admin.file, line: admin.line } : void 0,
5655
+ anonPolicy?.location
5656
+ ),
5039
5657
  data: {
5040
5658
  deterministic: false,
5041
5659
  ruleId: this.id,
5042
5660
  authenticated: h.authenticated,
5043
- query: q.text
5661
+ query: q.text,
5662
+ ...admin ? { adminOnly: true, roleCheck: admin.source } : {},
5663
+ ...anonPolicy ? { anonReadPolicy: anonPolicy.name } : {}
5044
5664
  }
5045
5665
  },
5046
5666
  { kind: "trace", summary: path.join(" -> ") }
5047
5667
  ];
5668
+ const who = admin ? "Admin-only" : h.authenticated ? "Cross-tenant" : "Unauthenticated";
5048
5669
  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
- })
5670
+ finding(
5671
+ ctx,
5672
+ this,
5673
+ {
5674
+ title: `${who} ${q.operation} on "${tableName}" via ${clientNoun(v.clientData)}${admin ? " (verify the admin check)" : ""}`,
5675
+ entrypoints: [h.data.entry],
5676
+ sources: h.inputs.map((i) => `${i.kind}:${i.name}`),
5677
+ sinks: [`supabase.${q.operation}:public.${tableName}`],
5678
+ path,
5679
+ evidence
5680
+ },
5681
+ downgrade
5682
+ )
5057
5683
  );
5058
5684
  }
5059
5685
  }
@@ -5170,7 +5796,13 @@ var rlsPolicyWithoutCallerPredicate = {
5170
5796
  ],
5171
5797
  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
5798
  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 },
5799
+ data: {
5800
+ deterministic: false,
5801
+ ruleId: this.id,
5802
+ policy: p.name,
5803
+ command: p.command,
5804
+ table: t.table
5805
+ },
5174
5806
  tail: locations(p.location)
5175
5807
  }));
5176
5808
  addReach(g, h, v, `supabase.${op}:public.${t.table}`);
@@ -5331,6 +5963,14 @@ var serviceRoleQueryWithoutAuthentication = {
5331
5963
  const v = views[0];
5332
5964
  if (!v) continue;
5333
5965
  const tables = [...new Set(views.map((x) => x.tableData?.table ?? x.data.table))];
5966
+ const anonPolicies = views.map(
5967
+ (x) => x.data.operation === "select" ? anonReadPolicy(x.tableData) : void 0
5968
+ );
5969
+ const allPublicReads = anonPolicies.every((x) => x !== void 0);
5970
+ const publicNote = allPublicReads ? views.map((x, i) => {
5971
+ const pol = anonPolicies[i];
5972
+ return pol ? publicReadNote(pol, x.tableData?.table ?? x.data.table) : "";
5973
+ }).join("") : "";
5334
5974
  const path = [
5335
5975
  h.data.kind === "server_action" ? "Server action call" : "HTTP request",
5336
5976
  h.data.entry,
@@ -5339,24 +5979,37 @@ var serviceRoleQueryWithoutAuthentication = {
5339
5979
  `public.${tables.join(", public.")}`
5340
5980
  ];
5341
5981
  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
- })
5982
+ finding(
5983
+ ctx,
5984
+ this,
5985
+ {
5986
+ title: `Unauthenticated ${views.some((v2) => v2.clientData?.kind === "direct_db") ? "database" : "service-role"} access to "${tables.join('", "')}" in ${h.data.entry}`,
5987
+ entrypoints: [h.data.entry],
5988
+ sources: h.inputs.map((i) => `${i.kind}:${i.name}`),
5989
+ sinks: views.map(
5990
+ (x) => `supabase.${x.data.operation}:public.${x.tableData?.table ?? x.data.table}`
5991
+ ),
5992
+ path,
5993
+ evidence: [
5994
+ {
5995
+ kind: "rule",
5996
+ 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}`,
5997
+ locations: locations(
5998
+ h.handler.location,
5999
+ ...views.map((x) => x.query.location),
6000
+ ...anonPolicies.map((x) => x?.location)
6001
+ ),
6002
+ data: {
6003
+ deterministic: false,
6004
+ ruleId: this.id,
6005
+ ...allPublicReads ? { anonReadPolicies: anonPolicies.map((x) => x?.name ?? "") } : {}
6006
+ }
6007
+ },
6008
+ { kind: "trace", summary: path.join(" -> ") }
6009
+ ]
6010
+ },
6011
+ allPublicReads ? "medium" : void 0
6012
+ )
5360
6013
  );
5361
6014
  }
5362
6015
  return out;
@@ -5751,8 +6404,40 @@ function runRules(rules, model, graph, opts = {}) {
5751
6404
  }
5752
6405
  }
5753
6406
  findings = applySuppressions(findings, model, now);
6407
+ findings = applyPublicTables(findings, opts.publicTables ?? [], now);
5754
6408
  return findings;
5755
6409
  }
6410
+ var PUBLIC_TABLE_RULES = /* @__PURE__ */ new Set([
6411
+ "supabase.service-role-query-without-authentication",
6412
+ "supabase.rls-policy-without-caller-predicate"
6413
+ ]);
6414
+ var READ_SINK = /^supabase\.select:public\.(.+)$/;
6415
+ function applyPublicTables(findings, publicTables, now) {
6416
+ if (publicTables.length === 0) return findings;
6417
+ const declared = new Set(publicTables.map((t) => t.toLowerCase()));
6418
+ return findings.map((f) => {
6419
+ if (f.status === "suppressed" || !PUBLIC_TABLE_RULES.has(f.ruleId)) return f;
6420
+ if (f.sinks.length === 0) return f;
6421
+ const tables = [];
6422
+ for (const sink of f.sinks) {
6423
+ const table = READ_SINK.exec(sink)?.[1];
6424
+ if (table === void 0 || !declared.has(table.toLowerCase())) return f;
6425
+ if (!tables.includes(table)) tables.push(table);
6426
+ }
6427
+ const command = f.evidence[0]?.data?.command;
6428
+ if (f.ruleId === "supabase.rls-policy-without-caller-predicate" && command !== "select") {
6429
+ return f;
6430
+ }
6431
+ return transition(f, "suppressed", {
6432
+ evidence: {
6433
+ kind: "rule",
6434
+ 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.`,
6435
+ data: { suppressed: true, ruleId: f.ruleId, publicTables: tables }
6436
+ },
6437
+ now
6438
+ });
6439
+ });
6440
+ }
5756
6441
  function applySuppressions(findings, model, now) {
5757
6442
  const byEntry = new Map(model.routes.map((h) => [h.entry, h.ignores]));
5758
6443
  return findings.map((f) => {
@@ -5783,6 +6468,8 @@ var defaultRules = [
5783
6468
  // packages/scanner/src/config.ts
5784
6469
  import { existsSync, readFileSync as readFileSync3, realpathSync, statSync } from "node:fs";
5785
6470
  import { isAbsolute, join as join4, resolve as resolve3, sep as sep2 } from "node:path";
6471
+ var MAX_PUBLIC_TABLES = 64;
6472
+ var TABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
5786
6473
  var MAX_REPO_MIGRATION_DIRS = 16;
5787
6474
  function shown2(value) {
5788
6475
  return JSON.stringify(value.length > 120 ? `${value.slice(0, 120)}...` : value);
@@ -5808,14 +6495,42 @@ function loadAuditConfig(root) {
5808
6495
  const obj = raw;
5809
6496
  const ignore = strings(obj.ignore);
5810
6497
  const migrations = strings(obj.migrations);
6498
+ const publicTables = publicTableNames(obj.publicTables);
5811
6499
  return {
5812
6500
  config: {
5813
6501
  ...ignore ? { ignore } : {},
5814
- ...migrations ? { migrations } : {}
6502
+ ...migrations ? { migrations } : {},
6503
+ ...publicTables.tables ? { publicTables: publicTables.tables } : {}
5815
6504
  },
5816
- warnings: []
6505
+ warnings: publicTables.warnings
5817
6506
  };
5818
6507
  }
6508
+ function publicTableNames(value) {
6509
+ const warnings = [];
6510
+ if (value === void 0) return { warnings };
6511
+ if (!Array.isArray(value)) {
6512
+ return { warnings: ["audit.config.json: publicTables ignored (not an array of table names)"] };
6513
+ }
6514
+ const tables = [];
6515
+ for (const entry of value) {
6516
+ if (typeof entry !== "string" || !TABLE_NAME.test(entry.trim())) {
6517
+ warnings.push(
6518
+ `audit.config.json: ignored publicTables entry ${shown2(String(entry))} (not a table name)`
6519
+ );
6520
+ continue;
6521
+ }
6522
+ const name = entry.trim().toLowerCase();
6523
+ if (tables.includes(name)) continue;
6524
+ if (tables.length >= MAX_PUBLIC_TABLES) {
6525
+ warnings.push(
6526
+ `audit.config.json: ignored publicTables entry ${shown2(entry)} (at most ${MAX_PUBLIC_TABLES} tables)`
6527
+ );
6528
+ continue;
6529
+ }
6530
+ tables.push(name);
6531
+ }
6532
+ return { tables, warnings };
6533
+ }
5819
6534
  function inside(base, p) {
5820
6535
  return p === base || p.startsWith(base.endsWith(sep2) ? base : `${base}${sep2}`);
5821
6536
  }
@@ -5881,7 +6596,28 @@ function repoMigrationDirs(root, entries) {
5881
6596
  }
5882
6597
 
5883
6598
  // packages/scanner/src/scan.ts
5884
- function summarize(model, rules) {
6599
+ var ScanError = class extends Error {
6600
+ constructor(code, path, message) {
6601
+ super(message);
6602
+ this.code = code;
6603
+ this.path = path;
6604
+ }
6605
+ code;
6606
+ path;
6607
+ name = "ScanError";
6608
+ };
6609
+ function ensureDirectory(path) {
6610
+ let isDirectory;
6611
+ try {
6612
+ isDirectory = statSync2(path).isDirectory();
6613
+ } catch (e) {
6614
+ const code = errorCode2(e);
6615
+ const why = code === "ENOENT" ? "no such file or directory" : code;
6616
+ throw new ScanError("path_not_found", path, `${path} is not a directory (${why})`);
6617
+ }
6618
+ if (!isDirectory) throw new ScanError("path_not_found", path, `${path} is not a directory`);
6619
+ }
6620
+ function summarize(model, rules, publicTables = []) {
5885
6621
  const apiTables = model.tables.filter((t) => !t.table.includes("."));
5886
6622
  return {
5887
6623
  root: model.root,
@@ -5891,10 +6627,12 @@ function summarize(model, rules) {
5891
6627
  tablesKnown: apiTables.length,
5892
6628
  tablesWithRls: apiTables.filter((t) => t.rlsEnabled).length,
5893
6629
  rules,
5894
- warnings: model.warnings
6630
+ warnings: model.warnings,
6631
+ publicTables: [...publicTables]
5895
6632
  };
5896
6633
  }
5897
6634
  function runScan(path, opts = {}) {
6635
+ ensureDirectory(path);
5898
6636
  const cfg = loadAuditConfig(path);
5899
6637
  const repoDirs = repoMigrationDirs(path, cfg.config.migrations ?? []);
5900
6638
  const sqlDirs = [...repoDirs.dirs, ...opts.sqlDirs ?? []];
@@ -5904,10 +6642,11 @@ function runScan(path, opts = {}) {
5904
6642
  ...ignore.globs.length > 0 ? { ignore: ignore.globs } : {}
5905
6643
  });
5906
6644
  const graph = buildGraph(model);
5907
- const runOpts = opts.now === void 0 ? {} : { now: opts.now };
6645
+ const publicTables = cfg.config.publicTables ?? [];
6646
+ const runOpts = { ...opts.now === void 0 ? {} : { now: opts.now }, publicTables };
5908
6647
  const findings = runRules(defaultRules, model, graph, runOpts);
5909
6648
  const coverage = summarizeCoverage(findings);
5910
- const summary = summarize(model, defaultRules.length);
6649
+ const summary = summarize(model, defaultRules.length, publicTables);
5911
6650
  return {
5912
6651
  summary: {
5913
6652
  ...summary,
@@ -5941,6 +6680,8 @@ Options:
5941
6680
  --migrations <dir> extra directory with Supabase migration SQL (repeatable)
5942
6681
  -h, --help show this help
5943
6682
 
6683
+ Exit code: 0 clean, 1 a finding reached --fail-on, 2 usage error or <path> is not a directory
6684
+
5944
6685
  Config: <path>/audit.config.json { "ignore": ["evals/**"], "migrations": ["supabase/migrations"] }
5945
6686
  Suppress a finding: // auditai:ignore <ruleId|*> -- reason (above the handler or at the top of a file)
5946
6687
  `;
@@ -5976,10 +6717,15 @@ function main(argv) {
5976
6717
  return 2;
5977
6718
  }
5978
6719
  const migrations = values.migrations.map((m) => resolve4(m));
5979
- const result = runScan(
5980
- positionals[0] ?? ".",
5981
- migrations.length > 0 ? { sqlDirs: migrations } : {}
5982
- );
6720
+ let result;
6721
+ try {
6722
+ result = runScan(positionals[0] ?? ".", migrations.length > 0 ? { sqlDirs: migrations } : {});
6723
+ } catch (e) {
6724
+ if (!(e instanceof ScanError)) throw e;
6725
+ process.stderr.write(`error: ${e.message}
6726
+ `);
6727
+ return 2;
6728
+ }
5983
6729
  process.stdout.write(
5984
6730
  values.json ? `${JSON.stringify(result, null, 2)}
5985
6731
  ` : formatScanText(result)