npmguard-cli 1.1.3 → 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
@@ -7,7 +7,7 @@ engine before it touches your `node_modules`.
7
7
  npx npmguard-cli install express
8
8
  ```
9
9
 
10
- - **SAFE** → installs immediately
10
+ - **SAFE** → installs immediately from the configured source
11
11
  - **DANGEROUS** → warns, shows findings, asks before installing
12
12
  - **No audit yet** → offers to pay for one (Stripe or crypto), then streams
13
13
  the results in real time
@@ -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,6 +63,30 @@ 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 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:
71
+
72
+ ```bash
73
+ # default: audited verdict, then ENS/Pinata when available
74
+ npmguard install express --install-source auto
75
+
76
+ # default: audited verdict, then normal registry install
77
+ npmguard install express --install-source npm
78
+
79
+ # audited verdict, then install the tarball published by NpmGuard on Pinata
80
+ npmguard install express --install-source pinata
81
+
82
+ # audited verdict, then resolve ENS text records to find the Pinata tarball
83
+ npmguard install express --install-source ens
84
+ ```
85
+
86
+ The source choice never replaces server-side verification. NpmGuard always
87
+ checks the report store first; `pinata` and `ens` only decide where the SAFE
88
+ package bytes come from.
89
+
66
90
  ### `npmguard audit <package>[@version]`
67
91
 
68
92
  Run a standalone audit without installing. Returns the verdict and exits.
@@ -151,6 +175,28 @@ export NPMGUARD_WEB_URL=http://localhost:3000
151
175
  npmguard install lodash
152
176
  ```
153
177
 
178
+ Install source:
179
+
180
+ ```bash
181
+ # via flag
182
+ npmguard install lodash --install-source ens
183
+
184
+ # via env
185
+ export NPMGUARD_INSTALL_SOURCE=ens
186
+ export NPMGUARD_ENS_ROOT_DOMAIN=npmguard-demo.eth
187
+ export NPMGUARD_ENS_RPC_URL=https://ethereum-sepolia-rpc.publicnode.com
188
+ npmguard install lodash
189
+ ```
190
+
191
+ Available values:
192
+
193
+ | Source | Behavior |
194
+ |---|---|
195
+ | `auto` | Tries ENS, then NpmGuard's Pinata storage API, then npm fallback |
196
+ | `npm` | Installs `<package>@<version>` from the normal npm registry |
197
+ | `pinata` | Reads `/package/<name>/storage?version=<version>` from NpmGuard and installs the pinned tarball URL |
198
+ | `ens` | Resolves Sepolia ENS `npmguard.*` text records and installs the announced Pinata tarball |
199
+
154
200
  No private key or paid RPC config is required from the user. For the
155
201
  WalletConnect path, the CLI uses `viem` with a public Base Sepolia RPC to
156
202
  read/confirm the transaction for local UX; your wallet broadcasts the tx,
package/dist/api.js CHANGED
@@ -78,3 +78,16 @@ export async function getPackageReport(apiUrl, packageName, version) {
78
78
  throw err;
79
79
  }
80
80
  }
81
+ export async function getPackageStorage(apiUrl, packageName, version) {
82
+ const query = `?version=${encodeURIComponent(version)}`;
83
+ const url = `${apiUrl}/package/${encodeURIComponent(packageName)}/storage${query}`;
84
+ try {
85
+ return await request(url);
86
+ }
87
+ catch (err) {
88
+ if (err instanceof Error && err.message.startsWith("HTTP 404")) {
89
+ return null;
90
+ }
91
+ throw err;
92
+ }
93
+ }
@@ -7,18 +7,85 @@ 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, resolvePublishedInstallSpec, } from "../install-source.js";
11
+ const POST_AUDIT_STORAGE_WAIT_MS = Number(process.env.NPMGUARD_STORAGE_WAIT_MS ?? 90_000);
10
12
  /**
11
13
  * Run the correct "add this package" command for the detected package manager.
12
14
  * `npm install <pkg>` adds the package. `pnpm add <pkg>` / `yarn add <pkg>` do
13
15
  * the same. Crucially, `yarn install <pkg>` would ignore <pkg> in yarn classic.
14
16
  */
