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