env-secrets 0.3.2 → 0.4.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/.codex/rules/cicd.md +170 -0
- package/.codex/rules/linting.md +174 -0
- package/.codex/rules/local-dev-badges.md +93 -0
- package/.codex/rules/local-dev-env.md +271 -0
- package/.codex/rules/local-dev-license.md +104 -0
- package/.codex/rules/local-dev-mcp.md +72 -0
- package/.codex/rules/logging.md +358 -0
- package/.codex/rules/observability.md +25 -0
- package/.codex/rules/testing.md +133 -0
- package/.github/workflows/lint.yaml +7 -8
- package/.github/workflows/release.yml +1 -1
- package/.github/workflows/unittests.yaml +1 -1
- package/AGENTS.md +10 -4
- package/README.md +14 -9
- package/__e2e__/README.md +2 -5
- package/__e2e__/index.test.ts +152 -1
- package/__e2e__/utils/test-utils.ts +61 -1
- package/__tests__/cli/helpers.test.ts +129 -0
- package/__tests__/vaults/aws-config.test.ts +85 -0
- package/__tests__/vaults/secretsmanager-admin.test.ts +312 -0
- package/__tests__/vaults/secretsmanager.test.ts +57 -20
- package/dist/cli/helpers.js +110 -0
- package/dist/index.js +221 -2
- package/dist/vaults/aws-config.js +29 -0
- package/dist/vaults/secretsmanager-admin.js +240 -0
- package/dist/vaults/secretsmanager.js +20 -16
- package/docs/AWS.md +78 -3
- package/eslint.config.js +67 -0
- package/jest.e2e.config.js +1 -0
- package/package.json +23 -13
- package/src/cli/helpers.ts +144 -0
- package/src/index.ts +287 -2
- package/src/vaults/aws-config.ts +51 -0
- package/src/vaults/secretsmanager-admin.ts +352 -0
- package/src/vaults/secretsmanager.ts +32 -20
- package/website/docs/cli-reference.mdx +67 -0
- package/website/docs/examples.mdx +1 -1
- package/website/docs/installation.mdx +1 -1
- package/website/docs/providers/aws-secrets-manager.mdx +32 -0
- package/.eslintignore +0 -4
- package/.eslintrc +0 -18
- package/.lintstagedrc +0 -4
package/src/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
/* istanbul ignore file */
|
|
2
3
|
|
|
3
4
|
import { Command, Argument } from 'commander';
|
|
4
5
|
import { spawn } from 'node:child_process';
|
|
@@ -7,12 +8,32 @@ import Debug from 'debug';
|
|
|
7
8
|
|
|
8
9
|
import { LIB_VERSION } from './version';
|
|
9
10
|
import { secretsmanager } from './vaults/secretsmanager';
|
|
11
|
+
import {
|
|
12
|
+
createSecret,
|
|
13
|
+
updateSecret,
|
|
14
|
+
listSecrets,
|
|
15
|
+
getSecretMetadata,
|
|
16
|
+
deleteSecret
|
|
17
|
+
} from './vaults/secretsmanager-admin';
|
|
18
|
+
import {
|
|
19
|
+
asOutputFormat,
|
|
20
|
+
printData,
|
|
21
|
+
parseRecoveryDays,
|
|
22
|
+
resolveAwsScope,
|
|
23
|
+
resolveSecretValue
|
|
24
|
+
} from './cli/helpers';
|
|
10
25
|
import { objectToExport } from './vaults/utils';
|
|
11
26
|
|
|
12
27
|
const debug = Debug('env-secrets');
|
|
13
28
|
|
|
14
29
|
const program = new Command();
|
|
15
30
|
|
|
31
|
+
const exitWithError = (error: unknown) => {
|
|
32
|
+
// eslint-disable-next-line no-console
|
|
33
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
34
|
+
process.exit(1);
|
|
35
|
+
};
|
|
36
|
+
|
|
16
37
|
// main program
|
|
17
38
|
program
|
|
18
39
|
.name('env-secrets')
|
|
@@ -22,11 +43,11 @@ program
|
|
|
22
43
|
.version(LIB_VERSION);
|
|
23
44
|
|
|
24
45
|
// aws secretsmanager
|
|
25
|
-
program
|
|
46
|
+
const awsCommand = program
|
|
26
47
|
.command('aws')
|
|
27
48
|
.description('get secrets from AWS secrets manager')
|
|
28
49
|
.addArgument(new Argument('[program...]', 'program to run'))
|
|
29
|
-
.
|
|
50
|
+
.option('-s, --secret <secret>', 'secret to get')
|
|
30
51
|
.option('-p, --profile <profile>', 'profile to use')
|
|
31
52
|
.option('-r, --region <region>', 'region to use')
|
|
32
53
|
.option(
|
|
@@ -34,6 +55,12 @@ program
|
|
|
34
55
|
'output secrets to file instead of environment variables'
|
|
35
56
|
)
|
|
36
57
|
.action(async (program, options) => {
|
|
58
|
+
if (!options.secret) {
|
|
59
|
+
exitWithError(
|
|
60
|
+
new Error('Missing required option --secret for this command.')
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
37
64
|
const secrets = await secretsmanager(options);
|
|
38
65
|
debug(secrets);
|
|
39
66
|
|
|
@@ -75,4 +102,262 @@ program
|
|
|
75
102
|
}
|
|
76
103
|
});
|
|
77
104
|
|
|
105
|
+
const secretCommand = awsCommand
|
|
106
|
+
.command('secret')
|
|
107
|
+
.description('manage AWS secrets');
|
|
108
|
+
|
|
109
|
+
secretCommand
|
|
110
|
+
.command('create')
|
|
111
|
+
.description('create a secret in AWS Secrets Manager')
|
|
112
|
+
.requiredOption('-n, --name <name>', 'secret name')
|
|
113
|
+
.option('-v, --value <value>', 'secret value')
|
|
114
|
+
.option('--value-stdin', 'read secret value from stdin')
|
|
115
|
+
.option('-d, --description <description>', 'secret description')
|
|
116
|
+
.option('-k, --kms-key-id <kmsKeyId>', 'kms key id')
|
|
117
|
+
.option('-t, --tag <tag...>', 'tag in key=value format')
|
|
118
|
+
.option('-p, --profile <profile>', 'profile to use')
|
|
119
|
+
.option('-r, --region <region>', 'region to use')
|
|
120
|
+
.option('--output <format>', 'output format: json|table')
|
|
121
|
+
.action(async (options, command) => {
|
|
122
|
+
try {
|
|
123
|
+
const { profile, region } = resolveAwsScope(options, command);
|
|
124
|
+
const globalOptions = command.optsWithGlobals();
|
|
125
|
+
const output =
|
|
126
|
+
options.output ??
|
|
127
|
+
(typeof globalOptions.output === 'string'
|
|
128
|
+
? globalOptions.output
|
|
129
|
+
: 'table');
|
|
130
|
+
const value = await resolveSecretValue(options.value, options.valueStdin);
|
|
131
|
+
if (!value) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
'Secret value is required. Provide --value or --value-stdin.'
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const result = await createSecret({
|
|
138
|
+
name: options.name,
|
|
139
|
+
value,
|
|
140
|
+
description: options.description,
|
|
141
|
+
kmsKeyId: options.kmsKeyId,
|
|
142
|
+
tags: options.tag,
|
|
143
|
+
profile,
|
|
144
|
+
region
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
printData(
|
|
148
|
+
asOutputFormat(output),
|
|
149
|
+
[
|
|
150
|
+
{ key: 'name', label: 'Name' },
|
|
151
|
+
{ key: 'arn', label: 'ARN' },
|
|
152
|
+
{ key: 'versionId', label: 'VersionId' }
|
|
153
|
+
],
|
|
154
|
+
[result]
|
|
155
|
+
);
|
|
156
|
+
} catch (error: unknown) {
|
|
157
|
+
exitWithError(error);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
secretCommand
|
|
162
|
+
.command('update')
|
|
163
|
+
.description('update secret value or metadata')
|
|
164
|
+
.requiredOption('-n, --name <name>', 'secret name')
|
|
165
|
+
.option('-v, --value <value>', 'new secret value')
|
|
166
|
+
.option('--value-stdin', 'read secret value from stdin')
|
|
167
|
+
.option('-d, --description <description>', 'secret description')
|
|
168
|
+
.option('-k, --kms-key-id <kmsKeyId>', 'kms key id')
|
|
169
|
+
.option('-p, --profile <profile>', 'profile to use')
|
|
170
|
+
.option('-r, --region <region>', 'region to use')
|
|
171
|
+
.option('--output <format>', 'output format: json|table')
|
|
172
|
+
.action(async (options, command) => {
|
|
173
|
+
try {
|
|
174
|
+
const { profile, region } = resolveAwsScope(options, command);
|
|
175
|
+
const globalOptions = command.optsWithGlobals();
|
|
176
|
+
const output =
|
|
177
|
+
options.output ??
|
|
178
|
+
(typeof globalOptions.output === 'string'
|
|
179
|
+
? globalOptions.output
|
|
180
|
+
: 'table');
|
|
181
|
+
const value = await resolveSecretValue(options.value, options.valueStdin);
|
|
182
|
+
if (!value && !options.description && !options.kmsKeyId) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
'Nothing to update. Provide --value/--value-stdin, --description, or --kms-key-id.'
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const result = await updateSecret({
|
|
189
|
+
name: options.name,
|
|
190
|
+
value,
|
|
191
|
+
description: options.description,
|
|
192
|
+
kmsKeyId: options.kmsKeyId,
|
|
193
|
+
profile,
|
|
194
|
+
region
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
printData(
|
|
198
|
+
asOutputFormat(output),
|
|
199
|
+
[
|
|
200
|
+
{ key: 'name', label: 'Name' },
|
|
201
|
+
{ key: 'arn', label: 'ARN' },
|
|
202
|
+
{ key: 'versionId', label: 'VersionId' }
|
|
203
|
+
],
|
|
204
|
+
[result]
|
|
205
|
+
);
|
|
206
|
+
} catch (error: unknown) {
|
|
207
|
+
exitWithError(error);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
secretCommand
|
|
212
|
+
.command('list')
|
|
213
|
+
.description('list secrets in AWS Secrets Manager')
|
|
214
|
+
.option('--prefix <prefix>', 'filter secrets by name prefix')
|
|
215
|
+
.option('-t, --tag <tag...>', 'filter tags in key=value format')
|
|
216
|
+
.option('-p, --profile <profile>', 'profile to use')
|
|
217
|
+
.option('-r, --region <region>', 'region to use')
|
|
218
|
+
.option('--output <format>', 'output format: json|table')
|
|
219
|
+
.action(async (options, command) => {
|
|
220
|
+
try {
|
|
221
|
+
const { profile, region } = resolveAwsScope(options, command);
|
|
222
|
+
const globalOptions = command.optsWithGlobals();
|
|
223
|
+
const output =
|
|
224
|
+
options.output ??
|
|
225
|
+
(typeof globalOptions.output === 'string'
|
|
226
|
+
? globalOptions.output
|
|
227
|
+
: 'table');
|
|
228
|
+
const result = await listSecrets({
|
|
229
|
+
prefix: options.prefix,
|
|
230
|
+
tags: options.tag,
|
|
231
|
+
profile,
|
|
232
|
+
region
|
|
233
|
+
});
|
|
234
|
+
const rows = result.map((secret) => ({
|
|
235
|
+
name: secret.name,
|
|
236
|
+
description: secret.description,
|
|
237
|
+
lastChangedDate: secret.lastChangedDate
|
|
238
|
+
}));
|
|
239
|
+
|
|
240
|
+
printData(
|
|
241
|
+
asOutputFormat(output),
|
|
242
|
+
[
|
|
243
|
+
{ key: 'name', label: 'Name' },
|
|
244
|
+
{ key: 'description', label: 'Description' },
|
|
245
|
+
{ key: 'lastChangedDate', label: 'LastChanged' }
|
|
246
|
+
],
|
|
247
|
+
rows
|
|
248
|
+
);
|
|
249
|
+
} catch (error: unknown) {
|
|
250
|
+
exitWithError(error);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
secretCommand
|
|
255
|
+
.command('get')
|
|
256
|
+
.description('get secret metadata and version information')
|
|
257
|
+
.requiredOption('-n, --name <name>', 'secret name')
|
|
258
|
+
.option('-p, --profile <profile>', 'profile to use')
|
|
259
|
+
.option('-r, --region <region>', 'region to use')
|
|
260
|
+
.option('--output <format>', 'output format: json|table')
|
|
261
|
+
.action(async (options, command) => {
|
|
262
|
+
try {
|
|
263
|
+
const { profile, region } = resolveAwsScope(options, command);
|
|
264
|
+
const globalOptions = command.optsWithGlobals();
|
|
265
|
+
const output =
|
|
266
|
+
options.output ??
|
|
267
|
+
(typeof globalOptions.output === 'string'
|
|
268
|
+
? globalOptions.output
|
|
269
|
+
: 'table');
|
|
270
|
+
const result = await getSecretMetadata({
|
|
271
|
+
name: options.name,
|
|
272
|
+
profile,
|
|
273
|
+
region
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const row = {
|
|
277
|
+
name: result.name,
|
|
278
|
+
arn: result.arn,
|
|
279
|
+
description: result.description,
|
|
280
|
+
kmsKeyId: result.kmsKeyId,
|
|
281
|
+
createdDate: result.createdDate,
|
|
282
|
+
lastChangedDate: result.lastChangedDate,
|
|
283
|
+
deletedDate: result.deletedDate
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
printData(
|
|
287
|
+
asOutputFormat(output),
|
|
288
|
+
[
|
|
289
|
+
{ key: 'name', label: 'Name' },
|
|
290
|
+
{ key: 'arn', label: 'ARN' },
|
|
291
|
+
{ key: 'description', label: 'Description' },
|
|
292
|
+
{ key: 'kmsKeyId', label: 'KmsKeyId' },
|
|
293
|
+
{ key: 'createdDate', label: 'Created' },
|
|
294
|
+
{ key: 'lastChangedDate', label: 'LastChanged' },
|
|
295
|
+
{ key: 'deletedDate', label: 'Deleted' }
|
|
296
|
+
],
|
|
297
|
+
[row]
|
|
298
|
+
);
|
|
299
|
+
} catch (error: unknown) {
|
|
300
|
+
exitWithError(error);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
secretCommand
|
|
305
|
+
.command('delete')
|
|
306
|
+
.description('delete a secret in AWS Secrets Manager')
|
|
307
|
+
.requiredOption('-n, --name <name>', 'secret name')
|
|
308
|
+
.option(
|
|
309
|
+
'--recovery-days <days>',
|
|
310
|
+
'recovery window in days (7-30)',
|
|
311
|
+
parseRecoveryDays
|
|
312
|
+
)
|
|
313
|
+
.option(
|
|
314
|
+
'--force-delete-without-recovery',
|
|
315
|
+
'permanently delete secret without recovery window',
|
|
316
|
+
false
|
|
317
|
+
)
|
|
318
|
+
.option('-y, --yes', 'confirm delete action', false)
|
|
319
|
+
.option('-p, --profile <profile>', 'profile to use')
|
|
320
|
+
.option('-r, --region <region>', 'region to use')
|
|
321
|
+
.option('--output <format>', 'output format: json|table')
|
|
322
|
+
.action(async (options, command) => {
|
|
323
|
+
try {
|
|
324
|
+
const { profile, region } = resolveAwsScope(options, command);
|
|
325
|
+
const globalOptions = command.optsWithGlobals();
|
|
326
|
+
const output =
|
|
327
|
+
options.output ??
|
|
328
|
+
(typeof globalOptions.output === 'string'
|
|
329
|
+
? globalOptions.output
|
|
330
|
+
: 'table');
|
|
331
|
+
if (!options.yes) {
|
|
332
|
+
throw new Error('Delete requires --yes confirmation.');
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (options.recoveryDays && options.forceDeleteWithoutRecovery) {
|
|
336
|
+
throw new Error(
|
|
337
|
+
'Use either --recovery-days or --force-delete-without-recovery, not both.'
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const result = await deleteSecret({
|
|
342
|
+
name: options.name,
|
|
343
|
+
recoveryDays: options.recoveryDays,
|
|
344
|
+
forceDeleteWithoutRecovery: options.forceDeleteWithoutRecovery,
|
|
345
|
+
profile,
|
|
346
|
+
region
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
printData(
|
|
350
|
+
asOutputFormat(output),
|
|
351
|
+
[
|
|
352
|
+
{ key: 'name', label: 'Name' },
|
|
353
|
+
{ key: 'arn', label: 'ARN' },
|
|
354
|
+
{ key: 'deletedDate', label: 'DeletedDate' }
|
|
355
|
+
],
|
|
356
|
+
[result]
|
|
357
|
+
);
|
|
358
|
+
} catch (error: unknown) {
|
|
359
|
+
exitWithError(error);
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
|
|
78
363
|
program.parse();
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { fromIni } from '@aws-sdk/credential-providers';
|
|
2
|
+
|
|
3
|
+
interface AwsConfigOptions {
|
|
4
|
+
profile?: string;
|
|
5
|
+
region?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface AwsClientConfig {
|
|
9
|
+
region?: string;
|
|
10
|
+
credentials?: ReturnType<typeof fromIni>;
|
|
11
|
+
endpoint?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const getCredentialsProvider = (options: AwsConfigOptions) => {
|
|
15
|
+
const {
|
|
16
|
+
AWS_ACCESS_KEY_ID: awsAccessKeyId,
|
|
17
|
+
AWS_SECRET_ACCESS_KEY: awsSecretAccessKey
|
|
18
|
+
} = process.env;
|
|
19
|
+
|
|
20
|
+
if (options.profile) {
|
|
21
|
+
return fromIni({ profile: options.profile });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (awsAccessKeyId && awsSecretAccessKey) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return fromIni({ profile: 'default' });
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const getEndpoint = () => {
|
|
32
|
+
return (
|
|
33
|
+
process.env.AWS_ENDPOINT_URL || process.env.AWS_SECRETS_MANAGER_ENDPOINT
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const buildAwsClientConfig = (
|
|
38
|
+
options: AwsConfigOptions
|
|
39
|
+
): AwsClientConfig => {
|
|
40
|
+
const endpoint = getEndpoint();
|
|
41
|
+
const config: AwsClientConfig = {
|
|
42
|
+
region: options.region,
|
|
43
|
+
credentials: getCredentialsProvider(options)
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
if (endpoint) {
|
|
47
|
+
config.endpoint = endpoint;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return config;
|
|
51
|
+
};
|