lpgp 0.2.0

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 (48) hide show
  1. package/LICENSE +52 -0
  2. package/README.md +218 -0
  3. package/dist/config.d.ts +17 -0
  4. package/dist/config.d.ts.map +1 -0
  5. package/dist/config.js +31 -0
  6. package/dist/config.js.map +1 -0
  7. package/dist/db.d.ts +80 -0
  8. package/dist/db.d.ts.map +1 -0
  9. package/dist/db.js +284 -0
  10. package/dist/db.js.map +1 -0
  11. package/dist/decrypt.d.ts +2 -0
  12. package/dist/decrypt.d.ts.map +1 -0
  13. package/dist/decrypt.js +25 -0
  14. package/dist/decrypt.js.map +1 -0
  15. package/dist/encrypt.d.ts +2 -0
  16. package/dist/encrypt.d.ts.map +1 -0
  17. package/dist/encrypt.js +18 -0
  18. package/dist/encrypt.js.map +1 -0
  19. package/dist/key-manager.d.ts +119 -0
  20. package/dist/key-manager.d.ts.map +1 -0
  21. package/dist/key-manager.js +1235 -0
  22. package/dist/key-manager.js.map +1 -0
  23. package/dist/key-utils.d.ts +47 -0
  24. package/dist/key-utils.d.ts.map +1 -0
  25. package/dist/key-utils.js +199 -0
  26. package/dist/key-utils.js.map +1 -0
  27. package/dist/keychain.d.ts +22 -0
  28. package/dist/keychain.d.ts.map +1 -0
  29. package/dist/keychain.js +73 -0
  30. package/dist/keychain.js.map +1 -0
  31. package/dist/pgp-tool.d.ts +3 -0
  32. package/dist/pgp-tool.d.ts.map +1 -0
  33. package/dist/pgp-tool.js +1061 -0
  34. package/dist/pgp-tool.js.map +1 -0
  35. package/dist/prompts.d.ts +11 -0
  36. package/dist/prompts.d.ts.map +1 -0
  37. package/dist/prompts.js +109 -0
  38. package/dist/prompts.js.map +1 -0
  39. package/dist/schema.sql +86 -0
  40. package/dist/system-keys.d.ts +32 -0
  41. package/dist/system-keys.d.ts.map +1 -0
  42. package/dist/system-keys.js +123 -0
  43. package/dist/system-keys.js.map +1 -0
  44. package/dist/ui.d.ts +94 -0
  45. package/dist/ui.d.ts.map +1 -0
  46. package/dist/ui.js +175 -0
  47. package/dist/ui.js.map +1 -0
  48. package/package.json +56 -0
