envmatic 1.0.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.
- package/README.md +567 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +203 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/add.d.ts +11 -0
- package/dist/commands/add.d.ts.map +1 -0
- package/dist/commands/add.js +77 -0
- package/dist/commands/add.js.map +1 -0
- package/dist/commands/delete.d.ts +6 -0
- package/dist/commands/delete.d.ts.map +1 -0
- package/dist/commands/delete.js +78 -0
- package/dist/commands/delete.js.map +1 -0
- package/dist/commands/edit.d.ts +13 -0
- package/dist/commands/edit.d.ts.map +1 -0
- package/dist/commands/edit.js +364 -0
- package/dist/commands/edit.js.map +1 -0
- package/dist/commands/import.d.ts +11 -0
- package/dist/commands/import.d.ts.map +1 -0
- package/dist/commands/import.js +103 -0
- package/dist/commands/import.js.map +1 -0
- package/dist/commands/init.d.ts +8 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +237 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/link.d.ts +16 -0
- package/dist/commands/link.d.ts.map +1 -0
- package/dist/commands/link.js +157 -0
- package/dist/commands/link.js.map +1 -0
- package/dist/commands/list.d.ts +9 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +73 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/lock.d.ts +16 -0
- package/dist/commands/lock.d.ts.map +1 -0
- package/dist/commands/lock.js +245 -0
- package/dist/commands/lock.js.map +1 -0
- package/dist/commands/rotate.d.ts +15 -0
- package/dist/commands/rotate.d.ts.map +1 -0
- package/dist/commands/rotate.js +406 -0
- package/dist/commands/rotate.js.map +1 -0
- package/dist/commands/show.d.ts +9 -0
- package/dist/commands/show.d.ts.map +1 -0
- package/dist/commands/show.js +72 -0
- package/dist/commands/show.js.map +1 -0
- package/dist/commands/sync.d.ts +13 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +174 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/commands/use.d.ts +19 -0
- package/dist/commands/use.d.ts.map +1 -0
- package/dist/commands/use.js +238 -0
- package/dist/commands/use.js.map +1 -0
- package/dist/constants.d.ts +20 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +47 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/services/config.d.ts +64 -0
- package/dist/services/config.d.ts.map +1 -0
- package/dist/services/config.js +133 -0
- package/dist/services/config.js.map +1 -0
- package/dist/services/encryption.d.ts +30 -0
- package/dist/services/encryption.d.ts.map +1 -0
- package/dist/services/encryption.js +146 -0
- package/dist/services/encryption.js.map +1 -0
- package/dist/services/envfile.d.ts +76 -0
- package/dist/services/envfile.d.ts.map +1 -0
- package/dist/services/envfile.js +247 -0
- package/dist/services/envfile.js.map +1 -0
- package/dist/services/git.d.ts +60 -0
- package/dist/services/git.d.ts.map +1 -0
- package/dist/services/git.js +239 -0
- package/dist/services/git.js.map +1 -0
- package/dist/services/linker.d.ts +46 -0
- package/dist/services/linker.d.ts.map +1 -0
- package/dist/services/linker.js +222 -0
- package/dist/services/linker.js.map +1 -0
- package/dist/services/protection.d.ts +32 -0
- package/dist/services/protection.d.ts.map +1 -0
- package/dist/services/protection.js +190 -0
- package/dist/services/protection.js.map +1 -0
- package/dist/types/index.d.ts +73 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/display.d.ts +74 -0
- package/dist/utils/display.d.ts.map +1 -0
- package/dist/utils/display.js +138 -0
- package/dist/utils/display.js.map +1 -0
- package/dist/utils/editor.d.ts +22 -0
- package/dist/utils/editor.d.ts.map +1 -0
- package/dist/utils/editor.js +159 -0
- package/dist/utils/editor.js.map +1 -0
- package/dist/utils/prompts.d.ts +41 -0
- package/dist/utils/prompts.d.ts.map +1 -0
- package/dist/utils/prompts.js +222 -0
- package/dist/utils/prompts.js.map +1 -0
- package/package.json +69 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lock Command
|
|
3
|
+
* List and lock mutable (unlocked) env files
|
|
4
|
+
*/
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import inquirer from 'inquirer';
|
|
7
|
+
import { listEnvFiles, getEnvFilePath, readEnvFile, updateEnvFile } from '../services/envfile.js';
|
|
8
|
+
import { sync, commitChanges } from '../services/git.js';
|
|
9
|
+
import { getConfig } from '../services/config.js';
|
|
10
|
+
import { makeImmutable, isImmutable } from '../services/protection.js';
|
|
11
|
+
import { syncCopies } from '../services/linker.js';
|
|
12
|
+
import { printBanner, success, error, info, warning, colors, formatFileId } from '../utils/display.js';
|
|
13
|
+
import { getEncryptionOptions, confirm } from '../utils/prompts.js';
|
|
14
|
+
/**
|
|
15
|
+
* Find all unlocked (mutable) env files
|
|
16
|
+
*/
|
|
17
|
+
async function findUnlockedFiles() {
|
|
18
|
+
const files = await listEnvFiles();
|
|
19
|
+
const unlocked = [];
|
|
20
|
+
for (const file of files) {
|
|
21
|
+
// Only check files that should be immutable
|
|
22
|
+
if (file.immutable) {
|
|
23
|
+
const filePath = getEnvFilePath(file.id, file.encrypted);
|
|
24
|
+
const currentlyImmutable = await isImmutable(filePath);
|
|
25
|
+
if (!currentlyImmutable) {
|
|
26
|
+
unlocked.push({ file, filePath });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return unlocked;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Lock a single file
|
|
34
|
+
*/
|
|
35
|
+
async function lockFile(file, filePath) {
|
|
36
|
+
try {
|
|
37
|
+
await makeImmutable(filePath);
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Lock command - list and lock mutable files
|
|
46
|
+
*/
|
|
47
|
+
export async function lockCommand(fileId, options = {}) {
|
|
48
|
+
printBanner();
|
|
49
|
+
const config = await getConfig();
|
|
50
|
+
if (!config) {
|
|
51
|
+
error('Envmatic is not initialized. Run `envmatic init` first.');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// If specific file ID provided, lock just that file
|
|
55
|
+
if (fileId) {
|
|
56
|
+
const files = await listEnvFiles();
|
|
57
|
+
const file = files.find(f => f.id === fileId);
|
|
58
|
+
if (!file) {
|
|
59
|
+
error(`Env file not found: ${fileId}`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const filePath = getEnvFilePath(file.id, file.encrypted);
|
|
63
|
+
const currentlyImmutable = await isImmutable(filePath);
|
|
64
|
+
if (currentlyImmutable) {
|
|
65
|
+
info(`${formatFileId(fileId)} is already locked.`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const spinner = ora('Locking file...').start();
|
|
69
|
+
const locked = await lockFile(file, filePath);
|
|
70
|
+
if (locked) {
|
|
71
|
+
spinner.succeed('File locked');
|
|
72
|
+
// If file was edited, we should sync
|
|
73
|
+
const shouldSync = await confirm('Sync changes to remote?', true);
|
|
74
|
+
if (shouldSync) {
|
|
75
|
+
const encryptionOptions = await getEncryptionOptions();
|
|
76
|
+
// Re-encrypt if needed (in case content was modified)
|
|
77
|
+
try {
|
|
78
|
+
const { variables } = await readEnvFile(file.id, encryptionOptions);
|
|
79
|
+
await updateEnvFile(file.id, variables, encryptionOptions);
|
|
80
|
+
await syncCopies(file.id, encryptionOptions);
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
// File might not have been modified, that's ok
|
|
84
|
+
}
|
|
85
|
+
const syncSpinner = ora('Syncing to remote...').start();
|
|
86
|
+
try {
|
|
87
|
+
await commitChanges(`Update ${file.id}`);
|
|
88
|
+
await sync();
|
|
89
|
+
syncSpinner.succeed('Synced');
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
syncSpinner.warn('Could not sync (will sync later)');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
console.log();
|
|
96
|
+
success(`${formatFileId(fileId)} is now locked.`);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
spinner.fail('Failed to lock file');
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// Find all unlocked files
|
|
104
|
+
const unlockedFiles = await findUnlockedFiles();
|
|
105
|
+
if (unlockedFiles.length === 0) {
|
|
106
|
+
success('All files are locked.');
|
|
107
|
+
console.log();
|
|
108
|
+
console.log(colors.muted('No unlocked files found.'));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
console.log(colors.warning(`Found ${unlockedFiles.length} unlocked file(s):\n`));
|
|
112
|
+
for (const { file } of unlockedFiles) {
|
|
113
|
+
const flags = [];
|
|
114
|
+
if (file.encrypted)
|
|
115
|
+
flags.push('🔒');
|
|
116
|
+
const flagStr = flags.length > 0 ? ' ' + flags.join(' ') : '';
|
|
117
|
+
console.log(` ${colors.accent('🔓')} ${formatFileId(file.id)}${flagStr}`);
|
|
118
|
+
}
|
|
119
|
+
console.log();
|
|
120
|
+
// Lock all option
|
|
121
|
+
if (options.all) {
|
|
122
|
+
const spinner = ora('Locking all files...').start();
|
|
123
|
+
let locked = 0;
|
|
124
|
+
let failed = 0;
|
|
125
|
+
for (const { file, filePath } of unlockedFiles) {
|
|
126
|
+
if (await lockFile(file, filePath)) {
|
|
127
|
+
locked++;
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
failed++;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (failed > 0) {
|
|
134
|
+
spinner.warn(`Locked ${locked} file(s), ${failed} failed`);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
spinner.succeed(`Locked ${locked} file(s)`);
|
|
138
|
+
}
|
|
139
|
+
// Sync changes
|
|
140
|
+
const shouldSync = await confirm('Sync changes to remote?', true);
|
|
141
|
+
if (shouldSync) {
|
|
142
|
+
const syncSpinner = ora('Syncing to remote...').start();
|
|
143
|
+
try {
|
|
144
|
+
await commitChanges('Lock files after editing');
|
|
145
|
+
await sync();
|
|
146
|
+
syncSpinner.succeed('Synced');
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
syncSpinner.warn('Could not sync (will sync later)');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
console.log();
|
|
153
|
+
success('All files locked.');
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
// Interactive selection
|
|
157
|
+
const { action } = await inquirer.prompt([
|
|
158
|
+
{
|
|
159
|
+
type: 'list',
|
|
160
|
+
name: 'action',
|
|
161
|
+
message: 'What would you like to do?',
|
|
162
|
+
choices: [
|
|
163
|
+
{ name: 'Lock all unlocked files', value: 'all' },
|
|
164
|
+
{ name: 'Select files to lock', value: 'select' },
|
|
165
|
+
{ name: 'Cancel', value: 'cancel' },
|
|
166
|
+
],
|
|
167
|
+
},
|
|
168
|
+
]);
|
|
169
|
+
if (action === 'cancel') {
|
|
170
|
+
info('No files were locked.');
|
|
171
|
+
console.log();
|
|
172
|
+
warning('Remember: Unlocked files are not protected from accidental edits.');
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (action === 'all') {
|
|
176
|
+
return lockCommand(undefined, { all: true });
|
|
177
|
+
}
|
|
178
|
+
// Select specific files
|
|
179
|
+
const { selectedFiles } = await inquirer.prompt([
|
|
180
|
+
{
|
|
181
|
+
type: 'checkbox',
|
|
182
|
+
name: 'selectedFiles',
|
|
183
|
+
message: 'Select files to lock:',
|
|
184
|
+
choices: unlockedFiles.map(({ file }) => ({
|
|
185
|
+
name: file.id,
|
|
186
|
+
value: file.id,
|
|
187
|
+
checked: true,
|
|
188
|
+
})),
|
|
189
|
+
},
|
|
190
|
+
]);
|
|
191
|
+
if (selectedFiles.length === 0) {
|
|
192
|
+
info('No files selected.');
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const spinner = ora('Locking selected files...').start();
|
|
196
|
+
let locked = 0;
|
|
197
|
+
let failed = 0;
|
|
198
|
+
for (const id of selectedFiles) {
|
|
199
|
+
const item = unlockedFiles.find(u => u.file.id === id);
|
|
200
|
+
if (item && await lockFile(item.file, item.filePath)) {
|
|
201
|
+
locked++;
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
failed++;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (failed > 0) {
|
|
208
|
+
spinner.warn(`Locked ${locked} file(s), ${failed} failed`);
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
spinner.succeed(`Locked ${locked} file(s)`);
|
|
212
|
+
}
|
|
213
|
+
// Sync changes
|
|
214
|
+
const shouldSync = await confirm('Sync changes to remote?', true);
|
|
215
|
+
if (shouldSync) {
|
|
216
|
+
const syncSpinner = ora('Syncing to remote...').start();
|
|
217
|
+
try {
|
|
218
|
+
await commitChanges('Lock files after editing');
|
|
219
|
+
await sync();
|
|
220
|
+
syncSpinner.succeed('Synced');
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
syncSpinner.warn('Could not sync (will sync later)');
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
console.log();
|
|
227
|
+
success('Selected files locked.');
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Unlock command - unlock a file for editing
|
|
231
|
+
* (This is primarily used internally by the edit --editor command)
|
|
232
|
+
*/
|
|
233
|
+
export async function unlockFileForEditing(fileId) {
|
|
234
|
+
const files = await listEnvFiles();
|
|
235
|
+
const file = files.find(f => f.id === fileId);
|
|
236
|
+
if (!file) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
const filePath = getEnvFilePath(file.id, file.encrypted);
|
|
240
|
+
// Make it mutable
|
|
241
|
+
const { makeMutable } = await import('../services/protection.js');
|
|
242
|
+
await makeMutable(filePath);
|
|
243
|
+
return filePath;
|
|
244
|
+
}
|
|
245
|
+
//# sourceMappingURL=lock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock.js","sourceRoot":"","sources":["../../src/commands/lock.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rotate Command
|
|
3
|
+
* Change encryption password or rotate encryption key/method
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Change password command
|
|
7
|
+
* Requires old password to decrypt, then re-encrypts with new password
|
|
8
|
+
*/
|
|
9
|
+
export declare function changePasswordCommand(): Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* Rotate encryption key/method
|
|
12
|
+
* Can switch between password and SSH key encryption
|
|
13
|
+
*/
|
|
14
|
+
export declare function rotateKeyCommand(): Promise<void>;
|
|
15
|
+
//# sourceMappingURL=rotate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rotate.d.ts","sourceRoot":"","sources":["../../src/commands/rotate.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAyBH;;;GAGG;AACH,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC,CAsK3D;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAqRtD"}
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rotate Command
|
|
3
|
+
* Change encryption password or rotate encryption key/method
|
|
4
|
+
*/
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import inquirer from 'inquirer';
|
|
7
|
+
import { readEnvFile, updateEnvFile, listEnvFiles, getEnvFilePath } from '../services/envfile.js';
|
|
8
|
+
import { sync } from '../services/git.js';
|
|
9
|
+
import { getConfig, updateConfig } from '../services/config.js';
|
|
10
|
+
import { verifyEncryption, validateSSHKey } from '../services/encryption.js';
|
|
11
|
+
import { makeMutable, makeImmutable } from '../services/protection.js';
|
|
12
|
+
import { printBanner, success, error, info, warning, colors } from '../utils/display.js';
|
|
13
|
+
/**
|
|
14
|
+
* Change password command
|
|
15
|
+
* Requires old password to decrypt, then re-encrypts with new password
|
|
16
|
+
*/
|
|
17
|
+
export async function changePasswordCommand() {
|
|
18
|
+
printBanner();
|
|
19
|
+
const config = await getConfig();
|
|
20
|
+
if (!config) {
|
|
21
|
+
error('Envmatic is not initialized. Run `envmatic init` first.');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (!config.encryptionEnabled) {
|
|
25
|
+
error('Encryption is not enabled. Nothing to change.');
|
|
26
|
+
info('Run `envmatic rotate-key` to enable encryption.');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (config.encryptionMethod !== 'password') {
|
|
30
|
+
error('Current encryption method is SSH key, not password.');
|
|
31
|
+
info('Use `envmatic rotate-key` to switch encryption methods.');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
console.log(colors.muted('Change your encryption password.\n'));
|
|
35
|
+
warning('You will need to enter your current password to proceed.');
|
|
36
|
+
console.log();
|
|
37
|
+
// Get old password
|
|
38
|
+
const { oldPassword } = await inquirer.prompt([
|
|
39
|
+
{
|
|
40
|
+
type: 'password',
|
|
41
|
+
name: 'oldPassword',
|
|
42
|
+
message: 'Enter your CURRENT password:',
|
|
43
|
+
mask: '*',
|
|
44
|
+
validate: (input) => {
|
|
45
|
+
if (!input || input.length < 8) {
|
|
46
|
+
return 'Password must be at least 8 characters';
|
|
47
|
+
}
|
|
48
|
+
return true;
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
]);
|
|
52
|
+
// Verify old password works
|
|
53
|
+
const oldOptions = {
|
|
54
|
+
method: 'password',
|
|
55
|
+
password: oldPassword,
|
|
56
|
+
};
|
|
57
|
+
const verifySpinner = ora('Verifying current password...').start();
|
|
58
|
+
const isValid = await verifyEncryption(oldOptions);
|
|
59
|
+
if (!isValid) {
|
|
60
|
+
verifySpinner.fail('Current password is incorrect');
|
|
61
|
+
error('Cannot proceed without the correct current password.');
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
verifySpinner.succeed('Current password verified');
|
|
65
|
+
// Get new password
|
|
66
|
+
console.log();
|
|
67
|
+
const { newPassword } = await inquirer.prompt([
|
|
68
|
+
{
|
|
69
|
+
type: 'password',
|
|
70
|
+
name: 'newPassword',
|
|
71
|
+
message: 'Enter your NEW password:',
|
|
72
|
+
mask: '*',
|
|
73
|
+
validate: (input) => {
|
|
74
|
+
if (!input || input.length < 8) {
|
|
75
|
+
return 'Password must be at least 8 characters';
|
|
76
|
+
}
|
|
77
|
+
if (input === oldPassword) {
|
|
78
|
+
return 'New password must be different from current password';
|
|
79
|
+
}
|
|
80
|
+
return true;
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
]);
|
|
84
|
+
// Confirm new password
|
|
85
|
+
const { confirmPassword } = await inquirer.prompt([
|
|
86
|
+
{
|
|
87
|
+
type: 'password',
|
|
88
|
+
name: 'confirmPassword',
|
|
89
|
+
message: 'Confirm your NEW password:',
|
|
90
|
+
mask: '*',
|
|
91
|
+
validate: (input) => {
|
|
92
|
+
if (input !== newPassword) {
|
|
93
|
+
return 'Passwords do not match';
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
]);
|
|
99
|
+
const newOptions = {
|
|
100
|
+
method: 'password',
|
|
101
|
+
password: newPassword,
|
|
102
|
+
};
|
|
103
|
+
// Get all encrypted files
|
|
104
|
+
const files = await listEnvFiles();
|
|
105
|
+
const encryptedFiles = files.filter(f => f.encrypted);
|
|
106
|
+
if (encryptedFiles.length === 0) {
|
|
107
|
+
info('No encrypted files found. Password updated for future files.');
|
|
108
|
+
success('Password changed successfully!');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
console.log();
|
|
112
|
+
console.log(colors.muted(`Found ${encryptedFiles.length} encrypted file(s) to re-encrypt.\n`));
|
|
113
|
+
// Re-encrypt all files
|
|
114
|
+
const spinner = ora('Re-encrypting files...').start();
|
|
115
|
+
let processed = 0;
|
|
116
|
+
let errors = 0;
|
|
117
|
+
for (const file of encryptedFiles) {
|
|
118
|
+
try {
|
|
119
|
+
// Make file mutable if needed
|
|
120
|
+
const filePath = getEnvFilePath(file.id, file.encrypted);
|
|
121
|
+
if (file.immutable) {
|
|
122
|
+
await makeMutable(filePath);
|
|
123
|
+
}
|
|
124
|
+
// Read with old password
|
|
125
|
+
const { variables } = await readEnvFile(file.id, oldOptions);
|
|
126
|
+
// Write with new password
|
|
127
|
+
await updateEnvFile(file.id, variables, newOptions);
|
|
128
|
+
// Restore protection if needed
|
|
129
|
+
if (file.immutable) {
|
|
130
|
+
await makeImmutable(filePath);
|
|
131
|
+
}
|
|
132
|
+
processed++;
|
|
133
|
+
spinner.text = `Re-encrypting files... (${processed}/${encryptedFiles.length})`;
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
errors++;
|
|
137
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
138
|
+
console.error(`\nFailed to re-encrypt ${file.id}: ${errorMessage}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (errors > 0) {
|
|
142
|
+
spinner.warn(`Re-encrypted ${processed} files with ${errors} error(s)`);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
spinner.succeed(`Re-encrypted ${processed} file(s)`);
|
|
146
|
+
}
|
|
147
|
+
// Sync to remote
|
|
148
|
+
const syncSpinner = ora('Syncing to remote...').start();
|
|
149
|
+
try {
|
|
150
|
+
await sync();
|
|
151
|
+
syncSpinner.succeed('Synced to remote');
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
syncSpinner.warn('Could not sync to remote (will sync later)');
|
|
155
|
+
}
|
|
156
|
+
console.log();
|
|
157
|
+
success('Password changed successfully!');
|
|
158
|
+
console.log();
|
|
159
|
+
warning('Remember your new password! It cannot be recovered.');
|
|
160
|
+
warning('If you forget it, all encrypted data will be permanently lost.');
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Rotate encryption key/method
|
|
164
|
+
* Can switch between password and SSH key encryption
|
|
165
|
+
*/
|
|
166
|
+
export async function rotateKeyCommand() {
|
|
167
|
+
printBanner();
|
|
168
|
+
const config = await getConfig();
|
|
169
|
+
if (!config) {
|
|
170
|
+
error('Envmatic is not initialized. Run `envmatic init` first.');
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
console.log(colors.muted('Rotate your encryption key or change encryption method.\n'));
|
|
174
|
+
// Determine current state
|
|
175
|
+
const currentMethod = config.encryptionEnabled
|
|
176
|
+
? config.encryptionMethod || 'none'
|
|
177
|
+
: 'none';
|
|
178
|
+
console.log(colors.muted('Current encryption:') + ' ' +
|
|
179
|
+
(currentMethod === 'none'
|
|
180
|
+
? colors.warning('disabled')
|
|
181
|
+
: colors.secondary(currentMethod)));
|
|
182
|
+
console.log();
|
|
183
|
+
// Get current encryption options if encryption is enabled
|
|
184
|
+
let oldOptions;
|
|
185
|
+
if (config.encryptionEnabled && config.encryptionMethod) {
|
|
186
|
+
warning('You will need to provide your current credentials to proceed.');
|
|
187
|
+
console.log();
|
|
188
|
+
if (config.encryptionMethod === 'password') {
|
|
189
|
+
const { password } = await inquirer.prompt([
|
|
190
|
+
{
|
|
191
|
+
type: 'password',
|
|
192
|
+
name: 'password',
|
|
193
|
+
message: 'Enter your CURRENT password:',
|
|
194
|
+
mask: '*',
|
|
195
|
+
},
|
|
196
|
+
]);
|
|
197
|
+
oldOptions = { method: 'password', password };
|
|
198
|
+
// Verify
|
|
199
|
+
const isValid = await verifyEncryption(oldOptions);
|
|
200
|
+
if (!isValid) {
|
|
201
|
+
error('Current password is incorrect. Cannot proceed.');
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
success('Current password verified');
|
|
205
|
+
}
|
|
206
|
+
else if (config.encryptionMethod === 'ssh') {
|
|
207
|
+
oldOptions = {
|
|
208
|
+
method: 'ssh',
|
|
209
|
+
sshKeyPath: config.sshKeyPath
|
|
210
|
+
};
|
|
211
|
+
// Verify SSH key exists
|
|
212
|
+
const isValid = await validateSSHKey(config.sshKeyPath);
|
|
213
|
+
if (!isValid) {
|
|
214
|
+
error('Current SSH key is not accessible. Cannot proceed.');
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
success('Current SSH key verified');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
console.log();
|
|
221
|
+
// Ask for new method
|
|
222
|
+
const { newMethod } = await inquirer.prompt([
|
|
223
|
+
{
|
|
224
|
+
type: 'list',
|
|
225
|
+
name: 'newMethod',
|
|
226
|
+
message: 'Select new encryption method:',
|
|
227
|
+
choices: [
|
|
228
|
+
{ name: 'Password encryption', value: 'password' },
|
|
229
|
+
{ name: 'SSH key encryption', value: 'ssh' },
|
|
230
|
+
{ name: 'Disable encryption (not recommended)', value: 'none' },
|
|
231
|
+
],
|
|
232
|
+
},
|
|
233
|
+
]);
|
|
234
|
+
let newOptions;
|
|
235
|
+
let newSshKeyPath;
|
|
236
|
+
if (newMethod === 'password') {
|
|
237
|
+
// Show password warning
|
|
238
|
+
console.log();
|
|
239
|
+
console.log(colors.error('╔══════════════════════════════════════════════════════════════╗'));
|
|
240
|
+
console.log(colors.error('║') + colors.warning(' ⚠️ PASSWORD SECURITY WARNING ') + colors.error('║'));
|
|
241
|
+
console.log(colors.error('╠══════════════════════════════════════════════════════════════╣'));
|
|
242
|
+
console.log(colors.error('║') + ' Your password is the ONLY way to decrypt your secrets. ' + colors.error('║'));
|
|
243
|
+
console.log(colors.error('║') + ' There is NO password recovery mechanism. ' + colors.error('║'));
|
|
244
|
+
console.log(colors.error('║') + ' ' + colors.error('║'));
|
|
245
|
+
console.log(colors.error('║') + colors.warning(' If you forget your password: ') + colors.error('║'));
|
|
246
|
+
console.log(colors.error('║') + colors.error(' → All encrypted data will be PERMANENTLY LOST ') + colors.error('║'));
|
|
247
|
+
console.log(colors.error('║') + colors.error(' → There is NO way to recover your secrets ') + colors.error('║'));
|
|
248
|
+
console.log(colors.error('║') + ' ' + colors.error('║'));
|
|
249
|
+
console.log(colors.error('║') + ' We strongly recommend: ' + colors.error('║'));
|
|
250
|
+
console.log(colors.error('║') + ' • Using a password manager to store your password ' + colors.error('║'));
|
|
251
|
+
console.log(colors.error('║') + ' • Writing it down and storing it securely offline ' + colors.error('║'));
|
|
252
|
+
console.log(colors.error('╚══════════════════════════════════════════════════════════════╝'));
|
|
253
|
+
console.log();
|
|
254
|
+
const { understood } = await inquirer.prompt([
|
|
255
|
+
{
|
|
256
|
+
type: 'confirm',
|
|
257
|
+
name: 'understood',
|
|
258
|
+
message: 'I understand that forgetting my password means losing all encrypted data',
|
|
259
|
+
default: false,
|
|
260
|
+
},
|
|
261
|
+
]);
|
|
262
|
+
if (!understood) {
|
|
263
|
+
info('Operation cancelled.');
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const { password } = await inquirer.prompt([
|
|
267
|
+
{
|
|
268
|
+
type: 'password',
|
|
269
|
+
name: 'password',
|
|
270
|
+
message: 'Enter your NEW password:',
|
|
271
|
+
mask: '*',
|
|
272
|
+
validate: (input) => {
|
|
273
|
+
if (!input || input.length < 8) {
|
|
274
|
+
return 'Password must be at least 8 characters';
|
|
275
|
+
}
|
|
276
|
+
return true;
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
]);
|
|
280
|
+
const { confirmPwd } = await inquirer.prompt([
|
|
281
|
+
{
|
|
282
|
+
type: 'password',
|
|
283
|
+
name: 'confirmPwd',
|
|
284
|
+
message: 'Confirm your NEW password:',
|
|
285
|
+
mask: '*',
|
|
286
|
+
validate: (input) => {
|
|
287
|
+
if (input !== password) {
|
|
288
|
+
return 'Passwords do not match';
|
|
289
|
+
}
|
|
290
|
+
return true;
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
]);
|
|
294
|
+
newOptions = { method: 'password', password };
|
|
295
|
+
}
|
|
296
|
+
else if (newMethod === 'ssh') {
|
|
297
|
+
const { keyPath } = await inquirer.prompt([
|
|
298
|
+
{
|
|
299
|
+
type: 'input',
|
|
300
|
+
name: 'keyPath',
|
|
301
|
+
message: 'Path to SSH private key:',
|
|
302
|
+
default: '~/.ssh/id_rsa',
|
|
303
|
+
},
|
|
304
|
+
]);
|
|
305
|
+
newSshKeyPath = keyPath.replace(/^~/, process.env.HOME || '');
|
|
306
|
+
const validKey = await validateSSHKey(newSshKeyPath);
|
|
307
|
+
if (!validKey) {
|
|
308
|
+
error('Invalid SSH key file. Please check the path and try again.');
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
success('SSH key validated');
|
|
312
|
+
newOptions = { method: 'ssh', sshKeyPath: newSshKeyPath };
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
// Disabling encryption
|
|
316
|
+
warning('Disabling encryption will store all files in PLAIN TEXT.');
|
|
317
|
+
warning('Anyone with access to the repository will be able to read your secrets.');
|
|
318
|
+
const { confirm } = await inquirer.prompt([
|
|
319
|
+
{
|
|
320
|
+
type: 'confirm',
|
|
321
|
+
name: 'confirm',
|
|
322
|
+
message: 'Are you sure you want to disable encryption?',
|
|
323
|
+
default: false,
|
|
324
|
+
},
|
|
325
|
+
]);
|
|
326
|
+
if (!confirm) {
|
|
327
|
+
info('Operation cancelled.');
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
// Get all files
|
|
332
|
+
const files = await listEnvFiles();
|
|
333
|
+
const encryptedFiles = files.filter(f => f.encrypted);
|
|
334
|
+
console.log();
|
|
335
|
+
if (encryptedFiles.length === 0) {
|
|
336
|
+
// No files to re-encrypt, just update config
|
|
337
|
+
await updateConfig({
|
|
338
|
+
encryptionEnabled: newMethod !== 'none',
|
|
339
|
+
encryptionMethod: newMethod === 'none' ? undefined : newMethod,
|
|
340
|
+
sshKeyPath: newSshKeyPath,
|
|
341
|
+
});
|
|
342
|
+
success('Encryption settings updated!');
|
|
343
|
+
info('New settings will apply to future files.');
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
console.log(colors.muted(`Found ${encryptedFiles.length} encrypted file(s) to process.\n`));
|
|
347
|
+
// Re-encrypt all files
|
|
348
|
+
const spinner = ora('Processing files...').start();
|
|
349
|
+
let processed = 0;
|
|
350
|
+
let errors = 0;
|
|
351
|
+
for (const file of encryptedFiles) {
|
|
352
|
+
try {
|
|
353
|
+
const filePath = getEnvFilePath(file.id, file.encrypted);
|
|
354
|
+
// Make file mutable if needed
|
|
355
|
+
if (file.immutable) {
|
|
356
|
+
await makeMutable(filePath);
|
|
357
|
+
}
|
|
358
|
+
// Read with old options
|
|
359
|
+
const { variables } = await readEnvFile(file.id, oldOptions);
|
|
360
|
+
// Write with new options (or no encryption if disabling)
|
|
361
|
+
await updateEnvFile(file.id, variables, newOptions);
|
|
362
|
+
// Restore protection if needed
|
|
363
|
+
if (file.immutable) {
|
|
364
|
+
// File path might have changed (added/removed .enc extension)
|
|
365
|
+
const newFilePath = getEnvFilePath(file.id, newMethod !== 'none');
|
|
366
|
+
await makeImmutable(newFilePath);
|
|
367
|
+
}
|
|
368
|
+
processed++;
|
|
369
|
+
spinner.text = `Processing files... (${processed}/${encryptedFiles.length})`;
|
|
370
|
+
}
|
|
371
|
+
catch (err) {
|
|
372
|
+
errors++;
|
|
373
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
374
|
+
console.error(`\nFailed to process ${file.id}: ${errorMessage}`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (errors > 0) {
|
|
378
|
+
spinner.warn(`Processed ${processed} files with ${errors} error(s)`);
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
spinner.succeed(`Processed ${processed} file(s)`);
|
|
382
|
+
}
|
|
383
|
+
// Update config
|
|
384
|
+
await updateConfig({
|
|
385
|
+
encryptionEnabled: newMethod !== 'none',
|
|
386
|
+
encryptionMethod: newMethod === 'none' ? undefined : newMethod,
|
|
387
|
+
sshKeyPath: newSshKeyPath,
|
|
388
|
+
});
|
|
389
|
+
// Sync to remote
|
|
390
|
+
const syncSpinner = ora('Syncing to remote...').start();
|
|
391
|
+
try {
|
|
392
|
+
await sync();
|
|
393
|
+
syncSpinner.succeed('Synced to remote');
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
syncSpinner.warn('Could not sync to remote (will sync later)');
|
|
397
|
+
}
|
|
398
|
+
console.log();
|
|
399
|
+
success('Encryption key rotated successfully!');
|
|
400
|
+
if (newMethod === 'password') {
|
|
401
|
+
console.log();
|
|
402
|
+
warning('Remember your password! It cannot be recovered.');
|
|
403
|
+
warning('If you forget it, all encrypted data will be permanently lost.');
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
//# sourceMappingURL=rotate.js.map
|