npmguard-cli 1.1.2 → 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
@@ -55,10 +55,12 @@ lockfiles and runs the correct add command (`npm install`, `pnpm add`,
55
55
  (bypass with `--force`)
56
56
  5. **Not found** → asks how you want to pay for the audit:
57
57
  - Stripe (credit card) — browser checkout via QR
58
- - WalletConnectmobile wallet signs a tx on Base Sepolia (~$0.30)
58
+ - Browser wallet MetaMask/Rabby signs in Brave or Chrome
59
+ - WalletConnect — mobile wallet signs a tx on Base Sepolia
59
60
  - Install without audit (yolo)
60
61
  - Cancel
61
- 6. Streams audit events live (phases, findings, verdict)
62
+ 6. Streams audit events live for Stripe/WalletConnect, or waits for the
63
+ report when the browser-wallet page owns the live view
62
64
  7. Runs the install if the verdict is SAFE, or prompts otherwise
63
65
 
64
66
  ### `npmguard audit <package>[@version]`
@@ -70,7 +72,9 @@ npmguard audit is-number
70
72
  npmguard audit express@5.2.1
71
73
  ```
72
74
 
73
- Same payment flow as `install` if the package hasn't been audited yet.
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.
74
78
 
75
79
  ### `npmguard check [--path <dir>]`
76
80
 
@@ -87,7 +91,7 @@ npmguard check --path /path/to/other-project
87
91
  ## Payment options
88
92
 
89
93
  When a package hasn't been audited yet, an audit run costs real compute
90
- (LLM calls, sandbox execution). Two ways to pay:
94
+ (LLM calls, sandbox execution). Three ways to pay:
91
95
 
92
96
  ### Stripe (fiat)
93
97
 
@@ -95,11 +99,26 @@ Opens a Stripe checkout page in the browser. After payment, the engine
95
99
  triggers the audit automatically. Works from any machine, no wallet
96
100
  required.
97
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
+
98
112
  ### WalletConnect (crypto)
99
113
 
100
114
  The CLI generates a WalletConnect v2 QR code in the terminal. Scan it with
101
115
  any mobile wallet (MetaMask, Rainbow, Coinbase Wallet, etc.) and confirm
102
- the transaction.
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.
103
122
 
104
123
  - **Chain**: Base Sepolia (testnet — free ETH from
105
124
  [Alchemy faucet](https://www.alchemy.com/faucets/base-sepolia))
@@ -108,7 +127,7 @@ the transaction.
108
127
 
109
128
  Flow:
110
129
 
111
- 1. CLI reads the fee from the contract
130
+ 1. CLI asks the engine for public crypto config (contract + fee), with a direct contract-read fallback
112
131
  2. You approve the tx in your wallet
113
132
  3. Engine verifies the receipt on Base Sepolia via Alchemy
114
133
  4. Audit starts, CLI streams events
@@ -120,20 +139,23 @@ against your request before launching the audit.
120
139
  ## Configuration
121
140
 
122
141
  The CLI talks to `https://npmguard.com` by default. You can override the
123
- API URL for local development:
142
+ API URL and the web app URL for local development:
124
143
 
125
144
  ```bash
126
145
  # via flag
127
- npmguard --api http://localhost:8000 install lodash
146
+ npmguard --api http://localhost:8000 --web http://localhost:3000 install lodash
128
147
 
129
148
  # via env
130
149
  export NPMGUARD_API_URL=http://localhost:8000
150
+ export NPMGUARD_WEB_URL=http://localhost:3000
131
151
  npmguard install lodash
132
152
  ```
133
153
 
134
- No blockchain config is required from the user the CLI reads the
135
- contract address + chain from its own code. Your wallet's RPC handles the
136
- broadcast.
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.
137
159
 
138
160
  ## Exit codes
139
161
 
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
@@ -18,7 +18,8 @@ program
18
18
  .name("npmguard")
19
19
  .description("NpmGuard CLI — audit npm packages for security issues")
20
20
  .version(readPackageVersion())
21
- .option("--api <url>", "NpmGuard engine API URL", process.env.NPMGUARD_API_URL ?? "https://npmguard.com");
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");
22
23
  program
23
24
  .command("audit")
24
25
  .description("Pay for and run a security audit on an npm package")
@@ -33,8 +34,8 @@ program
33
34
  .argument("<package>", "Package name, optionally with version (e.g. express@4.18.0)")
34
35
  .option("-f, --force", "Install even if the package is flagged as dangerous")
35
36
  .action(async (pkg, cmdOpts) => {
36
- const apiUrl = program.opts().api;
37
- 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 });
38
39
  });
39
40
  program
40
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.2",
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",