@candledottv/cli 0.11.7 → 0.11.8
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/README.md +3 -2
- package/dist/index.js +731 -75
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -55,8 +55,8 @@ node packages/cli/dist/index.js auth login
|
|
|
55
55
|
| `candle auth login [--profile <name>] [--scopes <a,b,c>] [--label <name>] [--no-browser]` | Authorizes this device: prints a code, opens (or prints) an approval URL, polls until approved, then stores the resulting device token and API key. `--label` names both the device and the key. |
|
|
56
56
|
| `candle auth status` | Shows which storage backend is in use, both credential prefixes, the config file path, and a live validity check for each credential. |
|
|
57
57
|
| `candle auth logout [--keep-key]` | Revokes the stored API key (skipped with `--keep-key`), clears local credentials and config, and prints the portal URL for revoking the device itself. |
|
|
58
|
-
| `candle keys list [--scopes]` | Lists this account's API keys: prefix, name, Read or Read:Write access, environment, timestamps, and which device minted each one. `--scopes` adds the raw scopes, sorted. |
|
|
59
|
-
| `candle keys create [--scopes <a,b,c>] [--label <name>] [--expires-in <days>] [--tx-limit <usd> [--reset daily\|weekly\|monthly\|never]]` | Creates a new API key and prints the plaintext exactly once, with the same optional name, expiration, and USD transaction limit the portal's create form takes. Stored locally only if the CLI does not already hold a working key. |
|
|
58
|
+
| `candle keys list [--scopes]` | Lists this account's API keys: prefix, name, Read, Read:Write or Read:Write:Transfer access, environment, timestamps, and which device minted each one. `--scopes` adds the raw scopes, sorted. |
|
|
59
|
+
| `candle keys create [--access read\|read-write\|read-write-transfer \| --scopes <a,b,c>] [--label <name>] [--expires-in <days>] [--tx-limit <usd> [--reset daily\|weekly\|monthly\|never]]` | Creates a new API key and prints the plaintext exactly once, with the same optional name, expiration, and USD transaction limit the portal's create form takes. `--access` mints one of the portal's three levels (`read-write-transfer` is Read:Write plus `transfer:bound`: the key can move funds out of the wallet it runs, to wallets you linked while signed in or marked trusted and to that wallet's vault); `--scopes` names raw scopes instead, never both. Stored locally only if the CLI does not already hold a working key. |
|
|
60
60
|
| `candle keys revoke <prefix>` | Revokes an API key by prefix. Revoking the CLI's own stored key also clears it locally. |
|
|
61
61
|
| `candle wallets` | Shows the account's embedded (launch) wallets and any linked wallets, using the API key, with a `Signer` column saying whether this machine holds each linked wallet's signing key. |
|
|
62
62
|
| `candle vault init\|status\|new-key\|phrase show\|restore` | The encrypted local vault, `vault.enc`. `init` creates it with a passphrase factor and a 24-word recovery phrase, `new-key` derives a key inside it, `phrase show` displays the phrase on a terminal (never in `--json`, a log or a pipe), and `restore --phrase` rebuilds the derived keys on another machine (`--count`, `--tee-count`, `--external-count`, or a gap scan with `--rpc-url`). No key ever leaves the vault to reach Candle. See [The vault](https://docs.candle.tv/developers/cli#the-vault). |
|
|
@@ -70,6 +70,7 @@ node packages/cli/dist/index.js auth login
|
|
|
70
70
|
| `candle tee new\|enable\|fund\|status\|disable\|sweep` | A dedicated, capped TEE wallet for one agent: the CLI generates the key and seals it locally in `tee-wallets.enc`, `enable` delegates it to this profile's API key with a pinned sweep vault, `fund` prints what your vault signs, and `disable` then `sweep` stop the agent and move everything back to the vault, signed locally. Solana only. See [TEE wallets](https://docs.candle.tv/developers/cli#tee-wallets). |
|
|
71
71
|
| `candle swap <from> <to> --amount <n>\|--percent <n> --wallet <tee>` | Quote, confirm and swap on Solana through the TEE wallet's bound key; first buy after a launch is this command. |
|
|
72
72
|
| `candle swap status <id> [--kind trade\|swap\|launch]` | Read an operation without resending it. |
|
|
73
|
+
| `candle transfer --to <address\|wallet name\|vault> --asset <SOL\|USDC\|CNDL>\|--mint <mint> --amount <decimal\|max> [--wallet <tee>] [--yes]` | Move funds out of a TEE wallet through its bound Read:Write:Transfer key, to the wallet's own pinned vault (`--to vault`, any token, `max` allowed) or to another of the account's wallets you linked while signed in or marked trusted (base assets, counted against the key's caps). The destination and its kind are shown before you confirm; Candle builds, the relay signs, Candle broadcasts. |
|
|
73
74
|
| `candle launch --name <name> --symbol <symbol> --image-url <url> --wallet <tee>` | Create a Solana token with no first buy; needs `launch:write` and operator-enabled `allowLaunch`. |
|
|
74
75
|
| `candle pnl [--profile <name>]` | P&L: realized net, fees, unrealized and total, then open positions. Without `--profile`, the account's books (every profile, the web app and the CLI, one ledger; needs the Read scope). With `--profile <name>`, that profile's key's own P&L. Unpriced positions are never valued at zero. Where Candle serves LP, DAMM v2 positions are included: realized (withdrawals against the cost basis at the add, plus claimed fees), unrealized (open positions at their share of the pool plus unclaimed fees, against that basis) and vs holding (what the deposited tokens would be worth held), with the total covering tokens and LP. |
|
|
75
76
|
| `candle portfolio [--rpc-url <url>]` | Vault, TEE and embedded wallets in one table with amount, price, value and a total. Vault and external balances are read over your own RPC; Candle is sent only the mints, for prices. Where Candle serves LP, each TEE wallet's DAMM v2 positions follow the tokens, valued by Candle at the same marks, and count in the total; one whose pool could not be read is shown as not read and the total says it is partial. |
|
package/dist/index.js
CHANGED
|
@@ -278,6 +278,16 @@ var init_args = __esm(() => {
|
|
|
278
278
|
});
|
|
279
279
|
|
|
280
280
|
// src/agent-key-access.ts
|
|
281
|
+
function scopesForPreset(preset) {
|
|
282
|
+
switch (preset) {
|
|
283
|
+
case "read":
|
|
284
|
+
return [...READ_SCOPES];
|
|
285
|
+
case "readwrite":
|
|
286
|
+
return [...READWRITE_SCOPES];
|
|
287
|
+
case "readwritetransfer":
|
|
288
|
+
return [...READWRITETRANSFER_SCOPES];
|
|
289
|
+
}
|
|
290
|
+
}
|
|
281
291
|
function sameSet(scopes, preset) {
|
|
282
292
|
const held = new Set(scopes);
|
|
283
293
|
return held.size === preset.length && preset.every((scope) => held.has(scope));
|
|
@@ -287,6 +297,8 @@ function presetForScopes(scopes) {
|
|
|
287
297
|
return "read";
|
|
288
298
|
if (sameSet(scopes, READWRITE_SCOPES))
|
|
289
299
|
return "readwrite";
|
|
300
|
+
if (sameSet(scopes, READWRITETRANSFER_SCOPES))
|
|
301
|
+
return "readwritetransfer";
|
|
290
302
|
return null;
|
|
291
303
|
}
|
|
292
304
|
function agentKeyCapabilities(scopes) {
|
|
@@ -295,6 +307,7 @@ function agentKeyCapabilities(scopes) {
|
|
|
295
307
|
launch: scopes.includes("launch:write"),
|
|
296
308
|
trade: scopes.includes("swap:write"),
|
|
297
309
|
transfer: scopes.includes("transfer:write"),
|
|
310
|
+
linkedTransfer: scopes.includes("transfer:bound"),
|
|
298
311
|
report: scopes.includes("activity:write")
|
|
299
312
|
};
|
|
300
313
|
}
|
|
@@ -308,7 +321,7 @@ function agentKeyAccess(scopes) {
|
|
|
308
321
|
function sortAgentKeyScopes(scopes) {
|
|
309
322
|
return [...scopes].sort();
|
|
310
323
|
}
|
|
311
|
-
var READ_SCOPES, READWRITE_SCOPES, AGENT_KEY_PRESET_LABELS, AGENT_KEY_CAPABILITY_CHIPS;
|
|
324
|
+
var READ_SCOPES, READWRITE_SCOPES, READWRITETRANSFER_SCOPES, AGENT_KEY_PRESET_LABELS, AGENT_KEY_CAPABILITY_CHIPS;
|
|
312
325
|
var init_agent_key_access = __esm(() => {
|
|
313
326
|
READ_SCOPES = ["account:read"];
|
|
314
327
|
READWRITE_SCOPES = [
|
|
@@ -319,18 +332,32 @@ var init_agent_key_access = __esm(() => {
|
|
|
319
332
|
"transfer:write",
|
|
320
333
|
"account:read"
|
|
321
334
|
];
|
|
322
|
-
|
|
335
|
+
READWRITETRANSFER_SCOPES = [
|
|
336
|
+
"launch:write",
|
|
337
|
+
"launch:read",
|
|
338
|
+
"activity:write",
|
|
339
|
+
"swap:write",
|
|
340
|
+
"transfer:write",
|
|
341
|
+
"account:read",
|
|
342
|
+
"transfer:bound"
|
|
343
|
+
];
|
|
344
|
+
AGENT_KEY_PRESET_LABELS = {
|
|
345
|
+
read: "Read",
|
|
346
|
+
readwrite: "Read:Write",
|
|
347
|
+
readwritetransfer: "Read:Write:Transfer"
|
|
348
|
+
};
|
|
323
349
|
AGENT_KEY_CAPABILITY_CHIPS = [
|
|
324
350
|
["Launch", "launch"],
|
|
325
351
|
["Trade", "trade"],
|
|
326
352
|
["Transfer", "transfer"],
|
|
353
|
+
["Linked transfer", "linkedTransfer"],
|
|
327
354
|
["Report", "report"]
|
|
328
355
|
];
|
|
329
356
|
});
|
|
330
357
|
|
|
331
358
|
// src/render.ts
|
|
332
359
|
function formatScopesForSummary(scopes) {
|
|
333
|
-
return sortAgentKeyScopes(scopes).map((scope) => scope === "swap:write" ? `${scope} (${SWAP_WRITE_NOTE})` : scope === "transfer:write" ? `${scope} (${TRANSFER_WRITE_NOTE})` : scope).join(", ");
|
|
360
|
+
return sortAgentKeyScopes(scopes).map((scope) => scope === "swap:write" ? `${scope} (${SWAP_WRITE_NOTE})` : scope === "transfer:write" ? `${scope} (${TRANSFER_WRITE_NOTE})` : scope === "transfer:bound" ? `${scope} (${TRANSFER_BOUND_NOTE})` : scope).join(", ");
|
|
334
361
|
}
|
|
335
362
|
function terminalText(value) {
|
|
336
363
|
return value.replace(/[\u0000-\u001f\u007f-\u009f]/g, "");
|
|
@@ -436,7 +463,7 @@ function portalDeviceUrl(apiUrl, portalOrigin) {
|
|
|
436
463
|
return `${apiUrl}/agents`;
|
|
437
464
|
}
|
|
438
465
|
}
|
|
439
|
-
var ALL_AGENT_SCOPES, DEFAULT_AGENT_SCOPES, SWAP_WRITE_NOTE = "moves funds -- this key can execute swaps on your behalf", TRANSFER_WRITE_NOTE = "moves funds -- this key can transfer assets between your wallets";
|
|
466
|
+
var ALL_AGENT_SCOPES, DEFAULT_AGENT_SCOPES, SWAP_WRITE_NOTE = "moves funds -- this key can execute swaps on your behalf", TRANSFER_WRITE_NOTE = "moves funds -- this key can transfer assets between your wallets", TRANSFER_BOUND_NOTE = "moves funds -- this key can move funds out of the wallet it runs, to your linked wallets and its vault";
|
|
440
467
|
var init_render = __esm(() => {
|
|
441
468
|
init_agent_key_access();
|
|
442
469
|
ALL_AGENT_SCOPES = [
|
|
@@ -445,9 +472,10 @@ var init_render = __esm(() => {
|
|
|
445
472
|
"account:read",
|
|
446
473
|
"activity:write",
|
|
447
474
|
"swap:write",
|
|
448
|
-
"transfer:write"
|
|
475
|
+
"transfer:write",
|
|
476
|
+
"transfer:bound"
|
|
449
477
|
];
|
|
450
|
-
DEFAULT_AGENT_SCOPES = ALL_AGENT_SCOPES.filter((scope) => scope !== "swap:write" && scope !== "transfer:write");
|
|
478
|
+
DEFAULT_AGENT_SCOPES = ALL_AGENT_SCOPES.filter((scope) => scope !== "swap:write" && scope !== "transfer:write" && scope !== "transfer:bound");
|
|
451
479
|
});
|
|
452
480
|
|
|
453
481
|
// src/profiles.ts
|
|
@@ -2164,6 +2192,9 @@ function libraryInstallInstruction(platform) {
|
|
|
2164
2192
|
}
|
|
2165
2193
|
|
|
2166
2194
|
// src/fido2-helper/protocol.ts
|
|
2195
|
+
function excludeCapacity(device) {
|
|
2196
|
+
return device.maxCredentialCountInList ?? 1;
|
|
2197
|
+
}
|
|
2167
2198
|
var HELPER_PROTOCOL = 1, RP_ID = "cli.candle.tv", PROBE_MAX_CREDENTIALS = 16, AUTHDATA_FLAG_UP = 1, AUTHDATA_FLAG_UV = 4, AUTHDATA_MIN_LENGTH = 37;
|
|
2168
2199
|
var init_protocol = () => {};
|
|
2169
2200
|
|
|
@@ -3144,7 +3175,8 @@ var init_errors = __esm(() => {
|
|
|
3144
3175
|
"TIER_REQUIRED",
|
|
3145
3176
|
"LINKED_WALLET_ROOM_UNREADABLE",
|
|
3146
3177
|
"PROMOTE_ACCOUNT_UNRESOLVED",
|
|
3147
|
-
"TRANSFER_SWEEP_PENDING"
|
|
3178
|
+
"TRANSFER_SWEEP_PENDING",
|
|
3179
|
+
"VAULT_KEY_ALREADY_ENROLLED"
|
|
3148
3180
|
];
|
|
3149
3181
|
VaultError = class VaultError extends Error {
|
|
3150
3182
|
code;
|
|
@@ -5957,6 +5989,9 @@ function translateHelperFailure(code, message, platform) {
|
|
|
5957
5989
|
LIBRARY_MISSING: "VAULT_HELPER_MISSING"
|
|
5958
5990
|
};
|
|
5959
5991
|
const suggestion = "Nothing was derived and no other factor was tried.";
|
|
5992
|
+
if (code === "CREDENTIAL_EXCLUDED") {
|
|
5993
|
+
return new VaultError("VAULT_KEY_ALREADY_ENROLLED", `This security key already holds one of this vault's credentials, so enrolling it again would not add a second key (${message}). Nothing was written.`, { suggestion: ALREADY_ENROLLED_SUGGESTION });
|
|
5994
|
+
}
|
|
5960
5995
|
if (code === "DEVICE_NOT_READABLE") {
|
|
5961
5996
|
return new VaultError("VAULT_AUTHENTICATOR_NOT_READABLE", platform === "linux" ? HIDRAW_MESSAGE : `A security key is attached but this user cannot open it: ${message}.`, { suggestion: "No other factor is substituted." });
|
|
5962
5997
|
}
|
|
@@ -6017,6 +6052,51 @@ async function callHelper(deps, helperPath, request) {
|
|
|
6017
6052
|
throw translateHelperFailure(String(response.code), String(response.message), deps.platform);
|
|
6018
6053
|
return response;
|
|
6019
6054
|
}
|
|
6055
|
+
function alreadyEnrolled(device, envelopeId) {
|
|
6056
|
+
const product = device.product || "This security key";
|
|
6057
|
+
return new VaultError("VAULT_KEY_ALREADY_ENROLLED", `${product} (--device ${device.deviceId}) already holds this vault's security key credential${envelopeId !== undefined ? ` for envelope ${envelopeId}` : ""}, so enrolling it again would not add a second key. Nothing was written.`, {
|
|
6058
|
+
suggestion: ALREADY_ENROLLED_SUGGESTION,
|
|
6059
|
+
...envelopeId !== undefined ? { details: { envelopeId } } : {}
|
|
6060
|
+
});
|
|
6061
|
+
}
|
|
6062
|
+
function helperPredatesExcludeList(helperPath, leftover) {
|
|
6063
|
+
return new VaultError("VAULT_HELPER_MISSING", `The security key helper at ${helperPath} predates the exclude list this command needs; reinstall the CLI so candle and candle-fido2 come from the same release.`, {
|
|
6064
|
+
suggestion: leftover ? "Nothing was written to the vault. The credential now on the authenticator can be removed with its vendor's tool. Reinstall the CLI so candle and candle-fido2 come from the same release, then enroll again." : "Nothing was sent to any key and nothing was written. Reinstall the CLI so candle and candle-fido2 come from the same release, then enroll again."
|
|
6065
|
+
});
|
|
6066
|
+
}
|
|
6067
|
+
function assertHelperExcludeList(info, helperPath, excludeCount) {
|
|
6068
|
+
if (excludeCount === 0)
|
|
6069
|
+
return;
|
|
6070
|
+
const features = Array.isArray(info.features) ? info.features : [];
|
|
6071
|
+
if (!features.includes(EXCLUDE_LIST_FEATURE))
|
|
6072
|
+
throw helperPredatesExcludeList(helperPath, false);
|
|
6073
|
+
}
|
|
6074
|
+
function assertExcludeListFits(device, excludeCount) {
|
|
6075
|
+
const capacity = excludeCapacity(device);
|
|
6076
|
+
if (excludeCount <= capacity)
|
|
6077
|
+
return;
|
|
6078
|
+
throw new VaultError("VAULT_FACTOR_UNAVAILABLE", `${device.product || "This security key"} (--device ${device.deviceId}) takes at most ${capacity} credential${capacity === 1 ? "" : "s"} in one request${device.maxCredentialCountInList === null ? " (it reports no maxCredentialCountInList)" : ""}, and this vault has ${excludeCount} security key credentials to exclude, so it cannot enroll on this vault. Nothing was sent to it.`, { suggestion: "Nothing was written. Use a security key that reports a larger maxCredentialCountInList." });
|
|
6079
|
+
}
|
|
6080
|
+
function assertDeviceServesFactor(device) {
|
|
6081
|
+
if (!device.extensions.includes("hmac-secret")) {
|
|
6082
|
+
throw new VaultError("VAULT_PRF_UNSUPPORTED", `${device.product || "This security key"} does not support the hmac-secret extension, which this factor needs.`, { suggestion: "Use a key that supports hmac-secret (FIDO2 with PRF). No other derivation is substituted." });
|
|
6083
|
+
}
|
|
6084
|
+
if (device.options.clientPin !== true && device.options.uv !== true) {
|
|
6085
|
+
throw new VaultError("VAULT_UV_UNSUPPORTED", `${device.product || "This security key"} has no PIN set and no built-in user verification, and this factor uses the user-verified secret only.`, { suggestion: "Set a PIN on this key (its vendor's tool does that) and retry. Nothing was written." });
|
|
6086
|
+
}
|
|
6087
|
+
}
|
|
6088
|
+
async function listSecurityKeys(deps, opts) {
|
|
6089
|
+
const location = await locateFido2Helper(deps);
|
|
6090
|
+
if (location.state === "absent")
|
|
6091
|
+
throw helperMissing(location);
|
|
6092
|
+
const info = await callHelper(deps, location.path, {
|
|
6093
|
+
op: "info",
|
|
6094
|
+
vaultId: opts.vaultId,
|
|
6095
|
+
envelopeId: opts.envelopeId,
|
|
6096
|
+
digest: base64.encode(operationDigest({ vaultId: opts.vaultId, envelopeId: opts.envelopeId, op: "info", nonce: b64u(randomBytes2(16)) }))
|
|
6097
|
+
});
|
|
6098
|
+
return { helperPath: location.path, info };
|
|
6099
|
+
}
|
|
6020
6100
|
function describeDeviceForList(device) {
|
|
6021
6101
|
const product = device.product || "security key";
|
|
6022
6102
|
return `--device ${device.deviceId} ${product}${device.manufacturer ? ` (${device.manufacturer})` : ""}${device.readable ? "" : " [not readable by this user]"}`;
|
|
@@ -6099,17 +6179,12 @@ async function openSecurityKeySession(deps, opts) {
|
|
|
6099
6179
|
deps.stderr.write(`Using the attached security key: ${device.product || "security key"} (--device ${device.deviceId})
|
|
6100
6180
|
`);
|
|
6101
6181
|
}
|
|
6102
|
-
if (opts.requireFeatures)
|
|
6103
|
-
|
|
6104
|
-
|
|
6105
|
-
}
|
|
6106
|
-
if (device.options.clientPin !== true && device.options.uv !== true) {
|
|
6107
|
-
throw new VaultError("VAULT_UV_UNSUPPORTED", `${device.product || "This security key"} has no PIN set and no built-in user verification, and this factor uses the user-verified secret only.`, { suggestion: "Set a PIN on this key (its vendor's tool does that) and retry. Nothing was written." });
|
|
6108
|
-
}
|
|
6109
|
-
}
|
|
6182
|
+
if (opts.requireFeatures)
|
|
6183
|
+
assertDeviceServesFactor(device);
|
|
6184
|
+
opts.beforePin?.(device, info);
|
|
6110
6185
|
const session = { helperPath: location.path, device, snapshotId: info.snapshotId };
|
|
6111
6186
|
if (device.options.clientPin === true) {
|
|
6112
|
-
const typed = await deps.promptSecret(`PIN for ${device.product || "the security key"} (input hidden): `);
|
|
6187
|
+
const typed = await deps.promptSecret(opts.pinPrompt?.(device) ?? `PIN for ${device.product || "the security key"} (input hidden): `);
|
|
6113
6188
|
if (typed === "") {
|
|
6114
6189
|
throw new VaultError("VAULT_PIN_REQUIRED", "This security key needs its PIN and none was typed; nothing was sent to it.");
|
|
6115
6190
|
}
|
|
@@ -6163,7 +6238,7 @@ async function probeAttachedKeys(deps, opts) {
|
|
|
6163
6238
|
if (opts.deviceFlag === undefined && allReadable && !uncertain)
|
|
6164
6239
|
state.set(envelopeId, "not attached");
|
|
6165
6240
|
}
|
|
6166
|
-
return { state, holder };
|
|
6241
|
+
return { state, holder, devices, credentials: credentialOf };
|
|
6167
6242
|
} catch {
|
|
6168
6243
|
return;
|
|
6169
6244
|
}
|
|
@@ -6171,20 +6246,37 @@ async function probeAttachedKeys(deps, opts) {
|
|
|
6171
6246
|
async function registerCredential(deps, session, opts) {
|
|
6172
6247
|
deps.stderr.write(`Touch ${session.device.product || "the security key"} to register the vault's credential on it.
|
|
6173
6248
|
`);
|
|
6174
|
-
const digest = operationDigest({
|
|
6175
|
-
const response = await callHelper(deps, session.helperPath, {
|
|
6176
|
-
op: "register",
|
|
6249
|
+
const digest = operationDigest({
|
|
6177
6250
|
vaultId: opts.vaultId,
|
|
6178
6251
|
envelopeId: opts.envelopeId,
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
expectSnapshot: session.snapshotId,
|
|
6182
|
-
rpId: RP_ID,
|
|
6183
|
-
userId: base64.encode(userIdFor(opts.vaultId, opts.envelopeId)),
|
|
6184
|
-
userName: userNameFor(opts.vaultId, opts.envelopeId),
|
|
6185
|
-
clientDataHash: base64.encode(digest),
|
|
6186
|
-
...session.pin !== undefined ? { pin: session.pin } : {}
|
|
6252
|
+
op: "register",
|
|
6253
|
+
nonce: b64u(randomBytes2(16))
|
|
6187
6254
|
});
|
|
6255
|
+
const exclude = opts.excludeCredentialIds ?? [];
|
|
6256
|
+
let response;
|
|
6257
|
+
try {
|
|
6258
|
+
response = await callHelper(deps, session.helperPath, {
|
|
6259
|
+
op: "register",
|
|
6260
|
+
vaultId: opts.vaultId,
|
|
6261
|
+
envelopeId: opts.envelopeId,
|
|
6262
|
+
digest: base64.encode(digest),
|
|
6263
|
+
deviceId: session.device.deviceId,
|
|
6264
|
+
expectSnapshot: session.snapshotId,
|
|
6265
|
+
rpId: RP_ID,
|
|
6266
|
+
userId: base64.encode(userIdFor(opts.vaultId, opts.envelopeId)),
|
|
6267
|
+
userName: userNameFor(opts.vaultId, opts.envelopeId),
|
|
6268
|
+
clientDataHash: base64.encode(digest),
|
|
6269
|
+
...session.pin !== undefined ? { pin: session.pin } : {},
|
|
6270
|
+
...exclude.length > 0 ? { excludeCredentialIds: exclude } : {}
|
|
6271
|
+
});
|
|
6272
|
+
} catch (error) {
|
|
6273
|
+
if (error instanceof VaultError && error.code === "VAULT_KEY_ALREADY_ENROLLED")
|
|
6274
|
+
throw alreadyEnrolled(session.device);
|
|
6275
|
+
throw error;
|
|
6276
|
+
}
|
|
6277
|
+
if (exclude.length > 0 && response.excluded !== exclude.length) {
|
|
6278
|
+
throw helperPredatesExcludeList(session.helperPath, true);
|
|
6279
|
+
}
|
|
6188
6280
|
const authData = base64.decode(response.authData);
|
|
6189
6281
|
assertAuthenticatorData(authData, RP_ID, "registration");
|
|
6190
6282
|
return {
|
|
@@ -6224,7 +6316,7 @@ async function assertPrf(deps, session, envelope, vaultId, purpose) {
|
|
|
6224
6316
|
}
|
|
6225
6317
|
return prfOutput;
|
|
6226
6318
|
}
|
|
6227
|
-
var HELPER_NAME = "candle-fido2", HELPER_ENV = "CANDLE_FIDO2_HELPER", HELPER_TIMEOUT_MS = 90000, HELPER_INSTALL_SUGGESTION = "Install a release build of the CLI (the installer script or Homebrew place candle-fido2 beside candle), or set CANDLE_FIDO2_HELPER to the path of a candle-fido2 executable. No other factor is substituted.";
|
|
6319
|
+
var HELPER_NAME = "candle-fido2", HELPER_ENV = "CANDLE_FIDO2_HELPER", HELPER_TIMEOUT_MS = 90000, HELPER_INSTALL_SUGGESTION = "Install a release build of the CLI (the installer script or Homebrew place candle-fido2 beside candle), or set CANDLE_FIDO2_HELPER to the path of a candle-fido2 executable. No other factor is substituted.", ALREADY_ENROLLED_SUGGESTION = "Insert a different security key, or name one with --device.", EXCLUDE_LIST_FEATURE = "exclude-list";
|
|
6228
6320
|
var init_fido2 = __esm(() => {
|
|
6229
6321
|
init_sha256();
|
|
6230
6322
|
init_esm();
|
|
@@ -8669,7 +8761,7 @@ async function keysList(args, ctx) {
|
|
|
8669
8761
|
async function keysCreate(args, ctx) {
|
|
8670
8762
|
const { deps, apiUrl, json } = ctx;
|
|
8671
8763
|
const parsed = parseArgs(args, {
|
|
8672
|
-
valueFlags: ["--scopes", "--environment", "--label", "--expires-in", "--tx-limit", "--reset"]
|
|
8764
|
+
valueFlags: ["--scopes", "--access", "--environment", "--label", "--expires-in", "--tx-limit", "--reset"]
|
|
8673
8765
|
});
|
|
8674
8766
|
if ("error" in parsed) {
|
|
8675
8767
|
writeUsageFailure(deps, parsed.error, json);
|
|
@@ -8679,7 +8771,20 @@ async function keysCreate(args, ctx) {
|
|
|
8679
8771
|
writeUsageFailure(deps, `Unexpected argument: ${parsed.positionals[0]}`, json);
|
|
8680
8772
|
return 2;
|
|
8681
8773
|
}
|
|
8682
|
-
|
|
8774
|
+
if (parsed.values["--access"] !== undefined && parsed.values["--scopes"] !== undefined) {
|
|
8775
|
+
writeUsageFailure(deps, "--access and --scopes are mutually exclusive; pass one of them.", json);
|
|
8776
|
+
return 2;
|
|
8777
|
+
}
|
|
8778
|
+
let accessScopes;
|
|
8779
|
+
if (parsed.values["--access"] !== undefined) {
|
|
8780
|
+
const preset = ACCESS_LEVELS[parsed.values["--access"]];
|
|
8781
|
+
if (preset === undefined) {
|
|
8782
|
+
writeUsageFailure(deps, `--access must be one of: ${Object.keys(ACCESS_LEVELS).join(", ")}.`, json);
|
|
8783
|
+
return 2;
|
|
8784
|
+
}
|
|
8785
|
+
accessScopes = scopesForPreset(preset);
|
|
8786
|
+
}
|
|
8787
|
+
const requestedScopes = accessScopes ?? (parsed.values["--scopes"] ? parseScopesList(parsed.values["--scopes"]) : undefined);
|
|
8683
8788
|
const environment = parsed.values["--environment"];
|
|
8684
8789
|
const label = parsed.values["--label"]?.trim();
|
|
8685
8790
|
if (parsed.values["--label"] !== undefined && (label === undefined || label.length < 1 || label.length > 64)) {
|
|
@@ -8775,7 +8880,7 @@ WARNING: the key above was NOT stored in the ${deps.backend} store: ${storeError
|
|
|
8775
8880
|
`);
|
|
8776
8881
|
}
|
|
8777
8882
|
if (!requestedScopes) {
|
|
8778
|
-
deps.stdout.write(`No --scopes given: the server granted the default scopes (swap:write excluded).
|
|
8883
|
+
deps.stdout.write(`No --access or --scopes given: the server granted the default scopes (swap:write excluded).
|
|
8779
8884
|
`);
|
|
8780
8885
|
}
|
|
8781
8886
|
if (storeError === undefined) {
|
|
@@ -8842,7 +8947,7 @@ async function keysRevoke(args, ctx) {
|
|
|
8842
8947
|
}
|
|
8843
8948
|
return 0;
|
|
8844
8949
|
}
|
|
8845
|
-
var KEYS_PATH = "/api/v1/agent/keys", NO_DEVICE_TOKEN;
|
|
8950
|
+
var KEYS_PATH = "/api/v1/agent/keys", ACCESS_LEVELS, NO_DEVICE_TOKEN;
|
|
8846
8951
|
var init_keys = __esm(() => {
|
|
8847
8952
|
init_agent_key_access();
|
|
8848
8953
|
init_args();
|
|
@@ -8850,6 +8955,11 @@ var init_keys = __esm(() => {
|
|
|
8850
8955
|
init_profiles();
|
|
8851
8956
|
init_render();
|
|
8852
8957
|
init_secret_store();
|
|
8958
|
+
ACCESS_LEVELS = {
|
|
8959
|
+
read: "read",
|
|
8960
|
+
"read-write": "readwrite",
|
|
8961
|
+
"read-write-transfer": "readwritetransfer"
|
|
8962
|
+
};
|
|
8853
8963
|
NO_DEVICE_TOKEN = {
|
|
8854
8964
|
code: "NO_DEVICE_TOKEN",
|
|
8855
8965
|
message: "No device token available.",
|
|
@@ -13707,11 +13817,13 @@ async function listTradingWallets(ctx, key, scope) {
|
|
|
13707
13817
|
const rows = [];
|
|
13708
13818
|
const cursors = new Set;
|
|
13709
13819
|
let appId = "";
|
|
13820
|
+
let scopes = [];
|
|
13710
13821
|
for (;; ) {
|
|
13711
13822
|
const response = walletPageSchema.parse(await request(ctx, key, `/api/v1/agent/wallets/trading${cursor ? `?cursor=${encodeURIComponent(cursor)}` : ""}`));
|
|
13712
13823
|
if (scope !== undefined && !response.scopes?.includes(scope))
|
|
13713
13824
|
throw new TradingError("SCOPE_MISSING", `The bound key needs ${scope}.`);
|
|
13714
13825
|
appId = response.privyAppId ?? "";
|
|
13826
|
+
scopes = response.scopes;
|
|
13715
13827
|
if (!Array.isArray(response.page))
|
|
13716
13828
|
throw new TradingError("INVALID_RESPONSE", "Wallet discovery did not return a page.");
|
|
13717
13829
|
rows.push(...response.page);
|
|
@@ -13722,7 +13834,7 @@ async function listTradingWallets(ctx, key, scope) {
|
|
|
13722
13834
|
throw new TradingError("INVALID_RESPONSE", "Wallet discovery did not complete.");
|
|
13723
13835
|
cursors.add(cursor);
|
|
13724
13836
|
}
|
|
13725
|
-
return { rows, appId };
|
|
13837
|
+
return { rows, appId, scopes };
|
|
13726
13838
|
}
|
|
13727
13839
|
function matchesName(row, name) {
|
|
13728
13840
|
return row.id === name || row.address === name || row.label === name;
|
|
@@ -13759,25 +13871,24 @@ async function tradingWallet(ctx, key, name, scope) {
|
|
|
13759
13871
|
}
|
|
13760
13872
|
return await completeTradingWallet(ctx, matches[0], appId, scope);
|
|
13761
13873
|
}
|
|
13762
|
-
async function tradingPayer(ctx, key, name) {
|
|
13763
|
-
const
|
|
13764
|
-
const { rows, appId } = await listTradingWallets(ctx, key, scope);
|
|
13874
|
+
async function tradingPayer(ctx, key, name, scope = "swap:write") {
|
|
13875
|
+
const { rows, appId, scopes } = await listTradingWallets(ctx, key, scope);
|
|
13765
13876
|
const embedded = embeddedSchema.parse(await request(ctx, key, "/api/v1/agent/wallets/embedded")).wallets?.solana?.address;
|
|
13766
|
-
const asEmbedded = () => ({ kind: "embedded", address: embedded });
|
|
13877
|
+
const asEmbedded = () => ({ kind: "embedded", address: embedded, scopes });
|
|
13767
13878
|
const options = [
|
|
13768
13879
|
...rows.map((row) => `TEE ${describeWallet(row)}`),
|
|
13769
13880
|
...embedded ? [`embedded (${embedded})`] : []
|
|
13770
13881
|
].join("; ");
|
|
13771
13882
|
if (name === undefined) {
|
|
13772
13883
|
if (rows.length === 1 && !embedded)
|
|
13773
|
-
return { kind: "tee", wallet: await completeTradingWallet(ctx, rows[0], appId, scope) };
|
|
13884
|
+
return { kind: "tee", wallet: await completeTradingWallet(ctx, rows[0], appId, scope), scopes };
|
|
13774
13885
|
if (rows.length === 0 && embedded)
|
|
13775
13886
|
return asEmbedded();
|
|
13776
13887
|
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}.`);
|
|
13777
13888
|
}
|
|
13778
13889
|
const matches = rows.filter((row) => matchesName(row, name));
|
|
13779
13890
|
if (matches.length === 1)
|
|
13780
|
-
return { kind: "tee", wallet: await completeTradingWallet(ctx, matches[0], appId, scope) };
|
|
13891
|
+
return { kind: "tee", wallet: await completeTradingWallet(ctx, matches[0], appId, scope), scopes };
|
|
13781
13892
|
if (matches.length === 0 && embedded === name)
|
|
13782
13893
|
return asEmbedded();
|
|
13783
13894
|
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}.`);
|
|
@@ -22179,6 +22290,8 @@ __export(exports_vault_support, {
|
|
|
22179
22290
|
menuSafeText: () => menuSafeText,
|
|
22180
22291
|
findExternalEntry: () => findExternalEntry,
|
|
22181
22292
|
factorMenu: () => factorMenu,
|
|
22293
|
+
factorAddPassphraseOnlyReason: () => factorAddPassphraseOnlyReason,
|
|
22294
|
+
factorAddAmong: () => factorAddAmong,
|
|
22182
22295
|
describeRole: () => describeRole,
|
|
22183
22296
|
describeEntry: () => describeEntry,
|
|
22184
22297
|
derivationNotice: () => derivationNotice,
|
|
@@ -22277,6 +22390,20 @@ async function requireVaultRaw(ctx, resolved) {
|
|
|
22277
22390
|
throw missingVault(ctx, resolved);
|
|
22278
22391
|
return raw;
|
|
22279
22392
|
}
|
|
22393
|
+
function factorAddAmong(envelopes) {
|
|
22394
|
+
return {
|
|
22395
|
+
envelopeIds: envelopes.filter((envelope) => isPassphraseEnvelope(envelope) || isCtap2Envelope(envelope)).map((envelope) => envelope.id),
|
|
22396
|
+
because: factorAddPassphraseOnlyReason,
|
|
22397
|
+
excludedBecause: "adding a factor opens the vault with the passphrase or a security key."
|
|
22398
|
+
};
|
|
22399
|
+
}
|
|
22400
|
+
function factorAddPassphraseOnlyReason(unusable) {
|
|
22401
|
+
if (unusable.length === 0) {
|
|
22402
|
+
return "adding a factor opens the vault with the passphrase or a security key, and this vault has no security key.";
|
|
22403
|
+
}
|
|
22404
|
+
const keys = unusable.map((entry) => `${entry.word} ${entry.id} cannot be used on this machine: ${entry.availability}`);
|
|
22405
|
+
return `adding a factor opens the vault with the passphrase or a security key, and ${keys.join("; ")}.`;
|
|
22406
|
+
}
|
|
22280
22407
|
function derivationNotice(line, purpose) {
|
|
22281
22408
|
if (purpose === undefined)
|
|
22282
22409
|
return line;
|
|
@@ -22303,6 +22430,9 @@ async function unlockInteractively(ctx, path, raw, opts = {}) {
|
|
|
22303
22430
|
const noticeFor = (purpose) => (line) => deps.stderr.write(derivationNotice(line, purpose));
|
|
22304
22431
|
const notice = noticeFor(opts.purpose);
|
|
22305
22432
|
if (choice.kind === "passphrase") {
|
|
22433
|
+
if (opts.onFactorChosen) {
|
|
22434
|
+
await opts.onFactorChosen(choice.envelopeId !== undefined ? { kind: "passphrase", envelopeId: choice.envelopeId } : { kind: "passphrase" });
|
|
22435
|
+
}
|
|
22306
22436
|
if (choice.onlyBecause !== undefined)
|
|
22307
22437
|
deps.stderr.write(`${PASSPHRASE_ONLY_PREFIX}${choice.onlyBecause}
|
|
22308
22438
|
`);
|
|
@@ -22324,6 +22454,8 @@ async function unlockInteractively(ctx, path, raw, opts = {}) {
|
|
|
22324
22454
|
}
|
|
22325
22455
|
if (choice.kind === "touch-id") {
|
|
22326
22456
|
const envelope2 = choice.envelope;
|
|
22457
|
+
if (opts.onFactorChosen)
|
|
22458
|
+
await opts.onFactorChosen({ kind: "touch-id", envelope: envelope2 });
|
|
22327
22459
|
const session2 = await openEnclaveSession(deps, envelope2.helper);
|
|
22328
22460
|
const open5 = async (p, r, reason2) => {
|
|
22329
22461
|
const current = parseVaultFile(r);
|
|
@@ -22352,6 +22484,8 @@ async function unlockInteractively(ctx, path, raw, opts = {}) {
|
|
|
22352
22484
|
}
|
|
22353
22485
|
if (choice.kind === "passkey") {
|
|
22354
22486
|
const envelope2 = choice.envelope;
|
|
22487
|
+
if (opts.onFactorChosen)
|
|
22488
|
+
await opts.onFactorChosen({ kind: "passkey", envelope: envelope2 });
|
|
22355
22489
|
const session2 = await openPasskeySession(deps, envelope2.helper);
|
|
22356
22490
|
const open5 = async (p, r, purpose) => {
|
|
22357
22491
|
const current = parseVaultFile(r);
|
|
@@ -22379,11 +22513,14 @@ async function unlockInteractively(ctx, path, raw, opts = {}) {
|
|
|
22379
22513
|
};
|
|
22380
22514
|
}
|
|
22381
22515
|
const envelope = choice.envelope;
|
|
22516
|
+
const keyDevice = opts.onFactorChosen ? await opts.onFactorChosen({ kind: "security-key", envelope }) : undefined;
|
|
22382
22517
|
const session = await openSecurityKeySession(deps, {
|
|
22383
22518
|
vaultId: file.vaultId,
|
|
22384
22519
|
envelopeId: envelope.id,
|
|
22385
|
-
|
|
22386
|
-
|
|
22520
|
+
...keyDevice !== undefined ? { preferDevice: keyDevice.preferDevice } : {
|
|
22521
|
+
deviceFlag: ctx.vaultDevice,
|
|
22522
|
+
...choice.preferDevice !== undefined ? { preferDevice: choice.preferDevice } : {}
|
|
22523
|
+
},
|
|
22387
22524
|
requireFeatures: false
|
|
22388
22525
|
});
|
|
22389
22526
|
const open4 = async (p, r) => {
|
|
@@ -22477,7 +22614,7 @@ async function chooseFactor(ctx, vaultId, allEnvelopes, facts, requested, opts =
|
|
|
22477
22614
|
}
|
|
22478
22615
|
if (passphrases.length === 0 && drivable.length === 1)
|
|
22479
22616
|
return choiceFor(drivable[0]);
|
|
22480
|
-
const attached = ctx.deps.isTTY.stdin ? await probeAttachedKeys(ctx.deps, { vaultId, envelopes: drivableKeys, deviceFlag: ctx.vaultDevice }) : undefined;
|
|
22617
|
+
const attached = "attached" in opts ? opts.attached : ctx.deps.isTTY.stdin ? await probeAttachedKeys(ctx.deps, { vaultId, envelopes: drivableKeys, deviceFlag: ctx.vaultDevice }) : undefined;
|
|
22481
22618
|
const notUsableHere = envelopes.filter((envelope) => !isPassphraseEnvelope(envelope) && !drivable.includes(envelope));
|
|
22482
22619
|
const menu = factorMenu({
|
|
22483
22620
|
keys: drivableKeys,
|
|
@@ -38715,7 +38852,7 @@ init_render();
|
|
|
38715
38852
|
init_secret_store();
|
|
38716
38853
|
|
|
38717
38854
|
// src/version.ts
|
|
38718
|
-
var CLI_VERSION = "0.11.
|
|
38855
|
+
var CLI_VERSION = "0.11.8";
|
|
38719
38856
|
|
|
38720
38857
|
// src/commands/auth.ts
|
|
38721
38858
|
var DEVICE_CODE_PATH = "/api/v1/agent/device/code";
|
|
@@ -38737,6 +38874,10 @@ async function authLogin(args, ctx) {
|
|
|
38737
38874
|
return 2;
|
|
38738
38875
|
}
|
|
38739
38876
|
const scopes = parsed.values["--scopes"] ? parseScopesList(parsed.values["--scopes"]) : undefined;
|
|
38877
|
+
if (scopes?.includes("transfer:bound")) {
|
|
38878
|
+
writeUsageFailure(deps, "transfer:bound is not available on a device login. Mint a Read:Write:Transfer key with: candle keys create --access read-write-transfer", json);
|
|
38879
|
+
return 2;
|
|
38880
|
+
}
|
|
38740
38881
|
const label = parsed.values["--label"]?.trim();
|
|
38741
38882
|
const noBrowser = parsed.booleans.has("--no-browser");
|
|
38742
38883
|
if (parsed.values["--label"] !== undefined && (label === undefined || label.length < 1 || label.length > MAX_CLIENT_NAME_LENGTH)) {
|
|
@@ -39253,6 +39394,32 @@ var HELP = {
|
|
|
39253
39394
|
],
|
|
39254
39395
|
env: ENV_API
|
|
39255
39396
|
},
|
|
39397
|
+
transfer: {
|
|
39398
|
+
group: "Trade",
|
|
39399
|
+
summary: "Move funds out of a TEE wallet to your own wallets or its vault, through its bound key",
|
|
39400
|
+
description: "Moves one asset out of a TEE wallet this machine can sign for. The bound key must be a Read:Write:Transfer key (transfer:bound); from that wallet, Candle allows only its own pinned vault or another of the account's wallets you linked while signed in or marked trusted. To a linked wallet the amount counts against the key's spend caps and must be a base asset; to the vault any token and max are allowed. Candle builds the transaction, this machine approves the relay, Privy signs, Candle broadcasts. The destination and its kind are shown before you confirm.",
|
|
39401
|
+
usage: [
|
|
39402
|
+
"candle transfer --to <address|wallet name|vault> --asset <SOL|USDC|CNDL>|--mint <mint> --amount <decimal|max> [--wallet <tee>] [--yes] [--json]"
|
|
39403
|
+
],
|
|
39404
|
+
rows: [],
|
|
39405
|
+
flags: [
|
|
39406
|
+
{
|
|
39407
|
+
invocation: "--to <address|wallet name|vault>",
|
|
39408
|
+
description: "Where the funds go: vault is the wallet's own pin"
|
|
39409
|
+
},
|
|
39410
|
+
{ invocation: "--asset <SOL|USDC|CNDL>", description: "A base asset (required for a linked-wallet destination)" },
|
|
39411
|
+
{ invocation: "--mint <mint>", description: "Any Solana mint, instead of --asset (vault destinations only)" },
|
|
39412
|
+
{ invocation: "--amount <decimal|max>", description: "How much, or max for the whole spendable balance" },
|
|
39413
|
+
{ invocation: "--wallet <tee>", description: "The TEE wallet the funds leave; optional with one payer" },
|
|
39414
|
+
{ invocation: "--rpc-url <url>", description: "Your own Solana RPC, to read a --mint's decimals" },
|
|
39415
|
+
{ invocation: "--yes", description: "Skip the confirmation prompt (the destination is still printed)" }
|
|
39416
|
+
],
|
|
39417
|
+
examples: [
|
|
39418
|
+
"candle transfer --to vault --asset SOL --amount max --wallet AgentOne",
|
|
39419
|
+
"candle transfer --to treasury --asset USDC --amount 250 --wallet AgentOne --yes --json"
|
|
39420
|
+
],
|
|
39421
|
+
env: ENV_API
|
|
39422
|
+
},
|
|
39256
39423
|
launch: {
|
|
39257
39424
|
group: "Trade",
|
|
39258
39425
|
summary: "Create a Solana token (the first buy is a separate swap)",
|
|
@@ -39301,11 +39468,11 @@ var HELP = {
|
|
|
39301
39468
|
rows: [
|
|
39302
39469
|
{
|
|
39303
39470
|
invocation: "list [--scopes]",
|
|
39304
|
-
description: "List API keys: name and Read or Read:Write access; --scopes adds the raw scopes"
|
|
39471
|
+
description: "List API keys: name and Read, Read:Write or Read:Write:Transfer access; --scopes adds the raw scopes"
|
|
39305
39472
|
},
|
|
39306
39473
|
{
|
|
39307
|
-
invocation: "create [--scopes <a,b,c>] [--label <name>] [--expires-in <days>] [--tx-limit <usd> [--reset daily|weekly|monthly|never]]",
|
|
39308
|
-
description: "Create an API key"
|
|
39474
|
+
invocation: "create [--access read|read-write|read-write-transfer | --scopes <a,b,c>] [--label <name>] [--expires-in <days>] [--tx-limit <usd> [--reset daily|weekly|monthly|never]]",
|
|
39475
|
+
description: "Create an API key; --access mints one of the three levels (read-write-transfer can move funds out of the wallet it runs)"
|
|
39309
39476
|
},
|
|
39310
39477
|
{ invocation: "revoke <prefix>", description: "Revoke an API key" },
|
|
39311
39478
|
{ invocation: "wallets <prefix>", description: "Wallets an agent profile can use" },
|
|
@@ -39317,6 +39484,7 @@ var HELP = {
|
|
|
39317
39484
|
],
|
|
39318
39485
|
examples: [
|
|
39319
39486
|
"candle keys list",
|
|
39487
|
+
"candle keys create --access read-write-transfer --label rebalancer",
|
|
39320
39488
|
"candle keys create --scopes trade:write --label agent-one",
|
|
39321
39489
|
"candle keys wallets ck_live_ab12",
|
|
39322
39490
|
"candle keys revoke ck_live_ab12"
|
|
@@ -39327,19 +39495,28 @@ var HELP = {
|
|
|
39327
39495
|
group: "Account",
|
|
39328
39496
|
display: "wallet",
|
|
39329
39497
|
summary: "Launch and linked wallets; import or revoke one (wallets is an alias)",
|
|
39330
|
-
description: "The account's embedded launch wallets and any wallet you linked, with a Signer column saying whether this machine holds the signing key. Keys are derived in the vault now: this command links and revokes, it never generates or prints one.",
|
|
39498
|
+
description: "The account's embedded launch wallets and any wallet you linked, with a Signer column saying whether this machine holds the signing key and a Trusted column saying whether it is yours: linked while signed in, or marked with wallet trust. Keys are derived in the vault now: this command links and revokes, it never generates or prints one.",
|
|
39331
39499
|
usage: ["candle wallet [flags]", "candle wallet <subcommand> [flags]"],
|
|
39332
39500
|
rows: [
|
|
39333
39501
|
{
|
|
39334
39502
|
invocation: "import --chain <solana|evm> [options]",
|
|
39335
39503
|
description: "Import a wallet you own (key via --key-file or hidden prompt)"
|
|
39336
39504
|
},
|
|
39337
|
-
{ invocation: "revoke <wallet-id>", description: "Revoke a linked wallet" }
|
|
39505
|
+
{ invocation: "revoke <wallet-id>", description: "Revoke a linked wallet" },
|
|
39506
|
+
{
|
|
39507
|
+
invocation: "trust <label|address|id|prefix*>...",
|
|
39508
|
+
description: "Mark linked wallets yours, so agents can move funds into them (owner only; typed confirm)"
|
|
39509
|
+
},
|
|
39510
|
+
{
|
|
39511
|
+
invocation: "untrust <label|address|id|prefix*>... [--yes]",
|
|
39512
|
+
description: "Clear the mark; moving funds in needs the withdrawal allowlist again (owner only)"
|
|
39513
|
+
}
|
|
39338
39514
|
],
|
|
39339
39515
|
examples: [
|
|
39340
39516
|
"candle wallet",
|
|
39341
39517
|
"candle wallet import --chain solana --key-file ./signer.json",
|
|
39342
|
-
"candle wallet revoke wal_123"
|
|
39518
|
+
"candle wallet revoke wal_123",
|
|
39519
|
+
"candle wallet trust 'tr-*' 'dest-*'"
|
|
39343
39520
|
],
|
|
39344
39521
|
env: ENV_API
|
|
39345
39522
|
},
|
|
@@ -39443,7 +39620,10 @@ var HELP = {
|
|
|
39443
39620
|
invocation: "--factor <id|kind>",
|
|
39444
39621
|
description: "Unlock with this envelope: an id from factor list, or passphrase, security-key, touch-id, passkey; without it, a terminal lists the factors to pick by number"
|
|
39445
39622
|
},
|
|
39446
|
-
{
|
|
39623
|
+
{
|
|
39624
|
+
invocation: "--device <id>",
|
|
39625
|
+
description: "The security key to use when more than one is attached; on factor add security-key, the key to add (the vault opens with the passphrase or another key)"
|
|
39626
|
+
},
|
|
39447
39627
|
{
|
|
39448
39628
|
invocation: "--balances",
|
|
39449
39629
|
description: "list: SOL per matched key, read from your RPC (one request per 100 matched keys). Every matched address goes to that one endpoint together, which links them; tokens are never read"
|
|
@@ -50465,6 +50645,11 @@ init_deps();
|
|
|
50465
50645
|
init_profiles();
|
|
50466
50646
|
init_render();
|
|
50467
50647
|
init_secret_store();
|
|
50648
|
+
function trustedCell(row) {
|
|
50649
|
+
if (row.revokedAt)
|
|
50650
|
+
return "-";
|
|
50651
|
+
return row.addedVia === "session" || typeof row.trustedAt === "number" ? "yes" : "no";
|
|
50652
|
+
}
|
|
50468
50653
|
var TEE_PROFILES = new Set(["ember-tee", "ember-hot"]);
|
|
50469
50654
|
function isTeeRow(row) {
|
|
50470
50655
|
return row.profile !== undefined && TEE_PROFILES.has(row.profile);
|
|
@@ -50515,6 +50700,9 @@ var NONE_HINT = `A wallet marked none has no signer on this machine, so a trade
|
|
|
50515
50700
|
var TEE_HINT = `A wallet marked tee is a TEE trading wallet: pass its id, address or label to candle swap --wallet.
|
|
50516
50701
|
` + `This account's embedded wallet, shown above, can pay for a token trade too.
|
|
50517
50702
|
`;
|
|
50703
|
+
var UNTRUSTED_HINT = `A wallet marked Trusted no was linked by an API key: agents can move funds into it only once you mark it.
|
|
50704
|
+
` + `Run: candle wallets trust <label|address|prefix*> (the device token, never an API key)
|
|
50705
|
+
`;
|
|
50518
50706
|
var STALE_HINT = `A wallet marked stale is revoked but its signer is still stored here. Run: candle wallets revoke <id>
|
|
50519
50707
|
`;
|
|
50520
50708
|
async function probeSignerStates(rows, store) {
|
|
@@ -50621,24 +50809,28 @@ Linked wallets (${linkedRows.length}):
|
|
|
50621
50809
|
`);
|
|
50622
50810
|
} else {
|
|
50623
50811
|
const cells = linkedRows.map((wallet) => signerCell(signerStates.get(wallet._id), wallet));
|
|
50624
|
-
deps.stdout.write(`${renderTable(["Id", "Wallet", "Address", "Label", "Kind", "Revoked", "Signer"], linkedRows.map((wallet, index) => [
|
|
50812
|
+
deps.stdout.write(`${renderTable(["Id", "Wallet", "Address", "Label", "Kind", "Revoked", "Trusted", "Signer"], linkedRows.map((wallet, index) => [
|
|
50625
50813
|
wallet._id,
|
|
50626
50814
|
wallet.chain,
|
|
50627
50815
|
wallet.address,
|
|
50628
50816
|
wallet.label ?? "-",
|
|
50629
50817
|
isTeeRow(wallet) ? "tee" : "linked",
|
|
50630
50818
|
wallet.revokedAt ? "yes" : "no",
|
|
50819
|
+
trustedCell(wallet),
|
|
50631
50820
|
cells[index] ?? "-"
|
|
50632
50821
|
]))}
|
|
50633
50822
|
`);
|
|
50634
50823
|
const anyNone = cells.includes("none");
|
|
50635
50824
|
const anyStale = cells.includes("stale");
|
|
50636
50825
|
const anyTee = linkedRows.some(isTeeRow);
|
|
50637
|
-
|
|
50826
|
+
const anyUntrusted = linkedRows.some((row) => trustedCell(row) === "no");
|
|
50827
|
+
if (anyNone || anyStale || anyTee || anyUntrusted)
|
|
50638
50828
|
deps.stdout.write(`
|
|
50639
50829
|
`);
|
|
50640
50830
|
if (anyTee)
|
|
50641
50831
|
deps.stdout.write(TEE_HINT);
|
|
50832
|
+
if (anyUntrusted)
|
|
50833
|
+
deps.stdout.write(UNTRUSTED_HINT);
|
|
50642
50834
|
if (anyNone)
|
|
50643
50835
|
deps.stdout.write(NONE_HINT);
|
|
50644
50836
|
if (anyStale)
|
|
@@ -52834,6 +53026,150 @@ async function teeRebinds(args, ctx) {
|
|
|
52834
53026
|
return 0;
|
|
52835
53027
|
}
|
|
52836
53028
|
|
|
53029
|
+
// src/commands/transfer.ts
|
|
53030
|
+
init_args();
|
|
53031
|
+
init_render();
|
|
53032
|
+
init_trading();
|
|
53033
|
+
var USAGE2 = "Usage: candle transfer --to <address|wallet name|vault> --asset <SOL|USDC|CNDL>|--mint <mint> --amount <decimal|max> [--wallet <name>] [--rpc-url <url>] [--yes] [--json]";
|
|
53034
|
+
var TRANSFER_ASSETS = Object.keys(BASES);
|
|
53035
|
+
var BASE58_ADDRESS = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
53036
|
+
async function readLinkedWallets(ctx, key) {
|
|
53037
|
+
const rows = [];
|
|
53038
|
+
let cursor = null;
|
|
53039
|
+
const seen = new Set;
|
|
53040
|
+
for (let page = 0;page < 25; page++) {
|
|
53041
|
+
const query = cursor === null ? "?limit=100" : `?limit=100&cursor=${encodeURIComponent(cursor)}`;
|
|
53042
|
+
const body = await request(ctx, key, `/api/v1/agent/wallets${query}`);
|
|
53043
|
+
if (!Array.isArray(body.page))
|
|
53044
|
+
throw new TradingError("INVALID_RESPONSE", "Wallet discovery did not return a page.");
|
|
53045
|
+
rows.push(...body.page);
|
|
53046
|
+
if (body.isDone !== false || typeof body.continueCursor !== "string" || seen.has(body.continueCursor))
|
|
53047
|
+
return rows;
|
|
53048
|
+
seen.add(body.continueCursor);
|
|
53049
|
+
cursor = body.continueCursor;
|
|
53050
|
+
}
|
|
53051
|
+
return rows;
|
|
53052
|
+
}
|
|
53053
|
+
async function resolveDestination(ctx, key, source, to) {
|
|
53054
|
+
if (to === "vault") {
|
|
53055
|
+
const lifecycle = await request(ctx, key, `/api/v1/agent/wallets/${encodeURIComponent(source.id)}/lifecycle`);
|
|
53056
|
+
const vault = lifecycle.vaultDestination;
|
|
53057
|
+
if (typeof vault !== "string" || vault.length === 0)
|
|
53058
|
+
throw new TradingError("VAULT_NOT_PINNED", "This wallet has no pinned vault recorded on Candle, so --to vault names nothing. Pass the address instead.");
|
|
53059
|
+
return { kind: "vault", address: vault };
|
|
53060
|
+
}
|
|
53061
|
+
const rows = (await readLinkedWallets(ctx, key)).filter((row) => row.chain === "solana" && row.revokedAt === undefined && row.address !== source.address);
|
|
53062
|
+
const matches = rows.filter((row) => row.label === to || row._id === to || row.address === to);
|
|
53063
|
+
if (matches.length > 1)
|
|
53064
|
+
throw new TradingError("DESTINATION_AMBIGUOUS", `"${to}" names ${matches.length} linked wallets: ${matches.map((row) => `${row.label ?? ""} (${row._id}, ${row.address})`.trim()).join("; ")}. Name one by id or address.`);
|
|
53065
|
+
const match = matches[0];
|
|
53066
|
+
if (match)
|
|
53067
|
+
return { kind: "linked", address: match.address, id: match._id, ...match.label ? { label: match.label } : {} };
|
|
53068
|
+
if (to === source.address)
|
|
53069
|
+
throw new TradingError("DESTINATION_IS_SOURCE", "--to names the wallet the funds would leave. Name a different destination.");
|
|
53070
|
+
if (BASE58_ADDRESS.test(to))
|
|
53071
|
+
return { kind: "address", address: to };
|
|
53072
|
+
throw new TradingError("DESTINATION_UNKNOWN", rows.length === 0 ? `"${to}" is not vault, a Solana address, or a linked wallet on this account (it has no other active Solana linked wallets).` : `"${to}" is not vault, a Solana address, or a linked wallet on this account. Linked wallets: ${rows.map((row) => `${row.label ?? ""} (${row._id}, ${row.address})`.trim()).join("; ")}.`);
|
|
53073
|
+
}
|
|
53074
|
+
function describeDestination(destination) {
|
|
53075
|
+
switch (destination.kind) {
|
|
53076
|
+
case "vault":
|
|
53077
|
+
return `this wallet's pinned vault ${destination.address}`;
|
|
53078
|
+
case "linked":
|
|
53079
|
+
return `linked wallet ${destination.label ? `${destination.label} ` : ""}(${destination.id}, ${destination.address})`;
|
|
53080
|
+
case "address":
|
|
53081
|
+
return `address ${destination.address} (not one of this account's linked wallets; Candle decides whether it is allowed)`;
|
|
53082
|
+
}
|
|
53083
|
+
}
|
|
53084
|
+
async function confirmTransfer(ctx, lines, yes) {
|
|
53085
|
+
const output = ctx.json ? ctx.deps.stderr : ctx.deps.stdout;
|
|
53086
|
+
for (const line of lines)
|
|
53087
|
+
output.write(`${safeText(line)}
|
|
53088
|
+
`);
|
|
53089
|
+
if (yes)
|
|
53090
|
+
return true;
|
|
53091
|
+
if (!ctx.deps.isTTY.stdin)
|
|
53092
|
+
throw new TradingError("CONFIRMATION_REQUIRED", "Run interactively to confirm, or use --yes for an ordinary transfer prompt.");
|
|
53093
|
+
return (await ctx.deps.promptLine("Proceed? [y/N] ")).trim().toLowerCase() === "y";
|
|
53094
|
+
}
|
|
53095
|
+
async function transfer(args, ctx) {
|
|
53096
|
+
const parsed = parseArgs(args, {
|
|
53097
|
+
valueFlags: ["--wallet", "--to", "--asset", "--mint", "--amount", "--rpc-url"],
|
|
53098
|
+
booleanFlags: ["--yes"]
|
|
53099
|
+
});
|
|
53100
|
+
if ("error" in parsed) {
|
|
53101
|
+
writeUsageFailure(ctx.deps, parsed.error, ctx.json);
|
|
53102
|
+
return 2;
|
|
53103
|
+
}
|
|
53104
|
+
const flags = parsed.values;
|
|
53105
|
+
const asset = flags["--asset"]?.toUpperCase();
|
|
53106
|
+
if (parsed.positionals.length !== 0 || !flags["--to"] || !flags["--amount"] || Boolean(flags["--asset"]) === Boolean(flags["--mint"]) || asset !== undefined && !TRANSFER_ASSETS.includes(asset) || flags["--mint"] !== undefined && !BASE58_ADDRESS.test(flags["--mint"])) {
|
|
53107
|
+
writeUsageFailure(ctx.deps, USAGE2, ctx.json);
|
|
53108
|
+
return 2;
|
|
53109
|
+
}
|
|
53110
|
+
const to = flags["--to"];
|
|
53111
|
+
const isMax = flags["--amount"] === "max";
|
|
53112
|
+
try {
|
|
53113
|
+
if (!isMax)
|
|
53114
|
+
rawAmount(flags["--amount"], 18);
|
|
53115
|
+
const key = await tradingKey(ctx);
|
|
53116
|
+
const payer = await tradingPayer(ctx, key, flags["--wallet"], "transfer:write");
|
|
53117
|
+
if (payer.kind === "embedded")
|
|
53118
|
+
throw new TradingError("PAYER_UNSUPPORTED", "This command moves a linked wallet this machine can sign for. The embedded wallet transfers through the agent transfer rail (MCP candle_transfer), not from here. Name a TEE wallet with --wallet.");
|
|
53119
|
+
if (!payer.scopes.includes("transfer:bound"))
|
|
53120
|
+
throw new TradingError("SCOPE_MISSING", "Moving funds out of a TEE wallet needs a Read:Write:Transfer key. Mint one with: candle keys create --access read-write-transfer, then bind the wallet to it with: candle tee rebind");
|
|
53121
|
+
const wallet = payer.wallet;
|
|
53122
|
+
const destination = await resolveDestination(ctx, key, wallet, to);
|
|
53123
|
+
const label = asset ?? flags["--mint"];
|
|
53124
|
+
let amountRaw;
|
|
53125
|
+
if (isMax)
|
|
53126
|
+
amountRaw = "max";
|
|
53127
|
+
else {
|
|
53128
|
+
const decimals = asset ? BASES[asset]?.decimals ?? 9 : await decimalsFor(ctx, flags["--mint"], flags["--rpc-url"]);
|
|
53129
|
+
amountRaw = rawAmount(flags["--amount"], decimals);
|
|
53130
|
+
}
|
|
53131
|
+
const amountText = isMax ? `the full spendable balance of ${label}` : `${flags["--amount"]} ${label}`;
|
|
53132
|
+
const confirmed = await confirmTransfer(ctx, [
|
|
53133
|
+
`Transfer ${amountText} from ${wallet.address} (${wallet.id}) to ${describeDestination(destination)}`,
|
|
53134
|
+
destination.kind === "vault" ? "Destination: this wallet's own vault, pinned when it was imported." : destination.kind === "linked" ? "Destination: a linked wallet on this account. Candle allows it only if you linked it while signed in or marked it trusted, and the amount counts against this key's spend caps." : "Destination: an address Candle will classify at build; from a TEE wallet only its vault or a trusted linked wallet is allowed."
|
|
53135
|
+
], parsed.booleans.has("--yes"));
|
|
53136
|
+
if (!confirmed)
|
|
53137
|
+
return printTradingResult(ctx, { success: true, status: "cancelled", walletId: wallet.id, destination });
|
|
53138
|
+
const built = await request(ctx, key, "/api/v1/agent/transfer/build", {
|
|
53139
|
+
walletId: wallet.id,
|
|
53140
|
+
chain: "solana",
|
|
53141
|
+
...asset ? { asset } : { mint: flags["--mint"] },
|
|
53142
|
+
amountRaw,
|
|
53143
|
+
to: destination.address
|
|
53144
|
+
});
|
|
53145
|
+
const transferId = built.transferId;
|
|
53146
|
+
const unsigned = built.unsignedTransactionsBase64;
|
|
53147
|
+
if (typeof transferId !== "string" || !Array.isArray(unsigned) || unsigned.length !== 1 || typeof unsigned[0] !== "string")
|
|
53148
|
+
throw new TradingError("INVALID_RESPONSE", "Candle did not return one unsigned transfer transaction; nothing was signed.");
|
|
53149
|
+
if (typeof built.payerAddress === "string" && built.payerAddress !== wallet.address)
|
|
53150
|
+
throw new TradingError("INVALID_RESPONSE", "The transfer build does not name the requested payer; nothing was signed.");
|
|
53151
|
+
const builtAmount = typeof built.amountRaw === "string" ? built.amountRaw : amountRaw;
|
|
53152
|
+
ctx.deps.stderr.write(`Built transfer ${transferId}: ${asset ? `${decimalAmount(builtAmount, BASES[asset]?.decimals ?? 0)} ${asset}` : `${builtAmount} raw units of ${label}`}${typeof built.destinationKind === "string" ? ` to ${built.destinationKind === "vault" ? "the vault" : "a linked wallet"}` : ""}.
|
|
53153
|
+
`);
|
|
53154
|
+
const signed = await relaySign(ctx, key, wallet, unsigned[0]);
|
|
53155
|
+
const result = await request(ctx, key, "/api/v1/agent/transfer/submit", {
|
|
53156
|
+
transferId,
|
|
53157
|
+
signedTransactionsBase64: [signed]
|
|
53158
|
+
});
|
|
53159
|
+
const receipt = {
|
|
53160
|
+
...result,
|
|
53161
|
+
transferId,
|
|
53162
|
+
walletId: wallet.id,
|
|
53163
|
+
wallet: safeText(wallet.address),
|
|
53164
|
+
destination,
|
|
53165
|
+
...typeof built.destinationKind === "string" ? { destinationKind: built.destinationKind } : {}
|
|
53166
|
+
};
|
|
53167
|
+
return printTradingResult(ctx, receipt);
|
|
53168
|
+
} catch (error) {
|
|
53169
|
+
return tradingFailure(ctx, error);
|
|
53170
|
+
}
|
|
53171
|
+
}
|
|
53172
|
+
|
|
52837
53173
|
// src/commands/update.ts
|
|
52838
53174
|
init_args();
|
|
52839
53175
|
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
@@ -54406,10 +54742,15 @@ async function addPasskeyFactor(ctx, parsed, resolvedVault, hold) {
|
|
|
54406
54742
|
const opened = await unlockInteractively(ctx, path, raw, {
|
|
54407
54743
|
acceptOlderCopy: parsed.booleans.has("--accept-older-copy"),
|
|
54408
54744
|
promptText: "Current vault passphrase, to unlock (input hidden): ",
|
|
54409
|
-
|
|
54410
|
-
passphraseOnlyBecause: "adding a factor opens the vault with the passphrase. A security key does not authorise enrollment: adding a second key while one is plugged in needs a device-selection rule this command does not have (D10)."
|
|
54745
|
+
among: factorAddAmong(parseVaultFile(raw).envelopes)
|
|
54411
54746
|
});
|
|
54412
54747
|
const vault = hold(opened.vault);
|
|
54748
|
+
const openedWith = {
|
|
54749
|
+
factor: opened.factor.kind === "passphrase" ? "passphrase" : "security-key",
|
|
54750
|
+
envelopeId: opened.factor.envelopeId
|
|
54751
|
+
};
|
|
54752
|
+
if (opened.factor.kind === "security-key")
|
|
54753
|
+
opened.factor.session.pin = undefined;
|
|
54413
54754
|
const vaultId = vault.file.vaultId;
|
|
54414
54755
|
let registered;
|
|
54415
54756
|
try {
|
|
@@ -54507,7 +54848,8 @@ async function addPasskeyFactor(ctx, parsed, resolvedVault, hold) {
|
|
|
54507
54848
|
saltDerivation: "platform",
|
|
54508
54849
|
helper: envelope.helper,
|
|
54509
54850
|
recoverableFactors: recoverable,
|
|
54510
|
-
verified: true
|
|
54851
|
+
verified: true,
|
|
54852
|
+
openedWith
|
|
54511
54853
|
});
|
|
54512
54854
|
return 0;
|
|
54513
54855
|
}
|
|
@@ -54530,6 +54872,7 @@ ${PASSKEY_NOTE}
|
|
|
54530
54872
|
}
|
|
54531
54873
|
|
|
54532
54874
|
// src/commands/vault-factor-security-key.ts
|
|
54875
|
+
init_esm();
|
|
54533
54876
|
init_crypto();
|
|
54534
54877
|
init_errors();
|
|
54535
54878
|
init_fido2();
|
|
@@ -54653,20 +54996,84 @@ async function addSecurityKeyFactor(ctx, parsed, resolvedVault, hold) {
|
|
|
54653
54996
|
const raw = await requireVaultRaw(ctx, resolvedVault);
|
|
54654
54997
|
const file = parseVaultFile(raw);
|
|
54655
54998
|
const envelopeId = freshEnvelopeId();
|
|
54656
|
-
const
|
|
54657
|
-
|
|
54658
|
-
|
|
54659
|
-
|
|
54660
|
-
|
|
54661
|
-
|
|
54999
|
+
const vaultId = file.vaultId;
|
|
55000
|
+
const keysInHeader = file.envelopes.filter(isCtap2Envelope);
|
|
55001
|
+
const { helperPath, info } = await listSecurityKeys(deps, { vaultId, envelopeId });
|
|
55002
|
+
assertHelperExcludeList(info, helperPath, keysInHeader.length);
|
|
55003
|
+
const attached = keysInHeader.length > 0 ? await probeAttachedKeys(deps, { vaultId, envelopes: keysInHeader }) : undefined;
|
|
55004
|
+
const excludeCountNow = keysInHeader.length;
|
|
55005
|
+
const addingLine = (device) => deps.stderr.write(`Adding: ${device.product || "security key"} (--device ${device.deviceId}), a different key from the one that opened the vault.
|
|
55006
|
+
`);
|
|
55007
|
+
let session;
|
|
55008
|
+
let enrolling;
|
|
55009
|
+
let deferredAfter;
|
|
54662
55010
|
const opened = await unlockInteractively(ctx, path, raw, {
|
|
54663
55011
|
acceptOlderCopy: parsed.booleans.has("--accept-older-copy"),
|
|
54664
55012
|
promptText: "Current vault passphrase, to unlock (input hidden): ",
|
|
54665
|
-
|
|
54666
|
-
|
|
55013
|
+
among: factorAddAmong(file.envelopes),
|
|
55014
|
+
attached,
|
|
55015
|
+
onFactorChosen: async (chosen) => {
|
|
55016
|
+
if (chosen.kind === "passphrase") {
|
|
55017
|
+
session = await openSecurityKeySession(deps, {
|
|
55018
|
+
vaultId,
|
|
55019
|
+
envelopeId,
|
|
55020
|
+
deviceFlag: ctx.vaultDevice,
|
|
55021
|
+
requireFeatures: true,
|
|
55022
|
+
beforePin: (device) => {
|
|
55023
|
+
refuseIfEnrolled(device, attached);
|
|
55024
|
+
assertExcludeListFits(device, excludeCountNow);
|
|
55025
|
+
}
|
|
55026
|
+
});
|
|
55027
|
+
return;
|
|
55028
|
+
}
|
|
55029
|
+
if (chosen.kind !== "security-key")
|
|
55030
|
+
return;
|
|
55031
|
+
const plan = planDevices(ctx, chosen.envelope, info.devices, attached);
|
|
55032
|
+
if (plan.enrolling !== undefined) {
|
|
55033
|
+
refuseIfEnrolled(plan.enrolling, attached);
|
|
55034
|
+
assertDeviceServesFactor(plan.enrolling);
|
|
55035
|
+
assertExcludeListFits(plan.enrolling, excludeCountNow);
|
|
55036
|
+
enrolling = plan.enrolling;
|
|
55037
|
+
addingLine(plan.enrolling);
|
|
55038
|
+
} else {
|
|
55039
|
+
deferredAfter = plan.opening;
|
|
55040
|
+
}
|
|
55041
|
+
return { preferDevice: plan.opening.deviceId };
|
|
55042
|
+
}
|
|
54667
55043
|
});
|
|
54668
55044
|
const vault = hold(opened.vault);
|
|
54669
|
-
const
|
|
55045
|
+
const openedWith = {
|
|
55046
|
+
factor: opened.factor.kind === "passphrase" ? "passphrase" : "security-key",
|
|
55047
|
+
envelopeId: opened.factor.envelopeId
|
|
55048
|
+
};
|
|
55049
|
+
if (opened.factor.kind === "security-key")
|
|
55050
|
+
opened.factor.session.pin = undefined;
|
|
55051
|
+
const enrolledKeys = vault.file.envelopes.filter(isCtap2Envelope);
|
|
55052
|
+
const excludeCredentialIds = enrolledKeys.map((envelope2) => base64.encode(unb64u(envelope2.credentialId, "credentialId")));
|
|
55053
|
+
if (session === undefined) {
|
|
55054
|
+
if (deferredAfter !== undefined) {
|
|
55055
|
+
await deps.promptLine(`The vault is open. Unplug ${deferredAfter.product || "the security key that opened it"}, insert the security key to add, then press Enter.`);
|
|
55056
|
+
const again = await listSecurityKeys(deps, { vaultId, envelopeId });
|
|
55057
|
+
const probeAgain = enrolledKeys.length > 0 ? await probeAttachedKeys(deps, { vaultId, envelopes: enrolledKeys }) : undefined;
|
|
55058
|
+
const inserted = chooseAfterInsert(ctx, again.info.devices, probeAgain);
|
|
55059
|
+
refuseIfEnrolled(inserted, probeAgain);
|
|
55060
|
+
assertDeviceServesFactor(inserted);
|
|
55061
|
+
assertExcludeListFits(inserted, excludeCredentialIds.length);
|
|
55062
|
+
addingLine(inserted);
|
|
55063
|
+
enrolling = inserted;
|
|
55064
|
+
}
|
|
55065
|
+
if (enrolling === undefined)
|
|
55066
|
+
throw new Error("factor add security-key: no enrolling device was decided");
|
|
55067
|
+
const target = enrolling;
|
|
55068
|
+
session = await openSecurityKeySession(deps, {
|
|
55069
|
+
vaultId,
|
|
55070
|
+
envelopeId,
|
|
55071
|
+
deviceFlag: target.deviceId,
|
|
55072
|
+
requireFeatures: true,
|
|
55073
|
+
pinPrompt: (device) => `PIN for the key being added, ${device.product || "the security key"} (input hidden): `
|
|
55074
|
+
});
|
|
55075
|
+
}
|
|
55076
|
+
const registered = await registerCredential(deps, session, { vaultId, envelopeId, excludeCredentialIds });
|
|
54670
55077
|
if (registered.backupEligible) {
|
|
54671
55078
|
throw new VaultError("VAULT_FACTOR_UNAVAILABLE", `${session.device.product || "This authenticator"} reports the credential it created as backup-eligible (synced), so it is not a hardware-bound security key credential and cannot be recorded as a hardware-token factor.`, {
|
|
54672
55079
|
suggestion: "Nothing was written to the vault. The credential now on the authenticator can be removed with its vendor's tool. Use a hardware security key for this factor."
|
|
@@ -54723,7 +55130,8 @@ async function addSecurityKeyFactor(ctx, parsed, resolvedVault, hold) {
|
|
|
54723
55130
|
backupState: envelope.backupState,
|
|
54724
55131
|
userVerification: "required",
|
|
54725
55132
|
recoverableFactors: recoverable,
|
|
54726
|
-
verified: true
|
|
55133
|
+
verified: true,
|
|
55134
|
+
openedWith
|
|
54727
55135
|
});
|
|
54728
55136
|
return 0;
|
|
54729
55137
|
}
|
|
@@ -54742,6 +55150,76 @@ ${SECURITY_KEY_PAIR_NOTE}
|
|
|
54742
55150
|
`);
|
|
54743
55151
|
return 0;
|
|
54744
55152
|
}
|
|
55153
|
+
function refuseIfEnrolled(device, attached) {
|
|
55154
|
+
const entry = attached?.devices?.find((candidate) => candidate.deviceId === device.deviceId);
|
|
55155
|
+
if (entry === undefined || attached?.credentials === undefined)
|
|
55156
|
+
return;
|
|
55157
|
+
for (const [envelopeId, credentialId] of attached.credentials) {
|
|
55158
|
+
if (entry.present.includes(credentialId))
|
|
55159
|
+
throw alreadyEnrolled(device, envelopeId);
|
|
55160
|
+
}
|
|
55161
|
+
}
|
|
55162
|
+
function planDevices(ctx, opener, devices, attached) {
|
|
55163
|
+
const platform = ctx.deps.platform;
|
|
55164
|
+
if (devices.length === 0) {
|
|
55165
|
+
throw new VaultError("VAULT_FACTOR_UNAVAILABLE", "No security key is attached.", {
|
|
55166
|
+
suggestion: "Plug the key in and run the command again. No other factor is substituted."
|
|
55167
|
+
});
|
|
55168
|
+
}
|
|
55169
|
+
const named = ctx.vaultDevice;
|
|
55170
|
+
const openerCredential = attached?.credentials?.get(opener.id);
|
|
55171
|
+
const holders = openerCredential === undefined ? [] : (attached?.devices ?? []).filter((device) => device.present.includes(openerCredential));
|
|
55172
|
+
let opening;
|
|
55173
|
+
if (holders.length === 1) {
|
|
55174
|
+
opening = devices.find((device) => device.deviceId === holders[0].deviceId);
|
|
55175
|
+
}
|
|
55176
|
+
if (opening === undefined) {
|
|
55177
|
+
const candidates = devices.filter((device) => device.deviceId !== named);
|
|
55178
|
+
if (candidates.length === 1)
|
|
55179
|
+
opening = candidates[0];
|
|
55180
|
+
else if (devices.length === 1)
|
|
55181
|
+
opening = devices[0];
|
|
55182
|
+
}
|
|
55183
|
+
if (opening === undefined) {
|
|
55184
|
+
throw new VaultError("VAULT_AUTHENTICATOR_AMBIGUOUS", `${devices.length} security keys are attached and the one that opens the vault could not be told apart. Leave only the key that opens the vault attached; this command asks for the key to add after the vault is open.`, { suggestion: "Nothing was sent to any key. Run again with only the key that opens the vault attached." });
|
|
55185
|
+
}
|
|
55186
|
+
if (!opening.readable)
|
|
55187
|
+
selectDevice([opening], undefined, platform);
|
|
55188
|
+
const others = devices.filter((device) => device.deviceId !== opening.deviceId);
|
|
55189
|
+
if (named !== undefined) {
|
|
55190
|
+
if (named === opening.deviceId)
|
|
55191
|
+
throw alreadyEnrolled(opening, opener.id);
|
|
55192
|
+
return { opening, enrolling: selectDevice(others, named, platform) };
|
|
55193
|
+
}
|
|
55194
|
+
if (others.length === 1)
|
|
55195
|
+
return { opening, enrolling: selectDevice(others, undefined, platform) };
|
|
55196
|
+
if (others.length > 1) {
|
|
55197
|
+
const listing = others.map((device) => ` ${describeDeviceForList(device)}`).join(`
|
|
55198
|
+
`);
|
|
55199
|
+
throw new VaultError("VAULT_AUTHENTICATOR_AMBIGUOUS", `${others.length} security keys are attached besides the one that opens the vault, and none was named, so nothing was sent to any of them:
|
|
55200
|
+
${listing}`, { suggestion: "Run again with --device <id> naming the key to add." });
|
|
55201
|
+
}
|
|
55202
|
+
return { opening, enrolling: undefined };
|
|
55203
|
+
}
|
|
55204
|
+
function chooseAfterInsert(ctx, devices, probe) {
|
|
55205
|
+
const platform = ctx.deps.platform;
|
|
55206
|
+
if (devices.length === 0) {
|
|
55207
|
+
throw new VaultError("VAULT_FACTOR_UNAVAILABLE", "No security key other than the one that opened the vault is attached. Nothing was written.", { suggestion: "Run the command again with the key to add inserted when asked." });
|
|
55208
|
+
}
|
|
55209
|
+
if (devices.length === 1)
|
|
55210
|
+
return selectDevice(devices, undefined, platform);
|
|
55211
|
+
const holdsVault = (device) => (probe?.devices?.find((entry) => entry.deviceId === device.deviceId)?.present.length ?? 0) > 0;
|
|
55212
|
+
const others = devices.filter((device) => !holdsVault(device));
|
|
55213
|
+
if (others.length === 1)
|
|
55214
|
+
return selectDevice(others, undefined, platform);
|
|
55215
|
+
if (others.length === 0) {
|
|
55216
|
+
throw new VaultError("VAULT_FACTOR_UNAVAILABLE", "Every attached security key already holds one of this vault's credentials, so there is no key to add. Nothing was written.", { suggestion: "Run the command again with a security key this vault does not have inserted when asked." });
|
|
55217
|
+
}
|
|
55218
|
+
const listing = others.map((device) => ` ${describeDeviceForList(device)}`).join(`
|
|
55219
|
+
`);
|
|
55220
|
+
throw new VaultError("VAULT_AUTHENTICATOR_AMBIGUOUS", `${others.length} security keys are attached besides the one that opened the vault, so nothing was sent to any of them:
|
|
55221
|
+
${listing}`, { suggestion: "Run the command again with only the key to add inserted when asked. Nothing was written." });
|
|
55222
|
+
}
|
|
54745
55223
|
|
|
54746
55224
|
// src/commands/vault-factor-touch-id.ts
|
|
54747
55225
|
init_enclave();
|
|
@@ -54780,10 +55258,15 @@ async function addTouchIdFactor(ctx, parsed, resolvedVault, hold) {
|
|
|
54780
55258
|
const opened = await unlockInteractively(ctx, path, raw, {
|
|
54781
55259
|
acceptOlderCopy: parsed.booleans.has("--accept-older-copy"),
|
|
54782
55260
|
promptText: "Current vault passphrase, to unlock (input hidden): ",
|
|
54783
|
-
|
|
54784
|
-
passphraseOnlyBecause: "adding a factor opens the vault with the passphrase. A security key does not authorise enrollment: adding a second key while one is plugged in needs a device-selection rule this command does not have (D10)."
|
|
55261
|
+
among: factorAddAmong(parseVaultFile(raw).envelopes)
|
|
54785
55262
|
});
|
|
54786
55263
|
const vault = hold(opened.vault);
|
|
55264
|
+
const openedWith = {
|
|
55265
|
+
factor: opened.factor.kind === "passphrase" ? "passphrase" : "security-key",
|
|
55266
|
+
envelopeId: opened.factor.envelopeId
|
|
55267
|
+
};
|
|
55268
|
+
if (opened.factor.kind === "security-key")
|
|
55269
|
+
opened.factor.session.pin = undefined;
|
|
54787
55270
|
const vaultId = vault.file.vaultId;
|
|
54788
55271
|
const keyTag = keyTagFor(vaultId, envelopeId);
|
|
54789
55272
|
let created;
|
|
@@ -54867,7 +55350,8 @@ async function addTouchIdFactor(ctx, parsed, resolvedVault, hold) {
|
|
|
54867
55350
|
helper: { teamId: session.identity.teamId, bundleId: session.identity.bundleId, minVersion: session.version },
|
|
54868
55351
|
accessControl: "biometryCurrentSet",
|
|
54869
55352
|
recoverableFactors: recoverable,
|
|
54870
|
-
verified: true
|
|
55353
|
+
verified: true,
|
|
55354
|
+
openedWith
|
|
54871
55355
|
});
|
|
54872
55356
|
return 0;
|
|
54873
55357
|
}
|
|
@@ -59889,7 +60373,7 @@ init_args();
|
|
|
59889
60373
|
init_release();
|
|
59890
60374
|
import { dirname as dirname10, join as join11 } from "node:path";
|
|
59891
60375
|
init_render();
|
|
59892
|
-
var
|
|
60376
|
+
var USAGE3 = "Usage: candle verify <file> --bundle <path> [--identity <uri>] [--issuer <url>]";
|
|
59893
60377
|
async function resolveIdentity(deps, bundlePath, flag) {
|
|
59894
60378
|
if (flag)
|
|
59895
60379
|
return { kind: "ok", uri: flag, provenance: "identity from --identity" };
|
|
@@ -59921,19 +60405,19 @@ async function verify(args, ctx) {
|
|
|
59921
60405
|
});
|
|
59922
60406
|
if ("error" in parsed) {
|
|
59923
60407
|
writeUsageFailure(deps, `${parsed.error}
|
|
59924
|
-
${
|
|
60408
|
+
${USAGE3}`, json);
|
|
59925
60409
|
return 2;
|
|
59926
60410
|
}
|
|
59927
60411
|
const file = parsed.positionals[0];
|
|
59928
60412
|
if (parsed.positionals.length !== 1 || file === undefined) {
|
|
59929
60413
|
writeUsageFailure(deps, `verify takes exactly one file.
|
|
59930
|
-
${
|
|
60414
|
+
${USAGE3}`, json);
|
|
59931
60415
|
return 2;
|
|
59932
60416
|
}
|
|
59933
60417
|
const bundlePath = parsed.values["--bundle"];
|
|
59934
60418
|
if (!bundlePath) {
|
|
59935
60419
|
writeUsageFailure(deps, `--bundle is required.
|
|
59936
|
-
${
|
|
60420
|
+
${USAGE3}`, json);
|
|
59937
60421
|
return 2;
|
|
59938
60422
|
}
|
|
59939
60423
|
const resolved = await resolveIdentity(deps, bundlePath, parsed.values["--identity"]);
|
|
@@ -59947,7 +60431,7 @@ ${USAGE2}`, json);
|
|
|
59947
60431
|
}
|
|
59948
60432
|
if (resolved.kind === "absent") {
|
|
59949
60433
|
writeUsageFailure(deps, `--identity is required: there is no latest.json beside ${bundlePath} to take the release version from.
|
|
59950
|
-
${
|
|
60434
|
+
${USAGE3}`, json);
|
|
59951
60435
|
return 2;
|
|
59952
60436
|
}
|
|
59953
60437
|
const identity = resolved.uri;
|
|
@@ -59988,6 +60472,177 @@ function messageOf2(error) {
|
|
|
59988
60472
|
return error instanceof Error ? error.message : String(error);
|
|
59989
60473
|
}
|
|
59990
60474
|
|
|
60475
|
+
// src/commands/wallets-trust.ts
|
|
60476
|
+
init_args();
|
|
60477
|
+
init_deps();
|
|
60478
|
+
init_profiles();
|
|
60479
|
+
init_render();
|
|
60480
|
+
init_promote_support();
|
|
60481
|
+
var TRUST_PATH = "/api/v1/agent/linked-wallets/trust";
|
|
60482
|
+
var USAGE_TRUST = "Usage: candle wallets trust <label|address|id|prefix*>... [--json]";
|
|
60483
|
+
var USAGE_UNTRUST = "Usage: candle wallets untrust <label|address|id|prefix*>... [--yes] [--json]";
|
|
60484
|
+
var DEVICE_TOKEN_REQUIRED2 = {
|
|
60485
|
+
code: "DEVICE_TOKEN_REQUIRED",
|
|
60486
|
+
message: "Marking a wallet trusted needs the device token, the owner's credential; an API key cannot do it.",
|
|
60487
|
+
suggestion: "Run: candle auth login"
|
|
60488
|
+
};
|
|
60489
|
+
function sessionLinkedLine(rows) {
|
|
60490
|
+
const n = rows.length;
|
|
60491
|
+
const it = n === 1 ? "it" : "them";
|
|
60492
|
+
const names = rows.map((row) => row.label ?? row.address).join(", ");
|
|
60493
|
+
return `${n} linked while signed in (${names}): always yours while linked. Revoke ${it} to remove ${it}.`;
|
|
60494
|
+
}
|
|
60495
|
+
function nothingToChangeLine(shown, trusted) {
|
|
60496
|
+
const session = shown.sessionLinked ?? [];
|
|
60497
|
+
const unchanged = shown.unchanged;
|
|
60498
|
+
if (session.length > 0 && unchanged.length === 0) {
|
|
60499
|
+
const one = session.length === 1;
|
|
60500
|
+
return `Nothing to change: ${one ? "that wallet is" : `those ${session.length} wallets are`} always yours while linked. Revoke ${one ? "it" : "them"} to remove ${one ? "it" : "them"}.
|
|
60501
|
+
`;
|
|
60502
|
+
}
|
|
60503
|
+
const bits = [];
|
|
60504
|
+
if (session.length > 0)
|
|
60505
|
+
bits.push(sessionLinkedLine(session).replace(/\.$/, ""));
|
|
60506
|
+
if (unchanged.length > 0) {
|
|
60507
|
+
bits.push(unchanged.length === 1 ? `that wallet is already ${trusted ? "trusted" : "untrusted"}` : `all ${unchanged.length} wallets are already ${trusted ? "trusted" : "untrusted"}`);
|
|
60508
|
+
}
|
|
60509
|
+
if (bits.length === 0)
|
|
60510
|
+
return `Nothing to change.
|
|
60511
|
+
`;
|
|
60512
|
+
return `Nothing to change: ${bits.join("; ")}.
|
|
60513
|
+
`;
|
|
60514
|
+
}
|
|
60515
|
+
function writeTrustFailure(ctx, result) {
|
|
60516
|
+
const { deps, apiUrl, json } = ctx;
|
|
60517
|
+
let code = result.code;
|
|
60518
|
+
let message = result.message;
|
|
60519
|
+
let suggestion;
|
|
60520
|
+
if (result.status === 404) {
|
|
60521
|
+
code = "TRUST_UNSUPPORTED";
|
|
60522
|
+
message = "This Candle API does not support trusting wallets yet; nothing changed.";
|
|
60523
|
+
} else {
|
|
60524
|
+
const error = result.raw?.error;
|
|
60525
|
+
if (error?.reason === "ambiguous" && Array.isArray(error.matches)) {
|
|
60526
|
+
const ids = error.matches.map((m) => String(m.id)).join(", ");
|
|
60527
|
+
suggestion = `Name one of them by id or address: ${ids}`;
|
|
60528
|
+
} else if (error?.reason === "not_found") {
|
|
60529
|
+
suggestion = "Run: candle wallets, to see this account's linked wallets and their labels.";
|
|
60530
|
+
}
|
|
60531
|
+
}
|
|
60532
|
+
const envelope = errorEnvelope({ ...result, code, message }, { apiUrl, authType: "device" });
|
|
60533
|
+
if (json) {
|
|
60534
|
+
deps.stdout.write(`${JSON.stringify({ ...envelope, ...suggestion ? { suggestion } : {} })}
|
|
60535
|
+
`);
|
|
60536
|
+
} else {
|
|
60537
|
+
deps.stderr.write(`${code ?? `HTTP ${result.status}`}: ${message}${suggestion ? ` ${suggestion}` : ""}
|
|
60538
|
+
`);
|
|
60539
|
+
}
|
|
60540
|
+
return 1;
|
|
60541
|
+
}
|
|
60542
|
+
function trustTable(rows) {
|
|
60543
|
+
return renderTable(["line", "label", "wallet", "address"], rows.map((row, i) => [String(i + 1), row.label ?? "-", row.chain, row.address]));
|
|
60544
|
+
}
|
|
60545
|
+
async function setTrust(args, ctx, trusted) {
|
|
60546
|
+
const { deps, apiUrl, json } = ctx;
|
|
60547
|
+
const usage5 = trusted ? USAGE_TRUST : USAGE_UNTRUST;
|
|
60548
|
+
const command = trusted ? "wallets trust" : "wallets untrust";
|
|
60549
|
+
const parsed = parseArgs(args, trusted ? {} : { booleanFlags: ["--yes"] });
|
|
60550
|
+
if ("error" in parsed) {
|
|
60551
|
+
writeUsageFailure(deps, parsed.error, json);
|
|
60552
|
+
return 2;
|
|
60553
|
+
}
|
|
60554
|
+
if (parsed.positionals.length === 0) {
|
|
60555
|
+
writeUsageFailure(deps, `Name at least one wallet. ${usage5}`, json);
|
|
60556
|
+
return 2;
|
|
60557
|
+
}
|
|
60558
|
+
const skipConfirm = !trusted && parsed.booleans.has("--yes");
|
|
60559
|
+
await printIdentity(ctx);
|
|
60560
|
+
const deviceToken = await resolveDeviceToken(deps, ctx.profile);
|
|
60561
|
+
if (!deviceToken) {
|
|
60562
|
+
writeLocalFailure(deps, DEVICE_TOKEN_REQUIRED2, json);
|
|
60563
|
+
return 1;
|
|
60564
|
+
}
|
|
60565
|
+
if (!skipConfirm && (!deps.isTTY.stdin || !deps.isTTY.stdout)) {
|
|
60566
|
+
writeLocalFailure(deps, {
|
|
60567
|
+
code: "TRUST_REQUIRES_TTY",
|
|
60568
|
+
message: `candle ${command} needs a terminal: the acknowledgement is typed, and nothing else supplies it.`,
|
|
60569
|
+
suggestion: trusted ? "Run it in an interactive shell; there is no flag and no environment variable for confirm." : "Run it in an interactive shell, or pass --yes to untrust without the prompt."
|
|
60570
|
+
}, json);
|
|
60571
|
+
return 1;
|
|
60572
|
+
}
|
|
60573
|
+
const call = (body) => apiRequest(TRUST_PATH, {
|
|
60574
|
+
method: "POST",
|
|
60575
|
+
body,
|
|
60576
|
+
auth: "device",
|
|
60577
|
+
credentials: { deviceToken },
|
|
60578
|
+
apiUrl,
|
|
60579
|
+
fetch: deps.fetch,
|
|
60580
|
+
env: deps.env
|
|
60581
|
+
});
|
|
60582
|
+
const preview = await call({ dryRun: true, wallets: parsed.positionals, trusted });
|
|
60583
|
+
if (!preview.ok)
|
|
60584
|
+
return writeTrustFailure(ctx, preview);
|
|
60585
|
+
const shown = preview.body;
|
|
60586
|
+
const changing = shown.changed;
|
|
60587
|
+
if (changing.length === 0) {
|
|
60588
|
+
if (json) {
|
|
60589
|
+
deps.stdout.write(`${JSON.stringify({ ...shown, command })}
|
|
60590
|
+
`);
|
|
60591
|
+
} else {
|
|
60592
|
+
deps.stdout.write(nothingToChangeLine(shown, trusted));
|
|
60593
|
+
}
|
|
60594
|
+
return 0;
|
|
60595
|
+
}
|
|
60596
|
+
const n = changing.length;
|
|
60597
|
+
const screen = [
|
|
60598
|
+
trustTable(changing),
|
|
60599
|
+
...shown.unchanged.length > 0 ? [
|
|
60600
|
+
`${shown.unchanged.length} already ${trusted ? "trusted" : "untrusted"}: ${shown.unchanged.map((row) => row.label ?? row.address).join(", ")}`
|
|
60601
|
+
] : [],
|
|
60602
|
+
...(shown.sessionLinked?.length ?? 0) > 0 ? [sessionLinkedLine(shown.sessionLinked ?? [])] : [],
|
|
60603
|
+
"",
|
|
60604
|
+
trusted ? `${n === 1 ? "This wallet" : `These ${n} wallets`} will be trusted: your agents can move funds into ${n === 1 ? "it" : "them"} with no cap, like a wallet you linked while signed in.` : `${n === 1 ? "This wallet" : `These ${n} wallets`} will no longer be trusted: moving funds into ${n === 1 ? "it" : "them"} will need the withdrawal allowlist again.`,
|
|
60605
|
+
""
|
|
60606
|
+
];
|
|
60607
|
+
deps.stderr.write(`${screen.join(`
|
|
60608
|
+
`)}
|
|
60609
|
+
`);
|
|
60610
|
+
if (!skipConfirm) {
|
|
60611
|
+
const typed = await deps.promptLine(`Type ${CONFIRM_WORD} to ${trusted ? "trust" : "untrust"} ${n === 1 ? "this wallet" : `these ${n} wallets`}: `);
|
|
60612
|
+
if (typed.trim().toLowerCase() !== CONFIRM_WORD) {
|
|
60613
|
+
writeLocalFailure(deps, {
|
|
60614
|
+
code: "TRUST_NOT_ACKNOWLEDGED",
|
|
60615
|
+
message: `The acknowledgement is the word ${CONFIRM_WORD}; nothing changed.`,
|
|
60616
|
+
suggestion: `Run the command again and type ${CONFIRM_WORD} at the prompt.`
|
|
60617
|
+
}, json);
|
|
60618
|
+
return 1;
|
|
60619
|
+
}
|
|
60620
|
+
}
|
|
60621
|
+
const committed = await call({ walletIds: changing.map((row) => row.id), trusted });
|
|
60622
|
+
if (!committed.ok)
|
|
60623
|
+
return writeTrustFailure(ctx, committed);
|
|
60624
|
+
const result = committed.body;
|
|
60625
|
+
if (json) {
|
|
60626
|
+
deps.stdout.write(`${JSON.stringify({ ...result, command })}
|
|
60627
|
+
`);
|
|
60628
|
+
return result.skipped.length > 0 ? 1 : 0;
|
|
60629
|
+
}
|
|
60630
|
+
const done = result.changed.length;
|
|
60631
|
+
deps.stdout.write(`${trusted ? "Trusted" : "Untrusted"} ${done} wallet${done === 1 ? "" : "s"}.
|
|
60632
|
+
`);
|
|
60633
|
+
for (const skip of result.skipped) {
|
|
60634
|
+
deps.stderr.write(`Skipped ${skip.walletId}: ${skip.reason === "revoked" ? "revoked" : "not on this account"}.
|
|
60635
|
+
`);
|
|
60636
|
+
}
|
|
60637
|
+
return result.skipped.length > 0 ? 1 : 0;
|
|
60638
|
+
}
|
|
60639
|
+
function walletsTrust(args, ctx) {
|
|
60640
|
+
return setTrust(args, ctx, true);
|
|
60641
|
+
}
|
|
60642
|
+
function walletsUntrust(args, ctx) {
|
|
60643
|
+
return setTrust(args, ctx, false);
|
|
60644
|
+
}
|
|
60645
|
+
|
|
59991
60646
|
// src/config.ts
|
|
59992
60647
|
import { chmod as chmod7, mkdir as mkdir7, readFile as readFile7, rm as rm4, writeFile as writeFile6 } from "node:fs/promises";
|
|
59993
60648
|
import { homedir as homedir5 } from "node:os";
|
|
@@ -60303,6 +60958,7 @@ function extractGlobalFlags(argv) {
|
|
|
60303
60958
|
}
|
|
60304
60959
|
var COMMANDS = {
|
|
60305
60960
|
swap: { bare: swap, subcommands: { status: swapStatus } },
|
|
60961
|
+
transfer: { bare: transfer },
|
|
60306
60962
|
launch: { bare: launch },
|
|
60307
60963
|
pnl: { bare: pnl },
|
|
60308
60964
|
portfolio: { bare: portfolio },
|
|
@@ -60310,7 +60966,7 @@ var COMMANDS = {
|
|
|
60310
60966
|
auth: { subcommands: { login: authLogin, status: authStatus, logout: authLogout } },
|
|
60311
60967
|
keys: { subcommands: { list: keysList, create: keysCreate, revoke: keysRevoke, wallets: keysWallets } },
|
|
60312
60968
|
wallets: {
|
|
60313
|
-
subcommands: { import: walletsImport, revoke: walletsRevoke },
|
|
60969
|
+
subcommands: { import: walletsImport, revoke: walletsRevoke, trust: walletsTrust, untrust: walletsUntrust },
|
|
60314
60970
|
bare: wallets
|
|
60315
60971
|
},
|
|
60316
60972
|
vault: {
|
package/package.json
CHANGED