@nostrbox/cli 1.7.0 → 1.7.1

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.
@@ -39,10 +39,88 @@ var __importStar = (this && this.__importStar) || (function () {
39
39
  })();
40
40
  Object.defineProperty(exports, "__esModule", { value: true });
41
41
  const core_1 = require("@nostrbox/core");
42
+ const crypto_1 = require("crypto");
42
43
  const argParser_js_1 = require("../../utils/argParser.js");
44
+ const cliHelpers_js_1 = require("../../utils/cliHelpers.js");
43
45
  const tabtab = __importStar(require("tabtab"));
44
46
  // Valid zodiac signs (converted to lowercase for CLI compatibility)
45
47
  const VALID_ZODIAC_SIGNS = core_1.ZODIAC_SIGNS.map((sign) => sign.toLowerCase());
48
+ /**
49
+ * Derives encryption password from user-memorable parameters
50
+ *
51
+ * @param color - Color selection (blue, green, purple, etc.)
52
+ * @param birthday - Birthday day number (1-31)
53
+ * @param zodiacSign - Zodiac sign (Aries, Taurus, etc.)
54
+ * @param operation - Operation type (add or subtract)
55
+ * @returns Derived password string with format:
56
+ * - ADD: UPPERCASE_COLOR + "+" + birthday + "+" + lowercase_zodiac (e.g., "BLUE+17+aries")
57
+ * - SUBTRACT: lowercase_color + "-" + birthday + "-" + UPPERCASE_ZODIAC (e.g., "blue-17-ARIES")
58
+ */
59
+ function deriveMemoryKeys(color, birthday, zodiacSign, operation) {
60
+ if (operation === 'add') {
61
+ return `${color.toUpperCase()}+${birthday}+${zodiacSign.toLowerCase()}`;
62
+ }
63
+ else {
64
+ return `${color.toLowerCase()}-${birthday}-${zodiacSign.toUpperCase()}`;
65
+ }
66
+ }
67
+ /**
68
+ * Generates random Base64 string of specified length
69
+ */
70
+ function generateRandomBase64(length) {
71
+ const bytesNeeded = Math.ceil((length * 3) / 4);
72
+ const randomBytesBuffer = (0, crypto_1.randomBytes)(bytesNeeded);
73
+ const base64String = randomBytesBuffer.toString('base64');
74
+ return base64String.substring(0, length);
75
+ }
76
+ /**
77
+ * Splits an encrypted zodiac password into two parts based on birthday and color
78
+ *
79
+ * @param encryptedPassword - The encrypted zodiac password
80
+ * @param birthday - Birthday value (1-31)
81
+ * @param colorValue - Color value (1-7)
82
+ * @returns Two parts of same length as original, each containing partial original data
83
+ */
84
+ function splitZodiacPassword(encryptedPassword, birthday, colorValue) {
85
+ const length = encryptedPassword.length;
86
+ const splitPoint = birthday + colorValue;
87
+ if (splitPoint >= length) {
88
+ throw new Error(`Split point (${splitPoint}) must be less than password length (${length})`);
89
+ }
90
+ // Part 1: First splitPoint chars from original + random garbage for rest
91
+ const part1Original = encryptedPassword.substring(0, splitPoint);
92
+ const part1Random = generateRandomBase64(length - splitPoint);
93
+ const part1 = part1Original + part1Random;
94
+ // Part 2: Random garbage for first splitPoint chars + remaining original chars
95
+ const part2Random = generateRandomBase64(splitPoint);
96
+ const part2Original = encryptedPassword.substring(splitPoint);
97
+ const part2 = part2Random + part2Original;
98
+ return {
99
+ part1,
100
+ part2,
101
+ splitPoint
102
+ };
103
+ }
104
+ /**
105
+ * Combines two split parts to recover the original encrypted zodiac password
106
+ *
107
+ * @param part1 - First split part
108
+ * @param part2 - Second split part
109
+ * @param birthday - Birthday value (1-31)
110
+ * @param colorValue - Color value (1-7)
111
+ * @returns Original encrypted zodiac password
112
+ */
113
+ function combineZodiacPassword(part1, part2, birthday, colorValue) {
114
+ const splitPoint = birthday + colorValue;
115
+ if (part1.length !== part2.length) {
116
+ throw new Error(`Split parts must have same length (part1: ${part1.length}, part2: ${part2.length})`);
117
+ }
118
+ // Extract first splitPoint chars from part1
119
+ const firstPart = part1.substring(0, splitPoint);
120
+ // Extract remaining chars from part2
121
+ const secondPart = part2.substring(splitPoint);
122
+ return firstPart + secondPart;
123
+ }
46
124
  // Color values mapping
