@solongate/proxy 0.60.0 → 0.61.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/index.js CHANGED
@@ -7020,56 +7020,128 @@ var init_hooks = __esm({
7020
7020
 
7021
7021
  // src/tui/panels/Policies.tsx
7022
7022
  import { Box as Box2, Text as Text2, useInput } from "ink";
7023
- import { useState as useState2 } from "react";
7023
+ import { useEffect as useEffect2, useState as useState2 } from "react";
7024
7024
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
7025
7025
  function PoliciesPanel({ focused }) {
7026
- const [sel, setSel] = useState2(0);
7027
7026
  const list4 = useLoader(() => api.policies.list());
7028
7027
  const policies = list4.data?.policies ?? [];
7029
- const selected = policies[Math.min(sel, Math.max(0, policies.length - 1))];
7028
+ const [view, setView] = useState2("list");
7029
+ const [pi, setPi] = useState2(0);
7030
+ const [ri, setRi] = useState2(0);
7031
+ const [rules, setRules] = useState2(null);
7032
+ const [mode, setMode] = useState2("denylist");
7033
+ const [status, setStatus] = useState2(null);
7034
+ const selected = policies[Math.min(pi, Math.max(0, policies.length - 1))];
7030
7035
  const detail = useLoader(() => selected ? api.policies.get(selected.id) : Promise.resolve(null), [selected?.id]);
7036
+ useEffect2(() => {
7037
+ if (detail.data) {
7038
+ setRules(detail.data.rules);
7039
+ setMode(detail.data.mode ?? "denylist");
7040
+ setRi(0);
7041
+ }
7042
+ }, [detail.data]);
7043
+ const persist = async (nextRules, nextMode) => {
7044
+ if (!detail.data || !selected) return;
7045
+ setStatus("Saving\u2026");
7046
+ try {
7047
+ const res = await api.policies.update(selected.id, {
7048
+ id: selected.id,
7049
+ name: detail.data.name,
7050
+ description: detail.data.description,
7051
+ mode: nextMode,
7052
+ agents: detail.data.agents,
7053
+ rules: nextRules
7054
+ });
7055
+ setRules(res.rules);
7056
+ setStatus("\u2713 Saved v" + res._version);
7057
+ list4.reload();
7058
+ } catch (e) {
7059
+ setStatus("\u2717 " + (e instanceof Error ? e.message : String(e)));
7060
+ }
7061
+ };
7031
7062
  useInput(
7032
- (_input, key) => {
7033
- if (key.upArrow) setSel((n) => Math.max(0, n - 1));
7034
- if (key.downArrow) setSel((n) => Math.min(policies.length - 1, n + 1));
7063
+ (input, key) => {
7064
+ if (view === "list") {
7065
+ if (key.upArrow) setPi((n) => Math.max(0, n - 1));
7066
+ else if (key.downArrow) setPi((n) => Math.min(policies.length - 1, n + 1));
7067
+ else if (key.return || key.rightArrow) {
7068
+ setStatus(null);
7069
+ setView("detail");
7070
+ }
7071
+ return;
7072
+ }
7073
+ const rs2 = rules ?? [];
7074
+ if (key.leftArrow) {
7075
+ setView("list");
7076
+ setStatus(null);
7077
+ } else if (key.upArrow) setRi((n) => Math.max(0, n - 1));
7078
+ else if (key.downArrow) setRi((n) => Math.min(rs2.length - 1, n + 1));
7079
+ else if (input === " " || input === "e") {
7080
+ if (!rs2[ri]) return;
7081
+ const next = rs2.map((r, i) => i === ri ? { ...r, enabled: !r.enabled } : r);
7082
+ setRules(next);
7083
+ void persist(next, mode);
7084
+ } else if (input === "d") {
7085
+ const target = rs2[ri];
7086
+ if (!target || !selected) return;
7087
+ setStatus("Deleting\u2026");
7088
+ void api.policies.revokeRule(selected.id, target.id).then(() => {
7089
+ setStatus("\u2713 Deleted");
7090
+ detail.reload();
7091
+ list4.reload();
7092
+ }).catch((e) => setStatus("\u2717 " + (e instanceof Error ? e.message : String(e))));
7093
+ } else if (input === "m") {
7094
+ const nm = mode === "denylist" ? "whitelist" : "denylist";
7095
+ setMode(nm);
7096
+ void persist(rs2, nm);
7097
+ }
7035
7098
  },
7036
7099
  { isActive: focused }
7037
7100
  );
7038
- return /* @__PURE__ */ jsx2(DataView, { loading: list4.loading && !list4.data, error: list4.error, empty: !!list4.data && policies.length === 0, emptyText: "No policies.", children: /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
7039
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: focused ? "\u2191\u2193 select policy" : "press \u2192 to browse" }),
7040
- /* @__PURE__ */ jsx2(Box2, { marginTop: 1, flexDirection: "column", children: policies.map((p, i) => /* @__PURE__ */ jsxs2(Text2, { color: i === sel ? theme.accentBright : void 0, bold: i === sel, children: [
7041
- (i === sel ? "\u25B8 " : " ") + truncate2(p.name, 26).padEnd(27),
7042
- /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
7043
- p.mode.padEnd(10),
7044
- " ",
7045
- p.rules.length,
7046
- " rules \xB7 v",
7047
- p.version
7048
- ] })
7049
- ] }, p.id)) }),
7050
- selected ? /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
7051
- /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
7052
- "Rules of ",
7053
- /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: selected.id })
7054
- ] }),
7055
- /* @__PURE__ */ jsx2(
7056
- Table,
7057
- {
7058
- columns: [
7059
- { header: "EFFECT", width: 7 },
7060
- { header: "PRIO", width: 4 },
7061
- { header: "TOOL", width: 22 },
7062
- { header: "DESCRIPTION", width: 34 }
7063
- ],
7064
- rows: (detail.data?.rules ?? []).slice(0, 10).map((r) => [
7065
- { value: r.effect, color: r.effect === "ALLOW" ? theme.ok : theme.bad },
7066
- { value: String(r.priority), dim: true },
7067
- { value: truncate2(r.toolPattern, 22), color: theme.accent },
7068
- { value: truncate2(r.description || "\u2014", 34) }
7069
- ])
7070
- }
7071
- )
7072
- ] }) : null
7101
+ if (view === "list") {
7102
+ return /* @__PURE__ */ jsx2(DataView, { loading: list4.loading && !list4.data, error: list4.error, empty: !!list4.data && policies.length === 0, emptyText: "No policies.", children: /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
7103
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: focused ? "\u2191\u2193 select \xB7 enter open" : "press \u2192 to browse" }),
7104
+ /* @__PURE__ */ jsx2(Box2, { marginTop: 1, flexDirection: "column", children: policies.map((p, i) => /* @__PURE__ */ jsxs2(Text2, { color: i === pi ? theme.accentBright : void 0, bold: i === pi, children: [
7105
+ (i === pi ? "\u25B8 " : " ") + truncate2(p.name, 26).padEnd(27),
7106
+ /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
7107
+ p.mode.padEnd(10),
7108
+ " ",
7109
+ p.rules.length,
7110
+ " rules \xB7 v",
7111
+ p.version
7112
+ ] })
7113
+ ] }, p.id)) })
7114
+ ] }) });
7115
+ }
7116
+ const rs = rules ?? [];
7117
+ return /* @__PURE__ */ jsx2(DataView, { loading: detail.loading && !rules, error: detail.error, children: /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
7118
+ /* @__PURE__ */ jsxs2(Box2, { children: [
7119
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: theme.accentBright, children: truncate2(selected?.name ?? "", 30) }),
7120
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " mode: " }),
7121
+ /* @__PURE__ */ jsx2(Text2, { color: mode === "whitelist" ? theme.ok : void 0, children: mode })
7122
+ ] }),
7123
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "\u2191\u2193 rule \xB7 space toggle \xB7 d delete \xB7 m mode \xB7 \u2190 back" }),
7124
+ /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(
7125
+ Table,
7126
+ {
7127
+ columns: [
7128
+ { header: "", width: 2 },
7129
+ { header: "EFFECT", width: 7 },
7130
+ { header: "PRIO", width: 4 },
7131
+ { header: "TOOL", width: 20 },
7132
+ { header: "DESCRIPTION", width: 34 }
7133
+ ],
7134
+ rows: rs.map((r, i) => [
7135
+ { value: i === ri ? "\u25B8" : "", color: theme.accentBright },
7136
+ { value: r.effect, color: r.effect === "ALLOW" ? theme.ok : theme.bad, dim: !r.enabled },
7137
+ { value: String(r.priority), dim: true },
7138
+ { value: (r.enabled ? "" : "\u2298 ") + truncate2(r.toolPattern, 18), color: theme.accent, dim: !r.enabled },
7139
+ { value: truncate2(r.description || "\u2014", 34), dim: !r.enabled }
7140
+ ])
7141
+ }
7142
+ ) }),
7143
+ rs.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "(no rules)" }) : null,
7144
+ status ? /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) }) : null
7073
7145
  ] }) });
