@ametyst/cli 0.2.2 → 0.2.34

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.
Binary file
package/native/index.d.ts CHANGED
@@ -5,5 +5,12 @@
5
5
 
6
6
  export declare function hardenProcess(): void
7
7
  export declare function createWallet(passphrase: string): string
8
+ /**
9
+ * Import an externally-provided raw private key (e.g. an Option-A session key
10
+ * generated + passkey-approved in the web app and pasted into the CLI) into
11
+ * the SAME passphrase-encrypted keystore format `create_wallet` emits. The key
12
+ * never leaves the machine — Ametyst never receives it (ADR-001 non-custodial).
13
+ */
14
+ export declare function importWallet(privateKey: string, passphrase: string): string
8
15
  export declare function decryptWallet(keystoreJson: string, passphrase: string): string
9
16
  export declare function getWalletAddress(keystoreJson: string): string
package/native/src/lib.rs CHANGED
@@ -1,6 +1,7 @@
1
1
  use aes_gcm::aead::{Aead, KeyInit};
2
2
  use aes_gcm::{Aes256Gcm, Nonce};
3
3
  use k256::ecdsa::SigningKey;
4
+ #[cfg(unix)]
4
5
  use libc::{c_void, mlock, munlock, rlimit, setrlimit, RLIMIT_CORE};
5
6
  use napi_derive::napi;
6
7
  use rand::rngs::OsRng;
