@solongate/proxy 0.81.54 → 0.81.56

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.
@@ -15,6 +15,8 @@ export interface SavedAccount {
15
15
  export declare function listAccounts(): SavedAccount[];
16
16
  /** Upsert an account (dedupe by apiKey). Called from `login`. */
17
17
  export declare function saveAccount(acc: SavedAccount): void;
18
+ /** Remove a saved account from this device (does NOT revoke the cloud key). */
19
+ export declare function removeAccount(apiKey: string): void;
18
20
  export declare function setViewCredentials(creds: Credentials | null): void;
19
21
  /** True when the given key is the ACTIVE one the guard hooks use. */
20
22
  export declare function isActiveAccount(apiKey: string): boolean;
@@ -61,6 +61,21 @@ export declare function deleteAlert(id: string): Promise<{
61
61
  export declare function setAlertEnabled(id: string, enabled: boolean): Promise<{
62
62
  ok: true;
63
63
  }>;
64
+ export declare function updateAlert(id: string, patch: {
65
+ signal?: AlertRule['signal'];
66
+ threshold?: number;
67
+ windowSeconds?: number;
68
+ emails?: string[];
69
+ telegram?: string[];
70
+ }): Promise<{
71
+ rule: AlertRule;
72
+ }>;
73
+ export declare function sendTestAlert(id: string): Promise<{
74
+ delivered: {
75
+ telegram: boolean;
76
+ email: boolean;
77
+ };
78
+ }>;
64
79
  export interface DenialWebhook {
65
80
  id: string;
66
81
  url: string;
@@ -221,11 +221,13 @@ __export(settings_exports, {
221
221
  getSecurityLayers: () => getSecurityLayers,
222
222
  getSelfProtection: () => getSelfProtection,
223
223
  getWebhooks: () => getWebhooks,
224
+ sendTestAlert: () => sendTestAlert,
224
225
  sendTestWebhook: () => sendTestWebhook,
225
226
  setAlertEnabled: () => setAlertEnabled,
226
227
  setLocalLogs: () => setLocalLogs,
227
228
  setSecurityLayers: () => setSecurityLayers,
228
229
  setSelfProtection: () => setSelfProtection,
230
+ updateAlert: () => updateAlert,
229
231
  updateWebhook: () => updateWebhook
230
232
  });
231
233
  function getSecurityLayers() {
@@ -267,6 +269,12 @@ function deleteAlert(id) {
267
269
  function setAlertEnabled(id, enabled) {
268
270
  return request("PATCH", "/settings/denial-alerts", { query: { id }, body: { enabled } });
269
271
  }
272
+ function updateAlert(id, patch) {
273
+ return request("PATCH", "/settings/denial-alerts", { query: { id }, body: patch });
274
+ }
275
+ function sendTestAlert(id) {
276
+ return request("POST", "/settings/denial-alerts/send-test", { body: { id } });
277
+ }
270
278
  function getWebhooks() {
271
279
  return request("GET", "/settings/denial-webhook");
272
280
  }
package/dist/index.js CHANGED
@@ -7086,6 +7086,20 @@ function saveAccount(acc) {
7086
7086
  } catch {
7087
7087
  }
7088
7088
  }
7089
+ function removeAccount(apiKey) {
7090
+ try {
7091
+ const list6 = (() => {
7092
+ try {
7093
+ const raw = JSON.parse(readFileSync8(accountsFile(), "utf-8"));
7094
+ return Array.isArray(raw) ? raw.filter((a) => a && a.apiKey) : [];
7095
+ } catch {
7096
+ return [];
7097
+ }
7098
+ })();
7099
+ writeFileSync6(accountsFile(), JSON.stringify(list6.filter((a) => a.apiKey !== apiKey), null, 2));
7100
+ } catch {
7101
+ }
7102
+ }
7089
7103
  function setViewCredentials(creds) {
7090
7104
  viewOverride = creds;
7091
7105
  cached2 = null;
@@ -7394,11 +7408,13 @@ __export(settings_exports, {
7394
7408
  getSecurityLayers: () => getSecurityLayers,
7395
7409
  getSelfProtection: () => getSelfProtection,
7396
7410
  getWebhooks: () => getWebhooks,
7411
+ sendTestAlert: () => sendTestAlert,
7397
7412
  sendTestWebhook: () => sendTestWebhook,
7398
7413
  setAlertEnabled: () => setAlertEnabled,
7399
7414
  setLocalLogs: () => setLocalLogs,
7400
7415
  setSecurityLayers: () => setSecurityLayers,
7401
7416
  setSelfProtection: () => setSelfProtection,
7417
+ updateAlert: () => updateAlert,
7402
7418
  updateWebhook: () => updateWebhook
7403
7419
  });
7404
7420
  function getSecurityLayers() {
@@ -7440,6 +7456,12 @@ function deleteAlert(id) {
7440
7456
  function setAlertEnabled(id, enabled) {
7441
7457
  return request("PATCH", "/settings/denial-alerts", { query: { id }, body: { enabled } });
7442
7458
  }
7459
+ function updateAlert(id, patch) {
7460
+ return request("PATCH", "/settings/denial-alerts", { query: { id }, body: patch });
7461
+ }
7462
+ function sendTestAlert(id) {
7463
+ return request("POST", "/settings/denial-alerts/send-test", { body: { id } });
7464
+ }
7443
7465
  function getWebhooks() {
7444
7466
  return request("GET", "/settings/denial-webhook");
7445
7467
  }
@@ -10076,6 +10098,7 @@ function SettingsPanel({
10076
10098
  const [confirmDel, setConfirmDel] = useState7(null);
10077
10099
  const [msg, setMsg] = useState7(null);
10078
10100
  const [busy, setBusy] = useState7(false);
10101
+ const [editor, setEditor] = useState7(null);
10079
10102
  const [accounts, setAccounts] = useState7(() => listAccounts());
10080
10103
  const [login, setLogin] = useState7(null);
10081
10104
  const [tick, setTick] = useState7(0);
@@ -10095,22 +10118,30 @@ function SettingsPanel({
10095
10118
  const localQ = useLoader(() => listAccounts().length ? api.settings.getLocalLogs() : Promise.resolve(null));
10096
10119
  const whQ = useLoader(() => listAccounts().length ? api.settings.getWebhooks() : Promise.resolve(null));
10097
10120
  const alertQ = useLoader(() => listAccounts().length ? api.settings.getAlerts() : Promise.resolve(null));
10121
+ const guardQ = useLoader(() => listAccounts().length ? api.settings.getGuardStatus() : Promise.resolve(null));
10122
+ const selfQ = useLoader(() => listAccounts().length ? api.settings.getSelfProtection() : Promise.resolve(null));
10098
10123
  const local = localQ.data;
10124
+ const guard = guardQ.data;
10125
+ const selfProt = selfQ.data;
10099
10126
  const [hidden, setHidden] = useState7(/* @__PURE__ */ new Set());
10100
10127
  const webhooks = (whQ.data?.webhooks ?? []).filter((w) => !hidden.has("wh:" + w.id));
10101
10128
  const alerts = (alertQ.data?.rules ?? []).filter((r) => !hidden.has("alert:" + r.id));
10129
+ const hasEmailAlert = alerts.some((r) => alertChannel(r) === "email");
10130
+ const hasTgAlert = alerts.some((r) => alertChannel(r) === "telegram");
10102
10131
  const rowsAll = [
10103
10132
  ...accounts.map((acc) => ({ kind: "acct", acc })),
10104
10133
  { kind: "acct-add" },
10105
10134
  ...locked ? [] : [
10135
+ { kind: "guard" },
10136
+ { kind: "self" },
10106
10137
  { kind: "ll-enabled" },
10107
10138
  { kind: "ll-path" },
10108
10139
  { kind: "ll-server" },
10109
10140
  ...webhooks.map((wh) => ({ kind: "wh", wh })),
10110
10141
  { kind: "wh-add" },
10111
10142
  ...alerts.map((rule) => ({ kind: "alert", rule })),
10112
- { kind: "alert-add-email" },
10113
- { kind: "alert-add-tg" }
10143
+ ...hasEmailAlert ? [] : [{ kind: "alert-add-email" }],
10144
+ ...hasTgAlert ? [] : [{ kind: "alert-add-tg" }]
10114
10145
  ]
10115
10146
  ];
10116
10147
  const selClamped = Math.min(sel, rowsAll.length - 1);
@@ -10121,6 +10152,8 @@ function SettingsPanel({
10121
10152
  localQ.reload();
10122
10153
  whQ.reload();
10123
10154
  alertQ.reload();
10155
+ guardQ.reload();
10156
+ selfQ.reload();
10124
10157
  };
10125
10158
  const run12 = (label, fn, reload) => {
10126
10159
  if (busy) return;
@@ -10165,6 +10198,57 @@ function SettingsPanel({
10165
10198
  if (!abort.current) setLogin({ phase: "error", msg: "timed out \u2014 press n to retry" });
10166
10199
  })();
10167
10200
  };
10201
+ const openAlertEditor = (channel, rule) => {
10202
+ setEditor({
10203
+ id: rule?.id,
10204
+ channel,
10205
+ target: (channel === "email" ? rule?.emails?.[0] : rule?.telegram?.[0]) ?? "",
10206
+ signal: rule?.signal ?? "any",
10207
+ threshold: rule?.threshold ?? THRESH_MIN,
10208
+ windowSeconds: rule?.windowSeconds ?? 300,
10209
+ field: rule ? 1 : 0
10210
+ // new rule starts on the target; existing on signal
10211
+ });
10212
+ if (!rule) {
10213
+ setInput("");
10214
+ setEditing("alert-target");
10215
+ }
10216
+ };
10217
+ const normTarget = (channel, raw) => {
10218
+ const v = raw.trim();
10219
+ if (channel === "email") {
10220
+ const email = v.replace(/^mailto:/i, "").replace(/^["'<]+|["'>]+$/g, "").replace(/[.,;:]+$/, "").replace(/\s+/g, "");
10221
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return { value: email, error: "that does not look like an e-mail address" };
10222
+ return { value: email };
10223
+ }
10224
+ const tg = v.replace(/^@/, "").replace(/^(chat[\s_-]*)?id[:=\s]*/i, "").replace(/\s+/g, "");
10225
+ if (!/^-?\d{5,}$/.test(tg)) return { value: tg, error: "a telegram chat id is a number \u2014 get yours from @userinfobot" };
10226
+ return { value: tg };
10227
+ };
10228
+ const saveEditor = () => {
10229
+ if (!editor) return;
10230
+ const t = normTarget(editor.channel, editor.target);
10231
+ if (t.error) {
10232
+ setMsg({ text: "\u2717 " + t.error, level: "bad" });
10233
+ return;
10234
+ }
10235
+ const channels = editor.channel === "email" ? { emails: [t.value], telegram: [] } : { telegram: [t.value], emails: [] };
10236
+ const body = { signal: editor.signal, threshold: editor.threshold, windowSeconds: editor.windowSeconds, ...channels };
10237
+ const id = editor.id;
10238
+ setEditor(null);
10239
+ if (id) run12("alert updated", () => api.settings.updateAlert(id, body), alertQ.reload);
10240
+ else run12(`${editor.channel} alert added`, () => api.settings.createAlert({ name: "dataroom alert", ...body }), alertQ.reload);
10241
+ };
10242
+ const testAlert = (rule) => {
10243
+ run12(
10244
+ "alert test sent \u2014 check your " + (alertChannel(rule) ?? "channel"),
10245
+ () => api.settings.sendTestAlert(rule.id).then((r) => {
10246
+ if (!r.delivered.telegram && !r.delivered.email) throw new Error("no channel on this rule");
10247
+ }),
10248
+ () => {
10249
+ }
10250
+ );
10251
+ };
10168
10252
  const activate = (r) => {
10169
10253
  if (r.kind === "acct") {
10170
10254
  setViewCredentials({ apiKey: r.acc.apiKey, apiUrl: r.acc.apiUrl });
@@ -10172,6 +10256,9 @@ function SettingsPanel({
10172
10256
  setMsg({ text: `viewing ${acctLabel(r.acc)}`, level: "ok" });
10173
10257
  } else if (r.kind === "acct-add") {
10174
10258
  beginLogin();
10259
+ } else if (r.kind === "self") {
10260
+ if (!selfProt) return;
10261
+ run12(selfProt.enabled ? "self-protection disabled" : "self-protection enabled", () => api.settings.setSelfProtection(!selfProt.enabled), selfQ.reload);
10175
10262
  } else if (r.kind === "ll-enabled") {
10176
10263
  if (!local) return;
10177
10264
  if (!local.enabled && !local.path.trim()) {
@@ -10194,13 +10281,12 @@ function SettingsPanel({
10194
10281
  setInput("");
10195
10282
  setEditing("wh-url");
10196
10283
  } else if (r.kind === "alert") {
10197
- run12(r.rule.enabled ? "alert disabled" : "alert enabled", () => api.settings.setAlertEnabled(r.rule.id, !r.rule.enabled), alertQ.reload);
10284
+ const ch = alertChannel(r.rule);
10285
+ if (ch) openAlertEditor(ch, r.rule);
10198
10286
  } else if (r.kind === "alert-add-email") {
10199
- setInput("");
10200
- setEditing("alert-email");
10201
- } else {
10202
- setInput("");
10203
- setEditing("alert-tg");
10287
+ openAlertEditor("email");
10288
+ } else if (r.kind === "alert-add-tg") {
10289
+ openAlertEditor("telegram");
10204
10290
  }
10205
10291
  };
10206
10292
  const submitInput = () => {
@@ -10211,22 +10297,15 @@ function SettingsPanel({
10211
10297
  const enabled = (local?.enabled ?? false) && v.length > 0;
10212
10298
  run12(v ? `path saved${enabled ? "" : " (press enter on enabled to turn on)"}` : "path cleared (local logs off)", () => api.settings.setLocalLogs({ enabled, path: v }), localQ.reload);
10213
10299
  } else if (which === "wh-url") {
10214
- if (!v) return;
10215
- run12("webhook added", () => api.settings.createWebhook({ url: v, events: "denials" }), whQ.reload);
10216
- } else if (which === "alert-email") {
10217
- if (!v) return;
10218
- if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)) {
10219
- setMsg({ text: "\u2717 that does not look like an e-mail address", level: "bad" });
10220
- return;
10221
- }
10222
- run12("email alert added", () => api.settings.createAlert({ ...ALERT_BASE, emails: [v] }), alertQ.reload);
10223
- } else if (which === "alert-tg") {
10224
- if (!v) return;
10225
- if (!/^-?\d{5,}$/.test(v)) {
10226
- setMsg({ text: "\u2717 a telegram chat id is a number \u2014 get yours from @userinfobot", level: "bad" });
10300
+ const url = v.replace(/^["'<]+|["'>]+$/g, "").trim();
10301
+ if (!url) return;
10302
+ if (!/^https?:\/\/\S+$/i.test(url)) {
10303
+ setMsg({ text: "\u2717 webhook url must start with http:// or https://", level: "bad" });
10227
10304
  return;
10228
10305
  }
10229
- run12("telegram alert added", () => api.settings.createAlert({ ...ALERT_BASE, telegram: [v] }), alertQ.reload);
10306
+ run12("webhook added", () => api.settings.createWebhook({ url, events: "denials" }), whQ.reload);
10307
+ } else if (which === "alert-target" && editor) {
10308
+ setEditor({ ...editor, target: v, field: 1 });
10230
10309
  }
10231
10310
  };
10232
10311
  useInput6(
@@ -10238,6 +10317,35 @@ function SettingsPanel({
10238
10317
  }
10239
10318
  return;
10240
10319
  }
10320
+ if (editor) {
10321
+ if (key.escape) {
10322
+ setEditor(null);
10323
+ return;
10324
+ }
10325
+ if (inp === "e" || key.return && editor.field === 0) {
10326
+ setInput(editor.target);
10327
+ setEditing("alert-target");
10328
+ return;
10329
+ }
10330
+ if (inp === "t" && editor.id) {
10331
+ const rule = alerts.find((a) => a.id === editor.id);
10332
+ if (rule) testAlert(rule);
10333
+ return;
10334
+ }
10335
+ if (key.return) {
10336
+ saveEditor();
10337
+ return;
10338
+ }
10339
+ if (key.upArrow) setEditor({ ...editor, field: (editor.field + EDITOR_FIELDS - 1) % EDITOR_FIELDS });
10340
+ else if (key.downArrow) setEditor({ ...editor, field: (editor.field + 1) % EDITOR_FIELDS });
10341
+ else if (key.leftArrow || key.rightArrow) {
10342
+ const d = key.rightArrow ? 1 : -1;
10343
+ if (editor.field === 1) setEditor({ ...editor, signal: SIGNALS2[(SIGNALS2.indexOf(editor.signal) + d + SIGNALS2.length) % SIGNALS2.length] });
10344
+ else if (editor.field === 2) setEditor({ ...editor, threshold: Math.max(THRESH_MIN, Math.min(THRESH_MAX, editor.threshold + d * 5)) });
10345
+ else if (editor.field === 3) setEditor({ ...editor, windowSeconds: WINDOWS[Math.max(0, Math.min(WINDOWS.length - 1, nearestWindowIdx(editor.windowSeconds) + d))] });
10346
+ }
10347
+ return;
10348
+ }
10241
10349
  setMsg(null);
10242
10350
  if (key.upArrow) {
10243
10351
  setSel((n) => Math.max(0, n - 1));
@@ -10251,28 +10359,41 @@ function SettingsPanel({
10251
10359
  const ok = setActiveAccount({ apiKey: cur.acc.apiKey, apiUrl: cur.acc.apiUrl });
10252
10360
  setMsg(ok ? { text: `\u2713 ${acctLabel(cur.acc)} is now the ACTIVE key (guard + logging)`, level: "ok" } : { text: "\u2717 could not set active", level: "bad" });
10253
10361
  refreshAccounts();
10254
- } else if (cur.kind === "wh" || cur.kind === "alert" || cur.kind === "ll-enabled" || cur.kind === "ll-server") activate(cur);
10362
+ } else if (cur.kind === "wh" || cur.kind === "alert" || cur.kind === "self" || cur.kind === "ll-enabled" || cur.kind === "ll-server") {
10363
+ if (cur.kind === "alert") run12(cur.rule.enabled ? "alert disabled" : "alert enabled", () => api.settings.setAlertEnabled(cur.rule.id, !cur.rule.enabled), alertQ.reload);
10364
+ else activate(cur);
10365
+ }
10366
+ } else if (inp === "x" && cur.kind === "acct") {
10367
+ const k = "acct:" + cur.acc.apiKey;
10368
+ if (confirmDel !== k) {
10369
+ setConfirmDel(k);
10370
+ setMsg({ text: `press x again to remove ${acctLabel(cur.acc)} from this device (the cloud account is untouched)`, level: "bad" });
10371
+ return;
10372
+ }
10373
+ setConfirmDel(null);
10374
+ removeAccount(cur.acc.apiKey);
10375
+ setMsg({ text: `\u2713 removed ${acctLabel(cur.acc)} from this device`, level: "ok" });
10376
+ refreshAccounts();
10255
10377
  } else if (inp === "t" && cur.kind === "wh") {
10256
- run12(
10257
- "test event sent \u2014 check your endpoint",
10258
- () => api.settings.sendTestWebhook(cur.wh.id).then((r) => {
10259
- if (!r.delivered) throw new Error("endpoint rejected the test (non-2xx response)");
10260
- }),
10261
- whQ.reload
10262
- );
10263
- } else if (inp === "n") beginLogin();
10378
+ run12("webhook test sent \u2014 check your endpoint", () => api.settings.sendTestWebhook(cur.wh.id).then((r) => {
10379
+ if (!r.delivered) throw new Error("endpoint rejected the test (non-2xx)");
10380
+ }), whQ.reload);
10381
+ } else if (inp === "t" && cur.kind === "alert") {
10382
+ testAlert(cur.rule);
10383
+ } else if (inp === "e" && cur.kind === "ll-path") activate(cur);
10264
10384
  else if (inp === "e" && cur.kind === "wh") {
10265
10385
  const next = EVENTS[(EVENTS.indexOf(cur.wh.events) + 1) % EVENTS.length];
10266
10386
  run12(`webhook events \u2192 ${next}`, () => api.settings.updateWebhook(cur.wh.id, { events: next }), whQ.reload);
10267
- } else if (inp === "e" && cur.kind === "ll-path") activate(cur);
10387
+ } else if (inp === "e" && cur.kind === "alert") {
10388
+ const ch = alertChannel(cur.rule);
10389
+ if (ch) openAlertEditor(ch, cur.rule);
10390
+ } else if (inp === "n") beginLogin();
10268
10391
  else if (inp === "a") {
10269
10392
  if (cur.kind === "wh" || cur.kind === "wh-add") {
10270
10393
  setInput("");
10271
10394
  setEditing("wh-url");
10272
- } else if (cur.kind === "alert" || cur.kind === "alert-add-email" || cur.kind === "alert-add-tg") {
10273
- setInput("");
10274
- setEditing("alert-email");
10275
- }
10395
+ } else if (cur.kind === "alert-add-email") openAlertEditor("email");
10396
+ else if (cur.kind === "alert-add-tg") openAlertEditor("telegram");
10276
10397
  } else if (inp === "d" && (cur.kind === "wh" || cur.kind === "alert")) {
10277
10398
  const k = keyOf(cur);
10278
10399
  if (confirmDel !== k) {
@@ -10312,30 +10433,70 @@ function SettingsPanel({
10312
10433
  ] })
10313
10434
  ] });
10314
10435
  }
10315
- const loading = !locked && (localQ.loading && !localQ.data || whQ.loading && !whQ.data || alertQ.loading && !alertQ.data);
10316
- const error = locked ? null : localQ.error ?? whQ.error ?? alertQ.error;
10436
+ if (editor) {
10437
+ const fieldRow = (idx, label, value, hint) => /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
10438
+ /* @__PURE__ */ jsx7(Text7, { color: editor.field === idx ? theme.accentBright : theme.dim, children: editor.field === idx ? "\u25B8 " : " " }),
10439
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: label.padEnd(11) }),
10440
+ value,
10441
+ editor.field === idx && hint ? /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " " + hint }) : null
10442
+ ] });
10443
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
10444
+ /* @__PURE__ */ jsxs7(Text7, { bold: true, color: theme.accentBright, children: [
10445
+ editor.id ? "Edit" : "New",
10446
+ " ",
10447
+ editor.channel,
10448
+ " alert"
10449
+ ] }),
10450
+ /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
10451
+ "\u2191\u2193 field \xB7 \u2190\u2192 change \xB7 e edit target \xB7 enter save",
10452
+ editor.id ? " \xB7 t test" : "",
10453
+ " \xB7 esc cancel"
10454
+ ] }),
10455
+ editing === "alert-target" ? /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, children: [
10456
+ /* @__PURE__ */ jsx7(Text7, { color: theme.warn, children: editor.channel === "email" ? "e-mail address: " : "telegram chat id (from @userinfobot): " }),
10457
+ /* @__PURE__ */ jsx7(TextInput5, { value: input, onChange: setInput, onSubmit: submitInput })
10458
+ ] }) : msg ? /* @__PURE__ */ jsx7(Text7, { color: msg.level === "bad" ? theme.bad : theme.ok, children: truncate2(msg.text, cols) }) : /* @__PURE__ */ jsx7(Text7, { children: " " }),
10459
+ /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexDirection: "column", children: [
10460
+ fieldRow(0, "target", editor.target ? /* @__PURE__ */ jsx7(Text7, { color: theme.accent, children: truncate2(editor.target, cols - 16) }) : /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "empty \u2014 press e" }), "press e to edit"),
10461
+ fieldRow(1, "signal", /* @__PURE__ */ jsx7(Text7, { color: theme.accent, children: SIGNAL_LABEL[editor.signal] }), "\u2190\u2192 change"),
10462
+ fieldRow(2, "threshold", /* @__PURE__ */ jsx7(Text7, { color: theme.accent, children: `\u2265 ${editor.threshold}` }), "\u2190\u2192 \xB15"),
10463
+ fieldRow(3, "window", /* @__PURE__ */ jsx7(Text7, { color: theme.accent, children: winLabel(editor.windowSeconds) }), "\u2190\u2192 change")
10464
+ ] }),
10465
+ /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
10466
+ "fires when ",
10467
+ /* @__PURE__ */ jsx7(Text7, { color: theme.accent, children: SIGNAL_LABEL[editor.signal] }),
10468
+ " reach ",
10469
+ /* @__PURE__ */ jsx7(Text7, { color: theme.accent, children: editor.threshold }),
10470
+ " within",
10471
+ " ",
10472
+ /* @__PURE__ */ jsx7(Text7, { color: theme.accent, children: winLabel(editor.windowSeconds) })
10473
+ ] }) })
10474
+ ] });
10475
+ }
10476
+ const loading = !locked && (localQ.loading && !localQ.data || whQ.loading && !whQ.data || alertQ.loading && !alertQ.data || guardQ.loading && !guardQ.data || selfQ.loading && !selfQ.data);
10477
+ const error = locked ? null : localQ.error ?? whQ.error ?? alertQ.error ?? guardQ.error ?? selfQ.error;
10317
10478
  const mark = (r) => keyOf(r) === keyOf(cur) && focused ? "\u25B8 " : " ";
