@usex/mikrotik-mcp 5.3.0 → 5.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +382 -318
- package/dist/index.js +1 -1
- package/dist/shared/{library-s7s3s14v.js → cli-cvq8pxvn.js} +3480 -1847
- package/dist/shared/{cli-f80vccvs.js → cli-qqwjbjnv.js} +1 -1
- package/dist/shared/{library-4a4mfnxf.js → library-241wsfpw.js} +1 -1
- package/dist/shared/{cli-54yv0kd3.js → library-5p5mk963.js} +2816 -2511
- package/dist/ui/observability.html +64 -64
- package/package.json +2 -2
- package/schemas/README.md +1 -1
- package/schemas/config.schema.json +54 -0
- package/schemas/tool-catalog.json +194 -2
- package/schemas/tools/audit_known_vulnerabilities.json +21 -0
- package/schemas/tools/get_access_scope.json +14 -0
- package/schemas/tools/list_security_advisories.json +12 -0
- package/schemas/tools/locate_host_port.json +13 -0
- package/schemas/tools/map_l2_fabric.json +21 -0
- package/schemas/tools/narrow_access_scope.json +51 -0
package/dist/cli.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// @bun
|
|
3
3
|
import {
|
|
4
4
|
AAA_ENTITIES,
|
|
5
|
+
ADVISORIES,
|
|
6
|
+
ADVISORY_DATASET_DATE,
|
|
5
7
|
ALWAYS_ON_MODULES,
|
|
6
8
|
AlertEngine,
|
|
7
9
|
AlertRuleSchema,
|
|
@@ -40,6 +42,7 @@ import {
|
|
|
40
42
|
blockDevice,
|
|
41
43
|
buildChangePlan,
|
|
42
44
|
buildChannelPlanCommands,
|
|
45
|
+
buildFabricMap,
|
|
43
46
|
buildFtCommands,
|
|
44
47
|
buildGuards,
|
|
45
48
|
buildHaCommands,
|
|
@@ -54,6 +57,8 @@ import {
|
|
|
54
57
|
closeAll,
|
|
55
58
|
closeDevice,
|
|
56
59
|
closeMemoryStore,
|
|
60
|
+
collectDeviceFacts,
|
|
61
|
+
collectFabric,
|
|
57
62
|
commandUnsupported,
|
|
58
63
|
configureRecorder,
|
|
59
64
|
conversations,
|
|
@@ -89,6 +94,7 @@ import {
|
|
|
89
94
|
fetchLatestRelease,
|
|
90
95
|
flowStore,
|
|
91
96
|
forgetJob,
|
|
97
|
+
getAccessPolicy,
|
|
92
98
|
getAlertEngine,
|
|
93
99
|
getCapabilities,
|
|
94
100
|
getConfig,
|
|
@@ -129,6 +135,7 @@ import {
|
|
|
129
135
|
logger,
|
|
130
136
|
looksLikeError,
|
|
131
137
|
makeDeviceStatic,
|
|
138
|
+
matchAdvisories,
|
|
132
139
|
moduleCatalog,
|
|
133
140
|
neverBlockSet,
|
|
134
141
|
nextRun,
|
|
@@ -154,6 +161,7 @@ import {
|
|
|
154
161
|
protocolMix,
|
|
155
162
|
publishRollout,
|
|
156
163
|
readBackup,
|
|
164
|
+
recentDenials,
|
|
157
165
|
redact,
|
|
158
166
|
redactChannels,
|
|
159
167
|
registerTools,
|
|
@@ -230,7 +238,7 @@ import {
|
|
|
230
238
|
validatePolicyText,
|
|
231
239
|
worstSeverity,
|
|
232
240
|
writeBackup
|
|
233
|
-
} from "./shared/cli-
|
|
241
|
+
} from "./shared/cli-cvq8pxvn.js";
|
|
234
242
|
|
|
235
243
|
// src/cli.ts
|
|
236
244
|
import { existsSync as existsSync2 } from "fs";
|
|
@@ -1688,11 +1696,64 @@ async function driftRoutes(req, url) {
|
|
|
1688
1696
|
return null;
|
|
1689
1697
|
}
|
|
1690
1698
|
|
|
1691
|
-
// src/observability/
|
|
1699
|
+
// src/observability/posture-routes.ts
|
|
1692
1700
|
var JSON_HEADERS2 = { "content-type": "application/json; charset=utf-8" };
|
|
1693
1701
|
function json2(body, status = 200) {
|
|
1694
1702
|
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS2 });
|
|
1695
1703
|
}
|
|
1704
|
+
async function postureRoutes(req, url) {
|
|
1705
|
+
const p = url.pathname;
|
|
1706
|
+
if (req.method !== "GET")
|
|
1707
|
+
return null;
|
|
1708
|
+
if (p === "/api/fabric") {
|
|
1709
|
+
const device = url.searchParams.get("device") ?? undefined;
|
|
1710
|
+
const ctx = createContext(undefined, device);
|
|
1711
|
+
try {
|
|
1712
|
+
const map = buildFabricMap(await collectFabric(device, ctx));
|
|
1713
|
+
return json2(map);
|
|
1714
|
+
} catch (e) {
|
|
1715
|
+
return json2({ error: e instanceof Error ? e.message : String(e) }, 502);
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
if (p === "/api/advisories") {
|
|
1719
|
+
const all = url.searchParams.get("all") === "1";
|
|
1720
|
+
const device = url.searchParams.get("device") ?? undefined;
|
|
1721
|
+
const targets = all ? listDevices().names : [device];
|
|
1722
|
+
try {
|
|
1723
|
+
const facts = await Promise.all(targets.map((d) => collectDeviceFacts(d)));
|
|
1724
|
+
const report = matchAdvisories(facts);
|
|
1725
|
+
return json2({
|
|
1726
|
+
...report,
|
|
1727
|
+
datasetDate: ADVISORY_DATASET_DATE,
|
|
1728
|
+
datasetSize: ADVISORIES.length,
|
|
1729
|
+
devices: facts.map((f) => ({
|
|
1730
|
+
device: f.device,
|
|
1731
|
+
version: f.version,
|
|
1732
|
+
board: f.board,
|
|
1733
|
+
enabledServices: f.services.filter((s) => !s.disabled).length,
|
|
1734
|
+
unrestrictedServices: f.services.filter((s) => !s.disabled && s.allowedFrom.length === 0).length
|
|
1735
|
+
}))
|
|
1736
|
+
});
|
|
1737
|
+
} catch (e) {
|
|
1738
|
+
return json2({ error: e instanceof Error ? e.message : String(e) }, 502);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
if (p === "/api/access") {
|
|
1742
|
+
const policy = getAccessPolicy();
|
|
1743
|
+
return json2({
|
|
1744
|
+
enabled: policy.enabled,
|
|
1745
|
+
scope: policy.scope,
|
|
1746
|
+
denials: recentDenials(50)
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1749
|
+
return null;
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
// src/observability/txn-routes.ts
|
|
1753
|
+
var JSON_HEADERS3 = { "content-type": "application/json; charset=utf-8" };
|
|
1754
|
+
function json3(body, status = 200) {
|
|
1755
|
+
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS3 });
|
|
1756
|
+
}
|
|
1696
1757
|
function liveRecord(id) {
|
|
1697
1758
|
const entry = getTxn(id);
|
|
1698
1759
|
return entry ? { ...toRecord(entry.txn, entry.ts, entry.label), updated: Date.now() } : null;
|
|
@@ -1717,16 +1778,16 @@ async function txnRoutes(req, url) {
|
|
|
1717
1778
|
for (const r of live)
|
|
1718
1779
|
byId.set(r.id, r);
|
|
1719
1780
|
const transactions = [...byId.values()].sort((a, b) => b.ts - a.ts).slice(0, limit);
|
|
1720
|
-
return
|
|
1781
|
+
return json3({ transactions, live: liveTxnUpdates(), error: storeError });
|
|
1721
1782
|
}
|
|
1722
1783
|
const parts = p.slice("/api/txn/".length).split("/").filter(Boolean);
|
|
1723
1784
|
if (parts.length === 0 || parts.length > 2)
|
|
1724
|
-
return
|
|
1785
|
+
return json3({ error: "not found" }, 404);
|
|
1725
1786
|
const id = decodeURIComponent(parts[0]);
|
|
1726
1787
|
if (parts[1] === "abort" && req.method === "POST") {
|
|
1727
1788
|
const entry = getTxn(id);
|
|
1728
1789
|
if (!entry)
|
|
1729
|
-
return
|
|
1790
|
+
return json3({ error: `no open transaction '${id}'` }, 404);
|
|
1730
1791
|
entry.txn = requestAbort(entry.txn, "aborted from the dashboard");
|
|
1731
1792
|
const run = await runTransaction({
|
|
1732
1793
|
txn: entry.txn,
|
|
@@ -1737,7 +1798,7 @@ async function txnRoutes(req, url) {
|
|
|
1737
1798
|
await persistTxn(entry);
|
|
1738
1799
|
if (run.state !== undefined)
|
|
1739
1800
|
dropTxn(id);
|
|
1740
|
-
return
|
|
1801
|
+
return json3({
|
|
1741
1802
|
state: run.state,
|
|
1742
1803
|
summary: run.summary,
|
|
1743
1804
|
transaction: toRecord(run.txn, entry.ts)
|
|
@@ -1746,16 +1807,16 @@ async function txnRoutes(req, url) {
|
|
|
1746
1807
|
if (parts.length === 1 && req.method === "GET") {
|
|
1747
1808
|
const record = liveRecord(id) ?? store?.get(id) ?? null;
|
|
1748
1809
|
if (!record)
|
|
1749
|
-
return
|
|
1750
|
-
return
|
|
1810
|
+
return json3({ error: `unknown transaction '${id}'` }, 404);
|
|
1811
|
+
return json3({ transaction: record, events: store?.events(id) ?? [] });
|
|
1751
1812
|
}
|
|
1752
|
-
return
|
|
1813
|
+
return json3({ error: "method not allowed" }, 405);
|
|
1753
1814
|
}
|
|
1754
1815
|
|
|
1755
1816
|
// src/observability/flow-routes.ts
|
|
1756
|
-
var
|
|
1757
|
-
function
|
|
1758
|
-
return new Response(JSON.stringify(body), { status, headers:
|
|
1817
|
+
var JSON_HEADERS4 = { "content-type": "application/json; charset=utf-8" };
|
|
1818
|
+
function json4(body, status = 200) {
|
|
1819
|
+
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS4 });
|
|
1759
1820
|
}
|
|
1760
1821
|
var WINDOWS = {
|
|
1761
1822
|
"5m": 5 * 60000,
|
|
@@ -1775,14 +1836,14 @@ async function flowRoutes(req, url) {
|
|
|
1775
1836
|
if (!p.startsWith("/api/flows"))
|
|
1776
1837
|
return null;
|
|
1777
1838
|
if (req.method !== "GET")
|
|
1778
|
-
return
|
|
1839
|
+
return json4({ error: "method not allowed" }, 405);
|
|
1779
1840
|
const collector = getFlowCollector();
|
|
1780
1841
|
if (p === "/api/flows/health") {
|
|
1781
1842
|
let store = null;
|
|
1782
1843
|
try {
|
|
1783
1844
|
store = (await flowStore()).stats();
|
|
1784
1845
|
} catch {}
|
|
1785
|
-
return
|
|
1846
|
+
return json4({ collector: collector.stats(), store });
|
|
1786
1847
|
}
|
|
1787
1848
|
collector.drain();
|
|
1788
1849
|
let records;
|
|
@@ -1793,12 +1854,12 @@ async function flowRoutes(req, url) {
|
|
|
1793
1854
|
try {
|
|
1794
1855
|
records = (await flowStore()).query({ from, to, address, limit: 200000 });
|
|
1795
1856
|
} catch (e) {
|
|
1796
|
-
return
|
|
1857
|
+
return json4({ error: e instanceof Error ? e.message : String(e), records: [] }, 200);
|
|
1797
1858
|
}
|
|
1798
1859
|
if (p === "/api/flows/top") {
|
|
1799
1860
|
const dimension = url.searchParams.get("dimension") ?? "source";
|
|
1800
1861
|
const limit = Number(url.searchParams.get("limit") ?? 10);
|
|
1801
|
-
return
|
|
1862
|
+
return json4({
|
|
1802
1863
|
window: { from, to },
|
|
1803
1864
|
totals: summarize(records),
|
|
1804
1865
|
top: topTalkers(records, dimension, limit, true),
|
|
@@ -1808,7 +1869,7 @@ async function flowRoutes(req, url) {
|
|
|
1808
1869
|
}
|
|
1809
1870
|
if (p === "/api/flows/conversations") {
|
|
1810
1871
|
const limit = Number(url.searchParams.get("limit") ?? 20);
|
|
1811
|
-
return
|
|
1872
|
+
return json4({
|
|
1812
1873
|
window: { from, to },
|
|
1813
1874
|
conversations: conversations(records, limit)
|
|
1814
1875
|
});
|
|
@@ -1817,20 +1878,20 @@ async function flowRoutes(req, url) {
|
|
|
1817
1878
|
const dimension = url.searchParams.get("dimension") ?? "source";
|
|
1818
1879
|
const topN = Number(url.searchParams.get("topN") ?? 5);
|
|
1819
1880
|
const bucket = bucketFor(span);
|
|
1820
|
-
return
|
|
1881
|
+
return json4({
|
|
1821
1882
|
window: { from, to },
|
|
1822
1883
|
bucketMs: bucket,
|
|
1823
1884
|
keys: topTalkers(records, dimension, topN).map((t) => t.key),
|
|
1824
1885
|
buckets: timeline(records, from, to, bucket, dimension, topN)
|
|
1825
1886
|
});
|
|
1826
1887
|
}
|
|
1827
|
-
return
|
|
1888
|
+
return json4({ error: "not found" }, 404);
|
|
1828
1889
|
}
|
|
1829
1890
|
|
|
1830
1891
|
// src/observability/rollout-routes.ts
|
|
1831
|
-
var
|
|
1832
|
-
function
|
|
1833
|
-
return new Response(JSON.stringify(body), { status, headers:
|
|
1892
|
+
var JSON_HEADERS5 = { "content-type": "application/json; charset=utf-8" };
|
|
1893
|
+
function json5(body, status = 200) {
|
|
1894
|
+
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS5 });
|
|
1834
1895
|
}
|
|
1835
1896
|
function liveRecord2(id) {
|
|
1836
1897
|
const entry = getRollout(id);
|
|
@@ -1859,17 +1920,17 @@ async function rolloutRoutes(req, url) {
|
|
|
1859
1920
|
for (const r of live)
|
|
1860
1921
|
byId.set(r.id, r);
|
|
1861
1922
|
const rollouts = [...byId.values()].sort((a, b) => b.ts - a.ts).slice(0, limit);
|
|
1862
|
-
return
|
|
1923
|
+
return json5({ rollouts, live: liveRolloutUpdates(), error: storeError });
|
|
1863
1924
|
}
|
|
1864
1925
|
const parts = p.slice("/api/rollout/".length).split("/").filter(Boolean);
|
|
1865
1926
|
if (parts.length === 0 || parts.length > 2)
|
|
1866
|
-
return
|
|
1927
|
+
return json5({ error: "not found" }, 404);
|
|
1867
1928
|
const id = decodeURIComponent(parts[0]);
|
|
1868
1929
|
const verb = parts[1];
|
|
1869
1930
|
if (verb && req.method === "POST") {
|
|
1870
1931
|
const entry = getRollout(id);
|
|
1871
1932
|
if (!entry)
|
|
1872
|
-
return
|
|
1933
|
+
return json5({ error: `no active rollout '${id}'` }, 404);
|
|
1873
1934
|
switch (verb) {
|
|
1874
1935
|
case "hold": {
|
|
1875
1936
|
entry.state = requestHold(entry.state);
|
|
@@ -1884,7 +1945,7 @@ async function rolloutRoutes(req, url) {
|
|
|
1884
1945
|
devices: entry.state.devices,
|
|
1885
1946
|
gates: entry.state.gates
|
|
1886
1947
|
});
|
|
1887
|
-
return
|
|
1948
|
+
return json5({ held: true, summary: summarize2(entry.state) });
|
|
1888
1949
|
}
|
|
1889
1950
|
case "resume": {
|
|
1890
1951
|
entry.state = resume(entry.state);
|
|
@@ -1912,7 +1973,7 @@ async function rolloutRoutes(req, url) {
|
|
|
1912
1973
|
await persistRollout(entry);
|
|
1913
1974
|
if (run.outcome)
|
|
1914
1975
|
dropRollout(id);
|
|
1915
|
-
return
|
|
1976
|
+
return json5({ outcome: run.outcome, summary: run.summary });
|
|
1916
1977
|
}
|
|
1917
1978
|
case "abort": {
|
|
1918
1979
|
entry.state = requestAbort2(entry.state, "aborted from the dashboard");
|
|
@@ -1925,19 +1986,19 @@ async function rolloutRoutes(req, url) {
|
|
|
1925
1986
|
await persistRollout(entry);
|
|
1926
1987
|
if (run.outcome)
|
|
1927
1988
|
dropRollout(id);
|
|
1928
|
-
return
|
|
1989
|
+
return json5({ outcome: run.outcome, summary: run.summary });
|
|
1929
1990
|
}
|
|
1930
1991
|
default:
|
|
1931
|
-
return
|
|
1992
|
+
return json5({ error: `unknown action '${verb}'` }, 404);
|
|
1932
1993
|
}
|
|
1933
1994
|
}
|
|
1934
1995
|
if (parts.length === 1 && req.method === "GET") {
|
|
1935
1996
|
const record = liveRecord2(id) ?? store?.get(id) ?? null;
|
|
1936
1997
|
if (!record)
|
|
1937
|
-
return
|
|
1938
|
-
return
|
|
1998
|
+
return json5({ error: `unknown rollout '${id}'` }, 404);
|
|
1999
|
+
return json5({ rollout: record, events: store?.events(id) ?? [] });
|
|
1939
2000
|
}
|
|
1940
|
-
return
|
|
2001
|
+
return json5({ error: "method not allowed" }, 405);
|
|
1941
2002
|
}
|
|
1942
2003
|
|
|
1943
2004
|
// src/policy/results.ts
|
|
@@ -2026,9 +2087,9 @@ async function policyResults(device, limit = 50) {
|
|
|
2026
2087
|
}
|
|
2027
2088
|
|
|
2028
2089
|
// src/observability/policy-routes.ts
|
|
2029
|
-
var
|
|
2030
|
-
function
|
|
2031
|
-
return new Response(JSON.stringify(body), { status, headers:
|
|
2090
|
+
var JSON_HEADERS6 = { "content-type": "application/json; charset=utf-8" };
|
|
2091
|
+
function json6(body, status = 200) {
|
|
2092
|
+
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS6 });
|
|
2032
2093
|
}
|
|
2033
2094
|
async function bodyJson2(req) {
|
|
2034
2095
|
return await req.json().catch(() => ({}));
|
|
@@ -2039,7 +2100,7 @@ async function policyRoutes(req, url) {
|
|
|
2039
2100
|
return null;
|
|
2040
2101
|
if (p === "/api/policies" && req.method === "GET") {
|
|
2041
2102
|
const set = currentPolicySet();
|
|
2042
|
-
return
|
|
2103
|
+
return json6({
|
|
2043
2104
|
files: set.files.map((f) => ({
|
|
2044
2105
|
path: f.path,
|
|
2045
2106
|
name: f.name,
|
|
@@ -2060,21 +2121,21 @@ async function policyRoutes(req, url) {
|
|
|
2060
2121
|
try {
|
|
2061
2122
|
results = await policyResults(device, limit);
|
|
2062
2123
|
} catch (e) {
|
|
2063
|
-
return
|
|
2124
|
+
return json6({ results: [], error: e instanceof Error ? e.message : String(e) });
|
|
2064
2125
|
}
|
|
2065
|
-
return
|
|
2126
|
+
return json6({ results });
|
|
2066
2127
|
}
|
|
2067
2128
|
if (p === "/api/policies/validate" && req.method === "POST") {
|
|
2068
2129
|
const body = await bodyJson2(req);
|
|
2069
2130
|
if (typeof body.content !== "string")
|
|
2070
|
-
return
|
|
2071
|
-
return
|
|
2131
|
+
return json6({ error: "content is required" }, 400);
|
|
2132
|
+
return json6(validatePolicyText(body.content));
|
|
2072
2133
|
}
|
|
2073
2134
|
if (p === "/api/policies/run" && req.method === "POST") {
|
|
2074
2135
|
const body = await bodyJson2(req);
|
|
2075
2136
|
const set = currentPolicySet();
|
|
2076
2137
|
if (set.policies.length === 0) {
|
|
2077
|
-
return
|
|
2138
|
+
return json6({ error: "no policy rules are loaded", reports: [] });
|
|
2078
2139
|
}
|
|
2079
2140
|
const cfg = getConfig();
|
|
2080
2141
|
const devices = body.devices && body.devices.length > 0 ? body.devices : Object.entries(cfg.devices).filter(([, d]) => !d.disabled).map(([name]) => name);
|
|
@@ -2103,9 +2164,9 @@ async function policyRoutes(req, url) {
|
|
|
2103
2164
|
reports.push({ device, error: e instanceof Error ? e.message : String(e) });
|
|
2104
2165
|
}
|
|
2105
2166
|
}
|
|
2106
|
-
return
|
|
2167
|
+
return json6({ reports });
|
|
2107
2168
|
}
|
|
2108
|
-
return
|
|
2169
|
+
return json6({ error: "not found" }, 404);
|
|
2109
2170
|
}
|
|
2110
2171
|
|
|
2111
2172
|
// src/sim/suites.ts
|
|
@@ -2198,9 +2259,9 @@ async function saveSuite(input) {
|
|
|
2198
2259
|
}
|
|
2199
2260
|
|
|
2200
2261
|
// src/observability/sim-routes.ts
|
|
2201
|
-
var
|
|
2202
|
-
function
|
|
2203
|
-
return new Response(JSON.stringify(body), { status, headers:
|
|
2262
|
+
var JSON_HEADERS7 = { "content-type": "application/json; charset=utf-8" };
|
|
2263
|
+
function json7(body, status = 200) {
|
|
2264
|
+
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS7 });
|
|
2204
2265
|
}
|
|
2205
2266
|
async function bodyJson3(req) {
|
|
2206
2267
|
return await req.json().catch(() => ({}));
|
|
@@ -2232,28 +2293,28 @@ async function simRoutes(req, url) {
|
|
|
2232
2293
|
if (!p.startsWith("/api/sim"))
|
|
2233
2294
|
return null;
|
|
2234
2295
|
if (p === "/api/sim/suites" && req.method === "GET") {
|
|
2235
|
-
return
|
|
2296
|
+
return json7({ suites: await listSuites() });
|
|
2236
2297
|
}
|
|
2237
2298
|
if (p === "/api/sim/suites" && req.method === "POST") {
|
|
2238
2299
|
const body = await bodyJson3(req);
|
|
2239
2300
|
if (!body.name || !Array.isArray(body.packets) || body.packets.length === 0) {
|
|
2240
|
-
return
|
|
2301
|
+
return json7({ error: "name and a non-empty packets array are required" }, 400);
|
|
2241
2302
|
}
|
|
2242
2303
|
const saved = await saveSuite({
|
|
2243
2304
|
id: body.id,
|
|
2244
2305
|
name: body.name,
|
|
2245
2306
|
packets: body.packets
|
|
2246
2307
|
});
|
|
2247
|
-
return
|
|
2308
|
+
return json7({ suite: saved });
|
|
2248
2309
|
}
|
|
2249
2310
|
if (p === "/api/sim/packet" && req.method === "POST") {
|
|
2250
2311
|
const body = await bodyJson3(req);
|
|
2251
2312
|
const config = await configFor(body);
|
|
2252
2313
|
if ("error" in config)
|
|
2253
|
-
return
|
|
2314
|
+
return json7(config, 400);
|
|
2254
2315
|
const model = buildModel(config.text);
|
|
2255
2316
|
const result = tracePacket({ model, packet: body.packet });
|
|
2256
|
-
return
|
|
2317
|
+
return json7({
|
|
2257
2318
|
source: config.source,
|
|
2258
2319
|
result,
|
|
2259
2320
|
coverage: {
|
|
@@ -2267,16 +2328,16 @@ async function simRoutes(req, url) {
|
|
|
2267
2328
|
const body = await bodyJson3(req);
|
|
2268
2329
|
const config = await configFor(body);
|
|
2269
2330
|
if ("error" in config)
|
|
2270
|
-
return
|
|
2331
|
+
return json7(config, 400);
|
|
2271
2332
|
if (!body.changes || body.changes.trim() === "") {
|
|
2272
|
-
return
|
|
2333
|
+
return json7({ error: "changes are required" }, 400);
|
|
2273
2334
|
}
|
|
2274
2335
|
const before = tracePacket({ model: buildModel(config.text), packet: body.packet });
|
|
2275
2336
|
const afterModel = buildModel(`${config.text}
|
|
2276
2337
|
${body.changes}
|
|
2277
2338
|
`);
|
|
2278
2339
|
const after = tracePacket({ model: afterModel, packet: body.packet });
|
|
2279
|
-
return
|
|
2340
|
+
return json7({
|
|
2280
2341
|
source: config.source,
|
|
2281
2342
|
before,
|
|
2282
2343
|
after,
|
|
@@ -2291,10 +2352,10 @@ ${body.changes}
|
|
|
2291
2352
|
packet: {}
|
|
2292
2353
|
});
|
|
2293
2354
|
if ("error" in config)
|
|
2294
|
-
return
|
|
2355
|
+
return json7(config, 400);
|
|
2295
2356
|
const model = buildModel(config.text);
|
|
2296
2357
|
const dead = unreachableRules(model.filter);
|
|
2297
|
-
return
|
|
2358
|
+
return json7({
|
|
2298
2359
|
source: config.source,
|
|
2299
2360
|
rules: model.filter.map((r) => ({
|
|
2300
2361
|
chain: r.chain,
|
|
@@ -2315,11 +2376,11 @@ ${body.changes}
|
|
|
2315
2376
|
const id = decodeURIComponent(p.slice("/api/sim/suite/".length, -"/run".length));
|
|
2316
2377
|
const suite = await getSuite(id);
|
|
2317
2378
|
if (!suite)
|
|
2318
|
-
return
|
|
2379
|
+
return json7({ error: `no suite '${id}'` }, 404);
|
|
2319
2380
|
const body = await bodyJson3(req);
|
|
2320
2381
|
const config = await configFor({ ...body, packet: {} });
|
|
2321
2382
|
if ("error" in config)
|
|
2322
|
-
return
|
|
2383
|
+
return json7(config, 400);
|
|
2323
2384
|
const model = buildModel(config.text);
|
|
2324
2385
|
const results = suite.packets.map((entry) => {
|
|
2325
2386
|
const result = tracePacket({ model, packet: entry.packet });
|
|
@@ -2331,7 +2392,7 @@ ${body.changes}
|
|
|
2331
2392
|
summary: result.summary
|
|
2332
2393
|
};
|
|
2333
2394
|
});
|
|
2334
|
-
return
|
|
2395
|
+
return json7({
|
|
2335
2396
|
source: config.source,
|
|
2336
2397
|
suite: { id: suite.id, name: suite.name },
|
|
2337
2398
|
results,
|
|
@@ -2339,13 +2400,13 @@ ${body.changes}
|
|
|
2339
2400
|
total: results.length
|
|
2340
2401
|
});
|
|
2341
2402
|
}
|
|
2342
|
-
return
|
|
2403
|
+
return json7({ error: "not found" }, 404);
|
|
2343
2404
|
}
|
|
2344
2405
|
|
|
2345
2406
|
// src/observability/schedule-routes.ts
|
|
2346
|
-
var
|
|
2347
|
-
function
|
|
2348
|
-
return new Response(JSON.stringify(body), { status, headers:
|
|
2407
|
+
var JSON_HEADERS8 = { "content-type": "application/json; charset=utf-8" };
|
|
2408
|
+
function json8(body, status = 200) {
|
|
2409
|
+
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS8 });
|
|
2349
2410
|
}
|
|
2350
2411
|
function latestPerDevice(runs) {
|
|
2351
2412
|
const latest = new Map;
|
|
@@ -2421,12 +2482,12 @@ async function scheduleRoutes(req, url) {
|
|
|
2421
2482
|
try {
|
|
2422
2483
|
store3 = await scheduleStore();
|
|
2423
2484
|
} catch (e) {
|
|
2424
|
-
return
|
|
2485
|
+
return json8({ error: e instanceof Error ? e.message : String(e), jobs: [] }, 503);
|
|
2425
2486
|
}
|
|
2426
2487
|
const now = Date.now();
|
|
2427
2488
|
if (p === "/api/schedules" && req.method === "GET") {
|
|
2428
2489
|
const jobs = store3.listJobs().map((job) => jobRow(job, store3.runs(job.id, 500), now));
|
|
2429
|
-
return
|
|
2490
|
+
return json8({
|
|
2430
2491
|
jobs,
|
|
2431
2492
|
schedulable: schedulableTools().map((a) => ({ tool: a.tool, summary: a.summary }))
|
|
2432
2493
|
});
|
|
@@ -2434,20 +2495,20 @@ async function scheduleRoutes(req, url) {
|
|
|
2434
2495
|
if (p === "/api/schedules" && req.method === "POST") {
|
|
2435
2496
|
const body = await req.json().catch(() => null);
|
|
2436
2497
|
if (!body)
|
|
2437
|
-
return
|
|
2498
|
+
return json8({ error: "invalid JSON body" }, 400);
|
|
2438
2499
|
const parsed = JobSchema.safeParse(body);
|
|
2439
2500
|
if (!parsed.success) {
|
|
2440
|
-
return
|
|
2501
|
+
return json8({
|
|
2441
2502
|
error: parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")
|
|
2442
2503
|
}, 400);
|
|
2443
2504
|
}
|
|
2444
2505
|
if (!auditAdapter(parsed.data.tool)) {
|
|
2445
|
-
return
|
|
2506
|
+
return json8({ error: `'${parsed.data.tool}' is not a schedulable auditor` }, 400);
|
|
2446
2507
|
}
|
|
2447
2508
|
await primeRiskIndex();
|
|
2448
2509
|
const risk = liveExecutor().riskOf(parsed.data.tool);
|
|
2449
2510
|
if (risk !== "READ") {
|
|
2450
|
-
return
|
|
2511
|
+
return json8({ error: `'${parsed.data.tool}' is annotated ${risk}; only READ may be scheduled` }, 400);
|
|
2451
2512
|
}
|
|
2452
2513
|
const existing = store3.getJob(parsed.data.id);
|
|
2453
2514
|
const job = { ...parsed.data, createdAt: existing?.createdAt ?? now };
|
|
@@ -2456,7 +2517,7 @@ async function scheduleRoutes(req, url) {
|
|
|
2456
2517
|
armJob(job, now);
|
|
2457
2518
|
else
|
|
2458
2519
|
forgetJob(job.id);
|
|
2459
|
-
return
|
|
2520
|
+
return json8({ job: jobRow(job, store3.runs(job.id, 500), now), replaced: existing !== null });
|
|
2460
2521
|
}
|
|
2461
2522
|
if (p === "/api/schedules/timeline" && req.method === "GET") {
|
|
2462
2523
|
const jobId = url.searchParams.get("job") ?? undefined;
|
|
@@ -2464,7 +2525,7 @@ async function scheduleRoutes(req, url) {
|
|
|
2464
2525
|
const days = Number(url.searchParams.get("days") ?? 30);
|
|
2465
2526
|
const since = now - days * 86400000;
|
|
2466
2527
|
const runs = store3.runs(jobId, 2000).filter((r) => r.startedAt >= since && (!device || r.device === device));
|
|
2467
|
-
return
|
|
2528
|
+
return json8({
|
|
2468
2529
|
points: runs.map((r) => ({
|
|
2469
2530
|
at: r.startedAt,
|
|
2470
2531
|
jobId: r.jobId,
|
|
@@ -2484,30 +2545,30 @@ async function scheduleRoutes(req, url) {
|
|
|
2484
2545
|
const limit = Number(url.searchParams.get("limit") ?? 50);
|
|
2485
2546
|
const jobs = jobId ? [jobId] : store3.listJobs().map((j) => j.id);
|
|
2486
2547
|
const items = jobs.flatMap((id2) => regressionsFor(store3.runs(id2, 500), id2)).sort((a, b) => b.at - a.at).slice(0, limit);
|
|
2487
|
-
return
|
|
2548
|
+
return json8({ regressions: items });
|
|
2488
2549
|
}
|
|
2489
2550
|
const parts = p.slice("/api/schedules/".length).split("/").filter(Boolean);
|
|
2490
2551
|
if (parts.length === 0 || parts.length > 2)
|
|
2491
|
-
return
|
|
2552
|
+
return json8({ error: "not found" }, 404);
|
|
2492
2553
|
const id = decodeURIComponent(parts[0]);
|
|
2493
2554
|
const verb = parts[1];
|
|
2494
2555
|
if (!verb && req.method === "DELETE") {
|
|
2495
2556
|
const job = store3.getJob(id);
|
|
2496
2557
|
if (!job)
|
|
2497
|
-
return
|
|
2558
|
+
return json8({ error: `no schedule '${id}'` }, 404);
|
|
2498
2559
|
const runCount = store3.runs(id, 1e4).length;
|
|
2499
2560
|
store3.removeJob(id);
|
|
2500
2561
|
forgetJob(id);
|
|
2501
|
-
return
|
|
2562
|
+
return json8({ removed: id, runsDiscarded: runCount });
|
|
2502
2563
|
}
|
|
2503
2564
|
if (verb === "run" && req.method === "POST") {
|
|
2504
2565
|
const job = store3.getJob(id);
|
|
2505
2566
|
if (!job)
|
|
2506
|
-
return
|
|
2567
|
+
return json8({ error: `no schedule '${id}'` }, 404);
|
|
2507
2568
|
await primeRiskIndex();
|
|
2508
2569
|
logger.info(`schedule '${id}' run requested from the dashboard`);
|
|
2509
2570
|
const result = await runJob(job, liveExecutor(), runnerOptions());
|
|
2510
|
-
return
|
|
2571
|
+
return json8({
|
|
2511
2572
|
...result,
|
|
2512
2573
|
job: jobRow(job, store3.runs(id, 500), Date.now())
|
|
2513
2574
|
});
|
|
@@ -2515,17 +2576,17 @@ async function scheduleRoutes(req, url) {
|
|
|
2515
2576
|
if (!verb && req.method === "GET") {
|
|
2516
2577
|
const job = store3.getJob(id);
|
|
2517
2578
|
if (!job)
|
|
2518
|
-
return
|
|
2579
|
+
return json8({ error: `no schedule '${id}'` }, 404);
|
|
2519
2580
|
const runs = store3.runs(id, 500);
|
|
2520
|
-
return
|
|
2581
|
+
return json8({ job: jobRow(job, runs, now), runs, regressions: regressionsFor(runs, id) });
|
|
2521
2582
|
}
|
|
2522
|
-
return
|
|
2583
|
+
return json8({ error: "not found" }, 404);
|
|
2523
2584
|
}
|
|
2524
2585
|
|
|
2525
2586
|
// src/observability/explain-routes.ts
|
|
2526
|
-
var
|
|
2527
|
-
function
|
|
2528
|
-
return new Response(JSON.stringify(body), { status, headers:
|
|
2587
|
+
var JSON_HEADERS9 = { "content-type": "application/json; charset=utf-8" };
|
|
2588
|
+
function json9(body, status = 200) {
|
|
2589
|
+
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS9 });
|
|
2529
2590
|
}
|
|
2530
2591
|
var storePromise5 = null;
|
|
2531
2592
|
function snapshots2() {
|
|
@@ -2565,53 +2626,53 @@ async function explainRoutes(req, url) {
|
|
|
2565
2626
|
if (p === "/api/explain/diff" && req.method === "POST") {
|
|
2566
2627
|
const body = await req.json().catch(() => null);
|
|
2567
2628
|
if (!body)
|
|
2568
|
-
return
|
|
2629
|
+
return json9({ error: "invalid JSON body" }, 400);
|
|
2569
2630
|
if (!body.before && !body.beforeText) {
|
|
2570
|
-
return
|
|
2631
|
+
return json9({ error: "a diff needs a baseline: pass `before` (a snapshot id) or `beforeText`" }, 400);
|
|
2571
2632
|
}
|
|
2572
2633
|
const device2 = body.device ?? "";
|
|
2573
2634
|
const before = body.beforeText ? { text: body.beforeText, device: device2, source: "supplied text" } : await configFor2(device2, body.before ?? null);
|
|
2574
2635
|
if ("error" in before)
|
|
2575
|
-
return
|
|
2636
|
+
return json9({ error: `before: ${before.error}` }, before.status);
|
|
2576
2637
|
const after = body.afterText ? { text: body.afterText, device: device2, source: "supplied text" } : await configFor2(device2, body.after ?? null);
|
|
2577
2638
|
if ("error" in after)
|
|
2578
|
-
return
|
|
2639
|
+
return json9({ error: `after: ${after.error}` }, after.status);
|
|
2579
2640
|
const diff = diffNarratives(analyzeDevice(before.text, before.device), analyzeDevice(after.text, after.device));
|
|
2580
|
-
return
|
|
2641
|
+
return json9({ diff, markdown: renderDiff(diff), before: before.source, after: after.source });
|
|
2581
2642
|
}
|
|
2582
2643
|
const rest = p.slice("/api/explain/".length).split("/").filter(Boolean);
|
|
2583
2644
|
if (rest.length === 0)
|
|
2584
|
-
return
|
|
2645
|
+
return json9({ error: "not found" }, 404);
|
|
2585
2646
|
if (req.method !== "GET")
|
|
2586
|
-
return
|
|
2647
|
+
return json9({ error: "not found" }, 404);
|
|
2587
2648
|
const device = decodeURIComponent(rest[0]);
|
|
2588
2649
|
const snapshotId = url.searchParams.get("snapshot");
|
|
2589
2650
|
let config;
|
|
2590
2651
|
try {
|
|
2591
2652
|
config = await configFor2(device, snapshotId);
|
|
2592
2653
|
} catch (e) {
|
|
2593
|
-
return
|
|
2654
|
+
return json9({ error: e instanceof Error ? e.message : String(e) }, 502);
|
|
2594
2655
|
}
|
|
2595
2656
|
if ("error" in config)
|
|
2596
|
-
return
|
|
2657
|
+
return json9({ error: config.error }, config.status);
|
|
2597
2658
|
const narrative = { ...analyzeDevice(config.text, config.device), generatedAt: Date.now() };
|
|
2598
2659
|
if (rest.length === 1) {
|
|
2599
|
-
return
|
|
2660
|
+
return json9(payload(narrative, config.source, renderNarrative(narrative)));
|
|
2600
2661
|
}
|
|
2601
2662
|
if (rest[1] === "section" && rest[2]) {
|
|
2602
2663
|
const section = decodeURIComponent(rest[2]);
|
|
2603
2664
|
if (!NARRATIVE_SECTIONS.includes(section)) {
|
|
2604
|
-
return
|
|
2665
|
+
return json9({ error: `unknown section '${section}'. Known: ${NARRATIVE_SECTIONS.join(", ")}` }, 400);
|
|
2605
2666
|
}
|
|
2606
|
-
return
|
|
2667
|
+
return json9(payload(narrative, config.source, renderNarrative(narrative, { sections: [section] })));
|
|
2607
2668
|
}
|
|
2608
|
-
return
|
|
2669
|
+
return json9({ error: "not found" }, 404);
|
|
2609
2670
|
}
|
|
2610
2671
|
|
|
2611
2672
|
// src/observability/attack-routes.ts
|
|
2612
|
-
var
|
|
2613
|
-
function
|
|
2614
|
-
return new Response(JSON.stringify(body), { status, headers:
|
|
2673
|
+
var JSON_HEADERS10 = { "content-type": "application/json; charset=utf-8" };
|
|
2674
|
+
function json10(body, status = 200) {
|
|
2675
|
+
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS10 });
|
|
2615
2676
|
}
|
|
2616
2677
|
async function attackRoutes(req, url) {
|
|
2617
2678
|
const p = url.pathname;
|
|
@@ -2621,7 +2682,7 @@ async function attackRoutes(req, url) {
|
|
|
2621
2682
|
try {
|
|
2622
2683
|
store3 = await attackStore();
|
|
2623
2684
|
} catch (e) {
|
|
2624
|
-
return
|
|
2685
|
+
return json10({ error: e instanceof Error ? e.message : String(e), incidents: [] }, 503);
|
|
2625
2686
|
}
|
|
2626
2687
|
const cfg = getConfig().attacks;
|
|
2627
2688
|
const now = Date.now();
|
|
@@ -2633,7 +2694,7 @@ async function attackRoutes(req, url) {
|
|
|
2633
2694
|
});
|
|
2634
2695
|
const responses = store3.listResponses({ active: true, limit: 500 });
|
|
2635
2696
|
const blocked = new Set(responses.map((r) => r.source));
|
|
2636
|
-
return
|
|
2697
|
+
return json10({
|
|
2637
2698
|
incidents: incidents.map((i) => ({ ...i, blocked: blocked.has(i.source) })),
|
|
2638
2699
|
responses,
|
|
2639
2700
|
posture: {
|
|
@@ -2673,7 +2734,7 @@ async function attackRoutes(req, url) {
|
|
|
2673
2734
|
}
|
|
2674
2735
|
}
|
|
2675
2736
|
const active = new Set(store3.listResponses({ active: true, limit: 500 }).map((r) => r.source));
|
|
2676
|
-
return
|
|
2737
|
+
return json10({
|
|
2677
2738
|
sources: [...bySource.values()].map((s) => ({
|
|
2678
2739
|
source: s.source,
|
|
2679
2740
|
devices: [...s.devices].sort(),
|
|
@@ -2691,14 +2752,14 @@ async function attackRoutes(req, url) {
|
|
|
2691
2752
|
const guards = buildGuards();
|
|
2692
2753
|
const never = [...neverBlockSet(guards)].sort();
|
|
2693
2754
|
const check = url.searchParams.get("check");
|
|
2694
|
-
return
|
|
2755
|
+
return json10({
|
|
2695
2756
|
config: cfg,
|
|
2696
2757
|
neverBlock: never,
|
|
2697
2758
|
check: check ? { address: check, protected: isNeverBlock(check, new Set(never)) } : undefined
|
|
2698
2759
|
});
|
|
2699
2760
|
}
|
|
2700
2761
|
if (p === "/api/attacks/config" && req.method === "POST") {
|
|
2701
|
-
return
|
|
2762
|
+
return json10({
|
|
2702
2763
|
error: "edit the `attacks` block in Config \u2014 the policy has to survive a restart, so it is not held in memory here"
|
|
2703
2764
|
}, 501);
|
|
2704
2765
|
}
|
|
@@ -2711,7 +2772,7 @@ async function attackRoutes(req, url) {
|
|
|
2711
2772
|
onlineOnly: body.onlineOnly,
|
|
2712
2773
|
respond: false
|
|
2713
2774
|
});
|
|
2714
|
-
return
|
|
2775
|
+
return json10({
|
|
2715
2776
|
incidents: result.incidents,
|
|
2716
2777
|
unavailable: result.unavailable,
|
|
2717
2778
|
devices: result.devices,
|
|
@@ -2719,7 +2780,7 @@ async function attackRoutes(req, url) {
|
|
|
2719
2780
|
});
|
|
2720
2781
|
}
|
|
2721
2782
|
if (p === "/api/attacks/devices" && req.method === "GET") {
|
|
2722
|
-
return
|
|
2783
|
+
return json10({
|
|
2723
2784
|
devices: listDevices().names.map((name) => ({
|
|
2724
2785
|
name,
|
|
2725
2786
|
reachable: getDeviceStatus(name).reachable
|
|
@@ -2732,24 +2793,24 @@ async function attackRoutes(req, url) {
|
|
|
2732
2793
|
const response = store3.responseFor(address);
|
|
2733
2794
|
const devices = response?.devices ?? [];
|
|
2734
2795
|
if (devices.length === 0)
|
|
2735
|
-
return
|
|
2796
|
+
return json10({ error: `no recorded block for ${address}` }, 404);
|
|
2736
2797
|
const results = await revokeBlock(address, devices, response?.list);
|
|
2737
2798
|
store3.revokeResponse(address, now);
|
|
2738
|
-
return
|
|
2799
|
+
return json10({ address, results });
|
|
2739
2800
|
}
|
|
2740
2801
|
if (rest.length === 1 && req.method === "GET") {
|
|
2741
2802
|
const incident = store3.getIncident(decodeURIComponent(rest[0]));
|
|
2742
2803
|
if (!incident)
|
|
2743
|
-
return
|
|
2744
|
-
return
|
|
2804
|
+
return json10({ error: "not found" }, 404);
|
|
2805
|
+
return json10({ incident, response: store3.responseFor(incident.source) });
|
|
2745
2806
|
}
|
|
2746
2807
|
if (rest.length === 2 && rest[1] === "respond" && req.method === "POST") {
|
|
2747
2808
|
const incident = store3.getIncident(decodeURIComponent(rest[0]));
|
|
2748
2809
|
if (!incident)
|
|
2749
|
-
return
|
|
2810
|
+
return json10({ error: "not found" }, 404);
|
|
2750
2811
|
const body = await req.json().catch(() => ({}));
|
|
2751
2812
|
if (body.action === "dismiss") {
|
|
2752
|
-
return
|
|
2813
|
+
return json10({ dismissed: incident.id });
|
|
2753
2814
|
}
|
|
2754
2815
|
const decision = decide({
|
|
2755
2816
|
incident,
|
|
@@ -2761,13 +2822,13 @@ async function attackRoutes(req, url) {
|
|
|
2761
2822
|
timeout: body.timeout
|
|
2762
2823
|
});
|
|
2763
2824
|
if (!isPlan(decision)) {
|
|
2764
|
-
return
|
|
2825
|
+
return json10({ refused: true, guard: decision.guard, reason: decision.reason }, 200);
|
|
2765
2826
|
}
|
|
2766
2827
|
if (decision.action === "escalate") {
|
|
2767
|
-
return
|
|
2828
|
+
return json10({ escalated: true, reason: decision.reason });
|
|
2768
2829
|
}
|
|
2769
2830
|
if (!body.confirm) {
|
|
2770
|
-
return
|
|
2831
|
+
return json10({ dryRun: true, plan: decision });
|
|
2771
2832
|
}
|
|
2772
2833
|
const applied = await executePlan(decision);
|
|
2773
2834
|
const ok = applied.some((r) => r.ok);
|
|
@@ -2783,15 +2844,15 @@ async function attackRoutes(req, url) {
|
|
|
2783
2844
|
ok,
|
|
2784
2845
|
error: ok ? undefined : applied.map((r) => r.detail).join("; ")
|
|
2785
2846
|
});
|
|
2786
|
-
return
|
|
2847
|
+
return json10({ applied, ok, plan: decision });
|
|
2787
2848
|
}
|
|
2788
|
-
return
|
|
2849
|
+
return json10({ error: "not found" }, 404);
|
|
2789
2850
|
}
|
|
2790
2851
|
|
|
2791
2852
|
// src/observability/alert-routes.ts
|
|
2792
|
-
var
|
|
2793
|
-
function
|
|
2794
|
-
return new Response(JSON.stringify(body), { status, headers:
|
|
2853
|
+
var JSON_HEADERS11 = { "content-type": "application/json; charset=utf-8" };
|
|
2854
|
+
function json11(body, status = 200) {
|
|
2855
|
+
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS11 });
|
|
2795
2856
|
}
|
|
2796
2857
|
function ruleRow(rule, subject, status, since) {
|
|
2797
2858
|
return {
|
|
@@ -2845,11 +2906,11 @@ async function alertRoutes(req, url, db) {
|
|
|
2845
2906
|
const channels = getConfig().alerts?.channels;
|
|
2846
2907
|
if (p === "/api/alerts" && req.method === "GET") {
|
|
2847
2908
|
if (!engine) {
|
|
2848
|
-
return
|
|
2909
|
+
return json11({ configured: false, rules: [], active: [], channels: {} });
|
|
2849
2910
|
}
|
|
2850
2911
|
const now = Date.now();
|
|
2851
2912
|
const rows = engine.snapshot().map(({ rule, subject, state }) => ruleRow(rule, subject, isMuted(rule, now) ? "muted" : !rule.enabled ? "disabled" : state.status, state.since));
|
|
2852
|
-
return
|
|
2913
|
+
return json11({
|
|
2853
2914
|
configured: true,
|
|
2854
2915
|
rules: rows,
|
|
2855
2916
|
active: rows.filter((r) => r.status === "firing"),
|
|
@@ -2858,41 +2919,41 @@ async function alertRoutes(req, url, db) {
|
|
|
2858
2919
|
}
|
|
2859
2920
|
if (p === "/api/alerts/history" && req.method === "GET") {
|
|
2860
2921
|
if (!engine)
|
|
2861
|
-
return
|
|
2922
|
+
return json11({ history: [] });
|
|
2862
2923
|
const hours = Number(url.searchParams.get("hours") ?? 24);
|
|
2863
2924
|
const history = await engine.history({
|
|
2864
2925
|
sinceMs: Date.now() - hours * 3600000,
|
|
2865
2926
|
ruleId: url.searchParams.get("rule") ?? undefined,
|
|
2866
2927
|
limit: Number(url.searchParams.get("limit") ?? 200)
|
|
2867
2928
|
});
|
|
2868
|
-
return
|
|
2929
|
+
return json11({ history });
|
|
2869
2930
|
}
|
|
2870
2931
|
if (p === "/api/alerts/rules" && req.method === "POST") {
|
|
2871
2932
|
if (!engine)
|
|
2872
|
-
return
|
|
2933
|
+
return json11({ error: "alerting is not configured" }, 400);
|
|
2873
2934
|
const parsed = AlertRuleSchema.safeParse(await req.json());
|
|
2874
2935
|
if (!parsed.success) {
|
|
2875
|
-
return
|
|
2936
|
+
return json11({ error: parsed.error.issues.map((i) => i.message).join("; ") }, 400);
|
|
2876
2937
|
}
|
|
2877
2938
|
const rules = engine.configuredRules();
|
|
2878
2939
|
if (rules.some((r) => r.id === parsed.data.id)) {
|
|
2879
|
-
return
|
|
2940
|
+
return json11({ error: `rule '${parsed.data.id}' already exists` }, 409);
|
|
2880
2941
|
}
|
|
2881
2942
|
engine.setRules([...rules, parsed.data]);
|
|
2882
|
-
return
|
|
2943
|
+
return json11({ ok: true, id: parsed.data.id });
|
|
2883
2944
|
}
|
|
2884
2945
|
const idMatch = p.match(/^\/api\/alerts\/rules\/([^/]+)$/);
|
|
2885
2946
|
if (idMatch) {
|
|
2886
2947
|
if (!engine)
|
|
2887
|
-
return
|
|
2948
|
+
return json11({ error: "alerting is not configured" }, 400);
|
|
2888
2949
|
const id = decodeURIComponent(idMatch[1]);
|
|
2889
2950
|
const rules = engine.configuredRules();
|
|
2890
2951
|
const existing = rules.find((r) => r.id === id);
|
|
2891
2952
|
if (!existing)
|
|
2892
|
-
return
|
|
2953
|
+
return json11({ error: `unknown rule: ${id}` }, 404);
|
|
2893
2954
|
if (req.method === "DELETE") {
|
|
2894
2955
|
engine.setRules(rules.filter((r) => r.id !== id));
|
|
2895
|
-
return
|
|
2956
|
+
return json11({ ok: true });
|
|
2896
2957
|
}
|
|
2897
2958
|
if (req.method === "PATCH") {
|
|
2898
2959
|
const body = await req.json();
|
|
@@ -2900,22 +2961,22 @@ async function alertRoutes(req, url, db) {
|
|
|
2900
2961
|
if (typeof body.mute === "string") {
|
|
2901
2962
|
const ms = parseDuration(body.mute);
|
|
2902
2963
|
if (ms === null)
|
|
2903
|
-
return
|
|
2964
|
+
return json11({ error: `invalid duration: ${body.mute}` }, 400);
|
|
2904
2965
|
mutedUntil = ms === 0 ? undefined : Date.now() + ms;
|
|
2905
2966
|
delete body.mute;
|
|
2906
2967
|
}
|
|
2907
2968
|
const merged = AlertRuleSchema.safeParse({ ...existing, ...body, mutedUntil });
|
|
2908
2969
|
if (!merged.success) {
|
|
2909
|
-
return
|
|
2970
|
+
return json11({ error: merged.error.issues.map((i) => i.message).join("; ") }, 400);
|
|
2910
2971
|
}
|
|
2911
2972
|
engine.setRules(rules.map((r) => r.id === id ? merged.data : r));
|
|
2912
|
-
return
|
|
2973
|
+
return json11({ ok: true, rule: merged.data });
|
|
2913
2974
|
}
|
|
2914
2975
|
}
|
|
2915
2976
|
if (p === "/api/alerts/test" && req.method === "POST") {
|
|
2916
2977
|
const body = await req.json();
|
|
2917
2978
|
if (!channels || !body?.channel)
|
|
2918
|
-
return
|
|
2979
|
+
return json11({ error: "channel is required" }, 400);
|
|
2919
2980
|
try {
|
|
2920
2981
|
const result = await deliver(body.channel, channels, {
|
|
2921
2982
|
ruleId: "test",
|
|
@@ -2925,20 +2986,20 @@ async function alertRoutes(req, url, db) {
|
|
|
2925
2986
|
body: "If you can read this, this channel is configured correctly.",
|
|
2926
2987
|
at: Date.now()
|
|
2927
2988
|
});
|
|
2928
|
-
return
|
|
2989
|
+
return json11(result);
|
|
2929
2990
|
} catch (e) {
|
|
2930
2991
|
logger.error(`Alert channel test failed: ${logError(e)}`);
|
|
2931
|
-
return
|
|
2992
|
+
return json11({ error: clientError(e) }, 502);
|
|
2932
2993
|
}
|
|
2933
2994
|
}
|
|
2934
2995
|
if (p === "/api/alerts/preview" && req.method === "POST") {
|
|
2935
2996
|
const parsed = AlertRuleSchema.safeParse(await req.json());
|
|
2936
2997
|
if (!parsed.success) {
|
|
2937
|
-
return
|
|
2998
|
+
return json11({ error: parsed.error.issues.map((i) => i.message).join("; ") }, 400);
|
|
2938
2999
|
}
|
|
2939
3000
|
const hours = Number(url.searchParams.get("hours") ?? 24);
|
|
2940
3001
|
if (!db) {
|
|
2941
|
-
return
|
|
3002
|
+
return json11({ hours, sampled: 0, fires: 0, resolves: 0, matched: 0, noHistory: true });
|
|
2942
3003
|
}
|
|
2943
3004
|
const events = db.query({ since: Date.now() - hours * 3600000, limit: 5000 }).map((e) => ({
|
|
2944
3005
|
ts: e.ts,
|
|
@@ -2947,15 +3008,15 @@ async function alertRoutes(req, url, db) {
|
|
|
2947
3008
|
risk: e.risk,
|
|
2948
3009
|
isError: e.isError
|
|
2949
3010
|
}));
|
|
2950
|
-
return
|
|
3011
|
+
return json11({ hours, sampled: events.length, ...previewRule(parsed.data, events) });
|
|
2951
3012
|
}
|
|
2952
3013
|
return null;
|
|
2953
3014
|
}
|
|
2954
3015
|
|
|
2955
3016
|
// src/observability/memory-routes.ts
|
|
2956
|
-
var
|
|
2957
|
-
function
|
|
2958
|
-
return new Response(JSON.stringify(body), { status, headers:
|
|
3017
|
+
var JSON_HEADERS12 = { "content-type": "application/json; charset=utf-8" };
|
|
3018
|
+
function json12(body, status = 200) {
|
|
3019
|
+
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS12 });
|
|
2959
3020
|
}
|
|
2960
3021
|
async function bodyJson4(req) {
|
|
2961
3022
|
return await req.json();
|
|
@@ -2966,7 +3027,7 @@ async function memoryRoutes(req, url) {
|
|
|
2966
3027
|
return null;
|
|
2967
3028
|
const cfg = getConfig();
|
|
2968
3029
|
if (!cfg.memory.enabled) {
|
|
2969
|
-
return
|
|
3030
|
+
return json12({ error: "Knowledge-graph memory is disabled" }, 503);
|
|
2970
3031
|
}
|
|
2971
3032
|
if (p === "/api/memory/config" && req.method === "GET") {
|
|
2972
3033
|
let stats = null;
|
|
@@ -2974,7 +3035,7 @@ async function memoryRoutes(req, url) {
|
|
|
2974
3035
|
const store4 = await getMemoryStore();
|
|
2975
3036
|
stats = store4.stats();
|
|
2976
3037
|
} catch {}
|
|
2977
|
-
return
|
|
3038
|
+
return json12({
|
|
2978
3039
|
enabled: cfg.memory.enabled,
|
|
2979
3040
|
dbPath: cfg.memory.dbPath,
|
|
2980
3041
|
stats
|
|
@@ -2988,7 +3049,7 @@ async function memoryRoutes(req, url) {
|
|
|
2988
3049
|
try {
|
|
2989
3050
|
await reopenMemoryStore(body.dbPath);
|
|
2990
3051
|
} catch (e) {
|
|
2991
|
-
return
|
|
3052
|
+
return json12({
|
|
2992
3053
|
error: `Failed to open memory DB at ${body.dbPath}: ${e instanceof Error ? e.message : String(e)}`
|
|
2993
3054
|
}, 400);
|
|
2994
3055
|
}
|
|
@@ -3010,7 +3071,7 @@ async function memoryRoutes(req, url) {
|
|
|
3010
3071
|
const store4 = await getMemoryStore();
|
|
3011
3072
|
stats = store4.stats();
|
|
3012
3073
|
} catch {}
|
|
3013
|
-
return
|
|
3074
|
+
return json12({
|
|
3014
3075
|
ok: true,
|
|
3015
3076
|
enabled: getConfig().memory.enabled,
|
|
3016
3077
|
dbPath: getConfig().memory.dbPath,
|
|
@@ -3019,67 +3080,67 @@ async function memoryRoutes(req, url) {
|
|
|
3019
3080
|
}
|
|
3020
3081
|
const store3 = await getMemoryStore();
|
|
3021
3082
|
if (p === "/api/memory/graph" && req.method === "GET") {
|
|
3022
|
-
return
|
|
3083
|
+
return json12(store3.readGraph());
|
|
3023
3084
|
}
|
|
3024
3085
|
if (p === "/api/memory/stats" && req.method === "GET") {
|
|
3025
|
-
return
|
|
3086
|
+
return json12(store3.stats());
|
|
3026
3087
|
}
|
|
3027
3088
|
if (p === "/api/memory/search" && req.method === "GET") {
|
|
3028
3089
|
const q = url.searchParams.get("q") || "";
|
|
3029
3090
|
const limit = Number(url.searchParams.get("limit") ?? 50);
|
|
3030
|
-
return
|
|
3091
|
+
return json12(store3.searchNodes(q, limit));
|
|
3031
3092
|
}
|
|
3032
3093
|
if (p === "/api/memory/activity" && req.method === "GET") {
|
|
3033
3094
|
const limit = Number(url.searchParams.get("limit") ?? 50);
|
|
3034
3095
|
const since = url.searchParams.get("since");
|
|
3035
|
-
return
|
|
3096
|
+
return json12(store3.activity(limit, since ? Number(since) : undefined));
|
|
3036
3097
|
}
|
|
3037
3098
|
const entityMatch = p.match(/^\/api\/memory\/entity\/(.+)$/);
|
|
3038
3099
|
if (entityMatch && req.method === "GET") {
|
|
3039
3100
|
const name = decodeURIComponent(entityMatch[1]);
|
|
3040
3101
|
const graph = store3.openNodes([name]);
|
|
3041
3102
|
if (graph.entities.length === 0)
|
|
3042
|
-
return
|
|
3043
|
-
return
|
|
3103
|
+
return json12({ error: "Entity not found" }, 404);
|
|
3104
|
+
return json12({ entity: graph.entities[0], relations: graph.relations });
|
|
3044
3105
|
}
|
|
3045
3106
|
if (p === "/api/memory/entities" && req.method === "POST") {
|
|
3046
3107
|
const body = await bodyJson4(req);
|
|
3047
3108
|
const created = store3.createEntities(body.entities ?? []);
|
|
3048
|
-
return
|
|
3109
|
+
return json12({ created, count: created.length });
|
|
3049
3110
|
}
|
|
3050
3111
|
if (p === "/api/memory/relations" && req.method === "POST") {
|
|
3051
3112
|
const body = await bodyJson4(req);
|
|
3052
3113
|
const created = store3.createRelations(body.relations ?? []);
|
|
3053
|
-
return
|
|
3114
|
+
return json12({ created, count: created.length });
|
|
3054
3115
|
}
|
|
3055
3116
|
if (p === "/api/memory/observations" && req.method === "POST") {
|
|
3056
3117
|
const body = await bodyJson4(req);
|
|
3057
3118
|
const results = store3.addObservations(body.observations ?? []);
|
|
3058
|
-
return
|
|
3119
|
+
return json12({ results });
|
|
3059
3120
|
}
|
|
3060
3121
|
if (p === "/api/memory/entities" && req.method === "DELETE") {
|
|
3061
3122
|
const body = await bodyJson4(req);
|
|
3062
3123
|
const removed = store3.deleteEntities(body.names ?? []);
|
|
3063
|
-
return
|
|
3124
|
+
return json12({ removed });
|
|
3064
3125
|
}
|
|
3065
3126
|
if (p === "/api/memory/relations" && req.method === "DELETE") {
|
|
3066
3127
|
const body = await bodyJson4(req);
|
|
3067
3128
|
const removed = store3.deleteRelations(body.relations ?? []);
|
|
3068
|
-
return
|
|
3129
|
+
return json12({ removed });
|
|
3069
3130
|
}
|
|
3070
3131
|
if (p === "/api/memory/observations" && req.method === "DELETE") {
|
|
3071
3132
|
const body = await bodyJson4(req);
|
|
3072
3133
|
const removed = store3.deleteObservations(body.deletions ?? []);
|
|
3073
|
-
return
|
|
3134
|
+
return json12({ removed });
|
|
3074
3135
|
}
|
|
3075
3136
|
return null;
|
|
3076
3137
|
}
|
|
3077
3138
|
|
|
3078
3139
|
// src/observability/dashboard.ts
|
|
3079
3140
|
var SERVER_TAG3 = "mikrotik-mcp";
|
|
3080
|
-
var
|
|
3081
|
-
function
|
|
3082
|
-
return new Response(JSON.stringify(body), { status, headers:
|
|
3141
|
+
var JSON_HEADERS13 = { "content-type": "application/json; charset=utf-8" };
|
|
3142
|
+
function json13(body, status = 200) {
|
|
3143
|
+
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS13 });
|
|
3083
3144
|
}
|
|
3084
3145
|
function restartProcess() {
|
|
3085
3146
|
let ok = false;
|
|
@@ -3379,11 +3440,11 @@ function issues(error) {
|
|
|
3379
3440
|
async function configRoutes(req, url, admin) {
|
|
3380
3441
|
const p = url.pathname;
|
|
3381
3442
|
if (p === "/api/config-schema" && req.method === "GET") {
|
|
3382
|
-
return
|
|
3443
|
+
return json13(configSchemaJson());
|
|
3383
3444
|
}
|
|
3384
3445
|
if (p === "/api/config/validate" && req.method === "POST") {
|
|
3385
3446
|
const merged = mergeSecrets(await readJson(req), getConfig());
|
|
3386
|
-
return
|
|
3447
|
+
return json13(validateConfig(merged));
|
|
3387
3448
|
}
|
|
3388
3449
|
if (p === "/api/config/test-device" && req.method === "POST") {
|
|
3389
3450
|
const body = await readJson(req);
|
|
@@ -3392,22 +3453,22 @@ async function configRoutes(req, url, admin) {
|
|
|
3392
3453
|
const merged = mergeSecrets(body?.config, currentDc);
|
|
3393
3454
|
const parsed = DeviceConfigSchema.safeParse(merged);
|
|
3394
3455
|
if (!parsed.success)
|
|
3395
|
-
return
|
|
3456
|
+
return json13({ ok: false, errors: issues(parsed.error) }, 400);
|
|
3396
3457
|
const status = await probeDevice(`config-test:${name}`, parsed.data);
|
|
3397
|
-
return
|
|
3458
|
+
return json13({ ok: true, status });
|
|
3398
3459
|
}
|
|
3399
3460
|
if (p === "/api/config/preview" && req.method === "POST") {
|
|
3400
3461
|
const before = JSON.stringify(redact(getConfig()), null, 2);
|
|
3401
3462
|
const after = JSON.stringify(await readJson(req) ?? {}, null, 2);
|
|
3402
3463
|
const d = diffLines(before, after, { fromLabel: "current", toLabel: "edited" });
|
|
3403
|
-
return
|
|
3464
|
+
return json13({ summary: d.summary, unified: d.unified });
|
|
3404
3465
|
}
|
|
3405
3466
|
if (p === "/api/config" && req.method === "POST") {
|
|
3406
3467
|
const body = await readJson(req);
|
|
3407
3468
|
const merged = mergeSecrets(body?.config, getConfig());
|
|
3408
3469
|
const v = validateConfig(merged);
|
|
3409
3470
|
if (!v.ok || !v.value)
|
|
3410
|
-
return
|
|
3471
|
+
return json13({ ok: false, errors: v.errors }, 400);
|
|
3411
3472
|
const prev = getConfig();
|
|
3412
3473
|
const before = JSON.stringify(redact(prev), null, 2);
|
|
3413
3474
|
const after = JSON.stringify(redact(v.value), null, 2);
|
|
@@ -3416,25 +3477,25 @@ async function configRoutes(req, url, admin) {
|
|
|
3416
3477
|
const nextDevs = Object.keys(v.value.devices);
|
|
3417
3478
|
const devicesChanged = prevDevs.length !== nextDevs.length || prevDevs.some((d) => !nextDevs.includes(d));
|
|
3418
3479
|
const res = admin.applyConfig(v.value, clampRollback(body?.rollbackMs));
|
|
3419
|
-
return
|
|
3480
|
+
return json13({ ok: true, ...res, devicesChanged, summary: diff.summary, unified: diff.unified });
|
|
3420
3481
|
}
|
|
3421
3482
|
if (p === "/api/config/keep" && req.method === "POST") {
|
|
3422
3483
|
const body = await readJson(req);
|
|
3423
3484
|
const kept = admin.keepConfig(String(body?.pendingId ?? ""));
|
|
3424
3485
|
if (kept)
|
|
3425
3486
|
recordVersion(getConfig(), "auto", Date.now());
|
|
3426
|
-
return
|
|
3487
|
+
return json13({ kept });
|
|
3427
3488
|
}
|
|
3428
3489
|
if (p === "/api/config/rollback" && req.method === "POST") {
|
|
3429
3490
|
const body = await readJson(req);
|
|
3430
3491
|
const rolledBack = admin.rollback(String(body?.pendingId ?? ""));
|
|
3431
|
-
return
|
|
3492
|
+
return json13({ rolledBack, config: redact(getConfig()) });
|
|
3432
3493
|
}
|
|
3433
3494
|
if (p === "/api/reload" && req.method === "POST") {
|
|
3434
3495
|
const body = await readJson(req);
|
|
3435
3496
|
if (body?.hard === true) {
|
|
3436
3497
|
const relaunched = restartProcess();
|
|
3437
|
-
return
|
|
3498
|
+
return json13({
|
|
3438
3499
|
ok: true,
|
|
3439
3500
|
mode: "restart",
|
|
3440
3501
|
note: relaunched ? "Restarting now \u2014 a fresh server process was launched and will bind the same port(s) in ~1.5s. Reconnect shortly." : "Could not self-relaunch; the process is exiting. It only comes back if a supervisor respawns it."
|
|
@@ -3445,10 +3506,10 @@ async function configRoutes(req, url, admin) {
|
|
|
3445
3506
|
setConfig(cfg);
|
|
3446
3507
|
const devices = Object.keys(cfg.devices);
|
|
3447
3508
|
logger.info(`Config reloaded from source \u2014 ${devices.length} device(s): ${devices.join(", ")}`);
|
|
3448
|
-
return
|
|
3509
|
+
return json13({ ok: true, mode: "config", count: devices.length, devices });
|
|
3449
3510
|
} catch (e) {
|
|
3450
3511
|
logger.error(`Config reload failed: ${logError(e)}`);
|
|
3451
|
-
return
|
|
3512
|
+
return json13({ ok: false, error: clientError(e) }, 500);
|
|
3452
3513
|
}
|
|
3453
3514
|
}
|
|
3454
3515
|
if (p === "/api/config/history" && req.method === "GET") {
|
|
@@ -3464,55 +3525,55 @@ async function configRoutes(req, url, admin) {
|
|
|
3464
3525
|
} catch {}
|
|
3465
3526
|
return { ...v, drift: { added, removed } };
|
|
3466
3527
|
});
|
|
3467
|
-
return
|
|
3528
|
+
return json13({ versions, bytes: historyBytes(), retention: AUTO_RETENTION });
|
|
3468
3529
|
}
|
|
3469
3530
|
if (p === "/api/config/history/get" && req.method === "GET") {
|
|
3470
3531
|
const id = url.searchParams.get("id");
|
|
3471
3532
|
if (!id)
|
|
3472
|
-
return
|
|
3533
|
+
return json13({ error: "id required" }, 400);
|
|
3473
3534
|
try {
|
|
3474
3535
|
const v = readVersion(id);
|
|
3475
|
-
return
|
|
3536
|
+
return json13({
|
|
3476
3537
|
ts: v.ts,
|
|
3477
3538
|
kind: v.kind,
|
|
3478
3539
|
label: v.label,
|
|
3479
3540
|
config: JSON.stringify(redact(v.config), null, 2)
|
|
3480
3541
|
});
|
|
3481
3542
|
} catch {
|
|
3482
|
-
return
|
|
3543
|
+
return json13({ error: "not found" }, 404);
|
|
3483
3544
|
}
|
|
3484
3545
|
}
|
|
3485
3546
|
if (p === "/api/config/history/diff" && req.method === "GET") {
|
|
3486
3547
|
const id = url.searchParams.get("id");
|
|
3487
3548
|
if (!id)
|
|
3488
|
-
return
|
|
3549
|
+
return json13({ error: "id required" }, 400);
|
|
3489
3550
|
try {
|
|
3490
3551
|
const before = JSON.stringify(redact(readVersion(id).config), null, 2);
|
|
3491
3552
|
const after = JSON.stringify(redact(getConfig()), null, 2);
|
|
3492
3553
|
const d = diffLines(before, after, { fromLabel: "this version", toLabel: "current" });
|
|
3493
|
-
return
|
|
3554
|
+
return json13({ summary: d.summary, unified: d.unified });
|
|
3494
3555
|
} catch {
|
|
3495
|
-
return
|
|
3556
|
+
return json13({ error: "not found" }, 404);
|
|
3496
3557
|
}
|
|
3497
3558
|
}
|
|
3498
3559
|
if (p === "/api/config/history/checkpoint" && req.method === "POST") {
|
|
3499
3560
|
const b = await readJson(req);
|
|
3500
3561
|
const label = b?.label?.trim() || "checkpoint";
|
|
3501
|
-
return
|
|
3562
|
+
return json13({ ok: true, version: recordVersion(getConfig(), "checkpoint", Date.now(), label) });
|
|
3502
3563
|
}
|
|
3503
3564
|
if (p === "/api/config/history/restore" && req.method === "POST") {
|
|
3504
3565
|
const b = await readJson(req);
|
|
3505
3566
|
if (!b?.id)
|
|
3506
|
-
return
|
|
3567
|
+
return json13({ error: "id required" }, 400);
|
|
3507
3568
|
let target;
|
|
3508
3569
|
try {
|
|
3509
3570
|
target = readVersion(b.id);
|
|
3510
3571
|
} catch {
|
|
3511
|
-
return
|
|
3572
|
+
return json13({ error: "not found" }, 404);
|
|
3512
3573
|
}
|
|
3513
3574
|
const v = validateConfig(target.config);
|
|
3514
3575
|
if (!v.ok || !v.value)
|
|
3515
|
-
return
|
|
3576
|
+
return json13({ ok: false, errors: v.errors }, 400);
|
|
3516
3577
|
recordVersion(getConfig(), "auto", Date.now(), "before restore");
|
|
3517
3578
|
setConfig(v.value);
|
|
3518
3579
|
let persisted = true;
|
|
@@ -3523,13 +3584,13 @@ async function configRoutes(req, url, admin) {
|
|
|
3523
3584
|
}
|
|
3524
3585
|
const label = target.label ? `restored "${target.label}"` : `restored ${b.id}`;
|
|
3525
3586
|
recordVersion(getConfig(), "auto", Date.now(), label);
|
|
3526
|
-
return
|
|
3587
|
+
return json13({ ok: true, persisted, restored: b.id, config: redact(getConfig()) });
|
|
3527
3588
|
}
|
|
3528
3589
|
if (p === "/api/config/history/delete" && req.method === "POST") {
|
|
3529
3590
|
const b = await readJson(req);
|
|
3530
3591
|
if (!b?.id)
|
|
3531
|
-
return
|
|
3532
|
-
return deleteVersion(b.id) ?
|
|
3592
|
+
return json13({ error: "id required" }, 400);
|
|
3593
|
+
return deleteVersion(b.id) ? json13({ ok: true }) : json13({ error: "not found" }, 404);
|
|
3533
3594
|
}
|
|
3534
3595
|
return null;
|
|
3535
3596
|
}
|
|
@@ -3540,7 +3601,7 @@ async function modulesRoutes(req, url) {
|
|
|
3540
3601
|
if (p === "/api/modules" && req.method === "GET") {
|
|
3541
3602
|
const cfg = getConfig();
|
|
3542
3603
|
const src = getConfigSource();
|
|
3543
|
-
return
|
|
3604
|
+
return json13({
|
|
3544
3605
|
...moduleSurface(cfg.tools),
|
|
3545
3606
|
filter: cfg.tools,
|
|
3546
3607
|
source: src,
|
|
@@ -3550,11 +3611,11 @@ async function modulesRoutes(req, url) {
|
|
|
3550
3611
|
if (p === "/api/modules/toggle" && req.method === "POST") {
|
|
3551
3612
|
const b = await readJson(req);
|
|
3552
3613
|
if (typeof b?.slug !== "string" || typeof b?.enabled !== "boolean") {
|
|
3553
|
-
return
|
|
3614
|
+
return json13({ error: "slug (string) and enabled (boolean) are required" }, 400);
|
|
3554
3615
|
}
|
|
3555
3616
|
const mod = moduleCatalog.find((m) => m.slug.toLowerCase() === b.slug.toLowerCase());
|
|
3556
3617
|
if (!mod)
|
|
3557
|
-
return
|
|
3618
|
+
return json13({ error: `unknown module: ${b.slug}` }, 404);
|
|
3558
3619
|
const cfg = getConfig();
|
|
3559
3620
|
const nextTools = ToolFilterSchema.parse(applyModuleToggle(cfg.tools, mod.slug, b.enabled));
|
|
3560
3621
|
const next = { ...cfg, tools: nextTools };
|
|
@@ -3570,7 +3631,7 @@ async function modulesRoutes(req, url) {
|
|
|
3570
3631
|
if (persisted) {
|
|
3571
3632
|
recordVersion(getConfig(), "auto", Date.now(), `module ${b.enabled ? "enabled" : "disabled"}: ${mod.slug}`);
|
|
3572
3633
|
}
|
|
3573
|
-
return
|
|
3634
|
+
return json13({
|
|
3574
3635
|
ok: true,
|
|
3575
3636
|
persisted,
|
|
3576
3637
|
requiresReconnect: true,
|
|
@@ -3585,7 +3646,7 @@ async function modulesRoutes(req, url) {
|
|
|
3585
3646
|
if (p === "/api/modules/app-views" && req.method === "POST") {
|
|
3586
3647
|
const b = await readJson(req);
|
|
3587
3648
|
if (typeof b?.enabled !== "boolean") {
|
|
3588
|
-
return
|
|
3649
|
+
return json13({ error: "enabled (boolean) is required" }, 400);
|
|
3589
3650
|
}
|
|
3590
3651
|
const cfg = getConfig();
|
|
3591
3652
|
const next = { ...cfg, mcp: { ...cfg.mcp, appViews: b.enabled } };
|
|
@@ -3601,7 +3662,7 @@ async function modulesRoutes(req, url) {
|
|
|
3601
3662
|
if (persisted) {
|
|
3602
3663
|
recordVersion(getConfig(), "auto", Date.now(), `app views ${b.enabled ? "enabled" : "disabled"}`);
|
|
3603
3664
|
}
|
|
3604
|
-
return
|
|
3665
|
+
return json13({
|
|
3605
3666
|
ok: true,
|
|
3606
3667
|
persisted,
|
|
3607
3668
|
requiresReconnect: true,
|
|
@@ -3615,11 +3676,11 @@ async function modulesRoutes(req, url) {
|
|
|
3615
3676
|
async function captureRoutes(req, url) {
|
|
3616
3677
|
const p = url.pathname;
|
|
3617
3678
|
if (p === "/api/capture/status" && req.method === "GET") {
|
|
3618
|
-
return
|
|
3679
|
+
return json13(capture.stats());
|
|
3619
3680
|
}
|
|
3620
3681
|
if (p === "/api/capture/packets" && req.method === "GET") {
|
|
3621
3682
|
const limit = Number(url.searchParams.get("limit") ?? 200) || 200;
|
|
3622
|
-
return
|
|
3683
|
+
return json13({ packets: capture.recent(limit), stats: capture.stats() });
|
|
3623
3684
|
}
|
|
3624
3685
|
if (p === "/api/capture/pcap" && req.method === "GET") {
|
|
3625
3686
|
return new Response(capture.pcap(), {
|
|
@@ -3631,11 +3692,11 @@ async function captureRoutes(req, url) {
|
|
|
3631
3692
|
}
|
|
3632
3693
|
if (p === "/api/capture/start" && req.method === "POST") {
|
|
3633
3694
|
const body = await readJson(req);
|
|
3634
|
-
return
|
|
3695
|
+
return json13(await capture.start(typeof body?.port === "number" ? body.port : DEFAULT_TZSP_PORT));
|
|
3635
3696
|
}
|
|
3636
3697
|
if (p === "/api/capture/stop" && req.method === "POST") {
|
|
3637
3698
|
capture.stop();
|
|
3638
|
-
return
|
|
3699
|
+
return json13({ ok: true, ...capture.stats() });
|
|
3639
3700
|
}
|
|
3640
3701
|
return null;
|
|
3641
3702
|
}
|
|
@@ -3647,24 +3708,24 @@ async function capsmanRoutes(req, url) {
|
|
|
3647
3708
|
if (req.method === "GET") {
|
|
3648
3709
|
const ctx = createContext(undefined, url.searchParams.get("device") ?? undefined);
|
|
3649
3710
|
if (p === "/api/capsman/overview") {
|
|
3650
|
-
return
|
|
3711
|
+
return json13(capsmanOverview(await fetchCapsmanState(ctx)));
|
|
3651
3712
|
}
|
|
3652
3713
|
if (p === "/api/capsman/clients") {
|
|
3653
3714
|
const state = await fetchCapsmanState(ctx);
|
|
3654
3715
|
const weakDbm = Number.parseInt(url.searchParams.get("weak_dbm") ?? "", 10);
|
|
3655
3716
|
const weak = reportWeakClients(state, Number.isFinite(weakDbm) ? weakDbm : undefined);
|
|
3656
|
-
return
|
|
3717
|
+
return json13({ clients: state.clients, weak });
|
|
3657
3718
|
}
|
|
3658
3719
|
if (p === "/api/capsman/audit") {
|
|
3659
|
-
return
|
|
3720
|
+
return json13(runCapsmanAudit(await fetchCapsmanState(ctx)));
|
|
3660
3721
|
}
|
|
3661
3722
|
if (p === "/api/capsman/trends") {
|
|
3662
3723
|
if (!capsmanStore)
|
|
3663
|
-
return
|
|
3724
|
+
return json13({ error: "capsman store not active" }, 503);
|
|
3664
3725
|
const device = resolveDeviceName(url.searchParams.get("device") ?? undefined);
|
|
3665
3726
|
const days = daysParam(url, 7, 30);
|
|
3666
3727
|
const series = capsmanStore.radioSeries(device, Date.now() - days * 86400000);
|
|
3667
|
-
return
|
|
3728
|
+
return json13({ series, days });
|
|
3668
3729
|
}
|
|
3669
3730
|
return null;
|
|
3670
3731
|
}
|
|
@@ -3675,45 +3736,45 @@ async function capsmanRoutes(req, url) {
|
|
|
3675
3736
|
if (p === "/api/capsman/apply/steer") {
|
|
3676
3737
|
const client = state.clients.find((c) => c.mac.toLowerCase() === (body.mac ?? "").toLowerCase());
|
|
3677
3738
|
if (!client)
|
|
3678
|
-
return
|
|
3739
|
+
return json13({ ok: false, error: "client not associated" }, 400);
|
|
3679
3740
|
if (steerAlreadyPresent(state, body.mac ?? "")) {
|
|
3680
|
-
return
|
|
3741
|
+
return json13({ ok: true, message: "already steered (no-op)" });
|
|
3681
3742
|
}
|
|
3682
3743
|
const mode = body.mode === "soft" ? "soft" : "hard";
|
|
3683
3744
|
const commands = buildSteerCommands(state, body.mac ?? "", client.radioId, mode);
|
|
3684
3745
|
if (!body.confirm)
|
|
3685
|
-
return
|
|
3686
|
-
return
|
|
3746
|
+
return json13({ ok: true, preview: commands });
|
|
3747
|
+
return json13(await applyCapsmanWrites(ctx, commands, `pre-steer-${body.mac}`));
|
|
3687
3748
|
}
|
|
3688
3749
|
if (p === "/api/capsman/apply/load-balance") {
|
|
3689
3750
|
const plan = loadBalancePlan(state);
|
|
3690
3751
|
const commands = buildLoadBalanceCommands(state, plan);
|
|
3691
3752
|
if (!body.confirm)
|
|
3692
|
-
return
|
|
3693
|
-
return
|
|
3753
|
+
return json13({ ok: true, preview: commands, plan });
|
|
3754
|
+
return json13(await applyCapsmanWrites(ctx, commands, "pre-load-balance"));
|
|
3694
3755
|
}
|
|
3695
3756
|
if (p === "/api/capsman/apply/channel-plan") {
|
|
3696
3757
|
if (state.path === "/caps-man") {
|
|
3697
|
-
return
|
|
3758
|
+
return json13({ ok: false, error: "channel-plan apply is v7 /interface wifi only" }, 400);
|
|
3698
3759
|
}
|
|
3699
3760
|
const commands = buildChannelPlanCommands(state);
|
|
3700
3761
|
if (!body.confirm)
|
|
3701
|
-
return
|
|
3702
|
-
return
|
|
3762
|
+
return json13({ ok: true, preview: commands });
|
|
3763
|
+
return json13(await applyCapsmanWrites(ctx, commands, "pre-channel-plan"));
|
|
3703
3764
|
}
|
|
3704
3765
|
if (p === "/api/capsman/apply/ft") {
|
|
3705
3766
|
const commands = buildFtCommands(state);
|
|
3706
3767
|
if (!body.confirm)
|
|
3707
|
-
return
|
|
3708
|
-
return
|
|
3768
|
+
return json13({ ok: true, preview: commands });
|
|
3769
|
+
return json13(await applyCapsmanWrites(ctx, commands, "pre-ft"));
|
|
3709
3770
|
}
|
|
3710
3771
|
if (p === "/api/capsman/apply/ha") {
|
|
3711
3772
|
const commands = buildHaCommands(state);
|
|
3712
3773
|
const guidance = haGuidance(state);
|
|
3713
3774
|
if (!body.confirm)
|
|
3714
|
-
return
|
|
3775
|
+
return json13({ ok: true, preview: commands, guidance });
|
|
3715
3776
|
const res = await applyCapsmanWrites(ctx, commands, "pre-ha");
|
|
3716
|
-
return
|
|
3777
|
+
return json13({ ...res, guidance });
|
|
3717
3778
|
}
|
|
3718
3779
|
}
|
|
3719
3780
|
return null;
|
|
@@ -3739,32 +3800,32 @@ async function clientsRoutes(req, url) {
|
|
|
3739
3800
|
const deviceFromQuery = () => url.searchParams.get("device") ?? undefined;
|
|
3740
3801
|
if (p === "/api/clients" && req.method === "GET") {
|
|
3741
3802
|
const ctx = createContext(undefined, deviceFromQuery());
|
|
3742
|
-
return
|
|
3803
|
+
return json13(devicesView(await fetchDevices(ctx)));
|
|
3743
3804
|
}
|
|
3744
3805
|
if (p === "/api/clients/traffic" && req.method === "GET") {
|
|
3745
3806
|
const ip = url.searchParams.get("ip");
|
|
3746
3807
|
if (!ip)
|
|
3747
|
-
return
|
|
3808
|
+
return json13({ error: "ip required" }, 400);
|
|
3748
3809
|
const ctx = createContext(undefined, deviceFromQuery());
|
|
3749
|
-
return
|
|
3810
|
+
return json13(await sampleDeviceTraffic(ctx, ip));
|
|
3750
3811
|
}
|
|
3751
3812
|
if (p === "/api/clients/traffic-bulk" && req.method === "GET") {
|
|
3752
3813
|
const ctx = createContext(undefined, deviceFromQuery());
|
|
3753
|
-
return
|
|
3814
|
+
return json13(await sampleAllTraffic(ctx));
|
|
3754
3815
|
}
|
|
3755
3816
|
if (req.method === "POST") {
|
|
3756
3817
|
const b = await readJson(req);
|
|
3757
3818
|
const ctx = createContext(undefined, b?.device);
|
|
3758
3819
|
if (p === "/api/clients/limits") {
|
|
3759
3820
|
if (!b?.ip)
|
|
3760
|
-
return
|
|
3761
|
-
return
|
|
3821
|
+
return json13({ error: "ip required" }, 400);
|
|
3822
|
+
return json13(await setDeviceLimits(ctx, b.ip, { download: b.download, upload: b.upload }));
|
|
3762
3823
|
}
|
|
3763
3824
|
if (!b?.mac)
|
|
3764
|
-
return
|
|
3825
|
+
return json13({ error: "mac required" }, 400);
|
|
3765
3826
|
const run = async (op) => {
|
|
3766
3827
|
const r = await op;
|
|
3767
|
-
return
|
|
3828
|
+
return json13({ ...r, view: r.ok ? devicesView(await fetchDevices(ctx)) : undefined });
|
|
3768
3829
|
};
|
|
3769
3830
|
if (p === "/api/clients/block")
|
|
3770
3831
|
return run(blockDevice(ctx, b.mac, b.comment));
|
|
@@ -3776,12 +3837,12 @@ async function clientsRoutes(req, url) {
|
|
|
3776
3837
|
return run(removeDeviceLease(ctx, b.mac));
|
|
3777
3838
|
if (p === "/api/clients/set-ip") {
|
|
3778
3839
|
if (!b.ip)
|
|
3779
|
-
return
|
|
3840
|
+
return json13({ error: "ip required" }, 400);
|
|
3780
3841
|
return run(setDeviceIp(ctx, b.mac, b.ip));
|
|
3781
3842
|
}
|
|
3782
3843
|
if (p === "/api/clients/label") {
|
|
3783
3844
|
if (typeof b.label !== "string")
|
|
3784
|
-
return
|
|
3845
|
+
return json13({ error: "label required" }, 400);
|
|
3785
3846
|
return run(setDeviceLabel(ctx, b.mac, b.label));
|
|
3786
3847
|
}
|
|
3787
3848
|
}
|
|
@@ -3794,20 +3855,20 @@ async function aaaRoutes(req, url) {
|
|
|
3794
3855
|
const deviceFromQuery = () => url.searchParams.get("device") ?? undefined;
|
|
3795
3856
|
if (req.method === "GET") {
|
|
3796
3857
|
if (p === "/api/aaa/entities") {
|
|
3797
|
-
return
|
|
3858
|
+
return json13({ entities: AAA_ENTITIES });
|
|
3798
3859
|
}
|
|
3799
3860
|
if (p === "/api/aaa/radius-incoming") {
|
|
3800
|
-
return
|
|
3861
|
+
return json13(await getRadiusIncoming(createContext(undefined, deviceFromQuery())));
|
|
3801
3862
|
}
|
|
3802
3863
|
if (p === "/api/aaa/um-settings") {
|
|
3803
|
-
return
|
|
3864
|
+
return json13(await getUmSettings(createContext(undefined, deviceFromQuery())));
|
|
3804
3865
|
}
|
|
3805
3866
|
const listMatch = p.match(/^\/api\/aaa\/list\/([\w-]+)$/);
|
|
3806
3867
|
if (listMatch) {
|
|
3807
3868
|
const slug = listMatch[1];
|
|
3808
3869
|
if (!AAA_ENTITIES[slug])
|
|
3809
|
-
return
|
|
3810
|
-
return
|
|
3870
|
+
return json13({ error: "unknown entity" }, 404);
|
|
3871
|
+
return json13(await listAaaEntity(createContext(undefined, deviceFromQuery()), slug));
|
|
3811
3872
|
}
|
|
3812
3873
|
return null;
|
|
3813
3874
|
}
|
|
@@ -3815,30 +3876,30 @@ async function aaaRoutes(req, url) {
|
|
|
3815
3876
|
const body = await readJson(req);
|
|
3816
3877
|
const ctx = createContext(undefined, body?.device);
|
|
3817
3878
|
if (p === "/api/aaa/radius-incoming")
|
|
3818
|
-
return
|
|
3879
|
+
return json13(await setRadiusIncoming(ctx, body?.fields ?? {}));
|
|
3819
3880
|
if (p === "/api/aaa/um-settings")
|
|
3820
|
-
return
|
|
3881
|
+
return json13(await setUmSettings(ctx, body?.fields ?? {}));
|
|
3821
3882
|
if (p === "/api/aaa/radius-reset-counters")
|
|
3822
|
-
return
|
|
3883
|
+
return json13(await resetRadiusCounters(ctx));
|
|
3823
3884
|
const slug = body?.slug ?? "";
|
|
3824
3885
|
if (!AAA_ENTITIES[slug])
|
|
3825
|
-
return
|
|
3886
|
+
return json13({ error: "unknown entity" }, 404);
|
|
3826
3887
|
if (p === "/api/aaa/add")
|
|
3827
|
-
return
|
|
3888
|
+
return json13(await addAaaEntity(ctx, slug, body?.fields ?? {}));
|
|
3828
3889
|
if (p === "/api/aaa/update") {
|
|
3829
3890
|
if (!body?.id)
|
|
3830
|
-
return
|
|
3831
|
-
return
|
|
3891
|
+
return json13({ error: "id required" }, 400);
|
|
3892
|
+
return json13(await updateAaaEntity(ctx, slug, body.id, body?.fields ?? {}));
|
|
3832
3893
|
}
|
|
3833
3894
|
if (p === "/api/aaa/remove") {
|
|
3834
3895
|
if (!body?.id)
|
|
3835
|
-
return
|
|
3836
|
-
return
|
|
3896
|
+
return json13({ error: "id required" }, 400);
|
|
3897
|
+
return json13(await removeAaaEntity(ctx, slug, body.id));
|
|
3837
3898
|
}
|
|
3838
3899
|
if (p === "/api/aaa/toggle") {
|
|
3839
3900
|
if (!body?.id)
|
|
3840
|
-
return
|
|
3841
|
-
return
|
|
3901
|
+
return json13({ error: "id required" }, 400);
|
|
3902
|
+
return json13(await toggleAaaEntity(ctx, slug, body.id, body?.enable === true));
|
|
3842
3903
|
}
|
|
3843
3904
|
}
|
|
3844
3905
|
return null;
|
|
@@ -3854,43 +3915,43 @@ async function usageRoutes(req, url) {
|
|
|
3854
3915
|
return null;
|
|
3855
3916
|
if (p === "/api/usage/sampler") {
|
|
3856
3917
|
if (req.method === "GET")
|
|
3857
|
-
return
|
|
3918
|
+
return json13({ intervalMs: getUsageSamplerInterval() });
|
|
3858
3919
|
if (req.method === "POST") {
|
|
3859
3920
|
const b = await readJson(req);
|
|
3860
3921
|
const applied = setUsageSamplerInterval(Number(b?.intervalMs));
|
|
3861
|
-
return
|
|
3922
|
+
return json13({ intervalMs: applied });
|
|
3862
3923
|
}
|
|
3863
3924
|
return null;
|
|
3864
3925
|
}
|
|
3865
3926
|
if (req.method !== "GET")
|
|
3866
3927
|
return null;
|
|
3867
3928
|
if (!usageStore)
|
|
3868
|
-
return
|
|
3929
|
+
return json13({ error: "usage store not active" }, 503);
|
|
3869
3930
|
const device = resolveDeviceName(url.searchParams.get("device") ?? undefined);
|
|
3870
3931
|
const sinceTs = (days) => Date.now() - days * 86400000;
|
|
3871
3932
|
if (p === "/api/usage/um-users") {
|
|
3872
|
-
return
|
|
3933
|
+
return json13({ users: usageStore.umUsers(device) });
|
|
3873
3934
|
}
|
|
3874
3935
|
if (p === "/api/usage/client") {
|
|
3875
3936
|
const ip = url.searchParams.get("ip");
|
|
3876
3937
|
if (!ip)
|
|
3877
|
-
return
|
|
3938
|
+
return json13({ error: "ip required" }, 400);
|
|
3878
3939
|
const series = usageStore.clientDailyUsage(device, ip, sinceTs(daysParam(url, 90, 400)));
|
|
3879
|
-
return
|
|
3940
|
+
return json13(withTotals(series));
|
|
3880
3941
|
}
|
|
3881
3942
|
if (p === "/api/usage/um-user") {
|
|
3882
3943
|
const user = url.searchParams.get("user");
|
|
3883
3944
|
if (!user)
|
|
3884
|
-
return
|
|
3945
|
+
return json13({ error: "user required" }, 400);
|
|
3885
3946
|
const series = usageStore.umUserDailyUsage(device, user, sinceTs(daysParam(url, 90, 400)));
|
|
3886
|
-
return
|
|
3947
|
+
return json13(withTotals(series));
|
|
3887
3948
|
}
|
|
3888
3949
|
if (p === "/api/usage/heatmap") {
|
|
3889
3950
|
const user = url.searchParams.get("user");
|
|
3890
3951
|
const days = usageStore.heatmap(device, user || null, sinceTs(daysParam(url, 371, 400)));
|
|
3891
3952
|
const total = days.reduce((s, d) => s + d.count, 0);
|
|
3892
3953
|
const max = days.reduce((m, d) => Math.max(m, d.count), 0);
|
|
3893
|
-
return
|
|
3954
|
+
return json13({ days, total, max });
|
|
3894
3955
|
}
|
|
3895
3956
|
return null;
|
|
3896
3957
|
}
|
|
@@ -3914,13 +3975,13 @@ async function featureRoutes(req, url) {
|
|
|
3914
3975
|
const cfg = getConfig();
|
|
3915
3976
|
const all = Object.keys(cfg.devices).flatMap((d) => store3.list(d, 200, false));
|
|
3916
3977
|
all.sort((a, b) => b.ts - a.ts);
|
|
3917
|
-
return
|
|
3978
|
+
return json13({ snapshots: all });
|
|
3918
3979
|
}
|
|
3919
3980
|
const snapMatch = p.match(/^\/api\/snapshot\/(.+)$/);
|
|
3920
3981
|
if (snapMatch && req.method === "GET") {
|
|
3921
3982
|
const store3 = await snapStore();
|
|
3922
3983
|
const s = store3.get(decodeURIComponent(snapMatch[1]));
|
|
3923
|
-
return s ?
|
|
3984
|
+
return s ? json13(s) : json13({ error: "not found" }, 404);
|
|
3924
3985
|
}
|
|
3925
3986
|
if (p === "/api/snapshots/diff" && req.method === "POST") {
|
|
3926
3987
|
const store3 = await snapStore();
|
|
@@ -3928,13 +3989,13 @@ async function featureRoutes(req, url) {
|
|
|
3928
3989
|
const from = b?.from ? store3.get(b.from) : null;
|
|
3929
3990
|
const to = b?.to ? store3.get(b.to) : null;
|
|
3930
3991
|
if (!from || !to) {
|
|
3931
|
-
return
|
|
3992
|
+
return json13({ error: "both 'from' and 'to' snapshot ids are required" }, 400);
|
|
3932
3993
|
}
|
|
3933
3994
|
const diff = diffLines(normalizeExport(from.body), normalizeExport(to.body), {
|
|
3934
3995
|
fromLabel: from.label ?? from.id,
|
|
3935
3996
|
toLabel: to.label ?? to.id
|
|
3936
3997
|
});
|
|
3937
|
-
return
|
|
3998
|
+
return json13({
|
|
3938
3999
|
from: { id: from.id, label: from.label, ts: from.ts, device: from.device },
|
|
3939
4000
|
to: { id: to.id, label: to.label, ts: to.ts, device: to.device },
|
|
3940
4001
|
summary: diff.summary,
|
|
@@ -3948,16 +4009,16 @@ async function featureRoutes(req, url) {
|
|
|
3948
4009
|
...b?.script ? splitCommands(b.script) : []
|
|
3949
4010
|
];
|
|
3950
4011
|
if (commands.length === 0)
|
|
3951
|
-
return
|
|
4012
|
+
return json13({ error: "no commands provided" }, 400);
|
|
3952
4013
|
const plan = buildChangePlan(commands);
|
|
3953
|
-
return
|
|
4014
|
+
return json13({ plan, text: renderPlan(plan) });
|
|
3954
4015
|
}
|
|
3955
4016
|
if (p === "/api/s3" && req.method === "GET") {
|
|
3956
|
-
return
|
|
4017
|
+
return json13({ configured: isS3Configured(), target: isS3Configured() ? s3Target() : null });
|
|
3957
4018
|
}
|
|
3958
4019
|
if (p === "/api/s3/list" && req.method === "GET") {
|
|
3959
4020
|
if (!isS3Configured())
|
|
3960
|
-
return
|
|
4021
|
+
return json13({ configured: false, objects: [] });
|
|
3961
4022
|
const prefix = url.searchParams.get("prefix") ?? "";
|
|
3962
4023
|
try {
|
|
3963
4024
|
const res = await getS3Client().list({ prefix: prefix || undefined, maxKeys: 1000 });
|
|
@@ -3966,42 +4027,42 @@ async function featureRoutes(req, url) {
|
|
|
3966
4027
|
size: o.size ?? 0,
|
|
3967
4028
|
lastModified: o.lastModified ?? null
|
|
3968
4029
|
}));
|
|
3969
|
-
return
|
|
4030
|
+
return json13({ configured: true, target: s3Target(), objects, truncated: !!res.isTruncated });
|
|
3970
4031
|
} catch (e) {
|
|
3971
|
-
return
|
|
4032
|
+
return json13({ error: clientError(e) }, 502);
|
|
3972
4033
|
}
|
|
3973
4034
|
}
|
|
3974
4035
|
if (p === "/api/s3/presign" && req.method === "GET") {
|
|
3975
4036
|
if (!isS3Configured())
|
|
3976
|
-
return
|
|
4037
|
+
return json13({ error: "S3 not configured" }, 400);
|
|
3977
4038
|
const key = url.searchParams.get("key");
|
|
3978
4039
|
if (!key)
|
|
3979
|
-
return
|
|
4040
|
+
return json13({ error: "key required" }, 400);
|
|
3980
4041
|
try {
|
|
3981
4042
|
const link = getS3Client().presign(key, { expiresIn: presignExpiresIn(), method: "GET" });
|
|
3982
|
-
return
|
|
4043
|
+
return json13({ url: link });
|
|
3983
4044
|
} catch (e) {
|
|
3984
|
-
return
|
|
4045
|
+
return json13({ error: clientError(e) }, 502);
|
|
3985
4046
|
}
|
|
3986
4047
|
}
|
|
3987
4048
|
if (p === "/api/s3/delete" && req.method === "POST") {
|
|
3988
4049
|
if (!isS3Configured())
|
|
3989
|
-
return
|
|
4050
|
+
return json13({ error: "S3 not configured" }, 400);
|
|
3990
4051
|
const b = await readJson(req);
|
|
3991
4052
|
if (!b?.key)
|
|
3992
|
-
return
|
|
4053
|
+
return json13({ error: "key required" }, 400);
|
|
3993
4054
|
try {
|
|
3994
4055
|
const client = getS3Client();
|
|
3995
4056
|
if (!await client.exists(b.key))
|
|
3996
|
-
return
|
|
4057
|
+
return json13({ error: "not found" }, 404);
|
|
3997
4058
|
await client.delete(b.key);
|
|
3998
|
-
return
|
|
4059
|
+
return json13({ ok: true, key: b.key });
|
|
3999
4060
|
} catch (e) {
|
|
4000
|
-
return
|
|
4061
|
+
return json13({ error: clientError(e) }, 502);
|
|
4001
4062
|
}
|
|
4002
4063
|
}
|
|
4003
4064
|
if (p === "/api/backups" && req.method === "GET") {
|
|
4004
|
-
return
|
|
4065
|
+
return json13({
|
|
4005
4066
|
dir: backupDir(),
|
|
4006
4067
|
devices: Object.keys(getConfig().devices),
|
|
4007
4068
|
backups: listBackups()
|
|
@@ -4011,14 +4072,14 @@ async function featureRoutes(req, url) {
|
|
|
4011
4072
|
const b = await readJson(req);
|
|
4012
4073
|
const raw = b?.dir?.trim();
|
|
4013
4074
|
if (!raw)
|
|
4014
|
-
return
|
|
4075
|
+
return json13({ error: "dir required" }, 400);
|
|
4015
4076
|
const dir = raw === "~" || raw.startsWith("~/") ? join4(homedir3(), raw.slice(1)) : raw;
|
|
4016
4077
|
const next = { ...getConfig(), backupDir: dir };
|
|
4017
4078
|
setConfig(next);
|
|
4018
4079
|
try {
|
|
4019
4080
|
atomicWrite(getConfigSource().path, serializeConfig(next));
|
|
4020
4081
|
} catch (e) {
|
|
4021
|
-
return
|
|
4082
|
+
return json13({
|
|
4022
4083
|
ok: true,
|
|
4023
4084
|
dir: backupDir(),
|
|
4024
4085
|
persisted: false,
|
|
@@ -4026,16 +4087,16 @@ async function featureRoutes(req, url) {
|
|
|
4026
4087
|
});
|
|
4027
4088
|
}
|
|
4028
4089
|
recordVersion(getConfig(), "auto", Date.now(), "backup path changed");
|
|
4029
|
-
return
|
|
4090
|
+
return json13({ ok: true, dir: backupDir(), persisted: true });
|
|
4030
4091
|
}
|
|
4031
4092
|
if (p === "/api/backups/get" && req.method === "GET") {
|
|
4032
4093
|
const name = url.searchParams.get("name");
|
|
4033
4094
|
if (!name)
|
|
4034
|
-
return
|
|
4095
|
+
return json13({ error: "name required" }, 400);
|
|
4035
4096
|
try {
|
|
4036
|
-
return
|
|
4097
|
+
return json13({ name, content: readBackup(name) });
|
|
4037
4098
|
} catch {
|
|
4038
|
-
return
|
|
4099
|
+
return json13({ error: "not found" }, 404);
|
|
4039
4100
|
}
|
|
4040
4101
|
}
|
|
4041
4102
|
if (p === "/api/backups/raw" && req.method === "GET") {
|
|
@@ -4056,41 +4117,41 @@ async function featureRoutes(req, url) {
|
|
|
4056
4117
|
if (p === "/api/backups/upload" && req.method === "POST") {
|
|
4057
4118
|
const b = await readJson(req);
|
|
4058
4119
|
if (!b?.name || typeof b.content !== "string") {
|
|
4059
|
-
return
|
|
4120
|
+
return json13({ error: "name and content are required" }, 400);
|
|
4060
4121
|
}
|
|
4061
4122
|
try {
|
|
4062
4123
|
const safe = b.name.endsWith(".rsc") ? b.name : `${b.name}.rsc`;
|
|
4063
|
-
return
|
|
4124
|
+
return json13({ ok: true, name: writeBackup(safe, b.content) });
|
|
4064
4125
|
} catch (e) {
|
|
4065
|
-
return
|
|
4126
|
+
return json13({ error: clientError(e) }, 400);
|
|
4066
4127
|
}
|
|
4067
4128
|
}
|
|
4068
4129
|
if (p === "/api/backups/rename" && req.method === "POST") {
|
|
4069
4130
|
const b = await readJson(req);
|
|
4070
4131
|
if (!b?.name || !b?.new_name)
|
|
4071
|
-
return
|
|
4132
|
+
return json13({ error: "name and new_name are required" }, 400);
|
|
4072
4133
|
try {
|
|
4073
|
-
return
|
|
4134
|
+
return json13({ ok: true, name: renameBackup(b.name, b.new_name) });
|
|
4074
4135
|
} catch (e) {
|
|
4075
|
-
return
|
|
4136
|
+
return json13({ error: clientError(e) }, 400);
|
|
4076
4137
|
}
|
|
4077
4138
|
}
|
|
4078
4139
|
if (p === "/api/backups/delete" && req.method === "POST") {
|
|
4079
4140
|
const b = await readJson(req);
|
|
4080
4141
|
if (!b?.name)
|
|
4081
|
-
return
|
|
4142
|
+
return json13({ error: "name required" }, 400);
|
|
4082
4143
|
try {
|
|
4083
|
-
return deleteBackup(b.name) ?
|
|
4144
|
+
return deleteBackup(b.name) ? json13({ ok: true }) : json13({ error: "not found" }, 404);
|
|
4084
4145
|
} catch (e) {
|
|
4085
|
-
return
|
|
4146
|
+
return json13({ error: clientError(e) }, 400);
|
|
4086
4147
|
}
|
|
4087
4148
|
}
|
|
4088
4149
|
if (p === "/api/backups/restore" && req.method === "POST") {
|
|
4089
4150
|
const b = await readJson(req);
|
|
4090
4151
|
if (!b?.name)
|
|
4091
|
-
return
|
|
4152
|
+
return json13({ error: "name required" }, 400);
|
|
4092
4153
|
const device = resolveDeviceName(b.device);
|
|
4093
|
-
return
|
|
4154
|
+
return json13(await restoreLocalBackup(device, b.name, b.confirm === true));
|
|
4094
4155
|
}
|
|
4095
4156
|
if (p === "/api/backups/create" && req.method === "POST") {
|
|
4096
4157
|
const b = await readJson(req);
|
|
@@ -4102,7 +4163,7 @@ async function featureRoutes(req, url) {
|
|
|
4102
4163
|
compact: b?.compact === true,
|
|
4103
4164
|
terse: b?.terse === true
|
|
4104
4165
|
});
|
|
4105
|
-
return r.ok ?
|
|
4166
|
+
return r.ok ? json13(r) : json13({ error: r.error ?? "export failed" }, 502);
|
|
4106
4167
|
}
|
|
4107
4168
|
return null;
|
|
4108
4169
|
}
|
|
@@ -4201,6 +4262,9 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
4201
4262
|
const driftResp = await driftRoutes(req, url);
|
|
4202
4263
|
if (driftResp)
|
|
4203
4264
|
return driftResp;
|
|
4265
|
+
const postureResp = await postureRoutes(req, url);
|
|
4266
|
+
if (postureResp)
|
|
4267
|
+
return postureResp;
|
|
4204
4268
|
const txnResp = await txnRoutes(req, url);
|
|
4205
4269
|
if (txnResp)
|
|
4206
4270
|
return txnResp;
|
|
@@ -4233,7 +4297,7 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
4233
4297
|
return memoryResp;
|
|
4234
4298
|
const db = getEventStore();
|
|
4235
4299
|
if (!db)
|
|
4236
|
-
return
|
|
4300
|
+
return json13({ error: "recorder not active" }, 503);
|
|
4237
4301
|
if (url.pathname === "/api/events" && req.method === "DELETE") {
|
|
4238
4302
|
let body = {};
|
|
4239
4303
|
try {
|
|
@@ -4241,7 +4305,7 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
4241
4305
|
} catch {}
|
|
4242
4306
|
const ids = Array.isArray(body.ids) ? body.ids.filter((x) => typeof x === "string") : [];
|
|
4243
4307
|
const removed = body.all === true ? db.clear() : db.delete(ids);
|
|
4244
|
-
return
|
|
4308
|
+
return json13({ removed, total: db.total() });
|
|
4245
4309
|
}
|
|
4246
4310
|
if (url.pathname === "/api/ssh-pool") {
|
|
4247
4311
|
const cfg2 = getConfig();
|
|
@@ -4250,7 +4314,7 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
4250
4314
|
const totalInflight = ps.reduce((s, p) => s + p.inflight, 0);
|
|
4251
4315
|
const totalIdle = ps.filter((p) => p.idle).length;
|
|
4252
4316
|
const totalBusy = ps.filter((p) => p.inflight > 0).length;
|
|
4253
|
-
return
|
|
4317
|
+
return json13({
|
|
4254
4318
|
enabled,
|
|
4255
4319
|
config: {
|
|
4256
4320
|
keepAlive: cfg2.ssh.keepAlive,
|
|
@@ -4279,39 +4343,39 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
4279
4343
|
});
|
|
4280
4344
|
}
|
|
4281
4345
|
if (url.pathname === "/api/devices") {
|
|
4282
|
-
return
|
|
4346
|
+
return json13(devicesPayload(db));
|
|
4283
4347
|
}
|
|
4284
4348
|
if (url.pathname === "/api/capabilities") {
|
|
4285
4349
|
const devices = Object.keys(getConfig().devices).map((name) => ({
|
|
4286
4350
|
device: name,
|
|
4287
4351
|
capabilities: serializeCapabilities(peekCapabilities(name))
|
|
4288
4352
|
}));
|
|
4289
|
-
return
|
|
4353
|
+
return json13({ devices });
|
|
4290
4354
|
}
|
|
4291
4355
|
if (url.pathname === "/api/capabilities/refresh" && req.method === "POST") {
|
|
4292
4356
|
const b = await readJson(req);
|
|
4293
4357
|
if (typeof b?.device !== "string")
|
|
4294
|
-
return
|
|
4358
|
+
return json13({ error: "device (string) is required" }, 400);
|
|
4295
4359
|
if (!(b.device in getConfig().devices)) {
|
|
4296
|
-
return
|
|
4360
|
+
return json13({ error: `unknown device: ${b.device}` }, 404);
|
|
4297
4361
|
}
|
|
4298
4362
|
invalidateCapabilities(b.device);
|
|
4299
4363
|
try {
|
|
4300
4364
|
const caps = await getCapabilities(b.device);
|
|
4301
|
-
return
|
|
4365
|
+
return json13({ device: b.device, capabilities: serializeCapabilities(caps) });
|
|
4302
4366
|
} catch (e) {
|
|
4303
4367
|
logger.error(`Capability probe failed for '${b.device}': ${logError(e)}`);
|
|
4304
|
-
return
|
|
4368
|
+
return json13({ error: clientError(e) }, 502);
|
|
4305
4369
|
}
|
|
4306
4370
|
}
|
|
4307
4371
|
if (url.pathname === "/api/devices/toggle" && req.method === "POST") {
|
|
4308
4372
|
const b = await readJson(req);
|
|
4309
4373
|
if (typeof b?.device !== "string" || typeof b?.disabled !== "boolean") {
|
|
4310
|
-
return
|
|
4374
|
+
return json13({ error: "device (string) and disabled (boolean) are required" }, 400);
|
|
4311
4375
|
}
|
|
4312
4376
|
const cfg2 = getConfig();
|
|
4313
4377
|
if (!(b.device in cfg2.devices))
|
|
4314
|
-
return
|
|
4378
|
+
return json13({ error: `unknown device: ${b.device}` }, 404);
|
|
4315
4379
|
const dc = cfg2.devices[b.device];
|
|
4316
4380
|
const next = {
|
|
4317
4381
|
...cfg2,
|
|
@@ -4332,7 +4396,7 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
4332
4396
|
if (persisted) {
|
|
4333
4397
|
recordVersion(getConfig(), "auto", Date.now(), `device ${b.disabled ? "disabled" : "enabled"}: ${b.device}`);
|
|
4334
4398
|
}
|
|
4335
|
-
return
|
|
4399
|
+
return json13({
|
|
4336
4400
|
ok: true,
|
|
4337
4401
|
persisted,
|
|
4338
4402
|
requiresReconnect: true,
|
|
@@ -4345,17 +4409,17 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
4345
4409
|
const name = typeof b?.device === "string" ? b.device : "";
|
|
4346
4410
|
const cfg2 = getConfig();
|
|
4347
4411
|
if (!(name in cfg2.devices))
|
|
4348
|
-
return
|
|
4412
|
+
return json13({ error: `unknown device: ${name}` }, 404);
|
|
4349
4413
|
if (url.pathname === "/api/devices/reconnect")
|
|
4350
4414
|
closeDevice(name);
|
|
4351
4415
|
const status = await probeDevice(name, cfg2.devices[name]);
|
|
4352
|
-
return
|
|
4416
|
+
return json13({ ok: true, status, ...devicesPayload(db) });
|
|
4353
4417
|
}
|
|
4354
4418
|
if (url.pathname === "/api/topology") {
|
|
4355
|
-
return
|
|
4419
|
+
return json13(topologyPayload());
|
|
4356
4420
|
}
|
|
4357
4421
|
if (url.pathname === "/api/config") {
|
|
4358
|
-
return
|
|
4422
|
+
return json13(configPayload());
|
|
4359
4423
|
}
|
|
4360
4424
|
if (url.pathname === "/" || url.pathname === "/index.html") {
|
|
4361
4425
|
return new Response(dashboardHtml(), {
|
|
@@ -4364,9 +4428,9 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
4364
4428
|
}
|
|
4365
4429
|
if (url.pathname === "/api/releases/latest") {
|
|
4366
4430
|
try {
|
|
4367
|
-
return
|
|
4431
|
+
return json13(await fetchLatestRelease());
|
|
4368
4432
|
} catch (e) {
|
|
4369
|
-
return
|
|
4433
|
+
return json13({ error: clientError(e, "fetch failed") }, 502);
|
|
4370
4434
|
}
|
|
4371
4435
|
}
|
|
4372
4436
|
if (url.pathname === "/api/catalog" && req.method === "GET") {
|
|
@@ -4385,7 +4449,7 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
4385
4449
|
}));
|
|
4386
4450
|
const prompts = listPrompts();
|
|
4387
4451
|
const groups = [...new Set(modules.map((m) => m.group))].sort();
|
|
4388
|
-
return
|
|
4452
|
+
return json13({
|
|
4389
4453
|
modules,
|
|
4390
4454
|
prompts,
|
|
4391
4455
|
groups,
|
|
@@ -4400,25 +4464,25 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
4400
4464
|
}
|
|
4401
4465
|
if (url.pathname === "/api/releases" && req.method === "GET") {
|
|
4402
4466
|
try {
|
|
4403
|
-
return
|
|
4467
|
+
return json13(await fetchAllReleases());
|
|
4404
4468
|
} catch (e) {
|
|
4405
|
-
return
|
|
4469
|
+
return json13({ error: clientError(e, "fetch failed") }, 502);
|
|
4406
4470
|
}
|
|
4407
4471
|
}
|
|
4408
4472
|
if (url.pathname === "/api/upgrade" && req.method === "POST") {
|
|
4409
4473
|
const body = await readJson(req);
|
|
4410
4474
|
const version = String(body?.version ?? "latest");
|
|
4411
4475
|
if (version !== "latest" && !/^\d+\.\d+\.\d+$/.test(version)) {
|
|
4412
|
-
return
|
|
4476
|
+
return json13({ ok: false, error: `Invalid version "${version}" (use "latest" or x.y.z).` }, 400);
|
|
4413
4477
|
}
|
|
4414
4478
|
const spec = `@usex/mikrotik-mcp@${version}`;
|
|
4415
4479
|
logger.warn(`Dashboard requested upgrade: bun i -g ${spec}`);
|
|
4416
4480
|
const { ok, log } = await runUpgrade(spec);
|
|
4417
4481
|
if (!ok)
|
|
4418
|
-
return
|
|
4482
|
+
return json13({ ok: false, version, log }, 500);
|
|
4419
4483
|
const willRestart = body?.restart !== false;
|
|
4420
4484
|
const relaunched = willRestart ? restartProcess() : false;
|
|
4421
|
-
return
|
|
4485
|
+
return json13({
|
|
4422
4486
|
ok: true,
|
|
4423
4487
|
version,
|
|
4424
4488
|
log,
|
|
@@ -4428,7 +4492,7 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
4428
4492
|
}
|
|
4429
4493
|
if (url.pathname === "/api/meta") {
|
|
4430
4494
|
const f = facets(db);
|
|
4431
|
-
return
|
|
4495
|
+
return json13({
|
|
4432
4496
|
...f,
|
|
4433
4497
|
version: VERSION,
|
|
4434
4498
|
risks: ["READ", "WRITE", "WRITE_IDEMPOTENT", "DESTRUCTIVE", "DANGEROUS"],
|
|
@@ -4439,19 +4503,19 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
4439
4503
|
}
|
|
4440
4504
|
if (url.pathname === "/api/events") {
|
|
4441
4505
|
const filter = filterFromQuery(url);
|
|
4442
|
-
return
|
|
4506
|
+
return json13({ events: db.query(filter), total: db.total() });
|
|
4443
4507
|
}
|
|
4444
4508
|
const eventMatch = url.pathname.match(/^\/api\/event\/(.+)$/);
|
|
4445
4509
|
if (eventMatch) {
|
|
4446
4510
|
const e = db.get(decodeURIComponent(eventMatch[1]));
|
|
4447
|
-
return e ?
|
|
4511
|
+
return e ? json13(e) : json13({ error: "not found" }, 404);
|
|
4448
4512
|
}
|
|
4449
4513
|
if (url.pathname === "/api/stats") {
|
|
4450
4514
|
const now = Date.now();
|
|
4451
4515
|
const windowMs2 = Number(url.searchParams.get("window") ?? 3600000);
|
|
4452
4516
|
const buckets = Number(url.searchParams.get("buckets") ?? 60);
|
|
4453
4517
|
const events = db.query({ since: now - windowMs2, limit: 5000 });
|
|
4454
|
-
return
|
|
4518
|
+
return json13(computeStats(events, { now, windowMs: windowMs2, buckets }));
|
|
4455
4519
|
}
|
|
4456
4520
|
return new Response("Not Found", { status: 404 });
|
|
4457
4521
|
}
|
|
@@ -4470,7 +4534,7 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
4470
4534
|
return await dashboardRoute(req, url, srv, configAdmin, transportLabel);
|
|
4471
4535
|
} catch (e) {
|
|
4472
4536
|
logger.error(`Dashboard request failed (${url.pathname}): ${logError(e)}`);
|
|
4473
|
-
return
|
|
4537
|
+
return json13({ error: clientError(e) }, 502);
|
|
4474
4538
|
}
|
|
4475
4539
|
},
|
|
4476
4540
|
websocket: {
|