brainblast 0.7.4 → 0.7.6

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.
Files changed (49) hide show
  1. package/dist/{chunk-5VYTURTO.js → chunk-A3U6JDMN.js} +64 -12
  2. package/dist/chunk-CSYGLMZR.js +129 -0
  3. package/dist/{chunk-5H5JQXXU.js → chunk-Q5DMQN67.js} +109 -25
  4. package/dist/cli.js +84 -5
  5. package/dist/feeConfigs-S4E5GQ7Q.js +19 -0
  6. package/dist/index.d.ts +39 -1
  7. package/dist/index.js +27 -7
  8. package/dist/{mcp-EOTWSQK7.js → mcp-O45PSOVU.js} +1 -1
  9. package/dist/packs/README.md +33 -0
  10. package/dist/packs/jito-bundle-zero-tip/README.md +33 -0
  11. package/dist/packs/jito-bundle-zero-tip/brainblast-pack.yaml +5 -0
  12. package/dist/packs/jito-bundle-zero-tip/fixtures/jito-bundle-zero-tip/fixed/bundle.ts +13 -0
  13. package/dist/packs/jito-bundle-zero-tip/fixtures/jito-bundle-zero-tip/vulnerable/bundle.ts +16 -0
  14. package/dist/packs/jito-bundle-zero-tip/rules/jito-bundle-zero-tip.yaml +54 -0
  15. package/dist/packs/jupiter-quote-zero-slippage/brainblast-pack.yaml +5 -0
  16. package/dist/packs/jupiter-quote-zero-slippage/fixtures/jupiter-quote-zero-slippage/fixed/arb.ts +17 -0
  17. package/dist/packs/jupiter-quote-zero-slippage/fixtures/jupiter-quote-zero-slippage/vulnerable/arb.ts +17 -0
  18. package/dist/packs/jupiter-quote-zero-slippage/rules/jupiter-quote-zero-slippage.yaml +51 -0
  19. package/dist/packs/metaplex-nft-royalty-zero/README.md +39 -0
  20. package/dist/packs/metaplex-nft-royalty-zero/brainblast-pack.yaml +5 -0
  21. package/dist/packs/metaplex-nft-royalty-zero/fixtures/metaplex-nft-royalty-zero/fixed/mint.ts +11 -0
  22. package/dist/packs/metaplex-nft-royalty-zero/fixtures/metaplex-nft-royalty-zero/vulnerable/mint.ts +11 -0
  23. package/dist/packs/metaplex-nft-royalty-zero/rules/metaplex-nft-royalty-zero.yaml +39 -0
  24. package/dist/packs/meteora-dlmm-zero-min-out/README.md +32 -0
  25. package/dist/packs/meteora-dlmm-zero-min-out/brainblast-pack.yaml +5 -0
  26. package/dist/packs/meteora-dlmm-zero-min-out/fixtures/meteora-dlmm-zero-min-out/fixed/swap.ts +19 -0
  27. package/dist/packs/meteora-dlmm-zero-min-out/fixtures/meteora-dlmm-zero-min-out/vulnerable/swap.ts +19 -0
  28. package/dist/packs/meteora-dlmm-zero-min-out/rules/meteora-dlmm-zero-min-out.yaml +47 -0
  29. package/dist/packs/pyth-price-unchecked-staleness/README.md +34 -0
  30. package/dist/packs/pyth-price-unchecked-staleness/brainblast-pack.yaml +5 -0
  31. package/dist/packs/pyth-price-unchecked-staleness/fixtures/pyth-price-unchecked-staleness/fixed/price.ts +14 -0
  32. package/dist/packs/pyth-price-unchecked-staleness/fixtures/pyth-price-unchecked-staleness/vulnerable/price.ts +14 -0
  33. package/dist/packs/pyth-price-unchecked-staleness/rules/pyth-price-unchecked-staleness.yaml +47 -0
  34. package/dist/packs/raydium-compute-zero-slippage/README.md +43 -0
  35. package/dist/packs/raydium-compute-zero-slippage/brainblast-pack.yaml +5 -0
  36. package/dist/packs/raydium-compute-zero-slippage/fixtures/raydium-compute-zero-slippage/fixed/swap.ts +12 -0
  37. package/dist/packs/raydium-compute-zero-slippage/fixtures/raydium-compute-zero-slippage/vulnerable/swap.ts +12 -0
  38. package/dist/packs/raydium-compute-zero-slippage/rules/raydium-compute-zero-slippage.yaml +40 -0
  39. package/dist/packs/solana-sendtx-unconfirmed/README.md +34 -0
  40. package/dist/packs/solana-sendtx-unconfirmed/brainblast-pack.yaml +5 -0
  41. package/dist/packs/solana-sendtx-unconfirmed/fixtures/solana-sendtx-unconfirmed/fixed/payment.ts +10 -0
  42. package/dist/packs/solana-sendtx-unconfirmed/fixtures/solana-sendtx-unconfirmed/vulnerable/payment.ts +10 -0
  43. package/dist/packs/solana-sendtx-unconfirmed/rules/solana-sendtx-unconfirmed.yaml +40 -0
  44. package/dist/packs/spl-transfer-not-checked-in-payout/brainblast-pack.yaml +5 -0
  45. package/dist/packs/spl-transfer-not-checked-in-payout/fixtures/spl-transfer-not-checked-in-payout/fixed/payout-processor.ts +29 -0
  46. package/dist/packs/spl-transfer-not-checked-in-payout/fixtures/spl-transfer-not-checked-in-payout/vulnerable/payout-processor.ts +22 -0
  47. package/dist/packs/spl-transfer-not-checked-in-payout/rules/spl-transfer-not-checked-in-payout.yaml +49 -0
  48. package/dist/rules/metaplex-seller-fee-zero.yaml +41 -0
  49. package/package.json +1 -1
