/** * `/.well-known/security.txt` (RFC 9116). * * ★ 왜 굽나 * 지금은 이 경로가 없어서 nginx 의 SPA 폴백이 사장님 앱 index.html 을 200 으로 준다. * 진단 도구는 200 을 받고 "security.txt 는 있는데 Contact·Expires 가 없다"로 읽는다 — * 404 보다 나쁜 상태다. 파일을 진짜로 놓아 규격대로 답하게 한다. * * ★ Expires 는 규격상 **필수**이고 지나면 파일이 무효다. 그래서 굽는 시점 기준으로 다시 * 계산한다 — 발행할 때마다 갱신되므로 사람이 손대야 하는 날짜가 코드에 남지 않는다. * 1년이 아니라 180일인 이유: 규격 권고가 "1년 이내"고, 반년이면 발행이 뜸한 호스트도 * 만료 전에 한 번은 다시 굽힌다. */ const EXPIRES_DAYS = 180; export interface SecurityTxtOptions { /** `mailto:` 또는 `https:` URL. 없으면 호스트의 security@ 주소로 만든다. */ contact?: string; /** 굽는 시각. 테스트가 고정값을 넣는다. */ now?: Date; } export function renderSecurityTxt(origin: string, options: SecurityTxtOptions = {}): string { const base = origin.replace(/\/+$/, ''); const host = base.replace(/^https?:\/\//, ''); const now = options.now ?? new Date(); const expires = new Date(now.getTime() + EXPIRES_DAYS * 24 * 60 * 60 * 1000); const contact = (options.contact ?? '').trim() || `mailto:security@${host}`; return [ `# ${host} — 취약점 제보 창구 (RFC 9116)`, `# 이 파일은 발행할 때마다 다시 구워진다. Expires 는 굽는 날 + ${EXPIRES_DAYS}일이다.`, '', `Contact: ${contact}`, `Expires: ${expires.toISOString()}`, 'Preferred-Languages: ko, en', `Canonical: ${base}/.well-known/security.txt`, '', ].join('\n'); }