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.
- package/dist/{chunk-EYFKA33G.js → chunk-A56IF3UX.js} +137 -940
- package/dist/chunk-SC6RNNDW.js +160 -0
- package/dist/chunk-ZZ6LBZV5.js +909 -0
- package/dist/cli.js +99 -4
- package/dist/diff-PZKZYBKF.js +12 -0
- package/dist/index.d.ts +26 -1
- package/dist/index.js +33 -19
- package/dist/mcp-RUVILE2Y.js +172 -0
- package/package.json +2 -1
|
@@ -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
|
|
551
|
+
import { SyntaxKind as SyntaxKind8 } from "ts-morph";
|
|
492
552
|
function calleeName(call) {
|
|
493
553
|
const exp = call.getExpression();
|
|
494
|
-
if (exp.getKind() ===
|
|
495
|
-
if (exp.getKind() ===
|
|
496
|
-
return exp.asKind(
|
|
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() ===
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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() ===
|
|
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(
|
|
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() ===
|
|
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
|
|
726
|
+
import { SyntaxKind as SyntaxKind9 } from "ts-morph";
|
|
667
727
|
function callName4(call) {
|
|
668
728
|
const exp = call.getExpression();
|
|
669
|
-
if (exp.getKind() ===
|
|
670
|
-
if (exp.getKind() ===
|
|
671
|
-
return exp.asKind(
|
|
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() ===
|
|
677
|
-
return node.getDescendantsOfKind(
|
|
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(
|
|
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
|
|
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(
|
|
1045
|
+
const calls = c.fn.getDescendantsOfKind(SyntaxKind11.CallExpression).filter((call) => {
|
|
955
1046
|
const exp = call.getExpression();
|
|
956
|
-
return exp.getKind() ===
|
|
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() ===
|
|
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
|
|
982
|
-
function
|
|
1072
|
+
import { SyntaxKind as SyntaxKind12 } from "ts-morph";
|
|
1073
|
+
function callName6(call) {
|
|
983
1074
|
const exp = call.getExpression();
|
|
984
|
-
if (exp.getKind() ===
|
|
985
|
-
if (exp.getKind() ===
|
|
986
|
-
return exp.asKind(
|
|
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(
|
|
1005
|
-
const verify = calls.filter((x) => p.verifyCalls.includes(
|
|
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(
|
|
1101
|
+
const obj = lastArg?.asKind(SyntaxKind12.ObjectLiteralExpression);
|
|
1011
1102
|
const presentNames = obj ? obj.getProperties().map((pr) => {
|
|
1012
|
-
const pa = pr.asKind(
|
|
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 ${
|
|
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 ${
|
|
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
|
|
1047
|
-
function
|
|
1137
|
+
import { SyntaxKind as SyntaxKind13 } from "ts-morph";
|
|
1138
|
+
function callName7(call) {
|
|
1048
1139
|
const exp = call.getExpression();
|
|
1049
|
-
if (exp.getKind() ===
|
|
1050
|
-
if (exp.getKind() ===
|
|
1051
|
-
return exp.asKind(
|
|
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() ===
|
|
1057
|
-
return node.getDescendantsOfKind(
|
|
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(
|
|
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
|
};
|