brainblast 0.3.0 → 0.4.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.
@@ -38,7 +38,13 @@ function findCandidates(targetDir, rule) {
38
38
  const sf = project.addSourceFileAtPath(file);
39
39
  const importsModule = sf.getImportDeclarations().some((d) => modules.has(d.getModuleSpecifierValue()));
40
40
  const consider = (fn, name) => {
41
- if (!(importsModule || name && nameRe.test(name) || bodyCallsAnyOf(fn, triggers))) return;
41
+ const hasName = !!(name && nameRe.test(name));
42
+ const hasTrigger = bodyCallsAnyOf(fn, triggers);
43
+ if (rule.detect.requiresImport) {
44
+ if (!(importsModule && (hasName || hasTrigger))) return;
45
+ } else {
46
+ if (!(hasName || hasTrigger)) return;
47
+ }
42
48
  out.push({
43
49
  filePath: file,
44
50
  fnName: name || "(anonymous)",
@@ -62,7 +68,20 @@ var positionalArgIdentity = (c, p) => {
62
68
  const exp = call.getExpression();
63
69
  return exp.getKind() === SyntaxKind2.PropertyAccessExpression && exp.asKind(SyntaxKind2.PropertyAccessExpression).getName() === p.call;
64
70
  });
65
- if (calls.length === 0) return { result: "fail", detail: p.absentDetail };
71
+ if (calls.length === 0) {
72
+ const sf = c.fn.getSourceFile();
73
+ const existsInFile = sf.getDescendantsOfKind(SyntaxKind2.CallExpression).some((call) => {
74
+ const exp = call.getExpression();
75
+ return exp.getKind() === SyntaxKind2.PropertyAccessExpression && exp.asKind(SyntaxKind2.PropertyAccessExpression).getName() === p.call;
76
+ });
77
+ if (existsInFile) {
78
+ return {
79
+ result: "cant_tell",
80
+ detail: `${p.call} is called elsewhere in this file; unable to confirm this function's delegation path statically.`
81
+ };
82
+ }
83
+ return { result: "fail", detail: p.absentDetail };
84
+ }
66
85
  const arg = calls[0].getArguments()[p.argIndex];
67
86
  const wantParam = c.params[p.paramIndex];
68
87
  if (arg && wantParam && arg.getKind() === SyntaxKind2.Identifier && arg.getText() === wantParam) {
@@ -538,6 +557,136 @@ function findRustCandidates(targetDir, rule) {
538
557
  return out;
539
558
  }
540
559
 
560
+ // src/fixers/positionalArgIdentity.ts
561
+ import { SyntaxKind as SyntaxKind7 } from "ts-morph";
562
+
563
+ // src/fixers/diffUtil.ts
564
+ function buildDiff(node, replacement) {
565
+ const sf = node.getSourceFile();
566
+ const filePath = sf.getFilePath();
567
+ const fullText = sf.getFullText();
568
+ const start = node.getStart();
569
+ const end = node.getEnd();
570
+ const startPos = sf.getLineAndColumnAtPos(start);
571
+ const endPos = sf.getLineAndColumnAtPos(end);
572
+ const lines = fullText.split("\n");
573
+ const oldMiddle = lines.slice(startPos.line - 1, endPos.line);
574
+ const oldFirst = oldMiddle[0].slice(0, startPos.column - 1);
575
+ const oldLast = oldMiddle[oldMiddle.length - 1].slice(endPos.column - 1);
576
+ const newMiddle = (oldFirst + replacement + oldLast).split("\n");
577
+ const removed = oldMiddle.map((l) => `-${l}`);
578
+ const added = newMiddle.map((l) => `+${l}`);
579
+ const hunkHeader = `@@ -${startPos.line},${oldMiddle.length} +${startPos.line},${newMiddle.length} @@`;
580
+ return [`--- a${filePath}`, `+++ b${filePath}`, hunkHeader, ...removed, ...added].join("\n");
581
+ }
582
+
583
+ // src/fixers/positionalArgIdentity.ts
584
+ var fixPositionalArgIdentity = (c, p, outcome) => {
585
+ if (outcome.result !== "fail") return void 0;
586
+ const calls = c.fn.getDescendantsOfKind(SyntaxKind7.CallExpression).filter((call) => {
587
+ const exp = call.getExpression();
588
+ return exp.getKind() === SyntaxKind7.PropertyAccessExpression && exp.asKind(SyntaxKind7.PropertyAccessExpression).getName() === p.call;
589
+ });
590
+ if (calls.length === 0) {
591
+ const wantParam2 = c.params[p.paramIndex] ?? "<rawBodyParam>";
592
+ return {
593
+ summary: `Add a ${p.call} call that verifies the raw request body`,
594
+ suggestion: `No '${p.call}' call was found in this handler. Verify the signature against the raw, unparsed request body \u2014 parameter '${wantParam2}' \u2014 before trusting the event, e.g.:
595
+
596
+ const event = stripe.webhooks.constructEvent(${wantParam2}, signature, process.env.STRIPE_WEBHOOK_SECRET!);
597
+
598
+ Do not call JSON.parse() on the body before this verification step.`
599
+ };
600
+ }
601
+ const arg = calls[0].getArguments()[p.argIndex];
602
+ const wantParam = c.params[p.paramIndex];
603
+ if (arg && wantParam && arg.getKind() === SyntaxKind7.CallExpression) {
604
+ return {
605
+ summary: `Pass the raw body parameter '${wantParam}' to ${p.call} instead of a parsed value`,
606
+ diff: buildDiff(arg, wantParam)
607
+ };
608
+ }
609
+ return void 0;
610
+ };
611
+
612
+ // src/fixers/requiredCallWithOptions.ts
613
+ import { SyntaxKind as SyntaxKind8 } from "ts-morph";
614
+ function callName4(call) {
615
+ const exp = call.getExpression();
616
+ if (exp.getKind() === SyntaxKind8.Identifier) return exp.getText();
617
+ if (exp.getKind() === SyntaxKind8.PropertyAccessExpression) {
618
+ return exp.asKind(SyntaxKind8.PropertyAccessExpression).getName();
619
+ }
620
+ return "";
621
+ }
622
+ function placeholderFor(propName) {
623
+ switch (propName) {
624
+ case "audience":
625
+ case "aud":
626
+ return `audience: process.env.PRIVY_APP_ID`;
627
+ case "issuer":
628
+ case "iss":
629
+ return `issuer: "https://privy.io"`;
630
+ default:
631
+ return `${propName}: undefined /* TODO: brainblast could not infer this value */`;
632
+ }
633
+ }
634
+ var fixRequiredCallWithOptions = (c, p, outcome) => {
635
+ if (outcome.result !== "fail") return void 0;
636
+ const calls = c.fn.getDescendantsOfKind(SyntaxKind8.CallExpression);
637
+ const verify = calls.filter((x) => p.verifyCalls.includes(callName4(x)));
638
+ if (verify.length > 0) {
639
+ const call = verify[0];
640
+ const args = call.getArguments();
641
+ const lastArg = args[args.length - 1];
642
+ const obj = lastArg?.asKind(SyntaxKind8.ObjectLiteralExpression);
643
+ const presentNames = obj ? obj.getProperties().map((pr) => {
644
+ const pa = pr.asKind(SyntaxKind8.PropertyAssignment) ?? pr.asKind(SyntaxKind8.ShorthandPropertyAssignment);
645
+ return pa?.getName() ?? "";
646
+ }) : [];
647
+ const missingGroups = p.requiredProps.filter(
648
+ (g) => !g.some((n) => presentNames.includes(n))
649
+ );
650
+ if (missingGroups.length === 0) return void 0;
651
+ const newProps = missingGroups.map((g) => placeholderFor(g[0])).join(", ");
652
+ const summary = `Add ${missingGroups.map((g) => g[0]).join(" and ")} to the ${callName4(call)} call`;
653
+ if (obj) {
654
+ const inner = obj.getText().slice(1, -1).trim();
655
+ const newText = inner.length > 0 ? `{ ${inner}, ${newProps} }` : `{ ${newProps} }`;
656
+ return { summary, diff: buildDiff(obj, newText) };
657
+ }
658
+ if (lastArg) {
659
+ const newText = `${lastArg.getText()}, { ${newProps} }`;
660
+ return { summary, diff: buildDiff(lastArg, newText) };
661
+ }
662
+ return {
663
+ summary,
664
+ suggestion: `Add an options object ({ ${newProps} }) as an argument to ${callName4(call)}.`
665
+ };
666
+ }
667
+ return {
668
+ summary: "Replace the decode-only call with a verified call",
669
+ suggestion: `This token is decoded without verifying its signature, accepting any forged token. Replace the decode call with a verifying call that asserts audience and issuer, e.g.:
670
+
671
+ const { payload } = await jwtVerify(token, JWKS, { audience: process.env.PRIVY_APP_ID, issuer: "https://privy.io" });
672
+
673
+ JWKS must come from Privy's published JWKS endpoint for your app.`
674
+ };
675
+ };
676
+
677
+ // src/fixers/index.ts
678
+ var registry2 = {
679
+ "positional-arg-identity": fixPositionalArgIdentity,
680
+ "required-call-with-options": fixRequiredCallWithOptions
681
+ };
682
+ function runFixer(kind, c, params, outcome) {
683
+ if (outcome.result !== "fail") return void 0;
684
+ const fn = registry2[kind];
685
+ if (!fn) return void 0;
686
+ return fn(c, params, outcome);
687
+ }
688
+ var fixerKinds = Object.keys(registry2);
689
+
541
690
  // src/emit.ts
542
691
  function buildReport(target, checks, rules2, costReport) {
543
692
  const byId = new Map(rules2.map((r) => [r.id, r]));
@@ -584,7 +733,9 @@ function buildReport(target, checks, rules2, costReport) {
584
733
  file: c.file,
585
734
  line: c.line,
586
735
  title: c.title,
587
- detail: c.detail
736
+ detail: c.detail,
737
+ ...c.fix ? { fix: c.fix } : {},
738
+ ...c.precedent ? { precedent: c.precedent } : {}
588
739
  })),
589
740
  checkTotals,
590
741
  openQuestions: [],
@@ -611,6 +762,7 @@ function auditWithRule(targetDir, rule) {
611
762
  }
612
763
  return findCandidates(targetDir, rule).map((c) => {
613
764
  const outcome = runChecker(rule.check.kind, c, rule.check.params);
765
+ const fix = runFixer(rule.check.kind, c, rule.check.params, outcome);
614
766
  return {
615
767
  ruleId: rule.id,
616
768
  severity: rule.severity,
@@ -618,7 +770,8 @@ function auditWithRule(targetDir, rule) {
618
770
  file: c.filePath,
619
771
  line: c.fn.getStartLineNumber(),
620
772
  exportName: c.fnName,
621
- ...outcome
773
+ ...outcome,
774
+ ...fix ? { fix } : {}
622
775
  };
623
776
  });
624
777
  }
@@ -903,7 +1056,7 @@ mod brainblast_reinit_guard_test {
903
1056
  `;
904
1057
 
905
1058
  // src/testTemplates/index.ts
906
- var registry2 = {
1059
+ var registry3 = {
907
1060
  "stripe-webhook-signature": stripeWebhookSignature,
908
1061
  "privy-jwt-claims": privyJwtClaims,
909
1062
  "bags-fee-share": bagsFeeShare,
@@ -913,14 +1066,14 @@ var registry2 = {
913
1066
  };
914
1067
  var JS_IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
915
1068
  function renderTest(kind, opts) {
916
- const tpl = registry2[kind];
1069
+ const tpl = registry3[kind];
917
1070
  if (!tpl) throw new Error(`Unknown test template kind '${kind}'.`);
918
1071
  if (!JS_IDENTIFIER.test(opts.handlerExport)) {
919
1072
  throw new Error(`Unsafe handler export name '${opts.handlerExport}' (not a JS identifier).`);
920
1073
  }
921
1074
  return tpl(opts);
922
1075
  }
923
- var testKinds = Object.keys(registry2);
1076
+ var testKinds = Object.keys(registry3);
924
1077
 
925
1078
  // src/loadRules.ts
926
1079
  import { readdirSync as readdirSync3, readFileSync as readFileSync2 } from "fs";
@@ -1384,7 +1537,7 @@ function renderTrustGraphMd(g) {
1384
1537
  }
1385
1538
 
1386
1539
  // src/costAnalysis.ts
1387
- import { Project as Project2, SyntaxKind as SyntaxKind7 } from "ts-morph";
1540
+ import { Project as Project2, SyntaxKind as SyntaxKind9 } from "ts-morph";
1388
1541
  var LAMPORTS_PER_BYTE_YEAR = 3480;
1389
1542
  var EXEMPTION_THRESHOLD = 2;
1390
1543
  var OVERHEAD_BYTES = 128;
@@ -1465,11 +1618,11 @@ var KNOWN_FLOWS = [
1465
1618
  }
1466
1619
  ];
1467
1620
  var LOOP_NODE_KINDS = /* @__PURE__ */ new Set([
1468
- SyntaxKind7.ForStatement,
1469
- SyntaxKind7.ForOfStatement,
1470
- SyntaxKind7.ForInStatement,
1471
- SyntaxKind7.WhileStatement,
1472
- SyntaxKind7.DoStatement
1621
+ SyntaxKind9.ForStatement,
1622
+ SyntaxKind9.ForOfStatement,
1623
+ SyntaxKind9.ForInStatement,
1624
+ SyntaxKind9.WhileStatement,
1625
+ SyntaxKind9.DoStatement
1473
1626
  ]);
1474
1627
  var ARRAY_METHOD_LOOPS = /* @__PURE__ */ new Set(["map", "forEach", "flatMap", "reduce", "filter"]);
1475
1628
  function isInsideLoop(node) {
@@ -1477,12 +1630,12 @@ function isInsideLoop(node) {
1477
1630
  while (cur) {
1478
1631
  const k = cur.getKind?.();
1479
1632
  if (k !== void 0 && LOOP_NODE_KINDS.has(k)) {
1480
- return { scalable: true, note: `call is inside a ${SyntaxKind7[k]} \u2014 cost scales with loop iterations` };
1633
+ return { scalable: true, note: `call is inside a ${SyntaxKind9[k]} \u2014 cost scales with loop iterations` };
1481
1634
  }
1482
- if (k === SyntaxKind7.CallExpression) {
1635
+ if (k === SyntaxKind9.CallExpression) {
1483
1636
  const expr = cur.getExpression?.();
1484
- if (expr?.getKind?.() === SyntaxKind7.PropertyAccessExpression) {
1485
- const name = expr.asKind?.(SyntaxKind7.PropertyAccessExpression)?.getName?.();
1637
+ if (expr?.getKind?.() === SyntaxKind9.PropertyAccessExpression) {
1638
+ const name = expr.asKind?.(SyntaxKind9.PropertyAccessExpression)?.getName?.();
1486
1639
  if (name && ARRAY_METHOD_LOOPS.has(name)) {
1487
1640
  return { scalable: true, note: `call is inside .${name}() \u2014 cost scales with array length` };
1488
1641
  }
@@ -1496,7 +1649,7 @@ function detectPriorityFee(targetDir) {
1496
1649
  const project = new Project2({ skipAddingFilesFromTsConfig: true });
1497
1650
  for (const file of walk(targetDir)) {
1498
1651
  const sf = project.addSourceFileAtPath(file);
1499
- const calls = sf.getDescendantsOfKind(SyntaxKind7.CallExpression);
1652
+ const calls = sf.getDescendantsOfKind(SyntaxKind9.CallExpression);
1500
1653
  for (const ce of calls) {
1501
1654
  const expr = ce.getExpression();
1502
1655
  const text = expr.getText();
@@ -1524,22 +1677,22 @@ function detectAccountFlows(targetDir) {
1524
1677
  const importedModules = new Set(
1525
1678
  sf.getImportDeclarations().map((d) => d.getModuleSpecifierValue())
1526
1679
  );
1527
- for (const ce of sf.getDescendantsOfKind(SyntaxKind7.CallExpression)) {
1680
+ for (const ce of sf.getDescendantsOfKind(SyntaxKind9.CallExpression)) {
1528
1681
  const expr = ce.getExpression();
1529
- let callName4 = null;
1530
- if (expr.getKind() === SyntaxKind7.Identifier) {
1531
- callName4 = expr.getText();
1532
- } else if (expr.getKind() === SyntaxKind7.PropertyAccessExpression) {
1533
- callName4 = expr.asKind(SyntaxKind7.PropertyAccessExpression).getName();
1682
+ let callName5 = null;
1683
+ if (expr.getKind() === SyntaxKind9.Identifier) {
1684
+ callName5 = expr.getText();
1685
+ } else if (expr.getKind() === SyntaxKind9.PropertyAccessExpression) {
1686
+ callName5 = expr.asKind(SyntaxKind9.PropertyAccessExpression).getName();
1534
1687
  }
1535
- if (!callName4) continue;
1536
- const known = callIndex.get(callName4);
1688
+ if (!callName5) continue;
1689
+ const known = callIndex.get(callName5);
1537
1690
  if (!known) continue;
1538
1691
  if (!importedModules.has(known.module)) continue;
1539
1692
  const lamports = rentExemptMinimum(known.dataLen);
1540
1693
  const { scalable, note } = isInsideLoop(ce);
1541
1694
  flows.push({
1542
- call: callName4,
1695
+ call: callName5,
1543
1696
  module: known.module,
1544
1697
  accountType: known.accountType,
1545
1698
  file,
package/dist/cli.js CHANGED
@@ -10,11 +10,84 @@ import {
10
10
  renderCostReportMd,
11
11
  renderTrustGraphMd,
12
12
  resolveRules
13
- } from "./chunk-BZVZ3WAU.js";
13
+ } from "./chunk-WVHGN2HR.js";
14
14
 
15
15
  // src/cli.ts
16
- import { writeFileSync, mkdirSync } from "fs";
16
+ import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
17
+ import { join as join2 } from "path";
18
+
19
+ // src/memory.ts
20
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
17
21
  import { join } from "path";
22
+ var EMPTY_MEMORY = { schemaVersion: "1.0", lastRun: [], fixHistory: [] };
23
+ function memoryPath(targetDir2) {
24
+ return join(targetDir2, ".agent-research", "memory.json");
25
+ }
26
+ function loadMemory(targetDir2) {
27
+ const p = memoryPath(targetDir2);
28
+ if (!existsSync(p)) return { ...EMPTY_MEMORY, lastRun: [], fixHistory: [] };
29
+ try {
30
+ const parsed = JSON.parse(readFileSync(p, "utf8"));
31
+ return {
32
+ schemaVersion: parsed.schemaVersion ?? "1.0",
33
+ lastRun: Array.isArray(parsed.lastRun) ? parsed.lastRun : [],
34
+ fixHistory: Array.isArray(parsed.fixHistory) ? parsed.fixHistory : []
35
+ };
36
+ } catch {
37
+ return { ...EMPTY_MEMORY, lastRun: [], fixHistory: [] };
38
+ }
39
+ }
40
+ function saveMemory(targetDir2, memory2) {
41
+ mkdirSync(join(targetDir2, ".agent-research"), { recursive: true });
42
+ writeFileSync(memoryPath(targetDir2), JSON.stringify(memory2, null, 2));
43
+ }
44
+ var snapshotKey = (e) => `${e.ruleId}::${e.file}::${e.exportName}`;
45
+ function precedentKey(c) {
46
+ return `${c.ruleId}::${c.file}`;
47
+ }
48
+ function updateMemory(memory2, checks2, now = /* @__PURE__ */ new Date()) {
49
+ const prevByKey = new Map(memory2.lastRun.map((e) => [snapshotKey(e), e]));
50
+ const fixedAt = now.toISOString().slice(0, 10);
51
+ const newFixEvents = [];
52
+ for (const c of checks2) {
53
+ const prev = prevByKey.get(snapshotKey(c));
54
+ if (prev?.result === "fail" && c.result !== "fail") {
55
+ newFixEvents.push({
56
+ ruleId: c.ruleId,
57
+ file: c.file,
58
+ exportName: c.exportName,
59
+ fixedAt,
60
+ detail: prev.detail
61
+ });
62
+ }
63
+ }
64
+ const fixHistory = [...memory2.fixHistory, ...newFixEvents];
65
+ const precedents2 = /* @__PURE__ */ new Map();
66
+ for (const c of checks2) {
67
+ if (c.result !== "fail") continue;
68
+ const pk = precedentKey(c);
69
+ if (precedents2.has(pk)) continue;
70
+ const matches = fixHistory.filter((e) => e.ruleId === c.ruleId && e.file !== c.file).sort((a, b) => a.fixedAt < b.fixedAt ? 1 : a.fixedAt > b.fixedAt ? -1 : 0);
71
+ if (matches[0]) {
72
+ precedents2.set(pk, {
73
+ file: matches[0].file,
74
+ exportName: matches[0].exportName,
75
+ fixedAt: matches[0].fixedAt,
76
+ detail: matches[0].detail
77
+ });
78
+ }
79
+ }
80
+ const lastRun = checks2.map((c) => ({
81
+ ruleId: c.ruleId,
82
+ file: c.file,
83
+ exportName: c.exportName,
84
+ result: c.result,
85
+ detail: c.detail
86
+ }));
87
+ return { memory: { schemaVersion: "1.0", lastRun, fixHistory }, precedents: precedents2 };
88
+ }
89
+
90
+ // src/cli.ts
18
91
  var args = process.argv.slice(2);
19
92
  if (args[0] === "trust-graph") {
20
93
  await runTrustGraph(args.slice(1));
@@ -25,20 +98,45 @@ var strict = args.includes("--strict");
25
98
  var targetDir = args.find((a) => !a.startsWith("--")) ?? process.cwd();
26
99
  var rules = resolveRules(targetDir);
27
100
  var { checks, report } = audit(targetDir, rules);
101
+ var memory = loadMemory(targetDir);
102
+ var { memory: nextMemory, precedents } = updateMemory(memory, checks);
103
+ for (const c of checks) {
104
+ const p = precedents.get(precedentKey(c));
105
+ if (p) c.precedent = p;
106
+ }
107
+ for (const rc of report.checks) {
108
+ const p = precedents.get(precedentKey(rc));
109
+ if (p) rc.precedent = p;
110
+ }
111
+ saveMemory(targetDir, nextMemory);
28
112
  var costReport = analyzeCosts(targetDir);
29
113
  report.costAnalysis = costReport;
30
- var outDir = join(targetDir, ".agent-research");
31
- mkdirSync(outDir, { recursive: true });
32
- var reportPath = join(outDir, "report.json");
33
- writeFileSync(reportPath, JSON.stringify(report, null, 2));
34
- var costMdPath = join(outDir, "cost-analysis.md");
35
- writeFileSync(costMdPath, renderCostReportMd(costReport));
114
+ var outDir = join2(targetDir, ".agent-research");
115
+ mkdirSync2(outDir, { recursive: true });
116
+ var reportPath = join2(outDir, "report.json");
117
+ writeFileSync2(reportPath, JSON.stringify(report, null, 2));
118
+ var costMdPath = join2(outDir, "cost-analysis.md");
119
+ writeFileSync2(costMdPath, renderCostReportMd(costReport));
36
120
  console.log(`brainblast: scanned ${targetDir} with ${rules.length} rule(s)`);
37
121
  if (checks.length === 0) console.log(" (no catastrophic components detected)");
38
122
  for (const c of checks) {
39
123
  const tag = c.result === "pass" ? "PASS " : c.result === "fail" ? "FAIL " : "WARN ";
40
124
  console.log(` [${tag}] ${c.ruleId} ${c.file}:${c.line}`);
41
125
  console.log(` ${c.detail}`);
126
+ if (c.precedent) {
127
+ console.log(
128
+ ` memory: same issue (${c.ruleId}) was fixed in ${c.precedent.file} on ${c.precedent.fixedAt}`
129
+ );
130
+ }
131
+ if (c.fix) {
132
+ console.log(` fix: ${c.fix.summary}`);
133
+ if (c.fix.diff) {
134
+ for (const line of c.fix.diff.split("\n")) console.log(` ${line}`);
135
+ }
136
+ if (c.fix.suggestion) {
137
+ for (const line of c.fix.suggestion.split("\n")) console.log(` ${line}`);
138
+ }
139
+ }
42
140
  }
43
141
  var fails = checks.filter((c) => c.result === "fail").length;
44
142
  var cantTell = checks.filter((c) => c.result === "cant_tell").length;
package/dist/index.d.ts CHANGED
@@ -78,6 +78,17 @@ interface CheckOutcome {
78
78
  result: CheckResultKind;
79
79
  detail: string;
80
80
  }
81
+ interface Fix {
82
+ summary: string;
83
+ diff?: string;
84
+ suggestion?: string;
85
+ }
86
+ interface Precedent {
87
+ file: string;
88
+ exportName: string;
89
+ fixedAt: string;
90
+ detail: string;
91
+ }
81
92
  interface CheckResult extends CheckOutcome {
82
93
  ruleId: string;
83
94
  severity: Severity;
@@ -85,6 +96,10 @@ interface CheckResult extends CheckOutcome {
85
96
  file: string;
86
97
  line: number;
87
98
  exportName: string;
99
+ /** Present when result === "fail" and a vetted fixer for check.kind produced one. */
100
+ fix?: Fix;
101
+ /** Present when result === "fail" and the same rule was previously fixed elsewhere. */
102
+ precedent?: Precedent;
88
103
  }
89
104
  interface Rule {
90
105
  id: string;
@@ -102,6 +117,19 @@ interface Rule {
102
117
  triggerCalls: string[];
103
118
  /** Defaults to "typescript". Set to "rust" for Anchor/Rust checker kinds. */
104
119
  lang?: "typescript" | "rust";
120
+ /**
121
+ * When true, a module import from `modules` is a REQUIRED condition for
122
+ * detection: a candidate must be in a file that imports one of the listed
123
+ * modules, AND either its name matches nameRegex or its body calls a
124
+ * triggerCall. This prevents generic name-only matches (e.g. a Fastify
125
+ * middleware named "verifyJwt" that calls `request.jwtVerify()`) from
126
+ * triggering jose-specific rules.
127
+ *
128
+ * When false or omitted (default), detection is: nameRegex match OR
129
+ * triggerCall in body. Module import is not required, so rules like
130
+ * stripe-webhook can still catch handlers that don't import stripe directly.
131
+ */
132
+ requiresImport?: boolean;
105
133
  };
106
134
  check: {
107
135
  kind: string;
@@ -150,6 +178,8 @@ declare function audit(targetDir: string, rules: Rule[]): {
150
178
  low: number;
151
179
  };
152
180
  checks: {
181
+ precedent?: Precedent | undefined;
182
+ fix?: Fix | undefined;
153
183
  ruleId: string;
154
184
  severity: Severity;
155
185
  result: CheckResultKind;
package/dist/index.js CHANGED
@@ -28,7 +28,7 @@ import {
28
28
  runChecker,
29
29
  saveProgramCache,
30
30
  testKinds
31
- } from "./chunk-BZVZ3WAU.js";
31
+ } from "./chunk-WVHGN2HR.js";
32
32
 
33
33
  // src/generate.ts
34
34
  import { writeFileSync, mkdirSync } from "fs";
@@ -14,7 +14,19 @@ detect:
14
14
  # Privy handlers are still picked up by the @privy-io/* / jose / jsonwebtoken
15
15
  # imports, and by triggerCalls like decodeJwt / jwtVerify.
16
16
  nameRegex: "privy|jwt"
17
- triggerCalls: [decodeJwt, jwtVerify, verify, decode]
17
+ # Only decodeJwt triggers detection; it's specific to jose and unambiguously
18
+ # indicates a decode-only pattern. jwtVerify/verify/decode were removed because:
19
+ # - "verify" matches Node.js crypto.verify(), jsonwebtoken jwt.verify(), etc.
20
+ # - "decode" matches bs58.decode() (Solana), Buffer.from(), etc.
21
+ # - "jwtVerify" matches Fastify's request.jwtVerify() plugin method, which is
22
+ # not a jose-based call and requires no audience/issuer options.
23
+ # Functions that import jose/@privy-io/* and are named with "privy|jwt" are still
24
+ # detected via requiresImport + (importsModule && hasName), the primary detection path.
25
+ triggerCalls: [decodeJwt]
26
+ # A module import is REQUIRED to be a candidate. Without this guard, a Fastify
27
+ # middleware named "verifyJwt" (which calls request.jwtVerify(), a completely
28
+ # different library) would match the nameRegex and be flagged as missing aud/iss.
29
+ requiresImport: true
18
30
  check:
19
31
  kind: required-call-with-options
20
32
  params:
@@ -11,6 +11,11 @@ detect:
11
11
  modules: [stripe]
12
12
  nameRegex: webhook
13
13
  triggerCalls: [constructEvent]
14
+ # Require the file to import stripe. Without this guard, any function whose
15
+ # name contains "webhook" — even LemonSqueezy, Polar, or Sendgrid handlers —
16
+ # is flagged for missing stripe.webhooks.constructEvent, producing false
17
+ # positives on non-Stripe payment integrations.
18
+ requiresImport: true
14
19
  check:
15
20
  kind: positional-arg-identity
16
21
  params:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brainblast",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "Deterministic auditor for catastrophic AI-integration bugs: scan a repo, find the silent money/auth traps, and generate the behavioral test that proves they're fixed.",
6
6
  "keywords": [