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/docs/AWS.md CHANGED
@@ -10,7 +10,7 @@ The `env-secrets` tool supports AWS Secrets Manager as a secret vault. It can re
10
10
 
11
11
  - [AWS CLI](https://docs.aws.amazon.com/cli/index.html) installed and configured
12
12
  - AWS credentials with appropriate permissions to access Secrets Manager
13
- - Node.js 18.0.0 or higher
13
+ - Node.js 20.0.0 or higher
14
14
 
15
15
  ## Authentication Methods
16
16
 
@@ -120,7 +120,7 @@ env-secrets aws -s my-secret-name -r us-east-1 -- node app.js
120
120
 
121
121
  ## Required Permissions
122
122
 
123
- Your AWS credentials must have the following permissions to use Secrets Manager:
123
+ Your AWS credentials must have the following permissions to use secret injection and secret management commands:
124
124
 
125
125
  ```json
126
126
  {
@@ -128,7 +128,14 @@ Your AWS credentials must have the following permissions to use Secrets Manager:
128
128
  "Statement": [
129
129
  {
130
130
  "Effect": "Allow",
131
- "Action": ["secretsmanager:GetSecretValue"],
131
+ "Action": [
132
+ "secretsmanager:GetSecretValue",
133
+ "secretsmanager:CreateSecret",
134
+ "secretsmanager:UpdateSecret",
135
+ "secretsmanager:ListSecrets",
136
+ "secretsmanager:DescribeSecret",
137
+ "secretsmanager:DeleteSecret"
138
+ ],
132
139
  "Resource": "arn:aws:secretsmanager:*:*:secret:*"
133
140
  },
134
141
  {
@@ -140,6 +147,74 @@ Your AWS credentials must have the following permissions to use Secrets Manager:
140
147
  }
141
148
  ```
142
149
 
150
+ ## Secret Management Commands
151
+
152
+ In addition to injecting variables into a process, `env-secrets` can manage AWS secrets directly:
153
+
154
+ - `env-secrets aws secret create`
155
+ - `env-secrets aws secret update`
156
+ - `env-secrets aws secret list`
157
+ - `env-secrets aws secret get`
158
+ - `env-secrets aws secret delete`
159
+
160
+ `aws secret` subcommands consistently honor `--region`, `--profile`, and `--output`.
161
+ Use these options directly with each subcommand.
162
+
163
+ ### Secret Management Examples
164
+
165
+ 1. **Create a secret with inline value:**
166
+
167
+ ```bash
168
+ env-secrets aws secret create \
169
+ -n my-app/dev/api \
170
+ -v '{"API_KEY":"abc123"}' \
171
+ -r us-east-1 \
172
+ --output json
173
+ ```
174
+
175
+ 2. **Create from stdin (recommended for sensitive values):**
176
+
177
+ ```bash
178
+ echo -n 'super-secret-value' | env-secrets aws secret create -n my-app/dev/raw --value-stdin -r us-east-1
179
+ ```
180
+
181
+ 3. **Update an existing secret value:**
182
+
183
+ ```bash
184
+ env-secrets aws secret update -n my-app/dev/api -v '{"API_KEY":"rotated"}' -r us-east-1
185
+ ```
186
+
187
+ 4. **List secrets by prefix:**
188
+
189
+ ```bash
190
+ env-secrets aws secret list --prefix my-app/dev -r us-east-1 --output table
191
+ ```
192
+
193
+ Multi-region validation example:
194
+
195
+ ```bash
196
+ env-secrets aws secret list --prefix my-app/dev -r us-west-2 --output json
197
+ env-secrets aws secret list --prefix my-app/dev -r us-east-1 --output json
198
+ ```
199
+
200
+ 5. **Get metadata and version info (without printing secret value):**
201
+
202
+ ```bash
203
+ env-secrets aws secret get -n my-app/dev/api -r us-east-1 --output json
204
+ ```
205
+
206
+ 6. **Delete with explicit confirmation:**
207
+
208
+ ```bash
209
+ env-secrets aws secret delete -n my-app/dev/raw --recovery-days 7 --yes -r us-east-1
210
+ ```
211
+
212
+ ### Secret Management Safety Notes
213
+
214
+ - `delete` requires `--yes`.
215
+ - Use `--value-stdin` to avoid shell history leakage for sensitive values.
216
+ - Use either `--recovery-days` or `--force-delete-without-recovery` for delete operations.
217
+
143
218
  ## Examples
144
219
 
145
220
  ### Basic Usage
@@ -0,0 +1,67 @@
1
+ const globals = require('globals');
2
+ const js = require('@eslint/js');
3
+ const tsParser = require('@typescript-eslint/parser');
4
+ const tsPlugin = require('@typescript-eslint/eslint-plugin');
5
+ const prettierPlugin = require('eslint-plugin-prettier');
6
+ const prettierConfig = require('eslint-config-prettier');
7
+
8
+ const tsEslintRecommendedRules =
9
+ tsPlugin.configs['eslint-recommended']?.overrides?.[0]?.rules ?? {};
10
+
11
+ module.exports = [
12
+ {
13
+ ignores: [
14
+ 'node_modules/**',
15
+ 'dist/**',
16
+ 'website/build/**',
17
+ 'website/node_modules/**',
18
+ 'website/.docusaurus/**',
19
+ 'coverage/**',
20
+ 'coverage-e2e/**'
21
+ ]
22
+ },
23
+ {
24
+ ...js.configs.recommended,
25
+ files: ['**/*.{js,mjs,cjs}'],
26
+ languageOptions: {
27
+ ...(js.configs.recommended.languageOptions ?? {}),
28
+ globals: globals.node
29
+ },
30
+ rules: {
31
+ ...(js.configs.recommended.rules ?? {}),
32
+ 'no-console': 'warn'
33
+ }
34
+ },
35
+ {
36
+ files: ['**/*.ts'],
37
+ languageOptions: {
38
+ parser: tsParser,
39
+ parserOptions: {
40
+ ecmaVersion: 'latest',
41
+ sourceType: 'module'
42
+ },
43
+ globals: {
44
+ ...globals.node,
45
+ ...globals.jest
46
+ }
47
+ },
48
+ plugins: {
49
+ '@typescript-eslint': tsPlugin
50
+ },
51
+ rules: {
52
+ ...(js.configs.recommended.rules ?? {}),
53
+ ...tsEslintRecommendedRules,
54
+ ...(tsPlugin.configs.recommended.rules ?? {}),
55
+ 'no-console': 'warn'
56
+ }
57
+ },
58
+ {
59
+ plugins: {
60
+ prettier: prettierPlugin
61
+ },
62
+ rules: {
63
+ 'prettier/prettier': 'error'
64
+ }
65
+ },
66
+ prettierConfig
67
+ ];
@@ -4,5 +4,6 @@ module.exports = {
4
4
  preset: 'ts-jest',
5
5
  testEnvironment: 'node',
6
6
  setupFilesAfterEnv: ['<rootDir>/__e2e__/setup.ts'],
7
+ testMatch: ['<rootDir>/__e2e__/**/*.test.ts'],
7
8
  testTimeout: 30000 // 30 seconds timeout for e2e tests
8
9
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "env-secrets",
3
- "version": "0.3.2",
3
+ "version": "0.4.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)",
@@ -16,10 +16,12 @@
16
16
  "build": "rimraf ./dist && tsc -b src",
17
17
  "start": "node dist/index.js",
18
18
  "postbuild": "chmod 755 ./dist/index.js",
19
- "lint": "eslint . --ext .ts,.js",
19
+ "lint": "eslint .",
20
+ "lint:fix": "eslint . --fix",
20
21
  "release": "release-it",
21
- "prettier:fix": "npx prettier --write .",
22
- "prettier:check": "npx prettier --check .",
22
+ "prettier": "prettier . --check",
23
+ "prettier:fix": "prettier . --write",
24
+ "prettier:check": "prettier . --check",
23
25
  "test": "npm run test:unit && npm run test:e2e",
24
26
  "test:unit": "jest __tests__",
25
27
  "test:unit:coverage": "jest __tests__ --coverage",
@@ -27,6 +29,7 @@
27
29
  "test:e2e:debug": "npm run build && DEBUG=true jest --config jest.e2e.config.js"
28
30
  },
29
31
  "devDependencies": {
32
+ "@everydaydevopsio/ballast": "^3.2.1",
30
33
  "@types/debug": "^4.1.12",
31
34
  "@types/jest": "^29.5.14",
32
35
  "@types/node": "^18.19.130",
@@ -36,30 +39,37 @@
36
39
  "eslint": "^8.57.1",
37
40
  "eslint-config-prettier": "^8.10.2",
38
41
  "eslint-plugin-prettier": "^4.2.5",
42
+ "globals": "^16.0.0",
39
43
  "husky": "^8.0.3",
40
44
  "jest": "^29.7.0",
41
45
  "lint-staged": "13.3.0",
42
46
  "prettier": "^2.8.8",
43
47
  "release-it": "^15.11.0",
44
48
  "rimraf": "^3.0.2",
49
+ "tsc-files": "^1.1.4",
45
50
  "ts-jest": "^29.4.6",
46
51
  "ts-node": "^10.9.2",
47
52
  "typescript": "^4.9.5"
48
53
  },
49
54
  "dependencies": {
50
- "@aws-sdk/client-secrets-manager": "^3.899.0",
51
- "@aws-sdk/client-sts": "^3.899.0",
52
- "@aws-sdk/credential-providers": "^3.899.0",
55
+ "@aws-sdk/client-secrets-manager": "^3.990.0",
56
+ "@aws-sdk/client-sts": "^3.990.0",
57
+ "@aws-sdk/credential-providers": "^3.990.0",
53
58
  "commander": "^9.5.0",
54
59
  "debug": "^4.4.3"
55
60
  },
56
61
  "lint-staged": {
57
- "*.{ts,js}": [
58
- "prettier --write .",
59
- "eslint --fix ."
62
+ "**/*.js": [
63
+ "prettier --write",
64
+ "eslint --fix"
60
65
  ],
61
- "*.{json,md,mdx,yaml,yml}": [
62
- "prettier --write ."
66
+ "**/*.ts": [
67
+ "bash -lc 'tsc --noEmit --project src/tsconfig.json'",
68
+ "prettier --write",
69
+ "eslint --fix"
70
+ ],
71
+ "**/*.{json,md,mdx,yaml,yml}": [
72
+ "prettier --write"
63
73
  ]
64
74
  },
65
75
  "publishConfig": {
@@ -70,6 +80,6 @@
70
80
  "env-secrets": "dist/index.js"
71
81
  },
72
82
  "engines": {
73
- "node": ">=18.0.0"
83
+ "node": ">=20.0.0"
74
84
  }
75
85
  }
@@ -0,0 +1,144 @@
1
+ export type OutputFormat = 'json' | 'table';
2
+ export interface AwsScopeOptions {
3
+ profile?: string;
4
+ region?: string;
5
+ }
6
+
7
+ interface CommandLikeWithGlobalOpts {
8
+ optsWithGlobals?: () => Record<string, unknown>;
9
+ }
10
+
11
+ export const asOutputFormat = (value: string): OutputFormat => {
12
+ if (value !== 'json' && value !== 'table') {
13
+ throw new Error(`Invalid output format "${value}". Use "json" or "table".`);
14
+ }
15
+
16
+ return value;
17
+ };
18
+
19
+ export const renderTable = (
20
+ headers: Array<{ key: string; label: string }>,
21
+ rows: Array<Record<string, string | undefined>>
22
+ ) => {
23
+ if (rows.length === 0) {
24
+ return 'No results.';
25
+ }
26
+
27
+ const widths = headers.map((header) => {
28
+ return Math.max(
29
+ header.label.length,
30
+ ...rows.map((row) => String(row[header.key] || '').length)
31
+ );
32
+ });
33
+
34
+ const headerLine = headers
35
+ .map((header, index) => header.label.padEnd(widths[index]))
36
+ .join(' ');
37
+ const divider = headers
38
+ .map((_, index) => '-'.repeat(widths[index]))
39
+ .join(' ');
40
+ const lines = rows.map((row) =>
41
+ headers
42
+ .map((header, index) =>
43
+ String(row[header.key] || '').padEnd(widths[index])
44
+ )
45
+ .join(' ')
46
+ );
47
+
48
+ return [headerLine, divider, ...lines].join('\n');
49
+ };
50
+
51
+ export const printData = (
52
+ format: OutputFormat,
53
+ headers: Array<{ key: string; label: string }>,
54
+ rows: Array<Record<string, string | undefined>>
55
+ ) => {
56
+ if (format === 'json') {
57
+ // eslint-disable-next-line no-console
58
+ console.log(JSON.stringify(rows, null, 2));
59
+ return;
60
+ }
61
+
62
+ // eslint-disable-next-line no-console
63
+ console.log(renderTable(headers, rows));
64
+ };
65
+
66
+ export const parseRecoveryDays = (value: string) => {
67
+ const parsed = Number(value);
68
+ if (!Number.isInteger(parsed) || parsed < 7 || parsed > 30) {
69
+ throw new Error('Recovery days must be an integer between 7 and 30.');
70
+ }
71
+
72
+ return parsed;
73
+ };
74
+
75
+ export const readStdin = async (stdin: NodeJS.ReadStream = process.stdin) => {
76
+ const chunks: Buffer[] = [];
77
+
78
+ return await new Promise<string>((resolve, reject) => {
79
+ const onData = (chunk: Buffer) => {
80
+ chunks.push(chunk);
81
+ };
82
+ const onEnd = () => {
83
+ cleanup();
84
+ resolve(
85
+ Buffer.concat(chunks)
86
+ .toString('utf8')
87
+ .replace(/\r?\n$/, '')
88
+ );
89
+ };
90
+ const onError = (error: Error) => {
91
+ cleanup();
92
+ reject(error);
93
+ };
94
+ const cleanup = () => {
95
+ stdin.off('data', onData);
96
+ stdin.off('end', onEnd);
97
+ stdin.off('error', onError);
98
+ };
99
+
100
+ stdin.on('data', onData);
101
+ stdin.once('end', onEnd);
102
+ stdin.once('error', onError);
103
+ });
104
+ };
105
+
106
+ export const resolveSecretValue = async (
107
+ value?: string,
108
+ valueStdin?: boolean
109
+ ): Promise<string | undefined> => {
110
+ if (value && valueStdin) {
111
+ throw new Error('Use either --value or --value-stdin, not both.');
112
+ }
113
+
114
+ if (valueStdin) {
115
+ if (process.stdin.isTTY) {
116
+ throw new Error(
117
+ 'No stdin detected. Pipe a value when using --value-stdin.'
118
+ );
119
+ }
120
+ return await readStdin();
121
+ }
122
+
123
+ return value;
124
+ };
125
+
126
+ export const resolveAwsScope = (
127
+ options: AwsScopeOptions,
128
+ command?: CommandLikeWithGlobalOpts
129
+ ): AwsScopeOptions => {
130
+ const globalOptions = command?.optsWithGlobals?.() || {};
131
+
132
+ const profile =
133
+ options.profile ||
134
+ (typeof globalOptions.profile === 'string'
135
+ ? globalOptions.profile
136
+ : undefined);
137
+ const region =
138
+ options.region ||
139
+ (typeof globalOptions.region === 'string'
140
+ ? globalOptions.region
141
+ : undefined);
142
+
143
+ return { profile, region };
144
+ };