@@ -0,0 +1,1235 @@
1
+ import inquirer from 'inquirer';
2
+ import * as readline from 'readline';
3
+ import * as openpgp from 'openpgp';
4
+ import { extractPublicKeyInfo, extractPrivateKeyInfo, verifyKeyPair, formatKeypairInfo, obfuscateEmail, } from './key-utils.js';
5
+ import { isGpgInstalled, listGpgKeys, exportGpgPublicKey, exportGpgSecretKey, getGpgHomeDir, } from './system-keys.js';
6
+ import { escapeablePrompt } from './prompts.js';
7
+ import { hasStoredPassphrase, deleteStoredPassphrase, } from './keychain.js';
8
+ import { colors, icons, printBanner, printSectionHeader, printDivider, showSuccess, showError, showWarning, showLoading, promptMessage, mainMenuChoice, backChoice, exitChoice, cancelChoice, showKeyValue, } from './ui.js';
9
+ export class KeyManager {
10
+ db;
11
+ constructor(db) {
12
+ this.db = db;
13
+ }
14
+ /**
15
+ * Check if there's a default keypair configured
16
+ */
17
+ hasDefaultKeypair() {
18
+ const keypairs = this.db.select({
19
+ table: 'keypair',
20
+ where: { key: 'is_default', compare: 'is', value: 1 },
21
+ });
22
+ return keypairs.length > 0;
23
+ }
24
+ /**
25
+ * Get the default keypair
26
+ */
27
+ getDefaultKeypair() {
28
+ const keypairs = this.db.select({
29
+ table: 'keypair',
30
+ where: { key: 'is_default', compare: 'is', value: 1 },
31
+ });
32
+ return keypairs[0] || null;
33
+ }
34
+ /**
35
+ * Prompt user to set up their first keypair
36
+ */
37
+ async setupFirstKeypair() {
38
+ printSectionHeader('First Time Setup');
39
+ showWarning('No PGP keypair found. You need to set up a keypair to use this tool.');
40
+ console.log();
41
+ // Check if GPG is available to offer system import
42
+ const gpgAvailable = isGpgInstalled();
43
+ const choices = [
44
+ { name: `${icons.import} Import existing keypair`, value: 'import' },
45
+ ];
46
+ if (gpgAvailable) {
47
+ choices.push({ name: `${icons.gpg} Import from system GPG`, value: 'import-gpg' });
48
+ }
49
+ choices.push({ name: `${icons.generate} Generate new keypair`, value: 'generate' }, new inquirer.Separator(), exitChoice());
50
+ const { action } = await escapeablePrompt([
51
+ {
52
+ type: 'list',
53
+ name: 'action',
54
+ message: promptMessage('What would you like to do?'),
55
+ choices,
56
+ },
57
+ ]);
58
+ if (action === 'exit') {
59
+ console.log(colors.muted('\nGoodbye!\n'));
60
+ process.exit(0);
61
+ }
62
+ if (action === 'import') {
63
+ await this.importKeypair(true);
64
+ }
65
+ else if (action === 'import-gpg') {
66
+ await this.importFromSystemGpg();
67
+ }
68
+ else if (action === 'generate') {
69
+ await this.generateKeypair(true);
70
+ }
71
+ }
72
+ /**
73
+ * Import a keypair (public + private keys)
74
+ */
75
+ async importKeypair(setAsDefault = false) {
76
+ printSectionHeader('Import Keypair');
77
+ // Check clipboard for keys
78
+ let clipboardContent = '';
79
+ let hasPublicInClipboard = false;
80
+ let hasPrivateInClipboard = false;
81
+ try {
82
+ const clipboardy = (await import('clipboardy')).default;
83
+ clipboardContent = await clipboardy.read();
84
+ hasPublicInClipboard = clipboardContent.includes('BEGIN PGP PUBLIC KEY BLOCK');
85
+ hasPrivateInClipboard = clipboardContent.includes('BEGIN PGP PRIVATE KEY BLOCK');
86
+ }
87
+ catch (e) {
88
+ // Clipboard not available, continue without it
89
+ }
90
+ // Prompt for keypair name
91
+ const { name } = await escapeablePrompt([
92
+ {
93
+ type: 'input',
94
+ name: 'name',
95
+ message: promptMessage('Keypair name (e.g., "Personal", "Work"):'),
96
+ default: 'Personal',
97
+ validate: (input) => input.trim().length > 0 || 'Name cannot be empty',
98
+ },
99
+ ]);
100
+ // Get public and private keys
101
+ let publicKey = '';
102
+ let privateKey = '';
103
+ let usedBothFromClipboard = false;
104
+ // Check if both keys are in clipboard
105
+ if (hasPublicInClipboard && hasPrivateInClipboard) {
106
+ const { useClipboard } = await escapeablePrompt([
107
+ {
108
+ type: 'confirm',
109
+ name: 'useClipboard',
110
+ message: 'Both public and private keys detected in clipboard. Use them?',
111
+ default: true,
112
+ },
113
+ ]);
114
+ if (useClipboard) {
115
+ // Extract both keys from clipboard
116
+ const publicMatch = clipboardContent.match(/-----BEGIN PGP PUBLIC KEY BLOCK-----[\s\S]*?-----END PGP PUBLIC KEY BLOCK-----/);
117
+ const privateMatch = clipboardContent.match(/-----BEGIN PGP PRIVATE KEY BLOCK-----[\s\S]*?-----END PGP PRIVATE KEY BLOCK-----/);
118
+ if (publicMatch) {
119
+ publicKey = publicMatch[0];
120
+ }
121
+ if (privateMatch) {
122
+ privateKey = privateMatch[0];
123
+ }
124
+ usedBothFromClipboard = true;
125
+ }
126
+ }
127
+ // Get public key if not already extracted
128
+ if (!publicKey && hasPublicInClipboard) {
129
+ const { useClipboard } = await escapeablePrompt([
130
+ {
131
+ type: 'confirm',
132
+ name: 'useClipboard',
133
+ message: 'Public key detected in clipboard. Use it?',
134
+ default: true,
135
+ },
136
+ ]);
137
+ if (useClipboard) {
138
+ const publicMatch = clipboardContent.match(/-----BEGIN PGP PUBLIC KEY BLOCK-----[\s\S]*?-----END PGP PUBLIC KEY BLOCK-----/);
139
+ if (publicMatch) {
140
+ publicKey = publicMatch[0];
141
+ }
142
+ }
143
+ }
144
+ if (!publicKey) {
145
+ console.log(promptMessage('\nPaste your PGP PUBLIC key:'));
146
+ console.log(colors.muted('(Press Enter to finish, or press Enter then Ctrl+D)'));
147
+ publicKey = await this.readKeyInput();
148
+ }
149
+ // Validate public key format
150
+ if (!publicKey.includes('BEGIN PGP PUBLIC KEY BLOCK')) {
151
+ console.log();
152
+ showError('Invalid public key format');
153
+ console.log();
154
+ return;
155
+ }
156
+ // Get private key if not already extracted
157
+ if (!privateKey && hasPrivateInClipboard) {
158
+ const { useClipboard } = await escapeablePrompt([
159
+ {
160
+ type: 'confirm',
161
+ name: 'useClipboard',
162
+ message: 'Private key detected in clipboard. Use it?',
163
+ default: true,
164
+ },
165
+ ]);
166
+ if (useClipboard) {
167
+ const privateMatch = clipboardContent.match(/-----BEGIN PGP PRIVATE KEY BLOCK-----[\s\S]*?-----END PGP PRIVATE KEY BLOCK-----/);
168
+ if (privateMatch) {
169
+ privateKey = privateMatch[0];
170
+ }
171
+ }
172
+ }
173
+ if (!privateKey) {
174
+ console.log(promptMessage('\nPaste your PGP PRIVATE key:'));
175
+ console.log(colors.muted('(Press Enter to finish, or press Enter then Ctrl+D)'));
176
+ privateKey = await this.readKeyInput();
177
+ }
178
+ // Validate private key format
179
+ if (!privateKey.includes('BEGIN PGP PRIVATE KEY BLOCK')) {
180
+ console.log();
181
+ showError('Invalid private key format');
182
+ console.log();
183
+ return;
184
+ }
185
+ // Verify keys match
186
+ console.log();
187
+ showLoading('Verifying keypair...');
188
+ const keysMatch = await verifyKeyPair(publicKey, privateKey);
189
+ if (!keysMatch) {
190
+ console.log();
191
+ showError('Public and private keys do not match. Please try again.');
192
+ console.log();
193
+ return;
194
+ }
195
+ // Prompt for passphrase if key is encrypted
196
+ const { passphrase } = await escapeablePrompt([
197
+ {
198
+ type: 'password',
199
+ name: 'passphrase',
200
+ message: promptMessage('Enter passphrase for private key (leave empty if none):'),
201
+ mask: '*',
202
+ },
203
+ ]);
204
+ // Extract key information
205
+ try {
206
+ const keyInfo = await extractPrivateKeyInfo(privateKey, passphrase || undefined);
207
+ console.log();
208
+ showSuccess('Keypair verified!');
209
+ console.log();
210
+ console.log(colors.muted('Key Information:'));
211
+ showKeyValue(' Email', keyInfo.email);
212
+ showKeyValue(' Fingerprint', keyInfo.fingerprint);
213
+ showKeyValue(' Algorithm', `${keyInfo.algorithm} (${keyInfo.keySize})`);
214
+ showKeyValue(' Passphrase Protected', keyInfo.passphraseProtected ? 'Yes' : 'No');
215
+ console.log();
216
+ // Check if default should be set
217
+ let makeDefault = setAsDefault;
218
+ if (!setAsDefault) {
219
+ const { setDefault } = await escapeablePrompt([
220
+ {
221
+ type: 'confirm',
222
+ name: 'setDefault',
223
+ message: 'Set this as your default keypair?',
224
+ default: true,
225
+ },
226
+ ]);
227
+ makeDefault = setDefault;
228
+ }
229
+ // If setting as default, unset current default
230
+ if (makeDefault) {
231
+ const currentDefaults = this.db.select({
232
+ table: 'keypair',
233
+ where: { key: 'is_default', compare: 'is', value: 1 },
234
+ });
235
+ for (const kp of currentDefaults) {
236
+ this.db.update('keypair', { key: 'id', value: kp.id }, { is_default: false });
237
+ }
238
+ }
239
+ // Save to database
240
+ this.db.insert('keypair', {
241
+ name: name.trim(),
242
+ email: keyInfo.email,
243
+ fingerprint: keyInfo.fingerprint,
244
+ public_key: publicKey,
245
+ private_key: privateKey,
246
+ passphrase_protected: keyInfo.passphraseProtected,
247
+ algorithm: keyInfo.algorithm,
248
+ key_size: keyInfo.keySize,
249
+ can_sign: keyInfo.canSign,
250
+ can_encrypt: keyInfo.canEncrypt,
251
+ can_certify: keyInfo.canCertify,
252
+ can_authenticate: keyInfo.canAuthenticate,
253
+ expires_at: keyInfo.expiresAt,
254
+ revoked: false,
255
+ revocation_reason: null,
256
+ last_used_at: null,
257
+ is_default: makeDefault,
258
+ });
259
+ console.log();
260
+ showSuccess('Keypair imported successfully!');
261
+ console.log();
262
+ }
263
+ catch (error) {
264
+ console.log();
265
+ showError(`Error importing keypair: ${error}`);
266
+ console.log();
267
+ }
268
+ }
269
+ /**
270
+ * Generate a new PGP keypair
271
+ */
272
+ async generateKeypair(setAsDefault = false) {
273
+ printSectionHeader('Generate New Keypair');
274
+ // Prompt for keypair details
275
+ const { name: userName } = await escapeablePrompt([
276
+ {
277
+ type: 'input',
278
+ name: 'name',
279
+ message: promptMessage('Your name:'),
280
+ validate: (input) => input.trim().length > 0 || 'Name cannot be empty',
281
+ },
282
+ ]);
283
+ const { email } = await escapeablePrompt([
284
+ {
285
+ type: 'input',
286
+ name: 'email',
287
+ message: promptMessage('Your email:'),
288
+ validate: (input) => {
289
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
290
+ return emailRegex.test(input) || 'Please enter a valid email address';
291
+ },
292
+ },
293
+ ]);
294
+ const { keypairName } = await escapeablePrompt([
295
+ {
296
+ type: 'input',
297
+ name: 'keypairName',
298
+ message: promptMessage('Keypair name (e.g., "Personal", "Work"):'),
299
+ default: 'Personal',
300
+ validate: (input) => input.trim().length > 0 || 'Name cannot be empty',
301
+ },
302
+ ]);
303
+ const { passphrase } = await escapeablePrompt([
304
+ {
305
+ type: 'password',
306
+ name: 'passphrase',
307
+ message: promptMessage('Enter a passphrase to protect your private key:'),
308
+ mask: '*',
309
+ validate: (input) => input.length >= 8 || 'Passphrase must be at least 8 characters',
310
+ },
311
+ ]);
312
+ const { passphraseConfirm } = await escapeablePrompt([
313
+ {
314
+ type: 'password',
315
+ name: 'passphraseConfirm',
316
+ message: promptMessage('Confirm passphrase:'),
317
+ mask: '*',
318
+ validate: (input) => input === passphrase || 'Passphrases do not match',
319
+ },
320
+ ]);
321
+ console.log();
322
+ showLoading('Generating keypair... (this may take a moment)');
323
+ console.log();
324
+ try {
325
+ // Generate the keypair
326
+ const { privateKey, publicKey } = await openpgp.generateKey({
327
+ type: 'rsa',
328
+ rsaBits: 4096,
329
+ userIDs: [{ name: userName, email: email }],
330
+ passphrase: passphrase,
331
+ format: 'armored',
332
+ });
333
+ // Extract key information
334
+ const publicKeyInfo = await extractPublicKeyInfo(publicKey);
335
+ const privateKeyInfo = await extractPrivateKeyInfo(privateKey, passphrase);
336
+ // Store in database
337
+ const keypair = {
338
+ name: keypairName.trim(),
339
+ email: publicKeyInfo.email,
340
+ fingerprint: publicKeyInfo.fingerprint,
341
+ public_key: publicKey,
342
+ private_key: privateKey,
343
+ passphrase_protected: true,
344
+ algorithm: publicKeyInfo.algorithm,
345
+ key_size: publicKeyInfo.keySize,
346
+ can_sign: publicKeyInfo.canSign,
347
+ can_encrypt: publicKeyInfo.canEncrypt,
348
+ can_certify: publicKeyInfo.canCertify,
349
+ can_authenticate: publicKeyInfo.canAuthenticate,
350
+ expires_at: publicKeyInfo.expiresAt,
351
+ revoked: false,
352
+ revocation_reason: null,
353
+ last_used_at: null,
354
+ is_default: setAsDefault,
355
+ };
356
+ // If setting as default, unset all other defaults
357
+ if (setAsDefault) {
358
+ const allKeypairs = this.db.select({ table: 'keypair' });
359
+ for (const kp of allKeypairs) {
360
+ this.db.update('keypair', { key: 'id', value: kp.id }, { is_default: false });
361
+ }
362
+ }
363
+ this.db.insert('keypair', keypair);
364
+ showSuccess('Keypair generated successfully!');
365
+ console.log();
366
+ console.log(colors.infoBold('Keypair details:'));
367
+ printDivider();
368
+ showKeyValue('Name', keypairName);
369
+ showKeyValue('Email', publicKeyInfo.email);
370
+ showKeyValue('Fingerprint', publicKeyInfo.fingerprint);
371
+ showKeyValue('Algorithm', `${publicKeyInfo.algorithm} (${publicKeyInfo.keySize} bits)`);
372
+ printDivider();
373
+ console.log();
374
+ }
375
+ catch (error) {
376
+ console.log();
377
+ showError(`Failed to generate keypair: ${error instanceof Error ? error.message : error}`);
378
+ }
379
+ }
380
+ /**
381
+ * Import a keypair from system GPG
382
+ */
383
+ async importFromSystemGpg() {
384
+ printSectionHeader('Import from System GPG');
385
+ // Check if GPG is installed
386
+ if (!isGpgInstalled()) {
387
+ showWarning('GPG is not installed on this system.');
388
+ console.log(colors.muted('Install GPG to import keys from your system keyring.'));
389
+ console.log();
390
+ return;
391
+ }
392
+ const gpgHome = getGpgHomeDir();
393
+ if (gpgHome) {
394
+ console.log(colors.muted(`GPG directory found: ${gpgHome}\n`));
395
+ }
396
+ // List available keys
397
+ const { secretKeys } = listGpgKeys();
398
+ if (secretKeys.length === 0) {
399
+ showWarning('No secret keys found in your GPG keyring.');
400
+ console.log(colors.muted('Generate or import keys into GPG first using:'));
401
+ console.log(colors.muted(' gpg --gen-key'));
402
+ console.log();
403
+ return;
404
+ }
405
+ console.log(colors.infoBold('Available GPG keys:\n'));
406
+ secretKeys.forEach((key, index) => {
407
+ console.log(colors.muted(`${index + 1}. ${key.name} <${key.email}>`));
408
+ console.log(colors.muted(` Fingerprint: ${key.fingerprint}`));
409
+ console.log();
410
+ });
411
+ // Prompt user to select a key
412
+ const { selectedIndex } = await escapeablePrompt([
413
+ {
414
+ type: 'list',
415
+ name: 'selectedIndex',
416
+ message: promptMessage('Select a key to import:'),
417
+ choices: [
418
+ ...secretKeys.map((key, index) => ({
419
+ name: `${icons.key} ${key.name} ${colors.muted(`<${key.email}>`)}`,
420
+ value: index,
421
+ })),
422
+ new inquirer.Separator(),
423
+ cancelChoice(),
424
+ mainMenuChoice(),
425
+ new inquirer.Separator(),
426
+ ],
427
+ },
428
+ ]);
429
+ if (selectedIndex === -1 || selectedIndex === 'cancel' || selectedIndex === 'main-menu') {
430
+ return;
431
+ }
432
+ const selectedKey = secretKeys[selectedIndex];
433
+ if (!selectedKey) {
434
+ showError('Invalid key selection');
435
+ console.log();
436
+ return;
437
+ }
438
+ // Export the keys
439
+ console.log();
440
+ showLoading('Exporting keys from GPG...');
441
+ console.log();
442
+ const publicKey = exportGpgPublicKey(selectedKey.fingerprint);
443
+ const privateKey = exportGpgSecretKey(selectedKey.fingerprint);
444
+ if (!publicKey || !privateKey) {
445
+ showError('Failed to export keys from GPG');
446
+ console.log();
447
+ return;
448
+ }
449
+ // Prompt for keypair name
450
+ const { name } = await escapeablePrompt([
451
+ {
452
+ type: 'input',
453
+ name: 'name',
454
+ message: promptMessage('Keypair name:'),
455
+ default: selectedKey.name || 'Imported from GPG',
456
+ validate: (input) => input.trim().length > 0 || 'Name cannot be empty',
457
+ },
458
+ ]);
459
+ // Prompt for passphrase
460
+ const { passphrase } = await escapeablePrompt([
461
+ {
462
+ type: 'password',
463
+ name: 'passphrase',
464
+ message: promptMessage('Enter GPG key passphrase (if any, leave empty if none):'),
465
+ mask: '*',
466
+ },
467
+ ]);
468
+ // Extract key information
469
+ try {
470
+ const keyInfo = await extractPrivateKeyInfo(privateKey, passphrase || undefined);
471
+ console.log();
472
+ showSuccess('Key exported successfully!');
473
+ console.log();
474
+ console.log(colors.muted('Key Information:'));
475
+ showKeyValue(' Email', keyInfo.email);
476
+ showKeyValue(' Fingerprint', keyInfo.fingerprint);
477
+ showKeyValue(' Algorithm', `${keyInfo.algorithm} (${keyInfo.keySize})`);
478
+ showKeyValue(' Passphrase Protected', keyInfo.passphraseProtected ? 'Yes' : 'No');
479
+ console.log();
480
+ // Check if default should be set
481
+ const { setDefault } = await escapeablePrompt([
482
+ {
483
+ type: 'confirm',
484
+ name: 'setDefault',
485
+ message: 'Set this as your default keypair?',
486
+ default: true,
487
+ },
488
+ ]);
489
+ // If setting as default, unset current default
490
+ if (setDefault) {
491
+ const currentDefaults = this.db.select({
492
+ table: 'keypair',
493
+ where: { key: 'is_default', compare: 'is', value: 1 },
494
+ });
495
+ for (const kp of currentDefaults) {
496
+ this.db.update('keypair', { key: 'id', value: kp.id }, { is_default: false });
497
+ }
498
+ }
499
+ // Save to database
500
+ this.db.insert('keypair', {
501
+ name: name.trim(),
502
+ email: keyInfo.email,
503
+ fingerprint: keyInfo.fingerprint,
504
+ public_key: publicKey,
505
+ private_key: privateKey,
506
+ passphrase_protected: keyInfo.passphraseProtected,
507
+ algorithm: keyInfo.algorithm,
508
+ key_size: keyInfo.keySize,
509
+ can_sign: keyInfo.canSign,
510
+ can_encrypt: keyInfo.canEncrypt,
511
+ can_certify: keyInfo.canCertify,
512
+ can_authenticate: keyInfo.canAuthenticate,
513
+ expires_at: keyInfo.expiresAt,
514
+ revoked: false,
515
+ revocation_reason: null,
516
+ last_used_at: null,
517
+ is_default: setDefault,
518
+ });
519
+ console.log();
520
+ showSuccess('Keypair imported from GPG successfully!');
521
+ console.log();
522
+ }
523
+ catch (error) {
524
+ console.log();
525
+ showError(`Error importing keypair: ${error}`);
526
+ console.log();
527
+ }
528
+ }
529
+ /**
530
+ * List all keypairs
531
+ */
532
+ listKeypairs() {
533
+ const keypairs = this.db.select({ table: 'keypair' });
534
+ if (keypairs.length === 0) {
535
+ console.log();
536
+ showWarning('No keypairs found.');
537
+ console.log();
538
+ return;
539
+ }
540
+ console.log(colors.infoBold('\nYour Keypairs:\n'));
541
+ for (const keypair of keypairs) {
542
+ printDivider();
543
+ console.log(formatKeypairInfo(keypair));
544
+ }
545
+ printDivider();
546
+ console.log();
547
+ }
548
+ /**
549
+ * Show key management menu
550
+ */
551
+ async showKeyManagementMenu() {
552
+ const { action } = await escapeablePrompt([
553
+ {
554
+ type: 'list',
555
+ name: 'action',
556
+ message: promptMessage('Key Management'),
557
+ choices: [
558
+ { name: `${icons.key} View/manage my keys`, value: 'view' },
559
+ { name: `${icons.contact} View/manage contacts`, value: 'contacts' },
560
+ { name: `${icons.import} Import keypair`, value: 'import' },
561
+ { name: `${icons.gpg} Import from system GPG`, value: 'import-gpg' },
562
+ { name: `${icons.generate} Generate new keypair`, value: 'generate' },
563
+ new inquirer.Separator(),
564
+ mainMenuChoice(),
565
+ ],
566
+ },
567
+ ]);
568
+ switch (action) {
569
+ case 'view':
570
+ const viewResult = await this.viewAndManageKeys();
571
+ if (viewResult === 'main-menu')
572
+ return 'main-menu';
573
+ await this.showKeyManagementMenu();
574
+ break;
575
+ case 'contacts':
576
+ const contactsResult = await this.viewAndManageContacts();
577
+ if (contactsResult === 'main-menu')
578
+ return 'main-menu';
579
+ await this.showKeyManagementMenu();
580
+ break;
581
+ case 'import':
582
+ await this.importKeypair();
583
+ await this.showKeyManagementMenu();
584
+ break;
585
+ case 'import-gpg':
586
+ await this.importFromSystemGpg();
587
+ await this.showKeyManagementMenu();
588
+ break;
589
+ case 'generate':
590
+ await this.generateKeypair();
591
+ await this.showKeyManagementMenu();
592
+ break;
593
+ case 'back':
594
+ case 'main-menu':
595
+ return;
596
+ }
597
+ }
598
+ /**
599
+ * View and manage individual keys
600
+ */
601
+ async viewAndManageKeys() {
602
+ const keypairs = this.db.select({ table: 'keypair' });
603
+ if (keypairs.length === 0) {
604
+ console.log();
605
+ showWarning('No keypairs found.');
606
+ console.log();
607
+ return;
608
+ }
609
+ const { keypairId } = await escapeablePrompt([
610
+ {
611
+ type: 'list',
612
+ name: 'keypairId',
613
+ message: promptMessage('Select a key to manage:'),
614
+ choices: [
615
+ ...keypairs.map((kp) => ({
616
+ name: `${icons.key} ${kp.name} ${colors.muted(`- ${obfuscateEmail(kp.email)}`)}${kp.is_default ? ` ${icons.default} Default` : ''}`,
617
+ value: kp.id,
618
+ })),
619
+ new inquirer.Separator(),
620
+ backChoice(),
621
+ mainMenuChoice(),
622
+ new inquirer.Separator(),
623
+ ],
624
+ },
625
+ ]);
626
+ if (keypairId === 'main-menu') {
627
+ return 'main-menu';
628
+ }
629
+ if (keypairId === null || keypairId === 'back') {
630
+ return;
631
+ }
632
+ const selectedKeypair = keypairs.find((kp) => kp.id === keypairId);
633
+ if (!selectedKeypair)
634
+ return;
635
+ const result = await this.manageIndividualKey(selectedKeypair);
636
+ if (result === 'main-menu')
637
+ return 'main-menu';
638
+ }
639
+ /**
640
+ * Manage an individual key
641
+ */
642
+ async manageIndividualKey(keypair) {
643
+ // Display key information
644
+ printSectionHeader('Key Details');
645
+ console.log(formatKeypairInfo(keypair));
646
+ console.log();
647
+ // Check if passphrase is stored in keychain
648
+ const hasStoredPw = keypair.passphrase_protected
649
+ ? await hasStoredPassphrase(keypair.fingerprint)
650
+ : false;
651
+ // Build menu choices dynamically
652
+ const choices = [
653
+ { name: `${icons.copy} Copy public key`, value: 'copy-public' },
654
+ { name: `${icons.export} Export keypair`, value: 'export' },
655
+ { name: `${icons.edit} Rename key`, value: 'rename' },
656
+ { name: `${icons.key} Set as default`, value: 'set-default' },
657
+ ];
658
+ // Add passphrase management option if applicable
659
+ if (hasStoredPw) {
660
+ choices.push({ name: `${icons.unlocked} Clear saved passphrase ${colors.muted('(from keychain)')}`, value: 'clear-passphrase' });
661
+ }
662
+ choices.push({ name: `${icons.exit} Delete key`, value: 'delete' }, new inquirer.Separator(), backChoice('Back to key list'), mainMenuChoice());
663
+ const { action } = await escapeablePrompt([
664
+ {
665
+ type: 'list',
666
+ name: 'action',
667
+ message: promptMessage('What would you like to do?'),
668
+ choices,
669
+ },
670
+ ]);
671
+ switch (action) {
672
+ case 'copy-public':
673
+ await this.copyPublicKey(keypair);
674
+ return this.manageIndividualKey(keypair);
675
+ case 'export':
676
+ await this.exportKeypair(keypair);
677
+ return this.manageIndividualKey(keypair);
678
+ case 'rename':
679
+ await this.renameKeypair(keypair);
680
+ // Refresh keypair data after rename
681
+ const updated = this.db.select({
682
+ table: 'keypair',
683
+ where: { key: 'id', compare: 'is', value: keypair.id },
684
+ })[0];
685
+ if (updated)
686
+ return this.manageIndividualKey(updated);
687
+ break;
688
+ case 'set-default':
689
+ await this.setDefaultKeypairById(keypair.id);
690
+ // Refresh keypair data
691
+ const refreshed = this.db.select({
692
+ table: 'keypair',
693
+ where: { key: 'id', compare: 'is', value: keypair.id },
694
+ })[0];
695
+ if (refreshed)
696
+ return this.manageIndividualKey(refreshed);
697
+ break;
698
+ case 'clear-passphrase':
699
+ await this.clearStoredPassphrase(keypair);
700
+ return this.manageIndividualKey(keypair);
701
+ case 'delete':
702
+ const deleted = await this.deleteKeypairById(keypair.id);
703
+ if (!deleted) {
704
+ return this.manageIndividualKey(keypair);
705
+ }
706
+ break;
707
+ case 'main-menu':
708
+ return 'main-menu';
709
+ case 'back':
710
+ return;
711
+ }
712
+ }
713
+ /**
714
+ * Copy public key to clipboard
715
+ */
716
+ async copyPublicKey(keypair) {
717
+ console.clear();
718
+ printBanner();
719
+ console.log(colors.successBold('Public Key:\n'));
720
+ printDivider();
721
+ console.log(keypair.public_key);
722
+ printDivider();
723
+ try {
724
+ const clipboardy = (await import('clipboardy')).default;
725
+ await clipboardy.write(keypair.public_key);
726
+ console.log();
727
+ showSuccess('Public key copied to clipboard');
728
+ console.log();
729
+ }
730
+ catch (error) {
731
+ console.log();
732
+ showWarning('Clipboard unavailable');
733
+ console.log();
734
+ }
735
+ }
736
+ /**
737
+ * Export keypair to files
738
+ */
739
+ async exportKeypair(keypair) {
740
+ const { exportType } = await escapeablePrompt([
741
+ {
742
+ type: 'list',
743
+ name: 'exportType',
744
+ message: promptMessage('What would you like to export?'),
745
+ choices: [
746
+ { name: `${icons.view} Public key only`, value: 'public' },
747
+ { name: `${icons.copy} Both public and private keys`, value: 'both' },
748
+ new inquirer.Separator(),
749
+ cancelChoice(),
750
+ mainMenuChoice(),
751
+ ],
752
+ },
753
+ ]);
754
+ if (exportType === 'cancel' || exportType === 'main-menu')
755
+ return;
756
+ const { exportMethod } = await escapeablePrompt([
757
+ {
758
+ type: 'list',
759
+ name: 'exportMethod',
760
+ message: promptMessage('How would you like to export?'),
761
+ choices: [
762
+ { name: `${icons.clipboard} Copy to clipboard`, value: 'clipboard' },
763
+ { name: `${icons.view} Display on screen`, value: 'display' },
764
+ new inquirer.Separator(),
765
+ cancelChoice(),
766
+ mainMenuChoice(),
767
+ ],
768
+ },
769
+ ]);
770
+ if (exportMethod === 'cancel' || exportMethod === 'main-menu')
771
+ return;
772
+ let content = '';
773
+ if (exportType === 'public') {
774
+ content = keypair.public_key;
775
+ }
776
+ else {
777
+ content = `PUBLIC KEY:\n${keypair.public_key}\n\nPRIVATE KEY:\n${keypair.private_key}`;
778
+ }
779
+ console.clear();
780
+ printBanner();
781
+ const label = exportType === 'public' ? 'Public Key' : 'Keypair';
782
+ console.log(colors.successBold(`Exported ${label}:\n`));
783
+ printDivider();
784
+ console.log(content);
785
+ printDivider();
786
+ if (exportMethod === 'clipboard') {
787
+ try {
788
+ const clipboardy = (await import('clipboardy')).default;
789
+ await clipboardy.write(content);
790
+ console.log();
791
+ showSuccess(`${label} copied to clipboard`);
792
+ console.log();
793
+ }
794
+ catch (error) {
795
+ console.log();
796
+ showWarning('Clipboard unavailable');
797
+ console.log();
798
+ }
799
+ }
800
+ else {
801
+ console.log();
802
+ }
803
+ }
804
+ /**
805
+ * Rename a keypair
806
+ */
807
+ async renameKeypair(keypair) {
808
+ const { newName } = await escapeablePrompt([
809
+ {
810
+ type: 'input',
811
+ name: 'newName',
812
+ message: promptMessage('Enter new name:'),
813
+ default: keypair.name,
814
+ validate: (input) => input.trim().length > 0 || 'Name cannot be empty',
815
+ },
816
+ ]);
817
+ this.db.update('keypair', { key: 'id', value: keypair.id }, { name: newName.trim() });
818
+ console.log();
819
+ showSuccess('Keypair renamed!');
820
+ console.log();
821
+ }
822
+ /**
823
+ * Set a keypair as default by ID
824
+ */
825
+ async setDefaultKeypairById(keypairId) {
826
+ const keypairs = this.db.select({ table: 'keypair' });
827
+ // Unset all defaults
828
+ for (const kp of keypairs) {
829
+ this.db.update('keypair', { key: 'id', value: kp.id }, { is_default: false });
830
+ }
831
+ // Set new default
832
+ this.db.update('keypair', { key: 'id', value: keypairId }, { is_default: true });
833
+ console.log();
834
+ showSuccess('Set as default keypair!');
835
+ console.log();
836
+ }
837
+ /**
838
+ * Clear stored passphrase from system keychain
839
+ */
840
+ async clearStoredPassphrase(keypair) {
841
+ const { confirm } = await escapeablePrompt([
842
+ {
843
+ type: 'confirm',
844
+ name: 'confirm',
845
+ message: promptMessage('Remove saved passphrase from system keychain?'),
846
+ default: true,
847
+ },
848
+ ]);
849
+ if (confirm) {
850
+ const deleted = await deleteStoredPassphrase(keypair.fingerprint);
851
+ if (deleted) {
852
+ console.log();
853
+ showSuccess('Saved passphrase removed from system keychain.');
854
+ console.log();
855
+ }
856
+ else {
857
+ console.log();
858
+ showWarning('Could not remove passphrase (may not exist or keychain unavailable).');
859
+ console.log();
860
+ }
861
+ }
862
+ }
863
+ /**
864
+ * Delete a keypair by ID
865
+ */
866
+ async deleteKeypairById(keypairId) {
867
+ const { confirm } = await escapeablePrompt([
868
+ {
869
+ type: 'confirm',
870
+ name: 'confirm',
871
+ message: colors.error('Are you sure? This action cannot be undone.'),
872
+ default: false,
873
+ },
874
+ ]);
875
+ if (confirm) {
876
+ this.db.delete('keypair', { key: 'id', value: keypairId });
877
+ console.log();
878
+ showSuccess('Keypair deleted.');
879
+ console.log();
880
+ return true;
881
+ }
882
+ return false;
883
+ }
884
+ /**
885
+ * Set a keypair as default
886
+ */
887
+ async setDefaultKeypair() {
888
+ const keypairs = this.db.select({ table: 'keypair' });
889
+ if (keypairs.length === 0) {
890
+ console.log();
891
+ showWarning('No keypairs available.');
892
+ console.log();
893
+ return;
894
+ }
895
+ const { keypairId } = await escapeablePrompt([
896
+ {
897
+ type: 'list',
898
+ name: 'keypairId',
899
+ message: promptMessage('Select default keypair:'),
900
+ choices: keypairs.map((kp) => ({
901
+ name: `${icons.key} ${kp.name} ${colors.muted(`(${obfuscateEmail(kp.email)})`)} ${kp.is_default ? `${icons.default} Current Default` : ''}`,
902
+ value: kp.id,
903
+ })),
904
+ },
905
+ ]);
906
+ // Unset all defaults
907
+ for (const kp of keypairs) {
908
+ this.db.update('keypair', { key: 'id', value: kp.id }, { is_default: false });
909
+ }
910
+ // Set new default
911
+ this.db.update('keypair', { key: 'id', value: keypairId }, { is_default: true });
912
+ console.log();
913
+ showSuccess('Default keypair updated!');
914
+ console.log();
915
+ }
916
+ /**
917
+ * Delete a keypair
918
+ */
919
+ async deleteKeypair() {
920
+ const keypairs = this.db.select({ table: 'keypair' });
921
+ if (keypairs.length === 0) {
922
+ console.log();
923
+ showWarning('No keypairs available.');
924
+ console.log();
925
+ return;
926
+ }
927
+ const { keypairId } = await escapeablePrompt([
928
+ {
929
+ type: 'list',
930
+ name: 'keypairId',
931
+ message: promptMessage('Select keypair to delete:'),
932
+ choices: keypairs.map((kp) => ({
933
+ name: `${icons.key} ${kp.name} ${colors.muted(`(${obfuscateEmail(kp.email)})`)}`,
934
+ value: kp.id,
935
+ })),
936
+ },
937
+ ]);
938
+ const { confirm } = await escapeablePrompt([
939
+ {
940
+ type: 'confirm',
941
+ name: 'confirm',
942
+ message: colors.error('Are you sure? This action cannot be undone.'),
943
+ default: false,
944
+ },
945
+ ]);
946
+ if (confirm) {
947
+ this.db.delete('keypair', { key: 'id', value: keypairId });
948
+ console.log();
949
+ showSuccess('Keypair deleted.');
950
+ console.log();
951
+ }
952
+ }
953
+ /**
954
+ * Read multiline input from stdin
955
+ */
956
+ async readMultilineInput() {
957
+ return new Promise((resolve) => {
958
+ const lines = [];
959
+ const rl = readline.createInterface({
960
+ input: process.stdin,
961
+ output: process.stdout,
962
+ });
963
+ rl.setPrompt('');
964
+ rl.on('line', (line) => {
965
+ lines.push(line);
966
+ });
967
+ rl.on('close', () => {
968
+ resolve(lines.join('\n'));
969
+ });
970
+ });
971
+ }
972
+ /**
973
+ * Read PGP key input with smart detection
974
+ * Allows finishing with Enter when a complete key is detected, or Ctrl+D
975
+ */
976
+ async readKeyInput() {
977
+ return new Promise((resolve) => {
978
+ const lines = [];
979
+ const rl = readline.createInterface({
980
+ input: process.stdin,
981
+ output: process.stdout,
982
+ });
983
+ rl.setPrompt('');
984
+ rl.on('line', (line) => {
985
+ lines.push(line);
986
+ const content = lines.join('\n');
987
+ // Check if we have a complete key block and current line is empty
988
+ if (line.trim() === '' &&
989
+ content.includes('-----BEGIN PGP') &&
990
+ content.includes('-----END PGP')) {
991
+ rl.close();
992
+ resolve(content.trim());
993
+ }
994
+ });
995
+ rl.on('close', () => {
996
+ resolve(lines.join('\n'));
997
+ });
998
+ });
999
+ }
1000
+ /**
1001
+ * View and manage contacts
1002
+ */
1003
+ async viewAndManageContacts() {
1004
+ const contacts = this.db.select({ table: 'contact' });
1005
+ if (contacts.length === 0) {
1006
+ console.log();
1007
+ showWarning('No contacts found.');
1008
+ console.log();
1009
+ return;
1010
+ }
1011
+ const { contactId } = await escapeablePrompt([
1012
+ {
1013
+ type: 'list',
1014
+ name: 'contactId',
1015
+ message: promptMessage('Select a contact to manage:'),
1016
+ choices: [
1017
+ ...contacts.map((c) => ({
1018
+ name: `${icons.contact} ${c.name} ${colors.muted(`- ${c.email}`)}`,
1019
+ value: c.id,
1020
+ })),
1021
+ new inquirer.Separator(),
1022
+ backChoice(),
1023
+ mainMenuChoice(),
1024
+ new inquirer.Separator(),
1025
+ ],
1026
+ },
1027
+ ]);
1028
+ if (contactId === 'main-menu') {
1029
+ return 'main-menu';
1030
+ }
1031
+ if (contactId === null || contactId === 'back') {
1032
+ return;
1033
+ }
1034
+ const selectedContact = contacts.find((c) => c.id === contactId);
1035
+ if (!selectedContact)
1036
+ return;
1037
+ const result = await this.manageIndividualContact(selectedContact);
1038
+ if (result === 'main-menu')
1039
+ return 'main-menu';
1040
+ }
1041
+ /**
1042
+ * Manage an individual contact
1043
+ */
1044
+ async manageIndividualContact(contact) {
1045
+ // Display contact information
1046
+ printSectionHeader('Contact Details');
1047
+ showKeyValue('Name', contact.name);
1048
+ showKeyValue('Email', contact.email);
1049
+ showKeyValue('Fingerprint', contact.fingerprint);
1050
+ showKeyValue('Algorithm', `${contact.algorithm} (${contact.key_size})`);
1051
+ showKeyValue('Trusted', contact.trusted ? 'Yes' : 'No');
1052
+ if (contact.expires_at) {
1053
+ showKeyValue('Expires', contact.expires_at);
1054
+ }
1055
+ if (contact.notes) {
1056
+ showKeyValue('Notes', contact.notes);
1057
+ }
1058
+ console.log();
1059
+ const { action } = await escapeablePrompt([
1060
+ {
1061
+ type: 'list',
1062
+ name: 'action',
1063
+ message: promptMessage('What would you like to do?'),
1064
+ choices: [
1065
+ { name: `${icons.copy} Copy public key`, value: 'copy-public' },
1066
+ { name: `${icons.view} View public key`, value: 'view-public' },
1067
+ { name: `${icons.edit} Rename contact`, value: 'rename' },
1068
+ { name: `${icons.notes} Edit notes`, value: 'edit-notes' },
1069
+ { name: `${icons.trust} Toggle trust`, value: 'toggle-trust' },
1070
+ { name: `${icons.exit} Delete contact`, value: 'delete' },
1071
+ new inquirer.Separator(),
1072
+ backChoice('Back to contact list'),
1073
+ mainMenuChoice(),
1074
+ ],
1075
+ },
1076
+ ]);
1077
+ switch (action) {
1078
+ case 'copy-public':
1079
+ await this.copyContactPublicKey(contact);
1080
+ return this.manageIndividualContact(contact);
1081
+ case 'view-public':
1082
+ await this.viewContactPublicKey(contact);
1083
+ return this.manageIndividualContact(contact);
1084
+ case 'rename':
1085
+ await this.renameContact(contact);
1086
+ const updated = this.db.select({
1087
+ table: 'contact',
1088
+ where: { key: 'id', compare: 'is', value: contact.id },
1089
+ })[0];
1090
+ if (updated)
1091
+ return this.manageIndividualContact(updated);
1092
+ break;
1093
+ case 'edit-notes':
1094
+ await this.editContactNotes(contact);
1095
+ const updatedNotes = this.db.select({
1096
+ table: 'contact',
1097
+ where: { key: 'id', compare: 'is', value: contact.id },
1098
+ })[0];
1099
+ if (updatedNotes)
1100
+ return this.manageIndividualContact(updatedNotes);
1101
+ break;
1102
+ case 'toggle-trust':
1103
+ await this.toggleContactTrust(contact);
1104
+ const refreshed = this.db.select({
1105
+ table: 'contact',
1106
+ where: { key: 'id', compare: 'is', value: contact.id },
1107
+ })[0];
1108
+ if (refreshed)
1109
+ return this.manageIndividualContact(refreshed);
1110
+ break;
1111
+ case 'delete':
1112
+ const deleted = await this.deleteContact(contact.id);
1113
+ if (!deleted) {
1114
+ return this.manageIndividualContact(contact);
1115
+ }
1116
+ break;
1117
+ case 'main-menu':
1118
+ return 'main-menu';
1119
+ case 'back':
1120
+ return;
1121
+ }
1122
+ }
1123
+ /**
1124
+ * Copy contact's public key to clipboard
1125
+ */
1126
+ async copyContactPublicKey(contact) {
1127
+ console.clear();
1128
+ printBanner();
1129
+ console.log(colors.successBold(`${contact.name}'s Public Key:\n`));
1130
+ printDivider();
1131
+ console.log(contact.public_key);
1132
+ printDivider();
1133
+ try {
1134
+ const clipboardy = (await import('clipboardy')).default;
1135
+ await clipboardy.write(contact.public_key);
1136
+ console.log();
1137
+ showSuccess('Public key copied to clipboard');
1138
+ console.log();
1139
+ }
1140
+ catch (error) {
1141
+ console.log();
1142
+ showWarning('Clipboard unavailable');
1143
+ console.log();
1144
+ }
1145
+ }
1146
+ /**
1147
+ * View contact's public key
1148
+ */
1149
+ async viewContactPublicKey(contact) {
1150
+ console.clear();
1151
+ printBanner();
1152
+ console.log(colors.successBold(`${contact.name}'s Public Key:\n`));
1153
+ printDivider();
1154
+ console.log(contact.public_key);
1155
+ printDivider();
1156
+ console.log();
1157
+ await escapeablePrompt([
1158
+ {
1159
+ type: 'input',
1160
+ name: 'continue',
1161
+ message: colors.muted('Press Enter to continue...'),
1162
+ },
1163
+ ]);
1164
+ }
1165
+ /**
1166
+ * Rename a contact
1167
+ */
1168
+ async renameContact(contact) {
1169
+ const { newName } = await escapeablePrompt([
1170
+ {
1171
+ type: 'input',
1172
+ name: 'newName',
1173
+ message: promptMessage('Enter new name:'),
1174
+ default: contact.name,
1175
+ validate: (input) => input.trim().length > 0 || 'Name cannot be empty',
1176
+ },
1177
+ ]);
1178
+ this.db.update('contact', { key: 'id', value: contact.id }, { name: newName.trim() });
1179
+ console.log();
1180
+ showSuccess('Contact renamed!');
1181
+ console.log();
1182
+ }
1183
+ /**
1184
+ * Edit contact notes
1185
+ */
1186
+ async editContactNotes(contact) {
1187
+ const { notes } = await escapeablePrompt([
1188
+ {
1189
+ type: 'input',
1190
+ name: 'notes',
1191
+ message: promptMessage('Enter notes:'),
1192
+ default: contact.notes || '',
1193
+ },
1194
+ ]);
1195
+ this.db.update('contact', { key: 'id', value: contact.id }, { notes: notes.trim() || null });
1196
+ console.log();
1197
+ showSuccess('Notes updated!');
1198
+ console.log();
1199
+ }
1200
+ /**
1201
+ * Toggle contact trust status
1202
+ */
1203
+ async toggleContactTrust(contact) {
1204
+ const newTrustStatus = !contact.trusted;
1205
+ this.db.update('contact', { key: 'id', value: contact.id }, {
1206
+ trusted: newTrustStatus,
1207
+ last_verified_at: newTrustStatus ? new Date().toISOString() : contact.last_verified_at
1208
+ });
1209
+ console.log();
1210
+ showSuccess(`Contact marked as ${newTrustStatus ? 'trusted' : 'untrusted'}!`);
1211
+ console.log();
1212
+ }
1213
+ /**
1214
+ * Delete a contact
1215
+ */
1216
+ async deleteContact(contactId) {
1217
+ const { confirm } = await escapeablePrompt([
1218
+ {
1219
+ type: 'confirm',
1220
+ name: 'confirm',
1221
+ message: colors.error('Are you sure? This action cannot be undone.'),
1222
+ default: false,
1223
+ },
1224
+ ]);
1225
+ if (confirm) {
1226
+ this.db.delete('contact', { key: 'id', value: contactId });
1227
+ console.log();
1228
+ showSuccess('Contact deleted.');
1229
+ console.log();
1230
+ return true;
1231
+ }
1232
+ return false;
1233
+ }
1234
+ }
1235
+ //# sourceMappingURL=key-manager.js.map