@solongate/proxy 0.83.3 → 0.83.5

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.
@@ -23,6 +23,19 @@ export declare function latestVersion(): Promise<string | null>;
23
23
  export declare function runUpdateCommand(): Promise<number>;
24
24
  /** The exact command that fixes a root-owned global prefix. */
25
25
  export declare function adminInstallCommand(): string;
26
+ /** Are we root — i.e. did someone run this under sudo? */
27
+ export declare function runningAsRoot(): boolean;
28
+ export declare function globalInstallCheck(): Promise<{
29
+ writable: boolean | null;
30
+ dir: string | null;
31
+ }>;
32
+ /**
33
+ * The two commands that update SolonGate on a machine whose global npm folder
34
+ * belongs to root. `repair` is deliberately NOT under sudo: it writes the guard
35
+ * hooks into the invoking user's home, so as root it would arm root's home and
36
+ * leave the actual user unguarded.
37
+ */
38
+ export declare function adminUpdateSteps(): string[];
26
39
  /**
27
40
  * Install the newest version NOW, whatever the auto-update setting says — the
28
41
  * dataroom's UPDATES row and `solongate update` both run through here.
@@ -36,7 +49,7 @@ export declare function updateNow(): Promise<{
36
49
  * Kept separate from `runUpdateCommand` so `solongate update` stays "update me
37
50
  * now" and never changes a setting as a side effect.
38
51
  */
39
- export declare function runAutoUpdateCommand(arg?: string): number;
52
+ export declare function runAutoUpdateCommand(arg?: string): Promise<number>;
40
53
  /** What the dataroom shows about the updater. */
