refactor: improve type safety and code quality for 9.0 score

Type Safety Improvements:
- Add isErrorResult() type guard for API responses (domain-tool.ts)
- Replace `any` with `unknown` in executeTool args (tools/index.ts)
- Add JSON.parse error handling in function calling (openai-service.ts)
- Fix nullable price handling with nullish coalescing
- Add array type guard for nameservers validation

Code Quality Improvements:
- Extract convertNamecheapDate() to eliminate duplicate functions
- Move hardcoded bank account info to environment variables
- Add JSDoc documentation to executeDepositFunction
- Fix unused variables in optimistic-lock.ts
- Handle Error.captureStackTrace for Workers environment

All TypeScript strict mode checks now pass.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
kappa
2026-01-19 23:33:35 +09:00
parent f5df0c0ffe
commit 61e5185916
5 changed files with 96 additions and 50 deletions

View File

@@ -39,6 +39,12 @@ function isNamecheapPriceResponse(result: unknown): result is NamecheapPriceResp
return typeof result === 'object' && result !== null && 'krw' in result;
}
// Namecheap 날짜 형식 변환 (MM/DD/YYYY → YYYY-MM-DD)
function convertNamecheapDate(date: string): string {
const [month, day, year] = date.split('/');
return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;
}
// KV 캐싱 인터페이스
interface CachedTLDPrice {
tld: string;
@@ -75,9 +81,10 @@ async function setCachedTLDPrice(
): Promise<void> {
try {
const key = `tld_price:${tld}`;
const krwPrice = price.krw ?? price.register_krw ?? 0;
const data: CachedTLDPrice = {
tld,
krw: price.krw || price.register_krw,
krw: krwPrice,
usd: price.usd,
cached_at: new Date().toISOString(),
};
@@ -210,18 +217,13 @@ async function callNamecheapApi(
}).then(r => r.json()),
{ maxRetries: 3 }
) as NamecheapDomainListItem[];
// MM/DD/YYYY → YYYY-MM-DD 변환 (Namecheap은 미국 형식 사용)
const convertDate = (date: string) => {
const [month, day, year] = date.split('/');
return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;
};
// 허용된 도메인만 필터링, 날짜는 ISO 형식으로 변환
return result
.filter((d: NamecheapDomainListItem) => allowedDomains.includes(d.name))
.map((d: NamecheapDomainListItem) => ({
...d,
created: convertDate(d.created),
expires: convertDate(d.expires),
created: convertNamecheapDate(d.created),
expires: convertNamecheapDate(d.expires),
user: undefined, // 민감 정보 제거
}));
}
@@ -238,16 +240,11 @@ async function callNamecheapApi(
if (!domainInfo) {
return { error: `도메인을 찾을 수 없습니다: ${funcArgs.domain}` };
}
// MM/DD/YYYY → YYYY-MM-DD 변환 (Namecheap은 미국 형식 사용)
const convertDate = (date: string) => {
const [month, day, year] = date.split('/');
return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;
};
// 민감 정보 필터링 (user/owner 제거), 날짜는 ISO 형식으로 변환
return {
domain: domainInfo.name,
created: convertDate(domainInfo.created),
expires: convertDate(domainInfo.expires),
created: convertNamecheapDate(domainInfo.created),
expires: convertNamecheapDate(domainInfo.expires),
is_expired: domainInfo.is_expired,
auto_renew: domainInfo.auto_renew,
is_locked: domainInfo.is_locked,
@@ -275,8 +272,9 @@ async function callNamecheapApi(
if (!res.ok) {
// Namecheap 에러 메시지 파싱
if (text.includes('subordinate hosts') || text.includes('Non existen')) {
const nsArray = Array.isArray(nameservers) ? nameservers : [];
return {
error: `네임서버 변경 실패: ${funcArgs.nameservers.join(', ')}는 등록되지 않은 네임서버입니다. 자기 도메인을 네임서버로 사용하려면 먼저 Namecheap에서 Child Nameserver(글루 레코드)를 IP 주소와 함께 등록해야 합니다.`
error: `네임서버 변경 실패: ${nsArray.join(', ')}는 등록되지 않은 네임서버입니다. 자기 도메인을 네임서버로 사용하려면 먼저 Namecheap에서 Child Nameserver(글루 레코드)를 IP 주소와 함께 등록해야 합니다.`
};
}
return { error: `네임서버 변경 실패: ${text}` };
@@ -485,19 +483,34 @@ async function executeDomainAction(
}
const result = await callNamecheapApi('get_domain_info', { domain }, allowedDomains, env, telegramUserId, db, userId);
if (result.error) {
if (isErrorResult(result)) {
// 계정 내 도메인 정보 조회 실패 시 WHOIS로 폴백
return executeDomainAction('whois', args, allowedDomains, env, telegramUserId, db, userId);
}
return `📋 ${domain} 정보\n\n• 생성일: ${result.created}\n• 만료일: ${result.expires}\n• 자동갱신: ${result.auto_renew ? '✅' : '❌'}\n• 잠금: ${result.is_locked ? '🔒' : '🔓'}\n• WHOIS Guard: ${result.whois_guard ? '✅' : '❌'}`;
// Type assertion after error check
const domainInfo = result as {
domain: string;
created: string;
expires: string;
is_expired: boolean;
auto_renew: boolean;
is_locked: boolean;
whois_guard: boolean;
};
return `📋 ${domain} 정보\n\n• 생성일: ${domainInfo.created}\n• 만료일: ${domainInfo.expires}\n• 자동갱신: ${domainInfo.auto_renew ? '✅' : '❌'}\n• 잠금: ${domainInfo.is_locked ? '🔒' : '🔓'}\n• WHOIS Guard: ${domainInfo.whois_guard ? '✅' : '❌'}`;
}
case 'get_ns': {
if (!domain) return '🚫 도메인을 지정해주세요.';
const result = await callNamecheapApi('get_nameservers', { domain }, allowedDomains, env, telegramUserId, db, userId);
if (result.error) return `🚫 ${result.error}`;
const nsList = (result.nameservers || result).map((ns: string) => `${ns}`).join('\n');
if (isErrorResult(result)) return `🚫 ${result.error}`;
const nsResult = result as { nameservers?: string[] } | string[];
const nameserverList = Array.isArray(nsResult) ? nsResult : (nsResult.nameservers || []);
const nsList = nameserverList.map((ns: string) => `${ns}`).join('\n');
return `🌐 ${domain} 네임서버\n\n${nsList}`;
}
@@ -506,7 +519,7 @@ async function executeDomainAction(
if (!nameservers?.length) return '🚫 네임서버를 지정해주세요.';
if (!allowedDomains.includes(domain)) return `🚫 ${domain}은 관리 권한이 없습니다.`;
const result = await callNamecheapApi('set_nameservers', { domain, nameservers }, allowedDomains, env, telegramUserId, db, userId);
if (result.error) return `🚫 ${result.error}`;
if (isErrorResult(result)) return `🚫 ${result.error}`;
return `${domain} 네임서버 변경 완료\n\n${nameservers.map(ns => `${ns}`).join('\n')}`;
}
@@ -542,7 +555,8 @@ async function executeDomainAction(
}
}
return `${domain}은 등록 가능합니다.\n\n💰 가격: ${price?.toLocaleString()}원/년\n\n등록하시려면 "${domain} 등록해줘"라고 말씀해주세요.`;
const priceStr = price ? `${price.toLocaleString()}원/년` : '가격 조회 중';
return `${domain}은 등록 가능합니다.\n\n💰 가격: ${priceStr}\n\n등록하시려면 "${domain} 등록해줘"라고 말씀해주세요.`;
}
return `${domain}은 이미 등록된 도메인입니다.`;
}
@@ -635,7 +649,9 @@ async function executeDomainAction(
if (statuses.length) response += `• 상태: ${statuses.join(', ')}\n`;
if (dnssec !== '-') response += `• DNSSEC: ${dnssec}`;
if (result.available === true) {
// Type guard to check if result has 'available' property
const typedResult = result as { available?: boolean };
if (typedResult.available === true) {
response += `\n\n✅ 이 도메인은 등록 가능합니다!`;
}
@@ -657,16 +673,19 @@ async function executeDomainAction(
// API 호출
const result = await callNamecheapApi('get_price', { tld: targetTld }, allowedDomains, env, telegramUserId, db, userId);
if (result.error) return `🚫 ${result.error}`;
if (isErrorResult(result)) return `🚫 ${result.error}`;
// Type assertion after error check
const priceResult = result as NamecheapPriceResponse;
// 캐시 저장
if (env?.RATE_LIMIT_KV) {
await setCachedTLDPrice(env.RATE_LIMIT_KV, targetTld, result);
await setCachedTLDPrice(env.RATE_LIMIT_KV, targetTld, priceResult);
}
// API 응답: { tld, usd, krw }
const price = result.krw || result.register_krw;
return `💰 .${targetTld} 도메인 가격\n\n• 등록/갱신: ${price?.toLocaleString()}원/년`;
const price = priceResult.krw ?? priceResult.register_krw ?? 0;
return `💰 .${targetTld} 도메인 가격\n\n• 등록/갱신: ${price.toLocaleString()}원/년`;
}
case 'cheapest': {
@@ -719,14 +738,18 @@ async function executeDomainAction(
// 1. 가용성 확인
const checkResult = await callNamecheapApi('check_domains', { domains: [domain] }, allowedDomains, env, telegramUserId, db, userId);
if (checkResult.error) return `🚫 ${checkResult.error}`;
if (!checkResult[domain]) return `${domain}은 이미 등록된 도메인입니다.`;
if (isErrorResult(checkResult)) return `🚫 ${checkResult.error}`;
const availability = checkResult as NamecheapCheckResult;
if (!availability[domain]) return `${domain}은 이미 등록된 도메인입니다.`;
// 2. 가격 조회
const domainTld = domain.split('.').pop() || '';
const priceResult = await callNamecheapApi('get_price', { tld: domainTld }, allowedDomains, env, telegramUserId, db, userId);
if (priceResult.error) return `🚫 가격 조회 실패: ${priceResult.error}`;
const price = priceResult.krw || priceResult.register_krw;
if (isErrorResult(priceResult)) return `🚫 가격 조회 실패: ${priceResult.error}`;
const priceData = priceResult as NamecheapPriceResponse;
const price = priceData.krw ?? priceData.register_krw ?? 0;
// 3. 잔액 조회
let balance = 0;
@@ -846,6 +869,9 @@ export async function executeSuggestDomains(args: { keywords: string }, env?: En
return '🚫 도메인 추천 기능이 설정되지 않았습니다. (NAMECHEAP_API_KEY 미설정)';
}
// Store API key after null check
const namecheapApiKey = env.NAMECHEAP_API_KEY;
try {
const namecheapApiUrl = env.NAMECHEAP_API_URL || 'https://namecheap-api.anvil.it.com';
const TARGET_COUNT = 10;
@@ -926,7 +952,7 @@ ${excludeList ? `- 다음 도메인은 제외하세요: ${excludeList}` : ''}
() => fetch(`${namecheapApiUrl}/domains/check`, {
method: 'POST',
headers: {
'X-API-Key': env.NAMECHEAP_API_KEY!, // Already checked above
'X-API-Key': namecheapApiKey,
'Content-Type': 'application/json',
},
body: JSON.stringify({ domains: newDomains }),
@@ -971,7 +997,7 @@ ${excludeList ? `- 다음 도메인은 제외하세요: ${excludeList}` : ''}
// 캐시 미스 시 API 호출
const priceRes = await retryWithBackoff(
() => fetch(`${namecheapApiUrl}/prices/${tld}`, {
headers: { 'X-API-Key': env.NAMECHEAP_API_KEY! }, // Already checked above
headers: { 'X-API-Key': namecheapApiKey },
}),
{ maxRetries: 3 }
);