10318
10479
  const onOff = (on) => on ? /* @__PURE__ */ jsx7(Text7, { color: theme.ok, children: "on " }) : /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "off" });
10319
10480
  const chanOf = (rule) => [...rule.emails ?? [], ...(rule.telegram ?? []).map((t) => "tg " + t)].join(", ") || "\u2014";
10320
- const listBudget = Math.max(4, rows - 10);
10481
+ const listBudget = Math.max(4, rows - 12);
10321
10482
  const start = Math.min(Math.max(0, selClamped - Math.floor(listBudget / 2)), Math.max(0, rowsAll.length - listBudget));
10322
10483
  const inWindow = (r) => {
10323
10484
  const i = rowsAll.findIndex((x) => keyOf(x) === keyOf(r));
10324
10485
  return i >= start && i < start + listBudget;
10325
10486
  };
10326
10487
  return /* @__PURE__ */ jsx7(DataView, { loading, error, children: /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
10327
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: focused ? "\u2191\u2193 move \xB7 enter/m act \xB7 t test webhook \xB7 n add account \xB7 e events \xB7 d delete \xB7 r refresh" : "press \u2192 to configure" }),
10488
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: focused ? "\u2191\u2193 move \xB7 enter act \xB7 m toggle \xB7 t test \xB7 e edit \xB7 x remove acct \xB7 d delete \xB7 n add acct \xB7 r refresh" : "press \u2192 to configure" }),
10328
10489
  editing ? /* @__PURE__ */ jsxs7(Box7, { children: [
10329
- /* @__PURE__ */ jsx7(Text7, { color: theme.warn, children: editing === "path" ? "local log path: " : editing === "wh-url" ? "webhook url: " : editing === "alert-email" ? "e-mail address: " : "telegram chat id (from @userinfobot): " }),
10490
+ /* @__PURE__ */ jsx7(Text7, { color: theme.warn, children: editing === "path" ? "local log path: " : "webhook url: " }),
10330
10491
  /* @__PURE__ */ jsx7(TextInput5, { value: input, onChange: setInput, onSubmit: submitInput })
10331
10492
  ] }) : msg ? /* @__PURE__ */ jsx7(Text7, { color: msg.level === "bad" ? theme.bad : theme.ok, children: truncate2(msg.text, cols) }) : login?.msg ? /* @__PURE__ */ jsx7(Text7, { color: login.phase === "error" ? theme.bad : theme.ok, children: login.msg }) : /* @__PURE__ */ jsx7(Text7, { children: " " }),
