moneyos 0.3.2 → 0.3.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/README.md CHANGED
@@ -12,7 +12,6 @@ but the repo is structured so each major surface can evolve independently.
12
12
  - `moneyos`: the root SDK + CLI package
13
13
  - `@moneyos/core`: runtime interfaces, shared types, chain/token registries
14
14
  - `@moneyos/tool-swap`: swap execution tool and provider surface
15
- - `@moneyos/executor-particle`: Particle AA smart-account executor
16
15
 
17
16
  ## CLI
18
17
 
@@ -23,6 +22,7 @@ moneyos init [--key 0x...] [--force] [--chain 42161] [--rpc https://...]
23
22
  moneyos auth unlock
24
23
  moneyos auth lock
25
24
  moneyos auth status
25
+ moneyos auth change-password
26
26
  moneyos backup export [--out ./wallet-backup.json] [--force]
27
27
  moneyos backup restore <path> [--force]
28
28
  moneyos backup status
@@ -79,6 +79,7 @@ What is landed in code today:
79
79
  - `~/.moneyos/config.json` now stores only non-secret settings such as chain and RPC configuration
80
80
  - `MONEYOS_PRIVATE_KEY` remains an explicit override for ephemeral CI or agent runs
81
81
  - `moneyos auth unlock` opens a short-lived local session for write commands
82
+ - `moneyos auth change-password` rotates the local wallet password and locks the current session
82
83
  - `moneyos backup export|restore|status` manages encrypted wallet backups
83
84
  - Normal wallet commands resolve their write path through one shared session-aware flow
84
85
  - Local EOA signers use viem's nonce manager, so back-to-back live transactions
@@ -120,6 +121,19 @@ If you have a legacy `~/.moneyos/config.json` with a plaintext `privateKey`
120
121
  field from a pre-encrypted-wallet version of the CLI, `moneyos init` with no
121
122
  flags will detect and import that key automatically.
122
123
 
124
+ ## Changing the wallet password
125
+
126
+ Use:
127
+
128
+ ```bash
129
+ moneyos auth change-password
130
+ ```
131
+
132
+ This re-encrypts the active local wallet file with a new password and locks the
133
+ current local session. Existing backup files and previously exported backups
134
+ remain snapshots encrypted with the old password. If you want a backup under
135
+ the new password, run `moneyos backup export` after the password change.
136
+
123
137
  ## Threat model
124
138
 
125
139
  What this wallet model is meant to protect against:
@@ -186,7 +200,6 @@ What still needs more hands-on validation:
186
200
  - live session-backed ETH send
187
201
  - live session-backed ERC-20 send
188
202
  - live session-backed swap
189
- - Particle executor against real infrastructure
190
203
  - more live usage of the encrypted-wallet/auth/backup flow in a real terminal
191
204
 
192
205
  ## Development
@@ -195,7 +208,6 @@ What still needs more hands-on validation:
195
208
  npm install
196
209
  npm run build:core
197
210
  npm run build:tool-swap
198
- npm run build:executor-particle
199
211
  npm run typecheck
200
212
  npm test
201
213
  npm run lint
package/dist/cli/index.js CHANGED
@@ -47,6 +47,10 @@ var DEFAULT_KDF = {
47
47
  };
48
48
  var SECURE_FILE_MODE = 384;
49
49
  var SECURE_PARENT_MODE = 448;
50
+ var WALLET_WRITE_LABELS = {
51
+ parentDescription: "Wallet directory",
52
+ fileDescription: "Wallet file"
53
+ };
50
54
  function normalizePassphrase(passphrase) {
51
55
  return passphrase.normalize("NFC");
52
56
  }
@@ -59,7 +63,7 @@ function walletProofMessage(wallet) {
59
63
  `createdAt=${wallet.createdAt}`
60
64
  ].join("|");
61
65
  }
