@gokul-kannur/env-guard 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/.pre-commit-hooks.yaml +11 -0
- package/LICENSE +21 -0
- package/README.md +135 -0
- package/bin/env-guard +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +105 -0
- package/dist/parse.d.ts +11 -0
- package/dist/parse.js +78 -0
- package/dist/report.d.ts +8 -0
- package/dist/report.js +78 -0
- package/dist/rules.d.ts +15 -0
- package/dist/rules.js +124 -0
- package/package.json +43 -0
- package/src/index.ts +76 -0
- package/src/parse.ts +65 -0
- package/src/report.ts +86 -0
- package/src/rules.ts +150 -0
- package/test/.env.example +8 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Pre-commit hooks for env-guard
|
|
2
|
+
# See https://pre-commit.com for more information
|
|
3
|
+
|
|
4
|
+
- id: env-guard
|
|
5
|
+
name: env-guard
|
|
6
|
+
description: Validate .env files for common mistakes
|
|
7
|
+
entry: env-guard
|
|
8
|
+
language: node
|
|
9
|
+
files: '\.env$'
|
|
10
|
+
types: [text]
|
|
11
|
+
pass_filenames: true
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 GokulKannur
|
|
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,135 @@
|
|
|
1
|
+
# env-guard
|
|
2
|
+
|
|
3
|
+
Catch stupid .env mistakes before deploy. No sync. No presets. Just validation.
|
|
4
|
+
|
|
5
|
+
## Why?
|
|
6
|
+
|
|
7
|
+
Because you've broken production due to:
|
|
8
|
+
|
|
9
|
+
- Missing env variable
|
|
10
|
+
- Typo in variable name
|
|
11
|
+
- Empty value you forgot to fill
|
|
12
|
+
- `DEBUG=true` in production
|
|
13
|
+
|
|
14
|
+
This tool catches those. Nothing more.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install -g env-guard
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Check current directory
|
|
26
|
+
env-guard
|
|
27
|
+
|
|
28
|
+
# Check specific file
|
|
29
|
+
env-guard .env.production
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Common Options
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Compare against a specific example file
|
|
36
|
+
env-guard --example .env.template
|
|
37
|
+
|
|
38
|
+
# Strict mode (treat all empty values as errors)
|
|
39
|
+
env-guard --strict
|
|
40
|
+
|
|
41
|
+
# CI mode (GitHub Actions annotations)
|
|
42
|
+
env-guard --ci
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
> **Note**: If `.env.example` (or the specified example file) is missing, comparison checks are skipped.
|
|
46
|
+
|
|
47
|
+
That's it. It will:
|
|
48
|
+
|
|
49
|
+
1. Read `.env`
|
|
50
|
+
2. Compare with `.env.example` (if exists)
|
|
51
|
+
3. Print errors/warnings
|
|
52
|
+
4. Exit with code 1 if errors found
|
|
53
|
+
|
|
54
|
+
## What it checks
|
|
55
|
+
|
|
56
|
+
| Check | Type | Description |
|
|
57
|
+
|-------|------|-------------|
|
|
58
|
+
| Missing vars | Error | Key in example but not in env |
|
|
59
|
+
| Duplicates | Error | Same key defined twice |
|
|
60
|
+
| Empty values | Warning* | Key has no value |
|
|
61
|
+
| Unused vars | Warning | Key in env but not in example |
|
|
62
|
+
| Unsafe defaults | Warning | `DEBUG=true`, `NODE_ENV=development`, etc. |
|
|
63
|
+
|
|
64
|
+
*Empty values become errors in `--strict` mode or for sensitive keys (DATABASE_*, PASSWORD, etc.)
|
|
65
|
+
|
|
66
|
+
## Example output
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
🔍 env-guard validation report
|
|
70
|
+
|
|
71
|
+
Errors (2)
|
|
72
|
+
❌ Missing variable: DATABASE_URL (defined in example)
|
|
73
|
+
❌ Duplicate key: API_KEY (lines 5 and 12)
|
|
74
|
+
|
|
75
|
+
Warnings (3)
|
|
76
|
+
⚠️ Empty value: OPTIONAL_VAR
|
|
77
|
+
⚠️ Unused variable: OLD_CONFIG (not in example)
|
|
78
|
+
⚠️ Unsafe default: DEBUG=true (may not be suitable for production)
|
|
79
|
+
|
|
80
|
+
─────────────────────────────
|
|
81
|
+
Total: 2 error(s), 3 warning(s)
|
|
82
|
+
|
|
83
|
+
env-guard: 2 errors, 3 warnings
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## CI/CD Usage
|
|
87
|
+
|
|
88
|
+
### GitHub Actions
|
|
89
|
+
|
|
90
|
+
```yaml
|
|
91
|
+
- name: Validate env
|
|
92
|
+
run: npx env-guard --ci --strict
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The `--ci` flag outputs GitHub Actions annotations:
|
|
96
|
+
```
|
|
97
|
+
::error file=.env,line=5::Missing variable: DATABASE_URL
|
|
98
|
+
::warning file=.env,line=12::Unsafe default: DEBUG=true
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Pre-commit Hook
|
|
102
|
+
|
|
103
|
+
This uses the CLI via a local hook, not a custom pre-commit plugin.
|
|
104
|
+
|
|
105
|
+
Add to your `.pre-commit-config.yaml`:
|
|
106
|
+
|
|
107
|
+
```yaml
|
|
108
|
+
repos:
|
|
109
|
+
- repo: https://github.com/YOUR_USERNAME/env-guard
|
|
110
|
+
rev: v1.0.0
|
|
111
|
+
hooks:
|
|
112
|
+
- id: env-guard
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## What this tool does NOT do
|
|
116
|
+
|
|
117
|
+
- ❌ Sync files
|
|
118
|
+
- ❌ Generate example files
|
|
119
|
+
- ❌ Manage secrets
|
|
120
|
+
- ❌ Framework integrations
|
|
121
|
+
- ❌ Custom rules
|
|
122
|
+
- ❌ Type validation
|
|
123
|
+
|
|
124
|
+
If you need those, use something else.
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
## Security & Privacy
|
|
129
|
+
|
|
130
|
+
- **Runs locally**: Your env vars never leave your machine.
|
|
131
|
+
- **No telemetry**: We don't track you.
|
|
132
|
+
- **No logging**: Values are never printed to stdout (except masked) or logged to files.
|
|
133
|
+
- **No external calls**: No analytics, no updates checks, nothing.
|
|
134
|
+
|
|
135
|
+
MIT
|
package/bin/env-guard
ADDED
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
const commander_1 = require("commander");
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const parse_1 = require("./parse");
|
|
41
|
+
const rules_1 = require("./rules");
|
|
42
|
+
const report_1 = require("./report");
|
|
43
|
+
const program = new commander_1.Command();
|
|
44
|
+
program
|
|
45
|
+
.name('env-guard')
|
|
46
|
+
.description('Catch stupid .env mistakes before deploy.')
|
|
47
|
+
.version('1.0.0')
|
|
48
|
+
.argument('[envFile]', 'Path to .env file', '.env')
|
|
49
|
+
.option('--example <file>', 'Path to .env.example file', '.env.example')
|
|
50
|
+
.option('--strict', 'Treat all empty values as errors')
|
|
51
|
+
.option('--json', 'Output as JSON')
|
|
52
|
+
.option('--ci', 'Output GitHub Actions annotations format')
|
|
53
|
+
.option('--no-example', 'Skip example file comparison')
|
|
54
|
+
.action((envFile, options) => {
|
|
55
|
+
try {
|
|
56
|
+
const envPath = path.resolve(process.cwd(), envFile);
|
|
57
|
+
if (!fs.existsSync(envPath)) {
|
|
58
|
+
console.error(`❌ Error: ${envFile} not found`);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
const envResult = (0, parse_1.parseEnvFile)(envFile);
|
|
62
|
+
if (envResult.errors.length > 0) {
|
|
63
|
+
for (const error of envResult.errors) {
|
|
64
|
+
console.error(`❌ ${error}`);
|
|
65
|
+
}
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
let exampleVars = null;
|
|
69
|
+
if (options.example !== false) {
|
|
70
|
+
const examplePath = path.resolve(process.cwd(), options.example);
|
|
71
|
+
if (fs.existsSync(examplePath)) {
|
|
72
|
+
const exampleResult = (0, parse_1.parseEnvFile)(options.example);
|
|
73
|
+
if (exampleResult.errors.length === 0) {
|
|
74
|
+
exampleVars = exampleResult.vars;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const result = (0, rules_1.validateEnv)(envResult.vars, exampleVars, options.strict);
|
|
79
|
+
if (options.json) {
|
|
80
|
+
(0, report_1.printJson)(result);
|
|
81
|
+
}
|
|
82
|
+
else if (options.ci) {
|
|
83
|
+
(0, report_1.printCi)(result, envFile);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
(0, report_1.printReport)(result);
|
|
87
|
+
}
|
|
88
|
+
if (result.errors.length > 0) {
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
process.exit(0);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
if (error instanceof Error) {
|
|
97
|
+
console.error(`❌ Error: ${error.message}`);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
console.error('❌ An unexpected error occurred');
|
|
101
|
+
}
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
program.parse();
|
package/dist/parse.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface EnvVar {
|
|
2
|
+
key: string;
|
|
3
|
+
value: string;
|
|
4
|
+
line: number;
|
|
5
|
+
}
|
|
6
|
+
export interface ParseResult {
|
|
7
|
+
vars: EnvVar[];
|
|
8
|
+
errors: string[];
|
|
9
|
+
}
|
|
10
|
+
export declare function parseEnvFile(filePath: string): ParseResult;
|
|
11
|
+
export declare function getKeys(vars: EnvVar[]): Set<string>;
|
package/dist/parse.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.parseEnvFile = parseEnvFile;
|
|
37
|
+
exports.getKeys = getKeys;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
function parseEnvFile(filePath) {
|
|
41
|
+
const result = {
|
|
42
|
+
vars: [],
|
|
43
|
+
errors: [],
|
|
44
|
+
};
|
|
45
|
+
const absolutePath = path.resolve(process.cwd(), filePath);
|
|
46
|
+
if (!fs.existsSync(absolutePath)) {
|
|
47
|
+
result.errors.push(`File not found: ${filePath}`);
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
const content = fs.readFileSync(absolutePath, 'utf-8');
|
|
51
|
+
const lines = content.split('\n');
|
|
52
|
+
for (let i = 0; i < lines.length; i++) {
|
|
53
|
+
const line = lines[i].trim();
|
|
54
|
+
const lineNumber = i + 1;
|
|
55
|
+
if (!line || line.startsWith('#')) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
59
|
+
if (!match) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const [, key, value] = match;
|
|
63
|
+
let cleanValue = value;
|
|
64
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
65
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
66
|
+
cleanValue = value.slice(1, -1);
|
|
67
|
+
}
|
|
68
|
+
result.vars.push({
|
|
69
|
+
key,
|
|
70
|
+
value: cleanValue,
|
|
71
|
+
line: lineNumber,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
function getKeys(vars) {
|
|
77
|
+
return new Set(vars.map(v => v.key));
|
|
78
|
+
}
|
package/dist/report.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ValidationResult } from './rules';
|
|
2
|
+
export declare function printReport(result: ValidationResult): void;
|
|
3
|
+
/**
|
|
4
|
+
* GitHub Actions annotation format
|
|
5
|
+
* https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions
|
|
6
|
+
*/
|
|
7
|
+
export declare function printCi(result: ValidationResult, envFile: string): void;
|
|
8
|
+
export declare function printJson(result: ValidationResult): void;
|
package/dist/report.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.printReport = printReport;
|
|
4
|
+
exports.printCi = printCi;
|
|
5
|
+
exports.printJson = printJson;
|
|
6
|
+
const colors = {
|
|
7
|
+
red: '\x1b[31m',
|
|
8
|
+
yellow: '\x1b[33m',
|
|
9
|
+
green: '\x1b[32m',
|
|
10
|
+
cyan: '\x1b[36m',
|
|
11
|
+
dim: '\x1b[2m',
|
|
12
|
+
reset: '\x1b[0m',
|
|
13
|
+
bold: '\x1b[1m',
|
|
14
|
+
};
|
|
15
|
+
function formatIssue(issue, isError) {
|
|
16
|
+
const icon = isError ? '❌' : '⚠️';
|
|
17
|
+
const color = isError ? colors.red : colors.yellow;
|
|
18
|
+
const lineInfo = (issue.line && !issue.hasInlineLineInfo) ? ` ${colors.dim}(line ${issue.line})${colors.reset}` : '';
|
|
19
|
+
return `${icon} ${color}${issue.message}${colors.reset}${lineInfo}`;
|
|
20
|
+
}
|
|
21
|
+
function printReport(result) {
|
|
22
|
+
const { errors, warnings } = result;
|
|
23
|
+
const total = errors.length + warnings.length;
|
|
24
|
+
console.log('');
|
|
25
|
+
console.log(`${colors.bold}🔍 env-guard validation report${colors.reset}`);
|
|
26
|
+
console.log('');
|
|
27
|
+
if (errors.length > 0) {
|
|
28
|
+
console.log(`${colors.red}${colors.bold}Errors (${errors.length})${colors.reset}`);
|
|
29
|
+
for (const error of errors) {
|
|
30
|
+
console.log(` ${formatIssue(error, true)}`);
|
|
31
|
+
}
|
|
32
|
+
console.log('');
|
|
33
|
+
}
|
|
34
|
+
if (warnings.length > 0) {
|
|
35
|
+
console.log(`${colors.yellow}${colors.bold}Warnings (${warnings.length})${colors.reset}`);
|
|
36
|
+
for (const warning of warnings) {
|
|
37
|
+
console.log(` ${formatIssue(warning, false)}`);
|
|
38
|
+
}
|
|
39
|
+
console.log('');
|
|
40
|
+
}
|
|
41
|
+
// Summary
|
|
42
|
+
if (total === 0) {
|
|
43
|
+
console.log(`${colors.green}✅ No issues found${colors.reset}`);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
console.log(`${colors.dim}─────────────────────────────${colors.reset}`);
|
|
47
|
+
console.log(`Total: ${errors.length} error(s), ${warnings.length} warning(s)`);
|
|
48
|
+
}
|
|
49
|
+
// One-line summary for logs
|
|
50
|
+
console.log('');
|
|
51
|
+
console.log(`env-guard: ${errors.length} errors, ${warnings.length} warnings`);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* GitHub Actions annotation format
|
|
55
|
+
* https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions
|
|
56
|
+
*/
|
|
57
|
+
function printCi(result, envFile) {
|
|
58
|
+
const { errors, warnings } = result;
|
|
59
|
+
// Output GitHub Actions annotations
|
|
60
|
+
for (const error of errors) {
|
|
61
|
+
const line = error.line ? `,line=${error.line}` : '';
|
|
62
|
+
console.log(`::error file=${envFile}${line}::${error.message}`);
|
|
63
|
+
}
|
|
64
|
+
for (const warning of warnings) {
|
|
65
|
+
const line = warning.line ? `,line=${warning.line}` : '';
|
|
66
|
+
console.log(`::warning file=${envFile}${line}::${warning.message}`);
|
|
67
|
+
}
|
|
68
|
+
// Summary line
|
|
69
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
70
|
+
console.log('env-guard: ✅ No issues found');
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
console.log(`env-guard: ${errors.length} errors, ${warnings.length} warnings`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function printJson(result) {
|
|
77
|
+
console.log(JSON.stringify(result, null, 2));
|
|
78
|
+
}
|
package/dist/rules.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { EnvVar } from './parse';
|
|
2
|
+
export interface ValidationResult {
|
|
3
|
+
errors: ValidationIssue[];
|
|
4
|
+
warnings: ValidationIssue[];
|
|
5
|
+
}
|
|
6
|
+
export interface ValidationIssue {
|
|
7
|
+
type: string;
|
|
8
|
+
key: string;
|
|
9
|
+
message: string;
|
|
10
|
+
line?: number;
|
|
11
|
+
hasInlineLineInfo?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function isSensitiveKey(key: string): boolean;
|
|
14
|
+
export declare function maskValue(key: string, value: string): string;
|
|
15
|
+
export declare function validateEnv(envVars: EnvVar[], exampleVars: EnvVar[] | null, strict?: boolean): ValidationResult;
|
package/dist/rules.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isSensitiveKey = isSensitiveKey;
|
|
4
|
+
exports.maskValue = maskValue;
|
|
5
|
+
exports.validateEnv = validateEnv;
|
|
6
|
+
// Keys that should NEVER be empty in production
|
|
7
|
+
const REQUIRED_PATTERNS = [
|
|
8
|
+
/^DATABASE/i,
|
|
9
|
+
/^DB_/i,
|
|
10
|
+
/^API_KEY/i,
|
|
11
|
+
/^SECRET/i,
|
|
12
|
+
/^PASSWORD/i,
|
|
13
|
+
];
|
|
14
|
+
// Keys that indicate unsafe defaults
|
|
15
|
+
const UNSAFE_DEFAULTS = {
|
|
16
|
+
'DEBUG': ['true', '1', 'yes'],
|
|
17
|
+
'APP_DEBUG': ['true', '1', 'yes'],
|
|
18
|
+
'NODE_ENV': ['development', 'dev'],
|
|
19
|
+
'LOG_LEVEL': ['debug', 'trace'],
|
|
20
|
+
};
|
|
21
|
+
// Sensitive keys that should be masked in output
|
|
22
|
+
const SENSITIVE_PATTERNS = [
|
|
23
|
+
/SECRET/i,
|
|
24
|
+
/PASSWORD/i,
|
|
25
|
+
/TOKEN/i,
|
|
26
|
+
/KEY/i,
|
|
27
|
+
/CREDENTIAL/i,
|
|
28
|
+
/AUTH/i,
|
|
29
|
+
/PRIVATE/i,
|
|
30
|
+
];
|
|
31
|
+
function isSensitiveKey(key) {
|
|
32
|
+
return SENSITIVE_PATTERNS.some(pattern => pattern.test(key));
|
|
33
|
+
}
|
|
34
|
+
function maskValue(key, value) {
|
|
35
|
+
if (isSensitiveKey(key)) {
|
|
36
|
+
if (value.length <= 4) {
|
|
37
|
+
return '****';
|
|
38
|
+
}
|
|
39
|
+
return value.substring(0, 2) + '****' + value.substring(value.length - 2);
|
|
40
|
+
}
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
function validateEnv(envVars, exampleVars, strict = false) {
|
|
44
|
+
const result = {
|
|
45
|
+
errors: [],
|
|
46
|
+
warnings: [],
|
|
47
|
+
};
|
|
48
|
+
const envKeys = new Set(envVars.map(v => v.key));
|
|
49
|
+
const exampleKeys = exampleVars ? new Set(exampleVars.map(v => v.key)) : null;
|
|
50
|
+
// Check for missing keys (in example but not in env)
|
|
51
|
+
if (exampleKeys) {
|
|
52
|
+
for (const exampleVar of exampleVars) {
|
|
53
|
+
if (!envKeys.has(exampleVar.key)) {
|
|
54
|
+
result.errors.push({
|
|
55
|
+
type: 'missing',
|
|
56
|
+
key: exampleVar.key,
|
|
57
|
+
message: `Missing variable: ${exampleVar.key} (defined in example)`,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Check for duplicates
|
|
63
|
+
const seen = new Map();
|
|
64
|
+
for (const envVar of envVars) {
|
|
65
|
+
if (seen.has(envVar.key)) {
|
|
66
|
+
result.errors.push({
|
|
67
|
+
type: 'duplicate',
|
|
68
|
+
key: envVar.key,
|
|
69
|
+
message: `Duplicate key: ${envVar.key} (lines ${seen.get(envVar.key)} and ${envVar.line})`,
|
|
70
|
+
line: envVar.line,
|
|
71
|
+
hasInlineLineInfo: true,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
seen.set(envVar.key, envVar.line);
|
|
75
|
+
}
|
|
76
|
+
// Check for empty values
|
|
77
|
+
for (const envVar of envVars) {
|
|
78
|
+
if (envVar.value === '' || envVar.value.trim() === '') {
|
|
79
|
+
const isRequired = REQUIRED_PATTERNS.some(p => p.test(envVar.key));
|
|
80
|
+
if (isRequired || strict) {
|
|
81
|
+
result.errors.push({
|
|
82
|
+
type: 'empty',
|
|
83
|
+
key: envVar.key,
|
|
84
|
+
message: `Empty value: ${envVar.key}`,
|
|
85
|
+
line: envVar.line,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
result.warnings.push({
|
|
90
|
+
type: 'empty',
|
|
91
|
+
key: envVar.key,
|
|
92
|
+
message: `Empty value: ${envVar.key}`,
|
|
93
|
+
line: envVar.line,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Check for unused keys (in env but not in example)
|
|
99
|
+
if (exampleKeys) {
|
|
100
|
+
for (const envVar of envVars) {
|
|
101
|
+
if (!exampleKeys.has(envVar.key)) {
|
|
102
|
+
result.warnings.push({
|
|
103
|
+
type: 'unused',
|
|
104
|
+
key: envVar.key,
|
|
105
|
+
message: `Unused variable: ${envVar.key} (not in example)`,
|
|
106
|
+
line: envVar.line,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Check for unsafe defaults
|
|
112
|
+
for (const envVar of envVars) {
|
|
113
|
+
const unsafeValues = UNSAFE_DEFAULTS[envVar.key];
|
|
114
|
+
if (unsafeValues && unsafeValues.includes(envVar.value.toLowerCase())) {
|
|
115
|
+
result.warnings.push({
|
|
116
|
+
type: 'unsafe',
|
|
117
|
+
key: envVar.key,
|
|
118
|
+
message: `Unsafe default: ${envVar.key}=${envVar.value} (may not be suitable for production)`,
|
|
119
|
+
line: envVar.line,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return result;
|
|
124
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gokul-kannur/env-guard",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Catch stupid .env mistakes before deploy. No sync. No presets. Just validation.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"env-guard": "./bin/env-guard"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"dev": "ts-node src/index.ts",
|
|
12
|
+
"prepublishOnly": "npm run build"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"env",
|
|
16
|
+
"dotenv",
|
|
17
|
+
"validator",
|
|
18
|
+
"cli",
|
|
19
|
+
"developer-tools",
|
|
20
|
+
"ci"
|
|
21
|
+
],
|
|
22
|
+
"author": "",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/GokulKannur/env-guard.git"
|
|
27
|
+
},
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/GokulKannur/env-guard/issues"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/GokulKannur/env-guard#readme",
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^20.10.0",
|
|
34
|
+
"typescript": "^5.3.0",
|
|
35
|
+
"ts-node": "^10.9.0"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"commander": "^11.1.0"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18.0.0"
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import { parseEnvFile } from './parse';
|
|
7
|
+
import { validateEnv } from './rules';
|
|
8
|
+
import { printReport, printJson, printCi } from './report';
|
|
9
|
+
|
|
10
|
+
const program = new Command();
|
|
11
|
+
|
|
12
|
+
program
|
|
13
|
+
.name('env-guard')
|
|
14
|
+
.description('Catch stupid .env mistakes before deploy.')
|
|
15
|
+
.version('1.0.0')
|
|
16
|
+
.argument('[envFile]', 'Path to .env file', '.env')
|
|
17
|
+
.option('--example <file>', 'Path to .env.example file', '.env.example')
|
|
18
|
+
.option('--strict', 'Treat all empty values as errors')
|
|
19
|
+
.option('--json', 'Output as JSON')
|
|
20
|
+
.option('--ci', 'Output GitHub Actions annotations format')
|
|
21
|
+
.option('--no-example', 'Skip example file comparison')
|
|
22
|
+
.action((envFile, options) => {
|
|
23
|
+
try {
|
|
24
|
+
const envPath = path.resolve(process.cwd(), envFile);
|
|
25
|
+
|
|
26
|
+
if (!fs.existsSync(envPath)) {
|
|
27
|
+
console.error(`❌ Error: ${envFile} not found`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const envResult = parseEnvFile(envFile);
|
|
32
|
+
if (envResult.errors.length > 0) {
|
|
33
|
+
for (const error of envResult.errors) {
|
|
34
|
+
console.error(`❌ ${error}`);
|
|
35
|
+
}
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let exampleVars = null;
|
|
40
|
+
if (options.example !== false) {
|
|
41
|
+
const examplePath = path.resolve(process.cwd(), options.example);
|
|
42
|
+
if (fs.existsSync(examplePath)) {
|
|
43
|
+
const exampleResult = parseEnvFile(options.example);
|
|
44
|
+
if (exampleResult.errors.length === 0) {
|
|
45
|
+
exampleVars = exampleResult.vars;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const result = validateEnv(envResult.vars, exampleVars, options.strict);
|
|
51
|
+
|
|
52
|
+
if (options.json) {
|
|
53
|
+
printJson(result);
|
|
54
|
+
} else if (options.ci) {
|
|
55
|
+
printCi(result, envFile);
|
|
56
|
+
} else {
|
|
57
|
+
printReport(result);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (result.errors.length > 0) {
|
|
61
|
+
process.exit(1);
|
|
62
|
+
} else {
|
|
63
|
+
process.exit(0);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (error instanceof Error) {
|
|
68
|
+
console.error(`❌ Error: ${error.message}`);
|
|
69
|
+
} else {
|
|
70
|
+
console.error('❌ An unexpected error occurred');
|
|
71
|
+
}
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
program.parse();
|
package/src/parse.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
export interface EnvVar {
|
|
5
|
+
key: string;
|
|
6
|
+
value: string;
|
|
7
|
+
line: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ParseResult {
|
|
11
|
+
vars: EnvVar[];
|
|
12
|
+
errors: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function parseEnvFile(filePath: string): ParseResult {
|
|
16
|
+
const result: ParseResult = {
|
|
17
|
+
vars: [],
|
|
18
|
+
errors: [],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const absolutePath = path.resolve(process.cwd(), filePath);
|
|
22
|
+
|
|
23
|
+
if (!fs.existsSync(absolutePath)) {
|
|
24
|
+
result.errors.push(`File not found: ${filePath}`);
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const content = fs.readFileSync(absolutePath, 'utf-8');
|
|
29
|
+
const lines = content.split('\n');
|
|
30
|
+
|
|
31
|
+
for (let i = 0; i < lines.length; i++) {
|
|
32
|
+
const line = lines[i].trim();
|
|
33
|
+
const lineNumber = i + 1;
|
|
34
|
+
|
|
35
|
+
if (!line || line.startsWith('#')) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
40
|
+
|
|
41
|
+
if (!match) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const [, key, value] = match;
|
|
46
|
+
|
|
47
|
+
let cleanValue = value;
|
|
48
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
49
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
50
|
+
cleanValue = value.slice(1, -1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
result.vars.push({
|
|
54
|
+
key,
|
|
55
|
+
value: cleanValue,
|
|
56
|
+
line: lineNumber,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function getKeys(vars: EnvVar[]): Set<string> {
|
|
64
|
+
return new Set(vars.map(v => v.key));
|
|
65
|
+
}
|
package/src/report.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { ValidationResult, ValidationIssue } from './rules';
|
|
2
|
+
|
|
3
|
+
const colors = {
|
|
4
|
+
red: '\x1b[31m',
|
|
5
|
+
yellow: '\x1b[33m',
|
|
6
|
+
green: '\x1b[32m',
|
|
7
|
+
cyan: '\x1b[36m',
|
|
8
|
+
dim: '\x1b[2m',
|
|
9
|
+
reset: '\x1b[0m',
|
|
10
|
+
bold: '\x1b[1m',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function formatIssue(issue: ValidationIssue, isError: boolean): string {
|
|
14
|
+
const icon = isError ? '❌' : '⚠️';
|
|
15
|
+
const color = isError ? colors.red : colors.yellow;
|
|
16
|
+
const lineInfo = (issue.line && !issue.hasInlineLineInfo) ? ` ${colors.dim}(line ${issue.line})${colors.reset}` : '';
|
|
17
|
+
|
|
18
|
+
return `${icon} ${color}${issue.message}${colors.reset}${lineInfo}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function printReport(result: ValidationResult): void {
|
|
22
|
+
const { errors, warnings } = result;
|
|
23
|
+
const total = errors.length + warnings.length;
|
|
24
|
+
|
|
25
|
+
console.log('');
|
|
26
|
+
console.log(`${colors.bold}🔍 env-guard validation report${colors.reset}`);
|
|
27
|
+
console.log('');
|
|
28
|
+
|
|
29
|
+
if (errors.length > 0) {
|
|
30
|
+
console.log(`${colors.red}${colors.bold}Errors (${errors.length})${colors.reset}`);
|
|
31
|
+
for (const error of errors) {
|
|
32
|
+
console.log(` ${formatIssue(error, true)}`);
|
|
33
|
+
}
|
|
34
|
+
console.log('');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (warnings.length > 0) {
|
|
38
|
+
console.log(`${colors.yellow}${colors.bold}Warnings (${warnings.length})${colors.reset}`);
|
|
39
|
+
for (const warning of warnings) {
|
|
40
|
+
console.log(` ${formatIssue(warning, false)}`);
|
|
41
|
+
}
|
|
42
|
+
console.log('');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Summary
|
|
46
|
+
if (total === 0) {
|
|
47
|
+
console.log(`${colors.green}✅ No issues found${colors.reset}`);
|
|
48
|
+
} else {
|
|
49
|
+
console.log(`${colors.dim}─────────────────────────────${colors.reset}`);
|
|
50
|
+
console.log(`Total: ${errors.length} error(s), ${warnings.length} warning(s)`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// One-line summary for logs
|
|
54
|
+
console.log('');
|
|
55
|
+
console.log(`env-guard: ${errors.length} errors, ${warnings.length} warnings`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* GitHub Actions annotation format
|
|
60
|
+
* https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions
|
|
61
|
+
*/
|
|
62
|
+
export function printCi(result: ValidationResult, envFile: string): void {
|
|
63
|
+
const { errors, warnings } = result;
|
|
64
|
+
|
|
65
|
+
// Output GitHub Actions annotations
|
|
66
|
+
for (const error of errors) {
|
|
67
|
+
const line = error.line ? `,line=${error.line}` : '';
|
|
68
|
+
console.log(`::error file=${envFile}${line}::${error.message}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (const warning of warnings) {
|
|
72
|
+
const line = warning.line ? `,line=${warning.line}` : '';
|
|
73
|
+
console.log(`::warning file=${envFile}${line}::${warning.message}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Summary line
|
|
77
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
78
|
+
console.log('env-guard: ✅ No issues found');
|
|
79
|
+
} else {
|
|
80
|
+
console.log(`env-guard: ${errors.length} errors, ${warnings.length} warnings`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function printJson(result: ValidationResult): void {
|
|
85
|
+
console.log(JSON.stringify(result, null, 2));
|
|
86
|
+
}
|
package/src/rules.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { EnvVar } from './parse';
|
|
2
|
+
|
|
3
|
+
export interface ValidationResult {
|
|
4
|
+
errors: ValidationIssue[];
|
|
5
|
+
warnings: ValidationIssue[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ValidationIssue {
|
|
9
|
+
type: string;
|
|
10
|
+
key: string;
|
|
11
|
+
message: string;
|
|
12
|
+
line?: number;
|
|
13
|
+
hasInlineLineInfo?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Keys that should NEVER be empty in production
|
|
17
|
+
const REQUIRED_PATTERNS = [
|
|
18
|
+
/^DATABASE/i,
|
|
19
|
+
/^DB_/i,
|
|
20
|
+
/^API_KEY/i,
|
|
21
|
+
/^SECRET/i,
|
|
22
|
+
/^PASSWORD/i,
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
// Keys that indicate unsafe defaults
|
|
26
|
+
const UNSAFE_DEFAULTS: Record<string, string[]> = {
|
|
27
|
+
'DEBUG': ['true', '1', 'yes'],
|
|
28
|
+
'APP_DEBUG': ['true', '1', 'yes'],
|
|
29
|
+
'NODE_ENV': ['development', 'dev'],
|
|
30
|
+
'LOG_LEVEL': ['debug', 'trace'],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Sensitive keys that should be masked in output
|
|
34
|
+
const SENSITIVE_PATTERNS = [
|
|
35
|
+
/SECRET/i,
|
|
36
|
+
/PASSWORD/i,
|
|
37
|
+
/TOKEN/i,
|
|
38
|
+
/KEY/i,
|
|
39
|
+
/CREDENTIAL/i,
|
|
40
|
+
/AUTH/i,
|
|
41
|
+
/PRIVATE/i,
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
export function isSensitiveKey(key: string): boolean {
|
|
45
|
+
return SENSITIVE_PATTERNS.some(pattern => pattern.test(key));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function maskValue(key: string, value: string): string {
|
|
49
|
+
if (isSensitiveKey(key)) {
|
|
50
|
+
if (value.length <= 4) {
|
|
51
|
+
return '****';
|
|
52
|
+
}
|
|
53
|
+
return value.substring(0, 2) + '****' + value.substring(value.length - 2);
|
|
54
|
+
}
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function validateEnv(
|
|
59
|
+
envVars: EnvVar[],
|
|
60
|
+
exampleVars: EnvVar[] | null,
|
|
61
|
+
strict: boolean = false
|
|
62
|
+
): ValidationResult {
|
|
63
|
+
const result: ValidationResult = {
|
|
64
|
+
errors: [],
|
|
65
|
+
warnings: [],
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const envKeys = new Set(envVars.map(v => v.key));
|
|
69
|
+
const exampleKeys = exampleVars ? new Set(exampleVars.map(v => v.key)) : null;
|
|
70
|
+
|
|
71
|
+
// Check for missing keys (in example but not in env)
|
|
72
|
+
if (exampleKeys) {
|
|
73
|
+
for (const exampleVar of exampleVars!) {
|
|
74
|
+
if (!envKeys.has(exampleVar.key)) {
|
|
75
|
+
result.errors.push({
|
|
76
|
+
type: 'missing',
|
|
77
|
+
key: exampleVar.key,
|
|
78
|
+
message: `Missing variable: ${exampleVar.key} (defined in example)`,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Check for duplicates
|
|
85
|
+
const seen = new Map<string, number>();
|
|
86
|
+
for (const envVar of envVars) {
|
|
87
|
+
if (seen.has(envVar.key)) {
|
|
88
|
+
result.errors.push({
|
|
89
|
+
type: 'duplicate',
|
|
90
|
+
key: envVar.key,
|
|
91
|
+
message: `Duplicate key: ${envVar.key} (lines ${seen.get(envVar.key)} and ${envVar.line})`,
|
|
92
|
+
line: envVar.line,
|
|
93
|
+
hasInlineLineInfo: true,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
seen.set(envVar.key, envVar.line);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Check for empty values
|
|
100
|
+
for (const envVar of envVars) {
|
|
101
|
+
if (envVar.value === '' || envVar.value.trim() === '') {
|
|
102
|
+
const isRequired = REQUIRED_PATTERNS.some(p => p.test(envVar.key));
|
|
103
|
+
|
|
104
|
+
if (isRequired || strict) {
|
|
105
|
+
result.errors.push({
|
|
106
|
+
type: 'empty',
|
|
107
|
+
key: envVar.key,
|
|
108
|
+
message: `Empty value: ${envVar.key}`,
|
|
109
|
+
line: envVar.line,
|
|
110
|
+
});
|
|
111
|
+
} else {
|
|
112
|
+
result.warnings.push({
|
|
113
|
+
type: 'empty',
|
|
114
|
+
key: envVar.key,
|
|
115
|
+
message: `Empty value: ${envVar.key}`,
|
|
116
|
+
line: envVar.line,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Check for unused keys (in env but not in example)
|
|
123
|
+
if (exampleKeys) {
|
|
124
|
+
for (const envVar of envVars) {
|
|
125
|
+
if (!exampleKeys.has(envVar.key)) {
|
|
126
|
+
result.warnings.push({
|
|
127
|
+
type: 'unused',
|
|
128
|
+
key: envVar.key,
|
|
129
|
+
message: `Unused variable: ${envVar.key} (not in example)`,
|
|
130
|
+
line: envVar.line,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Check for unsafe defaults
|
|
137
|
+
for (const envVar of envVars) {
|
|
138
|
+
const unsafeValues = UNSAFE_DEFAULTS[envVar.key];
|
|
139
|
+
if (unsafeValues && unsafeValues.includes(envVar.value.toLowerCase())) {
|
|
140
|
+
result.warnings.push({
|
|
141
|
+
type: 'unsafe',
|
|
142
|
+
key: envVar.key,
|
|
143
|
+
message: `Unsafe default: ${envVar.key}=${envVar.value} (may not be suitable for production)`,
|
|
144
|
+
line: envVar.line,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return result;
|
|
150
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": [
|
|
6
|
+
"ES2022"
|
|
7
|
+
],
|
|
8
|
+
"outDir": "./dist",
|
|
9
|
+
"rootDir": "./src",
|
|
10
|
+
"strict": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"forceConsistentCasingInFileNames": true,
|
|
14
|
+
"resolveJsonModule": true,
|
|
15
|
+
"declaration": true
|
|
16
|
+
},
|
|
17
|
+
"include": [
|
|
18
|
+
"src/**/*"
|
|
19
|
+
],
|
|
20
|
+
"exclude": [
|
|
21
|
+
"node_modules",
|
|
22
|
+
"dist"
|
|
23
|
+
]
|
|
24
|
+
}
|