@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.
Files changed (3) hide show
  1. package/README.md +3 -2
  2. package/dist/index.js +731 -75
  3. 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
- AGENT_KEY_PRESET_LABELS = { read: "Read", readwrite: "Read:Write" };
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
- if (!device.extensions.includes("hmac-secret")) {
6104
- 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." });
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({ ...opts, op: "register", nonce: b64u(randomBytes2(16)) });
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
- digest: base64.encode(digest),
6180
- deviceId: session.device.deviceId,
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
- const requestedScopes = parsed.values["--scopes"] ? parseScopesList(parsed.values["--scopes"]) : undefined;
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 scope = "swap:write";
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
- deviceFlag: ctx.vaultDevice,
22386
- ...choice.preferDevice !== undefined ? { preferDevice: choice.preferDevice } : {},
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.7";
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
- { invocation: "--device <id>", description: "The security key to use when more than one is attached" },
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
- if (anyNone || anyStale || anyTee)
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
- factor: "passphrase",
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 session = await openSecurityKeySession(deps, {
54657
- vaultId: file.vaultId,
54658
- envelopeId,
54659
- deviceFlag: ctx.vaultDevice,
54660
- requireFeatures: true
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
- factor: "passphrase",
54666
- passphraseOnlyBecause: "this command's security key session is for the key being added, so the vault opens with the passphrase here."
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 registered = await registerCredential(deps, session, { vaultId: vault.file.vaultId, envelopeId });
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
- factor: "passphrase",
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 USAGE2 = "Usage: candle verify <file> --bundle <path> [--identity <uri>] [--issuer <url>]";
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
- ${USAGE2}`, json);
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
- ${USAGE2}`, json);
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
- ${USAGE2}`, json);
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
- ${USAGE2}`, json);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@candledottv/cli",
3
- "version": "0.11.7",
3
+ "version": "0.11.8",
4
4
  "description": "The Candle CLI: authorize a device from your browser, then manage API keys, wallets, and setup health from the terminal",
5
5
  "type": "module",
6
6
  "bin": {