import type { Config, Holding, MediaChunk, Session, Snapshot } from './types'; export class ApiError extends Error { constructor(message: string, public status: number) { super(message); } } // Memory only: never persist passwords or tokens in browser storage or URLs. // This also works when the Hugging Face iframe cannot retain login cookies. let workspaceToken = ''; async function request(path: string, body?: unknown, token?: string, signal?: AbortSignal): Promise { const workspaceRequest = path === '/config' || path === '/sessions'; const response = await fetch(`/api${path}`, {method: body === undefined ? 'GET' : 'POST', headers: { ...(body === undefined ? {} : {'Content-Type': 'application/json'}), ...(token ? {'X-Session-Token': token} : {}), ...(workspaceRequest && workspaceToken ? {'X-Workspace-Token': workspaceToken} : {}) }, body: body === undefined ? undefined : JSON.stringify(body), signal, credentials: 'same-origin', cache: 'no-store', redirect: 'error'}); if (!response.ok) { if (response.status === 401 && workspaceRequest) workspaceToken = ''; // Never reflect arbitrary upstream response bodies, URLs, or credentials into the UI. let message = `Request could not be completed (${response.status}).`; try { const error = await response.json(); if (typeof error.detail === 'string' && error.detail.length < 350 && !/Bearer|hf_|token|https?:\/\//i.test(error.detail)) message = error.detail; } catch { /* retain safe generic message */ } throw new ApiError(message, response.status); } return response.json() as Promise; } export const api = { login: async (password: string) => { workspaceToken = ''; const result = await request<{authenticated: boolean; workspace_token: string}>('/login', {password}); if (!result.authenticated || typeof result.workspace_token !== 'string' || !/^[A-Za-z0-9_-]{43}$/.test(result.workspace_token)) { throw new ApiError('Workspace login could not be confirmed. Try unlocking again.', 502); } workspaceToken = result.workspace_token; }, config: () => request('/config'), create: (holdings: Holding[], objective: string, duration_ms: number | null, source_name: string, signal?: AbortSignal) => request('/sessions', {holdings, objective, duration_ms, source_name, authorized: true}, undefined, signal), poll: (s: Session, signal?: AbortSignal, afterRevision?: number) => request(`/sessions/${s.session_id}${afterRevision === undefined ? '' : `?after_revision=${afterRevision}&wait_seconds=20`}`, undefined, s.session_token, signal), timings: (s: Session) => request>(`/sessions/${s.session_id}/timings`, undefined, s.session_token), chunk: (s: Session, chunk: MediaChunk, signal?: AbortSignal) => request<{accepted_sequence: number; processed_sequence: number; queue_depth: number; queue_capacity: number; duplicate: boolean}>(`/sessions/${s.session_id}/chunks`, chunk, s.session_token, signal), epoch: (s: Session, source_time_ms: number, reason: string, signal?: AbortSignal) => request<{epoch: number}>(`/sessions/${s.session_id}/epoch`, {source_time_ms, reason}, s.session_token, signal), diagnostic: (s: Session, code: string, message: string, source_start_ms?: number, source_end_ms?: number) => request(`/sessions/${s.session_id}/diagnostics`, {epoch: s.epoch, code, message, source_start_ms, source_end_ms}, s.session_token), complete: (s: Session, signal?: AbortSignal) => request(`/sessions/${s.session_id}/complete`, {}, s.session_token, signal), stop: (s: Session) => request(`/sessions/${s.session_id}/stop`, {}, s.session_token), };