@ametyst/cli 0.2.0
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/README.md +36 -0
- package/ametyst.node +0 -0
- package/dist/index.js +113898 -0
- package/index.d.ts +9 -0
- package/native/Cargo.lock +1123 -0
- package/native/Cargo.toml +25 -0
- package/native/ametyst-vault.node +0 -0
- package/native/build.rs +4 -0
- package/native/index.d.ts +9 -0
- package/native/package.json +9 -0
- package/native/src/.gitkeep +1 -0
- package/native/src/lib.rs +229 -0
- package/package.json +76 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "ametyst-vault"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
|
|
6
|
+
[lib]
|
|
7
|
+
crate-type = ["cdylib"]
|
|
8
|
+
|
|
9
|
+
[dependencies]
|
|
10
|
+
napi = { version = "2", features = ["napi8"] }
|
|
11
|
+
napi-derive = "2"
|
|
12
|
+
aes-gcm = "0.10"
|
|
13
|
+
scrypt = { version = "0.11", features = ["simple"] }
|
|
14
|
+
zeroize = { version = "1", features = ["derive"] }
|
|
15
|
+
libc = "0.2"
|
|
16
|
+
rand = "0.8"
|
|
17
|
+
uuid = { version = "1", features = ["v4"] }
|
|
18
|
+
serde = { version = "1", features = ["derive"] }
|
|
19
|
+
serde_json = "1"
|
|
20
|
+
hex = "0.4"
|
|
21
|
+
k256 = { version = "0.13", features = ["ecdsa"] }
|
|
22
|
+
sha3 = "0.10"
|
|
23
|
+
|
|
24
|
+
[build-dependencies]
|
|
25
|
+
napi-build = "2"
|
|
Binary file
|
package/native/build.rs
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/* tslint:disable */
|
|
2
|
+
/* eslint-disable */
|
|
3
|
+
|
|
4
|
+
/* auto-generated by NAPI-RS */
|
|
5
|
+
|
|
6
|
+
export declare function hardenProcess(): void
|
|
7
|
+
export declare function createWallet(passphrase: string): string
|
|
8
|
+
export declare function decryptWallet(keystoreJson: string, passphrase: string): string
|
|
9
|
+
export declare function getWalletAddress(keystoreJson: string): string
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
use aes_gcm::aead::{Aead, KeyInit};
|
|
2
|
+
use aes_gcm::{Aes256Gcm, Nonce};
|
|
3
|
+
use k256::ecdsa::SigningKey;
|
|
4
|
+
use libc::{c_void, mlock, munlock, rlimit, setrlimit, RLIMIT_CORE};
|
|
5
|
+
use napi_derive::napi;
|
|
6
|
+
use rand::rngs::OsRng;
|
|
7
|
+
use rand::RngCore;
|
|
8
|
+
use scrypt::{scrypt, Params};
|
|
9
|
+
use serde::{Deserialize, Serialize};
|
|
10
|
+
use sha3::{Digest, Keccak256};
|
|
11
|
+
use std::ptr;
|
|
12
|
+
use uuid::Uuid;
|
|
13
|
+
use zeroize::Zeroize;
|
|
14
|
+
|
|
15
|
+
const SCRYPT_N_LOG2: u8 = 18;
|
|
16
|
+
const SCRYPT_R: u32 = 8;
|
|
17
|
+
const SCRYPT_P: u32 = 1;
|
|
18
|
+
const SCRYPT_DKLEN: usize = 32;
|
|
19
|
+
|
|
20
|
+
struct SecretBytes {
|
|
21
|
+
bytes: Vec<u8>,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
impl SecretBytes {
|
|
25
|
+
fn new(bytes: Vec<u8>) -> Self {
|
|
26
|
+
let secret = Self { bytes };
|
|
27
|
+
let ptr = secret.bytes.as_ptr() as *const c_void;
|
|
28
|
+
let len = secret.bytes.len();
|
|
29
|
+
unsafe {
|
|
30
|
+
let _ = mlock(ptr, len);
|
|
31
|
+
}
|
|
32
|
+
secret
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
fn as_slice(&self) -> &[u8] {
|
|
36
|
+
&self.bytes
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
fn to_vec(&self) -> Vec<u8> {
|
|
40
|
+
self.bytes.clone()
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
impl Drop for SecretBytes {
|
|
45
|
+
fn drop(&mut self) {
|
|
46
|
+
let ptr = self.bytes.as_ptr() as *const c_void;
|
|
47
|
+
let len = self.bytes.len();
|
|
48
|
+
self.bytes.zeroize();
|
|
49
|
+
unsafe {
|
|
50
|
+
let _ = munlock(ptr, len);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#[derive(Serialize, Deserialize)]
|
|
56
|
+
struct Keystore {
|
|
57
|
+
version: u8,
|
|
58
|
+
id: String,
|
|
59
|
+
address: String,
|
|
60
|
+
crypto: Crypto,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#[derive(Serialize, Deserialize)]
|
|
64
|
+
struct Crypto {
|
|
65
|
+
cipher: String,
|
|
66
|
+
ciphertext: String,
|
|
67
|
+
cipherparams: CipherParams,
|
|
68
|
+
kdf: String,
|
|
69
|
+
kdfparams: KdfParams,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#[derive(Serialize, Deserialize)]
|
|
73
|
+
struct CipherParams {
|
|
74
|
+
iv: String,
|
|
75
|
+
tag: String,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
#[derive(Serialize, Deserialize)]
|
|
79
|
+
struct KdfParams {
|
|
80
|
+
n: u32,
|
|
81
|
+
r: u32,
|
|
82
|
+
p: u32,
|
|
83
|
+
dklen: usize,
|
|
84
|
+
salt: String,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
fn derive_address_from_private_key(private_key: &[u8]) -> napi::Result<String> {
|
|
88
|
+
let signing_key = SigningKey::from_slice(private_key)
|
|
89
|
+
.map_err(|e| napi::Error::from_reason(format!("invalid private key: {e}")))?;
|
|
90
|
+
let verify_key = signing_key.verifying_key();
|
|
91
|
+
let encoded = verify_key.to_encoded_point(false);
|
|
92
|
+
let pubkey = encoded.as_bytes();
|
|
93
|
+
let mut hasher = Keccak256::new();
|
|
94
|
+
hasher.update(&pubkey[1..]);
|
|
95
|
+
let hash = hasher.finalize();
|
|
96
|
+
let address = &hash[12..];
|
|
97
|
+
Ok(format!("0x{}", hex::encode(address)))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
#[napi(js_name = "hardenProcess")]
|
|
101
|
+
pub fn harden_process() -> napi::Result<()> {
|
|
102
|
+
#[cfg(target_os = "macos")]
|
|
103
|
+
unsafe {
|
|
104
|
+
const PT_DENY_ATTACH: i32 = 31;
|
|
105
|
+
libc::ptrace(PT_DENY_ATTACH, 0, ptr::null_mut(), 0);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#[cfg(target_os = "linux")]
|
|
109
|
+
unsafe {
|
|
110
|
+
libc::prctl(libc::PR_SET_DUMPABLE, 0, 0, 0, 0);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let lim = rlimit {
|
|
114
|
+
rlim_cur: 0,
|
|
115
|
+
rlim_max: 0,
|
|
116
|
+
};
|
|
117
|
+
unsafe {
|
|
118
|
+
setrlimit(RLIMIT_CORE, &lim);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
Ok(())
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
#[napi(js_name = "createWallet")]
|
|
125
|
+
pub fn create_wallet(passphrase: String) -> napi::Result<String> {
|
|
126
|
+
let mut private_key = [0u8; 32];
|
|
127
|
+
OsRng.fill_bytes(&mut private_key);
|
|
128
|
+
let secret_private_key = SecretBytes::new(private_key.to_vec());
|
|
129
|
+
private_key.zeroize();
|
|
130
|
+
|
|
131
|
+
let address = derive_address_from_private_key(secret_private_key.as_slice())?;
|
|
132
|
+
|
|
133
|
+
let mut salt = [0u8; 32];
|
|
134
|
+
OsRng.fill_bytes(&mut salt);
|
|
135
|
+
let params = Params::new(SCRYPT_N_LOG2, SCRYPT_R, SCRYPT_P, SCRYPT_DKLEN)
|
|
136
|
+
.map_err(|e| napi::Error::from_reason(format!("scrypt params error: {e}")))?;
|
|
137
|
+
let mut derived_key = [0u8; SCRYPT_DKLEN];
|
|
138
|
+
scrypt(passphrase.as_bytes(), &salt, ¶ms, &mut derived_key)
|
|
139
|
+
.map_err(|e| napi::Error::from_reason(format!("scrypt failed: {e}")))?;
|
|
140
|
+
let secret_derived_key = SecretBytes::new(derived_key.to_vec());
|
|
141
|
+
derived_key.zeroize();
|
|
142
|
+
|
|
143
|
+
let mut iv = [0u8; 12];
|
|
144
|
+
OsRng.fill_bytes(&mut iv);
|
|
145
|
+
let cipher = Aes256Gcm::new_from_slice(secret_derived_key.as_slice())
|
|
146
|
+
.map_err(|e| napi::Error::from_reason(format!("cipher init failed: {e}")))?;
|
|
147
|
+
let encrypted = cipher
|
|
148
|
+
.encrypt(Nonce::from_slice(&iv), secret_private_key.as_slice())
|
|
149
|
+
.map_err(|e| napi::Error::from_reason(format!("encrypt failed: {e}")))?;
|
|
150
|
+
|
|
151
|
+
if encrypted.len() < 16 {
|
|
152
|
+
return Err(napi::Error::from_reason("encrypted payload too short"));
|
|
153
|
+
}
|
|
154
|
+
let split = encrypted.len() - 16;
|
|
155
|
+
let ciphertext = &encrypted[..split];
|
|
156
|
+
let tag = &encrypted[split..];
|
|
157
|
+
|
|
158
|
+
let keystore = Keystore {
|
|
159
|
+
version: 3,
|
|
160
|
+
id: Uuid::new_v4().to_string(),
|
|
161
|
+
address,
|
|
162
|
+
crypto: Crypto {
|
|
163
|
+
cipher: "aes-256-gcm".to_string(),
|
|
164
|
+
ciphertext: hex::encode(ciphertext),
|
|
165
|
+
cipherparams: CipherParams {
|
|
166
|
+
iv: hex::encode(iv),
|
|
167
|
+
tag: hex::encode(tag),
|
|
168
|
+
},
|
|
169
|
+
kdf: "scrypt".to_string(),
|
|
170
|
+
kdfparams: KdfParams {
|
|
171
|
+
n: 1 << SCRYPT_N_LOG2,
|
|
172
|
+
r: SCRYPT_R,
|
|
173
|
+
p: SCRYPT_P,
|
|
174
|
+
dklen: SCRYPT_DKLEN,
|
|
175
|
+
salt: hex::encode(salt),
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
serde_json::to_string(&keystore)
|
|
181
|
+
.map_err(|e| napi::Error::from_reason(format!("keystore serialization failed: {e}")))
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
#[napi(js_name = "decryptWallet")]
|
|
185
|
+
pub fn decrypt_wallet(keystore_json: String, passphrase: String) -> napi::Result<String> {
|
|
186
|
+
let keystore: Keystore = serde_json::from_str(&keystore_json)
|
|
187
|
+
.map_err(|e| napi::Error::from_reason(format!("invalid keystore json: {e}")))?;
|
|
188
|
+
|
|
189
|
+
let salt = hex::decode(&keystore.crypto.kdfparams.salt)
|
|
190
|
+
.map_err(|e| napi::Error::from_reason(format!("invalid salt: {e}")))?;
|
|
191
|
+
let params = Params::new(
|
|
192
|
+
(32 - keystore.crypto.kdfparams.n.leading_zeros() - 1) as u8,
|
|
193
|
+
keystore.crypto.kdfparams.r,
|
|
194
|
+
keystore.crypto.kdfparams.p,
|
|
195
|
+
keystore.crypto.kdfparams.dklen,
|
|
196
|
+
)
|
|
197
|
+
.map_err(|e| napi::Error::from_reason(format!("invalid scrypt params: {e}")))?;
|
|
198
|
+
|
|
199
|
+
let mut derived_key = vec![0u8; keystore.crypto.kdfparams.dklen];
|
|
200
|
+
scrypt(passphrase.as_bytes(), &salt, ¶ms, &mut derived_key)
|
|
201
|
+
.map_err(|e| napi::Error::from_reason(format!("scrypt failed: {e}")))?;
|
|
202
|
+
let secret_derived_key = SecretBytes::new(derived_key.clone());
|
|
203
|
+
derived_key.zeroize();
|
|
204
|
+
|
|
205
|
+
let iv = hex::decode(&keystore.crypto.cipherparams.iv)
|
|
206
|
+
.map_err(|e| napi::Error::from_reason(format!("invalid iv: {e}")))?;
|
|
207
|
+
let tag = hex::decode(&keystore.crypto.cipherparams.tag)
|
|
208
|
+
.map_err(|e| napi::Error::from_reason(format!("invalid tag: {e}")))?;
|
|
209
|
+
let ciphertext = hex::decode(&keystore.crypto.ciphertext)
|
|
210
|
+
.map_err(|e| napi::Error::from_reason(format!("invalid ciphertext: {e}")))?;
|
|
211
|
+
|
|
212
|
+
let mut payload = ciphertext;
|
|
213
|
+
payload.extend_from_slice(&tag);
|
|
214
|
+
let cipher = Aes256Gcm::new_from_slice(secret_derived_key.as_slice())
|
|
215
|
+
.map_err(|e| napi::Error::from_reason(format!("cipher init failed: {e}")))?;
|
|
216
|
+
let decrypted = cipher
|
|
217
|
+
.decrypt(Nonce::from_slice(&iv), payload.as_ref())
|
|
218
|
+
.map_err(|e| napi::Error::from_reason(format!("decrypt failed: {e}")))?;
|
|
219
|
+
let secret_private_key = SecretBytes::new(decrypted);
|
|
220
|
+
|
|
221
|
+
Ok(format!("0x{}", hex::encode(secret_private_key.to_vec())))
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
#[napi(js_name = "getWalletAddress")]
|
|
225
|
+
pub fn get_wallet_address(keystore_json: String) -> napi::Result<String> {
|
|
226
|
+
let keystore: Keystore = serde_json::from_str(&keystore_json)
|
|
227
|
+
.map_err(|e| napi::Error::from_reason(format!("invalid keystore json: {e}")))?;
|
|
228
|
+
Ok(keystore.address)
|
|
229
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ametyst/cli",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Ametyst CLI — embedded MCP server, wallet ops, agent payments",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"ametyst": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "dist/index.js",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"ametyst.node",
|
|
14
|
+
"index.d.ts",
|
|
15
|
+
"native"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=20",
|
|
19
|
+
"pnpm": ">=9"
|
|
20
|
+
},
|
|
21
|
+
"napi": {
|
|
22
|
+
"name": "ametyst",
|
|
23
|
+
"package": {
|
|
24
|
+
"name": "@ametyst-dev/cli"
|
|
25
|
+
},
|
|
26
|
+
"triples": {
|
|
27
|
+
"defaults": false,
|
|
28
|
+
"additional": [
|
|
29
|
+
"aarch64-apple-darwin",
|
|
30
|
+
"x86_64-apple-darwin",
|
|
31
|
+
"x86_64-unknown-linux-gnu"
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@modelcontextprotocol/sdk": "1.29.0",
|
|
37
|
+
"@napi-rs/keyring": "1.1.7",
|
|
38
|
+
"mcp-use": "1.24.2"
|
|
39
|
+
},
|
|
40
|
+
"_comment_optionalDependencies": "Injected at publish-time by publish-staging.yml / publish-prod.yml via `npm pkg set` with the version being published. Not committed here so pnpm install (locally + CI) doesn't try to resolve sub-package versions that may not exist on the registry between releases.",
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@ametyst-dev/sdk-prod": "^0.2.0",
|
|
43
|
+
"@napi-rs/cli": "2.18.4",
|
|
44
|
+
"@types/node": "20.19.39",
|
|
45
|
+
"@types/prompts": "2.4.9",
|
|
46
|
+
"@zerodev/permissions": "5.6.3",
|
|
47
|
+
"@zerodev/sdk": "5.5.7",
|
|
48
|
+
"commander": "12.0.0",
|
|
49
|
+
"prompts": "2.4.2",
|
|
50
|
+
"tsup": "8.5.1",
|
|
51
|
+
"tsx": "4.19.2",
|
|
52
|
+
"typescript": "5.2.2",
|
|
53
|
+
"viem": "2.38.6",
|
|
54
|
+
"vitest": "2.1.9"
|
|
55
|
+
},
|
|
56
|
+
"publishConfig": {
|
|
57
|
+
"registry": "https://registry.npmjs.org"
|
|
58
|
+
},
|
|
59
|
+
"optionalDependencies": {
|
|
60
|
+
"@ametyst/cli-darwin-arm64": "0.2.0",
|
|
61
|
+
"@ametyst/cli-darwin-x64": "0.2.0",
|
|
62
|
+
"@ametyst/cli-linux-x64-gnu": "0.2.0"
|
|
63
|
+
},
|
|
64
|
+
"scripts": {
|
|
65
|
+
"build:native": "cd native && cargo build --release && napi build --release",
|
|
66
|
+
"build:ts": "tsc",
|
|
67
|
+
"build": "tsup",
|
|
68
|
+
"build:staging": "AMETYST_BUILD_ENV=staging tsup",
|
|
69
|
+
"build:prod": "AMETYST_BUILD_ENV=prod tsup",
|
|
70
|
+
"dev": "tsup --watch",
|
|
71
|
+
"test": "vitest run",
|
|
72
|
+
"lint": "echo 'no lint configured yet' && exit 0",
|
|
73
|
+
"typecheck": "tsc --noEmit",
|
|
74
|
+
"perf:start-session": "tsx scripts/perf-start-session.ts"
|
|
75
|
+
}
|
|
76
|
+
}
|