@usex/mikrotik-mcp 5.8.0 → 5.9.0

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