npmguard-cli 0.3.1 → 0.5.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/README.md ADDED
@@ -0,0 +1,155 @@
1
+ # npmguard-cli
2
+
3
+ CLI that checks npm packages against on-chain audit records before installing. Published on npm as [`npmguard-cli`](https://www.npmjs.com/package/npmguard-cli).
4
+
5
+ ## `npmguard-cli check`
6
+
7
+ Scans your `package.json`, queries the npm registry for available updates, then resolves each package version against ENS on Sepolia to find audit records.
8
+
9
+ ```bash
10
+ npx npmguard-cli check --path /your/project
11
+ ```
12
+
13
+ ```mermaid
14
+ flowchart LR
15
+ A[package.json] -->|read deps| B[npm registry]
16
+ B -->|latest version| C{ENS Sepolia}
17
+ C -->|resolve 1-7-9.axios.npmguard.eth| D[Text Records]
18
+ D -->|verdict, score, capabilities, CIDs| E[Terminal output]
19
+ ```
20
+
21
+ What happens:
22
+
23
+ 1. Reads `package.json` and extracts all dependencies
24
+ 2. For each dependency, fetches `https://registry.npmjs.org/{package}/latest` to get the latest version
25
+ 3. Constructs the ENS name: `{version}.{package}.npmguard.eth` (e.g. `1-7-9.axios.npmguard.eth`)
26
+ 4. Resolves these ENS text records on Sepolia via a public RPC:
27
+ - `npmguard.verdict` — SAFE / CRITICAL
28
+ - `npmguard.score` — 0 to 100
29
+ - `npmguard.capabilities` — network, filesystem, process_spawn, etc.
30
+ - `npmguard.report_cid` — IPFS CID of the full audit report
31
+ - `npmguard.source_cid` — IPFS CID of the audited source code
32
+ 5. Displays a table with verdict and capabilities per package
33
+ 6. Lists clickable IPFS links to the full audit reports
34
+
35
+ Example output:
36
+
37
+ ```
38
+ NpmGuard Dependency Audit
39
+
40
+ ┌─────────┬───────────┬────────┬──────────────┬──────────────┐
41
+ │ Package │ Installed │ Latest │ Verdict │ Capabilities │
42
+ ├─────────┼───────────┼────────┼──────────────┼──────────────┤
43
+ │ axios │ 1.6.0 │ 1.7.9 │ SAFE (96) │ network │
44
+ ├─────────┼───────────┼────────┼──────────────┼──────────────┤
45
+ │ lodash │ 4.17.21 │ 4.18.1 │ CRITICAL (12)│ network, ... │
46
+ ├─────────┼───────────┼────────┼──────────────┼──────────────┤
47
+ │ chalk │ 5.6.2 │ 5.6.2 │ NOT AUDITED │ - │
48
+ └─────────┴───────────┴────────┴──────────────┴──────────────┘
49
+
50
+ 1 safe | 1 critical | 1 not audited
51
+
52
+ axios@1.7.9 report: https://gateway.pinata.cloud/ipfs/bafkrei...
53
+ lodash@4.18.1 report: https://gateway.pinata.cloud/ipfs/bafkrei...
54
+ ```
55
+
56
+ ## `npmguard-cli install`
57
+
58
+ Installs a package from IPFS instead of the npm registry when an audit record exists on ENS.
59
+
60
+ ```bash
61
+ npx npmguard-cli install axios
62
+ npx npmguard-cli install axios@1.7.9
63
+ ```
64
+
65
+ ```mermaid
66
+ flowchart TD
67
+ A[npmguard-cli install axios] --> B{ENS Sepolia}
68
+ B -->|resolve 1-7-9.axios.npmguard.eth| C[Get verdict + sourceCid]
69
+ C --> D{Verdict?}
70
+ D -->|SAFE / WARNING| E[npm install from IPFS gateway]
71
+ D -->|CRITICAL| F[Installation blocked]
72
+ F -->|--force| E
73
+ E -->|tarball from gateway.pinata.cloud/ipfs/sourceCid| G[node_modules/]
74
+ ```
75
+
76
+ What happens:
77
+
78
+ 1. If no version specified, fetches the latest version from npm registry
79
+ 2. Resolves `{version}.{package}.npmguard.eth` on Sepolia ENS
80
+ 3. Reads the `npmguard.source_cid` text record — the IPFS CID of the audited source tarball
81
+ 4. Runs `npm install https://gateway.pinata.cloud/ipfs/{sourceCid}`
82
+ 5. npm downloads the tarball directly from IPFS and installs it
83
+
84
+ The result in `package-lock.json`:
85
+
86
+ ```json
87
+ "node_modules/axios": {
88
+ "version": "1.7.9",
89
+ "resolved": "https://gateway.pinata.cloud/ipfs/bafkrei...",
90
+ "integrity": "sha512-..."
91
+ }
92
+ ```
93
+
94
+ The `resolved` field points to IPFS, not npm. Anyone running `npm install` on this project gets the exact same IPFS-verified code.
95
+
96
+ If the package is flagged **CRITICAL**, installation is blocked:
97
+
98
+ ```
99
+ lodash@4.18.1
100
+
101
+ CRITICAL (score: 12)
102
+ Capabilities: network, filesystem, process_spawn
103
+ Report: https://gateway.pinata.cloud/ipfs/bafkrei...
104
+
105
+ Installation blocked. This package has critical security issues.
106
+ Use --force to install anyway.
107
+ ```
108
+
109
+ If no audit record exists on ENS, falls back to standard `npm install`.
110
+
111
+ ## ENS structure
112
+
113
+ ```mermaid
114
+ graph TD
115
+ A[npmguard.eth] --> B[axios.npmguard.eth]
116
+ A --> C[lodash.npmguard.eth]
117
+ B --> D[1-7-9.axios.npmguard.eth]
118
+ B --> E[1-8-0.axios.npmguard.eth]
119
+ D --> F["npmguard.verdict = safe<br/>npmguard.score = 96<br/>npmguard.capabilities = network<br/>npmguard.report_cid = bafkrei...<br/>npmguard.source_cid = bafkrei..."]
120
+ E --> G["npmguard.verdict = critical<br/>npmguard.score = 66"]
121
+ ```
122
+
123
+ ## Project structure
124
+
125
+ ```
126
+ cli/
127
+ ├── src/
128
+ │ ├── index.ts # Entry point — commander setup
129
+ │ ├── audit-source.ts # AuditSource interface
130
+ │ ├── ens-source.ts # Reads ENS text records on Sepolia via viem
131
+ │ ├── mock-source.ts # Mock data for testing without ENS
132
+ │ ├── scanner.ts # Reads package.json + fetches npm for updates
133
+ │ └── commands/
134
+ │ ├── check.ts # npmguard-cli check
135
+ │ └── install.ts # npmguard-cli install
136
+ ├── reports/ # Sample audit reports (uploaded to IPFS)
137
+ ├── package.json
138
+ └── tsconfig.json
139
+ ```
140
+
141
+ ## Development
142
+
143
+ ```bash
144
+ cd cli && npm install
145
+
146
+ # Run locally
147
+ npx tsx src/index.ts check --path /tmp/test-project
148
+ npx tsx src/index.ts install axios@1.7.9
149
+
150
+ # Build
151
+ npm run build
152
+
153
+ # Publish
154
+ npm publish
155
+ ```
@@ -1,17 +1,188 @@
1
1
  import chalk from "chalk";
2
2
  import ora from "ora";
3
+ import qrcode from "qrcode-terminal";
4
+ import { createInterface } from "node:readline";
3
5
  import { execSync } from "node:child_process";
4
- import { readFile, writeFile, unlink, mkdir } from "node:fs/promises";
5
- import { join } from "node:path";
6
- import { tmpdir } from "node:os";
6
+ import { createPublicClient, createWalletClient, http, formatEther, encodeFunctionData, } from "viem";
7
+ import { privateKeyToAccount } from "viem/accounts";
8
+ import { defineChain } from "viem";
9
+ import { SignClient } from "@walletconnect/sign-client";
10
+ import { AUDIT_REQUEST_ADDRESS_0G, AUDIT_REQUEST_ABI, } from "../contract.js";
11
+ const ogGalileo = defineChain({
12
+ id: 16602,
13
+ name: "0G-Galileo-Testnet",
14
+ nativeCurrency: { name: "0G", symbol: "0G", decimals: 18 },
15
+ rpcUrls: { default: { http: ["https://evmrpc-testnet.0g.ai"] } },
16
+ blockExplorers: { default: { name: "0G Explorer", url: "https://chainscan-galileo.0g.ai" } },
17
+ testnet: true,
18
+ });
7
19
  const IPFS_GATEWAY = "https://gateway.pinata.cloud/ipfs";
8
- async function downloadFromIPFS(cid, dest) {
9
- const resp = await fetch(`${IPFS_GATEWAY}/${cid}`);
10
- if (!resp.ok) {
11
- throw new Error(`IPFS download failed: ${resp.status}`);
20
+ const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
21
+ const DEFAULT_AUDIT_API_URL = "http://209.38.42.28:8000/audit";
22
+ const WALLETCONNECT_PROJECT_ID = process.env.WALLETCONNECT_PROJECT_ID ?? "d5eb170c427570e15ac00ae53acc93ba";
23
+ const OG_RPC = "https://evmrpc-testnet.0g.ai";
24
+ const BLOCK_EXPLORER = "https://chainscan-galileo.0g.ai";
25
+ function prompt(question) {
26
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
27
+ return new Promise((resolve) => {
28
+ rl.question(question, (answer) => {
29
+ rl.close();
30
+ resolve(answer.trim().toLowerCase());
31
+ });
32
+ });
33
+ }
34
+ function generateQrCode(text) {
35
+ return new Promise((resolve) => {
36
+ qrcode.generate(text, { small: true }, (code) => {
37
+ console.log(code);
38
+ resolve();
39
+ });
40
+ });
41
+ }
42
+ async function askInstallWithoutAudit(packageSpec) {
43
+ const answer = await prompt(chalk.white(` Install from npm without audit? (y/n) `));
44
+ if (answer === "y" || answer === "yes") {
45
+ console.log();
46
+ execSync(`npm install ${packageSpec}`, { stdio: "inherit" });
47
+ }
48
+ }
49
+ async function requestAuditOnChain(packageName, version, privateKey) {
50
+ const account = privateKeyToAccount(privateKey);
51
+ const rpcUrl = OG_RPC;
52
+ const publicClient = createPublicClient({
53
+ chain: ogGalileo,
54
+ transport: http(rpcUrl),
55
+ });
56
+ const walletClient = createWalletClient({
57
+ account,
58
+ chain: ogGalileo,
59
+ transport: http(rpcUrl),
60
+ });
61
+ const auditFee = await publicClient.readContract({
62
+ address: AUDIT_REQUEST_ADDRESS_0G,
63
+ abi: AUDIT_REQUEST_ABI,
64
+ functionName: "auditFee",
65
+ });
66
+ const hash = await walletClient.writeContract({
67
+ address: AUDIT_REQUEST_ADDRESS_0G,
68
+ abi: AUDIT_REQUEST_ABI,
69
+ functionName: "requestAudit",
70
+ args: [packageName, version],
71
+ value: auditFee,
72
+ });
73
+ await publicClient.waitForTransactionReceipt({ hash });
74
+ return hash;
75
+ }
76
+ async function payViaWalletConnect(packageName, version, feeWei, feeDisplay) {
77
+ const calldata = encodeFunctionData({
78
+ abi: AUDIT_REQUEST_ABI,
79
+ functionName: "requestAudit",
80
+ args: [packageName, version],
81
+ });
82
+ let signClient = null;
83
+ // WalletConnect throws unhandled errors when MetaMask sends
84
+ // session events after the session is cleaned up — suppress them
85
+ const wcErrorHandler = (err) => {
86
+ if (err?.message?.includes("No matching key"))
87
+ return;
88
+ console.error(err);
89
+ process.exit(1);
90
+ };
91
+ process.on("uncaughtException", wcErrorHandler);
92
+ try {
93
+ const initSpinner = ora(" Connecting to WalletConnect...").start();
94
+ signClient = await SignClient.init({
95
+ projectId: WALLETCONNECT_PROJECT_ID,
96
+ metadata: {
97
+ name: "NpmGuard",
98
+ description: "NPM package security audit",
99
+ url: "https://npmguard.dev",
100
+ icons: [],
101
+ },
102
+ });
103
+ initSpinner.stop();
104
+ const { uri, approval } = await signClient.connect({
105
+ requiredNamespaces: {
106
+ eip155: {
107
+ methods: ["eth_sendTransaction"],
108
+ chains: ["eip155:16602"],
109
+ events: ["chainChanged", "accountsChanged"],
110
+ },
111
+ },
112
+ });
113
+ if (!uri) {
114
+ console.log(chalk.red(" Failed to generate WalletConnect URI"));
115
+ return false;
116
+ }
117
+ console.log();
118
+ console.log(chalk.cyan(` Scan with your wallet to connect:`));
119
+ console.log();
120
+ await generateQrCode(uri);
121
+ console.log();
122
+ const pairSpinner = ora(" Waiting for wallet connection...").start();
123
+ const session = await approval();
124
+ // Find the 0G Galileo account in approved namespaces
125
+ const accounts = session.namespaces.eip155?.accounts ?? [];
126
+ const ogAccount = accounts.find((a) => a.startsWith("eip155:16602:"));
127
+ const account = ogAccount
128
+ ? ogAccount.split(":")[2]
129
+ : accounts[0]?.split(":")[2];
130
+ if (!account) {
131
+ pairSpinner.fail("Wallet did not approve any accounts");
132
+ return false;
133
+ }
134
+ pairSpinner.succeed(`Connected: ${account.slice(0, 6)}...${account.slice(-4)}`);
135
+ console.log(chalk.cyan(` Confirm the ${feeDisplay} transaction in your wallet...`));
136
+ const txHash = await signClient.request({
137
+ topic: session.topic,
138
+ chainId: "eip155:16602",
139
+ request: {
140
+ method: "eth_sendTransaction",
141
+ params: [
142
+ {
143
+ from: account,
144
+ to: AUDIT_REQUEST_ADDRESS_0G,
145
+ data: calldata,
146
+ value: "0x" + feeWei.toString(16),
147
+ },
148
+ ],
149
+ },
150
+ });
151
+ const confirmSpinner = ora(" Waiting for on-chain confirmation...").start();
152
+ const ogClient = createPublicClient({
153
+ chain: ogGalileo,
154
+ transport: http(OG_RPC),
155
+ });
156
+ const receipt = await ogClient.waitForTransactionReceipt({
157
+ hash: txHash,
158
+ });
159
+ if (receipt.status === "success") {
160
+ confirmSpinner.succeed("Payment confirmed on-chain!");
161
+ console.log(chalk.gray(` Tx: ${BLOCK_EXPLORER}/tx/${txHash}`));
162
+ console.log();
163
+ return true;
164
+ }
165
+ else {
166
+ confirmSpinner.fail("Transaction reverted");
167
+ return false;
168
+ }
169
+ }
170
+ catch (err) {
171
+ const msg = err.message ?? String(err);
172
+ if (msg.includes("rejected") || msg.includes("denied")) {
173
+ console.log(chalk.yellow(" Transaction rejected by user."));
174
+ }
175
+ else {
176
+ console.log(chalk.red(` WalletConnect error: ${msg}`));
177
+ }
178
+ console.log();
179
+ return false;
180
+ }
181
+ finally {
182
+ signClient = null;
183
+ // Remove handler after a delay to catch late WC events
184
+ setTimeout(() => process.off("uncaughtException", wcErrorHandler), 5000);
12
185
  }
13
- const buffer = Buffer.from(await resp.arrayBuffer());
14
- await writeFile(dest, buffer);
15
186
  }
16
187
  export async function installCommand(packageSpec, auditSource, force = false) {
17
188
  const atIndex = packageSpec.lastIndexOf("@");
@@ -46,16 +217,144 @@ export async function installCommand(packageSpec, auditSource, force = false) {
46
217
  console.log();
47
218
  console.log(chalk.bold(` ${packageName}@${requestedVersion}`));
48
219
  console.log();
49
- // No audit found — fallback to npm
220
+ // ─── No audit found ───────────────────────────────────────────────
50
221
  if (!audit) {
51
222
  console.log(chalk.gray(` NOT AUDITED — no NpmGuard record found for this version.`));
52
223
  console.log();
53
- console.log(chalk.gray(" Falling back to npm install..."));
54
- console.log();
55
- execSync(`npm install ${packageSpec}`, { stdio: "inherit" });
56
- return;
224
+ const privateKey = process.env.NPMGUARD_PRIVATE_KEY;
225
+ const contractDeployed = AUDIT_REQUEST_ADDRESS_0G !== ZERO_ADDRESS;
226
+ if (contractDeployed) {
227
+ const publicClient = createPublicClient({
228
+ chain: ogGalileo,
229
+ transport: http(OG_RPC),
230
+ });
231
+ // Check if user already paid (previous attempt where audit engine failed)
232
+ let alreadyPaid = false;
233
+ try {
234
+ alreadyPaid = (await publicClient.readContract({
235
+ address: AUDIT_REQUEST_ADDRESS_0G,
236
+ abi: AUDIT_REQUEST_ABI,
237
+ functionName: "isRequested",
238
+ args: [packageName, requestedVersion],
239
+ }));
240
+ }
241
+ catch {
242
+ // can't read — assume not paid
243
+ }
244
+ if (alreadyPaid) {
245
+ console.log(chalk.cyan(` Already paid on-chain — re-triggering audit...`));
246
+ console.log();
247
+ }
248
+ else {
249
+ // Read fee for display
250
+ let feeDisplay = "0.01 0G";
251
+ let feeWei = 10000000000000000n;
252
+ try {
253
+ feeWei = (await publicClient.readContract({
254
+ address: AUDIT_REQUEST_ADDRESS_0G,
255
+ abi: AUDIT_REQUEST_ABI,
256
+ functionName: "auditFee",
257
+ }));
258
+ feeDisplay = `${formatEther(feeWei)} 0G`;
259
+ }
260
+ catch { }
261
+ const wantAudit = await prompt(chalk.yellow(` Request on-chain audit for ${feeDisplay}? (y/n) `));
262
+ if (wantAudit !== "y" && wantAudit !== "yes") {
263
+ return askInstallWithoutAudit(packageSpec);
264
+ }
265
+ // Ask how to pay
266
+ console.log();
267
+ console.log(chalk.bold(` How to pay?`));
268
+ if (privateKey) {
269
+ console.log(` 1) Wallet (NPMGUARD_PRIVATE_KEY)`);
270
+ console.log(` 2) WalletConnect (mobile wallet)`);
271
+ console.log(` 3) Back`);
272
+ }
273
+ else {
274
+ console.log(` 1) WalletConnect (mobile wallet)`);
275
+ console.log(` 2) Back`);
276
+ }
277
+ console.log();
278
+ const choice = await prompt(` Choice: `);
279
+ const backChoice = privateKey ? "3" : "2";
280
+ if (choice === backChoice) {
281
+ return askInstallWithoutAudit(packageSpec);
282
+ }
283
+ if (privateKey && choice === "1") {
284
+ const txSpinner = ora(" Sending payment transaction...").start();
285
+ try {
286
+ const txHash = await requestAuditOnChain(packageName, requestedVersion, privateKey);
287
+ txSpinner.succeed("Payment confirmed on-chain!");
288
+ console.log(chalk.gray(` Tx: ${BLOCK_EXPLORER}/tx/${txHash}`));
289
+ console.log();
290
+ }
291
+ catch (err) {
292
+ txSpinner.fail("Transaction failed");
293
+ console.log(chalk.red(` ${err.shortMessage ?? err.message}`));
294
+ console.log();
295
+ return askInstallWithoutAudit(packageSpec);
296
+ }
297
+ }
298
+ else if ((privateKey && choice === "2") || (!privateKey && choice === "1")) {
299
+ const paid = await payViaWalletConnect(packageName, requestedVersion, feeWei, feeDisplay);
300
+ if (!paid)
301
+ return askInstallWithoutAudit(packageSpec);
302
+ }
303
+ else {
304
+ return askInstallWithoutAudit(packageSpec);
305
+ }
306
+ }
307
+ // Trigger audit engine
308
+ const auditApiUrl = process.env.NPMGUARD_AUDIT_API_URL ?? DEFAULT_AUDIT_API_URL;
309
+ const auditSpinner = ora(" Running security audit...").start();
310
+ try {
311
+ const resp = await fetch(auditApiUrl, {
312
+ method: "POST",
313
+ headers: { "Content-Type": "application/json" },
314
+ body: JSON.stringify({ packageName, version: requestedVersion }),
315
+ });
316
+ if (!resp.ok)
317
+ throw new Error(`Audit engine returned ${resp.status}`);
318
+ const result = await resp.json();
319
+ auditSpinner.stop();
320
+ console.log();
321
+ const verdict = (result.verdict ?? "UNKNOWN").toUpperCase();
322
+ const capabilities = result.capabilities ?? [];
323
+ if (verdict === "SAFE") {
324
+ console.log(chalk.green(` Verdict: SAFE`));
325
+ }
326
+ else if (verdict === "DANGEROUS") {
327
+ console.log(chalk.red(` Verdict: DANGEROUS`));
328
+ }
329
+ else {
330
+ console.log(chalk.yellow(` Verdict: ${verdict}`));
331
+ }
332
+ if (capabilities.length > 0) {
333
+ console.log(chalk.gray(` Capabilities: ${capabilities.join(", ")}`));
334
+ }
335
+ console.log();
336
+ if (verdict === "DANGEROUS" && !force) {
337
+ console.log(chalk.red.bold(" Installation blocked. This package is dangerous."));
338
+ console.log(chalk.gray(" Use --force to install anyway."));
339
+ console.log();
340
+ return;
341
+ }
342
+ console.log(chalk.gray(" Installing from npm..."));
343
+ console.log();
344
+ execSync(`npm install ${packageSpec}`, { stdio: "inherit" });
345
+ }
346
+ catch (err) {
347
+ auditSpinner.fail("Audit engine unreachable");
348
+ console.log(chalk.gray(` ${err.message ?? err}`));
349
+ console.log();
350
+ return askInstallWithoutAudit(packageSpec);
351
+ }
352
+ return;
353
+ }
354
+ // Contract not deployed
355
+ return askInstallWithoutAudit(packageSpec);
57
356
  }
58
- // Show verdict
357
+ // ─── Audit found — show verdict ───────────────────────────────────
59
358
  if (audit.verdict === "SAFE") {
60
359
  console.log(chalk.green(` SAFE (score: ${audit.score})`));
61
360
  }
@@ -81,41 +380,18 @@ export async function installCommand(packageSpec, auditSource, force = false) {
81
380
  }
82
381
  // Install from IPFS if sourceCid is available
83
382
  if (audit.sourceCid) {
84
- const dlSpinner = ora(`Downloading verified source from IPFS (${audit.sourceCid.slice(0, 12)}...)`).start();
383
+ const ipfsUrl = `${IPFS_GATEWAY}/${audit.sourceCid}`;
384
+ console.log(chalk.green(` Installing from verified IPFS source...`));
385
+ console.log();
85
386
  try {
86
- const tmpDir = join(tmpdir(), "npmguard");
87
- await mkdir(tmpDir, { recursive: true });
88
- const tarballPath = join(tmpDir, `${packageName}-${requestedVersion}.tgz`);
89
- await downloadFromIPFS(audit.sourceCid, tarballPath);
90
- dlSpinner.succeed("Downloaded from IPFS");
91
- console.log(chalk.green(` Installing from verified IPFS source...`));
92
- console.log();
93
- execSync(`npm install ${tarballPath}`, { stdio: "inherit" });
94
- // Fix package.json — replace the file: path with the real version
95
- try {
96
- const pkgPath = join(process.cwd(), "package.json");
97
- const pkgRaw = await readFile(pkgPath, "utf-8");
98
- const pkg = JSON.parse(pkgRaw);
99
- if (pkg.dependencies?.[packageName]?.startsWith("file:")) {
100
- pkg.dependencies[packageName] = `^${requestedVersion}`;
101
- await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
102
- }
103
- }
104
- catch {
105
- // Not critical if this fails
106
- }
107
- // Cleanup tarball
108
- await unlink(tarballPath).catch(() => { });
387
+ execSync(`npm install ${ipfsUrl}`, { stdio: "inherit" });
109
388
  }
110
- catch (err) {
111
- dlSpinner.fail("IPFS download failed");
112
- console.log(chalk.yellow(" Falling back to npm install..."));
113
- console.log();
389
+ catch {
390
+ console.log(chalk.yellow(" IPFS install failed, falling back to npm..."));
114
391
  execSync(`npm install ${packageSpec}`, { stdio: "inherit" });
115
392
  }
116
393
  }
117
394
  else {
118
- // No sourceCid — install from npm
119
395
  console.log(chalk.gray(" No IPFS source available, installing from npm..."));
120
396
  console.log();
121
397
  execSync(`npm install ${packageSpec}`, { stdio: "inherit" });
@@ -0,0 +1,121 @@
1
+ export declare const AUDIT_REQUEST_ADDRESS: `0x${string}`;
2
+ export declare const AUDIT_REQUEST_ADDRESS_0G: `0x${string}`;
3
+ export declare const AUDIT_REQUEST_ABI: readonly [{
4
+ readonly inputs: readonly [{
5
+ readonly name: "_auditFee";
6
+ readonly type: "uint256";
7
+ }];
8
+ readonly stateMutability: "nonpayable";
9
+ readonly type: "constructor";
10
+ }, {
11
+ readonly anonymous: false;
12
+ readonly inputs: readonly [{
13
+ readonly indexed: false;
14
+ readonly name: "packageName";
15
+ readonly type: "string";
16
+ }, {
17
+ readonly indexed: false;
18
+ readonly name: "version";
19
+ readonly type: "string";
20
+ }, {
21
+ readonly indexed: true;
22
+ readonly name: "requester";
23
+ readonly type: "address";
24
+ }];
25
+ readonly name: "AuditRequested";
26
+ readonly type: "event";
27
+ }, {
28
+ readonly anonymous: false;
29
+ readonly inputs: readonly [{
30
+ readonly indexed: true;
31
+ readonly name: "key";
32
+ readonly type: "bytes32";
33
+ }, {
34
+ readonly indexed: true;
35
+ readonly name: "requester";
36
+ readonly type: "address";
37
+ }];
38
+ readonly name: "AuditRequestedByKey";
39
+ readonly type: "event";
40
+ }, {
41
+ readonly inputs: readonly [{
42
+ readonly name: "packageName";
43
+ readonly type: "string";
44
+ }, {
45
+ readonly name: "version";
46
+ readonly type: "string";
47
+ }];
48
+ readonly name: "requestAudit";
49
+ readonly outputs: readonly [];
50
+ readonly stateMutability: "payable";
51
+ readonly type: "function";
52
+ }, {
53
+ readonly inputs: readonly [{
54
+ readonly name: "key";
55
+ readonly type: "bytes32";
56
+ }];
57
+ readonly name: "requestAuditByKey";
58
+ readonly outputs: readonly [];
59
+ readonly stateMutability: "payable";
60
+ readonly type: "function";
61
+ }, {
62
+ readonly inputs: readonly [];
63
+ readonly name: "auditFee";
64
+ readonly outputs: readonly [{
65
+ readonly name: "";
66
+ readonly type: "uint256";
67
+ }];
68
+ readonly stateMutability: "view";
69
+ readonly type: "function";
70
+ }, {
71
+ readonly inputs: readonly [];
72
+ readonly name: "owner";
73
+ readonly outputs: readonly [{
74
+ readonly name: "";
75
+ readonly type: "address";
76
+ }];
77
+ readonly stateMutability: "view";
78
+ readonly type: "function";
79
+ }, {
80
+ readonly inputs: readonly [{
81
+ readonly name: "packageName";
82
+ readonly type: "string";
83
+ }, {
84
+ readonly name: "version";
85
+ readonly type: "string";
86
+ }];
87
+ readonly name: "isRequested";
88
+ readonly outputs: readonly [{
89
+ readonly name: "";
90
+ readonly type: "bool";
91
+ }];
92
+ readonly stateMutability: "view";
93
+ readonly type: "function";
94
+ }, {
95
+ readonly inputs: readonly [{
96
+ readonly name: "";
97
+ readonly type: "bytes32";
98
+ }];
99
+ readonly name: "requested";
100
+ readonly outputs: readonly [{
101
+ readonly name: "";
102
+ readonly type: "bool";
103
+ }];
104
+ readonly stateMutability: "view";
105
+ readonly type: "function";
106
+ }, {
107
+ readonly inputs: readonly [{
108
+ readonly name: "_fee";
109
+ readonly type: "uint256";
110
+ }];
111
+ readonly name: "setFee";
112
+ readonly outputs: readonly [];
113
+ readonly stateMutability: "nonpayable";
114
+ readonly type: "function";
115
+ }, {
116
+ readonly inputs: readonly [];
117
+ readonly name: "withdraw";
118
+ readonly outputs: readonly [];
119
+ readonly stateMutability: "nonpayable";
120
+ readonly type: "function";
121
+ }];
@@ -0,0 +1,92 @@
1
+ // NpmGuardAuditRequest contract — deployed on Sepolia + 0G Galileo Testnet
2
+ // Update these addresses after running: cd contracts && npm run deploy
3
+ export const AUDIT_REQUEST_ADDRESS = "0x4bbaf196bde9e02594631e03c28ebe16719214f3"; // Sepolia
4
+ export const AUDIT_REQUEST_ADDRESS_0G = "0x1201448ae5f00e1783036439569e71ab3757d0de"; // 0G Galileo Testnet
5
+ export const AUDIT_REQUEST_ABI = [
6
+ {
7
+ inputs: [{ name: "_auditFee", type: "uint256" }],
8
+ stateMutability: "nonpayable",
9
+ type: "constructor",
10
+ },
11
+ {
12
+ anonymous: false,
13
+ inputs: [
14
+ { indexed: false, name: "packageName", type: "string" },
15
+ { indexed: false, name: "version", type: "string" },
16
+ { indexed: true, name: "requester", type: "address" },
17
+ ],
18
+ name: "AuditRequested",
19
+ type: "event",
20
+ },
21
+ {
22
+ anonymous: false,
23
+ inputs: [
24
+ { indexed: true, name: "key", type: "bytes32" },
25
+ { indexed: true, name: "requester", type: "address" },
26
+ ],
27
+ name: "AuditRequestedByKey",
28
+ type: "event",
29
+ },
30
+ {
31
+ inputs: [
32
+ { name: "packageName", type: "string" },
33
+ { name: "version", type: "string" },
34
+ ],
35
+ name: "requestAudit",
36
+ outputs: [],
37
+ stateMutability: "payable",
38
+ type: "function",
39
+ },
40
+ {
41
+ inputs: [{ name: "key", type: "bytes32" }],
42
+ name: "requestAuditByKey",
43
+ outputs: [],
44
+ stateMutability: "payable",
45
+ type: "function",
46
+ },
47
+ {
48
+ inputs: [],
49
+ name: "auditFee",
50
+ outputs: [{ name: "", type: "uint256" }],
51
+ stateMutability: "view",
52
+ type: "function",
53
+ },
54
+ {
55
+ inputs: [],
56
+ name: "owner",
57
+ outputs: [{ name: "", type: "address" }],
58
+ stateMutability: "view",
59
+ type: "function",
60
+ },
61
+ {
62
+ inputs: [
63
+ { name: "packageName", type: "string" },
64
+ { name: "version", type: "string" },
65
+ ],
66
+ name: "isRequested",
67
+ outputs: [{ name: "", type: "bool" }],
68
+ stateMutability: "view",
69
+ type: "function",
70
+ },
71
+ {
72
+ inputs: [{ name: "", type: "bytes32" }],
73
+ name: "requested",
74
+ outputs: [{ name: "", type: "bool" }],
75
+ stateMutability: "view",
76
+ type: "function",
77
+ },
78
+ {
79
+ inputs: [{ name: "_fee", type: "uint256" }],
80
+ name: "setFee",
81
+ outputs: [],
82
+ stateMutability: "nonpayable",
83
+ type: "function",
84
+ },
85
+ {
86
+ inputs: [],
87
+ name: "withdraw",
88
+ outputs: [],
89
+ stateMutability: "nonpayable",
90
+ type: "function",
91
+ },
92
+ ];
@@ -1,30 +1,51 @@
1
1
  import { createPublicClient, http } from "viem";
2
2
  import { sepolia } from "viem/chains";
3
- const client = createPublicClient({
4
- chain: sepolia,
5
- transport: http("https://ethereum-sepolia-rpc.publicnode.com"),
6
- });
3
+ const RPC_URLS = process.env.SEPOLIA_RPC_URL
4
+ ? [process.env.SEPOLIA_RPC_URL]
5
+ : [
6
+ "https://ethereum-sepolia-rpc.publicnode.com",
7
+ "https://rpc.sepolia.org",
8
+ "https://sepolia.drpc.org",
9
+ ];
10
+ function makeClient(url) {
11
+ return createPublicClient({ chain: sepolia, transport: http(url) });
12
+ }
13
+ async function getText(ensName, key) {
14
+ for (const url of RPC_URLS) {
15
+ try {
16
+ return await makeClient(url).getEnsText({ name: ensName, key });
17
+ }
18
+ catch {
19
+ continue;
20
+ }
21
+ }
22
+ return null;
23
+ }
7
24
  export class ENSAuditSource {
8
25
  async getAudit(packageName, version) {
9
- // 1.14.0 1-14-0
10
- const versionSlug = version.replace(/\./g, "-");
26
+ const versionSlug = version
27
+ .replace(/[^a-z0-9]+/gi, "-")
28
+ .replace(/^-+|-+$/g, "")
29
+ .toLowerCase();
11
30
  const ensName = `${versionSlug}.${packageName}.npmguard.eth`;
12
31
  try {
13
32
  const [verdict, score, capabilities, reportCid, sourceCid] = await Promise.all([
14
- client.getEnsText({ name: ensName, key: "verdict" }),
15
- client.getEnsText({ name: ensName, key: "score" }),
16
- client.getEnsText({ name: ensName, key: "capabilities" }),
17
- client.getEnsText({ name: ensName, key: "reportCid" }),
18
- client.getEnsText({ name: ensName, key: "sourceCid" }),
33
+ getText(ensName, "npmguard.verdict"),
34
+ getText(ensName, "npmguard.score"),
35
+ getText(ensName, "npmguard.capabilities"),
36
+ getText(ensName, "npmguard.report_cid"),
37
+ getText(ensName, "npmguard.source_cid"),
19
38
  ]);
20
39
  if (!verdict)
21
40
  return null;
22
41
  return {
23
42
  packageName,
24
43
  version,
25
- verdict: verdict,
44
+ verdict: verdict.toUpperCase(),
26
45
  score: score ? parseInt(score, 10) : 0,
27
- capabilities: capabilities ? capabilities.split(",") : [],
46
+ capabilities: capabilities
47
+ ? capabilities.split(",").map((c) => c.trim()).filter(Boolean)
48
+ : [],
28
49
  reportCid: reportCid ?? undefined,
29
50
  sourceCid: sourceCid ?? undefined,
30
51
  };
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- export {};
2
+ import "dotenv/config";
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import "dotenv/config";
2
3
  import { Command } from "commander";
3
4
  import { resolve } from "node:path";
4
5
  import { ENSAuditSource } from "./ens-source.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "npmguard-cli",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "Check npm packages against NpmGuard security audits on ENS before installing",
6
6
  "bin": "./dist/index.js",
@@ -22,10 +22,13 @@
22
22
  ],
23
23
  "license": "MIT",
24
24
  "dependencies": {
25
+ "@walletconnect/sign-client": "^2.23.9",
25
26
  "chalk": "^5.4.0",
26
27
  "cli-table3": "^0.6.5",
27
28
  "commander": "^13.1.0",
29
+ "dotenv": "^17.4.0",
28
30
  "ora": "^8.2.0",
31
+ "qrcode-terminal": "^0.12.0",
29
32
  "viem": "^2.34.0"
30
33
  },
31
34
  "devDependencies": {