favacli 0.0.13 → 0.0.15

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.
@@ -0,0 +1,12 @@
1
+ import BaseCommand from '../../BaseCommand.mjs';
2
+ declare class ExportTextCommand extends BaseCommand {
3
+ static paths: string[][];
4
+ requireTwoFaLib: boolean;
5
+ pathOption: string | undefined;
6
+ passphraseSource: string | undefined;
7
+ static usage: import("clipanion").Usage;
8
+ exec(): Promise<{
9
+ success: boolean;
10
+ }>;
11
+ }
12
+ export default ExportTextCommand;
@@ -0,0 +1,118 @@
1
+ import fs from 'node:fs/promises';
2
+ import { Option } from 'clipanion';
3
+ import * as t from 'typanion';
4
+ import keytar from 'keytar';
5
+ import BaseCommand from '../../BaseCommand.mjs';
6
+ import { password, input } from '@inquirer/prompts';
7
+ class ExportTextCommand extends BaseCommand {
8
+ constructor() {
9
+ super(...arguments);
10
+ this.requireTwoFaLib = true;
11
+ this.pathOption = Option.String('--path', {
12
+ description: 'File path for the export, if not set will output to stdout',
13
+ });
14
+ this.passphraseSource = Option.String('--passphrase-source', {
15
+ description: 'Source of passphrase, either "stdin" or "stored"',
16
+ validator: t.isEnum(['stdin', 'stored']),
17
+ });
18
+ }
19
+ static { this.paths = [['export', 'text']]; }
20
+ static { this.usage = BaseCommand.Usage({
21
+ category: 'Export',
22
+ description: 'Export 2FA entries as plain text',
23
+ details: `
24
+ This command exports your 2FA entries as a plain text file.
25
+
26
+ You can specify a file path for the export using the --path flag.
27
+
28
+ You can specify the passphrase source with --passphrase-source flag:
29
+ - "stdin": You'll be prompted to enter a passphrase
30
+ - "stored": Will use the previously stored passphrase, if none is found, will be promted for a passphrase
31
+
32
+ If no passphrase source is specified, it will export without encryption (no passphrase).
33
+
34
+ WARNING: Exporting your 2FA secrets in plain text (without passphrase) is not secure.
35
+ `,
36
+ examples: [
37
+ ['Export entries as unencrypted text (UNSECURE)', 'export text'],
38
+ ['Export to specific file', 'export text --path=/path/to/export.txt'],
39
+ [
40
+ 'Export using stored passphrase',
41
+ 'export text --passphrase-source=stored',
42
+ ],
43
+ [
44
+ 'Export with manual passphrase entry',
45
+ 'export text --passphrase-source=stdin',
46
+ ],
47
+ ],
48
+ }); }
49
+ async exec() {
50
+ const entriesCount = this.twoFaLib.vault.listEntries().length;
51
+ // Get passphrase based on source
52
+ let passphrase = undefined;
53
+ // If passphrase source is "stored", try to get from keytar without prompting
54
+ if (this.passphraseSource === 'stored') {
55
+ try {
56
+ passphrase = await keytar.getPassword('favacli', 'export-passphrase');
57
+ if (!passphrase) {
58
+ this.context.stderr.write('No stored passphrase found. Please enter a passphrase to store.\n');
59
+ passphrase = await password({
60
+ message: 'Enter passphrase to store and use for encryption:',
61
+ });
62
+ // Store the passphrase for future use
63
+ await keytar.setPassword('favacli', 'export-passphrase', passphrase);
64
+ this.context.stdout.write('Passphrase stored.\n');
65
+ }
66
+ }
67
+ catch (error) {
68
+ if (error instanceof Error) {
69
+ this.context.stderr.write(`Failed to access stored passphrase: ${error.message}}\n`);
70
+ }
71
+ else {
72
+ this.context.stderr.write(`Failed to access stored passphrase: Unknown error\n`);
73
+ }
74
+ return { success: false };
75
+ }
76
+ }
77
+ // Default behavior or explicit "stdin"
78
+ else if (this.passphraseSource === 'stdin') {
79
+ passphrase = await password({
80
+ message: 'Enter passphrase to use for encryption:',
81
+ });
82
+ }
83
+ // Add warning and confirmation if no passphrase is provided
84
+ else if (!this.passphraseSource) {
85
+ this.context.stderr.write('WARNING: You are about to export 2FA secrets in plain text without encryption.\n');
86
+ this.context.stderr.write('This is NOT SECURE and could expose your 2FA secrets if the file is accessed by others.\n');
87
+ const confirm = await input({
88
+ message: 'Type "unsecure" to acknowldge you understand the risks and to proceed with unencrypted export:',
89
+ });
90
+ if (confirm !== 'unsecure') {
91
+ this.context.stderr.write('Export cancelled.\n');
92
+ return { success: false };
93
+ }
94
+ }
95
+ const content = await this.twoFaLib.exportImport.exportEntries('text', passphrase, true);
96
+ if (this.pathOption) {
97
+ try {
98
+ await fs.writeFile(this.pathOption, content);
99
+ this.context.stdout.write(`Successfully exported ${entriesCount} entries to ${this.pathOption}\n`);
100
+ return { success: true };
101
+ }
102
+ catch (error) {
103
+ if (error instanceof Error) {
104
+ this.context.stderr.write(`Failed to export: ${error.message}\n`);
105
+ }
106
+ else {
107
+ this.context.stderr.write('Failed to export: Unknown error\n');
108
+ }
109
+ return { success: false };
110
+ }
111
+ }
112
+ else {
113
+ this.context.stdout.write(content);
114
+ return { success: true };
115
+ }
116
+ }
117
+ }
118
+ export default ExportTextCommand;
@@ -0,0 +1,10 @@
1
+ import BaseCommand from '../../BaseCommand.mjs';
2
+ declare class ResilverCommand extends BaseCommand {
3
+ static paths: string[][];
4
+ requireTwoFaLib: boolean;
5
+ static usage: import("clipanion").Usage;
6
+ exec(): Promise<{
7
+ success: boolean;
8
+ }>;
9
+ }
10
+ export default ResilverCommand;
@@ -0,0 +1,24 @@
1
+ import BaseCommand from '../../BaseCommand.mjs';
2
+ class ResilverCommand extends BaseCommand {
3
+ constructor() {
4
+ super(...arguments);
5
+ this.requireTwoFaLib = true;
6
+ }
7
+ static { this.paths = [['sync', 'resilver']]; }
8
+ static { this.usage = BaseCommand.Usage({
9
+ category: 'Sync',
10
+ description: 'Resilver a vault',
11
+ details: `This command allows you to resync a vault to the server. This is useful if some devices have become desynced.`,
12
+ examples: [['Resilver a vault', 'sync resilver']],
13
+ }); }
14
+ async exec() {
15
+ if (!this.twoFaLib.sync) {
16
+ throw new Error('No server url set');
17
+ }
18
+ this.twoFaLib.sync.requestResilver();
19
+ // TODO: do this based on events
20
+ await new Promise((resolve) => setTimeout(resolve, 5000));
21
+ return { success: true };
22
+ }
23
+ }
24
+ export default ResilverCommand;
package/build/main.mjs CHANGED
@@ -7,6 +7,8 @@ import EntriesListCommand from './commands/entries/list.mjs';
7
7
  import EntriesSearchCommand from './commands/entries/search.mjs';
