// Minimal async client for the sing-box Clash API, built on libsoup 3. // // Endpoints used: // GET /version liveness + version string // GET /connections snapshot: totals + active connections // GET /proxies all proxies and selector groups // PUT /proxies/{group} {name} switch a Selector group's active node // GET /proxies/{name}/delay?url=... measure latency of a node import Soup from 'gi://Soup'; import GLib from 'gi://GLib'; const textDecoder = new TextDecoder(); const textEncoder = new TextEncoder(); export class ClashApi { constructor({ url = 'http://127.0.0.1:9090', secret = '' } = {}) { this._session = new Soup.Session(); this._session.timeout = 5; this.setEndpoint(url, secret); } setEndpoint(url, secret) { this._baseUrl = String(url || '').replace(/\/+$/, ''); this._secret = secret || ''; } _message(method, path, body) { const msg = Soup.Message.new(method, `${this._baseUrl}${path}`); const headers = msg.get_request_headers(); headers.append('Accept', 'application/json'); if (this._secret) headers.append('Authorization', `Bearer ${this._secret}`); if (body !== undefined) { const bytes = new GLib.Bytes(textEncoder.encode(JSON.stringify(body))); msg.set_request_body_from_bytes('application/json', bytes); } return msg; } _request(method, path, body, cancellable) { return new Promise((resolve, reject) => { const msg = this._message(method, path, body); this._session.send_and_read_async( msg, GLib.PRIORITY_DEFAULT, cancellable ?? null, (session, res) => { let bytes; try { bytes = session.send_and_read_finish(res); } catch (e) { reject(e); return; } const status = msg.get_status(); if (status < 200 || status >= 300) { reject(new Error(`HTTP ${status} ${msg.get_reason_phrase() ?? ''} (${path})`.trim())); return; } try { const data = bytes?.get_data(); const text = data && data.length ? textDecoder.decode(data) : ''; resolve(text ? JSON.parse(text) : null); } catch (e) { reject(e); } } ); }); } getVersion(cancellable) { return this._request('GET', '/version', undefined, cancellable); } getConnections(cancellable) { return this._request('GET', '/connections', undefined, cancellable); } getProxies(cancellable) { return this._request('GET', '/proxies', undefined, cancellable); } selectProxy(group, name, cancellable) { return this._request('PUT', `/proxies/${encodeURIComponent(group)}`, { name }, cancellable); } getDelay(name, { url = 'https://www.gstatic.com/generate_204', timeout = 3000 } = {}, cancellable) { const query = `url=${encodeURIComponent(url)}&timeout=${timeout}`; return this._request('GET', `/proxies/${encodeURIComponent(name)}/delay?${query}`, undefined, cancellable); } }