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/README-zh-CN.md +61 -124
- package/README.md +62 -125
- package/dist/cli.js +4050 -816
- package/dist/index.d.ts +78 -8
- package/dist/index.js +1450 -305
- package/docs/framework-support-zh-CN.md +95 -0
- package/docs/framework-support.md +95 -0
- package/docs/reference-zh-CN.md +221 -0
- package/docs/reference.md +221 -0
- package/package.json +13 -2
- package/schemas/config-v1.schema.json +114 -0
- package/schemas/scan-report-v1.schema.json +19 -1
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
|
-
|
|
799
|
+
if (excludePath?.(rel)) continue;
|
|
800
|
+
let metadata2;
|
|
797
801
|
try {
|
|
798
|
-
|
|
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 (
|
|
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 (
|
|
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 (!
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
|
3489
|
-
|
|
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])
|
|
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))
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
5014
|
+
if (!/\.[mc]?[jt]sx?$/.test(file.path)) continue;
|
|
4224
5015
|
const a = analyse(file);
|
|
4225
5016
|
const names = frameworkNamesOf(a);
|
|
4226
|
-
|
|
4227
|
-
for (const instance of
|
|
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
|
-
|
|
4260
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
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
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
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
|
-
|
|
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/
|
|
4403
|
-
var
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
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
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
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
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
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(
|
|
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
|
|
5801
|
-
if (
|
|
5802
|
-
if (topLevelPropertyStart(
|
|
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(
|
|
5806
|
-
const pattern2 = source === null ? null : quotedValue(
|
|
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 (!
|
|
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
|
-
|
|
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
|
|
6119
|
-
return
|
|
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 &&
|
|
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
|
-
|
|
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 =
|
|
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,
|
|
8179
|
+
const end = (from, limit) => expressionEnd2(code, from, limit, pairs, () => {
|
|
7174
8180
|
this.limited = true;
|
|
7175
8181
|
});
|
|
7176
|
-
const bodyEnd =
|
|
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(
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
9506
|
+
const examples2 = new Set(files.filter((f) => f.isExampleContext).map((f) => f.path));
|
|
8525
9507
|
return findings.map(
|
|
8526
|
-
(f) => f.file !== null &&
|
|
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
|
-
|
|
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) =>
|
|
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.
|
|
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/
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|