@usex/mikrotik-mcp 5.8.0 → 5.10.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 +13 -1
- package/dist/cli.js +653 -332
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -1
- package/dist/shared/{library-hsq1a106.js → library-cv5qe3zh.js} +1 -1
- package/dist/shared/{library-srhzw8ym.js → library-kpx18cna.js} +187 -16
- package/dist/ui/aaa.html +5 -2
- package/dist/ui/connected-devices.html +5 -2
- package/dist/ui/dashboard.html +5 -2
- package/dist/ui/fabric.html +123 -0
- package/dist/ui/firewall-audit.html +9 -6
- package/dist/ui/firewall.html +6 -3
- package/dist/ui/interfaces.html +19 -16
- package/dist/ui/investigations.html +123 -0
- package/dist/ui/observability.html +75 -71
- package/dist/ui/operations.html +123 -0
- package/dist/ui/records.html +6 -3
- package/dist/ui/reports.html +123 -0
- package/dist/ui/round-trip.html +123 -0
- package/dist/ui/service-health.html +123 -0
- package/package.json +1 -1
- package/schemas/tool-catalog.json +1 -1
- /package/dist/shared/{cli-ejryxvb3.js → cli-k3m3pxn8.js} +0 -0
package/dist/cli.js
CHANGED
|
@@ -17656,12 +17656,117 @@ function indicatesFailure(text) {
|
|
|
17656
17656
|
return /^Failed\b/.test(firstLine) || /\(line \d+ column \d+\)/.test(text);
|
|
17657
17657
|
}
|
|
17658
17658
|
|
|
17659
|
+
// src/core/report-views.ts
|
|
17660
|
+
var REPORT_VIEW_TOOLS = {
|
|
17661
|
+
investigations: [
|
|
17662
|
+
"create_investigation",
|
|
17663
|
+
"get_investigation",
|
|
17664
|
+
"list_investigations",
|
|
17665
|
+
"inspect_flow_path",
|
|
17666
|
+
"list_client_flow_candidates"
|
|
17667
|
+
],
|
|
17668
|
+
"round-trip": ["trace_round_trip"],
|
|
17669
|
+
"service-health": [
|
|
17670
|
+
"create_service_contract",
|
|
17671
|
+
"list_service_contracts",
|
|
17672
|
+
"list_service_probe_targets",
|
|
17673
|
+
"run_service_contract",
|
|
17674
|
+
"service_contract_history",
|
|
17675
|
+
"audit_service_contracts"
|
|
17676
|
+
],
|
|
17677
|
+
fabric: ["map_l2_fabric", "locate_host_port"],
|
|
17678
|
+
operations: [
|
|
17679
|
+
"plan_changes",
|
|
17680
|
+
"apply_plan",
|
|
17681
|
+
"begin_transaction",
|
|
17682
|
+
"add_transaction_step",
|
|
17683
|
+
"verify_transaction",
|
|
17684
|
+
"commit_transaction",
|
|
17685
|
+
"abort_transaction",
|
|
17686
|
+
"plan_rollout",
|
|
17687
|
+
"rollout_status",
|
|
17688
|
+
"config_check_drift",
|
|
17689
|
+
"diff_config_snapshots",
|
|
17690
|
+
"get_access_scope",
|
|
17691
|
+
"safe_mode_status",
|
|
17692
|
+
"packet_capture_status"
|
|
17693
|
+
],
|
|
17694
|
+
reports: [
|
|
17695
|
+
"diagnose",
|
|
17696
|
+
"trace_path",
|
|
17697
|
+
"correlate_events",
|
|
17698
|
+
"suggest_fix",
|
|
17699
|
+
"run_compliance_audit",
|
|
17700
|
+
"audit_fleet",
|
|
17701
|
+
"audit_known_vulnerabilities",
|
|
17702
|
+
"audit_certificate_expiry",
|
|
17703
|
+
"audit_firewall_hardening",
|
|
17704
|
+
"audit_firewall_default_deny",
|
|
17705
|
+
"audit_address_list_enforcement",
|
|
17706
|
+
"audit_kernel_ip_hardening",
|
|
17707
|
+
"audit_ipv6_firewall_baseline",
|
|
17708
|
+
"audit_ssh_hardening",
|
|
17709
|
+
"audit_ip_service_exposure",
|
|
17710
|
+
"audit_connection_tracking_helpers",
|
|
17711
|
+
"audit_management_plane_exposure",
|
|
17712
|
+
"audit_account_hygiene",
|
|
17713
|
+
"audit_certificate_hygiene",
|
|
17714
|
+
"audit_network_segmentation",
|
|
17715
|
+
"audit_dns_resolver_exposure",
|
|
17716
|
+
"run_security_hardening_audit",
|
|
17717
|
+
"run_capsman_audit",
|
|
17718
|
+
"audit_capsman_coverage",
|
|
17719
|
+
"report_weak_signal_clients",
|
|
17720
|
+
"audit_capsman_load",
|
|
17721
|
+
"audit_capsman_ft",
|
|
17722
|
+
"audit_capsman_ha",
|
|
17723
|
+
"simulate_packet",
|
|
17724
|
+
"simulate_change",
|
|
17725
|
+
"simulate_suite",
|
|
17726
|
+
"explain_rule_reachability",
|
|
17727
|
+
"run_policy_check",
|
|
17728
|
+
"check_policy_snapshot",
|
|
17729
|
+
"explain_policy_finding",
|
|
17730
|
+
"explain_device",
|
|
17731
|
+
"explain_section",
|
|
17732
|
+
"diff_explanations",
|
|
17733
|
+
"flow_top_talkers",
|
|
17734
|
+
"analyze_flows",
|
|
17735
|
+
"forecast_link_saturation"
|
|
17736
|
+
]
|
|
17737
|
+
};
|
|
17738
|
+
var REPORT_VIEW_META = "mikrotik/reportView";
|
|
17739
|
+
var byTool = new Map(Object.entries(REPORT_VIEW_TOOLS).flatMap(([kind, names]) => names.map((name) => [name, kind])));
|
|
17740
|
+
function reportViewForTool(name) {
|
|
17741
|
+
return byTool.get(name);
|
|
17742
|
+
}
|
|
17743
|
+
function buildReportView(kind, tool, title, device, raw, structured) {
|
|
17744
|
+
let data = structured;
|
|
17745
|
+
if (data === undefined) {
|
|
17746
|
+
try {
|
|
17747
|
+
data = JSON.parse(raw);
|
|
17748
|
+
} catch {
|
|
17749
|
+
data = null;
|
|
17750
|
+
}
|
|
17751
|
+
}
|
|
17752
|
+
return {
|
|
17753
|
+
__mikrotikView: "report",
|
|
17754
|
+
kind,
|
|
17755
|
+
tool,
|
|
17756
|
+
title,
|
|
17757
|
+
device: device ?? "default",
|
|
17758
|
+
raw,
|
|
17759
|
+
data,
|
|
17760
|
+
generatedAt: new Date().toISOString()
|
|
17761
|
+
};
|
|
17762
|
+
}
|
|
17763
|
+
|
|
17659
17764
|
// src/core/ui-meta.ts
|
|
17660
17765
|
var UI_META_KEY = "ui";
|
|
17661
17766
|
var UI_RESOURCE_URI_LEGACY_KEY = "ui/resourceUri";
|
|
17662
17767
|
var OPENAI_OUTPUT_TEMPLATE_KEY = "openai/outputTemplate";
|
|
17663
17768
|
function uiViewUri(id) {
|
|
17664
|
-
return `ui://mikrotik/${id}.html`;
|
|
17769
|
+
return `ui://mikrotik/${id}.html?v=2`;
|
|
17665
17770
|
}
|
|
17666
17771
|
function toolUiMeta(ui) {
|
|
17667
17772
|
const meta = {
|
|
@@ -17672,6 +17777,12 @@ function toolUiMeta(ui) {
|
|
|
17672
17777
|
return meta;
|
|
17673
17778
|
}
|
|
17674
17779
|
|
|
17780
|
+
// src/core/tool-pattern.ts
|
|
17781
|
+
function globMatch(pattern, name) {
|
|
17782
|
+
const rx = new RegExp(`^${pattern.toLowerCase().replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`);
|
|
17783
|
+
return rx.test(name.toLowerCase());
|
|
17784
|
+
}
|
|
17785
|
+
|
|
17675
17786
|
// src/core/access.ts
|
|
17676
17787
|
var RISK_ORDER = [
|
|
17677
17788
|
"READ",
|
|
@@ -17687,10 +17798,6 @@ var RISK_RANK = {
|
|
|
17687
17798
|
DESTRUCTIVE: 2,
|
|
17688
17799
|
DANGEROUS: 3
|
|
17689
17800
|
};
|
|
17690
|
-
function globMatch(pattern, name) {
|
|
17691
|
-
const rx = new RegExp(`^${pattern.toLowerCase().replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`);
|
|
17692
|
-
return rx.test(name.toLowerCase());
|
|
17693
|
-
}
|
|
17694
17801
|
function matchesAny(patterns, name) {
|
|
17695
17802
|
return (patterns ?? []).some((p) => globMatch(p, name));
|
|
17696
17803
|
}
|
|
@@ -17720,7 +17827,20 @@ function evaluateAccess(policy, req) {
|
|
|
17720
17827
|
reason: `Tool '${req.tool}' is outside the active access scope${where}. ` + `Allowed: ${s.tools.join(", ")}.`
|
|
17721
17828
|
};
|
|
17722
17829
|
}
|
|
17830
|
+
if ((s.toolAllowGroups ?? []).some((group) => !matchesAny(group, req.tool))) {
|
|
17831
|
+
return {
|
|
17832
|
+
allowed: false,
|
|
17833
|
+
rule: "tool",
|
|
17834
|
+
reason: `Tool '${req.tool}' does not satisfy every configured and session tool allow-list${where}.`
|
|
17835
|
+
};
|
|
17836
|
+
}
|
|
17723
17837
|
if (req.device !== undefined) {
|
|
17838
|
+
if (s.noDevices)
|
|
17839
|
+
return {
|
|
17840
|
+
allowed: false,
|
|
17841
|
+
rule: "device",
|
|
17842
|
+
reason: `No devices remain in the intersection of the configured and session access scopes${where}.`
|
|
17843
|
+
};
|
|
17724
17844
|
if ((s.denyDevices ?? []).some((d) => d.toLowerCase() === req.device.toLowerCase())) {
|
|
17725
17845
|
return {
|
|
17726
17846
|
allowed: false,
|
|
@@ -17759,7 +17879,14 @@ function narrowScope(base, requested) {
|
|
|
17759
17879
|
return b.filter((x) => lower.has(x.toLowerCase()));
|
|
17760
17880
|
};
|
|
17761
17881
|
out.devices = intersect(base.devices, requested.devices);
|
|
17882
|
+
if (base.noDevices || requested.noDevices || base.devices?.length && requested.devices?.length && !out.devices?.length)
|
|
17883
|
+
out.noDevices = true;
|
|
17762
17884
|
out.tools = requested.tools && requested.tools.length > 0 ? requested.tools : base.tools;
|
|
17885
|
+
const groups = [...base.toolAllowGroups ?? [], ...requested.toolAllowGroups ?? []];
|
|
17886
|
+
if (base.tools?.length && requested.tools?.length)
|
|
17887
|
+
groups.push(base.tools);
|
|
17888
|
+
if (groups.length)
|
|
17889
|
+
out.toolAllowGroups = [...new Map(groups.map((g) => [JSON.stringify(g), g])).values()];
|
|
17763
17890
|
out.denyDevices = [...new Set([...base.denyDevices ?? [], ...requested.denyDevices ?? []])];
|
|
17764
17891
|
out.denyTools = [...new Set([...base.denyTools ?? [], ...requested.denyTools ?? []])];
|
|
17765
17892
|
if (out.denyDevices.length === 0)
|
|
@@ -17794,14 +17921,20 @@ function adoptConfiguredPolicy() {
|
|
|
17794
17921
|
}
|
|
17795
17922
|
onConfigChanged(adoptConfiguredPolicy);
|
|
17796
17923
|
adoptConfiguredPolicy();
|
|
17797
|
-
function
|
|
17924
|
+
function previewAccessPolicy(policy) {
|
|
17798
17925
|
if (!sessionNarrowing)
|
|
17799
|
-
return
|
|
17926
|
+
return policy;
|
|
17800
17927
|
return {
|
|
17801
17928
|
enabled: true,
|
|
17802
|
-
scope: narrowScope(
|
|
17929
|
+
scope: narrowScope(policy.scope, sessionNarrowing)
|
|
17803
17930
|
};
|
|
17804
17931
|
}
|
|
17932
|
+
function getAccessPolicy() {
|
|
17933
|
+
return previewAccessPolicy(basePolicy);
|
|
17934
|
+
}
|
|
17935
|
+
function hasSessionNarrowing() {
|
|
17936
|
+
return sessionNarrowing !== undefined;
|
|
17937
|
+
}
|
|
17805
17938
|
function narrowSession(requested) {
|
|
17806
17939
|
sessionNarrowing = sessionNarrowing ? narrowScope(sessionNarrowing, requested) : narrowScope(basePolicy.scope, requested);
|
|
17807
17940
|
return getAccessPolicy().scope;
|
|
@@ -19390,6 +19523,9 @@ var UI_OUTPUT_SCHEMA = object({}).passthrough();
|
|
|
19390
19523
|
function effectiveUi(def) {
|
|
19391
19524
|
if (def.ui)
|
|
19392
19525
|
return { ui: def.ui, auto: false };
|
|
19526
|
+
const report = reportViewForTool(def.name);
|
|
19527
|
+
if (report)
|
|
19528
|
+
return { ui: { resourceUri: uiViewUri(report), visibility: ["model"] }, auto: true };
|
|
19393
19529
|
if (def.annotations.readOnlyHint === true && AUTO_RECORDS_VERB.test(def.name)) {
|
|
19394
19530
|
return { ui: { resourceUri: uiViewUri("records"), visibility: ["model", "app"] }, auto: true };
|
|
19395
19531
|
}
|
|
@@ -19470,6 +19606,7 @@ function defineTool(def) {
|
|
|
19470
19606
|
description: def.description,
|
|
19471
19607
|
annotations: def.annotations,
|
|
19472
19608
|
inputSchema: def.inputSchema,
|
|
19609
|
+
noDevice: def.noDevice,
|
|
19473
19610
|
ui: def.ui,
|
|
19474
19611
|
requires: def.requires,
|
|
19475
19612
|
handler: def.handler,
|
|
@@ -19568,10 +19705,9 @@ function defineTool(def) {
|
|
|
19568
19705
|
return { content: [{ type: "text", text: out.text }], isError: true };
|
|
19569
19706
|
}
|
|
19570
19707
|
if (auto && !out.structuredContent) {
|
|
19571
|
-
const
|
|
19572
|
-
|
|
19573
|
-
|
|
19574
|
-
}
|
|
19708
|
+
const report = reportViewForTool(def.name);
|
|
19709
|
+
const view = report ? buildReportView(report, def.name, def.title, deviceName, out.text) : buildRecordsView(def.name, def.title, out.text, new Date().toISOString());
|
|
19710
|
+
out.structuredContent = view;
|
|
19575
19711
|
}
|
|
19576
19712
|
const result = {
|
|
19577
19713
|
content: [{ type: "text", text: out.text }]
|
|
@@ -19580,6 +19716,12 @@ function defineTool(def) {
|
|
|
19580
19716
|
result.structuredContent = out.structuredContent;
|
|
19581
19717
|
hasStructured = true;
|
|
19582
19718
|
}
|
|
19719
|
+
const report = auto && reportViewForTool(def.name);
|
|
19720
|
+
if (report && out.structuredContent?.__mikrotikView !== "report") {
|
|
19721
|
+
result._meta = {
|
|
19722
|
+
[REPORT_VIEW_META]: buildReportView(report, def.name, def.title, deviceName, out.text, out.structuredContent)
|
|
19723
|
+
};
|
|
19724
|
+
}
|
|
19583
19725
|
if (deviceStamp)
|
|
19584
19726
|
result.content.push({ type: "text", text: deviceStamp });
|
|
19585
19727
|
return result;
|
|
@@ -19637,7 +19779,7 @@ function defineTool(def) {
|
|
|
19637
19779
|
inputSchema,
|
|
19638
19780
|
annotations: { ...def.annotations, title: def.title },
|
|
19639
19781
|
...ui ? { _meta: toolUiMeta(ui) } : {},
|
|
19640
|
-
...ui
|
|
19782
|
+
...ui ? { outputSchema: UI_OUTPUT_SCHEMA } : {}
|
|
19641
19783
|
}, callback);
|
|
19642
19784
|
}
|
|
19643
19785
|
};
|
|
@@ -24526,6 +24668,36 @@ var UI_VIEWS = [
|
|
|
24526
24668
|
id: "aaa",
|
|
24527
24669
|
name: "MikroTik RADIUS & User Manager",
|
|
24528
24670
|
description: "Full RADIUS client + built-in User Manager RADIUS server management: servers, users, " + "profiles, limitations, NAS clients, assignments, sessions and settings, with add/edit/remove."
|
|
24671
|
+
},
|
|
24672
|
+
{
|
|
24673
|
+
id: "investigations",
|
|
24674
|
+
name: "MikroTik Client Investigations",
|
|
24675
|
+
description: "Client attachment, router-scoped evidence, exported flow interfaces and unresolved visibility gaps."
|
|
24676
|
+
},
|
|
24677
|
+
{
|
|
24678
|
+
id: "round-trip",
|
|
24679
|
+
name: "MikroTik Round-trip Lab",
|
|
24680
|
+
description: "Forward and return snapshot paths with named interfaces, modelled boundaries and unknown hops. Not live packet proof."
|
|
24681
|
+
},
|
|
24682
|
+
{
|
|
24683
|
+
id: "service-health",
|
|
24684
|
+
name: "MikroTik Service Health",
|
|
24685
|
+
description: "Approved endpoint checks, contract definitions and historical PASS/FAIL/UNKNOWN evidence from the MCP host."
|
|
24686
|
+
},
|
|
24687
|
+
{
|
|
24688
|
+
id: "fabric",
|
|
24689
|
+
name: "MikroTik Layer-2 Fabric",
|
|
24690
|
+
description: "Bridge-port occupancy, attached clients and inferred access/uplink roles."
|
|
24691
|
+
},
|
|
24692
|
+
{
|
|
24693
|
+
id: "operations",
|
|
24694
|
+
name: "MikroTik Change Operations",
|
|
24695
|
+
description: "Read-only presentation of plans, transaction results, rollout progress, scope and drift. No apply/commit controls."
|
|
24696
|
+
},
|
|
24697
|
+
{
|
|
24698
|
+
id: "reports",
|
|
24699
|
+
name: "MikroTik Evidence Reports",
|
|
24700
|
+
description: "Readable diagnostic, compliance, security, simulation and traffic reports with searchable evidence and JSON export."
|
|
24529
24701
|
}
|
|
24530
24702
|
];
|
|
24531
24703
|
function placeholderHtml(view) {
|
|
@@ -30039,7 +30211,7 @@ var cache2 = null;
|
|
|
30039
30211
|
async function gateway() {
|
|
30040
30212
|
if (cache2)
|
|
30041
30213
|
return cache2;
|
|
30042
|
-
const { moduleCatalog } = await import("./shared/cli-
|
|
30214
|
+
const { moduleCatalog } = await import("./shared/cli-k3m3pxn8.js");
|
|
30043
30215
|
const forIndex = [];
|
|
30044
30216
|
const byName = new Map;
|
|
30045
30217
|
for (const mod of moduleCatalog) {
|
|
@@ -41454,7 +41626,7 @@ ${DATASET_NOTE}`;
|
|
|
41454
41626
|
function renderScope(scope) {
|
|
41455
41627
|
const lines = [];
|
|
41456
41628
|
lines.push(` max risk : ${scope.maxRisk ?? "(no ceiling)"}`);
|
|
41457
|
-
lines.push(` devices : ${scope.devices && scope.devices.length > 0 ? scope.devices.join(", ") : "(all configured)"}`);
|
|
41629
|
+
lines.push(` devices : ${scope.noDevices ? "(none: scopes do not overlap)" : scope.devices && scope.devices.length > 0 ? scope.devices.join(", ") : "(all configured)"}`);
|
|
41458
41630
|
if (scope.denyDevices && scope.denyDevices.length > 0) {
|
|
41459
41631
|
lines.push(` denied devs : ${scope.denyDevices.join(", ")}`);
|
|
41460
41632
|
}
|
|
@@ -41462,6 +41634,8 @@ function renderScope(scope) {
|
|
|
41462
41634
|
if (scope.denyTools && scope.denyTools.length > 0) {
|
|
41463
41635
|
lines.push(` denied tools: ${scope.denyTools.join(", ")}`);
|
|
41464
41636
|
}
|
|
41637
|
+
for (const group of scope.toolAllowGroups ?? [])
|
|
41638
|
+
lines.push(` also match : ${group.join(", ")}`);
|
|
41465
41639
|
lines.push(` expires : ${scope.expiresAt ? new Date(scope.expiresAt).toISOString() : "(never)"}`);
|
|
41466
41640
|
if (scope.label)
|
|
41467
41641
|
lines.push(` label : ${scope.label}`);
|
|
@@ -52263,7 +52437,7 @@ var riskByTool = null;
|
|
|
52263
52437
|
async function riskIndex() {
|
|
52264
52438
|
if (riskByTool)
|
|
52265
52439
|
return riskByTool;
|
|
52266
|
-
const { moduleCatalog } = await import("./shared/cli-
|
|
52440
|
+
const { moduleCatalog } = await import("./shared/cli-k3m3pxn8.js");
|
|
52267
52441
|
const index = new Map;
|
|
52268
52442
|
for (const mod of moduleCatalog) {
|
|
52269
52443
|
for (const tool of mod.tools)
|
|
@@ -64129,9 +64303,170 @@ async function postureRoutes(req, url) {
|
|
|
64129
64303
|
return null;
|
|
64130
64304
|
}
|
|
64131
64305
|
|
|
64306
|
+
// src/observability/access-settings.ts
|
|
64307
|
+
import { createHash as createHash4 } from "crypto";
|
|
64308
|
+
var list2 = array(string2().trim().min(1).max(200)).max(200);
|
|
64309
|
+
var accessSchema = AccessConfigSchema.extend({
|
|
64310
|
+
devices: list2,
|
|
64311
|
+
denyDevices: list2,
|
|
64312
|
+
tools: list2,
|
|
64313
|
+
denyTools: list2,
|
|
64314
|
+
label: string2().trim().max(200).optional()
|
|
64315
|
+
}).strict();
|
|
64316
|
+
var editSchema = object({
|
|
64317
|
+
revision: string2().min(1),
|
|
64318
|
+
access: accessSchema,
|
|
64319
|
+
device: string2().optional(),
|
|
64320
|
+
tool: string2().optional()
|
|
64321
|
+
}).strict();
|
|
64322
|
+
var idSchema = object({ pendingId: string2().min(1) }).strict();
|
|
64323
|
+
var json4 = (body, status = 200) => Response.json(body, { status });
|
|
64324
|
+
var policyFor = ({ enabled, ...scope }) => ({ enabled, scope });
|
|
64325
|
+
function createAccessSettingsRoutes(admin, deps = {
|
|
64326
|
+
getConfig,
|
|
64327
|
+
getConfigSource,
|
|
64328
|
+
getAccessPolicy,
|
|
64329
|
+
hasSessionNarrowing,
|
|
64330
|
+
previewAccessPolicy,
|
|
64331
|
+
recentDenials,
|
|
64332
|
+
recordVersion,
|
|
64333
|
+
now: Date.now
|
|
64334
|
+
}) {
|
|
64335
|
+
let pending = null;
|
|
64336
|
+
const tools = moduleCatalog2.flatMap((m) => m.tools.map((t) => ({ name: t.name, risk: riskOf(t.annotations), noDevice: !!t.noDevice })));
|
|
64337
|
+
const revision = () => createHash4("sha256").update(JSON.stringify([deps.getConfig(), deps.getAccessPolicy()])).digest("hex");
|
|
64338
|
+
const payload = () => {
|
|
64339
|
+
const cfg = deps.getConfig();
|
|
64340
|
+
const id = admin.pendingId();
|
|
64341
|
+
return {
|
|
64342
|
+
revision: revision(),
|
|
64343
|
+
configured: cfg.access,
|
|
64344
|
+
effective: deps.getAccessPolicy(),
|
|
64345
|
+
narrowed: deps.hasSessionNarrowing(),
|
|
64346
|
+
readOnly: cfg.readOnly,
|
|
64347
|
+
devices: Object.entries(cfg.devices).map(([name, d]) => ({ name, disabled: !!d.disabled })),
|
|
64348
|
+
tools,
|
|
64349
|
+
denials: deps.recentDenials(50),
|
|
64350
|
+
fromFile: deps.getConfigSource().fromFile,
|
|
64351
|
+
pending: id ? {
|
|
64352
|
+
id,
|
|
64353
|
+
owned: id === pending?.id,
|
|
64354
|
+
expiresAt: id === pending?.id ? pending.expiresAt : undefined
|
|
64355
|
+
} : null
|
|
64356
|
+
};
|
|
64357
|
+
};
|
|
64358
|
+
return async (req, url) => {
|
|
64359
|
+
const path = url.pathname;
|
|
64360
|
+
if (!path.startsWith("/api/access/settings"))
|
|
64361
|
+
return null;
|
|
64362
|
+
if (path === "/api/access/settings" && req.method === "GET")
|
|
64363
|
+
return json4(payload());
|
|
64364
|
+
if (!["preview", "apply", "keep", "rollback"].some((s) => path === `/api/access/settings/${s}`))
|
|
64365
|
+
return json4({ error: "Not found" }, 404);
|
|
64366
|
+
if (req.method !== "POST")
|
|
64367
|
+
return json4({ error: "Use POST" }, 405);
|
|
64368
|
+
if (req.headers.get("sec-fetch-site") === "cross-site")
|
|
64369
|
+
return json4({ error: "Cross-site access changes are not allowed" }, 403);
|
|
64370
|
+
if (req.headers.get("content-type")?.split(";")[0].trim() !== "application/json")
|
|
64371
|
+
return json4({ error: "Expected application/json" }, 415);
|
|
64372
|
+
let body;
|
|
64373
|
+
try {
|
|
64374
|
+
const text = await req.text();
|
|
64375
|
+
if (text.length > 65536)
|
|
64376
|
+
return json4({ error: "Access settings are too large" }, 413);
|
|
64377
|
+
body = JSON.parse(text);
|
|
64378
|
+
} catch {
|
|
64379
|
+
return json4({ error: "Invalid JSON" }, 400);
|
|
64380
|
+
}
|
|
64381
|
+
if (path.endsWith("/keep") || path.endsWith("/rollback")) {
|
|
64382
|
+
const parsed = idSchema.safeParse(body);
|
|
64383
|
+
if (!parsed.success)
|
|
64384
|
+
return json4({ error: "A pending change ID is required" }, 400);
|
|
64385
|
+
const id = parsed.data.pendingId;
|
|
64386
|
+
if (pending?.id !== id || admin.pendingId() !== id)
|
|
64387
|
+
return json4({ error: "This change expired or was replaced. Refresh the current policy." }, 409);
|
|
64388
|
+
const keep = path.endsWith("/keep");
|
|
64389
|
+
const ok = keep ? admin.keepConfig(id) : admin.rollback(id);
|
|
64390
|
+
if (!ok)
|
|
64391
|
+
return json4({ error: "This change is no longer pending" }, 409);
|
|
64392
|
+
pending = null;
|
|
64393
|
+
if (keep)
|
|
64394
|
+
deps.recordVersion(deps.getConfig(), "auto", deps.now(), "access scope updated");
|
|
64395
|
+
return json4({ ok: true, settings: payload() });
|
|
64396
|
+
}
|
|
64397
|
+
const parsed = editSchema.safeParse(body);
|
|
64398
|
+
if (!parsed.success)
|
|
64399
|
+
return json4({ error: parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ") }, 400);
|
|
64400
|
+
const { access, tool } = parsed.data;
|
|
64401
|
+
if (parsed.data.revision !== revision())
|
|
64402
|
+
return json4({ error: "Configuration changed since you opened this editor. Refresh before applying." }, 409);
|
|
64403
|
+
const cfg = deps.getConfig();
|
|
64404
|
+
const device = parsed.data.device ? Object.keys(cfg.devices).find((key) => key.toLowerCase() === parsed.data.device.toLowerCase()) : cfg.defaultDevice;
|
|
64405
|
+
const unknownDevice = [
|
|
64406
|
+
...access.devices,
|
|
64407
|
+
...access.denyDevices,
|
|
64408
|
+
...parsed.data.device ? [parsed.data.device] : []
|
|
64409
|
+
].find((name) => !Object.keys(cfg.devices).some((key) => key.toLowerCase() === name.toLowerCase()));
|
|
64410
|
+
if (unknownDevice)
|
|
64411
|
+
return json4({ error: `Unknown device: ${unknownDevice}` }, 400);
|
|
64412
|
+
if (tool && !tools.some((t) => t.name === tool))
|
|
64413
|
+
return json4({ error: `Unknown tool: ${tool}` }, 400);
|
|
64414
|
+
if (path.endsWith("/preview")) {
|
|
64415
|
+
const effective = deps.previewAccessPolicy(policyFor(access));
|
|
64416
|
+
const current = deps.getAccessPolicy();
|
|
64417
|
+
const now = deps.now();
|
|
64418
|
+
const decide = (policy, t) => {
|
|
64419
|
+
if (cfg.readOnly && t.risk !== "READ")
|
|
64420
|
+
return { allowed: false, reason: "Server-wide read-only mode prevents writes." };
|
|
64421
|
+
if (!t.noDevice && (!device || !cfg.devices[device]))
|
|
64422
|
+
return {
|
|
64423
|
+
allowed: false,
|
|
64424
|
+
rule: "device",
|
|
64425
|
+
reason: "No configured target router is available."
|
|
64426
|
+
};
|
|
64427
|
+
if (!t.noDevice && device && cfg.devices[device]?.disabled)
|
|
64428
|
+
return { allowed: false, rule: "device", reason: "This configured device is disabled." };
|
|
64429
|
+
return evaluateAccess(policy, {
|
|
64430
|
+
tool: t.name,
|
|
64431
|
+
risk: t.risk,
|
|
64432
|
+
device: t.noDevice ? undefined : device,
|
|
64433
|
+
now
|
|
64434
|
+
});
|
|
64435
|
+
};
|
|
64436
|
+
const result = {
|
|
64437
|
+
effective,
|
|
64438
|
+
allowed: 0,
|
|
64439
|
+
blocked: 0,
|
|
64440
|
+
newlyAllowed: 0,
|
|
64441
|
+
newlyBlocked: 0,
|
|
64442
|
+
device
|
|
64443
|
+
};
|
|
64444
|
+
for (const t of tools) {
|
|
64445
|
+
const next = decide(effective, t);
|
|
64446
|
+
const prev = decide(current, t);
|
|
64447
|
+
result[next.allowed ? "allowed" : "blocked"]++;
|
|
64448
|
+
if (next.allowed && !prev.allowed)
|
|
64449
|
+
result.newlyAllowed++;
|
|
64450
|
+
if (!next.allowed && prev.allowed)
|
|
64451
|
+
result.newlyBlocked++;
|
|
64452
|
+
if (tool === t.name)
|
|
64453
|
+
result.check = next;
|
|
64454
|
+
}
|
|
64455
|
+
return json4({ ok: true, preview: result });
|
|
64456
|
+
}
|
|
64457
|
+
if (admin.pendingId())
|
|
64458
|
+
return json4({
|
|
64459
|
+
error: "Another configuration change is awaiting confirmation. Keep or revert it first."
|
|
64460
|
+
}, 409);
|
|
64461
|
+
const res = admin.applyConfig({ ...cfg, access }, 60000);
|
|
64462
|
+
pending = { id: res.pendingId, expiresAt: deps.now() + res.rollbackMs };
|
|
64463
|
+
return json4({ ok: true, settings: payload() });
|
|
64464
|
+
};
|
|
64465
|
+
}
|
|
64466
|
+
|
|
64132
64467
|
// src/observability/txn-routes.ts
|
|
64133
64468
|
var JSON_HEADERS3 = { "content-type": "application/json; charset=utf-8" };
|
|
64134
|
-
function
|
|
64469
|
+
function json5(body, status = 200) {
|
|
64135
64470
|
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS3 });
|
|
64136
64471
|
}
|
|
64137
64472
|
function liveRecord(id) {
|
|
@@ -64158,16 +64493,16 @@ async function txnRoutes(req, url) {
|
|
|
64158
64493
|
for (const r of live)
|
|
64159
64494
|
byId.set(r.id, r);
|
|
64160
64495
|
const transactions = [...byId.values()].sort((a, b) => b.ts - a.ts).slice(0, limit);
|
|
64161
|
-
return
|
|
64496
|
+
return json5({ transactions, live: liveTxnUpdates(), error: storeError });
|
|
64162
64497
|
}
|
|
64163
64498
|
const parts = p.slice("/api/txn/".length).split("/").filter(Boolean);
|
|
64164
64499
|
if (parts.length === 0 || parts.length > 2)
|
|
64165
|
-
return
|
|
64500
|
+
return json5({ error: "not found" }, 404);
|
|
64166
64501
|
const id = decodeURIComponent(parts[0]);
|
|
64167
64502
|
if (parts[1] === "abort" && req.method === "POST") {
|
|
64168
64503
|
const entry = getTxn(id);
|
|
64169
64504
|
if (!entry)
|
|
64170
|
-
return
|
|
64505
|
+
return json5({ error: `no open transaction '${id}'` }, 404);
|
|
64171
64506
|
entry.txn = requestAbort(entry.txn, "aborted from the dashboard");
|
|
64172
64507
|
const run = await runTransaction({
|
|
64173
64508
|
txn: entry.txn,
|
|
@@ -64178,7 +64513,7 @@ async function txnRoutes(req, url) {
|
|
|
64178
64513
|
await persistTxn(entry);
|
|
64179
64514
|
if (run.state !== undefined)
|
|
64180
64515
|
dropTxn(id);
|
|
64181
|
-
return
|
|
64516
|
+
return json5({
|
|
64182
64517
|
state: run.state,
|
|
64183
64518
|
summary: run.summary,
|
|
64184
64519
|
transaction: toRecord2(run.txn, entry.ts)
|
|
@@ -64187,15 +64522,15 @@ async function txnRoutes(req, url) {
|
|
|
64187
64522
|
if (parts.length === 1 && req.method === "GET") {
|
|
64188
64523
|
const record = liveRecord(id) ?? store?.get(id) ?? null;
|
|
64189
64524
|
if (!record)
|
|
64190
|
-
return
|
|
64191
|
-
return
|
|
64525
|
+
return json5({ error: `unknown transaction '${id}'` }, 404);
|
|
64526
|
+
return json5({ transaction: record, events: store?.events(id) ?? [] });
|
|
64192
64527
|
}
|
|
64193
|
-
return
|
|
64528
|
+
return json5({ error: "method not allowed" }, 405);
|
|
64194
64529
|
}
|
|
64195
64530
|
|
|
64196
64531
|
// src/observability/flow-routes.ts
|
|
64197
64532
|
var JSON_HEADERS4 = { "content-type": "application/json; charset=utf-8" };
|
|
64198
|
-
function
|
|
64533
|
+
function json6(body, status = 200) {
|
|
64199
64534
|
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS4 });
|
|
64200
64535
|
}
|
|
64201
64536
|
var WINDOWS2 = {
|
|
@@ -64216,14 +64551,14 @@ async function flowRoutes(req, url) {
|
|
|
64216
64551
|
if (!p.startsWith("/api/flows"))
|
|
64217
64552
|
return null;
|
|
64218
64553
|
if (req.method !== "GET")
|
|
64219
|
-
return
|
|
64554
|
+
return json6({ error: "method not allowed" }, 405);
|
|
64220
64555
|
const collector = getFlowCollector();
|
|
64221
64556
|
if (p === "/api/flows/health") {
|
|
64222
64557
|
let store = null;
|
|
64223
64558
|
try {
|
|
64224
64559
|
store = (await flowStore()).stats();
|
|
64225
64560
|
} catch {}
|
|
64226
|
-
return
|
|
64561
|
+
return json6({ collector: collector.stats(), store });
|
|
64227
64562
|
}
|
|
64228
64563
|
collector.drain();
|
|
64229
64564
|
let records;
|
|
@@ -64234,12 +64569,12 @@ async function flowRoutes(req, url) {
|
|
|
64234
64569
|
try {
|
|
64235
64570
|
records = (await flowStore()).query({ from, to, address, limit: 200000 });
|
|
64236
64571
|
} catch (e) {
|
|
64237
|
-
return
|
|
64572
|
+
return json6({ error: e instanceof Error ? e.message : String(e), records: [] }, 200);
|
|
64238
64573
|
}
|
|
64239
64574
|
if (p === "/api/flows/top") {
|
|
64240
64575
|
const dimension = url.searchParams.get("dimension") ?? "source";
|
|
64241
64576
|
const limit = Number(url.searchParams.get("limit") ?? 10);
|
|
64242
|
-
return
|
|
64577
|
+
return json6({
|
|
64243
64578
|
window: { from, to },
|
|
64244
64579
|
totals: summarize2(records),
|
|
64245
64580
|
top: topTalkers(records, dimension, limit, true),
|
|
@@ -64249,7 +64584,7 @@ async function flowRoutes(req, url) {
|
|
|
64249
64584
|
}
|
|
64250
64585
|
if (p === "/api/flows/conversations") {
|
|
64251
64586
|
const limit = Number(url.searchParams.get("limit") ?? 20);
|
|
64252
|
-
return
|
|
64587
|
+
return json6({
|
|
64253
64588
|
window: { from, to },
|
|
64254
64589
|
conversations: conversations(records, limit)
|
|
64255
64590
|
});
|
|
@@ -64258,19 +64593,19 @@ async function flowRoutes(req, url) {
|
|
|
64258
64593
|
const dimension = url.searchParams.get("dimension") ?? "source";
|
|
64259
64594
|
const topN = Number(url.searchParams.get("topN") ?? 5);
|
|
64260
64595
|
const bucket = bucketFor2(span);
|
|
64261
|
-
return
|
|
64596
|
+
return json6({
|
|
64262
64597
|
window: { from, to },
|
|
64263
64598
|
bucketMs: bucket,
|
|
64264
64599
|
keys: topTalkers(records, dimension, topN).map((t) => t.key),
|
|
64265
64600
|
buckets: timeline(records, from, to, bucket, dimension, topN)
|
|
64266
64601
|
});
|
|
64267
64602
|
}
|
|
64268
|
-
return
|
|
64603
|
+
return json6({ error: "not found" }, 404);
|
|
64269
64604
|
}
|
|
64270
64605
|
|
|
64271
64606
|
// src/observability/rollout-routes.ts
|
|
64272
64607
|
var JSON_HEADERS5 = { "content-type": "application/json; charset=utf-8" };
|
|
64273
|
-
function
|
|
64608
|
+
function json7(body, status = 200) {
|
|
64274
64609
|
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS5 });
|
|
64275
64610
|
}
|
|
64276
64611
|
function liveRecord2(id) {
|
|
@@ -64300,17 +64635,17 @@ async function rolloutRoutes(req, url) {
|
|
|
64300
64635
|
for (const r of live)
|
|
64301
64636
|
byId.set(r.id, r);
|
|
64302
64637
|
const rollouts = [...byId.values()].sort((a, b) => b.ts - a.ts).slice(0, limit);
|
|
64303
|
-
return
|
|
64638
|
+
return json7({ rollouts, live: liveRolloutUpdates(), error: storeError });
|
|
64304
64639
|
}
|
|
64305
64640
|
const parts = p.slice("/api/rollout/".length).split("/").filter(Boolean);
|
|
64306
64641
|
if (parts.length === 0 || parts.length > 2)
|
|
64307
|
-
return
|
|
64642
|
+
return json7({ error: "not found" }, 404);
|
|
64308
64643
|
const id = decodeURIComponent(parts[0]);
|
|
64309
64644
|
const verb = parts[1];
|
|
64310
64645
|
if (verb && req.method === "POST") {
|
|
64311
64646
|
const entry = getRollout(id);
|
|
64312
64647
|
if (!entry)
|
|
64313
|
-
return
|
|
64648
|
+
return json7({ error: `no active rollout '${id}'` }, 404);
|
|
64314
64649
|
switch (verb) {
|
|
64315
64650
|
case "hold": {
|
|
64316
64651
|
entry.state = requestHold(entry.state);
|
|
@@ -64325,7 +64660,7 @@ async function rolloutRoutes(req, url) {
|
|
|
64325
64660
|
devices: entry.state.devices,
|
|
64326
64661
|
gates: entry.state.gates
|
|
64327
64662
|
});
|
|
64328
|
-
return
|
|
64663
|
+
return json7({ held: true, summary: summarize5(entry.state) });
|
|
64329
64664
|
}
|
|
64330
64665
|
case "resume": {
|
|
64331
64666
|
entry.state = resume(entry.state);
|
|
@@ -64353,7 +64688,7 @@ async function rolloutRoutes(req, url) {
|
|
|
64353
64688
|
await persistRollout(entry);
|
|
64354
64689
|
if (run.outcome)
|
|
64355
64690
|
dropRollout(id);
|
|
64356
|
-
return
|
|
64691
|
+
return json7({ outcome: run.outcome, summary: run.summary });
|
|
64357
64692
|
}
|
|
64358
64693
|
case "abort": {
|
|
64359
64694
|
entry.state = requestAbort2(entry.state, "aborted from the dashboard");
|
|
@@ -64366,19 +64701,19 @@ async function rolloutRoutes(req, url) {
|
|
|
64366
64701
|
await persistRollout(entry);
|
|
64367
64702
|
if (run.outcome)
|
|
64368
64703
|
dropRollout(id);
|
|
64369
|
-
return
|
|
64704
|
+
return json7({ outcome: run.outcome, summary: run.summary });
|
|
64370
64705
|
}
|
|
64371
64706
|
default:
|
|
64372
|
-
return
|
|
64707
|
+
return json7({ error: `unknown action '${verb}'` }, 404);
|
|
64373
64708
|
}
|
|
64374
64709
|
}
|
|
64375
64710
|
if (parts.length === 1 && req.method === "GET") {
|
|
64376
64711
|
const record = liveRecord2(id) ?? store?.get(id) ?? null;
|
|
64377
64712
|
if (!record)
|
|
64378
|
-
return
|
|
64379
|
-
return
|
|
64713
|
+
return json7({ error: `unknown rollout '${id}'` }, 404);
|
|
64714
|
+
return json7({ rollout: record, events: store?.events(id) ?? [] });
|
|
64380
64715
|
}
|
|
64381
|
-
return
|
|
64716
|
+
return json7({ error: "method not allowed" }, 405);
|
|
64382
64717
|
}
|
|
64383
64718
|
|
|
64384
64719
|
// src/policy/results.ts
|
|
@@ -64468,7 +64803,7 @@ async function policyResults(device, limit = 50) {
|
|
|
64468
64803
|
|
|
64469
64804
|
// src/observability/policy-routes.ts
|
|
64470
64805
|
var JSON_HEADERS6 = { "content-type": "application/json; charset=utf-8" };
|
|
64471
|
-
function
|
|
64806
|
+
function json8(body, status = 200) {
|
|
64472
64807
|
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS6 });
|
|
64473
64808
|
}
|
|
64474
64809
|
async function bodyJson2(req) {
|
|
@@ -64480,7 +64815,7 @@ async function policyRoutes(req, url) {
|
|
|
64480
64815
|
return null;
|
|
64481
64816
|
if (p === "/api/policies" && req.method === "GET") {
|
|
64482
64817
|
const set = currentPolicySet();
|
|
64483
|
-
return
|
|
64818
|
+
return json8({
|
|
64484
64819
|
files: set.files.map((f) => ({
|
|
64485
64820
|
path: f.path,
|
|
64486
64821
|
name: f.name,
|
|
@@ -64501,21 +64836,21 @@ async function policyRoutes(req, url) {
|
|
|
64501
64836
|
try {
|
|
64502
64837
|
results = await policyResults(device, limit);
|
|
64503
64838
|
} catch (e) {
|
|
64504
|
-
return
|
|
64839
|
+
return json8({ results: [], error: e instanceof Error ? e.message : String(e) });
|
|
64505
64840
|
}
|
|
64506
|
-
return
|
|
64841
|
+
return json8({ results });
|
|
64507
64842
|
}
|
|
64508
64843
|
if (p === "/api/policies/validate" && req.method === "POST") {
|
|
64509
64844
|
const body = await bodyJson2(req);
|
|
64510
64845
|
if (typeof body.content !== "string")
|
|
64511
|
-
return
|
|
64512
|
-
return
|
|
64846
|
+
return json8({ error: "content is required" }, 400);
|
|
64847
|
+
return json8(validatePolicyText(body.content));
|
|
64513
64848
|
}
|
|
64514
64849
|
if (p === "/api/policies/run" && req.method === "POST") {
|
|
64515
64850
|
const body = await bodyJson2(req);
|
|
64516
64851
|
const set = currentPolicySet();
|
|
64517
64852
|
if (set.policies.length === 0) {
|
|
64518
|
-
return
|
|
64853
|
+
return json8({ error: "no policy rules are loaded", reports: [] });
|
|
64519
64854
|
}
|
|
64520
64855
|
const cfg = getConfig();
|
|
64521
64856
|
const devices = body.devices && body.devices.length > 0 ? body.devices : Object.entries(cfg.devices).filter(([, d]) => !d.disabled).map(([name]) => name);
|
|
@@ -64544,14 +64879,14 @@ async function policyRoutes(req, url) {
|
|
|
64544
64879
|
reports.push({ device, error: e instanceof Error ? e.message : String(e) });
|
|
64545
64880
|
}
|
|
64546
64881
|
}
|
|
64547
|
-
return
|
|
64882
|
+
return json8({ reports });
|
|
64548
64883
|
}
|
|
64549
|
-
return
|
|
64884
|
+
return json8({ error: "not found" }, 404);
|
|
64550
64885
|
}
|
|
64551
64886
|
|
|
64552
64887
|
// src/observability/sim-routes.ts
|
|
64553
64888
|
var JSON_HEADERS7 = { "content-type": "application/json; charset=utf-8" };
|
|
64554
|
-
function
|
|
64889
|
+
function json9(body, status = 200) {
|
|
64555
64890
|
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS7 });
|
|
64556
64891
|
}
|
|
64557
64892
|
async function bodyJson3(req) {
|
|
@@ -64584,28 +64919,28 @@ async function simRoutes(req, url) {
|
|
|
64584
64919
|
if (!p.startsWith("/api/sim"))
|
|
64585
64920
|
return null;
|
|
64586
64921
|
if (p === "/api/sim/suites" && req.method === "GET") {
|
|
64587
|
-
return
|
|
64922
|
+
return json9({ suites: await listSuites() });
|
|
64588
64923
|
}
|
|
64589
64924
|
if (p === "/api/sim/suites" && req.method === "POST") {
|
|
64590
64925
|
const body = await bodyJson3(req);
|
|
64591
64926
|
if (!body.name || !Array.isArray(body.packets) || body.packets.length === 0) {
|
|
64592
|
-
return
|
|
64927
|
+
return json9({ error: "name and a non-empty packets array are required" }, 400);
|
|
64593
64928
|
}
|
|
64594
64929
|
const saved = await saveSuite({
|
|
64595
64930
|
id: body.id,
|
|
64596
64931
|
name: body.name,
|
|
64597
64932
|
packets: body.packets
|
|
64598
64933
|
});
|
|
64599
|
-
return
|
|
64934
|
+
return json9({ suite: saved });
|
|
64600
64935
|
}
|
|
64601
64936
|
if (p === "/api/sim/packet" && req.method === "POST") {
|
|
64602
64937
|
const body = await bodyJson3(req);
|
|
64603
64938
|
const config = await configFor(body);
|
|
64604
64939
|
if ("error" in config)
|
|
64605
|
-
return
|
|
64940
|
+
return json9(config, 400);
|
|
64606
64941
|
const model = buildModel(config.text);
|
|
64607
64942
|
const result = tracePacket({ model, packet: body.packet });
|
|
64608
|
-
return
|
|
64943
|
+
return json9({
|
|
64609
64944
|
source: config.source,
|
|
64610
64945
|
result,
|
|
64611
64946
|
coverage: {
|
|
@@ -64619,16 +64954,16 @@ async function simRoutes(req, url) {
|
|
|
64619
64954
|
const body = await bodyJson3(req);
|
|
64620
64955
|
const config = await configFor(body);
|
|
64621
64956
|
if ("error" in config)
|
|
64622
|
-
return
|
|
64957
|
+
return json9(config, 400);
|
|
64623
64958
|
if (!body.changes || body.changes.trim() === "") {
|
|
64624
|
-
return
|
|
64959
|
+
return json9({ error: "changes are required" }, 400);
|
|
64625
64960
|
}
|
|
64626
64961
|
const before = tracePacket({ model: buildModel(config.text), packet: body.packet });
|
|
64627
64962
|
const afterModel = buildModel(`${config.text}
|
|
64628
64963
|
${body.changes}
|
|
64629
64964
|
`);
|
|
64630
64965
|
const after = tracePacket({ model: afterModel, packet: body.packet });
|
|
64631
|
-
return
|
|
64966
|
+
return json9({
|
|
64632
64967
|
source: config.source,
|
|
64633
64968
|
before,
|
|
64634
64969
|
after,
|
|
@@ -64643,10 +64978,10 @@ ${body.changes}
|
|
|
64643
64978
|
packet: {}
|
|
64644
64979
|
});
|
|
64645
64980
|
if ("error" in config)
|
|
64646
|
-
return
|
|
64981
|
+
return json9(config, 400);
|
|
64647
64982
|
const model = buildModel(config.text);
|
|
64648
64983
|
const dead = unreachableRules(model.filter);
|
|
64649
|
-
return
|
|
64984
|
+
return json9({
|
|
64650
64985
|
source: config.source,
|
|
64651
64986
|
rules: model.filter.map((r) => ({
|
|
64652
64987
|
chain: r.chain,
|
|
@@ -64667,11 +65002,11 @@ ${body.changes}
|
|
|
64667
65002
|
const id = decodeURIComponent(p.slice("/api/sim/suite/".length, -"/run".length));
|
|
64668
65003
|
const suite = await getSuite(id);
|
|
64669
65004
|
if (!suite)
|
|
64670
|
-
return
|
|
65005
|
+
return json9({ error: `no suite '${id}'` }, 404);
|
|
64671
65006
|
const body = await bodyJson3(req);
|
|
64672
65007
|
const config = await configFor({ ...body, packet: {} });
|
|
64673
65008
|
if ("error" in config)
|
|
64674
|
-
return
|
|
65009
|
+
return json9(config, 400);
|
|
64675
65010
|
const model = buildModel(config.text);
|
|
64676
65011
|
const results = suite.packets.map((entry) => {
|
|
64677
65012
|
const result = tracePacket({ model, packet: entry.packet });
|
|
@@ -64683,7 +65018,7 @@ ${body.changes}
|
|
|
64683
65018
|
summary: result.summary
|
|
64684
65019
|
};
|
|
64685
65020
|
});
|
|
64686
|
-
return
|
|
65021
|
+
return json9({
|
|
64687
65022
|
source: config.source,
|
|
64688
65023
|
suite: { id: suite.id, name: suite.name },
|
|
64689
65024
|
results,
|
|
@@ -64691,12 +65026,12 @@ ${body.changes}
|
|
|
64691
65026
|
total: results.length
|
|
64692
65027
|
});
|
|
64693
65028
|
}
|
|
64694
|
-
return
|
|
65029
|
+
return json9({ error: "not found" }, 404);
|
|
64695
65030
|
}
|
|
64696
65031
|
|
|
64697
65032
|
// src/observability/schedule-routes.ts
|
|
64698
65033
|
var JSON_HEADERS8 = { "content-type": "application/json; charset=utf-8" };
|
|
64699
|
-
function
|
|
65034
|
+
function json10(body, status = 200) {
|
|
64700
65035
|
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS8 });
|
|
64701
65036
|
}
|
|
64702
65037
|
function latestPerDevice2(runs) {
|
|
@@ -64773,12 +65108,12 @@ async function scheduleRoutes(req, url) {
|
|
|
64773
65108
|
try {
|
|
64774
65109
|
store = await scheduleStore();
|
|
64775
65110
|
} catch (e) {
|
|
64776
|
-
return
|
|
65111
|
+
return json10({ error: e instanceof Error ? e.message : String(e), jobs: [] }, 503);
|
|
64777
65112
|
}
|
|
64778
65113
|
const now = Date.now();
|
|
64779
65114
|
if (p === "/api/schedules" && req.method === "GET") {
|
|
64780
65115
|
const jobs = store.listJobs().map((job) => jobRow(job, store.runs(job.id, 500), now));
|
|
64781
|
-
return
|
|
65116
|
+
return json10({
|
|
64782
65117
|
jobs,
|
|
64783
65118
|
schedulable: schedulableTools().map((a) => ({ tool: a.tool, summary: a.summary }))
|
|
64784
65119
|
});
|
|
@@ -64786,20 +65121,20 @@ async function scheduleRoutes(req, url) {
|
|
|
64786
65121
|
if (p === "/api/schedules" && req.method === "POST") {
|
|
64787
65122
|
const body = await req.json().catch(() => null);
|
|
64788
65123
|
if (!body)
|
|
64789
|
-
return
|
|
65124
|
+
return json10({ error: "invalid JSON body" }, 400);
|
|
64790
65125
|
const parsed = JobSchema.safeParse(body);
|
|
64791
65126
|
if (!parsed.success) {
|
|
64792
|
-
return
|
|
65127
|
+
return json10({
|
|
64793
65128
|
error: parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")
|
|
64794
65129
|
}, 400);
|
|
64795
65130
|
}
|
|
64796
65131
|
if (!auditAdapter(parsed.data.tool)) {
|
|
64797
|
-
return
|
|
65132
|
+
return json10({ error: `'${parsed.data.tool}' is not a schedulable auditor` }, 400);
|
|
64798
65133
|
}
|
|
64799
65134
|
await primeRiskIndex();
|
|
64800
65135
|
const risk = liveExecutor().riskOf(parsed.data.tool);
|
|
64801
65136
|
if (risk !== "READ") {
|
|
64802
|
-
return
|
|
65137
|
+
return json10({ error: `'${parsed.data.tool}' is annotated ${risk}; only READ may be scheduled` }, 400);
|
|
64803
65138
|
}
|
|
64804
65139
|
const existing = store.getJob(parsed.data.id);
|
|
64805
65140
|
const job = { ...parsed.data, createdAt: existing?.createdAt ?? now };
|
|
@@ -64808,7 +65143,7 @@ async function scheduleRoutes(req, url) {
|
|
|
64808
65143
|
armJob(job, now);
|
|
64809
65144
|
else
|
|
64810
65145
|
forgetJob(job.id);
|
|
64811
|
-
return
|
|
65146
|
+
return json10({ job: jobRow(job, store.runs(job.id, 500), now), replaced: existing !== null });
|
|
64812
65147
|
}
|
|
64813
65148
|
if (p === "/api/schedules/timeline" && req.method === "GET") {
|
|
64814
65149
|
const jobId = url.searchParams.get("job") ?? undefined;
|
|
@@ -64816,7 +65151,7 @@ async function scheduleRoutes(req, url) {
|
|
|
64816
65151
|
const days = Number(url.searchParams.get("days") ?? 30);
|
|
64817
65152
|
const since = now - days * 86400000;
|
|
64818
65153
|
const runs = store.runs(jobId, 2000).filter((r) => r.startedAt >= since && (!device || r.device === device));
|
|
64819
|
-
return
|
|
65154
|
+
return json10({
|
|
64820
65155
|
points: runs.map((r) => ({
|
|
64821
65156
|
at: r.startedAt,
|
|
64822
65157
|
jobId: r.jobId,
|
|
@@ -64836,30 +65171,30 @@ async function scheduleRoutes(req, url) {
|
|
|
64836
65171
|
const limit = Number(url.searchParams.get("limit") ?? 50);
|
|
64837
65172
|
const jobs = jobId ? [jobId] : store.listJobs().map((j) => j.id);
|
|
64838
65173
|
const items = jobs.flatMap((id) => regressionsFor(store.runs(id, 500), id)).sort((a, b) => b.at - a.at).slice(0, limit);
|
|
64839
|
-
return
|
|
65174
|
+
return json10({ regressions: items });
|
|
64840
65175
|
}
|
|
64841
65176
|
const parts = p.slice("/api/schedules/".length).split("/").filter(Boolean);
|
|
64842
65177
|
if (parts.length === 0 || parts.length > 2)
|
|
64843
|
-
return
|
|
65178
|
+
return json10({ error: "not found" }, 404);
|
|
64844
65179
|
const id = decodeURIComponent(parts[0]);
|
|
64845
65180
|
const verb = parts[1];
|
|
64846
65181
|
if (!verb && req.method === "DELETE") {
|
|
64847
65182
|
const job = store.getJob(id);
|
|
64848
65183
|
if (!job)
|
|
64849
|
-
return
|
|
65184
|
+
return json10({ error: `no schedule '${id}'` }, 404);
|
|
64850
65185
|
const runCount = store.runs(id, 1e4).length;
|
|
64851
65186
|
store.removeJob(id);
|
|
64852
65187
|
forgetJob(id);
|
|
64853
|
-
return
|
|
65188
|
+
return json10({ removed: id, runsDiscarded: runCount });
|
|
64854
65189
|
}
|
|
64855
65190
|
if (verb === "run" && req.method === "POST") {
|
|
64856
65191
|
const job = store.getJob(id);
|
|
64857
65192
|
if (!job)
|
|
64858
|
-
return
|
|
65193
|
+
return json10({ error: `no schedule '${id}'` }, 404);
|
|
64859
65194
|
await primeRiskIndex();
|
|
64860
65195
|
logger.info(`schedule '${id}' run requested from the dashboard`);
|
|
64861
65196
|
const result = await runJob(job, liveExecutor(), runnerOptions());
|
|
64862
|
-
return
|
|
65197
|
+
return json10({
|
|
64863
65198
|
...result,
|
|
64864
65199
|
job: jobRow(job, store.runs(id, 500), Date.now())
|
|
64865
65200
|
});
|
|
@@ -64867,16 +65202,16 @@ async function scheduleRoutes(req, url) {
|
|
|
64867
65202
|
if (!verb && req.method === "GET") {
|
|
64868
65203
|
const job = store.getJob(id);
|
|
64869
65204
|
if (!job)
|
|
64870
|
-
return
|
|
65205
|
+
return json10({ error: `no schedule '${id}'` }, 404);
|
|
64871
65206
|
const runs = store.runs(id, 500);
|
|
64872
|
-
return
|
|
65207
|
+
return json10({ job: jobRow(job, runs, now), runs, regressions: regressionsFor(runs, id) });
|
|
64873
65208
|
}
|
|
64874
|
-
return
|
|
65209
|
+
return json10({ error: "not found" }, 404);
|
|
64875
65210
|
}
|
|
64876
65211
|
|
|
64877
65212
|
// src/observability/explain-routes.ts
|
|
64878
65213
|
var JSON_HEADERS9 = { "content-type": "application/json; charset=utf-8" };
|
|
64879
|
-
function
|
|
65214
|
+
function json11(body, status = 200) {
|
|
64880
65215
|
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS9 });
|
|
64881
65216
|
}
|
|
64882
65217
|
var storePromise17 = null;
|
|
@@ -64917,52 +65252,52 @@ async function explainRoutes(req, url) {
|
|
|
64917
65252
|
if (p === "/api/explain/diff" && req.method === "POST") {
|
|
64918
65253
|
const body = await req.json().catch(() => null);
|
|
64919
65254
|
if (!body)
|
|
64920
|
-
return
|
|
65255
|
+
return json11({ error: "invalid JSON body" }, 400);
|
|
64921
65256
|
if (!body.before && !body.beforeText) {
|
|
64922
|
-
return
|
|
65257
|
+
return json11({ error: "a diff needs a baseline: pass `before` (a snapshot id) or `beforeText`" }, 400);
|
|
64923
65258
|
}
|
|
64924
65259
|
const device = body.device ?? "";
|
|
64925
65260
|
const before = body.beforeText ? { text: body.beforeText, device, source: "supplied text" } : await configFor2(device, body.before ?? null);
|
|
64926
65261
|
if ("error" in before)
|
|
64927
|
-
return
|
|
65262
|
+
return json11({ error: `before: ${before.error}` }, before.status);
|
|
64928
65263
|
const after = body.afterText ? { text: body.afterText, device, source: "supplied text" } : await configFor2(device, body.after ?? null);
|
|
64929
65264
|
if ("error" in after)
|
|
64930
|
-
return
|
|
65265
|
+
return json11({ error: `after: ${after.error}` }, after.status);
|
|
64931
65266
|
const diff = diffNarratives(analyzeDevice(before.text, before.device), analyzeDevice(after.text, after.device));
|
|
64932
|
-
return
|
|
65267
|
+
return json11({ diff, markdown: renderDiff(diff), before: before.source, after: after.source });
|
|
64933
65268
|
}
|
|
64934
65269
|
const rest = p.slice("/api/explain/".length).split("/").filter(Boolean);
|
|
64935
65270
|
if (rest.length === 0)
|
|
64936
|
-
return
|
|
65271
|
+
return json11({ error: "not found" }, 404);
|
|
64937
65272
|
if (req.method !== "GET")
|
|
64938
|
-
return
|
|
65273
|
+
return json11({ error: "not found" }, 404);
|
|
64939
65274
|
const device = decodeURIComponent(rest[0]);
|
|
64940
65275
|
const snapshotId = url.searchParams.get("snapshot");
|
|
64941
65276
|
let config;
|
|
64942
65277
|
try {
|
|
64943
65278
|
config = await configFor2(device, snapshotId);
|
|
64944
65279
|
} catch (e) {
|
|
64945
|
-
return
|
|
65280
|
+
return json11({ error: e instanceof Error ? e.message : String(e) }, 502);
|
|
64946
65281
|
}
|
|
64947
65282
|
if ("error" in config)
|
|
64948
|
-
return
|
|
65283
|
+
return json11({ error: config.error }, config.status);
|
|
64949
65284
|
const narrative = { ...analyzeDevice(config.text, config.device), generatedAt: Date.now() };
|
|
64950
65285
|
if (rest.length === 1) {
|
|
64951
|
-
return
|
|
65286
|
+
return json11(payload(narrative, config.source, renderNarrative(narrative)));
|
|
64952
65287
|
}
|
|
64953
65288
|
if (rest[1] === "section" && rest[2]) {
|
|
64954
65289
|
const section = decodeURIComponent(rest[2]);
|
|
64955
65290
|
if (!NARRATIVE_SECTIONS.includes(section)) {
|
|
64956
|
-
return
|
|
65291
|
+
return json11({ error: `unknown section '${section}'. Known: ${NARRATIVE_SECTIONS.join(", ")}` }, 400);
|
|
64957
65292
|
}
|
|
64958
|
-
return
|
|
65293
|
+
return json11(payload(narrative, config.source, renderNarrative(narrative, { sections: [section] })));
|
|
64959
65294
|
}
|
|
64960
|
-
return
|
|
65295
|
+
return json11({ error: "not found" }, 404);
|
|
64961
65296
|
}
|
|
64962
65297
|
|
|
64963
65298
|
// src/observability/attack-routes.ts
|
|
64964
65299
|
var JSON_HEADERS10 = { "content-type": "application/json; charset=utf-8" };
|
|
64965
|
-
function
|
|
65300
|
+
function json12(body, status = 200) {
|
|
64966
65301
|
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS10 });
|
|
64967
65302
|
}
|
|
64968
65303
|
async function attackRoutes(req, url) {
|
|
@@ -64973,7 +65308,7 @@ async function attackRoutes(req, url) {
|
|
|
64973
65308
|
try {
|
|
64974
65309
|
store = await attackStore();
|
|
64975
65310
|
} catch (e) {
|
|
64976
|
-
return
|
|
65311
|
+
return json12({ error: e instanceof Error ? e.message : String(e), incidents: [] }, 503);
|
|
64977
65312
|
}
|
|
64978
65313
|
const cfg = getConfig().attacks;
|
|
64979
65314
|
const now = Date.now();
|
|
@@ -64985,7 +65320,7 @@ async function attackRoutes(req, url) {
|
|
|
64985
65320
|
});
|
|
64986
65321
|
const responses = store.listResponses({ active: true, limit: 500 });
|
|
64987
65322
|
const blocked = new Set(responses.map((r) => r.source));
|
|
64988
|
-
return
|
|
65323
|
+
return json12({
|
|
64989
65324
|
incidents: incidents.map((i) => ({ ...i, blocked: blocked.has(i.source) })),
|
|
64990
65325
|
responses,
|
|
64991
65326
|
posture: {
|
|
@@ -65025,7 +65360,7 @@ async function attackRoutes(req, url) {
|
|
|
65025
65360
|
}
|
|
65026
65361
|
}
|
|
65027
65362
|
const active = new Set(store.listResponses({ active: true, limit: 500 }).map((r) => r.source));
|
|
65028
|
-
return
|
|
65363
|
+
return json12({
|
|
65029
65364
|
sources: [...bySource.values()].map((s) => ({
|
|
65030
65365
|
source: s.source,
|
|
65031
65366
|
devices: [...s.devices].sort(),
|
|
@@ -65043,14 +65378,14 @@ async function attackRoutes(req, url) {
|
|
|
65043
65378
|
const guards = buildGuards();
|
|
65044
65379
|
const never = [...neverBlockSet(guards)].sort();
|
|
65045
65380
|
const check = url.searchParams.get("check");
|
|
65046
|
-
return
|
|
65381
|
+
return json12({
|
|
65047
65382
|
config: cfg,
|
|
65048
65383
|
neverBlock: never,
|
|
65049
65384
|
check: check ? { address: check, protected: isNeverBlock(check, new Set(never)) } : undefined
|
|
65050
65385
|
});
|
|
65051
65386
|
}
|
|
65052
65387
|
if (p === "/api/attacks/config" && req.method === "POST") {
|
|
65053
|
-
return
|
|
65388
|
+
return json12({
|
|
65054
65389
|
error: "edit the `attacks` block in Config \u2014 the policy has to survive a restart, so it is not held in memory here"
|
|
65055
65390
|
}, 501);
|
|
65056
65391
|
}
|
|
@@ -65063,7 +65398,7 @@ async function attackRoutes(req, url) {
|
|
|
65063
65398
|
onlineOnly: body.onlineOnly,
|
|
65064
65399
|
respond: false
|
|
65065
65400
|
});
|
|
65066
|
-
return
|
|
65401
|
+
return json12({
|
|
65067
65402
|
incidents: result.incidents,
|
|
65068
65403
|
unavailable: result.unavailable,
|
|
65069
65404
|
devices: result.devices,
|
|
@@ -65071,7 +65406,7 @@ async function attackRoutes(req, url) {
|
|
|
65071
65406
|
});
|
|
65072
65407
|
}
|
|
65073
65408
|
if (p === "/api/attacks/devices" && req.method === "GET") {
|
|
65074
|
-
return
|
|
65409
|
+
return json12({
|
|
65075
65410
|
devices: listDevices().names.map((name) => ({
|
|
65076
65411
|
name,
|
|
65077
65412
|
reachable: getDeviceStatus(name).reachable
|
|
@@ -65084,24 +65419,24 @@ async function attackRoutes(req, url) {
|
|
|
65084
65419
|
const response = store.responseFor(address);
|
|
65085
65420
|
const devices = response?.devices ?? [];
|
|
65086
65421
|
if (devices.length === 0)
|
|
65087
|
-
return
|
|
65422
|
+
return json12({ error: `no recorded block for ${address}` }, 404);
|
|
65088
65423
|
const results = await revokeBlock(address, devices, response?.list);
|
|
65089
65424
|
store.revokeResponse(address, now);
|
|
65090
|
-
return
|
|
65425
|
+
return json12({ address, results });
|
|
65091
65426
|
}
|
|
65092
65427
|
if (rest.length === 1 && req.method === "GET") {
|
|
65093
65428
|
const incident = store.getIncident(decodeURIComponent(rest[0]));
|
|
65094
65429
|
if (!incident)
|
|
65095
|
-
return
|
|
65096
|
-
return
|
|
65430
|
+
return json12({ error: "not found" }, 404);
|
|
65431
|
+
return json12({ incident, response: store.responseFor(incident.source) });
|
|
65097
65432
|
}
|
|
65098
65433
|
if (rest.length === 2 && rest[1] === "respond" && req.method === "POST") {
|
|
65099
65434
|
const incident = store.getIncident(decodeURIComponent(rest[0]));
|
|
65100
65435
|
if (!incident)
|
|
65101
|
-
return
|
|
65436
|
+
return json12({ error: "not found" }, 404);
|
|
65102
65437
|
const body = await req.json().catch(() => ({}));
|
|
65103
65438
|
if (body.action === "dismiss") {
|
|
65104
|
-
return
|
|
65439
|
+
return json12({ dismissed: incident.id });
|
|
65105
65440
|
}
|
|
65106
65441
|
const decision = decide({
|
|
65107
65442
|
incident,
|
|
@@ -65113,13 +65448,13 @@ async function attackRoutes(req, url) {
|
|
|
65113
65448
|
timeout: body.timeout
|
|
65114
65449
|
});
|
|
65115
65450
|
if (!isPlan(decision)) {
|
|
65116
|
-
return
|
|
65451
|
+
return json12({ refused: true, guard: decision.guard, reason: decision.reason }, 200);
|
|
65117
65452
|
}
|
|
65118
65453
|
if (decision.action === "escalate") {
|
|
65119
|
-
return
|
|
65454
|
+
return json12({ escalated: true, reason: decision.reason });
|
|
65120
65455
|
}
|
|
65121
65456
|
if (!body.confirm) {
|
|
65122
|
-
return
|
|
65457
|
+
return json12({ dryRun: true, plan: decision });
|
|
65123
65458
|
}
|
|
65124
65459
|
const applied = await executePlan(decision);
|
|
65125
65460
|
const ok = applied.some((r) => r.ok);
|
|
@@ -65135,14 +65470,14 @@ async function attackRoutes(req, url) {
|
|
|
65135
65470
|
ok,
|
|
65136
65471
|
error: ok ? undefined : applied.map((r) => r.detail).join("; ")
|
|
65137
65472
|
});
|
|
65138
|
-
return
|
|
65473
|
+
return json12({ applied, ok, plan: decision });
|
|
65139
65474
|
}
|
|
65140
|
-
return
|
|
65475
|
+
return json12({ error: "not found" }, 404);
|
|
65141
65476
|
}
|
|
65142
65477
|
|
|
65143
65478
|
// src/observability/alert-routes.ts
|
|
65144
65479
|
var JSON_HEADERS11 = { "content-type": "application/json; charset=utf-8" };
|
|
65145
|
-
function
|
|
65480
|
+
function json13(body, status = 200) {
|
|
65146
65481
|
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS11 });
|
|
65147
65482
|
}
|
|
65148
65483
|
function ruleRow(rule, subject, status, since) {
|
|
@@ -65197,11 +65532,11 @@ async function alertRoutes(req, url, db) {
|
|
|
65197
65532
|
const channels = getConfig().alerts?.channels;
|
|
65198
65533
|
if (p === "/api/alerts" && req.method === "GET") {
|
|
65199
65534
|
if (!engine) {
|
|
65200
|
-
return
|
|
65535
|
+
return json13({ configured: false, rules: [], active: [], channels: {} });
|
|
65201
65536
|
}
|
|
65202
65537
|
const now = Date.now();
|
|
65203
65538
|
const rows = engine.snapshot().map(({ rule, subject, state }) => ruleRow(rule, subject, isMuted(rule, now) ? "muted" : !rule.enabled ? "disabled" : state.status, state.since));
|
|
65204
|
-
return
|
|
65539
|
+
return json13({
|
|
65205
65540
|
configured: true,
|
|
65206
65541
|
rules: rows,
|
|
65207
65542
|
active: rows.filter((r) => r.status === "firing"),
|
|
@@ -65210,41 +65545,41 @@ async function alertRoutes(req, url, db) {
|
|
|
65210
65545
|
}
|
|
65211
65546
|
if (p === "/api/alerts/history" && req.method === "GET") {
|
|
65212
65547
|
if (!engine)
|
|
65213
|
-
return
|
|
65548
|
+
return json13({ history: [] });
|
|
65214
65549
|
const hours = Number(url.searchParams.get("hours") ?? 24);
|
|
65215
65550
|
const history = await engine.history({
|
|
65216
65551
|
sinceMs: Date.now() - hours * 3600000,
|
|
65217
65552
|
ruleId: url.searchParams.get("rule") ?? undefined,
|
|
65218
65553
|
limit: Number(url.searchParams.get("limit") ?? 200)
|
|
65219
65554
|
});
|
|
65220
|
-
return
|
|
65555
|
+
return json13({ history });
|
|
65221
65556
|
}
|
|
65222
65557
|
if (p === "/api/alerts/rules" && req.method === "POST") {
|
|
65223
65558
|
if (!engine)
|
|
65224
|
-
return
|
|
65559
|
+
return json13({ error: "alerting is not configured" }, 400);
|
|
65225
65560
|
const parsed = AlertRuleSchema.safeParse(await req.json());
|
|
65226
65561
|
if (!parsed.success) {
|
|
65227
|
-
return
|
|
65562
|
+
return json13({ error: parsed.error.issues.map((i) => i.message).join("; ") }, 400);
|
|
65228
65563
|
}
|
|
65229
65564
|
const rules = engine.configuredRules();
|
|
65230
65565
|
if (rules.some((r) => r.id === parsed.data.id)) {
|
|
65231
|
-
return
|
|
65566
|
+
return json13({ error: `rule '${parsed.data.id}' already exists` }, 409);
|
|
65232
65567
|
}
|
|
65233
65568
|
engine.setRules([...rules, parsed.data]);
|
|
65234
|
-
return
|
|
65569
|
+
return json13({ ok: true, id: parsed.data.id });
|
|
65235
65570
|
}
|
|
65236
65571
|
const idMatch = p.match(/^\/api\/alerts\/rules\/([^/]+)$/);
|
|
65237
65572
|
if (idMatch) {
|
|
65238
65573
|
if (!engine)
|
|
65239
|
-
return
|
|
65574
|
+
return json13({ error: "alerting is not configured" }, 400);
|
|
65240
65575
|
const id = decodeURIComponent(idMatch[1]);
|
|
65241
65576
|
const rules = engine.configuredRules();
|
|
65242
65577
|
const existing = rules.find((r) => r.id === id);
|
|
65243
65578
|
if (!existing)
|
|
65244
|
-
return
|
|
65579
|
+
return json13({ error: `unknown rule: ${id}` }, 404);
|
|
65245
65580
|
if (req.method === "DELETE") {
|
|
65246
65581
|
engine.setRules(rules.filter((r) => r.id !== id));
|
|
65247
|
-
return
|
|
65582
|
+
return json13({ ok: true });
|
|
65248
65583
|
}
|
|
65249
65584
|
if (req.method === "PATCH") {
|
|
65250
65585
|
const body = await req.json();
|
|
@@ -65252,22 +65587,22 @@ async function alertRoutes(req, url, db) {
|
|
|
65252
65587
|
if (typeof body.mute === "string") {
|
|
65253
65588
|
const ms = parseDuration(body.mute);
|
|
65254
65589
|
if (ms === null)
|
|
65255
|
-
return
|
|
65590
|
+
return json13({ error: `invalid duration: ${body.mute}` }, 400);
|
|
65256
65591
|
mutedUntil = ms === 0 ? undefined : Date.now() + ms;
|
|
65257
65592
|
delete body.mute;
|
|
65258
65593
|
}
|
|
65259
65594
|
const merged = AlertRuleSchema.safeParse({ ...existing, ...body, mutedUntil });
|
|
65260
65595
|
if (!merged.success) {
|
|
65261
|
-
return
|
|
65596
|
+
return json13({ error: merged.error.issues.map((i) => i.message).join("; ") }, 400);
|
|
65262
65597
|
}
|
|
65263
65598
|
engine.setRules(rules.map((r) => r.id === id ? merged.data : r));
|
|
65264
|
-
return
|
|
65599
|
+
return json13({ ok: true, rule: merged.data });
|
|
65265
65600
|
}
|
|
65266
65601
|
}
|
|
65267
65602
|
if (p === "/api/alerts/test" && req.method === "POST") {
|
|
65268
65603
|
const body = await req.json();
|
|
65269
65604
|
if (!channels || !body?.channel)
|
|
65270
|
-
return
|
|
65605
|
+
return json13({ error: "channel is required" }, 400);
|
|
65271
65606
|
try {
|
|
65272
65607
|
const result = await deliver(body.channel, channels, {
|
|
65273
65608
|
ruleId: "test",
|
|
@@ -65277,20 +65612,20 @@ async function alertRoutes(req, url, db) {
|
|
|
65277
65612
|
body: "If you can read this, this channel is configured correctly.",
|
|
65278
65613
|
at: Date.now()
|
|
65279
65614
|
});
|
|
65280
|
-
return
|
|
65615
|
+
return json13(result);
|
|
65281
65616
|
} catch (e) {
|
|
65282
65617
|
logger.error(`Alert channel test failed: ${logError(e)}`);
|
|
65283
|
-
return
|
|
65618
|
+
return json13({ error: clientError(e) }, 502);
|
|
65284
65619
|
}
|
|
65285
65620
|
}
|
|
65286
65621
|
if (p === "/api/alerts/preview" && req.method === "POST") {
|
|
65287
65622
|
const parsed = AlertRuleSchema.safeParse(await req.json());
|
|
65288
65623
|
if (!parsed.success) {
|
|
65289
|
-
return
|
|
65624
|
+
return json13({ error: parsed.error.issues.map((i) => i.message).join("; ") }, 400);
|
|
65290
65625
|
}
|
|
65291
65626
|
const hours = Number(url.searchParams.get("hours") ?? 24);
|
|
65292
65627
|
if (!db) {
|
|
65293
|
-
return
|
|
65628
|
+
return json13({ hours, sampled: 0, fires: 0, resolves: 0, matched: 0, noHistory: true });
|
|
65294
65629
|
}
|
|
65295
65630
|
const events = db.query({ since: Date.now() - hours * 3600000, limit: 5000 }).map((e) => ({
|
|
65296
65631
|
ts: e.ts,
|
|
@@ -65299,14 +65634,14 @@ async function alertRoutes(req, url, db) {
|
|
|
65299
65634
|
risk: e.risk,
|
|
65300
65635
|
isError: e.isError
|
|
65301
65636
|
}));
|
|
65302
|
-
return
|
|
65637
|
+
return json13({ hours, sampled: events.length, ...previewRule(parsed.data, events) });
|
|
65303
65638
|
}
|
|
65304
65639
|
return null;
|
|
65305
65640
|
}
|
|
65306
65641
|
|
|
65307
65642
|
// src/observability/memory-routes.ts
|
|
65308
65643
|
var JSON_HEADERS12 = { "content-type": "application/json; charset=utf-8" };
|
|
65309
|
-
function
|
|
65644
|
+
function json14(body, status = 200) {
|
|
65310
65645
|
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS12 });
|
|
65311
65646
|
}
|
|
65312
65647
|
async function bodyJson4(req) {
|
|
@@ -65318,7 +65653,7 @@ async function memoryRoutes(req, url) {
|
|
|
65318
65653
|
return null;
|
|
65319
65654
|
const cfg = getConfig();
|
|
65320
65655
|
if (!cfg.memory.enabled) {
|
|
65321
|
-
return
|
|
65656
|
+
return json14({ error: "Knowledge-graph memory is disabled" }, 503);
|
|
65322
65657
|
}
|
|
65323
65658
|
if (p === "/api/memory/config" && req.method === "GET") {
|
|
65324
65659
|
let stats = null;
|
|
@@ -65326,7 +65661,7 @@ async function memoryRoutes(req, url) {
|
|
|
65326
65661
|
const store = await getMemoryStore();
|
|
65327
65662
|
stats = store.stats();
|
|
65328
65663
|
} catch {}
|
|
65329
|
-
return
|
|
65664
|
+
return json14({
|
|
65330
65665
|
enabled: cfg.memory.enabled,
|
|
65331
65666
|
dbPath: cfg.memory.dbPath,
|
|
65332
65667
|
stats
|
|
@@ -65340,7 +65675,7 @@ async function memoryRoutes(req, url) {
|
|
|
65340
65675
|
try {
|
|
65341
65676
|
await reopenMemoryStore(body.dbPath);
|
|
65342
65677
|
} catch (e) {
|
|
65343
|
-
return
|
|
65678
|
+
return json14({
|
|
65344
65679
|
error: `Failed to open memory DB at ${body.dbPath}: ${e instanceof Error ? e.message : String(e)}`
|
|
65345
65680
|
}, 400);
|
|
65346
65681
|
}
|
|
@@ -65362,7 +65697,7 @@ async function memoryRoutes(req, url) {
|
|
|
65362
65697
|
const store = await getMemoryStore();
|
|
65363
65698
|
stats = store.stats();
|
|
65364
65699
|
} catch {}
|
|
65365
|
-
return
|
|
65700
|
+
return json14({
|
|
65366
65701
|
ok: true,
|
|
65367
65702
|
enabled: getConfig().memory.enabled,
|
|
65368
65703
|
dbPath: getConfig().memory.dbPath,
|
|
@@ -65371,58 +65706,58 @@ async function memoryRoutes(req, url) {
|
|
|
65371
65706
|
}
|
|
65372
65707
|
const store = await getMemoryStore();
|
|
65373
65708
|
if (p === "/api/memory/graph" && req.method === "GET") {
|
|
65374
|
-
return
|
|
65709
|
+
return json14(store.readGraph());
|
|
65375
65710
|
}
|
|
65376
65711
|
if (p === "/api/memory/stats" && req.method === "GET") {
|
|
65377
|
-
return
|
|
65712
|
+
return json14(store.stats());
|
|
65378
65713
|
}
|
|
65379
65714
|
if (p === "/api/memory/search" && req.method === "GET") {
|
|
65380
65715
|
const q = url.searchParams.get("q") || "";
|
|
65381
65716
|
const limit = Number(url.searchParams.get("limit") ?? 50);
|
|
65382
|
-
return
|
|
65717
|
+
return json14(store.searchNodes(q, limit));
|
|
65383
65718
|
}
|
|
65384
65719
|
if (p === "/api/memory/activity" && req.method === "GET") {
|
|
65385
65720
|
const limit = Number(url.searchParams.get("limit") ?? 50);
|
|
65386
65721
|
const since = url.searchParams.get("since");
|
|
65387
|
-
return
|
|
65722
|
+
return json14(store.activity(limit, since ? Number(since) : undefined));
|
|
65388
65723
|
}
|
|
65389
65724
|
const entityMatch = p.match(/^\/api\/memory\/entity\/(.+)$/);
|
|
65390
65725
|
if (entityMatch && req.method === "GET") {
|
|
65391
65726
|
const name = decodeURIComponent(entityMatch[1]);
|
|
65392
65727
|
const graph = store.openNodes([name]);
|
|
65393
65728
|
if (graph.entities.length === 0)
|
|
65394
|
-
return
|
|
65395
|
-
return
|
|
65729
|
+
return json14({ error: "Entity not found" }, 404);
|
|
65730
|
+
return json14({ entity: graph.entities[0], relations: graph.relations });
|
|
65396
65731
|
}
|
|
65397
65732
|
if (p === "/api/memory/entities" && req.method === "POST") {
|
|
65398
65733
|
const body = await bodyJson4(req);
|
|
65399
65734
|
const created = store.createEntities(body.entities ?? []);
|
|
65400
|
-
return
|
|
65735
|
+
return json14({ created, count: created.length });
|
|
65401
65736
|
}
|
|
65402
65737
|
if (p === "/api/memory/relations" && req.method === "POST") {
|
|
65403
65738
|
const body = await bodyJson4(req);
|
|
65404
65739
|
const created = store.createRelations(body.relations ?? []);
|
|
65405
|
-
return
|
|
65740
|
+
return json14({ created, count: created.length });
|
|
65406
65741
|
}
|
|
65407
65742
|
if (p === "/api/memory/observations" && req.method === "POST") {
|
|
65408
65743
|
const body = await bodyJson4(req);
|
|
65409
65744
|
const results = store.addObservations(body.observations ?? []);
|
|
65410
|
-
return
|
|
65745
|
+
return json14({ results });
|
|
65411
65746
|
}
|
|
65412
65747
|
if (p === "/api/memory/entities" && req.method === "DELETE") {
|
|
65413
65748
|
const body = await bodyJson4(req);
|
|
65414
65749
|
const removed = store.deleteEntities(body.names ?? []);
|
|
65415
|
-
return
|
|
65750
|
+
return json14({ removed });
|
|
65416
65751
|
}
|
|
65417
65752
|
if (p === "/api/memory/relations" && req.method === "DELETE") {
|
|
65418
65753
|
const body = await bodyJson4(req);
|
|
65419
65754
|
const removed = store.deleteRelations(body.relations ?? []);
|
|
65420
|
-
return
|
|
65755
|
+
return json14({ removed });
|
|
65421
65756
|
}
|
|
65422
65757
|
if (p === "/api/memory/observations" && req.method === "DELETE") {
|
|
65423
65758
|
const body = await bodyJson4(req);
|
|
65424
65759
|
const removed = store.deleteObservations(body.deletions ?? []);
|
|
65425
|
-
return
|
|
65760
|
+
return json14({ removed });
|
|
65426
65761
|
}
|
|
65427
65762
|
return null;
|
|
65428
65763
|
}
|
|
@@ -65430,7 +65765,7 @@ async function memoryRoutes(req, url) {
|
|
|
65430
65765
|
// src/observability/dashboard.ts
|
|
65431
65766
|
var SERVER_TAG3 = "mikrotik-mcp";
|
|
65432
65767
|
var JSON_HEADERS13 = { "content-type": "application/json; charset=utf-8" };
|
|
65433
|
-
function
|
|
65768
|
+
function json15(body, status = 200) {
|
|
65434
65769
|
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS13 });
|
|
65435
65770
|
}
|
|
65436
65771
|
function restartProcess() {
|
|
@@ -65611,25 +65946,7 @@ function topologyPayload() {
|
|
|
65611
65946
|
};
|
|
65612
65947
|
}
|
|
65613
65948
|
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
|
-
});
|
|
65949
|
+
return redact(getConfig());
|
|
65633
65950
|
}
|
|
65634
65951
|
function sseResponse(transportLabel) {
|
|
65635
65952
|
let unsub;
|
|
@@ -65731,11 +66048,11 @@ function issues(error) {
|
|
|
65731
66048
|
async function configRoutes(req, url, admin) {
|
|
65732
66049
|
const p = url.pathname;
|
|
65733
66050
|
if (p === "/api/config-schema" && req.method === "GET") {
|
|
65734
|
-
return
|
|
66051
|
+
return json15(configSchemaJson());
|
|
65735
66052
|
}
|
|
65736
66053
|
if (p === "/api/config/validate" && req.method === "POST") {
|
|
65737
66054
|
const merged = mergeSecrets(await readJson(req), getConfig());
|
|
65738
|
-
return
|
|
66055
|
+
return json15(validateConfig(merged));
|
|
65739
66056
|
}
|
|
65740
66057
|
if (p === "/api/config/test-device" && req.method === "POST") {
|
|
65741
66058
|
const body = await readJson(req);
|
|
@@ -65744,22 +66061,22 @@ async function configRoutes(req, url, admin) {
|
|
|
65744
66061
|
const merged = mergeSecrets(body?.config, currentDc);
|
|
65745
66062
|
const parsed = DeviceConfigSchema.safeParse(merged);
|
|
65746
66063
|
if (!parsed.success)
|
|
65747
|
-
return
|
|
66064
|
+
return json15({ ok: false, errors: issues(parsed.error) }, 400);
|
|
65748
66065
|
const status = await probeDevice2(`config-test:${name}`, parsed.data);
|
|
65749
|
-
return
|
|
66066
|
+
return json15({ ok: true, status });
|
|
65750
66067
|
}
|
|
65751
66068
|
if (p === "/api/config/preview" && req.method === "POST") {
|
|
65752
66069
|
const before = JSON.stringify(redact(getConfig()), null, 2);
|
|
65753
66070
|
const after = JSON.stringify(await readJson(req) ?? {}, null, 2);
|
|
65754
66071
|
const d = diffLines(before, after, { fromLabel: "current", toLabel: "edited" });
|
|
65755
|
-
return
|
|
66072
|
+
return json15({ summary: d.summary, unified: d.unified });
|
|
65756
66073
|
}
|
|
65757
66074
|
if (p === "/api/config" && req.method === "POST") {
|
|
65758
66075
|
const body = await readJson(req);
|
|
65759
66076
|
const merged = mergeSecrets(body?.config, getConfig());
|
|
65760
66077
|
const v = validateConfig(merged);
|
|
65761
66078
|
if (!v.ok || !v.value)
|
|
65762
|
-
return
|
|
66079
|
+
return json15({ ok: false, errors: v.errors }, 400);
|
|
65763
66080
|
const prev = getConfig();
|
|
65764
66081
|
const before = JSON.stringify(redact(prev), null, 2);
|
|
65765
66082
|
const after = JSON.stringify(redact(v.value), null, 2);
|
|
@@ -65768,25 +66085,25 @@ async function configRoutes(req, url, admin) {
|
|
|
65768
66085
|
const nextDevs = Object.keys(v.value.devices);
|
|
65769
66086
|
const devicesChanged = prevDevs.length !== nextDevs.length || prevDevs.some((d) => !nextDevs.includes(d));
|
|
65770
66087
|
const res = admin.applyConfig(v.value, clampRollback(body?.rollbackMs));
|
|
65771
|
-
return
|
|
66088
|
+
return json15({ ok: true, ...res, devicesChanged, summary: diff.summary, unified: diff.unified });
|
|
65772
66089
|
}
|
|
65773
66090
|
if (p === "/api/config/keep" && req.method === "POST") {
|
|
65774
66091
|
const body = await readJson(req);
|
|
65775
66092
|
const kept = admin.keepConfig(String(body?.pendingId ?? ""));
|
|
65776
66093
|
if (kept)
|
|
65777
66094
|
recordVersion(getConfig(), "auto", Date.now());
|
|
65778
|
-
return
|
|
66095
|
+
return json15({ kept });
|
|
65779
66096
|
}
|
|
65780
66097
|
if (p === "/api/config/rollback" && req.method === "POST") {
|
|
65781
66098
|
const body = await readJson(req);
|
|
65782
66099
|
const rolledBack = admin.rollback(String(body?.pendingId ?? ""));
|
|
65783
|
-
return
|
|
66100
|
+
return json15({ rolledBack, config: redact(getConfig()) });
|
|
65784
66101
|
}
|
|
65785
66102
|
if (p === "/api/reload" && req.method === "POST") {
|
|
65786
66103
|
const body = await readJson(req);
|
|
65787
66104
|
if (body?.hard === true) {
|
|
65788
66105
|
const relaunched = restartProcess();
|
|
65789
|
-
return
|
|
66106
|
+
return json15({
|
|
65790
66107
|
ok: true,
|
|
65791
66108
|
mode: "restart",
|
|
65792
66109
|
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 +66114,10 @@ async function configRoutes(req, url, admin) {
|
|
|
65797
66114
|
setConfig(cfg);
|
|
65798
66115
|
const devices = Object.keys(cfg.devices);
|
|
65799
66116
|
logger.info(`Config reloaded from source \u2014 ${devices.length} device(s): ${devices.join(", ")}`);
|
|
65800
|
-
return
|
|
66117
|
+
return json15({ ok: true, mode: "config", count: devices.length, devices });
|
|
65801
66118
|
} catch (e) {
|
|
65802
66119
|
logger.error(`Config reload failed: ${logError(e)}`);
|
|
65803
|
-
return
|
|
66120
|
+
return json15({ ok: false, error: clientError(e) }, 500);
|
|
65804
66121
|
}
|
|
65805
66122
|
}
|
|
65806
66123
|
if (p === "/api/config/history" && req.method === "GET") {
|
|
@@ -65816,55 +66133,55 @@ async function configRoutes(req, url, admin) {
|
|
|
65816
66133
|
} catch {}
|
|
65817
66134
|
return { ...v, drift: { added, removed } };
|
|
65818
66135
|
});
|
|
65819
|
-
return
|
|
66136
|
+
return json15({ versions, bytes: historyBytes(), retention: AUTO_RETENTION });
|
|
65820
66137
|
}
|
|
65821
66138
|
if (p === "/api/config/history/get" && req.method === "GET") {
|
|
65822
66139
|
const id = url.searchParams.get("id");
|
|
65823
66140
|
if (!id)
|
|
65824
|
-
return
|
|
66141
|
+
return json15({ error: "id required" }, 400);
|
|
65825
66142
|
try {
|
|
65826
66143
|
const v = readVersion(id);
|
|
65827
|
-
return
|
|
66144
|
+
return json15({
|
|
65828
66145
|
ts: v.ts,
|
|
65829
66146
|
kind: v.kind,
|
|
65830
66147
|
label: v.label,
|
|
65831
66148
|
config: JSON.stringify(redact(v.config), null, 2)
|
|
65832
66149
|
});
|
|
65833
66150
|
} catch {
|
|
65834
|
-
return
|
|
66151
|
+
return json15({ error: "not found" }, 404);
|
|
65835
66152
|
}
|
|
65836
66153
|
}
|
|
65837
66154
|
if (p === "/api/config/history/diff" && req.method === "GET") {
|
|
65838
66155
|
const id = url.searchParams.get("id");
|
|
65839
66156
|
if (!id)
|
|
65840
|
-
return
|
|
66157
|
+
return json15({ error: "id required" }, 400);
|
|
65841
66158
|
try {
|
|
65842
66159
|
const before = JSON.stringify(redact(readVersion(id).config), null, 2);
|
|
65843
66160
|
const after = JSON.stringify(redact(getConfig()), null, 2);
|
|
65844
66161
|
const d = diffLines(before, after, { fromLabel: "this version", toLabel: "current" });
|
|
65845
|
-
return
|
|
66162
|
+
return json15({ summary: d.summary, unified: d.unified });
|
|
65846
66163
|
} catch {
|
|
65847
|
-
return
|
|
66164
|
+
return json15({ error: "not found" }, 404);
|
|
65848
66165
|
}
|
|
65849
66166
|
}
|
|
65850
66167
|
if (p === "/api/config/history/checkpoint" && req.method === "POST") {
|
|
65851
66168
|
const b = await readJson(req);
|
|
65852
66169
|
const label = b?.label?.trim() || "checkpoint";
|
|
65853
|
-
return
|
|
66170
|
+
return json15({ ok: true, version: recordVersion(getConfig(), "checkpoint", Date.now(), label) });
|
|
65854
66171
|
}
|
|
65855
66172
|
if (p === "/api/config/history/restore" && req.method === "POST") {
|
|
65856
66173
|
const b = await readJson(req);
|
|
65857
66174
|
if (!b?.id)
|
|
65858
|
-
return
|
|
66175
|
+
return json15({ error: "id required" }, 400);
|
|
65859
66176
|
let target;
|
|
65860
66177
|
try {
|
|
65861
66178
|
target = readVersion(b.id);
|
|
65862
66179
|
} catch {
|
|
65863
|
-
return
|
|
66180
|
+
return json15({ error: "not found" }, 404);
|
|
65864
66181
|
}
|
|
65865
66182
|
const v = validateConfig(target.config);
|
|
65866
66183
|
if (!v.ok || !v.value)
|
|
65867
|
-
return
|
|
66184
|
+
return json15({ ok: false, errors: v.errors }, 400);
|
|
65868
66185
|
recordVersion(getConfig(), "auto", Date.now(), "before restore");
|
|
65869
66186
|
setConfig(v.value);
|
|
65870
66187
|
let persisted = true;
|
|
@@ -65875,13 +66192,13 @@ async function configRoutes(req, url, admin) {
|
|
|
65875
66192
|
}
|
|
65876
66193
|
const label = target.label ? `restored "${target.label}"` : `restored ${b.id}`;
|
|
65877
66194
|
recordVersion(getConfig(), "auto", Date.now(), label);
|
|
65878
|
-
return
|
|
66195
|
+
return json15({ ok: true, persisted, restored: b.id, config: redact(getConfig()) });
|
|
65879
66196
|
}
|
|
65880
66197
|
if (p === "/api/config/history/delete" && req.method === "POST") {
|
|
65881
66198
|
const b = await readJson(req);
|
|
65882
66199
|
if (!b?.id)
|
|
65883
|
-
return
|
|
65884
|
-
return deleteVersion(b.id) ?
|
|
66200
|
+
return json15({ error: "id required" }, 400);
|
|
66201
|
+
return deleteVersion(b.id) ? json15({ ok: true }) : json15({ error: "not found" }, 404);
|
|
65885
66202
|
}
|
|
65886
66203
|
return null;
|
|
65887
66204
|
}
|
|
@@ -65892,7 +66209,7 @@ async function modulesRoutes(req, url) {
|
|
|
65892
66209
|
if (p === "/api/modules" && req.method === "GET") {
|
|
65893
66210
|
const cfg = getConfig();
|
|
65894
66211
|
const src = getConfigSource();
|
|
65895
|
-
return
|
|
66212
|
+
return json15({
|
|
65896
66213
|
...moduleSurface(cfg.tools),
|
|
65897
66214
|
filter: cfg.tools,
|
|
65898
66215
|
source: src,
|
|
@@ -65902,11 +66219,11 @@ async function modulesRoutes(req, url) {
|
|
|
65902
66219
|
if (p === "/api/modules/toggle" && req.method === "POST") {
|
|
65903
66220
|
const b = await readJson(req);
|
|
65904
66221
|
if (typeof b?.slug !== "string" || typeof b?.enabled !== "boolean") {
|
|
65905
|
-
return
|
|
66222
|
+
return json15({ error: "slug (string) and enabled (boolean) are required" }, 400);
|
|
65906
66223
|
}
|
|
65907
66224
|
const mod = moduleCatalog2.find((m) => m.slug.toLowerCase() === b.slug.toLowerCase());
|
|
65908
66225
|
if (!mod)
|
|
65909
|
-
return
|
|
66226
|
+
return json15({ error: `unknown module: ${b.slug}` }, 404);
|
|
65910
66227
|
const cfg = getConfig();
|
|
65911
66228
|
const nextTools = ToolFilterSchema.parse(applyModuleToggle(cfg.tools, mod.slug, b.enabled));
|
|
65912
66229
|
const next = { ...cfg, tools: nextTools };
|
|
@@ -65922,7 +66239,7 @@ async function modulesRoutes(req, url) {
|
|
|
65922
66239
|
if (persisted) {
|
|
65923
66240
|
recordVersion(getConfig(), "auto", Date.now(), `module ${b.enabled ? "enabled" : "disabled"}: ${mod.slug}`);
|
|
65924
66241
|
}
|
|
65925
|
-
return
|
|
66242
|
+
return json15({
|
|
65926
66243
|
ok: true,
|
|
65927
66244
|
persisted,
|
|
65928
66245
|
requiresReconnect: true,
|
|
@@ -65937,7 +66254,7 @@ async function modulesRoutes(req, url) {
|
|
|
65937
66254
|
if (p === "/api/modules/app-views" && req.method === "POST") {
|
|
65938
66255
|
const b = await readJson(req);
|
|
65939
66256
|
if (typeof b?.enabled !== "boolean") {
|
|
65940
|
-
return
|
|
66257
|
+
return json15({ error: "enabled (boolean) is required" }, 400);
|
|
65941
66258
|
}
|
|
65942
66259
|
const cfg = getConfig();
|
|
65943
66260
|
const next = { ...cfg, mcp: { ...cfg.mcp, appViews: b.enabled } };
|
|
@@ -65953,7 +66270,7 @@ async function modulesRoutes(req, url) {
|
|
|
65953
66270
|
if (persisted) {
|
|
65954
66271
|
recordVersion(getConfig(), "auto", Date.now(), `app views ${b.enabled ? "enabled" : "disabled"}`);
|
|
65955
66272
|
}
|
|
65956
|
-
return
|
|
66273
|
+
return json15({
|
|
65957
66274
|
ok: true,
|
|
65958
66275
|
persisted,
|
|
65959
66276
|
requiresReconnect: true,
|
|
@@ -65967,11 +66284,11 @@ async function modulesRoutes(req, url) {
|
|
|
65967
66284
|
async function captureRoutes(req, url) {
|
|
65968
66285
|
const p = url.pathname;
|
|
65969
66286
|
if (p === "/api/capture/status" && req.method === "GET") {
|
|
65970
|
-
return
|
|
66287
|
+
return json15(capture2.stats());
|
|
65971
66288
|
}
|
|
65972
66289
|
if (p === "/api/capture/packets" && req.method === "GET") {
|
|
65973
66290
|
const limit = Number(url.searchParams.get("limit") ?? 200) || 200;
|
|
65974
|
-
return
|
|
66291
|
+
return json15({ packets: capture2.recent(limit), stats: capture2.stats() });
|
|
65975
66292
|
}
|
|
65976
66293
|
if (p === "/api/capture/pcap" && req.method === "GET") {
|
|
65977
66294
|
return new Response(capture2.pcap(), {
|
|
@@ -65983,11 +66300,11 @@ async function captureRoutes(req, url) {
|
|
|
65983
66300
|
}
|
|
65984
66301
|
if (p === "/api/capture/start" && req.method === "POST") {
|
|
65985
66302
|
const body = await readJson(req);
|
|
65986
|
-
return
|
|
66303
|
+
return json15(await capture2.start(typeof body?.port === "number" ? body.port : DEFAULT_TZSP_PORT));
|
|
65987
66304
|
}
|
|
65988
66305
|
if (p === "/api/capture/stop" && req.method === "POST") {
|
|
65989
66306
|
capture2.stop();
|
|
65990
|
-
return
|
|
66307
|
+
return json15({ ok: true, ...capture2.stats() });
|
|
65991
66308
|
}
|
|
65992
66309
|
return null;
|
|
65993
66310
|
}
|
|
@@ -65999,24 +66316,24 @@ async function capsmanRoutes(req, url) {
|
|
|
65999
66316
|
if (req.method === "GET") {
|
|
66000
66317
|
const ctx = createContext(undefined, url.searchParams.get("device") ?? undefined);
|
|
66001
66318
|
if (p === "/api/capsman/overview") {
|
|
66002
|
-
return
|
|
66319
|
+
return json15(capsmanOverview(await fetchCapsmanState(ctx)));
|
|
66003
66320
|
}
|
|
66004
66321
|
if (p === "/api/capsman/clients") {
|
|
66005
66322
|
const state = await fetchCapsmanState(ctx);
|
|
66006
66323
|
const weakDbm = Number.parseInt(url.searchParams.get("weak_dbm") ?? "", 10);
|
|
66007
66324
|
const weak = reportWeakClients(state, Number.isFinite(weakDbm) ? weakDbm : undefined);
|
|
66008
|
-
return
|
|
66325
|
+
return json15({ clients: state.clients, weak });
|
|
66009
66326
|
}
|
|
66010
66327
|
if (p === "/api/capsman/audit") {
|
|
66011
|
-
return
|
|
66328
|
+
return json15(runCapsmanAudit(await fetchCapsmanState(ctx)));
|
|
66012
66329
|
}
|
|
66013
66330
|
if (p === "/api/capsman/trends") {
|
|
66014
66331
|
if (!capsmanStore)
|
|
66015
|
-
return
|
|
66332
|
+
return json15({ error: "capsman store not active" }, 503);
|
|
66016
66333
|
const device = resolveDeviceName(url.searchParams.get("device") ?? undefined);
|
|
66017
66334
|
const days = daysParam(url, 7, 30);
|
|
66018
66335
|
const series = capsmanStore.radioSeries(device, Date.now() - days * 86400000);
|
|
66019
|
-
return
|
|
66336
|
+
return json15({ series, days });
|
|
66020
66337
|
}
|
|
66021
66338
|
return null;
|
|
66022
66339
|
}
|
|
@@ -66027,45 +66344,45 @@ async function capsmanRoutes(req, url) {
|
|
|
66027
66344
|
if (p === "/api/capsman/apply/steer") {
|
|
66028
66345
|
const client = state.clients.find((c) => c.mac.toLowerCase() === (body.mac ?? "").toLowerCase());
|
|
66029
66346
|
if (!client)
|
|
66030
|
-
return
|
|
66347
|
+
return json15({ ok: false, error: "client not associated" }, 400);
|
|
66031
66348
|
if (steerAlreadyPresent(state, body.mac ?? "")) {
|
|
66032
|
-
return
|
|
66349
|
+
return json15({ ok: true, message: "already steered (no-op)" });
|
|
66033
66350
|
}
|
|
66034
66351
|
const mode = body.mode === "soft" ? "soft" : "hard";
|
|
66035
66352
|
const commands = buildSteerCommands(state, body.mac ?? "", client.radioId, mode);
|
|
66036
66353
|
if (!body.confirm)
|
|
66037
|
-
return
|
|
66038
|
-
return
|
|
66354
|
+
return json15({ ok: true, preview: commands });
|
|
66355
|
+
return json15(await applyCapsmanWrites(ctx, commands, `pre-steer-${body.mac}`));
|
|
66039
66356
|
}
|
|
66040
66357
|
if (p === "/api/capsman/apply/load-balance") {
|
|
66041
66358
|
const plan = loadBalancePlan(state);
|
|
66042
66359
|
const commands = buildLoadBalanceCommands(state, plan);
|
|
66043
66360
|
if (!body.confirm)
|
|
66044
|
-
return
|
|
66045
|
-
return
|
|
66361
|
+
return json15({ ok: true, preview: commands, plan });
|
|
66362
|
+
return json15(await applyCapsmanWrites(ctx, commands, "pre-load-balance"));
|
|
66046
66363
|
}
|
|
66047
66364
|
if (p === "/api/capsman/apply/channel-plan") {
|
|
66048
66365
|
if (state.path === "/caps-man") {
|
|
66049
|
-
return
|
|
66366
|
+
return json15({ ok: false, error: "channel-plan apply is v7 /interface wifi only" }, 400);
|
|
66050
66367
|
}
|
|
66051
66368
|
const commands = buildChannelPlanCommands(state);
|
|
66052
66369
|
if (!body.confirm)
|
|
66053
|
-
return
|
|
66054
|
-
return
|
|
66370
|
+
return json15({ ok: true, preview: commands });
|
|
66371
|
+
return json15(await applyCapsmanWrites(ctx, commands, "pre-channel-plan"));
|
|
66055
66372
|
}
|
|
66056
66373
|
if (p === "/api/capsman/apply/ft") {
|
|
66057
66374
|
const commands = buildFtCommands(state);
|
|
66058
66375
|
if (!body.confirm)
|
|
66059
|
-
return
|
|
66060
|
-
return
|
|
66376
|
+
return json15({ ok: true, preview: commands });
|
|
66377
|
+
return json15(await applyCapsmanWrites(ctx, commands, "pre-ft"));
|
|
66061
66378
|
}
|
|
66062
66379
|
if (p === "/api/capsman/apply/ha") {
|
|
66063
66380
|
const commands = buildHaCommands(state);
|
|
66064
66381
|
const guidance = haGuidance(state);
|
|
66065
66382
|
if (!body.confirm)
|
|
66066
|
-
return
|
|
66383
|
+
return json15({ ok: true, preview: commands, guidance });
|
|
66067
66384
|
const res = await applyCapsmanWrites(ctx, commands, "pre-ha");
|
|
66068
|
-
return
|
|
66385
|
+
return json15({ ...res, guidance });
|
|
66069
66386
|
}
|
|
66070
66387
|
}
|
|
66071
66388
|
return null;
|
|
@@ -66091,32 +66408,32 @@ async function clientsRoutes(req, url) {
|
|
|
66091
66408
|
const deviceFromQuery = () => url.searchParams.get("device") ?? undefined;
|
|
66092
66409
|
if (p === "/api/clients" && req.method === "GET") {
|
|
66093
66410
|
const ctx = createContext(undefined, deviceFromQuery());
|
|
66094
|
-
return
|
|
66411
|
+
return json15(devicesView(await fetchDevices(ctx)));
|
|
66095
66412
|
}
|
|
66096
66413
|
if (p === "/api/clients/traffic" && req.method === "GET") {
|
|
66097
66414
|
const ip = url.searchParams.get("ip");
|
|
66098
66415
|
if (!ip)
|
|
66099
|
-
return
|
|
66416
|
+
return json15({ error: "ip required" }, 400);
|
|
66100
66417
|
const ctx = createContext(undefined, deviceFromQuery());
|
|
66101
|
-
return
|
|
66418
|
+
return json15(await sampleDeviceTraffic(ctx, ip));
|
|
66102
66419
|
}
|
|
66103
66420
|
if (p === "/api/clients/traffic-bulk" && req.method === "GET") {
|
|
66104
66421
|
const ctx = createContext(undefined, deviceFromQuery());
|
|
66105
|
-
return
|
|
66422
|
+
return json15(await sampleAllTraffic(ctx));
|
|
66106
66423
|
}
|
|
66107
66424
|
if (req.method === "POST") {
|
|
66108
66425
|
const b = await readJson(req);
|
|
66109
66426
|
const ctx = createContext(undefined, b?.device);
|
|
66110
66427
|
if (p === "/api/clients/limits") {
|
|
66111
66428
|
if (!b?.ip)
|
|
66112
|
-
return
|
|
66113
|
-
return
|
|
66429
|
+
return json15({ error: "ip required" }, 400);
|
|
66430
|
+
return json15(await setDeviceLimits(ctx, b.ip, { download: b.download, upload: b.upload }));
|
|
66114
66431
|
}
|
|
66115
66432
|
if (!b?.mac)
|
|
66116
|
-
return
|
|
66433
|
+
return json15({ error: "mac required" }, 400);
|
|
66117
66434
|
const run = async (op) => {
|
|
66118
66435
|
const r = await op;
|
|
66119
|
-
return
|
|
66436
|
+
return json15({ ...r, view: r.ok ? devicesView(await fetchDevices(ctx)) : undefined });
|
|
66120
66437
|
};
|
|
66121
66438
|
if (p === "/api/clients/block")
|
|
66122
66439
|
return run(blockDevice(ctx, b.mac, b.comment));
|
|
@@ -66128,12 +66445,12 @@ async function clientsRoutes(req, url) {
|
|
|
66128
66445
|
return run(removeDeviceLease(ctx, b.mac));
|
|
66129
66446
|
if (p === "/api/clients/set-ip") {
|
|
66130
66447
|
if (!b.ip)
|
|
66131
|
-
return
|
|
66448
|
+
return json15({ error: "ip required" }, 400);
|
|
66132
66449
|
return run(setDeviceIp(ctx, b.mac, b.ip));
|
|
66133
66450
|
}
|
|
66134
66451
|
if (p === "/api/clients/label") {
|
|
66135
66452
|
if (typeof b.label !== "string")
|
|
66136
|
-
return
|
|
66453
|
+
return json15({ error: "label required" }, 400);
|
|
66137
66454
|
return run(setDeviceLabel(ctx, b.mac, b.label));
|
|
66138
66455
|
}
|
|
66139
66456
|
}
|
|
@@ -66146,20 +66463,20 @@ async function aaaRoutes(req, url) {
|
|
|
66146
66463
|
const deviceFromQuery = () => url.searchParams.get("device") ?? undefined;
|
|
66147
66464
|
if (req.method === "GET") {
|
|
66148
66465
|
if (p === "/api/aaa/entities") {
|
|
66149
|
-
return
|
|
66466
|
+
return json15({ entities: AAA_ENTITIES });
|
|
66150
66467
|
}
|
|
66151
66468
|
if (p === "/api/aaa/radius-incoming") {
|
|
66152
|
-
return
|
|
66469
|
+
return json15(await getRadiusIncoming(createContext(undefined, deviceFromQuery())));
|
|
66153
66470
|
}
|
|
66154
66471
|
if (p === "/api/aaa/um-settings") {
|
|
66155
|
-
return
|
|
66472
|
+
return json15(await getUmSettings(createContext(undefined, deviceFromQuery())));
|
|
66156
66473
|
}
|
|
66157
66474
|
const listMatch = p.match(/^\/api\/aaa\/list\/([\w-]+)$/);
|
|
66158
66475
|
if (listMatch) {
|
|
66159
66476
|
const slug = listMatch[1];
|
|
66160
66477
|
if (!AAA_ENTITIES[slug])
|
|
66161
|
-
return
|
|
66162
|
-
return
|
|
66478
|
+
return json15({ error: "unknown entity" }, 404);
|
|
66479
|
+
return json15(await listAaaEntity(createContext(undefined, deviceFromQuery()), slug));
|
|
66163
66480
|
}
|
|
66164
66481
|
return null;
|
|
66165
66482
|
}
|
|
@@ -66167,30 +66484,30 @@ async function aaaRoutes(req, url) {
|
|
|
66167
66484
|
const body = await readJson(req);
|
|
66168
66485
|
const ctx = createContext(undefined, body?.device);
|
|
66169
66486
|
if (p === "/api/aaa/radius-incoming")
|
|
66170
|
-
return
|
|
66487
|
+
return json15(await setRadiusIncoming(ctx, body?.fields ?? {}));
|
|
66171
66488
|
if (p === "/api/aaa/um-settings")
|
|
66172
|
-
return
|
|
66489
|
+
return json15(await setUmSettings(ctx, body?.fields ?? {}));
|
|
66173
66490
|
if (p === "/api/aaa/radius-reset-counters")
|
|
66174
|
-
return
|
|
66491
|
+
return json15(await resetRadiusCounters(ctx));
|
|
66175
66492
|
const slug = body?.slug ?? "";
|
|
66176
66493
|
if (!AAA_ENTITIES[slug])
|
|
66177
|
-
return
|
|
66494
|
+
return json15({ error: "unknown entity" }, 404);
|
|
66178
66495
|
if (p === "/api/aaa/add")
|
|
66179
|
-
return
|
|
66496
|
+
return json15(await addAaaEntity(ctx, slug, body?.fields ?? {}));
|
|
66180
66497
|
if (p === "/api/aaa/update") {
|
|
66181
66498
|
if (!body?.id)
|
|
66182
|
-
return
|
|
66183
|
-
return
|
|
66499
|
+
return json15({ error: "id required" }, 400);
|
|
66500
|
+
return json15(await updateAaaEntity(ctx, slug, body.id, body?.fields ?? {}));
|
|
66184
66501
|
}
|
|
66185
66502
|
if (p === "/api/aaa/remove") {
|
|
66186
66503
|
if (!body?.id)
|
|
66187
|
-
return
|
|
66188
|
-
return
|
|
66504
|
+
return json15({ error: "id required" }, 400);
|
|
66505
|
+
return json15(await removeAaaEntity(ctx, slug, body.id));
|
|
66189
66506
|
}
|
|
66190
66507
|
if (p === "/api/aaa/toggle") {
|
|
66191
66508
|
if (!body?.id)
|
|
66192
|
-
return
|
|
66193
|
-
return
|
|
66509
|
+
return json15({ error: "id required" }, 400);
|
|
66510
|
+
return json15(await toggleAaaEntity(ctx, slug, body.id, body?.enable === true));
|
|
66194
66511
|
}
|
|
66195
66512
|
}
|
|
66196
66513
|
return null;
|
|
@@ -66206,43 +66523,43 @@ async function usageRoutes(req, url) {
|
|
|
66206
66523
|
return null;
|
|
66207
66524
|
if (p === "/api/usage/sampler") {
|
|
66208
66525
|
if (req.method === "GET")
|
|
66209
|
-
return
|
|
66526
|
+
return json15({ intervalMs: getUsageSamplerInterval() });
|
|
66210
66527
|
if (req.method === "POST") {
|
|
66211
66528
|
const b = await readJson(req);
|
|
66212
66529
|
const applied = setUsageSamplerInterval(Number(b?.intervalMs));
|
|
66213
|
-
return
|
|
66530
|
+
return json15({ intervalMs: applied });
|
|
66214
66531
|
}
|
|
66215
66532
|
return null;
|
|
66216
66533
|
}
|
|
66217
66534
|
if (req.method !== "GET")
|
|
66218
66535
|
return null;
|
|
66219
66536
|
if (!usageStore)
|
|
66220
|
-
return
|
|
66537
|
+
return json15({ error: "usage store not active" }, 503);
|
|
66221
66538
|
const device = resolveDeviceName(url.searchParams.get("device") ?? undefined);
|
|
66222
66539
|
const sinceTs = (days) => Date.now() - days * 86400000;
|
|
66223
66540
|
if (p === "/api/usage/um-users") {
|
|
66224
|
-
return
|
|
66541
|
+
return json15({ users: usageStore.umUsers(device) });
|
|
66225
66542
|
}
|
|
66226
66543
|
if (p === "/api/usage/client") {
|
|
66227
66544
|
const ip = url.searchParams.get("ip");
|
|
66228
66545
|
if (!ip)
|
|
66229
|
-
return
|
|
66546
|
+
return json15({ error: "ip required" }, 400);
|
|
66230
66547
|
const series = usageStore.clientDailyUsage(device, ip, sinceTs(daysParam(url, 90, 400)));
|
|
66231
|
-
return
|
|
66548
|
+
return json15(withTotals(series));
|
|
66232
66549
|
}
|
|
66233
66550
|
if (p === "/api/usage/um-user") {
|
|
66234
66551
|
const user = url.searchParams.get("user");
|
|
66235
66552
|
if (!user)
|
|
66236
|
-
return
|
|
66553
|
+
return json15({ error: "user required" }, 400);
|
|
66237
66554
|
const series = usageStore.umUserDailyUsage(device, user, sinceTs(daysParam(url, 90, 400)));
|
|
66238
|
-
return
|
|
66555
|
+
return json15(withTotals(series));
|
|
66239
66556
|
}
|
|
66240
66557
|
if (p === "/api/usage/heatmap") {
|
|
66241
66558
|
const user = url.searchParams.get("user");
|
|
66242
66559
|
const days = usageStore.heatmap(device, user || null, sinceTs(daysParam(url, 371, 400)));
|
|
66243
66560
|
const total = days.reduce((s, d) => s + d.count, 0);
|
|
66244
66561
|
const max = days.reduce((m, d) => Math.max(m, d.count), 0);
|
|
66245
|
-
return
|
|
66562
|
+
return json15({ days, total, max });
|
|
66246
66563
|
}
|
|
66247
66564
|
return null;
|
|
66248
66565
|
}
|
|
@@ -66266,13 +66583,13 @@ async function featureRoutes(req, url) {
|
|
|
66266
66583
|
const cfg = getConfig();
|
|
66267
66584
|
const all = Object.keys(cfg.devices).flatMap((d) => store.list(d, 200, false));
|
|
66268
66585
|
all.sort((a, b) => b.ts - a.ts);
|
|
66269
|
-
return
|
|
66586
|
+
return json15({ snapshots: all });
|
|
66270
66587
|
}
|
|
66271
66588
|
const snapMatch = p.match(/^\/api\/snapshot\/(.+)$/);
|
|
66272
66589
|
if (snapMatch && req.method === "GET") {
|
|
66273
66590
|
const store = await snapStore();
|
|
66274
66591
|
const s = store.get(decodeURIComponent(snapMatch[1]));
|
|
66275
|
-
return s ?
|
|
66592
|
+
return s ? json15(s) : json15({ error: "not found" }, 404);
|
|
66276
66593
|
}
|
|
66277
66594
|
if (p === "/api/snapshots/diff" && req.method === "POST") {
|
|
66278
66595
|
const store = await snapStore();
|
|
@@ -66280,13 +66597,13 @@ async function featureRoutes(req, url) {
|
|
|
66280
66597
|
const from = b?.from ? store.get(b.from) : null;
|
|
66281
66598
|
const to = b?.to ? store.get(b.to) : null;
|
|
66282
66599
|
if (!from || !to) {
|
|
66283
|
-
return
|
|
66600
|
+
return json15({ error: "both 'from' and 'to' snapshot ids are required" }, 400);
|
|
66284
66601
|
}
|
|
66285
66602
|
const diff = diffLines(normalizeExport(from.body), normalizeExport(to.body), {
|
|
66286
66603
|
fromLabel: from.label ?? from.id,
|
|
66287
66604
|
toLabel: to.label ?? to.id
|
|
66288
66605
|
});
|
|
66289
|
-
return
|
|
66606
|
+
return json15({
|
|
66290
66607
|
from: { id: from.id, label: from.label, ts: from.ts, device: from.device },
|
|
66291
66608
|
to: { id: to.id, label: to.label, ts: to.ts, device: to.device },
|
|
66292
66609
|
summary: diff.summary,
|
|
@@ -66300,16 +66617,16 @@ async function featureRoutes(req, url) {
|
|
|
66300
66617
|
...b?.script ? splitCommands(b.script) : []
|
|
66301
66618
|
];
|
|
66302
66619
|
if (commands.length === 0)
|
|
66303
|
-
return
|
|
66620
|
+
return json15({ error: "no commands provided" }, 400);
|
|
66304
66621
|
const plan = buildChangePlan(commands);
|
|
66305
|
-
return
|
|
66622
|
+
return json15({ plan, text: renderPlan(plan) });
|
|
66306
66623
|
}
|
|
66307
66624
|
if (p === "/api/s3" && req.method === "GET") {
|
|
66308
|
-
return
|
|
66625
|
+
return json15({ configured: isS3Configured(), target: isS3Configured() ? s3Target() : null });
|
|
66309
66626
|
}
|
|
66310
66627
|
if (p === "/api/s3/list" && req.method === "GET") {
|
|
66311
66628
|
if (!isS3Configured())
|
|
66312
|
-
return
|
|
66629
|
+
return json15({ configured: false, objects: [] });
|
|
66313
66630
|
const prefix = url.searchParams.get("prefix") ?? "";
|
|
66314
66631
|
try {
|
|
66315
66632
|
const res = await getS3Client().list({ prefix: prefix || undefined, maxKeys: 1000 });
|
|
@@ -66318,42 +66635,42 @@ async function featureRoutes(req, url) {
|
|
|
66318
66635
|
size: o.size ?? 0,
|
|
66319
66636
|
lastModified: o.lastModified ?? null
|
|
66320
66637
|
}));
|
|
66321
|
-
return
|
|
66638
|
+
return json15({ configured: true, target: s3Target(), objects, truncated: !!res.isTruncated });
|
|
66322
66639
|
} catch (e) {
|
|
66323
|
-
return
|
|
66640
|
+
return json15({ error: clientError(e) }, 502);
|
|
66324
66641
|
}
|
|
66325
66642
|
}
|
|
66326
66643
|
if (p === "/api/s3/presign" && req.method === "GET") {
|
|
66327
66644
|
if (!isS3Configured())
|
|
66328
|
-
return
|
|
66645
|
+
return json15({ error: "S3 not configured" }, 400);
|
|
66329
66646
|
const key = url.searchParams.get("key");
|
|
66330
66647
|
if (!key)
|
|
66331
|
-
return
|
|
66648
|
+
return json15({ error: "key required" }, 400);
|
|
66332
66649
|
try {
|
|
66333
66650
|
const link = getS3Client().presign(key, { expiresIn: presignExpiresIn(), method: "GET" });
|
|
66334
|
-
return
|
|
66651
|
+
return json15({ url: link });
|
|
66335
66652
|
} catch (e) {
|
|
66336
|
-
return
|
|
66653
|
+
return json15({ error: clientError(e) }, 502);
|
|
66337
66654
|
}
|
|
66338
66655
|
}
|
|
66339
66656
|
if (p === "/api/s3/delete" && req.method === "POST") {
|
|
66340
66657
|
if (!isS3Configured())
|
|
66341
|
-
return
|
|
66658
|
+
return json15({ error: "S3 not configured" }, 400);
|
|
66342
66659
|
const b = await readJson(req);
|
|
66343
66660
|
if (!b?.key)
|
|
66344
|
-
return
|
|
66661
|
+
return json15({ error: "key required" }, 400);
|
|
66345
66662
|
try {
|
|
66346
66663
|
const client = getS3Client();
|
|
66347
66664
|
if (!await client.exists(b.key))
|
|
66348
|
-
return
|
|
66665
|
+
return json15({ error: "not found" }, 404);
|
|
66349
66666
|
await client.delete(b.key);
|
|
66350
|
-
return
|
|
66667
|
+
return json15({ ok: true, key: b.key });
|
|
66351
66668
|
} catch (e) {
|
|
66352
|
-
return
|
|
66669
|
+
return json15({ error: clientError(e) }, 502);
|
|
66353
66670
|
}
|
|
66354
66671
|
}
|
|
66355
66672
|
if (p === "/api/backups" && req.method === "GET") {
|
|
66356
|
-
return
|
|
66673
|
+
return json15({
|
|
66357
66674
|
dir: backupDir(),
|
|
66358
66675
|
devices: Object.keys(getConfig().devices),
|
|
66359
66676
|
backups: listBackups()
|
|
@@ -66363,14 +66680,14 @@ async function featureRoutes(req, url) {
|
|
|
66363
66680
|
const b = await readJson(req);
|
|
66364
66681
|
const raw = b?.dir?.trim();
|
|
66365
66682
|
if (!raw)
|
|
66366
|
-
return
|
|
66683
|
+
return json15({ error: "dir required" }, 400);
|
|
66367
66684
|
const dir = raw === "~" || raw.startsWith("~/") ? join12(homedir5(), raw.slice(1)) : raw;
|
|
66368
66685
|
const next = { ...getConfig(), backupDir: dir };
|
|
66369
66686
|
setConfig(next);
|
|
66370
66687
|
try {
|
|
66371
66688
|
atomicWrite(getConfigSource().path, serializeConfig(next));
|
|
66372
66689
|
} catch (e) {
|
|
66373
|
-
return
|
|
66690
|
+
return json15({
|
|
66374
66691
|
ok: true,
|
|
66375
66692
|
dir: backupDir(),
|
|
66376
66693
|
persisted: false,
|
|
@@ -66378,16 +66695,16 @@ async function featureRoutes(req, url) {
|
|
|
66378
66695
|
});
|
|
66379
66696
|
}
|
|
66380
66697
|
recordVersion(getConfig(), "auto", Date.now(), "backup path changed");
|
|
66381
|
-
return
|
|
66698
|
+
return json15({ ok: true, dir: backupDir(), persisted: true });
|
|
66382
66699
|
}
|
|
66383
66700
|
if (p === "/api/backups/get" && req.method === "GET") {
|
|
66384
66701
|
const name = url.searchParams.get("name");
|
|
66385
66702
|
if (!name)
|
|
66386
|
-
return
|
|
66703
|
+
return json15({ error: "name required" }, 400);
|
|
66387
66704
|
try {
|
|
66388
|
-
return
|
|
66705
|
+
return json15({ name, content: readBackup(name) });
|
|
66389
66706
|
} catch {
|
|
66390
|
-
return
|
|
66707
|
+
return json15({ error: "not found" }, 404);
|
|
66391
66708
|
}
|
|
66392
66709
|
}
|
|
66393
66710
|
if (p === "/api/backups/raw" && req.method === "GET") {
|
|
@@ -66408,41 +66725,41 @@ async function featureRoutes(req, url) {
|
|
|
66408
66725
|
if (p === "/api/backups/upload" && req.method === "POST") {
|
|
66409
66726
|
const b = await readJson(req);
|
|
66410
66727
|
if (!b?.name || typeof b.content !== "string") {
|
|
66411
|
-
return
|
|
66728
|
+
return json15({ error: "name and content are required" }, 400);
|
|
66412
66729
|
}
|
|
66413
66730
|
try {
|
|
66414
66731
|
const safe = b.name.endsWith(".rsc") ? b.name : `${b.name}.rsc`;
|
|
66415
|
-
return
|
|
66732
|
+
return json15({ ok: true, name: writeBackup(safe, b.content) });
|
|
66416
66733
|
} catch (e) {
|
|
66417
|
-
return
|
|
66734
|
+
return json15({ error: clientError(e) }, 400);
|
|
66418
66735
|
}
|
|
66419
66736
|
}
|
|
66420
66737
|
if (p === "/api/backups/rename" && req.method === "POST") {
|
|
66421
66738
|
const b = await readJson(req);
|
|
66422
66739
|
if (!b?.name || !b?.new_name)
|
|
66423
|
-
return
|
|
66740
|
+
return json15({ error: "name and new_name are required" }, 400);
|
|
66424
66741
|
try {
|
|
66425
|
-
return
|
|
66742
|
+
return json15({ ok: true, name: renameBackup(b.name, b.new_name) });
|
|
66426
66743
|
} catch (e) {
|
|
66427
|
-
return
|
|
66744
|
+
return json15({ error: clientError(e) }, 400);
|
|
66428
66745
|
}
|
|
66429
66746
|
}
|
|
66430
66747
|
if (p === "/api/backups/delete" && req.method === "POST") {
|
|
66431
66748
|
const b = await readJson(req);
|
|
66432
66749
|
if (!b?.name)
|
|
66433
|
-
return
|
|
66750
|
+
return json15({ error: "name required" }, 400);
|
|
66434
66751
|
try {
|
|
66435
|
-
return deleteBackup(b.name) ?
|
|
66752
|
+
return deleteBackup(b.name) ? json15({ ok: true }) : json15({ error: "not found" }, 404);
|
|
66436
66753
|
} catch (e) {
|
|
66437
|
-
return
|
|
66754
|
+
return json15({ error: clientError(e) }, 400);
|
|
66438
66755
|
}
|
|
66439
66756
|
}
|
|
66440
66757
|
if (p === "/api/backups/restore" && req.method === "POST") {
|
|
66441
66758
|
const b = await readJson(req);
|
|
66442
66759
|
if (!b?.name)
|
|
66443
|
-
return
|
|
66760
|
+
return json15({ error: "name required" }, 400);
|
|
66444
66761
|
const device = resolveDeviceName(b.device);
|
|
66445
|
-
return
|
|
66762
|
+
return json15(await restoreLocalBackup(device, b.name, b.confirm === true));
|
|
66446
66763
|
}
|
|
66447
66764
|
if (p === "/api/backups/create" && req.method === "POST") {
|
|
66448
66765
|
const b = await readJson(req);
|
|
@@ -66454,7 +66771,7 @@ async function featureRoutes(req, url) {
|
|
|
66454
66771
|
compact: b?.compact === true,
|
|
66455
66772
|
terse: b?.terse === true
|
|
66456
66773
|
});
|
|
66457
|
-
return r.ok ?
|
|
66774
|
+
return r.ok ? json15(r) : json15({ error: r.error ?? "export failed" }, 502);
|
|
66458
66775
|
}
|
|
66459
66776
|
return null;
|
|
66460
66777
|
}
|
|
@@ -66515,6 +66832,7 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
66515
66832
|
const bearer = req.headers.get("authorization")?.replace(/^Bearer\s+/i, "");
|
|
66516
66833
|
return bearer === cfg.token || url.searchParams.get("token") === cfg.token;
|
|
66517
66834
|
};
|
|
66835
|
+
const accessSettingsRoutes = createAccessSettingsRoutes(configAdmin);
|
|
66518
66836
|
async function dashboardRoute(req, url, srv, ca, tl) {
|
|
66519
66837
|
if (url.pathname === "/api/stream") {
|
|
66520
66838
|
const traffic = url.searchParams.has("traffic") ? url.searchParams.get("traffic") ?? "" : undefined;
|
|
@@ -66526,6 +66844,9 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
66526
66844
|
const traffic = url.searchParams.has("traffic") ? url.searchParams.get("traffic") ?? "" : undefined;
|
|
66527
66845
|
return traffic === undefined ? sseResponse(tl) : sseTrafficResponse(traffic, tl);
|
|
66528
66846
|
}
|
|
66847
|
+
const accessResp = await accessSettingsRoutes(req, url);
|
|
66848
|
+
if (accessResp)
|
|
66849
|
+
return accessResp;
|
|
66529
66850
|
const configResp = await configRoutes(req, url, ca);
|
|
66530
66851
|
if (configResp)
|
|
66531
66852
|
return configResp;
|
|
@@ -66597,7 +66918,7 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
66597
66918
|
return memoryResp;
|
|
66598
66919
|
const db = getEventStore();
|
|
66599
66920
|
if (!db)
|
|
66600
|
-
return
|
|
66921
|
+
return json15({ error: "recorder not active" }, 503);
|
|
66601
66922
|
if (url.pathname === "/api/events" && req.method === "DELETE") {
|
|
66602
66923
|
let body = {};
|
|
66603
66924
|
try {
|
|
@@ -66605,7 +66926,7 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
66605
66926
|
} catch {}
|
|
66606
66927
|
const ids = Array.isArray(body.ids) ? body.ids.filter((x) => typeof x === "string") : [];
|
|
66607
66928
|
const removed = body.all === true ? db.clear() : db.delete(ids);
|
|
66608
|
-
return
|
|
66929
|
+
return json15({ removed, total: db.total() });
|
|
66609
66930
|
}
|
|
66610
66931
|
if (url.pathname === "/api/ssh-pool") {
|
|
66611
66932
|
const cfg = getConfig();
|
|
@@ -66614,7 +66935,7 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
66614
66935
|
const totalInflight = ps.reduce((s, p) => s + p.inflight, 0);
|
|
66615
66936
|
const totalIdle = ps.filter((p) => p.idle).length;
|
|
66616
66937
|
const totalBusy = ps.filter((p) => p.inflight > 0).length;
|
|
66617
|
-
return
|
|
66938
|
+
return json15({
|
|
66618
66939
|
enabled,
|
|
66619
66940
|
config: {
|
|
66620
66941
|
keepAlive: cfg.ssh.keepAlive,
|
|
@@ -66643,39 +66964,39 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
66643
66964
|
});
|
|
66644
66965
|
}
|
|
66645
66966
|
if (url.pathname === "/api/devices") {
|
|
66646
|
-
return
|
|
66967
|
+
return json15(devicesPayload(db));
|
|
66647
66968
|
}
|
|
66648
66969
|
if (url.pathname === "/api/capabilities") {
|
|
66649
66970
|
const devices = Object.keys(getConfig().devices).map((name) => ({
|
|
66650
66971
|
device: name,
|
|
66651
66972
|
capabilities: serializeCapabilities(peekCapabilities(name))
|
|
66652
66973
|
}));
|
|
66653
|
-
return
|
|
66974
|
+
return json15({ devices });
|
|
66654
66975
|
}
|
|
66655
66976
|
if (url.pathname === "/api/capabilities/refresh" && req.method === "POST") {
|
|
66656
66977
|
const b = await readJson(req);
|
|
66657
66978
|
if (typeof b?.device !== "string")
|
|
66658
|
-
return
|
|
66979
|
+
return json15({ error: "device (string) is required" }, 400);
|
|
66659
66980
|
if (!(b.device in getConfig().devices)) {
|
|
66660
|
-
return
|
|
66981
|
+
return json15({ error: `unknown device: ${b.device}` }, 404);
|
|
66661
66982
|
}
|
|
66662
66983
|
invalidateCapabilities(b.device);
|
|
66663
66984
|
try {
|
|
66664
66985
|
const caps = await getCapabilities(b.device);
|
|
66665
|
-
return
|
|
66986
|
+
return json15({ device: b.device, capabilities: serializeCapabilities(caps) });
|
|
66666
66987
|
} catch (e) {
|
|
66667
66988
|
logger.error(`Capability probe failed for '${b.device}': ${logError(e)}`);
|
|
66668
|
-
return
|
|
66989
|
+
return json15({ error: clientError(e) }, 502);
|
|
66669
66990
|
}
|
|
66670
66991
|
}
|
|
66671
66992
|
if (url.pathname === "/api/devices/toggle" && req.method === "POST") {
|
|
66672
66993
|
const b = await readJson(req);
|
|
66673
66994
|
if (typeof b?.device !== "string" || typeof b?.disabled !== "boolean") {
|
|
66674
|
-
return
|
|
66995
|
+
return json15({ error: "device (string) and disabled (boolean) are required" }, 400);
|
|
66675
66996
|
}
|
|
66676
66997
|
const cfg = getConfig();
|
|
66677
66998
|
if (!(b.device in cfg.devices))
|
|
66678
|
-
return
|
|
66999
|
+
return json15({ error: `unknown device: ${b.device}` }, 404);
|
|
66679
67000
|
const dc = cfg.devices[b.device];
|
|
66680
67001
|
const next = {
|
|
66681
67002
|
...cfg,
|
|
@@ -66696,7 +67017,7 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
66696
67017
|
if (persisted) {
|
|
66697
67018
|
recordVersion(getConfig(), "auto", Date.now(), `device ${b.disabled ? "disabled" : "enabled"}: ${b.device}`);
|
|
66698
67019
|
}
|
|
66699
|
-
return
|
|
67020
|
+
return json15({
|
|
66700
67021
|
ok: true,
|
|
66701
67022
|
persisted,
|
|
66702
67023
|
requiresReconnect: true,
|
|
@@ -66709,17 +67030,17 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
66709
67030
|
const name = typeof b?.device === "string" ? b.device : "";
|
|
66710
67031
|
const cfg = getConfig();
|
|
66711
67032
|
if (!(name in cfg.devices))
|
|
66712
|
-
return
|
|
67033
|
+
return json15({ error: `unknown device: ${name}` }, 404);
|
|
66713
67034
|
if (url.pathname === "/api/devices/reconnect")
|
|
66714
67035
|
closeDevice(name);
|
|
66715
67036
|
const status = await probeDevice2(name, cfg.devices[name]);
|
|
66716
|
-
return
|
|
67037
|
+
return json15({ ok: true, status, ...devicesPayload(db) });
|
|
66717
67038
|
}
|
|
66718
67039
|
if (url.pathname === "/api/topology") {
|
|
66719
|
-
return
|
|
67040
|
+
return json15(topologyPayload());
|
|
66720
67041
|
}
|
|
66721
67042
|
if (url.pathname === "/api/config") {
|
|
66722
|
-
return
|
|
67043
|
+
return json15(configPayload());
|
|
66723
67044
|
}
|
|
66724
67045
|
if (url.pathname === "/" || url.pathname === "/index.html") {
|
|
66725
67046
|
return new Response(dashboardHtml(), {
|
|
@@ -66728,9 +67049,9 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
66728
67049
|
}
|
|
66729
67050
|
if (url.pathname === "/api/releases/latest") {
|
|
66730
67051
|
try {
|
|
66731
|
-
return
|
|
67052
|
+
return json15(await fetchLatestRelease());
|
|
66732
67053
|
} catch (e) {
|
|
66733
|
-
return
|
|
67054
|
+
return json15({ error: clientError(e, "fetch failed") }, 502);
|
|
66734
67055
|
}
|
|
66735
67056
|
}
|
|
66736
67057
|
if (url.pathname === "/api/catalog" && req.method === "GET") {
|
|
@@ -66749,7 +67070,7 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
66749
67070
|
}));
|
|
66750
67071
|
const prompts = listPrompts();
|
|
66751
67072
|
const groups = [...new Set(modules.map((m) => m.group))].sort();
|
|
66752
|
-
return
|
|
67073
|
+
return json15({
|
|
66753
67074
|
modules,
|
|
66754
67075
|
prompts,
|
|
66755
67076
|
groups,
|
|
@@ -66764,25 +67085,25 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
66764
67085
|
}
|
|
66765
67086
|
if (url.pathname === "/api/releases" && req.method === "GET") {
|
|
66766
67087
|
try {
|
|
66767
|
-
return
|
|
67088
|
+
return json15(await fetchAllReleases());
|
|
66768
67089
|
} catch (e) {
|
|
66769
|
-
return
|
|
67090
|
+
return json15({ error: clientError(e, "fetch failed") }, 502);
|
|
66770
67091
|
}
|
|
66771
67092
|
}
|
|
66772
67093
|
if (url.pathname === "/api/upgrade" && req.method === "POST") {
|
|
66773
67094
|
const body = await readJson(req);
|
|
66774
67095
|
const version = String(body?.version ?? "latest");
|
|
66775
67096
|
if (version !== "latest" && !/^\d+\.\d+\.\d+$/.test(version)) {
|
|
66776
|
-
return
|
|
67097
|
+
return json15({ ok: false, error: `Invalid version "${version}" (use "latest" or x.y.z).` }, 400);
|
|
66777
67098
|
}
|
|
66778
67099
|
const spec = `@usex/mikrotik-mcp@${version}`;
|
|
66779
67100
|
logger.warn(`Dashboard requested upgrade: bun i -g ${spec}`);
|
|
66780
67101
|
const { ok, log } = await runUpgrade(spec);
|
|
66781
67102
|
if (!ok)
|
|
66782
|
-
return
|
|
67103
|
+
return json15({ ok: false, version, log }, 500);
|
|
66783
67104
|
const willRestart = body?.restart !== false;
|
|
66784
67105
|
const relaunched = willRestart ? restartProcess() : false;
|
|
66785
|
-
return
|
|
67106
|
+
return json15({
|
|
66786
67107
|
ok: true,
|
|
66787
67108
|
version,
|
|
66788
67109
|
log,
|
|
@@ -66792,7 +67113,7 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
66792
67113
|
}
|
|
66793
67114
|
if (url.pathname === "/api/meta") {
|
|
66794
67115
|
const f = facets(db);
|
|
66795
|
-
return
|
|
67116
|
+
return json15({
|
|
66796
67117
|
...f,
|
|
66797
67118
|
version: VERSION,
|
|
66798
67119
|
risks: ["READ", "WRITE", "WRITE_IDEMPOTENT", "DESTRUCTIVE", "DANGEROUS"],
|
|
@@ -66803,19 +67124,19 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
66803
67124
|
}
|
|
66804
67125
|
if (url.pathname === "/api/events") {
|
|
66805
67126
|
const filter = filterFromQuery(url);
|
|
66806
|
-
return
|
|
67127
|
+
return json15({ events: db.query(filter), total: db.total() });
|
|
66807
67128
|
}
|
|
66808
67129
|
const eventMatch = url.pathname.match(/^\/api\/event\/(.+)$/);
|
|
66809
67130
|
if (eventMatch) {
|
|
66810
67131
|
const e = db.get(decodeURIComponent(eventMatch[1]));
|
|
66811
|
-
return e ?
|
|
67132
|
+
return e ? json15(e) : json15({ error: "not found" }, 404);
|
|
66812
67133
|
}
|
|
66813
67134
|
if (url.pathname === "/api/stats") {
|
|
66814
67135
|
const now = Date.now();
|
|
66815
67136
|
const windowMs = Number(url.searchParams.get("window") ?? 3600000);
|
|
66816
67137
|
const buckets = Number(url.searchParams.get("buckets") ?? 60);
|
|
66817
67138
|
const events = db.query({ since: now - windowMs, limit: 5000 });
|
|
66818
|
-
return
|
|
67139
|
+
return json15(computeStats(events, { now, windowMs, buckets }));
|
|
66819
67140
|
}
|
|
66820
67141
|
return new Response("Not Found", { status: 404 });
|
|
66821
67142
|
}
|
|
@@ -66834,7 +67155,7 @@ async function runDashboard(cfg, transportLabel) {
|
|
|
66834
67155
|
return await dashboardRoute(req, url, srv, configAdmin, transportLabel);
|
|
66835
67156
|
} catch (e) {
|
|
66836
67157
|
logger.error(`Dashboard request failed (${url.pathname}): ${logError(e)}`);
|
|
66837
|
-
return
|
|
67158
|
+
return json15({ error: clientError(e) }, 502);
|
|
66838
67159
|
}
|
|
66839
67160
|
},
|
|
66840
67161
|
websocket: {
|