env-secrets 0.5.3 → 0.5.4

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 (52) hide show
  1. package/.claude/rules/cicd.md +189 -0
  2. package/.claude/rules/docs.md +96 -0
  3. package/.claude/rules/git-hooks.md +43 -0
  4. package/.claude/rules/local-dev-badges.md +91 -0
  5. package/.claude/rules/local-dev-env.md +382 -0
  6. package/.claude/rules/local-dev-license.md +104 -0
  7. package/.claude/rules/local-dev-mcp.md +70 -0
  8. package/.claude/rules/observability.md +23 -0
  9. package/.claude/rules/publishing-api.md +158 -0
  10. package/.claude/rules/publishing-apps.md +204 -0
  11. package/.claude/rules/publishing-apt.md +146 -0
  12. package/.claude/rules/publishing-brew.md +110 -0
  13. package/.claude/rules/publishing-cli.md +238 -0
  14. package/.claude/rules/publishing-libraries.md +115 -0
  15. package/.claude/rules/publishing-sdks.md +109 -0
  16. package/.claude/rules/publishing-web.md +185 -0
  17. package/.claude/rules/typescript-linting.md +141 -0
  18. package/.claude/rules/typescript-logging.md +356 -0
  19. package/.claude/rules/typescript-testing.md +185 -0
  20. package/.claude/settings.json +18 -0
  21. package/.claude/skills/github-health-check.skill +0 -0
  22. package/.codex/rules/cicd.md +21 -0
  23. package/.codex/rules/docs.md +98 -0
  24. package/.codex/rules/git-hooks.md +43 -0
  25. package/.codex/rules/github-health-check.md +440 -0
  26. package/.codex/rules/local-dev-env.md +47 -0
  27. package/.codex/rules/local-dev-license.md +3 -1
  28. package/.codex/rules/publishing-api.md +160 -0
  29. package/.codex/rules/publishing-apps.md +206 -0
  30. package/.codex/rules/publishing-apt.md +148 -0
  31. package/.codex/rules/publishing-brew.md +112 -0
  32. package/.codex/rules/publishing-cli.md +240 -0
  33. package/.codex/rules/publishing-libraries.md +117 -0
  34. package/.codex/rules/publishing-sdks.md +111 -0
  35. package/.codex/rules/publishing-web.md +187 -0
  36. package/.codex/rules/typescript-linting.md +143 -0
  37. package/.codex/rules/typescript-logging.md +358 -0
  38. package/.codex/rules/typescript-testing.md +187 -0
  39. package/.github/workflows/deploy-docs.yml +1 -1
  40. package/.github/workflows/unittests.yaml +1 -1
  41. package/.rulesrc.json +20 -0
  42. package/AGENTS.md +34 -0
  43. package/CLAUDE.md +58 -0
  44. package/README.md +17 -3
  45. package/__e2e__/aws-secret-value-args.test.ts +142 -0
  46. package/__tests__/cli/helpers.test.ts +35 -0
  47. package/dist/cli/helpers.js +13 -1
  48. package/dist/index.js +79 -40
  49. package/docs/AWS.md +42 -13
  50. package/package.json +5 -5
  51. package/src/cli/helpers.ts +16 -0
  52. package/src/index.ts +97 -48
