@candledottv/cli 0.11.1 → 0.11.3

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 +154 -78
  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);
@@ -14884,7 +14885,7 @@ function strengthFor(ownPassphrase) {
14884
14885
  function strengthLabel(strength) {
14885
14886
  return strength === "generated-103" ? `generated, ${GENERATED_WORD_COUNT} words (about ${generatedEntropyBits()} bits)` : "chosen by you (this CLI cannot know its entropy)";
14886
14887
  }
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.";
14888
+ 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
14889
  var init_passphrase = __esm(() => {
14889
14890
  init_eff_wordlist();
14890
14891
  init_errors();
@@ -14931,6 +14932,7 @@ __export(exports_vault_support, {
14931
14932
  confirmLastSix: () => confirmLastSix,
14932
14933
  assertVaultHelperIdentities: () => assertVaultHelperIdentities,
14933
14934
  assertNotOlderCopy: () => assertNotOlderCopy,
14935
+ askForOwnPassphrase: () => askForOwnPassphrase,
14934
14936
  RPC_URL_ENV: () => RPC_URL_ENV
14935
14937
  });
14936
14938
  import { dirname as dirname7 } from "node:path";
@@ -14942,6 +14944,16 @@ function refuseEnvPassphrase(ctx) {
14942
14944
  }));
14943
14945
  return false;
14944
14946
  }
