@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.
@@ -142,18 +142,237 @@ function fingerprintOf(publicKeyHex) {
142
142
  }
143
143
 
144
144
  // src/lib/as-config.ts
145
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
146
- import { dirname as dirname2, join as join3 } from "path";
145
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
146
+ import { dirname as dirname2, join as join4 } from "path";
147
+
148
+ // src/lib/policy.ts
149
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
150
+ import { join as join3 } from "path";
151
+ import { spawnSync } from "child_process";
152
+ var POLICY_KEYS = ["AsUrl", "CaFile", "PinTls", "Proxy", "NoProxy", "Simulation", "InstallMethod"];
153
+ var DEFAULT_REGISTRY_BASE_KEY = "SOFTWARE\\Policies\\Suveren\\Gateway";
154
+ function registryKeyPath(hive) {
155
+ const baseKey = process.env.SUVEREN_POLICY_REGISTRY_KEY || DEFAULT_REGISTRY_BASE_KEY;
156
+ return `${hive}\\${baseKey}`;
157
+ }
158
+ function validateAsUrl(candidate) {
159
+ const trimmed = candidate.trim();
160
+ if (!trimmed) return { ok: false, error: "The Authority Server URL is empty." };
161
+ let parsed;
162
+ try {
163
+ parsed = new URL(trimmed);
164
+ } catch {
165
+ return { ok: false, error: `"${trimmed}" is not a valid URL.` };
166
+ }
167
+ const isLocalHttp = parsed.protocol === "http:" && (parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1");
168
+ if (parsed.protocol !== "https:" && !isLocalHttp) {
169
+ return {
170
+ ok: false,
171
+ error: `"${trimmed}" must use https:// (http:// is only allowed for http://localhost or http://127.0.0.1).`
172
+ };
173
+ }
174
+ let normalized = `${parsed.origin}${parsed.pathname}${parsed.search}${parsed.hash}`;
175
+ if (normalized.endsWith("/") && normalized !== `${parsed.origin}/`) {
176
+ normalized = normalized.slice(0, -1);
177
+ } else if (normalized === `${parsed.origin}/`) {
178
+ normalized = parsed.origin;
179
+ }
180
+ return { ok: true, url: normalized };
181
+ }
182
+ function validateCaFile(candidate) {
183
+ const trimmed = candidate.trim();
184
+ if (!trimmed) return { ok: false, error: "The CA file path is empty." };
185
+ if (!existsSync3(trimmed)) return { ok: false, error: `No file at "${trimmed}".` };
186
+ return { ok: true, path: trimmed };
187
+ }
188
+ function validateProxyUrl(candidate) {
189
+ const trimmed = candidate.trim();
190
+ if (!trimmed) return { ok: false, error: "The proxy URL is empty." };
191
+ let parsed;
192
+ try {
193
+ parsed = new URL(trimmed);
194
+ } catch {
195
+ return { ok: false, error: `"${trimmed}" is not a valid URL.` };
196
+ }
197
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
198
+ return { ok: false, error: `"${trimmed}" must use http:// or https://.` };
199
+ }
200
+ return { ok: true, url: trimmed };
201
+ }
202
+ function coerceBoolean(value) {
203
+ if (typeof value === "boolean") return value;
204
+ if (typeof value === "number") {
205
+ if (value === 0) return false;
206
+ if (value === 1) return true;
207
+ return void 0;
208
+ }
209
+ if (typeof value === "string") {
210
+ const v = value.trim().toLowerCase();
211
+ if (v === "true" || v === "1") return true;
212
+ if (v === "false" || v === "0") return false;
213
+ }
214
+ return void 0;
215
+ }
216
+ function parseRegQueryOutput(stdout) {
217
+ const out = {};
218
+ const lineRe = /^ {4}(\S+)\s+(REG_SZ|REG_EXPAND_SZ|REG_DWORD|REG_QWORD|REG_BINARY|REG_MULTI_SZ)\s+(.*)$/;
219
+ for (const line of stdout.split(/\r?\n/)) {
220
+ const m = lineRe.exec(line);
221
+ if (!m) continue;
222
+ const [, name, type, rawValue] = m;
223
+ if (!POLICY_KEYS.includes(name)) continue;
224
+ const key = name;
225
+ if (type === "REG_DWORD" || type === "REG_QWORD") {
226
+ out[key] = parseInt(rawValue, 16);
227
+ } else {
228
+ out[key] = rawValue;
229
+ }
230
+ }
231
+ return out;
232
+ }
233
+ function readRegistryHive(hive) {
234
+ const path = registryKeyPath(hive);
235
+ const source = `the registry policy (${path})`;
236
+ if (process.env.SUVEREN_POLICY_REGISTRY === "off") return { values: {}, source };
237
+ if (process.platform !== "win32") return { values: {}, source };
238
+ let result;
239
+ try {
240
+ result = spawnSync("reg", ["query", path], { encoding: "utf8" });
241
+ } catch {
242
+ return { values: {}, source };
243
+ }
244
+ if (!result || result.status !== 0 || !result.stdout) return { values: {}, source };
245
+ return { values: parseRegQueryOutput(result.stdout), source };
246
+ }
247
+ function policyFilePath() {
248
+ if (process.env.SUVEREN_POLICY_FILE) return process.env.SUVEREN_POLICY_FILE;
249
+ if (process.platform === "win32") {
250
+ const programData = process.env.ProgramData || process.env.PROGRAMDATA || "C:\\ProgramData";
251
+ return join3(programData, "Suveren", "gateway-policy.json");
252
+ }
253
+ if (process.platform === "darwin") {
254
+ return "/Library/Application Support/Suveren/gateway-policy.json";
255
+ }
256
+ return "/etc/suveren/gateway-policy.json";
257
+ }
258
+ function readPolicyFile() {
259
+ const path = policyFilePath();
260
+ const source = `the policy file (${path})`;
261
+ if (!existsSync3(path)) return { values: {}, source };
262
+ let data;
263
+ try {
264
+ data = JSON.parse(readFileSync3(path, "utf8"));
265
+ } catch (err) {
266
+ throw new Error(`Could not parse policy file ${path}: ${err instanceof Error ? err.message : String(err)}`);
267
+ }
268
+ const values = {};
269
+ for (const key of POLICY_KEYS) {
270
+ if (Object.prototype.hasOwnProperty.call(data, key)) {
271
+ values[key] = data[key];
272
+ }
273
+ }
274
+ return { values, source };
275
+ }
276
+ function mergeBySource(entries) {
277
+ const merged = {};
278
+ const sourceOf = {};
279
+ for (const key of POLICY_KEYS) {
280
+ for (const entry of entries) {
281
+ if (Object.prototype.hasOwnProperty.call(entry.values, key) && entry.values[key] !== void 0) {
282
+ merged[key] = entry.values[key];
283
+ sourceOf[key] = entry.source;
284
+ break;
285
+ }
286
+ }
287
+ }
288
+ return { merged, sourceOf };
289
+ }
290
+ var cached = null;
291
+ function readPolicy() {
292
+ if (cached) return cached;
293
+ const hklm = readRegistryHive("HKLM");
294
+ const hkcu = readRegistryHive("HKCU");
295
+ const file = readPolicyFile();
296
+ const { merged, sourceOf } = mergeBySource([hklm, hkcu, file]);
297
+ const policy = {};
298
+ const locked = /* @__PURE__ */ new Set();
299
+ if (merged.AsUrl !== void 0) {
300
+ const v = validateAsUrl(String(merged.AsUrl));
301
+ if (!v.ok) {
302
+ throw new Error(`Invalid policy AsUrl ("${merged.AsUrl}") from ${sourceOf.AsUrl}: ${v.error}`);
303
+ }
304
+ policy.asUrl = v.url;
305
+ locked.add("asUrl");
306
+ }
307
+ if (merged.CaFile !== void 0) {
308
+ const v = validateCaFile(String(merged.CaFile));
309
+ if (!v.ok) {
310
+ throw new Error(`Invalid policy CaFile ("${merged.CaFile}") from ${sourceOf.CaFile}: ${v.error}`);
311
+ }
312
+ policy.caFile = v.path;
313
+ locked.add("caFile");
314
+ }
315
+ if (merged.PinTls !== void 0) {
316
+ const b = coerceBoolean(merged.PinTls);
317
+ if (b === void 0) {
318
+ throw new Error(
319
+ `Invalid policy PinTls ("${String(merged.PinTls)}") from ${sourceOf.PinTls}: must be a boolean or 0/1.`
320
+ );
321
+ }
322
+ policy.pinTls = b;
323
+ locked.add("pinTls");
324
+ }
325
+ if (merged.Proxy !== void 0) {
326
+ const v = validateProxyUrl(String(merged.Proxy));
327
+ if (!v.ok) {
328
+ throw new Error(`Invalid policy Proxy ("${String(merged.Proxy)}") from ${sourceOf.Proxy}: ${v.error}`);
329
+ }
330
+ policy.proxy = v.url;
331
+ locked.add("proxy");
332
+ }
333
+ if (merged.NoProxy !== void 0) {
334
+ const raw = String(merged.NoProxy).trim();
335
+ if (!raw) {
336
+ throw new Error(`Invalid policy NoProxy ("${String(merged.NoProxy)}") from ${sourceOf.NoProxy}: must not be empty.`);
337
+ }
338
+ policy.noProxy = raw;
339
+ locked.add("noProxy");
340
+ }
341
+ if (merged.Simulation !== void 0) {
342
+ const b = coerceBoolean(merged.Simulation);
343
+ if (b === void 0) {
344
+ throw new Error(
345
+ `Invalid policy Simulation ("${String(merged.Simulation)}") from ${sourceOf.Simulation}: must be a boolean or 0/1.`
346
+ );
347
+ }
348
+ policy.simulation = b;
349
+ locked.add("simulation");
350
+ }
351
+ if (merged.InstallMethod !== void 0) {
352
+ const raw = String(merged.InstallMethod).trim().toLowerCase();
353
+ if (raw !== "managed") {
354
+ throw new Error(
355
+ `Invalid policy InstallMethod ("${String(merged.InstallMethod)}") from ${sourceOf.InstallMethod}: only "managed" is supported.`
356
+ );
357
+ }
358
+ policy.installMethod = "managed";
359
+ locked.add("installMethod");
360
+ }
361
+ cached = { policy, locked };
362
+ return cached;
363
+ }
364
+
365
+ // src/lib/as-config.ts
147
366
  var DEFAULT_AS_URL = "https://www.suveren.ai";
