env-verifier
Advanced tools
Comparing version 1.1.3 to 1.2.0-github-workflows.1
@@ -1,28 +0,51 @@ | ||
export interface MappedConfig { | ||
[key: string]: any | string | undefined | MappedConfig; | ||
declare type Cast<X extends any, Y extends any> = X extends Y ? X : Y; | ||
declare type TransformTuple_<U extends any> = (string | ((_: string) => U))[]; | ||
declare type MappedConfigElement_<E extends any> = E extends any[] ? E extends TransformTuple_<infer R> ? R : never : E extends InsertValue<infer U> ? U : E extends SecretKey ? Secret : E extends string ? string | undefined : MappedConfig<E>; | ||
declare type MappedConfigElement<E extends any> = MappedConfigElement_<E> extends infer X ? Cast<X, any> : never; | ||
export declare type MappedConfig<T> = { | ||
[P in keyof T]: MappedConfigElement<T[P]>; | ||
}; | ||
export declare type TransformTuple<T> = [string, (_: string) => T]; | ||
export declare type ConfigWithEnvKeys<T> = { | ||
[P in keyof T]: T[P] extends InsertValue<infer U> ? InsertValue<U> : T[P] extends SecretKey ? SecretKey : T[P] extends string ? string : T[P] extends TransformTuple<infer U> ? TransformTuple<U> : T[P] extends ConfigWithEnvKeys<T[P]> ? ConfigWithEnvKeys<T[P]> : never; | ||
}; | ||
declare type MissingValue = { | ||
path: string; | ||
envKey: string; | ||
}; | ||
export declare type VerifiedConfig<T> = { | ||
[P in keyof T]: T[P] extends SecretKey ? Secret : T[P] extends TransformTuple_<infer U> ? U : T[P] extends InsertValue<infer U> ? U : T[P] extends string ? string : VerifiedConfig<T[P]>; | ||
}; | ||
export declare class InsertValue<T> { | ||
value: T; | ||
constructor(value: T); | ||
} | ||
export interface TransformFn { | ||
(envValue: string): any; | ||
export declare class SecretKey { | ||
secret: string; | ||
constructor(secret: string); | ||
} | ||
export declare type TransformTuple = [string, TransformFn]; | ||
export interface ConfigWithEnvKeys { | ||
[key: string]: string | InsertValue | TransformTuple | ConfigWithEnvKeys; | ||
export declare class Secret { | ||
toJSON(): string; | ||
reveal: { | ||
(): string; | ||
}; | ||
constructor(secret: string); | ||
} | ||
export interface VerifiedConfig { | ||
[key: string]: any | string | VerifiedConfig; | ||
} | ||
declare class InsertValue { | ||
value: any; | ||
constructor(value: any); | ||
} | ||
export declare function verify(config: ConfigWithEnvKeys, env?: { | ||
[key: string]: string | undefined; | ||
}): { | ||
config: MappedConfig; | ||
export declare type VerifyReturnObject<T> = { | ||
config: MappedConfig<T>; | ||
missingValues: MissingValue[]; | ||
missingValueMessages: string[]; | ||
/** | ||
* @deprecated please use missingValues or missingValueMessages instead. | ||
*/ | ||
errors: string[]; | ||
}; | ||
export declare function strictVerify(config: ConfigWithEnvKeys, env?: { | ||
export declare function verify<T>(config: T extends ConfigWithEnvKeys<T> ? T : never, env?: { | ||
[key: string]: string | undefined; | ||
}): VerifiedConfig; | ||
export declare function insert(value: any): InsertValue; | ||
}): VerifyReturnObject<T>; | ||
export declare function strictVerify<T>(config: T extends ConfigWithEnvKeys<T> ? T : never, env?: { | ||
[key: string]: string | undefined; | ||
}): VerifiedConfig<T>; | ||
export declare function insert<T>(value: T): InsertValue<T>; | ||
export declare function secret(envKey: string): SecretKey; | ||
export {}; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.secret = exports.insert = exports.strictVerify = exports.verify = exports.Secret = exports.SecretKey = exports.InsertValue = void 0; | ||
class InsertValue { | ||
@@ -8,55 +9,96 @@ constructor(value) { | ||
} | ||
const recursiveVerify = ({ config, env, errors = [], path = '' }) => { | ||
const mapConf = (key) => { | ||
const value = config[key]; | ||
const subPath = path.length === 0 ? key : `${path}.${key}`; | ||
const getValueFromEnv = (key) => { | ||
const envValue = env[key]; | ||
if (envValue === undefined || envValue.length === 0) { | ||
errors.push(new Error(`environment value ${key} is missing from config object at ${subPath}`)); | ||
return undefined; | ||
} | ||
return envValue; | ||
}; | ||
if (value instanceof InsertValue) { | ||
return { [key]: value.value }; | ||
} | ||
else if (Array.isArray(value)) { | ||
const [envKey, transformFn] = value; | ||
const envValue = getValueFromEnv(envKey); | ||
const transforedValue = envValue && transformFn(envValue); | ||
return { [key]: transforedValue }; | ||
} | ||
else if (typeof value === 'string') { | ||
const envValue = getValueFromEnv(value); | ||
return { [key]: envValue }; | ||
} | ||
else { | ||
const { errors: subErrors, config: subConfig } = recursiveVerify({ | ||
config: value, | ||
env, | ||
path: subPath | ||
}); | ||
errors = errors.concat(subErrors); | ||
return { [key]: subConfig }; | ||
} | ||
}; | ||
const reduceConf = (acc, obj) => ({ | ||
...acc, | ||
...obj | ||
exports.InsertValue = InsertValue; | ||
class SecretKey { | ||
constructor(secret) { | ||
this.secret = secret; | ||
} | ||
} | ||
exports.SecretKey = SecretKey; | ||
class Secret { | ||
constructor(secret) { | ||
this.reveal = () => secret; | ||
} | ||
toJSON() { | ||
return '[secret]'; | ||
} | ||
} | ||
exports.Secret = Secret; | ||
const isNode = () => typeof process === 'object' && process + '' === '[object process]'; | ||
const getSecretObject = (secret) => { | ||
const secretObj = new Secret(secret); | ||
if (isNode()) { | ||
const util = require('util'); | ||
secretObj[util.inspect.custom] = () => '[secret]'; | ||
} | ||
return secretObj; | ||
}; | ||
const getEnvValueOrErrorCurried = (env, subPath) => (key) => { | ||
const envValue = env[key]; | ||
if (envValue === undefined || envValue.length === 0) { | ||
const error = { envKey: key, path: subPath }; | ||
return [undefined, [error]]; | ||
} | ||
return [envValue, []]; | ||
}; | ||
const getMapConfigFunction = ({ config, env, path = '', }) => (key) => { | ||
const value = config[key]; | ||
const subPath = path.length === 0 ? key + '' : `${path}.${key}`; | ||
const getEnvValueOrError = getEnvValueOrErrorCurried(env, subPath); | ||
if (value instanceof SecretKey) { | ||
const secretKey = value.secret; | ||
const [secretValue, errors] = getEnvValueOrError(secretKey); | ||
return [ | ||
{ [key]: getSecretObject(secretValue) }, | ||
errors, | ||
]; | ||
} | ||
if (value instanceof InsertValue) { | ||
return [{ [key]: value.value }, []]; | ||
} | ||
if (Array.isArray(value)) { | ||
const [envKey, transformFn] = value; | ||
const [envValue, errors] = getEnvValueOrError(envKey); | ||
const transformedValue = envValue && transformFn(envValue); | ||
return [{ [key]: transformedValue }, errors]; | ||
} | ||
if (typeof value === 'string') { | ||
const [envValue, errors] = getEnvValueOrError(value); | ||
return [{ [key]: envValue }, errors]; | ||
} | ||
const { errors, config: subConfig } = recursiveVerify({ | ||
config: value, | ||
env, | ||
path: subPath, | ||
}); | ||
const mappedConf = Object.keys(config).map(mapConf); | ||
const newConfig = mappedConf.reduce(reduceConf, {}); | ||
return { config: newConfig, env, errors, path }; | ||
return [{ [key]: subConfig }, errors]; | ||
}; | ||
const reduceConf = (acc, [config, errors]) => { | ||
return { | ||
config: Object.assign(Object.assign({}, acc.config), config), | ||
errors: acc.errors.concat(errors), | ||
}; | ||
}; | ||
const recursiveVerify = (paramCollection) => { | ||
const mapConf = getMapConfigFunction(paramCollection); | ||
const mappedConf = Object.keys(paramCollection.config).map(mapConf); | ||
return mappedConf.reduce(reduceConf, { config: {}, errors: [] }); | ||
}; | ||
function verify(config, env = process.env) { | ||
const { config: builtConfig, errors } = recursiveVerify({ config, env }); | ||
const errorMessages = errors.map(({ message }) => message); | ||
return { config: builtConfig, errors: errorMessages }; | ||
const { config: builtConfig, errors } = recursiveVerify({ | ||
config, | ||
env, | ||
}); | ||
const missingValueMessages = errors.map(({ envKey, path }) => `environment value ${envKey} is missing from config object at ${path}`); | ||
return { | ||
config: builtConfig, | ||
missingValues: errors, | ||
missingValueMessages, | ||
errors: missingValueMessages, | ||
}; | ||
} | ||
exports.verify = verify; | ||
function strictVerify(config, env = process.env) { | ||
const { config: builtConfig, errors } = verify(config, env); | ||
if (errors.length > 0) { | ||
throw new Error(`Missing configuration values: ${errors.join('\n')}`); | ||
const { config: builtConfig, missingValueMessages } = verify(config, env); | ||
if (missingValueMessages.length > 0) { | ||
throw new Error(`Missing configuration values: ${missingValueMessages.join('\n')}`); | ||
} | ||
@@ -70,2 +112,6 @@ return builtConfig; | ||
exports.insert = insert; | ||
function secret(envKey) { | ||
return new SecretKey(envKey); | ||
} | ||
exports.secret = secret; | ||
//# sourceMappingURL=index.js.map |
{ | ||
"name": "env-verifier", | ||
"version": "1.1.3", | ||
"version": "1.2.0-github-workflows.1", | ||
"description": "\"Make sure you have all your env variables!\"", | ||
@@ -11,2 +11,3 @@ "main": "dist/index.js", | ||
"prepack": "tsc", | ||
"prepare": "husky install", | ||
"build": "tsc" | ||
@@ -18,13 +19,20 @@ }, | ||
], | ||
"files": [ | ||
"dist" | ||
], | ||
"author": "Snugbear", | ||
"license": "MIT", | ||
"devDependencies": { | ||
"@commitlint/cli": "^13.1.0", | ||
"@commitlint/config-conventional": "^13.1.0", | ||
"@semantic-release/git": "^9.0.0", | ||
"@types/jest": "^26.0.10", | ||
"@types/node": "^12.7.1", | ||
"jest": "^24.8.0", | ||
"onchange": "^6.0.0", | ||
"@types/jest": "^24.0.17", | ||
"prettier": "1.18.2", | ||
"ts-jest": "^24.0.2", | ||
"typescript": "^3.5.3" | ||
"husky": "^7.0.1", | ||
"jest": "^26.6.3", | ||
"onchange": "^7.0.2", | ||
"prettier": "2.0.5", | ||
"ts-jest": "^26.5.1", | ||
"typescript": "^4.1.5" | ||
} | ||
} |
281
README.md
# env-verifier | ||
Quickly verify that incoming variables from process.env aren't missing. | ||
Verify that your environment variables exist, and build up your config object at the same time! | ||
@@ -9,2 +9,12 @@ [GitHub](https://github.com/pluralsight/env-verifier) | ||
## Package Purpose | ||
Certain types of apps require the use of different variables depending on the environment that the app is run in. | ||
The purpose of this package is to fail early whenever one of those values is missing from the environment object (ie: `process.env`). | ||
Using this package properly will prevent the sometimes cryptic errors that occur when environment variables are missing. | ||
Because every missing environment variable that `env-verifier` encountered is returned (or is displayed in a thrown error), this package can also help with the: `run the app, app crashes because of missing environment variable, add environment variable, repeat` loop that sometimes occurs. | ||
## Getting Started | ||
@@ -48,3 +58,3 @@ | ||
const { config, errors } = verify({ | ||
const { config, missingValues } = verify({ | ||
database: { | ||
@@ -59,3 +69,5 @@ name: 'DB_NAME' | ||
// do custom error logging, possibly throw your own errors | ||
errors.forEach(console.error) | ||
missingValues.forEach( | ||
({ envKey, path }) => console.log(`missing env variable: ${envKey} from config at path: ${path}`) | ||
) | ||
@@ -71,44 +83,120 @@ module.exports = config | ||
- [Function Signatures](#functionSignatures) | ||
- [Arbitrary Value Insertion](#arbitraryValueInsertion) | ||
- [Error Generation and Reporting](#errorGenerationAndReporting) | ||
- [Variable Transformation (TransformTuple)](#variableTransformation) | ||
- [Function Parameters and Return Types](#function-parameters-and-return-types) | ||
- [Processing Missing Values](#processing-missing-values) | ||
- [Arbitrary Value Insertion](#arbitrary-value-insertion) | ||
- [Secret Insertion](#secret-insertion) | ||
- [Error Generation and Reporting](#error-generation-and-reporting) | ||
- [Variable Transformation (TransformTuple)](#variable-transformation) | ||
- [Dynamic Typings](#dynamic-typings) | ||
#### <a name="functionSignatures"><a/> Function signatures | ||
### Function Parameters and Return Types | ||
#### `verify` | ||
```typescript | ||
interface TransformFn { | ||
(envValue: string): any | ||
export function insert<T>(value: T) => Insert<T> // see `Arbitrary Value Insertion` documentation | ||
export function secret(envKey: string) => Secret // see `Secret Insertion` documentation | ||
export type TransformTuple = [string, (envValue: string) => any] | ||
// Type given as example only, the real type is a bit more complex | ||
export type ConfigWithEnvKeys<T> = { | ||
[P in keyof T]: string | TransformTuple | ReturnType<typeof insert> | ReturnType<typeof secret> | ConfigWithEnvKeys<T[P]> | ||
} | ||
//see below | ||
// [envKeyName, TransformFn] | ||
type TransformTuple = [string, TransformFn] | ||
// Type given as example only, the real type is a bit more complex | ||
export type MappedConfig<T> = { | ||
[P in keyof T]: string | null | ReturnType<typeof secret> | MappedConfig<T[P]> | any // ie: return type of `insert` or `TransformTuple` function | ||
} | ||
interface ConfigWithEnvKeys { | ||
[key: string]: string | InsertValue | TransformTuple | ConfigWithEnvKeys | ||
export type MissingValue = { | ||
path: string | ||
envKey: string | ||
} | ||
interface MappedConfig { | ||
[key: string]: any | string | undefined | Config | ||
export function verify<T>( | ||
config: ConfigWithEnvKeys<T> | ||
env: { [key: string]: string | undefined } = process.env | ||
): { | ||
config: MappedConfig<T>, | ||
missingValues: MissingValue[], | ||
missingValueMessages: string[], | ||
/** | ||
* @deprecated Please use missingValueMessages | ||
*/ | ||
errors: string[] | ||
} | ||
``` | ||
interface VerifiedConfig { | ||
[key: string]: any | string | VerifiedConfig | ||
#### `strictVerify` | ||
```typescript | ||
export function insert<T extends any>(value: T) => Insert<T> // see `Arbitrary Value Insertion` documentation | ||
export function secret(envKey: string) => Secret // see `Secret Insertion` documentation | ||
export type TransformTuple = [string, (envValue: string) => any] | ||
// Type given as example only, the real type is a bit more complex | ||
export type ConfigWithEnvKeys<T> = { | ||
[P in keyof T]: string | TransformTuple | ReturnType<typeof insert> | ReturnType<typeof secret> | ConfigWithEnvKeys<T[P]> | ||
} | ||
interface Env { | ||
[key: string]: string | undefined | ||
// Type given as example only, the real type is a bit more complex | ||
// Similar to MappedConfig<T>, but does not contain nulls (except for as returned by `TransformTuple` functions or `insert` calls) | ||
export type VerifiedConfig<T> = { | ||
[P in keyof T]: string | ReturnType<typeof secret> | VerifiedConfig<T[P]> | any // ie: return type of `insert` or `TransformTuple` function | ||
} | ||
function insert(value: any): InsertValue | ||
export function strictVerify<T>( | ||
config: ConfigWithEnvKeys<T> | ||
env: { [key: string]: string | undefined } = process.env | ||
): VerifiedConfig<T> | ||
``` | ||
function verify(config: ConfigWithEnvKeys, env: Env = process.env): { config: MappedConfig, errors: string[] } | ||
### Processing Missing Values | ||
function strictVerify(config: ConfigWithEnvKeys, env: Env = process.env): VerifiedConfig | ||
An array of objects of type `MissingValue` is returned from the `verify` function. | ||
```typescript | ||
type MissingValue = { | ||
path: string | ||
envKey: string | ||
} | ||
``` | ||
#### <a name="arbitraryValueInsertion"><a/> Arbitrary Value Insertion | ||
#### `envKey` | ||
The key of the missing `env` value. | ||
#### `path` | ||
The path in the `config` argument to the missing `env` variable. | ||
Example: | ||
```transcript | ||
import { verify } from 'env-verifier' | ||
const config = { | ||
db: { | ||
password: 'DB_PASSWORD' | ||
} | ||
} | ||
const env = { | ||
DB_PASSWORD: undefined | ||
} | ||
const result = verify(config, env) | ||
console.log(result.missingValues) | ||
// results in: | ||
// [{ path: 'db.password', envKey: 'DB_PASSWORD' }] | ||
``` | ||
### Arbitrary Value Insertion | ||
You may have values that aren't present on your `env` object, but that you would like to live in your config object, this can be achieved by using the `insert()` function. | ||
@@ -131,18 +219,81 @@ | ||
#### <a name="errorGenerationAndReporting"><a/> Error Generation and Reporting | ||
### Secret Insertion | ||
As of env-verifier version `1.2.0`, the obfuscation of env secrets is supported. | ||
by wrapping the env key of the secret in the `secret` function exported by `env-verifier`, the secret will be retrieved and wrapped in a `Secret` object (see function specification above). | ||
Note: support for transforming or inserting secrets is not supported at this time. | ||
To retrieve the secret, the `reveal` function can be called. | ||
What secret obfuscation will do: | ||
- protect secrets from casual logging of the produced config object | ||
- `JSON.stringify` of the config object will replace all secrets with the string `'[secret]'` | ||
What secret obfuscation will not do: | ||
- prevent the actually logging of the revealed secret | ||
- mutate the actual string returned from the `env` object | ||
```javascript | ||
const { verify, secret } = require('env-verifier') | ||
const env = { | ||
PASSWORD: 'superSecretPassword' | ||
} | ||
const { config } = verify({ | ||
password: secret('PASSWORD') | ||
... // other env key names | ||
}, env) | ||
module.exports = config | ||
//exports: | ||
{ | ||
password: { | ||
reveal(): string | ||
} | ||
... // other env values | ||
} | ||
config.password.reveal() | ||
// returns: | ||
'superSecretPassword' | ||
console.log(config) | ||
// prints: | ||
// { | ||
// if you're using nodejs: | ||
// password: [secret] | ||
// if you're using other JS environments: | ||
// password: Secret { reveal: [Function] } | ||
// ... other env values | ||
// } | ||
JSON.stringify(config) | ||
// returns | ||
// { | ||
// "password": "[secret]" | ||
// ... other env values | ||
// } | ||
``` | ||
### Error Generation and Reporting | ||
Error reports are generated when an `env` variable is missing. An `env` variable is considered missing under the following circumstances: | ||
- `undefined` is returned from the `env` object. | ||
- an empty string, `''`, is returned from the `env` object. | ||
- an empty string, `''`, is returned from the `env` object. (useful for development with Docker) | ||
`verify` will always return the errors array, but it will be an empty array if there are no `env` misses. | ||
`verify` will always return an array of `MissingValue`s, which will be empty if there are no `env` misses. | ||
`strictVerify` will not throw an error on the first encountered missing `env` value. Instead it will continue in order to report all missing `env` variables. | ||
`strictVerify` will evaluate the entire `config` object before throwing any errors in order to report all missing `env` variables | ||
#### <a name="variableTransformation"><a/> Variable Transformation (TransformTuple) | ||
### Variable Transformation (TransformTuple) | ||
Since process.env only returns strings, sometimes its necessary to transform those strings into something else (IE: transform the string `"true"` to a boolean `true`) | ||
Since `env-verifier` only takes environment key-value pair objects that have `strings` as the values, its sometimes necessary to transform those strings into something else (IE: transform the string `"true"` to a boolean `true`) | ||
This can be done by passing in an array (TransformTuple) containing the `env` variable name, and the function that you would like to use to transform the `env` variable value like so: | ||
This can be done by passing in an array (called a `TransformTuple` in this context) containing the `env` variable name, and the function that you would like to use to transform the `env` variable value like so: | ||
@@ -160,6 +311,64 @@ ```javascript | ||
### Prerequisites | ||
### Dynamic Typings | ||
This package works best with projects that have centralized config files, IE: You map your `.env` variables to a `config` object in a file, and `import`/`require` that config object wherever you need `.env` values. | ||
**Important**: as of `v1.4.0` `env-verifier` should now be able to correctly and dynamically infer the return types of both `verify` and `strictVerify` without any extra help. the below is only valid for versions that pre-date `v1.4.0` | ||
`env-verifier` tries to give typescript typings for the config object that it returns, but needs a little help to get the correct types | ||
If you are using TypeScript, you can do the following: | ||
```typescript | ||
const config: { | ||
a: 'A', | ||
b: insert([1, 2]) | ||
c: { | ||
d: ['A', (envValue) => ([envValue])] | ||
} | ||
} | ||
const verifiedConfig = strictVerify(config) | ||
// pre-v1.4.0 typings: | ||
// typeof verifiedConfig = { | ||
// a: VerifiedConfig<unknown> | ||
// b: VerifiedConfig<unknown> | ||
// c: VerifiedConfig<unknown> | ||
// } | ||
// add typeof config object | ||
const verifiedConfig = strictVerify<typeof config>(config) | ||
// better typings: | ||
// typeof verifiedConfig = { | ||
// a: string, | ||
// b: number[], | ||
// c: { | ||
// d: (string | (envVerify: any) => any) | ||
// } | ||
// } | ||
// cast TransformTuple types correctly | ||
const config = { | ||
a: 'A', | ||
b: insert([1, 2]) | ||
c: { | ||
d: ['A', (envValue) => ([envValue])] as TransformTuple<string> | ||
} | ||
} | ||
const verifiedConfig = strictVerify<typeof config>(config) | ||
// best typings: | ||
// typeof verifiedConfig = { | ||
// a: string, | ||
// b: number[], | ||
// c: { | ||
// d: string | ||
// } | ||
// } | ||
``` | ||
## Prerequisites | ||
This package is written in TypeScript@4.1.5 and is built/distributed for environments that support the majority of the es2016 specification. | ||
This package also works best with projects that have centralized config files, IE: You map your `.env` variables to a `config` object in a file, and `import`/`require` that config object wherever you need `.env` values. | ||
Other than that, just install the package and get going! | ||
@@ -189,3 +398,3 @@ | ||
Please read [CONTRIBUTING.md](CONSTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests to us. | ||
Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests to us. | ||
@@ -192,0 +401,0 @@ ## Versioning |
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
407
29908
11
6
165
2
3
1