Files
harbor-net/src/web/api.js
T
dokril e9433e754f
Build and Deploy Gateway / build-and-push (push) Successful in 15s
Build and Deploy Gateway / deploy (push) Successful in 13s
Add targeted connectivity diagnostics with sampled results
2026-08-07 22:11:04 +03:00

111 lines
3.5 KiB
JavaScript

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 }),
}),
},
};