@gatedpath/preflight 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/checks/slither.d.ts +10 -0
- package/dist/checks/slither.js +65 -3
- package/dist/doctor-cli.d.ts +1 -1
- package/dist/doctor-cli.js +1 -1
- package/dist/doctor.d.ts +4 -0
- package/dist/doctor.js +31 -13
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -64,7 +64,8 @@ passes; `--json` for agents. The fix text is the failure table's (`@gatedpath/ag
|
|
|
64
64
|
| `forge`, `anvil`, `cast` | yes | each on PATH and answering `--version` |
|
|
65
65
|
| `foundry-version` | yes | the three report one version |
|
|
66
66
|
| `foundry-exit-code` | yes | each hands its exit code back. The `@foundry-rs` npm packages (1.7.1) install a Node shim that exits 0 whatever the binary returned, so a failed `forge build` reports success; doctor probes with a flag no binary knows (the real ones exit 2, the shim 0), names the shim, and prints the `ln -sf` line to the real binary it found in the platform package beside it |
|
|
67
|
-
| `slither
|
|
67
|
+
| `slither` | no | on PATH; a warning when missing or not at the version CI pins (0.11.6) |
|
|
68
|
+
| `aderyn` | no | reported, never a warning: Cyfrin retired Aderyn in November 2026 and withdrew its binaries and npm archives, so the scaffold's CI no longer runs it (0.2.5). A copy already installed is named with its version |
|
|
68
69
|
| `env-tracked` | yes | no `.env` or `.env.*` (other than `.env.example`) in `git ls-files` |
|
|
69
70
|
| `vendor` | yes | in a scaffold, each `vendor/redbelly-*/package.json` version equals the record the scaffolder wrote under `redbelly.vendor` in the root `package.json`; a scaffold without the record warns |
|
|
70
71
|
|
package/dist/checks/slither.d.ts
CHANGED
|
@@ -9,3 +9,13 @@ export declare const SOURCES_HASH_FILE = "slither.sources.sha256";
|
|
|
9
9
|
*/
|
|
10
10
|
export declare function hashSources(dir: string): string;
|
|
11
11
|
export declare function checkSlitherReport(project: string, sourceDir: string, reportPath?: string): Promise<CheckResult>;
|
|
12
|
+
/** The project's `fail_on` from slither.config.json, or Slither's default, medium. */
|
|
13
|
+
export declare function readFailOn(project: string): string;
|
|
14
|
+
export interface SlitherFinding {
|
|
15
|
+
check: string;
|
|
16
|
+
impact: string;
|
|
17
|
+
confidence: string;
|
|
18
|
+
where: string;
|
|
19
|
+
}
|
|
20
|
+
/** Every detector result in a Slither JSON report, with one location each. */
|
|
21
|
+
export declare function readFindings(reportFile: string): SlitherFinding[] | null;
|
package/dist/checks/slither.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
// Does a Slither report exist,
|
|
1
|
+
// Does a Slither report exist, does it describe the sources as they are now, and is it clean at the
|
|
2
|
+
// project's own threshold? Freshness first, two ways to
|
|
2
3
|
// answer. Preferred: a sidecar `slither.sources.sha256` next to the report holding the hash
|
|
3
4
|
// of every .sol file under the source directory at the time Slither ran (the scaffold's
|
|
4
5
|
// `npm run lint:slither` writes it); the check recomputes the hash and compares. Fallback,
|
|
@@ -8,6 +9,12 @@
|
|
|
8
9
|
// time rule cannot be satisfied by re-running Slither on a clean project, because a report
|
|
9
10
|
// with no findings is byte-identical and git records no new change for it (found on
|
|
10
11
|
// 2026-09-12 running the prompt cards), which is why the hash sidecar exists.
|
|
12
|
+
// Then the findings: a fresh report is not a clean one. Until 29 October 2026 this check stopped at
|
|
13
|
+
// freshness, and a ship report said "pass" over a report holding a High (found on the first public CI
|
|
14
|
+
// run of grant-milestone-escrow, where the same report failed `fail-on: medium`). A JSON report's
|
|
15
|
+
// detectors are now read and any finding at or above the project's `fail_on` in slither.config.json
|
|
16
|
+
// (default medium, Slither's own default) fails the check, naming them. A markdown report cannot be
|
|
17
|
+
// read; the check says so and passes on freshness alone.
|
|
11
18
|
import { execFileSync } from 'node:child_process';
|
|
12
19
|
import { createHash } from 'node:crypto';
|
|
13
20
|
import { existsSync, readdirSync, readFileSync, statSync, realpathSync } from 'node:fs';
|
|
@@ -52,7 +59,7 @@ export async function checkSlitherReport(project, sourceDir, reportPath) {
|
|
|
52
59
|
const current = hashSources(srcAbs);
|
|
53
60
|
const data = { report: relative(project, found), sidecar: relative(project, sidecar), recordedHash: recorded, currentHash: current, sources: sources.length };
|
|
54
61
|
if (recorded === current)
|
|
55
|
-
return
|
|
62
|
+
return withFindings(project, found, `${data.report} describes the current sources: ${data.sidecar} matches the hash of ${sources.length} .sol file(s) under ${sourceDir}`, data);
|
|
56
63
|
return { id: 'slither-report', status: 'fail', reason: `${data.report} is stale: the sources under ${sourceDir} no longer match ${data.sidecar}; re-run slither.`, data };
|
|
57
64
|
}
|
|
58
65
|
const times = await fileTimes(project, [found, ...sources]);
|
|
@@ -68,9 +75,64 @@ export async function checkSlitherReport(project, sourceDir, reportPath) {
|
|
|
68
75
|
}
|
|
69
76
|
const data = { report: relative(project, found), reportTime: iso(reportTime), newestSource: relative(project, newestSource), newestSourceTime: iso(newestTime), sources: sources.length };
|
|
70
77
|
if (reportTime >= newestTime)
|
|
71
|
-
return
|
|
78
|
+
return withFindings(project, found, `${data.report} (${data.reportTime}) is newer than the newest source ${data.newestSource} (${data.newestSourceTime})`, data);
|
|
72
79
|
return { id: 'slither-report', status: 'fail', reason: `${data.report} (${data.reportTime}) is older than ${data.newestSource} (${data.newestSourceTime}); re-run slither and write ${SOURCES_HASH_FILE} beside the report (the scaffold's npm run lint:slither does both).`, data };
|
|
73
80
|
}
|
|
81
|
+
const IMPACT_RANK = { high: 3, medium: 2, low: 1, informational: 0, optimization: 0 };
|
|
82
|
+
const FAIL_ON_RANK = { high: 3, medium: 2, low: 1, informational: 0, pedantic: 0, none: 99 };
|
|
83
|
+
/** The project's `fail_on` from slither.config.json, or Slither's default, medium. */
|
|
84
|
+
export function readFailOn(project) {
|
|
85
|
+
const candidates = [join(project, 'slither.config.json'), join(project, 'contracts', 'slither.config.json')];
|
|
86
|
+
for (const file of candidates) {
|
|
87
|
+
if (!existsSync(file))
|
|
88
|
+
continue;
|
|
89
|
+
try {
|
|
90
|
+
const cfg = JSON.parse(readFileSync(file, 'utf8'));
|
|
91
|
+
if (typeof cfg.fail_on === 'string' && cfg.fail_on.toLowerCase() in FAIL_ON_RANK)
|
|
92
|
+
return cfg.fail_on.toLowerCase();
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
/* an unreadable config falls back to the default, and the reason says which threshold applied */
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return 'medium';
|
|
99
|
+
}
|
|
100
|
+
/** Every detector result in a Slither JSON report, with one location each. */
|
|
101
|
+
export function readFindings(reportFile) {
|
|
102
|
+
if (!reportFile.endsWith('.json'))
|
|
103
|
+
return null;
|
|
104
|
+
let parsed;
|
|
105
|
+
try {
|
|
106
|
+
parsed = JSON.parse(readFileSync(reportFile, 'utf8'));
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const detectors = parsed.results?.detectors ?? [];
|
|
112
|
+
return detectors.map((d) => {
|
|
113
|
+
const el = d.elements?.[0]?.source_mapping;
|
|
114
|
+
const where = el?.filename_relative ? `${el.filename_relative}${el.lines?.length ? `:${el.lines[0]}` : ''}` : 'unknown location';
|
|
115
|
+
return { check: d.check ?? 'unknown', impact: d.impact ?? 'Unknown', confidence: d.confidence ?? 'Unknown', where };
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
/** A fresh report passes only if nothing in it reaches the project's `fail_on`. */
|
|
119
|
+
function withFindings(project, reportFile, freshness, data) {
|
|
120
|
+
const findings = readFindings(reportFile);
|
|
121
|
+
const failOn = readFailOn(project);
|
|
122
|
+
if (findings === null) {
|
|
123
|
+
return { id: 'slither-report', status: 'pass', reason: `${freshness}; its findings were not read (not a JSON report), so freshness alone is checked.`, data: { ...data, failOn, findingsRead: false } };
|
|
124
|
+
}
|
|
125
|
+
const threshold = FAIL_ON_RANK[failOn] ?? 2;
|
|
126
|
+
const blocking = findings.filter((f) => (IMPACT_RANK[f.impact.toLowerCase()] ?? 0) >= threshold);
|
|
127
|
+
const counts = findings.reduce((acc, f) => { acc[f.impact] = (acc[f.impact] ?? 0) + 1; return acc; }, {});
|
|
128
|
+
const summary = Object.entries(counts).map(([k, v]) => `${v} ${k}`).join(', ') || 'no findings';
|
|
129
|
+
if (blocking.length === 0) {
|
|
130
|
+
return { id: 'slither-report', status: 'pass', reason: `${freshness}; ${summary}, none at or above fail_on ${failOn}.`, data: { ...data, failOn, findingsRead: true, counts } };
|
|
131
|
+
}
|
|
132
|
+
const named = blocking.slice(0, 5).map((f) => `${f.check} (${f.impact}) at ${f.where}`).join('; ');
|
|
133
|
+
const more = blocking.length > 5 ? ` and ${blocking.length - 5} more` : '';
|
|
134
|
+
return { id: 'slither-report', status: 'fail', reason: `${data['report']} is fresh but holds ${blocking.length} finding(s) at or above fail_on ${failOn}: ${named}${more}. Fix them or triage them in the source with a reasoned slither-disable comment, then re-run slither.`, data: { ...data, failOn, findingsRead: true, counts, blocking } };
|
|
135
|
+
}
|
|
74
136
|
function iso(t) {
|
|
75
137
|
return t ? new Date(t * 1000).toISOString() : 'unknown';
|
|
76
138
|
}
|
package/dist/doctor-cli.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
export declare const DOCTOR_HELP = "redbelly-doctor [options]\n\nChecks the machine before the first command: Node 22 or later; forge, anvil and cast on\nPATH, one version, and the real binaries rather than the @foundry-rs npm shim that exits 0\nwhatever the binary returned; slither
|
|
2
|
+
export declare const DOCTOR_HELP = "redbelly-doctor [options]\n\nChecks the machine before the first command: Node 22 or later; forge, anvil and cast on\nPATH, one version, and the real binaries rather than the @foundry-rs npm shim that exits 0\nwhatever the binary returned; slither (optional); git; no .env tracked by git;\nand, in a scaffold, vendor/ matching the versions the scaffolder recorded. One line per\ncheck with the fix on the same line. Touches no network and no key.\n\n --project <dir> scaffold root, its contracts/ folder, or any directory (default: cwd)\n --json print the report as JSON\n --version, --help\n\nExit 0 when every required check passes (optional checks may warn), 1 otherwise, 2 on bad arguments.\n";
|
|
3
3
|
export declare function parseDoctorArgs(argv: readonly string[]): {
|
|
4
4
|
project?: string;
|
|
5
5
|
json: boolean;
|
package/dist/doctor-cli.js
CHANGED
|
@@ -8,7 +8,7 @@ export const DOCTOR_HELP = `redbelly-doctor [options]
|
|
|
8
8
|
|
|
9
9
|
Checks the machine before the first command: Node 22 or later; forge, anvil and cast on
|
|
10
10
|
PATH, one version, and the real binaries rather than the @foundry-rs npm shim that exits 0
|
|
11
|
-
whatever the binary returned; slither
|
|
11
|
+
whatever the binary returned; slither (optional); git; no .env tracked by git;
|
|
12
12
|
and, in a scaffold, vendor/ matching the versions the scaffolder recorded. One line per
|
|
13
13
|
check with the fix on the same line. Touches no network and no key.
|
|
14
14
|
|
package/dist/doctor.d.ts
CHANGED
|
@@ -37,6 +37,10 @@ export interface DoctorReport {
|
|
|
37
37
|
}
|
|
38
38
|
export declare const NODE_MIN_MAJOR = 22;
|
|
39
39
|
export declare const SLITHER_PIN = "0.11.6";
|
|
40
|
+
/**
|
|
41
|
+
* @deprecated Cyfrin retired Aderyn in November 2026 and withdrew its binaries and npm archives, so
|
|
42
|
+
* CI no longer runs it and doctor no longer compares against a pin. Kept so imports of 0.2.4 still resolve.
|
|
43
|
+
*/
|
|
40
44
|
export declare const ADERYN_PIN = "0.6.8";
|
|
41
45
|
declare const FOUNDRY_TOOLS: readonly ["forge", "anvil", "cast"];
|
|
42
46
|
type FoundryTool = (typeof FOUNDRY_TOOLS)[number];
|
package/dist/doctor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// redbelly-doctor: the machine before the first command. Node 22 or later; forge, anvil and cast
|
|
2
2
|
// present, one version, and the real binaries rather than the @foundry-rs npm shim that exits 0
|
|
3
|
-
// whatever the binary returned; slither
|
|
3
|
+
// whatever the binary returned; slither optional; aderyn reported, never warned on (retired); git; no .env tracked; and, in a
|
|
4
4
|
// scaffold, vendor/ matching the versions the scaffolder recorded. One line per check with the fix
|
|
5
5
|
// on the same line. The fix text comes from the failure table in @gatedpath/agent-rules, so
|
|
6
6
|
// explain_failure and doctor use the same words. Nothing here touches a network or a key.
|
|
@@ -14,6 +14,10 @@ export const DOCTOR_VERSION = require('../package.json').version;
|
|
|
14
14
|
export const DOCTOR_CHECK_IDS = ['node', 'git', 'forge', 'anvil', 'cast', 'foundry-version', 'foundry-exit-code', 'slither', 'aderyn', 'env-tracked', 'vendor'];
|
|
15
15
|
export const NODE_MIN_MAJOR = 22;
|
|
16
16
|
export const SLITHER_PIN = '0.11.6';
|
|
17
|
+
/**
|
|
18
|
+
* @deprecated Cyfrin retired Aderyn in November 2026 and withdrew its binaries and npm archives, so
|
|
19
|
+
* CI no longer runs it and doctor no longer compares against a pin. Kept so imports of 0.2.4 still resolve.
|
|
20
|
+
*/
|
|
17
21
|
export const ADERYN_PIN = '0.6.8';
|
|
18
22
|
const FOUNDRY_TOOLS = ['forge', 'anvil', 'cast'];
|
|
19
23
|
/** The flag no Foundry binary knows. clap exits 2 on it; the npm shim exits 0. */
|
|
@@ -160,20 +164,34 @@ export async function runDoctor(opts = {}) {
|
|
|
160
164
|
const concrete = shims.map((t) => (found[t].real ? `ln -sf ${found[t].real} ${found[t].path}` : null)).filter(Boolean).join(' && ');
|
|
161
165
|
checks.push({ id: 'foundry-exit-code', required: true, status: 'fail', reason: `${detail}. A failed forge build or test reports success through it.`, fix: concrete || fixFor('foundry-exit-code-shim'), kind: 'foundry-exit-code-shim', data: { shims: shims.map((t) => ({ tool: t, path: found[t].path, real: found[t].real })) } });
|
|
162
166
|
}
|
|
163
|
-
// slither
|
|
164
|
-
|
|
165
|
-
const path = whichOnPath(
|
|
167
|
+
// slither (optional)
|
|
168
|
+
{
|
|
169
|
+
const path = whichOnPath('slither', env, platform);
|
|
166
170
|
if (!path) {
|
|
167
|
-
checks.push({ id:
|
|
168
|
-
|
|
171
|
+
checks.push({ id: 'slither', required: false, status: 'warn', reason: 'not found on PATH (optional; pre-flight wants its report before a deploy).', fix: fixFor('slither-not-found'), kind: 'slither-not-found' });
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
const v = await toolVersion(path);
|
|
175
|
+
if (!v.version)
|
|
176
|
+
checks.push({ id: 'slither', required: false, status: 'warn', reason: `${path} did not answer --version (optional).`, fix: fixFor('slither-not-found'), kind: 'slither-not-found', data: { path } });
|
|
177
|
+
else if (v.version !== SLITHER_PIN)
|
|
178
|
+
checks.push({ id: 'slither', required: false, status: 'warn', reason: `${v.version} at ${path}; CI pins ${SLITHER_PIN}, so findings may differ.`, fix: null, kind: null, data: { path, version: v.version, pin: SLITHER_PIN } });
|
|
179
|
+
else
|
|
180
|
+
checks.push({ id: 'slither', required: false, status: 'pass', reason: `${v.version} at ${path}`, fix: null, kind: null, data: { path, version: v.version } });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// aderyn: reported, never warned on. Cyfrin retired it in November 2026 and withdrew every install route,
|
|
184
|
+
// so a missing copy is the normal case and there is nothing for the builder to fix. The id stays so the
|
|
185
|
+
// report keeps its shape for agents that read it.
|
|
186
|
+
{
|
|
187
|
+
const path = whichOnPath('aderyn', env, platform);
|
|
188
|
+
if (!path) {
|
|
189
|
+
checks.push({ id: 'aderyn', required: false, status: 'pass', reason: 'not installed, and not needed: Cyfrin retired Aderyn in November 2026; Slither is the analyser CI and pre-flight use.', fix: null, kind: null });
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
const v = await toolVersion(path);
|
|
193
|
+
checks.push({ id: 'aderyn', required: false, status: 'pass', reason: `${v.version ?? 'a copy'} at ${path}; retired by Cyfrin in November 2026, so CI no longer runs it, but npm run lint:aderyn still does.`, fix: null, kind: null, data: { path, version: v.version } });
|
|
169
194
|
}
|
|
170
|
-
const v = await toolVersion(path);
|
|
171
|
-
if (!v.version)
|
|
172
|
-
checks.push({ id: tool, required: false, status: 'warn', reason: `${path} did not answer --version (optional).`, fix: fixFor(kind), kind, data: { path } });
|
|
173
|
-
else if (v.version !== pin)
|
|
174
|
-
checks.push({ id: tool, required: false, status: 'warn', reason: `${v.version} at ${path}; CI pins ${pin}, so findings may differ.`, fix: null, kind: null, data: { path, version: v.version, pin } });
|
|
175
|
-
else
|
|
176
|
-
checks.push({ id: tool, required: false, status: 'pass', reason: `${v.version} at ${path}`, fix: null, kind: null, data: { path, version: v.version } });
|
|
177
195
|
}
|
|
178
196
|
// no .env tracked
|
|
179
197
|
if (!gitPath) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gatedpath/preflight",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Pre-flight checks before deploying to Redbelly Network (151 mainnet, 153 testnet): chain ID, deployer verification, Safe admin, compiler pins, secrets in git history, RBNT balance, Slither report.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"prepack": "npm run build"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@gatedpath/agent-rules": "0.2.
|
|
44
|
-
"@gatedpath/chains": "0.2.
|
|
43
|
+
"@gatedpath/agent-rules": "0.2.5",
|
|
44
|
+
"@gatedpath/chains": "0.2.5"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/node": "^22.20.2",
|