@ldgr/cli 0.5.2 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @ldgr/cli
2
2
 
3
- LDGR's self-custody terminal wallet. Version 0.5.2 creates or imports local wallets, lets you select between them, shows addresses and balances, sends native Ledra or a token, and persistently switches the selected wallet between mainnet and testnet. It also reviews prepared MCP requests for transfers, swaps, token creation and escrow creation in a separate interactive terminal.
3
+ LDGR's self-custody terminal wallet. Version 0.5.3 creates or imports local wallets, lets you select between them, shows addresses and balances, sends native Ledra or a token, and persistently switches the selected wallet between mainnet and testnet. It also exports encrypted files for the web wallet and reviews prepared MCP requests for transfers, swaps, token creation and escrow creation in a separate interactive terminal.
4
4
 
5
5
  The MCP launcher uses `ldgr mcp --stdin` with a UTF-8 JSON envelope for
6
6
  `wallet-info` and `save-request`. Envelopes are limited to 128 KiB. Public wallet
@@ -16,6 +16,7 @@ npm install -g @ldgr/cli
16
16
  ldgr wallet create
17
17
  ldgr wallet import ~/Downloads/ldgr-recovery-key.txt
18
18
  ldgr wallet export ~/Desktop/ldgr-recovery-key.txt
19
+ ldgr wallet export-web ~/Desktop/ldgr-web-wallet.json
19
20
  ldgr wallet list
20
21
  ldgr wallet select 2
21
22
  ldgr address
@@ -26,6 +27,14 @@ ldgr network show
26
27
  ldgr network set testnet
