@solongate/proxy 0.81.55 → 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 = () => {
@@ -10218,22 +10304,8 @@ function SettingsPanel({
10218
10304
  return;
10219
10305
  }
10220
10306
  run12("webhook added", () => api.settings.createWebhook({ url, events: "denials" }), whQ.reload);
10221
- } else if (which === "alert-email") {
10222
- if (!v) return;
10223
- const email = v.replace(/^mailto:/i, "").replace(/^["'<]+|["'>]+$/g, "").replace(/[.,;:]+$/, "").replace(/\s+/g, "").trim();
10224
- if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
10225
- setMsg({ text: "\u2717 that does not look like an e-mail address", level: "bad" });
10226
- return;
10227
- }
10228
- run12("email alert added", () => api.settings.createAlert({ ...ALERT_BASE, emails: [email] }), alertQ.reload);
10229
- } else if (which === "alert-tg") {
10230
- if (!v) return;
10231
- const tg = v.replace(/^@/, "").replace(/^(chat[\s_-]*)?id[:=\s]*/i, "").replace(/\s+/g, "").trim();
10232
- if (!/^-?\d{5,}$/.test(tg)) {
10233
- setMsg({ text: "\u2717 a telegram chat id is a number \u2014 get yours from @userinfobot", level: "bad" });
10234
- return;
10235
- }
10236
- run12("telegram alert added", () => api.settings.createAlert({ ...ALERT_BASE, telegram: [tg] }), alertQ.reload);
10307
+ } else if (which === "alert-target" && editor) {
10308
+ setEditor({ ...editor, target: v, field: 1 });
10237
10309
  }
10238
10310
  };
10239
10311
  useInput6(
@@ -10245,6 +10317,35 @@ function SettingsPanel({
10245
10317
  }
10246
10318
  return;
10247
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
+ }
10248
10349
  setMsg(null);
10249
10350
  if (key.upArrow) {
10250
10351
  setSel((n) => Math.max(0, n - 1));
@@ -10258,28 +10359,41 @@ function SettingsPanel({
10258
10359
  const ok = setActiveAccount({ apiKey: cur.acc.apiKey, apiUrl: cur.acc.apiUrl });
10259
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" });
10260
10361
  refreshAccounts();
10261
- } 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();
10262
10377
  } else if (inp === "t" && cur.kind === "wh") {
10263
- run12(
10264
- "test event sent \u2014 check your endpoint",
10265
- () => api.settings.sendTestWebhook(cur.wh.id).then((r) => {
10266
- if (!r.delivered) throw new Error("endpoint rejected the test (non-2xx response)");
10267
- }),
10268
- whQ.reload
10269
- );
10270
- } 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);
10271
10384
  else if (inp === "e" && cur.kind === "wh") {
10272
10385
  const next = EVENTS[(EVENTS.indexOf(cur.wh.events) + 1) % EVENTS.length];
10273
10386
  run12(`webhook events \u2192 ${next}`, () => api.settings.updateWebhook(cur.wh.id, { events: next }), whQ.reload);
10274
- } 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();
10275
10391
  else if (inp === "a") {
10276
10392
  if (cur.kind === "wh" || cur.kind === "wh-add") {
10277
10393
  setInput("");
10278
10394
  setEditing("wh-url");
10279
- } else if (cur.kind === "alert" || cur.kind === "alert-add-email" || cur.kind === "alert-add-tg") {
10280
- setInput("");
10281
- setEditing("alert-email");
10282
- }
10395
+ } else if (cur.kind === "alert-add-email") openAlertEditor("email");
10396
+ else if (cur.kind === "alert-add-tg") openAlertEditor("telegram");
10283
10397
  } else if (inp === "d" && (cur.kind === "wh" || cur.kind === "alert")) {
10284
10398
  const k = keyOf(cur);
10285
10399
  if (confirmDel !== k) {
@@ -10319,30 +10433,70 @@ function SettingsPanel({
10319
10433
  ] })
10320
10434
  ] });
10321
10435
  }
10322
- const loading = !locked && (localQ.loading && !localQ.data || whQ.loading && !whQ.data || alertQ.loading && !alertQ.data);
10323
- 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;
10324
10478
  const mark = (r) => keyOf(r) === keyOf(cur) && focused ? "\u25B8 " : " ";