41
54
  export type UpdateStatus = {
42
55
  kind: 'idle';
package/dist/tui/index.js CHANGED
@@ -530,8 +530,8 @@ var init_global_install = __esm({
530
530
 
531
531
  // src/tui/index.tsx
532
532
  import { appendFileSync, mkdirSync as mkdirSync7 } from "fs";
533
- import { homedir as homedir11 } from "os";
534
- import { join as join11 } from "path";
533
+ import { homedir as homedir10 } from "os";
534
+ import { join as join10 } from "path";
535
535
  import { render } from "ink";
536
536
 
537
537
  // src/tui/App.tsx
@@ -935,16 +935,6 @@ function setViewCredentials(creds) {
935
935
  viewOverride = creds;
936
936
  cached2 = null;
937
937
  }
938
- var GUEST_EMAIL_SUFFIX = "@anonymous.solongate.invalid";
939
- function isGuestAccount() {
940
- try {
941
- const active2 = loginCredentialFile().apiKey;
942
- if (!active2) return false;
943
- return listAccounts().some((a) => a.apiKey === active2 && (a.email ?? "").endsWith(GUEST_EMAIL_SUFFIX));
944
- } catch {
945
- return false;
946
- }
947
- }
948
938
  function enforcingKey() {
949
939
  return loginCredentialFile().apiKey ?? null;
950
940
  }
@@ -1000,12 +990,6 @@ var ApiError = class extends Error {
1000
990
  this.code = code;
1001
991
  }
1002
992
  };
1003
- var GuestReadOnlyError = class extends Error {
1004
- constructor() {
1005
- super("Guest account: sign up at auth.solongate.com to change anything. Your project, its policy and everything recorded carry over.");
1006
- this.name = "GuestReadOnlyError";
1007
- }
1008
- };
1009
993
  var NotAuthenticatedError = class extends Error {
1010
994
  constructor() {
1011
995
  super("Not logged in. Run `solongate` and log in from the Accounts panel.");
@@ -1070,9 +1054,6 @@ function buildUrl(base, path, query) {
1070
1054
  return url.toString();
1071
1055
  }
1072
1056
  async function request(method, path, opts = {}) {
1073
- if (method !== "GET" && isGuestAccount()) {
1074
- throw new GuestReadOnlyError();
1075
- }
1076
1057
  const creds = resolveCredentials(opts.apiUrl);
1077
1058
  const url = buildUrl(creds.apiUrl, path, opts.query);
1078
1059
  const headers = {
@@ -2422,18 +2403,18 @@ function LivePanel({ active: active2 }) {
2422
2403
  }
2423
2404
  ) : null,
2424
2405
  activeAction ? /* @__PURE__ */ jsx2(Text2, { wrap: "truncate", backgroundColor: activeAction.level === "bad" ? "#3d1220" : "#123d1f", color: activeAction.level === "bad" ? "#ff6b6b" : "#7bd88f", bold: true, children: ` ${truncate(activeAction.text, innerW - 4)} ` }) : null,
2425
- /* @__PURE__ */ jsxs2(Box2, { height: 1 + chartH, children: [
2426
- /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: leftW, marginRight: 2, children: [
2406
+ /* @__PURE__ */ jsxs2(Box2, { height: 1 + chartH, overflow: "hidden", children: [
2407
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: leftW, height: 1 + chartH, marginRight: 2, overflow: "hidden", children: [
2427
2408
  /* @__PURE__ */ jsx2(PaneTitle, { label: "TRAFFIC", extra: `${trafficLabel} \xB7 peak ${Math.max(0, ...traffic)} \xB7 red = denials`, width: leftW }),
2428
2409
  /* @__PURE__ */ jsx2(ColumnChart, { series: traffic, hot: trafficHot, height: chartH, width: leftW, color: theme.accent })
2429
2410
  ] }),
2430
- /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: rightW, children: [
2411
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: rightW, height: 1 + chartH, overflow: "hidden", children: [
2431
2412
  /* @__PURE__ */ jsx2(PaneTitle, { label: "API LATENCY", extra: `now ${latNow}ms \xB7 med ${latMed}ms \xB7 amber >${Math.round(latHotAt)}ms`, width: rightW }),
2432
2413
  /* @__PURE__ */ jsx2(ColumnChart, { series: lat, hot: lat.map((v) => v > latHotAt), height: chartH, width: rightW, color: "white", hotColor: "#ffb454" })
2433
2414
  ] })
2434
2415
  ] }),
2435
- /* @__PURE__ */ jsxs2(Box2, { height: colH, children: [
2436
- /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, marginRight: 2, overflow: "hidden", children: [
2416
+ /* @__PURE__ */ jsxs2(Box2, { height: colH, overflow: "hidden", children: [
2417
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, height: colH, marginRight: 2, overflow: "hidden", children: [
2437
2418
  /* @__PURE__ */ jsx2(PaneTitle, { label: "LAYERS", extra: "l = inspect", width: colW }),
2438
2419
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
2439
2420
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "RATELIMIT ".padEnd(10) }),
@@ -2446,7 +2427,11 @@ function LivePanel({ active: active2 }) {
2446
2427
  /* @__PURE__ */ jsx2(Text2, { color: dl?.mode === "block" ? theme.ok : dl?.mode === "detect" ? theme.warn : theme.dim, children: (dl?.mode ?? "?").padEnd(7) }),
2447
2428
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: `${(dl?.patterns ?? []).length} builtin \xB7 ${(dl?.custom ?? []).length} custom` })
2448
2429
  ] }),
2449
- dlpBars.length ? dlpBars.map((d2) => /* @__PURE__ */ jsx2(HBar, { label: truncate(d2.pattern, 10), value: d2.count, max: maxDlpBar, width: Math.max(6, colW - 16), color: theme.bad }, d2.pattern)) : /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " no dlp hits in last 7 days" }),
2430
+ dlpBars.length ? dlpBars.map((d2) => /* @__PURE__ */ jsx2(HBar, { label: truncate(d2.pattern, 10), value: d2.count, max: maxDlpBar, width: Math.max(6, colW - 16), color: theme.bad }, d2.pattern)) : (
2431
+ // 36 chars against a colW that is ~31 at cols=100: without truncate
2432
+ // this wrapped onto a second line and pushed the column over colH.
2433
+ /* @__PURE__ */ jsx2(Text2, { wrap: "truncate", color: theme.dim, children: " no dlp hits in last 7 days" })
2434
+ ),
2450
2435
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
2451
2436
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "GUARD".padEnd(10) }),
2452
2437
  ring ? /* @__PURE__ */ jsxs2(Text2, { color: theme.ok, children: [
@@ -2455,7 +2440,7 @@ function LivePanel({ active: active2 }) {
2455
2440
  ] }) : /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "no local ring in cwd" })
2456
2441
  ] })
2457
2442
  ] }),
2458
- /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, marginRight: 2, overflow: "hidden", children: [
2443
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, height: colH, marginRight: 2, overflow: "hidden", children: [
2459
2444
  /* @__PURE__ */ jsx2(PaneTitle, { label: "SESSIONS", extra: mode === "pick" ? "\u2191\u2193 pick \xB7 enter open" : `${combinedSess.length} \xB7 s = inspect`, width: colW }),
2460
2445
  pickable.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "scanning\u2026" }) : null,
2461
2446
  pickable.map((r, i) => {
@@ -2481,7 +2466,7 @@ function LivePanel({ active: active2 }) {
2481
2466
  ] }, r.id);
