@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.
- package/dist/index.js +498 -114
- 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);
|
|
@@ -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
|
|
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
|
|
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.
|
|
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]
|
|
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
|
|
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
|
-
|
|
41329
|
-
|
|
41330
|
-
|
|
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
|
|
42355
|
+
async function teeWallets(ctx, key, scope) {
|
|
42303
42356
|
let cursor;
|
|
42304
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
42323
|
-
|
|
42324
|
-
|
|
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 ||
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
50387
|
+
try {
|
|
50388
|
+
await copyFile(path, destination);
|
|
50389
|
+
} catch (error) {
|
|
50390
|
+
throw copyWriteFailed(destination, error, "copy");
|
|
50391
|
+
}
|
|
50240
50392
|
}
|
|
50241
|
-
const
|
|
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
|
|
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
|
|
50597
|
-
import { dirname as
|
|
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 =
|
|
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
|
|
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
|
|
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"
|
|
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
|
|
51399
|
-
if (ctx.json && !
|
|
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 (!
|
|
51608
|
+
if (!ownFlag)
|
|
51414
51609
|
deps.stdout.write(`${INIT_GENERATED_PASSPHRASE_NOTICE}
|
|
51415
51610
|
`);
|
|
51416
|
-
const
|
|
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(
|
|
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
|
-
|
|
51491
|
-
Your vault passphrase, ${GENERATED_WORD_COUNT} words, about ${generatedEntropyBits()} bits.
|
|
51682
|
+
deps.stdout.write(`
|
|
51683
|
+
Your vault passphrase, ${GENERATED_WORD_COUNT} words, about ${generatedEntropyBits()} bits.
|
|
51492
51684
|
|
|
51493
51685
|
`);
|
|
51494
|
-
|
|
51686
|
+
deps.stdout.write(` ${passphrase}
|
|
51495
51687
|
|
|
51496
51688
|
`);
|
|
51497
|
-
|
|
51498
|
-
|
|
51499
|
-
|
|
51500
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
54217
|
-
await
|
|
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
|
|
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