npmguard-cli 1.1.1 → 1.1.3

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
@@ -1,9 +1,197 @@
1
- # NpmGuard CLI
1
+ # npmguard-cli
2
2
 
3
- > **TODO** CLI will talk to the NpmGuard API server (not ENS/IPFS). Not yet implemented.
3
+ Security-gated npm install. Runs every package through NpmGuard's audit
4
+ engine before it touches your `node_modules`.
4
5
 
5
- Planned features:
6
+ ```bash
7
+ npx npmguard-cli install express
8
+ ```
6
9
 
7
- - `npmguard check <package>` — run a security audit via the API
8
- - `npmguard install <package>` audit-then-install workflow
9
- - Machine-readable JSON output for CI/CD integration
10
+ - **SAFE** installs immediately
11
+ - **DANGEROUS** warns, shows findings, asks before installing
12
+ - **No audit yet** → offers to pay for one (Stripe or crypto), then streams
13
+ the results in real time
14
+
15
+ ## Install
16
+
17
+ No install required — `npx` pulls the latest from npm:
18
+
19
+ ```bash
20
+ npx npmguard-cli@latest install <pkg>
21
+ ```
22
+
23
+ Or install globally:
24
+
25
+ ```bash
26
+ npm install -g npmguard-cli
27
+ npmguard install <pkg>
28
+ ```
29
+
30
+ ## Commands
31
+
32
+ ### `npmguard install <package>[@version]`
33
+
34
+ The main command. Runs the full gate-then-install flow.
35
+
36
+ ```bash
37
+ npmguard install express
38
+ npmguard install lodash@4.17.21
39
+ npmguard install @types/node@22
40
+
41
+ # Force install even if the package is flagged DANGEROUS
42
+ npmguard install left-pad --force
43
+ ```
44
+
45
+ The command auto-detects your package manager (`npm` / `pnpm` / `yarn`) from
46
+ lockfiles and runs the correct add command (`npm install`, `pnpm add`,
47
+ `yarn add`).
48
+
49
+ **Flow**:
50
+
51
+ 1. Resolves the version (`latest` if omitted)
52
+ 2. Asks the engine if the package has an existing audit
53
+ 3. **Found + SAFE** → runs `<pm> add <pkg>` directly
54
+ 4. **Found + DANGEROUS** → shows findings + capabilities, prompts `y/N`
55
+ (bypass with `--force`)
56
+ 5. **Not found** → asks how you want to pay for the audit:
57
+ - Stripe (credit card) — browser checkout via QR
58
+ - Browser wallet — MetaMask/Rabby signs in Brave or Chrome
59
+ - WalletConnect — mobile wallet signs a tx on Base Sepolia
60
+ - Install without audit (yolo)
61
+ - Cancel
62
+ 6. Streams audit events live for Stripe/WalletConnect, or waits for the
63
+ report when the browser-wallet page owns the live view
64
+ 7. Runs the install if the verdict is SAFE, or prompts otherwise
65
+
66
+ ### `npmguard audit <package>[@version]`
67
+
68
+ Run a standalone audit without installing. Returns the verdict and exits.
69
+
70
+ ```bash
71
+ npmguard audit is-number
72
+ npmguard audit express@5.2.1
73
+ ```
74
+
75
+ If the package hasn't been audited yet, the standalone audit command starts
76
+ the Stripe checkout flow. Use `install` for the interactive browser-wallet
77
+ and WalletConnect choices.
78
+
79
+ ### `npmguard check [--path <dir>]`
80
+
81
+ Walk `package.json` in the given directory and check every dependency
82
+ against NpmGuard's audit database. Useful for auditing an existing project.
83
+
84
+ ```bash
85
+ cd my-project
86
+ npmguard check
87
+ # or
88
+ npmguard check --path /path/to/other-project
89
+ ```
90
+
91
+ ## Payment options
92
+
93
+ When a package hasn't been audited yet, an audit run costs real compute
94
+ (LLM calls, sandbox execution). Three ways to pay:
95
+
96
+ ### Stripe (fiat)
97
+
98
+ Opens a Stripe checkout page in the browser. After payment, the engine
99
+ triggers the audit automatically. Works from any machine, no wallet
100
+ required.
101
+
102
+ ### Browser wallet (crypto)
103
+
104
+ The CLI prints a `https://npmguard.com/pay?...` URL and can open it in your
105
+ default browser. Open that page in Brave or Chrome with MetaMask/Rabby
106
+ enabled, connect the wallet, and confirm the Base Sepolia transaction. The
107
+ browser starts the server-side audit with the tx hash, while the CLI waits
108
+ for the persisted report before deciding whether to install.
109
+
110
+ This is the desktop-friendly flow for extension wallets.
111
+
112
+ ### WalletConnect (crypto)
113
+
114
+ The CLI generates a WalletConnect v2 QR code in the terminal. Scan it with
115
+ any mobile wallet (MetaMask, Rainbow, Coinbase Wallet, etc.) and confirm
116
+ the transaction. It also prints the raw WalletConnect URI for desktop
117
+ wallets that support a "connect by URI" flow.
118
+
119
+ Browser extension wallets do not automatically connect to a terminal
120
+ process. If your wallet only works as a Brave/Chrome extension, use the
121
+ browser wallet option instead.
122
+
123
+ - **Chain**: Base Sepolia (testnet — free ETH from
124
+ [Alchemy faucet](https://www.alchemy.com/faucets/base-sepolia))
125
+ - **Fee**: `0.0001 ETH` per audit
126
+ - **Contract**: [`0xBF562626e4Afb883423Ec719e0270DB232bcB9eD`](https://sepolia.basescan.org/address/0xbf562626e4afb883423ec719e0270db232bcb9ed)
127
+
128
+ Flow:
129
+
130
+ 1. CLI asks the engine for public crypto config (contract + fee), with a direct contract-read fallback
131
+ 2. You approve the tx in your wallet
132
+ 3. Engine verifies the receipt on Base Sepolia via Alchemy
133
+ 4. Audit starts, CLI streams events
134
+
135
+ The on-chain event `AuditRequested(packageName, version, requester, feePaid)`
136
+ acts as the payment proof. The engine decodes it and matches the args
137
+ against your request before launching the audit.
138
+
139
+ ## Configuration
140
+
141
+ The CLI talks to `https://npmguard.com` by default. You can override the
142
+ API URL and the web app URL for local development:
143
+
144
+ ```bash
145
+ # via flag
146
+ npmguard --api http://localhost:8000 --web http://localhost:3000 install lodash
147
+
148
+ # via env
149
+ export NPMGUARD_API_URL=http://localhost:8000
150
+ export NPMGUARD_WEB_URL=http://localhost:3000
151
+ npmguard install lodash
152
+ ```
153
+
154
+ No private key or paid RPC config is required from the user. For the
155
+ WalletConnect path, the CLI uses `viem` with a public Base Sepolia RPC to
156
+ read/confirm the transaction for local UX; your wallet broadcasts the tx,
157
+ and the engine re-verifies the receipt with its own RPC before starting the
158
+ audit.
159
+
160
+ ## Exit codes
161
+
162
+ | Code | Meaning |
163
+ |---|---|
164
+ | 0 | Audit passed, package installed |
165
+ | 1 | Audit failed / install aborted / network error |
166
+
167
+ ## Dependencies
168
+
169
+ Intentionally minimal:
170
+
171
+ - `commander` — CLI arg parsing
172
+ - `chalk`, `ora`, `qrcode-terminal` — terminal UI
173
+ - `eventsource` — SSE client for audit events
174
+ - `viem` — read contract, encode calldata, wait for receipt
175
+ - `@walletconnect/sign-client` — WalletConnect v2 session
176
+
177
+ No private key handling in the CLI. The wallet signs and broadcasts; the
178
+ CLI only observes.
179
+
180
+ ## Development
181
+
182
+ ```bash
183
+ cd cli
184
+ npm install
185
+ npm run build
186
+
187
+ # Test against local engine
188
+ node dist/index.js --api http://localhost:8000 install is-number
189
+ ```
190
+
191
+ ## Release
192
+
193
+ ```bash
194
+ npm version patch # or minor / major
195
+ npm run build
196
+ npm publish --access public
197
+ ```
package/dist/api.js CHANGED
@@ -10,6 +10,9 @@ async function request(url, options) {
10
10
  }
11
11
  return res.json();
12
12
  }
13
+ export async function getPublicConfig(apiUrl) {
14
+ return request(`${apiUrl}/config/public`);
15
+ }
13
16
  export async function checkout(apiUrl, packageName, version) {
14
17
  const body = { packageName };
15
18
  if (version)
@@ -3,7 +3,7 @@ import ora from "ora";
3
3
  import { spawnSync } from "node:child_process";
4
4
  import { formatEther } from "viem";
5
5
  import * as api from "../api.js";
6
- import { parsePackageArg, prompt, resolveLatestVersion, detectPackageManager, } from "../utils.js";
6
+ import { parsePackageArg, prompt, resolveLatestVersion, detectPackageManager, openExternalUrl, } from "../utils.js";
7
7
  import { auditCommand } from "./audit.js";
8
8
  import { payViaWalletConnect, readAuditFee } from "../wallet/walletconnect.js";
9
9
  import { streamAuditEvents } from "../stream.js";
@@ -30,6 +30,7 @@ function extractCapabilities(report) {
30
30
  }
31
31
  export async function installCommand(packageSpec, opts) {
32
32
  const apiUrl = opts.api;
33
+ const webUrl = opts.web;
33
34
  let parsed;
34
35
  try {
35
36
  parsed = parsePackageArg(packageSpec);
@@ -73,20 +74,25 @@ export async function installCommand(packageSpec, opts) {
73
74
  console.log();
74
75
  console.log(chalk.bold(" How do you want to pay for the audit?"));
75
76
  console.log(" 1) Stripe (credit card)");
76
- console.log(" 2) WalletConnectETH on Base Sepolia");
77
- console.log(" 3) Install without audit (at your own risk)");
78
- console.log(" 4) Cancel");
77
+ console.log(" 2) Browser wallet MetaMask/Rabby in Brave or Chrome");
78
+ console.log(" 3) WalletConnect QR/mobile wallet");
79
+ console.log(" 4) Install without audit (at your own risk)");
80
+ console.log(" 5) Cancel");
79
81
  console.log();
80
- const choice = await prompt(" Choice [1/2/3/4]: ");
82
+ const choice = await prompt(" Choice [1/2/3/4/5]: ");
81
83
  if (choice === "1") {
82
84
  await runStripeAuditAndInstall(fullSpec, name, version, apiUrl);
83
85
  return;
84
86
  }
85
87
  if (choice === "2") {
86
- await runCryptoAuditAndInstall(fullSpec, name, version, apiUrl);
88
+ await runBrowserWalletAuditAndInstall(fullSpec, name, version, apiUrl, webUrl);
87
89
  return;
88
90
  }
89
91
  if (choice === "3") {
92
+ await runCryptoAuditAndInstall(fullSpec, name, version, apiUrl);
93
+ return;
94
+ }
95
+ if (choice === "4") {
90
96
  console.log(chalk.yellow(" Installing without audit. Proceed at your own risk."));
91
97
  process.exit(runInstall(fullSpec));
92
98
  }
@@ -139,16 +145,86 @@ async function runStripeAuditAndInstall(fullSpec, name, version, apiUrl) {
139
145
  }
140
146
  await finalizeAfterAudit(fullSpec, name, version, apiUrl);
141
147
  }
148
+ function buildBrowserWalletUrl(webUrl, packageName, version) {
149
+ const url = new URL("/pay", webUrl.replace(/\/+$/, ""));
150
+ url.searchParams.set("packageName", packageName);
151
+ url.searchParams.set("version", version);
152
+ url.searchParams.set("source", "cli");
153
+ return url.toString();
154
+ }
155
+ function sleep(ms) {
156
+ return new Promise((resolve) => setTimeout(resolve, ms));
157
+ }
158
+ async function waitForPackageReport(apiUrl, packageName, version, timeoutMs) {
159
+ const deadline = Date.now() + timeoutMs;
160
+ while (Date.now() < deadline) {
161
+ const report = await api.getPackageReport(apiUrl, packageName, version);
162
+ if (report)
163
+ return report;
164
+ await sleep(3000);
165
+ }
166
+ return null;
167
+ }
168
+ async function runBrowserWalletAuditAndInstall(fullSpec, name, version, apiUrl, webUrl) {
169
+ const paymentUrl = buildBrowserWalletUrl(webUrl, name, version);
170
+ console.log();
171
+ console.log(chalk.bold(" Open this URL in Brave or Chrome with MetaMask/Rabby:"));
172
+ console.log(chalk.blue.underline(` ${paymentUrl}`));
173
+ console.log();
174
+ console.log(chalk.gray(" The browser page will connect your wallet, sign the Base Sepolia tx, and start the audit."));
175
+ console.log(chalk.gray(" Keep this terminal open; it will continue when the report is ready."));
176
+ console.log();
177
+ const openAnswer = await prompt(" Open it now in your default browser? (Y/n) ");
178
+ if (openAnswer !== "n" && openAnswer !== "no") {
179
+ const opened = openExternalUrl(paymentUrl);
180
+ if (!opened) {
181
+ console.log(chalk.yellow(" Could not open automatically. Copy the URL above."));
182
+ }
183
+ }
184
+ const spinner = ora(" Waiting for browser payment and audit report...").start();
185
+ let report = null;
186
+ try {
187
+ report = await waitForPackageReport(apiUrl, name, version, 30 * 60 * 1000);
188
+ }
189
+ catch (err) {
190
+ spinner.fail("Could not read report: " +
191
+ (err instanceof Error ? err.message : String(err)));
192
+ process.exit(1);
193
+ }
194
+ if (!report) {
195
+ spinner.fail("Timed out waiting for the browser audit report.");
196
+ console.log(chalk.gray(` Check ${webUrl.replace(/\/+$/, "")}/package/${encodeURIComponent(name)}`));
197
+ process.exit(1);
198
+ }
199
+ spinner.succeed("Audit report received");
200
+ await finalizeWithReport(fullSpec, report);
201
+ }
142
202
  async function runCryptoAuditAndInstall(fullSpec, name, version, apiUrl) {
143
- // 1. Read current fee from contract
203
+ // 1. Read current fee from the engine's public config, then fall back to
204
+ // direct contract read if the engine is older or config is unavailable.
144
205
  let feeWei;
206
+ let contractAddress;
145
207
  try {
146
- feeWei = await readAuditFee();
208
+ const publicConfig = await api.getPublicConfig(apiUrl);
209
+ const contract = publicConfig.crypto?.contract;
210
+ const auditFeeWei = publicConfig.crypto?.auditFeeWei;
211
+ if (contract && /^0x[0-9a-fA-F]{40}$/.test(contract) && auditFeeWei) {
212
+ contractAddress = contract;
213
+ feeWei = BigInt(auditFeeWei);
214
+ }
215
+ else {
216
+ feeWei = await readAuditFee();
217
+ }
147
218
  }
148
219
  catch (err) {
149
- console.error(chalk.red("Could not read fee from contract: " +
150
- (err instanceof Error ? err.message : String(err))));
151
- process.exit(1);
220
+ try {
221
+ feeWei = await readAuditFee();
222
+ }
223
+ catch {
224
+ console.error(chalk.red("Could not read fee from engine or contract: " +
225
+ (err instanceof Error ? err.message : String(err))));
226
+ process.exit(1);
227
+ }
152
228
  }
153
229
  const feeDisplay = `${formatEther(feeWei)} ETH`;
154
230
  const confirm = await prompt(chalk.yellow(` Pay ${feeDisplay} on Base Sepolia? (y/N) `));
@@ -157,7 +233,7 @@ async function runCryptoAuditAndInstall(fullSpec, name, version, apiUrl) {
157
233
  process.exit(0);
158
234
  }
159
235
  // 2. WalletConnect → user signs → we get txHash
160
- const result = await payViaWalletConnect(name, version, feeWei, feeDisplay);
236
+ const result = await payViaWalletConnect(name, version, feeWei, feeDisplay, contractAddress);
161
237
  if (!result.paid || !result.txHash) {
162
238
  console.log(chalk.red(" Payment failed, aborting."));
163
239
  process.exit(1);
@@ -189,7 +265,10 @@ async function finalizeAfterAudit(fullSpec, name, version, apiUrl) {
189
265
  console.log(chalk.red(" Audit finished but report not found."));
190
266
  process.exit(1);
191
267
  }
192
- const verdict = extractVerdict(freshReport);
268
+ await finalizeWithReport(fullSpec, freshReport);
269
+ }
270
+ async function finalizeWithReport(fullSpec, report) {
271
+ const verdict = extractVerdict(report);
193
272
  if (verdict === "SAFE") {
194
273
  console.log(chalk.green("\n ✓ SAFE — proceeding with install"));
195
274
  process.exit(runInstall(fullSpec));
package/dist/index.js CHANGED
@@ -1,14 +1,25 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
+ import { readFileSync } from "node:fs";
3
4
  import { auditCommand } from "./commands/audit.js";
4
5
  import { checkCommand } from "./commands/check.js";
5
6
  import { installCommand } from "./commands/install.js";
7
+ function readPackageVersion() {
8
+ try {
9
+ const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
10
+ return typeof pkg.version === "string" ? pkg.version : "0.0.0";
11
+ }
12
+ catch {
13
+ return "0.0.0";
14
+ }
15
+ }
6
16
  const program = new Command();
7
17
  program
8
18
  .name("npmguard")
9
19
  .description("NpmGuard CLI — audit npm packages for security issues")
10
- .version("1.0.0")
11
- .option("--api <url>", "NpmGuard engine API URL", process.env.NPMGUARD_API_URL ?? "https://npmguard.com");
20
+ .version(readPackageVersion())
21
+ .option("--api <url>", "NpmGuard engine API URL", process.env.NPMGUARD_API_URL ?? "https://npmguard.com")
22
+ .option("--web <url>", "NpmGuard web app URL for browser wallet payments", process.env.NPMGUARD_WEB_URL ?? "https://npmguard.com");
12
23
  program
13
24
  .command("audit")
14
25
  .description("Pay for and run a security audit on an npm package")
@@ -23,8 +34,8 @@ program
23
34
  .argument("<package>", "Package name, optionally with version (e.g. express@4.18.0)")
24
35
  .option("-f, --force", "Install even if the package is flagged as dangerous")
25
36
  .action(async (pkg, cmdOpts) => {
26
- const apiUrl = program.opts().api;
27
- await installCommand(pkg, { api: apiUrl, force: cmdOpts.force });
37
+ const opts = program.opts();
38
+ await installCommand(pkg, { api: opts.api, web: opts.web, force: cmdOpts.force });
28
39
  });
29
40
  program
30
41
  .command("check")
package/dist/utils.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createInterface } from "node:readline";
2
2
  import { existsSync } from "node:fs";
3
3
  import { join } from "node:path";
4
+ import { spawn } from "node:child_process";
4
5
  export function parsePackageArg(pkg) {
5
6
  if (pkg.startsWith("@")) {
6
7
  const slashIndex = pkg.indexOf("/");
@@ -54,3 +55,27 @@ export function detectPackageManager(cwd = process.cwd()) {
54
55
  return "yarn";
55
56
  return "npm";
56
57
  }
58
+ export function openExternalUrl(url) {
59
+ try {
60
+ const platform = process.platform;
61
+ const command = platform === "darwin"
62
+ ? "open"
63
+ : platform === "win32"
64
+ ? "cmd"
65
+ : "xdg-open";
66
+ const args = platform === "win32"
67
+ ? ["/c", "start", "", url]
68
+ : [url];
69
+ const child = spawn(command, args, {
70
+ detached: true,
71
+ stdio: "ignore",
72
+ windowsHide: true,
73
+ });
74
+ child.on("error", () => { });
75
+ child.unref();
76
+ return true;
77
+ }
78
+ catch {
79
+ return false;
80
+ }
81
+ }
@@ -14,7 +14,7 @@ function generateQrCode(text) {
14
14
  });
15
15
  });
16
16
  }
17
- export async function payViaWalletConnect(packageName, version, feeWei, feeDisplay) {
17
+ export async function payViaWalletConnect(packageName, version, feeWei, feeDisplay, contractAddress = AUDIT_REQUEST_ADDRESS_BASE_SEPOLIA) {
18
18
  const calldata = encodeFunctionData({
19
19
  abi: AUDIT_REQUEST_ABI,
20
20
  functionName: "requestAudit",
@@ -63,6 +63,10 @@ export async function payViaWalletConnect(packageName, version, feeWei, feeDispl
63
63
  console.log();
64
64
  await generateQrCode(uri);
65
65
  console.log();
66
+ console.log(chalk.cyan(" Desktop wallet? Paste this WalletConnect URI into your wallet:"));
67
+ console.log(chalk.gray(` ${uri}`));
68
+ console.log(chalk.gray(" Keep it private; it is only for this pairing session."));
69
+ console.log();
66
70
  const pairSpinner = ora(" Waiting for wallet connection...").start();
67
71
  const session = await approval();
68
72
  const accounts = session.namespaces.eip155?.accounts ?? [];
@@ -84,7 +88,7 @@ export async function payViaWalletConnect(packageName, version, feeWei, feeDispl
84
88
  params: [
85
89
  {
86
90
  from: sender,
87
- to: AUDIT_REQUEST_ADDRESS_BASE_SEPOLIA,
91
+ to: contractAddress,
88
92
  data: calldata,
89
93
  value: "0x" + feeWei.toString(16),
90
94
  },
@@ -120,13 +124,13 @@ export async function payViaWalletConnect(packageName, version, feeWei, feeDispl
120
124
  setTimeout(() => process.off("uncaughtException", wcErrorHandler), 5000);
121
125
  }
122
126
  }
123
- export async function readAuditFee() {
127
+ export async function readAuditFee(contractAddress = AUDIT_REQUEST_ADDRESS_BASE_SEPOLIA) {
124
128
  const publicClient = createPublicClient({
125
129
  chain: baseSepolia,
126
130
  transport: http(),
127
131
  });
128
132
  return (await publicClient.readContract({
129
- address: AUDIT_REQUEST_ADDRESS_BASE_SEPOLIA,
133
+ address: contractAddress,
130
134
  abi: AUDIT_REQUEST_ABI,
131
135
  functionName: "auditFee",
132
136
  }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "npmguard-cli",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "type": "module",
5
5
  "description": "NpmGuard CLI — check npm packages against NpmGuard security audits",
6
6
  "main": "dist/index.js",
@@ -8,7 +8,7 @@
8
8
  "dist"
9
9
  ],
10
10
  "bin": {
11
- "npmguard": "./dist/index.js"
11
+ "npmguard": "dist/index.js"
12
12
  },
13
13
  "scripts": {
14
14
  "build": "tsc"
@@ -27,7 +27,7 @@
27
27
  "eventsource": "^2.0.2",
28
28
  "ora": "^8.1.0",
29
29
  "qrcode-terminal": "^0.12.0",
30
- "viem": "^2.47.17"
30
+ "viem": "^2.52.2"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "^22.0.0",