10325
10479
  const onOff = (on) => on ? /* @__PURE__ */ jsx7(Text7, { color: theme.ok, children: "on " }) : /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "off" });
10326
10480
  const chanOf = (rule) => [...rule.emails ?? [], ...(rule.telegram ?? []).map((t) => "tg " + t)].join(", ") || "\u2014";
10327
- const listBudget = Math.max(4, rows - 10);
10481
+ const listBudget = Math.max(4, rows - 12);
10328
10482
  const start = Math.min(Math.max(0, selClamped - Math.floor(listBudget / 2)), Math.max(0, rowsAll.length - listBudget));
10329
10483
  const inWindow = (r) => {
10330
10484
  const i = rowsAll.findIndex((x) => keyOf(x) === keyOf(r));
10331
10485
  return i >= start && i < start + listBudget;
10332
10486
  };
10333
10487
  return /* @__PURE__ */ jsx7(DataView, { loading, error, children: /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
10334
- /* @__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" }),
10335
10489
  editing ? /* @__PURE__ */ jsxs7(Box7, { children: [
10336
- /* @__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: " }),
10337
10491
  /* @__PURE__ */ jsx7(TextInput5, { value: input, onChange: setInput, onSubmit: submitInput })
10338
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: " " }),
10339
10493
  /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexDirection: "column", children: [
10340
10494
  /* @__PURE__ */ jsxs7(Text7, { bold: true, color: theme.accentBright, children: [
10341
10495
  "ACCOUNTS ",
10342
10496
  /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
10343
- "\u2014 logged in on this device (",
10497
+ "\u2014 on this device (",
10344
10498
  accounts.length,
10345
- ") \xB7 \u25CF viewing = shown here \xB7 ACTIVE = key the guard uses"
10499
+ ") \xB7 \u25CF viewing \xB7 ACTIVE = key the guard uses \xB7 x removes"
10346
10500
  ] })
10347
10501
  ] }),
10348
10502
  accounts.length === 0 ? /* @__PURE__ */ jsxs7(Text7, { children: [
@@ -10358,7 +10512,7 @@ function SettingsPanel({
10358
10512
  /* @__PURE__ */ jsx7(Text7, { color: theme.accent, children: truncate2(acctLabel(a), 30).padEnd(31) }),
10359
10513
  isView ? /* @__PURE__ */ jsx7(Text7, { color: theme.ok, children: "\u25CF viewing " }) : /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " " }),
10360
10514
  isActive ? /* @__PURE__ */ jsx7(Text7, { color: theme.warn, children: "ACTIVE " }) : /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " " }),
10361
- /* @__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) : "" })
10362
10516
  ] }, a.apiKey);
10363
10517
  }),
10364
10518
  inWindow({ kind: "acct-add" }) ? /* @__PURE__ */ jsxs7(Text7, { children: [
@@ -10367,28 +10521,49 @@ function SettingsPanel({
10367
10521
  ] }) : null
10368
10522
  ] }),
10369
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,
10370
10545
  inWindow({ kind: "ll-enabled" }) || inWindow({ kind: "ll-path" }) || inWindow({ kind: "ll-server" }) ? /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexDirection: "column", children: [
10371
10546
  /* @__PURE__ */ jsxs7(Text7, { bold: true, color: theme.accentBright, children: [
10372
10547
  "LOCAL LOGS ",
10373
- /* @__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" })
10374
10549
  ] }),
10375
- /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
10550
+ inWindow({ kind: "ll-enabled" }) ? /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
10376
10551
  /* @__PURE__ */ jsx7(Text7, { color: keyOf(cur) === "ll-enabled" && focused ? theme.accentBright : theme.dim, children: mark({ kind: "ll-enabled" }) }),
10377
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "enabled ".padEnd(10) }),
10552
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "enabled ".padEnd(11) }),
10378
10553
  onOff(!!local?.enabled),
10379
10554
  /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " enter toggles" })
10380
- ] }),
10381
- /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
10555
+ ] }) : null,
10556
+ inWindow({ kind: "ll-path" }) ? /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
10382
10557
  /* @__PURE__ */ jsx7(Text7, { color: keyOf(cur) === "ll-path" && focused ? theme.accentBright : theme.dim, children: mark({ kind: "ll-path" }) }),
