@faizahmed/secret-keystore 1.1.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/LICENSE +21 -0
- package/README.md +1203 -0
- package/SECURITY.md +505 -0
- package/bin/cli.js +969 -0
- package/package.json +77 -0
- package/src/attestation/attestation-client.js +146 -0
- package/src/attestation/attestation-manager.js +339 -0
- package/src/attestation/cms-unwrap.js +166 -0
- package/src/attestation/index.js +66 -0
- package/src/attestation/key-pair.js +129 -0
- package/src/config.js +130 -0
- package/src/content-operations.js +494 -0
- package/src/errors.js +372 -0
- package/src/index.d.ts +641 -0
- package/src/index.js +438 -0
- package/src/keystore.js +678 -0
- package/src/kms.js +858 -0
- package/src/object-operations.js +232 -0
- package/src/options.js +541 -0
- package/src/path-matcher.js +319 -0
- package/src/rotate.js +92 -0
- package/src/yaml-utils.js +265 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,969 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @faizahmed/secret-keystore CLI
|
|
5
|
+
*
|
|
6
|
+
* Command-line interface for encrypting configuration files.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* npx @faizahmed/secret-keystore encrypt [options]
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('node:fs');
|
|
13
|
+
const path = require('node:path');
|
|
14
|
+
const os = require('node:os');
|
|
15
|
+
const crypto = require('node:crypto');
|
|
16
|
+
const { spawn, spawnSync } = require('node:child_process');
|
|
17
|
+
const {
|
|
18
|
+
encryptKMSEnvContent,
|
|
19
|
+
encryptKMSJsonContent,
|
|
20
|
+
encryptKMSYamlContent,
|
|
21
|
+
decryptKMSEnvContent,
|
|
22
|
+
decryptKMSJsonContent,
|
|
23
|
+
decryptKMSYamlContent,
|
|
24
|
+
parseEnvContent,
|
|
25
|
+
maskKmsKeyId,
|
|
26
|
+
validateKmsKeyId,
|
|
27
|
+
config,
|
|
28
|
+
rotateKMSContent,
|
|
29
|
+
isAlreadyEncrypted,
|
|
30
|
+
getAllPaths,
|
|
31
|
+
getByPath,
|
|
32
|
+
parseYaml
|
|
33
|
+
} = require('../src/index');
|
|
34
|
+
|
|
35
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
36
|
+
// ARGUMENT PARSING
|
|
37
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Strip surrounding quotes from a value
|
|
41
|
+
*/
|
|
42
|
+
function stripQuotes(value) {
|
|
43
|
+
if (!value) return value;
|
|
44
|
+
return value.replaceAll(/(^["'])|(["']$)/g, '');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Parse comma-separated list into array
|
|
49
|
+
*/
|
|
50
|
+
function parseCommaSeparated(value) {
|
|
51
|
+
return value
|
|
52
|
+
.split(',')
|
|
53
|
+
.map(item => item.trim())
|
|
54
|
+
.filter(Boolean);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Apply key-value argument to parsed object
|
|
59
|
+
*/
|
|
60
|
+
function applyKeyValueArg(parsed, key, value) {
|
|
61
|
+
const handlers = {
|
|
62
|
+
path: () => {
|
|
63
|
+
parsed.path = value;
|
|
64
|
+
},
|
|
65
|
+
format: () => {
|
|
66
|
+
parsed.format = value;
|
|
67
|
+
},
|
|
68
|
+
'kms-key-id': () => {
|
|
69
|
+
parsed.kmsKeyId = value;
|
|
70
|
+
},
|
|
71
|
+
'old-kms-key-id': () => {
|
|
72
|
+
parsed.oldKmsKeyId = value;
|
|
73
|
+
},
|
|
74
|
+
keys: () => {
|
|
75
|
+
parsed.keys = parseCommaSeparated(value);
|
|
76
|
+
},
|
|
77
|
+
patterns: () => {
|
|
78
|
+
parsed.patterns = parseCommaSeparated(value);
|
|
79
|
+
},
|
|
80
|
+
exclude: () => {
|
|
81
|
+
parsed.exclude = parseCommaSeparated(value);
|
|
82
|
+
},
|
|
83
|
+
region: () => {
|
|
84
|
+
parsed.region = value;
|
|
85
|
+
},
|
|
86
|
+
output: () => {
|
|
87
|
+
parsed.output = value;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const handler = handlers[key];
|
|
92
|
+
if (handler) handler();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Parse a --key=value or --key value style argument
|
|
97
|
+
*/
|
|
98
|
+
function parseKeyValueArg(arg, args, currentIndex) {
|
|
99
|
+
let key, value;
|
|
100
|
+
let nextIndex = currentIndex;
|
|
101
|
+
|
|
102
|
+
if (arg.includes('=')) {
|
|
103
|
+
const eqIndex = arg.indexOf('=');
|
|
104
|
+
key = arg.substring(2, eqIndex);
|
|
105
|
+
value = arg.substring(eqIndex + 1);
|
|
106
|
+
} else {
|
|
107
|
+
key = arg.substring(2);
|
|
108
|
+
nextIndex = currentIndex + 1;
|
|
109
|
+
value = args[nextIndex];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
value = stripQuotes(value);
|
|
113
|
+
return { key, value, nextIndex };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function parseArgs(args) {
|
|
117
|
+
const parsed = {
|
|
118
|
+
command: null,
|
|
119
|
+
path: './.env',
|
|
120
|
+
format: null, // auto-detect
|
|
121
|
+
kmsKeyId: null, // REQUIRED
|
|
122
|
+
oldKmsKeyId: null, // required for rotate
|
|
123
|
+
keys: null,
|
|
124
|
+
patterns: null,
|
|
125
|
+
exclude: null,
|
|
126
|
+
region: null,
|
|
127
|
+
output: null,
|
|
128
|
+
useCredentials: false,
|
|
129
|
+
dryRun: false,
|
|
130
|
+
exec: null, // command + args after `--` (for `run`)
|
|
131
|
+
help: false,
|
|
132
|
+
version: false
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const setCommand = name => () => {
|
|
136
|
+
parsed.command = name;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const flagHandlers = {
|
|
140
|
+
encrypt: setCommand('encrypt'),
|
|
141
|
+
decrypt: setCommand('decrypt'),
|
|
142
|
+
run: setCommand('run'),
|
|
143
|
+
rotate: setCommand('rotate'),
|
|
144
|
+
edit: setCommand('edit'),
|
|
145
|
+
init: setCommand('init'),
|
|
146
|
+
keys: setCommand('keys'),
|
|
147
|
+
status: setCommand('status'),
|
|
148
|
+
import: setCommand('import'),
|
|
149
|
+
'--help': () => {
|
|
150
|
+
parsed.help = true;
|
|
151
|
+
},
|
|
152
|
+
'-h': () => {
|
|
153
|
+
parsed.help = true;
|
|
154
|
+
},
|
|
155
|
+
'--version': () => {
|
|
156
|
+
parsed.version = true;
|
|
157
|
+
},
|
|
158
|
+
'-v': () => {
|
|
159
|
+
parsed.version = true;
|
|
160
|
+
},
|
|
161
|
+
'--use-credentials': () => {
|
|
162
|
+
parsed.useCredentials = true;
|
|
163
|
+
},
|
|
164
|
+
'--dry-run': () => {
|
|
165
|
+
parsed.dryRun = true;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
let i = 0;
|
|
170
|
+
while (i < args.length) {
|
|
171
|
+
const arg = args[i];
|
|
172
|
+
|
|
173
|
+
// Everything after `--` is the command to exec (for `run`)
|
|
174
|
+
if (arg === '--') {
|
|
175
|
+
parsed.exec = args.slice(i + 1);
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Handle simple flags
|
|
180
|
+
const flagHandler = flagHandlers[arg];
|
|
181
|
+
if (flagHandler) {
|
|
182
|
+
flagHandler();
|
|
183
|
+
i += 1;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Handle --key=value or --key value format
|
|
188
|
+
if (arg.startsWith('--')) {
|
|
189
|
+
const { key, value, nextIndex } = parseKeyValueArg(arg, args, i);
|
|
190
|
+
applyKeyValueArg(parsed, key, value);
|
|
191
|
+
i = nextIndex + 1;
|
|
192
|
+
} else {
|
|
193
|
+
i += 1;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return parsed;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
201
|
+
// HELP & VERSION
|
|
202
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
203
|
+
|
|
204
|
+
function printVersion() {
|
|
205
|
+
const pkg = require('../package.json');
|
|
206
|
+
console.log(`${pkg.name} v${pkg.version}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function printHelp() {
|
|
210
|
+
console.log(String.raw`
|
|
211
|
+
@faizahmed/secret-keystore - Secure secrets management with AWS KMS
|
|
212
|
+
|
|
213
|
+
USAGE:
|
|
214
|
+
npx @faizahmed/secret-keystore <command> [options]
|
|
215
|
+
|
|
216
|
+
COMMANDS:
|
|
217
|
+
encrypt Encrypt values in a configuration file
|
|
218
|
+
decrypt Decrypt values in a configuration file
|
|
219
|
+
run Decrypt and run a command with secrets injected into its env
|
|
220
|
+
rotate Re-encrypt a file under a new KMS Key ID (requires --old-kms-key-id)
|
|
221
|
+
edit Decrypt → open in $EDITOR → re-encrypt on save (via a secure temp file)
|
|
222
|
+
init Scaffold a starter .env
|
|
223
|
+
keys List the keys/paths in a file (no values)
|
|
224
|
+
status Show which keys are encrypted vs plaintext (no values)
|
|
225
|
+
import Encrypt an existing plaintext .env in place (migration)
|
|
226
|
+
|
|
227
|
+
OPTIONS:
|
|
228
|
+
--kms-key-id=<id> REQUIRED. KMS Key ID (ARN, UUID, or alias)
|
|
229
|
+
|
|
230
|
+
--old-kms-key-id=<id> For "rotate": the current key the file is encrypted with
|
|
231
|
+
|
|
232
|
+
--path=<path> Path to config file (default: ./.env)
|
|
233
|
+
|
|
234
|
+
--format=<format> File format: env, json, yaml (auto-detected if omitted)
|
|
235
|
+
|
|
236
|
+
--keys=<keys> Comma-separated list of keys to encrypt
|
|
237
|
+
(encrypts all non-reserved keys if omitted)
|
|
238
|
+
|
|
239
|
+
--patterns=<patterns> Comma-separated glob patterns (** only)
|
|
240
|
+
Example: --patterns="**.password,**.secret_key"
|
|
241
|
+
|
|
242
|
+
--exclude=<keys> Comma-separated keys/paths to exclude
|
|
243
|
+
|
|
244
|
+
--region=<region> AWS region (uses AWS_REGION env var if omitted)
|
|
245
|
+
|
|
246
|
+
--output=<path> Output file (default: overwrite input file)
|
|
247
|
+
|
|
248
|
+
--use-credentials Use explicit AWS credentials instead of IAM role
|
|
249
|
+
Requires AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
|
|
250
|
+
|
|
251
|
+
--dry-run Show what would be encrypted without making changes
|
|
252
|
+
|
|
253
|
+
--help, -h Show this help message
|
|
254
|
+
|
|
255
|
+
--version, -v Show version number
|
|
256
|
+
|
|
257
|
+
AUTHENTICATION:
|
|
258
|
+
By default, this CLI uses IAM roles for AWS authentication.
|
|
259
|
+
This is the recommended approach for production environments.
|
|
260
|
+
|
|
261
|
+
To use explicit credentials (e.g., for local development):
|
|
262
|
+
1. Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables
|
|
263
|
+
2. Pass --use-credentials flag
|
|
264
|
+
|
|
265
|
+
EXAMPLES:
|
|
266
|
+
# Encrypt all keys in .env (kms-key-id is REQUIRED)
|
|
267
|
+
npx @faizahmed/secret-keystore encrypt --kms-key-id="alias/my-key"
|
|
268
|
+
|
|
269
|
+
# Encrypt specific keys only
|
|
270
|
+
npx @faizahmed/secret-keystore encrypt \
|
|
271
|
+
--kms-key-id="arn:aws:kms:us-east-1:123456789:key/abc-123" \
|
|
272
|
+
--keys="DB_PASSWORD,API_KEY"
|
|
273
|
+
|
|
274
|
+
# Encrypt YAML file with patterns
|
|
275
|
+
npx @faizahmed/secret-keystore encrypt \
|
|
276
|
+
--path="./secrets.yaml" \
|
|
277
|
+
--kms-key-id="alias/my-key" \
|
|
278
|
+
--patterns="**.password,**.secret"
|
|
279
|
+
|
|
280
|
+
# Dry run to preview changes
|
|
281
|
+
npx @faizahmed/secret-keystore encrypt \
|
|
282
|
+
--kms-key-id="alias/my-key" \
|
|
283
|
+
--dry-run
|
|
284
|
+
|
|
285
|
+
# Encrypt to a different output file
|
|
286
|
+
npx @faizahmed/secret-keystore encrypt \
|
|
287
|
+
--path="./.env" \
|
|
288
|
+
--output="./.env.encrypted" \
|
|
289
|
+
--kms-key-id="alias/my-key"
|
|
290
|
+
|
|
291
|
+
# Decrypt all encrypted values in a file (in place)
|
|
292
|
+
npx @faizahmed/secret-keystore decrypt \
|
|
293
|
+
--path="./.env" \
|
|
294
|
+
--kms-key-id="alias/my-key"
|
|
295
|
+
|
|
296
|
+
# Decrypt to a separate output file
|
|
297
|
+
npx @faizahmed/secret-keystore decrypt \
|
|
298
|
+
--path="./.env.encrypted" \
|
|
299
|
+
--output="./.env" \
|
|
300
|
+
--kms-key-id="alias/my-key"
|
|
301
|
+
|
|
302
|
+
# Run your app with decrypted secrets injected into its environment
|
|
303
|
+
npx @faizahmed/secret-keystore run \
|
|
304
|
+
--kms-key-id="alias/my-key" -- node server.js
|
|
305
|
+
|
|
306
|
+
# Rotate a file from an old key to a new key
|
|
307
|
+
npx @faizahmed/secret-keystore rotate \
|
|
308
|
+
--old-kms-key-id="alias/old-key" \
|
|
309
|
+
--kms-key-id="alias/new-key"
|
|
310
|
+
|
|
311
|
+
# Edit an encrypted file in $EDITOR (re-encrypts on save)
|
|
312
|
+
npx @faizahmed/secret-keystore edit \
|
|
313
|
+
--kms-key-id="alias/my-key" --path="./.env"
|
|
314
|
+
|
|
315
|
+
# Inspect a file without revealing values
|
|
316
|
+
npx @faizahmed/secret-keystore status --path="./.env"
|
|
317
|
+
`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
321
|
+
// FORMAT DETECTION
|
|
322
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
323
|
+
|
|
324
|
+
function detectFormat(filePath) {
|
|
325
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
326
|
+
|
|
327
|
+
switch (ext) {
|
|
328
|
+
case '.json':
|
|
329
|
+
return 'json';
|
|
330
|
+
case '.yaml':
|
|
331
|
+
case '.yml':
|
|
332
|
+
return 'yaml';
|
|
333
|
+
case '.env':
|
|
334
|
+
default:
|
|
335
|
+
return 'env';
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
340
|
+
// ENCRYPT COMMAND - HELPER FUNCTIONS
|
|
341
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Validate required KMS key ID argument
|
|
345
|
+
*/
|
|
346
|
+
function validateRequiredKmsKeyId(kmsKeyId) {
|
|
347
|
+
if (!kmsKeyId) {
|
|
348
|
+
console.error('❌ Error: --kms-key-id is REQUIRED');
|
|
349
|
+
console.error(' Example: --kms-key-id="arn:aws:kms:us-east-1:123456789:key/abc-123"');
|
|
350
|
+
console.error(' Example: --kms-key-id="alias/my-key"');
|
|
351
|
+
process.exit(1);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
validateKmsKeyId(kmsKeyId);
|
|
356
|
+
} catch (error) {
|
|
357
|
+
console.error(`❌ Error: Invalid KMS Key ID - ${error.message}`);
|
|
358
|
+
process.exit(1);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Validate and resolve file path
|
|
364
|
+
*/
|
|
365
|
+
function resolveAndValidatePath(inputPath) {
|
|
366
|
+
const resolvedPath = path.resolve(process.cwd(), inputPath);
|
|
367
|
+
|
|
368
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
369
|
+
console.error(`❌ Error: File not found: ${resolvedPath}`);
|
|
370
|
+
process.exit(1);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return resolvedPath;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Build AWS credentials from environment variables
|
|
378
|
+
*/
|
|
379
|
+
function buildAwsCredentials() {
|
|
380
|
+
const accessKeyId = process.env.AWS_ACCESS_KEY_ID;
|
|
381
|
+
const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY;
|
|
382
|
+
const sessionToken = process.env.AWS_SESSION_TOKEN;
|
|
383
|
+
|
|
384
|
+
if (!accessKeyId || !secretAccessKey) {
|
|
385
|
+
console.error(
|
|
386
|
+
'❌ Error: --use-credentials requires AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY'
|
|
387
|
+
);
|
|
388
|
+
process.exit(1);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const credentials = { accessKeyId, secretAccessKey };
|
|
392
|
+
if (sessionToken) {
|
|
393
|
+
credentials.sessionToken = sessionToken;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return credentials;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Match a key against a pattern
|
|
401
|
+
*/
|
|
402
|
+
function matchesPattern(key, pattern) {
|
|
403
|
+
if (pattern.startsWith('**.')) {
|
|
404
|
+
const suffix = pattern.slice(3);
|
|
405
|
+
return key.endsWith(suffix) || key === suffix;
|
|
406
|
+
}
|
|
407
|
+
return key === pattern;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Filter keys for dry run preview
|
|
412
|
+
*/
|
|
413
|
+
function filterKeysForDryRun(allKeys, args) {
|
|
414
|
+
let keysToEncrypt = args.keys || allKeys;
|
|
415
|
+
|
|
416
|
+
if (args.patterns) {
|
|
417
|
+
keysToEncrypt = allKeys.filter(k => args.patterns.some(p => matchesPattern(k, p)));
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (args.exclude) {
|
|
421
|
+
keysToEncrypt = keysToEncrypt.filter(k => !args.exclude.includes(k));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return keysToEncrypt;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Run dry run mode - show what would be encrypted
|
|
429
|
+
*/
|
|
430
|
+
function runDryRun(content, format, args) {
|
|
431
|
+
console.log('Keys that would be encrypted:');
|
|
432
|
+
|
|
433
|
+
if (format === 'env') {
|
|
434
|
+
const parsed = parseEnvContent(content);
|
|
435
|
+
const allKeys = parsed.filter(e => e.type === 'keyvalue').map(e => e.key);
|
|
436
|
+
const keysToEncrypt = filterKeysForDryRun(allKeys, args);
|
|
437
|
+
|
|
438
|
+
keysToEncrypt.forEach(k => console.log(` • ${k}`));
|
|
439
|
+
console.log(`\nTotal: ${keysToEncrypt.length} keys`);
|
|
440
|
+
} else {
|
|
441
|
+
console.log(' (pattern matching preview for JSON/YAML not implemented in dry-run)');
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
console.log('\n✨ Dry run complete. No changes made.\n');
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Encrypt content based on format
|
|
449
|
+
*/
|
|
450
|
+
async function encryptByFormat(content, format, kmsKeyId, options) {
|
|
451
|
+
const encryptors = {
|
|
452
|
+
json: encryptKMSJsonContent,
|
|
453
|
+
yaml: encryptKMSYamlContent,
|
|
454
|
+
env: encryptKMSEnvContent
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
const encryptor = encryptors[format] || encryptKMSEnvContent;
|
|
458
|
+
return encryptor(content, kmsKeyId, options);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Print operation summary (encrypt or decrypt)
|
|
463
|
+
*/
|
|
464
|
+
function printSummary(result, verb = 'Encrypted') {
|
|
465
|
+
const processed = (result.encrypted || result.decrypted || []).length;
|
|
466
|
+
console.log('\n📊 Summary:');
|
|
467
|
+
console.log(` ✅ ${verb}: ${processed}`);
|
|
468
|
+
console.log(` ⏭️ Skipped: ${result.skipped.length}`);
|
|
469
|
+
console.log(` ❌ Failed: ${result.failed.length}`);
|
|
470
|
+
|
|
471
|
+
if (result.failed.length > 0) {
|
|
472
|
+
console.log('\n⚠️ Failed keys:');
|
|
473
|
+
result.failed.forEach(f => console.log(` • ${f.key || f.path}: ${f.error.message}`));
|
|
474
|
+
process.exit(1);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
console.log('\n✨ Done!\n');
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
481
|
+
// ENCRYPT COMMAND
|
|
482
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
483
|
+
|
|
484
|
+
async function runEncrypt(args) {
|
|
485
|
+
console.log('\n🔐 @faizahmed/secret-keystore - Encrypt\n');
|
|
486
|
+
|
|
487
|
+
validateRequiredKmsKeyId(args.kmsKeyId);
|
|
488
|
+
|
|
489
|
+
const resolvedPath = resolveAndValidatePath(args.path);
|
|
490
|
+
const format = args.format || detectFormat(resolvedPath);
|
|
491
|
+
const content = fs.readFileSync(resolvedPath, 'utf-8');
|
|
492
|
+
|
|
493
|
+
console.log(`📂 File: ${resolvedPath}`);
|
|
494
|
+
console.log(`📄 Format: ${format}`);
|
|
495
|
+
console.log(`🔑 KMS Key: ${maskKmsKeyId(args.kmsKeyId)}`);
|
|
496
|
+
console.log(args.dryRun ? '🔍 Mode: DRY RUN (no changes will be made)\n' : '');
|
|
497
|
+
|
|
498
|
+
// Build credentials
|
|
499
|
+
const credentials = args.useCredentials ? buildAwsCredentials() : null;
|
|
500
|
+
console.log(
|
|
501
|
+
args.useCredentials
|
|
502
|
+
? '🔑 Using explicit AWS credentials\n'
|
|
503
|
+
: '🔑 Using IAM role (default)\n'
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
const options = {
|
|
507
|
+
aws: {
|
|
508
|
+
credentials,
|
|
509
|
+
region: args.region || process.env.AWS_REGION
|
|
510
|
+
},
|
|
511
|
+
paths: args.keys,
|
|
512
|
+
patterns: args.patterns,
|
|
513
|
+
exclude: args.exclude ? { paths: args.exclude } : undefined,
|
|
514
|
+
logLevel: 'info'
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
if (args.dryRun) {
|
|
518
|
+
runDryRun(content, format, args);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
let result;
|
|
523
|
+
try {
|
|
524
|
+
result = await encryptByFormat(content, format, args.kmsKeyId, options);
|
|
525
|
+
} catch (error) {
|
|
526
|
+
console.error(`\n❌ Error: ${error.message}`);
|
|
527
|
+
if (error.cause) {
|
|
528
|
+
console.error(` Cause: ${error.cause.message}`);
|
|
529
|
+
}
|
|
530
|
+
process.exit(1);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const outputPath = args.output ? path.resolve(process.cwd(), args.output) : resolvedPath;
|
|
534
|
+
|
|
535
|
+
if (result.encrypted.length > 0) {
|
|
536
|
+
fs.writeFileSync(outputPath, result.content, 'utf-8');
|
|
537
|
+
console.log(`\n💾 Written to: ${outputPath}`);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
printSummary(result);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
544
|
+
// DECRYPT COMMAND
|
|
545
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Decrypt content based on format
|
|
549
|
+
*/
|
|
550
|
+
async function decryptByFormat(content, format, kmsKeyId, options) {
|
|
551
|
+
const decryptors = {
|
|
552
|
+
json: decryptKMSJsonContent,
|
|
553
|
+
yaml: decryptKMSYamlContent,
|
|
554
|
+
env: decryptKMSEnvContent
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
const decryptor = decryptors[format] || decryptKMSEnvContent;
|
|
558
|
+
return decryptor(content, kmsKeyId, options);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
async function runDecrypt(args) {
|
|
562
|
+
console.log('\n🔓 @faizahmed/secret-keystore - Decrypt\n');
|
|
563
|
+
|
|
564
|
+
validateRequiredKmsKeyId(args.kmsKeyId);
|
|
565
|
+
|
|
566
|
+
const resolvedPath = resolveAndValidatePath(args.path);
|
|
567
|
+
const format = args.format || detectFormat(resolvedPath);
|
|
568
|
+
const content = fs.readFileSync(resolvedPath, 'utf-8');
|
|
569
|
+
|
|
570
|
+
console.log(`📂 File: ${resolvedPath}`);
|
|
571
|
+
console.log(`📄 Format: ${format}`);
|
|
572
|
+
console.log(`🔑 KMS Key: ${maskKmsKeyId(args.kmsKeyId)}`);
|
|
573
|
+
|
|
574
|
+
// Build credentials
|
|
575
|
+
const credentials = args.useCredentials ? buildAwsCredentials() : null;
|
|
576
|
+
console.log(
|
|
577
|
+
args.useCredentials
|
|
578
|
+
? '🔑 Using explicit AWS credentials\n'
|
|
579
|
+
: '🔑 Using IAM role (default)\n'
|
|
580
|
+
);
|
|
581
|
+
|
|
582
|
+
const options = {
|
|
583
|
+
aws: {
|
|
584
|
+
credentials,
|
|
585
|
+
region: args.region || process.env.AWS_REGION
|
|
586
|
+
},
|
|
587
|
+
paths: args.keys,
|
|
588
|
+
patterns: args.patterns,
|
|
589
|
+
exclude: args.exclude ? { paths: args.exclude } : undefined,
|
|
590
|
+
logLevel: 'info'
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
let result;
|
|
594
|
+
try {
|
|
595
|
+
result = await decryptByFormat(content, format, args.kmsKeyId, options);
|
|
596
|
+
} catch (error) {
|
|
597
|
+
console.error(`\n❌ Error: ${error.message}`);
|
|
598
|
+
if (error.cause) {
|
|
599
|
+
console.error(` Cause: ${error.cause.message}`);
|
|
600
|
+
}
|
|
601
|
+
process.exit(1);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const outputPath = args.output ? path.resolve(process.cwd(), args.output) : resolvedPath;
|
|
605
|
+
|
|
606
|
+
if (result.decrypted.length > 0) {
|
|
607
|
+
fs.writeFileSync(outputPath, result.content, 'utf-8');
|
|
608
|
+
console.log(`\n💾 Written to: ${outputPath}`);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
printSummary(result, 'Decrypted');
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
615
|
+
// SHARED OPTIONS
|
|
616
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
617
|
+
|
|
618
|
+
function buildCliOptions(args) {
|
|
619
|
+
const credentials = args.useCredentials ? buildAwsCredentials() : null;
|
|
620
|
+
return {
|
|
621
|
+
aws: { credentials, region: args.region || process.env.AWS_REGION },
|
|
622
|
+
paths: args.keys,
|
|
623
|
+
patterns: args.patterns,
|
|
624
|
+
exclude: args.exclude ? { paths: args.exclude } : undefined,
|
|
625
|
+
logLevel: 'info'
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/** List keys/paths in a config file (names only — never returns values to callers that print). */
|
|
630
|
+
function listFileEntries(content, format) {
|
|
631
|
+
if (format === 'env') {
|
|
632
|
+
return parseEnvContent(content)
|
|
633
|
+
.filter(e => e.type === 'keyvalue')
|
|
634
|
+
.map(e => ({ name: e.key, value: e.value }));
|
|
635
|
+
}
|
|
636
|
+
const obj = format === 'json' ? JSON.parse(content) : parseYaml(content);
|
|
637
|
+
return getAllPaths(obj).map(p => ({ name: p, value: getByPath(obj, p) }));
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/** Keys/paths whose values are currently encrypted. */
|
|
641
|
+
function encryptedSelection(content, format) {
|
|
642
|
+
return listFileEntries(content, format)
|
|
643
|
+
.filter(e => typeof e.value === 'string' && isAlreadyEncrypted(e.value))
|
|
644
|
+
.map(e => e.name);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
648
|
+
// RUN COMMAND
|
|
649
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
650
|
+
|
|
651
|
+
async function runRun(args) {
|
|
652
|
+
validateRequiredKmsKeyId(args.kmsKeyId);
|
|
653
|
+
|
|
654
|
+
if (!args.exec || args.exec.length === 0) {
|
|
655
|
+
console.error('❌ Error: `run` requires a command after `--`.');
|
|
656
|
+
console.error(' Example: secret-keystore run --kms-key-id="alias/k" -- node server.js');
|
|
657
|
+
process.exit(1);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const credentials = args.useCredentials ? buildAwsCredentials() : null;
|
|
661
|
+
const explicitPath = args.path && args.path !== './.env' ? args.path : undefined;
|
|
662
|
+
|
|
663
|
+
let store;
|
|
664
|
+
try {
|
|
665
|
+
store = await config({
|
|
666
|
+
kmsKeyId: args.kmsKeyId,
|
|
667
|
+
cwd: process.cwd(),
|
|
668
|
+
path: explicitPath,
|
|
669
|
+
aws: { credentials, region: args.region || process.env.AWS_REGION }
|
|
670
|
+
});
|
|
671
|
+
} catch (error) {
|
|
672
|
+
console.error(`\n❌ Error: ${error.message}`);
|
|
673
|
+
process.exit(1);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Secrets are injected into the CHILD's environment only. The parent never
|
|
677
|
+
// places them in its own process.env.
|
|
678
|
+
const childEnv = { ...process.env, ...store.getAll() };
|
|
679
|
+
const [command, ...commandArgs] = args.exec;
|
|
680
|
+
|
|
681
|
+
const child = spawn(command, commandArgs, { stdio: 'inherit', env: childEnv });
|
|
682
|
+
|
|
683
|
+
child.on('error', error => {
|
|
684
|
+
store.destroy();
|
|
685
|
+
console.error(`\n❌ Failed to start "${command}": ${error.message}`);
|
|
686
|
+
process.exit(1);
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
child.on('exit', (code, signal) => {
|
|
690
|
+
store.destroy();
|
|
691
|
+
if (signal) {
|
|
692
|
+
process.kill(process.pid, signal);
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
process.exit(code ?? 0);
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
700
|
+
// ROTATE COMMAND
|
|
701
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
702
|
+
|
|
703
|
+
async function runRotate(args) {
|
|
704
|
+
console.log('\n🔄 @faizahmed/secret-keystore - Rotate\n');
|
|
705
|
+
|
|
706
|
+
validateRequiredKmsKeyId(args.kmsKeyId);
|
|
707
|
+
|
|
708
|
+
if (!args.oldKmsKeyId) {
|
|
709
|
+
console.error('❌ Error: `rotate` requires --old-kms-key-id (the current key).');
|
|
710
|
+
process.exit(1);
|
|
711
|
+
}
|
|
712
|
+
try {
|
|
713
|
+
validateKmsKeyId(args.oldKmsKeyId);
|
|
714
|
+
} catch (error) {
|
|
715
|
+
console.error(`❌ Error: Invalid --old-kms-key-id - ${error.message}`);
|
|
716
|
+
process.exit(1);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const resolvedPath = resolveAndValidatePath(args.path);
|
|
720
|
+
const format = args.format || detectFormat(resolvedPath);
|
|
721
|
+
const content = fs.readFileSync(resolvedPath, 'utf-8');
|
|
722
|
+
|
|
723
|
+
console.log(`📂 File: ${resolvedPath}`);
|
|
724
|
+
console.log(`🔑 Old Key: ${maskKmsKeyId(args.oldKmsKeyId)}`);
|
|
725
|
+
console.log(`🔑 New Key: ${maskKmsKeyId(args.kmsKeyId)}\n`);
|
|
726
|
+
|
|
727
|
+
let result;
|
|
728
|
+
try {
|
|
729
|
+
result = await rotateKMSContent(
|
|
730
|
+
content,
|
|
731
|
+
format,
|
|
732
|
+
args.oldKmsKeyId,
|
|
733
|
+
args.kmsKeyId,
|
|
734
|
+
buildCliOptions(args)
|
|
735
|
+
);
|
|
736
|
+
} catch (error) {
|
|
737
|
+
console.error(`\n❌ Error: ${error.message}`);
|
|
738
|
+
if (error.cause) console.error(` Cause: ${error.cause.message}`);
|
|
739
|
+
process.exit(1);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const outputPath = args.output ? path.resolve(process.cwd(), args.output) : resolvedPath;
|
|
743
|
+
if (result.rotated.length > 0) {
|
|
744
|
+
fs.writeFileSync(outputPath, result.content, 'utf-8');
|
|
745
|
+
console.log(`💾 Written to: ${outputPath}`);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
console.log(`\n📊 Rotated ${result.rotated.length} value(s).`);
|
|
749
|
+
console.log('\n✨ Done!\n');
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
753
|
+
// EDIT COMMAND
|
|
754
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
755
|
+
|
|
756
|
+
function secureDelete(file) {
|
|
757
|
+
try {
|
|
758
|
+
const size = fs.statSync(file).size;
|
|
759
|
+
fs.writeFileSync(file, crypto.randomBytes(size));
|
|
760
|
+
} catch {
|
|
761
|
+
// best-effort overwrite
|
|
762
|
+
}
|
|
763
|
+
try {
|
|
764
|
+
fs.rmSync(file, { force: true });
|
|
765
|
+
} catch {
|
|
766
|
+
// best-effort delete
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
async function runEdit(args) {
|
|
771
|
+
console.log('\n📝 @faizahmed/secret-keystore - Edit\n');
|
|
772
|
+
|
|
773
|
+
validateRequiredKmsKeyId(args.kmsKeyId);
|
|
774
|
+
|
|
775
|
+
const editor = process.env.EDITOR || process.env.VISUAL;
|
|
776
|
+
if (!editor) {
|
|
777
|
+
console.error(
|
|
778
|
+
'❌ Error: no editor found. Set $EDITOR or $VISUAL (e.g. export EDITOR=vim).'
|
|
779
|
+
);
|
|
780
|
+
process.exit(1);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const resolvedPath = resolveAndValidatePath(args.path);
|
|
784
|
+
const format = args.format || detectFormat(resolvedPath);
|
|
785
|
+
const content = fs.readFileSync(resolvedPath, 'utf-8');
|
|
786
|
+
const options = buildCliOptions(args);
|
|
787
|
+
|
|
788
|
+
// Remember which keys were encrypted so we can re-encrypt exactly those on save.
|
|
789
|
+
const selection = encryptedSelection(content, format);
|
|
790
|
+
|
|
791
|
+
let decrypted;
|
|
792
|
+
try {
|
|
793
|
+
decrypted = await decryptByFormat(content, format, args.kmsKeyId, options);
|
|
794
|
+
} catch (error) {
|
|
795
|
+
console.error(`\n❌ Error: ${error.message}`);
|
|
796
|
+
process.exit(1);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sks-edit-'));
|
|
800
|
+
const tmpFile = path.join(tmpDir, path.basename(resolvedPath));
|
|
801
|
+
fs.writeFileSync(tmpFile, decrypted.content, { mode: 0o600 });
|
|
802
|
+
|
|
803
|
+
try {
|
|
804
|
+
const res = spawnSync(editor, [tmpFile], { stdio: 'inherit', shell: true });
|
|
805
|
+
if (res.status !== 0) {
|
|
806
|
+
console.error('\n❌ Editor exited non-zero; aborting (no changes written).');
|
|
807
|
+
process.exit(1);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const edited = fs.readFileSync(tmpFile, 'utf-8');
|
|
811
|
+
|
|
812
|
+
if (selection.length === 0) {
|
|
813
|
+
// Nothing was encrypted; write the edited content back as-is.
|
|
814
|
+
fs.writeFileSync(resolvedPath, edited, 'utf-8');
|
|
815
|
+
console.log(`\n💾 Saved: ${resolvedPath} (no encrypted values to re-encrypt)`);
|
|
816
|
+
} else {
|
|
817
|
+
const reencrypted = await encryptByFormat(edited, format, args.kmsKeyId, {
|
|
818
|
+
...options,
|
|
819
|
+
paths: selection,
|
|
820
|
+
patterns: undefined
|
|
821
|
+
});
|
|
822
|
+
fs.writeFileSync(resolvedPath, reencrypted.content, 'utf-8');
|
|
823
|
+
console.log(`\n💾 Saved & re-encrypted: ${resolvedPath}`);
|
|
824
|
+
console.log(`📊 Re-encrypted ${reencrypted.encrypted.length} value(s).`);
|
|
825
|
+
}
|
|
826
|
+
} finally {
|
|
827
|
+
secureDelete(tmpFile);
|
|
828
|
+
try {
|
|
829
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
830
|
+
} catch {
|
|
831
|
+
// best-effort cleanup
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
console.log('\n✨ Done!\n');
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
839
|
+
// INIT COMMAND
|
|
840
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
841
|
+
|
|
842
|
+
function runInit(args) {
|
|
843
|
+
console.log('\n🚀 @faizahmed/secret-keystore - Init\n');
|
|
844
|
+
|
|
845
|
+
const target = path.resolve(process.cwd(), args.path || './.env');
|
|
846
|
+
if (fs.existsSync(target)) {
|
|
847
|
+
console.error(`❌ Error: ${target} already exists. Refusing to overwrite.`);
|
|
848
|
+
process.exit(1);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
const template = [
|
|
852
|
+
'# secret-keystore configuration',
|
|
853
|
+
'# Reserved keys (never encrypted):',
|
|
854
|
+
'KMS_KEY_ID=alias/your-kms-key',
|
|
855
|
+
'AWS_REGION=us-east-1',
|
|
856
|
+
'',
|
|
857
|
+
'# Your secrets — encrypt with:',
|
|
858
|
+
'# secret-keystore encrypt --kms-key-id="alias/your-kms-key"',
|
|
859
|
+
'DB_PASSWORD=change-me',
|
|
860
|
+
'API_KEY=change-me',
|
|
861
|
+
''
|
|
862
|
+
].join('\n');
|
|
863
|
+
|
|
864
|
+
fs.writeFileSync(target, template, 'utf-8');
|
|
865
|
+
|
|
866
|
+
console.log(`✅ Created ${target}\n`);
|
|
867
|
+
console.log('Next steps:');
|
|
868
|
+
console.log(' 1. Set KMS_KEY_ID and your secret values in the file');
|
|
869
|
+
console.log(' 2. Encrypt: secret-keystore encrypt --kms-key-id="alias/your-kms-key"');
|
|
870
|
+
console.log(' 3. At runtime: const s = await config({ kmsKeyId }) — secrets stay in memory\n');
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
874
|
+
// KEYS & STATUS COMMANDS (read-only — never print secret values)
|
|
875
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
876
|
+
|
|
877
|
+
function runKeys(args) {
|
|
878
|
+
const resolvedPath = resolveAndValidatePath(args.path);
|
|
879
|
+
const format = args.format || detectFormat(resolvedPath);
|
|
880
|
+
const content = fs.readFileSync(resolvedPath, 'utf-8');
|
|
881
|
+
|
|
882
|
+
for (const entry of listFileEntries(content, format)) {
|
|
883
|
+
console.log(entry.name);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function runStatus(args) {
|
|
888
|
+
console.log('\n📋 @faizahmed/secret-keystore - Status\n');
|
|
889
|
+
|
|
890
|
+
const resolvedPath = resolveAndValidatePath(args.path);
|
|
891
|
+
const format = args.format || detectFormat(resolvedPath);
|
|
892
|
+
const content = fs.readFileSync(resolvedPath, 'utf-8');
|
|
893
|
+
|
|
894
|
+
let encrypted = 0;
|
|
895
|
+
let plaintext = 0;
|
|
896
|
+
for (const entry of listFileEntries(content, format)) {
|
|
897
|
+
const isEnc = typeof entry.value === 'string' && isAlreadyEncrypted(entry.value);
|
|
898
|
+
if (isEnc) encrypted += 1;
|
|
899
|
+
else plaintext += 1;
|
|
900
|
+
console.log(` ${isEnc ? '🔒 encrypted' : '🔓 plaintext'} ${entry.name}`);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
const total = encrypted + plaintext;
|
|
904
|
+
console.log(`\n📊 ${encrypted} encrypted, ${plaintext} plaintext, ${total} total\n`);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
908
|
+
// IMPORT COMMAND (encrypt an existing plaintext .env in place)
|
|
909
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
910
|
+
|
|
911
|
+
async function runImport(args) {
|
|
912
|
+
console.log('\n📥 @faizahmed/secret-keystore - Import (migrate plaintext → encrypted)\n');
|
|
913
|
+
// Encrypt all non-reserved keys in place.
|
|
914
|
+
await runEncrypt({ ...args, keys: null, patterns: null });
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
918
|
+
// MAIN
|
|
919
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
920
|
+
|
|
921
|
+
async function main() {
|
|
922
|
+
const args = parseArgs(process.argv.slice(2));
|
|
923
|
+
|
|
924
|
+
// Show version
|
|
925
|
+
if (args.version) {
|
|
926
|
+
printVersion();
|
|
927
|
+
process.exit(0);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// Show help
|
|
931
|
+
if (args.help || process.argv.length <= 2) {
|
|
932
|
+
printHelp();
|
|
933
|
+
process.exit(0);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// Validate and dispatch command
|
|
937
|
+
const commands = {
|
|
938
|
+
encrypt: runEncrypt,
|
|
939
|
+
decrypt: runDecrypt,
|
|
940
|
+
run: runRun,
|
|
941
|
+
rotate: runRotate,
|
|
942
|
+
edit: runEdit,
|
|
943
|
+
init: runInit,
|
|
944
|
+
keys: runKeys,
|
|
945
|
+
status: runStatus,
|
|
946
|
+
import: runImport
|
|
947
|
+
};
|
|
948
|
+
|
|
949
|
+
const handler = commands[args.command];
|
|
950
|
+
if (!handler) {
|
|
951
|
+
console.error(
|
|
952
|
+
'Error: Unknown command. Use one of: ' + Object.keys(commands).join(', ') + '.'
|
|
953
|
+
);
|
|
954
|
+
console.error('Run with --help for usage information.');
|
|
955
|
+
process.exit(1);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
await handler(args);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// Top-level await with IIFE for error handling
|
|
962
|
+
(async () => {
|
|
963
|
+
try {
|
|
964
|
+
await main();
|
|
965
|
+
} catch (error) {
|
|
966
|
+
console.error(`\n❌ Unexpected error: ${error.message}\n`);
|
|
967
|
+
process.exit(1);
|
|
968
|
+
}
|
|
969
|
+
})();
|