@profullstack/coinpay 0.3.7 → 0.3.9

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 (2) hide show
  1. package/bin/coinpay.js +269 -2
  2. package/package.json +1 -1
package/bin/coinpay.js CHANGED
@@ -8,8 +8,11 @@
8
8
  import { CoinPayClient } from '../src/client.js';
9
9
  import { PaymentStatus, Blockchain, FiatCurrency } from '../src/payments.js';
10
10
  import { readFileSync, writeFileSync, existsSync } from 'fs';
11
+ import { execSync } from 'child_process';
12
+ import { createInterface } from 'readline';
11
13
  import { homedir } from 'os';
12
14
  import { join } from 'path';
15
+ import { tmpdir } from 'os';
13
16
 
14
17
  const VERSION = '0.3.3';
15
18
  const CONFIG_FILE = join(homedir(), '.coinpay.json');
@@ -95,8 +98,19 @@ function parseArgs(args) {
95
98
  const arg = args[i];
96
99
 
97
100
  if (arg.startsWith('--')) {
98
- const [key, value] = arg.slice(2).split('=');
99
- result.flags[key] = value ?? true;
101
+ const eqIdx = arg.indexOf('=');
102
+ if (eqIdx !== -1) {
103
+ result.flags[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);
104
+ } else {
105
+ // Peek ahead: if next arg doesn't start with '-', it's the value
106
+ const next = args[i + 1];
107
+ if (next && !next.startsWith('-')) {
108
+ result.flags[arg.slice(2)] = next;
109
+ i++;
110
+ } else {
111
+ result.flags[arg.slice(2)] = true;
112
+ }
113
+ }
100
114
  } else if (arg.startsWith('-')) {
101
115
  result.flags[arg.slice(1)] = args[++i] ?? true;
102
116
  } else if (!result.command) {
@@ -143,6 +157,10 @@ ${colors.cyan}Commands:${colors.reset}
143
157
  get <crypto> Get exchange rate
144
158
  list Get all exchange rates
145
159
 
160
+ ${colors.bright}wallet${colors.reset}
161
+ backup-seed Encrypt seed phrase to GPG file
162
+ decrypt-backup <file> Decrypt a GPG backup file
163
+
146
164
  ${colors.bright}webhook${colors.reset}
147
165
  logs <business-id> Get webhook logs
148
166
  test <business-id> Send test webhook
@@ -156,6 +174,10 @@ ${colors.cyan}Options:${colors.reset}
156
174
  --currency <code> Fiat currency (USD, EUR, etc.) - default: USD
157
175
  --blockchain <code> Blockchain (BTC, ETH, SOL, POL, BCH, USDC_ETH, USDC_POL, USDC_SOL)
158
176
  --description <text> Payment description
177
+ --seed <phrase> Seed phrase (or reads from stdin)
178
+ --password <pass> GPG passphrase (or prompts interactively)
179
+ --wallet-id <id> Wallet ID for backup filename
180
+ --output <path> Output file path (default: wallet_<id>_seedphrase.txt.gpg)
159
181
 
160
182
  ${colors.cyan}Examples:${colors.reset}
161
183
  # Configure your API key (get it from your CoinPay dashboard)
@@ -179,6 +201,18 @@ ${colors.cyan}Examples:${colors.reset}
179
201
  # List your businesses
180
202
  coinpay business list
181
203
 
204
+ # Encrypt seed phrase to GPG backup file
205
+ coinpay wallet backup-seed --seed "word1 word2 ..." --password "mypass" --wallet-id "wid-abc"
206
+
207
+ # Encrypt seed phrase (interactive password prompt)
208
+ coinpay wallet backup-seed --seed "word1 word2 ..." --wallet-id "wid-abc"
209
+
210
+ # Pipe seed phrase from stdin
211
+ echo "word1 word2 ..." | coinpay wallet backup-seed --wallet-id "wid-abc" --password "mypass"
212
+
213
+ # Decrypt a backup file
214
+ coinpay wallet decrypt-backup wallet_wid-abc_seedphrase.txt.gpg --password "mypass"
215
+
182
216
  ${colors.cyan}Environment Variables:${colors.reset}
183
217
  COINPAY_API_KEY API key (overrides config)
184
218
  COINPAY_BASE_URL Custom API URL
@@ -453,6 +487,235 @@ async function handleWebhook(subcommand, args, flags) {
453
487
  }
454
488
  }
455
489
 
490
+ /**
491
+ * Prompt for password interactively (hides input)
492
+ */
493
+ function promptPassword(prompt = 'Password: ') {
494
+ return new Promise((resolve) => {
495
+ process.stdout.write(prompt);
496
+
497
+ // If stdin is a TTY, read with hidden input
498
+ if (process.stdin.isTTY) {
499
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
500
+ // Temporarily override output to hide password chars
501
+ const origWrite = process.stdout.write.bind(process.stdout);
502
+ process.stdout.write = (chunk) => {
503
+ // Only suppress characters that are the user's input
504
+ if (typeof chunk === 'string' && chunk !== prompt && chunk !== '\n' && chunk !== '\r\n') {
505
+ return true;
506
+ }
507
+ return origWrite(chunk);
508
+ };
509
+
510
+ rl.question('', (answer) => {
511
+ process.stdout.write = origWrite;
512
+ process.stdout.write('\n');
513
+ rl.close();
514
+ resolve(answer);
515
+ });
516
+ } else {
517
+ // Pipe mode — read from stdin
518
+ const chunks = [];
519
+ process.stdin.on('data', (chunk) => chunks.push(chunk));
520
+ process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString().trim()));
521
+ process.stdin.resume();
522
+ }
523
+ });
524
+ }
525
+
526
+ /**
527
+ * Check if gpg is available
528
+ */
529
+ function hasGpg() {
530
+ try {
531
+ execSync('gpg --version', { stdio: 'pipe' });
532
+ return true;
533
+ } catch {
534
+ return false;
535
+ }
536
+ }
537
+
538
+ /**
539
+ * Wallet commands
540
+ */
541
+ async function handleWallet(subcommand, args, flags) {
542
+ switch (subcommand) {
543
+ case 'backup-seed': {
544
+ if (!hasGpg()) {
545
+ print.error('gpg is required but not found. Install it with:');
546
+ print.info(' Ubuntu/Debian: sudo apt install gnupg');
547
+ print.info(' macOS: brew install gnupg');
548
+ print.info(' Windows: https://www.gnupg.org/download/');
549
+ process.exit(1);
550
+ }
551
+
552
+ const walletId = flags['wallet-id'];
553
+ if (!walletId) {
554
+ print.error('Required: --wallet-id <id>');
555
+ return;
556
+ }
557
+
558
+ // Get seed phrase from --seed flag or stdin
559
+ let seed = flags.seed;
560
+ if (!seed) {
561
+ if (process.stdin.isTTY) {
562
+ print.error('Required: --seed <phrase> (or pipe via stdin)');
563
+ print.info('Example: coinpay wallet backup-seed --seed "word1 word2 ..." --wallet-id "wid-abc"');
564
+ return;
565
+ }
566
+ // Read from stdin
567
+ const chunks = [];
568
+ for await (const chunk of process.stdin) {
569
+ chunks.push(chunk);
570
+ }
571
+ seed = Buffer.concat(chunks).toString().trim();
572
+ }
573
+
574
+ if (!seed) {
575
+ print.error('Seed phrase is empty');
576
+ return;
577
+ }
578
+
579
+ // Get password from --password flag or prompt
580
+ let password = flags.password;
581
+ if (!password) {
582
+ if (!process.stdin.isTTY) {
583
+ print.error('Required: --password <pass> (cannot prompt in pipe mode)');
584
+ return;
585
+ }
586
+ password = await promptPassword('Encryption password: ');
587
+ const confirm = await promptPassword('Confirm password: ');
588
+ if (password !== confirm) {
589
+ print.error('Passwords do not match');
590
+ return;
591
+ }
592
+ }
593
+
594
+ if (!password) {
595
+ print.error('Password is empty');
596
+ return;
597
+ }
598
+
599
+ // Build the plaintext content
600
+ const filename = `wallet_${walletId}_seedphrase.txt`;
601
+ const content = [
602
+ '# CoinPayPortal Wallet Seed Phrase Backup',
603
+ `# Wallet ID: ${walletId}`,
604
+ `# Created: ${new Date().toISOString()}`,
605
+ '#',
606
+ '# KEEP THIS FILE SAFE. Anyone with this phrase can access your funds.',
607
+ `# Decrypt with: gpg --decrypt ${filename}.gpg`,
608
+ '',
609
+ seed,
610
+ '',
611
+ ].join('\n');
612
+
613
+ // Determine output path
614
+ const outputPath = flags.output || `${filename}.gpg`;
615
+
616
+ // Write plaintext to temp file, encrypt with gpg, remove temp
617
+ const tmpFile = join(tmpdir(), `coinpay-backup-${Date.now()}.txt`);
618
+ try {
619
+ writeFileSync(tmpFile, content, { mode: 0o600 });
620
+
621
+ // Write passphrase to a temp file for gpg (avoids shell escaping issues)
622
+ const passFile = join(tmpdir(), `coinpay-pass-${Date.now()}`);
623
+ writeFileSync(passFile, password, { mode: 0o600 });
624
+ try {
625
+ execSync(
626
+ `gpg --batch --yes --passphrase-file "${passFile}" --pinentry-mode loopback --symmetric --cipher-algo AES256 --output "${outputPath}" "${tmpFile}"`,
627
+ { stdio: ['pipe', 'pipe', 'pipe'] }
628
+ );
629
+ } finally {
630
+ try { writeFileSync(passFile, Buffer.alloc(password.length, 0)); } catch {}
631
+ try { const { unlinkSync: u } = await import('fs'); u(passFile); } catch {}
632
+ }
633
+
634
+ print.success(`Encrypted backup saved to: ${outputPath}`);
635
+ print.info(`Decrypt with: gpg --decrypt ${outputPath}`);
636
+ } finally {
637
+ // Securely delete temp file
638
+ try {
639
+ writeFileSync(tmpFile, Buffer.alloc(content.length, 0));
640
+ const { unlinkSync } = await import('fs');
641
+ unlinkSync(tmpFile);
642
+ } catch {
643
+ // Best effort cleanup
644
+ }
645
+ }
646
+ break;
647
+ }
648
+
649
+ case 'decrypt-backup': {
650
+ if (!hasGpg()) {
651
+ print.error('gpg is required but not found.');
652
+ process.exit(1);
653
+ }
654
+
655
+ const filePath = args[0];
656
+ if (!filePath) {
657
+ print.error('Backup file path required');
658
+ print.info('Example: coinpay wallet decrypt-backup wallet_wid-abc_seedphrase.txt.gpg');
659
+ return;
660
+ }
661
+
662
+ if (!existsSync(filePath)) {
663
+ print.error(`File not found: ${filePath}`);
664
+ return;
665
+ }
666
+
667
+ // Get password
668
+ let password = flags.password;
669
+ if (!password) {
670
+ if (!process.stdin.isTTY) {
671
+ print.error('Required: --password <pass> (cannot prompt in pipe mode)');
672
+ return;
673
+ }
674
+ password = await promptPassword('Decryption password: ');
675
+ }
676
+
677
+ try {
678
+ const passFile = join(tmpdir(), `coinpay-pass-${Date.now()}`);
679
+ writeFileSync(passFile, password, { mode: 0o600 });
680
+ let result;
681
+ try {
682
+ result = execSync(
683
+ `gpg --batch --yes --passphrase-file "${passFile}" --pinentry-mode loopback --decrypt "${filePath}"`,
684
+ { stdio: ['pipe', 'pipe', 'pipe'] }
685
+ );
686
+ } finally {
687
+ try { writeFileSync(passFile, Buffer.alloc(password.length, 0)); } catch {}
688
+ try { const { unlinkSync: u } = await import('fs'); u(passFile); } catch {}
689
+ }
690
+
691
+ const output = result.toString();
692
+ // Extract just the mnemonic (skip comments)
693
+ const lines = output.split('\n');
694
+ const mnemonic = lines
695
+ .filter((l) => !l.startsWith('#') && l.trim().length > 0)
696
+ .join(' ')
697
+ .trim();
698
+
699
+ if (flags.json) {
700
+ print.json({ mnemonic, raw: output });
701
+ } else {
702
+ print.success('Backup decrypted successfully');
703
+ console.log(`\n${colors.bright}Seed Phrase:${colors.reset}`);
704
+ console.log(`${colors.yellow}${mnemonic}${colors.reset}\n`);
705
+ print.warn('This is sensitive data — do not share it with anyone.');
706
+ }
707
+ } catch (err) {
708
+ print.error('Decryption failed — wrong password or corrupted file');
709
+ }
710
+ break;
711
+ }
712
+
713
+ default:
714
+ print.error(`Unknown wallet command: ${subcommand}`);
715
+ print.info('Available: backup-seed, decrypt-backup');
716
+ }
717
+ }
718
+
456
719
  /**
457
720
  * Main entry point
458
721
  */
@@ -491,6 +754,10 @@ async function main() {
491
754
  case 'webhook':
492
755
  await handleWebhook(subcommand, args, flags);
493
756
  break;
757
+
758
+ case 'wallet':
759
+ await handleWallet(subcommand, args, flags);
760
+ break;
494
761
 
495
762
  default:
496
763
  print.error(`Unknown command: ${command}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@profullstack/coinpay",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
4
4
  "description": "CoinPay SDK & CLI — Accept cryptocurrency payments (BTC, ETH, SOL, POL, BCH, USDC) in your Node.js application",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",