import { HttpsError } from 'firebase-functions/v2/https'; import type { CallableRequest } from 'firebase-functions/v2/https'; import { ERR, CatalogError } from './errors.js'; /** 호출자 UID. 익명 호출은 여기서 끊는다 — 설계서 §8 「전부 인증 필요」. */ export function callerUid(req: CallableRequest): string { const uid = req.auth?.uid; if (!uid) throw new CatalogError(ERR.authFailed, '로그인이 필요합니다.'); return uid; } export type AdminRole = 'master' | 'ops' | 'viewer'; /** * 운영자 판정 — 커스텀 클레임만 본다. * * Firestore 문서 조회로 판정하지 않는 이유는 보안 규칙과 같다: 판정 1회당 읽기 * 비용이 붙고, 우회 표면이 늘어난다. */ export function requireRole(req: CallableRequest, allowed: AdminRole[]): AdminRole { const role = req.auth?.token?.role as AdminRole | undefined; if (!role || !allowed.includes(role)) { throw new CatalogError(ERR.permissionDenied, '권한이 없습니다.'); } return role; } /** 문자열 필드를 안전하게 꺼낸다. */ export function str(data: unknown, key: string): string { const value = (data as Record | null)?.[key]; if (typeof value !== 'string' || value.length === 0) { throw new HttpsError('invalid-argument', `${key} 이(가) 필요합니다.`); } return value; } export function optionalStr(data: unknown, key: string): string | undefined { const value = (data as Record | null)?.[key]; return typeof value === 'string' && value.length > 0 ? value : undefined; } export function num(data: unknown, key: string): number { const value = (data as Record | null)?.[key]; if (typeof value !== 'number' || !Number.isFinite(value)) { throw new HttpsError('invalid-argument', `${key} 이(가) 필요합니다.`); } return value; }