@suveren/gateway 0.12.0 → 0.13.1

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.
Files changed (38) hide show
  1. package/README.md +53 -3
  2. package/bin/suveren-gateway.js +128 -12
  3. package/dist/control-plane/index.mjs +555 -122
  4. package/dist/mcp-server/http.mjs +647 -273
  5. package/dist/ui/assets/{index-BzcgeHlq.js → index-CmLqzgqA.js} +21 -21
  6. package/dist/ui/assets/{index-BFOprnFZ.css → index-y1OFeYnS.css} +1 -1
  7. package/dist/ui/index.html +2 -2
  8. package/lib/autostart-templates.d.mts +6 -0
  9. package/lib/autostart-templates.mjs +21 -4
  10. package/lib/config.d.mts +10 -0
  11. package/lib/config.mjs +79 -18
  12. package/lib/policy.d.mts +34 -0
  13. package/lib/policy.mjs +369 -0
  14. package/node_modules/@hap/core/dist/index.d.mts +49 -1
  15. package/node_modules/@hap/core/dist/index.d.ts +49 -1
  16. package/node_modules/@hap/core/dist/index.js +50 -0
  17. package/node_modules/@hap/core/dist/index.mjs +49 -0
  18. package/node_modules/@hap/core/package.json +1 -1
  19. package/node_modules/@hap/core/src/frame.ts +63 -1
  20. package/node_modules/@hap/core/src/gatekeeper.ts +37 -0
  21. package/node_modules/@hap/core/src/types.ts +29 -1
  22. package/node_modules/hono/dist/cjs/hono-base.js +2 -0
  23. package/node_modules/hono/dist/cjs/middleware/language/language.js +1 -1
  24. package/node_modules/hono/dist/cjs/middleware/mount/index.js +71 -0
  25. package/node_modules/hono/dist/cjs/request.js +4 -4
  26. package/node_modules/hono/dist/hono-base.js +2 -0
  27. package/node_modules/hono/dist/middleware/mount/index.js +70 -0
  28. package/node_modules/hono/dist/request.js +4 -4
  29. package/node_modules/hono/dist/types/hono-base.d.ts +2 -0
  30. package/node_modules/hono/dist/types/jsx/base.d.ts +1 -1
  31. package/node_modules/hono/dist/types/middleware/mount/index.d.ts +46 -0
  32. package/node_modules/hono/dist/types/request.d.ts +4 -4
  33. package/node_modules/hono/package.json +9 -1
  34. package/package.json +2 -2
  35. package/profiles/index.json +3 -1
  36. package/profiles/purchase/0.6.profile.json +234 -0
  37. package/profiles/sales/0.3.profile.json +290 -0
  38. package/server.js +51 -8
@@ -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;
@@ -1888,7 +2202,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1888
2202
  import { z as z2 } from "zod";
1889
2203
 
1890
2204
  // src/lib/mandate-brief.ts
1891
- import { getProfile } from "@hap/core";
2205
+ import { getProfile as getProfile3 } from "@hap/core";
1892
2206
 
1893
2207
  // src/lib/consumption.ts
1894
2208
  import { boundActionTypes } from "@hap/core";
@@ -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;
@@ -1982,87 +2296,16 @@ function getContextForBrief(dataDir2) {
1982
2296
  return { brief: truncated, truncated: true };
1983
2297
  }
1984
2298
 