2482
2467
  })
2483
2468
  ] }),
2484
- /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, overflow: "hidden", children: [
2469
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, height: colH, overflow: "hidden", children: [
2485
2470
  /* @__PURE__ */ jsx2(PaneTitle, { label: "EVENT LOG", extra: "system heartbeat", width: colW }),
2486
2471
  log.slice(-(colH - 1)).map((l, i) => /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
2487
2472
  /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
@@ -4495,9 +4480,9 @@ function stopLogsServerDaemon() {
4495
4480
 
4496
4481
  // src/self-update.ts
4497
4482
  import { execFile, execFileSync as execFileSync2, spawn as spawn4 } from "child_process";
4498
- import { mkdirSync as mkdirSync6, openSync as openSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "fs";
4483
+ import { access, mkdirSync as mkdirSync6, openSync as openSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync7, constants as FS } from "fs";
4499
4484
  import { homedir as homedir9 } from "os";
4500
- import { dirname as dirname3, join as join9 } from "path";
4485
+ import { dirname as dirname3, join as join9, sep } from "path";
4501
4486
  import { fileURLToPath as fileURLToPath3 } from "url";
4502
4487
  var PKG = "@solongate/proxy";
4503
4488
  var CHECK_EVERY_MS = 30 * 60 * 1e3;
@@ -4598,11 +4583,41 @@ ${output}
4598
4583
  function adminInstallCommand() {
4599
4584
  return process.platform === "win32" ? `npm i -g ${PKG}@latest (in an Administrator terminal)` : `sudo npm i -g ${PKG}@latest`;
4600
4585
  }
4586
+ function ownNodeModules() {
4587
+ let dir = dirname3(fileURLToPath3(import.meta.url));
4588
+ for (let i = 0; i < 12; i++) {
4589
+ const parent = dirname3(dir);
4590
+ if (parent === dir) break;
4591
+ if (dir.endsWith(`${sep}node_modules`)) return dir;
4592
+ dir = parent;
4593
+ }
4594
+ return null;
4595
+ }
4596
+ var prefixCheck = null;
4597
+ function globalInstallCheck() {
4598
+ prefixCheck ??= new Promise((resolve3) => {
4599
+ try {
4600
+ const dir = ownNodeModules();
4601
+ if (!dir) return resolve3({ writable: null, dir: null });
4602
+ access(dir, FS.W_OK, (e) => resolve3({ writable: !e, dir }));
4603
+ } catch {
4604
+ resolve3({ writable: null, dir: null });
4605
+ }
4606
+ });
4607
+ return prefixCheck;
4608
+ }
4609
+ function adminUpdateSteps() {
4610
+ return [` ${adminInstallCommand()}`, ` solongate repair${process.platform === "win32" ? "" : " (this one WITHOUT sudo)"}`];
4611
+ }
4601
4612
  async function updateNow() {
4602
4613
  const cur = currentVersion();
4603
4614
  const latest = await fetchLatest();
4604
4615
  if (!latest) return { status: "unreachable", version: cur };
4605
4616
  if (!newerThan(latest, cur)) return { status: "current", version: cur };
4617
+ if ((await globalInstallCheck()).writable === false) {
4618
+ writeState2({ ...readState2(), needsAdmin: latest });
4619
+ return { status: "needs-admin", version: latest };
4620
+ }
4606
4621
  const r = await runGlobalInstall(latest);
4607
4622
  if (r.ok) {
4608
4623
  writeState2({ ...readState2(), installed: latest, needsAdmin: void 0 });
@@ -4647,26 +4662,7 @@ async function tuiUpdateFlow(onStatus) {
4647
4662
  }
4648
4663
 
4649
4664
  // src/tui/panels/Settings.tsx
4650
- import { readFileSync as readFileSync8, readdirSync as readdirSync2 } from "fs";
4651
- import { homedir as homedir10 } from "os";
4652
- import { join as join10 } from "path";
4653
4665
  import { Fragment as Fragment5, jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
4654
- function guestAllowance() {
4655
- try {
4656
- const dir = join10(homedir10(), ".solongate");
4657
- for (const f of readdirSync2(dir)) {
4658
- if (!f.startsWith(".policy-cache-") || !f.endsWith(".json")) continue;
4659
- try {
4660
- const c2 = JSON.parse(readFileSync8(join10(dir, f), "utf-8"));
4661
- const q = c2?.security?.guestQuota;
4662
- if (q && typeof q.limit === "number") return { used: q.used ?? 0, limit: q.limit };
4663
- } catch {
4664
- }
4665
- }
4666
- } catch {
4667
- }
4668
- return null;
4669
- }
4670
4666
  var EVENTS = ["denials", "allowed", "all"];
4671
4667
  var SPIN2 = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
4672
4668
  var SIGNALS2 = ["any", "deny", "dlp", "ratelimit"];
@@ -4703,6 +4699,17 @@ function SettingsPanel({
4703
4699
  const [diagBusy, setDiagBusy] = useState8(null);
4704
4700
  const [autoUp, setAutoUp] = useState8(() => autoUpdateEnabled());
4705
4701
  const [updBusy, setUpdBusy] = useState8(false);
4702
+ const [canInstall, setCanInstall] = useState8(null);
4703
+ useEffect7(() => {
4704
+ let live2 = true;
4705
+ globalInstallCheck().then((r) => {
4706
+ if (live2) setCanInstall(r.writable);
4707
+ }).catch(() => {
4708
+ });
4709
+ return () => {
4710
+ live2 = false;
4711
+ };
4712
+ }, []);
4706
4713
  const [diag, setDiag] = useState8(null);
4707
4714
  const [diagStep, setDiagStep] = useState8(0);
4708
4715
  const DIAG_STEPS = {
@@ -4720,7 +4727,6 @@ function SettingsPanel({
4720
4727
  const step = setInterval(() => setDiagStep((s) => Math.min(s + 1, total - 1)), 220);
4721
4728
  return () => clearInterval(step);
4722
4729
  }, [diagBusy]);
4723
- void guestAllowance;
4724
4730
  const ver = currentVersion();
4725
4731
  useEffect7(() => {
4726
4732
  let live2 = true;
@@ -4982,15 +4988,27 @@ function SettingsPanel({
4982
4988
  if (updBusy) return;
4983
4989
  setUpdBusy(true);
4984
4990
  setMsg({ text: "updating\u2026", level: "ok" });
4991
+ setDiag(null);
4985
4992
  void (async () => {
4986
4993
  try {
4987
4994
  const res = await updateNow();
4988
4995
  if (res.status === "updated") setMsg({ text: `\u2713 v${res.version} installed \xB7 restart (q, then solongate) to apply`, level: "ok" });
4989
4996
  else if (res.status === "current") setMsg({ text: `\u2713 already on the latest version (v${res.version})`, level: "ok" });
4990
- else if (res.status === "needs-admin") setMsg({ text: `\u2717 npm needs admin rights here \u2014 run: ${adminInstallCommand()}`, level: "bad" });
4991
- else if (res.status === "unreachable") setMsg({ text: "\u2717 could not reach the npm registry", level: "bad" });
4997
+ else if (res.status === "needs-admin") {
4998
+ setMsg({ text: `\u2717 v${res.version} needs admin rights \u2014 run the two commands below`, level: "bad" });
4999
+ setDiag({
5000
+ of: "cli-update",
5001
+ lines: [
5002
+ { text: "npm\u2019s global folder belongs to root on this machine (the usual macOS", level: "warn" },
5003
+ { text: "setup), so SolonGate cannot install there by itself. In a terminal:", level: "warn" },
5004
+ ...adminUpdateSteps().map((s) => ({ text: s.trim(), level: "ok" })),
5005
+ { text: "run repair WITHOUT sudo, or the guard hooks land in root\u2019s home", level: "dim" }
5006
+ ]
5007
+ });
5008
+ } else if (res.status === "unreachable") setMsg({ text: "\u2717 could not reach the npm registry", level: "bad" });
4992
5009
  else setMsg({ text: `\u2717 update to v${res.version} failed \u2014 see ~/.solongate/self-update.log`, level: "bad" });
4993
5010
  setLatest(await latestVersion());
5011
+ setCanInstall((await globalInstallCheck()).writable);
4994
5012
  } finally {
4995
5013
  setUpdBusy(false);
4996
5014
  }
@@ -5004,7 +5022,7 @@ function SettingsPanel({
5004
5022
  setAutoUpdate(next);
5005
5023
  setAutoUp(next);
5006
5024
  setMsg(
5007
- next ? { text: "\u2713 auto-update on \u2014 new versions install in the background", level: "ok" } : { text: "\u2713 auto-update off \u2014 update from this row or with: solongate update", level: "ok" }
5025
+ next ? canInstall === false ? { text: "\u2713 auto-update on \u2014 but npm needs sudo here, so it still cannot install on its own", level: "bad" } : { text: "\u2713 auto-update on \u2014 new versions install in the background", level: "ok" } : { text: "\u2713 auto-update off \u2014 update from this row or with: solongate update", level: "ok" }
5008
5026
  );
5009
5027
  } else if (r.kind === "ll-enabled") {
5010
5028
  if (!local) return;
@@ -5273,15 +5291,6 @@ function SettingsPanel({
5273
5291
  cursor(r),
5274
5292
  /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "+ add account (enter \u2014 device login in the browser)" })
5275
5293
  ] });
5276
- case "allowance": {
5277
- const left = Math.max(0, r.limit - r.used);
5278
- return /* @__PURE__ */ jsxs8(Text8, { wrap: "truncate", children: [
5279
- cursor(r),
5280
- /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "allowance".padEnd(11) }),
5281
- /* @__PURE__ */ jsx8(Text8, { color: left === 0 ? theme.bad : left <= 20 ? theme.warn : theme.ok, bold: true, children: `${r.used}/${r.limit} tool calls` }),
5282
- /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: left === 0 ? " used up \u2014 every call is denied \xB7 create an account to carry on" : ` ${left} left on this guest account \xB7 create an account for no limit` })
5283
- ] });
5284
- }
5285
5294
  case "guard":
5286
5295
  return /* @__PURE__ */ jsxs8(Text8, { wrap: "truncate", children: [
5287
5296
  cursor(r),
@@ -5322,7 +5331,10 @@ function SettingsPanel({
5322
5331
  cursor(r),
5323
5332
  /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "version".padEnd(11) }),
5324
5333
  /* @__PURE__ */ jsx8(Text8, { color: behind ? theme.warn : theme.ok, children: `v${ver}` }),
5325
- updBusy ? /* @__PURE__ */ jsx8(Text8, { color: theme.accentBright, children: ` ${spin} updating\u2026` }) : behind ? /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: ` v${latest} on npm \xB7 enter updates now` }) : /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: latest ? " latest \xB7 enter checks again" : " enter checks npm and updates" })
5334
+ updBusy ? /* @__PURE__ */ jsx8(Text8, { color: theme.accentBright, children: ` ${spin} updating\u2026` }) : behind && canInstall === false ? (
5335
+ // Said before they press enter, not after a minute of npm noise.
5336
+ /* @__PURE__ */ jsx8(Text8, { color: theme.warn, children: ` v${latest} on npm \xB7 needs sudo here \xB7 enter shows how` })
5337
+ ) : behind ? /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: ` v${latest} on npm \xB7 enter updates now` }) : /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: latest ? " latest \xB7 enter checks again" : " enter checks npm and updates" })
5326
5338
  ] });
5327
5339
  }
5328
5340
  case "auto-update":
@@ -5330,7 +5342,7 @@ function SettingsPanel({
5330
5342
  cursor(r),
5331
5343
  /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "auto".padEnd(11) }),
5332
5344
  onOff(autoUp),
5333
- /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: autoUpdateForcedByEnv() ? " set by SOLONGATE_AUTO_UPDATE" : autoUp ? " installs new versions in the background \xB7 enter toggles" : " off: nothing installs on its own (npm -g may need sudo) \xB7 enter toggles" })
5345
+ /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: autoUpdateForcedByEnv() ? " set by SOLONGATE_AUTO_UPDATE" : canInstall === false ? " cannot work here: npm needs sudo, so updates stay manual \xB7 enter toggles" : autoUp ? " installs new versions in the background \xB7 enter toggles" : " off: nothing installs on its own (npm -g may need sudo) \xB7 enter toggles" })
5334
5346
  ] });
