/** * Thin API client for the database-admin backend. Same-origin cookie auth, * reusing the platform session (`cph_session`) and the admin plane's /api/me. */ export class ApiError extends Error { code: string; status: number; constructor(code: string, message: string, status: number) { super(message); this.name = 'ApiError'; this.code = code; this.status = status; } } async function request(method: string, url: string, body?: unknown): Promise { const init: RequestInit = { method, credentials: 'same-origin', headers: body !== undefined ? { 'content-type': 'application/json' } : undefined, body: body !== undefined ? JSON.stringify(body) : undefined, }; const res = await fetch(url, init); const text = await res.text(); let data: unknown = null; if (text !== '') { try { data = JSON.parse(text); } catch { data = text; } } if (!res.ok) { const err = (data as { error?: { code?: string; message?: string } } | null)?.error; throw new ApiError(err?.code ?? 'http_error', err?.message ?? `HTTP ${res.status}`, res.status); } return data; } const get = (u: string) => request('GET', u); const post = (u: string, b?: unknown) => request('POST', u, b); // --- Types --- export interface OrgMembership { id: string; slug: string; name: string; status: string; role: 'OWNER' | 'ADMIN' | 'MEMBER'; } export interface MeResponse { user: { id: string; feishuOpenId: string; displayName: string; avatarUrl: string | null; }; organizations: OrgMembership[]; } /** Unauthenticated bootstrap the login page needs: which org to OAuth against + dev toggle. */ export interface DatabaseConfig { siloOrganizationSlug: string; devLoginEnabled: boolean; } // --- API --- export const api = { me: () => get('/api/me') as Promise, logout: () => post('/auth/logout'), databaseConfig: () => get('/database/config') as Promise, };