@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 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
+ [![npm version](https://img.shields.io/npm/v/env-guardian.svg)](https://www.npmjs.com/package/env-guardian)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+ [![Node requirement](https://img.shields.io/node/v/env-guardian.svg)](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
+ };
@@ -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
+ };
@@ -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
+ });