Files
agentic-mobile-control/cloud-console/src/api.ts
T

230 lines
6.4 KiB
TypeScript

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<string, string>;
allowAnonymous?: boolean;
sessionOnly?: boolean;
}
async function request<T>(path: string, init: RequestInitLike = {}): Promise<T> {
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<string, string> = {
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<string> {
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<CloudUser> {
return request<CloudUser>("/v1/auth/login", {
method: "POST",
body: JSON.stringify({ username, password }),
allowAnonymous: true,
sessionOnly: true,
});
}
export function getCurrentUser(): Promise<CloudUser> {
return request<CloudUser>("/v1/auth/me", { sessionOnly: true });
}
export function logout(): Promise<void> {
return request<void>("/v1/auth/logout", { method: "POST", sessionOnly: true });
}
export function changePassword(
currentPassword: string,
newPassword: string,
): Promise<void> {
return request<void>("/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<TaskListResponse> {
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<TaskListResponse>(`/v1/tasks${query ? `?${query}` : ""}`);
}
export function getTaskAttempts(taskId: string): Promise<TaskAttempt[]> {
return request<TaskAttempt[]>(`/v1/tasks/${encodeURIComponent(taskId)}/attempts`);
}
export function listDevices(): Promise<DeviceRecord[]> {
return request<DeviceRecord[]>("/v1/devices");
}
export function listHosts(): Promise<HostRecord[]> {
return request<HostRecord[]>("/v1/hosts");
}
export function listPlugins(): Promise<PluginRecord[]> {
return request<PluginRecord[]>("/v1/plugins");
}
export function registerPlugin(payload: PluginRegistrationPayload): Promise<PluginRecord> {
return request<PluginRecord>("/v1/plugins", {
method: "POST",
body: JSON.stringify(payload),
});
}
export function listUsers(options?: {
limit?: number;
offset?: number;
}): Promise<UserListResponse> {
const params = new URLSearchParams({
limit: String(options?.limit ?? 50),
offset: String(options?.offset ?? 0),
});
return request<UserListResponse>(`/v1/users?${params.toString()}`);
}
export function createUser(payload: UserCreatePayload): Promise<CloudUser> {
return request<CloudUser>("/v1/users", {
method: "POST",
body: JSON.stringify(payload),
});
}
export function updateUser(userId: string, payload: UserUpdatePayload): Promise<CloudUser> {
return request<CloudUser>(`/v1/users/${encodeURIComponent(userId)}`, {
method: "PATCH",
body: JSON.stringify(payload),
});
}
export function resetUserPassword(userId: string, password: string): Promise<CloudUser> {
return request<CloudUser>(`/v1/users/${encodeURIComponent(userId)}/password`, {
method: "POST",
body: JSON.stringify({ password }),
});
}
export function revokeUserSessions(userId: string): Promise<void> {
return request<void>(`/v1/users/${encodeURIComponent(userId)}/sessions`, {
method: "DELETE",
});
}