Files
2026-01-16 19:04:48 +02:00

244 lines
7.3 KiB
TypeScript

#!/usr/bin/env tsx
/**
* Analyze .env.local file for missing variables, misconfigurations, and security issues
*/
import { readFileSync } from 'fs';
import { join } from 'path';
const envPath = join(process.cwd(), '.env.local');
interface EnvAnalysis {
variable: string;
status: 'present' | 'missing' | 'empty' | 'commented';
value?: string;
required: boolean;
category: string;
issues: string[];
recommendations: string[];
}
const requiredVars = [
'NODE_ENV',
'NEXT_PUBLIC_APP_URL',
'ADMIN_PASSWORD',
'SESSION_SECRET',
];
const recommendedVars = [
'DATABASE_URL',
'RUNTIME_DATABASE_URL',
'NEXT_PUBLIC_SUPABASE_URL',
'NEXT_PUBLIC_SUPABASE_ANON_KEY',
'CSRF_SECRET',
'RESEND_API_KEY',
'FROM_EMAIL',
'REDIS_URL',
];
const optionalVars = [
'RESEND_WEBHOOK_SECRET',
'SUPABASE_SERVICE_ROLE_KEY',
'SHADOW_DATABASE_URL',
'APPLE_MUSIC_DEVELOPER_TOKEN',
'NEXT_PUBLIC_SENTRY_DSN',
'NEXT_PUBLIC_GA_MEASUREMENT_ID',
'OPENWEATHER_API_KEY',
'OLLAMA_URL',
'OLLAMA_MODEL',
'OLLAMA_TEMPERATURE',
'SMTP_HOST',
'SMTP_PORT',
'SMTP_USER',
'SMTP_PASS',
'TWILIO_ACCOUNT_SID',
'TWILIO_AUTH_TOKEN',
'TWILIO_PHONE_NUMBER',
];
function analyzeEnvFile(): void {
let envContent: string;
try {
envContent = readFileSync(envPath, 'utf-8');
} catch (error) {
console.error('❌ Could not read .env.local file');
process.exit(1);
}
const lines = envContent.split('\n');
const envVars: Map<string, { value: string; line: number }> = new Map();
const commentedVars: Set<string> = new Set();
lines.forEach((line, index) => {
const trimmed = line.trim();
if (trimmed.startsWith('#') || !trimmed) return;
const match = trimmed.match(/^([A-Z_]+)=(.*)$/);
if (match) {
const [, key, value] = match;
envVars.set(key, { value: value.replace(/^["']|["']$/g, ''), line: index + 1 });
}
// Check for commented variables
const commentedMatch = trimmed.match(/^#\s*([A-Z_]+)=/);
if (commentedMatch) {
commentedVars.add(commentedMatch[1]);
}
});
const analysis: EnvAnalysis[] = [];
const allVars = [...requiredVars, ...recommendedVars, ...optionalVars];
allVars.forEach(varName => {
const entry = envVars.get(varName);
const isCommented = commentedVars.has(varName);
let status: EnvAnalysis['status'];
let issues: string[] = [];
let recommendations: string[] = [];
if (isCommented) {
status = 'commented';
recommendations.push(`Uncomment ${varName} if you need it`);
} else if (!entry) {
status = 'missing';
if (requiredVars.includes(varName)) {
issues.push('Required variable is missing');
} else if (recommendedVars.includes(varName)) {
recommendations.push(`Consider adding ${varName} for full functionality`);
}
} else if (!entry.value || entry.value.trim() === '') {
status = 'empty';
if (requiredVars.includes(varName)) {
issues.push('Required variable is empty');
} else {
recommendations.push(`Set ${varName} if you need this feature`);
}
} else {
status = 'present';
// Validate specific variables
if (varName === 'DATABASE_URL' && !entry.value.includes('supabase')) {
recommendations.push('DATABASE_URL should point to Supabase PostgreSQL');
}
if (varName === 'NEXT_PUBLIC_APP_URL' && !entry.value.startsWith('https://')) {
recommendations.push('Use HTTPS for production URLs');
}
if (varName === 'SESSION_SECRET' && entry.value.length < 32) {
issues.push('SESSION_SECRET should be at least 32 characters');
}
if (varName === 'ADMIN_PASSWORD' && entry.value.length < 8) {
issues.push('ADMIN_PASSWORD should be at least 8 characters');
}
if (varName === 'RESEND_API_KEY' && !entry.value.startsWith('re_')) {
issues.push('RESEND_API_KEY format looks incorrect (should start with "re_")');
}
if (varName === 'FROM_EMAIL' && !entry.value.includes('@')) {
issues.push('FROM_EMAIL should be a valid email address');
}
}
analysis.push({
variable: varName,
status,
value: entry?.value,
required: requiredVars.includes(varName),
category: requiredVars.includes(varName) ? 'Required' :
recommendedVars.includes(varName) ? 'Recommended' : 'Optional',
issues,
recommendations,
});
});
// Print analysis
console.log('📋 Environment Variables Analysis\n');
console.log('='.repeat(80));
const categories = ['Required', 'Recommended', 'Optional'];
categories.forEach(category => {
const vars = analysis.filter(a => a.category === category);
if (vars.length === 0) return;
console.log(`\n## ${category} Variables\n`);
vars.forEach(item => {
const icon = item.status === 'present' ? '✅' :
item.status === 'missing' && item.required ? '❌' :
item.status === 'empty' && item.required ? '⚠️' :
item.status === 'missing' ? '⚪' : '💬';
console.log(`${icon} ${item.variable}`);
if (item.status === 'present') {
const displayValue = item.value && item.value.length > 50
? item.value.substring(0, 50) + '...'
: item.value;
console.log(` Value: ${displayValue || '(empty)'}`);
} else if (item.status === 'missing') {
console.log(` Status: Missing`);
} else if (item.status === 'empty') {
console.log(` Status: Empty`);
} else {
console.log(` Status: Commented out`);
}
if (item.issues.length > 0) {
item.issues.forEach(issue => {
console.log(` ⚠️ Issue: ${issue}`);
});
}
if (item.recommendations.length > 0) {
item.recommendations.forEach(rec => {
console.log(` 💡 ${rec}`);
});
}
});
});
// Summary
console.log('\n' + '='.repeat(80));
console.log('\n📊 Summary\n');
const present = analysis.filter(a => a.status === 'present').length;
const missing = analysis.filter(a => a.status === 'missing').length;
const empty = analysis.filter(a => a.status === 'empty').length;
const commented = analysis.filter(a => a.status === 'commented').length;
const hasIssues = analysis.filter(a => a.issues.length > 0).length;
console.log(`Total variables checked: ${analysis.length}`);
console.log(`✅ Present: ${present}`);
console.log(`❌ Missing: ${missing}`);
console.log(`⚠️ Empty: ${empty}`);
console.log(`💬 Commented: ${commented}`);
console.log(`🔴 Has issues: ${hasIssues}`);
// Critical missing variables
const criticalMissing = analysis.filter(a =>
a.required && (a.status === 'missing' || a.status === 'empty')
);
if (criticalMissing.length > 0) {
console.log('\n🚨 Critical Issues:\n');
criticalMissing.forEach(item => {
console.log(`${item.variable} is ${item.status} but required`);
});
}
// Recommendations
const missingRecommended = analysis.filter(a =>
a.category === 'Recommended' && a.status === 'missing'
);
if (missingRecommended.length > 0) {
console.log('\n💡 Recommendations:\n');
missingRecommended.forEach(item => {
console.log(` • Add ${item.variable} for better functionality`);
});
}
console.log('\n' + '='.repeat(80));
}
analyzeEnvFile();