@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.
- package/bin/aifinpay-mcp.js +43 -9
- package/package.json +1 -1
package/bin/aifinpay-mcp.js
CHANGED
|
@@ -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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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(
|
|
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
|
+
"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",
|