@awish/env-guardian 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +143 -0
- package/package.json +29 -0
- package/src/cli.js +20 -0
- package/src/errors.js +20 -0
- package/src/index.js +16 -0
- package/src/parser.js +55 -0
- package/src/security.js +15 -0
- package/src/types.js +26 -0
- package/src/validator.js +45 -0
- package/tests/index.test.js +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# 🛡️ env-guardian
|
|
2
|
+
|
|
3
|
+
> A lightweight, zero-dependency, and highly secure environment variable validation library for Node.js.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/env-guardian)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://nodejs.org/)
|
|
8
|
+
|
|
9
|
+
**env-guardian** enforces strict schemas on your `.env` files and runtime environment variables. It prevents prototype pollution, masks sensitive secrets from crash logs, and guarantees that your app never boots with missing or invalid configurations.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## ✨ Features
|
|
14
|
+
|
|
15
|
+
- **🔒 Security First:** Actively scans for prototype pollution attempts (`__proto__`, `constructor`) and blocks them.
|
|
16
|
+
- **🤫 Secret Masking:** Automatically prevents sensitive keys (e.g., `PASSWORD`, `API_KEY`, `TOKEN`) from leaking in error traces.
|
|
17
|
+
- **🛡️ Type Safety:** Casts string variables into actual booleans and numbers.
|
|
18
|
+
- **✅ Schema Validation:** Enforce `required` variables, apply `default` fallbacks, and restrict to `allowedValues`.
|
|
19
|
+
- **🚀 Zero Dependencies:** Tiny footprint. Built entirely with native Node.js APIs.
|
|
20
|
+
- **💻 CLI Included:** Verify your environments directly from the terminal or CI/CD pipelines.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 📦 Installation
|
|
25
|
+
|
|
26
|
+
To install the package, run the following command in your project directory:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install env-guardian
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
*(Note: If the package name is scoped, replace this with `npm install @your-username/env-guardian`)*
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 💻 Usage
|
|
37
|
+
|
|
38
|
+
Create a validation schema and pass it to `loadAndValidate()`. If the validation fails, it throws an early, descriptive error—preventing your app from running in a broken state.
|
|
39
|
+
|
|
40
|
+
### 1. Basic Example
|
|
41
|
+
|
|
42
|
+
```javascript
|
|
43
|
+
import { loadAndValidate } from 'env-guardian';
|
|
44
|
+
|
|
45
|
+
// Define how your environment should look
|
|
46
|
+
const schema = {
|
|
47
|
+
PORT: {
|
|
48
|
+
type: 'number',
|
|
49
|
+
required: true,
|
|
50
|
+
default: 3000
|
|
51
|
+
},
|
|
52
|
+
NODE_ENV: {
|
|
53
|
+
type: 'string',
|
|
54
|
+
allowedValues: ['development', 'staging', 'production'],
|
|
55
|
+
default: 'development'
|
|
56
|
+
},
|
|
57
|
+
DB_PASSWORD: {
|
|
58
|
+
type: 'string',
|
|
59
|
+
required: true
|
|
60
|
+
},
|
|
61
|
+
ENABLE_FEATURE_X: {
|
|
62
|
+
type: 'boolean',
|
|
63
|
+
default: false
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Validate!
|
|
68
|
+
// This automatically loads from '.env' by default and applies your schema
|
|
69
|
+
const env = loadAndValidate(schema);
|
|
70
|
+
|
|
71
|
+
// Your variables are now safely typed and guaranteed to be present
|
|
72
|
+
console.log(typeof env.PORT); // "number"
|
|
73
|
+
console.log(env.ENABLE_FEATURE_X); // true or false boolean
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 2. Error Handling & Secret Masking
|
|
77
|
+
|
|
78
|
+
If a developer configured something incorrectly, `env-guardian` provides clear errors. However, it will **never** leak secrets in the stack trace.
|
|
79
|
+
|
|
80
|
+
```javascript
|
|
81
|
+
// A developer accidentally typed `DB_PASSWORD=12345` instead of a string...
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const env = loadAndValidate(schema);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error(error.message);
|
|
87
|
+
// Output: "Invalid value for DB_PASSWORD (value masked for security)"
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 🛠️ Configuration Options
|
|
94
|
+
|
|
95
|
+
The `loadAndValidate` method accepts an optional second argument for configuration:
|
|
96
|
+
|
|
97
|
+
```javascript
|
|
98
|
+
const env = loadAndValidate(schema, {
|
|
99
|
+
path: './config/.env.production', // Load a custom file path
|
|
100
|
+
skipDotenv: true, // Don't read from disk, just validate process.env
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## 🖥 CLI Tool
|
|
107
|
+
|
|
108
|
+
Want to validate your `.env` without writing any code? **env-guardian** includes a terminal tool perfect for CI/CD pipelines (like GitHub Actions) to catch bad environment configurations before deployment.
|
|
109
|
+
|
|
110
|
+
1. Ensure you have an `env-schema.json` file in your root directory:
|
|
111
|
+
```json
|
|
112
|
+
{
|
|
113
|
+
"PORT": { "type": "number", "required": true },
|
|
114
|
+
"API_KEY": { "type": "string", "required": true }
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
2. Run the guardian:
|
|
119
|
+
```bash
|
|
120
|
+
npx env-guardian
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**Output:**
|
|
124
|
+
```
|
|
125
|
+
✅ Environment configuration is valid and secure.
|
|
126
|
+
```
|
|
127
|
+
*(Will exit with code `1` and print errors if the configuration is invalid).*
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## 🧪 Testing
|
|
132
|
+
|
|
133
|
+
To run the internal test suite:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
npm test
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## 📄 License
|
|
142
|
+
|
|
143
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@awish/env-guardian",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Secure environment variable validation library",
|
|
5
|
+
"main": "./src/index.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"import": "./src/index.js",
|
|
9
|
+
"require": "./src/index.js"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"type": "module",
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18.0.0"
|
|
15
|
+
},
|
|
16
|
+
"bin": "./src/cli.js",
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "node --test"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"env",
|
|
22
|
+
"environment",
|
|
23
|
+
"variable",
|
|
24
|
+
"validation",
|
|
25
|
+
"security"
|
|
26
|
+
],
|
|
27
|
+
"author": "",
|
|
28
|
+
"license": "MIT"
|
|
29
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { loadAndValidate } from './index.js'
|
|
5
|
+
|
|
6
|
+
const schemaPath = path.resolve(process.cwd(), 'env-schema.json');
|
|
7
|
+
|
|
8
|
+
if(!fs.existsSync(schemaPath)) {
|
|
9
|
+
console.error('Error: env-schema.json not found in current directory.')
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf-8'));
|
|
15
|
+
loadAndValidate(schema);
|
|
16
|
+
console.log('✅ Environment configuration is valid and secure.');
|
|
17
|
+
} catch (err) {
|
|
18
|
+
console.error(`❌ Validation Failed:\n${err.message}`)
|
|
19
|
+
process.exit(1)
|
|
20
|
+
}
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export class EnvValidationError extends Error {
|
|
2
|
+
constructor(message, property) {
|
|
3
|
+
let safeMessage = message;
|
|
4
|
+
|
|
5
|
+
if(property && /PASSWORD|TOKEN|API_KEY/i.test(property)) {
|
|
6
|
+
safeMessage = `Invalid value for ${property} (value masked for security)`
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
super(safeMessage);
|
|
10
|
+
this.name = 'EnvValidationError';
|
|
11
|
+
this.property = property;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class EnvSecurityError extends Error {
|
|
16
|
+
constructor(message) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = 'EnvSecurityError';
|
|
19
|
+
}
|
|
20
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { parseEnvFile } from "./parser.js";
|
|
2
|
+
import { validateEnv } from "./validator.js";
|
|
3
|
+
import { checkSecurity } from "./security.js";
|
|
4
|
+
|
|
5
|
+
export const loadAndValidate = (schema, options = {}) => {
|
|
6
|
+
const parsed = options.skipDotenv ? {} : parseEnvFile(options.path || '.env');
|
|
7
|
+
const combinedEnv = { ...process.env, ...parsed};
|
|
8
|
+
|
|
9
|
+
checkSecurity(combinedEnv);
|
|
10
|
+
|
|
11
|
+
return validateEnv(schema, combinedEnv);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export { validateEnv } from './validator.js';
|
|
15
|
+
export { parseEnvFile as parseDotenv } from './parser.js';
|
|
16
|
+
export { EnvValidationError, EnvSecurityError } from './errors.js';
|
package/src/parser.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const MAX_FILE_SIZE = 1024 * 1024; // 1MB
|
|
5
|
+
|
|
6
|
+
const DANGEROUS_KEYS = ['__proto__', 'constructor', 'prototype'];
|
|
7
|
+
|
|
8
|
+
const isValidKey = (key) => {
|
|
9
|
+
if (DANGEROUS_KEYS.includes(key.toLowerCase())) return false;
|
|
10
|
+
return /^[a-zA-Z0-9_]+$/.test(key);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const parseEnvFile = (filePath) => {
|
|
14
|
+
const fullPath = path.resolve(process.cwd(), filePath);
|
|
15
|
+
|
|
16
|
+
let stats;
|
|
17
|
+
try {
|
|
18
|
+
stats = fs.statSync(fullPath);
|
|
19
|
+
} catch (err) {
|
|
20
|
+
if (err.code === 'ENOENT') return {};
|
|
21
|
+
throw err;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (stats.size > MAX_FILE_SIZE) {
|
|
25
|
+
throw new Error('Environment file exceeds maximum allowed size of 1MB');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
29
|
+
const result = Object.create(null);
|
|
30
|
+
|
|
31
|
+
const lines = content.split(/\r?\n/);
|
|
32
|
+
for (const line of lines) {
|
|
33
|
+
const trimmed = line.trim();
|
|
34
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
35
|
+
|
|
36
|
+
const match = trimmed.match(/^([^=]+)=(.*)$/);
|
|
37
|
+
if (!match) continue;
|
|
38
|
+
|
|
39
|
+
const key = match[1].trim();
|
|
40
|
+
let value = match[2].trim();
|
|
41
|
+
|
|
42
|
+
if (!isValidKey(key)) {
|
|
43
|
+
throw new Error(`Invalid or dangerous key detected: ${key}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
47
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
48
|
+
value = value.slice(1, -1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
result[key] = value;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return result;
|
|
55
|
+
};
|
package/src/security.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { EnvSecurityError } from "./errors.js";
|
|
2
|
+
|
|
3
|
+
export const checkSecurity = (env) => {
|
|
4
|
+
const dangerousKeys = [
|
|
5
|
+
'__PROTO__',
|
|
6
|
+
'CONSTRUCTOR',
|
|
7
|
+
'PROTOTYPE'
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
for (const key of dangerousKeys) {
|
|
11
|
+
if (key.toUpperCase() in env || key.toLowerCase() in env) {
|
|
12
|
+
throw new EnvSecurityError(`Potentially dangerous environment variable detected: ${key}`)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
};
|
package/src/types.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const parseType = (value, type) => {
|
|
2
|
+
if (value === undefined || value === null) {
|
|
3
|
+
return value;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
if (type === 'string') {
|
|
7
|
+
return String(value);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (type === 'number') {
|
|
11
|
+
const num = Number(value)
|
|
12
|
+
if (Number.isNaN(num)) {
|
|
13
|
+
throw new Error('Must be a valid number.')
|
|
14
|
+
}
|
|
15
|
+
return num;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (type === 'boolean') {
|
|
19
|
+
const normalized = String(value).trim().toLowerCase();
|
|
20
|
+
if (['true', '1', 'yes', 'on'].includes(normalized)) return true;
|
|
21
|
+
if (['false', '0', 'no', 'off'].includes(normalized)) return false;
|
|
22
|
+
throw new Error('Must be a boolean (true/false) | (1/0) | (yes/no)')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
throw new Error(`Unknown type: ${type}`)
|
|
26
|
+
};
|
package/src/validator.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { EnvValidationError } from "./errors.js";
|
|
2
|
+
import { parseType } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export const validateEnv = (schema, sourceEnv = process.env) => {
|
|
5
|
+
const validated = {};
|
|
6
|
+
const errors = [];
|
|
7
|
+
|
|
8
|
+
for (const [key, rules] of Object.entries(schema)) {
|
|
9
|
+
let rawValue = sourceEnv[key];
|
|
10
|
+
|
|
11
|
+
if (rawValue === undefined && rules.default !== undefined) {
|
|
12
|
+
rawValue = rules.default;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (rules.required && (rawValue === undefined || rawValue === '')) {
|
|
16
|
+
errors.push(new EnvValidationError(`Missing required environment variable: ${key}`, key));
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (rawValue === undefined) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
let parsedValue = rawValue;
|
|
26
|
+
if (rules.type) {
|
|
27
|
+
parsedValue = parseType(rawValue, rules.type);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (rules.allowedValues && !rules.allowedValues.includes(parsedValue)) {
|
|
31
|
+
throw new Error(`Must be one of: ${rules.allowedValues.join(', ')}`);
|
|
32
|
+
}
|
|
33
|
+
validated[key] = parsedValue;
|
|
34
|
+
} catch (err) {
|
|
35
|
+
errors.push(new EnvValidationError(`Invalid value for ${key}: ${err.message}`, key));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (errors.length > 0) {
|
|
40
|
+
const errorMessage = errors.map(e => `-${e.message}`).join('\n');
|
|
41
|
+
throw new EnvValidationError(`Environment validation failed:\n${errorMessage}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return validated;
|
|
45
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { validateEnv } from '../src/validator.js';
|
|
4
|
+
import { checkSecurity } from '../src/security.js';
|
|
5
|
+
|
|
6
|
+
test('Validator: Detects missing required variables', () => {
|
|
7
|
+
const schema = { PORT: { required: true } };
|
|
8
|
+
assert.throws(() => validateEnv(schema, {}), /Missing required environment variable/);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('Validator: Applies default values', () => {
|
|
12
|
+
const schema = { HOST: { default: 'localhost' } };
|
|
13
|
+
const validated = validateEnv(schema, {});
|
|
14
|
+
assert.strictEqual(validated.HOST, 'localhost');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('Validator: Enforces allowed values', () => {
|
|
18
|
+
const schema = { NODE_ENV: { allowedValues: ['development', 'production'] } };
|
|
19
|
+
assert.throws(() => validateEnv(schema, { NODE_ENV: 'testing' }), /Must be one of/);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('Security: Detects prototype pollution keys', () => {
|
|
23
|
+
assert.throws(() => checkSecurity({ '__PROTO__': 'malicious' }), /potentially dangerous environment variable detected/i);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('Security: Masks sensitive error messages', () => {
|
|
27
|
+
const schema = { DB_PASSWORD: { type: 'number' } };
|
|
28
|
+
assert.throws(() => validateEnv(schema, { DB_PASSWORD: 'my_secret_password' }), /value masked for security/);
|
|
29
|
+
});
|