/** * 서버 함수 공용 출처 검사 (Vercel 서버 함수 전용, '_' 로 시작해 주소로 열리지 않는다). * * infinith-demo 와 그 미리보기, 로컬 개발에서 온 요청만 받는다. 데모는 Vercel 배포 보호(팀원 로그인) 뒤에 있어 * 서버 함수도 로그인한 팀원만 부를 수 있다. 공개 배포(webforai.kr, VITE_SITE=discovery)에서는 내부 함수를 열지 않는다. */ const ALLOWED_ORIGINS = [ 'https://infinith-demo.vercel.app', 'http://localhost:3000', 'http://127.0.0.1:3000', ]; /** 이 프로젝트의 미리보기 배포 주소. 미리보기는 Vercel 로그인(배포 보호) 뒤에 있어 팀원만 연다. */ const PREVIEW_ORIGIN = /^https:\/\/infinith-demo-[a-z0-9]+-o2odev-5530s-projects\.vercel\.app$/; const isAllowedOrigin = (o) => ALLOWED_ORIGINS.includes(o) || PREVIEW_ORIGIN.test(o); /** * CORS·메서드·출처를 확인한다. 통과하면 null, 아니면 이미 응답을 보낸 뒤 true 를 돌려준다. * @param {string[]} methods 허용 메서드 */ export function guard(req, res, methods) { if (process.env.VITE_SITE === 'discovery') { res.status(404).json({ ok: false, error: 'not found' }); return true; } const origin = req.headers.origin || ''; const allowed = isAllowedOrigin(origin); if (allowed) { res.setHeader('Access-Control-Allow-Origin', origin); res.setHeader('Vary', 'Origin'); res.setHeader('Access-Control-Allow-Methods', [...methods, 'OPTIONS'].join(', ')); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); } if (req.method === 'OPTIONS') { res.status(allowed ? 204 : 403).end(); return true; } if (!methods.includes(req.method)) { res.status(405).json({ ok: false, error: 'method not allowed' }); return true; } // 같은 출처 GET 은 브라우저가 Origin 을 붙이지 않을 수 있어 Referer 로도 확인한다. const referer = req.headers.referer || ''; const refOrigin = (referer.match(/^https?:\/\/[^/]+/) || [''])[0]; if (!allowed && !isAllowedOrigin(refOrigin)) { res.status(403).json({ ok: false, error: 'forbidden origin' }); return true; } return null; } export function safeParse(s) { try { return JSON.parse(s); } catch { return null; } }