10383
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "path ".padEnd(10) }),
10558
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "path ".padEnd(11) }),
10384
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" })
10385
- ] }),
10386
- /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
10560
+ ] }) : null,
10561
+ inWindow({ kind: "ll-server" }) ? /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
10387
10562
  /* @__PURE__ */ jsx7(Text7, { color: keyOf(cur) === "ll-server" && focused ? theme.accentBright : theme.dim, children: mark({ kind: "ll-server" }) }),
10388
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "dashboard".padEnd(10) }),
10563
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "dashboard".padEnd(11) }),
10389
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" }),
10390
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" })
10391
- ] })
10566
+ ] }) : null
10392
10567
  ] }) : null,
10393
10568
  /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexDirection: "column", children: [
10394
10569
  /* @__PURE__ */ jsxs7(Text7, { bold: true, color: theme.accentBright, children: [
@@ -10396,7 +10571,7 @@ function SettingsPanel({
10396
10571
  /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
10397
10572
  "\u2014 POST every matching event to your endpoint (",
10398
10573
  webhooks.length,
10399
- ")"
10574
+ ") \xB7 t tests"
10400
10575
  ] })
10401
10576
  ] }),
10402
10577
  webhooks.filter((wh) => inWindow({ kind: "wh", wh })).map((wh) => /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
@@ -10414,17 +10589,17 @@ function SettingsPanel({
10414
10589
  /* @__PURE__ */ jsxs7(Text7, { bold: true, color: theme.accentBright, children: [
10415
10590
  "ALERTS ",
10416
10591
  /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
10417
- "\u2014 notify on a signal spike (",
10592
+ "\u2014 spike alerts to one email + one telegram (",
10418
10593
  alerts.length,
10419
- ")"
10594
+ ") \xB7 enter edits \xB7 t tests"
10420
10595
  ] })
10421
10596
  ] }),
10422
10597
  alerts.filter((rule) => inWindow({ kind: "alert", rule })).map((rule) => /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
10423
10598
  /* @__PURE__ */ jsx7(Text7, { color: keyOf(cur) === "alert:" + rule.id && focused ? theme.accentBright : theme.dim, children: mark({ kind: "alert", rule }) }),
10424
10599
  onOff(rule.enabled),
10425
- /* @__PURE__ */ jsx7(Text7, { color: theme.warn, children: (" " + rule.signal).padEnd(11) }),
10426
- /* @__PURE__ */ jsx7(Text7, { children: `\u2265${rule.threshold}/${rule.windowSeconds}s ` }),
10427
- /* @__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) })
10428
10603
  ] }, rule.id)),
10429
10604
  inWindow({ kind: "alert-add-email" }) ? /* @__PURE__ */ jsxs7(Text7, { children: [
10430
10605
  /* @__PURE__ */ jsx7(Text7, { color: cur.kind === "alert-add-email" && focused ? theme.accentBright : theme.dim, children: mark({ kind: "alert-add-email" }) }),
@@ -10432,13 +10607,14 @@ function SettingsPanel({
10432
10607
  ] }) : null,
10433
10608
  inWindow({ kind: "alert-add-tg" }) ? /* @__PURE__ */ jsxs7(Text7, { children: [
10434
10609
  /* @__PURE__ */ jsx7(Text7, { color: cur.kind === "alert-add-tg" && focused ? theme.accentBright : theme.dim, children: mark({ kind: "alert-add-tg" }) }),
10435
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "+ add telegram alert (enter \u2014 numeric chat id, get it from @userinfobot)" })
10436
- ] }) : 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
10437
10613
  ] })
10438
10614
  ] })
10439
10615
  ] }) });
10440
10616
  }
