refactor: add KV cache, env validation, logger types, constants
- Add KV cache abstraction layer (src/services/kv-cache.ts) - Add Zod-based env validation (src/utils/env-validation.ts) - Improve logger types: any → unknown for type safety - Add centralized constants file (src/constants/index.ts) - Fix security.ts unused import Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
105
src/utils/env-validation.ts
Normal file
105
src/utils/env-validation.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { z } from 'zod';
|
||||
import { createLogger } from './logger';
|
||||
|
||||
const logger = createLogger('env-validation');
|
||||
|
||||
/**
|
||||
* Environment variable schema with validation rules
|
||||
*/
|
||||
export const EnvSchema = z.object({
|
||||
// Required secrets
|
||||
BOT_TOKEN: z.string().min(1, 'BOT_TOKEN is required'),
|
||||
WEBHOOK_SECRET: z.string().min(10, 'WEBHOOK_SECRET must be at least 10 characters'),
|
||||
|
||||
// Optional secrets with defaults handled elsewhere
|
||||
OPENAI_API_KEY: z.string().optional(),
|
||||
DEPOSIT_API_SECRET: z.string().optional(),
|
||||
BRAVE_API_KEY: z.string().optional(),
|
||||
NAMECHEAP_API_KEY: z.string().optional(),
|
||||
NAMECHEAP_API_KEY_INTERNAL: z.string().optional(),
|
||||
|
||||
// Configuration with defaults
|
||||
ENVIRONMENT: z.enum(['development', 'production']).default('production'),
|
||||
SUMMARY_THRESHOLD: z.string().default('20').transform(Number),
|
||||
MAX_SUMMARIES_PER_USER: z.string().default('3').transform(Number),
|
||||
|
||||
// Admin IDs
|
||||
DOMAIN_OWNER_ID: z.string().optional(),
|
||||
DEPOSIT_ADMIN_ID: z.string().optional(),
|
||||
|
||||
// API URLs (optional, have defaults in code)
|
||||
OPENAI_API_BASE: z.string().url().optional(),
|
||||
NAMECHEAP_API_URL: z.string().url().optional(),
|
||||
WHOIS_API_URL: z.string().url().optional(),
|
||||
CONTEXT7_API_BASE: z.string().url().optional(),
|
||||
BRAVE_API_BASE: z.string().url().optional(),
|
||||
WTTR_IN_URL: z.string().url().optional(),
|
||||
HOSTING_SITE_URL: z.string().url().optional(),
|
||||
CLOUD_ORCHESTRATOR_URL: z.string().url().optional(),
|
||||
});
|
||||
|
||||
export type ValidatedEnv = z.infer<typeof EnvSchema>;
|
||||
|
||||
/**
|
||||
* Validation result type
|
||||
*/
|
||||
export interface EnvValidationResult {
|
||||
success: boolean;
|
||||
errors: string[];
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate environment variables
|
||||
* Call this early in worker initialization
|
||||
*/
|
||||
export function validateEnv(env: Record<string, unknown>): EnvValidationResult {
|
||||
const result: EnvValidationResult = {
|
||||
success: true,
|
||||
errors: [],
|
||||
warnings: [],
|
||||
};
|
||||
|
||||
// Validate with Zod
|
||||
const parsed = EnvSchema.safeParse(env);
|
||||
|
||||
if (!parsed.success) {
|
||||
result.success = false;
|
||||
for (const issue of parsed.error.issues) {
|
||||
const path = issue.path.join('.');
|
||||
result.errors.push(`${path}: ${issue.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Additional warnings for recommended but optional vars
|
||||
if (!env.OPENAI_API_KEY) {
|
||||
result.warnings.push('OPENAI_API_KEY not set - will use Workers AI fallback');
|
||||
}
|
||||
|
||||
if (!env.DEPOSIT_ADMIN_ID) {
|
||||
result.warnings.push('DEPOSIT_ADMIN_ID not set - admin notifications disabled');
|
||||
}
|
||||
|
||||
// Log results
|
||||
if (result.errors.length > 0) {
|
||||
logger.error('Environment validation failed', new Error('Invalid configuration'), {
|
||||
errors: result.errors,
|
||||
});
|
||||
}
|
||||
|
||||
if (result.warnings.length > 0) {
|
||||
logger.warn('Environment validation warnings', { warnings: result.warnings });
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick check for critical env vars - throws on failure
|
||||
*/
|
||||
export function requireEnv(env: Record<string, unknown>, keys: string[]): void {
|
||||
const missing = keys.filter(key => !env[key]);
|
||||
if (missing.length > 0) {
|
||||
throw new Error(`Missing required environment variables: ${missing.join(', ')}`);
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,31 @@ export enum LogLevel {
|
||||
FATAL = 'FATAL',
|
||||
}
|
||||
|
||||
/**
|
||||
* 로그 컨텍스트 값 타입
|
||||
*
|
||||
* any 대신 unknown을 사용하여 타입 안정성 개선
|
||||
* - unknown은 사용 전 타입 체크를 강제하므로 any보다 안전
|
||||
* - 모든 타입의 값을 저장 가능하지만, 사용 시 타입 검증 필요
|
||||
*
|
||||
* 기본 타입 (string, number, boolean 등)과 객체/배열 모두 허용하여
|
||||
* 실제 로깅 사용 사례를 지원하면서도 타입 안정성을 유지합니다.
|
||||
*/
|
||||
export type LogContextValue =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| undefined
|
||||
| unknown
|
||||
| LogContextValue[]
|
||||
| { [key: string]: unknown };
|
||||
|
||||
/**
|
||||
* 로그 컨텍스트 타입
|
||||
*/
|
||||
export type LogContext = Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* 구조화된 로그 엔트리 인터페이스
|
||||
*/
|
||||
@@ -48,7 +73,7 @@ export interface LogEntry {
|
||||
/** 서비스명 (예: 'openai', 'telegram', 'deposit') */
|
||||
service?: string;
|
||||
/** 추가 컨텍스트 정보 */
|
||||
context?: Record<string, any>;
|
||||
context?: LogContext;
|
||||
/** 에러 정보 (ERROR/FATAL 레벨용) */
|
||||
error?: {
|
||||
/** 에러 이름 */
|
||||
@@ -209,7 +234,7 @@ export class Logger {
|
||||
private createEntry(
|
||||
level: LogLevel,
|
||||
message: string,
|
||||
context?: Record<string, any>,
|
||||
context?: LogContext,
|
||||
error?: Error
|
||||
): LogEntry {
|
||||
const entry: LogEntry = {
|
||||
@@ -245,7 +270,7 @@ export class Logger {
|
||||
* logger.debug('함수 호출', { functionName: 'processData', args: [1, 2, 3] });
|
||||
* ```
|
||||
*/
|
||||
debug(message: string, context?: Record<string, any>): void {
|
||||
debug(message: string, context?: LogContext): void {
|
||||
if (!this.shouldLog(LogLevel.DEBUG)) return;
|
||||
this.write(this.createEntry(LogLevel.DEBUG, message, context));
|
||||
}
|
||||
@@ -261,7 +286,7 @@ export class Logger {
|
||||
* logger.info('요청 처리 시작', { userId: '123', endpoint: '/api/data' });
|
||||
* ```
|
||||
*/
|
||||
info(message: string, context?: Record<string, any>): void {
|
||||
info(message: string, context?: LogContext): void {
|
||||
if (!this.shouldLog(LogLevel.INFO)) return;
|
||||
this.write(this.createEntry(LogLevel.INFO, message, context));
|
||||
}
|
||||
@@ -277,7 +302,7 @@ export class Logger {
|
||||
* logger.warn('API 응답 지연', { endpoint: '/api/data', responseTime: 5000 });
|
||||
* ```
|
||||
*/
|
||||
warn(message: string, context?: Record<string, any>): void {
|
||||
warn(message: string, context?: LogContext): void {
|
||||
if (!this.shouldLog(LogLevel.WARN)) return;
|
||||
this.write(this.createEntry(LogLevel.WARN, message, context));
|
||||
}
|
||||
@@ -298,7 +323,7 @@ export class Logger {
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
error(message: string, error?: Error, context?: Record<string, any>): void {
|
||||
error(message: string, error?: Error, context?: LogContext): void {
|
||||
if (!this.shouldLog(LogLevel.ERROR)) return;
|
||||
this.write(this.createEntry(LogLevel.ERROR, message, context, error));
|
||||
}
|
||||
@@ -315,7 +340,7 @@ export class Logger {
|
||||
* logger.fatal('데이터베이스 연결 실패', error, { database: 'main' });
|
||||
* ```
|
||||
*/
|
||||
fatal(message: string, error?: Error, context?: Record<string, any>): void {
|
||||
fatal(message: string, error?: Error, context?: LogContext): void {
|
||||
if (!this.shouldLog(LogLevel.FATAL)) return;
|
||||
this.write(this.createEntry(LogLevel.FATAL, message, context, error));
|
||||
}
|
||||
@@ -336,7 +361,7 @@ export class Logger {
|
||||
* end(); // duration이 자동으로 로그에 포함됨
|
||||
* ```
|
||||
*/
|
||||
startTimer(message?: string, context?: Record<string, any>): () => void {
|
||||
startTimer(message?: string, context?: LogContext): () => void {
|
||||
const startTime = Date.now();
|
||||
const timerMessage = message || 'Operation completed';
|
||||
|
||||
@@ -404,7 +429,7 @@ export class Logger {
|
||||
export function createLogger(service: string, env?: Partial<Env>): Logger {
|
||||
// 환경 감지: 명시적 ENVIRONMENT 변수 또는 프로덕션 감지
|
||||
const environment =
|
||||
(env as any)?.ENVIRONMENT === 'production'
|
||||
env && 'ENVIRONMENT' in env && env.ENVIRONMENT === 'production'
|
||||
? 'production'
|
||||
: 'development';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user