7074
7146
  }
7075
7147
  var init_Policies = __esm({
@@ -7084,26 +7156,26 @@ var init_Policies = __esm({
7084
7156
 
7085
7157
  // src/tui/panels/RateLimit.tsx
7086
7158
  import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
7087
- import { useEffect as useEffect2, useState as useState3 } from "react";
7159
+ import { useEffect as useEffect3, useState as useState3 } from "react";
7088
7160
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
7089
7161
  function RateLimitPanel({ focused }) {
7090
7162
  const layersQ = useLoader(() => api.settings.getSecurityLayers());
7091
7163
  const historyQ = useLoader(() => api.settings.getRateLimitHistory());
7164
+ const insightsQ = useLoader(() => api.stats.securityInsights(7));
7092
7165
  const [draft, setDraft] = useState3(null);
7093
7166
  const [dirty, setDirty] = useState3(false);
7094
7167
  const [fi, setFi] = useState3(0);
7095
7168
  const [status, setStatus] = useState3(null);
7096
- useEffect2(() => {
7169
+ useEffect3(() => {
7097
7170
  if (layersQ.data && !draft) setDraft({ ...layersQ.data.layers.rateLimit });
7098
7171
  }, [layersQ.data, draft]);
7099
- const adjust = (dir) => {
7172
+ const adjust = (dir, step) => {
7100
7173
  if (!draft) return;
7101
7174
  const field = FIELDS[fi];
7102
7175
  if (field === "mode") {
7103
7176
  const idx = (MODES.indexOf(draft.mode) + dir + MODES.length) % MODES.length;
7104
7177
  setDraft({ ...draft, mode: MODES[idx] });
7105
7178
  } else {
7106
- const step = 10;
7107
7179
  setDraft({ ...draft, [field]: Math.max(0, draft[field] + dir * step) });
7108
7180
  }
7109
7181
  setDirty(true);
@@ -7125,35 +7197,81 @@ function RateLimitPanel({ focused }) {
7125
7197
  };
7126
7198
  useInput2(
7127
7199
  (input, key) => {
7200
+ const step = key.shift ? 10 : 1;
7128
7201
  if (key.upArrow) setFi((n) => (n - 1 + FIELDS.length) % FIELDS.length);
7129
7202
  else if (key.downArrow) setFi((n) => (n + 1) % FIELDS.length);
7130
- else if (key.leftArrow) adjust(-1);
7131
- else if (key.rightArrow) adjust(1);
7203
+ else if (key.leftArrow) adjust(-1, step);
7204
+ else if (key.rightArrow) adjust(1, step);
7132
7205
  else if (input === "s") void save();
7133
7206
  },
7134
7207
  { isActive: focused }
7135
7208
  );
7136
- const history = historyQ.data?.history ?? [];
7209
+ const history = (historyQ.data?.history ?? []).slice().sort((a, b) => b.ts - a.ts);
7210
+ const bursts = (insightsQ.data?.["anomalies"] ?? []).slice(0, 6);
7137
7211
  return /* @__PURE__ */ jsx3(DataView, { loading: layersQ.loading && !draft, error: layersQ.error, children: draft ? /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
7138
- /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: focused ? "\u2191\u2193 field \u2190\u2192 change s save" : "press \u2192 to edit" }),
7212
+ /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: focused ? "\u2191\u2193 field \xB7 \u2190\u2192 \xB11 \xB7 shift+\u2190\u2192 \xB110 \xB7 s save" : "press \u2192 to edit" }),
7139
7213
  /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
7140
- /* @__PURE__ */ jsx3(Row, { label: "Mode", active: focused && fi === 0, children: /* @__PURE__ */ jsx3(Text3, { color: modeColor(draft.mode), children: draft.mode }) }),
7141
- /* @__PURE__ */ jsx3(Row, { label: "Per minute", active: focused && fi === 1, children: /* @__PURE__ */ jsx3(Text3, { bold: true, children: draft.perMinute }) }),
7142
- /* @__PURE__ */ jsx3(Row, { label: "Per hour", active: focused && fi === 2, children: /* @__PURE__ */ jsx3(Text3, { bold: true, children: draft.perHour === 0 ? "off" : draft.perHour }) }),
7143
- /* @__PURE__ */ jsx3(Row, { label: "Per day", active: focused && fi === 3, children: /* @__PURE__ */ jsx3(Text3, { bold: true, children: draft.perDay === 0 ? "off" : draft.perDay }) })
7214
+ /* @__PURE__ */ jsx3(FieldRow, { label: "Mode", active: focused && fi === 0, children: /* @__PURE__ */ jsx3(Text3, { color: modeColor(draft.mode), children: draft.mode }) }),
7215
+ /* @__PURE__ */ jsx3(FieldRow, { label: "Per minute", active: focused && fi === 1, children: /* @__PURE__ */ jsx3(Text3, { bold: true, children: draft.perMinute }) }),
7216
+ /* @__PURE__ */ jsx3(FieldRow, { label: "Per hour", active: focused && fi === 2, children: /* @__PURE__ */ jsx3(Text3, { bold: true, children: draft.perHour === 0 ? "off" : draft.perHour }) }),
7217
+ /* @__PURE__ */ jsx3(FieldRow, { label: "Per day", active: focused && fi === 3, children: /* @__PURE__ */ jsx3(Text3, { bold: true, children: draft.perDay === 0 ? "off" : draft.perDay }) })
7144
7218
  ] }),
7145
- history.length ? /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, children: [
7146
- /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: "history " }),
7147
- /* @__PURE__ */ jsx3(Text3, { color: theme.accent, children: sparkline(history.map((h) => h.minute), 40) }),
7148
- /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: " " + history.length + " changes" })
7149
- ] }) : null,
7150
- /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, children: [
7219
+ dirty || status ? /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, children: [
7151
7220
  dirty ? /* @__PURE__ */ jsx3(Text3, { color: theme.warn, children: "\u25CF unsaved \u2014 press s to apply " }) : null,
7152
7221
  status ? /* @__PURE__ */ jsx3(Text3, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) : null
7222
+ ] }) : null,
7223
+ /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
7224
+ /* @__PURE__ */ jsxs3(Text3, { color: theme.dim, children: [
7225
+ "Change history ",
7226
+ history.length ? `(${history.length})` : ""
7227
+ ] }),
7228
+ history.length ? /* @__PURE__ */ jsx3(
7229
+ Table,
7230
+ {
7231
+ columns: [
7232
+ { header: "WHEN", width: 8 },
7233
+ { header: "/MIN", width: 6 },
7234
+ { header: "/HOUR", width: 7 },
7235
+ { header: "/DAY", width: 6 }
7236
+ ],
7237
+ rows: history.slice(0, 5).map((h, i) => [
7238
+ { value: ago(h.ts) + " ago", dim: true, bold: i === 0 },
7239
+ { value: String(h.minute), bold: i === 0 },
7240
+ { value: h.hour === 0 ? "off" : String(h.hour), dim: h.hour === 0 },
7241
+ { value: h.day === 0 ? "off" : String(h.day), dim: h.day === 0 }
7242
+ ])
7243
+ }
7244
+ ) : /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: " no changes" })
7245
+ ] }),
7246
+ /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
7247
+ /* @__PURE__ */ jsxs3(Text3, { color: theme.dim, children: [
7248
+ "Recent bursts ",
7249
+ insightsQ.loading && !insightsQ.data ? "\u2026" : `(${bursts.length})`
7250
+ ] }),
7251
+ bursts.length ? /* @__PURE__ */ jsx3(
7252
+ Table,
7253
+ {
7254
+ columns: [
7255
+ { header: "RESULT", width: 9 },
7256
+ { header: "AGENT", width: 16 },
7257
+ { header: "CALLS/LIMIT", width: 12 },
7258
+ { header: "WHEN", width: 8 }
7259
+ ],
7260
+ rows: bursts.map((b) => [
7261
+ { value: b.blocked ? "blocked" : "BYPASSED", color: b.blocked ? theme.accent : theme.bad, bold: !b.blocked },
7262
+ { value: (b.agent || "\u2014").slice(0, 16), dim: true },
7263
+ { value: `${b.count}/${b.limit}` },
7264
+ { value: ago(b.minute) + " ago", dim: true }
7265
+ ])
7266
+ }
7267
+ ) : /* @__PURE__ */ jsxs3(Text3, { color: theme.dim, children: [
7268
+ " ",
7269
+ insightsQ.error ? insightsQ.error : "none in 7d"
7270
+ ] })
7153
7271
  ] })
