@solongate/proxy 0.83.0 → 0.83.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-client/auth.d.ts +8 -0
- package/dist/index.js +22 -2
- package/dist/tui/index.js +22 -2
- package/hooks/guard.bundled.mjs +42 -3
- package/hooks/guard.mjs +46 -3
- package/package.json +1 -1
|
@@ -8,5 +8,13 @@ export interface Me {
|
|
|
8
8
|
name?: string;
|
|
9
9
|
slug?: string;
|
|
10
10
|
} | null;
|
|
11
|
+
/**
|
|
12
|
+
* Present only for a guest account, and carrying only whether the allowance is
|
|
13
|
+
* spent. The size of it and how much is left are the operator's numbers and
|
|
14
|
+
* are not sent here.
|
|
15
|
+
*/
|
|
16
|
+
guest?: {
|
|
17
|
+
exhausted?: boolean;
|
|
18
|
+
} | null;
|
|
11
19
|
}
|
|
12
20
|
export declare function me(): Promise<Me>;
|
package/dist/index.js
CHANGED
|
@@ -12288,6 +12288,21 @@ function App() {
|
|
|
12288
12288
|
const rawEmail = cur?.email ?? "";
|
|
12289
12289
|
const isGuestAccount2 = rawEmail.endsWith("@anonymous.solongate.invalid");
|
|
12290
12290
|
const acctLabel2 = !cur ? "not logged in" : isGuestAccount2 ? "Anonymous" : rawEmail || cur.user || cur.project || `account \u2026${cur.apiKey.slice(-4)}`;
|
|
12291
|
+
const [guestSpent, setGuestSpent] = useState9(false);
|
|
12292
|
+
useEffect8(() => {
|
|
12293
|
+
if (!isGuestAccount2) {
|
|
12294
|
+
setGuestSpent(false);
|
|
12295
|
+
return;
|
|
12296
|
+
}
|
|
12297
|
+
let alive = true;
|
|
12298
|
+
api.auth.me().then((r) => {
|
|
12299
|
+
if (alive) setGuestSpent(r.guest?.exhausted === true);
|
|
12300
|
+
}).catch(() => {
|
|
12301
|
+
});
|
|
12302
|
+
return () => {
|
|
12303
|
+
alive = false;
|
|
12304
|
+
};
|
|
12305
|
+
}, [isGuestAccount2, cur?.apiKey]);
|
|
12291
12306
|
useEffect8(() => {
|
|
12292
12307
|
if (!cur || cur.email) return;
|
|
12293
12308
|
let alive = true;
|
|
@@ -12373,8 +12388,13 @@ function App() {
|
|
|
12373
12388
|
/* @__PURE__ */ jsxs9(Text9, { wrap: "truncate", children: [
|
|
12374
12389
|
/* @__PURE__ */ jsx9(Text9, { color: theme.dim, children: "account: " }),
|
|
12375
12390
|
/* @__PURE__ */ jsx9(Text9, { color: locked ? theme.warn : theme.accentBright, bold: true, children: acctLabel2 }),
|
|
12376
|
-
locked ? /* @__PURE__ */ jsx9(Text9, { color: theme.dim, children: " \xB7 log in from Settings to unlock the dataroom" }) :
|
|
12377
|
-
|
|
12391
|
+
locked ? /* @__PURE__ */ jsx9(Text9, { color: theme.dim, children: " \xB7 log in from Settings to unlock the dataroom" }) : guestSpent ? (
|
|
12392
|
+
// Said on this line rather than on a new one: every panel budgets its
|
|
12393
|
+
// height against a fixed shell, so one more line up here silently clips
|
|
12394
|
+
// a row off whichever panel is open.
|
|
12395
|
+
/* @__PURE__ */ jsx9(Text9, { color: theme.bad, bold: true, children: " \xB7 allowance used up \xB7 every call denied \xB7 create an account to carry on" })
|
|
12396
|
+
) : 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" }),
|
|
12397
|
+
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` }) : null
|
|
12378
12398
|
] }),
|
|
12379
12399
|
/* @__PURE__ */ jsxs9(Box9, { marginTop: 1, flexGrow: 1, children: [
|
|
12380
12400
|
/* @__PURE__ */ jsx9(Box9, { flexDirection: "column", flexShrink: 0, width: 16, borderStyle: "round", borderColor: focus === "nav" ? theme.accent : "gray", paddingX: 1, children: SECTIONS.map((s, i) => {
|
package/dist/tui/index.js
CHANGED
|
@@ -5460,6 +5460,21 @@ function App() {
|
|
|
5460
5460
|
const rawEmail = cur?.email ?? "";
|
|
5461
5461
|
const isGuestAccount2 = rawEmail.endsWith("@anonymous.solongate.invalid");
|
|
5462
5462
|
const acctLabel2 = !cur ? "not logged in" : isGuestAccount2 ? "Anonymous" : rawEmail || cur.user || cur.project || `account \u2026${cur.apiKey.slice(-4)}`;
|
|
5463
|
+
const [guestSpent, setGuestSpent] = useState9(false);
|
|
5464
|
+
useEffect8(() => {
|
|
5465
|
+
if (!isGuestAccount2) {
|
|
5466
|
+
setGuestSpent(false);
|
|
5467
|
+
return;
|
|
5468
|
+
}
|
|
5469
|
+
let alive = true;
|
|
5470
|
+
api.auth.me().then((r) => {
|
|
5471
|
+
if (alive) setGuestSpent(r.guest?.exhausted === true);
|
|
5472
|
+
}).catch(() => {
|
|
5473
|
+
});
|
|
5474
|
+
return () => {
|
|
5475
|
+
alive = false;
|
|
5476
|
+
};
|
|
5477
|
+
}, [isGuestAccount2, cur?.apiKey]);
|
|
5463
5478
|
useEffect8(() => {
|
|
5464
5479
|
if (!cur || cur.email) return;
|
|
5465
5480
|
let alive = true;
|
|
@@ -5545,8 +5560,13 @@ function App() {
|
|
|
5545
5560
|
/* @__PURE__ */ jsxs9(Text9, { wrap: "truncate", children: [
|
|
5546
5561
|
/* @__PURE__ */ jsx9(Text9, { color: theme.dim, children: "account: " }),
|
|
5547
5562
|
/* @__PURE__ */ jsx9(Text9, { color: locked ? theme.warn : theme.accentBright, bold: true, children: acctLabel2 }),
|
|
5548
|
-
locked ? /* @__PURE__ */ jsx9(Text9, { color: theme.dim, children: " \xB7 log in from Settings to unlock the dataroom" }) :
|
|
5549
|
-
|
|
5563
|
+
locked ? /* @__PURE__ */ jsx9(Text9, { color: theme.dim, children: " \xB7 log in from Settings to unlock the dataroom" }) : guestSpent ? (
|
|
5564
|
+
// Said on this line rather than on a new one: every panel budgets its
|
|
5565
|
+
// height against a fixed shell, so one more line up here silently clips
|
|
5566
|
+
// a row off whichever panel is open.
|
|
5567
|
+
/* @__PURE__ */ jsx9(Text9, { color: theme.bad, bold: true, children: " \xB7 allowance used up \xB7 every call denied \xB7 create an account to carry on" })
|
|
5568
|
+
) : 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" }),
|
|
5569
|
+
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` }) : null
|
|
5550
5570
|
] }),
|
|
5551
5571
|
/* @__PURE__ */ jsxs9(Box9, { marginTop: 1, flexGrow: 1, children: [
|
|
5552
5572
|
/* @__PURE__ */ jsx9(Box9, { flexDirection: "column", flexShrink: 0, width: 16, borderStyle: "round", borderColor: focus === "nav" ? theme.accent : "gray", paddingX: 1, children: SECTIONS.map((s, i) => {
|
package/hooks/guard.bundled.mjs
CHANGED
|
@@ -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 =
|
|
6539
|
+
var HOOK_VERSION = 70;
|
|
6540
6540
|
function localLogsOnly(security) {
|
|
6541
6541
|
if (security && typeof security === "object") {
|
|
6542
6542
|
const l = security.localLogs;
|
|
@@ -7951,13 +7951,52 @@ 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
|
+
}
|
|
7954
7988
|
function securityLayerCheck(toolName, args, cfg, agentKey) {
|
|
7955
7989
|
if (!cfg)
|
|
7956
7990
|
return null;
|
|
7957
7991
|
try {
|
|
7958
7992
|
const q = cfg.guestQuota;
|
|
7959
|
-
if (q
|
|
7960
|
-
|
|
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();
|
|
7961
8000
|
}
|
|
7962
8001
|
if (cfg.dlpBlock) {
|
|
7963
8002
|
const hit = dlpScan(args, cfg.dlpBlock);
|
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 =
|
|
35
|
+
const HOOK_VERSION = 70;
|
|
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,6 +1761,40 @@ 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
|
+
|
|
1764
1798
|
// Runs all enabled enforcement layers; returns a deny reason or null (allow).
|
|
1765
1799
|
function securityLayerCheck(toolName, args, cfg, agentKey) {
|
|
1766
1800
|
if (!cfg) return null;
|
|
@@ -1771,8 +1805,17 @@ function securityLayerCheck(toolName, args, cfg, agentKey) {
|
|
|
1771
1805
|
// that is stopped and told why. Checked first, so no other layer can let a
|
|
1772
1806
|
// call through after the allowance is gone.
|
|
1773
1807
|
const q = cfg.guestQuota;
|
|
1774
|
-
if (q
|
|
1775
|
-
|
|
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();
|
|
1776
1819
|
}
|
|
1777
1820
|
if (cfg.dlpBlock) {
|
|
1778
1821
|
const hit = dlpScan(args, cfg.dlpBlock);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solongate/proxy",
|
|
3
|
-
"version": "0.83.
|
|
3
|
+
"version": "0.83.2",
|
|
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": {
|