Socket
Socket
Sign inDemoInstall

varium

Package Overview
Dependencies
7
Maintainers
1
Versions
5
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 1.0.1 to 2.0.3

src/.npmignore

25

package.json
{
"name": "varium",
"version": "1.0.1",
"description": "A strict parser and validator of environment config variables",
"version": "2.0.3",
"description": "Declare and validate environment variables",
"engines": {

@@ -24,12 +24,19 @@ "node": ">=6.5"

"dependencies": {
"debug": "^2.6.1",
"ramda": "^0.23.0"
"js-yaml": "^3.13.1",
"ramda": "^0.26.1"
},
"peerDependencies": {
"debug": "2.x"
},
"devDependencies": {
"chai": "^3.5.0",
"eslint": "^3.15.0",
"eslint-config-airbnb-base": "^11.1.0",
"eslint-plugin-import": "^2.2.0",
"mocha": "^3.0.2"
}
"eslint": "^6.1.0",
"eslint-config-airbnb-base": "^11.2.0",
"eslint-plugin-import": "^2.6.1",
"mocha": "^6.1.4"
},
"files": [
"/src"
],
"repository": "ahultgren/node-varium"
}
# Varium
A strict parser and validator of environment config variables. The fundamental
priniciples are that you want:
Varium is a library and syntax for managing environment variables in a sane way.
You should use it if you want to:
1. to have _one_ place where _all_ environment variables are declared and documented
2. CI and/or builds to _fail_ if any environment variables are missing
3. to prevent developers from ever using an undeclared env var
4. e.g. `SOME_FLAG=false` to be treated like a boolean.
* **declare** all used environment variables in **one place**
* **specify** which **types** they have
* **validate** that they are of the right type
* **cast environment variables** to the right type when used
* **require** certain variables
* **default** to a value for other variables
* **abort CI** if variables are missing or fail validation
* **warn developers** if they use an undeclared environment variable
In short you will never again have to hunt around in the source code for any
envrionment variables you might be missing.
## Installation
(Note that Varium does not handle loading of environment variables from files
(for example in develeopment environments). Use foreman/nf or dotenv, or
whatever you prefer, for that.)
`npm install varium --save`
_Requires node v6.5 or above._
## Installation
## Usage example
`npm install varium -S`
Create a file called `env.manifest` in the project root. It should contain all
environment variables used in the project. For example:
_Requires node v6.5 or above._
```
API_BASE_URL : String
API_SECRET : String
# This is a comment
# The following is an optional variable (the above were required):
NUMBER_OF_ITEMS : Int |
## Usage
FLAG : Bool | False # Variables can also have default values. Here it is False
COMPLEX_VALUE : Json | [{ "object": 42 }] # Use json for advanced data structures
The central piece of your environment configuration is the manifest. I suggest
you create a file named `env.manifest`. It looks like this:
QUOTED_STRING : String | "Quote the string if it contains # or \\escaped chars"
```
REQUIRED_URL : String
A_NUMBER : Int | 7
FLAG : Bool | false # Comment, can be used as documentation
LIST_OF_THINGS : Json | []
OPTIONAL_WITH_UNDEFINED_DEFAULT : String |
```
Then you need a central file for your config, probably `config/index.js`, where
you need the following:
Then create the file which all your other files imports to obtain the config.
For example `config/index.js`. This needs to at least contain:
```js
const varium = require('varium');
module.exports = varium(process.env, 'env.manifest');
module.exports = varium();
```
Now you can use the config in other modules. For example:
Import this file in the rest of your project to read environment variables:
```js
const config = require('./config');
console.log(config.get('A_NUMBER')); // 7
console.log(config.get('WAIT_WHAT_IS_THIS')); // throws Error: Varium: Undeclared env var "WAIT_WHAT_IS_THIS"
const config = require('../config');
const url = config.API_BASE_URL;
// An error will be thrown if you try to load an undeclared variable:
const wrong = config.API_BASE_ULR;
// -> Error('Varium: Undeclared env var "API_BASE_ULR.\nMaybe you meant API_BASE_URL?"')
```
To abort builds, for example on heroku, you can run the config file postbuild.
Just add the following to your package.json:
To prevent other developers or your future self from using `process.env`
directly, use the `no-process-env`
[eslint rule](https://eslint.org/docs/rules/no-process-env).
Your environment now needs to contain the required variables. If you use a
library to load `.env` files (such as node-forman or dotenv), the `.env` could
contain this:
```bash
API_BASE_URL=https://example.com/
API_SECRET=1337
NUMBER_OF_ITEMS=3
```
To abort builds during CI when environment variables are missing, just run the
config file during th build step. For example, on heroku the following would be
enough:
```js
{
"scripts": {
"heroku-postbuild": "node config"
"heroku-postbuild": "node ./config"
}

@@ -66,59 +86,3 @@ }

