@elytro/cli 0.6.0 → 0.6.2

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.
Files changed (2) hide show
  1. package/dist/index.js +144 -47
  2. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -365,6 +365,12 @@ var KeyringService = class {
365
365
  vault = null;
366
366
  /** Vault key kept in memory for re-encryption operations (addOwner, switchOwner). */
367
367
  vaultKey = null;
368
+ /**
369
+ * In-memory private key buffers indexed by owner address.
370
+ * These are the scrubbable counterparts of VaultData.owners[].key.
371
+ * Zero-filled on lock().
372
+ */
373
+ keyBuffers = /* @__PURE__ */ new Map();
368
374
  constructor(store) {
369
375
  this.store = store;
370
376
  }
@@ -389,6 +395,7 @@ var KeyringService = class {
389
395
  await this.store.save(STORAGE_KEY, encrypted);
390
396
  this.vault = vault;
391
397
  this.vaultKey = new Uint8Array(vaultKey);
398
+ this.hydrateKeyBuffers(vault);
392
399
  return account.address;
393
400
  }
394
401
  // ─── Unlock / Access ────────────────────────────────────────────
@@ -403,9 +410,14 @@ var KeyringService = class {
403
410
  }
404
411
  this.vault = await decryptWithKey(vaultKey, encrypted);
405
412
  this.vaultKey = new Uint8Array(vaultKey);
413
+ this.hydrateKeyBuffers(this.vault);
406
414
  }
407
415
  /** Lock the vault, clearing decrypted keys and vault key from memory. */