15
- function runInstall(packageSpec) {
17
+ function runInstall(packageSpec, label) {
16
18
  const pm = detectPackageManager();
17
19
  const verb = pm === "npm" ? "install" : "add";
18
- console.log(chalk.gray(`\n Running: ${pm} ${verb} ${packageSpec}\n`));
20
+ console.log(chalk.gray(`\n Running: ${pm} ${verb} ${packageSpec}${label ? `\n Source: ${label}` : ""}\n`));
19
21
  const res = spawnSync(pm, [verb, packageSpec], { stdio: "inherit" });
20
22
  return res.status ?? 1;
21
23
  }
24
+ async function resolveInstallTarget(fullSpec, packageName, version, opts, waitForPublishedMs = 0) {
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
+ }
45
+ if (source === "npm") {
46
+ return { spec: fullSpec, detail: "npm registry" };
47
+ }
48
+ if (source === "pinata") {
49
+ return retryInstallSource(() => resolvePinataInstallSpec(opts.api, packageName, version), waitForPublishedMs);
50
+ }
51
+ return retryInstallSource(() => resolveEnsInstallSpec({
52
+ packageName,
53
+ version,
54
+ rootDomain,
55
+ rpcUrl,
56
+ }), waitForPublishedMs);
57
+ }
58
+ async function installSafePackage(fullSpec, packageName, version, opts, waitForPublishedMs = 0) {
59
+ let target;
60
+ try {
61
+ target = await resolveInstallTarget(fullSpec, packageName, version, opts, waitForPublishedMs);
62
+ }
63
+ catch (err) {
64
+ console.error(chalk.red("Could not resolve install source: " +
65
+ (err instanceof Error ? err.message : String(err))));
66
+ console.log(chalk.gray(" Retry with --install-source npm to install from the npm registry."));
67
+ process.exit(1);
68
+ }
69
+ process.exit(runInstall(target.spec, target.detail));
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
+ }
22
89
  function extractVerdict(report) {
23
90
  const nested = report.report?.verdict;
24
91
  return (nested ?? report.verdict ?? "UNKNOWN").toUpperCase();
@@ -66,7 +133,7 @@ export async function installCommand(packageSpec, opts) {
66
133
  }
67
134
  spinner.stop();
68
135
  if (report) {
69
- handleExistingReport(report, name, fullSpec, apiUrl, opts);
136
+ await handleExistingReport(report, name, version, fullSpec, apiUrl, opts);
70
137
  return;
71
138
  }
72
139
  // No audit found — ask how to pay
@@ -81,25 +148,25 @@ export async function installCommand(packageSpec, opts) {
81
148
  console.log();
82
149
  const choice = await prompt(" Choice [1/2/3/4/5]: ");
83
150
  if (choice === "1") {
84
- await runStripeAuditAndInstall(fullSpec, name, version, apiUrl);
151
+ await runStripeAuditAndInstall(fullSpec, name, version, apiUrl, opts);
85
152
  return;
86
153
  }
87
154
  if (choice === "2") {
88
- await runBrowserWalletAuditAndInstall(fullSpec, name, version, apiUrl, webUrl);
155
+ await runBrowserWalletAuditAndInstall(fullSpec, name, version, apiUrl, webUrl, opts);
89
156
  return;
90
157
  }
91
158
  if (choice === "3") {
92
- await runCryptoAuditAndInstall(fullSpec, name, version, apiUrl);
159
+ await runCryptoAuditAndInstall(fullSpec, name, version, apiUrl, opts);
93
160
  return;
94
161
  }
95
162
  if (choice === "4") {
96
163
  console.log(chalk.yellow(" Installing without audit. Proceed at your own risk."));
97
- process.exit(runInstall(fullSpec));
164
+ process.exit(runInstall(fullSpec, "npm registry (audit skipped)"));
98
165
  }
99
166
  console.log(chalk.gray(" Cancelled."));
100
167
  process.exit(0);
101
168
  }
102
- function handleExistingReport(report, name, fullSpec, apiUrl, opts) {
169
+ async function handleExistingReport(report, name, version, fullSpec, apiUrl, opts) {
103
170
  const verdict = extractVerdict(report);
104
171
  const capabilities = extractCapabilities(report);
105
172
  if (verdict === "SAFE") {
@@ -107,7 +174,8 @@ function handleExistingReport(report, name, fullSpec, apiUrl, opts) {
107
174
  if (capabilities.length > 0) {
108
175
  console.log(chalk.gray(` Capabilities: ${capabilities.join(", ")}`));
109
176
  }
110
- process.exit(runInstall(fullSpec));
177
+ await installSafePackage(fullSpec, name, version, opts);
178
+ return;
111
179
  }
112
180
  if (verdict === "DANGEROUS" || verdict === "CRITICAL" || verdict === "WARNING") {
113
181
  console.log(chalk.bgRed.white.bold(` ${verdict} `));
@@ -119,23 +187,24 @@ function handleExistingReport(report, name, fullSpec, apiUrl, opts) {
119
187
  console.log();
120
188
  if (opts.force) {
121
189
  console.log(chalk.yellow(" --force passed, installing anyway..."));
122
- process.exit(runInstall(fullSpec));
190
+ await installSafePackage(fullSpec, name, version, opts);
191
+ return;
123
192
  }
124
- promptAndInstallIfAccepted(fullSpec, " Install anyway? This package is flagged. (y/N) ");
193
+ await promptAndInstallIfAccepted(fullSpec, name, version, opts, " Install anyway? This package is flagged. (y/N) ");
125
194
  return;
126
195
  }
127
196
  console.log(chalk.yellow(` Verdict: ${verdict}`));
128
- promptAndInstallIfAccepted(fullSpec, " Proceed with install? (y/N) ");
197
+ await promptAndInstallIfAccepted(fullSpec, name, version, opts, " Proceed with install? (y/N) ");
129
198
  }
130
- async function promptAndInstallIfAccepted(fullSpec, question) {
199
+ async function promptAndInstallIfAccepted(fullSpec, name, version, opts, question) {
131
200
  const answer = await prompt(chalk.red.bold(question));
132
201
  if (answer === "y" || answer === "yes") {
133
- process.exit(runInstall(fullSpec));
202
+ await installSafePackage(fullSpec, name, version, opts);
134
203
  }
135
204
  console.log(chalk.gray(" Aborted."));
136
205
  process.exit(1);
137
206
  }
138
- async function runStripeAuditAndInstall(fullSpec, name, version, apiUrl) {
207
+ async function runStripeAuditAndInstall(fullSpec, name, version, apiUrl, opts) {
139
208
  try {
140
209
  await auditCommand(fullSpec, { api: apiUrl, exit: false });
141
210
  }
@@ -143,7 +212,7 @@ async function runStripeAuditAndInstall(fullSpec, name, version, apiUrl) {
143
212
  console.error(chalk.red("Audit failed: " + (err instanceof Error ? err.message : String(err))));
144
213
  process.exit(1);
145
214
  }
146
- await finalizeAfterAudit(fullSpec, name, version, apiUrl);
215
+ await finalizeAfterAudit(fullSpec, name, version, apiUrl, opts);
147
216
  }
148
217
  function buildBrowserWalletUrl(webUrl, packageName, version) {
149
218
  const url = new URL("/pay", webUrl.replace(/\/+$/, ""));
@@ -153,7 +222,7 @@ function buildBrowserWalletUrl(webUrl, packageName, version) {
153
222
  return url.toString();
154
223
  }
155
224
  function sleep(ms) {
156
- return new Promise((resolve) => setTimeout(resolve, ms));
225
+ return delay(ms);
157
226
  }
158
227
  async function waitForPackageReport(apiUrl, packageName, version, timeoutMs) {
159
228
  const deadline = Date.now() + timeoutMs;
@@ -165,7 +234,7 @@ async function waitForPackageReport(apiUrl, packageName, version, timeoutMs) {
165
234
  }
166
235
  return null;
167
236
  }
168
- async function runBrowserWalletAuditAndInstall(fullSpec, name, version, apiUrl, webUrl) {
237
+ async function runBrowserWalletAuditAndInstall(fullSpec, name, version, apiUrl, webUrl, opts) {
169
238
  const paymentUrl = buildBrowserWalletUrl(webUrl, name, version);
170
239
  console.log();
171
240
  console.log(chalk.bold(" Open this URL in Brave or Chrome with MetaMask/Rabby:"));
@@ -197,9 +266,9 @@ async function runBrowserWalletAuditAndInstall(fullSpec, name, version, apiUrl,
197
266
  process.exit(1);
198
267
  }
199
268
  spinner.succeed("Audit report received");
200
- await finalizeWithReport(fullSpec, report);
269
+ await finalizeWithReport(fullSpec, name, version, report, opts, POST_AUDIT_STORAGE_WAIT_MS);
201
270
  }
202
- async function runCryptoAuditAndInstall(fullSpec, name, version, apiUrl) {
271
+ async function runCryptoAuditAndInstall(fullSpec, name, version, apiUrl, opts) {
203
272
  // 1. Read current fee from the engine's public config, then fall back to
204
273
  // direct contract read if the engine is older or config is unavailable.
205
274
  let feeWei;
@@ -257,26 +326,26 @@ async function runCryptoAuditAndInstall(fullSpec, name, version, apiUrl) {
257
326
  // would trigger a second, unpaid audit via /checkout).
258
327
  await streamAuditEvents(apiUrl, auditId);
259
328
  // 5. Fetch the persisted report and decide whether to install
260
- await finalizeAfterAudit(fullSpec, name, version, apiUrl);
329
+ await finalizeAfterAudit(fullSpec, name, version, apiUrl, opts);
261
330
  }
262
- async function finalizeAfterAudit(fullSpec, name, version, apiUrl) {
331
+ async function finalizeAfterAudit(fullSpec, name, version, apiUrl, opts) {
263
332
  const freshReport = await api.getPackageReport(apiUrl, name, version);
264
333
  if (!freshReport) {
265
334
  console.log(chalk.red(" Audit finished but report not found."));
266
335
  process.exit(1);
267
336
  }
268
- await finalizeWithReport(fullSpec, freshReport);
337
+ await finalizeWithReport(fullSpec, name, version, freshReport, opts, POST_AUDIT_STORAGE_WAIT_MS);
269
338
  }
270
- async function finalizeWithReport(fullSpec, report) {
339
+ async function finalizeWithReport(fullSpec, name, version, report, opts, waitForPublishedMs = 0) {
271
340
  const verdict = extractVerdict(report);
272
341
  if (verdict === "SAFE") {
273
342
  console.log(chalk.green("\n ✓ SAFE — proceeding with install"));
274
- process.exit(runInstall(fullSpec));
343
+ await installSafePackage(fullSpec, name, version, opts, waitForPublishedMs);
275
344
  }
276
345
  console.log(chalk.red(`\n Audit verdict: ${verdict}`));
277
346
  const confirm = await prompt(chalk.red.bold(" Install anyway? (y/N) "));
278
347
  if (confirm === "y" || confirm === "yes") {
279
- process.exit(runInstall(fullSpec));
348
+ await installSafePackage(fullSpec, name, version, opts);
280
349
  }
281
350
  process.exit(1);
282
351
  }
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import { readFileSync } from "node:fs";
4
4
  import { auditCommand } from "./commands/audit.js";
5
5
  import { checkCommand } from "./commands/check.js";
6
6
  import { installCommand } from "./commands/install.js";
7
+ import { defaultEnsRootDomain, defaultEnsRpcUrl, normalizeInstallSource } from "./install-source.js";
7
8
  function readPackageVersion() {
8
9
  try {
9
10
  const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
@@ -33,9 +34,19 @@ program
33
34
  .description("Install an npm package with NpmGuard security check")
34
35
  .argument("<package>", "Package name, optionally with version (e.g. express@4.18.0)")
35
36
  .option("-f, --force", "Install even if the package is flagged as dangerous")
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())
36
40
  .action(async (pkg, cmdOpts) => {
37
41
  const opts = program.opts();
38
- await installCommand(pkg, { api: opts.api, web: opts.web, force: cmdOpts.force });
42
+ await installCommand(pkg, {
43
+ api: opts.api,
44
+ web: opts.web,
45
+ force: cmdOpts.force,
46
+ installSource: normalizeInstallSource(cmdOpts.installSource),
47
+ ensRoot: cmdOpts.ensRoot,
48
+ ensRpc: cmdOpts.ensRpc,
49
+ });
39
50
  });
40
51
  program
41
52
  .command("check")
@@ -0,0 +1,150 @@
1
+ import { createPublicClient, http, parseAbi, toHex, zeroAddress } from "viem";
2
+ import { sepolia } from "viem/chains";
3
+ import { namehash, packetToBytes } from "viem/ens";
4
+ import * as api from "./api.js";
5
+ const DEFAULT_ENS_RPC_URL = "https://ethereum-sepolia-rpc.publicnode.com";
6
+ const DEFAULT_ENS_ROOT_DOMAIN = "npmguard-demo.eth";
7
+ const UNIVERSAL_RESOLVER = "0xeEeEEEeE14D718C2B47D9923Deab1335E144EeEe";
8
+ const universalResolverAbi = parseAbi([
9
+ "function findResolver(bytes name) view returns (address resolver, bytes32 node, uint256 offset)",
10
+ ]);
11
+ const resolverAbi = parseAbi([
12
+ "function text(bytes32 node, string key) view returns (string)",
13
+ ]);
14
+ export function normalizeInstallSource(value) {
15
+ const source = (value ?? "auto").toLowerCase();
16
+ if (source === "auto" || source === "npm" || source === "pinata" || source === "ens")
17
+ return source;
18
+ throw new Error(`Invalid install source "${value}". Expected auto, npm, pinata, or ens.`);
19
+ }
20
+ export function defaultEnsRootDomain() {
21
+ return process.env.NPMGUARD_ENS_ROOT_DOMAIN ?? DEFAULT_ENS_ROOT_DOMAIN;
22
+ }
23
+ export function defaultEnsRpcUrl() {
24
+ return process.env.NPMGUARD_ENS_RPC_URL ?? process.env.SEPOLIA_RPC_URL ?? DEFAULT_ENS_RPC_URL;
25
+ }
26
+ function ensSafeLabel(value) {
27
+ const label = value
28
+ .replace(/^@/, "")
29
+ .replace(/[^a-z0-9-]+/gi, "-")
30
+ .replace(/^-+|-+$/g, "")
31
+ .toLowerCase();
32
+ if (!label)
33
+ throw new Error(`Cannot derive an ENS label from "${value}"`);
34
+ return label.slice(0, 63);
35
+ }
36
+ function tarballFromStorage(storage) {
37
+ return (storage.storage.tarball?.gatewayUrl ??
38
+ storage.storage.manifest?.value?.tarball?.gatewayUrl ??
39
+ null);
40
+ }
41
+ export async function resolvePinataInstallSpec(apiUrl, packageName, version) {
42
+ const publication = await api.getPackageStorage(apiUrl, packageName, version);
43
+ if (!publication) {
44
+ throw new Error(`No Pinata publication found for ${packageName}@${version}`);
45
+ }
46
+ const tarballUrl = tarballFromStorage(publication);
47
+ if (!tarballUrl) {
48
+ throw new Error(`Pinata publication for ${packageName}@${version} has no installable tarball URL`);
49
+ }
50
+ return {
51
+ spec: tarballUrl,
52
+ detail: publication.storage.ens?.recordName
53
+ ? `Pinata tarball announced by ${publication.storage.ens.recordName}`
54
+ : "Pinata tarball from NpmGuard storage API",
55
+ };
56
+ }
57
+ async function readTextRecords(name, keys, rpcUrl) {
58
+ const client = createPublicClient({ chain: sepolia, transport: http(rpcUrl) });
59
+ const found = await client.readContract({
60
+ address: UNIVERSAL_RESOLVER,
61
+ abi: universalResolverAbi,
62
+ functionName: "findResolver",
63
+ args: [toHex(packetToBytes(name))],
64
+ });
65
+ const resolver = found.resolver ?? found[0] ?? zeroAddress;
66
+ if (!resolver || resolver === zeroAddress)
67
+ return {};
68
+ const node = namehash(name);
69
+ const records = {};
70
+ for (const key of keys) {
71
+ try {
72
+ const value = await client.readContract({
73
+ address: resolver,
74
+ abi: resolverAbi,
75
+ functionName: "text",
76
+ args: [node, key],
77
+ });
78
+ if (value)
79
+ records[key] = value;
80
+ }
81
+ catch {
82
+ // Some resolvers throw for missing keys; treat that as an empty record.
83
+ }
84
+ }
85
+ return records;
86
+ }
87
+ async function tarballFromManifest(manifestUrl) {
88
+ const res = await fetch(manifestUrl);
89
+ if (!res.ok)
90
+ return null;
91
+ const manifest = await res.json();
92
+ return manifest.tarball?.gatewayUrl ?? null;
93
+ }
94
+ export async function resolveEnsInstallSpec(options) {
95
+ const packageLabel = ensSafeLabel(options.packageName);
96
+ const versionLabel = ensSafeLabel(options.version || "latest");
97
+ const root = options.rootDomain.replace(/\.+$/, "");
98
+ const candidates = [
99
+ `${versionLabel}.${packageLabel}.${root}`,
100
+ `${packageLabel}.${root}`,
101
+ root,
102
+ ];
103
+ const keys = [
104
+ "npmguard.tarball_uri",
105
+ "npmguard.latest_tarball_uri",
106
+ "npmguard.manifest_uri",
107
+ "npmguard.latest_manifest_uri",
108
+ "npmguard.package",
109
+ "npmguard.version",
110
+ "npmguard.latest_version",
111
+ ];
112
+ for (const name of candidates) {
113
+ const records = await readTextRecords(name, keys, options.rpcUrl);
114
+ const recordPackage = records["npmguard.package"];
115
+ const recordVersion = records["npmguard.version"] ?? records["npmguard.latest_version"];
116
+ const packageMatches = !recordPackage || recordPackage === options.packageName;
117
+ const versionMatches = !recordVersion || recordVersion === options.version;
118
+ if (!packageMatches || !versionMatches)
119
+ continue;
120
+ const directTarball = records["npmguard.tarball_uri"] ?? records["npmguard.latest_tarball_uri"];
121
+ if (directTarball)
122
+ return { spec: directTarball, detail: `ENS ${name} -> Pinata tarball` };
123
+ const manifestUrl = records["npmguard.manifest_uri"] ?? records["npmguard.latest_manifest_uri"];
124
+ if (manifestUrl) {
125
+ const tarballUrl = await tarballFromManifest(manifestUrl);
126
+ if (tarballUrl)
127
+ return { spec: tarballUrl, detail: `ENS ${name} -> manifest -> Pinata tarball` };
128
+ }
129
+ }
130
+ throw new Error(`No installable ENS tarball record found for ${options.packageName}@${options.version}`);
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.3",
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",