7154
7272
  ] }) : null });
7155
7273
  }
7156
- function Row({ label, active: active2, children }) {
7274
+ function FieldRow({ label, active: active2, children }) {
7157
7275
  return /* @__PURE__ */ jsxs3(Box3, { children: [
7158
7276
  /* @__PURE__ */ jsx3(Text3, { color: active2 ? theme.accentBright : void 0, children: (active2 ? "\u25B8 " : " ") + label.padEnd(12) }),
7159
7277
  children
@@ -7174,7 +7292,7 @@ var init_RateLimit = __esm({
7174
7292
 
7175
7293
  // src/tui/panels/Dlp.tsx
7176
7294
  import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
7177
- import { useEffect as useEffect3, useState as useState4 } from "react";
7295
+ import { useEffect as useEffect4, useState as useState4 } from "react";
7178
7296
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
7179
7297
  function DlpPanel({ focused }) {
7180
7298
  const q = useLoader(() => api.settings.getSecurityLayers());
@@ -7182,7 +7300,7 @@ function DlpPanel({ focused }) {
7182
7300
  const [available, setAvailable] = useState4([]);
7183
7301
  const [sel, setSel] = useState4(0);
7184
7302
  const [status, setStatus] = useState4(null);
7185
- useEffect3(() => {
7303
+ useEffect4(() => {
7186
7304
  if (q.data && !dlp) {
7187
7305
  setDlp({ ...q.data.layers.dlp, patterns: [...q.data.layers.dlp.patterns] });
7188
7306
  setAvailable(q.data.availablePatterns);
package/dist/tui/index.js CHANGED
@@ -126,7 +126,7 @@ function KeyHints({ hints }) {
126
126
 
127
127
  // src/tui/panels/Policies.tsx
128
128
  import { Box as Box2, Text as Text2, useInput } from "ink";
129
- import { useState as useState2 } from "react";
129
+ import { useEffect as useEffect2, useState as useState2 } from "react";
130
130
 
131
131
  // src/api-client/client.ts
132
132
  import { readFileSync, existsSync } from "fs";
@@ -432,80 +432,152 @@ function usePoll(reload, intervalMs, enabled = true) {
432
432
  // src/tui/panels/Policies.tsx
433
433
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
434
434
  function PoliciesPanel({ focused }) {
435
- const [sel, setSel] = useState2(0);
436
435
  const list3 = useLoader(() => api.policies.list());
437
436
  const policies = list3.data?.policies ?? [];
438
- const selected = policies[Math.min(sel, Math.max(0, policies.length - 1))];
437
+ const [view, setView] = useState2("list");
438
+ const [pi, setPi] = useState2(0);
439
+ const [ri, setRi] = useState2(0);
440
+ const [rules, setRules] = useState2(null);
441
+ const [mode, setMode] = useState2("denylist");
442
+ const [status, setStatus] = useState2(null);
443
+ const selected = policies[Math.min(pi, Math.max(0, policies.length - 1))];
439
444
  const detail = useLoader(() => selected ? api.policies.get(selected.id) : Promise.resolve(null), [selected?.id]);
445
+ useEffect2(() => {
446
+ if (detail.data) {
447
+ setRules(detail.data.rules);
448
+ setMode(detail.data.mode ?? "denylist");
449
+ setRi(0);
450
+ }
451
+ }, [detail.data]);
452
+ const persist = async (nextRules, nextMode) => {
453
+ if (!detail.data || !selected) return;
454
+ setStatus("Saving\u2026");
455
+ try {
456
+ const res = await api.policies.update(selected.id, {
457
+ id: selected.id,
458
+ name: detail.data.name,
459
+ description: detail.data.description,
460
+ mode: nextMode,
461
+ agents: detail.data.agents,
462
+ rules: nextRules
463
+ });
464
+ setRules(res.rules);
465
+ setStatus("\u2713 Saved v" + res._version);
466
+ list3.reload();
467
+ } catch (e) {
468
+ setStatus("\u2717 " + (e instanceof Error ? e.message : String(e)));
469
+ }
470
+ };
440
471
  useInput(
441
- (_input, key) => {
442
- if (key.upArrow) setSel((n) => Math.max(0, n - 1));
443
- if (key.downArrow) setSel((n) => Math.min(policies.length - 1, n + 1));
472
+ (input, key) => {
473
+ if (view === "list") {
474
+ if (key.upArrow) setPi((n) => Math.max(0, n - 1));
475
+ else if (key.downArrow) setPi((n) => Math.min(policies.length - 1, n + 1));
476
+ else if (key.return || key.rightArrow) {
477
+ setStatus(null);
478
+ setView("detail");
479
+ }
480
+ return;
481
+ }
482
+ const rs2 = rules ?? [];
483
+ if (key.leftArrow) {
484
+ setView("list");
485
+ setStatus(null);
486
+ } else if (key.upArrow) setRi((n) => Math.max(0, n - 1));
487
+ else if (key.downArrow) setRi((n) => Math.min(rs2.length - 1, n + 1));
488
+ else if (input === " " || input === "e") {
489
+ if (!rs2[ri]) return;
490
+ const next = rs2.map((r, i) => i === ri ? { ...r, enabled: !r.enabled } : r);
491
+ setRules(next);
492
+ void persist(next, mode);
493
+ } else if (input === "d") {
494
+ const target = rs2[ri];
495
+ if (!target || !selected) return;
496
+ setStatus("Deleting\u2026");
497
+ void api.policies.revokeRule(selected.id, target.id).then(() => {
498
+ setStatus("\u2713 Deleted");
499
+ detail.reload();
500
+ list3.reload();
501
+ }).catch((e) => setStatus("\u2717 " + (e instanceof Error ? e.message : String(e))));
502
+ } else if (input === "m") {
503
+ const nm = mode === "denylist" ? "whitelist" : "denylist";
504
+ setMode(nm);
505
+ void persist(rs2, nm);
506
+ }
444
507
  },
445
508
  { isActive: focused }
446
509
  );
447
- return /* @__PURE__ */ jsx2(DataView, { loading: list3.loading && !list3.data, error: list3.error, empty: !!list3.data && policies.length === 0, emptyText: "No policies.", children: /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
448
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: focused ? "\u2191\u2193 select policy" : "press \u2192 to browse" }),
449
- /* @__PURE__ */ jsx2(Box2, { marginTop: 1, flexDirection: "column", children: policies.map((p, i) => /* @__PURE__ */ jsxs2(Text2, { color: i === sel ? theme.accentBright : void 0, bold: i === sel, children: [
450
- (i === sel ? "\u25B8 " : " ") + truncate(p.name, 26).padEnd(27),
451
- /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
452
- p.mode.padEnd(10),
453
- " ",
454
- p.rules.length,
455
- " rules \xB7 v",
456
- p.version
457
- ] })
458
- ] }, p.id)) }),
459
- selected ? /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
460
- /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
461
- "Rules of ",
462
- /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: selected.id })
463
- ] }),
464
- /* @__PURE__ */ jsx2(
465
- Table,
466
- {
467
- columns: [
468
- { header: "EFFECT", width: 7 },
469
- { header: "PRIO", width: 4 },
470
- { header: "TOOL", width: 22 },
471
- { header: "DESCRIPTION", width: 34 }
472
- ],
473
- rows: (detail.data?.rules ?? []).slice(0, 10).map((r) => [
474
- { value: r.effect, color: r.effect === "ALLOW" ? theme.ok : theme.bad },
475
- { value: String(r.priority), dim: true },
476
- { value: truncate(r.toolPattern, 22), color: theme.accent },
477
- { value: truncate(r.description || "\u2014", 34) }
478
- ])
479
- }
480
- )
481
- ] }) : null
510
+ if (view === "list") {
511
+ return /* @__PURE__ */ jsx2(DataView, { loading: list3.loading && !list3.data, error: list3.error, empty: !!list3.data && policies.length === 0, emptyText: "No policies.", children: /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
512
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: focused ? "\u2191\u2193 select \xB7 enter open" : "press \u2192 to browse" }),
513
+ /* @__PURE__ */ jsx2(Box2, { marginTop: 1, flexDirection: "column", children: policies.map((p, i) => /* @__PURE__ */ jsxs2(Text2, { color: i === pi ? theme.accentBright : void 0, bold: i === pi, children: [
514
+ (i === pi ? "\u25B8 " : " ") + truncate(p.name, 26).padEnd(27),
515
+ /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
516
+ p.mode.padEnd(10),
517
+ " ",
518
+ p.rules.length,
519
+ " rules \xB7 v",
520
+ p.version
521
+ ] })
522
+ ] }, p.id)) })
523
+ ] }) });
524
+ }
525
+ const rs = rules ?? [];
526
+ return /* @__PURE__ */ jsx2(DataView, { loading: detail.loading && !rules, error: detail.error, children: /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
527
+ /* @__PURE__ */ jsxs2(Box2, { children: [
528
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: theme.accentBright, children: truncate(selected?.name ?? "", 30) }),
529
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " mode: " }),
530
+ /* @__PURE__ */ jsx2(Text2, { color: mode === "whitelist" ? theme.ok : void 0, children: mode })
531
+ ] }),
532
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "\u2191\u2193 rule \xB7 space toggle \xB7 d delete \xB7 m mode \xB7 \u2190 back" }),
533
+ /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(
534
+ Table,
535
+ {
536
+ columns: [
537
+ { header: "", width: 2 },
538
+ { header: "EFFECT", width: 7 },
539
+ { header: "PRIO", width: 4 },
540
+ { header: "TOOL", width: 20 },
541
+ { header: "DESCRIPTION", width: 34 }
542
+ ],
543
+ rows: rs.map((r, i) => [
544
+ { value: i === ri ? "\u25B8" : "", color: theme.accentBright },
545
+ { value: r.effect, color: r.effect === "ALLOW" ? theme.ok : theme.bad, dim: !r.enabled },
546
+ { value: String(r.priority), dim: true },
547
+ { value: (r.enabled ? "" : "\u2298 ") + truncate(r.toolPattern, 18), color: theme.accent, dim: !r.enabled },
548
+ { value: truncate(r.description || "\u2014", 34), dim: !r.enabled }
549
+ ])
550
+ }
551
+ ) }),
552
+ rs.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "(no rules)" }) : null,
553
+ status ? /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) }) : null
482
554
  ] }) });