408
416
  lock() {
417
+ for (const buf of this.keyBuffers.values()) {
418
+ buf.fill(0);
419
+ }
420
+ this.keyBuffers.clear();
409
421
  this.vault = null;
410
422
  if (this.vaultKey) {
411
423
  this.vaultKey.fill(0);
@@ -453,7 +465,8 @@ var KeyringService = class {
453
465
  this.ensureUnlocked();
454
466
  const privateKey = generatePrivateKey();
455
467
  const account = privateKeyToAccount(privateKey);
456
- this.vault.owners.push({ id: account.address, key: privateKey });
468
+ this.vault.owners.push({ id: account.address, key: "" });
469
+ this.keyBuffers.set(account.address, hexToBytes(privateKey));
457
470
  await this.persistVault();
458
471
  return account.address;
459
472
  }
@@ -473,7 +486,7 @@ var KeyringService = class {
473
486
  */
474
487
  async exportVault(password2) {
475
488
  this.ensureUnlocked();
476
- return encrypt(password2, this.vault);
489
+ return encrypt(password2, this.buildVaultWithKeys());
477
490
  }
478
491
  /**
479
492
  * Import vault from a password-encrypted backup.
@@ -481,10 +494,11 @@ var KeyringService = class {
481
494
  */
482
495
  async importVault(encrypted, password2, vaultKey) {
483
496
  const vault = await decrypt(password2, encrypted);
484
- this.vault = vault;
485
497
  this.vaultKey = new Uint8Array(vaultKey);
486
498
  const reEncrypted = await encryptWithKey(vaultKey, vault);
487
499
  await this.store.save(STORAGE_KEY, reEncrypted);
500
+ this.vault = vault;
501
+ this.hydrateKeyBuffers(vault);
488
502
  }
489
503
  // ─── Rekey (vault key rotation) ───────────────────────────────
490
504
  async rekey(newVaultKey) {
@@ -493,15 +507,23 @@ var KeyringService = class {
493
507
  await this.persistVault();
494
508
  }
495
509
  // ─── Internal ───────────────────────────────────────────────────
510
+ /**
511
+ * Get the current owner's private key as Hex for signing.
512
+ *
513
+ * Reads from the scrubbable Uint8Array keyBuffers and converts to Hex
514
+ * on the fly. The returned Hex string is short-lived (used immediately
515
+ * by viem's privateKeyToAccount, then goes out of scope for GC).
516
+ */
496
517
  getCurrentKey() {
497
518
  if (!this.vault) {
498
519
  throw new Error("Keyring is locked. Cannot sign.");
499
520
  }
500
- const owner = this.vault.owners.find((o) => o.id === this.vault.currentOwnerId);
501
- if (!owner) {
521
+ const ownerId = this.vault.currentOwnerId;
522
+ const buf = this.keyBuffers.get(ownerId);
523
+ if (!buf) {
502
524
  throw new Error("Current owner key not found in vault.");
503
525
  }
504
- return owner.key;
526
+ return `0x${Buffer.from(buf).toString("hex")}`;
505
527
  }
506
528
  ensureUnlocked() {
507
529
  if (!this.vault) {
@@ -511,10 +533,44 @@ var KeyringService = class {
511
533
  async persistVault() {
512
534
  if (!this.vault) throw new Error("No vault to persist.");
513
535
  if (!this.vaultKey) throw new Error("No vault key available for re-encryption.");
514
- const encrypted = await encryptWithKey(this.vaultKey, this.vault);
536
+ const encrypted = await encryptWithKey(this.vaultKey, this.buildVaultWithKeys());
515
537
  await this.store.save(STORAGE_KEY, encrypted);
516
538
  }
539
+ /**
540
+ * Reconstruct a complete VaultData with hex keys sourced from keyBuffers.
541
+ * Used by persistVault and exportVault — the live this.vault has keys scrubbed.
542
+ */
543
+ buildVaultWithKeys() {
544
+ return {
545
+ ...this.vault,
546
+ owners: this.vault.owners.map((owner) => {
547
+ const buf = this.keyBuffers.get(owner.id);
548
+ if (!buf) throw new Error(`Key buffer missing for owner ${owner.id}`);
549
+ return { ...owner, key: `0x${Buffer.from(buf).toString("hex")}` };
550
+ })
551
+ };
552
+ }
553
+ /**
554
+ * Convert vault owner hex keys into scrubbable Uint8Array buffers,
555
+ * then scrub the hex strings from the in-memory vault so the sole live
556
+ * copy of each private key is the zero-fillable Uint8Array in keyBuffers.
557
+ * Called after decryption (unlock, createNewOwner, importVault).
558
+ */
559
+ hydrateKeyBuffers(vault) {
560
+ for (const buf of this.keyBuffers.values()) {
561
+ buf.fill(0);
562
+ }
563
+ this.keyBuffers.clear();
564
+ for (const owner of vault.owners) {
565
+ this.keyBuffers.set(owner.id, hexToBytes(owner.key));
566
+ owner.key = "";
567
+ }
568
+ }
517
569
  };
570
+ function hexToBytes(hex) {
571
+ const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
572
+ return new Uint8Array(Buffer.from(clean, "hex"));
573
+ }
518
574
 
519
575
  // src/utils/config.ts
520
576
  var PUBLIC_RPC = {
@@ -635,19 +691,55 @@ var ChainService = class {
635
691
  store;
636
692
  config;
637
693
  userKeys = {};
694
+ /** Vault key for encrypting/decrypting user keys. null = vault not yet unlocked. */
695
+ vaultKey = null;
638
696
  constructor(store) {
639
697
  this.store = store;
640
698
  this.config = getDefaultConfig();
641
699
  }
642
- /** Load persisted config and user keys, rebuild chain endpoints. */
700
+ /**
701
+ * Load persisted config (non-sensitive).
702
+ * User keys are NOT loaded here — call `unlockUserKeys()` after the
703
+ * vault key becomes available.
704
+ */
643
705
  async init() {
644
- this.userKeys = await this.store.load(USER_KEYS_KEY) ?? {};
645
706
  const saved = await this.store.load(STORAGE_KEY2);
646
707
  if (saved) {
647
708
  this.config = { ...getDefaultConfig(), ...saved };
648
709
  }
710
+ this.config.chains = buildChains(void 0, void 0);
711
+ }
712
+ /**
713
+ * Decrypt (or migrate) user API keys using the vault key.
714
+ * Call this after the vault key is available (post keyring.unlock).
715
+ *
716
+ * - If user-keys.json is encrypted (has `version` field): decrypt with vault key.
717
+ * - If user-keys.json is plaintext (legacy): load, encrypt, overwrite.
718
+ * - If user-keys.json is absent: no-op.
719
+ */
720
+ async unlockUserKeys(vaultKey) {
721
+ this.vaultKey = new Uint8Array(vaultKey);
722
+ const raw = await this.store.load(USER_KEYS_KEY);
723
+ if (!raw) {
724
+ this.config.chains = buildChains(void 0, void 0);
725
+ return;
726
+ }
727
+ if (isEncryptedData(raw)) {
728
+ this.userKeys = await decryptWithKey(vaultKey, raw);
729
+ } else {
730
+ this.userKeys = raw;
731
+ await this.persistUserKeys();
732
+ }
649
733
  this.config.chains = buildChains(this.userKeys.alchemyKey, this.userKeys.pimlicoKey);
650
734
  }
735
+ /** Clear vault key and plaintext user keys from memory. Called on CLI exit. */
736
+ lockUserKeys() {
737
+ if (this.vaultKey) {
738
+ this.vaultKey.fill(0);
739
+ this.vaultKey = null;
740
+ }
741
+ this.userKeys = {};
742
+ }
651
743
  // ─── User Keys ──────────────────────────────────────────────────
652
744
  /** Get current user keys (for display — values are masked). */
653
745
  getUserKeys() {
@@ -656,13 +748,13 @@ var ChainService = class {
656
748
  /** Set a user API key and rebuild chain endpoints. */
657
749
  async setUserKey(key, value) {
658
750
  this.userKeys[key] = value;
659
- await this.store.save(USER_KEYS_KEY, this.userKeys);
751
+ await this.persistUserKeys();
660
752
  this.config.chains = buildChains(this.userKeys.alchemyKey, this.userKeys.pimlicoKey);
661
753
  }
662
754
  /** Remove a user API key and fall back to env / public endpoints. */
663
755
  async removeUserKey(key) {
664
756
  delete this.userKeys[key];
665
- await this.store.save(USER_KEYS_KEY, this.userKeys);
757
+ await this.persistUserKeys();
666
758
  this.config.chains = buildChains(this.userKeys.alchemyKey, this.userKeys.pimlicoKey);
667
759
  }
668
760
  // ─── Getters ────────────────────────────────────────────────────
@@ -713,7 +805,29 @@ var ChainService = class {
713
805
  async persist() {
714
806
  await this.store.save(STORAGE_KEY2, this.config);
715
807
  }
808
+ /**
809
+ * Persist user keys — encrypted if vault key is available, plaintext otherwise.
810
+ * The plaintext path only applies before `elytro init` (no vault key yet).
811
+ */
812
+ async persistUserKeys() {
813
+ const hasKeys = this.userKeys.alchemyKey || this.userKeys.pimlicoKey;
814
+ if (!hasKeys) {
815
+ await this.store.remove(USER_KEYS_KEY);
816
+ return;
817
+ }
818
+ if (this.vaultKey) {
819
+ const encrypted = await encryptWithKey(this.vaultKey, this.userKeys);
820
+ await this.store.save(USER_KEYS_KEY, encrypted);
821
+ } else {
822
+ await this.store.save(USER_KEYS_KEY, this.userKeys);
823
+ }
824
+ }
716
825
  };
826
+ function isEncryptedData(obj) {
827
+ if (typeof obj !== "object" || obj === null) return false;
828
+ const o = obj;
829
+ return typeof o.data === "string" && typeof o.iv === "string" && typeof o.version === "number";
830
+ }
717
831
 
718
832
  // src/services/sdk.ts
719
833
  import { padHex, createPublicClient, http, toHex as toHex2, parseEther } from "viem";
@@ -2440,7 +2554,11 @@ or import a backup with \`elytro import\`.`
2440
2554
  The vault key may not match the encrypted keyring. Re-run \`elytro init\` or import a backup.`
2441
2555
  );
2442
2556
  }
2557
+ await chain.unlockUserKeys(vaultKey);
2443
2558
  vaultKey.fill(0);
2559
+ const unlockedChain = chain.currentChain;
2560
+ walletClient.initForChain(unlockedChain);
2561
+ await sdk.initForChain(unlockedChain);
2444
2562
  }
2445
2563
  const account = new AccountService({
2446
2564
  store,
@@ -2452,7 +2570,9 @@ The vault key may not match the encrypted keyring. Re-run \`elytro init\` or imp
2452
2570
  await account.init();
2453
2571
  const currentAccount = account.currentAccount;
2454
2572
  if (currentAccount) {
2455
- const acctInfo = account.resolveAccount(currentAccount.alias ?? currentAccount.address);
2573
+ const acctInfo = account.resolveAccount(
2574
+ currentAccount.alias ?? currentAccount.address
2575
+ );
2456
2576
  if (acctInfo) {
2457
2577
  const acctChain = chain.chains.find((c) => c.id === acctInfo.chainId);
2458
2578
  if (acctChain && acctChain.id !== defaultChain.id) {
@@ -2461,7 +2581,15 @@ The vault key may not match the encrypted keyring. Re-run \`elytro init\` or imp
2461
2581
  }
2462
2582
  }
2463
2583
  }
2464
- return { store, keyring, chain, sdk, walletClient, account, secretProvider: loadProvider };
2584
+ return {
2585
+ store,
2586
+ keyring,
2587
+ chain,
2588
+ sdk,
2589
+ walletClient,
2590
+ account,
2591
+ secretProvider: loadProvider
2592
+ };
2465
2593
  }
2466
2594
  function noProviderHint() {
2467
2595
  switch (process.platform) {
@@ -2560,10 +2688,7 @@ import ora2 from "ora";
2560
2688
  import { formatEther as formatEther2, padHex as padHex2 } from "viem";
2561
2689
 
2562
2690
  // src/utils/prompt.ts
2563
- import { password, confirm, select, input } from "@inquirer/prompts";
2564
- async function askConfirm(message, defaultValue = false) {
2565
- return confirm({ message, default: defaultValue });
2566
- }
2691
+ import { password, select, input } from "@inquirer/prompts";
2567
2692
  async function askSelect(message, choices) {
2568
2693
  return select({ message, choices });
2569
2694
  }
@@ -3272,11 +3397,6 @@ function registerTxCommand(program2, ctx) {
3272
3397
  estimatedGas: estimatedGas.toString()
3273
3398
  }
3274
3399
  }, null, 2));
3275
- const confirmed = await askConfirm("Sign and send this transaction?");
3276
- if (!confirmed) {
3277
- outputResult({ status: "cancelled" });
3278
- return;
3279
- }
3280
3400
  const spinner = ora3("Signing UserOperation...").start();
3281
3401
  let opHash;
3282
3402
  try {
@@ -4175,13 +4295,6 @@ function registerSecurityCommand(program2, ctx) {
4175
4295
  if (![1, 2, 3].includes(capabilityFlags)) {
4176
4296
  throw new SecurityError(ERR_INTERNAL3, "Invalid capability flags. Use 1, 2, or 3.");
4177
4297
  }
4178
- const confirmed = await askConfirm(
4179
- `Install SecurityHook on ${account.alias} (${address(account.address)})? Capability: ${CAPABILITY_LABELS[capabilityFlags]}, Safety Delay: ${DEFAULT_SAFETY_DELAY}s`
4180
- );
4181
- if (!confirmed) {
4182
- outputResult({ status: "cancelled" });
4183
- return;
4184
- }
4185
4298
  const installTx = encodeInstallHook(account.address, hookAddress, DEFAULT_SAFETY_DELAY, capabilityFlags);
4186
4299
  const buildSpinner = ora5("Building UserOp...").start();
4187
4300
  try {
@@ -4312,11 +4425,6 @@ async function handleForceExecute(ctx, chainConfig, account, currentStatus) {
4312
4425
  `Safety delay not elapsed. Available after ${currentStatus.forceUninstall.availableAfter}.`
4313
4426
  );
4314
4427
  }
4315
- const confirmed = await askConfirm(`Execute force uninstall on ${account.alias} (${address(account.address)})? This will remove the SecurityHook.`);
4316
- if (!confirmed) {
4317
- outputResult({ status: "cancelled" });
4318
- return;
4319
- }
4320
4428
  const uninstallTx = encodeUninstallHook(account.address, currentStatus.hookAddress);
4321
4429
  const spinner = ora5("Executing force uninstall...").start();
4322
4430
  try {
@@ -4338,13 +4446,6 @@ async function handleForceStart(ctx, chainConfig, account, currentStatus, hookAd
4338
4446
  });
4339
4447
  return;
4340
4448
  }
4341
- const confirmed = await askConfirm(
4342
- `Start force-uninstall countdown on ${account.alias} (${address(account.address)})? You must wait ${DEFAULT_SAFETY_DELAY}s before executing.`
4343
- );
4344
- if (!confirmed) {
4345
- outputResult({ status: "cancelled" });
4346
- return;
4347
- }
4348
4449
  const preUninstallTx = encodeForcePreUninstall(hookAddress);
4349
4450
  const spinner = ora5("Starting force-uninstall countdown...").start();
4350
4451
  try {
@@ -4362,11 +4463,6 @@ async function handleForceStart(ctx, chainConfig, account, currentStatus, hookAd
4362
4463
  }
4363
4464
  }
4364
4465
  async function handleNormalUninstall(ctx, chainConfig, account, hookService, hookAddress) {
4365
- const confirmed = await askConfirm(`Uninstall SecurityHook from ${account.alias} (${address(account.address)})? (requires 2FA approval)`);
4366
- if (!confirmed) {
4367
- outputResult({ status: "cancelled" });
4368
- return;
4369
- }
4370
4466
  const uninstallTx = encodeUninstallHook(account.address, hookAddress);
4371
4467
  const spinner = ora5("Building UserOp...").start();
4372
4468
  try {
@@ -4716,7 +4812,7 @@ import { execSync } from "child_process";
4716
4812
  import { createRequire } from "module";
4717
4813
  function resolveVersion() {
4718
4814
  if (true) {
4719
- return "0.6.0";
4815
+ return "0.6.2";
4720
4816
  }
4721
4817
  try {
4722
4818
  const require2 = createRequire(import.meta.url);
@@ -4898,6 +4994,7 @@ async function main() {
4898
4994
  outputError(-32e3, sanitizeErrorMessage(err.message));
4899
4995
  } finally {
4900
4996
  ctx?.keyring.lock();
4997
+ ctx?.chain.lockUserKeys();
4901
4998
  }
4902
4999
  }
4903
5000
  main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elytro/cli",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Elytro ERC-4337 Smart Account Wallet CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -50,6 +50,7 @@
50
50
  },
51
51
  "devDependencies": {
52
52
  "@types/node": "^22.5.4",
53
+ "esbuild": "^0.27.4",
53
54
  "tsup": "^8.0.0",
54
55
  "tsx": "^4.0.0",
55
56
  "typescript": "^5.5.3"