10332
10493
  /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexDirection: "column", children: [
10333
10494
  /* @__PURE__ */ jsxs7(Text7, { bold: true, color: theme.accentBright, children: [
10334
10495
  "ACCOUNTS ",
10335
10496
  /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
10336
- "\u2014 logged in on this device (",
10497
+ "\u2014 on this device (",
10337
10498
  accounts.length,
10338
- ") \xB7 \u25CF viewing = shown here \xB7 ACTIVE = key the guard uses"
10499
+ ") \xB7 \u25CF viewing \xB7 ACTIVE = key the guard uses \xB7 x removes"
10339
10500
  ] })
10340
10501
  ] }),
10341
10502
  accounts.length === 0 ? /* @__PURE__ */ jsxs7(Text7, { children: [
@@ -10351,7 +10512,7 @@ function SettingsPanel({
10351
10512
  /* @__PURE__ */ jsx7(Text7, { color: theme.accent, children: truncate2(acctLabel(a), 30).padEnd(31) }),
10352
10513
  isView ? /* @__PURE__ */ jsx7(Text7, { color: theme.ok, children: "\u25CF viewing " }) : /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " " }),
10353
10514
  isActive ? /* @__PURE__ */ jsx7(Text7, { color: theme.warn, children: "ACTIVE " }) : /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " " }),
10354
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: a.project ? truncate2(a.project, 24) : "" })
10515
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: a.project ? truncate2(a.project, 20) : "" })
10355
10516
  ] }, a.apiKey);
