@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/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 getAccessPolicy() {
17924
+ function previewAccessPolicy(policy) {
17798
17925
  if (!sessionNarrowing)
17799
- return basePolicy;
17926
+ return policy;
17800
17927
  return {
17801
17928
  enabled: true,
17802
- scope: narrowScope(basePolicy.scope, sessionNarrowing)
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 view = buildRecordsView(def.name, def.title, out.text, new Date().toISOString());
19572
- if (view.rows.length > 0) {
19573
- out.structuredContent = view;
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 && !auto ? { outputSchema: UI_OUTPUT_SCHEMA } : {}
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-ejryxvb3.js");
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-ejryxvb3.js");
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 json4(body, status = 200) {
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 json4({ transactions, live: liveTxnUpdates(), error: storeError });
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 json4({ error: "not found" }, 404);
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 json4({ error: `no open transaction '${id}'` }, 404);
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 json4({
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 json4({ error: `unknown transaction '${id}'` }, 404);
64191
- return json4({ transaction: record, events: store?.events(id) ?? [] });
64525
+ return json5({ error: `unknown transaction '${id}'` }, 404);
64526
+ return json5({ transaction: record, events: store?.events(id) ?? [] });
64192
64527
  }
64193
- return json4({ error: "method not allowed" }, 405);
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 json5(body, status = 200) {
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 json5({ error: "method not allowed" }, 405);
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 json5({ collector: collector.stats(), store });
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 json5({ error: e instanceof Error ? e.message : String(e), records: [] }, 200);
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 json5({
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 json5({
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 json5({
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 json5({ error: "not found" }, 404);
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 json6(body, status = 200) {
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 json6({ rollouts, live: liveRolloutUpdates(), error: storeError });
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 json6({ error: "not found" }, 404);
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 json6({ error: `no active rollout '${id}'` }, 404);
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 json6({ held: true, summary: summarize5(entry.state) });
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 json6({ outcome: run.outcome, summary: run.summary });
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 json6({ outcome: run.outcome, summary: run.summary });
64704
+ return json7({ outcome: run.outcome, summary: run.summary });
64370
64705
  }
64371
64706
  default:
64372
- return json6({ error: `unknown action '${verb}'` }, 404);
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 json6({ error: `unknown rollout '${id}'` }, 404);
64379
- return json6({ rollout: record, events: store?.events(id) ?? [] });
64713
+ return json7({ error: `unknown rollout '${id}'` }, 404);
64714
+ return json7({ rollout: record, events: store?.events(id) ?? [] });
64380
64715
  }
64381
- return json6({ error: "method not allowed" }, 405);
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 json7(body, status = 200) {
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 json7({
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 json7({ results: [], error: e instanceof Error ? e.message : String(e) });
64839
+ return json8({ results: [], error: e instanceof Error ? e.message : String(e) });
64505
64840
  }
64506
- return json7({ results });
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 json7({ error: "content is required" }, 400);
64512
- return json7(validatePolicyText(body.content));
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 json7({ error: "no policy rules are loaded", reports: [] });
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 json7({ reports });
64882
+ return json8({ reports });
64548
64883
  }
64549
- return json7({ error: "not found" }, 404);
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 json8(body, status = 200) {
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 json8({ suites: await listSuites() });
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 json8({ error: "name and a non-empty packets array are required" }, 400);
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 json8({ suite: saved });
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 json8(config, 400);
64940
+ return json9(config, 400);
64606
64941
  const model = buildModel(config.text);
64607
64942
  const result = tracePacket({ model, packet: body.packet });
64608
- return json8({
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 json8(config, 400);
64957
+ return json9(config, 400);
64623
64958
  if (!body.changes || body.changes.trim() === "") {
64624
- return json8({ error: "changes are required" }, 400);
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 json8({
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 json8(config, 400);
64981
+ return json9(config, 400);
64647
64982
  const model = buildModel(config.text);
64648
64983
  const dead = unreachableRules(model.filter);
64649
- return json8({
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 json8({ error: `no suite '${id}'` }, 404);
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 json8(config, 400);
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 json8({
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 json8({ error: "not found" }, 404);
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 json9(body, status = 200) {
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 json9({ error: e instanceof Error ? e.message : String(e), jobs: [] }, 503);
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 json9({
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 json9({ error: "invalid JSON body" }, 400);
65124
+ return json10({ error: "invalid JSON body" }, 400);
64790
65125
  const parsed = JobSchema.safeParse(body);
64791
65126
  if (!parsed.success) {
64792
- return json9({
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 json9({ error: `'${parsed.data.tool}' is not a schedulable auditor` }, 400);
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 json9({ error: `'${parsed.data.tool}' is annotated ${risk}; only READ may be scheduled` }, 400);
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 json9({ job: jobRow(job, store.runs(job.id, 500), now), replaced: existing !== null });
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 json9({
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 json9({ regressions: items });
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 json9({ error: "not found" }, 404);
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 json9({ error: `no schedule '${id}'` }, 404);
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 json9({ removed: id, runsDiscarded: runCount });
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 json9({ error: `no schedule '${id}'` }, 404);
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 json9({
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 json9({ error: `no schedule '${id}'` }, 404);
65205
+ return json10({ error: `no schedule '${id}'` }, 404);
64871
65206
  const runs = store.runs(id, 500);
64872
- return json9({ job: jobRow(job, runs, now), runs, regressions: regressionsFor(runs, id) });
65207
+ return json10({ job: jobRow(job, runs, now), runs, regressions: regressionsFor(runs, id) });
64873
65208
  }
64874
- return json9({ error: "not found" }, 404);
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 json10(body, status = 200) {
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 json10({ error: "invalid JSON body" }, 400);
65255
+ return json11({ error: "invalid JSON body" }, 400);
64921
65256
  if (!body.before && !body.beforeText) {
64922
- return json10({ error: "a diff needs a baseline: pass `before` (a snapshot id) or `beforeText`" }, 400);
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 json10({ error: `before: ${before.error}` }, before.status);
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 json10({ error: `after: ${after.error}` }, after.status);
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 json10({ diff, markdown: renderDiff(diff), before: before.source, after: after.source });
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 json10({ error: "not found" }, 404);
65271
+ return json11({ error: "not found" }, 404);
64937
65272
  if (req.method !== "GET")
64938
- return json10({ error: "not found" }, 404);
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 json10({ error: e instanceof Error ? e.message : String(e) }, 502);
65280
+ return json11({ error: e instanceof Error ? e.message : String(e) }, 502);
64946
65281
  }
64947
65282
  if ("error" in config)
64948
- return json10({ error: config.error }, config.status);
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 json10(payload(narrative, config.source, renderNarrative(narrative)));
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 json10({ error: `unknown section '${section}'. Known: ${NARRATIVE_SECTIONS.join(", ")}` }, 400);
65291
+ return json11({ error: `unknown section '${section}'. Known: ${NARRATIVE_SECTIONS.join(", ")}` }, 400);
64957
65292
  }
64958
- return json10(payload(narrative, config.source, renderNarrative(narrative, { sections: [section] })));
65293
+ return json11(payload(narrative, config.source, renderNarrative(narrative, { sections: [section] })));
64959
65294
  }
64960
- return json10({ error: "not found" }, 404);
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 json11(body, status = 200) {
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 json11({ error: e instanceof Error ? e.message : String(e), incidents: [] }, 503);
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 json11({
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 json11({
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 json11({
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 json11({
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 json11({
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 json11({
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 json11({ error: `no recorded block for ${address}` }, 404);
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 json11({ address, results });
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 json11({ error: "not found" }, 404);
65096
- return json11({ incident, response: store.responseFor(incident.source) });
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 json11({ error: "not found" }, 404);
65436
+ return json12({ error: "not found" }, 404);
65102
65437
  const body = await req.json().catch(() => ({}));
65103
65438
  if (body.action === "dismiss") {
65104
- return json11({ dismissed: incident.id });
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 json11({ refused: true, guard: decision.guard, reason: decision.reason }, 200);
65451
+ return json12({ refused: true, guard: decision.guard, reason: decision.reason }, 200);
65117
65452
  }
65118
65453
  if (decision.action === "escalate") {
65119
- return json11({ escalated: true, reason: decision.reason });
65454
+ return json12({ escalated: true, reason: decision.reason });
65120
65455
  }
65121
65456
  if (!body.confirm) {
65122
- return json11({ dryRun: true, plan: decision });
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 json11({ applied, ok, plan: decision });
65473
+ return json12({ applied, ok, plan: decision });
65139
65474
  }
65140
- return json11({ error: "not found" }, 404);
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 json12(body, status = 200) {
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 json12({ configured: false, rules: [], active: [], channels: {} });
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 json12({
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 json12({ history: [] });
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 json12({ history });
65555
+ return json13({ history });
65221
65556
  }
65222
65557
  if (p === "/api/alerts/rules" && req.method === "POST") {
65223
65558
  if (!engine)
65224
- return json12({ error: "alerting is not configured" }, 400);
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 json12({ error: parsed.error.issues.map((i) => i.message).join("; ") }, 400);
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 json12({ error: `rule '${parsed.data.id}' already exists` }, 409);
65566
+ return json13({ error: `rule '${parsed.data.id}' already exists` }, 409);
65232
65567
  }
65233
65568
  engine.setRules([...rules, parsed.data]);
65234
- return json12({ ok: true, id: parsed.data.id });
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 json12({ error: "alerting is not configured" }, 400);
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 json12({ error: `unknown rule: ${id}` }, 404);
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 json12({ ok: true });
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 json12({ error: `invalid duration: ${body.mute}` }, 400);
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 json12({ error: merged.error.issues.map((i) => i.message).join("; ") }, 400);
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 json12({ ok: true, rule: merged.data });
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 json12({ error: "channel is required" }, 400);
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 json12(result);
65615
+ return json13(result);
65281
65616
  } catch (e) {
65282
65617
  logger.error(`Alert channel test failed: ${logError(e)}`);
65283
- return json12({ error: clientError(e) }, 502);
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 json12({ error: parsed.error.issues.map((i) => i.message).join("; ") }, 400);
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 json12({ hours, sampled: 0, fires: 0, resolves: 0, matched: 0, noHistory: true });
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 json12({ hours, sampled: events.length, ...previewRule(parsed.data, events) });
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 json13(body, status = 200) {
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 json13({ error: "Knowledge-graph memory is disabled" }, 503);
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 json13({
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 json13({
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 json13({
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 json13(store.readGraph());
65709
+ return json14(store.readGraph());
65375
65710
  }
65376
65711
  if (p === "/api/memory/stats" && req.method === "GET") {
65377
- return json13(store.stats());
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 json13(store.searchNodes(q, limit));
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 json13(store.activity(limit, since ? Number(since) : undefined));
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 json13({ error: "Entity not found" }, 404);
65395
- return json13({ entity: graph.entities[0], relations: graph.relations });
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 json13({ created, count: created.length });
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 json13({ created, count: created.length });
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 json13({ results });
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 json13({ removed });
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 json13({ removed });
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 json13({ removed });
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 json14(body, status = 200) {
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
- const cfg = getConfig();
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 json14(configSchemaJson());
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 json14(validateConfig(merged));
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 json14({ ok: false, errors: issues(parsed.error) }, 400);
66064
+ return json15({ ok: false, errors: issues(parsed.error) }, 400);
65748
66065
  const status = await probeDevice2(`config-test:${name}`, parsed.data);
65749
- return json14({ ok: true, status });
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 json14({ summary: d.summary, unified: d.unified });
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 json14({ ok: false, errors: v.errors }, 400);
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 json14({ ok: true, ...res, devicesChanged, summary: diff.summary, unified: diff.unified });
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 json14({ kept });
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 json14({ rolledBack, config: redact(getConfig()) });
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 json14({
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 json14({ ok: true, mode: "config", count: devices.length, devices });
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 json14({ ok: false, error: clientError(e) }, 500);
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 json14({ versions, bytes: historyBytes(), retention: AUTO_RETENTION });
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 json14({ error: "id required" }, 400);
66141
+ return json15({ error: "id required" }, 400);
65825
66142
  try {
65826
66143
  const v = readVersion(id);
65827
- return json14({
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 json14({ error: "not found" }, 404);
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 json14({ error: "id required" }, 400);
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 json14({ summary: d.summary, unified: d.unified });
66162
+ return json15({ summary: d.summary, unified: d.unified });
65846
66163
  } catch {
65847
- return json14({ error: "not found" }, 404);
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 json14({ ok: true, version: recordVersion(getConfig(), "checkpoint", Date.now(), label) });
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 json14({ error: "id required" }, 400);
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 json14({ error: "not found" }, 404);
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 json14({ ok: false, errors: v.errors }, 400);
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 json14({ ok: true, persisted, restored: b.id, config: redact(getConfig()) });
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 json14({ error: "id required" }, 400);
65884
- return deleteVersion(b.id) ? json14({ ok: true }) : json14({ error: "not found" }, 404);
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 json14({
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 json14({ error: "slug (string) and enabled (boolean) are required" }, 400);
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 json14({ error: `unknown module: ${b.slug}` }, 404);
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 json14({
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 json14({ error: "enabled (boolean) is required" }, 400);
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 json14({
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 json14(capture2.stats());
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 json14({ packets: capture2.recent(limit), stats: capture2.stats() });
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 json14(await capture2.start(typeof body?.port === "number" ? body.port : DEFAULT_TZSP_PORT));
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 json14({ ok: true, ...capture2.stats() });
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 json14(capsmanOverview(await fetchCapsmanState(ctx)));
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 json14({ clients: state.clients, weak });
66325
+ return json15({ clients: state.clients, weak });
66009
66326
  }
66010
66327
  if (p === "/api/capsman/audit") {
66011
- return json14(runCapsmanAudit(await fetchCapsmanState(ctx)));
66328
+ return json15(runCapsmanAudit(await fetchCapsmanState(ctx)));
66012
66329
  }
66013
66330
  if (p === "/api/capsman/trends") {
66014
66331
  if (!capsmanStore)
66015
- return json14({ error: "capsman store not active" }, 503);
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 json14({ series, days });
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 json14({ ok: false, error: "client not associated" }, 400);
66347
+ return json15({ ok: false, error: "client not associated" }, 400);
66031
66348
  if (steerAlreadyPresent(state, body.mac ?? "")) {
66032
- return json14({ ok: true, message: "already steered (no-op)" });
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 json14({ ok: true, preview: commands });
66038
- return json14(await applyCapsmanWrites(ctx, commands, `pre-steer-${body.mac}`));
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 json14({ ok: true, preview: commands, plan });
66045
- return json14(await applyCapsmanWrites(ctx, commands, "pre-load-balance"));
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 json14({ ok: false, error: "channel-plan apply is v7 /interface wifi only" }, 400);
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 json14({ ok: true, preview: commands });
66054
- return json14(await applyCapsmanWrites(ctx, commands, "pre-channel-plan"));
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 json14({ ok: true, preview: commands });
66060
- return json14(await applyCapsmanWrites(ctx, commands, "pre-ft"));
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 json14({ ok: true, preview: commands, guidance });
66383
+ return json15({ ok: true, preview: commands, guidance });
66067
66384
  const res = await applyCapsmanWrites(ctx, commands, "pre-ha");
66068
- return json14({ ...res, guidance });
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 json14(devicesView(await fetchDevices(ctx)));
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 json14({ error: "ip required" }, 400);
66416
+ return json15({ error: "ip required" }, 400);
66100
66417
  const ctx = createContext(undefined, deviceFromQuery());
66101
- return json14(await sampleDeviceTraffic(ctx, ip));
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 json14(await sampleAllTraffic(ctx));
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 json14({ error: "ip required" }, 400);
66113
- return json14(await setDeviceLimits(ctx, b.ip, { download: b.download, upload: b.upload }));
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 json14({ error: "mac required" }, 400);
66433
+ return json15({ error: "mac required" }, 400);
66117
66434
  const run = async (op) => {
66118
66435
  const r = await op;
66119
- return json14({ ...r, view: r.ok ? devicesView(await fetchDevices(ctx)) : undefined });
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 json14({ error: "ip required" }, 400);
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 json14({ error: "label required" }, 400);
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 json14({ entities: AAA_ENTITIES });
66466
+ return json15({ entities: AAA_ENTITIES });
66150
66467
  }
66151
66468
  if (p === "/api/aaa/radius-incoming") {
66152
- return json14(await getRadiusIncoming(createContext(undefined, deviceFromQuery())));
66469
+ return json15(await getRadiusIncoming(createContext(undefined, deviceFromQuery())));
66153
66470
  }
66154
66471
  if (p === "/api/aaa/um-settings") {
66155
- return json14(await getUmSettings(createContext(undefined, deviceFromQuery())));
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 json14({ error: "unknown entity" }, 404);
66162
- return json14(await listAaaEntity(createContext(undefined, deviceFromQuery()), slug));
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 json14(await setRadiusIncoming(ctx, body?.fields ?? {}));
66487
+ return json15(await setRadiusIncoming(ctx, body?.fields ?? {}));
66171
66488
  if (p === "/api/aaa/um-settings")
66172
- return json14(await setUmSettings(ctx, body?.fields ?? {}));
66489
+ return json15(await setUmSettings(ctx, body?.fields ?? {}));
66173
66490
  if (p === "/api/aaa/radius-reset-counters")
66174
- return json14(await resetRadiusCounters(ctx));
66491
+ return json15(await resetRadiusCounters(ctx));
66175
66492
  const slug = body?.slug ?? "";
66176
66493
  if (!AAA_ENTITIES[slug])
66177
- return json14({ error: "unknown entity" }, 404);
66494
+ return json15({ error: "unknown entity" }, 404);
66178
66495
  if (p === "/api/aaa/add")
66179
- return json14(await addAaaEntity(ctx, slug, body?.fields ?? {}));
66496
+ return json15(await addAaaEntity(ctx, slug, body?.fields ?? {}));
66180
66497
  if (p === "/api/aaa/update") {
66181
66498
  if (!body?.id)
66182
- return json14({ error: "id required" }, 400);
66183
- return json14(await updateAaaEntity(ctx, slug, body.id, body?.fields ?? {}));
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 json14({ error: "id required" }, 400);
66188
- return json14(await removeAaaEntity(ctx, slug, body.id));
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 json14({ error: "id required" }, 400);
66193
- return json14(await toggleAaaEntity(ctx, slug, body.id, body?.enable === true));
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 json14({ intervalMs: getUsageSamplerInterval() });
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 json14({ intervalMs: applied });
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 json14({ error: "usage store not active" }, 503);
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 json14({ users: usageStore.umUsers(device) });
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 json14({ error: "ip required" }, 400);
66546
+ return json15({ error: "ip required" }, 400);
66230
66547
  const series = usageStore.clientDailyUsage(device, ip, sinceTs(daysParam(url, 90, 400)));
66231
- return json14(withTotals(series));
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 json14({ error: "user required" }, 400);
66553
+ return json15({ error: "user required" }, 400);
66237
66554
  const series = usageStore.umUserDailyUsage(device, user, sinceTs(daysParam(url, 90, 400)));
66238
- return json14(withTotals(series));
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 json14({ days, total, max });
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 json14({ snapshots: all });
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 ? json14(s) : json14({ error: "not found" }, 404);
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 json14({ error: "both 'from' and 'to' snapshot ids are required" }, 400);
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 json14({
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 json14({ error: "no commands provided" }, 400);
66620
+ return json15({ error: "no commands provided" }, 400);
66304
66621
  const plan = buildChangePlan(commands);
66305
- return json14({ plan, text: renderPlan(plan) });
66622
+ return json15({ plan, text: renderPlan(plan) });
66306
66623
  }
66307
66624
  if (p === "/api/s3" && req.method === "GET") {
66308
- return json14({ configured: isS3Configured(), target: isS3Configured() ? s3Target() : null });
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 json14({ configured: false, objects: [] });
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 json14({ configured: true, target: s3Target(), objects, truncated: !!res.isTruncated });
66638
+ return json15({ configured: true, target: s3Target(), objects, truncated: !!res.isTruncated });
66322
66639
  } catch (e) {
66323
- return json14({ error: clientError(e) }, 502);
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 json14({ error: "S3 not configured" }, 400);
66645
+ return json15({ error: "S3 not configured" }, 400);
66329
66646
  const key = url.searchParams.get("key");
66330
66647
  if (!key)
66331
- return json14({ error: "key required" }, 400);
66648
+ return json15({ error: "key required" }, 400);
66332
66649
  try {
66333
66650
  const link = getS3Client().presign(key, { expiresIn: presignExpiresIn(), method: "GET" });
66334
- return json14({ url: link });
66651
+ return json15({ url: link });
66335
66652
  } catch (e) {
66336
- return json14({ error: clientError(e) }, 502);
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 json14({ error: "S3 not configured" }, 400);
66658
+ return json15({ error: "S3 not configured" }, 400);
66342
66659
  const b = await readJson(req);
66343
66660
  if (!b?.key)
66344
- return json14({ error: "key required" }, 400);
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 json14({ error: "not found" }, 404);
66665
+ return json15({ error: "not found" }, 404);
66349
66666
  await client.delete(b.key);
66350
- return json14({ ok: true, key: b.key });
66667
+ return json15({ ok: true, key: b.key });
66351
66668
  } catch (e) {
66352
- return json14({ error: clientError(e) }, 502);
66669
+ return json15({ error: clientError(e) }, 502);
66353
66670
  }
66354
66671
  }
66355
66672
  if (p === "/api/backups" && req.method === "GET") {
66356
- return json14({
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 json14({ error: "dir required" }, 400);
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 json14({
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 json14({ ok: true, dir: backupDir(), persisted: true });
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 json14({ error: "name required" }, 400);
66703
+ return json15({ error: "name required" }, 400);
66387
66704
  try {
66388
- return json14({ name, content: readBackup(name) });
66705
+ return json15({ name, content: readBackup(name) });
66389
66706
  } catch {
66390
- return json14({ error: "not found" }, 404);
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 json14({ error: "name and content are required" }, 400);
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 json14({ ok: true, name: writeBackup(safe, b.content) });
66732
+ return json15({ ok: true, name: writeBackup(safe, b.content) });
66416
66733
  } catch (e) {
66417
- return json14({ error: clientError(e) }, 400);
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 json14({ error: "name and new_name are required" }, 400);
66740
+ return json15({ error: "name and new_name are required" }, 400);
66424
66741
  try {
66425
- return json14({ ok: true, name: renameBackup(b.name, b.new_name) });
66742
+ return json15({ ok: true, name: renameBackup(b.name, b.new_name) });
66426
66743
  } catch (e) {
66427
- return json14({ error: clientError(e) }, 400);
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 json14({ error: "name required" }, 400);
66750
+ return json15({ error: "name required" }, 400);
66434
66751
  try {
66435
- return deleteBackup(b.name) ? json14({ ok: true }) : json14({ error: "not found" }, 404);
66752
+ return deleteBackup(b.name) ? json15({ ok: true }) : json15({ error: "not found" }, 404);
66436
66753
  } catch (e) {
66437
- return json14({ error: clientError(e) }, 400);
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 json14({ error: "name required" }, 400);
66760
+ return json15({ error: "name required" }, 400);
66444
66761
  const device = resolveDeviceName(b.device);
66445
- return json14(await restoreLocalBackup(device, b.name, b.confirm === true));
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 ? json14(r) : json14({ error: r.error ?? "export failed" }, 502);
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 json14({ error: "recorder not active" }, 503);
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 json14({ removed, total: db.total() });
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 json14({
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 json14(devicesPayload(db));
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 json14({ devices });
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 json14({ error: "device (string) is required" }, 400);
66979
+ return json15({ error: "device (string) is required" }, 400);
66659
66980
  if (!(b.device in getConfig().devices)) {
66660
- return json14({ error: `unknown device: ${b.device}` }, 404);
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 json14({ device: b.device, capabilities: serializeCapabilities(caps) });
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 json14({ error: clientError(e) }, 502);
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 json14({ error: "device (string) and disabled (boolean) are required" }, 400);
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 json14({ error: `unknown device: ${b.device}` }, 404);
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 json14({
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 json14({ error: `unknown device: ${name}` }, 404);
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 json14({ ok: true, status, ...devicesPayload(db) });
67037
+ return json15({ ok: true, status, ...devicesPayload(db) });
66717
67038
  }
66718
67039
  if (url.pathname === "/api/topology") {
66719
- return json14(topologyPayload());
67040
+ return json15(topologyPayload());
66720
67041
  }
66721
67042
  if (url.pathname === "/api/config") {
66722
- return json14(configPayload());
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 json14(await fetchLatestRelease());
67052
+ return json15(await fetchLatestRelease());
66732
67053
  } catch (e) {
66733
- return json14({ error: clientError(e, "fetch failed") }, 502);
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 json14({
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 json14(await fetchAllReleases());
67088
+ return json15(await fetchAllReleases());
66768
67089
  } catch (e) {
66769
- return json14({ error: clientError(e, "fetch failed") }, 502);
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 json14({ ok: false, error: `Invalid version "${version}" (use "latest" or x.y.z).` }, 400);
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 json14({ ok: false, version, log }, 500);
67103
+ return json15({ ok: false, version, log }, 500);
66783
67104
  const willRestart = body?.restart !== false;
66784
67105
  const relaunched = willRestart ? restartProcess() : false;
66785
- return json14({
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 json14({
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 json14({ events: db.query(filter), total: db.total() });
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 ? json14(e) : json14({ error: "not found" }, 404);
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 json14(computeStats(events, { now, windowMs, buckets }));
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 json14({ error: clientError(e) }, 502);
67158
+ return json15({ error: clientError(e) }, 502);
66838
67159
  }
66839
67160
  },
66840
67161
  websocket: {