/** * RouterOS 7.x REST API client * * RouterOS 7.1+ exposes a REST API at https:///rest/ * All responses are JSON; authentication is HTTP Basic or Bearer token. * * Usage (browser / server-component): * const client = new RouterOsApiClient({ host: "10.0.0.1", port: 443, ... }) * const ifaces = await client.getInterfaces() */ export interface ApiClientConfig { host: string // "10.0.0.1" port: number // 443 (HTTPS) or 80 (HTTP) tls: boolean // use HTTPS username: string password: string tlsVerify: boolean // verify TLS certificate (false for self-signed) timeout: number // ms, default 8000 } // ─── RouterOS REST response shapes ─────────────────────────────────────────── export interface RosSystemIdentity { name: string } export interface RosSystemResource { uptime: string version: string "build-time": string "free-memory": string "total-memory": string "cpu": string "cpu-count": string "cpu-load": string "board-name": string "platform": string "architecture-name": string "bad-blocks": string } export interface RosInterface { ".id": string name: string type: string mtu: string "actual-mtu": string "mac-address": string running: string disabled: string comment?: string } export interface RosBgpSession { ".id": string name: string "remote.address": string "remote.as": string state: string "uptime": string "prefix-count": string disabled: string } export interface RosWireGuardInterface { ".id": string name: string "listen-port": string mtu: string "public-key": string running: string disabled: string comment?: string } export interface RosWireGuardPeer { ".id": string interface: string "public-key": string "endpoint-address"?: string "endpoint-port"?: string "allowed-address": string "last-handshake"?: string "rx": string "tx": string disabled: string comment?: string } // ─── Error class ───────────────────────────────────────────────────────────── export class RouterOsApiError extends Error { constructor( public readonly status: number, public readonly detail: string, message: string, ) { super(message) this.name = "RouterOsApiError" } } // ─── Client class ───────────────────────────────────────────────────────────── export class RouterOsApiClient { private readonly baseUrl: string private readonly headers: Record private readonly timeout: number constructor(private readonly cfg: ApiClientConfig) { const scheme = cfg.tls ? "https" : "http" this.baseUrl = `${scheme}://${cfg.host}:${cfg.port}/rest` const creds = btoa(`${cfg.username}:${cfg.password}`) this.headers = { "Authorization": `Basic ${creds}`, "Content-Type": "application/json", } this.timeout = cfg.timeout ?? 8000 } // ── low-level fetch ────────────────────────────────────────────────────── private async request( method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE", path: string, body?: unknown, ): Promise { const controller = new AbortController() const timer = setTimeout(() => controller.abort(), this.timeout) try { const res = await fetch(`${this.baseUrl}${path}`, { method, headers: this.headers, body: body !== undefined ? JSON.stringify(body) : undefined, signal: controller.signal, }) clearTimeout(timer) if (!res.ok) { let detail = "" try { detail = (await res.json())?.message ?? "" } catch { /* ignore */ } throw new RouterOsApiError(res.status, detail, `RouterOS API ${method} ${path} → ${res.status}: ${detail}`) } if (res.status === 204) return undefined as T return res.json() as Promise } catch (e) { clearTimeout(timer) throw e } } async get(path: string): Promise { return this.request("GET", path) } async post(path: string, body: unknown): Promise { return this.request("POST", path, body) } /** RouterOS uses PUT for updates on a specific item by /.id */ async put(path: string, id: string, body: unknown): Promise { return this.request("PUT", `${path}/${encodeURIComponent(id)}`, body) } async patch(path: string, id: string, body: unknown): Promise { return this.request("PATCH", `${path}/${encodeURIComponent(id)}`, body) } async delete(path: string, id: string): Promise { return this.request("DELETE", `${path}/${encodeURIComponent(id)}`) } // ── high-level helpers ─────────────────────────────────────────────────── async getSystemIdentity(): Promise { return this.get("/system/identity") } async getSystemResource(): Promise { return this.get("/system/resource") } async getInterfaces(): Promise { return this.get("/interface") } async getBgpSessions(): Promise { return this.get("/routing/bgp/session") } async getWireGuardInterfaces(): Promise { return this.get("/interface/wireguard") } async getWireGuardPeers(): Promise { return this.get("/interface/wireguard/peers") } /** Add a WireGuard interface */ async addWireGuardInterface(params: { name: string listenPort: number mtu?: number comment?: string }): Promise { return this.post("/interface/wireguard", { name: params.name, "listen-port": String(params.listenPort), mtu: String(params.mtu ?? 1420), comment: params.comment ?? "", }) } /** Add a WireGuard peer */ async addWireGuardPeer(params: { iface: string publicKey: string allowedAddresses: string[] endpoint?: string endpointPort?: number persistentKeepalive?: number comment?: string }): Promise { return this.post("/interface/wireguard/peers", { interface: params.iface, "public-key": params.publicKey, "allowed-address": params.allowedAddresses.join(","), ...(params.endpoint ? { "endpoint-address": params.endpoint } : {}), ...(params.endpointPort ? { "endpoint-port": String(params.endpointPort) } : {}), ...(params.persistentKeepalive ? { "persistent-keepalive": String(params.persistentKeepalive) } : {}), comment: params.comment ?? "", }) } /** Ping test from the router — uses /tool/ping */ async ping(address: string, count = 4): Promise<{ "avg-rtt": string; "packet-loss": string }[]> { return this.post("/tool/ping", { address, count: String(count) }) } } // ─── Factory helper ─────────────────────────────────────────────────────────── export function createClient(overrides: Partial & { host: string }): RouterOsApiClient { return new RouterOsApiClient({ port: 443, tls: true, username: "admin", password: "", tlsVerify: false, timeout: 8000, ...overrides, }) }