@elytro/cli 0.5.0 → 0.5.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 +239 -69
  2. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -233,9 +233,9 @@ var FileStore = class {
233
233
  return join(this.root, `${key}.json`);
234
234
  }
235
235
  async load(key) {
236
- const path = this.filePath(key);
236
+ const path2 = this.filePath(key);
237
237
  try {
238
- const raw = await readFile(path, "utf-8");
238
+ const raw = await readFile(path2, "utf-8");
239
239
  return JSON.parse(raw);
240
240
  } catch (err) {
241
241
  if (err.code === "ENOENT") {
@@ -245,15 +245,15 @@ var FileStore = class {
245
245
  }
246
246
  }
247
247
  async save(key, data) {
248
- const path = this.filePath(key);
249
- await mkdir(dirname(path), { recursive: true });
250
- await writeFile(path, JSON.stringify(data, null, 2), "utf-8");
248
+ const path2 = this.filePath(key);
249
+ await mkdir(dirname(path2), { recursive: true });
250
+ await writeFile(path2, JSON.stringify(data, null, 2), "utf-8");
251
251
  }
252
252
  async remove(key) {
253
- const { unlink } = await import("fs/promises");
254
- const path = this.filePath(key);
253
+ const { unlink: unlink2 } = await import("fs/promises");
254
+ const path2 = this.filePath(key);
255
255
  try {
256
- await unlink(path);
256
+ await unlink2(path2);
257
257
  } catch (err) {
258
258
  if (err.code !== "ENOENT") {
259
259
  throw err;
@@ -261,9 +261,9 @@ var FileStore = class {
261
261
  }
262
262
  }
263
263
  async exists(key) {
264
- const path = this.filePath(key);
264
+ const path2 = this.filePath(key);
265
265
  try {
266
- await access(path);
266
+ await access(path2);
267
267
  return true;
268
268
  } catch {
269
269
  return false;
@@ -521,6 +521,7 @@ var PUBLIC_RPC = {
521
521
  1: "https://ethereum-rpc.publicnode.com",
522
522
  10: "https://optimism-rpc.publicnode.com",
523
523
  42161: "https://arbitrum-one-rpc.publicnode.com",
524
+ 8453: "https://base-rpc.publicnode.com",
524
525
  11155111: "https://ethereum-sepolia-rpc.publicnode.com",
525
526
  11155420: "https://optimism-sepolia-rpc.publicnode.com"
526
527
  };
@@ -528,6 +529,7 @@ var PUBLIC_BUNDLER = {
528
529
  1: "https://public.pimlico.io/v2/1/rpc",
529
530
  10: "https://public.pimlico.io/v2/10/rpc",
530
531
  42161: "https://public.pimlico.io/v2/42161/rpc",
532
+ 8453: "https://public.pimlico.io/v2/8453/rpc",
531
533
  11155111: "https://public.pimlico.io/v2/11155111/rpc",
532
534
  11155420: "https://public.pimlico.io/v2/11155420/rpc"
533
535
  };
@@ -541,6 +543,7 @@ var ALCHEMY_NETWORK = {
541
543
  1: "eth-mainnet",
542
544
  10: "opt-mainnet",
543
545
  42161: "arb-mainnet",
546
+ 8453: "base-mainnet",
544
547
  11155111: "eth-sepolia",
545
548
  11155420: "opt-sepolia"
546
549
  };
@@ -548,6 +551,7 @@ var PIMLICO_SLUG = {
548
551
  1: "ethereum",
549
552
  10: "optimism",
550
553
  42161: "arbitrum",
554
+ 8453: "base",
551
555
  11155111: "sepolia",
552
556
  11155420: "optimism-sepolia"
553
557
  };
@@ -584,6 +588,12 @@ var CHAIN_META = [
584
588
  nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
585
589
  blockExplorer: "https://arbiscan.io"
586
590
  },
591
+ {
592
+ id: 8453,
593
+ name: "Base",
594
+ nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
595
+ blockExplorer: "https://basescan.org"
596
+ },
587
597
  {
588
598
  id: 11155111,
589
599
  name: "Sepolia",
@@ -1434,6 +1444,27 @@ var AccountService = class {
1434
1444
  await this.persist();
1435
1445
  return account;
1436
1446
  }
1447
+ // ─── Rename ───────────────────────────────────────────────────────
1448
+ /**
1449
+ * Rename an account's alias.
1450
+ * @param aliasOrAddress - Current alias or address to identify the account.
1451
+ * @param newAlias - The new alias. Must be unique.
1452
+ */
1453
+ async renameAccount(aliasOrAddress, newAlias) {
1454
+ const account = this.resolveAccount(aliasOrAddress);
1455
+ if (!account) {
1456
+ throw new Error(`Account "${aliasOrAddress}" not found.`);
1457
+ }
1458
+ const conflict = this.state.accounts.find(
1459
+ (a) => a.alias.toLowerCase() === newAlias.toLowerCase() && a.address !== account.address
1460
+ );
1461
+ if (conflict) {
1462
+ throw new Error(`Alias "${newAlias}" is already taken by ${conflict.address}.`);
1463
+ }
1464
+ account.alias = newAlias;
1465
+ await this.persist();
1466
+ return account;
1467
+ }
1437
1468
  // ─── Activation ───────────────────────────────────────────────────
1438
1469
  /**
1439
1470
  * Mark an account as deployed on-chain.
@@ -2141,120 +2172,227 @@ var SecurityHookService = class {
2141
2172
  }
2142
2173
  };
2143
2174
 
2144
- // src/providers/keychainProvider.ts
2145
- import { execFile } from "child_process";
2146
- import { promisify } from "util";
2147
- var execFileAsync = promisify(execFile);
2148
- var KeychainProvider = class {
2149
- name = "macos-keychain";
2175
+ // src/providers/keyringProvider.ts
2176
+ import { Entry } from "@napi-rs/keyring";
2177
+ var KeyringProvider = class {
2178
+ name;
2150
2179
  service = "elytro-wallet";
2151
2180
  account = "vault-key";
2181
+ constructor() {
2182
+ const platform = process.platform;
2183
+ if (platform === "darwin") this.name = "macos-keychain";
2184
+ else if (platform === "win32") this.name = "windows-credential-manager";
2185
+ else this.name = "linux-secret-service";
2186
+ }
2152
2187
  async available() {
2153
- if (process.platform !== "darwin") return false;
2154
2188
  try {
2155
- await execFileAsync("security", ["help"], { timeout: 5e3 });
2189
+ const entry = new Entry(this.service, this.account);
2190
+ entry.getSecret();
2156
2191
  return true;
2157
- } catch {
2158
- return process.platform === "darwin";
2192
+ } catch (err) {
2193
+ const msg = err.message || "";
2194
+ if (isNotFoundError(msg)) return true;
2195
+ return false;
2159
2196
  }
2160
2197
  }
2161
2198
  async store(secret) {
2162
2199
  validateKeyLength(secret);
2200
+ try {
2201
+ const entry = new Entry(this.service, this.account);
2202
+ entry.setSecret(Buffer.from(secret));
2203
+ } catch (err) {
2204
+ throw new Error(`Failed to store vault key in OS credential store: ${err.message}`);
2205
+ }
2206
+ }
2207
+ async load() {
2208
+ try {
2209
+ const entry = new Entry(this.service, this.account);
2210
+ const raw = entry.getSecret();
2211
+ if (!raw || raw.length === 0) return null;
2212
+ const key = new Uint8Array(raw);
2213
+ if (key.length !== 32) {
2214
+ throw new Error(
2215
+ `OS credential store returned vault key with invalid length: expected 32 bytes, got ${key.length}.`
2216
+ );
2217
+ }
2218
+ return key;
2219
+ } catch (err) {
2220
+ const msg = err.message || "";
2221
+ if (isNotFoundError(msg)) return null;
2222
+ throw new Error(`Failed to load vault key from OS credential store: ${msg}`);
2223
+ }
2224
+ }
2225
+ async delete() {
2226
+ try {
2227
+ const entry = new Entry(this.service, this.account);
2228
+ entry.deleteCredential();
2229
+ } catch {
2230
+ }
2231
+ }
2232
+ };
2233
+ function isNotFoundError(msg) {
2234
+ const lower = msg.toLowerCase();
2235
+ return lower.includes("not found") || lower.includes("no matching") || lower.includes("no such") || lower.includes("itemnotfound") || lower.includes("element not found") || // Windows
2236
+ lower.includes("no result") || lower.includes("no password");
2237
+ }
2238
+ function validateKeyLength(key) {
2239
+ if (key.length !== 32) {
2240
+ throw new Error(`Invalid vault key: expected 32 bytes, got ${key.length}.`);
2241
+ }
2242
+ }
2243
+
2244
+ // src/providers/fileProvider.ts
2245
+ import * as fs from "fs/promises";
2246
+ import * as path from "path";
2247
+ import { constants } from "fs";
2248
+ var FileProvider = class {
2249
+ name = "file-protected";
2250
+ keyPath;
2251
+ /**
2252
+ * @param dataDir — the ~/.elytro/ directory path (from FileStore.dataDir).
2253
+ * Defaults to ~/.elytro if not provided.
2254
+ */
2255
+ constructor(dataDir) {
2256
+ const base = dataDir ?? path.join(process.env.HOME || "~", ".elytro");
2257
+ this.keyPath = path.join(base, ".vault-key");
2258
+ }
2259
+ /**
2260
+ * FileProvider is always technically available on any OS (filesystem always exists).
2261
+ * But it should only be used on Linux when the KeyringProvider is not available.
2262
+ * The resolution logic in resolveProvider handles this gating — FileProvider
2263
+ * itself does not restrict by platform.
2264
+ */
2265
+ async available() {
2266
+ try {
2267
+ await fs.access(this.keyPath, constants.R_OK);
2268
+ return true;
2269
+ } catch {
2270
+ try {
2271
+ await fs.access(path.dirname(this.keyPath), constants.W_OK);
2272
+ return true;
2273
+ } catch {
2274
+ return false;
2275
+ }
2276
+ }
2277
+ }
2278
+ async store(secret) {
2279
+ validateKeyLength2(secret);
2163
2280
  const b64 = Buffer.from(secret).toString("base64");
2281
+ const tmpPath = this.keyPath + ".tmp";
2164
2282
  try {
2165
- await execFileAsync("security", [
2166
- "add-generic-password",
2167
- "-U",
2168
- "-s",
2169
- this.service,
2170
- "-a",
2171
- this.account,
2172
- "-w",
2173
- b64
2174
- ]);
2283
+ await fs.writeFile(tmpPath, b64, { encoding: "utf-8", mode: 384 });
2284
+ await fs.rename(tmpPath, this.keyPath);
2285
+ await fs.chmod(this.keyPath, 384);
2175
2286
  } catch (err) {
2176
- throw new Error(`Failed to store vault key in Keychain: ${err.message}`);
2287
+ try {
2288
+ await fs.unlink(tmpPath);
2289
+ } catch {
2290
+ }
2291
+ throw new Error(`Failed to store vault key to file: ${err.message}`);
2177
2292
  }
2178
2293
  }
2179
2294
  async load() {
2180
2295
  try {
2181
- const { stdout } = await execFileAsync("security", [
2182
- "find-generic-password",
2183
- "-s",
2184
- this.service,
2185
- "-a",
2186
- this.account,
2187
- "-w"
2188
- ]);
2189
- const trimmed = stdout.trim();
2296
+ const stat2 = await fs.stat(this.keyPath);
2297
+ const mode = stat2.mode & 511;
2298
+ if (mode !== 384) {
2299
+ throw new Error(
2300
+ `Vault key file has insecure permissions: ${modeToOctal(mode)} (expected 0600).
2301
+ Fix with: chmod 600 ${this.keyPath}
2302
+ Refusing to load until permissions are corrected.`
2303
+ );
2304
+ }
2305
+ const raw = await fs.readFile(this.keyPath, "utf-8");
2306
+ const trimmed = raw.trim();
2190
2307
  if (!trimmed) return null;
2191
2308
  const key = Buffer.from(trimmed, "base64");
2192
2309
  if (key.length !== 32) {
2193
- throw new Error(`Keychain vault key has invalid length: expected 32 bytes, got ${key.length}.`);
2310
+ throw new Error(
2311
+ `Vault key file has invalid content: expected 32 bytes (base64), got ${key.length}.`
2312
+ );
2194
2313
  }
2195
2314
  return new Uint8Array(key);
2196
2315
  } catch (err) {
2197
2316
  const msg = err.message || "";
2198
- if (msg.includes("could not be found") || msg.includes("SecKeychainSearchCopyNext")) {
2199
- return null;
2200
- }
2201
- throw new Error(`Failed to load vault key from Keychain: ${msg}`);
2317
+ if (msg.includes("ENOENT")) return null;
2318
+ throw err;
2202
2319
  }
2203
2320
  }
2204
2321
  async delete() {
2205
2322
  try {
2206
- await execFileAsync("security", ["delete-generic-password", "-s", this.service, "-a", this.account]);
2323
+ await fs.unlink(this.keyPath);
2207
2324
  } catch {
2208
2325
  }
2209
2326
  }
2210
2327
  };
2211
- function validateKeyLength(key) {
2328
+ function validateKeyLength2(key) {
2212
2329
  if (key.length !== 32) {
2213
2330
  throw new Error(`Invalid vault key: expected 32 bytes, got ${key.length}.`);
2214
2331
  }
2215
2332
  }
2333
+ function modeToOctal(mode) {
2334
+ return "0" + mode.toString(8);
2335
+ }
2216
2336
 
2217
2337
  // src/providers/envVarProvider.ts
2218
2338
  var ENV_KEY = "ELYTRO_VAULT_SECRET";
2339
+ var ENV_ALLOW = "ELYTRO_ALLOW_ENV";
2219
2340
  var EnvVarProvider = class {
2220
2341
  name = "env-var";
2221
2342
  async available() {
2222
- return !!process.env[ENV_KEY];
2343
+ return !!process.env[ENV_KEY] && process.env[ENV_ALLOW] === "1";
2223
2344
  }
2224
2345
  async store(_secret) {
2225
2346
  throw new Error(
2226
- "EnvVarProvider is read-only. Cannot store vault key in an environment variable. Use a persistent provider (macOS Keychain) or store the secret manually."
2347
+ "EnvVarProvider is read-only. Cannot store vault key in an environment variable.\nUse a persistent provider (OS keychain or file-protected) or store the secret manually."
2227
2348
  );
2228
2349
  }
2229
2350
  async load() {
2351
+ if (process.env[ENV_ALLOW] !== "1") return null;
2230
2352
  const raw = process.env[ENV_KEY];
2231
2353
  if (!raw) return null;
2232
2354
  delete process.env[ENV_KEY];
2355
+ delete process.env[ENV_ALLOW];
2233
2356
  const key = Buffer.from(raw, "base64");
2234
2357
  if (key.length !== 32) {
2235
2358
  throw new Error(
2236
- `${ENV_KEY} has invalid length: expected 32 bytes (base64), got ${key.length}. The value must be a base64-encoded 256-bit key.`
2359
+ `${ENV_KEY} has invalid length: expected 32 bytes (base64), got ${key.length}.
2360
+ The value must be a base64-encoded 256-bit key.`
2237
2361
  );
2238
2362
  }
2239
2363
  return new Uint8Array(key);
2240
2364
  }
2241
2365
  async delete() {
2242
2366
  delete process.env[ENV_KEY];
2367
+ delete process.env[ENV_ALLOW];
2243
2368
  }
2244
2369
  };
2245
2370
 
2246
2371
  // src/providers/resolveProvider.ts
2247
2372
  async function resolveProvider() {
2248
- const keychainProvider = new KeychainProvider();
2373
+ const keyringProvider = new KeyringProvider();
2374
+ const fileProvider = new FileProvider();
2249
2375
  const envProvider = new EnvVarProvider();
2250
- const initProvider = await keychainProvider.available() ? keychainProvider : null;
2251
- let loadProvider = null;
2252
- if (await keychainProvider.available()) {
2253
- loadProvider = keychainProvider;
2254
- } else if (await envProvider.available()) {
2255
- loadProvider = envProvider;
2256
- }
2257
- return { initProvider, loadProvider };
2376
+ if (await keyringProvider.available()) {
2377
+ return {
2378
+ initProvider: keyringProvider,
2379
+ loadProvider: keyringProvider
2380
+ };
2381
+ }
2382
+ if (process.platform === "linux" && await fileProvider.available()) {
2383
+ return {
2384
+ initProvider: fileProvider,
2385
+ loadProvider: fileProvider
2386
+ };
2387
+ }
2388
+ if (await envProvider.available()) {
2389
+ return {
2390
+ initProvider: null,
2391
+ // Cannot store via env var
2392
+ loadProvider: envProvider
2393
+ };
2394
+ }
2395
+ return { initProvider: null, loadProvider: null };
2258
2396
  }
2259
2397
 
2260
2398
  // src/context.ts
@@ -2274,14 +2412,15 @@ async function createAppContext() {
2274
2412
  if (isInitialized) {
2275
2413
  if (!loadProvider) {
2276
2414
  throw new Error(
2277
- "Wallet is initialized but no secret provider is available.\n" + (process.platform === "darwin" ? "Keychain access failed. Check macOS Keychain permissions." : "Set the ELYTRO_VAULT_SECRET environment variable.")
2415
+ "Wallet is initialized but no secret provider is available.\n" + noProviderHint()
2278
2416
  );
2279
2417
  }
2280
2418
  const vaultKey = await loadProvider.load();
2281
2419
  if (!vaultKey) {
2282
2420
  throw new Error(
2283
2421
  `Wallet is initialized but vault key not found in ${loadProvider.name}.
2284
- ` + (process.platform === "darwin" ? "The Keychain item may have been deleted. Re-run `elytro init` to create a new wallet,\nor import a backup with `elytro import`." : "Set ELYTRO_VAULT_SECRET to the base64-encoded vault key.")
2422
+ The credential may have been deleted. Re-run \`elytro init\` to create a new wallet,
2423
+ or import a backup with \`elytro import\`.`
2285
2424
  );
2286
2425
  }
2287
2426
  try {
@@ -2316,6 +2455,16 @@ The vault key may not match the encrypted keyring. Re-run \`elytro init\` or imp
2316
2455
  }
2317
2456
  return { store, keyring, chain, sdk, walletClient, account, secretProvider: loadProvider };
2318
2457
  }
2458
+ function noProviderHint() {
2459
+ switch (process.platform) {
2460
+ case "darwin":
2461
+ return "macOS Keychain access failed. Check Keychain permissions or security settings.";
2462
+ case "win32":
2463
+ return "Windows Credential Manager access failed. Run as the same user who initialized the wallet.";
2464
+ default:
2465
+ return "No secret provider available. Options:\n 1. Install and start a Secret Service provider (GNOME Keyring or KWallet)\n 2. The vault key file (~/.elytro/.vault-key) may have been deleted\n 3. For CI: set ELYTRO_VAULT_SECRET and ELYTRO_ALLOW_ENV=1";
2466
+ }
2467
+ }
2319
2468
 
2320
2469
  // src/commands/init.ts
2321
2470
  import { webcrypto as webcrypto2 } from "crypto";
@@ -2386,7 +2535,9 @@ function registerInitCommand(program2, ctx) {
2386
2535
  dataDir: ctx.store.dataDir,
2387
2536
  secretProvider: providerName,
2388
2537
  ...vaultSecretB64 ? { vaultSecret: vaultSecretB64 } : {},
2389
- ...vaultSecretB64 ? { hint: "Save ELYTRO_VAULT_SECRET \u2014 it will NOT be shown again." } : {},
2538
+ ...vaultSecretB64 ? {
2539
+ hint: "No persistent secret provider available. Save this vault key securely \u2014 it will NOT be shown again.\nFor CI: set ELYTRO_VAULT_SECRET=<key> and ELYTRO_ALLOW_ENV=1."
2540
+ } : {},
2390
2541
  nextStep: "Run `elytro account create --chain <chainId>` to create your first smart account."
2391
2542
  });
2392
2543
  } catch (err) {
@@ -2463,6 +2614,14 @@ function registerAccountCommand(program2, ctx) {
2463
2614
  outputError(ERR_INVALID_PARAMS, "Invalid chain ID.", { chain: opts.chain });
2464
2615
  return;
2465
2616
  }
2617
+ const chainConfig = ctx.chain.chains.find((c) => c.id === chainId);
2618
+ if (!chainConfig) {
2619
+ const supported = ctx.chain.chains.map((c) => `${c.id} (${c.name})`);
2620
+ outputError(ERR_INVALID_PARAMS, `Chain ${chainId} is not supported.`, {
2621
+ supportedChains: supported
2622
+ });
2623
+ return;
2624
+ }
2466
2625
  let dailyLimitUsd;
2467
2626
  if (opts.dailyLimit !== void 0) {
2468
2627
  dailyLimitUsd = parseFloat(opts.dailyLimit);
@@ -2477,12 +2636,9 @@ function registerAccountCommand(program2, ctx) {
2477
2636
  } : void 0;
2478
2637
  const spinner = ora2("Creating smart account...").start();
2479
2638
  try {
2480
- const chainConfig = ctx.chain.chains.find((c) => c.id === chainId);
2481
- const chainName = chainConfig?.name ?? String(chainId);
2482
- if (chainConfig) {
2483
- await ctx.sdk.initForChain(chainConfig);
2484
- ctx.walletClient.initForChain(chainConfig);
2485
- }
2639
+ const chainName = chainConfig.name;
2640
+ await ctx.sdk.initForChain(chainConfig);
2641
+ ctx.walletClient.initForChain(chainConfig);
2486
2642
  const accountInfo = await ctx.account.createAccount(chainId, opts.alias, securityIntent);
2487
2643
  spinner.text = "Registering with backend...";
2488
2644
  const { guardianHash, guardianSafePeriod } = ctx.sdk.initDefaults;
@@ -2738,6 +2894,20 @@ function registerAccountCommand(program2, ctx) {
2738
2894
  outputError(ERR_INTERNAL, err.message);
2739
2895
  }
2740
2896
  });
2897
+ account.command("rename").description("Rename an account alias").argument("<account>", "Current alias or address").argument("<newAlias>", "New alias").action(async (target, newAlias) => {
2898
+ try {
2899
+ const renamed = await ctx.account.renameAccount(target, newAlias);
2900
+ const chainConfig = ctx.chain.chains.find((c) => c.id === renamed.chainId);
2901
+ outputResult({
2902
+ alias: renamed.alias,
2903
+ address: renamed.address,
2904
+ chain: chainConfig?.name ?? String(renamed.chainId),
2905
+ chainId: renamed.chainId
2906
+ });
2907
+ } catch (err) {
2908
+ outputError(ERR_INTERNAL, err.message);
2909
+ }
2910
+ });
2741
2911
  account.command("switch").description("Switch the active account").argument("[account]", "Alias or address").action(async (target) => {
2742
2912
  const accounts = ctx.account.allAccounts;
2743
2913
  if (accounts.length === 0) {
@@ -4228,7 +4398,7 @@ import { execSync } from "child_process";
4228
4398
  import { createRequire } from "module";
4229
4399
  function resolveVersion() {
4230
4400
  if (true) {
4231
- return "0.5.0";
4401
+ return "0.5.2";
4232
4402
  }
4233
4403
  try {
4234
4404
  const require2 = createRequire(import.meta.url);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elytro/cli",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Elytro ERC-4337 Smart Account Wallet CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -41,6 +41,7 @@
41
41
  "@elytro/abi": "latest",
42
42
  "@elytro/sdk": "latest",
43
43
  "@inquirer/prompts": "^7.0.0",
44
+ "@napi-rs/keyring": "^1.2.0",
44
45
  "chalk": "^5.3.0",
45
46
  "commander": "^13.0.0",
46
47
  "graphql": "^16.9.0",