10441
- var EVENTS, SPIN2, ALERT_BASE, acctLabel;
10617
+ var EVENTS, SPIN2, SIGNALS2, SIGNAL_LABEL, WINDOWS, winLabel, THRESH_MIN, THRESH_MAX, nearestWindowIdx, EDITOR_FIELDS, acctLabel, alertChannel;
10442
10618
  var init_Settings = __esm({
10443
10619
  "src/tui/panels/Settings.tsx"() {
10444
10620
  "use strict";
@@ -10451,8 +10627,20 @@ var init_Settings = __esm({
10451
10627
  init_theme();
10452
10628
  EVENTS = ["denials", "allowed", "all"];
10453
10629
  SPIN2 = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
10454
- 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;
10455
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;
10456
10644
  }
10457
10645
  });
10458
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 = () => {
@@ -3333,22 +3431,8 @@ function SettingsPanel({
3333
3431
  return;
3334
3432
  }
3335
3433
  run("webhook added", () => api.settings.createWebhook({ url, events: "denials" }), whQ.reload);
3336
- } else if (which === "alert-email") {
3337
- if (!v) return;
3338
- const email = v.replace(/^mailto:/i, "").replace(/^["'<]+|["'>]+$/g, "").replace(/[.,;:]+$/, "").replace(/\s+/g, "").trim();
3339
- if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
3340
- setMsg({ text: "\u2717 that does not look like an e-mail address", level: "bad" });
3341
- return;
3342
- }
3343
- run("email alert added", () => api.settings.createAlert({ ...ALERT_BASE, emails: [email] }), alertQ.reload);
3344
- } else if (which === "alert-tg") {
3345
- if (!v) return;
3346
- const tg = v.replace(/^@/, "").replace(/^(chat[\s_-]*)?id[:=\s]*/i, "").replace(/\s+/g, "").trim();
3347
- if (!/^-?\d{5,}$/.test(tg)) {
3348
- setMsg({ text: "\u2717 a telegram chat id is a number \u2014 get yours from @userinfobot", level: "bad" });
3349
- return;
3350
- }
3351
- run("telegram alert added", () => api.settings.createAlert({ ...ALERT_BASE, telegram: [tg] }), alertQ.reload);
3434
+ } else if (which === "alert-target" && editor) {
3435
+ setEditor({ ...editor, target: v, field: 1 });
3352
3436
  }
3353
3437
  };
3354
3438
  useInput6(
@@ -3360,6 +3444,35 @@ function SettingsPanel({
3360
3444
  }
3361
3445
  return;
3362
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
+ }
3363
3476
  setMsg(null);
3364
3477
  if (key.upArrow) {
3365
3478
  setSel((n) => Math.max(0, n - 1));
@@ -3373,28 +3486,41 @@ function SettingsPanel({
3373
3486
  const ok = setActiveAccount({ apiKey: cur.acc.apiKey, apiUrl: cur.acc.apiUrl });
3374
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" });
3375
3488
  refreshAccounts();
3376
- } 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();
3377
3504
  } else if (inp === "t" && cur.kind === "wh") {
3378
- run(
3379
- "test event sent \u2014 check your endpoint",
3380
- () => api.settings.sendTestWebhook(cur.wh.id).then((r) => {
3381
- if (!r.delivered) throw new Error("endpoint rejected the test (non-2xx response)");
3382
- }),
3383
- whQ.reload
3384
- );
3385
- } 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);
3386
3511
  else if (inp === "e" && cur.kind === "wh") {
3387
3512
  const next = EVENTS[(EVENTS.indexOf(cur.wh.events) + 1) % EVENTS.length];
3388
3513
  run(`webhook events \u2192 ${next}`, () => api.settings.updateWebhook(cur.wh.id, { events: next }), whQ.reload);
3389
- } 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();
3390
3518
  else if (inp === "a") {
3391
3519
  if (cur.kind === "wh" || cur.kind === "wh-add") {
3392
3520
  setInput("");
3393
3521
  setEditing("wh-url");
3394
- } else if (cur.kind === "alert" || cur.kind === "alert-add-email" || cur.kind === "alert-add-tg") {
3395
- setInput("");
3396
- setEditing("alert-email");
3397
- }
3522
+ } else if (cur.kind === "alert-add-email") openAlertEditor("email");
3523
+ else if (cur.kind === "alert-add-tg") openAlertEditor("telegram");
3398
3524
  } else if (inp === "d" && (cur.kind === "wh" || cur.kind === "alert")) {
3399
3525
  const k = keyOf(cur);
3400
3526
  if (confirmDel !== k) {
@@ -3434,30 +3560,70 @@ function SettingsPanel({
3434
3560
  ] })
3435
3561
  ] });
3436
3562
  }