# Documentation
## Manifest syntax
`Varname : Type[ |[ Default]]`
* **Varname**: Name of an environment variable.
* **Type**: The type of the variable. Can be one of `Int, Float, String, Bool, Json`
* **Default** (optional): The default value. Must be the same type as **Type**.
If neither `|` nor a default value is set, the variable will be required. If
only `|` is set, the default will be `undefined` (thus the variable is optional).
For examples see Usage above.
## API
### varium : Env -> PathToManifest -> config
* **Env**: Object String, an object with a key for each environment variable and
the values are strings.
* ** PathToManifest**: String, path to the manifest. Relative to `process.cwd()`
or absolute (e.g. `path.join(__dirname, 'env.manifest')`).
**Returns** an instance of Config.
### config.get : VarName -> value
Takes the name of an environment variable and returns its value. Will throw if
the environment variable is not defined.
* **VarName**: The name of the variable.
**Returns** the value.
[debug]: https://www.npmjs.com/package/debug
### varium.Varium : Config -> varium
* **Config.customValidators** : Use your own validators for custom types, or
overwrite the built-in ones. For example:
```js
Varium({
FortyTwo : (value, def) => value === "42" ? 42 : def
}, process.env, "env.manifest");
```
Returns an instance with the same api as described for `varium` above.
## Logs
[Debug][debug] is used for logging. Thus if you need to debug something, set
`DEBUG=varium:*`. Notice, however, that it's not adviced to use this level in
production. It logs env var values and may thus potentially log secrets, which
is generally frowned upon.
For a complete syntax and api reference (for example how to add your own custom
types), see the [docs](./DOCS.md).
const fs = require("fs");
const path = require("path");
const R = require("ramda");
const interpret = require("./interpret");
const validate = require("./validate");
const nameError = require("./util/suggest");
const reader = R.curry((config, env, manifestString) => {
const result = validate(config.validators, interpret(manifestString), env);
const errors = result.filter(R.has("error$")).map(R.prop("error$"));
const reader = (config, env, manifestString) => {
const result = validate(config.types, interpret(manifestString), env);
const errors = result.map(x => x.error$).filter(Boolean);

@@ -26,33 +26,40 @@ if (errors.length) {

const values = R.mergeAll(result);
const values = Object.assign.apply(null, [{}].concat(result));
return {
get: (name) => {
if (Object.prototype.hasOwnProperty.call(values, name)) {
return values[name];
return new Proxy(values, {
get(target, prop) {
if (!Object.prototype.hasOwnProperty.call(target, prop)) {
if (prop === "get") {
return (name) => {
throw new Error(`Varium upgrade notice: config.get("${name}") is obsolete. Access the property directly using config.${name}`);
};
} else {
const suggestion = nameError(Object.keys(values), prop);
throw new Error(`Varium: Undeclared env var '${prop}'.\n${suggestion}`);
}
} else {
throw new Error(`Varium: Undeclared env var "${name}"`);
return target[prop];
}
},
};
});
}
});
};
const loader = R.curry((read, manifestPath) => {
const absPath = path.resolve(process.cwd(), manifestPath);
let manifest;
const loader = (manifestPath) => {
const appDir = path.dirname(require.main.filename);
const absPath = path.resolve(appDir, manifestPath);
try {
manifest = fs.readFileSync(absPath, { encoding: "utf8" });
return fs.readFileSync(absPath, { encoding: "utf8" });
} catch (e) {
throw new Error(`Varium: Could not find env var manifest at ${absPath}`);
throw new Error(`Varium: Could not find a manifest at ${absPath}`);
}
};
return read(manifest);
});
module.exports = ({
types = {},
env = process.env,
manifestPath = "env.manifest",
noProcessExit = false,
}) => reader({ types, noProcessExit }, env, loader(manifestPath));
const Varium = R.curry((validators, env, manifestPath) =>
loader(reader(validators, env), manifestPath));
module.exports = Varium({});
module.exports.Varium = Varium;
module.exports.reader = reader;

@@ -1,170 +0,11 @@

const debug = require("debug");
const fs = require("fs");
const path = require("path");
const yaml = require("js-yaml");
const parser = require("./syntax-parser");
const logTokens = debug("varium:lexer:tokens");
const syntaxPath = path.join(__dirname, "./syntax.yml");
const syntaxYml = fs.readFileSync(syntaxPath, { encoding: "utf8" });
const Input = (data) => {
let i = 0;
return {
peek: () => data[i] === undefined ? "\n" : data[i],
pop: () => {
i += 1;
return data[i - 1] === undefined ? "\n" : data[i - 1];
},
eof: () => i >= data.length,
};
};
const syntax = yaml.safeLoad(syntaxYml);
const State = {
Noop: Symbol("Noop"),
DeclarationName: Symbol("DeclarationName"),
DeclarationSeparator: Symbol("DeclarationSeparator"),
DeclarationType: Symbol("DeclarationType"),
DeclarationEnd: Symbol("DeclarationEnd"),
DeclarationDefaultStart: Symbol("DeclarationDefaultStart"),
QuotedDeclarationDefault: Symbol("QuotedDeclarationDefault"),
DeclarationDefault: Symbol("DeclarationDefault"),
Comment: Symbol("Comment"),
};
const validDeclarationChar = /[a-zA-Z0-9_]/;
const validComment = /[#]/;
const whiteSpace = /[ \t]/;
const newLine = /[\n]/;
const validDeclarationSeparator = /[:]/;
const validTypeChar = /[a-zA-Z]/;
const validDefaultStart = /[|]/;
const validDefaultChar = /[^#\n|]/;
const validQuote = /["]/;
const escapeChar = /[\\]/;
const validTypeEnd = /[ \t\n|#]/;
module.exports = (chars) => {
const input = Input(chars);
const tokens = [];
let state = State.Noop;
let currentToken = "";
let escaped = false;
let char = "";
let keepGoing = true;
while (keepGoing) {
char = input.peek();
if (input.eof()) {
keepGoing = false;
}
if (state === State.Noop) {
if (char.match(validDeclarationChar)) {
state = State.DeclarationName;
currentToken = "";
} else if (char.match(validComment)) {
state = State.Comment;
input.pop();
} else if (char.match(whiteSpace) || char.match(newLine)) {
input.pop();
} else {
throw new SyntaxError(`Unexpected ${char}`);
}
} else if (state === State.DeclarationName) {
if (char.match(validDeclarationChar)) {
currentToken += input.pop();
} else if (char.match(whiteSpace) || char.match(validDeclarationSeparator)) {
tokens.push({
type: "DeclarationName",
value: currentToken,
});
state = State.DeclarationSeparator;
} else {
throw new SyntaxError(`Unexpected ${char} in DeclarationName`);
}
} else if (state === State.DeclarationSeparator) {
if (char.match(validTypeChar)) {
state = State.DeclarationType;
currentToken = "";
} else if (char.match(validDeclarationSeparator) || char.match(whiteSpace)) {
// TODO Check multiple chars to ensure " *: *"
input.pop();
} else {
throw new SyntaxError(`Unexpected ${char} in DeclarationSeparator`);
}
} else if (state === State.DeclarationType) {
if (char.match(validTypeChar)) {
currentToken += input.pop();
} else if (char.match(validTypeEnd)) {
tokens.push({
type: "DeclarationType",
value: currentToken,
});
state = State.DeclarationEnd;
} else {
throw new SyntaxError(`Unexpected char ${char} in DeclarationType`);
}
} else if (state === State.DeclarationEnd) {
if (char.match(whiteSpace)) {
input.pop();
} else if (char.match(validDefaultStart)) {
input.pop();
state = State.DeclarationDefaultStart;
} else if (char.match(newLine) || char.match(validComment)) {
state = State.Noop;
} else {
throw new SyntaxError(`Unexpected char ${char} in DeclarationEnd`);
}
} else if (state === State.DeclarationDefaultStart) {
if (char.match(whiteSpace)) {
input.pop();
} else if (char.match(newLine) || char.match(validComment)) {
tokens.push({
type: "DeclarationDefault",
value: "",
});
state = State.Noop;
} else if (char.match(validQuote)) {
state = State.QuotedDeclarationDefault;
currentToken = "";
input.pop();
} else if (char.match(validDefaultChar)) {
state = State.DeclarationDefault;
currentToken = "";
} else {
throw new SyntaxError(`Unexpected char ${char} in DeclarationDefaultStart`);
}
} else if (state === State.QuotedDeclarationDefault) {
if (char.match(validQuote) && !escaped) {
tokens.push({
type: "DeclarationDefault",
value: currentToken,
});
state = State.Noop;
input.pop();
} else if (char.match(escapeChar) && !escaped) {
escaped = true;
input.pop();
} else {
currentToken += input.pop();
escaped = false;
}
} else if (state === State.DeclarationDefault) {
if (char.match(validDefaultChar)) {
currentToken += input.pop();
} else {
state = State.Noop;
tokens.push({
type: "DeclarationDefault",
value: currentToken.trim(),
});
}
} else if (state === State.Comment) {
input.pop();
if (char.match(newLine)) {
state = State.Noop;
}
} else {
throw new SyntaxError(`Seriously wtf just happened? Debug: ${state}`);
}
}
logTokens(tokens);
return tokens;
};
module.exports = chars => parser(syntax, "Noop", chars);

@@ -1,38 +0,43 @@

const R = require("ramda");
const duplicatedDefinitions = (manifest) => {
const duplicates = [];
const duplicateMap = {};
const duplicatedDefinitions = R.pipe(
R.groupBy(R.prop("name")),
R.values,
R.filter(R.compose(R.lt(1), R.prop("length"))),
R.map(R.head)
);
module.exports = R.pipe(
R.reduce((definitions, token) => {
/* eslint no-param-reassign: 0 */
if (token.type === "DeclarationName") {
return [...definitions, {
name: token.value,
}];
} else if (token.type === "DeclarationType") {
definitions[definitions.length - 1].type = token.value;
} else if (token.type === "DeclarationDefault") {
definitions[definitions.length - 1].default = token.value;
manifest.forEach((definition) => {
if (duplicateMap[definition.name]) {
duplicates.push(definition.name);
}
return definitions;
}, []),
(manifest) => {
const duplicated = duplicatedDefinitions(manifest);
duplicateMap[definition.name] = 1;
});
if (duplicated.length) {
const stack = duplicated.map(definition =>
` Env var ${definition.name} is declared more than once.`
).join("\n");
const err = new Error(`Varium: Error reading manifest`);
err.stack = stack;
throw err;
} else {
return manifest;
}
return duplicates;
};
const parseTokens = (definitions, token) => {
/* eslint no-param-reassign: 0 */
if (token.type === "DeclarationName") {
return [...definitions, {
name: token.value,
}];
} else if (token.type === "DeclarationType") {
definitions[definitions.length - 1].type = token.value;
} else if (token.type === "DeclarationDefault") {
definitions[definitions.length - 1].default = token.value;
}
);
return definitions;
};
module.exports = (manifest) => {
const definitions = manifest.reduce(parseTokens, []);
const duplicated = duplicatedDefinitions(definitions);
if (duplicated.length) {
const stack = duplicated.map(definition =>
` Env var ${definition.name} is declared more than once.`
).join("\n");
const err = new Error("Varium: Error reading manifest");
err.stack = stack;
throw err;
} else {
return definitions;
}
};

@@ -1,78 +0,17 @@

const debug = require("debug");
const Validators = require("./validators");
const validatorError = require("../util/suggest");
const logName = debug("varium:validate:name");
const logValue = debug("varium:validate:value");
let logName;
let logValue;
const isnt = x => x === "" || x === undefined;
try {
// eslint-disable-next-line
const debug = require("debug");
logName = debug("varium:validate:name");
logValue = debug("varium:validate:value");
} catch (e) {
logName = () => {};
logValue = () => {};
}
const Validators = {
String: (value, def) => value || def,
Int: (value, def) => {
const validDef = isnt(def) ? undefined : parseInt(def, 10);
const validValue = isnt(value) ? undefined : parseInt(value, 10);
if (typeof validDef === "number" && (isNaN(validDef) || String(validDef) !== def)) {
throw new Error("default is not a valid Int");
}
if (typeof validValue === "number" && (isNaN(validValue) || String(validValue) !== value)) {
throw new Error("value is not a valid Int");
}
return !isNaN(validValue)
? validValue
: validDef;
},
Float: (value, def) => {
const validDef = isnt(def) ? undefined : parseFloat(def, 10);
const validValue = isnt(value) ? undefined : parseFloat(value, 10);
if (typeof validDef === "number" && (isNaN(validDef) || isNaN(def))) {
throw new Error("default is not a valid Float");
}
if (typeof validValue === "number" && (isNaN(validValue) || isNaN(value))) {
throw new Error("value is not a valid Float");
}
return !isNaN(validValue)
? validValue
: validDef;
},
Bool: (value, def) => {
let validDef;
let validValue;
if (def === "false") { validDef = false; }
else if (def === "true") { validDef = true; }
else if (isnt(def)) { validDef = undefined; }
else { throw new Error("default is not a valid Bool"); }
if (value === "false") { validValue = false; }
else if (value === "true") { validValue = true; }
else if (isnt(value)) { validValue = undefined; }
else { throw new Error("value is not a valid Bool"); }
return typeof validValue === "boolean" ? validValue : validDef;
},
Json: (value, def) => {
let validDef;
let validValue;
try {
validDef = isnt(def) ? undefined : JSON.parse(def);
} catch (e) {
throw new Error("default is not a valid Json");
}
try {
validValue = isnt(value) ? undefined : JSON.parse(value);
} catch (e) {
throw new Error("value is not a valid Json");
}
return validValue !== undefined ? validValue : validDef;
},
};
module.exports = (customValidators, manifest, env) => {

@@ -82,8 +21,15 @@ const validators = Object.assign({}, Validators, customValidators);

return manifest.map((definition) => {
if (!validators[definition.type]) {
const validator = validators[definition.type];
const envValue = env[definition.name];
const envDefault = definition.default;
if (!validator) {
const errorMessage = validatorError(Object.keys(validators), definition.type);
return {
error$: `The type ${definition.type} for env var "${definition.name}" does not exist.`,
error$: `The type ${definition.type} for env var "${definition.name}" does not exist.\n${errorMessage}`,
};
}
if (env[definition.name] === undefined && definition.default === undefined) {
if (envValue === undefined && envDefault === undefined) {
return {

@@ -95,12 +41,26 @@ error$: `Env var "${definition.name}" requires a value.`,

logName(definition.name);
logValue(`Value: ${env[definition.name]}`);
logValue(`Default: ${definition.default}`);
logValue(`Value: ${envValue}`);
logValue(`Default: ${envDefault}`);
if (envDefault !== undefined) {
try {
validator(envDefault);
} catch (e) {
return {
error$: `Default value for "${definition.name}" is invalid: ${e.message}`,
};
}
}
const value = envValue !== undefined
? envValue
: envDefault;
try {
return {
[definition.name]: validators[definition.type](env[definition.name], definition.default),
[definition.name]: validator(value),
};
} catch (e) {
return {
error$: `Env var "${definition.name}" is invalid: ${e.message}`,
error$: `Value for "${definition.name}" is invalid: ${e.message}`,
};

@@ -107,0 +67,0 @@ }

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc