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 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** → runs `<pm> add <pkg>` directly
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 is configurable:
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=pinata
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 |
@@ -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: opts.ensRoot ?? defaultEnsRootDomain(),
35
- rpcUrl: opts.ensRpc ?? defaultEnsRpcUrl(),
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 new Promise((resolve) => setTimeout(resolve, ms));
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 ?? "npm")
38
- .option("--ens-root <name>", "ENS root domain for --install-source ens", defaultEnsRootDomain())
39
- .option("--ens-rpc <url>", "Sepolia RPC URL for --install-source ens", defaultEnsRpcUrl())
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, {
@@ -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 ?? "npm").toLowerCase();
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} Pinata tarball` };
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} manifest Pinata tarball` };
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "npmguard-cli",
3
- "version": "1.1.4",
3
+ "version": "1.1.5",
4
4
  "type": "module",
5
5
  "description": "NpmGuard CLI — check npm packages against NpmGuard security audits",
6
6
  "main": "dist/index.js",