10356
10517
  }),
10357
10518
  inWindow({ kind: "acct-add" }) ? /* @__PURE__ */ jsxs7(Text7, { children: [
@@ -10360,28 +10521,49 @@ function SettingsPanel({
10360
10521
  ] }) : null
10361
10522
  ] }),
10362
10523
  locked ? null : /* @__PURE__ */ jsxs7(Fragment5, { children: [
10524
+ inWindow({ kind: "guard" }) || inWindow({ kind: "self" }) ? /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexDirection: "column", children: [
10525
+ /* @__PURE__ */ jsxs7(Text7, { bold: true, color: theme.accentBright, children: [
10526
+ "PROTECTION ",
10527
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "\u2014 the guard hook enforcing every layer on this project" })
10528
+ ] }),
10529
+ inWindow({ kind: "guard" }) ? /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
10530
+ /* @__PURE__ */ jsx7(Text7, { color: keyOf(cur) === "guard" && focused ? theme.accentBright : theme.dim, children: mark({ kind: "guard" }) }),
10531
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "guard".padEnd(11) }),
10532
+ guard ? /* @__PURE__ */ jsxs7(Fragment5, { children: [
10533
+ /* @__PURE__ */ jsx7(Text7, { color: guard.up_to_date ? theme.ok : theme.warn, children: `v${guard.installed ?? "?"}` }),
10534
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: guard.up_to_date ? " (latest)" : ` \u2192 v${guard.latest} available` }),
10535
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: ` \xB7 ${guard.device_count} device${guard.device_count === 1 ? "" : "s"}${guard.outdated_count ? ` \xB7 ${guard.outdated_count} outdated` : ""}` })
10536
+ ] }) : /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "loading\u2026" })
10537
+ ] }) : null,
10538
+ inWindow({ kind: "self" }) ? /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
10539
+ /* @__PURE__ */ jsx7(Text7, { color: keyOf(cur) === "self" && focused ? theme.accentBright : theme.dim, children: mark({ kind: "self" }) }),
10540
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "self-prot".padEnd(11) }),
10541
+ selfProt ? onOff(selfProt.enabled) : /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "\u2026" }),
10542
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " blocks agents from editing SolonGate\u2019s own hooks/config \xB7 enter toggles" })
10543
+ ] }) : null
10544
+ ] }) : null,
10363
10545
  inWindow({ kind: "ll-enabled" }) || inWindow({ kind: "ll-path" }) || inWindow({ kind: "ll-server" }) ? /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexDirection: "column", children: [
10364
10546
  /* @__PURE__ */ jsxs7(Text7, { bold: true, color: theme.accentBright, children: [
10365
10547
  "LOCAL LOGS ",
10366
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "\u2014 hooks mirror every decision to a file on the agent machine" })
10548
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "\u2014 hooks mirror every decision to a file on this machine" })
10367
10549
  ] }),
10368
- /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
10550
+ inWindow({ kind: "ll-enabled" }) ? /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
10369
10551
  /* @__PURE__ */ jsx7(Text7, { color: keyOf(cur) === "ll-enabled" && focused ? theme.accentBright : theme.dim, children: mark({ kind: "ll-enabled" }) }),
10370
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "enabled ".padEnd(10) }),
10552
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "enabled ".padEnd(11) }),
10371
10553
  onOff(!!local?.enabled),
10372
10554
  /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " enter toggles" })
10373
- ] }),
10374
- /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
10555
+ ] }) : null,
10556
+ inWindow({ kind: "ll-path" }) ? /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
10375
10557
  /* @__PURE__ */ jsx7(Text7, { color: keyOf(cur) === "ll-path" && focused ? theme.accentBright : theme.dim, children: mark({ kind: "ll-path" }) }),
10376
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "path ".padEnd(10) }),
10558
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "path ".padEnd(11) }),
10377
10559
  local?.path ? /* @__PURE__ */ jsx7(Text7, { color: theme.accent, children: truncate2(local.path, cols - 14) }) : /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "not set \u2014 enter to edit" })
10378
- ] }),
10379
- /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
10560
+ ] }) : null,
10561
+ inWindow({ kind: "ll-server" }) ? /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
10380
10562
  /* @__PURE__ */ jsx7(Text7, { color: keyOf(cur) === "ll-server" && focused ? theme.accentBright : theme.dim, children: mark({ kind: "ll-server" }) }),
10381
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "dashboard".padEnd(10) }),
10563
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "dashboard".padEnd(11) }),
10382
10564
  srv.running ? /* @__PURE__ */ jsx7(Text7, { color: theme.ok, children: `running 127.0.0.1:${srv.port}` }) : /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "stopped" }),
10383
10565
  /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: srv.running ? " serves local logs to the dashboard \xB7 survives closing the dataroom \xB7 enter stops" : " enter starts the local-logs link for the dashboard" })
10384
- ] })
10566
+ ] }) : null
10385
10567
  ] }) : null,
10386
10568
  /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexDirection: "column", children: [
10387
10569
  /* @__PURE__ */ jsxs7(Text7, { bold: true, color: theme.accentBright, children: [
@@ -10389,7 +10571,7 @@ function SettingsPanel({
10389
10571
  /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
10390
10572
  "\u2014 POST every matching event to your endpoint (",
10391
10573
  webhooks.length,
10392
- ")"
10574
+ ") \xB7 t tests"
10393
10575
  ] })
10394
10576
  ] }),
10395
10577
  webhooks.filter((wh) => inWindow({ kind: "wh", wh })).map((wh) => /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
@@ -10407,17 +10589,17 @@ function SettingsPanel({
10407
10589
  /* @__PURE__ */ jsxs7(Text7, { bold: true, color: theme.accentBright, children: [
10408
10590
  "ALERTS ",
10409
10591
  /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
10410
- "\u2014 notify on a signal spike (",
10592
+ "\u2014 spike alerts to one email + one telegram (",
10411
10593
  alerts.length,
10412
- ")"
10594
+ ") \xB7 enter edits \xB7 t tests"
10413
10595
  ] })
10414
10596
  ] }),
10415
10597
  alerts.filter((rule) => inWindow({ kind: "alert", rule })).map((rule) => /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
10416
10598
  /* @__PURE__ */ jsx7(Text7, { color: keyOf(cur) === "alert:" + rule.id && focused ? theme.accentBright : theme.dim, children: mark({ kind: "alert", rule }) }),
10417
10599
  onOff(rule.enabled),
10418
- /* @__PURE__ */ jsx7(Text7, { color: theme.warn, children: (" " + rule.signal).padEnd(11) }),
10419
- /* @__PURE__ */ jsx7(Text7, { children: `\u2265${rule.threshold}/${rule.windowSeconds}s ` }),
10420
- /* @__PURE__ */ jsx7(Text7, { color: theme.accent, children: truncate2(chanOf(rule), cols - 26) })
10600
+ /* @__PURE__ */ jsx7(Text7, { color: theme.warn, children: (" " + SIGNAL_LABEL[rule.signal]).padEnd(13) }),
10601
+ /* @__PURE__ */ jsx7(Text7, { children: `\u2265${rule.threshold}/${winLabel(rule.windowSeconds)} `.padEnd(11) }),
10602
+ /* @__PURE__ */ jsx7(Text7, { color: theme.accent, children: truncate2(chanOf(rule), cols - 34) })
10421
10603
  ] }, rule.id)),
10422
10604
  inWindow({ kind: "alert-add-email" }) ? /* @__PURE__ */ jsxs7(Text7, { children: [
10423
10605
  /* @__PURE__ */ jsx7(Text7, { color: cur.kind === "alert-add-email" && focused ? theme.accentBright : theme.dim, children: mark({ kind: "alert-add-email" }) }),
@@ -10425,13 +10607,14 @@ function SettingsPanel({
10425
10607
  ] }) : null,
10426
10608
  inWindow({ kind: "alert-add-tg" }) ? /* @__PURE__ */ jsxs7(Text7, { children: [
10427
10609
  /* @__PURE__ */ jsx7(Text7, { color: cur.kind === "alert-add-tg" && focused ? theme.accentBright : theme.dim, children: mark({ kind: "alert-add-tg" }) }),
10428
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "+ add telegram alert (enter \u2014 numeric chat id, get it from @userinfobot)" })
10429
- ] }) : null
10610
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "+ add telegram alert (enter \u2014 numeric chat id from @userinfobot)" })
10611
+ ] }) : null,
10612
+ hasEmailAlert && hasTgAlert ? /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " both channels in use \u2014 remove one (d) to change it" }) : null
10430
10613
  ] })
10431
10614
  ] })
10432
10615
  ] }) });
10433
10616
  }
