Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Set up NodeJS
uses: actions/setup-node@v2
uses: actions/setup-node@v4
with:
node-version: 18.20.4
cache: 'npm'
Expand Down
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

# good-env

<p align="center">
Expand Down Expand Up @@ -157,6 +156,29 @@ const {
const credentials = env.getAWS({ region: 'us-west-2' });
```

### AWS Secrets Manager Integration

Some folks like to store secrets in AWS secrets manager in the form of a JSON object as opposed (or in addition) to environment variables. It's me, I'm some folks. Good Env now supports this pattern. To avoid introducing a dependency you'll have to bring your own instance of AWS Secrets Manager though. Be sure to specify your AWS region as an environment variable, otherwise, it'll default to `us-east-1`.

Note, if something goes wrong, this function _will_ throw an error.

```javascript
const awsSecretsManager = require('@aws-sdk/client-secrets-manager');

(async function() {
// Load secrets from AWS Secrets Manager
await env.use(awsSecretsManager, 'my-secret-id');

// The secret ID can also be specified via environment variables
// AWS_SECRET_ID or SECRET_ID
await env.use(awsSecretsManager);

// Secrets are automatically merged with existing environment variables
// and can be accessed using any of the standard methods
const secretValue = env.get('someSecretFromAWSSecretsManager');
}());
```

## Important Behavior Notes

### Boolean Existence vs Value
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"name": "good-env",
"version": "7.4.0",
"version": "7.5.0",
"description": "Better environment variable handling for Twelve-Factor node apps",
"main": "src/index.js",
"scripts": {
"test": "nyc --check-coverage --lines 100 node test/test.js",
"lint": "semistandard test/ src/index.js",
"ci": "semistandard test/ src/index.js && nyc --check-coverage --lines 100 node test/test.js",
"format": "semistandard --fix test/ src/index.js"
"ci": "semistandard test/test.js src/index.js && nyc --check-coverage --lines 100 node test/test.js",
"format": "semistandard --fix test/test.js src/*.js"
},
"keywords": [
"environment",
Expand Down
8 changes: 8 additions & 0 deletions src/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
declare module "good-env" {
/**
* @description Tell Good Env to go to secrets manager, grab the object under the specified secretId and merge it with the
* environment.
* @param {any} awsSecretsManager - An instance of AWS Secrets Manager is imported from the SDK
* @param {string} awsSecretsManager - The secret ID to use to fetch the secrets object. If not supplied, the function will
* check environment variables AWS_SECRET_ID and SECRET_ID. If neither of which are defined, the function will throw an error
*/
export const use: (awsSecretsManager: any, secretId?: string) => Promise<void>;
/**
* @description Fetches an IP address from the environment. If the value found under the specified key is not a valid IPv4
* or IPv6 IP and there's no default value, null is returned. If a default value is provided and it is a valid IPv4 or IPv6
Expand Down
25 changes: 22 additions & 3 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,29 @@ let store = { ...process.env };

module.exports = Object
.create({
async _mergeSecrets_ ({ fetcherFunc }) {
const secret = await fetcherFunc();
async use (awsSecretsManager, secretId) {
const { SecretsManagerClient, GetSecretValueCommand } = awsSecretsManager;
const client = new SecretsManagerClient({
region: process.env.AWS_REGION || 'us-east-1'
});

if (!secretId) {
secretId = this.get(['AWS_SECRET_ID', 'SECRET_ID']);
}

if (!secretId) {
throw new Error('\'secretId\' was not specified, and it wasn\'t found as \'AWS_SECRET_ID\' or \'SECRET_ID\' in the environment.');
}

const response = await client.send(
new GetSecretValueCommand({
SecretId: secretId,
VersionStage: 'AWSCURRENT' // VersionStage defaults to AWSCURRENT if unspecified
})
);
const secretStr = response.SecretString;
const secret = JSON.parse(secretStr);
store = { ...store, ...secret };
return this;
},
/**
* @description Fetches an IP address from the environment. If the value found under the specified key is not a valid IPv4
Expand Down
24 changes: 24 additions & 0 deletions test/mocks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@

class GetSecretValueCommand {
constructor ({}) {}
}

class SecretsManagerClientHappy {
constructor ({}) {}
send () {
return Promise.resolve({ SecretString: JSON.stringify({ secretVal1: 'val1', secretVal2: 'val2' }) });
}
}

class SecretsManagerClientNotHappy {
constructor ({}) {}
send () {
return Promise.reject('Something went wrong');
}
}

module.exports = {
GetSecretValueCommand,
SecretsManagerClientHappy,
SecretsManagerClientNotHappy,
};
42 changes: 28 additions & 14 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,35 @@ require('dotenv').config({ path: 'test/test.env' });

const test = require('tape');
const env = require('../src/index');
const {
GetSecretValueCommand,
SecretsManagerClientHappy
} = require('./mocks');

test('it throws when no secretId is given when attempting to use secretsManager', async (t) => {
const awsSecretsManager = { SecretsManagerClient: SecretsManagerClientHappy, GetSecretValueCommand };

try {
await env.use(awsSecretsManager);
t.fail('We should not be here. An error should have been thrown');
} catch (e) {
console.log(e.message);
t.equals(e.message, '\'secretId\' was not specified, and it wasn\'t found as \'AWS_SECRET_ID\' or \'SECRET_ID\' in the environment.');
t.end();
}
});

test('it uses secrets manager (happy path)', async (t) => {
const awsSecretsManager = { SecretsManagerClient: SecretsManagerClientHappy, GetSecretValueCommand };
await env.use(awsSecretsManager, 'my-secret');
const foo = env.get('FOO');
const secretVal1 = env.get('secretVal1');
const secretVal2 = env.get('secretVal2');

t.equals(foo, 'bar');
t.equals(secretVal1, 'val1');
t.equals(secretVal2, 'val2');

test('it merges secrets', async (t) => {
const fetcherFunc = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
FOOB: 'bar',
BARZ: 'baz'
});
}, 10);
});
};
const env2 = await env._mergeSecrets_({ fetcherFunc });
t.equals(env2.get('FOOB'), 'bar');
t.equals(env2.get('BARZ'), 'baz');
t.end();
});

Expand Down