@aifinpay/mcp 2.0.0-rc.3 → 2.0.0-rc.5
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 +67 -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,20 +78,48 @@ 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") {
|
|
91
119
|
const { Agent, AiFinPayAgent } = await import("@aifinpay/agent");
|
|
92
120
|
|
|
93
121
|
let store = readKeystore();
|
|
122
|
+
const freshlyCreated = !store;
|
|
94
123
|
if (store) {
|
|
95
124
|
// Never silently overwrite. The file is the only copy of a key that may
|
|
96
125
|
// already hold funds; a second `init` that regenerated it would destroy a
|
|
@@ -98,10 +127,39 @@ if (arg === "init") {
|
|
|
98
127
|
process.stdout.write(`Existing wallet found at ${KEYSTORE} — keeping it.\n\n`);
|
|
99
128
|
} else {
|
|
100
129
|
mkdirSync(HOME, { recursive: true, mode: 0o700 });
|
|
101
|
-
|
|
102
|
-
|
|
130
|
+
const secretB58 = Agent.new().secretB58;
|
|
131
|
+
store = { secretB58, created: new Date().toISOString() };
|
|
132
|
+
const onDisk = PASSPHRASE ? encryptSecret(secretB58) : store;
|
|
133
|
+
writeFileSync(KEYSTORE, JSON.stringify(onDisk, null, 2) + "\n", { mode: 0o600 });
|
|
103
134
|
chmodSync(KEYSTORE, 0o600); // writeFileSync honours umask; this does not
|
|
104
|
-
process.stdout.write(
|
|
135
|
+
process.stdout.write(
|
|
136
|
+
PASSPHRASE
|
|
137
|
+
? `Created ${KEYSTORE} (mode 600, ENCRYPTED). Keep AIFINPAY_WALLET_PASSPHRASE — the wallet is unrecoverable without it.\n\n`
|
|
138
|
+
: `Created ${KEYSTORE} (mode 600, plaintext). For at-rest encryption, set AIFINPAY_WALLET_PASSPHRASE before init.\n\n`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// The one-time recovery output.
|
|
143
|
+
//
|
|
144
|
+
// Shown ONLY on creation, ONLY in the terminal, and ONLY when the keystore is
|
|
145
|
+
// plaintext. This is NOT the leak the audit (AIFINP-220 §3) forbids — that is
|
|
146
|
+
// a key written to CHAT or LOGS, where an LLM provider or a log shipper keeps
|
|
147
|
+
// it forever. This is the deliberate one-time backup prompt every wallet CLI
|
|
148
|
+
// shows, on a channel the user controls. Opposite things: the owner reading
|
|
149
|
+
// their own recovery phrase once, vs a secret leaking into a transcript.
|
|
150
|
+
//
|
|
151
|
+
// Encrypted keystores print nothing here: recovery there IS the keystore file
|
|
152
|
+
// plus the passphrase, and re-printing the plaintext secret would defeat the
|
|
153
|
+
// encryption the user just asked for.
|
|
154
|
+
if (freshlyCreated && !PASSPHRASE) {
|
|
155
|
+
process.stdout.write(
|
|
156
|
+
`\n RECOVERY KEY (shown once, never again):\n\n` +
|
|
157
|
+
` ${store.secretB58}\n\n` +
|
|
158
|
+
` Save this off this machine NOW. Anyone with it controls the wallet and\n` +
|
|
159
|
+
` its funds. Do NOT paste it into a chat, an issue, or a config file — the\n` +
|
|
160
|
+
` keystore at ${KEYSTORE} already holds it (encrypt it with\n` +
|
|
161
|
+
` AIFINPAY_WALLET_PASSPHRASE). This line is your OFF-machine backup.\n\n`,
|
|
162
|
+
);
|
|
105
163
|
}
|
|
106
164
|
|
|
107
165
|
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.5",
|
|
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",
|