483
555
  }
484
556
 
485
557
  // src/tui/panels/RateLimit.tsx
486
558
  import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
487
- import { useEffect as useEffect2, useState as useState3 } from "react";
559
+ import { useEffect as useEffect3, useState as useState3 } from "react";
488
560
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
489
561
  var MODES = ["off", "detect", "block"];
490
562
  var FIELDS = ["mode", "perMinute", "perHour", "perDay"];
491
563
  function RateLimitPanel({ focused }) {
492
564
  const layersQ = useLoader(() => api.settings.getSecurityLayers());
493
565
  const historyQ = useLoader(() => api.settings.getRateLimitHistory());
566
+ const insightsQ = useLoader(() => api.stats.securityInsights(7));
494
567
  const [draft, setDraft] = useState3(null);
495
568
  const [dirty, setDirty] = useState3(false);
496
569
  const [fi, setFi] = useState3(0);
497
570
  const [status, setStatus] = useState3(null);
498
- useEffect2(() => {
571
+ useEffect3(() => {
499
572
  if (layersQ.data && !draft) setDraft({ ...layersQ.data.layers.rateLimit });
500
573
  }, [layersQ.data, draft]);
501
- const adjust = (dir) => {
574
+ const adjust = (dir, step) => {
502
575
  if (!draft) return;
503
576
  const field = FIELDS[fi];
504
577
  if (field === "mode") {
505
578
  const idx = (MODES.indexOf(draft.mode) + dir + MODES.length) % MODES.length;
506
579
  setDraft({ ...draft, mode: MODES[idx] });
507
580
  } else {
508
- const step = 10;
509
581
  setDraft({ ...draft, [field]: Math.max(0, draft[field] + dir * step) });
510
582
  }
511
583
  setDirty(true);
@@ -527,35 +599,81 @@ function RateLimitPanel({ focused }) {
527
599
  };
528
600
  useInput2(
529
601
  (input, key) => {
602
+ const step = key.shift ? 10 : 1;
530
603
  if (key.upArrow) setFi((n) => (n - 1 + FIELDS.length) % FIELDS.length);
531
604
  else if (key.downArrow) setFi((n) => (n + 1) % FIELDS.length);
532
- else if (key.leftArrow) adjust(-1);
533
- else if (key.rightArrow) adjust(1);
605
+ else if (key.leftArrow) adjust(-1, step);
606
+ else if (key.rightArrow) adjust(1, step);
534
607
  else if (input === "s") void save();
535
608
  },
536
609
  { isActive: focused }
537
610
  );
538
- const history = historyQ.data?.history ?? [];
611
+ const history = (historyQ.data?.history ?? []).slice().sort((a, b) => b.ts - a.ts);
612
+ const bursts = (insightsQ.data?.["anomalies"] ?? []).slice(0, 6);
539
613
  return /* @__PURE__ */ jsx3(DataView, { loading: layersQ.loading && !draft, error: layersQ.error, children: draft ? /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
540
- /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: focused ? "\u2191\u2193 field \u2190\u2192 change s save" : "press \u2192 to edit" }),
614
+ /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: focused ? "\u2191\u2193 field \xB7 \u2190\u2192 \xB11 \xB7 shift+\u2190\u2192 \xB110 \xB7 s save" : "press \u2192 to edit" }),
541
615
  /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
542
- /* @__PURE__ */ jsx3(Row, { label: "Mode", active: focused && fi === 0, children: /* @__PURE__ */ jsx3(Text3, { color: modeColor(draft.mode), children: draft.mode }) }),
543
- /* @__PURE__ */ jsx3(Row, { label: "Per minute", active: focused && fi === 1, children: /* @__PURE__ */ jsx3(Text3, { bold: true, children: draft.perMinute }) }),
544
- /* @__PURE__ */ jsx3(Row, { label: "Per hour", active: focused && fi === 2, children: /* @__PURE__ */ jsx3(Text3, { bold: true, children: draft.perHour === 0 ? "off" : draft.perHour }) }),
545
- /* @__PURE__ */ jsx3(Row, { label: "Per day", active: focused && fi === 3, children: /* @__PURE__ */ jsx3(Text3, { bold: true, children: draft.perDay === 0 ? "off" : draft.perDay }) })
616
+ /* @__PURE__ */ jsx3(FieldRow, { label: "Mode", active: focused && fi === 0, children: /* @__PURE__ */ jsx3(Text3, { color: modeColor(draft.mode), children: draft.mode }) }),
617
+ /* @__PURE__ */ jsx3(FieldRow, { label: "Per minute", active: focused && fi === 1, children: /* @__PURE__ */ jsx3(Text3, { bold: true, children: draft.perMinute }) }),
618
+ /* @__PURE__ */ jsx3(FieldRow, { label: "Per hour", active: focused && fi === 2, children: /* @__PURE__ */ jsx3(Text3, { bold: true, children: draft.perHour === 0 ? "off" : draft.perHour }) }),
619
+ /* @__PURE__ */ jsx3(FieldRow, { label: "Per day", active: focused && fi === 3, children: /* @__PURE__ */ jsx3(Text3, { bold: true, children: draft.perDay === 0 ? "off" : draft.perDay }) })
546
620
  ] }),
