@candledottv/cli 0.11.2 → 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 +498 -114
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -405,8 +405,9 @@ async function readKeystore(raw, passphrase, opts = {}) {
405
405
  }
406
406
  async function writeKeystoreFile(path, contents) {
407
407
  const dir = dirname2(path);
408
- await mkdir2(dir, { recursive: true });
409
- await chmod2(dir, 448);
408
+ const created = await mkdir2(dir, { recursive: true });
409
+ if (created !== undefined)
410
+ await chmod2(dir, 448).catch(() => {});
410
411
  const tmpPath = `${path}.${crypto.randomUUID()}.tmp`;
411
412
  await writeFile2(tmpPath, contents, { encoding: "utf8", mode: 384 });
412
413
  await chmod2(tmpPath, 384);
@@ -2253,7 +2254,12 @@ var init_errors = __esm(() => {
2253
2254
  "SIGN_SIMULATION_FAILED",
2254
2255
  "SIGN_LOOKUP_TABLE_UNRESOLVED",
2255
2256
  "SIGN_TRANSACTION_UNDECODABLE",
2256
- "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"
2257
2263
  ];
2258
2264
  VaultError = class VaultError extends Error {
2259
2265
  code;
@@ -14884,7 +14890,7 @@ function strengthFor(ownPassphrase) {
14884
14890
  function strengthLabel(strength) {
14885
14891
  return strength === "generated-103" ? `generated, ${GENERATED_WORD_COUNT} words (about ${generatedEntropyBits()} bits)` : "chosen by you (this CLI cannot know its entropy)";
14886
14892
  }
14887
- var GENERATED_WORD_COUNT = 8, OWN_PASSPHRASE_MIN_LENGTH = 16, DENYLIST, APPLE_ACCOUNT_NOTICE = "Keep this passphrase and your recovery phrase outside the Apple account that holds a synced passkey: an Apple-generated password saved to iCloud Keychain lands in that account.";
14893
+ var GENERATED_WORD_COUNT = 8, OWN_PASSPHRASE_MIN_LENGTH = 16, DENYLIST, APPLE_ACCOUNT_NOTICE = "Keep this passphrase and your recovery phrase outside the Apple account that holds a synced passkey: an Apple-generated password saved to iCloud Keychain lands in that account.", SAVE_THE_PASSPHRASE = "Save it now, in your password manager or on paper. This CLI keeps no copy and cannot recover it. If you lose it, your 24-word recovery phrase is the way back in.", SAVED_IT_PROMPT = "Press Enter when you have saved it: ";
14888
14894
  var init_passphrase = __esm(() => {
14889
14895
  init_eff_wordlist();
14890
14896
  init_errors();
@@ -14928,9 +14934,11 @@ __export(exports_vault_support, {
14928
14934
  missingVault: () => missingVault,
14929
14935
  findExternalEntry: () => findExternalEntry,
14930
14936
  describeRole: () => describeRole,
14937
+ derivationNotice: () => derivationNotice,
14931
14938
  confirmLastSix: () => confirmLastSix,
14932
14939
  assertVaultHelperIdentities: () => assertVaultHelperIdentities,
14933
14940
  assertNotOlderCopy: () => assertNotOlderCopy,
14941
+ askForOwnPassphrase: () => askForOwnPassphrase,
14934
14942
  RPC_URL_ENV: () => RPC_URL_ENV
14935
14943
  });
14936
14944
  import { dirname as dirname7 } from "node:path";
@@ -14942,6 +14950,16 @@ function refuseEnvPassphrase(ctx) {
14942
14950
  }));
14943
14951
  return false;
14944
14952
  }
14953
+ async function askForOwnPassphrase(ctx, promptText) {
14954
+ for (let attempt = 0;attempt < 2; attempt++) {
14955
+ const answer = (await ctx.deps.promptLine(promptText)).trim().toLowerCase();
14956
+ if (answer === "own")
14957
+ return true;
14958
+ if (answer === "")
14959
+ return false;
14960
+ }
14961
+ return false;
14962
+ }
14945
14963
  function requireTty(ctx, what) {
14946
14964
  if (ctx.deps.isTTY.stdin && ctx.deps.isTTY.stdout)
14947
14965
  return true;
@@ -15005,6 +15023,13 @@ async function requireVaultRaw(ctx, resolved) {
15005
15023
  throw missingVault(ctx, resolved);
15006
15024
  return raw;
15007
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
+ }
15008
15033
  function assertVaultHelperIdentities(deps, envelopes) {
15009
15034
  if (deps.releasePolicy.macosHelper.release === "signed") {
15010
15035
  for (const envelope of envelopes) {
@@ -15021,12 +15046,13 @@ async function unlockInteractively(ctx, path, raw, opts = {}) {
15021
15046
  assertVaultHelperIdentities(deps, file.envelopes);
15022
15047
  const facts = await currentPlatformFacts(deps);
15023
15048
  const choice = await chooseFactor(ctx, file.envelopes, facts, opts.factor ?? ctx.vaultFactor);
15024
- const notice = (line) => deps.stderr.write(line);
15049
+ const noticeFor = (purpose) => (line) => deps.stderr.write(derivationNotice(line, purpose));
15050
+ const notice = noticeFor(opts.purpose);
15025
15051
  if (choice.kind === "passphrase") {
15026
15052
  const typed = await deps.promptSecret(opts.promptText ?? "Vault passphrase (input hidden): ");
15027
- const openWith = (p, r, candidate) => choice.envelopeId === undefined ? unlockWithPassphrase(p, r, candidate, { notice }) : unlockVault(p, r, { factor: "passphrase", passphrase: candidate, envelopeId: choice.envelopeId }, { notice });
15028
- const { vault: vault2, passphrase } = await openWithTypedPassphrase(typed, (candidate) => openWith(path, raw, candidate));
15029
- 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);
15030
15056
  return {
15031
15057
  vault: vault2,
15032
15058
  factor: { kind: "passphrase", envelopeId: vault2.envelope.id },
@@ -35207,7 +35233,7 @@ var init_server2 = __esm(() => {
35207
35233
  // src/index.ts
35208
35234
  import { spawn as spawn3 } from "node:child_process";
35209
35235
  import { realpathSync } from "node:fs";
35210
- import { chmod as chmod7, readFile as readFile8, realpath, rename as rename5, unlink, writeFile as writeFile7 } from "node:fs/promises";
35236
+ import { chmod as chmod8, readFile as readFile8, realpath, rename as rename5, unlink, writeFile as writeFile7 } from "node:fs/promises";
35211
35237
  import { hostname } from "node:os";
35212
35238
  import { pathToFileURL } from "node:url";
35213
35239
 
@@ -35776,7 +35802,7 @@ async function resolveApiKey(deps, profile) {
35776
35802
  init_render();
35777
35803
 
35778
35804
  // src/version.ts
35779
- var CLI_VERSION = "0.11.2";
35805
+ var CLI_VERSION = "0.11.4";
35780
35806
 
35781
35807
  // src/commands/auth.ts
35782
35808
  var DEVICE_CODE_PATH = "/api/v1/agent/device/code";
@@ -36333,13 +36359,17 @@ var HELP = {
36333
36359
  usage: ["candle vault <subcommand> [flags]"],
36334
36360
  rows: [
36335
36361
  {
36336
- invocation: "init [--own-passphrase] [--high-value]",
36362
+ invocation: "init [--own-passphrase]",
36337
36363
  description: "Create the vault: one passphrase factor and an HD root"
36338
36364
  },
36339
36365
  { invocation: "status [--unlock]", description: "What the vault holds, and what opens it" },
36340
36366
  {
36341
36367
  invocation: "new-key --chain solana [--label <name>] [--count <n>] [--labels-from <file>]",
36342
- 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"
36343
36373
  },
36344
36374
  { invocation: "phrase show", description: "Show the 24-word recovery phrase (terminal only)" },
36345
36375
  {
@@ -36359,8 +36389,8 @@ var HELP = {
36359
36389
  description: `Same as factor add: ${FACTOR_KINDS.join(", ")}`
36360
36390
  },
36361
36391
  {
36362
- invocation: "backup --to <path> [--accept-shared-domain]",
36363
- description: "Copy the vault and verify the copy in full"
36392
+ invocation: "backup --to <path>|icloud [--accept-shared-domain]",
36393
+ description: "Copy the vault and verify the copy in full; icloud is iCloud Drive"
36364
36394
  },
36365
36395
  { invocation: "verify-backup <path>", description: "Verify a copy in full (all eight steps)" },
36366
36396
  { invocation: "import-legacy --tee [--from <path>]", description: "Migrate tee-wallets.enc into the vault" },
@@ -36404,9 +36434,11 @@ var HELP = {
36404
36434
  examples: [
36405
36435
  "candle vault init",
36406
36436
  "candle vault new-key --chain solana --label treasury",
36437
+ "candle vault rename key-7 treasury-cold",
36407
36438
  "candle vault new-key --chain solana --labels-from ./replacement-names.txt",
36408
36439
  "candle vault enroll security-key --label yubikey-a",
36409
36440
  "candle vault backup --to /Volumes/BACKUP/vault.enc",
36441
+ "candle vault backup --to icloud",
36410
36442
  "CANDLE_CONFIG_DIR=$HOME/t47 candle vault status"
36411
36443
  ],
36412
36444
  env: ["CANDLE_CONFIG_DIR", "CANDLE_FIDO2_HELPER", "CANDLE_ENCLAVE_HELPER", "CANDLE_KEYSTORE_PASSPHRASE"]
@@ -38159,6 +38191,21 @@ function createSolanaRpc(url, fetchFn) {
38159
38191
  init_errors();
38160
38192
  import { homedir as homedir4 } from "node:os";
38161
38193
  import { basename, dirname as dirname4, isAbsolute, join as join6, resolve, sep } from "node:path";
38194
+ var ICLOUD_DRIVE_SEGMENTS = ["Library", "Mobile Documents", "com~apple~CloudDocs"];
38195
+ var ICLOUD_SHORTHAND = "icloud";
38196
+ var ICLOUD_BACKUP_FOLDER = "Candle";
38197
+ function icloudDriveDir(home) {
38198
+ return join6(home, ...ICLOUD_DRIVE_SEGMENTS);
38199
+ }
38200
+ function icloudBackupPath(home, at) {
38201
+ return join6(icloudDriveDir(home), ICLOUD_BACKUP_FOLDER, `vault-${backupStamp(at)}.enc`);
38202
+ }
38203
+ function backupStamp(at) {
38204
+ return new Date(at).toISOString().replace(/[-:]/gu, "").replace(/\.\d+Z$/u, "Z");
38205
+ }
38206
+ function homeDirOf(env) {
38207
+ return env.HOME?.trim() || homedir4();
38208
+ }
38162
38209
  var OTHER_CLOUD_MARKERS = [
38163
38210
  "Library/CloudStorage",
38164
38211
  "Dropbox",
@@ -41234,7 +41281,6 @@ init_store();
41234
41281
  init_args();
41235
41282
  init_errors();
41236
41283
  init_format();
41237
- init_sidecar();
41238
41284
  init_store();
41239
41285
  init_vault_support();
41240
41286
  var MAX_NEW_KEY_COUNT = 256;
@@ -41319,18 +41365,14 @@ async function vaultNewKey(args, ctx) {
41319
41365
  });
41320
41366
  const vault = hold(opened.vault);
41321
41367
  assertRecoverableFactorExists(vault.file.envelopes);
41322
- await assertHighValueSatisfied(path, vault.file.envelopes);
41323
41368
  if (vault.index.hd.discovery !== undefined) {
41324
41369
  throw new VaultError("VAULT_ALLOCATION_BOUNDARY_UNKNOWN", "This vault was built by `vault restore --phrase`, so the highest index its root ever allocated was never established and claiming a new one could re-derive an address that is already in use elsewhere.", {
41325
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."
41326
41371
  });
41327
41372
  }
41328
- if (batch.labels !== undefined) {
41329
- const taken = new Set(vault.index.entries.map((entry) => entry.label));
41330
- const clash = batch.labels.find((label) => taken.has(label));
41331
- if (clash !== undefined) {
41332
- return usage(ctx, `A key labelled ${clash} already exists in this vault; every --labels-from name must be new.`);
41333
- }
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.`);
41334
41376
  }
41335
41377
  const derived = [];
41336
41378
  let last;
@@ -41426,23 +41468,32 @@ ${derived.length} of ${requested} keys were created and ARE in the vault; the va
41426
41468
  ` + `Re-run for the remaining ${requested - derived.length}${derived.length > 0 ? " (with a --labels-from file holding the names that did not land)" : ""}.
41427
41469
  `);
41428
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
+ }
41429
41491
  function nextAllocatableIndex(counter, exposed) {
41430
41492
  let index = counter;
41431
41493
  while (exposed.includes(index))
41432
41494
  index++;
41433
41495
  return index;
41434
41496
  }
41435
- async function assertHighValueSatisfied(path, envelopes) {
41436
- const sidecar = await readSidecar(sidecarPath(path));
41437
- if (sidecar?.highValue !== true)
41438
- return;
41439
- const generated = envelopes.some((envelope) => envelope.factor === "passphrase" && envelope.strength === "generated-103");
41440
- if (generated)
41441
- return;
41442
- if (countRecoverableFactors(envelopes) >= 2)
41443
- return;
41444
- throw new VaultError("VAULT_NO_RECOVERABLE_FACTOR", "This vault was created with --high-value, which needs either a generated passphrase or two recoverable factors in different domains before a key is created in it.", { suggestion: "Add a second recoverable factor: candle vault factor add passphrase" });
41445
- }
41446
41497
  async function verifyWrittenFromDisk(vault, address, keyId) {
41447
41498
  const raw = await readVaultRaw(vault.path);
41448
41499
  if (raw === null) {
@@ -41513,7 +41564,6 @@ async function externalNew(args, ctx) {
41513
41564
  });
41514
41565
  const vault = hold(opened.vault);
41515
41566
  assertRecoverableFactorExists(vault.file.envelopes);
41516
- await assertHighValueSatisfied(path, vault.file.envelopes);
41517
41567
  if (vault.index.hd.discovery !== undefined) {
41518
41568
  throw new VaultError("VAULT_ALLOCATION_BOUNDARY_UNKNOWN", "This vault was built by `vault restore --phrase`, so the highest index its root ever allocated was never established and claiming a new external index could re-derive an address that is already in use elsewhere.", {
41519
41569
  suggestion: "Create a second vault with a fresh root (`candle vault init`) and move the funds across with `candle vault transfer`. Recovered external keys still fund, sweep and sign here; only allocation is refused."
@@ -42245,6 +42295,9 @@ var walletPageSchema = exports_external.object({
42245
42295
  isDone: exports_external.boolean(),
42246
42296
  continueCursor: exports_external.string().nullable().optional()
42247
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();
42248
42301
  var operationSchema = exports_external.object({ job: exports_external.object({ status: exports_external.string() }).passthrough() }).passthrough();
42249
42302
  var BASES = {
42250
42303
  SOL: { mint: "So11111111111111111111111111111111111111112", decimals: 9 },
@@ -42299,9 +42352,9 @@ async function request(ctx, key, path, body) {
42299
42352
  throw new TradingError("INVALID_RESPONSE", "Candle returned an invalid response.");
42300
42353
  return result.body;
42301
42354
  }
42302
- async function tradingWallet(ctx, key, name, scope) {
42355
+ async function teeWallets(ctx, key, scope) {
42303
42356
  let cursor;
42304
- const matches = [];
42357
+ const rows = [];
42305
42358
  const cursors = new Set;
42306
42359
  let appId = "";
42307
42360
  for (;; ) {
@@ -42311,7 +42364,7 @@ async function tradingWallet(ctx, key, name, scope) {
42311
42364
  appId = response.privyAppId ?? "";
42312
42365
  if (!Array.isArray(response.page))
42313
42366
  throw new TradingError("INVALID_RESPONSE", "Wallet discovery did not return a page.");
42314
- matches.push(...response.page.filter((row2) => row2.id === name || row2.address === name || row2.label === name));
42367
+ rows.push(...response.page);
42315
42368
  if (response.isDone === true)
42316
42369
  break;
42317
42370
  cursor = response.continueCursor ?? undefined;
@@ -42319,9 +42372,18 @@ async function tradingWallet(ctx, key, name, scope) {
42319
42372
  throw new TradingError("INVALID_RESPONSE", "Wallet discovery did not complete.");
42320
42373
  cursors.add(cursor);
42321
42374
  }
42322
- if (matches.length !== 1)
42323
- throw new TradingError("TEE_WALLET_REQUIRED", "Name exactly one TEE wallet bound to the active profile's key, by id, address or unique label.");
42324
- 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) {
42325
42387
  if (!row.active || row.chain !== "solana")
42326
42388
  throw new TradingError("TEE_WALLET_INACTIVE", "The payer must be a verified-active Solana TEE wallet.");
42327
42389
  if (scope === "launch:write" && row.allowLaunch !== true)
@@ -42339,6 +42401,37 @@ async function tradingWallet(ctx, key, name, scope) {
42339
42401
  signer: storedSignerToPem(signer)
42340
42402
  };
42341
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
+ }
42342
42435
  function authorizationSignature(wallet, transaction) {
42343
42436
  const body = { method: "signTransaction", params: { encoding: "base64", transaction } };
42344
42437
  const payload = JSON.stringify({
@@ -42536,6 +42629,16 @@ async function decimalsFor(ctx, asset, url) {
42536
42629
  throw new TradingError("INVALID_RESPONSE", "RPC returned invalid mint decimals.");
42537
42630
  return decimals;
42538
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
+ }
42539
42642
  async function swap(args, ctx) {
42540
42643
  const parsed = parseArgs(args, {
42541
42644
  valueFlags: ["--amount", "--percent", "--wallet", "--client-trade-id", "--slippage-bps", "--rpc-url"],
@@ -42548,8 +42651,8 @@ async function swap(args, ctx) {
42548
42651
  const flags = parsed.values;
42549
42652
  const id = flags["--client-trade-id"] ?? `swap-${randomUUID()}`;
42550
42653
  const slippage = Number(flags["--slippage-bps"] ?? "50");
42551
- if (parsed.positionals.length !== 2 || !flags["--wallet"] || Boolean(flags["--amount"]) === Boolean(flags["--percent"]) || !validClientId(id) || !Number.isInteger(slippage) || slippage < 0 || slippage > 1e4) {
42552
- 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);
42553
42656
  return 2;
42554
42657
  }
42555
42658
  try {
@@ -42571,7 +42674,10 @@ async function swap(args, ctx) {
42571
42674
  const prior = await lookupOperation(ctx, key, id, kind);
42572
42675
  if (prior)
42573
42676
  return printTradingResult(ctx, prior);
42574
- 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 };
42575
42681
  const decimals = await decimalsFor(ctx, from, flags["--rpc-url"]);
42576
42682
  const outDecimals = await decimalsFor(ctx, to, flags["--rpc-url"]);
42577
42683
  let amountRaw;
@@ -42595,11 +42701,13 @@ async function swap(args, ctx) {
42595
42701
  amountRaw = rawAmount(flags["--amount"], decimals);
42596
42702
  if (BigInt(amountRaw) > BigInt(Number.MAX_SAFE_INTEGER))
42597
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);
42598
42706
  if (!await claimOperation(ctx, key, id, kind))
42599
42707
  throw new TradingError("OPERATION_ALREADY_STARTED", "This machine already started this id; no write was resent.");
42600
42708
  ctx.deps.stderr.write(`Operation: ${id}
42601
42709
  `);
42602
- const payer = { type: "linked", linkedWalletId: wallet.id };
42710
+ const payer = payerWallet.kind === "tee" ? { type: "linked", linkedWalletId: payerWallet.wallet.id } : { type: "main" };
42603
42711
  const built = kind === "swap" ? await request(ctx, key, "/api/v1/agent/swap/build", {
42604
42712
  clientTradeId: id,
42605
42713
  from,
@@ -42615,7 +42723,8 @@ async function swap(args, ctx) {
42615
42723
  quoteAsset: (fromBase ?? toBase)?.toLowerCase(),
42616
42724
  amountRaw,
42617
42725
  maxSlippageBps: slippage,
42618
- payer
42726
+ payer,
42727
+ ...payerWallet.kind === "embedded" ? { deferExecution: true } : {}
42619
42728
  });
42620
42729
  if (built.job || built.status === "executed")
42621
42730
  return printTradingResult(ctx, { ...built, clientTradeId: id, kind });
@@ -42645,14 +42754,24 @@ async function swap(args, ctx) {
42645
42754
  };
42646
42755
  if (!await confirmQuote(ctx, quote, parsed.booleans.has("--yes")))
42647
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
+ }
42648
42769
  const transaction = kind === "swap" ? data.transactionsBase64?.[0] : artifacts.transactionBase64;
42649
42770
  if (kind === "swap" && data.transactionsBase64?.length !== 1)
42650
42771
  throw new TradingError("INVALID_RESPONSE", "A TEE swap must contain exactly one same-chain transaction.");
42651
- if (!Number.isFinite(data.expiresAt) || data.expiresAt <= ctx.deps.now())
42652
- throw new TradingError("QUOTE_EXPIRED", "The quote expired before signing. Start a new intention with a new id.");
42653
42772
  if (!transaction)
42654
42773
  throw new TradingError("INVALID_RESPONSE", "Missing transaction.");
42655
- const signed = await relaySign(ctx, key, wallet, transaction);
42774
+ const signed = await relaySign(ctx, key, payerWallet.wallet, transaction);
42656
42775
  const result = kind === "swap" ? await request(ctx, key, "/api/v1/agent/swap/submit", {
42657
42776
  clientTradeId: id,
42658
42777
  swapId: data.swapId,
@@ -47613,6 +47732,10 @@ init_wallet_keystore();
47613
47732
  init_esm();
47614
47733
  init_args();
47615
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
+ }
47616
47739
  var LINKED_WALLETS_PAGE_LIMIT = 100;
47617
47740
  var LINKED_WALLETS_PAGE_CAP = 25;
47618
47741
  async function readAllLinkedWallets(args) {
@@ -47656,6 +47779,9 @@ function incompleteNotice(listing) {
47656
47779
  var NONE_HINT = `A wallet marked none has no signer on this machine, so a trade from here cannot sign with it.
47657
47780
  ` + `Import it here (candle wallets import), or run the trade from the machine that imported it.
47658
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
+ `;
47659
47785
  var STALE_HINT = `A wallet marked stale is revoked but its signer is still stored here. Run: candle wallets revoke <id>
47660
47786
  `;
47661
47787
  async function probeSignerStates(rows, store) {
@@ -47762,20 +47888,24 @@ Linked wallets (${linkedRows.length}):
47762
47888
  `);
47763
47889
  } else {
47764
47890
  const cells = linkedRows.map((wallet) => signerCell(signerStates.get(wallet._id), wallet));
47765
- 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) => [
47766
47892
  wallet._id,
47767
47893
  wallet.chain,
47768
47894
  wallet.address,
47769
47895
  wallet.label ?? "-",
47896
+ isTeeRow(wallet) ? "tee" : "linked",
47770
47897
  wallet.revokedAt ? "yes" : "no",
47771
47898
  cells[index] ?? "-"
47772
47899
  ]))}
47773
47900
  `);
47774
47901
  const anyNone = cells.includes("none");
47775
47902
  const anyStale = cells.includes("stale");
47776
- if (anyNone || anyStale)
47903
+ const anyTee = linkedRows.some(isTeeRow);
47904
+ if (anyNone || anyStale || anyTee)
47777
47905
  deps.stdout.write(`
47778
47906
  `);
47907
+ if (anyTee)
47908
+ deps.stdout.write(TEE_HINT);
47779
47909
  if (anyNone)
47780
47910
  deps.stdout.write(NONE_HINT);
47781
47911
  if (anyStale)
@@ -50067,8 +50197,8 @@ function messageOf(error) {
50067
50197
 
50068
50198
  // src/commands/vault-backup.ts
50069
50199
  init_args();
50070
- import { copyFile, stat as stat3 } from "node:fs/promises";
50071
- import nodePath, { resolve as resolve2 } from "node:path";
50200
+ import { chmod as chmod5, copyFile, mkdir as mkdir6, stat as stat3 } from "node:fs/promises";
50201
+ import nodePath, { dirname as dirname8, resolve as resolve2 } from "node:path";
50072
50202
  init_errors();
50073
50203
  init_format();
50074
50204
  init_passphrase();
@@ -50212,11 +50342,28 @@ async function vaultBackup(args, ctx) {
50212
50342
  if ("error" in resolvedVault)
50213
50343
  return usage(ctx, resolvedVault.error);
50214
50344
  const path = resolvedVault.path;
50215
- const destination = resolve2(to);
50345
+ const target = resolveBackupDestination(to, deps);
50346
+ if (target.requires !== undefined && !await exists(target.requires)) {
50347
+ return usage(ctx, `There is no iCloud Drive folder at ${target.requires} on this machine. Sign in to iCloud and turn on iCloud Drive, or pass --to <path> with somewhere else to write.`);
50348
+ }
50349
+ const destination = target.path;
50350
+ if (target.requires !== undefined)
50351
+ deps.stderr.write(`--to ${ICLOUD_SHORTHAND} is ${destination}
50352
+ `);
50216
50353
  return runVaultCommand(ctx, async ({ hold }) => {
50217
50354
  const raw = await requireVaultRaw(ctx, resolvedVault);
50218
50355
  assertOutsideConfigDir(destination, deps.env);
50219
50356
  const file = parseVaultFile(raw);
50357
+ if (target.requires !== undefined) {
50358
+ const folder = dirname8(destination);
50359
+ try {
50360
+ const created = await mkdir6(folder, { recursive: true });
50361
+ if (created !== undefined)
50362
+ await chmod5(folder, 448).catch(() => {});
50363
+ } catch (error) {
50364
+ throw copyWriteFailed(destination, error, "copy");
50365
+ }
50366
+ }
50220
50367
  const verdict = await assertBackupDomainAllowed(file.envelopes, destination, {
50221
50368
  acceptSharedDomain: parsed.booleans.has("--accept-shared-domain"),
50222
50369
  realpath: deps.realpath
@@ -50230,15 +50377,21 @@ async function vaultBackup(args, ctx) {
50230
50377
  }
50231
50378
  const opened = await unlockInteractively(ctx, path, raw, {
50232
50379
  acceptOlderCopy: parsed.booleans.has("--accept-older-copy"),
50233
- ...verdict.sealed ? { factor: "passphrase" } : {}
50380
+ ...verdict.sealed ? { factor: "passphrase" } : {},
50381
+ purpose: "opening the vault"
50234
50382
  });
50235
50383
  const live = hold(opened.vault);
50236
50384
  if (verdict.sealed) {
50237
50385
  await writeSealedCopy(live, destination);
50238
50386
  } else {
50239
- await copyFile(path, destination);
50387
+ try {
50388
+ await copyFile(path, destination);
50389
+ } catch (error) {
50390
+ throw copyWriteFailed(destination, error, "copy");
50391
+ }
50240
50392
  }
50241
- 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");
50242
50395
  const sidecar = sidecarPath(path);
50243
50396
  await writeSidecar(sidecar, {
50244
50397
  ...nextSidecar(await readSidecar(sidecar), live.file),
@@ -50248,6 +50401,7 @@ async function vaultBackup(args, ctx) {
50248
50401
  ...verdict.sharedDomainAccepted ? { lastBackupSharedDomainAccepted: true } : {}
50249
50402
  });
50250
50403
  const copyEnvelopes = verdict.sealed ? sealedEnvelopes(live.file.envelopes) : live.file.envelopes;
50404
+ const mode = fileModeOctal(written.mode);
50251
50405
  if (ctx.json) {
50252
50406
  writeJson(deps, {
50253
50407
  ok: true,
@@ -50259,23 +50413,63 @@ async function vaultBackup(args, ctx) {
50259
50413
  sharedDomainAccepted: verdict.sharedDomainAccepted,
50260
50414
  sharedDomain: verdict.sharedDomain,
50261
50415
  verified: true,
50262
- ...reportJson(report, live)
50416
+ ...reportJson(report, live),
50417
+ bytesWritten: written.size,
50418
+ mode
50263
50419
  });
50264
50420
  return 0;
50265
50421
  }
50422
+ for (const line of wroteLines(destination, written.size, mode))
50423
+ deps.stdout.write(`${line}
50424
+ `);
50266
50425
  writeVerifiedReport(ctx, destination, verdict, report, live);
50267
50426
  return 0;
50268
50427
  });
50269
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
+ }
50435
+ function resolveBackupDestination(to, deps) {
50436
+ if (to.trim().toLowerCase() !== ICLOUD_SHORTHAND)
50437
+ return { path: resolve2(to) };
50438
+ const home = homeDirOf(deps.env);
50439
+ return { path: icloudBackupPath(home, deps.now()), requires: icloudDriveDir(home) };
50440
+ }
50270
50441
  async function writeSealedCopy(live, destination) {
50271
50442
  const { index: _index, ...header } = live.file;
50272
50443
  const sealed = await sealIndex({ ...header, envelopes: sealedEnvelopes(live.file.envelopes) }, live.index, live.payloadKey);
50273
50444
  try {
50274
50445
  await writeKeystoreFile(destination, serializeVault(sealed));
50275
- } catch {
50276
- throw new VaultError("VAULT_WRITE_FAILED", `Could not write the sealed copy at ${destination}.`);
50446
+ } catch (error) {
50447
+ throw copyWriteFailed(destination, error, "sealed copy");
50277
50448
  }
50278
50449
  }
50450
+ function errnoCodeOf(error) {
50451
+ const code = error?.code;
50452
+ return typeof code === "string" && code.length > 0 ? code : undefined;
50453
+ }
50454
+ function suggestionForErrno(code) {
50455
+ if (code === "ENOSPC")
50456
+ return "The volume is full. Free space there, or back up somewhere else. Nothing was written.";
50457
+ if (code === "EROFS")
50458
+ return "That volume is mounted read-only. Nothing was written.";
50459
+ if (code === "ENOENT")
50460
+ return "A directory on that path does not exist and could not be created. Check the path, and that the volume is mounted. Nothing was written.";
50461
+ if (code === "EACCES" || code === "EPERM")
50462
+ return "Check that you can write there, and that the volume or sync folder is mounted and not locked. Nothing was written.";
50463
+ return "Nothing was written. Try another destination, or run the same write by hand to see what the filesystem says.";
50464
+ }
50465
+ function copyWriteFailed(destination, error, what) {
50466
+ const code = errnoCodeOf(error);
50467
+ const reason = error instanceof Error ? error.message : String(error);
50468
+ return new VaultError("VAULT_WRITE_FAILED", `Could not write the ${what} at ${destination}: ${code ?? "no error code"} -- ${reason}`, {
50469
+ suggestion: suggestionForErrno(code),
50470
+ details: { path: destination, reason, ...code === undefined ? {} : { code } }
50471
+ });
50472
+ }
50279
50473
  function isSealedCopy(copyRaw) {
50280
50474
  const envelopes = parseVaultFile(copyRaw).envelopes;
50281
50475
  return envelopes.length > 0 && envelopes.every(isPassphraseEnvelope);
@@ -50317,7 +50511,8 @@ async function vaultVerifyBackup(args, ctx) {
50317
50511
  }
50318
50512
  const opened = await unlockInteractively(ctx, path, raw, {
50319
50513
  acceptOlderCopy: parsed.booleans.has("--accept-older-copy"),
50320
- ...sealed ? { factor: "passphrase" } : {}
50514
+ ...sealed ? { factor: "passphrase" } : {},
50515
+ purpose: "opening the live vault"
50321
50516
  });
50322
50517
  const live = hold(opened.vault);
50323
50518
  const sameEnvelope = parseVaultFile(copyRaw).envelopes.some((envelope) => JSON.stringify(envelope) === JSON.stringify(live.envelope));
@@ -50326,11 +50521,12 @@ async function vaultVerifyBackup(args, ctx) {
50326
50521
  const copy = hold((await unlockInteractively(ctx, resolve2(copyPath), copyRaw, {
50327
50522
  factor: "passphrase",
50328
50523
  acceptOlderCopy: parsed.booleans.has("--accept-older-copy"),
50329
- promptText: "Passphrase this backup was sealed under (input hidden): "
50524
+ promptText: "Passphrase this backup was sealed under (input hidden): ",
50525
+ purpose: "opening the copy"
50330
50526
  })).vault);
50331
50527
  report = await verifyVaultIntegrity(copy, { live });
50332
50528
  } else {
50333
- report = await verifyCopy(ctx, resolve2(copyPath), opened.reopen, live);
50529
+ report = await verifyCopy(ctx, resolve2(copyPath), opened.reopen, live, "opening the copy");
50334
50530
  }
50335
50531
  const sidecar = sidecarPath(path);
50336
50532
  await writeSidecar(sidecar, {
@@ -50345,13 +50541,13 @@ async function vaultVerifyBackup(args, ctx) {
50345
50541
  return 0;
50346
50542
  });
50347
50543
  }
50348
- async function verifyCopy(_ctx, copyPath, reopen, live) {
50544
+ async function verifyCopy(_ctx, copyPath, reopen, live, purpose) {
50349
50545
  const raw = await readVaultRaw(copyPath);
50350
50546
  if (raw === null)
50351
50547
  throw new VaultError("VAULT_MISSING", `No file at ${copyPath}.`, {
50352
50548
  suggestion: `The copy this run just wrote is not there. Check the path and the volume: ls -l ${copyPath}`
50353
50549
  });
50354
- const copy = await reopen(copyPath, raw);
50550
+ const copy = await reopen(copyPath, raw, purpose);
50355
50551
  try {
50356
50552
  return await verifyVaultIntegrity(copy, { live });
50357
50553
  } finally {
@@ -50593,8 +50789,8 @@ init_errors();
50593
50789
  init_promote_support();
50594
50790
  init_store();
50595
50791
  init_vault_support();
50596
- import { access as access3, chmod as chmod5, constants as constants4, lstat, writeFile as writeFile5 } from "node:fs/promises";
50597
- import { dirname as dirname8, resolve as resolve3 } from "node:path";
50792
+ import { access as access3, chmod as chmod6, constants as constants4, lstat, writeFile as writeFile5 } from "node:fs/promises";
50793
+ import { dirname as dirname9, resolve as resolve3 } from "node:path";
50598
50794
  async function vaultExportKey(args, ctx) {
50599
50795
  const parsed = parseArgs(args, {
50600
50796
  valueFlags: ["--keystore", "--to"],
@@ -50706,7 +50902,7 @@ async function assertExportTargetWritable(destination) {
50706
50902
  if (error.code !== "ENOENT")
50707
50903
  throw error;
50708
50904
  }
50709
- const parent = dirname8(destination);
50905
+ const parent = dirname9(destination);
50710
50906
  try {
50711
50907
  const parentInfo = await lstat(parent);
50712
50908
  if (parentInfo.isSymbolicLink()) {
@@ -50738,7 +50934,7 @@ async function assertExportTargetWritable(destination) {
50738
50934
  async function writeExportFile(destination, body) {
50739
50935
  try {
50740
50936
  await writeFile5(destination, body, { encoding: "utf8", flag: "wx", mode: 384 });
50741
- await chmod5(destination, 384);
50937
+ await chmod6(destination, 384);
50742
50938
  } catch (error) {
50743
50939
  const code = error.code;
50744
50940
  if (code === "EEXIST") {
@@ -51246,7 +51442,6 @@ async function createVault(request2, clock) {
51246
51442
  init_crypto();
51247
51443
  init_errors();
51248
51444
  init_passphrase();
51249
- init_sidecar();
51250
51445
  init_store();
51251
51446
 
51252
51447
  // src/commands/vault-phrase.ts
@@ -51381,12 +51576,13 @@ function randomPositions(count, of = PHRASE_WORDS) {
51381
51576
 
51382
51577
  // src/commands/vault-init.ts
51383
51578
  init_vault_support();
51384
- var INIT_GENERATED_PASSPHRASE_NOTICE = "Your vault passphrase is about to be generated and shown once. This CLI keeps no copy and cannot recover it. To choose your own instead: candle vault init --own-passphrase";
51579
+ var INIT_GENERATED_PASSPHRASE_NOTICE = "Your vault passphrase is about to be generated and shown once. This CLI keeps no copy and cannot recover it. To choose your own instead, type own at the prompt below.";
51580
+ var INIT_PASSPHRASE_PROMPT = "Passphrase for this vault. Press Enter to have one generated (8 words, shown once), or type own to choose your own (16+ characters, typed twice, never shown): ";
51385
51581
  var GENERATED_PASSPHRASE_NEEDS_TERMINAL = "A generated passphrase is shown once on the terminal, and --json reserves stdout for one JSON value that never carries a secret. Under --json pass --own-passphrase (typed at a hidden prompt, nothing shown), or run without --json.";
51386
51582
  async function vaultInit(args, ctx) {
51387
51583
  const parsed = parseArgs(args, {
51388
51584
  valueFlags: ["--keystore", "--label"],
51389
- booleanFlags: ["--own-passphrase", "--high-value"],
51585
+ booleanFlags: ["--own-passphrase"],
51390
51586
  pathFlags: ["--keystore"]
51391
51587
  });
51392
51588
  if ("error" in parsed)
@@ -51395,8 +51591,8 @@ async function vaultInit(args, ctx) {
51395
51591
  return usage(ctx, `Unexpected argument: ${parsed.positionals[0]}`);
51396
51592
  if (!refuseEnvPassphrase(ctx))
51397
51593
  return 1;
51398
- const ownPassphrase = parsed.booleans.has("--own-passphrase");
51399
- if (ctx.json && !ownPassphrase)
51594
+ const ownFlag = parsed.booleans.has("--own-passphrase");
51595
+ if (ctx.json && !ownFlag)
51400
51596
  return usage(ctx, GENERATED_PASSPHRASE_NEEDS_TERMINAL);
51401
51597
  if (!requireTty(ctx, "vault init"))
51402
51598
  return 1;
@@ -51405,32 +51601,25 @@ async function vaultInit(args, ctx) {
51405
51601
  if ("error" in resolvedVault)
51406
51602
  return usage(ctx, resolvedVault.error);
51407
51603
  const path = resolvedVault.path;
51408
- const highValue = parsed.booleans.has("--high-value");
51409
51604
  return runVaultCommand(ctx, async () => {
51410
51605
  if (await fileExists(path)) {
51411
51606
  throw vaultAlreadyExists(ctx, resolvedVault, "This CLI never overwrites one, including after an interrupted init. Move it aside if you really mean to start over.");
51412
51607
  }
51413
- if (!ownPassphrase)
51608
+ if (!ownFlag)
51414
51609
  deps.stdout.write(`${INIT_GENERATED_PASSPHRASE_NOTICE}
51415
51610
  `);
51416
- const passphrase = ownPassphrase ? await collectOwnPassphrase(ctx) : await collectGeneratedPassphrase(ctx);
51611
+ const own = ownFlag || await askForOwnPassphrase(ctx, INIT_PASSPHRASE_PROMPT);
51612
+ const passphrase = own ? await collectOwnPassphrase(ctx) : await collectGeneratedPassphrase(ctx);
51417
51613
  const entropy = randomBytes2(ROOT_ENTROPY_BYTES);
51418
51614
  const vault = await withSecret(entropy, async (rootEntropy) => createVault({
51419
51615
  path,
51420
51616
  passphrase,
51421
- strength: strengthFor(ownPassphrase),
51617
+ strength: strengthFor(own),
51422
51618
  rootEntropy,
51423
51619
  label: parsed.values["--label"],
51424
51620
  notice: (line) => deps.stderr.write(line)
51425
51621
  }, deps));
51426
51622
  try {
51427
- if (highValue) {
51428
- const sidecar = sidecarPath(path);
51429
- await writeSidecar(sidecar, {
51430
- ...nextSidecar(await readSidecar(sidecar), vault.file),
51431
- highValue: true
51432
- });
51433
- }
51434
51623
  if (ctx.json) {
51435
51624
  writeJson(deps, {
51436
51625
  ok: true,
@@ -51442,7 +51631,6 @@ async function vaultInit(args, ctx) {
51442
51631
  factor: envelope.factor,
51443
51632
  domain: envelope.domain
51444
51633
  })),
51445
- highValue,
51446
51634
  phraseCeremonyOffered: false
51447
51635
  });
51448
51636
  deps.stderr.write(`The recovery phrase ceremony is interactive only and was not offered under --json. Run: candle vault phrase show
@@ -51456,9 +51644,6 @@ async function vaultInit(args, ctx) {
51456
51644
  deps.stdout.write(` factors 1 (passphrase, human-memory)
51457
51645
  `);
51458
51646
  deps.stdout.write(` keys 0 -- create one with: candle vault new-key --chain solana
51459
- `);
51460
- if (highValue)
51461
- deps.stdout.write(` high value yes: new-key needs a generated passphrase, or two recoverable factors in different domains
51462
51647
  `);
51463
51648
  deps.stdout.write(`
51464
51649
  Verified: the file was re-read and opened with the passphrase you set, and its root blob decrypted.
@@ -51466,6 +51651,11 @@ Verified: the file was re-read and opened with the passphrase you set, and its r
51466
51651
  deps.stdout.write(`
51467
51652
  ${APPLE_ACCOUNT_NOTICE}
51468
51653
  `);
51654
+ if (countRecoverableFactors(vault.file.envelopes) === 1) {
51655
+ deps.stdout.write(`
51656
+ This vault has exactly one recoverable factor: the passphrase. Lose it and no copy of this file can be opened, and the recovery phrase becomes the only route back. Add a second when you can: candle vault enroll security-key
51657
+ `);
51658
+ }
51469
51659
  deps.stdout.write(`
51470
51660
  This vault has a 24-word recovery phrase. It re-derives every key this vault derives, on any BIP-39 wallet, and it is the only way back if you lose both the file and your backups.
51471
51661
  `);
@@ -51476,6 +51666,7 @@ This vault has a 24-word recovery phrase. It re-derives every key this vault der
51476
51666
  deps.stdout.write(`Skipped. You can run the ceremony later with: candle vault phrase show
51477
51667
  `);
51478
51668
  }
51669
+ await offerIcloudBackup(ctx, resolvedVault);
51479
51670
  const footer = nonDefaultVaultFooter(resolvedVault);
51480
51671
  if (footer !== undefined)
51481
51672
  deps.stdout.write(footer);
@@ -51486,20 +51677,19 @@ This vault has a 24-word recovery phrase. It re-derives every key this vault der
51486
51677
  });
51487
51678
  }
51488
51679
  async function collectGeneratedPassphrase(ctx) {
51680
+ const { deps } = ctx;
51489
51681
  const passphrase = generatePassphrase();
51490
- ctx.deps.stdout.write(`
51491
- Your vault passphrase, ${GENERATED_WORD_COUNT} words, about ${generatedEntropyBits()} bits. Write it down now; it is shown once and this CLI keeps no copy.
51682
+ deps.stdout.write(`
51683
+ Your vault passphrase, ${GENERATED_WORD_COUNT} words, about ${generatedEntropyBits()} bits.
51492
51684
 
51493
51685
  `);
51494
- ctx.deps.stdout.write(` ${passphrase}
51686
+ deps.stdout.write(` ${passphrase}
51495
51687
 
51496
51688
  `);
51497
- const typed = await ctx.deps.promptSecret("Type it back in full to confirm (input hidden): ");
51498
- if (typed.trim() !== passphrase) {
51499
- throw new VaultError("VAULT_UNLOCK_FAILED", "That did not match the passphrase shown above. Nothing was written.", {
51500
- suggestion: "Run `candle vault init` again for a new one."
51501
- });
51502
- }
51689
+ deps.stdout.write(`${SAVE_THE_PASSPHRASE}
51690
+
51691
+ `);
51692
+ await deps.promptLine(SAVED_IT_PROMPT);
51503
51693
  return passphrase;
51504
51694
  }
51505
51695
  async function collectOwnPassphrase(ctx) {
@@ -51514,6 +51704,28 @@ async function collectOwnPassphrase(ctx) {
51514
51704
  }
51515
51705
  return first;
51516
51706
  }
51707
+ async function offerIcloudBackup(ctx, resolved) {
51708
+ const { deps } = ctx;
51709
+ if (!await fileExists(icloudDriveDir(homeDirOf(deps.env))))
51710
+ return;
51711
+ deps.stdout.write(`
51712
+ Nothing has a copy of this vault yet. A copy in iCloud Drive is a copy of the ciphertext, and Candle seals it: it opens with the passphrase only, and carries no Touch ID, security key or synced passkey envelope, so one Apple account never holds both the blob and a factor that opens it.
51713
+ `);
51714
+ deps.stdout.write(`This vault holds no keys yet, so what a copy taken now protects is the root every key comes back from. Back it up again after your first \`vault new-key\`: until then \`verify-backup\` will report this copy as stale, correctly.
51715
+ `);
51716
+ const answer = (await deps.promptLine("Back up your encrypted vault to iCloud Drive now? Type yes to back it up, anything else to skip: ")).trim().toLowerCase();
51717
+ if (answer !== "yes") {
51718
+ deps.stdout.write(`Skipped. Back it up whenever you like with: candle vault backup --to ${ICLOUD_SHORTHAND}
51719
+ `);
51720
+ return;
51721
+ }
51722
+ const code = await vaultBackup(["--to", ICLOUD_SHORTHAND, "--keystore", resolved.path], ctx);
51723
+ if (code !== 0) {
51724
+ deps.stdout.write(`
51725
+ The vault itself is created and verified; only the copy failed. Run it again when you have dealt with the reason above: candle vault backup --to ${ICLOUD_SHORTHAND}
51726
+ `);
51727
+ }
51728
+ }
51517
51729
 
51518
51730
  // src/commands/vault-factor.ts
51519
51731
  init_vault_support();
@@ -53101,6 +53313,159 @@ async function runTeeImport(ctx, opts) {
53101
53313
  return submitted.remoteAuthority === "verified-active" ? 0 : 3;
53102
53314
  }
53103
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
+
53104
53469
  // src/commands/vault-restore.ts
53105
53470
  init_args();
53106
53471
  import { rm as rm3 } from "node:fs/promises";
@@ -53156,7 +53521,7 @@ async function vaultRestore(args, ctx) {
53156
53521
  if (rootEntropy.length !== ROOT_ENTROPY_BYTES) {
53157
53522
  throw new VaultError("PHRASE_INVALID", `That phrase carries ${rootEntropy.length} bytes of entropy, not ${ROOT_ENTROPY_BYTES}.`, { suggestion: "Nothing was written. Check the word count and order, then run it again." });
53158
53523
  }
53159
- const own = parsed.booleans.has("--own-passphrase") || await askForOwnPassphrase(ctx);
53524
+ const own = parsed.booleans.has("--own-passphrase") || await askForOwnPassphrase(ctx, RESTORE_PASSPHRASE_PROMPT);
53160
53525
  const passphrase = own ? await collectOwn2(ctx) : await collectGenerated2(ctx);
53161
53526
  const ownPassphrase = own;
53162
53527
  return createVault({
@@ -53618,16 +53983,6 @@ async function vaultReconcileExposure(args, ctx) {
53618
53983
  }
53619
53984
  var RESTORE_NEW_PASSPHRASE_NOTICE = "This builds a NEW vault from your 24 words, and it gets a NEW passphrase: the one that opened the vault the words came from does not carry over. The words carry the keys; a passphrase belongs to one file.";
53620
53985
  var RESTORE_PASSPHRASE_PROMPT = "Passphrase for the new vault. Press Enter to have one generated (8 words, shown once, typed back), or type own to choose your own (16+ characters, typed twice, never shown): ";
53621
- async function askForOwnPassphrase(ctx) {
53622
- for (let attempt = 0;attempt < 2; attempt++) {
53623
- const answer = (await ctx.deps.promptLine(RESTORE_PASSPHRASE_PROMPT)).trim().toLowerCase();
53624
- if (answer === "own")
53625
- return true;
53626
- if (answer === "")
53627
- return false;
53628
- }
53629
- return false;
53630
- }
53631
53986
  async function collectGenerated2(ctx) {
53632
53987
  const passphrase = generatePassphrase();
53633
53988
  ctx.deps.stdout.write(`
@@ -53775,6 +54130,7 @@ init_platform();
53775
54130
  init_sidecar();
53776
54131
  init_store();
53777
54132
  init_vault_support();
54133
+ var NO_VERIFIED_BACKUP_NOTE = "No backup of this vault has ever been verified from this machine. If this file is lost, only the 24-word recovery phrase can rebuild it, and it rebuilds derived keys only. Take one now: candle vault backup --to <path> (on a Mac with iCloud Drive: candle vault backup --to icloud)";
53778
54134
  async function vaultStatus(args, ctx) {
53779
54135
  const parsed = parseArgs(args, {
53780
54136
  valueFlags: ["--keystore"],
@@ -53813,6 +54169,10 @@ async function vaultStatus(args, ctx) {
53813
54169
  const vault = hold((await unlockInteractively(ctx, path, raw, { acceptOlderCopy: parsed.booleans.has("--accept-older-copy") })).vault);
53814
54170
  unlocked = {
53815
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
+ })),
53816
54176
  nextIndex: vault.index.hd.nextIndex,
53817
54177
  exposedIndexes: vault.index.hd.exposedIndexes,
53818
54178
  rootExported: vault.index.hd.rootExported,
@@ -53893,6 +54253,11 @@ This machine's record (vault.state.json, cleartext, best effort):
53893
54253
  } else {
53894
54254
  deps.stdout.write(`
53895
54255
  No vault.state.json beside this vault, so an older copy of it cannot be recognized on this machine.
54256
+ `);
54257
+ }
54258
+ if (sidecar?.lastVerifiedBackupAt === undefined) {
54259
+ deps.stdout.write(`
54260
+ ${NO_VERIFIED_BACKUP_NOTE}
53896
54261
  `);
53897
54262
  }
53898
54263
  if (legacyPresent) {
@@ -53933,6 +54298,14 @@ Keys (${unlocked.entries.length}):
53933
54298
  `);
53934
54299
  }
53935
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
+ }
53936
54309
  deps.stdout.write(`
53937
54310
  Derivation counters (next index per branch):
53938
54311
  `);
@@ -53954,6 +54327,15 @@ The recovery phrase restores derived keys only. It does not restore any key impo
53954
54327
  return 0;
53955
54328
  });
53956
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
+ }
53957
54339
  function describeEnvelope(envelope, facts) {
53958
54340
  const availability = envelopeAvailability(envelope, facts);
53959
54341
  const strength = typeof envelope.strength === "string" ? envelope.strength : undefined;
@@ -53985,6 +54367,7 @@ function describeEntryInner(entry) {
53985
54367
  return {
53986
54368
  address: entry.address,
53987
54369
  label: entry.label,
54370
+ id: entry.id,
53988
54371
  role: entry.role,
53989
54372
  origin: entry.origin,
53990
54373
  derivation: entry.derivation?.path,
@@ -54088,7 +54471,7 @@ async function vaultTransfer(args, ctx) {
54088
54471
  // src/commands/verify.ts
54089
54472
  init_args();
54090
54473
  init_release();
54091
- import { dirname as dirname9, join as join11 } from "node:path";
54474
+ import { dirname as dirname10, join as join11 } from "node:path";
54092
54475
  init_render();
54093
54476
  var USAGE2 = "Usage: candle verify <file> --bundle <path> [--identity <uri>] [--issuer <url>]";
54094
54477
  async function resolveIdentity(deps, bundlePath, flag) {
@@ -54096,7 +54479,7 @@ async function resolveIdentity(deps, bundlePath, flag) {
54096
54479
  return { kind: "ok", uri: flag, provenance: "identity from --identity" };
54097
54480
  let version;
54098
54481
  try {
54099
- const manifest = JSON.parse(await deps.readFile(join11(dirname9(bundlePath), "latest.json")));
54482
+ const manifest = JSON.parse(await deps.readFile(join11(dirname10(bundlePath), "latest.json")));
54100
54483
  if (typeof manifest.version !== "string" || manifest.version.length === 0)
54101
54484
  return { kind: "absent" };
54102
54485
  version = manifest.version;
@@ -54190,7 +54573,7 @@ function messageOf2(error) {
54190
54573
  }
54191
54574
 
54192
54575
  // src/config.ts
54193
- import { chmod as chmod6, mkdir as mkdir6, readFile as readFile7, rm as rm4, writeFile as writeFile6 } from "node:fs/promises";
54576
+ import { chmod as chmod7, mkdir as mkdir7, readFile as readFile7, rm as rm4, writeFile as writeFile6 } from "node:fs/promises";
54194
54577
  import { homedir as homedir6 } from "node:os";
54195
54578
  import { join as join12 } from "node:path";
54196
54579
  function configDir2() {
@@ -54213,8 +54596,8 @@ async function writeConfig(patch) {
54213
54596
  const current = await readConfig();
54214
54597
  const next = { ...current, ...patch };
54215
54598
  const dir = configDir2();
54216
- await mkdir6(dir, { recursive: true });
54217
- await chmod6(dir, 448);
54599
+ await mkdir7(dir, { recursive: true });
54600
+ await chmod7(dir, 448);
54218
54601
  await writeFile6(configFilePath(), JSON.stringify(next, null, 2), "utf8");
54219
54602
  }
54220
54603
  async function updateProfile(name, patch) {
@@ -54512,6 +54895,7 @@ var COMMANDS = {
54512
54895
  init: vaultInit,
54513
54896
  status: vaultStatus,
54514
54897
  "new-key": vaultNewKey,
54898
+ rename: vaultRename,
54515
54899
  phrase: vaultPhrase,
54516
54900
  restore: vaultRestore,
54517
54901
  "reconcile-exposure": vaultReconcileExposure,
@@ -54857,7 +55241,7 @@ async function buildRealDeps() {
54857
55241
  releasePolicy: RELEASE_POLICY,
54858
55242
  writeBytes: async (path, bytes) => {
54859
55243
  await writeFile7(path, bytes, { flag: "wx", mode: 493 });
54860
- await chmod7(path, 493);
55244
+ await chmod8(path, 493);
54861
55245
  },
54862
55246
  rename: (from, to) => rename5(from, to),
54863
55247
  unlink: (path) => unlink(path)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@candledottv/cli",
3
- "version": "0.11.2",
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": {