envilder 0.5.0 â 0.5.2
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/README.md +80 -29
- package/lib/cli/cli.d.ts +10 -0
- package/lib/cli/cli.d.ts.map +1 -0
- package/lib/cli/cli.js +76 -0
- package/lib/cli/cli.js.map +1 -0
- package/lib/index.d.ts +11 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +116 -0
- package/lib/index.js.map +1 -0
- package/package.json +29 -6
- package/.gitattributes +0 -67
- package/.gitconfig +0 -2
- package/.github/dependabot.yml +0 -40
- package/.github/pull_request_template.md +0 -20
- package/.github/workflows/cli-validation.yml +0 -58
- package/.github/workflows/codeql-analysis.yml +0 -49
- package/.github/workflows/coverage-report.yml +0 -73
- package/.github/workflows/publish.yml +0 -80
- package/.github/workflows/unit-tests.yml +0 -52
- package/.secretlintrc.json +0 -7
- package/biome.json +0 -30
- package/scripts/chmod-cli.js +0 -24
- package/scripts/validate-cli.ts +0 -141
- package/src/cli/cli.ts +0 -76
- package/src/index.ts +0 -119
- package/tests/cli/cli.test.ts +0 -59
- package/tests/index.test.ts +0 -150
- package/tests/sample/autogenerated.env +0 -1
- package/tests/sample/cli-validation.env +0 -1
- package/tests/sample/param-map.json +0 -3
- package/tsconfig.build.json +0 -14
- package/tsconfig.json +0 -30
- package/vite.config.ts +0 -17
- package/vitest.config.js +0 -12
package/README.md
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
<h1 align="center">
|
|
2
2
|
<br>
|
|
3
3
|
<img src="https://github.com/user-attachments/assets/96bf1efa-7d21-440a-a414-3a20e7f9a1f1" alt="Envilder">
|
|
4
|
-
<br>
|
|
5
|
-
Envilder
|
|
6
|
-
<br>
|
|
7
4
|
</h1>
|
|
8
5
|
|
|
9
|
-
<h4 align="center">
|
|
6
|
+
<h4 align="center">A CLI that securely centralizes your environment variables from AWS SSM as a single source of truth</h4>
|
|
10
7
|
|
|
11
8
|
<p align="center">
|
|
12
9
|
<a href="https://www.npmjs.com/package/envilder">
|
|
@@ -20,11 +17,16 @@
|
|
|
20
17
|
</a>
|
|
21
18
|
</p>
|
|
22
19
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
## ð Key benefits
|
|
21
|
+
|
|
22
|
+
- **ð Strict access control** - AWS IAM policies control who accesses which secrets (dev vs prod)
|
|
23
|
+
- **ð Full audit trail** - All parameter access is logged in CloudTrail for compliance requirements
|
|
24
|
+
- **ð§Đ Single source of truth** - No more copying .env files from Notion or emails - SSM is your only source
|
|
25
|
+
- **ð Idempotent operations** - Won't overwrite your local values - safe for automation
|
|
26
|
+
- **âïļ Environment-aware** - Use templates like `/project/${ENV}/DB_PASSWORD` to dynamically fetch the right secrets
|
|
27
|
+
- **ð§ą No extra infrastructure** - Uses AWS SSM's existing reliability instead of additional secret managers
|
|
26
28
|
|
|
27
|
-
## ⥠Quick
|
|
29
|
+
## ⥠Quick start
|
|
28
30
|
|
|
29
31
|
```bash
|
|
30
32
|
# Install globally
|
|
@@ -37,7 +39,7 @@ echo '{"DB_PASSWORD": "/my-app/db/password"}' > param-map.json
|
|
|
37
39
|
envilder --map=param-map.json --envfile=.env
|
|
38
40
|
```
|
|
39
41
|
|
|
40
|
-
## ðĪ What
|
|
42
|
+
## ðĪ What problem does Envilder solve?
|
|
41
43
|
|
|
42
44
|
<table>
|
|
43
45
|
<tr>
|
|
@@ -72,23 +74,23 @@ envilder --map=param-map.json --envfile=.env
|
|
|
72
74
|
|
|
73
75
|
## ðĄ Why Envilder?
|
|
74
76
|
|
|
75
|
-
- ð **No
|
|
76
|
-
- ðĪ **Automate
|
|
77
|
-
- ð **Always in
|
|
78
|
-
- ðïļ **Fast to
|
|
79
|
-
- ðŠķ **Simple but
|
|
77
|
+
- ð **No more secrets in git** - Store credentials in AWS SSM Parameter Store instead of version control
|
|
78
|
+
- ðĪ **Automate everything** - One command to generate your `.env` files across all environments
|
|
79
|
+
- ð **Always in sync** - Keep your local, dev, and production environments consistent
|
|
80
|
+
- ðïļ **Fast to set up** - Configure once, then generate `.env` files with a single command
|
|
81
|
+
- ðŠķ **Simple but powerful** - Easy interface with support for encrypted parameters and multiple AWS profiles
|
|
80
82
|
|
|
81
|
-
## ðŊ Perfect for
|
|
83
|
+
## ðŊ Perfect for teams
|
|
82
84
|
|
|
83
85
|
Envilder is the tool you need if you:
|
|
84
86
|
|
|
85
|
-
- ðĨ **Work in a
|
|
86
|
-
- ð **Deal with API
|
|
87
|
-
- âïļ **Run CI/CD
|
|
88
|
-
- âïļ **Use AWS
|
|
89
|
-
- ð **Manage
|
|
87
|
+
- ðĨ **Work in a development team** - Ensure everyone has the same environment without sharing raw secrets
|
|
88
|
+
- ð **Deal with API keys & tokens** - Securely store and retrieve sensitive credentials
|
|
89
|
+
- âïļ **Run CI/CD pipelines** - Automatically generate environment files during deployments
|
|
90
|
+
- âïļ **Use AWS already** - Leverage your existing AWS infrastructure more effectively
|
|
91
|
+
- ð **Manage multiple environments** - Switch easily between dev, staging, and production
|
|
90
92
|
|
|
91
|
-
## ð How
|
|
93
|
+
## ð How it works (simple!)
|
|
92
94
|
|
|
93
95
|
```mermaid
|
|
94
96
|
graph LR
|
|
@@ -98,10 +100,10 @@ graph LR
|
|
|
98
100
|
E[SSM Parameters] --> B
|
|
99
101
|
```
|
|
100
102
|
|
|
101
|
-
1. ð **Define
|
|
103
|
+
1. ð **Define your mapping** - Simple JSON mapping env vars to SSM paths
|
|
102
104
|
2. ð **Run Envilder** - One command with your mapping file
|
|
103
|
-
3. ð **Auto-
|
|
104
|
-
4. ðū **Get
|
|
105
|
+
3. ð **Auto-fetch from AWS** - Retrieves values using your AWS credentials
|
|
106
|
+
4. ðū **Get your .env file** - Ready to use in your project
|
|
105
107
|
|
|
106
108
|
## âïļ Prerequisites
|
|
107
109
|
|
|
@@ -110,7 +112,7 @@ You'll need:
|
|
|
110
112
|
- â
**AWS CLI** - Installed and configured with proper permissions to access SSM Parameter Store
|
|
111
113
|
- â
**Node.js** - Version 14 or higher
|
|
112
114
|
|
|
113
|
-
### AWS CLI
|
|
115
|
+
### AWS CLI setup
|
|
114
116
|
|
|
115
117
|
1. Install the AWS CLI by following the [official instructions](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html).
|
|
116
118
|
2. After installation, configure the AWS CLI:
|
|
@@ -149,7 +151,7 @@ envilder --map=<mapping-file> --envfile=<output-file> [--profile=<aws-profile>]
|
|
|
149
151
|
| `--envfile` | Path to output .env file (required) |
|
|
150
152
|
| `--profile` | AWS CLI profile to use (optional) |
|
|
151
153
|
|
|
152
|
-
## ð§ Quick
|
|
154
|
+
## ð§ Quick example
|
|
153
155
|
|
|
154
156
|
1. Create a mapping file `param-map.json`:
|
|
155
157
|
|
|
@@ -172,7 +174,7 @@ envilder --map=<mapping-file> --envfile=<output-file> [--profile=<aws-profile>]
|
|
|
172
174
|
envilder --map=param-map.json --envfile=.env --profile=dev-account
|
|
173
175
|
```
|
|
174
176
|
|
|
175
|
-
## ð Working with
|
|
177
|
+
## ð Working with multiple AWS profiles
|
|
176
178
|
|
|
177
179
|
For multiple AWS accounts or environments, configure different profiles in your AWS credentials file:
|
|
178
180
|
|
|
@@ -203,14 +205,63 @@ or `%USERPROFILE%\.aws\credentials` on Windows):
|
|
|
203
205
|
envilder --map=param-map.json --envfile=.env.production --profile=prod-account
|
|
204
206
|
```
|
|
205
207
|
|
|
206
|
-
##
|
|
208
|
+
## ð ïļ Advanced usage: environment-specific parameters
|
|
209
|
+
|
|
210
|
+
Envilder works brilliantly with environment variables for dynamic parameter paths:
|
|
211
|
+
|
|
212
|
+
1. Set up your SSM parameters with environment-specific paths:
|
|
213
|
+
|
|
214
|
+
```text
|
|
215
|
+
/project/dev/DB_PASSWORD
|
|
216
|
+
/project/stage/DB_PASSWORD
|
|
217
|
+
/project/prod/DB_PASSWORD
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
2. Create a template-based mapping file `env-map.json`:
|
|
221
|
+
|
|
222
|
+
```json
|
|
223
|
+
{
|
|
224
|
+
"DB_PASSWORD": "/project/${ENV}/DB_PASSWORD"
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
3. Generate environment-specific .env files:
|
|
229
|
+
|
|
230
|
+
```powershell
|
|
231
|
+
# Development
|
|
232
|
+
$env:ENV = "dev"
|
|
233
|
+
envilder --map=env-map.json --envfile=.env.dev
|
|
234
|
+
|
|
235
|
+
# Staging
|
|
236
|
+
$env:ENV = "stage"
|
|
237
|
+
envilder --map=env-map.json --envfile=.env.stage
|
|
238
|
+
|
|
239
|
+
# Production
|
|
240
|
+
$env:ENV = "prod"
|
|
241
|
+
envilder --map=env-map.json --envfile=.env.prod --profile=prod-account
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
This approach ensures the right variables are pulled for each environment with minimal configuration.
|
|
245
|
+
|
|
246
|
+
## ð Sample `.env` output
|
|
207
247
|
|
|
208
248
|
```ini
|
|
209
249
|
SECRET_TOKEN=mockedEmail@example.com
|
|
210
250
|
SECRET_KEY=mockedPassword
|
|
211
251
|
```
|
|
212
252
|
|
|
213
|
-
##
|
|
253
|
+
## ðŊ Why use Envilder in practice?
|
|
254
|
+
|
|
255
|
+
Envilder eliminates common problems in development teams:
|
|
256
|
+
|
|
257
|
+
- **ð No more "it works on my machine"** - Everyone uses the exact same environment variables from the same source
|
|
258
|
+
- **ð Always fresh credentials** - Update a secret in SSM and everyone gets it automatically on next run
|
|
259
|
+
- **ðĄïļ Access control built-in** - Developers only see dev secrets, CI/CD systems see what they need
|
|
260
|
+
- **ð§ Zero mental overhead** - No need to remember which variables are needed - the mapping defines everything
|
|
261
|
+
- **ðŦ No more sharing secrets** - Stop pasting credentials in Slack, email, or Notion documents
|
|
262
|
+
- **ð Compliance ready** - All accesses are logged in AWS CloudTrail for auditing
|
|
263
|
+
|
|
264
|
+
## ð§Š Running tests
|
|
214
265
|
|
|
215
266
|
```bash
|
|
216
267
|
yarn test
|
package/lib/cli/cli.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Parses CLI arguments and runs the environment file generator.
|
|
4
|
+
*
|
|
5
|
+
* Expects `--map` and `--envfile` options to be provided, with an optional `--profile` for AWS CLI profile selection. Invokes the main process to generate a `.env` file from AWS SSM parameters based on the provided mapping.
|
|
6
|
+
*
|
|
7
|
+
* @throws {Error} If either `--map` or `--envfile` arguments are missing.
|
|
8
|
+
*/
|
|
9
|
+
export declare function main(): Promise<void>;
|
|
10
|
+
//# sourceMappingURL=cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/cli/cli.ts"],"names":[],"mappings":";AA4CA;;;;;;GAMG;AACH,wBAAsB,IAAI,kBAkBzB"}
|
package/lib/cli/cli.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
12
|
+
import { dirname, join } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { Command } from 'commander';
|
|
15
|
+
import { run } from '../index.js';
|
|
16
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
17
|
+
const __dirname = dirname(__filename);
|
|
18
|
+
/**
|
|
19
|
+
* Find the package.json file by traversing up directories
|
|
20
|
+
* @param startDir The directory to start searching from
|
|
21
|
+
* @param maxDepth Maximum number of parent directories to check
|
|
22
|
+
* @returns Path to package.json if found, or null if not found
|
|
23
|
+
*/
|
|
24
|
+
function findPackageJson(startDir, maxDepth = 5) {
|
|
25
|
+
let currentDir = startDir;
|
|
26
|
+
let depth = 0;
|
|
27
|
+
while (depth < maxDepth) {
|
|
28
|
+
const packagePath = join(currentDir, 'package.json');
|
|
29
|
+
if (existsSync(packagePath)) {
|
|
30
|
+
return packagePath;
|
|
31
|
+
}
|
|
32
|
+
// Go up one directory
|
|
33
|
+
const parentDir = dirname(currentDir);
|
|
34
|
+
if (parentDir === currentDir) {
|
|
35
|
+
// We've reached the root
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
currentDir = parentDir;
|
|
39
|
+
depth++;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
// Get package.json path by searching up from current file
|
|
44
|
+
const packageJsonPath = findPackageJson(__dirname) || join(__dirname, '..', '..', 'package.json');
|
|
45
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
46
|
+
/**
|
|
47
|
+
* Parses CLI arguments and runs the environment file generator.
|
|
48
|
+
*
|
|
49
|
+
* Expects `--map` and `--envfile` options to be provided, with an optional `--profile` for AWS CLI profile selection. Invokes the main process to generate a `.env` file from AWS SSM parameters based on the provided mapping.
|
|
50
|
+
*
|
|
51
|
+
* @throws {Error} If either `--map` or `--envfile` arguments are missing.
|
|
52
|
+
*/
|
|
53
|
+
export function main() {
|
|
54
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
55
|
+
const program = new Command();
|
|
56
|
+
program
|
|
57
|
+
.name('envilder')
|
|
58
|
+
.description('A CLI tool to generate .env files from AWS SSM parameters')
|
|
59
|
+
.version(packageJson.version)
|
|
60
|
+
.requiredOption('--map <path>', 'Path to the JSON file with environment variable mapping')
|
|
61
|
+
.requiredOption('--envfile <path>', 'Path to the .env file to be generated')
|
|
62
|
+
.option('--profile <name>', 'AWS CLI profile to use');
|
|
63
|
+
yield program.parseAsync(process.argv);
|
|
64
|
+
const options = program.opts();
|
|
65
|
+
if (!options.map || !options.envfile) {
|
|
66
|
+
throw new Error('Missing required arguments: --map and --envfile');
|
|
67
|
+
}
|
|
68
|
+
yield run(options.map, options.envfile, options.profile);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
// Execute the CLI
|
|
72
|
+
main().catch((error) => {
|
|
73
|
+
console.error('ðĻ Uh-oh! Looks like Mario fell into the wrong pipe! ððĨ');
|
|
74
|
+
console.error(error);
|
|
75
|
+
});
|
|
76
|
+
//# sourceMappingURL=cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../../src/cli/cli.ts"],"names":[],"mappings":";;;;;;;;;;AACA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAElC,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClD,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;AAEtC;;;;;GAKG;AACH,SAAS,eAAe,CAAC,QAAgB,EAAE,QAAQ,GAAG,CAAC;IACrD,IAAI,UAAU,GAAG,QAAQ,CAAC;IAC1B,IAAI,KAAK,GAAG,CAAC,CAAC;IAEd,OAAO,KAAK,GAAG,QAAQ,EAAE,CAAC;QACxB,MAAM,WAAW,GAAG,IAAI,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC;QACrD,IAAI,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;YAC5B,OAAO,WAAW,CAAC;QACrB,CAAC;QAED,sBAAsB;QACtB,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;QACtC,IAAI,SAAS,KAAK,UAAU,EAAE,CAAC;YAC7B,yBAAyB;YACzB,MAAM;QACR,CAAC;QAED,UAAU,GAAG,SAAS,CAAC;QACvB,KAAK,EAAE,CAAC;IACV,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED,0DAA0D;AAC1D,MAAM,eAAe,GAAG,eAAe,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,cAAc,CAAC,CAAC;AAClG,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC,CAAC;AAEtE;;;;;;GAMG;AACH,MAAM,UAAgB,IAAI;;QACxB,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;QAC9B,OAAO;aACJ,IAAI,CAAC,UAAU,CAAC;aAChB,WAAW,CAAC,2DAA2D,CAAC;aACxE,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC;aAC5B,cAAc,CAAC,cAAc,EAAE,yDAAyD,CAAC;aACzF,cAAc,CAAC,kBAAkB,EAAE,uCAAuC,CAAC;aAC3E,MAAM,CAAC,kBAAkB,EAAE,wBAAwB,CAAC,CAAC;QAExD,MAAM,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACvC,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;QAE/B,IAAI,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;YACrC,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;QACrE,CAAC;QAED,MAAM,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3D,CAAC;CAAA;AAED,kBAAkB;AAClB,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;IACrB,OAAO,CAAC,KAAK,CAAC,2DAA2D,CAAC,CAAC;IAC3E,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AACvB,CAAC,CAAC,CAAC"}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orchestrates the process of fetching environment variable values from AWS SSM Parameter Store and writing them to a local environment file.
|
|
3
|
+
*
|
|
4
|
+
* Loads a parameter mapping from a JSON file, retrieves existing environment variables, fetches updated values from SSM (optionally using a specified AWS profile), merges them, and writes the result to the specified environment file.
|
|
5
|
+
*
|
|
6
|
+
* @param mapPath - Path to the JSON file mapping environment variable names to SSM parameter names.
|
|
7
|
+
* @param envFilePath - Path to the local environment file to read and update.
|
|
8
|
+
* @param profile - Optional AWS profile name to use for credentials.
|
|
9
|
+
*/
|
|
10
|
+
export declare function run(mapPath: string, envFilePath: string, profile?: string): Promise<void>;
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA;;;;;;;;GAQG;AACH,wBAAsB,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,iBAY/E"}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import * as fs from 'node:fs';
|
|
11
|
+
import { GetParameterCommand, SSM } from '@aws-sdk/client-ssm';
|
|
12
|
+
import { fromIni } from '@aws-sdk/credential-providers';
|
|
13
|
+
import * as dotenv from 'dotenv';
|
|
14
|
+
/**
|
|
15
|
+
* Orchestrates the process of fetching environment variable values from AWS SSM Parameter Store and writing them to a local environment file.
|
|
16
|
+
*
|
|
17
|
+
* Loads a parameter mapping from a JSON file, retrieves existing environment variables, fetches updated values from SSM (optionally using a specified AWS profile), merges them, and writes the result to the specified environment file.
|
|
18
|
+
*
|
|
19
|
+
* @param mapPath - Path to the JSON file mapping environment variable names to SSM parameter names.
|
|
20
|
+
* @param envFilePath - Path to the local environment file to read and update.
|
|
21
|
+
* @param profile - Optional AWS profile name to use for credentials.
|
|
22
|
+
*/
|
|
23
|
+
export function run(mapPath, envFilePath, profile) {
|
|
24
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
25
|
+
const defaultAwsConfig = {};
|
|
26
|
+
const ssmClientConfig = profile ? { credentials: fromIni({ profile }) } : defaultAwsConfig;
|
|
27
|
+
const ssm = new SSM(ssmClientConfig);
|
|
28
|
+
const paramMap = loadParamMap(mapPath);
|
|
29
|
+
const existingEnvVariables = loadExistingEnvVariables(envFilePath);
|
|
30
|
+
const updatedEnvVariables = yield fetchAndUpdateEnvVariables(paramMap, existingEnvVariables, ssm);
|
|
31
|
+
writeEnvFile(envFilePath, updatedEnvVariables);
|
|
32
|
+
console.log(`Environment File generated at '${envFilePath}'`);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function loadParamMap(mapPath) {
|
|
36
|
+
const content = fs.readFileSync(mapPath, 'utf-8');
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(content);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
console.error(`Error parsing JSON from ${mapPath}`);
|
|
42
|
+
throw new Error(`Invalid JSON in parameter map file: ${mapPath}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function loadExistingEnvVariables(envFilePath) {
|
|
46
|
+
const envVariables = {};
|
|
47
|
+
if (!fs.existsSync(envFilePath))
|
|
48
|
+
return envVariables;
|
|
49
|
+
const existingEnvContent = fs.readFileSync(envFilePath, 'utf-8');
|
|
50
|
+
const parsedEnv = dotenv.parse(existingEnvContent);
|
|
51
|
+
Object.assign(envVariables, parsedEnv);
|
|
52
|
+
return envVariables;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Fetches parameter values from AWS SSM for each environment variable in the map and updates the existing environment variables record.
|
|
56
|
+
*
|
|
57
|
+
* For each mapping, retrieves the corresponding SSM parameter value and updates the environment variable if found. Logs masked values and warnings for missing parameters. Throws an error if any parameters fail to fetch.
|
|
58
|
+
*
|
|
59
|
+
* @param paramMap - Mapping of environment variable names to SSM parameter names.
|
|
60
|
+
* @param existingEnvVariables - Current environment variables to be updated.
|
|
61
|
+
* @param ssm - AWS SSM client instance used for fetching parameters.
|
|
62
|
+
* @returns The updated environment variables record.
|
|
63
|
+
*
|
|
64
|
+
* @throws {Error} If any SSM parameters cannot be fetched.
|
|
65
|
+
*/
|
|
66
|
+
function fetchAndUpdateEnvVariables(paramMap, existingEnvVariables, ssm) {
|
|
67
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
68
|
+
const errors = [];
|
|
69
|
+
for (const [envVar, ssmName] of Object.entries(paramMap)) {
|
|
70
|
+
try {
|
|
71
|
+
const value = yield fetchSSMParameter(ssmName, ssm);
|
|
72
|
+
if (value) {
|
|
73
|
+
existingEnvVariables[envVar] = value;
|
|
74
|
+
console.log(`${envVar}=${value.length > 3 ? '*'.repeat(value.length - 3) + value.slice(-3) : '*'.repeat(value.length)}`);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
console.error(`Warning: No value found for: '${ssmName}'`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
console.error(`Error fetching parameter: '${ssmName}'`);
|
|
82
|
+
errors.push(`ParameterNotFound: ${ssmName}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (errors.length > 0) {
|
|
86
|
+
throw new Error(`Some parameters could not be fetched:\n${errors.join('\n')}`);
|
|
87
|
+
}
|
|
88
|
+
return existingEnvVariables;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Retrieves the value of a parameter from AWS SSM Parameter Store with decryption enabled.
|
|
93
|
+
*
|
|
94
|
+
* @param ssmName - The name of the SSM parameter to retrieve.
|
|
95
|
+
* @returns The decrypted parameter value if found, or undefined if the parameter does not exist.
|
|
96
|
+
*/
|
|
97
|
+
function fetchSSMParameter(ssmName, ssm) {
|
|
98
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
99
|
+
const command = new GetParameterCommand({
|
|
100
|
+
Name: ssmName,
|
|
101
|
+
WithDecryption: true,
|
|
102
|
+
});
|
|
103
|
+
const { Parameter } = yield ssm.send(command);
|
|
104
|
+
return Parameter === null || Parameter === void 0 ? void 0 : Parameter.Value;
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function writeEnvFile(envFilePath, envVariables) {
|
|
108
|
+
const envContent = Object.entries(envVariables)
|
|
109
|
+
.map(([key, value]) => {
|
|
110
|
+
const escapedValue = value.replace(/(\n|\r|\n\r)/g, '\\n').replace(/"/g, '\\"');
|
|
111
|
+
return `${key}=${escapedValue}`;
|
|
112
|
+
})
|
|
113
|
+
.join('\n');
|
|
114
|
+
fs.writeFileSync(envFilePath, envContent);
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "envilder",
|
|
3
|
-
"version": "0.5.
|
|
4
|
-
"description": "A CLI
|
|
3
|
+
"version": "0.5.2",
|
|
4
|
+
"description": "A CLI that securely centralizes your environment variables from AWS SSM as a single source of truth",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
7
7
|
"import": "./lib/index.js",
|
|
@@ -13,9 +13,10 @@
|
|
|
13
13
|
"envilder": "lib/cli/cli.js"
|
|
14
14
|
},
|
|
15
15
|
"scripts": {
|
|
16
|
-
"clean": "npx jest --clearCache && yarn cache clean --force && npx rimraf lib && npx rimraf node_modules && npx rimraf coverage
|
|
16
|
+
"clean": "npx jest --clearCache && yarn cache clean --force && npx rimraf lib && npx rimraf node_modules && npx rimraf coverage",
|
|
17
17
|
"test-run": "yarn build && node lib/cli/cli.js --map=tests/sample/param-map.json --envfile=tests/sample/autogenerated.env",
|
|
18
|
-
"build": "tsc -p tsconfig.build.json --sourceMap --declaration
|
|
18
|
+
"build": "tsc -p tsconfig.build.json --sourceMap --declaration",
|
|
19
|
+
"local-deploy": "yarn build && node --no-warnings scripts/pack-and-install.js",
|
|
19
20
|
"validate-cli": "node --no-warnings --loader ts-node/esm scripts/validate-cli.ts",
|
|
20
21
|
"format": "npx biome format",
|
|
21
22
|
"format:write": "npx biome format --write",
|
|
@@ -23,12 +24,28 @@
|
|
|
23
24
|
"lint:fix": "npx biome lint --fix",
|
|
24
25
|
"test": "vitest run --reporter verbose --coverage",
|
|
25
26
|
"cli-run": "yarn build && node --trace-warnings lib",
|
|
26
|
-
"npm-publish": "yarn lint && yarn build && yarn publish",
|
|
27
|
+
"npm-publish": "yarn lint && yarn build && yarn test && npm pack --dry-run && yarn publish",
|
|
27
28
|
"npm-release-patch": "yarn version --new-version patch",
|
|
28
29
|
"npm-release-minor": "yarn version --new-version minor",
|
|
29
30
|
"npm-release-prerelease": "yarn version --new-version prerelease"
|
|
30
31
|
},
|
|
31
|
-
"keywords": [
|
|
32
|
+
"keywords": [
|
|
33
|
+
"env",
|
|
34
|
+
"dotenv",
|
|
35
|
+
"aws",
|
|
36
|
+
"ssm",
|
|
37
|
+
"parameter-store",
|
|
38
|
+
"cli",
|
|
39
|
+
"environment",
|
|
40
|
+
"secrets",
|
|
41
|
+
"automation",
|
|
42
|
+
"config",
|
|
43
|
+
"aws-cli",
|
|
44
|
+
"devops",
|
|
45
|
+
"ci-cd",
|
|
46
|
+
"secure",
|
|
47
|
+
"envfile"
|
|
48
|
+
],
|
|
32
49
|
"repository": {
|
|
33
50
|
"type": "git",
|
|
34
51
|
"url": "git://github.com/macalbert/envilder.git"
|
|
@@ -41,6 +58,12 @@
|
|
|
41
58
|
"publishConfig": {
|
|
42
59
|
"access": "public"
|
|
43
60
|
},
|
|
61
|
+
"files": [
|
|
62
|
+
"lib/**/*",
|
|
63
|
+
"README.md",
|
|
64
|
+
"LICENSE",
|
|
65
|
+
"ROADMAP.md"
|
|
66
|
+
],
|
|
44
67
|
"type": "module",
|
|
45
68
|
"dependencies": {
|
|
46
69
|
"@aws-sdk/client-ssm": "^3.806.0",
|
package/.gitattributes
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
###############################################################################
|
|
2
|
-
# Set default behavior to automatically normalize line endings.
|
|
3
|
-
###############################################################################
|
|
4
|
-
* text=auto
|
|
5
|
-
|
|
6
|
-
# Explicitly declare text files you want to always be normalized and converted
|
|
7
|
-
# to native line endings on checkout.
|
|
8
|
-
*.sh text eol=lf
|
|
9
|
-
|
|
10
|
-
###############################################################################
|
|
11
|
-
# Set default behavior for command prompt diff.
|
|
12
|
-
#
|
|
13
|
-
# This is need for earlier builds of msysgit that does not have it on by
|
|
14
|
-
# default for csharp files.
|
|
15
|
-
# Note: This is only used by command line
|
|
16
|
-
###############################################################################
|
|
17
|
-
#*.cs diff=csharp
|
|
18
|
-
|
|
19
|
-
###############################################################################
|
|
20
|
-
# Set the merge driver for project and solution files
|
|
21
|
-
#
|
|
22
|
-
# Merging from the command prompt will add diff markers to the files if there
|
|
23
|
-
# are conflicts (Merging from VS is not affected by the settings below, in VS
|
|
24
|
-
# the diff markers are never inserted). Diff markers may cause the following
|
|
25
|
-
# file extensions to fail to load in VS. An alternative would be to treat
|
|
26
|
-
# these files as binary and thus will always conflict and require user
|
|
27
|
-
# intervention with every merge. To do so, just uncomment the entries below
|
|
28
|
-
###############################################################################
|
|
29
|
-
#*.sln merge=binary
|
|
30
|
-
#*.csproj merge=binary
|
|
31
|
-
#*.vbproj merge=binary
|
|
32
|
-
#*.vcxproj merge=binary
|
|
33
|
-
#*.vcproj merge=binary
|
|
34
|
-
#*.dbproj merge=binary
|
|
35
|
-
#*.fsproj merge=binary
|
|
36
|
-
#*.lsproj merge=binary
|
|
37
|
-
#*.wixproj merge=binary
|
|
38
|
-
#*.modelproj merge=binary
|
|
39
|
-
#*.sqlproj merge=binary
|
|
40
|
-
#*.wwaproj merge=binary
|
|
41
|
-
|
|
42
|
-
###############################################################################
|
|
43
|
-
# behavior for image files
|
|
44
|
-
#
|
|
45
|
-
# image files are treated as binary by default.
|
|
46
|
-
###############################################################################
|
|
47
|
-
#*.jpg binary
|
|
48
|
-
#*.png binary
|
|
49
|
-
#*.gif binary
|
|
50
|
-
|
|
51
|
-
###############################################################################
|
|
52
|
-
# diff behavior for common document formats
|
|
53
|
-
#
|
|
54
|
-
# Convert binary document formats to text before diffing them. This feature
|
|
55
|
-
# is only available from the command line. Turn it on by uncommenting the
|
|
56
|
-
# entries below.
|
|
57
|
-
###############################################################################
|
|
58
|
-
#*.doc diff=astextplain
|
|
59
|
-
#*.DOC diff=astextplain
|
|
60
|
-
#*.docx diff=astextplain
|
|
61
|
-
#*.DOCX diff=astextplain
|
|
62
|
-
#*.dot diff=astextplain
|
|
63
|
-
#*.DOT diff=astextplain
|
|
64
|
-
#*.pdf diff=astextplain
|
|
65
|
-
#*.PDF diff=astextplain
|
|
66
|
-
#*.rtf diff=astextplain
|
|
67
|
-
#*.RTF diff=astextplain
|
package/.gitconfig
DELETED
package/.github/dependabot.yml
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
# To get started with Dependabot version updates, you'll need to specify which
|
|
2
|
-
# package ecosystems to update and where the package manifests are located.
|
|
3
|
-
# Please see the documentation for all configuration options:
|
|
4
|
-
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
|
5
|
-
|
|
6
|
-
version: 2
|
|
7
|
-
updates:
|
|
8
|
-
- package-ecosystem: npm
|
|
9
|
-
directory: "/"
|
|
10
|
-
pull-request-branch-name:
|
|
11
|
-
separator: "/"
|
|
12
|
-
schedule:
|
|
13
|
-
interval: monthly
|
|
14
|
-
day: monday
|
|
15
|
-
time: "10:00"
|
|
16
|
-
timezone: Europe/Madrid
|
|
17
|
-
labels:
|
|
18
|
-
- "npm"
|
|
19
|
-
- "dependencies"
|
|
20
|
-
reviewers:
|
|
21
|
-
- macalbert
|
|
22
|
-
assignees:
|
|
23
|
-
- macalbert
|
|
24
|
-
|
|
25
|
-
- package-ecosystem: github-actions
|
|
26
|
-
directory: "/"
|
|
27
|
-
pull-request-branch-name:
|
|
28
|
-
separator: "/"
|
|
29
|
-
schedule:
|
|
30
|
-
interval: monthly
|
|
31
|
-
day: monday
|
|
32
|
-
time: "10:00"
|
|
33
|
-
timezone: Europe/Madrid
|
|
34
|
-
labels:
|
|
35
|
-
- "github-actions"
|
|
36
|
-
- "dependencies"
|
|
37
|
-
reviewers:
|
|
38
|
-
- macalbert
|
|
39
|
-
assignees:
|
|
40
|
-
- macalbert
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
# Description
|
|
2
|
-
|
|
3
|
-
_Describe the problem or feature in addition to a link to the issues._
|
|
4
|
-
|
|
5
|
-
## Approach
|
|
6
|
-
|
|
7
|
-
_How does this change address the problem?_
|
|
8
|
-
|
|
9
|
-
## Open Questions and Pre-Merge TODOs
|
|
10
|
-
|
|
11
|
-
- [ ] Use github checklists. When solved, check the box and explain the answer.
|
|
12
|
-
|
|
13
|
-
## Learning
|
|
14
|
-
|
|
15
|
-
_Describe the research stage_
|
|
16
|
-
_Links to blog posts, patterns, libraries or addons used to solve this problem_
|
|
17
|
-
|
|
18
|
-
## Blog Posts
|
|
19
|
-
|
|
20
|
-
- [How to Pull Request](https://github.com/flexyford/pull-request) Github Repo with Learning focused Pull Request Template.
|