diff --git a/README.md b/README.md index afbbf0c..0b90d8d 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,7 @@ positional arguments: * `environment` - An object containing your env vars (eg. `process.env`) * `validators` - An object that specifies the format of required vars. * `options` - An (optional) object, which supports the following keys: - * `strict` - (default: `false`) If true, the output of `cleanEnv` will *only* - contain the env vars that were specified in the `validators` argument. + * `strict` - (default: `false`) Enable more rigorous behavior. See "Strict Mode" below * `reporter` - Pass in a function to override the default error handling and console output. See `lib/reporter.js` for the default implementation. * `transformer` - A function used to transform the cleaned environment object @@ -135,6 +134,16 @@ const env = cleanEnv(process.env, myValidators, { ``` +## Strict mode + +By passing the `{ strict: true }` option, envalid gives you extra tight guarantees +about the cleaned env object: + +* The env object will *only* contain the env vars that were specified by your `validators`. +* Any attempt to access an invalid/missing property on the env object will cause a thrown error. +* Any attempt to mutate the cleaned env object will cause a thrown error. + + ## `.env` File Support Envalid wraps the very handy [dotenv](https://www.npmjs.com/package/dotenv) package, diff --git a/index.js b/index.js index 6ebe9bd..a9d15d9 100644 --- a/index.js +++ b/index.js @@ -117,6 +117,8 @@ function cleanEnv(inputEnv, specs = {}, options = {}) { const reporter = options.reporter || require('./lib/reporter') reporter({ errors, env: output }) + if (options.strict) output = require('./lib/strictProxy')(output) + return Object.freeze(output) } diff --git a/lib/strictProxy.js b/lib/strictProxy.js new file mode 100644 index 0000000..72cd836 --- /dev/null +++ b/lib/strictProxy.js @@ -0,0 +1,25 @@ +/** +* Wrap the environment object with a Proxy that throws when: +* a) trying to mutate an env var +* b) trying to access an invalid (unset) env var +* +* @return {Object} - Proxied environment object with get/set traps +*/ +module.exports = envObj => new Proxy(envObj, { + get(target, name) { + // These checks are needed because calling console.log on a + // proxy that throws crashes the entire process. This whitelists + // the necessary properties for `console.log(envObj)` to work. + if (['inspect', Symbol.toStringTag].includes(name)) return envObj[name] + if (name.toString() === 'Symbol(util.inspect.custom)') return envObj[name] + + const varExists = envObj.hasOwnProperty(name) + if (!varExists) throw new Error(`[envalid] Environment var not found: ${name}`) + + return envObj[name] + }, + + set(name) { + throw new Error(`[envalid] Attempt to mutate environment value: ${name}`) + }, +}) diff --git a/tests/test_basics.js b/tests/test_basics.js index 9c3f284..ebc3d6c 100644 --- a/tests/test_basics.js +++ b/tests/test_basics.js @@ -8,14 +8,6 @@ test('string passthrough', () => { assertPassthrough({ FOO: 'bar' }, { FOO: str() }) }) -test('strict option: only specified fields are passed through', () => { - const opts = { strict: true } - const env = cleanEnv({ FOO: 'bar', BAZ: 'baz' }, { - FOO: str() - }, opts) - assert.deepEqual(env, { FOO: 'bar' }) -}) - test('transformer option: allow transformation of keys', () => { const lowerCaseKey = (acc, [key, value]) => Object.assign(acc, { [key.toLowerCase()]: value }) const opts = { diff --git a/tests/test_dotenv.js b/tests/test_dotenv.js index e6c29d3..18ea86a 100644 --- a/tests/test_dotenv.js +++ b/tests/test_dotenv.js @@ -18,14 +18,6 @@ test('.env contents are cleaned', () => { assert.deepEqual(env, { FOO: 'bar', BAR: 'asdfasdf', MYNUM: 4 }) }) -test('.env test in strict mode', () => { - const opts = { strict: true } - const env = cleanEnv({ FOO: 'bar', BAZ: 'baz' }, { - MYNUM: num() - }, opts) - assert.deepEqual(env, { MYNUM: 4 }) -}) - test('can opt out of dotenv with dotEnvPath=null', () => { const env = cleanEnv({ FOO: 'bar' }, {}, { dotEnvPath: null }) assert.deepEqual(env, { FOO: 'bar' }) diff --git a/tests/test_strict.js b/tests/test_strict.js new file mode 100644 index 0000000..fbf8b20 --- /dev/null +++ b/tests/test_strict.js @@ -0,0 +1,55 @@ +const fs = require('fs') +const { createGroup, assert } = require('painless') +const { cleanEnv, str, num } = require('..') +const test = createGroup() +const strictOption = { strict: true } + + +// assert.deepEqual() substitute for assertions on proxied strict-mode env objects +// Chai's deepEqual() performs a few checks that the Proxy chokes on, so rather than +// adding special-case code inside the proxy's get() trap, we use this custom assert +// function +const objStrictDeepEqual = (actual, desired) => { + const desiredKeys = Object.keys(desired) + assert.deepEqual(Object.keys(actual), desiredKeys) + for (const k of desiredKeys) { + assert.strictEqual(actual[k], desired[k]) + } +} + +test.beforeEach(() => fs.writeFileSync('.env', ` +BAR=asdfasdf +MYNUM=4 +`)) +test.afterEach(() => fs.unlinkSync('.env')) + + +test('strict option: only specified fields are passed through', () => { + const env = cleanEnv({ FOO: 'bar', BAZ: 'baz' }, { + FOO: str() + }, strictOption) + objStrictDeepEqual(env, { FOO: 'bar' }) +}) + +test('.env test in strict mode', () => { + const env = cleanEnv({ FOO: 'bar', BAZ: 'baz' }, { + MYNUM: num() + }, strictOption) + objStrictDeepEqual(env, { MYNUM: 4 }) +}) + +test('strict mode objects throw when invalid attrs are accessed', () => { + const env = cleanEnv({ FOO: 'bar', BAZ: 'baz' }, { + FOO: str() + }, strictOption) + assert.strictEqual(env.FOO, 'bar') + assert.throws(() => env.ASDF) +}) + +test('strict mode objects throw when attempting to mutate', () => { + const env = cleanEnv({ FOO: 'bar', BAZ: 'baz' }, { + FOO: str() + }, strictOption) + assert.throws(() => env.FOO = 'foooooo') +}) +