@usex/mikrotik-mcp 5.8.0 → 5.9.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/README.md +7 -1
- package/dist/cli.js +504 -326
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -1
- package/dist/shared/{library-srhzw8ym.js → library-dyeaqzj6.js} +38 -10
- package/dist/shared/{library-hsq1a106.js → library-rkr0yp3t.js} +1 -1
- package/dist/ui/observability.html +75 -71
- package/package.json +1 -1
- package/schemas/tool-catalog.json +1 -1
- /package/dist/shared/{cli-ejryxvb3.js → cli-2a9bs005.js} +0 -0
package/dist/cli.js
CHANGED
|
@@ -17672,6 +17672,12 @@ function toolUiMeta(ui) {
|
|
|
17672
17672
|
return meta;
|
|
17673
17673
|
}
|
|
17674
17674
|
|
|
17675
|
+
// src/core/tool-pattern.ts
|
|
17676
|
+
function globMatch(pattern, name) {
|
|
17677
|
+
const rx = new RegExp(`^${pattern.toLowerCase().replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`);
|
|
17678
|
+
return rx.test(name.toLowerCase());
|
|
17679
|
+
}
|
|
17680
|
+
|
|
17675
17681
|
// src/core/access.ts
|
|
17676
17682
|
var RISK_ORDER = [
|
|
17677
17683
|
"READ",
|
|
@@ -17687,10 +17693,6 @@ var RISK_RANK = {
|
|
|
17687
17693
|
DESTRUCTIVE: 2,
|
|
17688
17694
|
DANGEROUS: 3
|
|
17689
17695
|
};
|
|
17690
|
-
function globMatch(pattern, name) {
|
|
17691
|
-
const rx = new RegExp(`^${pattern.toLowerCase().replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`);
|
|
17692
|
-
return rx.test(name.toLowerCase());
|
|
17693
|
-
}
|
|
17694
17696
|
function matchesAny(patterns, name) {
|
|
17695
17697
|
return (patterns ?? []).some((p) => globMatch(p, name));
|
|
17696
17698
|
}
|
|
@@ -17720,7 +17722,20 @@ function evaluateAccess(policy, req) {
|
|
|
17720
17722
|
reason: `Tool '${req.tool}' is outside the active access scope${where}. ` + `Allowed: ${s.tools.join(", ")}.`
|
|
17721
17723
|
};
|
|
17722
17724
|
}
|
|
17725
|
+
if ((s.toolAllowGroups ?? []).some((group) => !matchesAny(group, req.tool))) {
|
|
17726
|
+
return {
|
|
17727
|
+
allowed: false,
|
|
17728
|
+
rule: "tool",
|
|
17729
|
+
reason: `Tool '${req.tool}' does not satisfy every configured and session tool allow-list${where}.`
|
|
17730
|
+
};
|
|
17731
|
+
}
|
|
17723
17732
|
if (req.device !== undefined) {
|
|
17733
|
+
if (s.noDevices)
|
|
17734
|
+
return {
|
|
17735
|
+
allowed: false,
|
|
17736
|
+
rule: "device",
|
|
17737
|
+
reason: `No devices remain in the intersection of the configured and session access scopes${where}.`
|
|
17738
|
+
};
|
|
17724
17739
|
if ((s.denyDevices ?? []).some((d) => d.toLowerCase() === req.device.toLowerCase())) {
|
|
17725
17740
|
return {
|
|
17726
17741
|
allowed: false,
|
|
@@ -17759,7 +17774,14 @@ function narrowScope(base, requested) {
|
|
|
17759
17774
|
return b.filter((x) => lower.has(x.toLowerCase()));
|
|
17760
17775
|
};
|
|
17761
17776
|
out.devices = intersect(base.devices, requested.devices);
|
|
17777
|
+
if (base.noDevices || requested.noDevices || base.devices?.length && requested.devices?.length && !out.devices?.length)
|
|
17778
|
+
out.noDevices = true;
|
|
17762
17779
|
out.tools = requested.tools && requested.tools.length > 0 ? requested.tools : base.tools;
|
|
17780
|
+
const groups = [...base.toolAllowGroups ?? [], ...requested.toolAllowGroups ?? []];
|
|
17781
|
+
if (base.tools?.length && requested.tools?.length)
|
|
17782
|
+
groups.push(base.tools);
|
|
17783
|
+
if (groups.length)
|
|
17784
|
+
out.toolAllowGroups = [...new Map(groups.map((g) => [JSON.stringify(g), g])).values()];
|
|
17763
17785
|
out.denyDevices = [...new Set([...base.denyDevices ?? [], ...requested.denyDevices ?? []])];
|
|
17764
17786
|
out.denyTools = [...new Set([...base.denyTools ?? [], ...requested.denyTools ?? []])];
|
|
17765
17787
|
if (out.denyDevices.length === 0)
|
|
@@ -17794,14 +17816,20 @@ function adoptConfiguredPolicy() {
|
|
|
17794
17816
|
}
|
|
17795
17817
|
onConfigChanged(adoptConfiguredPolicy);
|
|
17796
17818
|
adoptConfiguredPolicy();
|
|
17797
|
-
function
|
|
17819
|
+
function previewAccessPolicy(policy) {
|
|
17798
17820
|
if (!sessionNarrowing)
|
|
17799
|
-
return
|
|
17821
|
+
return policy;
|
|
17800
17822
|
return {
|
|
17801
17823
|
enabled: true,
|
|
17802
|
-
scope: narrowScope(
|
|
17824
|
+
scope: narrowScope(policy.scope, sessionNarrowing)
|
|
17803
17825
|
};
|
|
17804
17826
|
}
|
|
17827
|
+
function getAccessPolicy() {
|
|
17828
|
+
return previewAccessPolicy(basePolicy);
|
|
17829
|
+
}
|
|
17830
|
+
function hasSessionNarrowing() {
|
|
17831
|
+
return sessionNarrowing !== undefined;
|
|
17832
|
+
}
|
|
17805
17833
|
function narrowSession(requested) {
|
|
17806
17834
|
sessionNarrowing = sessionNarrowing ? narrowScope(sessionNarrowing, requested) : narrowScope(basePolicy.scope, requested);
|
|
17807
17835
|
return getAccessPolicy().scope;
|
|
@@ -19470,6 +19498,7 @@ function defineTool(def) {
|
|
|
19470
19498
|
description: def.description,
|
|
19471
19499
|
annotations: def.annotations,
|
|
19472
19500
|
inputSchema: def.inputSchema,
|
|
19501
|
+
noDevice: def.noDevice,
|
|
19473
19502
|
ui: def.ui,
|
|
19474
19503
|
requires: def.requires,
|
|
19475
19504
|
handler: def.handler,
|
|
@@ -30039,7 +30068,7 @@ var cache2 = null;
|
|
|
30039
30068
|
async function gateway() {
|
|
30040
30069
|
if (cache2)
|
|
30041
30070
|
return cache2;
|
|
30042
|
-
const { moduleCatalog } = await import("./shared/cli-
|
|
30071
|
+
const { moduleCatalog } = await import("./shared/cli-2a9bs005.js");
|
|
30043
30072
|
const forIndex = [];
|
|
30044
30073
|
const byName = new Map;
|
|
30045
30074
|
for (const mod of moduleCatalog) {
|
|
@@ -41454,7 +41483,7 @@ ${DATASET_NOTE}`;
|
|
|
41454
41483
|
function renderScope(scope) {
|
|
41455
41484
|
const lines = [];
|
|
41456
41485
|
lines.push(` max risk : ${scope.maxRisk ?? "(no ceiling)"}`);
|
|
41457
|
-
lines.push(` devices : ${scope.devices && scope.devices.length > 0 ? scope.devices.join(", ") : "(all configured)"}`);
|
|
41486
|
+
lines.push(` devices : ${scope.noDevices ? "(none: scopes do not overlap)" : scope.devices && scope.devices.length > 0 ? scope.devices.join(", ") : "(all configured)"}`);
|
|
41458
41487
|
if (scope.denyDevices && scope.denyDevices.length > 0) {
|
|
41459
41488
|
lines.push(` denied devs : ${scope.denyDevices.join(", ")}`);
|
|
41460
41489
|
}
|
|
@@ -41462,6 +41491,8 @@ function renderScope(scope) {
|
|
|
41462
41491
|
if (scope.denyTools && scope.denyTools.length > 0) {
|
|
41463
41492
|
lines.push(` denied tools: ${scope.denyTools.join(", ")}`);
|
|
41464
41493
|
}
|
|
41494
|
+
for (const group of scope.toolAllowGroups ?? [])
|
|
41495
|
+
lines.push(` also match : ${group.join(", ")}`);
|
|
41465
41496
|
lines.push(` expires : ${scope.expiresAt ? new Date(scope.expiresAt).toISOString() : "(never)"}`);
|
|
41466
41497
|
if (scope.label)
|
|
41467
41498
|
lines.push(` label : ${scope.label}`);
|
|
@@ -52263,7 +52294,7 @@ var riskByTool = null;
|
|
|
52263
52294
|
async function riskIndex() {
|
|
52264
52295
|
if (riskByTool)
|
|
52265
52296
|
return riskByTool;
|
|
52266
|
-
const { moduleCatalog } = await import("./shared/cli-
|
|
52297
|
+
const { moduleCatalog } = await import("./shared/cli-2a9bs005.js");
|
|
52267
52298
|
const index = new Map;
|
|
52268
52299
|
for (const mod of moduleCatalog) {
|
|
52269
52300
|
for (const tool of mod.tools)
|
|
@@ -64129,9 +64160,170 @@ async function postureRoutes(req, url) {
|
|
|
64129
64160
|
return null;
|
|
64130
64161
|
}
|
|
64131
64162
|
|
|
64163
|
+
// src/observability/access-settings.ts
|
|
64164
|
+
import { createHash as createHash4 } from "crypto";
|
|
64165
|
+
var list2 = array(string2().trim().min(1).max(200)).max(200);
|
|
64166
|
+
var accessSchema = AccessConfigSchema.extend({
|
|
64167
|
+
devices: list2,
|
|
64168
|
+
denyDevices: list2,
|
|
64169
|
+
tools: list2,
|
|
64170
|
+
denyTools: list2,
|
|
64171
|
+
label: string2().trim().max(200).optional()
|
|
64172
|
+
}).strict();
|
|
64173
|
+
var editSchema = object({
|
|
64174
|
+
revision: string2().min(1),
|
|
64175
|
+
access: accessSchema,
|
|
64176
|
+
device: string2().optional(),
|
|
64177
|
+
tool: string2().optional()
|
|
64178
|
+
}).strict();
|
|
64179
|
+
var idSchema = object({ pendingId: string2().min(1) }).strict();
|
|
64180
|
+
var json4 = (body, status = 200) => Response.json(body, { status });
|
|
64181
|
+
var policyFor = ({ enabled, ...scope }) => ({ enabled, scope });
|
|
64182
|
+
function createAccessSettingsRoutes(admin, deps = {
|
|
64183
|
+
getConfig,
|
|
64184
|
+
getConfigSource,
|
|
64185
|
+
getAccessPolicy,
|
|
64186
|
+
hasSessionNarrowing,
|
|
64187
|
+
previewAccessPolicy,
|
|
64188
|
+
recentDenials,
|
|
64189
|
+
recordVersion,
|
|
64190
|
+
now: Date.now
|
|
64191
|
+
}) {
|
|
64192
|
+
let pending = null;
|
|
64193
|
+
const tools = moduleCatalog2.flatMap((m) => m.tools.map((t) => ({ name: t.name, risk: riskOf(t.annotations), noDevice: !!t.noDevice })));
|
|
64194
|
+
const revision = () => createHash4("sha256").update(JSON.stringify([deps.getConfig(), deps.getAccessPolicy()])).digest("hex");
|
|
64195
|
+
const payload = () => {
|
|
64196
|
+
const cfg = deps.getConfig();
|
|
64197
|
+
const id = admin.pendingId();
|
|
64198
|
+
return {
|
|
64199
|
+
revision: revision(),
|
|
64200
|
+
configured: cfg.access,
|
|
64201
|
+
effective: deps.getAccessPolicy(),
|
|
64202
|
+
narrowed: deps.hasSessionNarrowing(),
|
|
64203
|
+
readOnly: cfg.readOnly,
|
|
64204
|
+
devices: Object.entries(cfg.devices).map(([name, d]) => ({ name, disabled: !!d.disabled })),
|
|
64205
|
+
tools,
|
|
64206
|
+
denials: deps.recentDenials(50),
|
|
64207
|
+
fromFile: deps.getConfigSource().fromFile,
|
|
64208
|
+
pending: id ? {
|
|
64209
|
+
id,
|
|
64210
|
+
owned: id === pending?.id,
|
|
64211
|
+
expiresAt: id === pending?.id ? pending.expiresAt : undefined
|
|
64212
|
+
} : null
|
|
64213
|
+
};
|
|
64214
|
+
};
|
|
64215
|
+
return async (req, url) => {
|
|
64216
|
+
const path = url.pathname;
|
|
64217
|
+
if (!path.startsWith("/api/access/settings"))
|
|
64218
|
+
return null;
|
|
64219
|
+
if (path === "/api/access/settings" && req.method === "GET")
|
|
64220
|
+
return json4(payload());
|
|
64221
|
+
if (!["preview", "apply", "keep", "rollback"].some((s) => path === `/api/access/settings/${s}`))
|
|
64222
|
+
return json4({ error: "Not found" }, 404);
|
|
64223
|
+
if (req.method !== "POST")
|
|
64224
|
+
return json4({ error: "Use POST" }, 405);
|
|
64225
|
+
if (req.headers.get("sec-fetch-site") === "cross-site")
|
|
64226
|
+
return json4({ error: "Cross-site access changes are not allowed" }, 403);
|
|
64227
|
+
if (req.headers.get("content-type")?.split(";")[0].trim() !== "application/json")
|
|
64228
|
+
return json4({ error: "Expected application/json" }, 415);
|
|
64229
|
+
let body;
|
|
64230
|
+
try {
|
|
64231
|
+
const text = await req.text();
|
|
64232
|
+
if (text.length > 65536)
|
|
64233
|
+
return json4({ error: "Access settings are too large" }, 413);
|
|
64234
|
+
body = JSON.parse(text);
|
|
64235
|
+
} catch {
|
|
64236
|
+
return json4({ error: "Invalid JSON" }, 400);
|
|
64237
|
+
}
|
|
64238
|
+
if (path.endsWith("/keep") || path.endsWith("/rollback")) {
|
|
64239
|
+
const parsed = idSchema.safeParse(body);
|
|
64240
|
+
if (!parsed.success)
|
|
64241
|
+
return json4({ error: "A pending change ID is required" }, 400);
|
|
64242
|
+
const id = parsed.data.pendingId;
|
|
64243
|
+
if (pending?.id !== id || admin.pendingId() !== id)
|
|
64244
|
+
return json4({ error: "This change expired or was replaced. Refresh the current policy." }, 409);
|
|
64245
|
+
const keep = path.endsWith("/keep");
|
|
64246
|
+
const ok = keep ? admin.keepConfig(id) : admin.rollback(id);
|
|
64247
|
+
if (!ok)
|
|
64248
|
+
return json4({ error: "This change is no longer pending" }, 409);
|
|
64249
|
+
pending = null;
|
|
64250
|
+
if (keep)
|
|
64251
|
+
deps.recordVersion(deps.getConfig(), "auto", deps.now(), "access scope updated");
|
|
64252
|
+
return json4({ ok: true, settings: payload() });
|
|
64253
|
+
}
|
|
64254
|
+
const parsed = editSchema.safeParse(body);
|
|
64255
|
+
if (!parsed.success)
|
|
64256
|
+
return json4({ error: parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ") }, 400);
|
|
64257
|
+
const { access, tool } = parsed.data;
|
|
64258
|
+
if (parsed.data.revision !== revision())
|
|
64259
|
+
return json4({ error: "Configuration changed since you opened this editor. Refresh before applying." }, 409);
|
|
64260
|
+
const cfg = deps.getConfig();
|
|
64261
|
+
const device = parsed.data.device ? Object.keys(cfg.devices).find((key) => key.toLowerCase() === parsed.data.device.toLowerCase()) : cfg.defaultDevice;
|
|
64262
|
+
const unknownDevice = [
|
|
64263
|
+
...access.devices,
|
|
64264
|
+
...access.denyDevices,
|
|
64265
|
+
...parsed.data.device ? [parsed.data.device] : []
|
|
64266
|
+
].find((name) => !Object.keys(cfg.devices).some((key) => key.toLowerCase() === name.toLowerCase()));
|
|
64267
|
+
if (unknownDevice)
|
|
64268
|
+
return json4({ error: `Unknown device: ${unknownDevice}` }, 400);
|
|
64269
|
+
if (tool && !tools.some((t) => t.name === tool))
|
|
64270
|
+
return json4({ error: `Unknown tool: ${tool}` }, 400);
|
|
64271
|
+
if (path.endsWith("/preview")) {
|
|
64272
|
+
const effective = deps.previewAccessPolicy(policyFor(access));
|
|
64273
|
+
const current = deps.getAccessPolicy();
|
|
64274
|
+
const now = deps.now();
|
|
64275
|
+
const decide = (policy, t) => {
|
|
64276
|
+
if (cfg.readOnly && t.risk !== "READ")
|
|
64277
|
+
return { allowed: false, reason: "Server-wide read-only mode prevents writes." };
|
|
64278
|
+
if (!t.noDevice && (!device || !cfg.devices[device]))
|
|
64279
|
+
return {
|
|
64280
|
+
allowed: false,
|
|
64281
|
+
rule: "device",
|
|
64282
|
+
reason: "No configured target router is available."
|
|
64283
|
+
};
|
|
64284
|
+
if (!t.noDevice && device && cfg.devices[device]?.disabled)
|
|
64285
|
+
return { allowed: false, rule: "device", reason: "This configured device is disabled." };
|
|
64286
|
+
return evaluateAccess(policy, {
|
|
64287
|
+
tool: t.name,
|
|
64288
|
+
risk: t.risk,
|
|
64289
|
+
device: t.noDevice ? undefined : device,
|
|
64290
|
+
now
|
|
64291
|
+
});
|
|
64292
|
+
};
|
|
64293
|
+
const result = {
|
|
64294
|
+
effective,
|
|
64295
|
+
allowed: 0,
|
|
64296
|
+
blocked: 0,
|
|
64297
|
+
newlyAllowed: 0,
|
|
64298
|
+
newlyBlocked: 0,
|
|
64299
|
+
device
|
|
64300
|
+
};
|
|
64301
|
+
for (const t of tools) {
|
|
64302
|
+
const next = decide(effective, t);
|
|
64303
|
+
const prev = decide(current, t);
|
|
64304
|
+
result[next.allowed ? "allowed" : "blocked"]++;
|
|
64305
|
+
if (next.allowed && !prev.allowed)
|
|
64306
|
+
result.newlyAllowed++;
|
|
64307
|
+
if (!next.allowed && prev.allowed)
|
|
64308
|
+
result.newlyBlocked++;
|
|
64309
|
+
if (tool === t.name)
|
|
64310
|
+
result.check = next;
|
|
64311
|
+
}
|
|
64312
|
+
return json4({ ok: true, preview: result });
|
|
64313
|
+
}
|
|
64314
|
+
if (admin.pendingId())
|
|
64315
|
+
return json4({
|
|
64316
|
+
error: "Another configuration change is awaiting confirmation. Keep or revert it first."
|
|
64317
|
+
}, 409);
|
|
64318
|
+
const res = admin.applyConfig({ ...cfg, access }, 60000);
|
|
64319
|
+
pending = { id: res.pendingId, expiresAt: deps.now() + res.rollbackMs };
|
|
64320
|
+
return json4({ ok: true, settings: payload() });
|
|
64321
|
+
};
|
|
64322
|
+
}
|
|
64323
|
+
|
|
64132
64324
|
// src/observability/txn-routes.ts
|
|
64133
64325
|
var JSON_HEADERS3 = { "content-type": "application/json; charset=utf-8" };
|
|
64134
|
-
function
|
|
64326
|
+
function json5(body, status = 200) {
|
|
64135
64327
|
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS3 });
|
|
64136
64328
|
}
|
|
64137
64329
|
function liveRecord(id) {
|
|
@@ -64158,16 +64350,16 @@ async function txnRoutes(req, url) {
|
|
|
64158
64350
|
for (const r of live)
|
|
64159
64351
|
byId.set(r.id, r);
|
|
64160
64352
|
const transactions = [...byId.values()].sort((a, b) => b.ts - a.ts).slice(0, limit);
|
|
64161
|
-
return
|
|
64353
|
+
return json5({ transactions, live: liveTxnUpdates(), error: storeError });
|
|
64162
64354
|
}
|
|
64163
64355
|
const parts = p.slice("/api/txn/".length).split("/").filter(Boolean);
|
|
64164
64356
|
if (parts.length === 0 || parts.length > 2)
|
|
64165
|
-
return
|
|
64357
|
+
return json5({ error: "not found" }, 404);
|
|
64166
64358
|
const id = decodeURIComponent(parts[0]);
|
|
64167
64359
|
if (parts[1] === "abort" && req.method === "POST") {
|
|
64168
64360
|
const entry = getTxn(id);
|
|
64169
64361
|
if (!entry)
|
|
64170
|
-
return
|
|
64362
|
+
return json5({ error: `no open transaction '${id}'` }, 404);
|
|
64171
64363
|
entry.txn = requestAbort(entry.txn, "aborted from the dashboard");
|
|
64172
64364
|
const run = await runTransaction({
|
|
64173
64365
|
txn: entry.txn,
|
|
@@ -64178,7 +64370,7 @@ async function txnRoutes(req, url) {
|
|
|
64178
64370
|
await persistTxn(entry);
|
|
64179
64371
|
if (run.state !== undefined)
|
|
64180
64372
|
dropTxn(id);
|
|
64181
|
-
return
|
|
64373
|
+
return json5({
|
|
64182
64374
|
state: run.state,
|
|
64183
64375
|
summary: run.summary,
|
|
64184
64376
|
transaction: toRecord2(run.txn, entry.ts)
|
|
@@ -64187,15 +64379,15 @@ async function txnRoutes(req, url) {
|
|
|
64187
64379
|
if (parts.length === 1 && req.method === "GET") {
|
|
64188
64380
|
const record = liveRecord(id) ?? store?.get(id) ?? null;
|
|
64189
64381
|
if (!record)
|
|
64190
|
-
return
|
|
64191
|
-
return
|
|
64382
|
+
return json5({ error: `unknown transaction '${id}'` }, 404);
|
|
64383
|
+
return json5({ transaction: record, events: store?.events(id) ?? [] });
|
|
64192
64384
|
}
|
|
64193
|
-
return
|
|
64385
|
+
return json5({ error: "method not allowed" }, 405);
|
|
64194
64386
|
}
|
|
64195
64387
|
|
|
64196
64388
|
// src/observability/flow-routes.ts
|
|
64197
64389
|
var JSON_HEADERS4 = { "content-type": "application/json; charset=utf-8" };
|
|
64198
|
-
function
|
|
64390
|
+
function json6(body, status = 200) {
|
|
64199
64391
|
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS4 });
|
|
64200
64392
|
}
|
|
64201
64393
|
var WINDOWS2 = {
|
|
@@ -64216,14 +64408,14 @@ async function flowRoutes(req, url) {
|
|
|
64216
64408
|
if (!p.startsWith("/api/flows"))
|
|
64217
64409
|
return null;
|
|
64218
64410
|
if (req.method !== "GET")
|
|
64219
|
-
return
|
|
64411
|
+
return json6({ error: "method not allowed" }, 405);
|
|
64220
64412
|
const collector = getFlowCollector();
|
|
64221
64413
|
if (p === "/api/flows/health") {
|
|
64222
64414
|
let store = null;
|
|
64223
64415
|
try {
|
|
64224
64416
|
store = (await flowStore()).stats();
|
|
64225
64417
|
} catch {}
|
|
64226
|
-
return
|
|
64418
|
+
return json6({ collector: collector.stats(), store });
|
|
64227
64419
|
}
|
|
64228
64420
|
collector.drain();
|
|
64229
64421
|
let records;
|
|
@@ -64234,12 +64426,12 @@ async function flowRoutes(req, url) {
|
|
|
64234
64426
|
try {
|
|
64235
64427
|
records = (await flowStore()).query({ from, to, address, limit: 200000 });
|
|
64236
64428
|
} catch (e) {
|
|
64237
|
-
return
|
|
64429
|
+
return json6({ error: e instanceof Error ? e.message : String(e), records: [] }, 200);
|
|
64238
64430
|
}
|
|
64239
64431
|
if (p === "/api/flows/top") {
|
|
64240
64432
|
const dimension = url.searchParams.get("dimension") ?? "source";
|
|
64241
64433
|
const limit = Number(url.searchParams.get("limit") ?? 10);
|
|
64242
|
-
return
|
|
64434
|
+
return json6({
|
|
64243
64435
|
window: { from, to },
|
|
64244
64436
|
totals: summarize2(records),
|
|
64245
64437
|
top: topTalkers(records, dimension, limit, true),
|
|
@@ -64249,7 +64441,7 @@ async function flowRoutes(req, url) {
|
|
|
64249
64441
|
}
|
|
64250
64442
|
if (p === "/api/flows/conversations") {
|
|
64251
64443
|
const limit = Number(url.searchParams.get("limit") ?? 20);
|
|
64252
|
-
return
|
|
64444
|
+
return json6({
|
|
64253
64445
|
window: { from, to },
|
|
64254
64446
|
conversations: conversations(records, limit)
|
|
64255
64447
|
});
|
|
@@ -64258,19 +64450,19 @@ async function flowRoutes(req, url) {
|
|
|
64258
64450
|
const dimension = url.searchParams.get("dimension") ?? "source";
|
|
64259
64451
|
const topN = Number(url.searchParams.get("topN") ?? 5);
|
|
64260
64452
|
const bucket = bucketFor2(span);
|
|
64261
|
-
return
|
|
64453
|
+
return json6({
|
|
64262
64454
|
window: { from, to },
|
|
64263
64455
|
bucketMs: bucket,
|
|
64264
64456
|
keys: topTalkers(records, dimension, topN).map((t) => t.key),
|
|
64265
64457
|
buckets: timeline(records, from, to, bucket, dimension, topN)
|
|
64266
64458
|
});
|
|
64267
64459
|
}
|
|
64268
|
-
return
|
|
64460
|
+
return json6({ error: "not found" }, 404);
|
|
64269
64461
|
}
|
|
64270
64462
|
|
|
64271
64463
|
// src/observability/rollout-routes.ts
|
|
64272
64464
|
var JSON_HEADERS5 = { "content-type": "application/json; charset=utf-8" };
|
|
64273
|
-
function
|
|
64465
|
+
function json7(body, status = 200) {
|
|
64274
64466
|
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS5 });
|
|
64275
64467
|
}
|
|
64276
64468
|
function liveRecord2(id) {
|
|
@@ -64300,17 +64492,17 @@ async function rolloutRoutes(req, url) {
|
|
|
64300
64492
|
for (const r of live)
|
|
64301
64493
|
byId.set(r.id, r);
|
|
64302
64494
|
const rollouts = [...byId.values()].sort((a, b) => b.ts - a.ts).slice(0, limit);
|
|
64303
|
-
return
|
|
64495
|
+
return json7({ rollouts, live: liveRolloutUpdates(), error: storeError });
|
|
64304
64496
|
}
|
|
64305
64497
|
const parts = p.slice("/api/rollout/".length).split("/").filter(Boolean);
|
|
64306
64498
|
if (parts.length === 0 || parts.length > 2)
|
|
64307
|
-
return
|
|
64499
|
+
return json7({ error: "not found" }, 404);
|
|
64308
64500
|
const id = decodeURIComponent(parts[0]);
|
|
64309
64501
|
const verb = parts[1];
|
|
64310
64502
|
if (verb && req.method === "POST") {
|
|
64311
64503
|
const entry = getRollout(id);
|
|
64312
64504
|
if (!entry)
|
|
64313
|
-
return
|
|
64505
|
+
return json7({ error: `no active rollout '${id}'` }, 404);
|
|
64314
64506
|
switch (verb) {
|
|
64315
64507
|
case "hold": {
|
|
64316
64508
|
entry.state = requestHold(entry.state);
|
|
@@ -64325,7 +64517,7 @@ async function rolloutRoutes(req, url) {
|
|
|
64325
64517
|
devices: entry.state.devices,
|
|
64326
64518
|
gates: entry.state.gates
|
|
64327
64519
|
});
|
|
64328
|
-
return
|
|
64520
|
+
return json7({ held: true, summary: summarize5(entry.state) });
|
|
64329
64521
|
}
|
|
64330
64522
|
case "resume": {
|
|
64331
64523
|
entry.state = resume(entry.state);
|
|
@@ -64353,7 +64545,7 @@ async function rolloutRoutes(req, url) {
|
|
|
64353
64545
|
await persistRollout(entry);
|
|
64354
64546
|
if (run.outcome)
|
|
64355
64547
|
dropRollout(id);
|
|
64356
|
-
return
|
|
64548
|
+
return json7({ outcome: run.outcome, summary: run.summary });
|
|
64357
64549
|
}
|
|
64358
64550
|
case "abort": {
|
|
64359
64551
|
entry.state = requestAbort2(entry.state, "aborted from the dashboard");
|
|
@@ -64366,19 +64558,19 @@ async function rolloutRoutes(req, url) {
|
|
|
64366
64558
|
await persistRollout(entry);
|
|
64367
64559
|
if (run.outcome)
|
|
64368
64560
|
dropRollout(id);
|
|
64369
|
-
return
|
|
64561
|
+
return json7({ outcome: run.outcome, summary: run.summary });
|
|
64370
64562
|
}
|
|
64371
64563
|
default:
|
|
64372
|
-
return
|
|
64564
|
+
return json7({ error: `unknown action '${verb}'` }, 404);
|
|
64373
64565
|
}
|
|
64374
64566
|
}
|
|
64375
64567
|
if (parts.length === 1 && req.method === "GET") {
|
|
64376
64568
|
const record = liveRecord2(id) ?? store?.get(id) ?? null;
|
|
64377
64569
|
if (!record)
|
|
64378
|
-
return
|
|
64379
|
-
return
|
|
64570
|
+
return json7({ error: `unknown rollout '${id}'` }, 404);
|
|
64571
|
+
return json7({ rollout: record, events: store?.events(id) ?? [] });
|
|
64380
64572
|
}
|
|
64381
|
-
return
|
|
64573
|
+
return json7({ error: "method not allowed" }, 405);
|
|
64382
64574
|
}
|
|
64383
64575
|
|
|
64384
64576
|
// src/policy/results.ts
|
|
@@ -64468,7 +64660,7 @@ async function policyResults(device, limit = 50) {
|
|
|
64468
64660
|
|
|
64469
64661
|
// src/observability/policy-routes.ts
|
|
64470
64662
|
var JSON_HEADERS6 = { "content-type": "application/json; charset=utf-8" };
|
|
64471
|
-
function
|
|
64663
|
+
function json8(body, status = 200) {
|
|
64472
64664
|
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS6 });
|
|
64473
64665
|
}
|
|
64474
64666
|
async function bodyJson2(req) {
|
|
@@ -64480,7 +64672,7 @@ async function policyRoutes(req, url) {
|
|
|
64480
64672
|
return null;
|
|
64481
64673
|
if (p === "/api/policies" && req.method === "GET") {
|
|
64482
64674
|
const set = currentPolicySet();
|
|
64483
|
-
return
|
|
64675
|
+
return json8({
|
|
64484
64676
|
files: set.files.map((f) => ({
|
|
64485
64677
|
path: f.path,
|
|
64486
64678
|
name: f.name,
|
|
@@ -64501,21 +64693,21 @@ async function policyRoutes(req, url) {
|
|
|
64501
64693
|
try {
|
|
64502
64694
|
results = await policyResults(device, limit);
|
|
64503
64695
|
} catch (e) {
|
|
64504
|
-
return
|
|
64696
|
+
return json8({ results: [], error: e instanceof Error ? e.message : String(e) });
|
|
64505
64697
|
}
|
|
64506
|
-
return
|
|
64698
|
+
return json8({ results });
|
|
64507
64699
|
}
|
|
64508
64700
|
if (p === "/api/policies/validate" && req.method === "POST") {
|
|
64509
64701
|
const body = await bodyJson2(req);
|
|
64510
64702
|
if (typeof body.content !== "string")
|
|
64511
|
-
return
|
|
64512
|
-
return
|
|
64703
|
+
return json8({ error: "content is required" }, 400);
|
|
64704
|
+
return json8(validatePolicyText(body.content));
|
|
64513
64705
|
}
|
|
64514
64706
|
if (p === "/api/policies/run" && req.method === "POST") {
|
|
64515
64707
|
const body = await bodyJson2(req);
|
|
64516
64708
|
const set = currentPolicySet();
|
|
64517
64709
|
if (set.policies.length === 0) {
|
|
64518
|
-
return
|
|
64710
|
+
return json8({ error: "no policy rules are loaded", reports: [] });
|
|
64519
64711
|
}
|
|
64520
64712
|
const cfg = getConfig();
|
|
64521
64713
|
const devices = body.devices && body.devices.length > 0 ? body.devices : Object.entries(cfg.devices).filter(([, d]) => !d.disabled).map(([name]) => name);
|
|
@@ -64544,14 +64736,14 @@ async function policyRoutes(req, url) {
|
|
|
64544
64736
|
reports.push({ device, error: e instanceof Error ? e.message : String(e) });
|
|
64545
64737
|
}
|
|
64546
64738
|
}
|
|
64547
|
-
return
|
|
64739
|
+
return json8({ reports });
|
|
64548
64740
|
}
|
|
64549
|
-
return
|
|
64741
|
+
return json8({ error: "not found" }, 404);
|
|
64550
64742
|
}
|
|
64551
64743
|
|
|
64552
64744
|
// src/observability/sim-routes.ts
|
|
64553
64745
|
var JSON_HEADERS7 = { "content-type": "application/json; charset=utf-8" };
|
|
64554
|
-
function
|
|
64746
|
+
function json9(body, status = 200) {
|
|
64555
64747
|
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS7 });
|
|
64556
64748
|
}
|
|
64557
64749
|
async function bodyJson3(req) {
|
|
@@ -64584,28 +64776,28 @@ async function simRoutes(req, url) {
|
|
|
64584
64776
|
if (!p.startsWith("/api/sim"))
|
|
64585
64777
|
return null;
|
|
64586
64778
|
if (p === "/api/sim/suites" && req.method === "GET") {
|
|
64587
|
-
return
|
|
64779
|
+
return json9({ suites: await listSuites() });
|
|
64588
64780
|
}
|
|
64589
64781
|
if (p === "/api/sim/suites" && req.method === "POST") {
|
|
64590
64782
|
const body = await bodyJson3(req);
|
|
64591
64783
|
if (!body.name || !Array.isArray(body.packets) || body.packets.length === 0) {
|
|
64592
|
-
return
|
|
64784
|
+
return json9({ error: "name and a non-empty packets array are required" }, 400);
|
|
64593
64785
|
}
|
|
64594
64786
|
const saved = await saveSuite({
|
|
64595
64787
|
id: body.id,
|
|
64596
64788
|
name: body.name,
|
|
64597
64789
|
packets: body.packets
|
|
64598
64790
|
});
|
|
64599
|
-
return
|
|
64791
|
+
return json9({ suite: saved });
|
|
64600
64792
|
}
|
|
64601
64793
|
if (p === "/api/sim/packet" && req.method === "POST") {
|
|
64602
64794
|
const body = await bodyJson3(req);
|
|
64603
64795
|
const config = await configFor(body);
|
|
64604
64796
|
if ("error" in config)
|
|
64605
|
-
return
|
|
64797
|
+
return json9(config, 400);
|
|
64606
64798
|
const model = buildModel(config.text);
|
|
64607
64799
|
const result = tracePacket({ model, packet: body.packet });
|
|
64608
|
-
return
|
|
64800
|
+
return json9({
|
|
64609
64801
|
source: config.source,
|
|
64610
64802
|
result,
|
|
64611
64803
|
coverage: {
|
|
@@ -64619,16 +64811,16 @@ async function simRoutes(req, url) {
|
|
|
64619
64811
|
const body = await bodyJson3(req);
|
|
64620
64812
|
const config = await configFor(body);
|
|
64621
64813
|
if ("error" in config)
|
|
64622
|
-
return
|
|
64814
|
+
return json9(config, 400);
|
|
64623
64815
|
if (!body.changes || body.changes.trim() === "") {
|
|
64624
|
-
return
|
|
64816
|
+
return json9({ error: "changes are required" }, 400);
|
|
64625
64817
|
}
|
|
64626
64818
|
const before = tracePacket({ model: buildModel(config.text), packet: body.packet });
|
|
64627
64819
|
const afterModel = buildModel(`${config.text}
|
|
64628
64820
|
${body.changes}
|
|
64629
64821
|
`);
|
|
64630
64822
|
const after = tracePacket({ model: afterModel, packet: body.packet });
|
|
64631
|
-
return
|
|
64823
|
+
return json9({
|
|
64632
64824
|
source: config.source,
|
|
64633
64825
|
before,
|
|
64634
64826
|
after,
|
|
@@ -64643,10 +64835,10 @@ ${body.changes}
|
|
|
64643
64835
|
packet: {}
|
|
64644
64836
|
});
|
|
64645
64837
|
if ("error" in config)
|
|
64646
|
-
return
|
|
64838
|
+
return json9(config, 400);
|
|
64647
64839
|
const model = buildModel(config.text);
|
|
64648
64840
|
const dead = unreachableRules(model.filter);
|
|
64649
|
-
return
|
|
64841
|
+
return json9({
|
|
64650
64842
|
source: config.source,
|
|
64651
64843
|
rules: model.filter.map((r) => ({
|
|
64652
64844
|
chain: r.chain,
|
|
@@ -64667,11 +64859,11 @@ ${body.changes}
|
|
|
64667
64859
|
const id = decodeURIComponent(p.slice("/api/sim/suite/".length, -"/run".length));
|
|
64668
64860
|
const suite = await getSuite(id);
|
|
64669
64861
|
if (!suite)
|
|
64670
|
-
return
|
|
64862
|
+
return json9({ error: `no suite '${id}'` }, 404);
|
|
64671
64863
|
const body = await bodyJson3(req);
|
|
64672
64864
|
const config = await configFor({ ...body, packet: {} });
|
|
64673
64865
|
if ("error" in config)
|
|
64674
|
-
return
|
|
64866
|
+
return json9(config, 400);
|
|
64675
64867
|
const model = buildModel(config.text);
|
|
64676
64868
|
const results = suite.packets.map((entry) => {
|
|
64677
64869
|
const result = tracePacket({ model, packet: entry.packet });
|
|
@@ -64683,7 +64875,7 @@ ${body.changes}
|
|
|
64683
64875
|
summary: result.summary
|
|
64684
64876
|
};
|
|
64685
64877
|
});
|
|
64686
|
-
return
|
|
64878
|
+
return json9({
|
|
64687
64879
|
source: config.source,
|
|
64688
64880
|
suite: { id: suite.id, name: suite.name },
|
|
64689
64881
|
results,
|
|
@@ -64691,12 +64883,12 @@ ${body.changes}
|
|
|
64691
64883
|
total: results.length
|
|
64692
64884
|
});
|
|
64693
64885
|
}
|
|
64694
|
-
return
|
|
64886
|
+
return json9({ error: "not found" }, 404);
|
|
64695
64887
|
}
|
|
64696
64888
|
|
|
64697
64889
|
// src/observability/schedule-routes.ts
|
|
64698
64890
|
var JSON_HEADERS8 = { "content-type": "application/json; charset=utf-8" };
|
|
64699
|
-
function
|
|
64891
|
+
function json10(body, status = 200) {
|
|
64700
64892
|
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS8 });
|
|
64701
64893
|
}
|
|
64702
64894
|
function latestPerDevice2(runs) {
|
|
@@ -64773,12 +64965,12 @@ async function scheduleRoutes(req, url) {
|
|
|
64773
64965
|
try {
|
|
64774
64966
|
store = await scheduleStore();
|
|
64775
64967
|
} catch (e) {
|
|
64776
|
-
return
|
|
64968
|
+
return json10({ error: e instanceof Error ? e.message : String(e), jobs: [] }, 503);
|
|
64777
64969
|
}
|
|
64778
64970
|
const now = Date.now();
|
|
64779
64971
|
if (p === "/api/schedules" && req.method === "GET") {
|
|
64780
64972
|
const jobs = store.listJobs().map((job) => jobRow(job, store.runs(job.id, 500), now));
|
|
64781
|
-
return
|
|
64973
|
+
return json10({
|
|
64782
64974
|
jobs,
|
|
64783
64975
|
schedulable: schedulableTools().map((a) => ({ tool: a.tool, summary: a.summary }))
|
|
64784
64976
|
});
|
|
@@ -64786,20 +64978,20 @@ async function scheduleRoutes(req, url) {
|
|
|
64786
64978
|
if (p === "/api/schedules" && req.method === "POST") {
|
|
64787
64979
|
const body = await req.json().catch(() => null);
|
|
64788
64980
|
if (!body)
|
|
64789
|
-
return
|
|
64981
|
+
return json10({ error: "invalid JSON body" }, 400);
|
|
64790
64982
|
const parsed = JobSchema.safeParse(body);
|
|
64791
64983
|
if (!parsed.success) {
|
|
64792
|
-
return
|
|
64984
|
+
return json10({
|
|
64793
64985
|
error: parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")
|
|
64794
64986
|
}, 400);
|
|
64795
64987
|
}
|
|
64796
64988
|
if (!auditAdapter(parsed.data.tool)) {
|
|
64797
|
-
return
|
|
64989
|
+
return json10({ error: `'${parsed.data.tool}' is not a schedulable auditor` }, 400);
|
|
64798
64990
|
}
|
|
64799
64991
|
await primeRiskIndex();
|
|
64800
64992
|
const risk = liveExecutor().riskOf(parsed.data.tool);
|
|
64801
64993
|
if (risk !== "READ") {
|
|
64802
|
-
return
|
|
64994
|
+
return json10({ error: `'${parsed.data.tool}' is annotated ${risk}; only READ may be scheduled` }, 400);
|
|
64803
64995
|
}
|
|
64804
64996
|
const existing = store.getJob(parsed.data.id);
|
|
64805
64997
|
const job = { ...parsed.data, createdAt: existing?.createdAt ?? now };
|
|
@@ -64808,7 +65000,7 @@ async function scheduleRoutes(req, url) {
|
|
|
64808
65000
|
armJob(job, now);
|
|
64809
65001
|
else
|
|
64810
65002
|
forgetJob(job.id);
|
|
64811
|
-
return
|
|
65003
|
+
return json10({ job: jobRow(job, store.runs(job.id, 500), now), replaced: existing !== null });
|
|
64812
65004
|
}
|
|
64813
65005
|
if (p === "/api/schedules/timeline" && req.method === "GET") {
|
|
64814
65006
|
const jobId = url.searchParams.get("job") ?? undefined;
|
|
@@ -64816,7 +65008,7 @@ async function scheduleRoutes(req, url) {
|
|
|
64816
65008
|
const days = Number(url.searchParams.get("days") ?? 30);
|
|
64817
65009
|
const since = now - days * 86400000;
|
|
64818
65010
|
const runs = store.runs(jobId, 2000).filter((r) => r.startedAt >= since && (!device || r.device === device));
|
|
64819
|
-
return
|
|
65011
|
+
return json10({
|
|
64820
65012
|
points: runs.map((r) => ({
|
|
64821
65013
|
at: r.startedAt,
|
|
64822
65014
|
jobId: r.jobId,
|
|
@@ -64836,30 +65028,30 @@ async function scheduleRoutes(req, url) {
|
|
|
64836
65028
|
const limit = Number(url.searchParams.get("limit") ?? 50);
|
|
64837
65029
|
const jobs = jobId ? [jobId] : store.listJobs().map((j) => j.id);
|
|
64838
65030
|
const items = jobs.flatMap((id) => regressionsFor(store.runs(id, 500), id)).sort((a, b) => b.at - a.at).slice(0, limit);
|
|
64839
|
-
return
|
|
65031
|
+
return json10({ regressions: items });
|
|
64840
65032
|
}
|
|
64841
65033
|
const parts = p.slice("/api/schedules/".length).split("/").filter(Boolean);
|
|
64842
65034
|
if (parts.length === 0 || parts.length > 2)
|
|
64843
|
-
return
|
|
65035
|
+
return json10({ error: "not found" }, 404);
|
|
64844
65036
|
const id = decodeURIComponent(parts[0]);
|
|
64845
65037
|
const verb = parts[1];
|
|
64846
65038
|
if (!verb && req.method === "DELETE") {
|
|
64847
65039
|
const job = store.getJob(id);
|
|
64848
65040
|
if (!job)
|
|
64849
|
-
return
|
|
65041
|
+
return json10({ error: `no schedule '${id}'` }, 404);
|
|
64850
65042
|
const runCount = store.runs(id, 1e4).length;
|
|
64851
65043
|
store.removeJob(id);
|
|
64852
65044
|
forgetJob(id);
|
|
64853
|
-
return
|
|
65045
|
+
return json10({ removed: id, runsDiscarded: runCount });
|
|
64854
65046
|
}
|
|
64855
65047
|
if (verb === "run" && req.method === "POST") {
|
|
64856
65048
|
const job = store.getJob(id);
|
|
64857
65049
|
if (!job)
|
|
64858
|
-
return
|
|
65050
|
+
return json10({ error: `no schedule '${id}'` }, 404);
|
|
64859
65051
|
await primeRiskIndex();
|
|
64860
65052
|
logger.info(`schedule '${id}' run requested from the dashboard`);
|
|
64861
65053
|
const result = await runJob(job, liveExecutor(), runnerOptions());
|
|
64862
|
-
return
|
|
65054
|
+
return json10({
|
|
64863
65055
|
...result,
|
|
64864
65056
|
job: jobRow(job, store.runs(id, 500), Date.now())
|
|
64865
65057
|
});
|
|
@@ -64867,16 +65059,16 @@ async function scheduleRoutes(req, url) {
|
|
|
64867
65059
|
if (!verb && req.method === "GET") {
|
|
64868
65060
|
const job = store.getJob(id);
|
|
64869
65061
|
if (!job)
|
|
64870
|
-
return
|
|
65062
|
+
return json10({ error: `no schedule '${id}'` }, 404);
|
|
64871
65063
|
const runs = store.runs(id, 500);
|
|
64872
|
-
return
|
|
65064
|
+
return json10({ job: jobRow(job, runs, now), runs, regressions: regressionsFor(runs, id) });
|
|
64873
65065
|
}
|
|
64874
|
-
return
|
|
65066
|
+
return json10({ error: "not found" }, 404);
|
|
64875
65067
|
}
|
|
64876
65068
|
|
|
64877
65069
|
// src/observability/explain-routes.ts
|
|
64878
65070
|
var JSON_HEADERS9 = { "content-type": "application/json; charset=utf-8" };
|
|
64879
|
-
function
|
|
65071
|
+
function json11(body, status = 200) {
|
|
64880
65072
|
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS9 });
|
|
64881
65073
|
}
|
|
64882
65074
|
var storePromise17 = null;
|
|
@@ -64917,52 +65109,52 @@ async function explainRoutes(req, url) {
|
|
|
64917
65109
|
if (p === "/api/explain/diff" && req.method === "POST") {
|
|
64918
65110
|
const body = await req.json().catch(() => null);
|
|
64919
65111
|
if (!body)
|
|
64920
|
-
return
|
|
65112
|
+
return json11({ error: "invalid JSON body" }, 400);
|
|
64921
65113
|
if (!body.before && !body.beforeText) {
|
|
64922
|
-
return
|
|
65114
|
+
return json11({ error: "a diff needs a baseline: pass `before` (a snapshot id) or `beforeText`" }, 400);
|
|
64923
65115
|
}
|
|
64924
65116
|
const device = body.device ?? "";
|
|
64925
65117
|
const before = body.beforeText ? { text: body.beforeText, device, source: "supplied text" } : await configFor2(device, body.before ?? null);
|
|
64926
65118
|
if ("error" in before)
|
|
64927
|
-
return
|
|
65119
|
+
return json11({ error: `before: ${before.error}` }, before.status);
|
|
64928
65120
|
const after = body.afterText ? { text: body.afterText, device, source: "supplied text" } : await configFor2(device, body.after ?? null);
|
|
64929
65121
|
if ("error" in after)
|
|
64930
|
-
return
|
|
65122
|
+
return json11({ error: `after: ${after.error}` }, after.status);
|
|
64931
65123
|
const diff = diffNarratives(analyzeDevice(before.text, before.device), analyzeDevice(after.text, after.device));
|
|
64932
|
-
return
|
|
65124
|
+
return json11({ diff, markdown: renderDiff(diff), before: before.source, after: after.source });
|
|
64933
65125
|
}
|
|
64934
65126
|
const rest = p.slice("/api/explain/".length).split("/").filter(Boolean);
|
|
64935
65127
|
if (rest.length === 0)
|
|
64936
|
-
return
|
|
65128
|
+
return json11({ error: "not found" }, 404);
|
|
64937
65129
|
if (req.method !== "GET")
|
|
64938
|
-
return
|
|
65130
|
+
return json11({ error: "not found" }, 404);
|
|
64939
65131
|
const device = decodeURIComponent(rest[0]);
|
|
64940
65132
|
const snapshotId = url.searchParams.get("snapshot");
|
|
64941
65133
|
let config;
|
|
64942
65134
|
try {
|
|
64943
65135
|
config = await configFor2(device, snapshotId);
|
|
64944
65136
|
} catch (e) {
|
|
64945
|
-
return
|
|
65137
|
+
return json11({ error: e instanceof Error ? e.message : String(e) }, 502);
|
|
64946
65138
|
}
|
|
64947
65139
|
if ("error" in config)
|
|
64948
|
-
return
|
|
65140
|
+
return json11({ error: config.error }, config.status);
|
|
64949
65141
|
const narrative = { ...analyzeDevice(config.text, config.device), generatedAt: Date.now() };
|
|
64950
65142
|
if (rest.length === 1) {
|
|
64951
|
-
return
|
|
65143
|
+
return json11(payload(narrative, config.source, renderNarrative(narrative)));
|
|
64952
65144
|
}
|
|
64953
65145
|
if (rest[1] === "section" && rest[2]) {
|
|
64954
65146
|
const section = decodeURIComponent(rest[2]);
|
|
64955
65147
|
if (!NARRATIVE_SECTIONS.includes(section)) {
|
|
64956
|
-
return
|
|
65148
|
+
return json11({ error: `unknown section '${section}'. Known: ${NARRATIVE_SECTIONS.join(", ")}` }, 400);
|
|
64957
65149
|
}
|
|
64958
|
-
return
|
|
65150
|
+
return json11(payload(narrative, config.source, renderNarrative(narrative, { sections: [section] })));
|
|
64959
65151
|
}
|
|
64960
|
-
return
|
|
65152
|
+
return json11({ error: "not found" }, 404);
|
|
64961
65153
|
}
|
|
64962
65154
|
|
|
64963
65155
|
// src/observability/attack-routes.ts
|
|
64964
65156
|
var JSON_HEADERS10 = { "content-type": "application/json; charset=utf-8" };
|
|
64965
|
-
function
|
|
65157
|
+
function json12(body, status = 200) {
|
|
64966
65158
|
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS10 });
|
|
64967
65159
|
}
|
|
64968
65160
|
async function attackRoutes(req, url) {
|
|
@@ -64973,7 +65165,7 @@ async function attackRoutes(req, url) {
|
|
|
64973
65165
|
try {
|
|
64974
65166
|
store = await attackStore();
|
|
64975
65167
|
} catch (e) {
|
|
64976
|
-
return
|
|
65168
|
+
return json12({ error: e instanceof Error ? e.message : String(e), incidents: [] }, 503);
|
|
64977
65169
|
}
|
|
64978
65170
|
const cfg = getConfig().attacks;
|
|
64979
65171
|
const now = Date.now();
|
|
@@ -64985,7 +65177,7 @@ async function attackRoutes(req, url) {
|
|
|
64985
65177
|
});
|
|
64986
65178
|
const responses = store.listResponses({ active: true, limit: 500 });
|
|
64987
65179
|
const blocked = new Set(responses.map((r) => r.source));
|
|
64988
|
-
return
|
|
65180
|
+
return json12({
|
|
64989
65181
|
incidents: incidents.map((i) => ({ ...i, blocked: blocked.has(i.source) })),
|
|
64990
65182
|
responses,
|
|
64991
65183
|
posture: {
|
|
@@ -65025,7 +65217,7 @@ async function attackRoutes(req, url) {
|
|
|
65025
65217
|
}
|
|
65026
65218
|
}
|
|
65027
65219
|
const active = new Set(store.listResponses({ active: true, limit: 500 }).map((r) => r.source));
|
|
65028
|
-
return
|
|
65220
|
+
return json12({
|
|
65029
65221
|
sources: [...bySource.values()].map((s) => ({
|
|
65030
65222
|
source: s.source,
|
|
65031
65223
|
devices: [...s.devices].sort(),
|
|
@@ -65043,14 +65235,14 @@ async function attackRoutes(req, url) {
|
|
|
65043
65235
|
const guards = buildGuards();
|
|
65044
65236
|
const never = [...neverBlockSet(guards)].sort();
|
|
65045
65237
|
const check = url.searchParams.get("check");
|
|
65046
|
-
return
|
|
65238
|
+
return json12({
|
|
65047
65239
|
config: cfg,
|
|
65048
65240
|
neverBlock: never,
|
|
65049
65241
|
check: check ? { address: check, protected: isNeverBlock(check, new Set(never)) } : undefined
|
|
65050
65242
|
});
|
|
65051
65243
|
}
|
|
65052
65244
|
if (p === "/api/attacks/config" && req.method === "POST") {
|
|
65053
|
-
return
|
|
65245
|
+
return json12({
|
|
65054
65246
|
error: "edit the `attacks` block in Config \u2014 the policy has to survive a restart, so it is not held in memory here"
|
|
65055
65247
|
}, 501);
|
|
65056
65248
|
}
|
|
@@ -65063,7 +65255,7 @@ async function attackRoutes(req, url) {
|
|
|
65063
65255
|
onlineOnly: body.onlineOnly,
|
|
65064
65256
|
respond: false
|
|
65065
65257
|
});
|
|
65066
|
-
return
|
|
65258
|
+
return json12({
|
|
65067
65259
|
incidents: result.incidents,
|
|
65068
65260
|
unavailable: result.unavailable,
|
|
65069
65261
|
devices: result.devices,
|
|
@@ -65071,7 +65263,7 @@ async function attackRoutes(req, url) {
|
|
|
65071
65263
|
});
|
|
65072
65264
|
}
|
|
65073
65265
|
if (p === "/api/attacks/devices" && req.method === "GET") {
|
|
65074
|
-
return
|
|
65266
|
+
return json12({
|
|
65075
65267
|
devices: listDevices().names.map((name) => ({
|
|
65076
65268
|
name,
|
|
65077
65269
|
reachable: getDeviceStatus(name).reachable
|
|
@@ -65084,24 +65276,24 @@ async function attackRoutes(req, url) {
|
|
|
65084
65276
|
const response = store.responseFor(address);
|
|
65085
65277
|
const devices = response?.devices ?? [];
|
|
65086
65278
|
if (devices.length === 0)
|
|
65087
|
-
return
|
|
65279
|
+
return json12({ error: `no recorded block for ${address}` }, 404);
|
|
65088
65280
|
const results = await revokeBlock(address, devices, response?.list);
|
|
65089
65281
|
store.revokeResponse(address, now);
|
|
65090
|
-
return
|
|
65282
|
+
return json12({ address, results });
|
|
65091
65283
|
}
|
|
65092
65284
|
if (rest.length === 1 && req.method === "GET") {
|
|
65093
65285
|
const incident = store.getIncident(decodeURIComponent(rest[0]));
|
|
65094
65286
|
if (!incident)
|
|
65095
|
-
return
|
|
65096
|
-
return
|
|
65287
|
+
return json12({ error: "not found" }, 404);
|
|
65288
|
+
return json12({ incident, response: store.responseFor(incident.source) });
|
|
65097
65289
|
}
|
|
65098
65290
|
if (rest.length === 2 && rest[1] === "respond" && req.method === "POST") {
|
|
65099
65291
|
const incident = store.getIncident(decodeURIComponent(rest[0]));
|
|
65100
65292
|
if (!incident)
|
|
65101
|
-
return
|
|
65293
|
+
return json12({ error: "not found" }, 404);
|
|
65102
65294
|
const body = await req.json().catch(() => ({}));
|
|
65103
65295
|
if (body.action === "dismiss") {
|
|
65104
|
-
return
|
|
65296
|
+
return json12({ dismissed: incident.id });
|
|
65105
65297
|
}
|
|
65106
65298
|
const decision = decide({
|
|
65107
65299
|
incident,
|
|
@@ -65113,13 +65305,13 @@ async function attackRoutes(req, url) {
|
|
|
65113
65305
|
timeout: body.timeout
|
|
65114
65306
|
});
|
|
65115
65307
|
if (!isPlan(decision)) {
|
|
65116
|
-
return
|
|
65308
|
+
return json12({ refused: true, guard: decision.guard, reason: decision.reason }, 200);
|
|
65117
65309
|
}
|
|
65118
65310
|
if (decision.action === "escalate") {
|
|
65119
|
-
return
|
|
65311
|
+
return json12({ escalated: true, reason: decision.reason });
|
|
65120
65312
|
}
|
|
65121
65313
|
if (!body.confirm) {
|
|
65122
|
-
return
|
|
65314
|
+
return json12({ dryRun: true, plan: decision });
|
|
65123
65315
|
}
|
|
65124
65316
|
const applied = await executePlan(decision);
|
|
65125
65317
|
const ok = applied.some((r) => r.ok);
|
|
@@ -65135,14 +65327,14 @@ async function attackRoutes(req, url) {
|
|
|
65135
65327
|
ok,
|
|
65136
65328
|
error: ok ? undefined : applied.map((r) => r.detail).join("; ")
|
|
65137
65329
|
});
|
|
65138
|
-
return
|
|
65330
|
+
return json12({ applied, ok, plan: decision });
|
|
65139
65331
|
}
|
|
65140
|
-
return
|
|
65332
|
+
return json12({ error: "not found" }, 404);
|
|
65141
65333
|
}
|
|
65142
65334
|
|
|
65143
65335
|
// src/observability/alert-routes.ts
|
|
65144
65336
|
var JSON_HEADERS11 = { "content-type": "application/json; charset=utf-8" };
|
|
65145
|
-
function
|
|
65337
|
+
function json13(body, status = 200) {
|
|
65146
65338
|
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS11 });
|
|
65147
65339
|
}
|
|
65148
65340
|
function ruleRow(rule, subject, status, since) {
|
|
@@ -65197,11 +65389,11 @@ async function alertRoutes(req, url, db) {
|
|
|
65197
65389
|
const channels = getConfig().alerts?.channels;
|
|
65198
65390
|
if (p === "/api/alerts" && req.method === "GET") {
|
|
65199
65391
|
if (!engine) {
|
|
65200
|
-
return
|
|
65392
|
+
return json13({ configured: false, rules: [], active: [], channels: {} });
|
|
65201
65393
|
}
|
|
65202
65394
|
const now = Date.now();
|
|
65203
65395
|
const rows = engine.snapshot().map(({ rule, subject, state }) => ruleRow(rule, subject, isMuted(rule, now) ? "muted" : !rule.enabled ? "disabled" : state.status, state.since));
|
|
65204
|
-
return
|
|
65396
|
+
return json13({
|
|
65205
65397
|
configured: true,
|
|
65206
65398
|
rules: rows,
|
|
65207
65399
|
active: rows.filter((r) => r.status === "firing"),
|
|
@@ -65210,41 +65402,41 @@ async function alertRoutes(req, url, db) {
|
|
|
65210
65402
|
}
|
|
65211
65403
|
if (p === "/api/alerts/history" && req.method === "GET") {
|
|
65212
65404
|
if (!engine)
|
|
65213
|
-
return
|
|
65405
|
+
return json13({ history: [] });
|
|
65214
65406
|
const hours = Number(url.searchParams.get("hours") ?? 24);
|
|
65215
65407
|
const history = await engine.history({
|
|
65216
65408
|
sinceMs: Date.now() - hours * 3600000,
|
|
65217
65409
|
ruleId: url.searchParams.get("rule") ?? undefined,
|
|
65218
65410
|
limit: Number(url.searchParams.get("limit") ?? 200)
|
|
65219
65411
|
});
|
|
65220
|
-
return
|
|
65412
|
+
return json13({ history });
|
|
65221
65413
|
}
|
|
65222
65414
|
if (p === "/api/alerts/rules" && req.method === "POST") {
|
|
65223
65415
|
if (!engine)
|
|
65224
|
-
return
|
|
65416
|
+
return json13({ error: "alerting is not configured" }, 400);
|
|
65225
65417
|
const parsed = AlertRuleSchema.safeParse(await req.json());
|
|
65226
65418
|
if (!parsed.success) {
|
|
65227
|
-
return
|
|
65419
|
+
return json13({ error: parsed.error.issues.map((i) => i.message).join("; ") }, 400);
|
|
65228
65420
|
}
|
|
65229
65421
|
const rules = engine.configuredRules();
|
|
65230
65422
|
if (rules.some((r) => r.id === parsed.data.id)) {
|
|
65231
|
-
return
|
|
65423
|
+
return json13({ error: `rule '${parsed.data.id}' already exists` }, 409);
|
|
65232
65424
|
}
|
|
65233
65425
|
engine.setRules([...rules, parsed.data]);
|
|
65234
|
-
return
|
|
65426
|
+
return json13({ ok: true, id: parsed.data.id });
|
|
65235
65427
|
}
|
|
65236
65428
|
const idMatch = p.match(/^\/api\/alerts\/rules\/([^/]+)$/);
|
|
65237
65429
|
if (idMatch) {
|
|
65238
65430
|
if (!engine)
|
|
65239
|
-
return
|
|
65431
|
+
return json13({ error: "alerting is not configured" }, 400);
|
|
65240
65432
|
const id = decodeURIComponent(idMatch[1]);
|
|
65241
65433
|
const rules = engine.configuredRules();
|
|
65242
65434
|
const existing = rules.find((r) => r.id === id);
|
|
65243
65435
|
if (!existing)
|
|
65244
|
-
return
|
|
65436
|
+
return json13({ error: `unknown rule: ${id}` }, 404);
|
|
65245
65437
|
if (req.method === "DELETE") {
|
|
65246
65438
|
engine.setRules(rules.filter((r) => r.id !== id));
|
|
65247
|
-
return
|
|
65439
|
+
return json13({ ok: true });
|
|
65248
65440
|
}
|
|
65249
65441
|
if (req.method === "PATCH") {
|
|
65250
65442
|
const body = await req.json();
|
|
@@ -65252,22 +65444,22 @@ async function alertRoutes(req, url, db) {
|
|
|
65252
65444
|
if (typeof body.mute === "string") {
|
|
65253
65445
|
const ms = parseDuration(body.mute);
|
|
65254
65446
|
if (ms === null)
|
|
65255
|
-
return
|
|
65447
|
+
return json13({ error: `invalid duration: ${body.mute}` }, 400);
|
|
65256
65448
|
mutedUntil = ms === 0 ? undefined : Date.now() + ms;
|
|
65257
65449
|
delete body.mute;
|
|
65258
65450
|
}
|
|
65259
65451
|
const merged = AlertRuleSchema.safeParse({ ...existing, ...body, mutedUntil });
|
|
65260
65452
|
if (!merged.success) {
|
|
65261
|
-
return
|
|
65453
|
+
return json13({ error: merged.error.issues.map((i) => i.message).join("; ") }, 400);
|
|
65262
65454
|
}
|
|
65263
65455
|
engine.setRules(rules.map((r) => r.id === id ? merged.data : r));
|
|
65264
|
-
return
|
|
65456
|
+
return json13({ ok: true, rule: merged.data });
|
|
65265
65457
|
}
|
|
65266
65458
|
}
|
|
65267
65459
|
if (p === "/api/alerts/test" && req.method === "POST") {
|
|
65268
65460
|
const body = await req.json();
|
|
65269
65461
|
if (!channels || !body?.channel)
|
|
65270
|
-
return
|
|
65462
|
+
return json13({ error: "channel is required" }, 400);
|
|
65271
65463
|
try {
|
|
65272
65464
|
const result = await deliver(body.channel, channels, {
|
|
65273
65465
|
ruleId: "test",
|
|
@@ -65277,20 +65469,20 @@ async function alertRoutes(req, url, db) {
|
|
|
65277
65469
|
body: "If you can read this, this channel is configured correctly.",
|
|
65278
65470
|
at: Date.now()
|
|
65279
65471
|
});
|
|
65280
|
-
return
|
|
65472
|
+
return json13(result);
|
|
65281
65473
|
} catch (e) {
|
|
65282
65474
|
logger.error(`Alert channel test failed: ${logError(e)}`);
|
|
65283
|
-
return
|
|
65475
|
+
return json13({ error: clientError(e) }, 502);
|
|
65284
65476
|
}
|
|
65285
65477
|
}
|
|
65286
65478
|
if (p === "/api/alerts/preview" && req.method === "POST") {
|
|
65287
65479
|
const parsed = AlertRuleSchema.safeParse(await req.json());
|
|
65288
65480
|
if (!parsed.success) {
|
|
65289
|
-
return
|
|
65481
|
+
return json13({ error: parsed.error.issues.map((i) => i.message).join("; ") }, 400);
|
|
65290
65482
|
}
|
|
65291
65483
|
const hours = Number(url.searchParams.get("hours") ?? 24);
|
|
65292
65484
|
if (!db) {
|
|
65293
|
-
return
|
|
65485
|
+
return json13({ hours, sampled: 0, fires: 0, resolves: 0, matched: 0, noHistory: true });
|
|
65294
65486
|
}
|
|
65295
65487
|
const events = db.query({ since: Date.now() - hours * 3600000, limit: 5000 }).map((e) => ({
|
|
65296
65488
|
ts: e.ts,
|
|
@@ -65299,14 +65491,14 @@ async function alertRoutes(req, url, db) {
|
|
|
65299
65491
|
risk: e.risk,
|
|
65300
65492
|
isError: e.isError
|
|
65301
65493
|
}));
|
|
65302
|
-
return
|
|
65494
|
+
return json13({ hours, sampled: events.length, ...previewRule(parsed.data, events) });
|
|
65303
65495
|
}
|
|
65304
65496
|
return null;
|
|
65305
65497
|
}
|
|
65306
65498
|
|
|
65307
65499
|
// src/observability/memory-routes.ts
|
|
65308
65500
|
var JSON_HEADERS12 = { "content-type": "application/json; charset=utf-8" };
|
|
65309
|
-
function
|
|
65501
|
+
function json14(body, status = 200) {
|
|
65310
65502
|
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS12 });
|
|
65311
65503
|
}
|
|
65312
65504
|
async function bodyJson4(req) {
|
|
@@ -65318,7 +65510,7 @@ async function memoryRoutes(req, url) {
|
|
|
65318
65510
|
return null;
|
|
65319
65511
|
const cfg = getConfig();
|
|
65320
65512
|
if (!cfg.memory.enabled) {
|
|
65321
|
-
return
|
|
65513
|
+
return json14({ error: "Knowledge-graph memory is disabled" }, 503);
|
|
65322
65514
|
}
|
|
65323
65515
|
if (p === "/api/memory/config" && req.method === "GET") {
|
|
65324
65516
|
let stats = null;
|
|
@@ -65326,7 +65518,7 @@ async function memoryRoutes(req, url) {
|
|
|
65326
65518
|
const store = await getMemoryStore();
|
|
65327
65519
|
stats = store.stats();
|
|
65328
65520
|
} catch {}
|
|
65329
|
-
return
|
|
65521
|
+
return json14({
|
|
65330
65522
|
enabled: cfg.memory.enabled,
|
|
65331
65523
|
dbPath: cfg.memory.dbPath,
|
|
65332
65524
|
stats
|
|
@@ -65340,7 +65532,7 @@ async function memoryRoutes(req, url) {
|
|
|
65340
65532
|
try {
|
|
65341
65533
|
await reopenMemoryStore(body.dbPath);
|
|
65342
65534
|
} catch (e) {
|
|
65343
|
-
return
|
|
65535
|
+
return json14({
|
|
65344
65536
|
error: `Failed to open memory DB at ${body.dbPath}: ${e instanceof Error ? e.message : String(e)}`
|
|
65345
65537
|
}, 400);
|
|
65346
65538
|
}
|
|
@@ -65362,7 +65554,7 @@ async function memoryRoutes(req, url) {
|
|
|
65362
65554
|
const store = await getMemoryStore();
|
|
65363
65555
|
stats = store.stats();
|
|
65364
65556
|
} catch {}
|
|
65365
|
-
return
|
|
65557
|
+
return json14({
|
|
65366
65558
|
ok: true,
|
|
65367
65559
|
enabled: getConfig().memory.enabled,
|
|
65368
65560
|
dbPath: getConfig().memory.dbPath,
|
|
@@ -65371,58 +65563,58 @@ async function memoryRoutes(req, url) {
|
|
|
65371
65563
|
}
|
|
65372
65564
|
const store = await getMemoryStore();
|
|
65373
65565
|
if (p === "/api/memory/graph" && req.method === "GET") {
|
|
65374
|
-
return
|
|
65566
|
+
return json14(store.readGraph());
|
|
65375
65567
|
}
|
|
65376
65568
|
if (p === "/api/memory/stats" && req.method === "GET") {
|
|
65377
|
-
return
|
|
65569
|
+
return json14(store.stats());
|
|
65378
65570
|
}
|
|
65379
65571
|
if (p === "/api/memory/search" && req.method === "GET") {
|
|
65380
65572
|
const q = url.searchParams.get("q") || "";
|
|
65381
65573
|
const limit = Number(url.searchParams.get("limit") ?? 50);
|
|
65382
|
-
return
|
|
65574
|
+
return json14(store.searchNodes(q, limit));
|
|
65383
65575
|
}
|
|
65384
65576
|
if (p === "/api/memory/activity" && req.method === "GET") {
|
|
65385
65577
|
const limit = Number(url.searchParams.get("limit") ?? 50);
|
|
65386
65578
|
const since = url.searchParams.get("since");
|
|
65387
|
-
return
|
|
65579
|
+
return json14(store.activity(limit, since ? Number(since) : undefined));
|
|
65388
65580
|
}
|
|
65389
65581
|
const entityMatch = p.match(/^\/api\/memory\/entity\/(.+)$/);
|
|
65390
65582
|
if (entityMatch && req.method === "GET") {
|
|
65391
65583
|
const name = decodeURIComponent(entityMatch[1]);
|
|
65392
65584
|
const graph = store.openNodes([name]);
|
|
65393
65585
|
if (graph.entities.length === 0)
|
|
65394
|
-
return
|
|
65395
|
-
return
|
|
65586
|
+
return json14({ error: "Entity not found" }, 404);
|
|
65587
|
+
return json14({ entity: graph.entities[0], relations: graph.relations });
|
|
65396
65588
|
}
|
|
65397
65589
|
if (p === "/api/memory/entities" && req.method === "POST") {
|
|
65398
65590
|
const body = await bodyJson4(req);
|
|
65399
65591
|
const created = store.createEntities(body.entities ?? []);
|
|
65400
|
-
return
|
|
65592
|
+
return json14({ created, count: created.length });
|
|
65401
65593
|
}
|
|
65402
65594
|
if (p === "/api/memory/relations" && req.method === "POST") {
|
|
65403
65595
|
const body = await bodyJson4(req);
|
|
65404
65596
|
const created = store.createRelations(body.relations ?? []);
|
|
65405
|
-
return
|
|
65597
|
+
return json14({ created, count: created.length });
|
|
65406
65598
|
}
|
|
65407
65599
|
if (p === "/api/memory/observations" && req.method === "POST") {
|
|
65408
65600
|
const body = await bodyJson4(req);
|
|
65409
65601
|
const results = store.addObservations(body.observations ?? []);
|
|
65410
|
-
return
|
|
65602
|
+
return json14({ results });
|
|
65411
65603
|
}
|
|
65412
65604
|
if (p === "/api/memory/entities" && req.method === "DELETE") {
|
|
65413
65605
|
const body = await bodyJson4(req);
|
|
65414
65606
|
const removed = store.deleteEntities(body.names ?? []);
|
|
65415
|
-
return
|
|
65607
|
+
return json14({ removed });
|
|
65416
65608
|
}
|
|
65417
65609
|
if (p === "/api/memory/relations" && req.method === "DELETE") {
|
|
65418
65610
|
const body = await bodyJson4(req);
|
|
65419
65611
|
const removed = store.deleteRelations(body.relations ?? []);
|
|
65420
|
-
return
|
|
65612
|
+
return json14({ removed });
|
|
65421
65613
|
}
|
|
65422
65614
|
if (p === "/api/memory/observations" && req.method === "DELETE") {
|
|
65423
65615
|
const body = await bodyJson4(req);
|
|
65424
65616
|
const removed = store.deleteObservations(body.deletions ?? []);
|
|
65425
|
-
return
|
|
65617
|
+
return json14({ removed });
|
|
65426
65618
|
}
|
|
65427
65619
|
return null;
|
|
65428
65620
|
}
|
|
@@ -65430,7 +65622,7 @@ async function memoryRoutes(req, url) {
|
|
|
65430
65622
|
// src/observability/dashboard.ts
|
|
65431
65623
|
var SERVER_TAG3 = "mikrotik-mcp";
|
|
65432
65624
|
var JSON_HEADERS13 = { "content-type": "application/json; charset=utf-8" };
|
|
65433
|
-
function
|
|
65625
|
+
function json15(body, status = 200) {
|
|
65434
65626
|
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS13 });
|
|
65435
65627
|
}
|
|
65436
65628
|
function restartProcess() {
|
|
@@ -65611,25 +65803,7 @@ function topologyPayload() {
|
|
|
65611
65803
|
};
|
|
65612
65804
|
}
|
|
65613
65805
|
function configPayload() {
|
|
65614
|
-
|
|
65615
|
-
return redact({
|
|
65616
|
-
devices: cfg.devices,
|
|
65617
|
-
defaultDevice: cfg.defaultDevice,
|
|
65618
|
-
mcp: cfg.mcp,
|
|
65619
|
-
s3: cfg.s3,
|
|
65620
|
-
dashboard: cfg.dashboard,
|
|
65621
|
-
ssh: cfg.ssh,
|
|
65622
|
-
alerts: cfg.alerts,
|
|
65623
|
-
flows: cfg.flows,
|
|
65624
|
-
policy: cfg.policy,
|
|
65625
|
-
schedules: cfg.schedules,
|
|
65626
|
-
attacks: cfg.attacks,
|
|
65627
|
-
readOnly: cfg.readOnly,
|
|
65628
|
-
tools: cfg.tools,
|
|
65629
|
-
memory: cfg.memory,
|
|
65630
|
-
backupDir: cfg.backupDir,
|
|
65631
|
-
disableUpdateCheck: cfg.disableUpdateCheck
|
|
65632
|
-
});
|
|
65806
|
+
return redact(getConfig());
|
|
65633
65807
|
}
|
|
65634
65808
|
function sseResponse(transportLabel) {
|
|
65635
65809
|
let unsub;
|
|
@@ -65731,11 +65905,11 @@ function issues(error) {
|
|
|
65731
65905
|
async function configRoutes(req, url, admin) {
|
|
65732
65906
|
const p = url.pathname;
|
|
65733
65907
|
if (p === "/api/config-schema" && req.method === "GET") {
|
|
65734
|
-
return
|
|
65908
|
+
return json15(configSchemaJson());
|
|
65735
65909
|
}
|
|
65736
65910
|
if (p === "/api/config/validate" && req.method === "POST") {
|
|
65737
65911
|
const merged = mergeSecrets(await readJson(req), getConfig());
|
|
65738
|
-
return
|
|
65912
|
+
return json15(validateConfig(merged));
|
|
65739
65913
|
}
|
|
65740
65914
|
if (p === "/api/config/test-device" && req.method === "POST") {
|
|
65741
65915
|
const body = await readJson(req);
|
|
@@ -65744,22 +65918,22 @@ async function configRoutes(req, url, admin) {
|
|
|
65744
65918
|
const merged = mergeSecrets(body?.config, currentDc);
|
|
65745
65919
|
const parsed = DeviceConfigSchema.safeParse(merged);
|
|
65746
65920
|
if (!parsed.success)
|
|
65747
|
-
return
|
|
65921
|
+
return json15({ ok: false, errors: issues(parsed.error) }, 400);
|
|
65748
65922
|
const status = await probeDevice2(`config-test:${name}`, parsed.data);
|
|
65749
|
-
return
|
|
65923
|
+
return json15({ ok: true, status });
|
|
65750
65924
|
}
|
|
65751
65925
|
if (p === "/api/config/preview" && req.method === "POST") {
|
|
65752
65926
|
const before = JSON.stringify(redact(getConfig()), null, 2);
|
|
65753
65927
|
const after = JSON.stringify(await readJson(req) ?? {}, null, 2);
|
|
65754
65928
|
const d = diffLines(before, after, { fromLabel: "current", toLabel: "edited" });
|
|
65755
|
-
return
|
|
65929
|
+
return json15({ summary: d.summary, unified: d.unified });
|
|
65756
65930
|
}
|
|
65757
65931
|
if (p === "/api/config" && req.method === "POST") {
|
|
65758
65932
|
const body = await readJson(req);
|
|
65759
65933
|
const merged = mergeSecrets(body?.config, getConfig());
|
|
65760
65934
|
const v = validateConfig(merged);
|
|
65761
65935
|
if (!v.ok || !v.value)
|
|
65762
|
-
return
|
|
65936
|
+
return json15({ ok: false, errors: v.errors }, 400);
|
|
65763
65937
|
const prev = getConfig();
|
|
65764
65938
|
const before = JSON.stringify(redact(prev), null, 2);
|
|
65765
65939
|
const after = JSON.stringify(redact(v.value), null, 2);
|
|
@@ -65768,25 +65942,25 @@ async function configRoutes(req, url, admin) {
|
|
|
65768
65942
|
const nextDevs = Object.keys(v.value.devices);
|
|
65769
65943
|
const devicesChanged = prevDevs.length !== nextDevs.length || prevDevs.some((d) => !nextDevs.includes(d));
|
|
65770
65944
|
const res = admin.applyConfig(v.value, clampRollback(body?.rollbackMs));
|
|
65771
|
-
return
|
|
65945
|
+
return json15({ ok: true, ...res, devicesChanged, summary: diff.summary, unified: diff.unified });
|
|
65772
65946
|
}
|
|
65773
65947
|
if (p === "/api/config/keep" && req.method === "POST") {
|
|
65774
65948
|
const body = await readJson(req);
|
|
65775
65949
|
const kept = admin.keepConfig(String(body?.pendingId ?? ""));
|
|
65776
65950
|
if (kept)
|
|
65777
65951
|
recordVersion(getConfig(), "auto", Date.now());
|
|
65778
|
-
return
|
|
65952
|
+
return json15({ kept });
|
|
65779
65953
|
}
|
|
65780
65954
|
if (p === "/api/config/rollback" && req.method === "POST") {
|
|
65781
65955
|
const body = await readJson(req);
|
|
65782
65956
|
const rolledBack = admin.rollback(String(body?.pendingId ?? ""));
|
|
65783
|
-
return
|
|
65957
|
+
return json15({ rolledBack, config: redact(getConfig()) });
|
|
65784
65958
|
}
|
|
65785
65959
|
if (p === "/api/reload" && req.method === "POST") {
|
|
65786
65960
|
const body = await readJson(req);
|
|
65787
65961
|
if (body?.hard === true) {
|
|
65788
65962
|
const relaunched = restartProcess();
|
|
65789
|
-
return
|
|
65963
|
+
return json15({
|
|
65790
65964
|
ok: true,
|
|
65791
65965
|
mode: "restart",
|
|
65792
65966
|
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."
|
|
@@ -65797,10 +65971,10 @@ async function configRoutes(req, url, admin) {
|
|
|
65797
65971
|
setConfig(cfg);
|
|
65798
65972
|
const devices = Object.keys(cfg.devices);
|
|
65799
65973
|
logger.info(`Config reloaded from source \u2014 ${devices.length} device(s): ${devices.join(", ")}`);
|
|
65800
|
-
return
|
|
65974
|
+
return json15({ ok: true, mode: "config", count: devices.length, devices });
|
|
65801
65975
|
} catch (e) {
|
|
65802
65976
|
logger.error(`Config reload failed: ${logError(e)}`);
|
|
65803
|
-
return
|
|
65977
|
+
return json15({ ok: false, error: clientError(e) }, 500);
|
|
65804
65978
|
}
|
|
65805
65979
|
}
|
|
65806
65980
|
if (p === "/api/config/history" && req.method === "GET") {
|
|
@@ -65816,55 +65990,55 @@ async function configRoutes(req, url, admin) {
|
|
|
65816
65990
|
} catch {}
|
|
65817
65991
|
return { ...v, drift: { added, removed } };
|
|
65818
65992
|
});
|
|
65819
|
-
return
|
|
65993
|
+
return json15({ versions, bytes: historyBytes(), retention: AUTO_RETENTION });
|
|
65820
65994
|
}
|
|
65821
65995
|
if (p === "/api/config/history/get" && req.method === "GET") {
|
|
65822
65996
|
const id = url.searchParams.get("id");
|
|
65823
65997
|
if (!id)
|
|
65824
|
-
return
|
|
65998
|
+
return json15({ error: "id required" }, 400);
|
|
65825
65999
|
try {
|
|
65826
66000
|
const v = readVersion(id);
|
|
65827
|
-
return
|
|
66001
|
+
return json15({
|
|
65828
66002
|
ts: v.ts,
|
|
65829
66003
|
kind: v.kind,
|
|
65830
66004
|
label: v.label,
|
|
65831
66005
|
config: JSON.stringify(redact(v.config), null, 2)
|
|
65832
66006
|
});
|
|
65833
66007
|
} catch {
|
|
65834
|
-
return
|
|
66008
|
+
return json15({ error: "not found" }, 404);
|
|
65835
66009
|
}
|
|
65836
66010
|
}
|
|
65837
66011
|
if (p === "/api/config/history/diff" && req.method === "GET") {
|
|
65838
66012
|
const id = url.searchParams.get("id");
|
|
65839
66013
|
if (!id)
|
|
65840
|
-
return
|
|
66014
|
+
return json15({ error: "id required" }, 400);
|
|
65841
66015
|
try {
|
|
65842
66016
|
const before = JSON.stringify(redact(readVersion(id).config), null, 2);
|
|
65843
66017
|
const after = JSON.stringify(redact(getConfig()), null, 2);
|
|
65844
66018
|
const d = diffLines(before, after, { fromLabel: "this version", toLabel: "current" });
|
|
65845
|
-
return
|
|
66019
|
+
return json15({ summary: d.summary, unified: d.unified });
|
|
65846
66020
|
} catch {
|
|
65847
|
-
return
|
|
66021
|
+
return json15({ error: "not found" }, 404);
|
|
65848
66022
|
}
|
|
65849
66023
|
}
|
|
65850
66024
|
if (p === "/api/config/history/checkpoint" && req.method === "POST") {
|
|
65851
66025
|
const b = await readJson(req);
|
|
65852
66026
|
const label = b?.label?.trim() || "checkpoint";
|
|
65853
|
-
return
|
|
66027
|
+
return json15({ ok: true, version: recordVersion(getConfig(), "checkpoint", Date.now(), label) });
|
|
65854
66028
|
}
|
|
65855
66029
|
if (p === "/api/config/history/restore" && req.method === "POST") {
|
|
65856
66030
|
const b = await readJson(req);
|
|
65857
66031
|
if (!b?.id)
|
|
65858
|
-
return
|
|
66032
|
+
return json15({ error: "id required" }, 400);
|
|
65859
66033
|
let target;
|
|
65860
66034
|
try {
|
|
65861
66035
|
target = readVersion(b.id);
|
|
65862
66036
|
} catch {
|
|
65863
|
-
return
|
|
66037
|
+
return json15({ error: "not found" }, 404);
|
|
65864
66038
|
}
|
|
65865
66039
|
const v = validateConfig(target.config);
|
|
65866
66040
|
if (!v.ok || !v.value)
|
|
65867
|
-
return
|
|
66041
|
+
return json15({ ok: false, errors: v.errors }, 400);
|
|
65868
66042
|
recordVersion(getConfig(), "auto", Date.now(), "before restore");
|
|
65869
66043
|
setConfig(v.value);
|
|
65870
66044
|
let persisted = true;
|
|
@@ -65875,13 +66049,13 @@ async function configRoutes(req, url, admin) {
|
|
|
65875
66049
|
}
|
|
65876
66050
|
const label = target.label ? `restored "${target.label}"` : `restored ${b.id}`;
|
|
65877
66051
|
recordVersion(getConfig(), "auto", Date.now(), label);
|
|
65878
|
-
return
|
|
66052
|
+
return json15({ ok: true, persisted, restored: b.id, config: redact(getConfig()) });
|
|
65879
66053
|
}
|
|
65880
66054
|
if (p === "/api/config/history/delete" && req.method === "POST") {
|
|
65881
66055
|
const b = await readJson(req);
|
|
65882
66056
|
if (!b?.id)
|
|
65883
|
-
return
|
|
65884
|
-
return deleteVersion(b.id) ?
|
|
66057
|
+
return json15({ error: "id required" }, 400);
|
|
66058
|
+
return deleteVersion(b.id) ? json15({ ok: true }) : json15({ error: "not found" }, 404);
|
|
65885
66059
|
}
|
|
65886
66060
|
return null;
|
|
65887
66061
|
}
|
|
@@ -65892,7 +66066,7 @@ async function modulesRoutes(req, url) {
|
|
|
65892
66066
|
if (p === "/api/modules" && req.method === "GET") {
|
|
65893
66067
|
const cfg = getConfig();
|
|
65894
66068
|
const src = getConfigSource();
|
|
65895
|
-
return
|
|
66069
|
+
return json15({
|
|
65896
66070
|
...moduleSurface(cfg.tools),
|
|
65897
66071
|
filter: cfg.tools,
|
|
65898
66072
|
source: src,
|
|
@@ -65902,11 +66076,11 @@ async function modulesRoutes(req, url) {
|
|
|
65902
66076
|
if (p === "/api/modules/toggle" && req.method === "POST") {
|
|
65903
66077
|
const b = await readJson(req);
|
|
65904
66078
|
if (typeof b?.slug !== "string" || typeof b?.enabled !== "boolean") {
|
|
65905
|
-
return
|
|
66079
|
+
return json15({ error: "slug (string) and enabled (boolean) are required" }, 400);
|
|
65906
66080
|
}
|
|
65907
66081
|
const mod = moduleCatalog2.find((m) => m.slug.toLowerCase() === b.slug.toLowerCase());
|
|
65908
66082
|
if (!mod)
|
|
65909
|
-
return
|
|
66083
|
+
return json15({ error: `unknown module: ${b.slug}` }, 404);
|
|
65910
66084
|
const cfg = getConfig();
|
|
65911
66085
|
const nextTools = ToolFilterSchema.parse(applyModuleToggle(cfg.tools, mod.slug, b.enabled));
|
|
65912
66086
|
const next = { ...cfg, tools: nextTools };
|
|
@@ -65922,7 +66096,7 @@ async function modulesRoutes(req, url) {
|
|
|
65922
66096
|
if (persisted) {
|
|
65923
66097
|
recordVersion(getConfig(), "auto", Date.now(), `module ${b.enabled ? "enabled" : "disabled"}: ${mod.slug}`);
|
|
65924
66098
|
}
|
|
65925
|
-
return
|
|
66099
|
+
return json15({
|
|
65926
66100
|
ok: true,
|
|
65927
66101
|
persisted,
|
|
65928
66102
|
requiresReconnect: true,
|
|
@@ -65937,7 +66111,7 @@ async function modulesRoutes(req, url) {
|
|
|
65937
66111
|
if (p === "/api/modules/app-views" && req.method === "POST") {
|
|
65938
66112
|
const b = await readJson(req);
|
|
65939
66113
|
if (typeof b?.enabled !== "boolean") {
|
|
65940
|
-
return
|
|
66114
|
+
return json15({ error: "enabled (boolean) is required" }, 400);
|
|
65941
66115
|
}
|
|
65942
66116
|
const cfg = getConfig();
|
|
65943
66117
|
const next = { ...cfg, mcp: { ...cfg.mcp, appViews: b.enabled } };
|
|
@@ -65953,7 +66127,7 @@ async function modulesRoutes(req, url) {
|
|
|
65953
66127
|
if (persisted) {
|
|
65954
66128
|
recordVersion(getConfig(), "auto", Date.now(), `app views ${b.enabled ? "enabled" : "disabled"}`);
|
|
65955
66129
|
}
|
|
65956
|
-
return
|
|
66130
|
+
return json15({
|
|
65957
66131
|
ok: true,
|
|
65958
66132
|
persisted,
|
|
65959
66133
|
requiresReconnect: true,
|
|
@@ -65967,11 +66141,11 @@ async function modulesRoutes(req, url) {
|
|
|
65967
66141
|
async function captureRoutes(req, url) {
|
|
65968
66142
|
const p = url.pathname;
|
|
65969
66143
|
if (p === "/api/capture/status" && req.method === "GET") {
|
|
65970
|
-
return
|
|
66144
|
+
return json15(capture2.stats());
|
|
65971
66145
|
}
|
|
65972
66146
|
if (p === "/api/capture/packets" && req.method === "GET") {
|
|
65973
66147
|
const limit = Number(url.searchParams.get("limit") ?? 200) || 200;
|
|
65974
|
-
return
|
|
66148
|
+
return json15({ packets: capture2.recent(limit), stats: capture2.stats() });
|
|
65975
66149
|
}
|
|
65976
66150
|
if (p === "/api/capture/pcap" && req.method === "GET") {
|
|
65977
66151
|
return new Response(capture2.pcap(), {
|
|
@@ -65983,11 +66157,11 @@ async function captureRoutes(req, url) {
|
|
|
65983
66157
|
}
|
|
65984
66158
|
if (p === "/api/capture/start" && req.method === "POST") {
|
|
65985
66159
|
const body = await readJson(req);
|
|
65986
|
-
return
|
|
66160
|
+
return json15(await capture2.start(typeof body?.port === "number" ? body.port : DEFAULT_TZSP_PORT));
|
|
65987
66161
|
}
|
|
65988
66162
|
if (p === "/api/capture/stop" && req.method === "POST") {
|
|
65989
66163
|
capture2.stop();
|
|
65990
|
-
return
|
|
66164
|
+
return json15({ ok: true, ...capture2.stats() });
|
|
65991
66165
|
}
|
|
65992
66166
|
return null;
|
|
65993
66167
|
}
|
|
@@ -65999,24 +66173,24 @@ async function capsmanRoutes(req, url) {
|
|
|
65999
66173
|
if (req.method === "GET") {
|
|
66000
66174
|
const ctx = createContext(undefined, url.searchParams.get("device") ?? undefined);
|
|
66001
66175
|
if (p === "/api/capsman/overview") {
|
|
66002
|
-
return
|
|
66176
|
+
return json15(capsmanOverview(await fetchCapsmanState(ctx)));
|
|
66003
66177
|
}
|
|
66004
66178
|
if (p === "/api/capsman/clients") {
|
|
66005
66179
|
const state = await fetchCapsmanState(ctx);
|
|
66006
66180
|
const weakDbm = Number.parseInt(url.searchParams.get("weak_dbm") ?? "", 10);
|
|
66007
66181
|
const weak = reportWeakClients(state, Number.isFinite(weakDbm) ? weakDbm : undefined);
|
|
66008
|
-
return
|
|
66182
|
+
return json15({ clients: state.clients, weak });
|
|
66009
66183
|
}
|
|
66010
66184
|
if (p === "/api/capsman/audit") {
|
|
66011
|
-
return
|
|
66185
|
+
return json15(runCapsmanAudit(await fetchCapsmanState(ctx)));
|
|
66012
66186
|
}
|
|
66013
66187
|
if (p === "/api/capsman/trends") {
|
|
66014
66188
|
if (!capsmanStore)
|
|
66015
|
-
return
|
|
66189
|
+
return json15({ error: "capsman store not active" }, 503);
|
|
66016
66190
|
const device = resolveDeviceName(url.searchParams.get("device") ?? undefined);
|
|
66017
66191
|
const days = daysParam(url, 7, 30);
|
|
66018
66192
|
const series = capsmanStore.radioSeries(device, Date.now() - days * 86400000);
|
|
66019
|
-
return
|
|
66193
|
+
return json15({ series, days });
|
|
66020
66194
|
}
|
|
66021
66195
|
return null;
|
|
66022
66196
|
}
|
|
@@ -66027,45 +66201,45 @@ async function capsmanRoutes(req, url) {
|
|
|
66027
66201
|
if (p === "/api/capsman/apply/steer") {
|
|
66028
66202
|
const client = state.clients.find((c) => c.mac.toLowerCase() === (body.mac ?? "").toLowerCase());
|
|
66029
66203
|
if (!client)
|
|
66030
|
-
return
|
|
66204
|
+
return json15({ ok: false, error: "client not associated" }, 400);
|
|
66031
66205
|
if (steerAlreadyPresent(state, body.mac ?? "")) {
|
|
66032
|
-
return
|
|
66206
|
+
return json15({ ok: true, message: "already steered (no-op)" });
|
|
66033
66207
|
}
|
|
66034
66208
|
const mode = body.mode === "soft" ? "soft" : "hard";
|
|
66035
66209
|
const commands = buildSteerCommands(state, body.mac ?? "", client.radioId, mode);
|
|
66036
66210
|
if (!body.confirm)
|
|
66037
|
-
return
|
|
66038
|
-
return
|
|
66211
|
+
return json15({ ok: true, preview: commands });
|
|
66212
|
+
return json15(await applyCapsmanWrites(ctx, commands, `pre-steer-${body.mac}`));
|
|
66039
66213
|
}
|
|
66040
66214
|
if (p === "/api/capsman/apply/load-balance") {
|
|
66041
66215
|
const plan = loadBalancePlan(state);
|
|
66042
66216
|
const commands = buildLoadBalanceCommands(state, plan);
|
|
66043
66217
|
if (!body.confirm)
|
|
66044
|
-
return
|
|
66045
|
-
return
|
|
66218
|
+
return json15({ ok: true, preview: commands, plan });
|
|
66219
|
+
return json15(await applyCapsmanWrites(ctx, commands, "pre-load-balance"));
|
|
66046
66220
|
}
|
|
66047
66221
|
if (p === "/api/capsman/apply/channel-plan") {
|
|
66048
66222
|
if (state.path === "/caps-man") {
|
|
66049
|
-
return
|
|
66223
|
+
return json15({ ok: false, error: "channel-plan apply is v7 /interface wifi only" }, 400);
|
|
66050
66224
|
}
|
|
66051
66225
|
const commands = buildChannelPlanCommands(state);
|
|
66052
66226
|
if (!body.confirm)
|
|
66053
|
-
return
|
|
66054
|
-
return
|
|
66227
|
+
return json15({ ok: true, preview: commands });
|
|
66228
|
+
return json15(await applyCapsmanWrites(ctx, commands, "pre-channel-plan"));
|
|
66055
66229
|
}
|
|
66056
66230
|
if (p === "/api/capsman/apply/ft") {
|
|
66057
66231
|
const commands = buildFtCommands(state);
|
|
66058
66232
|
if (!body.confirm)
|
|
66059
|
-
return
|
|
66060
|
-
return
|
|
66233
|
+
return json15({ ok: true, preview: commands });
|
|
66234
|
+
return json15(await applyCapsmanWrites(ctx, commands, "pre-ft"));
|
|
66061
66235
|
}
|
|
66062
66236
|
if (p === "/api/capsman/apply/ha") {
|
|
66063
66237
|
const commands = buildHaCommands(state);
|
|
66064
66238
|
const guidance = haGuidance(state);
|
|
66065
66239
|
if (!body.confirm)
|
|
66066
|
-
return
|
|
66240
|
+
return json15({ ok: true, preview: commands, guidance });
|
|
66067
66241
|
const res = await applyCapsmanWrites(ctx, commands, "pre-ha");
|
|
66068
|
-
return
|
|
66242
|
+
return json15({ ...res, guidance });
|
|
66069
66243
|
}
|
|
66070
66244
|
}
|
|
66071
66245
|
return null;
|
|
@@ -66091,32 +66265,32 @@ async function clientsRoutes(req, url) {
|
|
|
66091
66265
|
const deviceFromQuery = () => url.searchParams.get("device") ?? undefined;
|
|
66092
66266
|
if (p === "/api/clients" && req.method === "GET") {
|
|
66093
66267
|
const ctx = createContext(undefined, deviceFromQuery());
|
|
66094
|
-
return
|
|
66268
|
+
return json15(devicesView(await fetchDevices(ctx)));
|
|
66095
66269
|
}
|
|
66096
66270
|
if (p === "/api/clients/traffic" && req.method === "GET") {
|
|
66097
66271
|
const ip = url.searchParams.get("ip");
|
|
66098
66272
|
if (!ip)
|
|
66099
|
-
return
|
|
66273
|
+
return json15({ error: "ip required" }, 400);
|
|
66100
66274
|
const ctx = createContext(undefined, deviceFromQuery());
|
|
66101
|
-
return
|
|
66275
|
+
return json15(await sampleDeviceTraffic(ctx, ip));
|
|
66102
66276
|
}
|
|
66103
66277
|
if (p === "/api/clients/traffic-bulk" && req.method === "GET") {
|
|
66104
66278
|
const ctx = createContext(undefined, deviceFromQuery());
|
|
66105
|
-
return
|
|
66279
|
+
return json15(await sampleAllTraffic(ctx));
|
|
66106
66280
|
}
|
|
66107
66281
|
if (req.method === "POST") {
|
|
66108
66282
|
const b = await readJson(req);
|
|
66109
66283
|
const ctx = createContext(undefined, b?.device);
|
|
66110
66284
|
if (p === "/api/clients/limits") {
|
|
66111
66285
|
if (!b?.ip)
|
|
66112
|
-
return
|
|
66113
|
-
return
|
|
66286
|
+
return json15({ error: "ip required" }, 400);
|
|
66287
|
+
return json15(await setDeviceLimits(ctx, b.ip, { download: b.download, upload: b.upload }));
|
|
66114
66288
|
}
|
|
66115
66289
|
if (!b?.mac)
|
|
66116
|
-
return
|
|
66290
|
+
return json15({ error: "mac required" }, 400);
|
|
66117
66291
|
const run = async (op) => {
|
|
66118
66292
|
const r = await op;
|
|
66119
|
-
return
|
|
66293
|
+
return json15({ ...r, view: r.ok ? devicesView(await fetchDevices(ctx)) : undefined });
|
|
66120
66294
|
};
|
|
66121
66295
|
if (p === "/api/clients/block")
|
|
66122
66296
|
return run(blockDevice(ctx, b.mac, b.comment));
|
|
@@ -66128,12 +66302,12 @@ async function clientsRoutes(req, url) {
|
|
|
66128
66302
|
return run(removeDeviceLease(ctx, b.mac));
|
|
66129
66303
|
if (p === "/api/clients/set-ip") {
|
|
66130
66304
|
if (!b.ip)
|
|
66131
|
-
return
|
|
66305
|
+
return json15({ error: "ip required" }, 400);
|
|
66132
66306
|
return run(setDeviceIp(ctx, b.mac, b.ip));
|
|
66133
66307
|
}
|
|
66134
66308
|
if (p === "/api/clients/label") {
|
|
66135
66309
|
if (typeof b.label !== "string")
|
|
66136
|
-
return
|
|
66310
|
+
return json15({ error: "label required" }, 400);
|
|
66137
66311
|
return run(setDeviceLabel(ctx, b.mac, b.label));
|
|
66138
66312
|
}
|
|
66139
66313
|
}
|
|
@@ -66146,20 +66320,20 @@ async function aaaRoutes(req, url) {
|
|
|
66146
66320
|
const deviceFromQuery = () => url.searchParams.get("device") ?? undefined;
|
|
66147
66321
|
if (req.method === "GET") {
|
|
66148
66322
|
if (p === "/api/aaa/entities") {
|
|
66149
|
-
return
|
|
66323
|
+
return json15({ entities: AAA_ENTITIES });
|
|
66150
66324
|
}
|
|
66151
66325
|
if (p === "/api/aaa/radius-incoming") {
|
|
66152
|
-
return
|
|
66326
|
+
return json15(await getRadiusIncoming(createContext(undefined, deviceFromQuery())));
|
|
66153
66327
|
}
|
|
66154
66328
|
if (p === "/api/aaa/um-settings") {
|
|
66155
|
-
return
|
|
66329
|
+
return json15(await getUmSettings(createContext(undefined, deviceFromQuery())));
|
|
66156
66330
|
}
|
|
66157
66331
|
const listMatch = p.match(/^\/api\/aaa\/list\/([\w-]+)$/);
|
|
66158
66332
|
if (listMatch) {
|
|
66159
66333
|
const slug = listMatch[1];
|
|
66160
66334
|
if (!AAA_ENTITIES[slug])
|
|
66161
|
-
return
|
|
66162
|
-
return
|
|
66335
|
+
return json15({ error: "unknown entity" }, 404);
|
|
66336
|
+
return json15(await listAaaEntity(createContext(undefined, deviceFromQuery()), slug));
|
|
66163
66337
|
}
|
|
66164
66338
|
return null;
|
|
66165
66339
|
}
|
|
@@ -66167,30 +66341,30 @@ async function aaaRoutes(req, url) {
|
|
|
66167
66341
|
const body = await readJson(req);
|
|
66168
66342
|
const ctx = createContext(undefined, body?.device);
|
|
66169
66343
|
if (p === "/api/aaa/radius-incoming")
|
|
66170
|
-
return
|
|
66344
|
+
return json15(await setRadiusIncoming(ctx, body?.fields ?? {}));
|
|
66171
66345
|
if (p === "/api/aaa/um-settings")
|
|
66172
|
-
return
|
|
66346
|
+
return json15(await setUmSettings(ctx, body?.fields ?? {}));
|
|
66173
66347
|
if (p === "/api/aaa/radius-reset-counters")
|
|
66174
|
-
return
|
|
66348
|
+
return json15(await resetRadiusCounters(ctx));
|
|
66175
66349
|
const slug = body?.slug ?? "";
|
|
66176
66350
|
if (!AAA_ENTITIES[slug])
|
|
66177
|
-
return
|
|
66351
|
+
return json15({ error: "unknown entity" }, 404);
|
|
66178
66352
|
if (p === "/api/aaa/add")
|
|
66179
|
-
return
|
|
66353
|
+
return json15(await addAaaEntity(ctx, slug, body?.fields ?? {}));
|
|
66180
66354
|
if (p === "/api/aaa/update") {
|
|
66181
66355
|
if (!body?.id)
|
|
66182
|
-
return
|
|
66183
|
-
return
|
|
66356
|
+
return json15({ error: "id required" }, 400);
|
|
66357
|
+
return json15(await updateAaaEntity(ctx, slug, body.id, body?.fields ?? {}));
|
|
66184
66358
|
}
|
|
66185
66359
|
if (p === "/api/aaa/remove") {
|
|
66186
66360
|
if (!body?.id)
|
|
66187
|
-
return
|
|
66188
|
-
return
|
|
66361
|
+
return json15({ error: "id required" }, 400);
|
|
66362
|
+
return json15(await removeAaaEntity(ctx, slug, body.id));
|
|
66189
66363
|
}
|
|
66190
66364
|
if (p === "/api/aaa/toggle") {
|
|
66191
66365
|
if (!body?.id)
|
|
66192
|
-
return
|
|
66193
|
-
return
|
|
66366
|
+
return json15({ error: "id required" }, 400);
|
|
66367
|
+
return json15(await toggleAaaEntity(ctx, slug, body.id, body?.enable === true));
|
|
66194
66368
|
}
|
|
66195
66369
|
}
|
|
66196
66370
|
return null;
|
|
@@ -66206,43 +66380,43 @@ async function usageRoutes(req, url) {
|
|
|
66206
66380
|
return null;
|
|
66207
66381
|
if (p === "/api/usage/sampler") {
|
|
66208
66382
|
if (req.method === "GET")
|
|
66209
|
-
return
|
|
66383
|
+
return json15({ intervalMs: getUsageSamplerInterval() });
|
|
66210
66384
|
if (req.method === "POST") {
|
|
66211
66385
|
const b = await readJson(req);
|
|
66212
66386
|
const applied = setUsageSamplerInterval(Number(b?.intervalMs));
|
|
66213
|
-
return
|
|
66387
|
+
return json15({ intervalMs: applied });
|
|
66214
66388
|
}
|
|
66215
66389
|
return null;
|
|
66216
66390
|
}
|
|
66217
66391
|
if (req.method !== "GET")
|
|
66218
66392
|
return null;
|
|
66219
66393
|
if (!usageStore)
|
|
66220
|
-
return
|
|
66394
|
+
return json15({ error: "usage store not active" }, 503);
|
|
66221
66395
|
const device = resolveDeviceName(url.searchParams.get("device") ?? undefined);
|
|
66222
66396
|
const sinceTs = (days) => Date.now() - days * 86400000;
|
|
66223
66397
|
if (p === "/api/usage/um-users") {
|
|
66224
|
-
return
|
|
66398
|
+
return json15({ users: usageStore.umUsers(device) });
|
|
66225
66399
|
}
|
|
66226
66400
|
if (p === "/api/usage/client") {
|
|
66227
66401
|
const ip = url.searchParams.get("ip");
|
|
66228
66402
|
if (!ip)
|
|
66229
|
-
return
|
|
66403
|
+
return json15({ error: "ip required" }, 400);
|
|
66230
66404
|
const series = usageStore.clientDailyUsage(device, ip, sinceTs(daysParam(url, 90, 400)));
|
|
66231
|
-
return
|
|
66405
|
+
return json15(withTotals(series));
|
|
66232
66406
|
}
|
|
66233
66407
|
if (p === "/api/usage/um-user") {
|
|
66234
66408
|
const user = url.searchParams.get("user");
|
|
66235
66409
|
if (!user)
|
|
66236
|
-
return
|
|
66410
|
+
return json15({ error: "user required" }, 400);
|
|
66237
66411
|
const series = usageStore.umUserDailyUsage(device, user, sinceTs(daysParam(url, 90, 400)));
|
|
66238
|
-
return
|
|
66412
|
+
return json15(withTotals(series));
|
|
66239
66413
|
}
|
|
66240
66414
|
if (p === "/api/usage/heatmap") {
|
|
66241
66415
|
const user = url.searchParams.get("user");
|
|
66242
66416
|
const days = usageStore.heatmap(device, user || null, sinceTs(daysParam(url, 371, 400)));
|
|
66243
66417
|
const total = days.reduce((s, d) => s + d.count, 0);
|
|
66244
66418
|
const max = days.reduce((m, d) => Math.max(m, d.count), 0);
|
|
66245
|
-
return
|
|
66419
|
+
return json15({ days, total, max });
|
|
66246
66420
|
}
|
|
66247
66421
|
return null;
|
|
66248
66422
|
}
|
|
@@ -66266,13 +66440,13 @@ async function featureRoutes(req, url) {
|
|
|
66266
66440
|
const cfg = getConfig();
|
|
66267
66441
|
const all = Object.keys(cfg.devices).flatMap((d) => store.list(d, 200, false));
|
|
66268
66442
|
all.sort((a, b) => b.ts - a.ts);
|
|
66269
|
-
return
|
|
66443
|
+
return json15({ snapshots: all });
|
|
66270
66444
|
}
|
|
66271
66445
|
const snapMatch = p.match(/^\/api\/snapshot\/(.+)$/);
|
|
66272
66446
|
if (snapMatch && req.method === "GET") {
|
|
66273
66447
|
const store = await snapStore();
|
|
66274
66448
|
const s = store.get(decodeURIComponent(snapMatch[1]));
|
|
66275
|
-
return s ?
|
|
66449
|
+
return s ? json15(s) : json15({ error: "not found" }, 404);
|
|
66276
66450
|
}
|
|
66277
66451
|
if (p === "/api/snapshots/diff" && req.method === "POST") {
|
|
66278
66452
|
const store = await snapStore();
|
|
@@ -66280,13 +66454,13 @@ async function featureRoutes(req, url) {
|
|
|
66280
66454
|
const from = b?.from ? store.get(b.from) : null;
|
|
66281
66455
|
const to = b?.to ? store.get(b.to) : null;
|
|
66282
66456
|
if (!from || !to) {
|
|
66283
|
-
return
|
|
66457
|
+
return json15({ error: "both 'from' and 'to' snapshot ids are required" }, 400);
|
|
66284
66458
|
}
|
|
66285
66459
|
const diff = diffLines(normalizeExport(from.body), normalizeExport(to.body), {
|
|
66286
66460
|
fromLabel: from.label ?? from.id,
|
|
66287
66461
|
toLabel: to.label ?? to.id
|
|
66288
66462
|
});
|
|
66289
|
-
return
|
|
66463
|
+
return json15({
|
|
66290
66464
|
from: { id: from.id, label: from.label, ts: from.ts, device: from.device },
|
|
66291
66465
|
to: { id: to.id, label: to.label, ts: to.ts, device: to.device },
|
|
66292
66466
|
summary: diff.summary,
|
|
@@ -66300,16 +66474,16 @@ async function featureRoutes(req, url) {
|
|
|
66300
66474
|
...b?.script ? splitCommands(b.script) : []
|
|
66301
66475
|
];
|
|
66302
66476
|
if (commands.length === 0)
|
|
66303
|
-
return
|
|
66477
|
+
return json15({ error: "no commands provided" }, 400);
|
|
66304
66478
|
const plan = buildChangePlan(commands);
|
|
66305
|
-
return
|
|
66479
|
+
return json15({ plan, text: renderPlan(plan) });
|
|
66306
66480
|
}
|
|
66307
66481
|
if (p === "/api/s3" && req.method === "GET") {
|
|
66308
|
-
return
|
|
66482
|
+
return json15({ configured: isS3Configured(), target: isS3Configured() ? s3Target() : null });
|
|
66309
66483
|
}
|
|
66310
66484
|
if (p === "/api/s3/list" && req.method === "GET") {
|
|
66311
66485
|
if (!isS3Configured())
|
|
66312
|
-
return
|
|
66486
|
+
return json15({ configured: false, objects: [] });
|
|
66313
66487
|
const prefix = url.searchParams.get("prefix") ?? "";
|
|
66314
66488
|
try {
|
|
66315
66489
|
const res = await getS3Client().list({ prefix: prefix || undefined, maxKeys: 1000 });
|
|
@@ -66318,42 +66492,42 @@ async function featureRoutes(req, url) {
|
|
|
66318
66492
|
size: o.size ?? 0,
|
|
66319
66493
|
lastModified: o.lastModified ?? null
|
|
66320
66494
|
}));
|
|
66321
|
-
return
|
|
66495
|
+
return json15({ configured: true, target: s3Target(), objects, truncated: !!res.isTruncated });
|
|
66322
66496
|
} catch (e) {
|
|
66323
|
-
return
|
|
66497
|
+
return json15({ error: clientError(e) }, 502);
|
|
66324
66498
|
}
|
|
66325
66499
|
}
|
|
66326
66500
|
if (p === "/api/s3/presign" && req.method === "GET") {
|
|
66327
66501
|
if (!isS3Configured())
|
|
66328
|
-
return
|
|
66502
|
+
return json15({ error: "S3 not configured" }, 400);
|
|
66329
66503
|
const key = url.searchParams.get("key");
|
|
66330
66504
|
if (!key)
|
|
66331
|
-
return
|
|
66505
|
+
return json15({ error: "key required" }, 400);
|
|
66332
66506
|
try {
|
|
66333
66507
|
const link = getS3Client().presign(key, { expiresIn: presignExpiresIn(), method: "GET" });
|
|
66334
|
-
return
|
|
66508
|
+
return json15({ url: link });
|
|
66335
66509
|
} catch (e) {
|
|
66336
|
-
return
|
|
66510
|
+
return json15({ error: clientError(e) }, 502);
|
|
66337
66511
|
}
|
|
66338
66512
|
}
|
|
66339
66513
|
if (p === "/api/s3/delete" && req.method === "POST") {
|
|
66340
66514
|
if (!isS3Configured())
|
|
66341
|
-
return
|
|
66515
|
+
return json15({ error: "S3 not configured" }, 400);
|
|
66342
66516
|
const b = await readJson(req);
|
|
66343
66517
|
if (!b?.key)
|
|
66344
|
-
return
|
|
66518
|
+
return json15({ error: "key required" }, 400);
|
|
66345
66519
|
try {
|
|
66346
66520
|
const client = getS3Client();
|
|
66347
66521
|
if (!await client.exists(b.key))
|
|
66348
|
-
return
|
|
66522
|
+
return json15({ error: "not found" }, 404);
|
|
66349
66523
|
await client.delete(b.key);
|
|
66350
|
-
return
|
|
66524
|
+
return json15({ ok: true, key: b.key });
|
|
66351
66525
|
} catch (e) {
|
|
66352
|
-
return
|
|
66526
|
+
return json15({ error: clientError(e) }, 502);
|
|
66353
66527
|
}
|
|
66354
66528
|
}
|
|
66355
66529
|
if (p === "/api/backups" && req.method === "GET") {
|
|
66356
|
-
return
|
|
66530
|
+
return json15({
|
|
66357
66531
|
dir: backupDir(),
|
|
66358
66532
|
devices: Object.keys(getConfig().devices),
|
|
66359
66533
|
backups: listBackups()
|
|
@@ -66363,14 +66537,14 @@ async function featureRoutes(req, url) {
|
|
|
66363
66537
|
const b = await readJson(req);
|
|
66364
66538
|
const raw = b?.dir?.trim();
|
|
66365
66539
|
if (!raw)
|
|
66366
|
-
return
|
|
66540
|
+
return json15({ error: "dir required" }, 400);
|
|
66367
66541
|
const dir = raw === "~" || raw.startsWith("~/") ? join12(homedir5(), raw.slice(1)) : raw;
|
|
66368
66542
|
const next = { ...getConfig(), backupDir: dir };
|
|
66369
66543
|
setConfig(next);
|
|
66370
66544
|
try {
|
|
66371
66545
|
atomicWrite(getConfigSource().path, serializeConfig(next));
|
|
66372
66546
|
} catch (e) {
|
|
66373
|
-
return
|
|
66547
|
+
return json15({
|
|
66374
66548
|
ok: true,
|
|
66375
66549
|
dir: backupDir(),
|
|
66376
66550
|
persisted: false,
|
|
@@ -66378,16 +66552,16 @@ async function featureRoutes(req, url) {
|
|
|
66378
66552
|
});
|
|
66379
66553
|
}
|
|
66380
66554
|
recordVersion(getConfig(), "auto", Date.now(), "backup path changed");
|
|
66381
|
-
return
|
|
66555
|
+
return json15({ ok: true, dir: backupDir(), persisted: true });
|
|
66382
66556
|
}
|
|
66383
66557
|
if (p === "/api/backups/get" && req.method === "GET") {
|
|
66384
66558
|
const name = url.searchParams.get("name");
|
|
66385
66559
|
if (!name)
|
|
66386
|
-
return
|
|
66560
|
+
return json15({ error: "name required" }, 400);
|
|
66387
66561
|
try {
|
|
66388
|
-
return
|
|
66562
|
+
return json15({ name, content: readBackup(name) });
|
|
66389
66563
|
} catch {
|
|
66390
|
-
return
|
|
66564
|
+
return json15({ error: "not found" }, 404);
|
|
66391
66565
|
}
|
|
66392
66566
|
}
|
|
66393
66567
|
if (p === "/api/backups/raw" && req.method === "GET") {
|
|
@@ -66408,41 +66582,41 @@ async function featureRoutes(req, url) {
|
|
|
66408
66582
|
if (p === "/api/backups/upload" && req.method === "POST") {
|
|
66409
66583
|
const b = await readJson(req);
|
|
66410
66584
|
if (!b?.name || typeof b.content !== "string") {
|
|
66411
|
-
return
|
|
66585
|
+
return json15({ error: "name and content are required" }, 400);
|
|
66412
66586
|
}
|
|
66413
66587
|
try {
|
|
66414
66588
|
const safe = b.name.endsWith(".rsc") ? b.name : `${b.name}.rsc`;
|
|
66415
|
-
return
|
|
66589
|
+
return json15({ ok: true, name: writeBackup(safe, b.content) });
|
|
66416
66590
|
} catch (e) {
|
|
66417
|
-
return
|
|
66591
|
+
return json15({ error: clientError(e) }, 400);
|
|
66418
66592
|
}
|
|
66419
66593
|
}
|
|
66420
66594
|
if (p === "/api/backups/rename" && req.method === "POST") {
|
|
66421
66595
|
const b = await readJson(req);
|
|
66422
66596
|
if (!b?.name || !b?.new_name)
|
|
66423
|
-
return
|
|
66597
|
+
return json15({ error: "name and new_name are required" }, 400);
|
|
66424
66598
|
try {
|
|
66425
|
-
return
|
|
66599
|
+
return json15({ ok: true, name: renameBackup(b.name, b.new_name) });
|
|
66426
66600
|
} catch (e) {
|
|
66427
|
-
return
|
|
66601
|
+
return json15({ error: clientError(e) }, 400);
|
|
66428
66602
|
}
|
|
66429
66603
|
}
|
|
66430
66604
|
if (p === "/api/backups/delete" && req.method === "POST") {
|
|
66431
66605
|
const b = await readJson(req);
|
|
66432
66606
|
if (!b?.name)
|
|
66433
|
-
return
|
|
66607
|
+
return json15({ error: "name required" }, 400);
|
|
66434
66608
|
try {
|
|
66435
|
-
return deleteBackup(b.name) ?
|
|
66609
|
+
return deleteBackup(b.name) ? json15({ ok: true }) : json15({ error: "not found" }, 404);
|
|
66436
66610
|
} catch (e) {
|
|
66437
|
-
return
|
|
66611
|
+
return json15({ error: clientError(e) }, 400);
|
|
66438
66612
|
}
|
|
66439
66613
|
}
|
|
66440
66614
|
if (p === "/api/backups/restore" && req.method === "POST") {
|
|
66441
66615
|
const b = await readJson(req);
|
|
66442
66616
|
if (!b?.name)
|
|
66443
|
-
return
|
|
66617
|
+
return json15({ error: "name required" }, 400);
|
|
66444
66618
|
const device = resolveDeviceName(b.device);
|
|
66445
|
-
return
|
|
66619
|
+
return json15(await restoreLocalBackup(device, b.name, b.confirm === true));
|
|
66446
66620
|
}
|
|
66447
66621
|
if (p === "/api/backups/create" && req.method === "POST") {
|
|
66448
66622
|
const b = await readJson(req);
|
|
@@ -66454,7 +66628,7 @@ async function featureRoutes(req, url) {
|
|
|
66454
66628
|
compact: b?.compact === true,
|
|
66455
66629
|
terse: b?.terse === true
|
|
66456
66630
|
});
|
|
66457
|
-
return r.ok ?
|
|
66631
|
+
return r.ok ? json15(r) : json15({ error: r.error ?? "export failed" }, 502);
|
|
66458
66632
|
}
|
|
66459
66633
|
return null;
|
|
66460
66634
|
}
|
|
@@ -66515,6 +66689,7 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
66515
66689
|
const bearer = req.headers.get("authorization")?.replace(/^Bearer\s+/i, "");
|
|
66516
66690
|
return bearer === cfg.token || url.searchParams.get("token") === cfg.token;
|
|
66517
66691
|
};
|
|
66692
|
+
const accessSettingsRoutes = createAccessSettingsRoutes(configAdmin);
|
|
66518
66693
|
async function dashboardRoute(req, url, srv, ca, tl) {
|
|
66519
66694
|
if (url.pathname === "/api/stream") {
|
|
66520
66695
|
const traffic = url.searchParams.has("traffic") ? url.searchParams.get("traffic") ?? "" : undefined;
|
|
@@ -66526,6 +66701,9 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
66526
66701
|
const traffic = url.searchParams.has("traffic") ? url.searchParams.get("traffic") ?? "" : undefined;
|
|
66527
66702
|
return traffic === undefined ? sseResponse(tl) : sseTrafficResponse(traffic, tl);
|
|
66528
66703
|
}
|
|
66704
|
+
const accessResp = await accessSettingsRoutes(req, url);
|
|
66705
|
+
if (accessResp)
|
|
66706
|
+
return accessResp;
|
|
66529
66707
|
const configResp = await configRoutes(req, url, ca);
|
|
66530
66708
|
if (configResp)
|
|
66531
66709
|
return configResp;
|
|
@@ -66597,7 +66775,7 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
66597
66775
|
return memoryResp;
|
|
66598
66776
|
const db = getEventStore();
|
|
66599
66777
|
if (!db)
|
|
66600
|
-
return
|
|
66778
|
+
return json15({ error: "recorder not active" }, 503);
|
|
66601
66779
|
if (url.pathname === "/api/events" && req.method === "DELETE") {
|
|
66602
66780
|
let body = {};
|
|
66603
66781
|
try {
|
|
@@ -66605,7 +66783,7 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
66605
66783
|
} catch {}
|
|
66606
66784
|
const ids = Array.isArray(body.ids) ? body.ids.filter((x) => typeof x === "string") : [];
|
|
66607
66785
|
const removed = body.all === true ? db.clear() : db.delete(ids);
|
|
66608
|
-
return
|
|
66786
|
+
return json15({ removed, total: db.total() });
|
|
66609
66787
|
}
|
|
66610
66788
|
if (url.pathname === "/api/ssh-pool") {
|
|
66611
66789
|
const cfg = getConfig();
|
|
@@ -66614,7 +66792,7 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
66614
66792
|
const totalInflight = ps.reduce((s, p) => s + p.inflight, 0);
|
|
66615
66793
|
const totalIdle = ps.filter((p) => p.idle).length;
|
|
66616
66794
|
const totalBusy = ps.filter((p) => p.inflight > 0).length;
|
|
66617
|
-
return
|
|
66795
|
+
return json15({
|
|
66618
66796
|
enabled,
|
|
66619
66797
|
config: {
|
|
66620
66798
|
keepAlive: cfg.ssh.keepAlive,
|
|
@@ -66643,39 +66821,39 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
66643
66821
|
});
|
|
66644
66822
|
}
|
|
66645
66823
|
if (url.pathname === "/api/devices") {
|
|
66646
|
-
return
|
|
66824
|
+
return json15(devicesPayload(db));
|
|
66647
66825
|
}
|
|
66648
66826
|
if (url.pathname === "/api/capabilities") {
|
|
66649
66827
|
const devices = Object.keys(getConfig().devices).map((name) => ({
|
|
66650
66828
|
device: name,
|
|
66651
66829
|
capabilities: serializeCapabilities(peekCapabilities(name))
|
|
66652
66830
|
}));
|
|
66653
|
-
return
|
|
66831
|
+
return json15({ devices });
|
|
66654
66832
|
}
|
|
66655
66833
|
if (url.pathname === "/api/capabilities/refresh" && req.method === "POST") {
|
|
66656
66834
|
const b = await readJson(req);
|
|
66657
66835
|
if (typeof b?.device !== "string")
|
|
66658
|
-
return
|
|
66836
|
+
return json15({ error: "device (string) is required" }, 400);
|
|
66659
66837
|
if (!(b.device in getConfig().devices)) {
|
|
66660
|
-
return
|
|
66838
|
+
return json15({ error: `unknown device: ${b.device}` }, 404);
|
|
66661
66839
|
}
|
|
66662
66840
|
invalidateCapabilities(b.device);
|
|
66663
66841
|
try {
|
|
66664
66842
|
const caps = await getCapabilities(b.device);
|
|
66665
|
-
return
|
|
66843
|
+
return json15({ device: b.device, capabilities: serializeCapabilities(caps) });
|
|
66666
66844
|
} catch (e) {
|
|
66667
66845
|
logger.error(`Capability probe failed for '${b.device}': ${logError(e)}`);
|
|
66668
|
-
return
|
|
66846
|
+
return json15({ error: clientError(e) }, 502);
|
|
66669
66847
|
}
|
|
66670
66848
|
}
|
|
66671
66849
|
if (url.pathname === "/api/devices/toggle" && req.method === "POST") {
|
|
66672
66850
|
const b = await readJson(req);
|
|
66673
66851
|
if (typeof b?.device !== "string" || typeof b?.disabled !== "boolean") {
|
|
66674
|
-
return
|
|
66852
|
+
return json15({ error: "device (string) and disabled (boolean) are required" }, 400);
|
|
66675
66853
|
}
|
|
66676
66854
|
const cfg = getConfig();
|
|
66677
66855
|
if (!(b.device in cfg.devices))
|
|
66678
|
-
return
|
|
66856
|
+
return json15({ error: `unknown device: ${b.device}` }, 404);
|
|
66679
66857
|
const dc = cfg.devices[b.device];
|
|
66680
66858
|
const next = {
|
|
66681
66859
|
...cfg,
|
|
@@ -66696,7 +66874,7 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
66696
66874
|
if (persisted) {
|
|
66697
66875
|
recordVersion(getConfig(), "auto", Date.now(), `device ${b.disabled ? "disabled" : "enabled"}: ${b.device}`);
|
|
66698
66876
|
}
|
|
66699
|
-
return
|
|
66877
|
+
return json15({
|
|
66700
66878
|
ok: true,
|
|
66701
66879
|
persisted,
|
|
66702
66880
|
requiresReconnect: true,
|
|
@@ -66709,17 +66887,17 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
66709
66887
|
const name = typeof b?.device === "string" ? b.device : "";
|
|
66710
66888
|
const cfg = getConfig();
|
|
66711
66889
|
if (!(name in cfg.devices))
|
|
66712
|
-
return
|
|
66890
|
+
return json15({ error: `unknown device: ${name}` }, 404);
|
|
66713
66891
|
if (url.pathname === "/api/devices/reconnect")
|
|
66714
66892
|
closeDevice(name);
|
|
66715
66893
|
const status = await probeDevice2(name, cfg.devices[name]);
|
|
66716
|
-
return
|
|
66894
|
+
return json15({ ok: true, status, ...devicesPayload(db) });
|
|
66717
66895
|
}
|
|
66718
66896
|
if (url.pathname === "/api/topology") {
|
|
66719
|
-
return
|
|
66897
|
+
return json15(topologyPayload());
|
|
66720
66898
|
}
|
|
66721
66899
|
if (url.pathname === "/api/config") {
|
|
66722
|
-
return
|
|
66900
|
+
return json15(configPayload());
|
|
66723
66901
|
}
|
|
66724
66902
|
if (url.pathname === "/" || url.pathname === "/index.html") {
|
|
66725
66903
|
return new Response(dashboardHtml(), {
|
|
@@ -66728,9 +66906,9 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
66728
66906
|
}
|
|
66729
66907
|
if (url.pathname === "/api/releases/latest") {
|
|
66730
66908
|
try {
|
|
66731
|
-
return
|
|
66909
|
+
return json15(await fetchLatestRelease());
|
|
66732
66910
|
} catch (e) {
|
|
66733
|
-
return
|
|
66911
|
+
return json15({ error: clientError(e, "fetch failed") }, 502);
|
|
66734
66912
|
}
|
|
66735
66913
|
}
|
|
66736
66914
|
if (url.pathname === "/api/catalog" && req.method === "GET") {
|
|
@@ -66749,7 +66927,7 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
66749
66927
|
}));
|
|
66750
66928
|
const prompts = listPrompts();
|
|
66751
66929
|
const groups = [...new Set(modules.map((m) => m.group))].sort();
|
|
66752
|
-
return
|
|
66930
|
+
return json15({
|
|
66753
66931
|
modules,
|
|
66754
66932
|
prompts,
|
|
66755
66933
|
groups,
|
|
@@ -66764,25 +66942,25 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
66764
66942
|
}
|
|
66765
66943
|
if (url.pathname === "/api/releases" && req.method === "GET") {
|
|
66766
66944
|
try {
|
|
66767
|
-
return
|
|
66945
|
+
return json15(await fetchAllReleases());
|
|
66768
66946
|
} catch (e) {
|
|
66769
|
-
return
|
|
66947
|
+
return json15({ error: clientError(e, "fetch failed") }, 502);
|
|
66770
66948
|
}
|
|
66771
66949
|
}
|
|
66772
66950
|
if (url.pathname === "/api/upgrade" && req.method === "POST") {
|
|
66773
66951
|
const body = await readJson(req);
|
|
66774
66952
|
const version = String(body?.version ?? "latest");
|
|
66775
66953
|
if (version !== "latest" && !/^\d+\.\d+\.\d+$/.test(version)) {
|
|
66776
|
-
return
|
|
66954
|
+
return json15({ ok: false, error: `Invalid version "${version}" (use "latest" or x.y.z).` }, 400);
|
|
66777
66955
|
}
|
|
66778
66956
|
const spec = `@usex/mikrotik-mcp@${version}`;
|
|
66779
66957
|
logger.warn(`Dashboard requested upgrade: bun i -g ${spec}`);
|
|
66780
66958
|
const { ok, log } = await runUpgrade(spec);
|
|
66781
66959
|
if (!ok)
|
|
66782
|
-
return
|
|
66960
|
+
return json15({ ok: false, version, log }, 500);
|
|
66783
66961
|
const willRestart = body?.restart !== false;
|
|
66784
66962
|
const relaunched = willRestart ? restartProcess() : false;
|
|
66785
|
-
return
|
|
66963
|
+
return json15({
|
|
66786
66964
|
ok: true,
|
|
66787
66965
|
version,
|
|
66788
66966
|
log,
|
|
@@ -66792,7 +66970,7 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
66792
66970
|
}
|
|
66793
66971
|
if (url.pathname === "/api/meta") {
|
|
66794
66972
|
const f = facets(db);
|
|
66795
|
-
return
|
|
66973
|
+
return json15({
|
|
66796
66974
|
...f,
|
|
66797
66975
|
version: VERSION,
|
|
66798
66976
|
risks: ["READ", "WRITE", "WRITE_IDEMPOTENT", "DESTRUCTIVE", "DANGEROUS"],
|
|
@@ -66803,19 +66981,19 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
66803
66981
|
}
|
|
66804
66982
|
if (url.pathname === "/api/events") {
|
|
66805
66983
|
const filter = filterFromQuery(url);
|
|
66806
|
-
return
|
|
66984
|
+
return json15({ events: db.query(filter), total: db.total() });
|
|
66807
66985
|
}
|
|
66808
66986
|
const eventMatch = url.pathname.match(/^\/api\/event\/(.+)$/);
|
|
66809
66987
|
if (eventMatch) {
|
|
66810
66988
|
const e = db.get(decodeURIComponent(eventMatch[1]));
|
|
66811
|
-
return e ?
|
|
66989
|
+
return e ? json15(e) : json15({ error: "not found" }, 404);
|
|
66812
66990
|
}
|
|
66813
66991
|
if (url.pathname === "/api/stats") {
|
|
66814
66992
|
const now = Date.now();
|
|
66815
66993
|
const windowMs = Number(url.searchParams.get("window") ?? 3600000);
|
|
66816
66994
|
const buckets = Number(url.searchParams.get("buckets") ?? 60);
|
|
66817
66995
|
const events = db.query({ since: now - windowMs, limit: 5000 });
|
|
66818
|
-
return
|
|
66996
|
+
return json15(computeStats(events, { now, windowMs, buckets }));
|
|
66819
66997
|
}
|
|
66820
66998
|
return new Response("Not Found", { status: 404 });
|
|
66821
66999
|
}
|
|
@@ -66834,7 +67012,7 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
66834
67012
|
return await dashboardRoute(req, url, srv, configAdmin, transportLabel);
|
|
66835
67013
|
} catch (e) {
|
|
66836
67014
|
logger.error(`Dashboard request failed (${url.pathname}): ${logError(e)}`);
|
|
66837
|
-
return
|
|
67015
|
+
return json15({ error: clientError(e) }, 502);
|
|
66838
67016
|
}
|
|
66839
67017
|
},
|
|
66840
67018
|
websocket: {
|