@suveren/gateway 0.12.0 → 0.13.0

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.
@@ -5,8 +5,8 @@ import {
5
5
 
6
6
  // src/index.ts
7
7
  import { randomBytes as randomBytes3 } from "crypto";
8
- import { existsSync as existsSync9, readFileSync as readFileSync11, renameSync as renameSync2 } from "fs";
9
- import { dirname as dirname8, join as join11 } from "path";
8
+ import { existsSync as existsSync10, readFileSync as readFileSync12, renameSync as renameSync2 } from "fs";
9
+ import { dirname as dirname8, join as join12 } from "path";
10
10
  import { homedir as homedir7 } from "os";
11
11
  import express from "express";
12
12
  import { createProxyMiddleware } from "http-proxy-middleware";
@@ -732,9 +732,138 @@ import { canonicalize } from "@hap/core";
732
732
  // src/lib/as-tls-pin.ts
733
733
  import { createHash as createHash3 } from "crypto";
734
734
  import { checkServerIdentity as defaultCheckServerIdentity, rootCertificates } from "tls";
735
- import { Agent as HttpsAgent } from "https";
735
+ import { Agent as HttpsAgent2 } from "https";
736
736
  import { readFileSync as readFileSync4 } from "fs";
737
- import { Agent, fetch as undiciFetch } from "undici";
737
+ import { Agent, ProxyAgent, fetch as undiciFetch } from "undici";
738
+
739
+ // src/lib/proxy-env.ts
740
+ function isLoopbackHostname(hostname) {
741
+ const h = hostname.replace(/^\[|\]$/g, "").toLowerCase();
742
+ if (h === "localhost") return true;
743
+ if (h === "::1" || h === "0:0:0:0:0:0:0:1") return true;
744
+ return /^127\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.test(h);
745
+ }
746
+ function readProxyEnv() {
747
+ const http = process.env.http_proxy || process.env.HTTP_PROXY || void 0;
748
+ const https = process.env.https_proxy || process.env.HTTPS_PROXY || void 0;
749
+ const noProxy = process.env.no_proxy ?? process.env.NO_PROXY ?? "";
750
+ return { http, https, noProxy };
751
+ }
752
+ function matchesNoProxy(hostname, port2, noProxy) {
753
+ const entries = noProxy.split(/[,\s]+/).filter(Boolean);
754
+ if (entries.length === 0) return false;
755
+ if (entries.includes("*")) return true;
756
+ const h = hostname.toLowerCase();
757
+ for (const raw of entries) {
758
+ const m = raw.match(/^(.+):(\d+)$/);
759
+ const entryHost = (m ? m[1] : raw).replace(/^\*?\./, "").toLowerCase();
760
+ const entryPort = m ? Number(m[2]) : 0;
761
+ if (entryPort && entryPort !== port2) continue;
762
+ if (h === entryHost || h.endsWith(`.${entryHost}`)) return true;
763
+ }
764
+ return false;
765
+ }
766
+ function selectProxyUrl(targetUrl) {
767
+ const url = typeof targetUrl === "string" ? new URL(targetUrl) : targetUrl;
768
+ if (isLoopbackHostname(url.hostname)) return void 0;
769
+ const { http, https, noProxy } = readProxyEnv();
770
+ const port2 = url.port ? Number(url.port) : url.protocol === "https:" ? 443 : 80;
771
+ if (matchesNoProxy(url.hostname, port2, noProxy)) return void 0;
772
+ return url.protocol === "https:" ? https ?? http : http;
773
+ }
774
+
775
+ // src/lib/proxy-https-agent.ts
776
+ import { Agent as HttpsAgent } from "https";
777
+ import { connect as netConnect } from "net";
778
+ import { connect as tlsConnect } from "tls";
779
+ var MAX_CONNECT_RESPONSE_BYTES = 64 * 1024;
780
+ function openConnectTunnel(proxyUrl, host, port2) {
781
+ return new Promise((resolve2, reject) => {
782
+ const socket = netConnect({ host: proxyUrl.hostname, port: Number(proxyUrl.port) || 80 });
783
+ const onSocketError = (err) => {
784
+ socket.removeListener("data", onData);
785
+ reject(err);
786
+ };
787
+ socket.once("error", onSocketError);
788
+ socket.once("connect", () => {
789
+ const authHeader = proxyUrl.username ? `Proxy-Authorization: Basic ${Buffer.from(
790
+ `${decodeURIComponent(proxyUrl.username)}:${decodeURIComponent(proxyUrl.password)}`
791
+ ).toString("base64")}\r
792
+ ` : "";
793
+ socket.write(
794
+ `CONNECT ${host}:${port2} HTTP/1.1\r
795
+ Host: ${host}:${port2}\r
796
+ ${authHeader}Proxy-Connection: Keep-Alive\r
797
+ \r
798
+ `
799
+ );
800
+ });
801
+ let buffered = Buffer.alloc(0);
802
+ const onData = (chunk) => {
803
+ buffered = Buffer.concat([buffered, chunk]);
804
+ const headerEnd = buffered.indexOf("\r\n\r\n");
805
+ if (headerEnd === -1) {
806
+ if (buffered.length > MAX_CONNECT_RESPONSE_BYTES) {
807
+ socket.removeListener("error", onSocketError);
808
+ socket.destroy();
809
+ reject(new Error(`Proxy CONNECT response to ${host}:${port2} exceeded ${MAX_CONNECT_RESPONSE_BYTES} bytes without completing`));
810
+ }
811
+ return;
812
+ }
813
+ socket.removeListener("data", onData);
814
+ socket.removeListener("error", onSocketError);
815
+ const statusLine = buffered.subarray(0, buffered.indexOf("\r\n")).toString("latin1");
816
+ const statusCode = Number(statusLine.split(" ")[1]);
817
+ if (statusCode !== 200) {
818
+ socket.destroy();
819
+ reject(new Error(`Proxy CONNECT to ${host}:${port2} failed: ${statusLine.trim() || `unexpected status ${statusCode}`}`));
820
+ return;
821
+ }
822
+ const extra = buffered.subarray(headerEnd + 4);
823
+ if (extra.length > 0) socket.unshift(extra);
824
+ resolve2(socket);
825
+ };
826
+ socket.on("data", onData);
827
+ });
828
+ }
829
+ var ConnectTunnelHttpsAgent = class extends HttpsAgent {
830
+ proxyUrl;
831
+ constructor(options) {
832
+ const { proxyUrl, ...agentOptions } = options;
833
+ super(agentOptions);
834
+ this.proxyUrl = new URL(proxyUrl);
835
+ }
836
+ /**
837
+ * Matches `https.Agent`'s own `createConnection` signature exactly (see
838
+ * `node:https`'s type definitions) so this remains a true override — the
839
+ * base type's `callback` parameter is optional (a from-scratch `Agent`
840
+ * subclass is allowed to connect synchronously and return the stream
841
+ * directly instead), but every real caller in this codebase (Node's own
842
+ * HTTP client machinery) always supplies one, since a CONNECT tunnel is
843
+ * inherently asynchronous. The explicit runtime check turns a theoretical
844
+ * synchronous caller into a clear error instead of a silent no-op.
845
+ */
846
+ createConnection(options, callback) {
847
+ if (!callback) {
848
+ throw new Error("ConnectTunnelHttpsAgent.createConnection requires a callback (synchronous connection creation is not supported)");
849
+ }
850
+ const host = options.host ?? "localhost";
851
+ const port2 = options.port != null ? Number(options.port) : 443;
852
+ openConnectTunnel(this.proxyUrl, host, port2).then((tunnelSocket) => {
853
+ const tlsSocket = tlsConnect({
854
+ ...this.options,
855
+ ...options,
856
+ socket: tunnelSocket
857
+ });
858
+ tlsSocket.once("close", () => tunnelSocket.destroy());
859
+ tlsSocket.once("secureConnect", () => callback(null, tlsSocket));
860
+ tlsSocket.once("error", (err) => callback(err, tlsSocket));
861
+ }).catch((err) => callback(err, void 0));
862
+ return void 0;
863
+ }
864
+ };
865
+
866
+ // src/lib/as-tls-pin.ts
738
867
  function effectiveCa() {
739
868
  const path = process.env.NODE_EXTRA_CA_CERTS;
740
869
  if (!path) return void 0;
@@ -794,42 +923,47 @@ function pinnedCheckServerIdentity(pinnedSpkiHex) {
794
923
  return void 0;
795
924
  };
796
925
  }
797
- function buildPinnedDispatcher(pinnedSpkiHex) {
798
- return new Agent({
799
- connect: {
800
- ca: effectiveCa(),
801
- checkServerIdentity: pinnedCheckServerIdentity(pinnedSpkiHex)
802
- }
926
+ function buildTlsDispatcher(url, connectOpts) {
927
+ const proxyUrl = selectProxyUrl(url);
928
+ if (proxyUrl) {
929
+ return new ProxyAgent({ uri: proxyUrl, requestTls: connectOpts });
930
+ }
931
+ return new Agent({ connect: connectOpts });
932
+ }
933
+ function buildPinnedDispatcher(pinnedSpkiHex, url) {
934
+ return buildTlsDispatcher(url, {
935
+ ca: effectiveCa(),
936
+ checkServerIdentity: pinnedCheckServerIdentity(pinnedSpkiHex)
803
937
  });
804
938
  }
805
- function buildCapturingDispatcher() {
939
+ function buildCapturingDispatcher(url) {
806
940
  let captured;
807
- const dispatcher = new Agent({
808
- connect: {
809
- ca: effectiveCa(),
810
- checkServerIdentity: (hostname, cert) => {
811
- captured = cert;
812
- const defaultErr = defaultCheckServerIdentity(hostname, cert);
813
- return defaultErr ? taggedCheckError(defaultErr) : void 0;
814
- }
941
+ const dispatcher = buildTlsDispatcher(url, {
942
+ ca: effectiveCa(),
943
+ checkServerIdentity: (hostname, cert) => {
944
+ captured = cert;
945
+ const defaultErr = defaultCheckServerIdentity(hostname, cert);
946
+ return defaultErr ? taggedCheckError(defaultErr) : void 0;
815
947
  }
816
948
  });
817
949
  return { dispatcher, getCapturedCert: () => captured };
818
950
  }
819
- function buildPinnedHttpsAgent(getPinning) {
820
- return new HttpsAgent({
821
- checkServerIdentity: (hostname, cert) => {
822
- const pinning = getPinning();
823
- if (!pinning.enforce) return defaultCheckServerIdentity(hostname, cert);
824
- if (!pinning.pinnedSpkiHex) {
825
- return taggedCheckError(new Error(
826
- `pin-tls is on but no certificate is pinned yet for ${hostname} \u2014 sign in again to establish it (re-pairing required).`
827
- ));
828
- }
829
- return pinnedCheckServerIdentity(pinning.pinnedSpkiHex)(hostname, cert);
830
- },
831
- ca: effectiveCa()
832
- });
951
+ function buildPinnedHttpsAgent(getPinning, asUrl) {
952
+ const checkServerIdentity = (hostname, cert) => {
953
+ const pinning = getPinning();
954
+ if (!pinning.enforce) return defaultCheckServerIdentity(hostname, cert);
955
+ if (!pinning.pinnedSpkiHex) {
956
+ return taggedCheckError(new Error(
957
+ `pin-tls is on but no certificate is pinned yet for ${hostname} \u2014 sign in again to establish it (re-pairing required).`
958
+ ));
959
+ }
960
+ return pinnedCheckServerIdentity(pinning.pinnedSpkiHex)(hostname, cert);
961
+ };
962
+ const proxyUrl = selectProxyUrl(asUrl);
963
+ if (proxyUrl) {
964
+ return new ConnectTunnelHttpsAgent({ proxyUrl, checkServerIdentity, ca: effectiveCa() });
965
+ }
966
+ return new HttpsAgent2({ checkServerIdentity, ca: effectiveCa() });
833
967
  }
834
968
  var DEFAULT_AS_FETCH_TIMEOUT_MS = 15e3;
835
969
  function withBoundedSignal(init) {
@@ -837,6 +971,34 @@ function withBoundedSignal(init) {
837
971
  const signal = init?.signal ? AbortSignal.any([init.signal, timeout]) : timeout;
838
972
  return { ...init, signal };
839
973
  }
974
+ var UNKNOWN_ISSUER_CODES = /* @__PURE__ */ new Set([
975
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
976
+ "UNABLE_TO_GET_ISSUER_CERT",
977
+ "UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
978
+ "SELF_SIGNED_CERT_IN_CHAIN",
979
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
980
+ "CERT_UNTRUSTED"
981
+ ]);
982
+ function tlsErrorCode(err) {
983
+ if (!(err instanceof Error)) return void 0;
984
+ const cause = err.cause;
985
+ const withCode = cause instanceof Error ? cause : err;
986
+ return withCode.code;
987
+ }
988
+ function isUnknownIssuerError(err) {
989
+ const code = tlsErrorCode(err);
990
+ return !!code && UNKNOWN_ISSUER_CODES.has(code);
991
+ }
992
+ function rethrowWithIssuerHint(err, url) {
993
+ if (isUnknownIssuerError(err)) {
994
+ const hinted = new Error(
995
+ `Could not verify the certificate presented for ${url} \u2014 its issuer is not trusted (${tlsErrorCode(err)}). This is the typical signature of a company network that inspects HTTPS traffic with its own root certificate: trust it with \`suveren-gateway config set ca-file <path-to-company-root.pem>\` (or --ca-file / NODE_EXTRA_CA_CERTS), then try again. Original error: ${describeFetchError(err)}`
996
+ );
997
+ hinted.cause = err;
998
+ throw hinted;
999
+ }
1000
+ throw err;
1001
+ }
840
1002
  function closeAfter(dispatcher, pending) {
841
1003
  pending.catch(() => {
842
1004
  }).finally(() => {
@@ -846,8 +1008,24 @@ function closeAfter(dispatcher, pending) {
846
1008
  }
847
1009
  async function fetchAs(url, init, pinning) {
848
1010
  if (!pinning.enforce && !pinning.capture) {
849
- const res2 = await fetch(url, withBoundedSignal(init));
850
- return { res: res2 };
1011
+ const proxyUrl = selectProxyUrl(url);
1012
+ if (!proxyUrl) {
1013
+ try {
1014
+ const res2 = await fetch(url, withBoundedSignal(init));
1015
+ return { res: res2 };
1016
+ } catch (err) {
1017
+ rethrowWithIssuerHint(err, url);
1018
+ }
1019
+ }
1020
+ const dispatcher2 = new ProxyAgent({ uri: proxyUrl, requestTls: { ca: effectiveCa() } });
1021
+ const pending2 = undiciFetch(url, { ...withBoundedSignal(init), dispatcher: dispatcher2 });
1022
+ closeAfter(dispatcher2, pending2);
1023
+ try {
1024
+ const res2 = await pending2;
1025
+ return { res: res2 };
1026
+ } catch (err) {
1027
+ rethrowWithIssuerHint(err, url);
1028
+ }
851
1029
  }
852
1030
  if (pinning.enforce && !pinning.capture && !pinning.pinnedSpkiHex) {
853
1031
  throw new AsTlsMismatchError(
@@ -855,27 +1033,27 @@ async function fetchAs(url, init, pinning) {
855
1033
  );
856
1034
  }
857
1035
  if (pinning.pinnedSpkiHex && (pinning.enforce || !pinning.capture)) {
858
- const dispatcher2 = buildPinnedDispatcher(pinning.pinnedSpkiHex);
1036
+ const dispatcher2 = buildPinnedDispatcher(pinning.pinnedSpkiHex, url);
859
1037
  const pending2 = undiciFetch(url, { ...withBoundedSignal(init), dispatcher: dispatcher2 });
860
1038
  closeAfter(dispatcher2, pending2);
861
1039
  try {
862
1040
  const res2 = await pending2;
863
1041
  return { res: res2 };
864
1042
  } catch (err) {
865
- if (!isPinCheckError(err)) throw err;
1043
+ if (!isPinCheckError(err)) rethrowWithIssuerHint(err, url);
866
1044
  throw new AsTlsMismatchError(
867
- `TLS pin check failed for ${url} \u2014 ${describeFetchError(err)}. Refusing the connection; the API key / session cookie were never sent. If the Authority Server's certificate changed intentionally (a new key, not just renewal), an operator must clear the pairing and sign in again to re-pin it.`
1045
+ `TLS pin check failed for ${url} \u2014 ${describeFetchError(err)}. Refusing the connection; the API key / session cookie were never sent. Two things produce this: (1) the Authority Server's TLS certificate genuinely changed (a new signing key, not just a renewal) \u2014 clear the pairing and sign in again to re-pin it; or (2) a TLS-inspecting proxy on this network is re-signing traffic with its own certificate \u2014 ask network/IT to exempt the Authority Server's host from TLS inspection (pin-tls refusing this, even with a trusted --ca-file, is working as intended).`
868
1046
  );
869
1047
  }
870
1048
  }
871
- const { dispatcher, getCapturedCert } = buildCapturingDispatcher();
1049
+ const { dispatcher, getCapturedCert } = buildCapturingDispatcher(url);
872
1050
  const pending = undiciFetch(url, { ...withBoundedSignal(init), dispatcher });
873
1051
  closeAfter(dispatcher, pending);
874
1052
  let res;
875
1053
  try {
876
1054
  res = await pending;
877
1055
  } catch (err) {
878
- if (!isPinCheckError(err)) throw err;
1056
+ if (!isPinCheckError(err)) rethrowWithIssuerHint(err, url);
879
1057
  throw new AsTlsMismatchError(
880
1058
  `could not verify the Authority Server at ${url} while establishing its TLS pin \u2014 ${describeFetchError(err)}.`
881
1059
  );
@@ -994,18 +1172,237 @@ async function verifyAsHoldsKey(asUrl, publicKeyHex, pinning = { enforce: false,
994
1172
  }
995
1173
 
996
1174
  // src/lib/as-config.ts
997
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
998
- import { dirname as dirname3, join as join4 } from "path";
1175
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
1176
+ import { dirname as dirname3, join as join5 } from "path";
1177
+
1178
+ // src/lib/policy.ts
1179
+ import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
1180
+ import { join as join4 } from "path";
1181
+ import { spawnSync } from "child_process";
1182
+ var POLICY_KEYS = ["AsUrl", "CaFile", "PinTls", "Proxy", "NoProxy", "Simulation", "InstallMethod"];
1183
+ var DEFAULT_REGISTRY_BASE_KEY = "SOFTWARE\\Policies\\Suveren\\Gateway";
1184
+ function registryKeyPath(hive) {
1185
+ const baseKey = process.env.SUVEREN_POLICY_REGISTRY_KEY || DEFAULT_REGISTRY_BASE_KEY;
1186
+ return `${hive}\\${baseKey}`;
1187
+ }
1188
+ function validateAsUrl(candidate) {
1189
+ const trimmed = candidate.trim();
1190
+ if (!trimmed) return { ok: false, error: "The Authority Server URL is empty." };
1191
+ let parsed;
1192
+ try {
1193
+ parsed = new URL(trimmed);
1194
+ } catch {
1195
+ return { ok: false, error: `"${trimmed}" is not a valid URL.` };
1196
+ }
1197
+ const isLocalHttp = parsed.protocol === "http:" && (parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1");
1198
+ if (parsed.protocol !== "https:" && !isLocalHttp) {
1199
+ return {
1200
+ ok: false,
1201
+ error: `"${trimmed}" must use https:// (http:// is only allowed for http://localhost or http://127.0.0.1).`
1202
+ };
1203
+ }
1204
+ let normalized = `${parsed.origin}${parsed.pathname}${parsed.search}${parsed.hash}`;
1205
+ if (normalized.endsWith("/") && normalized !== `${parsed.origin}/`) {
1206
+ normalized = normalized.slice(0, -1);
1207
+ } else if (normalized === `${parsed.origin}/`) {
1208
+ normalized = parsed.origin;
1209
+ }
1210
+ return { ok: true, url: normalized };
1211
+ }
1212
+ function validateCaFile(candidate) {
1213
+ const trimmed = candidate.trim();
1214
+ if (!trimmed) return { ok: false, error: "The CA file path is empty." };
1215
+ if (!existsSync3(trimmed)) return { ok: false, error: `No file at "${trimmed}".` };
1216
+ return { ok: true, path: trimmed };
1217
+ }
1218
+ function validateProxyUrl(candidate) {
1219
+ const trimmed = candidate.trim();
1220
+ if (!trimmed) return { ok: false, error: "The proxy URL is empty." };
1221
+ let parsed;
1222
+ try {
1223
+ parsed = new URL(trimmed);
1224
+ } catch {
1225
+ return { ok: false, error: `"${trimmed}" is not a valid URL.` };
1226
+ }
1227
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
1228
+ return { ok: false, error: `"${trimmed}" must use http:// or https://.` };
1229
+ }
1230
+ return { ok: true, url: trimmed };
1231
+ }
1232
+ function coerceBoolean(value) {
1233
+ if (typeof value === "boolean") return value;
1234
+ if (typeof value === "number") {
1235
+ if (value === 0) return false;
1236
+ if (value === 1) return true;
1237
+ return void 0;
1238
+ }
1239
+ if (typeof value === "string") {
1240
+ const v = value.trim().toLowerCase();
1241
+ if (v === "true" || v === "1") return true;
1242
+ if (v === "false" || v === "0") return false;
1243
+ }
1244
+ return void 0;
1245
+ }
1246
+ function parseRegQueryOutput(stdout) {
1247
+ const out = {};
1248
+ const lineRe = /^ {4}(\S+)\s+(REG_SZ|REG_EXPAND_SZ|REG_DWORD|REG_QWORD|REG_BINARY|REG_MULTI_SZ)\s+(.*)$/;
1249
+ for (const line of stdout.split(/\r?\n/)) {
1250
+ const m = lineRe.exec(line);
1251
+ if (!m) continue;
1252
+ const [, name, type, rawValue] = m;
1253
+ if (!POLICY_KEYS.includes(name)) continue;
1254
+ const key = name;
1255
+ if (type === "REG_DWORD" || type === "REG_QWORD") {
1256
+ out[key] = parseInt(rawValue, 16);
1257
+ } else {
1258
+ out[key] = rawValue;
1259
+ }
1260
+ }
1261
+ return out;
1262
+ }
1263
+ function readRegistryHive(hive) {
1264
+ const path = registryKeyPath(hive);
1265
+ const source = `the registry policy (${path})`;
1266
+ if (process.env.SUVEREN_POLICY_REGISTRY === "off") return { values: {}, source };
1267
+ if (process.platform !== "win32") return { values: {}, source };
1268
+ let result;
1269
+ try {
1270
+ result = spawnSync("reg", ["query", path], { encoding: "utf8" });
1271
+ } catch {
1272
+ return { values: {}, source };
1273
+ }
1274
+ if (!result || result.status !== 0 || !result.stdout) return { values: {}, source };
1275
+ return { values: parseRegQueryOutput(result.stdout), source };
1276
+ }
1277
+ function policyFilePath() {
1278
+ if (process.env.SUVEREN_POLICY_FILE) return process.env.SUVEREN_POLICY_FILE;
1279
+ if (process.platform === "win32") {
1280
+ const programData = process.env.ProgramData || process.env.PROGRAMDATA || "C:\\ProgramData";
1281
+ return join4(programData, "Suveren", "gateway-policy.json");
1282
+ }
1283
+ if (process.platform === "darwin") {
1284
+ return "/Library/Application Support/Suveren/gateway-policy.json";
1285
+ }
1286
+ return "/etc/suveren/gateway-policy.json";
1287
+ }
1288
+ function readPolicyFile() {
1289
+ const path = policyFilePath();
1290
+ const source = `the policy file (${path})`;
1291
+ if (!existsSync3(path)) return { values: {}, source };
1292
+ let data;
1293
+ try {
1294
+ data = JSON.parse(readFileSync5(path, "utf8"));
1295
+ } catch (err) {
1296
+ throw new Error(`Could not parse policy file ${path}: ${err instanceof Error ? err.message : String(err)}`);
1297
+ }
1298
+ const values = {};
1299
+ for (const key of POLICY_KEYS) {
1300
+ if (Object.prototype.hasOwnProperty.call(data, key)) {
1301
+ values[key] = data[key];
1302
+ }
1303
+ }
1304
+ return { values, source };
1305
+ }
1306
+ function mergeBySource(entries) {
1307
+ const merged = {};
1308
+ const sourceOf = {};
1309
+ for (const key of POLICY_KEYS) {
1310
+ for (const entry of entries) {
1311
+ if (Object.prototype.hasOwnProperty.call(entry.values, key) && entry.values[key] !== void 0) {
1312
+ merged[key] = entry.values[key];
1313
+ sourceOf[key] = entry.source;
1314
+ break;
1315
+ }
1316
+ }
1317
+ }
1318
+ return { merged, sourceOf };
1319
+ }
1320
+ var cached = null;
1321
+ function readPolicy() {
1322
+ if (cached) return cached;
1323
+ const hklm = readRegistryHive("HKLM");
1324
+ const hkcu = readRegistryHive("HKCU");
1325
+ const file = readPolicyFile();
1326
+ const { merged, sourceOf } = mergeBySource([hklm, hkcu, file]);
1327
+ const policy = {};
1328
+ const locked = /* @__PURE__ */ new Set();
1329
+ if (merged.AsUrl !== void 0) {
1330
+ const v = validateAsUrl(String(merged.AsUrl));
1331
+ if (!v.ok) {
1332
+ throw new Error(`Invalid policy AsUrl ("${merged.AsUrl}") from ${sourceOf.AsUrl}: ${v.error}`);
1333
+ }
1334
+ policy.asUrl = v.url;
1335
+ locked.add("asUrl");
1336
+ }
1337
+ if (merged.CaFile !== void 0) {
1338
+ const v = validateCaFile(String(merged.CaFile));
1339
+ if (!v.ok) {
1340
+ throw new Error(`Invalid policy CaFile ("${merged.CaFile}") from ${sourceOf.CaFile}: ${v.error}`);
1341
+ }
1342
+ policy.caFile = v.path;
1343
+ locked.add("caFile");
1344
+ }
1345
+ if (merged.PinTls !== void 0) {
1346
+ const b = coerceBoolean(merged.PinTls);
1347
+ if (b === void 0) {
1348
+ throw new Error(
1349
+ `Invalid policy PinTls ("${String(merged.PinTls)}") from ${sourceOf.PinTls}: must be a boolean or 0/1.`
1350
+ );
1351
+ }
1352
+ policy.pinTls = b;
1353
+ locked.add("pinTls");
1354
+ }
1355
+ if (merged.Proxy !== void 0) {
1356
+ const v = validateProxyUrl(String(merged.Proxy));
1357
+ if (!v.ok) {
1358
+ throw new Error(`Invalid policy Proxy ("${String(merged.Proxy)}") from ${sourceOf.Proxy}: ${v.error}`);
1359
+ }
1360
+ policy.proxy = v.url;
1361
+ locked.add("proxy");
1362
+ }
1363
+ if (merged.NoProxy !== void 0) {
1364
+ const raw = String(merged.NoProxy).trim();
1365
+ if (!raw) {
1366
+ throw new Error(`Invalid policy NoProxy ("${String(merged.NoProxy)}") from ${sourceOf.NoProxy}: must not be empty.`);
1367
+ }
1368
+ policy.noProxy = raw;
1369
+ locked.add("noProxy");
1370
+ }
1371
+ if (merged.Simulation !== void 0) {
1372
+ const b = coerceBoolean(merged.Simulation);
1373
+ if (b === void 0) {
1374
+ throw new Error(
1375
+ `Invalid policy Simulation ("${String(merged.Simulation)}") from ${sourceOf.Simulation}: must be a boolean or 0/1.`
1376
+ );
1377
+ }
1378
+ policy.simulation = b;
1379
+ locked.add("simulation");
1380
+ }
1381
+ if (merged.InstallMethod !== void 0) {
1382
+ const raw = String(merged.InstallMethod).trim().toLowerCase();
1383
+ if (raw !== "managed") {
1384
+ throw new Error(
1385
+ `Invalid policy InstallMethod ("${String(merged.InstallMethod)}") from ${sourceOf.InstallMethod}: only "managed" is supported.`
1386
+ );
1387
+ }
1388
+ policy.installMethod = "managed";
1389
+ locked.add("installMethod");
1390
+ }
1391
+ cached = { policy, locked };
1392
+ return cached;
1393
+ }
1394
+
1395
+ // src/lib/as-config.ts
999
1396
  var DEFAULT_AS_URL = "https://www.suveren.ai";
1000
1397
  function configPath(dataDir) {
1001
- return join4(dataDir, "config.json");
1398
+ return join5(dataDir, "config.json");
1002
1399
  }
1003
1400
  function readAsConfigStrict(dataDir) {
1004
1401
  const path = configPath(dataDir);
1005
- if (!existsSync3(path)) return {};
1402
+ if (!existsSync4(path)) return {};
1006
1403
  let data;
1007
1404
  try {
1008
- data = JSON.parse(readFileSync5(path, "utf-8"));
1405
+ data = JSON.parse(readFileSync6(path, "utf-8"));
1009
1406
  } catch (err) {
1010
1407
  throw new Error(
1011
1408
  `Could not parse ${path}: ${err instanceof Error ? err.message : String(err)}`
@@ -1020,9 +1417,9 @@ function readAsConfigStrict(dataDir) {
1020
1417
  }
1021
1418
  function readAsConfig(dataDir) {
1022
1419
  const path = configPath(dataDir);
1023
- if (!existsSync3(path)) return {};
1420
+ if (!existsSync4(path)) return {};
1024
1421
  try {
1025
- const data = JSON.parse(readFileSync5(path, "utf-8"));
1422
+ const data = JSON.parse(readFileSync6(path, "utf-8"));
1026
1423
  const out = {};
1027
1424
  if (typeof data.asUrl === "string") out.asUrl = data.asUrl;
1028
1425
  if (typeof data.caFile === "string") out.caFile = data.caFile;
@@ -1040,7 +1437,7 @@ function writeAsConfig(dataDir, patch) {
1040
1437
  writeFileSync3(path, JSON.stringify(merged, null, 2), { encoding: "utf-8", mode: 384 });
1041
1438
  return merged;
1042
1439
  }
1043
- function validateAsUrl(candidate) {
1440
+ function validateAsUrl2(candidate) {
1044
1441
  const trimmed = candidate.trim();
1045
1442
  if (!trimmed) return { ok: false, error: "The Authority Server URL is empty." };
1046
1443
  let parsed;
@@ -1065,26 +1462,30 @@ function validateAsUrl(candidate) {
1065
1462
  return { ok: true, url: normalized };
1066
1463
  }
1067
1464
  function resolveAsUrl(dataDir, flag) {
1465
+ const policyUrl = readPolicy().policy.asUrl;
1466
+ if (policyUrl) return policyUrl;
1068
1467
  if (flag) {
1069
- const v = validateAsUrl(flag);
1468
+ const v = validateAsUrl2(flag);
1070
1469
  if (!v.ok) throw new Error(`Invalid --as-url: ${v.error}`);
1071
1470
  return v.url;
1072
1471
  }
1073
1472
  const envUrl = process.env.SUVEREN_AS_URL;
1074
1473
  if (envUrl) {
1075
- const v = validateAsUrl(envUrl);
1474
+ const v = validateAsUrl2(envUrl);
1076
1475
  if (!v.ok) throw new Error(`Invalid SUVEREN_AS_URL: ${v.error}`);
1077
1476
  return v.url;
1078
1477
  }
1079
1478
  const saved = readAsConfigStrict(dataDir).asUrl;
1080
1479
  if (saved) {
1081
- const v = validateAsUrl(saved);
1480
+ const v = validateAsUrl2(saved);
1082
1481
  if (!v.ok) throw new Error(`Invalid saved as-url "${saved}" in ${dataDir}/config.json: ${v.error}`);
1083
1482
  return v.url;
1084
1483
  }
1085
1484
  return DEFAULT_AS_URL;
1086
1485
  }
1087
1486
  function resolvePinTls(dataDir) {
1487
+ const policyPinTls = readPolicy().policy.pinTls;
1488
+ if (policyPinTls !== void 0) return policyPinTls;
1088
1489
  return readAsConfig(dataDir).pinTls === true;
1089
1490
  }
1090
1491
  function validatePinTlsForUrl(pinTls, asUrl) {
@@ -1107,7 +1508,7 @@ function clearPinTlsExpectedFingerprint(dataDir) {
1107
1508
  var DEFAULT_SP_URL = process.env.SUVEREN_AS_URL ?? "https://www.suveren.ai";
1108
1509
  async function fetchAsPublicKey(asUrl) {
1109
1510
  try {
1110
- const res = await fetch(`${asUrl}/api/as/pubkey`, { signal: AbortSignal.timeout(5e3) });
1511
+ const { res } = await fetchAs(`${asUrl}/api/as/pubkey`, { signal: AbortSignal.timeout(5e3) }, { enforce: false });
1111
1512
  if (!res.ok) {
1112
1513
  return {
1113
1514
  ok: false,
@@ -1465,6 +1866,21 @@ function credentialView(cred, textFields) {
1465
1866
  };
1466
1867
  }
1467
1868
 
1869
+ // src/lib/proxied-fetch.ts
1870
+ import { ProxyAgent as ProxyAgent2, fetch as undiciFetch2 } from "undici";
1871
+ function proxiedFetch(url, init) {
1872
+ const proxyUrl = selectProxyUrl(url);
1873
+ if (!proxyUrl) return fetch(url, init);
1874
+ const dispatcher = new ProxyAgent2({ uri: proxyUrl });
1875
+ const pending = undiciFetch2(url, { ...init, dispatcher });
1876
+ pending.catch(() => {
1877
+ }).finally(() => {
1878
+ void dispatcher.close().catch(() => {
1879
+ });
1880
+ });
1881
+ return pending;
1882
+ }
1883
+
1468
1884
  // src/routes/vault.ts
1469
1885
  var manifestCache = null;
1470
1886
  async function cachedManifests() {
@@ -1573,12 +1989,12 @@ function createVaultRouter(vault2) {
1573
1989
  const headers = {};
1574
1990
  if (cred.apiKey) headers["Authorization"] = `Bearer ${cred.apiKey}`;
1575
1991
  if (cred.provider === "ollama") {
1576
- const r = await fetch(`${cred.endpoint}/api/tags`, {
1992
+ const r = await proxiedFetch(`${cred.endpoint}/api/tags`, {
1577
1993
  signal: AbortSignal.timeout(3e3)
1578
1994
  });
1579
1995
  if (!r.ok) throw new Error(`Ollama: ${r.status}`);
1580
1996
  } else {
1581
- const r = await fetch(`${cred.endpoint}/models`, {
1997
+ const r = await proxiedFetch(`${cred.endpoint}/models`, {
1582
1998
  headers,
1583
1999
  signal: AbortSignal.timeout(3e3)
1584
2000
  });
@@ -1697,7 +2113,7 @@ ${contextParts.join("\n")}` : "",
1697
2113
  try {
1698
2114
  let suggestion;
1699
2115
  if (config.provider === "ollama") {
1700
- const response = await fetch(`${config.endpoint}/api/chat`, {
2116
+ const response = await proxiedFetch(`${config.endpoint}/api/chat`, {
1701
2117
  method: "POST",
1702
2118
  headers: { "Content-Type": "application/json" },
1703
2119
  body: JSON.stringify({
@@ -1714,7 +2130,7 @@ ${contextParts.join("\n")}` : "",
1714
2130
  } else {
1715
2131
  const headers = { "Content-Type": "application/json" };
1716
2132
  if (config.apiKey) headers["Authorization"] = `Bearer ${config.apiKey}`;
1717
- const response = await fetch(`${config.endpoint}/chat/completions`, {
2133
+ const response = await proxiedFetch(`${config.endpoint}/chat/completions`, {
1718
2134
  method: "POST",
1719
2135
  headers,
1720
2136
  body: JSON.stringify({
@@ -1827,7 +2243,7 @@ async function getAIChatResponse(config, request) {
1827
2243
  try {
1828
2244
  let reply;
1829
2245
  if (config.provider === "ollama") {
1830
- const response = await fetch(`${config.endpoint}/api/chat`, {
2246
+ const response = await proxiedFetch(`${config.endpoint}/api/chat`, {
1831
2247
  method: "POST",
1832
2248
  headers: { "Content-Type": "application/json" },
1833
2249
  body: JSON.stringify({
@@ -1851,7 +2267,7 @@ async function getAIChatResponse(config, request) {
1851
2267
  } else {
1852
2268
  const headers = { "Content-Type": "application/json" };
1853
2269
  if (config.apiKey) headers["Authorization"] = `Bearer ${config.apiKey}`;
1854
- const response = await fetch(`${config.endpoint}/chat/completions`, {
2270
+ const response = await proxiedFetch(`${config.endpoint}/chat/completions`, {
1855
2271
  method: "POST",
1856
2272
  headers,
1857
2273
  body: JSON.stringify({
@@ -1889,7 +2305,7 @@ async function getAIChatResponse(config, request) {
1889
2305
  }
1890
2306
  async function callModel(config, messages, opts) {
1891
2307
  if (config.provider === "ollama") {
1892
- const response2 = await fetch(`${config.endpoint}/api/chat`, {
2308
+ const response2 = await proxiedFetch(`${config.endpoint}/api/chat`, {
1893
2309
  method: "POST",
1894
2310
  headers: { "Content-Type": "application/json" },
1895
2311
  body: JSON.stringify({
@@ -1906,7 +2322,7 @@ async function callModel(config, messages, opts) {
1906
2322
  }
1907
2323
  const headers = { "Content-Type": "application/json" };
1908
2324
  if (config.apiKey) headers["Authorization"] = `Bearer ${config.apiKey}`;
1909
- const response = await fetch(`${config.endpoint}/chat/completions`, {
2325
+ const response = await proxiedFetch(`${config.endpoint}/chat/completions`, {
1910
2326
  method: "POST",
1911
2327
  headers,
1912
2328
  body: JSON.stringify({ model: config.model, messages, temperature: opts.temperature, max_tokens: opts.maxTokens }),
@@ -1968,7 +2384,7 @@ async function getIntentReview(config, request) {
1968
2384
  async function listAIModels(config) {
1969
2385
  try {
1970
2386
  if (config.provider === "ollama") {
1971
- const res2 = await fetch(`${config.endpoint}/api/tags`, {
2387
+ const res2 = await proxiedFetch(`${config.endpoint}/api/tags`, {
1972
2388
  signal: AbortSignal.timeout(5e3)
1973
2389
  });
1974
2390
  if (!res2.ok) throw new Error(`Ollama: ${res2.status}`);
@@ -1978,7 +2394,7 @@ async function listAIModels(config) {
1978
2394
  }
1979
2395
  const headers = {};
1980
2396
  if (config.apiKey) headers["Authorization"] = `Bearer ${config.apiKey}`;
1981
- const res = await fetch(`${config.endpoint}/models`, {
2397
+ const res = await proxiedFetch(`${config.endpoint}/models`, {
1982
2398
  headers,
1983
2399
  signal: AbortSignal.timeout(8e3)
1984
2400
  });
@@ -2000,7 +2416,7 @@ async function listAIModels(config) {
2000
2416
  async function testAIConnectivity(config) {
2001
2417
  try {
2002
2418
  if (config.provider === "ollama") {
2003
- const res = await fetch(`${config.endpoint}/api/tags`, {
2419
+ const res = await proxiedFetch(`${config.endpoint}/api/tags`, {
2004
2420
  signal: AbortSignal.timeout(3e3)
2005
2421
  });
2006
2422
  if (!res.ok) throw new Error(`Ollama: ${res.status}`);
@@ -2008,7 +2424,7 @@ async function testAIConnectivity(config) {
2008
2424
  } else {
2009
2425
  const headers = {};
2010
2426
  if (config.apiKey) headers["Authorization"] = `Bearer ${config.apiKey}`;
2011
- const res = await fetch(`${config.endpoint}/models`, {
2427
+ const res = await proxiedFetch(`${config.endpoint}/models`, {
2012
2428
  headers,
2013
2429
  signal: AbortSignal.timeout(3e3)
2014
2430
  });
@@ -2021,18 +2437,18 @@ async function testAIConnectivity(config) {
2021
2437
  }
2022
2438
 
2023
2439
  // src/lib/agent-brief-store.ts
2024
- import { existsSync as existsSync4, readFileSync as readFileSync6 } from "fs";
2440
+ import { existsSync as existsSync5, readFileSync as readFileSync7 } from "fs";
2025
2441
  import { homedir as homedir2 } from "os";
2026
- import { join as join5 } from "path";
2442
+ import { join as join6 } from "path";
2027
2443
  var AGENT_CONTEXT_MAX_BYTES = 16 * 1024;
2028
2444
  function agentBriefPath() {
2029
- const dataDir = process.env.SUVEREN_DATA_DIR ?? join5(homedir2(), ".suveren");
2030
- return join5(dataDir, "context.md");
2445
+ const dataDir = process.env.SUVEREN_DATA_DIR ?? join6(homedir2(), ".suveren");
2446
+ return join6(dataDir, "context.md");
2031
2447
  }
2032
2448
  function readAgentBrief() {
2033
2449
  const filePath = agentBriefPath();
2034
- if (!existsSync4(filePath)) return "";
2035
- return readFileSync6(filePath, "utf-8");
2450
+ if (!existsSync5(filePath)) return "";
2451
+ return readFileSync7(filePath, "utf-8");
2036
2452
  }
2037
2453
 
2038
2454
  // src/routes/ai.ts
@@ -2396,7 +2812,7 @@ function createMCPRouter() {
2396
2812
  // src/routes/autostart.ts
2397
2813
  import { Router as Router6 } from "express";
2398
2814
  import { execFile } from "child_process";
2399
- import { existsSync as existsSync5 } from "fs";
2815
+ import { existsSync as existsSync6 } from "fs";
2400
2816
  import { dirname as pathDirname, resolve } from "path";
2401
2817
  import { fileURLToPath } from "url";
2402
2818
  import { promisify } from "util";
@@ -2417,7 +2833,7 @@ function findCli(dirname9 = MODULE_DIR) {
2417
2833
  // resolves differently between a shell and a login service — and it would
2418
2834
  // mask a genuinely missing CLI whenever the cwd happened to be the repo.
2419
2835
  ];
2420
- return candidates.find(existsSync5) ?? null;
2836
+ return candidates.find(existsSync6) ?? null;
2421
2837
  }
2422
2838
  async function cli(args) {
2423
2839
  const bin = findCli();
@@ -2495,18 +2911,18 @@ function createAutostartRouter() {
2495
2911
  }
2496
2912
 
2497
2913
  // src/lib/desktop-notify.ts
2498
- import { spawn, spawnSync } from "child_process";
2914
+ import { spawn, spawnSync as spawnSync2 } from "child_process";
2499
2915
 
2500
2916
  // src/lib/notifier-icon.ts
2501
- import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, existsSync as existsSync6 } from "fs";
2917
+ import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, existsSync as existsSync7 } from "fs";
2502
2918
  import { homedir as homedir3 } from "os";
2503
- import { join as join6, dirname as dirname4 } from "path";
2919
+ import { join as join7, dirname as dirname4 } from "path";
2504
2920
  var ICON_BASE64 = "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";
2505
2921
  function ensureNotifierIcon(dataDir) {
2506
2922
  try {
2507
- const dir = dataDir ?? process.env.SUVEREN_DATA_DIR ?? join6(homedir3(), ".suveren");
2508
- const path = join6(dir, "notifier-icon.png");
2509
- if (!existsSync6(path)) {
2923
+ const dir = dataDir ?? process.env.SUVEREN_DATA_DIR ?? join7(homedir3(), ".suveren");
2924
+ const path = join7(dir, "notifier-icon.png");
2925
+ if (!existsSync7(path)) {
2510
2926
  mkdirSync4(dirname4(path), { recursive: true });
2511
2927
  writeFileSync4(path, Buffer.from(ICON_BASE64, "base64"));
2512
2928
  }
@@ -2600,7 +3016,7 @@ var terminalNotifierCache = null;
2600
3016
  function hasTerminalNotifier() {
2601
3017
  if (terminalNotifierCache !== null) return terminalNotifierCache;
2602
3018
  try {
2603
- const probe = spawnSync("terminal-notifier", ["-help"], { stdio: "ignore" });
3019
+ const probe = spawnSync2("terminal-notifier", ["-help"], { stdio: "ignore" });
2604
3020
  terminalNotifierCache = probe.error === void 0;
2605
3021
  } catch {
2606
3022
  terminalNotifierCache = false;
@@ -2710,17 +3126,17 @@ import { Router as Router9 } from "express";
2710
3126
 
2711
3127
  // src/routes/approved-intents.ts
2712
3128
  import { Router as Router8 } from "express";
2713
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync7, mkdirSync as mkdirSync5 } from "fs";
3129
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync5, existsSync as existsSync8, mkdirSync as mkdirSync5 } from "fs";
2714
3130
  import { homedir as homedir4 } from "os";
2715
- import { join as join7 } from "path";
2716
- var SUVEREN_DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join7(homedir4(), ".suveren");
2717
- var FILE_PATH = join7(SUVEREN_DATA_DIR, "approved-intents.enc.json");
3131
+ import { join as join8 } from "path";
3132
+ var SUVEREN_DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join8(homedir4(), ".suveren");
3133
+ var FILE_PATH = join8(SUVEREN_DATA_DIR, "approved-intents.enc.json");
2718
3134
  function readFile() {
2719
- if (!existsSync7(FILE_PATH)) {
3135
+ if (!existsSync8(FILE_PATH)) {
2720
3136
  return { version: 1, entries: {} };
2721
3137
  }
2722
3138
  try {
2723
- return JSON.parse(readFileSync7(FILE_PATH, "utf-8"));
3139
+ return JSON.parse(readFileSync8(FILE_PATH, "utf-8"));
2724
3140
  } catch {
2725
3141
  return { version: 1, entries: {} };
2726
3142
  }
@@ -3025,12 +3441,12 @@ async function forceCheck() {
3025
3441
  }
3026
3442
  }
3027
3443
  async function ghcrToken() {
3028
- const res = await fetch(`https://ghcr.io/token?scope=repository:${GHCR_IMAGE}:pull`);
3444
+ const res = await proxiedFetch(`https://ghcr.io/token?scope=repository:${GHCR_IMAGE}:pull`);
3029
3445
  const data = await res.json();
3030
3446
  return data.token;
3031
3447
  }
3032
3448
  async function ghcrDigest(tag, token) {
3033
- const res = await fetch(`https://ghcr.io/v2/${GHCR_IMAGE}/manifests/${tag}`, {
3449
+ const res = await proxiedFetch(`https://ghcr.io/v2/${GHCR_IMAGE}/manifests/${tag}`, {
3034
3450
  method: "HEAD",
3035
3451
  headers: {
3036
3452
  Authorization: `Bearer ${token}`,
@@ -3050,7 +3466,7 @@ async function checkDocker() {
3050
3466
  lastCheckedAt = Date.now();
3051
3467
  }
3052
3468
  async function checkNpm() {
3053
- const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(NPM_PACKAGE)}/latest`);
3469
+ const res = await proxiedFetch(`https://registry.npmjs.org/${encodeURIComponent(NPM_PACKAGE)}/latest`);
3054
3470
  if (!res.ok) return;
3055
3471
  const data = await res.json();
3056
3472
  latestVersion = data.version;
@@ -3093,7 +3509,7 @@ async function check() {
3093
3509
  checkDev();
3094
3510
  return;
3095
3511
  }
3096
- if (installMethod === "npm") {
3512
+ if (installMethod === "npm" || installMethod === "managed") {
3097
3513
  await checkNpm();
3098
3514
  return;
3099
3515
  }
@@ -3143,19 +3559,19 @@ data: ${data}
3143
3559
  import { Router as Router12 } from "express";
3144
3560
 
3145
3561
  // src/lib/gateway-settings.ts
3146
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync6 } from "fs";
3562
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync6, mkdirSync as mkdirSync6 } from "fs";
3147
3563
  import { homedir as homedir5 } from "os";
3148
- import { join as join8, dirname as dirname6 } from "path";
3564
+ import { join as join9, dirname as dirname6 } from "path";
3149
3565
  var DEFAULT_SETTINGS = {
3150
3566
  desktopNotifications: true
3151
3567
  };
3152
3568
  function settingsPath(dataDir) {
3153
- const dir = dataDir ?? process.env.SUVEREN_DATA_DIR ?? join8(homedir5(), ".suveren");
3154
- return join8(dir, "settings.json");
3569
+ const dir = dataDir ?? process.env.SUVEREN_DATA_DIR ?? join9(homedir5(), ".suveren");
3570
+ return join9(dir, "settings.json");
3155
3571
  }
3156
3572
  function readSettings(dataDir) {
3157
3573
  try {
3158
- const raw = JSON.parse(readFileSync8(settingsPath(dataDir), "utf8"));
3574
+ const raw = JSON.parse(readFileSync9(settingsPath(dataDir), "utf8"));
3159
3575
  return {
3160
3576
  desktopNotifications: typeof raw.desktopNotifications === "boolean" ? raw.desktopNotifications : DEFAULT_SETTINGS.desktopNotifications
3161
3577
  };
@@ -3202,20 +3618,20 @@ function createGatewaySettingsRouter() {
3202
3618
  import { Router as Router13 } from "express";
3203
3619
 
3204
3620
  // src/lib/archived-mandates-store.ts
3205
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, mkdirSync as mkdirSync7, renameSync } from "fs";
3621
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync7, mkdirSync as mkdirSync7, renameSync } from "fs";
3206
3622
  import { homedir as homedir6 } from "os";
3207
- import { join as join9, dirname as dirname7 } from "path";
3623
+ import { join as join10, dirname as dirname7 } from "path";
3208
3624
  var ID_PATTERN = /^[A-Za-z0-9_.:-]{1,200}$/;
3209
3625
  function isValidMandateId(id) {
3210
3626
  return typeof id === "string" && ID_PATTERN.test(id);
3211
3627
  }
3212
3628
  function storePath(dataDir) {
3213
- const dir = dataDir ?? process.env.SUVEREN_DATA_DIR ?? join9(homedir6(), ".suveren");
3214
- return join9(dir, "archived-mandates.json");
3629
+ const dir = dataDir ?? process.env.SUVEREN_DATA_DIR ?? join10(homedir6(), ".suveren");
3630
+ return join10(dir, "archived-mandates.json");
3215
3631
  }
3216
3632
  function readArchived(dataDir) {
3217
3633
  try {
3218
- const raw = JSON.parse(readFileSync9(storePath(dataDir), "utf8"));
3634
+ const raw = JSON.parse(readFileSync10(storePath(dataDir), "utf8"));
3219
3635
  if (!Array.isArray(raw.archived)) return [];
3220
3636
  return [...new Set(raw.archived.filter(isValidMandateId))];
3221
3637
  } catch {
@@ -3521,17 +3937,17 @@ function buildSessionHealth(vault2) {
3521
3937
  }
3522
3938
 
3523
3939
  // src/lib/denials-reader.ts
3524
- import { existsSync as existsSync8, readFileSync as readFileSync10 } from "fs";
3525
- import { join as join10 } from "path";
3940
+ import { existsSync as existsSync9, readFileSync as readFileSync11 } from "fs";
3941
+ import { join as join11 } from "path";
3526
3942
  function loadDenials(dataDir, decrypt) {
3527
- const encPath = join10(dataDir, "denials.enc.json");
3528
- const plainPath = join10(dataDir, "denials.json");
3529
- if (existsSync8(encPath)) {
3530
- const { blob } = JSON.parse(readFileSync10(encPath, "utf-8"));
3943
+ const encPath = join11(dataDir, "denials.enc.json");
3944
+ const plainPath = join11(dataDir, "denials.json");
3945
+ if (existsSync9(encPath)) {
3946
+ const { blob } = JSON.parse(readFileSync11(encPath, "utf-8"));
3531
3947
  return JSON.parse(decrypt(blob)).records ?? [];
3532
3948
  }
3533
- if (existsSync8(plainPath)) {
3534
- return JSON.parse(readFileSync10(plainPath, "utf-8")).records ?? [];
3949
+ if (existsSync9(plainPath)) {
3950
+ return JSON.parse(readFileSync11(plainPath, "utf-8")).records ?? [];
3535
3951
  }
3536
3952
  return [];
3537
3953
  }
@@ -3547,15 +3963,17 @@ function selectDenials(records, opts = {}) {
3547
3963
 
3548
3964
  // src/lib/simulation-mode.ts
3549
3965
  function isSimulationMode() {
3966
+ const policySimulation = readPolicy().policy.simulation;
3967
+ if (policySimulation !== void 0) return policySimulation;
3550
3968
  return process.env.SUVEREN_SIMULATION === "1";
3551
3969
  }
3552
3970
 
3553
3971
  // src/index.ts
3554
3972
  (function migrateDataDir() {
3555
3973
  if (process.env.SUVEREN_DATA_DIR) return;
3556
- const oldDir = join11(homedir7(), ".hap");
3557
- const newDir = join11(homedir7(), ".suveren");
3558
- if (!existsSync9(newDir) && existsSync9(oldDir)) {
3974
+ const oldDir = join12(homedir7(), ".hap");
3975
+ const newDir = join12(homedir7(), ".suveren");
3976
+ if (!existsSync10(newDir) && existsSync10(oldDir)) {
3559
3977
  try {
3560
3978
  renameSync2(oldDir, newDir);
3561
3979
  console.error(`[Control Plane] Migrated data directory: ${oldDir} \u2192 ${newDir}`);
@@ -3565,12 +3983,14 @@ function isSimulationMode() {
3565
3983
  }
3566
3984
  }
3567
3985
  })();
3568
- var DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join11(homedir7(), ".suveren");
3986
+ var DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join12(homedir7(), ".suveren");
3569
3987
  var SP_URL = resolveAsUrl(DATA_DIR);
3570
3988
  validatePinTlsForUrl(resolvePinTls(DATA_DIR), SP_URL);
3989
+ var POLICY_LOCKED = [...readPolicy().locked];
3990
+ var POLICY_LOCKED_INSTALL_METHOD = readPolicy().policy.installMethod;
3571
3991
  var port = parseInt(process.env.SUVEREN_CP_PORT ?? "3402", 10);
3572
3992
  var HAP_MODE = process.env.HAP_MODE ?? "personal";
3573
- var UI_DIST = process.env.HAP_UI_DIST ?? join11(import.meta.dirname ?? __dirname, "../../ui/dist");
3993
+ var UI_DIST = process.env.HAP_UI_DIST ?? join12(import.meta.dirname ?? __dirname, "../../ui/dist");
3574
3994
  var vault = new Vault(DATA_DIR);
3575
3995
  (function repairIfAsUrlChanged() {
3576
3996
  const pairing = readPairing(DATA_DIR);
@@ -3984,12 +4404,17 @@ app.get("/as-pairing", authGuard, (_req, res) => {
3984
4404
  // key (formatPinFingerprint mirrors fingerprintOf's grouping), applied
3985
4405
  // to the already-computed SPKI digest rather than re-hashing a key.
3986
4406
  pinTlsEnabled: resolvePinTls(DATA_DIR),
3987
- tlsPinFingerprint: paired && pairing.tlsSpkiPinHex ? formatPinFingerprint(pairing.tlsSpkiPinHex) : null
4407
+ tlsPinFingerprint: paired && pairing.tlsSpkiPinHex ? formatPinFingerprint(pairing.tlsSpkiPinHex) : null,
4408
+ // IT policy (see lib/policy.ts) — read-only flags so Settings can show
4409
+ // "set by your IT" instead of the "change it via the CLI" hint, which
4410
+ // would be actively misleading for a locked value.
4411
+ asUrlLockedByPolicy: POLICY_LOCKED.includes("asUrl"),
4412
+ pinTlsLockedByPolicy: POLICY_LOCKED.includes("pinTls")
3988
4413
  });
3989
4414
  });
3990
4415
  app.get("/denials", authGuard, (req, res) => {
3991
4416
  try {
3992
- const dataDir = process.env.SUVEREN_DATA_DIR ?? join11(homedir7(), ".suveren");
4417
+ const dataDir = process.env.SUVEREN_DATA_DIR ?? join12(homedir7(), ".suveren");
3993
4418
  const all = loadDenials(dataDir, (blob) => vault.decrypt(blob));
3994
4419
  const since = Number(req.query.since);
3995
4420
  const limit = Number(req.query.limit);
@@ -4059,7 +4484,7 @@ var pinnedProxyAgent = SP_URL.startsWith("https:") ? buildPinnedHttpsAgent(() =>
4059
4484
  enforce: resolvePinTls(DATA_DIR),
4060
4485
  pinnedSpkiHex: pin && pin.asUrl === SP_URL ? pin.tlsSpkiPinHex : void 0
4061
4486
  };
4062
- }) : void 0;
4487
+ }, SP_URL) : void 0;
4063
4488
  app.use(
4064
4489
  createProxyMiddleware({
4065
4490
  target: SP_URL,
@@ -4120,12 +4545,13 @@ app.use(
4120
4545
  })
4121
4546
  );
4122
4547
  function detectInstallMethod() {
4123
- if (existsSync9("/.dockerenv")) return "docker";
4548
+ if (process.env.SUVEREN_INSTALL_METHOD === "managed" || POLICY_LOCKED_INSTALL_METHOD === "managed") return "managed";
4549
+ if (existsSync10("/.dockerenv")) return "docker";
4124
4550
  const dir = import.meta.dirname ?? __dirname;
4125
4551
  if (dir.includes("/node_modules/@suveren/gateway/")) return "npm";
4126
4552
  let cursor = dir;
4127
4553
  for (let i = 0; i < 8; i++) {
4128
- if (existsSync9(join11(cursor, ".git"))) return "dev";
4554
+ if (existsSync10(join12(cursor, ".git"))) return "dev";
4129
4555
  const parent = dirname8(cursor);
4130
4556
  if (parent === cursor) break;
4131
4557
  cursor = parent;
@@ -4138,10 +4564,10 @@ function detectRunningVersion() {
4138
4564
  return process.env.HAP_BUILD_SHA ?? "dev";
4139
4565
  }
4140
4566
  const dir = import.meta.dirname ?? __dirname;
4141
- const bundlePkg = join11(dir, "..", "..", "package.json");
4142
- if (existsSync9(bundlePkg)) {
4567
+ const bundlePkg = join12(dir, "..", "..", "package.json");
4568
+ if (existsSync10(bundlePkg)) {
4143
4569
  try {
4144
- const pkg = JSON.parse(readFileSync11(bundlePkg, "utf8"));
4570
+ const pkg = JSON.parse(readFileSync12(bundlePkg, "utf8"));
4145
4571
  if (pkg.name === "@suveren/gateway" && typeof pkg.version === "string") {
4146
4572
  return pkg.version;
4147
4573
  }
@@ -4167,13 +4593,20 @@ app.get("/health", async (req, res) => {
4167
4593
  // Purely local — the Authority Server never learns this. UI reads it to
4168
4594
  // show a gateway-wide banner ("real systems are blocked").
4169
4595
  simulation: isSimulationMode(),
4596
+ // Settings IT has locked centrally (Windows registry policy, or a
4597
+ // policy file — see lib/policy.ts). Read-only here: there is no API
4598
+ // route that lets the UI change any of these (as-url/ca-file/pin-tls
4599
+ // are CLI-only; simulation has no UI control either), so this exists
4600
+ // purely so Settings can render "set by your IT" instead of controls
4601
+ // that would silently do nothing.
4602
+ policyLocked: POLICY_LOCKED,
4170
4603
  session: buildSessionHealth(vault),
4171
4604
  security: {
4172
4605
  note: "Gateway secures tool execution. Agent host isolation is the user's responsibility."
4173
4606
  }
4174
4607
  });
4175
4608
  });
4176
- if (existsSync9(UI_DIST)) {
4609
+ if (existsSync10(UI_DIST)) {
4177
4610
  app.use(express.static(UI_DIST, {
4178
4611
  setHeaders: (res, filePath) => {
4179
4612
  if (filePath.endsWith("index.html")) {
@@ -4183,7 +4616,7 @@ if (existsSync9(UI_DIST)) {
4183
4616
  }));
4184
4617
  app.get("*", (_req, res) => {
4185
4618
  res.setHeader("Cache-Control", "no-store, must-revalidate");
4186
- res.sendFile(join11(UI_DIST, "index.html"));
4619
+ res.sendFile(join12(UI_DIST, "index.html"));
4187
4620
  });
4188
4621
  } else {
4189
4622
  app.get("/", (_req, res) => {