62
- function ensureParentDir(path) {
66
+ function ensureParentDir(path, label) {
63
67
  const dir = dirname(path);
64
68
  if (!existsSync(dir)) {
65
69
  mkdirSync(dir, { recursive: true, mode: SECURE_PARENT_MODE });
@@ -68,7 +72,7 @@ function ensureParentDir(path) {
68
72
  const mode = statSync(dir).mode & 511;
69
73
  if ((mode & 63) !== 0) {
70
74
  throw new Error(
71
- `Wallet directory ${dir} has insecure permissions (${mode.toString(8)}). Restrict it to 700 before continuing.`
75
+ `${label} ${dir} has insecure permissions (${mode.toString(8)}). Restrict it to 700 before continuing.`
72
76
  );
73
77
  }
74
78
  }
@@ -161,9 +165,9 @@ function walletAad(kdf) {
161
165
  "utf8"
162
166
  );
163
167
  }
164
- function writeFileAtomicSecure(path, contents) {
165
- ensureParentDir(path);
166
- assertSecureFileMode(path, "Wallet file");
168
+ function writeFileAtomicSecure(path, contents, labels = WALLET_WRITE_LABELS) {
169
+ ensureParentDir(path, labels.parentDescription);
170
+ assertSecureFileMode(path, labels.fileDescription);
167
171
  const tmpPath = join(
168
172
  dirname(path),
169
173
  `.${basename(path)}.${randomBytes(6).toString("hex")}.tmp`
@@ -190,7 +194,7 @@ function writeFileAtomicSecure(path, contents) {
190
194
  }
191
195
  async function encryptWallet(params) {
192
196
  const account = privateKeyToAccount(params.privateKey);
193
- const createdAt = (/* @__PURE__ */ new Date()).toISOString();
197
+ const createdAt = params.identity?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString();
194
198
  const salt = randomBytes(16);
195
199
  const nonce = randomBytes(12);
196
200
  const key = deriveKey(params.passphrase, salt, DEFAULT_KDF);
@@ -202,7 +206,7 @@ async function encryptWallet(params) {
202
206
  );
203
207
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
204
208
  const authTag = cipher.getAuthTag();
205
- const addressProof = await account.signMessage({
209
+ const addressProof = params.identity?.addressProof ?? await account.signMessage({
206
210
  message: walletProofMessage({
207
211
  version: 1,
208
212
  kind: "encrypted-local-eoa",
@@ -210,10 +214,13 @@ async function encryptWallet(params) {
210
214
  createdAt
211
215
  })
212
216
  });
217
+ if (params.identity && account.address.toLowerCase() !== params.identity.address.toLowerCase()) {
218
+ throw new Error("wallet address metadata mismatch");
219
+ }
213
220
  return {
214
- version: 1,
215
- kind: "encrypted-local-eoa",
216
- address: account.address,
221
+ version: params.identity?.version ?? 1,
222
+ kind: params.identity?.kind ?? "encrypted-local-eoa",
223
+ address: params.identity?.address ?? account.address,
217
224
  createdAt,
218
225
  addressProof,
219
226
  kdf: DEFAULT_KDF,
@@ -226,6 +233,15 @@ async function encryptWallet(params) {
226
233
  }
227
234
  };
228
235
  }
236
+ function walletIdentity(wallet) {
237
+ return {
238
+ version: wallet.version,
239
+ kind: wallet.kind,
240
+ address: wallet.address,
241
+ createdAt: wallet.createdAt,
242
+ addressProof: wallet.addressProof
243
+ };
244
+ }
229
245
  async function decryptWalletFile(wallet, passphrase) {
230
246
  try {
231
247
  const salt = Buffer.from(wallet.crypto.salt, "base64");
@@ -276,9 +292,32 @@ var FileEncryptedWalletStore = class {
276
292
  }
277
293
  async save(params) {
278
294
  const wallet = await encryptWallet(params);
279
- writeFileAtomicSecure(this.walletPath, JSON.stringify(wallet, null, 2));
295
+ writeFileAtomicSecure(
296
+ this.walletPath,
297
+ JSON.stringify(wallet, null, 2),
298
+ WALLET_WRITE_LABELS
299
+ );
280
300
  return toMetadata(wallet);
281
301
  }
302
+ async rotatePassphrase(params) {
303
+ if (!this.exists()) {
304
+ throw new Error("No wallet configured. Run `moneyos init`.");
305
+ }
306
+ assertSecureFileMode(this.walletPath, "Wallet file");
307
+ const wallet = parseWalletFile(
308
+ readFileSync(this.walletPath, "utf8"),
309
+ this.walletPath
310
+ );
311
+ await verifyWalletAddressProof(wallet, this.walletPath);
312
+ const privateKey = await decryptWalletFile(wallet, params.oldPassphrase);
313
+ const rotated = await encryptWallet({
314
+ privateKey,
315
+ passphrase: params.newPassphrase,
316
+ identity: walletIdentity(wallet)
317
+ });
318
+ writeFileAtomicSecure(this.walletPath, JSON.stringify(rotated, null, 2));
319
+ return toMetadata(rotated);
320
+ }
282
321
  async decrypt(passphrase) {
283
322
  if (!this.exists()) {
284
323
  throw new Error("No wallet configured. Run `moneyos init`.");
@@ -306,10 +345,20 @@ var FileEncryptedWalletStore = class {
306
345
  async restore(data) {
307
346
  const wallet = parseWalletFile(JSON.stringify(data), this.walletPath);
308
347
  await verifyWalletAddressProof(wallet, this.walletPath);
309
- writeFileAtomicSecure(this.walletPath, JSON.stringify(wallet, null, 2));
348
+ writeFileAtomicSecure(
349
+ this.walletPath,
350
+ JSON.stringify(wallet, null, 2),
351
+ WALLET_WRITE_LABELS
352
+ );
310
353
  return toMetadata(wallet);
311
354
  }
312
355
  };
356
+ async function writeEncryptedWalletFile(path, data, labels = WALLET_WRITE_LABELS) {
357
+ const wallet = parseWalletFile(JSON.stringify(data), path);
358
+ await verifyWalletAddressProof(wallet, path);
359
+ writeFileAtomicSecure(path, JSON.stringify(wallet, null, 2), labels);
360
+ return toMetadata(wallet);
361
+ }
313
362
  async function readEncryptedWalletFile(path) {
314
363
  assertSecureFileMode(path, "Wallet file");
315
364
  const wallet = parseWalletFile(readFileSync(path, "utf8"), path);
@@ -360,7 +409,10 @@ var FileBackupProvider = class {
360
409
  "Backup file already exists. Re-run with `--force` if you really want to overwrite it."
361
410
  );
362
411
  }
363
- await new FileEncryptedWalletStore(targetPath).restore(wallet);
412
+ await writeEncryptedWalletFile(targetPath, wallet, {
413
+ parentDescription: "Backup export destination directory",
414
+ fileDescription: "Backup export file"
415
+ });
364
416
  return targetPath;
365
417
  }
366
418
  async restoreWallet(fromPath, options) {
@@ -1259,38 +1311,58 @@ function parseChainId(value, fallback) {
1259
1311
  }
1260
1312
  return parsed;
1261
1313
  }
1262
- var initCommand = new Command("init").description("Initialize MoneyOS with a new or imported encrypted wallet").option("-k, --key <privateKey>", "Import an existing private key").option("--force", "Overwrite the existing encrypted wallet").option("--chain <chainId>", "Default chain ID (default: 42161 Arbitrum)").option("--rpc <url>", "Custom RPC URL").action(async (options) => {
1263
- const existing = loadFileConfig();
1264
- const walletPath = getWalletPath(existing);
1265
- const backupDir = getBackupDir(existing);
1266
- const wallet = new FileEncryptedWalletStore(walletPath);
1314
+ var defaultInitCommandDependencies = {
1315
+ loadFileConfig,
1316
+ getWalletPath,
1317
+ getBackupDir,
1318
+ getConfigPath,
1319
+ createWalletStore: (walletPath) => new FileEncryptedWalletStore(walletPath),
1320
+ createBackupProvider: (params) => new FileBackupProvider(params),
1321
+ hasRemovedOnePasswordConfig,
1322
+ getRemovedOnePasswordStorageMessage,
1323
+ hasLegacyPlaintextWalletConfig,
1324
+ getLegacyPlaintextWalletStorageMessage,
1325
+ promptHidden,
1326
+ lockSession,
1327
+ getSessionSocketPath,
1328
+ getSessionTokenPath,
1329
+ saveConfig,
1330
+ generatePrivateKey,
1331
+ log: (message) => console.log(message),
1332
+ error: (message) => console.error(message)
1333
+ };
1334
+ async function runInitCommand(options, deps = defaultInitCommandDependencies) {
1335
+ const existing = deps.loadFileConfig();
1336
+ const walletPath = deps.getWalletPath(existing);
1337
+ const backupDir = deps.getBackupDir(existing);
1338
+ const wallet = deps.createWalletStore(walletPath);
1267
1339
  if (wallet.exists() && !options.force) {
1268
1340
  const metadata = await wallet.metadata();
1269
- console.log(`Already initialized.`);
1341
+ deps.log(`Already initialized.`);
1270
1342
  if (metadata?.address) {
1271
- console.log(`Address: ${metadata.address}`);
1343
+ deps.log(`Address: ${metadata.address}`);
1272
1344
  }
1273
- console.log(`Wallet: ${walletPath}`);
1274
- console.log(`Config: ${getConfigPath()}`);
1275
- console.log(`
1345
+ deps.log(`Wallet: ${walletPath}`);
1346
+ deps.log(`Config: ${deps.getConfigPath()}`);
1347
+ deps.log(`
1276
1348
  To reinitialize, run: moneyos init --force --key <privateKey>`);
1277
1349
  return;
1278
1350
  }
1279
- if (hasRemovedOnePasswordConfig(existing) && !options.key) {
1280
- console.error(getRemovedOnePasswordStorageMessage());
1281
- console.error(`Config: ${getConfigPath()}`);
1351
+ if (deps.hasRemovedOnePasswordConfig(existing) && !options.key) {
1352
+ deps.error(deps.getRemovedOnePasswordStorageMessage());
1353
+ deps.error(`Config: ${deps.getConfigPath()}`);
1282
1354
  return;
1283
1355
  }
1284
1356
  try {
1285
- const privateKey = options.key ?? (hasLegacyPlaintextWalletConfig(existing) ? existing.privateKey : generatePrivateKey());
1357
+ const privateKey = options.key ?? (deps.hasLegacyPlaintextWalletConfig(existing) ? existing.privateKey : deps.generatePrivateKey());
1286
1358
  const account = privateKeyToAccount3(privateKey);
1287
1359
  const chainId = parseChainId(options.chain, existing.chainId ?? 42161);
1288
1360
  const rpcUrl = options.rpc ?? existing.rpcUrl;
1289
- const passphrase = await promptHidden("Choose wallet password: ");
1361
+ const passphrase = await deps.promptHidden("Choose wallet password: ");
1290
1362
  if (passphrase.length < 8) {
1291
1363
  throw new Error("Wallet password must be at least 8 characters long.");
1292
1364
  }
1293
- const confirmPassphrase = await promptHidden("Confirm wallet password: ");
1365
+ const confirmPassphrase = await deps.promptHidden("Confirm wallet password: ");
1294
1366
  if (passphrase !== confirmPassphrase) {
1295
1367
  throw new Error("Wallet password confirmation did not match.");
1296
1368
  }
@@ -1298,41 +1370,47 @@ To reinitialize, run: moneyos init --force --key <privateKey>`);
1298
1370
  privateKey,
1299
1371
  passphrase
1300
1372
  });
1301
- await lockSession(getSessionSocketPath(), getSessionTokenPath());
1302
- saveConfig({
1373
+ await deps.lockSession(
1374
+ deps.getSessionSocketPath(),
1375
+ deps.getSessionTokenPath()
1376
+ );
1377
+ deps.saveConfig({
1303
1378
  chainId,
1304
1379
  rpcUrl,
1305
1380
  walletPath: existing.walletPath,
1306
1381
  backupDir: existing.backupDir
1307
1382
  });
1308
- const backupProvider = new FileBackupProvider({
1383
+ const backupProvider = deps.createBackupProvider({
1309
1384
  walletPath,
1310
1385
  backupDir
1311
1386
  });
1312
1387
  const backupPath = await backupProvider.exportWallet();
1313
- console.log(`MoneyOS initialized.`);
1314
- console.log(`Address: ${account.address}`);
1315
- console.log(`Wallet: ${walletPath}`);
1316
- console.log(`Config: ${getConfigPath()}`);
1317
- console.log(`Backup: ${backupPath}`);
1318
- console.log(
1388
+ deps.log(`MoneyOS initialized.`);
1389
+ deps.log(`Address: ${account.address}`);
1390
+ deps.log(`Wallet: ${walletPath}`);
1391
+ deps.log(`Config: ${deps.getConfigPath()}`);
1392
+ deps.log(`Backup: ${backupPath}`);
1393
+ deps.log(
1319
1394
  `
1320
1395
  Save your wallet password in your password manager of choice. MoneyOS does not store or sync it for you.`
1321
1396
  );
1322
- if (hasLegacyPlaintextWalletConfig(existing) && !options.key) {
1323
- console.log(`
1397
+ if (deps.hasLegacyPlaintextWalletConfig(existing) && !options.key) {
1398
+ deps.log(`
1324
1399
  Imported legacy plaintext wallet into the encrypted wallet file.`);
1325
- console.log(getLegacyPlaintextWalletStorageMessage());
1400
+ deps.log(deps.getLegacyPlaintextWalletStorageMessage());
1326
1401
  } else if (!options.key) {
1327
- console.log(
1402
+ deps.log(
1328
1403
  `
1329
1404
  This is a new account. Fund it before sending transactions.`
1330
1405
  );
1331
1406
  }
1332
1407
  } catch (error) {
1333
- console.error(error instanceof Error ? error.message : String(error));
1408
+ deps.error(error instanceof Error ? error.message : String(error));
1334
1409
  process.exitCode = 1;
1335
1410
  }
1411
+ }
1412
+ var initCommand = new Command("init").description("Initialize MoneyOS with a new or imported encrypted wallet").option("-k, --key <privateKey>", "Import an existing private key").option("--force", "Overwrite the existing encrypted wallet").option("--chain <chainId>", "Default chain ID (default: 42161 Arbitrum)").option("--rpc <url>", "Custom RPC URL").action(async (options) => {
1413
+ await runInitCommand(options);
1336
1414
  });
1337
1415
 
1338
1416
  // src/cli/commands/balance.ts
@@ -1992,8 +2070,70 @@ function formatSessionStatus(params) {
1992
2070
  }
1993
2071
  return lines.join("\n");
1994
2072
  }
2073
+ var defaultChangePasswordCommandDependencies = {
2074
+ loadFileConfig,
2075
+ getWalletPath,
2076
+ createWalletStore: (walletPath) => new FileEncryptedWalletStore(walletPath),
2077
+ promptHidden,
2078
+ lockSession,
2079
+ getSessionSocketPath,
2080
+ getSessionTokenPath,
2081
+ log: (message) => console.log(message),
2082
+ error: (message) => console.error(message)
2083
+ };
2084
+ async function runChangePasswordCommand(deps = defaultChangePasswordCommandDependencies) {
2085
+ const config = deps.loadFileConfig();
2086
+ const walletPath = deps.getWalletPath(config);
2087
+ const wallet = deps.createWalletStore(walletPath);
2088
+ if (!wallet.exists()) {
2089
+ deps.error("No encrypted wallet found. Run `moneyos init` first.");
2090
+ process.exitCode = 1;
2091
+ return;
2092
+ }
2093
+ try {
2094
+ const currentPassphrase = await deps.promptHidden("Current wallet password: ");
2095
+ if (currentPassphrase.length === 0) {
2096
+ throw new Error("Current wallet password cannot be empty.");
2097
+ }
2098
+ const newPassphrase = await deps.promptHidden("New wallet password: ");
2099
+ if (newPassphrase.length < 8) {
2100
+ throw new Error("New wallet password must be at least 8 characters long.");
2101
+ }
2102
+ if (newPassphrase === currentPassphrase) {
2103
+ throw new Error(
2104
+ "New wallet password must differ from the current password."
2105
+ );
2106
+ }
2107
+ const confirmPassphrase = await deps.promptHidden(
2108
+ "Confirm new wallet password: "
2109
+ );
2110
+ if (newPassphrase !== confirmPassphrase) {
2111
+ throw new Error("New wallet password confirmation did not match.");
2112
+ }
2113
+ const metadata = await wallet.rotatePassphrase({
2114
+ oldPassphrase: currentPassphrase,
2115
+ newPassphrase
2116
+ });
2117
+ await deps.lockSession(
2118
+ deps.getSessionSocketPath(),
2119
+ deps.getSessionTokenPath()
2120
+ );
2121
+ deps.log("Wallet password changed.");
2122
+ deps.log(`Address: ${metadata.address}`);
2123
+ deps.log(formatSessionStatus({ state: "locked" }));
2124
+ deps.log(
2125
+ "Existing backup files and exported copies still require the old wallet password."
2126
+ );
2127
+ deps.log(
2128
+ "Run `moneyos backup export` to create a backup encrypted with the new wallet password."
2129
+ );
2130
+ } catch (error) {
2131
+ deps.error(error instanceof Error ? error.message : String(error));
2132
+ process.exitCode = 1;
2133
+ }
2134
+ }
1995
2135
  var authCommand = new Command6("auth").description(
1996
- "Unlock, inspect, and lock the local MoneyOS wallet session"
2136
+ "Unlock, inspect, lock, and change the local MoneyOS wallet password"
1997
2137
  );
1998
2138
  authCommand.command("unlock").description("Unlock the local wallet and start a short-lived session").action(async () => {
1999
2139
  const config = loadFileConfig();
@@ -2032,6 +2172,9 @@ authCommand.command("unlock").description("Unlock the local wallet and start a s
2032
2172
  process.exitCode = 1;
2033
2173
  }
2034
2174
  });
2175
+ authCommand.command("change-password").description("Change the local wallet password and lock the current session").action(async () => {
2176
+ await runChangePasswordCommand();
2177
+ });
2035
2178
  authCommand.command("lock").description("Lock the local MoneyOS wallet session").action(async () => {
2036
2179
  const locked = await lockSession(
2037
2180
  getSessionSocketPath(),
@@ -2084,6 +2227,13 @@ function formatBackupStatus(params) {
2084
2227
  }
2085
2228
  return lines.join("\n");
2086
2229
  }
2230
+ function getBackupExportPasswordGuidance() {
2231
+ return [
2232
+ "This backup is encrypted with the same wallet password as your active wallet.",
2233
+ "You will need that same wallet password to restore it.",
2234
+ "MoneyOS does not store or sync it for you."
2235
+ ].join(" ");
2236
+ }
2087
2237
  var backupCommand = new Command7("backup").description(
2088
2238
  "Export, restore, and inspect encrypted wallet backups"
2089
2239
  );
@@ -2099,9 +2249,7 @@ backupCommand.command("export").description("Write a copy of the encrypted walle
2099
2249
  allowOverwrite: Boolean(options.force)
2100
2250
  });
2101
2251
  console.log(`Backup exported to ${targetPath}`);
2102
- console.log(
2103
- "Save your wallet password in your password manager of choice. MoneyOS does not store or sync it for you."
2104
- );
2252
+ console.log(getBackupExportPasswordGuidance());
2105
2253
  } catch (error) {
2106
2254
  console.error(error instanceof Error ? error.message : String(error));
2107
2255
  process.exitCode = 1;