5335
5347
  case "ll-enabled":
5336
5348
  return /* @__PURE__ */ jsxs8(Text8, { wrap: "truncate", children: [
@@ -5384,7 +5396,7 @@ function SettingsPanel({
5384
5396
  ] });
5385
5397
  }
5386
5398
  };
5387
- const sectionOf = (r) => r.kind === "acct" || r.kind === "acct-add" || r.kind === "allowance" ? "ACCOUNTS" : r.kind === "guard" || r.kind === "self" || r.kind === "doctor" || r.kind === "repair" ? "PROTECTION" : r.kind === "cli-update" || r.kind === "auto-update" ? "UPDATES" : r.kind === "ll-enabled" || r.kind === "ll-path" || r.kind === "ll-server" ? "LOCAL LOGS" : r.kind === "wh" || r.kind === "wh-add" ? "WEBHOOKS" : "ALERTS";
5399
+ const sectionOf = (r) => r.kind === "acct" || r.kind === "acct-add" ? "ACCOUNTS" : r.kind === "guard" || r.kind === "self" || r.kind === "doctor" || r.kind === "repair" ? "PROTECTION" : r.kind === "cli-update" || r.kind === "auto-update" ? "UPDATES" : r.kind === "ll-enabled" || r.kind === "ll-path" || r.kind === "ll-server" ? "LOCAL LOGS" : r.kind === "wh" || r.kind === "wh-add" ? "WEBHOOKS" : "ALERTS";
5388
5400
  const SECTION_DESC = {
5389
5401
  ACCOUNTS: `on this device (${accounts.length}) \xB7 \u25CF viewing \xB7 ACTIVE = guard key \xB7 x removes`,
5390
5402
  PROTECTION: "guard hook: enter install/update \xB7 d remove \xB7 self-protection \xB7 doctor + repair",
@@ -5429,7 +5441,7 @@ function SettingsPanel({
5429
5441
  lineKey.push("");
5430
5442
  });
5431
5443
  }
