env-secrets 0.4.0 → 0.5.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/AGENTS.md +3 -0
- package/README.md +40 -1
- package/__e2e__/index.test.ts +189 -1
- package/__tests__/cli/helpers.test.ts +89 -1
- package/__tests__/vaults/secretsmanager-admin.test.ts +43 -0
- package/dist/cli/helpers.js +66 -4
- package/dist/index.js +245 -10
- package/dist/vaults/secretsmanager-admin.js +34 -1
- package/docs/AWS.md +55 -3
- package/package.json +4 -4
- package/src/cli/helpers.ts +98 -3
- package/src/index.ts +313 -5
- package/src/vaults/secretsmanager-admin.ts +45 -0
- package/website/docs/cli-reference.mdx +36 -2
- package/website/docs/providers/aws-secrets-manager.mdx +28 -1
package/dist/index.js
CHANGED
|
@@ -30,6 +30,19 @@ const exitWithError = (error) => {
|
|
|
30
30
|
console.error(error instanceof Error ? error.message : String(error));
|
|
31
31
|
process.exit(1);
|
|
32
32
|
};
|
|
33
|
+
const parseSecretJsonObject = (secretName, value) => {
|
|
34
|
+
let parsed;
|
|
35
|
+
try {
|
|
36
|
+
parsed = JSON.parse(value);
|
|
37
|
+
}
|
|
38
|
+
catch (_a) {
|
|
39
|
+
throw new Error(`Secret "${secretName}" is not valid JSON. append/remove requires a JSON object secret.`);
|
|
40
|
+
}
|
|
41
|
+
if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object') {
|
|
42
|
+
throw new Error(`Secret "${secretName}" must be a JSON object. append/remove does not support arrays or scalar values.`);
|
|
43
|
+
}
|
|
44
|
+
return parsed;
|
|
45
|
+
};
|
|
33
46
|
// main program
|
|
34
47
|
program
|
|
35
48
|
.name('env-secrets')
|
|
@@ -92,6 +105,7 @@ secretCommand
|
|
|
92
105
|
.requiredOption('-n, --name <name>', 'secret name')
|
|
93
106
|
.option('-v, --value <value>', 'secret value')
|
|
94
107
|
.option('--value-stdin', 'read secret value from stdin')
|
|
108
|
+
.option('-f, --file <path>', 'read secret value from local file')
|
|
95
109
|
.option('-d, --description <description>', 'secret description')
|
|
96
110
|
.option('-k, --kms-key-id <kmsKeyId>', 'kms key id')
|
|
97
111
|
.option('-t, --tag <tag...>', 'tag in key=value format')
|
|
@@ -106,9 +120,9 @@ secretCommand
|
|
|
106
120
|
const output = (_a = options.output) !== null && _a !== void 0 ? _a : (typeof globalOptions.output === 'string'
|
|
107
121
|
? globalOptions.output
|
|
108
122
|
: 'table');
|
|
109
|
-
const value = yield (0, helpers_1.resolveSecretValue)(options.value, options.valueStdin);
|
|
123
|
+
const value = yield (0, helpers_1.resolveSecretValue)(options.value, options.valueStdin, options.file);
|
|
110
124
|
if (!value) {
|
|
111
|
-
throw new Error('Secret value is required. Provide --value
|
|
125
|
+
throw new Error('Secret value is required. Provide --value, --value-stdin, or --file.');
|
|
112
126
|
}
|
|
113
127
|
const result = yield (0, secretsmanager_admin_1.createSecret)({
|
|
114
128
|
name: options.name,
|
|
@@ -135,6 +149,7 @@ secretCommand
|
|
|
135
149
|
.requiredOption('-n, --name <name>', 'secret name')
|
|
136
150
|
.option('-v, --value <value>', 'new secret value')
|
|
137
151
|
.option('--value-stdin', 'read secret value from stdin')
|
|
152
|
+
.option('-f, --file <path>', 'read secret value from local file')
|
|
138
153
|
.option('-d, --description <description>', 'secret description')
|
|
139
154
|
.option('-k, --kms-key-id <kmsKeyId>', 'kms key id')
|
|
140
155
|
.option('-p, --profile <profile>', 'profile to use')
|
|
@@ -148,9 +163,9 @@ secretCommand
|
|
|
148
163
|
const output = (_b = options.output) !== null && _b !== void 0 ? _b : (typeof globalOptions.output === 'string'
|
|
149
164
|
? globalOptions.output
|
|
150
165
|
: 'table');
|
|
151
|
-
const value = yield (0, helpers_1.resolveSecretValue)(options.value, options.valueStdin);
|
|
166
|
+
const value = yield (0, helpers_1.resolveSecretValue)(options.value, options.valueStdin, options.file);
|
|
152
167
|
if (!value && !options.description && !options.kmsKeyId) {
|
|
153
|
-
throw new Error('Nothing to update. Provide --value/--value-stdin, --description, or --kms-key-id.');
|
|
168
|
+
throw new Error('Nothing to update. Provide --value/--value-stdin/--file, --description, or --kms-key-id.');
|
|
154
169
|
}
|
|
155
170
|
const result = yield (0, secretsmanager_admin_1.updateSecret)({
|
|
156
171
|
name: options.name,
|
|
@@ -170,6 +185,226 @@ secretCommand
|
|
|
170
185
|
exitWithError(error);
|
|
171
186
|
}
|
|
172
187
|
}));
|
|
188
|
+
secretCommand
|
|
189
|
+
.command('upsert')
|
|
190
|
+
.alias('import')
|
|
191
|
+
.description('create or update a secret from a local env file')
|
|
192
|
+
.requiredOption('-f, --file <path>', 'path to env file')
|
|
193
|
+
.requiredOption('-n, --name <name>', 'secret name')
|
|
194
|
+
.option('-d, --description <description>', 'secret description')
|
|
195
|
+
.option('-k, --kms-key-id <kmsKeyId>', 'kms key id')
|
|
196
|
+
.option('-t, --tag <tag...>', 'tag in key=value format (applies on create)')
|
|
197
|
+
.option('-p, --profile <profile>', 'profile to use')
|
|
198
|
+
.option('-r, --region <region>', 'region to use')
|
|
199
|
+
.option('--output <format>', 'output format: json|table')
|
|
200
|
+
.action((options, command) => __awaiter(void 0, void 0, void 0, function* () {
|
|
201
|
+
var _c;
|
|
202
|
+
try {
|
|
203
|
+
const { profile, region } = (0, helpers_1.resolveAwsScope)(options, command);
|
|
204
|
+
const globalOptions = command.optsWithGlobals();
|
|
205
|
+
const output = (_c = options.output) !== null && _c !== void 0 ? _c : (typeof globalOptions.output === 'string'
|
|
206
|
+
? globalOptions.output
|
|
207
|
+
: 'table');
|
|
208
|
+
const parsed = yield (0, helpers_1.parseEnvSecretsFile)(options.file);
|
|
209
|
+
if (parsed.entries.length === 0) {
|
|
210
|
+
throw new Error('No env entries found. Include lines like KEY=value or export KEY=value.');
|
|
211
|
+
}
|
|
212
|
+
const payload = Object.fromEntries(parsed.entries.map((entry) => [entry.key, entry.value]));
|
|
213
|
+
const value = JSON.stringify(payload);
|
|
214
|
+
const rows = [];
|
|
215
|
+
let created = 0;
|
|
216
|
+
let updated = 0;
|
|
217
|
+
let failed = 0;
|
|
218
|
+
const skipped = parsed.skipped.length;
|
|
219
|
+
for (const skip of parsed.skipped) {
|
|
220
|
+
rows.push({
|
|
221
|
+
name: options.name,
|
|
222
|
+
status: 'skipped',
|
|
223
|
+
line: String(skip.line),
|
|
224
|
+
message: `${skip.reason}: ${skip.key}`
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
try {
|
|
229
|
+
yield (0, secretsmanager_admin_1.createSecret)({
|
|
230
|
+
name: options.name,
|
|
231
|
+
value,
|
|
232
|
+
description: options.description,
|
|
233
|
+
kmsKeyId: options.kmsKeyId,
|
|
234
|
+
tags: options.tag,
|
|
235
|
+
profile,
|
|
236
|
+
region
|
|
237
|
+
});
|
|
238
|
+
created += 1;
|
|
239
|
+
rows.push({
|
|
240
|
+
name: options.name,
|
|
241
|
+
status: 'created',
|
|
242
|
+
message: `imported ${parsed.entries.length} keys`
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
catch (createError) {
|
|
246
|
+
const message = createError instanceof Error
|
|
247
|
+
? createError.message
|
|
248
|
+
: String(createError);
|
|
249
|
+
if (!/already exists/i.test(message)) {
|
|
250
|
+
throw createError;
|
|
251
|
+
}
|
|
252
|
+
yield (0, secretsmanager_admin_1.updateSecret)({
|
|
253
|
+
name: options.name,
|
|
254
|
+
value,
|
|
255
|
+
description: options.description,
|
|
256
|
+
kmsKeyId: options.kmsKeyId,
|
|
257
|
+
profile,
|
|
258
|
+
region
|
|
259
|
+
});
|
|
260
|
+
updated += 1;
|
|
261
|
+
rows.push({
|
|
262
|
+
name: options.name,
|
|
263
|
+
status: 'updated',
|
|
264
|
+
message: `imported ${parsed.entries.length} keys`
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
failed += 1;
|
|
270
|
+
rows.push({
|
|
271
|
+
name: options.name,
|
|
272
|
+
status: 'failed',
|
|
273
|
+
message: error instanceof Error ? error.message : String(error)
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
const summary = { created, updated, skipped, failed };
|
|
277
|
+
if (output === 'json') {
|
|
278
|
+
// eslint-disable-next-line no-console
|
|
279
|
+
console.log(JSON.stringify({ summary, results: rows }, null, 2));
|
|
280
|
+
if (failed > 0) {
|
|
281
|
+
process.exitCode = 1;
|
|
282
|
+
}
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
(0, helpers_1.printData)((0, helpers_1.asOutputFormat)(output), [
|
|
286
|
+
{ key: 'name', label: 'Name' },
|
|
287
|
+
{ key: 'status', label: 'Status' },
|
|
288
|
+
{ key: 'line', label: 'Line' },
|
|
289
|
+
{ key: 'message', label: 'Message' }
|
|
290
|
+
], rows);
|
|
291
|
+
// eslint-disable-next-line no-console
|
|
292
|
+
console.log(`Summary: created=${created}, updated=${updated}, skipped=${skipped}, failed=${failed}`);
|
|
293
|
+
if (failed > 0) {
|
|
294
|
+
process.exitCode = 1;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
exitWithError(error);
|
|
299
|
+
}
|
|
300
|
+
}));
|
|
301
|
+
secretCommand
|
|
302
|
+
.command('append')
|
|
303
|
+
.description('append or overwrite one key in an existing JSON secret')
|
|
304
|
+
.requiredOption('-n, --name <name>', 'secret name')
|
|
305
|
+
.requiredOption('--key <key>', 'key to append/update')
|
|
306
|
+
.option('-v, --value <value>', 'value for the key')
|
|
307
|
+
.option('--value-stdin', 'read value from stdin')
|
|
308
|
+
.option('-f, --file <path>', 'read value from local file')
|
|
309
|
+
.option('-p, --profile <profile>', 'profile to use')
|
|
310
|
+
.option('-r, --region <region>', 'region to use')
|
|
311
|
+
.option('--output <format>', 'output format: json|table')
|
|
312
|
+
.action((options, command) => __awaiter(void 0, void 0, void 0, function* () {
|
|
313
|
+
var _d;
|
|
314
|
+
try {
|
|
315
|
+
const { profile, region } = (0, helpers_1.resolveAwsScope)(options, command);
|
|
316
|
+
const globalOptions = command.optsWithGlobals();
|
|
317
|
+
const output = (_d = options.output) !== null && _d !== void 0 ? _d : (typeof globalOptions.output === 'string'
|
|
318
|
+
? globalOptions.output
|
|
319
|
+
: 'table');
|
|
320
|
+
const value = yield (0, helpers_1.resolveSecretValue)(options.value, options.valueStdin, options.file);
|
|
321
|
+
if (!value) {
|
|
322
|
+
throw new Error('Append value is required. Provide --value, --value-stdin, or --file.');
|
|
323
|
+
}
|
|
324
|
+
const current = yield (0, secretsmanager_admin_1.getSecretString)({
|
|
325
|
+
name: options.name,
|
|
326
|
+
profile,
|
|
327
|
+
region
|
|
328
|
+
});
|
|
329
|
+
const payload = parseSecretJsonObject(options.name, current);
|
|
330
|
+
payload[options.key] = value;
|
|
331
|
+
const result = yield (0, secretsmanager_admin_1.updateSecret)({
|
|
332
|
+
name: options.name,
|
|
333
|
+
value: JSON.stringify(payload),
|
|
334
|
+
profile,
|
|
335
|
+
region
|
|
336
|
+
});
|
|
337
|
+
(0, helpers_1.printData)((0, helpers_1.asOutputFormat)(output), [
|
|
338
|
+
{ key: 'name', label: 'Name' },
|
|
339
|
+
{ key: 'arn', label: 'ARN' },
|
|
340
|
+
{ key: 'versionId', label: 'VersionId' },
|
|
341
|
+
{ key: 'key', label: 'Key' },
|
|
342
|
+
{ key: 'action', label: 'Action' }
|
|
343
|
+
], [
|
|
344
|
+
Object.assign(Object.assign({}, result), { key: options.key, action: 'appended' })
|
|
345
|
+
]);
|
|
346
|
+
}
|
|
347
|
+
catch (error) {
|
|
348
|
+
exitWithError(error);
|
|
349
|
+
}
|
|
350
|
+
}));
|
|
351
|
+
secretCommand
|
|
352
|
+
.command('remove')
|
|
353
|
+
.description('remove one or more keys from an existing JSON secret')
|
|
354
|
+
.requiredOption('-n, --name <name>', 'secret name')
|
|
355
|
+
.requiredOption('--key <key...>', 'one or more keys to remove')
|
|
356
|
+
.option('-p, --profile <profile>', 'profile to use')
|
|
357
|
+
.option('-r, --region <region>', 'region to use')
|
|
358
|
+
.option('--output <format>', 'output format: json|table')
|
|
359
|
+
.action((options, command) => __awaiter(void 0, void 0, void 0, function* () {
|
|
360
|
+
var _e;
|
|
361
|
+
try {
|
|
362
|
+
const { profile, region } = (0, helpers_1.resolveAwsScope)(options, command);
|
|
363
|
+
const globalOptions = command.optsWithGlobals();
|
|
364
|
+
const output = (_e = options.output) !== null && _e !== void 0 ? _e : (typeof globalOptions.output === 'string'
|
|
365
|
+
? globalOptions.output
|
|
366
|
+
: 'table');
|
|
367
|
+
const keys = options.key;
|
|
368
|
+
const current = yield (0, secretsmanager_admin_1.getSecretString)({
|
|
369
|
+
name: options.name,
|
|
370
|
+
profile,
|
|
371
|
+
region
|
|
372
|
+
});
|
|
373
|
+
const payload = parseSecretJsonObject(options.name, current);
|
|
374
|
+
const removed = [];
|
|
375
|
+
const missing = [];
|
|
376
|
+
for (const key of keys) {
|
|
377
|
+
if (Object.prototype.hasOwnProperty.call(payload, key)) {
|
|
378
|
+
delete payload[key];
|
|
379
|
+
removed.push(key);
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
missing.push(key);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (removed.length === 0) {
|
|
386
|
+
throw new Error(`None of the requested keys exist in secret "${options.name}".`);
|
|
387
|
+
}
|
|
388
|
+
const result = yield (0, secretsmanager_admin_1.updateSecret)({
|
|
389
|
+
name: options.name,
|
|
390
|
+
value: JSON.stringify(payload),
|
|
391
|
+
profile,
|
|
392
|
+
region
|
|
393
|
+
});
|
|
394
|
+
(0, helpers_1.printData)((0, helpers_1.asOutputFormat)(output), [
|
|
395
|
+
{ key: 'name', label: 'Name' },
|
|
396
|
+
{ key: 'arn', label: 'ARN' },
|
|
397
|
+
{ key: 'versionId', label: 'VersionId' },
|
|
398
|
+
{ key: 'removed', label: 'RemovedKeys' },
|
|
399
|
+
{ key: 'missing', label: 'MissingKeys' }
|
|
400
|
+
], [
|
|
401
|
+
Object.assign(Object.assign({}, result), { removed: removed.join(','), missing: missing.join(',') })
|
|
402
|
+
]);
|
|
403
|
+
}
|
|
404
|
+
catch (error) {
|
|
405
|
+
exitWithError(error);
|
|
406
|
+
}
|
|
407
|
+
}));
|
|
173
408
|
secretCommand
|
|
174
409
|
.command('list')
|
|
175
410
|
.description('list secrets in AWS Secrets Manager')
|
|
@@ -179,11 +414,11 @@ secretCommand
|
|
|
179
414
|
.option('-r, --region <region>', 'region to use')
|
|
180
415
|
.option('--output <format>', 'output format: json|table')
|
|
181
416
|
.action((options, command) => __awaiter(void 0, void 0, void 0, function* () {
|
|
182
|
-
var
|
|
417
|
+
var _f;
|
|
183
418
|
try {
|
|
184
419
|
const { profile, region } = (0, helpers_1.resolveAwsScope)(options, command);
|
|
185
420
|
const globalOptions = command.optsWithGlobals();
|
|
186
|
-
const output = (
|
|
421
|
+
const output = (_f = options.output) !== null && _f !== void 0 ? _f : (typeof globalOptions.output === 'string'
|
|
187
422
|
? globalOptions.output
|
|
188
423
|
: 'table');
|
|
189
424
|
const result = yield (0, secretsmanager_admin_1.listSecrets)({
|
|
@@ -215,11 +450,11 @@ secretCommand
|
|
|
215
450
|
.option('-r, --region <region>', 'region to use')
|
|
216
451
|
.option('--output <format>', 'output format: json|table')
|
|
217
452
|
.action((options, command) => __awaiter(void 0, void 0, void 0, function* () {
|
|
218
|
-
var
|
|
453
|
+
var _g;
|
|
219
454
|
try {
|
|
220
455
|
const { profile, region } = (0, helpers_1.resolveAwsScope)(options, command);
|
|
221
456
|
const globalOptions = command.optsWithGlobals();
|
|
222
|
-
const output = (
|
|
457
|
+
const output = (_g = options.output) !== null && _g !== void 0 ? _g : (typeof globalOptions.output === 'string'
|
|
223
458
|
? globalOptions.output
|
|
224
459
|
: 'table');
|
|
225
460
|
const result = yield (0, secretsmanager_admin_1.getSecretMetadata)({
|
|
@@ -261,11 +496,11 @@ secretCommand
|
|
|
261
496
|
.option('-r, --region <region>', 'region to use')
|
|
262
497
|
.option('--output <format>', 'output format: json|table')
|
|
263
498
|
.action((options, command) => __awaiter(void 0, void 0, void 0, function* () {
|
|
264
|
-
var
|
|
499
|
+
var _h;
|
|
265
500
|
try {
|
|
266
501
|
const { profile, region } = (0, helpers_1.resolveAwsScope)(options, command);
|
|
267
502
|
const globalOptions = command.optsWithGlobals();
|
|
268
|
-
const output = (
|
|
503
|
+
const output = (_h = options.output) !== null && _h !== void 0 ? _h : (typeof globalOptions.output === 'string'
|
|
269
504
|
? globalOptions.output
|
|
270
505
|
: 'table');
|
|
271
506
|
if (!options.yes) {
|
|
@@ -12,7 +12,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
12
12
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
13
|
};
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.deleteSecret = exports.getSecretMetadata = exports.listSecrets = exports.updateSecret = exports.createSecret = exports.validateSecretName = void 0;
|
|
15
|
+
exports.deleteSecret = exports.getSecretString = exports.secretExists = exports.getSecretMetadata = exports.listSecrets = exports.updateSecret = exports.createSecret = exports.validateSecretName = void 0;
|
|
16
16
|
const client_secrets_manager_1 = require("@aws-sdk/client-secrets-manager");
|
|
17
17
|
const client_sts_1 = require("@aws-sdk/client-sts");
|
|
18
18
|
const debug_1 = __importDefault(require("debug"));
|
|
@@ -213,6 +213,39 @@ const getSecretMetadata = (options) => __awaiter(void 0, void 0, void 0, functio
|
|
|
213
213
|
}
|
|
214
214
|
});
|
|
215
215
|
exports.getSecretMetadata = getSecretMetadata;
|
|
216
|
+
const secretExists = (options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
217
|
+
(0, exports.validateSecretName)(options.name);
|
|
218
|
+
debug('secretExists called', { name: options.name });
|
|
219
|
+
const client = yield createClient(options);
|
|
220
|
+
try {
|
|
221
|
+
yield client.send(new client_secrets_manager_1.DescribeSecretCommand({ SecretId: options.name }));
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
const awsError = error;
|
|
226
|
+
if ((awsError === null || awsError === void 0 ? void 0 : awsError.name) === 'ResourceNotFoundException') {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
return mapAwsError(error, options.name);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
exports.secretExists = secretExists;
|
|
233
|
+
const getSecretString = (options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
234
|
+
(0, exports.validateSecretName)(options.name);
|
|
235
|
+
debug('getSecretString called', { name: options.name });
|
|
236
|
+
const client = yield createClient(options);
|
|
237
|
+
try {
|
|
238
|
+
const result = yield client.send(new client_secrets_manager_1.GetSecretValueCommand({ SecretId: options.name }));
|
|
239
|
+
if (typeof result.SecretString !== 'string') {
|
|
240
|
+
throw new Error(`Secret "${options.name}" is not stored as a string value and cannot be edited with append/remove.`);
|
|
241
|
+
}
|
|
242
|
+
return result.SecretString;
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
return mapAwsError(error, options.name);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
exports.getSecretString = getSecretString;
|
|
216
249
|
const deleteSecret = (options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
217
250
|
(0, exports.validateSecretName)(options.name);
|
|
218
251
|
debug('deleteSecret called', {
|
package/docs/AWS.md
CHANGED
|
@@ -153,6 +153,9 @@ In addition to injecting variables into a process, `env-secrets` can manage AWS
|
|
|
153
153
|
|
|
154
154
|
- `env-secrets aws secret create`
|
|
155
155
|
- `env-secrets aws secret update`
|
|
156
|
+
- `env-secrets aws secret append`
|
|
157
|
+
- `env-secrets aws secret remove`
|
|
158
|
+
- `env-secrets aws secret upsert` (alias: `import`)
|
|
156
159
|
- `env-secrets aws secret list`
|
|
157
160
|
- `env-secrets aws secret get`
|
|
158
161
|
- `env-secrets aws secret delete`
|
|
@@ -160,6 +163,31 @@ In addition to injecting variables into a process, `env-secrets` can manage AWS
|
|
|
160
163
|
`aws secret` subcommands consistently honor `--region`, `--profile`, and `--output`.
|
|
161
164
|
Use these options directly with each subcommand.
|
|
162
165
|
|
|
166
|
+
### `aws -s` vs `aws secret ...`
|
|
167
|
+
|
|
168
|
+
- `env-secrets aws -s <secret-name> -- <command>`: retrieves a secret value and injects it into the environment for the spawned process (or use `-o <file>` to write exports to a file).
|
|
169
|
+
- `env-secrets aws secret ...`: management commands only (`create`, `update`, `append`, `remove`, `upsert/import`, `list`, `get`, `delete`).
|
|
170
|
+
|
|
171
|
+
Example:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
# inject secret values
|
|
175
|
+
env-secrets aws -s my-app/dev/api -r us-east-1 -- node app.js
|
|
176
|
+
|
|
177
|
+
# manage secrets
|
|
178
|
+
env-secrets aws secret get -n my-app/dev/api -r us-east-1 --output json
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Load secrets into your current shell
|
|
182
|
+
|
|
183
|
+
`env-secrets aws -s ... -- <command>` injects variables into the spawned child process only.
|
|
184
|
+
If you want variables in your current shell session, write exports to a file and source it:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
env-secrets aws -s my-app/dev/api -r us-east-1 -o secrets.env
|
|
188
|
+
source secrets.env
|
|
189
|
+
```
|
|
190
|
+
|
|
163
191
|
### Secret Management Examples
|
|
164
192
|
|
|
165
193
|
1. **Create a secret with inline value:**
|
|
@@ -184,7 +212,28 @@ Use these options directly with each subcommand.
|
|
|
184
212
|
env-secrets aws secret update -n my-app/dev/api -v '{"API_KEY":"rotated"}' -r us-east-1
|
|
185
213
|
```
|
|
186
214
|
|
|
187
|
-
4. **
|
|
215
|
+
4. **Upsert from an env file into one JSON secret (`export KEY=value` or `KEY=value`):**
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
env-secrets aws secret upsert --file .env --name my-app/dev -r us-east-1 --output json
|
|
219
|
+
# alias:
|
|
220
|
+
env-secrets aws secret import --file .env --name my-app/dev -r us-east-1 --output json
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
This creates/updates one secret named `my-app/dev` with a JSON payload like:
|
|
224
|
+
|
|
225
|
+
```json
|
|
226
|
+
{ "API_KEY": "abc123", "DATABASE_URL": "postgres://..." }
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
5. **Append/remove keys in an existing JSON secret:**
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
env-secrets aws secret append -n my-app/dev --key JIRA_EMAIL_TOKEN -v blah -r us-east-1
|
|
233
|
+
env-secrets aws secret remove -n my-app/dev --key OLD_TOKEN -r us-east-1
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
6. **List secrets by prefix:**
|
|
188
237
|
|
|
189
238
|
```bash
|
|
190
239
|
env-secrets aws secret list --prefix my-app/dev -r us-east-1 --output table
|
|
@@ -197,13 +246,13 @@ Use these options directly with each subcommand.
|
|
|
197
246
|
env-secrets aws secret list --prefix my-app/dev -r us-east-1 --output json
|
|
198
247
|
```
|
|
199
248
|
|
|
200
|
-
|
|
249
|
+
7. **Get metadata and version info (without printing secret value):**
|
|
201
250
|
|
|
202
251
|
```bash
|
|
203
252
|
env-secrets aws secret get -n my-app/dev/api -r us-east-1 --output json
|
|
204
253
|
```
|
|
205
254
|
|
|
206
|
-
|
|
255
|
+
8. **Delete with explicit confirmation:**
|
|
207
256
|
|
|
208
257
|
```bash
|
|
209
258
|
env-secrets aws secret delete -n my-app/dev/raw --recovery-days 7 --yes -r us-east-1
|
|
@@ -212,6 +261,9 @@ Use these options directly with each subcommand.
|
|
|
212
261
|
### Secret Management Safety Notes
|
|
213
262
|
|
|
214
263
|
- `delete` requires `--yes`.
|
|
264
|
+
- `create`/`update` accept `--value`, `--value-stdin`, or `--file` (use only one).
|
|
265
|
+
- `append` and `remove` require the secret value to be a JSON object.
|
|
266
|
+
- `upsert/import --file --name` parses `export KEY=value` and `KEY=value`, stores them as one JSON secret object, ignores blank lines/comments, and reports `created`, `updated`, `skipped`, and `failed`.
|
|
215
267
|
- Use `--value-stdin` to avoid shell history leakage for sensitive values.
|
|
216
268
|
- Use either `--recovery-days` or `--force-delete-without-recovery` for delete operations.
|
|
217
269
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "env-secrets",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "get secrets from a secrets vault and inject them into the running environment",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"author": "Mark C Allen (@markcallen)",
|
|
@@ -52,9 +52,9 @@
|
|
|
52
52
|
"typescript": "^4.9.5"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@aws-sdk/client-secrets-manager": "^3.
|
|
56
|
-
"@aws-sdk/client-sts": "^3.
|
|
57
|
-
"@aws-sdk/credential-providers": "^3.
|
|
55
|
+
"@aws-sdk/client-secrets-manager": "^3.996.0",
|
|
56
|
+
"@aws-sdk/client-sts": "^3.996.0",
|
|
57
|
+
"@aws-sdk/credential-providers": "^3.996.0",
|
|
58
58
|
"commander": "^9.5.0",
|
|
59
59
|
"debug": "^4.4.3"
|
|
60
60
|
},
|
package/src/cli/helpers.ts
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
|
|
1
3
|
export type OutputFormat = 'json' | 'table';
|
|
2
4
|
export interface AwsScopeOptions {
|
|
3
5
|
profile?: string;
|
|
4
6
|
region?: string;
|
|
5
7
|
}
|
|
8
|
+
export interface EnvSecretEntry {
|
|
9
|
+
key: string;
|
|
10
|
+
value: string;
|
|
11
|
+
line: number;
|
|
12
|
+
}
|
|
13
|
+
export interface ParsedEnvSecrets {
|
|
14
|
+
entries: EnvSecretEntry[];
|
|
15
|
+
skipped: Array<{ key: string; line: number; reason: string }>;
|
|
16
|
+
}
|
|
6
17
|
|
|
7
18
|
interface CommandLikeWithGlobalOpts {
|
|
8
19
|
optsWithGlobals?: () => Record<string, unknown>;
|
|
@@ -105,10 +116,18 @@ export const readStdin = async (stdin: NodeJS.ReadStream = process.stdin) => {
|
|
|
105
116
|
|
|
106
117
|
export const resolveSecretValue = async (
|
|
107
118
|
value?: string,
|
|
108
|
-
valueStdin?: boolean
|
|
119
|
+
valueStdin?: boolean,
|
|
120
|
+
valueFile?: string
|
|
109
121
|
): Promise<string | undefined> => {
|
|
110
|
-
|
|
111
|
-
|
|
122
|
+
const providedSources = [
|
|
123
|
+
value !== undefined,
|
|
124
|
+
valueStdin === true,
|
|
125
|
+
valueFile !== undefined
|
|
126
|
+
].filter(Boolean).length;
|
|
127
|
+
if (providedSources > 1) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
'Use only one secret value source: --value, --value-stdin, or --file.'
|
|
130
|
+
);
|
|
112
131
|
}
|
|
113
132
|
|
|
114
133
|
if (valueStdin) {
|
|
@@ -120,9 +139,85 @@ export const resolveSecretValue = async (
|
|
|
120
139
|
return await readStdin();
|
|
121
140
|
}
|
|
122
141
|
|
|
142
|
+
if (valueFile) {
|
|
143
|
+
const content = await readFile(valueFile, 'utf8');
|
|
144
|
+
return content.replace(/\r?\n$/, '');
|
|
145
|
+
}
|
|
146
|
+
|
|
123
147
|
return value;
|
|
124
148
|
};
|
|
125
149
|
|
|
150
|
+
const parseEnvLine = (
|
|
151
|
+
line: string,
|
|
152
|
+
lineNumber: number
|
|
153
|
+
): { key: string; value: string } | undefined => {
|
|
154
|
+
const trimmed = line.trim();
|
|
155
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const candidate = trimmed.startsWith('export ')
|
|
160
|
+
? trimmed.slice('export '.length).trimStart()
|
|
161
|
+
: trimmed;
|
|
162
|
+
const separatorIndex = candidate.indexOf('=');
|
|
163
|
+
|
|
164
|
+
if (separatorIndex <= 0) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`Malformed env line ${lineNumber}. Expected KEY=value or export KEY=value.`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const key = candidate.slice(0, separatorIndex).trim();
|
|
171
|
+
const value = candidate.slice(separatorIndex + 1).trim();
|
|
172
|
+
|
|
173
|
+
if (!key) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`Malformed env line ${lineNumber}. Expected KEY=value or export KEY=value.`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return { key, value };
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
export const parseEnvSecrets = (content: string): ParsedEnvSecrets => {
|
|
183
|
+
const seenKeys = new Set<string>();
|
|
184
|
+
const entries: EnvSecretEntry[] = [];
|
|
185
|
+
const skipped: Array<{ key: string; line: number; reason: string }> = [];
|
|
186
|
+
|
|
187
|
+
const lines = content.split(/\r?\n/);
|
|
188
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
189
|
+
const parsed = parseEnvLine(lines[index], index + 1);
|
|
190
|
+
if (!parsed) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (seenKeys.has(parsed.key)) {
|
|
195
|
+
skipped.push({
|
|
196
|
+
key: parsed.key,
|
|
197
|
+
line: index + 1,
|
|
198
|
+
reason: 'duplicate key'
|
|
199
|
+
});
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
seenKeys.add(parsed.key);
|
|
204
|
+
entries.push({
|
|
205
|
+
key: parsed.key,
|
|
206
|
+
value: parsed.value,
|
|
207
|
+
line: index + 1
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return { entries, skipped };
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
export const parseEnvSecretsFile = async (
|
|
215
|
+
path: string
|
|
216
|
+
): Promise<ParsedEnvSecrets> => {
|
|
217
|
+
const content = await readFile(path, 'utf8');
|
|
218
|
+
return parseEnvSecrets(content);
|
|
219
|
+
};
|
|
220
|
+
|
|
126
221
|
export const resolveAwsScope = (
|
|
127
222
|
options: AwsScopeOptions,
|
|
128
223
|
command?: CommandLikeWithGlobalOpts
|