27
28
  ```
28
29
 
30
+ ## Encrypted web-wallet transfer
31
+
32
+ Run `ldgr wallet export-web <file> [--wallet <path>]` in your own interactive terminal. It shows the selected public address, network and destination `https://wlt.ldgr.ltd`, then asks for confirmation with **No** selected. A protected source wallet prompts privately for its existing passphrase. Every export requires a separate transfer-file passphrase of at least 12 characters, entered twice through masked prompts, including when the source wallet is unprotected. Recovery keys and passphrases are never command arguments, environment variables, or terminal output for this operation.
33
+
34
+ The command writes a new owner-only file without overwriting an existing path. Its version-1 `ldgr-web-wallet-transfer` envelope contains `ledgerId`, `address`, `publicKey`, and a passphrase-encrypted `ldgr-browser-wallet` vault. Open [Ledra Wallet](https://wlt.ldgr.ltd/#import), choose **Import encrypted wallet file**, select this file, and enter its transfer passphrase there. Delete the transfer file after import and keep the passphrase private. The encrypted file is sensitive backup material. On Windows its permissions rely on native ACLs, as the source wallet does.
35
+
36
+ The MCP command envelope may launch `wallet export-web` in a separate terminal, but the machine bridge and `@ldgr/cli/mcp` module cannot export or unlock a wallet. Piped input or output is rejected before the command reads or migrates the source vault.
37
+
29
38
  ## MCP approval requests
30
39
 
31
40
  The public `@ldgr/cli/mcp` module exposes `getMcpWalletInfo`, `validateMcpRequest`, and `saveMcpRequest`. Metadata is an explicit public projection: address, public key, network, ledger ID and protection mode. It never returns a recovery key, passphrase, encrypted vault or signer. Request preparation does not unlock, sign or submit.
package/dist/cli.js CHANGED
@@ -812,7 +812,7 @@ function commandArgs(input) {
812
812
  const creation = first === "wallet" && (second === "create" || second === "import");
813
813
  if (testnet && !creation)
814
814
  throw new TypeError("Invalid network flag");
815
- const valid = first === "wallet" && second === "create" && positional.length === 2 || first === "wallet" && second === "import" && (positional.length === 2 || positional.length === 3) || first === "wallet" && second === "list" && positional.length === 2 || first === "wallet" && second === "select" && positional.length === 3 && /^[0-9]+$/.test(third) || first === "approve" && positional.length === 2 || first === "address" && positional.length === 1 || first === "network" && second === "show" && positional.length === 2 || first === "network" && second === "set" && positional.length === 3 && (third === "mainnet" || third === "testnet");
815
+ const valid = first === "wallet" && second === "create" && positional.length === 2 || first === "wallet" && second === "import" && (positional.length === 2 || positional.length === 3) || first === "wallet" && second === "list" && positional.length === 2 || first === "wallet" && second === "export-web" && positional.length === 3 || first === "wallet" && second === "select" && positional.length === 3 && /^[0-9]+$/.test(third) || first === "approve" && positional.length === 2 || first === "address" && positional.length === 1 || first === "network" && second === "show" && positional.length === 2 || first === "network" && second === "set" && positional.length === 3 && (third === "mainnet" || third === "testnet");
816
816
  if (!valid)
817
817
  throw new TypeError("Unsupported command");
818
818
  return [...input];
@@ -873,14 +873,58 @@ async function runMcpBridge(args) {
873
873
  return;
874
874
  }
875
875
 
876
+ // src/web-wallet-export.ts
877
+ import { importEncryptedWalletVault } from "@ldgr/wallet-core";
878
+ import { mkdir as mkdir4, open as open4 } from "node:fs/promises";
879
+ import { dirname as dirname4 } from "node:path";
880
+ async function exportWebWalletFile(input) {
881
+ if (input.transferPassphrase.length < 12)
882
+ throw new RangeError("Transfer passphrase must be at least 12 characters");
883
+ const file = await walletFileMetadata(input.walletPath);
884
+ if (input.expectedIdentity && (file.ledgerId !== input.expectedIdentity.ledgerId || file.address !== input.expectedIdentity.address || file.publicKey !== input.expectedIdentity.publicKey)) {
885
+ throw new Error("Selected wallet changed after review; start the export again");
886
+ }
887
+ const unlocked = await unlockWalletFile(file, input.sourcePassphrase);
888
+ const imported = await importEncryptedWalletVault({
889
+ recoveryKey: unlocked.recoveryKey,
890
+ ledgerId: file.ledgerId,
891
+ passphrase: input.transferPassphrase
892
+ });
893
+ if (imported.address !== file.address || imported.publicKey !== file.publicKey || imported.vault.publicKey !== file.publicKey || imported.vault.protection !== "passphrase") {
894
+ throw new Error("Exported wallet identity or protection does not match the source wallet");
895
+ }
896
+ const envelope = {
897
+ format: "ldgr-web-wallet-transfer",
898
+ version: 1,
899
+ ledgerId: file.ledgerId,
900
+ address: file.address,
901
+ publicKey: file.publicKey,
902
+ vault: imported.vault
903
+ };
904
+ await mkdir4(dirname4(input.backupPath), { recursive: true, mode: 448 });
905
+ const handle = await open4(input.backupPath, "wx", 384).catch((error) => {
906
+ if (error.code === "EEXIST")
907
+ throw new Error(`Web-wallet transfer file already exists: ${input.backupPath}`);
908
+ throw error;
909
+ });
910
+ try {
911
+ await handle.writeFile(JSON.stringify(envelope) + `
912
+ `, "utf8");
913
+ await handle.sync();
914
+ } finally {
915
+ await handle.close();
916
+ }
917
+ }
918
+
876
919
  // src/cli.ts
877
920
  var HELP = `
878
- LDGR CLI 0.5.2 — self-custody wallet
921
+ LDGR CLI 0.5.3 — self-custody wallet
879
922
 
880
923
  Usage:
881
924
  ldgr wallet create [--testnet] [--wallet <path>]
882
925
  ldgr wallet import [recovery-key-file] [--testnet] [--wallet <path>]
883
926
  ldgr wallet export <recovery-key-file> [--wallet <path>]
927
+ ldgr wallet export-web <transfer-file> [--wallet <path>]
884
928
  ldgr wallet list [--wallet <path>]
885
929
  ldgr wallet select <index> [--wallet <path>]
886
930
  ldgr address [--wallet <path>]
@@ -895,7 +939,9 @@ The default wallet store is ~/.ldgr/wallet.json. A wallet can be
895
939
  passphrase-protected or unprotected (local-file permissions only). The CLI
896
940
  never accepts recovery-key values or passphrases as arguments or environment
897
941
  variables. Use \`ldgr wallet export <path>\` for an extension-compatible
898
- recovery-key file. Testnet mode prints a warning on every wallet command.
942
+ recovery-key file. Use \`ldgr wallet export-web <path>\` in an interactive
943
+ terminal for a passphrase-encrypted transfer file for https://wlt.ldgr.ltd.
944
+ Testnet mode prints a warning on every wallet command.
899
945
  `;
900
946
  function parseArguments(argv) {
901
947
  const command = [];
@@ -1041,6 +1087,30 @@ async function runExport(path, backupPath) {
1041
1087
  p2.log.warn("Anyone with this recovery-key file can control the wallet. Keep it offline and never share it.");
1042
1088
  p2.outro(`Recovery-key file saved at ${backupPath}`);
1043
1089
  }
1090
+ async function runExportWeb(path, backupPath) {
1091
+ const file = await walletFileMetadata(path);
1092
+ showNetworkWarning(file);
1093
+ p2.note(`Address
1094
+ ${file.address}
1095
+
1096
+ Network
1097
+ ${networkLabel(file)}
1098
+
1099
+ Destination
1100
+ https://wlt.ldgr.ltd
1101
+
1102
+ Transfer file
1103
+ ${backupPath.replace(/[\x00-\x1f\x7f-\x9f\p{Cf}\u2028\u2029]/gu, "?")}`, "Review encrypted wallet export");
1104
+ await confirm3("Export this wallet for https://wlt.ldgr.ltd?");
1105
+ const sourcePassphrase = await signingPassphrase(file, "Enter your source wallet passphrase");
1106
+ const transferPassphrase = await askPassphrase("Choose a separate transfer-file passphrase");
1107
+ const repeated = await askPassphrase("Repeat the transfer-file passphrase");
1108
+ if (transferPassphrase !== repeated)
1109
+ throw new Error("Passphrases did not match");
1110
+ await exportWebWalletFile({ walletPath: path, backupPath, sourcePassphrase, transferPassphrase, expectedIdentity: file });
1111
+ p2.log.info("Open https://wlt.ldgr.ltd/#import, choose Import encrypted wallet file, and select this file. Enter its passphrase only in the wallet. Delete the transfer file after import.");
1112
+ p2.outro("Encrypted web-wallet transfer file saved");
1113
+ }
1044
1114
  async function runWalletList(path) {
1045
1115
  const wallets = await listWalletFileMetadata(path);
1046
1116
  const selected = wallets.find((wallet) => wallet.selected);
@@ -1189,6 +1259,9 @@ async function main() {
1189
1259
  }
1190
1260
  p2.intro("LDGR CLI");
1191
1261
  try {
1262
+ if (first === "wallet" && second === "export-web" && (!process.stdin.isTTY || !process.stdout.isTTY)) {
1263
+ throw new Error("Web-wallet export requires an interactive terminal with terminal input and output");
1264
+ }
1192
1265
  const resolved = parsed.walletPathExplicit ? { path: parsed.walletPath, migrated: false } : await migrateDefaultWalletPath();
1193
1266
  if (resolved.migrated)
1194
1267
  p2.log.info(`Moved legacy wallet store to ${resolved.path}`);
@@ -1198,6 +1271,8 @@ async function main() {
1198
1271
  await runImport(resolved.path, parsed.walletOnTestnet, third);
1199
1272
  else if (first === "wallet" && second === "export" && third && fourth === undefined)
1200
1273
  await runExport(resolved.path, third);
1274
+ else if (first === "wallet" && second === "export-web" && third && fourth === undefined)
1275
+ await runExportWeb(resolved.path, third);
1201
1276
  else if (first === "wallet" && second === "list" && third === undefined)
1202
1277
  await runWalletList(resolved.path);
1203
1278
  else if (first === "wallet" && second === "select" && third && fourth === undefined)
@@ -67,6 +67,10 @@ export declare function migrateDefaultWalletPath(input?: DefaultWalletPathInput)
67
67
  readonly path: string;
68
68
  readonly migrated: boolean;
69
69
  }>;
70
+ export declare function unlockWalletFile(file: LocalWalletFile, passphrase?: string): Promise<{
71
+ readonly wallet: LdgrSigner;
72
+ readonly recoveryKey: string;
73
+ }>;
70
74
  export declare function readEncryptedWalletFile(path: string, passphrase?: string): Promise<LdgrSigner>;
71
75
  /** Writes the exact raw recovery-key text accepted by the extension importer. */
72
76
  export declare function exportRecoveryKeyFile(input: {
@@ -0,0 +1,21 @@
1
+ import { type EncryptedWalletVault } from "@ldgr/wallet-core";
2
+ import type { LedgerId } from "@ldgr/protocol";
3
+ type WalletIdentity = {
4
+ readonly ledgerId: LedgerId;
5
+ readonly address: string;
6
+ readonly publicKey: string;
7
+ };
8
+ export type WebWalletTransfer = WalletIdentity & {
9
+ readonly format: "ldgr-web-wallet-transfer";
10
+ readonly version: 1;
11
+ readonly vault: EncryptedWalletVault;
12
+ };
13
+ /** Local-only export; never expose this operation through the public MCP module. */
14
+ export declare function exportWebWalletFile(input: {
15
+ readonly walletPath: string;
16
+ readonly backupPath: string;
17
+ readonly sourcePassphrase?: string | undefined;
18
+ readonly transferPassphrase: string;
19
+ readonly expectedIdentity?: WalletIdentity;
20
+ }): Promise<void>;
21
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ldgr/cli",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "ldgr": "dist/cli.js"