@@ -24,9 +25,10 @@ struct SecretBytes {
24
25
  impl SecretBytes {
25
26
  fn new(bytes: Vec<u8>) -> Self {
26
27
  let secret = Self { bytes };
27
- let ptr = secret.bytes.as_ptr() as *const c_void;
28
- let len = secret.bytes.len();
28
+ #[cfg(unix)]
29
29
  unsafe {
30
+ let ptr = secret.bytes.as_ptr() as *const c_void;
31
+ let len = secret.bytes.len();
30
32
  let _ = mlock(ptr, len);
31
33
  }
32
34
  secret
@@ -43,10 +45,11 @@ impl SecretBytes {
43
45
 
44
46
  impl Drop for SecretBytes {
45
47
  fn drop(&mut self) {
46
- let ptr = self.bytes.as_ptr() as *const c_void;
47
- let len = self.bytes.len();
48
48
  self.bytes.zeroize();
49
+ #[cfg(unix)]
49
50
  unsafe {
51
+ let ptr = self.bytes.as_ptr() as *const c_void;
52
+ let len = self.bytes.len();
50
53
  let _ = munlock(ptr, len);
51
54
  }
52
55
  }
@@ -110,24 +113,49 @@ pub fn harden_process() -> napi::Result<()> {
110
113
  libc::prctl(libc::PR_SET_DUMPABLE, 0, 0, 0, 0);
111
114
  }
112
115
 
113
- let lim = rlimit {
114
- rlim_cur: 0,
115
- rlim_max: 0,
116
- };
117
- unsafe {
118
- setrlimit(RLIMIT_CORE, &lim);
116
+ // Windows writes no core dump to cwd; setrlimit is Unix-only.
117
+ // Memory-lock (VirtualLock) parity on Windows is a backlog item.
118
+ #[cfg(unix)]
119
+ {
120
+ let lim = rlimit {
121
+ rlim_cur: 0,
122
+ rlim_max: 0,
123
+ };
124
+ unsafe {
125
+ setrlimit(RLIMIT_CORE, &lim);
126
+ }
119
127
  }
120
128
 
121
129
  Ok(())
122
130
  }
123
131
 
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();
132
+ /// Parse a raw secp256k1 private key from a hex string (with or without a
133
+ /// leading `0x`) into exactly 32 bytes. Rejects anything that is not a valid
134
+ /// 32-byte key so a bad paste fails loudly instead of producing a keystore for
135
+ /// the wrong address.
136
+ fn parse_private_key_hex(private_key: &str) -> napi::Result<Vec<u8>> {
137
+ let trimmed = private_key.trim();
138
+ let stripped = trimmed.strip_prefix("0x").unwrap_or(trimmed);
139
+ let bytes = hex::decode(stripped)
140
+ .map_err(|e| napi::Error::from_reason(format!("invalid private key hex: {e}")))?;
141
+ if bytes.len() != 32 {
142
+ return Err(napi::Error::from_reason(format!(
143
+ "invalid private key length: expected 32 bytes, got {}",
144
+ bytes.len()
145
+ )));
146
+ }
147
+ Ok(bytes)
148
+ }
130
149
 
150
+ /// Encrypt an in-memory private key into the version-3 keystore JSON that
151
+ /// `createWallet` produces (scrypt KDF + AES-256-GCM). Shared by `create_wallet`
152
+ /// (fresh random key) and `import_wallet` (externally-provided key) so both
153
+ /// emit byte-for-byte the same keystore shape that `decrypt_wallet` /
154
+ /// `get_wallet_address` read back.
155
+ fn encrypt_private_key_to_keystore(
156
+ secret_private_key: &SecretBytes,
157
+ passphrase: &str,
158
+ ) -> napi::Result<String> {
131
159
  let address = derive_address_from_private_key(secret_private_key.as_slice())?;
132
160
 
133
161
  let mut salt = [0u8; 32];
@@ -181,6 +209,27 @@ pub fn create_wallet(passphrase: String) -> napi::Result<String> {
181
209
  .map_err(|e| napi::Error::from_reason(format!("keystore serialization failed: {e}")))
182
210
  }
183
211
 
212
+ #[napi(js_name = "createWallet")]
213
+ pub fn create_wallet(passphrase: String) -> napi::Result<String> {
214
+ let mut private_key = [0u8; 32];
215
+ OsRng.fill_bytes(&mut private_key);
216
+ let secret_private_key = SecretBytes::new(private_key.to_vec());
217
+ private_key.zeroize();
218
+
219
+ encrypt_private_key_to_keystore(&secret_private_key, &passphrase)
220
+ }
221
+
222
+ /// Import an externally-provided raw private key (e.g. an Option-A session key
223
+ /// generated + passkey-approved in the web app and pasted into the CLI) into
224
+ /// the SAME passphrase-encrypted keystore format `create_wallet` emits. The key
225
+ /// never leaves the machine — Ametyst never receives it (ADR-001 non-custodial).
226
+ #[napi(js_name = "importWallet")]
227
+ pub fn import_wallet(private_key: String, passphrase: String) -> napi::Result<String> {
228
+ let key_bytes = parse_private_key_hex(&private_key)?;
229
+ let secret_private_key = SecretBytes::new(key_bytes);
230
+ encrypt_private_key_to_keystore(&secret_private_key, &passphrase)
231
+ }
232
+
184
233
  #[napi(js_name = "decryptWallet")]
185
234
  pub fn decrypt_wallet(keystore_json: String, passphrase: String) -> napi::Result<String> {
186
235
  let keystore: Keystore = serde_json::from_str(&keystore_json)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ametyst/cli",
3
- "version": "0.2.2",
3
+ "version": "0.2.34",
4
4
  "private": false,
5
5
  "description": "Ametyst CLI — embedded MCP server, wallet ops, agent payments",
6
6
  "type": "module",
@@ -31,18 +31,20 @@
31
31
  "additional": [
32
32
  "aarch64-apple-darwin",
33
33
  "x86_64-apple-darwin",
34
- "x86_64-unknown-linux-gnu"
34
+ "x86_64-unknown-linux-gnu",
35
+ "x86_64-pc-windows-msvc"
35
36
  ]
36
37
  }
37
38
  },
38
39
  "dependencies": {
40
+ "@ametyst-dev/sdk-prod": "^0.8.29",
39
41
  "@modelcontextprotocol/sdk": "1.29.0",
40
42
  "@napi-rs/keyring": "1.1.7",
41
43
  "mcp-use": "1.24.2"
42
44
  },
43
45
  "_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.",
44
46
  "devDependencies": {
45
- "@ametyst-dev/sdk-prod": "^0.2.0",
47
+ "@ametyst-dev/sdk-prod": "^0.8.29",
46
48
  "@napi-rs/cli": "2.18.4",
47
49
  "@types/node": "20.19.39",
48
50
  "@types/prompts": "2.4.9",
@@ -54,15 +56,32 @@
54
56
  "tsx": "4.19.2",
55
57
  "typescript": "5.2.2",
56
58
  "viem": "2.38.6",
57
- "vitest": "2.1.9"
59
+ "vitest": "3.2.6"
58
60
  },
59
61
  "publishConfig": {
60
62
  "registry": "https://registry.npmjs.org"
61
63
  },
64
+ "_comment_overrides": "Security overrides: pin transitive deps to their first patched version to clear npm/pnpm audit advisories (bn.js infinite-loop, ws memory-disclosure/DoS, plus the rest of the flagged transitive tree). All within-major to avoid breaking changes. The npm `overrides` block below mirrors `pnpm.overrides` so a GLOBAL npm install of the published package (where the cli is the install root) inherits the same patched resolution; note a project that installs the cli as a *dependency* will NOT inherit these — the consuming project must add its own overrides.",
65
+ "overrides": {
66
+ "bn.js@<4.12.3": "4.12.4",
67
+ "ws@>=8.0.0 <8.21.0": "8.21.0",
68
+ "protobufjs@<7.6.3": "7.6.4",
69
+ "form-data@<4.0.6": "4.0.6",
70
+ "tar@<7.5.16": "7.5.19",
71
+ "dompurify@<3.4.11": "3.4.11",
72
+ "@opentelemetry/core@<2.8.0": "2.8.0",
73
+ "hono@<4.12.25": "4.12.27",
74
+ "vite@>=8.0.0 <8.0.16": "8.1.2",
75
+ "vite@>=5.0.0 <6.4.3": "6.4.3",
76
+ "vitest@<3.2.6": "3.2.6",
77
+ "esbuild@<0.25.0": "0.25.12",
78
+ "esbuild@>=0.27.3 <0.28.1": "0.28.1"
79
+ },
62
80
  "optionalDependencies": {
63
- "@ametyst/cli-darwin-arm64": "0.2.2",
64
- "@ametyst/cli-darwin-x64": "0.2.2",
65
- "@ametyst/cli-linux-x64-gnu": "0.2.2"
81
+ "@ametyst/cli-darwin-arm64": "0.2.34",
82
+ "@ametyst/cli-darwin-x64": "0.2.34",
83
+ "@ametyst/cli-linux-x64-gnu": "0.2.34",
84
+ "@ametyst/cli-win32-x64-msvc": "0.2.34"
66
85
  },
67
86
  "scripts": {
68
87
  "build:native": "cd native && cargo build --release && napi build --release",