8
8
  import SyncSetServerUrlCommand from './commands/sync/setServerUrl.mjs';
9
9
  import SyncConnect from './commands/sync/connect.mjs';
10
+ import SyncResilver from './commands/sync/resilver.mjs';
11
+ import ExportTextCommand from './commands/export/text.mjs';
10
12
  // check node version
11
13
  const nodeRuntimeMajorVersion = parseInt(process.version.split('.')[0]);
12
14
  if (nodeRuntimeMajorVersion < 20) {
@@ -16,7 +18,7 @@ const [, , ...args] = process.argv;
16
18
  const cli = new Cli({
17
19
  binaryLabel: 'FavaCli',
18
20
  binaryName: `favacli`,
19
- binaryVersion: '0.0.12',
21
+ binaryVersion: '0.0.15',
20
22
  });
21
23
  cli.register(VaultCreateCommand);
22
24
  cli.register(VaultDeleteCommand);
@@ -25,5 +27,7 @@ cli.register(EntriesListCommand);
25
27
  cli.register(EntriesSearchCommand);
26
28
  cli.register(SyncSetServerUrlCommand);
27
29
  cli.register(SyncConnect);
30
+ cli.register(SyncResilver);
31
+ cli.register(ExportTextCommand);
28
32
  cli.register(Builtins.HelpCommand);
29
33
  void cli.runExit(args);
@@ -7,8 +7,31 @@ const twoFaLibVaultCreationUtils = getTwoFaLibVaultCreationUtils(cryptoLib, 'cli
7
7
  const loadVault = async (vaultData, settings, verbose = false) => {
8
8
  const passphrase = (await keytar.getPassword('favacli', 'vault-passphrase'));
9
9
  const twoFaLib = await twoFaLibVaultCreationUtils.loadTwoFaLibFromLockedRepesentation(vaultData, passphrase);
10
- twoFaLib.addEventListener(TwoFaLibEvent.Changed, (ev) => {
11
- return fs.writeFile(settings.vaultLocation, ev.detail.newLockedRepresentationString);
10
+ twoFaLib.addEventListener(TwoFaLibEvent.Changed, async (ev) => {
11
+ const tempFile = `${settings.vaultLocation}.tmp`;
12
+ const backupFile = `${settings.vaultLocation}.backup`;
13
+ try {
14
+ // Write to temporary file first, so we don't have to worry about partial writes
15
+ await fs.writeFile(tempFile, ev.detail.newLockedRepresentationString);
16
+ // Create backup of existing vault if it exists
17
+ try {
18
+ await fs.copyFile(settings.vaultLocation, backupFile);
19
+ }
20
+ catch (err) {
21
+ // If the error is not because the original file doesn't exist yet, throw it
22
+ if (err instanceof Error && 'code' in err && err.code !== 'ENOENT')
23
+ throw err;
24
+ }
25
+ // Atomically rename temp file to target file
26
+ await fs.rename(tempFile, settings.vaultLocation);
27
+ }
28
+ catch (error) {
29
+ // Clean up temp file if something went wrong
30
+ await fs.unlink(tempFile).catch((err) => {
31
+ console.error('Failed to clean up temp file:', err);
32
+ });
33
+ throw error;
34
+ }
12
35
  });
13
36
  twoFaLib.addEventListener(TwoFaLibEvent.Log, (ev) => {
14
37
  if (ev.detail.severity !== 'info' || verbose) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "favacli",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "author": "",
@@ -10,7 +10,7 @@
10
10
  "bufferutil": "^4.0.8",
11
11
  "clipanion": "^4.0.0-rc.4",
12
12
  "env-paths": "^3.0.0",
13
- "favalib": "^0.0.4",
13
+ "favalib": "^0.0.5",
14
14
  "keytar": "^7.9.0",
15
15
  "node-loader": "^2.0.0",
16
16
  "ts-loader": "^9.5.1",