3437
- const loading = !locked && (localQ.loading && !localQ.data || whQ.loading && !whQ.data || alertQ.loading && !alertQ.data);
3438
- 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;
3439
3605
  const mark = (r) => keyOf(r) === keyOf(cur) && focused ? "\u25B8 " : " ";
3440
3606
  const onOff = (on) => on ? /* @__PURE__ */ jsx7(Text7, { color: theme.ok, children: "on " }) : /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "off" });
3441
3607
  const chanOf = (rule) => [...rule.emails ?? [], ...(rule.telegram ?? []).map((t) => "tg " + t)].join(", ") || "\u2014";
3442
- const listBudget = Math.max(4, rows - 10);
3608
+ const listBudget = Math.max(4, rows - 12);
3443
3609
  const start = Math.min(Math.max(0, selClamped - Math.floor(listBudget / 2)), Math.max(0, rowsAll.length - listBudget));
3444
3610
  const inWindow = (r) => {
3445
3611
  const i = rowsAll.findIndex((x) => keyOf(x) === keyOf(r));
3446
3612
  return i >= start && i < start + listBudget;
3447
3613
  };
3448
3614
  return /* @__PURE__ */ jsx7(DataView, { loading, error, children: /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
3449
- /* @__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" }),
3450
3616
  editing ? /* @__PURE__ */ jsxs7(Box7, { children: [
3451
- /* @__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: " }),
3452
3618
  /* @__PURE__ */ jsx7(TextInput5, { value: input, onChange: setInput, onSubmit: submitInput })
3453
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: " " }),
3454
3620
  /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexDirection: "column", children: [
3455
3621
  /* @__PURE__ */ jsxs7(Text7, { bold: true, color: theme.accentBright, children: [
3456
3622
  "ACCOUNTS ",
3457
3623
  /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
3458
- "\u2014 logged in on this device (",
3624
+ "\u2014 on this device (",
3459
3625
  accounts.length,
3460
- ") \xB7 \u25CF viewing = shown here \xB7 ACTIVE = key the guard uses"
3626
+ ") \xB7 \u25CF viewing \xB7 ACTIVE = key the guard uses \xB7 x removes"
3461
3627
  ] })
3462
3628
  ] }),
3463
3629
  accounts.length === 0 ? /* @__PURE__ */ jsxs7(Text7, { children: [
@@ -3473,7 +3639,7 @@ function SettingsPanel({
3473
3639
  /* @__PURE__ */ jsx7(Text7, { color: theme.accent, children: truncate(acctLabel(a), 30).padEnd(31) }),
3474
3640
  isView ? /* @__PURE__ */ jsx7(Text7, { color: theme.ok, children: "\u25CF viewing " }) : /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " " }),
3475
3641
  isActive ? /* @__PURE__ */ jsx7(Text7, { color: theme.warn, children: "ACTIVE " }) : /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " " }),
3476
- /* @__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) : "" })
3477
3643
  ] }, a.apiKey);
3478
3644
  }),
3479
3645
  inWindow({ kind: "acct-add" }) ? /* @__PURE__ */ jsxs7(Text7, { children: [
@@ -3482,28 +3648,49 @@ function SettingsPanel({
3482
3648
  ] }) : null
3483
3649
  ] }),
3484
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,
3485
3672
  inWindow({ kind: "ll-enabled" }) || inWindow({ kind: "ll-path" }) || inWindow({ kind: "ll-server" }) ? /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexDirection: "column", children: [
3486
3673
  /* @__PURE__ */ jsxs7(Text7, { bold: true, color: theme.accentBright, children: [
3487
3674
  "LOCAL LOGS ",
3488
- /* @__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" })
3489
3676
  ] }),
3490
- /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
3677
+ inWindow({ kind: "ll-enabled" }) ? /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
3491
3678
  /* @__PURE__ */ jsx7(Text7, { color: keyOf(cur) === "ll-enabled" && focused ? theme.accentBright : theme.dim, children: mark({ kind: "ll-enabled" }) }),
3492
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "enabled ".padEnd(10) }),
3679
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "enabled ".padEnd(11) }),
3493
3680
  onOff(!!local?.enabled),
3494
3681
  /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " enter toggles" })