package/docs/AWS.md CHANGED
@@ -153,11 +153,12 @@ 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 upsert` (alias: `import`)
156
157
  - `env-secrets aws secret append`
157
158
  - `env-secrets aws secret remove`
158
- - `env-secrets aws secret upsert` (alias: `import`)
159
159
  - `env-secrets aws secret list`
160
160
  - `env-secrets aws secret get`
161
+ - `env-secrets aws secret value`
161
162
  - `env-secrets aws secret delete`
162
163
 
163
164
  `aws secret` subcommands consistently honor `--region`, `--profile`, and `--output`.
@@ -165,8 +166,8 @@ Use these options directly with each subcommand.
165
166
 
166
167
  ### `aws -s` vs `aws secret ...`
167
168
 
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`).
169
+ - `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). Use `--no-shell` to run the program directly without a shell wrapper (disables shell expansion).
170
+ - `env-secrets aws secret ...`: management commands only (`create`, `update`, `upsert/import`, `append`, `remove`, `list`, `get`, `value`, `delete`).
170
171
 
171
172
  Example:
172
173
 
@@ -206,18 +207,24 @@ source secrets.env
206
207
  - dotenv-style input is converted (`KEY=value` -> `{"KEY":"value"}`)
207
208
  - non-object/scalar input is wrapped (`super-secret-value` -> `{"value":"super-secret-value"}`)
208
209
 
210
+ Optional flags: `-d/--description`, `-k/--kms-key-id`, `-t/--tag key=value` (repeatable).
211
+
209
212
  2. **Create from stdin (recommended for sensitive values):**
210
213
 
211
214
  ```bash
212
215
  echo -n 'super-secret-value' | env-secrets aws secret create -n my-app/dev/raw --value-stdin -r us-east-1
213
216
  ```
214
217
 
218
+ `create` and `update` accept `--value`, `--value-stdin`, or `--file` — use only one.
219
+
215
220
  3. **Update an existing secret value:**
216
221
 
217
222
  ```bash
218
223
  env-secrets aws secret update -n my-app/dev/api -v '{"API_KEY":"rotated"}' -r us-east-1
219
224
  ```
220
225
 
226
+ Optional flags: `-d/--description`, `-k/--kms-key-id`. Accepts `--value-stdin` or `--file` instead of `-v`.
227
+
221
228
  4. **Upsert from an env file into one JSON secret (`export KEY=value` or `KEY=value`):**
222
229
 
223
230
  ```bash
@@ -232,47 +239,69 @@ source secrets.env
232
239
  { "API_KEY": "abc123", "DATABASE_URL": "postgres://..." }
233
240
  ```
234
241
 
242
+ Optional flags: `-d/--description`, `-k/--kms-key-id`, `-t/--tag key=value` (repeatable, applies on create only).
243
+
235
244
  5. **Append/remove keys in an existing JSON secret:**
236
245
 
237
246
  ```bash
247
+ # Append a key (accepts --value-stdin or --file instead of -v)
238
248
  env-secrets aws secret append -n my-app/dev --key JIRA_EMAIL_TOKEN -v blah -r us-east-1
239
- env-secrets aws secret remove -n my-app/dev --key OLD_TOKEN -r us-east-1
249
+
250
+ # Remove one or more keys
251
+ env-secrets aws secret remove -n my-app/dev --key OLD_TOKEN --key UNUSED_KEY -r us-east-1
240
252
  ```
241
253
 
242
- 6. **List secrets by prefix:**
254
+ 6. **List secrets by prefix or tag:**
243
255
 
244
256
  ```bash
245
257
  env-secrets aws secret list --prefix my-app/dev -r us-east-1 --output table
246
- ```
247
258
 
248
- Multi-region validation example:
259
+ # Filter by tag
260
+ env-secrets aws secret list -t env=production -t team=platform -r us-east-1
249
261
 
250
- ```bash
262
+ # Multi-region comparison
251
263
  env-secrets aws secret list --prefix my-app/dev -r us-west-2 --output json
252
264
  env-secrets aws secret list --prefix my-app/dev -r us-east-1 --output json
253
265
  ```
254
266
 
255
- 7. **Get metadata and version info (without printing secret value):**
267
+ 7. **Get metadata and version info (without printing secret values):**
256
268
 
257
269
  ```bash
258
270
  env-secrets aws secret get -n my-app/dev/api -r us-east-1 --output json