14947
+ async function askForOwnPassphrase(ctx, promptText) {
14948
+ for (let attempt = 0;attempt < 2; attempt++) {
14949
+ const answer = (await ctx.deps.promptLine(promptText)).trim().toLowerCase();
14950
+ if (answer === "own")
14951
+ return true;
14952
+ if (answer === "")
14953
+ return false;
14954
+ }
14955
+ return false;
14956
+ }
14945
14957
  function requireTty(ctx, what) {
14946
14958
  if (ctx.deps.isTTY.stdin && ctx.deps.isTTY.stdout)
14947
14959
  return true;
@@ -35207,7 +35219,7 @@ var init_server2 = __esm(() => {
35207
35219
  // src/index.ts
35208
35220
  import { spawn as spawn3 } from "node:child_process";
35209
35221
  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";
35222
+ import { chmod as chmod8, readFile as readFile8, realpath, rename as rename5, unlink, writeFile as writeFile7 } from "node:fs/promises";
35211
35223
  import { hostname } from "node:os";
35212
35224
  import { pathToFileURL } from "node:url";
35213
35225
 
@@ -35776,7 +35788,7 @@ async function resolveApiKey(deps, profile) {
35776
35788
  init_render();
35777
35789
 
35778
35790
  // src/version.ts
35779
- var CLI_VERSION = "0.11.1";
35791
+ var CLI_VERSION = "0.11.3";
35780
35792
 
35781
35793
  // src/commands/auth.ts
35782
35794
  var DEVICE_CODE_PATH = "/api/v1/agent/device/code";
@@ -36333,7 +36345,7 @@ var HELP = {
36333
36345
  usage: ["candle vault <subcommand> [flags]"],
36334
36346
  rows: [
36335
36347
  {
36336
- invocation: "init [--own-passphrase] [--high-value]",
36348
+ invocation: "init [--own-passphrase]",
36337
36349
  description: "Create the vault: one passphrase factor and an HD root"
36338
36350
  },
36339
36351
  { invocation: "status [--unlock]", description: "What the vault holds, and what opens it" },
@@ -36359,8 +36371,8 @@ var HELP = {
36359
36371
  description: `Same as factor add: ${FACTOR_KINDS.join(", ")}`
36360
36372
  },
36361
36373
  {
36362
- invocation: "backup --to <path> [--accept-shared-domain]",
36363
- description: "Copy the vault and verify the copy in full"
36374
+ invocation: "backup --to <path>|icloud [--accept-shared-domain]",
36375
+ description: "Copy the vault and verify the copy in full; icloud is iCloud Drive"
36364
36376
  },
36365
36377
  { invocation: "verify-backup <path>", description: "Verify a copy in full (all eight steps)" },
36366
36378
  { invocation: "import-legacy --tee [--from <path>]", description: "Migrate tee-wallets.enc into the vault" },
@@ -36407,6 +36419,7 @@ var HELP = {
36407
36419
  "candle vault new-key --chain solana --labels-from ./replacement-names.txt",
36408
36420
  "candle vault enroll security-key --label yubikey-a",
36409
36421
  "candle vault backup --to /Volumes/BACKUP/vault.enc",
36422
+ "candle vault backup --to icloud",
36410
36423
  "CANDLE_CONFIG_DIR=$HOME/t47 candle vault status"
36411
36424
  ],
36412
36425
  env: ["CANDLE_CONFIG_DIR", "CANDLE_FIDO2_HELPER", "CANDLE_ENCLAVE_HELPER", "CANDLE_KEYSTORE_PASSPHRASE"]
@@ -38159,6 +38172,21 @@ function createSolanaRpc(url, fetchFn) {
38159
38172
  init_errors();
38160
38173
  import { homedir as homedir4 } from "node:os";
38161
38174
  import { basename, dirname as dirname4, isAbsolute, join as join6, resolve, sep } from "node:path";
38175
+ var ICLOUD_DRIVE_SEGMENTS = ["Library", "Mobile Documents", "com~apple~CloudDocs"];
38176
+ var ICLOUD_SHORTHAND = "icloud";
38177
+ var ICLOUD_BACKUP_FOLDER = "Candle";
38178
+ function icloudDriveDir(home) {
38179
+ return join6(home, ...ICLOUD_DRIVE_SEGMENTS);
38180
+ }
38181
+ function icloudBackupPath(home, at) {
38182
+ return join6(icloudDriveDir(home), ICLOUD_BACKUP_FOLDER, `vault-${backupStamp(at)}.enc`);
38183
+ }
38184
+ function backupStamp(at) {
38185
+ return new Date(at).toISOString().replace(/[-:]/gu, "").replace(/\.\d+Z$/u, "Z");
38186
+ }
38187
+ function homeDirOf(env) {
38188
+ return env.HOME?.trim() || homedir4();
38189
+ }
38162
38190
  var OTHER_CLOUD_MARKERS = [
38163
38191
  "Library/CloudStorage",
38164
38192
  "Dropbox",
@@ -41234,7 +41262,6 @@ init_store();
41234
41262
  init_args();
41235
41263
  init_errors();
41236
41264
  init_format();
41237
- init_sidecar();
41238
41265
  init_store();
41239
41266
  init_vault_support();
41240
41267
  var MAX_NEW_KEY_COUNT = 256;
@@ -41319,7 +41346,6 @@ async function vaultNewKey(args, ctx) {
41319
41346
  });
41320
41347
  const vault = hold(opened.vault);
41321
41348
  assertRecoverableFactorExists(vault.file.envelopes);
41322
- await assertHighValueSatisfied(path, vault.file.envelopes);
41323
41349
  if (vault.index.hd.discovery !== undefined) {
41324
41350
  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
41351
  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."
@@ -41432,17 +41458,6 @@ function nextAllocatableIndex(counter, exposed) {
41432
41458
  index++;
41433
41459
  return index;
41434
41460
  }
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
41461
  async function verifyWrittenFromDisk(vault, address, keyId) {
41447
41462
  const raw = await readVaultRaw(vault.path);
41448
41463
  if (raw === null) {
@@ -41513,7 +41528,6 @@ async function externalNew(args, ctx) {
41513
41528
  });
41514
41529
  const vault = hold(opened.vault);
41515
41530
  assertRecoverableFactorExists(vault.file.envelopes);
41516
- await assertHighValueSatisfied(path, vault.file.envelopes);
41517
41531
  if (vault.index.hd.discovery !== undefined) {
41518
41532
  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
41533
  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."
@@ -50067,8 +50081,8 @@ function messageOf(error) {
50067
50081
 
50068
50082
  // src/commands/vault-backup.ts
50069
50083
  init_args();
50070
- import { copyFile, stat as stat3 } from "node:fs/promises";
50071
- import nodePath, { resolve as resolve2 } from "node:path";
50084
+ import { chmod as chmod5, copyFile, mkdir as mkdir6, stat as stat3 } from "node:fs/promises";
50085
+ import nodePath, { dirname as dirname8, resolve as resolve2 } from "node:path";
50072
50086
  init_errors();
50073
50087
  init_format();
50074
50088
  init_passphrase();
@@ -50212,11 +50226,28 @@ async function vaultBackup(args, ctx) {
50212
50226
  if ("error" in resolvedVault)
50213
50227
  return usage(ctx, resolvedVault.error);
50214
50228
  const path = resolvedVault.path;
50215
- const destination = resolve2(to);
50229
+ const target = resolveBackupDestination(to, deps);
50230
+ if (target.requires !== undefined && !await exists(target.requires)) {
50231
+ 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.`);
50232
+ }
50233
+ const destination = target.path;
50234
+ if (target.requires !== undefined)
50235
+ deps.stderr.write(`--to ${ICLOUD_SHORTHAND} is ${destination}
50236
+ `);
50216
50237
  return runVaultCommand(ctx, async ({ hold }) => {
50217
50238
  const raw = await requireVaultRaw(ctx, resolvedVault);
50218
50239
  assertOutsideConfigDir(destination, deps.env);
50219
50240
  const file = parseVaultFile(raw);
50241
+ if (target.requires !== undefined) {
50242
+ const folder = dirname8(destination);
50243
+ try {
50244
+ const created = await mkdir6(folder, { recursive: true });
50245
+ if (created !== undefined)
50246
+ await chmod5(folder, 448).catch(() => {});
50247
+ } catch (error) {
50248
+ throw copyWriteFailed(destination, error, "copy");
50249
+ }
50250
+ }
50220
50251
  const verdict = await assertBackupDomainAllowed(file.envelopes, destination, {
50221
50252
  acceptSharedDomain: parsed.booleans.has("--accept-shared-domain"),
50222
50253
  realpath: deps.realpath
@@ -50236,7 +50267,11 @@ async function vaultBackup(args, ctx) {
50236
50267
  if (verdict.sealed) {
50237
50268
  await writeSealedCopy(live, destination);
50238
50269
  } else {
50239
- await copyFile(path, destination);
50270
+ try {
50271
+ await copyFile(path, destination);
50272
+ } catch (error) {
50273
+ throw copyWriteFailed(destination, error, "copy");
50274
+ }
50240
50275
  }
50241
50276
  const report = await verifyCopy(ctx, destination, opened.reopen, live);
50242
50277
  const sidecar = sidecarPath(path);
@@ -50267,15 +50302,44 @@ async function vaultBackup(args, ctx) {
50267
50302
  return 0;
50268
50303
  });
50269
50304
  }
50305
+ function resolveBackupDestination(to, deps) {
50306
+ if (to.trim().toLowerCase() !== ICLOUD_SHORTHAND)
50307
+ return { path: resolve2(to) };
50308
+ const home = homeDirOf(deps.env);
50309
+ return { path: icloudBackupPath(home, deps.now()), requires: icloudDriveDir(home) };
50310
+ }
50270
50311
  async function writeSealedCopy(live, destination) {
50271
50312
  const { index: _index, ...header } = live.file;
50272
50313
  const sealed = await sealIndex({ ...header, envelopes: sealedEnvelopes(live.file.envelopes) }, live.index, live.payloadKey);
50273
50314
  try {
50274
50315
  await writeKeystoreFile(destination, serializeVault(sealed));
50275
- } catch {
50276
- throw new VaultError("VAULT_WRITE_FAILED", `Could not write the sealed copy at ${destination}.`);
50316
+ } catch (error) {
50317
+ throw copyWriteFailed(destination, error, "sealed copy");
50277
50318
  }
50278
50319
  }
50320
+ function errnoCodeOf(error) {
50321
+ const code = error?.code;
50322
+ return typeof code === "string" && code.length > 0 ? code : undefined;
50323
+ }
50324
+ function suggestionForErrno(code) {
50325
+ if (code === "ENOSPC")
50326
+ return "The volume is full. Free space there, or back up somewhere else. Nothing was written.";
50327
+ if (code === "EROFS")
50328
+ return "That volume is mounted read-only. Nothing was written.";
50329
+ if (code === "ENOENT")
50330
+ 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.";
50331
+ if (code === "EACCES" || code === "EPERM")
50332
+ return "Check that you can write there, and that the volume or sync folder is mounted and not locked. Nothing was written.";
50333
+ return "Nothing was written. Try another destination, or run the same write by hand to see what the filesystem says.";
50334
+ }
50335
+ function copyWriteFailed(destination, error, what) {
50336
+ const code = errnoCodeOf(error);
50337
+ const reason = error instanceof Error ? error.message : String(error);
50338
+ return new VaultError("VAULT_WRITE_FAILED", `Could not write the ${what} at ${destination}: ${code ?? "no error code"} -- ${reason}`, {
50339
+ suggestion: suggestionForErrno(code),
50340
+ details: { path: destination, reason, ...code === undefined ? {} : { code } }
50341
+ });
50342
+ }
50279
50343
  function isSealedCopy(copyRaw) {
50280
50344
  const envelopes = parseVaultFile(copyRaw).envelopes;
50281
50345
  return envelopes.length > 0 && envelopes.every(isPassphraseEnvelope);
@@ -50593,8 +50657,8 @@ init_errors();
50593
50657
  init_promote_support();
50594
50658
  init_store();
50595
50659
  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";
50660
+ import { access as access3, chmod as chmod6, constants as constants4, lstat, writeFile as writeFile5 } from "node:fs/promises";
50661
+ import { dirname as dirname9, resolve as resolve3 } from "node:path";
50598
50662
  async function vaultExportKey(args, ctx) {
50599
50663
  const parsed = parseArgs(args, {
50600
50664
  valueFlags: ["--keystore", "--to"],
@@ -50706,7 +50770,7 @@ async function assertExportTargetWritable(destination) {
50706
50770
  if (error.code !== "ENOENT")
50707
50771
  throw error;
50708
50772
  }
50709
- const parent = dirname8(destination);
50773
+ const parent = dirname9(destination);
50710
50774
  try {
50711
50775
  const parentInfo = await lstat(parent);
50712
50776
  if (parentInfo.isSymbolicLink()) {
@@ -50738,7 +50802,7 @@ async function assertExportTargetWritable(destination) {
50738
50802
  async function writeExportFile(destination, body) {
50739
50803
  try {
50740
50804
  await writeFile5(destination, body, { encoding: "utf8", flag: "wx", mode: 384 });
50741
- await chmod5(destination, 384);
50805
+ await chmod6(destination, 384);
50742
50806
  } catch (error) {
50743
50807
  const code = error.code;
50744
50808
  if (code === "EEXIST") {
@@ -51246,7 +51310,6 @@ async function createVault(request2, clock) {
51246
51310
  init_crypto();
51247
51311
  init_errors();
51248
51312
  init_passphrase();
51249
- init_sidecar();
51250
51313
  init_store();
51251
51314
 
51252
51315
  // src/commands/vault-phrase.ts
@@ -51381,12 +51444,13 @@ function randomPositions(count, of = PHRASE_WORDS) {
51381
51444
 
51382
51445
  // src/commands/vault-init.ts
51383
51446
  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";
51447
+ 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.";
51448
+ 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
51449
  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
51450
  async function vaultInit(args, ctx) {
51387
51451
  const parsed = parseArgs(args, {
51388
51452
  valueFlags: ["--keystore", "--label"],
51389
- booleanFlags: ["--own-passphrase", "--high-value"],
51453
+ booleanFlags: ["--own-passphrase"],
51390
51454
  pathFlags: ["--keystore"]
51391
51455
  });
51392
51456
  if ("error" in parsed)
@@ -51395,8 +51459,8 @@ async function vaultInit(args, ctx) {
51395
51459
  return usage(ctx, `Unexpected argument: ${parsed.positionals[0]}`);
51396
51460
  if (!refuseEnvPassphrase(ctx))
51397
51461
  return 1;
51398
- const ownPassphrase = parsed.booleans.has("--own-passphrase");
51399
- if (ctx.json && !ownPassphrase)
51462
+ const ownFlag = parsed.booleans.has("--own-passphrase");
51463
+ if (ctx.json && !ownFlag)
51400
51464
  return usage(ctx, GENERATED_PASSPHRASE_NEEDS_TERMINAL);
51401
51465
  if (!requireTty(ctx, "vault init"))
51402
51466
  return 1;
@@ -51405,32 +51469,25 @@ async function vaultInit(args, ctx) {
51405
51469
  if ("error" in resolvedVault)
51406
51470
  return usage(ctx, resolvedVault.error);
51407
51471
  const path = resolvedVault.path;
51408
- const highValue = parsed.booleans.has("--high-value");
51409
51472
  return runVaultCommand(ctx, async () => {
51410
51473
  if (await fileExists(path)) {
51411
51474
  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
51475
  }
51413
- if (!ownPassphrase)
51476
+ if (!ownFlag)
51414
51477
  deps.stdout.write(`${INIT_GENERATED_PASSPHRASE_NOTICE}
51415
51478
  `);
51416
- const passphrase = ownPassphrase ? await collectOwnPassphrase(ctx) : await collectGeneratedPassphrase(ctx);
51479
+ const own = ownFlag || await askForOwnPassphrase(ctx, INIT_PASSPHRASE_PROMPT);
51480
+ const passphrase = own ? await collectOwnPassphrase(ctx) : await collectGeneratedPassphrase(ctx);
51417
51481
  const entropy = randomBytes2(ROOT_ENTROPY_BYTES);
51418
51482
  const vault = await withSecret(entropy, async (rootEntropy) => createVault({
51419
51483
  path,
51420
51484
  passphrase,
51421
- strength: strengthFor(ownPassphrase),
51485
+ strength: strengthFor(own),
51422
51486
  rootEntropy,
51423
51487
  label: parsed.values["--label"],
51424
51488
  notice: (line) => deps.stderr.write(line)
51425
51489
  }, deps));
51426
51490
  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
51491
  if (ctx.json) {
51435
51492
  writeJson(deps, {
51436
51493
  ok: true,
@@ -51442,7 +51499,6 @@ async function vaultInit(args, ctx) {
51442
51499
  factor: envelope.factor,
51443
51500
  domain: envelope.domain
51444
51501
  })),
51445
- highValue,
51446
51502
  phraseCeremonyOffered: false
51447
51503
  });
51448
51504
  deps.stderr.write(`The recovery phrase ceremony is interactive only and was not offered under --json. Run: candle vault phrase show
@@ -51456,9 +51512,6 @@ async function vaultInit(args, ctx) {
51456
51512
  deps.stdout.write(` factors 1 (passphrase, human-memory)
51457
51513
  `);
51458
51514
  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
51515
  `);
51463
51516
  deps.stdout.write(`
51464
51517
  Verified: the file was re-read and opened with the passphrase you set, and its root blob decrypted.
@@ -51466,6 +51519,11 @@ Verified: the file was re-read and opened with the passphrase you set, and its r
51466
51519
  deps.stdout.write(`
51467
51520
  ${APPLE_ACCOUNT_NOTICE}
51468
51521
  `);
51522
+ if (countRecoverableFactors(vault.file.envelopes) === 1) {
51523
+ deps.stdout.write(`
51524
+ 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
51525
+ `);
51526
+ }
51469
51527
  deps.stdout.write(`
51470
51528
  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
51529
  `);
@@ -51476,6 +51534,7 @@ This vault has a 24-word recovery phrase. It re-derives every key this vault der
51476
51534
  deps.stdout.write(`Skipped. You can run the ceremony later with: candle vault phrase show
51477
51535
  `);
51478
51536
  }
51537
+ await offerIcloudBackup(ctx, resolvedVault);
51479
51538
  const footer = nonDefaultVaultFooter(resolvedVault);
51480
51539
  if (footer !== undefined)
51481
51540
  deps.stdout.write(footer);
@@ -51486,20 +51545,19 @@ This vault has a 24-word recovery phrase. It re-derives every key this vault der
51486
51545
  });
51487
51546
  }
51488
51547
  async function collectGeneratedPassphrase(ctx) {
51548
+ const { deps } = ctx;
51489
51549
  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.
51550
+ deps.stdout.write(`
51551
+ Your vault passphrase, ${GENERATED_WORD_COUNT} words, about ${generatedEntropyBits()} bits.
51492
51552
 
51493
51553
  `);
51494
- ctx.deps.stdout.write(` ${passphrase}
51554
+ deps.stdout.write(` ${passphrase}
51495
51555
 
51496
51556
  `);
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
- }
51557
+ deps.stdout.write(`${SAVE_THE_PASSPHRASE}
51558
+
51559
+ `);
51560
+ await deps.promptLine(SAVED_IT_PROMPT);
51503
51561
  return passphrase;
51504
51562
  }
51505
51563
  async function collectOwnPassphrase(ctx) {
@@ -51514,6 +51572,28 @@ async function collectOwnPassphrase(ctx) {
51514
51572
  }
51515
51573
  return first;
51516
51574
  }
51575
+ async function offerIcloudBackup(ctx, resolved) {
51576
+ const { deps } = ctx;
51577
+ if (!await fileExists(icloudDriveDir(homeDirOf(deps.env))))
51578
+ return;
51579
+ deps.stdout.write(`
51580
+ 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.
51581
+ `);
51582
+ 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.
51583
+ `);
51584
+ 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();
51585
+ if (answer !== "yes") {
51586
+ deps.stdout.write(`Skipped. Back it up whenever you like with: candle vault backup --to ${ICLOUD_SHORTHAND}
51587
+ `);
51588
+ return;
51589
+ }
51590
+ const code = await vaultBackup(["--to", ICLOUD_SHORTHAND, "--keystore", resolved.path], ctx);
51591
+ if (code !== 0) {
51592
+ deps.stdout.write(`
51593
+ 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}
51594
+ `);
51595
+ }
51596
+ }
51517
51597
 
51518
51598
  // src/commands/vault-factor.ts
51519
51599
  init_vault_support();
@@ -53156,7 +53236,7 @@ async function vaultRestore(args, ctx) {
53156
53236
  if (rootEntropy.length !== ROOT_ENTROPY_BYTES) {
53157
53237
  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
53238
  }
53159
- const own = parsed.booleans.has("--own-passphrase") || await askForOwnPassphrase(ctx);
53239
+ const own = parsed.booleans.has("--own-passphrase") || await askForOwnPassphrase(ctx, RESTORE_PASSPHRASE_PROMPT);
53160
53240
  const passphrase = own ? await collectOwn2(ctx) : await collectGenerated2(ctx);
53161
53241
  const ownPassphrase = own;
53162
53242
  return createVault({
@@ -53618,16 +53698,6 @@ async function vaultReconcileExposure(args, ctx) {
53618
53698
  }
53619
53699
  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
53700
  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
53701
  async function collectGenerated2(ctx) {
53632
53702
  const passphrase = generatePassphrase();
53633
53703
  ctx.deps.stdout.write(`
@@ -53775,6 +53845,7 @@ init_platform();
53775
53845
  init_sidecar();
53776
53846
  init_store();
53777
53847
  init_vault_support();
53848
+ 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
53849
  async function vaultStatus(args, ctx) {
53779
53850
  const parsed = parseArgs(args, {
53780
53851
  valueFlags: ["--keystore"],
@@ -53893,6 +53964,11 @@ This machine's record (vault.state.json, cleartext, best effort):
53893
53964
  } else {
53894
53965
  deps.stdout.write(`
53895
53966
  No vault.state.json beside this vault, so an older copy of it cannot be recognized on this machine.
53967
+ `);
53968
+ }
53969
+ if (sidecar?.lastVerifiedBackupAt === undefined) {
53970
+ deps.stdout.write(`
53971
+ ${NO_VERIFIED_BACKUP_NOTE}
53896
53972
  `);
53897
53973
  }
53898
53974
  if (legacyPresent) {
@@ -54088,7 +54164,7 @@ async function vaultTransfer(args, ctx) {
54088
54164
  // src/commands/verify.ts
54089
54165
  init_args();
54090
54166
  init_release();
54091
- import { dirname as dirname9, join as join11 } from "node:path";
54167
+ import { dirname as dirname10, join as join11 } from "node:path";
54092
54168
  init_render();
54093
54169
  var USAGE2 = "Usage: candle verify <file> --bundle <path> [--identity <uri>] [--issuer <url>]";
54094
54170
  async function resolveIdentity(deps, bundlePath, flag) {
@@ -54096,7 +54172,7 @@ async function resolveIdentity(deps, bundlePath, flag) {
54096
54172
  return { kind: "ok", uri: flag, provenance: "identity from --identity" };
54097
54173
  let version;
54098
54174
  try {
54099
- const manifest = JSON.parse(await deps.readFile(join11(dirname9(bundlePath), "latest.json")));
54175
+ const manifest = JSON.parse(await deps.readFile(join11(dirname10(bundlePath), "latest.json")));
54100
54176
  if (typeof manifest.version !== "string" || manifest.version.length === 0)
54101
54177
  return { kind: "absent" };
54102
54178
  version = manifest.version;
@@ -54190,7 +54266,7 @@ function messageOf2(error) {
54190
54266
  }
54191
54267
 
54192
54268
  // src/config.ts
54193
- import { chmod as chmod6, mkdir as mkdir6, readFile as readFile7, rm as rm4, writeFile as writeFile6 } from "node:fs/promises";
54269
+ import { chmod as chmod7, mkdir as mkdir7, readFile as readFile7, rm as rm4, writeFile as writeFile6 } from "node:fs/promises";
54194
54270
  import { homedir as homedir6 } from "node:os";
54195
54271
  import { join as join12 } from "node:path";
54196
54272
  function configDir2() {
@@ -54213,8 +54289,8 @@ async function writeConfig(patch) {
54213
54289
  const current = await readConfig();
54214
54290
  const next = { ...current, ...patch };
54215
54291
  const dir = configDir2();
54216
- await mkdir6(dir, { recursive: true });
54217
- await chmod6(dir, 448);
54292
+ await mkdir7(dir, { recursive: true });
54293
+ await chmod7(dir, 448);
54218
54294
  await writeFile6(configFilePath(), JSON.stringify(next, null, 2), "utf8");
54219
54295
  }
54220
54296
  async function updateProfile(name, patch) {
@@ -54857,7 +54933,7 @@ async function buildRealDeps() {
54857
54933
  releasePolicy: RELEASE_POLICY,
54858
54934
  writeBytes: async (path, bytes) => {
54859
54935
  await writeFile7(path, bytes, { flag: "wx", mode: 493 });
54860
- await chmod7(path, 493);
54936
+ await chmod8(path, 493);
54861
54937
  },
54862
54938
  rename: (from, to) => rename5(from, to),
54863
54939
  unlink: (path) => unlink(path)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@candledottv/cli",
3
- "version": "0.11.1",
3
+ "version": "0.11.3",
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": {