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 function validationStatusForError(error) { return error?.code === 'SUBSCRIPTION_INVALID' ? 'invalid' : 'unavailable'; } 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: (selectedTag) => request('/api/apply', { method: 'POST', body: JSON.stringify({ selectedTag }), }), 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 }), }), }, singbox: { stop: () => request('/api/singbox/stop', { method: 'POST' }), restart: () => request('/api/singbox/restart', { method: 'POST' }), }, servers: { pingAll: () => request('/api/servers/ping-all', { method: 'POST' }), }, };