wolverine-ai 3.7.9 → 3.8.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wolverine-ai",
3
- "version": "3.7.9",
3
+ "version": "3.8.1",
4
4
  "description": "Self-healing Node.js server framework powered by AI. Catches crashes, diagnoses errors, generates fixes, verifies, and restarts — automatically.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -32,6 +32,8 @@ const NEVER_ROLLBACK = [
32
32
  "server/lib/redis.js",
33
33
  ".env",
34
34
  ".env.local",
35
+ ".wolverine/vault/master.key",
36
+ ".wolverine/vault/eth.vault",
35
37
  ];
36
38
 
37
39
  class BackupManager {
@@ -78,6 +80,19 @@ class BackupManager {
78
80
  });
79
81
  }
80
82
 
83
+ // Also back up vault keys (catastrophic recovery)
84
+ try {
85
+ const vaultDir = path.join(this.projectRoot, ".wolverine", "vault");
86
+ for (const vaultFile of ["master.key", "eth.vault"]) {
87
+ const src = path.join(vaultDir, vaultFile);
88
+ if (fs.existsSync(src)) {
89
+ const dest = path.join(backupDir, ".wolverine__vault__" + vaultFile);
90
+ fs.copyFileSync(src, dest);
91
+ files.push({ original: src, relative: `.wolverine/vault/${vaultFile}`, backup: dest });
92
+ }
93
+ }
94
+ } catch {}
95
+
81
96
  const entry = {
82
97
  id: backupId,
83
98
  timestamp,
@@ -177,6 +177,10 @@ const SEED_DOCS = [
177
177
  text: "Notifications: detects human-required errors (expired keys, billing, service down, certs, permissions, disk). Classifies errors as AI-fixable vs human-required using pattern matching. Generates AI summary (CHAT_MODEL). Fires before wasting tokens on repair. Console alert + dashboard event + optional webhook. Categories: auth, billing, service, cert, permission, disk.",
178
178
  metadata: { topic: "notifications" },
179
179
  },
180
+ {
181
+ text: "Vault: encrypted key storage in .wolverine/vault/. AES-256-GCM encryption. master.key (32 bytes raw) encrypts eth.vault (Ethereum private key). Generated on first run if missing. Private key NEVER exists as a JS string — only Buffer, wiped after use. wallet-ops.js exposes getWalletAddress(), signTransaction(), signMessage() — all decrypt→use→wipe with generic error messages only. Injection detector blocks heal if 0x+64hex chars detected in error (key_leak_critical). Redactor scrubs all hex key patterns. Sandbox blocks agent access to vault paths. Backed up in every snapshot. Rollback-protected (NEVER_ROLLBACK list). Vault skill in src/skills/vault.js for agent discovery. Server code uses vault skill to earn/spend ETH without touching the private key.",
182
+ metadata: { topic: "vault-security" },
183
+ },
180
184
  {
181
185
  text: "MCP integration: connect external tools via Model Context Protocol. Configure in .wolverine/mcp.json with per-server tool allowlists. Security: arg sanitization (secrets redacted before sending to MCP servers), result injection scanning, rate limiting per server, audit logging. Tools appear as mcp__server__tool in the agent. Supports stdio and HTTP transports.",
182
186
  metadata: { topic: "mcp" },
@@ -194,6 +194,23 @@ class WolverineRunner {
194
194
  console.log(chalk.yellow(` ⚠️ MCP init failed (non-fatal): ${err.message}`));
195
195
  }
196
196
 
197
+ // Initialize vault (encrypted key storage)
198
+ try {
199
+ const { initVault, isVaultInitialized } = require("../vault/vault-manager");
200
+ const vaultResult = await initVault();
201
+ if (vaultResult.created) {
202
+ try {
203
+ const { getWalletAddress } = require("../vault/wallet-ops");
204
+ const addr = await getWalletAddress();
205
+ console.log(chalk.green(` 🔐 Vault initialized — wallet: ${addr}`));
206
+ } catch { console.log(chalk.green(" 🔐 Vault initialized")); }
207
+ } else if (isVaultInitialized()) {
208
+ console.log(chalk.gray(" 🔐 Vault: ready"));
209
+ }
210
+ } catch (err) {
211
+ console.log(chalk.yellow(` ⚠️ Vault init failed (non-fatal): ${err.message}`));
212
+ }
213
+
197
214
  // Log redactor stats
198
215
  const redactorStats = this.redactor.getStats();
199
216
  console.log(chalk.gray(` 🔐 Secret redactor: ${redactorStats.trackedSecrets} secrets tracked from ${redactorStats.envFiles} env file(s)`));
@@ -57,9 +57,9 @@ const HUMAN_REQUIRED_PATTERNS = [
57
57
  { pattern: /EACCES/i, category: "permission", hint: "Permission denied on file system" },
58
58
  { pattern: /EPERM/i, category: "permission", hint: "Operation not permitted — check file/process permissions" },
59
59
 
60
- // Environment
60
+ // Environment — only match system-level env issues, not app config files the agent can create
61
61
  { pattern: /not\s+set|undefined.*env|missing.*env/i, category: "env", hint: "Environment variable not configured" },
62
- { pattern: /missing.*config/i, category: "env", hint: "Configuration file or value missing" },
62
+ { pattern: /missing.*(\.env|environment|env\s*var)/i, category: "env", hint: "Environment variable or .env file missing" },
63
63
 
64
64
  // Disk
65
65
  { pattern: /ENOSPC/i, category: "disk", hint: "Disk space full" },
@@ -41,6 +41,9 @@ const INJECTION_PATTERNS = [
41
41
  { pattern: /fs\.(unlink|rmdir|rm)Sync/i, label: "destructive-fs" },
42
42
  { pattern: /rimraf/i, label: "destructive-fs" },
43
43
  { pattern: /rm\s+-rf/i, label: "destructive-fs" },
44
+ // Vault key material leak — CRITICAL: block heal entirely
45
+ { pattern: /0x[0-9a-fA-F]{64}/i, label: "key-leak-critical" },
46
+ { pattern: /master\.key|eth\.vault|\.wolverine\/vault/i, label: "vault-path-leak" },
44
47
  ];
45
48
 
46
49
  /**
@@ -12,11 +12,27 @@ class Sandbox {
12
12
  this.rootDir = path.resolve(rootDir);
13
13
  }
14
14
 
15
+ // Paths the agent can NEVER read, write, or delete — even within the sandbox
16
+ static PROTECTED_PATHS = [
17
+ ".wolverine/vault/master.key",
18
+ ".wolverine/vault/eth.vault",
19
+ ".wolverine/vault",
20
+ ];
21
+
15
22
  /**
16
- * Resolve and validate a file path. Throws if the path escapes the sandbox.
23
+ * Resolve and validate a file path. Throws if the path escapes the sandbox
24
+ * or targets a protected vault path.
17
25
  * Returns the resolved absolute path.
18
26
  */
19
27
  resolve(filePath) {
28
+ // Block vault access before any resolution
29
+ const normalized = filePath.replace(/\\/g, "/").toLowerCase();
30
+ for (const p of Sandbox.PROTECTED_PATHS) {
31
+ if (normalized.includes(p.toLowerCase())) {
32
+ throw new SandboxViolationError(`Access denied: "${filePath}" is a protected vault path`);
33
+ }
34
+ }
35
+
20
36
  // Resolve relative to sandbox root
21
37
  const resolved = path.isAbsolute(filePath)
22
38
  ? path.resolve(filePath)
@@ -32,6 +48,13 @@ class Sandbox {
32
48
  );
33
49
  }
34
50
 
51
+ // Double-check resolved path against vault
52
+ for (const p of Sandbox.PROTECTED_PATHS) {
53
+ if (normalizedResolved.includes(p.toLowerCase())) {
54
+ throw new SandboxViolationError(`Access denied: resolved path targets protected vault`);
55
+ }
56
+ }
57
+
35
58
  // Check for symlink escape
36
59
  if (fs.existsSync(resolved)) {
37
60
  const real = fs.realpathSync(resolved);
@@ -40,6 +40,11 @@ const SECRET_PATTERNS = [
40
40
  { pattern: /(?:password|secret|token|key|credential|auth)['"`]?\s*[:=]\s*['"`]([a-zA-Z0-9+/=_-]{32,})['"`]/gi, label: null }, // handled specially
41
41
  // JWT tokens
42
42
  { pattern: /eyJ[a-zA-Z0-9_-]{10,}\.eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}/g, label: "[REDACTED_JWT]" },
43
+ // Vault: Ethereum private keys and encryption key material
44
+ { pattern: /0x[0-9a-fA-F]{64}/g, label: "[REDACTED_PRIVATE_KEY]" },
45
+ { pattern: /(?<![0-9a-fA-F])[0-9a-fA-F]{64}(?![0-9a-fA-F])/g, label: "[REDACTED_HEX_KEY]" },
46
+ // Vault file paths — prevent path disclosure that could guide file reads
47
+ { pattern: /master\.key|eth\.vault|\.wolverine\/vault/g, label: "[VAULT_REDACTED]" },
43
48
  ];
44
49
 
45
50
  class SecretRedactor {
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Vault Skill — secure wallet operations for the agent and server.
3
+ *
4
+ * Exposes wallet functionality (sign, address, status) through the
5
+ * skill registry so agents and server code can use the wallet WITHOUT
6
+ * ever touching the private key directly.
7
+ *
8
+ * The private key is decrypted in-memory, used, and wiped. Error messages
9
+ * are always generic to prevent key leakage through the AI repair pipeline.
10
+ */
11
+
12
+ const SKILL_NAME = "vault";
13
+ const SKILL_DESCRIPTION = "Secure Ethereum wallet — sign transactions, get address, check balance. Private key encrypted at rest, never exposed in code or errors.";
14
+ const SKILL_KEYWORDS = ["wallet", "ethereum", "eth", "sign", "transaction", "vault", "private key", "address", "crypto", "blockchain", "send", "transfer"];
15
+ const SKILL_USAGE = `
16
+ vault.status() — check if vault is initialized, show address
17
+ vault.address() — get the wallet's Ethereum address
18
+ vault.sign_tx(tx) — sign a transaction { to, value, chainId, nonce, gasLimit }
19
+ vault.sign_message(msg) — sign an arbitrary message
20
+ vault.public_key() — get compressed public key hex
21
+
22
+ SECURITY: The private key never leaves the vault. All operations decrypt
23
+ in-memory, use, and wipe. Error messages are generic — no key material
24
+ can leak into the AI repair pipeline.
25
+ `;
26
+
27
+ /**
28
+ * Execute a vault action. Called by skill registry when matched.
29
+ * @param {string} action — "status" | "address" | "sign_tx" | "sign_message" | "public_key"
30
+ * @param {object} params — action-specific parameters
31
+ */
32
+ async function execute(action, params = {}) {
33
+ const { getWalletAddress, getPublicKeyHex, signMessage, signTransaction, getVaultStatus } = require("../vault/wallet-ops");
34
+
35
+ switch (action) {
36
+ case "status": {
37
+ const status = getVaultStatus();
38
+ if (status.initialized) {
39
+ try {
40
+ status.address = await getWalletAddress();
41
+ } catch {
42
+ status.address = null;
43
+ }
44
+ }
45
+ return status;
46
+ }
47
+
48
+ case "address":
49
+ return { address: await getWalletAddress() };
50
+
51
+ case "public_key":
52
+ return { publicKey: await getPublicKeyHex() };
53
+
54
+ case "sign_message":
55
+ if (!params.message) return { error: "message required" };
56
+ return { signature: await signMessage(params.message) };
57
+
58
+ case "sign_tx": {
59
+ if (!params.to || !params.chainId) return { error: "to and chainId required" };
60
+ const signed = await signTransaction(params);
61
+ return { signedTransaction: signed };
62
+ }
63
+
64
+ default:
65
+ return { error: `Unknown vault action: ${action}. Use: status, address, sign_tx, sign_message, public_key` };
66
+ }
67
+ }
68
+
69
+ module.exports = { SKILL_NAME, SKILL_DESCRIPTION, SKILL_KEYWORDS, SKILL_USAGE, execute };
@@ -0,0 +1,185 @@
1
+ const crypto = require("crypto");
2
+ const fs = require("fs");
3
+ const path = require("path");
4
+
5
+ /**
6
+ * Vault Manager — encrypted key storage for the wolverine framework.
7
+ *
8
+ * Two files in .wolverine/vault/:
9
+ * master.key — 32 bytes raw AES-256 key (chmod 0600)
10
+ * eth.vault — JSON with AES-256-GCM encrypted Ethereum private key
11
+ *
12
+ * Design principles:
13
+ * - Private keys NEVER exist as JavaScript strings (only Buffers, wipe-able)
14
+ * - Generic errors only — wallet-ops swallows details before they reach AI
15
+ * - master.key is the single secret on disk — everything else is encrypted
16
+ * - Survives git pull, npm install, auto-update (lives in .wolverine/)
17
+ */
18
+
19
+ const VAULT_DIR = () => path.join(process.cwd(), ".wolverine", "vault");
20
+ const MASTER_KEY_PATH = () => path.join(VAULT_DIR(), "master.key");
21
+ const ETH_VAULT_PATH = () => path.join(VAULT_DIR(), "eth.vault");
22
+
23
+ const ALGORITHM = "aes-256-gcm";
24
+ const IV_LENGTH = 16;
25
+ const AUTH_TAG_LENGTH = 16;
26
+
27
+ /**
28
+ * Initialize the vault. Idempotent — creates keys only if missing.
29
+ * Called during runner startup before any server code runs.
30
+ */
31
+ async function initVault() {
32
+ const vaultDir = VAULT_DIR();
33
+ fs.mkdirSync(vaultDir, { recursive: true });
34
+
35
+ let created = false;
36
+
37
+ // Master encryption key
38
+ if (!fs.existsSync(MASTER_KEY_PATH())) {
39
+ const masterKey = crypto.randomBytes(32);
40
+ fs.writeFileSync(MASTER_KEY_PATH(), masterKey);
41
+ try { fs.chmodSync(MASTER_KEY_PATH(), 0o600); } catch {}
42
+ masterKey.fill(0);
43
+ created = true;
44
+ console.log(" 🔐 Vault: master encryption key generated");
45
+ }
46
+
47
+ // Ethereum private key (encrypted)
48
+ if (!fs.existsSync(ETH_VAULT_PATH())) {
49
+ const ethKey = crypto.randomBytes(32);
50
+ await encryptAndStore(ethKey);
51
+ ethKey.fill(0);
52
+ created = true;
53
+ console.log(" 🔐 Vault: ethereum wallet created");
54
+ }
55
+
56
+ return { created };
57
+ }
58
+
59
+ /**
60
+ * Check if vault is fully initialized.
61
+ */
62
+ function isVaultInitialized() {
63
+ return fs.existsSync(MASTER_KEY_PATH()) && fs.existsSync(ETH_VAULT_PATH());
64
+ }
65
+
66
+ /**
67
+ * Encrypt a private key Buffer and write to eth.vault.
68
+ * Wipes the master key from memory after use.
69
+ */
70
+ async function encryptAndStore(keyBuf) {
71
+ let masterKey = null;
72
+ try {
73
+ masterKey = fs.readFileSync(MASTER_KEY_PATH());
74
+ const iv = crypto.randomBytes(IV_LENGTH);
75
+ const cipher = crypto.createCipheriv(ALGORITHM, masterKey, iv);
76
+ const encrypted = Buffer.concat([cipher.update(keyBuf), cipher.final()]);
77
+ const authTag = cipher.getAuthTag();
78
+
79
+ const vault = {
80
+ version: 1,
81
+ algorithm: ALGORITHM,
82
+ iv: iv.toString("hex"),
83
+ authTag: authTag.toString("hex"),
84
+ ciphertext: encrypted.toString("hex"),
85
+ created: new Date().toISOString(),
86
+ rotated: null,
87
+ };
88
+
89
+ const tmpPath = ETH_VAULT_PATH() + ".tmp";
90
+ fs.writeFileSync(tmpPath, JSON.stringify(vault, null, 2), "utf-8");
91
+ fs.renameSync(tmpPath, ETH_VAULT_PATH());
92
+ try { fs.chmodSync(ETH_VAULT_PATH(), 0o600); } catch {}
93
+ } finally {
94
+ if (masterKey) masterKey.fill(0);
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Decrypt the Ethereum private key. Returns a Buffer.
100
+ * CALLER MUST call .fill(0) on the returned Buffer when done.
101
+ */
102
+ function decryptPrivateKey() {
103
+ if (!isVaultInitialized()) {
104
+ throw new Error("vault not initialized");
105
+ }
106
+
107
+ let masterKey = null;
108
+ try {
109
+ masterKey = fs.readFileSync(MASTER_KEY_PATH());
110
+ const vault = JSON.parse(fs.readFileSync(ETH_VAULT_PATH(), "utf-8"));
111
+
112
+ if (vault.version !== 1) throw new Error("unsupported vault version");
113
+
114
+ const iv = Buffer.from(vault.iv, "hex");
115
+ const authTag = Buffer.from(vault.authTag, "hex");
116
+ const ciphertext = Buffer.from(vault.ciphertext, "hex");
117
+
118
+ const decipher = crypto.createDecipheriv(ALGORITHM, masterKey, iv);
119
+ decipher.setAuthTag(authTag);
120
+
121
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
122
+ } finally {
123
+ if (masterKey) masterKey.fill(0);
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Re-encrypt with a fresh IV. Defensive measure if key material was
129
+ * potentially exposed in an error message.
130
+ */
131
+ async function rotateEncryption() {
132
+ let keyBuf = null;
133
+ try {
134
+ keyBuf = decryptPrivateKey();
135
+ await encryptAndStore(keyBuf);
136
+
137
+ // Update rotated timestamp
138
+ const vault = JSON.parse(fs.readFileSync(ETH_VAULT_PATH(), "utf-8"));
139
+ vault.rotated = new Date().toISOString();
140
+ fs.writeFileSync(ETH_VAULT_PATH(), JSON.stringify(vault, null, 2), "utf-8");
141
+ } finally {
142
+ if (keyBuf) keyBuf.fill(0);
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Export vault contents for backup. Returns raw Buffers.
148
+ * Caller MUST wipe masterKey after writing to backup.
149
+ */
150
+ function exportVaultForBackup() {
151
+ if (!isVaultInitialized()) return null;
152
+ return {
153
+ masterKey: fs.readFileSync(MASTER_KEY_PATH()),
154
+ vaultFile: fs.readFileSync(ETH_VAULT_PATH(), "utf-8"),
155
+ };
156
+ }
157
+
158
+ /**
159
+ * Import vault from backup. Only used during catastrophic recovery
160
+ * when both vault files are missing.
161
+ */
162
+ function importVaultFromBackup(masterKeyBuf, vaultFileStr) {
163
+ const vaultDir = VAULT_DIR();
164
+ fs.mkdirSync(vaultDir, { recursive: true });
165
+
166
+ fs.writeFileSync(MASTER_KEY_PATH(), masterKeyBuf);
167
+ try { fs.chmodSync(MASTER_KEY_PATH(), 0o600); } catch {}
168
+
169
+ fs.writeFileSync(ETH_VAULT_PATH(), vaultFileStr, "utf-8");
170
+ try { fs.chmodSync(ETH_VAULT_PATH(), 0o600); } catch {}
171
+ }
172
+
173
+ function getVaultPath() { return VAULT_DIR(); }
174
+
175
+ module.exports = {
176
+ initVault,
177
+ isVaultInitialized,
178
+ decryptPrivateKey,
179
+ rotateEncryption,
180
+ exportVaultForBackup,
181
+ importVaultFromBackup,
182
+ getVaultPath,
183
+ MASTER_KEY_PATH,
184
+ ETH_VAULT_PATH,
185
+ };
@@ -0,0 +1,167 @@
1
+ const { decryptPrivateKey } = require("./vault-manager");
2
+
3
+ /**
4
+ * Wallet Operations — safe Ethereum wallet interactions.
5
+ *
6
+ * Every function follows the same pattern:
7
+ * 1. Decrypt private key into Buffer
8
+ * 2. Use it (sign, derive address, etc.)
9
+ * 3. Wipe Buffer in finally block
10
+ * 4. Catch block swallows real error, throws generic message
11
+ *
12
+ * The private key NEVER exists as a JavaScript string.
13
+ * Error messages are always generic — no hex, no key material, no details
14
+ * that could leak through the AI repair pipeline.
15
+ */
16
+
17
+ /**
18
+ * Derive the Ethereum address from the vault's private key.
19
+ * Address is not secret — safe to return as string.
20
+ */
21
+ async function getWalletAddress() {
22
+ let keyBuf = null;
23
+ try {
24
+ keyBuf = decryptPrivateKey();
25
+ const { keccak256, getPublicKey } = _getCrypto();
26
+ const pubKey = getPublicKey(keyBuf);
27
+ const address = _pubKeyToAddress(pubKey, keccak256);
28
+ return address;
29
+ } catch {
30
+ throw new Error("wallet operation failed: could not derive address");
31
+ } finally {
32
+ if (keyBuf) keyBuf.fill(0);
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Get the compressed public key hex. Not secret.
38
+ */
39
+ async function getPublicKeyHex() {
40
+ let keyBuf = null;
41
+ try {
42
+ keyBuf = decryptPrivateKey();
43
+ const { getPublicKey } = _getCrypto();
44
+ const pubKey = getPublicKey(keyBuf, true); // compressed
45
+ return Buffer.from(pubKey).toString("hex");
46
+ } catch {
47
+ throw new Error("wallet operation failed: could not derive public key");
48
+ } finally {
49
+ if (keyBuf) keyBuf.fill(0);
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Sign an arbitrary message. Returns signature hex string.
55
+ * @param {string|Buffer} message — the message to sign
56
+ */
57
+ async function signMessage(message) {
58
+ let keyBuf = null;
59
+ try {
60
+ keyBuf = decryptPrivateKey();
61
+ const { keccak256, sign } = _getCrypto();
62
+ const msgBuf = typeof message === "string" ? Buffer.from(message) : message;
63
+ // Ethereum signed message prefix
64
+ const prefix = Buffer.from(`\x19Ethereum Signed Message:\n${msgBuf.length}`);
65
+ const hash = keccak256(Buffer.concat([prefix, msgBuf]));
66
+ const sig = sign(hash, keyBuf);
67
+ return sig;
68
+ } catch {
69
+ throw new Error("wallet operation failed: could not sign message");
70
+ } finally {
71
+ if (keyBuf) keyBuf.fill(0);
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Sign a transaction object. Returns signed transaction hex.
77
+ * @param {object} tx — { to, value, data?, nonce, gasLimit, gasPrice?, maxFeePerGas?, maxPriorityFeePerGas?, chainId }
78
+ */
79
+ async function signTransaction(tx) {
80
+ // Validate BEFORE decrypting — don't touch the key for bad input
81
+ if (!tx || typeof tx !== "object") throw new Error("wallet operation failed: invalid transaction");
82
+ if (!tx.to || !tx.chainId) throw new Error("wallet operation failed: missing required fields");
83
+
84
+ let keyBuf = null;
85
+ try {
86
+ keyBuf = decryptPrivateKey();
87
+ // Use ethers for tx serialization + signing, but feed it a raw signer
88
+ const { Wallet } = require("ethers");
89
+ const wallet = new Wallet(keyBuf);
90
+ const signed = await wallet.signTransaction(tx);
91
+ return signed;
92
+ } catch {
93
+ throw new Error("wallet operation failed: could not sign transaction");
94
+ } finally {
95
+ if (keyBuf) keyBuf.fill(0);
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Get vault status — safe metadata only, no key material.
101
+ */
102
+ function getVaultStatus() {
103
+ const { isVaultInitialized, getVaultPath } = require("./vault-manager");
104
+ const fs = require("fs");
105
+ const masterExists = fs.existsSync(require("./vault-manager").MASTER_KEY_PATH());
106
+ const ethExists = fs.existsSync(require("./vault-manager").ETH_VAULT_PATH());
107
+
108
+ return {
109
+ initialized: isVaultInitialized(),
110
+ vaultPath: getVaultPath(),
111
+ masterKeyExists: masterExists,
112
+ ethVaultExists: ethExists,
113
+ };
114
+ }
115
+
116
+ // ── Crypto helpers (use @noble/secp256k1 or built-in) ──
117
+
118
+ function _getCrypto() {
119
+ const crypto = require("crypto");
120
+
121
+ function keccak256(data) {
122
+ return crypto.createHash("sha3-256").update(data).digest();
123
+ }
124
+
125
+ function getPublicKey(privKeyBuf, compressed = false) {
126
+ const ecdh = crypto.createECDH("secp256k1");
127
+ ecdh.setPrivateKey(privKeyBuf);
128
+ return compressed ? ecdh.getPublicKey("buffer", "compressed") : ecdh.getPublicKey("buffer", "uncompressed");
129
+ }
130
+
131
+ function sign(hash, privKeyBuf) {
132
+ const sig = crypto.sign(null, hash, {
133
+ key: crypto.createPrivateKey({
134
+ key: Buffer.concat([
135
+ // DER-encode secp256k1 private key
136
+ Buffer.from("302e0201010420", "hex"),
137
+ privKeyBuf,
138
+ Buffer.from("a00706052b8104000a", "hex"),
139
+ ]),
140
+ format: "der",
141
+ type: "sec1",
142
+ }),
143
+ dsaEncoding: "ieee-p1363",
144
+ });
145
+ return sig.toString("hex");
146
+ }
147
+
148
+ return { keccak256, getPublicKey, sign };
149
+ }
150
+
151
+ function _pubKeyToAddress(uncompressedPubKey, keccak256) {
152
+ // Skip the 0x04 prefix byte, hash the rest, take last 20 bytes
153
+ const pubBytes = uncompressedPubKey.slice(1);
154
+ const hash = keccak256(pubBytes);
155
+ const addrBytes = hash.slice(hash.length - 20);
156
+ const addr = "0x" + addrBytes.toString("hex");
157
+ // EIP-55 checksum
158
+ const addrLower = addr.slice(2).toLowerCase();
159
+ const hashHex = keccak256(Buffer.from(addrLower)).toString("hex");
160
+ let checksummed = "0x";
161
+ for (let i = 0; i < 40; i++) {
162
+ checksummed += parseInt(hashHex[i], 16) >= 8 ? addrLower[i].toUpperCase() : addrLower[i];
163
+ }
164
+ return checksummed;
165
+ }
166
+
167
+ module.exports = { getWalletAddress, getPublicKeyHex, signMessage, signTransaction, getVaultStatus };