10434
- var EVENTS, SPIN2, ALERT_BASE, acctLabel;
10617
+ var EVENTS, SPIN2, SIGNALS2, SIGNAL_LABEL, WINDOWS, winLabel, THRESH_MIN, THRESH_MAX, nearestWindowIdx, EDITOR_FIELDS, acctLabel, alertChannel;
10435
10618
  var init_Settings = __esm({
10436
10619
  "src/tui/panels/Settings.tsx"() {
10437
10620
  "use strict";
@@ -10444,8 +10627,20 @@ var init_Settings = __esm({
10444
10627
  init_theme();
10445
10628
  EVENTS = ["denials", "allowed", "all"];
10446
10629
  SPIN2 = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
10447
- ALERT_BASE = { name: "dataroom alert", threshold: 5, windowSeconds: 60, signal: "any" };
10630
+ SIGNALS2 = ["any", "deny", "dlp", "ratelimit"];
10631
+ SIGNAL_LABEL = { any: "any signal", deny: "denials", dlp: "dlp secrets", ratelimit: "rate-limit" };
10632
+ WINDOWS = [30, 60, 300, 3600, 86400];
10633
+ winLabel = (s) => s < 60 ? `${s}s` : s < 3600 ? `${s / 60}m` : s < 86400 ? `${s / 3600}h` : `${s / 86400}d`;
10634
+ THRESH_MIN = 5;
10635
+ THRESH_MAX = 100;
10636
+ nearestWindowIdx = (s) => {
10637
+ let best = 0;
10638
+ for (let i = 1; i < WINDOWS.length; i++) if (Math.abs(WINDOWS[i] - s) < Math.abs(WINDOWS[best] - s)) best = i;
10639
+ return best;
10640
+ };
10641
+ EDITOR_FIELDS = 4;
10448
10642
  acctLabel = (a) => a.email || a.user || "account \u2026" + a.apiKey.slice(-4);
10643
+ alertChannel = (r) => r.emails && r.emails.length ? "email" : r.telegram && r.telegram.length ? "telegram" : null;
10449
10644
  }
10450
10645
  });
10451
10646
 
package/dist/tui/index.js CHANGED
@@ -283,6 +283,20 @@ function saveAccount(acc) {
283
283
  } catch {
284
284
  }
285
285
  }
286
+ function removeAccount(apiKey) {
287
+ try {
288
+ const list5 = (() => {
289
+ try {
290
+ const raw = JSON.parse(readFileSync3(accountsFile(), "utf-8"));
291
+ return Array.isArray(raw) ? raw.filter((a) => a && a.apiKey) : [];
292
+ } catch {
293
+ return [];
294
+ }
295
+ })();
296
+ writeFileSync2(accountsFile(), JSON.stringify(list5.filter((a) => a.apiKey !== apiKey), null, 2));
297
+ } catch {
298
+ }
299
+ }
286
300
  var viewOverride = null;
287
301
  function setViewCredentials(creds) {
288
302
  viewOverride = creds;
@@ -550,11 +564,13 @@ __export(settings_exports, {
550
564
  getSecurityLayers: () => getSecurityLayers,
551
565
  getSelfProtection: () => getSelfProtection,
552
566
  getWebhooks: () => getWebhooks,
567
+ sendTestAlert: () => sendTestAlert,
553
568
  sendTestWebhook: () => sendTestWebhook,
554
569
  setAlertEnabled: () => setAlertEnabled,
555
570
  setLocalLogs: () => setLocalLogs,
556
571
  setSecurityLayers: () => setSecurityLayers,
557
572
  setSelfProtection: () => setSelfProtection,
573
+ updateAlert: () => updateAlert,
558
574
  updateWebhook: () => updateWebhook
559
575
  });
560
576
  function getSecurityLayers() {
@@ -596,6 +612,12 @@ function deleteAlert(id) {
596
612
  function setAlertEnabled(id, enabled) {
597
613
  return request("PATCH", "/settings/denial-alerts", { query: { id }, body: { enabled } });
598
614
  }
615
+ function updateAlert(id, patch) {
616
+ return request("PATCH", "/settings/denial-alerts", { query: { id }, body: patch });
617
+ }
618
+ function sendTestAlert(id) {
619
+ return request("POST", "/settings/denial-alerts/send-test", { body: { id } });
620
+ }
599
621
  function getWebhooks() {
600
622
  return request("GET", "/settings/denial-webhook");
601
623
  }
@@ -3175,8 +3197,20 @@ function stopLogsServerDaemon() {
3175
3197
  import { Fragment as Fragment5, jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
3176
3198
  var EVENTS = ["denials", "allowed", "all"];
3177
3199
  var SPIN2 = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
3178
- var ALERT_BASE = { name: "dataroom alert", threshold: 5, windowSeconds: 60, signal: "any" };
3200
+ var SIGNALS2 = ["any", "deny", "dlp", "ratelimit"];
3201
+ var SIGNAL_LABEL = { any: "any signal", deny: "denials", dlp: "dlp secrets", ratelimit: "rate-limit" };
3202
+ var WINDOWS = [30, 60, 300, 3600, 86400];
3203
+ var winLabel = (s) => s < 60 ? `${s}s` : s < 3600 ? `${s / 60}m` : s < 86400 ? `${s / 3600}h` : `${s / 86400}d`;
3204
+ var THRESH_MIN = 5;
3205
+ var THRESH_MAX = 100;
3206
+ var nearestWindowIdx = (s) => {
3207
+ let best = 0;
3208
+ for (let i = 1; i < WINDOWS.length; i++) if (Math.abs(WINDOWS[i] - s) < Math.abs(WINDOWS[best] - s)) best = i;
3209
+ return best;
3210
+ };
3211
+ var EDITOR_FIELDS = 4;
3179
3212
  var acctLabel = (a) => a.email || a.user || "account \u2026" + a.apiKey.slice(-4);
3213
+ var alertChannel = (r) => r.emails && r.emails.length ? "email" : r.telegram && r.telegram.length ? "telegram" : null;
3180
3214
  function SettingsPanel({
3181
3215
  active: active2,
3182
3216
  focused,
@@ -3191,6 +3225,7 @@ function SettingsPanel({
3191
3225
  const [confirmDel, setConfirmDel] = useState7(null);
3192
3226
  const [msg, setMsg] = useState7(null);
3193
3227
  const [busy, setBusy] = useState7(false);
3228
+ const [editor, setEditor] = useState7(null);
3194
3229
  const [accounts, setAccounts] = useState7(() => listAccounts());
3195
3230
  const [login, setLogin] = useState7(null);
3196
3231
  const [tick, setTick] = useState7(0);
@@ -3210,22 +3245,30 @@ function SettingsPanel({
3210
3245
  const localQ = useLoader(() => listAccounts().length ? api.settings.getLocalLogs() : Promise.resolve(null));
3211
3246
  const whQ = useLoader(() => listAccounts().length ? api.settings.getWebhooks() : Promise.resolve(null));
3212
3247
  const alertQ = useLoader(() => listAccounts().length ? api.settings.getAlerts() : Promise.resolve(null));
3248
+ const guardQ = useLoader(() => listAccounts().length ? api.settings.getGuardStatus() : Promise.resolve(null));
3249
+ const selfQ = useLoader(() => listAccounts().length ? api.settings.getSelfProtection() : Promise.resolve(null));
3213
3250
  const local = localQ.data;
3251
+ const guard = guardQ.data;
3252
+ const selfProt = selfQ.data;
3214
3253
  const [hidden, setHidden] = useState7(/* @__PURE__ */ new Set());
3215
3254
  const webhooks = (whQ.data?.webhooks ?? []).filter((w) => !hidden.has("wh:" + w.id));
3216
3255
  const alerts = (alertQ.data?.rules ?? []).filter((r) => !hidden.has("alert:" + r.id));
3256
+ const hasEmailAlert = alerts.some((r) => alertChannel(r) === "email");
3257
+ const hasTgAlert = alerts.some((r) => alertChannel(r) === "telegram");
3217
3258
  const rowsAll = [
3218
3259
  ...accounts.map((acc) => ({ kind: "acct", acc })),
3219
3260
  { kind: "acct-add" },
3220
3261
  ...locked ? [] : [
3262
+ { kind: "guard" },
3263
+ { kind: "self" },
3221
3264
  { kind: "ll-enabled" },
3222
3265
  { kind: "ll-path" },
3223
3266
  { kind: "ll-server" },
3224
3267
  ...webhooks.map((wh) => ({ kind: "wh", wh })),
3225
3268
  { kind: "wh-add" },
3226
3269
  ...alerts.map((rule) => ({ kind: "alert", rule })),
3227
- { kind: "alert-add-email" },
3228
- { kind: "alert-add-tg" }
3270
+ ...hasEmailAlert ? [] : [{ kind: "alert-add-email" }],
3271
+ ...hasTgAlert ? [] : [{ kind: "alert-add-tg" }]
3229
3272
  ]
3230
3273
  ];
3231
3274
  const selClamped = Math.min(sel, rowsAll.length - 1);
@@ -3236,6 +3279,8 @@ function SettingsPanel({
3236
3279
  localQ.reload();
3237
3280
  whQ.reload();
3238
3281
  alertQ.reload();
3282
+ guardQ.reload();
3283
+ selfQ.reload();
3239
3284
  };
3240
3285
  const run = (label, fn, reload) => {
3241
3286
  if (busy) return;
@@ -3280,6 +3325,57 @@ function SettingsPanel({
3280
3325
  if (!abort.current) setLogin({ phase: "error", msg: "timed out \u2014 press n to retry" });
3281
3326
  })();
3282
3327
  };
3328
+ const openAlertEditor = (channel, rule) => {
3329
+ setEditor({
3330
+ id: rule?.id,
3331
+ channel,
3332
+ target: (channel === "email" ? rule?.emails?.[0] : rule?.telegram?.[0]) ?? "",
3333
+ signal: rule?.signal ?? "any",
3334
+ threshold: rule?.threshold ?? THRESH_MIN,
3335
+ windowSeconds: rule?.windowSeconds ?? 300,
3336
+ field: rule ? 1 : 0
3337
+ // new rule starts on the target; existing on signal
3338
+ });
3339
+ if (!rule) {
3340
+ setInput("");
3341
+ setEditing("alert-target");
3342
+ }
3343
+ };
3344
+ const normTarget = (channel, raw) => {
3345
+ const v = raw.trim();
3346
+ if (channel === "email") {
3347
+ const email = v.replace(/^mailto:/i, "").replace(/^["'<]+|["'>]+$/g, "").replace(/[.,;:]+$/, "").replace(/\s+/g, "");
3348
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return { value: email, error: "that does not look like an e-mail address" };
3349
+ return { value: email };
3350
+ }
3351
+ const tg = v.replace(/^@/, "").replace(/^(chat[\s_-]*)?id[:=\s]*/i, "").replace(/\s+/g, "");
3352
+ if (!/^-?\d{5,}$/.test(tg)) return { value: tg, error: "a telegram chat id is a number \u2014 get yours from @userinfobot" };
3353
+ return { value: tg };
3354
+ };
3355
+ const saveEditor = () => {
3356
+ if (!editor) return;
3357
+ const t = normTarget(editor.channel, editor.target);
3358
+ if (t.error) {
3359
+ setMsg({ text: "\u2717 " + t.error, level: "bad" });
3360
+ return;
3361
+ }
3362
+ const channels = editor.channel === "email" ? { emails: [t.value], telegram: [] } : { telegram: [t.value], emails: [] };
3363
+ const body = { signal: editor.signal, threshold: editor.threshold, windowSeconds: editor.windowSeconds, ...channels };
3364
+ const id = editor.id;
3365
+ setEditor(null);
3366
+ if (id) run("alert updated", () => api.settings.updateAlert(id, body), alertQ.reload);
3367
+ else run(`${editor.channel} alert added`, () => api.settings.createAlert({ name: "dataroom alert", ...body }), alertQ.reload);
3368
+ };
3369
+ const testAlert = (rule) => {
3370
+ run(
3371
+ "alert test sent \u2014 check your " + (alertChannel(rule) ?? "channel"),
3372
+ () => api.settings.sendTestAlert(rule.id).then((r) => {
3373
+ if (!r.delivered.telegram && !r.delivered.email) throw new Error("no channel on this rule");
3374
+ }),
3375
+ () => {
3376
+ }
3377
+ );
3378
+ };
3283
3379
  const activate = (r) => {
3284
3380
  if (r.kind === "acct") {
3285
3381
  setViewCredentials({ apiKey: r.acc.apiKey, apiUrl: r.acc.apiUrl });
@@ -3287,6 +3383,9 @@ function SettingsPanel({
3287
3383
  setMsg({ text: `viewing ${acctLabel(r.acc)}`, level: "ok" });
3288
3384
  } else if (r.kind === "acct-add") {
3289
3385
  beginLogin();
3386
+ } else if (r.kind === "self") {
3387
+ if (!selfProt) return;
3388
+ run(selfProt.enabled ? "self-protection disabled" : "self-protection enabled", () => api.settings.setSelfProtection(!selfProt.enabled), selfQ.reload);
3290
3389
  } else if (r.kind === "ll-enabled") {
3291
3390
  if (!local) return;
3292
3391
  if (!local.enabled && !local.path.trim()) {
@@ -3309,13 +3408,12 @@ function SettingsPanel({
3309
3408
  setInput("");
3310
3409
  setEditing("wh-url");
3311
3410
  } else if (r.kind === "alert") {
3312
- run(r.rule.enabled ? "alert disabled" : "alert enabled", () => api.settings.setAlertEnabled(r.rule.id, !r.rule.enabled), alertQ.reload);
3411
+ const ch = alertChannel(r.rule);
3412
+ if (ch) openAlertEditor(ch, r.rule);
3313
3413
  } else if (r.kind === "alert-add-email") {
3314
- setInput("");
3315
- setEditing("alert-email");
3316
- } else {
3317
- setInput("");
3318
- setEditing("alert-tg");
3414
+ openAlertEditor("email");
3415
+ } else if (r.kind === "alert-add-tg") {
3416
+ openAlertEditor("telegram");
3319
3417
  }
3320
3418
  };
3321
3419
  const submitInput = () => {
@@ -3326,22 +3424,15 @@ function SettingsPanel({
3326
3424
  const enabled = (local?.enabled ?? false) && v.length > 0;
3327
3425
  run(v ? `path saved${enabled ? "" : " (press enter on enabled to turn on)"}` : "path cleared (local logs off)", () => api.settings.setLocalLogs({ enabled, path: v }), localQ.reload);
3328
3426
  } else if (which === "wh-url") {
3329
- if (!v) return;
3330
- run("webhook added", () => api.settings.createWebhook({ url: v, events: "denials" }), whQ.reload);
3331
- } else if (which === "alert-email") {
3332
- if (!v) return;
3333
- if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)) {
3334
- setMsg({ text: "\u2717 that does not look like an e-mail address", level: "bad" });
3427
+ const url = v.replace(/^["'<]+|["'>]+$/g, "").trim();
3428
+ if (!url) return;
3429
+ if (!/^https?:\/\/\S+$/i.test(url)) {
3430
+ setMsg({ text: "\u2717 webhook url must start with http:// or https://", level: "bad" });
3335
3431
  return;
3336
3432
  }
3337
- run("email alert added", () => api.settings.createAlert({ ...ALERT_BASE, emails: [v] }), alertQ.reload);
3338
- } else if (which === "alert-tg") {
3339
- if (!v) return;
3340
- if (!/^-?\d{5,}$/.test(v)) {
3341
- setMsg({ text: "\u2717 a telegram chat id is a number \u2014 get yours from @userinfobot", level: "bad" });
3342
- return;
3343
- }
3344
- run("telegram alert added", () => api.settings.createAlert({ ...ALERT_BASE, telegram: [v] }), alertQ.reload);
3433
+ run("webhook added", () => api.settings.createWebhook({ url, events: "denials" }), whQ.reload);
3434
+ } else if (which === "alert-target" && editor) {
3435
+ setEditor({ ...editor, target: v, field: 1 });
3345
3436
  }
3346
3437
  };
3347
3438
  useInput6(
@@ -3353,6 +3444,35 @@ function SettingsPanel({
3353
3444
  }
3354
3445
  return;
3355
3446
  }
3447
+ if (editor) {
3448
+ if (key.escape) {
3449
+ setEditor(null);
3450
+ return;
3451
+ }
3452
+ if (inp === "e" || key.return && editor.field === 0) {
3453
+ setInput(editor.target);
3454
+ setEditing("alert-target");
3455
+ return;
3456
+ }
3457
+ if (inp === "t" && editor.id) {
3458
+ const rule = alerts.find((a) => a.id === editor.id);
3459
+ if (rule) testAlert(rule);
3460
+ return;
3461
+ }
3462
+ if (key.return) {
3463
+ saveEditor();
3464
+ return;
3465
+ }
3466
+ if (key.upArrow) setEditor({ ...editor, field: (editor.field + EDITOR_FIELDS - 1) % EDITOR_FIELDS });
3467
+ else if (key.downArrow) setEditor({ ...editor, field: (editor.field + 1) % EDITOR_FIELDS });
3468
+ else if (key.leftArrow || key.rightArrow) {
3469
+ const d = key.rightArrow ? 1 : -1;
3470
+ if (editor.field === 1) setEditor({ ...editor, signal: SIGNALS2[(SIGNALS2.indexOf(editor.signal) + d + SIGNALS2.length) % SIGNALS2.length] });
3471
+ else if (editor.field === 2) setEditor({ ...editor, threshold: Math.max(THRESH_MIN, Math.min(THRESH_MAX, editor.threshold + d * 5)) });
3472
+ else if (editor.field === 3) setEditor({ ...editor, windowSeconds: WINDOWS[Math.max(0, Math.min(WINDOWS.length - 1, nearestWindowIdx(editor.windowSeconds) + d))] });
3473
+ }
3474
+ return;
3475
+ }
3356
3476
  setMsg(null);
3357
3477
  if (key.upArrow) {
3358
3478
  setSel((n) => Math.max(0, n - 1));
@@ -3366,28 +3486,41 @@ function SettingsPanel({
3366
3486
  const ok = setActiveAccount({ apiKey: cur.acc.apiKey, apiUrl: cur.acc.apiUrl });
3367
3487
  setMsg(ok ? { text: `\u2713 ${acctLabel(cur.acc)} is now the ACTIVE key (guard + logging)`, level: "ok" } : { text: "\u2717 could not set active", level: "bad" });
3368
3488
  refreshAccounts();
3369
- } else if (cur.kind === "wh" || cur.kind === "alert" || cur.kind === "ll-enabled" || cur.kind === "ll-server") activate(cur);
3489
+ } else if (cur.kind === "wh" || cur.kind === "alert" || cur.kind === "self" || cur.kind === "ll-enabled" || cur.kind === "ll-server") {
3490
+ if (cur.kind === "alert") run(cur.rule.enabled ? "alert disabled" : "alert enabled", () => api.settings.setAlertEnabled(cur.rule.id, !cur.rule.enabled), alertQ.reload);
3491
+ else activate(cur);
3492
+ }
3493
+ } else if (inp === "x" && cur.kind === "acct") {
3494
+ const k = "acct:" + cur.acc.apiKey;
3495
+ if (confirmDel !== k) {
3496
+ setConfirmDel(k);
3497
+ setMsg({ text: `press x again to remove ${acctLabel(cur.acc)} from this device (the cloud account is untouched)`, level: "bad" });
3498
+ return;
3499
+ }
3500
+ setConfirmDel(null);
3501
+ removeAccount(cur.acc.apiKey);
3502
+ setMsg({ text: `\u2713 removed ${acctLabel(cur.acc)} from this device`, level: "ok" });
3503
+ refreshAccounts();
3370
3504
  } else if (inp === "t" && cur.kind === "wh") {
3371
- run(
3372
- "test event sent \u2014 check your endpoint",
3373
- () => api.settings.sendTestWebhook(cur.wh.id).then((r) => {
3374
- if (!r.delivered) throw new Error("endpoint rejected the test (non-2xx response)");
3375
- }),
3376
- whQ.reload
3377
- );
3378
- } else if (inp === "n") beginLogin();
3505
+ run("webhook test sent \u2014 check your endpoint", () => api.settings.sendTestWebhook(cur.wh.id).then((r) => {
3506
+ if (!r.delivered) throw new Error("endpoint rejected the test (non-2xx)");
3507
+ }), whQ.reload);
3508
+ } else if (inp === "t" && cur.kind === "alert") {
3509
+ testAlert(cur.rule);
3510
+ } else if (inp === "e" && cur.kind === "ll-path") activate(cur);
3379
3511
  else if (inp === "e" && cur.kind === "wh") {
3380
3512
  const next = EVENTS[(EVENTS.indexOf(cur.wh.events) + 1) % EVENTS.length];
3381
3513
  run(`webhook events \u2192 ${next}`, () => api.settings.updateWebhook(cur.wh.id, { events: next }), whQ.reload);
3382
- } else if (inp === "e" && cur.kind === "ll-path") activate(cur);
3514
+ } else if (inp === "e" && cur.kind === "alert") {
3515
+ const ch = alertChannel(cur.rule);
3516
+ if (ch) openAlertEditor(ch, cur.rule);
3517
+ } else if (inp === "n") beginLogin();
3383
3518
  else if (inp === "a") {
3384
3519
  if (cur.kind === "wh" || cur.kind === "wh-add") {
3385
3520
  setInput("");
3386
3521
  setEditing("wh-url");
3387
- } else if (cur.kind === "alert" || cur.kind === "alert-add-email" || cur.kind === "alert-add-tg") {
3388
- setInput("");
3389
- setEditing("alert-email");
3390
- }
3522
+ } else if (cur.kind === "alert-add-email") openAlertEditor("email");
3523
+ else if (cur.kind === "alert-add-tg") openAlertEditor("telegram");
3391
3524
  } else if (inp === "d" && (cur.kind === "wh" || cur.kind === "alert")) {
3392
3525
  const k = keyOf(cur);
3393
3526
  if (confirmDel !== k) {
@@ -3427,30 +3560,70 @@ function SettingsPanel({
3427
3560
  ] })
3428
3561
  ] });
3429
3562
  }
3430
- const loading = !locked && (localQ.loading && !localQ.data || whQ.loading && !whQ.data || alertQ.loading && !alertQ.data);
3431
- const error = locked ? null : localQ.error ?? whQ.error ?? alertQ.error;
3563
+ if (editor) {
3564
+ const fieldRow = (idx, label, value, hint) => /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
3565
+ /* @__PURE__ */ jsx7(Text7, { color: editor.field === idx ? theme.accentBright : theme.dim, children: editor.field === idx ? "\u25B8 " : " " }),
3566
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: label.padEnd(11) }),
3567
+ value,
3568
+ editor.field === idx && hint ? /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " " + hint }) : null
3569
+ ] });
3570
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
3571
+ /* @__PURE__ */ jsxs7(Text7, { bold: true, color: theme.accentBright, children: [
3572
+ editor.id ? "Edit" : "New",
3573
+ " ",
3574
+ editor.channel,
3575
+ " alert"
3576
+ ] }),
3577
+ /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
3578
+ "\u2191\u2193 field \xB7 \u2190\u2192 change \xB7 e edit target \xB7 enter save",
3579
+ editor.id ? " \xB7 t test" : "",
3580
+ " \xB7 esc cancel"
3581
+ ] }),
3582
+ editing === "alert-target" ? /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, children: [
3583
+ /* @__PURE__ */ jsx7(Text7, { color: theme.warn, children: editor.channel === "email" ? "e-mail address: " : "telegram chat id (from @userinfobot): " }),
3584
+ /* @__PURE__ */ jsx7(TextInput5, { value: input, onChange: setInput, onSubmit: submitInput })
3585
+ ] }) : msg ? /* @__PURE__ */ jsx7(Text7, { color: msg.level === "bad" ? theme.bad : theme.ok, children: truncate(msg.text, cols) }) : /* @__PURE__ */ jsx7(Text7, { children: " " }),
3586
+ /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexDirection: "column", children: [
3587
+ fieldRow(0, "target", editor.target ? /* @__PURE__ */ jsx7(Text7, { color: theme.accent, children: truncate(editor.target, cols - 16) }) : /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "empty \u2014 press e" }), "press e to edit"),
3588
+ fieldRow(1, "signal", /* @__PURE__ */ jsx7(Text7, { color: theme.accent, children: SIGNAL_LABEL[editor.signal] }), "\u2190\u2192 change"),
3589
+ fieldRow(2, "threshold", /* @__PURE__ */ jsx7(Text7, { color: theme.accent, children: `\u2265 ${editor.threshold}` }), "\u2190\u2192 \xB15"),
3590
+ fieldRow(3, "window", /* @__PURE__ */ jsx7(Text7, { color: theme.accent, children: winLabel(editor.windowSeconds) }), "\u2190\u2192 change")
3591
+ ] }),
3592
+ /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
3593
+ "fires when ",
3594
+ /* @__PURE__ */ jsx7(Text7, { color: theme.accent, children: SIGNAL_LABEL[editor.signal] }),
3595
+ " reach ",
3596
+ /* @__PURE__ */ jsx7(Text7, { color: theme.accent, children: editor.threshold }),
3597
+ " within",
3598
+ " ",
3599
+ /* @__PURE__ */ jsx7(Text7, { color: theme.accent, children: winLabel(editor.windowSeconds) })
3600
+ ] }) })
3601
+ ] });
3602
+ }
3603
+ const loading = !locked && (localQ.loading && !localQ.data || whQ.loading && !whQ.data || alertQ.loading && !alertQ.data || guardQ.loading && !guardQ.data || selfQ.loading && !selfQ.data);
3604
+ const error = locked ? null : localQ.error ?? whQ.error ?? alertQ.error ?? guardQ.error ?? selfQ.error;
3432
3605
  const mark = (r) => keyOf(r) === keyOf(cur) && focused ? "\u25B8 " : " ";
