Files
cryptowallet/apps/api/src/config/env.ts
ZOMBIIIIIII 762a46871b init2222
2026-05-13 12:07:48 +03:00

162 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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'),
redis: {
host: p.REDIS_HOST || 'keydb',
port: parseInt(p.REDIS_PORT || '6379'),
password: p.REDIS_PASSWORD || '',
db: parseInt(p.REDIS_DB || '0'),
},
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<void> {
// 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}"`);
}