@solongate/proxy 0.62.0 → 0.64.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 +238 -88
- package/dist/tui/index.js +237 -87
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -7020,78 +7020,205 @@ var init_hooks = __esm({
|
|
|
7020
7020
|
|
|
7021
7021
|
// src/tui/panels/Live.tsx
|
|
7022
7022
|
import { Box as Box2, Text as Text2 } from "ink";
|
|
7023
|
+
import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
|
|
7023
7024
|
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
7024
|
-
function
|
|
7025
|
-
|
|
7026
|
-
|
|
7027
|
-
|
|
7025
|
+
function ColumnChart({ series, height, width: width2, color, limit = 0 }) {
|
|
7026
|
+
const tailVals = series.slice(-width2);
|
|
7027
|
+
const data = [...new Array(Math.max(0, width2 - tailVals.length)).fill(0), ...tailVals];
|
|
7028
|
+
const max = Math.max(1, ...data, limit);
|
|
7029
|
+
const lines = [];
|
|
7030
|
+
for (let r = height; r >= 1; r--) {
|
|
7031
|
+
const segs = [];
|
|
7032
|
+
for (let i = 0; i < data.length; i++) {
|
|
7033
|
+
const frac = data[i] / max;
|
|
7034
|
+
const ch = frac >= r / height ? "\u2588" : frac >= (r - 0.5) / height ? "\u2584" : r === 1 && data[i] > 0 ? "\u2581" : " ";
|
|
7035
|
+
const c2 = limit > 0 && data[i] > limit ? theme.bad : color;
|
|
7036
|
+
const last = segs[segs.length - 1];
|
|
7037
|
+
if (last && last.color === c2) last.text += ch;
|
|
7038
|
+
else segs.push({ text: ch, color: c2 });
|
|
7039
|
+
}
|
|
7040
|
+
lines.push(
|
|
7041
|
+
/* @__PURE__ */ jsx2(Text2, { children: segs.map((s, i) => /* @__PURE__ */ jsx2(Text2, { color: s.color, children: s.text }, i)) }, r)
|
|
7042
|
+
);
|
|
7043
|
+
}
|
|
7044
|
+
return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: lines });
|
|
7045
|
+
}
|
|
7046
|
+
function HBar({ label, value, max, width: width2, color }) {
|
|
7047
|
+
const filled = max > 0 ? Math.round(value / max * width2) : 0;
|
|
7048
|
+
return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
|
|
7049
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: label.padEnd(9) }),
|
|
7050
|
+
/* @__PURE__ */ jsx2(Text2, { color, children: "\u2588".repeat(Math.min(width2, filled)) }),
|
|
7051
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "\u2591".repeat(Math.max(0, width2 - filled)) + " " + value })
|
|
7028
7052
|
] });
|
|
7029
7053
|
}
|
|
7054
|
+
function SectionTitle({ children, width: width2 }) {
|
|
7055
|
+
const label = `\u2500\u2500 ${children} `;
|
|
7056
|
+
return /* @__PURE__ */ jsx2(Text2, { color: theme.dim, wrap: "truncate", children: label + "\u2500".repeat(Math.max(0, width2 - label.length)) });
|
|
7057
|
+
}
|
|
7030
7058
|
function LivePanel({ active: active2 }) {
|
|
7031
7059
|
const stats = useLoader(() => api.stats.get());
|
|
7032
7060
|
const ts = useLoader(() => api.stats.timeseries({ period: "24h" }));
|
|
7033
|
-
const feed = useLoader(() => api.audit.list({ limit:
|
|
7061
|
+
const feed = useLoader(() => api.audit.list({ limit: 50 }));
|
|
7062
|
+
const insights = useLoader(() => api.stats.securityInsights(7));
|
|
7034
7063
|
usePoll(() => {
|
|
7035
7064
|
stats.reload();
|
|
7036
7065
|
ts.reload();
|
|
7037
7066
|
feed.reload();
|
|
7038
7067
|
}, 2e3, active2);
|
|
7068
|
+
usePoll(insights.reload, 1e4, active2);
|
|
7069
|
+
const [tick, setTick] = useState2(0);
|
|
7070
|
+
useEffect2(() => {
|
|
7071
|
+
if (!active2) return;
|
|
7072
|
+
const t = setInterval(() => setTick((n) => n + 1), 200);
|
|
7073
|
+
return () => clearInterval(t);
|
|
7074
|
+
}, [active2]);
|
|
7075
|
+
const startRef = useRef2(Date.now());
|
|
7076
|
+
const [buffer, setBuffer] = useState2([]);
|
|
7077
|
+
useEffect2(() => {
|
|
7078
|
+
const incoming = feed.data?.entries;
|
|
7079
|
+
if (!incoming?.length) return;
|
|
7080
|
+
setBuffer((prev) => {
|
|
7081
|
+
const seen = new Set(prev.map((e) => e.id));
|
|
7082
|
+
const fresh = incoming.filter((e) => !seen.has(e.id)).sort((a, b) => Date.parse(a.created_at) - Date.parse(b.created_at));
|
|
7083
|
+
return fresh.length ? [...prev, ...fresh].slice(-400) : prev;
|
|
7084
|
+
});
|
|
7085
|
+
}, [feed.data]);
|
|
7086
|
+
const cols = process.stdout.columns ?? 100;
|
|
7087
|
+
const rows = process.stdout.rows ?? 30;
|
|
7039
7088
|
const s = stats.data;
|
|
7040
|
-
const
|
|
7041
|
-
const
|
|
7042
|
-
const
|
|
7043
|
-
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
7089
|
+
const ins = insights.data ?? {};
|
|
7090
|
+
const minuteRaw = (ins.activity?.minute ?? []).map((b) => b.count);
|
|
7091
|
+
const hourRaw = (ins.activity?.hour ?? []).map((b) => b.count);
|
|
7092
|
+
const minuteLive = minuteRaw.some((c2) => c2 > 0);
|
|
7093
|
+
const minuteSeries = minuteLive ? minuteRaw : hourRaw;
|
|
7094
|
+
const activityLabel = minuteLive ? "calls/min \xB7 last 60m" : "calls/hour \xB7 last 24h";
|
|
7095
|
+
const limit = minuteLive ? ins.layers?.rateLimit?.perMinute ?? 0 : 0;
|
|
7096
|
+
const denySeries = (ts.data?.timeseries ?? []).map((p) => p.denied);
|
|
7097
|
+
const spin = SPIN[tick % SPIN.length];
|
|
7098
|
+
const cursor = tick % 4 < 2 ? "\u258C" : " ";
|
|
7099
|
+
const toolCounts = /* @__PURE__ */ new Map();
|
|
7100
|
+
const permCounts = /* @__PURE__ */ new Map();
|
|
7101
|
+
for (const e of buffer) {
|
|
7102
|
+
toolCounts.set(e.tool_name, (toolCounts.get(e.tool_name) ?? 0) + 1);
|
|
7103
|
+
const perm = (e.permission || "?").toUpperCase();
|
|
7104
|
+
permCounts.set(perm, (permCounts.get(perm) ?? 0) + 1);
|
|
7105
|
+
}
|
|
7106
|
+
const topTools = [...toolCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5);
|
|
7107
|
+
const maxTool = topTools[0]?.[1] ?? 1;
|
|
7108
|
+
const perms = ["READ", "WRITE", "EXECUTE", "NETWORK"].map((p) => [p, permCounts.get(p) ?? 0]);
|
|
7109
|
+
const maxPerm = Math.max(1, ...perms.map(([, c2]) => c2));
|
|
7110
|
+
const dlpTop = (ins.dlpByPattern ?? []).slice(0, 2);
|
|
7111
|
+
const denials = buffer.filter((e) => e.decision !== "ALLOW");
|
|
7112
|
+
const lastDeny = denials[denials.length - 1];
|
|
7113
|
+
const bursts = limit > 0 ? minuteSeries.filter((c2) => c2 > limit).length : 0;
|
|
7114
|
+
const bigH = rows >= 34 ? 6 : 4;
|
|
7115
|
+
const colH = rows >= 34 ? 6 : 5;
|
|
7116
|
+
const streamRows = Math.max(4, rows - (2 + 1 + bigH + 1 + colH + 1 + 1));
|
|
7117
|
+
const tail = buffer.slice(-streamRows);
|
|
7118
|
+
const colW = Math.max(18, Math.floor((cols - 6) / 3));
|
|
7119
|
+
if (stats.error) return /* @__PURE__ */ jsxs2(Text2, { color: theme.bad, children: [
|
|
7120
|
+
"\u2717 ",
|
|
7121
|
+
stats.error
|
|
7122
|
+
] });
|
|
7123
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
|
|
7124
|
+
/* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
|
|
7125
|
+
/* @__PURE__ */ jsx2(Text2, { backgroundColor: "#1432A0", color: "#AAC8FA", bold: true, children: " SOLONGATE::LIVE " }),
|
|
7126
|
+
/* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
|
|
7127
|
+
" ",
|
|
7128
|
+
spin,
|
|
7129
|
+
" up ",
|
|
7130
|
+
fmtUp(Date.now() - startRef.current),
|
|
7131
|
+
" \xB7 ",
|
|
7132
|
+
hhmmss(Date.now()),
|
|
7133
|
+
" \xB7 poll 2s"
|
|
7134
|
+
] }),
|
|
7135
|
+
lastDeny ? /* @__PURE__ */ jsxs2(Text2, { color: theme.bad, children: [
|
|
7136
|
+
" \u26A0 ",
|
|
7137
|
+
hhmmss(lastDeny.created_at),
|
|
7138
|
+
" ",
|
|
7139
|
+
lastDeny.tool_name,
|
|
7140
|
+
" denied"
|
|
7141
|
+
] }) : null,
|
|
7142
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " esc menu \xB7 q quit" })
|
|
7047
7143
|
] }),
|
|
7048
|
-
|
|
7049
|
-
/* @__PURE__ */ jsx2(
|
|
7050
|
-
/* @__PURE__ */ jsx2(
|
|
7051
|
-
/* @__PURE__ */ jsx2(
|
|
7052
|
-
/* @__PURE__ */ jsx2(
|
|
7053
|
-
/* @__PURE__ */ jsx2(
|
|
7054
|
-
|
|
7055
|
-
|
|
7056
|
-
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children:
|
|
7057
|
-
/* @__PURE__ */
|
|
7058
|
-
|
|
7059
|
-
|
|
7144
|
+
/* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
|
|
7145
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "calls " }),
|
|
7146
|
+
/* @__PURE__ */ jsx2(Text2, { bold: true, children: s ? s.total_calls : "\xB7\xB7\xB7\xB7" }),
|
|
7147
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " allow " }),
|
|
7148
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: s ? s.allowed : "\xB7\xB7\xB7" }),
|
|
7149
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " deny " }),
|
|
7150
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.bad, bold: true, children: s ? s.denied : "\xB7\xB7" }),
|
|
7151
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " bursts(60m) " }),
|
|
7152
|
+
/* @__PURE__ */ jsx2(Text2, { color: bursts ? theme.bad : theme.dim, children: bursts }),
|
|
7153
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " limit/min " }),
|
|
7154
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: limit || "off" }),
|
|
7155
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " policies " }),
|
|
7156
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: s ? s.active_policies : "\xB7" })
|
|
7157
|
+
] }),
|
|
7158
|
+
/* @__PURE__ */ jsx2(SectionTitle, { width: cols - 2, children: `ACTIVITY ${activityLabel} \xB7 peak ${Math.max(0, ...minuteSeries)}${limit ? ` \xB7 limit ${limit} (red = over)` : ""}` }),
|
|
7159
|
+
minuteSeries.length ? /* @__PURE__ */ jsx2(ColumnChart, { series: minuteSeries, height: bigH, width: cols - 2, color: theme.accent, limit }) : /* @__PURE__ */ jsx2(Box2, { height: bigH, children: /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: insights.loading ? "loading activity\u2026" : "no activity data" }) }),
|
|
7160
|
+
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
7161
|
+
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, marginRight: 2, children: [
|
|
7162
|
+
/* @__PURE__ */ jsx2(SectionTitle, { width: colW, children: "DENIALS 24h" }),
|
|
7163
|
+
/* @__PURE__ */ jsx2(ColumnChart, { series: denySeries, height: colH - 1, width: colW, color: theme.bad })
|
|
7164
|
+
] }),
|
|
7165
|
+
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, marginRight: 2, children: [
|
|
7166
|
+
/* @__PURE__ */ jsx2(SectionTitle, { width: colW, children: "TOP TOOLS (buffer)" }),
|
|
7167
|
+
topTools.length ? topTools.slice(0, colH - 1).map(([tool, count]) => /* @__PURE__ */ jsx2(HBar, { label: truncate2(tool, 9), value: count, max: maxTool, width: Math.max(6, colW - 16), color: theme.accent }, tool)) : /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "no traffic yet" })
|
|
7060
7168
|
] }),
|
|
7061
|
-
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
7062
|
-
/* @__PURE__ */ jsx2(
|
|
7063
|
-
/* @__PURE__ */ jsx2(
|
|
7169
|
+
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, children: [
|
|
7170
|
+
/* @__PURE__ */ jsx2(SectionTitle, { width: colW, children: "PERMISSIONS \xB7 DLP" }),
|
|
7171
|
+
perms.map(([p, c2]) => /* @__PURE__ */ jsx2(HBar, { label: p, value: c2, max: maxPerm, width: Math.max(6, colW - 16), color: p === "EXECUTE" ? theme.warn : theme.accent }, p)),
|
|
7172
|
+
dlpTop.map((d) => /* @__PURE__ */ jsx2(Text2, { wrap: "truncate", color: theme.bad, children: "DLP " + truncate2(d.pattern, colW - 10) + " \xD7" + d.count }, d.pattern))
|
|
7064
7173
|
] })
|
|
7065
7174
|
] }),
|
|
7066
|
-
/* @__PURE__ */
|
|
7067
|
-
|
|
7068
|
-
|
|
7069
|
-
|
|
7070
|
-
|
|
7071
|
-
|
|
7072
|
-
|
|
7073
|
-
|
|
7074
|
-
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
|
-
|
|
7175
|
+
/* @__PURE__ */ jsx2(SectionTitle, { width: cols - 2, children: `TOOL STREAM \xB7 live tail \xB7 ${buffer.length} buffered` }),
|
|
7176
|
+
tail.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "awaiting traffic\u2026" }) : null,
|
|
7177
|
+
tail.map((e) => /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
|
|
7178
|
+
/* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
|
|
7179
|
+
"[",
|
|
7180
|
+
hhmmss(e.created_at),
|
|
7181
|
+
"] "
|
|
7182
|
+
] }),
|
|
7183
|
+
/* @__PURE__ */ jsx2(Text2, { color: decisionColor(e.decision), bold: e.decision !== "ALLOW", children: e.decision.padEnd(6) }),
|
|
7184
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: truncate2(e.tool_name, 14).padEnd(15) }),
|
|
7185
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: (e.permission ?? "").slice(0, 4).padEnd(5) }),
|
|
7186
|
+
e.dlp_matches?.length ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: "DLP! " }) : null,
|
|
7187
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: (e.arguments_summary ? JSON.stringify(e.arguments_summary) : e.reason ?? "").replace(/\s+/g, " ") })
|
|
7188
|
+
] }, e.id)),
|
|
7189
|
+
/* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
|
|
7190
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: "root@solongate" }),
|
|
7191
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ":" }),
|
|
7192
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, children: "~/live" }),
|
|
7193
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "$ " }),
|
|
7194
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, children: cursor })
|
|
7078
7195
|
] })
|
|
7079
|
-
] })
|
|
7196
|
+
] });
|
|
7080
7197
|
}
|
|
7198
|
+
var SPIN, hhmmss, fmtUp;
|
|
7081
7199
|
var init_Live = __esm({
|
|
7082
7200
|
"src/tui/panels/Live.tsx"() {
|
|
7083
7201
|
"use strict";
|
|
7084
7202
|
init_api_client();
|
|
7085
|
-
init_components();
|
|
7086
7203
|
init_hooks();
|
|
7087
7204
|
init_theme();
|
|
7205
|
+
SPIN = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
7206
|
+
hhmmss = (ts) => {
|
|
7207
|
+
const d = new Date(ts);
|
|
7208
|
+
return Number.isNaN(d.getTime()) ? "--:--:--" : d.toTimeString().slice(0, 8);
|
|
7209
|
+
};
|
|
7210
|
+
fmtUp = (ms) => {
|
|
7211
|
+
const s = Math.floor(ms / 1e3);
|
|
7212
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
7213
|
+
return `${p(Math.floor(s / 3600))}:${p(Math.floor(s % 3600 / 60))}:${p(s % 60)}`;
|
|
7214
|
+
};
|
|
7088
7215
|
}
|
|
7089
7216
|
});
|
|
7090
7217
|
|
|
7091
7218
|
// src/tui/panels/Policies.tsx
|
|
7092
7219
|
import { Box as Box3, Text as Text3, useInput } from "ink";
|
|
7093
7220
|
import TextInput from "ink-text-input";
|
|
7094
|
-
import { useEffect as
|
|
7221
|
+
import { useEffect as useEffect3, useState as useState3 } from "react";
|
|
7095
7222
|
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
7096
7223
|
function ruleSummary(r) {
|
|
7097
7224
|
const bits = [];
|
|
@@ -7105,19 +7232,19 @@ function ruleSummary(r) {
|
|
|
7105
7232
|
function PoliciesPanel({ focused }) {
|
|
7106
7233
|
const list4 = useLoader(() => api.policies.list());
|
|
7107
7234
|
const policies = list4.data?.policies ?? [];
|
|
7108
|
-
const [view, setView] =
|
|
7109
|
-
const [pi, setPi] =
|
|
7110
|
-
const [ri, setRi] =
|
|
7111
|
-
const [fi, setFi] =
|
|
7112
|
-
const [rules, setRules] =
|
|
7113
|
-
const [mode, setMode] =
|
|
7114
|
-
const [dirty, setDirty] =
|
|
7115
|
-
const [editing, setEditing] =
|
|
7116
|
-
const [editVal, setEditVal] =
|
|
7117
|
-
const [status, setStatus] =
|
|
7235
|
+
const [view, setView] = useState3("list");
|
|
7236
|
+
const [pi, setPi] = useState3(0);
|
|
7237
|
+
const [ri, setRi] = useState3(0);
|
|
7238
|
+
const [fi, setFi] = useState3(0);
|
|
7239
|
+
const [rules, setRules] = useState3([]);
|
|
7240
|
+
const [mode, setMode] = useState3("denylist");
|
|
7241
|
+
const [dirty, setDirty] = useState3(false);
|
|
7242
|
+
const [editing, setEditing] = useState3(false);
|
|
7243
|
+
const [editVal, setEditVal] = useState3("");
|
|
7244
|
+
const [status, setStatus] = useState3(null);
|
|
7118
7245
|
const selected = policies[Math.min(pi, Math.max(0, policies.length - 1))];
|
|
7119
7246
|
const detail = useLoader(() => selected ? api.policies.get(selected.id) : Promise.resolve(null), [selected?.id]);
|
|
7120
|
-
|
|
7247
|
+
useEffect3(() => {
|
|
7121
7248
|
if (detail.data) {
|
|
7122
7249
|
setRules(detail.data.rules);
|
|
7123
7250
|
setMode(detail.data.mode ?? "denylist");
|
|
@@ -7335,7 +7462,7 @@ var init_Policies = __esm({
|
|
|
7335
7462
|
|
|
7336
7463
|
// src/tui/panels/RateLimit.tsx
|
|
7337
7464
|
import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
|
|
7338
|
-
import { useEffect as
|
|
7465
|
+
import { useEffect as useEffect4, useState as useState4 } from "react";
|
|
7339
7466
|
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
7340
7467
|
function ColoredSpark({ values, limit, width: width2 = 60 }) {
|
|
7341
7468
|
const v = values.length > width2 ? values.slice(values.length - width2) : values;
|
|
@@ -7350,11 +7477,11 @@ function RateLimitPanel({ focused }) {
|
|
|
7350
7477
|
const layersQ = useLoader(() => api.settings.getSecurityLayers());
|
|
7351
7478
|
const historyQ = useLoader(() => api.settings.getRateLimitHistory());
|
|
7352
7479
|
const insightsQ = useLoader(() => api.stats.securityInsights(7));
|
|
7353
|
-
const [draft, setDraft] =
|
|
7354
|
-
const [dirty, setDirty] =
|
|
7355
|
-
const [fi, setFi] =
|
|
7356
|
-
const [status, setStatus] =
|
|
7357
|
-
|
|
7480
|
+
const [draft, setDraft] = useState4(null);
|
|
7481
|
+
const [dirty, setDirty] = useState4(false);
|
|
7482
|
+
const [fi, setFi] = useState4(0);
|
|
7483
|
+
const [status, setStatus] = useState4(null);
|
|
7484
|
+
useEffect4(() => {
|
|
7358
7485
|
if (layersQ.data && !draft) setDraft({ ...layersQ.data.layers.rateLimit });
|
|
7359
7486
|
}, [layersQ.data, draft]);
|
|
7360
7487
|
const adjust = (dir, step) => {
|
|
@@ -7476,19 +7603,19 @@ var init_RateLimit = __esm({
|
|
|
7476
7603
|
// src/tui/panels/Dlp.tsx
|
|
7477
7604
|
import { Box as Box5, Text as Text5, useInput as useInput3 } from "ink";
|
|
7478
7605
|
import TextInput2 from "ink-text-input";
|
|
7479
|
-
import { useEffect as
|
|
7606
|
+
import { useEffect as useEffect5, useState as useState5 } from "react";
|
|
7480
7607
|
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
7481
7608
|
function DlpPanel({ focused }) {
|
|
7482
7609
|
const q = useLoader(() => api.settings.getSecurityLayers());
|
|
7483
|
-
const [dlp, setDlp] =
|
|
7484
|
-
const [available, setAvailable] =
|
|
7485
|
-
const [sel, setSel] =
|
|
7486
|
-
const [dirty, setDirty] =
|
|
7487
|
-
const [status, setStatus] =
|
|
7488
|
-
const [adding, setAdding] =
|
|
7489
|
-
const [newName, setNewName] =
|
|
7490
|
-
const [newRe, setNewRe] =
|
|
7491
|
-
|
|
7610
|
+
const [dlp, setDlp] = useState5(null);
|
|
7611
|
+
const [available, setAvailable] = useState5([]);
|
|
7612
|
+
const [sel, setSel] = useState5(0);
|
|
7613
|
+
const [dirty, setDirty] = useState5(false);
|
|
7614
|
+
const [status, setStatus] = useState5(null);
|
|
7615
|
+
const [adding, setAdding] = useState5(null);
|
|
7616
|
+
const [newName, setNewName] = useState5("");
|
|
7617
|
+
const [newRe, setNewRe] = useState5("");
|
|
7618
|
+
useEffect5(() => {
|
|
7492
7619
|
if (q.data && !dlp) {
|
|
7493
7620
|
setDlp({ ...q.data.layers.dlp, patterns: [...q.data.layers.dlp.patterns], custom: [...q.data.layers.dlp.custom] });
|
|
7494
7621
|
setAvailable(q.data.availablePatterns);
|
|
@@ -7702,17 +7829,17 @@ var init_Stats = __esm({
|
|
|
7702
7829
|
// src/tui/panels/Audit.tsx
|
|
7703
7830
|
import { Box as Box7, Text as Text7, useInput as useInput4 } from "ink";
|
|
7704
7831
|
import TextInput3 from "ink-text-input";
|
|
7705
|
-
import { useState as
|
|
7832
|
+
import { useState as useState6 } from "react";
|
|
7706
7833
|
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
7707
7834
|
function AuditPanel({ active: active2, focused }) {
|
|
7708
|
-
const [di, setDi] =
|
|
7709
|
-
const [gi, setGi] =
|
|
7710
|
-
const [tool, setTool] =
|
|
7711
|
-
const [agent, setAgent] =
|
|
7712
|
-
const [search, setSearch] =
|
|
7713
|
-
const [sel, setSel] =
|
|
7714
|
-
const [view, setView] =
|
|
7715
|
-
const [editing, setEditing] =
|
|
7835
|
+
const [di, setDi] = useState6(0);
|
|
7836
|
+
const [gi, setGi] = useState6(0);
|
|
7837
|
+
const [tool, setTool] = useState6("");
|
|
7838
|
+
const [agent, setAgent] = useState6("");
|
|
7839
|
+
const [search, setSearch] = useState6("");
|
|
7840
|
+
const [sel, setSel] = useState6(0);
|
|
7841
|
+
const [view, setView] = useState6("list");
|
|
7842
|
+
const [editing, setEditing] = useState6(null);
|
|
7716
7843
|
const query = {
|
|
7717
7844
|
filter: DECISIONS[di],
|
|
7718
7845
|
signal: SIGNALS[gi],
|
|
@@ -7880,8 +8007,20 @@ var init_Audit = __esm({
|
|
|
7880
8007
|
|
|
7881
8008
|
// src/tui/App.tsx
|
|
7882
8009
|
import { Box as Box8, Text as Text8, useApp, useInput as useInput5 } from "ink";
|
|
7883
|
-
import { useState as
|
|
8010
|
+
import { useState as useState7 } from "react";
|
|
7884
8011
|
import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
8012
|
+
function LiveHint() {
|
|
8013
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
|
|
8014
|
+
/* @__PURE__ */ jsx8(Text8, { color: theme.accentBright, bold: true, children: "\u25B6 REALTIME CONSOLE" }),
|
|
8015
|
+
/* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "Fullscreen live monitor \u2014 streaming tool calls, active charts," }),
|
|
8016
|
+
/* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "counters, DLP hits and denial alerts. Updates every 2s." }),
|
|
8017
|
+
/* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsxs8(Text8, { children: [
|
|
8018
|
+
"press ",
|
|
8019
|
+
/* @__PURE__ */ jsx8(Text8, { color: theme.accent, children: "enter" }),
|
|
8020
|
+
" to go live"
|
|
8021
|
+
] }) })
|
|
8022
|
+
] });
|
|
8023
|
+
}
|
|
7885
8024
|
function Banner() {
|
|
7886
8025
|
const wide = (process.stdout.columns ?? 80) >= 82;
|
|
7887
8026
|
if (!wide) {
|
|
@@ -7897,8 +8036,8 @@ function Banner() {
|
|
|
7897
8036
|
}
|
|
7898
8037
|
function App() {
|
|
7899
8038
|
const { exit } = useApp();
|
|
7900
|
-
const [section, setSection] =
|
|
7901
|
-
const [focus, setFocus] =
|
|
8039
|
+
const [section, setSection] = useState7(0);
|
|
8040
|
+
const [focus, setFocus] = useState7("nav");
|
|
7902
8041
|
useInput5((input, key) => {
|
|
7903
8042
|
if (focus === "nav") {
|
|
7904
8043
|
if (key.upArrow) setSection((n) => (n - 1 + SECTIONS.length) % SECTIONS.length);
|
|
@@ -7907,17 +8046,23 @@ function App() {
|
|
|
7907
8046
|
else if (input === "q") exit();
|
|
7908
8047
|
} else {
|
|
7909
8048
|
if (key.escape || key.tab) setFocus("nav");
|
|
8049
|
+
else if (input === "q" && SECTIONS[section].label === "Live") exit();
|
|
7910
8050
|
}
|
|
7911
8051
|
});
|
|
8052
|
+
const cols = process.stdout.columns ?? 100;
|
|
8053
|
+
const rows = process.stdout.rows ?? 30;
|
|
7912
8054
|
const current = SECTIONS[section];
|
|
8055
|
+
if (current.label === "Live" && focus === "panel") {
|
|
8056
|
+
return /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", width: cols, height: rows, children: /* @__PURE__ */ jsx8(LivePanel, { active: true, focused: true }) });
|
|
8057
|
+
}
|
|
7913
8058
|
const Panel = current.Panel;
|
|
7914
|
-
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", paddingX: 1, paddingTop: 1, children: [
|
|
8059
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", width: cols, height: rows, paddingX: 1, paddingTop: 1, children: [
|
|
7915
8060
|
/* @__PURE__ */ jsx8(Banner, {}),
|
|
7916
|
-
/* @__PURE__ */ jsxs8(Box8, { marginTop: 1, children: [
|
|
8061
|
+
/* @__PURE__ */ jsxs8(Box8, { marginTop: 1, flexGrow: 1, children: [
|
|
7917
8062
|
/* @__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)) }),
|
|
7918
|
-
/* @__PURE__ */ jsx8(Box8, { flexGrow: 1,
|
|
8063
|
+
/* @__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" }) })
|
|
7919
8064
|
] }),
|
|
7920
|
-
/* @__PURE__ */ jsx8(Box8, {
|
|
8065
|
+
/* @__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"]] }) })
|
|
7921
8066
|
] });
|
|
7922
8067
|
}
|
|
7923
8068
|
var SECTIONS, BANNER_HEX;
|
|
@@ -7934,7 +8079,7 @@ var init_App = __esm({
|
|
|
7934
8079
|
init_Stats();
|
|
7935
8080
|
init_Audit();
|
|
7936
8081
|
SECTIONS = [
|
|
7937
|
-
{ label: "Live", Panel:
|
|
8082
|
+
{ label: "Live", Panel: LiveHint },
|
|
7938
8083
|
{ label: "Policies", Panel: PoliciesPanel },
|
|
7939
8084
|
{ label: "Rate Limit", Panel: RateLimitPanel },
|
|
7940
8085
|
{ label: "DLP", Panel: DlpPanel },
|
|
@@ -7963,8 +8108,13 @@ async function launchTui() {
|
|
|
7963
8108
|
process.stderr.write("Not logged in. Run `solongate login` first.\n");
|
|
7964
8109
|
return;
|
|
7965
8110
|
}
|
|
7966
|
-
|
|
7967
|
-
|
|
8111
|
+
process.stdout.write("\x1B[?1049h\x1B[H");
|
|
8112
|
+
try {
|
|
8113
|
+
const { waitUntilExit } = render(/* @__PURE__ */ jsx9(App, {}));
|
|
8114
|
+
await waitUntilExit();
|
|
8115
|
+
} finally {
|
|
8116
|
+
process.stdout.write("\x1B[?1049l");
|
|
8117
|
+
}
|
|
7968
8118
|
}
|
|
7969
8119
|
var init_tui = __esm({
|
|
7970
8120
|
"src/tui/index.tsx"() {
|
package/dist/tui/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { render } from "ink";
|
|
|
9
9
|
|
|
10
10
|
// src/tui/App.tsx
|
|
11
11
|
import { Box as Box8, Text as Text8, useApp, useInput as useInput5 } from "ink";
|
|
12
|
-
import { useState as
|
|
12
|
+
import { useState as useState7 } from "react";
|
|
13
13
|
|
|
14
14
|
// src/cli-utils.ts
|
|
15
15
|
var c = {
|
|
@@ -126,6 +126,7 @@ function KeyHints({ hints }) {
|
|
|
126
126
|
|
|
127
127
|
// src/tui/panels/Live.tsx
|
|
128
128
|
import { Box as Box2, Text as Text2 } from "ink";
|
|
129
|
+
import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
|
|
129
130
|
|
|
130
131
|
// src/api-client/client.ts
|
|
131
132
|
import { readFileSync, existsSync } from "fs";
|
|
@@ -430,68 +431,194 @@ function usePoll(reload, intervalMs, enabled = true) {
|
|
|
430
431
|
|
|
431
432
|
// src/tui/panels/Live.tsx
|
|
432
433
|
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
434
|
+
var SPIN = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
435
|
+
var hhmmss = (ts) => {
|
|
436
|
+
const d = new Date(ts);
|
|
437
|
+
return Number.isNaN(d.getTime()) ? "--:--:--" : d.toTimeString().slice(0, 8);
|
|
438
|
+
};
|
|
439
|
+
var fmtUp = (ms) => {
|
|
440
|
+
const s = Math.floor(ms / 1e3);
|
|
441
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
442
|
+
return `${p(Math.floor(s / 3600))}:${p(Math.floor(s % 3600 / 60))}:${p(s % 60)}`;
|
|
443
|
+
};
|
|
444
|
+
function ColumnChart({ series, height, width, color, limit = 0 }) {
|
|
445
|
+
const tailVals = series.slice(-width);
|
|
446
|
+
const data = [...new Array(Math.max(0, width - tailVals.length)).fill(0), ...tailVals];
|
|
447
|
+
const max = Math.max(1, ...data, limit);
|
|
448
|
+
const lines = [];
|
|
449
|
+
for (let r = height; r >= 1; r--) {
|
|
450
|
+
const segs = [];
|
|
451
|
+
for (let i = 0; i < data.length; i++) {
|
|
452
|
+
const frac = data[i] / max;
|
|
453
|
+
const ch = frac >= r / height ? "\u2588" : frac >= (r - 0.5) / height ? "\u2584" : r === 1 && data[i] > 0 ? "\u2581" : " ";
|
|
454
|
+
const c2 = limit > 0 && data[i] > limit ? theme.bad : color;
|
|
455
|
+
const last = segs[segs.length - 1];
|
|
456
|
+
if (last && last.color === c2) last.text += ch;
|
|
457
|
+
else segs.push({ text: ch, color: c2 });
|
|
458
|
+
}
|
|
459
|
+
lines.push(
|
|
460
|
+
/* @__PURE__ */ jsx2(Text2, { children: segs.map((s, i) => /* @__PURE__ */ jsx2(Text2, { color: s.color, children: s.text }, i)) }, r)
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: lines });
|
|
464
|
+
}
|
|
465
|
+
function HBar({ label, value, max, width, color }) {
|
|
466
|
+
const filled = max > 0 ? Math.round(value / max * width) : 0;
|
|
467
|
+
return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
|
|
468
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: label.padEnd(9) }),
|
|
469
|
+
/* @__PURE__ */ jsx2(Text2, { color, children: "\u2588".repeat(Math.min(width, filled)) }),
|
|
470
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "\u2591".repeat(Math.max(0, width - filled)) + " " + value })
|
|
437
471
|
] });
|
|
438
472
|
}
|
|
473
|
+
function SectionTitle({ children, width }) {
|
|
474
|
+
const label = `\u2500\u2500 ${children} `;
|
|
475
|
+
return /* @__PURE__ */ jsx2(Text2, { color: theme.dim, wrap: "truncate", children: label + "\u2500".repeat(Math.max(0, width - label.length)) });
|
|
476
|
+
}
|
|
439
477
|
function LivePanel({ active: active2 }) {
|
|
440
478
|
const stats = useLoader(() => api.stats.get());
|
|
441
479
|
const ts = useLoader(() => api.stats.timeseries({ period: "24h" }));
|
|
442
|
-
const feed = useLoader(() => api.audit.list({ limit:
|
|
480
|
+
const feed = useLoader(() => api.audit.list({ limit: 50 }));
|
|
481
|
+
const insights = useLoader(() => api.stats.securityInsights(7));
|
|
443
482
|
usePoll(() => {
|
|
444
483
|
stats.reload();
|
|
445
484
|
ts.reload();
|
|
446
485
|
feed.reload();
|
|
447
486
|
}, 2e3, active2);
|
|
487
|
+
usePoll(insights.reload, 1e4, active2);
|
|
488
|
+
const [tick, setTick] = useState2(0);
|
|
489
|
+
useEffect2(() => {
|
|
490
|
+
if (!active2) return;
|
|
491
|
+
const t = setInterval(() => setTick((n) => n + 1), 200);
|
|
492
|
+
return () => clearInterval(t);
|
|
493
|
+
}, [active2]);
|
|
494
|
+
const startRef = useRef2(Date.now());
|
|
495
|
+
const [buffer, setBuffer] = useState2([]);
|
|
496
|
+
useEffect2(() => {
|
|
497
|
+
const incoming = feed.data?.entries;
|
|
498
|
+
if (!incoming?.length) return;
|
|
499
|
+
setBuffer((prev) => {
|
|
500
|
+
const seen = new Set(prev.map((e) => e.id));
|
|
501
|
+
const fresh = incoming.filter((e) => !seen.has(e.id)).sort((a, b) => Date.parse(a.created_at) - Date.parse(b.created_at));
|
|
502
|
+
return fresh.length ? [...prev, ...fresh].slice(-400) : prev;
|
|
503
|
+
});
|
|
504
|
+
}, [feed.data]);
|
|
505
|
+
const cols = process.stdout.columns ?? 100;
|
|
506
|
+
const rows = process.stdout.rows ?? 30;
|
|
448
507
|
const s = stats.data;
|
|
449
|
-
const
|
|
450
|
-
const
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
508
|
+
const ins = insights.data ?? {};
|
|
509
|
+
const minuteRaw = (ins.activity?.minute ?? []).map((b) => b.count);
|
|
510
|
+
const hourRaw = (ins.activity?.hour ?? []).map((b) => b.count);
|
|
511
|
+
const minuteLive = minuteRaw.some((c2) => c2 > 0);
|
|
512
|
+
const minuteSeries = minuteLive ? minuteRaw : hourRaw;
|
|
513
|
+
const activityLabel = minuteLive ? "calls/min \xB7 last 60m" : "calls/hour \xB7 last 24h";
|
|
514
|
+
const limit = minuteLive ? ins.layers?.rateLimit?.perMinute ?? 0 : 0;
|
|
515
|
+
const denySeries = (ts.data?.timeseries ?? []).map((p) => p.denied);
|
|
516
|
+
const spin = SPIN[tick % SPIN.length];
|
|
517
|
+
const cursor = tick % 4 < 2 ? "\u258C" : " ";
|
|
518
|
+
const toolCounts = /* @__PURE__ */ new Map();
|
|
519
|
+
const permCounts = /* @__PURE__ */ new Map();
|
|
520
|
+
for (const e of buffer) {
|
|
521
|
+
toolCounts.set(e.tool_name, (toolCounts.get(e.tool_name) ?? 0) + 1);
|
|
522
|
+
const perm = (e.permission || "?").toUpperCase();
|
|
523
|
+
permCounts.set(perm, (permCounts.get(perm) ?? 0) + 1);
|
|
524
|
+
}
|
|
525
|
+
const topTools = [...toolCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5);
|
|
526
|
+
const maxTool = topTools[0]?.[1] ?? 1;
|
|
527
|
+
const perms = ["READ", "WRITE", "EXECUTE", "NETWORK"].map((p) => [p, permCounts.get(p) ?? 0]);
|
|
528
|
+
const maxPerm = Math.max(1, ...perms.map(([, c2]) => c2));
|
|
529
|
+
const dlpTop = (ins.dlpByPattern ?? []).slice(0, 2);
|
|
530
|
+
const denials = buffer.filter((e) => e.decision !== "ALLOW");
|
|
531
|
+
const lastDeny = denials[denials.length - 1];
|
|
532
|
+
const bursts = limit > 0 ? minuteSeries.filter((c2) => c2 > limit).length : 0;
|
|
533
|
+
const bigH = rows >= 34 ? 6 : 4;
|
|
534
|
+
const colH = rows >= 34 ? 6 : 5;
|
|
535
|
+
const streamRows = Math.max(4, rows - (2 + 1 + bigH + 1 + colH + 1 + 1));
|
|
536
|
+
const tail = buffer.slice(-streamRows);
|
|
537
|
+
const colW = Math.max(18, Math.floor((cols - 6) / 3));
|
|
538
|
+
if (stats.error) return /* @__PURE__ */ jsxs2(Text2, { color: theme.bad, children: [
|
|
539
|
+
"\u2717 ",
|
|
540
|
+
stats.error
|
|
541
|
+
] });
|
|
542
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
|
|
543
|
+
/* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
|
|
544
|
+
/* @__PURE__ */ jsx2(Text2, { backgroundColor: "#1432A0", color: "#AAC8FA", bold: true, children: " SOLONGATE::LIVE " }),
|
|
545
|
+
/* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
|
|
546
|
+
" ",
|
|
547
|
+
spin,
|
|
548
|
+
" up ",
|
|
549
|
+
fmtUp(Date.now() - startRef.current),
|
|
550
|
+
" \xB7 ",
|
|
551
|
+
hhmmss(Date.now()),
|
|
552
|
+
" \xB7 poll 2s"
|
|
553
|
+
] }),
|
|
554
|
+
lastDeny ? /* @__PURE__ */ jsxs2(Text2, { color: theme.bad, children: [
|
|
555
|
+
" \u26A0 ",
|
|
556
|
+
hhmmss(lastDeny.created_at),
|
|
557
|
+
" ",
|
|
558
|
+
lastDeny.tool_name,
|
|
559
|
+
" denied"
|
|
560
|
+
] }) : null,
|
|
561
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " esc menu \xB7 q quit" })
|
|
456
562
|
] }),
|
|
457
|
-
|
|
458
|
-
/* @__PURE__ */ jsx2(
|
|
459
|
-
/* @__PURE__ */ jsx2(
|
|
460
|
-
/* @__PURE__ */ jsx2(
|
|
461
|
-
/* @__PURE__ */ jsx2(
|
|
462
|
-
/* @__PURE__ */ jsx2(
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children:
|
|
466
|
-
/* @__PURE__ */
|
|
467
|
-
|
|
468
|
-
|
|
563
|
+
/* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
|
|
564
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "calls " }),
|
|
565
|
+
/* @__PURE__ */ jsx2(Text2, { bold: true, children: s ? s.total_calls : "\xB7\xB7\xB7\xB7" }),
|
|
566
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " allow " }),
|
|
567
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: s ? s.allowed : "\xB7\xB7\xB7" }),
|
|
568
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " deny " }),
|
|
569
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.bad, bold: true, children: s ? s.denied : "\xB7\xB7" }),
|
|
570
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " bursts(60m) " }),
|
|
571
|
+
/* @__PURE__ */ jsx2(Text2, { color: bursts ? theme.bad : theme.dim, children: bursts }),
|
|
572
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " limit/min " }),
|
|
573
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: limit || "off" }),
|
|
574
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " policies " }),
|
|
575
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: s ? s.active_policies : "\xB7" })
|
|
576
|
+
] }),
|
|
577
|
+
/* @__PURE__ */ jsx2(SectionTitle, { width: cols - 2, children: `ACTIVITY ${activityLabel} \xB7 peak ${Math.max(0, ...minuteSeries)}${limit ? ` \xB7 limit ${limit} (red = over)` : ""}` }),
|
|
578
|
+
minuteSeries.length ? /* @__PURE__ */ jsx2(ColumnChart, { series: minuteSeries, height: bigH, width: cols - 2, color: theme.accent, limit }) : /* @__PURE__ */ jsx2(Box2, { height: bigH, children: /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: insights.loading ? "loading activity\u2026" : "no activity data" }) }),
|
|
579
|
+
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
580
|
+
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, marginRight: 2, children: [
|
|
581
|
+
/* @__PURE__ */ jsx2(SectionTitle, { width: colW, children: "DENIALS 24h" }),
|
|
582
|
+
/* @__PURE__ */ jsx2(ColumnChart, { series: denySeries, height: colH - 1, width: colW, color: theme.bad })
|
|
583
|
+
] }),
|
|
584
|
+
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, marginRight: 2, children: [
|
|
585
|
+
/* @__PURE__ */ jsx2(SectionTitle, { width: colW, children: "TOP TOOLS (buffer)" }),
|
|
586
|
+
topTools.length ? topTools.slice(0, colH - 1).map(([tool, count]) => /* @__PURE__ */ jsx2(HBar, { label: truncate(tool, 9), value: count, max: maxTool, width: Math.max(6, colW - 16), color: theme.accent }, tool)) : /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "no traffic yet" })
|
|
469
587
|
] }),
|
|
470
|
-
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
471
|
-
/* @__PURE__ */ jsx2(
|
|
472
|
-
/* @__PURE__ */ jsx2(
|
|
588
|
+
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, children: [
|
|
589
|
+
/* @__PURE__ */ jsx2(SectionTitle, { width: colW, children: "PERMISSIONS \xB7 DLP" }),
|
|
590
|
+
perms.map(([p, c2]) => /* @__PURE__ */ jsx2(HBar, { label: p, value: c2, max: maxPerm, width: Math.max(6, colW - 16), color: p === "EXECUTE" ? theme.warn : theme.accent }, p)),
|
|
591
|
+
dlpTop.map((d) => /* @__PURE__ */ jsx2(Text2, { wrap: "truncate", color: theme.bad, children: "DLP " + truncate(d.pattern, colW - 10) + " \xD7" + d.count }, d.pattern))
|
|
473
592
|
] })
|
|
474
593
|
] }),
|
|
475
|
-
/* @__PURE__ */
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
594
|
+
/* @__PURE__ */ jsx2(SectionTitle, { width: cols - 2, children: `TOOL STREAM \xB7 live tail \xB7 ${buffer.length} buffered` }),
|
|
595
|
+
tail.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "awaiting traffic\u2026" }) : null,
|
|
596
|
+
tail.map((e) => /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
|
|
597
|
+
/* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
|
|
598
|
+
"[",
|
|
599
|
+
hhmmss(e.created_at),
|
|
600
|
+
"] "
|
|
601
|
+
] }),
|
|
602
|
+
/* @__PURE__ */ jsx2(Text2, { color: decisionColor(e.decision), bold: e.decision !== "ALLOW", children: e.decision.padEnd(6) }),
|
|
603
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: truncate(e.tool_name, 14).padEnd(15) }),
|
|
604
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: (e.permission ?? "").slice(0, 4).padEnd(5) }),
|
|
605
|
+
e.dlp_matches?.length ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: "DLP! " }) : null,
|
|
606
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: (e.arguments_summary ? JSON.stringify(e.arguments_summary) : e.reason ?? "").replace(/\s+/g, " ") })
|
|
607
|
+
] }, e.id)),
|
|
608
|
+
/* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
|
|
609
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: "root@solongate" }),
|
|
610
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ":" }),
|
|
611
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, children: "~/live" }),
|
|
612
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "$ " }),
|
|
613
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, children: cursor })
|
|
487
614
|
] })
|
|
488
|
-
] })
|
|
615
|
+
] });
|
|
489
616
|
}
|
|
490
617
|
|
|
491
618
|
// src/tui/panels/Policies.tsx
|
|
492
619
|
import { Box as Box3, Text as Text3, useInput } from "ink";
|
|
493
620
|
import TextInput from "ink-text-input";
|
|
494
|
-
import { useEffect as
|
|
621
|
+
import { useEffect as useEffect3, useState as useState3 } from "react";
|
|
495
622
|
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
496
623
|
var constr = (r, g, side) => (r[g]?.[side] ?? []).join(", ");
|
|
497
624
|
var setConstr = (r, g, side, val) => {
|
|
@@ -526,19 +653,19 @@ function ruleSummary(r) {
|
|
|
526
653
|
function PoliciesPanel({ focused }) {
|
|
527
654
|
const list3 = useLoader(() => api.policies.list());
|
|
528
655
|
const policies = list3.data?.policies ?? [];
|
|
529
|
-
const [view, setView] =
|
|
530
|
-
const [pi, setPi] =
|
|
531
|
-
const [ri, setRi] =
|
|
532
|
-
const [fi, setFi] =
|
|
533
|
-
const [rules, setRules] =
|
|
534
|
-
const [mode, setMode] =
|
|
535
|
-
const [dirty, setDirty] =
|
|
536
|
-
const [editing, setEditing] =
|
|
537
|
-
const [editVal, setEditVal] =
|
|
538
|
-
const [status, setStatus] =
|
|
656
|
+
const [view, setView] = useState3("list");
|
|
657
|
+
const [pi, setPi] = useState3(0);
|
|
658
|
+
const [ri, setRi] = useState3(0);
|
|
659
|
+
const [fi, setFi] = useState3(0);
|
|
660
|
+
const [rules, setRules] = useState3([]);
|
|
661
|
+
const [mode, setMode] = useState3("denylist");
|
|
662
|
+
const [dirty, setDirty] = useState3(false);
|
|
663
|
+
const [editing, setEditing] = useState3(false);
|
|
664
|
+
const [editVal, setEditVal] = useState3("");
|
|
665
|
+
const [status, setStatus] = useState3(null);
|
|
539
666
|
const selected = policies[Math.min(pi, Math.max(0, policies.length - 1))];
|
|
540
667
|
const detail = useLoader(() => selected ? api.policies.get(selected.id) : Promise.resolve(null), [selected?.id]);
|
|
541
|
-
|
|
668
|
+
useEffect3(() => {
|
|
542
669
|
if (detail.data) {
|
|
543
670
|
setRules(detail.data.rules);
|
|
544
671
|
setMode(detail.data.mode ?? "denylist");
|
|
@@ -725,7 +852,7 @@ function PoliciesPanel({ focused }) {
|
|
|
725
852
|
|
|
726
853
|
// src/tui/panels/RateLimit.tsx
|
|
727
854
|
import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
|
|
728
|
-
import { useEffect as
|
|
855
|
+
import { useEffect as useEffect4, useState as useState4 } from "react";
|
|
729
856
|
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
730
857
|
var MODES = ["off", "detect", "block"];
|
|
731
858
|
var FIELDS2 = ["mode", "perMinute", "perHour", "perDay"];
|
|
@@ -743,11 +870,11 @@ function RateLimitPanel({ focused }) {
|
|
|
743
870
|
const layersQ = useLoader(() => api.settings.getSecurityLayers());
|
|
744
871
|
const historyQ = useLoader(() => api.settings.getRateLimitHistory());
|
|
745
872
|
const insightsQ = useLoader(() => api.stats.securityInsights(7));
|
|
746
|
-
const [draft, setDraft] =
|
|
747
|
-
const [dirty, setDirty] =
|
|
748
|
-
const [fi, setFi] =
|
|
749
|
-
const [status, setStatus] =
|
|
750
|
-
|
|
873
|
+
const [draft, setDraft] = useState4(null);
|
|
874
|
+
const [dirty, setDirty] = useState4(false);
|
|
875
|
+
const [fi, setFi] = useState4(0);
|
|
876
|
+
const [status, setStatus] = useState4(null);
|
|
877
|
+
useEffect4(() => {
|
|
751
878
|
if (layersQ.data && !draft) setDraft({ ...layersQ.data.layers.rateLimit });
|
|
752
879
|
}, [layersQ.data, draft]);
|
|
753
880
|
const adjust = (dir, step) => {
|
|
@@ -856,20 +983,20 @@ function FieldRow({ label, active: active2, children }) {
|
|
|
856
983
|
// src/tui/panels/Dlp.tsx
|
|
857
984
|
import { Box as Box5, Text as Text5, useInput as useInput3 } from "ink";
|
|
858
985
|
import TextInput2 from "ink-text-input";
|
|
859
|
-
import { useEffect as
|
|
986
|
+
import { useEffect as useEffect5, useState as useState5 } from "react";
|
|
860
987
|
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
861
988
|
var MODES2 = ["off", "detect", "block"];
|
|
862
989
|
function DlpPanel({ focused }) {
|
|
863
990
|
const q = useLoader(() => api.settings.getSecurityLayers());
|
|
864
|
-
const [dlp, setDlp] =
|
|
865
|
-
const [available, setAvailable] =
|
|
866
|
-
const [sel, setSel] =
|
|
867
|
-
const [dirty, setDirty] =
|
|
868
|
-
const [status, setStatus] =
|
|
869
|
-
const [adding, setAdding] =
|
|
870
|
-
const [newName, setNewName] =
|
|
871
|
-
const [newRe, setNewRe] =
|
|
872
|
-
|
|
991
|
+
const [dlp, setDlp] = useState5(null);
|
|
992
|
+
const [available, setAvailable] = useState5([]);
|
|
993
|
+
const [sel, setSel] = useState5(0);
|
|
994
|
+
const [dirty, setDirty] = useState5(false);
|
|
995
|
+
const [status, setStatus] = useState5(null);
|
|
996
|
+
const [adding, setAdding] = useState5(null);
|
|
997
|
+
const [newName, setNewName] = useState5("");
|
|
998
|
+
const [newRe, setNewRe] = useState5("");
|
|
999
|
+
useEffect5(() => {
|
|
873
1000
|
if (q.data && !dlp) {
|
|
874
1001
|
setDlp({ ...q.data.layers.dlp, patterns: [...q.data.layers.dlp.patterns], custom: [...q.data.layers.dlp.custom] });
|
|
875
1002
|
setAvailable(q.data.availablePatterns);
|
|
@@ -1063,19 +1190,19 @@ function StatsPanel({ active: active2 }) {
|
|
|
1063
1190
|
// src/tui/panels/Audit.tsx
|
|
1064
1191
|
import { Box as Box7, Text as Text7, useInput as useInput4 } from "ink";
|
|
1065
1192
|
import TextInput3 from "ink-text-input";
|
|
1066
|
-
import { useState as
|
|
1193
|
+
import { useState as useState6 } from "react";
|
|
1067
1194
|
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1068
1195
|
var DECISIONS = [void 0, "DENY", "ALLOW"];
|
|
1069
1196
|
var SIGNALS = [void 0, "dlp", "ratelimit"];
|
|
1070
1197
|
function AuditPanel({ active: active2, focused }) {
|
|
1071
|
-
const [di, setDi] =
|
|
1072
|
-
const [gi, setGi] =
|
|
1073
|
-
const [tool, setTool] =
|
|
1074
|
-
const [agent, setAgent] =
|
|
1075
|
-
const [search, setSearch] =
|
|
1076
|
-
const [sel, setSel] =
|
|
1077
|
-
const [view, setView] =
|
|
1078
|
-
const [editing, setEditing] =
|
|
1198
|
+
const [di, setDi] = useState6(0);
|
|
1199
|
+
const [gi, setGi] = useState6(0);
|
|
1200
|
+
const [tool, setTool] = useState6("");
|
|
1201
|
+
const [agent, setAgent] = useState6("");
|
|
1202
|
+
const [search, setSearch] = useState6("");
|
|
1203
|
+
const [sel, setSel] = useState6(0);
|
|
1204
|
+
const [view, setView] = useState6("list");
|
|
1205
|
+
const [editing, setEditing] = useState6(null);
|
|
1079
1206
|
const query = {
|
|
1080
1207
|
filter: DECISIONS[di],
|
|
1081
1208
|
signal: SIGNALS[gi],
|
|
@@ -1231,8 +1358,20 @@ function Detail({ label, value, color }) {
|
|
|
1231
1358
|
|
|
1232
1359
|
// src/tui/App.tsx
|
|
1233
1360
|
import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1361
|
+
function LiveHint() {
|
|
1362
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
|
|
1363
|
+
/* @__PURE__ */ jsx8(Text8, { color: theme.accentBright, bold: true, children: "\u25B6 REALTIME CONSOLE" }),
|
|
1364
|
+
/* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "Fullscreen live monitor \u2014 streaming tool calls, active charts," }),
|
|
1365
|
+
/* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "counters, DLP hits and denial alerts. Updates every 2s." }),
|
|
1366
|
+
/* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsxs8(Text8, { children: [
|
|
1367
|
+
"press ",
|
|
1368
|
+
/* @__PURE__ */ jsx8(Text8, { color: theme.accent, children: "enter" }),
|
|
1369
|
+
" to go live"
|
|
1370
|
+
] }) })
|
|
1371
|
+
] });
|
|
1372
|
+
}
|
|
1234
1373
|
var SECTIONS = [
|
|
1235
|
-
{ label: "Live", Panel:
|
|
1374
|
+
{ label: "Live", Panel: LiveHint },
|
|
1236
1375
|
{ label: "Policies", Panel: PoliciesPanel },
|
|
1237
1376
|
{ label: "Rate Limit", Panel: RateLimitPanel },
|
|
1238
1377
|
{ label: "DLP", Panel: DlpPanel },
|
|
@@ -1255,8 +1394,8 @@ function Banner() {
|
|
|
1255
1394
|
}
|
|
1256
1395
|
function App() {
|
|
1257
1396
|
const { exit } = useApp();
|
|
1258
|
-
const [section, setSection] =
|
|
1259
|
-
const [focus, setFocus] =
|
|
1397
|
+
const [section, setSection] = useState7(0);
|
|
1398
|
+
const [focus, setFocus] = useState7("nav");
|
|
1260
1399
|
useInput5((input, key) => {
|
|
1261
1400
|
if (focus === "nav") {
|
|
1262
1401
|
if (key.upArrow) setSection((n) => (n - 1 + SECTIONS.length) % SECTIONS.length);
|
|
@@ -1265,17 +1404,23 @@ function App() {
|
|
|
1265
1404
|
else if (input === "q") exit();
|
|
1266
1405
|
} else {
|
|
1267
1406
|
if (key.escape || key.tab) setFocus("nav");
|
|
1407
|
+
else if (input === "q" && SECTIONS[section].label === "Live") exit();
|
|
1268
1408
|
}
|
|
1269
1409
|
});
|
|
1410
|
+
const cols = process.stdout.columns ?? 100;
|
|
1411
|
+
const rows = process.stdout.rows ?? 30;
|
|
1270
1412
|
const current = SECTIONS[section];
|
|
1413
|
+
if (current.label === "Live" && focus === "panel") {
|
|
1414
|
+
return /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", width: cols, height: rows, children: /* @__PURE__ */ jsx8(LivePanel, { active: true, focused: true }) });
|
|
1415
|
+
}
|
|
1271
1416
|
const Panel = current.Panel;
|
|
1272
|
-
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", paddingX: 1, paddingTop: 1, children: [
|
|
1417
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", width: cols, height: rows, paddingX: 1, paddingTop: 1, children: [
|
|
1273
1418
|
/* @__PURE__ */ jsx8(Banner, {}),
|
|
1274
|
-
/* @__PURE__ */ jsxs8(Box8, { marginTop: 1, children: [
|
|
1419
|
+
/* @__PURE__ */ jsxs8(Box8, { marginTop: 1, flexGrow: 1, children: [
|
|
1275
1420
|
/* @__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)) }),
|
|
1276
|
-
/* @__PURE__ */ jsx8(Box8, { flexGrow: 1,
|
|
1421
|
+
/* @__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" }) })
|
|
1277
1422
|
] }),
|
|
1278
|
-
/* @__PURE__ */ jsx8(Box8, {
|
|
1423
|
+
/* @__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"]] }) })
|
|
1279
1424
|
] });
|
|
1280
1425
|
}
|
|
1281
1426
|
|
|
@@ -1292,8 +1437,13 @@ async function launchTui() {
|
|
|
1292
1437
|
process.stderr.write("Not logged in. Run `solongate login` first.\n");
|
|
1293
1438
|
return;
|
|
1294
1439
|
}
|
|
1295
|
-
|
|
1296
|
-
|
|
1440
|
+
process.stdout.write("\x1B[?1049h\x1B[H");
|
|
1441
|
+
try {
|
|
1442
|
+
const { waitUntilExit } = render(/* @__PURE__ */ jsx9(App, {}));
|
|
1443
|
+
await waitUntilExit();
|
|
1444
|
+
} finally {
|
|
1445
|
+
process.stdout.write("\x1B[?1049l");
|
|
1446
|
+
}
|
|
1297
1447
|
}
|
|
1298
1448
|
export {
|
|
1299
1449
|
launchTui
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solongate/proxy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.64.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": {
|