brainblast 0.5.2 → 0.6.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.
@@ -421,6 +421,66 @@ var objectArgPropertyLiteralEquals = (c, p) => {
421
421
  };
422
422
  };
423
423
 
424
+ // src/checkers/objectArgPropertyForbiddenLiteral.ts
425
+ import { SyntaxKind as SyntaxKind7 } from "ts-morph";
426
+ var objectArgPropertyForbiddenLiteral = (c, p) => {
427
+ const calls = c.fn.getDescendantsOfKind(SyntaxKind7.CallExpression).filter((ce2) => {
428
+ const expr = ce2.getExpression();
429
+ if (expr.getKind() === SyntaxKind7.Identifier) return expr.getText() === p.call;
430
+ if (expr.getKind() === SyntaxKind7.PropertyAccessExpression) {
431
+ return expr.asKind(SyntaxKind7.PropertyAccessExpression).getName() === p.call;
432
+ }
433
+ return false;
434
+ });
435
+ if (calls.length === 0) {
436
+ return {
437
+ result: "cant_tell",
438
+ detail: p.absentCallDetail ?? `no ${p.call} call found`
439
+ };
440
+ }
441
+ const ce = calls[0];
442
+ const args = ce.getArguments();
443
+ const arg = args[p.argIndex];
444
+ const objLit = arg?.asKind(SyntaxKind7.ObjectLiteralExpression);
445
+ if (!objLit) {
446
+ return {
447
+ result: "cant_tell",
448
+ detail: p.absentArgDetail ?? `${p.call} arg[${p.argIndex}] is not an inline object literal \u2014 cannot statically inspect ${p.propName}`
449
+ };
450
+ }
451
+ const propAssignment = objLit.getProperties().map((prop) => prop.asKind(SyntaxKind7.PropertyAssignment)).find((pa) => pa?.getName() === p.propName);
452
+ if (!propAssignment) {
453
+ return {
454
+ result: "cant_tell",
455
+ detail: p.absentArgDetail ?? `${p.propName} is absent from the ${p.call} options`
456
+ };
457
+ }
458
+ const init = propAssignment.getInitializer();
459
+ if (!init) {
460
+ return { result: "cant_tell", detail: `${p.propName} has no initializer` };
461
+ }
462
+ const kind = init.getKind();
463
+ const text = init.getText();
464
+ const forbidden = JSON.stringify(p.forbiddenValue);
465
+ const isForbidden = (kind === SyntaxKind7.NumericLiteral || kind === SyntaxKind7.StringLiteral) && (text === forbidden || text === String(p.forbiddenValue));
466
+ if (isForbidden) {
467
+ return {
468
+ result: "fail",
469
+ detail: p.failDetail ?? `${p.propName} is ${p.forbiddenValue}`
470
+ };
471
+ }
472
+ if (kind === SyntaxKind7.NumericLiteral || kind === SyntaxKind7.StringLiteral) {
473
+ return {
474
+ result: "pass",
475
+ detail: p.passDetail ?? `${p.propName} is ${text}`
476
+ };
477
+ }
478
+ return {
479
+ result: "cant_tell",
480
+ detail: `${p.propName} is a non-literal expression \u2014 cannot determine statically`
481
+ };
482
+ };
483
+
424
484
  // src/checkers/anchorInitIfNeededGuarded.ts