47
125
  const COLOR_VALUES = {
48
126
  blue: 1,
@@ -54,6 +132,86 @@ const COLOR_VALUES = {
54
132
  brown: 7
55
133
  };
56
134
  const VALID_COLORS = Object.keys(COLOR_VALUES);
135
+ /**
136
+ * Generates export JSON with encrypted split parts and all chain addresses
137
+ * Derives public addresses from the seed phrase for Bitcoin, Ethereum, Solana, TRON, and Nostr
138
+ */
139
+ function generateExportData(seedPhrase, splitPart1, splitPart2, passphrase = '') {
140
+ const mnemonic = seedPhrase.join(' ');
141
+ // Derive Bitcoin native (bc1q...) address
142
+ const bitcoinResult = (0, core_1.deriveKeyFromMnemonic)({
143
+ mnemonic,
144
+ chain: 'bitcoin',
145
+ format: 'native',
146
+ passphrase,
147
+ accountIndex: 0,
148
+ changeIndex: 0,
149
+ addressIndex: 0
150
+ });
151
+ // Derive Nostr public key (npub1...)
152
+ const nostrResult = (0, core_1.deriveKeyFromMnemonic)({
153
+ mnemonic,
154
+ chain: 'nostr',
155
+ format: 'bech32',
156
+ passphrase,
157
+ accountIndex: 0,
158
+ changeIndex: 0,
159
+ addressIndex: 0
160
+ });
161
+ // Derive Ethereum address (0x...)
162
+ const ethereumResult = (0, core_1.deriveKeyFromMnemonic)({
163
+ mnemonic,
164
+ chain: 'ethereum',
165
+ format: 'native',
166
+ passphrase,
167
+ accountIndex: 0,
168
+ changeIndex: 0,
169
+ addressIndex: 0
170
+ });
171
+ // Derive Solana address
172
+ const solanaResult = (0, core_1.deriveKeyFromMnemonic)({
173
+ mnemonic,
174
+ chain: 'solana',
175
+ format: 'native',
176
+ passphrase,
177
+ accountIndex: 0
178
+ });
179
+ // Derive TRON address (T...)
180
+ const tronResult = (0, core_1.deriveKeyFromMnemonic)({
181
+ mnemonic,
182
+ chain: 'tron',
183
+ format: 'native',
184
+ passphrase,
185
+ accountIndex: 0,
186
+ changeIndex: 0,
187
+ addressIndex: 0
188
+ });
189
+ // Validate that all addresses were derived successfully
190
+ if (!bitcoinResult.address) {
191
+ throw new Error('Failed to derive Bitcoin address');
192
+ }
193
+ if (!nostrResult.publicKey) {
194
+ throw new Error('Failed to derive Nostr public key');
195
+ }
196
+ if (!ethereumResult.address) {
197
+ throw new Error('Failed to derive Ethereum address');
198
+ }
199
+ if (!solanaResult.address) {
200
+ throw new Error('Failed to derive Solana address');
201
+ }
202
+ if (!tronResult.address) {
203
+ throw new Error('Failed to derive TRON address');
204
+ }
205
+ return {
206
+ encZodiacPwdSplit1: splitPart1,
207
+ encZodiacPwdSplit2: splitPart2,
208
+ nostr: nostrResult.publicKey,
209
+ bitcoin: bitcoinResult.address,
210
+ ethereum: ethereumResult.address,
211
+ solana: solanaResult.address,
212
+ tron: tronResult.address
213
+ };
214
+ }
57
215
  // Constants for format detection and validation
58
216
  const BASE58_PATTERN = /^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$/;
59
217
  const MAX_ZODIAC_PASSWORD_LENGTH = 30;
@@ -116,15 +274,47 @@ function createParser() {
116
274
  },
117
275
  {
118
276
  name: 'load',
277
+ type: 'flag',
278
+ description: 'Modifier flag: when used, data input flags expect file paths instead of direct values'
279
+ },
280
+ // Old import methods (for backward compatibility)
281
+ {
282
+ name: 'codes',
283
+ type: 'string',
284
+ description: 'Secret codes: provide numbers directly, or file path if --load is used'
285
+ },
286
+ {
287
+ name: 'zodiacPwd',
119
288
  type: 'string',
120
- description: 'Load from file (seed phrase or numbers depending on --restore flag)'
289
+ description: 'Plain zodiac password: provide Base58 directly, or file path if --load is used'
290
+ },
291
+ {
292
+ name: 'encZodiacPwd',
293
+ type: 'string',
294
+ description: 'Encrypted zodiac password: provide string directly, or file path if --load is used'
295
+ },
296
+ {
297
+ name: 'encZodiacPwdSplit',
298
+ type: 'string',
299
+ description: 'Encrypted split parts: provide comma-separated directly, or file path if --load is used'
300
+ },
301
+ // Encryption options
302
+ {
303
+ name: 'passphrase',
304
+ type: 'string',
305
+ description: 'Custom user password for encryption/decryption (overrides auto-derived memory keys)'
121
306
  },
122
307
  // Output format options
123
308
  {
124
309
  name: 'zodiac-password',
125
310
  alias: 'p',
126
311
  type: 'flag',
127
- description: 'Output/input zodiac password (Base58) instead of 12 numbers'
312
+ description: 'Output/input zodiac password (Base58, always encrypted and split) instead of 12 numbers'
313
+ },
314
+ {
315
+ name: 'export',
316
+ type: 'string',
317
+ description: 'Export JSON with encrypted split parts and public addresses to specified file'
128
318
  },
129
319
  // File options
130
320
  {
@@ -151,7 +341,9 @@ async function setupCompletion(env) {
151
341
  '--seed',
152
342
  '--restore',
153
343
  '--load',
344
+ '--passphrase',
154
345
  '--zodiac-password',
346
+ '--export',
155
347
  '--save'
156
348
  ];
157
349
  const commandCompletions = ['install', 'uninstall'];
@@ -166,70 +358,56 @@ async function setupCompletion(env) {
166
358
  }
167
359
  }
168
360
  // Special case for operation values
169
- if (env.line && env.line.includes('--operation=')) {
170
- const operationPart = current.replace('--operation=', '');
171
- const matches = ['add', 'subtract'].filter((op) => op.startsWith(operationPart));
172
- return tabtab.log(matches);
361
+ const operationMatches = (0, cliHelpers_js_1.handleOptionValueCompletion)(env.line || '', current, 'operation', ['add', 'subtract']);
362
+ if (operationMatches) {
363
+ return tabtab.log(operationMatches);
173
364
  }
174
365
  // Special case for zodiac values
175
- if (env.line && env.line.includes('--zodiac=')) {
176
- const zodiacPart = current.replace('--zodiac=', '');
177
- const matches = VALID_ZODIAC_SIGNS.filter((sign) => sign.startsWith(zodiacPart));
178
- return tabtab.log(matches);
366
+ const zodiacMatches = (0, cliHelpers_js_1.handleOptionValueCompletion)(env.line || '', current, 'zodiac', VALID_ZODIAC_SIGNS);
367
+ if (zodiacMatches) {
368
+ return tabtab.log(zodiacMatches);
179
369
  }
180
370
  // Special case for color values
181
- if (env.line && env.line.includes('--color=')) {
182
- const colorPart = current.replace('--color=', '');
183
- const matches = VALID_COLORS.filter((color) => color.startsWith(colorPart));
184
- return tabtab.log(matches);
371
+ const colorMatches = (0, cliHelpers_js_1.handleOptionValueCompletion)(env.line || '', current, 'color', VALID_COLORS);
372
+ if (colorMatches) {
373
+ return tabtab.log(colorMatches);
185
374
  }
186
375
  // Filter flag names based on current input
187
376
  const matches = flagCompletions.filter((comp) => comp.startsWith(current));
188
377
  return tabtab.log(matches);
189
378
  }
190
- async function installCompletion() {
191
- try {
192
- await tabtab.install({
193
- name: 'bip39zodiac',
194
- completer: 'bip39zodiac'
195
- });
196
- console.log('āœ… Shell completion installed successfully!');
197
- console.log('šŸ“ You may need to restart your shell or run: source ~/.bashrc (or equivalent)');
198
- }
199
- catch (error) {
200
- console.error('āŒ Failed to install completion:', error instanceof Error ? error.message : String(error));
201
- process.exit(1);
202
- }
203
- }
204
- async function uninstallCompletion() {
205
- try {
206
- await tabtab.uninstall({
207
- name: 'bip39zodiac'
208
- });
209
- console.log('āœ… Shell completion uninstalled successfully!');
210
- }
211
- catch (error) {
212
- console.error('āŒ Failed to uninstall completion:', error instanceof Error ? error.message : String(error));
213
- process.exit(1);
214
- }
215
- }
379
+ // Create shared completion installer and uninstaller using utility functions
380
+ const installCompletion = (0, cliHelpers_js_1.createCompletionInstaller)('bip39zodiac');
381
+ const uninstallCompletion = (0, cliHelpers_js_1.createCompletionUninstaller)('bip39zodiac');
216
382
  function showHelp() {
217
383
  console.log(`
218
384
  🌌 Seed Phrase CLI Tool - BIP39 Zodiac Operations
219
385
 
220
386
  USAGE:
221
- bip39zodiac [INPUT_METHOD] [OPTIONS] [CONVERSION_FLAGS]
387
+ # Generation
388
+ bip39zodiac [GENERATION_INPUT] [CONVERSION_FLAGS] [OPTIONS]
389
+
390
+ # Restoration
391
+ bip39zodiac [RESTORE_INPUT] --restore [CONVERSION_FLAGS] [OPTIONS]
392
+
393
+ # Commands
222
394
  bip39zodiac <install|uninstall>
223
395
 
224
- INPUT METHODS (choose one):
396
+ GENERATION INPUT METHODS (choose one):
225
397
  --new, -n Generate new random 12-word seed phrase
226
- --seed "content" Transform seed phrase or restore from codes
227
- --load <file> Load from file (seed phrase or codes)
398
+ --seed "12 words" Transform existing seed phrase (12 words directly)
399
+ --seed <file> ... or file path when used with --load flag
228
400
 
229
- OPTIONS:
230
- --restore Restore mode (reverses the transformation)
231
- --zodiac-password, -p Output/input zodiac password (Base58) instead of 12 numbers
232
- --save <file> Save results to file
401
+ RESTORE INPUT METHODS (choose one, use with --restore):
402
+ --codes "numbers" Secret codes: provide numbers directly
403
+ --codes <file> ... or file path when used with --load flag
404
+ --zodiacPwd "base58" Plain zodiac password: provide Base58 directly
405
+ --zodiacPwd <file> ... or file path when used with --load flag
406
+ --encZodiacPwd "encrypted" Encrypted password: provide string directly
407
+ --encZodiacPwd <file> ... or file path when used with --load flag
408
+ --encZodiacPwdSplit "p1,p2" Split parts: provide comma-separated directly
409
+ --encZodiacPwdSplit <file> ... or file path when used with --load flag (2 lines or 1 line with comma)
410
+ --load Modifier flag: makes above flags expect file paths
233
411
 
234
412
  CONVERSION FLAGS (all required):
235
413
  --operation, -o <add|subtract> Operation type (mandatory):
@@ -241,6 +419,18 @@ CONVERSION FLAGS (all required):
241
419
  --color, -c <color> Color for additional offset (mandatory):
242
420
  blue(1), green(2), purple(3), orange(4), yellow(5), pink(6), brown(7)
243
421
 
422
+ ENCRYPTION OPTIONS:
423
+ --passphrase <password> Custom user password for encryption/decryption
424
+ (overrides auto-derived memory keys)
425
+ Requirements: min 8 chars, letters + numbers + special char,
426
+ no common weak passwords (e.g., "password", "123456")
427
+
428
+ OUTPUT OPTIONS:
429
+ --zodiac-password, -p Output zodiac password (Base58) format
430
+ --export <file> Export JSON with encrypted split parts and public addresses
431
+ to specified file (Bitcoin, Ethereum, Solana, TRON, Nostr)
432
+ --save <file> Save results to file
433
+
244
434
  COMMANDS:
245
435
  install Install shell completion for this command
246
436
  uninstall Uninstall shell completion for this command
@@ -265,30 +455,49 @@ ZODIAC SIGNS & ALGORITHMS:
265
455
  Not your own birthday and zodiac for maximum security.
266
456
 
267
457
  EXAMPLES:
458
+ # GENERATION
268
459
  # Generate new seed phrase and transform it
269
460
  bip39zodiac --new --operation=add --birthday=17 --zodiac=aries --color=blue
270
461
 
271
- # Transform existing seed phrase into codes and save
272
- bip39zodiac --seed "abandon ability able..." --operation=subtract --birthday=25 --zodiac=leo --color=green --save codes.txt
462
+ # Transform existing seed phrase and save
463
+ bip39zodiac --seed "abandon ability able..." --save codes.txt --operation=subtract --birthday=25 --zodiac=leo --color=green
464
+
465
+ # Load seed phrase from file and save with zodiac password format
466
+ bip39zodiac --load --seed seedphrase.txt --zodiac-password --save password.txt --operation=add --birthday=10 --zodiac=virgo --color=orange
273
467
 
274
- # Transform to zodiac password (compact Base58 format)
275
- bip39zodiac --seed "abandon ability able..." --operation=add --birthday=10 --zodiac=virgo --color=purple --zodiac-password
468
+ # Generate with custom passphrase instead of memory keys
469
+ bip39zodiac --new --passphrase "MySecretPassword.123" --operation=add --birthday=17 --zodiac=aries --color=blue
276
470
 
277
- # Load seed phrase from file and output zodiac password
278
- bip39zodiac --load seedphrase.txt --operation=add --birthday=10 --zodiac=virgo --color=orange --zodiac-password --save password.txt
471
+ # Export JSON with encrypted split parts and all chain addresses to file
472
+ bip39zodiac --new --export export.json --operation=add --birthday=17 --zodiac=aries --color=blue
279
473
 
280
- # Restore original from codes (same parameters, auto-reverses) and save
281
- bip39zodiac --seed "1234 567 890..." --restore --operation=add --birthday=17 --zodiac=aries --color=blue --save restored.txt
474
+ # RESTORATION
475
+ # Restore from secret codes
476
+ bip39zodiac --restore --codes "22 23 24 25 26 27 28 29 30 19 20 21" --operation=add --birthday=17 --zodiac=aries --color=blue
282
477
 
283
- # Restore from zodiac password
284
- bip39zodiac --seed "AoWJhqGkNxEe2D" --restore --zodiac-password --operation=subtract --birthday=25 --zodiac=leo --color=yellow
478
+ # Restore from secret codes loaded from file
479
+ bip39zodiac --restore --load --codes codes.txt --operation=add --birthday=17 --zodiac=aries --color=blue
285
480
 
286
- # Load zodiac password from file to restore
287
- bip39zodiac --load password.txt --restore --zodiac-password --operation=add --birthday=10 --zodiac=virgo --color=pink
481
+ # Restore from plain zodiac password
482
+ bip39zodiac --restore --zodiacPwd "1P1Q1R1S1T1U1V1W1X1L1M1N" --operation=subtract --birthday=25 --zodiac=leo --color=yellow
288
483
 
289
- # Full workflow: transform to zodiac password and save, then restore
290
- bip39zodiac --seed "abandon ability able..." --operation=add --birthday=15 --zodiac=gemini --color=brown --zodiac-password --save secret.txt
291
- bip39zodiac --load secret.txt --restore --zodiac-password --operation=add --birthday=15 --zodiac=gemini --color=brown
484
+ # Restore from encrypted zodiac password (single string)
485
+ bip39zodiac --restore --encZodiacPwd "s2ubbiTDzq..." --operation=add --birthday=10 --zodiac=virgo --color=pink
486
+
487
+ # Restore from encrypted split parts
488
+ bip39zodiac --restore --encZodiacPwdSplit "part1...,part2..." --operation=add --birthday=15 --zodiac=gemini --color=brown
489
+
490
+ # Load codes from file and restore (--load makes --codes expect file path)
491
+ bip39zodiac --restore --load --codes codes.txt --operation=add --birthday=17 --zodiac=aries --color=blue
492
+
493
+ # Load encrypted split from file (2 lines or 1 line with comma)
494
+ bip39zodiac --restore --load --encZodiacPwdSplit password.txt --operation=add --birthday=10 --zodiac=virgo --color=orange
495
+
496
+ # Restore with custom passphrase
497
+ bip39zodiac --restore --encZodiacPwdSplit "part1...,part2..." --passphrase "MySecretPassword.123" --operation=add --birthday=15 --zodiac=gemini --color=brown
498
+
499
+ # INSTALL COMPLETION
500
+ bip39zodiac install
292
501
  `);
293
502
  }
294
503
  /**
@@ -326,20 +535,51 @@ function validateCoreFlags(args) {
326
535
  }
327
536
  /**
328
537
  * Validates that exactly one input method is specified and required combinations are met
329
- * Ensures restore mode has proper input and only one of new/seed/load is used
538
+ * For generation: use --new or --seed (seed phrase)
539
+ * For restore: use --codes, --zodiacPwd, --encZodiacPwd, or --encZodiacPwdSplit (optionally with --load)
330
540
  */
331
541
  function validateUseCase(args) {
332
- // --restore requires either --seed or --load for input
333
- if (args.restore && !args.seed && !args.load) {
334
- throw new Error('--restore requires either --seed or --load to provide the numbers');
542
+ // For restore mode
543
+ if (args.restore) {
544
+ // --seed cannot be used with --restore
545
+ if (args.seed) {
546
+ throw new Error('--seed is only for seed phrases during generation. For restore, use --codes, --zodiacPwd, --encZodiacPwd, or --encZodiacPwdSplit');
547
+ }
548
+ if (args.new) {
549
+ throw new Error('--new cannot be used with --restore');
550
+ }
551
+ // Must have one of the restore input methods
552
+ if (!args.codes &&
553
+ !args.zodiacPwd &&
554
+ !args.encZodiacPwd &&
555
+ !args.encZodiacPwdSplit) {
556
+ throw new Error('--restore requires one of: --codes, --zodiacPwd, --encZodiacPwd, or --encZodiacPwdSplit (optionally with --load <file>)');
557
+ }
558
+ }
559
+ else {
560
+ // For generation mode, cannot use restore-specific flags
561
+ if (args.codes ||
562
+ args.zodiacPwd ||
563
+ args.encZodiacPwd ||
564
+ args.encZodiacPwdSplit) {
565
+ throw new Error('Restore input methods (--codes, --zodiacPwd, --encZodiacPwd, --encZodiacPwdSplit) require --restore flag');
566
+ }
335
567
  }
336
568
  // Check that we have at least one input method
337
- const inputs = [args.new, args.seed, args.load].filter(Boolean);
569
+ const inputs = [
570
+ args.new,
571
+ args.seed,
572
+ args.codes,
573
+ args.zodiacPwd,
574
+ args.encZodiacPwd,
575
+ args.encZodiacPwdSplit
576
+ ].filter(Boolean);
338
577
  if (inputs.length === 0) {
339
- throw new Error('Input required: exactly one of --new, --seed, or --load must be specified');
578
+ throw new Error('Input required: for generation use --new or --seed; for restore use --codes, --zodiacPwd, --encZodiacPwd, or --encZodiacPwdSplit');
340
579
  }
580
+ // Only one data input method allowed (--load is separate as it's just the file path)
341
581
  if (inputs.length > 1) {
342
- throw new Error('Input conflict: only one of --new, --seed, or --load can be used');
582
+ throw new Error('Input conflict: only one data input method can be used at a time');
343
583
  }
344
584
  }
345
585
  /**
@@ -356,29 +596,33 @@ function getSeedPhrase(args) {
356
596
  return (0, core_1.generateSecureSeedPhrase)(12, 128);
357
597
  }
358
598
  if (args.seed) {
359
- console.log('šŸ“„ Processing existing seed phrase...');
360
- const words = args.seed
361
- .split(' ')
362
- .map((word) => word.trim())
363
- .filter((word) => word.length > 0);
364
- (0, core_1.validateBip39Words)(words);
365
- return words;
366
- }
367
- if (args.load) {
368
- console.log(`šŸ“‚ Loading from file: ${args.load}`);
369
- try {
370
- const fileContent = (0, core_1.loadArrayFromFile)(args.load);
371
- // fileContent is always string[], join and split to handle both formats
372
- const content = fileContent
373
- .join(' ')
374
- .split(/\s+/)
375
- .filter((item) => item.length > 0);
376
- console.log('šŸ“„ Loading seed phrase for transformation');
377
- (0, core_1.validateBip39Words)(content);
378
- return content;
599
+ if (args.load) {
600
+ // When --load is present, args.seed contains the file path
601
+ console.log(`šŸ“‚ Loading seed phrase from file: ${args.seed}`);
602
+ try {
603
+ const fileContent = (0, core_1.loadArrayFromFile)(args.seed);
604
+ // fileContent is always string[], join and split to handle both formats
605
+ const content = fileContent
606
+ .join(' ')
607
+ .split(/\s+/)
608
+ .filter((item) => item.length > 0);
609
+ console.log('šŸ“„ Processing seed phrase for transformation');
610
+ (0, core_1.validateBip39Words)(content);
611
+ return content;
612
+ }
613
+ catch (error) {
614
+ throw new Error(`Failed to load file '${args.seed}': ${error instanceof Error ? error.message : String(error)}`);
615
+ }
379
616
  }
380
- catch (error) {
381
- throw new Error(`Failed to load file '${args.load}': ${error instanceof Error ? error.message : String(error)}`);
617
+ else {
618
+ // Direct input as string
619
+ console.log('šŸ“„ Processing existing seed phrase...');
620
+ const words = args.seed
621
+ .split(' ')
622
+ .map((word) => word.trim())
623
+ .filter((word) => word.length > 0);
624
+ (0, core_1.validateBip39Words)(words);
625
+ return words;
382
626
  }
383
627
  }
384
628
  throw new Error('No valid use case specified');
@@ -462,23 +706,21 @@ function parseSecretInput(input, useZodiacPassword, source) {
462
706
  * Displays a formatted table showing the complete transformation process
463
707
  * Shows original words through each transformation step to final shuffled codes
464
708
  */
465
- function displayTable(originalWords, mappedNumbers, birthdayOffsetNumbers, colorOffsetNumbers, shuffledNumbers, base58Values, zodiacSign) {
709
+ function displayTable(originalWords, mappedNumbers, offsetNumbers, shuffledNumbers, base58Values, zodiacSign) {
466
710
  console.log('\nšŸ“Š SEED PHRASE TRANSFORMATION TABLE');
467
- console.log('====================================');
468
- console.log(`Original Words | Mapped Numbers | Birthday Offset | Color Offset | ${zodiacSign} Shuffled | Base58`);
469
- console.log('---------------|----------------|-----------------|--------------|-----------------|-------');
711
+ console.log('================================================================');
712
+ console.log(`# | Original Words | Mapped | Offset | ${zodiacSign} Shuffled | Base58`);
713
+ console.log('-----|------------------|--------|--------|----------------|--------');
470
714
  for (let i = 0; i < originalWords.length; i++) {
471
- const word = originalWords[i].padEnd(13);
472
- const mapped = String(mappedNumbers[i]).padEnd(15);
473
- const birthdayOffset = String(birthdayOffsetNumbers[i]).padEnd(16);
474
- const colorOffset = String(colorOffsetNumbers[i]).padEnd(13);
475
- const shuffled = String(shuffledNumbers[i]).padEnd(15);
476
- const base58 = base58Values[i].padEnd(2);
477
- console.log(`${String(i + 1).padStart(2)}. ${word} ${mapped} ${birthdayOffset} ${colorOffset} ${shuffled} ${base58}`);
478
- }
479
- console.log('---------------|----------------|-----------------|--------------|-----------------|-------');
480
- console.log(`Final secret codes: ${shuffledNumbers.join(' ')}`);
481
- console.log(`Original seed phrase: ${originalWords.join(' ')}`);
715
+ const num = String(i + 1).padStart(4);
716
+ const word = originalWords[i].padEnd(16);
717
+ const mapped = String(mappedNumbers[i]).padStart(6);
718
+ const offset = String(offsetNumbers[i]).padStart(6);
719
+ const shuffled = String(shuffledNumbers[i]).padStart(14);
720
+ const base58 = base58Values[i].padStart(6);
721
+ console.log(`${num} | ${word} ${mapped} ${offset} ${shuffled} ${base58}`);
722
+ }
723
+ console.log('-----|------------------|--------|--------|----------------|--------');
482
724
  }
483
725
  /**
484
726
  * Main entry point for the BIP39 Zodiac CLI
@@ -519,19 +761,128 @@ async function main() {
519
761
  const colorValue = COLOR_VALUES[args.color];
520
762
  const isAdd = args.operation === 'add';
521
763
  let secretCodes;
522
- if (args.seed) {
523
- const input = args.seed.trim();
524
- secretCodes = parseSecretInput(input, args['zodiac-password'] || false, '');
764
+ let inputString = '';
765
+ // Handle old import methods
766
+ if (args.codes) {
767
+ // --codes: Secret codes (numbers from string or file)
768
+ console.log('šŸ“„ Processing secret codes...');
769
+ if (args.load) {
770
+ // When --load is present, args.codes contains the file path
771
+ console.log(`šŸ“‚ Loading from file: ${args.codes}`);
772
+ const fileContent = (0, core_1.loadArrayFromFile)(args.codes);
773
+ inputString = fileContent.join(' ').trim();
774
+ secretCodes = parseNumbers(inputString);
775
+ }
776
+ else {
777
+ // Direct input as string
778
+ secretCodes = parseNumbers(args.codes);
779
+ }
780
+ (0, core_1.validateBip39Indices)(secretCodes);
781
+ }
782
+ else if (args.zodiacPwd) {
783
+ // --zodiacPwd: Plain unencrypted zodiac password (from string or file)
784
+ console.log('šŸ“„ Processing plain zodiac password...');
785
+ let passwordInput;
786
+ if (args.load) {
787
+ // When --load is present, args.zodiacPwd contains the file path
788
+ console.log(`šŸ“‚ Loading from file: ${args.zodiacPwd}`);
789
+ passwordInput = (0, core_1.loadArrayFromFile)(args.zodiacPwd).join('').trim();
790
+ }
791
+ else {
792
+ // Direct input as string
793
+ passwordInput = args.zodiacPwd;
794
+ }
795
+ secretCodes = (0, core_1.decodePassword)(passwordInput);
796
+ }
797
+ else if (args.encZodiacPwd) {
798
+ // --encZodiacPwd: Single encrypted password (from string or file)
799
+ console.log('šŸ“„ Processing encrypted zodiac password...');
800
+ let encryptedInput;
801
+ if (args.load) {
802
+ // When --load is present, args.encZodiacPwd contains the file path
803
+ console.log(`šŸ“‚ Loading from file: ${args.encZodiacPwd}`);
804
+ encryptedInput = (0, core_1.loadArrayFromFile)(args.encZodiacPwd).join('').trim();
805
+ }
806
+ else {
807
+ // Direct input as string
808
+ encryptedInput = args.encZodiacPwd;
809
+ }
810
+ // Use custom passphrase if provided, otherwise derive from memory keys
811
+ const memoryKeys = args.passphrase
812
+ ? args.passphrase
813
+ : deriveMemoryKeys(args.color, birthday, zodiacSign, args.operation);
814
+ if (args.passphrase) {
815
+ console.log(` Using custom passphrase`);
816
+ }
817
+ else {
818
+ console.log(` Memory keys: ${memoryKeys} (${args.operation} operation)`);
819
+ }
820
+ const decryptionResult = (0, core_1.decryptString)(encryptedInput, memoryKeys);
821
+ const decryptedPassword = decryptionResult.decrypted;
822
+ console.log(`āœ… Decryption successful!`);
823
+ console.log(`šŸ”“ Decrypted zodiac password: ${decryptedPassword}`);
824
+ secretCodes = (0, core_1.decodePassword)(decryptedPassword);
525
825
  }
526
- else if (args.load) {
527
- const fileContent = (0, core_1.loadArrayFromFile)(args.load);
528
- const content = fileContent.join(' ').trim();
529
- secretCodes = parseSecretInput(content, args['zodiac-password'] || false, 'file');
826
+ else if (args.encZodiacPwdSplit) {
827
+ // --encZodiacPwdSplit: Comma-separated split parts or from file (2 lines)
828
+ console.log('šŸ“„ Processing encrypted zodiac password (split parts)...');
829
+ let part1;
830
+ let part2;
831
+ if (args.load) {
832
+ // When --load is present, args.encZodiacPwdSplit contains the file path
833
+ console.log(`šŸ“‚ Loading from file: ${args.encZodiacPwdSplit}`);
834
+ const fileContent = (0, core_1.loadArrayFromFile)(args.encZodiacPwdSplit);
835
+ if (fileContent.length === 2) {
836
+ // Two lines: each line is a part
837
+ part1 = fileContent[0].trim();
838
+ part2 = fileContent[1].trim();
839
+ }
840
+ else if (fileContent.length === 1 && fileContent[0].includes(',')) {
841
+ // One line with comma: split by comma
842
+ const parts = fileContent[0].split(',').map((p) => p.trim());
843
+ if (parts.length !== 2) {
844
+ throw new Error(`Expected 2 comma-separated parts, got ${parts.length}`);
845
+ }
846
+ ;
847
+ [part1, part2] = parts;
848
+ }
849
+ else {
850
+ throw new Error(`Expected file to contain either 2 lines or 1 line with comma-separated parts, got ${fileContent.length} line(s)`);
851
+ }
852
+ }
853
+ else {
854
+ // Direct input as comma-separated string
855
+ const parts = args.encZodiacPwdSplit.split(',').map((p) => p.trim());
856
+ if (parts.length !== 2) {
857
+ throw new Error(`Expected 2 comma-separated parts, got ${parts.length}`);
858
+ }
859
+ ;
860
+ [part1, part2] = parts;
861
+ }
862
+ console.log(`šŸ”€ Combining split parts...`);
863
+ console.log(` Split point: ${birthday} + ${colorValue} = ${birthday + colorValue}`);
864
+ const combinedPassword = combineZodiacPassword(part1, part2, birthday, colorValue);
865
+ console.log(`āœ… Parts combined successfully!`);
866
+ // Use custom passphrase if provided, otherwise derive from memory keys
867
+ const memoryKeys = args.passphrase
868
+ ? args.passphrase
869
+ : deriveMemoryKeys(args.color, birthday, zodiacSign, args.operation);
870
+ console.log(`\nšŸ”“ Decrypting zodiac password...`);
871
+ if (args.passphrase) {
872
+ console.log(` Using custom passphrase`);
873
+ }
874
+ else {
875
+ console.log(` Memory keys: ${memoryKeys} (${args.operation} operation)`);
876
+ }
877
+ const decryptionResult = (0, core_1.decryptString)(combinedPassword, memoryKeys);
878
+ const decryptedPassword = decryptionResult.decrypted;
879
+ console.log(`āœ… Decryption successful!`);
880
+ console.log(`šŸ”“ Decrypted zodiac password: ${decryptedPassword}`);
881
+ secretCodes = (0, core_1.decodePassword)(decryptedPassword);
530
882
  }
531
883
  else {
532
- throw new Error('No secret codes provided for restoration');
884
+ throw new Error('No restore input method specified. Use --codes, --zodiacPwd, --encZodiacPwd, or --encZodiacPwdSplit');
533
885
  }
534
- (0, core_1.validateBip39Indices)(secretCodes);
535
886
  console.log(`šŸ“„ Input secret codes: ${secretCodes.join(' ')}`);
536
887
  // Reverse the process: unshuffle → remove color offset → remove birthday offset → get original words
537
888
  console.log(`šŸ”„ Unshuffling with ${zodiacSign}...`);
@@ -602,42 +953,115 @@ async function main() {
602
953
  const shuffledNumbers = shuffleResult.shuffled;
603
954
  console.log(`šŸ“ Step 4: Shuffle with ${zodiacSign}`);
604
955
  // Generate zodiac password and get individual Base58 values
605
- let zodiacPassword;
956
+ let unencryptedZodiacPassword;
606
957
  let base58Values;
607
958
  try {
608
959
  const passwordResult = (0, core_1.encodePassword)(shuffledNumbers);
609
- zodiacPassword = passwordResult.password;
960
+ unencryptedZodiacPassword = passwordResult.password;
610
961
  base58Values = passwordResult.individual;
611
962
  }
612
963
  catch (error) {
613
964
  throw new Error(`Failed to generate zodiac password: ${error instanceof Error ? error.message : String(error)}`);
614
965
  }
615
- // Display the 6-column table with Base58 values
616
- displayTable(originalWords, mappedNumbers, birthdayOffsetNumbers, colorOffsetNumbers, shuffledNumbers, base58Values, zodiacSign);
617
- // Display zodiac password section
618
- console.log('\nšŸ” ZODIAC PASSWORD');
619
- console.log('==================');
966
+ // Encrypt zodiac password (ALWAYS)
967
+ let zodiacPassword;
968
+ let splitResult;
969
+ let memoryKeys;
970
+ try {
971
+ // Use custom passphrase if provided, otherwise derive from memory keys
972
+ if (args.passphrase) {
973
+ memoryKeys = args.passphrase;
974
+ console.log(`\nšŸ”’ Encrypting zodiac password...`);
975
+ console.log(` Using custom passphrase`);
976
+ }
977
+ else {
978
+ memoryKeys = deriveMemoryKeys(args.color, birthday, zodiacSign, args.operation);
979
+ console.log(`\nšŸ”’ Encrypting zodiac password...`);
980
+ console.log(` Memory keys: ${memoryKeys} (${args.operation} operation)`);
981
+ }
982
+ const encryptionResult = (0, core_1.encryptString)(unencryptedZodiacPassword, memoryKeys);
983
+ zodiacPassword = encryptionResult.encrypted;
984
+ console.log(`āœ… Encryption complete!`);
985
+ // Split the encrypted password using birthday + color
986
+ console.log(`\nšŸ”€ Splitting encrypted password at position ${birthday} + ${colorValue} = ${birthday + colorValue}...`);
987
+ splitResult = splitZodiacPassword(zodiacPassword, birthday, colorValue);
988
+ console.log(`āœ… Split complete!`);
989
+ }
990
+ catch (error) {
991
+ throw new Error(`Failed to encrypt zodiac password: ${error instanceof Error ? error.message : String(error)}`);
992
+ }
993
+ // Handle export mode - output JSON and exit
994
+ if (args.export) {
995
+ console.log('\nšŸ“¦ Generating export data...');
996
+ const bip39Passphrase = args.passphrase || '';
997
+ const exportData = generateExportData(originalWords, splitResult.part1, splitResult.part2, bip39Passphrase);
998
+ const jsonOutput = JSON.stringify(exportData, null, 2);
999
+ try {
1000
+ (0, core_1.saveArrayToFile)([jsonOutput], args.export);
1001
+ console.log(`āœ… Export data saved to: ${args.export}`);
1002
+ }
1003
+ catch (error) {
1004
+ console.error(`āš ļø Failed to save to file '${args.export}': ${error instanceof Error ? error.message : String(error)}`);
1005
+ }
1006
+ return;
1007
+ }
1008
+ // Display the transformation table
1009
+ displayTable(originalWords, mappedNumbers, colorOffsetNumbers, // Combined offset (birthday + color applied)
1010
+ shuffledNumbers, base58Values, zodiacSign);
1011
+ // Display inline format below table
1012
+ console.log('\nOriginal Words:');
1013
+ console.log(originalWords.join(' '));
1014
+ console.log('\nMapped Numbers:');
1015
+ console.log(mappedNumbers.join(' '));
1016
+ console.log('\nOffset Applied:');
1017
+ console.log(colorOffsetNumbers.join(' '));
1018
+ console.log(`\n${zodiacSign} Shuffled:`);
1019
+ console.log(shuffledNumbers.join(' '));
1020
+ console.log('\nBase58:');
1021
+ console.log(base58Values.join(' '));
1022
+ // Display password information
1023
+ console.log('\nZodiac Password:');
1024
+ console.log(unencryptedZodiacPassword);
1025
+ console.log('\nEncrypted Zodiac Password:');
620
1026
  console.log(zodiacPassword);
621
- console.log('\nāœ… TRANSFORMATION COMPLETE!');
1027
+ console.log('\nEncrypted Zodiac Password (Split Parts):');
1028
+ console.log(`šŸ’” Each part contains a portion of the encrypted password padded with random data`);
1029
+ console.log(` Both parts are required to reconstruct the original encrypted password`);
1030
+ console.log(`Part 1: ${splitResult.part1}`);
1031
+ console.log(`Part 2: ${splitResult.part2}`);
1032
+ console.log(`Split point: ${birthday} + ${colorValue} = ${birthday + colorValue}`);
1033
+ const passphraseFlag = args.passphrase
1034
+ ? ` --passphrase "${args.passphrase}"`
1035
+ : '';
622
1036
  if (args['zodiac-password']) {
623
- console.log(`šŸ” Zodiac Password: ${zodiacPassword}`);
624
- console.log(`šŸ“Š Secret codes: ${shuffledNumbers.join(' ')} (for reference)`);
625
- console.log(`šŸ”„ To restore, use: bip39zodiac --seed "${zodiacPassword}" --restore --zodiac-password --operation=${args.operation} --birthday=${birthday} --zodiac=${args.zodiac} --color=${args.color}`);
1037
+ console.log(`\nšŸ”„ To restore, use: bip39zodiac --encZodiacPwdSplit "${splitResult.part1},${splitResult.part2}" --restore --operation=${args.operation} --birthday=${birthday} --zodiac=${args.zodiac} --color=${args.color}${passphraseFlag}`);
1038
+ if (args.passphrase) {
1039
+ console.log(`\nšŸ’” Using custom passphrase for encryption`);
1040
+ }
1041
+ else {
1042
+ console.log(`\nšŸ’” Memory keys: ${memoryKeys} (${args.operation} operation - auto-derived from your parameters)`);
1043
+ }
626
1044
  }
627
1045
  else {
628
- console.log(`šŸ” Use these secret codes: ${shuffledNumbers.join(' ')}`);
629
- console.log(`šŸ”‘ Zodiac Password: ${zodiacPassword} (compact format)`);
630
- console.log(`šŸ”„ To restore, use: bip39zodiac --seed "${shuffledNumbers.join(' ')}" --restore --operation=${args.operation} --birthday=${birthday} --zodiac=${args.zodiac} --color=${args.color}`);
631
- console.log(`šŸ”„ Or with zodiac password: bip39zodiac --seed "${zodiacPassword}" --restore --zodiac-password --operation=${args.operation} --birthday=${birthday} --zodiac=${args.zodiac} --color=${args.color}`);
1046
+ console.log(`\nšŸ”„ To restore with codes: bip39zodiac --codes "${shuffledNumbers.join(' ')}" --restore --operation=${args.operation} --birthday=${birthday} --zodiac=${args.zodiac} --color=${args.color}`);
1047
+ console.log(`\nšŸ”„ To restore with zodiac password: bip39zodiac --zodiacPwd "${unencryptedZodiacPassword}" --restore --operation=${args.operation} --birthday=${birthday} --zodiac=${args.zodiac} --color=${args.color}`);
1048
+ console.log(`\nšŸ”„ To restore with encrypted split: bip39zodiac --encZodiacPwdSplit "${splitResult.part1},${splitResult.part2}" --restore --operation=${args.operation} --birthday=${birthday} --zodiac=${args.zodiac} --color=${args.color}${passphraseFlag}`);
632
1049
  }
633
1050
  // Save to file if --save option provided
634
1051
  if (args.save) {
635
1052
  try {
636
- const contentToSave = args['zodiac-password']
637
- ? zodiacPassword
638
- : shuffledNumbers.join(' ');
639
- (0, core_1.saveArrayToFile)([contentToSave], args.save);
640
- console.log(`šŸ’¾ Results saved to: ${args.save}`);
1053
+ if (args['zodiac-password']) {
1054
+ // Save both split parts (part1 on first line, part2 on second line)
1055
+ (0, core_1.saveArrayToFile)([splitResult.part1, splitResult.part2], args.save);
1056
+ console.log(`šŸ’¾ Split parts saved to: ${args.save}`);
1057
+ console.log(` Part 1: Line 1`);
1058
+ console.log(` Part 2: Line 2`);
1059
+ }
1060
+ else {
1061
+ // Save numeric codes
1062
+ (0, core_1.saveArrayToFile)([shuffledNumbers.join(' ')], args.save);
1063
+ console.log(`šŸ’¾ Results saved to: ${args.save}`);
1064
+ }
641
1065
  }
642
1066
  catch (error) {
643
1067
  console.error(`āš ļø Failed to save to file '${args.save}': ${error instanceof Error ? error.message : String(error)}`);