148
367
  function configPath(dataDir2) {
149
- return join3(dataDir2, "config.json");
368
+ return join4(dataDir2, "config.json");
150
369
  }
151
370
  function readAsConfigStrict(dataDir2) {
152
371
  const path = configPath(dataDir2);
153
- if (!existsSync3(path)) return {};
372
+ if (!existsSync4(path)) return {};
154
373
  let data;
155
374
  try {
156
- data = JSON.parse(readFileSync3(path, "utf-8"));
375
+ data = JSON.parse(readFileSync4(path, "utf-8"));
157
376
  } catch (err) {
158
377
  throw new Error(
159
378
  `Could not parse ${path}: ${err instanceof Error ? err.message : String(err)}`
@@ -168,9 +387,9 @@ function readAsConfigStrict(dataDir2) {
168
387
  }
169
388
  function readAsConfig(dataDir2) {
170
389
  const path = configPath(dataDir2);
171
- if (!existsSync3(path)) return {};
390
+ if (!existsSync4(path)) return {};
172
391
  try {
173
- const data = JSON.parse(readFileSync3(path, "utf-8"));
392
+ const data = JSON.parse(readFileSync4(path, "utf-8"));
174
393
  const out = {};
175
394
  if (typeof data.asUrl === "string") out.asUrl = data.asUrl;
176
395
  if (typeof data.caFile === "string") out.caFile = data.caFile;
@@ -181,7 +400,7 @@ function readAsConfig(dataDir2) {
181
400
  return {};
182
401
  }
183
402
  }
184
- function validateAsUrl(candidate) {
403
+ function validateAsUrl2(candidate) {
185
404
  const trimmed = candidate.trim();
186
405
  if (!trimmed) return { ok: false, error: "The Authority Server URL is empty." };
187
406
  let parsed;
@@ -206,26 +425,30 @@ function validateAsUrl(candidate) {
206
425
  return { ok: true, url: normalized };
207
426
  }
208
427
  function resolveAsUrl(dataDir2, flag) {
428
+ const policyUrl = readPolicy().policy.asUrl;
429
+ if (policyUrl) return policyUrl;
209
430
  if (flag) {
210
- const v = validateAsUrl(flag);
431
+ const v = validateAsUrl2(flag);
211
432
  if (!v.ok) throw new Error(`Invalid --as-url: ${v.error}`);
212
433
  return v.url;
213
434
  }
214
435
  const envUrl = process.env.SUVEREN_AS_URL;
215
436
  if (envUrl) {
216
- const v = validateAsUrl(envUrl);
437
+ const v = validateAsUrl2(envUrl);
217
438
  if (!v.ok) throw new Error(`Invalid SUVEREN_AS_URL: ${v.error}`);
218
439
  return v.url;
219
440
  }
220
441
  const saved = readAsConfigStrict(dataDir2).asUrl;
221
442
  if (saved) {
222
- const v = validateAsUrl(saved);
443
+ const v = validateAsUrl2(saved);
223
444
  if (!v.ok) throw new Error(`Invalid saved as-url "${saved}" in ${dataDir2}/config.json: ${v.error}`);
224
445
  return v.url;
225
446
  }
226
447
  return DEFAULT_AS_URL;
227
448
  }
228
449
  function resolvePinTls(dataDir2) {
450
+ const policyPinTls = readPolicy().policy.pinTls;
451
+ if (policyPinTls !== void 0) return policyPinTls;
229
452
  return readAsConfig(dataDir2).pinTls === true;
230
453
  }
231
454
  function validatePinTlsForUrl(pinTls, asUrl) {
@@ -240,14 +463,58 @@ function validatePinTlsForUrl(pinTls, asUrl) {
240
463
  // src/lib/as-tls-pin.ts
241
464
  import { createHash as createHash2 } from "crypto";
242
465
  import { checkServerIdentity as defaultCheckServerIdentity, rootCertificates } from "tls";
466
+ import { Agent as HttpsAgent2 } from "https";
467
+ import { readFileSync as readFileSync5 } from "fs";
468
+ import { Agent, ProxyAgent, fetch as undiciFetch } from "undici";
469
+
470
+ // src/lib/proxy-env.ts
471
+ function isLoopbackHostname(hostname) {
472
+ const h = hostname.replace(/^\[|\]$/g, "").toLowerCase();
473
+ if (h === "localhost") return true;
474
+ if (h === "::1" || h === "0:0:0:0:0:0:0:1") return true;
475
+ return /^127\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.test(h);
476
+ }
477
+ function readProxyEnv() {
478
+ const http = process.env.http_proxy || process.env.HTTP_PROXY || void 0;
479
+ const https = process.env.https_proxy || process.env.HTTPS_PROXY || void 0;
480
+ const noProxy = process.env.no_proxy ?? process.env.NO_PROXY ?? "";
481
+ return { http, https, noProxy };
482
+ }
483
+ function matchesNoProxy(hostname, port2, noProxy) {
484
+ const entries = noProxy.split(/[,\s]+/).filter(Boolean);
485
+ if (entries.length === 0) return false;
486
+ if (entries.includes("*")) return true;
487
+ const h = hostname.toLowerCase();
488
+ for (const raw of entries) {
489
+ const m = raw.match(/^(.+):(\d+)$/);
490
+ const entryHost = (m ? m[1] : raw).replace(/^\*?\./, "").toLowerCase();
491
+ const entryPort = m ? Number(m[2]) : 0;
492
+ if (entryPort && entryPort !== port2) continue;
493
+ if (h === entryHost || h.endsWith(`.${entryHost}`)) return true;
494
+ }
495
+ return false;
496
+ }
497
+ function selectProxyUrl(targetUrl) {
498
+ const url = typeof targetUrl === "string" ? new URL(targetUrl) : targetUrl;
499
+ if (isLoopbackHostname(url.hostname)) return void 0;
500
+ const { http, https, noProxy } = readProxyEnv();
501
+ const port2 = url.port ? Number(url.port) : url.protocol === "https:" ? 443 : 80;
502
+ if (matchesNoProxy(url.hostname, port2, noProxy)) return void 0;
503
+ return url.protocol === "https:" ? https ?? http : http;
504
+ }
505
+
506
+ // src/lib/proxy-https-agent.ts
243
507
  import { Agent as HttpsAgent } from "https";
244
- import { readFileSync as readFileSync4 } from "fs";
245
- import { Agent, fetch as undiciFetch } from "undici";
508
+ import { connect as netConnect } from "net";
509
+ import { connect as tlsConnect } from "tls";
510
+ var MAX_CONNECT_RESPONSE_BYTES = 64 * 1024;
511
+
512
+ // src/lib/as-tls-pin.ts
246
513
  function effectiveCa() {
247
514
  const path = process.env.NODE_EXTRA_CA_CERTS;
248
515
  if (!path) return void 0;
249
516
  try {
250
- return [...rootCertificates, readFileSync4(path, "utf-8")];
517
+ return [...rootCertificates, readFileSync5(path, "utf-8")];
251
518
  } catch {
252
519
  return void 0;
253
520
  }
@@ -302,24 +569,27 @@ function pinnedCheckServerIdentity(pinnedSpkiHex) {
302
569
  return void 0;
303
570
  };
304
571
  }
305
- function buildPinnedDispatcher(pinnedSpkiHex) {
306
- return new Agent({
307
- connect: {
308
- ca: effectiveCa(),
309
- checkServerIdentity: pinnedCheckServerIdentity(pinnedSpkiHex)
310
- }
572
+ function buildTlsDispatcher(url, connectOpts) {
573
+ const proxyUrl = selectProxyUrl(url);
574
+ if (proxyUrl) {
575
+ return new ProxyAgent({ uri: proxyUrl, requestTls: connectOpts });
576
+ }
577
+ return new Agent({ connect: connectOpts });
578
+ }
579
+ function buildPinnedDispatcher(pinnedSpkiHex, url) {
580
+ return buildTlsDispatcher(url, {
581
+ ca: effectiveCa(),
582
+ checkServerIdentity: pinnedCheckServerIdentity(pinnedSpkiHex)
311
583
  });
312
584
  }
313
- function buildCapturingDispatcher() {
585
+ function buildCapturingDispatcher(url) {
314
586
  let captured;
315
- const dispatcher = new Agent({
316
- connect: {
317
- ca: effectiveCa(),
318
- checkServerIdentity: (hostname, cert) => {
319
- captured = cert;
320
- const defaultErr = defaultCheckServerIdentity(hostname, cert);
321
- return defaultErr ? taggedCheckError(defaultErr) : void 0;
322
- }
587
+ const dispatcher = buildTlsDispatcher(url, {
588
+ ca: effectiveCa(),
589
+ checkServerIdentity: (hostname, cert) => {
590
+ captured = cert;
591
+ const defaultErr = defaultCheckServerIdentity(hostname, cert);
592
+ return defaultErr ? taggedCheckError(defaultErr) : void 0;
323
593
  }
324
594
  });
325
595
  return { dispatcher, getCapturedCert: () => captured };
@@ -330,6 +600,34 @@ function withBoundedSignal(init) {
330
600
  const signal = init?.signal ? AbortSignal.any([init.signal, timeout]) : timeout;
331
601
  return { ...init, signal };
332
602
  }
603
+ var UNKNOWN_ISSUER_CODES = /* @__PURE__ */ new Set([
604
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
605
+ "UNABLE_TO_GET_ISSUER_CERT",
606
+ "UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
607
+ "SELF_SIGNED_CERT_IN_CHAIN",
608
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
609
+ "CERT_UNTRUSTED"
610
+ ]);
611
+ function tlsErrorCode(err) {
612
+ if (!(err instanceof Error)) return void 0;
613
+ const cause = err.cause;
614
+ const withCode = cause instanceof Error ? cause : err;
615
+ return withCode.code;
616
+ }
617
+ function isUnknownIssuerError(err) {
618
+ const code = tlsErrorCode(err);
619
+ return !!code && UNKNOWN_ISSUER_CODES.has(code);
620
+ }
621
+ function rethrowWithIssuerHint(err, url) {
622
+ if (isUnknownIssuerError(err)) {
623
+ const hinted = new Error(
624
+ `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)}`
625
+ );
626
+ hinted.cause = err;
627
+ throw hinted;
628
+ }
629
+ throw err;
630
+ }
333
631
  function closeAfter(dispatcher, pending) {
334
632
  pending.catch(() => {
335
633
  }).finally(() => {
@@ -339,8 +637,24 @@ function closeAfter(dispatcher, pending) {
339
637
  }
340
638
  async function fetchAs(url, init, pinning) {
341
639
  if (!pinning.enforce && !pinning.capture) {
342
- const res2 = await fetch(url, withBoundedSignal(init));
343
- return { res: res2 };
640
+ const proxyUrl = selectProxyUrl(url);
641
+ if (!proxyUrl) {
642
+ try {
643
+ const res2 = await fetch(url, withBoundedSignal(init));
644
+ return { res: res2 };
645
+ } catch (err) {
646
+ rethrowWithIssuerHint(err, url);
647
+ }
648
+ }
649
+ const dispatcher2 = new ProxyAgent({ uri: proxyUrl, requestTls: { ca: effectiveCa() } });
650
+ const pending2 = undiciFetch(url, { ...withBoundedSignal(init), dispatcher: dispatcher2 });
651
+ closeAfter(dispatcher2, pending2);
652
+ try {
653
+ const res2 = await pending2;
654
+ return { res: res2 };
655
+ } catch (err) {
656
+ rethrowWithIssuerHint(err, url);
657
+ }
344
658
  }
345
659
  if (pinning.enforce && !pinning.capture && !pinning.pinnedSpkiHex) {
346
660
  throw new AsTlsMismatchError(
@@ -348,27 +662,27 @@ async function fetchAs(url, init, pinning) {
348
662
  );
349
663
  }
350
664
  if (pinning.pinnedSpkiHex && (pinning.enforce || !pinning.capture)) {
351
- const dispatcher2 = buildPinnedDispatcher(pinning.pinnedSpkiHex);
665
+ const dispatcher2 = buildPinnedDispatcher(pinning.pinnedSpkiHex, url);
352
666
  const pending2 = undiciFetch(url, { ...withBoundedSignal(init), dispatcher: dispatcher2 });
353
667
  closeAfter(dispatcher2, pending2);
354
668
  try {
355
669
  const res2 = await pending2;
356
670
  return { res: res2 };
357
671
  } catch (err) {
358
- if (!isPinCheckError(err)) throw err;
672
+ if (!isPinCheckError(err)) rethrowWithIssuerHint(err, url);
359
673
  throw new AsTlsMismatchError(
360
- `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.`
674
+ `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).`
361
675
  );
362
676
  }
363
677
  }
364
- const { dispatcher, getCapturedCert } = buildCapturingDispatcher();
678
+ const { dispatcher, getCapturedCert } = buildCapturingDispatcher(url);
365
679
  const pending = undiciFetch(url, { ...withBoundedSignal(init), dispatcher });
366
680
  closeAfter(dispatcher, pending);
367
681
  let res;
368
682
  try {
369
683
  res = await pending;
370
684
  } catch (err) {
371
- if (!isPinCheckError(err)) throw err;
685
+ if (!isPinCheckError(err)) rethrowWithIssuerHint(err, url);
372
686
  throw new AsTlsMismatchError(
373
687
  `could not verify the Authority Server at ${url} while establishing its TLS pin \u2014 ${describeFetchError(err)}.`
374
688
  );
@@ -834,11 +1148,11 @@ var AttestationCache = class {
834
1148
  };
835
1149
 
836
1150
  // src/lib/gate-store.ts
837
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3, existsSync as existsSync4, unlinkSync as unlinkSync2 } from "fs";
1151
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3, existsSync as existsSync5, unlinkSync as unlinkSync2 } from "fs";
838
1152
  import { homedir } from "os";
839
- import { dirname as dirname3, join as join4 } from "path";
1153
+ import { dirname as dirname3, join as join5 } from "path";
840
1154
  import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
841
- var DEFAULT_DIR = process.env.SUVEREN_DATA_DIR ?? join4(homedir(), ".suveren");
1155
+ var DEFAULT_DIR = process.env.SUVEREN_DATA_DIR ?? join5(homedir(), ".suveren");
842
1156
  var GateStore = class {
843
1157
  entries = /* @__PURE__ */ new Map();
844
1158
  baseDir;
@@ -856,11 +1170,11 @@ var GateStore = class {
856
1170
  */
857
1171
  setVaultKey(key) {
858
1172
  this.vaultKey = key;
859
- if (existsSync4(this.encryptedFilePath)) {
1173
+ if (existsSync5(this.encryptedFilePath)) {
860
1174
  this.loadEncrypted();
861
1175
  } else if (this.entries.size > 0) {
862
1176
  this.persistEncrypted();
863
- if (existsSync4(this.plaintextFilePath)) {
1177
+ if (existsSync5(this.plaintextFilePath)) {
864
1178
  try {
865
1179
  unlinkSync2(this.plaintextFilePath);
866
1180
  } catch {
@@ -898,11 +1212,11 @@ var GateStore = class {
898
1212
  clearAll() {
899
1213
  this.entries = /* @__PURE__ */ new Map();
900
1214
  try {
901
- if (existsSync4(this.plaintextFilePath)) unlinkSync2(this.plaintextFilePath);
1215
+ if (existsSync5(this.plaintextFilePath)) unlinkSync2(this.plaintextFilePath);
902
1216
  } catch {
903
1217
  }
904
1218
  try {
905
- if (existsSync4(this.encryptedFilePath)) unlinkSync2(this.encryptedFilePath);
1219
+ if (existsSync5(this.encryptedFilePath)) unlinkSync2(this.encryptedFilePath);
906
1220
  } catch {
907
1221
  }
908
1222
  }
@@ -935,19 +1249,19 @@ var GateStore = class {
935
1249
  }
936
1250
  // ─── File paths ─────────────────────────────────────────────────────────
937
1251
  get plaintextFilePath() {
938
- return join4(this.baseDir, "gates.json");
1252
+ return join5(this.baseDir, "gates.json");
939
1253
  }
940
1254
  get encryptedFilePath() {
941
- return join4(this.baseDir, "gates.enc.json");
1255
+ return join5(this.baseDir, "gates.enc.json");
942
1256
  }
943
1257
  // ─── Load / Persist ─────────────────────────────────────────────────────
944
1258
  loadPlaintext() {
945
- if (!existsSync4(this.plaintextFilePath)) {
1259
+ if (!existsSync5(this.plaintextFilePath)) {
946
1260
  mkdirSync3(this.baseDir, { recursive: true });
947
1261
  return;
948
1262
  }
949
1263
  try {
950
- const raw = readFileSync5(this.plaintextFilePath, "utf-8");
1264
+ const raw = readFileSync6(this.plaintextFilePath, "utf-8");
951
1265
  const data = JSON.parse(raw);
952
1266
  this.entries = new Map(Object.entries(data.entries));
953
1267
  } catch {
@@ -956,9 +1270,9 @@ var GateStore = class {
956
1270
  }
957
1271
  }
958
1272
  loadEncrypted() {
959
- if (!existsSync4(this.encryptedFilePath)) return;
1273
+ if (!existsSync5(this.encryptedFilePath)) return;
960
1274
  try {
961
- const raw = readFileSync5(this.encryptedFilePath, "utf-8");
1275
+ const raw = readFileSync6(this.encryptedFilePath, "utf-8");
962
1276
  const data = JSON.parse(raw);
963
1277
  this.entries = /* @__PURE__ */ new Map();
964
1278
  for (const [key, blob] of Object.entries(data.entries)) {
@@ -999,11 +1313,11 @@ var GateStore = class {
999
1313
  };
1000
1314
 
1001
1315
  // src/lib/execution-log.ts
1002
- import { mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4, existsSync as existsSync5, unlinkSync as unlinkSync3 } from "fs";
1316
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync4, existsSync as existsSync6, unlinkSync as unlinkSync3 } from "fs";
1003
1317
  import { homedir as homedir2 } from "os";
1004
- import { dirname as dirname4, join as join5 } from "path";
1318
+ import { dirname as dirname4, join as join6 } from "path";
1005
1319
  import { createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2, randomBytes as randomBytes2 } from "crypto";
1006
- var DEFAULT_DIR2 = process.env.SUVEREN_DATA_DIR ?? join5(homedir2(), ".suveren");
1320
+ var DEFAULT_DIR2 = process.env.SUVEREN_DATA_DIR ?? join6(homedir2(), ".suveren");
1007
1321
  var MAX_AGE_SECONDS = 31 * 24 * 60 * 60;
1008
1322
  var ExecutionLog = class {
1009
1323
  entries = [];
@@ -1022,11 +1336,11 @@ var ExecutionLog = class {
1022
1336
  */
1023
1337
  setVaultKey(key) {
1024
1338
  this.vaultKey = key;
1025
- if (existsSync5(this.encryptedFilePath)) {
1339
+ if (existsSync6(this.encryptedFilePath)) {
1026
1340
  this.loadEncrypted();
1027
1341
  } else if (this.entries.length > 0) {
1028
1342
  this.persistEncrypted();
1029
- if (existsSync5(this.plaintextFilePath)) {
1343
+ if (existsSync6(this.plaintextFilePath)) {
1030
1344
  try {
1031
1345
  unlinkSync3(this.plaintextFilePath);
1032
1346
  } catch {
@@ -1130,19 +1444,19 @@ var ExecutionLog = class {
1130
1444
  }
1131
1445
  // ─── File paths ─────────────────────────────────────────────────────────
1132
1446
  get plaintextFilePath() {
1133
- return join5(this.baseDir, "execution-log.json");
1447
+ return join6(this.baseDir, "execution-log.json");
1134
1448
  }
1135
1449
  get encryptedFilePath() {
1136
- return join5(this.baseDir, "execution-log.enc.json");
1450
+ return join6(this.baseDir, "execution-log.enc.json");
1137
1451
  }
1138
1452
  // ─── Load / Persist ─────────────────────────────────────────────────────
1139
1453
  loadPlaintext() {
1140
- if (!existsSync5(this.plaintextFilePath)) {
1454
+ if (!existsSync6(this.plaintextFilePath)) {
1141
1455
  mkdirSync4(this.baseDir, { recursive: true });
1142
1456
  return;
1143
1457
  }
1144
1458
  try {
1145
- const raw = readFileSync6(this.plaintextFilePath, "utf-8");
1459
+ const raw = readFileSync7(this.plaintextFilePath, "utf-8");
1146
1460
  const data = JSON.parse(raw);
1147
1461
  this.entries = data.entries;
1148
1462
  this.prune();
@@ -1152,9 +1466,9 @@ var ExecutionLog = class {
1152
1466
  }
1153
1467
  }
1154
1468
  loadEncrypted() {
1155
- if (!existsSync5(this.encryptedFilePath)) return;
1469
+ if (!existsSync6(this.encryptedFilePath)) return;
1156
1470
  try {
1157
- const raw = readFileSync6(this.encryptedFilePath, "utf-8");
1471
+ const raw = readFileSync7(this.encryptedFilePath, "utf-8");
1158
1472
  const data = JSON.parse(raw);
1159
1473
  const decrypted = this.decrypt(data.blob);
1160
1474
  const logFile = JSON.parse(decrypted);
@@ -1201,11 +1515,11 @@ function windowCutoff(window, now) {
1201
1515
  }
1202
1516
 
1203
1517
  // src/lib/denial-log.ts
1204
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync6, unlinkSync as unlinkSync4 } from "fs";
1518
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync5, existsSync as existsSync7, unlinkSync as unlinkSync4 } from "fs";
1205
1519
  import { homedir as homedir3 } from "os";
1206
- import { dirname as dirname5, join as join6 } from "path";
1520
+ import { dirname as dirname5, join as join7 } from "path";
1207
1521
  import { createCipheriv as createCipheriv3, createDecipheriv as createDecipheriv3, randomBytes as randomBytes3 } from "crypto";
1208
- var DEFAULT_DIR3 = process.env.SUVEREN_DATA_DIR ?? join6(homedir3(), ".suveren");
1522
+ var DEFAULT_DIR3 = process.env.SUVEREN_DATA_DIR ?? join7(homedir3(), ".suveren");
1209
1523
  var MAX_RECORDS = 200;
1210
1524
  var MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
1211
1525
  var DenialLog = class {
@@ -1218,11 +1532,11 @@ var DenialLog = class {
1218
1532
  }
1219
1533
  setVaultKey(key) {
1220
1534
  this.vaultKey = key;
1221
- if (existsSync6(this.encryptedFilePath)) {
1535
+ if (existsSync7(this.encryptedFilePath)) {
1222
1536
  this.loadEncrypted();
1223
1537
  } else if (this.records.length > 0) {
1224
1538
  this.persistEncrypted();
1225
- if (existsSync6(this.plaintextFilePath)) {
1539
+ if (existsSync7(this.plaintextFilePath)) {
1226
1540
  try {
1227
1541
  unlinkSync4(this.plaintextFilePath);
1228
1542
  } catch {
@@ -1272,18 +1586,18 @@ var DenialLog = class {
1272
1586
  return Buffer.concat([decipher.update(Buffer.from(blob.ciphertext, "hex")), decipher.final()]).toString("utf8");
1273
1587
  }
1274
1588
  get plaintextFilePath() {
1275
- return join6(this.baseDir, "denials.json");
1589
+ return join7(this.baseDir, "denials.json");
1276
1590
  }
1277
1591
  get encryptedFilePath() {
1278
- return join6(this.baseDir, "denials.enc.json");
1592
+ return join7(this.baseDir, "denials.enc.json");
1279
1593
  }
1280
1594
  loadPlaintext() {
1281
- if (!existsSync6(this.plaintextFilePath)) {
1595
+ if (!existsSync7(this.plaintextFilePath)) {
1282
1596
  mkdirSync5(this.baseDir, { recursive: true });
1283
1597
  return;
1284
1598
  }
1285
1599
  try {
1286
- const data = JSON.parse(readFileSync7(this.plaintextFilePath, "utf-8"));
1600
+ const data = JSON.parse(readFileSync8(this.plaintextFilePath, "utf-8"));
1287
1601
  this.records = data.records ?? [];
1288
1602
  } catch {
1289
1603
  console.error(`[DenialLog] Could not parse ${this.plaintextFilePath}, starting fresh`);
@@ -1291,9 +1605,9 @@ var DenialLog = class {
1291
1605
  }
1292
1606
  }
1293
1607
  loadEncrypted() {
1294
- if (!existsSync6(this.encryptedFilePath)) return;
1608
+ if (!existsSync7(this.encryptedFilePath)) return;
1295
1609
  try {
1296
- const data = JSON.parse(readFileSync7(this.encryptedFilePath, "utf-8"));
1610
+ const data = JSON.parse(readFileSync8(this.encryptedFilePath, "utf-8"));
1297
1611
  const logFile = JSON.parse(this.decrypt(data.blob));
1298
1612
  this.records = logFile.records ?? [];
1299
1613
  } catch (err) {
@@ -1319,11 +1633,11 @@ var DenialLog = class {
1319
1633
  };
1320
1634
 
1321
1635
  // src/lib/receipt-archive.ts
1322
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync6, existsSync as existsSync7, unlinkSync as unlinkSync5 } from "fs";
1636
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync6, existsSync as existsSync8, unlinkSync as unlinkSync5 } from "fs";
1323
1637
  import { homedir as homedir4 } from "os";
1324
- import { dirname as dirname6, join as join7 } from "path";
1638
+ import { dirname as dirname6, join as join8 } from "path";
1325
1639
  import { createCipheriv as createCipheriv4, createDecipheriv as createDecipheriv4, randomBytes as randomBytes4 } from "crypto";
1326
- var DEFAULT_DIR4 = process.env.SUVEREN_DATA_DIR ?? join7(homedir4(), ".suveren");
1640
+ var DEFAULT_DIR4 = process.env.SUVEREN_DATA_DIR ?? join8(homedir4(), ".suveren");
1327
1641
  function stripPreviews(proposal) {
1328
1642
  const args = proposal.toolArgs;
1329
1643
  if (!args || typeof args !== "object") return proposal;
@@ -1348,11 +1662,11 @@ var ReceiptArchive = class {
1348
1662
  */
1349
1663
  setVaultKey(key) {
1350
1664
  this.vaultKey = key;
1351
- if (existsSync7(this.encryptedFilePath)) {
1665
+ if (existsSync8(this.encryptedFilePath)) {
1352
1666
  this.loadEncrypted();
1353
1667
  } else if (this.data.receipts.length > 0 || Object.keys(this.data.authorizations).length > 0) {
1354
1668
  this.persistEncrypted();
1355
- if (existsSync7(this.plaintextFilePath)) {
1669
+ if (existsSync8(this.plaintextFilePath)) {
1356
1670
  try {
1357
1671
  unlinkSync5(this.plaintextFilePath);
1358
1672
  } catch {
@@ -1366,7 +1680,7 @@ var ReceiptArchive = class {
1366
1680
  * this state — that would report a hollow success over unreadable evidence.
1367
1681
  */
1368
1682
  isLocked() {
1369
- return this.vaultKey === null && existsSync7(this.encryptedFilePath);
1683
+ return this.vaultKey === null && existsSync8(this.encryptedFilePath);
1370
1684
  }
1371
1685
  /**
1372
1686
  * Archive a signed receipt (and the grant it was issued under).
@@ -1467,19 +1781,19 @@ var ReceiptArchive = class {
1467
1781
  }
1468
1782
  // ─── File paths ─────────────────────────────────────────────────────────
1469
1783
  get plaintextFilePath() {
1470
- return join7(this.baseDir, "receipt-archive.json");
1784
+ return join8(this.baseDir, "receipt-archive.json");
1471
1785
  }
1472
1786
  get encryptedFilePath() {
1473
- return join7(this.baseDir, "receipt-archive.enc.json");
1787
+ return join8(this.baseDir, "receipt-archive.enc.json");
1474
1788
  }
1475
1789
  // ─── Load / Persist ─────────────────────────────────────────────────────
1476
1790
  loadPlaintext() {
1477
- if (!existsSync7(this.plaintextFilePath)) {
1791
+ if (!existsSync8(this.plaintextFilePath)) {
1478
1792
  mkdirSync6(this.baseDir, { recursive: true });
1479
1793
  return;
1480
1794
  }
1481
1795
  try {
1482
- const raw = readFileSync8(this.plaintextFilePath, "utf-8");
1796
+ const raw = readFileSync9(this.plaintextFilePath, "utf-8");
1483
1797
  const parsed = JSON.parse(raw);
1484
1798
  this.data = {
1485
1799
  version: 1,
@@ -1491,16 +1805,16 @@ var ReceiptArchive = class {
1491
1805
  try {
1492
1806
  writeFileSync6(
1493
1807
  `${this.plaintextFilePath}.corrupt-${Date.now()}`,
1494
- readFileSync8(this.plaintextFilePath)
1808
+ readFileSync9(this.plaintextFilePath)
1495
1809
  );
1496
1810
  } catch {
1497
1811
  }
1498
1812
  }
1499
1813
  }
1500
1814
  loadEncrypted() {
1501
- if (!existsSync7(this.encryptedFilePath)) return;
1815
+ if (!existsSync8(this.encryptedFilePath)) return;
1502
1816
  try {
1503
- const raw = readFileSync8(this.encryptedFilePath, "utf-8");
1817
+ const raw = readFileSync9(this.encryptedFilePath, "utf-8");
1504
1818
  const data = JSON.parse(raw);
1505
1819
  const decrypted = this.decrypt(data.blob);
1506
1820
  const parsed = JSON.parse(decrypted);
@@ -1514,7 +1828,7 @@ var ReceiptArchive = class {
1514
1828
  try {
1515
1829
  writeFileSync6(
1516
1830
  `${this.encryptedFilePath}.corrupt-${Date.now()}`,
1517
- readFileSync8(this.encryptedFilePath)
1831
+ readFileSync9(this.encryptedFilePath)
1518
1832
  );
1519
1833
  } catch {
1520
1834
  }
@@ -1542,11 +1856,11 @@ var ReceiptArchive = class {
1542
1856
  };
1543
1857
 
1544
1858
  // src/lib/execution-journal.ts
1545
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync9, writeFileSync as writeFileSync7, existsSync as existsSync8, renameSync } from "fs";
1859
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync7, existsSync as existsSync9, renameSync } from "fs";
1546
1860
  import { homedir as homedir5 } from "os";
1547
- import { dirname as dirname7, join as join8 } from "path";
1861
+ import { dirname as dirname7, join as join9 } from "path";
1548
1862
  import { createHash as createHash3 } from "crypto";
1549
- var DEFAULT_DIR5 = process.env.SUVEREN_DATA_DIR ?? join8(homedir5(), ".suveren");
1863
+ var DEFAULT_DIR5 = process.env.SUVEREN_DATA_DIR ?? join9(homedir5(), ".suveren");
1550
1864
  var FILE_NAME = "execution-journal.json";
1551
1865
  var MAX_AGE_SECONDS2 = 90 * 24 * 60 * 60;
1552
1866
  function hashToolArgs(args) {
@@ -1563,7 +1877,7 @@ var ExecutionJournal = class {
1563
1877
  filePath;
1564
1878
  constructor(filePath) {
1565
1879
  const baseDir = filePath && filePath.endsWith(".json") ? dirname7(filePath) : filePath ?? DEFAULT_DIR5;
1566
- this.filePath = join8(baseDir, FILE_NAME);
1880
+ this.filePath = join9(baseDir, FILE_NAME);
1567
1881
  }
1568
1882
  /** Look a ticket up. Always reads from disk — another process may have written. */
1569
1883
  get(ticketId) {
@@ -1602,9 +1916,9 @@ var ExecutionJournal = class {
1602
1916
  }
1603
1917
  // ─── Persistence ────────────────────────────────────────────────────────
1604
1918
  load() {
1605
- if (!existsSync8(this.filePath)) return { version: 1, entries: [] };
1919
+ if (!existsSync9(this.filePath)) return { version: 1, entries: [] };
1606
1920
  try {
1607
- const parsed = JSON.parse(readFileSync9(this.filePath, "utf8"));
1921
+ const parsed = JSON.parse(readFileSync10(this.filePath, "utf8"));
1608
1922
  if (parsed?.version !== 1 || !Array.isArray(parsed.entries)) {
1609
1923
  throw new Error("unexpected shape");
1610
1924
  }
@@ -1629,10 +1943,10 @@ function nowSec() {
1629
1943
  }
1630
1944
 
1631
1945
  // src/lib/proposal-submission-store.ts
1632
- import { mkdirSync as mkdirSync8, readFileSync as readFileSync10, writeFileSync as writeFileSync8, existsSync as existsSync9, renameSync as renameSync2 } from "fs";
1946
+ import { mkdirSync as mkdirSync8, readFileSync as readFileSync11, writeFileSync as writeFileSync8, existsSync as existsSync10, renameSync as renameSync2 } from "fs";
1633
1947
  import { homedir as homedir6 } from "os";
1634
- import { dirname as dirname8, join as join9 } from "path";
1635
- var DEFAULT_DIR6 = process.env.SUVEREN_DATA_DIR ?? join9(homedir6(), ".suveren");
1948
+ import { dirname as dirname8, join as join10 } from "path";
1949
+ var DEFAULT_DIR6 = process.env.SUVEREN_DATA_DIR ?? join10(homedir6(), ".suveren");
1636
1950
  var FILE_NAME2 = "proposal-submissions.json";
1637
1951
  var MAX_AGE_SECONDS3 = 90 * 24 * 60 * 60;
1638
1952
  function nowSec2() {
@@ -1642,7 +1956,7 @@ var ProposalSubmissionStore = class {
1642
1956
  filePath;
1643
1957
  constructor(filePath) {
1644
1958
  const baseDir = filePath && filePath.endsWith(".json") ? dirname8(filePath) : filePath ?? DEFAULT_DIR6;
1645
- this.filePath = join9(baseDir, FILE_NAME2);
1959
+ this.filePath = join10(baseDir, FILE_NAME2);
1646
1960
  }
1647
1961
  /** Record what was submitted, right after a successful submitProposal(). */
1648
1962
  record(entry) {
@@ -1665,9 +1979,9 @@ var ProposalSubmissionStore = class {
1665
1979
  return this.load().entries.find((e) => e.proposalId === proposalId);
1666
1980
  }
1667
1981
  load() {
1668
- if (!existsSync9(this.filePath)) return { version: 1, entries: [] };
1982
+ if (!existsSync10(this.filePath)) return { version: 1, entries: [] };
1669
1983
  try {
1670
- const parsed = JSON.parse(readFileSync10(this.filePath, "utf-8"));
1984
+ const parsed = JSON.parse(readFileSync11(this.filePath, "utf-8"));
1671
1985
  const cutoff = nowSec2() - MAX_AGE_SECONDS3;
1672
1986
  parsed.entries = (parsed.entries ?? []).filter((e) => e.submittedAt >= cutoff);
1673
1987
  return parsed;
@@ -1956,17 +2270,17 @@ function formatConsumptionFull(entries) {
1956
2270
  }
1957
2271
 
1958
2272
  // src/lib/context-loader.ts
1959
- import { readFileSync as readFileSync11, existsSync as existsSync10 } from "fs";
2273
+ import { readFileSync as readFileSync12, existsSync as existsSync11 } from "fs";
1960
2274
  import { homedir as homedir7 } from "os";
1961
- import { join as join10 } from "path";
1962
- var DEFAULT_DIR7 = process.env.SUVEREN_DATA_DIR ?? join10(homedir7(), ".suveren");
2275
+ import { join as join11 } from "path";
2276
+ var DEFAULT_DIR7 = process.env.SUVEREN_DATA_DIR ?? join11(homedir7(), ".suveren");
1963
2277
  var BRIEF_MAX_CHARS = 16 * 1024;
1964
2278
  function readContextFile(dataDir2) {
1965
2279
  const dir = dataDir2 ?? DEFAULT_DIR7;
1966
- const filePath = join10(dir, "context.md");
1967
- if (!existsSync10(filePath)) return null;
2280
+ const filePath = join11(dir, "context.md");
2281
+ if (!existsSync11(filePath)) return null;
1968
2282
  try {
1969
- const content = readFileSync11(filePath, "utf-8").trim();
2283
+ const content = readFileSync12(filePath, "utf-8").trim();
1970
2284
  return content || null;
1971
2285
  } catch {
1972
2286
  return null;
@@ -2605,8 +2919,8 @@ import { extname } from "path";
2605
2919
  import { randomUUID } from "crypto";
2606
2920
 
2607
2921
  // src/lib/manifest-loader.ts
2608
- import { readFileSync as readFileSync12, existsSync as existsSync11 } from "fs";
2609
- import { join as join11, resolve } from "path";
2922
+ import { readFileSync as readFileSync13, existsSync as existsSync12 } from "fs";
2923
+ import { join as join12, resolve } from "path";
2610
2924
  var EXACT_SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
2611
2925
  function isExactSemver(value) {
2612
2926
  return typeof value === "string" && EXACT_SEMVER_RE.test(value);
@@ -2624,25 +2938,25 @@ function invalidNpmPinReason(manifest) {
2624
2938
  var manifests = /* @__PURE__ */ new Map();
2625
2939
  function loadManifests(integrationsDir) {
2626
2940
  const dir = resolve(
2627
- integrationsDir ?? process.env.SUVEREN_MANIFESTS_DIR ?? join11(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "content", "integrations")
2941
+ integrationsDir ?? process.env.SUVEREN_MANIFESTS_DIR ?? join12(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "content", "integrations")
2628
2942
  );
2629
- const indexPath = join11(dir, "index.json");
2630
- if (!existsSync11(indexPath)) {
2943
+ const indexPath = join12(dir, "index.json");
2944
+ if (!existsSync12(indexPath)) {
2631
2945
  console.error(`[ManifestLoader] No index.json found at ${indexPath}, skipping manifest loading`);
2632
2946
  return 0;
2633
2947
  }
2634
2948
  let index;
2635
2949
  try {
2636
- index = JSON.parse(readFileSync12(indexPath, "utf-8"));
2950
+ index = JSON.parse(readFileSync13(indexPath, "utf-8"));
2637
2951
  } catch (err) {
2638
2952
  console.error(`[ManifestLoader] Failed to parse ${indexPath}:`, err);
2639
2953
  return 0;
2640
2954
  }
2641
2955
  let loaded = 0;
2642
2956
  for (const [id, relativePath] of Object.entries(index.integrations)) {
2643
- const manifestPath = join11(dir, relativePath);
2957
+ const manifestPath = join12(dir, relativePath);
2644
2958
  try {
2645
- const manifest = JSON.parse(readFileSync12(manifestPath, "utf-8"));
2959
+ const manifest = JSON.parse(readFileSync13(manifestPath, "utf-8"));
2646
2960
  const pinReason = invalidNpmPinReason(manifest);
2647
2961
  if (pinReason) {
2648
2962
  console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${pinReason}`);
@@ -2666,6 +2980,8 @@ function getAllManifests() {
2666
2980
 
2667
2981
  // src/lib/simulation-mode.ts
2668
2982
  function isSimulationMode() {
2983
+ const policySimulation = readPolicy().policy.simulation;
2984
+ if (policySimulation !== void 0) return policySimulation;
2669
2985
  return process.env.SUVEREN_SIMULATION === "1";
2670
2986
  }
2671
2987
  function manifestIsSimulated(manifest) {
@@ -4195,16 +4511,16 @@ function verifyGateContentHashes(content, auth) {
4195
4511
  }
4196
4512
 
4197
4513
  // src/lib/integration-registry.ts
4198
- import { mkdirSync as mkdirSync9, readFileSync as readFileSync13, writeFileSync as writeFileSync9, existsSync as existsSync12 } from "fs";
4514
+ import { mkdirSync as mkdirSync9, readFileSync as readFileSync14, writeFileSync as writeFileSync9, existsSync as existsSync13 } from "fs";
4199
4515
  import { homedir as homedir8 } from "os";
4200
- import { dirname as dirname9, join as join12 } from "path";
4201
- var DEFAULT_DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join12(homedir8(), ".suveren");
4516
+ import { dirname as dirname9, join as join13 } from "path";
4517
+ var DEFAULT_DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join13(homedir8(), ".suveren");
4202
4518
  var IntegrationRegistry = class {
4203
4519
  integrations = /* @__PURE__ */ new Map();
4204
4520
  filePath;
4205
4521
  constructor(dataDir2) {
4206
4522
  const dir = dataDir2 ?? DEFAULT_DATA_DIR;
4207
- this.filePath = join12(dir, "integrations.json");
4523
+ this.filePath = join13(dir, "integrations.json");
4208
4524
  this.load();
4209
4525
  }
4210
4526
  // ─── CRUD ───────────────────────────────────────────────────────────────
@@ -4235,12 +4551,12 @@ var IntegrationRegistry = class {
4235
4551
  }
4236
4552
  // ─── Load / Save ────────────────────────────────────────────────────────
4237
4553
  load() {
4238
- if (!existsSync12(this.filePath)) {
4554
+ if (!existsSync13(this.filePath)) {
4239
4555
  mkdirSync9(dirname9(this.filePath), { recursive: true });
4240
4556
  return;
4241
4557
  }
4242
4558
  try {
4243
- const raw = readFileSync13(this.filePath, "utf-8");
4559
+ const raw = readFileSync14(this.filePath, "utf-8");
4244
4560
  const data = JSON.parse(raw);
4245
4561
  const migrated = data.integrations.map((i) => {
4246
4562
  if ("toolGating" in i && !("profile" in i)) {
@@ -4276,8 +4592,8 @@ var IntegrationRegistry = class {
4276
4592
 
4277
4593
  // src/lib/integration-manager.ts
4278
4594
  import { homedir as homedir9 } from "os";
4279
- import { join as join13, delimiter } from "path";
4280
- import { existsSync as existsSync13, writeFileSync as writeFileSync10, mkdirSync as mkdirSync10, readFileSync as readFileSync14, rmSync } from "fs";
4595
+ import { join as join14, delimiter } from "path";
4596
+ import { existsSync as existsSync14, writeFileSync as writeFileSync10, mkdirSync as mkdirSync10, readFileSync as readFileSync15, rmSync } from "fs";
4281
4597
  import { execFile } from "child_process";
4282
4598
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
4283
4599
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
@@ -4354,25 +4670,26 @@ function safeHost(url) {
4354
4670
  }
4355
4671
 
4356
4672
  // src/lib/integration-manager.ts
4357
- var DEFAULT_DATA_DIR2 = process.env.SUVEREN_DATA_DIR ?? join13(homedir9(), ".suveren");
4358
- var INTEGRATIONS_DIR = process.env.SUVEREN_INTEGRATIONS_DIR ?? join13(DEFAULT_DATA_DIR2, "integrations");
4673
+ var DEFAULT_DATA_DIR2 = process.env.SUVEREN_DATA_DIR ?? join14(homedir9(), ".suveren");
4674
+ var INTEGRATIONS_DIR = process.env.SUVEREN_INTEGRATIONS_DIR ?? join14(DEFAULT_DATA_DIR2, "integrations");
4675
+ var OFFLINE_MODE = process.env.SUVEREN_OFFLINE === "1";
4359
4676
  var installLock = Promise.resolve();
4360
4677
  function withInstallLock(task) {
4361
4678
  const run = installLock.then(task, task);
4362
4679
  installLock = run.then(() => void 0, () => void 0);
4363
4680
  return run;
4364
4681
  }
4365
- var INTEGRATIONS_BIN = join13(INTEGRATIONS_DIR, "node_modules", ".bin");
4682
+ var INTEGRATIONS_BIN = join14(INTEGRATIONS_DIR, "node_modules", ".bin");
4366
4683
  function buildPath() {
4367
4684
  const base = process.env.PATH ?? "";
4368
4685
  return [INTEGRATIONS_BIN, base].join(delimiter);
4369
4686
  }
4370
4687
  function ensureIntegrationsDir() {
4371
- if (!existsSync13(INTEGRATIONS_DIR)) {
4688
+ if (!existsSync14(INTEGRATIONS_DIR)) {
4372
4689
  mkdirSync10(INTEGRATIONS_DIR, { recursive: true });
4373
4690
  }
4374
- const pkgPath = join13(INTEGRATIONS_DIR, "package.json");
4375
- if (!existsSync13(pkgPath)) {
4691
+ const pkgPath = join14(INTEGRATIONS_DIR, "package.json");
4692
+ if (!existsSync14(pkgPath)) {
4376
4693
  writeFileSync10(pkgPath, JSON.stringify({ name: "suveren-integrations", version: "1.0.0", private: true }, null, 2));
4377
4694
  }
4378
4695
  }
@@ -4460,19 +4777,19 @@ var IntegrationManager = class {
4460
4777
  * exist, and treat anything else as not-installed (and reinstallable).
4461
4778
  */
4462
4779
  isUsableInstall(npmPackage) {
4463
- const pkgDir = join13(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
4464
- const pkgJson = join13(pkgDir, "package.json");
4465
- if (!existsSync13(pkgDir) || !existsSync13(pkgJson)) return false;
4780
+ const pkgDir = join14(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
4781
+ const pkgJson = join14(pkgDir, "package.json");
4782
+ if (!existsSync14(pkgDir) || !existsSync14(pkgJson)) return false;
4466
4783
  try {
4467
- const pkg = JSON.parse(readFileSync14(pkgJson, "utf8"));
4784
+ const pkg = JSON.parse(readFileSync15(pkgJson, "utf8"));
4468
4785
  const defaultBinName = (pkg.name ?? npmPackage).split("/").pop() ?? "";
4469
4786
  const binMap = typeof pkg.bin === "string" ? { [defaultBinName]: pkg.bin } : pkg.bin ?? {};
4470
4787
  const binEntries = Object.entries(binMap);
4471
4788
  for (const [binName, target] of binEntries) {
4472
- if (!existsSync13(join13(pkgDir, target))) return false;
4789
+ if (!existsSync14(join14(pkgDir, target))) return false;
4473
4790
  if (!this.binShimExists(binName)) return false;
4474
4791
  }
4475
- if (binEntries.length === 0 && pkg.main && !existsSync13(join13(pkgDir, pkg.main))) return false;
4792
+ if (binEntries.length === 0 && pkg.main && !existsSync14(join14(pkgDir, pkg.main))) return false;
4476
4793
  return true;
4477
4794
  } catch {
4478
4795
  return false;
@@ -4487,9 +4804,9 @@ var IntegrationManager = class {
4487
4804
  */
4488
4805
  readInstalledVersion(npmPackage) {
4489
4806
  if (!this.isUsableInstall(npmPackage)) return null;
4490
- const pkgJson = join13(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"), "package.json");
4807
+ const pkgJson = join14(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"), "package.json");
4491
4808
  try {
4492
- const pkg = JSON.parse(readFileSync14(pkgJson, "utf8"));
4809
+ const pkg = JSON.parse(readFileSync15(pkgJson, "utf8"));
4493
4810
  return typeof pkg.version === "string" ? pkg.version : null;
4494
4811
  } catch {
4495
4812
  return null;
@@ -4503,7 +4820,7 @@ var IntegrationManager = class {
4503
4820
  binShimExists(binName) {
4504
4821
  if (!binName) return false;
4505
4822
  const candidates = process.platform === "win32" ? [binName, `${binName}.cmd`, `${binName}.CMD`, `${binName}.ps1`] : [binName];
4506
- return candidates.some((c) => existsSync13(join13(INTEGRATIONS_BIN, c)));
4823
+ return candidates.some((c) => existsSync14(join14(INTEGRATIONS_BIN, c)));
4507
4824
  }
4508
4825
  /**
4509
4826
  * The exact version THIS gateway's CURRENTLY LOADED manifest pins
@@ -4545,6 +4862,19 @@ var IntegrationManager = class {
4545
4862
  if (!npmPackage) return;
4546
4863
  ensureIntegrationsDir();
4547
4864
  const pinnedVersion = this.pinnedVersionFor(config);
4865
+ if (OFFLINE_MODE) {
4866
+ const current = this.readInstalledVersion(npmPackage);
4867
+ if (pinnedVersion) {
4868
+ if (current === pinnedVersion) return;
4869
+ throw new Error(
4870
+ `${config.id}: connector ${npmPackage} version ${pinnedVersion} is not part of this installation (found ${current ?? "nothing installed"} at ${INTEGRATIONS_DIR}; SUVEREN_OFFLINE=1 so no npm install was attempted).`
4871
+ );
4872
+ }
4873
+ if (current !== null) return;
4874
+ throw new Error(
4875
+ `${config.id}: connector ${npmPackage} is not part of this installation (nothing installed at ${INTEGRATIONS_DIR}; SUVEREN_OFFLINE=1 so no npm install was attempted).`
4876
+ );
4877
+ }
4548
4878
  if (pinnedVersion) {
4549
4879
  if (this.readInstalledVersion(npmPackage) === pinnedVersion) return;
4550
4880
  return withInstallLock(async () => {
@@ -4580,8 +4910,8 @@ var IntegrationManager = class {
4580
4910
  * nothing at all.
4581
4911
  */
4582
4912
  async installNow(npmPackage, exactVersion, cleanFirst = true) {
4583
- const pkgDir = join13(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
4584
- if (cleanFirst && existsSync13(pkgDir)) {
4913
+ const pkgDir = join14(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
4914
+ if (cleanFirst && existsSync14(pkgDir)) {
4585
4915
  console.error(`[IntegrationManager] Removing ${exactVersion ? "outdated" : "unusable"} install of ${npmPackage}`);
4586
4916
  try {
4587
4917
  rmSync(pkgDir, { recursive: true, force: true });
@@ -5070,28 +5400,28 @@ var IntegrationManager = class {
5070
5400
  };
5071
5401
 
5072
5402
  // src/lib/profile-loader.ts
5073
- import { readFileSync as readFileSync15, existsSync as existsSync14 } from "fs";
5074
- import { join as join14, resolve as resolve2 } from "path";
5403
+ import { readFileSync as readFileSync16, existsSync as existsSync15 } from "fs";
5404
+ import { join as join15, resolve as resolve2 } from "path";
5075
5405
  import { registerProfile } from "@hap/core";
5076
5406
  function loadProfiles(profilesDir) {
5077
- const dir = resolve2(profilesDir ?? process.env.SUVEREN_PROFILES_DIR ?? join14(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "..", "hap-profiles"));
5078
- const indexPath = join14(dir, "index.json");
5079
- if (!existsSync14(indexPath)) {
5407
+ const dir = resolve2(profilesDir ?? process.env.SUVEREN_PROFILES_DIR ?? join15(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "..", "hap-profiles"));
5408
+ const indexPath = join15(dir, "index.json");
5409
+ if (!existsSync15(indexPath)) {
5080
5410
  warnNoProfiles(`No index.json at ${indexPath}`);
5081
5411
  return 0;
5082
5412
  }
5083
5413
  let index;
5084
5414
  try {
5085
- index = JSON.parse(readFileSync15(indexPath, "utf-8"));
5415
+ index = JSON.parse(readFileSync16(indexPath, "utf-8"));
5086
5416
  } catch (err) {
5087
5417
  console.error(`[ProfileLoader] Failed to parse ${indexPath}:`, err);
5088
5418
  return 0;
5089
5419
  }
5090
5420
  let loaded = 0;
5091
5421
  for (const [profileId, relativePath] of Object.entries(index.profiles)) {
5092
- const profilePath = join14(dir, relativePath);
5422
+ const profilePath = join15(dir, relativePath);
5093
5423
  try {
5094
- const profile = JSON.parse(readFileSync15(profilePath, "utf-8"));
5424
+ const profile = JSON.parse(readFileSync16(profilePath, "utf-8"));
5095
5425
  registerProfile(profileId, profile);
5096
5426
  const shortName = profileId.split("/").pop()?.replace(/@.*$/, "");
5097
5427
  if (shortName && shortName !== profileId) {
@@ -5127,8 +5457,8 @@ function warnNoProfiles(reason) {
5127
5457
  import { decodeAttestationBlob as decodeAttestationBlob3 } from "@hap/core";
5128
5458
 
5129
5459
  // src/lib/committed-executor.ts
5130
- import { existsSync as existsSync15, mkdirSync as mkdirSync11, readFileSync as readFileSync16, unlinkSync as unlinkSync6, writeFileSync as writeFileSync11 } from "fs";
5131
- import { dirname as dirname10, join as join15 } from "path";
5460
+ import { existsSync as existsSync16, mkdirSync as mkdirSync11, readFileSync as readFileSync17, unlinkSync as unlinkSync6, writeFileSync as writeFileSync11 } from "fs";
5461
+ import { dirname as dirname10, join as join16 } from "path";
5132
5462
  var CommittedExecutor = class {
5133
5463
  constructor(run) {
5134
5464
  this.run = run;
@@ -5156,7 +5486,7 @@ var ExecutorLock = class {
5156
5486
  path;
5157
5487
  held = false;
5158
5488
  constructor(dataDir2) {
5159
- this.path = join15(dataDir2, "executor.lock");
5489
+ this.path = join16(dataDir2, "executor.lock");
5160
5490
  }
5161
5491
  /** Try to take the lock. Returns the holder's pid when someone else has it. */
5162
5492
  acquire() {
@@ -5188,9 +5518,9 @@ var ExecutorLock = class {
5188
5518
  }
5189
5519
  }
5190
5520
  read() {
5191
- if (!existsSync15(this.path)) return null;
5521
+ if (!existsSync16(this.path)) return null;
5192
5522
  try {
5193
- const parsed = JSON.parse(readFileSync16(this.path, "utf8"));
5523
+ const parsed = JSON.parse(readFileSync17(this.path, "utf8"));
5194
5524
  return typeof parsed?.pid === "number" ? parsed : null;
5195
5525
  } catch {
5196
5526
  return null;
@@ -5208,19 +5538,19 @@ function isAlive(pid) {
5208
5538
 
5209
5539
  // bin/http.ts
5210
5540
  import { homedir as homedir10 } from "os";
5211
- import { join as join17 } from "path";
5541
+ import { join as join18 } from "path";
5212
5542
 
5213
5543
  // src/lib/mcp-as-tracker.ts
5214
- import { existsSync as existsSync16, mkdirSync as mkdirSync12, readFileSync as readFileSync17, writeFileSync as writeFileSync12 } from "fs";
5215
- import { dirname as dirname11, join as join16 } from "path";
5544
+ import { existsSync as existsSync17, mkdirSync as mkdirSync12, readFileSync as readFileSync18, writeFileSync as writeFileSync12 } from "fs";
5545
+ import { dirname as dirname11, join as join17 } from "path";
5216
5546
  function trackerPath(dataDir2) {
5217
- return join16(dataDir2, "mcp-as-pairing.json");
5547
+ return join17(dataDir2, "mcp-as-pairing.json");
5218
5548
  }
5219
5549
  function readMcpPairedAsUrl(dataDir2) {
5220
5550
  const path = trackerPath(dataDir2);
5221
- if (!existsSync16(path)) return null;
5551
+ if (!existsSync17(path)) return null;
5222
5552
  try {
5223
- const data = JSON.parse(readFileSync17(path, "utf-8"));
5553
+ const data = JSON.parse(readFileSync18(path, "utf-8"));
5224
5554
  return typeof data.asUrl === "string" ? data.asUrl : null;
5225
5555
  } catch {
5226
5556
  return null;
@@ -5233,7 +5563,7 @@ function writeMcpPairedAsUrl(dataDir2, asUrl) {
5233
5563
  }
5234
5564
 
5235
5565
  // bin/http.ts
5236
- var dataDir = process.env.SUVEREN_DATA_DIR ?? join17(homedir10(), ".suveren");
5566
+ var dataDir = process.env.SUVEREN_DATA_DIR ?? join18(homedir10(), ".suveren");
5237
5567
  var spUrl = resolveAsUrl(dataDir);
5238
5568
  validatePinTlsForUrl(resolvePinTls(dataDir), spUrl);
5239
5569
  var port = parseInt(process.env.SUVEREN_MCP_PORT ?? "3430", 10);
@@ -5760,7 +6090,7 @@ app.listen(port, "0.0.0.0", () => {
5760
6090
  (proposal) => executeCommitted(proposal, state, integrationManager)
5761
6091
  );
5762
6092
  installCommittedExecutor(committedExecutor);
5763
- const dataDir2 = process.env.SUVEREN_DATA_DIR ?? join17(homedir10(), ".suveren");
6093
+ const dataDir2 = process.env.SUVEREN_DATA_DIR ?? join18(homedir10(), ".suveren");
5764
6094
  const executorLock = new ExecutorLock(dataDir2);
5765
6095
  const lock = executorLock.acquire();
5766
6096
  if (!lock.held) {