@@ -7,7 +7,7 @@ import {
7
7
  loadPack,
8
8
  resolveRules,
9
9
  walk
10
- } from "./chunk-5H5JQXXU.js";
10
+ } from "./chunk-Q5DMQN67.js";
11
11
 
12
12
  // src/costAnalysis.ts
13
13
  import { Project, SyntaxKind } from "ts-morph";
@@ -521,12 +521,62 @@ function validatePack(dir) {
521
521
  return { manifest, rules, ruleResults, ok };
522
522
  }
523
523
 
524
+ // src/bundledPacks.ts
525
+ import { existsSync as existsSync2, readdirSync, statSync, readFileSync as readFileSync2 } from "fs";
526
+ import { join as join2 } from "path";
527
+ import { fileURLToPath } from "url";
528
+ import { parse } from "yaml";
529
+ function packsRoot() {
530
+ const here = fileURLToPath(new URL(".", import.meta.url));
531
+ const candidates = [
532
+ join2(here, "packs"),
533
+ // dist/packs (npm — copied by postbuild)
534
+ join2(here, "..", "..", "..", "packs"),
535
+ // src/../../../packs (dev — repo root)
536
+ join2(here, "..", "packs")
537
+ // fallback
538
+ ];
539
+ return candidates.find((p) => existsSync2(p)) ?? null;
540
+ }
541
+ function listBundledPacks() {
542
+ const root = packsRoot();
543
+ if (!root) return [];
544
+ const out = [];
545
+ for (const entry of readdirSync(root)) {
546
+ const dir = join2(root, entry);
547
+ const manifestPath = join2(dir, PACK_MANIFEST_FILE);
548
+ let isDir = false;
549
+ try {
550
+ isDir = statSync(dir).isDirectory();
551
+ } catch {
552
+ continue;
553
+ }
554
+ if (!isDir || !existsSync2(manifestPath)) continue;
555
+ try {
556
+ const manifest = parse(readFileSync2(manifestPath, "utf8"));
557
+ if (manifest?.id) out.push({ id: manifest.id, dir, manifest });
558
+ } catch {
559
+ }
560
+ }
561
+ return out.sort((a, b) => a.id.localeCompare(b.id));
562
+ }
563
+ function resolveBundledPackToken(token) {
564
+ const packs = listBundledPacks();
565
+ const exact = packs.find((p) => p.id === token);
566
+ if (exact) return exact.dir;
567
+ const lead = packs.filter((p) => p.id === token || p.id.startsWith(token + "-"));
568
+ if (lead.length === 1) return lead[0].dir;
569
+ const sub = packs.filter((p) => p.id.includes(token));
570
+ if (sub.length === 1) return sub[0].dir;
571
+ return null;
572
+ }
573
+
524
574
  // src/telemetry.ts
525
575
  import { createHash, randomUUID } from "crypto";
526
- import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
576
+ import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
527
577
  import { execFileSync } from "child_process";
528
578
  import { homedir } from "os";
529
- import { dirname, join as join2, resolve } from "path";
579
+ import { dirname, join as join3, resolve } from "path";
530
580
  function sha256Hex(s) {
531
581
  return createHash("sha256").update(s).digest("hex");
532
582
  }
@@ -534,20 +584,20 @@ function isTelemetryEnabled(targetDir) {
534
584
  const env = process.env.BRAINBLAST_TELEMETRY;
535
585
  if (env === "1" || env === "true") return true;
536
586
  if (env === "0" || env === "false") return false;
537
- const configPath = join2(targetDir, ".agent-research", "config.json");
538
- if (!existsSync2(configPath)) return false;
587
+ const configPath = join3(targetDir, ".agent-research", "config.json");
588
+ if (!existsSync3(configPath)) return false;
539
589
  try {
540
- const cfg = JSON.parse(readFileSync2(configPath, "utf8"));
590
+ const cfg = JSON.parse(readFileSync3(configPath, "utf8"));
541
591
  return cfg?.telemetry === true;
542
592
  } catch {
543
593
  return false;
544
594
  }
545
595
  }
