canship 0.7.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -3180,9 +3180,32 @@ function synchronousLocal(file, expr) {
3180
3180
  const code = noiseMaskedOf(file);
3181
3181
  const escaped = name.replace(/\$/g, "\\$");
3182
3182
  const declared = new RegExp(`(?:^|[^\\w$])(async\\s+)?function\\s*\\*?\\s*${escaped}\\s*\\(`).exec(code);
3183
- if (declared) return !declared[1];
3183
+ if (declared?.[1]) return false;
3184
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];
3185
+ if (!declared && (!assigned || assigned[1])) return false;
3186
+ const declaration = declared ?? assigned;
3187
+ const pairedEnd = (open, left, right) => {
3188
+ let depth = 0;
3189
+ for (let i = open; i < Math.min(code.length, open + 65536); i++) {
3190
+ if (code[i] === left) depth++;
3191
+ else if (code[i] === right && --depth === 0) return i;
3192
+ }
3193
+ return -1;
3194
+ };
3195
+ let start = declaration.index + declaration[0].length;
3196
+ if (declared) start = pairedEnd(start - 1, "(", ")") + 1;
3197
+ else if (/function\b$/.test(declaration[0])) {
3198
+ const open = code.indexOf("(", start);
3199
+ start = open < 0 ? 0 : pairedEnd(open, "(", ")") + 1;
3200
+ }
3201
+ if (start === 0) return false;
3202
+ start += /^\s*(?::[\w\s.<>,[\]|?]+)?\s*/.exec(code.slice(start))[0].length;
3203
+ const close = code[start] === "{" ? pairedEnd(start, "{", "}") : -1;
3204
+ if (code[start] === "{" && close < 0) return false;
3205
+ const body = close >= 0 ? code.slice(start, close + 1) : `return ${code.slice(start).split(/[;\n]/)[0] ?? ""}`;
3206
+ if (/\b(?:Promise|then|await|new|function|async)\b|=>|\.\.\./.test(body)) return false;
3207
+ if ([...body.matchAll(/([\w$]+)\s*\(/g)].some((m) => !/^(?:if|switch)$/.test(m[1]))) return false;
3208
+ return /\breturn\s+(?:null|false|true|\{|[\w$]+\s*\?)/.test(body);
3186
3209
  }
3187
3210
  var limited = /* @__PURE__ */ new WeakSet();
3188
3211
  var cache2 = /* @__PURE__ */ new WeakMap();
@@ -3618,11 +3641,13 @@ function usePrefixes(a, arg) {
3618
3641
  if (arg.text.startsWith("[")) {
3619
3642
  const close = a.pairs.get(arg.at);
3620
3643
  const items = close === void 0 ? [] : argsOf(a, arg.at, close);
3621
- if (items.length === 0 || !items.every((item) => /^['"`]/.test(item.source))) return null;
3644
+ if (items.length === 0 || !items.some((item) => /^['"`]/.test(item.source))) return null;
3622
3645
  return items.map((item) => pathOf(a, item)).filter((path) => path !== null);
3623
3646
  }
3624
3647
  const prefix = pathOf(a, arg);
3625
- return prefix === null ? null : [prefix];
3648
+ if (prefix !== null) return [prefix];
3649
+ if (/^['"`/]|^new\s+RegExp\b|^(?:process\s*\.\s*env|import\s*\.\s*meta\s*\.\s*env|(?:[\w$]+\s*\.\s*)?env)\s*[.[]/.test(arg.source)) return [];
3650
+ return null;
3626
3651
  }
3627
3652
  function pathOf(a, arg) {
3628
3653
  if (arg.text.startsWith("[")) return null;
@@ -4088,7 +4113,7 @@ function opsOf(site) {
4088
4113
  return result;
4089
4114
  }
4090
4115
  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);
4116
+ const args = opsOf(site).uses.filter((use) => !use.mount && use.at < before && (use.prefix === null || site.framework !== "hono" && use.prefix === "/" || /^\/?\*$/.test(use.prefix))).flatMap((use) => use.args).filter((arg) => site.framework !== "express" || mountedFile(site.a, arg, files) === null);
4092
4117
  return toRefs(site.a, args);
4093
4118
  }
4094
4119
  function passedSites(site, files) {
@@ -4276,8 +4301,7 @@ function serverIndex(files) {
4276
4301
  };
4277
4302
  add2(handler, callbacks);
4278
4303
  callbacks.forEach((callback, index2) => {
4279
- if (!FUNCTION_EXPRESSION.test(callback.text)) return;
4280
- const range = functionAt(a, callback.at, callback.at + callback.text.length);
4304
+ const range = handlerOf(a, callback, files);
4281
4305
  if (range) add2(range, callbacks.slice(0, index2));
4282
4306
  });
4283
4307
  }
@@ -4330,25 +4354,37 @@ function serverRoutesOf(file, files) {
4330
4354
  const actions = serverActionRoutes(file);
4331
4355
  return actions.length ? actions : nodeRoutesFor(file, files);
4332
4356
  }
4333
- var decoratorCache = /* @__PURE__ */ new WeakMap();
4334
- function decoratorOf(name, files) {
4335
- let cache3 = decoratorCache.get(files);
4357
+ function decoratorOf(ref, receiver, name, files) {
4358
+ const a = analyse(ref.file);
4359
+ const owner = (at) => a.bodies.filter((b) => b.start < at && at < b.end).sort((x, y) => y.start - x.start)[0];
4360
+ const pattern = new RegExp(`(?<![\\w$.])${receiver.replace(/\$/g, "\\$")}\\s*\\.\\s*decorate\\s*\\(\\s*['"]${name.replace(/\$/g, "\\$")}['"]\\s*,`, "g");
4361
+ let result = null;
4362
+ for (const m of a.source.matchAll(pattern)) {
4363
+ if (m.index >= ref.at || owner(m.index) !== owner(ref.at)) continue;
4364
+ const open = a.source.indexOf("(", m.index);
4365
+ const close = a.pairs.get(open);
4366
+ const value = close === void 0 ? void 0 : argsOf(a, open, close)[1];
4367
+ result = value ? handlerOf(a, value, files) : null;
4368
+ }
4369
+ return result;
4370
+ }
4371
+ var authPluginCache = /* @__PURE__ */ new WeakMap();
4372
+ function registeredAuthPlugin(ref, pkg) {
4373
+ const a = analyse(ref.file);
4374
+ const receiver = /^([A-Za-z_$][\w$]*)\s*\./.exec(ref.text)?.[1];
4375
+ if (!receiver) return false;
4376
+ let cache3 = authPluginCache.get(ref.file);
4336
4377
  if (!cache3) {
4337
4378
  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);
4379
+ authPluginCache.set(ref.file, cache3);
4380
+ }
4381
+ let registers = cache3.get(receiver);
4382
+ if (!registers) {
4383
+ registers = fastifyOps(a, receiver).registers;
4384
+ cache3.set(receiver, registers);
4350
4385
  }
4351
- return cache3.get(name) ?? null;
4386
+ const owner = (at) => a.bodies.filter((b) => b.start < at && at < b.end).sort((x, y) => y.start - x.start)[0];
4387
+ return registers.some((reg) => reg.at < ref.at && owner(reg.at) === owner(ref.at) && /^[A-Za-z_$][\w$]*$/.test(reg.target.text) && packageOf(a, reg.target.text) === pkg);
4352
4388
  }
4353
4389
  function authCompositionOf(ref) {
4354
4390
  const a = analyse(ref.file);
@@ -4382,7 +4418,7 @@ function middlewareDefinition(ref, files) {
4382
4418
  const callee = member ?? root;
4383
4419
  const spec = packageOf(a, root);
4384
4420
  if (member && (spec === null || spec === "fastify")) {
4385
- const decorated = decoratorOf(member, files);
4421
+ const decorated = decoratorOf(ref, root, member, files);
4386
4422
  if (decorated) return { name: callee, range: decorated, spec: null, inline };
4387
4423
  }
4388
4424
  if (spec) return { name: callee, range: null, spec, inline };
@@ -4399,6 +4435,181 @@ function propertyValue(a, name) {
4399
4435
  return { file: a.file, start: from, end: expressionEnd(a, from) - 1 };
4400
4436
  }
4401
4437
 
4438
+ // src/rules/verification.ts
4439
+ var LocalVerification = class {
4440
+ constructor(file, responseExits = false) {
4441
+ this.responseExits = responseExits;
4442
+ this.code = noiseMaskedOf(file);
4443
+ this.source = commentsMaskedOf(file);
4444
+ this.pairs = delimiterPairs(this.code);
4445
+ this.bodies = functionBodies(this.code, this.pairs);
4446
+ }
4447
+ responseExits;
4448
+ code;
4449
+ source;
4450
+ pairs;
4451
+ bodies;
4452
+ owner(at) {
4453
+ return this.bodies.filter((b) => b.start < at && at < b.end).sort((a, b) => b.start - a.start)[0];
4454
+ }
4455
+ skipSpace(at) {
4456
+ while (/\s/.test(this.code[at] ?? "")) at++;
4457
+ return at;
4458
+ }
4459
+ endOf(at, limit = this.code.length) {
4460
+ if (this.code[at] === "{") return (this.pairs.get(at) ?? limit - 1) + 1;
4461
+ for (let i = at; i < limit; i++) {
4462
+ if (/[;\n}]/.test(this.code[i])) return i;
4463
+ const end = this.pairs.get(i);
4464
+ if (end !== void 0) i = end;
4465
+ }
4466
+ return limit;
4467
+ }
4468
+ /**
4469
+ * 块的顶层有无条件退出即算退出,前面可以有记录日志等语句;
4470
+ * 嵌套分支、函数中的 return 以及无花括号 if/else/循环控制的 return 不算。
4471
+ */
4472
+ exits(from, to) {
4473
+ let at = this.skipSpace(from);
4474
+ if (this.code[at] !== "{") return /^(?:return|throw)\b|^(?:redirect|notFound)\s*\(/.test(this.code.slice(at, to));
4475
+ const end = Math.min(to, this.pairs.get(at) ?? to);
4476
+ at = this.skipSpace(at + 1);
4477
+ while (at < end) {
4478
+ const statement = this.code.slice(at, this.statementEnd(at, end));
4479
+ if (/^return\b/.test(statement) && this.responseExits) {
4480
+ if (/^return\s+(?:(?:await\s+)?(?:reply|res|response)\s*\.[\s\S]*\b(?:send|end|json|sendStatus)\s*\(|(?:c|ctx|context)\s*\.\s*(?:json|text|body|html|redirect|newResponse)\s*\(|new\s+Response\s*\(|next\s*\(\s*[^)\s])/.test(statement)) return true;
4481
+ return false;
4482
+ }
4483
+ if (/^(?:return|throw)\b|^(?:redirect|notFound)\s*\(/.test(statement)) return true;
4484
+ if (this.responseExits && /^reply\s*\.\s*(?:(?:code|status)\s*\([^)]*\)\s*\.\s*)?send\s*\(/.test(this.code.slice(at, end))) return true;
4485
+ const next = this.statementEnd(at, end);
4486
+ if (next <= at) return false;
4487
+ at = this.skipSpace(next);
4488
+ }
4489
+ return false;
4490
+ }
4491
+ /** 跳过完整语句;分支、循环和函数内的退出不提升为外层退出。 */
4492
+ statementEnd(at, limit, depth = 0) {
4493
+ if (depth >= 64) return limit;
4494
+ if (this.code[at] === "{") return (this.pairs.get(at) ?? limit - 1) + 1;
4495
+ const control = /^(if|for(?:\s+await)?|while|with|switch)\s*\(/.exec(this.code.slice(at, at + 80));
4496
+ if (control) {
4497
+ const close = this.pairs.get(at + control[0].length - 1);
4498
+ if (close === void 0) return limit;
4499
+ let end = this.statementEnd(this.skipSpace(close + 1), limit, depth + 1);
4500
+ const next = this.skipSpace(end);
4501
+ if (control[1] === "if" && /^else\b/.test(this.code.slice(next, next + 5))) {
4502
+ end = this.statementEnd(this.skipSpace(next + 4), limit, depth + 1);
4503
+ }
4504
+ return end;
4505
+ }
4506
+ if (/^(?:try|do)\b/.test(this.code.slice(at, at + 8))) return limit;
4507
+ for (let i = at; i < limit; i++) {
4508
+ if (this.code[i] === ";") return i + 1;
4509
+ const close = this.pairs.get(i);
4510
+ if (close !== void 0) {
4511
+ i = close;
4512
+ continue;
4513
+ }
4514
+ if (this.code[i] !== "\n") continue;
4515
+ const before = this.code.slice(at, i).trimEnd();
4516
+ const after = this.code.slice(i + 1, limit).trimStart();
4517
+ if (!before || /[=+\-*/%&|^!?:,.([{<>]$/.test(before)) continue;
4518
+ if (/^[.?+\-*/%&|^,:<>=([{`]/.test(after)) continue;
4519
+ if (/\b(?:const|let|var|function|async|await|new|yield)$/.test(before)) continue;
4520
+ return i + 1;
4521
+ }
4522
+ return limit;
4523
+ }
4524
+ /** 验证与使用须在同一函数,且不能依赖可选分支或被吞掉的异常。 */
4525
+ enforcedBefore(from, to) {
4526
+ const owner = this.owner(from);
4527
+ if (from >= to || owner !== this.owner(to)) return false;
4528
+ const start = owner ? owner.start + 1 : 0;
4529
+ for (let i = start; i < from; i++) {
4530
+ const conditional = /^(?:if|for|while|switch)\s*\(/.exec(this.code.slice(i, i + 32));
4531
+ if (conditional && !/[\w$]/.test(this.code[i - 1] ?? "")) {
4532
+ const close = this.pairs.get(i + conditional[0].length - 1);
4533
+ if (close !== void 0) {
4534
+ const body = this.skipSpace(close + 1);
4535
+ const end2 = this.endOf(body);
4536
+ if (from < end2 && to >= end2) return false;
4537
+ }
4538
+ }
4539
+ const end = this.pairs.get(i);
4540
+ if (end === void 0) continue;
4541
+ if (end < from) {
4542
+ i = end;
4543
+ continue;
4544
+ }
4545
+ if (end >= to) continue;
4546
+ if (this.code[i] === "(" && !/(?:\?\.|\b(?:if|for|while|switch|catch))\s*$/.test(this.code.slice(Math.max(start, i - 20), i))) continue;
4547
+ if (this.code[i] !== "{" || !/\btry\s*$/.test(this.code.slice(Math.max(start, i - 16), i))) return false;
4548
+ let next = this.skipSpace(end + 1);
4549
+ if (/^catch\b/.test(this.code.slice(next))) {
4550
+ next = this.skipSpace(next + 5);
4551
+ if (this.code[next] === "(") next = this.skipSpace((this.pairs.get(next) ?? next) + 1);
4552
+ const caughtEnd = this.endOf(next);
4553
+ if (!this.exits(next, caughtEnd)) return false;
4554
+ next = this.skipSpace(caughtEnd);
4555
+ }
4556
+ if (/^finally\b/.test(this.code.slice(next))) return false;
4557
+ }
4558
+ let boundary = start;
4559
+ for (const [open, close] of this.pairs) {
4560
+ if (this.code[open] === "{" && open < from && close > from) boundary = Math.max(boundary, open + 1);
4561
+ }
4562
+ while (boundary < from) {
4563
+ const next = this.statementEnd(this.skipSpace(boundary), this.code.length);
4564
+ if (next <= boundary || next > from) break;
4565
+ boundary = next;
4566
+ }
4567
+ return !/[?]|&&|\|\||=>/.test(this.code.slice(boundary, from));
4568
+ }
4569
+ /** 找出当前函数中最后一次赋值;未知或条件覆盖同样会使旧证明失效。 */
4570
+ assignment(name, at) {
4571
+ const escaped = name.replace(/\$/g, "\\$");
4572
+ const pattern = new RegExp(`(?<![\\w$.])(?:const\\s+|let\\s+|var\\s+)?${escaped}((?:\\s*\\.[\\w$]+|\\s*\\[[^\\]]*\\])*)\\s*(?::[^=;{}\\n]{0,100})?=(?![=>])`, "g");
4573
+ let result = null;
4574
+ for (const m of this.code.slice(0, at).matchAll(pattern)) {
4575
+ if (this.owner(m.index) !== this.owner(at)) continue;
4576
+ const from = this.skipSpace(m.index + m[0].length);
4577
+ const end = this.endOf(from);
4578
+ if (end <= at) result = { at: m.index, from, end, expression: this.code.slice(from, end).trim(), member: m[1].length > 0 };
4579
+ }
4580
+ return result;
4581
+ }
4582
+ };
4583
+ function enforcedVerification(source, pattern, synchronous = /(?!)/) {
4584
+ const fragment = new LocalVerification({ path: "middleware.ts", content: source, lines: source.split("\n"), isExampleContext: false });
4585
+ const first = fragment.code.search(/\S/);
4586
+ let body = fragment.code[first] === "{" ? { start: first, end: fragment.pairs.get(first) ?? fragment.code.length - 1 } : fragment.bodies.find((b) => b.declaration >= first);
4587
+ if (body) {
4588
+ const returned = fragment.bodies.find((b) => b.declaration > body.start && b.end < body.end && /^\s*return\s+(?:createMiddleware\s*\(\s*)?(?:async\s*)?(?:\([^)]*\)\s*)?$/.test(fragment.code.slice(body.start + 1, b.declaration)));
4589
+ if (returned) body = returned;
4590
+ }
4591
+ const inner = body ? source.slice(body.start + 1, body.end) : source;
4592
+ const content = `async function __guard__(){
4593
+ ${inner}
4594
+ __continuation__();
4595
+ }`;
4596
+ const context = new LocalVerification({ path: "middleware.ts", content, lines: content.split("\n"), isExampleContext: false }, true);
4597
+ const owner = context.bodies[0];
4598
+ let end = content.lastIndexOf("__continuation__");
4599
+ for (const m of context.code.matchAll(/(?<![\w$.])next\s*\(\s*\)/g)) {
4600
+ if (context.owner(m.index) === owner) end = Math.min(end, m.index);
4601
+ }
4602
+ const calls = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
4603
+ for (const m of context.code.matchAll(calls)) {
4604
+ if (context.owner(m.index) !== owner || !context.enforcedBefore(m.index, end)) continue;
4605
+ const close = context.pairs.get(m.index + m[0].length - 1);
4606
+ if (close === void 0 || /^[.[]|^\?\./.test(context.code.slice(context.skipSpace(close + 1)))) continue;
4607
+ const before = context.code.slice(Math.max(owner?.start ?? 0, m.index - 300), m.index).replace(/(?:[A-Za-z_$][\w$]*(?:\s*\([^()]*\))?\s*\??\.\s*)*$/, "").trimEnd();
4608
+ if (/(?:^|[^\w$.])(?:await|return)$/.test(before) || synchronous.test(m[0])) return true;
4609
+ }
4610
+ return false;
4611
+ }
4612
+
4402
4613
  // src/rules/apiauth.ts
4403
4614
  var APP_ROUTER = /(?:^|\/)app\/api\/(?:.+\/)?route\.[mc]?[jt]sx?$/;
4404
4615
  var PAGES_ROUTER = /(?:^|\/)pages\/api\/.+\.[mc]?[jt]sx?$/;
@@ -6018,13 +6229,6 @@ function passesOnlyAuthenticated(code) {
6018
6229
  }
6019
6230
  return false;
6020
6231
  }
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
6232
  function maskUnusedFunctions(code) {
6029
6233
  let out = code;
6030
6234
  const definitions = /\b(?:(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=;]{0,200})?=\s*(?:async\b\s*)?(?:function\b[^(]{0,80}\([^()]*\)|\([^()]*\)\s*(?::[^=;{]{0,200})?=>|[A-Za-z_$][\w$]*\s*=>)|(?:async\s+)?function\s*\*?\s*([A-Za-z_$][\w$]*)\s*\([^()]*\))\s*/g;
@@ -6052,7 +6256,7 @@ function maskUnusedFunctions(code) {
6052
6256
  return out;
6053
6257
  }
6054
6258
  function jwtVerifyRejects(code) {
6055
- if (!awaitedCall(code, /\bjwtVerify\s*\(/)) return false;
6259
+ if (!enforcedVerification(code, /\bjwtVerify\s*\(/)) return false;
6056
6260
  const handlers = [...code.matchAll(/\bcatch\s*(?:\([^()]*\))?\s*\{/g)].map((m) => {
6057
6261
  const open = m.index + m[0].length - 1;
6058
6262
  return code.slice(open, closingDelimiter(code, open, "{", "}") ?? open);
@@ -6079,7 +6283,8 @@ function catchDenies(code) {
6079
6283
  const tryOpen = m.index + m[0].length - 1;
6080
6284
  const tryClose = closingDelimiter(code, tryOpen, "{", "}");
6081
6285
  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;
6286
+ const calls = /\b\w*(?:auth|valid|verify|session|token|user|client|key|secret|permission|access)\w*\s*\(/i;
6287
+ if (!enforcedVerification(code, calls, /^(?:assertAuth(?:enticated)?|constructEvent)\b/i)) continue;
6083
6288
  const handler = /^\s*catch\s*(?:\([^()]*\))?\s*\{/.exec(code.slice(tryClose + 1));
6084
6289
  if (!handler) continue;
6085
6290
  const open = tryClose + handler[0].length;
@@ -6115,8 +6320,8 @@ function rejectsMissingCredential(code) {
6115
6320
  }
6116
6321
  function middlewareRejects(source) {
6117
6322
  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);
6323
+ if (hasConditionalAuthGuard(code) || rejectsMissingCredential(code) || passesOnlyAuthenticated(code) || rejectsSecretMismatch(code) || catchDenies(code) || enforcedVerification(code, AUTH_ENFORCING_CALL, /^(?:assertAuth(?:enticated)?|constructEvent)\b/i) || jwtVerifyRejects(code)) return true;
6324
+ return false;
6120
6325
  }
6121
6326
  function middlewareStatus(ref, files, depth = 0) {
6122
6327
  const composition = depth < 3 ? authCompositionOf(ref) : null;
@@ -6142,7 +6347,7 @@ function middlewareStatus(ref, files, depth = 0) {
6142
6347
  if (inner.status === "guard") return { status: "guard", name };
6143
6348
  }
6144
6349
  } 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}"`)));
6350
+ const plugin = FASTIFY_AUTH_DECORATORS.find(([pkg, decorator]) => decorator === name && registeredAuthPlugin(ref, pkg));
6146
6351
  if (plugin) return { status: "guard", name };
6147
6352
  }
6148
6353
  return { status: AUTH_HELPER_NAME.test(name) ? "unconfirmed" : "none", name };
@@ -6989,12 +7194,13 @@ function fixedTablesOf(code, source, pairs) {
6989
7194
  return result;
6990
7195
  }
6991
7196
  var InputFlow = class {
6992
- constructor(code, source, span, pairs, roles, fixedTables) {
7197
+ constructor(code, source, span, pairs, roles, fixedTables, definition) {
6993
7198
  this.code = code;
6994
7199
  this.source = source;
6995
7200
  this.span = span;
6996
7201
  this.pairs = pairs;
6997
7202
  this.fixedTables = fixedTables;
7203
+ this.definition = definition;
6998
7204
  for (const [name, at] of roles.values) this.parameters.set(name, { level: "direct", origin: at, names: /* @__PURE__ */ new Set([name]), ownUrl: false });
6999
7205
  this.sourcePattern = sourceRegex(roles);
7000
7206
  this.contextRead = roles.contexts.size > 0 ? new RegExp(String.raw`(?<![\w$.])(?:${namePattern(roles.contexts.keys())})\s*\??\.\s*req(?:\s*\??\.\s*raw)?\s*\??\.\s*(${HONO_READ_CALLS})\s*\(`) : null;
@@ -7039,6 +7245,7 @@ var InputFlow = class {
7039
7245
  span;
7040
7246
  pairs;
7041
7247
  fixedTables;
7248
+ definition;
7042
7249
  parameters = /* @__PURE__ */ new Map();
7043
7250
  limited = false;
7044
7251
  assignmentList = /* @__PURE__ */ new Map();
@@ -7148,7 +7355,8 @@ var InputFlow = class {
7148
7355
  if (!new RegExp(`^(?:(?:await|new)\\s+)*${escaped}\\s*\\(`).test(expression)) return false;
7149
7356
  const cached = this.mirrors.get(callee);
7150
7357
  if (cached !== void 0) return cached;
7151
- const code = this.code;
7358
+ let code = this.code;
7359
+ let pairs = this.pairs;
7152
7360
  let params = "";
7153
7361
  let after = -1;
7154
7362
  const declared = new RegExp(`(?:^|[^\\w$.])(?:async\\s+)?function\\s*\\*?\\s*${escaped}\\s*\\(`).exec(code);
@@ -7159,21 +7367,31 @@ var InputFlow = class {
7159
7367
  after = match.index + match[0].length;
7160
7368
  } else if (match) {
7161
7369
  const open = match.index + match[0].length - 1;
7162
- const close = this.pairs.get(open);
7370
+ const close = pairs.get(open);
7163
7371
  if (close !== void 0) {
7164
7372
  params = code.slice(open + 1, close);
7165
7373
  after = close + 1;
7166
7374
  }
7167
7375
  }
7376
+ if (!match) {
7377
+ const target = this.definition?.(callee);
7378
+ if (target) {
7379
+ code = noiseMaskedOf(target.file);
7380
+ pairs = delimiterPairs(code);
7381
+ params = functionParams(target).join(",");
7382
+ after = code[target.start] === "{" ? target.start : code.indexOf("=>", target.start) + 2;
7383
+ if (after < target.start || after > target.end) after = -1;
7384
+ }
7385
+ }
7168
7386
  let result = false;
7169
7387
  if (after !== -1) {
7170
7388
  const names = params.split(",").flatMap((part) => part.replace(/[:=][\s\S]*$/, "").match(/[A-Za-z_$][\w$]*/g) ?? []);
7171
7389
  const head = /^\s*(?::[^={]{0,200})?(?:=>)?\s*/.exec(code.slice(after, after + 260));
7172
7390
  const start = after + head[0].length;
7173
- const end = (from, limit) => expressionEnd2(code, from, limit, this.pairs, () => {
7391
+ const end = (from, limit) => expressionEnd2(code, from, limit, pairs, () => {
7174
7392
  this.limited = true;
7175
7393
  });
7176
- const bodyEnd = this.pairs.get(start) ?? start;
7394
+ const bodyEnd = pairs.get(start) ?? start;
7177
7395
  const returns = code[start] === "{" ? [...code.slice(start, bodyEnd).matchAll(/\breturn\b/g)].map((m) => {
7178
7396
  const from = start + m.index + m[0].length;
7179
7397
  return code.slice(from, end(from, bodyEnd));
@@ -7454,12 +7672,13 @@ function lowerBound(values, value) {
7454
7672
  return low;
7455
7673
  }
7456
7674
  var HandlerFile = class {
7457
- constructor(code, source, pairs, lineStarts, candidates) {
7675
+ constructor(code, source, pairs, lineStarts, candidates, definition) {
7458
7676
  this.code = code;
7459
7677
  this.source = source;
7460
7678
  this.pairs = pairs;
7461
7679
  this.lineStarts = lineStarts;
7462
7680
  this.candidates = candidates;
7681
+ this.definition = definition;
7463
7682
  this.fixedTables = fixedTablesOf(code, source, pairs);
7464
7683
  }
7465
7684
  code;
@@ -7467,6 +7686,7 @@ var HandlerFile = class {
7467
7686
  pairs;
7468
7687
  lineStarts;
7469
7688
  candidates;
7689
+ definition;
7470
7690
  flows = /* @__PURE__ */ new Map();
7471
7691
  fixedTables;
7472
7692
  get limited() {
@@ -7483,7 +7703,7 @@ var HandlerFile = class {
7483
7703
  if (first >= positions.length || positions[first] >= body.end) continue;
7484
7704
  let flow = this.flows.get(candidate);
7485
7705
  if (!flow) {
7486
- flow = new InputFlow(this.code, this.source, { start: body.start, end: body.end }, this.pairs, candidate.roles, this.fixedTables);
7706
+ flow = new InputFlow(this.code, this.source, { start: body.start, end: body.end }, this.pairs, candidate.roles, this.fixedTables, this.definition);
7487
7707
  this.flows.set(candidate, flow);
7488
7708
  }
7489
7709
  result.push({ route: candidate.route, body, flow });
@@ -7528,7 +7748,14 @@ function handlerFileOf(file, files) {
7528
7748
  if (hasInput(roles)) candidates.push({ route: r, body, roles });
7529
7749
  }
7530
7750
  }
7531
- if (candidates.length > 0) result = new HandlerFile(code, source, pairs, lineStartsOf(file.content), candidates);
7751
+ if (candidates.length > 0) result = new HandlerFile(
7752
+ code,
7753
+ source,
7754
+ pairs,
7755
+ lineStartsOf(file.content),
7756
+ candidates,
7757
+ (name) => calleeDefinition(file, name, null, files)
7758
+ );
7532
7759
  }
7533
7760
  }
7534
7761
  handlerCache.set(file, result);
@@ -8050,140 +8277,6 @@ function check(ctx, kind) {
8050
8277
  var ssrfRule = { id: "ssrf/request-url", severity: "P1", check: (ctx) => check(ctx, "ssrf") };
8051
8278
  var redirectRule = { id: "redirect/open", severity: "P2", check: (ctx) => check(ctx, "redirect") };
8052
8279
 
8053
- // src/rules/verification.ts
8054
- var LocalVerification = class {
8055
- code;
8056
- source;
8057
- pairs;
8058
- bodies;
8059
- constructor(file) {
8060
- this.code = noiseMaskedOf(file);
8061
- this.source = commentsMaskedOf(file);
8062
- this.pairs = delimiterPairs(this.code);
8063
- this.bodies = functionBodies(this.code, this.pairs);
8064
- }
8065
- owner(at) {
8066
- return this.bodies.filter((b) => b.start < at && at < b.end).sort((a, b) => b.start - a.start)[0];
8067
- }
8068
- skipSpace(at) {
8069
- while (/\s/.test(this.code[at] ?? "")) at++;
8070
- return at;
8071
- }
8072
- endOf(at, limit = this.code.length) {
8073
- if (this.code[at] === "{") return (this.pairs.get(at) ?? limit - 1) + 1;
8074
- for (let i = at; i < limit; i++) {
8075
- if (/[;\n}]/.test(this.code[i])) return i;
8076
- const end = this.pairs.get(i);
8077
- if (end !== void 0) i = end;
8078
- }
8079
- return limit;
8080
- }
8081
- /**
8082
- * 块的顶层有无条件退出即算退出,前面可以有记录日志等语句;
8083
- * 嵌套分支、函数中的 return 以及无花括号 if/else/循环控制的 return 不算。
8084
- */
8085
- exits(from, to) {
8086
- let at = this.skipSpace(from);
8087
- if (this.code[at] !== "{") return /^(?:return|throw)\b|^(?:redirect|notFound)\s*\(/.test(this.code.slice(at, to));
8088
- const end = Math.min(to, this.pairs.get(at) ?? to);
8089
- at = this.skipSpace(at + 1);
8090
- while (at < end) {
8091
- if (/^(?:return|throw)\b|^(?:redirect|notFound)\s*\(/.test(this.code.slice(at, at + 32))) return true;
8092
- const next = this.statementEnd(at, end);
8093
- if (next <= at) return false;
8094
- at = this.skipSpace(next);
8095
- }
8096
- return false;
8097
- }
8098
- /** 跳过完整语句;分支、循环和函数内的退出不提升为外层退出。 */
8099
- statementEnd(at, limit, depth = 0) {
8100
- if (depth >= 64) return limit;
8101
- if (this.code[at] === "{") return (this.pairs.get(at) ?? limit - 1) + 1;
8102
- const control = /^(if|for(?:\s+await)?|while|with|switch)\s*\(/.exec(this.code.slice(at, at + 80));
8103
- if (control) {
8104
- const close = this.pairs.get(at + control[0].length - 1);
8105
- if (close === void 0) return limit;
8106
- let end = this.statementEnd(this.skipSpace(close + 1), limit, depth + 1);
8107
- const next = this.skipSpace(end);
8108
- if (control[1] === "if" && /^else\b/.test(this.code.slice(next, next + 5))) {
8109
- end = this.statementEnd(this.skipSpace(next + 4), limit, depth + 1);
8110
- }
8111
- return end;
8112
- }
8113
- if (/^(?:try|do)\b/.test(this.code.slice(at, at + 8))) return limit;
8114
- for (let i = at; i < limit; i++) {
8115
- if (this.code[i] === ";") return i + 1;
8116
- const close = this.pairs.get(i);
8117
- if (close !== void 0) {
8118
- i = close;
8119
- continue;
8120
- }
8121
- if (this.code[i] !== "\n") continue;
8122
- const before = this.code.slice(at, i).trimEnd();
8123
- const after = this.code.slice(i + 1, limit).trimStart();
8124
- if (!before || /[=+\-*/%&|^!?:,.([{<>]$/.test(before)) continue;
8125
- if (/^[.?+\-*/%&|^,:<>=([{`]/.test(after)) continue;
8126
- if (/\b(?:const|let|var|function|async|await|new|yield)$/.test(before)) continue;
8127
- return i + 1;
8128
- }
8129
- return limit;
8130
- }
8131
- /** 验证与使用须在同一函数,且不能依赖可选分支或被吞掉的异常。 */
8132
- enforcedBefore(from, to) {
8133
- const owner = this.owner(from);
8134
- if (from >= to || owner !== this.owner(to)) return false;
8135
- const start = owner ? owner.start + 1 : 0;
8136
- for (let i = start; i < from; i++) {
8137
- const conditional = /^(?:if|for|while|switch)\s*\(/.exec(this.code.slice(i, i + 32));
8138
- if (conditional && !/[\w$]/.test(this.code[i - 1] ?? "")) {
8139
- const close = this.pairs.get(i + conditional[0].length - 1);
8140
- if (close !== void 0) {
8141
- const body = this.skipSpace(close + 1);
8142
- const end2 = this.endOf(body);
8143
- if (from < end2 && to >= end2) return false;
8144
- }
8145
- }
8146
- const end = this.pairs.get(i);
8147
- if (end === void 0) continue;
8148
- if (end < from) {
8149
- i = end;
8150
- continue;
8151
- }
8152
- if (end >= to) continue;
8153
- if (this.code[i] !== "{" || !/\btry\s*$/.test(this.code.slice(Math.max(start, i - 16), i))) return false;
8154
- let next = this.skipSpace(end + 1);
8155
- if (/^catch\b/.test(this.code.slice(next))) {
8156
- next = this.skipSpace(next + 5);
8157
- if (this.code[next] === "(") next = this.skipSpace((this.pairs.get(next) ?? next) + 1);
8158
- const caughtEnd = this.endOf(next);
8159
- if (!this.exits(next, caughtEnd)) return false;
8160
- next = this.skipSpace(caughtEnd);
8161
- }
8162
- if (/^finally\b/.test(this.code.slice(next))) return false;
8163
- }
8164
- const boundary = Math.max(
8165
- start,
8166
- this.code.lastIndexOf(";", from - 1) + 1,
8167
- this.code.lastIndexOf("\n", from - 1) + 1,
8168
- this.code.lastIndexOf("{", from - 1) + 1
8169
- );
8170
- return !/[?]|&&|\|\|/.test(this.code.slice(boundary, from));
8171
- }
8172
- /** 找出当前函数中最后一次赋值;未知或条件覆盖同样会使旧证明失效。 */
8173
- assignment(name, at) {
8174
- const escaped = name.replace(/\$/g, "\\$");
8175
- const pattern = new RegExp(`(?<![\\w$.])(?:const\\s+|let\\s+|var\\s+)?${escaped}((?:\\s*\\.[\\w$]+|\\s*\\[[^\\]]*\\])*)\\s*(?::[^=;{}\\n]{0,100})?=(?![=>])`, "g");
8176
- let result = null;
8177
- for (const m of this.code.slice(0, at).matchAll(pattern)) {
8178
- if (this.owner(m.index) !== this.owner(at)) continue;
8179
- const from = this.skipSpace(m.index + m[0].length);
8180
- const end = this.endOf(from);
8181
- if (end <= at) result = { at: m.index, from, end, expression: this.code.slice(from, end).trim(), member: m[1].length > 0 };
8182
- }
8183
- return result;
8184
- }
8185
- };
8186
-
8187
8280
  // src/rules/session.ts
8188
8281
  var GET_SESSION = /\.auth\s*\.\s*getSession\s*\(/g;
8189
8282
  function verifiedSession(file, context, sessionAt, names) {
@@ -8321,7 +8414,7 @@ var READS_BODY = /(?<![\w$])(?:req|request)\s*(?:\.\s*raw\s*)?\.\s*(?:json|text|
8321
8414
  var SIGNATURE_HEADER = /['"]stripe-signature['"]/i;
8322
8415
  function middlewareVerifies(code) {
8323
8416
  const masked = maskUnusedFunctions(code);
8324
- const verifies = /\bconstructEvent\s*\(/.test(masked) || awaitedCall(masked, /\bconstructEventAsync\s*\(/);
8417
+ const verifies = enforcedVerification(masked, /\bconstructEvent(?:Async)?\s*\(/, /^constructEvent\s*\(/);
8325
8418
  if (!verifies) return false;
8326
8419
  for (const m of masked.matchAll(/\bcatch\s*(?:\([^()]*\))?\s*\{/g)) {
8327
8420
  const open = m.index + m[0].length - 1;
@@ -8347,6 +8440,8 @@ function verifiedByMiddleware(route, files) {
8347
8440
  const masked = maskUnusedFunctions(code);
8348
8441
  const calls = [...masked.matchAll(/(?<![\w$.])([A-Za-z_$][\w$]*)\s*(?:\.\s*([A-Za-z_$][\w$]*)\s*)?\(/g)].slice(0, 16);
8349
8442
  return calls.some((m) => {
8443
+ const call = new RegExp(`(?<![\\w$.])${m[1].replace(/\$/g, "\\$")}${m[2] ? `\\s*\\.\\s*${m[2].replace(/\$/g, "\\$")}` : ""}\\s*\\(`);
8444
+ if (!enforcedVerification(masked, call)) return false;
8350
8445
  const definition = calleeDefinition(file, m[1], m[2] ?? null, files);
8351
8446
  return definition !== null && middlewareVerifies(commentsMaskedOf(definition.file).slice(definition.start, definition.end + 1));
8352
8447
  });
@@ -10250,7 +10345,7 @@ function openReport(path, onError, options = {}) {
10250
10345
  }
10251
10346
 
10252
10347
  // src/cli.ts
10253
- var VERSION = true ? "0.7.0" : "0.0.0-dev";
10348
+ var VERSION = true ? "0.7.1" : "0.0.0-dev";
10254
10349
  function finish(code) {
10255
10350
  process.exitCode = code;
10256
10351
  }