259
271
  ```
260
272
 
261
- 8. **Delete with explicit confirmation:**
273
+ 8. **Get the values of a secret:**
274
+
275
+ ```bash
276
+ # Table output — values masked by default
277
+ env-secrets aws secret value -n my-app/dev/api -r us-east-1
278
+
279
+ # Reveal actual values (warning printed to stderr)
280
+ env-secrets aws secret value -n my-app/dev/api -r us-east-1 --reveal
281
+
282
+ # JSON output — always returns full values (warns on TTY)
283
+ env-secrets aws secret value -n my-app/dev/api -r us-east-1 --output json
284
+ ```
285
+
286
+ 9. **Delete with explicit confirmation:**
262
287
 
263
288
  ```bash
289
+ # With a recovery window (7–30 days)
264
290
  env-secrets aws secret delete -n my-app/dev/raw --recovery-days 7 --yes -r us-east-1
291
+
292
+ # Permanent delete with no recovery window
293
+ env-secrets aws secret delete -n my-app/dev/raw --force-delete-without-recovery --yes -r us-east-1
265
294
  ```
266
295
 
267
296
  ### Secret Management Safety Notes
268
297
 
269
- - `delete` requires `--yes`.
270
- - `create`/`update` accept `--value`, `--value-stdin`, or `--file` (use only one).
298
+ - `delete` requires `--yes`. Use either `--recovery-days <7-30>` or `--force-delete-without-recovery`, not both.
299
+ - `create`, `update`, and `append` accept `--value`, `--value-stdin`, or `--file` (use only one).
271
300
  - `create` always stores `SecretString` as a JSON object.
272
301
  - `append` and `remove` require the secret value to be a JSON object.
273
302
  - `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`.
274
303
  - Use `--value-stdin` to avoid shell history leakage for sensitive values.
275
- - Use either `--recovery-days` or `--force-delete-without-recovery` for delete operations.
304
+ - `value` masks secret values as `****` in table output by default. Use `--reveal` to show them (prints a warning to stderr). JSON output always returns full values and warns when stdout is a terminal.
276
305
 
277
306
  ## Examples
278
307
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "env-secrets",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
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)",
@@ -48,14 +48,14 @@
48
48
  "release-it": "^15.11.0",
49
49
  "rimraf": "^3.0.2",
50
50
  "tsc-files": "^1.1.4",
51
- "ts-jest": "^29.4.6",
51
+ "ts-jest": "^29.4.9",
52
52
  "ts-node": "^10.9.2",
53
53
  "typescript": "^4.9.5"
54
54
  },
55
55
  "dependencies": {
56
- "@aws-sdk/client-secrets-manager": "^3.1030.0",
57
- "@aws-sdk/client-sts": "^3.1030.0",
58
- "@aws-sdk/credential-providers": "^3.1030.0",
56
+ "@aws-sdk/client-secrets-manager": "^3.1038.0",
57
+ "@aws-sdk/client-sts": "^3.1038.0",
58
+ "@aws-sdk/credential-providers": "^3.1038.0",
59
59
  "commander": "^9.5.0",
60
60
  "debug": "^4.4.3"
61
61
  },
@@ -218,6 +218,22 @@ export const parseEnvSecretsFile = async (
218
218
  return parseEnvSecrets(content);
219
219
  };
220
220
 
221
+ export const resolveOutputFormat = (
222
+ options: { output?: string },
223
+ command?: CommandLikeWithGlobalOpts
224
+ ): OutputFormat => {
225
+ if (options.output) {
226
+ return asOutputFormat(options.output);
227
+ }
228
+
229
+ const globalOutput = command?.optsWithGlobals?.()?.output;
230
+ if (globalOutput === 'json' || globalOutput === 'table') {
231
+ return globalOutput;
232
+ }
233
+
234
+ return 'table';
235
+ };
236
+
221
237
  export const resolveAwsScope = (
222
238
  options: AwsScopeOptions,
223
239
  command?: CommandLikeWithGlobalOpts
package/src/index.ts CHANGED
@@ -23,6 +23,7 @@ import {
23
23
  printData,
24
24
  parseRecoveryDays,
25
25
  resolveAwsScope,
26
+ resolveOutputFormat,
26
27
  resolveSecretValue
27
28
  } from './cli/helpers';
28
29
  import { objectToExport } from './vaults/utils';
@@ -208,12 +209,7 @@ secretCommand
208
209
  .action(async (options, command) => {
209
210
  try {
210
211
  const { profile, region } = resolveAwsScope(options, command);
211
- const globalOptions = command.optsWithGlobals();
212
- const output =
213
- options.output ??
214
- (typeof globalOptions.output === 'string'
215
- ? globalOptions.output
216
- : 'table');
212
+ const output = resolveOutputFormat(options, command);
217
213
  const value = await resolveSecretValue(
218
214
  options.value,
219
215
  options.valueStdin,
@@ -265,12 +261,7 @@ secretCommand
265
261
  .action(async (options, command) => {
266
262
  try {
267
263
  const { profile, region } = resolveAwsScope(options, command);
268
- const globalOptions = command.optsWithGlobals();
269
- const output =
270
- options.output ??
271
- (typeof globalOptions.output === 'string'
272
- ? globalOptions.output
273
- : 'table');
264
+ const output = resolveOutputFormat(options, command);
274
265
  const value = await resolveSecretValue(
275
266
  options.value,
276
267
  options.valueStdin,
@@ -320,12 +311,7 @@ secretCommand
320
311
  .action(async (options, command) => {
321
312
  try {
322
313
  const { profile, region } = resolveAwsScope(options, command);
323
- const globalOptions = command.optsWithGlobals();
324
- const output =
325
- options.output ??
326
- (typeof globalOptions.output === 'string'
327
- ? globalOptions.output
328
- : 'table');
314
+ const output = resolveOutputFormat(options, command);
329
315
  const parsed = await parseEnvSecretsFile(options.file);
330
316
 
331
317
  if (parsed.entries.length === 0) {
@@ -454,12 +440,7 @@ secretCommand
454
440
  .action(async (options, command) => {
455
441
  try {
456
442
  const { profile, region } = resolveAwsScope(options, command);
457
- const globalOptions = command.optsWithGlobals();
458
- const output =
459
- options.output ??
460
- (typeof globalOptions.output === 'string'
461
- ? globalOptions.output
462
- : 'table');
443
+ const output = resolveOutputFormat(options, command);
463
444
  const value = await resolveSecretValue(
464
445
  options.value,
465
446
  options.valueStdin,
@@ -519,12 +500,7 @@ secretCommand
519
500
  .action(async (options, command) => {
520
501
  try {
521
502
  const { profile, region } = resolveAwsScope(options, command);
522
- const globalOptions = command.optsWithGlobals();
523
- const output =
524
- options.output ??
525
- (typeof globalOptions.output === 'string'
526
- ? globalOptions.output
527
- : 'table');
503
+ const output = resolveOutputFormat(options, command);
528
504
  const keys = options.key as string[];
529
505
  const current = await getSecretString({
530
506
  name: options.name,
@@ -590,12 +566,7 @@ secretCommand
590
566
  .action(async (options, command) => {
591
567
  try {
592
568
  const { profile, region } = resolveAwsScope(options, command);
593
- const globalOptions = command.optsWithGlobals();
594
- const output =
595
- options.output ??
596
- (typeof globalOptions.output === 'string'
597
- ? globalOptions.output
598
- : 'table');
569
+ const output = resolveOutputFormat(options, command);
599
570
  const result = await listSecrets({
600
571
  prefix: options.prefix,
601
572
  tags: options.tag,
@@ -632,12 +603,7 @@ secretCommand
632
603
  .action(async (options, command) => {
633
604
  try {
634
605
  const { profile, region } = resolveAwsScope(options, command);
635
- const globalOptions = command.optsWithGlobals();
636
- const output =
637
- options.output ??
638
- (typeof globalOptions.output === 'string'
639
- ? globalOptions.output
640
- : 'table');
606
+ const output = resolveOutputFormat(options, command);
641
607
  const result = await getSecretMetadata({
642
608
  name: options.name,
643
609
  profile,
@@ -672,6 +638,94 @@ secretCommand
672
638
  }
673
639
  });
674
640
 
641
+ secretCommand
642
+ .command('value')
643
+ .description('get the values of a secret')
644
+ .requiredOption('-n, --name <name>', 'secret name')
645
+ .option(
646
+ '--reveal',
647
+ 'reveal secret values in table output (values are masked by default)',
648
+ false
649
+ )
650
+ .option('-p, --profile <profile>', 'profile to use')
651
+ .option('-r, --region <region>', 'region to use')
652
+ .option('--output <format>', 'output format: json|table')
653
+ .action(async (options, command) => {
654
+ try {
655
+ const { profile, region } = resolveAwsScope(options, command);
656
+ const output = resolveOutputFormat(options, command);
657
+
658
+ let secretString: string;
659
+ try {
660
+ secretString = await getSecretString({
661
+ name: options.name,
662
+ profile,
663
+ region
664
+ });
665
+ } catch (error: unknown) {
666
+ const msg = error instanceof Error ? error.message : String(error);
667
+ if (msg.includes('cannot be edited with append/remove')) {
668
+ throw new Error(
669
+ `Secret "${options.name}" is stored as binary and cannot be displayed as text.`
670
+ );
671
+ }
672
+ throw error;
673
+ }
674
+
675
+ let jsonEntries: Array<{ key: string; value: unknown }>;
676
+ try {
677
+ const parsed = JSON.parse(secretString) as unknown;
678
+ if (parsed && !Array.isArray(parsed) && typeof parsed === 'object') {
679
+ jsonEntries = Object.entries(parsed as Record<string, unknown>).map(
680
+ ([key, value]) => ({ key, value })
681
+ );
682
+ } else {
683
+ jsonEntries = [{ key: options.name, value: secretString }];
684
+ }
685
+ } catch {
686
+ jsonEntries = [{ key: options.name, value: secretString }];
687
+ }
688
+
689
+ if (output === 'json') {
690
+ if (process.stdout.isTTY) {
691
+ // eslint-disable-next-line no-console
692
+ console.error('Warning: displaying sensitive secret values.');
693
+ }
694
+ const result = Object.fromEntries(
695
+ jsonEntries.map(({ key, value }) => [key, value])
696
+ );
697
+ // eslint-disable-next-line no-console
698
+ console.log(JSON.stringify(result, null, 2));
699
+ return;
700
+ }
701
+
702
+ if (options.reveal) {
703
+ // eslint-disable-next-line no-console
704
+ console.error('Warning: displaying sensitive secret values.');
705
+ }
706
+
707
+ const rows = jsonEntries.map(({ key, value }) => ({
708
+ key,
709
+ value: options.reveal
710
+ ? typeof value === 'string'
711
+ ? value
712
+ : JSON.stringify(value)
713
+ : '****'
714
+ }));
715
+
716
+ printData(
717
+ asOutputFormat(output),
718
+ [
719
+ { key: 'key', label: 'Key' },
720
+ { key: 'value', label: 'Value' }
721
+ ],
722
+ rows
723
+ );
724
+ } catch (error: unknown) {
725
+ exitWithError(error);
726
+ }
727
+ });
728
+
675
729
  secretCommand
676
730
  .command('delete')
677
731
  .description('delete a secret in AWS Secrets Manager')
@@ -693,12 +747,7 @@ secretCommand
693
747
  .action(async (options, command) => {
694
748
  try {
695
749
  const { profile, region } = resolveAwsScope(options, command);
696
- const globalOptions = command.optsWithGlobals();
697
- const output =
698
- options.output ??
699
- (typeof globalOptions.output === 'string'
700
- ? globalOptions.output
701
- : 'table');
750
+ const output = resolveOutputFormat(options, command);
702
751
  if (!options.yes) {
703
752
  throw new Error('Delete requires --yes confirmation.');
704
753
  }