546
596
  function getUserHash() {
547
- const idPath = join2(homedir(), ".brainblast", "telemetry-id");
597
+ const idPath = join3(homedir(), ".brainblast", "telemetry-id");
548
598
  let id;
549
- if (existsSync2(idPath)) {
550
- id = readFileSync2(idPath, "utf8").trim();
599
+ if (existsSync3(idPath)) {
600
+ id = readFileSync3(idPath, "utf8").trim();
551
601
  } else {
552
602
  id = randomUUID();
553
603
  mkdirSync2(dirname(idPath), { recursive: true });
@@ -569,15 +619,15 @@ function getRepoHash(targetDir) {
569
619
  return sha256Hex(key).slice(0, 16);
570
620
  }
571
621
  function telemetryFilePath(targetDir) {
572
- return join2(targetDir, ".agent-research", "telemetry.ndjson");
622
+ return join3(targetDir, ".agent-research", "telemetry.ndjson");
573
623
  }
574
624
  var DEFAULT_REGISTRY_URL = "https://registry.brainblast.tech";
575
625
  async function submitTelemetry(targetDir, registryUrl = process.env.BRAINBLAST_REGISTRY_URL || DEFAULT_REGISTRY_URL) {
576
626
  const file = telemetryFilePath(targetDir);
577
- if (!existsSync2(file)) {
627
+ if (!existsSync3(file)) {
578
628
  return { submitted: 0, accepted: 0, rejected: 0, graduations: [] };
579
629
  }
580
- const events = readFileSync2(file, "utf8").trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
630
+ const events = readFileSync3(file, "utf8").trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
581
631
  if (events.length === 0) {
582
632
  return { submitted: 0, accepted: 0, rejected: 0, graduations: [] };
583
633
  }
@@ -622,6 +672,8 @@ export {
622
672
  applyDiffToFile,
623
673
  initPack,
624
674
  validatePack,
675
+ listBundledPacks,
676
+ resolveBundledPackToken,
625
677
  isTelemetryEnabled,
626
678
  getUserHash,
627
679
  getRepoHash,
@@ -0,0 +1,129 @@
1
+ // src/feeConfigs.ts
2
+ var FEE_CONFIGS = [
3
+ {
4
+ id: "metaplex-seller-fee",
5
+ category: "royalty",
6
+ sdk: "Metaplex Token Metadata",
7
+ call: "createV1 / createNft / createFungible",
8
+ field: "sellerFeeBasisPoints",
9
+ whatZeroMeans: "Omitted \u2192 defaults to 0. Creators earn no royalty on any secondary sale, permanently \u2014 the token mints fine and looks correct on-chain.",
10
+ fix: "Set sellerFeeBasisPoints explicitly at mint time (e.g. 500 = 5%). There is no after-the-fact migration once minted.",
11
+ ruleId: "metaplex-seller-fee-zero",
12
+ status: "enforced",
13
+ docsUrl: "https://developers.metaplex.com/token-metadata/mint"
14
+ },
15
+ {
16
+ id: "bags-fee-share-creator",
17
+ category: "fee",
18
+ sdk: "Bags",
19
+ call: "createBagsFeeShareConfig",
20
+ field: "feeClaimers[].userBps (creator inclusion)",
21
+ whatZeroMeans: "The creator wallet omitted from feeClaimers, or the userBps not summing to 10000 \u2192 the creator's share of trading fees is silently zero. This is the original Bags trap.",
22
+ fix: "Include the creator wallet as a feeClaimers entry and ensure every userBps sums to 10000.",
23
+ ruleId: "bags-fee-share-creator-included",
24
+ status: "enforced",
25
+ docsUrl: "https://bags.fm"
26
+ },
27
+ {
28
+ id: "token2022-transfer-fee",
29
+ category: "fee",
30
+ sdk: "SPL Token-2022 (Transfer Fee extension)",
31
+ call: "createInitializeTransferFeeConfigInstruction",
32
+ field: "transferFeeBasisPoints",
33
+ whatZeroMeans: "Initializing the transfer-fee extension with 0 basis points \u2192 no fee is ever withheld on transfers. The extension is 'configured' but collects nothing.",
34
+ fix: "Pass a non-zero transferFeeBasisPoints (and a sensible maximumFee) when initializing the extension.",
35
+ // Positional-argument call — not an object-literal field, so the bundled
36
+ // object-field checker doesn't cover it yet. Advisory until a positional
37
+ // variant ships.
38
+ ruleId: null,
39
+ status: "advisory",
40
+ docsUrl: "https://www.solana-program.com/docs/token-2022/extensions#transfer-fee"
41
+ },
42
+ {
43
+ id: "reward-rate-zero",
44
+ category: "reward",
45
+ sdk: "Staking / LP reward distributors (generic)",
46
+ call: "initialize / configureReward (varies)",
47
+ field: "rewardRate / emissionsPerSecond",
48
+ whatZeroMeans: "A reward-rate or emissions field omitted/zeroed \u2192 stakers and LPs accrue nothing while the pool looks live. A silent, ongoing zero-yield misconfiguration.",
49
+ fix: "Set the reward-rate field explicitly and assert it is non-zero in your deploy script; add a project-local fee-configs-zero-or-missing rule for your SDK's call shape.",
50
+ ruleId: null,
51
+ status: "advisory"
52
+ }
53
+ ];
54
+ function getFeeConfig(id) {
55
+ return FEE_CONFIGS.find((e) => e.id === id || e.ruleId === id);
56
+ }
57
+ function feeConfigsByCategory(cat) {
58
+ return FEE_CONFIGS.filter((e) => e.category === cat);
59
+ }
60
+ function enforcedCount(patterns = FEE_CONFIGS) {
61
+ return patterns.filter((e) => e.status === "enforced").length;
62
+ }
63
+ var CATEGORY_LABEL = {
64
+ royalty: "Royalties",
65
+ fee: "Fees",
66
+ reward: "Reward distribution"
67
+ };
68
+ function renderFeeConfigsMd(patterns = FEE_CONFIGS) {
69
+ const L = ["## Fee Configs \u2014 silent zero-revenue class\n"];
70
+ L.push(
71
+ "The Bags exploit generalized: a revenue-bearing field that, if omitted or zeroed, silently collects nothing \u2014 forever. Watch these across every protocol that touches fees, royalties, or rewards.\n"
72
+ );
73
+ L.push("| Category | SDK | Field | Status | Rule |");
74
+ L.push("|----------|-----|-------|--------|------|");
75
+ for (const e of patterns) {
76
+ const status = e.status === "enforced" ? "\u2705 enforced" : "\u26A0\uFE0F advisory";
77
+ L.push(`| ${CATEGORY_LABEL[e.category]} | ${e.sdk} | \`${e.field}\` | ${status} | ${e.ruleId ? `\`${e.ruleId}\`` : "\u2014"} |`);
78
+ }
79
+ L.push("");
80
+ L.push(`**${enforcedCount(patterns)} of ${patterns.length} enforced by a bundled rule; the rest are advisories (grep targets / project-local rule candidates).**
81
+ `);
82
+ for (const e of patterns) {
83
+ L.push(`### ${e.sdk} \u2014 \`${e.field}\` (${CATEGORY_LABEL[e.category]})
84
+ `);
85
+ L.push(`- **Call:** \`${e.call}\``);
86
+ L.push(`- **Zero/omitted means:** ${e.whatZeroMeans}`);
87
+ L.push(`- **Fix:** ${e.fix}`);
88
+ L.push(`- **Detection:** ${e.ruleId ? `\`${e.ruleId}\` (bundled)` : "advisory \u2014 no bundled rule yet"}`);
89
+ if (e.docsUrl) L.push(`- **Docs:** ${e.docsUrl}`);
90
+ L.push("");
91
+ }
92
+ return L.join("\n");
93
+ }
94
+ function renderFeeConfigsText(patterns = FEE_CONFIGS) {
95
+ const L = [];
96
+ L.push("\u2500\u2500 Fee Configs \u2014 silent zero-revenue class \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
97
+ L.push(" fields that default to zero and silently collect nothing");
98
+ L.push("");
99
+ for (const e of patterns) {
100
+ const status = e.status === "enforced" ? "[enforced]" : "[advisory]";
101
+ L.push(` ${status} ${CATEGORY_LABEL[e.category]}: ${e.sdk} \xB7 ${e.field}`);
102
+ L.push(` ${e.whatZeroMeans}`);
103
+ if (e.ruleId) L.push(` rule: ${e.ruleId}`);
104
+ L.push("");
105
+ }
106
+ L.push(` ${enforcedCount(patterns)}/${patterns.length} enforced by a bundled rule`);
107
+ return L.join("\n");
108
+ }
109
+ function renderFeeConfigDetailText(e) {
110
+ const L = [];
111
+ L.push(`\u2500\u2500 ${e.sdk} \u2014 ${e.field} \u2500\u2500`);
112
+ L.push(` category: ${CATEGORY_LABEL[e.category]}`);
113
+ L.push(` call: ${e.call}`);
114
+ L.push(` zero means: ${e.whatZeroMeans}`);
115
+ L.push(` fix: ${e.fix}`);
116
+ L.push(` detection: ${e.ruleId ? `${e.ruleId} (bundled rule)` : "advisory \u2014 no bundled rule yet"}`);
117
+ if (e.docsUrl) L.push(` docs: ${e.docsUrl}`);
118
+ return L.join("\n");
119
+ }
120
+
121
+ export {
122
+ FEE_CONFIGS,
123
+ getFeeConfig,
124
+ feeConfigsByCategory,
125
+ enforcedCount,
126
+ renderFeeConfigsMd,
127
+ renderFeeConfigsText,
128
+ renderFeeConfigDetailText
129
+ };
@@ -430,6 +430,19 @@ var objectArgPropertyLiteralEquals = (c, p) => {
430
430
 
431
431
  // src/checkers/objectArgPropertyForbiddenLiteral.ts
432
432
  import { SyntaxKind as SyntaxKind7 } from "ts-morph";
433
+ function isBnWrappedZero(init) {
434
+ const isNewOrCall = init.getKind() === SyntaxKind7.NewExpression || init.getKind() === SyntaxKind7.CallExpression;
435
+ if (!isNewOrCall) return false;
436
+ const calleeText = init.getExpression?.()?.getText?.() ?? "";
437
+ if (!/(^|\.)bn$/i.test(calleeText)) return false;
438
+ const args = init.getArguments?.() ?? [];
439
+ if (args.length !== 1) return false;
440
+ const a = args[0];
441
+ const k = a.getKind();
442
+ if (k === SyntaxKind7.NumericLiteral) return Number(a.getLiteralValue()) === 0;
443
+ if (k === SyntaxKind7.StringLiteral) return a.getLiteralValue() === "0";
444
+ return false;
445
+ }
433
446
  var objectArgPropertyForbiddenLiteral = (c, p) => {
434
447
  const calls = c.fn.getDescendantsOfKind(SyntaxKind7.CallExpression).filter((ce2) => {
435
448
  const expr = ce2.getExpression();
@@ -469,7 +482,8 @@ var objectArgPropertyForbiddenLiteral = (c, p) => {
469
482
  const kind = init.getKind();
470
483
  const text = init.getText();
471
484
  const forbidden = JSON.stringify(p.forbiddenValue);
472
- const isForbidden = (kind === SyntaxKind7.NumericLiteral || kind === SyntaxKind7.StringLiteral) && (text === forbidden || text === String(p.forbiddenValue));
485
+ const isLiteralForbidden = (kind === SyntaxKind7.NumericLiteral || kind === SyntaxKind7.StringLiteral) && (text === forbidden || text === String(p.forbiddenValue));
486
+ const isForbidden = isLiteralForbidden || p.forbiddenValue === 0 && isBnWrappedZero(init);
473
487
  if (isForbidden) {
474
488
  return {
475
489
  result: "fail",
@@ -1050,6 +1064,75 @@ function anchorCpiUnverifiedProgram(c, p) {
1050
1064
  };
1051
1065
  }
1052
1066
 
1067
+ // src/checkers/feeConfigsZeroOrMissing.ts
1068
+ import { SyntaxKind as SyntaxKind12 } from "ts-morph";
1069
+ function callName6(call) {
1070
+ const exp = call.getExpression();
1071
+ if (exp.getKind() === SyntaxKind12.Identifier) return exp.getText();
1072
+ if (exp.getKind() === SyntaxKind12.PropertyAccessExpression) {
1073
+ return exp.asKind(SyntaxKind12.PropertyAccessExpression).getName();
1074
+ }
1075
+ return "";
1076
+ }
1077
+ function unwrap(expr) {
1078
+ let e = expr;
1079
+ while (e) {
1080
+ const k = e.getKind();
1081
+ if (k === SyntaxKind12.AsExpression || k === SyntaxKind12.ParenthesizedExpression || k === SyntaxKind12.TypeAssertionExpression || k === SyntaxKind12.NonNullExpression) {
1082
+ e = e.getExpression();
1083
+ } else break;
1084
+ }
1085
+ return e;
1086
+ }
1087
+ function isLiteralZero(expr) {
1088
+ const u = unwrap(expr);
1089
+ if (!u) return false;
1090
+ const num = u.asKind(SyntaxKind12.NumericLiteral);
1091
+ if (num) return Number(num.getLiteralValue()) === 0;
1092
+ const big = u.asKind(SyntaxKind12.BigIntLiteral);
1093
+ if (big) return /^0n?$/.test(big.getText().replace(/_/g, ""));
1094
+ return false;
1095
+ }
1096
+ var feeConfigsZeroOrMissing = (c, p) => {
1097
+ const names = Array.isArray(p.calls) ? p.calls : [p.calls];
1098
+ const field = p.field;
1099
+ const calls = c.fn.getDescendantsOfKind(SyntaxKind12.CallExpression).filter((x) => names.includes(callName6(x)));
1100
+ if (calls.length === 0) {
1101
+ return {
1102
+ result: "cant_tell",
1103
+ detail: p.absentDetail ?? `No call to ${names.join("/")} in '${c.fnName}'; fee-configs check does not apply.`
1104
+ };
1105
+ }
1106
+ for (const call of calls) {
1107
+ const obj = call.getArguments().map((a) => unwrap(a)?.asKind(SyntaxKind12.ObjectLiteralExpression)).find((o) => !!o);
1108
+ if (!obj) continue;
1109
+ const member = obj.getProperty(field);
1110
+ if (!member) {
1111
+ return {
1112
+ result: "fail",
1113
+ detail: p.omittedDetail ?? `'${field}' is omitted from the ${callName6(call)} config \u2014 it defaults to zero. This is a permanent, silent zero-revenue misconfiguration: the call succeeds and no value is ever collected. Set '${field}' explicitly.`
1114
+ };
1115
+ }
1116
+ const pa = member.asKind(SyntaxKind12.PropertyAssignment);
1117
+ const shorthand = member.asKind(SyntaxKind12.ShorthandPropertyAssignment);
1118
+ const init = pa?.getInitializer() ?? shorthand?.getNameNode();
1119
+ if (isLiteralZero(init)) {
1120
+ return {
1121
+ result: "fail",
1122
+ detail: p.zeroDetail ?? `'${field}' is set to a literal 0 in the ${callName6(call)} config \u2014 zero revenue will ever be collected. If intentional, this is a footgun; otherwise set a non-zero value.`
1123
+ };
1124
+ }
1125
+ return {
1126
+ result: "pass",
1127
+ detail: p.passDetail ?? `'${field}' is set to a non-zero value in the ${callName6(call)} config.`
1128
+ };
1129
+ }
1130
+ return {
1131
+ result: "cant_tell",
1132
+ detail: p.absentDetail ?? `Call to ${names.join("/")} found but its config argument isn't an object literal; can't validate '${field}'.`
1133
+ };
1134
+ };
1135
+
1053
1136
  // src/checkers/index.ts
1054
1137
  var registry = {
1055
1138
  "positional-arg-identity": positionalArgIdentity,
@@ -1068,7 +1151,8 @@ var registry = {
1068
1151
  "anchor-account-missing-constraint": anchorAccountMissingConstraint,
1069
1152
  "anchor-forbidden-account-type": anchorForbiddenAccountType,
1070
1153
  "anchor-body-call-pattern": anchorBodyCallPattern,
1071
- "anchor-cpi-unverified-program": anchorCpiUnverifiedProgram
1154
+ "anchor-cpi-unverified-program": anchorCpiUnverifiedProgram,
1155
+ "fee-configs-zero-or-missing": feeConfigsZeroOrMissing
1072
1156
  };
1073
1157
  function runChecker(kind, c, params) {
1074
1158
  const fn = registry[kind];
@@ -1284,7 +1368,7 @@ function findRustCandidates(targetDir, rule) {
1284
1368
  }
1285
1369
 
1286
1370
  // src/fixers/positionalArgIdentity.ts
1287
- import { SyntaxKind as SyntaxKind12 } from "ts-morph";
1371
+ import { SyntaxKind as SyntaxKind13 } from "ts-morph";
1288
1372
 
1289
1373
  // src/fixers/diffUtil.ts
1290
1374
  function buildDiff(node, replacement) {
@@ -1309,9 +1393,9 @@ function buildDiff(node, replacement) {
1309
1393
  // src/fixers/positionalArgIdentity.ts
1310
1394
  var fixPositionalArgIdentity = (c, p, outcome) => {
1311
1395
  if (outcome.result !== "fail") return void 0;
1312
- const calls = c.fn.getDescendantsOfKind(SyntaxKind12.CallExpression).filter((call) => {
1396
+ const calls = c.fn.getDescendantsOfKind(SyntaxKind13.CallExpression).filter((call) => {
1313
1397
  const exp = call.getExpression();
1314
- return exp.getKind() === SyntaxKind12.PropertyAccessExpression && exp.asKind(SyntaxKind12.PropertyAccessExpression).getName() === p.call;
1398
+ return exp.getKind() === SyntaxKind13.PropertyAccessExpression && exp.asKind(SyntaxKind13.PropertyAccessExpression).getName() === p.call;
1315
1399
  });
1316
1400
  if (calls.length === 0) {
1317
1401
  const wantParam2 = c.params[p.paramIndex] ?? "<rawBodyParam>";
@@ -1326,7 +1410,7 @@ Do not call JSON.parse() on the body before this verification step.`
1326
1410
  }
1327
1411
  const arg = calls[0].getArguments()[p.argIndex];
1328
1412
  const wantParam = c.params[p.paramIndex];
1329
- if (arg && wantParam && arg.getKind() === SyntaxKind12.CallExpression) {
1413
+ if (arg && wantParam && arg.getKind() === SyntaxKind13.CallExpression) {
1330
1414
  return {
1331
1415
  summary: `Pass the raw body parameter '${wantParam}' to ${p.call} instead of a parsed value`,
1332
1416
  diff: buildDiff(arg, wantParam)
@@ -1336,12 +1420,12 @@ Do not call JSON.parse() on the body before this verification step.`
1336
1420
  };
1337
1421
 
1338
1422
  // src/fixers/requiredCallWithOptions.ts
1339
- import { SyntaxKind as SyntaxKind13 } from "ts-morph";
1340
- function callName6(call) {
1423
+ import { SyntaxKind as SyntaxKind14 } from "ts-morph";
1424
+ function callName7(call) {
1341
1425
  const exp = call.getExpression();
1342
- if (exp.getKind() === SyntaxKind13.Identifier) return exp.getText();
1343
- if (exp.getKind() === SyntaxKind13.PropertyAccessExpression) {
1344
- return exp.asKind(SyntaxKind13.PropertyAccessExpression).getName();
1426
+ if (exp.getKind() === SyntaxKind14.Identifier) return exp.getText();
1427
+ if (exp.getKind() === SyntaxKind14.PropertyAccessExpression) {
1428
+ return exp.asKind(SyntaxKind14.PropertyAccessExpression).getName();
1345
1429
  }
1346
1430
  return "";
1347
1431
  }
@@ -1359,15 +1443,15 @@ function placeholderFor(propName) {
1359
1443
  }
1360
1444
  var fixRequiredCallWithOptions = (c, p, outcome) => {
1361
1445
  if (outcome.result !== "fail") return void 0;
1362
- const calls = c.fn.getDescendantsOfKind(SyntaxKind13.CallExpression);
1363
- const verify = calls.filter((x) => p.verifyCalls.includes(callName6(x)));
1446
+ const calls = c.fn.getDescendantsOfKind(SyntaxKind14.CallExpression);
1447
+ const verify = calls.filter((x) => p.verifyCalls.includes(callName7(x)));
1364
1448
  if (verify.length > 0) {
1365
1449
  const call = verify[0];
1366
1450
  const args = call.getArguments();
1367
1451
  const lastArg = args[args.length - 1];
1368
- const obj = lastArg?.asKind(SyntaxKind13.ObjectLiteralExpression);
1452
+ const obj = lastArg?.asKind(SyntaxKind14.ObjectLiteralExpression);
1369
1453
  const presentNames = obj ? obj.getProperties().map((pr) => {
1370
- const pa = pr.asKind(SyntaxKind13.PropertyAssignment) ?? pr.asKind(SyntaxKind13.ShorthandPropertyAssignment);
1454
+ const pa = pr.asKind(SyntaxKind14.PropertyAssignment) ?? pr.asKind(SyntaxKind14.ShorthandPropertyAssignment);
1371
1455
  return pa?.getName() ?? "";
1372
1456
  }) : [];
1373
1457
  const missingGroups = p.requiredProps.filter(
@@ -1375,7 +1459,7 @@ var fixRequiredCallWithOptions = (c, p, outcome) => {
1375
1459
  );
1376
1460
  if (missingGroups.length === 0) return void 0;
1377
1461
  const newProps = missingGroups.map((g) => placeholderFor(g[0])).join(", ");
1378
- const summary = `Add ${missingGroups.map((g) => g[0]).join(" and ")} to the ${callName6(call)} call`;
1462
+ const summary = `Add ${missingGroups.map((g) => g[0]).join(" and ")} to the ${callName7(call)} call`;
1379
1463
  if (obj) {
1380
1464
  const inner = obj.getText().slice(1, -1).trim();
1381
1465
  const newText = inner.length > 0 ? `{ ${inner}, ${newProps} }` : `{ ${newProps} }`;
@@ -1387,7 +1471,7 @@ var fixRequiredCallWithOptions = (c, p, outcome) => {
1387
1471
  }
1388
1472
  return {
1389
1473
  summary,
1390
- suggestion: `Add an options object ({ ${newProps} }) as an argument to ${callName6(call)}.`
1474
+ suggestion: `Add an options object ({ ${newProps} }) as an argument to ${callName7(call)}.`
1391
1475
  };
1392
1476
  }
1393
1477
  return {
@@ -1401,22 +1485,22 @@ JWKS must come from Privy's published JWKS endpoint for your app.`
1401
1485
  };
1402
1486
 
1403
1487
  // src/fixers/literalMultiplierWrongConstant.ts
1404
- import { SyntaxKind as SyntaxKind14 } from "ts-morph";
1405
- function callName7(call) {
1488
+ import { SyntaxKind as SyntaxKind15 } from "ts-morph";
1489
+ function callName8(call) {
1406
1490
  const exp = call.getExpression();
1407
- if (exp.getKind() === SyntaxKind14.Identifier) return exp.getText();
1408
- if (exp.getKind() === SyntaxKind14.PropertyAccessExpression) {
1409
- return exp.asKind(SyntaxKind14.PropertyAccessExpression).getName();
1491
+ if (exp.getKind() === SyntaxKind15.Identifier) return exp.getText();
1492
+ if (exp.getKind() === SyntaxKind15.PropertyAccessExpression) {
1493
+ return exp.asKind(SyntaxKind15.PropertyAccessExpression).getName();
1410
1494
  }
1411
1495
  return "";
1412
1496
  }
1413
1497
  function findIdentifier(node, name) {
1414
- if (node.getKind() === SyntaxKind14.Identifier && node.getText() === name) return node;
1415
- return node.getDescendantsOfKind(SyntaxKind14.Identifier).find((id) => id.getText() === name);
1498
+ if (node.getKind() === SyntaxKind15.Identifier && node.getText() === name) return node;
1499
+ return node.getDescendantsOfKind(SyntaxKind15.Identifier).find((id) => id.getText() === name);
1416
1500
  }
1417
1501
  var fixLiteralMultiplierWrongConstant = (c, p, outcome) => {
1418
1502
  if (outcome.result !== "fail") return void 0;
1419
- const calls = c.fn.getDescendantsOfKind(SyntaxKind14.CallExpression).filter((call) => callName7(call) === p.call);
1503
+ const calls = c.fn.getDescendantsOfKind(SyntaxKind15.CallExpression).filter((call) => callName8(call) === p.call);
1420
1504
  if (calls.length === 0) return void 0;
1421
1505
  const arg = calls[0].getArguments()[p.argIndex];
1422
1506
  if (!arg) return void 0;
package/dist/cli.js CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  initPack,
8
8
  isTelemetryEnabled,
9
9
  lamportsToSol,
10
+ listBundledPacks,
10
11
  parseDiff,
11
12
  recordGraduationEvents,
12
13
  renderCostReportMd,
@@ -14,11 +15,12 @@ import {
14
15
  renderExploitsMd,
15
16
  renderExploitsText,
16
17
  rentExemptMinimum,
18
+ resolveBundledPackToken,
17
19
  startWatch,
18
20
  submitTelemetry,
19
21
  telemetryFilePath,
20
22
  validatePack
21
- } from "./chunk-5VYTURTO.js";
23
+ } from "./chunk-A3U6JDMN.js";
22
24
  import {
23
25
  renderTrustGraphMd
24
26
  } from "./chunk-QMJEZ6NO.js";
@@ -32,7 +34,7 @@ import {
32
34
  audit,
33
35
  getChangedRanges,
34
36
  resolveRules
35
- } from "./chunk-5H5JQXXU.js";
37
+ } from "./chunk-Q5DMQN67.js";
36
38
  import "./chunk-2XJORJPQ.js";
37
39
  import "./chunk-O5Z4ZJHC.js";
38
40
  import "./chunk-XSVQSK53.js";
@@ -42,7 +44,7 @@ import {
42
44
  import "./chunk-3RG5ZIWI.js";
43
45
 
44
46
  // src/cli.ts
45
- import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
47
+ import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "fs";
46
48
  import { join as join3 } from "path";
47
49
 
48
50
  // src/memory.ts
@@ -469,7 +471,16 @@ function parsePackDirs(argv) {
469
471
  if (idx < 0) return [];
470
472
  const value = argv[idx + 1];
471
473
  if (!value || value.startsWith("--")) return [];
472
- return value.split(",").map((s) => s.trim()).filter(Boolean);
474
+ const tokens = value.split(",").map((s) => s.trim()).filter(Boolean);
475
+ return tokens.map((t) => {
476
+ if (existsSync3(join3(t, "brainblast-pack.yaml"))) return t;
477
+ const resolved = resolveBundledPackToken(t);
478
+ if (resolved) return resolved;
479
+ console.error(
480
+ `brainblast: --packs '${t}' is not a known bundled pack or a pack directory. Run 'brainblast packs' to list bundled packs.`
481
+ );
482
+ process.exit(2);
483
+ });
473
484
  }
474
485
  if (args[0] === "diff") {
475
486
  await runDiff(args.slice(1));
@@ -480,7 +491,7 @@ if (args[0] === "drift") {
480
491
  process.exit(0);
481
492
  }
482
493
  if (args[0] === "mcp") {
483
- const { startMcpServer } = await import("./mcp-EOTWSQK7.js");
494
+ const { startMcpServer } = await import("./mcp-O45PSOVU.js");
484
495
  await startMcpServer();
485
496
  process.exit(0);
486
497
  }
@@ -492,6 +503,10 @@ if (args[0] === "pack") {
492
503
  runPack(args.slice(1));
493
504
  process.exit(0);
494
505
  }
506
+ if (args[0] === "packs") {
507
+ runPacks(args.slice(1));
508
+ process.exit(0);
509
+ }
495
510
  if (args[0] === "telemetry") {
496
511
  await runTelemetry(args.slice(1));
497
512
  process.exit(0);
@@ -567,6 +582,10 @@ if (args[0] === "oracle") {
567
582
  await runOracle(args.slice(1));
568
583
  process.exit(0);
569
584
  }
585
+ if (args[0] === "fee-configs") {
586
+ await runFeeConfigs(args.slice(1));
587
+ process.exit(0);
588
+ }
570
589
  if (args[0] === "fix") {
571
590
  await runFix(args.slice(1));
572
591
  process.exit(0);
@@ -704,6 +723,45 @@ function runDeployPlan(argv) {
704
723
  writeFileSync2(mdPath, renderDeployPlanMd(plan));
705
724
  console.log(` deploy plan: ${mdPath}`);
706
725
  }
726
+ async function runFeeConfigs(argv) {
727
+ const {
728
+ FEE_CONFIGS,
729
+ getFeeConfig,
730
+ renderFeeConfigsText,
731
+ renderFeeConfigsMd,
732
+ renderFeeConfigDetailText
733
+ } = await import("./feeConfigs-S4E5GQ7Q.js");
734
+ if (argv.includes("--help") || argv.includes("-h")) {
735
+ console.log("usage: brainblast fee-configs [id] [--json]");
736
+ console.log(" Fee Config Validator: the silent zero-revenue class (fees, royalties,");
737
+ console.log(" rewards) \u2014 fields that default to zero and quietly collect nothing. Pass an");
738
+ console.log(" id to see one in detail. Known ids:");
739
+ console.log(` ${FEE_CONFIGS.map((e) => e.id).join(", ")}`);
740
+ process.exit(0);
741
+ }
742
+ const json = argv.includes("--json");
743
+ const id = argv.find((a) => !a.startsWith("--"));
744
+ if (id) {
745
+ const e = getFeeConfig(id);
746
+ if (!e) {
747
+ console.error(`error: no fee-config '${id}'. Known: ${FEE_CONFIGS.map((x) => x.id).join(", ")}`);
748
+ process.exit(2);
749
+ }
750
+ if (json) console.log(JSON.stringify(e, null, 2));
751
+ else console.log(renderFeeConfigDetailText(e));
752
+ return;
753
+ }
754
+ if (json) {
755
+ console.log(JSON.stringify(FEE_CONFIGS, null, 2));
756
+ return;
757
+ }
758
+ console.log(renderFeeConfigsText());
759
+ const outDir2 = join3(process.cwd(), ".agent-research");
760
+ mkdirSync2(outDir2, { recursive: true });
761
+ writeFileSync2(join3(outDir2, "fee-configs.md"), renderFeeConfigsMd());
762
+ console.log(`
763
+ catalog: ${join3(outDir2, "fee-configs.md")}`);
764
+ }
707
765
  async function runOracle(argv) {
708
766
  if (argv.includes("--help") || argv.includes("-h") || argv.filter((a) => !a.startsWith("--")).length === 0) {
709
767
  console.log("usage: brainblast oracle <account> [--rpc URL] [--max-staleness-slots N | --max-staleness-seconds N] [--json]");
@@ -779,6 +837,27 @@ function runExploits(argv) {
779
837
  console.log(`
780
838
  database: ${mdPath}`);
781
839
  }
840
+ function runPacks(argv) {
841
+ const packs = listBundledPacks();
842
+ if (argv.includes("--json")) {
843
+ console.log(JSON.stringify(packs.map((p) => ({ ...p.manifest, dir: p.dir })), null, 2));
844
+ return;
845
+ }
846
+ if (packs.length === 0) {
847
+ console.log("No bundled protocol packs found.");
848
+ return;
849
+ }
850
+ console.log("Protocol Pack Library \u2014 opt into the exact stack you build on:\n");
851
+ console.log(" brainblast --packs <name>[,<name>...] .\n");
852
+ for (const p of packs) {
853
+ const lead = p.id.split("-")[0];
854
+ const shortName = resolveBundledPackToken(lead) === p.dir ? lead : p.id;
855
+ console.log(` ${shortName.padEnd(12)} ${p.manifest.name}`);
856
+ if (p.manifest.description) console.log(` ${" ".repeat(12)} ${p.manifest.description}`);
857
+ console.log("");
858
+ }
859
+ console.log(`${packs.length} pack(s). Each ships RED\u2192GREEN fixtures; run 'brainblast pack validate <dir>' to verify.`);
860
+ }
782
861
  function runPack(argv) {
783
862
  const sub = argv[0];
784
863
  if (sub === "init") {
@@ -0,0 +1,19 @@
1
+ import {
2
+ FEE_CONFIGS,
3
+ enforcedCount,
4
+ feeConfigsByCategory,
5
+ getFeeConfig,
6
+ renderFeeConfigDetailText,
7
+ renderFeeConfigsMd,
8
+ renderFeeConfigsText
9
+ } from "./chunk-CSYGLMZR.js";
10
+ import "./chunk-3RG5ZIWI.js";
11
+ export {
12
+ FEE_CONFIGS,
13
+ enforcedCount,
14
+ feeConfigsByCategory,
15
+ getFeeConfig,
16
+ renderFeeConfigDetailText,
17
+ renderFeeConfigsMd,
18
+ renderFeeConfigsText
19
+ };