@claude-agent/envcheck 1.2.0 → 1.4.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 +109 -3
- package/package.json +4 -2
- package/src/index.js +285 -8
package/README.md
CHANGED
|
@@ -233,6 +233,111 @@ curl -o .git/hooks/pre-commit https://raw.githubusercontent.com/claude-agent-too
|
|
|
233
233
|
chmod +x .git/hooks/pre-commit
|
|
234
234
|
```
|
|
235
235
|
|
|
236
|
+
## Type Validation
|
|
237
|
+
|
|
238
|
+
envcheck supports **static type validation** - validate variable formats without running your app.
|
|
239
|
+
|
|
240
|
+
### Using Type Hints in .env.example
|
|
241
|
+
|
|
242
|
+
Add type hints as comments above variables:
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
# type: url
|
|
246
|
+
DATABASE_URL=postgres://localhost/mydb
|
|
247
|
+
|
|
248
|
+
# @type port
|
|
249
|
+
PORT=3000
|
|
250
|
+
|
|
251
|
+
# type: boolean
|
|
252
|
+
DEBUG=false
|
|
253
|
+
|
|
254
|
+
# type: email
|
|
255
|
+
ADMIN_EMAIL=admin@example.com
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Supported Types
|
|
259
|
+
|
|
260
|
+
| Type | Description | Examples |
|
|
261
|
+
|------|-------------|----------|
|
|
262
|
+
| `url` | Valid URL | `https://example.com`, `postgres://host/db` |
|
|
263
|
+
| `port` | Port number (1-65535) | `3000`, `8080` |
|
|
264
|
+
| `boolean`/`bool` | Boolean values | `true`, `false`, `1`, `0`, `yes`, `no` |
|
|
265
|
+
| `email` | Email address | `user@example.com` |
|
|
266
|
+
| `number` | Any number | `42`, `3.14`, `-10` |
|
|
267
|
+
| `integer`/`int` | Whole numbers | `42`, `-10` |
|
|
268
|
+
| `json` | Valid JSON | `{"key":"value"}`, `[1,2,3]` |
|
|
269
|
+
| `uuid` | UUID format | `550e8400-e29b-41d4-a716-446655440000` |
|
|
270
|
+
| `string`/`str` | Any string (no validation) | anything |
|
|
271
|
+
|
|
272
|
+
## Secret Detection
|
|
273
|
+
|
|
274
|
+
envcheck can warn you if your `.env` file contains values that look like real secrets:
|
|
275
|
+
|
|
276
|
+
```javascript
|
|
277
|
+
const result = check('.env', {
|
|
278
|
+
examplePath: '.env.example',
|
|
279
|
+
detectSecrets: true // Warns about potential secrets
|
|
280
|
+
});
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Detected Secret Patterns
|
|
284
|
+
|
|
285
|
+
- AWS Access Keys (`AKIA...`)
|
|
286
|
+
- GitHub Tokens (`ghp_...`, `gho_...`, etc.)
|
|
287
|
+
- Stripe Keys (`sk_live_...`, `rk_live_...`)
|
|
288
|
+
- Private Keys (`-----BEGIN PRIVATE KEY-----`)
|
|
289
|
+
- Slack Tokens (`xox...`)
|
|
290
|
+
- Twilio Credentials
|
|
291
|
+
- SendGrid API Keys
|
|
292
|
+
- Google API Keys
|
|
293
|
+
- High-entropy hex strings (with sensitive key names)
|
|
294
|
+
|
|
295
|
+
### Placeholder Detection
|
|
296
|
+
|
|
297
|
+
envcheck won't warn about obvious placeholders like:
|
|
298
|
+
- `your-api-key`, `my-secret`
|
|
299
|
+
- `changeme`, `placeholder`
|
|
300
|
+
- `xxx`, `...`
|
|
301
|
+
- `example`, `test`, `dummy`
|
|
302
|
+
|
|
303
|
+
### API Usage with Secrets
|
|
304
|
+
|
|
305
|
+
```javascript
|
|
306
|
+
const { check, validate, detectSecret } = require('@claude-agent/envcheck');
|
|
307
|
+
|
|
308
|
+
// Enable secret detection
|
|
309
|
+
const result = check('.env', {
|
|
310
|
+
examplePath: '.env.example',
|
|
311
|
+
detectSecrets: true // Warns about potential secrets
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// Check a single value
|
|
315
|
+
const secret = detectSecret('API_KEY', 'sk_live_abc123...');
|
|
316
|
+
if (secret) {
|
|
317
|
+
console.log(`Warning: ${secret.description} detected`);
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### API Usage with Types
|
|
322
|
+
|
|
323
|
+
```javascript
|
|
324
|
+
const { check, validate } = require('@claude-agent/envcheck');
|
|
325
|
+
|
|
326
|
+
// Explicit type validation
|
|
327
|
+
const result = validate('.env', {
|
|
328
|
+
types: {
|
|
329
|
+
DATABASE_URL: 'url',
|
|
330
|
+
PORT: 'port',
|
|
331
|
+
DEBUG: 'boolean'
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// Types from example file are used automatically
|
|
336
|
+
const result2 = check('.env', {
|
|
337
|
+
examplePath: '.env.example' // Type hints in example are used
|
|
338
|
+
});
|
|
339
|
+
```
|
|
340
|
+
|
|
236
341
|
## .env File Format
|
|
237
342
|
|
|
238
343
|
Supports standard .env syntax:
|
|
@@ -252,7 +357,7 @@ WITH_EQUALS=postgres://user:pass@host/db?opt=val
|
|
|
252
357
|
- **Auto-detection** - Finds .env.example automatically
|
|
253
358
|
- **CI-friendly** - Exit codes and JSON output
|
|
254
359
|
- **Comprehensive** - Parse, validate, compare, generate
|
|
255
|
-
- **Well-tested** -
|
|
360
|
+
- **Well-tested** - 72 tests covering edge cases
|
|
256
361
|
|
|
257
362
|
## vs. dotenv-safe / envalid
|
|
258
363
|
|
|
@@ -263,10 +368,11 @@ WITH_EQUALS=postgres://user:pass@host/db?opt=val
|
|
|
263
368
|
| **Static validation** | ✅ | ❌ | ❌ |
|
|
264
369
|
| **CI/CD integration** | ✅ GitHub Action | ❌ | ❌ |
|
|
265
370
|
| **Pre-commit hook** | ✅ | ❌ | ❌ |
|
|
266
|
-
| Type validation |
|
|
371
|
+
| Type validation | ✅ (static) | ❌ | ✅ (runtime) |
|
|
372
|
+
| **Secret detection** | ✅ | ❌ | ❌ |
|
|
267
373
|
| Zero dependencies | ✅ | ❌ | ❌ |
|
|
268
374
|
|
|
269
|
-
**Key difference:** envcheck validates *before* deployment (shift-left), while dotenv-safe and envalid validate at runtime when your app starts. Catch missing env vars in CI, not in production.
|
|
375
|
+
**Key difference:** envcheck validates *before* deployment (shift-left), while dotenv-safe and envalid validate at runtime when your app starts. Catch missing env vars and type errors in CI, not in production.
|
|
270
376
|
|
|
271
377
|
## License
|
|
272
378
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-agent/envcheck",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Validate .env files, compare with .env.example, find missing or empty variables",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
"github-action",
|
|
21
21
|
"ci-cd",
|
|
22
22
|
"pre-commit",
|
|
23
|
-
"git-hooks"
|
|
23
|
+
"git-hooks",
|
|
24
|
+
"secrets",
|
|
25
|
+
"security"
|
|
24
26
|
],
|
|
25
27
|
"author": "Claude Agent <claude-agent@agentmail.to>",
|
|
26
28
|
"license": "MIT",
|
package/src/index.js
CHANGED
|
@@ -3,6 +3,192 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Type validators for environment variables
|
|
8
|
+
*/
|
|
9
|
+
const typeValidators = {
|
|
10
|
+
url: (value) => {
|
|
11
|
+
if (!value) return { valid: true };
|
|
12
|
+
try {
|
|
13
|
+
new URL(value);
|
|
14
|
+
return { valid: true };
|
|
15
|
+
} catch {
|
|
16
|
+
return { valid: false, message: 'must be a valid URL' };
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
port: (value) => {
|
|
21
|
+
if (!value) return { valid: true };
|
|
22
|
+
const num = parseInt(value, 10);
|
|
23
|
+
if (isNaN(num) || num < 1 || num > 65535 || String(num) !== value) {
|
|
24
|
+
return { valid: false, message: 'must be a valid port number (1-65535)' };
|
|
25
|
+
}
|
|
26
|
+
return { valid: true };
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
boolean: (value) => {
|
|
30
|
+
if (!value) return { valid: true };
|
|
31
|
+
const lower = value.toLowerCase();
|
|
32
|
+
const valid = ['true', 'false', '1', '0', 'yes', 'no', 'on', 'off'].includes(lower);
|
|
33
|
+
if (!valid) {
|
|
34
|
+
return { valid: false, message: 'must be a boolean (true/false/1/0/yes/no)' };
|
|
35
|
+
}
|
|
36
|
+
return { valid: true };
|
|
37
|
+
},
|
|
38
|
+
bool: (value) => typeValidators.boolean(value),
|
|
39
|
+
|
|
40
|
+
email: (value) => {
|
|
41
|
+
if (!value) return { valid: true };
|
|
42
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
43
|
+
if (!emailRegex.test(value)) {
|
|
44
|
+
return { valid: false, message: 'must be a valid email address' };
|
|
45
|
+
}
|
|
46
|
+
return { valid: true };
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
number: (value) => {
|
|
50
|
+
if (!value) return { valid: true };
|
|
51
|
+
if (isNaN(parseFloat(value))) {
|
|
52
|
+
return { valid: false, message: 'must be a number' };
|
|
53
|
+
}
|
|
54
|
+
return { valid: true };
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
integer: (value) => {
|
|
58
|
+
if (!value) return { valid: true };
|
|
59
|
+
const num = parseInt(value, 10);
|
|
60
|
+
if (isNaN(num) || String(num) !== value) {
|
|
61
|
+
return { valid: false, message: 'must be an integer' };
|
|
62
|
+
}
|
|
63
|
+
return { valid: true };
|
|
64
|
+
},
|
|
65
|
+
int: (value) => typeValidators.integer(value),
|
|
66
|
+
|
|
67
|
+
string: () => ({ valid: true }),
|
|
68
|
+
str: () => ({ valid: true }),
|
|
69
|
+
|
|
70
|
+
json: (value) => {
|
|
71
|
+
if (!value) return { valid: true };
|
|
72
|
+
try {
|
|
73
|
+
JSON.parse(value);
|
|
74
|
+
return { valid: true };
|
|
75
|
+
} catch {
|
|
76
|
+
return { valid: false, message: 'must be valid JSON' };
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
uuid: (value) => {
|
|
81
|
+
if (!value) return { valid: true };
|
|
82
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
83
|
+
if (!uuidRegex.test(value)) {
|
|
84
|
+
return { valid: false, message: 'must be a valid UUID' };
|
|
85
|
+
}
|
|
86
|
+
return { valid: true };
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Secret detection patterns
|
|
92
|
+
* Each pattern has: regex, description, and optional keyPattern (for key-specific checks)
|
|
93
|
+
*/
|
|
94
|
+
const secretPatterns = [
|
|
95
|
+
// AWS
|
|
96
|
+
{ regex: /^AKIA[0-9A-Z]{16}$/, description: 'AWS Access Key ID' },
|
|
97
|
+
{ regex: /^[A-Za-z0-9/+=]{40}$/, description: 'AWS Secret Access Key', keyPattern: /aws.*secret|secret.*key/i },
|
|
98
|
+
|
|
99
|
+
// Private keys
|
|
100
|
+
{ regex: /-----BEGIN (?:RSA |DSA |EC |OPENSSH |PGP )?PRIVATE KEY-----/, description: 'Private key' },
|
|
101
|
+
{ regex: /-----BEGIN CERTIFICATE-----/, description: 'Certificate' },
|
|
102
|
+
|
|
103
|
+
// GitHub
|
|
104
|
+
{ regex: /^ghp_[a-zA-Z0-9]{36}$/, description: 'GitHub Personal Access Token' },
|
|
105
|
+
{ regex: /^gho_[a-zA-Z0-9]{36}$/, description: 'GitHub OAuth Access Token' },
|
|
106
|
+
{ regex: /^ghu_[a-zA-Z0-9]{36}$/, description: 'GitHub User-to-Server Token' },
|
|
107
|
+
{ regex: /^ghs_[a-zA-Z0-9]{36}$/, description: 'GitHub Server-to-Server Token' },
|
|
108
|
+
{ regex: /^ghr_[a-zA-Z0-9]{36}$/, description: 'GitHub Refresh Token' },
|
|
109
|
+
|
|
110
|
+
// Slack
|
|
111
|
+
{ regex: /^xox[baprs]-[0-9]{10,}-[0-9]{10,}-[a-zA-Z0-9]{24}$/, description: 'Slack Token' },
|
|
112
|
+
|
|
113
|
+
// Stripe
|
|
114
|
+
{ regex: /^sk_live_[a-zA-Z0-9]{24,}$/, description: 'Stripe Live Secret Key' },
|
|
115
|
+
{ regex: /^rk_live_[a-zA-Z0-9]{24,}$/, description: 'Stripe Live Restricted Key' },
|
|
116
|
+
|
|
117
|
+
// Twilio
|
|
118
|
+
{ regex: /^AC[a-f0-9]{32}$/, description: 'Twilio Account SID' },
|
|
119
|
+
{ regex: /^SK[a-f0-9]{32}$/, description: 'Twilio API Key' },
|
|
120
|
+
|
|
121
|
+
// SendGrid
|
|
122
|
+
{ regex: /^SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}$/, description: 'SendGrid API Key' },
|
|
123
|
+
|
|
124
|
+
// Google
|
|
125
|
+
{ regex: /^AIza[0-9A-Za-z_-]{35}$/, description: 'Google API Key' },
|
|
126
|
+
|
|
127
|
+
// Generic high-entropy (likely real secrets)
|
|
128
|
+
{ regex: /^[a-f0-9]{32}$/, description: 'Hex string (32 chars)', keyPattern: /api.*key|secret|token|password/i },
|
|
129
|
+
{ regex: /^[a-f0-9]{64}$/, description: 'Hex string (64 chars)', keyPattern: /api.*key|secret|token|password/i },
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Placeholder patterns that are NOT secrets
|
|
134
|
+
*/
|
|
135
|
+
const placeholderPatterns = [
|
|
136
|
+
/^your[-_]?/i,
|
|
137
|
+
/^my[-_]?/i,
|
|
138
|
+
/^xxx+$/i,
|
|
139
|
+
/^placeholder/i,
|
|
140
|
+
/^example/i,
|
|
141
|
+
/^test[-_]?/i,
|
|
142
|
+
/^dummy/i,
|
|
143
|
+
/^fake/i,
|
|
144
|
+
/^sample/i,
|
|
145
|
+
/^changeme/i,
|
|
146
|
+
/^replace[-_]?/i,
|
|
147
|
+
/^insert[-_]?/i,
|
|
148
|
+
/^todo/i,
|
|
149
|
+
/^\*+$/,
|
|
150
|
+
/^\.\.\.$/,
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Check if a value looks like a placeholder
|
|
155
|
+
* @param {string} value - Value to check
|
|
156
|
+
* @returns {boolean} True if it looks like a placeholder
|
|
157
|
+
*/
|
|
158
|
+
function isPlaceholder(value) {
|
|
159
|
+
if (!value || value.length < 3) return true;
|
|
160
|
+
return placeholderPatterns.some(pattern => pattern.test(value));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Detect potential secrets in environment variables
|
|
165
|
+
* @param {string} key - Variable name
|
|
166
|
+
* @param {string} value - Variable value
|
|
167
|
+
* @returns {Object|null} Detection result or null if no secret detected
|
|
168
|
+
*/
|
|
169
|
+
function detectSecret(key, value) {
|
|
170
|
+
if (!value || isPlaceholder(value)) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
for (const pattern of secretPatterns) {
|
|
175
|
+
// If pattern has keyPattern, only check if key matches
|
|
176
|
+
if (pattern.keyPattern && !pattern.keyPattern.test(key)) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (pattern.regex.test(value)) {
|
|
181
|
+
return {
|
|
182
|
+
detected: true,
|
|
183
|
+
description: pattern.description,
|
|
184
|
+
message: `may contain a real ${pattern.description}`
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
6
192
|
/**
|
|
7
193
|
* Parse a .env file into an object
|
|
8
194
|
* @param {string} content - File content
|
|
@@ -13,18 +199,30 @@ function parseEnv(content) {
|
|
|
13
199
|
variables: {},
|
|
14
200
|
errors: [],
|
|
15
201
|
warnings: [],
|
|
16
|
-
lineInfo: {}
|
|
202
|
+
lineInfo: {},
|
|
203
|
+
typeHints: {} // NEW: Store type hints from comments
|
|
17
204
|
};
|
|
18
205
|
|
|
19
206
|
const lines = content.split('\n');
|
|
207
|
+
let pendingTypeHint = null;
|
|
20
208
|
|
|
21
209
|
for (let i = 0; i < lines.length; i++) {
|
|
22
210
|
const lineNum = i + 1;
|
|
23
211
|
const line = lines[i];
|
|
24
212
|
const trimmed = line.trim();
|
|
25
213
|
|
|
26
|
-
//
|
|
27
|
-
if (
|
|
214
|
+
// Check for type hint in comments: # type: url OR # @type url
|
|
215
|
+
if (trimmed.startsWith('#')) {
|
|
216
|
+
const typeMatch = trimmed.match(/^#\s*(?:type:|@type)\s*(\w+)/i);
|
|
217
|
+
if (typeMatch) {
|
|
218
|
+
pendingTypeHint = typeMatch[1].toLowerCase();
|
|
219
|
+
}
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Skip empty lines
|
|
224
|
+
if (!trimmed) {
|
|
225
|
+
pendingTypeHint = null;
|
|
28
226
|
continue;
|
|
29
227
|
}
|
|
30
228
|
|
|
@@ -74,6 +272,12 @@ function parseEnv(content) {
|
|
|
74
272
|
|
|
75
273
|
result.variables[key] = value;
|
|
76
274
|
result.lineInfo[key] = lineNum;
|
|
275
|
+
|
|
276
|
+
// Store type hint if present
|
|
277
|
+
if (pendingTypeHint) {
|
|
278
|
+
result.typeHints[key] = pendingTypeHint;
|
|
279
|
+
pendingTypeHint = null;
|
|
280
|
+
}
|
|
77
281
|
}
|
|
78
282
|
|
|
79
283
|
return result;
|
|
@@ -129,7 +333,8 @@ function readEnvFile(filePath) {
|
|
|
129
333
|
variables: {},
|
|
130
334
|
errors: [{ message: `File not found: ${absolutePath}` }],
|
|
131
335
|
warnings: [],
|
|
132
|
-
lineInfo: {}
|
|
336
|
+
lineInfo: {},
|
|
337
|
+
typeHints: {}
|
|
133
338
|
};
|
|
134
339
|
}
|
|
135
340
|
|
|
@@ -202,6 +407,20 @@ function compare(envPath, examplePath) {
|
|
|
202
407
|
return result;
|
|
203
408
|
}
|
|
204
409
|
|
|
410
|
+
/**
|
|
411
|
+
* Validate a value against a type
|
|
412
|
+
* @param {string} value - Value to validate
|
|
413
|
+
* @param {string} type - Type name
|
|
414
|
+
* @returns {Object} Validation result {valid, message}
|
|
415
|
+
*/
|
|
416
|
+
function validateType(value, type) {
|
|
417
|
+
const validator = typeValidators[type.toLowerCase()];
|
|
418
|
+
if (!validator) {
|
|
419
|
+
return { valid: true, message: `Unknown type '${type}'` };
|
|
420
|
+
}
|
|
421
|
+
return validator(value);
|
|
422
|
+
}
|
|
423
|
+
|
|
205
424
|
/**
|
|
206
425
|
* Validate an env file
|
|
207
426
|
* @param {string} filePath - Path to .env file
|
|
@@ -209,7 +428,7 @@ function compare(envPath, examplePath) {
|
|
|
209
428
|
* @returns {Object} Validation result
|
|
210
429
|
*/
|
|
211
430
|
function validate(filePath, options = {}) {
|
|
212
|
-
const { required = [], noEmpty = false } = options;
|
|
431
|
+
const { required = [], noEmpty = false, types = {}, validateTypes = false, detectSecrets = false } = options;
|
|
213
432
|
|
|
214
433
|
const env = readEnvFile(filePath);
|
|
215
434
|
|
|
@@ -271,6 +490,38 @@ function validate(filePath, options = {}) {
|
|
|
271
490
|
}
|
|
272
491
|
}
|
|
273
492
|
|
|
493
|
+
// Type validation (from options.types or env.typeHints)
|
|
494
|
+
if (validateTypes || Object.keys(types).length > 0) {
|
|
495
|
+
const allTypes = { ...env.typeHints, ...types }; // options.types override hints
|
|
496
|
+
for (const [key, typeName] of Object.entries(allTypes)) {
|
|
497
|
+
if (key in env.variables && env.variables[key] !== '') {
|
|
498
|
+
const typeResult = validateType(env.variables[key], typeName);
|
|
499
|
+
if (!typeResult.valid) {
|
|
500
|
+
result.valid = false;
|
|
501
|
+
result.issues.push({
|
|
502
|
+
type: 'error',
|
|
503
|
+
line: env.lineInfo[key],
|
|
504
|
+
message: `Variable '${key}' ${typeResult.message} (got: ${env.variables[key]})`
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Secret detection
|
|
512
|
+
if (detectSecrets) {
|
|
513
|
+
for (const [key, value] of Object.entries(env.variables)) {
|
|
514
|
+
const secretResult = detectSecret(key, value);
|
|
515
|
+
if (secretResult) {
|
|
516
|
+
result.issues.push({
|
|
517
|
+
type: 'warning',
|
|
518
|
+
line: env.lineInfo[key],
|
|
519
|
+
message: `Variable '${key}' ${secretResult.message}`
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
274
525
|
// Add warnings
|
|
275
526
|
for (const warning of env.warnings) {
|
|
276
527
|
result.issues.push({
|
|
@@ -295,7 +546,10 @@ function check(envPath, options = {}) {
|
|
|
295
546
|
required = [],
|
|
296
547
|
noEmpty = false,
|
|
297
548
|
noExtra = false,
|
|
298
|
-
strict = false
|
|
549
|
+
strict = false,
|
|
550
|
+
types = {},
|
|
551
|
+
validateTypes = false,
|
|
552
|
+
detectSecrets = false
|
|
299
553
|
} = options;
|
|
300
554
|
|
|
301
555
|
const result = {
|
|
@@ -307,8 +561,26 @@ function check(envPath, options = {}) {
|
|
|
307
561
|
}
|
|
308
562
|
};
|
|
309
563
|
|
|
310
|
-
//
|
|
311
|
-
|
|
564
|
+
// Get type hints from example file if provided
|
|
565
|
+
let exampleTypeHints = {};
|
|
566
|
+
if (examplePath) {
|
|
567
|
+
const example = readEnvFile(examplePath);
|
|
568
|
+
if (example.exists) {
|
|
569
|
+
exampleTypeHints = example.typeHints || {};
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Merge type hints: example hints < explicit types
|
|
574
|
+
const mergedTypes = { ...exampleTypeHints, ...types };
|
|
575
|
+
|
|
576
|
+
// First validate the env file (including type validation and secret detection)
|
|
577
|
+
const validation = validate(envPath, {
|
|
578
|
+
required,
|
|
579
|
+
noEmpty,
|
|
580
|
+
types: mergedTypes,
|
|
581
|
+
validateTypes: validateTypes || Object.keys(mergedTypes).length > 0,
|
|
582
|
+
detectSecrets
|
|
583
|
+
});
|
|
312
584
|
|
|
313
585
|
if (!validation.valid) {
|
|
314
586
|
result.valid = false;
|
|
@@ -417,6 +689,11 @@ module.exports = {
|
|
|
417
689
|
readEnvFile,
|
|
418
690
|
compare,
|
|
419
691
|
validate,
|
|
692
|
+
validateType,
|
|
693
|
+
typeValidators,
|
|
694
|
+
detectSecret,
|
|
695
|
+
secretPatterns,
|
|
696
|
+
isPlaceholder,
|
|
420
697
|
check,
|
|
421
698
|
generate,
|
|
422
699
|
list,
|