npmguard-cli 1.1.4 → 1.1.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 +11 -3
- package/dist/commands/install.js +52 -14
- package/dist/index.js +3 -3
- package/dist/install-source.js +24 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ lockfiles and runs the correct add command (`npm install`, `pnpm add`,
|
|
|
50
50
|
|
|
51
51
|
1. Resolves the version (`latest` if omitted)
|
|
52
52
|
2. Asks the engine if the package has an existing audit
|
|
53
|
-
3. **Found + SAFE** →
|
|
53
|
+
3. **Found + SAFE** → installs from the configured source
|
|
54
54
|
4. **Found + DANGEROUS** → shows findings + capabilities, prompts `y/N`
|
|
55
55
|
(bypass with `--force`)
|
|
56
56
|
5. **Not found** → asks how you want to pay for the audit:
|
|
@@ -63,9 +63,16 @@ lockfiles and runs the correct add command (`npm install`, `pnpm add`,
|
|
|
63
63
|
report when the browser-wallet page owns the live view
|
|
64
64
|
7. Runs the install if the verdict is SAFE, or prompts otherwise
|
|
65
65
|
|
|
66
|
-
The install source
|
|
66
|
+
The install source defaults to `auto`: after a SAFE verdict the CLI waits
|
|
67
|
+
briefly for NpmGuard publication, then tries ENS, then the NpmGuard Pinata
|
|
68
|
+
storage API, and only falls back to npm if no publication is available.
|
|
69
|
+
|
|
70
|
+
The source is configurable:
|
|
67
71
|
|
|
68
72
|
```bash
|
|
73
|
+
# default: audited verdict, then ENS/Pinata when available
|
|
74
|
+
npmguard install express --install-source auto
|
|
75
|
+
|
|
69
76
|
# default: audited verdict, then normal registry install
|
|
70
77
|
npmguard install express --install-source npm
|
|
71
78
|
|
|
@@ -175,7 +182,7 @@ Install source:
|
|
|
175
182
|
npmguard install lodash --install-source ens
|
|
176
183
|
|
|
177
184
|
# via env
|
|
178
|
-
export NPMGUARD_INSTALL_SOURCE=
|
|
185
|
+
export NPMGUARD_INSTALL_SOURCE=ens
|
|
179
186
|
export NPMGUARD_ENS_ROOT_DOMAIN=npmguard-demo.eth
|
|
180
187
|
export NPMGUARD_ENS_RPC_URL=https://ethereum-sepolia-rpc.publicnode.com
|
|
181
188
|
npmguard install lodash
|
|
@@ -185,6 +192,7 @@ Available values:
|
|
|
185
192
|
|
|
186
193
|
| Source | Behavior |
|
|
187
194
|
|---|---|
|
|
195
|
+
| `auto` | Tries ENS, then NpmGuard's Pinata storage API, then npm fallback |
|
|
188
196
|
| `npm` | Installs `<package>@<version>` from the normal npm registry |
|
|
189
197
|
| `pinata` | Reads `/package/<name>/storage?version=<version>` from NpmGuard and installs the pinned tarball URL |
|
|
190
198
|
| `ens` | Resolves Sepolia ENS `npmguard.*` text records and installs the announced Pinata tarball |
|
package/dist/commands/install.js
CHANGED
|
@@ -7,7 +7,8 @@ import { parsePackageArg, prompt, resolveLatestVersion, detectPackageManager, op
|
|
|
7
7
|
import { auditCommand } from "./audit.js";
|
|
8
8
|
import { payViaWalletConnect, readAuditFee } from "../wallet/walletconnect.js";
|
|
9
9
|
import { streamAuditEvents } from "../stream.js";
|
|
10
|
-
import { defaultEnsRootDomain, defaultEnsRpcUrl, normalizeInstallSource, resolveEnsInstallSpec, resolvePinataInstallSpec, } from "../install-source.js";
|
|
10
|
+
import { defaultEnsRootDomain, defaultEnsRpcUrl, normalizeInstallSource, resolveEnsInstallSpec, resolvePinataInstallSpec, resolvePublishedInstallSpec, } from "../install-source.js";
|
|
11
|
+
const POST_AUDIT_STORAGE_WAIT_MS = Number(process.env.NPMGUARD_STORAGE_WAIT_MS ?? 90_000);
|
|
11
12
|
/**
|
|
12
13
|
* Run the correct "add this package" command for the detected package manager.
|
|
13
14
|
* `npm install <pkg>` adds the package. `pnpm add <pkg>` / `yarn add <pkg>` do
|
|
@@ -20,25 +21,44 @@ function runInstall(packageSpec, label) {
|
|
|
20
21
|
const res = spawnSync(pm, [verb, packageSpec], { stdio: "inherit" });
|
|
21
22
|
return res.status ?? 1;
|
|
22
23
|
}
|
|
23
|
-
async function resolveInstallTarget(fullSpec, packageName, version, opts) {
|
|
24
|
+
async function resolveInstallTarget(fullSpec, packageName, version, opts, waitForPublishedMs = 0) {
|
|
24
25
|
const source = normalizeInstallSource(opts.installSource);
|
|
26
|
+
const rootDomain = opts.ensRoot ?? defaultEnsRootDomain();
|
|
27
|
+
const rpcUrl = opts.ensRpc ?? defaultEnsRpcUrl();
|
|
28
|
+
if (source === "auto") {
|
|
29
|
+
const deadline = Date.now() + waitForPublishedMs;
|
|
30
|
+
do {
|
|
31
|
+
const published = await resolvePublishedInstallSpec({
|
|
32
|
+
apiUrl: opts.api,
|
|
33
|
+
packageName,
|
|
34
|
+
version,
|
|
35
|
+
rootDomain,
|
|
36
|
+
rpcUrl,
|
|
37
|
+
});
|
|
38
|
+
if (published)
|
|
39
|
+
return published;
|
|
40
|
+
if (Date.now() < deadline)
|
|
41
|
+
await delay(3000);
|
|
42
|
+
} while (Date.now() < deadline);
|
|
43
|
+
return { spec: fullSpec, detail: "npm registry (ENS/Pinata publication not available yet)" };
|
|
44
|
+
}
|
|
25
45
|
if (source === "npm") {
|
|
26
46
|
return { spec: fullSpec, detail: "npm registry" };
|
|
27
47
|
}
|
|
28
48
|
if (source === "pinata") {
|
|
29
|
-
return resolvePinataInstallSpec(opts.api, packageName, version);
|
|
49
|
+
return retryInstallSource(() => resolvePinataInstallSpec(opts.api, packageName, version), waitForPublishedMs);
|
|
30
50
|
}
|
|
31
|
-
return resolveEnsInstallSpec({
|
|
51
|
+
return retryInstallSource(() => resolveEnsInstallSpec({
|
|
32
52
|
packageName,
|
|
33
53
|
version,
|
|
34
|
-
rootDomain
|
|
35
|
-
rpcUrl
|
|
36
|
-
});
|
|
54
|
+
rootDomain,
|
|
55
|
+
rpcUrl,
|
|
56
|
+
}), waitForPublishedMs);
|
|
37
57
|
}
|
|
38
|
-
async function installSafePackage(fullSpec, packageName, version, opts) {
|
|
58
|
+
async function installSafePackage(fullSpec, packageName, version, opts, waitForPublishedMs = 0) {
|
|
39
59
|
let target;
|
|
40
60
|
try {
|
|
41
|
-
target = await resolveInstallTarget(fullSpec, packageName, version, opts);
|
|
61
|
+
target = await resolveInstallTarget(fullSpec, packageName, version, opts, waitForPublishedMs);
|
|
42
62
|
}
|
|
43
63
|
catch (err) {
|
|
44
64
|
console.error(chalk.red("Could not resolve install source: " +
|
|
@@ -48,6 +68,24 @@ async function installSafePackage(fullSpec, packageName, version, opts) {
|
|
|
48
68
|
}
|
|
49
69
|
process.exit(runInstall(target.spec, target.detail));
|
|
50
70
|
}
|
|
71
|
+
function delay(ms) {
|
|
72
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
73
|
+
}
|
|
74
|
+
async function retryInstallSource(resolve, waitMs) {
|
|
75
|
+
const deadline = Date.now() + waitMs;
|
|
76
|
+
let lastError;
|
|
77
|
+
do {
|
|
78
|
+
try {
|
|
79
|
+
return await resolve();
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
lastError = err;
|
|
83
|
+
if (Date.now() < deadline)
|
|
84
|
+
await delay(3000);
|
|
85
|
+
}
|
|
86
|
+
} while (Date.now() < deadline);
|
|
87
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
88
|
+
}
|
|
51
89
|
function extractVerdict(report) {
|
|
52
90
|
const nested = report.report?.verdict;
|
|
53
91
|
return (nested ?? report.verdict ?? "UNKNOWN").toUpperCase();
|
|
@@ -184,7 +222,7 @@ function buildBrowserWalletUrl(webUrl, packageName, version) {
|
|
|
184
222
|
return url.toString();
|
|
185
223
|
}
|
|
186
224
|
function sleep(ms) {
|
|
187
|
-
return
|
|
225
|
+
return delay(ms);
|
|
188
226
|
}
|
|
189
227
|
async function waitForPackageReport(apiUrl, packageName, version, timeoutMs) {
|
|
190
228
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -228,7 +266,7 @@ async function runBrowserWalletAuditAndInstall(fullSpec, name, version, apiUrl,
|
|
|
228
266
|
process.exit(1);
|
|
229
267
|
}
|
|
230
268
|
spinner.succeed("Audit report received");
|
|
231
|
-
await finalizeWithReport(fullSpec, name, version, report, opts);
|
|
269
|
+
await finalizeWithReport(fullSpec, name, version, report, opts, POST_AUDIT_STORAGE_WAIT_MS);
|
|
232
270
|
}
|
|
233
271
|
async function runCryptoAuditAndInstall(fullSpec, name, version, apiUrl, opts) {
|
|
234
272
|
// 1. Read current fee from the engine's public config, then fall back to
|
|
@@ -296,13 +334,13 @@ async function finalizeAfterAudit(fullSpec, name, version, apiUrl, opts) {
|
|
|
296
334
|
console.log(chalk.red(" Audit finished but report not found."));
|
|
297
335
|
process.exit(1);
|
|
298
336
|
}
|
|
299
|
-
await finalizeWithReport(fullSpec, name, version, freshReport, opts);
|
|
337
|
+
await finalizeWithReport(fullSpec, name, version, freshReport, opts, POST_AUDIT_STORAGE_WAIT_MS);
|
|
300
338
|
}
|
|
301
|
-
async function finalizeWithReport(fullSpec, name, version, report, opts) {
|
|
339
|
+
async function finalizeWithReport(fullSpec, name, version, report, opts, waitForPublishedMs = 0) {
|
|
302
340
|
const verdict = extractVerdict(report);
|
|
303
341
|
if (verdict === "SAFE") {
|
|
304
342
|
console.log(chalk.green("\n ✓ SAFE — proceeding with install"));
|
|
305
|
-
await installSafePackage(fullSpec, name, version, opts);
|
|
343
|
+
await installSafePackage(fullSpec, name, version, opts, waitForPublishedMs);
|
|
306
344
|
}
|
|
307
345
|
console.log(chalk.red(`\n Audit verdict: ${verdict}`));
|
|
308
346
|
const confirm = await prompt(chalk.red.bold(" Install anyway? (y/N) "));
|
package/dist/index.js
CHANGED
|
@@ -34,9 +34,9 @@ program
|
|
|
34
34
|
.description("Install an npm package with NpmGuard security check")
|
|
35
35
|
.argument("<package>", "Package name, optionally with version (e.g. express@4.18.0)")
|
|
36
36
|
.option("-f, --force", "Install even if the package is flagged as dangerous")
|
|
37
|
-
.option("--install-source <source>", "Install SAFE packages from npm, pinata, or ens", process.env.NPMGUARD_INSTALL_SOURCE ?? "
|
|
38
|
-
.option("--ens-root <name>", "ENS root domain for
|
|
39
|
-
.option("--ens-rpc <url>", "Sepolia RPC URL for
|
|
37
|
+
.option("--install-source <source>", "Install SAFE packages from auto, npm, pinata, or ens", process.env.NPMGUARD_INSTALL_SOURCE ?? "auto")
|
|
38
|
+
.option("--ens-root <name>", "ENS root domain for auto/ens install resolution", defaultEnsRootDomain())
|
|
39
|
+
.option("--ens-rpc <url>", "Sepolia RPC URL for auto/ens install resolution", defaultEnsRpcUrl())
|
|
40
40
|
.action(async (pkg, cmdOpts) => {
|
|
41
41
|
const opts = program.opts();
|
|
42
42
|
await installCommand(pkg, {
|
package/dist/install-source.js
CHANGED
|
@@ -12,10 +12,10 @@ const resolverAbi = parseAbi([
|
|
|
12
12
|
"function text(bytes32 node, string key) view returns (string)",
|
|
13
13
|
]);
|
|
14
14
|
export function normalizeInstallSource(value) {
|
|
15
|
-
const source = (value ?? "
|
|
16
|
-
if (source === "npm" || source === "pinata" || source === "ens")
|
|
15
|
+
const source = (value ?? "auto").toLowerCase();
|
|
16
|
+
if (source === "auto" || source === "npm" || source === "pinata" || source === "ens")
|
|
17
17
|
return source;
|
|
18
|
-
throw new Error(`Invalid install source "${value}". Expected npm, pinata, or ens.`);
|
|
18
|
+
throw new Error(`Invalid install source "${value}". Expected auto, npm, pinata, or ens.`);
|
|
19
19
|
}
|
|
20
20
|
export function defaultEnsRootDomain() {
|
|
21
21
|
return process.env.NPMGUARD_ENS_ROOT_DOMAIN ?? DEFAULT_ENS_ROOT_DOMAIN;
|
|
@@ -119,13 +119,32 @@ export async function resolveEnsInstallSpec(options) {
|
|
|
119
119
|
continue;
|
|
120
120
|
const directTarball = records["npmguard.tarball_uri"] ?? records["npmguard.latest_tarball_uri"];
|
|
121
121
|
if (directTarball)
|
|
122
|
-
return { spec: directTarball, detail: `ENS ${name}
|
|
122
|
+
return { spec: directTarball, detail: `ENS ${name} -> Pinata tarball` };
|
|
123
123
|
const manifestUrl = records["npmguard.manifest_uri"] ?? records["npmguard.latest_manifest_uri"];
|
|
124
124
|
if (manifestUrl) {
|
|
125
125
|
const tarballUrl = await tarballFromManifest(manifestUrl);
|
|
126
126
|
if (tarballUrl)
|
|
127
|
-
return { spec: tarballUrl, detail: `ENS ${name}
|
|
127
|
+
return { spec: tarballUrl, detail: `ENS ${name} -> manifest -> Pinata tarball` };
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
130
|
throw new Error(`No installable ENS tarball record found for ${options.packageName}@${options.version}`);
|
|
131
131
|
}
|
|
132
|
+
export async function resolvePublishedInstallSpec(options) {
|
|
133
|
+
try {
|
|
134
|
+
return await resolveEnsInstallSpec({
|
|
135
|
+
packageName: options.packageName,
|
|
136
|
+
version: options.version,
|
|
137
|
+
rootDomain: options.rootDomain,
|
|
138
|
+
rpcUrl: options.rpcUrl,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// Fall through to the API-backed Pinata publication.
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
return await resolvePinataInstallSpec(options.apiUrl, options.packageName, options.version);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|