UNPKG

run402-mcp

Version:

MCP server for Run402 — full-stack backend infrastructure for AI agents: Postgres, auth, storage, serverless functions and atomic deploys. Paid with x402/MPP. Includes $0.03 image generation.

96 lines • 4.14 kB
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, renameSync, statSync } from "node:fs"; import { dirname, join } from "node:path"; import { randomBytes } from "node:crypto"; import { getWalletPath } from "./config.js"; // 0x-prefixed 40-hex EVM address. const ADDRESS_RE = /^0x[a-fA-F0-9]{40}$/; // 0x-prefixed 64-hex secp256k1 private key (32 bytes). const PRIVATE_KEY_RE = /^0x[a-fA-F0-9]{64}$/; /** * Load the agent wallet from disk. * * Returns `null` for the two "no local wallet configured" cases: * - the file does not exist * - the file exists but is not parseable JSON (preserve existing UX — * consumers print "no_wallet" and tell the user to run init) * * Throws a structured `Error` when the file parses as JSON but the * shape is wrong (missing/wrong-type/wrong-length fields). Without this guard * downstream callers crash with raw stack traces: * - `cli/lib/status.mjs` reaches for `wallet.address.toLowerCase()` * and crashes with `TypeError: Cannot read properties of undefined`. * - `core/src/wallet-auth.ts` passes a malformed `privateKey` to * `@noble/curves` which throws "expected 32 bytes, got N". * * The CLI's `cli/lib/config.mjs:readWallet()` wrapper and the Node SDK's * `readLocalWallet()` (`sdk/src/node/wallets.ts`) translate the throw into * their own structured envelopes (`code: BAD_WALLET_FILE`). */ /** * If a wallet file is readable by group or other (any of the low 0o077 * bits set), tighten it to 0600 and warn once on stderr. This self-heals the * historical case where a legacy world-readable `wallet.json` (mode 0644) was * migrated to `wallet.json` via a mode-preserving rename, leaving the * private key exposed on a shared machine. Best-effort: POSIX-only and silent * on platforms without meaningful Unix modes (e.g. Windows). */ function selfHealPermissions(p) { if (process.platform === "win32") return; try { const mode = statSync(p).mode & 0o777; if ((mode & 0o077) !== 0) { chmodSync(p, 0o600); process.stderr.write(`warning: tightened permissions on ${p} from ${mode.toString(8)} to 600 (was readable by other users).\n`); } } catch { // Best-effort; never block a read on a chmod/stat failure. } } export function readWallet(path) { const p = path ?? getWalletPath(); if (!existsSync(p)) return null; selfHealPermissions(p); let raw; try { raw = readFileSync(p, "utf-8"); } catch { return null; } let parsed; try { parsed = JSON.parse(raw); } catch { // Preserve historical UX — completely unparseable input reads as "no // wallet configured" rather than as an error. Consumers already handle // null with a friendly "run 'run402 init'" message. return null; } if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) { throw new Error(`wallet.json must contain a JSON object (got ${Array.isArray(parsed) ? "array" : parsed === null ? "null" : typeof parsed}). Back up the file and run 'run402 init' to recreate it.`); } const data = parsed; if (typeof data.address !== "string" || !ADDRESS_RE.test(data.address)) { throw new Error("wallet.json missing valid 'address' (expected 0x-prefixed 40-hex string). " + "Back up the file and run 'run402 init' to recreate it."); } if (typeof data.privateKey !== "string" || !PRIVATE_KEY_RE.test(data.privateKey)) { throw new Error("wallet.json missing valid 'privateKey' (expected 0x-prefixed 64-hex string). " + "Back up the file and run 'run402 init' to recreate it."); } return data; } export function saveWallet(data, path) { const p = path ?? getWalletPath(); const dir = dirname(p); mkdirSync(dir, { recursive: true }); const tmp = join(dir, `.wallet.${randomBytes(4).toString("hex")}.tmp`); writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 0o600 }); renameSync(tmp, p); chmodSync(p, 0o600); } //# sourceMappingURL=wallet.js.map