export const API_BASE = process.env.NEXT_PUBLIC_API_URL || ''; // Auth rides on the Better Auth httpOnly session cookie. With API_BASE unset // (same-origin via Next rewrites) 'same-origin' sends it; a cross-origin // API_BASE needs 'include' plus CORS credentials on the backend. const CREDENTIALS: RequestCredentials = API_BASE ? 'include' : 'same-origin'; export interface ApiError { error: string; } export async function fetchApi( endpoint: string, options: RequestInit = {} ): Promise { const headers: HeadersInit = { 'Content-Type': 'application/json', ...options.headers, }; const res = await fetch(`${API_BASE}${endpoint}`, { credentials: CREDENTIALS, ...options, headers, }); if (!res.ok) { const errorData = await res.json().catch(() => ({ error: 'Request failed' })); const errorMessage = typeof errorData.error === 'string' ? errorData.error : (errorData.message || JSON.stringify(errorData) || 'Request failed'); const error = new Error(errorMessage); // Preserve structured error info (e.g. code: 'EVENT_OVER_CAPACITY') so // callers can react beyond the message text. (error as any).code = errorData.code; (error as any).data = errorData; throw error; } return res.json(); } /** * Fetch a file download (CSV/blob) with auth, returning the blob and the * filename parsed from the Content-Disposition header. */ export async function fetchBlob( endpoint: string, fallbackFilename: string ): Promise<{ blob: Blob; filename: string }> { const res = await fetch(`${API_BASE}${endpoint}`, { credentials: CREDENTIALS }); if (!res.ok) { const errorData = await res.json().catch(() => ({ error: 'Export failed' })); throw new Error(errorData.error || 'Export failed'); } const disposition = res.headers.get('Content-Disposition') || ''; const filenameMatch = disposition.match(/filename="?([^"]+)"?/); const filename = filenameMatch ? filenameMatch[1] : fallbackFilename; const blob = await res.blob(); return { blob, filename }; }