canship 0.7.0 → 0.8.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,5 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // <define:__CANSHIP_BUILD_INFO__>
4
+ var define_CANSHIP_BUILD_INFO_default = { version: "0.8.0-rc.1", channel: "prerelease", revision: "c290998ae023d55a646c757ab181302ab73cee77", dirty: false };
5
+
3
6
  // src/index.ts
4
7
  import { statSync as statSync3 } from "fs";
5
8
  import { resolve as resolve2 } from "path";
@@ -749,7 +752,7 @@ function listViaGit(root, gitExecutable) {
749
752
  return null;
750
753
  }
751
754
  }
752
- function walkTree(root, skipped, wantAll, maxEntries) {
755
+ function walkTree(root, skipped, wantAll, maxEntries, excludePath) {
753
756
  const all = [];
754
757
  const found = [];
755
758
  let visited = 0;
@@ -793,9 +796,10 @@ function walkTree(root, skipped, wantAll, maxEntries) {
793
796
  visited++;
794
797
  const full = join2(dir, entry.name);
795
798
  const rel = relative2(root, full).split(sep2).join("/");
796
- let metadata;
799
+ if (excludePath?.(rel)) continue;
800
+ let metadata2;
797
801
  try {
798
- metadata = lstatSync2(full);
802
+ metadata2 = lstatSync2(full);
799
803
  } catch (err) {
800
804
  if (!isMissing(err)) skipped.push({
801
805
  path: rel,
@@ -804,18 +808,18 @@ function walkTree(root, skipped, wantAll, maxEntries) {
804
808
  });
805
809
  continue;
806
810
  }
807
- if (metadata.isSymbolicLink()) {
811
+ if (metadata2.isSymbolicLink()) {
808
812
  if (!SKIP_DIRS.has(entry.name)) {
809
813
  skipped.push({ path: rel, reason: "symlink", detail: "symbolic links are not followed" });
810
814
  }
811
815
  continue;
812
816
  }
813
- if (metadata.isDirectory()) {
817
+ if (metadata2.isDirectory()) {
814
818
  if (SKIP_DIRS.has(entry.name)) continue;
815
819
  walk(full, depth + 1);
816
820
  continue;
817
821
  }
818
- if (!metadata.isFile()) continue;
822
+ if (!metadata2.isFile()) continue;
819
823
  if (wantAll) all.push(rel);
820
824
  if (isCredentialName(entry.name) || isWorthProbing(rel)) found.push(rel);
821
825
  }
@@ -902,16 +906,17 @@ function readablePath(root, path, skipped) {
902
906
  }
903
907
  return true;
904
908
  }
905
- function collectFiles(root, isGitRepo, gitExecutable = resolveGitExecutable(root), limits = {}, honorIgnoreMarkers = true) {
909
+ function collectFiles(root, isGitRepo, gitExecutable = resolveGitExecutable(root), limits = {}, honorIgnoreMarkers = true, excludePath) {
906
910
  root = realpathSync2(root);
907
911
  const skipped = [];
908
912
  const ignored = [];
909
913
  const fromGit = isGitRepo ? listViaGit(root, gitExecutable) : null;
910
- const walked = walkTree(root, skipped, fromGit === null, limits.maxEntries ?? MAX_WALK_ENTRIES);
914
+ const walked = walkTree(root, skipped, fromGit === null, limits.maxEntries ?? MAX_WALK_ENTRIES, excludePath);
911
915
  const listed = fromGit?.files ?? walked.all;
912
916
  const candidates = /* @__PURE__ */ new Set();
913
917
  let vendored = 0;
914
918
  for (const path of fromGit?.nestedRepositories ?? []) {
919
+ if (excludePath?.(path)) continue;
915
920
  if (isVendored(path)) {
916
921
  vendored++;
917
922
  continue;
@@ -923,11 +928,13 @@ function collectFiles(root, isGitRepo, gitExecutable = resolveGitExecutable(root
923
928
  });
924
929
  }
925
930
  for (const path of listed) {
931
+ if (excludePath?.(path)) continue;
926
932
  if (isVendored(path)) vendored++;
927
933
  else candidates.add(path);
928
934
  }
929
935
  const forced = /* @__PURE__ */ new Set();
930
936
  for (const hidden of walked.forced) {
937
+ if (excludePath?.(hidden)) continue;
931
938
  candidates.add(hidden);
932
939
  forced.add(hidden);
933
940
  }
@@ -1870,7 +1877,7 @@ function gitOrThrow(root, gitExecutable, args) {
1870
1877
  return out;
1871
1878
  }
1872
1879
  function repoPrefix(root, gitExecutable) {
1873
- return (git(root, gitExecutable, ["rev-parse", "--show-prefix"]) ?? "").trim();
1880
+ return gitOrThrow(root, gitExecutable, ["rev-parse", "--show-prefix"]).trim();
1874
1881
  }
1875
1882
  function trackedEnvFiles(root, gitExecutable) {
1876
1883
  const out = gitOrThrow(root, gitExecutable, ["ls-files", "-z"]);
@@ -1979,6 +1986,7 @@ var gitleakRule = {
1979
1986
  const remoteNote = remote ? `This repository has a remote configured, so these commits have most likely been pushed. Bots scrape public commits within minutes \u2014 assume every key in this file is already in someone else's hands.` : `This repository has no remote yet, so the damage may still be contained. Fix it before you push.`;
1980
1987
  const reportedTracked = /* @__PURE__ */ new Set();
1981
1988
  for (const path of tracked) {
1989
+ if (ctx.excludePath?.(path)) continue;
1982
1990
  const scanned = ctx.files.find((f) => f.path === path);
1983
1991
  const evidence = scanned ? evidenceIn(scanned.lines) : "hint";
1984
1992
  if (evidence === "none") continue;
@@ -2009,6 +2017,7 @@ var gitleakRule = {
2009
2017
  for (const entry of historical) {
2010
2018
  if (reportedTracked.has(entry.localPath)) continue;
2011
2019
  const path = entry.localPath;
2020
+ if (ctx.excludePath?.(path)) continue;
2012
2021
  const stillTracked = tracked.has(entry.localPath);
2013
2022
  const history = historicalEvidence(ctx.root, ctx.gitExecutable, entry);
2014
2023
  if (history && history.unread > 0) {
@@ -3092,7 +3101,7 @@ var firebaseRulesRule = {
3092
3101
  };
3093
3102
 
3094
3103
  // src/rules/apiauth.ts
3095
- import { posix } from "path";
3104
+ import { posix as posix2 } from "path";
3096
3105
 
3097
3106
  // src/rules/bindings.ts
3098
3107
  var cache = /* @__PURE__ */ new WeakMap();
@@ -3180,9 +3189,32 @@ function synchronousLocal(file, expr) {
3180
3189
  const code = noiseMaskedOf(file);
3181
3190
  const escaped = name.replace(/\$/g, "\\$");
3182
3191
  const declared = new RegExp(`(?:^|[^\\w$])(async\\s+)?function\\s*\\*?\\s*${escaped}\\s*\\(`).exec(code);
3183
- if (declared) return !declared[1];
3192
+ if (declared?.[1]) return false;
3184
3193
  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];
3194
+ if (!declared && (!assigned || assigned[1])) return false;
3195
+ const declaration = declared ?? assigned;
3196
+ const pairedEnd = (open, left, right) => {
3197
+ let depth = 0;
3198
+ for (let i = open; i < Math.min(code.length, open + 65536); i++) {
3199
+ if (code[i] === left) depth++;
3200
+ else if (code[i] === right && --depth === 0) return i;
3201
+ }
3202
+ return -1;
3203
+ };
3204
+ let start = declaration.index + declaration[0].length;
3205
+ if (declared) start = pairedEnd(start - 1, "(", ")") + 1;
3206
+ else if (/function\b$/.test(declaration[0])) {
3207
+ const open = code.indexOf("(", start);
3208
+ start = open < 0 ? 0 : pairedEnd(open, "(", ")") + 1;
3209
+ }
3210
+ if (start === 0) return false;
3211
+ start += /^\s*(?::[\w\s.<>,[\]|?]+)?\s*/.exec(code.slice(start))[0].length;
3212
+ const close = code[start] === "{" ? pairedEnd(start, "{", "}") : -1;
3213
+ if (code[start] === "{" && close < 0) return false;
3214
+ const body = close >= 0 ? code.slice(start, close + 1) : `return ${code.slice(start).split(/[;\n]/)[0] ?? ""}`;
3215
+ if (/\b(?:Promise|then|await|new|function|async)\b|=>|\.\.\./.test(body)) return false;
3216
+ if ([...body.matchAll(/([\w$]+)\s*\(/g)].some((m) => !/^(?:if|switch)$/.test(m[1]))) return false;
3217
+ return /\breturn\s+(?:null|false|true|\{|[\w$]+\s*\?)/.test(body);
3186
3218
  }
3187
3219
  var limited = /* @__PURE__ */ new WeakSet();
3188
3220
  var cache2 = /* @__PURE__ */ new WeakMap();
@@ -3441,12 +3473,364 @@ function identityRequirement(value, allowParameters) {
3441
3473
  return null;
3442
3474
  }
3443
3475
 
3476
+ // src/rules/routers.ts
3477
+ import { createHash as createHash2 } from "crypto";
3478
+
3479
+ // src/rules/factory-modules.ts
3480
+ import { posix } from "path";
3481
+ var unknown = (limited2 = false) => ({ handled: true, file: null, ...limited2 ? { limited: true } : {} });
3482
+ var object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
3483
+ var safePath = (path) => Boolean(path) && path.length <= 512 && !/[\\\u0000-\u0020:%?#]/.test(path) && !posix.isAbsolute(path);
3484
+ var contained = (path) => path !== ".." && !path.startsWith("../") && !posix.isAbsolute(path);
3485
+ function metadata(file) {
3486
+ if (file.content.length > 65536) return null;
3487
+ let source = commentsMaskedOf(file);
3488
+ const code = noiseMaskedOf(file);
3489
+ const commas = [...code.matchAll(/,\s*[}\]]/g)].map((match) => match.index);
3490
+ if (commas.length) {
3491
+ const characters = source.split("");
3492
+ for (const at of commas) characters[at] = " ";
3493
+ source = characters.join("");
3494
+ }
3495
+ try {
3496
+ const value = JSON.parse(source);
3497
+ return object(value) ? value : null;
3498
+ } catch {
3499
+ return null;
3500
+ }
3501
+ }
3502
+ function exportTarget(value, limit, depth = 0) {
3503
+ if (depth >= 8) {
3504
+ limit();
3505
+ return null;
3506
+ }
3507
+ if (typeof value === "string") return value.startsWith("./") && safePath(value) && !value.includes("*") && value.split("/").slice(1).every((part) => part !== "." && part !== ".." && part !== "node_modules") ? value : null;
3508
+ if (!object(value)) return null;
3509
+ if (Object.keys(value).length > 32) {
3510
+ limit();
3511
+ return null;
3512
+ }
3513
+ const entries = Object.entries(value).filter(([key]) => key !== "types" && !key.startsWith("types@"));
3514
+ if (!entries.length || entries.some(([key]) => key.startsWith("."))) return null;
3515
+ const targets = entries.map(([, item]) => exportTarget(item, limit, depth + 1));
3516
+ return targets.every((target) => target !== null && target === targets[0]) ? targets[0] : null;
3517
+ }
3518
+ function factoryModuleResolver(files) {
3519
+ const byPath = new Map(files.map((file) => [file.path, file]));
3520
+ const parsed = /* @__PURE__ */ new Map();
3521
+ let metadataLimited = false;
3522
+ const json = (file) => {
3523
+ if (file.content.length > 65536) {
3524
+ metadataLimited = true;
3525
+ return null;
3526
+ }
3527
+ if (!parsed.has(file)) parsed.set(file, metadata(file));
3528
+ return parsed.get(file);
3529
+ };
3530
+ const parents = (file) => {
3531
+ const found = [];
3532
+ for (let dir = posix.dirname(file.path); ; dir = posix.dirname(dir)) {
3533
+ found.push(dir);
3534
+ if (dir === "." || posix.dirname(dir) === dir) break;
3535
+ }
3536
+ return found;
3537
+ };
3538
+ const nearest = (file, names) => {
3539
+ for (const dir of parents(file)) for (const name of names) {
3540
+ const found = byPath.get(posix.join(dir, name));
3541
+ if (found) return found;
3542
+ }
3543
+ return void 0;
3544
+ };
3545
+ const sourceAt = (path, from) => {
3546
+ if (!contained(path)) return null;
3547
+ const relative3 = posix.relative(posix.dirname(from.path), path);
3548
+ return bindingModule(relative3.startsWith(".") ? relative3 : "./" + relative3, from, files, "");
3549
+ };
3550
+ const configPath = (spec, from) => {
3551
+ const config = nearest(from, ["tsconfig.json", "jsconfig.json"]);
3552
+ if (!config) return null;
3553
+ if (config.content.length > 65536) return unknown(true);
3554
+ const data = json(config);
3555
+ if (!data || "extends" in data || "references" in data) return unknown();
3556
+ const options = data.compilerOptions;
3557
+ if (options === void 0) return null;
3558
+ if (!object(options)) return unknown();
3559
+ if (options.paths === void 0) return null;
3560
+ if (!object(options.paths)) return unknown();
3561
+ if (Object.keys(options.paths).length > 128) return unknown(true);
3562
+ const matches = [];
3563
+ for (const [pattern, target2] of Object.entries(options.paths)) {
3564
+ if (pattern.length > 512) return unknown(true);
3565
+ if (pattern.split("*").length > 2) return unknown();
3566
+ const [prefix, suffix = ""] = pattern.split("*");
3567
+ const exact = !pattern.includes("*");
3568
+ if (exact ? pattern !== spec : !spec.startsWith(prefix) || !spec.endsWith(suffix) || spec.length < prefix.length + suffix.length) continue;
3569
+ matches.push({ target: target2, prefix, suffix, exact, middle: exact ? "" : spec.slice(prefix.length, spec.length - suffix.length) });
3570
+ }
3571
+ if (!matches.length) return null;
3572
+ matches.sort((a, b) => Number(b.exact) - Number(a.exact) || b.prefix.length - a.prefix.length);
3573
+ const selected = matches[0];
3574
+ if (matches[1] && matches[1].exact === selected.exact && matches[1].prefix.length === selected.prefix.length) return unknown();
3575
+ if (!Array.isArray(selected.target) || selected.target.length !== 1 || typeof selected.target[0] !== "string") return unknown();
3576
+ const target = selected.target[0];
3577
+ if (!safePath(target) || target.split("*").length > 2 || selected.exact && target.includes("*")) return unknown();
3578
+ const base = options.baseUrl ?? ".";
3579
+ if (typeof base !== "string" || !safePath(base) || base.includes("*")) return unknown();
3580
+ const path = posix.normalize(posix.join(posix.dirname(config.path), base, target.replace("*", selected.middle)));
3581
+ return { handled: true, file: sourceAt(path, from) };
3582
+ };
3583
+ const manifests = files.filter((file) => /(?:^|\/)package\.json$/.test(file.path));
3584
+ const workspace = (spec, from) => {
3585
+ const name = spec.startsWith("@") ? spec.split("/").slice(0, 2).join("/") : spec.split("/")[0];
3586
+ const subpath = spec === name ? "." : "." + spec.slice(name.length);
3587
+ const owner = nearest(from, ["package.json"]);
3588
+ if (!owner) return null;
3589
+ if (owner.content.length > 65536) return unknown(true);
3590
+ const ownerData = json(owner);
3591
+ const deps = ownerData?.dependencies;
3592
+ const link = object(deps) ? deps[name] : void 0;
3593
+ if (typeof link !== "string" || !/^workspace:(?:\*|\^|~)$/.test(link)) return null;
3594
+ const roots = parents(owner).map((dir2) => byPath.get(posix.join(dir2, "package.json"))).filter((file) => Boolean(file));
3595
+ if (roots.some((file) => file.content.length > 65536)) return unknown(true);
3596
+ const root = roots.find((file) => json(file)?.workspaces !== void 0);
3597
+ if (!root) return unknown();
3598
+ const raw = json(root).workspaces;
3599
+ const patterns = Array.isArray(raw) ? raw : object(raw) ? raw.packages : null;
3600
+ if (!Array.isArray(patterns)) return unknown();
3601
+ if (patterns.length > 64) return unknown(true);
3602
+ if (patterns.some((item) => typeof item !== "string" || !safePath(item) || item.split("*").length > 2)) return unknown();
3603
+ const rootDir = posix.dirname(root.path);
3604
+ const matched = manifests.filter((file) => {
3605
+ const dir2 = posix.relative(rootDir, posix.dirname(file.path));
3606
+ return Boolean(dir2) && contained(dir2) && patterns.some((pattern) => {
3607
+ const [before, after = ""] = pattern.replace(/\/$/, "").split("*");
3608
+ return pattern.includes("*") ? dir2.length >= before.length + after.length && dir2.startsWith(before) && dir2.endsWith(after) && !dir2.slice(before.length, dir2.length - after.length).includes("/") : dir2 === before;
3609
+ }) && json(file)?.name === name;
3610
+ });
3611
+ if (matched.length !== 1) return unknown();
3612
+ const manifest = matched[0], data = json(manifest);
3613
+ let target;
3614
+ if ("exports" in data) {
3615
+ const value = data.exports;
3616
+ if (object(value) && Object.keys(value).some((key) => key.startsWith(".")) && Object.keys(value).some((key) => !key.startsWith("."))) return unknown();
3617
+ const entry = object(value) && Object.keys(value).some((key) => key.startsWith(".")) ? value[subpath] : subpath === "." ? value : null;
3618
+ let limited2 = false;
3619
+ target = exportTarget(entry, () => {
3620
+ limited2 = true;
3621
+ });
3622
+ if (limited2) return unknown(true);
3623
+ } else target = subpath === "." && typeof data.main === "string" && (!data.module || data.module === data.main) ? data.main : null;
3624
+ if (!target || !safePath(target)) return unknown();
3625
+ const dir = posix.dirname(manifest.path), path = posix.normalize(posix.join(dir, target));
3626
+ if (!contained(posix.relative(dir, path))) return unknown();
3627
+ return { handled: true, file: sourceAt(path, from) };
3628
+ };
3629
+ const resolve3 = (spec, from) => {
3630
+ if (!spec || spec.length > 512 || /[\\\u0000-\u0020:%?]/.test(spec) || posix.isAbsolute(spec)) return unknown();
3631
+ if (spec.startsWith(".")) return { handled: true, file: bindingModule(spec, from, files, "") };
3632
+ const configured = configPath(spec, from);
3633
+ if (configured) return configured;
3634
+ const local = workspace(spec, from);
3635
+ if (local) return local;
3636
+ const scope = /^(.*?\/)?(?:src|app|lib|server|routes|api|controllers|plugins)\//.exec(from.path)?.[1] ?? "";
3637
+ const file = bindingModule(spec, from, files, scope);
3638
+ return { handled: Boolean(file), file };
3639
+ };
3640
+ return (spec, from) => {
3641
+ metadataLimited = false;
3642
+ const result = resolve3(spec, from);
3643
+ if (result.file && (!/\.[mc]?[jt]sx?$/.test(result.file.path) || /\.d\.[cm]?ts$/.test(result.file.path))) return unknown();
3644
+ return metadataLimited ? unknown(true) : result;
3645
+ };
3646
+ }
3647
+
3648
+ // src/rules/registration-context.ts
3649
+ function registrationContext(code, pairs, bodies) {
3650
+ const regions = [];
3651
+ const openers = new Map([...pairs].map(([open, close]) => [close, open]));
3652
+ const controlBraces = /* @__PURE__ */ new Set();
3653
+ let limited2 = false;
3654
+ const space = (at) => {
3655
+ while (at < code.length && /\s/.test(code[at])) at++;
3656
+ return at;
3657
+ };
3658
+ const add = (start, end, dead = false, owner2 = false) => {
3659
+ if (regions.some((region) => region.start === start && region.end === end && region.dead === dead && region.owner === owner2)) return;
3660
+ if (regions.length >= 512) {
3661
+ limited2 = true;
3662
+ return;
3663
+ }
3664
+ regions.push({ start, end, dead, owner: owner2 });
3665
+ };
3666
+ const literal2 = (text) => {
3667
+ const match = /^\s*(!\s*)?(true|false)\s*$/.exec(text);
3668
+ return match ? match[2] === "true" !== Boolean(match[1]) : null;
3669
+ };
3670
+ const simpleEnd = (start) => {
3671
+ for (let at = start; at < code.length; at++) {
3672
+ const quote = code[at];
3673
+ if (quote === '"' || quote === "'" || quote === "`") {
3674
+ let end2 = at + 1;
3675
+ for (; end2 < code.length; end2++) {
3676
+ if (quote === "`" && code[end2] === "$" && code[end2 + 1] === "{") {
3677
+ const close = pairs.get(end2 + 1);
3678
+ if (close !== void 0) {
3679
+ end2 = close;
3680
+ continue;
3681
+ }
3682
+ }
3683
+ if (code[end2] === quote) break;
3684
+ }
3685
+ if (end2 === code.length) limited2 = true;
3686
+ at = end2;
3687
+ continue;
3688
+ }
3689
+ const end = pairs.get(at);
3690
+ if (end !== void 0) {
3691
+ at = end;
3692
+ continue;
3693
+ }
3694
+ if (/[;,}\n]/.test(code[at])) return at + (code[at] === ";" ? 1 : 0);
3695
+ }
3696
+ return code.length;
3697
+ };
3698
+ const statementEnd3 = (rawStart, depth = 0) => {
3699
+ const start = space(rawStart);
3700
+ if (depth >= 32) {
3701
+ limited2 = true;
3702
+ return code.length;
3703
+ }
3704
+ if (code[start] === "{") {
3705
+ const end = pairs.get(start);
3706
+ if (end === void 0) limited2 = true;
3707
+ return (end ?? code.length - 1) + 1;
3708
+ }
3709
+ const head = /^(if|for(?:\s+await)?|while|switch|with)\s*\(/.exec(code.slice(start, start + 100));
3710
+ if (head) {
3711
+ const close = pairs.get(start + head[0].length - 1);
3712
+ if (close === void 0) {
3713
+ limited2 = true;
3714
+ return code.length;
3715
+ }
3716
+ let end = statementEnd3(close + 1, depth + 1);
3717
+ const tail = space(end);
3718
+ if (head[1] === "if" && /^else\b/.test(code.slice(tail, tail + 8))) end = statementEnd3(tail + 4, depth + 1);
3719
+ return end;
3720
+ }
3721
+ return simpleEnd(start);
3722
+ };
3723
+ for (const body of bodies) add(body.start, body.end + 1, false, true);
3724
+ for (const match of code.matchAll(/=>\s*(?!\s*\{)/g)) {
3725
+ const start = space(match.index + 2);
3726
+ if (code[start] !== "{") add(start, simpleEnd(start), false, true);
3727
+ }
3728
+ for (const match of code.matchAll(/(?<![\w$.])(if|for(?:\s+await)?|while|switch|catch|with)\s*\(/g)) {
3729
+ const open = match.index + match[0].length - 1, close = pairs.get(open);
3730
+ if (close === void 0) {
3731
+ limited2 = true;
3732
+ continue;
3733
+ }
3734
+ const start = space(close + 1), end = statementEnd3(start);
3735
+ if (code[start] === "{") controlBraces.add(start);
3736
+ const condition = literal2(code.slice(open + 1, close));
3737
+ if (match[1] === "if") {
3738
+ if (condition !== true) add(start, end, condition === false);
3739
+ const next = space(end);
3740
+ if (/^else\b/.test(code.slice(next, next + 8))) {
3741
+ const alternate = space(next + 4), alternateEnd = statementEnd3(alternate);
3742
+ if (code[alternate] === "{") controlBraces.add(alternate);
3743
+ if (condition !== false) add(alternate, alternateEnd, condition === true);
3744
+ }
3745
+ } else {
3746
+ add(start, end, match[1] === "while" && condition === false);
3747
+ if (match[1] === "switch" && code[start] === "{") {
3748
+ const labels = [];
3749
+ for (let at = start + 1; at < end - 1; at++) {
3750
+ const nested = pairs.get(at);
3751
+ if (nested !== void 0) {
3752
+ at = nested;
3753
+ continue;
3754
+ }
3755
+ if ((at === 0 || !/[\w$.]/.test(code[at - 1])) && /^(?:case|default)\b/.test(code.slice(at, at + 10))) labels.push(at);
3756
+ }
3757
+ labels.forEach((at, index) => add(at, labels[index + 1] ?? end - 1));
3758
+ }
3759
+ }
3760
+ }
3761
+ for (const match of code.matchAll(/(?<![\w$.])(?:try|finally|do|catch)\s*\{/g)) {
3762
+ const start = match.index + match[0].length - 1;
3763
+ controlBraces.add(start);
3764
+ add(start, (pairs.get(start) ?? code.length - 1) + 1);
3765
+ }
3766
+ for (const [start, end] of pairs) {
3767
+ if (code[start] !== "{" || controlBraces.has(start) || bodies.some((body) => body.start === start)) continue;
3768
+ if (/\)\s*(?::[\w\s.<>,[\]|?]+)?$/.test(code.slice(Math.max(0, start - 200), start))) add(start, end + 1, false, true);
3769
+ }
3770
+ const contains = (region, at) => region.start <= at && at < region.end;
3771
+ const owner = (at) => regions.filter((region) => region.owner && contains(region, at)).sort((a, b) => a.end - a.start - (b.end - b.start))[0];
3772
+ const inline = (at) => {
3773
+ let start = at;
3774
+ for (let cursor = at - 1; cursor >= 0; cursor--) {
3775
+ if (at - cursor > 4e3) {
3776
+ limited2 = true;
3777
+ return "conditional";
3778
+ }
3779
+ const open = openers.get(cursor);
3780
+ if (open !== void 0) {
3781
+ cursor = open;
3782
+ start = open;
3783
+ continue;
3784
+ }
3785
+ if (code[cursor] === ";" || code[cursor] === "{") {
3786
+ start = cursor + 1;
3787
+ break;
3788
+ }
3789
+ if (code[cursor] === "\n" && !/[&|?:=,(.\\]$/.test(code.slice(Math.max(0, cursor - 100), cursor).trimEnd())) {
3790
+ start = cursor + 1;
3791
+ break;
3792
+ }
3793
+ start = cursor;
3794
+ }
3795
+ let prefix = "";
3796
+ for (let cursor = start; cursor < at; cursor++) {
3797
+ const close = pairs.get(cursor);
3798
+ if (close !== void 0 && close < at) {
3799
+ prefix += " ";
3800
+ cursor = close;
3801
+ continue;
3802
+ }
3803
+ prefix += code[cursor];
3804
+ }
3805
+ prefix = prefix.trim();
3806
+ const simple = /^(?:const\s+[\w$]+\s*=\s*)?(!?\s*(?:true|false))\s*(&&|\|\|)\s*$/.exec(prefix);
3807
+ if (simple) {
3808
+ const truth = literal2(simple[1]);
3809
+ return (simple[2] === "&&" ? truth : !truth) ? "always" : "dead";
3810
+ }
3811
+ return /&&|\|\||\?/.test(prefix) ? "conditional" : "always";
3812
+ };
3813
+ const reachable = (at) => limited2 || !regions.some((region) => region.dead && contains(region, at)) && inline(at) !== "dead";
3814
+ return {
3815
+ get limited() {
3816
+ return limited2;
3817
+ },
3818
+ reachable,
3819
+ covers(guard, operation) {
3820
+ if (limited2 || !reachable(guard) || inline(guard) !== "always") return false;
3821
+ const guardOwner = owner(guard);
3822
+ if (guardOwner && guardOwner !== owner(operation)) return false;
3823
+ return regions.filter((region) => !region.dead && contains(region, guard)).every((region) => contains(region, operation));
3824
+ }
3825
+ };
3826
+ }
3827
+
3444
3828
  // src/rules/routers.ts
3445
3829
  var NODE_FRAMEWORKS = /* @__PURE__ */ new Set(["express", "hono", "fastify"]);
3446
3830
  var METHODS = "get|post|put|patch|delete|all|options|head";
3447
3831
  var GENERICS = String.raw`(?:<[^()]{0,300}>\s*)?`;
3448
3832
  var FASTIFY_HOOKS = /^(?:onRequest|preParsing|preValidation|preHandler)$/;
3449
- var HONO_CALL = `${METHODS}|on|use|route`;
3833
+ var HONO_CALL = `${METHODS}|on|use|route|openapiRoutes|openapi`;
3450
3834
  var analysedCache = /* @__PURE__ */ new WeakMap();
3451
3835
  function analyse(file) {
3452
3836
  const cached = analysedCache.get(file);
@@ -3485,8 +3869,9 @@ function argsOf(a, open, close) {
3485
3869
  function flatten(a, args) {
3486
3870
  return args.flatMap((arg) => {
3487
3871
  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);
3872
+ const owner = arg.file ? analyse(arg.file) : a;
3873
+ const close = owner.pairs.get(arg.at);
3874
+ return close === void 0 ? [arg] : argsOf(owner, arg.at, close).map((item) => ({ ...item, file: owner.file }));
3490
3875
  });
3491
3876
  }
3492
3877
  function scopeOf(path) {
@@ -3519,7 +3904,7 @@ function entriesOf(a, arg) {
3519
3904
  return entries;
3520
3905
  }
3521
3906
  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() };
3907
+ const names = { express: /* @__PURE__ */ new Set(), router: /* @__PURE__ */ new Set(), hono: /* @__PURE__ */ new Set(), openapi: /* @__PURE__ */ new Set(), fastify: /* @__PURE__ */ new Set(), types: /* @__PURE__ */ new Set() };
3523
3908
  const named = (list, wanted, into, fallback) => {
3524
3909
  for (const part of list.split(",")) {
3525
3910
  const m = /^\s*(?:type\s+)?([A-Za-z_$][\w$]*)\s*(?:(?:as|:)\s*([A-Za-z_$][\w$]*))?\s*$/.exec(part);
@@ -3529,6 +3914,7 @@ function frameworkNamesOf(a) {
3529
3914
  }
3530
3915
  };
3531
3916
  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)) {
3917
+ if (a.code.slice(m.index, m.index + 6) !== "import") continue;
3532
3918
  const typeOnly = Boolean(m[1]);
3533
3919
  if (m[5] === "express") {
3534
3920
  if (!typeOnly && m[2]) names.express.add(m[2]);
@@ -3541,20 +3927,27 @@ function frameworkNamesOf(a) {
3541
3927
  names.types.add("fastify");
3542
3928
  } else {
3543
3929
  if (!typeOnly && m[3]) named(m[3], /^(?:Hono|OpenAPIHono)$/, names.hono);
3930
+ if (!typeOnly && m[3] && m[5] === "@hono/zod-openapi") named(m[3], /^OpenAPIHono$/, names.openapi);
3544
3931
  names.types.add("hono");
3545
3932
  }
3546
3933
  }
3547
3934
  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)) {
3935
+ if (!/^(?:const|let|var)\b/.test(a.code.slice(m.index))) continue;
3548
3936
  if (m[3] === "express") {
3549
3937
  if (m[1]) names.express.add(m[1]);
3550
3938
  if (m[2]) named(m[2], /^Router$/, names.router);
3551
3939
  } else if (m[3] === "fastify") {
3552
3940
  if (m[1]) names.fastify.add(m[1]);
3553
3941
  if (m[2]) named(m[2], /^fastify$/, names.fastify);
3554
- } else if (m[2]) named(m[2], /^(?:Hono|OpenAPIHono)$/, names.hono);
3942
+ } else if (m[2]) {
3943
+ named(m[2], /^(?:Hono|OpenAPIHono)$/, names.hono);
3944
+ if (m[3] === "@hono/zod-openapi") named(m[2], /^OpenAPIHono$/, names.openapi);
3945
+ }
3555
3946
  names.types.add(m[3] === "express" || m[3] === "fastify" ? m[3] : "hono");
3556
3947
  }
3557
- for (const m of a.source.matchAll(/\brequire\s*\(\s*['"](express|fastify)['"]\s*\)/g)) names.types.add(m[1]);
3948
+ for (const m of a.source.matchAll(/\brequire\s*\(\s*['"](express|fastify)['"]\s*\)/g)) {
3949
+ if (a.code.slice(m.index, m.index + 7) === "require") names.types.add(m[1]);
3950
+ }
3558
3951
  return names.types.size ? names : null;
3559
3952
  }
3560
3953
  function instancesOf(a, names) {
@@ -3563,6 +3956,7 @@ function instancesOf(a, names) {
3563
3956
  if (!found.some((other) => other.name === instance.name)) found.push(instance);
3564
3957
  };
3565
3958
  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)) {
3959
+ if (!/^(?:const|let|var)\b/.test(a.code.slice(m.index))) continue;
3566
3960
  const [, name, isNew, callee, router] = m;
3567
3961
  if (!isNew && (names.express.has(callee) && (router === "Router" || router === void 0) || names.router.has(callee) && router === void 0)) {
3568
3962
  add({ name, framework: "express", prefix: "", weak: false });
@@ -3579,12 +3973,13 @@ function instancesOf(a, names) {
3579
3973
  prefix = (arg && pathOf(a, arg)) ?? "";
3580
3974
  if (baseClose !== void 0) close = baseClose;
3581
3975
  }
3582
- add({ name, framework: "hono", prefix, weak: false, chainFrom: close });
3976
+ add({ name, framework: "hono", prefix, weak: false, chainFrom: close, openapi: names.openapi.has(callee) });
3583
3977
  } else if (!isNew && names.fastify.has(callee) && router === void 0) {
3584
3978
  add({ name, framework: "fastify", prefix: "", weak: false });
3585
3979
  }
3586
3980
  }
3587
3981
  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)) {
3982
+ if (!/^(?:const|let|var)\b/.test(a.code.slice(m.index))) continue;
3588
3983
  if (m[2] === "express" || !m[3]) add({ name: m[1], framework: m[2], prefix: "", weak: false });
3589
3984
  }
3590
3985
  const typed = [
@@ -3595,11 +3990,180 @@ function instancesOf(a, names) {
3595
3990
  for (const [framework, type] of typed) {
3596
3991
  if (!names.types.has(framework)) continue;
3597
3992
  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 });
3993
+ add({ name: m[1], framework, prefix: "", weak: true, openapi: framework === "hono" && /\bOpenAPIHono\b/.test(m[0]) });
3599
3994
  }
3600
3995
  }
3601
3996
  return found;
3602
3997
  }
3998
+ function factoryBindingClean(a, name, initializer = -1) {
3999
+ const escaped = name.replace(/\$/g, "\\$");
4000
+ if ([...a.code.matchAll(new RegExp(`\\b(?:const|let|var|function|class)\\s+${escaped}(?![\\w$])`, "g"))].length > 1) return false;
4001
+ if (initializer < 0) {
4002
+ const declaration = new RegExp(`\\bconst\\s+(${escaped})\\s*(?::[^=;]{0,200})?=`).exec(a.code);
4003
+ if (declaration) initializer = declaration.index + /^const\s+/.exec(declaration[0])[0].length;
4004
+ }
4005
+ if (new RegExp(`[(,]\\s*${escaped}\\s*(?=[:,)=])|(?<![\\w$.])${escaped}\\s*=>`).test(a.code)) return false;
4006
+ for (const match of a.code.matchAll(/\b(?:const|let|var)\s*([\[{])/g)) {
4007
+ const open = match.index + match[0].length - 1, close = a.pairs.get(open);
4008
+ if (close !== void 0 && new RegExp(`(?<![\\w$])${escaped}(?![\\w$])`).test(a.code.slice(open, close + 1))) return false;
4009
+ }
4010
+ const writes = new RegExp(`(?<![\\w$.])${escaped}\\s*(?:=(?![=>])|[+*/%&|^-]=|\\?\\?=|&&=|\\|\\|=|\\+\\+|--)`, "g");
4011
+ for (const match of a.code.matchAll(writes)) if (match.index !== initializer) return false;
4012
+ return !new RegExp(`(?<![\\w$.])${escaped}(?:\\s*\\.\\s*[\\w$]+|\\s*\\[[^\\]]{0,200}\\])+\\s*=(?!=)|\\bObject\\s*\\.\\s*(?:assign|defineProperty|defineProperties)\\s*\\(\\s*${escaped}(?![\\w$])`).test(a.code);
4013
+ }
4014
+ function projectFactoryResolver(files, gaps) {
4015
+ const cache3 = /* @__PURE__ */ new Map();
4016
+ const module = factoryModuleResolver(files);
4017
+ let limited2 = false;
4018
+ const topLevel = (a, at) => ![...a.pairs].some(([open, close]) => a.code[open] === "{" && open < at && at < close);
4019
+ const argAt = (a, at, end) => {
4020
+ const lead = a.code.slice(at, end).length - a.code.slice(at, end).trimStart().length;
4021
+ at += lead;
4022
+ const text = a.code.slice(at, end).trimEnd();
4023
+ return { at, text, source: a.source.slice(at, at + text.length) };
4024
+ };
4025
+ const basePath = (a, close, shape) => {
4026
+ for (let i = 0; i < 8; i++) {
4027
+ const next = /^\s*\.\s*basePath\s*\(/.exec(a.code.slice(close + 1, close + 200));
4028
+ if (!next) return { shape, close };
4029
+ if (shape.framework !== "hono") return null;
4030
+ const open = close + next[0].length, end = a.pairs.get(open);
4031
+ const args = end === void 0 ? [] : argsOf(a, open, end);
4032
+ const path = args.length === 1 ? literalOf(args[0]) : null;
4033
+ if (end === void 0 || path === null) return null;
4034
+ shape = { ...shape, prefix: joinPath(shape.prefix, path) };
4035
+ close = end;
4036
+ }
4037
+ if (/^\s*\.\s*basePath\s*\(/.test(a.code.slice(close + 1, close + 200))) {
4038
+ limited2 = true;
4039
+ return null;
4040
+ }
4041
+ return { shape, close };
4042
+ };
4043
+ const expression = (a, arg, depth) => {
4044
+ if (depth >= 8 || arg.text.length > 4e3) {
4045
+ limited2 = true;
4046
+ return null;
4047
+ }
4048
+ if (arg.text[0] === "(" && a.pairs.get(arg.at) === arg.at + arg.text.length - 1) return expression(a, argAt(a, arg.at + 1, arg.at + arg.text.length - 1), depth + 1);
4049
+ const call = /^(new\s+)?([A-Za-z_$][\w$]*)(?:\s*\.\s*(Router))?\s*(?:<[^()]{0,300}>\s*)?\(/.exec(arg.text);
4050
+ if (!call) return null;
4051
+ const [, isNew, name, member] = call;
4052
+ const open = arg.at + call[0].length - 1, close = a.pairs.get(open);
4053
+ if (close === void 0 || close >= arg.at + arg.text.length || !factoryBindingClean(a, name)) return null;
4054
+ const imports = bindingsOf(a.file).imports.filter((item) => item.local === name);
4055
+ const imported = imports.length === 1 ? imports[0] : void 0;
4056
+ let shape = null;
4057
+ const origin = imported?.spec ? module(imported.spec, a.file) : null;
4058
+ if (origin?.limited) limited2 = true;
4059
+ if (imported?.spec && !origin?.handled && !new RegExp(`\\b(?:const|let|var|function|class)\\s+${name.replace(/\$/g, "\\$")}(?![\\w$])`).test(a.code)) {
4060
+ if (isNew && !member && ["hono", "hono/tiny", "hono/quick"].includes(imported.spec ?? "") && imported.imported === "Hono") shape = { framework: "hono", prefix: "", openapi: false };
4061
+ if (isNew && !member && imported.spec === "@hono/zod-openapi" && imported.imported === "OpenAPIHono") shape = { framework: "hono", prefix: "", openapi: true };
4062
+ if (!isNew && imported.spec === "express" && (imported.imported === "default" && (!member || member === "Router") || imported.imported === "Router" && !member)) shape = { framework: "express", prefix: "", openapi: false };
4063
+ if (!isNew && !member && imported.spec === "fastify" && ["default", "fastify"].includes(imported.imported)) shape = { framework: "fastify", prefix: "", openapi: false };
4064
+ }
4065
+ if (!shape && !isNew && !member && !a.source.slice(open + 1, close).trim()) shape = resolve3(a, name, false, depth + 1);
4066
+ if (!shape) return null;
4067
+ const chained = basePath(a, close, shape);
4068
+ return chained && !arg.text.slice(chained.close - arg.at + 1).trim() ? chained.shape : null;
4069
+ };
4070
+ const functionValue = (a, value, depth) => {
4071
+ if (depth >= 8) {
4072
+ limited2 = true;
4073
+ return null;
4074
+ }
4075
+ if (/^[A-Za-z_$][\w$]*$/.test(value.text)) return resolve3(a, value.text, false, depth + 1);
4076
+ const signature = /^(?:function(?:\s+[A-Za-z_$][\w$]*)?\s*\(\s*\)\s*(?::[^{};=]{1,200})?\s*|\(\s*\)\s*(?::[^{};=]{1,200})?\s*=>\s*)/.exec(value.text);
4077
+ if (!signature) return null;
4078
+ let body = argAt(a, value.at + signature[0].length, value.at + value.text.length);
4079
+ if (body.text.startsWith("{")) {
4080
+ const end = a.pairs.get(body.at);
4081
+ if (end !== body.at + body.text.length - 1) return null;
4082
+ const returned = /^\s*return\b[ \t]*/.exec(a.code.slice(body.at + 1, end));
4083
+ if (!returned) return null;
4084
+ const from = body.at + 1 + returned[0].length;
4085
+ if (/[\r\n]/.test(a.code[from] ?? "")) return null;
4086
+ body = argAt(a, from, end);
4087
+ if (body.text.endsWith(";")) body = argAt(a, body.at, body.at + body.text.length - 1);
4088
+ }
4089
+ return expression(a, body, depth);
4090
+ };
4091
+ const resolve3 = (a, name, exported, depth) => {
4092
+ if (depth >= 8) {
4093
+ limited2 = true;
4094
+ return null;
4095
+ }
4096
+ const key = `${a.file.path}\0${name}\0${exported}\0${depth}`;
4097
+ if (cache3.has(key)) return cache3.get(key);
4098
+ const follow = (spec, symbol) => {
4099
+ const resolved = module(spec, a.file);
4100
+ if (resolved.limited) limited2 = true;
4101
+ const target = resolved.file;
4102
+ return target ? resolve3(analyse(target), symbol, true, depth + 1) : null;
4103
+ };
4104
+ const bindings = bindingsOf(a.file), escaped = name.replace(/\$/g, "\\$");
4105
+ let result = null;
4106
+ if (exported) {
4107
+ if (name === "default") {
4108
+ const match = /\bexport\s+default\s+/.exec(a.code);
4109
+ if (match && topLevel(a, match.index)) {
4110
+ const at = match.index + match[0].length;
4111
+ const named = /^function\s+([A-Za-z_$][\w$]*)/.exec(a.code.slice(at));
4112
+ if (!named || factoryBindingClean(a, named[1])) result = functionValue(a, argAt(a, at, expressionEnd(a, at)), depth);
4113
+ }
4114
+ } else if (new RegExp(`\\bexport\\s+(?:const|function)\\s+${escaped}(?![\\w$])`).test(a.code)) result = resolve3(a, name, false, depth);
4115
+ else {
4116
+ const matches = bindings.exports.filter((item) => item.local === name);
4117
+ if (matches.length === 1) {
4118
+ const entry = matches[0];
4119
+ result = entry.spec ? follow(entry.spec, entry.imported) : resolve3(a, entry.imported, false, depth + 1);
4120
+ } else if (!matches.length && name !== "default" && bindings.stars.length === 1) result = follow(bindings.stars[0], name);
4121
+ }
4122
+ } else {
4123
+ const declarations = [...a.code.matchAll(new RegExp(`\\b(const|function)\\s+${escaped}(?![\\w$])`, "g"))];
4124
+ const declaration = declarations.length === 1 ? declarations[0] : void 0;
4125
+ if (declaration && topLevel(a, declaration.index)) {
4126
+ const at = declaration.index;
4127
+ if (declaration[1] === "function" && factoryBindingClean(a, name)) result = functionValue(a, argAt(a, at, expressionEnd(a, at)), depth);
4128
+ if (declaration[1] === "const") {
4129
+ const prefix = new RegExp(`^const\\s+${escaped}\\s*(?::[^=;]{0,200})?=\\s*`).exec(a.code.slice(at));
4130
+ const nameAt = at + /^const\s+/.exec(declaration[0])[0].length;
4131
+ if (prefix && factoryBindingClean(a, name, nameAt)) {
4132
+ const valueAt = at + prefix[0].length;
4133
+ result = functionValue(a, argAt(a, valueAt, expressionEnd(a, valueAt)), depth);
4134
+ }
4135
+ }
4136
+ } else if (!declarations.length && factoryBindingClean(a, name)) {
4137
+ const imports = bindings.imports.filter((item) => item.local === name && item.spec);
4138
+ if (imports.length === 1) result = follow(imports[0].spec, imports[0].imported);
4139
+ }
4140
+ }
4141
+ if (result) cache3.set(key, result);
4142
+ return result;
4143
+ };
4144
+ return (a, known) => {
4145
+ const found = [];
4146
+ const receivers = new Set([...a.code.matchAll(new RegExp(`(?<![\\w$.])([A-Za-z_$][\\w$]*)\\s*\\.\\s*(?:${METHODS}|on|openapi|openapiRoutes|route|register)\\s*${GENERICS}\\(`, "g"))].map((match) => match[1]));
4147
+ let inspected = 0;
4148
+ for (const m of a.code.matchAll(/\bconst\s+([A-Za-z_$][\w$]*)\s*=\s*([A-Za-z_$][\w$]*)\s*\(\s*\)/g)) {
4149
+ const [, name, factory] = m;
4150
+ if (!receivers.has(name) || known.has(name)) continue;
4151
+ if (++inspected > 256) {
4152
+ gaps.set(a.file, m.index);
4153
+ break;
4154
+ }
4155
+ const open = a.code.indexOf("(", m.index), close = m.index + m[0].length - 1;
4156
+ if (!topLevel(a, m.index) || a.source.slice(open + 1, close).trim() || !factoryBindingClean(a, factory) || !factoryBindingClean(a, name)) continue;
4157
+ limited2 = false;
4158
+ const shape = resolve3(a, factory, false, 0);
4159
+ const chained = shape ? basePath(a, m.index + m[0].length - 1, shape) : null;
4160
+ if (limited2) gaps.set(a.file, m.index);
4161
+ const end = expressionEnd(a, open);
4162
+ if (chained && !a.code.slice(chained.close + 1, end).trim()) found.push({ name, ...chained.shape, weak: false, chainFrom: chained.close });
4163
+ }
4164
+ return found;
4165
+ };
4166
+ }
3603
4167
  function literalOf(arg) {
3604
4168
  const m = /^(['"`])([^'"`$]*)\1$/.exec(arg.source);
3605
4169
  return m ? m[2] : null;
@@ -3618,11 +4182,13 @@ function usePrefixes(a, arg) {
3618
4182
  if (arg.text.startsWith("[")) {
3619
4183
  const close = a.pairs.get(arg.at);
3620
4184
  const items = close === void 0 ? [] : argsOf(a, arg.at, close);
3621
- if (items.length === 0 || !items.every((item) => /^['"`]/.test(item.source))) return null;
4185
+ if (items.length === 0 || !items.some((item) => /^['"`]/.test(item.source))) return null;
3622
4186
  return items.map((item) => pathOf(a, item)).filter((path) => path !== null);
3623
4187
  }
3624
4188
  const prefix = pathOf(a, arg);
3625
- return prefix === null ? null : [prefix];
4189
+ if (prefix !== null) return [prefix];
4190
+ if (/^['"`/]|^new\s+RegExp\b|^(?:process\s*\.\s*env|import\s*\.\s*meta\s*\.\s*env|(?:[\w$]+\s*\.\s*)?env)\s*[.[]/.test(arg.source)) return [];
4191
+ return null;
3626
4192
  }
3627
4193
  function pathOf(a, arg) {
3628
4194
  if (arg.text.startsWith("[")) return null;
@@ -3691,11 +4257,199 @@ function expressUses(a, name) {
3691
4257
  }
3692
4258
  return found.sort((x, y) => x.at - y.at);
3693
4259
  }
3694
- function honoOps(a, name, chainFrom) {
4260
+ function openapiMiddleware(a, arg, depth = 0) {
4261
+ if (depth >= 8) return [];
4262
+ if (arg.text.startsWith("[")) {
4263
+ const close = a.pairs.get(arg.at);
4264
+ if (close === void 0 || !/^\s*(?:as\s+const)?\s*$/.test(arg.text.slice(close - arg.at + 1))) return [];
4265
+ return argsOf(a, arg.at, close).flatMap((item) => openapiMiddleware(a, item, depth + 1));
4266
+ }
4267
+ if (/^[A-Za-z_$][\w$]*(?:\s*\.\s*[A-Za-z_$][\w$]*)*$/.test(arg.text) || FUNCTION_EXPRESSION.test(arg.text)) return [arg];
4268
+ const call = /^[A-Za-z_$][\w$.]*\s*\(/.exec(arg.text);
4269
+ return call && a.pairs.get(arg.at + call[0].length - 1) === arg.at + arg.text.length - 1 ? [arg] : [];
4270
+ }
4271
+ function openapiEntries(a, arg, files, depth = 0) {
4272
+ const value = openapiValue(a, arg, files, depth);
4273
+ if (!value || !value.text.startsWith("{")) return /* @__PURE__ */ new Map();
4274
+ const owner = analyse(value.file);
4275
+ const close = owner.pairs.get(value.at);
4276
+ if (argsOf(owner, value.at, close).some((part) => /^(?:\.\.\.|\[|(?:get|set)\s+)/.test(part.text))) return /* @__PURE__ */ new Map();
4277
+ return new Map([...entriesOf(owner, value)].map(([key, entry]) => [key, {
4278
+ ...entry,
4279
+ value: entry.value ? { ...entry.value, file: owner.file } : null
4280
+ }]));
4281
+ }
4282
+ function openapiValue(a, arg, files, depth = 0) {
4283
+ if (depth >= 8) return null;
4284
+ const root = /^([A-Za-z_$][\w$]*)(?:\s*[.(]|$)/.exec(arg.text)?.[1];
4285
+ if (root) {
4286
+ const token = new RegExp(`(?<![\\w$])${root.replace(/\$/g, "\\$")}(?![\\w$])`);
4287
+ if (a.bodies.some((body) => body.start < arg.at && arg.at < body.end && token.test(a.code.slice(body.declaration, body.start)))) return null;
4288
+ for (const match of a.code.matchAll(/\b(?:const|let|var)\s*([\[{])/g)) {
4289
+ const open = match.index + match[0].length - 1;
4290
+ const close = a.pairs.get(open);
4291
+ if (close !== void 0 && token.test(a.code.slice(open, close + 1))) return null;
4292
+ }
4293
+ }
4294
+ if (/^[{[]/.test(arg.text)) {
4295
+ const close = a.pairs.get(arg.at);
4296
+ if (close === void 0 || !/^\s*(?:as\s+const|satisfies\s+[\w$.]+)?\s*$/.test(arg.text.slice(close - arg.at + 1))) return null;
4297
+ return { ...arg, file: a.file };
4298
+ }
4299
+ const call = /^([A-Za-z_$][\w$]*)\s*\(/.exec(arg.text);
4300
+ if (call) {
4301
+ const binding = bindingsOf(a.file).imports.find((item) => item.local === call[1]);
4302
+ if (!binding || !["createRoute", "defineOpenAPIRoute"].includes(binding.imported) || binding.spec !== "@hono/zod-openapi") return null;
4303
+ if (new RegExp(`\\b(?:const|let|var|function)\\s+${call[1].replace(/\$/g, "\\$")}(?![\\w$])`).test(a.code)) return null;
4304
+ const open = arg.at + call[0].length - 1;
4305
+ const close = a.pairs.get(open);
4306
+ if (close !== arg.at + arg.text.length - 1) return null;
4307
+ const args = argsOf(a, open, close);
4308
+ return args.length === 1 ? openapiValue(a, args[0], files, depth + 1) : null;
4309
+ }
4310
+ if (root && !unchangedOpenapiReference(a, root)) return null;
4311
+ const member = /^([A-Za-z_$][\w$]*)\s*\.\s*([A-Za-z_$][\w$]*)$/.exec(arg.text);
4312
+ if (member) {
4313
+ const escaped = member[1].replace(/\$/g, "\\$");
4314
+ if (new RegExp(`\\b(?:const|let|var|function|class)\\s+${escaped}(?![\\w$])`).test(a.code)) return null;
4315
+ const namespace = new RegExp(`\\bimport\\s+\\*\\s+as\\s+${escaped}\\s+from\\s*['"]([^'"]+)['"]`).exec(a.source);
4316
+ const module = namespace && a.code.slice(namespace.index, namespace.index + 6) === "import" ? moduleFor(namespace[1], a.file, files) : null;
4317
+ return module ? openapiExportValue(analyse(module), member[2], files, depth + 1) : null;
4318
+ }
4319
+ if (!/^[A-Za-z_$][\w$]*$/.test(arg.text)) return null;
4320
+ const name = arg.text.replace(/\$/g, "\\$");
4321
+ const declarations = [...a.code.matchAll(new RegExp(`\\b(const|let|var)\\s+${name}\\s*(?::[^=;]{0,200})?=(?![=>])\\s*`, "g"))];
4322
+ if (declarations.length === 0) {
4323
+ const imports = bindingsOf(a.file).imports.filter((item) => item.local === arg.text && item.spec);
4324
+ const imported = imports.length === 1 ? imports[0] : void 0;
4325
+ const module = imported ? moduleFor(imported.spec, a.file, files) : null;
4326
+ return module ? openapiExportValue(analyse(module), imported.imported, files, depth + 1) : null;
4327
+ }
4328
+ if (declarations.length !== 1 || declarations[0][1] !== "const") return null;
4329
+ const declaration = declarations[0];
4330
+ if (declaration.index >= arg.at) return null;
4331
+ for (const [open, close] of a.pairs) {
4332
+ if (a.code[open] === "{" && open < declaration.index && declaration.index < close && !(open < arg.at && arg.at < close)) return null;
4333
+ }
4334
+ const from = declaration.index + declaration[0].length;
4335
+ const end = expressionEnd(a, from);
4336
+ const text = a.code.slice(from, end).trimEnd();
4337
+ return openapiValue(a, { at: from, text, source: a.source.slice(from, from + text.length) }, files, depth + 1);
4338
+ }
4339
+ function unchangedOpenapiReference(a, name) {
4340
+ const aliases = /* @__PURE__ */ new Set([name]);
4341
+ const assignedAliases = [...a.code.matchAll(/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*([A-Za-z_$][\w$]*)\s*(?=[;,\n]|$)/g)];
4342
+ for (let pass = 0; pass < 8; pass++) {
4343
+ const size = aliases.size;
4344
+ for (const match of assignedAliases) if (aliases.has(match[2])) aliases.add(match[1]);
4345
+ if (aliases.size > 64) return false;
4346
+ if (aliases.size === size) break;
4347
+ if (pass === 7) return false;
4348
+ }
4349
+ for (const alias of aliases) {
4350
+ const escaped = alias.replace(/\$/g, "\\$");
4351
+ const mutable = new RegExp(`(?<![\\w$.])${escaped}\\s*(?:\\.\\s*[\\w$]+|\\[[^\\]]{0,200}\\])+(?:\\s*(?:=|\\?\\?=|\\|\\|=|&&=)|\\s*\\()|\\bdelete\\s+${escaped}(?![\\w$])|\\bObject\\s*\\.\\s*(?:assign|defineProperty|defineProperties)\\s*\\(\\s*${escaped}(?![\\w$])`);
4352
+ if (mutable.test(a.code)) return false;
4353
+ }
4354
+ return true;
4355
+ }
4356
+ function openapiExportValue(a, name, files, depth) {
4357
+ if (depth >= 8) return null;
4358
+ if (name === "default") {
4359
+ const declaration = /\bexport\s+default\s+/.exec(a.code);
4360
+ if (declaration) {
4361
+ const at = declaration.index + declaration[0].length;
4362
+ const text = a.code.slice(at, expressionEnd(a, at)).trimEnd();
4363
+ return openapiValue(a, { at, text, source: a.source.slice(at, at + text.length) }, files, depth + 1);
4364
+ }
4365
+ } else if (new RegExp(`\\bexport\\s+const\\s+${name.replace(/\$/g, "\\$")}\\s*(?=[:=])`).test(a.code)) {
4366
+ return openapiValue(a, { text: name, source: name, at: a.code.length }, files, depth + 1);
4367
+ }
4368
+ const bindings = bindingsOf(a.file);
4369
+ const matches = bindings.exports.filter((item) => item.local === name);
4370
+ if (matches.length > 1) return null;
4371
+ const binding = matches[0];
4372
+ if (binding?.spec) {
4373
+ const module = moduleFor(binding.spec, a.file, files);
4374
+ return module ? openapiExportValue(analyse(module), binding.imported, files, depth + 1) : null;
4375
+ }
4376
+ if (binding) return openapiValue(a, { text: binding.imported, source: binding.imported, at: a.code.length }, files, depth + 1);
4377
+ if (name !== "default" && bindings.stars.length === 1) {
4378
+ const module = moduleFor(bindings.stars[0], a.file, files);
4379
+ if (module) return openapiExportValue(analyse(module), name, files, depth + 1);
4380
+ }
4381
+ return null;
4382
+ }
4383
+ function honoOps(a, name, chainFrom, files, openapi = false) {
3695
4384
  const registrations = [];
3696
4385
  const uses = [];
4386
+ const batchGaps = /* @__PURE__ */ new Set();
4387
+ const addOpenapi = (config, handler, handlerRange, at, batch = false) => {
4388
+ const entries = openapiEntries(config.file ? analyse(config.file) : a, config, files);
4389
+ const path = entries.get("path")?.value;
4390
+ const resolved = path ? literalOf(path) : null;
4391
+ const routingPath = resolved?.replace(/\/\{(.+?)\}/g, "/:$1") ?? null;
4392
+ const middleware = entries.get("middleware")?.value;
4393
+ const owner = middleware?.file ? analyse(middleware.file) : a;
4394
+ registrations.push({
4395
+ path: routingPath,
4396
+ middleware: middleware ? openapiMiddleware(owner, middleware).map((arg) => ({ ...arg, file: owner.file })) : [],
4397
+ handler,
4398
+ handlerRange,
4399
+ at,
4400
+ batch
4401
+ });
4402
+ return [routingPath];
4403
+ };
3697
4404
  const visit = (method, open, close, at, previous) => {
3698
4405
  const args = argsOf(a, open, close);
4406
+ if (method === "openapiRoutes") {
4407
+ if (!openapi) return previous;
4408
+ let paths2 = previous;
4409
+ let inspected = 0;
4410
+ const gap = () => {
4411
+ batchGaps.add(at);
4412
+ paths2 = [null];
4413
+ };
4414
+ const walk = (origin, list, depth) => {
4415
+ if (!list || depth >= 8) {
4416
+ gap();
4417
+ return;
4418
+ }
4419
+ const value = openapiValue(origin, list, files);
4420
+ if (!value || !value.text.startsWith("[")) {
4421
+ gap();
4422
+ return;
4423
+ }
4424
+ const owner = analyse(value.file);
4425
+ for (const item of argsOf(owner, value.at, owner.pairs.get(value.at))) {
4426
+ if (++inspected > 256) {
4427
+ gap();
4428
+ return;
4429
+ }
4430
+ if (item.text.startsWith("...")) {
4431
+ const lead = 3 + item.text.slice(3).length - item.text.slice(3).trimStart().length;
4432
+ walk(owner, { at: item.at + lead, text: item.text.slice(lead), source: item.source.slice(lead) }, depth + 1);
4433
+ continue;
4434
+ }
4435
+ const entries = openapiEntries(owner, item, files);
4436
+ if (entries.get("addRoute")?.value?.text === "false") continue;
4437
+ const config = entries.get("route")?.value;
4438
+ const handler = entries.get("handler");
4439
+ if (!config || !handler || !handler.value && !handler.range) {
4440
+ gap();
4441
+ continue;
4442
+ }
4443
+ paths2 = addOpenapi(config, handler.value, handler.range, at, true);
4444
+ }
4445
+ };
4446
+ walk(a, args[0], 0);
4447
+ return paths2;
4448
+ }
4449
+ if (method === "openapi") {
4450
+ if (!openapi || !args[0] || !args[1]) return previous;
4451
+ return addOpenapi(args[0], args[1], null, at);
4452
+ }
3699
4453
  if (method === "use") {
3700
4454
  const prefixes = args[0] ? usePrefixes(a, args[0]) : null;
3701
4455
  const middleware = flatten(a, prefixes === null ? args : args.slice(1));
@@ -3737,7 +4491,7 @@ function honoOps(a, name, chainFrom) {
3737
4491
  }
3738
4492
  registrations.sort((x, y) => x.at - y.at);
3739
4493
  uses.sort((x, y) => x.at - y.at);
3740
- return { registrations, uses };
4494
+ return { registrations, uses, batchGaps: [...batchGaps] };
3741
4495
  }
3742
4496
  function fastifyOps(a, name) {
3743
4497
  const registrations = [];
@@ -4019,7 +4773,7 @@ function exportedPlugin(t, files, depth) {
4019
4773
  const text = t.code.slice(from, end).trimEnd();
4020
4774
  return pluginOf(t, { text, source: t.source.slice(from, from + text.length), at: from }, files, depth);
4021
4775
  }
4022
- var toRefs = (a, args) => args.map((arg) => ({ text: arg.source, at: arg.at, file: a.file }));
4776
+ var toRefs = (a, args) => args.map((arg) => ({ text: arg.source, at: arg.at, file: arg.file ?? a.file }));
4023
4777
  function mountedFile(a, arg, files) {
4024
4778
  const inline = /^require\s*\(\s*['"]([^'"]+)['"]\s*\)$/.exec(arg.source);
4025
4779
  if (inline) return moduleFor(inline[1], a.file, files);
@@ -4071,31 +4825,42 @@ function paramNamesOf(a, range) {
4071
4825
  }
4072
4826
  var within = (site, at) => site.range === null || site.range.start <= at && at <= site.range.end;
4073
4827
  var opsCache = /* @__PURE__ */ new WeakMap();
4074
- function opsOf(site) {
4828
+ var contextCache = /* @__PURE__ */ new WeakMap();
4829
+ function contextOf(a) {
4830
+ let context = contextCache.get(a.file);
4831
+ if (!context) {
4832
+ context = registrationContext(a.code, a.pairs, a.bodies);
4833
+ contextCache.set(a.file, context);
4834
+ }
4835
+ return context;
4836
+ }
4837
+ function opsOf(site, files) {
4075
4838
  const cached = opsCache.get(site);
4076
4839
  if (cached) return cached;
4077
4840
  const { a, name } = site;
4078
4841
  let ops;
4079
4842
  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: [] };
4843
+ else if (site.framework === "hono") ops = { ...honoOps(a, name, site.chainFrom, files, site.openapi), registers: [] };
4081
4844
  else ops = fastifyOps(a, name);
4845
+ const context = contextOf(a);
4082
4846
  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))
4847
+ registrations: ops.registrations.filter((reg) => within(site, reg.at) && context.reachable(reg.at)),
4848
+ uses: ops.uses.filter((use) => within(site, use.at) && context.reachable(use.at)),
4849
+ registers: ops.registers.filter((reg) => within(site, reg.at) && context.reachable(reg.at)),
4850
+ batchGaps: (ops.batchGaps ?? []).filter((at) => within(site, at))
4086
4851
  };
4087
4852
  opsCache.set(site, result);
4088
4853
  return result;
4089
4854
  }
4090
4855
  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);
4856
+ const args = opsOf(site, files).uses.filter((use) => !use.mount && use.at < before && contextOf(site.a).covers(use.at, Number.isFinite(before) ? before : (site.range?.end ?? site.a.code.length) - 1) && (use.prefix === null || site.framework !== "hono" && use.prefix === "/" || /^\/?\*$/.test(use.prefix))).flatMap((use) => use.args).filter((arg) => site.framework !== "express" || mountedFile(site.a, arg, files) === null);
4092
4857
  return toRefs(site.a, args);
4093
4858
  }
4094
4859
  function passedSites(site, files) {
4095
4860
  const { a } = site;
4096
4861
  const found = [];
4097
4862
  const receive = (range, index, at) => {
4098
- if (!range) return;
4863
+ if (!range || !contextOf(a).reachable(at)) return;
4099
4864
  const target = analyse(range.file);
4100
4865
  const name = paramNamesOf(target, range)[index];
4101
4866
  if (!name) return;
@@ -4103,6 +4868,7 @@ function passedSites(site, files) {
4103
4868
  a: target,
4104
4869
  name,
4105
4870
  framework: site.framework,
4871
+ openapi: Boolean(site.openapi),
4106
4872
  range: { start: range.start, end: range.end },
4107
4873
  prefix: site.prefix,
4108
4874
  root: site.root ?? site,
@@ -4152,7 +4918,7 @@ function normalizePath(path) {
4152
4918
  function registeredSites(site, files) {
4153
4919
  const { a } = site;
4154
4920
  const found = [];
4155
- for (const reg of opsOf(site).registers) {
4921
+ for (const reg of opsOf(site, files).registers) {
4156
4922
  const options = entriesOf(a, reg.options);
4157
4923
  const prefixArg = options.get("prefix")?.value;
4158
4924
  const prefix = joinPath(site.prefix, (prefixArg && pathOf(a, prefixArg)) ?? "");
@@ -4211,24 +4977,50 @@ function serverIndex(files) {
4211
4977
  const cached = indexCache.get(files);
4212
4978
  if (cached) return cached;
4213
4979
  const sites = [];
4980
+ const registrationGaps = /* @__PURE__ */ new Set();
4981
+ const factoryGaps = /* @__PURE__ */ new Map();
4982
+ const factories = projectFactoryResolver(files, factoryGaps);
4214
4983
  const seen = /* @__PURE__ */ new Set();
4215
4984
  const add = (site) => {
4216
- const key = `${site.a.file.path}\0${site.name}\0${site.range?.start ?? -1}\0${site.prefix}`;
4985
+ if (site.inherited.length > 256) {
4986
+ registrationGaps.add(site.a.file);
4987
+ site.inherited = [];
4988
+ }
4989
+ const root = site.root ?? site;
4990
+ const identity2 = createHash2("sha256").update(JSON.stringify([
4991
+ site.a.file.path,
4992
+ site.name,
4993
+ site.framework,
4994
+ site.range?.start ?? -1,
4995
+ site.prefix,
4996
+ Boolean(site.openapi),
4997
+ root.a.file.path,
4998
+ root.name,
4999
+ root.range?.start ?? -1,
5000
+ root.prefix
5001
+ ]));
5002
+ for (const ref of site.inherited) identity2.update(JSON.stringify([ref.file.path, ref.at]));
5003
+ const key = identity2.digest("hex");
4217
5004
  if (seen.has(key)) return false;
5005
+ if (sites.length >= 4096) {
5006
+ registrationGaps.add(site.a.file);
5007
+ return false;
5008
+ }
4218
5009
  seen.add(key);
4219
5010
  sites.push(site);
4220
5011
  return true;
4221
5012
  };
4222
5013
  for (const file of files) {
4223
- if (!/\.[mc]?[jt]sx?$/.test(file.path) || !/\b(?:express|hono|fastify)\b/.test(file.content)) continue;
5014
+ if (!/\.[mc]?[jt]sx?$/.test(file.path)) continue;
4224
5015
  const a = analyse(file);
4225
5016
  const names = frameworkNamesOf(a);
4226
- if (!names) continue;
4227
- for (const instance of instancesOf(a, names)) {
5017
+ const direct = names ? instancesOf(a, names) : [];
5018
+ for (const instance of [...direct, ...factories(a, new Set(direct.map((item) => item.name)))]) {
4228
5019
  add({
4229
5020
  a,
4230
5021
  name: instance.name,
4231
5022
  framework: instance.framework,
5023
+ openapi: Boolean(instance.openapi),
4232
5024
  range: null,
4233
5025
  prefix: instance.prefix,
4234
5026
  inherited: [],
@@ -4249,15 +5041,22 @@ function serverIndex(files) {
4249
5041
  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
5042
  const byRouterFile = /* @__PURE__ */ new Map();
4251
5043
  const byRoot = /* @__PURE__ */ new Map();
5044
+ const batchGaps = /* @__PURE__ */ new Map();
4252
5045
  for (const site of sites) {
4253
5046
  const { a } = site;
4254
- const { registrations, uses } = opsOf(site);
5047
+ const { registrations, uses, batchGaps: gaps } = opsOf(site, files);
5048
+ for (const at of gaps ?? []) if (!shadowed(site, at) && !batchGaps.has(a.file)) batchGaps.set(a.file, at);
4255
5049
  const routes = byRouterFile.get(a.file) ?? [];
4256
5050
  for (const reg of registrations) {
4257
5051
  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));
5052
+ const handler = reg.handlerRange ?? (reg.handler ? handlerOf(reg.handler.file ? analyse(reg.handler.file) : a, reg.handler, files) : null);
5053
+ const handlerSource = handler ? analyse(handler.file) : null;
5054
+ const knownBatchHandler = !reg.batch || reg.handlerRange !== null || handler && handlerSource && (handler.arrow || FUNCTION_EXPRESSION.test(handlerSource.code.slice(handler.start, handler.end)) || handlerSource.bodies.some((body) => handler.start <= body.start && body.end <= handler.end));
5055
+ if (!handler || !knownBatchHandler) {
5056
+ if (reg.batch && !batchGaps.has(a.file)) batchGaps.set(a.file, reg.at);
5057
+ continue;
5058
+ }
5059
+ const before = uses.filter((use) => !use.mount && use.at < reg.at && contextOf(a).covers(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
5060
  const url = reg.path === null ? "(dynamic path)" : joinPath(site.prefix, reg.path);
4262
5061
  const callbacks = flatten(a, reg.middleware);
4263
5062
  const root = site.root ?? site;
@@ -4276,8 +5075,7 @@ function serverIndex(files) {
4276
5075
  };
4277
5076
  add2(handler, callbacks);
4278
5077
  callbacks.forEach((callback, index2) => {
4279
- if (!FUNCTION_EXPRESSION.test(callback.text)) return;
4280
- const range = functionAt(a, callback.at, callback.at + callback.text.length);
5078
+ const range = handlerOf(callback.file ? analyse(callback.file) : a, callback, files);
4281
5079
  if (range) add2(range, callbacks.slice(0, index2));
4282
5080
  });
4283
5081
  }
@@ -4290,7 +5088,7 @@ function serverIndex(files) {
4290
5088
  for (const site of sites) {
4291
5089
  if (site.framework === "fastify") continue;
4292
5090
  const { a } = site;
4293
- const uses = opsOf(site).uses;
5091
+ const uses = opsOf(site, files).uses;
4294
5092
  for (const use of uses) {
4295
5093
  if (site.framework === "hono" && !use.mount) continue;
4296
5094
  use.args.forEach((arg, index2) => {
@@ -4299,7 +5097,7 @@ function serverIndex(files) {
4299
5097
  if (!routes || routes.length === 0) return;
4300
5098
  const isMiddleware = (x) => mountedFile(a, x, files) === null && localRoutes(a, x) === void 0;
4301
5099
  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));
5100
+ const earlier = uses.filter((other) => !other.mount && other.at < use.at && contextOf(a).covers(other.at, use.at) && covers2(other)).flatMap((other) => site.framework === "hono" ? other.args : other.args.filter(isMiddleware));
4303
5101
  const middleware = [...site.inherited, ...toRefs(a, earlier), ...toRefs(a, use.args.slice(0, index2).filter(isMiddleware))];
4304
5102
  const prefix = joinPath(site.prefix, use.prefix ?? "/");
4305
5103
  for (const route of routes) {
@@ -4317,38 +5115,64 @@ function serverIndex(files) {
4317
5115
  byHandlerFile.set(route.file, list);
4318
5116
  }
4319
5117
  }
4320
- const index = { byHandlerFile };
5118
+ for (const site of sites) if (contextOf(site.a).limited) registrationGaps.add(site.a.file);
5119
+ const index = { byHandlerFile, batchGaps, factoryGaps, registrationGaps };
4321
5120
  indexCache.set(files, index);
4322
5121
  return index;
4323
5122
  }
4324
5123
  function nodeRoutesFor(file, files) {
4325
5124
  return serverIndex(files).byHandlerFile.get(file) ?? [];
4326
5125
  }
5126
+ function reportOpenapiBatchCoverage(ctx) {
5127
+ for (const file of serverIndex(ctx.files).registrationGaps) {
5128
+ ctx.reportIncomplete("engine/route-registration", `${file.path}: Route registration context reached a documented scope or graph limit. Conditional or truncated inherited middleware was not accepted as protection.`);
5129
+ }
5130
+ for (const [file, at] of serverIndex(ctx.files).factoryGaps) {
5131
+ const line = file.content.slice(0, at).split("\n").length;
5132
+ ctx.reportIncomplete("engine/router-factories", `${file.path}:${line}: Project router-factory analysis reached a resolution, expression, candidate or module-configuration limit. See the documented bounds; simplify the wrapper or scan its direct route entry separately.`);
5133
+ }
5134
+ for (const [file, at] of serverIndex(ctx.files).batchGaps) {
5135
+ const line = file.content.slice(0, at).split("\n").length;
5136
+ ctx.reportIncomplete("engine/openapi-routes", `${file.path}:${line}: OpenAPI batch routes were not fully resolved. Use static entries and resolvable handlers; limits are 8 spread levels and 256 entries per call.`);
5137
+ }
5138
+ }
4327
5139
  function serverRoutesOf(file, files) {
4328
5140
  const route = routeOf(file);
4329
5141
  if (route) return [route];
4330
5142
  const actions = serverActionRoutes(file);
4331
5143
  return actions.length ? actions : nodeRoutesFor(file, files);
4332
5144
  }
4333
- var decoratorCache = /* @__PURE__ */ new WeakMap();
4334
- function decoratorOf(name, files) {
4335
- let cache3 = decoratorCache.get(files);
5145
+ function decoratorOf(ref, receiver, name, files) {
5146
+ const a = analyse(ref.file);
5147
+ const owner = (at) => a.bodies.filter((b) => b.start < at && at < b.end).sort((x, y) => y.start - x.start)[0];
5148
+ const pattern = new RegExp(`(?<![\\w$.])${receiver.replace(/\$/g, "\\$")}\\s*\\.\\s*decorate\\s*\\(\\s*['"]${name.replace(/\$/g, "\\$")}['"]\\s*,`, "g");
5149
+ let result = null;
5150
+ for (const m of a.source.matchAll(pattern)) {
5151
+ if (m.index >= ref.at || owner(m.index) !== owner(ref.at)) continue;
5152
+ const open = a.source.indexOf("(", m.index);
5153
+ const close = a.pairs.get(open);
5154
+ const value = close === void 0 ? void 0 : argsOf(a, open, close)[1];
5155
+ result = value ? handlerOf(a, value, files) : null;
5156
+ }
5157
+ return result;
5158
+ }
5159
+ var authPluginCache = /* @__PURE__ */ new WeakMap();
5160
+ function registeredAuthPlugin(ref, pkg) {
5161
+ const a = analyse(ref.file);
5162
+ const receiver = /^([A-Za-z_$][\w$]*)\s*\./.exec(ref.text)?.[1];
5163
+ if (!receiver) return false;
5164
+ let cache3 = authPluginCache.get(ref.file);
4336
5165
  if (!cache3) {
4337
5166
  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);
5167
+ authPluginCache.set(ref.file, cache3);
5168
+ }
5169
+ let registers = cache3.get(receiver);
5170
+ if (!registers) {
5171
+ registers = fastifyOps(a, receiver).registers;
5172
+ cache3.set(receiver, registers);
4350
5173
  }
4351
- return cache3.get(name) ?? null;
5174
+ const owner = (at) => a.bodies.filter((b) => b.start < at && at < b.end).sort((x, y) => y.start - x.start)[0];
5175
+ return registers.some((reg) => reg.at < ref.at && owner(reg.at) === owner(ref.at) && /^[A-Za-z_$][\w$]*$/.test(reg.target.text) && packageOf(a, reg.target.text) === pkg);
4352
5176
  }
4353
5177
  function authCompositionOf(ref) {
4354
5178
  const a = analyse(ref.file);
@@ -4382,7 +5206,7 @@ function middlewareDefinition(ref, files) {
4382
5206
  const callee = member ?? root;
4383
5207
  const spec = packageOf(a, root);
4384
5208
  if (member && (spec === null || spec === "fastify")) {
4385
- const decorated = decoratorOf(member, files);
5209
+ const decorated = decoratorOf(ref, root, member, files);
4386
5210
  if (decorated) return { name: callee, range: decorated, spec: null, inline };
4387
5211
  }
4388
5212
  if (spec) return { name: callee, range: null, spec, inline };
@@ -4399,66 +5223,241 @@ function propertyValue(a, name) {
4399
5223
  return { file: a.file, start: from, end: expressionEnd(a, from) - 1 };
4400
5224
  }
4401
5225
 
4402
- // src/rules/apiauth.ts
4403
- var APP_ROUTER = /(?:^|\/)app\/api\/(?:.+\/)?route\.[mc]?[jt]sx?$/;
4404
- var PAGES_ROUTER = /(?:^|\/)pages\/api\/.+\.[mc]?[jt]sx?$/;
4405
- var APP_ROUTE_FILE = /(?:^|\/)app\/(?:.+\/)?route\.[mc]?[jt]sx?$/;
4406
- var NEXT_METHODS = "GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS";
4407
- var NEXT_METHOD_EXPORT = new RegExp(
4408
- String.raw`\bexport\s+(?:async\s+)?function\s+(?:${NEXT_METHODS})\b|\bexport\s+const\s+(?:${NEXT_METHODS})\b|` + // export { handler as GET } 与 Auth.js 的 export const { GET, POST } = handlers;长度有界以免回溯。
4409
- String.raw`\bexport\s+(?:const\s*)?\{[^{}]{0,500}\b(?:${NEXT_METHODS})\b`
4410
- );
4411
- var SVELTEKIT_ENDPOINT = /^(.*?\/)?src\/routes\/(?:(.*)\/)?\+server\.[mc]?[jt]s$/;
4412
- var SVELTEKIT_PAGE_SERVER = /^(.*?\/)?src\/routes\/(?:(.*)\/)?\+page\.server\.[mc]?[jt]s$/;
4413
- var SVELTEKIT_ACTIONS = /\bexport\s+const\s+actions\b\s*(?::[^=;{}]{0,200})?=\s*\{/;
4414
- var NUXT_ROUTE = /^(.*?\/)?server\/(api|routes)\/(.+)\.[mc]?[jt]s$/;
4415
- var H3_HANDLER = /\b(?:defineEventHandler|eventHandler|defineCachedEventHandler|defineLazyEventHandler)\s*\(/;
4416
- var NUXT_METHOD_SUFFIX = /\.(?:get|post|put|patch|delete|head|options|connect|trace)$/i;
4417
- var REMIX_ROUTE = /^(.*?\/)?app\/routes\/(.+)\.[mc]?[jt]sx?$/;
4418
- var REMIX_EXPORT = /\bexport\s+(?:async\s+)?function\s+(?:loader|action)\b|\bexport\s+const\s+(?:loader|action)\b/;
4419
- var ASTRO_ENDPOINT = /^(.*?\/)?src\/pages\/(.+)\.[mc]?[jt]s$/;
4420
- var ASTRO_EXPORT = /\bexport\s+(?:async\s+)?function\s+(?:GET|POST|PUT|PATCH|DELETE|ALL)\b|\bexport\s+const\s+(?:GET|POST|PUT|PATCH|DELETE|ALL)\b/;
4421
- function isAstroEndpoint(file) {
4422
- const code = noiseMaskedOf(file);
4423
- return ASTRO_EXPORT.test(code) && !/\bexport\s+default\b/.test(code);
4424
- }
4425
- function serverActionRoutes(file) {
4426
- if (!/use server/.test(file.content) || !/\.[mc]?[jt]sx?$/.test(file.path)) return [];
4427
- const code = noiseMaskedOf(file);
4428
- const source = commentsMaskedOf(file);
4429
- const fileLevel = startsWithDirective(source, 0, "use server");
4430
- const pairs = delimiterPairs(code);
4431
- const openers = /* @__PURE__ */ new Map();
4432
- for (const [open, close] of pairs) openers.set(close, open);
4433
- const exportedList = /* @__PURE__ */ new Set();
4434
- for (const m of code.matchAll(/\bexport\s*\{([^{}]{0,1000})\}/g)) {
4435
- for (const part of m[1].split(",")) {
4436
- const name = /^\s*([A-Za-z_$][\w$]*)/.exec(part)?.[1];
4437
- if (name) exportedList.add(name);
4438
- }
5226
+ // src/rules/verification.ts
5227
+ var LocalVerification = class {
5228
+ constructor(file, responseExits = false) {
5229
+ this.responseExits = responseExits;
5230
+ this.code = noiseMaskedOf(file);
5231
+ this.source = commentsMaskedOf(file);
5232
+ this.pairs = delimiterPairs(this.code);
5233
+ this.bodies = functionBodies(this.code, this.pairs);
4439
5234
  }
4440
- const scope = /^(.*?\/)?(?:src|app|lib|actions|server|utils)\//.exec(file.path)?.[1] ?? "";
4441
- const routes = [];
4442
- for (const body of functionBodies(code, pairs)) {
4443
- const inline = startsWithDirective(source, body.start + 1, "use server");
4444
- const declared = declarationOf(code, body, openers);
4445
- const exported = fileLevel && declared !== null && (declared.exported || exportedList.has(declared.name));
4446
- if (!inline && !exported) continue;
4447
- const name = declared?.name ?? "anonymous";
4448
- routes.push({
4449
- file,
4450
- framework: "next",
4451
- url: `the server action ${name}`,
4452
- scope,
4453
- reachable: { start: body.start, end: body.end },
4454
- action: name
4455
- });
5235
+ responseExits;
5236
+ code;
5237
+ source;
5238
+ pairs;
5239
+ bodies;
5240
+ owner(at) {
5241
+ return this.bodies.filter((b) => b.start < at && at < b.end).sort((a, b) => b.start - a.start)[0];
4456
5242
  }
4457
- return routes;
4458
- }
4459
- function startsWithDirective(source, from, directive) {
4460
- let i = from;
4461
- while (i < source.length) {
5243
+ skipSpace(at) {
5244
+ while (/\s/.test(this.code[at] ?? "")) at++;
5245
+ return at;
5246
+ }
5247
+ endOf(at, limit = this.code.length) {
5248
+ if (this.code[at] === "{") return (this.pairs.get(at) ?? limit - 1) + 1;
5249
+ for (let i = at; i < limit; i++) {
5250
+ if (/[;\n}]/.test(this.code[i])) return i;
5251
+ const end = this.pairs.get(i);
5252
+ if (end !== void 0) i = end;
5253
+ }
5254
+ return limit;
5255
+ }
5256
+ /**
5257
+ * 块的顶层有无条件退出即算退出,前面可以有记录日志等语句;
5258
+ * 嵌套分支、函数中的 return 以及无花括号 if/else/循环控制的 return 不算。
5259
+ */
5260
+ exits(from, to) {
5261
+ let at = this.skipSpace(from);
5262
+ if (this.code[at] !== "{") return /^(?:return|throw)\b|^(?:redirect|notFound)\s*\(/.test(this.code.slice(at, to));
5263
+ const end = Math.min(to, this.pairs.get(at) ?? to);
5264
+ at = this.skipSpace(at + 1);
5265
+ while (at < end) {
5266
+ const statement = this.code.slice(at, this.statementEnd(at, end));
5267
+ if (/^return\b/.test(statement) && this.responseExits) {
5268
+ if (/^return\s+(?:(?:await\s+)?(?:reply|res|response)\s*\.[\s\S]*\b(?:send|end|json|sendStatus)\s*\(|(?:c|ctx|context)\s*\.\s*(?:json|text|body|html|redirect|newResponse)\s*\(|new\s+Response\s*\(|next\s*\(\s*[^)\s])/.test(statement)) return true;
5269
+ return false;
5270
+ }
5271
+ if (/^(?:return|throw)\b|^(?:redirect|notFound)\s*\(/.test(statement)) return true;
5272
+ if (this.responseExits && /^reply\s*\.\s*(?:(?:code|status)\s*\([^)]*\)\s*\.\s*)?send\s*\(/.test(this.code.slice(at, end))) return true;
5273
+ const next = this.statementEnd(at, end);
5274
+ if (next <= at) return false;
5275
+ at = this.skipSpace(next);
5276
+ }
5277
+ return false;
5278
+ }
5279
+ /** 跳过完整语句;分支、循环和函数内的退出不提升为外层退出。 */
5280
+ statementEnd(at, limit, depth = 0) {
5281
+ if (depth >= 64) return limit;
5282
+ if (this.code[at] === "{") return (this.pairs.get(at) ?? limit - 1) + 1;
5283
+ const control = /^(if|for(?:\s+await)?|while|with|switch)\s*\(/.exec(this.code.slice(at, at + 80));
5284
+ if (control) {
5285
+ const close = this.pairs.get(at + control[0].length - 1);
5286
+ if (close === void 0) return limit;
5287
+ let end = this.statementEnd(this.skipSpace(close + 1), limit, depth + 1);
5288
+ const next = this.skipSpace(end);
5289
+ if (control[1] === "if" && /^else\b/.test(this.code.slice(next, next + 5))) {
5290
+ end = this.statementEnd(this.skipSpace(next + 4), limit, depth + 1);
5291
+ }
5292
+ return end;
5293
+ }
5294
+ if (/^(?:try|do)\b/.test(this.code.slice(at, at + 8))) return limit;
5295
+ for (let i = at; i < limit; i++) {
5296
+ if (this.code[i] === ";") return i + 1;
5297
+ const close = this.pairs.get(i);
5298
+ if (close !== void 0) {
5299
+ i = close;
5300
+ continue;
5301
+ }
5302
+ if (this.code[i] !== "\n") continue;
5303
+ const before = this.code.slice(at, i).trimEnd();
5304
+ const after = this.code.slice(i + 1, limit).trimStart();
5305
+ if (!before || /[=+\-*/%&|^!?:,.([{<>]$/.test(before)) continue;
5306
+ if (/^[.?+\-*/%&|^,:<>=([{`]/.test(after)) continue;
5307
+ if (/\b(?:const|let|var|function|async|await|new|yield)$/.test(before)) continue;
5308
+ return i + 1;
5309
+ }
5310
+ return limit;
5311
+ }
5312
+ /** 验证与使用须在同一函数,且不能依赖可选分支或被吞掉的异常。 */
5313
+ enforcedBefore(from, to) {
5314
+ const owner = this.owner(from);
5315
+ if (from >= to || owner !== this.owner(to)) return false;
5316
+ const start = owner ? owner.start + 1 : 0;
5317
+ for (let i = start; i < from; i++) {
5318
+ const conditional = /^(?:if|for|while|switch)\s*\(/.exec(this.code.slice(i, i + 32));
5319
+ if (conditional && !/[\w$]/.test(this.code[i - 1] ?? "")) {
5320
+ const close = this.pairs.get(i + conditional[0].length - 1);
5321
+ if (close !== void 0) {
5322
+ const body = this.skipSpace(close + 1);
5323
+ const end2 = this.endOf(body);
5324
+ if (from < end2 && to >= end2) return false;
5325
+ }
5326
+ }
5327
+ const end = this.pairs.get(i);
5328
+ if (end === void 0) continue;
5329
+ if (end < from) {
5330
+ i = end;
5331
+ continue;
5332
+ }
5333
+ if (end >= to) continue;
5334
+ if (this.code[i] === "(" && !/(?:\?\.|\b(?:if|for|while|switch|catch))\s*$/.test(this.code.slice(Math.max(start, i - 20), i))) continue;
5335
+ if (this.code[i] !== "{" || !/\btry\s*$/.test(this.code.slice(Math.max(start, i - 16), i))) return false;
5336
+ let next = this.skipSpace(end + 1);
5337
+ if (/^catch\b/.test(this.code.slice(next))) {
5338
+ next = this.skipSpace(next + 5);
5339
+ if (this.code[next] === "(") next = this.skipSpace((this.pairs.get(next) ?? next) + 1);
5340
+ const caughtEnd = this.endOf(next);
5341
+ if (!this.exits(next, caughtEnd)) return false;
5342
+ next = this.skipSpace(caughtEnd);
5343
+ }
5344
+ if (/^finally\b/.test(this.code.slice(next))) return false;
5345
+ }
5346
+ let boundary = start;
5347
+ for (const [open, close] of this.pairs) {
5348
+ if (this.code[open] === "{" && open < from && close > from) boundary = Math.max(boundary, open + 1);
5349
+ }
5350
+ while (boundary < from) {
5351
+ const next = this.statementEnd(this.skipSpace(boundary), this.code.length);
5352
+ if (next <= boundary || next > from) break;
5353
+ boundary = next;
5354
+ }
5355
+ return !/[?]|&&|\|\||=>/.test(this.code.slice(boundary, from));
5356
+ }
5357
+ /** 找出当前函数中最后一次赋值;未知或条件覆盖同样会使旧证明失效。 */
5358
+ assignment(name, at) {
5359
+ const escaped = name.replace(/\$/g, "\\$");
5360
+ const pattern = new RegExp(`(?<![\\w$.])(?:const\\s+|let\\s+|var\\s+)?${escaped}((?:\\s*\\.[\\w$]+|\\s*\\[[^\\]]*\\])*)\\s*(?::[^=;{}\\n]{0,100})?=(?![=>])`, "g");
5361
+ let result = null;
5362
+ for (const m of this.code.slice(0, at).matchAll(pattern)) {
5363
+ if (this.owner(m.index) !== this.owner(at)) continue;
5364
+ const from = this.skipSpace(m.index + m[0].length);
5365
+ const end = this.endOf(from);
5366
+ if (end <= at) result = { at: m.index, from, end, expression: this.code.slice(from, end).trim(), member: m[1].length > 0 };
5367
+ }
5368
+ return result;
5369
+ }
5370
+ };
5371
+ function enforcedVerification(source, pattern, synchronous = /(?!)/) {
5372
+ const fragment = new LocalVerification({ path: "middleware.ts", content: source, lines: source.split("\n"), isExampleContext: false });
5373
+ const first = fragment.code.search(/\S/);
5374
+ let body = fragment.code[first] === "{" ? { start: first, end: fragment.pairs.get(first) ?? fragment.code.length - 1 } : fragment.bodies.find((b) => b.declaration >= first);
5375
+ if (body) {
5376
+ const returned = fragment.bodies.find((b) => b.declaration > body.start && b.end < body.end && /^\s*return\s+(?:createMiddleware\s*\(\s*)?(?:async\s*)?(?:\([^)]*\)\s*)?$/.test(fragment.code.slice(body.start + 1, b.declaration)));
5377
+ if (returned) body = returned;
5378
+ }
5379
+ const inner = body ? source.slice(body.start + 1, body.end) : source;
5380
+ const content = `async function __guard__(){
5381
+ ${inner}
5382
+ __continuation__();
5383
+ }`;
5384
+ const context = new LocalVerification({ path: "middleware.ts", content, lines: content.split("\n"), isExampleContext: false }, true);
5385
+ const owner = context.bodies[0];
5386
+ let end = content.lastIndexOf("__continuation__");
5387
+ for (const m of context.code.matchAll(/(?<![\w$.])next\s*\(\s*\)/g)) {
5388
+ if (context.owner(m.index) === owner) end = Math.min(end, m.index);
5389
+ }
5390
+ const calls = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
5391
+ for (const m of context.code.matchAll(calls)) {
5392
+ if (context.owner(m.index) !== owner || !context.enforcedBefore(m.index, end)) continue;
5393
+ const close = context.pairs.get(m.index + m[0].length - 1);
5394
+ if (close === void 0 || /^[.[]|^\?\./.test(context.code.slice(context.skipSpace(close + 1)))) continue;
5395
+ const before = context.code.slice(Math.max(owner?.start ?? 0, m.index - 300), m.index).replace(/(?:[A-Za-z_$][\w$]*(?:\s*\([^()]*\))?\s*\??\.\s*)*$/, "").trimEnd();
5396
+ if (/(?:^|[^\w$.])(?:await|return)$/.test(before) || synchronous.test(m[0])) return true;
5397
+ }
5398
+ return false;
5399
+ }
5400
+
5401
+ // src/rules/apiauth.ts
5402
+ var APP_ROUTER = /(?:^|\/)app\/api\/(?:.+\/)?route\.[mc]?[jt]sx?$/;
5403
+ var PAGES_ROUTER = /(?:^|\/)pages\/api\/.+\.[mc]?[jt]sx?$/;
5404
+ var APP_ROUTE_FILE = /(?:^|\/)app\/(?:.+\/)?route\.[mc]?[jt]sx?$/;
5405
+ var NEXT_METHODS = "GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS";
5406
+ var NEXT_METHOD_EXPORT = new RegExp(
5407
+ String.raw`\bexport\s+(?:async\s+)?function\s+(?:${NEXT_METHODS})\b|\bexport\s+const\s+(?:${NEXT_METHODS})\b|` + // export { handler as GET } 与 Auth.js 的 export const { GET, POST } = handlers;长度有界以免回溯。
5408
+ String.raw`\bexport\s+(?:const\s*)?\{[^{}]{0,500}\b(?:${NEXT_METHODS})\b`
5409
+ );
5410
+ var SVELTEKIT_ENDPOINT = /^(.*?\/)?src\/routes\/(?:(.*)\/)?\+server\.[mc]?[jt]s$/;
5411
+ var SVELTEKIT_PAGE_SERVER = /^(.*?\/)?src\/routes\/(?:(.*)\/)?\+page\.server\.[mc]?[jt]s$/;
5412
+ var SVELTEKIT_ACTIONS = /\bexport\s+const\s+actions\b\s*(?::[^=;{}]{0,200})?=\s*\{/;
5413
+ var NUXT_ROUTE = /^(.*?\/)?server\/(api|routes)\/(.+)\.[mc]?[jt]s$/;
5414
+ var H3_HANDLER = /\b(?:defineEventHandler|eventHandler|defineCachedEventHandler|defineLazyEventHandler)\s*\(/;
5415
+ var NUXT_METHOD_SUFFIX = /\.(?:get|post|put|patch|delete|head|options|connect|trace)$/i;
5416
+ var REMIX_ROUTE = /^(.*?\/)?app\/routes\/(.+)\.[mc]?[jt]sx?$/;
5417
+ var REMIX_EXPORT = /\bexport\s+(?:async\s+)?function\s+(?:loader|action)\b|\bexport\s+const\s+(?:loader|action)\b/;
5418
+ var ASTRO_ENDPOINT = /^(.*?\/)?src\/pages\/(.+)\.[mc]?[jt]s$/;
5419
+ var ASTRO_EXPORT = /\bexport\s+(?:async\s+)?function\s+(?:GET|POST|PUT|PATCH|DELETE|ALL)\b|\bexport\s+const\s+(?:GET|POST|PUT|PATCH|DELETE|ALL)\b/;
5420
+ function isAstroEndpoint(file) {
5421
+ const code = noiseMaskedOf(file);
5422
+ return ASTRO_EXPORT.test(code) && !/\bexport\s+default\b/.test(code);
5423
+ }
5424
+ function serverActionRoutes(file) {
5425
+ if (!/use server/.test(file.content) || !/\.[mc]?[jt]sx?$/.test(file.path)) return [];
5426
+ const code = noiseMaskedOf(file);
5427
+ const source = commentsMaskedOf(file);
5428
+ const fileLevel = startsWithDirective(source, 0, "use server");
5429
+ const pairs = delimiterPairs(code);
5430
+ const openers = /* @__PURE__ */ new Map();
5431
+ for (const [open, close] of pairs) openers.set(close, open);
5432
+ const exportedList = /* @__PURE__ */ new Set();
5433
+ for (const m of code.matchAll(/\bexport\s*\{([^{}]{0,1000})\}/g)) {
5434
+ for (const part of m[1].split(",")) {
5435
+ const name = /^\s*([A-Za-z_$][\w$]*)/.exec(part)?.[1];
5436
+ if (name) exportedList.add(name);
5437
+ }
5438
+ }
5439
+ const scope = /^(.*?\/)?(?:src|app|lib|actions|server|utils)\//.exec(file.path)?.[1] ?? "";
5440
+ const routes = [];
5441
+ for (const body of functionBodies(code, pairs)) {
5442
+ const inline = startsWithDirective(source, body.start + 1, "use server");
5443
+ const declared = declarationOf(code, body, openers);
5444
+ const exported = fileLevel && declared !== null && (declared.exported || exportedList.has(declared.name));
5445
+ if (!inline && !exported) continue;
5446
+ const name = declared?.name ?? "anonymous";
5447
+ routes.push({
5448
+ file,
5449
+ framework: "next",
5450
+ url: `the server action ${name}`,
5451
+ scope,
5452
+ reachable: { start: body.start, end: body.end },
5453
+ action: name
5454
+ });
5455
+ }
5456
+ return routes;
5457
+ }
5458
+ function startsWithDirective(source, from, directive) {
5459
+ let i = from;
5460
+ while (i < source.length) {
4462
5461
  while (i < source.length && /\s/.test(source[i])) i++;
4463
5462
  const quote = source[i++];
4464
5463
  if (quote !== "'" && quote !== '"') return false;
@@ -5170,7 +6169,7 @@ var IMPORT_SPEC = /(?:from|import|require)\s*\(?\s*['"]([^'"]+)['"]/g;
5170
6169
  function normalizeSpec(spec, fromPath) {
5171
6170
  if (spec.startsWith(".")) {
5172
6171
  return {
5173
- key: moduleKey(posix.normalize(posix.join(posix.dirname(fromPath), spec))),
6172
+ key: moduleKey(posix2.normalize(posix2.join(posix2.dirname(fromPath), spec))),
5174
6173
  alias: false
5175
6174
  };
5176
6175
  }
@@ -5797,13 +6796,13 @@ function quotedValue(text) {
5797
6796
  function matcherEntry(text) {
5798
6797
  const trimmed = text.trimStart();
5799
6798
  if (trimmed.startsWith("{")) {
5800
- const object = sliceDelimited(trimmed, "{", "}");
5801
- if (object === null) return null;
5802
- if (topLevelPropertyStart(object, "has") !== null || topLevelPropertyStart(object, "missing") !== null) {
6799
+ const object2 = sliceDelimited(trimmed, "{", "}");
6800
+ if (object2 === null) return null;
6801
+ if (topLevelPropertyStart(object2, "has") !== null || topLevelPropertyStart(object2, "missing") !== null) {
5803
6802
  return { conditional: true };
5804
6803
  }
5805
- const source = topLevelPropertyStart(object, "source");
5806
- const pattern2 = source === null ? null : quotedValue(object.slice(source));
6804
+ const source = topLevelPropertyStart(object2, "source");
6805
+ const pattern2 = source === null ? null : quotedValue(object2.slice(source));
5807
6806
  return pattern2 === null ? null : { pattern: pattern2 };
5808
6807
  }
5809
6808
  const pattern = quotedValue(trimmed);
@@ -6018,13 +7017,6 @@ function passesOnlyAuthenticated(code) {
6018
7017
  }
6019
7018
  return false;
6020
7019
  }
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
7020
  function maskUnusedFunctions(code) {
6029
7021
  let out = code;
6030
7022
  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;
@@ -6052,7 +7044,7 @@ function maskUnusedFunctions(code) {
6052
7044
  return out;
6053
7045
  }
6054
7046
  function jwtVerifyRejects(code) {
6055
- if (!awaitedCall(code, /\bjwtVerify\s*\(/)) return false;
7047
+ if (!enforcedVerification(code, /\bjwtVerify\s*\(/)) return false;
6056
7048
  const handlers = [...code.matchAll(/\bcatch\s*(?:\([^()]*\))?\s*\{/g)].map((m) => {
6057
7049
  const open = m.index + m[0].length - 1;
6058
7050
  return code.slice(open, closingDelimiter(code, open, "{", "}") ?? open);
@@ -6079,7 +7071,8 @@ function catchDenies(code) {
6079
7071
  const tryOpen = m.index + m[0].length - 1;
6080
7072
  const tryClose = closingDelimiter(code, tryOpen, "{", "}");
6081
7073
  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;
7074
+ const calls = /\b\w*(?:auth|valid|verify|session|token|user|client|key|secret|permission|access)\w*\s*\(/i;
7075
+ if (!enforcedVerification(code, calls, /^(?:assertAuth(?:enticated)?|constructEvent)\b/i)) continue;
6083
7076
  const handler = /^\s*catch\s*(?:\([^()]*\))?\s*\{/.exec(code.slice(tryClose + 1));
6084
7077
  if (!handler) continue;
6085
7078
  const open = tryClose + handler[0].length;
@@ -6115,8 +7108,8 @@ function rejectsMissingCredential(code) {
6115
7108
  }
6116
7109
  function middlewareRejects(source) {
6117
7110
  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);
7111
+ if (hasConditionalAuthGuard(code) || rejectsMissingCredential(code) || passesOnlyAuthenticated(code) || rejectsSecretMismatch(code) || catchDenies(code) || enforcedVerification(code, AUTH_ENFORCING_CALL, /^(?:assertAuth(?:enticated)?|constructEvent)\b/i) || jwtVerifyRejects(code)) return true;
7112
+ return false;
6120
7113
  }
6121
7114
  function middlewareStatus(ref, files, depth = 0) {
6122
7115
  const composition = depth < 3 ? authCompositionOf(ref) : null;
@@ -6142,7 +7135,7 @@ function middlewareStatus(ref, files, depth = 0) {
6142
7135
  if (inner.status === "guard") return { status: "guard", name };
6143
7136
  }
6144
7137
  } 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}"`)));
7138
+ const plugin = FASTIFY_AUTH_DECORATORS.find(([pkg, decorator]) => decorator === name && registeredAuthPlugin(ref, pkg));
6146
7139
  if (plugin) return { status: "guard", name };
6147
7140
  }
6148
7141
  return { status: AUTH_HELPER_NAME.test(name) ? "unconfirmed" : "none", name };
@@ -6989,12 +7982,13 @@ function fixedTablesOf(code, source, pairs) {
6989
7982
  return result;
6990
7983
  }
6991
7984
  var InputFlow = class {
6992
- constructor(code, source, span, pairs, roles, fixedTables) {
7985
+ constructor(code, source, span, pairs, roles, fixedTables, definition) {
6993
7986
  this.code = code;
6994
7987
  this.source = source;
6995
7988
  this.span = span;
6996
7989
  this.pairs = pairs;
6997
7990
  this.fixedTables = fixedTables;
7991
+ this.definition = definition;
6998
7992
  for (const [name, at] of roles.values) this.parameters.set(name, { level: "direct", origin: at, names: /* @__PURE__ */ new Set([name]), ownUrl: false });
6999
7993
  this.sourcePattern = sourceRegex(roles);
7000
7994
  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;
@@ -7039,6 +8033,7 @@ var InputFlow = class {
7039
8033
  span;
7040
8034
  pairs;
7041
8035
  fixedTables;
8036
+ definition;
7042
8037
  parameters = /* @__PURE__ */ new Map();
7043
8038
  limited = false;
7044
8039
  assignmentList = /* @__PURE__ */ new Map();
@@ -7148,7 +8143,8 @@ var InputFlow = class {
7148
8143
  if (!new RegExp(`^(?:(?:await|new)\\s+)*${escaped}\\s*\\(`).test(expression)) return false;
7149
8144
  const cached = this.mirrors.get(callee);
7150
8145
  if (cached !== void 0) return cached;
7151
- const code = this.code;
8146
+ let code = this.code;
8147
+ let pairs = this.pairs;
7152
8148
  let params = "";
7153
8149
  let after = -1;
7154
8150
  const declared = new RegExp(`(?:^|[^\\w$.])(?:async\\s+)?function\\s*\\*?\\s*${escaped}\\s*\\(`).exec(code);
@@ -7159,21 +8155,31 @@ var InputFlow = class {
7159
8155
  after = match.index + match[0].length;
7160
8156
  } else if (match) {
7161
8157
  const open = match.index + match[0].length - 1;
7162
- const close = this.pairs.get(open);
8158
+ const close = pairs.get(open);
7163
8159
  if (close !== void 0) {
7164
8160
  params = code.slice(open + 1, close);
7165
8161
  after = close + 1;
7166
8162
  }
7167
8163
  }
8164
+ if (!match) {
8165
+ const target = this.definition?.(callee);
8166
+ if (target) {
8167
+ code = noiseMaskedOf(target.file);
8168
+ pairs = delimiterPairs(code);
8169
+ params = functionParams(target).join(",");
8170
+ after = code[target.start] === "{" ? target.start : code.indexOf("=>", target.start) + 2;
8171
+ if (after < target.start || after > target.end) after = -1;
8172
+ }
8173
+ }
7168
8174
  let result = false;
7169
8175
  if (after !== -1) {
7170
8176
  const names = params.split(",").flatMap((part) => part.replace(/[:=][\s\S]*$/, "").match(/[A-Za-z_$][\w$]*/g) ?? []);
7171
8177
  const head = /^\s*(?::[^={]{0,200})?(?:=>)?\s*/.exec(code.slice(after, after + 260));
7172
8178
  const start = after + head[0].length;
7173
- const end = (from, limit) => expressionEnd2(code, from, limit, this.pairs, () => {
8179
+ const end = (from, limit) => expressionEnd2(code, from, limit, pairs, () => {
7174
8180
  this.limited = true;
7175
8181
  });
7176
- const bodyEnd = this.pairs.get(start) ?? start;
8182
+ const bodyEnd = pairs.get(start) ?? start;
7177
8183
  const returns = code[start] === "{" ? [...code.slice(start, bodyEnd).matchAll(/\breturn\b/g)].map((m) => {
7178
8184
  const from = start + m.index + m[0].length;
7179
8185
  return code.slice(from, end(from, bodyEnd));
@@ -7454,12 +8460,13 @@ function lowerBound(values, value) {
7454
8460
  return low;
7455
8461
  }
7456
8462
  var HandlerFile = class {
7457
- constructor(code, source, pairs, lineStarts, candidates) {
8463
+ constructor(code, source, pairs, lineStarts, candidates, definition) {
7458
8464
  this.code = code;
7459
8465
  this.source = source;
7460
8466
  this.pairs = pairs;
7461
8467
  this.lineStarts = lineStarts;
7462
8468
  this.candidates = candidates;
8469
+ this.definition = definition;
7463
8470
  this.fixedTables = fixedTablesOf(code, source, pairs);
7464
8471
  }
7465
8472
  code;
@@ -7467,6 +8474,7 @@ var HandlerFile = class {
7467
8474
  pairs;
7468
8475
  lineStarts;
7469
8476
  candidates;
8477
+ definition;
7470
8478
  flows = /* @__PURE__ */ new Map();
7471
8479
  fixedTables;
7472
8480
  get limited() {
@@ -7483,7 +8491,7 @@ var HandlerFile = class {
7483
8491
  if (first >= positions.length || positions[first] >= body.end) continue;
7484
8492
  let flow = this.flows.get(candidate);
7485
8493
  if (!flow) {
7486
- flow = new InputFlow(this.code, this.source, { start: body.start, end: body.end }, this.pairs, candidate.roles, this.fixedTables);
8494
+ flow = new InputFlow(this.code, this.source, { start: body.start, end: body.end }, this.pairs, candidate.roles, this.fixedTables, this.definition);
7487
8495
  this.flows.set(candidate, flow);
7488
8496
  }
7489
8497
  result.push({ route: candidate.route, body, flow });
@@ -7528,7 +8536,14 @@ function handlerFileOf(file, files) {
7528
8536
  if (hasInput(roles)) candidates.push({ route: r, body, roles });
7529
8537
  }
7530
8538
  }
7531
- if (candidates.length > 0) result = new HandlerFile(code, source, pairs, lineStartsOf(file.content), candidates);
8539
+ if (candidates.length > 0) result = new HandlerFile(
8540
+ code,
8541
+ source,
8542
+ pairs,
8543
+ lineStartsOf(file.content),
8544
+ candidates,
8545
+ (name) => calleeDefinition(file, name, null, files)
8546
+ );
7532
8547
  }
7533
8548
  }
7534
8549
  handlerCache.set(file, result);
@@ -8050,140 +9065,6 @@ function check(ctx, kind) {
8050
9065
  var ssrfRule = { id: "ssrf/request-url", severity: "P1", check: (ctx) => check(ctx, "ssrf") };
8051
9066
  var redirectRule = { id: "redirect/open", severity: "P2", check: (ctx) => check(ctx, "redirect") };
8052
9067
 
8053
- // src/rules/verification.ts
8054
- var LocalVerification = class {
8055
- code;
8056
- source;
8057
- pairs;
8058
- bodies;
8059
- constructor(file) {
8060
- this.code = noiseMaskedOf(file);
8061
- this.source = commentsMaskedOf(file);
8062
- this.pairs = delimiterPairs(this.code);
8063
- this.bodies = functionBodies(this.code, this.pairs);
8064
- }
8065
- owner(at) {
8066
- return this.bodies.filter((b) => b.start < at && at < b.end).sort((a, b) => b.start - a.start)[0];
8067
- }
8068
- skipSpace(at) {
8069
- while (/\s/.test(this.code[at] ?? "")) at++;
8070
- return at;
8071
- }
8072
- endOf(at, limit = this.code.length) {
8073
- if (this.code[at] === "{") return (this.pairs.get(at) ?? limit - 1) + 1;
8074
- for (let i = at; i < limit; i++) {
8075
- if (/[;\n}]/.test(this.code[i])) return i;
8076
- const end = this.pairs.get(i);
8077
- if (end !== void 0) i = end;
8078
- }
8079
- return limit;
8080
- }
8081
- /**
8082
- * 块的顶层有无条件退出即算退出,前面可以有记录日志等语句;
8083
- * 嵌套分支、函数中的 return 以及无花括号 if/else/循环控制的 return 不算。
8084
- */
8085
- exits(from, to) {
8086
- let at = this.skipSpace(from);
8087
- if (this.code[at] !== "{") return /^(?:return|throw)\b|^(?:redirect|notFound)\s*\(/.test(this.code.slice(at, to));
8088
- const end = Math.min(to, this.pairs.get(at) ?? to);
8089
- at = this.skipSpace(at + 1);
8090
- while (at < end) {
8091
- if (/^(?:return|throw)\b|^(?:redirect|notFound)\s*\(/.test(this.code.slice(at, at + 32))) return true;
8092
- const next = this.statementEnd(at, end);
8093
- if (next <= at) return false;
8094
- at = this.skipSpace(next);
8095
- }
8096
- return false;
8097
- }
8098
- /** 跳过完整语句;分支、循环和函数内的退出不提升为外层退出。 */
8099
- statementEnd(at, limit, depth = 0) {
8100
- if (depth >= 64) return limit;
8101
- if (this.code[at] === "{") return (this.pairs.get(at) ?? limit - 1) + 1;
8102
- const control = /^(if|for(?:\s+await)?|while|with|switch)\s*\(/.exec(this.code.slice(at, at + 80));
8103
- if (control) {
8104
- const close = this.pairs.get(at + control[0].length - 1);
8105
- if (close === void 0) return limit;
8106
- let end = this.statementEnd(this.skipSpace(close + 1), limit, depth + 1);
8107
- const next = this.skipSpace(end);
8108
- if (control[1] === "if" && /^else\b/.test(this.code.slice(next, next + 5))) {
8109
- end = this.statementEnd(this.skipSpace(next + 4), limit, depth + 1);
8110
- }
8111
- return end;
8112
- }
8113
- if (/^(?:try|do)\b/.test(this.code.slice(at, at + 8))) return limit;
8114
- for (let i = at; i < limit; i++) {
8115
- if (this.code[i] === ";") return i + 1;
8116
- const close = this.pairs.get(i);
8117
- if (close !== void 0) {
8118
- i = close;
8119
- continue;
8120
- }
8121
- if (this.code[i] !== "\n") continue;
8122
- const before = this.code.slice(at, i).trimEnd();
8123
- const after = this.code.slice(i + 1, limit).trimStart();
8124
- if (!before || /[=+\-*/%&|^!?:,.([{<>]$/.test(before)) continue;
8125
- if (/^[.?+\-*/%&|^,:<>=([{`]/.test(after)) continue;
8126
- if (/\b(?:const|let|var|function|async|await|new|yield)$/.test(before)) continue;
8127
- return i + 1;
8128
- }
8129
- return limit;
8130
- }
8131
- /** 验证与使用须在同一函数,且不能依赖可选分支或被吞掉的异常。 */
8132
- enforcedBefore(from, to) {
8133
- const owner = this.owner(from);
8134
- if (from >= to || owner !== this.owner(to)) return false;
8135
- const start = owner ? owner.start + 1 : 0;
8136
- for (let i = start; i < from; i++) {
8137
- const conditional = /^(?:if|for|while|switch)\s*\(/.exec(this.code.slice(i, i + 32));
8138
- if (conditional && !/[\w$]/.test(this.code[i - 1] ?? "")) {
8139
- const close = this.pairs.get(i + conditional[0].length - 1);
8140
- if (close !== void 0) {
8141
- const body = this.skipSpace(close + 1);
8142
- const end2 = this.endOf(body);
8143
- if (from < end2 && to >= end2) return false;
8144
- }
8145
- }
8146
- const end = this.pairs.get(i);
8147
- if (end === void 0) continue;
8148
- if (end < from) {
8149
- i = end;
8150
- continue;
8151
- }
8152
- if (end >= to) continue;
8153
- if (this.code[i] !== "{" || !/\btry\s*$/.test(this.code.slice(Math.max(start, i - 16), i))) return false;
8154
- let next = this.skipSpace(end + 1);
8155
- if (/^catch\b/.test(this.code.slice(next))) {
8156
- next = this.skipSpace(next + 5);
8157
- if (this.code[next] === "(") next = this.skipSpace((this.pairs.get(next) ?? next) + 1);
8158
- const caughtEnd = this.endOf(next);
8159
- if (!this.exits(next, caughtEnd)) return false;
8160
- next = this.skipSpace(caughtEnd);
8161
- }
8162
- if (/^finally\b/.test(this.code.slice(next))) return false;
8163
- }
8164
- const boundary = Math.max(
8165
- start,
8166
- this.code.lastIndexOf(";", from - 1) + 1,
8167
- this.code.lastIndexOf("\n", from - 1) + 1,
8168
- this.code.lastIndexOf("{", from - 1) + 1
8169
- );
8170
- return !/[?]|&&|\|\|/.test(this.code.slice(boundary, from));
8171
- }
8172
- /** 找出当前函数中最后一次赋值;未知或条件覆盖同样会使旧证明失效。 */
8173
- assignment(name, at) {
8174
- const escaped = name.replace(/\$/g, "\\$");
8175
- const pattern = new RegExp(`(?<![\\w$.])(?:const\\s+|let\\s+|var\\s+)?${escaped}((?:\\s*\\.[\\w$]+|\\s*\\[[^\\]]*\\])*)\\s*(?::[^=;{}\\n]{0,100})?=(?![=>])`, "g");
8176
- let result = null;
8177
- for (const m of this.code.slice(0, at).matchAll(pattern)) {
8178
- if (this.owner(m.index) !== this.owner(at)) continue;
8179
- const from = this.skipSpace(m.index + m[0].length);
8180
- const end = this.endOf(from);
8181
- if (end <= at) result = { at: m.index, from, end, expression: this.code.slice(from, end).trim(), member: m[1].length > 0 };
8182
- }
8183
- return result;
8184
- }
8185
- };
8186
-
8187
9068
  // src/rules/session.ts
8188
9069
  var GET_SESSION = /\.auth\s*\.\s*getSession\s*\(/g;
8189
9070
  function verifiedSession(file, context, sessionAt, names) {
@@ -8321,7 +9202,7 @@ var READS_BODY = /(?<![\w$])(?:req|request)\s*(?:\.\s*raw\s*)?\.\s*(?:json|text|
8321
9202
  var SIGNATURE_HEADER = /['"]stripe-signature['"]/i;
8322
9203
  function middlewareVerifies(code) {
8323
9204
  const masked = maskUnusedFunctions(code);
8324
- const verifies = /\bconstructEvent\s*\(/.test(masked) || awaitedCall(masked, /\bconstructEventAsync\s*\(/);
9205
+ const verifies = enforcedVerification(masked, /\bconstructEvent(?:Async)?\s*\(/, /^constructEvent\s*\(/);
8325
9206
  if (!verifies) return false;
8326
9207
  for (const m of masked.matchAll(/\bcatch\s*(?:\([^()]*\))?\s*\{/g)) {
8327
9208
  const open = m.index + m[0].length - 1;
@@ -8347,6 +9228,8 @@ function verifiedByMiddleware(route, files) {
8347
9228
  const masked = maskUnusedFunctions(code);
8348
9229
  const calls = [...masked.matchAll(/(?<![\w$.])([A-Za-z_$][\w$]*)\s*(?:\.\s*([A-Za-z_$][\w$]*)\s*)?\(/g)].slice(0, 16);
8349
9230
  return calls.some((m) => {
9231
+ const call = new RegExp(`(?<![\\w$.])${m[1].replace(/\$/g, "\\$")}${m[2] ? `\\s*\\.\\s*${m[2].replace(/\$/g, "\\$")}` : ""}\\s*\\(`);
9232
+ if (!enforcedVerification(masked, call)) return false;
8350
9233
  const definition = calleeDefinition(file, m[1], m[2] ?? null, files);
8351
9234
  return definition !== null && middlewareVerifies(commentsMaskedOf(definition.file).slice(definition.start, definition.end + 1));
8352
9235
  });
@@ -8457,7 +9340,106 @@ function shouldRunRule(id, only, skip) {
8457
9340
  }
8458
9341
 
8459
9342
  // src/engine.ts
8460
- import { createHash as createHash2 } from "crypto";
9343
+ import { createHash as createHash3 } from "crypto";
9344
+
9345
+ // src/diagnostics.ts
9346
+ var ScanInputError = class extends TypeError {
9347
+ code = "INVALID_ARGUMENT";
9348
+ };
9349
+ function diagnosticCodeOf(error) {
9350
+ if (error.kind === "crashed") return "RULE_EXECUTION_FAILED";
9351
+ if (["engine/openapi-routes", "engine/router-factories", "engine/route-registration"].includes(error.ruleId)) return "ROUTE_UNRESOLVED";
9352
+ if (error.ruleId === "engine/findings-limit") return "FINDINGS_LIMIT";
9353
+ if (error.ruleId.startsWith("gitleak/")) return "GIT_CHECK_INCOMPLETE";
9354
+ return "ANALYSIS_INCOMPLETE";
9355
+ }
9356
+
9357
+ // src/scan-control.ts
9358
+ var ScanCancelledError = class extends Error {
9359
+ code = "SCAN_CANCELLED";
9360
+ constructor() {
9361
+ super("Scan cancelled.");
9362
+ this.name = "AbortError";
9363
+ }
9364
+ };
9365
+ var ScanProgressError = class extends Error {
9366
+ code = "PROGRESS_CALLBACK_FAILED";
9367
+ constructor(cause) {
9368
+ super("Scan progress callback failed.", { cause });
9369
+ }
9370
+ };
9371
+ function checkScanCancelled(signal) {
9372
+ if (signal?.aborted) throw new ScanCancelledError();
9373
+ }
9374
+ async function waitForProgress(value, signal) {
9375
+ if (!signal) {
9376
+ await value;
9377
+ return;
9378
+ }
9379
+ let onAbort = () => {
9380
+ };
9381
+ const aborted = new Promise((_, reject) => {
9382
+ onAbort = () => reject(new ScanCancelledError());
9383
+ signal.addEventListener("abort", onAbort, { once: true });
9384
+ if (signal.aborted) onAbort();
9385
+ });
9386
+ try {
9387
+ await Promise.race([Promise.resolve(value), aborted]);
9388
+ } finally {
9389
+ signal.removeEventListener("abort", onAbort);
9390
+ }
9391
+ }
9392
+ function createScanControl(options) {
9393
+ const { signal, onProgress } = options;
9394
+ return {
9395
+ active: signal !== void 0 || onProgress !== void 0,
9396
+ check: () => checkScanCancelled(signal),
9397
+ async checkpoint(progress) {
9398
+ checkScanCancelled(signal);
9399
+ await new Promise((resolve3) => setImmediate(resolve3));
9400
+ checkScanCancelled(signal);
9401
+ if (onProgress) {
9402
+ try {
9403
+ await waitForProgress(onProgress(Object.freeze({ ...progress })), signal);
9404
+ } catch (error) {
9405
+ if (signal?.aborted || error instanceof ScanCancelledError) throw new ScanCancelledError();
9406
+ throw new ScanProgressError(error);
9407
+ }
9408
+ }
9409
+ checkScanCancelled(signal);
9410
+ }
9411
+ };
9412
+ }
9413
+
9414
+ // src/exclusions.ts
9415
+ var MAX_EXCLUSIONS = 64;
9416
+ var MAX_EXCLUSION_LENGTH = 512;
9417
+ var SEGMENT = String.raw`[^\s/\\](?:[^/\\]*[^\s/\\])?`;
9418
+ var EXCLUSION_PATH_PATTERN = String.raw`^(?![!])(?!.*[:*?\[\]{}\u0000-\u001f\u007f-\u009f\u2028\u2029])(?!(?:.*[/\\])?\.{1,2}(?:[/\\]|$))` + SEGMENT + String.raw`(?:[/\\]` + SEGMENT + String.raw`)*[/\\]?$`;
9419
+ var PATH = new RegExp(EXCLUSION_PATH_PATTERN);
9420
+ function isExclusionPath(value) {
9421
+ return typeof value === "string" && value.length > 0 && value.length <= MAX_EXCLUSION_LENGTH && PATH.test(value);
9422
+ }
9423
+ function createExclusions(values = []) {
9424
+ if (values.length > MAX_EXCLUSIONS || !values.every(isExclusionPath)) throw new TypeError("Exclusions must be at most 64 literal relative paths without traversal, wildcards or control characters.");
9425
+ const requested = [...new Set(values.map((value) => value.replace(/\\/g, "/").replace(/\/$/, "")))];
9426
+ const matched = /* @__PURE__ */ new Set();
9427
+ return {
9428
+ requested,
9429
+ matches(path) {
9430
+ const normalized = path.replace(/\\/g, "/").replace(/\/$/, "");
9431
+ let excluded = false;
9432
+ for (const prefix of requested) if (normalized === prefix || normalized.startsWith(prefix + "/")) {
9433
+ matched.add(prefix);
9434
+ excluded = true;
9435
+ }
9436
+ return excluded;
9437
+ },
9438
+ matched: () => [...matched].sort()
9439
+ };
9440
+ }
9441
+
9442
+ // src/engine.ts
8461
9443
  var SEVERITY_ORDER = { P0: 0, P1: 1, P2: 2 };
8462
9444
  var CONFIDENCE_ORDER = { certain: 0, likely: 1 };
8463
9445
  function dedupe(findings) {
@@ -8486,7 +9468,7 @@ function sanitize(findings, files) {
8486
9468
  const file = f.file === null ? void 0 : byPath.get(f.file);
8487
9469
  const source = f.line === null ? file?.content : file?.lines[f.line - 1];
8488
9470
  return source === void 0 ? {} : {
8489
- sourceFingerprint: createHash2("sha256").update(source.trim(), "utf8").digest("hex")
9471
+ sourceFingerprint: createHash3("sha256").update(source.trim(), "utf8").digest("hex")
8490
9472
  };
8491
9473
  };
8492
9474
  return findings.map((f) => ({
@@ -8521,9 +9503,9 @@ function sanitizeSkippedForOutput(items) {
8521
9503
  }));
8522
9504
  }
8523
9505
  function downgradeExampleContext(findings, files) {
8524
- const examples = new Set(files.filter((f) => f.isExampleContext).map((f) => f.path));
9506
+ const examples2 = new Set(files.filter((f) => f.isExampleContext).map((f) => f.path));
8525
9507
  return findings.map(
8526
- (f) => f.file !== null && examples.has(f.file) ? { ...f, confidence: "likely" } : f
9508
+ (f) => f.file !== null && examples2.has(f.file) ? { ...f, confidence: "likely" } : f
8527
9509
  );
8528
9510
  }
8529
9511
  function suppressIgnoredLines(findings, files) {
@@ -8576,20 +9558,36 @@ function sortFindings(findings) {
8576
9558
  }
8577
9559
  async function scan(root, options = {}) {
8578
9560
  const started = Date.now();
9561
+ const exclusions = createExclusions(options.exclude);
9562
+ const control = createScanControl(options);
9563
+ control.check();
9564
+ const progress = {
9565
+ phase: "discovery",
9566
+ filesCompleted: 0,
9567
+ filesTotal: null,
9568
+ projectRulesCompleted: 0,
9569
+ projectRulesTotal: null
9570
+ };
9571
+ if (control.active) await control.checkpoint(progress);
8579
9572
  const gitExecutable = resolveGitExecutable(root);
8580
9573
  const git2 = detectGitRepo(root, gitExecutable);
8581
9574
  const honorIgnoreMarkers = options.honorIgnoreMarkers !== false;
8582
- const { files, skipped, ignored, vendored } = collectFiles(root, git2 === "repo", gitExecutable, {}, honorIgnoreMarkers);
9575
+ const { files, skipped, ignored, vendored } = collectFiles(root, git2 === "repo", gitExecutable, {}, honorIgnoreMarkers, exclusions.matches);
8583
9576
  const findings = [];
8584
9577
  const errors = [];
8585
9578
  const fileRules = FILE_RULES.filter((rule2) => shouldRunRule(rule2.id, options.only ?? [], options.skip ?? []));
8586
9579
  const projectRules = PROJECT_RULES.filter((rule2) => shouldRunRule(rule2.id, options.only ?? [], options.skip ?? []));
9580
+ progress.filesTotal = files.length;
9581
+ progress.projectRulesTotal = projectRules.length;
9582
+ progress.phase = "files";
9583
+ if (control.active) await control.checkpoint(progress);
8587
9584
  const incompleteSeen = /* @__PURE__ */ new Set();
8588
9585
  const ctx = {
8589
9586
  root,
8590
9587
  files,
8591
9588
  git: git2,
8592
9589
  gitExecutable,
9590
+ excludePath: exclusions.matches,
8593
9591
  // 统一记录不完整状态,避免重复提示。
8594
9592
  reportIncomplete: (ruleId, message) => {
8595
9593
  if (incompleteSeen.has(`${ruleId} ${message}`)) return;
@@ -8598,6 +9596,7 @@ async function scan(root, options = {}) {
8598
9596
  }
8599
9597
  };
8600
9598
  for (const file of files) {
9599
+ control.check();
8601
9600
  for (const rule2 of fileRules) {
8602
9601
  try {
8603
9602
  if (!rule2.appliesTo(file)) continue;
@@ -8606,13 +9605,29 @@ async function scan(root, options = {}) {
8606
9605
  errors.push({ ruleId: rule2.id, file: file.path, message: messageOf(err), kind: "crashed" });
8607
9606
  }
8608
9607
  }
9608
+ progress.filesCompleted++;
9609
+ if (control.active && progress.filesCompleted % 32 === 0) await control.checkpoint(progress);
8609
9610
  }
9611
+ if (control.active) await control.checkpoint(progress);
8610
9612
  for (const rule2 of projectRules) {
9613
+ progress.phase = rule2.id.startsWith("gitleak/") ? "history" : "project";
9614
+ if (control.active) await control.checkpoint(progress);
8611
9615
  try {
8612
9616
  findings.push(...await rule2.check(ctx));
8613
9617
  } catch (err) {
8614
9618
  errors.push({ ruleId: rule2.id, file: null, message: messageOf(err), kind: "crashed" });
8615
9619
  }
9620
+ control.check();
9621
+ progress.projectRulesCompleted++;
9622
+ }
9623
+ progress.phase = "finalize";
9624
+ if (control.active) await control.checkpoint(progress);
9625
+ if ([...fileRules, ...projectRules].some((rule2) => /^(?:api|auth|injection|ssrf|redirect|webhook)\//.test(rule2.id))) {
9626
+ try {
9627
+ reportOpenapiBatchCoverage(ctx);
9628
+ } catch (err) {
9629
+ errors.push({ ruleId: "engine/openapi-routes", file: null, message: messageOf(err), kind: "crashed" });
9630
+ }
8616
9631
  }
8617
9632
  const deduped = dedupe(downgradeExampleContext(findings, files));
8618
9633
  const { kept, ignored: ignoredFindings } = honorIgnoreMarkers ? suppressIgnoredLines(deduped, files) : { kept: deduped, ignored: [] };
@@ -8628,12 +9643,14 @@ async function scan(root, options = {}) {
8628
9643
  );
8629
9644
  return false;
8630
9645
  });
8631
- return {
9646
+ const result = {
9647
+ ...exclusions.requested.length ? { exclusions: { requested: exclusions.requested.map(clean), matched: exclusions.matched().map(clean) } } : {},
8632
9648
  findings: sanitize(bounded, files),
8633
9649
  filesScanned: files.length,
8634
9650
  durationMs: Date.now() - started,
8635
9651
  errors: errors.map((e) => ({
8636
9652
  ...e,
9653
+ code: e.code ?? diagnosticCodeOf(e),
8637
9654
  file: e.file === null ? null : clean(e.file),
8638
9655
  message: clean(e.message)
8639
9656
  })),
@@ -8651,14 +9668,113 @@ async function scan(root, options = {}) {
8651
9668
  // 主动忽略不影响完整性;错误、跳过或零文件扫描均标记为未完成。
8652
9669
  partial: errors.length > 0 || skipped.length > 0 || files.length === 0
8653
9670
  };
9671
+ progress.phase = "complete";
9672
+ if (control.active) await control.checkpoint(progress);
9673
+ return result;
8654
9674
  }
8655
9675
  function messageOf(err) {
8656
9676
  if (err instanceof Error) return err.message;
8657
9677
  return String(err);
8658
9678
  }
8659
9679
 
9680
+ // src/rules/examples.ts
9681
+ var publicEnv = {
9682
+ context: "Server-only environment value",
9683
+ before: "NEXT_PUBLIC_SERVER_TOKEN=<private value>",
9684
+ after: "SERVER_TOKEN=<server-only value>",
9685
+ limitation: "Use the value only in server code. Rotate any exposed credential; renaming does not revoke it."
9686
+ };
9687
+ var auth = {
9688
+ context: "Supabase route with a request-scoped session client and a separate admin client",
9689
+ before: "await admin.from('items').delete().eq('id', id);",
9690
+ after: "const { data: { user }, error } = await sessionClient.auth.getUser();\nif (error || !user) return new Response(null, { status: 401 });\nif (!await canDelete(user.id, id)) return new Response(null, { status: 403 });\nawait admin.from('items').delete().eq('id', id);",
9691
+ limitation: "Bind sessionClient to the caller. Implement canDelete with actual resource ownership or permissions; authentication alone is insufficient."
9692
+ };
9693
+ var firestore = {
9694
+ context: "Firestore document ownership",
9695
+ before: "match /users/{uid} { allow read, write: if true; }",
9696
+ after: "match /users/{uid} {\n allow read, write: if request.auth != null && request.auth.uid == uid;\n}",
9697
+ limitation: "Firestore syntax only. Review field validation and overlapping rules; adapt ownership to the data model."
9698
+ };
9699
+ var cors = {
9700
+ context: "Express cors middleware with credentialed access from one trusted site",
9701
+ before: "app.use(cors({ origin: true, credentials: true }));",
9702
+ after: "app.use(cors({ origin: 'https://app.example.com', credentials: true }));",
9703
+ limitation: "Replace the example origin with an exact trusted origin. CORS does not replace authentication, authorization or CSRF controls."
9704
+ };
9705
+ var examples = {
9706
+ "api/admin-db-access-without-auth": auth,
9707
+ "api/db-write-without-auth": auth,
9708
+ "exposure/private-name-in-public-env": publicEnv,
9709
+ "exposure/secret-in-public-env": publicEnv,
9710
+ "exposure/supabase-service-role-in-client": {
9711
+ ...publicEnv,
9712
+ context: "Supabase admin credential",
9713
+ limitation: "Never ship an admin key to the client. Use a publishable/anon key with appropriate RLS for client access; rotate exposed admin credentials."
9714
+ },
9715
+ "cors/reflected-origin-with-credentials": cors,
9716
+ "cors/wildcard-with-credentials": { ...cors, before: "app.use(cors({ origin: '*', credentials: true }));" },
9717
+ "firebase/open-rules": firestore,
9718
+ "firebase/test-mode-rules": { ...firestore, before: "allow read, write: if request.time < timestamp.date(2030, 1, 1);" },
9719
+ "supabase/rls-not-enabled": {
9720
+ context: "Supabase table with a UUID owner_id column",
9721
+ before: "CREATE TABLE public.notes (id uuid PRIMARY KEY, owner_id uuid NOT NULL, body text);",
9722
+ after: "ALTER TABLE public.notes ENABLE ROW LEVEL SECURITY;\nCREATE POLICY owner_access ON public.notes TO authenticated\nUSING ((SELECT auth.uid()) = owner_id)\nWITH CHECK ((SELECT auth.uid()) = owner_id);",
9723
+ limitation: "Apply ownership checks appropriate to the application. Review grants and other policies; service-role access bypasses RLS."
9724
+ },
9725
+ "auth/unverified-session": {
9726
+ context: "Server-side Supabase identity verification",
9727
+ before: "const { data: { session } } = await supabase.auth.getSession();\nif (!session) return new Response(null, { status: 401 });",
9728
+ after: "const { data: { user }, error } = await supabase.auth.getUser();\nif (error || !user) return new Response(null, { status: 401 });",
9729
+ limitation: "Use a client bound to the current request. Verified identity still requires resource-specific authorization."
9730
+ },
9731
+ "injection/sql": {
9732
+ context: "PostgreSQL driver with bound parameters",
9733
+ before: 'await db.query("SELECT * FROM users WHERE id = " + id);',
9734
+ after: "await db.query('SELECT * FROM users WHERE id = $1', [id]);",
9735
+ limitation: "Use the parameter syntax of your actual driver. Parameters do not substitute table names or authorize access."
9736
+ },
9737
+ "injection/command": {
9738
+ context: "A fixed executable with separately validated arguments",
9739
+ before: "exec('tool --input ' + requestValue);",
9740
+ after: "execFile(TRUSTED_EXECUTABLE, ['--input', validatedValue], { shell: false });",
9741
+ limitation: "Set TRUSTED_EXECUTABLE to a trusted absolute path. Validate argument meaning and filesystem access; never let the caller choose the executable."
9742
+ },
9743
+ "redirect/open": {
9744
+ context: "Named destinations on a fixed application origin",
9745
+ before: "return Response.redirect(requestedUrl);",
9746
+ after: "const path = destination === 'settings' ? '/settings' : '/dashboard';\nreturn Response.redirect(new URL(path, APP_ORIGIN));",
9747
+ limitation: "APP_ORIGIN must be trusted configuration, not a request header. Do not replace the fixed paths with caller-controlled URLs."
9748
+ },
9749
+ "ssrf/request-url": {
9750
+ context: "Named server resources instead of caller-provided hosts",
9751
+ before: "return fetch(requestedUrl);",
9752
+ after: "if (resource !== 'status') return new Response(null, { status: 400 });\nreturn fetch('https://api.example.com/status', { redirect: 'error' });",
9753
+ limitation: "Use a destination you control. Review DNS and egress restrictions; a URL allowlist alone is not a complete SSRF defense."
9754
+ },
9755
+ "webhook/unverified-signature": {
9756
+ context: "Stripe webhook using the original request-body bytes",
9757
+ before: "const event = JSON.parse(rawBody);\nawait handle(event);",
9758
+ after: "let event;\ntry { event = stripe.webhooks.constructEvent(rawBody, signature, SERVER_SIGNING_SECRET); }\ncatch { return new Response(null, { status: 400 }); }\nawait handle(event);",
9759
+ limitation: "Preserve raw bytes and use the endpoint signing secret. Verification must finish before side effects; implement replay/idempotency handling separately."
9760
+ }
9761
+ };
9762
+ function fixExampleFor(ruleId) {
9763
+ if (SECRET_PATTERNS.some((pattern) => !pattern.publicByDesign && ruleId === `secrets/hardcoded/${pattern.id}`)) return {
9764
+ context: "A credential used only by a server runtime",
9765
+ before: "const apiKey = '<private value>';",
9766
+ after: "const apiKey = process.env.SERVICE_API_KEY;\nif (!apiKey) throw new Error('Server credential is missing');",
9767
+ limitation: "Use trusted server-side configuration, never a public/client environment prefix. Rotate the old credential and review history and deployed bundles."
9768
+ };
9769
+ if (!Object.hasOwn(examples, ruleId)) return null;
9770
+ return { ...examples[ruleId] };
9771
+ }
9772
+
8660
9773
  // src/rules/catalog.ts
8661
- var rule = (id, name, severity, confidence, scope, limitation) => ({ id, name, severity, confidence, scope, limitation, reportsFindings: true });
9774
+ var rule = (id, name, severity, confidence, scope, limitation) => {
9775
+ const example = fixExampleFor(id);
9776
+ return { id, name, severity, confidence, scope, limitation, reportsFindings: true, ...example ? { example } : {} };
9777
+ };
8662
9778
  var RULE_CATALOG = [
8663
9779
  rule(
8664
9780
  "api/admin-db-access-without-auth",
@@ -8673,8 +9789,8 @@ var RULE_CATALOG = [
8673
9789
  "Database write without a recognized auth guard",
8674
9790
  "P1",
8675
9791
  "likely",
8676
- "Server routes and Next.js Server Functions in Next.js, SvelteKit (endpoints and form actions), Nuxt, Remix/React Router, Astro, Express, Hono, and Fastify.",
8677
- "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."
9792
+ "Server routes and Next.js Server Functions in Next.js, SvelteKit (endpoints and form actions), Nuxt, Remix/React Router, Astro, Express, Hono (including OpenAPIHono.openapi and openapiRoutes), and Fastify.",
9793
+ "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. OpenAPI configuration follows constants and static ESM imports/re-exports within eight resolution steps. Unresolved configuration does not suppress findings; unresolved batch entries or handlers mark coverage incomplete. OpenAPI security declarations are not authentication, and arbitrary module side effects are not modelled."
8678
9794
  ),
8679
9795
  rule(
8680
9796
  "auth/unverified-session",
@@ -8850,10 +9966,7 @@ function verdictOf(findings) {
8850
9966
  return { blocking, minor, unsure };
8851
9967
  }
8852
9968
 
8853
- // src/index.ts
8854
- function listRules() {
8855
- return RULE_CATALOG.map((rule2) => ({ ...rule2 }));
8856
- }
9969
+ // src/summary.ts
8857
9970
  function summarize(result) {
8858
9971
  const verdict = verdictOf(result.findings);
8859
9972
  const findings = result.changeView?.totalFindings ?? result.findings.length;
@@ -8867,37 +9980,69 @@ function summarize(result) {
8867
9980
  exitCode: blocking > 0 ? 1 : findings > 0 ? 2 : partial ? 3 : 0
8868
9981
  };
8869
9982
  }
9983
+
9984
+ // src/build-info.ts
9985
+ var VERSION = true ? "0.8.0-rc.1" : "0.0.0-dev";
9986
+ var identity = typeof define_CANSHIP_BUILD_INFO_default === "object" ? define_CANSHIP_BUILD_INFO_default : { version: VERSION, channel: "development", revision: null, dirty: null };
9987
+ function getBuildInfo() {
9988
+ return { ...identity };
9989
+ }
9990
+ function getCapabilities() {
9991
+ return {
9992
+ staticScan: { network: false, projectCodeExecution: false, projectWrites: false },
9993
+ fileOutputs: "explicit-only",
9994
+ onlineValidation: true,
9995
+ onlineValidationPolicy: { cliOnly: true, defaultEnabled: false, confirmation: "plan-digest", credentials: false, responseBodiesRetained: false }
9996
+ };
9997
+ }
9998
+
9999
+ // src/index.ts
10000
+ function listRules() {
10001
+ return RULE_CATALOG.map((rule2) => ({ ...rule2, ...rule2.example ? { example: { ...rule2.example } } : {} }));
10002
+ }
8870
10003
  async function scan2(root, options = {}) {
8871
- if (typeof root !== "string" || root.length === 0) throw new TypeError("Scan root must be a non-empty path.");
10004
+ if (typeof root !== "string" || root.length === 0) throw new ScanInputError("Scan root must be a non-empty path.");
8872
10005
  if (options === null || typeof options !== "object" || Array.isArray(options)) {
8873
- throw new TypeError("Scan options must be an object.");
10006
+ throw new ScanInputError("Scan options must be an object.");
8874
10007
  }
8875
- const allowed = /* @__PURE__ */ new Set(["only", "skip", "honorIgnoreMarkers", "noExcerpts"]);
8876
- if (Object.keys(options).some((key) => !allowed.has(key))) throw new TypeError("Unknown scan option.");
10008
+ const allowed = /* @__PURE__ */ new Set(["only", "skip", "honorIgnoreMarkers", "noExcerpts", "signal", "onProgress", "exclude"]);
10009
+ if (Object.keys(options).some((key) => !allowed.has(key))) throw new ScanInputError("Unknown scan option.");
10010
+ if (options.signal !== void 0 && (options.signal === null || typeof options.signal !== "object" || typeof options.signal.aborted !== "boolean" || typeof options.signal.addEventListener !== "function" || typeof options.signal.removeEventListener !== "function")) throw new ScanInputError("Expected an AbortSignal.");
10011
+ if (options.onProgress !== void 0 && typeof options.onProgress !== "function") throw new ScanInputError("Expected a progress callback.");
10012
+ if (options.exclude !== void 0 && (!Array.isArray(options.exclude) || options.exclude.length > MAX_EXCLUSIONS || !options.exclude.every(isExclusionPath))) throw new ScanInputError("Expected at most 64 literal relative exclusion paths.");
8877
10013
  for (const name of ["honorIgnoreMarkers", "noExcerpts"]) {
8878
- if (options[name] !== void 0 && typeof options[name] !== "boolean") throw new TypeError("Expected a boolean scan option.");
10014
+ if (options[name] !== void 0 && typeof options[name] !== "boolean") throw new ScanInputError("Expected a boolean scan option.");
8879
10015
  }
8880
10016
  for (const name of ["only", "skip"]) {
8881
10017
  const values = options[name];
8882
10018
  if (values !== void 0 && (!Array.isArray(values) || [...values].some((value) => typeof value !== "string" || !isKnownSelector(value)))) {
8883
- throw new TypeError("Rule selectors must be an array of known IDs or namespaces.");
10019
+ throw new ScanInputError("Rule selectors must be an array of known IDs or namespaces.");
8884
10020
  }
8885
10021
  }
8886
- if (options.only?.length && options.skip?.length) throw new TypeError("only and skip are mutually exclusive.");
10022
+ if (options.only?.length && options.skip?.length) throw new ScanInputError("only and skip are mutually exclusive.");
10023
+ checkScanCancelled(options.signal);
8887
10024
  const directory = resolve2(root);
8888
10025
  try {
8889
10026
  if (!statSync3(directory).isDirectory()) throw new Error("not a directory");
8890
10027
  } catch {
8891
- throw new Error("Scan root must be an accessible directory.");
10028
+ throw Object.assign(new Error("Scan root must be an accessible directory."), { code: "SCAN_ROOT_UNAVAILABLE" });
8892
10029
  }
10030
+ const omitExcerpts = options.noExcerpts === true;
8893
10031
  const result = await scan(directory, {
8894
10032
  only: [...options.only ?? []],
8895
10033
  skip: [...options.skip ?? []],
8896
- honorIgnoreMarkers: options.honorIgnoreMarkers ?? true
10034
+ honorIgnoreMarkers: options.honorIgnoreMarkers ?? true,
10035
+ exclude: [...options.exclude ?? []],
10036
+ ...options.signal === void 0 ? {} : { signal: options.signal },
10037
+ ...options.onProgress === void 0 ? {} : { onProgress: options.onProgress }
8897
10038
  });
8898
- return options.noExcerpts ? { ...result, findings: result.findings.map((finding) => ({ ...finding, excerpt: null })) } : result;
10039
+ return omitExcerpts ? { ...result, findings: result.findings.map((finding) => ({ ...finding, excerpt: null })) } : result;
8899
10040
  }
8900
10041
  export {
10042
+ ScanCancelledError,
10043
+ ScanProgressError,
10044
+ getBuildInfo,
10045
+ getCapabilities,
8901
10046
  listRules,
8902
10047
  scan2 as scan,
8903
10048
  summarize