@solongate/proxy 0.82.88 → 0.82.90

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.
@@ -28,6 +28,8 @@ export declare const GUEST_EMAIL_SUFFIX = "@anonymous.solongate.invalid";
28
28
  * way the dashboard does rather than half-working here.
29
29
  */
30
30
  export declare function isGuestAccount(): boolean;
31
+ /** The key this device enforces with, or null when it is not paired. */
32
+ export declare function enforcingKey(): string | null;
31
33
  /** True when the given key is the ACTIVE one the guard hooks use. */
32
34
  export declare function isActiveAccount(apiKey: string): boolean;
33
35
  /**
@@ -54,6 +56,10 @@ export declare class ApiError extends Error {
54
56
  readonly code: string;
55
57
  constructor(status: number, code: string, message: string);
56
58
  }
59
+ /** Raised when a guest account tries to change something. */
60
+ export declare class GuestReadOnlyError extends Error {
61
+ constructor();
62
+ }
57
63
  /** Raised when no API key can be found — tells the user to run `solongate` and pair. */
58
64
  export declare class NotAuthenticatedError extends Error {
59
65
  constructor();
@@ -125,7 +125,31 @@ function codexHooksStatus() {
125
125
 
126
126
  // src/api-client/client.ts
127
127
  var DEFAULT_API_URL = "https://api.solongate.com";
128
+ var accountsFile = () => join2(homedir2(), ".solongate", "accounts.json");
129
+ function listAccounts() {
130
+ let list5 = [];
131
+ try {
132
+ const raw = JSON.parse(readFileSync2(accountsFile(), "utf-8"));
133
+ if (Array.isArray(raw)) list5 = raw.filter((a) => a && typeof a.apiKey === "string");
134
+ } catch {
135
+ }
136
+ const active2 = loginCredentialFile();
137
+ if (active2.apiKey && !list5.some((a) => a.apiKey === active2.apiKey)) {
138
+ list5.unshift({ apiKey: active2.apiKey, apiUrl: active2.apiUrl || DEFAULT_API_URL });
139
+ }
140
+ return list5;
141
+ }
128
142
  var viewOverride = null;
143
+ var GUEST_EMAIL_SUFFIX = "@anonymous.solongate.invalid";
144
+ function isGuestAccount() {
145
+ try {
146
+ const active2 = loginCredentialFile().apiKey;
147
+ if (!active2) return false;
148
+ return listAccounts().some((a) => a.apiKey === active2 && (a.email ?? "").endsWith(GUEST_EMAIL_SUFFIX));
149
+ } catch {
150
+ return false;
151
+ }
152
+ }
129
153
  var ApiError = class extends Error {
130
154
  status;
131
155
  code;
@@ -136,6 +160,12 @@ var ApiError = class extends Error {
136
160
  this.code = code;
137
161
  }
138
162
  };
163
+ var GuestReadOnlyError = class extends Error {
164
+ constructor() {
165
+ super("Guest account: sign up at auth.solongate.com to change anything. Your project, its policy and everything recorded carry over.");
166
+ this.name = "GuestReadOnlyError";
167
+ }
168
+ };
139
169
  var NotAuthenticatedError = class extends Error {
140
170
  constructor() {
141
171
  super("Not logged in. Run `solongate` and log in from the Accounts panel.");
@@ -200,6 +230,9 @@ function buildUrl(base, path, query) {
200
230
  return url.toString();
201
231
  }
202
232
  async function request(method, path, opts = {}) {
233
+ if (method !== "GET" && isGuestAccount()) {
234
+ throw new GuestReadOnlyError();
235
+ }
203
236
  const creds = resolveCredentials(opts.apiUrl);
204
237
  const url = buildUrl(creds.apiUrl, path, opts.query);
205
238
  const headers = {
@@ -1149,6 +1182,7 @@ import { join as join4 } from "path";
1149
1182
  // src/tui/local-log.ts
1150
1183
  import { closeSync, existsSync as existsSync3, openSync, readdirSync, readFileSync as readFileSync4, readSync, statSync, writeFileSync as writeFileSync3 } from "fs";
1151
1184
  import { homedir as homedir3 } from "os";
1185
+ import { createHash } from "crypto";
1152
1186
  import { isAbsolute, join as join3 } from "path";
1153
1187
  var DEFAULT_LOCAL_LOG = join3(homedir3(), ".solongate", "local-logs", "solongate-audit.jsonl");
1154
1188
  function localLogFile() {
package/dist/index.js CHANGED
@@ -7404,6 +7404,7 @@ var init_components = __esm({
7404
7404
  // src/tui/local-log.ts
7405
7405
  import { closeSync, existsSync as existsSync4, openSync as openSync3, readdirSync, readFileSync as readFileSync8, readSync, statSync, writeFileSync as writeFileSync6 } from "fs";
7406
7406
  import { homedir as homedir6 } from "os";
7407
+ import { createHash as createHash2 } from "crypto";
7407
7408
  import { isAbsolute, join as join8 } from "path";
7408
7409
  function localLogFile() {
7409
7410
  try {
@@ -7435,6 +7436,30 @@ function localLogFile() {
7435
7436
  }
7436
7437
  return DEFAULT_LOCAL_LOG;
7437
7438
  }
7439
+ function ensureLocalLogOwner(activeApiKey) {
7440
+ try {
7441
+ const marker = join8(homedir6(), ".solongate", "local-logs", ".owner");
7442
+ const want = activeApiKey ? createHash2("sha256").update(activeApiKey).digest("hex").slice(0, 16) : "";
7443
+ let have = "";
7444
+ try {
7445
+ have = readFileSync8(marker, "utf-8").trim();
7446
+ } catch {
7447
+ }
7448
+ if (have === want) return false;
7449
+ const cleared = have !== "" && localLogExists();
7450
+ if (cleared) clearLocalLog();
7451
+ try {
7452
+ writeFileSync6(marker, want);
7453
+ } catch {
7454
+ }
7455
+ return cleared;
7456
+ } catch {
7457
+ return false;
7458
+ }
7459
+ }
7460
+ function localLogExists() {
7461
+ return existsSync4(localLogFile());
7462
+ }
7438
7463
  function tailLines(file, maxBytes = 131072) {
7439
7464
  try {
7440
7465
  const size = statSync(file).size;
@@ -7523,8 +7548,10 @@ __export(client_exports, {
7523
7548
  ApiError: () => ApiError,
7524
7549
  DEFAULT_API_URL: () => DEFAULT_API_URL2,
7525
7550
  GUEST_EMAIL_SUFFIX: () => GUEST_EMAIL_SUFFIX,
7551
+ GuestReadOnlyError: () => GuestReadOnlyError,
7526
7552
  NotAuthenticatedError: () => NotAuthenticatedError,
7527
7553
  clearActiveCredential: () => clearActiveCredential,
7554
+ enforcingKey: () => enforcingKey,
7528
7555
  isActiveAccount: () => isActiveAccount,
7529
7556
  isAuthenticated: () => isAuthenticated,
7530
7557
  isGuestAccount: () => isGuestAccount,
@@ -7596,6 +7623,9 @@ function isGuestAccount() {
7596
7623
  return false;
7597
7624
  }
7598
7625
  }
7626
+ function enforcingKey() {
7627
+ return loginCredentialFile().apiKey ?? null;
7628
+ }
7599
7629
  function isActiveAccount(apiKey) {
7600
7630
  return loginCredentialFile().apiKey === apiKey;
7601
7631
  }
@@ -7695,6 +7725,9 @@ function buildUrl(base, path, query) {
7695
7725
  return url.toString();
7696
7726
  }
7697
7727
  async function request(method, path, opts = {}) {
7728
+ if (method !== "GET" && isGuestAccount()) {
7729
+ throw new GuestReadOnlyError();
7730
+ }
7698
7731
  const creds = resolveCredentials(opts.apiUrl);
7699
7732
  const url = buildUrl(creds.apiUrl, path, opts.query);
7700
7733
  const headers = {
@@ -7750,7 +7783,7 @@ async function request(method, path, opts = {}) {
7750
7783
  }
7751
7784
  return json;
7752
7785
  }
7753
- var DEFAULT_API_URL2, accountsFile, viewOverride, GUEST_EMAIL_SUFFIX, ApiError, NotAuthenticatedError, cached2;
7786
+ var DEFAULT_API_URL2, accountsFile, viewOverride, GUEST_EMAIL_SUFFIX, ApiError, GuestReadOnlyError, NotAuthenticatedError, cached2;
7754
7787
  var init_client = __esm({
7755
7788
  "src/api-client/client.ts"() {
7756
7789
  "use strict";
@@ -7769,6 +7802,12 @@ var init_client = __esm({
7769
7802
  this.code = code;
7770
7803
  }
7771
7804
  };
7805
+ GuestReadOnlyError = class extends Error {
7806
+ constructor() {
7807
+ super("Guest account: sign up at auth.solongate.com to change anything. Your project, its policy and everything recorded carry over.");
7808
+ this.name = "GuestReadOnlyError";
7809
+ }
7810
+ };
7772
7811
  NotAuthenticatedError = class extends Error {
7773
7812
  constructor() {
7774
7813
  super("Not logged in. Run `solongate` and log in from the Accounts panel.");
@@ -11741,6 +11780,7 @@ function SettingsPanel({
11741
11780
  else if (inp === "m") {
11742
11781
  if (cur.kind === "acct") {
11743
11782
  const ok = setActiveAccount({ apiKey: cur.acc.apiKey, apiUrl: cur.acc.apiUrl });
11783
+ if (ok) clearLocalLog();
11744
11784
  setMsg(ok ? { text: `\u2713 ${acctLabel(cur.acc)} is now the ACTIVE key (guard + logging)`, level: "ok" } : { text: "\u2717 could not set active", level: "bad" });
11745
11785
  refreshAccounts();
11746
11786
  } else if (cur.kind === "wh" || cur.kind === "alert" || cur.kind === "self" || cur.kind === "ll-enabled" || cur.kind === "ll-server") {
@@ -11760,6 +11800,7 @@ function SettingsPanel({
11760
11800
  }
11761
11801
  setConfirmDel(null);
11762
11802
  removeAccount(target.apiKey);
11803
+ clearLocalLog();
11763
11804
  let extra = " from this device";
11764
11805
  if (wasActive) {
11765
11806
  if (others.length) {
@@ -12107,6 +12148,7 @@ var init_Settings = __esm({
12107
12148
  init_components();
12108
12149
  init_hooks();
12109
12150
  init_theme();
12151
+ init_local_log();
12110
12152
  EVENTS = ["denials", "allowed", "all"];
12111
12153
  SPIN2 = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
12112
12154
  SIGNALS2 = ["any", "deny", "dlp", "ratelimit"];
@@ -12190,8 +12232,7 @@ function App() {
12190
12232
  const cur = accounts[acctIdx];
12191
12233
  const rawEmail = cur?.email ?? "";
12192
12234
  const isGuestAccount2 = rawEmail.endsWith("@anonymous.solongate.invalid");
12193
- const hasKey = isAuthenticated();
12194
- const acctLabel2 = !cur ? "not logged in" : !hasKey ? "signed out" : isGuestAccount2 ? "Anonymous" : rawEmail || cur.user || cur.project || `account \u2026${cur.apiKey.slice(-4)}`;
12235
+ const acctLabel2 = !cur ? "not logged in" : isGuestAccount2 ? "Anonymous" : rawEmail || cur.user || cur.project || `account \u2026${cur.apiKey.slice(-4)}`;
12195
12236
  useEffect8(() => {
12196
12237
  if (!cur || cur.email) return;
12197
12238
  let alive = true;
@@ -12226,7 +12267,13 @@ function App() {
12226
12267
  setViewKey(next?.apiKey);
12227
12268
  setViewNonce((n) => n + 1);
12228
12269
  };
12229
- const locked = accounts.length === 0 || !hasKey;
12270
+ useEffect8(() => {
12271
+ try {
12272
+ ensureLocalLogOwner(enforcingKey());
12273
+ } catch {
12274
+ }
12275
+ }, [accounts.length]);
12276
+ const locked = accounts.length === 0;
12230
12277
  const effectiveSection = locked ? SETTINGS_SECTION : section;
12231
12278
  useInput8((input, key) => {
12232
12279
  if (help) {
@@ -12317,6 +12364,7 @@ var init_App = __esm({
12317
12364
  init_nav();
12318
12365
  init_client();
12319
12366
  init_api_client();
12367
+ init_local_log();
12320
12368
  init_self_update();
12321
12369
  SECTIONS = [
12322
12370
  { label: "Live", Panel: LiveHint },
package/dist/tui/index.js CHANGED
@@ -738,6 +738,7 @@ import { join as join5 } from "path";
738
738
  // src/tui/local-log.ts
739
739
  import { closeSync, existsSync, openSync, readdirSync, readFileSync as readFileSync2, readSync, statSync, writeFileSync } from "fs";
740
740
  import { homedir as homedir2 } from "os";
741
+ import { createHash } from "crypto";
741
742
  import { isAbsolute, join as join2 } from "path";
742
743
  var DEFAULT_LOCAL_LOG = join2(homedir2(), ".solongate", "local-logs", "solongate-audit.jsonl");
743
744
  function localLogFile() {
@@ -771,6 +772,30 @@ function localLogFile() {
771
772
  return DEFAULT_LOCAL_LOG;
772
773
  }
773
774
  var LOCAL_LOG = localLogFile();
775
+ function ensureLocalLogOwner(activeApiKey) {
776
+ try {
777
+ const marker = join2(homedir2(), ".solongate", "local-logs", ".owner");
778
+ const want = activeApiKey ? createHash("sha256").update(activeApiKey).digest("hex").slice(0, 16) : "";
779
+ let have = "";
780
+ try {
781
+ have = readFileSync2(marker, "utf-8").trim();
782
+ } catch {
783
+ }
784
+ if (have === want) return false;
785
+ const cleared = have !== "" && localLogExists();
786
+ if (cleared) clearLocalLog();
787
+ try {
788
+ writeFileSync(marker, want);
789
+ } catch {
790
+ }
791
+ return cleared;
792
+ } catch {
793
+ return false;
794
+ }
795
+ }
796
+ function localLogExists() {
797
+ return existsSync(localLogFile());
798
+ }
774
799
  function tailLines(file, maxBytes = 131072) {
775
800
  try {
776
801
  const size = statSync(file).size;
@@ -904,6 +929,19 @@ function setViewCredentials(creds) {
904
929
  viewOverride = creds;
905
930
  cached2 = null;
906
931
  }
932
+ var GUEST_EMAIL_SUFFIX = "@anonymous.solongate.invalid";
933
+ function isGuestAccount() {
934
+ try {
935
+ const active2 = loginCredentialFile().apiKey;
936
+ if (!active2) return false;
937
+ return listAccounts().some((a) => a.apiKey === active2 && (a.email ?? "").endsWith(GUEST_EMAIL_SUFFIX));
938
+ } catch {
939
+ return false;
940
+ }
941
+ }
942
+ function enforcingKey() {
943
+ return loginCredentialFile().apiKey ?? null;
944
+ }
907
945
  function isActiveAccount(apiKey) {
908
946
  return loginCredentialFile().apiKey === apiKey;
909
947
  }
@@ -956,6 +994,12 @@ var ApiError = class extends Error {
956
994
  this.code = code;
957
995
  }
958
996
  };
997
+ var GuestReadOnlyError = class extends Error {
998
+ constructor() {
999
+ super("Guest account: sign up at auth.solongate.com to change anything. Your project, its policy and everything recorded carry over.");
1000
+ this.name = "GuestReadOnlyError";
1001
+ }
1002
+ };
959
1003
  var NotAuthenticatedError = class extends Error {
960
1004
  constructor() {
961
1005
  super("Not logged in. Run `solongate` and log in from the Accounts panel.");
@@ -1020,6 +1064,9 @@ function buildUrl(base, path, query) {
1020
1064
  return url.toString();
1021
1065
  }
1022
1066
  async function request(method, path, opts = {}) {
1067
+ if (method !== "GET" && isGuestAccount()) {
1068
+ throw new GuestReadOnlyError();
1069
+ }
1023
1070
  const creds = resolveCredentials(opts.apiUrl);
1024
1071
  const url = buildUrl(creds.apiUrl, path, opts.query);
1025
1072
  const headers = {
@@ -4929,6 +4976,7 @@ function SettingsPanel({
4929
4976
  else if (inp === "m") {
4930
4977
  if (cur.kind === "acct") {
4931
4978
  const ok = setActiveAccount({ apiKey: cur.acc.apiKey, apiUrl: cur.acc.apiUrl });
4979
+ if (ok) clearLocalLog();
4932
4980
  setMsg(ok ? { text: `\u2713 ${acctLabel(cur.acc)} is now the ACTIVE key (guard + logging)`, level: "ok" } : { text: "\u2717 could not set active", level: "bad" });
4933
4981
  refreshAccounts();
4934
4982
  } else if (cur.kind === "wh" || cur.kind === "alert" || cur.kind === "self" || cur.kind === "ll-enabled" || cur.kind === "ll-server") {
@@ -4948,6 +4996,7 @@ function SettingsPanel({
4948
4996
  }
4949
4997
  setConfirmDel(null);
4950
4998
  removeAccount(target.apiKey);
4999
+ clearLocalLog();
4951
5000
  let extra = " from this device";
4952
5001
  if (wasActive) {
4953
5002
  if (others.length) {
@@ -5354,9 +5403,8 @@ function App() {
5354
5403
  const acctIdx = Math.max(0, accounts.findIndex((a) => a.apiKey === viewKey));
5355
5404
  const cur = accounts[acctIdx];
5356
5405
  const rawEmail = cur?.email ?? "";
5357
- const isGuestAccount = rawEmail.endsWith("@anonymous.solongate.invalid");
5358
- const hasKey = isAuthenticated();
5359
- const acctLabel2 = !cur ? "not logged in" : !hasKey ? "signed out" : isGuestAccount ? "Anonymous" : rawEmail || cur.user || cur.project || `account \u2026${cur.apiKey.slice(-4)}`;
5406
+ const isGuestAccount2 = rawEmail.endsWith("@anonymous.solongate.invalid");
5407
+ const acctLabel2 = !cur ? "not logged in" : isGuestAccount2 ? "Anonymous" : rawEmail || cur.user || cur.project || `account \u2026${cur.apiKey.slice(-4)}`;
5360
5408
  useEffect8(() => {
5361
5409
  if (!cur || cur.email) return;
5362
5410
  let alive = true;
@@ -5391,7 +5439,13 @@ function App() {
5391
5439
  setViewKey(next?.apiKey);
5392
5440
  setViewNonce((n) => n + 1);
5393
5441
  };
5394
- const locked = accounts.length === 0 || !hasKey;
5442
+ useEffect8(() => {
5443
+ try {
5444
+ ensureLocalLogOwner(enforcingKey());
5445
+ } catch {
5446
+ }
5447
+ }, [accounts.length]);
5448
+ const locked = accounts.length === 0;
5395
5449
  const effectiveSection = locked ? SETTINGS_SECTION : section;
5396
5450
  useInput8((input, key) => {
5397
5451
  if (help) {
@@ -23,6 +23,18 @@ export declare function localLogFile(): string;
23
23
  export declare const LOCAL_LOG: string;
24
24
  /** True when the resolved file exists — viewers use it to say "no entries yet"
25
25
  * instead of pointing at a path that was never written. */
26
+ /**
27
+ * Make sure the local log belongs to the account now in use.
28
+ *
29
+ * The file is one per machine and no entry records which account produced it,
30
+ * so after pairing a different account the previous one's calls were still
31
+ * sitting in Live. Clearing on an in-app account switch was not enough: the
32
+ * account can change by pairing a new device, by signing out, or from the
33
+ * dashboard. A marker beside the log records whose it is (a hash, never the
34
+ * key itself); when that stops matching, the file is not ours and is started
35
+ * over. Returns true when it cleared.
36
+ */
37
+ export declare function ensureLocalLogOwner(activeApiKey: string | null | undefined): boolean;
26
38
  export declare function localLogExists(): boolean;
27
39
  /** Read the last `maxBytes` of a file as complete lines (first partial dropped). */
28
40
  export declare function tailLines(file: string, maxBytes?: number): string[];
@@ -44,7 +56,13 @@ export interface LocalLogLine {
44
56
  }
45
57
  /** Delete ONE matching line (ts + tool [+ session]) from the local log file. */
46
58
  export declare function deleteLocalEntry(at: number, tool: string, session?: string | null): number;
47
- /** Empty the local log file. Returns how many lines were removed. */
59
+ /**
60
+ * Empty the local log file. Returns how many lines were removed.
61
+ *
62
+ * The file is one per machine, not one per account, and nothing in an entry
63
+ * says which account produced it — so when the account changes, the honest move
64
+ * is to start it over rather than keep showing another account's calls in Live.
65
+ */
48
66
  export declare function clearLocalLog(): number;
49
67
  /** DLP pattern name + rate-limit-burst flag derived from a decision's reason. */
50
68
  export declare function reasonSignals(reason: string | null | undefined): {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.82.88",
3
+ "version": "0.82.90",
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": {