diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 167c7e3..86d7df4 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -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'
diff --git a/README.md b/README.md
index 1280801..4d221f3 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,3 @@
-
# good-env
@@ -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
diff --git a/package.json b/package.json
index 708f87e..d7ce904 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/index.d.ts b/src/index.d.ts
index 03d2be7..b4401a4 100644
--- a/src/index.d.ts
+++ b/src/index.d.ts
@@ -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;
/**
* @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
diff --git a/src/index.js b/src/index.js
index 0c8eb30..204d4f4 100644
--- a/src/index.js
+++ b/src/index.js
@@ -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
diff --git a/test/mocks.js b/test/mocks.js
new file mode 100644
index 0000000..25e4ff8
--- /dev/null
+++ b/test/mocks.js
@@ -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,
+};
\ No newline at end of file
diff --git a/test/test.js b/test/test.js
index cc17714..4905244 100644
--- a/test/test.js
+++ b/test/test.js
@@ -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();
});