Files
auto-trade/lib/kis/approval.ts

138 lines
3.9 KiB
TypeScript
Raw Normal View History

2026-02-06 17:50:35 +09:00
import { createHash } from "node:crypto";
import type { KisCredentialInput } from "@/lib/kis/config";
import { getKisConfig, getKisWebSocketUrl } from "@/lib/kis/config";
/**
* @file lib/kis/approval.ts
2026-02-11 15:27:03 +09:00
* @description KIS .
2026-02-06 17:50:35 +09:00
*/
interface KisApprovalResponse {
approval_key?: string;
msg1?: string;
msg_cd?: string;
error?: string;
error_description?: string;
}
interface KisApprovalCache {
approvalKey: string;
expiresAt: number;
}
const approvalCacheMap = new Map<string, KisApprovalCache>();
const approvalIssueInFlightMap = new Map<string, Promise<KisApprovalCache>>();
const APPROVAL_REFRESH_BUFFER_MS = 60 * 1000;
function hashKey(value: string) {
return createHash("sha256").update(value).digest("hex");
}
function getApprovalCacheKey(credentials?: KisCredentialInput) {
const config = getKisConfig(credentials);
return `${config.tradingEnv}:${hashKey(config.appKey)}`;
}
/**
2026-02-11 15:27:03 +09:00
* @description .
* @see app/api/kis/ws/approval/route.ts
2026-02-06 17:50:35 +09:00
*/
2026-02-11 15:27:03 +09:00
async function issueKisApprovalKey(
credentials?: KisCredentialInput,
): Promise<KisApprovalCache> {
2026-02-06 17:50:35 +09:00
const config = getKisConfig(credentials);
const response = await fetch(`${config.baseUrl}/oauth2/Approval`, {
method: "POST",
headers: {
"content-type": "application/json; charset=utf-8",
},
body: JSON.stringify({
grant_type: "client_credentials",
appkey: config.appKey,
2026-02-11 15:27:03 +09:00
// Official samples use `secretkey` for Approval endpoint.
2026-02-06 17:50:35 +09:00
secretkey: config.appSecret,
}),
cache: "no-store",
});
const rawText = await response.text();
const payload = tryParseApprovalResponse(rawText);
if (!response.ok || !payload.approval_key) {
const detail = [payload.msg1, payload.error_description, payload.error, payload.msg_cd]
.filter(Boolean)
.join(" / ");
throw new Error(
detail
? `KIS 웹소켓 승인키 발급 실패 (${config.tradingEnv}, ${response.status}): ${detail}`
: `KIS 웹소켓 승인키 발급 실패 (${config.tradingEnv}, ${response.status})`,
);
}
2026-02-11 15:27:03 +09:00
// KIS samples recommend daily refresh. Cache for 23 hours conservatively.
2026-02-06 17:50:35 +09:00
return {
approvalKey: payload.approval_key,
expiresAt: Date.now() + 23 * 60 * 60 * 1000,
};
}
function tryParseApprovalResponse(rawText: string): KisApprovalResponse {
try {
return JSON.parse(rawText) as KisApprovalResponse;
} catch {
return {
msg1: rawText.slice(0, 200),
};
}
}
/**
2026-02-11 15:27:03 +09:00
* @description .
2026-02-11 16:31:28 +09:00
* @see features/settings/store/use-kis-runtime-store.ts
2026-02-06 17:50:35 +09:00
*/
export async function getKisApprovalKey(credentials?: KisCredentialInput) {
const cacheKey = getApprovalCacheKey(credentials);
const cached = approvalCacheMap.get(cacheKey);
if (cached && cached.expiresAt - APPROVAL_REFRESH_BUFFER_MS > Date.now()) {
return cached.approvalKey;
}
const inFlight = approvalIssueInFlightMap.get(cacheKey);
if (inFlight) {
const shared = await inFlight;
return shared.approvalKey;
}
const nextPromise = issueKisApprovalKey(credentials);
approvalIssueInFlightMap.set(cacheKey, nextPromise);
2026-02-11 15:27:03 +09:00
2026-02-06 17:50:35 +09:00
const next = await nextPromise.finally(() => {
approvalIssueInFlightMap.delete(cacheKey);
});
approvalCacheMap.set(cacheKey, next);
return next.approvalKey;
}
/**
2026-02-11 15:27:03 +09:00
* @description URL을 .
* @see app/api/kis/ws/approval/route.ts
2026-02-06 17:50:35 +09:00
*/
export function resolveKisWebSocketUrl(credentials?: KisCredentialInput) {
const config = getKisConfig(credentials);
return getKisWebSocketUrl(config.tradingEnv);
}
/**
2026-02-11 15:27:03 +09:00
* @description .
* @see lib/kis/token.ts
2026-02-06 17:50:35 +09:00
*/
export function clearKisApprovalKeyCache(credentials?: KisCredentialInput) {
const cacheKey = getApprovalCacheKey(credentials);
approvalCacheMap.delete(cacheKey);
approvalIssueInFlightMap.delete(cacheKey);
}