@claude-agent/envcheck 1.3.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.
Files changed (3) hide show
  1. package/README.md +51 -1
  2. package/package.json +4 -2
  3. package/src/index.js +125 -4
package/README.md CHANGED
@@ -269,6 +269,55 @@ ADMIN_EMAIL=admin@example.com
269
269
  | `uuid` | UUID format | `550e8400-e29b-41d4-a716-446655440000` |
270
270
  | `string`/`str` | Any string (no validation) | anything |
271
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
+
272
321
  ### API Usage with Types
273
322
 
274
323
  ```javascript
@@ -308,7 +357,7 @@ WITH_EQUALS=postgres://user:pass@host/db?opt=val
308
357
  - **Auto-detection** - Finds .env.example automatically
309
358
  - **CI-friendly** - Exit codes and JSON output
310
359
  - **Comprehensive** - Parse, validate, compare, generate
311
- - **Well-tested** - 46 tests covering edge cases
360
+ - **Well-tested** - 72 tests covering edge cases
312
361
 
313
362
  ## vs. dotenv-safe / envalid
314
363
 
@@ -320,6 +369,7 @@ WITH_EQUALS=postgres://user:pass@host/db?opt=val
320
369
  | **CI/CD integration** | ✅ GitHub Action | ❌ | ❌ |
321
370
  | **Pre-commit hook** | ✅ | ❌ | ❌ |
322
371
  | Type validation | ✅ (static) | ❌ | ✅ (runtime) |
372
+ | **Secret detection** | ✅ | ❌ | ❌ |
323
373
  | Zero dependencies | ✅ | ❌ | ❌ |
324
374
 
325
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-agent/envcheck",
3
- "version": "1.3.0",
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
@@ -87,6 +87,108 @@ const typeValidators = {
87
87
  }
88
88
  };
89
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
+
90
192
  /**
91
193
  * Parse a .env file into an object
92
194
  * @param {string} content - File content
@@ -326,7 +428,7 @@ function validateType(value, type) {
326
428
  * @returns {Object} Validation result
327
429
  */
328
430
  function validate(filePath, options = {}) {
329
- const { required = [], noEmpty = false, types = {}, validateTypes = false } = options;
431
+ const { required = [], noEmpty = false, types = {}, validateTypes = false, detectSecrets = false } = options;
330
432
 
331
433
  const env = readEnvFile(filePath);
332
434
 
@@ -406,6 +508,20 @@ function validate(filePath, options = {}) {
406
508
  }
407
509
  }
408
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
+
409
525
  // Add warnings
410
526
  for (const warning of env.warnings) {
411
527
  result.issues.push({
@@ -432,7 +548,8 @@ function check(envPath, options = {}) {
432
548
  noExtra = false,
433
549
  strict = false,
434
550
  types = {},
435
- validateTypes = false
551
+ validateTypes = false,
552
+ detectSecrets = false
436
553
  } = options;
437
554
 
438
555
  const result = {
@@ -456,12 +573,13 @@ function check(envPath, options = {}) {
456
573
  // Merge type hints: example hints < explicit types
457
574
  const mergedTypes = { ...exampleTypeHints, ...types };
458
575
 
459
- // First validate the env file (including type validation)
576
+ // First validate the env file (including type validation and secret detection)
460
577
  const validation = validate(envPath, {
461
578
  required,
462
579
  noEmpty,
463
580
  types: mergedTypes,
464
- validateTypes: validateTypes || Object.keys(mergedTypes).length > 0
581
+ validateTypes: validateTypes || Object.keys(mergedTypes).length > 0,
582
+ detectSecrets
465
583
  });
466
584
 
467
585
  if (!validation.valid) {
@@ -573,6 +691,9 @@ module.exports = {
573
691
  validate,
574
692
  validateType,
575
693
  typeValidators,
694
+ detectSecret,
695
+ secretPatterns,
696
+ isPlaceholder,
576
697
  check,
577
698
  generate,
578
699
  list,