canship 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3144,6 +3144,69 @@ function project(value, path) {
3144
3144
  }
3145
3145
  return current;
3146
3146
  }
3147
+ var SERVER_ENV = /^(?:process\s*\.\s*env|(?:[A-Za-z_$][\w$]*\s*\.\s*)?env(?:\s*\(\s*\))?)\s*\.\s*[A-Z_][A-Z0-9_]*$/;
3148
+ var SERVER_ENV_OBJECT = /^(?:process\s*\.\s*env|(?:[A-Za-z_$][\w$]*\s*\.\s*)?env(?:\s*\(\s*\))?)$/;
3149
+ function isServerSecretExpression(expression) {
3150
+ const text = expression.trim();
3151
+ if (SERVER_ENV.test(text)) return true;
3152
+ const template = /^`[^`$]*\$\{([^{}`]{1,200})\}[^`$]*`$/.exec(text);
3153
+ return template !== null && SERVER_ENV.test(template[1].trim());
3154
+ }
3155
+ var CLAIMED_IDENTITY = new RegExp(
3156
+ "^(?:get|validate|verify|authenticate|authorize|require|check|resolve|read|load|fetch)(?:Current|Session|Authenticated|LoggedIn|Request|Auth|Api|Access|Bearer|Jwt)?(?:User|UserId|Session|SessionToken|SessionCookie|Auth|AuthToken|Token|AccessToken|Jwt|Claims|Identity|Principal|Viewer|Account|ApiKey|Key|Credentials)(?:From[A-Z]\\w*|OrThrow|OrNull)?$"
3157
+ );
3158
+ var CREDENTIAL_ID = /^(?:session|token|apiKey|key|auth|access|refresh)(?:Id|_id)$/i;
3159
+ var SIGNATURE_CHECK = /^(?:verify|verify\w*Signature|validate\w*Signature|check\w*Signature|isValid\w*Signature)$/;
3160
+ function claimedIdentityCall(expr) {
3161
+ const call = /^(?:await\s+)?(?:[A-Za-z_$][\w$]*\s*\.\s*)*([A-Za-z_$][\w$]*)\s*\(/.exec(expr);
3162
+ if (!call) return false;
3163
+ const signature = SIGNATURE_CHECK.test(call[1]);
3164
+ if (signature ? !/^await\s/.test(expr) : !CLAIMED_IDENTITY.test(call[1])) return false;
3165
+ const masked = maskJsNoise(expr);
3166
+ let depth = 1, end = call[0].length;
3167
+ for (; end < masked.length && depth > 0; end++) {
3168
+ if (masked[end] === "(") depth++;
3169
+ else if (masked[end] === ")") depth--;
3170
+ }
3171
+ if (depth !== 0 || masked.slice(end).trim() !== "") return false;
3172
+ if (signature) return true;
3173
+ const args = masked.slice(call[0].length, end - 1);
3174
+ if (/\.\s*(?:params|query|body|searchParams)\b|\bparams\b|\bid\b/.test(args)) return false;
3175
+ return ![...args.matchAll(/\b[A-Za-z_$][\w$]*(?:[a-z]Id|_id)\b/g)].some((m) => !CREDENTIAL_ID.test(m[0]));
3176
+ }
3177
+ function synchronousLocal(file, expr) {
3178
+ const name = /^([A-Za-z_$][\w$]*)\s*\(/.exec(expr)?.[1];
3179
+ if (!name) return false;
3180
+ const code = noiseMaskedOf(file);
3181
+ const escaped = name.replace(/\$/g, "\\$");
3182
+ const declared = new RegExp(`(?:^|[^\\w$])(async\\s+)?function\\s*\\*?\\s*${escaped}\\s*\\(`).exec(code);
3183
+ if (declared?.[1]) return false;
3184
+ const assigned = new RegExp(`\\b(?:const|let|var)\\s+${escaped}\\s*(?::[^=;]{0,200})?=\\s*(async\\b)?\\s*(?:function\\b|\\([^()]*\\)\\s*(?::[^=;{]{0,200})?=>|[A-Za-z_$][\\w$]*\\s*=>)`).exec(code);
3185
+ if (!declared && (!assigned || assigned[1])) return false;
3186
+ const declaration = declared ?? assigned;
3187
+ const pairedEnd = (open, left, right) => {
3188
+ let depth = 0;
3189
+ for (let i = open; i < Math.min(code.length, open + 65536); i++) {
3190
+ if (code[i] === left) depth++;
3191
+ else if (code[i] === right && --depth === 0) return i;
3192
+ }
3193
+ return -1;
3194
+ };
3195
+ let start = declaration.index + declaration[0].length;
3196
+ if (declared) start = pairedEnd(start - 1, "(", ")") + 1;
3197
+ else if (/function\b$/.test(declaration[0])) {
3198
+ const open = code.indexOf("(", start);
3199
+ start = open < 0 ? 0 : pairedEnd(open, "(", ")") + 1;
3200
+ }
3201
+ if (start === 0) return false;
3202
+ start += /^\s*(?::[\w\s.<>,[\]|?]+)?\s*/.exec(code.slice(start))[0].length;
3203
+ const close = code[start] === "{" ? pairedEnd(start, "{", "}") : -1;
3204
+ if (code[start] === "{" && close < 0) return false;
3205
+ const body = close >= 0 ? code.slice(start, close + 1) : `return ${code.slice(start).split(/[;\n]/)[0] ?? ""}`;
3206
+ if (/\b(?:Promise|then|await|new|function|async)\b|=>|\.\.\./.test(body)) return false;
3207
+ if ([...body.matchAll(/([\w$]+)\s*\(/g)].some((m) => !/^(?:if|switch)$/.test(m[1]))) return false;
3208
+ return /\breturn\s+(?:null|false|true|\{|[\w$]+\s*\?)/.test(body);
3209
+ }
3147
3210
  var limited = /* @__PURE__ */ new WeakSet();
3148
3211
  var cache2 = /* @__PURE__ */ new WeakMap();
3149
3212
  var reverseCache = /* @__PURE__ */ new WeakMap();
@@ -3340,7 +3403,7 @@ var AuthValues = class {
3340
3403
  limited.add(this.file);
3341
3404
  return { kind: "opaque" };
3342
3405
  }
3343
- if (/^process\.env\.[A-Z_][A-Z0-9_]*$/.test(expr)) return { kind: "secret" };
3406
+ if (SERVER_ENV.test(expr)) return { kind: "secret" };
3344
3407
  const template = /^`[^`$]*\$\{([^{}`]{1,200})\}[^`$]*`$/.exec(expr);
3345
3408
  if (template) return this.value(template[1], at, depth + 1).kind === "secret" ? { kind: "secret" } : { kind: "literal" };
3346
3409
  if (/^(?:(?:true|false|null|undefined)\b|\d|['"`{\[])/.test(expr)) return { kind: "literal" };
@@ -3362,7 +3425,12 @@ var AuthValues = class {
3362
3425
  if (envelope) return { kind: "envelope", field: envelope === "getUser" ? "user" : "claims" };
3363
3426
  return { kind: "identity" };
3364
3427
  }
3428
+ if (claimedIdentityCall(expr)) return /^await\s/.test(expr) || synchronousLocal(this.file, expr) ? { kind: "claimed" } : { kind: "promise" };
3365
3429
  if (/\.(?:body|query|headers|cookies|searchParams)\b|\.(?:json|text|formData)\s*\(/.test(expr)) return { kind: "input" };
3430
+ const keyed = /^([A-Za-z_$][\w$]*)\s*\.\s*get\s*\(\s*['"][\w$.-]+['"]\s*\)$/.exec(expr);
3431
+ if (keyed && this.parameters.has(keyed[1]) && !this.assignments.has(keyed[1])) {
3432
+ return { kind: "parameter", parameters: [this.parameters.get(keyed[1])] };
3433
+ }
3366
3434
  const access = /^([A-Za-z_$][\w$]*)((?:\??\.[A-Za-z_$][\w$]*)*)$/.exec(expr);
3367
3435
  if (!access) return { kind: "opaque" };
3368
3436
  const name = access[1];
@@ -3381,6 +3449,8 @@ var AuthValues = class {
3381
3449
  return { kind: "opaque" };
3382
3450
  }
3383
3451
  if (this.conditions.some((range) => range.start <= assignment.at && assignment.at <= range.end && !(range.start <= at && at <= range.end))) return { kind: "opaque" };
3452
+ const key = [...assignment.projection ?? [], ...access[2].split(/\??\./).filter(Boolean)];
3453
+ if (assignment.projection !== null && key.length === 1 && /^[A-Z_][A-Z0-9_]*$/.test(key[0]) && SERVER_ENV_OBJECT.test(assignment.expression)) return { kind: "secret" };
3384
3454
  value = this.value(assignment.expression, assignment.at, depth + 1);
3385
3455
  value = assignment.projection === null ? { kind: "opaque" } : project(value, assignment.projection);
3386
3456
  } else if (this.parameters.has(name)) value = { kind: "parameter", parameters: [this.parameters.get(name)] };
@@ -3394,6 +3464,1152 @@ function identityRequirement(value, allowParameters) {
3394
3464
  return null;
3395
3465
  }
3396
3466
 
3467
+ // src/rules/routers.ts
3468
+ var NODE_FRAMEWORKS = /* @__PURE__ */ new Set(["express", "hono", "fastify"]);
3469
+ var METHODS = "get|post|put|patch|delete|all|options|head";
3470
+ var GENERICS = String.raw`(?:<[^()]{0,300}>\s*)?`;
3471
+ var FASTIFY_HOOKS = /^(?:onRequest|preParsing|preValidation|preHandler)$/;
3472
+ var HONO_CALL = `${METHODS}|on|use|route`;
3473
+ var analysedCache = /* @__PURE__ */ new WeakMap();
3474
+ function analyse(file) {
3475
+ const cached = analysedCache.get(file);
3476
+ if (cached) return cached;
3477
+ const code = noiseMaskedOf(file);
3478
+ const pairs = delimiterPairs(code);
3479
+ const openers = /* @__PURE__ */ new Map();
3480
+ for (const [open, close] of pairs) openers.set(close, open);
3481
+ const result = { file, code, source: commentsMaskedOf(file), pairs, openers, bodies: functionBodies(code, pairs) };
3482
+ analysedCache.set(file, result);
3483
+ return result;
3484
+ }
3485
+ function argsOf(a, open, close) {
3486
+ const args = [];
3487
+ let from = open + 1;
3488
+ const push = (end) => {
3489
+ const raw = a.code.slice(from, end);
3490
+ const lead = raw.length - raw.trimStart().length;
3491
+ const text = raw.trim();
3492
+ if (text) args.push({ text, source: a.source.slice(from + lead, from + lead + text.length), at: from + lead });
3493
+ };
3494
+ for (let i = open + 1; i < close; i++) {
3495
+ const end = a.pairs.get(i);
3496
+ if (end !== void 0) {
3497
+ i = end;
3498
+ continue;
3499
+ }
3500
+ if (a.code[i] === ",") {
3501
+ push(i);
3502
+ from = i + 1;
3503
+ }
3504
+ }
3505
+ push(close);
3506
+ return args;
3507
+ }
3508
+ function flatten(a, args) {
3509
+ return args.flatMap((arg) => {
3510
+ if (!arg.text.startsWith("[")) return [arg];
3511
+ const close = a.pairs.get(arg.at);
3512
+ return close === void 0 ? [arg] : argsOf(a, arg.at, close);
3513
+ });
3514
+ }
3515
+ function scopeOf(path) {
3516
+ return /^(.*?\/)?(?:src|app|lib|server|routes|api|controllers|plugins)\//.exec(path)?.[1] ?? "";
3517
+ }
3518
+ function entriesOf(a, arg) {
3519
+ const entries = /* @__PURE__ */ new Map();
3520
+ if (!arg || a.code[arg.at] !== "{") return entries;
3521
+ const close = a.pairs.get(arg.at);
3522
+ if (close === void 0) return entries;
3523
+ for (const part of argsOf(a, arg.at, close)) {
3524
+ const key = /^(?:async\s+)?['"]?([A-Za-z_$][\w$]*)['"]?/.exec(part.source);
3525
+ if (!key) continue;
3526
+ const rest = part.text.slice(key[0].length);
3527
+ const colon = /^\s*:\s*/.exec(rest);
3528
+ if (colon) {
3529
+ const offset = key[0].length + colon[0].length;
3530
+ entries.set(key[1], { value: { text: part.text.slice(offset), source: part.source.slice(offset), at: part.at + offset }, range: null });
3531
+ } else if (/^\s*(?:<[^()]{0,200}>\s*)?\(/.test(rest)) {
3532
+ const open = part.at + key[0].length + rest.indexOf("(");
3533
+ const close2 = a.pairs.get(open);
3534
+ const brace = close2 === void 0 ? null : /^\s*(?::[^{;=]{0,200})?\{/.exec(a.code.slice(close2 + 1, close2 + 260));
3535
+ const start = close2 === void 0 || !brace ? void 0 : close2 + brace[0].length;
3536
+ const end = start === void 0 ? void 0 : a.pairs.get(start);
3537
+ entries.set(key[1], { value: null, range: start !== void 0 && end !== void 0 ? { file: a.file, start, end } : null });
3538
+ } else if (rest.trim() === "") {
3539
+ entries.set(key[1], { value: part, range: null });
3540
+ }
3541
+ }
3542
+ return entries;
3543
+ }
3544
+ function frameworkNamesOf(a) {
3545
+ const names = { express: /* @__PURE__ */ new Set(), router: /* @__PURE__ */ new Set(), hono: /* @__PURE__ */ new Set(), fastify: /* @__PURE__ */ new Set(), types: /* @__PURE__ */ new Set() };
3546
+ const named = (list, wanted, into, fallback) => {
3547
+ for (const part of list.split(",")) {
3548
+ const m = /^\s*(?:type\s+)?([A-Za-z_$][\w$]*)\s*(?:(?:as|:)\s*([A-Za-z_$][\w$]*))?\s*$/.exec(part);
3549
+ if (!m) continue;
3550
+ if (wanted.test(m[1]) && !/^\s*type\s/.test(part)) into.add(m[2] ?? m[1]);
3551
+ fallback?.(m[1]);
3552
+ }
3553
+ };
3554
+ for (const m of a.source.matchAll(/\bimport\s+(type\s+)?(?:([A-Za-z_$][\w$]*)\s*,?\s*)?(?:\{([^{}]*)\}|\*\s+as\s+([A-Za-z_$][\w$]*))?\s*from\s*['"](express|hono(?:\/tiny|\/quick)?|@hono\/zod-openapi|fastify)['"]/g)) {
3555
+ const typeOnly = Boolean(m[1]);
3556
+ if (m[5] === "express") {
3557
+ if (!typeOnly && m[2]) names.express.add(m[2]);
3558
+ if (!typeOnly && m[4]) names.express.add(m[4]);
3559
+ if (m[3]) named(m[3], /^Router$/, names.router);
3560
+ names.types.add("express");
3561
+ } else if (m[5] === "fastify") {
3562
+ if (!typeOnly && m[2]) names.fastify.add(m[2]);
3563
+ if (!typeOnly && m[3]) named(m[3], /^fastify$/, names.fastify);
3564
+ names.types.add("fastify");
3565
+ } else {
3566
+ if (!typeOnly && m[3]) named(m[3], /^(?:Hono|OpenAPIHono)$/, names.hono);
3567
+ names.types.add("hono");
3568
+ }
3569
+ }
3570
+ for (const m of a.source.matchAll(/\b(?:const|let|var)\s+(?:([A-Za-z_$][\w$]*)|\{([^{}]*)\})\s*=\s*require\s*\(\s*['"](express|hono(?:\/tiny|\/quick)?|@hono\/zod-openapi|fastify)['"]\s*\)(?!\s*\()/g)) {
3571
+ if (m[3] === "express") {
3572
+ if (m[1]) names.express.add(m[1]);
3573
+ if (m[2]) named(m[2], /^Router$/, names.router);
3574
+ } else if (m[3] === "fastify") {
3575
+ if (m[1]) names.fastify.add(m[1]);
3576
+ if (m[2]) named(m[2], /^fastify$/, names.fastify);
3577
+ } else if (m[2]) named(m[2], /^(?:Hono|OpenAPIHono)$/, names.hono);
3578
+ names.types.add(m[3] === "express" || m[3] === "fastify" ? m[3] : "hono");
3579
+ }
3580
+ for (const m of a.source.matchAll(/\brequire\s*\(\s*['"](express|fastify)['"]\s*\)/g)) names.types.add(m[1]);
3581
+ return names.types.size ? names : null;
3582
+ }
3583
+ function instancesOf(a, names) {
3584
+ const found = [];
3585
+ const add = (instance) => {
3586
+ if (!found.some((other) => other.name === instance.name)) found.push(instance);
3587
+ };
3588
+ for (const m of a.source.matchAll(/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=;]{0,100})?=\s*(?:await\s+)?(new\s+)?([A-Za-z_$][\w$]*)\s*(?:\.\s*(Router)\s*)?(<[^()]{0,300}>\s*)?\(/g)) {
3589
+ const [, name, isNew, callee, router] = m;
3590
+ if (!isNew && (names.express.has(callee) && (router === "Router" || router === void 0) || names.router.has(callee) && router === void 0)) {
3591
+ add({ name, framework: "express", prefix: "", weak: false });
3592
+ } else if (isNew && names.hono.has(callee) && router === void 0) {
3593
+ const open = m.index + m[0].length - 1;
3594
+ let close = a.pairs.get(open);
3595
+ if (close === void 0) continue;
3596
+ let prefix = "";
3597
+ const base = /^\s*\.\s*basePath\s*\(/.exec(a.code.slice(close + 1, close + 200));
3598
+ if (base) {
3599
+ const baseOpen = close + base[0].length;
3600
+ const baseClose = a.pairs.get(baseOpen);
3601
+ const arg = baseClose === void 0 ? void 0 : argsOf(a, baseOpen, baseClose)[0];
3602
+ prefix = (arg && pathOf(a, arg)) ?? "";
3603
+ if (baseClose !== void 0) close = baseClose;
3604
+ }
3605
+ add({ name, framework: "hono", prefix, weak: false, chainFrom: close });
3606
+ } else if (!isNew && names.fastify.has(callee) && router === void 0) {
3607
+ add({ name, framework: "fastify", prefix: "", weak: false });
3608
+ }
3609
+ }
3610
+ for (const m of a.source.matchAll(/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:await\s+)?require\s*\(\s*['"](express|fastify)['"]\s*\)\s*(?:\.\s*(Router)\s*)?\(/g)) {
3611
+ if (m[2] === "express" || !m[3]) add({ name: m[1], framework: m[2], prefix: "", weak: false });
3612
+ }
3613
+ const typed = [
3614
+ ["express", String.raw`(?:Express|Router|Application)\b(?!\s*[.<(])`],
3615
+ ["hono", String.raw`(?:Hono|OpenAPIHono)\b(?!\s*[.(])`],
3616
+ ["fastify", String.raw`FastifyInstance\b(?!\s*[.(])`]
3617
+ ];
3618
+ for (const [framework, type] of typed) {
3619
+ if (!names.types.has(framework)) continue;
3620
+ for (const m of a.code.matchAll(new RegExp(String.raw`([A-Za-z_$][\w$]*)\s*\??:\s*(?:[A-Za-z_$][\w$]*\s*\.\s*)?${type}`, "g"))) {
3621
+ add({ name: m[1], framework, prefix: "", weak: true });
3622
+ }
3623
+ }
3624
+ return found;
3625
+ }
3626
+ function literalOf(arg) {
3627
+ const m = /^(['"`])([^'"`$]*)\1$/.exec(arg.source);
3628
+ return m ? m[2] : null;
3629
+ }
3630
+ function constantOf(a, name) {
3631
+ const m = new RegExp(`\\bconst\\s+${name.replace(/\$/g, "\\$")}\\s*(?::\\s*string\\s*)?=\\s*(['"\`])([^'"\`$]*)\\1`).exec(a.source);
3632
+ return m ? m[2] : null;
3633
+ }
3634
+ function pathsOf(a, arg) {
3635
+ if (!arg.text.startsWith("[")) return [pathOf(a, arg)];
3636
+ const close = a.pairs.get(arg.at);
3637
+ const items = close === void 0 ? [] : argsOf(a, arg.at, close);
3638
+ return items.length ? items.map((item) => pathOf(a, item)) : [null];
3639
+ }
3640
+ function usePrefixes(a, arg) {
3641
+ if (arg.text.startsWith("[")) {
3642
+ const close = a.pairs.get(arg.at);
3643
+ const items = close === void 0 ? [] : argsOf(a, arg.at, close);
3644
+ if (items.length === 0 || !items.some((item) => /^['"`]/.test(item.source))) return null;
3645
+ return items.map((item) => pathOf(a, item)).filter((path) => path !== null);
3646
+ }
3647
+ const prefix = pathOf(a, arg);
3648
+ if (prefix !== null) return [prefix];
3649
+ if (/^['"`/]|^new\s+RegExp\b|^(?:process\s*\.\s*env|import\s*\.\s*meta\s*\.\s*env|(?:[\w$]+\s*\.\s*)?env)\s*[.[]/.test(arg.source)) return [];
3650
+ return null;
3651
+ }
3652
+ function pathOf(a, arg) {
3653
+ if (arg.text.startsWith("[")) return null;
3654
+ const literal2 = literalOf(arg);
3655
+ if (literal2 !== null) return literal2;
3656
+ if (/^[A-Za-z_$][\w$]*$/.test(arg.source)) return constantOf(a, arg.source);
3657
+ const template = /^`((?:[^`$\\]|\$\{\s*[A-Za-z_$][\w$]*\s*\})*)`$/.exec(arg.source);
3658
+ if (!template) return null;
3659
+ let unresolved = false;
3660
+ const path = template[1].replace(/\$\{\s*([A-Za-z_$][\w$]*)\s*\}/g, (_whole, name) => {
3661
+ const value = constantOf(a, name);
3662
+ if (value === null) unresolved = true;
3663
+ return value ?? "";
3664
+ });
3665
+ return unresolved ? null : path;
3666
+ }
3667
+ function joinPath(prefix, path) {
3668
+ if (!prefix || prefix === "/" || !path.startsWith("/")) return path;
3669
+ const base = prefix.replace(/\/+$/, "");
3670
+ return path === "/" ? base || "/" : `${base}${path}`;
3671
+ }
3672
+ function expressRegistrations(a, name) {
3673
+ const found = [];
3674
+ const escaped = name.replace(/\$/g, "\\$");
3675
+ const add = (paths, args, at) => {
3676
+ const callbacks = flatten(a, args);
3677
+ if (callbacks.length === 0) return;
3678
+ for (const path of paths) found.push({ path, middleware: callbacks.slice(0, -1), handler: callbacks.at(-1), handlerRange: null, at });
3679
+ };
3680
+ for (const m of a.code.matchAll(new RegExp(`(?<![\\w$.])${escaped}\\s*\\.\\s*(?:${METHODS})\\s*${GENERICS}\\(`, "g"))) {
3681
+ const open = m.index + m[0].length - 1;
3682
+ const close = a.pairs.get(open);
3683
+ if (close === void 0) continue;
3684
+ const args = argsOf(a, open, close);
3685
+ if (args.length < 2) continue;
3686
+ add(pathsOf(a, args[0]), args.slice(1), m.index);
3687
+ }
3688
+ for (const m of a.code.matchAll(new RegExp(`(?<![\\w$.])${escaped}\\s*\\.\\s*route\\s*\\(`, "g"))) {
3689
+ const open = m.index + m[0].length - 1;
3690
+ let close = a.pairs.get(open);
3691
+ if (close === void 0) continue;
3692
+ const pathArg = argsOf(a, open, close)[0];
3693
+ const paths = pathArg ? pathsOf(a, pathArg) : [null];
3694
+ for (let guard = 0; guard < 16; guard++) {
3695
+ const next = new RegExp(`^\\s*\\.\\s*(?:${METHODS})\\s*${GENERICS}\\(`).exec(a.code.slice(close + 1, close + 500));
3696
+ if (!next) break;
3697
+ const methodOpen = close + 1 + next[0].length - 1;
3698
+ const methodClose = a.pairs.get(methodOpen);
3699
+ if (methodClose === void 0) break;
3700
+ add(paths, argsOf(a, methodOpen, methodClose), close + 1 + next[0].search(/[a-z]/));
3701
+ close = methodClose;
3702
+ }
3703
+ }
3704
+ return found.sort((x, y) => x.at - y.at);
3705
+ }
3706
+ function expressUses(a, name) {
3707
+ const found = [];
3708
+ for (const m of a.code.matchAll(new RegExp(`(?<![\\w$.])${name.replace(/\$/g, "\\$")}\\s*\\.\\s*use\\s*\\(`, "g"))) {
3709
+ const open = m.index + m[0].length - 1;
3710
+ const close = a.pairs.get(open);
3711
+ if (close === void 0) continue;
3712
+ const args = argsOf(a, open, close);
3713
+ const prefixes = args[0] ? usePrefixes(a, args[0]) : null;
3714
+ const middleware = flatten(a, prefixes === null ? args : args.slice(1));
3715
+ for (const prefix of prefixes ?? [null]) found.push({ prefix, args: middleware, at: m.index });
3716
+ }
3717
+ return found.sort((x, y) => x.at - y.at);
3718
+ }
3719
+ function honoOps(a, name, chainFrom) {
3720
+ const registrations = [];
3721
+ const uses = [];
3722
+ const visit = (method, open, close, at, previous) => {
3723
+ const args = argsOf(a, open, close);
3724
+ if (method === "use") {
3725
+ const prefixes = args[0] ? usePrefixes(a, args[0]) : null;
3726
+ const middleware = flatten(a, prefixes === null ? args : args.slice(1));
3727
+ for (const prefix of prefixes ?? [null]) uses.push({ prefix, args: middleware, at });
3728
+ return previous;
3729
+ }
3730
+ if (method === "route") {
3731
+ if (args.length >= 2) uses.push({ prefix: pathOf(a, args[0]), args: [args[1]], at, mount: true });
3732
+ return previous;
3733
+ }
3734
+ let rest = method === "on" ? args.slice(1) : args;
3735
+ let paths = previous;
3736
+ if (rest[0] && /^['"`[]/.test(rest[0].source)) {
3737
+ paths = pathsOf(a, rest[0]);
3738
+ rest = rest.slice(1);
3739
+ }
3740
+ if (rest.length > 0) {
3741
+ for (const path of paths) registrations.push({ path, middleware: rest.slice(0, -1), handler: rest.at(-1), handlerRange: null, at });
3742
+ }
3743
+ return paths;
3744
+ };
3745
+ const chain = (after, previous) => {
3746
+ for (let guard = 0; guard < 64; guard++) {
3747
+ const next = new RegExp(`^\\s*\\.\\s*(${HONO_CALL})\\s*${GENERICS}\\(`).exec(a.code.slice(after + 1, after + 500));
3748
+ if (!next) return;
3749
+ const open = after + next[0].length;
3750
+ const close = a.pairs.get(open);
3751
+ if (close === void 0) return;
3752
+ previous = visit(next[1], open, close, after + 1 + next[0].search(/[a-z]/), previous);
3753
+ after = close;
3754
+ }
3755
+ };
3756
+ if (chainFrom !== void 0) chain(chainFrom, [null]);
3757
+ for (const m of a.code.matchAll(new RegExp(`(?<![\\w$.])${name.replace(/\$/g, "\\$")}\\s*\\.\\s*(${HONO_CALL})\\s*${GENERICS}\\(`, "g"))) {
3758
+ const open = m.index + m[0].length - 1;
3759
+ const close = a.pairs.get(open);
3760
+ if (close === void 0) continue;
3761
+ chain(close, visit(m[1], open, close, m.index, [null]));
3762
+ }
3763
+ registrations.sort((x, y) => x.at - y.at);
3764
+ uses.sort((x, y) => x.at - y.at);
3765
+ return { registrations, uses };
3766
+ }
3767
+ function fastifyOps(a, name) {
3768
+ const registrations = [];
3769
+ const uses = [];
3770
+ const registers = [];
3771
+ const lead = String.raw`(?<![\w$.])${name.replace(/\$/g, "\\$")}\s*\.\s*(?:withTypeProvider\s*<[^()]{0,200}>\s*\(\s*\)\s*\.\s*)?`;
3772
+ const hooksOf = (entries) => [...entries].filter(([key, entry]) => FASTIFY_HOOKS.test(key) && entry.value).flatMap(([, entry]) => flatten(a, [entry.value]));
3773
+ for (const m of a.code.matchAll(new RegExp(`${lead}(${METHODS}|route|addHook|register)\\s*${GENERICS}\\(`, "g"))) {
3774
+ const open = m.index + m[0].length - 1;
3775
+ const close = a.pairs.get(open);
3776
+ if (close === void 0) continue;
3777
+ const args = argsOf(a, open, close);
3778
+ const call = m[1];
3779
+ if (call === "addHook") {
3780
+ const hook = args[0] ? literalOf(args[0]) : null;
3781
+ if (hook && FASTIFY_HOOKS.test(hook) && args[1]) uses.push({ prefix: null, args: flatten(a, [args[1]]), at: m.index });
3782
+ } else if (call === "register") {
3783
+ if (!args[0]) continue;
3784
+ registers.push({ target: args[0], options: args[1], at: m.index });
3785
+ const ident = /^[A-Za-z_$][\w$]*$/.exec(args[0].text)?.[0];
3786
+ if (ident && packageOf(a, ident) === "@fastify/bearer-auth" && !/^false$/.test(entriesOf(a, args[1]).get("addHook")?.value?.text ?? "")) {
3787
+ uses.push({ prefix: null, args: [args[0]], at: m.index });
3788
+ }
3789
+ } else if (call === "route") {
3790
+ const entries = entriesOf(a, args[0]);
3791
+ const url = entries.get("url")?.value ?? entries.get("path")?.value;
3792
+ const handler = entries.get("handler");
3793
+ if (!handler) continue;
3794
+ registrations.push({ path: url ? pathOf(a, url) : null, middleware: hooksOf(entries), handler: handler.value, handlerRange: handler.range, at: m.index });
3795
+ } else {
3796
+ if (args.length < 2) continue;
3797
+ const path = pathOf(a, args[0]);
3798
+ const options = entriesOf(a, args[1]);
3799
+ const objectOnly = args.length === 2 && args[1].text.startsWith("{");
3800
+ const inOptions = objectOnly ? options.get("handler") : void 0;
3801
+ if (objectOnly && !inOptions) continue;
3802
+ registrations.push({
3803
+ path,
3804
+ middleware: args.length >= 3 || inOptions ? hooksOf(options) : [],
3805
+ handler: inOptions ? inOptions.value : args.at(-1),
3806
+ handlerRange: inOptions?.range ?? null,
3807
+ at: m.index
3808
+ });
3809
+ }
3810
+ }
3811
+ registrations.sort((x, y) => x.at - y.at);
3812
+ uses.sort((x, y) => x.at - y.at);
3813
+ return { registrations, uses, registers };
3814
+ }
3815
+ var pathMatches = (prefix, path, framework) => {
3816
+ if (prefix === null) return true;
3817
+ const wildcard = /\*$/.test(prefix);
3818
+ if (framework === "hono" && !wildcard) return path !== null && path.replace(/\/+$/, "") === prefix.replace(/\/+$/, "");
3819
+ const base = prefix.replace(/\/?\*$/, "");
3820
+ return base === "" || base === "/" || path !== null && (path === base || path.startsWith(base.endsWith("/") ? base : `${base}/`));
3821
+ };
3822
+ function expressionEnd(a, from) {
3823
+ for (let i = from; i < a.code.length; i++) {
3824
+ const end = a.pairs.get(i);
3825
+ if (end !== void 0) {
3826
+ i = end;
3827
+ continue;
3828
+ }
3829
+ const ch = a.code[i];
3830
+ if (ch === ";" || ch === "," || ch === ")" || ch === "]" || ch === "}") return i;
3831
+ if (ch === "\n") {
3832
+ const after = a.code.slice(i + 1, i + 40).trimStart();
3833
+ const before = a.code.slice(from, i).trimEnd();
3834
+ if (before === "" || /[=+\-*/%&|^!?:,.([{<>]$/.test(before) || /^(?:[.?+\-*/%&|^,:<>=]|=>)/.test(after)) continue;
3835
+ return i;
3836
+ }
3837
+ }
3838
+ return a.code.length;
3839
+ }
3840
+ function skipGenerics(code, from) {
3841
+ if (code[from] !== "<") return from;
3842
+ let depth = 0;
3843
+ for (let i = from; i < Math.min(code.length, from + 2e3); i++) {
3844
+ if (code[i] === "<") depth++;
3845
+ else if (code[i] === ">" && code[i - 1] !== "=" && --depth === 0) return i + 1;
3846
+ }
3847
+ return from;
3848
+ }
3849
+ function functionAt(a, from, to) {
3850
+ const body = a.bodies.find((b) => b.declaration >= from && b.start < to);
3851
+ if (body) return { file: a.file, start: body.start, end: body.end };
3852
+ const arrow = a.code.indexOf("=>", from);
3853
+ return arrow !== -1 && arrow < to ? { file: a.file, start: arrow, end: to, arrow: true } : null;
3854
+ }
3855
+ var FUNCTION_EXPRESSION = /^(?:async\b\s*)?(?:function\b|\([^()]*\)\s*(?::[^=]{0,200})?=>|[A-Za-z_$][\w$]*\s*=>)/;
3856
+ function findFunction(a, name, depth = 0) {
3857
+ if (depth > 3) return null;
3858
+ const escaped = name.replace(/\$/g, "\\$");
3859
+ for (const body of a.bodies) {
3860
+ if (declarationOf(a.code, body, a.openers)?.name === name && /^function\b/.test(a.code.slice(body.declaration, body.declaration + 8))) {
3861
+ return { file: a.file, start: body.start, end: body.end };
3862
+ }
3863
+ }
3864
+ const assigned = new RegExp(`(?:\\b(?:const|let|var)\\s+${escaped}\\s*(?::[^=;]{0,200})?|(?<![\\w$])(?:module\\s*\\.\\s*)?exports\\s*\\.\\s*${escaped}\\s*)=(?![=>])\\s*`, "g");
3865
+ for (const m of a.code.matchAll(assigned)) {
3866
+ const from = m.index + m[0].length;
3867
+ if (/^(?:await\s+)?(?:require\s*\(|import\s*\()/.test(a.code.slice(from, from + 20))) continue;
3868
+ const alias = /^([A-Za-z_$][\w$]*)\s*(?:[;,\n}]|$)/.exec(a.code.slice(from, from + 120));
3869
+ if (alias && alias[1] !== name && !/^(?:async|function)$/.test(alias[1])) {
3870
+ const target = findFunction(a, alias[1], depth + 1);
3871
+ if (target) return target;
3872
+ }
3873
+ return { file: a.file, start: from, end: expressionEnd(a, skipGenerics(a.code, from)) };
3874
+ }
3875
+ for (const m of a.code.matchAll(new RegExp(`(?<![\\w$.])(?:async\\s+)?${escaped}\\s*(?:\\(|:\\s*)`, "g"))) {
3876
+ const at = m.index + m[0].length;
3877
+ if (m[0].endsWith("(")) {
3878
+ const close = a.pairs.get(at - 1);
3879
+ if (close === void 0) continue;
3880
+ const brace = /^\s*(?::[^{;=]{0,200})?\{/.exec(a.code.slice(close + 1, close + 260));
3881
+ if (!brace) continue;
3882
+ const open = close + brace[0].length;
3883
+ const end = a.pairs.get(open);
3884
+ if (end !== void 0) return { file: a.file, start: open, end };
3885
+ continue;
3886
+ }
3887
+ if (/^(?:async\b\s*)?(?:function\b|\(|[A-Za-z_$][\w$]*\s*=>)/.test(a.code.slice(at, at + 40))) {
3888
+ return { file: a.file, start: at, end: expressionEnd(a, at) };
3889
+ }
3890
+ }
3891
+ return null;
3892
+ }
3893
+ function defaultExport(a) {
3894
+ const m = /\bexport\s+default\s+|(?<![\w$.])module\s*\.\s*exports\s*=(?!=)\s*/.exec(a.code);
3895
+ if (!m) return null;
3896
+ const from = m.index + m[0].length;
3897
+ const name = /^([A-Za-z_$][\w$]*)\s*(?:[;\n]|$)/.exec(a.code.slice(from, from + 120))?.[1];
3898
+ if (name && !/^(?:async|function|class)$/.test(name)) return findFunction(a, name);
3899
+ return functionAt(a, from, expressionEnd(a, from)) ?? { file: a.file, start: from, end: expressionEnd(a, from) };
3900
+ }
3901
+ function moduleFor(spec, file, files) {
3902
+ const direct = bindingModule(spec, file, files, scopeOf(file.path));
3903
+ if (direct || spec.startsWith(".")) return direct;
3904
+ const parts = file.path.split("/").slice(0, -1);
3905
+ for (let i = parts.length; i >= 0; i--) {
3906
+ const found = bindingModule(spec, file, files, i > 0 ? `${parts.slice(0, i).join("/")}/` : "");
3907
+ if (found) return found;
3908
+ }
3909
+ return null;
3910
+ }
3911
+ function importOf(a, local, files) {
3912
+ const resolve3 = (spec, name) => {
3913
+ const file = moduleFor(spec, a.file, files);
3914
+ return file ? { file, name } : null;
3915
+ };
3916
+ const escaped = local.replace(/\$/g, "\\$");
3917
+ const es = bindingsOf(a.file).imports.find((binding) => binding.local === local && binding.spec);
3918
+ if (es) return resolve3(es.spec, es.imported);
3919
+ const namespace = new RegExp(`\\bimport\\s+(?:${escaped}\\s*,[^;]*?|\\*\\s+as\\s+${escaped}\\s+|${escaped}\\s*,\\s*\\{[^{}]*\\}\\s*)from\\s*['"]([^'"]+)['"]`).exec(a.source);
3920
+ if (namespace) return resolve3(namespace[1], "default");
3921
+ const whole = new RegExp(`\\b(?:const|let|var)\\s+${escaped}\\s*=\\s*require\\s*\\(\\s*['"]([^'"]+)['"]\\s*\\)`).exec(a.source);
3922
+ if (whole) return resolve3(whole[1], "default");
3923
+ for (const m of a.source.matchAll(/\b(?:const|let|var)\s*\{([^{}]*)\}\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/g)) {
3924
+ for (const part of m[1].split(",")) {
3925
+ const names = /^\s*([A-Za-z_$][\w$]*)\s*(?::\s*([A-Za-z_$][\w$]*))?\s*$/.exec(part);
3926
+ if (names && (names[2] ?? names[1]) === local) return resolve3(m[2], names[1]);
3927
+ }
3928
+ }
3929
+ return null;
3930
+ }
3931
+ function packageOf(a, local) {
3932
+ const escaped = local.replace(/\$/g, "\\$");
3933
+ const es = bindingsOf(a.file).imports.find((binding) => binding.local === local);
3934
+ const cjs = new RegExp(`\\b(?:const|let|var)\\s+(?:${escaped}|\\{[^{}]*\\b${escaped}\\b[^{}]*\\})\\s*=\\s*require\\s*\\(\\s*['"]([^'"]+)['"]\\s*\\)`).exec(a.source);
3935
+ const named = new RegExp(`\\bimport\\s+${escaped}\\s*(?:,[^;]*?)?from\\s*['"]([^'"]+)['"]`).exec(a.source);
3936
+ const spec = es?.spec ?? cjs?.[1] ?? named?.[1] ?? null;
3937
+ return spec && !spec.startsWith(".") && !/^(?:[@~#]|~~|@@|\$lib)\//.test(spec) ? spec : null;
3938
+ }
3939
+ function importedFunction(target, member) {
3940
+ const ta = analyse(target.file);
3941
+ if (member) return findFunction(ta, member);
3942
+ return target.name === "default" ? defaultExport(ta) : findFunction(ta, target.name);
3943
+ }
3944
+ function exportedMember(file, name, files, depth) {
3945
+ const t = analyse(file);
3946
+ const own = findFunction(t, name);
3947
+ if (own || depth >= 3) return own;
3948
+ const escaped = name.replace(/\$/g, "\\$");
3949
+ for (const m of t.source.matchAll(/\bexport\s*\{([^{}]*)\}\s*from\s*['"]([^'"]+)['"]/g)) {
3950
+ for (const part of m[1].split(",")) {
3951
+ const names = new RegExp(`^\\s*(?:type\\s+)?([A-Za-z_$][\\w$]*)\\s*(?:as\\s+${escaped})?\\s*$`).exec(part);
3952
+ if (!names || names[1] !== name && !part.includes(" as ")) continue;
3953
+ const module = moduleFor(m[2], file, files);
3954
+ if (module) return exportedMember(module, names[1], files, depth + 1);
3955
+ }
3956
+ }
3957
+ const spreads = [
3958
+ ...[...t.source.matchAll(/\bexport\s*\*\s*from\s*['"]([^'"]+)['"]/g)].map((m) => moduleFor(m[1], file, files)),
3959
+ ...[...t.source.matchAll(/\.\.\.\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/g)].map((m) => moduleFor(m[1], file, files)),
3960
+ ...[...t.code.matchAll(/\.\.\.\s*([A-Za-z_$][\w$]*)\s*[,}]/g)].map((m) => importOf(t, m[1], files)?.file ?? null)
3961
+ ];
3962
+ for (const module of spreads) {
3963
+ const found = module && module !== file ? exportedMember(module, name, files, depth + 1) : null;
3964
+ if (found) return found;
3965
+ }
3966
+ const imported = importOf(t, name, files);
3967
+ return imported && imported.file !== file ? exportedMember(imported.file, imported.name === "default" ? name : imported.name, files, depth + 1) : null;
3968
+ }
3969
+ function calleeDefinition(file, name, member, files) {
3970
+ const a = analyse(file);
3971
+ if (member === null) {
3972
+ const local = findFunction(a, name);
3973
+ if (local) return local;
3974
+ }
3975
+ const target = importOf(a, name, files);
3976
+ if (!target || target.file === file) return null;
3977
+ if (member === null && target.name === "default") return defaultExport(analyse(target.file));
3978
+ return exportedMember(target.file, member ?? target.name, files, 0);
3979
+ }
3980
+ function handlerOf(a, arg, files, depth = 0) {
3981
+ const text = arg.text;
3982
+ if (FUNCTION_EXPRESSION.test(text)) return functionAt(a, arg.at, arg.at + text.length);
3983
+ const call = /^([A-Za-z_$][\w$.]*)\s*\(/.exec(text);
3984
+ if (call && depth < 3) {
3985
+ const open = arg.at + call[0].length - 1;
3986
+ const close = a.pairs.get(open);
3987
+ if (close === arg.at + text.length - 1) {
3988
+ const inner = argsOf(a, open, close).at(-1);
3989
+ return inner ? handlerOf(a, inner, files, depth + 1) : null;
3990
+ }
3991
+ return null;
3992
+ }
3993
+ const ident = /^([A-Za-z_$][\w$]*)$/.exec(text);
3994
+ if (ident) {
3995
+ const local = findFunction(a, ident[1]);
3996
+ if (local) return local;
3997
+ const target = importOf(a, ident[1], files);
3998
+ return target ? importedFunction(target, null) : null;
3999
+ }
4000
+ const member = /^([A-Za-z_$][\w$]*)\s*\.\s*([A-Za-z_$][\w$]*)$/.exec(text);
4001
+ if (member) {
4002
+ const target = importOf(a, member[1], files);
4003
+ return target ? importedFunction(target, member[2]) : null;
4004
+ }
4005
+ return null;
4006
+ }
4007
+ function pluginOf(a, arg, files, depth = 0) {
4008
+ if (depth > 3) return null;
4009
+ const inline = /^(?:await\s+)?(?:require|import)\s*\(\s*['"]([^'"]+)['"]\s*\)$/.exec(arg.source);
4010
+ if (inline) {
4011
+ const module = moduleFor(inline[1], a.file, files);
4012
+ return module ? exportedPlugin(analyse(module), files, depth + 1) : null;
4013
+ }
4014
+ if (FUNCTION_EXPRESSION.test(arg.text)) return functionAt(a, arg.at, arg.at + arg.text.length);
4015
+ const call = /^([A-Za-z_$][\w$.]*)\s*\(/.exec(arg.text);
4016
+ if (call) {
4017
+ const open = arg.at + call[0].length - 1;
4018
+ const close = a.pairs.get(open);
4019
+ if (close === void 0) return null;
4020
+ for (const inner of argsOf(a, open, close)) {
4021
+ const range = pluginOf(a, inner, files, depth + 1);
4022
+ if (range) return range;
4023
+ }
4024
+ return null;
4025
+ }
4026
+ const ident = /^([A-Za-z_$][\w$]*)$/.exec(arg.text);
4027
+ if (ident) {
4028
+ const local = findFunction(a, ident[1]);
4029
+ if (local) {
4030
+ const text = a.code.slice(local.start, local.end).trim();
4031
+ return /^[A-Za-z_$][\w$.]*\s*\(/.test(text) && !FUNCTION_EXPRESSION.test(text) && a.code.startsWith(text, local.start) ? pluginOf(a, { text, source: a.source.slice(local.start, local.start + text.length), at: local.start }, files, depth + 1) : local;
4032
+ }
4033
+ const target = importOf(a, ident[1], files);
4034
+ if (!target) return null;
4035
+ return target.name === "default" ? exportedPlugin(analyse(target.file), files, depth + 1) : findFunction(analyse(target.file), target.name);
4036
+ }
4037
+ return handlerOf(a, arg, files);
4038
+ }
4039
+ function exportedPlugin(t, files, depth) {
4040
+ const m = /\bexport\s+default\s+|(?<![\w$.])module\s*\.\s*exports\s*=(?!=)\s*/.exec(t.code);
4041
+ if (!m) return null;
4042
+ const from = m.index + m[0].length;
4043
+ const end = expressionEnd(t, from);
4044
+ const text = t.code.slice(from, end).trimEnd();
4045
+ return pluginOf(t, { text, source: t.source.slice(from, from + text.length), at: from }, files, depth);
4046
+ }
4047
+ var toRefs = (a, args) => args.map((arg) => ({ text: arg.source, at: arg.at, file: a.file }));
4048
+ function mountedFile(a, arg, files) {
4049
+ const inline = /^require\s*\(\s*['"]([^'"]+)['"]\s*\)$/.exec(arg.source);
4050
+ if (inline) return moduleFor(inline[1], a.file, files);
4051
+ const ident = /^([A-Za-z_$][\w$]*)$/.exec(arg.text);
4052
+ return ident ? importOf(a, ident[1], files)?.file ?? null : null;
4053
+ }
4054
+ function functionParams(range) {
4055
+ return paramNamesOf(analyse(range.file), range).filter(Boolean);
4056
+ }
4057
+ function paramNamesOf(a, range) {
4058
+ const code = a.code;
4059
+ let open;
4060
+ if (code[range.start] === "{") {
4061
+ let i = range.start - 1;
4062
+ const skip = () => {
4063
+ while (i >= 0 && /\s/.test(code[i])) i--;
4064
+ };
4065
+ skip();
4066
+ let arrow = false;
4067
+ if (code[i] === ">" && code[i - 1] === "=") {
4068
+ arrow = true;
4069
+ i -= 2;
4070
+ skip();
4071
+ }
4072
+ if (arrow && /[\w$]/.test(code[i] ?? "")) {
4073
+ const end = i + 1;
4074
+ while (i >= 0 && /[\w$]/.test(code[i])) i--;
4075
+ return [code.slice(i + 1, end)];
4076
+ }
4077
+ if (code[i] !== ")") {
4078
+ const window = code.slice(Math.max(0, i - 200), i + 1);
4079
+ const close2 = window.lastIndexOf(")");
4080
+ if (close2 === -1) return [];
4081
+ i -= window.length - 1 - close2;
4082
+ }
4083
+ open = a.openers.get(i);
4084
+ } else {
4085
+ const head = /^\s*(?:async\b\s*)?(?:function\b[^(]{0,80})?\(/.exec(code.slice(range.start, range.start + 200));
4086
+ if (head) open = range.start + head[0].length - 1;
4087
+ else {
4088
+ const single = /^\s*(?:async\s+)?([A-Za-z_$][\w$]*)\s*=>/.exec(code.slice(range.start, range.start + 200));
4089
+ return single ? [single[1]] : [];
4090
+ }
4091
+ }
4092
+ if (open === void 0) return [];
4093
+ const close = a.pairs.get(open);
4094
+ if (close === void 0) return [];
4095
+ return argsOf(a, open, close).map((arg) => /^(?:\.\.\.)?\s*([A-Za-z_$][\w$]*)/.exec(arg.text)?.[1] ?? "");
4096
+ }
4097
+ var within = (site, at) => site.range === null || site.range.start <= at && at <= site.range.end;
4098
+ var opsCache = /* @__PURE__ */ new WeakMap();
4099
+ function opsOf(site) {
4100
+ const cached = opsCache.get(site);
4101
+ if (cached) return cached;
4102
+ const { a, name } = site;
4103
+ let ops;
4104
+ if (site.framework === "express") ops = { registrations: expressRegistrations(a, name), uses: expressUses(a, name), registers: [] };
4105
+ else if (site.framework === "hono") ops = { ...honoOps(a, name, site.chainFrom), registers: [] };
4106
+ else ops = fastifyOps(a, name);
4107
+ const result = {
4108
+ registrations: ops.registrations.filter((reg) => within(site, reg.at)),
4109
+ uses: ops.uses.filter((use) => within(site, use.at)),
4110
+ registers: ops.registers.filter((reg) => within(site, reg.at))
4111
+ };
4112
+ opsCache.set(site, result);
4113
+ return result;
4114
+ }
4115
+ function generalMiddleware(site, before, files) {
4116
+ const args = opsOf(site).uses.filter((use) => !use.mount && use.at < before && (use.prefix === null || site.framework !== "hono" && use.prefix === "/" || /^\/?\*$/.test(use.prefix))).flatMap((use) => use.args).filter((arg) => site.framework !== "express" || mountedFile(site.a, arg, files) === null);
4117
+ return toRefs(site.a, args);
4118
+ }
4119
+ function passedSites(site, files) {
4120
+ const { a } = site;
4121
+ const found = [];
4122
+ const receive = (range, index, at) => {
4123
+ if (!range) return;
4124
+ const target = analyse(range.file);
4125
+ const name = paramNamesOf(target, range)[index];
4126
+ if (!name) return;
4127
+ found.push({
4128
+ a: target,
4129
+ name,
4130
+ framework: site.framework,
4131
+ range: { start: range.start, end: range.end },
4132
+ prefix: site.prefix,
4133
+ root: site.root ?? site,
4134
+ inherited: [...site.inherited, ...generalMiddleware(site, at, files)]
4135
+ });
4136
+ };
4137
+ for (const m of a.source.matchAll(/\brequire\s*\(\s*['"]([^'"]+)['"]\s*\)\s*\(/g)) {
4138
+ if (!within(site, m.index)) continue;
4139
+ const open = m.index + m[0].length - 1;
4140
+ const close = a.pairs.get(open);
4141
+ if (close === void 0) continue;
4142
+ const index = argsOf(a, open, close).findIndex((arg) => arg.text === site.name);
4143
+ const module = index === -1 ? null : moduleFor(m[1], a.file, files);
4144
+ if (module) receive(defaultExport(analyse(module)), index, m.index);
4145
+ }
4146
+ for (const m of a.code.matchAll(/(?<![\w$.])(?:([A-Za-z_$][\w$]*)\s*\.\s*)?([A-Za-z_$][\w$]*)\s*\(/g)) {
4147
+ if (!within(site, m.index) || m[1] === site.name || m[2] === "require" || m[2] === site.name) continue;
4148
+ const open = m.index + m[0].length - 1;
4149
+ const close = a.pairs.get(open);
4150
+ if (close === void 0) continue;
4151
+ const index = argsOf(a, open, close).findIndex((arg) => arg.text === site.name);
4152
+ if (index === -1) continue;
4153
+ if (m[1]) {
4154
+ const target = importOf(a, m[1], files);
4155
+ receive(target ? importedFunction(target, m[2]) : null, index, m.index);
4156
+ } else {
4157
+ const local = findFunction(a, m[2]);
4158
+ if (local) {
4159
+ receive(local, index, m.index);
4160
+ continue;
4161
+ }
4162
+ const target = importOf(a, m[2], files);
4163
+ receive(target ? importedFunction(target, null) : null, index, m.index);
4164
+ }
4165
+ }
4166
+ return found;
4167
+ }
4168
+ function normalizePath(path) {
4169
+ const parts = [];
4170
+ for (const part of path.split("/")) {
4171
+ if (part === "" || part === ".") continue;
4172
+ if (part === "..") parts.pop();
4173
+ else parts.push(part);
4174
+ }
4175
+ return parts.join("/");
4176
+ }
4177
+ function registeredSites(site, files) {
4178
+ const { a } = site;
4179
+ const found = [];
4180
+ for (const reg of opsOf(site).registers) {
4181
+ const options = entriesOf(a, reg.options);
4182
+ const prefixArg = options.get("prefix")?.value;
4183
+ const prefix = joinPath(site.prefix, (prefixArg && pathOf(a, prefixArg)) ?? "");
4184
+ const inherited = [...site.inherited, ...generalMiddleware(site, reg.at, files)];
4185
+ const child = (range, childPrefix, hooks) => {
4186
+ if (!range) return;
4187
+ const target = analyse(range.file);
4188
+ const name = paramNamesOf(target, range)[0];
4189
+ if (name) found.push({
4190
+ a: target,
4191
+ name,
4192
+ framework: "fastify",
4193
+ range: { start: range.start, end: range.end },
4194
+ prefix: childPrefix,
4195
+ root: site.root ?? site,
4196
+ inherited: [...inherited, ...hooks]
4197
+ });
4198
+ };
4199
+ const ident = /^[A-Za-z_$][\w$]*$/.exec(reg.target.text)?.[0];
4200
+ if (ident && packageOf(a, ident) === "@fastify/autoload") {
4201
+ const dirValue = options.get("dir")?.value;
4202
+ if (!dirValue) continue;
4203
+ const parts = [...dirValue.source.matchAll(/['"`]([^'"`$]+)['"`]/g)].map((m) => m[1]);
4204
+ if (parts.length === 0) continue;
4205
+ const here = a.file.path.includes("/") ? a.file.path.slice(0, a.file.path.lastIndexOf("/")) : "";
4206
+ const dir = normalizePath(`${here}/${parts.join("/")}`);
4207
+ const optionPrefix = entriesOf(a, options.get("options")?.value ?? void 0).get("prefix")?.value;
4208
+ const base = joinPath(prefix, (optionPrefix && pathOf(a, optionPrefix)) ?? "");
4209
+ const autoHooks = /^true$/.test(options.get("autoHooks")?.value?.text ?? "");
4210
+ const plugins = files.filter((f) => f.path.startsWith(`${dir}/`) && /\.[mc]?[jt]s$/.test(f.path) && !/\.d\.[mc]?ts$|\.(?:test|spec)\.[mc]?[jt]s$/.test(f.path));
4211
+ const hooksByDir = /* @__PURE__ */ new Map();
4212
+ if (autoHooks) {
4213
+ for (const file of plugins.filter((f) => /(?:^|\/)autohooks\.[mc]?[jt]s$/.test(f.path))) {
4214
+ const range = exportedPlugin(analyse(file), files, 0);
4215
+ const name = range ? paramNamesOf(analyse(file), range)[0] : void 0;
4216
+ if (!range || !name) continue;
4217
+ const hookSite = { a: analyse(file), name, framework: "fastify", range: { start: range.start, end: range.end }, prefix: "", inherited: [] };
4218
+ hooksByDir.set(file.path.slice(0, file.path.lastIndexOf("/")), generalMiddleware(hookSite, Infinity, files));
4219
+ }
4220
+ }
4221
+ for (const file of plugins) {
4222
+ if (/(?:^|\/)autohooks\.[mc]?[jt]s$/.test(file.path)) continue;
4223
+ const relative3 = file.path.slice(dir.length + 1);
4224
+ const folders = relative3.split("/").slice(0, -1);
4225
+ const hooks = [...hooksByDir].filter(([folder]) => file.path.startsWith(`${folder}/`)).flatMap(([, refs]) => refs);
4226
+ child(exportedPlugin(analyse(file), files, 0), joinPath(base, folders.length ? `/${folders.join("/")}` : "/"), hooks);
4227
+ }
4228
+ continue;
4229
+ }
4230
+ child(pluginOf(a, reg.target, files), prefix, []);
4231
+ }
4232
+ return found;
4233
+ }
4234
+ var indexCache = /* @__PURE__ */ new WeakMap();
4235
+ function serverIndex(files) {
4236
+ const cached = indexCache.get(files);
4237
+ if (cached) return cached;
4238
+ const sites = [];
4239
+ const seen = /* @__PURE__ */ new Set();
4240
+ const add = (site) => {
4241
+ const key = `${site.a.file.path}\0${site.name}\0${site.range?.start ?? -1}\0${site.prefix}`;
4242
+ if (seen.has(key)) return false;
4243
+ seen.add(key);
4244
+ sites.push(site);
4245
+ return true;
4246
+ };
4247
+ for (const file of files) {
4248
+ if (!/\.[mc]?[jt]sx?$/.test(file.path) || !/\b(?:express|hono|fastify)\b/.test(file.content)) continue;
4249
+ const a = analyse(file);
4250
+ const names = frameworkNamesOf(a);
4251
+ if (!names) continue;
4252
+ for (const instance of instancesOf(a, names)) {
4253
+ add({
4254
+ a,
4255
+ name: instance.name,
4256
+ framework: instance.framework,
4257
+ range: null,
4258
+ prefix: instance.prefix,
4259
+ inherited: [],
4260
+ weak: instance.weak,
4261
+ ...instance.chainFrom === void 0 ? {} : { chainFrom: instance.chainFrom }
4262
+ });
4263
+ }
4264
+ }
4265
+ let frontier = [...sites];
4266
+ for (let level = 0; level < 4 && frontier.length; level++) {
4267
+ const next = [];
4268
+ for (const site of frontier) {
4269
+ const children = [...passedSites(site, files), ...site.framework === "fastify" ? registeredSites(site, files) : []];
4270
+ for (const child of children) if (add(child)) next.push(child);
4271
+ }
4272
+ frontier = next;
4273
+ }
4274
+ const shadowed = (site, at) => Boolean(site.weak) && sites.some((other) => other !== site && !other.weak && other.a === site.a && other.name === site.name && other.range !== null && within(other, at));
4275
+ const byRouterFile = /* @__PURE__ */ new Map();
4276
+ const byRoot = /* @__PURE__ */ new Map();
4277
+ for (const site of sites) {
4278
+ const { a } = site;
4279
+ const { registrations, uses } = opsOf(site);
4280
+ const routes = byRouterFile.get(a.file) ?? [];
4281
+ for (const reg of registrations) {
4282
+ if (shadowed(site, reg.at)) continue;
4283
+ const handler = reg.handlerRange ?? (reg.handler ? handlerOf(a, reg.handler, files) : null);
4284
+ if (!handler) continue;
4285
+ const before = uses.filter((use) => !use.mount && use.at < reg.at && pathMatches(use.prefix, reg.path, site.framework)).flatMap((use) => use.args.filter((arg) => site.framework !== "express" || mountedFile(a, arg, files) === null));
4286
+ const url = reg.path === null ? "(dynamic path)" : joinPath(site.prefix, reg.path);
4287
+ const callbacks = flatten(a, reg.middleware);
4288
+ const root = site.root ?? site;
4289
+ const add2 = (range, middleware) => {
4290
+ const route = {
4291
+ file: range.file,
4292
+ framework: site.framework,
4293
+ url,
4294
+ scope: scopeOf(range.file.path),
4295
+ reachable: { start: range.start, end: range.end },
4296
+ middleware: [...site.inherited, ...toRefs(a, before), ...toRefs(a, middleware)],
4297
+ mounts: []
4298
+ };
4299
+ routes.push(route);
4300
+ byRoot.set(root, [...byRoot.get(root) ?? [], route]);
4301
+ };
4302
+ add2(handler, callbacks);
4303
+ callbacks.forEach((callback, index2) => {
4304
+ const range = handlerOf(a, callback, files);
4305
+ if (range) add2(range, callbacks.slice(0, index2));
4306
+ });
4307
+ }
4308
+ if (routes.length) byRouterFile.set(a.file, routes);
4309
+ }
4310
+ const localRoutes = (a, arg) => {
4311
+ const root = sites.find((other) => other.a === a && other.range === null && other.name === arg.text && !other.root);
4312
+ return root ? byRoot.get(root) : void 0;
4313
+ };
4314
+ for (const site of sites) {
4315
+ if (site.framework === "fastify") continue;
4316
+ const { a } = site;
4317
+ const uses = opsOf(site).uses;
4318
+ for (const use of uses) {
4319
+ if (site.framework === "hono" && !use.mount) continue;
4320
+ use.args.forEach((arg, index2) => {
4321
+ const target = mountedFile(a, arg, files);
4322
+ const routes = target && target !== a.file ? byRouterFile.get(target) : localRoutes(a, arg);
4323
+ if (!routes || routes.length === 0) return;
4324
+ const isMiddleware = (x) => mountedFile(a, x, files) === null && localRoutes(a, x) === void 0;
4325
+ const covers2 = (other) => site.framework === "hono" ? other.prefix === null || /\*$/.test(other.prefix) && pathMatches(other.prefix, use.prefix, "hono") : pathMatches(other.prefix, use.prefix, site.framework);
4326
+ const earlier = uses.filter((other) => !other.mount && other.at < use.at && covers2(other)).flatMap((other) => site.framework === "hono" ? other.args : other.args.filter(isMiddleware));
4327
+ const middleware = [...site.inherited, ...toRefs(a, earlier), ...toRefs(a, use.args.slice(0, index2).filter(isMiddleware))];
4328
+ const prefix = joinPath(site.prefix, use.prefix ?? "/");
4329
+ for (const route of routes) {
4330
+ route.mounts.push(middleware);
4331
+ if (prefix !== "/" && route.mounts.length === 1 && route.url.startsWith("/")) route.url = joinPath(prefix, route.url);
4332
+ }
4333
+ });
4334
+ }
4335
+ }
4336
+ const byHandlerFile = /* @__PURE__ */ new Map();
4337
+ for (const routes of byRouterFile.values()) {
4338
+ for (const route of routes) {
4339
+ const list = byHandlerFile.get(route.file) ?? [];
4340
+ list.push(route);
4341
+ byHandlerFile.set(route.file, list);
4342
+ }
4343
+ }
4344
+ const index = { byHandlerFile };
4345
+ indexCache.set(files, index);
4346
+ return index;
4347
+ }
4348
+ function nodeRoutesFor(file, files) {
4349
+ return serverIndex(files).byHandlerFile.get(file) ?? [];
4350
+ }
4351
+ function serverRoutesOf(file, files) {
4352
+ const route = routeOf(file);
4353
+ if (route) return [route];
4354
+ const actions = serverActionRoutes(file);
4355
+ return actions.length ? actions : nodeRoutesFor(file, files);
4356
+ }
4357
+ function decoratorOf(ref, receiver, name, files) {
4358
+ const a = analyse(ref.file);
4359
+ const owner = (at) => a.bodies.filter((b) => b.start < at && at < b.end).sort((x, y) => y.start - x.start)[0];
4360
+ const pattern = new RegExp(`(?<![\\w$.])${receiver.replace(/\$/g, "\\$")}\\s*\\.\\s*decorate\\s*\\(\\s*['"]${name.replace(/\$/g, "\\$")}['"]\\s*,`, "g");
4361
+ let result = null;
4362
+ for (const m of a.source.matchAll(pattern)) {
4363
+ if (m.index >= ref.at || owner(m.index) !== owner(ref.at)) continue;
4364
+ const open = a.source.indexOf("(", m.index);
4365
+ const close = a.pairs.get(open);
4366
+ const value = close === void 0 ? void 0 : argsOf(a, open, close)[1];
4367
+ result = value ? handlerOf(a, value, files) : null;
4368
+ }
4369
+ return result;
4370
+ }
4371
+ var authPluginCache = /* @__PURE__ */ new WeakMap();
4372
+ function registeredAuthPlugin(ref, pkg) {
4373
+ const a = analyse(ref.file);
4374
+ const receiver = /^([A-Za-z_$][\w$]*)\s*\./.exec(ref.text)?.[1];
4375
+ if (!receiver) return false;
4376
+ let cache3 = authPluginCache.get(ref.file);
4377
+ if (!cache3) {
4378
+ cache3 = /* @__PURE__ */ new Map();
4379
+ authPluginCache.set(ref.file, cache3);
4380
+ }
4381
+ let registers = cache3.get(receiver);
4382
+ if (!registers) {
4383
+ registers = fastifyOps(a, receiver).registers;
4384
+ cache3.set(receiver, registers);
4385
+ }
4386
+ const owner = (at) => a.bodies.filter((b) => b.start < at && at < b.end).sort((x, y) => y.start - x.start)[0];
4387
+ return registers.some((reg) => reg.at < ref.at && owner(reg.at) === owner(ref.at) && /^[A-Za-z_$][\w$]*$/.test(reg.target.text) && packageOf(a, reg.target.text) === pkg);
4388
+ }
4389
+ function authCompositionOf(ref) {
4390
+ const a = analyse(ref.file);
4391
+ const m = /^[A-Za-z_$][\w$]*\s*\.\s*auth\s*\(/.exec(ref.text);
4392
+ if (!m) return null;
4393
+ const open = ref.at + m[0].length - 1;
4394
+ const close = a.pairs.get(open);
4395
+ if (close === void 0) return null;
4396
+ const args = argsOf(a, open, close);
4397
+ if (!args[0]?.text.startsWith("[")) return null;
4398
+ const relation = entriesOf(a, args[1]).get("relation")?.value;
4399
+ return { refs: toRefs(a, flatten(a, [args[0]])), relation: relation && literalOf(relation) === "and" ? "and" : "or" };
4400
+ }
4401
+ function middlewareDefinition(ref, files) {
4402
+ const a = analyse(ref.file);
4403
+ const text = a.code.slice(ref.at, ref.at + ref.text.length);
4404
+ let inline = null;
4405
+ if (FUNCTION_EXPRESSION.test(text)) {
4406
+ inline = functionAt(a, ref.at, ref.at + text.length);
4407
+ return { name: "an inline middleware", range: null, spec: null, inline };
4408
+ }
4409
+ const call = /^[A-Za-z_$][\w$.]*\s*\(/.exec(text);
4410
+ if (call) {
4411
+ const open = ref.at + call[0].length - 1;
4412
+ const close = a.pairs.get(open);
4413
+ const last = close === void 0 ? void 0 : argsOf(a, open, close).at(-1);
4414
+ if (last && FUNCTION_EXPRESSION.test(last.text)) inline = functionAt(a, last.at, last.at + last.text.length);
4415
+ }
4416
+ const root = /^([A-Za-z_$][\w$]*)/.exec(ref.text)?.[1] ?? "";
4417
+ const member = /^[A-Za-z_$][\w$]*\s*\.\s*([A-Za-z_$][\w$]*)/.exec(ref.text)?.[1] ?? null;
4418
+ const callee = member ?? root;
4419
+ const spec = packageOf(a, root);
4420
+ if (member && (spec === null || spec === "fastify")) {
4421
+ const decorated = decoratorOf(ref, root, member, files);
4422
+ if (decorated) return { name: callee, range: decorated, spec: null, inline };
4423
+ }
4424
+ if (spec) return { name: callee, range: null, spec, inline };
4425
+ const local = member === null ? findFunction(a, root) : null;
4426
+ if (local) return { name: callee, range: local, spec: null, inline };
4427
+ const target = importOf(a, root, files);
4428
+ const range = target ? importedFunction(target, member) ?? (member ? propertyValue(analyse(target.file), member) : null) : null;
4429
+ return { name: callee, range, spec: null, inline };
4430
+ }
4431
+ function propertyValue(a, name) {
4432
+ const m = new RegExp(`(?<![\\w$.])${name.replace(/\$/g, "\\$")}\\s*:\\s*(?=[A-Za-z_$][\\w$.]*\\s*\\()`).exec(a.code);
4433
+ if (!m) return null;
4434
+ const from = m.index + m[0].length;
4435
+ return { file: a.file, start: from, end: expressionEnd(a, from) - 1 };
4436
+ }
4437
+
4438
+ // src/rules/verification.ts
4439
+ var LocalVerification = class {
4440
+ constructor(file, responseExits = false) {
4441
+ this.responseExits = responseExits;
4442
+ this.code = noiseMaskedOf(file);
4443
+ this.source = commentsMaskedOf(file);
4444
+ this.pairs = delimiterPairs(this.code);
4445
+ this.bodies = functionBodies(this.code, this.pairs);
4446
+ }
4447
+ responseExits;
4448
+ code;
4449
+ source;
4450
+ pairs;
4451
+ bodies;
4452
+ owner(at) {
4453
+ return this.bodies.filter((b) => b.start < at && at < b.end).sort((a, b) => b.start - a.start)[0];
4454
+ }
4455
+ skipSpace(at) {
4456
+ while (/\s/.test(this.code[at] ?? "")) at++;
4457
+ return at;
4458
+ }
4459
+ endOf(at, limit = this.code.length) {
4460
+ if (this.code[at] === "{") return (this.pairs.get(at) ?? limit - 1) + 1;
4461
+ for (let i = at; i < limit; i++) {
4462
+ if (/[;\n}]/.test(this.code[i])) return i;
4463
+ const end = this.pairs.get(i);
4464
+ if (end !== void 0) i = end;
4465
+ }
4466
+ return limit;
4467
+ }
4468
+ /**
4469
+ * 块的顶层有无条件退出即算退出,前面可以有记录日志等语句;
4470
+ * 嵌套分支、函数中的 return 以及无花括号 if/else/循环控制的 return 不算。
4471
+ */
4472
+ exits(from, to) {
4473
+ let at = this.skipSpace(from);
4474
+ if (this.code[at] !== "{") return /^(?:return|throw)\b|^(?:redirect|notFound)\s*\(/.test(this.code.slice(at, to));
4475
+ const end = Math.min(to, this.pairs.get(at) ?? to);
4476
+ at = this.skipSpace(at + 1);
4477
+ while (at < end) {
4478
+ const statement = this.code.slice(at, this.statementEnd(at, end));
4479
+ if (/^return\b/.test(statement) && this.responseExits) {
4480
+ if (/^return\s+(?:(?:await\s+)?(?:reply|res|response)\s*\.[\s\S]*\b(?:send|end|json|sendStatus)\s*\(|(?:c|ctx|context)\s*\.\s*(?:json|text|body|html|redirect|newResponse)\s*\(|new\s+Response\s*\(|next\s*\(\s*[^)\s])/.test(statement)) return true;
4481
+ return false;
4482
+ }
4483
+ if (/^(?:return|throw)\b|^(?:redirect|notFound)\s*\(/.test(statement)) return true;
4484
+ if (this.responseExits && /^reply\s*\.\s*(?:(?:code|status)\s*\([^)]*\)\s*\.\s*)?send\s*\(/.test(this.code.slice(at, end))) return true;
4485
+ const next = this.statementEnd(at, end);
4486
+ if (next <= at) return false;
4487
+ at = this.skipSpace(next);
4488
+ }
4489
+ return false;
4490
+ }
4491
+ /** 跳过完整语句;分支、循环和函数内的退出不提升为外层退出。 */
4492
+ statementEnd(at, limit, depth = 0) {
4493
+ if (depth >= 64) return limit;
4494
+ if (this.code[at] === "{") return (this.pairs.get(at) ?? limit - 1) + 1;
4495
+ const control = /^(if|for(?:\s+await)?|while|with|switch)\s*\(/.exec(this.code.slice(at, at + 80));
4496
+ if (control) {
4497
+ const close = this.pairs.get(at + control[0].length - 1);
4498
+ if (close === void 0) return limit;
4499
+ let end = this.statementEnd(this.skipSpace(close + 1), limit, depth + 1);
4500
+ const next = this.skipSpace(end);
4501
+ if (control[1] === "if" && /^else\b/.test(this.code.slice(next, next + 5))) {
4502
+ end = this.statementEnd(this.skipSpace(next + 4), limit, depth + 1);
4503
+ }
4504
+ return end;
4505
+ }
4506
+ if (/^(?:try|do)\b/.test(this.code.slice(at, at + 8))) return limit;
4507
+ for (let i = at; i < limit; i++) {
4508
+ if (this.code[i] === ";") return i + 1;
4509
+ const close = this.pairs.get(i);
4510
+ if (close !== void 0) {
4511
+ i = close;
4512
+ continue;
4513
+ }
4514
+ if (this.code[i] !== "\n") continue;
4515
+ const before = this.code.slice(at, i).trimEnd();
4516
+ const after = this.code.slice(i + 1, limit).trimStart();
4517
+ if (!before || /[=+\-*/%&|^!?:,.([{<>]$/.test(before)) continue;
4518
+ if (/^[.?+\-*/%&|^,:<>=([{`]/.test(after)) continue;
4519
+ if (/\b(?:const|let|var|function|async|await|new|yield)$/.test(before)) continue;
4520
+ return i + 1;
4521
+ }
4522
+ return limit;
4523
+ }
4524
+ /** 验证与使用须在同一函数,且不能依赖可选分支或被吞掉的异常。 */
4525
+ enforcedBefore(from, to) {
4526
+ const owner = this.owner(from);
4527
+ if (from >= to || owner !== this.owner(to)) return false;
4528
+ const start = owner ? owner.start + 1 : 0;
4529
+ for (let i = start; i < from; i++) {
4530
+ const conditional = /^(?:if|for|while|switch)\s*\(/.exec(this.code.slice(i, i + 32));
4531
+ if (conditional && !/[\w$]/.test(this.code[i - 1] ?? "")) {
4532
+ const close = this.pairs.get(i + conditional[0].length - 1);
4533
+ if (close !== void 0) {
4534
+ const body = this.skipSpace(close + 1);
4535
+ const end2 = this.endOf(body);
4536
+ if (from < end2 && to >= end2) return false;
4537
+ }
4538
+ }
4539
+ const end = this.pairs.get(i);
4540
+ if (end === void 0) continue;
4541
+ if (end < from) {
4542
+ i = end;
4543
+ continue;
4544
+ }
4545
+ if (end >= to) continue;
4546
+ if (this.code[i] === "(" && !/(?:\?\.|\b(?:if|for|while|switch|catch))\s*$/.test(this.code.slice(Math.max(start, i - 20), i))) continue;
4547
+ if (this.code[i] !== "{" || !/\btry\s*$/.test(this.code.slice(Math.max(start, i - 16), i))) return false;
4548
+ let next = this.skipSpace(end + 1);
4549
+ if (/^catch\b/.test(this.code.slice(next))) {
4550
+ next = this.skipSpace(next + 5);
4551
+ if (this.code[next] === "(") next = this.skipSpace((this.pairs.get(next) ?? next) + 1);
4552
+ const caughtEnd = this.endOf(next);
4553
+ if (!this.exits(next, caughtEnd)) return false;
4554
+ next = this.skipSpace(caughtEnd);
4555
+ }
4556
+ if (/^finally\b/.test(this.code.slice(next))) return false;
4557
+ }
4558
+ let boundary = start;
4559
+ for (const [open, close] of this.pairs) {
4560
+ if (this.code[open] === "{" && open < from && close > from) boundary = Math.max(boundary, open + 1);
4561
+ }
4562
+ while (boundary < from) {
4563
+ const next = this.statementEnd(this.skipSpace(boundary), this.code.length);
4564
+ if (next <= boundary || next > from) break;
4565
+ boundary = next;
4566
+ }
4567
+ return !/[?]|&&|\|\||=>/.test(this.code.slice(boundary, from));
4568
+ }
4569
+ /** 找出当前函数中最后一次赋值;未知或条件覆盖同样会使旧证明失效。 */
4570
+ assignment(name, at) {
4571
+ const escaped = name.replace(/\$/g, "\\$");
4572
+ const pattern = new RegExp(`(?<![\\w$.])(?:const\\s+|let\\s+|var\\s+)?${escaped}((?:\\s*\\.[\\w$]+|\\s*\\[[^\\]]*\\])*)\\s*(?::[^=;{}\\n]{0,100})?=(?![=>])`, "g");
4573
+ let result = null;
4574
+ for (const m of this.code.slice(0, at).matchAll(pattern)) {
4575
+ if (this.owner(m.index) !== this.owner(at)) continue;
4576
+ const from = this.skipSpace(m.index + m[0].length);
4577
+ const end = this.endOf(from);
4578
+ if (end <= at) result = { at: m.index, from, end, expression: this.code.slice(from, end).trim(), member: m[1].length > 0 };
4579
+ }
4580
+ return result;
4581
+ }
4582
+ };
4583
+ function enforcedVerification(source, pattern, synchronous = /(?!)/) {
4584
+ const fragment = new LocalVerification({ path: "middleware.ts", content: source, lines: source.split("\n"), isExampleContext: false });
4585
+ const first = fragment.code.search(/\S/);
4586
+ let body = fragment.code[first] === "{" ? { start: first, end: fragment.pairs.get(first) ?? fragment.code.length - 1 } : fragment.bodies.find((b) => b.declaration >= first);
4587
+ if (body) {
4588
+ const returned = fragment.bodies.find((b) => b.declaration > body.start && b.end < body.end && /^\s*return\s+(?:createMiddleware\s*\(\s*)?(?:async\s*)?(?:\([^)]*\)\s*)?$/.test(fragment.code.slice(body.start + 1, b.declaration)));
4589
+ if (returned) body = returned;
4590
+ }
4591
+ const inner = body ? source.slice(body.start + 1, body.end) : source;
4592
+ const content = `async function __guard__(){
4593
+ ${inner}
4594
+ __continuation__();
4595
+ }`;
4596
+ const context = new LocalVerification({ path: "middleware.ts", content, lines: content.split("\n"), isExampleContext: false }, true);
4597
+ const owner = context.bodies[0];
4598
+ let end = content.lastIndexOf("__continuation__");
4599
+ for (const m of context.code.matchAll(/(?<![\w$.])next\s*\(\s*\)/g)) {
4600
+ if (context.owner(m.index) === owner) end = Math.min(end, m.index);
4601
+ }
4602
+ const calls = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
4603
+ for (const m of context.code.matchAll(calls)) {
4604
+ if (context.owner(m.index) !== owner || !context.enforcedBefore(m.index, end)) continue;
4605
+ const close = context.pairs.get(m.index + m[0].length - 1);
4606
+ if (close === void 0 || /^[.[]|^\?\./.test(context.code.slice(context.skipSpace(close + 1)))) continue;
4607
+ const before = context.code.slice(Math.max(owner?.start ?? 0, m.index - 300), m.index).replace(/(?:[A-Za-z_$][\w$]*(?:\s*\([^()]*\))?\s*\??\.\s*)*$/, "").trimEnd();
4608
+ if (/(?:^|[^\w$.])(?:await|return)$/.test(before) || synchronous.test(m[0])) return true;
4609
+ }
4610
+ return false;
4611
+ }
4612
+
3397
4613
  // src/rules/apiauth.ts
3398
4614
  var APP_ROUTER = /(?:^|\/)app\/api\/(?:.+\/)?route\.[mc]?[jt]sx?$/;
3399
4615
  var PAGES_ROUTER = /(?:^|\/)pages\/api\/.+\.[mc]?[jt]sx?$/;
@@ -3601,11 +4817,38 @@ var AUTH_ENDPOINT_NAMES = /^(?:\/api)?\/auth\/(?:sign[-_]?in|sign[-_]?up|sign[-_
3601
4817
  var AUTH_CALLBACK = /^(?:\/api)?\/auth\/callback(?:\/[^/]+)?$/;
3602
4818
  var PROVIDER_CALLBACK = /^(?:\/api)?\/(?:auth|oauth|login|sign-?in)\/[^/]+\/callback$/;
3603
4819
  var OAUTH_HANDLER = /\bdefineOAuth\w*EventHandler\s*\(/;
4820
+ var NODE_AUTH_PATH = /\/(?:sign[-_]?(?:in|up|out)|log[-_]?(?:in|out)|register|forgot[-_]?password|reset[-_]?password|verify(?:[-_]?email)?|confirm(?:[-_]?email)?|magic[-_]?link|otp|callback|refresh(?:[-_]?token)?|(?:[\w-]*(?:password|passkey|webauthn|credentials)|oauth|sso)\/authori[sz]e|oauth\/(?:token|revoke|introspect|register)|request[-_]?token)\/?$/i;
3604
4821
  function isAuthEndpoint(route) {
3605
4822
  if (route.action !== void 0) return AUTH_ACTION_NAMES.test(route.action);
4823
+ if (NODE_FRAMEWORKS.has(route.framework)) return NODE_AUTH_PATH.test(route.url) || /\/callback(?:\/|$)/i.test(route.url);
3606
4824
  return AUTH_ENDPOINT_NAMES.test(route.url) || AUTH_CALLBACK.test(route.url) || PROVIDER_CALLBACK.test(route.url) || OAUTH_HANDLER.test(noiseMaskedOf(route.file));
3607
4825
  }
3608
4826
  var AUTH_ENFORCING_CALL = /\b(?:NextAuth|require(?:Auth|User|Session|Admin)\w*|withAuth|verifyAuth|ensureAuth|assertAuth(?:enticated)?|verifyIdToken|constructEvent)\s*\(/i;
4827
+ var WEBHOOK_VERIFIERS = [
4828
+ { spec: /^@polar-sh\/sdk\/webhooks$/, imported: "validateEvent", sync: true, instance: false },
4829
+ { spec: /^@clerk\/[\w-]+(?:\/webhooks)?$/, imported: "verifyWebhook", sync: false, instance: false },
4830
+ { spec: /^(?:svix|standardwebhooks)$/, imported: "Webhook", sync: true, instance: true }
4831
+ ];
4832
+ var webhookVerifierCache = /* @__PURE__ */ new WeakMap();
4833
+ function webhookVerifiersOf(file) {
4834
+ if (webhookVerifierCache.has(file)) return webhookVerifierCache.get(file);
4835
+ const calls = [];
4836
+ const syncCalls = [];
4837
+ for (const binding of bindingsOf(file).imports) {
4838
+ const verifier = WEBHOOK_VERIFIERS.find((v) => binding.spec && v.spec.test(binding.spec) && binding.imported === v.imported);
4839
+ if (!verifier) continue;
4840
+ const local = binding.local.replace(/\$/g, "\\$");
4841
+ const names = verifier.instance ? [...noiseMaskedOf(file).matchAll(new RegExp(`\\b(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)\\s*=\\s*new\\s+${local}\\s*\\(`, "g"))].map((m) => `${m[1].replace(/\$/g, "\\$")}\\s*\\.\\s*verify`) : [local];
4842
+ calls.push(...names);
4843
+ if (verifier.sync) syncCalls.push(...names);
4844
+ }
4845
+ const result = calls.length === 0 ? null : {
4846
+ call: new RegExp(`^(?:${calls.join("|")})\\s*\\(`),
4847
+ sync: new RegExp(`^(?:${syncCalls.length ? syncCalls.join("|") : "(?!)"})\\s*\\(`)
4848
+ };
4849
+ webhookVerifierCache.set(file, result);
4850
+ return result;
4851
+ }
3609
4852
  var AUTH_CONDITION = /\b(?:session|token|user|userid|user_id|authorization|bearer|jwt|auth|signature|CRON_SECRET|WEBHOOK_SECRET|REVALIDATE_SECRET|ADMIN_SECRET)\b|\blocals\s*\.\s*user\b|\b(?:getUser|getSession|getServerSession|currentUser|getAuth|isAuthenticated|checkAuth|verifyAuth|ensureAuth|verifyIdToken|timingSafeEqual)\s*\(/i;
3610
4853
  var ACCESS_CHAIN = String.raw`[\w$]+(?:\??\.[\w$]+)*`;
3611
4854
  var NEGATED_IDENTITY = new RegExp(`^!\\s*${ACCESS_CHAIN}(?:\\s*\\([^=]*\\))?$`);
@@ -3614,7 +4857,7 @@ var IDENTITY_MISMATCH = new RegExp(`^${ACCESS_CHAIN}\\s*!={1,2}\\s*(?!(?:null|un
3614
4857
  var COMPARED_VALUE = String.raw`(?!(?:null|undefined|true|false)\b)(?:${ACCESS_CHAIN}(?:\s*\([^()]{0,200}\))?|\`[^\`]{0,300}\`)`;
3615
4858
  var SECRET_MISMATCH = new RegExp(`^${COMPARED_VALUE}\\s*!={1,2}\\s*${COMPARED_VALUE}$`);
3616
4859
  var STOPS_REQUEST = /^(?:(?:return|throw|redirect|notFound)\b|error\s*\(\s*40[13]\b)/;
3617
- var DENIED_STATUS = /\b(?:return|throw)\b[\s\S]{0,300}\bstatus(?:Code)?\s*[:(=]\s*(?:401|403)\b|\berror\s*\(\s*40[13]\b/i;
4860
+ var DENIED_STATUS = /\b(?:return|throw)\b[\s\S]{0,300}(?:\bstatus(?:Code)?\s*[:(=]\s*|\bcode\s*\(\s*|\b\w*(?:Error|Exception|Response)\s*\(\s*|\b(?:c|ctx|context)\s*\.\s*(?:json|text|body|html|newResponse)\s*\([^;]{0,300}?,\s*)(?:401|403)\b|\berror\s*\(\s*40[13]\b/i;
3618
4861
  function closingDelimiter(source, start, open, close) {
3619
4862
  let depth = 0;
3620
4863
  for (let i = start; i < source.length; i++) {
@@ -3631,16 +4874,28 @@ function controlledStatement(source, afterCondition) {
3631
4874
  let start = afterCondition;
3632
4875
  while (/\s/.test(source[start] ?? "")) start++;
3633
4876
  if (source[start] === "{") {
3634
- const end2 = closingDelimiter(source, start, "{", "}");
3635
- return source.slice(start, end2 === null ? Math.min(source.length, start + 600) : end2 + 1);
4877
+ const end = closingDelimiter(source, start, "{", "}");
4878
+ return source.slice(start, end === null ? Math.min(source.length, start + 600) : end + 1);
3636
4879
  }
3637
- const semicolon = source.indexOf(";", start);
3638
- const end = semicolon === -1 ? Math.min(source.length, start + 400) : Math.min(semicolon + 1, start + 400);
3639
- return source.slice(start, end);
4880
+ const limit = Math.min(source.length, start + 400);
4881
+ let depth = 0;
4882
+ for (let i = start; i < limit; i++) {
4883
+ const ch = source[i];
4884
+ if ("([{".includes(ch)) depth++;
4885
+ else if (")]}".includes(ch)) depth--;
4886
+ else if (depth === 0 && ch === ";") return source.slice(start, i + 1);
4887
+ else if (depth === 0 && ch === "\n") {
4888
+ const before = source.slice(start, i).trimEnd();
4889
+ const after = source.slice(i + 1, i + 40).trimStart();
4890
+ if (before !== "" && !/[=+\-*/%&|^!?:,.([{<>]$/.test(before) && !/^(?:[.?+\-*/%&|^,:<>=)\]}]|=>)/.test(after)) return source.slice(start, i);
4891
+ }
4892
+ }
4893
+ return source.slice(start, limit);
3640
4894
  }
4895
+ var NON_IDENTITY_CHECK = /origin|referr?er|cors|rate[_-]?limit|captcha/i;
3641
4896
  function rejectsMissingIdentity(term, returnsDeniedStatus) {
3642
4897
  const rejects = NEGATED_IDENTITY.test(term) || IDENTITY_IS_EMPTY.test(term) || IDENTITY_MISMATCH.test(term);
3643
- return rejects && (AUTH_CONDITION.test(term) || returnsDeniedStatus);
4898
+ return rejects && (AUTH_CONDITION.test(term) || returnsDeniedStatus && !NON_IDENTITY_CHECK.test(term));
3644
4899
  }
3645
4900
  function hasConditionalAuthGuard(code, requireThrow = false, accept) {
3646
4901
  const starts = /\bif\s*\(/g;
@@ -3765,10 +5020,14 @@ function statementEnd2(code, start, limit, pairs) {
3765
5020
  return limit;
3766
5021
  }
3767
5022
  var unguardedCache = /* @__PURE__ */ new WeakMap();
3768
- function unguardedOpsOf(file) {
5023
+ var nodeRouteFiles = /* @__PURE__ */ new WeakSet();
5024
+ function unguardedOpsOf(file, files) {
3769
5025
  let hit = unguardedCache.get(file);
3770
5026
  if (hit === void 0) {
3771
- hit = unguardedOperations(file, findDataOps(file));
5027
+ const node = serverRoutesOf(file, files).some((route) => NODE_FRAMEWORKS.has(route.framework));
5028
+ if (node) nodeRouteFiles.add(file);
5029
+ const ops = [...findDataOps(file), ...node ? delegatedWrites(file, files) : []].sort((a, b) => a.index - b.index);
5030
+ hit = unguardedOperations(file, ops);
3772
5031
  unguardedCache.set(file, hit);
3773
5032
  }
3774
5033
  return hit;
@@ -3826,6 +5085,7 @@ function unguardedOperations(file, ops, extra, requireThrow = false, observe) {
3826
5085
  const functionStarts = new Set(bodies.map((body) => body.start));
3827
5086
  const bodiesByStart = new Map(bodies.map((body) => [body.start, body]));
3828
5087
  const localNames = localAuthNames(file);
5088
+ const webhookVerifiers = webhookVerifiersOf(file);
3829
5089
  const guardEnds = /* @__PURE__ */ new Map();
3830
5090
  const guardNames = /* @__PURE__ */ new Map();
3831
5091
  const guardRequirements = /* @__PURE__ */ new Map();
@@ -3945,7 +5205,12 @@ function unguardedOperations(file, ops, extra, requireThrow = false, observe) {
3945
5205
  dependencies.forEach((index) => required.add(index));
3946
5206
  return true;
3947
5207
  }
3948
- return !observe && value.kind === "unknown" && rejectsMissingIdentity(term, DENIED_STATUS.test(code.slice(i, end)));
5208
+ if (value.kind === "claimed") {
5209
+ const branch = code.slice(i, end);
5210
+ return rejectsMissingIdentity(term, true) && (/\b(?:throw|redirect)\b/.test(branch) || DENIED_STATUS.test(branch));
5211
+ }
5212
+ const frameworkContext = value.kind === "parameter" && nodeRouteFiles.has(file) && !/\.\s*(?:params|param|query|queries|body|headers?|cookies|raw|url|originalUrl|path|ip|ips|hostname|files?)\b/.test(subject);
5213
+ return !observe && (value.kind === "unknown" || frameworkContext) && rejectsMissingIdentity(term, DENIED_STATUS.test(code.slice(i, end)));
3949
5214
  });
3950
5215
  if (end <= owner.end && accepted) {
3951
5216
  guardRequirements.set(owner.start, [...required]);
@@ -3965,7 +5230,9 @@ function unguardedOperations(file, ops, extra, requireThrow = false, observe) {
3965
5230
  continue;
3966
5231
  }
3967
5232
  const indirectCall = i > 0 && /[\w$.]/.test(code[i - 1]) ? null : extraCalls?.exec(code.slice(i, i + 100));
3968
- const call = indirectCall ?? AUTH_ENFORCING_CALL.exec(code.slice(i, i + 100));
5233
+ const enforcing = AUTH_ENFORCING_CALL.exec(code.slice(i, i + 100));
5234
+ const verifier = enforcing?.index === 0 ? null : webhookVerifiers?.call.exec(code.slice(i, i + 100)) ?? null;
5235
+ const call = indirectCall ?? verifier ?? enforcing;
3969
5236
  const callName = call?.[0].replace(/\s*\($/, "");
3970
5237
  if (!indirectCall && call?.index === 0 && callName && localNames.has(callName)) continue;
3971
5238
  if (call?.index === 0 && !/^(?:withAuth|NextAuth)\b/i.test(call[0]) && !/\bfunction\s*$/.test(code.slice(Math.max(owner.start, i - 30), i))) {
@@ -3977,7 +5244,7 @@ function unguardedOperations(file, ops, extra, requireThrow = false, observe) {
3977
5244
  const prefix = code.slice(prefixStart, i).trim();
3978
5245
  if (/(?:&&|\|\||\?|:)\s*$/.test(code.slice(owner.start + 1, prefixStart))) continue;
3979
5246
  const awaited = /^(?:await|(?:const|let|var)\s+[\w${},:\s]+?=\s*await)(?:\s+[\w$.]+\.)?$/.test(prefix) || requireThrow && /^return(?:\s+await)?$/.test(prefix);
3980
- const synchronous = /^(?:assertAuth(?:enticated)?|constructEvent)\b/i.test(call[0]) && /^(?:(?:const|let|var)\s+[\w$]+\s*=\s*)?(?:[\w$]+\.)*$/.test(prefix);
5247
+ const synchronous = (/^(?:assertAuth(?:enticated)?|constructEvent)\b/i.test(call[0]) || verifier !== null && webhookVerifiers.sync.test(call[0])) && /^(?:(?:const|let|var)\s+[\w$]+\s*=\s*)?(?:[\w$]+\.)*$/.test(prefix);
3981
5248
  if (!awaited && !synchronous) continue;
3982
5249
  const close2 = pairs.get(i + call[0].length - 1);
3983
5250
  if (close2 !== void 0 && close2 < owner.end) {
@@ -4255,9 +5522,23 @@ function guardDefinitions(file) {
4255
5522
  for (const body of bodies) {
4256
5523
  if (body.start < until) continue;
4257
5524
  until = body.end;
4258
- const declaration = declarationOf(code, body, openers);
5525
+ let declaration = declarationOf(code, body, openers);
5526
+ let declaredAt = body.declaration;
5527
+ let curried = false;
5528
+ if (!declaration && code.startsWith("=>", body.declaration)) {
5529
+ let outer = curriedOuterArrow(code, body.declaration, openers);
5530
+ for (let level = 0; outer !== null && level < 3; level++) {
5531
+ declaration = declarationOf(code, { declaration: outer, start: body.start, end: body.end }, openers);
5532
+ if (declaration) {
5533
+ declaredAt = outer;
5534
+ curried = true;
5535
+ break;
5536
+ }
5537
+ outer = curriedOuterArrow(code, outer, openers);
5538
+ }
5539
+ }
4259
5540
  if (!declaration) continue;
4260
- const returned = /\breturn\s+(?:async\s+)?(?:function\b|(?:\([^)]{0,200}\)|[A-Za-z_$][\w$]*)\s*=>)/.exec(code.slice(body.start + 1, body.end));
5541
+ const returned = curried ? null : /\breturn\s+(?:async\s+)?(?:function\b|(?:\([^)]{0,200}\)|[A-Za-z_$][\w$]*)\s*=>)/.exec(code.slice(body.start + 1, body.end));
4261
5542
  const nestedAt = returned ? body.start + 1 + returned.index + (returned[0].includes("=>") ? returned[0].lastIndexOf("=>") : returned[0].lastIndexOf("function")) : -1;
4262
5543
  const target = byDeclaration.get(nestedAt) ?? body;
4263
5544
  let at = target.end;
@@ -4281,10 +5562,10 @@ function guardDefinitions(file) {
4281
5562
  }
4282
5563
  const definition = {
4283
5564
  file,
4284
- line: lineNumberAt(lineStartsCached(file), body.declaration),
4285
- wrapper: target !== body
5565
+ line: lineNumberAt(lineStartsCached(file), declaredAt),
5566
+ wrapper: curried || target !== body
4286
5567
  };
4287
- const exported = !declaration.exported ? null : /\bexport\s+default\s+(?:async\s+)?$/.test(code.slice(Math.max(0, body.declaration - 80), body.declaration)) ? "default" : declaration.name;
5568
+ const exported = !declaration.exported ? null : /\bexport\s+default\s+(?:async\s+)?$/.test(code.slice(Math.max(0, declaredAt - 80), declaredAt)) ? "default" : declaration.name;
4288
5569
  candidates.push({ name: declaration.name, exported, definition, at, start: target.start });
4289
5570
  }
4290
5571
  const unguarded = /* @__PURE__ */ new Set();
@@ -4311,6 +5592,26 @@ function guardDefinitions(file) {
4311
5592
  definitionsCache.set(file, result);
4312
5593
  return result;
4313
5594
  }
5595
+ function curriedOuterArrow(code, arrow, openers) {
5596
+ let i = arrow - 1;
5597
+ while (i >= 0 && /\s/.test(code[i])) i--;
5598
+ const window = code.slice(Math.max(0, arrow - 200), arrow);
5599
+ const close = window.lastIndexOf(")");
5600
+ if (close !== -1 && /^\s*(?::[^=;{()]*)?$/.test(window.slice(close + 1))) {
5601
+ const open = openers.get(arrow - window.length + close);
5602
+ if (open === void 0) return null;
5603
+ i = open - 1;
5604
+ } else {
5605
+ if (!/[\w$]/.test(code[i] ?? "")) return null;
5606
+ while (i >= 0 && /[\w$]/.test(code[i])) i--;
5607
+ }
5608
+ while (i >= 0 && /\s/.test(code[i])) i--;
5609
+ if (code.slice(i - 4, i + 1) === "async" && !/[\w$]/.test(code[i - 5] ?? "")) {
5610
+ i -= 5;
5611
+ while (i >= 0 && /\s/.test(code[i])) i--;
5612
+ }
5613
+ return code[i] === ">" && code[i - 1] === "=" ? i - 1 : null;
5614
+ }
4314
5615
  function bindingModule(spec, file, allFiles, scope) {
4315
5616
  const target = normalizeSpec(spec, file.path);
4316
5617
  if (!target) return null;
@@ -4426,25 +5727,72 @@ function delegatedGuard(file, candidate, allFiles, scope, state, depth, seen) {
4426
5727
  });
4427
5728
  return selected ? { ...candidate.definition, requiredArgs, chain: [selected, ...selected.chain ?? []] } : null;
4428
5729
  }
5730
+ var HELPER_BUDGET = 64;
5731
+ var ROUTE_BUDGET = 1024;
5732
+ var AUTH_HELPER_NAME = /auth|session|user|admin|guard|protect|verify|require|permission|role|access|owner|member|login|sign[_-]?in|token|jwt|^with[A-Z]/i;
5733
+ var EXPORT_WRAPPER = /\bexport\s+(?:const\s+(?:[A-Za-z_$][\w$]*|\{[^{}]{0,500}\})\s*(?::[^=;]{0,200})?=|default)\s*(?:await\s+)?([A-Za-z_$][\w$]*)\s*(?:<[^()]{0,200}>\s*)?\(/g;
5734
+ var AUTH_DENIAL = /\b(?:status(?:Code)?\s*[:(=]\s*|code\s*\(\s*|,\s*)40[13]\b|\b\w*(?:Unauthori[sz]ed|Forbidden)\w*|['"](?:unauthorized|forbidden)['"]|redirect\s*\(\s*['"`][^'"`]*(?:login|sign-?in|auth)/i;
5735
+ var AUTH_SIGNAL = /\b(?:session|token|apiKey|api_key|authorization|bearer|jwt|auth|credentials?|currentUser)\b|\b(?:getServerSession|getSession|getUser|getClaims|verifyIdToken|authenticate)\s*\(/i;
5736
+ function evidenceWrapper(file, name, files) {
5737
+ const definition = calleeDefinition(file, name, null, files);
5738
+ if (!definition) return null;
5739
+ const queue = [{ range: definition, depth: 0 }];
5740
+ const seen = /* @__PURE__ */ new Set();
5741
+ for (let i = 0; i < queue.length && i < 24; i++) {
5742
+ const { range, depth } = queue[i];
5743
+ const key = `${range.file.path}\0${range.start}`;
5744
+ if (seen.has(key)) continue;
5745
+ seen.add(key);
5746
+ const text = commentsMaskedOf(range.file).slice(range.start, range.end + 1);
5747
+ if (AUTH_DENIAL.test(text) && AUTH_SIGNAL.test(text)) {
5748
+ return { file: definition.file, line: lineNumberAt(lineStartsCached(definition.file), definition.start), wrapper: true };
5749
+ }
5750
+ if (depth >= 2) continue;
5751
+ const code = noiseMaskedOf(range.file).slice(range.start, range.end + 1);
5752
+ const params = new Set(functionParams({ file: range.file, start: range.start, end: range.end }));
5753
+ const pairs = delimiterPairs(code);
5754
+ for (const m of code.matchAll(/(?<![\w$.])([A-Za-z_$][\w$]*)\s*(?:<[^()]{0,200}>\s*)?\(|(?<=[(,]\s*)([A-Za-z_$][\w$]*)\s*(?=[,)])/g)) {
5755
+ const callee = m[1] ?? m[2];
5756
+ if (NOT_CALLEE.test(callee) || params.has(callee)) continue;
5757
+ if (m[1]) {
5758
+ const close = pairs.get(m.index + m[0].length - 1);
5759
+ const args = close === void 0 ? "" : code.slice(m.index + m[0].length, close);
5760
+ if ([...params].some((p) => new RegExp(`(?<![\\w$.])${p.replace(/\$/g, "\\$")}(?![\\w$])`).test(args))) continue;
5761
+ }
5762
+ const target = calleeDefinition(range.file, callee, null, files);
5763
+ if (target) queue.push({ range: target, depth: depth + 1 });
5764
+ }
5765
+ }
5766
+ return null;
5767
+ }
4429
5768
  function indirectGuardOperations(route, ctx) {
4430
- const state = { remaining: 128, truncated: false };
4431
5769
  const { called } = guardBindings(route.file);
5770
+ const wrappers = new Set([...noiseMaskedOf(route.file).matchAll(EXPORT_WRAPPER)].map((m) => m[1]));
5771
+ const rank = (name) => wrappers.has(name) ? 0 : AUTH_HELPER_NAME.test(name) ? 1 : 2;
5772
+ const ordered = [...called].sort((a, b) => rank(a) - rank(b));
4432
5773
  const names = /* @__PURE__ */ new Map();
4433
- for (const name of called) {
4434
- if (state.remaining <= 0) {
4435
- state.truncated = true;
5774
+ let total = ROUTE_BUDGET;
5775
+ let truncated = false;
5776
+ let evidenceBudget = 16;
5777
+ for (const name of ordered) {
5778
+ if (total <= 0) {
5779
+ truncated = true;
4436
5780
  break;
4437
5781
  }
5782
+ const budget = Math.min(HELPER_BUDGET, total);
5783
+ const state = { remaining: budget, truncated: false };
4438
5784
  const definition = namedGuard(route.file, name, ctx.files, route.scope, state, 0, /* @__PURE__ */ new Set());
5785
+ total -= budget - Math.max(0, state.remaining);
5786
+ if (state.truncated) truncated = true;
4439
5787
  if (definition) names.set(name, definition);
5788
+ else if (rank(name) <= 1 && evidenceBudget-- > 0) {
5789
+ const wrapper = evidenceWrapper(route.file, name, ctx.files);
5790
+ if (wrapper) names.set(name, wrapper);
5791
+ }
4440
5792
  }
4441
- if (state.truncated) ctx.reportIncomplete(
4442
- "api/db-access-without-auth",
4443
- `${route.file.path} reached the authentication resolution limit (8 hops or 128 symbols); unresolved guards were not accepted`
4444
- );
4445
5793
  const protectedOps = /* @__PURE__ */ new Map();
4446
- if (names.size === 0) return protectedOps;
4447
- unguardedOperations(route.file, unguardedOpsOf(route.file), {
5794
+ if (names.size === 0) return { ops: protectedOps, truncated };
5795
+ unguardedOperations(route.file, unguardedOpsOf(route.file, ctx.files), {
4448
5796
  guards: new Set([...names].filter(([, definition]) => !definition.wrapper).map(([name]) => name)),
4449
5797
  wrappers: new Set([...names].filter(([, definition]) => definition.wrapper).map(([name]) => name)),
4450
5798
  requirements: new Map([...names].map(([name, guard]) => [name, guard.requiredArgs ?? []])),
@@ -4453,7 +5801,63 @@ function indirectGuardOperations(route, ctx) {
4453
5801
  if (definition) protectedOps.set(index, definition);
4454
5802
  }
4455
5803
  });
4456
- return protectedOps;
5804
+ return { ops: protectedOps, truncated };
5805
+ }
5806
+ var DELEGATE_DEPTH = 2;
5807
+ var DELEGATE_BUDGET = 256;
5808
+ var NOT_CALLEE = /^(?:if|for|while|switch|catch|function|return|typeof|await|new|super|this|import|require|async|void|delete|in|of)$/;
5809
+ var directUnguardedCache = /* @__PURE__ */ new WeakMap();
5810
+ function directUnguardedOps(file) {
5811
+ let hit = directUnguardedCache.get(file);
5812
+ if (hit === void 0) {
5813
+ hit = unguardedOperations(file, findDataOps(file));
5814
+ directUnguardedCache.set(file, hit);
5815
+ }
5816
+ return hit;
5817
+ }
5818
+ var delegatedCache = /* @__PURE__ */ new WeakMap();
5819
+ function delegatedWrites(file, files, depth = 0) {
5820
+ let byDepth = delegatedCache.get(file);
5821
+ if (!byDepth) {
5822
+ byDepth = /* @__PURE__ */ new Map();
5823
+ delegatedCache.set(file, byDepth);
5824
+ }
5825
+ const cached = byDepth.get(depth);
5826
+ if (cached) return cached;
5827
+ byDepth.set(depth, []);
5828
+ const code = noiseMaskedOf(file);
5829
+ const pairs = delimiterPairs(code);
5830
+ const chains = /* @__PURE__ */ new Map();
5831
+ const hits = [];
5832
+ for (const m of code.matchAll(/(?<![\w$.])([A-Za-z_$][\w$]*)(?:\s*\??\.\s*([A-Za-z_$][\w$]*))?\s*\(/g)) {
5833
+ const name = m[1];
5834
+ const member = m[2] ?? null;
5835
+ if (NOT_CALLEE.test(name)) continue;
5836
+ if (/\bfunction\s*\*?\s*$|\b(?:get|set|static)\s+$/.test(code.slice(Math.max(0, m.index - 12), m.index))) continue;
5837
+ const close = pairs.get(m.index + m[0].length - 1);
5838
+ if (close !== void 0 && /^\s*(?::[^{;=]{0,200})?\{/.test(code.slice(close + 1, close + 220))) continue;
5839
+ const key = `${name}.${member ?? ""}`;
5840
+ let resolved = chains.get(key);
5841
+ if (resolved === void 0) {
5842
+ resolved = chains.size < DELEGATE_BUDGET ? writeChain(file, name, member, files, depth) : null;
5843
+ chains.set(key, resolved);
5844
+ }
5845
+ if (!resolved || resolved.range && resolved.range.start <= m.index && m.index <= resolved.range.end) continue;
5846
+ hits.push({ index: m.index, writes: true, via: resolved.chain });
5847
+ }
5848
+ byDepth.set(depth, hits);
5849
+ return hits;
5850
+ }
5851
+ function writeChain(file, name, member, files, depth) {
5852
+ const range = calleeDefinition(file, name, member, files);
5853
+ if (!range) return null;
5854
+ if (/^with[A-Z]/.test(member ?? name) || middlewareRejects(noiseMaskedOf(range.file).slice(range.start, range.end + 1))) return null;
5855
+ const local = range.file === file ? { start: range.start, end: range.end } : null;
5856
+ const own = operationsInRange(directUnguardedOps(range.file), range.start, range.end).find((op) => op.writes);
5857
+ if (own) return { range: local, chain: [{ file: range.file, index: own.index }] };
5858
+ if (depth + 1 >= DELEGATE_DEPTH) return null;
5859
+ const nested = operationsInRange(delegatedWrites(range.file, files, depth + 1), range.start, range.end)[0];
5860
+ return nested ? { range: local, chain: [{ file: range.file, index: nested.index }, ...nested.via] } : null;
4457
5861
  }
4458
5862
  var SUPABASE_TABLE = /\.from\(\s*['"`][^'"`]+['"`]\s*\)\s*\.?\s*(\w+)?/g;
4459
5863
  var SUPABASE_ADMIN_API = /\bauth\s*\.\s*admin\s*\.\s*(\w+)\s*\(/g;
@@ -4462,7 +5866,7 @@ var PRISMA_OP = /\b(?:prisma|db)\s*\.\s*\$?(\w+)\s*\.\s*(findMany|findFirst|find
4462
5866
  var PRISMA_RAW = /\b(?:prisma|db)\s*\.\s*\$(queryRaw|executeRaw)/g;
4463
5867
  var PRISMA_WRITES = /^(?:create|createMany|update|updateMany|upsert|delete|deleteMany)$/;
4464
5868
  var DRIZZLE_OP = /\bdb\s*\.\s*(select|insert|update|delete)\s*\(/g;
4465
- var MONGO_OP = /\.(?:deleteMany|deleteOne|updateMany|updateOne|insertMany|insertOne|findOneAndDelete|findOneAndUpdate)\s*\(/g;
5869
+ var MONGO_OP = /\.(?:deleteMany|deleteOne|updateMany|updateOne|insertMany|insertOne|findOneAndDelete|findOneAndUpdate|findOneAndReplace|replaceOne|bulkWrite|findByIdAndUpdate|findByIdAndDelete|findByIdAndRemove)\s*\(/g;
4466
5870
  var RAW_SQL = /\b(?:sql|query|execute)\s*(?:`|\(\s*['"`])\s*(select|insert|update|delete|drop|truncate)\b/gi;
4467
5871
  var RAW_SQL_WRITES = /^(?:insert|update|delete|drop|truncate)$/i;
4468
5872
  function findDataOps(file) {
@@ -4485,8 +5889,11 @@ function findDataOps(file) {
4485
5889
  push(m.index, PRISMA_WRITES.test(m[2] ?? ""));
4486
5890
  }
4487
5891
  PRISMA_RAW.lastIndex = 0;
5892
+ const source = commentsMaskedOf(file);
4488
5893
  while ((m = PRISMA_RAW.exec(code)) !== null) {
4489
- push(m.index, (m[1] ?? "") === "executeRaw");
5894
+ const statement = source.slice(m.index + m[0].length, m.index + m[0].length + 300);
5895
+ const readOnly = /^(?:Unsafe)?\s*(?:`|\(\s*['"`])\s*SELECT\b(?![^`'"]*\bINTO\b)/i.test(statement);
5896
+ push(m.index, (m[1] ?? "") === "executeRaw" && !readOnly);
4490
5897
  }
4491
5898
  DRIZZLE_OP.lastIndex = 0;
4492
5899
  while ((m = DRIZZLE_OP.exec(code)) !== null) {
@@ -4752,41 +6159,216 @@ function covers(ctx, mw, url) {
4752
6159
  );
4753
6160
  return true;
4754
6161
  }
4755
- if (config.patterns.length === 0) return false;
4756
- if (readable.length === 0) return true;
4757
- return readable.some((re) => re.test(url));
6162
+ if (config.patterns.length === 0) return false;
6163
+ if (readable.length === 0) return true;
6164
+ return readable.some((re) => re.test(url));
6165
+ }
6166
+ var matcherCache = /* @__PURE__ */ new WeakMap();
6167
+ function compiledMatchersOf(file) {
6168
+ const cached = matcherCache.get(file);
6169
+ if (cached !== void 0) return cached;
6170
+ const config = extractMatcherConfig(file);
6171
+ const readable = config.kind === "patterns" ? config.patterns.map(matcherToRegex).filter((re) => re !== null) : [];
6172
+ const compiled = { config, readable };
6173
+ matcherCache.set(file, compiled);
6174
+ return compiled;
6175
+ }
6176
+ function globalGuardFor(ctx, route) {
6177
+ const relative3 = (file) => file.path.startsWith(route.scope) ? file.path.slice(route.scope.length) : null;
6178
+ const candidates = ctx.files.filter((file) => {
6179
+ const path = relative3(file);
6180
+ if (path === null) return false;
6181
+ if (route.framework === "sveltekit") return /^src\/hooks\.server\.[mc]?[jt]s$/.test(path);
6182
+ if (route.framework === "nuxt") return /^server\/middleware\/.+\.[mc]?[jt]s$/.test(path);
6183
+ return false;
6184
+ });
6185
+ return candidates.find(containsAuthCheck)?.path ?? null;
6186
+ }
6187
+ var authCheckCache = /* @__PURE__ */ new WeakMap();
6188
+ function containsAuthCheck(file) {
6189
+ const cached = authCheckCache.get(file);
6190
+ if (cached !== void 0) return cached;
6191
+ const code = noiseMaskedOf(file);
6192
+ const found = AUTH_ENFORCING_CALL.test(code) || containsConditionalAuthCheck(code);
6193
+ authCheckCache.set(file, found);
6194
+ return found;
6195
+ }
6196
+ var SECRET_SHAPED = /['"`]([A-Za-z0-9_\-.]{32,})['"`]/g;
6197
+ var AUTH_LIBRARIES = [
6198
+ [/^passport$/, /\.\s*authenticate\s*\((?!\s*['"]anonymous['"])/],
6199
+ // credentialsRequired: false / authRequired: false 时只解析令牌、不拒绝请求。
6200
+ [/^express-jwt$/, /^(?![\s\S]*\bcredentialsRequired\s*:\s*false\b)[A-Za-z_$][\w$]*\s*\(/],
6201
+ [/^express-openid-connect$/, /^requiresAuth\s*\(/],
6202
+ [/^express-oauth2-jwt-bearer$/, /^(?![\s\S]*\bauthRequired\s*:\s*false\b)(?:auth|requiredScopes|claimCheck|claimEquals|claimIncludes)\s*\(/],
6203
+ [/^@clerk\/(?:express|clerk-sdk-node)$/, /^(?:requireAuth|ClerkExpressRequireAuth)\s*\(/],
6204
+ [/^connect-ensure-login$/, /^(?:ensureLoggedIn|[A-Za-z_$][\w$]*\s*\.\s*ensureLoggedIn)\s*\(/],
6205
+ [/^express-basic-auth$/, /^[A-Za-z_$][\w$]*\s*\(/],
6206
+ [/^hono\/jwt$/, /^jwt\s*\(/],
6207
+ [/^hono\/jwk$/, /^jwk\s*\(/],
6208
+ [/^hono\/bearer-auth$/, /^bearerAuth\s*\(/],
6209
+ [/^hono\/basic-auth$/, /^basicAuth\s*\(/],
6210
+ // 注册即为所在作用域添加 onRequest 校验钩子。
6211
+ [/^@fastify\/bearer-auth$/, /^[A-Za-z_$][\w$]*$/]
6212
+ ];
6213
+ var FASTIFY_AUTH_DECORATORS = [["@fastify/basic-auth", "basicAuth"]];
6214
+ var NON_AUTH_MIDDLEWARE = /^(?:express-session|cookie-session|cookie-parser|lusca|helmet|cors|csurf|express-rate-limit|body-parser|multer|compression|morgan|connect-flash|express-flash|connect-mongo|express-validator)$/;
6215
+ var DENIED_RESPONSE = /\b(?:sendStatus|status|code)\s*\(\s*40[13]\b|\bstatus(?:Code)?\s*:\s*40[13]\b|,\s*40[13]\s*\)|\bHTTPException\s*\(\s*40[13]\b/;
6216
+ var DENIES_REQUEST = new RegExp(
6217
+ String.raw`\b(?:res(?:ponse)?|c|ctx)\s*\.\s*redirect\s*\(|${DENIED_RESPONSE.source}|\bnext\s*\(\s*(?:new\s+)?[\w$.]*(?:Unauthori[sz]ed|Forbidden|Auth)\w*`
6218
+ );
6219
+ function passesOnlyAuthenticated(code) {
6220
+ for (const m of code.matchAll(/\bif\s*\(/g)) {
6221
+ const open = m.index + m[0].length - 1;
6222
+ const close = closingDelimiter(code, open, "(", ")");
6223
+ if (close === null) continue;
6224
+ const condition = code.slice(open + 1, close);
6225
+ if (condition.trimStart().startsWith("!") || !AUTH_CONDITION.test(condition)) continue;
6226
+ const rest = code.slice(close + 1);
6227
+ const pass = /^\s*\{?\s*(?:return\s+)?(?:await\s+)?next\s*\(\s*\)\s*;?\s*(?:return\s*;?\s*)?\}?/.exec(rest);
6228
+ if (pass && DENIES_REQUEST.test(rest.slice(pass[0].length))) return true;
6229
+ }
6230
+ return false;
6231
+ }
6232
+ function maskUnusedFunctions(code) {
6233
+ let out = code;
6234
+ const definitions = /\b(?:(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=;]{0,200})?=\s*(?:async\b\s*)?(?:function\b[^(]{0,80}\([^()]*\)|\([^()]*\)\s*(?::[^=;{]{0,200})?=>|[A-Za-z_$][\w$]*\s*=>)|(?:async\s+)?function\s*\*?\s*([A-Za-z_$][\w$]*)\s*\([^()]*\))\s*/g;
6235
+ for (const m of code.matchAll(definitions)) {
6236
+ const name = m[1] ?? m[2];
6237
+ const bodyAt = m.index + m[0].length;
6238
+ let end;
6239
+ if (code[bodyAt] === "{") end = closingDelimiter(code, bodyAt, "{", "}") ?? code.length - 1;
6240
+ else {
6241
+ end = bodyAt;
6242
+ for (let depth = 0; end < code.length; end++) {
6243
+ const ch = code[end];
6244
+ if ("([{".includes(ch)) depth++;
6245
+ else if (")]}".includes(ch)) {
6246
+ if (depth === 0) break;
6247
+ depth--;
6248
+ } else if (depth === 0 && (ch === ";" || ch === "," || ch === "\n")) break;
6249
+ }
6250
+ end--;
6251
+ }
6252
+ const reference = new RegExp(`(?<![\\w$.])${name.replace(/\$/g, "\\$")}(?![\\w$])`, "g");
6253
+ const used = [...code.matchAll(reference)].some((r) => r.index < m.index || r.index > end);
6254
+ if (!used) out = out.slice(0, m.index) + out.slice(m.index, end + 1).replace(/[^\n]/g, " ") + out.slice(end + 1);
6255
+ }
6256
+ return out;
6257
+ }
6258
+ function jwtVerifyRejects(code) {
6259
+ if (!enforcedVerification(code, /\bjwtVerify\s*\(/)) return false;
6260
+ const handlers = [...code.matchAll(/\bcatch\s*(?:\([^()]*\))?\s*\{/g)].map((m) => {
6261
+ const open = m.index + m[0].length - 1;
6262
+ return code.slice(open, closingDelimiter(code, open, "{", "}") ?? open);
6263
+ });
6264
+ return handlers.every((body) => /\bsend\s*\(|\bthrow\b/.test(body) || DENIED_RESPONSE.test(body));
6265
+ }
6266
+ function rejectsSecretMismatch(code) {
6267
+ for (const m of code.matchAll(/\bif\s*\(/g)) {
6268
+ const open = m.index + m[0].length - 1;
6269
+ const close = closingDelimiter(code, open, "(", ")");
6270
+ if (close === null) continue;
6271
+ const condition = code.slice(open + 1, close);
6272
+ if (/&&|\?(?!\.)/.test(condition)) continue;
6273
+ const mismatch = condition.split("||").some((part) => {
6274
+ const sides = /^([\s\S]*?)\s*!={1,2}\s*([\s\S]*)$/.exec(part.trim());
6275
+ return sides !== null && (isServerSecretExpression(sides[1]) || isServerSecretExpression(sides[2]));
6276
+ });
6277
+ if (mismatch && DENIED_STATUS.test(controlledStatement(code, close + 1))) return true;
6278
+ }
6279
+ return false;
6280
+ }
6281
+ function catchDenies(code) {
6282
+ for (const m of code.matchAll(/\btry\s*\{/g)) {
6283
+ const tryOpen = m.index + m[0].length - 1;
6284
+ const tryClose = closingDelimiter(code, tryOpen, "{", "}");
6285
+ if (tryClose === null) continue;
6286
+ const calls = /\b\w*(?:auth|valid|verify|session|token|user|client|key|secret|permission|access)\w*\s*\(/i;
6287
+ if (!enforcedVerification(code, calls, /^(?:assertAuth(?:enticated)?|constructEvent)\b/i)) continue;
6288
+ const handler = /^\s*catch\s*(?:\([^()]*\))?\s*\{/.exec(code.slice(tryClose + 1));
6289
+ if (!handler) continue;
6290
+ const open = tryClose + handler[0].length;
6291
+ const close = closingDelimiter(code, open, "{", "}");
6292
+ if (close === null) continue;
6293
+ const body = code.slice(open + 1, close);
6294
+ const exits = [...body.matchAll(/\b(?:return|throw)\b/g)].map((exit) => exit.index);
6295
+ if (exits.length === 0) continue;
6296
+ const denies = exits.every((at, i) => {
6297
+ const statement = body.slice(at, exits[i + 1] ?? body.length);
6298
+ return statement.startsWith("throw") || DENIED_STATUS.test(statement) || /\bsend\s*\(\s*(?:err|error|e)\s*\)/.test(statement);
6299
+ });
6300
+ const pairs = delimiterPairs(body);
6301
+ let topLevelExit = false;
6302
+ for (let i = 0; i < body.length; i++) {
6303
+ const end = pairs.get(i);
6304
+ if (end !== void 0) {
6305
+ i = end;
6306
+ continue;
6307
+ }
6308
+ if (/^(?:return|throw)\b/.test(body.slice(i, i + 6)) && (i === 0 || !/[\w$.]/.test(body[i - 1]))) {
6309
+ topLevelExit = true;
6310
+ break;
6311
+ }
6312
+ }
6313
+ if (denies && topLevelExit) return true;
6314
+ }
6315
+ return false;
4758
6316
  }
4759
- var matcherCache = /* @__PURE__ */ new WeakMap();
4760
- function compiledMatchersOf(file) {
4761
- const cached = matcherCache.get(file);
4762
- if (cached !== void 0) return cached;
4763
- const config = extractMatcherConfig(file);
4764
- const readable = config.kind === "patterns" ? config.patterns.map(matcherToRegex).filter((re) => re !== null) : [];
4765
- const compiled = { config, readable };
4766
- matcherCache.set(file, compiled);
4767
- return compiled;
6317
+ var MIDDLEWARE_CREDENTIAL = /auth|secret|token|api[_-]?key|signature|session|jwt|bearer|credential|password|timingSafeEqual|verif/i;
6318
+ function rejectsMissingCredential(code) {
6319
+ return hasConditionalAuthGuard(code.replace(/!\s*await\s+/g, "!"), false, (term) => (NEGATED_IDENTITY.test(term) || IDENTITY_IS_EMPTY.test(term)) && MIDDLEWARE_CREDENTIAL.test(term));
4768
6320
  }
4769
- function globalGuardFor(ctx, route) {
4770
- const relative3 = (file) => file.path.startsWith(route.scope) ? file.path.slice(route.scope.length) : null;
4771
- const candidates = ctx.files.filter((file) => {
4772
- const path = relative3(file);
4773
- if (path === null) return false;
4774
- if (route.framework === "sveltekit") return /^src\/hooks\.server\.[mc]?[jt]s$/.test(path);
4775
- if (route.framework === "nuxt") return /^server\/middleware\/.+\.[mc]?[jt]s$/.test(path);
4776
- return false;
4777
- });
4778
- return candidates.find(containsAuthCheck)?.path ?? null;
6321
+ function middlewareRejects(source) {
6322
+ const code = maskUnusedFunctions(source);
6323
+ if (hasConditionalAuthGuard(code) || rejectsMissingCredential(code) || passesOnlyAuthenticated(code) || rejectsSecretMismatch(code) || catchDenies(code) || enforcedVerification(code, AUTH_ENFORCING_CALL, /^(?:assertAuth(?:enticated)?|constructEvent)\b/i) || jwtVerifyRejects(code)) return true;
6324
+ return false;
4779
6325
  }
4780
- var authCheckCache = /* @__PURE__ */ new WeakMap();
4781
- function containsAuthCheck(file) {
4782
- const cached = authCheckCache.get(file);
4783
- if (cached !== void 0) return cached;
4784
- const code = noiseMaskedOf(file);
4785
- const found = AUTH_ENFORCING_CALL.test(code) || containsConditionalAuthCheck(code);
4786
- authCheckCache.set(file, found);
4787
- return found;
6326
+ function middlewareStatus(ref, files, depth = 0) {
6327
+ const composition = depth < 3 ? authCompositionOf(ref) : null;
6328
+ if (composition) {
6329
+ const parts = composition.refs.map((part) => middlewareStatus(part, files, depth + 1));
6330
+ const guarded = composition.relation === "and" ? parts.some((part) => part.status === "guard") : parts.length > 0 && parts.every((part) => part.status === "guard");
6331
+ if (guarded) return { status: "guard", name: "auth" };
6332
+ const unconfirmed = parts.find((part) => part.status === "unconfirmed");
6333
+ return unconfirmed ?? { status: "none", name: "auth" };
6334
+ }
6335
+ const { name, range, spec, inline } = middlewareDefinition(ref, files);
6336
+ const known = spec ? AUTH_LIBRARIES.find(([pkg]) => pkg.test(spec)) : void 0;
6337
+ if (known && known[1].test(ref.text)) return { status: "guard", name };
6338
+ if (inline && middlewareRejects(noiseMaskedOf(inline.file).slice(inline.start, inline.end + 1))) return { status: "guard", name };
6339
+ if (spec) {
6340
+ if (known || NON_AUTH_MIDDLEWARE.test(spec)) return { status: "none", name };
6341
+ } else if (range) {
6342
+ const code = noiseMaskedOf(range.file).slice(range.start, range.end + 1);
6343
+ if (middlewareRejects(code)) return { status: "guard", name };
6344
+ const initializer = commentsMaskedOf(range.file).slice(range.start, range.end + 1).trim();
6345
+ if (initializer !== ref.text && /^[A-Za-z_$][\w$.]*\s*\(/.test(initializer) && depth < 3) {
6346
+ const inner = middlewareStatus({ text: initializer, at: range.start, file: range.file }, files, depth + 1);
6347
+ if (inner.status === "guard") return { status: "guard", name };
6348
+ }
6349
+ } else if (/^[A-Za-z_$][\w$]*\s*\.\s*[A-Za-z_$][\w$]*$/.test(ref.text)) {
6350
+ const plugin = FASTIFY_AUTH_DECORATORS.find(([pkg, decorator]) => decorator === name && registeredAuthPlugin(ref, pkg));
6351
+ if (plugin) return { status: "guard", name };
6352
+ }
6353
+ return { status: AUTH_HELPER_NAME.test(name) ? "unconfirmed" : "none", name };
6354
+ }
6355
+ function middlewareGuardStatus(route, files) {
6356
+ const unconfirmed = /* @__PURE__ */ new Set();
6357
+ const scan3 = (refs) => {
6358
+ let guarded = false;
6359
+ for (const ref of refs) {
6360
+ const { status, name } = middlewareStatus(ref, files);
6361
+ if (status === "guard") guarded = true;
6362
+ else if (status === "unconfirmed") unconfirmed.add(name);
6363
+ }
6364
+ return guarded;
6365
+ };
6366
+ if (scan3(route.middleware ?? [])) return { guarded: true, unconfirmed: [] };
6367
+ const mounts = route.mounts ?? [];
6368
+ const mounted = mounts.map(scan3);
6369
+ if (mounted.length > 0 && mounted.every(Boolean)) return { guarded: true, unconfirmed: [] };
6370
+ return { guarded: false, unconfirmed: [...unconfirmed] };
4788
6371
  }
4789
- var SECRET_SHAPED = /['"`]([A-Za-z0-9_\-.]{32,})['"`]/g;
4790
6372
  function excerptFor(file, line) {
4791
6373
  const raw = (file.lines[line - 1] ?? "").trim();
4792
6374
  let out = raw;
@@ -4801,10 +6383,7 @@ var apiAuthRule = {
4801
6383
  id: "api/db-access-without-auth",
4802
6384
  severity: "P0",
4803
6385
  check(ctx) {
4804
- const routes = ctx.files.flatMap((file) => {
4805
- const route = routeOf(file);
4806
- return route === null ? serverActionRoutes(file) : [route];
4807
- });
6386
+ const routes = ctx.files.flatMap((file) => serverRoutesOf(file, ctx.files));
4808
6387
  if (routes.length === 0) return [];
4809
6388
  for (const middlewareFile of ctx.files.filter((f) => isAnyMiddlewareFile(f.path))) {
4810
6389
  const config = extractMatcherConfig(middlewareFile);
@@ -4820,21 +6399,31 @@ var apiAuthRule = {
4820
6399
  const sessionModules = ctx.files.filter(buildsSessionClient);
4821
6400
  const findings = [];
4822
6401
  const indirectByFile = /* @__PURE__ */ new Map();
6402
+ const reportedRanges = /* @__PURE__ */ new Set();
4823
6403
  for (const route of routes) {
4824
6404
  const reachable = route.reachable;
4825
- const unguarded = unguardedOpsOf(route.file);
6405
+ const unguarded = unguardedOpsOf(route.file, ctx.files);
4826
6406
  let ops = reachable === void 0 ? unguarded : operationsInRange(unguarded, reachable.start, reachable.end);
4827
6407
  if (ops.length === 0) continue;
4828
- let indirectOps = indirectByFile.get(route.file);
4829
- if (!indirectOps) {
4830
- indirectOps = indirectGuardOperations(route, ctx);
4831
- indirectByFile.set(route.file, indirectOps);
6408
+ let indirectGuards = indirectByFile.get(route.file);
6409
+ if (!indirectGuards) {
6410
+ indirectGuards = indirectGuardOperations(route, ctx);
6411
+ indirectByFile.set(route.file, indirectGuards);
4832
6412
  }
6413
+ const indirectOps = indirectGuards.ops;
6414
+ ops = ops.filter((op) => !(op.via && indirectOps.has(op.index)));
6415
+ if (ops.length === 0) continue;
4833
6416
  const unprotected = ops.filter((op) => !indirectOps.has(op.index));
4834
- if (unprotected.length > 0) ops = unprotected;
6417
+ if (unprotected.length > 0 && (unprotected.some((op) => op.writes) || !ops.some((op) => op.writes) || usesAdminClient(route, adminModules, ctx.files))) ops = unprotected;
4835
6418
  const url = route.url;
4836
6419
  if (isAuthEndpoint(route)) continue;
4837
6420
  if ((route.framework === "next" || route.framework === "astro") && route.action === void 0 && middlewareCovers(ctx, route)) continue;
6421
+ let unconfirmedMiddleware = [];
6422
+ if (NODE_FRAMEWORKS.has(route.framework)) {
6423
+ const status = middlewareGuardStatus(route, ctx.files);
6424
+ if (status.guarded) continue;
6425
+ unconfirmedMiddleware = status.unconfirmed;
6426
+ }
4838
6427
  const globalGuard = globalGuardFor(ctx, route);
4839
6428
  const globalNote = globalGuard === null ? [] : [
4840
6429
  `${globalGuard} contains an authentication check that may cover ${url}. Its path conditions are written in code, so the scan cannot tell which routes it applies to; this finding is reported at lower confidence. Confirm that check runs for ${url}.`
@@ -4843,6 +6432,15 @@ var apiAuthRule = {
4843
6432
  if (/\.auth\s*\.\s*getSession\s*\(/.test(handlerCode)) globalNote.push(
4844
6433
  "This handler checks supabase.auth.getSession(). On the server that reads the session from the cookie without verifying it, so a forged cookie passes; it is not counted as authentication. Use supabase.auth.getClaims() or getUser() instead."
4845
6434
  );
6435
+ for (const name of unconfirmedMiddleware) globalNote.push(
6436
+ `The route goes through ${name}, which looks like authentication but could not be followed to code that rejects unauthenticated requests, so this finding is reported at lower confidence. Confirm ${name} rejects requests without a valid user.`
6437
+ );
6438
+ if (indirectGuards.truncated) globalNote.push(
6439
+ "The scan stopped following local helpers in this file at its limit (8 hops, 64 symbols per helper, 1,024 per file). Unfollowed helpers cannot hide a finding, but if one of them enforces authentication for this operation, treat this as a finding to review."
6440
+ );
6441
+ const rangeKey = `${route.file.path}\0${reachable?.start ?? -1}\0${reachable?.end ?? -1}`;
6442
+ if (reportedRanges.has(rangeKey)) continue;
6443
+ reportedRanges.add(rangeKey);
4846
6444
  const admin = usesAdminClient(route, adminModules, ctx.files);
4847
6445
  const hit = ops.find((o) => o.writes) ?? ops[0];
4848
6446
  const indirect = indirectOps.get(hit.index);
@@ -4855,17 +6453,28 @@ var apiAuthRule = {
4855
6453
  description: "Local authentication helper in the recognized delegation chain; verify runtime behaviour and caller coverage."
4856
6454
  })) : [];
4857
6455
  if (indirect) globalNote.push("A local authentication helper or wrapper was recognized before this operation. This finding is retained for review because indirect control flow is not a proof of authorization.");
6456
+ const delegated = (hit.via ?? []).map((step) => ({ file: step.file.path, line: lineNumberAt(lineStartsCached(step.file), step.index) }));
6457
+ const delegatedEvidence = delegated.map((step, i) => ({
6458
+ kind: "operation",
6459
+ file: step.file,
6460
+ line: step.line,
6461
+ description: i === delegated.length - 1 ? "Database write reached from this handler." : "Project function on the call path to the write."
6462
+ }));
6463
+ if (delegated.length > 0) {
6464
+ const target = delegated.at(-1);
6465
+ globalNote.push(`The write itself happens in ${target.file}:${target.line}, reached from this call` + (delegated.length > 1 ? ` through ${delegated.slice(0, -1).map((step) => `${step.file}:${step.line}`).join(", ")}` : "") + ". The scan followed the call into project code and found no authentication check on the way.");
6466
+ }
4858
6467
  if (admin) {
4859
6468
  const trace = adminEvidence(route, adminModules, ctx.files, line);
4860
- const evidence = [...trace.evidence ?? [], ...indirectEvidence];
6469
+ const evidence = [...trace.evidence ?? [], ...delegatedEvidence, ...indirectEvidence];
4861
6470
  findings.push({
4862
6471
  ruleId: "api/admin-db-access-without-auth",
4863
6472
  ...trace,
4864
6473
  evidence: evidence.slice(0, 24),
4865
6474
  evidenceTruncated: trace.evidenceTruncated || evidence.length > 24,
4866
6475
  severity: "P0",
4867
- // 管理员客户端缺少鉴权时使用确定置信度;可能受全局鉴权覆盖时降为疑似。
4868
- confidence: globalGuard === null && !indirect ? "certain" : "likely",
6476
+ // 管理员客户端缺少鉴权时使用确定置信度;可能受全局鉴权或未确认的中间件覆盖时降为疑似。
6477
+ confidence: globalGuard === null && !indirect && unconfirmedMiddleware.length === 0 ? "certain" : "likely",
4869
6478
  title: indirect ? `Review indirect authentication for admin database access in ${url}` : `Anyone can call ${url} and it queries your database as admin`,
4870
6479
  file: route.file.path,
4871
6480
  line,
@@ -4900,12 +6509,16 @@ var apiAuthRule = {
4900
6509
  file: route.file.path,
4901
6510
  line,
4902
6511
  excerpt,
4903
- ...indirect ? { evidence: [{
4904
- kind: "operation",
4905
- file: route.file.path,
4906
- line,
4907
- description: "Data write using a local authentication helper."
4908
- }, ...indirectEvidence] } : {},
6512
+ ...indirect || delegated.length > 0 ? { evidence: [
6513
+ {
6514
+ kind: "operation",
6515
+ file: route.file.path,
6516
+ line,
6517
+ description: indirect ? "Data write using a local authentication helper." : "Call that leads to a database write."
6518
+ },
6519
+ ...delegatedEvidence,
6520
+ ...indirectEvidence
6521
+ ] } : {},
4909
6522
  why: [
4910
6523
  ...globalNote,
4911
6524
  indirect ? "This route changes data after a local authentication helper; verify that it rejects unauthorized callers." : `This route changes data, and the scan did not recognize an authentication guard before the operation. Indirect guards and runtime controls require manual verification.`,
@@ -5304,7 +6917,7 @@ var corsRule = {
5304
6917
  // src/rules/request-input.ts
5305
6918
  var MAX_VALUE_HOPS = 8;
5306
6919
  var MAX_ASSIGNMENTS = 512;
5307
- var MAX_EXPRESSION = 4e3;
6920
+ var MAX_EXPRESSION = 65536;
5308
6921
  var REQUEST_OBJECT_NAMES = /^(?:req|request|event|evt|ctx|context)$/;
5309
6922
  var DESTRUCTURED_OBJECTS = /* @__PURE__ */ new Set(["request", "req", "event"]);
5310
6923
  var DESTRUCTURED_URLS = /* @__PURE__ */ new Set(["url"]);
@@ -5346,6 +6959,8 @@ var TRANSPARENT_CALLS = /* @__PURE__ */ new Set([
5346
6959
  "getRequestURL"
5347
6960
  ]);
5348
6961
  var H3_READERS = "readBody|readFormData|readMultipartFormData|readRawBody|readValidatedBody|getQuery|getValidatedQuery|getRouterParams?|getValidatedRouterParams|getHeaders?|getRequestHeaders?|getCookie|parseCookies|getRequestURL";
6962
+ var HONO_MEMBERS = "param|query|queries|header|json|text|parseBody|formData|arrayBuffer|blob|url|path|valid|headers";
6963
+ var HONO_READ_CALLS = "param|query|queries|header|json|text|parseBody|formData|arrayBuffer|blob|valid";
5349
6964
  var LITERAL = String.raw`(?:'[^']*'|"[^"]*"|\x60[^\x60$]*\x60|-?\d+(?:\.\d+)?|null|undefined|true|false)`;
5350
6965
  var CLEAN_EXPRESSION = [
5351
6966
  /^(?:await\s+)?(?:Number|parseInt|parseFloat|Boolean|BigInt|isNaN|Math\s*\.\s*\w+|Number\s*\.\s*\w+)\s*\(/,
@@ -5356,10 +6971,86 @@ var CLEAN_EXPRESSION = [
5356
6971
  // 带标签的模板由标签函数处理插值(Prisma.sql、sql、html),String.raw 除外。
5357
6972
  /^(?:await\s+)?(?!String\s*\.\s*raw\b)[\w$]+(?:\s*\.\s*[\w$]+)*\s*\x60/
5358
6973
  ];
6974
+ var MIRROR_CALLS = /* @__PURE__ */ new Set([
6975
+ "parse",
6976
+ "safeParse",
6977
+ "parseAsync",
6978
+ "safeParseAsync",
6979
+ "validate",
6980
+ "validateSync",
6981
+ "validateAsync",
6982
+ "cast",
6983
+ "structuredClone",
6984
+ "clone",
6985
+ "assign"
6986
+ ]);
6987
+ var VALUE_MEMBERS = /^\s*\??\.\s*(?:toString|valueOf|href|trim|trimStart|trimEnd|toLowerCase|toUpperCase|normalize)\b/;
6988
+ var HOST_HEADER = /^\s*(?:\??\.\s*get\s*\(\s*['"]host['"]\s*\)|\??\.\s*host\b|\[\s*['"]host['"]\s*\]|\(\s*['"]host['"]\s*\))/i;
5359
6989
  var URL_INPUT_ACCESS = /\??\.\s*(?:searchParams|pathname|search|hash)\b/;
5360
6990
  var URL_INPUT_KEYS = /* @__PURE__ */ new Set(["searchParams", "pathname", "search", "hash"]);
5361
6991
  var URL_ORIGIN_ACCESS = /^\s*\??\.\s*(?:origin|host|hostname|protocol|port)\b/;
5362
6992
  var URL_ORIGIN_KEYS = /* @__PURE__ */ new Set(["origin", "host", "hostname", "protocol", "port"]);
6993
+ function closeOf(text, open) {
6994
+ const pairs = { "(": ")", "[": "]", "{": "}" };
6995
+ const stack = [];
6996
+ for (let i = open; i < text.length; i++) {
6997
+ const ch = text[i];
6998
+ if (pairs[ch]) stack.push(pairs[ch]);
6999
+ else if (ch === stack[stack.length - 1]) {
7000
+ stack.pop();
7001
+ if (stack.length === 0) return i;
7002
+ }
7003
+ }
7004
+ return -1;
7005
+ }
7006
+ function callResultOf(expression) {
7007
+ const text = expression.trim().replace(/\s+as\s+[\w$.<>[\]| ]+$/, "").replace(/^(?:(?:await|new)\s+)+/, "");
7008
+ let i = 0;
7009
+ let callee = null;
7010
+ let members = false;
7011
+ if (text[0] === "(") {
7012
+ const close = closeOf(text, 0);
7013
+ if (close === -1) return null;
7014
+ const inner = callResultOf(text.slice(1, close));
7015
+ if (!inner || inner.members) return null;
7016
+ callee = inner.callee;
7017
+ i = close + 1;
7018
+ } else {
7019
+ const head = /^[A-Za-z_$][\w$]*/.exec(text);
7020
+ if (!head) return null;
7021
+ i = head[0].length;
7022
+ }
7023
+ let last = callee === null ? text.slice(0, i) : callee;
7024
+ while (i < text.length) {
7025
+ const rest = text.slice(i);
7026
+ const member = /^\s*(?:\??\.)\s*([A-Za-z_$][\w$]*)/.exec(rest);
7027
+ if (member) {
7028
+ last = member[1];
7029
+ if (callee !== null) members = true;
7030
+ i += member[0].length;
7031
+ continue;
7032
+ }
7033
+ const open = /^\s*(?:\?\.)?\s*([([])/.exec(rest);
7034
+ if (open) {
7035
+ const at = i + open[0].length - 1;
7036
+ const close = closeOf(text, at);
7037
+ if (close === -1) return null;
7038
+ if (open[1] === "(") {
7039
+ callee = last;
7040
+ members = false;
7041
+ } else if (callee !== null) members = true;
7042
+ i = close + 1;
7043
+ continue;
7044
+ }
7045
+ if (/^\s*!/.test(rest) && !/^\s*!=/.test(rest)) {
7046
+ i += rest.indexOf("!") + 1;
7047
+ continue;
7048
+ }
7049
+ if (rest.trim() === "") break;
7050
+ return null;
7051
+ }
7052
+ return callee === null ? null : { callee, members };
7053
+ }
5363
7054
  function isComparison(expr) {
5364
7055
  return !/\?/.test(expr) && !/=>/.test(expr) && /(?:===|!==|==|!=|<=|>=|\binstanceof\b)/.test(expr);
5365
7056
  }
@@ -5393,10 +7084,10 @@ function patternNames(pattern) {
5393
7084
  const stripped = text.replace(/[A-Za-z_$][\w$]*\s*:(?!:)/g, " ").replace(/=[^,}\]]*/g, " ");
5394
7085
  return stripped.match(/[A-Za-z_$][\w$]*/g) ?? [];
5395
7086
  }
5396
- function paramRoles(paramsText, paramsStart, allInput) {
5397
- const roles = { objects: /* @__PURE__ */ new Map(), urls: /* @__PURE__ */ new Map(), cookies: /* @__PURE__ */ new Map(), values: /* @__PURE__ */ new Map() };
7087
+ function paramRoles(paramsText, paramsStart, allInput, context = false) {
7088
+ const roles = { objects: /* @__PURE__ */ new Map(), urls: /* @__PURE__ */ new Map(), cookies: /* @__PURE__ */ new Map(), values: /* @__PURE__ */ new Map(), contexts: /* @__PURE__ */ new Map() };
5398
7089
  let offset = 0;
5399
- for (const raw of parameterList(paramsText)) {
7090
+ for (const [index, raw] of parameterList(paramsText).entries()) {
5400
7091
  const at = paramsStart + Math.max(0, paramsText.indexOf(raw, offset));
5401
7092
  offset = Math.max(offset, paramsText.indexOf(raw, offset) + raw.length);
5402
7093
  const param = raw.startsWith("{") || raw.startsWith("[") ? raw.slice(0, matchingClose(raw) + 1) : raw.replace(/^\.\.\./, "").split(/[:=]/)[0].trim();
@@ -5404,6 +7095,10 @@ function paramRoles(paramsText, paramsStart, allInput) {
5404
7095
  for (const name of patternNames(param)) roles.values.set(name, at);
5405
7096
  continue;
5406
7097
  }
7098
+ if (context && index === 0 && /^[A-Za-z_$][\w$]*$/.test(param)) {
7099
+ roles.contexts.set(param, at);
7100
+ continue;
7101
+ }
5407
7102
  if (param.startsWith("{")) {
5408
7103
  for (const { key, local } of destructuredKeys(param)) {
5409
7104
  if (DESTRUCTURED_OBJECTS.has(key)) roles.objects.set(local, at);
@@ -5440,7 +7135,7 @@ function parametersOf(code, body, pairs, openers) {
5440
7135
  const single = /([A-Za-z_$][\w$]*)\s*$/.exec(window);
5441
7136
  return single ? { start: body.declaration - window.length + single.index, end: body.declaration - window.length + single.index + single[1].length } : null;
5442
7137
  }
5443
- function expressionEnd(code, from, limit, pairs, truncated) {
7138
+ function expressionEnd2(code, from, limit, pairs, truncated) {
5444
7139
  const stop = Math.min(limit, from + MAX_EXPRESSION);
5445
7140
  for (let i = from; i < stop; i++) {
5446
7141
  const ch = code[i];
@@ -5499,14 +7194,16 @@ function fixedTablesOf(code, source, pairs) {
5499
7194
  return result;
5500
7195
  }
5501
7196
  var InputFlow = class {
5502
- constructor(code, source, span, pairs, roles, fixedTables) {
7197
+ constructor(code, source, span, pairs, roles, fixedTables, definition) {
5503
7198
  this.code = code;
5504
7199
  this.source = source;
5505
7200
  this.span = span;
5506
7201
  this.pairs = pairs;
5507
7202
  this.fixedTables = fixedTables;
7203
+ this.definition = definition;
5508
7204
  for (const [name, at] of roles.values) this.parameters.set(name, { level: "direct", origin: at, names: /* @__PURE__ */ new Set([name]), ownUrl: false });
5509
7205
  this.sourcePattern = sourceRegex(roles);
7206
+ this.contextRead = roles.contexts.size > 0 ? new RegExp(String.raw`(?<![\w$.])(?:${namePattern(roles.contexts.keys())})\s*\??\.\s*req(?:\s*\??\.\s*raw)?\s*\??\.\s*(${HONO_READ_CALLS})\s*\(`) : null;
5510
7207
  const assignments = this.assignments().sort((a, b) => a.at - b.at);
5511
7208
  const region = code.slice(span.start, span.end);
5512
7209
  const functions = this.localFunctions(region, assignments);
@@ -5520,7 +7217,7 @@ var InputFlow = class {
5520
7217
  const open = span.start + m.index + m[0].length - 1;
5521
7218
  let start = m[0] === "else" ? open + 1 : (pairs.get(open) ?? open) + 1;
5522
7219
  while (/\s/.test(code[start] ?? "")) start++;
5523
- controls.push({ start, end: pairs.get(start) ?? expressionEnd(code, start, span.end, pairs, () => {
7220
+ controls.push({ start, end: pairs.get(start) ?? expressionEnd2(code, start, span.end, pairs, () => {
5524
7221
  this.limited = true;
5525
7222
  }) });
5526
7223
  if (controls.length >= MAX_ASSIGNMENTS) {
@@ -5548,13 +7245,16 @@ var InputFlow = class {
5548
7245
  span;
5549
7246
  pairs;
5550
7247
  fixedTables;
7248
+ definition;
5551
7249
  parameters = /* @__PURE__ */ new Map();
5552
7250
  limited = false;
5553
7251
  assignmentList = /* @__PURE__ */ new Map();
5554
7252
  sourcePattern;
7253
+ contextRead;
5555
7254
  references = null;
5556
7255
  values = /* @__PURE__ */ new Map();
5557
7256
  strings = /* @__PURE__ */ new Map();
7257
+ mirrors = /* @__PURE__ */ new Map();
5558
7258
  /** 只索引当前处理函数;未引用的具名闭包不影响外层值。 */
5559
7259
  localFunctions(region, assignments) {
5560
7260
  const offset = this.span.start;
@@ -5565,7 +7265,7 @@ var InputFlow = class {
5565
7265
  }));
5566
7266
  for (const m of region.matchAll(/=>\s*(?!\s*\{)/g)) {
5567
7267
  const start = offset + m.index + m[0].length;
5568
- const end = expressionEnd(this.code, start, this.span.end, this.pairs, () => {
7268
+ const end = expressionEnd2(this.code, start, this.span.end, this.pairs, () => {
5569
7269
  this.limited = true;
5570
7270
  });
5571
7271
  bodies.push({ declaration: offset + m.index, start: start - 1, end });
@@ -5607,13 +7307,16 @@ var InputFlow = class {
5607
7307
  this.sourcePattern.lastIndex = 0;
5608
7308
  for (const m of observed.matchAll(this.sourcePattern)) {
5609
7309
  const after = observed.slice(m.index + m[0].length);
5610
- const ownUrl = m[1] === "url" || m[1] === "nextUrl";
7310
+ const member = m.slice(1).find((group) => group !== void 0);
7311
+ const ownUrl = member === "url" || member === "nextUrl";
5611
7312
  if (ownUrl && URL_ORIGIN_ACCESS.test(after)) continue;
7313
+ const hostAt = exprAt + m.index + m[0].length;
7314
+ const hostHeader = (member === "headers" || member === "header") && HOST_HEADER.test(this.source.slice(hostAt, hostAt + 80));
5612
7315
  found = mergeTaint(found, {
5613
7316
  level: "direct",
5614
7317
  origin: exprAt + m.index,
5615
7318
  names: /* @__PURE__ */ new Set(),
5616
- ownUrl: ownUrl && !URL_INPUT_ACCESS.test(after.slice(0, 40))
7319
+ ownUrl: hostHeader || ownUrl && !URL_INPUT_ACCESS.test(after.slice(0, 40))
5617
7320
  });
5618
7321
  }
5619
7322
  }
@@ -5626,16 +7329,83 @@ var InputFlow = class {
5626
7329
  if (/^\s*:(?!:)/.test(after) && /[{,]$/.test(before)) continue;
5627
7330
  const t = this.valueOf(m[0], exprAt, depth);
5628
7331
  if (!t) continue;
7332
+ if (t.opaque && /^\s*(?:\??\.\s*[\w$]|\?\.\s*\[|\[)/.test(after) && !VALUE_MEMBERS.test(after)) continue;
5629
7333
  if (t.ownUrl && URL_ORIGIN_ACCESS.test(after)) continue;
5630
7334
  found = mergeTaint(found, t.ownUrl && URL_INPUT_ACCESS.test(after.slice(0, 40)) ? { ...t, ownUrl: false } : t);
5631
7335
  }
5632
7336
  }
5633
7337
  if (!found) return null;
5634
7338
  if (found.ownUrl && URL_INPUT_ACCESS.test(text)) found.ownUrl = false;
5635
- const calls = [...text.replace(/\bJSON\s*\.\s*parse\s*\(/g, "(").matchAll(/([\w$]+)\s*\(/g)].map((m) => m[1]);
7339
+ const read = this.contextRead ? text.replace(new RegExp(this.contextRead.source, "g"), (_whole, call2) => call2 === "valid" ? "parse(" : "json(") : text;
7340
+ const calls = [...read.replace(/\bJSON\s*\.\s*parse\s*\(/g, "(").matchAll(/([\w$]+)\s*\(/g)].map((m) => m[1]);
5636
7341
  if (calls.some((name) => !TRANSPARENT_CALLS.has(name))) found.level = "derived";
7342
+ const call = callResultOf(read);
7343
+ if (call && !TRANSPARENT_CALLS.has(call.callee) && !MIRROR_CALLS.has(call.callee) && !this.returnsArgument(read, call.callee)) {
7344
+ if (call.members) return null;
7345
+ found.opaque = true;
7346
+ }
5637
7347
  return found;
5638
7348
  }
7349
+ /**
7350
+ * 表达式直接调用同文件中定义的函数,且该函数在调用参数之外返回某个形参:return value、return { ...value }、
7351
+ * (v) => v.trim()。return db.find(value) 之类把形参交给其他调用的不算,结果仍按未知函数处理。
7352
+ */
7353
+ returnsArgument(expression, callee) {
7354
+ const escaped = callee.replace(/\$/g, "\\$");
7355
+ if (!new RegExp(`^(?:(?:await|new)\\s+)*${escaped}\\s*\\(`).test(expression)) return false;
7356
+ const cached = this.mirrors.get(callee);
7357
+ if (cached !== void 0) return cached;
7358
+ let code = this.code;
7359
+ let pairs = this.pairs;
7360
+ let params = "";
7361
+ let after = -1;
7362
+ const declared = new RegExp(`(?:^|[^\\w$.])(?:async\\s+)?function\\s*\\*?\\s*${escaped}\\s*\\(`).exec(code);
7363
+ const assigned = declared ? null : new RegExp(`\\b(?:const|let|var)\\s+${escaped}\\s*(?::[^=;]{0,200})?=\\s*(?:async\\b\\s*)?(?:function\\b[^(]{0,80}\\(|\\(|([A-Za-z_$][\\w$]*)\\s*=>)`).exec(code);
7364
+ const match = declared ?? assigned;
7365
+ if (match && assigned?.[1]) {
7366
+ params = assigned[1];
7367
+ after = match.index + match[0].length;
7368
+ } else if (match) {
7369
+ const open = match.index + match[0].length - 1;
7370
+ const close = pairs.get(open);
7371
+ if (close !== void 0) {
7372
+ params = code.slice(open + 1, close);
7373
+ after = close + 1;
7374
+ }
7375
+ }
7376
+ if (!match) {
7377
+ const target = this.definition?.(callee);
7378
+ if (target) {
7379
+ code = noiseMaskedOf(target.file);
7380
+ pairs = delimiterPairs(code);
7381
+ params = functionParams(target).join(",");
7382
+ after = code[target.start] === "{" ? target.start : code.indexOf("=>", target.start) + 2;
7383
+ if (after < target.start || after > target.end) after = -1;
7384
+ }
7385
+ }
7386
+ let result = false;
7387
+ if (after !== -1) {
7388
+ const names = params.split(",").flatMap((part) => part.replace(/[:=][\s\S]*$/, "").match(/[A-Za-z_$][\w$]*/g) ?? []);
7389
+ const head = /^\s*(?::[^={]{0,200})?(?:=>)?\s*/.exec(code.slice(after, after + 260));
7390
+ const start = after + head[0].length;
7391
+ const end = (from, limit) => expressionEnd2(code, from, limit, pairs, () => {
7392
+ this.limited = true;
7393
+ });
7394
+ const bodyEnd = pairs.get(start) ?? start;
7395
+ const returns = code[start] === "{" ? [...code.slice(start, bodyEnd).matchAll(/\breturn\b/g)].map((m) => {
7396
+ const from = start + m.index + m[0].length;
7397
+ return code.slice(from, end(from, bodyEnd));
7398
+ }) : [code.slice(start, end(start, code.length))];
7399
+ const outsideCalls = (text) => {
7400
+ let current = text.replace(/\.\.\./g, " ");
7401
+ for (let i = 0; i < 8 && /[\w$)\]]\s*\([^()]*\)/.test(current); i++) current = current.replace(/([\w$)\]])\s*\([^()]*\)/g, "$1");
7402
+ return current;
7403
+ };
7404
+ result = names.length > 0 && returns.some((text) => names.some((name) => new RegExp(`(?<![\\w$.])${name.replace(/\$/g, "\\$")}(?![\\w$])`).test(outsideCalls(text))));
7405
+ }
7406
+ this.mirrors.set(callee, result);
7407
+ return result;
7408
+ }
5639
7409
  /** 字符串拼接:无标签模板的插值或含字符串操作数的 + 拼接中携带请求输入。 */
5640
7410
  builtOf(expr, exprAt, depth = 0) {
5641
7411
  if (expr.length > MAX_EXPRESSION) this.limited = true;
@@ -5687,6 +7457,7 @@ var InputFlow = class {
5687
7457
  for (const a of assignments) {
5688
7458
  const taint = this.builtOf(a.expr, a.exprAt, depth + 1) ?? this.taintOf(a.expr, a.exprAt, depth + 1);
5689
7459
  if (!taint) continue;
7460
+ if (taint.opaque && (a.iterate || /^[{[]/.test(a.pattern.trim()))) continue;
5690
7461
  const own = this.targetsOf(a, taint).find(([target]) => target === name);
5691
7462
  if (own) found = mergeTaint(found, { ...taint, level: a.indirect ? "derived" : taint.level, ownUrl: own[1], names: /* @__PURE__ */ new Set([name, ...taint.names]) });
5692
7463
  }
@@ -5826,7 +7597,7 @@ ${part}`.slice(0, MAX_EXPRESSION);
5826
7597
  const forOf = /^\s+of\s+/.exec(rest);
5827
7598
  if (forOf) {
5828
7599
  const exprAt2 = patternEnd + forOf[0].length;
5829
- const end2 = expressionEnd(code, exprAt2, span.end, pairs, () => {
7600
+ const end2 = expressionEnd2(code, exprAt2, span.end, pairs, () => {
5830
7601
  this.limited = true;
5831
7602
  });
5832
7603
  push({ at, pattern, names: patternNames(pattern), expr: code.slice(exprAt2, end2), exprAt: exprAt2, iterate: true, append: false, declaration: true });
@@ -5835,7 +7606,7 @@ ${part}`.slice(0, MAX_EXPRESSION);
5835
7606
  const eq = /^\s*(?::[^=;]{0,200}?)?=(?![=>])/.exec(rest);
5836
7607
  if (!eq) break;
5837
7608
  const exprAt = patternEnd + eq[0].length;
5838
- const end = expressionEnd(code, exprAt, span.end, pairs, () => {
7609
+ const end = expressionEnd2(code, exprAt, span.end, pairs, () => {
5839
7610
  this.limited = true;
5840
7611
  });
5841
7612
  push({ at, pattern, names: patternNames(pattern), expr: code.slice(exprAt, end), exprAt, iterate: false, append: false, declaration: true });
@@ -5848,7 +7619,7 @@ ${part}`.slice(0, MAX_EXPRESSION);
5848
7619
  const before = region.slice(Math.max(0, m.index - 8), m.index);
5849
7620
  if (/\b(?:const|let|var)\s+$/.test(before)) continue;
5850
7621
  const exprAt = span.start + m.index + m[0].length;
5851
- const end = expressionEnd(code, exprAt, span.end, pairs, () => {
7622
+ const end = expressionEnd2(code, exprAt, span.end, pairs, () => {
5852
7623
  this.limited = true;
5853
7624
  });
5854
7625
  push({ at: span.start + m.index, pattern: m[1], names: [m[1]], expr: code.slice(exprAt, end), exprAt, iterate: false, append: m[2] === "+=" });
@@ -5858,6 +7629,9 @@ ${part}`.slice(0, MAX_EXPRESSION);
5858
7629
  };
5859
7630
  function sourceRegex(roles) {
5860
7631
  const parts = [];
7632
+ if (roles.contexts.size > 0) {
7633
+ parts.push(String.raw`(?<![\w$.])(?:${namePattern(roles.contexts.keys())})\s*\??\.\s*req(?:\s*\??\.\s*raw)?\s*\??\.\s*(${HONO_MEMBERS})\b`);
7634
+ }
5861
7635
  if (roles.objects.size > 0) {
5862
7636
  const objects = namePattern(roles.objects.keys());
5863
7637
  parts.push(
@@ -5875,7 +7649,8 @@ function sourceRegex(roles) {
5875
7649
  return parts.length === 0 ? null : new RegExp(parts.join("|"), "g");
5876
7650
  }
5877
7651
  function mergeTaint(found, t) {
5878
- if (!found) return { level: t.level, origin: t.origin, names: new Set(t.names), ownUrl: t.ownUrl };
7652
+ if (!found) return { level: t.level, origin: t.origin, names: new Set(t.names), ownUrl: t.ownUrl, ...t.opaque ? { opaque: true } : {} };
7653
+ if (found.opaque && !t.opaque) delete found.opaque;
5879
7654
  if (t.level === "derived") found.level = "derived";
5880
7655
  if (found.ownUrl && !t.ownUrl) {
5881
7656
  found.ownUrl = false;
@@ -5885,7 +7660,7 @@ function mergeTaint(found, t) {
5885
7660
  return found;
5886
7661
  }
5887
7662
  function hasInput(roles) {
5888
- return roles.objects.size + roles.urls.size + roles.cookies.size + roles.values.size > 0;
7663
+ return roles.objects.size + roles.urls.size + roles.cookies.size + roles.values.size + roles.contexts.size > 0;
5889
7664
  }
5890
7665
  function lowerBound(values, value) {
5891
7666
  let low = 0, high = values.length;
@@ -5897,12 +7672,13 @@ function lowerBound(values, value) {
5897
7672
  return low;
5898
7673
  }
5899
7674
  var HandlerFile = class {
5900
- constructor(code, source, pairs, lineStarts, candidates) {
7675
+ constructor(code, source, pairs, lineStarts, candidates, definition) {
5901
7676
  this.code = code;
5902
7677
  this.source = source;
5903
7678
  this.pairs = pairs;
5904
7679
  this.lineStarts = lineStarts;
5905
7680
  this.candidates = candidates;
7681
+ this.definition = definition;
5906
7682
  this.fixedTables = fixedTablesOf(code, source, pairs);
5907
7683
  }
5908
7684
  code;
@@ -5910,6 +7686,7 @@ var HandlerFile = class {
5910
7686
  pairs;
5911
7687
  lineStarts;
5912
7688
  candidates;
7689
+ definition;
5913
7690
  flows = /* @__PURE__ */ new Map();
5914
7691
  fixedTables;
5915
7692
  get limited() {
@@ -5926,7 +7703,7 @@ var HandlerFile = class {
5926
7703
  if (first >= positions.length || positions[first] >= body.end) continue;
5927
7704
  let flow = this.flows.get(candidate);
5928
7705
  if (!flow) {
5929
- flow = new InputFlow(this.code, this.source, { start: body.start, end: body.end }, this.pairs, candidate.roles, this.fixedTables);
7706
+ flow = new InputFlow(this.code, this.source, { start: body.start, end: body.end }, this.pairs, candidate.roles, this.fixedTables, this.definition);
5930
7707
  this.flows.set(candidate, flow);
5931
7708
  }
5932
7709
  result.push({ route: candidate.route, body, flow });
@@ -5935,20 +7712,25 @@ var HandlerFile = class {
5935
7712
  }
5936
7713
  };
5937
7714
  var handlerCache = /* @__PURE__ */ new WeakMap();
5938
- function handlerFileOf(file) {
7715
+ function handlerFileOf(file, files) {
5939
7716
  const cached = handlerCache.get(file);
5940
7717
  if (cached !== void 0) return cached;
5941
7718
  let result = null;
5942
7719
  if (/\.[mc]?[jt]sx?$/.test(file.path)) {
5943
- const route = routeOf(file);
5944
- const routes = route === null ? serverActionRoutes(file) : [route];
7720
+ const routes = serverRoutesOf(file, files);
5945
7721
  if (routes.length > 0) {
5946
7722
  const code = noiseMaskedOf(file);
5947
7723
  const source = commentsMaskedOf(file);
5948
7724
  const pairs = delimiterPairs(code);
5949
7725
  const openers = /* @__PURE__ */ new Map();
5950
7726
  for (const [open, close] of pairs) openers.set(close, open);
5951
- const bodies = functionBodies(code, pairs).sort((a, b) => a.start - b.start);
7727
+ const bodies = functionBodies(code, pairs);
7728
+ for (const r of routes) {
7729
+ if (NODE_FRAMEWORKS.has(r.framework) && r.reachable && code.startsWith("=>", r.reachable.start)) {
7730
+ bodies.push({ declaration: r.reachable.start, start: r.reachable.start, end: r.reachable.end });
7731
+ }
7732
+ }
7733
+ bodies.sort((a, b) => a.start - b.start);
5952
7734
  const starts = bodies.map((body) => body.start);
5953
7735
  const candidates = [];
5954
7736
  for (const r of routes) {
@@ -5961,11 +7743,19 @@ function handlerFileOf(file) {
5961
7743
  const action = r.action !== void 0 && reachable !== void 0 && body.start === reachable.start;
5962
7744
  const params = parametersOf(code, body, pairs, openers);
5963
7745
  if (!params) continue;
5964
- const roles = paramRoles(source.slice(params.start, params.end), params.start, action);
7746
+ const context = r.framework === "hono" && reachable !== void 0 && body.start === reachable.start;
7747
+ const roles = paramRoles(source.slice(params.start, params.end), params.start, action, context);
5965
7748
  if (hasInput(roles)) candidates.push({ route: r, body, roles });
5966
7749
  }
5967
7750
  }
5968
- if (candidates.length > 0) result = new HandlerFile(code, source, pairs, lineStartsOf(file.content), candidates);
7751
+ if (candidates.length > 0) result = new HandlerFile(
7752
+ code,
7753
+ source,
7754
+ pairs,
7755
+ lineStartsOf(file.content),
7756
+ candidates,
7757
+ (name) => calleeDefinition(file, name, null, files)
7758
+ );
5969
7759
  }
5970
7760
  }
5971
7761
  handlerCache.set(file, result);
@@ -5974,7 +7764,7 @@ function handlerFileOf(file) {
5974
7764
  function reportInputLimit(ctx, file, analysed) {
5975
7765
  if (analysed.limited) ctx.reportIncomplete(
5976
7766
  "request-input/tracking",
5977
- `${file.path} reached the request-input tracking limit (8 value hops, 512 assignments or control/function regions, 4000 expression characters, or bounded URL analysis)`
7767
+ `${file.path} reached the request-input tracking limit (8 value hops, 512 assignments or control/function regions, 64 KiB expressions, or bounded URL analysis)`
5978
7768
  );
5979
7769
  }
5980
7770
 
@@ -6146,7 +7936,7 @@ var injectionRule = {
6146
7936
  check(ctx) {
6147
7937
  const findings = [];
6148
7938
  for (const file of ctx.files) {
6149
- const analysed = handlerFileOf(file);
7939
+ const analysed = handlerFileOf(file, ctx.files);
6150
7940
  if (!analysed) continue;
6151
7941
  const { code, source, pairs, lineStarts } = analysed;
6152
7942
  const sinks = sinksIn(code, pairs, commandCallees(file));
@@ -6181,7 +7971,7 @@ var injectionRule = {
6181
7971
  // src/rules/outbound.ts
6182
7972
  var FETCH_CALL = /(?<![\w$.])(?:fetch|\$fetch|ofetch|axios|got|ky|needle)\s*(?:<[^()]{0,200}>\s*)?\(/g;
6183
7973
  var FETCH_METHOD = /(?<![\w$.])(?:axios|got|ky|needle|http|https|undici)\s*\.\s*(?:get|post|put|patch|delete|head|request|stream)\s*(?:<[^()]{0,200}>\s*)?\(/g;
6184
- var REDIRECT_CALL = /(?<![\w$.])(?:redirect|permanentRedirect|sendRedirect)\s*\(|(?<![\w$.])(?:NextResponse|Response|res|reply|response|ctx|context|Astro)\s*\.\s*redirect\s*\(/g;
7974
+ var REDIRECT_CALL = /(?<![\w$.])(?:redirect|permanentRedirect|sendRedirect)\s*\(|(?<![\w$.])(?:NextResponse|Response|res|reply|response|ctx|context|c|Astro)\s*\.\s*redirect\s*\(/g;
6185
7975
  function callsIn(code, pairs) {
6186
7976
  const calls = [];
6187
7977
  const add = (regex, kind) => {
@@ -6199,6 +7989,8 @@ function callsIn(code, pairs) {
6199
7989
  }
6200
7990
  var HOST_OPEN = /^(?:X?|[a-z][\w+.-]*:\/\/[^/?#\\]*)$/i;
6201
7991
  var REDIRECT_OPEN = /^(?:\/|[/\\]{2}[^/?#\\]*)$/;
7992
+ var HOST_OPENABLE = /^(?:X?|[a-z][\w+.-]*(?::(?:\/(?:\/[^/?#\\]*)?)?)?)$/i;
7993
+ var REDIRECT_OPENABLE = /^(?:[/\\](?:[/\\][^/?#\\]*)?)?$/;
6202
7994
  function controlsStart(expr, at, flow, source, kind, depth = 0) {
6203
7995
  const lead = expr.length - expr.trimStart().length;
6204
7996
  const text = expr.trim();
@@ -6208,11 +8000,19 @@ function controlsStart(expr, at, flow, source, kind, depth = 0) {
6208
8000
  flow.limited = true;
6209
8001
  return null;
6210
8002
  }
6211
- if (text.length > 4e3) {
8003
+ if (text.length > MAX_EXPRESSION) {
6212
8004
  flow.limited = true;
6213
8005
  return null;
6214
8006
  }
6215
8007
  const open = (prefix) => HOST_OPEN.test(prefix) || kind === "redirect" && REDIRECT_OPEN.test(prefix);
8008
+ const branches = alternativesOf(text);
8009
+ if (branches) {
8010
+ for (const branch of branches) {
8011
+ const t2 = controlsStart(branch.text, start + branch.at, flow, source, kind, depth + 1);
8012
+ if (t2) return t2;
8013
+ }
8014
+ return null;
8015
+ }
6216
8016
  const url = /^new\s+URL\s*\(/.exec(text);
6217
8017
  if (url) {
6218
8018
  const argsStart = start + url[0].length;
@@ -6223,9 +8023,10 @@ function controlsStart(expr, at, flow, source, kind, depth = 0) {
6223
8023
  return first === void 0 ? null : controlsStart(first, argsStart, flow, source, "redirect", depth + 1);
6224
8024
  }
6225
8025
  }
6226
- if (text.startsWith("`")) return templateStart(text, start, flow, source, open);
6227
- const operands = flow.operandsOf(text, start);
6228
- if (operands.length > 1) {
8026
+ if (text.startsWith("`")) return templateStart(text, start, flow, source, open, kind, depth);
8027
+ const joined = joinedElements(text, start);
8028
+ const operands = joined ?? flow.operandsOf(text, start);
8029
+ if (operands.length > 1 || joined && operands.length > 0) {
6229
8030
  let prefix = "";
6230
8031
  for (const operand of operands) {
6231
8032
  const inner = operand.text.trim();
@@ -6233,7 +8034,7 @@ function controlsStart(expr, at, flow, source, kind, depth = 0) {
6233
8034
  prefix += source.slice(operand.at + operand.text.indexOf(inner) + 1, operand.at + operand.text.indexOf(inner) + inner.length - 1);
6234
8035
  continue;
6235
8036
  }
6236
- const t2 = inner.startsWith("`") ? templateStart(inner, operand.at + operand.text.indexOf(inner), flow, source, open) : controlsStart(inner, operand.at + operand.text.indexOf(inner), flow, source, kind, depth + 1);
8037
+ const t2 = inner.startsWith("`") ? templateStart(inner, operand.at + operand.text.indexOf(inner), flow, source, open, kind, depth) : controlsStart(inner, operand.at + operand.text.indexOf(inner), flow, source, kind, depth + 1);
6237
8038
  if (t2) return open(prefix) ? t2 : null;
6238
8039
  prefix += "X";
6239
8040
  if (!open(prefix)) return null;
@@ -6247,6 +8048,11 @@ function controlsStart(expr, at, flow, source, kind, depth = 0) {
6247
8048
  return t2 && !t2.ownUrl ? t2 : null;
6248
8049
  }
6249
8050
  for (const a of assignments) {
8051
+ if (a.iterate || /^[{[]/.test(a.pattern.trim())) {
8052
+ const t3 = flow.valueOf(text, at);
8053
+ if (t3 && !t3.ownUrl) return t3;
8054
+ continue;
8055
+ }
6250
8056
  const t2 = controlsStart(a.expr, a.exprAt, flow, source, kind, depth + 1);
6251
8057
  if (t2) return { ...t2, level: a.indirect ? "derived" : t2.level, names: /* @__PURE__ */ new Set([...t2.names, text]) };
6252
8058
  }
@@ -6255,7 +8061,42 @@ function controlsStart(expr, at, flow, source, kind, depth = 0) {
6255
8061
  const t = flow.taintOf(text, start);
6256
8062
  return t && !t.ownUrl ? t : null;
6257
8063
  }
6258
- function templateStart(text, start, flow, source, open) {
8064
+ function alternativesOf(text) {
8065
+ const top = [];
8066
+ let depth = 0;
8067
+ for (let i = 0; i < text.length; i++) {
8068
+ const ch = text[i];
8069
+ if ("([{".includes(ch)) depth++;
8070
+ else if (")]}".includes(ch)) depth--;
8071
+ else if (depth === 0) top.push(i);
8072
+ }
8073
+ const at = (i) => top.includes(i) ? text[i] : "";
8074
+ for (const i of top) {
8075
+ if (text[i] !== "?" || text[i + 1] === "." || text[i + 1] === "?" || text[i - 1] === "?") continue;
8076
+ let nested = 0;
8077
+ for (const j of top) {
8078
+ if (j <= i) continue;
8079
+ if (text[j] === "?" && text[j + 1] !== "." && text[j + 1] !== "?" && text[j - 1] !== "?") nested++;
8080
+ else if (text[j] === ":" && nested-- === 0) {
8081
+ return [{ text: text.slice(i + 1, j), at: i + 1 }, { text: text.slice(j + 1), at: j + 1 }];
8082
+ }
8083
+ }
8084
+ return null;
8085
+ }
8086
+ const parts = [];
8087
+ let from = 0;
8088
+ for (const i of top) {
8089
+ const pair = at(i) + at(i + 1);
8090
+ if ((pair === "??" || pair === "||") && i >= from) {
8091
+ parts.push({ text: text.slice(from, i), at: from });
8092
+ from = i + 2;
8093
+ }
8094
+ }
8095
+ if (parts.length === 0) return null;
8096
+ parts.push({ text: text.slice(from), at: from });
8097
+ return parts;
8098
+ }
8099
+ function templateStart(text, start, flow, source, open, kind, depth) {
6259
8100
  let prefix = "";
6260
8101
  for (let i = 1; i < text.length; i++) {
6261
8102
  if (text[i] === "`") return null;
@@ -6263,12 +8104,16 @@ function templateStart(text, start, flow, source, open) {
6263
8104
  const close = matchingBrace(text, i + 1);
6264
8105
  if (close === null) return null;
6265
8106
  const t = flow.taintOf(text.slice(i + 2, close), start + i + 2);
6266
- if (t && !t.ownUrl) return open(prefix) ? t : null;
8107
+ if (t && !t.ownUrl) {
8108
+ if (!open(prefix)) return null;
8109
+ return prefix === "" ? controlsStart(text.slice(i + 2, close), start + i + 2, flow, source, kind, depth + 1) : t;
8110
+ }
6267
8111
  prefix += "X";
6268
8112
  i = close;
6269
8113
  continue;
6270
8114
  }
6271
8115
  prefix += source[start + i];
8116
+ if (!HOST_OPENABLE.test(prefix) && !(kind === "redirect" && REDIRECT_OPENABLE.test(prefix))) return null;
6272
8117
  if (prefix.length > 200) {
6273
8118
  flow.limited = true;
6274
8119
  return null;
@@ -6292,6 +8137,30 @@ function matchingBrace(text, open) {
6292
8137
  }
6293
8138
  return null;
6294
8139
  }
8140
+ function joinedElements(text, start) {
8141
+ if (!text.startsWith("[")) return null;
8142
+ let depth = 0, close = -1;
8143
+ for (let i = 0; i < text.length; i++) {
8144
+ if ("([{".includes(text[i])) depth++;
8145
+ else if (")]}".includes(text[i]) && --depth === 0) {
8146
+ close = i;
8147
+ break;
8148
+ }
8149
+ }
8150
+ if (close === -1 || text[close] !== "]" || !/^\s*\.\s*join\s*\(\s*(['"\x60])\1\s*\)\s*$/.test(text.slice(close + 1))) return null;
8151
+ const elements = [];
8152
+ let from = 1;
8153
+ depth = 0;
8154
+ for (let i = 1; i <= close; i++) {
8155
+ if (i < close && "([{".includes(text[i])) depth++;
8156
+ else if (i < close && ")]}".includes(text[i])) depth--;
8157
+ else if (i === close || text[i] === "," && depth === 0) {
8158
+ if (text.slice(from, i).trim() !== "") elements.push({ text: text.slice(from, i), at: start + from });
8159
+ from = i + 1;
8160
+ }
8161
+ }
8162
+ return elements;
8163
+ }
6295
8164
  function splitTopLevel(text) {
6296
8165
  const parts = [];
6297
8166
  let depth = 0, from = 0;
@@ -6380,13 +8249,14 @@ function redirectFinding(route, file, line, originLine, certain) {
6380
8249
  function check(ctx, kind) {
6381
8250
  const findings = [];
6382
8251
  for (const file of ctx.files) {
6383
- const analysed = handlerFileOf(file);
8252
+ const analysed = handlerFileOf(file, ctx.files);
6384
8253
  if (!analysed) continue;
6385
8254
  const calls = callsIn(analysed.code, analysed.pairs).filter((call) => call.kind === kind).sort((a, b) => a.at - b.at);
6386
8255
  if (calls.length === 0) continue;
6387
8256
  const positions = calls.map((call) => call.at);
6388
8257
  const reported = /* @__PURE__ */ new Set();
6389
8258
  for (const { route, body, flow } of analysed.handlersAround(positions)) {
8259
+ if (kind === "redirect" && route.action !== void 0) continue;
6390
8260
  for (let i = lowerBound(positions, body.start + 1); i < calls.length && calls[i].at < body.end; i++) {
6391
8261
  const call = calls[i];
6392
8262
  if (call.close >= body.end || reported.has(call.at)) continue;
@@ -6407,140 +8277,6 @@ function check(ctx, kind) {
6407
8277
  var ssrfRule = { id: "ssrf/request-url", severity: "P1", check: (ctx) => check(ctx, "ssrf") };
6408
8278
  var redirectRule = { id: "redirect/open", severity: "P2", check: (ctx) => check(ctx, "redirect") };
6409
8279
 
6410
- // src/rules/verification.ts
6411
- var LocalVerification = class {
6412
- code;
6413
- source;
6414
- pairs;
6415
- bodies;
6416
- constructor(file) {
6417
- this.code = noiseMaskedOf(file);
6418
- this.source = commentsMaskedOf(file);
6419
- this.pairs = delimiterPairs(this.code);
6420
- this.bodies = functionBodies(this.code, this.pairs);
6421
- }
6422
- owner(at) {
6423
- return this.bodies.filter((b) => b.start < at && at < b.end).sort((a, b) => b.start - a.start)[0];
6424
- }
6425
- skipSpace(at) {
6426
- while (/\s/.test(this.code[at] ?? "")) at++;
6427
- return at;
6428
- }
6429
- endOf(at, limit = this.code.length) {
6430
- if (this.code[at] === "{") return (this.pairs.get(at) ?? limit - 1) + 1;
6431
- for (let i = at; i < limit; i++) {
6432
- if (/[;\n}]/.test(this.code[i])) return i;
6433
- const end = this.pairs.get(i);
6434
- if (end !== void 0) i = end;
6435
- }
6436
- return limit;
6437
- }
6438
- /**
6439
- * 块的顶层有无条件退出即算退出,前面可以有记录日志等语句;
6440
- * 嵌套分支、函数中的 return 以及无花括号 if/else/循环控制的 return 不算。
6441
- */
6442
- exits(from, to) {
6443
- let at = this.skipSpace(from);
6444
- if (this.code[at] !== "{") return /^(?:return|throw)\b|^(?:redirect|notFound)\s*\(/.test(this.code.slice(at, to));
6445
- const end = Math.min(to, this.pairs.get(at) ?? to);
6446
- at = this.skipSpace(at + 1);
6447
- while (at < end) {
6448
- if (/^(?:return|throw)\b|^(?:redirect|notFound)\s*\(/.test(this.code.slice(at, at + 32))) return true;
6449
- const next = this.statementEnd(at, end);
6450
- if (next <= at) return false;
6451
- at = this.skipSpace(next);
6452
- }
6453
- return false;
6454
- }
6455
- /** 跳过完整语句;分支、循环和函数内的退出不提升为外层退出。 */
6456
- statementEnd(at, limit, depth = 0) {
6457
- if (depth >= 64) return limit;
6458
- if (this.code[at] === "{") return (this.pairs.get(at) ?? limit - 1) + 1;
6459
- const control = /^(if|for(?:\s+await)?|while|with|switch)\s*\(/.exec(this.code.slice(at, at + 80));
6460
- if (control) {
6461
- const close = this.pairs.get(at + control[0].length - 1);
6462
- if (close === void 0) return limit;
6463
- let end = this.statementEnd(this.skipSpace(close + 1), limit, depth + 1);
6464
- const next = this.skipSpace(end);
6465
- if (control[1] === "if" && /^else\b/.test(this.code.slice(next, next + 5))) {
6466
- end = this.statementEnd(this.skipSpace(next + 4), limit, depth + 1);
6467
- }
6468
- return end;
6469
- }
6470
- if (/^(?:try|do)\b/.test(this.code.slice(at, at + 8))) return limit;
6471
- for (let i = at; i < limit; i++) {
6472
- if (this.code[i] === ";") return i + 1;
6473
- const close = this.pairs.get(i);
6474
- if (close !== void 0) {
6475
- i = close;
6476
- continue;
6477
- }
6478
- if (this.code[i] !== "\n") continue;
6479
- const before = this.code.slice(at, i).trimEnd();
6480
- const after = this.code.slice(i + 1, limit).trimStart();
6481
- if (!before || /[=+\-*/%&|^!?:,.([{<>]$/.test(before)) continue;
6482
- if (/^[.?+\-*/%&|^,:<>=([{`]/.test(after)) continue;
6483
- if (/\b(?:const|let|var|function|async|await|new|yield)$/.test(before)) continue;
6484
- return i + 1;
6485
- }
6486
- return limit;
6487
- }
6488
- /** 验证与使用须在同一函数,且不能依赖可选分支或被吞掉的异常。 */
6489
- enforcedBefore(from, to) {
6490
- const owner = this.owner(from);
6491
- if (from >= to || owner !== this.owner(to)) return false;
6492
- const start = owner ? owner.start + 1 : 0;
6493
- for (let i = start; i < from; i++) {
6494
- const conditional = /^(?:if|for|while|switch)\s*\(/.exec(this.code.slice(i, i + 32));
6495
- if (conditional && !/[\w$]/.test(this.code[i - 1] ?? "")) {
6496
- const close = this.pairs.get(i + conditional[0].length - 1);
6497
- if (close !== void 0) {
6498
- const body = this.skipSpace(close + 1);
6499
- const end2 = this.endOf(body);
6500
- if (from < end2 && to >= end2) return false;
6501
- }
6502
- }
6503
- const end = this.pairs.get(i);
6504
- if (end === void 0) continue;
6505
- if (end < from) {
6506
- i = end;
6507
- continue;
6508
- }
6509
- if (end >= to) continue;
6510
- if (this.code[i] !== "{" || !/\btry\s*$/.test(this.code.slice(Math.max(start, i - 16), i))) return false;
6511
- let next = this.skipSpace(end + 1);
6512
- if (/^catch\b/.test(this.code.slice(next))) {
6513
- next = this.skipSpace(next + 5);
6514
- if (this.code[next] === "(") next = this.skipSpace((this.pairs.get(next) ?? next) + 1);
6515
- const caughtEnd = this.endOf(next);
6516
- if (!this.exits(next, caughtEnd)) return false;
6517
- next = this.skipSpace(caughtEnd);
6518
- }
6519
- if (/^finally\b/.test(this.code.slice(next))) return false;
6520
- }
6521
- const boundary = Math.max(
6522
- start,
6523
- this.code.lastIndexOf(";", from - 1) + 1,
6524
- this.code.lastIndexOf("\n", from - 1) + 1,
6525
- this.code.lastIndexOf("{", from - 1) + 1
6526
- );
6527
- return !/[?]|&&|\|\|/.test(this.code.slice(boundary, from));
6528
- }
6529
- /** 找出当前函数中最后一次赋值;未知或条件覆盖同样会使旧证明失效。 */
6530
- assignment(name, at) {
6531
- const escaped = name.replace(/\$/g, "\\$");
6532
- const pattern = new RegExp(`(?<![\\w$.])(?:const\\s+|let\\s+|var\\s+)?${escaped}((?:\\s*\\.[\\w$]+|\\s*\\[[^\\]]*\\])*)\\s*(?::[^=;{}\\n]{0,100})?=(?![=>])`, "g");
6533
- let result = null;
6534
- for (const m of this.code.slice(0, at).matchAll(pattern)) {
6535
- if (this.owner(m.index) !== this.owner(at)) continue;
6536
- const from = this.skipSpace(m.index + m[0].length);
6537
- const end = this.endOf(from);
6538
- if (end <= at) result = { at: m.index, from, end, expression: this.code.slice(from, end).trim(), member: m[1].length > 0 };
6539
- }
6540
- return result;
6541
- }
6542
- };
6543
-
6544
8280
  // src/rules/session.ts
6545
8281
  var GET_SESSION = /\.auth\s*\.\s*getSession\s*\(/g;
6546
8282
  function verifiedSession(file, context, sessionAt, names) {
@@ -6589,18 +8325,19 @@ var SERVER_PATH = [
6589
8325
  /\.server\.[mc]?[jt]s$/,
6590
8326
  /(?:^|\/)server\/.+\.[mc]?[jt]s$/
6591
8327
  ];
6592
- function isServerFile(file) {
8328
+ function isServerFile(file, files) {
6593
8329
  if (!/\.[mc]?[jt]sx?$/.test(file.path)) return false;
6594
8330
  if (/^\s*(?:(?:'use strict'|"use strict");?\s*)?['"]use client['"]/.test(commentsMaskedOf(file))) return false;
6595
- return SERVER_PATH.some((pattern) => pattern.test(file.path)) || routeOf(file) !== null || serverActionRoutes(file).length > 0;
8331
+ return SERVER_PATH.some((pattern) => pattern.test(file.path)) || serverRoutesOf(file, files).length > 0;
6596
8332
  }
6597
8333
  var sessionRule = {
6598
8334
  id: "auth/unverified-session",
6599
8335
  severity: "P1",
6600
8336
  appliesTo(file) {
6601
- return file.content.includes("getSession") && isServerFile(file);
8337
+ return file.content.includes("getSession");
6602
8338
  },
6603
- check(file) {
8339
+ check(file, ctx) {
8340
+ if (!isServerFile(file, ctx.files)) return [];
6604
8341
  const code = noiseMaskedOf(file);
6605
8342
  const context = new LocalVerification(file);
6606
8343
  const { pairs, bodies } = context;
@@ -6657,7 +8394,7 @@ var STRIPE_EVENT = /(?:\bcase\s*|\.type\s*[!=]==?\s*)['"`]((?:checkout\.session|
6657
8394
  function verifiedEvent(context, name, at) {
6658
8395
  const assignment = context.assignment(name, at);
6659
8396
  if (!assignment || assignment.member || !context.enforcedBefore(assignment.at, at)) return false;
6660
- const call = /^(?:(await)\s+)?[\w$.]+\.(constructEvent(?:Async)?|retrieve|parseEventNotification)\s*\(/.exec(assignment.expression);
8397
+ const call = /^(?:(await)\s+)?(?:[\w$]+(?:\s*\([^()]*\))?\s*\??\.\s*)+(constructEvent(?:Async)?|retrieve|parseEventNotification)\s*\(/.exec(assignment.expression);
6661
8398
  if (!call || call[2] !== "constructEvent" && !call[1]) return false;
6662
8399
  if (call[2] === "retrieve" && !/\.events\s*\.\s*retrieve/.test(call[0])) return false;
6663
8400
  const close = context.pairs.get(assignment.from + call[0].length - 1);
@@ -6673,22 +8410,73 @@ function eventName(context, event) {
6673
8410
  }
6674
8411
  return name;
6675
8412
  }
6676
- var READS_BODY = /\.\s*(?:json|text|arrayBuffer|formData)\s*\(|\breq(?:uest)?\s*\.\s*body\b|\b(?:readBody|readRawBody)\s*\(|\bbuffer\s*\(\s*req\b/;
8413
+ var READS_BODY = /(?<![\w$])(?:req|request)\s*(?:\.\s*raw\s*)?\.\s*(?:json|text|arrayBuffer|formData|parseBody)\s*\(|\breq(?:uest)?\s*\.\s*body\b|\b(?:readBody|readRawBody)\s*\(|\bbuffer\s*\(\s*req\b/;
8414
+ var SIGNATURE_HEADER = /['"]stripe-signature['"]/i;
8415
+ function middlewareVerifies(code) {
8416
+ const masked = maskUnusedFunctions(code);
8417
+ const verifies = enforcedVerification(masked, /\bconstructEvent(?:Async)?\s*\(/, /^constructEvent\s*\(/);
8418
+ if (!verifies) return false;
8419
+ for (const m of masked.matchAll(/\bcatch\s*(?:\([^()]*\))?\s*\{/g)) {
8420
+ const open = m.index + m[0].length - 1;
8421
+ let close = open;
8422
+ for (let depth = 0; close < masked.length; close++) {
8423
+ if (masked[close] === "{") depth++;
8424
+ else if (masked[close] === "}" && --depth === 0) break;
8425
+ }
8426
+ const body = masked.slice(open, close);
8427
+ if (/\bnext\s*\(\s*\)/.test(body)) return false;
8428
+ }
8429
+ return true;
8430
+ }
8431
+ function verifiedByMiddleware(route, files) {
8432
+ if (!NODE_FRAMEWORKS.has(route.framework)) return null;
8433
+ const codeOf = (ref) => {
8434
+ const { inline, range } = middlewareDefinition(ref, files);
8435
+ const target = inline ?? range;
8436
+ return target === null ? null : { file: target.file, code: commentsMaskedOf(target.file).slice(target.start, target.end + 1) };
8437
+ };
8438
+ const verifies = ({ file, code }) => {
8439
+ if (middlewareVerifies(code)) return true;
8440
+ const masked = maskUnusedFunctions(code);
8441
+ const calls = [...masked.matchAll(/(?<![\w$.])([A-Za-z_$][\w$]*)\s*(?:\.\s*([A-Za-z_$][\w$]*)\s*)?\(/g)].slice(0, 16);
8442
+ return calls.some((m) => {
8443
+ const call = new RegExp(`(?<![\\w$.])${m[1].replace(/\$/g, "\\$")}${m[2] ? `\\s*\\.\\s*${m[2].replace(/\$/g, "\\$")}` : ""}\\s*\\(`);
8444
+ if (!enforcedVerification(masked, call)) return false;
8445
+ const definition = calleeDefinition(file, m[1], m[2] ?? null, files);
8446
+ return definition !== null && middlewareVerifies(commentsMaskedOf(definition.file).slice(definition.start, definition.end + 1));
8447
+ });
8448
+ };
8449
+ const status = (test) => {
8450
+ const check2 = (ref) => {
8451
+ const target = codeOf(ref);
8452
+ return target !== null && test(target);
8453
+ };
8454
+ const mounts = route.mounts ?? [];
8455
+ return (route.middleware ?? []).some(check2) || mounts.length > 0 && mounts.every((refs) => refs.some(check2));
8456
+ };
8457
+ if (status(verifies)) return "verified";
8458
+ return status(({ code }) => SIGNATURE_HEADER.test(code)) ? "custom" : null;
8459
+ }
6677
8460
  var webhookRule = {
6678
8461
  id: "webhook/unverified-signature",
6679
8462
  severity: "P1",
6680
8463
  appliesTo(file) {
6681
- return /stripe/i.test(file.content) && routeOf(file) !== null;
8464
+ return /stripe/i.test(file.content);
6682
8465
  },
6683
- check(file) {
6684
- const route = routeOf(file);
6685
- if (!route) return [];
8466
+ check(file, ctx) {
8467
+ const routes = serverRoutesOf(file, ctx.files);
8468
+ if (routes.length === 0) return [];
8469
+ const routeAt = (at) => routes.find((r) => !r.reachable || r.reachable.start <= at && at <= r.reachable.end);
6686
8470
  const context = new LocalVerification(file);
6687
8471
  const { source, code } = context;
6688
8472
  const findings = [];
6689
8473
  const reported = /* @__PURE__ */ new Set();
6690
8474
  for (const event of source.matchAll(new RegExp(STRIPE_EVENT.source, "g"))) {
6691
8475
  if (code[event.index] !== source[event.index]) continue;
8476
+ const route = routeAt(event.index);
8477
+ if (!route) continue;
8478
+ const middleware = verifiedByMiddleware(route, ctx.files);
8479
+ if (middleware === "verified") continue;
6692
8480
  const owner = context.owner(event.index);
6693
8481
  const name = eventName(context, event);
6694
8482
  if (name && verifiedEvent(context, name, event.index)) continue;
@@ -6698,7 +8486,7 @@ var webhookRule = {
6698
8486
  const line = lineNumberAt(lineStartsOf(file.content), event.index);
6699
8487
  const region = code.slice(owner?.start ?? 0, owner?.end ?? code.length);
6700
8488
  const readsBody = READS_BODY.test(region);
6701
- const customCheck = /\bverifyHeader\s*\(/.test(region) || /\bcreateHmac\s*\(/.test(region) && /\btimingSafeEqual\s*\(/.test(region);
8489
+ const customCheck = middleware === "custom" || /\bverifyHeader\s*\(/.test(region) || /\bcreateHmac\s*\(/.test(region) && /\btimingSafeEqual\s*\(/.test(region);
6702
8490
  findings.push({
6703
8491
  ruleId: "webhook/unverified-signature",
6704
8492
  severity: "P1",
@@ -6972,16 +8760,16 @@ var RULE_CATALOG = [
6972
8760
  "Admin database access without a recognized auth guard",
6973
8761
  "P0",
6974
8762
  "certain",
6975
- "Server routes and Next.js Server Functions in Next.js, SvelteKit (endpoints and form actions), Nuxt, Remix/React Router, and Astro, including workspace applications.",
6976
- "Syntactic guards only; runtime authentication and business authorization are not verified. Local calls, identity arguments, branches and exception paths are analysed within documented limits. Local auth evidence, SvelteKit hooks and Nuxt middleware may lower confidence without removing findings; exceeded limits disclose incomplete coverage."
8763
+ "Server routes and Next.js Server Functions in Next.js, SvelteKit (endpoints and form actions), Nuxt, Remix/React Router, Astro, Express, Hono, and Fastify, including workspace applications.",
8764
+ "Syntactic guards only; runtime authentication and business authorization are not verified. Local calls, identity arguments, branches and exception paths are analysed within documented limits. Express, Hono, and Fastify middleware and hooks remove findings only when they resolve to code that rejects unauthenticated requests or to a known auth library; auth-like middleware that cannot be followed, local auth evidence, SvelteKit hooks and Nuxt middleware may lower confidence without removing findings; exceeded limits disclose incomplete coverage."
6977
8765
  ),
6978
8766
  rule(
6979
8767
  "api/db-write-without-auth",
6980
8768
  "Database write without a recognized auth guard",
6981
8769
  "P1",
6982
8770
  "likely",
6983
- "Server routes and Next.js Server Functions in Next.js, SvelteKit (endpoints and form actions), Nuxt, Remix/React Router, and Astro.",
6984
- "Session-scoped clients and indirect guards may need manual review."
8771
+ "Server routes and Next.js Server Functions in Next.js, SvelteKit (endpoints and form actions), Nuxt, Remix/React Router, Astro, Express, Hono, and Fastify.",
8772
+ "Session-scoped clients and indirect guards may need manual review. In Express, Hono, and Fastify routes, writes in called project functions are followed two levels deep; file-based routes report writes in the route file only. Hono app.openapi() routes are not analysed."
6985
8773
  ),
6986
8774
  rule(
6987
8775
  "auth/unverified-session",