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.
Files changed (42) hide show
  1. package/.codex/rules/cicd.md +170 -0
  2. package/.codex/rules/linting.md +174 -0
  3. package/.codex/rules/local-dev-badges.md +93 -0
  4. package/.codex/rules/local-dev-env.md +271 -0
  5. package/.codex/rules/local-dev-license.md +104 -0
  6. package/.codex/rules/local-dev-mcp.md +72 -0
  7. package/.codex/rules/logging.md +358 -0
  8. package/.codex/rules/observability.md +25 -0
  9. package/.codex/rules/testing.md +133 -0
  10. package/.github/workflows/lint.yaml +7 -8
  11. package/.github/workflows/release.yml +1 -1
  12. package/.github/workflows/unittests.yaml +1 -1
  13. package/AGENTS.md +10 -4
  14. package/README.md +14 -9
  15. package/__e2e__/README.md +2 -5
  16. package/__e2e__/index.test.ts +152 -1
  17. package/__e2e__/utils/test-utils.ts +61 -1
  18. package/__tests__/cli/helpers.test.ts +129 -0
  19. package/__tests__/vaults/aws-config.test.ts +85 -0
  20. package/__tests__/vaults/secretsmanager-admin.test.ts +312 -0
  21. package/__tests__/vaults/secretsmanager.test.ts +57 -20
  22. package/dist/cli/helpers.js +110 -0
  23. package/dist/index.js +221 -2
  24. package/dist/vaults/aws-config.js +29 -0
  25. package/dist/vaults/secretsmanager-admin.js +240 -0
  26. package/dist/vaults/secretsmanager.js +20 -16
  27. package/docs/AWS.md +78 -3
  28. package/eslint.config.js +67 -0
  29. package/jest.e2e.config.js +1 -0
  30. package/package.json +23 -13
  31. package/src/cli/helpers.ts +144 -0
  32. package/src/index.ts +287 -2
  33. package/src/vaults/aws-config.ts +51 -0
  34. package/src/vaults/secretsmanager-admin.ts +352 -0
  35. package/src/vaults/secretsmanager.ts +32 -20
  36. package/website/docs/cli-reference.mdx +67 -0
  37. package/website/docs/examples.mdx +1 -1
  38. package/website/docs/installation.mdx +1 -1
  39. package/website/docs/providers/aws-secrets-manager.mdx +32 -0
  40. package/.eslintignore +0 -4
  41. package/.eslintrc +0 -18
  42. 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
- .requiredOption('-s, --secret <secret>', 'secret to get')
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
+ };