brainblast 0.7.4 → 0.7.5
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/{chunk-5VYTURTO.js → chunk-HFYBK2VA.js} +1 -1
- package/dist/chunk-S7X53LWF.js +129 -0
- package/dist/{chunk-5H5JQXXU.js → chunk-V4XS5DKD.js} +94 -24
- package/dist/cli.js +46 -3
- package/dist/index.d.ts +31 -1
- package/dist/index.js +23 -7
- package/dist/{mcp-EOTWSQK7.js → mcp-LITBQHBF.js} +1 -1
- package/dist/rules/metaplex-seller-fee-zero.yaml +41 -0
- package/dist/tokenEconomics-HBF3DYNH.js +19 -0
- package/package.json +1 -1
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// src/tokenEconomics.ts
|
|
2
|
+
var ECONOMIC_PATTERNS = [
|
|
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 economic-value-zero-or-missing rule for your SDK's call shape.",
|
|
50
|
+
ruleId: null,
|
|
51
|
+
status: "advisory"
|
|
52
|
+
}
|
|
53
|
+
];
|
|
54
|
+
function getEconomicPattern(id) {
|
|
55
|
+
return ECONOMIC_PATTERNS.find((e) => e.id === id || e.ruleId === id);
|
|
56
|
+
}
|
|
57
|
+
function economicPatternsByCategory(cat) {
|
|
58
|
+
return ECONOMIC_PATTERNS.filter((e) => e.category === cat);
|
|
59
|
+
}
|
|
60
|
+
function enforcedCount(patterns = ECONOMIC_PATTERNS) {
|
|
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 renderEconomicsMd(patterns = ECONOMIC_PATTERNS) {
|
|
69
|
+
const L = ["## Token Economics \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 renderEconomicsText(patterns = ECONOMIC_PATTERNS) {
|
|
95
|
+
const L = [];
|
|
96
|
+
L.push("\u2500\u2500 Token Economics \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 renderEconomicDetailText(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
|
+
ECONOMIC_PATTERNS,
|
|
123
|
+
getEconomicPattern,
|
|
124
|
+
economicPatternsByCategory,
|
|
125
|
+
enforcedCount,
|
|
126
|
+
renderEconomicsMd,
|
|
127
|
+
renderEconomicsText,
|
|
128
|
+
renderEconomicDetailText
|
|
129
|
+
};
|
|
@@ -1050,6 +1050,75 @@ function anchorCpiUnverifiedProgram(c, p) {
|
|
|
1050
1050
|
};
|
|
1051
1051
|
}
|
|
1052
1052
|
|
|
1053
|
+
// src/checkers/economicValueZeroOrMissing.ts
|
|
1054
|
+
import { SyntaxKind as SyntaxKind12 } from "ts-morph";
|
|
1055
|
+
function callName6(call) {
|
|
1056
|
+
const exp = call.getExpression();
|
|
1057
|
+
if (exp.getKind() === SyntaxKind12.Identifier) return exp.getText();
|
|
1058
|
+
if (exp.getKind() === SyntaxKind12.PropertyAccessExpression) {
|
|
1059
|
+
return exp.asKind(SyntaxKind12.PropertyAccessExpression).getName();
|
|
1060
|
+
}
|
|
1061
|
+
return "";
|
|
1062
|
+
}
|
|
1063
|
+
function unwrap(expr) {
|
|
1064
|
+
let e = expr;
|
|
1065
|
+
while (e) {
|
|
1066
|
+
const k = e.getKind();
|
|
1067
|
+
if (k === SyntaxKind12.AsExpression || k === SyntaxKind12.ParenthesizedExpression || k === SyntaxKind12.TypeAssertionExpression || k === SyntaxKind12.NonNullExpression) {
|
|
1068
|
+
e = e.getExpression();
|
|
1069
|
+
} else break;
|
|
1070
|
+
}
|
|
1071
|
+
return e;
|
|
1072
|
+
}
|
|
1073
|
+
function isLiteralZero(expr) {
|
|
1074
|
+
const u = unwrap(expr);
|
|
1075
|
+
if (!u) return false;
|
|
1076
|
+
const num = u.asKind(SyntaxKind12.NumericLiteral);
|
|
1077
|
+
if (num) return Number(num.getLiteralValue()) === 0;
|
|
1078
|
+
const big = u.asKind(SyntaxKind12.BigIntLiteral);
|
|
1079
|
+
if (big) return /^0n?$/.test(big.getText().replace(/_/g, ""));
|
|
1080
|
+
return false;
|
|
1081
|
+
}
|
|
1082
|
+
var economicValueZeroOrMissing = (c, p) => {
|
|
1083
|
+
const names = Array.isArray(p.calls) ? p.calls : [p.calls];
|
|
1084
|
+
const field = p.field;
|
|
1085
|
+
const calls = c.fn.getDescendantsOfKind(SyntaxKind12.CallExpression).filter((x) => names.includes(callName6(x)));
|
|
1086
|
+
if (calls.length === 0) {
|
|
1087
|
+
return {
|
|
1088
|
+
result: "cant_tell",
|
|
1089
|
+
detail: p.absentDetail ?? `No call to ${names.join("/")} in '${c.fnName}'; economic-value check does not apply.`
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
for (const call of calls) {
|
|
1093
|
+
const obj = call.getArguments().map((a) => unwrap(a)?.asKind(SyntaxKind12.ObjectLiteralExpression)).find((o) => !!o);
|
|
1094
|
+
if (!obj) continue;
|
|
1095
|
+
const member = obj.getProperty(field);
|
|
1096
|
+
if (!member) {
|
|
1097
|
+
return {
|
|
1098
|
+
result: "fail",
|
|
1099
|
+
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.`
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
const pa = member.asKind(SyntaxKind12.PropertyAssignment);
|
|
1103
|
+
const shorthand = member.asKind(SyntaxKind12.ShorthandPropertyAssignment);
|
|
1104
|
+
const init = pa?.getInitializer() ?? shorthand?.getNameNode();
|
|
1105
|
+
if (isLiteralZero(init)) {
|
|
1106
|
+
return {
|
|
1107
|
+
result: "fail",
|
|
1108
|
+
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.`
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
return {
|
|
1112
|
+
result: "pass",
|
|
1113
|
+
detail: p.passDetail ?? `'${field}' is set to a non-zero value in the ${callName6(call)} config.`
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
return {
|
|
1117
|
+
result: "cant_tell",
|
|
1118
|
+
detail: p.absentDetail ?? `Call to ${names.join("/")} found but its config argument isn't an object literal; can't validate '${field}'.`
|
|
1119
|
+
};
|
|
1120
|
+
};
|
|
1121
|
+
|
|
1053
1122
|
// src/checkers/index.ts
|
|
1054
1123
|
var registry = {
|
|
1055
1124
|
"positional-arg-identity": positionalArgIdentity,
|
|
@@ -1068,7 +1137,8 @@ var registry = {
|
|
|
1068
1137
|
"anchor-account-missing-constraint": anchorAccountMissingConstraint,
|
|
1069
1138
|
"anchor-forbidden-account-type": anchorForbiddenAccountType,
|
|
1070
1139
|
"anchor-body-call-pattern": anchorBodyCallPattern,
|
|
1071
|
-
"anchor-cpi-unverified-program": anchorCpiUnverifiedProgram
|
|
1140
|
+
"anchor-cpi-unverified-program": anchorCpiUnverifiedProgram,
|
|
1141
|
+
"economic-value-zero-or-missing": economicValueZeroOrMissing
|
|
1072
1142
|
};
|
|
1073
1143
|
function runChecker(kind, c, params) {
|
|
1074
1144
|
const fn = registry[kind];
|
|
@@ -1284,7 +1354,7 @@ function findRustCandidates(targetDir, rule) {
|
|
|
1284
1354
|
}
|
|
1285
1355
|
|
|
1286
1356
|
// src/fixers/positionalArgIdentity.ts
|
|
1287
|
-
import { SyntaxKind as
|
|
1357
|
+
import { SyntaxKind as SyntaxKind13 } from "ts-morph";
|
|
1288
1358
|
|
|
1289
1359
|
// src/fixers/diffUtil.ts
|
|
1290
1360
|
function buildDiff(node, replacement) {
|
|
@@ -1309,9 +1379,9 @@ function buildDiff(node, replacement) {
|
|
|
1309
1379
|
// src/fixers/positionalArgIdentity.ts
|
|
1310
1380
|
var fixPositionalArgIdentity = (c, p, outcome) => {
|
|
1311
1381
|
if (outcome.result !== "fail") return void 0;
|
|
1312
|
-
const calls = c.fn.getDescendantsOfKind(
|
|
1382
|
+
const calls = c.fn.getDescendantsOfKind(SyntaxKind13.CallExpression).filter((call) => {
|
|
1313
1383
|
const exp = call.getExpression();
|
|
1314
|
-
return exp.getKind() ===
|
|
1384
|
+
return exp.getKind() === SyntaxKind13.PropertyAccessExpression && exp.asKind(SyntaxKind13.PropertyAccessExpression).getName() === p.call;
|
|
1315
1385
|
});
|
|
1316
1386
|
if (calls.length === 0) {
|
|
1317
1387
|
const wantParam2 = c.params[p.paramIndex] ?? "<rawBodyParam>";
|
|
@@ -1326,7 +1396,7 @@ Do not call JSON.parse() on the body before this verification step.`
|
|
|
1326
1396
|
}
|
|
1327
1397
|
const arg = calls[0].getArguments()[p.argIndex];
|
|
1328
1398
|
const wantParam = c.params[p.paramIndex];
|
|
1329
|
-
if (arg && wantParam && arg.getKind() ===
|
|
1399
|
+
if (arg && wantParam && arg.getKind() === SyntaxKind13.CallExpression) {
|
|
1330
1400
|
return {
|
|
1331
1401
|
summary: `Pass the raw body parameter '${wantParam}' to ${p.call} instead of a parsed value`,
|
|
1332
1402
|
diff: buildDiff(arg, wantParam)
|
|
@@ -1336,12 +1406,12 @@ Do not call JSON.parse() on the body before this verification step.`
|
|
|
1336
1406
|
};
|
|
1337
1407
|
|
|
1338
1408
|
// src/fixers/requiredCallWithOptions.ts
|
|
1339
|
-
import { SyntaxKind as
|
|
1340
|
-
function
|
|
1409
|
+
import { SyntaxKind as SyntaxKind14 } from "ts-morph";
|
|
1410
|
+
function callName7(call) {
|
|
1341
1411
|
const exp = call.getExpression();
|
|
1342
|
-
if (exp.getKind() ===
|
|
1343
|
-
if (exp.getKind() ===
|
|
1344
|
-
return exp.asKind(
|
|
1412
|
+
if (exp.getKind() === SyntaxKind14.Identifier) return exp.getText();
|
|
1413
|
+
if (exp.getKind() === SyntaxKind14.PropertyAccessExpression) {
|
|
1414
|
+
return exp.asKind(SyntaxKind14.PropertyAccessExpression).getName();
|
|
1345
1415
|
}
|
|
1346
1416
|
return "";
|
|
1347
1417
|
}
|
|
@@ -1359,15 +1429,15 @@ function placeholderFor(propName) {
|
|
|
1359
1429
|
}
|
|
1360
1430
|
var fixRequiredCallWithOptions = (c, p, outcome) => {
|
|
1361
1431
|
if (outcome.result !== "fail") return void 0;
|
|
1362
|
-
const calls = c.fn.getDescendantsOfKind(
|
|
1363
|
-
const verify = calls.filter((x) => p.verifyCalls.includes(
|
|
1432
|
+
const calls = c.fn.getDescendantsOfKind(SyntaxKind14.CallExpression);
|
|
1433
|
+
const verify = calls.filter((x) => p.verifyCalls.includes(callName7(x)));
|
|
1364
1434
|
if (verify.length > 0) {
|
|
1365
1435
|
const call = verify[0];
|
|
1366
1436
|
const args = call.getArguments();
|
|
1367
1437
|
const lastArg = args[args.length - 1];
|
|
1368
|
-
const obj = lastArg?.asKind(
|
|
1438
|
+
const obj = lastArg?.asKind(SyntaxKind14.ObjectLiteralExpression);
|
|
1369
1439
|
const presentNames = obj ? obj.getProperties().map((pr) => {
|
|
1370
|
-
const pa = pr.asKind(
|
|
1440
|
+
const pa = pr.asKind(SyntaxKind14.PropertyAssignment) ?? pr.asKind(SyntaxKind14.ShorthandPropertyAssignment);
|
|
1371
1441
|
return pa?.getName() ?? "";
|
|
1372
1442
|
}) : [];
|
|
1373
1443
|
const missingGroups = p.requiredProps.filter(
|
|
@@ -1375,7 +1445,7 @@ var fixRequiredCallWithOptions = (c, p, outcome) => {
|
|
|
1375
1445
|
);
|
|
1376
1446
|
if (missingGroups.length === 0) return void 0;
|
|
1377
1447
|
const newProps = missingGroups.map((g) => placeholderFor(g[0])).join(", ");
|
|
1378
|
-
const summary = `Add ${missingGroups.map((g) => g[0]).join(" and ")} to the ${
|
|
1448
|
+
const summary = `Add ${missingGroups.map((g) => g[0]).join(" and ")} to the ${callName7(call)} call`;
|
|
1379
1449
|
if (obj) {
|
|
1380
1450
|
const inner = obj.getText().slice(1, -1).trim();
|
|
1381
1451
|
const newText = inner.length > 0 ? `{ ${inner}, ${newProps} }` : `{ ${newProps} }`;
|
|
@@ -1387,7 +1457,7 @@ var fixRequiredCallWithOptions = (c, p, outcome) => {
|
|
|
1387
1457
|
}
|
|
1388
1458
|
return {
|
|
1389
1459
|
summary,
|
|
1390
|
-
suggestion: `Add an options object ({ ${newProps} }) as an argument to ${
|
|
1460
|
+
suggestion: `Add an options object ({ ${newProps} }) as an argument to ${callName7(call)}.`
|
|
1391
1461
|
};
|
|
1392
1462
|
}
|
|
1393
1463
|
return {
|
|
@@ -1401,22 +1471,22 @@ JWKS must come from Privy's published JWKS endpoint for your app.`
|
|
|
1401
1471
|
};
|
|
1402
1472
|
|
|
1403
1473
|
// src/fixers/literalMultiplierWrongConstant.ts
|
|
1404
|
-
import { SyntaxKind as
|
|
1405
|
-
function
|
|
1474
|
+
import { SyntaxKind as SyntaxKind15 } from "ts-morph";
|
|
1475
|
+
function callName8(call) {
|
|
1406
1476
|
const exp = call.getExpression();
|
|
1407
|
-
if (exp.getKind() ===
|
|
1408
|
-
if (exp.getKind() ===
|
|
1409
|
-
return exp.asKind(
|
|
1477
|
+
if (exp.getKind() === SyntaxKind15.Identifier) return exp.getText();
|
|
1478
|
+
if (exp.getKind() === SyntaxKind15.PropertyAccessExpression) {
|
|
1479
|
+
return exp.asKind(SyntaxKind15.PropertyAccessExpression).getName();
|
|
1410
1480
|
}
|
|
1411
1481
|
return "";
|
|
1412
1482
|
}
|
|
1413
1483
|
function findIdentifier(node, name) {
|
|
1414
|
-
if (node.getKind() ===
|
|
1415
|
-
return node.getDescendantsOfKind(
|
|
1484
|
+
if (node.getKind() === SyntaxKind15.Identifier && node.getText() === name) return node;
|
|
1485
|
+
return node.getDescendantsOfKind(SyntaxKind15.Identifier).find((id) => id.getText() === name);
|
|
1416
1486
|
}
|
|
1417
1487
|
var fixLiteralMultiplierWrongConstant = (c, p, outcome) => {
|
|
1418
1488
|
if (outcome.result !== "fail") return void 0;
|
|
1419
|
-
const calls = c.fn.getDescendantsOfKind(
|
|
1489
|
+
const calls = c.fn.getDescendantsOfKind(SyntaxKind15.CallExpression).filter((call) => callName8(call) === p.call);
|
|
1420
1490
|
if (calls.length === 0) return void 0;
|
|
1421
1491
|
const arg = calls[0].getArguments()[p.argIndex];
|
|
1422
1492
|
if (!arg) return void 0;
|
package/dist/cli.js
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
submitTelemetry,
|
|
19
19
|
telemetryFilePath,
|
|
20
20
|
validatePack
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-HFYBK2VA.js";
|
|
22
22
|
import {
|
|
23
23
|
renderTrustGraphMd
|
|
24
24
|
} from "./chunk-QMJEZ6NO.js";
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
audit,
|
|
33
33
|
getChangedRanges,
|
|
34
34
|
resolveRules
|
|
35
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-V4XS5DKD.js";
|
|
36
36
|
import "./chunk-2XJORJPQ.js";
|
|
37
37
|
import "./chunk-O5Z4ZJHC.js";
|
|
38
38
|
import "./chunk-XSVQSK53.js";
|
|
@@ -480,7 +480,7 @@ if (args[0] === "drift") {
|
|
|
480
480
|
process.exit(0);
|
|
481
481
|
}
|
|
482
482
|
if (args[0] === "mcp") {
|
|
483
|
-
const { startMcpServer } = await import("./mcp-
|
|
483
|
+
const { startMcpServer } = await import("./mcp-LITBQHBF.js");
|
|
484
484
|
await startMcpServer();
|
|
485
485
|
process.exit(0);
|
|
486
486
|
}
|
|
@@ -567,6 +567,10 @@ if (args[0] === "oracle") {
|
|
|
567
567
|
await runOracle(args.slice(1));
|
|
568
568
|
process.exit(0);
|
|
569
569
|
}
|
|
570
|
+
if (args[0] === "economics") {
|
|
571
|
+
await runEconomics(args.slice(1));
|
|
572
|
+
process.exit(0);
|
|
573
|
+
}
|
|
570
574
|
if (args[0] === "fix") {
|
|
571
575
|
await runFix(args.slice(1));
|
|
572
576
|
process.exit(0);
|
|
@@ -704,6 +708,45 @@ function runDeployPlan(argv) {
|
|
|
704
708
|
writeFileSync2(mdPath, renderDeployPlanMd(plan));
|
|
705
709
|
console.log(` deploy plan: ${mdPath}`);
|
|
706
710
|
}
|
|
711
|
+
async function runEconomics(argv) {
|
|
712
|
+
const {
|
|
713
|
+
ECONOMIC_PATTERNS,
|
|
714
|
+
getEconomicPattern,
|
|
715
|
+
renderEconomicsText,
|
|
716
|
+
renderEconomicsMd,
|
|
717
|
+
renderEconomicDetailText
|
|
718
|
+
} = await import("./tokenEconomics-HBF3DYNH.js");
|
|
719
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
720
|
+
console.log("usage: brainblast economics [id] [--json]");
|
|
721
|
+
console.log(" Token Economics Validator: the silent zero-revenue class (fees, royalties,");
|
|
722
|
+
console.log(" rewards) \u2014 fields that default to zero and quietly collect nothing. Pass an");
|
|
723
|
+
console.log(" id to see one in detail. Known ids:");
|
|
724
|
+
console.log(` ${ECONOMIC_PATTERNS.map((e) => e.id).join(", ")}`);
|
|
725
|
+
process.exit(0);
|
|
726
|
+
}
|
|
727
|
+
const json = argv.includes("--json");
|
|
728
|
+
const id = argv.find((a) => !a.startsWith("--"));
|
|
729
|
+
if (id) {
|
|
730
|
+
const e = getEconomicPattern(id);
|
|
731
|
+
if (!e) {
|
|
732
|
+
console.error(`error: no economic pattern '${id}'. Known: ${ECONOMIC_PATTERNS.map((x) => x.id).join(", ")}`);
|
|
733
|
+
process.exit(2);
|
|
734
|
+
}
|
|
735
|
+
if (json) console.log(JSON.stringify(e, null, 2));
|
|
736
|
+
else console.log(renderEconomicDetailText(e));
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
if (json) {
|
|
740
|
+
console.log(JSON.stringify(ECONOMIC_PATTERNS, null, 2));
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
console.log(renderEconomicsText());
|
|
744
|
+
const outDir2 = join3(process.cwd(), ".agent-research");
|
|
745
|
+
mkdirSync2(outDir2, { recursive: true });
|
|
746
|
+
writeFileSync2(join3(outDir2, "token-economics.md"), renderEconomicsMd());
|
|
747
|
+
console.log(`
|
|
748
|
+
catalog: ${join3(outDir2, "token-economics.md")}`);
|
|
749
|
+
}
|
|
707
750
|
async function runOracle(argv) {
|
|
708
751
|
if (argv.includes("--help") || argv.includes("-h") || argv.filter((a) => !a.startsWith("--")).length === 0) {
|
|
709
752
|
console.log("usage: brainblast oracle <account> [--rpc URL] [--max-staleness-slots N | --max-staleness-seconds N] [--json]");
|
package/dist/index.d.ts
CHANGED
|
@@ -918,6 +918,36 @@ declare function checkOracleFreshness(account: string, opts?: OracleOpts): Promi
|
|
|
918
918
|
declare function renderOracleText(f: OracleFreshness): string;
|
|
919
919
|
declare function renderOracleMd(f: OracleFreshness): string;
|
|
920
920
|
|
|
921
|
+
type EconomicCategory = "royalty" | "fee" | "reward";
|
|
922
|
+
type EconomicStatus = "enforced" | "advisory";
|
|
923
|
+
interface EconomicPattern {
|
|
924
|
+
/** Stable slug. */
|
|
925
|
+
id: string;
|
|
926
|
+
category: EconomicCategory;
|
|
927
|
+
/** SDK / protocol the field belongs to. */
|
|
928
|
+
sdk: string;
|
|
929
|
+
/** The setup/config call that takes the field. */
|
|
930
|
+
call: string;
|
|
931
|
+
/** The revenue-bearing field that silently defaults to zero. */
|
|
932
|
+
field: string;
|
|
933
|
+
/** What a zero/omitted value costs you, in one sentence. */
|
|
934
|
+
whatZeroMeans: string;
|
|
935
|
+
/** The safe pattern. */
|
|
936
|
+
fix: string;
|
|
937
|
+
/** Bundled rule that detects this, or null for an advisory entry. */
|
|
938
|
+
ruleId: string | null;
|
|
939
|
+
status: EconomicStatus;
|
|
940
|
+
/** Optional reference / docs URL. */
|
|
941
|
+
docsUrl?: string;
|
|
942
|
+
}
|
|
943
|
+
declare const ECONOMIC_PATTERNS: EconomicPattern[];
|
|
944
|
+
declare function getEconomicPattern(id: string): EconomicPattern | undefined;
|
|
945
|
+
declare function economicPatternsByCategory(cat: EconomicCategory): EconomicPattern[];
|
|
946
|
+
declare function enforcedCount(patterns?: EconomicPattern[]): number;
|
|
947
|
+
declare function renderEconomicsMd(patterns?: EconomicPattern[]): string;
|
|
948
|
+
declare function renderEconomicsText(patterns?: EconomicPattern[]): string;
|
|
949
|
+
declare function renderEconomicDetailText(e: EconomicPattern): string;
|
|
950
|
+
|
|
921
951
|
interface ExploitPattern {
|
|
922
952
|
/** Stable slug, e.g. "wormhole". */
|
|
923
953
|
id: string;
|
|
@@ -947,4 +977,4 @@ declare function renderExploitsMd(patterns?: ExploitPattern[]): string;
|
|
|
947
977
|
declare function renderExploitsText(patterns?: ExploitPattern[]): string;
|
|
948
978
|
declare function renderExploitDetailText(e: ExploitPattern): string;
|
|
949
979
|
|
|
950
|
-
export { type AccountFlow, type AnchorIdl, type AuditRef, type AuthorityClassification, type BatchResult, type BatchRow, type BatchScanOpts, type BuildOpts, CANONICAL_BY_MINT, CANONICAL_MINTS, type Candidate, type CanonicalMint, type ChainEvent, type ChainWatchOpts, type ChainWatchState, type ChangedRanges, type CheckOutcome, type CheckResult, type CheckResultKind, type Checker, type ConfigCandidate, type ConfigChecker, type CostReport, DEFAULT_REGISTRY_URL, DEFAULT_STALENESS_SLOTS, DEFAULT_TTL_HOURS, type DecodedInstruction, type DecodedTx, type DiffResult, type DriftAdvisory, type DriftBaseline, type DriftPackage, type DriftResult, EXPLOIT_PATTERNS, type ExploitPattern, type FirewallFinding, type FirewallOpts, type FirewallProgram, type FirewallReport, type FirewallSeverity, type FirewallVerdict, type Grade, type GraduationEvent, type IdentityStatus, type IdlAccount, type IdlConstraintParams, type IdlInstruction, KNOWN_AUTHORITY_OWNERS, KNOWN_PROGRAMS, type MintInfo, type OnChainProgram, type OracleFreshness, type OracleOpts, type OracleVerdict, type OsvAdvisory, PACK_MANIFEST_FILE, type PackInitOptions, type PackManifest, type PackRuleValidation, type PackValidateResult, type ParityNote, type ParsedDiff, type PreflightCheck, type PreflightOpts, type PreflightReport, type PreflightStatus, type PreflightVerdict, type PriorityFeePosture, type ProgramCache, type ProgramCacheEntry, type Recoverability, type RicoOutcome, type RicoResult, type RicoTokenSecurity, type Rule, type RustAccountField, type RustCandidate, type RustChecker, SYSTEM_PROGRAM, type ScoreFactor, type Severity, type TelemetrySubmitResult, type TokenIdentity, type TrustGraph, type TrustScore, type UpgradeAuthority, type UpgradeAuthorityKind, type UpgradeAuthoritySource, type VerifiedBuildState, type VerifyOpts, type WatchEvent, type WatchOptions, analyzeCosts, analyzeInstructions, analyzeToken, applyDiffToFile, audit, auditWithRule, base58Decode, base58Encode, batchScan, buildConstraintParams, buildTrustGraph, rules as bundledRules, cacheSize, canonicalMintForSymbol, checkDrift, checkOracleFreshness, checkerKinds, classifyUpgradeAuthority, decodeTransaction, defaultCachePath, deployerFlagsFrom, diffVersions, enrichAuthorityClassification, fileChanged, findCandidates, findConfigCandidates, formatUsd, generateRulesFromIdl, generateTestForResult, getCacheEntry, getCacheEntryMeta, getChangedRanges, getExploitPattern, getRepoHash, getUserHash, getWorkingTreeChanges, gradeAtLeast, gradeForScore, idlProgramName, initPack, initialChainWatchState, inspectTransaction, isCanonicalMint, isEntryExpired, isTelemetryEnabled, isValidSolanaAddress, lamportsToSol, loadDirectory, loadPack, loadPacksFromDir, loadProgramCache, loadRules, parseCpiPrograms, parseDiff, parseIdl, parseMintAccount, parseMintList, pollChainOnce, pumpPreflight, putCacheEntry, queryOsv, rangeChanged, recordGraduationEvents, renderBatchText, renderCostReportMd, renderDiffMd, renderDiffText, renderDriftText, renderExploitDetailText, renderExploitsMd, renderExploitsText, renderFirewallText, renderOracleMd, renderOracleText, renderPreflightText, renderRicoText, renderRulesYaml, renderScoreText, renderTest, renderTrustGraphMd, rentExemptMinimum, resolveRules, riskScore, runChecker, runIncrementalScan, saveProgramCache, scoreFromProgram, scoreProgram, seedPackages, startChainWatch, startWatch, submitTelemetry, telemetryFilePath, testKinds, toSnakeCase, totalLossUsd, validatePack, validatePackManifest, verifyTokenIdentity };
|
|
980
|
+
export { type AccountFlow, type AnchorIdl, type AuditRef, type AuthorityClassification, type BatchResult, type BatchRow, type BatchScanOpts, type BuildOpts, CANONICAL_BY_MINT, CANONICAL_MINTS, type Candidate, type CanonicalMint, type ChainEvent, type ChainWatchOpts, type ChainWatchState, type ChangedRanges, type CheckOutcome, type CheckResult, type CheckResultKind, type Checker, type ConfigCandidate, type ConfigChecker, type CostReport, DEFAULT_REGISTRY_URL, DEFAULT_STALENESS_SLOTS, DEFAULT_TTL_HOURS, type DecodedInstruction, type DecodedTx, type DiffResult, type DriftAdvisory, type DriftBaseline, type DriftPackage, type DriftResult, ECONOMIC_PATTERNS, EXPLOIT_PATTERNS, type EconomicCategory, type EconomicPattern, type EconomicStatus, type ExploitPattern, type FirewallFinding, type FirewallOpts, type FirewallProgram, type FirewallReport, type FirewallSeverity, type FirewallVerdict, type Grade, type GraduationEvent, type IdentityStatus, type IdlAccount, type IdlConstraintParams, type IdlInstruction, KNOWN_AUTHORITY_OWNERS, KNOWN_PROGRAMS, type MintInfo, type OnChainProgram, type OracleFreshness, type OracleOpts, type OracleVerdict, type OsvAdvisory, PACK_MANIFEST_FILE, type PackInitOptions, type PackManifest, type PackRuleValidation, type PackValidateResult, type ParityNote, type ParsedDiff, type PreflightCheck, type PreflightOpts, type PreflightReport, type PreflightStatus, type PreflightVerdict, type PriorityFeePosture, type ProgramCache, type ProgramCacheEntry, type Recoverability, type RicoOutcome, type RicoResult, type RicoTokenSecurity, type Rule, type RustAccountField, type RustCandidate, type RustChecker, SYSTEM_PROGRAM, type ScoreFactor, type Severity, type TelemetrySubmitResult, type TokenIdentity, type TrustGraph, type TrustScore, type UpgradeAuthority, type UpgradeAuthorityKind, type UpgradeAuthoritySource, type VerifiedBuildState, type VerifyOpts, type WatchEvent, type WatchOptions, analyzeCosts, analyzeInstructions, analyzeToken, applyDiffToFile, audit, auditWithRule, base58Decode, base58Encode, batchScan, buildConstraintParams, buildTrustGraph, rules as bundledRules, cacheSize, canonicalMintForSymbol, checkDrift, checkOracleFreshness, checkerKinds, classifyUpgradeAuthority, decodeTransaction, defaultCachePath, deployerFlagsFrom, diffVersions, economicPatternsByCategory, enforcedCount, enrichAuthorityClassification, fileChanged, findCandidates, findConfigCandidates, formatUsd, generateRulesFromIdl, generateTestForResult, getCacheEntry, getCacheEntryMeta, getChangedRanges, getEconomicPattern, getExploitPattern, getRepoHash, getUserHash, getWorkingTreeChanges, gradeAtLeast, gradeForScore, idlProgramName, initPack, initialChainWatchState, inspectTransaction, isCanonicalMint, isEntryExpired, isTelemetryEnabled, isValidSolanaAddress, lamportsToSol, loadDirectory, loadPack, loadPacksFromDir, loadProgramCache, loadRules, parseCpiPrograms, parseDiff, parseIdl, parseMintAccount, parseMintList, pollChainOnce, pumpPreflight, putCacheEntry, queryOsv, rangeChanged, recordGraduationEvents, renderBatchText, renderCostReportMd, renderDiffMd, renderDiffText, renderDriftText, renderEconomicDetailText, renderEconomicsMd, renderEconomicsText, renderExploitDetailText, renderExploitsMd, renderExploitsText, renderFirewallText, renderOracleMd, renderOracleText, renderPreflightText, renderRicoText, renderRulesYaml, renderScoreText, renderTest, renderTrustGraphMd, rentExemptMinimum, resolveRules, riskScore, runChecker, runIncrementalScan, saveProgramCache, scoreFromProgram, scoreProgram, seedPackages, startChainWatch, startWatch, submitTelemetry, telemetryFilePath, testKinds, toSnakeCase, totalLossUsd, validatePack, validatePackManifest, verifyTokenIdentity };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
batchScan,
|
|
3
|
+
parseMintList,
|
|
4
|
+
renderBatchText
|
|
5
|
+
} from "./chunk-QC27GNQ7.js";
|
|
6
|
+
import {
|
|
7
|
+
ECONOMIC_PATTERNS,
|
|
8
|
+
economicPatternsByCategory,
|
|
9
|
+
enforcedCount,
|
|
10
|
+
getEconomicPattern,
|
|
11
|
+
renderEconomicDetailText,
|
|
12
|
+
renderEconomicsMd,
|
|
13
|
+
renderEconomicsText
|
|
14
|
+
} from "./chunk-S7X53LWF.js";
|
|
1
15
|
import {
|
|
2
16
|
DEFAULT_STALENESS_SLOTS,
|
|
3
17
|
checkOracleFreshness,
|
|
@@ -29,11 +43,6 @@ import {
|
|
|
29
43
|
pumpPreflight,
|
|
30
44
|
renderPreflightText
|
|
31
45
|
} from "./chunk-WX3IR7LK.js";
|
|
32
|
-
import {
|
|
33
|
-
batchScan,
|
|
34
|
-
parseMintList,
|
|
35
|
-
renderBatchText
|
|
36
|
-
} from "./chunk-QC27GNQ7.js";
|
|
37
46
|
import {
|
|
38
47
|
analyzeToken,
|
|
39
48
|
deployerFlagsFrom,
|
|
@@ -67,7 +76,7 @@ import {
|
|
|
67
76
|
telemetryFilePath,
|
|
68
77
|
totalLossUsd,
|
|
69
78
|
validatePack
|
|
70
|
-
} from "./chunk-
|
|
79
|
+
} from "./chunk-HFYBK2VA.js";
|
|
71
80
|
import {
|
|
72
81
|
renderTrustGraphMd
|
|
73
82
|
} from "./chunk-QMJEZ6NO.js";
|
|
@@ -108,7 +117,7 @@ import {
|
|
|
108
117
|
runChecker,
|
|
109
118
|
testKinds,
|
|
110
119
|
validatePackManifest
|
|
111
|
-
} from "./chunk-
|
|
120
|
+
} from "./chunk-V4XS5DKD.js";
|
|
112
121
|
import {
|
|
113
122
|
CANONICAL_BY_MINT,
|
|
114
123
|
CANONICAL_MINTS,
|
|
@@ -162,6 +171,7 @@ export {
|
|
|
162
171
|
DEFAULT_REGISTRY_URL,
|
|
163
172
|
DEFAULT_STALENESS_SLOTS,
|
|
164
173
|
DEFAULT_TTL_HOURS,
|
|
174
|
+
ECONOMIC_PATTERNS,
|
|
165
175
|
EXPLOIT_PATTERNS,
|
|
166
176
|
KNOWN_AUTHORITY_OWNERS,
|
|
167
177
|
KNOWN_PROGRAMS,
|
|
@@ -189,6 +199,8 @@ export {
|
|
|
189
199
|
defaultCachePath,
|
|
190
200
|
deployerFlagsFrom,
|
|
191
201
|
diffVersions,
|
|
202
|
+
economicPatternsByCategory,
|
|
203
|
+
enforcedCount,
|
|
192
204
|
enrichAuthorityClassification,
|
|
193
205
|
fileChanged,
|
|
194
206
|
findCandidates,
|
|
@@ -199,6 +211,7 @@ export {
|
|
|
199
211
|
getCacheEntry,
|
|
200
212
|
getCacheEntryMeta,
|
|
201
213
|
getChangedRanges,
|
|
214
|
+
getEconomicPattern,
|
|
202
215
|
getExploitPattern,
|
|
203
216
|
getRepoHash,
|
|
204
217
|
getUserHash,
|
|
@@ -235,6 +248,9 @@ export {
|
|
|
235
248
|
renderDiffMd,
|
|
236
249
|
renderDiffText,
|
|
237
250
|
renderDriftText,
|
|
251
|
+
renderEconomicDetailText,
|
|
252
|
+
renderEconomicsMd,
|
|
253
|
+
renderEconomicsText,
|
|
238
254
|
renderExploitDetailText,
|
|
239
255
|
renderExploitsMd,
|
|
240
256
|
renderExploitsText,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Token Economics Validator (v0.7.5) — the generalized Bags exploit.
|
|
2
|
+
# A revenue-bearing field omitted from a config object silently defaults to
|
|
3
|
+
# zero: the call succeeds, nothing is collected, forever. Here: Metaplex
|
|
4
|
+
# `sellerFeeBasisPoints` — the on-chain royalty creators earn on secondary
|
|
5
|
+
# sales. Omit it (or set 0) and the creators earn nothing, permanently.
|
|
6
|
+
id: metaplex-seller-fee-zero
|
|
7
|
+
severity: high
|
|
8
|
+
title: Metaplex token created with sellerFeeBasisPoints omitted or zero — creators earn no royalties
|
|
9
|
+
component:
|
|
10
|
+
name: Metaplex Token Metadata
|
|
11
|
+
type: Blockchain
|
|
12
|
+
version: unversioned
|
|
13
|
+
sourceUrl: https://developers.metaplex.com/token-metadata/mint
|
|
14
|
+
detect:
|
|
15
|
+
modules:
|
|
16
|
+
- "@metaplex-foundation/mpl-token-metadata"
|
|
17
|
+
- "@metaplex-foundation/js"
|
|
18
|
+
nameRegex: "metaplex|mpl|createNft|mint"
|
|
19
|
+
triggerCalls: [create, createV1, createNft, createFungible, createAndMint]
|
|
20
|
+
check:
|
|
21
|
+
kind: economic-value-zero-or-missing
|
|
22
|
+
params:
|
|
23
|
+
calls: [create, createV1, createNft, createFungible, createAndMint]
|
|
24
|
+
field: sellerFeeBasisPoints
|
|
25
|
+
omittedDetail: >-
|
|
26
|
+
The Metaplex create call omits sellerFeeBasisPoints, which defaults to 0.
|
|
27
|
+
The token mints fine, but creators earn ZERO royalties on every secondary
|
|
28
|
+
sale — permanently and silently. There is no after-the-fact fix once
|
|
29
|
+
minted. Set sellerFeeBasisPoints explicitly (e.g. 500 = 5%).
|
|
30
|
+
zeroDetail: >-
|
|
31
|
+
sellerFeeBasisPoints is set to a literal 0 — creators will earn no royalty
|
|
32
|
+
on secondary sales. If this token is meant to be royalty-free that's fine;
|
|
33
|
+
otherwise set a non-zero basis-points value.
|
|
34
|
+
passDetail: >-
|
|
35
|
+
sellerFeeBasisPoints is set to a non-zero value — secondary-sale royalties
|
|
36
|
+
are configured.
|
|
37
|
+
absentDetail: >-
|
|
38
|
+
Handler is in Metaplex scope but does not call a token-create function;
|
|
39
|
+
the royalty-allocation rule does not apply here.
|
|
40
|
+
test:
|
|
41
|
+
kind: none
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ECONOMIC_PATTERNS,
|
|
3
|
+
economicPatternsByCategory,
|
|
4
|
+
enforcedCount,
|
|
5
|
+
getEconomicPattern,
|
|
6
|
+
renderEconomicDetailText,
|
|
7
|
+
renderEconomicsMd,
|
|
8
|
+
renderEconomicsText
|
|
9
|
+
} from "./chunk-S7X53LWF.js";
|
|
10
|
+
import "./chunk-3RG5ZIWI.js";
|
|
11
|
+
export {
|
|
12
|
+
ECONOMIC_PATTERNS,
|
|
13
|
+
economicPatternsByCategory,
|
|
14
|
+
enforcedCount,
|
|
15
|
+
getEconomicPattern,
|
|
16
|
+
renderEconomicDetailText,
|
|
17
|
+
renderEconomicsMd,
|
|
18
|
+
renderEconomicsText
|
|
19
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brainblast",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.5",
|
|
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": [
|