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/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
|
|
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
|
|
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": [
|
|
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
|
package/eslint.config.js
ADDED
|
@@ -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
|
+
];
|
package/jest.e2e.config.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "env-secrets",
|
|
3
|
-
"version": "0.
|
|
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 .
|
|
19
|
+
"lint": "eslint .",
|
|
20
|
+
"lint:fix": "eslint . --fix",
|
|
20
21
|
"release": "release-it",
|
|
21
|
-
"prettier
|
|
22
|
-
"prettier:
|
|
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.
|
|
51
|
-
"@aws-sdk/client-sts": "^3.
|
|
52
|
-
"@aws-sdk/credential-providers": "^3.
|
|
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
|
-
"
|
|
58
|
-
"prettier --write
|
|
59
|
-
"eslint --fix
|
|
62
|
+
"**/*.js": [
|
|
63
|
+
"prettier --write",
|
|
64
|
+
"eslint --fix"
|
|
60
65
|
],
|
|
61
|
-
"
|
|
62
|
-
"
|
|
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": ">=
|
|
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
|
+
};
|