import dotenv from 'dotenv'; import path from 'path'; import { vaultAppRoleLogin, fetchVaultKV2 } from './vault'; import { logger } from '../lib/logger'; dotenv.config({ path: path.resolve(__dirname, '../../../../.env') }); const p = process.env; export let env = { db: { host: p.DB_HOST || '', port: parseInt(p.DB_PORT || '5432'), user: p.DB_USER || '', password: p.DB_PASSWORD || '', name: p.DB_NAME || '', }, jwt: { algorithm: p.JWT_ALGORITHM || 'RS256', // Намеренно без default — каждый деплой ЯВНО указывает iss/aud, иначе сервис // примет любой токен подписанный нашими ключами с любым iss/aud. issuer: p.JWT_ISSUER || '', audience: p.JWT_AUDIENCE || '', }, vault: { addr: p.VAULT_ADDR || '', roleId: p.VAULT_ROLE_ID || '', secretId: p.VAULT_SECRET_ID || '', mount: p.VAULT_MOUNT_POINT || 'dev-secrets', secretPath: p.VAULT_SECRET_PATH || 'database', jwtKidPath: p.VAULT_JWT_KID_PATH || 'jwt/kid', jwtKidsPrefix: p.VAULT_JWT_KIDS_PREFIX || 'jwt/kids', csrfPath: p.VAULT_CSRF_PATH || '', cryptoKeyPath: p.VAULT_CRYPTO_KEY_PATH || 'crypto/master', }, cors: { // Каждый деплой явно указывает CORS_ORIGINS, иначе пустой массив = нет cross-origin. // Validate каждый origin через URL parse — отклоняем мусор. Default https-only для prod-safety. origins: (p.CORS_ORIGINS || '') .split(',') .map((o) => o.trim()) .filter(Boolean) .filter((o) => { try { const u = new URL(o); return u.protocol === 'https:' || u.protocol === 'http:'; } catch { return false; } }), // Default = false (fail-secure). Чтобы включить credentials cross-origin — // ОБЯЗАТЕЛЬНО явный CORS_ALLOW_CREDENTIALS=true. allowCredentials: p.CORS_ALLOW_CREDENTIALS === 'true', }, port: parseInt(p.API_PORT || '3001'), relayApiKey: p.RELAY_API_KEY || null, tronApiKey: p.TRON_API_KEY || null, jupiterApiKey: p.JUPITER_API_KEY || null, jupiterReferralAccount: p.JUPITER_REFERRAL_ACCOUNT || null, jupiterFeeBps: parseInt(p.JUPITER_FEE_BPS || '70'), etherscanApiKey: p.ETHERSCAN_API_KEY || null, bscscanApiKey: p.BSCSCAN_API_KEY || null, }; let vaultToken: string | null = null; export function getVaultToken(): string | null { return vaultToken; } export async function initEnv(): Promise { // Fail-fast на отсутствующие критические env vars if (!env.jwt.issuer || !env.jwt.audience) { throw new Error('JWT_ISSUER and JWT_AUDIENCE must be explicitly set (no defaults)'); } const { addr, roleId, secretId, mount, secretPath } = env.vault; if (!addr || !roleId || !secretId) { logger.info('Vault not configured, using .env'); return; } // H7 — HTTPS-only Vault enforce. Plaintext HTTP means master-key + AppRole secret_id // travel through WAN unencrypted. Override via VAULT_ALLOW_INSECURE=true (only для local dev). try { const parsed = new URL(addr); if (parsed.protocol !== 'https:' && p.VAULT_ALLOW_INSECURE !== 'true') { throw new Error(`VAULT_ADDR must use https:// (got ${parsed.protocol}). Set VAULT_ALLOW_INSECURE=true only for local dev.`); } } catch (err: any) { if (err.message?.includes('Invalid URL')) { throw new Error(`VAULT_ADDR is malformed: ${addr}`); } throw err; } const token = await vaultAppRoleLogin(addr, roleId, secretId); if (!token) { logger.warn('Vault AppRole login failed, using .env fallback'); return; } vaultToken = token; logger.info('Vault AppRole login successful'); const secrets = await fetchVaultKV2(addr, token, mount, secretPath); if (!secrets) { logger.warn('Failed to read DB secrets from Vault'); return; } logger.info('Loaded DB secrets from Vault'); const s = (key: string) => secrets[key]; const si = (key: string, fallback: number) => { const v = secrets[key]; return v ? parseInt(v) : fallback; }; // Vault stores DB secrets in lowercase (host, user, password, name, port). // Accept uppercase DB_* as fallback for compatibility. env = { ...env, db: { host: s('host') || s('DB_HOST') || env.db.host, port: si('port', si('DB_PORT', env.db.port)), user: s('user') || s('DB_USER') || env.db.user, password: s('password') || s('DB_PASSWORD') || env.db.password, name: s('name') || s('DB_NAME') || env.db.name, }, jwt: { ...env.jwt, // H17 — trim whitespace; пустая строка после trim → fallback на env issuer: (s('JWT_ISSUER')?.trim() || env.jwt.issuer), audience: (s('JWT_AUDIENCE')?.trim() || env.jwt.audience), }, cors: { origins: s('CORS_ORIGINS') ? s('CORS_ORIGINS')!.split(',') : env.cors.origins, // H5 — fail-secure consistent with line 53: explicit 'true' required, default false. // Раньше: `!== 'false'` → defaults TRUE if field missing/empty (security inversion). allowCredentials: secrets['CORS_ALLOW_CREDENTIALS'] === 'true', }, relayApiKey: s('RELAY_API_KEY') || env.relayApiKey, tronApiKey: s('TRON_API_KEY') || env.tronApiKey, jupiterApiKey: s('JUPITER_API_KEY') || env.jupiterApiKey, }; // Re-validate after Vault load. Vault мог переписать iss/aud — если они теперь пустые // или невалидные, fail-fast. if (!env.jwt.issuer || !env.jwt.audience) { throw new Error('JWT_ISSUER and JWT_AUDIENCE became empty after Vault load'); } logger.info(`JWT validation: iss="${env.jwt.issuer}", aud="${env.jwt.audience}"`); }