File size: 4,627 Bytes
e31f25c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
/**
 * Environment variable validation.
 * Import this at the top of your app to get clear startup errors
 * instead of cryptic runtime failures.
 * 
 * Usage: import '@/lib/env' in layout.tsx or next.config.ts
 */

type EnvVar = {
    name: string
    required: boolean
    description: string
}

const ENV_VARS: EnvVar[] = [
    // Required
    { name: 'DATABASE_URL', required: true, description: 'PostgreSQL connection string (Neon)' },
    { name: 'NEXT_PUBLIC_STACK_PROJECT_ID', required: true, description: 'Stack Auth project ID' },
    { name: 'NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY', required: true, description: 'Stack Auth publishable key' },
    { name: 'STACK_SECRET_SERVER_KEY', required: true, description: 'Stack Auth server secret key' },

    // AI Providers (at least one required)
    { name: 'OPENAI_API_KEY', required: false, description: 'OpenAI API key (for GPT-4o models)' },
    { name: 'ANTHROPIC_API_KEY', required: false, description: 'Anthropic API key (for Claude models)' },
    { name: 'GOOGLE_AI_API_KEY', required: false, description: 'Google AI API key (for Gemini models)' },

    // Optional services
    { name: 'UPSTASH_REDIS_REST_URL', required: false, description: 'Upstash Redis URL (caching & rate limiting)' },
    { name: 'UPSTASH_REDIS_REST_TOKEN', required: false, description: 'Upstash Redis auth token' },
    { name: 'NEXT_PUBLIC_TURNSTILE_SITE_KEY', required: false, description: 'Cloudflare Turnstile site key (bot protection)' },
    { name: 'TURNSTILE_SECRET_KEY', required: false, description: 'Cloudflare Turnstile secret key' },
    { name: 'STRIPE_SECRET_KEY', required: false, description: 'Stripe secret key (payments)' },
    { name: 'STRIPE_WEBHOOK_SECRET', required: false, description: 'Stripe webhook signing secret' },
    { name: 'NEXT_PUBLIC_APP_URL', required: false, description: 'Public URL of the app (defaults to open-prompt.netlify.app)' },
]

export function validateEnv(): { valid: boolean; errors: string[]; warnings: string[] } {
    const errors: string[] = []
    const warnings: string[] = []

    for (const envVar of ENV_VARS) {
        const value = process.env[envVar.name]

        if (envVar.required && !value) {
            errors.push(`❌ Missing required: ${envVar.name}${envVar.description}`)
        } else if (!envVar.required && !value) {
            warnings.push(`⚠️  Optional missing: ${envVar.name}${envVar.description}`)
        }
    }

    // Check that at least one AI provider is configured
    const hasAiProvider = !!(
        process.env.OPENAI_API_KEY ||
        process.env.ANTHROPIC_API_KEY ||
        process.env.GOOGLE_AI_API_KEY
    )

    if (!hasAiProvider) {
        errors.push('❌ No AI provider configured. Set at least one of: OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_AI_API_KEY')
    }

    // Check Redis pair (both or neither)
    const hasRedisUrl = !!process.env.UPSTASH_REDIS_REST_URL
    const hasRedisToken = !!process.env.UPSTASH_REDIS_REST_TOKEN
    if (hasRedisUrl !== hasRedisToken) {
        warnings.push('⚠️  Redis partially configured. Set both UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN, or neither.')
    }

    // Check Turnstile pair
    const hasTurnstileSite = !!process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY
    const hasTurnstileSecret = !!process.env.TURNSTILE_SECRET_KEY
    if (hasTurnstileSite !== hasTurnstileSecret) {
        warnings.push('⚠️  Turnstile partially configured. Set both NEXT_PUBLIC_TURNSTILE_SITE_KEY and TURNSTILE_SECRET_KEY, or neither.')
    }

    return {
        valid: errors.length === 0,
        errors,
        warnings,
    }
}

/**
 * Log environment validation results during startup.
 * Call this in development to catch issues early.
 */
export function logEnvValidation(): void {
    // Only validate in development or on first build
    if (process.env.NODE_ENV === 'production' && process.env.NEXT_PHASE !== 'phase-production-build') {
        return
    }

    const { valid, errors, warnings } = validateEnv()

    if (errors.length > 0) {
        console.error('\n🔴 Environment Validation Failed:\n')
        errors.forEach(e => console.error(`  ${e}`))
        console.error('')
    }

    if (warnings.length > 0 && process.env.NODE_ENV === 'development') {
        console.warn('\n🟡 Environment Warnings:\n')
        warnings.forEach(w => console.warn(`  ${w}`))
        console.warn('')
    }

    if (valid && process.env.NODE_ENV === 'development') {
        console.log('\n✅ Environment validation passed\n')
    }
}

// Auto-run validation when this module is imported
logEnvValidation()