@elytro/cli 0.5.1 → 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 +229 -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;
@@ -1444,6 +1444,27 @@ var AccountService = class {
1444
1444
  await this.persist();
1445
1445
  return account;
1446
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
+ }
1447
1468
  // ─── Activation ───────────────────────────────────────────────────
1448
1469
  /**
1449
1470
  * Mark an account as deployed on-chain.
@@ -2151,120 +2172,227 @@ var SecurityHookService = class {
2151
2172
  }
2152
2173
  };
2153
2174
 
2154
- // src/providers/keychainProvider.ts
2155
- import { execFile } from "child_process";
2156
- import { promisify } from "util";
2157
- var execFileAsync = promisify(execFile);
2158
- var KeychainProvider = class {
2159
- name = "macos-keychain";
2175
+ // src/providers/keyringProvider.ts
2176
+ import { Entry } from "@napi-rs/keyring";
2177
+ var KeyringProvider = class {
2178
+ name;
2160
2179
  service = "elytro-wallet";
2161
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
+ }
2162
2187
  async available() {
2163
- if (process.platform !== "darwin") return false;
2164
2188
  try {
2165
- await execFileAsync("security", ["help"], { timeout: 5e3 });
2189
+ const entry = new Entry(this.service, this.account);
2190
+ entry.getSecret();
2166
2191
  return true;
2167
- } catch {
2168
- return process.platform === "darwin";
2192
+ } catch (err) {
2193
+ const msg = err.message || "";
2194
+ if (isNotFoundError(msg)) return true;
2195
+ return false;
2169
2196
  }
2170
2197
  }
2171
2198
  async store(secret) {
2172
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);
2173
2280
  const b64 = Buffer.from(secret).toString("base64");
2281
+ const tmpPath = this.keyPath + ".tmp";
2174
2282
  try {
2175
- await execFileAsync("security", [
2176
- "add-generic-password",
2177
- "-U",
2178
- "-s",
2179
- this.service,
2180
- "-a",
2181
- this.account,
2182
- "-w",
2183
- b64
2184
- ]);
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);
2185
2286
  } catch (err) {
2186
- 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}`);
2187
2292
  }
2188
2293
  }
2189
2294
  async load() {
2190
2295
  try {
2191
- const { stdout } = await execFileAsync("security", [
2192
- "find-generic-password",
2193
- "-s",
2194
- this.service,
2195
- "-a",
2196
- this.account,
2197
- "-w"
2198
- ]);
2199
- 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();
2200
2307
  if (!trimmed) return null;
2201
2308
  const key = Buffer.from(trimmed, "base64");
2202
2309
  if (key.length !== 32) {
2203
- 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
+ );
2204
2313
  }
2205
2314
  return new Uint8Array(key);
2206
2315
  } catch (err) {
2207
2316
  const msg = err.message || "";
2208
- if (msg.includes("could not be found") || msg.includes("SecKeychainSearchCopyNext")) {
2209
- return null;
2210
- }
2211
- throw new Error(`Failed to load vault key from Keychain: ${msg}`);
2317
+ if (msg.includes("ENOENT")) return null;
2318
+ throw err;
2212
2319
  }
2213
2320
  }
2214
2321
  async delete() {
2215
2322
  try {
2216
- await execFileAsync("security", ["delete-generic-password", "-s", this.service, "-a", this.account]);
2323
+ await fs.unlink(this.keyPath);
2217
2324
  } catch {
2218
2325
  }
2219
2326
  }
2220
2327
  };
2221
- function validateKeyLength(key) {
2328
+ function validateKeyLength2(key) {
2222
2329
  if (key.length !== 32) {
2223
2330
  throw new Error(`Invalid vault key: expected 32 bytes, got ${key.length}.`);
2224
2331
  }
2225
2332
  }
2333
+ function modeToOctal(mode) {
2334
+ return "0" + mode.toString(8);
2335
+ }
2226
2336
 
2227
2337
  // src/providers/envVarProvider.ts
2228
2338
  var ENV_KEY = "ELYTRO_VAULT_SECRET";
2339
+ var ENV_ALLOW = "ELYTRO_ALLOW_ENV";
2229
2340
  var EnvVarProvider = class {
2230
2341
  name = "env-var";
2231
2342
  async available() {
2232
- return !!process.env[ENV_KEY];
2343
+ return !!process.env[ENV_KEY] && process.env[ENV_ALLOW] === "1";
2233
2344
  }
2234
2345
  async store(_secret) {
2235
2346
  throw new Error(
2236
- "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."
2237
2348
  );
2238
2349
  }
2239
2350
  async load() {
2351
+ if (process.env[ENV_ALLOW] !== "1") return null;
2240
2352
  const raw = process.env[ENV_KEY];
2241
2353
  if (!raw) return null;
2242
2354
  delete process.env[ENV_KEY];
2355
+ delete process.env[ENV_ALLOW];
2243
2356
  const key = Buffer.from(raw, "base64");
2244
2357
  if (key.length !== 32) {
2245
2358
  throw new Error(
2246
- `${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.`
2247
2361
  );
2248
2362
  }
2249
2363
  return new Uint8Array(key);
2250
2364
  }
2251
2365
  async delete() {
2252
2366
  delete process.env[ENV_KEY];
2367
+ delete process.env[ENV_ALLOW];
2253
2368
  }
2254
2369
  };
2255
2370
 
2256
2371
  // src/providers/resolveProvider.ts
2257
2372
  async function resolveProvider() {
2258
- const keychainProvider = new KeychainProvider();
2373
+ const keyringProvider = new KeyringProvider();
2374
+ const fileProvider = new FileProvider();
2259
2375
  const envProvider = new EnvVarProvider();
2260
- const initProvider = await keychainProvider.available() ? keychainProvider : null;
2261
- let loadProvider = null;
2262
- if (await keychainProvider.available()) {
2263
- loadProvider = keychainProvider;
2264
- } else if (await envProvider.available()) {
2265
- loadProvider = envProvider;
2266
- }
2267
- 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 };
2268
2396
  }
2269
2397
 
2270
2398
  // src/context.ts
@@ -2284,14 +2412,15 @@ async function createAppContext() {
2284
2412
  if (isInitialized) {
2285
2413
  if (!loadProvider) {
2286
2414
  throw new Error(
2287
- "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()
2288
2416
  );
2289
2417
  }
2290
2418
  const vaultKey = await loadProvider.load();
2291
2419
  if (!vaultKey) {
2292
2420
  throw new Error(
2293
2421
  `Wallet is initialized but vault key not found in ${loadProvider.name}.
2294
- ` + (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\`.`
2295
2424
  );
2296
2425
  }
2297
2426
  try {
@@ -2326,6 +2455,16 @@ The vault key may not match the encrypted keyring. Re-run \`elytro init\` or imp
2326
2455
  }
2327
2456
  return { store, keyring, chain, sdk, walletClient, account, secretProvider: loadProvider };
2328
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
+ }
2329
2468
 
2330
2469
  // src/commands/init.ts
2331
2470
  import { webcrypto as webcrypto2 } from "crypto";
@@ -2396,7 +2535,9 @@ function registerInitCommand(program2, ctx) {
2396
2535
  dataDir: ctx.store.dataDir,
2397
2536
  secretProvider: providerName,
2398
2537
  ...vaultSecretB64 ? { vaultSecret: vaultSecretB64 } : {},
2399
- ...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
+ } : {},
2400
2541
  nextStep: "Run `elytro account create --chain <chainId>` to create your first smart account."
2401
2542
  });
2402
2543
  } catch (err) {
@@ -2473,6 +2614,14 @@ function registerAccountCommand(program2, ctx) {
2473
2614
  outputError(ERR_INVALID_PARAMS, "Invalid chain ID.", { chain: opts.chain });
2474
2615
  return;
2475
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
+ }
2476
2625
  let dailyLimitUsd;
2477
2626
  if (opts.dailyLimit !== void 0) {
2478
2627
  dailyLimitUsd = parseFloat(opts.dailyLimit);
@@ -2487,12 +2636,9 @@ function registerAccountCommand(program2, ctx) {
2487
2636
  } : void 0;
2488
2637
  const spinner = ora2("Creating smart account...").start();
2489
2638
  try {
2490
- const chainConfig = ctx.chain.chains.find((c) => c.id === chainId);
2491
- const chainName = chainConfig?.name ?? String(chainId);
2492
- if (chainConfig) {
2493
- await ctx.sdk.initForChain(chainConfig);
2494
- ctx.walletClient.initForChain(chainConfig);
2495
- }
2639
+ const chainName = chainConfig.name;
2640
+ await ctx.sdk.initForChain(chainConfig);
2641
+ ctx.walletClient.initForChain(chainConfig);
2496
2642
  const accountInfo = await ctx.account.createAccount(chainId, opts.alias, securityIntent);
2497
2643
  spinner.text = "Registering with backend...";
2498
2644
  const { guardianHash, guardianSafePeriod } = ctx.sdk.initDefaults;
@@ -2748,6 +2894,20 @@ function registerAccountCommand(program2, ctx) {
2748
2894
  outputError(ERR_INTERNAL, err.message);
2749
2895
  }
2750
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
+ });
2751
2911
  account.command("switch").description("Switch the active account").argument("[account]", "Alias or address").action(async (target) => {
2752
2912
  const accounts = ctx.account.allAccounts;
2753
2913
  if (accounts.length === 0) {
@@ -4238,7 +4398,7 @@ import { execSync } from "child_process";
4238
4398
  import { createRequire } from "module";
4239
4399
  function resolveVersion() {
4240
4400
  if (true) {
4241
- return "0.5.1";
4401
+ return "0.5.2";
4242
4402
  }
4243
4403
  try {
4244
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.1",
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",