UNPKG

@spfn/core

Version:

SPFN Framework Core - File-based routing, transactions, repository pattern

1,106 lines (1,095 loc) 31.1 kB
import { config } from 'dotenv'; import { existsSync, mkdirSync, accessSync, constants, writeFileSync, unlinkSync, createWriteStream, statSync, readdirSync, renameSync } from 'fs'; import { join } from 'path'; import pino from 'pino'; // src/env/loader.ts var PinoAdapter = class _PinoAdapter { logger; constructor(config) { this.logger = pino({ level: config.level, // 기본 필드 base: config.module ? { module: config.module } : void 0 }); } child(module) { const childLogger = new _PinoAdapter({ level: this.logger.level, module }); childLogger.logger = this.logger.child({ module }); return childLogger; } debug(message, context) { this.logger.debug(context || {}, message); } info(message, context) { this.logger.info(context || {}, message); } warn(message, errorOrContext, context) { if (errorOrContext instanceof Error) { this.logger.warn({ err: errorOrContext, ...context }, message); } else { this.logger.warn(errorOrContext || {}, message); } } error(message, errorOrContext, context) { if (errorOrContext instanceof Error) { this.logger.error({ err: errorOrContext, ...context }, message); } else { this.logger.error(errorOrContext || {}, message); } } fatal(message, errorOrContext, context) { if (errorOrContext instanceof Error) { this.logger.fatal({ err: errorOrContext, ...context }, message); } else { this.logger.fatal(errorOrContext || {}, message); } } async close() { } }; // src/logger/types.ts var LOG_LEVEL_PRIORITY = { debug: 0, info: 1, warn: 2, error: 3, fatal: 4 }; // src/logger/formatters.ts var SENSITIVE_KEYS = [ "password", "passwd", "pwd", "secret", "token", "apikey", "api_key", "accesstoken", "access_token", "refreshtoken", "refresh_token", "authorization", "auth", "cookie", "session", "sessionid", "session_id", "privatekey", "private_key", "creditcard", "credit_card", "cardnumber", "card_number", "cvv", "ssn", "pin" ]; var MASKED_VALUE = "***MASKED***"; function isSensitiveKey(key) { const lowerKey = key.toLowerCase(); return SENSITIVE_KEYS.some((sensitive) => lowerKey.includes(sensitive)); } function maskSensitiveData(data) { if (data === null || data === void 0) { return data; } if (Array.isArray(data)) { return data.map((item) => maskSensitiveData(item)); } if (typeof data === "object") { const masked = {}; for (const [key, value] of Object.entries(data)) { if (isSensitiveKey(key)) { masked[key] = MASKED_VALUE; } else if (typeof value === "object" && value !== null) { masked[key] = maskSensitiveData(value); } else { masked[key] = value; } } return masked; } return data; } var COLORS = { reset: "\x1B[0m", bright: "\x1B[1m", dim: "\x1B[2m", // 로그 레벨 컬러 debug: "\x1B[36m", // cyan info: "\x1B[32m", // green warn: "\x1B[33m", // yellow error: "\x1B[31m", // red fatal: "\x1B[35m", // magenta // 추가 컬러 gray: "\x1B[90m" }; function formatTimestamp(date) { return date.toISOString(); } function formatTimestampHuman(date) { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); const hours = String(date.getHours()).padStart(2, "0"); const minutes = String(date.getMinutes()).padStart(2, "0"); const seconds = String(date.getSeconds()).padStart(2, "0"); const ms = String(date.getMilliseconds()).padStart(3, "0"); return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${ms}`; } function formatError(error) { const lines = []; lines.push(`${error.name}: ${error.message}`); if (error.stack) { const stackLines = error.stack.split("\n").slice(1); lines.push(...stackLines); } return lines.join("\n"); } function formatConsole(metadata, colorize = true) { const parts = []; const timestamp = formatTimestampHuman(metadata.timestamp); if (colorize) { parts.push(`${COLORS.gray}[${timestamp}]${COLORS.reset}`); } else { parts.push(`[${timestamp}]`); } if (metadata.module) { if (colorize) { parts.push(`${COLORS.dim}[module=${metadata.module}]${COLORS.reset}`); } else { parts.push(`[module=${metadata.module}]`); } } if (metadata.context && Object.keys(metadata.context).length > 0) { Object.entries(metadata.context).forEach(([key, value]) => { const valueStr = typeof value === "string" ? value : String(value); if (colorize) { parts.push(`${COLORS.dim}[${key}=${valueStr}]${COLORS.reset}`); } else { parts.push(`[${key}=${valueStr}]`); } }); } const levelStr = metadata.level.toUpperCase(); if (colorize) { const color = COLORS[metadata.level]; parts.push(`${color}(${levelStr})${COLORS.reset}:`); } else { parts.push(`(${levelStr}):`); } if (colorize) { parts.push(`${COLORS.bright}${metadata.message}${COLORS.reset}`); } else { parts.push(metadata.message); } let output = parts.join(" "); if (metadata.error) { output += "\n" + formatError(metadata.error); } return output; } function formatJSON(metadata) { const obj = { timestamp: formatTimestamp(metadata.timestamp), level: metadata.level, message: metadata.message }; if (metadata.module) { obj.module = metadata.module; } if (metadata.context) { obj.context = metadata.context; } if (metadata.error) { obj.error = { name: metadata.error.name, message: metadata.error.message, stack: metadata.error.stack }; } return JSON.stringify(obj); } // src/logger/logger.ts var Logger = class _Logger { config; module; constructor(config) { this.config = config; this.module = config.module; } /** * Get current log level */ get level() { return this.config.level; } /** * Create child logger (per module) */ child(module) { return new _Logger({ ...this.config, module }); } /** * Debug log */ debug(message, context) { this.log("debug", message, void 0, context); } /** * Info log */ info(message, context) { this.log("info", message, void 0, context); } warn(message, errorOrContext, context) { if (errorOrContext instanceof Error) { this.log("warn", message, errorOrContext, context); } else { this.log("warn", message, void 0, errorOrContext); } } error(message, errorOrContext, context) { if (errorOrContext instanceof Error) { this.log("error", message, errorOrContext, context); } else { this.log("error", message, void 0, errorOrContext); } } fatal(message, errorOrContext, context) { if (errorOrContext instanceof Error) { this.log("fatal", message, errorOrContext, context); } else { this.log("fatal", message, void 0, errorOrContext); } } /** * Log processing (internal) */ log(level, message, error, context) { if (LOG_LEVEL_PRIORITY[level] < LOG_LEVEL_PRIORITY[this.config.level]) { return; } const metadata = { timestamp: /* @__PURE__ */ new Date(), level, message, module: this.module, error, // Mask sensitive information in context to prevent credential leaks context: context ? maskSensitiveData(context) : void 0 }; this.processTransports(metadata); } /** * Process Transports */ processTransports(metadata) { const promises = this.config.transports.filter((transport) => transport.enabled).map((transport) => this.safeTransportLog(transport, metadata)); Promise.all(promises).catch((error) => { const errorMessage = error instanceof Error ? error.message : String(error); process.stderr.write(`[Logger] Transport error: ${errorMessage} `); }); } /** * Transport log (error-safe) */ async safeTransportLog(transport, metadata) { try { await transport.log(metadata); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); process.stderr.write(`[Logger] Transport "${transport.name}" failed: ${errorMessage} `); } } /** * Close all Transports */ async close() { const closePromises = this.config.transports.filter((transport) => transport.close).map((transport) => transport.close()); await Promise.all(closePromises); } }; // src/logger/transports/console.ts var ConsoleTransport = class { name = "console"; level; enabled; colorize; constructor(config) { this.level = config.level; this.enabled = config.enabled; this.colorize = config.colorize ?? true; } async log(metadata) { if (!this.enabled) { return; } if (LOG_LEVEL_PRIORITY[metadata.level] < LOG_LEVEL_PRIORITY[this.level]) { return; } const message = formatConsole(metadata, this.colorize); if (metadata.level === "warn" || metadata.level === "error" || metadata.level === "fatal") { console.error(message); } else { console.log(message); } } }; var FileTransport = class { name = "file"; level; enabled; logDir; maxFileSize; maxFiles; currentStream = null; currentFilename = null; constructor(config) { this.level = config.level; this.enabled = config.enabled; this.logDir = config.logDir; this.maxFileSize = config.maxFileSize ?? 10 * 1024 * 1024; this.maxFiles = config.maxFiles ?? 10; if (!existsSync(this.logDir)) { mkdirSync(this.logDir, { recursive: true }); } } async log(metadata) { if (!this.enabled) { return; } if (LOG_LEVEL_PRIORITY[metadata.level] < LOG_LEVEL_PRIORITY[this.level]) { return; } const message = formatJSON(metadata); const filename = this.getLogFilename(metadata.timestamp); if (this.currentFilename !== filename) { await this.rotateStream(filename); await this.cleanOldFiles(); } else if (this.currentFilename) { await this.checkAndRotateBySize(); } if (this.currentStream) { return new Promise((resolve, reject) => { this.currentStream.write(message + "\n", "utf-8", (error) => { if (error) { process.stderr.write(`[FileTransport] Failed to write log: ${error.message} `); reject(error); } else { resolve(); } }); }); } } /** * 스트림 교체 (날짜 변경 시) */ async rotateStream(filename) { if (this.currentStream) { await this.closeStream(); } const filepath = join(this.logDir, filename); this.currentStream = createWriteStream(filepath, { flags: "a", // append mode encoding: "utf-8" }); this.currentFilename = filename; this.currentStream.on("error", (error) => { process.stderr.write(`[FileTransport] Stream error: ${error.message} `); this.currentStream = null; this.currentFilename = null; }); } /** * 현재 스트림 닫기 */ async closeStream() { if (!this.currentStream) { return; } return new Promise((resolve, reject) => { this.currentStream.end((error) => { if (error) { reject(error); } else { this.currentStream = null; this.currentFilename = null; resolve(); } }); }); } /** * 파일 크기 체크 및 크기 기반 로테이션 */ async checkAndRotateBySize() { if (!this.currentFilename) { return; } const filepath = join(this.logDir, this.currentFilename); if (!existsSync(filepath)) { return; } try { const stats = statSync(filepath); if (stats.size >= this.maxFileSize) { await this.rotateBySize(); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); process.stderr.write(`[FileTransport] Failed to check file size: ${errorMessage} `); } } /** * 크기 기반 로테이션 수행 * 예: 2025-01-01.log -> 2025-01-01.1.log, 2025-01-01.1.log -> 2025-01-01.2.log */ async rotateBySize() { if (!this.currentFilename) { return; } await this.closeStream(); const baseName = this.currentFilename.replace(/\.log$/, ""); const files = readdirSync(this.logDir); const relatedFiles = files.filter((file) => file.startsWith(baseName) && file.endsWith(".log")).sort().reverse(); for (const file of relatedFiles) { const match = file.match(/\.(\d+)\.log$/); if (match) { const oldNum = parseInt(match[1], 10); const newNum = oldNum + 1; const oldPath = join(this.logDir, file); const newPath2 = join(this.logDir, `${baseName}.${newNum}.log`); try { renameSync(oldPath, newPath2); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); process.stderr.write(`[FileTransport] Failed to rotate file: ${errorMessage} `); } } } const currentPath = join(this.logDir, this.currentFilename); const newPath = join(this.logDir, `${baseName}.1.log`); try { if (existsSync(currentPath)) { renameSync(currentPath, newPath); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); process.stderr.write(`[FileTransport] Failed to rotate current file: ${errorMessage} `); } await this.rotateStream(this.currentFilename); } /** * 오래된 로그 파일 정리 * maxFiles 개수를 초과하는 로그 파일 삭제 */ async cleanOldFiles() { try { if (!existsSync(this.logDir)) { return; } const files = readdirSync(this.logDir); const logFiles = files.filter((file) => file.endsWith(".log")).map((file) => { const filepath = join(this.logDir, file); const stats = statSync(filepath); return { file, mtime: stats.mtime }; }).sort((a, b) => b.mtime.getTime() - a.mtime.getTime()); if (logFiles.length > this.maxFiles) { const filesToDelete = logFiles.slice(this.maxFiles); for (const { file } of filesToDelete) { const filepath = join(this.logDir, file); try { unlinkSync(filepath); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); process.stderr.write(`[FileTransport] Failed to delete old file "${file}": ${errorMessage} `); } } } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); process.stderr.write(`[FileTransport] Failed to clean old files: ${errorMessage} `); } } /** * 날짜별 로그 파일명 생성 */ getLogFilename(date) { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); return `${year}-${month}-${day}.log`; } async close() { await this.closeStream(); } }; function isFileLoggingEnabled() { return process.env.LOGGER_FILE_ENABLED === "true"; } function getDefaultLogLevel() { const isProduction = process.env.NODE_ENV === "production"; const isDevelopment = process.env.NODE_ENV === "development"; if (isDevelopment) { return "debug"; } if (isProduction) { return "info"; } return "warn"; } function getConsoleConfig() { const isProduction = process.env.NODE_ENV === "production"; return { level: "debug", enabled: true, colorize: !isProduction // Dev: colored output, Production: plain text }; } function getFileConfig() { const isProduction = process.env.NODE_ENV === "production"; return { level: "info", enabled: isProduction, // File logging in production only logDir: process.env.LOG_DIR || "./logs", maxFileSize: 10 * 1024 * 1024, // 10MB maxFiles: 10 }; } function validateDirectoryWritable(dirPath) { if (!existsSync(dirPath)) { try { mkdirSync(dirPath, { recursive: true }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Failed to create log directory "${dirPath}": ${errorMessage}`); } } try { accessSync(dirPath, constants.W_OK); } catch { throw new Error(`Log directory "${dirPath}" is not writable. Please check permissions.`); } const testFile = join(dirPath, ".logger-write-test"); try { writeFileSync(testFile, "test", "utf-8"); unlinkSync(testFile); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Cannot write to log directory "${dirPath}": ${errorMessage}`); } } function validateFileConfig() { if (!isFileLoggingEnabled()) { return; } const logDir = process.env.LOG_DIR; if (!logDir) { throw new Error( "LOG_DIR environment variable is required when LOGGER_FILE_ENABLED=true. Example: LOG_DIR=/var/log/myapp" ); } validateDirectoryWritable(logDir); } function validateSlackConfig() { const webhookUrl = process.env.SLACK_WEBHOOK_URL; if (!webhookUrl) { return; } if (!webhookUrl.startsWith("https://hooks.slack.com/")) { throw new Error( `Invalid SLACK_WEBHOOK_URL: "${webhookUrl}". Slack webhook URLs must start with "https://hooks.slack.com/"` ); } } function validateEmailConfig() { const smtpHost = process.env.SMTP_HOST; const smtpPort = process.env.SMTP_PORT; const emailFrom = process.env.EMAIL_FROM; const emailTo = process.env.EMAIL_TO; const hasAnyEmailConfig = smtpHost || smtpPort || emailFrom || emailTo; if (!hasAnyEmailConfig) { return; } const missingFields = []; if (!smtpHost) missingFields.push("SMTP_HOST"); if (!smtpPort) missingFields.push("SMTP_PORT"); if (!emailFrom) missingFields.push("EMAIL_FROM"); if (!emailTo) missingFields.push("EMAIL_TO"); if (missingFields.length > 0) { throw new Error( `Email transport configuration incomplete. Missing: ${missingFields.join(", ")}. Either set all required fields or remove all email configuration.` ); } const port = parseInt(smtpPort, 10); if (isNaN(port) || port < 1 || port > 65535) { throw new Error( `Invalid SMTP_PORT: "${smtpPort}". Must be a number between 1 and 65535.` ); } const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(emailFrom)) { throw new Error(`Invalid EMAIL_FROM format: "${emailFrom}"`); } const recipients = emailTo.split(",").map((e) => e.trim()); for (const email of recipients) { if (!emailRegex.test(email)) { throw new Error(`Invalid email address in EMAIL_TO: "${email}"`); } } } function validateEnvironment() { const nodeEnv = process.env.NODE_ENV; if (!nodeEnv) { process.stderr.write( "[Logger] Warning: NODE_ENV is not set. Defaulting to test environment.\n" ); } } function validateConfig() { try { validateEnvironment(); validateFileConfig(); validateSlackConfig(); validateEmailConfig(); } catch (error) { if (error instanceof Error) { throw new Error(`[Logger] Configuration validation failed: ${error.message}`); } throw error; } } // src/logger/adapters/custom.ts function initializeTransports() { const transports = []; const consoleConfig = getConsoleConfig(); transports.push(new ConsoleTransport(consoleConfig)); const fileConfig = getFileConfig(); if (fileConfig.enabled) { transports.push(new FileTransport(fileConfig)); } return transports; } var CustomAdapter = class _CustomAdapter { logger; constructor(config) { this.logger = new Logger({ level: config.level, module: config.module, transports: initializeTransports() }); } child(module) { const adapter = new _CustomAdapter({ level: this.logger.level, module }); adapter.logger = this.logger.child(module); return adapter; } debug(message, context) { this.logger.debug(message, context); } info(message, context) { this.logger.info(message, context); } warn(message, errorOrContext, context) { if (errorOrContext instanceof Error) { this.logger.warn(message, errorOrContext, context); } else { this.logger.warn(message, errorOrContext); } } error(message, errorOrContext, context) { if (errorOrContext instanceof Error) { this.logger.error(message, errorOrContext, context); } else { this.logger.error(message, errorOrContext); } } fatal(message, errorOrContext, context) { if (errorOrContext instanceof Error) { this.logger.fatal(message, errorOrContext, context); } else { this.logger.fatal(message, errorOrContext); } } async close() { await this.logger.close(); } }; // src/logger/adapter-factory.ts function createAdapter(type) { const level = getDefaultLogLevel(); switch (type) { case "pino": return new PinoAdapter({ level }); case "custom": return new CustomAdapter({ level }); default: return new PinoAdapter({ level }); } } function getAdapterType() { const adapterEnv = process.env.LOGGER_ADAPTER; if (adapterEnv === "custom" || adapterEnv === "pino") { return adapterEnv; } return "pino"; } function initializeLogger() { validateConfig(); return createAdapter(getAdapterType()); } var logger = initializeLogger(); // src/env/config.ts var ENV_FILE_PRIORITY = [ ".env", // Base configuration (lowest priority) ".env.{NODE_ENV}", // Environment-specific ".env.local", // Local overrides (excluded in test) ".env.{NODE_ENV}.local" // Local environment-specific (highest priority) ]; var TEST_ONLY_FILES = [ ".env.test", ".env.test.local" ]; // src/env/loader.ts var envLogger = logger.child("environment"); var environmentLoaded = false; var cachedLoadResult; function buildFileList(basePath, nodeEnv) { const files = []; if (!nodeEnv) { files.push(join(basePath, ".env")); files.push(join(basePath, ".env.local")); return files; } for (const pattern of ENV_FILE_PRIORITY) { const fileName = pattern.replace("{NODE_ENV}", nodeEnv); if (nodeEnv === "test" && fileName === ".env.local") { continue; } if (nodeEnv === "local" && pattern === ".env.local") { continue; } if (nodeEnv !== "test" && TEST_ONLY_FILES.includes(fileName)) { continue; } files.push(join(basePath, fileName)); } return files; } function loadSingleFile(filePath, debug) { if (!existsSync(filePath)) { if (debug) { envLogger.debug("Environment file not found (optional)", { path: filePath }); } return { success: false, parsed: {}, error: "File not found" }; } try { const result = config({ path: filePath }); if (result.error) { envLogger.warn("Failed to parse environment file", { path: filePath, error: result.error.message }); return { success: false, parsed: {}, error: result.error.message }; } const parsed = result.parsed || {}; if (debug) { envLogger.debug("Environment file loaded successfully", { path: filePath, variables: Object.keys(parsed), count: Object.keys(parsed).length }); } return { success: true, parsed }; } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; envLogger.error("Error loading environment file", { path: filePath, error: message }); return { success: false, parsed: {}, error: message }; } } function validateRequiredVars(required, debug) { const missing = []; for (const varName of required) { if (!process.env[varName]) { missing.push(varName); } } if (missing.length > 0) { const error = `Required environment variables missing: ${missing.join(", ")}`; envLogger.error("Environment validation failed", { missing, required }); throw new Error(error); } if (debug) { envLogger.debug("Required environment variables validated", { required, allPresent: true }); } } function loadEnvironment(options = {}) { const { basePath = process.cwd(), customPaths = [], debug = false, nodeEnv = process.env.NODE_ENV || "", required = [], useCache = true } = options; if (useCache && environmentLoaded && cachedLoadResult) { if (debug) { envLogger.debug("Returning cached environment", { loaded: cachedLoadResult.loaded.length, variables: Object.keys(cachedLoadResult.parsed).length }); } return cachedLoadResult; } if (debug) { envLogger.debug("Loading environment variables", { basePath, nodeEnv, customPaths, required }); } const result = { success: true, loaded: [], failed: [], parsed: {}, warnings: [] }; const standardFiles = buildFileList(basePath, nodeEnv); const allFiles = [...standardFiles, ...customPaths]; if (debug) { envLogger.debug("Environment files to load", { standardFiles, customPaths, total: allFiles.length }); } const reversedFiles = [...allFiles].reverse(); for (const filePath of reversedFiles) { const fileResult = loadSingleFile(filePath, debug); if (fileResult.success) { result.loaded.push(filePath); Object.assign(result.parsed, fileResult.parsed); if (fileResult.parsed["NODE_ENV"]) { const fileName = filePath.split("/").pop() || filePath; result.warnings.push( `NODE_ENV found in ${fileName}. It's recommended to set NODE_ENV via CLI (e.g., 'spfn dev', 'spfn build') instead of .env files for consistent environment behavior.` ); } } else if (fileResult.error) { result.failed.push({ path: filePath, reason: fileResult.error }); } } if (debug || result.loaded.length > 0) { envLogger.info("Environment loading complete", { loaded: result.loaded.length, failed: result.failed.length, variables: Object.keys(result.parsed).length, files: result.loaded }); } if (required.length > 0) { try { validateRequiredVars(required, debug); } catch (error) { result.success = false; result.errors = [ error instanceof Error ? error.message : "Validation failed" ]; throw error; } } if (result.warnings.length > 0) { for (const warning of result.warnings) { envLogger.warn(warning); } } environmentLoaded = true; cachedLoadResult = result; return result; } function getEnvVar(key, options = {}) { const { required = false, default: defaultValue, validator, validationError } = options; const value = process.env[key]; if (value === void 0 || value === "") { if (required) { throw new Error(`Required environment variable not found: ${key}`); } return defaultValue; } if (validator && !validator(value)) { const message = validationError || `Invalid value for environment variable: ${key}`; throw new Error(message); } return value; } function requireEnvVar(key) { return getEnvVar(key, { required: true }); } function hasEnvVar(key) { const value = process.env[key]; return value !== void 0 && value !== ""; } function getEnvVars(keys) { const result = {}; for (const key of keys) { result[key] = process.env[key]; } return result; } function isEnvironmentLoaded() { return environmentLoaded; } function resetEnvironment() { environmentLoaded = false; cachedLoadResult = void 0; } // src/env/validator.ts function validateUrl(value, options = {}) { const { protocol = "any" } = options; try { const url = new URL(value); if (protocol === "http" && url.protocol !== "http:") { return false; } if (protocol === "https" && url.protocol !== "https:") { return false; } return true; } catch { return false; } } function createUrlValidator(protocol = "any") { return (value) => validateUrl(value, { protocol }); } function validateNumber(value, options = {}) { const { min, max, integer = false } = options; if (value.trim() === "") { return false; } const num = Number(value); if (isNaN(num)) { return false; } if (integer && !Number.isInteger(num)) { return false; } if (min !== void 0 && num < min) { return false; } if (max !== void 0 && num > max) { return false; } return true; } function createNumberValidator(options = {}) { return (value) => validateNumber(value, options); } function validateBoolean(value) { const normalized = value.toLowerCase().trim(); return ["true", "false", "1", "0", "yes", "no"].includes(normalized); } function parseBoolean(value) { const normalized = value.toLowerCase().trim(); return ["true", "1", "yes"].includes(normalized); } function validateEnum(value, allowed, caseInsensitive = false) { if (caseInsensitive) { const normalizedValue = value.toLowerCase(); const normalizedAllowed = allowed.map((v) => v.toLowerCase()); return normalizedAllowed.includes(normalizedValue); } return allowed.includes(value); } function createEnumValidator(allowed, caseInsensitive = false) { return (value) => validateEnum(value, allowed, caseInsensitive); } function validatePattern(value, pattern) { return pattern.test(value); } function createPatternValidator(pattern) { return (value) => validatePattern(value, pattern); } function validateNotEmpty(value) { return value.trim().length > 0; } function validateMinLength(value, minLength) { return value.length >= minLength; } function createMinLengthValidator(minLength) { return (value) => validateMinLength(value, minLength); } function combineValidators(validators) { return (value) => validators.every((validator) => validator(value)); } function validatePostgresUrl(value) { try { const url = new URL(value); return url.protocol === "postgres:" || url.protocol === "postgresql:"; } catch { return false; } } function validateRedisUrl(value) { try { const url = new URL(value); return url.protocol === "redis:" || url.protocol === "rediss:"; } catch { return false; } } export { ENV_FILE_PRIORITY, TEST_ONLY_FILES, combineValidators, createEnumValidator, createMinLengthValidator, createNumberValidator, createPatternValidator, createUrlValidator, getEnvVar, getEnvVars, hasEnvVar, isEnvironmentLoaded, loadEnvironment, parseBoolean, requireEnvVar, resetEnvironment, validateBoolean, validateEnum, validateMinLength, validateNotEmpty, validateNumber, validatePattern, validatePostgresUrl, validateRedisUrl, validateUrl }; //# sourceMappingURL=index.js.map //# sourceMappingURL=index.js.map