@solongate/proxy 0.78.0 → 0.80.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/tui/index.js CHANGED
@@ -325,10 +325,18 @@ function dryRun(body) {
325
325
  var settings_exports = {};
326
326
  __export(settings_exports, {
327
327
  clearRateLimitHistory: () => clearRateLimitHistory,
328
+ createAlert: () => createAlert,
329
+ createWebhook: () => createWebhook,
330
+ deleteAlert: () => deleteAlert,
331
+ deleteWebhook: () => deleteWebhook,
332
+ getAlerts: () => getAlerts,
328
333
  getGuardStatus: () => getGuardStatus,
329
334
  getRateLimitHistory: () => getRateLimitHistory,
330
335
  getSecurityLayers: () => getSecurityLayers,
331
- setSecurityLayers: () => setSecurityLayers
336
+ getSelfProtection: () => getSelfProtection,
337
+ getWebhooks: () => getWebhooks,
338
+ setSecurityLayers: () => setSecurityLayers,
339
+ setSelfProtection: () => setSelfProtection
332
340
  });
333
341
  function getSecurityLayers() {
334
342
  return request("GET", "/settings/security-layers");
@@ -345,6 +353,30 @@ function clearRateLimitHistory() {
345
353
  function getGuardStatus() {
346
354
  return request("GET", "/settings/guard-status");
347
355
  }
356
+ function getSelfProtection() {
357
+ return request("GET", "/settings/self-protection");
358
+ }
359
+ function setSelfProtection(enabled) {
360
+ return request("PUT", "/settings/self-protection", { body: { enabled } });
361
+ }
362
+ function getAlerts() {
363
+ return request("GET", "/settings/denial-alerts");
364
+ }
365
+ function createAlert(body) {
366
+ return request("POST", "/settings/denial-alerts", { body });
367
+ }
368
+ function deleteAlert(id) {
369
+ return request("DELETE", "/settings/denial-alerts", { query: { id } });
370
+ }
371
+ function getWebhooks() {
372
+ return request("GET", "/settings/denial-webhook");
373
+ }
374
+ function createWebhook(body) {
375
+ return request("POST", "/settings/denial-webhook", { body });
376
+ }
377
+ function deleteWebhook(id) {
378
+ return request("DELETE", "/settings/denial-webhook", { query: { id } });
379
+ }
348
380
 
349
381
  // src/api-client/stats.ts
350
382
  var stats_exports = {};
@@ -405,8 +437,34 @@ function anomalies(id, limit) {
405
437
  });
406
438
  }
407
439
 
440
+ // src/api-client/keys.ts
441
+ var keys_exports = {};
442
+ __export(keys_exports, {
443
+ create: () => create2,
444
+ list: () => list3,
445
+ revoke: () => revoke
446
+ });
447
+ function list3() {
448
+ return request("GET", "/keys");
449
+ }
450
+ function create2(name, isLive = true) {
451
+ return request("POST", "/keys", { body: { name, is_live: isLive } });
452
+ }
453
+ function revoke(id) {
454
+ return request("DELETE", `/keys/${encodeURIComponent(id)}`);
455
+ }
456
+
457
+ // src/api-client/mcp.ts
458
+ var mcp_exports = {};
459
+ __export(mcp_exports, {
460
+ list: () => list4
461
+ });
462
+ function list4() {
463
+ return request("GET", "/mcp-servers");
464
+ }
465
+
408
466
  // src/api-client/index.ts
409
- var api = { policies: policies_exports, settings: settings_exports, stats: stats_exports, audit: audit_exports, agents: agents_exports };
467
+ var api = { policies: policies_exports, settings: settings_exports, stats: stats_exports, audit: audit_exports, agents: agents_exports, keys: keys_exports, mcp: mcp_exports };
410
468
 
411
469
  // src/tui/hooks.ts
412
470
  import { useCallback, useEffect, useRef, useState } from "react";
@@ -1291,12 +1349,13 @@ function ruleSummary(r) {
1291
1349
  return bits.join(" ");
1292
1350
  }
1293
1351
  function PoliciesPanel({ focused }) {
1294
- const list3 = useLoader(() => api.policies.list());
1295
- const policies = list3.data?.policies ?? [];
1352
+ const list5 = useLoader(() => api.policies.list());
1353
+ const policies = list5.data?.policies ?? [];
1296
1354
  const [view, setView] = useState3("list");
1297
1355
  const [pi, setPi] = useState3(0);
1298
1356
  const [ri, setRi] = useState3(0);
1299
1357
  const [fi, setFi] = useState3(0);
1358
+ const [vi, setVi] = useState3(0);
1300
1359
  const [rules, setRules] = useState3([]);
1301
1360
  const [mode, setMode] = useState3("denylist");
1302
1361
  const [dirty, setDirty] = useState3(false);
@@ -1305,6 +1364,10 @@ function PoliciesPanel({ focused }) {
1305
1364
  const [status, setStatus] = useState3(null);
1306
1365
  const selected = policies[Math.min(pi, Math.max(0, policies.length - 1))];
1307
1366
  const detail = useLoader(() => selected ? api.policies.get(selected.id) : Promise.resolve(null), [selected?.id]);
1367
+ const versionsQ = useLoader(
1368
+ () => view === "versions" && selected ? api.policies.versions(selected.id, { limit: 50 }) : Promise.resolve(null),
1369
+ [view, selected?.id]
1370
+ );
1308
1371
  useEffect3(() => {
1309
1372
  if (detail.data) {
1310
1373
  setRules(detail.data.rules);
@@ -1334,7 +1397,7 @@ function PoliciesPanel({ focused }) {
1334
1397
  setRules(res.rules);
1335
1398
  setDirty(false);
1336
1399
  setStatus("\u2713 Saved v" + res._version);
1337
- list3.reload();
1400
+ list5.reload();
1338
1401
  } catch (e) {
1339
1402
  setStatus("\u2717 " + (e instanceof Error ? e.message : String(e)));
1340
1403
  }
@@ -1379,10 +1442,41 @@ function PoliciesPanel({ focused }) {
1379
1442
  }
1380
1443
  } else if (input === "m") {
1381
1444
  mutate(rules, mode === "denylist" ? "whitelist" : "denylist");
1445
+ } else if (input === "n") {
1446
+ const nr = { id: `rule-${Date.now()}`, description: "", effect: "DENY", priority: 100, toolPattern: "*", minimumTrustLevel: "UNTRUSTED", enabled: true };
1447
+ mutate([nr, ...rules]);
1448
+ setRi(0);
1449
+ setFi(0);
1450
+ setView("rule");
1451
+ } else if (input === "D") {
1452
+ setStatus("Dry-running\u2026");
1453
+ void api.policies.dryRun({ rules, mode }).then((res) => setStatus(`dry-run ${res.evaluated} calls \xB7 allow ${res.would_allow} / deny ${res.would_deny} \xB7 newly blocked ${res.newly_blocked} \xB7 newly allowed ${res.newly_allowed}`)).catch((e) => setStatus("\u2717 " + (e instanceof Error ? e.message : String(e))));
1454
+ } else if (input === "v") {
1455
+ setVi(0);
1456
+ setView("versions");
1382
1457
  } else if (input === "s") void save();
1383
1458
  else if (input === "x") discard();
1384
1459
  return;
1385
1460
  }
1461
+ if (view === "versions") {
1462
+ const vers = versionsQ.data?.versions ?? [];
1463
+ if (key.leftArrow) return void setView("rules");
1464
+ if (key.upArrow) setVi((n) => Math.max(0, n - 1));
1465
+ else if (key.downArrow) setVi((n) => Math.min(vers.length - 1, n + 1));
1466
+ else if (key.return) {
1467
+ const v = vers[vi];
1468
+ if (v && selected) {
1469
+ setStatus("Rolling back\u2026");
1470
+ void api.policies.rollback(selected.id, v.version).then((r) => {
1471
+ setStatus(`\u2713 rolled back to v${v.version} \u2192 new v${r.version}`);
1472
+ detail.reload();
1473
+ list5.reload();
1474
+ setView("rules");
1475
+ }).catch((e) => setStatus("\u2717 " + (e instanceof Error ? e.message : String(e))));
1476
+ }
1477
+ }
1478
+ return;
1479
+ }
1386
1480
  const rule2 = rules[ri];
1387
1481
  if (!rule2) return setView("rules");
1388
1482
  const field = FIELDS[fi];
@@ -1404,7 +1498,7 @@ function PoliciesPanel({ focused }) {
1404
1498
  { isActive: focused && !editing }
1405
1499
  );
1406
1500
  if (view === "list") {
1407
- return /* @__PURE__ */ jsx3(DataView, { loading: list3.loading && !list3.data, error: list3.error, empty: !!list3.data && policies.length === 0, emptyText: "No policies.", children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
1501
+ return /* @__PURE__ */ jsx3(DataView, { loading: list5.loading && !list5.data, error: list5.error, empty: !!list5.data && policies.length === 0, emptyText: "No policies.", children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
1408
1502
  /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: focused ? "\u2191\u2193 select \xB7 enter open" : "press \u2192 to browse" }),
1409
1503
  /* @__PURE__ */ jsx3(Box3, { marginTop: 1, flexDirection: "column", children: policies.map((p, i) => /* @__PURE__ */ jsxs3(Text3, { color: i === pi ? theme.accentBright : void 0, bold: i === pi, children: [
1410
1504
  (i === pi ? "\u25B8 " : " ") + truncate(p.name, 26).padEnd(27),
@@ -1427,7 +1521,7 @@ function PoliciesPanel({ focused }) {
1427
1521
  /* @__PURE__ */ jsx3(Text3, { color: mode === "whitelist" ? theme.warn : void 0, children: mode }),
1428
1522
  dirtyTag
1429
1523
  ] }),
1430
- /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: "\u2191\u2193 rule \xB7 enter edit \xB7 space on/off \xB7 e effect \xB7 d delete \xB7 m mode \xB7 s save \xB7 \u2190 back" }),
1524
+ /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: "\u2191\u2193 \xB7 enter edit \xB7 space on/off \xB7 e effect \xB7 n new \xB7 d del \xB7 m mode \xB7 D dry-run \xB7 v versions \xB7 s save \xB7 \u2190 back" }),
1431
1525
  /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(
1432
1526
  Table,
1433
1527
  {
@@ -1453,6 +1547,33 @@ function PoliciesPanel({ focused }) {
1453
1547
  status ? /* @__PURE__ */ jsx3(Text3, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) : null
1454
1548
  ] }) });
1455
1549
  }
1550
+ if (view === "versions") {
1551
+ const vers = versionsQ.data?.versions ?? [];
1552
+ return /* @__PURE__ */ jsx3(DataView, { loading: versionsQ.loading && !versionsQ.data, error: versionsQ.error, empty: !!versionsQ.data && vers.length === 0, emptyText: "No versions.", children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
1553
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: theme.accentBright, children: truncate(selected?.name ?? "", 28) }),
1554
+ /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: "\u2191\u2193 select \xB7 enter roll back to that version \xB7 \u2190 back" }),
1555
+ /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(
1556
+ Table,
1557
+ {
1558
+ columns: [
1559
+ { header: "", width: 2 },
1560
+ { header: "VER", width: 5 },
1561
+ { header: "RULES", width: 5 },
1562
+ { header: "REASON", width: 34 },
1563
+ { header: "WHEN", width: 16 }
1564
+ ],
1565
+ rows: vers.map((v, i) => [
1566
+ { value: i === vi ? "\u25B8" : "", color: theme.accentBright },
1567
+ { value: `v${v.version}`, bold: i === vi },
1568
+ { value: String(v.rules_count), dim: true },
1569
+ { value: truncate(v.reason || "\u2014", 34) },
1570
+ { value: truncate(v.created_at, 16), dim: true }
1571
+ ])
1572
+ }
1573
+ ) }),
1574
+ status ? /* @__PURE__ */ jsx3(Text3, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) : null
1575
+ ] }) });
1576
+ }
1456
1577
  const rule = rules[ri];
