/** * Typed fetch client for the panel backend. Same-origin; the session cookie rides automatically * (HttpOnly — JS never reads it). Every call narrows the response and throws `ApiError` on failure * so the UI can react (e.g. bounce to the login screen on 401). */ export interface HostView { readonly hostId: string readonly subdomain: string readonly status: string readonly lastSeen?: string readonly createdAt?: string readonly revokedAt?: string | null readonly notAfter?: string } export interface PairingArtifacts { readonly code: string readonly expiresAt: string readonly pairCommand: string readonly qrDataUrl: string } export class ApiError extends Error { readonly status: number constructor(status: number, message: string) { super(message) this.name = 'ApiError' this.status = status } } async function request(method: string, url: string, body?: unknown): Promise { const init: RequestInit = { method, credentials: 'same-origin', headers: {} } if (body !== undefined) { init.headers = { 'content-type': 'application/json' } init.body = JSON.stringify(body) } return fetch(url, init) } async function json(res: Response): Promise { if (!res.ok) throw new ApiError(res.status, `request failed (${res.status})`) return (await res.json()) as T } export async function getSession(): Promise { const res = await request('GET', '/api/session') const data = await json<{ authenticated: boolean }>(res) return data.authenticated === true } /** Attempt login. Returns true on success; throws ApiError (status) otherwise so callers can message. */ export async function login(password: string): Promise { const res = await request('POST', '/login', { password }) if (res.ok) return true throw new ApiError(res.status, `login failed (${res.status})`) } export async function logout(): Promise { await request('POST', '/logout') } export async function getHosts(): Promise { const res = await request('GET', '/api/hosts') const data = await json<{ hosts: HostView[] }>(res) return data.hosts ?? [] } export async function createPairingCode(): Promise { const res = await request('POST', '/api/pairing-codes') return json(res) } export async function revokeHost(hostId: string): Promise { const res = await request('DELETE', `/api/hosts/${encodeURIComponent(hostId)}`) if (!res.ok && res.status !== 204) throw new ApiError(res.status, `revoke failed (${res.status})`) }