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