@kevinpatil/envguard 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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +125 -0
  3. package/dist/index.js +307 -0
  4. package/package.json +50 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kevin Patil
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,125 @@
1
+ # @kevinpatil/envguard
2
+
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
+ [![CI](https://github.com/kevinpatildxd/envguard/actions/workflows/ci.yml/badge.svg)](https://github.com/kevinpatildxd/envguard/actions/workflows/ci.yml)
5
+ [![npm version](https://img.shields.io/npm/v/%40kevinpatil%2Fenvguard.svg)](https://www.npmjs.com/package/@kevinpatil/envguard)
6
+ [![npm downloads](https://img.shields.io/npm/dm/%40kevinpatil%2Fenvguard.svg)](https://www.npmjs.com/package/@kevinpatil/envguard)
7
+
8
+ Validate your `.env` file against `.env.example` before your app ships.
9
+
10
+ Catches missing keys, insecure defaults, type mismatches, weak secrets, and more — in a single fast command.
11
+
12
+ ```
13
+ $ npx @kevinpatil/envguard
14
+
15
+ envguard — checking .env against .env.example
16
+
17
+ ERRORS (2)
18
+ ✗ DATABASE_URL — Missing required key (defined in .env.example)
19
+ ✗ JWT_SECRET — Insecure placeholder value: 'secret'
20
+
21
+ WARNINGS (2)
22
+ ⚠ PORT — Expected a number but got 'abc'
23
+ ⚠ STRIPE_KEY — Key is not declared in .env.example
24
+
25
+ 2 error(s) found. Fix them before deploying.
26
+ ```
27
+
28
+ ---
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ npm install --save-dev @kevinpatil/envguard
34
+ ```
35
+
36
+ Or run without installing:
37
+
38
+ ```bash
39
+ npx @kevinpatil/envguard
40
+ ```
41
+
42
+ ---
43
+
44
+ ## Usage
45
+
46
+ ```bash
47
+ # Validate .env against .env.example in the current directory
48
+ npx @kevinpatil/envguard
49
+
50
+ # Target a specific env file
51
+ npx @kevinpatil/envguard --env .env.production
52
+
53
+ # Use a custom example file
54
+ npx @kevinpatil/envguard --example .env.example.production
55
+
56
+ # Exit with code 1 if any errors are found (for CI)
57
+ npx @kevinpatil/envguard --strict
58
+
59
+ # Output results as JSON
60
+ npx @kevinpatil/envguard --json
61
+ ```
62
+
63
+ ---
64
+
65
+ ## Validation Rules
66
+
67
+ | Rule | Severity | Description |
68
+ |---|---|---|
69
+ | `missing-key` | ERROR | Key defined in `.env.example` is absent from `.env` |
70
+ | `empty-value` | ERROR | Key is present but has no value |
71
+ | `insecure-defaults` | ERROR | Value matches a known insecure placeholder (`changeme`, `secret`, `todo`…) |
72
+ | `undeclared-key` | WARNING | Key exists in `.env` but is not in `.env.example` |
73
+ | `weak-secret` | WARNING | Secret key is under 16 characters |
74
+ | `type-mismatch` | WARNING | Numeric key (`PORT`, `TIMEOUT`…) has a non-numeric value |
75
+ | `malformed-url` | WARNING | URL key has a value with a missing or unrecognized protocol |
76
+ | `boolean-mismatch` | WARNING | Boolean key (`FEATURE_*`, `ENABLE_*`…) has a non-boolean value |
77
+
78
+ ---
79
+
80
+ ## CI Integration
81
+
82
+ Add envguard to your pipeline to block deployments with bad config:
83
+
84
+ ### GitHub Actions
85
+
86
+ ```yaml
87
+ - name: Validate environment variables
88
+ run: npx @kevinpatil/envguard --strict
89
+ ```
90
+
91
+ ### Any CI
92
+
93
+ ```bash
94
+ npx @kevinpatil/envguard --strict # exits with code 1 if errors are found
95
+ ```
96
+
97
+ ### JSON output for custom pipelines
98
+
99
+ ```bash
100
+ npx @kevinpatil/envguard --json | jq '.[] | select(.severity == "error")'
101
+ ```
102
+
103
+ ---
104
+
105
+ ## How it works
106
+
107
+ 1. Reads `.env.example` as the source of truth
108
+ 2. Reads your `.env` file
109
+ 3. Runs all validation rules against both
110
+ 4. Prints a color-coded report to the terminal
111
+ 5. In `--strict` mode, exits with code `1` if any errors are found
112
+
113
+ No config files required. No API keys. Works offline, in Docker, everywhere.
114
+
115
+ ---
116
+
117
+ ## Contributing
118
+
119
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
120
+
121
+ ---
122
+
123
+ ## License
124
+
125
+ MIT © [Kevin Patil](https://github.com/kevinpatildxd)
package/dist/index.js ADDED
@@ -0,0 +1,307 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/index.ts
27
+ var import_commander = require("commander");
28
+ var import_path = __toESM(require("path"));
29
+
30
+ // src/parser.ts
31
+ var import_fs = __toESM(require("fs"));
32
+ function parseLines(content) {
33
+ const map = /* @__PURE__ */ new Map();
34
+ for (const rawLine of content.split("\n")) {
35
+ const line = rawLine.trim();
36
+ if (!line || line.startsWith("#")) continue;
37
+ const eqIndex = line.indexOf("=");
38
+ if (eqIndex === -1) continue;
39
+ const key = line.slice(0, eqIndex).trim();
40
+ if (!key) continue;
41
+ let value = line.slice(eqIndex + 1).trim();
42
+ const commentIndex = value.indexOf(" #");
43
+ if (commentIndex !== -1) {
44
+ value = value.slice(0, commentIndex).trim();
45
+ }
46
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
47
+ value = value.slice(1, -1);
48
+ }
49
+ map.set(key, value);
50
+ }
51
+ return map;
52
+ }
53
+ function parseEnvFile(filePath) {
54
+ if (!import_fs.default.existsSync(filePath)) {
55
+ throw new Error(`File not found: ${filePath}`);
56
+ }
57
+ const content = import_fs.default.readFileSync(filePath, "utf-8");
58
+ return parseLines(content);
59
+ }
60
+ function parseEnvExample(filePath) {
61
+ if (!import_fs.default.existsSync(filePath)) {
62
+ throw new Error(`File not found: ${filePath}`);
63
+ }
64
+ const content = import_fs.default.readFileSync(filePath, "utf-8");
65
+ return parseLines(content);
66
+ }
67
+
68
+ // src/rules/missing-key.ts
69
+ function missingKey(env, example) {
70
+ const results = [];
71
+ for (const key of example.keys()) {
72
+ if (!env.has(key)) {
73
+ results.push({
74
+ rule: "missing-key",
75
+ severity: "error",
76
+ key,
77
+ message: `Missing required key (defined in .env.example)`
78
+ });
79
+ }
80
+ }
81
+ return results;
82
+ }
83
+
84
+ // src/rules/empty-value.ts
85
+ function emptyValue(env, example) {
86
+ const results = [];
87
+ for (const key of example.keys()) {
88
+ if (env.has(key) && env.get(key) === "") {
89
+ results.push({
90
+ rule: "empty-value",
91
+ severity: "error",
92
+ key,
93
+ message: `Value is empty`
94
+ });
95
+ }
96
+ }
97
+ return results;
98
+ }
99
+
100
+ // src/rules/undeclared-key.ts
101
+ function undeclaredKey(env, example) {
102
+ const results = [];
103
+ for (const key of env.keys()) {
104
+ if (!example.has(key)) {
105
+ results.push({
106
+ rule: "undeclared-key",
107
+ severity: "warning",
108
+ key,
109
+ message: `Key is not declared in .env.example`
110
+ });
111
+ }
112
+ }
113
+ return results;
114
+ }
115
+
116
+ // src/rules/insecure-defaults.ts
117
+ var INSECURE_VALUES = /* @__PURE__ */ new Set([
118
+ "changeme",
119
+ "change_me",
120
+ "todo",
121
+ "secret",
122
+ "password",
123
+ "1234",
124
+ "12345",
125
+ "123456",
126
+ "test",
127
+ "example",
128
+ "placeholder",
129
+ "dummy",
130
+ "fake",
131
+ "temp"
132
+ ]);
133
+ function insecureDefaults(env, _example) {
134
+ const results = [];
135
+ for (const [key, value] of env.entries()) {
136
+ if (INSECURE_VALUES.has(value.toLowerCase())) {
137
+ results.push({
138
+ rule: "insecure-defaults",
139
+ severity: "error",
140
+ key,
141
+ message: `Insecure placeholder value: '${value}'`
142
+ });
143
+ }
144
+ }
145
+ return results;
146
+ }
147
+
148
+ // src/rules/weak-secret.ts
149
+ var SECRET_PATTERN = /(_SECRET|_KEY|_TOKEN|_PASSWORD|_PASS|_PWD|^JWT_|^API_)/i;
150
+ var MIN_LENGTH = 16;
151
+ function weakSecret(env, _example) {
152
+ const results = [];
153
+ for (const [key, value] of env.entries()) {
154
+ if (SECRET_PATTERN.test(key) && value.length > 0 && value.length < MIN_LENGTH) {
155
+ results.push({
156
+ rule: "weak-secret",
157
+ severity: "warning",
158
+ key,
159
+ message: `Secret is too short (${value.length} chars, minimum is ${MIN_LENGTH})`
160
+ });
161
+ }
162
+ }
163
+ return results;
164
+ }
165
+
166
+ // src/rules/type-mismatch.ts
167
+ var NUMERIC_PATTERN = /^(PORT|TIMEOUT|MAX_|MIN_|LIMIT|RETRY_|WORKERS|THREADS)/i;
168
+ function typeMismatch(env, _example) {
169
+ const results = [];
170
+ for (const [key, value] of env.entries()) {
171
+ if (NUMERIC_PATTERN.test(key) && value !== "" && isNaN(Number(value))) {
172
+ results.push({
173
+ rule: "type-mismatch",
174
+ severity: "warning",
175
+ key,
176
+ message: `Expected a number but got '${value}'`
177
+ });
178
+ }
179
+ }
180
+ return results;
181
+ }
182
+
183
+ // src/rules/malformed-url.ts
184
+ var URL_PATTERN = /(_URL|_URI|_HOST|^DATABASE_|^REDIS_|^MONGO_)/i;
185
+ var VALID_PROTOCOLS = /* @__PURE__ */ new Set([
186
+ "http:",
187
+ "https:",
188
+ "postgres:",
189
+ "postgresql:",
190
+ "mysql:",
191
+ "mongodb:",
192
+ "mongodb+srv:",
193
+ "redis:",
194
+ "rediss:",
195
+ "amqp:",
196
+ "amqps:",
197
+ "ftp:",
198
+ "ftps:"
199
+ ]);
200
+ function isValidUrl(value) {
201
+ try {
202
+ const url = new URL(value);
203
+ return VALID_PROTOCOLS.has(url.protocol);
204
+ } catch {
205
+ return false;
206
+ }
207
+ }
208
+ function malformedUrl(env, _example) {
209
+ const results = [];
210
+ for (const [key, value] of env.entries()) {
211
+ if (URL_PATTERN.test(key) && value !== "" && !isValidUrl(value)) {
212
+ results.push({
213
+ rule: "malformed-url",
214
+ severity: "warning",
215
+ key,
216
+ message: `Value does not appear to be a valid URL: '${value}'`
217
+ });
218
+ }
219
+ }
220
+ return results;
221
+ }
222
+
223
+ // src/rules/boolean-mismatch.ts
224
+ var BOOLEAN_PATTERN = /^(FEATURE_|ENABLE_|DISABLE_|IS_|USE_|ALLOW_|FLAG_)/i;
225
+ var VALID_BOOLEANS = /* @__PURE__ */ new Set(["true", "false", "1", "0"]);
226
+ function booleanMismatch(env, _example) {
227
+ const results = [];
228
+ for (const [key, value] of env.entries()) {
229
+ if (BOOLEAN_PATTERN.test(key) && value !== "" && !VALID_BOOLEANS.has(value.toLowerCase())) {
230
+ results.push({
231
+ rule: "boolean-mismatch",
232
+ severity: "warning",
233
+ key,
234
+ message: `Expected a boolean (true/false/1/0) but got '${value}'`
235
+ });
236
+ }
237
+ }
238
+ return results;
239
+ }
240
+
241
+ // src/validator.ts
242
+ function validate(env, example) {
243
+ return [
244
+ ...missingKey(env, example),
245
+ ...emptyValue(env, example),
246
+ ...undeclaredKey(env, example),
247
+ ...insecureDefaults(env, example),
248
+ ...weakSecret(env, example),
249
+ ...typeMismatch(env, example),
250
+ ...malformedUrl(env, example),
251
+ ...booleanMismatch(env, example)
252
+ ];
253
+ }
254
+
255
+ // src/reporter.ts
256
+ var import_chalk = __toESM(require("chalk"));
257
+ function report(results, options = {}) {
258
+ if (options.json) {
259
+ console.log(JSON.stringify(results, null, 2));
260
+ return;
261
+ }
262
+ const errors = results.filter((r) => r.severity === "error");
263
+ const warnings = results.filter((r) => r.severity === "warning");
264
+ const passedCount = results.length === 0 ? "all checks" : "remaining keys";
265
+ console.log(import_chalk.default.bold("\nenvguard \u2014 checking .env against .env.example\n"));
266
+ if (errors.length > 0) {
267
+ console.log(import_chalk.default.red.bold(`ERRORS (${errors.length})`));
268
+ for (const r of errors) {
269
+ console.log(import_chalk.default.red(` \u2717 ${r.key} \u2014 ${r.message}`));
270
+ }
271
+ console.log();
272
+ }
273
+ if (warnings.length > 0) {
274
+ console.log(import_chalk.default.yellow.bold(`WARNINGS (${warnings.length})`));
275
+ for (const r of warnings) {
276
+ console.log(import_chalk.default.yellow(` \u26A0 ${r.key} \u2014 ${r.message}`));
277
+ }
278
+ console.log();
279
+ }
280
+ if (errors.length === 0 && warnings.length === 0) {
281
+ console.log(import_chalk.default.green.bold(" \u2714 All checks passed"));
282
+ } else {
283
+ console.log(import_chalk.default.green(`PASSED \u2014 ${passedCount} ok`));
284
+ }
285
+ console.log();
286
+ if (errors.length > 0) {
287
+ console.log(import_chalk.default.red.bold(`${errors.length} error(s) found. Fix them before deploying.`));
288
+ } else if (warnings.length > 0) {
289
+ console.log(import_chalk.default.yellow(`${warnings.length} warning(s). Review before deploying.`));
290
+ }
291
+ }
292
+
293
+ // src/index.ts
294
+ var program = new import_commander.Command();
295
+ program.name("envguard").description("Validate .env files against .env.example before your app ships").version("1.0.0").option("--env <file>", "path to .env file", ".env").option("--example <file>", "path to .env.example file", ".env.example").option("--strict", "exit with code 1 if any errors are found").option("--json", "output results as JSON").action((options) => {
296
+ const envPath = import_path.default.resolve(process.cwd(), options.env);
297
+ const examplePath = import_path.default.resolve(process.cwd(), options.example);
298
+ const env = parseEnvFile(envPath);
299
+ const example = parseEnvExample(examplePath);
300
+ const results = validate(env, example);
301
+ report(results, { json: options.json });
302
+ const hasErrors = results.some((r) => r.severity === "error");
303
+ if (options.strict && hasErrors) {
304
+ process.exit(1);
305
+ }
306
+ });
307
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@kevinpatil/envguard",
3
+ "version": "1.0.0",
4
+ "description": "CLI tool that validates .env files against .env.example before your app ships",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "envguard": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsup",
14
+ "test": "vitest run",
15
+ "typecheck": "tsc --noEmit",
16
+ "dev": "tsup --watch"
17
+ },
18
+ "keywords": [
19
+ "cli",
20
+ "dotenv",
21
+ "env",
22
+ "environment-variables",
23
+ "validation",
24
+ "devtools",
25
+ "typescript"
26
+ ],
27
+ "author": "Kevin Patil",
28
+ "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/kevinpatildxd/envguard.git"
32
+ },
33
+ "bugs": {
34
+ "url": "https://github.com/kevinpatildxd/envguard/issues"
35
+ },
36
+ "homepage": "https://github.com/kevinpatildxd/envguard#readme",
37
+ "engines": {
38
+ "node": ">=18"
39
+ },
40
+ "dependencies": {
41
+ "chalk": "^4.1.2",
42
+ "commander": "^12.1.0"
43
+ },
44
+ "devDependencies": {
45
+ "@types/node": "^20.0.0",
46
+ "tsup": "^8.1.0",
47
+ "typescript": "^5.4.5",
48
+ "vitest": "^1.6.0"
49
+ }
50
+ }