1457
1578
  return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
1458
1579
  /* @__PURE__ */ jsxs3(Box3, { children: [
@@ -1636,12 +1757,19 @@ function DlpPanel({ focused }) {
1636
1757
  const [adding, setAdding] = useState5(null);
1637
1758
  const [newName, setNewName] = useState5("");
1638
1759
  const [newRe, setNewRe] = useState5("");
1760
+ const [ghostOn, setGhostOn] = useState5(false);
1761
+ const spQ = useLoader(() => api.settings.getSelfProtection());
1762
+ const [selfProt, setSelfProt] = useState5(null);
1639
1763
  useEffect5(() => {
1640
1764
  if (q.data && !dlp) {
1641
1765
  setDlp({ ...q.data.layers.dlp, patterns: [...q.data.layers.dlp.patterns], custom: [...q.data.layers.dlp.custom] });
1642
1766
  setAvailable(q.data.availablePatterns);
1767
+ setGhostOn(q.data.layers.ghost.mode === "on");
1643
1768
  }
1644
1769
  }, [q.data, dlp]);
1770
+ useEffect5(() => {
1771
+ if (spQ.data && selfProt === null) setSelfProt(spQ.data.enabled);
1772
+ }, [spQ.data, selfProt]);
1645
1773
  const mutate = (next) => {
1646
1774
  setDlp(next);
1647
1775
  setDirty(true);
@@ -1651,14 +1779,27 @@ function DlpPanel({ focused }) {
1651
1779
  if (!dlp || !q.data) return;
1652
1780
  setStatus("Saving\u2026");
1653
1781
  try {
1654
- const res = await api.settings.setSecurityLayers({ ...q.data.layers, dlp });
1782
+ const ghost = { ...q.data.layers.ghost, mode: ghostOn ? "on" : "off" };
1783
+ const res = await api.settings.setSecurityLayers({ ...q.data.layers, dlp, ghost });
1655
1784
  setDlp({ ...res.layers.dlp, patterns: [...res.layers.dlp.patterns], custom: [...res.layers.dlp.custom] });
1785
+ setGhostOn(res.layers.ghost.mode === "on");
1656
1786
  setDirty(false);
1657
1787
  setStatus("\u2713 Saved");
1658
1788
  } catch (e) {
1659
1789
  setStatus("\u2717 " + (e instanceof Error ? e.message : String(e)));
1660
1790
  }
1661
1791
  };
1792
+ const toggleSelfProt = async () => {
1793
+ const next = !(selfProt ?? false);
1794
+ setSelfProt(next);
1795
+ try {
1796
+ await api.settings.setSelfProtection(next);
1797
+ setStatus(`\u2713 self-protection ${next ? "on" : "off"}`);
1798
+ } catch (e) {
1799
+ setSelfProt(!next);
1800
+ setStatus("\u2717 " + (e instanceof Error ? e.message : String(e)));
1801
+ }
1802
+ };
1662
1803
  const discard = () => {
1663
1804
  if (q.data) setDlp({ ...q.data.layers.dlp, patterns: [...q.data.layers.dlp.patterns], custom: [...q.data.layers.dlp.custom] });
1664
1805
  setDirty(false);
@@ -1688,7 +1829,12 @@ function DlpPanel({ focused }) {
1688
1829
  const ci = sel - available.length;
1689
1830
  mutate({ ...dlp, custom: dlp.custom.filter((_, i) => i !== ci) });
1690
1831
  setSel((n) => Math.max(0, n - 1));
1691
- } else if (input === "s") void save();
1832
+ } else if (input === "g") {
1833
+ setGhostOn((v) => !v);
1834
+ setDirty(true);
1835
+ setStatus(null);
1836
+ } else if (input === "P") void toggleSelfProt();
1837
+ else if (input === "s") void save();
1692
1838
  else if (input === "x") discard();
1693
1839
  },
1694
1840
  { isActive: focused && !adding }
@@ -1700,7 +1846,13 @@ function DlpPanel({ focused }) {
1700
1846
  /* @__PURE__ */ jsx5(Text5, { color: modeColor(dlp.mode), children: dlp.mode }),
1701
1847
  dirty ? /* @__PURE__ */ jsx5(Text5, { color: theme.warn, children: " \u25CF unsaved (s save \xB7 x discard)" }) : null
1702
1848
  ] }),
1703
- /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: focused ? "\u2191\u2193 move \xB7 space toggle \xB7 m mode \xB7 a add \xB7 d remove \xB7 s save" : "press \u2192 to edit" }),
1849
+ /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: focused ? "\u2191\u2193 \xB7 space toggle \xB7 m mode \xB7 a add \xB7 d remove \xB7 g ghost \xB7 P self-protect \xB7 s save" : "press \u2192 to edit" }),
1850
+ /* @__PURE__ */ jsxs5(Box5, { children: [
1851
+ /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: "ghost: " }),
1852
+ /* @__PURE__ */ jsx5(Text5, { color: ghostOn ? theme.ok : theme.dim, children: ghostOn ? "on" : "off" }),
1853
+ /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: " self-protection: " }),
1854
+ /* @__PURE__ */ jsx5(Text5, { color: selfProt ? theme.ok : theme.dim, children: selfProt === null ? "\u2026" : selfProt ? "on" : "off" })
1855
+ ] }),
1704
1856
  /* @__PURE__ */ jsx5(Box5, { marginTop: 1, flexDirection: "column", children: available.map((p, i) => {
1705
1857
  const on = enabled.has(p);
1706
1858
  const active2 = focused && sel === i;
@@ -1757,12 +1909,15 @@ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1757
1909
  function StatsPanel({ active: active2 }) {
1758
1910
  const stats = useLoader(() => api.stats.get());
1759
1911
  const ts = useLoader(() => api.stats.timeseries({ period: "24h" }));
1912
+ const drift2 = useLoader(() => api.stats.drift(7));
1760
1913
  usePoll(() => {
1761
1914
  stats.reload();
1762
1915
  ts.reload();
1763
1916
  }, 5e3, active2);
1917
+ usePoll(drift2.reload, 3e4, active2);
1764
1918
  const s = stats.data;
1765
1919
  const points = ts.data?.timeseries ?? [];
1920
+ const driftRules = (drift2.data?.rules ?? []).slice(0, 5);
1766
1921
  return /* @__PURE__ */ jsx6(DataView, { loading: stats.loading && !s, error: stats.error, children: s ? /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
1767
1922
  /* @__PURE__ */ jsxs6(Box6, { children: [
1768
1923
  /* @__PURE__ */ jsxs6(Text6, { bold: true, children: [
@@ -1815,7 +1970,7 @@ function StatsPanel({ active: active2 }) {
1815
1970
  { header: "MS", width: 5 },
1816
1971
  { header: "WHEN", width: 6 }
1817
1972
  ],
1818
- rows: (s.recent_activity ?? []).slice(0, 8).map((a) => [
1973
+ rows: (s.recent_activity ?? []).slice(0, 6).map((a) => [
1819
1974
  { value: a.decision, color: decisionColor(a.decision) },
1820
1975
  { value: truncate(a.tool_name, 20), color: theme.accent },
1821
1976
  { value: String(a.evaluation_time_ms ?? "\u2014"), dim: true },
@@ -1823,6 +1978,31 @@ function StatsPanel({ active: active2 }) {
1823
1978
  ])
1824
1979
  }
1825
1980
  )
1981
+ ] }),
1982
+ /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, flexDirection: "column", children: [
1983
+ /* @__PURE__ */ jsxs6(Text6, { color: theme.dim, children: [
1984
+ "Denial drift ",
1985
+ drift2.data ? `(7d: ${drift2.data.total_current} now vs ${drift2.data.total_previous} prev)` : ""
1986
+ ] }),
1987
+ driftRules.length ? /* @__PURE__ */ jsx6(
1988
+ Table,
1989
+ {
1990
+ columns: [
1991
+ { header: "NOW", width: 5 },
1992
+ { header: "PREV", width: 5 },
1993
+ { header: "\u0394", width: 6 },
1994
+ { header: "RULE", width: 22 },
1995
+ { header: "REASON", width: 26 }
1996
+ ],
1997
+ rows: driftRules.map((r) => [
1998
+ { value: String(r.current), bold: true },
1999
+ { value: String(r.previous), dim: true },
2000
+ { value: r.is_new ? "NEW" : r.spike ? `+${r.delta}` : String(r.delta), color: r.is_new ? theme.ok : r.spike ? theme.bad : void 0 },
2001
+ { value: truncate(r.rule_id ?? "\u2014", 22), color: theme.accent },
2002
+ { value: truncate(r.reason ?? r.last_tool ?? "\u2014", 26), dim: true }
2003
+ ])
2004
+ }
2005
+ ) : /* @__PURE__ */ jsx6(Text6, { color: theme.dim, children: " no denials in window" })
1826
2006
  ] })
1827
2007
  ] }) : null });
1828
2008
  }
@@ -2036,7 +2216,16 @@ function App() {
2036
2216
  const { exit } = useApp();
2037
2217
  const [section, setSection] = useState7(0);
2038
2218
  const [focus, setFocus] = useState7("nav");
2219
+ const [help, setHelp] = useState7(false);
2039
2220
  useInput6((input, key) => {
2221
+ if (help) {
2222
+ setHelp(false);
2223
+ return;
2224
+ }
2225
+ if (input === "?" && focus === "nav") {
2226
+ setHelp(true);
2227
+ return;
2228
+ }
2040
2229
  if (focus === "nav") {
2041
2230
  if (key.upArrow) setSection((n) => (n - 1 + SECTIONS.length) % SECTIONS.length);
2042
2231
  else if (key.downArrow) setSection((n) => (n + 1) % SECTIONS.length);
@@ -2050,6 +2239,7 @@ function App() {
2050
2239
  const cols = process.stdout.columns ?? 100;
2051
2240
  const rows = process.stdout.rows ?? 30;
2052
2241
  const current = SECTIONS[section];
2242
+ if (help) return /* @__PURE__ */ jsx8(HelpOverlay, { cols, rows });
2053
2243
  if (current.label === "Live" && focus === "panel") {
2054
2244
  return /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", width: cols, height: rows, children: /* @__PURE__ */ jsx8(LivePanel, { active: true, focused: true }) });
2055
2245
  }
@@ -2060,7 +2250,26 @@ function App() {
2060
2250
  /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", width: 16, borderStyle: "round", borderColor: focus === "nav" ? theme.accent : "gray", paddingX: 1, children: SECTIONS.map((s, i) => /* @__PURE__ */ jsx8(Text8, { color: i === section ? theme.accentBright : void 0, bold: i === section, children: (i === section ? "\u25B8 " : " ") + s.label }, s.label)) }),
2061
2251
  /* @__PURE__ */ jsx8(Box8, { flexGrow: 1, borderStyle: "round", borderColor: focus === "panel" ? theme.accent : "gray", paddingX: 1, paddingY: 0, children: /* @__PURE__ */ jsx8(Panel, { active: focus === "panel" || current.label !== "Live", focused: focus === "panel" }) })
2062
2252
  ] }),
2063
- /* @__PURE__ */ jsx8(Box8, { children: focus === "nav" ? /* @__PURE__ */ jsx8(KeyHints, { hints: [["\u2191\u2193", "section"], ["\u2192/enter", "open"], ["q", "quit"]] }) : /* @__PURE__ */ jsx8(KeyHints, { hints: [["\u2190/esc", "back"], ["\u2191\u2193", "in-panel"], ["space/s", "act"]] }) })
2253
+ /* @__PURE__ */ jsx8(Box8, { children: focus === "nav" ? /* @__PURE__ */ jsx8(KeyHints, { hints: [["\u2191\u2193", "section"], ["\u2192/enter", "open"], ["?", "help"], ["q", "quit"]] }) : /* @__PURE__ */ jsx8(KeyHints, { hints: [["\u2190/esc", "back"], ["\u2191\u2193", "in-panel"], ["space/s", "act"]] }) })
2254
+ ] });
2255
+ }
2256
+ var HELP = [
2257
+ ["Global", [["\u2191\u2193", "move between sections"], ["\u2192 / enter", "open a section"], ["\u2190 / esc", "back to the menu"], ["?", "this help"], ["q", "quit"]]],
2258
+ ["Live", [["\u2191\u2193", "select a stream row"], ["w", "whitelist the selected DENY"], ["b", "block the selected ALLOW"], ["d / x / r", "filter denies / dlp / rate-limit"], ["f", "local / cloud filter"], ["/", "search"], ["s", "sessions (\u2191\u2193 pick, enter open)"], ["space", "copy mode (freeze)"]]],
2259
+ ["Policies", [["\u2191\u2193", "browse / select"], ["enter", "open rules \u2192 open a rule"], ["space", "toggle a rule on/off"], ["e", "flip effect"], ["n", "new rule"], ["d", "delete rule"], ["m", "flip mode"], ["D", "dry-run the draft"], ["v", "versions / rollback"], ["s", "save"], ["x", "discard"]]],
2260
+ ["Rate limit / DLP", [["\u2191\u2193", "field / pattern"], ["\u2190\u2192", "adjust (shift = \xB110)"], ["space", "toggle pattern"], ["m", "mode"], ["a / d", "add / remove custom"], ["g", "ghost mode"], ["P", "self-protection"], ["s", "save"]]]
2261
+ ];
2262
+ function HelpOverlay({ cols, rows }) {
2263
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", width: cols, height: rows, paddingX: 2, paddingTop: 1, children: [
2264
+ /* @__PURE__ */ jsx8(Text8, { bold: true, color: theme.accentBright, children: "SolonGate \u2014 keyboard shortcuts" }),
2265
+ /* @__PURE__ */ jsx8(Box8, { marginTop: 1, flexDirection: "column", children: HELP.map(([group, keys]) => /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginBottom: 1, children: [
2266
+ /* @__PURE__ */ jsx8(Text8, { bold: true, color: theme.accent, children: group }),
2267
+ keys.map(([k, desc]) => /* @__PURE__ */ jsxs8(Text8, { children: [
2268
+ /* @__PURE__ */ jsx8(Text8, { color: theme.accentBright, children: (" " + k).padEnd(16) }),
2269
+ /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: desc })
2270
+ ] }, k))
2271
+ ] }, group)) }),
2272
+ /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "press any key to close" })
2064
2273
  ] });
2065
2274
  }
2066
2275
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.78.0",
3
+ "version": "0.80.0",
4
4
  "description": "AI tool security proxy: protect any AI tool server with customizable policies, path/command constraints, rate limiting, and audit logging. No code changes required.",
5
5
  "type": "module",
6
6
  "bin": {