import type { CloudUser, DeviceRecord, HostRecord, PluginRecord, PluginRegistrationPayload, TaskAttempt, TaskListResponse, TaskStatus, UserCreatePayload, UserListResponse, UserUpdatePayload, } from "./types"; const configuredBaseUrl = import.meta.env.VITE_CLOUD_API_BASE_URL as | string | undefined; export const API_BASE_URL = (configuredBaseUrl || window.location.origin).replace( /\/$/, "", ); const TOKEN_STORAGE_KEY = "cloudConsole.bearerToken"; const CSRF_COOKIE_NAME = "amcp_csrf"; export const AUTH_INVALID_EVENT = "cloud-console:auth-invalid"; export class CloudApiError extends Error { readonly status: number; constructor(status: number, message: string) { super(message); this.status = status; this.name = "CloudApiError"; } } export function getStoredToken(): string | null { try { return sessionStorage.getItem(TOKEN_STORAGE_KEY); } catch { return null; } } export function storeToken(token: string): void { sessionStorage.setItem(TOKEN_STORAGE_KEY, token); } export function clearStoredToken(): void { sessionStorage.removeItem(TOKEN_STORAGE_KEY); } export function hasTokenMode(): boolean { return getStoredToken() !== null; } interface RequestInitLike { method?: string; body?: string | null; headers?: Record; allowAnonymous?: boolean; sessionOnly?: boolean; } async function request(path: string, init: RequestInitLike = {}): Promise { const token = init.sessionOnly ? null : getStoredToken(); if (!init.allowAnonymous && !token && !init.sessionOnly) { // Session mode is permitted, so a missing token is not itself an error. } const method = init.method || "GET"; const headers: Record = { Accept: "application/json", ...init.headers, }; if (token) { headers.Authorization = `Bearer ${token}`; } else if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) { const csrfToken = getCookie(CSRF_COOKIE_NAME); if (csrfToken) headers["X-CSRF-Token"] = csrfToken; } if (init.body !== undefined && init.body !== null) { headers["Content-Type"] = "application/json"; } const response = await fetch(`${API_BASE_URL}${path}`, { method, body: init.body ?? null, headers, credentials: "include", }); if (response.status === 401) { if (token) clearStoredToken(); window.dispatchEvent(new CustomEvent(AUTH_INVALID_EVENT)); throw new CloudApiError(401, await responseDetail(response, "authentication expired")); } if (!response.ok) { throw new CloudApiError( response.status, await responseDetail(response, `${response.status} ${response.statusText}`), ); } if (response.status === 204) return undefined as T; return (await response.json()) as T; } async function responseDetail(response: Response, fallback: string): Promise { try { const payload = (await response.json()) as { detail?: unknown }; if (typeof payload.detail === "string") return payload.detail; if (payload.detail) return JSON.stringify(payload.detail); } catch { // Preserve the fallback for empty or non-JSON responses. } return fallback; } function getCookie(name: string): string | null { const prefix = `${encodeURIComponent(name)}=`; for (const part of document.cookie.split(";")) { const value = part.trim(); if (value.startsWith(prefix)) return decodeURIComponent(value.slice(prefix.length)); } return null; } export function login(username: string, password: string): Promise { return request("/v1/auth/login", { method: "POST", body: JSON.stringify({ username, password }), allowAnonymous: true, sessionOnly: true, }); } export function getCurrentUser(): Promise { return request("/v1/auth/me", { sessionOnly: true }); } export function logout(): Promise { return request("/v1/auth/logout", { method: "POST", sessionOnly: true }); } export function changePassword( currentPassword: string, newPassword: string, ): Promise { return request("/v1/auth/password", { method: "POST", body: JSON.stringify({ current_password: currentPassword, new_password: newPassword, }), sessionOnly: true, }); } export function listTasks(options?: { status?: TaskStatus; limit?: number; offset?: number; }): Promise { const params = new URLSearchParams(); if (options?.status) params.set("status", options.status); params.set("limit", String(options?.limit ?? 50)); params.set("offset", String(options?.offset ?? 0)); const query = params.toString(); return request(`/v1/tasks${query ? `?${query}` : ""}`); } export function getTaskAttempts(taskId: string): Promise { return request(`/v1/tasks/${encodeURIComponent(taskId)}/attempts`); } export function listDevices(): Promise { return request("/v1/devices"); } export function listHosts(): Promise { return request("/v1/hosts"); } export function listPlugins(): Promise { return request("/v1/plugins"); } export function registerPlugin(payload: PluginRegistrationPayload): Promise { return request("/v1/plugins", { method: "POST", body: JSON.stringify(payload), }); } export function listUsers(options?: { limit?: number; offset?: number; }): Promise { const params = new URLSearchParams({ limit: String(options?.limit ?? 50), offset: String(options?.offset ?? 0), }); return request(`/v1/users?${params.toString()}`); } export function createUser(payload: UserCreatePayload): Promise { return request("/v1/users", { method: "POST", body: JSON.stringify(payload), }); } export function updateUser(userId: string, payload: UserUpdatePayload): Promise { return request(`/v1/users/${encodeURIComponent(userId)}`, { method: "PATCH", body: JSON.stringify(payload), }); } export function resetUserPassword(userId: string, password: string): Promise { return request(`/v1/users/${encodeURIComponent(userId)}/password`, { method: "POST", body: JSON.stringify({ password }), }); } export function revokeUserSessions(userId: string): Promise { return request(`/v1/users/${encodeURIComponent(userId)}/sessions`, { method: "DELETE", }); }