5432
- if (diag && (r.kind === "doctor" && diag.of === "doctor" || r.kind === "repair" && diag.of === "repair")) {
5444
+ if (diag && (r.kind === "doctor" && diag.of === "doctor" || r.kind === "repair" && diag.of === "repair" || r.kind === "cli-update" && diag.of === "cli-update")) {
5433
5445
  diag.lines.forEach((l, i) => {
5434
5446
  lineEls.push(
5435
5447
  /* @__PURE__ */ jsx8(
@@ -5554,23 +5566,7 @@ function App() {
5554
5566
  const acctIdx = Math.max(0, accounts.findIndex((a) => a.apiKey === viewKey));
5555
5567
  const cur = accounts[acctIdx];
5556
5568
  const rawEmail = cur?.email ?? "";
5557
- const isGuestAccount2 = rawEmail.endsWith("@anonymous.solongate.invalid");
5558
- const acctLabel2 = !cur ? "not logged in" : isGuestAccount2 ? "Anonymous" : rawEmail || cur.user || cur.project || `account \u2026${cur.apiKey.slice(-4)}`;
5559
- const [guestSpent, setGuestSpent] = useState9(false);
5560
- useEffect8(() => {
5561
- if (!isGuestAccount2) {
5562
- setGuestSpent(false);
5563
- return;
5564
- }
5565
- let alive = true;
5566
- api.auth.me().then((r) => {
5567
- if (alive) setGuestSpent(r.guest?.exhausted === true);
5568
- }).catch(() => {
5569
- });
5570
- return () => {
5571
- alive = false;
5572
- };
5573
- }, [isGuestAccount2, cur?.apiKey]);
5569
+ const acctLabel2 = !cur ? "not logged in" : rawEmail || cur.user || cur.project || `account \u2026${cur.apiKey.slice(-4)}`;
5574
5570
  useEffect8(() => {
5575
5571
  if (!cur || cur.email) return;
5576
5572
  let alive = true;
@@ -5656,12 +5652,7 @@ function App() {
5656
5652
  /* @__PURE__ */ jsxs9(Text9, { wrap: "truncate", children: [
5657
5653
  /* @__PURE__ */ jsx9(Text9, { color: theme.dim, children: "account: " }),
5658
5654
  /* @__PURE__ */ jsx9(Text9, { color: locked ? theme.warn : theme.accentBright, bold: true, children: acctLabel2 }),
5659
- locked ? /* @__PURE__ */ jsx9(Text9, { color: theme.dim, children: " \xB7 log in from Settings to unlock the dataroom" }) : guestSpent ? (
5660
- // Said on this line rather than on a new one: every panel budgets its
5661
- // height against a fixed shell, so one more line up here silently clips
5662
- // a row off whichever panel is open.
5663
- /* @__PURE__ */ jsx9(Text9, { color: theme.bad, bold: true, children: " \xB7 allowance used up \xB7 every call denied \xB7 create an account to carry on" })
5664
- ) : accounts.length > 1 ? /* @__PURE__ */ jsx9(Text9, { color: theme.dim, children: ` (${acctIdx + 1}/${accounts.length} \xB7 a switch \xB7 Settings to manage)` }) : /* @__PURE__ */ jsx9(Text9, { color: theme.dim, children: " \xB7 Settings to add another" }),
5655
+ locked ? /* @__PURE__ */ jsx9(Text9, { color: theme.dim, children: " \xB7 log in from Settings to unlock the dataroom" }) : accounts.length > 1 ? /* @__PURE__ */ jsx9(Text9, { color: theme.dim, children: ` (${acctIdx + 1}/${accounts.length} \xB7 a switch \xB7 Settings to manage)` }) : /* @__PURE__ */ jsx9(Text9, { color: theme.dim, children: " \xB7 Settings to add another" }),
5665
5656
  update2.kind === "updating" ? /* @__PURE__ */ jsx9(Text9, { color: theme.warn, children: ` \u2191 updating to v${update2.version}\u2026` }) : update2.kind === "updated" ? /* @__PURE__ */ jsx9(Text9, { color: theme.ok, bold: true, children: ` \u2191 v${update2.version} installed \xB7 restart (q, then solongate) to apply` }) : update2.kind === "available" ? /* @__PURE__ */ jsx9(Text9, { color: theme.warn, children: ` \u2191 v${update2.version} out \xB7 Settings \u2192 UPDATES` }) : update2.kind === "needs-admin" ? /* @__PURE__ */ jsx9(Text9, { color: theme.warn, children: ` \u2191 v${update2.version} needs admin rights \xB7 Settings \u2192 UPDATES` }) : null
5666
5657
  ] }),
5667
5658
  /* @__PURE__ */ jsxs9(Box9, { marginTop: 1, flexGrow: 1, children: [
@@ -5710,11 +5701,11 @@ async function launchTui() {
5710
5701
  return;
5711
5702
  }
5712
5703
  process.stdout.write("\x1B[?1049h\x1B[H");
5713
- const debugLog = join11(homedir11(), ".solongate", "dataroom-debug.log");
5704
+ const debugLog = join10(homedir10(), ".solongate", "dataroom-debug.log");
5714
5705
  const saved = { log: console.log, warn: console.warn, error: console.error, info: console.info, debug: console.debug };
5715
5706
  const toFile = (level) => (...args) => {
5716
5707
  try {
5717
- mkdirSync7(join11(homedir11(), ".solongate"), { recursive: true });
5708
+ mkdirSync7(join10(homedir10(), ".solongate"), { recursive: true });
5718
5709
  appendFileSync(debugLog, `${(/* @__PURE__ */ new Date()).toISOString()} [${level}] ${args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ")}
5719
5710
  `);
5720
5711
  } catch {
@@ -6536,7 +6536,7 @@ import { resolve, join, dirname, isAbsolute } from "node:path";
6536
6536
  import { homedir } from "node:os";
6537
6537
  import { gunzipSync } from "node:zlib";
6538
6538
  import { createHash } from "node:crypto";
6539
- var HOOK_VERSION = 70;
6539
+ var HOOK_VERSION = 71;
6540
6540
  function localLogsOnly(security) {
6541
6541
  if (security && typeof security === "object") {
6542
6542
  const l = security.localLogs;
@@ -7951,53 +7951,10 @@ function rateLimitCheck(agentKey, limits) {
7951
7951
  return null;
7952
7952
  }
7953
7953
  }
7954
- function quotaTally(used) {
7955
- const dir = resolve(homedir(), ".solongate");
7956
- const markFile = join(dir, ".quota-mark");
7957
- const tallyFile = join(dir, ".quota-tally");
7958
- let n = 0;
7959
- try {
7960
- let mark = null;
7961
- if (existsSync(markFile)) {
7962
- try {
7963
- mark = readFileSync(markFile, "utf-8").trim();
7964
- } catch {
7965
- }
7966
- }
7967
- if (mark !== String(used)) {
7968
- writeFileSync(markFile, String(used));
7969
- writeFileSync(tallyFile, "");
7970
- } else if (existsSync(tallyFile)) {
7971
- try {
7972
- n = statSync(tallyFile).size;
7973
- } catch {
7974
- }
7975
- }
7976
- } catch {
7977
- }
7978
- return {
7979
- n,
7980
- add() {
7981
- try {
7982
- appendFileSync(tallyFile, ".");
7983
- } catch {
7984
- }
7985
- }
7986
- };
7987
- }
7988
7954
  function securityLayerCheck(toolName, args, cfg, agentKey) {
7989
7955
  if (!cfg)
7990
7956
  return null;
7991
7957
  try {
7992
- const q = cfg.guestQuota;
7993
- if (q) {
7994
- const tally = quotaTally(q.used ?? 0);
7995
- const spent = (q.used ?? 0) + tally.n;
7996
- if (q.exhausted || spent >= q.limit) {
7997
- return `Guest allowance used up (${q.limit} tool calls). Create an account at https://auth.solongate.com to carry on. Your project, its policy and everything recorded carry over, and this device keeps working. To stop guarding this machine instead: solongate, Settings, guard, then press d.`;
7998
- }
7999
- tally.add();
8000
- }
8001
7958
  if (cfg.dlpBlock) {
8002
7959
  const hit = dlpScan(args, cfg.dlpBlock);
8003
7960
  if (hit)
package/hooks/guard.mjs CHANGED
@@ -32,7 +32,7 @@ import { createHash } from 'node:crypto';
32
32
  // the installed hook self-updates when the cloud version is higher (see
33
33
  // maybeSelfUpdate). This is what makes guard fixes propagate without a manual
34
34
  // reinstall — the same trust model as the OPA WASM this hook already runs.
35
- const HOOK_VERSION = 70;
35
+ const HOOK_VERSION = 71;
36
36
 
37
37
  // True when local log storage is ON. In that mode logs are kept LOCAL ONLY and
38
38
  // nothing is sent to the cloud audit log.
@@ -1761,62 +1761,10 @@ function rateLimitCheck(agentKey, limits) {
1761
1761
  }
1762
1762
  }
1763
1763
 
1764
- // The allowance figure the API sends is built from audit rows, and those are
1765
- // written AFTER a call runs. Under a burst of parallel tool calls every hook
1766
- // therefore reads the same pre-burst figure, and the allowance overshoots by
1767
- // roughly a batch before the count catches up.
1768
- //
1769
- // The guard is the thing making the calls, so it can close that gap without
1770
- // another round trip: it tallies what it has already let through since the
1771
- // server's figure last moved, and adds it in. One byte is appended per call and
1772
- // the file's LENGTH is the count, because many hooks run at once and a
1773
- // read-modify-write would lose increments; an O_APPEND write of a single byte
1774
- // does not interleave.
1775
- function quotaTally(used) {
1776
- const dir = resolve(homedir(), '.solongate');
1777
- const markFile = join(dir, '.quota-mark');
1778
- const tallyFile = join(dir, '.quota-tally');
1779
- let n = 0;
1780
- try {
1781
- let mark = null;
1782
- if (existsSync(markFile)) { try { mark = readFileSync(markFile, 'utf-8').trim(); } catch {} }
1783
- // A figure that has moved means the audit rows caught up and absorbed the
1784
- // local tally, so it starts again from there.
1785
- if (mark !== String(used)) {
1786
- writeFileSync(markFile, String(used));
1787
- writeFileSync(tallyFile, '');
1788
- } else if (existsSync(tallyFile)) {
1789
- try { n = statSync(tallyFile).size; } catch {}
1790
- }
1791
- } catch { /* a missing tally only costs accuracy near the edge, never the gate */ }
1792
- return {
1793
- n,
1794
- add() { try { appendFileSync(tallyFile, '.'); } catch {} },
1795
- };
1796
- }
1797
-
1798
1764
  // Runs all enabled enforcement layers; returns a deny reason or null (allow).
1799
1765
  function securityLayerCheck(toolName, args, cfg, agentKey) {
1800
1766
  if (!cfg) return null;
1801
1767
  try {
1802
- // Guest allowance. A guest account is a trial, so past its allowance every
1803
- // call is denied without exception rather than quietly running unguarded:
1804
- // an agent that believes it is protected and is not is worse off than one
1805
- // that is stopped and told why. Checked first, so no other layer can let a
1806
- // call through after the allowance is gone.
1807
- const q = cfg.guestQuota;
1808
- if (q) {
1809
- // The server's figure plus what this device has run since, so a burst
1810
- // cannot spend past the allowance while the audit rows are still in flight.
1811
- const tally = quotaTally(q.used ?? 0);
1812
- const spent = (q.used ?? 0) + tally.n;
1813
- if (q.exhausted || spent >= q.limit) {
1814
- return `Guest allowance used up (${q.limit} tool calls). Create an account at https://auth.solongate.com to carry on. Your project, its policy and everything recorded carry over, and this device keeps working. To stop guarding this machine instead: solongate, Settings, guard, then press d.`;
1815
- }
1816
- // Counted here rather than after the verdict, because a call the policy
1817
- // goes on to deny is still recorded and still spends the allowance.
1818
- tally.add();
1819
- }
1820
1768
  if (cfg.dlpBlock) {
1821
1769
  const hit = dlpScan(args, cfg.dlpBlock);
1822
1770
  if (hit) return 'Security layer (DLP): blocked - arguments contain a ' + hit +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.83.3",
3
+ "version": "0.83.5",
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": {