1985
- // src/lib/mandate-brief.ts
1986
- function shortProfileName(profileId) {
1987
- const withoutVersion = profileId.replace(/@.*$/, "");
1988
- const parts = withoutVersion.split("/");
1989
- return parts[parts.length - 1];
1990
- }
1991
- function countToolsByGating(profileId, integrationManager2) {
1992
- if (!integrationManager2) return { gated: 0, readOnly: 0 };
1993
- const allTools = integrationManager2.getAllTools();
1994
- let gated = 0;
1995
- let readOnly = 0;
1996
- for (const tool of allTools) {
1997
- if (!tool.gating || !tool.gating.profile) {
1998
- readOnly++;
1999
- } else if (tool.gating.profile === profileId || tool.gating.profile === shortProfileName(profileId)) {
2000
- if (tool.gating.staticExecution?.action_type === "read" && Object.keys(tool.gating.executionMapping ?? {}).length === 0) {
2001
- readOnly++;
2002
- } else {
2003
- gated++;
2004
- }
2005
- }
2006
- }
2007
- return { gated, readOnly };
2299
+ // src/lib/simulation-mode.ts
2300
+ function isSimulationMode() {
2301
+ const policySimulation = readPolicy().policy.simulation;
2302
+ if (policySimulation !== void 0) return policySimulation;
2303
+ return process.env.SUVEREN_SIMULATION === "1";
2008
2304
  }
2009
- function buildMandateBrief(opts) {
2010
- const { authorizations, executionLog, integrationManager: integrationManager2, contextDir } = opts;
2011
- const lines = [
2012
- "You are connected to Suveren \u2014 the gateway that gates every privileged tool call you make.",
2013
- "Suveren implements the bounded-authority model from the open Human Agency Protocol (HAP).",
2014
- "You have bounded authorities granted by human decision owners.",
2015
- "You MUST stay within these bounds \u2014 the Gatekeeper will reject actions that exceed them."
2016
- ];
2017
- const { brief: contextBrief } = getContextForBrief(contextDir);
2018
- if (contextBrief) {
2019
- lines.push("");
2020
- lines.push("=== CONTEXT ===");
2021
- lines.push("");
2022
- lines.push(contextBrief);
2023
- }
2024
- const active = authorizations.filter((a) => a.complete);
2025
- const pending = authorizations.filter((a) => !a.complete);
2026
- const now = Math.floor(Date.now() / 1e3);
2027
- if (active.length > 0) {
2028
- lines.push("");
2029
- lines.push("=== ACTIVE AUTHORITIES ===");
2030
- lines.push("");
2031
- for (const auth of active) {
2032
- const earliestExpiry = Math.min(...auth.attestations.map((a) => a.expiresAt));
2033
- const remainingMin = Math.max(0, Math.round((earliestExpiry - now) / 60));
2034
- const shortName = shortProfileName(auth.profileId);
2035
- const version = auth.profileId.match(/@(.+)$/)?.[1] ?? "";
2036
- const shortId = version ? `${shortName}@${version}` : shortName;
2037
- const boundsDesc = Object.entries(auth.frame).filter(([key]) => key !== "profile" && key !== "path").map(([key, value]) => `${key}:${value}`).join(" \xB7 ");
2038
- const parts = [];
2039
- if (boundsDesc) parts.push(boundsDesc);
2040
- if (executionLog) {
2041
- const profile = getProfile(auth.profileId) ?? getProfile(shortName);
2042
- const consumption = getConsumptionState(auth, executionLog, profile);
2043
- const compact = formatConsumptionCompact(consumption);
2044
- if (compact) parts.push(compact);
2045
- }
2046
- const { gated, readOnly } = countToolsByGating(auth.profileId, integrationManager2);
2047
- if (gated > 0 || readOnly > 0) parts.push(`${gated} gated tools`);
2048
- parts.push(`${remainingMin} min remaining`);
2049
- lines.push(`[${shortId}] ${parts.join(" \xB7 ")}`);
2050
- lines.push(` \u2192 list-authorizations(domain: "${shortName}") for intent & details`);
2051
- lines.push("");
2052
- }
2053
- }
2054
- if (pending.length > 0) {
2055
- lines.push("=== PENDING (awaiting attestations) ===");
2056
- lines.push("");
2057
- for (const auth of pending) {
2058
- const missing = auth.requiredDomains.filter((d) => !auth.attestedDomains.includes(d));
2059
- lines.push(`[${auth.path}] ${auth.profileId} \u2014 needs: ${missing.join(", ")}`);
2060
- }
2061
- lines.push("");
2062
- }
2063
- lines.push("When you receive a task, call list-authorizations(domain) to load full details for the relevant domain.");
2064
- return lines.join("\n");
2305
+ function manifestIsSimulated(manifest) {
2306
+ return Boolean(manifest?.simulation);
2065
2307
  }
2308
+ var SIMULATION_BLOCK_REASON = "blocked: simulation mode \u2014 real systems are off";
2066
2309
 
2067
2310
  // src/lib/locked-notice.ts
2068
2311
  function uiUrl() {
@@ -2112,9 +2355,6 @@ function lockedNotice(action, reason = "restart") {
2112
2355
  }
2113
2356
  }
2114
2357
 
2115
- // src/tools/authorizations.ts
2116
- import { getProfile as getProfile4 } from "@hap/core";
2117
-
2118
2358
  // src/lib/receipt-footer.ts
2119
2359
  import { deriveIdentityLine } from "@hap/core";
2120
2360
  var AS_BASE = (process.env.SUVEREN_AS_URL ?? "https://www.suveren.ai").replace(/\/$/, "");
@@ -2176,10 +2416,10 @@ import {
2176
2416
  selectBoundFields,
2177
2417
  isFieldBinding,
2178
2418
  bindingAppliesTo,
2179
- getProfile as getProfile2
2419
+ getProfile
2180
2420
  } from "@hap/core";
2181
2421
  function getContentBinding(profileId) {
2182
- return getProfile2(profileId)?.content_binding;
2422
+ return getProfile(profileId)?.content_binding;
2183
2423
  }
2184
2424
  function computeContentBinding(profileId, tool, toolArgs, actionType) {
2185
2425
  const binding = getContentBinding(profileId);
@@ -2599,14 +2839,14 @@ function clampAgeFloor(requested, maxAgeDays, nowMs, format = "iso") {
2599
2839
  }
2600
2840
 
2601
2841
  // src/lib/tool-proxy.ts
2602
- import { getProfile as getProfile3, ContentBindingError, boundActionTypes as boundActionTypes2 } from "@hap/core";
2842
+ import { getProfile as getProfile2, ContentBindingError, boundActionTypes as boundActionTypes2 } from "@hap/core";
2603
2843
  import { readFile } from "fs/promises";
2604
2844
  import { extname } from "path";
2605
2845
  import { randomUUID } from "crypto";
2606
2846
 
2607
2847
  // src/lib/manifest-loader.ts
2608
- import { readFileSync as readFileSync12, existsSync as existsSync11 } from "fs";
2609
- import { join as join11, resolve } from "path";
2848
+ import { readFileSync as readFileSync13, existsSync as existsSync12 } from "fs";
2849
+ import { join as join12, resolve } from "path";
2610
2850
  var EXACT_SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
2611
2851
  function isExactSemver(value) {
2612
2852
  return typeof value === "string" && EXACT_SEMVER_RE.test(value);
@@ -2624,25 +2864,25 @@ function invalidNpmPinReason(manifest) {
2624
2864
  var manifests = /* @__PURE__ */ new Map();
2625
2865
  function loadManifests(integrationsDir) {
2626
2866
  const dir = resolve(
2627
- integrationsDir ?? process.env.SUVEREN_MANIFESTS_DIR ?? join11(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "content", "integrations")
2867
+ integrationsDir ?? process.env.SUVEREN_MANIFESTS_DIR ?? join12(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "content", "integrations")
2628
2868
  );
2629
- const indexPath = join11(dir, "index.json");
2630
- if (!existsSync11(indexPath)) {
2869
+ const indexPath = join12(dir, "index.json");
2870
+ if (!existsSync12(indexPath)) {
2631
2871
  console.error(`[ManifestLoader] No index.json found at ${indexPath}, skipping manifest loading`);
2632
2872
  return 0;
2633
2873
  }
2634
2874
  let index;
2635
2875
  try {
2636
- index = JSON.parse(readFileSync12(indexPath, "utf-8"));
2876
+ index = JSON.parse(readFileSync13(indexPath, "utf-8"));
2637
2877
  } catch (err) {
2638
2878
  console.error(`[ManifestLoader] Failed to parse ${indexPath}:`, err);
2639
2879
  return 0;
2640
2880
  }
2641
2881
  let loaded = 0;
2642
2882
  for (const [id, relativePath] of Object.entries(index.integrations)) {
2643
- const manifestPath = join11(dir, relativePath);
2883
+ const manifestPath = join12(dir, relativePath);
2644
2884
  try {
2645
- const manifest = JSON.parse(readFileSync12(manifestPath, "utf-8"));
2885
+ const manifest = JSON.parse(readFileSync13(manifestPath, "utf-8"));
2646
2886
  const pinReason = invalidNpmPinReason(manifest);
2647
2887
  if (pinReason) {
2648
2888
  console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${pinReason}`);
@@ -2664,15 +2904,6 @@ function getAllManifests() {
2664
2904
  return Array.from(manifests.values());
2665
2905
  }
2666
2906
 
2667
- // src/lib/simulation-mode.ts
2668
- function isSimulationMode() {
2669
- return process.env.SUVEREN_SIMULATION === "1";
2670
- }
2671
- function manifestIsSimulated(manifest) {
2672
- return Boolean(manifest?.simulation);
2673
- }
2674
- var SIMULATION_BLOCK_REASON = "blocked: simulation mode \u2014 real systems are off";
2675
-
2676
2907
  // src/lib/tool-proxy.ts
2677
2908
  var IMAGE_MIME = {
2678
2909
  ".jpg": "image/jpeg",
@@ -2734,7 +2965,7 @@ function profileMatches(profileId, shortName) {
2734
2965
  return profileId === shortName || profileId.includes("/" + shortName + "@") || profileId.endsWith("/" + shortName);
2735
2966
  }
2736
2967
  function countBoundsFor(profileId, actionType) {
2737
- const fields = getProfile3(profileId)?.boundsSchema?.fields;
2968
+ const fields = getProfile2(profileId)?.boundsSchema?.fields;
2738
2969
  if (!fields) return null;
2739
2970
  return Object.entries(fields).filter(([, def]) => def.boundType?.kind === "cumulative_count").filter(([name, def]) => {
2740
2971
  const applies = boundActionTypes2(name, def);
@@ -2794,7 +3025,7 @@ function createGatedToolHandler(tool, integrationManager2, state2) {
2794
3025
  return async () => ({
2795
3026
  content: [{
2796
3027
  type: "text",
2797
- text: `Refused: "${tool.namespacedName}" talks to a real system, but this gateway is running in simulation mode (real systems are off). No ticket was requested.`
3028
+ text: `Refused: "${tool.namespacedName}" is not available. No ticket was requested.`
2798
3029
  }],
2799
3030
  isError: true
2800
3031
  });
@@ -2815,6 +3046,17 @@ function createGatedToolHandler(tool, integrationManager2, state2) {
2815
3046
  };
2816
3047
  };
2817
3048
  }
3049
+ function isStaleMandateRefusal(err) {
3050
+ const errors = err.body?.errors;
3051
+ const code = errors?.[0]?.code;
3052
+ if (err.statusCode === 403 && (code === "ATTESTATION_REVOKED" || code === "ATTESTATION_EXPIRED")) {
3053
+ return true;
3054
+ }
3055
+ if (err.statusCode === 404 && code === "ATTESTATION_NOT_FOUND") {
3056
+ return true;
3057
+ }
3058
+ return false;
3059
+ }
2818
3060
  function createGatedToolHandlerInner(tool, integrationManager2, state2) {
2819
3061
  if (!tool.gating || !tool.gating.profile) {
2820
3062
  return async () => {
@@ -2886,7 +3128,7 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
2886
3128
  );
2887
3129
  }
2888
3130
  }
2889
- const readProfile = readAdapter ? getProfile3(matchingAuths[0].profileId) : void 0;
3131
+ const readProfile = readAdapter ? getProfile2(matchingAuths[0].profileId) : void 0;
2890
3132
  const boundsSchema = readProfile?.boundsSchema;
2891
3133
  const ageBoundField = readAdapter?.ageField ? resolveAgeBoundField(boundsSchema, readAdapter.ageField) : null;
2892
3134
  const hasAgeDimension = Boolean(readAdapter?.ageField);
@@ -3042,7 +3284,7 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
3042
3284
  };
3043
3285
  }
3044
3286
  {
3045
- const profileDef = getProfile3(profile);
3287
+ const profileDef = getProfile2(profile);
3046
3288
  const registry = profileDef?.boundsSchema?.actionTypes;
3047
3289
  if (Array.isArray(registry) && registry.length > 0 && !registry.includes(declaredActionType)) {
3048
3290
  return {
@@ -3099,23 +3341,24 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
3099
3341
  isError: true
3100
3342
  };
3101
3343
  }
3102
- const contextKeys = getProfile3(passers[0].profileId)?.contextSchema?.keyOrder ?? [];
3103
- const selection = selectAuthorization(
3104
- contextKeys,
3105
- passers.map((a) => ({
3106
- id: a.authorizationId,
3107
- auth: a,
3108
- context: a.context ?? {},
3109
- requiresApproval: (a.deferredCommitmentDomains ?? []).length > 0
3110
- }))
3111
- );
3112
- const auth = selection.chosen.auth;
3113
- if (selection.superseded.length > 0) {
3114
- console.error(
3115
- `[Suveren MCP] selection(${tool.namespacedName}): chose ${auth.authorizationId} (${selection.reason}) over [${selection.superseded.map((s) => s.id).join(", ")}]`
3344
+ const contextKeys = getProfile2(passers[0].profileId)?.contextSchema?.keyOrder ?? [];
3345
+ return attemptWithCandidates(passers);
3346
+ async function attemptWithCandidates(candidates) {
3347
+ const selection = selectAuthorization(
3348
+ contextKeys,
3349
+ candidates.map((a) => ({
3350
+ id: a.authorizationId,
3351
+ auth: a,
3352
+ context: a.context ?? {},
3353
+ requiresApproval: (a.deferredCommitmentDomains ?? []).length > 0
3354
+ }))
3116
3355
  );
3117
- }
3118
- {
3356
+ const auth = selection.chosen.auth;
3357
+ if (selection.superseded.length > 0) {
3358
+ console.error(
3359
+ `[Suveren MCP] selection(${tool.namespacedName}): chose ${auth.authorizationId} (${selection.reason}) over [${selection.superseded.map((s) => s.id).join(", ")}]`
3360
+ );
3361
+ }
3119
3362
  const authzId = auth.authorizationId;
3120
3363
  if (isCommitmentDowngrade(auth)) {
3121
3364
  return {
@@ -3228,6 +3471,24 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
3228
3471
  isError: true
3229
3472
  };
3230
3473
  }
3474
+ if (err instanceof SPReceiptError && isStaleMandateRefusal(err)) {
3475
+ state2.cache.invalidate(auth.authorizationId);
3476
+ const code = err.body?.errors?.[0]?.code ?? "unknown";
3477
+ const remaining = candidates.filter((c) => c.authorizationId !== auth.authorizationId);
3478
+ if (remaining.length === 0) {
3479
+ console.error(
3480
+ `[Suveren MCP] fallback(${tool.namespacedName}): mandate ${auth.authorizationId} invalid (${code}) \u2014 no remaining candidates`
3481
+ );
3482
+ return {
3483
+ content: [{ type: "text", text: `Blocked by SP: ${err.message}` }],
3484
+ isError: true
3485
+ };
3486
+ }
3487
+ console.error(
3488
+ `[Suveren MCP] fallback(${tool.namespacedName}): mandate ${auth.authorizationId} invalid (${code}) \u2192 retrying with [${remaining.map((c) => c.authorizationId).join(", ")}]`
3489
+ );
3490
+ return attemptWithCandidates(remaining);
3491
+ }
3231
3492
  if (err instanceof SPReceiptError && err.statusCode === 409) {
3232
3493
  const spBody = err.body;
3233
3494
  let pendingApprovers = spBody.approvers ?? [];
@@ -3384,7 +3645,106 @@ function buildProxiedToolDescription(tool, state2) {
3384
3645
  return `[Suveren: ${profile} \u2014 ${tag}] ${tool.description}`;
3385
3646
  }
3386
3647
 
3648
+ // src/lib/agent-visibility.ts
3649
+ function agentVisibleAuthorizations(authorizations, integrationManager2) {
3650
+ if (!isSimulationMode()) return authorizations;
3651
+ if (!integrationManager2) return [];
3652
+ const runningProfiles = /* @__PURE__ */ new Set();
3653
+ for (const tool of integrationManager2.getAllTools()) {
3654
+ const profile = tool.gating?.profile;
3655
+ if (profile) runningProfiles.add(profile);
3656
+ }
3657
+ if (runningProfiles.size === 0) return [];
3658
+ return authorizations.filter(
3659
+ (auth) => [...runningProfiles].some((profile) => profileMatches(auth.profileId, profile))
3660
+ );
3661
+ }
3662
+
3663
+ // src/lib/mandate-brief.ts
3664
+ function shortProfileName(profileId) {
3665
+ const withoutVersion = profileId.replace(/@.*$/, "");
3666
+ const parts = withoutVersion.split("/");
3667
+ return parts[parts.length - 1];
3668
+ }
3669
+ function countToolsByGating(profileId, integrationManager2) {
3670
+ if (!integrationManager2) return { gated: 0, readOnly: 0 };
3671
+ const allTools = integrationManager2.getAllTools();
3672
+ let gated = 0;
3673
+ let readOnly = 0;
3674
+ for (const tool of allTools) {
3675
+ if (!tool.gating || !tool.gating.profile) {
3676
+ readOnly++;
3677
+ } else if (tool.gating.profile === profileId || tool.gating.profile === shortProfileName(profileId)) {
3678
+ if (tool.gating.staticExecution?.action_type === "read" && Object.keys(tool.gating.executionMapping ?? {}).length === 0) {
3679
+ readOnly++;
3680
+ } else {
3681
+ gated++;
3682
+ }
3683
+ }
3684
+ }
3685
+ return { gated, readOnly };
3686
+ }
3687
+ function buildMandateBrief(opts) {
3688
+ const { executionLog, integrationManager: integrationManager2, contextDir } = opts;
3689
+ const authorizations = agentVisibleAuthorizations(opts.authorizations, integrationManager2);
3690
+ const lines = [
3691
+ "You are connected to Suveren \u2014 the gateway that gates every privileged tool call you make.",
3692
+ "Suveren implements the bounded-authority model from the open Human Agency Protocol (HAP).",
3693
+ "You have bounded authorities granted by human decision owners.",
3694
+ "You MUST stay within these bounds \u2014 the Gatekeeper will reject actions that exceed them."
3695
+ ];
3696
+ const { brief: contextBrief } = getContextForBrief(contextDir);
3697
+ if (contextBrief) {
3698
+ lines.push("");
3699
+ lines.push("=== CONTEXT ===");
3700
+ lines.push("");
3701
+ lines.push(contextBrief);
3702
+ }
3703
+ const active = authorizations.filter((a) => a.complete);
3704
+ const pending = authorizations.filter((a) => !a.complete);
3705
+ const now = Math.floor(Date.now() / 1e3);
3706
+ if (active.length > 0) {
3707
+ lines.push("");
3708
+ lines.push("=== ACTIVE AUTHORITIES ===");
3709
+ lines.push("");
3710
+ for (const auth of active) {
3711
+ const earliestExpiry = Math.min(...auth.attestations.map((a) => a.expiresAt));
3712
+ const remainingMin = Math.max(0, Math.round((earliestExpiry - now) / 60));
3713
+ const shortName = shortProfileName(auth.profileId);
3714
+ const version = auth.profileId.match(/@(.+)$/)?.[1] ?? "";
3715
+ const shortId = version ? `${shortName}@${version}` : shortName;
3716
+ const boundsDesc = Object.entries(auth.frame).filter(([key]) => key !== "profile" && key !== "path").map(([key, value]) => `${key}:${value}`).join(" \xB7 ");
3717
+ const parts = [];
3718
+ if (boundsDesc) parts.push(boundsDesc);
3719
+ if (executionLog) {
3720
+ const profile = getProfile3(auth.profileId) ?? getProfile3(shortName);
3721
+ const consumption = getConsumptionState(auth, executionLog, profile);
3722
+ const compact = formatConsumptionCompact(consumption);
3723
+ if (compact) parts.push(compact);
3724
+ }
3725
+ const { gated, readOnly } = countToolsByGating(auth.profileId, integrationManager2);
3726
+ if (gated > 0 || readOnly > 0) parts.push(`${gated} gated tools`);
3727
+ parts.push(`${remainingMin} min remaining`);
3728
+ lines.push(`[${shortId}] ${parts.join(" \xB7 ")}`);
3729
+ lines.push(` \u2192 list-authorizations(domain: "${shortName}") for intent & details`);
3730
+ lines.push("");
3731
+ }
3732
+ }
3733
+ if (pending.length > 0) {
3734
+ lines.push("=== PENDING (awaiting attestations) ===");
3735
+ lines.push("");
3736
+ for (const auth of pending) {
3737
+ const missing = auth.requiredDomains.filter((d) => !auth.attestedDomains.includes(d));
3738
+ lines.push(`[${auth.path}] ${auth.profileId} \u2014 needs: ${missing.join(", ")}`);
3739
+ }
3740
+ lines.push("");
3741
+ }
3742
+ lines.push("When you receive a task, call list-authorizations(domain) to load full details for the relevant domain.");
3743
+ return lines.join("\n");
3744
+ }
3745
+
3387
3746
  // src/tools/authorizations.ts
3747
+ import { getProfile as getProfile4 } from "@hap/core";
3388
3748
  function shortProfileName2(profileId) {
3389
3749
  const withoutVersion = profileId.replace(/@.*$/, "");
3390
3750
  const parts = withoutVersion.split("/");
@@ -3445,7 +3805,7 @@ function listAuthorizationsHandler(state2, integrationManager2, contextDir) {
3445
3805
  if (!state2.spClient.isUnlocked()) {
3446
3806
  return { content: [{ type: "text", text: lockedNotice("list authorizations", state2.spClient.getLockReason() ?? "restart") }] };
3447
3807
  }
3448
- const authorizations = state2.getEnrichedAuthorizations();
3808
+ const authorizations = agentVisibleAuthorizations(state2.getEnrichedAuthorizations(), integrationManager2);
3449
3809
  const now = Math.floor(Date.now() / 1e3);
3450
3810
  const domain = args?.domain;
3451
3811
  if (authorizations.length === 0) {
@@ -4195,16 +4555,16 @@ function verifyGateContentHashes(content, auth) {
4195
4555
  }
4196
4556
 
4197
4557
  // src/lib/integration-registry.ts
4198
- import { mkdirSync as mkdirSync9, readFileSync as readFileSync13, writeFileSync as writeFileSync9, existsSync as existsSync12 } from "fs";
4558
+ import { mkdirSync as mkdirSync9, readFileSync as readFileSync14, writeFileSync as writeFileSync9, existsSync as existsSync13 } from "fs";
4199
4559
  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");
4560
+ import { dirname as dirname9, join as join13 } from "path";
4561
+ var DEFAULT_DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join13(homedir8(), ".suveren");
4202
4562
  var IntegrationRegistry = class {
4203
4563
  integrations = /* @__PURE__ */ new Map();
4204
4564
  filePath;
4205
4565
  constructor(dataDir2) {
4206
4566
  const dir = dataDir2 ?? DEFAULT_DATA_DIR;
4207
- this.filePath = join12(dir, "integrations.json");
4567
+ this.filePath = join13(dir, "integrations.json");
4208
4568
  this.load();
4209
4569
  }
4210
4570
  // ─── CRUD ───────────────────────────────────────────────────────────────
@@ -4235,12 +4595,12 @@ var IntegrationRegistry = class {
4235
4595
  }
4236
4596
  // ─── Load / Save ────────────────────────────────────────────────────────
4237
4597
  load() {
4238
- if (!existsSync12(this.filePath)) {
4598
+ if (!existsSync13(this.filePath)) {
4239
4599
  mkdirSync9(dirname9(this.filePath), { recursive: true });
4240
4600
  return;
4241
4601
  }
4242
4602
  try {
4243
- const raw = readFileSync13(this.filePath, "utf-8");
4603
+ const raw = readFileSync14(this.filePath, "utf-8");
4244
4604
  const data = JSON.parse(raw);
4245
4605
  const migrated = data.integrations.map((i) => {
4246
4606
  if ("toolGating" in i && !("profile" in i)) {
@@ -4276,8 +4636,8 @@ var IntegrationRegistry = class {
4276
4636
 
4277
4637
  // src/lib/integration-manager.ts
4278
4638
  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";
4639
+ import { join as join14, delimiter } from "path";
4640
+ import { existsSync as existsSync14, writeFileSync as writeFileSync10, mkdirSync as mkdirSync10, readFileSync as readFileSync15, rmSync } from "fs";
4281
4641
  import { execFile } from "child_process";
4282
4642
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
4283
4643
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
@@ -4354,25 +4714,26 @@ function safeHost(url) {
4354
4714
  }
4355
4715
 
4356
4716
  // 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");
4717
+ var DEFAULT_DATA_DIR2 = process.env.SUVEREN_DATA_DIR ?? join14(homedir9(), ".suveren");
4718
+ var INTEGRATIONS_DIR = process.env.SUVEREN_INTEGRATIONS_DIR ?? join14(DEFAULT_DATA_DIR2, "integrations");
4719
+ var OFFLINE_MODE = process.env.SUVEREN_OFFLINE === "1";
4359
4720
  var installLock = Promise.resolve();
4360
4721
  function withInstallLock(task) {
4361
4722
  const run = installLock.then(task, task);
4362
4723
  installLock = run.then(() => void 0, () => void 0);
4363
4724
  return run;
4364
4725
  }
4365
- var INTEGRATIONS_BIN = join13(INTEGRATIONS_DIR, "node_modules", ".bin");
4726
+ var INTEGRATIONS_BIN = join14(INTEGRATIONS_DIR, "node_modules", ".bin");
4366
4727
  function buildPath() {
4367
4728
  const base = process.env.PATH ?? "";
4368
4729
  return [INTEGRATIONS_BIN, base].join(delimiter);
4369
4730
  }
4370
4731
  function ensureIntegrationsDir() {
4371
- if (!existsSync13(INTEGRATIONS_DIR)) {
4732
+ if (!existsSync14(INTEGRATIONS_DIR)) {
4372
4733
  mkdirSync10(INTEGRATIONS_DIR, { recursive: true });
4373
4734
  }
4374
- const pkgPath = join13(INTEGRATIONS_DIR, "package.json");
4375
- if (!existsSync13(pkgPath)) {
4735
+ const pkgPath = join14(INTEGRATIONS_DIR, "package.json");
4736
+ if (!existsSync14(pkgPath)) {
4376
4737
  writeFileSync10(pkgPath, JSON.stringify({ name: "suveren-integrations", version: "1.0.0", private: true }, null, 2));
4377
4738
  }
4378
4739
  }
@@ -4460,19 +4821,19 @@ var IntegrationManager = class {
4460
4821
  * exist, and treat anything else as not-installed (and reinstallable).
4461
4822
  */
4462
4823
  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;
4824
+ const pkgDir = join14(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
4825
+ const pkgJson = join14(pkgDir, "package.json");
4826
+ if (!existsSync14(pkgDir) || !existsSync14(pkgJson)) return false;
4466
4827
  try {
4467
- const pkg = JSON.parse(readFileSync14(pkgJson, "utf8"));
4828
+ const pkg = JSON.parse(readFileSync15(pkgJson, "utf8"));
4468
4829
  const defaultBinName = (pkg.name ?? npmPackage).split("/").pop() ?? "";
4469
4830
  const binMap = typeof pkg.bin === "string" ? { [defaultBinName]: pkg.bin } : pkg.bin ?? {};
4470
4831
  const binEntries = Object.entries(binMap);
4471
4832
  for (const [binName, target] of binEntries) {
4472
- if (!existsSync13(join13(pkgDir, target))) return false;
4833
+ if (!existsSync14(join14(pkgDir, target))) return false;
4473
4834
  if (!this.binShimExists(binName)) return false;
4474
4835
  }
4475
- if (binEntries.length === 0 && pkg.main && !existsSync13(join13(pkgDir, pkg.main))) return false;
4836
+ if (binEntries.length === 0 && pkg.main && !existsSync14(join14(pkgDir, pkg.main))) return false;
4476
4837
  return true;
4477
4838
  } catch {
4478
4839
  return false;
@@ -4487,9 +4848,9 @@ var IntegrationManager = class {
4487
4848
  */
4488
4849
  readInstalledVersion(npmPackage) {
4489
4850
  if (!this.isUsableInstall(npmPackage)) return null;
4490
- const pkgJson = join13(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"), "package.json");
4851
+ const pkgJson = join14(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"), "package.json");
4491
4852
  try {
4492
- const pkg = JSON.parse(readFileSync14(pkgJson, "utf8"));
4853
+ const pkg = JSON.parse(readFileSync15(pkgJson, "utf8"));
4493
4854
  return typeof pkg.version === "string" ? pkg.version : null;
4494
4855
  } catch {
4495
4856
  return null;
@@ -4503,7 +4864,7 @@ var IntegrationManager = class {
4503
4864
  binShimExists(binName) {
4504
4865
  if (!binName) return false;
4505
4866
  const candidates = process.platform === "win32" ? [binName, `${binName}.cmd`, `${binName}.CMD`, `${binName}.ps1`] : [binName];
4506
- return candidates.some((c) => existsSync13(join13(INTEGRATIONS_BIN, c)));
4867
+ return candidates.some((c) => existsSync14(join14(INTEGRATIONS_BIN, c)));
4507
4868
  }
4508
4869
  /**
4509
4870
  * The exact version THIS gateway's CURRENTLY LOADED manifest pins
@@ -4545,6 +4906,19 @@ var IntegrationManager = class {
4545
4906
  if (!npmPackage) return;
4546
4907
  ensureIntegrationsDir();
4547
4908
  const pinnedVersion = this.pinnedVersionFor(config);
4909
+ if (OFFLINE_MODE) {
4910
+ const current = this.readInstalledVersion(npmPackage);
4911
+ if (pinnedVersion) {
4912
+ if (current === pinnedVersion) return;
4913
+ throw new Error(
4914
+ `${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).`
4915
+ );
4916
+ }
4917
+ if (current !== null) return;
4918
+ throw new Error(
4919
+ `${config.id}: connector ${npmPackage} is not part of this installation (nothing installed at ${INTEGRATIONS_DIR}; SUVEREN_OFFLINE=1 so no npm install was attempted).`
4920
+ );
4921
+ }
4548
4922
  if (pinnedVersion) {
4549
4923
  if (this.readInstalledVersion(npmPackage) === pinnedVersion) return;
4550
4924
  return withInstallLock(async () => {
@@ -4580,8 +4954,8 @@ var IntegrationManager = class {
4580
4954
  * nothing at all.
4581
4955
  */
4582
4956
  async installNow(npmPackage, exactVersion, cleanFirst = true) {
4583
- const pkgDir = join13(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
4584
- if (cleanFirst && existsSync13(pkgDir)) {
4957
+ const pkgDir = join14(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
4958
+ if (cleanFirst && existsSync14(pkgDir)) {
4585
4959
  console.error(`[IntegrationManager] Removing ${exactVersion ? "outdated" : "unusable"} install of ${npmPackage}`);
4586
4960
  try {
4587
4961
  rmSync(pkgDir, { recursive: true, force: true });
@@ -5070,28 +5444,28 @@ var IntegrationManager = class {
5070
5444
  };
5071
5445
 
5072
5446
  // 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";
5447
+ import { readFileSync as readFileSync16, existsSync as existsSync15 } from "fs";
5448
+ import { join as join15, resolve as resolve2 } from "path";
5075
5449
  import { registerProfile } from "@hap/core";
5076
5450
  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)) {
5451
+ const dir = resolve2(profilesDir ?? process.env.SUVEREN_PROFILES_DIR ?? join15(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "..", "hap-profiles"));
5452
+ const indexPath = join15(dir, "index.json");
5453
+ if (!existsSync15(indexPath)) {
5080
5454
  warnNoProfiles(`No index.json at ${indexPath}`);
5081
5455
  return 0;
5082
5456
  }
5083
5457
  let index;
5084
5458
  try {
5085
- index = JSON.parse(readFileSync15(indexPath, "utf-8"));
5459
+ index = JSON.parse(readFileSync16(indexPath, "utf-8"));
5086
5460
  } catch (err) {
5087
5461
  console.error(`[ProfileLoader] Failed to parse ${indexPath}:`, err);
5088
5462
  return 0;
5089
5463
  }
5090
5464
  let loaded = 0;
5091
5465
  for (const [profileId, relativePath] of Object.entries(index.profiles)) {
5092
- const profilePath = join14(dir, relativePath);
5466
+ const profilePath = join15(dir, relativePath);
5093
5467
  try {
5094
- const profile = JSON.parse(readFileSync15(profilePath, "utf-8"));
5468
+ const profile = JSON.parse(readFileSync16(profilePath, "utf-8"));
5095
5469
  registerProfile(profileId, profile);
5096
5470
  const shortName = profileId.split("/").pop()?.replace(/@.*$/, "");
5097
5471
  if (shortName && shortName !== profileId) {
@@ -5127,8 +5501,8 @@ function warnNoProfiles(reason) {
5127
5501
  import { decodeAttestationBlob as decodeAttestationBlob3 } from "@hap/core";
5128
5502
 
5129
5503
  // 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";
5504
+ import { existsSync as existsSync16, mkdirSync as mkdirSync11, readFileSync as readFileSync17, unlinkSync as unlinkSync6, writeFileSync as writeFileSync11 } from "fs";
5505
+ import { dirname as dirname10, join as join16 } from "path";
5132
5506
  var CommittedExecutor = class {
5133
5507
  constructor(run) {
5134
5508
  this.run = run;
@@ -5156,7 +5530,7 @@ var ExecutorLock = class {
5156
5530
  path;
5157
5531
  held = false;
5158
5532
  constructor(dataDir2) {
5159
- this.path = join15(dataDir2, "executor.lock");
5533
+ this.path = join16(dataDir2, "executor.lock");
5160
5534
  }
5161
5535
  /** Try to take the lock. Returns the holder's pid when someone else has it. */
5162
5536
  acquire() {
@@ -5188,9 +5562,9 @@ var ExecutorLock = class {
5188
5562
  }
5189
5563
  }
5190
5564
  read() {
5191
- if (!existsSync15(this.path)) return null;
5565
+ if (!existsSync16(this.path)) return null;
5192
5566
  try {
5193
- const parsed = JSON.parse(readFileSync16(this.path, "utf8"));
5567
+ const parsed = JSON.parse(readFileSync17(this.path, "utf8"));
5194
5568
  return typeof parsed?.pid === "number" ? parsed : null;
5195
5569
  } catch {
5196
5570
  return null;
@@ -5208,19 +5582,19 @@ function isAlive(pid) {
5208
5582
 
5209
5583
  // bin/http.ts
5210
5584
  import { homedir as homedir10 } from "os";
5211
- import { join as join17 } from "path";
5585
+ import { join as join18 } from "path";
5212
5586
 
5213
5587
  // 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";
5588
+ import { existsSync as existsSync17, mkdirSync as mkdirSync12, readFileSync as readFileSync18, writeFileSync as writeFileSync12 } from "fs";
5589
+ import { dirname as dirname11, join as join17 } from "path";
5216
5590
  function trackerPath(dataDir2) {
5217
- return join16(dataDir2, "mcp-as-pairing.json");
5591
+ return join17(dataDir2, "mcp-as-pairing.json");
5218
5592
  }
5219
5593
  function readMcpPairedAsUrl(dataDir2) {
5220
5594
  const path = trackerPath(dataDir2);
5221
- if (!existsSync16(path)) return null;
5595
+ if (!existsSync17(path)) return null;
5222
5596
  try {
5223
- const data = JSON.parse(readFileSync17(path, "utf-8"));
5597
+ const data = JSON.parse(readFileSync18(path, "utf-8"));
5224
5598
  return typeof data.asUrl === "string" ? data.asUrl : null;
5225
5599
  } catch {
5226
5600
  return null;
@@ -5233,7 +5607,7 @@ function writeMcpPairedAsUrl(dataDir2, asUrl) {
5233
5607
  }
5234
5608
 
5235
5609
  // bin/http.ts
5236
- var dataDir = process.env.SUVEREN_DATA_DIR ?? join17(homedir10(), ".suveren");
5610
+ var dataDir = process.env.SUVEREN_DATA_DIR ?? join18(homedir10(), ".suveren");
5237
5611
  var spUrl = resolveAsUrl(dataDir);
5238
5612
  validatePinTlsForUrl(resolvePinTls(dataDir), spUrl);
5239
5613
  var port = parseInt(process.env.SUVEREN_MCP_PORT ?? "3430", 10);
@@ -5760,7 +6134,7 @@ app.listen(port, "0.0.0.0", () => {
5760
6134
  (proposal) => executeCommitted(proposal, state, integrationManager)
5761
6135
  );
5762
6136
  installCommittedExecutor(committedExecutor);
5763
- const dataDir2 = process.env.SUVEREN_DATA_DIR ?? join17(homedir10(), ".suveren");
6137
+ const dataDir2 = process.env.SUVEREN_DATA_DIR ?? join18(homedir10(), ".suveren");
5764
6138
  const executorLock = new ExecutorLock(dataDir2);
5765
6139
  const lock = executorLock.acquire();
5766
6140
  if (!lock.held) {