116 lines
3.5 KiB
TypeScript
116 lines
3.5 KiB
TypeScript
const MIN_PORT = 1;
|
|
const MIN_REQUESTS_PER_SECOND = 1;
|
|
|
|
const DEFAULT_PORT = 3000;
|
|
const DEFAULT_TBANK_RATE_LIMIT_PER_SECOND = 5;
|
|
const MAX_PORT = 65_535;
|
|
const MIN_JWT_SECRET_LENGTH = 32;
|
|
const TOKEN_KEY_BYTES = 32;
|
|
export interface Environment {
|
|
DATABASE_URL: string;
|
|
FRONTEND_ORIGIN: string;
|
|
JWT_SECRET: string;
|
|
LOG_LEVEL: 'debug' | 'error' | 'info' | 'warn';
|
|
PORT: number;
|
|
TBANK_CA_CERT_PATH?: string;
|
|
TBANK_GRPC_ENDPOINT: string;
|
|
TBANK_RATE_LIMIT_PER_SECOND: number;
|
|
TOKEN_ENCRYPTION_KEY: string;
|
|
}
|
|
|
|
export function validateEnv(config: Record<string, unknown>): Environment {
|
|
const databaseUrl = config['DATABASE_URL'];
|
|
const frontendOrigin = config['FRONTEND_ORIGIN'];
|
|
const port = config['PORT'] ?? DEFAULT_PORT;
|
|
const jwtSecret = config['JWT_SECRET'];
|
|
const tokenEncryptionKey = config['TOKEN_ENCRYPTION_KEY'];
|
|
const tbankEndpoint =
|
|
config['TBANK_GRPC_ENDPOINT'] ?? 'invest-public-api.tbank.ru:443';
|
|
const tbankRateLimit =
|
|
config['TBANK_RATE_LIMIT_PER_SECOND'] ??
|
|
DEFAULT_TBANK_RATE_LIMIT_PER_SECOND;
|
|
const tbankCaCertPath = config['TBANK_CA_CERT_PATH'];
|
|
const logLevel = config['LOG_LEVEL'] ?? 'info';
|
|
|
|
if (typeof databaseUrl !== 'string' || !databaseUrl.startsWith('file:')) {
|
|
throw new Error('DATABASE_URL must be a SQLite file URL');
|
|
}
|
|
|
|
if (typeof frontendOrigin !== 'string') {
|
|
throw new Error('FRONTEND_ORIGIN must be a valid HTTP origin');
|
|
}
|
|
|
|
let parsedOrigin: URL;
|
|
try {
|
|
parsedOrigin = new URL(frontendOrigin);
|
|
} catch {
|
|
throw new Error('FRONTEND_ORIGIN must be a valid HTTP origin');
|
|
}
|
|
|
|
if (
|
|
(parsedOrigin.protocol !== 'http:' && parsedOrigin.protocol !== 'https:') ||
|
|
parsedOrigin.pathname !== '/' ||
|
|
parsedOrigin.search ||
|
|
parsedOrigin.hash
|
|
) {
|
|
throw new Error('FRONTEND_ORIGIN must be a valid HTTP origin');
|
|
}
|
|
|
|
const parsedPort = Number(port);
|
|
if (
|
|
!Number.isInteger(parsedPort) ||
|
|
parsedPort < MIN_PORT ||
|
|
parsedPort > MAX_PORT
|
|
) {
|
|
throw new Error('PORT must be a valid port number');
|
|
}
|
|
|
|
if (
|
|
typeof jwtSecret !== 'string' ||
|
|
jwtSecret.length < MIN_JWT_SECRET_LENGTH
|
|
) {
|
|
throw new Error('JWT_SECRET must be at least 32 characters long');
|
|
}
|
|
|
|
if (
|
|
typeof tokenEncryptionKey !== 'string' ||
|
|
!/^[A-Z0-9+/]+={0,2}$/i.test(tokenEncryptionKey) ||
|
|
Buffer.from(tokenEncryptionKey, 'base64').length !== TOKEN_KEY_BYTES
|
|
) {
|
|
throw new Error(
|
|
'TOKEN_ENCRYPTION_KEY must be a base64-encoded 32-byte key',
|
|
);
|
|
}
|
|
|
|
if (typeof tbankEndpoint !== 'string' || !tbankEndpoint) {
|
|
throw new Error('TBANK_GRPC_ENDPOINT must be a non-empty endpoint');
|
|
}
|
|
|
|
const parsedTbankRateLimit = Number(tbankRateLimit);
|
|
if (!Number.isInteger(parsedTbankRateLimit) || parsedTbankRateLimit < MIN_REQUESTS_PER_SECOND) {
|
|
throw new Error('TBANK_RATE_LIMIT_PER_SECOND must be a positive integer');
|
|
}
|
|
|
|
if (tbankCaCertPath !== undefined && typeof tbankCaCertPath !== 'string') {
|
|
throw new Error('TBANK_CA_CERT_PATH must be a file path');
|
|
}
|
|
|
|
if (!['debug', 'error', 'info', 'warn'].includes(logLevel as string)) {
|
|
throw new Error('LOG_LEVEL must be debug, info, warn, or error');
|
|
}
|
|
|
|
return {
|
|
DATABASE_URL: databaseUrl,
|
|
FRONTEND_ORIGIN: parsedOrigin.origin,
|
|
JWT_SECRET: jwtSecret,
|
|
PORT: parsedPort,
|
|
TOKEN_ENCRYPTION_KEY: tokenEncryptionKey,
|
|
...(typeof tbankCaCertPath === 'string' && tbankCaCertPath
|
|
? { TBANK_CA_CERT_PATH: tbankCaCertPath }
|
|
: {}),
|
|
TBANK_GRPC_ENDPOINT: tbankEndpoint,
|
|
TBANK_RATE_LIMIT_PER_SECOND: parsedTbankRateLimit,
|
|
LOG_LEVEL: logLevel as Environment['LOG_LEVEL'],
|
|
};
|
|
}
|