547
- history.length ? /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, children: [
548
- /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: "history " }),
549
- /* @__PURE__ */ jsx3(Text3, { color: theme.accent, children: sparkline(history.map((h) => h.minute), 40) }),
550
- /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: " " + history.length + " changes" })
551
- ] }) : null,
552
- /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, children: [
621
+ dirty || status ? /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, children: [
553
622
  dirty ? /* @__PURE__ */ jsx3(Text3, { color: theme.warn, children: "\u25CF unsaved \u2014 press s to apply " }) : null,
554
623
  status ? /* @__PURE__ */ jsx3(Text3, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) : null
624
+ ] }) : null,
625
+ /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
626
+ /* @__PURE__ */ jsxs3(Text3, { color: theme.dim, children: [
627
+ "Change history ",
628
+ history.length ? `(${history.length})` : ""
629
+ ] }),
630
+ history.length ? /* @__PURE__ */ jsx3(
631
+ Table,
632
+ {
633
+ columns: [
634
+ { header: "WHEN", width: 8 },
635
+ { header: "/MIN", width: 6 },
636
+ { header: "/HOUR", width: 7 },
637
+ { header: "/DAY", width: 6 }
638
+ ],
639
+ rows: history.slice(0, 5).map((h, i) => [
640
+ { value: ago(h.ts) + " ago", dim: true, bold: i === 0 },
641
+ { value: String(h.minute), bold: i === 0 },
642
+ { value: h.hour === 0 ? "off" : String(h.hour), dim: h.hour === 0 },
643
+ { value: h.day === 0 ? "off" : String(h.day), dim: h.day === 0 }
644
+ ])
645
+ }
646
+ ) : /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: " no changes" })
647
+ ] }),
648
+ /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
649
+ /* @__PURE__ */ jsxs3(Text3, { color: theme.dim, children: [
650
+ "Recent bursts ",
651
+ insightsQ.loading && !insightsQ.data ? "\u2026" : `(${bursts.length})`
652
+ ] }),
653
+ bursts.length ? /* @__PURE__ */ jsx3(
654
+ Table,
655
+ {
656
+ columns: [
657
+ { header: "RESULT", width: 9 },
658
+ { header: "AGENT", width: 16 },
659
+ { header: "CALLS/LIMIT", width: 12 },
660
+ { header: "WHEN", width: 8 }
661
+ ],
662
+ rows: bursts.map((b) => [
663
+ { value: b.blocked ? "blocked" : "BYPASSED", color: b.blocked ? theme.accent : theme.bad, bold: !b.blocked },
664
+ { value: (b.agent || "\u2014").slice(0, 16), dim: true },
665
+ { value: `${b.count}/${b.limit}` },
666
+ { value: ago(b.minute) + " ago", dim: true }
667
+ ])
668
+ }
669
+ ) : /* @__PURE__ */ jsxs3(Text3, { color: theme.dim, children: [
670
+ " ",
671
+ insightsQ.error ? insightsQ.error : "none in 7d"
672
+ ] })
555
673
  ] })
