canship 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -3144,6 +3144,46 @@ 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) return !declared[1];
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
+ return assigned !== null && !assigned[1];
3186
+ }
3147
3187
  var limited = /* @__PURE__ */ new WeakSet();
3148
3188
  var cache2 = /* @__PURE__ */ new WeakMap();
3149
3189
  var reverseCache = /* @__PURE__ */ new WeakMap();
@@ -3340,7 +3380,7 @@ var AuthValues = class {
3340
3380
  limited.add(this.file);
3341
3381
  return { kind: "opaque" };
3342
3382
  }
3343
- if (/^process\.env\.[A-Z_][A-Z0-9_]*$/.test(expr)) return { kind: "secret" };
3383
+ if (SERVER_ENV.test(expr)) return { kind: "secret" };
3344
3384
  const template = /^`[^`$]*\$\{([^{}`]{1,200})\}[^`$]*`$/.exec(expr);
3345
3385
  if (template) return this.value(template[1], at, depth + 1).kind === "secret" ? { kind: "secret" } : { kind: "literal" };
3346
3386
  if (/^(?:(?:true|false|null|undefined)\b|\d|['"`{\[])/.test(expr)) return { kind: "literal" };
@@ -3362,7 +3402,12 @@ var AuthValues = class {
3362
3402
  if (envelope) return { kind: "envelope", field: envelope === "getUser" ? "user" : "claims" };
3363
3403
  return { kind: "identity" };
3364
3404
  }
3405
+ if (claimedIdentityCall(expr)) return /^await\s/.test(expr) || synchronousLocal(this.file, expr) ? { kind: "claimed" } : { kind: "promise" };
3365
3406
  if (/\.(?:body|query|headers|cookies|searchParams)\b|\.(?:json|text|formData)\s*\(/.test(expr)) return { kind: "input" };
3407
+ const keyed = /^([A-Za-z_$][\w$]*)\s*\.\s*get\s*\(\s*['"][\w$.-]+['"]\s*\)$/.exec(expr);
3408
+ if (keyed && this.parameters.has(keyed[1]) && !this.assignments.has(keyed[1])) {
3409
+ return { kind: "parameter", parameters: [this.parameters.get(keyed[1])] };
3410
+ }
3366
3411
  const access = /^([A-Za-z_$][\w$]*)((?:\??\.[A-Za-z_$][\w$]*)*)$/.exec(expr);
3367
3412
  if (!access) return { kind: "opaque" };
3368
3413
  const name = access[1];
@@ -3381,6 +3426,8 @@ var AuthValues = class {
3381
3426
  return { kind: "opaque" };
3382
3427
  }
3383
3428
  if (this.conditions.some((range) => range.start <= assignment.at && assignment.at <= range.end && !(range.start <= at && at <= range.end))) return { kind: "opaque" };
3429
+ const key = [...assignment.projection ?? [], ...access[2].split(/\??\./).filter(Boolean)];
3430
+ 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
3431
  value = this.value(assignment.expression, assignment.at, depth + 1);
3385
3432
  value = assignment.projection === null ? { kind: "opaque" } : project(value, assignment.projection);
3386
3433
  } else if (this.parameters.has(name)) value = { kind: "parameter", parameters: [this.parameters.get(name)] };
@@ -3394,6 +3441,964 @@ function identityRequirement(value, allowParameters) {
3394
3441
  return null;
3395
3442
  }
3396
3443
 
3444
+ // src/rules/routers.ts
3445
+ var NODE_FRAMEWORKS = /* @__PURE__ */ new Set(["express", "hono", "fastify"]);
3446
+ var METHODS = "get|post|put|patch|delete|all|options|head";
3447
+ var GENERICS = String.raw`(?:<[^()]{0,300}>\s*)?`;
3448
+ var FASTIFY_HOOKS = /^(?:onRequest|preParsing|preValidation|preHandler)$/;
3449
+ var HONO_CALL = `${METHODS}|on|use|route`;
3450
+ var analysedCache = /* @__PURE__ */ new WeakMap();
3451
+ function analyse(file) {
3452
+ const cached = analysedCache.get(file);
3453
+ if (cached) return cached;
3454
+ const code = noiseMaskedOf(file);
3455
+ const pairs = delimiterPairs(code);
3456
+ const openers = /* @__PURE__ */ new Map();
3457
+ for (const [open, close] of pairs) openers.set(close, open);
3458
+ const result = { file, code, source: commentsMaskedOf(file), pairs, openers, bodies: functionBodies(code, pairs) };
3459
+ analysedCache.set(file, result);
3460
+ return result;
3461
+ }
3462
+ function argsOf(a, open, close) {
3463
+ const args = [];
3464
+ let from = open + 1;
3465
+ const push = (end) => {
3466
+ const raw = a.code.slice(from, end);
3467
+ const lead = raw.length - raw.trimStart().length;
3468
+ const text = raw.trim();
3469
+ if (text) args.push({ text, source: a.source.slice(from + lead, from + lead + text.length), at: from + lead });
3470
+ };
3471
+ for (let i = open + 1; i < close; i++) {
3472
+ const end = a.pairs.get(i);
3473
+ if (end !== void 0) {
3474
+ i = end;
3475
+ continue;
3476
+ }
3477
+ if (a.code[i] === ",") {
3478
+ push(i);
3479
+ from = i + 1;
3480
+ }
3481
+ }
3482
+ push(close);
3483
+ return args;
3484
+ }
3485
+ function flatten(a, args) {
3486
+ return args.flatMap((arg) => {
3487
+ if (!arg.text.startsWith("[")) return [arg];
3488
+ const close = a.pairs.get(arg.at);
3489
+ return close === void 0 ? [arg] : argsOf(a, arg.at, close);
3490
+ });
3491
+ }
3492
+ function scopeOf(path) {
3493
+ return /^(.*?\/)?(?:src|app|lib|server|routes|api|controllers|plugins)\//.exec(path)?.[1] ?? "";
3494
+ }
3495
+ function entriesOf(a, arg) {
3496
+ const entries = /* @__PURE__ */ new Map();
3497
+ if (!arg || a.code[arg.at] !== "{") return entries;
3498
+ const close = a.pairs.get(arg.at);
3499
+ if (close === void 0) return entries;
3500
+ for (const part of argsOf(a, arg.at, close)) {
3501
+ const key = /^(?:async\s+)?['"]?([A-Za-z_$][\w$]*)['"]?/.exec(part.source);
3502
+ if (!key) continue;
3503
+ const rest = part.text.slice(key[0].length);
3504
+ const colon = /^\s*:\s*/.exec(rest);
3505
+ if (colon) {
3506
+ const offset = key[0].length + colon[0].length;
3507
+ entries.set(key[1], { value: { text: part.text.slice(offset), source: part.source.slice(offset), at: part.at + offset }, range: null });
3508
+ } else if (/^\s*(?:<[^()]{0,200}>\s*)?\(/.test(rest)) {
3509
+ const open = part.at + key[0].length + rest.indexOf("(");
3510
+ const close2 = a.pairs.get(open);
3511
+ const brace = close2 === void 0 ? null : /^\s*(?::[^{;=]{0,200})?\{/.exec(a.code.slice(close2 + 1, close2 + 260));
3512
+ const start = close2 === void 0 || !brace ? void 0 : close2 + brace[0].length;
3513
+ const end = start === void 0 ? void 0 : a.pairs.get(start);
3514
+ entries.set(key[1], { value: null, range: start !== void 0 && end !== void 0 ? { file: a.file, start, end } : null });
3515
+ } else if (rest.trim() === "") {
3516
+ entries.set(key[1], { value: part, range: null });
3517
+ }
3518
+ }
3519
+ return entries;
3520
+ }
3521
+ function frameworkNamesOf(a) {
3522
+ const names = { express: /* @__PURE__ */ new Set(), router: /* @__PURE__ */ new Set(), hono: /* @__PURE__ */ new Set(), fastify: /* @__PURE__ */ new Set(), types: /* @__PURE__ */ new Set() };
3523
+ const named = (list, wanted, into, fallback) => {
3524
+ for (const part of list.split(",")) {
3525
+ const m = /^\s*(?:type\s+)?([A-Za-z_$][\w$]*)\s*(?:(?:as|:)\s*([A-Za-z_$][\w$]*))?\s*$/.exec(part);
3526
+ if (!m) continue;
3527
+ if (wanted.test(m[1]) && !/^\s*type\s/.test(part)) into.add(m[2] ?? m[1]);
3528
+ fallback?.(m[1]);
3529
+ }
3530
+ };
3531
+ 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)) {
3532
+ const typeOnly = Boolean(m[1]);
3533
+ if (m[5] === "express") {
3534
+ if (!typeOnly && m[2]) names.express.add(m[2]);
3535
+ if (!typeOnly && m[4]) names.express.add(m[4]);
3536
+ if (m[3]) named(m[3], /^Router$/, names.router);
3537
+ names.types.add("express");
3538
+ } else if (m[5] === "fastify") {
3539
+ if (!typeOnly && m[2]) names.fastify.add(m[2]);
3540
+ if (!typeOnly && m[3]) named(m[3], /^fastify$/, names.fastify);
3541
+ names.types.add("fastify");
3542
+ } else {
3543
+ if (!typeOnly && m[3]) named(m[3], /^(?:Hono|OpenAPIHono)$/, names.hono);
3544
+ names.types.add("hono");
3545
+ }
3546
+ }
3547
+ 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)) {
3548
+ if (m[3] === "express") {
3549
+ if (m[1]) names.express.add(m[1]);
3550
+ if (m[2]) named(m[2], /^Router$/, names.router);
3551
+ } else if (m[3] === "fastify") {
3552
+ if (m[1]) names.fastify.add(m[1]);
3553
+ if (m[2]) named(m[2], /^fastify$/, names.fastify);
3554
+ } else if (m[2]) named(m[2], /^(?:Hono|OpenAPIHono)$/, names.hono);
3555
+ names.types.add(m[3] === "express" || m[3] === "fastify" ? m[3] : "hono");
3556
+ }
3557
+ for (const m of a.source.matchAll(/\brequire\s*\(\s*['"](express|fastify)['"]\s*\)/g)) names.types.add(m[1]);
3558
+ return names.types.size ? names : null;
3559
+ }
3560
+ function instancesOf(a, names) {
3561
+ const found = [];
3562
+ const add = (instance) => {
3563
+ if (!found.some((other) => other.name === instance.name)) found.push(instance);
3564
+ };
3565
+ 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)) {
3566
+ const [, name, isNew, callee, router] = m;
3567
+ if (!isNew && (names.express.has(callee) && (router === "Router" || router === void 0) || names.router.has(callee) && router === void 0)) {
3568
+ add({ name, framework: "express", prefix: "", weak: false });
3569
+ } else if (isNew && names.hono.has(callee) && router === void 0) {
3570
+ const open = m.index + m[0].length - 1;
3571
+ let close = a.pairs.get(open);
3572
+ if (close === void 0) continue;
3573
+ let prefix = "";
3574
+ const base = /^\s*\.\s*basePath\s*\(/.exec(a.code.slice(close + 1, close + 200));
3575
+ if (base) {
3576
+ const baseOpen = close + base[0].length;
3577
+ const baseClose = a.pairs.get(baseOpen);
3578
+ const arg = baseClose === void 0 ? void 0 : argsOf(a, baseOpen, baseClose)[0];
3579
+ prefix = (arg && pathOf(a, arg)) ?? "";
3580
+ if (baseClose !== void 0) close = baseClose;
3581
+ }
3582
+ add({ name, framework: "hono", prefix, weak: false, chainFrom: close });
3583
+ } else if (!isNew && names.fastify.has(callee) && router === void 0) {
3584
+ add({ name, framework: "fastify", prefix: "", weak: false });
3585
+ }
3586
+ }
3587
+ 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)) {
3588
+ if (m[2] === "express" || !m[3]) add({ name: m[1], framework: m[2], prefix: "", weak: false });
3589
+ }
3590
+ const typed = [
3591
+ ["express", String.raw`(?:Express|Router|Application)\b(?!\s*[.<(])`],
3592
+ ["hono", String.raw`(?:Hono|OpenAPIHono)\b(?!\s*[.(])`],
3593
+ ["fastify", String.raw`FastifyInstance\b(?!\s*[.(])`]
3594
+ ];
3595
+ for (const [framework, type] of typed) {
3596
+ if (!names.types.has(framework)) continue;
3597
+ 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"))) {
3598
+ add({ name: m[1], framework, prefix: "", weak: true });
3599
+ }
3600
+ }
3601
+ return found;
3602
+ }
3603
+ function literalOf(arg) {
3604
+ const m = /^(['"`])([^'"`$]*)\1$/.exec(arg.source);
3605
+ return m ? m[2] : null;
3606
+ }
3607
+ function constantOf(a, name) {
3608
+ const m = new RegExp(`\\bconst\\s+${name.replace(/\$/g, "\\$")}\\s*(?::\\s*string\\s*)?=\\s*(['"\`])([^'"\`$]*)\\1`).exec(a.source);
3609
+ return m ? m[2] : null;
3610
+ }
3611
+ function pathsOf(a, arg) {
3612
+ if (!arg.text.startsWith("[")) return [pathOf(a, arg)];
3613
+ const close = a.pairs.get(arg.at);
3614
+ const items = close === void 0 ? [] : argsOf(a, arg.at, close);
3615
+ return items.length ? items.map((item) => pathOf(a, item)) : [null];
3616
+ }
3617
+ function usePrefixes(a, arg) {
3618
+ if (arg.text.startsWith("[")) {
3619
+ const close = a.pairs.get(arg.at);
3620
+ const items = close === void 0 ? [] : argsOf(a, arg.at, close);
3621
+ if (items.length === 0 || !items.every((item) => /^['"`]/.test(item.source))) return null;
3622
+ return items.map((item) => pathOf(a, item)).filter((path) => path !== null);
3623
+ }
3624
+ const prefix = pathOf(a, arg);
3625
+ return prefix === null ? null : [prefix];
3626
+ }
3627
+ function pathOf(a, arg) {
3628
+ if (arg.text.startsWith("[")) return null;
3629
+ const literal2 = literalOf(arg);
3630
+ if (literal2 !== null) return literal2;
3631
+ if (/^[A-Za-z_$][\w$]*$/.test(arg.source)) return constantOf(a, arg.source);
3632
+ const template = /^`((?:[^`$\\]|\$\{\s*[A-Za-z_$][\w$]*\s*\})*)`$/.exec(arg.source);
3633
+ if (!template) return null;
3634
+ let unresolved = false;
3635
+ const path = template[1].replace(/\$\{\s*([A-Za-z_$][\w$]*)\s*\}/g, (_whole, name) => {
3636
+ const value = constantOf(a, name);
3637
+ if (value === null) unresolved = true;
3638
+ return value ?? "";
3639
+ });
3640
+ return unresolved ? null : path;
3641
+ }
3642
+ function joinPath(prefix, path) {
3643
+ if (!prefix || prefix === "/" || !path.startsWith("/")) return path;
3644
+ const base = prefix.replace(/\/+$/, "");
3645
+ return path === "/" ? base || "/" : `${base}${path}`;
3646
+ }
3647
+ function expressRegistrations(a, name) {
3648
+ const found = [];
3649
+ const escaped = name.replace(/\$/g, "\\$");
3650
+ const add = (paths, args, at) => {
3651
+ const callbacks = flatten(a, args);
3652
+ if (callbacks.length === 0) return;
3653
+ for (const path of paths) found.push({ path, middleware: callbacks.slice(0, -1), handler: callbacks.at(-1), handlerRange: null, at });
3654
+ };
3655
+ for (const m of a.code.matchAll(new RegExp(`(?<![\\w$.])${escaped}\\s*\\.\\s*(?:${METHODS})\\s*${GENERICS}\\(`, "g"))) {
3656
+ const open = m.index + m[0].length - 1;
3657
+ const close = a.pairs.get(open);
3658
+ if (close === void 0) continue;
3659
+ const args = argsOf(a, open, close);
3660
+ if (args.length < 2) continue;
3661
+ add(pathsOf(a, args[0]), args.slice(1), m.index);
3662
+ }
3663
+ for (const m of a.code.matchAll(new RegExp(`(?<![\\w$.])${escaped}\\s*\\.\\s*route\\s*\\(`, "g"))) {
3664
+ const open = m.index + m[0].length - 1;
3665
+ let close = a.pairs.get(open);
3666
+ if (close === void 0) continue;
3667
+ const pathArg = argsOf(a, open, close)[0];
3668
+ const paths = pathArg ? pathsOf(a, pathArg) : [null];
3669
+ for (let guard = 0; guard < 16; guard++) {
3670
+ const next = new RegExp(`^\\s*\\.\\s*(?:${METHODS})\\s*${GENERICS}\\(`).exec(a.code.slice(close + 1, close + 500));
3671
+ if (!next) break;
3672
+ const methodOpen = close + 1 + next[0].length - 1;
3673
+ const methodClose = a.pairs.get(methodOpen);
3674
+ if (methodClose === void 0) break;
3675
+ add(paths, argsOf(a, methodOpen, methodClose), close + 1 + next[0].search(/[a-z]/));
3676
+ close = methodClose;
3677
+ }
3678
+ }
3679
+ return found.sort((x, y) => x.at - y.at);
3680
+ }
3681
+ function expressUses(a, name) {
3682
+ const found = [];
3683
+ for (const m of a.code.matchAll(new RegExp(`(?<![\\w$.])${name.replace(/\$/g, "\\$")}\\s*\\.\\s*use\\s*\\(`, "g"))) {
3684
+ const open = m.index + m[0].length - 1;
3685
+ const close = a.pairs.get(open);
3686
+ if (close === void 0) continue;
3687
+ const args = argsOf(a, open, close);
3688
+ const prefixes = args[0] ? usePrefixes(a, args[0]) : null;
3689
+ const middleware = flatten(a, prefixes === null ? args : args.slice(1));
3690
+ for (const prefix of prefixes ?? [null]) found.push({ prefix, args: middleware, at: m.index });
3691
+ }
3692
+ return found.sort((x, y) => x.at - y.at);
3693
+ }
3694
+ function honoOps(a, name, chainFrom) {
3695
+ const registrations = [];
3696
+ const uses = [];
3697
+ const visit = (method, open, close, at, previous) => {
3698
+ const args = argsOf(a, open, close);
3699
+ if (method === "use") {
3700
+ const prefixes = args[0] ? usePrefixes(a, args[0]) : null;
3701
+ const middleware = flatten(a, prefixes === null ? args : args.slice(1));
3702
+ for (const prefix of prefixes ?? [null]) uses.push({ prefix, args: middleware, at });
3703
+ return previous;
3704
+ }
3705
+ if (method === "route") {
3706
+ if (args.length >= 2) uses.push({ prefix: pathOf(a, args[0]), args: [args[1]], at, mount: true });
3707
+ return previous;
3708
+ }
3709
+ let rest = method === "on" ? args.slice(1) : args;
3710
+ let paths = previous;
3711
+ if (rest[0] && /^['"`[]/.test(rest[0].source)) {
3712
+ paths = pathsOf(a, rest[0]);
3713
+ rest = rest.slice(1);
3714
+ }
3715
+ if (rest.length > 0) {
3716
+ for (const path of paths) registrations.push({ path, middleware: rest.slice(0, -1), handler: rest.at(-1), handlerRange: null, at });
3717
+ }
3718
+ return paths;
3719
+ };
3720
+ const chain = (after, previous) => {
3721
+ for (let guard = 0; guard < 64; guard++) {
3722
+ const next = new RegExp(`^\\s*\\.\\s*(${HONO_CALL})\\s*${GENERICS}\\(`).exec(a.code.slice(after + 1, after + 500));
3723
+ if (!next) return;
3724
+ const open = after + next[0].length;
3725
+ const close = a.pairs.get(open);
3726
+ if (close === void 0) return;
3727
+ previous = visit(next[1], open, close, after + 1 + next[0].search(/[a-z]/), previous);
3728
+ after = close;
3729
+ }
3730
+ };
3731
+ if (chainFrom !== void 0) chain(chainFrom, [null]);
3732
+ for (const m of a.code.matchAll(new RegExp(`(?<![\\w$.])${name.replace(/\$/g, "\\$")}\\s*\\.\\s*(${HONO_CALL})\\s*${GENERICS}\\(`, "g"))) {
3733
+ const open = m.index + m[0].length - 1;
3734
+ const close = a.pairs.get(open);
3735
+ if (close === void 0) continue;
3736
+ chain(close, visit(m[1], open, close, m.index, [null]));
3737
+ }
3738
+ registrations.sort((x, y) => x.at - y.at);
3739
+ uses.sort((x, y) => x.at - y.at);
3740
+ return { registrations, uses };
3741
+ }
3742
+ function fastifyOps(a, name) {
3743
+ const registrations = [];
3744
+ const uses = [];
3745
+ const registers = [];
3746
+ const lead = String.raw`(?<![\w$.])${name.replace(/\$/g, "\\$")}\s*\.\s*(?:withTypeProvider\s*<[^()]{0,200}>\s*\(\s*\)\s*\.\s*)?`;
3747
+ const hooksOf = (entries) => [...entries].filter(([key, entry]) => FASTIFY_HOOKS.test(key) && entry.value).flatMap(([, entry]) => flatten(a, [entry.value]));
3748
+ for (const m of a.code.matchAll(new RegExp(`${lead}(${METHODS}|route|addHook|register)\\s*${GENERICS}\\(`, "g"))) {
3749
+ const open = m.index + m[0].length - 1;
3750
+ const close = a.pairs.get(open);
3751
+ if (close === void 0) continue;
3752
+ const args = argsOf(a, open, close);
3753
+ const call = m[1];
3754
+ if (call === "addHook") {
3755
+ const hook = args[0] ? literalOf(args[0]) : null;
3756
+ if (hook && FASTIFY_HOOKS.test(hook) && args[1]) uses.push({ prefix: null, args: flatten(a, [args[1]]), at: m.index });
3757
+ } else if (call === "register") {
3758
+ if (!args[0]) continue;
3759
+ registers.push({ target: args[0], options: args[1], at: m.index });
3760
+ const ident = /^[A-Za-z_$][\w$]*$/.exec(args[0].text)?.[0];
3761
+ if (ident && packageOf(a, ident) === "@fastify/bearer-auth" && !/^false$/.test(entriesOf(a, args[1]).get("addHook")?.value?.text ?? "")) {
3762
+ uses.push({ prefix: null, args: [args[0]], at: m.index });
3763
+ }
3764
+ } else if (call === "route") {
3765
+ const entries = entriesOf(a, args[0]);
3766
+ const url = entries.get("url")?.value ?? entries.get("path")?.value;
3767
+ const handler = entries.get("handler");
3768
+ if (!handler) continue;
3769
+ registrations.push({ path: url ? pathOf(a, url) : null, middleware: hooksOf(entries), handler: handler.value, handlerRange: handler.range, at: m.index });
3770
+ } else {
3771
+ if (args.length < 2) continue;
3772
+ const path = pathOf(a, args[0]);
3773
+ const options = entriesOf(a, args[1]);
3774
+ const objectOnly = args.length === 2 && args[1].text.startsWith("{");
3775
+ const inOptions = objectOnly ? options.get("handler") : void 0;
3776
+ if (objectOnly && !inOptions) continue;
3777
+ registrations.push({
3778
+ path,
3779
+ middleware: args.length >= 3 || inOptions ? hooksOf(options) : [],
3780
+ handler: inOptions ? inOptions.value : args.at(-1),
3781
+ handlerRange: inOptions?.range ?? null,
3782
+ at: m.index
3783
+ });
3784
+ }
3785
+ }
3786
+ registrations.sort((x, y) => x.at - y.at);
3787
+ uses.sort((x, y) => x.at - y.at);
3788
+ return { registrations, uses, registers };
3789
+ }
3790
+ var pathMatches = (prefix, path, framework) => {
3791
+ if (prefix === null) return true;
3792
+ const wildcard = /\*$/.test(prefix);
3793
+ if (framework === "hono" && !wildcard) return path !== null && path.replace(/\/+$/, "") === prefix.replace(/\/+$/, "");
3794
+ const base = prefix.replace(/\/?\*$/, "");
3795
+ return base === "" || base === "/" || path !== null && (path === base || path.startsWith(base.endsWith("/") ? base : `${base}/`));
3796
+ };
3797
+ function expressionEnd(a, from) {
3798
+ for (let i = from; i < a.code.length; i++) {
3799
+ const end = a.pairs.get(i);
3800
+ if (end !== void 0) {
3801
+ i = end;
3802
+ continue;
3803
+ }
3804
+ const ch = a.code[i];
3805
+ if (ch === ";" || ch === "," || ch === ")" || ch === "]" || ch === "}") return i;
3806
+ if (ch === "\n") {
3807
+ const after = a.code.slice(i + 1, i + 40).trimStart();
3808
+ const before = a.code.slice(from, i).trimEnd();
3809
+ if (before === "" || /[=+\-*/%&|^!?:,.([{<>]$/.test(before) || /^(?:[.?+\-*/%&|^,:<>=]|=>)/.test(after)) continue;
3810
+ return i;
3811
+ }
3812
+ }
3813
+ return a.code.length;
3814
+ }
3815
+ function skipGenerics(code, from) {
3816
+ if (code[from] !== "<") return from;
3817
+ let depth = 0;
3818
+ for (let i = from; i < Math.min(code.length, from + 2e3); i++) {
3819
+ if (code[i] === "<") depth++;
3820
+ else if (code[i] === ">" && code[i - 1] !== "=" && --depth === 0) return i + 1;
3821
+ }
3822
+ return from;
3823
+ }
3824
+ function functionAt(a, from, to) {
3825
+ const body = a.bodies.find((b) => b.declaration >= from && b.start < to);
3826
+ if (body) return { file: a.file, start: body.start, end: body.end };
3827
+ const arrow = a.code.indexOf("=>", from);
3828
+ return arrow !== -1 && arrow < to ? { file: a.file, start: arrow, end: to, arrow: true } : null;
3829
+ }
3830
+ var FUNCTION_EXPRESSION = /^(?:async\b\s*)?(?:function\b|\([^()]*\)\s*(?::[^=]{0,200})?=>|[A-Za-z_$][\w$]*\s*=>)/;
3831
+ function findFunction(a, name, depth = 0) {
3832
+ if (depth > 3) return null;
3833
+ const escaped = name.replace(/\$/g, "\\$");
3834
+ for (const body of a.bodies) {
3835
+ if (declarationOf(a.code, body, a.openers)?.name === name && /^function\b/.test(a.code.slice(body.declaration, body.declaration + 8))) {
3836
+ return { file: a.file, start: body.start, end: body.end };
3837
+ }
3838
+ }
3839
+ const assigned = new RegExp(`(?:\\b(?:const|let|var)\\s+${escaped}\\s*(?::[^=;]{0,200})?|(?<![\\w$])(?:module\\s*\\.\\s*)?exports\\s*\\.\\s*${escaped}\\s*)=(?![=>])\\s*`, "g");
3840
+ for (const m of a.code.matchAll(assigned)) {
3841
+ const from = m.index + m[0].length;
3842
+ if (/^(?:await\s+)?(?:require\s*\(|import\s*\()/.test(a.code.slice(from, from + 20))) continue;
3843
+ const alias = /^([A-Za-z_$][\w$]*)\s*(?:[;,\n}]|$)/.exec(a.code.slice(from, from + 120));
3844
+ if (alias && alias[1] !== name && !/^(?:async|function)$/.test(alias[1])) {
3845
+ const target = findFunction(a, alias[1], depth + 1);
3846
+ if (target) return target;
3847
+ }
3848
+ return { file: a.file, start: from, end: expressionEnd(a, skipGenerics(a.code, from)) };
3849
+ }
3850
+ for (const m of a.code.matchAll(new RegExp(`(?<![\\w$.])(?:async\\s+)?${escaped}\\s*(?:\\(|:\\s*)`, "g"))) {
3851
+ const at = m.index + m[0].length;
3852
+ if (m[0].endsWith("(")) {
3853
+ const close = a.pairs.get(at - 1);
3854
+ if (close === void 0) continue;
3855
+ const brace = /^\s*(?::[^{;=]{0,200})?\{/.exec(a.code.slice(close + 1, close + 260));
3856
+ if (!brace) continue;
3857
+ const open = close + brace[0].length;
3858
+ const end = a.pairs.get(open);
3859
+ if (end !== void 0) return { file: a.file, start: open, end };
3860
+ continue;
3861
+ }
3862
+ if (/^(?:async\b\s*)?(?:function\b|\(|[A-Za-z_$][\w$]*\s*=>)/.test(a.code.slice(at, at + 40))) {
3863
+ return { file: a.file, start: at, end: expressionEnd(a, at) };
3864
+ }
3865
+ }
3866
+ return null;
3867
+ }
3868
+ function defaultExport(a) {
3869
+ const m = /\bexport\s+default\s+|(?<![\w$.])module\s*\.\s*exports\s*=(?!=)\s*/.exec(a.code);
3870
+ if (!m) return null;
3871
+ const from = m.index + m[0].length;
3872
+ const name = /^([A-Za-z_$][\w$]*)\s*(?:[;\n]|$)/.exec(a.code.slice(from, from + 120))?.[1];
3873
+ if (name && !/^(?:async|function|class)$/.test(name)) return findFunction(a, name);
3874
+ return functionAt(a, from, expressionEnd(a, from)) ?? { file: a.file, start: from, end: expressionEnd(a, from) };
3875
+ }
3876
+ function moduleFor(spec, file, files) {
3877
+ const direct = bindingModule(spec, file, files, scopeOf(file.path));
3878
+ if (direct || spec.startsWith(".")) return direct;
3879
+ const parts = file.path.split("/").slice(0, -1);
3880
+ for (let i = parts.length; i >= 0; i--) {
3881
+ const found = bindingModule(spec, file, files, i > 0 ? `${parts.slice(0, i).join("/")}/` : "");
3882
+ if (found) return found;
3883
+ }
3884
+ return null;
3885
+ }
3886
+ function importOf(a, local, files) {
3887
+ const resolve3 = (spec, name) => {
3888
+ const file = moduleFor(spec, a.file, files);
3889
+ return file ? { file, name } : null;
3890
+ };
3891
+ const escaped = local.replace(/\$/g, "\\$");
3892
+ const es = bindingsOf(a.file).imports.find((binding) => binding.local === local && binding.spec);
3893
+ if (es) return resolve3(es.spec, es.imported);
3894
+ const namespace = new RegExp(`\\bimport\\s+(?:${escaped}\\s*,[^;]*?|\\*\\s+as\\s+${escaped}\\s+|${escaped}\\s*,\\s*\\{[^{}]*\\}\\s*)from\\s*['"]([^'"]+)['"]`).exec(a.source);
3895
+ if (namespace) return resolve3(namespace[1], "default");
3896
+ const whole = new RegExp(`\\b(?:const|let|var)\\s+${escaped}\\s*=\\s*require\\s*\\(\\s*['"]([^'"]+)['"]\\s*\\)`).exec(a.source);
3897
+ if (whole) return resolve3(whole[1], "default");
3898
+ for (const m of a.source.matchAll(/\b(?:const|let|var)\s*\{([^{}]*)\}\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/g)) {
3899
+ for (const part of m[1].split(",")) {
3900
+ const names = /^\s*([A-Za-z_$][\w$]*)\s*(?::\s*([A-Za-z_$][\w$]*))?\s*$/.exec(part);
3901
+ if (names && (names[2] ?? names[1]) === local) return resolve3(m[2], names[1]);
3902
+ }
3903
+ }
3904
+ return null;
3905
+ }
3906
+ function packageOf(a, local) {
3907
+ const escaped = local.replace(/\$/g, "\\$");
3908
+ const es = bindingsOf(a.file).imports.find((binding) => binding.local === local);
3909
+ const cjs = new RegExp(`\\b(?:const|let|var)\\s+(?:${escaped}|\\{[^{}]*\\b${escaped}\\b[^{}]*\\})\\s*=\\s*require\\s*\\(\\s*['"]([^'"]+)['"]\\s*\\)`).exec(a.source);
3910
+ const named = new RegExp(`\\bimport\\s+${escaped}\\s*(?:,[^;]*?)?from\\s*['"]([^'"]+)['"]`).exec(a.source);
3911
+ const spec = es?.spec ?? cjs?.[1] ?? named?.[1] ?? null;
3912
+ return spec && !spec.startsWith(".") && !/^(?:[@~#]|~~|@@|\$lib)\//.test(spec) ? spec : null;
3913
+ }
3914
+ function importedFunction(target, member) {
3915
+ const ta = analyse(target.file);
3916
+ if (member) return findFunction(ta, member);
3917
+ return target.name === "default" ? defaultExport(ta) : findFunction(ta, target.name);
3918
+ }
3919
+ function exportedMember(file, name, files, depth) {
3920
+ const t = analyse(file);
3921
+ const own = findFunction(t, name);
3922
+ if (own || depth >= 3) return own;
3923
+ const escaped = name.replace(/\$/g, "\\$");
3924
+ for (const m of t.source.matchAll(/\bexport\s*\{([^{}]*)\}\s*from\s*['"]([^'"]+)['"]/g)) {
3925
+ for (const part of m[1].split(",")) {
3926
+ const names = new RegExp(`^\\s*(?:type\\s+)?([A-Za-z_$][\\w$]*)\\s*(?:as\\s+${escaped})?\\s*$`).exec(part);
3927
+ if (!names || names[1] !== name && !part.includes(" as ")) continue;
3928
+ const module = moduleFor(m[2], file, files);
3929
+ if (module) return exportedMember(module, names[1], files, depth + 1);
3930
+ }
3931
+ }
3932
+ const spreads = [
3933
+ ...[...t.source.matchAll(/\bexport\s*\*\s*from\s*['"]([^'"]+)['"]/g)].map((m) => moduleFor(m[1], file, files)),
3934
+ ...[...t.source.matchAll(/\.\.\.\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/g)].map((m) => moduleFor(m[1], file, files)),
3935
+ ...[...t.code.matchAll(/\.\.\.\s*([A-Za-z_$][\w$]*)\s*[,}]/g)].map((m) => importOf(t, m[1], files)?.file ?? null)
3936
+ ];
3937
+ for (const module of spreads) {
3938
+ const found = module && module !== file ? exportedMember(module, name, files, depth + 1) : null;
3939
+ if (found) return found;
3940
+ }
3941
+ const imported = importOf(t, name, files);
3942
+ return imported && imported.file !== file ? exportedMember(imported.file, imported.name === "default" ? name : imported.name, files, depth + 1) : null;
3943
+ }
3944
+ function calleeDefinition(file, name, member, files) {
3945
+ const a = analyse(file);
3946
+ if (member === null) {
3947
+ const local = findFunction(a, name);
3948
+ if (local) return local;
3949
+ }
3950
+ const target = importOf(a, name, files);
3951
+ if (!target || target.file === file) return null;
3952
+ if (member === null && target.name === "default") return defaultExport(analyse(target.file));
3953
+ return exportedMember(target.file, member ?? target.name, files, 0);
3954
+ }
3955
+ function handlerOf(a, arg, files, depth = 0) {
3956
+ const text = arg.text;
3957
+ if (FUNCTION_EXPRESSION.test(text)) return functionAt(a, arg.at, arg.at + text.length);
3958
+ const call = /^([A-Za-z_$][\w$.]*)\s*\(/.exec(text);
3959
+ if (call && depth < 3) {
3960
+ const open = arg.at + call[0].length - 1;
3961
+ const close = a.pairs.get(open);
3962
+ if (close === arg.at + text.length - 1) {
3963
+ const inner = argsOf(a, open, close).at(-1);
3964
+ return inner ? handlerOf(a, inner, files, depth + 1) : null;
3965
+ }
3966
+ return null;
3967
+ }
3968
+ const ident = /^([A-Za-z_$][\w$]*)$/.exec(text);
3969
+ if (ident) {
3970
+ const local = findFunction(a, ident[1]);
3971
+ if (local) return local;
3972
+ const target = importOf(a, ident[1], files);
3973
+ return target ? importedFunction(target, null) : null;
3974
+ }
3975
+ const member = /^([A-Za-z_$][\w$]*)\s*\.\s*([A-Za-z_$][\w$]*)$/.exec(text);
3976
+ if (member) {
3977
+ const target = importOf(a, member[1], files);
3978
+ return target ? importedFunction(target, member[2]) : null;
3979
+ }
3980
+ return null;
3981
+ }
3982
+ function pluginOf(a, arg, files, depth = 0) {
3983
+ if (depth > 3) return null;
3984
+ const inline = /^(?:await\s+)?(?:require|import)\s*\(\s*['"]([^'"]+)['"]\s*\)$/.exec(arg.source);
3985
+ if (inline) {
3986
+ const module = moduleFor(inline[1], a.file, files);
3987
+ return module ? exportedPlugin(analyse(module), files, depth + 1) : null;
3988
+ }
3989
+ if (FUNCTION_EXPRESSION.test(arg.text)) return functionAt(a, arg.at, arg.at + arg.text.length);
3990
+ const call = /^([A-Za-z_$][\w$.]*)\s*\(/.exec(arg.text);
3991
+ if (call) {
3992
+ const open = arg.at + call[0].length - 1;
3993
+ const close = a.pairs.get(open);
3994
+ if (close === void 0) return null;
3995
+ for (const inner of argsOf(a, open, close)) {
3996
+ const range = pluginOf(a, inner, files, depth + 1);
3997
+ if (range) return range;
3998
+ }
3999
+ return null;
4000
+ }
4001
+ const ident = /^([A-Za-z_$][\w$]*)$/.exec(arg.text);
4002
+ if (ident) {
4003
+ const local = findFunction(a, ident[1]);
4004
+ if (local) {
4005
+ const text = a.code.slice(local.start, local.end).trim();
4006
+ 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;
4007
+ }
4008
+ const target = importOf(a, ident[1], files);
4009
+ if (!target) return null;
4010
+ return target.name === "default" ? exportedPlugin(analyse(target.file), files, depth + 1) : findFunction(analyse(target.file), target.name);
4011
+ }
4012
+ return handlerOf(a, arg, files);
4013
+ }
4014
+ function exportedPlugin(t, files, depth) {
4015
+ const m = /\bexport\s+default\s+|(?<![\w$.])module\s*\.\s*exports\s*=(?!=)\s*/.exec(t.code);
4016
+ if (!m) return null;
4017
+ const from = m.index + m[0].length;
4018
+ const end = expressionEnd(t, from);
4019
+ const text = t.code.slice(from, end).trimEnd();
4020
+ return pluginOf(t, { text, source: t.source.slice(from, from + text.length), at: from }, files, depth);
4021
+ }
4022
+ var toRefs = (a, args) => args.map((arg) => ({ text: arg.source, at: arg.at, file: a.file }));
4023
+ function mountedFile(a, arg, files) {
4024
+ const inline = /^require\s*\(\s*['"]([^'"]+)['"]\s*\)$/.exec(arg.source);
4025
+ if (inline) return moduleFor(inline[1], a.file, files);
4026
+ const ident = /^([A-Za-z_$][\w$]*)$/.exec(arg.text);
4027
+ return ident ? importOf(a, ident[1], files)?.file ?? null : null;
4028
+ }
4029
+ function functionParams(range) {
4030
+ return paramNamesOf(analyse(range.file), range).filter(Boolean);
4031
+ }
4032
+ function paramNamesOf(a, range) {
4033
+ const code = a.code;
4034
+ let open;
4035
+ if (code[range.start] === "{") {
4036
+ let i = range.start - 1;
4037
+ const skip = () => {
4038
+ while (i >= 0 && /\s/.test(code[i])) i--;
4039
+ };
4040
+ skip();
4041
+ let arrow = false;
4042
+ if (code[i] === ">" && code[i - 1] === "=") {
4043
+ arrow = true;
4044
+ i -= 2;
4045
+ skip();
4046
+ }
4047
+ if (arrow && /[\w$]/.test(code[i] ?? "")) {
4048
+ const end = i + 1;
4049
+ while (i >= 0 && /[\w$]/.test(code[i])) i--;
4050
+ return [code.slice(i + 1, end)];
4051
+ }
4052
+ if (code[i] !== ")") {
4053
+ const window = code.slice(Math.max(0, i - 200), i + 1);
4054
+ const close2 = window.lastIndexOf(")");
4055
+ if (close2 === -1) return [];
4056
+ i -= window.length - 1 - close2;
4057
+ }
4058
+ open = a.openers.get(i);
4059
+ } else {
4060
+ const head = /^\s*(?:async\b\s*)?(?:function\b[^(]{0,80})?\(/.exec(code.slice(range.start, range.start + 200));
4061
+ if (head) open = range.start + head[0].length - 1;
4062
+ else {
4063
+ const single = /^\s*(?:async\s+)?([A-Za-z_$][\w$]*)\s*=>/.exec(code.slice(range.start, range.start + 200));
4064
+ return single ? [single[1]] : [];
4065
+ }
4066
+ }
4067
+ if (open === void 0) return [];
4068
+ const close = a.pairs.get(open);
4069
+ if (close === void 0) return [];
4070
+ return argsOf(a, open, close).map((arg) => /^(?:\.\.\.)?\s*([A-Za-z_$][\w$]*)/.exec(arg.text)?.[1] ?? "");
4071
+ }
4072
+ var within = (site, at) => site.range === null || site.range.start <= at && at <= site.range.end;
4073
+ var opsCache = /* @__PURE__ */ new WeakMap();
4074
+ function opsOf(site) {
4075
+ const cached = opsCache.get(site);
4076
+ if (cached) return cached;
4077
+ const { a, name } = site;
4078
+ let ops;
4079
+ if (site.framework === "express") ops = { registrations: expressRegistrations(a, name), uses: expressUses(a, name), registers: [] };
4080
+ else if (site.framework === "hono") ops = { ...honoOps(a, name, site.chainFrom), registers: [] };
4081
+ else ops = fastifyOps(a, name);
4082
+ const result = {
4083
+ registrations: ops.registrations.filter((reg) => within(site, reg.at)),
4084
+ uses: ops.uses.filter((use) => within(site, use.at)),
4085
+ registers: ops.registers.filter((reg) => within(site, reg.at))
4086
+ };
4087
+ opsCache.set(site, result);
4088
+ return result;
4089
+ }
4090
+ function generalMiddleware(site, before, files) {
4091
+ const args = opsOf(site).uses.filter((use) => !use.mount && use.at < before && (use.prefix === null || use.prefix === "/" || /^\/?\*$/.test(use.prefix))).flatMap((use) => use.args).filter((arg) => site.framework !== "express" || mountedFile(site.a, arg, files) === null);
4092
+ return toRefs(site.a, args);
4093
+ }
4094
+ function passedSites(site, files) {
4095
+ const { a } = site;
4096
+ const found = [];
4097
+ const receive = (range, index, at) => {
4098
+ if (!range) return;
4099
+ const target = analyse(range.file);
4100
+ const name = paramNamesOf(target, range)[index];
4101
+ if (!name) return;
4102
+ found.push({
4103
+ a: target,
4104
+ name,
4105
+ framework: site.framework,
4106
+ range: { start: range.start, end: range.end },
4107
+ prefix: site.prefix,
4108
+ root: site.root ?? site,
4109
+ inherited: [...site.inherited, ...generalMiddleware(site, at, files)]
4110
+ });
4111
+ };
4112
+ for (const m of a.source.matchAll(/\brequire\s*\(\s*['"]([^'"]+)['"]\s*\)\s*\(/g)) {
4113
+ if (!within(site, m.index)) continue;
4114
+ const open = m.index + m[0].length - 1;
4115
+ const close = a.pairs.get(open);
4116
+ if (close === void 0) continue;
4117
+ const index = argsOf(a, open, close).findIndex((arg) => arg.text === site.name);
4118
+ const module = index === -1 ? null : moduleFor(m[1], a.file, files);
4119
+ if (module) receive(defaultExport(analyse(module)), index, m.index);
4120
+ }
4121
+ for (const m of a.code.matchAll(/(?<![\w$.])(?:([A-Za-z_$][\w$]*)\s*\.\s*)?([A-Za-z_$][\w$]*)\s*\(/g)) {
4122
+ if (!within(site, m.index) || m[1] === site.name || m[2] === "require" || m[2] === site.name) continue;
4123
+ const open = m.index + m[0].length - 1;
4124
+ const close = a.pairs.get(open);
4125
+ if (close === void 0) continue;
4126
+ const index = argsOf(a, open, close).findIndex((arg) => arg.text === site.name);
4127
+ if (index === -1) continue;
4128
+ if (m[1]) {
4129
+ const target = importOf(a, m[1], files);
4130
+ receive(target ? importedFunction(target, m[2]) : null, index, m.index);
4131
+ } else {
4132
+ const local = findFunction(a, m[2]);
4133
+ if (local) {
4134
+ receive(local, index, m.index);
4135
+ continue;
4136
+ }
4137
+ const target = importOf(a, m[2], files);
4138
+ receive(target ? importedFunction(target, null) : null, index, m.index);
4139
+ }
4140
+ }
4141
+ return found;
4142
+ }
4143
+ function normalizePath(path) {
4144
+ const parts = [];
4145
+ for (const part of path.split("/")) {
4146
+ if (part === "" || part === ".") continue;
4147
+ if (part === "..") parts.pop();
4148
+ else parts.push(part);
4149
+ }
4150
+ return parts.join("/");
4151
+ }
4152
+ function registeredSites(site, files) {
4153
+ const { a } = site;
4154
+ const found = [];
4155
+ for (const reg of opsOf(site).registers) {
4156
+ const options = entriesOf(a, reg.options);
4157
+ const prefixArg = options.get("prefix")?.value;
4158
+ const prefix = joinPath(site.prefix, (prefixArg && pathOf(a, prefixArg)) ?? "");
4159
+ const inherited = [...site.inherited, ...generalMiddleware(site, reg.at, files)];
4160
+ const child = (range, childPrefix, hooks) => {
4161
+ if (!range) return;
4162
+ const target = analyse(range.file);
4163
+ const name = paramNamesOf(target, range)[0];
4164
+ if (name) found.push({
4165
+ a: target,
4166
+ name,
4167
+ framework: "fastify",
4168
+ range: { start: range.start, end: range.end },
4169
+ prefix: childPrefix,
4170
+ root: site.root ?? site,
4171
+ inherited: [...inherited, ...hooks]
4172
+ });
4173
+ };
4174
+ const ident = /^[A-Za-z_$][\w$]*$/.exec(reg.target.text)?.[0];
4175
+ if (ident && packageOf(a, ident) === "@fastify/autoload") {
4176
+ const dirValue = options.get("dir")?.value;
4177
+ if (!dirValue) continue;
4178
+ const parts = [...dirValue.source.matchAll(/['"`]([^'"`$]+)['"`]/g)].map((m) => m[1]);
4179
+ if (parts.length === 0) continue;
4180
+ const here = a.file.path.includes("/") ? a.file.path.slice(0, a.file.path.lastIndexOf("/")) : "";
4181
+ const dir = normalizePath(`${here}/${parts.join("/")}`);
4182
+ const optionPrefix = entriesOf(a, options.get("options")?.value ?? void 0).get("prefix")?.value;
4183
+ const base = joinPath(prefix, (optionPrefix && pathOf(a, optionPrefix)) ?? "");
4184
+ const autoHooks = /^true$/.test(options.get("autoHooks")?.value?.text ?? "");
4185
+ 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));
4186
+ const hooksByDir = /* @__PURE__ */ new Map();
4187
+ if (autoHooks) {
4188
+ for (const file of plugins.filter((f) => /(?:^|\/)autohooks\.[mc]?[jt]s$/.test(f.path))) {
4189
+ const range = exportedPlugin(analyse(file), files, 0);
4190
+ const name = range ? paramNamesOf(analyse(file), range)[0] : void 0;
4191
+ if (!range || !name) continue;
4192
+ const hookSite = { a: analyse(file), name, framework: "fastify", range: { start: range.start, end: range.end }, prefix: "", inherited: [] };
4193
+ hooksByDir.set(file.path.slice(0, file.path.lastIndexOf("/")), generalMiddleware(hookSite, Infinity, files));
4194
+ }
4195
+ }
4196
+ for (const file of plugins) {
4197
+ if (/(?:^|\/)autohooks\.[mc]?[jt]s$/.test(file.path)) continue;
4198
+ const relative3 = file.path.slice(dir.length + 1);
4199
+ const folders = relative3.split("/").slice(0, -1);
4200
+ const hooks = [...hooksByDir].filter(([folder]) => file.path.startsWith(`${folder}/`)).flatMap(([, refs]) => refs);
4201
+ child(exportedPlugin(analyse(file), files, 0), joinPath(base, folders.length ? `/${folders.join("/")}` : "/"), hooks);
4202
+ }
4203
+ continue;
4204
+ }
4205
+ child(pluginOf(a, reg.target, files), prefix, []);
4206
+ }
4207
+ return found;
4208
+ }
4209
+ var indexCache = /* @__PURE__ */ new WeakMap();
4210
+ function serverIndex(files) {
4211
+ const cached = indexCache.get(files);
4212
+ if (cached) return cached;
4213
+ const sites = [];
4214
+ const seen = /* @__PURE__ */ new Set();
4215
+ const add = (site) => {
4216
+ const key = `${site.a.file.path}\0${site.name}\0${site.range?.start ?? -1}\0${site.prefix}`;
4217
+ if (seen.has(key)) return false;
4218
+ seen.add(key);
4219
+ sites.push(site);
4220
+ return true;
4221
+ };
4222
+ for (const file of files) {
4223
+ if (!/\.[mc]?[jt]sx?$/.test(file.path) || !/\b(?:express|hono|fastify)\b/.test(file.content)) continue;
4224
+ const a = analyse(file);
4225
+ const names = frameworkNamesOf(a);
4226
+ if (!names) continue;
4227
+ for (const instance of instancesOf(a, names)) {
4228
+ add({
4229
+ a,
4230
+ name: instance.name,
4231
+ framework: instance.framework,
4232
+ range: null,
4233
+ prefix: instance.prefix,
4234
+ inherited: [],
4235
+ weak: instance.weak,
4236
+ ...instance.chainFrom === void 0 ? {} : { chainFrom: instance.chainFrom }
4237
+ });
4238
+ }
4239
+ }
4240
+ let frontier = [...sites];
4241
+ for (let level = 0; level < 4 && frontier.length; level++) {
4242
+ const next = [];
4243
+ for (const site of frontier) {
4244
+ const children = [...passedSites(site, files), ...site.framework === "fastify" ? registeredSites(site, files) : []];
4245
+ for (const child of children) if (add(child)) next.push(child);
4246
+ }
4247
+ frontier = next;
4248
+ }
4249
+ 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));
4250
+ const byRouterFile = /* @__PURE__ */ new Map();
4251
+ const byRoot = /* @__PURE__ */ new Map();
4252
+ for (const site of sites) {
4253
+ const { a } = site;
4254
+ const { registrations, uses } = opsOf(site);
4255
+ const routes = byRouterFile.get(a.file) ?? [];
4256
+ for (const reg of registrations) {
4257
+ if (shadowed(site, reg.at)) continue;
4258
+ const handler = reg.handlerRange ?? (reg.handler ? handlerOf(a, reg.handler, files) : null);
4259
+ if (!handler) continue;
4260
+ 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));
4261
+ const url = reg.path === null ? "(dynamic path)" : joinPath(site.prefix, reg.path);
4262
+ const callbacks = flatten(a, reg.middleware);
4263
+ const root = site.root ?? site;
4264
+ const add2 = (range, middleware) => {
4265
+ const route = {
4266
+ file: range.file,
4267
+ framework: site.framework,
4268
+ url,
4269
+ scope: scopeOf(range.file.path),
4270
+ reachable: { start: range.start, end: range.end },
4271
+ middleware: [...site.inherited, ...toRefs(a, before), ...toRefs(a, middleware)],
4272
+ mounts: []
4273
+ };
4274
+ routes.push(route);
4275
+ byRoot.set(root, [...byRoot.get(root) ?? [], route]);
4276
+ };
4277
+ add2(handler, callbacks);
4278
+ callbacks.forEach((callback, index2) => {
4279
+ if (!FUNCTION_EXPRESSION.test(callback.text)) return;
4280
+ const range = functionAt(a, callback.at, callback.at + callback.text.length);
4281
+ if (range) add2(range, callbacks.slice(0, index2));
4282
+ });
4283
+ }
4284
+ if (routes.length) byRouterFile.set(a.file, routes);
4285
+ }
4286
+ const localRoutes = (a, arg) => {
4287
+ const root = sites.find((other) => other.a === a && other.range === null && other.name === arg.text && !other.root);
4288
+ return root ? byRoot.get(root) : void 0;
4289
+ };
4290
+ for (const site of sites) {
4291
+ if (site.framework === "fastify") continue;
4292
+ const { a } = site;
4293
+ const uses = opsOf(site).uses;
4294
+ for (const use of uses) {
4295
+ if (site.framework === "hono" && !use.mount) continue;
4296
+ use.args.forEach((arg, index2) => {
4297
+ const target = mountedFile(a, arg, files);
4298
+ const routes = target && target !== a.file ? byRouterFile.get(target) : localRoutes(a, arg);
4299
+ if (!routes || routes.length === 0) return;
4300
+ const isMiddleware = (x) => mountedFile(a, x, files) === null && localRoutes(a, x) === void 0;
4301
+ 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);
4302
+ const earlier = uses.filter((other) => !other.mount && other.at < use.at && covers2(other)).flatMap((other) => site.framework === "hono" ? other.args : other.args.filter(isMiddleware));
4303
+ const middleware = [...site.inherited, ...toRefs(a, earlier), ...toRefs(a, use.args.slice(0, index2).filter(isMiddleware))];
4304
+ const prefix = joinPath(site.prefix, use.prefix ?? "/");
4305
+ for (const route of routes) {
4306
+ route.mounts.push(middleware);
4307
+ if (prefix !== "/" && route.mounts.length === 1 && route.url.startsWith("/")) route.url = joinPath(prefix, route.url);
4308
+ }
4309
+ });
4310
+ }
4311
+ }
4312
+ const byHandlerFile = /* @__PURE__ */ new Map();
4313
+ for (const routes of byRouterFile.values()) {
4314
+ for (const route of routes) {
4315
+ const list = byHandlerFile.get(route.file) ?? [];
4316
+ list.push(route);
4317
+ byHandlerFile.set(route.file, list);
4318
+ }
4319
+ }
4320
+ const index = { byHandlerFile };
4321
+ indexCache.set(files, index);
4322
+ return index;
4323
+ }
4324
+ function nodeRoutesFor(file, files) {
4325
+ return serverIndex(files).byHandlerFile.get(file) ?? [];
4326
+ }
4327
+ function serverRoutesOf(file, files) {
4328
+ const route = routeOf(file);
4329
+ if (route) return [route];
4330
+ const actions = serverActionRoutes(file);
4331
+ return actions.length ? actions : nodeRoutesFor(file, files);
4332
+ }
4333
+ var decoratorCache = /* @__PURE__ */ new WeakMap();
4334
+ function decoratorOf(name, files) {
4335
+ let cache3 = decoratorCache.get(files);
4336
+ if (!cache3) {
4337
+ cache3 = /* @__PURE__ */ new Map();
4338
+ for (const file of files) {
4339
+ if (!/\.[mc]?[jt]sx?$/.test(file.path) || !file.content.includes("decorate")) continue;
4340
+ const a = analyse(file);
4341
+ for (const m of a.source.matchAll(/\.\s*decorate\s*\(\s*['"]([A-Za-z_$][\w$]*)['"]\s*,/g)) {
4342
+ if (cache3.has(m[1])) continue;
4343
+ const open = a.source.indexOf("(", m.index);
4344
+ const close = a.pairs.get(open);
4345
+ const value = close === void 0 ? void 0 : argsOf(a, open, close)[1];
4346
+ cache3.set(m[1], value ? handlerOf(a, value, files) : null);
4347
+ }
4348
+ }
4349
+ decoratorCache.set(files, cache3);
4350
+ }
4351
+ return cache3.get(name) ?? null;
4352
+ }
4353
+ function authCompositionOf(ref) {
4354
+ const a = analyse(ref.file);
4355
+ const m = /^[A-Za-z_$][\w$]*\s*\.\s*auth\s*\(/.exec(ref.text);
4356
+ if (!m) return null;
4357
+ const open = ref.at + m[0].length - 1;
4358
+ const close = a.pairs.get(open);
4359
+ if (close === void 0) return null;
4360
+ const args = argsOf(a, open, close);
4361
+ if (!args[0]?.text.startsWith("[")) return null;
4362
+ const relation = entriesOf(a, args[1]).get("relation")?.value;
4363
+ return { refs: toRefs(a, flatten(a, [args[0]])), relation: relation && literalOf(relation) === "and" ? "and" : "or" };
4364
+ }
4365
+ function middlewareDefinition(ref, files) {
4366
+ const a = analyse(ref.file);
4367
+ const text = a.code.slice(ref.at, ref.at + ref.text.length);
4368
+ let inline = null;
4369
+ if (FUNCTION_EXPRESSION.test(text)) {
4370
+ inline = functionAt(a, ref.at, ref.at + text.length);
4371
+ return { name: "an inline middleware", range: null, spec: null, inline };
4372
+ }
4373
+ const call = /^[A-Za-z_$][\w$.]*\s*\(/.exec(text);
4374
+ if (call) {
4375
+ const open = ref.at + call[0].length - 1;
4376
+ const close = a.pairs.get(open);
4377
+ const last = close === void 0 ? void 0 : argsOf(a, open, close).at(-1);
4378
+ if (last && FUNCTION_EXPRESSION.test(last.text)) inline = functionAt(a, last.at, last.at + last.text.length);
4379
+ }
4380
+ const root = /^([A-Za-z_$][\w$]*)/.exec(ref.text)?.[1] ?? "";
4381
+ const member = /^[A-Za-z_$][\w$]*\s*\.\s*([A-Za-z_$][\w$]*)/.exec(ref.text)?.[1] ?? null;
4382
+ const callee = member ?? root;
4383
+ const spec = packageOf(a, root);
4384
+ if (member && (spec === null || spec === "fastify")) {
4385
+ const decorated = decoratorOf(member, files);
4386
+ if (decorated) return { name: callee, range: decorated, spec: null, inline };
4387
+ }
4388
+ if (spec) return { name: callee, range: null, spec, inline };
4389
+ const local = member === null ? findFunction(a, root) : null;
4390
+ if (local) return { name: callee, range: local, spec: null, inline };
4391
+ const target = importOf(a, root, files);
4392
+ const range = target ? importedFunction(target, member) ?? (member ? propertyValue(analyse(target.file), member) : null) : null;
4393
+ return { name: callee, range, spec: null, inline };
4394
+ }
4395
+ function propertyValue(a, name) {
4396
+ const m = new RegExp(`(?<![\\w$.])${name.replace(/\$/g, "\\$")}\\s*:\\s*(?=[A-Za-z_$][\\w$.]*\\s*\\()`).exec(a.code);
4397
+ if (!m) return null;
4398
+ const from = m.index + m[0].length;
4399
+ return { file: a.file, start: from, end: expressionEnd(a, from) - 1 };
4400
+ }
4401
+
3397
4402
  // src/rules/apiauth.ts
3398
4403
  var APP_ROUTER = /(?:^|\/)app\/api\/(?:.+\/)?route\.[mc]?[jt]sx?$/;
3399
4404
  var PAGES_ROUTER = /(?:^|\/)pages\/api\/.+\.[mc]?[jt]sx?$/;
@@ -3601,11 +4606,38 @@ var AUTH_ENDPOINT_NAMES = /^(?:\/api)?\/auth\/(?:sign[-_]?in|sign[-_]?up|sign[-_
3601
4606
  var AUTH_CALLBACK = /^(?:\/api)?\/auth\/callback(?:\/[^/]+)?$/;
3602
4607
  var PROVIDER_CALLBACK = /^(?:\/api)?\/(?:auth|oauth|login|sign-?in)\/[^/]+\/callback$/;
3603
4608
  var OAUTH_HANDLER = /\bdefineOAuth\w*EventHandler\s*\(/;
4609
+ 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
4610
  function isAuthEndpoint(route) {
3605
4611
  if (route.action !== void 0) return AUTH_ACTION_NAMES.test(route.action);
4612
+ if (NODE_FRAMEWORKS.has(route.framework)) return NODE_AUTH_PATH.test(route.url) || /\/callback(?:\/|$)/i.test(route.url);
3606
4613
  return AUTH_ENDPOINT_NAMES.test(route.url) || AUTH_CALLBACK.test(route.url) || PROVIDER_CALLBACK.test(route.url) || OAUTH_HANDLER.test(noiseMaskedOf(route.file));
3607
4614
  }
3608
4615
  var AUTH_ENFORCING_CALL = /\b(?:NextAuth|require(?:Auth|User|Session|Admin)\w*|withAuth|verifyAuth|ensureAuth|assertAuth(?:enticated)?|verifyIdToken|constructEvent)\s*\(/i;
4616
+ var WEBHOOK_VERIFIERS = [
4617
+ { spec: /^@polar-sh\/sdk\/webhooks$/, imported: "validateEvent", sync: true, instance: false },
4618
+ { spec: /^@clerk\/[\w-]+(?:\/webhooks)?$/, imported: "verifyWebhook", sync: false, instance: false },
4619
+ { spec: /^(?:svix|standardwebhooks)$/, imported: "Webhook", sync: true, instance: true }
4620
+ ];
4621
+ var webhookVerifierCache = /* @__PURE__ */ new WeakMap();
4622
+ function webhookVerifiersOf(file) {
4623
+ if (webhookVerifierCache.has(file)) return webhookVerifierCache.get(file);
4624
+ const calls = [];
4625
+ const syncCalls = [];
4626
+ for (const binding of bindingsOf(file).imports) {
4627
+ const verifier = WEBHOOK_VERIFIERS.find((v) => binding.spec && v.spec.test(binding.spec) && binding.imported === v.imported);
4628
+ if (!verifier) continue;
4629
+ const local = binding.local.replace(/\$/g, "\\$");
4630
+ 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];
4631
+ calls.push(...names);
4632
+ if (verifier.sync) syncCalls.push(...names);
4633
+ }
4634
+ const result = calls.length === 0 ? null : {
4635
+ call: new RegExp(`^(?:${calls.join("|")})\\s*\\(`),
4636
+ sync: new RegExp(`^(?:${syncCalls.length ? syncCalls.join("|") : "(?!)"})\\s*\\(`)
4637
+ };
4638
+ webhookVerifierCache.set(file, result);
4639
+ return result;
4640
+ }
3609
4641
  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
4642
  var ACCESS_CHAIN = String.raw`[\w$]+(?:\??\.[\w$]+)*`;
3611
4643
  var NEGATED_IDENTITY = new RegExp(`^!\\s*${ACCESS_CHAIN}(?:\\s*\\([^=]*\\))?$`);
@@ -3614,7 +4646,7 @@ var IDENTITY_MISMATCH = new RegExp(`^${ACCESS_CHAIN}\\s*!={1,2}\\s*(?!(?:null|un
3614
4646
  var COMPARED_VALUE = String.raw`(?!(?:null|undefined|true|false)\b)(?:${ACCESS_CHAIN}(?:\s*\([^()]{0,200}\))?|\`[^\`]{0,300}\`)`;
3615
4647
  var SECRET_MISMATCH = new RegExp(`^${COMPARED_VALUE}\\s*!={1,2}\\s*${COMPARED_VALUE}$`);
3616
4648
  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;
4649
+ 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
4650
  function closingDelimiter(source, start, open, close) {
3619
4651
  let depth = 0;
3620
4652
  for (let i = start; i < source.length; i++) {
@@ -3631,16 +4663,28 @@ function controlledStatement(source, afterCondition) {
3631
4663
  let start = afterCondition;
3632
4664
  while (/\s/.test(source[start] ?? "")) start++;
3633
4665
  if (source[start] === "{") {
3634
- const end2 = closingDelimiter(source, start, "{", "}");
3635
- return source.slice(start, end2 === null ? Math.min(source.length, start + 600) : end2 + 1);
4666
+ const end = closingDelimiter(source, start, "{", "}");
4667
+ return source.slice(start, end === null ? Math.min(source.length, start + 600) : end + 1);
4668
+ }
4669
+ const limit = Math.min(source.length, start + 400);
4670
+ let depth = 0;
4671
+ for (let i = start; i < limit; i++) {
4672
+ const ch = source[i];
4673
+ if ("([{".includes(ch)) depth++;
4674
+ else if (")]}".includes(ch)) depth--;
4675
+ else if (depth === 0 && ch === ";") return source.slice(start, i + 1);
4676
+ else if (depth === 0 && ch === "\n") {
4677
+ const before = source.slice(start, i).trimEnd();
4678
+ const after = source.slice(i + 1, i + 40).trimStart();
4679
+ if (before !== "" && !/[=+\-*/%&|^!?:,.([{<>]$/.test(before) && !/^(?:[.?+\-*/%&|^,:<>=)\]}]|=>)/.test(after)) return source.slice(start, i);
4680
+ }
3636
4681
  }
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);
4682
+ return source.slice(start, limit);
3640
4683
  }
4684
+ var NON_IDENTITY_CHECK = /origin|referr?er|cors|rate[_-]?limit|captcha/i;
3641
4685
  function rejectsMissingIdentity(term, returnsDeniedStatus) {
3642
4686
  const rejects = NEGATED_IDENTITY.test(term) || IDENTITY_IS_EMPTY.test(term) || IDENTITY_MISMATCH.test(term);
3643
- return rejects && (AUTH_CONDITION.test(term) || returnsDeniedStatus);
4687
+ return rejects && (AUTH_CONDITION.test(term) || returnsDeniedStatus && !NON_IDENTITY_CHECK.test(term));
3644
4688
  }
3645
4689
  function hasConditionalAuthGuard(code, requireThrow = false, accept) {
3646
4690
  const starts = /\bif\s*\(/g;
@@ -3765,10 +4809,14 @@ function statementEnd2(code, start, limit, pairs) {
3765
4809
  return limit;
3766
4810
  }
3767
4811
  var unguardedCache = /* @__PURE__ */ new WeakMap();
3768
- function unguardedOpsOf(file) {
4812
+ var nodeRouteFiles = /* @__PURE__ */ new WeakSet();
4813
+ function unguardedOpsOf(file, files) {
3769
4814
  let hit = unguardedCache.get(file);
3770
4815
  if (hit === void 0) {
3771
- hit = unguardedOperations(file, findDataOps(file));
4816
+ const node = serverRoutesOf(file, files).some((route) => NODE_FRAMEWORKS.has(route.framework));
4817
+ if (node) nodeRouteFiles.add(file);
4818
+ const ops = [...findDataOps(file), ...node ? delegatedWrites(file, files) : []].sort((a, b) => a.index - b.index);
4819
+ hit = unguardedOperations(file, ops);
3772
4820
  unguardedCache.set(file, hit);
3773
4821
  }
3774
4822
  return hit;
@@ -3826,6 +4874,7 @@ function unguardedOperations(file, ops, extra, requireThrow = false, observe) {
3826
4874
  const functionStarts = new Set(bodies.map((body) => body.start));
3827
4875
  const bodiesByStart = new Map(bodies.map((body) => [body.start, body]));
3828
4876
  const localNames = localAuthNames(file);
4877
+ const webhookVerifiers = webhookVerifiersOf(file);
3829
4878
  const guardEnds = /* @__PURE__ */ new Map();
3830
4879
  const guardNames = /* @__PURE__ */ new Map();
3831
4880
  const guardRequirements = /* @__PURE__ */ new Map();
@@ -3945,7 +4994,12 @@ function unguardedOperations(file, ops, extra, requireThrow = false, observe) {
3945
4994
  dependencies.forEach((index) => required.add(index));
3946
4995
  return true;
3947
4996
  }
3948
- return !observe && value.kind === "unknown" && rejectsMissingIdentity(term, DENIED_STATUS.test(code.slice(i, end)));
4997
+ if (value.kind === "claimed") {
4998
+ const branch = code.slice(i, end);
4999
+ return rejectsMissingIdentity(term, true) && (/\b(?:throw|redirect)\b/.test(branch) || DENIED_STATUS.test(branch));
5000
+ }
5001
+ 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);
5002
+ return !observe && (value.kind === "unknown" || frameworkContext) && rejectsMissingIdentity(term, DENIED_STATUS.test(code.slice(i, end)));
3949
5003
  });
3950
5004
  if (end <= owner.end && accepted) {
3951
5005
  guardRequirements.set(owner.start, [...required]);
@@ -3965,7 +5019,9 @@ function unguardedOperations(file, ops, extra, requireThrow = false, observe) {
3965
5019
  continue;
3966
5020
  }
3967
5021
  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));
5022
+ const enforcing = AUTH_ENFORCING_CALL.exec(code.slice(i, i + 100));
5023
+ const verifier = enforcing?.index === 0 ? null : webhookVerifiers?.call.exec(code.slice(i, i + 100)) ?? null;
5024
+ const call = indirectCall ?? verifier ?? enforcing;
3969
5025
  const callName = call?.[0].replace(/\s*\($/, "");
3970
5026
  if (!indirectCall && call?.index === 0 && callName && localNames.has(callName)) continue;
3971
5027
  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 +5033,7 @@ function unguardedOperations(file, ops, extra, requireThrow = false, observe) {
3977
5033
  const prefix = code.slice(prefixStart, i).trim();
3978
5034
  if (/(?:&&|\|\||\?|:)\s*$/.test(code.slice(owner.start + 1, prefixStart))) continue;
3979
5035
  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);
5036
+ 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
5037
  if (!awaited && !synchronous) continue;
3982
5038
  const close2 = pairs.get(i + call[0].length - 1);
3983
5039
  if (close2 !== void 0 && close2 < owner.end) {
@@ -4255,9 +5311,23 @@ function guardDefinitions(file) {
4255
5311
  for (const body of bodies) {
4256
5312
  if (body.start < until) continue;
4257
5313
  until = body.end;
4258
- const declaration = declarationOf(code, body, openers);
5314
+ let declaration = declarationOf(code, body, openers);
5315
+ let declaredAt = body.declaration;
5316
+ let curried = false;
5317
+ if (!declaration && code.startsWith("=>", body.declaration)) {
5318
+ let outer = curriedOuterArrow(code, body.declaration, openers);
5319
+ for (let level = 0; outer !== null && level < 3; level++) {
5320
+ declaration = declarationOf(code, { declaration: outer, start: body.start, end: body.end }, openers);
5321
+ if (declaration) {
5322
+ declaredAt = outer;
5323
+ curried = true;
5324
+ break;
5325
+ }
5326
+ outer = curriedOuterArrow(code, outer, openers);
5327
+ }
5328
+ }
4259
5329
  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));
5330
+ 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
5331
  const nestedAt = returned ? body.start + 1 + returned.index + (returned[0].includes("=>") ? returned[0].lastIndexOf("=>") : returned[0].lastIndexOf("function")) : -1;
4262
5332
  const target = byDeclaration.get(nestedAt) ?? body;
4263
5333
  let at = target.end;
@@ -4281,10 +5351,10 @@ function guardDefinitions(file) {
4281
5351
  }
4282
5352
  const definition = {
4283
5353
  file,
4284
- line: lineNumberAt(lineStartsCached(file), body.declaration),
4285
- wrapper: target !== body
5354
+ line: lineNumberAt(lineStartsCached(file), declaredAt),
5355
+ wrapper: curried || target !== body
4286
5356
  };
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;
5357
+ const exported = !declaration.exported ? null : /\bexport\s+default\s+(?:async\s+)?$/.test(code.slice(Math.max(0, declaredAt - 80), declaredAt)) ? "default" : declaration.name;
4288
5358
  candidates.push({ name: declaration.name, exported, definition, at, start: target.start });
4289
5359
  }
4290
5360
  const unguarded = /* @__PURE__ */ new Set();
@@ -4311,6 +5381,26 @@ function guardDefinitions(file) {
4311
5381
  definitionsCache.set(file, result);
4312
5382
  return result;
4313
5383
  }
5384
+ function curriedOuterArrow(code, arrow, openers) {
5385
+ let i = arrow - 1;
5386
+ while (i >= 0 && /\s/.test(code[i])) i--;
5387
+ const window = code.slice(Math.max(0, arrow - 200), arrow);
5388
+ const close = window.lastIndexOf(")");
5389
+ if (close !== -1 && /^\s*(?::[^=;{()]*)?$/.test(window.slice(close + 1))) {
5390
+ const open = openers.get(arrow - window.length + close);
5391
+ if (open === void 0) return null;
5392
+ i = open - 1;
5393
+ } else {
5394
+ if (!/[\w$]/.test(code[i] ?? "")) return null;
5395
+ while (i >= 0 && /[\w$]/.test(code[i])) i--;
5396
+ }
5397
+ while (i >= 0 && /\s/.test(code[i])) i--;
5398
+ if (code.slice(i - 4, i + 1) === "async" && !/[\w$]/.test(code[i - 5] ?? "")) {
5399
+ i -= 5;
5400
+ while (i >= 0 && /\s/.test(code[i])) i--;
5401
+ }
5402
+ return code[i] === ">" && code[i - 1] === "=" ? i - 1 : null;
5403
+ }
4314
5404
  function bindingModule(spec, file, allFiles, scope) {
4315
5405
  const target = normalizeSpec(spec, file.path);
4316
5406
  if (!target) return null;
@@ -4426,25 +5516,72 @@ function delegatedGuard(file, candidate, allFiles, scope, state, depth, seen) {
4426
5516
  });
4427
5517
  return selected ? { ...candidate.definition, requiredArgs, chain: [selected, ...selected.chain ?? []] } : null;
4428
5518
  }
5519
+ var HELPER_BUDGET = 64;
5520
+ var ROUTE_BUDGET = 1024;
5521
+ 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;
5522
+ 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;
5523
+ 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;
5524
+ 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;
5525
+ function evidenceWrapper(file, name, files) {
5526
+ const definition = calleeDefinition(file, name, null, files);
5527
+ if (!definition) return null;
5528
+ const queue = [{ range: definition, depth: 0 }];
5529
+ const seen = /* @__PURE__ */ new Set();
5530
+ for (let i = 0; i < queue.length && i < 24; i++) {
5531
+ const { range, depth } = queue[i];
5532
+ const key = `${range.file.path}\0${range.start}`;
5533
+ if (seen.has(key)) continue;
5534
+ seen.add(key);
5535
+ const text = commentsMaskedOf(range.file).slice(range.start, range.end + 1);
5536
+ if (AUTH_DENIAL.test(text) && AUTH_SIGNAL.test(text)) {
5537
+ return { file: definition.file, line: lineNumberAt(lineStartsCached(definition.file), definition.start), wrapper: true };
5538
+ }
5539
+ if (depth >= 2) continue;
5540
+ const code = noiseMaskedOf(range.file).slice(range.start, range.end + 1);
5541
+ const params = new Set(functionParams({ file: range.file, start: range.start, end: range.end }));
5542
+ const pairs = delimiterPairs(code);
5543
+ for (const m of code.matchAll(/(?<![\w$.])([A-Za-z_$][\w$]*)\s*(?:<[^()]{0,200}>\s*)?\(|(?<=[(,]\s*)([A-Za-z_$][\w$]*)\s*(?=[,)])/g)) {
5544
+ const callee = m[1] ?? m[2];
5545
+ if (NOT_CALLEE.test(callee) || params.has(callee)) continue;
5546
+ if (m[1]) {
5547
+ const close = pairs.get(m.index + m[0].length - 1);
5548
+ const args = close === void 0 ? "" : code.slice(m.index + m[0].length, close);
5549
+ if ([...params].some((p) => new RegExp(`(?<![\\w$.])${p.replace(/\$/g, "\\$")}(?![\\w$])`).test(args))) continue;
5550
+ }
5551
+ const target = calleeDefinition(range.file, callee, null, files);
5552
+ if (target) queue.push({ range: target, depth: depth + 1 });
5553
+ }
5554
+ }
5555
+ return null;
5556
+ }
4429
5557
  function indirectGuardOperations(route, ctx) {
4430
- const state = { remaining: 128, truncated: false };
4431
5558
  const { called } = guardBindings(route.file);
5559
+ const wrappers = new Set([...noiseMaskedOf(route.file).matchAll(EXPORT_WRAPPER)].map((m) => m[1]));
5560
+ const rank = (name) => wrappers.has(name) ? 0 : AUTH_HELPER_NAME.test(name) ? 1 : 2;
5561
+ const ordered = [...called].sort((a, b) => rank(a) - rank(b));
4432
5562
  const names = /* @__PURE__ */ new Map();
4433
- for (const name of called) {
4434
- if (state.remaining <= 0) {
4435
- state.truncated = true;
5563
+ let total = ROUTE_BUDGET;
5564
+ let truncated = false;
5565
+ let evidenceBudget = 16;
5566
+ for (const name of ordered) {
5567
+ if (total <= 0) {
5568
+ truncated = true;
4436
5569
  break;
4437
5570
  }
5571
+ const budget = Math.min(HELPER_BUDGET, total);
5572
+ const state = { remaining: budget, truncated: false };
4438
5573
  const definition = namedGuard(route.file, name, ctx.files, route.scope, state, 0, /* @__PURE__ */ new Set());
5574
+ total -= budget - Math.max(0, state.remaining);
5575
+ if (state.truncated) truncated = true;
4439
5576
  if (definition) names.set(name, definition);
5577
+ else if (rank(name) <= 1 && evidenceBudget-- > 0) {
5578
+ const wrapper = evidenceWrapper(route.file, name, ctx.files);
5579
+ if (wrapper) names.set(name, wrapper);
5580
+ }
4440
5581
  }
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
5582
  const protectedOps = /* @__PURE__ */ new Map();
4446
- if (names.size === 0) return protectedOps;
4447
- unguardedOperations(route.file, unguardedOpsOf(route.file), {
5583
+ if (names.size === 0) return { ops: protectedOps, truncated };
5584
+ unguardedOperations(route.file, unguardedOpsOf(route.file, ctx.files), {
4448
5585
  guards: new Set([...names].filter(([, definition]) => !definition.wrapper).map(([name]) => name)),
4449
5586
  wrappers: new Set([...names].filter(([, definition]) => definition.wrapper).map(([name]) => name)),
4450
5587
  requirements: new Map([...names].map(([name, guard]) => [name, guard.requiredArgs ?? []])),
@@ -4453,7 +5590,63 @@ function indirectGuardOperations(route, ctx) {
4453
5590
  if (definition) protectedOps.set(index, definition);
4454
5591
  }
4455
5592
  });
4456
- return protectedOps;
5593
+ return { ops: protectedOps, truncated };
5594
+ }
5595
+ var DELEGATE_DEPTH = 2;
5596
+ var DELEGATE_BUDGET = 256;
5597
+ var NOT_CALLEE = /^(?:if|for|while|switch|catch|function|return|typeof|await|new|super|this|import|require|async|void|delete|in|of)$/;
5598
+ var directUnguardedCache = /* @__PURE__ */ new WeakMap();
5599
+ function directUnguardedOps(file) {
5600
+ let hit = directUnguardedCache.get(file);
5601
+ if (hit === void 0) {
5602
+ hit = unguardedOperations(file, findDataOps(file));
5603
+ directUnguardedCache.set(file, hit);
5604
+ }
5605
+ return hit;
5606
+ }
5607
+ var delegatedCache = /* @__PURE__ */ new WeakMap();
5608
+ function delegatedWrites(file, files, depth = 0) {
5609
+ let byDepth = delegatedCache.get(file);
5610
+ if (!byDepth) {
5611
+ byDepth = /* @__PURE__ */ new Map();
5612
+ delegatedCache.set(file, byDepth);
5613
+ }
5614
+ const cached = byDepth.get(depth);
5615
+ if (cached) return cached;
5616
+ byDepth.set(depth, []);
5617
+ const code = noiseMaskedOf(file);
5618
+ const pairs = delimiterPairs(code);
5619
+ const chains = /* @__PURE__ */ new Map();
5620
+ const hits = [];
5621
+ for (const m of code.matchAll(/(?<![\w$.])([A-Za-z_$][\w$]*)(?:\s*\??\.\s*([A-Za-z_$][\w$]*))?\s*\(/g)) {
5622
+ const name = m[1];
5623
+ const member = m[2] ?? null;
5624
+ if (NOT_CALLEE.test(name)) continue;
5625
+ if (/\bfunction\s*\*?\s*$|\b(?:get|set|static)\s+$/.test(code.slice(Math.max(0, m.index - 12), m.index))) continue;
5626
+ const close = pairs.get(m.index + m[0].length - 1);
5627
+ if (close !== void 0 && /^\s*(?::[^{;=]{0,200})?\{/.test(code.slice(close + 1, close + 220))) continue;
5628
+ const key = `${name}.${member ?? ""}`;
5629
+ let resolved = chains.get(key);
5630
+ if (resolved === void 0) {
5631
+ resolved = chains.size < DELEGATE_BUDGET ? writeChain(file, name, member, files, depth) : null;
5632
+ chains.set(key, resolved);
5633
+ }
5634
+ if (!resolved || resolved.range && resolved.range.start <= m.index && m.index <= resolved.range.end) continue;
5635
+ hits.push({ index: m.index, writes: true, via: resolved.chain });
5636
+ }
5637
+ byDepth.set(depth, hits);
5638
+ return hits;
5639
+ }
5640
+ function writeChain(file, name, member, files, depth) {
5641
+ const range = calleeDefinition(file, name, member, files);
5642
+ if (!range) return null;
5643
+ if (/^with[A-Z]/.test(member ?? name) || middlewareRejects(noiseMaskedOf(range.file).slice(range.start, range.end + 1))) return null;
5644
+ const local = range.file === file ? { start: range.start, end: range.end } : null;
5645
+ const own = operationsInRange(directUnguardedOps(range.file), range.start, range.end).find((op) => op.writes);
5646
+ if (own) return { range: local, chain: [{ file: range.file, index: own.index }] };
5647
+ if (depth + 1 >= DELEGATE_DEPTH) return null;
5648
+ const nested = operationsInRange(delegatedWrites(range.file, files, depth + 1), range.start, range.end)[0];
5649
+ return nested ? { range: local, chain: [{ file: range.file, index: nested.index }, ...nested.via] } : null;
4457
5650
  }
4458
5651
  var SUPABASE_TABLE = /\.from\(\s*['"`][^'"`]+['"`]\s*\)\s*\.?\s*(\w+)?/g;
4459
5652
  var SUPABASE_ADMIN_API = /\bauth\s*\.\s*admin\s*\.\s*(\w+)\s*\(/g;
@@ -4462,7 +5655,7 @@ var PRISMA_OP = /\b(?:prisma|db)\s*\.\s*\$?(\w+)\s*\.\s*(findMany|findFirst|find
4462
5655
  var PRISMA_RAW = /\b(?:prisma|db)\s*\.\s*\$(queryRaw|executeRaw)/g;
4463
5656
  var PRISMA_WRITES = /^(?:create|createMany|update|updateMany|upsert|delete|deleteMany)$/;
4464
5657
  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;
5658
+ var MONGO_OP = /\.(?:deleteMany|deleteOne|updateMany|updateOne|insertMany|insertOne|findOneAndDelete|findOneAndUpdate|findOneAndReplace|replaceOne|bulkWrite|findByIdAndUpdate|findByIdAndDelete|findByIdAndRemove)\s*\(/g;
4466
5659
  var RAW_SQL = /\b(?:sql|query|execute)\s*(?:`|\(\s*['"`])\s*(select|insert|update|delete|drop|truncate)\b/gi;
4467
5660
  var RAW_SQL_WRITES = /^(?:insert|update|delete|drop|truncate)$/i;
4468
5661
  function findDataOps(file) {
@@ -4485,8 +5678,11 @@ function findDataOps(file) {
4485
5678
  push(m.index, PRISMA_WRITES.test(m[2] ?? ""));
4486
5679
  }
4487
5680
  PRISMA_RAW.lastIndex = 0;
5681
+ const source = commentsMaskedOf(file);
4488
5682
  while ((m = PRISMA_RAW.exec(code)) !== null) {
4489
- push(m.index, (m[1] ?? "") === "executeRaw");
5683
+ const statement = source.slice(m.index + m[0].length, m.index + m[0].length + 300);
5684
+ const readOnly = /^(?:Unsafe)?\s*(?:`|\(\s*['"`])\s*SELECT\b(?![^`'"]*\bINTO\b)/i.test(statement);
5685
+ push(m.index, (m[1] ?? "") === "executeRaw" && !readOnly);
4490
5686
  }
4491
5687
  DRIZZLE_OP.lastIndex = 0;
4492
5688
  while ((m = DRIZZLE_OP.exec(code)) !== null) {
@@ -4787,6 +5983,187 @@ function containsAuthCheck(file) {
4787
5983
  return found;
4788
5984
  }
4789
5985
  var SECRET_SHAPED = /['"`]([A-Za-z0-9_\-.]{32,})['"`]/g;
5986
+ var AUTH_LIBRARIES = [
5987
+ [/^passport$/, /\.\s*authenticate\s*\((?!\s*['"]anonymous['"])/],
5988
+ // credentialsRequired: false / authRequired: false 时只解析令牌、不拒绝请求。
5989
+ [/^express-jwt$/, /^(?![\s\S]*\bcredentialsRequired\s*:\s*false\b)[A-Za-z_$][\w$]*\s*\(/],
5990
+ [/^express-openid-connect$/, /^requiresAuth\s*\(/],
5991
+ [/^express-oauth2-jwt-bearer$/, /^(?![\s\S]*\bauthRequired\s*:\s*false\b)(?:auth|requiredScopes|claimCheck|claimEquals|claimIncludes)\s*\(/],
5992
+ [/^@clerk\/(?:express|clerk-sdk-node)$/, /^(?:requireAuth|ClerkExpressRequireAuth)\s*\(/],
5993
+ [/^connect-ensure-login$/, /^(?:ensureLoggedIn|[A-Za-z_$][\w$]*\s*\.\s*ensureLoggedIn)\s*\(/],
5994
+ [/^express-basic-auth$/, /^[A-Za-z_$][\w$]*\s*\(/],
5995
+ [/^hono\/jwt$/, /^jwt\s*\(/],
5996
+ [/^hono\/jwk$/, /^jwk\s*\(/],
5997
+ [/^hono\/bearer-auth$/, /^bearerAuth\s*\(/],
5998
+ [/^hono\/basic-auth$/, /^basicAuth\s*\(/],
5999
+ // 注册即为所在作用域添加 onRequest 校验钩子。
6000
+ [/^@fastify\/bearer-auth$/, /^[A-Za-z_$][\w$]*$/]
6001
+ ];
6002
+ var FASTIFY_AUTH_DECORATORS = [["@fastify/basic-auth", "basicAuth"]];
6003
+ 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)$/;
6004
+ 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/;
6005
+ var DENIES_REQUEST = new RegExp(
6006
+ 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*`
6007
+ );
6008
+ function passesOnlyAuthenticated(code) {
6009
+ for (const m of code.matchAll(/\bif\s*\(/g)) {
6010
+ const open = m.index + m[0].length - 1;
6011
+ const close = closingDelimiter(code, open, "(", ")");
6012
+ if (close === null) continue;
6013
+ const condition = code.slice(open + 1, close);
6014
+ if (condition.trimStart().startsWith("!") || !AUTH_CONDITION.test(condition)) continue;
6015
+ const rest = code.slice(close + 1);
6016
+ const pass = /^\s*\{?\s*(?:return\s+)?(?:await\s+)?next\s*\(\s*\)\s*;?\s*(?:return\s*;?\s*)?\}?/.exec(rest);
6017
+ if (pass && DENIES_REQUEST.test(rest.slice(pass[0].length))) return true;
6018
+ }
6019
+ return false;
6020
+ }
6021
+ function awaitedCall(code, pattern) {
6022
+ for (const m of code.matchAll(new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`))) {
6023
+ const before = code.slice(Math.max(0, m.index - 300), m.index).replace(/(?:[A-Za-z_$][\w$]*(?:\s*\([^()]*\))?\s*\??\.\s*)*$/, "").trimEnd();
6024
+ if (/(?:^|[^\w$.])(?:await|return)$/.test(before)) return true;
6025
+ }
6026
+ return false;
6027
+ }
6028
+ function maskUnusedFunctions(code) {
6029
+ let out = code;
6030
+ 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;
6031
+ for (const m of code.matchAll(definitions)) {
6032
+ const name = m[1] ?? m[2];
6033
+ const bodyAt = m.index + m[0].length;
6034
+ let end;
6035
+ if (code[bodyAt] === "{") end = closingDelimiter(code, bodyAt, "{", "}") ?? code.length - 1;
6036
+ else {
6037
+ end = bodyAt;
6038
+ for (let depth = 0; end < code.length; end++) {
6039
+ const ch = code[end];
6040
+ if ("([{".includes(ch)) depth++;
6041
+ else if (")]}".includes(ch)) {
6042
+ if (depth === 0) break;
6043
+ depth--;
6044
+ } else if (depth === 0 && (ch === ";" || ch === "," || ch === "\n")) break;
6045
+ }
6046
+ end--;
6047
+ }
6048
+ const reference = new RegExp(`(?<![\\w$.])${name.replace(/\$/g, "\\$")}(?![\\w$])`, "g");
6049
+ const used = [...code.matchAll(reference)].some((r) => r.index < m.index || r.index > end);
6050
+ if (!used) out = out.slice(0, m.index) + out.slice(m.index, end + 1).replace(/[^\n]/g, " ") + out.slice(end + 1);
6051
+ }
6052
+ return out;
6053
+ }
6054
+ function jwtVerifyRejects(code) {
6055
+ if (!awaitedCall(code, /\bjwtVerify\s*\(/)) return false;
6056
+ const handlers = [...code.matchAll(/\bcatch\s*(?:\([^()]*\))?\s*\{/g)].map((m) => {
6057
+ const open = m.index + m[0].length - 1;
6058
+ return code.slice(open, closingDelimiter(code, open, "{", "}") ?? open);
6059
+ });
6060
+ return handlers.every((body) => /\bsend\s*\(|\bthrow\b/.test(body) || DENIED_RESPONSE.test(body));
6061
+ }
6062
+ function rejectsSecretMismatch(code) {
6063
+ for (const m of code.matchAll(/\bif\s*\(/g)) {
6064
+ const open = m.index + m[0].length - 1;
6065
+ const close = closingDelimiter(code, open, "(", ")");
6066
+ if (close === null) continue;
6067
+ const condition = code.slice(open + 1, close);
6068
+ if (/&&|\?(?!\.)/.test(condition)) continue;
6069
+ const mismatch = condition.split("||").some((part) => {
6070
+ const sides = /^([\s\S]*?)\s*!={1,2}\s*([\s\S]*)$/.exec(part.trim());
6071
+ return sides !== null && (isServerSecretExpression(sides[1]) || isServerSecretExpression(sides[2]));
6072
+ });
6073
+ if (mismatch && DENIED_STATUS.test(controlledStatement(code, close + 1))) return true;
6074
+ }
6075
+ return false;
6076
+ }
6077
+ function catchDenies(code) {
6078
+ for (const m of code.matchAll(/\btry\s*\{/g)) {
6079
+ const tryOpen = m.index + m[0].length - 1;
6080
+ const tryClose = closingDelimiter(code, tryOpen, "{", "}");
6081
+ if (tryClose === null) continue;
6082
+ if (!/\b\w*(?:auth|valid|verify|session|token|user|client|key|secret|permission|access)\w*\s*\(/i.test(code.slice(tryOpen, tryClose))) continue;
6083
+ const handler = /^\s*catch\s*(?:\([^()]*\))?\s*\{/.exec(code.slice(tryClose + 1));
6084
+ if (!handler) continue;
6085
+ const open = tryClose + handler[0].length;
6086
+ const close = closingDelimiter(code, open, "{", "}");
6087
+ if (close === null) continue;
6088
+ const body = code.slice(open + 1, close);
6089
+ const exits = [...body.matchAll(/\b(?:return|throw)\b/g)].map((exit) => exit.index);
6090
+ if (exits.length === 0) continue;
6091
+ const denies = exits.every((at, i) => {
6092
+ const statement = body.slice(at, exits[i + 1] ?? body.length);
6093
+ return statement.startsWith("throw") || DENIED_STATUS.test(statement) || /\bsend\s*\(\s*(?:err|error|e)\s*\)/.test(statement);
6094
+ });
6095
+ const pairs = delimiterPairs(body);
6096
+ let topLevelExit = false;
6097
+ for (let i = 0; i < body.length; i++) {
6098
+ const end = pairs.get(i);
6099
+ if (end !== void 0) {
6100
+ i = end;
6101
+ continue;
6102
+ }
6103
+ if (/^(?:return|throw)\b/.test(body.slice(i, i + 6)) && (i === 0 || !/[\w$.]/.test(body[i - 1]))) {
6104
+ topLevelExit = true;
6105
+ break;
6106
+ }
6107
+ }
6108
+ if (denies && topLevelExit) return true;
6109
+ }
6110
+ return false;
6111
+ }
6112
+ var MIDDLEWARE_CREDENTIAL = /auth|secret|token|api[_-]?key|signature|session|jwt|bearer|credential|password|timingSafeEqual|verif/i;
6113
+ function rejectsMissingCredential(code) {
6114
+ return hasConditionalAuthGuard(code.replace(/!\s*await\s+/g, "!"), false, (term) => (NEGATED_IDENTITY.test(term) || IDENTITY_IS_EMPTY.test(term)) && MIDDLEWARE_CREDENTIAL.test(term));
6115
+ }
6116
+ function middlewareRejects(source) {
6117
+ const code = maskUnusedFunctions(source);
6118
+ if (hasConditionalAuthGuard(code) || rejectsMissingCredential(code) || passesOnlyAuthenticated(code) || rejectsSecretMismatch(code) || catchDenies(code) || AUTH_ENFORCING_CALL.test(code) || jwtVerifyRejects(code)) return true;
6119
+ return (/\b(?:jwt\s*\.\s*verify|verifyToken|verifyJwt|verifyIdToken|verifyAccessToken)\s*\(/.test(code) || awaitedCall(code, /\bjwtVerify\s*\(/)) && DENIED_RESPONSE.test(code);
6120
+ }
6121
+ function middlewareStatus(ref, files, depth = 0) {
6122
+ const composition = depth < 3 ? authCompositionOf(ref) : null;
6123
+ if (composition) {
6124
+ const parts = composition.refs.map((part) => middlewareStatus(part, files, depth + 1));
6125
+ const guarded = composition.relation === "and" ? parts.some((part) => part.status === "guard") : parts.length > 0 && parts.every((part) => part.status === "guard");
6126
+ if (guarded) return { status: "guard", name: "auth" };
6127
+ const unconfirmed = parts.find((part) => part.status === "unconfirmed");
6128
+ return unconfirmed ?? { status: "none", name: "auth" };
6129
+ }
6130
+ const { name, range, spec, inline } = middlewareDefinition(ref, files);
6131
+ const known = spec ? AUTH_LIBRARIES.find(([pkg]) => pkg.test(spec)) : void 0;
6132
+ if (known && known[1].test(ref.text)) return { status: "guard", name };
6133
+ if (inline && middlewareRejects(noiseMaskedOf(inline.file).slice(inline.start, inline.end + 1))) return { status: "guard", name };
6134
+ if (spec) {
6135
+ if (known || NON_AUTH_MIDDLEWARE.test(spec)) return { status: "none", name };
6136
+ } else if (range) {
6137
+ const code = noiseMaskedOf(range.file).slice(range.start, range.end + 1);
6138
+ if (middlewareRejects(code)) return { status: "guard", name };
6139
+ const initializer = commentsMaskedOf(range.file).slice(range.start, range.end + 1).trim();
6140
+ if (initializer !== ref.text && /^[A-Za-z_$][\w$.]*\s*\(/.test(initializer) && depth < 3) {
6141
+ const inner = middlewareStatus({ text: initializer, at: range.start, file: range.file }, files, depth + 1);
6142
+ if (inner.status === "guard") return { status: "guard", name };
6143
+ }
6144
+ } else if (/^[A-Za-z_$][\w$]*\s*\.\s*[A-Za-z_$][\w$]*$/.test(ref.text)) {
6145
+ const plugin = FASTIFY_AUTH_DECORATORS.find(([pkg, decorator]) => decorator === name && files.some((file) => file.content.includes(`'${pkg}'`) || file.content.includes(`"${pkg}"`)));
6146
+ if (plugin) return { status: "guard", name };
6147
+ }
6148
+ return { status: AUTH_HELPER_NAME.test(name) ? "unconfirmed" : "none", name };
6149
+ }
6150
+ function middlewareGuardStatus(route, files) {
6151
+ const unconfirmed = /* @__PURE__ */ new Set();
6152
+ const scan2 = (refs) => {
6153
+ let guarded = false;
6154
+ for (const ref of refs) {
6155
+ const { status, name } = middlewareStatus(ref, files);
6156
+ if (status === "guard") guarded = true;
6157
+ else if (status === "unconfirmed") unconfirmed.add(name);
6158
+ }
6159
+ return guarded;
6160
+ };
6161
+ if (scan2(route.middleware ?? [])) return { guarded: true, unconfirmed: [] };
6162
+ const mounts = route.mounts ?? [];
6163
+ const mounted = mounts.map(scan2);
6164
+ if (mounted.length > 0 && mounted.every(Boolean)) return { guarded: true, unconfirmed: [] };
6165
+ return { guarded: false, unconfirmed: [...unconfirmed] };
6166
+ }
4790
6167
  function excerptFor(file, line) {
4791
6168
  const raw = (file.lines[line - 1] ?? "").trim();
4792
6169
  let out = raw;
@@ -4801,10 +6178,7 @@ var apiAuthRule = {
4801
6178
  id: "api/db-access-without-auth",
4802
6179
  severity: "P0",
4803
6180
  check(ctx) {
4804
- const routes = ctx.files.flatMap((file) => {
4805
- const route = routeOf(file);
4806
- return route === null ? serverActionRoutes(file) : [route];
4807
- });
6181
+ const routes = ctx.files.flatMap((file) => serverRoutesOf(file, ctx.files));
4808
6182
  if (routes.length === 0) return [];
4809
6183
  for (const middlewareFile of ctx.files.filter((f) => isAnyMiddlewareFile(f.path))) {
4810
6184
  const config = extractMatcherConfig(middlewareFile);
@@ -4820,21 +6194,31 @@ var apiAuthRule = {
4820
6194
  const sessionModules = ctx.files.filter(buildsSessionClient);
4821
6195
  const findings = [];
4822
6196
  const indirectByFile = /* @__PURE__ */ new Map();
6197
+ const reportedRanges = /* @__PURE__ */ new Set();
4823
6198
  for (const route of routes) {
4824
6199
  const reachable = route.reachable;
4825
- const unguarded = unguardedOpsOf(route.file);
6200
+ const unguarded = unguardedOpsOf(route.file, ctx.files);
4826
6201
  let ops = reachable === void 0 ? unguarded : operationsInRange(unguarded, reachable.start, reachable.end);
4827
6202
  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);
6203
+ let indirectGuards = indirectByFile.get(route.file);
6204
+ if (!indirectGuards) {
6205
+ indirectGuards = indirectGuardOperations(route, ctx);
6206
+ indirectByFile.set(route.file, indirectGuards);
4832
6207
  }
6208
+ const indirectOps = indirectGuards.ops;
6209
+ ops = ops.filter((op) => !(op.via && indirectOps.has(op.index)));
6210
+ if (ops.length === 0) continue;
4833
6211
  const unprotected = ops.filter((op) => !indirectOps.has(op.index));
4834
- if (unprotected.length > 0) ops = unprotected;
6212
+ if (unprotected.length > 0 && (unprotected.some((op) => op.writes) || !ops.some((op) => op.writes) || usesAdminClient(route, adminModules, ctx.files))) ops = unprotected;
4835
6213
  const url = route.url;
4836
6214
  if (isAuthEndpoint(route)) continue;
4837
6215
  if ((route.framework === "next" || route.framework === "astro") && route.action === void 0 && middlewareCovers(ctx, route)) continue;
6216
+ let unconfirmedMiddleware = [];
6217
+ if (NODE_FRAMEWORKS.has(route.framework)) {
6218
+ const status = middlewareGuardStatus(route, ctx.files);
6219
+ if (status.guarded) continue;
6220
+ unconfirmedMiddleware = status.unconfirmed;
6221
+ }
4838
6222
  const globalGuard = globalGuardFor(ctx, route);
4839
6223
  const globalNote = globalGuard === null ? [] : [
4840
6224
  `${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 +6227,15 @@ var apiAuthRule = {
4843
6227
  if (/\.auth\s*\.\s*getSession\s*\(/.test(handlerCode)) globalNote.push(
4844
6228
  "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
6229
  );
6230
+ for (const name of unconfirmedMiddleware) globalNote.push(
6231
+ `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.`
6232
+ );
6233
+ if (indirectGuards.truncated) globalNote.push(
6234
+ "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."
6235
+ );
6236
+ const rangeKey = `${route.file.path}\0${reachable?.start ?? -1}\0${reachable?.end ?? -1}`;
6237
+ if (reportedRanges.has(rangeKey)) continue;
6238
+ reportedRanges.add(rangeKey);
4846
6239
  const admin = usesAdminClient(route, adminModules, ctx.files);
4847
6240
  const hit = ops.find((o) => o.writes) ?? ops[0];
4848
6241
  const indirect = indirectOps.get(hit.index);
@@ -4855,17 +6248,28 @@ var apiAuthRule = {
4855
6248
  description: "Local authentication helper in the recognized delegation chain; verify runtime behaviour and caller coverage."
4856
6249
  })) : [];
4857
6250
  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.");
6251
+ const delegated = (hit.via ?? []).map((step) => ({ file: step.file.path, line: lineNumberAt(lineStartsCached(step.file), step.index) }));
6252
+ const delegatedEvidence = delegated.map((step, i) => ({
6253
+ kind: "operation",
6254
+ file: step.file,
6255
+ line: step.line,
6256
+ description: i === delegated.length - 1 ? "Database write reached from this handler." : "Project function on the call path to the write."
6257
+ }));
6258
+ if (delegated.length > 0) {
6259
+ const target = delegated.at(-1);
6260
+ 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.");
6261
+ }
4858
6262
  if (admin) {
4859
6263
  const trace = adminEvidence(route, adminModules, ctx.files, line);
4860
- const evidence = [...trace.evidence ?? [], ...indirectEvidence];
6264
+ const evidence = [...trace.evidence ?? [], ...delegatedEvidence, ...indirectEvidence];
4861
6265
  findings.push({
4862
6266
  ruleId: "api/admin-db-access-without-auth",
4863
6267
  ...trace,
4864
6268
  evidence: evidence.slice(0, 24),
4865
6269
  evidenceTruncated: trace.evidenceTruncated || evidence.length > 24,
4866
6270
  severity: "P0",
4867
- // 管理员客户端缺少鉴权时使用确定置信度;可能受全局鉴权覆盖时降为疑似。
4868
- confidence: globalGuard === null && !indirect ? "certain" : "likely",
6271
+ // 管理员客户端缺少鉴权时使用确定置信度;可能受全局鉴权或未确认的中间件覆盖时降为疑似。
6272
+ confidence: globalGuard === null && !indirect && unconfirmedMiddleware.length === 0 ? "certain" : "likely",
4869
6273
  title: indirect ? `Review indirect authentication for admin database access in ${url}` : `Anyone can call ${url} and it queries your database as admin`,
4870
6274
  file: route.file.path,
4871
6275
  line,
@@ -4900,12 +6304,16 @@ var apiAuthRule = {
4900
6304
  file: route.file.path,
4901
6305
  line,
4902
6306
  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] } : {},
6307
+ ...indirect || delegated.length > 0 ? { evidence: [
6308
+ {
6309
+ kind: "operation",
6310
+ file: route.file.path,
6311
+ line,
6312
+ description: indirect ? "Data write using a local authentication helper." : "Call that leads to a database write."
6313
+ },
6314
+ ...delegatedEvidence,
6315
+ ...indirectEvidence
6316
+ ] } : {},
4909
6317
  why: [
4910
6318
  ...globalNote,
4911
6319
  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 +6712,7 @@ var corsRule = {
5304
6712
  // src/rules/request-input.ts
5305
6713
  var MAX_VALUE_HOPS = 8;
5306
6714
  var MAX_ASSIGNMENTS = 512;
5307
- var MAX_EXPRESSION = 4e3;
6715
+ var MAX_EXPRESSION = 65536;
5308
6716
  var REQUEST_OBJECT_NAMES = /^(?:req|request|event|evt|ctx|context)$/;
5309
6717
  var DESTRUCTURED_OBJECTS = /* @__PURE__ */ new Set(["request", "req", "event"]);
5310
6718
  var DESTRUCTURED_URLS = /* @__PURE__ */ new Set(["url"]);
@@ -5346,6 +6754,8 @@ var TRANSPARENT_CALLS = /* @__PURE__ */ new Set([
5346
6754
  "getRequestURL"
5347
6755
  ]);
5348
6756
  var H3_READERS = "readBody|readFormData|readMultipartFormData|readRawBody|readValidatedBody|getQuery|getValidatedQuery|getRouterParams?|getValidatedRouterParams|getHeaders?|getRequestHeaders?|getCookie|parseCookies|getRequestURL";
6757
+ var HONO_MEMBERS = "param|query|queries|header|json|text|parseBody|formData|arrayBuffer|blob|url|path|valid|headers";
6758
+ var HONO_READ_CALLS = "param|query|queries|header|json|text|parseBody|formData|arrayBuffer|blob|valid";
5349
6759
  var LITERAL = String.raw`(?:'[^']*'|"[^"]*"|\x60[^\x60$]*\x60|-?\d+(?:\.\d+)?|null|undefined|true|false)`;
5350
6760
  var CLEAN_EXPRESSION = [
5351
6761
  /^(?:await\s+)?(?:Number|parseInt|parseFloat|Boolean|BigInt|isNaN|Math\s*\.\s*\w+|Number\s*\.\s*\w+)\s*\(/,
@@ -5356,10 +6766,86 @@ var CLEAN_EXPRESSION = [
5356
6766
  // 带标签的模板由标签函数处理插值(Prisma.sql、sql、html),String.raw 除外。
5357
6767
  /^(?:await\s+)?(?!String\s*\.\s*raw\b)[\w$]+(?:\s*\.\s*[\w$]+)*\s*\x60/
5358
6768
  ];
6769
+ var MIRROR_CALLS = /* @__PURE__ */ new Set([
6770
+ "parse",
6771
+ "safeParse",
6772
+ "parseAsync",
6773
+ "safeParseAsync",
6774
+ "validate",
6775
+ "validateSync",
6776
+ "validateAsync",
6777
+ "cast",
6778
+ "structuredClone",
6779
+ "clone",
6780
+ "assign"
6781
+ ]);
6782
+ var VALUE_MEMBERS = /^\s*\??\.\s*(?:toString|valueOf|href|trim|trimStart|trimEnd|toLowerCase|toUpperCase|normalize)\b/;
6783
+ var HOST_HEADER = /^\s*(?:\??\.\s*get\s*\(\s*['"]host['"]\s*\)|\??\.\s*host\b|\[\s*['"]host['"]\s*\]|\(\s*['"]host['"]\s*\))/i;
5359
6784
  var URL_INPUT_ACCESS = /\??\.\s*(?:searchParams|pathname|search|hash)\b/;
5360
6785
  var URL_INPUT_KEYS = /* @__PURE__ */ new Set(["searchParams", "pathname", "search", "hash"]);
5361
6786
  var URL_ORIGIN_ACCESS = /^\s*\??\.\s*(?:origin|host|hostname|protocol|port)\b/;
5362
6787
  var URL_ORIGIN_KEYS = /* @__PURE__ */ new Set(["origin", "host", "hostname", "protocol", "port"]);
6788
+ function closeOf(text, open) {
6789
+ const pairs = { "(": ")", "[": "]", "{": "}" };
6790
+ const stack = [];
6791
+ for (let i = open; i < text.length; i++) {
6792
+ const ch = text[i];
6793
+ if (pairs[ch]) stack.push(pairs[ch]);
6794
+ else if (ch === stack[stack.length - 1]) {
6795
+ stack.pop();
6796
+ if (stack.length === 0) return i;
6797
+ }
6798
+ }
6799
+ return -1;
6800
+ }
6801
+ function callResultOf(expression) {
6802
+ const text = expression.trim().replace(/\s+as\s+[\w$.<>[\]| ]+$/, "").replace(/^(?:(?:await|new)\s+)+/, "");
6803
+ let i = 0;
6804
+ let callee = null;
6805
+ let members = false;
6806
+ if (text[0] === "(") {
6807
+ const close = closeOf(text, 0);
6808
+ if (close === -1) return null;
6809
+ const inner = callResultOf(text.slice(1, close));
6810
+ if (!inner || inner.members) return null;
6811
+ callee = inner.callee;
6812
+ i = close + 1;
6813
+ } else {
6814
+ const head = /^[A-Za-z_$][\w$]*/.exec(text);
6815
+ if (!head) return null;
6816
+ i = head[0].length;
6817
+ }
6818
+ let last = callee === null ? text.slice(0, i) : callee;
6819
+ while (i < text.length) {
6820
+ const rest = text.slice(i);
6821
+ const member = /^\s*(?:\??\.)\s*([A-Za-z_$][\w$]*)/.exec(rest);
6822
+ if (member) {
6823
+ last = member[1];
6824
+ if (callee !== null) members = true;
6825
+ i += member[0].length;
6826
+ continue;
6827
+ }
6828
+ const open = /^\s*(?:\?\.)?\s*([([])/.exec(rest);
6829
+ if (open) {
6830
+ const at = i + open[0].length - 1;
6831
+ const close = closeOf(text, at);
6832
+ if (close === -1) return null;
6833
+ if (open[1] === "(") {
6834
+ callee = last;
6835
+ members = false;
6836
+ } else if (callee !== null) members = true;
6837
+ i = close + 1;
6838
+ continue;
6839
+ }
6840
+ if (/^\s*!/.test(rest) && !/^\s*!=/.test(rest)) {
6841
+ i += rest.indexOf("!") + 1;
6842
+ continue;
6843
+ }
6844
+ if (rest.trim() === "") break;
6845
+ return null;
6846
+ }
6847
+ return callee === null ? null : { callee, members };
6848
+ }
5363
6849
  function isComparison(expr) {
5364
6850
  return !/\?/.test(expr) && !/=>/.test(expr) && /(?:===|!==|==|!=|<=|>=|\binstanceof\b)/.test(expr);
5365
6851
  }
@@ -5393,10 +6879,10 @@ function patternNames(pattern) {
5393
6879
  const stripped = text.replace(/[A-Za-z_$][\w$]*\s*:(?!:)/g, " ").replace(/=[^,}\]]*/g, " ");
5394
6880
  return stripped.match(/[A-Za-z_$][\w$]*/g) ?? [];
5395
6881
  }
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() };
6882
+ function paramRoles(paramsText, paramsStart, allInput, context = false) {
6883
+ const roles = { objects: /* @__PURE__ */ new Map(), urls: /* @__PURE__ */ new Map(), cookies: /* @__PURE__ */ new Map(), values: /* @__PURE__ */ new Map(), contexts: /* @__PURE__ */ new Map() };
5398
6884
  let offset = 0;
5399
- for (const raw of parameterList(paramsText)) {
6885
+ for (const [index, raw] of parameterList(paramsText).entries()) {
5400
6886
  const at = paramsStart + Math.max(0, paramsText.indexOf(raw, offset));
5401
6887
  offset = Math.max(offset, paramsText.indexOf(raw, offset) + raw.length);
5402
6888
  const param = raw.startsWith("{") || raw.startsWith("[") ? raw.slice(0, matchingClose(raw) + 1) : raw.replace(/^\.\.\./, "").split(/[:=]/)[0].trim();
@@ -5404,6 +6890,10 @@ function paramRoles(paramsText, paramsStart, allInput) {
5404
6890
  for (const name of patternNames(param)) roles.values.set(name, at);
5405
6891
  continue;
5406
6892
  }
6893
+ if (context && index === 0 && /^[A-Za-z_$][\w$]*$/.test(param)) {
6894
+ roles.contexts.set(param, at);
6895
+ continue;
6896
+ }
5407
6897
  if (param.startsWith("{")) {
5408
6898
  for (const { key, local } of destructuredKeys(param)) {
5409
6899
  if (DESTRUCTURED_OBJECTS.has(key)) roles.objects.set(local, at);
@@ -5440,7 +6930,7 @@ function parametersOf(code, body, pairs, openers) {
5440
6930
  const single = /([A-Za-z_$][\w$]*)\s*$/.exec(window);
5441
6931
  return single ? { start: body.declaration - window.length + single.index, end: body.declaration - window.length + single.index + single[1].length } : null;
5442
6932
  }
5443
- function expressionEnd(code, from, limit, pairs, truncated) {
6933
+ function expressionEnd2(code, from, limit, pairs, truncated) {
5444
6934
  const stop = Math.min(limit, from + MAX_EXPRESSION);
5445
6935
  for (let i = from; i < stop; i++) {
5446
6936
  const ch = code[i];
@@ -5507,6 +6997,7 @@ var InputFlow = class {
5507
6997
  this.fixedTables = fixedTables;
5508
6998
  for (const [name, at] of roles.values) this.parameters.set(name, { level: "direct", origin: at, names: /* @__PURE__ */ new Set([name]), ownUrl: false });
5509
6999
  this.sourcePattern = sourceRegex(roles);
7000
+ 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
7001
  const assignments = this.assignments().sort((a, b) => a.at - b.at);
5511
7002
  const region = code.slice(span.start, span.end);
5512
7003
  const functions = this.localFunctions(region, assignments);
@@ -5520,7 +7011,7 @@ var InputFlow = class {
5520
7011
  const open = span.start + m.index + m[0].length - 1;
5521
7012
  let start = m[0] === "else" ? open + 1 : (pairs.get(open) ?? open) + 1;
5522
7013
  while (/\s/.test(code[start] ?? "")) start++;
5523
- controls.push({ start, end: pairs.get(start) ?? expressionEnd(code, start, span.end, pairs, () => {
7014
+ controls.push({ start, end: pairs.get(start) ?? expressionEnd2(code, start, span.end, pairs, () => {
5524
7015
  this.limited = true;
5525
7016
  }) });
5526
7017
  if (controls.length >= MAX_ASSIGNMENTS) {
@@ -5552,9 +7043,11 @@ var InputFlow = class {
5552
7043
  limited = false;
5553
7044
  assignmentList = /* @__PURE__ */ new Map();
5554
7045
  sourcePattern;
7046
+ contextRead;
5555
7047
  references = null;
5556
7048
  values = /* @__PURE__ */ new Map();
5557
7049
  strings = /* @__PURE__ */ new Map();
7050
+ mirrors = /* @__PURE__ */ new Map();
5558
7051
  /** 只索引当前处理函数;未引用的具名闭包不影响外层值。 */
5559
7052
  localFunctions(region, assignments) {
5560
7053
  const offset = this.span.start;
@@ -5565,7 +7058,7 @@ var InputFlow = class {
5565
7058
  }));
5566
7059
  for (const m of region.matchAll(/=>\s*(?!\s*\{)/g)) {
5567
7060
  const start = offset + m.index + m[0].length;
5568
- const end = expressionEnd(this.code, start, this.span.end, this.pairs, () => {
7061
+ const end = expressionEnd2(this.code, start, this.span.end, this.pairs, () => {
5569
7062
  this.limited = true;
5570
7063
  });
5571
7064
  bodies.push({ declaration: offset + m.index, start: start - 1, end });
@@ -5607,13 +7100,16 @@ var InputFlow = class {
5607
7100
  this.sourcePattern.lastIndex = 0;
5608
7101
  for (const m of observed.matchAll(this.sourcePattern)) {
5609
7102
  const after = observed.slice(m.index + m[0].length);
5610
- const ownUrl = m[1] === "url" || m[1] === "nextUrl";
7103
+ const member = m.slice(1).find((group) => group !== void 0);
7104
+ const ownUrl = member === "url" || member === "nextUrl";
5611
7105
  if (ownUrl && URL_ORIGIN_ACCESS.test(after)) continue;
7106
+ const hostAt = exprAt + m.index + m[0].length;
7107
+ const hostHeader = (member === "headers" || member === "header") && HOST_HEADER.test(this.source.slice(hostAt, hostAt + 80));
5612
7108
  found = mergeTaint(found, {
5613
7109
  level: "direct",
5614
7110
  origin: exprAt + m.index,
5615
7111
  names: /* @__PURE__ */ new Set(),
5616
- ownUrl: ownUrl && !URL_INPUT_ACCESS.test(after.slice(0, 40))
7112
+ ownUrl: hostHeader || ownUrl && !URL_INPUT_ACCESS.test(after.slice(0, 40))
5617
7113
  });
5618
7114
  }
5619
7115
  }
@@ -5626,16 +7122,72 @@ var InputFlow = class {
5626
7122
  if (/^\s*:(?!:)/.test(after) && /[{,]$/.test(before)) continue;
5627
7123
  const t = this.valueOf(m[0], exprAt, depth);
5628
7124
  if (!t) continue;
7125
+ if (t.opaque && /^\s*(?:\??\.\s*[\w$]|\?\.\s*\[|\[)/.test(after) && !VALUE_MEMBERS.test(after)) continue;
5629
7126
  if (t.ownUrl && URL_ORIGIN_ACCESS.test(after)) continue;
5630
7127
  found = mergeTaint(found, t.ownUrl && URL_INPUT_ACCESS.test(after.slice(0, 40)) ? { ...t, ownUrl: false } : t);
5631
7128
  }
5632
7129
  }
5633
7130
  if (!found) return null;
5634
7131
  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]);
7132
+ const read = this.contextRead ? text.replace(new RegExp(this.contextRead.source, "g"), (_whole, call2) => call2 === "valid" ? "parse(" : "json(") : text;
7133
+ const calls = [...read.replace(/\bJSON\s*\.\s*parse\s*\(/g, "(").matchAll(/([\w$]+)\s*\(/g)].map((m) => m[1]);
5636
7134
  if (calls.some((name) => !TRANSPARENT_CALLS.has(name))) found.level = "derived";
7135
+ const call = callResultOf(read);
7136
+ if (call && !TRANSPARENT_CALLS.has(call.callee) && !MIRROR_CALLS.has(call.callee) && !this.returnsArgument(read, call.callee)) {
7137
+ if (call.members) return null;
7138
+ found.opaque = true;
7139
+ }
5637
7140
  return found;
5638
7141
  }
7142
+ /**
7143
+ * 表达式直接调用同文件中定义的函数,且该函数在调用参数之外返回某个形参:return value、return { ...value }、
7144
+ * (v) => v.trim()。return db.find(value) 之类把形参交给其他调用的不算,结果仍按未知函数处理。
7145
+ */
7146
+ returnsArgument(expression, callee) {
7147
+ const escaped = callee.replace(/\$/g, "\\$");
7148
+ if (!new RegExp(`^(?:(?:await|new)\\s+)*${escaped}\\s*\\(`).test(expression)) return false;
7149
+ const cached = this.mirrors.get(callee);
7150
+ if (cached !== void 0) return cached;
7151
+ const code = this.code;
7152
+ let params = "";
7153
+ let after = -1;
7154
+ const declared = new RegExp(`(?:^|[^\\w$.])(?:async\\s+)?function\\s*\\*?\\s*${escaped}\\s*\\(`).exec(code);
7155
+ 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);
7156
+ const match = declared ?? assigned;
7157
+ if (match && assigned?.[1]) {
7158
+ params = assigned[1];
7159
+ after = match.index + match[0].length;
7160
+ } else if (match) {
7161
+ const open = match.index + match[0].length - 1;
7162
+ const close = this.pairs.get(open);
7163
+ if (close !== void 0) {
7164
+ params = code.slice(open + 1, close);
7165
+ after = close + 1;
7166
+ }
7167
+ }
7168
+ let result = false;
7169
+ if (after !== -1) {
7170
+ const names = params.split(",").flatMap((part) => part.replace(/[:=][\s\S]*$/, "").match(/[A-Za-z_$][\w$]*/g) ?? []);
7171
+ const head = /^\s*(?::[^={]{0,200})?(?:=>)?\s*/.exec(code.slice(after, after + 260));
7172
+ const start = after + head[0].length;
7173
+ const end = (from, limit) => expressionEnd2(code, from, limit, this.pairs, () => {
7174
+ this.limited = true;
7175
+ });
7176
+ const bodyEnd = this.pairs.get(start) ?? start;
7177
+ const returns = code[start] === "{" ? [...code.slice(start, bodyEnd).matchAll(/\breturn\b/g)].map((m) => {
7178
+ const from = start + m.index + m[0].length;
7179
+ return code.slice(from, end(from, bodyEnd));
7180
+ }) : [code.slice(start, end(start, code.length))];
7181
+ const outsideCalls = (text) => {
7182
+ let current = text.replace(/\.\.\./g, " ");
7183
+ for (let i = 0; i < 8 && /[\w$)\]]\s*\([^()]*\)/.test(current); i++) current = current.replace(/([\w$)\]])\s*\([^()]*\)/g, "$1");
7184
+ return current;
7185
+ };
7186
+ result = names.length > 0 && returns.some((text) => names.some((name) => new RegExp(`(?<![\\w$.])${name.replace(/\$/g, "\\$")}(?![\\w$])`).test(outsideCalls(text))));
7187
+ }
7188
+ this.mirrors.set(callee, result);
7189
+ return result;
7190
+ }
5639
7191
  /** 字符串拼接:无标签模板的插值或含字符串操作数的 + 拼接中携带请求输入。 */
5640
7192
  builtOf(expr, exprAt, depth = 0) {
5641
7193
  if (expr.length > MAX_EXPRESSION) this.limited = true;
@@ -5687,6 +7239,7 @@ var InputFlow = class {
5687
7239
  for (const a of assignments) {
5688
7240
  const taint = this.builtOf(a.expr, a.exprAt, depth + 1) ?? this.taintOf(a.expr, a.exprAt, depth + 1);
5689
7241
  if (!taint) continue;
7242
+ if (taint.opaque && (a.iterate || /^[{[]/.test(a.pattern.trim()))) continue;
5690
7243
  const own = this.targetsOf(a, taint).find(([target]) => target === name);
5691
7244
  if (own) found = mergeTaint(found, { ...taint, level: a.indirect ? "derived" : taint.level, ownUrl: own[1], names: /* @__PURE__ */ new Set([name, ...taint.names]) });
5692
7245
  }
@@ -5826,7 +7379,7 @@ ${part}`.slice(0, MAX_EXPRESSION);
5826
7379
  const forOf = /^\s+of\s+/.exec(rest);
5827
7380
  if (forOf) {
5828
7381
  const exprAt2 = patternEnd + forOf[0].length;
5829
- const end2 = expressionEnd(code, exprAt2, span.end, pairs, () => {
7382
+ const end2 = expressionEnd2(code, exprAt2, span.end, pairs, () => {
5830
7383
  this.limited = true;
5831
7384
  });
5832
7385
  push({ at, pattern, names: patternNames(pattern), expr: code.slice(exprAt2, end2), exprAt: exprAt2, iterate: true, append: false, declaration: true });
@@ -5835,7 +7388,7 @@ ${part}`.slice(0, MAX_EXPRESSION);
5835
7388
  const eq = /^\s*(?::[^=;]{0,200}?)?=(?![=>])/.exec(rest);
5836
7389
  if (!eq) break;
5837
7390
  const exprAt = patternEnd + eq[0].length;
5838
- const end = expressionEnd(code, exprAt, span.end, pairs, () => {
7391
+ const end = expressionEnd2(code, exprAt, span.end, pairs, () => {
5839
7392
  this.limited = true;
5840
7393
  });
5841
7394
  push({ at, pattern, names: patternNames(pattern), expr: code.slice(exprAt, end), exprAt, iterate: false, append: false, declaration: true });
@@ -5848,7 +7401,7 @@ ${part}`.slice(0, MAX_EXPRESSION);
5848
7401
  const before = region.slice(Math.max(0, m.index - 8), m.index);
5849
7402
  if (/\b(?:const|let|var)\s+$/.test(before)) continue;
5850
7403
  const exprAt = span.start + m.index + m[0].length;
5851
- const end = expressionEnd(code, exprAt, span.end, pairs, () => {
7404
+ const end = expressionEnd2(code, exprAt, span.end, pairs, () => {
5852
7405
  this.limited = true;
5853
7406
  });
5854
7407
  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 +7411,9 @@ ${part}`.slice(0, MAX_EXPRESSION);
5858
7411
  };
5859
7412
  function sourceRegex(roles) {
5860
7413
  const parts = [];
7414
+ if (roles.contexts.size > 0) {
7415
+ parts.push(String.raw`(?<![\w$.])(?:${namePattern(roles.contexts.keys())})\s*\??\.\s*req(?:\s*\??\.\s*raw)?\s*\??\.\s*(${HONO_MEMBERS})\b`);
7416
+ }
5861
7417
  if (roles.objects.size > 0) {
5862
7418
  const objects = namePattern(roles.objects.keys());
5863
7419
  parts.push(
@@ -5875,7 +7431,8 @@ function sourceRegex(roles) {
5875
7431
  return parts.length === 0 ? null : new RegExp(parts.join("|"), "g");
5876
7432
  }
5877
7433
  function mergeTaint(found, t) {
5878
- if (!found) return { level: t.level, origin: t.origin, names: new Set(t.names), ownUrl: t.ownUrl };
7434
+ if (!found) return { level: t.level, origin: t.origin, names: new Set(t.names), ownUrl: t.ownUrl, ...t.opaque ? { opaque: true } : {} };
7435
+ if (found.opaque && !t.opaque) delete found.opaque;
5879
7436
  if (t.level === "derived") found.level = "derived";
5880
7437
  if (found.ownUrl && !t.ownUrl) {
5881
7438
  found.ownUrl = false;
@@ -5885,7 +7442,7 @@ function mergeTaint(found, t) {
5885
7442
  return found;
5886
7443
  }
5887
7444
  function hasInput(roles) {
5888
- return roles.objects.size + roles.urls.size + roles.cookies.size + roles.values.size > 0;
7445
+ return roles.objects.size + roles.urls.size + roles.cookies.size + roles.values.size + roles.contexts.size > 0;
5889
7446
  }
5890
7447
  function lowerBound(values, value) {
5891
7448
  let low = 0, high = values.length;
@@ -5935,20 +7492,25 @@ var HandlerFile = class {
5935
7492
  }
5936
7493
  };
5937
7494
  var handlerCache = /* @__PURE__ */ new WeakMap();
5938
- function handlerFileOf(file) {
7495
+ function handlerFileOf(file, files) {
5939
7496
  const cached = handlerCache.get(file);
5940
7497
  if (cached !== void 0) return cached;
5941
7498
  let result = null;
5942
7499
  if (/\.[mc]?[jt]sx?$/.test(file.path)) {
5943
- const route = routeOf(file);
5944
- const routes = route === null ? serverActionRoutes(file) : [route];
7500
+ const routes = serverRoutesOf(file, files);
5945
7501
  if (routes.length > 0) {
5946
7502
  const code = noiseMaskedOf(file);
5947
7503
  const source = commentsMaskedOf(file);
5948
7504
  const pairs = delimiterPairs(code);
5949
7505
  const openers = /* @__PURE__ */ new Map();
5950
7506
  for (const [open, close] of pairs) openers.set(close, open);
5951
- const bodies = functionBodies(code, pairs).sort((a, b) => a.start - b.start);
7507
+ const bodies = functionBodies(code, pairs);
7508
+ for (const r of routes) {
7509
+ if (NODE_FRAMEWORKS.has(r.framework) && r.reachable && code.startsWith("=>", r.reachable.start)) {
7510
+ bodies.push({ declaration: r.reachable.start, start: r.reachable.start, end: r.reachable.end });
7511
+ }
7512
+ }
7513
+ bodies.sort((a, b) => a.start - b.start);
5952
7514
  const starts = bodies.map((body) => body.start);
5953
7515
  const candidates = [];
5954
7516
  for (const r of routes) {
@@ -5961,7 +7523,8 @@ function handlerFileOf(file) {
5961
7523
  const action = r.action !== void 0 && reachable !== void 0 && body.start === reachable.start;
5962
7524
  const params = parametersOf(code, body, pairs, openers);
5963
7525
  if (!params) continue;
5964
- const roles = paramRoles(source.slice(params.start, params.end), params.start, action);
7526
+ const context = r.framework === "hono" && reachable !== void 0 && body.start === reachable.start;
7527
+ const roles = paramRoles(source.slice(params.start, params.end), params.start, action, context);
5965
7528
  if (hasInput(roles)) candidates.push({ route: r, body, roles });
5966
7529
  }
5967
7530
  }
@@ -5974,7 +7537,7 @@ function handlerFileOf(file) {
5974
7537
  function reportInputLimit(ctx, file, analysed) {
5975
7538
  if (analysed.limited) ctx.reportIncomplete(
5976
7539
  "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)`
7540
+ `${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
7541
  );
5979
7542
  }
5980
7543
 
@@ -6146,7 +7709,7 @@ var injectionRule = {
6146
7709
  check(ctx) {
6147
7710
  const findings = [];
6148
7711
  for (const file of ctx.files) {
6149
- const analysed = handlerFileOf(file);
7712
+ const analysed = handlerFileOf(file, ctx.files);
6150
7713
  if (!analysed) continue;
6151
7714
  const { code, source, pairs, lineStarts } = analysed;
6152
7715
  const sinks = sinksIn(code, pairs, commandCallees(file));
@@ -6181,7 +7744,7 @@ var injectionRule = {
6181
7744
  // src/rules/outbound.ts
6182
7745
  var FETCH_CALL = /(?<![\w$.])(?:fetch|\$fetch|ofetch|axios|got|ky|needle)\s*(?:<[^()]{0,200}>\s*)?\(/g;
6183
7746
  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;
7747
+ var REDIRECT_CALL = /(?<![\w$.])(?:redirect|permanentRedirect|sendRedirect)\s*\(|(?<![\w$.])(?:NextResponse|Response|res|reply|response|ctx|context|c|Astro)\s*\.\s*redirect\s*\(/g;
6185
7748
  function callsIn(code, pairs) {
6186
7749
  const calls = [];
6187
7750
  const add = (regex, kind) => {
@@ -6199,6 +7762,8 @@ function callsIn(code, pairs) {
6199
7762
  }
6200
7763
  var HOST_OPEN = /^(?:X?|[a-z][\w+.-]*:\/\/[^/?#\\]*)$/i;
6201
7764
  var REDIRECT_OPEN = /^(?:\/|[/\\]{2}[^/?#\\]*)$/;
7765
+ var HOST_OPENABLE = /^(?:X?|[a-z][\w+.-]*(?::(?:\/(?:\/[^/?#\\]*)?)?)?)$/i;
7766
+ var REDIRECT_OPENABLE = /^(?:[/\\](?:[/\\][^/?#\\]*)?)?$/;
6202
7767
  function controlsStart(expr, at, flow, source, kind, depth = 0) {
6203
7768
  const lead = expr.length - expr.trimStart().length;
6204
7769
  const text = expr.trim();
@@ -6208,11 +7773,19 @@ function controlsStart(expr, at, flow, source, kind, depth = 0) {
6208
7773
  flow.limited = true;
6209
7774
  return null;
6210
7775
  }
6211
- if (text.length > 4e3) {
7776
+ if (text.length > MAX_EXPRESSION) {
6212
7777
  flow.limited = true;
6213
7778
  return null;
6214
7779
  }
6215
7780
  const open = (prefix) => HOST_OPEN.test(prefix) || kind === "redirect" && REDIRECT_OPEN.test(prefix);
7781
+ const branches = alternativesOf(text);
7782
+ if (branches) {
7783
+ for (const branch of branches) {
7784
+ const t2 = controlsStart(branch.text, start + branch.at, flow, source, kind, depth + 1);
7785
+ if (t2) return t2;
7786
+ }
7787
+ return null;
7788
+ }
6216
7789
  const url = /^new\s+URL\s*\(/.exec(text);
6217
7790
  if (url) {
6218
7791
  const argsStart = start + url[0].length;
@@ -6223,9 +7796,10 @@ function controlsStart(expr, at, flow, source, kind, depth = 0) {
6223
7796
  return first === void 0 ? null : controlsStart(first, argsStart, flow, source, "redirect", depth + 1);
6224
7797
  }
6225
7798
  }
6226
- if (text.startsWith("`")) return templateStart(text, start, flow, source, open);
6227
- const operands = flow.operandsOf(text, start);
6228
- if (operands.length > 1) {
7799
+ if (text.startsWith("`")) return templateStart(text, start, flow, source, open, kind, depth);
7800
+ const joined = joinedElements(text, start);
7801
+ const operands = joined ?? flow.operandsOf(text, start);
7802
+ if (operands.length > 1 || joined && operands.length > 0) {
6229
7803
  let prefix = "";
6230
7804
  for (const operand of operands) {
6231
7805
  const inner = operand.text.trim();
@@ -6233,7 +7807,7 @@ function controlsStart(expr, at, flow, source, kind, depth = 0) {
6233
7807
  prefix += source.slice(operand.at + operand.text.indexOf(inner) + 1, operand.at + operand.text.indexOf(inner) + inner.length - 1);
6234
7808
  continue;
6235
7809
  }
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);
7810
+ 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
7811
  if (t2) return open(prefix) ? t2 : null;
6238
7812
  prefix += "X";
6239
7813
  if (!open(prefix)) return null;
@@ -6247,6 +7821,11 @@ function controlsStart(expr, at, flow, source, kind, depth = 0) {
6247
7821
  return t2 && !t2.ownUrl ? t2 : null;
6248
7822
  }
6249
7823
  for (const a of assignments) {
7824
+ if (a.iterate || /^[{[]/.test(a.pattern.trim())) {
7825
+ const t3 = flow.valueOf(text, at);
7826
+ if (t3 && !t3.ownUrl) return t3;
7827
+ continue;
7828
+ }
6250
7829
  const t2 = controlsStart(a.expr, a.exprAt, flow, source, kind, depth + 1);
6251
7830
  if (t2) return { ...t2, level: a.indirect ? "derived" : t2.level, names: /* @__PURE__ */ new Set([...t2.names, text]) };
6252
7831
  }
@@ -6255,7 +7834,42 @@ function controlsStart(expr, at, flow, source, kind, depth = 0) {
6255
7834
  const t = flow.taintOf(text, start);
6256
7835
  return t && !t.ownUrl ? t : null;
6257
7836
  }
6258
- function templateStart(text, start, flow, source, open) {
7837
+ function alternativesOf(text) {
7838
+ const top = [];
7839
+ let depth = 0;
7840
+ for (let i = 0; i < text.length; i++) {
7841
+ const ch = text[i];
7842
+ if ("([{".includes(ch)) depth++;
7843
+ else if (")]}".includes(ch)) depth--;
7844
+ else if (depth === 0) top.push(i);
7845
+ }
7846
+ const at = (i) => top.includes(i) ? text[i] : "";
7847
+ for (const i of top) {
7848
+ if (text[i] !== "?" || text[i + 1] === "." || text[i + 1] === "?" || text[i - 1] === "?") continue;
7849
+ let nested = 0;
7850
+ for (const j of top) {
7851
+ if (j <= i) continue;
7852
+ if (text[j] === "?" && text[j + 1] !== "." && text[j + 1] !== "?" && text[j - 1] !== "?") nested++;
7853
+ else if (text[j] === ":" && nested-- === 0) {
7854
+ return [{ text: text.slice(i + 1, j), at: i + 1 }, { text: text.slice(j + 1), at: j + 1 }];
7855
+ }
7856
+ }
7857
+ return null;
7858
+ }
7859
+ const parts = [];
7860
+ let from = 0;
7861
+ for (const i of top) {
7862
+ const pair = at(i) + at(i + 1);
7863
+ if ((pair === "??" || pair === "||") && i >= from) {
7864
+ parts.push({ text: text.slice(from, i), at: from });
7865
+ from = i + 2;
7866
+ }
7867
+ }
7868
+ if (parts.length === 0) return null;
7869
+ parts.push({ text: text.slice(from), at: from });
7870
+ return parts;
7871
+ }
7872
+ function templateStart(text, start, flow, source, open, kind, depth) {
6259
7873
  let prefix = "";
6260
7874
  for (let i = 1; i < text.length; i++) {
6261
7875
  if (text[i] === "`") return null;
@@ -6263,12 +7877,16 @@ function templateStart(text, start, flow, source, open) {
6263
7877
  const close = matchingBrace(text, i + 1);
6264
7878
  if (close === null) return null;
6265
7879
  const t = flow.taintOf(text.slice(i + 2, close), start + i + 2);
6266
- if (t && !t.ownUrl) return open(prefix) ? t : null;
7880
+ if (t && !t.ownUrl) {
7881
+ if (!open(prefix)) return null;
7882
+ return prefix === "" ? controlsStart(text.slice(i + 2, close), start + i + 2, flow, source, kind, depth + 1) : t;
7883
+ }
6267
7884
  prefix += "X";
6268
7885
  i = close;
6269
7886
  continue;
6270
7887
  }
6271
7888
  prefix += source[start + i];
7889
+ if (!HOST_OPENABLE.test(prefix) && !(kind === "redirect" && REDIRECT_OPENABLE.test(prefix))) return null;
6272
7890
  if (prefix.length > 200) {
6273
7891
  flow.limited = true;
6274
7892
  return null;
@@ -6292,6 +7910,30 @@ function matchingBrace(text, open) {
6292
7910
  }
6293
7911
  return null;
6294
7912
  }
7913
+ function joinedElements(text, start) {
7914
+ if (!text.startsWith("[")) return null;
7915
+ let depth = 0, close = -1;
7916
+ for (let i = 0; i < text.length; i++) {
7917
+ if ("([{".includes(text[i])) depth++;
7918
+ else if (")]}".includes(text[i]) && --depth === 0) {
7919
+ close = i;
7920
+ break;
7921
+ }
7922
+ }
7923
+ if (close === -1 || text[close] !== "]" || !/^\s*\.\s*join\s*\(\s*(['"\x60])\1\s*\)\s*$/.test(text.slice(close + 1))) return null;
7924
+ const elements = [];
7925
+ let from = 1;
7926
+ depth = 0;
7927
+ for (let i = 1; i <= close; i++) {
7928
+ if (i < close && "([{".includes(text[i])) depth++;
7929
+ else if (i < close && ")]}".includes(text[i])) depth--;
7930
+ else if (i === close || text[i] === "," && depth === 0) {
7931
+ if (text.slice(from, i).trim() !== "") elements.push({ text: text.slice(from, i), at: start + from });
7932
+ from = i + 1;
7933
+ }
7934
+ }
7935
+ return elements;
7936
+ }
6295
7937
  function splitTopLevel(text) {
6296
7938
  const parts = [];
6297
7939
  let depth = 0, from = 0;
@@ -6380,13 +8022,14 @@ function redirectFinding(route, file, line, originLine, certain) {
6380
8022
  function check(ctx, kind) {
6381
8023
  const findings = [];
6382
8024
  for (const file of ctx.files) {
6383
- const analysed = handlerFileOf(file);
8025
+ const analysed = handlerFileOf(file, ctx.files);
6384
8026
  if (!analysed) continue;
6385
8027
  const calls = callsIn(analysed.code, analysed.pairs).filter((call) => call.kind === kind).sort((a, b) => a.at - b.at);
6386
8028
  if (calls.length === 0) continue;
6387
8029
  const positions = calls.map((call) => call.at);
6388
8030
  const reported = /* @__PURE__ */ new Set();
6389
8031
  for (const { route, body, flow } of analysed.handlersAround(positions)) {
8032
+ if (kind === "redirect" && route.action !== void 0) continue;
6390
8033
  for (let i = lowerBound(positions, body.start + 1); i < calls.length && calls[i].at < body.end; i++) {
6391
8034
  const call = calls[i];
6392
8035
  if (call.close >= body.end || reported.has(call.at)) continue;
@@ -6589,18 +8232,19 @@ var SERVER_PATH = [
6589
8232
  /\.server\.[mc]?[jt]s$/,
6590
8233
  /(?:^|\/)server\/.+\.[mc]?[jt]s$/
6591
8234
  ];
6592
- function isServerFile(file) {
8235
+ function isServerFile(file, files) {
6593
8236
  if (!/\.[mc]?[jt]sx?$/.test(file.path)) return false;
6594
8237
  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;
8238
+ return SERVER_PATH.some((pattern) => pattern.test(file.path)) || serverRoutesOf(file, files).length > 0;
6596
8239
  }
6597
8240
  var sessionRule = {
6598
8241
  id: "auth/unverified-session",
6599
8242
  severity: "P1",
6600
8243
  appliesTo(file) {
6601
- return file.content.includes("getSession") && isServerFile(file);
8244
+ return file.content.includes("getSession");
6602
8245
  },
6603
- check(file) {
8246
+ check(file, ctx) {
8247
+ if (!isServerFile(file, ctx.files)) return [];
6604
8248
  const code = noiseMaskedOf(file);
6605
8249
  const context = new LocalVerification(file);
6606
8250
  const { pairs, bodies } = context;
@@ -6657,7 +8301,7 @@ var STRIPE_EVENT = /(?:\bcase\s*|\.type\s*[!=]==?\s*)['"`]((?:checkout\.session|
6657
8301
  function verifiedEvent(context, name, at) {
6658
8302
  const assignment = context.assignment(name, at);
6659
8303
  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);
8304
+ const call = /^(?:(await)\s+)?(?:[\w$]+(?:\s*\([^()]*\))?\s*\??\.\s*)+(constructEvent(?:Async)?|retrieve|parseEventNotification)\s*\(/.exec(assignment.expression);
6661
8305
  if (!call || call[2] !== "constructEvent" && !call[1]) return false;
6662
8306
  if (call[2] === "retrieve" && !/\.events\s*\.\s*retrieve/.test(call[0])) return false;
6663
8307
  const close = context.pairs.get(assignment.from + call[0].length - 1);
@@ -6673,22 +8317,71 @@ function eventName(context, event) {
6673
8317
  }
6674
8318
  return name;
6675
8319
  }
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/;
8320
+ 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/;
8321
+ var SIGNATURE_HEADER = /['"]stripe-signature['"]/i;
8322
+ function middlewareVerifies(code) {
8323
+ const masked = maskUnusedFunctions(code);
8324
+ const verifies = /\bconstructEvent\s*\(/.test(masked) || awaitedCall(masked, /\bconstructEventAsync\s*\(/);
8325
+ if (!verifies) return false;
8326
+ for (const m of masked.matchAll(/\bcatch\s*(?:\([^()]*\))?\s*\{/g)) {
8327
+ const open = m.index + m[0].length - 1;
8328
+ let close = open;
8329
+ for (let depth = 0; close < masked.length; close++) {
8330
+ if (masked[close] === "{") depth++;
8331
+ else if (masked[close] === "}" && --depth === 0) break;
8332
+ }
8333
+ const body = masked.slice(open, close);
8334
+ if (/\bnext\s*\(\s*\)/.test(body)) return false;
8335
+ }
8336
+ return true;
8337
+ }
8338
+ function verifiedByMiddleware(route, files) {
8339
+ if (!NODE_FRAMEWORKS.has(route.framework)) return null;
8340
+ const codeOf = (ref) => {
8341
+ const { inline, range } = middlewareDefinition(ref, files);
8342
+ const target = inline ?? range;
8343
+ return target === null ? null : { file: target.file, code: commentsMaskedOf(target.file).slice(target.start, target.end + 1) };
8344
+ };
8345
+ const verifies = ({ file, code }) => {
8346
+ if (middlewareVerifies(code)) return true;
8347
+ const masked = maskUnusedFunctions(code);
8348
+ const calls = [...masked.matchAll(/(?<![\w$.])([A-Za-z_$][\w$]*)\s*(?:\.\s*([A-Za-z_$][\w$]*)\s*)?\(/g)].slice(0, 16);
8349
+ return calls.some((m) => {
8350
+ const definition = calleeDefinition(file, m[1], m[2] ?? null, files);
8351
+ return definition !== null && middlewareVerifies(commentsMaskedOf(definition.file).slice(definition.start, definition.end + 1));
8352
+ });
8353
+ };
8354
+ const status = (test) => {
8355
+ const check2 = (ref) => {
8356
+ const target = codeOf(ref);
8357
+ return target !== null && test(target);
8358
+ };
8359
+ const mounts = route.mounts ?? [];
8360
+ return (route.middleware ?? []).some(check2) || mounts.length > 0 && mounts.every((refs) => refs.some(check2));
8361
+ };
8362
+ if (status(verifies)) return "verified";
8363
+ return status(({ code }) => SIGNATURE_HEADER.test(code)) ? "custom" : null;
8364
+ }
6677
8365
  var webhookRule = {
6678
8366
  id: "webhook/unverified-signature",
6679
8367
  severity: "P1",
6680
8368
  appliesTo(file) {
6681
- return /stripe/i.test(file.content) && routeOf(file) !== null;
8369
+ return /stripe/i.test(file.content);
6682
8370
  },
6683
- check(file) {
6684
- const route = routeOf(file);
6685
- if (!route) return [];
8371
+ check(file, ctx) {
8372
+ const routes = serverRoutesOf(file, ctx.files);
8373
+ if (routes.length === 0) return [];
8374
+ const routeAt = (at) => routes.find((r) => !r.reachable || r.reachable.start <= at && at <= r.reachable.end);
6686
8375
  const context = new LocalVerification(file);
6687
8376
  const { source, code } = context;
6688
8377
  const findings = [];
6689
8378
  const reported = /* @__PURE__ */ new Set();
6690
8379
  for (const event of source.matchAll(new RegExp(STRIPE_EVENT.source, "g"))) {
6691
8380
  if (code[event.index] !== source[event.index]) continue;
8381
+ const route = routeAt(event.index);
8382
+ if (!route) continue;
8383
+ const middleware = verifiedByMiddleware(route, ctx.files);
8384
+ if (middleware === "verified") continue;
6692
8385
  const owner = context.owner(event.index);
6693
8386
  const name = eventName(context, event);
6694
8387
  if (name && verifiedEvent(context, name, event.index)) continue;
@@ -6698,7 +8391,7 @@ var webhookRule = {
6698
8391
  const line = lineNumberAt(lineStartsOf(file.content), event.index);
6699
8392
  const region = code.slice(owner?.start ?? 0, owner?.end ?? code.length);
6700
8393
  const readsBody = READS_BODY.test(region);
6701
- const customCheck = /\bverifyHeader\s*\(/.test(region) || /\bcreateHmac\s*\(/.test(region) && /\btimingSafeEqual\s*\(/.test(region);
8394
+ const customCheck = middleware === "custom" || /\bverifyHeader\s*\(/.test(region) || /\bcreateHmac\s*\(/.test(region) && /\btimingSafeEqual\s*\(/.test(region);
6702
8395
  findings.push({
6703
8396
  ruleId: "webhook/unverified-signature",
6704
8397
  severity: "P1",
@@ -8247,16 +9940,16 @@ var RULE_CATALOG = [
8247
9940
  "Admin database access without a recognized auth guard",
8248
9941
  "P0",
8249
9942
  "certain",
8250
- "Server routes and Next.js Server Functions in Next.js, SvelteKit (endpoints and form actions), Nuxt, Remix/React Router, and Astro, including workspace applications.",
8251
- "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."
9943
+ "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.",
9944
+ "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."
8252
9945
  ),
8253
9946
  rule(
8254
9947
  "api/db-write-without-auth",
8255
9948
  "Database write without a recognized auth guard",
8256
9949
  "P1",
8257
9950
  "likely",
8258
- "Server routes and Next.js Server Functions in Next.js, SvelteKit (endpoints and form actions), Nuxt, Remix/React Router, and Astro.",
8259
- "Session-scoped clients and indirect guards may need manual review."
9951
+ "Server routes and Next.js Server Functions in Next.js, SvelteKit (endpoints and form actions), Nuxt, Remix/React Router, Astro, Express, Hono, and Fastify.",
9952
+ "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."
8260
9953
  ),
8261
9954
  rule(
8262
9955
  "auth/unverified-session",
@@ -8557,7 +10250,7 @@ function openReport(path, onError, options = {}) {
8557
10250
  }
8558
10251
 
8559
10252
  // src/cli.ts
8560
- var VERSION = true ? "0.6.0" : "0.0.0-dev";
10253
+ var VERSION = true ? "0.7.0" : "0.0.0-dev";
8561
10254
  function finish(code) {
8562
10255
  process.exitCode = code;
8563
10256
  }