3495
- ] }),
3496
- /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
3682
+ ] }) : null,
3683
+ inWindow({ kind: "ll-path" }) ? /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
3497
3684
  /* @__PURE__ */ jsx7(Text7, { color: keyOf(cur) === "ll-path" && focused ? theme.accentBright : theme.dim, children: mark({ kind: "ll-path" }) }),
3498
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "path ".padEnd(10) }),
3685
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "path ".padEnd(11) }),
3499
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" })
3500
- ] }),
3501
- /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
3687
+ ] }) : null,
3688
+ inWindow({ kind: "ll-server" }) ? /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
3502
3689
  /* @__PURE__ */ jsx7(Text7, { color: keyOf(cur) === "ll-server" && focused ? theme.accentBright : theme.dim, children: mark({ kind: "ll-server" }) }),
3503
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "dashboard".padEnd(10) }),
3690
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "dashboard".padEnd(11) }),
3504
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" }),
3505
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" })
3506
- ] })
3693
+ ] }) : null
3507
3694
  ] }) : null,
3508
3695
  /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexDirection: "column", children: [
3509
3696
  /* @__PURE__ */ jsxs7(Text7, { bold: true, color: theme.accentBright, children: [
@@ -3511,7 +3698,7 @@ function SettingsPanel({
3511
3698
  /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
3512
3699
  "\u2014 POST every matching event to your endpoint (",
3513
3700
  webhooks.length,
3514
- ")"
3701
+ ") \xB7 t tests"
3515
3702
  ] })
3516
3703
  ] }),
3517
3704
  webhooks.filter((wh) => inWindow({ kind: "wh", wh })).map((wh) => /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
@@ -3529,17 +3716,17 @@ function SettingsPanel({
3529
3716
  /* @__PURE__ */ jsxs7(Text7, { bold: true, color: theme.accentBright, children: [
3530
3717
  "ALERTS ",
3531
3718
  /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
3532
- "\u2014 notify on a signal spike (",
3719
+ "\u2014 spike alerts to one email + one telegram (",
3533
3720
  alerts.length,
3534
- ")"
3721
+ ") \xB7 enter edits \xB7 t tests"
3535
3722
  ] })
3536
3723
  ] }),
3537
3724
  alerts.filter((rule) => inWindow({ kind: "alert", rule })).map((rule) => /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
3538
3725
  /* @__PURE__ */ jsx7(Text7, { color: keyOf(cur) === "alert:" + rule.id && focused ? theme.accentBright : theme.dim, children: mark({ kind: "alert", rule }) }),
3539
3726
  onOff(rule.enabled),
3540
- /* @__PURE__ */ jsx7(Text7, { color: theme.warn, children: (" " + rule.signal).padEnd(11) }),
3541
- /* @__PURE__ */ jsx7(Text7, { children: `\u2265${rule.threshold}/${rule.windowSeconds}s ` }),
3542
- /* @__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) })
3543
3730
  ] }, rule.id)),
3544
3731
  inWindow({ kind: "alert-add-email" }) ? /* @__PURE__ */ jsxs7(Text7, { children: [
3545
3732
  /* @__PURE__ */ jsx7(Text7, { color: cur.kind === "alert-add-email" && focused ? theme.accentBright : theme.dim, children: mark({ kind: "alert-add-email" }) }),
@@ -3547,8 +3734,9 @@ function SettingsPanel({
3547
3734
  ] }) : null,
3548
3735
  inWindow({ kind: "alert-add-tg" }) ? /* @__PURE__ */ jsxs7(Text7, { children: [
3549
3736
  /* @__PURE__ */ jsx7(Text7, { color: cur.kind === "alert-add-tg" && focused ? theme.accentBright : theme.dim, children: mark({ kind: "alert-add-tg" }) }),
3550
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "+ add telegram alert (enter \u2014 numeric chat id, get it from @userinfobot)" })
3551
- ] }) : 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
3552
3740
  ] })
3553
3741
  ] })
3554
3742
  ] }) });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.81.55",
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": {