@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.
- package/dist/index.js +144 -47
- 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:
|
|
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.
|
|
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
|
|
501
|
-
|
|
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
|
|
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.
|
|
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
|
-
/**
|
|
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.
|
|
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.
|
|
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(
|
|
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 {
|
|
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,
|
|
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.
|
|
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.
|
|
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"
|