@candledottv/cli 0.11.2 → 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.
- package/dist/index.js +154 -78
- 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
|
-
|
|
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
|
|
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.
|
|
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]
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
50597
|
-
import { dirname as
|
|
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 =
|
|
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
|
|
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
|
|
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"
|
|
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
|
|
51399
|
-
if (ctx.json && !
|
|
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 (!
|
|
51476
|
+
if (!ownFlag)
|
|
51414
51477
|
deps.stdout.write(`${INIT_GENERATED_PASSPHRASE_NOTICE}
|
|
51415
51478
|
`);
|
|
51416
|
-
const
|
|
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(
|
|
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
|
-
|
|
51491
|
-
Your vault passphrase, ${GENERATED_WORD_COUNT} words, about ${generatedEntropyBits()} bits.
|
|
51550
|
+
deps.stdout.write(`
|
|
51551
|
+
Your vault passphrase, ${GENERATED_WORD_COUNT} words, about ${generatedEntropyBits()} bits.
|
|
51492
51552
|
|
|
51493
51553
|
`);
|
|
51494
|
-
|
|
51554
|
+
deps.stdout.write(` ${passphrase}
|
|
51495
51555
|
|
|
51496
51556
|
`);
|
|
51497
|
-
|
|
51498
|
-
|
|
51499
|
-
|
|
51500
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
54217
|
-
await
|
|
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
|
|
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