Initial commit
This commit is contained in:
12
.claude-plugin/plugin.json
Normal file
12
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "env-config-validator",
|
||||||
|
"description": "Validate environment configuration files across local, staging, and production environments. Ensure required secrets, database URLs, API keys, and public variables are properly scoped and set. Use this skill when setting up environments, validating configuration, checking for missing secrets, auditing environment variables, ensuring proper scoping of public vs private vars, or troubleshooting environment issues. Trigger terms include env, environment variables, secrets, configuration, .env file,",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": {
|
||||||
|
"name": "Hope Overture",
|
||||||
|
"email": "support@worldbuilding-app-skills.dev"
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
"./skills"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# env-config-validator
|
||||||
|
|
||||||
|
Validate environment configuration files across local, staging, and production environments. Ensure required secrets, database URLs, API keys, and public variables are properly scoped and set. Use this skill when setting up environments, validating configuration, checking for missing secrets, auditing environment variables, ensuring proper scoping of public vs private vars, or troubleshooting environment issues. Trigger terms include env, environment variables, secrets, configuration, .env file,
|
||||||
57
plugin.lock.json
Normal file
57
plugin.lock.json
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||||
|
"pluginId": "gh:hopeoverture/worldbuilding-app-skills:plugins/env-config-validator",
|
||||||
|
"normalized": {
|
||||||
|
"repo": null,
|
||||||
|
"ref": "refs/tags/v20251128.0",
|
||||||
|
"commit": "b2c3f080f31b2a15405472268e698ec0ad9f783d",
|
||||||
|
"treeHash": "6f13a21125141e2c3e4ddb76bb380b437c15e8dc8818a600b1042b09ab333f5c",
|
||||||
|
"generatedAt": "2025-11-28T10:17:31.434758Z",
|
||||||
|
"toolVersion": "publish_plugins.py@0.2.0"
|
||||||
|
},
|
||||||
|
"origin": {
|
||||||
|
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||||
|
"branch": "master",
|
||||||
|
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||||
|
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "env-config-validator",
|
||||||
|
"description": "Validate environment configuration files across local, staging, and production environments. Ensure required secrets, database URLs, API keys, and public variables are properly scoped and set. Use this skill when setting up environments, validating configuration, checking for missing secrets, auditing environment variables, ensuring proper scoping of public vs private vars, or troubleshooting environment issues. Trigger terms include env, environment variables, secrets, configuration, .env file,",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "README.md",
|
||||||
|
"sha256": "80527c1dc047637a40c65f606c727fdb45a66505cc4f5148870ac9c5158b3290"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".claude-plugin/plugin.json",
|
||||||
|
"sha256": "0bc45bac5a2d6f18a05163ce61f34dc66d0adee5e2394bfa86c8801211208027"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/env-config-validator/SKILL.md",
|
||||||
|
"sha256": "bb8fba82a2d4301e16be0670d20be5f55ca76a5b24f7555eeb8364e76108f0b0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/env-config-validator/references/env_best_practices.md",
|
||||||
|
"sha256": "05efa89e43c449da24f351d8db252da4d12a6b2605d12fee4d292fed9214b3b0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/env-config-validator/scripts/validate_env.py",
|
||||||
|
"sha256": "c20c9f958252d79050655f42b3807c50775c66139c92390a60e0db0a28460aac"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/env-config-validator/assets/.env.example",
|
||||||
|
"sha256": "bb2e09eff0e9ce29b63b6e891518c6f4444e2e355683ea083c87021b1b838596"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dirSha256": "6f13a21125141e2c3e4ddb76bb380b437c15e8dc8818a600b1042b09ab333f5c"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"scannedAt": null,
|
||||||
|
"scannerVersion": null,
|
||||||
|
"flags": []
|
||||||
|
}
|
||||||
|
}
|
||||||
402
skills/env-config-validator/SKILL.md
Normal file
402
skills/env-config-validator/SKILL.md
Normal file
@@ -0,0 +1,402 @@
|
|||||||
|
---
|
||||||
|
name: env-config-validator
|
||||||
|
description: Validate environment configuration files across local, staging, and production environments. Ensure required secrets, database URLs, API keys, and public variables are properly scoped and set. Use this skill when setting up environments, validating configuration, checking for missing secrets, auditing environment variables, ensuring proper scoping of public vs private vars, or troubleshooting environment issues. Trigger terms include env, environment variables, secrets, configuration, .env file, environment validation, missing variables, config check, NEXT_PUBLIC, env vars, database URL, API keys.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Environment Configuration Validator
|
||||||
|
|
||||||
|
Validate `.env` files across local, staging, and production environments. Ensure all required secrets, database URLs, API keys, and public variables are properly scoped, set, and secure.
|
||||||
|
|
||||||
|
## Core Capabilities
|
||||||
|
|
||||||
|
### 1. Validate Environment Files
|
||||||
|
|
||||||
|
To validate environment configuration:
|
||||||
|
|
||||||
|
- Parse `.env`, `.env.local`, `.env.production`, etc.
|
||||||
|
- Check for required variables
|
||||||
|
- Verify variable naming conventions
|
||||||
|
- Detect security issues (exposed secrets, weak values)
|
||||||
|
- Use `scripts/validate_env.py` for automated validation
|
||||||
|
|
||||||
|
### 2. Check Variable Scoping
|
||||||
|
|
||||||
|
Ensure proper scoping of environment variables:
|
||||||
|
|
||||||
|
- **Public variables** (`NEXT_PUBLIC_*`): Accessible in browser
|
||||||
|
- **Private variables**: Server-side only
|
||||||
|
- **Database credentials**: Never exposed to client
|
||||||
|
- **API keys**: Properly scoped based on usage
|
||||||
|
|
||||||
|
### 3. Cross-Environment Validation
|
||||||
|
|
||||||
|
Compare configurations across environments:
|
||||||
|
|
||||||
|
- Identify missing variables in staging/production
|
||||||
|
- Check for environment-specific overrides
|
||||||
|
- Ensure consistency in variable names
|
||||||
|
- Validate environment-specific values (URLs, keys)
|
||||||
|
|
||||||
|
### 4. Security Auditing
|
||||||
|
|
||||||
|
Detect security vulnerabilities in environment configuration:
|
||||||
|
|
||||||
|
- Exposed secrets in public variables
|
||||||
|
- Weak or default values
|
||||||
|
- Hardcoded credentials in code
|
||||||
|
- Missing required security variables (JWT secrets, encryption keys)
|
||||||
|
|
||||||
|
## Validation Rules
|
||||||
|
|
||||||
|
### Required Variables
|
||||||
|
|
||||||
|
Ensure these categories of variables are present:
|
||||||
|
|
||||||
|
1. **Database Connection**
|
||||||
|
- `DATABASE_URL` or equivalent
|
||||||
|
- Connection pool settings (optional)
|
||||||
|
|
||||||
|
2. **Authentication**
|
||||||
|
- `JWT_SECRET` or `AUTH_SECRET`
|
||||||
|
- OAuth credentials (if using OAuth)
|
||||||
|
- Session secrets
|
||||||
|
|
||||||
|
3. **External APIs**
|
||||||
|
- Third-party API keys
|
||||||
|
- Service endpoints
|
||||||
|
- Rate limiting tokens
|
||||||
|
|
||||||
|
4. **Application Config**
|
||||||
|
- `NODE_ENV`
|
||||||
|
- `NEXT_PUBLIC_APP_URL` or `APP_URL`
|
||||||
|
- Feature flags (optional)
|
||||||
|
|
||||||
|
5. **Email/Notifications** (if used)
|
||||||
|
- SMTP credentials
|
||||||
|
- Email service API keys
|
||||||
|
|
||||||
|
### Naming Conventions
|
||||||
|
|
||||||
|
Follow Next.js environment variable conventions:
|
||||||
|
|
||||||
|
- **Public variables**: `NEXT_PUBLIC_*` prefix
|
||||||
|
- Example: `NEXT_PUBLIC_API_URL`
|
||||||
|
- Accessible in browser
|
||||||
|
- Never put secrets here
|
||||||
|
|
||||||
|
- **Private variables**: No prefix
|
||||||
|
- Example: `DATABASE_URL`, `API_SECRET`
|
||||||
|
- Server-side only
|
||||||
|
- Safe for secrets
|
||||||
|
|
||||||
|
- **Naming style**: `SCREAMING_SNAKE_CASE`
|
||||||
|
- Example: `DATABASE_URL`, `JWT_SECRET`, `STRIPE_API_KEY`
|
||||||
|
|
||||||
|
### Security Rules
|
||||||
|
|
||||||
|
1. **Never expose secrets in public variables**
|
||||||
|
- [ERROR] `NEXT_PUBLIC_DATABASE_URL`
|
||||||
|
- [OK] `DATABASE_URL`
|
||||||
|
|
||||||
|
2. **Database URLs must be private**
|
||||||
|
- [ERROR] `NEXT_PUBLIC_DB_URL`
|
||||||
|
- [OK] `DATABASE_URL`
|
||||||
|
|
||||||
|
3. **API keys scoping**
|
||||||
|
- Client-side API keys → `NEXT_PUBLIC_*` (e.g., Google Maps)
|
||||||
|
- Server-side API keys → No prefix (e.g., Stripe secret)
|
||||||
|
|
||||||
|
4. **No hardcoded secrets in code**
|
||||||
|
- Use environment variables for all secrets
|
||||||
|
- Never commit `.env.local` or `.env.production`
|
||||||
|
|
||||||
|
5. **Strong secrets**
|
||||||
|
- JWT/session secrets: minimum 32 characters
|
||||||
|
- Use cryptographically random values
|
||||||
|
- No default or example values in production
|
||||||
|
|
||||||
|
## Validation Script
|
||||||
|
|
||||||
|
Use `scripts/validate_env.py` to automate validation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Validate current .env file
|
||||||
|
python scripts/validate_env.py
|
||||||
|
|
||||||
|
# Validate specific file
|
||||||
|
python scripts/validate_env.py --file .env.production
|
||||||
|
|
||||||
|
# Compare multiple environments
|
||||||
|
python scripts/validate_env.py --compare .env.local .env.production
|
||||||
|
|
||||||
|
# Check against required variables template
|
||||||
|
python scripts/validate_env.py --template .env.example
|
||||||
|
```
|
||||||
|
|
||||||
|
The script checks:
|
||||||
|
- Required variables are present
|
||||||
|
- Naming conventions are followed
|
||||||
|
- No secrets in public variables
|
||||||
|
- No weak or default values
|
||||||
|
- Consistent naming across environments
|
||||||
|
|
||||||
|
## Common Issues and Solutions
|
||||||
|
|
||||||
|
### Issue: Missing DATABASE_URL in Production
|
||||||
|
|
||||||
|
**Detection**: Script reports missing required variable
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
```bash
|
||||||
|
# Add to .env.production
|
||||||
|
DATABASE_URL="postgresql://user:password@host:5432/dbname"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: Use different databases for dev/staging/prod
|
||||||
|
|
||||||
|
### Issue: Secret Exposed in Public Variable
|
||||||
|
|
||||||
|
**Detection**: Script finds `NEXT_PUBLIC_` prefix on secret
|
||||||
|
|
||||||
|
**Problem**:
|
||||||
|
```bash
|
||||||
|
# [ERROR] WRONG - secret exposed to browser
|
||||||
|
NEXT_PUBLIC_API_SECRET="secret123"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
```bash
|
||||||
|
# [OK] CORRECT - server-side only
|
||||||
|
API_SECRET="secret123"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue: Weak JWT Secret
|
||||||
|
|
||||||
|
**Detection**: Script detects short or weak secret
|
||||||
|
|
||||||
|
**Problem**:
|
||||||
|
```bash
|
||||||
|
# [ERROR] WRONG - too short, predictable
|
||||||
|
JWT_SECRET="secret"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
```bash
|
||||||
|
# [OK] CORRECT - strong, random, 32+ characters
|
||||||
|
JWT_SECRET="a8f3d9c2e1b7f4a6d8c3e9b2f1a7d4c8e3b9f2a1d7c4e8b3f9a2d1c7e4b8f3a9"
|
||||||
|
```
|
||||||
|
|
||||||
|
Generate with:
|
||||||
|
```bash
|
||||||
|
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue: Inconsistent Variable Names Across Environments
|
||||||
|
|
||||||
|
**Detection**: Script comparison shows name mismatch
|
||||||
|
|
||||||
|
**Problem**:
|
||||||
|
```bash
|
||||||
|
# .env.local
|
||||||
|
DATABASE_URL="..."
|
||||||
|
|
||||||
|
# .env.production
|
||||||
|
DB_URL="..." # [ERROR] Different name
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution**: Use consistent names
|
||||||
|
```bash
|
||||||
|
# Both files
|
||||||
|
DATABASE_URL="..."
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue: Missing Public API URL
|
||||||
|
|
||||||
|
**Detection**: Client-side code fails to connect to API
|
||||||
|
|
||||||
|
**Problem**: `NEXT_PUBLIC_API_URL` not set
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
```bash
|
||||||
|
# .env.local
|
||||||
|
NEXT_PUBLIC_API_URL="http://localhost:3000"
|
||||||
|
|
||||||
|
# .env.production
|
||||||
|
NEXT_PUBLIC_API_URL="https://api.yourapp.com"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Resource Files
|
||||||
|
|
||||||
|
### scripts/validate_env.py
|
||||||
|
Python script to validate environment files, check for security issues, compare across environments, and verify against templates. Provides detailed error messages and suggestions.
|
||||||
|
|
||||||
|
### references/env_best_practices.md
|
||||||
|
Comprehensive guide to environment variable management including:
|
||||||
|
- Security best practices
|
||||||
|
- Naming conventions
|
||||||
|
- Scoping rules (public vs private)
|
||||||
|
- Common patterns for different services
|
||||||
|
- Environment-specific configuration
|
||||||
|
- Secret rotation strategies
|
||||||
|
|
||||||
|
### assets/.env.example
|
||||||
|
Template showing all required environment variables for a worldbuilding application. Use as a reference for setting up new environments or auditing existing ones.
|
||||||
|
|
||||||
|
## Environment-Specific Configuration
|
||||||
|
|
||||||
|
### Development (.env.local)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Database
|
||||||
|
DATABASE_URL="postgresql://user:password@localhost:5432/worldbuilding_dev"
|
||||||
|
|
||||||
|
# Authentication
|
||||||
|
JWT_SECRET="dev-secret-change-in-production"
|
||||||
|
NEXTAUTH_URL="http://localhost:3000"
|
||||||
|
NEXTAUTH_SECRET="dev-nextauth-secret"
|
||||||
|
|
||||||
|
# Public
|
||||||
|
NEXT_PUBLIC_API_URL="http://localhost:3000"
|
||||||
|
NEXT_PUBLIC_APP_NAME="Worldbuilding App (Dev)"
|
||||||
|
|
||||||
|
# External APIs (test keys)
|
||||||
|
OPENAI_API_KEY="sk-test-..."
|
||||||
|
STRIPE_SECRET_KEY="sk_test_..."
|
||||||
|
```
|
||||||
|
|
||||||
|
### Staging (.env.staging)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Database
|
||||||
|
DATABASE_URL="postgresql://user:password@staging-db.com:5432/worldbuilding_staging"
|
||||||
|
|
||||||
|
# Authentication
|
||||||
|
JWT_SECRET="staging-secret-32-chars-minimum"
|
||||||
|
NEXTAUTH_URL="https://staging.yourapp.com"
|
||||||
|
NEXTAUTH_SECRET="staging-nextauth-secret"
|
||||||
|
|
||||||
|
# Public
|
||||||
|
NEXT_PUBLIC_API_URL="https://staging.yourapp.com"
|
||||||
|
NEXT_PUBLIC_APP_NAME="Worldbuilding App (Staging)"
|
||||||
|
|
||||||
|
# External APIs (test keys)
|
||||||
|
OPENAI_API_KEY="sk-test-..."
|
||||||
|
STRIPE_SECRET_KEY="sk_test_..."
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production (.env.production)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Database
|
||||||
|
DATABASE_URL="postgresql://user:password@prod-db.com:5432/worldbuilding_prod"
|
||||||
|
|
||||||
|
# Authentication
|
||||||
|
JWT_SECRET="production-secret-use-crypto-random-32-chars-minimum"
|
||||||
|
NEXTAUTH_URL="https://yourapp.com"
|
||||||
|
NEXTAUTH_SECRET="production-nextauth-secret"
|
||||||
|
|
||||||
|
# Public
|
||||||
|
NEXT_PUBLIC_API_URL="https://api.yourapp.com"
|
||||||
|
NEXT_PUBLIC_APP_NAME="Worldbuilding App"
|
||||||
|
|
||||||
|
# External APIs (production keys)
|
||||||
|
OPENAI_API_KEY="sk-live-..."
|
||||||
|
STRIPE_SECRET_KEY="sk_live_..."
|
||||||
|
|
||||||
|
# Monitoring
|
||||||
|
SENTRY_DSN="https://..."
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Never commit secrets**
|
||||||
|
- Add `.env.local`, `.env.production` to `.gitignore`
|
||||||
|
- Commit `.env.example` as a template
|
||||||
|
|
||||||
|
2. **Use strong, random secrets**
|
||||||
|
- Minimum 32 characters for JWT/session secrets
|
||||||
|
- Use `crypto.randomBytes()` or password manager
|
||||||
|
|
||||||
|
3. **Scope variables correctly**
|
||||||
|
- Public (`NEXT_PUBLIC_*`): Only non-sensitive, client-accessible data
|
||||||
|
- Private (no prefix): All secrets, credentials, server-only config
|
||||||
|
|
||||||
|
4. **Consistent naming**
|
||||||
|
- Use same variable names across all environments
|
||||||
|
- Follow `SCREAMING_SNAKE_CASE` convention
|
||||||
|
|
||||||
|
5. **Environment-specific values**
|
||||||
|
- Different database URLs per environment
|
||||||
|
- Test API keys in dev/staging, production keys in prod
|
||||||
|
- Environment-specific URLs and endpoints
|
||||||
|
|
||||||
|
6. **Document required variables**
|
||||||
|
- Keep `.env.example` updated
|
||||||
|
- Add comments explaining each variable
|
||||||
|
- Document where to get values (API dashboard, etc.)
|
||||||
|
|
||||||
|
7. **Validate on deployment**
|
||||||
|
- Run validation script in CI/CD pipeline
|
||||||
|
- Fail deployment if required variables missing
|
||||||
|
- Check for security issues before deploying
|
||||||
|
|
||||||
|
8. **Rotate secrets regularly**
|
||||||
|
- Change JWT secrets periodically
|
||||||
|
- Rotate API keys on schedule
|
||||||
|
- Update after team member departures
|
||||||
|
|
||||||
|
9. **Use secret management tools**
|
||||||
|
- Consider Vercel Environment Variables
|
||||||
|
- AWS Secrets Manager, HashiCorp Vault for sensitive data
|
||||||
|
- Never store production secrets in code or comments
|
||||||
|
|
||||||
|
10. **Test environment parity**
|
||||||
|
- Staging should mirror production as closely as possible
|
||||||
|
- Use same variable names, just different values
|
||||||
|
- Test with production-like data
|
||||||
|
|
||||||
|
## Integration with Worldbuilding App
|
||||||
|
|
||||||
|
Common environment variables for worldbuilding applications:
|
||||||
|
|
||||||
|
### Database
|
||||||
|
```bash
|
||||||
|
DATABASE_URL="postgresql://..."
|
||||||
|
DATABASE_POOL_SIZE="10" # Optional
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
```bash
|
||||||
|
JWT_SECRET="..."
|
||||||
|
NEXTAUTH_URL="..."
|
||||||
|
NEXTAUTH_SECRET="..."
|
||||||
|
```
|
||||||
|
|
||||||
|
### External APIs
|
||||||
|
```bash
|
||||||
|
# AI services (optional)
|
||||||
|
OPENAI_API_KEY="..."
|
||||||
|
|
||||||
|
# Maps (if using)
|
||||||
|
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY="..."
|
||||||
|
|
||||||
|
# Image hosting (if using)
|
||||||
|
CLOUDINARY_URL="..."
|
||||||
|
```
|
||||||
|
|
||||||
|
### Application
|
||||||
|
```bash
|
||||||
|
NODE_ENV="production"
|
||||||
|
NEXT_PUBLIC_APP_URL="https://..."
|
||||||
|
NEXT_PUBLIC_APP_NAME="Worldbuilding App"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Email (if using)
|
||||||
|
```bash
|
||||||
|
SMTP_HOST="..."
|
||||||
|
SMTP_PORT="587"
|
||||||
|
SMTP_USER="..."
|
||||||
|
SMTP_PASSWORD="..."
|
||||||
|
```
|
||||||
|
|
||||||
|
Consult `references/env_best_practices.md` for detailed guidance and `assets/.env.example` for a complete template.
|
||||||
157
skills/env-config-validator/assets/.env.example
Normal file
157
skills/env-config-validator/assets/.env.example
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
# Environment Configuration Template
|
||||||
|
# Copy this file to .env.local for development
|
||||||
|
# DO NOT commit .env.local or .env.production to git
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# ENVIRONMENT
|
||||||
|
# ============================================
|
||||||
|
NODE_ENV=development
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# DATABASE
|
||||||
|
# ============================================
|
||||||
|
# PostgreSQL connection string
|
||||||
|
DATABASE_URL="postgresql://user:password@localhost:5432/worldbuilding_dev"
|
||||||
|
|
||||||
|
# Optional: Connection pool settings
|
||||||
|
# DATABASE_POOL_SIZE=10
|
||||||
|
# DATABASE_POOL_TIMEOUT=30000
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# AUTHENTICATION
|
||||||
|
# ============================================
|
||||||
|
# JWT secret (generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))")
|
||||||
|
JWT_SECRET="your-jwt-secret-here-32-chars-minimum"
|
||||||
|
|
||||||
|
# NextAuth.js (if using)
|
||||||
|
NEXTAUTH_URL="http://localhost:3000"
|
||||||
|
NEXTAUTH_SECRET="your-nextauth-secret-here-32-chars-minimum"
|
||||||
|
|
||||||
|
# OAuth providers (if using)
|
||||||
|
# GOOGLE_CLIENT_ID=""
|
||||||
|
# GOOGLE_CLIENT_SECRET=""
|
||||||
|
# GITHUB_CLIENT_ID=""
|
||||||
|
# GITHUB_CLIENT_SECRET=""
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# APPLICATION URLs
|
||||||
|
# ============================================
|
||||||
|
# Public-facing application URL
|
||||||
|
NEXT_PUBLIC_APP_URL="http://localhost:3000"
|
||||||
|
|
||||||
|
# API base URL (if different from app URL)
|
||||||
|
NEXT_PUBLIC_API_URL="http://localhost:3000"
|
||||||
|
|
||||||
|
# Application name
|
||||||
|
NEXT_PUBLIC_APP_NAME="Worldbuilding App"
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# EXTERNAL APIs (Optional)
|
||||||
|
# ============================================
|
||||||
|
# OpenAI (for AI features)
|
||||||
|
# OPENAI_API_KEY="sk-..."
|
||||||
|
# OPENAI_ORG_ID="org-..."
|
||||||
|
|
||||||
|
# Anthropic Claude (alternative to OpenAI)
|
||||||
|
# ANTHROPIC_API_KEY="sk-ant-..."
|
||||||
|
|
||||||
|
# Google Maps (if using maps features)
|
||||||
|
# NEXT_PUBLIC_GOOGLE_MAPS_KEY="..." # Only if client-side
|
||||||
|
# GOOGLE_MAPS_SERVER_KEY="..." # For server-side geocoding
|
||||||
|
|
||||||
|
# Cloudinary (if using image hosting)
|
||||||
|
# CLOUDINARY_URL="cloudinary://..."
|
||||||
|
# NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME="..."
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# EMAIL (Optional)
|
||||||
|
# ============================================
|
||||||
|
# SMTP Configuration
|
||||||
|
# SMTP_HOST="smtp.gmail.com"
|
||||||
|
# SMTP_PORT=587
|
||||||
|
# SMTP_USER="your-email@gmail.com"
|
||||||
|
# SMTP_PASSWORD="your-app-specific-password"
|
||||||
|
# SMTP_FROM="noreply@example.com"
|
||||||
|
|
||||||
|
# Email service API (alternative to SMTP)
|
||||||
|
# SENDGRID_API_KEY="SG...."
|
||||||
|
# MAILGUN_API_KEY="..."
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# PAYMENT (Optional)
|
||||||
|
# ============================================
|
||||||
|
# Stripe
|
||||||
|
# NEXT_PUBLIC_STRIPE_PUBLIC_KEY="pk_test_..." # Client-side
|
||||||
|
# STRIPE_SECRET_KEY="sk_test_..." # Server-side
|
||||||
|
# STRIPE_WEBHOOK_SECRET="whsec_..." # Webhook verification
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# FILE STORAGE (Optional)
|
||||||
|
# ============================================
|
||||||
|
# AWS S3
|
||||||
|
# AWS_ACCESS_KEY_ID="..."
|
||||||
|
# AWS_SECRET_ACCESS_KEY="..."
|
||||||
|
# AWS_REGION="us-east-1"
|
||||||
|
# AWS_S3_BUCKET="your-bucket-name"
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# MONITORING & LOGGING (Optional)
|
||||||
|
# ============================================
|
||||||
|
# Sentry (error tracking)
|
||||||
|
# SENTRY_DSN="https://...@sentry.io/..."
|
||||||
|
# SENTRY_ENVIRONMENT="development"
|
||||||
|
# SENTRY_AUTH_TOKEN="..." # For source maps upload
|
||||||
|
|
||||||
|
# LogRocket (session replay)
|
||||||
|
# NEXT_PUBLIC_LOGROCKET_APP_ID="..."
|
||||||
|
|
||||||
|
# Custom logging
|
||||||
|
# LOG_LEVEL="debug" # debug, info, warn, error
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# RATE LIMITING (Optional)
|
||||||
|
# ============================================
|
||||||
|
# Redis (for rate limiting)
|
||||||
|
# REDIS_URL="redis://localhost:6379"
|
||||||
|
|
||||||
|
# Upstash Redis (managed Redis)
|
||||||
|
# UPSTASH_REDIS_REST_URL="https://..."
|
||||||
|
# UPSTASH_REDIS_REST_TOKEN="..."
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# SEARCH (Optional)
|
||||||
|
# ============================================
|
||||||
|
# Algolia (if using search)
|
||||||
|
# NEXT_PUBLIC_ALGOLIA_APP_ID="..."
|
||||||
|
# NEXT_PUBLIC_ALGOLIA_SEARCH_KEY="..." # Search-only public key
|
||||||
|
# ALGOLIA_ADMIN_KEY="..." # Admin key (server-side only)
|
||||||
|
|
||||||
|
# Meilisearch (alternative to Algolia)
|
||||||
|
# MEILISEARCH_HOST="http://localhost:7700"
|
||||||
|
# MEILISEARCH_KEY="..."
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# FEATURE FLAGS (Optional)
|
||||||
|
# ============================================
|
||||||
|
# NEXT_PUBLIC_FEATURE_AI_ASSISTANCE="false"
|
||||||
|
# NEXT_PUBLIC_FEATURE_COLLABORATIVE_EDITING="false"
|
||||||
|
# NEXT_PUBLIC_FEATURE_TIMELINE_VISUALIZATION="true"
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# DEVELOPMENT TOOLS (Optional)
|
||||||
|
# ============================================
|
||||||
|
# Enable debug mode
|
||||||
|
# DEBUG="true"
|
||||||
|
|
||||||
|
# Disable telemetry
|
||||||
|
# NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# NOTES
|
||||||
|
# ============================================
|
||||||
|
# 1. Copy this file to .env.local for local development
|
||||||
|
# 2. Generate strong secrets using:
|
||||||
|
# node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
||||||
|
# 3. Never commit .env.local or .env.production to git
|
||||||
|
# 4. Use NEXT_PUBLIC_ prefix only for non-sensitive, client-accessible variables
|
||||||
|
# 5. Keep this template updated as you add new environment variables
|
||||||
635
skills/env-config-validator/references/env_best_practices.md
Normal file
635
skills/env-config-validator/references/env_best_practices.md
Normal file
@@ -0,0 +1,635 @@
|
|||||||
|
# Environment Variable Best Practices
|
||||||
|
|
||||||
|
Comprehensive guide to managing environment variables in Next.js applications.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Security Best Practices](#security-best-practices)
|
||||||
|
2. [Naming Conventions](#naming-conventions)
|
||||||
|
3. [Scoping Rules](#scoping-rules)
|
||||||
|
4. [Common Patterns](#common-patterns)
|
||||||
|
5. [Environment-Specific Configuration](#environment-specific-configuration)
|
||||||
|
6. [Secret Rotation](#secret-rotation)
|
||||||
|
7. [Testing Strategies](#testing-strategies)
|
||||||
|
8. [Deployment Checklist](#deployment-checklist)
|
||||||
|
|
||||||
|
## Security Best Practices
|
||||||
|
|
||||||
|
### 1. Never Commit Secrets
|
||||||
|
|
||||||
|
**Rule**: Never commit `.env.local` or `.env.production` files
|
||||||
|
|
||||||
|
**Setup `.gitignore`**:
|
||||||
|
```
|
||||||
|
# Environment files with secrets
|
||||||
|
.env*.local
|
||||||
|
.env.production
|
||||||
|
.env.staging
|
||||||
|
|
||||||
|
# Only commit the template
|
||||||
|
!.env.example
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Use Strong, Random Secrets
|
||||||
|
|
||||||
|
**Requirements for secrets**:
|
||||||
|
- Minimum 32 characters
|
||||||
|
- Cryptographically random
|
||||||
|
- High entropy (mix of characters, numbers, symbols)
|
||||||
|
|
||||||
|
**Generate strong secrets**:
|
||||||
|
```bash
|
||||||
|
# Node.js (recommended for JWT/session secrets)
|
||||||
|
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
||||||
|
|
||||||
|
# OpenSSL
|
||||||
|
openssl rand -hex 32
|
||||||
|
|
||||||
|
# Python
|
||||||
|
python -c "import secrets; print(secrets.token_hex(32))"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Scope Variables Correctly
|
||||||
|
|
||||||
|
**Public variables** (`NEXT_PUBLIC_*`):
|
||||||
|
- [OK] API endpoints
|
||||||
|
- [OK] Feature flags
|
||||||
|
- [OK] Client-side config
|
||||||
|
- [X] API keys
|
||||||
|
- [X] Database credentials
|
||||||
|
- [X] Any secrets
|
||||||
|
|
||||||
|
**Private variables** (no prefix):
|
||||||
|
- [OK] Database URLs
|
||||||
|
- [OK] API secrets
|
||||||
|
- [OK] JWT/session secrets
|
||||||
|
- [OK] Third-party service credentials
|
||||||
|
|
||||||
|
### 4. Validate on Startup
|
||||||
|
|
||||||
|
**Validate environment variables when app starts**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// lib/env.ts
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const envSchema = z.object({
|
||||||
|
DATABASE_URL: z.string().url(),
|
||||||
|
JWT_SECRET: z.string().min(32),
|
||||||
|
NEXTAUTH_SECRET: z.string().min(32),
|
||||||
|
NODE_ENV: z.enum(['development', 'staging', 'production']),
|
||||||
|
NEXT_PUBLIC_API_URL: z.string().url(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const env = envSchema.parse(process.env);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits**:
|
||||||
|
- Fail fast on missing/invalid variables
|
||||||
|
- Type-safe access to env vars
|
||||||
|
- Clear documentation of required variables
|
||||||
|
|
||||||
|
### 5. Use Secret Management Tools
|
||||||
|
|
||||||
|
**For production environments**:
|
||||||
|
|
||||||
|
- **Vercel**: Use Environment Variables UI (automatic encryption)
|
||||||
|
- **AWS**: AWS Secrets Manager or Systems Manager Parameter Store
|
||||||
|
- **GCP**: Secret Manager
|
||||||
|
- **Azure**: Key Vault
|
||||||
|
- **HashiCorp Vault**: Enterprise secret management
|
||||||
|
- **Doppler**: Universal secrets manager
|
||||||
|
|
||||||
|
**Benefits**:
|
||||||
|
- Encryption at rest
|
||||||
|
- Access control and auditing
|
||||||
|
- Automatic rotation
|
||||||
|
- Version history
|
||||||
|
|
||||||
|
## Naming Conventions
|
||||||
|
|
||||||
|
### Standard Format
|
||||||
|
|
||||||
|
Use `SCREAMING_SNAKE_CASE` for all environment variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# [OK] CORRECT
|
||||||
|
DATABASE_URL="..."
|
||||||
|
JWT_SECRET="..."
|
||||||
|
NEXT_PUBLIC_API_URL="..."
|
||||||
|
|
||||||
|
# [X] WRONG
|
||||||
|
databaseUrl="..."
|
||||||
|
jwt-secret="..."
|
||||||
|
NextPublicApiUrl="..."
|
||||||
|
```
|
||||||
|
|
||||||
|
### Prefixes
|
||||||
|
|
||||||
|
**Next.js public variables**:
|
||||||
|
```bash
|
||||||
|
NEXT_PUBLIC_API_URL="https://api.example.com"
|
||||||
|
NEXT_PUBLIC_APP_NAME="My App"
|
||||||
|
NEXT_PUBLIC_GOOGLE_MAPS_KEY="..." # Only if client-side usage
|
||||||
|
```
|
||||||
|
|
||||||
|
**Service-specific prefixes** (optional but recommended):
|
||||||
|
```bash
|
||||||
|
# Database
|
||||||
|
DATABASE_URL="..."
|
||||||
|
DATABASE_POOL_SIZE="..."
|
||||||
|
|
||||||
|
# Stripe
|
||||||
|
STRIPE_PUBLIC_KEY="..."
|
||||||
|
STRIPE_SECRET_KEY="..."
|
||||||
|
STRIPE_WEBHOOK_SECRET="..."
|
||||||
|
|
||||||
|
# Email
|
||||||
|
SMTP_HOST="..."
|
||||||
|
SMTP_PORT="..."
|
||||||
|
SMTP_USER="..."
|
||||||
|
SMTP_PASSWORD="..."
|
||||||
|
```
|
||||||
|
|
||||||
|
### Avoid Redundancy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# [OK] GOOD - clear and concise
|
||||||
|
DATABASE_URL="..."
|
||||||
|
JWT_SECRET="..."
|
||||||
|
|
||||||
|
# [X] BAD - redundant
|
||||||
|
DATABASE_CONNECTION_URL="..." # "CONNECTION" is redundant with "URL"
|
||||||
|
JWT_SECRET_KEY="..." # "KEY" is redundant with "SECRET"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Scoping Rules
|
||||||
|
|
||||||
|
### Public Variables (`NEXT_PUBLIC_*`)
|
||||||
|
|
||||||
|
**When to use**:
|
||||||
|
- Client-side API endpoints
|
||||||
|
- Feature flags that affect UI
|
||||||
|
- Public configuration (app name, version)
|
||||||
|
- Client-side analytics IDs
|
||||||
|
- Map API keys (if client-side only)
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```bash
|
||||||
|
NEXT_PUBLIC_API_URL="https://api.example.com"
|
||||||
|
NEXT_PUBLIC_ANALYTICS_ID="G-XXXXXXXXXX"
|
||||||
|
NEXT_PUBLIC_FEATURE_NEW_UI="true"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Access in code**:
|
||||||
|
```typescript
|
||||||
|
// Available in both server and client components
|
||||||
|
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Private Variables (no prefix)
|
||||||
|
|
||||||
|
**When to use**:
|
||||||
|
- Database credentials
|
||||||
|
- API secrets
|
||||||
|
- JWT/session secrets
|
||||||
|
- Third-party service credentials
|
||||||
|
- Server-side API keys
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```bash
|
||||||
|
DATABASE_URL="postgresql://..."
|
||||||
|
JWT_SECRET="..."
|
||||||
|
STRIPE_SECRET_KEY="sk_live_..."
|
||||||
|
OPENAI_API_KEY="sk-..."
|
||||||
|
```
|
||||||
|
|
||||||
|
**Access in code**:
|
||||||
|
```typescript
|
||||||
|
// Only available in server components and API routes
|
||||||
|
const dbUrl = process.env.DATABASE_URL; // Server-side only
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mixed Scenarios
|
||||||
|
|
||||||
|
**Stripe example** (public + private keys):
|
||||||
|
```bash
|
||||||
|
# Public key - client-side checkout
|
||||||
|
NEXT_PUBLIC_STRIPE_PUBLIC_KEY="pk_live_..."
|
||||||
|
|
||||||
|
# Secret key - server-side payments
|
||||||
|
STRIPE_SECRET_KEY="sk_live_..."
|
||||||
|
|
||||||
|
# Webhook secret - server-side webhooks
|
||||||
|
STRIPE_WEBHOOK_SECRET="whsec_..."
|
||||||
|
```
|
||||||
|
|
||||||
|
**Map services example**:
|
||||||
|
```bash
|
||||||
|
# If using client-side maps
|
||||||
|
NEXT_PUBLIC_GOOGLE_MAPS_KEY="..."
|
||||||
|
|
||||||
|
# If using server-side geocoding
|
||||||
|
GOOGLE_MAPS_SERVER_KEY="..."
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Database Configuration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Standard database URL
|
||||||
|
DATABASE_URL="postgresql://user:password@host:5432/dbname"
|
||||||
|
|
||||||
|
# Optional connection pool settings
|
||||||
|
DATABASE_POOL_SIZE="10"
|
||||||
|
DATABASE_POOL_TIMEOUT="30000"
|
||||||
|
|
||||||
|
# Read replicas (optional)
|
||||||
|
DATABASE_READ_URL="postgresql://user:password@read-host:5432/dbname"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authentication (NextAuth.js)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Required for NextAuth
|
||||||
|
NEXTAUTH_URL="https://example.com"
|
||||||
|
NEXTAUTH_SECRET="..." # 32+ character random string
|
||||||
|
|
||||||
|
# OAuth providers (if using)
|
||||||
|
GOOGLE_CLIENT_ID="..."
|
||||||
|
GOOGLE_CLIENT_SECRET="..."
|
||||||
|
|
||||||
|
GITHUB_CLIENT_ID="..."
|
||||||
|
GITHUB_CLIENT_SECRET="..."
|
||||||
|
```
|
||||||
|
|
||||||
|
### JWT Authentication (Custom)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# JWT secret
|
||||||
|
JWT_SECRET="..." # 32+ character random string
|
||||||
|
|
||||||
|
# Optional: token expiration
|
||||||
|
JWT_EXPIRES_IN="7d"
|
||||||
|
|
||||||
|
# Optional: refresh token secret
|
||||||
|
JWT_REFRESH_SECRET="..." # Different from JWT_SECRET
|
||||||
|
```
|
||||||
|
|
||||||
|
### External APIs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# OpenAI
|
||||||
|
OPENAI_API_KEY="sk-..."
|
||||||
|
OPENAI_ORG_ID="org-..." # Optional
|
||||||
|
|
||||||
|
# Stripe
|
||||||
|
NEXT_PUBLIC_STRIPE_PUBLIC_KEY="pk_live_..."
|
||||||
|
STRIPE_SECRET_KEY="sk_live_..."
|
||||||
|
STRIPE_WEBHOOK_SECRET="whsec_..."
|
||||||
|
|
||||||
|
# SendGrid
|
||||||
|
SENDGRID_API_KEY="SG...."
|
||||||
|
|
||||||
|
# AWS
|
||||||
|
AWS_ACCESS_KEY_ID="..."
|
||||||
|
AWS_SECRET_ACCESS_KEY="..."
|
||||||
|
AWS_REGION="us-east-1"
|
||||||
|
|
||||||
|
# Cloudinary
|
||||||
|
CLOUDINARY_URL="cloudinary://..."
|
||||||
|
```
|
||||||
|
|
||||||
|
### Email Configuration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# SMTP
|
||||||
|
SMTP_HOST="smtp.gmail.com"
|
||||||
|
SMTP_PORT="587"
|
||||||
|
SMTP_USER="your-email@gmail.com"
|
||||||
|
SMTP_PASSWORD="app-specific-password"
|
||||||
|
|
||||||
|
# Email service API (alternative)
|
||||||
|
SENDGRID_API_KEY="SG...."
|
||||||
|
MAILGUN_API_KEY="..."
|
||||||
|
```
|
||||||
|
|
||||||
|
### Monitoring and Logging
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Sentry
|
||||||
|
SENTRY_DSN="https://...@sentry.io/..."
|
||||||
|
SENTRY_AUTH_TOKEN="..." # For source maps upload
|
||||||
|
|
||||||
|
# LogRocket
|
||||||
|
LOGROCKET_APP_ID="..."
|
||||||
|
|
||||||
|
# Custom logging
|
||||||
|
LOG_LEVEL="info" # debug, info, warn, error
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment-Specific Configuration
|
||||||
|
|
||||||
|
### Development (.env.local)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Permissive settings for development
|
||||||
|
NODE_ENV="development"
|
||||||
|
|
||||||
|
# Local database
|
||||||
|
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/myapp_dev"
|
||||||
|
|
||||||
|
# Development secrets (can be weak)
|
||||||
|
JWT_SECRET="dev-secret-change-in-production"
|
||||||
|
NEXTAUTH_SECRET="dev-nextauth-secret"
|
||||||
|
|
||||||
|
# Local URLs
|
||||||
|
NEXTAUTH_URL="http://localhost:3000"
|
||||||
|
NEXT_PUBLIC_API_URL="http://localhost:3000"
|
||||||
|
|
||||||
|
# Test API keys
|
||||||
|
STRIPE_SECRET_KEY="sk_test_..."
|
||||||
|
OPENAI_API_KEY="sk-test-..."
|
||||||
|
|
||||||
|
# Debug flags
|
||||||
|
DEBUG="true"
|
||||||
|
LOG_LEVEL="debug"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Staging (.env.staging)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Staging environment
|
||||||
|
NODE_ENV="production" # Use production mode
|
||||||
|
|
||||||
|
# Staging database
|
||||||
|
DATABASE_URL="postgresql://user:password@staging-db:5432/myapp_staging"
|
||||||
|
|
||||||
|
# Production-like secrets (but different from prod)
|
||||||
|
JWT_SECRET="staging-secret-32-chars-minimum"
|
||||||
|
NEXTAUTH_SECRET="staging-nextauth-secret"
|
||||||
|
|
||||||
|
# Staging URLs
|
||||||
|
NEXTAUTH_URL="https://staging.example.com"
|
||||||
|
NEXT_PUBLIC_API_URL="https://staging.example.com"
|
||||||
|
|
||||||
|
# Test API keys (same as dev)
|
||||||
|
STRIPE_SECRET_KEY="sk_test_..."
|
||||||
|
OPENAI_API_KEY="sk-test-..."
|
||||||
|
|
||||||
|
# Staging-specific config
|
||||||
|
SENTRY_ENVIRONMENT="staging"
|
||||||
|
LOG_LEVEL="info"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production (.env.production)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Production environment
|
||||||
|
NODE_ENV="production"
|
||||||
|
|
||||||
|
# Production database
|
||||||
|
DATABASE_URL="postgresql://user:strong-password@prod-db:5432/myapp_prod"
|
||||||
|
|
||||||
|
# Strong, random secrets
|
||||||
|
JWT_SECRET="production-secret-use-crypto-random-32-chars-minimum"
|
||||||
|
NEXTAUTH_SECRET="production-nextauth-secret-also-32-chars-minimum"
|
||||||
|
|
||||||
|
# Production URLs
|
||||||
|
NEXTAUTH_URL="https://example.com"
|
||||||
|
NEXT_PUBLIC_API_URL="https://api.example.com"
|
||||||
|
|
||||||
|
# Production API keys
|
||||||
|
STRIPE_SECRET_KEY="sk_live_..."
|
||||||
|
OPENAI_API_KEY="sk-live-..."
|
||||||
|
|
||||||
|
# Production-specific config
|
||||||
|
SENTRY_ENVIRONMENT="production"
|
||||||
|
LOG_LEVEL="warn"
|
||||||
|
|
||||||
|
# Performance optimization
|
||||||
|
DATABASE_POOL_SIZE="20"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Secret Rotation
|
||||||
|
|
||||||
|
### Why Rotate Secrets?
|
||||||
|
|
||||||
|
- **Security best practice**: Limit impact of potential leaks
|
||||||
|
- **Compliance**: Required by some standards (PCI-DSS, HIPAA)
|
||||||
|
- **Team changes**: Rotate after team member departures
|
||||||
|
- **Suspected breach**: Rotate immediately if compromise suspected
|
||||||
|
|
||||||
|
### What to Rotate
|
||||||
|
|
||||||
|
**Critical secrets** (rotate regularly):
|
||||||
|
- JWT/session secrets (every 90 days)
|
||||||
|
- Database passwords (every 90 days)
|
||||||
|
- API keys for financial services (every 90 days)
|
||||||
|
|
||||||
|
**Less critical** (rotate on schedule or as needed):
|
||||||
|
- Third-party API keys (annually or on breach)
|
||||||
|
- OAuth secrets (annually or on breach)
|
||||||
|
|
||||||
|
**Never rotate**:
|
||||||
|
- Public API keys (if they're truly public)
|
||||||
|
- Client IDs (non-secret identifiers)
|
||||||
|
|
||||||
|
### Rotation Process
|
||||||
|
|
||||||
|
**For JWT/session secrets**:
|
||||||
|
|
||||||
|
1. Generate new secret
|
||||||
|
2. Deploy with NEW_JWT_SECRET environment variable
|
||||||
|
3. Update code to validate with both old and new secrets
|
||||||
|
4. After grace period (e.g., 7 days), remove old secret
|
||||||
|
5. Rename NEW_JWT_SECRET to JWT_SECRET
|
||||||
|
|
||||||
|
**For database passwords**:
|
||||||
|
|
||||||
|
1. Create new database user/password
|
||||||
|
2. Deploy with new DATABASE_URL (no downtime)
|
||||||
|
3. Monitor for 24 hours
|
||||||
|
4. Delete old database user
|
||||||
|
5. Update password manager/secrets storage
|
||||||
|
|
||||||
|
**For API keys** (if service supports multiple keys):
|
||||||
|
|
||||||
|
1. Generate new API key (keep old active)
|
||||||
|
2. Deploy with new key
|
||||||
|
3. Monitor for 24 hours
|
||||||
|
4. Revoke old API key
|
||||||
|
|
||||||
|
## Testing Strategies
|
||||||
|
|
||||||
|
### Local Testing
|
||||||
|
|
||||||
|
**Use `.env.test` for test environment**:
|
||||||
|
```bash
|
||||||
|
# .env.test
|
||||||
|
NODE_ENV="test"
|
||||||
|
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/myapp_test"
|
||||||
|
JWT_SECRET="test-secret"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Load in tests**:
|
||||||
|
```typescript
|
||||||
|
// jest.config.js
|
||||||
|
module.exports = {
|
||||||
|
setupFiles: ['<rootDir>/jest.setup.js'],
|
||||||
|
};
|
||||||
|
|
||||||
|
// jest.setup.js
|
||||||
|
import { loadEnvConfig } from '@next/env';
|
||||||
|
loadEnvConfig(process.cwd());
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mock Environment Variables
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// test/utils/env.ts
|
||||||
|
export function mockEnv(vars: Record<string, string>) {
|
||||||
|
const original = { ...process.env };
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
Object.assign(process.env, vars);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
process.env = original;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// In test file
|
||||||
|
import { mockEnv } from './utils/env';
|
||||||
|
|
||||||
|
describe('API', () => {
|
||||||
|
mockEnv({
|
||||||
|
JWT_SECRET: 'test-secret',
|
||||||
|
DATABASE_URL: 'postgresql://localhost/test',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Your tests...
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Validate Before Tests
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// test/setup.ts
|
||||||
|
import { envSchema } from '@/lib/env';
|
||||||
|
|
||||||
|
try {
|
||||||
|
envSchema.parse(process.env);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Invalid test environment configuration:');
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment Checklist
|
||||||
|
|
||||||
|
### Before Deploying to Production
|
||||||
|
|
||||||
|
- [ ] All required variables are set in production environment
|
||||||
|
- [ ] Secrets are strong (32+ characters, random)
|
||||||
|
- [ ] No secrets in `NEXT_PUBLIC_*` variables
|
||||||
|
- [ ] Database URL points to production database
|
||||||
|
- [ ] API keys are production keys (not test keys)
|
||||||
|
- [ ] `NODE_ENV` is set to `production`
|
||||||
|
- [ ] URLs are production URLs (no localhost)
|
||||||
|
- [ ] `.env.production` is NOT committed to git
|
||||||
|
- [ ] Secrets are stored in secure secret management system
|
||||||
|
- [ ] CI/CD has access to environment variables
|
||||||
|
- [ ] Monitoring/logging is configured (Sentry, etc.)
|
||||||
|
|
||||||
|
### Validation Script
|
||||||
|
|
||||||
|
Run before deployment:
|
||||||
|
```bash
|
||||||
|
python scripts/validate_env.py --file .env.production
|
||||||
|
```
|
||||||
|
|
||||||
|
### CI/CD Integration
|
||||||
|
|
||||||
|
**GitHub Actions example**:
|
||||||
|
```yaml
|
||||||
|
- name: Validate environment
|
||||||
|
run: python scripts/validate_env.py --file .env.production
|
||||||
|
env:
|
||||||
|
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
||||||
|
JWT_SECRET: ${{ secrets.JWT_SECRET }}
|
||||||
|
# ... other secrets from GitHub Secrets
|
||||||
|
```
|
||||||
|
|
||||||
|
### Post-Deployment Verification
|
||||||
|
|
||||||
|
- [ ] Application starts without errors
|
||||||
|
- [ ] Database connection works
|
||||||
|
- [ ] Authentication works
|
||||||
|
- [ ] External API calls work
|
||||||
|
- [ ] No environment-related errors in logs
|
||||||
|
- [ ] Monitoring shows healthy status
|
||||||
|
|
||||||
|
## Common Mistakes to Avoid
|
||||||
|
|
||||||
|
### 1. Committing Secrets to Git
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# [X] NEVER DO THIS
|
||||||
|
git add .env.production
|
||||||
|
git commit -m "Add production config" # [ERROR] Secrets in git history
|
||||||
|
```
|
||||||
|
|
||||||
|
**If you accidentally commit secrets**:
|
||||||
|
1. Rotate all exposed secrets immediately
|
||||||
|
2. Use `git filter-branch` or BFG Repo-Cleaner to remove from history
|
||||||
|
3. Force push (if safe to do so)
|
||||||
|
4. Notify team and audit access
|
||||||
|
|
||||||
|
### 2. Exposing Secrets in Public Variables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# [X] WRONG
|
||||||
|
NEXT_PUBLIC_DATABASE_URL="postgresql://..." # [ERROR] Exposed to client
|
||||||
|
NEXT_PUBLIC_JWT_SECRET="..." # [ERROR] Exposed to client
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Weak Secrets
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# [X] WRONG
|
||||||
|
JWT_SECRET="secret" # [ERROR] Too weak
|
||||||
|
JWT_SECRET="password123" # [ERROR] Predictable
|
||||||
|
JWT_SECRET="myapp-secret" # [ERROR] Too short
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Copy-Pasting Between Environments
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# [X] WRONG - Same secrets in dev and prod
|
||||||
|
# .env.local
|
||||||
|
JWT_SECRET="abc123..."
|
||||||
|
|
||||||
|
# .env.production
|
||||||
|
JWT_SECRET="abc123..." # [ERROR] Should be different
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Hardcoding in Code
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// [X] WRONG
|
||||||
|
const apiKey = 'sk_live_hardcoded_key'; // [ERROR] Never hardcode
|
||||||
|
|
||||||
|
// [OK] CORRECT
|
||||||
|
const apiKey = process.env.STRIPE_SECRET_KEY;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [Next.js Environment Variables Documentation](https://nextjs.org/docs/basic-features/environment-variables)
|
||||||
|
- [OWASP Secret Management Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html)
|
||||||
|
- [12-Factor App Config](https://12factor.net/config)
|
||||||
|
- [Vercel Environment Variables](https://vercel.com/docs/concepts/projects/environment-variables)
|
||||||
453
skills/env-config-validator/scripts/validate_env.py
Normal file
453
skills/env-config-validator/scripts/validate_env.py
Normal file
@@ -0,0 +1,453 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Validate environment configuration files.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python validate_env.py
|
||||||
|
python validate_env.py --file .env.production
|
||||||
|
python validate_env.py --compare .env.local .env.production
|
||||||
|
python validate_env.py --template .env.example
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import io
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Configure stdout for UTF-8 encoding (prevents Windows encoding errors)
|
||||||
|
if sys.platform == 'win32':
|
||||||
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||||
|
from typing import Dict, List, Set, Tuple, Optional
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ValidationIssue:
|
||||||
|
"""Represents a validation issue."""
|
||||||
|
severity: str # 'error', 'warning', 'info'
|
||||||
|
category: str # 'security', 'naming', 'missing', 'weak'
|
||||||
|
variable: str
|
||||||
|
message: str
|
||||||
|
suggestion: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class EnvValidator:
|
||||||
|
"""Validate environment configuration files."""
|
||||||
|
|
||||||
|
# Required variable categories
|
||||||
|
REQUIRED_CATEGORIES = {
|
||||||
|
'database': ['DATABASE_URL', 'DB_URL', 'POSTGRES_URL', 'MONGODB_URI', 'MYSQL_URL'],
|
||||||
|
'auth': ['JWT_SECRET', 'AUTH_SECRET', 'NEXTAUTH_SECRET', 'SESSION_SECRET'],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Common secrets that should NOT be public
|
||||||
|
SECRET_PATTERNS = [
|
||||||
|
'SECRET', 'PASSWORD', 'KEY', 'TOKEN', 'CREDENTIAL',
|
||||||
|
'DATABASE', 'DB_URL', 'CONNECTION', 'PRIVATE',
|
||||||
|
]
|
||||||
|
|
||||||
|
# Weak or default values
|
||||||
|
WEAK_VALUES = [
|
||||||
|
'secret', 'password', 'test', 'example', 'changeme',
|
||||||
|
'123456', 'admin', 'default', 'your-secret-here',
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, env_file: str = '.env'):
|
||||||
|
self.env_file = Path(env_file)
|
||||||
|
self.variables: Dict[str, str] = {}
|
||||||
|
self.issues: List[ValidationIssue] = []
|
||||||
|
|
||||||
|
def load_env_file(self) -> bool:
|
||||||
|
"""Load and parse environment file."""
|
||||||
|
if not self.env_file.exists():
|
||||||
|
self.issues.append(ValidationIssue(
|
||||||
|
severity='error',
|
||||||
|
category='missing',
|
||||||
|
variable='',
|
||||||
|
message=f'Environment file not found: {self.env_file}',
|
||||||
|
suggestion='Create the file or check the path'
|
||||||
|
))
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
content = self.env_file.read_text(encoding='utf-8')
|
||||||
|
except Exception as e:
|
||||||
|
self.issues.append(ValidationIssue(
|
||||||
|
severity='error',
|
||||||
|
category='parsing',
|
||||||
|
variable='',
|
||||||
|
message=f'Failed to read file: {e}',
|
||||||
|
))
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Parse environment variables
|
||||||
|
for line in content.split('\n'):
|
||||||
|
line = line.strip()
|
||||||
|
|
||||||
|
# Skip comments and empty lines
|
||||||
|
if not line or line.startswith('#'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Parse KEY=VALUE
|
||||||
|
match = re.match(r'^([A-Z_][A-Z0-9_]*)\s*=\s*(.*)$', line)
|
||||||
|
if match:
|
||||||
|
key = match.group(1)
|
||||||
|
value = match.group(2).strip()
|
||||||
|
|
||||||
|
# Remove quotes if present
|
||||||
|
if value.startswith('"') and value.endswith('"'):
|
||||||
|
value = value[1:-1]
|
||||||
|
elif value.startswith("'") and value.endswith("'"):
|
||||||
|
value = value[1:-1]
|
||||||
|
|
||||||
|
self.variables[key] = value
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def validate(self) -> List[ValidationIssue]:
|
||||||
|
"""Run all validation checks."""
|
||||||
|
self.issues = []
|
||||||
|
|
||||||
|
if not self.load_env_file():
|
||||||
|
return self.issues
|
||||||
|
|
||||||
|
# Run validation checks
|
||||||
|
self._check_required_variables()
|
||||||
|
self._check_naming_conventions()
|
||||||
|
self._check_public_secrets()
|
||||||
|
self._check_weak_values()
|
||||||
|
self._check_secret_strength()
|
||||||
|
|
||||||
|
return self.issues
|
||||||
|
|
||||||
|
def _check_required_variables(self):
|
||||||
|
"""Check for required variables."""
|
||||||
|
for category, possible_vars in self.REQUIRED_CATEGORIES.items():
|
||||||
|
found = any(var in self.variables for var in possible_vars)
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
self.issues.append(ValidationIssue(
|
||||||
|
severity='error',
|
||||||
|
category='missing',
|
||||||
|
variable=category,
|
||||||
|
message=f'Missing required {category} configuration',
|
||||||
|
suggestion=f'Add one of: {", ".join(possible_vars)}'
|
||||||
|
))
|
||||||
|
|
||||||
|
def _check_naming_conventions(self):
|
||||||
|
"""Check variable naming conventions."""
|
||||||
|
for key in self.variables.keys():
|
||||||
|
# Check for lowercase or mixed case
|
||||||
|
if not key.isupper():
|
||||||
|
self.issues.append(ValidationIssue(
|
||||||
|
severity='warning',
|
||||||
|
category='naming',
|
||||||
|
variable=key,
|
||||||
|
message=f'Variable should use SCREAMING_SNAKE_CASE',
|
||||||
|
suggestion=f'Rename to: {key.upper()}'
|
||||||
|
))
|
||||||
|
|
||||||
|
# Check for invalid characters
|
||||||
|
if not re.match(r'^[A-Z_][A-Z0-9_]*$', key):
|
||||||
|
self.issues.append(ValidationIssue(
|
||||||
|
severity='warning',
|
||||||
|
category='naming',
|
||||||
|
variable=key,
|
||||||
|
message='Variable name contains invalid characters',
|
||||||
|
suggestion='Use only uppercase letters, numbers, and underscores'
|
||||||
|
))
|
||||||
|
|
||||||
|
def _check_public_secrets(self):
|
||||||
|
"""Check for secrets in public variables."""
|
||||||
|
for key, value in self.variables.items():
|
||||||
|
# Check if variable is public
|
||||||
|
if not key.startswith('NEXT_PUBLIC_'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check if it looks like a secret
|
||||||
|
for pattern in self.SECRET_PATTERNS:
|
||||||
|
if pattern in key.upper():
|
||||||
|
self.issues.append(ValidationIssue(
|
||||||
|
severity='error',
|
||||||
|
category='security',
|
||||||
|
variable=key,
|
||||||
|
message='Secret exposed in public variable',
|
||||||
|
suggestion=f'Remove NEXT_PUBLIC_ prefix to make it private'
|
||||||
|
))
|
||||||
|
break
|
||||||
|
|
||||||
|
# Check for database URLs in public vars
|
||||||
|
if any(db in key.upper() for db in ['DATABASE', 'DB_URL', 'POSTGRES', 'MONGODB', 'MYSQL']):
|
||||||
|
self.issues.append(ValidationIssue(
|
||||||
|
severity='error',
|
||||||
|
category='security',
|
||||||
|
variable=key,
|
||||||
|
message='Database credentials exposed in public variable',
|
||||||
|
suggestion='Remove NEXT_PUBLIC_ prefix - database URLs must be private'
|
||||||
|
))
|
||||||
|
|
||||||
|
def _check_weak_values(self):
|
||||||
|
"""Check for weak or default values."""
|
||||||
|
for key, value in self.variables.items():
|
||||||
|
value_lower = value.lower()
|
||||||
|
|
||||||
|
# Check for weak values
|
||||||
|
for weak in self.WEAK_VALUES:
|
||||||
|
if weak in value_lower:
|
||||||
|
# Only error for secrets, warn for others
|
||||||
|
severity = 'error' if any(p in key.upper() for p in self.SECRET_PATTERNS) else 'warning'
|
||||||
|
|
||||||
|
self.issues.append(ValidationIssue(
|
||||||
|
severity=severity,
|
||||||
|
category='weak',
|
||||||
|
variable=key,
|
||||||
|
message=f'Weak or default value detected',
|
||||||
|
suggestion='Use a strong, random value'
|
||||||
|
))
|
||||||
|
break
|
||||||
|
|
||||||
|
# Check for empty values in required vars
|
||||||
|
if not value:
|
||||||
|
self.issues.append(ValidationIssue(
|
||||||
|
severity='warning',
|
||||||
|
category='missing',
|
||||||
|
variable=key,
|
||||||
|
message='Variable is defined but has no value',
|
||||||
|
suggestion='Provide a value or remove the variable'
|
||||||
|
))
|
||||||
|
|
||||||
|
def _check_secret_strength(self):
|
||||||
|
"""Check strength of secret values."""
|
||||||
|
secret_keys = [
|
||||||
|
'JWT_SECRET', 'AUTH_SECRET', 'NEXTAUTH_SECRET',
|
||||||
|
'SESSION_SECRET', 'ENCRYPTION_KEY', 'SECRET_KEY'
|
||||||
|
]
|
||||||
|
|
||||||
|
for key in secret_keys:
|
||||||
|
if key not in self.variables:
|
||||||
|
continue
|
||||||
|
|
||||||
|
value = self.variables[key]
|
||||||
|
|
||||||
|
# Check minimum length (32 characters recommended)
|
||||||
|
if len(value) < 32:
|
||||||
|
self.issues.append(ValidationIssue(
|
||||||
|
severity='error',
|
||||||
|
category='weak',
|
||||||
|
variable=key,
|
||||||
|
message=f'Secret is too short ({len(value)} characters, minimum 32 recommended)',
|
||||||
|
suggestion='Generate a stronger secret with: node -e "console.log(require(\'crypto\').randomBytes(32).toString(\'hex\'))"'
|
||||||
|
))
|
||||||
|
|
||||||
|
# Check for low entropy (repeating characters, patterns)
|
||||||
|
if self._is_low_entropy(value):
|
||||||
|
self.issues.append(ValidationIssue(
|
||||||
|
severity='warning',
|
||||||
|
category='weak',
|
||||||
|
variable=key,
|
||||||
|
message='Secret appears to have low entropy',
|
||||||
|
suggestion='Use a cryptographically random value'
|
||||||
|
))
|
||||||
|
|
||||||
|
def _is_low_entropy(self, value: str) -> bool:
|
||||||
|
"""Check if a value has low entropy."""
|
||||||
|
if len(value) < 10:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check for repeating characters
|
||||||
|
unique_chars = len(set(value))
|
||||||
|
if unique_chars < len(value) / 3:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check for sequential patterns
|
||||||
|
if re.search(r'(.)\1{5,}', value): # Same char repeated 5+ times
|
||||||
|
return True
|
||||||
|
|
||||||
|
if re.search(r'(abc|123|xyz|789)', value.lower()): # Sequential patterns
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def compare_with(self, other_env_file: str) -> List[ValidationIssue]:
|
||||||
|
"""Compare with another environment file."""
|
||||||
|
other = EnvValidator(other_env_file)
|
||||||
|
other.load_env_file()
|
||||||
|
|
||||||
|
comparison_issues = []
|
||||||
|
|
||||||
|
# Variables in this file but not in other
|
||||||
|
missing_in_other = set(self.variables.keys()) - set(other.variables.keys())
|
||||||
|
for var in missing_in_other:
|
||||||
|
comparison_issues.append(ValidationIssue(
|
||||||
|
severity='warning',
|
||||||
|
category='missing',
|
||||||
|
variable=var,
|
||||||
|
message=f'Variable exists in {self.env_file} but missing in {other_env_file}',
|
||||||
|
suggestion=f'Add to {other_env_file} if required'
|
||||||
|
))
|
||||||
|
|
||||||
|
# Variables in other but not in this file
|
||||||
|
missing_in_this = set(other.variables.keys()) - set(self.variables.keys())
|
||||||
|
for var in missing_in_this:
|
||||||
|
comparison_issues.append(ValidationIssue(
|
||||||
|
severity='warning',
|
||||||
|
category='missing',
|
||||||
|
variable=var,
|
||||||
|
message=f'Variable exists in {other_env_file} but missing in {self.env_file}',
|
||||||
|
suggestion=f'Add to {self.env_file} if required'
|
||||||
|
))
|
||||||
|
|
||||||
|
# Check for same values (potential copy-paste error)
|
||||||
|
for var in set(self.variables.keys()) & set(other.variables.keys()):
|
||||||
|
# Skip checking NODE_ENV and similar environment-specific vars
|
||||||
|
skip_vars = ['NODE_ENV', 'NEXT_PUBLIC_APP_NAME']
|
||||||
|
if var in skip_vars:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if self.variables[var] == other.variables[var]:
|
||||||
|
# Only warn for secrets or URLs (should differ per environment)
|
||||||
|
if any(p in var.upper() for p in ['SECRET', 'URL', 'KEY', 'TOKEN']):
|
||||||
|
if not var.startswith('NEXT_PUBLIC_'): # Public URLs might be same
|
||||||
|
comparison_issues.append(ValidationIssue(
|
||||||
|
severity='warning',
|
||||||
|
category='security',
|
||||||
|
variable=var,
|
||||||
|
message=f'Same value in both {self.env_file} and {other_env_file}',
|
||||||
|
suggestion='Ensure environment-specific values are different'
|
||||||
|
))
|
||||||
|
|
||||||
|
return comparison_issues
|
||||||
|
|
||||||
|
def validate_against_template(self, template_file: str) -> List[ValidationIssue]:
|
||||||
|
"""Validate against a template file (.env.example)."""
|
||||||
|
template = EnvValidator(template_file)
|
||||||
|
template.load_env_file()
|
||||||
|
|
||||||
|
template_issues = []
|
||||||
|
|
||||||
|
# Check for missing required variables
|
||||||
|
for var in template.variables.keys():
|
||||||
|
if var not in self.variables:
|
||||||
|
template_issues.append(ValidationIssue(
|
||||||
|
severity='error',
|
||||||
|
category='missing',
|
||||||
|
variable=var,
|
||||||
|
message=f'Required variable (from {template_file}) is missing',
|
||||||
|
suggestion=f'Add {var} to {self.env_file}'
|
||||||
|
))
|
||||||
|
|
||||||
|
return template_issues
|
||||||
|
|
||||||
|
|
||||||
|
def format_issues(issues: List[ValidationIssue]) -> str:
|
||||||
|
"""Format issues for display."""
|
||||||
|
if not issues:
|
||||||
|
return "[OK] No issues found!"
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
|
||||||
|
# Group by severity
|
||||||
|
errors = [i for i in issues if i.severity == 'error']
|
||||||
|
warnings = [i for i in issues if i.severity == 'warning']
|
||||||
|
info = [i for i in issues if i.severity == 'info']
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
lines.append("\n=== ERRORS ===\n")
|
||||||
|
for issue in errors:
|
||||||
|
lines.append(f"[X] [{issue.category.upper()}] {issue.variable}")
|
||||||
|
lines.append(f" {issue.message}")
|
||||||
|
if issue.suggestion:
|
||||||
|
lines.append(f" [TIP] {issue.suggestion}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
if warnings:
|
||||||
|
lines.append("\n=== WARNINGS ===\n")
|
||||||
|
for issue in warnings:
|
||||||
|
lines.append(f"[WARN] [{issue.category.upper()}] {issue.variable}")
|
||||||
|
lines.append(f" {issue.message}")
|
||||||
|
if issue.suggestion:
|
||||||
|
lines.append(f" [TIP] {issue.suggestion}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
if info:
|
||||||
|
lines.append("\n=== INFO ===\n")
|
||||||
|
for issue in info:
|
||||||
|
lines.append(f"[INFO] [{issue.category.upper()}] {issue.variable}")
|
||||||
|
lines.append(f" {issue.message}")
|
||||||
|
if issue.suggestion:
|
||||||
|
lines.append(f" [TIP] {issue.suggestion}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
lines.append("\n=== SUMMARY ===")
|
||||||
|
lines.append(f"Errors: {len(errors)}")
|
||||||
|
lines.append(f"Warnings: {len(warnings)}")
|
||||||
|
lines.append(f"Info: {len(info)}")
|
||||||
|
|
||||||
|
return '\n'.join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Validate environment configuration files'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--file',
|
||||||
|
default='.env',
|
||||||
|
help='Environment file to validate (default: .env)'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--compare',
|
||||||
|
nargs=2,
|
||||||
|
metavar=('FILE1', 'FILE2'),
|
||||||
|
help='Compare two environment files'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--template',
|
||||||
|
help='Validate against a template file (.env.example)'
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Comparison mode
|
||||||
|
if args.compare:
|
||||||
|
file1, file2 = args.compare
|
||||||
|
print(f"Comparing {file1} and {file2}...\n")
|
||||||
|
|
||||||
|
validator = EnvValidator(file1)
|
||||||
|
validator.load_env_file()
|
||||||
|
|
||||||
|
comparison_issues = validator.compare_with(file2)
|
||||||
|
|
||||||
|
print(format_issues(comparison_issues))
|
||||||
|
return 1 if any(i.severity == 'error' for i in comparison_issues) else 0
|
||||||
|
|
||||||
|
# Template validation mode
|
||||||
|
if args.template:
|
||||||
|
print(f"Validating {args.file} against template {args.template}...\n")
|
||||||
|
|
||||||
|
validator = EnvValidator(args.file)
|
||||||
|
issues = validator.validate()
|
||||||
|
|
||||||
|
template_issues = validator.validate_against_template(args.template)
|
||||||
|
all_issues = issues + template_issues
|
||||||
|
|
||||||
|
print(format_issues(all_issues))
|
||||||
|
return 1 if any(i.severity == 'error' for i in all_issues) else 0
|
||||||
|
|
||||||
|
# Standard validation mode
|
||||||
|
print(f"Validating {args.file}...\n")
|
||||||
|
|
||||||
|
validator = EnvValidator(args.file)
|
||||||
|
issues = validator.validate()
|
||||||
|
|
||||||
|
print(format_issues(issues))
|
||||||
|
|
||||||
|
return 1 if any(i.severity == 'error' for i in issues) else 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
exit(main())
|
||||||
Reference in New Issue
Block a user