@aifinpay/mcp 2.0.0-rc.3 → 2.0.0-rc.4

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.
@@ -31,6 +31,7 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync, statSync, chmodSync
31
31
  import { join } from "node:path";
32
32
  import { homedir } from "node:os";
33
33
  import { createRequire } from "node:module";
34
+ import { scryptSync, randomBytes, createCipheriv, createDecipheriv } from "node:crypto";
34
35
 
35
36
  const HOME = process.env.AIFINPAY_HOME || join(homedir(), ".aifinpay");
36
37
  const KEYSTORE = join(HOME, "agent.json");
@@ -77,14 +78,41 @@ if (arg === "--version" || arg === "-v" || arg === "version") {
77
78
 
78
79
  // ── init ──────────────────────────────────────────────────────────────────
79
80
 
81
+ const PASSPHRASE = process.env.AIFINPAY_WALLET_PASSPHRASE || null;
82
+
83
+ // Optional at-rest encryption. Mode 600 keeps the keystore from OTHER users; it
84
+ // does nothing against malware running as YOU — the threat the OSINT write-up of
85
+ // a compromised dev machine described. A passphrase makes the on-disk file
86
+ // ciphertext; the secret is only in memory while the passphrase is supplied.
87
+ // Opt-in and env-supplied so the non-interactive `npx @aifinpay/mcp` launch path
88
+ // is unaffected: no passphrase => plaintext, exactly as before. scrypt to
89
+ // stretch, AES-256-GCM so tampering is detected rather than decrypting to junk.
90
+ function encryptSecret(secretB58) {
91
+ const salt = randomBytes(16), iv = randomBytes(12);
92
+ const key = scryptSync(PASSPHRASE, salt, 32, { N: 1 << 15, r: 8, p: 1, maxmem: 64 * 1024 * 1024 });
93
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
94
+ const ct = Buffer.concat([cipher.update(secretB58, "utf8"), cipher.final()]);
95
+ return { enc: "scrypt-aes-256-gcm", salt: salt.toString("base64"), iv: iv.toString("base64"),
96
+ tag: cipher.getAuthTag().toString("base64"), ct: ct.toString("base64"),
97
+ created: new Date().toISOString() };
98
+ }
99
+ function decryptSecret(store) {
100
+ if (!PASSPHRASE) throw new Error(`${KEYSTORE} is encrypted but AIFINPAY_WALLET_PASSPHRASE is not set.`);
101
+ const key = scryptSync(PASSPHRASE, Buffer.from(store.salt, "base64"), 32, { N: 1 << 15, r: 8, p: 1, maxmem: 64 * 1024 * 1024 });
102
+ const dc = createDecipheriv("aes-256-gcm", key, Buffer.from(store.iv, "base64"));
103
+ dc.setAuthTag(Buffer.from(store.tag, "base64"));
104
+ try { return Buffer.concat([dc.update(Buffer.from(store.ct, "base64")), dc.final()]).toString("utf8"); }
105
+ catch { throw new Error(`could not decrypt ${KEYSTORE}: wrong AIFINPAY_WALLET_PASSPHRASE or the file was modified.`); }
106
+ }
107
+
80
108
  function readKeystore() {
81
109
  if (!existsSync(KEYSTORE)) return null;
82
- try {
83
- const parsed = JSON.parse(readFileSync(KEYSTORE, "utf8"));
84
- return typeof parsed?.secretB58 === "string" ? parsed : null;
85
- } catch {
86
- return null;
87
- }
110
+ let parsed;
111
+ try { parsed = JSON.parse(readFileSync(KEYSTORE, "utf8")); } catch { return null; }
112
+ // A decrypt failure THROWS rather than returning null — null reads as "no
113
+ // wallet" and mints a new one, which is how a mistyped passphrase loses a key.
114
+ if (parsed?.enc) return { secretB58: decryptSecret(parsed), created: parsed.created, encrypted: true };
115
+ return typeof parsed?.secretB58 === "string" ? parsed : null;
88
116
  }
89
117
 
90
118
  if (arg === "init") {
@@ -98,10 +126,16 @@ if (arg === "init") {
98
126
  process.stdout.write(`Existing wallet found at ${KEYSTORE} — keeping it.\n\n`);
99
127
  } else {
100
128
  mkdirSync(HOME, { recursive: true, mode: 0o700 });
101
- store = { secretB58: Agent.new().secretB58, created: new Date().toISOString() };
102
- writeFileSync(KEYSTORE, JSON.stringify(store, null, 2) + "\n", { mode: 0o600 });
129
+ const secretB58 = Agent.new().secretB58;
130
+ store = { secretB58, created: new Date().toISOString() };
131
+ const onDisk = PASSPHRASE ? encryptSecret(secretB58) : store;
132
+ writeFileSync(KEYSTORE, JSON.stringify(onDisk, null, 2) + "\n", { mode: 0o600 });
103
133
  chmodSync(KEYSTORE, 0o600); // writeFileSync honours umask; this does not
104
- process.stdout.write(`Created ${KEYSTORE} (mode 600).\n\n`);
134
+ process.stdout.write(
135
+ PASSPHRASE
136
+ ? `Created ${KEYSTORE} (mode 600, ENCRYPTED). Keep AIFINPAY_WALLET_PASSPHRASE — the wallet is unrecoverable without it.\n\n`
137
+ : `Created ${KEYSTORE} (mode 600, plaintext). For at-rest encryption, set AIFINPAY_WALLET_PASSPHRASE before init.\n\n`,
138
+ );
105
139
  }
106
140
 
107
141
  const agent = await AiFinPayAgent.fromSolanaSecret(store.secretB58);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aifinpay/mcp",
3
- "version": "2.0.0-rc.3",
3
+ "version": "2.0.0-rc.4",
4
4
  "description": "AiFinPay MCP production-RC control surface for Agent Passport resolution and runtime-verified AIFP-1/AIFP-2 settlement invoices. Signing tools remain gated until @aifinpay/agent v2 release.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",