From 13e4ed29b2f6bf4ce1f31940a29378ba43fa4e4a Mon Sep 17 00:00:00 2001 From: recursivefunk Date: Sun, 1 Jun 2025 18:27:09 -0400 Subject: [PATCH 1/4] feat: aws secrets manager --- README.md | 24 +++++++++++++++++++++++- package.json | 4 ++-- src/index.d.ts | 8 ++++++++ src/index.js | 25 ++++++++++++++++++++++--- test/mocks.js | 24 ++++++++++++++++++++++++ test/test.js | 42 ++++++++++++++++++++++++++++-------------- 6 files changed, 107 insertions(+), 20 deletions(-) create mode 100644 test/mocks.js diff --git a/README.md b/README.md index 1280801..3b8d56b 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 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..16d9c6d 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" + "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..9352752 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' + }); + let response; + 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.'); + } + + 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(); }); From 9ba536830b52d1d1abbb80f76b6b333ce1584ead Mon Sep 17 00:00:00 2001 From: recursivefunk Date: Sun, 1 Jun 2025 18:28:04 -0400 Subject: [PATCH 2/4] docs update --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3b8d56b..4d221f3 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,7 @@ 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 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`. +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. From c22cb7f2533ef52a0641c596e8faba02cc2e5293 Mon Sep 17 00:00:00 2001 From: recursivefunk Date: Sun, 1 Jun 2025 18:33:12 -0400 Subject: [PATCH 3/4] upgrade node setup --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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' From 761aba7f51fb49e30af95ca17c7764cb4c639bb8 Mon Sep 17 00:00:00 2001 From: recursivefunk Date: Sun, 1 Jun 2025 18:35:01 -0400 Subject: [PATCH 4/4] ci --- package.json | 2 +- src/index.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 16d9c6d..d7ce904 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "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", + "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": [ diff --git a/src/index.js b/src/index.js index 9352752..204d4f4 100644 --- a/src/index.js +++ b/src/index.js @@ -22,7 +22,7 @@ module.exports = Object const client = new SecretsManagerClient({ region: process.env.AWS_REGION || 'us-east-1' }); - let response; + if (!secretId) { secretId = this.get(['AWS_SECRET_ID', 'SECRET_ID']); } @@ -31,7 +31,7 @@ module.exports = Object throw new Error('\'secretId\' was not specified, and it wasn\'t found as \'AWS_SECRET_ID\' or \'SECRET_ID\' in the environment.'); } - response = await client.send( + const response = await client.send( new GetSecretValueCommand({ SecretId: secretId, VersionStage: 'AWSCURRENT' // VersionStage defaults to AWSCURRENT if unspecified