refactor: improve OpenAI service and tools

- Enhance OpenAI message types with tool_calls support
- Improve security validation and rate limiting
- Update utility tools and weather tool
- Minor fixes in deposit-agent and domain-register

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
kappa
2026-01-28 20:26:31 +09:00
parent 7ef0ec7594
commit e32e3c6a44
7 changed files with 238 additions and 94 deletions

View File

@@ -1,7 +1,5 @@
import { Env, TelegramUpdate } from './types';
// KV 오류 시 인메모리 폴백 (Worker 인스턴스 내)
const fallbackRateLimits = new Map<string, { count: number; resetAt: number }>();
import { createLogger } from './utils/logger';
// Telegram 서버 IP 대역 (2024년 기준)
// https://core.telegram.org/bots/webhooks#the-short-version
@@ -65,9 +63,16 @@ function isValidRequestBody(body: unknown): body is TelegramUpdate {
);
}
// 타임스탬프 검증 (비활성화 - WEBHOOK_SECRET으로 충분)
function isRecentUpdate(_message: TelegramUpdate['message']): boolean {
return true;
// 타임스탬프 검증 (5분 이내 메시지만 허용 - 리플레이 공격 방지)
function isRecentUpdate(message: TelegramUpdate['message']): boolean {
// message가 없으면 callback_query 등일 수 있음 - 허용
if (!message?.date) return true;
const messageTime = message.date * 1000; // Telegram uses Unix timestamp in seconds
const now = Date.now();
const MAX_AGE_MS = 5 * 60 * 1000; // 5 minutes
return (now - messageTime) < MAX_AGE_MS;
}
export interface SecurityCheckResult {
@@ -144,6 +149,7 @@ export async function checkRateLimit(
): Promise<boolean> {
const key = `ratelimit:${userId}`;
const now = Date.now();
const logger = createLogger('rate-limit');
try {
// KV에서 기존 데이터 조회
@@ -159,11 +165,24 @@ export async function checkRateLimit(
await kv.put(key, JSON.stringify(newData), {
expirationTtl: Math.ceil(windowMs / 1000), // 초 단위
});
logger.info('Rate limit 윈도우 시작', {
userId,
resetAt: new Date(newData.resetAt).toISOString(),
maxRequests,
});
return true;
}
// Rate limit 초과
if (data.count >= maxRequests) {
const resetInSeconds = Math.ceil((data.resetAt - now) / 1000);
logger.warn('Rate limit 초과', {
userId,
currentCount: data.count,
maxRequests,
resetInSeconds,
resetAt: new Date(data.resetAt).toISOString(),
});
return false;
}
@@ -178,25 +197,11 @@ export async function checkRateLimit(
});
return true;
} catch (error) {
console.error('[RateLimit] KV 오류:', error);
// KV 오류 시 요청 허용 (fail-open)
// Rate limiting은 abuse 방지 목적이므로 가용성 우선
// 심각한 abuse는 Cloudflare WAF/Firewall Rules로 별도 대응
logger.warn('KV 오류 - 요청 허용 (fail-open)', { userId, error: (error as Error).message });
// 인메모리 폴백으로 기본 보호
const fallbackKey = `fallback:${userId}`;
const existing = fallbackRateLimits.get(fallbackKey);
// 윈도우 만료 시 리셋
if (!existing || existing.resetAt < now) {
fallbackRateLimits.set(fallbackKey, { count: 1, resetAt: now + 60000 }); // 1분 윈도우
return true;
}
// 제한 초과 체크 (인메모리에서는 더 보수적으로 10회)
if (existing.count >= 10) {
console.warn('[RateLimit] Fallback limit exceeded', { userId });
return false;
}
existing.count++;
return true;
}
}