import { z } from 'zod'; import { Env } from './types'; import { createLogger } from './utils/logger'; import { executeWithOptimisticLock, OptimisticLockError } from './utils/optimistic-lock'; const logger = createLogger('domain-register'); // Zod schemas for API response validation const NamecheapRegisterResponseSchema = z.object({ registered: z.boolean().optional(), domain: z.string().optional(), error: z.string().optional(), detail: z.string().optional(), }); const DomainInfoResponseSchema = z.object({ expires: z.string().optional(), }); const NameserverResponseSchema = z.object({ nameservers: z.array(z.string()).optional(), }); const PriceResponseSchema = z.object({ krw: z.number().optional(), register_krw: z.number().optional(), }); interface RegisterResult { success: boolean; domain?: string; price?: number; newBalance?: number; nameservers?: string[]; expiresAt?: string; error?: string; } // 도메인 등록 실행 export async function executeDomainRegister( env: Env, userId: number, telegramUserId: string, domain: string, price: number ): Promise { const apiKey = env.NAMECHEAP_API_KEY; const apiUrl = env.NAMECHEAP_API_URL || 'https://namecheap.api.inouter.com'; if (!apiKey) { return { success: false, error: 'API 키가 설정되지 않았습니다.' }; } try { // 1. Verify price from Namecheap API (security: prevent price manipulation) const domainTld = domain.split('.').pop() || ''; const priceCheckResponse = await fetch(`${apiUrl}/prices/${domainTld}`, { headers: { 'X-API-Key': apiKey } }); if (!priceCheckResponse.ok) { logger.error('Failed to fetch price from Namecheap API', new Error(`HTTP ${priceCheckResponse.status}`)); return { success: false, error: '가격 정보를 가져올 수 없습니다.' }; } const priceJsonData = await priceCheckResponse.json(); const priceParseResult = PriceResponseSchema.safeParse(priceJsonData); if (!priceParseResult.success) { logger.error('Price response schema validation failed', priceParseResult.error); return { success: false, error: '가격 정보 형식이 올바르지 않습니다.' }; } const priceData = priceParseResult.data; const actualPrice = priceData.krw || priceData.register_krw; if (!actualPrice || typeof actualPrice !== 'number') { logger.error('Invalid price data from API', new Error('Missing or invalid krw/register_krw'), { priceData }); return { success: false, error: '가격 정보가 올바르지 않습니다.' }; } // SECURITY: Verify callback price matches actual API price (allow 5% tolerance for exchange rate fluctuation) const priceDiff = Math.abs(actualPrice - price); const tolerance = actualPrice * 0.05; // 5% if (priceDiff > tolerance) { logger.warn('Price mismatch detected - potential price manipulation', { callbackPrice: price, actualPrice, difference: priceDiff, domain }); return { success: false, error: `가격이 변경되었습니다. 현재 가격: ${actualPrice.toLocaleString()}원\n다시 등록을 시도해주세요.` }; } logger.info('Price verification passed', { domain, callbackPrice: price, actualPrice }); // 2. 현재 잔액 확인 const balanceRow = await env.DB.prepare( 'SELECT balance FROM user_deposits WHERE user_id = ?' ).bind(userId).first<{ balance: number }>(); const currentBalance = balanceRow?.balance || 0; // Use actual price from API instead of callback price if (currentBalance < actualPrice) { return { success: false, error: `잔액이 부족합니다. (현재: ${currentBalance.toLocaleString()}원, 필요: ${actualPrice.toLocaleString()}원)` }; } // 3. Namecheap API로 도메인 등록 logger.info('도메인 등록 요청', { domain, actualPrice, callbackPrice: price }); const registerResponse = await fetch(`${apiUrl}/domains/register`, { method: 'POST', headers: { 'X-API-Key': apiKey, 'Content-Type': 'application/json' }, body: JSON.stringify({ domain: domain, years: 1, telegram_id: telegramUserId, }), }); const jsonData = await registerResponse.json(); const parseResult = NamecheapRegisterResponseSchema.safeParse(jsonData); if (!parseResult.success) { logger.error('Namecheap register response schema validation failed', parseResult.error); return { success: false, error: '도메인 등록 응답 형식이 올바르지 않습니다.' }; } const registerResult = parseResult.data; if (!registerResponse.ok || !registerResult.registered) { const errorMsg = registerResult.error || registerResult.detail || '도메인 등록에 실패했습니다.'; logger.error('등록 실패', new Error(errorMsg), { registerResult }); return { success: false, error: errorMsg }; } logger.info('등록 성공', { registerResult }); // 4. 잔액 차감 + 거래 기록 (Optimistic Locking) - USE ACTUAL PRICE try { await executeWithOptimisticLock(env.DB, async () => { // Read current balance and version const current = await env.DB.prepare( 'SELECT balance, version FROM user_deposits WHERE user_id = ?' ).bind(userId).first<{ balance: number; version: number }>(); if (!current || current.balance < actualPrice) { throw new Error('잔액이 부족합니다.'); } // Update balance with version check - USE ACTUAL PRICE const updateResult = await env.DB.prepare( 'UPDATE user_deposits SET balance = balance - ?, version = version + 1, updated_at = CURRENT_TIMESTAMP WHERE user_id = ? AND version = ?' ).bind(actualPrice, userId, current.version).run(); if (!updateResult.success || updateResult.meta.changes === 0) { throw new OptimisticLockError('Version mismatch on balance update'); } // Insert transaction record - USE ACTUAL PRICE const txResult = await env.DB.prepare( `INSERT INTO deposit_transactions (user_id, type, amount, status, description, confirmed_at) VALUES (?, 'withdrawal', ?, 'confirmed', ?, CURRENT_TIMESTAMP)` ).bind(userId, actualPrice, `도메인 등록: ${domain}`).run(); if (!txResult.success) { throw new Error('거래 기록 생성 실패'); } logger.info('Domain registration payment completed with optimistic locking', { userId, telegramUserId, domain, actualPrice, callbackPrice: price, newBalance: current.balance - actualPrice, }); }); } catch (error) { if (error instanceof OptimisticLockError) { logger.warn('동시성 충돌 감지 (도메인 등록)', { userId, telegramUserId, domain, actualPrice, }); return { success: false, error: '처리 중 동시성 충돌이 발생했습니다. 잠시 후 다시 시도해주세요.', }; } throw error; // Re-throw other errors to be caught by outer catch } // 4. user_domains 테이블에 추가 await env.DB.prepare( 'INSERT INTO user_domains (user_id, domain, verified, created_at) VALUES (?, ?, 1, datetime("now"))' ).bind(userId, domain).run(); // 5. 도메인 정보 조회 (네임서버 + 만료일) - 병렬 처리 let nameservers: string[] = []; let expiresAt: string | undefined; try { // 도메인 정보 + 네임서버 병렬 조회 const [infoResponse, nsResponse] = await Promise.all([ fetch(`${apiUrl}/domains/${domain}/info`, { headers: { 'X-API-Key': apiKey } }), fetch(`${apiUrl}/domains/${domain}/nameservers`, { headers: { 'X-API-Key': apiKey } }) ]); // 도메인 정보 처리 (만료일) if (infoResponse.ok) { const infoJsonData = await infoResponse.json(); const infoParseResult = DomainInfoResponseSchema.safeParse(infoJsonData); if (!infoParseResult.success) { logger.warn('Domain info response schema validation failed', { domain }); } else { const infoResult = infoParseResult.data; if (infoResult.expires) { // MM/DD/YYYY → YYYY-MM-DD 변환 const [month, day, year] = infoResult.expires.split('/'); expiresAt = `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`; } } } // 네임서버 처리 if (nsResponse.ok) { const nsJsonData = await nsResponse.json(); const nsParseResult = NameserverResponseSchema.safeParse(nsJsonData); if (!nsParseResult.success) { logger.warn('Nameserver response schema validation failed', { domain }); } else { const nsResult = nsParseResult.data; nameservers = nsResult.nameservers || []; } } } catch (infoError) { logger.info('도메인 정보 조회 실패 (무시)', { error: infoError }); } const newBalance = currentBalance - actualPrice; logger.info('도메인 등록 완료', { domain, oldBalance: currentBalance, newBalance, actualPrice, expiresAt, nameservers: nameservers.join(', ') }); return { success: true, domain: domain, price: actualPrice, // Return actual price charged newBalance: newBalance, nameservers: nameservers, expiresAt: expiresAt, }; } catch (error) { logger.error('도메인 등록 중 오류', error as Error, { domain, callbackPrice: price }); return { success: false, error: '도메인 등록 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.' }; } }