556
674
  ] }) : null });
557
675
  }
558
- function Row({ label, active: active2, children }) {
676
+ function FieldRow({ label, active: active2, children }) {
559
677
  return /* @__PURE__ */ jsxs3(Box3, { children: [
560
678
  /* @__PURE__ */ jsx3(Text3, { color: active2 ? theme.accentBright : void 0, children: (active2 ? "\u25B8 " : " ") + label.padEnd(12) }),
561
679
  children
@@ -564,7 +682,7 @@ function Row({ label, active: active2, children }) {
564
682
 
565
683
  // src/tui/panels/Dlp.tsx
566
684
  import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
567
- import { useEffect as useEffect3, useState as useState4 } from "react";
685
+ import { useEffect as useEffect4, useState as useState4 } from "react";
568
686
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
569
687
  var MODES2 = ["off", "detect", "block"];
570
688
  function DlpPanel({ focused }) {
@@ -573,7 +691,7 @@ function DlpPanel({ focused }) {
573
691
  const [available, setAvailable] = useState4([]);
574
692
  const [sel, setSel] = useState4(0);
575
693
  const [status, setStatus] = useState4(null);
576
- useEffect3(() => {
694
+ useEffect4(() => {
577
695
  if (q.data && !dlp) {
578
696
  setDlp({ ...q.data.layers.dlp, patterns: [...q.data.layers.dlp.patterns] });
579
697
  setAvailable(q.data.availablePatterns);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.60.0",
3
+ "version": "0.61.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": {