@candledottv/cli 0.11.3 → 0.11.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.
Files changed (2) hide show
  1. package/dist/index.js +345 -37
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2254,7 +2254,12 @@ var init_errors = __esm(() => {
2254
2254
  "SIGN_SIMULATION_FAILED",
2255
2255
  "SIGN_LOOKUP_TABLE_UNRESOLVED",
2256
2256
  "SIGN_TRANSACTION_UNDECODABLE",
2257
- "SIGN_BROADCAST_FAILED"
2257
+ "SIGN_BROADCAST_FAILED",
2258
+ "VAULT_LABEL_NOT_FOUND",
2259
+ "VAULT_LABEL_AMBIGUOUS",
2260
+ "VAULT_LABEL_TAKEN",
2261
+ "VAULT_LABEL_UNCHANGED",
2262
+ "VAULT_RENAME_ROLE_REFUSED"
2258
2263
  ];
2259
2264
  VaultError = class VaultError extends Error {
2260
2265
  code;
@@ -14929,6 +14934,7 @@ __export(exports_vault_support, {
14929
14934
  missingVault: () => missingVault,
14930
14935
  findExternalEntry: () => findExternalEntry,
14931
14936
  describeRole: () => describeRole,
14937
+ derivationNotice: () => derivationNotice,
14932
14938
  confirmLastSix: () => confirmLastSix,
14933
14939
  assertVaultHelperIdentities: () => assertVaultHelperIdentities,
14934
14940
  assertNotOlderCopy: () => assertNotOlderCopy,
@@ -15017,6 +15023,13 @@ async function requireVaultRaw(ctx, resolved) {
15017
15023
  throw missingVault(ctx, resolved);
15018
15024
  return raw;
15019
15025
  }
15026
+ function derivationNotice(line, purpose) {
15027
+ if (purpose === undefined)
15028
+ return line;
15029
+ return line.endsWith(`
15030
+ `) ? `${line.slice(0, -1)} -- ${purpose}
15031
+ ` : `${line} -- ${purpose}`;
15032
+ }
15020
15033
  function assertVaultHelperIdentities(deps, envelopes) {
15021
15034
  if (deps.releasePolicy.macosHelper.release === "signed") {
15022
15035
  for (const envelope of envelopes) {
@@ -15033,12 +15046,13 @@ async function unlockInteractively(ctx, path, raw, opts = {}) {
15033
15046
  assertVaultHelperIdentities(deps, file.envelopes);
15034
15047
  const facts = await currentPlatformFacts(deps);
15035
15048
  const choice = await chooseFactor(ctx, file.envelopes, facts, opts.factor ?? ctx.vaultFactor);
15036
- const notice = (line) => deps.stderr.write(line);
15049
+ const noticeFor = (purpose) => (line) => deps.stderr.write(derivationNotice(line, purpose));
15050
+ const notice = noticeFor(opts.purpose);
15037
15051
  if (choice.kind === "passphrase") {
15038
15052
  const typed = await deps.promptSecret(opts.promptText ?? "Vault passphrase (input hidden): ");
15039
- const openWith = (p, r, candidate) => choice.envelopeId === undefined ? unlockWithPassphrase(p, r, candidate, { notice }) : unlockVault(p, r, { factor: "passphrase", passphrase: candidate, envelopeId: choice.envelopeId }, { notice });
15040
- const { vault: vault2, passphrase } = await openWithTypedPassphrase(typed, (candidate) => openWith(path, raw, candidate));
15041
- const open4 = (p, r) => openWith(p, r, passphrase);
15053
+ const openWith = (p, r, candidate, purpose) => choice.envelopeId === undefined ? unlockWithPassphrase(p, r, candidate, { notice: noticeFor(purpose) }) : unlockVault(p, r, { factor: "passphrase", passphrase: candidate, envelopeId: choice.envelopeId }, { notice: noticeFor(purpose) });
15054
+ const { vault: vault2, passphrase } = await openWithTypedPassphrase(typed, (candidate) => openWith(path, raw, candidate, opts.purpose));
15055
+ const open4 = (p, r, purpose) => openWith(p, r, passphrase, purpose);
15042
15056
  return {
15043
15057
  vault: vault2,
15044
15058
  factor: { kind: "passphrase", envelopeId: vault2.envelope.id },
@@ -35788,7 +35802,7 @@ async function resolveApiKey(deps, profile) {
35788
35802
  init_render();
35789
35803
 
35790
35804
  // src/version.ts
35791
- var CLI_VERSION = "0.11.3";
35805
+ var CLI_VERSION = "0.11.4";
35792
35806
 
35793
35807
  // src/commands/auth.ts
35794
35808
  var DEVICE_CODE_PATH = "/api/v1/agent/device/code";
@@ -36351,7 +36365,11 @@ var HELP = {
36351
36365
  { invocation: "status [--unlock]", description: "What the vault holds, and what opens it" },
36352
36366
  {
36353
36367
  invocation: "new-key --chain solana [--label <name>] [--count <n>] [--labels-from <file>]",
36354
- description: "Derive the next Solana key, or n of them under one unlock"
36368
+ description: "Derive the next Solana key, or n of them under one unlock; the name must be free"
36369
+ },
36370
+ {
36371
+ invocation: "rename <label|address> <new-label> [--id <entry-id>]",
36372
+ description: "Rename one key. The address, the derivation and the key blob do not change"
36355
36373
  },
36356
36374
  { invocation: "phrase show", description: "Show the 24-word recovery phrase (terminal only)" },
36357
36375
  {
@@ -36416,6 +36434,7 @@ var HELP = {
36416
36434
  examples: [
36417
36435
  "candle vault init",
36418
36436
  "candle vault new-key --chain solana --label treasury",
36437
+ "candle vault rename key-7 treasury-cold",
36419
36438
  "candle vault new-key --chain solana --labels-from ./replacement-names.txt",
36420
36439
  "candle vault enroll security-key --label yubikey-a",
36421
36440
  "candle vault backup --to /Volumes/BACKUP/vault.enc",
@@ -41351,12 +41370,9 @@ async function vaultNewKey(args, ctx) {
41351
41370
  suggestion: "Create a second vault with a fresh root (`candle vault init`) and move the funds across with `candle vault transfer`. There is no flag for this: no fact you could assert would make the old boundary known."
41352
41371
  });
41353
41372
  }
41354
- if (batch.labels !== undefined) {
41355
- const taken = new Set(vault.index.entries.map((entry) => entry.label));
41356
- const clash = batch.labels.find((label) => taken.has(label));
41357
- if (clash !== undefined) {
41358
- return usage(ctx, `A key labelled ${clash} already exists in this vault; every --labels-from name must be new.`);
41359
- }
41373
+ const clash = labelClash(vault.index, plannedLabels(vault.index.hd, batch, parsed.values["--label"]));
41374
+ if (clash !== undefined) {
41375
+ return usage(ctx, batch.labels !== undefined ? `A key labelled ${clash} already exists in this vault; every --labels-from name must be new.` : parsed.values["--label"] !== undefined ? `A key labelled ${clash} already exists in this vault; choose another --label.` : `A key labelled ${clash} already exists in this vault; pass --label <name> to choose a different name for this key.`);
41360
41376
  }
41361
41377
  const derived = [];
41362
41378
  let last;
@@ -41452,6 +41468,26 @@ ${derived.length} of ${requested} keys were created and ARE in the vault; the va
41452
41468
  ` + `Re-run for the remaining ${requested - derived.length}${derived.length > 0 ? " (with a --labels-from file holding the names that did not land)" : ""}.
41453
41469
  `);
41454
41470
  }
41471
+ function plannedLabels(hd, batch, labelFlag) {
41472
+ const labels = [];
41473
+ let counter = hd.nextIndex.solanaVault;
41474
+ for (let made = 0;made < batch.count; made++) {
41475
+ const index = nextAllocatableIndex(counter, hd.exposedIndexes.solanaVault);
41476
+ labels.push(batch.labels?.[made] ?? labelFlag ?? `key-${index}`);
41477
+ counter = index + 1;
41478
+ }
41479
+ return labels;
41480
+ }
41481
+ function labelClash(index, planned) {
41482
+ const taken = new Set(index.entries.map((entry) => entry.label));
41483
+ const seen = new Set;
41484
+ for (const label of planned) {
41485
+ if (taken.has(label) || seen.has(label))
41486
+ return label;
41487
+ seen.add(label);
41488
+ }
41489
+ return;
41490
+ }
41455
41491
  function nextAllocatableIndex(counter, exposed) {
41456
41492
  let index = counter;
41457
41493
  while (exposed.includes(index))
@@ -42259,6 +42295,9 @@ var walletPageSchema = exports_external.object({
42259
42295
  isDone: exports_external.boolean(),
42260
42296
  continueCursor: exports_external.string().nullable().optional()
42261
42297
  });
42298
+ var embeddedSchema = exports_external.object({
42299
+ wallets: exports_external.object({ solana: exports_external.object({ address: exports_external.string() }).passthrough().nullable().optional() }).passthrough().optional()
42300
+ }).passthrough();
42262
42301
  var operationSchema = exports_external.object({ job: exports_external.object({ status: exports_external.string() }).passthrough() }).passthrough();
42263
42302
  var BASES = {
42264
42303
  SOL: { mint: "So11111111111111111111111111111111111111112", decimals: 9 },
@@ -42313,9 +42352,9 @@ async function request(ctx, key, path, body) {
42313
42352
  throw new TradingError("INVALID_RESPONSE", "Candle returned an invalid response.");
42314
42353
  return result.body;
42315
42354
  }
42316
- async function tradingWallet(ctx, key, name, scope) {
42355
+ async function teeWallets(ctx, key, scope) {
42317
42356
  let cursor;
42318
- const matches = [];
42357
+ const rows = [];
42319
42358
  const cursors = new Set;
42320
42359
  let appId = "";
42321
42360
  for (;; ) {
@@ -42325,7 +42364,7 @@ async function tradingWallet(ctx, key, name, scope) {
42325
42364
  appId = response.privyAppId ?? "";
42326
42365
  if (!Array.isArray(response.page))
42327
42366
  throw new TradingError("INVALID_RESPONSE", "Wallet discovery did not return a page.");
42328
- matches.push(...response.page.filter((row2) => row2.id === name || row2.address === name || row2.label === name));
42367
+ rows.push(...response.page);
42329
42368
  if (response.isDone === true)
42330
42369
  break;
42331
42370
  cursor = response.continueCursor ?? undefined;
@@ -42333,9 +42372,18 @@ async function tradingWallet(ctx, key, name, scope) {
42333
42372
  throw new TradingError("INVALID_RESPONSE", "Wallet discovery did not complete.");
42334
42373
  cursors.add(cursor);
42335
42374
  }
42336
- if (matches.length !== 1)
42337
- throw new TradingError("TEE_WALLET_REQUIRED", "Name exactly one TEE wallet bound to the active profile's key, by id, address or unique label.");
42338
- const row = matches[0];
42375
+ return { rows, appId };
42376
+ }
42377
+ function matchesName(row, name) {
42378
+ return row.id === name || row.address === name || row.label === name;
42379
+ }
42380
+ function describeWallet(row) {
42381
+ return `${row.label ? `${row.label} ` : ""}(${row.id}, ${row.address})`;
42382
+ }
42383
+ function teeWalletList(rows) {
42384
+ return rows.map(describeWallet).join("; ");
42385
+ }
42386
+ async function completeTradingWallet(ctx, row, appId, scope) {
42339
42387
  if (!row.active || row.chain !== "solana")
42340
42388
  throw new TradingError("TEE_WALLET_INACTIVE", "The payer must be a verified-active Solana TEE wallet.");
42341
42389
  if (scope === "launch:write" && row.allowLaunch !== true)
@@ -42353,6 +42401,37 @@ async function tradingWallet(ctx, key, name, scope) {
42353
42401
  signer: storedSignerToPem(signer)
42354
42402
  };
42355
42403
  }
42404
+ async function tradingWallet(ctx, key, name, scope) {
42405
+ const { rows, appId } = await teeWallets(ctx, key, scope);
42406
+ const matches = rows.filter((row) => matchesName(row, name));
42407
+ if (matches.length !== 1) {
42408
+ throw new TradingError("TEE_WALLET_REQUIRED", matches.length > 1 ? `"${name}" matches ${matches.length} TEE wallets on this key: ${teeWalletList(matches)}. Name one by id or address.` : rows.length === 0 ? "This key has no TEE wallets bound to it. Enrol one, or select the profile whose key holds it." : `No TEE wallet on this key is called "${name}". Bound to this key: ${teeWalletList(rows)}.`);
42409
+ }
42410
+ return await completeTradingWallet(ctx, matches[0], appId, scope);
42411
+ }
42412
+ async function tradingPayer(ctx, key, name) {
42413
+ const scope = "swap:write";
42414
+ const { rows, appId } = await teeWallets(ctx, key, scope);
42415
+ const embedded = embeddedSchema.parse(await request(ctx, key, "/api/v1/agent/wallets/embedded")).wallets?.solana?.address;
42416
+ const asEmbedded = () => ({ kind: "embedded", address: embedded });
42417
+ const options = [
42418
+ ...rows.map((row) => `TEE ${describeWallet(row)}`),
42419
+ ...embedded ? [`embedded (${embedded})`] : []
42420
+ ].join("; ");
42421
+ if (name === undefined) {
42422
+ if (rows.length === 1 && !embedded)
42423
+ return { kind: "tee", wallet: await completeTradingWallet(ctx, rows[0], appId, scope) };
42424
+ if (rows.length === 0 && embedded)
42425
+ return asEmbedded();
42426
+ throw new TradingError("PAYER_REQUIRED", options.length === 0 ? "This account has no wallet that can pay for a swap. Enrol a TEE wallet, or create an embedded wallet in the app." : `Name the payer with --wallet. This account can pay from: ${options}.`);
42427
+ }
42428
+ const matches = rows.filter((row) => matchesName(row, name));
42429
+ if (matches.length === 1)
42430
+ return { kind: "tee", wallet: await completeTradingWallet(ctx, matches[0], appId, scope) };
42431
+ if (matches.length === 0 && embedded === name)
42432
+ return asEmbedded();
42433
+ throw new TradingError("TEE_WALLET_REQUIRED", matches.length > 1 ? `"${name}" matches ${matches.length} TEE wallets on this key: ${teeWalletList(matches)}. Name one by id or address.` : options.length === 0 ? `"${name}" is not a wallet this account can pay from, and it has none: enrol a TEE wallet, or create an embedded wallet in the app.` : `"${name}" is not a wallet this account can pay from. It can pay from: ${options}.`);
42434
+ }
42356
42435
  function authorizationSignature(wallet, transaction) {
42357
42436
  const body = { method: "signTransaction", params: { encoding: "base64", transaction } };
42358
42437
  const payload = JSON.stringify({
@@ -42550,6 +42629,16 @@ async function decimalsFor(ctx, asset, url) {
42550
42629
  throw new TradingError("INVALID_RESPONSE", "RPC returned invalid mint decimals.");
42551
42630
  return decimals;
42552
42631
  }
42632
+ async function assertDeferredExecuteSupported(ctx, key, id) {
42633
+ try {
42634
+ await request(ctx, key, "/api/v1/trade/agent/execute", { clientTradeId: id });
42635
+ } catch (error) {
42636
+ if (error instanceof TradingError && error.code === "JOB_NOT_FOUND")
42637
+ return;
42638
+ throw new TradingError("EMBEDDED_PAYER_UNSUPPORTED", "This Candle deployment cannot hold an embedded-wallet trade back for confirmation, so the quote could not be shown before the money moved. Nothing was built. Name a TEE wallet with --wallet, or point at a deployment that has it.");
42639
+ }
42640
+ throw new TradingError("INVALID_RESPONSE", "The execute route answered for a trade that does not exist.");
42641
+ }
42553
42642
  async function swap(args, ctx) {
42554
42643
  const parsed = parseArgs(args, {
42555
42644
  valueFlags: ["--amount", "--percent", "--wallet", "--client-trade-id", "--slippage-bps", "--rpc-url"],
@@ -42562,8 +42651,8 @@ async function swap(args, ctx) {
42562
42651
  const flags = parsed.values;
42563
42652
  const id = flags["--client-trade-id"] ?? `swap-${randomUUID()}`;
42564
42653
  const slippage = Number(flags["--slippage-bps"] ?? "50");
42565
- if (parsed.positionals.length !== 2 || !flags["--wallet"] || Boolean(flags["--amount"]) === Boolean(flags["--percent"]) || !validClientId(id) || !Number.isInteger(slippage) || slippage < 0 || slippage > 1e4) {
42566
- writeUsageFailure(ctx.deps, "Usage: candle swap <from> <to> --amount <decimal> | --percent <n> --wallet <tee> [--client-trade-id <id>] [--slippage-bps 50] [--rpc-url <url>] [--yes]", ctx.json);
42654
+ if (parsed.positionals.length !== 2 || Boolean(flags["--amount"]) === Boolean(flags["--percent"]) || !validClientId(id) || !Number.isInteger(slippage) || slippage < 0 || slippage > 1e4) {
42655
+ writeUsageFailure(ctx.deps, "Usage: candle swap <from> <to> --amount <decimal> | --percent <n> [--wallet <tee-or-embedded>] [--client-trade-id <id>] [--slippage-bps 50] [--rpc-url <url>] [--yes]", ctx.json);
42567
42656
  return 2;
42568
42657
  }
42569
42658
  try {
@@ -42585,7 +42674,10 @@ async function swap(args, ctx) {
42585
42674
  const prior = await lookupOperation(ctx, key, id, kind);
42586
42675
  if (prior)
42587
42676
  return printTradingResult(ctx, prior);
42588
- const wallet = await tradingWallet(ctx, key, flags["--wallet"], "swap:write");
42677
+ const payerWallet = await tradingPayer(ctx, key, flags["--wallet"]);
42678
+ if (payerWallet.kind === "embedded" && kind === "swap")
42679
+ throw new TradingError("PAIR_UNSUPPORTED", "The embedded wallet cannot swap between base assets from this command yet: that rail executes in one call, so there would be nothing to confirm. Trade a token with it, or name a TEE wallet for a base pair.");
42680
+ const wallet = payerWallet.kind === "tee" ? payerWallet.wallet : { address: payerWallet.address };
42589
42681
  const decimals = await decimalsFor(ctx, from, flags["--rpc-url"]);
42590
42682
  const outDecimals = await decimalsFor(ctx, to, flags["--rpc-url"]);
42591
42683
  let amountRaw;
@@ -42609,11 +42701,13 @@ async function swap(args, ctx) {
42609
42701
  amountRaw = rawAmount(flags["--amount"], decimals);
42610
42702
  if (BigInt(amountRaw) > BigInt(Number.MAX_SAFE_INTEGER))
42611
42703
  throw new TradingError("INVALID_AMOUNT", "Amount exceeds the venue's exact integer range.");
42704
+ if (payerWallet.kind === "embedded")
42705
+ await assertDeferredExecuteSupported(ctx, key, id);
42612
42706
  if (!await claimOperation(ctx, key, id, kind))
42613
42707
  throw new TradingError("OPERATION_ALREADY_STARTED", "This machine already started this id; no write was resent.");
42614
42708
  ctx.deps.stderr.write(`Operation: ${id}
42615
42709
  `);
42616
- const payer = { type: "linked", linkedWalletId: wallet.id };
42710
+ const payer = payerWallet.kind === "tee" ? { type: "linked", linkedWalletId: payerWallet.wallet.id } : { type: "main" };
42617
42711
  const built = kind === "swap" ? await request(ctx, key, "/api/v1/agent/swap/build", {
42618
42712
  clientTradeId: id,
42619
42713
  from,
@@ -42629,7 +42723,8 @@ async function swap(args, ctx) {
42629
42723
  quoteAsset: (fromBase ?? toBase)?.toLowerCase(),
42630
42724
  amountRaw,
42631
42725
  maxSlippageBps: slippage,
42632
- payer
42726
+ payer,
42727
+ ...payerWallet.kind === "embedded" ? { deferExecution: true } : {}
42633
42728
  });
42634
42729
  if (built.job || built.status === "executed")
42635
42730
  return printTradingResult(ctx, { ...built, clientTradeId: id, kind });
@@ -42659,14 +42754,24 @@ async function swap(args, ctx) {
42659
42754
  };
42660
42755
  if (!await confirmQuote(ctx, quote, parsed.booleans.has("--yes")))
42661
42756
  return printTradingResult(ctx, { success: true, status: "cancelled", clientTradeId: id, kind, quote });
42757
+ if (!Number.isFinite(data.expiresAt) || data.expiresAt <= ctx.deps.now())
42758
+ throw new TradingError("QUOTE_EXPIRED", "The quote expired before signing. Start a new intention with a new id.");
42759
+ if (payerWallet.kind === "embedded") {
42760
+ const executed = await request(ctx, key, "/api/v1/trade/agent/execute", { clientTradeId: id });
42761
+ return printTradingResult(ctx, {
42762
+ ...executed,
42763
+ clientTradeId: id,
42764
+ kind,
42765
+ quote,
42766
+ wallet: safeText(payerWallet.address)
42767
+ });
42768
+ }
42662
42769
  const transaction = kind === "swap" ? data.transactionsBase64?.[0] : artifacts.transactionBase64;
42663
42770
  if (kind === "swap" && data.transactionsBase64?.length !== 1)
42664
42771
  throw new TradingError("INVALID_RESPONSE", "A TEE swap must contain exactly one same-chain transaction.");
42665
- if (!Number.isFinite(data.expiresAt) || data.expiresAt <= ctx.deps.now())
42666
- throw new TradingError("QUOTE_EXPIRED", "The quote expired before signing. Start a new intention with a new id.");
42667
42772
  if (!transaction)
42668
42773
  throw new TradingError("INVALID_RESPONSE", "Missing transaction.");
42669
- const signed = await relaySign(ctx, key, wallet, transaction);
42774
+ const signed = await relaySign(ctx, key, payerWallet.wallet, transaction);
42670
42775
  const result = kind === "swap" ? await request(ctx, key, "/api/v1/agent/swap/submit", {
42671
42776
  clientTradeId: id,
42672
42777
  swapId: data.swapId,
@@ -47627,6 +47732,10 @@ init_wallet_keystore();
47627
47732
  init_esm();
47628
47733
  init_args();
47629
47734
  init_render();
47735
+ var TEE_PROFILES = new Set(["ember-tee", "ember-hot"]);
47736
+ function isTeeRow(row) {
47737
+ return row.profile !== undefined && TEE_PROFILES.has(row.profile);
47738
+ }
47630
47739
  var LINKED_WALLETS_PAGE_LIMIT = 100;
47631
47740
  var LINKED_WALLETS_PAGE_CAP = 25;
47632
47741
  async function readAllLinkedWallets(args) {
@@ -47670,6 +47779,9 @@ function incompleteNotice(listing) {
47670
47779
  var NONE_HINT = `A wallet marked none has no signer on this machine, so a trade from here cannot sign with it.
47671
47780
  ` + `Import it here (candle wallets import), or run the trade from the machine that imported it.
47672
47781
  `;
47782
+ var TEE_HINT = `A wallet marked tee is a TEE trading wallet: pass its id, address or label to candle swap --wallet.
47783
+ ` + `This account's embedded wallet, shown above, can pay for a token trade too.
47784
+ `;
47673
47785
  var STALE_HINT = `A wallet marked stale is revoked but its signer is still stored here. Run: candle wallets revoke <id>
47674
47786
  `;
47675
47787
  async function probeSignerStates(rows, store) {
@@ -47776,20 +47888,24 @@ Linked wallets (${linkedRows.length}):
47776
47888
  `);
47777
47889
  } else {
47778
47890
  const cells = linkedRows.map((wallet) => signerCell(signerStates.get(wallet._id), wallet));
47779
- deps.stdout.write(`${renderTable(["Id", "Wallet", "Address", "Label", "Revoked", "Signer"], linkedRows.map((wallet, index) => [
47891
+ deps.stdout.write(`${renderTable(["Id", "Wallet", "Address", "Label", "Kind", "Revoked", "Signer"], linkedRows.map((wallet, index) => [
47780
47892
  wallet._id,
47781
47893
  wallet.chain,
47782
47894
  wallet.address,
47783
47895
  wallet.label ?? "-",
47896
+ isTeeRow(wallet) ? "tee" : "linked",
47784
47897
  wallet.revokedAt ? "yes" : "no",
47785
47898
  cells[index] ?? "-"
47786
47899
  ]))}
47787
47900
  `);
47788
47901
  const anyNone = cells.includes("none");
47789
47902
  const anyStale = cells.includes("stale");
47790
- if (anyNone || anyStale)
47903
+ const anyTee = linkedRows.some(isTeeRow);
47904
+ if (anyNone || anyStale || anyTee)
47791
47905
  deps.stdout.write(`
47792
47906
  `);
47907
+ if (anyTee)
47908
+ deps.stdout.write(TEE_HINT);
47793
47909
  if (anyNone)
47794
47910
  deps.stdout.write(NONE_HINT);
47795
47911
  if (anyStale)
@@ -50261,7 +50377,8 @@ async function vaultBackup(args, ctx) {
50261
50377
  }
50262
50378
  const opened = await unlockInteractively(ctx, path, raw, {
50263
50379
  acceptOlderCopy: parsed.booleans.has("--accept-older-copy"),
50264
- ...verdict.sealed ? { factor: "passphrase" } : {}
50380
+ ...verdict.sealed ? { factor: "passphrase" } : {},
50381
+ purpose: "opening the vault"
50265
50382
  });
50266
50383
  const live = hold(opened.vault);
50267
50384
  if (verdict.sealed) {
@@ -50273,7 +50390,8 @@ async function vaultBackup(args, ctx) {
50273
50390
  throw copyWriteFailed(destination, error, "copy");
50274
50391
  }
50275
50392
  }
50276
- const report = await verifyCopy(ctx, destination, opened.reopen, live);
50393
+ const written = await stat3(destination);
50394
+ const report = await verifyCopy(ctx, destination, opened.reopen, live, "re-opening the copy to verify it");
50277
50395
  const sidecar = sidecarPath(path);
50278
50396
  await writeSidecar(sidecar, {
50279
50397
  ...nextSidecar(await readSidecar(sidecar), live.file),
@@ -50283,6 +50401,7 @@ async function vaultBackup(args, ctx) {
50283
50401
  ...verdict.sharedDomainAccepted ? { lastBackupSharedDomainAccepted: true } : {}
50284
50402
  });
50285
50403
  const copyEnvelopes = verdict.sealed ? sealedEnvelopes(live.file.envelopes) : live.file.envelopes;
50404
+ const mode = fileModeOctal(written.mode);
50286
50405
  if (ctx.json) {
50287
50406
  writeJson(deps, {
50288
50407
  ok: true,
@@ -50294,14 +50413,25 @@ async function vaultBackup(args, ctx) {
50294
50413
  sharedDomainAccepted: verdict.sharedDomainAccepted,
50295
50414
  sharedDomain: verdict.sharedDomain,
50296
50415
  verified: true,
50297
- ...reportJson(report, live)
50416
+ ...reportJson(report, live),
50417
+ bytesWritten: written.size,
50418
+ mode
50298
50419
  });
50299
50420
  return 0;
50300
50421
  }
50422
+ for (const line of wroteLines(destination, written.size, mode))
50423
+ deps.stdout.write(`${line}
50424
+ `);
50301
50425
  writeVerifiedReport(ctx, destination, verdict, report, live);
50302
50426
  return 0;
50303
50427
  });
50304
50428
  }
50429
+ function fileModeOctal(mode) {
50430
+ return (mode & 511).toString(8).padStart(4, "0");
50431
+ }
50432
+ function wroteLines(destination, size, mode) {
50433
+ return [`Wrote ${destination}`, ` size ${formatBytes(size)} (${size} bytes)`, ` mode ${mode}`];
50434
+ }
50305
50435
  function resolveBackupDestination(to, deps) {
50306
50436
  if (to.trim().toLowerCase() !== ICLOUD_SHORTHAND)
50307
50437
  return { path: resolve2(to) };
@@ -50381,7 +50511,8 @@ async function vaultVerifyBackup(args, ctx) {
50381
50511
  }
50382
50512
  const opened = await unlockInteractively(ctx, path, raw, {
50383
50513
  acceptOlderCopy: parsed.booleans.has("--accept-older-copy"),
50384
- ...sealed ? { factor: "passphrase" } : {}
50514
+ ...sealed ? { factor: "passphrase" } : {},
50515
+ purpose: "opening the live vault"
50385
50516
  });
50386
50517
  const live = hold(opened.vault);
50387
50518
  const sameEnvelope = parseVaultFile(copyRaw).envelopes.some((envelope) => JSON.stringify(envelope) === JSON.stringify(live.envelope));
@@ -50390,11 +50521,12 @@ async function vaultVerifyBackup(args, ctx) {
50390
50521
  const copy = hold((await unlockInteractively(ctx, resolve2(copyPath), copyRaw, {
50391
50522
  factor: "passphrase",
50392
50523
  acceptOlderCopy: parsed.booleans.has("--accept-older-copy"),
50393
- promptText: "Passphrase this backup was sealed under (input hidden): "
50524
+ promptText: "Passphrase this backup was sealed under (input hidden): ",
50525
+ purpose: "opening the copy"
50394
50526
  })).vault);
50395
50527
  report = await verifyVaultIntegrity(copy, { live });
50396
50528
  } else {
50397
- report = await verifyCopy(ctx, resolve2(copyPath), opened.reopen, live);
50529
+ report = await verifyCopy(ctx, resolve2(copyPath), opened.reopen, live, "opening the copy");
50398
50530
  }
50399
50531
  const sidecar = sidecarPath(path);
50400
50532
  await writeSidecar(sidecar, {
@@ -50409,13 +50541,13 @@ async function vaultVerifyBackup(args, ctx) {
50409
50541
  return 0;
50410
50542
  });
50411
50543
  }
50412
- async function verifyCopy(_ctx, copyPath, reopen, live) {
50544
+ async function verifyCopy(_ctx, copyPath, reopen, live, purpose) {
50413
50545
  const raw = await readVaultRaw(copyPath);
50414
50546
  if (raw === null)
50415
50547
  throw new VaultError("VAULT_MISSING", `No file at ${copyPath}.`, {
50416
50548
  suggestion: `The copy this run just wrote is not there. Check the path and the volume: ls -l ${copyPath}`
50417
50549
  });
50418
- const copy = await reopen(copyPath, raw);
50550
+ const copy = await reopen(copyPath, raw, purpose);
50419
50551
  try {
50420
50552
  return await verifyVaultIntegrity(copy, { live });
50421
50553
  } finally {
@@ -53181,6 +53313,159 @@ async function runTeeImport(ctx, opts) {
53181
53313
  return submitted.remoteAuthority === "verified-active" ? 0 : 3;
53182
53314
  }
53183
53315
 
53316
+ // src/commands/vault-rename.ts
53317
+ init_args();
53318
+ init_errors();
53319
+
53320
+ // src/vault/labels.ts
53321
+ function entriesWithLabel(index, label) {
53322
+ return index.entries.filter((entry) => entry.label === label);
53323
+ }
53324
+ function duplicateLabels(index) {
53325
+ const byLabel = new Map;
53326
+ for (const entry of index.entries) {
53327
+ const holders = byLabel.get(entry.label);
53328
+ if (holders === undefined)
53329
+ byLabel.set(entry.label, [entry]);
53330
+ else
53331
+ holders.push(entry);
53332
+ }
53333
+ const out = [];
53334
+ for (const [label, entries] of byLabel) {
53335
+ if (entries.length > 1)
53336
+ out.push({ label, entries });
53337
+ }
53338
+ return out;
53339
+ }
53340
+ function hasControlCharacter(label) {
53341
+ for (const char of label) {
53342
+ const code = char.codePointAt(0) ?? 0;
53343
+ if (code < 32 || code === 127)
53344
+ return true;
53345
+ }
53346
+ return false;
53347
+ }
53348
+ function validateLabel(label) {
53349
+ if (label.trim().length === 0)
53350
+ return "A key's label cannot be empty.";
53351
+ if (hasControlCharacter(label)) {
53352
+ return "A key's label cannot contain a newline, a tab or a control character.";
53353
+ }
53354
+ if (label.startsWith("-")) {
53355
+ return `A key's label cannot begin with "-": it would be read as a flag everywhere a label is typed.`;
53356
+ }
53357
+ return;
53358
+ }
53359
+ function resolveRenameTarget(index, old, id) {
53360
+ if (id !== undefined) {
53361
+ const entry = index.entries.find((candidate) => candidate.id === id);
53362
+ return entry === undefined ? { kind: "none" } : { kind: "found", entry, by: "id" };
53363
+ }
53364
+ const byLabel = entriesWithLabel(index, old);
53365
+ if (byLabel.length === 1)
53366
+ return { kind: "found", entry: byLabel[0], by: "label" };
53367
+ if (byLabel.length > 1)
53368
+ return { kind: "ambiguous", by: "label", candidates: byLabel };
53369
+ const byAddress = index.entries.filter((entry) => entry.address === old);
53370
+ if (byAddress.length === 1)
53371
+ return { kind: "found", entry: byAddress[0], by: "address" };
53372
+ if (byAddress.length > 1)
53373
+ return { kind: "ambiguous", by: "address", candidates: byAddress };
53374
+ return { kind: "none" };
53375
+ }
53376
+
53377
+ // src/commands/vault-rename.ts
53378
+ init_store();
53379
+ init_vault_support();
53380
+ var RENAME_USAGE = "Usage: candle vault rename <label|address> <new-label> [--id <entry-id>]";
53381
+ var SAME_STRING_LINE = "The two arguments are the same string; nothing to do.";
53382
+ async function vaultRename(args, ctx) {
53383
+ const parsed = parseArgs(args, {
53384
+ valueFlags: ["--keystore", "--id"],
53385
+ booleanFlags: ["--accept-older-copy"],
53386
+ pathFlags: ["--keystore"]
53387
+ });
53388
+ if ("error" in parsed)
53389
+ return usage(ctx, parsed.error);
53390
+ const [old, next, extra] = parsed.positionals;
53391
+ if (old === undefined || next === undefined || extra !== undefined)
53392
+ return usage(ctx, RENAME_USAGE);
53393
+ const invalid = validateLabel(next);
53394
+ if (invalid !== undefined)
53395
+ return usage(ctx, invalid);
53396
+ if (old === next)
53397
+ return usage(ctx, SAME_STRING_LINE);
53398
+ const id = parsed.values["--id"];
53399
+ if (!refuseEnvPassphrase(ctx))
53400
+ return 1;
53401
+ if (!requireTty(ctx, "vault rename"))
53402
+ return 1;
53403
+ const { deps } = ctx;
53404
+ const resolvedVault = vaultPathFor(ctx, parsed);
53405
+ if ("error" in resolvedVault)
53406
+ return usage(ctx, resolvedVault.error);
53407
+ const path = resolvedVault.path;
53408
+ return runVaultCommand(ctx, async ({ hold }) => {
53409
+ const raw = await requireVaultRaw(ctx, resolvedVault);
53410
+ const opened = await unlockInteractively(ctx, path, raw, {
53411
+ acceptOlderCopy: parsed.booleans.has("--accept-older-copy")
53412
+ });
53413
+ const vault = hold(opened.vault);
53414
+ const entry = resolveTarget(vault.index, old, id);
53415
+ if (entry.role === "tee-wallet") {
53416
+ throw new VaultError("VAULT_RENAME_ROLE_REFUSED", `${entry.label} is a TEE wallet. Its label was sent to Candle when it was enabled and \`candle wallets\` lists that copy, so renaming it here would give one wallet two names and nothing reconciles them.`, {
53417
+ suggestion: "A vault key or an external wallet renames here. For a TEE wallet, nothing in this release changes the name on either side."
53418
+ });
53419
+ }
53420
+ if (entry.label === next) {
53421
+ throw new VaultError("VAULT_LABEL_UNCHANGED", `${next} is already this key's label. Nothing was written.`, {
53422
+ suggestion: "Nothing to rename. `candle vault status --unlock` lists every label."
53423
+ });
53424
+ }
53425
+ if (entriesWithLabel(vault.index, next).length > 0) {
53426
+ throw new VaultError("VAULT_LABEL_TAKEN", `A key labelled ${next} already exists in this vault. Nothing was written.`, {
53427
+ suggestion: "Choose a name no key has, or rename that key first: `candle vault status --unlock` lists them."
53428
+ });
53429
+ }
53430
+ const from = entry.label;
53431
+ await commitVault(vault, {
53432
+ index: {
53433
+ hd: vault.index.hd,
53434
+ entries: vault.index.entries.map((candidate) => candidate.id === entry.id ? { ...candidate, label: next } : candidate)
53435
+ }
53436
+ }, deps);
53437
+ if (ctx.json) {
53438
+ writeJson(deps, { ok: true, id: entry.id, address: entry.address, role: entry.role, from, to: next });
53439
+ return 0;
53440
+ }
53441
+ deps.stdout.write(`Renamed ${from} to ${next}.
53442
+ `);
53443
+ deps.stdout.write(` address ${entry.address}
53444
+ `);
53445
+ deps.stdout.write(` id ${entry.id}
53446
+ `);
53447
+ deps.stdout.write(` role ${entry.role}
53448
+ `);
53449
+ deps.stdout.write(` unchanged address, derivation path, key blob, every envelope
53450
+ `);
53451
+ return 0;
53452
+ });
53453
+ }
53454
+ function resolveTarget(index, old, id) {
53455
+ const match = resolveRenameTarget(index, old, id);
53456
+ if (match.kind === "found")
53457
+ return match.entry;
53458
+ if (match.kind === "none") {
53459
+ throw new VaultError("VAULT_LABEL_NOT_FOUND", id === undefined ? `No key in this vault is called ${old}, and no key has that address or id.` : `No key in this vault has the id ${id}.`, { suggestion: "List them with their labels: `candle vault status --unlock`" });
53460
+ }
53461
+ const candidates = match.candidates.map((entry) => `${entry.address} (${entry.id})`).join(", ");
53462
+ const count = match.candidates.length;
53463
+ const first = match.candidates[0];
53464
+ throw new VaultError("VAULT_LABEL_AMBIGUOUS", match.by === "label" ? `${count} keys in this vault are called ${old}, so this rename would not say which one it meant. Nothing was written.` : `${count} keys in this vault have the address ${old}, so this rename would not say which one it meant. Nothing was written.`, {
53465
+ suggestion: match.by === "label" ? `Name one by address or id. The candidates are ${candidates}.` : `Name one by id. The candidates are ${candidates}. Re-run: \`candle vault rename ${old} <new-label> --id ${first.id}\`.`
53466
+ });
53467
+ }
53468
+
53184
53469
  // src/commands/vault-restore.ts
53185
53470
  init_args();
53186
53471
  import { rm as rm3 } from "node:fs/promises";
@@ -53884,6 +54169,10 @@ async function vaultStatus(args, ctx) {
53884
54169
  const vault = hold((await unlockInteractively(ctx, path, raw, { acceptOlderCopy: parsed.booleans.has("--accept-older-copy") })).vault);
53885
54170
  unlocked = {
53886
54171
  entries: vault.index.entries.map(describeEntry),
54172
+ duplicateLabels: duplicateLabels(vault.index).map((duplicate) => ({
54173
+ label: duplicate.label,
54174
+ entries: duplicate.entries.map((entry) => ({ address: entry.address, id: entry.id }))
54175
+ })),
53887
54176
  nextIndex: vault.index.hd.nextIndex,
53888
54177
  exposedIndexes: vault.index.hd.exposedIndexes,
53889
54178
  rootExported: vault.index.hd.rootExported,
@@ -54009,6 +54298,14 @@ Keys (${unlocked.entries.length}):
54009
54298
  `);
54010
54299
  }
54011
54300
  }
54301
+ if (unlocked.duplicateLabels.length > 0) {
54302
+ deps.stdout.write(`
54303
+ Duplicate labels (${unlocked.duplicateLabels.length}):
54304
+ `);
54305
+ for (const line of duplicateLabelLines(unlocked.duplicateLabels))
54306
+ deps.stdout.write(`${line}
54307
+ `);
54308
+ }
54012
54309
  deps.stdout.write(`
54013
54310
  Derivation counters (next index per branch):
54014
54311
  `);
@@ -54030,6 +54327,15 @@ The recovery phrase restores derived keys only. It does not restore any key impo
54030
54327
  return 0;
54031
54328
  });
54032
54329
  }
54330
+ function duplicateLabelLines(duplicates) {
54331
+ const lines = [];
54332
+ for (const duplicate of duplicates) {
54333
+ const holders = duplicate.entries.map((entry) => `${entry.address} (${entry.id})`).join(", ");
54334
+ lines.push(` ${duplicate.label.padEnd(14)}${duplicate.entries.length} keys: ${holders}`);
54335
+ lines.push(` --from ${duplicate.label} always picks the first; rename one: candle vault rename <address> <new-label>`);
54336
+ }
54337
+ return lines;
54338
+ }
54033
54339
  function describeEnvelope(envelope, facts) {
54034
54340
  const availability = envelopeAvailability(envelope, facts);
54035
54341
  const strength = typeof envelope.strength === "string" ? envelope.strength : undefined;
@@ -54061,6 +54367,7 @@ function describeEntryInner(entry) {
54061
54367
  return {
54062
54368
  address: entry.address,
54063
54369
  label: entry.label,
54370
+ id: entry.id,
54064
54371
  role: entry.role,
54065
54372
  origin: entry.origin,
54066
54373
  derivation: entry.derivation?.path,
@@ -54588,6 +54895,7 @@ var COMMANDS = {
54588
54895
  init: vaultInit,
54589
54896
  status: vaultStatus,
54590
54897
  "new-key": vaultNewKey,
54898
+ rename: vaultRename,
54591
54899
  phrase: vaultPhrase,
54592
54900
  restore: vaultRestore,
54593
54901
  "reconcile-exposure": vaultReconcileExposure,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@candledottv/cli",
3
- "version": "0.11.3",
3
+ "version": "0.11.4",
4
4
  "description": "The Candle CLI: authorize a device from your browser, then manage API keys, wallets, and setup health from the terminal",
5
5
  "type": "module",
6
6
  "bin": {