3433
3606
  const onOff = (on) => on ? /* @__PURE__ */ jsx7(Text7, { color: theme.ok, children: "on " }) : /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "off" });
3434
3607
  const chanOf = (rule) => [...rule.emails ?? [], ...(rule.telegram ?? []).map((t) => "tg " + t)].join(", ") || "\u2014";
3435
- const listBudget = Math.max(4, rows - 10);
3608
+ const listBudget = Math.max(4, rows - 12);
3436
3609
  const start = Math.min(Math.max(0, selClamped - Math.floor(listBudget / 2)), Math.max(0, rowsAll.length - listBudget));
3437
3610
  const inWindow = (r) => {
3438
3611
  const i = rowsAll.findIndex((x) => keyOf(x) === keyOf(r));
3439
3612
  return i >= start && i < start + listBudget;
3440
3613
  };
3441
3614
  return /* @__PURE__ */ jsx7(DataView, { loading, error, children: /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
3442
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: focused ? "\u2191\u2193 move \xB7 enter/m act \xB7 t test webhook \xB7 n add account \xB7 e events \xB7 d delete \xB7 r refresh" : "press \u2192 to configure" }),
3615
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: focused ? "\u2191\u2193 move \xB7 enter act \xB7 m toggle \xB7 t test \xB7 e edit \xB7 x remove acct \xB7 d delete \xB7 n add acct \xB7 r refresh" : "press \u2192 to configure" }),
3443
3616
  editing ? /* @__PURE__ */ jsxs7(Box7, { children: [
3444
- /* @__PURE__ */ jsx7(Text7, { color: theme.warn, children: editing === "path" ? "local log path: " : editing === "wh-url" ? "webhook url: " : editing === "alert-email" ? "e-mail address: " : "telegram chat id (from @userinfobot): " }),
3617
+ /* @__PURE__ */ jsx7(Text7, { color: theme.warn, children: editing === "path" ? "local log path: " : "webhook url: " }),
3445
3618
  /* @__PURE__ */ jsx7(TextInput5, { value: input, onChange: setInput, onSubmit: submitInput })
3446
3619
  ] }) : msg ? /* @__PURE__ */ jsx7(Text7, { color: msg.level === "bad" ? theme.bad : theme.ok, children: truncate(msg.text, cols) }) : login?.msg ? /* @__PURE__ */ jsx7(Text7, { color: login.phase === "error" ? theme.bad : theme.ok, children: login.msg }) : /* @__PURE__ */ jsx7(Text7, { children: " " }),