425
485
  var GUARD_PATTERNS = [
426
486
  /\brequire!\s*\(/,
@@ -488,18 +548,18 @@ var envSecretsCommitted = (c, p) => {
488
548
  };
489
549
 
490
550
  // src/checkers/taintToSink.ts
491
- import { SyntaxKind as SyntaxKind7 } from "ts-morph";
551
+ import { SyntaxKind as SyntaxKind8 } from "ts-morph";
492
552
  function calleeName(call) {
493
553
  const exp = call.getExpression();
494
- if (exp.getKind() === SyntaxKind7.Identifier) return exp.getText();
495
- if (exp.getKind() === SyntaxKind7.PropertyAccessExpression) {
496
- return exp.asKind(SyntaxKind7.PropertyAccessExpression).getName();
554
+ if (exp.getKind() === SyntaxKind8.Identifier) return exp.getText();
555
+ if (exp.getKind() === SyntaxKind8.PropertyAccessExpression) {
556
+ return exp.asKind(SyntaxKind8.PropertyAccessExpression).getName();
497
557
  }
498
558
  return "";
499
559
  }
500
560
  function calleeIdentifierName(call) {
501
561
  const exp = call.getExpression();
502
- return exp.getKind() === SyntaxKind7.Identifier ? exp.getText() : void 0;
562
+ return exp.getKind() === SyntaxKind8.Identifier ? exp.getText() : void 0;
503
563
  }
504
564
  function wordIn(text, name) {
505
565
  return new RegExp(`\\b${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(text);
@@ -509,14 +569,14 @@ function matchesSource(text, sourceRes) {
509
569
  }
510
570
  function localTaintedNames(fn, sourceRes) {
511
571
  const names = /* @__PURE__ */ new Set();
512
- for (const decl of fn.getDescendantsOfKind(SyntaxKind7.VariableDeclaration)) {
572
+ for (const decl of fn.getDescendantsOfKind(SyntaxKind8.VariableDeclaration)) {
513
573
  const init = decl.getInitializer();
514
574
  if (init && matchesSource(init.getText(), sourceRes)) names.add(decl.getName());
515
575
  }
516
576
  return names;
517
577
  }
518
578
  function findDirectLeak(fn, sinkCalls, sourceRes, taintedNames) {
519
- for (const call of fn.getDescendantsOfKind(SyntaxKind7.CallExpression)) {
579
+ for (const call of fn.getDescendantsOfKind(SyntaxKind8.CallExpression)) {
520
580
  const name = calleeName(call);
521
581
  if (!sinkCalls.has(name)) continue;
522
582
  for (const arg of call.getArguments()) {
@@ -548,7 +608,7 @@ function resolveFunction(sourceFile, name) {
548
608
  }
549
609
  function findForwardLeak(fn, rootName, sinkCalls, sourceRes, taintedNames, hopsLeft, visited) {
550
610
  if (hopsLeft <= 0) return void 0;
551
- for (const call of fn.getDescendantsOfKind(SyntaxKind7.CallExpression)) {
611
+ for (const call of fn.getDescendantsOfKind(SyntaxKind8.CallExpression)) {
552
612
  const name = calleeIdentifierName(call);
553
613
  if (!name || name === rootName) continue;
554
614
  const args = call.getArguments();
@@ -590,7 +650,7 @@ function findForwardLeak(fn, rootName, sinkCalls, sourceRes, taintedNames, hopsL
590
650
  function paramsUsedInSink(fn, sinkCalls) {
591
651
  const params = new Set(fn.getParameters().map((p) => p.getName()));
592
652
  const sinked = /* @__PURE__ */ new Set();
593
- for (const call of fn.getDescendantsOfKind(SyntaxKind7.CallExpression)) {
653
+ for (const call of fn.getDescendantsOfKind(SyntaxKind8.CallExpression)) {
594
654
  if (!sinkCalls.has(calleeName(call))) continue;
595
655
  for (const arg of call.getArguments()) {
596
656
  const text = arg.getText();
@@ -603,13 +663,13 @@ function paramsUsedInSink(fn, sinkCalls) {
603
663
  }
604
664
  function enclosingFunction(node) {
605
665
  return node.getFirstAncestor(
606
- (a) => a.getKind() === SyntaxKind7.FunctionDeclaration || a.getKind() === SyntaxKind7.ArrowFunction
666
+ (a) => a.getKind() === SyntaxKind8.FunctionDeclaration || a.getKind() === SyntaxKind8.ArrowFunction
607
667
  );
608
668
  }
609
669
  function findBackwardLeak(candidateFn, fnName, candidateFile, params, sinkedParams, sourceRes) {
610
670
  const project = candidateFn.getProject();
611
671
  for (const sf of project.getSourceFiles()) {
612
- for (const call of sf.getDescendantsOfKind(SyntaxKind7.CallExpression)) {
672
+ for (const call of sf.getDescendantsOfKind(SyntaxKind8.CallExpression)) {
613
673
  if (calleeIdentifierName(call) !== fnName) continue;
614
674
  if (call.getFirstAncestor((a) => a === candidateFn)) continue;
615
675
  const args = call.getArguments();
@@ -621,7 +681,7 @@ function findBackwardLeak(candidateFn, fnName, candidateFile, params, sinkedPara
621
681
  if (matchesSource(text, sourceRes)) {
622
682
  return `'${fnName}' is called from ${sf.getFilePath()}:${call.getStartLineNumber()} with '${text}' as '${pname}', which this function passes to a sink.`;
623
683
  }
624
- if (arg.getKind() === SyntaxKind7.Identifier) {
684
+ if (arg.getKind() === SyntaxKind8.Identifier) {
625
685
  const callerFn = enclosingFunction(arg);
626
686
  if (callerFn) {
627
687
  const callerTainted = localTaintedNames(callerFn, sourceRes);
@@ -663,21 +723,21 @@ var taintToSink = (c, p) => {
663
723
  };
664
724
 
665
725
  // src/checkers/literalMultiplierWrongConstant.ts
666
- import { SyntaxKind as SyntaxKind8 } from "ts-morph";
726
+ import { SyntaxKind as SyntaxKind9 } from "ts-morph";
667
727
  function callName4(call) {
668
728
  const exp = call.getExpression();
669
- if (exp.getKind() === SyntaxKind8.Identifier) return exp.getText();
670
- if (exp.getKind() === SyntaxKind8.PropertyAccessExpression) {
671
- return exp.asKind(SyntaxKind8.PropertyAccessExpression).getName();
729
+ if (exp.getKind() === SyntaxKind9.Identifier) return exp.getText();
730
+ if (exp.getKind() === SyntaxKind9.PropertyAccessExpression) {
731
+ return exp.asKind(SyntaxKind9.PropertyAccessExpression).getName();
672
732
  }
673
733
  return "";
674
734
  }
675
735
  function containsIdentifier(node, name) {
676
- if (node.getKind() === SyntaxKind8.Identifier && node.getText() === name) return true;
677
- return node.getDescendantsOfKind(SyntaxKind8.Identifier).some((id) => id.getText() === name);
736
+ if (node.getKind() === SyntaxKind9.Identifier && node.getText() === name) return true;
737
+ return node.getDescendantsOfKind(SyntaxKind9.Identifier).some((id) => id.getText() === name);
678
738
  }
679
739
  var literalMultiplierWrongConstant = (c, p) => {
680
- const calls = c.fn.getDescendantsOfKind(SyntaxKind8.CallExpression).filter((x) => callName4(x) === p.call);
740
+ const calls = c.fn.getDescendantsOfKind(SyntaxKind9.CallExpression).filter((x) => callName4(x) === p.call);
681
741
  if (calls.length === 0) {
682
742
  return { result: "cant_tell", detail: p.absentCallDetail };
683
743
  }
@@ -700,6 +760,35 @@ var literalMultiplierWrongConstant = (c, p) => {
700
760
  return { result: "cant_tell", detail: p.cantTellDetail };
701
761
  };
702
762
 
763
+ // src/checkers/forbiddenCallReplacement.ts
764
+ import { SyntaxKind as SyntaxKind10 } from "ts-morph";
765
+ function callName5(call) {
766
+ const exp = call.getExpression();
767
+ if (exp.getKind() === SyntaxKind10.Identifier) return exp.getText();
768
+ if (exp.getKind() === SyntaxKind10.PropertyAccessExpression) {
769
+ return exp.asKind(SyntaxKind10.PropertyAccessExpression).getName();
770
+ }
771
+ return "";
772
+ }
773
+ var forbiddenCallReplacement = (c, p) => {
774
+ const calls = c.fn.getDescendantsOfKind(SyntaxKind10.CallExpression);
775
+ const names = calls.map(callName5);
776
+ const forbidden = p.forbiddenCalls ?? [];
777
+ const safer = p.saferCalls ?? [];
778
+ const usesSafer = names.some((n) => safer.includes(n));
779
+ if (usesSafer) {
780
+ return { result: "pass", detail: p.passDetail ?? `uses ${safer.join("/")}` };
781
+ }
782
+ const usesForbidden = names.some((n) => forbidden.includes(n));
783
+ if (usesForbidden) {
784
+ return { result: "fail", detail: p.failDetail ?? `uses ${forbidden.join("/")}` };
785
+ }
786
+ return {
787
+ result: "cant_tell",
788
+ detail: p.absentDetail ?? `neither ${forbidden.join("/")} nor ${safer.join("/")} found`
789
+ };
790
+ };
791
+
703
792
  // src/checkers/index.ts
704
793
  var registry = {
705
794
  "positional-arg-identity": positionalArgIdentity,
@@ -707,10 +796,12 @@ var registry = {
707
796
  "fee-allocation-shape": feeAllocationShape,
708
797
  "arg-equals-constant-identifier": argEqualsConstantIdentifier,
709
798
  "object-arg-property-literal-equals": objectArgPropertyLiteralEquals,
799
+ "object-arg-property-forbidden-literal": objectArgPropertyForbiddenLiteral,
710
800
  "anchor-init-if-needed-guarded": anchorInitIfNeededGuarded,
711
801
  "env-secrets-committed": envSecretsCommitted,
712
802
  "taint-to-sink": taintToSink,
713
- "literal-multiplier-wrong-constant": literalMultiplierWrongConstant
803
+ "literal-multiplier-wrong-constant": literalMultiplierWrongConstant,
804
+ "forbidden-call-replacement": forbiddenCallReplacement
714
805
  };
715
806
  function runChecker(kind, c, params) {
716
807
  const fn = registry[kind];
@@ -926,7 +1017,7 @@ function findRustCandidates(targetDir, rule) {
926
1017
  }
927
1018
 
928
1019
  // src/fixers/positionalArgIdentity.ts
929
- import { SyntaxKind as SyntaxKind9 } from "ts-morph";
1020
+ import { SyntaxKind as SyntaxKind11 } from "ts-morph";
930
1021
 
931
1022
  // src/fixers/diffUtil.ts
932
1023
  function buildDiff(node, replacement) {
@@ -951,9 +1042,9 @@ function buildDiff(node, replacement) {
951
1042
  // src/fixers/positionalArgIdentity.ts
952
1043
  var fixPositionalArgIdentity = (c, p, outcome) => {
953
1044
  if (outcome.result !== "fail") return void 0;
954
- const calls = c.fn.getDescendantsOfKind(SyntaxKind9.CallExpression).filter((call) => {
1045
+ const calls = c.fn.getDescendantsOfKind(SyntaxKind11.CallExpression).filter((call) => {
955
1046
  const exp = call.getExpression();
956
- return exp.getKind() === SyntaxKind9.PropertyAccessExpression && exp.asKind(SyntaxKind9.PropertyAccessExpression).getName() === p.call;
1047
+ return exp.getKind() === SyntaxKind11.PropertyAccessExpression && exp.asKind(SyntaxKind11.PropertyAccessExpression).getName() === p.call;
957
1048
  });
958
1049
  if (calls.length === 0) {
959
1050
  const wantParam2 = c.params[p.paramIndex] ?? "<rawBodyParam>";
@@ -968,7 +1059,7 @@ Do not call JSON.parse() on the body before this verification step.`
968
1059
  }
969
1060
  const arg = calls[0].getArguments()[p.argIndex];
970
1061
  const wantParam = c.params[p.paramIndex];
971
- if (arg && wantParam && arg.getKind() === SyntaxKind9.CallExpression) {
1062
+ if (arg && wantParam && arg.getKind() === SyntaxKind11.CallExpression) {
972
1063
  return {
973
1064
  summary: `Pass the raw body parameter '${wantParam}' to ${p.call} instead of a parsed value`,
974
1065
  diff: buildDiff(arg, wantParam)
@@ -978,12 +1069,12 @@ Do not call JSON.parse() on the body before this verification step.`
978
1069
  };
979
1070
 
980
1071
  // src/fixers/requiredCallWithOptions.ts
981
- import { SyntaxKind as SyntaxKind10 } from "ts-morph";
982
- function callName5(call) {
1072
+ import { SyntaxKind as SyntaxKind12 } from "ts-morph";
1073
+ function callName6(call) {
983
1074
  const exp = call.getExpression();
984
- if (exp.getKind() === SyntaxKind10.Identifier) return exp.getText();
985
- if (exp.getKind() === SyntaxKind10.PropertyAccessExpression) {
986
- return exp.asKind(SyntaxKind10.PropertyAccessExpression).getName();
1075
+ if (exp.getKind() === SyntaxKind12.Identifier) return exp.getText();
1076
+ if (exp.getKind() === SyntaxKind12.PropertyAccessExpression) {
1077
+ return exp.asKind(SyntaxKind12.PropertyAccessExpression).getName();
987
1078
  }
988
1079
  return "";
989
1080
  }
@@ -1001,15 +1092,15 @@ function placeholderFor(propName) {
1001
1092
  }
1002
1093
  var fixRequiredCallWithOptions = (c, p, outcome) => {
1003
1094
  if (outcome.result !== "fail") return void 0;
1004
- const calls = c.fn.getDescendantsOfKind(SyntaxKind10.CallExpression);
1005
- const verify = calls.filter((x) => p.verifyCalls.includes(callName5(x)));
1095
+ const calls = c.fn.getDescendantsOfKind(SyntaxKind12.CallExpression);
1096
+ const verify = calls.filter((x) => p.verifyCalls.includes(callName6(x)));
1006
1097
  if (verify.length > 0) {
1007
1098
  const call = verify[0];
1008
1099
  const args = call.getArguments();
1009
1100
  const lastArg = args[args.length - 1];
1010
- const obj = lastArg?.asKind(SyntaxKind10.ObjectLiteralExpression);
1101
+ const obj = lastArg?.asKind(SyntaxKind12.ObjectLiteralExpression);
1011
1102
  const presentNames = obj ? obj.getProperties().map((pr) => {
1012
- const pa = pr.asKind(SyntaxKind10.PropertyAssignment) ?? pr.asKind(SyntaxKind10.ShorthandPropertyAssignment);
1103
+ const pa = pr.asKind(SyntaxKind12.PropertyAssignment) ?? pr.asKind(SyntaxKind12.ShorthandPropertyAssignment);
1013
1104
  return pa?.getName() ?? "";
1014
1105
  }) : [];
1015
1106
  const missingGroups = p.requiredProps.filter(
@@ -1017,7 +1108,7 @@ var fixRequiredCallWithOptions = (c, p, outcome) => {
1017
1108
  );
1018
1109
  if (missingGroups.length === 0) return void 0;
1019
1110
  const newProps = missingGroups.map((g) => placeholderFor(g[0])).join(", ");
1020
- const summary = `Add ${missingGroups.map((g) => g[0]).join(" and ")} to the ${callName5(call)} call`;
1111
+ const summary = `Add ${missingGroups.map((g) => g[0]).join(" and ")} to the ${callName6(call)} call`;
1021
1112
  if (obj) {
1022
1113
  const inner = obj.getText().slice(1, -1).trim();
1023
1114
  const newText = inner.length > 0 ? `{ ${inner}, ${newProps} }` : `{ ${newProps} }`;
@@ -1029,7 +1120,7 @@ var fixRequiredCallWithOptions = (c, p, outcome) => {
1029
1120
  }
1030
1121
  return {
1031
1122
  summary,
1032
- suggestion: `Add an options object ({ ${newProps} }) as an argument to ${callName5(call)}.`
1123
+ suggestion: `Add an options object ({ ${newProps} }) as an argument to ${callName6(call)}.`
1033
1124
  };
1034
1125
  }
1035
1126
  return {
@@ -1043,22 +1134,22 @@ JWKS must come from Privy's published JWKS endpoint for your app.`
1043
1134
  };
1044
1135
 
1045
1136
  // src/fixers/literalMultiplierWrongConstant.ts
1046
- import { SyntaxKind as SyntaxKind11 } from "ts-morph";
1047
- function callName6(call) {
1137
+ import { SyntaxKind as SyntaxKind13 } from "ts-morph";
1138
+ function callName7(call) {
1048
1139
  const exp = call.getExpression();
1049
- if (exp.getKind() === SyntaxKind11.Identifier) return exp.getText();
1050
- if (exp.getKind() === SyntaxKind11.PropertyAccessExpression) {
1051
- return exp.asKind(SyntaxKind11.PropertyAccessExpression).getName();
1140
+ if (exp.getKind() === SyntaxKind13.Identifier) return exp.getText();
1141
+ if (exp.getKind() === SyntaxKind13.PropertyAccessExpression) {
1142
+ return exp.asKind(SyntaxKind13.PropertyAccessExpression).getName();
1052
1143
  }
1053
1144
  return "";
1054
1145
  }
1055
1146
  function findIdentifier(node, name) {
1056
- if (node.getKind() === SyntaxKind11.Identifier && node.getText() === name) return node;
1057
- return node.getDescendantsOfKind(SyntaxKind11.Identifier).find((id) => id.getText() === name);
1147
+ if (node.getKind() === SyntaxKind13.Identifier && node.getText() === name) return node;
1148
+ return node.getDescendantsOfKind(SyntaxKind13.Identifier).find((id) => id.getText() === name);
1058
1149
  }
1059
1150
  var fixLiteralMultiplierWrongConstant = (c, p, outcome) => {
1060
1151
  if (outcome.result !== "fail") return void 0;
1061
- const calls = c.fn.getDescendantsOfKind(SyntaxKind11.CallExpression).filter((call) => callName6(call) === p.call);
1152
+ const calls = c.fn.getDescendantsOfKind(SyntaxKind13.CallExpression).filter((call) => callName7(call) === p.call);
1062
1153
  if (calls.length === 0) return void 0;
1063
1154
  const arg = calls[0].getArguments()[p.argIndex];
1064
1155
  if (!arg) return void 0;
@@ -1646,871 +1737,9 @@ function resolveRules(targetDir, extraPackDirs = []) {
1646
1737
  return all;
1647
1738
  }
1648
1739
 
1649
- // src/trustGraph/directory.ts
1650
- import { readFileSync as readFileSync6, existsSync as existsSync4 } from "fs";
1651
- import { fileURLToPath as fileURLToPath2 } from "url";
1652
- import { join as join8 } from "path";
1653
- import { parse as parse3 } from "yaml";
1654
- var cache = null;
1655
- function bundledPath() {
1656
- const here = fileURLToPath2(new URL(".", import.meta.url));
1657
- const candidates = [
1658
- join8(here, "programs", "directory.yaml"),
1659
- // dist/programs/directory.yaml
1660
- join8(here, "..", "..", "programs", "directory.yaml"),
1661
- // src/../../programs/
1662
- join8(here, "..", "programs", "directory.yaml")
1663
- // fallback
1664
- ];
1665
- for (const c of candidates) {
1666
- if (existsSync4(c)) return c;
1667
- }
1668
- return candidates[0];
1669
- }
1670
- function loadDirectory(path = bundledPath()) {
1671
- if (cache && path === bundledPath()) return cache;
1672
- const raw = parse3(readFileSync6(path, "utf8"));
1673
- if (!raw || !Array.isArray(raw.programs)) {
1674
- throw new Error(`invalid program directory at ${path}: missing 'programs' array`);
1675
- }
1676
- const m = /* @__PURE__ */ new Map();
1677
- for (const p of raw.programs) {
1678
- if (!p.programId || !p.name) throw new Error(`directory entry missing programId/name: ${JSON.stringify(p)}`);
1679
- if (m.has(p.programId)) throw new Error(`directory has duplicate programId ${p.programId}`);
1680
- m.set(p.programId, { ...p, provenance: { ...p.provenance ?? {}, directoryFile: path } });
1681
- }
1682
- if (path === bundledPath()) cache = m;
1683
- return m;
1684
- }
1685
-
1686
- // src/trustGraph/base58.ts
1687
- var ALPHA = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
1688
- var MAP = {};
1689
- for (let i = 0; i < ALPHA.length; i++) MAP[ALPHA[i]] = i;
1690
- function base58Encode(bytes) {
1691
- let zeros = 0;
1692
- while (zeros < bytes.length && bytes[zeros] === 0) zeros++;
1693
- const buf = Array.from(bytes);
1694
- const out = [];
1695
- let start = zeros;
1696
- while (start < buf.length) {
1697
- let rem = 0;
1698
- for (let i = start; i < buf.length; i++) {
1699
- const acc = rem * 256 + buf[i];
1700
- buf[i] = Math.floor(acc / 58);
1701
- rem = acc % 58;
1702
- }
1703
- out.push(rem);
1704
- if (buf[start] === 0) start++;
1705
- }
1706
- let s = "";
1707
- for (let i = 0; i < zeros; i++) s += "1";
1708
- for (let i = out.length - 1; i >= 0; i--) s += ALPHA[out[i]];
1709
- return s;
1710
- }
1711
- function base58Decode(s) {
1712
- let zeros = 0;
1713
- while (zeros < s.length && s[zeros] === "1") zeros++;
1714
- const buf = [];
1715
- for (let i = zeros; i < s.length; i++) {
1716
- const v = MAP[s[i]];
1717
- if (v === void 0) throw new Error(`base58: invalid char '${s[i]}' at ${i}`);
1718
- let carry = v;
1719
- for (let j = 0; j < buf.length; j++) {
1720
- const acc = buf[j] * 58 + carry;
1721
- buf[j] = acc & 255;
1722
- carry = acc >>> 8;
1723
- }
1724
- while (carry > 0) {
1725
- buf.push(carry & 255);
1726
- carry >>>= 8;
1727
- }
1728
- }
1729
- const out = new Uint8Array(zeros + buf.length);
1730
- for (let i = 0; i < buf.length; i++) out[zeros + buf.length - 1 - i] = buf[i];
1731
- return out;
1732
- }
1733
- function isValidSolanaAddress(s) {
1734
- if (typeof s !== "string" || s.length < 32 || s.length > 44) return false;
1735
- try {
1736
- return base58Decode(s).length === 32;
1737
- } catch {
1738
- return false;
1739
- }
1740
- }
1741
-
1742
- // src/trustGraph/programCache.ts
1743
- import { readFileSync as readFileSync7, writeFileSync, mkdirSync, existsSync as existsSync5 } from "fs";
1744
- import { join as join9, dirname as dirname2 } from "path";
1745
- import { homedir } from "os";
1746
- var DEFAULT_TTL_HOURS = 168;
1747
- var SCHEMA_VERSION = "1.0";
1748
- function defaultCachePath() {
1749
- const envOverride = process.env["BRAINBLAST_CACHE_PATH"];
1750
- return envOverride ?? join9(homedir(), ".brainblast", "program-cache.json");
1751
- }
1752
- function emptyCache() {
1753
- return { schemaVersion: SCHEMA_VERSION, entries: {} };
1754
- }
1755
- function loadProgramCache(cachePath) {
1756
- const path = cachePath ?? defaultCachePath();
1757
- if (!existsSync5(path)) return emptyCache();
1758
- try {
1759
- const raw = JSON.parse(readFileSync7(path, "utf8"));
1760
- if (raw?.schemaVersion !== SCHEMA_VERSION) {
1761
- return emptyCache();
1762
- }
1763
- if (!raw.entries || typeof raw.entries !== "object") return emptyCache();
1764
- return { schemaVersion: SCHEMA_VERSION, entries: raw.entries };
1765
- } catch {
1766
- return emptyCache();
1767
- }
1768
- }
1769
- function saveProgramCache(cache2, cachePath) {
1770
- const path = cachePath ?? defaultCachePath();
1771
- mkdirSync(dirname2(path), { recursive: true });
1772
- writeFileSync(path, JSON.stringify(cache2, null, 2), "utf8");
1773
- }
1774
- function getCacheEntry(cache2, programId, ttlHoursOverride) {
1775
- const entry = cache2.entries[programId];
1776
- if (!entry) return null;
1777
- if (isEntryExpired(entry, ttlHoursOverride)) return null;
1778
- return entry.program;
1779
- }
1780
- function putCacheEntry(cache2, programId, program, sourceRun, ttlHours = DEFAULT_TTL_HOURS) {
1781
- cache2.entries[programId] = {
1782
- program,
1783
- cachedAt: (/* @__PURE__ */ new Date()).toISOString(),
1784
- sourceRun,
1785
- ttlHours
1786
- };
1787
- return cache2;
1788
- }
1789
- function getCacheEntryMeta(cache2, programId) {
1790
- return cache2.entries[programId] ?? null;
1791
- }
1792
- function isEntryExpired(entry, ttlHoursOverride) {
1793
- const ttl = ttlHoursOverride ?? entry.ttlHours ?? DEFAULT_TTL_HOURS;
1794
- if (ttl <= 0) return true;
1795
- const cachedMs = Date.parse(entry.cachedAt);
1796
- if (Number.isNaN(cachedMs)) return true;
1797
- const ageMs = Date.now() - cachedMs;
1798
- return ageMs >= ttl * 36e5;
1799
- }
1800
- function cacheSize(cache2, ttlHoursOverride) {
1801
- return Object.values(cache2.entries).filter((e) => !isEntryExpired(e, ttlHoursOverride)).length;
1802
- }
1803
-
1804
- // src/trustGraph/rpc.ts
1805
- var BPF_UPGRADEABLE_LOADER = "BPFLoaderUpgradeab1e11111111111111111111111";
1806
- var BPF_LOADER_2 = "BPFLoader2111111111111111111111111111111111";
1807
- var NATIVE_LOADER = "NativeLoader1111111111111111111111111111111";
1808
- var DEFAULT_RPC = "https://api.mainnet-beta.solana.com";
1809
- async function rpc(method, params, opts) {
1810
- const url = opts.rpcUrl ?? DEFAULT_RPC;
1811
- const fetchImpl = opts.fetchImpl ?? fetch;
1812
- const ac = new AbortController();
1813
- const t = setTimeout(() => ac.abort(), opts.timeoutMs ?? 1e4);
1814
- try {
1815
- const res = await fetchImpl(url, {
1816
- method: "POST",
1817
- headers: { "content-type": "application/json" },
1818
- body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
1819
- signal: ac.signal
1820
- });
1821
- if (!res.ok) throw new Error(`rpc ${method}: HTTP ${res.status}`);
1822
- const body = await res.json();
1823
- if (body.error) throw new Error(`rpc ${method}: ${body.error.message}`);
1824
- if (body.result === void 0) throw new Error(`rpc ${method}: empty result`);
1825
- return body.result;
1826
- } finally {
1827
- clearTimeout(t);
1828
- }
1829
- }
1830
- async function getAccountInfo(address, opts = {}) {
1831
- if (!isValidSolanaAddress(address)) throw new Error(`invalid Solana address: ${address}`);
1832
- const result = await rpc(
1833
- "getAccountInfo",
1834
- [address, { encoding: "base64", commitment: "confirmed" }],
1835
- opts
1836
- );
1837
- if (!result || !result.value) return null;
1838
- const v = result.value;
1839
- const [b64] = v.data;
1840
- return {
1841
- owner: v.owner,
1842
- data: Buffer.from(b64, "base64"),
1843
- executable: v.executable,
1844
- lamports: v.lamports
1845
- };
1846
- }
1847
- async function probeUpgradeAuthority(programId, opts = {}) {
1848
- const acct = await getAccountInfo(programId, opts);
1849
- if (!acct) {
1850
- return {
1851
- kind: "unknown",
1852
- address: null,
1853
- source: "rpc",
1854
- checkedAt: (/* @__PURE__ */ new Date()).toISOString()
1855
- };
1856
- }
1857
- if (acct.owner === BPF_LOADER_2 || acct.owner === NATIVE_LOADER) {
1858
- return {
1859
- kind: "renounced",
1860
- address: null,
1861
- source: "rpc",
1862
- checkedAt: (/* @__PURE__ */ new Date()).toISOString()
1863
- };
1864
- }
1865
- if (acct.owner !== BPF_UPGRADEABLE_LOADER) {
1866
- throw new Error(
1867
- `program ${programId} is owned by ${acct.owner}, not a known loader; not a deployed program?`
1868
- );
1869
- }
1870
- if (acct.data.length < 36) throw new Error(`program account too small: ${acct.data.length}`);
1871
- const tag = acct.data[0] | acct.data[1] << 8 | acct.data[2] << 16 | acct.data[3] << 24;
1872
- if (tag !== 2) throw new Error(`expected Program (tag=2) state, got tag=${tag}`);
1873
- const programDataAddr = base58Encode(acct.data.subarray(4, 36));
1874
- const pd = await getAccountInfo(programDataAddr, opts);
1875
- if (!pd) {
1876
- throw new Error(`program ${programId} ProgramData ${programDataAddr} not found`);
1877
- }
1878
- if (pd.data.length < 45) throw new Error(`ProgramData account too small: ${pd.data.length}`);
1879
- const pdTag = pd.data[0] | pd.data[1] << 8 | pd.data[2] << 16 | pd.data[3] << 24;
1880
- if (pdTag !== 3) throw new Error(`expected ProgramData (tag=3), got tag=${pdTag}`);
1881
- const optionTag = pd.data[12];
1882
- const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
1883
- if (optionTag === 0) {
1884
- return { kind: "renounced", address: null, source: "rpc", checkedAt };
1885
- }
1886
- if (optionTag !== 1) throw new Error(`unexpected Option tag in ProgramData: ${optionTag}`);
1887
- const authority = base58Encode(pd.data.subarray(13, 45));
1888
- return { kind: "unknown", address: authority, source: "rpc", checkedAt };
1889
- }
1890
-
1891
- // src/trustGraph/build.ts
1892
- async function buildTrustGraph(programIds, opts = {}) {
1893
- const dir = loadDirectory(opts.directoryPath);
1894
- const programs = [];
1895
- const unresolved = [];
1896
- const cacheEnabled = opts.cachePath !== null;
1897
- const cachePathArg = opts.cachePath === null ? void 0 : opts.cachePath;
1898
- const cache2 = cacheEnabled ? loadProgramCache(cachePathArg) : null;
1899
- const newFromRpc = [];
1900
- const runId = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:T]/g, "").slice(0, 14);
1901
- const seen = /* @__PURE__ */ new Set();
1902
- const ordered = programIds.filter((id) => seen.has(id) ? false : (seen.add(id), true));
1903
- for (const id of ordered) {
1904
- const directoryHit = dir.get(id);
1905
- if (directoryHit) {
1906
- programs.push(directoryHit);
1907
- continue;
1908
- }
1909
- if (cache2) {
1910
- const cached = getCacheEntry(cache2, id);
1911
- if (cached) {
1912
- const meta = getCacheEntryMeta(cache2, id);
1913
- programs.push({
1914
- ...cached,
1915
- provenance: {
1916
- ...cached.provenance ?? {},
1917
- notes: [
1918
- cached.provenance?.notes,
1919
- `cache-hit: cachedAt=${meta.cachedAt} sourceRun=${meta.sourceRun}`
1920
- ].filter(Boolean).join("; ")
1921
- }
1922
- });
1923
- continue;
1924
- }
1925
- }
1926
- if (opts.probeRpc === false) {
1927
- unresolved.push({
1928
- programId: id,
1929
- reason: "not_in_directory_or_cache_and_rpc_disabled"
1930
- });
1931
- continue;
1932
- }
1933
- let authority;
1934
- try {
1935
- authority = await probeUpgradeAuthority(id, opts);
1936
- } catch (e) {
1937
- unresolved.push({ programId: id, reason: `rpc_error: ${e?.message ?? String(e)}` });
1938
- continue;
1939
- }
1940
- const probed = {
1941
- programId: id,
1942
- name: `Unknown program (${id.slice(0, 8)}\u2026)`,
1943
- kind: "app",
1944
- upgradeAuthority: authority,
1945
- verifiedBuild: { state: "unknown" },
1946
- audits: [],
1947
- parity: { mainnet: "unknown", devnet: "unknown" },
1948
- provenance: { rpcUrl: opts.rpcUrl, notes: "live-probed; not in curated directory" }
1949
- };
1950
- programs.push(probed);
1951
- newFromRpc.push(id);
1952
- if (cache2) {
1953
- putCacheEntry(cache2, id, probed, runId);
1954
- }
1955
- }
1956
- if (cache2 && newFromRpc.length > 0) {
1957
- saveProgramCache(cache2, cachePathArg);
1958
- }
1959
- return { programs, unresolved, generatedAt: (/* @__PURE__ */ new Date()).toISOString() };
1960
- }
1961
-
1962
- // src/trustGraph/render.ts
1963
- function renderAuthority(p) {
1964
- const a = p.upgradeAuthority;
1965
- switch (a.kind) {
1966
- case "renounced":
1967
- return "\u{1F512} **Renounced** \u2014 program is frozen; no key can upgrade it.";
1968
- case "single-key":
1969
- return `\u26A0\uFE0F **Single key** \`${a.address}\` \u2014 one private key can replace this program at any time.`;
1970
- case "multisig":
1971
- return `\u{1F510} **Multisig** \`${a.address}\` \u2014 a threshold of signers can upgrade.`;
1972
- case "dao":
1973
- return `\u{1F3DB} **DAO** \`${a.address}\` \u2014 governance program controls upgrades.`;
1974
- case "unknown":
1975
- return a.address ? `\u2753 **Unclassified authority** \`${a.address}\` \u2014 needs research to confirm single-key vs multisig/DAO.` : "\u2753 **Unknown** \u2014 could not determine upgrade authority.";
1976
- }
1977
- }
1978
- function renderVerified(p) {
1979
- const v = p.verifiedBuild;
1980
- switch (v.state) {
1981
- case "verified":
1982
- return `\u2705 Verified build${v.commit ? ` @ \`${v.commit.slice(0, 12)}\`` : ""} \u2014 [registry](${v.registryUrl})`;
1983
- case "unverified":
1984
- return "\u274C Unverified \u2014 on-chain bytecode does not match any source we trust.";
1985
- case "unknown":
1986
- return "\u2753 Verified-build status not checked.";
1987
- }
1988
- }
1989
- function renderAudits(p) {
1990
- if (!p.audits.length) return "_No audits on file._";
1991
- return p.audits.map((a) => `- ${a.firm} (${a.date}) \u2014 [report](${a.reportUrl})${a.auditedCommit ? ` @ \`${a.auditedCommit.slice(0, 12)}\`` : ""}`).join("\n");
1992
- }
1993
- function renderParity(p) {
1994
- const { mainnet, devnet, testnet, notes } = p.parity;
1995
- const cells = [`mainnet=\`${mainnet}\``, `devnet=\`${devnet}\``];
1996
- if (testnet) cells.push(`testnet=\`${testnet}\``);
1997
- return cells.join(" \xB7 ") + (notes ? `
1998
- _${notes}_` : "");
1999
- }
2000
- function renderProgram(p) {
2001
- return [
2002
- `### ${p.name}`,
2003
- "",
2004
- `\`${p.programId}\`${p.kind ? ` \xB7 kind: \`${p.kind}\`` : ""}`,
2005
- "",
2006
- `- **Upgrade authority:** ${renderAuthority(p)}`,
2007
- `- **Verified build:** ${renderVerified(p)}`,
2008
- `- **Parity:** ${renderParity(p)}`,
2009
- `- **Audits:**
2010
- ${renderAudits(p).split("\n").map((l) => " " + l).join("\n")}`,
2011
- p.invokes && p.invokes.length ? `- **Invokes (CPI):** ${p.invokes.map((id) => `\`${id}\``).join(", ")}` : ""
2012
- ].filter(Boolean).join("\n");
2013
- }
2014
- function renderTrustGraphMd(g) {
2015
- const head = [
2016
- "# Trust Graph",
2017
- "",
2018
- `_Generated ${g.generatedAt}._`,
2019
- "",
2020
- "Every program your code transitively invokes, with the authority that controls it, the build-verification status, and the audits we found.",
2021
- ""
2022
- ].join("\n");
2023
- const body = g.programs.map(renderProgram).join("\n\n---\n\n");
2024
- const tail = g.unresolved.length ? [
2025
- "",
2026
- "---",
2027
- "",
2028
- "## Unresolved",
2029
- "",
2030
- ...g.unresolved.map((u) => `- \`${u.programId}\` \u2014 ${u.reason}`)
2031
- ].join("\n") : "";
2032
- return head + body + tail + "\n";
2033
- }
2034
-
2035
- // src/costAnalysis.ts
2036
- import { Project as Project2, SyntaxKind as SyntaxKind12 } from "ts-morph";
2037
- var LAMPORTS_PER_BYTE_YEAR = 3480;
2038
- var EXEMPTION_THRESHOLD = 2;
2039
- var OVERHEAD_BYTES = 128;
2040
- var LAMPORTS_PER_SOL = 1e9;
2041
- function rentExemptMinimum(dataLen) {
2042
- return (dataLen + OVERHEAD_BYTES) * LAMPORTS_PER_BYTE_YEAR * EXEMPTION_THRESHOLD;
2043
- }
2044
- function lamportsToSol(lamports) {
2045
- return (lamports / LAMPORTS_PER_SOL).toFixed(9).replace(/\.?0+$/, "");
2046
- }
2047
- var KNOWN_FLOWS = [
2048
- {
2049
- call: "createMint",
2050
- module: "@solana/spl-token",
2051
- accountType: "SPL Token Mint",
2052
- dataLen: 82,
2053
- recoverability: "conditionally-recoverable",
2054
- recoverabilityNote: "Recoverable via `closeAccount` on the mint \u2014 requires mint supply = 0 and mint authority disabled. Most production mints never meet these conditions."
2055
- },
2056
- {
2057
- call: "createAssociatedTokenAccount",
2058
- module: "@solana/spl-token",
2059
- accountType: "Associated Token Account (ATA)",
2060
- dataLen: 165,
2061
- recoverability: "recoverable",
2062
- recoverabilityNote: "Recovered by calling `closeAccount`; lamports return to the destination wallet. Requires zero token balance."
2063
- },
2064
- {
2065
- call: "createAssociatedTokenAccountIdempotent",
2066
- module: "@solana/spl-token",
2067
- accountType: "Associated Token Account (ATA, idempotent)",
2068
- dataLen: 165,
2069
- recoverability: "recoverable",
2070
- recoverabilityNote: "Recovered by calling `closeAccount`; lamports return to the destination wallet. Requires zero token balance."
2071
- },
2072
- {
2073
- call: "createAccount",
2074
- module: "@solana/spl-token",
2075
- accountType: "SPL Token Account (explicit)",
2076
- dataLen: 165,
2077
- recoverability: "recoverable",
2078
- recoverabilityNote: "Recovered by calling `closeAccount`; lamports return to the destination wallet. Requires zero token balance."
2079
- },
2080
- {
2081
- call: "createV1",
2082
- module: "@metaplex-foundation/mpl-token-metadata",
2083
- accountType: "Metaplex Token Metadata",
2084
- // Base metadata: 1(key) + 32(update_auth) + 32(mint) + 4+name + 4+symbol + 4+uri
2085
- // + 2(seller_fee) + 1(creators opt) + 1(primary_sale) + 1(is_mutable) ≈ 679 bytes typical
2086
- dataLen: 679,
2087
- recoverability: "non-recoverable",
2088
- recoverabilityNote: "Metaplex metadata accounts cannot be closed. The lamport lockup is permanent for the lifetime of the token."
2089
- },
2090
- {
2091
- call: "createNft",
2092
- module: "@metaplex-foundation/mpl-token-metadata",
2093
- accountType: "Metaplex NFT Metadata",
2094
- dataLen: 679,
2095
- recoverability: "non-recoverable",
2096
- recoverabilityNote: "Metaplex metadata accounts cannot be closed. The lamport lockup is permanent."
2097
- },
2098
- {
2099
- call: "createAndMint",
2100
- module: "@metaplex-foundation/mpl-token-metadata",
2101
- accountType: "Metaplex Token Metadata + Mint",
2102
- dataLen: 679 + 82,
2103
- // metadata + mint
2104
- recoverability: "non-recoverable",
2105
- recoverabilityNote: "Metadata accounts cannot be closed. Mint rent is conditionally recoverable (requires 0 supply + disabled authority)."
2106
- },
2107
- {
2108
- call: "createFungible",
2109
- module: "@metaplex-foundation/mpl-token-metadata",
2110
- accountType: "Metaplex Fungible Token Metadata",
2111
- dataLen: 679,
2112
- recoverability: "non-recoverable",
2113
- recoverabilityNote: "Metaplex metadata accounts cannot be closed. The lamport lockup is permanent."
2114
- }
2115
- ];
2116
- var LOOP_NODE_KINDS = /* @__PURE__ */ new Set([
2117
- SyntaxKind12.ForStatement,
2118
- SyntaxKind12.ForOfStatement,
2119
- SyntaxKind12.ForInStatement,
2120
- SyntaxKind12.WhileStatement,
2121
- SyntaxKind12.DoStatement
2122
- ]);
2123
- var ARRAY_METHOD_LOOPS = /* @__PURE__ */ new Set(["map", "forEach", "flatMap", "reduce", "filter"]);
2124
- function isInsideLoop(node) {
2125
- let cur = node;
2126
- while (cur) {
2127
- const k = cur.getKind?.();
2128
- if (k !== void 0 && LOOP_NODE_KINDS.has(k)) {
2129
- return { scalable: true, note: `call is inside a ${SyntaxKind12[k]} \u2014 cost scales with loop iterations` };
2130
- }
2131
- if (k === SyntaxKind12.CallExpression) {
2132
- const expr = cur.getExpression?.();
2133
- if (expr?.getKind?.() === SyntaxKind12.PropertyAccessExpression) {
2134
- const name = expr.asKind?.(SyntaxKind12.PropertyAccessExpression)?.getName?.();
2135
- if (name && ARRAY_METHOD_LOOPS.has(name)) {
2136
- return { scalable: true, note: `call is inside .${name}() \u2014 cost scales with array length` };
2137
- }
2138
- }
2139
- }
2140
- cur = cur.getParent?.();
2141
- }
2142
- return { scalable: false };
2143
- }
2144
- function detectPriorityFee(targetDir) {
2145
- const project = new Project2({ skipAddingFilesFromTsConfig: true });
2146
- for (const file of walk(targetDir)) {
2147
- const sf = project.addSourceFileAtPath(file);
2148
- const calls = sf.getDescendantsOfKind(SyntaxKind12.CallExpression);
2149
- for (const ce of calls) {
2150
- const expr = ce.getExpression();
2151
- const text = expr.getText();
2152
- if (text.includes("setComputeUnitPrice")) {
2153
- return {
2154
- found: true,
2155
- file,
2156
- line: ce.getStartLineNumber(),
2157
- detail: `ComputeBudgetProgram.setComputeUnitPrice detected at ${file}:${ce.getStartLineNumber()} \u2014 priority fee configured.`
2158
- };
2159
- }
2160
- }
2161
- }
2162
- return {
2163
- found: false,
2164
- detail: "No setComputeUnitPrice call detected. During network congestion, transactions without a priority fee may stall or be dropped. Add ComputeBudgetProgram.setComputeUnitPrice() to critical transaction paths."
2165
- };
2166
- }
2167
- function detectAccountFlows(targetDir) {
2168
- const project = new Project2({ skipAddingFilesFromTsConfig: true });
2169
- const callIndex = new Map(KNOWN_FLOWS.map((f) => [f.call, f]));
2170
- const flows = [];
2171
- for (const file of walk(targetDir)) {
2172
- const sf = project.addSourceFileAtPath(file);
2173
- const importedModules = new Set(
2174
- sf.getImportDeclarations().map((d) => d.getModuleSpecifierValue())
2175
- );
2176
- for (const ce of sf.getDescendantsOfKind(SyntaxKind12.CallExpression)) {
2177
- const expr = ce.getExpression();
2178
- let callName7 = null;
2179
- if (expr.getKind() === SyntaxKind12.Identifier) {
2180
- callName7 = expr.getText();
2181
- } else if (expr.getKind() === SyntaxKind12.PropertyAccessExpression) {
2182
- callName7 = expr.asKind(SyntaxKind12.PropertyAccessExpression).getName();
2183
- }
2184
- if (!callName7) continue;
2185
- const known = callIndex.get(callName7);
2186
- if (!known) continue;
2187
- if (!importedModules.has(known.module)) continue;
2188
- const lamports = rentExemptMinimum(known.dataLen);
2189
- const { scalable, note } = isInsideLoop(ce);
2190
- flows.push({
2191
- call: callName7,
2192
- module: known.module,
2193
- accountType: known.accountType,
2194
- file,
2195
- line: ce.getStartLineNumber(),
2196
- dataLen: known.dataLen,
2197
- lamports,
2198
- sol: lamportsToSol(lamports),
2199
- recoverability: known.recoverability,
2200
- recoverabilityNote: known.recoverabilityNote,
2201
- scalable,
2202
- scalableNote: note
2203
- });
2204
- }
2205
- }
2206
- return flows;
2207
- }
2208
- function analyzeCosts(targetDir) {
2209
- const accountFlows = detectAccountFlows(targetDir);
2210
- const priorityFee = detectPriorityFee(targetDir);
2211
- const staticFlows = accountFlows.filter((f) => !f.scalable);
2212
- const scalableFlows = accountFlows.filter((f) => f.scalable);
2213
- const totalLockupLamports = staticFlows.reduce((s, f) => s + f.lamports, 0);
2214
- return {
2215
- accountFlows,
2216
- priorityFee,
2217
- totalLockupLamports,
2218
- totalLockupSol: lamportsToSol(totalLockupLamports),
2219
- scalableFlows,
2220
- generatedAt: (/* @__PURE__ */ new Date()).toISOString()
2221
- };
2222
- }
2223
- function renderCostReportMd(r) {
2224
- const lines = ["## Cost & Rent Analysis\n"];
2225
- if (r.priorityFee.found) {
2226
- lines.push(`\u2705 **Priority fee configured** \u2014 \`setComputeUnitPrice\` detected.`);
2227
- lines.push(` ${r.priorityFee.detail}
2228
- `);
2229
- } else {
2230
- lines.push(`\u26A0\uFE0F **HIGH \u2014 Priority fee not configured**`);
2231
- lines.push(` ${r.priorityFee.detail}
2232
- `);
2233
- }
2234
- if (r.accountFlows.length === 0) {
2235
- lines.push("_No account-creation calls from tracked modules detected._\n");
2236
- return lines.join("\n");
2237
- }
2238
- lines.push("### Account Creation Flows\n");
2239
- lines.push("| Call | Account Type | Data | Lamports Locked | SOL | Recoverable? |");
2240
- lines.push("|------|-------------|------|-----------------|-----|--------------|");
2241
- for (const f of r.accountFlows) {
2242
- const file = f.file.split("/").slice(-2).join("/");
2243
- const recov = f.recoverability === "recoverable" ? "\u2705 Yes" : f.recoverability === "conditionally-recoverable" ? "\u26A0\uFE0F Conditional" : "\u274C No";
2244
- const scaleMark = f.scalable ? " \u{1F504}" : "";
2245
- lines.push(
2246
- `| \`${f.call}\`${scaleMark} (${file}:${f.line}) | ${f.accountType} | ${f.dataLen} B | ${f.lamports.toLocaleString()} | ${f.sol} SOL | ${recov} |`
2247
- );
2248
- }
2249
- lines.push("");
2250
- const unique = /* @__PURE__ */ new Map();
2251
- for (const f of r.accountFlows) unique.set(f.accountType, f.recoverabilityNote);
2252
- lines.push("**Recoverability notes:**");
2253
- for (const [type, note] of unique) lines.push(`- **${type}:** ${note}`);
2254
- lines.push("");
2255
- if (r.totalLockupLamports > 0) {
2256
- lines.push(
2257
- `**Total static lockup: ${r.totalLockupLamports.toLocaleString()} lamports (~${r.totalLockupSol} SOL)**`
2258
- );
2259
- lines.push(
2260
- `_(Excludes ${r.scalableFlows.length} scalable flow(s) whose cost grows with N \u2014 see below.)_
2261
- `
2262
- );
2263
- }
2264
- if (r.scalableFlows.length > 0) {
2265
- lines.push("### Scalable Cost Flows (cost grows with N)\n");
2266
- for (const f of r.scalableFlows) {
2267
- const file = f.file.split("/").slice(-2).join("/");
2268
- lines.push(
2269
- `- **\`${f.call}\`** at \`${file}:${f.line}\` \u2014 ${f.scalableNote}
2270
- Per-iteration cost: ${f.lamports.toLocaleString()} lamports (${f.sol} SOL) for each ${f.accountType}.`
2271
- );
2272
- }
2273
- lines.push("");
2274
- }
2275
- return lines.join("\n");
2276
- }
2277
-
2278
- // src/watch.ts
2279
- import { watch as fsWatch } from "fs";
2280
- function runIncrementalScan(targetDir, rules2, emit) {
2281
- const start = Date.now();
2282
- let changedRanges;
2283
- try {
2284
- changedRanges = getWorkingTreeChanges(targetDir);
2285
- } catch (e) {
2286
- emit({ type: "scan_error", message: e?.message ?? String(e) });
2287
- return;
2288
- }
2289
- if (changedRanges.size === 0) {
2290
- emit({ type: "scan_complete", filesChanged: 0, findings: 0, durationMs: Date.now() - start });
2291
- return;
2292
- }
2293
- const { checks } = audit(targetDir, rules2, changedRanges);
2294
- let findings = 0;
2295
- for (const c of checks) {
2296
- if (c.result === "pass") continue;
2297
- findings++;
2298
- emit({
2299
- type: "finding",
2300
- ruleId: c.ruleId,
2301
- severity: c.severity,
2302
- result: c.result,
2303
- file: c.file,
2304
- line: c.line,
2305
- detail: c.detail,
2306
- ...c.fix ? { fix: c.fix } : {}
2307
- });
2308
- }
2309
- emit({ type: "scan_complete", filesChanged: changedRanges.size, findings, durationMs: Date.now() - start });
2310
- }
2311
- function startWatch(targetDir, opts = {}) {
2312
- const debounceMs = opts.debounceMs ?? 300;
2313
- const emit = opts.emit ?? ((e) => process.stdout.write(JSON.stringify(e) + "\n"));
2314
- const rules2 = resolveRules(targetDir);
2315
- let timer;
2316
- const scheduleScan = () => {
2317
- if (timer) clearTimeout(timer);
2318
- timer = setTimeout(() => runIncrementalScan(targetDir, rules2, emit), debounceMs);
2319
- };
2320
- const watcher = fsWatch(targetDir, { recursive: true }, (_event, filename) => {
2321
- if (!filename) return;
2322
- const parts = filename.split(/[\\/]/);
2323
- if (parts.some((p) => SKIP_DIRS.has(p))) return;
2324
- scheduleScan();
2325
- });
2326
- emit({ type: "watch_started", targetDir });
2327
- return {
2328
- close: () => {
2329
- if (timer) clearTimeout(timer);
2330
- watcher.close();
2331
- }
2332
- };
2333
- }
2334
-
2335
- // src/fixers/applyDiff.ts
2336
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync2 } from "fs";
2337
- function parseDiff(diff) {
2338
- const lines = diff.split("\n");
2339
- const fileLine = lines.find((l) => l.startsWith("+++ b"));
2340
- if (!fileLine) throw new Error("parseDiff: no '+++ b<path>' line found");
2341
- const filePath = fileLine.slice("+++ b".length);
2342
- const hunkLine = lines.find((l) => l.startsWith("@@"));
2343
- if (!hunkLine) throw new Error("parseDiff: no hunk header found");
2344
- const m = hunkLine.match(/^@@ -(\d+),(\d+) \+\d+,\d+ @@/);
2345
- if (!m) throw new Error(`parseDiff: unrecognized hunk header '${hunkLine}'`);
2346
- const oldStart = Number(m[1]);
2347
- const oldCount = Number(m[2]);
2348
- const newLines = lines.filter((l) => l.startsWith("+") && !l.startsWith("+++")).map((l) => l.slice(1));
2349
- return { filePath, oldStart, oldCount, newLines };
2350
- }
2351
- function applyDiffToFile(diff) {
2352
- const { filePath, oldStart, oldCount, newLines } = parseDiff(diff);
2353
- const content = readFileSync8(filePath, "utf8");
2354
- const fileLines = content.split("\n");
2355
- const removedLines = diff.split("\n").filter((l) => l.startsWith("-") && !l.startsWith("---")).map((l) => l.slice(1));
2356
- const actual = fileLines.slice(oldStart - 1, oldStart - 1 + oldCount);
2357
- if (JSON.stringify(actual) !== JSON.stringify(removedLines)) return false;
2358
- fileLines.splice(oldStart - 1, oldCount, ...newLines);
2359
- writeFileSync2(filePath, fileLines.join("\n"));
2360
- return true;
2361
- }
2362
-
2363
- // src/pack.ts
2364
- import { existsSync as existsSync6, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
2365
- import { join as join10 } from "path";
2366
- function initPack(dir, opts) {
2367
- if (existsSync6(join10(dir, PACK_MANIFEST_FILE))) {
2368
- throw new Error(`${dir} already contains a ${PACK_MANIFEST_FILE}`);
2369
- }
2370
- const manifest = {
2371
- id: opts.id,
2372
- name: opts.name ?? opts.id,
2373
- version: opts.version ?? "0.1.0",
2374
- author: opts.author ?? "unknown",
2375
- ...opts.description ? { description: opts.description } : {}
2376
- };
2377
- mkdirSync2(dir, { recursive: true });
2378
- mkdirSync2(join10(dir, "rules"), { recursive: true });
2379
- mkdirSync2(join10(dir, "fixtures"), { recursive: true });
2380
- const manifestYaml = [
2381
- `id: ${manifest.id}`,
2382
- `name: ${manifest.name}`,
2383
- `version: ${manifest.version}`,
2384
- `author: ${manifest.author}`,
2385
- ...manifest.description ? [`description: ${manifest.description}`] : [],
2386
- ""
2387
- ].join("\n");
2388
- const manifestFile = join10(dir, PACK_MANIFEST_FILE);
2389
- writeFileSync3(manifestFile, manifestYaml, "utf8");
2390
- return manifestFile;
2391
- }
2392
- function validatePack(dir) {
2393
- const { manifest, rules: rules2 } = loadPack(dir);
2394
- const fixturesRoot = join10(dir, "fixtures");
2395
- const ruleResults = rules2.map((rule) => {
2396
- const ruleFixturesDir = join10(fixturesRoot, rule.id);
2397
- const vulnerableDir = join10(ruleFixturesDir, "vulnerable");
2398
- const fixedDir = join10(ruleFixturesDir, "fixed");
2399
- if (!existsSync6(vulnerableDir) || !existsSync6(fixedDir)) {
2400
- return {
2401
- ruleId: rule.id,
2402
- status: "missing-fixtures",
2403
- detail: `no fixtures/${rule.id}/{vulnerable,fixed}/ directory \u2014 prove gate skipped`
2404
- };
2405
- }
2406
- const redChecks = auditWithRule(vulnerableDir, rule);
2407
- const redFails = redChecks.filter((c) => c.result === "fail");
2408
- if (redFails.length === 0) {
2409
- return {
2410
- ruleId: rule.id,
2411
- status: "red-failed",
2412
- detail: `expected at least one FAIL against fixtures/${rule.id}/vulnerable/, got none`
2413
- };
2414
- }
2415
- const greenChecks = auditWithRule(fixedDir, rule);
2416
- const greenFails = greenChecks.filter((c) => c.result === "fail");
2417
- if (greenFails.length > 0) {
2418
- return {
2419
- ruleId: rule.id,
2420
- status: "green-failed",
2421
- detail: `expected no FAIL against fixtures/${rule.id}/fixed/, got ${greenFails.length}`
2422
- };
2423
- }
2424
- return { ruleId: rule.id, status: "ok", detail: "RED -> GREEN proven" };
2425
- });
2426
- const ok = ruleResults.every((r) => r.status === "ok" || r.status === "missing-fixtures");
2427
- return { manifest, rules: rules2, ruleResults, ok };
2428
- }
2429
-
2430
- // src/telemetry.ts
2431
- import { createHash, randomUUID } from "crypto";
2432
- import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
2433
- import { execFileSync as execFileSync3 } from "child_process";
2434
- import { homedir as homedir2 } from "os";
2435
- import { dirname as dirname3, join as join11, resolve } from "path";
2436
- function sha256Hex(s) {
2437
- return createHash("sha256").update(s).digest("hex");
2438
- }
2439
- function isTelemetryEnabled(targetDir) {
2440
- const env = process.env.BRAINBLAST_TELEMETRY;
2441
- if (env === "1" || env === "true") return true;
2442
- if (env === "0" || env === "false") return false;
2443
- const configPath = join11(targetDir, ".agent-research", "config.json");
2444
- if (!existsSync7(configPath)) return false;
2445
- try {
2446
- const cfg = JSON.parse(readFileSync9(configPath, "utf8"));
2447
- return cfg?.telemetry === true;
2448
- } catch {
2449
- return false;
2450
- }
2451
- }
2452
- function getUserHash() {
2453
- const idPath = join11(homedir2(), ".brainblast", "telemetry-id");
2454
- let id;
2455
- if (existsSync7(idPath)) {
2456
- id = readFileSync9(idPath, "utf8").trim();
2457
- } else {
2458
- id = randomUUID();
2459
- mkdirSync3(dirname3(idPath), { recursive: true });
2460
- writeFileSync4(idPath, id, "utf8");
2461
- }
2462
- return sha256Hex(id).slice(0, 16);
2463
- }
2464
- function getRepoHash(targetDir) {
2465
- let key = "";
2466
- try {
2467
- key = execFileSync3("git", ["config", "--get", "remote.origin.url"], {
2468
- cwd: targetDir,
2469
- encoding: "utf8",
2470
- stdio: ["ignore", "pipe", "ignore"]
2471
- }).trim();
2472
- } catch {
2473
- }
2474
- if (!key) key = resolve(targetDir);
2475
- return sha256Hex(key).slice(0, 16);
2476
- }
2477
- function telemetryFilePath(targetDir) {
2478
- return join11(targetDir, ".agent-research", "telemetry.ndjson");
2479
- }
2480
- var DEFAULT_REGISTRY_URL = "https://registry.brainblast.tech";
2481
- async function submitTelemetry(targetDir, registryUrl = process.env.BRAINBLAST_REGISTRY_URL || DEFAULT_REGISTRY_URL) {
2482
- const file = telemetryFilePath(targetDir);
2483
- if (!existsSync7(file)) {
2484
- return { submitted: 0, accepted: 0, rejected: 0, graduations: [] };
2485
- }
2486
- const events = readFileSync9(file, "utf8").trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
2487
- if (events.length === 0) {
2488
- return { submitted: 0, accepted: 0, rejected: 0, graduations: [] };
2489
- }
2490
- const res = await fetch(`${registryUrl.replace(/\/$/, "")}/api/telemetry`, {
2491
- method: "POST",
2492
- headers: { "content-type": "application/json" },
2493
- body: JSON.stringify({ events })
2494
- });
2495
- if (!res.ok) {
2496
- const body = await res.text().catch(() => "");
2497
- throw new Error(`telemetry submit failed: ${res.status} ${res.statusText} ${body}`.trim());
2498
- }
2499
- const json = await res.json();
2500
- return { submitted: events.length, ...json };
2501
- }
2502
- function recordGraduationEvents(targetDir, events) {
2503
- if (events.length === 0) return;
2504
- const file = telemetryFilePath(targetDir);
2505
- mkdirSync3(dirname3(file), { recursive: true });
2506
- const repo_hash = getRepoHash(targetDir);
2507
- const user_hash = getUserHash();
2508
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2509
- const lines = events.map((e) => JSON.stringify({ ...e, repo_hash, user_hash, timestamp })).join("\n");
2510
- appendFileSync(file, lines + "\n", "utf8");
2511
- }
2512
-
2513
1740
  export {
1741
+ SKIP_DIRS,
1742
+ walk,
2514
1743
  findCandidates,
2515
1744
  findConfigCandidates,
2516
1745
  runChecker,
@@ -2529,37 +1758,5 @@ export {
2529
1758
  loadPack,
2530
1759
  loadPacksFromDir,
2531
1760
  rules,
2532
- resolveRules,
2533
- loadDirectory,
2534
- base58Encode,
2535
- base58Decode,
2536
- isValidSolanaAddress,
2537
- DEFAULT_TTL_HOURS,
2538
- defaultCachePath,
2539
- loadProgramCache,
2540
- saveProgramCache,
2541
- getCacheEntry,
2542
- putCacheEntry,
2543
- getCacheEntryMeta,
2544
- isEntryExpired,
2545
- cacheSize,
2546
- buildTrustGraph,
2547
- renderTrustGraphMd,
2548
- rentExemptMinimum,
2549
- lamportsToSol,
2550
- analyzeCosts,
2551
- renderCostReportMd,
2552
- runIncrementalScan,
2553
- startWatch,
2554
- parseDiff,
2555
- applyDiffToFile,
2556
- initPack,
2557
- validatePack,
2558
- isTelemetryEnabled,
2559
- getUserHash,
2560
- getRepoHash,
2561
- telemetryFilePath,
2562
- DEFAULT_REGISTRY_URL,
2563
- submitTelemetry,
2564
- recordGraduationEvents
1761
+ resolveRules
2565
1762
  };