import { ERROR_DEFINITIONS, errorDefinition } from '../shared/errors.js'; export class HarborApiError extends Error { constructor(payload = {}, status = 0) { const code = ERROR_DEFINITIONS[payload.code] ? payload.code : 'UNKNOWN'; const definition = errorDefinition(code); super(definition.message); this.name = 'HarborApiError'; this.code = code; this.status = status >= 400 ? status : definition.status; this.retryable = definition.retryable; this.details = payload.details; this.correlationId = payload.correlationId || globalThis.crypto?.randomUUID?.() || new Date().toISOString(); } } export async function request(url, options = {}, fetchImpl = fetch) { let response; try { response = await fetchImpl(url, { ...options, headers: { 'content-type': 'application/json', ...(options.headers || {}), }, }); } catch (error) { if (error?.name === 'AbortError') throw error; throw new HarborApiError({ code: 'CONTROL_UNREACHABLE' }); } let data = {}; try { data = await response.json(); } catch { if (response.ok) throw new HarborApiError({ code: 'UNKNOWN' }, response.status); } if (!response.ok || data?.success === false) { const payload = data?.error && typeof data.error === 'object' ? data.error : { code: response.status >= 500 ? 'CONTROL_UNREACHABLE' : 'UNKNOWN' }; throw new HarborApiError(payload, response.status); } return data; } export const api = { state: () => request('/api/state'), version: () => request('/api/version'), subscription: { validate: (url, { signal } = {}) => request('/api/subscription/validate', { method: 'POST', body: JSON.stringify({ url }), signal, }), fetch: (url) => request('/api/subscription/fetch', { method: 'POST', body: JSON.stringify({ url }), }), refresh: () => request('/api/subscription/refresh', { method: 'POST' }), forget: () => request('/api/subscription', { method: 'DELETE' }), }, apply: (serverId) => request('/api/apply', { method: 'POST', // selectedTag keeps this client compatible with pre-ID Harbor backends. body: JSON.stringify({ serverId, selectedTag: serverId }), }), gatewayAuto: { setEnabled: (enabled) => request('/api/gateway-auto', { method: 'POST', body: JSON.stringify({ enabled }), }), }, routeRules: { update: (rules, expectedRulesRevision) => request('/api/route-rules', { method: 'PUT', body: JSON.stringify({ rules, expectedRulesRevision }), }), }, devices: { list: () => request('/api/devices'), refresh: () => request('/api/devices/refresh', { method: 'POST' }), update: (id, patch, expectedRevision) => request(`/api/devices/${id}`, { method: 'PUT', body: JSON.stringify({ ...patch, expectedRevision }), }), setPolicy: (id, mode, expectedRevision) => request(`/api/devices/${id}/policy`, { method: 'PUT', body: JSON.stringify({ mode, expectedRevision }), }), }, diagnostics: { connectivity: (services = [], target = null) => request('/api/diagnostics/connectivity', { method: 'POST', body: JSON.stringify({ services, target }), }), }, singbox: { stop: () => request('/api/singbox/stop', { method: 'POST' }), restart: () => request('/api/singbox/restart', { method: 'POST' }), }, servers: { ping: (serverIds) => request('/api/servers/ping-all', { method: 'POST', body: JSON.stringify({ serverIds }), }), }, };