npmguard-cli 1.0.1 → 1.1.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/dist/api.js CHANGED
@@ -30,6 +30,13 @@ export async function startAudit(apiUrl, stripeSessionId) {
30
30
  body: JSON.stringify({ stripeSessionId }),
31
31
  });
32
32
  }
33
+ export async function startAuditWithTxHash(apiUrl, packageName, version, txHash) {
34
+ return request(`${apiUrl}/audit/stream`, {
35
+ method: "POST",
36
+ headers: { "Content-Type": "application/json" },
37
+ body: JSON.stringify({ packageName, version, txHash, chain: "base-sepolia" }),
38
+ });
39
+ }
33
40
  export async function startAuditFree(apiUrl, packageName, version) {
34
41
  return request(`${apiUrl}/audit/stream`, {
35
42
  method: "POST",
@@ -4,35 +4,16 @@ import qrcode from "qrcode-terminal";
4
4
  import EventSource from "eventsource";
5
5
  import * as api from "../api.js";
6
6
  import { renderVerdict, renderFinding, renderPhase } from "../render.js";
7
- function parsePackageArg(pkg) {
8
- // Handle scoped packages: @scope/name@version
9
- if (pkg.startsWith("@")) {
10
- const slashIndex = pkg.indexOf("/");
11
- if (slashIndex === -1) {
12
- throw new Error(`Invalid scoped package name: ${pkg}`);
13
- }
14
- const rest = pkg.slice(slashIndex + 1);
15
- const atIndex = rest.lastIndexOf("@");
16
- if (atIndex > 0) {
17
- return {
18
- name: pkg.slice(0, slashIndex + 1 + atIndex),
19
- version: rest.slice(atIndex + 1),
20
- };
21
- }
22
- return { name: pkg };
23
- }
24
- // Handle unscoped packages: name@version
25
- const atIndex = pkg.lastIndexOf("@");
26
- if (atIndex > 0) {
27
- return {
28
- name: pkg.slice(0, atIndex),
29
- version: pkg.slice(atIndex + 1),
30
- };
31
- }
32
- return { name: pkg };
33
- }
7
+ import { parsePackageArg } from "../utils.js";
34
8
  export async function auditCommand(pkg, opts) {
35
9
  const apiUrl = opts.api;
10
+ const shouldExit = opts.exit !== false;
11
+ const done = (code) => {
12
+ if (shouldExit)
13
+ process.exit(code);
14
+ if (code !== 0)
15
+ throw new Error(`Audit aborted (code ${code})`);
16
+ };
36
17
  // 1. Parse package name and version
37
18
  let parsed;
38
19
  try {
@@ -40,7 +21,7 @@ export async function auditCommand(pkg, opts) {
40
21
  }
41
22
  catch {
42
23
  console.error(chalk.red(`Invalid package: ${pkg}`));
43
- process.exit(1);
24
+ return done(1);
44
25
  }
45
26
  console.log(chalk.bold(`Auditing ${parsed.name}`) +
46
27
  (parsed.version ? chalk.gray(`@${parsed.version}`) : ""));
@@ -54,7 +35,7 @@ export async function auditCommand(pkg, opts) {
54
35
  renderVerdict(verdict, existing.report?.capabilities ?? [], existing.report?.proofs?.length ?? 0);
55
36
  console.log();
56
37
  console.log(chalk.dim(`View full report: ${apiUrl}/package/${encodeURIComponent(parsed.name)}/report`));
57
- process.exit(verdict === "SAFE" ? 0 : 1);
38
+ return done(verdict === "SAFE" ? 0 : 1);
58
39
  }
59
40
  // 2. Try checkout — if payments not configured (501), go straight to free audit
60
41
  const spinner = ora("Connecting...").start();
@@ -66,7 +47,7 @@ export async function auditCommand(pkg, opts) {
66
47
  catch (err) {
67
48
  spinner.fail("Failed to create checkout session: " +
68
49
  (err instanceof Error ? err.message : String(err)));
69
- process.exit(1);
50
+ return done(1);
70
51
  }
71
52
  if (checkoutResult.status === 501 || !checkoutResult.data) {
72
53
  // Payments not configured — start audit directly (dev/free mode)
@@ -78,7 +59,7 @@ export async function auditCommand(pkg, opts) {
78
59
  catch (err) {
79
60
  spinner.fail("Failed to start audit: " +
80
61
  (err instanceof Error ? err.message : String(err)));
81
- process.exit(1);
62
+ return done(1);
82
63
  }
83
64
  }
84
65
  else {
@@ -112,7 +93,7 @@ export async function auditCommand(pkg, opts) {
112
93
  catch (err) {
113
94
  spinner.fail("Payment polling failed: " +
114
95
  (err instanceof Error ? err.message : String(err)));
115
- process.exit(1);
96
+ return done(1);
116
97
  }
117
98
  spinner.text = "Payment confirmed. Starting audit...";
118
99
  if (status.auditId) {
@@ -126,7 +107,7 @@ export async function auditCommand(pkg, opts) {
126
107
  catch (err) {
127
108
  spinner.fail("Failed to start audit: " +
128
109
  (err instanceof Error ? err.message : String(err)));
129
- process.exit(1);
110
+ return done(1);
130
111
  }
131
112
  }
132
113
  }
@@ -191,5 +172,5 @@ export async function auditCommand(pkg, opts) {
191
172
  }
192
173
  };
193
174
  });
194
- process.exit(exitCode);
175
+ return done(exitCode);
195
176
  }
@@ -0,0 +1,201 @@
1
+ import chalk from "chalk";
2
+ import ora from "ora";
3
+ import { spawnSync } from "node:child_process";
4
+ import { formatEther } from "viem";
5
+ import * as api from "../api.js";
6
+ import { parsePackageArg, prompt, resolveLatestVersion, detectPackageManager, } from "../utils.js";
7
+ import { auditCommand } from "./audit.js";
8
+ import { payViaWalletConnect, readAuditFee } from "../wallet/walletconnect.js";
9
+ import { streamAuditEvents } from "../stream.js";
10
+ /**
11
+ * Run the correct "add this package" command for the detected package manager.
12
+ * `npm install <pkg>` adds the package. `pnpm add <pkg>` / `yarn add <pkg>` do
13
+ * the same. Crucially, `yarn install <pkg>` would ignore <pkg> in yarn classic.
14
+ */
15
+ function runInstall(packageSpec) {
16
+ const pm = detectPackageManager();
17
+ const verb = pm === "npm" ? "install" : "add";
18
+ console.log(chalk.gray(`\n Running: ${pm} ${verb} ${packageSpec}\n`));
19
+ const res = spawnSync(pm, [verb, packageSpec], { stdio: "inherit" });
20
+ return res.status ?? 1;
21
+ }
22
+ function extractVerdict(report) {
23
+ const nested = report.report?.verdict;
24
+ return (nested ?? report.verdict ?? "UNKNOWN").toUpperCase();
25
+ }
26
+ function extractCapabilities(report) {
27
+ const nested = report.report
28
+ ?.capabilities;
29
+ return nested ?? report.capabilities ?? [];
30
+ }
31
+ export async function installCommand(packageSpec, opts) {
32
+ const apiUrl = opts.api;
33
+ let parsed;
34
+ try {
35
+ parsed = parsePackageArg(packageSpec);
36
+ }
37
+ catch {
38
+ console.error(chalk.red(`Invalid package: ${packageSpec}`));
39
+ process.exit(1);
40
+ }
41
+ let { name, version } = parsed;
42
+ if (!version) {
43
+ const spinner = ora(`Resolving latest version of ${name}...`).start();
44
+ const resolved = await resolveLatestVersion(name);
45
+ if (!resolved) {
46
+ spinner.fail("Could not resolve version from npm registry.");
47
+ process.exit(1);
48
+ }
49
+ version = resolved;
50
+ spinner.succeed(`Resolved ${name}@${version}`);
51
+ }
52
+ const fullSpec = `${name}@${version}`;
53
+ console.log();
54
+ console.log(chalk.bold(` ${fullSpec}`));
55
+ console.log();
56
+ const spinner = ora("Checking NpmGuard audit...").start();
57
+ let report;
58
+ try {
59
+ report = await api.getPackageReport(apiUrl, name, version);
60
+ }
61
+ catch (err) {
62
+ spinner.fail("Could not reach NpmGuard API: " +
63
+ (err instanceof Error ? err.message : String(err)));
64
+ process.exit(1);
65
+ }
66
+ spinner.stop();
67
+ if (report) {
68
+ handleExistingReport(report, name, fullSpec, apiUrl, opts);
69
+ return;
70
+ }
71
+ // No audit found — ask how to pay
72
+ console.log(chalk.gray(" NOT AUDITED — no NpmGuard record for this version."));
73
+ console.log();
74
+ console.log(chalk.bold(" How do you want to pay for the audit?"));
75
+ console.log(" 1) Stripe (credit card)");
76
+ console.log(" 2) WalletConnect — ETH on Base Sepolia");
77
+ console.log(" 3) Install without audit (at your own risk)");
78
+ console.log(" 4) Cancel");
79
+ console.log();
80
+ const choice = await prompt(" Choice [1/2/3/4]: ");
81
+ if (choice === "1") {
82
+ await runStripeAuditAndInstall(fullSpec, name, version, apiUrl);
83
+ return;
84
+ }
85
+ if (choice === "2") {
86
+ await runCryptoAuditAndInstall(fullSpec, name, version, apiUrl);
87
+ return;
88
+ }
89
+ if (choice === "3") {
90
+ console.log(chalk.yellow(" Installing without audit. Proceed at your own risk."));
91
+ process.exit(runInstall(fullSpec));
92
+ }
93
+ console.log(chalk.gray(" Cancelled."));
94
+ process.exit(0);
95
+ }
96
+ function handleExistingReport(report, name, fullSpec, apiUrl, opts) {
97
+ const verdict = extractVerdict(report);
98
+ const capabilities = extractCapabilities(report);
99
+ if (verdict === "SAFE") {
100
+ console.log(chalk.green(" ✓ SAFE — audited by NpmGuard"));
101
+ if (capabilities.length > 0) {
102
+ console.log(chalk.gray(` Capabilities: ${capabilities.join(", ")}`));
103
+ }
104
+ process.exit(runInstall(fullSpec));
105
+ }
106
+ if (verdict === "DANGEROUS" || verdict === "CRITICAL" || verdict === "WARNING") {
107
+ console.log(chalk.bgRed.white.bold(` ${verdict} `));
108
+ if (capabilities.length > 0) {
109
+ console.log(chalk.red(" Capabilities: ") +
110
+ capabilities.map((c) => chalk.yellow(c)).join(", "));
111
+ }
112
+ console.log(chalk.dim(` Full report: ${apiUrl}/package/${encodeURIComponent(name)}/report`));
113
+ console.log();
114
+ if (opts.force) {
115
+ console.log(chalk.yellow(" --force passed, installing anyway..."));
116
+ process.exit(runInstall(fullSpec));
117
+ }
118
+ promptAndInstallIfAccepted(fullSpec, " Install anyway? This package is flagged. (y/N) ");
119
+ return;
120
+ }
121
+ console.log(chalk.yellow(` Verdict: ${verdict}`));
122
+ promptAndInstallIfAccepted(fullSpec, " Proceed with install? (y/N) ");
123
+ }
124
+ async function promptAndInstallIfAccepted(fullSpec, question) {
125
+ const answer = await prompt(chalk.red.bold(question));
126
+ if (answer === "y" || answer === "yes") {
127
+ process.exit(runInstall(fullSpec));
128
+ }
129
+ console.log(chalk.gray(" Aborted."));
130
+ process.exit(1);
131
+ }
132
+ async function runStripeAuditAndInstall(fullSpec, name, version, apiUrl) {
133
+ try {
134
+ await auditCommand(fullSpec, { api: apiUrl, exit: false });
135
+ }
136
+ catch (err) {
137
+ console.error(chalk.red("Audit failed: " + (err instanceof Error ? err.message : String(err))));
138
+ process.exit(1);
139
+ }
140
+ await finalizeAfterAudit(fullSpec, name, version, apiUrl);
141
+ }
142
+ async function runCryptoAuditAndInstall(fullSpec, name, version, apiUrl) {
143
+ // 1. Read current fee from contract
144
+ let feeWei;
145
+ try {
146
+ feeWei = await readAuditFee();
147
+ }
148
+ 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);
152
+ }
153
+ const feeDisplay = `${formatEther(feeWei)} ETH`;
154
+ const confirm = await prompt(chalk.yellow(` Pay ${feeDisplay} on Base Sepolia? (y/N) `));
155
+ if (confirm !== "y" && confirm !== "yes") {
156
+ console.log(chalk.gray(" Cancelled."));
157
+ process.exit(0);
158
+ }
159
+ // 2. WalletConnect → user signs → we get txHash
160
+ const result = await payViaWalletConnect(name, version, feeWei, feeDisplay);
161
+ if (!result.paid || !result.txHash) {
162
+ console.log(chalk.red(" Payment failed, aborting."));
163
+ process.exit(1);
164
+ }
165
+ // 3. Engine verifies txHash + returns auditId
166
+ const startSpinner = ora(" Starting audit on engine...").start();
167
+ let auditId;
168
+ try {
169
+ const res = await api.startAuditWithTxHash(apiUrl, name, version, result.txHash);
170
+ auditId = res.auditId;
171
+ startSpinner.succeed(`Audit started (id: ${auditId})`);
172
+ }
173
+ catch (err) {
174
+ startSpinner.fail("Engine rejected txHash: " +
175
+ (err instanceof Error ? err.message : String(err)));
176
+ process.exit(1);
177
+ }
178
+ // 4. Stream the audit we just paid for (do NOT call auditCommand — that
179
+ // would trigger a second, unpaid audit via /checkout).
180
+ await streamAuditEvents(apiUrl, auditId);
181
+ // 5. Fetch the persisted report and decide whether to install
182
+ await finalizeAfterAudit(fullSpec, name, version, apiUrl);
183
+ }
184
+ async function finalizeAfterAudit(fullSpec, name, version, apiUrl) {
185
+ const freshReport = await api.getPackageReport(apiUrl, name, version);
186
+ if (!freshReport) {
187
+ console.log(chalk.red(" Audit finished but report not found."));
188
+ process.exit(1);
189
+ }
190
+ const verdict = extractVerdict(freshReport);
191
+ if (verdict === "SAFE") {
192
+ console.log(chalk.green("\n ✓ SAFE — proceeding with install"));
193
+ process.exit(runInstall(fullSpec));
194
+ }
195
+ console.log(chalk.red(`\n Audit verdict: ${verdict}`));
196
+ const confirm = await prompt(chalk.red.bold(" Install anyway? (y/N) "));
197
+ if (confirm === "y" || confirm === "yes") {
198
+ process.exit(runInstall(fullSpec));
199
+ }
200
+ process.exit(1);
201
+ }
@@ -0,0 +1,60 @@
1
+ // NpmGuardAuditRequest — deployed on Base Sepolia
2
+ // Deploy script: contracts/deploy.sh
3
+ // Source: contracts/src/NpmGuardAuditRequest.sol
4
+ export const AUDIT_REQUEST_ADDRESS_BASE_SEPOLIA = "0xBF562626e4Afb883423Ec719e0270DB232bcB9eD";
5
+ // Base mainnet — not deployed yet
6
+ export const AUDIT_REQUEST_ADDRESS_BASE = "0x";
7
+ export const BASE_SEPOLIA_CHAIN_ID = 84532;
8
+ export const BASE_CHAIN_ID = 8453;
9
+ export const AUDIT_REQUEST_ABI = [
10
+ {
11
+ type: "constructor",
12
+ inputs: [{ name: "_auditFee", type: "uint256" }],
13
+ stateMutability: "nonpayable",
14
+ },
15
+ {
16
+ type: "function",
17
+ name: "requestAudit",
18
+ inputs: [
19
+ { name: "packageName", type: "string" },
20
+ { name: "version", type: "string" },
21
+ ],
22
+ outputs: [],
23
+ stateMutability: "payable",
24
+ },
25
+ {
26
+ type: "function",
27
+ name: "isRequested",
28
+ inputs: [
29
+ { name: "packageName", type: "string" },
30
+ { name: "version", type: "string" },
31
+ ],
32
+ outputs: [{ name: "", type: "bool" }],
33
+ stateMutability: "view",
34
+ },
35
+ {
36
+ type: "function",
37
+ name: "auditFee",
38
+ inputs: [],
39
+ outputs: [{ name: "", type: "uint256" }],
40
+ stateMutability: "view",
41
+ },
42
+ {
43
+ type: "function",
44
+ name: "owner",
45
+ inputs: [],
46
+ outputs: [{ name: "", type: "address" }],
47
+ stateMutability: "view",
48
+ },
49
+ {
50
+ type: "event",
51
+ name: "AuditRequested",
52
+ inputs: [
53
+ { name: "packageName", type: "string", indexed: false },
54
+ { name: "version", type: "string", indexed: false },
55
+ { name: "requester", type: "address", indexed: true },
56
+ { name: "feePaid", type: "uint256", indexed: false },
57
+ ],
58
+ anonymous: false,
59
+ },
60
+ ];
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { Command } from "commander";
3
3
  import { auditCommand } from "./commands/audit.js";
4
4
  import { checkCommand } from "./commands/check.js";
5
+ import { installCommand } from "./commands/install.js";
5
6
  const program = new Command();
6
7
  program
7
8
  .name("npmguard")
@@ -16,6 +17,15 @@ program
16
17
  const apiUrl = program.opts().api;
17
18
  await auditCommand(pkg, { api: apiUrl });
18
19
  });
20
+ program
21
+ .command("install")
22
+ .description("Install an npm package with NpmGuard security check")
23
+ .argument("<package>", "Package name, optionally with version (e.g. express@4.18.0)")
24
+ .option("-f, --force", "Install even if the package is flagged as dangerous")
25
+ .action(async (pkg, cmdOpts) => {
26
+ const apiUrl = program.opts().api;
27
+ await installCommand(pkg, { api: apiUrl, force: cmdOpts.force });
28
+ });
19
29
  program
20
30
  .command("check")
21
31
  .description("Check all dependencies of a project against existing audits")
package/dist/stream.js ADDED
@@ -0,0 +1,72 @@
1
+ import chalk from "chalk";
2
+ import ora from "ora";
3
+ import EventSource from "eventsource";
4
+ import { renderVerdict, renderFinding, renderPhase } from "./render.js";
5
+ /**
6
+ * Connect to the engine SSE stream for an existing audit and render events
7
+ * until a verdict or error arrives. Does not exit the process.
8
+ */
9
+ export async function streamAuditEvents(apiUrl, auditId) {
10
+ const eventsUrl = `${apiUrl}/audit/${encodeURIComponent(auditId)}/events`;
11
+ const es = new EventSource(eventsUrl);
12
+ const spinner = ora("Audit in progress...").start();
13
+ let verdict = "UNKNOWN";
14
+ let exitCode = 0;
15
+ await new Promise((resolve) => {
16
+ es.addEventListener("phase_started", (event) => {
17
+ try {
18
+ const data = JSON.parse(event.data);
19
+ spinner.text = renderPhase(data.phase ?? data.name ?? "");
20
+ }
21
+ catch {
22
+ // ignore
23
+ }
24
+ });
25
+ es.addEventListener("finding_discovered", (event) => {
26
+ try {
27
+ const data = JSON.parse(event.data);
28
+ const finding = data.finding ?? data;
29
+ spinner.stop();
30
+ renderFinding(finding);
31
+ spinner.start();
32
+ }
33
+ catch {
34
+ // ignore
35
+ }
36
+ });
37
+ es.addEventListener("verdict_reached", (event) => {
38
+ try {
39
+ const data = JSON.parse(event.data);
40
+ spinner.stop();
41
+ verdict = (data.verdict ?? "UNKNOWN").toString().toUpperCase();
42
+ renderVerdict(verdict, data.capabilities ?? [], data.proofCount ?? data.findings?.length ?? 0);
43
+ exitCode = verdict === "SAFE" ? 0 : 1;
44
+ }
45
+ catch {
46
+ spinner.stop();
47
+ }
48
+ es.close();
49
+ resolve();
50
+ });
51
+ es.addEventListener("audit_error", (event) => {
52
+ try {
53
+ const data = JSON.parse(event.data);
54
+ spinner.fail(chalk.red("Audit error: " + (data.error ?? event.data)));
55
+ }
56
+ catch {
57
+ spinner.fail(chalk.red("Audit error: " + event.data));
58
+ }
59
+ exitCode = 1;
60
+ es.close();
61
+ resolve();
62
+ });
63
+ es.onerror = () => {
64
+ if (es.readyState === EventSource.CLOSED) {
65
+ spinner.stop();
66
+ es.close();
67
+ resolve();
68
+ }
69
+ };
70
+ });
71
+ return { verdict, exitCode };
72
+ }
package/dist/utils.js ADDED
@@ -0,0 +1,56 @@
1
+ import { createInterface } from "node:readline";
2
+ import { existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ export function parsePackageArg(pkg) {
5
+ if (pkg.startsWith("@")) {
6
+ const slashIndex = pkg.indexOf("/");
7
+ if (slashIndex === -1) {
8
+ throw new Error(`Invalid scoped package name: ${pkg}`);
9
+ }
10
+ const rest = pkg.slice(slashIndex + 1);
11
+ const atIndex = rest.lastIndexOf("@");
12
+ if (atIndex > 0) {
13
+ return {
14
+ name: pkg.slice(0, slashIndex + 1 + atIndex),
15
+ version: rest.slice(atIndex + 1),
16
+ };
17
+ }
18
+ return { name: pkg };
19
+ }
20
+ const atIndex = pkg.lastIndexOf("@");
21
+ if (atIndex > 0) {
22
+ return {
23
+ name: pkg.slice(0, atIndex),
24
+ version: pkg.slice(atIndex + 1),
25
+ };
26
+ }
27
+ return { name: pkg };
28
+ }
29
+ export function prompt(question) {
30
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
31
+ return new Promise((resolve) => {
32
+ rl.question(question, (answer) => {
33
+ rl.close();
34
+ resolve(answer.trim().toLowerCase());
35
+ });
36
+ });
37
+ }
38
+ export async function resolveLatestVersion(packageName) {
39
+ try {
40
+ const res = await fetch(`https://registry.npmjs.org/${packageName}/latest`);
41
+ if (!res.ok)
42
+ return null;
43
+ const data = (await res.json());
44
+ return data.version ?? null;
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ }
50
+ export function detectPackageManager(cwd = process.cwd()) {
51
+ if (existsSync(join(cwd, "pnpm-lock.yaml")))
52
+ return "pnpm";
53
+ if (existsSync(join(cwd, "yarn.lock")))
54
+ return "yarn";
55
+ return "npm";
56
+ }
@@ -0,0 +1,133 @@
1
+ import chalk from "chalk";
2
+ import ora from "ora";
3
+ import qrcode from "qrcode-terminal";
4
+ import { SignClient } from "@walletconnect/sign-client";
5
+ import { createPublicClient, http, encodeFunctionData } from "viem";
6
+ import { baseSepolia } from "viem/chains";
7
+ import { AUDIT_REQUEST_ADDRESS_BASE_SEPOLIA, AUDIT_REQUEST_ABI, BASE_SEPOLIA_CHAIN_ID, } from "../contract.js";
8
+ const WALLETCONNECT_PROJECT_ID = process.env.WALLETCONNECT_PROJECT_ID ?? "d5eb170c427570e15ac00ae53acc93ba";
9
+ function generateQrCode(text) {
10
+ return new Promise((resolve) => {
11
+ qrcode.generate(text, { small: true }, (code) => {
12
+ console.log(code);
13
+ resolve();
14
+ });
15
+ });
16
+ }
17
+ export async function payViaWalletConnect(packageName, version, feeWei, feeDisplay) {
18
+ const calldata = encodeFunctionData({
19
+ abi: AUDIT_REQUEST_ABI,
20
+ functionName: "requestAudit",
21
+ args: [packageName, version],
22
+ });
23
+ const publicClient = createPublicClient({
24
+ chain: baseSepolia,
25
+ transport: http(),
26
+ });
27
+ let signClient = null;
28
+ // WalletConnect throws late "No matching key" errors when sessions clean up
29
+ const wcErrorHandler = (err) => {
30
+ if (err?.message?.includes("No matching key"))
31
+ return;
32
+ console.error(err);
33
+ process.exit(1);
34
+ };
35
+ process.on("uncaughtException", wcErrorHandler);
36
+ try {
37
+ const initSpinner = ora(" Connecting to WalletConnect...").start();
38
+ signClient = await SignClient.init({
39
+ projectId: WALLETCONNECT_PROJECT_ID,
40
+ metadata: {
41
+ name: "NpmGuard",
42
+ description: "NPM package security audit",
43
+ url: "https://npmguard.com",
44
+ icons: [],
45
+ },
46
+ });
47
+ initSpinner.stop();
48
+ const { uri, approval } = await signClient.connect({
49
+ requiredNamespaces: {
50
+ eip155: {
51
+ methods: ["eth_sendTransaction"],
52
+ chains: [`eip155:${BASE_SEPOLIA_CHAIN_ID}`],
53
+ events: ["chainChanged", "accountsChanged"],
54
+ },
55
+ },
56
+ });
57
+ if (!uri) {
58
+ console.log(chalk.red(" Failed to generate WalletConnect URI"));
59
+ return { paid: false };
60
+ }
61
+ console.log();
62
+ console.log(chalk.cyan(" Scan with your wallet to connect:"));
63
+ console.log();
64
+ await generateQrCode(uri);
65
+ console.log();
66
+ const pairSpinner = ora(" Waiting for wallet connection...").start();
67
+ const session = await approval();
68
+ const accounts = session.namespaces.eip155?.accounts ?? [];
69
+ const baseAccount = accounts.find((a) => a.startsWith(`eip155:${BASE_SEPOLIA_CHAIN_ID}:`));
70
+ const sender = baseAccount
71
+ ? baseAccount.split(":")[2]
72
+ : accounts[0]?.split(":")[2];
73
+ if (!sender) {
74
+ pairSpinner.fail("Wallet did not approve any accounts");
75
+ return { paid: false };
76
+ }
77
+ pairSpinner.succeed(`Connected: ${sender.slice(0, 6)}...${sender.slice(-4)}`);
78
+ console.log(chalk.cyan(` Confirm the ${feeDisplay} transaction in your wallet (Base Sepolia)...`));
79
+ const txHash = (await signClient.request({
80
+ topic: session.topic,
81
+ chainId: `eip155:${BASE_SEPOLIA_CHAIN_ID}`,
82
+ request: {
83
+ method: "eth_sendTransaction",
84
+ params: [
85
+ {
86
+ from: sender,
87
+ to: AUDIT_REQUEST_ADDRESS_BASE_SEPOLIA,
88
+ data: calldata,
89
+ value: "0x" + feeWei.toString(16),
90
+ },
91
+ ],
92
+ },
93
+ }));
94
+ const confirmSpinner = ora(" Waiting for on-chain confirmation...").start();
95
+ const receipt = await publicClient.waitForTransactionReceipt({
96
+ hash: txHash,
97
+ });
98
+ if (receipt.status === "success") {
99
+ confirmSpinner.succeed("Payment confirmed on-chain");
100
+ console.log(chalk.gray(` Tx: https://sepolia.basescan.org/tx/${txHash}`));
101
+ console.log();
102
+ return { paid: true, txHash, sender };
103
+ }
104
+ confirmSpinner.fail("Transaction reverted");
105
+ return { paid: false };
106
+ }
107
+ catch (err) {
108
+ const msg = err instanceof Error ? err.message : String(err);
109
+ if (msg.includes("rejected") || msg.includes("denied")) {
110
+ console.log(chalk.yellow(" Transaction rejected by user."));
111
+ }
112
+ else {
113
+ console.log(chalk.red(` WalletConnect error: ${msg}`));
114
+ }
115
+ console.log();
116
+ return { paid: false };
117
+ }
118
+ finally {
119
+ signClient = null;
120
+ setTimeout(() => process.off("uncaughtException", wcErrorHandler), 5000);
121
+ }
122
+ }
123
+ export async function readAuditFee() {
124
+ const publicClient = createPublicClient({
125
+ chain: baseSepolia,
126
+ transport: http(),
127
+ });
128
+ return (await publicClient.readContract({
129
+ address: AUDIT_REQUEST_ADDRESS_BASE_SEPOLIA,
130
+ abi: AUDIT_REQUEST_ABI,
131
+ functionName: "auditFee",
132
+ }));
133
+ }
package/package.json CHANGED
@@ -1,10 +1,12 @@
1
1
  {
2
2
  "name": "npmguard-cli",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "description": "NpmGuard CLI — check npm packages against NpmGuard security audits",
6
6
  "main": "dist/index.js",
7
- "files": ["dist"],
7
+ "files": [
8
+ "dist"
9
+ ],
8
10
  "bin": {
9
11
  "npmguard": "./dist/index.js"
10
12
  },
@@ -19,11 +21,13 @@
19
21
  ],
20
22
  "license": "MIT",
21
23
  "dependencies": {
24
+ "@walletconnect/sign-client": "^2.23.9",
22
25
  "chalk": "^5.3.0",
23
26
  "commander": "^12.1.0",
24
27
  "eventsource": "^2.0.2",
25
28
  "ora": "^8.1.0",
26
- "qrcode-terminal": "^0.12.0"
29
+ "qrcode-terminal": "^0.12.0",
30
+ "viem": "^2.47.17"
27
31
  },
28
32
  "devDependencies": {
29
33
  "@types/node": "^22.0.0",