3447
3620
  /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexDirection: "column", children: [
3448
3621
  /* @__PURE__ */ jsxs7(Text7, { bold: true, color: theme.accentBright, children: [
3449
3622
  "ACCOUNTS ",
3450
3623
  /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
3451
- "\u2014 logged in on this device (",
3624
+ "\u2014 on this device (",
3452
3625
  accounts.length,
3453
- ") \xB7 \u25CF viewing = shown here \xB7 ACTIVE = key the guard uses"
3626
+ ") \xB7 \u25CF viewing \xB7 ACTIVE = key the guard uses \xB7 x removes"
3454
3627
  ] })
3455
3628
  ] }),
3456
3629
  accounts.length === 0 ? /* @__PURE__ */ jsxs7(Text7, { children: [
@@ -3466,7 +3639,7 @@ function SettingsPanel({
3466
3639
  /* @__PURE__ */ jsx7(Text7, { color: theme.accent, children: truncate(acctLabel(a), 30).padEnd(31) }),
3467
3640
  isView ? /* @__PURE__ */ jsx7(Text7, { color: theme.ok, children: "\u25CF viewing " }) : /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " " }),
3468
3641
  isActive ? /* @__PURE__ */ jsx7(Text7, { color: theme.warn, children: "ACTIVE " }) : /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " " }),
3469
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: a.project ? truncate(a.project, 24) : "" })
3642
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: a.project ? truncate(a.project, 20) : "" })
3470
3643
  ] }, a.apiKey);
3471
3644
  }),
3472
3645
  inWindow({ kind: "acct-add" }) ? /* @__PURE__ */ jsxs7(Text7, { children: [
@@ -3475,28 +3648,49 @@ function SettingsPanel({
3475
3648
  ] }) : null
3476
3649
  ] }),
3477
3650
  locked ? null : /* @__PURE__ */ jsxs7(Fragment5, { children: [
3651
+ inWindow({ kind: "guard" }) || inWindow({ kind: "self" }) ? /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexDirection: "column", children: [
3652
+ /* @__PURE__ */ jsxs7(Text7, { bold: true, color: theme.accentBright, children: [
3653
+ "PROTECTION ",
3654
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "\u2014 the guard hook enforcing every layer on this project" })
3655
+ ] }),
3656
+ inWindow({ kind: "guard" }) ? /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
3657
+ /* @__PURE__ */ jsx7(Text7, { color: keyOf(cur) === "guard" && focused ? theme.accentBright : theme.dim, children: mark({ kind: "guard" }) }),
3658
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "guard".padEnd(11) }),
3659
+ guard ? /* @__PURE__ */ jsxs7(Fragment5, { children: [
3660
+ /* @__PURE__ */ jsx7(Text7, { color: guard.up_to_date ? theme.ok : theme.warn, children: `v${guard.installed ?? "?"}` }),
3661
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: guard.up_to_date ? " (latest)" : ` \u2192 v${guard.latest} available` }),
3662
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: ` \xB7 ${guard.device_count} device${guard.device_count === 1 ? "" : "s"}${guard.outdated_count ? ` \xB7 ${guard.outdated_count} outdated` : ""}` })
3663
+ ] }) : /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "loading\u2026" })
3664
+ ] }) : null,
3665
+ inWindow({ kind: "self" }) ? /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
3666
+ /* @__PURE__ */ jsx7(Text7, { color: keyOf(cur) === "self" && focused ? theme.accentBright : theme.dim, children: mark({ kind: "self" }) }),
3667
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "self-prot".padEnd(11) }),
3668
+ selfProt ? onOff(selfProt.enabled) : /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "\u2026" }),
3669
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " blocks agents from editing SolonGate\u2019s own hooks/config \xB7 enter toggles" })
3670
+ ] }) : null
3671
+ ] }) : null,
3478
3672
  inWindow({ kind: "ll-enabled" }) || inWindow({ kind: "ll-path" }) || inWindow({ kind: "ll-server" }) ? /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexDirection: "column", children: [
3479
3673
  /* @__PURE__ */ jsxs7(Text7, { bold: true, color: theme.accentBright, children: [
3480
3674
  "LOCAL LOGS ",
3481
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "\u2014 hooks mirror every decision to a file on the agent machine" })
3675
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "\u2014 hooks mirror every decision to a file on this machine" })
3482
3676
  ] }),
3483
- /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
3677
+ inWindow({ kind: "ll-enabled" }) ? /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
3484
3678
  /* @__PURE__ */ jsx7(Text7, { color: keyOf(cur) === "ll-enabled" && focused ? theme.accentBright : theme.dim, children: mark({ kind: "ll-enabled" }) }),
3485
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "enabled ".padEnd(10) }),
3679
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "enabled ".padEnd(11) }),
3486
3680
  onOff(!!local?.enabled),
3487
3681
  /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " enter toggles" })
3488
- ] }),
3489
- /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
3682
+ ] }) : null,
3683
+ inWindow({ kind: "ll-path" }) ? /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
3490
3684
  /* @__PURE__ */ jsx7(Text7, { color: keyOf(cur) === "ll-path" && focused ? theme.accentBright : theme.dim, children: mark({ kind: "ll-path" }) }),
3491
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "path ".padEnd(10) }),
3685
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "path ".padEnd(11) }),
3492
3686
  local?.path ? /* @__PURE__ */ jsx7(Text7, { color: theme.accent, children: truncate(local.path, cols - 14) }) : /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "not set \u2014 enter to edit" })
3493
- ] }),
3494
- /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
3687
+ ] }) : null,
3688
+ inWindow({ kind: "ll-server" }) ? /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
3495
3689
  /* @__PURE__ */ jsx7(Text7, { color: keyOf(cur) === "ll-server" && focused ? theme.accentBright : theme.dim, children: mark({ kind: "ll-server" }) }),
3496
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "dashboard".padEnd(10) }),
3690
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "dashboard".padEnd(11) }),
3497
3691
  srv.running ? /* @__PURE__ */ jsx7(Text7, { color: theme.ok, children: `running 127.0.0.1:${srv.port}` }) : /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "stopped" }),
3498
3692
  /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: srv.running ? " serves local logs to the dashboard \xB7 survives closing the dataroom \xB7 enter stops" : " enter starts the local-logs link for the dashboard" })
3499
- ] })
3693
+ ] }) : null
3500
3694
  ] }) : null,
3501
3695
  /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexDirection: "column", children: [
3502
3696
  /* @__PURE__ */ jsxs7(Text7, { bold: true, color: theme.accentBright, children: [
@@ -3504,7 +3698,7 @@ function SettingsPanel({
3504
3698
  /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
3505
3699
  "\u2014 POST every matching event to your endpoint (",
3506
3700
  webhooks.length,
3507
- ")"
3701
+ ") \xB7 t tests"
3508
3702
  ] })
3509
3703
  ] }),
3510
3704
  webhooks.filter((wh) => inWindow({ kind: "wh", wh })).map((wh) => /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
@@ -3522,17 +3716,17 @@ function SettingsPanel({
3522
3716
  /* @__PURE__ */ jsxs7(Text7, { bold: true, color: theme.accentBright, children: [
3523
3717
  "ALERTS ",
3524
3718
  /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
3525
- "\u2014 notify on a signal spike (",
3719
+ "\u2014 spike alerts to one email + one telegram (",
3526
3720
  alerts.length,
3527
- ")"
3721
+ ") \xB7 enter edits \xB7 t tests"
3528
3722
  ] })
3529
3723
  ] }),
3530
3724
  alerts.filter((rule) => inWindow({ kind: "alert", rule })).map((rule) => /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
3531
3725
  /* @__PURE__ */ jsx7(Text7, { color: keyOf(cur) === "alert:" + rule.id && focused ? theme.accentBright : theme.dim, children: mark({ kind: "alert", rule }) }),
3532
3726
  onOff(rule.enabled),
3533
- /* @__PURE__ */ jsx7(Text7, { color: theme.warn, children: (" " + rule.signal).padEnd(11) }),
3534
- /* @__PURE__ */ jsx7(Text7, { children: `\u2265${rule.threshold}/${rule.windowSeconds}s ` }),
3535
- /* @__PURE__ */ jsx7(Text7, { color: theme.accent, children: truncate(chanOf(rule), cols - 26) })
3727
+ /* @__PURE__ */ jsx7(Text7, { color: theme.warn, children: (" " + SIGNAL_LABEL[rule.signal]).padEnd(13) }),
3728
+ /* @__PURE__ */ jsx7(Text7, { children: `\u2265${rule.threshold}/${winLabel(rule.windowSeconds)} `.padEnd(11) }),
3729
+ /* @__PURE__ */ jsx7(Text7, { color: theme.accent, children: truncate(chanOf(rule), cols - 34) })
3536
3730
  ] }, rule.id)),
3537
3731
  inWindow({ kind: "alert-add-email" }) ? /* @__PURE__ */ jsxs7(Text7, { children: [
3538
3732
  /* @__PURE__ */ jsx7(Text7, { color: cur.kind === "alert-add-email" && focused ? theme.accentBright : theme.dim, children: mark({ kind: "alert-add-email" }) }),
@@ -3540,8 +3734,9 @@ function SettingsPanel({
3540
3734
  ] }) : null,
3541
3735
  inWindow({ kind: "alert-add-tg" }) ? /* @__PURE__ */ jsxs7(Text7, { children: [
3542
3736
  /* @__PURE__ */ jsx7(Text7, { color: cur.kind === "alert-add-tg" && focused ? theme.accentBright : theme.dim, children: mark({ kind: "alert-add-tg" }) }),
3543
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "+ add telegram alert (enter \u2014 numeric chat id, get it from @userinfobot)" })
3544
- ] }) : null
3737
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "+ add telegram alert (enter \u2014 numeric chat id from @userinfobot)" })
3738
+ ] }) : null,
3739
+ hasEmailAlert && hasTgAlert ? /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " both channels in use \u2014 remove one (d) to change it" }) : null
3545
3740
  ] })
3546
3741
  ] })
3547
3742
  ] }) });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.81.54",
3
+ "version": "0.81.56",
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": {