@suveren/gateway 0.8.5 → 0.8.7

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 (60) hide show
  1. package/dist/control-plane/index.mjs +340 -56
  2. package/dist/mcp-server/http.mjs +73 -9
  3. package/dist/ui/assets/{index-CheBVmNI.css → index-BJqF5rmx.css} +1 -1
  4. package/dist/ui/assets/index-tzStUGuj.js +110 -0
  5. package/dist/ui/icons/apple-touch-icon.png +0 -0
  6. package/dist/ui/icons/icon-192.png +0 -0
  7. package/dist/ui/icons/icon-512.png +0 -0
  8. package/dist/ui/icons/icon-maskable-512.png +0 -0
  9. package/dist/ui/index.html +8 -2
  10. package/dist/ui/manifest.webmanifest +15 -0
  11. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/auth.d.ts +15 -4
  12. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/auth.d.ts.map +1 -1
  13. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/auth.js +22 -3
  14. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/auth.js.map +1 -1
  15. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/express.d.ts.map +1 -1
  16. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/express.js +3 -1
  17. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/express.js.map +1 -1
  18. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/requestBody.d.ts +25 -0
  19. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/requestBody.d.ts.map +1 -0
  20. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/requestBody.js +64 -0
  21. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/requestBody.js.map +1 -0
  22. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/webStandardStreamableHttp.d.ts +10 -1
  23. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/webStandardStreamableHttp.d.ts.map +1 -1
  24. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/webStandardStreamableHttp.js +13 -1
  25. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/webStandardStreamableHttp.js.map +1 -1
  26. package/node_modules/@modelcontextprotocol/sdk/dist/esm/client/auth.d.ts +15 -4
  27. package/node_modules/@modelcontextprotocol/sdk/dist/esm/client/auth.d.ts.map +1 -1
  28. package/node_modules/@modelcontextprotocol/sdk/dist/esm/client/auth.js +22 -3
  29. package/node_modules/@modelcontextprotocol/sdk/dist/esm/client/auth.js.map +1 -1
  30. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/express.d.ts.map +1 -1
  31. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/express.js +3 -1
  32. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/express.js.map +1 -1
  33. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/requestBody.d.ts +25 -0
  34. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/requestBody.d.ts.map +1 -0
  35. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/requestBody.js +58 -0
  36. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/requestBody.js.map +1 -0
  37. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/webStandardStreamableHttp.d.ts +10 -1
  38. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/webStandardStreamableHttp.d.ts.map +1 -1
  39. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/webStandardStreamableHttp.js +13 -1
  40. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/webStandardStreamableHttp.js.map +1 -1
  41. package/node_modules/@modelcontextprotocol/sdk/package.json +1 -1
  42. package/node_modules/hono/dist/adapter/aws-lambda/handler.js +6 -3
  43. package/node_modules/hono/dist/adapter/lambda-edge/handler.js +26 -12
  44. package/node_modules/hono/dist/cjs/adapter/aws-lambda/handler.js +6 -3
  45. package/node_modules/hono/dist/cjs/adapter/lambda-edge/handler.js +26 -12
  46. package/node_modules/hono/dist/cjs/helper/accepts/accepts.js +7 -5
  47. package/node_modules/hono/dist/cjs/jsx/components.js +1 -1
  48. package/node_modules/hono/dist/cjs/jsx/streaming.js +1 -1
  49. package/node_modules/hono/dist/cjs/middleware/pretty-json/index.js +7 -1
  50. package/node_modules/hono/dist/cjs/router/linear-router/router.js +3 -3
  51. package/node_modules/hono/dist/cjs/utils/jwt/jwt.js +7 -6
  52. package/node_modules/hono/dist/helper/accepts/accepts.js +7 -5
  53. package/node_modules/hono/dist/jsx/components.js +1 -1
  54. package/node_modules/hono/dist/jsx/streaming.js +1 -1
  55. package/node_modules/hono/dist/middleware/pretty-json/index.js +7 -1
  56. package/node_modules/hono/dist/router/linear-router/router.js +3 -3
  57. package/node_modules/hono/dist/utils/jwt/jwt.js +7 -6
  58. package/node_modules/hono/package.json +1 -1
  59. package/package.json +1 -1
  60. package/dist/ui/assets/index-IVfLWq4W.js +0 -110
@@ -5,9 +5,9 @@ import {
5
5
 
6
6
  // src/index.ts
7
7
  import { randomBytes as randomBytes2 } from "crypto";
8
- import { existsSync as existsSync7, readFileSync as readFileSync6, renameSync } from "fs";
9
- import { dirname as dirname5, join as join7 } from "path";
10
- import { homedir as homedir6 } from "os";
8
+ import { existsSync as existsSync7, readFileSync as readFileSync8, renameSync as renameSync2 } from "fs";
9
+ import { dirname as dirname6, join as join9 } from "path";
10
+ import { homedir as homedir7 } from "os";
11
11
  import express from "express";
12
12
  import { createProxyMiddleware } from "http-proxy-middleware";
13
13
 
@@ -33,6 +33,15 @@ var Vault = class {
33
33
  spSessionCookie = null;
34
34
  unsealedBy = null;
35
35
  dataDir;
36
+ /**
37
+ * When the AS session (captured at login into `spSessionCookie`) is due to
38
+ * end — unix seconds, from the login response's `sessionExpiresAt`. Held in
39
+ * memory only, exactly like the vault key and the cookie: never written to
40
+ * disk, gone the moment the process restarts or the vault locks.
41
+ */
42
+ spSessionExpiresAt = null;
43
+ /** Set by lockExpired(); cleared by any fresh unseal (a new login). */
44
+ lockedReason = null;
36
45
  constructor(dataDir) {
37
46
  this.dataDir = dataDir ?? process.env.SUVEREN_DATA_DIR ?? join(homedir(), ".suveren");
38
47
  }
@@ -59,6 +68,7 @@ var Vault = class {
59
68
  async unseal(provider) {
60
69
  this.vaultKey = await provider.deriveKey(this.ensureSalt());
61
70
  this.unsealedBy = provider.id;
71
+ this.lockedReason = null;
62
72
  }
63
73
  /**
64
74
  * Unlock from the user's API key. Also records the key's hash so subsequent
@@ -86,6 +96,33 @@ var Vault = class {
86
96
  this.apiKeyHash = null;
87
97
  this.spSessionCookie = null;
88
98
  this.unsealedBy = null;
99
+ this.spSessionExpiresAt = null;
100
+ this.lockedReason = null;
101
+ }
102
+ /**
103
+ * Lock because the AS session ended — a 401 surfaced it, or the scheduled
104
+ * expiry arrived. Distinct from logout/clearKey() only in that it records
105
+ * WHY, so the next `/health` call and the next locked-tool notice can say
106
+ * something better than "boots locked by design" when that is not what
107
+ * happened.
108
+ */
109
+ lockExpired() {
110
+ this.clearKey();
111
+ this.lockedReason = "expired";
112
+ }
113
+ /** What locked the vault, if known. `null` covers both "still unlocked"
114
+ * and the default boot-locked case — callers that care about the
115
+ * difference should check `isUnlocked()` first. */
116
+ getLockedReason() {
117
+ return this.lockedReason;
118
+ }
119
+ /** unix seconds the current AS session is due to end, or null (locked, or
120
+ * the AS didn't report one — a pre-30-day-rollout server). Never persisted. */
121
+ setSessionExpiresAt(expiresAt) {
122
+ this.spSessionExpiresAt = expiresAt;
123
+ }
124
+ getSessionExpiresAt() {
125
+ return this.spSessionExpiresAt;
89
126
  }
90
127
  /**
91
128
  * Check if the vault was created by a different API key.
@@ -263,6 +300,15 @@ async function configure(sessionCookie, vaultKeyHex) {
263
300
  clearTimeout(timeout);
264
301
  }
265
302
  }
303
+ async function unconfigureSession() {
304
+ const res = await fetch(`${MCP_BASE}/internal/clear-session`, {
305
+ method: "POST",
306
+ headers: internalHeaders()
307
+ });
308
+ if (!res.ok) {
309
+ throw new Error(`MCP clear-session failed: ${res.status}`);
310
+ }
311
+ }
266
312
  async function pushGateContent(data) {
267
313
  const res = await fetch(`${MCP_BASE}/internal/gate-content`, {
268
314
  method: "POST",
@@ -552,9 +598,45 @@ async function loadOrGenerateKeyPair(vault2) {
552
598
  return kp;
553
599
  }
554
600
 
601
+ // src/lib/client-version.ts
602
+ import { readFileSync as readFileSync2 } from "fs";
603
+ import { join as join2 } from "path";
604
+ var UNKNOWN = "unknown";
605
+ function readJson(path) {
606
+ try {
607
+ return JSON.parse(readFileSync2(path, "utf8"));
608
+ } catch {
609
+ return null;
610
+ }
611
+ }
612
+ function detectGatewayVersion() {
613
+ const override = process.env.SUVEREN_GATEWAY_VERSION;
614
+ if (override) return override;
615
+ const start = import.meta.dirname ?? process.cwd();
616
+ let dir = start;
617
+ for (let up = 0; up < 6; up++) {
618
+ const pkg = readJson(join2(dir, "package.json"));
619
+ const name = pkg?.name;
620
+ const version = pkg?.version;
621
+ if (typeof version === "string" && (name === "@suveren/gateway" || name === "suveren-gateway")) {
622
+ return version;
623
+ }
624
+ const parent = join2(dir, "..");
625
+ if (parent === dir) break;
626
+ dir = parent;
627
+ }
628
+ return process.env.HAP_BUILD_SHA ?? UNKNOWN;
629
+ }
630
+ var GATEWAY_VERSION = detectGatewayVersion();
631
+ function clientVersionHeaders() {
632
+ return {
633
+ "x-suveren-gateway-version": GATEWAY_VERSION
634
+ };
635
+ }
636
+
555
637
  // src/routes/auth.ts
556
638
  var SP_URL = process.env.SUVEREN_AS_URL ?? "https://www.suveren.ai";
557
- function createAuthRouter(vault2, logoutAuth, loginRateLimit2) {
639
+ function createAuthRouter(vault2, logoutAuth, loginRateLimit2, onSessionEstablished) {
558
640
  const router = Router();
559
641
  router.post("/login", loginRateLimit2, async (req, res) => {
560
642
  const apiKey = req.headers["x-api-key"] || req.body?.apiKey;
@@ -568,7 +650,11 @@ function createAuthRouter(vault2, logoutAuth, loginRateLimit2) {
568
650
  method: "POST",
569
651
  headers: {
570
652
  "Content-Type": "application/json",
571
- "X-API-Key": apiKey
653
+ "X-API-Key": apiKey,
654
+ // The AS grants a 30-day gateway session ONLY to a login that
655
+ // identifies itself this way; without it, a 24-hour browser
656
+ // session (see doc at the top of this file).
657
+ ...clientVersionHeaders()
572
658
  }
573
659
  });
574
660
  if (!spRes.ok) {
@@ -576,6 +662,7 @@ function createAuthRouter(vault2, logoutAuth, loginRateLimit2) {
576
662
  res.status(spRes.status).json(err);
577
663
  return;
578
664
  }
665
+ const data = await spRes.json();
579
666
  const setCookieHeaders = spRes.headers.getSetCookie?.() ?? [];
580
667
  const sessionCookie = setCookieHeaders.join("; ");
581
668
  vault2.setSpCookie(sessionCookie);
@@ -605,6 +692,9 @@ function createAuthRouter(vault2, logoutAuth, loginRateLimit2) {
605
692
  vault2.wipe();
606
693
  await vault2.deriveAndSetKey(apiKey);
607
694
  }
695
+ const sessionExpiresAt = typeof data.sessionExpiresAt === "number" ? data.sessionExpiresAt : null;
696
+ vault2.setSessionExpiresAt(sessionExpiresAt);
697
+ onSessionEstablished?.();
608
698
  if (sessionCookie) {
609
699
  try {
610
700
  await configure(sessionCookie, vault2.getVaultKeyHex());
@@ -612,7 +702,6 @@ function createAuthRouter(vault2, logoutAuth, loginRateLimit2) {
612
702
  console.error("[Control Plane] Failed to configure MCP:", err);
613
703
  }
614
704
  }
615
- const data = await spRes.json();
616
705
  res.json(data);
617
706
  (async () => {
618
707
  try {
@@ -684,7 +773,16 @@ function createAuthRouter(vault2, logoutAuth, loginRateLimit2) {
684
773
  }
685
774
  });
686
775
  router.post("/logout", logoutAuth, async (_req, res) => {
776
+ const cookie = vault2.getSpCookie();
687
777
  vault2.clearKey();
778
+ await Promise.allSettled([
779
+ unconfigureSession(),
780
+ cookie ? fetch(`${SP_URL}/api/auth/logout`, {
781
+ method: "POST",
782
+ headers: { cookie, ...clientVersionHeaders() },
783
+ redirect: "manual"
784
+ }) : Promise.resolve()
785
+ ]);
688
786
  res.json({ ok: true });
689
787
  });
690
788
  return router;
@@ -1294,18 +1392,18 @@ async function testAIConnectivity(config) {
1294
1392
  }
1295
1393
 
1296
1394
  // src/lib/agent-brief-store.ts
1297
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
1395
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
1298
1396
  import { homedir as homedir2 } from "os";
1299
- import { join as join2 } from "path";
1397
+ import { join as join3 } from "path";
1300
1398
  var AGENT_CONTEXT_MAX_BYTES = 16 * 1024;
1301
1399
  function agentBriefPath() {
1302
- const dataDir = process.env.SUVEREN_DATA_DIR ?? join2(homedir2(), ".suveren");
1303
- return join2(dataDir, "context.md");
1400
+ const dataDir = process.env.SUVEREN_DATA_DIR ?? join3(homedir2(), ".suveren");
1401
+ return join3(dataDir, "context.md");
1304
1402
  }
1305
1403
  function readAgentBrief() {
1306
1404
  const filePath = agentBriefPath();
1307
1405
  if (!existsSync2(filePath)) return "";
1308
- return readFileSync2(filePath, "utf-8");
1406
+ return readFileSync3(filePath, "utf-8");
1309
1407
  }
1310
1408
 
1311
1409
  // src/routes/ai.ts
@@ -1676,15 +1774,15 @@ import { promisify } from "util";
1676
1774
  var run = promisify(execFile);
1677
1775
  var SUPPORTED = /* @__PURE__ */ new Set(["darwin", "win32", "linux"]);
1678
1776
  var MODULE_DIR = pathDirname(fileURLToPath(import.meta.url));
1679
- function findCli(dirname6 = MODULE_DIR) {
1777
+ function findCli(dirname7 = MODULE_DIR) {
1680
1778
  const candidates = [
1681
1779
  // Bundled (the shipped layout): dist/control-plane → dist → root → bin
1682
- resolve(dirname6, "..", "..", "bin", "suveren-gateway.js"),
1780
+ resolve(dirname7, "..", "..", "bin", "suveren-gateway.js"),
1683
1781
  // Bundled, were the routes/ directory ever preserved.
1684
- resolve(dirname6, "..", "..", "..", "bin", "suveren-gateway.js"),
1782
+ resolve(dirname7, "..", "..", "..", "bin", "suveren-gateway.js"),
1685
1783
  // Dev, running from source: the repo's own bundle directory.
1686
- resolve(dirname6, "..", "..", "..", "..", "bundle", "bin", "suveren-gateway.js"),
1687
- resolve(dirname6, "..", "..", "..", "..", "..", "bundle", "bin", "suveren-gateway.js")
1784
+ resolve(dirname7, "..", "..", "..", "..", "bundle", "bin", "suveren-gateway.js"),
1785
+ resolve(dirname7, "..", "..", "..", "..", "..", "bundle", "bin", "suveren-gateway.js")
1688
1786
  // Deliberately NO process.cwd() fallback: it makes the result depend on
1689
1787
  // where the process happened to be launched from, so the same install
1690
1788
  // resolves differently between a shell and a login service — and it would
@@ -1773,12 +1871,12 @@ import { spawn, spawnSync } from "child_process";
1773
1871
  // src/lib/notifier-icon.ts
1774
1872
  import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync4 } from "fs";
1775
1873
  import { homedir as homedir3 } from "os";
1776
- import { join as join3, dirname as dirname2 } from "path";
1874
+ import { join as join4, dirname as dirname2 } from "path";
1777
1875
  var ICON_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAACXBIWXMAACxLAAAsSwGlPZapAAAaEElEQVR42u2deXxNV9fH7/O2z/vHk+HmJoIoQQxpBtVPi+LTIWZqeDQ6V0tRFUWIeagSRAheyasxVNUsWtRQSquoMTRi9mgJooZWhBqaSIX97nWa9CVN4ubes8/Z55zf+ny+H1rknrv3Wr+zh7XXttmMaY94e3vXdDgcL3JifHx8ZnE2cvbw/z7MyeBc5eRxGABukFfgS+RTh8nHCnxtFvke+SD3xRrkkzaYGPP19fXmDd6eM403eDoCG0gqFPu5j07ltCOfReS6YbwRn+ANGsd/TeW/5sPBgMG4U+C7Ezh1ENFOGB9K+fLG6skbbiccCJgJ7tPH+K9D/f39KyLS/z7Eb84baD2pJpwFWGBksN5utzezetz/o2BevwdOASxKOn/5vUKxYKnA51/8tYLVejgBAA7HQUsIgZ+fXzB/43+LDgegWLZzwk0X+AEBAf/iX2wMtu8AePgaASeRvyy9zLKd147zMzoWgDLtGpzjv7Y1cuw/WvDWv4sOBcAl7tFogMfSfxsq8vlDB3IF240OBEAVfuDbhkGGCH7+oB35A19DpwGgKlf5S7WD7PP9DzDkB0DolCBG1mH/UHQQAJosEMZLdTS34GgkOgcA7fiMFtplCP7P0RkA6DISWK5nHQLK5Z+DjgBAVxGYrdecfwI6AAApGKv1an9vNDoAUo0EorXc58dWHwBycZeO1wvP8ONko7EBkDNZiL+gq4mK/38ivRcA6acCe4WcHeA//H/QwMAM+Po6WKNQH9arlZ0ldPZmKwZ4sfQ4T/afKZ7s4gwP9tsnf0K/p/+3f4Kn8ncm879L/4b+Lf0MiUVgstqLfu0L0hDhQMCQVK3kYL1be7Pl0V7sXJIH+32ee9DPSOE/K6qVt/KzJUwZflHNYh5n4UTAaPjxt/TLje3s8/5e7OocT7eDviToZ5MYdOKfJcvIgOoJ8ArEnmq8/afAmYCRKOfnYN2a2dmBOHFBXxI0lXi3qV15BgnaYqK78/4wzh9wKmAU6C18ZJKH5oFflCOTPNlLjex6t0ceLzYa4k713q1wKmAEQqr5sAW9vXQP/KKsHujF6gT56Nk237tUbdjxZ+luOBeQnjeft7NLH3tIF/yF/Jrswbo21W80wHMDIl15+6NuP5Caiv4+bGYPL2kDvygfd/fiz6xLWx0o0yiAL/z9Gw4GZN/W2/qhp2GCv5AdH3mxoMq6tNmLZRGAXXAyICu1A33YD+ONF/yF0O5EaDUfrbcF9zh9USecDMjK41V92PEE4wZ/IfQd6LtovBbQxJm3/3o4GpB12E9puUYP/kLS+ChG4yzCdaUGf/ny5Ss4cEU3kHTBz4hz/oexZZSnlguDdyjGS9v6GwhnAzIy6z1v0wV/ITO6eclROIT/hYNwNiAbb0fYTRv8hbyrXZ5AWknBHw5nA7IRzrPoKJHG7AJAiUwa7gzUQZFPYAjoHL7Zg78QOqqsUbvGFrf6vxcOB2SiY0O7ZYK/EDrMpME6wK6ib387Jx9OB2Q60ivDqT6tORzvqcVR4js838e7aMUfOB6QBjrPb7XgL6RLE7u2qcFcAKbB6YBMNfv0KOYhU4KQ6MpCPOYT7p8CpMPxgCxQGS+rBn8hkeKLify1HfgI/4/bcDwgC1TDz+oCsLSv8B2B28rFot7e3jXhdEAWAgMcLHu2p+UFgAqNij4nwA8HVafhf1s4HpAFKt1t9eAv5P2WwqcBrUkAYuB4QBYoGQbBr800QDkXQPeLw/GALKv/alzaYRYyEz1E7wYkkwBshPMBGWjMr9xC4D9IwxAfkSOADSQAe+B8QAbo3j0E/YO818IuUgB2kgAcgfMBGaCLOhH0DxL/prfINj9kw71/QBZWDsACYFG+GCB0IfA0CUA2nA84Q+XKldnzzz/POnbsyFq2bMlCQ0NV/flp47H/X5R94zxF9mkWCUAenBuUFvQjRoxg6enpLD8/nxW1ixcvstmzZ7MGDRq4/Vk/ToUAFOU/UzzFZgPCyUFJREVFsezsbOaM3b17ly1dupSFhIS4/HkXZiDgi/Lz/3oI7WMIAPj7Wfxy5di8efOYK5aTk6P8e1c+99ocBPzfU4IhAEDTZBxftmTJEuaOVahQAQIAAQBGZPDgwcxdCwoKwhQAUwDgDvwCBxYREcHeeOMN1rVrV2Xl3Z35tTPUrl2b3bp1y20BeOKJJ7AIaIxFQAiAdIUweaCvWbOG5ebmFhtcp06dYhMnTmQ1atRQ/bOnTZvG1LDGjRtjG9AY24AQABnw9/dnPXr0YIcOHXI6yG7cuMFiYmKUObtac//z58+rIgAtWrRAIpAxEoEgAHry2GOPsWHDhrHMzEyXgy0lJcXlRbf7oX18teydd95BKrAxUoEhAHpQsWJFNnbsWHb16lVVAo5EwN2RAAWtWjZu3DgcBjLAYSAIgA7Qot7p06eZ2jZo0CC3nmvAgAGqPcuyZctceoZGOA78N54J8YEAmIGGDRuyrVu3MlF28+ZNtxYG+/Tpo9qzpKWluVwQhIpgIPALCoIkiS8PDgEQXeQyMJDNnDmT/fHHH0y0xcfHu/ycnTp1Uu05fvvtN5QEU4El4isDQwBEQifmzp49y7SyjIwMl5+1Vq1a7N69e6o9i6uHg6JaYSFQw6KgEABRufRjxozR5K1f1MLCwlx+7r1796r2HAMHDkRZcDfLglNbQAAMBmXB7dmzh+llNJR35/SfWrZ69WpMA+S+GAQCoDZdunRR5r96Wvfu3d1KSDp+/Lgqz3HlyhXm5+fn2noErgZjLzWyQwCMxPDhw5Uz8Xpb586d3foedP7g9u3bqjwL/SxXdwPSLXw56A8aXA4KAVAxjXfhwoVMFmvatKnb34mSgtRYv0hKSnL5Gd618PXg70TYNfNfCIAbVKlShW3evFma4Kc3N2UZqvHdaC3B2WpAJdmlS5dcLg5Szs/Bjkyy3ijgULwn8/N1QABk5/HHH2cHDx5kMtm6detU/Y7BwcFs5cqVbm0PRkZGuvz5/37GjmvBIQDyER4erun+vrP20ksvCfm+bdq0YQcOHHDpmZYvX+7WZ3/R3zqjgJRoL819GQLgQtEMOpMvm23btk3o96YV/b59+7LLly+X6bmorgG1maufG1bdh/2SbP7gv/SxBwup5gMBkBnKtT927Jh0wU9zdVer8LiS2kyLe3l5eU4/X2Jioluf2fkF808F3nrBRxefhgA4SdWqVZXa+LIZFQZp1aqV5u1Rr149tmHDBqee8ffff2c1a9Z06/M+7m7e5KDErl66+TUEwAkqVarEUlNTpQt+yv1/7rnndG0bWnc4ceKE0INKSg0Ffwf7bpT51gM2j/BkFfwdEACZWbVqlVSBf/36dTZ58mRFmGTJhejfvz/Lysoq8ZmvXbvmdh3DqpXMVTeQEn60yPeHALjBqFGjpAl82lf/6KOPlOmIrGskc+fOZXfu3Cn2+Slhyu3t16o+7HiC8UXg2GQPFhzoo3ufQQBKoUOHDiU6s5ZGuw6UaqxWko9oqCow7UoUd30YHZF2eyeGBw5VyzVq8FOasx4r/hCAMu71l3XLS8Qb//3331et8q8M5c9oF4WmDG4vylYy5prA5pH6D/shAE4U7dRzxZ/y8GfNmqVsuZmlACqVLCu00aNHq/Oz+eLZjG7G2B249akHS3rXS9cFPwiAk8yYMUO34N+xYwd76qmnTJk6TXcO0jSABE7Nrcs3nrezixJfK/ZrsrYHfCAAbtCsWTOWn5+veeDTWgNtlbl6eMYo0GnFffv2KZeQqHm7ES0OLugt32hg9UAvFh7kI21/QADugy7YUKsgRlmMLgZp3bq15cqjL1iwQPX1jY4N7exwvKcUp/raN7BL3w8QgPugvXWtjY4T07Fiq96M1L59e/XPLfDjtF2b2nXJGaC9fRrua3mkFwKgAs8++6zmRTwXL16s3AKMugpiBJCq6tDx2mX9vIQWGqWfTTX8qIyXr6+x2h4CUHDSTctVfzpfHxsbC+HV8n6GgD+vHqNApQs33A36s4meSt3+nrx0d5UA47YLBIBDN/NqGfyUNot21w96S9OVWxS8k97yVm7gpcQiyjC8wHcTrs35E/o9/b+9/M/o79BFnXRXn+jruiAAGkJDcDpUo5WptQcOAARABWJiYjQL/gkTJsDpAARAFgICApR0Wy0sOTkZDgcgADJB13dpYd9//70q+e8AQABULPLhbtlrZ+zcuXNuV8MBAAKgMv369dOkTr8aF3UAAAFQGVfLXJfFRo4cCScDEADZoLx70ZaWlmb6gz0AAmBIVqxYIXzo36hRIzgYgAA4JLzYoyw17V2xuLg4OBeAAMgIFdUUaRcuXDBM7T4ALCcA+/fvFyoAtLsAxwIQAAkJCQlx66ZbZ6r3IuEHQAAkZdCgQULf/t26dYNTAQiArBRXq14t+/nnn7HtByAAslK9enWhFX/oXAEcSj5IlKkCMY3+qOTb1KlT2YgRI1jHjh2Vw2AQAIt80ffee0/ovj/y/eWiVq1abPr06aVe7nLr1i2lLNvTTz8NATA7dGedKFu6dCmCTjKxp8tInTXKCxk/frxSGg4CYFIOHjwoTABoOInAk4OEhASX+3Ht2rVKaXgIgAmP/oq65PPq1auo7Oswz03Oy5YtgwCYjXbt2mH4b3JeeOEF1RZ5o6KiIACo/OOcde7cGQEoAbt371atT2nhkEaNEACTQHM7Uff50e02CEB9admypep9O2DAAAiAWTh58qQQAaCFRQSg/sycOVP1vt2+fTsEwGGKSyB8WW5urhAB+OSTTxCAJq3ulJOTY4lzHaYXgODgYGHz/169eiEAJeD69etC+rdu3boQAKPTvHlzYQJQr149BKAEI7y7d+8K6d8WLVpAAIwOndATYfn5+Tj6Kwk3b94U0sdvvfUWBAAVgEo+/Yfgk4MzZ84I6WMrXOJqegGYM2eOEOegfWcEnxzs27dPSB9PnDgRAmB0lixZIsQ5UlJSEHyS8NVXXwnpYzpABgFwoAR4cZaUlITgk4R58+YJ6WNKIIMAGJx169YJcQ4qLoHgk4P4+HghffzNN99AAIwOdaIIGzduHIJPEmJjY4X08aZNmyAARoeu5sa9fxAAV2zjxo0QAKOTmpoqxDkGDx6M4JMEGo2JsK+//hoCAAHACEB2qJyXCNuwYQMEwOjQPE6EUekpBJ8c0H69CFu/fj0EwOgsX74cJwGR7OWS0Q4SBADOUax98cUXCD6TF3xZuHAhBMDo0H69CNuyZQuCTxL27t2LXA8IQPHQYp0Iy8zMRPBJwrlz54T0cUxMDATA6PTp00eIc9AZdNQD1B+q408Xe6DgKwSgWOjSDlFGpagRhPoSEREhrH+bNWsGATA6oaGhwhyErqBCEOpLv379hPVveHg4BAA140q25ORkBKHO0HasCKNLRqxwTZglBCAtLU2Ikxw5cgRBaNJiIEePHmUoC46iIKXavXv3lGuoEYj6UL16dWF3PtK14RAAB+oCPsy6du2KYNSJHj16COvXoUOHQgDMwmuvvSbMUebPn49gNFmaN1mbNm0gAGahWrVqShlvEZaVlYXrwXWgXLly7MqVK8JyPCpXrgwBMBPp6enC3havv/46gtJE+R0//fQTw/XgJiMxMVGYw3z55ZcISo2hNhdln332GQTAbERGRgpzmNu3byvTDASmNtDOi6j0X7I333wTAmA2KlWqpASqKBsyZAiCUyNGjRolVMytdMbDZiXH2bFjhzDHoRNpWAwUD93HmJGRIawfN2/ebKn2tJQAjB07lom0Dz74AEEqmN69ewvtQ6uN5CwlAHXq1BF2lTTZyZMnmZ+fHwJV4NbfqVOnhArAk08+CQHANMB16969O4JVEH379hXad4cPH7Zcm1pOAEQVCGH3XRuOQiFiFnFFVf5hFr7rwXICEBgYyHJycoQ60tSpUxG0BsrjIMvNzVUOF0EALMCqVauEOhPtUdevXx+BqxKNGzdWzueLtKVLl1qybS0pAK+++ioTbdu2bWO+vr4IYDehRVVRZ/7vt9atW0MArLSaTCv2om306NEIYjf58MMPhffTiRMnLNu+Nqt+cdGLgYVlpZo3b45AdhF6K4se+ls9i9OyAkBZe6JXlQt3BYKCghDQLuT7X7x4UXj//PLLLywgIAACYEVI+bUwumeeUlgR2M5P0bZu3apJ31j9DIelBaBixYrKG0ALs8I9c2pAC6cLFizQpE8uXbpk6be/5QVAi/MB9xtdY40gL51p06Zp1h9WuPoLAvAQqPTThQsXNHE4qiIcHR2NQC+B4cOHaxb8tP5jhbr/EAAnoMq+WhmJADk62v1BqAqvyINaRa1Xr15odwjA/0PnwLW06dOno90L5vzx8fGatv3OnTuRpAUBeJAGDRoILTNVnM2ePVtZ8bZycY9FixZp2uZU8adevXrweQjA36G3stZGKcNWvF2oZs2amo+6yOLi4uDrEIDioWO858+f19wp6TNbtGhhmXZu1aqVLu38448/YuEPAlA67du3F3aJyMNOEI4YMcLUFYVo3k25/Vqk97JiLvto27YtfBwC8HAmTZrE9LJDhw6xiIgI07UpzbtpuqOXTZkyBb4NAXA+FZVWivUyGg1MnjxZyVQ0QyUfWlvR461faFQGzsqLrRAAFwgLCxN291xZklWoDp4RzxFQwNFee2Zmpq5t+Ouvv7Lg4GD4NASg7LzyyitK4o7eRkHUv39/Q7zFaA2DEqu0qLfwMKO1HLpDEL4MAXAZGorLYqdPn1aKjNAWmmztREeeKZuPnlEWi42NhQ9DANxfuV68eDGTySiZ5fPPP1d2LPQcFdBnv/zyy8pFnSKvXXPFUlJSkO0HAVCveMi3337LZLTs7GzF2bt06cKqVKmiSQIP3X2wZMkSzY5Sl9U2bdqEa9ogAOqfGkxPT2cyG+0e7N+/n82dO1e5poyq6bozQqDEqKZNmyrl05KTk5Xvr+WBHVcsNTVV2XmAz0IAVKd27dpSzXGZk3UJqSwZBcbKlSuVQKZ1jTFjxjxAQkIC+/TTT9natWuVLVD6nrIHe1E7fvy4JWv7QwA0hO6OO3PmDIPJZWfPnmUhISHwUQiAeGhf+ejRo4g6SYxy/MPDw+GbEADtoKGmFhdWwEo3WvOQcVsUAmCR04NbtmxBFOpkdLaAFmfhixAAXSsL08IZTFtbtmwZSq1DAORJFqKVdD2OEVvNqI2pujKSfCAA0kHnzenwCUyMZWVlsU6dOsHXIADyEhoaquy5w9S1Xbt2YZsPAmCc1GFKuDFaIo2MRm1IxTxwnh8CYMjad3T1NMw1y8jIwHFeCIDxRwO0QCjbaTnZ05epipAZKiJBAMBfNfG2b9+O6GYPv7TjmWeegc9AAMwHVcuJiorCWQJW/E29PXv2hJ9AAKwxLSAhoAMsVrfLly8rUySrX9MNAbAgdEkF1fqTtbAGE3wZCl2SisCHAOBMAT9TMHDgQEucMKQ6A/RdscAHAQDF0KRJEzZ//nyWk5NjmqCnHZDVq1crW3pI4YUAACdr79E1WnRTkFGNjktHR0ezwMBA9CkEALhKnTp12ODBg5Wjx1pfYV4Wy83NZd999x0bOXIkq1+/PvoOAgDUht6mdPHGokWL2LFjx3Q/gUh1+GbMmMEiIyOxoAcBAHosILZp00a5WXjFihXs1KlTQu7hu3HjhjKkp/WJIUOGsA4dOrAaNWqgDyAAQMaEIzo517x5c/b222+zYcOGsaSkJOUyEVqMI9asWaNU1CmEhu7057NmzWJxcXFs0KBBykiDLh+pW7cu2tXEApCHhgDAktwmAchGQwBgSbJIAM6iIQCwJKdtPj4+R9AQAFiSQyQAu9EQAFgPHvs7SQA2ojEAsKQAbCABmI3GAMCSJNMiYAwaAgBLjgCiSQDaojEAsCStbd7e3jXQEABYD7vdXs3G7RHKCEKDAGCp4X8uxT4JAE0D0tEoAFiKNFuhcTWYigYBwFIjgMn3C0A7NAoAlqLNXwLAj4968f9xB40CgCW4QzFvu9/4KCAVDQOARVKAixr/g/FoHAAsQWxxAhCGhgHAEoTbijP+BwfQOACYmh9sJRnOBQBg+vl/vxIFwMPDozx2AwAw7+o/v5i2gq004wqxHg0FgClZa3uY8QMCzdBQAJjy8E+EzRnjo4BdaDAATDX332Nz1vhfbo9GA8Ckqb9O2D/4P9iPRgPAFBygmC6LANj4Pe6voOEAMMXcv6PNBaNRwBY0IACGZluZ3/73jQJC+Q/4A40IgCHJ4zEcYnPHqHAAGhIAQzLB5q4FBAT8i/+gM2hMAAxFZoUKFTxsapjjz9Lh99CoABiCe0rJbzWNTwWmoGEBMETST7xNgD2KDEEApA/+VB6r/xQhADQVqMK5goYGQEqucgGoahNp/AM68A+6i8YGQCryOS/atDD+Qb3Q4ABINfTva9PS+IeORcMDIEXwj7bpYfzDk9ABAOga/LNsOtoj/AGWoyMA0CX4U/664FNnEZiJDgFAU+bR1rxNFuMPNBSdAoBxE33U2CLsjS1CAIRu9fWyyWwF5cSy0VkAqMoVzfb51cgYpAsI0WkAqDLk38er+lS3Gcwe5Q8/BlMCANw61ZcoLLdfo9HAi3Q2GZ0JQJk4q/qRXr2soKgIjQZuo2MBKBUqwZfo7+/vaTObeXl51eLzmU3oZACKZRsnzGZ24wsakfyLpqPDAVDY72rpbkMb/+LPcr6DAwCLru7vom1zl8t2m2hE0IQ3yDoHriYH5od8fB35vA32oHl7e/vyxumJHAJgwrf9MUqXL1++fAVEunPTgzBOLG+43RgZACO+6QtqaI6lC3YQ0W6Yn5+fF+UTFFQmTsN2IpAQ8skfuI8mkK+achtPInuEz6GCKFGC/9qfjiNzvi5Q3IOcjILzCHlwTOAmeQW+RD51kKan5Gv898n81+gCH6Q03f8yYiD9H4Lpi7OZQSaFAAAAAElFTkSuQmCC";
1778
1876
  function ensureNotifierIcon(dataDir) {
1779
1877
  try {
1780
- const dir = dataDir ?? process.env.SUVEREN_DATA_DIR ?? join3(homedir3(), ".suveren");
1781
- const path = join3(dir, "notifier-icon.png");
1878
+ const dir = dataDir ?? process.env.SUVEREN_DATA_DIR ?? join4(homedir3(), ".suveren");
1879
+ const path = join4(dir, "notifier-icon.png");
1782
1880
  if (!existsSync4(path)) {
1783
1881
  mkdirSync2(dirname2(path), { recursive: true });
1784
1882
  writeFileSync2(path, Buffer.from(ICON_BASE64, "base64"));
@@ -1861,6 +1959,14 @@ function lockedNotification(port2) {
1861
1959
  url
1862
1960
  };
1863
1961
  }
1962
+ function sessionExpiredNotification(port2) {
1963
+ const url = `http://localhost:${port2}`;
1964
+ return {
1965
+ title: "Suveren Gateway locked",
1966
+ message: "Your sign-in has ended. Sign in again so your agents can act.",
1967
+ url
1968
+ };
1969
+ }
1864
1970
  var terminalNotifierCache = null;
1865
1971
  function hasTerminalNotifier() {
1866
1972
  if (terminalNotifierCache !== null) return terminalNotifierCache;
@@ -1975,17 +2081,17 @@ import { Router as Router9 } from "express";
1975
2081
 
1976
2082
  // src/routes/approved-intents.ts
1977
2083
  import { Router as Router8 } from "express";
1978
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
2084
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
1979
2085
  import { homedir as homedir4 } from "os";
1980
- import { join as join4 } from "path";
1981
- var SUVEREN_DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join4(homedir4(), ".suveren");
1982
- var FILE_PATH = join4(SUVEREN_DATA_DIR, "approved-intents.enc.json");
2086
+ import { join as join5 } from "path";
2087
+ var SUVEREN_DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join5(homedir4(), ".suveren");
2088
+ var FILE_PATH = join5(SUVEREN_DATA_DIR, "approved-intents.enc.json");
1983
2089
  function readFile() {
1984
2090
  if (!existsSync5(FILE_PATH)) {
1985
2091
  return { version: 1, entries: {} };
1986
2092
  }
1987
2093
  try {
1988
- return JSON.parse(readFileSync3(FILE_PATH, "utf-8"));
2094
+ return JSON.parse(readFileSync4(FILE_PATH, "utf-8"));
1989
2095
  } catch {
1990
2096
  return { version: 1, entries: {} };
1991
2097
  }
@@ -2367,6 +2473,7 @@ function startUpdateChecker(method, version) {
2367
2473
 
2368
2474
  // src/routes/events.ts
2369
2475
  var KEEPALIVE_INTERVAL_MS = 25e3;
2476
+ var PAYLOAD_ALLOWED = /* @__PURE__ */ new Set(["session-locked"]);
2370
2477
  function createEventsHandler() {
2371
2478
  return function eventsHandler(_req, res) {
2372
2479
  res.setHeader("Content-Type", "text/event-stream");
@@ -2375,8 +2482,9 @@ function createEventsHandler() {
2375
2482
  res.setHeader("X-Accel-Buffering", "no");
2376
2483
  res.flushHeaders();
2377
2484
  const unsubscribe = eventBus.subscribe((event) => {
2485
+ const data = PAYLOAD_ALLOWED.has(event.type) ? JSON.stringify(event.payload ?? null) : "null";
2378
2486
  res.write(`event: ${event.type}
2379
- data: null
2487
+ data: ${data}
2380
2488
 
2381
2489
  `);
2382
2490
  });
@@ -2394,19 +2502,19 @@ data: null
2394
2502
  import { Router as Router12 } from "express";
2395
2503
 
2396
2504
  // src/lib/gateway-settings.ts
2397
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4 } from "fs";
2505
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4 } from "fs";
2398
2506
  import { homedir as homedir5 } from "os";
2399
- import { join as join5, dirname as dirname4 } from "path";
2507
+ import { join as join6, dirname as dirname4 } from "path";
2400
2508
  var DEFAULT_SETTINGS = {
2401
2509
  desktopNotifications: true
2402
2510
  };
2403
2511
  function settingsPath(dataDir) {
2404
- const dir = dataDir ?? process.env.SUVEREN_DATA_DIR ?? join5(homedir5(), ".suveren");
2405
- return join5(dir, "settings.json");
2512
+ const dir = dataDir ?? process.env.SUVEREN_DATA_DIR ?? join6(homedir5(), ".suveren");
2513
+ return join6(dir, "settings.json");
2406
2514
  }
2407
2515
  function readSettings(dataDir) {
2408
2516
  try {
2409
- const raw = JSON.parse(readFileSync4(settingsPath(dataDir), "utf8"));
2517
+ const raw = JSON.parse(readFileSync5(settingsPath(dataDir), "utf8"));
2410
2518
  return {
2411
2519
  desktopNotifications: typeof raw.desktopNotifications === "boolean" ? raw.desktopNotifications : DEFAULT_SETTINGS.desktopNotifications
2412
2520
  };
@@ -2449,10 +2557,76 @@ function createGatewaySettingsRouter() {
2449
2557
  return router;
2450
2558
  }
2451
2559
 
2452
- // src/routes/internal-events.ts
2560
+ // src/routes/archived-mandates.ts
2453
2561
  import { Router as Router13 } from "express";
2562
+
2563
+ // src/lib/archived-mandates-store.ts
2564
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, renameSync } from "fs";
2565
+ import { homedir as homedir6 } from "os";
2566
+ import { join as join7, dirname as dirname5 } from "path";
2567
+ var ID_PATTERN = /^[A-Za-z0-9_.:-]{1,200}$/;
2568
+ function isValidMandateId(id) {
2569
+ return typeof id === "string" && ID_PATTERN.test(id);
2570
+ }
2571
+ function storePath(dataDir) {
2572
+ const dir = dataDir ?? process.env.SUVEREN_DATA_DIR ?? join7(homedir6(), ".suveren");
2573
+ return join7(dir, "archived-mandates.json");
2574
+ }
2575
+ function readArchived(dataDir) {
2576
+ try {
2577
+ const raw = JSON.parse(readFileSync6(storePath(dataDir), "utf8"));
2578
+ if (!Array.isArray(raw.archived)) return [];
2579
+ return [...new Set(raw.archived.filter(isValidMandateId))];
2580
+ } catch {
2581
+ return [];
2582
+ }
2583
+ }
2584
+ function writeArchived(ids, dataDir) {
2585
+ const next = [...new Set(ids.filter(isValidMandateId))].sort();
2586
+ const path = storePath(dataDir);
2587
+ mkdirSync5(dirname5(path), { recursive: true });
2588
+ const tmp = `${path}.tmp`;
2589
+ writeFileSync5(tmp, JSON.stringify({ archived: next }, null, 2) + "\n", "utf8");
2590
+ renameSync(tmp, path);
2591
+ return next;
2592
+ }
2593
+ function archiveMandate(id, dataDir) {
2594
+ if (!isValidMandateId(id)) throw new Error("invalid mandate id");
2595
+ return writeArchived([...readArchived(dataDir), id], dataDir);
2596
+ }
2597
+ function unarchiveMandate(id, dataDir) {
2598
+ if (!isValidMandateId(id)) throw new Error("invalid mandate id");
2599
+ return writeArchived(readArchived(dataDir).filter((x) => x !== id), dataDir);
2600
+ }
2601
+
2602
+ // src/routes/archived-mandates.ts
2603
+ function createArchivedMandatesRouter(dataDir) {
2604
+ const router = Router13();
2605
+ router.get("/", (_req, res) => {
2606
+ res.json({ archived: readArchived(dataDir) });
2607
+ });
2608
+ const change = (fn) => (req, res) => {
2609
+ const id = req.params.id;
2610
+ if (!isValidMandateId(id)) {
2611
+ res.status(400).json({ error: "invalid mandate id" });
2612
+ return;
2613
+ }
2614
+ try {
2615
+ res.json({ archived: fn(id, dataDir) });
2616
+ } catch (err) {
2617
+ res.status(500).json({ error: `Could not save: ${err.message}` });
2618
+ }
2619
+ };
2620
+ router.put("/:id", change(archiveMandate));
2621
+ router.delete("/:id", change(unarchiveMandate));
2622
+ return router;
2623
+ }
2624
+
2625
+ // src/routes/internal-events.ts
2626
+ import { Router as Router14 } from "express";
2454
2627
  import { timingSafeEqual as timingSafeEqual2 } from "crypto";
2455
2628
  var ALLOWED = ["proposal-added", "action-approval-needed"];
2629
+ var SESSION_EXPIRED = "session-expired";
2456
2630
  function secretMatches(provided, expected) {
2457
2631
  if (!expected) return false;
2458
2632
  if (!provided) return false;
@@ -2461,14 +2635,19 @@ function secretMatches(provided, expected) {
2461
2635
  if (a.length !== b.length) return false;
2462
2636
  return timingSafeEqual2(a, b);
2463
2637
  }
2464
- function createInternalEventsRouter(getSecret) {
2465
- const router = Router13();
2638
+ function createInternalEventsRouter(getSecret, onSessionExpired) {
2639
+ const router = Router14();
2466
2640
  router.post("/event", (req, res) => {
2467
2641
  if (!secretMatches(req.headers["x-internal-secret"], getSecret())) {
2468
2642
  res.status(401).json({ error: "Unauthorized" });
2469
2643
  return;
2470
2644
  }
2471
2645
  const type = (req.body ?? {}).type;
2646
+ if (type === SESSION_EXPIRED) {
2647
+ onSessionExpired?.();
2648
+ res.json({ ok: true });
2649
+ return;
2650
+ }
2472
2651
  if (typeof type !== "string" || !ALLOWED.includes(type)) {
2473
2652
  res.status(400).json({ error: "Unsupported event type" });
2474
2653
  return;
@@ -2565,23 +2744,112 @@ function startNotificationDispatcher(opts = {}) {
2565
2744
  return new NotificationDispatcher(opts).start();
2566
2745
  }
2567
2746
 
2747
+ // src/lib/session-lock.ts
2748
+ var SESSION_LOCKED_MESSAGE = "Your sign-in ended after 30 days or was revoked. Sign in again.";
2749
+ function createSessionLock(deps) {
2750
+ const { vault: vault2, port: port2 } = deps;
2751
+ const notifyFn = deps.notifyFn ?? notify;
2752
+ const unconfigureSessionFn = deps.unconfigureSessionFn ?? unconfigureSession;
2753
+ const emit = deps.emit ?? eventBus.emit.bind(eventBus);
2754
+ return function lockExpiredSession2() {
2755
+ if (!vault2.isUnlocked()) return;
2756
+ vault2.lockExpired();
2757
+ console.error("[Control Plane] Authority Server session ended \u2014 gateway LOCKED");
2758
+ void unconfigureSessionFn().catch((err) => {
2759
+ console.error("[Control Plane] Failed to push cleared session to MCP:", err);
2760
+ });
2761
+ emit("session-locked", { reason: "expired", message: SESSION_LOCKED_MESSAGE });
2762
+ const { title, message, url } = sessionExpiredNotification(port2);
2763
+ notifyFn(title, message, process.platform, url);
2764
+ };
2765
+ }
2766
+
2767
+ // src/lib/session-expiry-scheduler.ts
2768
+ var MAX_TIMEOUT_MS = 2147483647;
2769
+ var SessionExpiryScheduler = class {
2770
+ constructor(getExpiresAt, onExpired, opts = {}) {
2771
+ this.getExpiresAt = getExpiresAt;
2772
+ this.onExpired = onExpired;
2773
+ this.now = opts.now ?? Date.now;
2774
+ this.setTimeoutFn = opts.setTimeoutFn ?? setTimeout;
2775
+ this.clearTimeoutFn = opts.clearTimeoutFn ?? clearTimeout;
2776
+ this.setIntervalFn = opts.setIntervalFn ?? setInterval;
2777
+ this.clearIntervalFn = opts.clearIntervalFn ?? clearInterval;
2778
+ this.coarseIntervalMs = opts.coarseIntervalMs ?? 5 * 6e4;
2779
+ }
2780
+ preciseTimer = null;
2781
+ coarseTimer = null;
2782
+ now;
2783
+ setTimeoutFn;
2784
+ clearTimeoutFn;
2785
+ setIntervalFn;
2786
+ clearIntervalFn;
2787
+ coarseIntervalMs;
2788
+ /** Start the coarse safety-net interval and arm the precise timer. */
2789
+ start() {
2790
+ if (this.coarseTimer) return;
2791
+ this.coarseTimer = this.setIntervalFn(() => this.check(), this.coarseIntervalMs);
2792
+ this.reschedule();
2793
+ }
2794
+ stop() {
2795
+ if (this.preciseTimer) this.clearTimeoutFn(this.preciseTimer);
2796
+ if (this.coarseTimer) this.clearIntervalFn(this.coarseTimer);
2797
+ this.preciseTimer = null;
2798
+ this.coarseTimer = null;
2799
+ }
2800
+ /** Call whenever `sessionExpiresAt` changes — a login (new value) or a
2801
+ * lock (now null, via getExpiresAt) — to re-arm the precise timer. */
2802
+ reschedule() {
2803
+ if (this.preciseTimer) {
2804
+ this.clearTimeoutFn(this.preciseTimer);
2805
+ this.preciseTimer = null;
2806
+ }
2807
+ const exp = this.getExpiresAt();
2808
+ if (exp === null) return;
2809
+ const delayMs = Math.max(0, exp * 1e3 - this.now());
2810
+ if (delayMs > MAX_TIMEOUT_MS) {
2811
+ this.preciseTimer = this.setTimeoutFn(() => this.reschedule(), MAX_TIMEOUT_MS);
2812
+ return;
2813
+ }
2814
+ this.preciseTimer = this.setTimeoutFn(() => this.check(), delayMs);
2815
+ }
2816
+ /** Idempotent: only fires onExpired when the expiry has actually arrived. */
2817
+ check() {
2818
+ const exp = this.getExpiresAt();
2819
+ if (exp !== null && this.now() >= exp * 1e3) this.onExpired();
2820
+ }
2821
+ };
2822
+
2823
+ // src/lib/session-health.ts
2824
+ function buildSessionHealth(vault2) {
2825
+ const unlocked = vault2.isUnlocked();
2826
+ const lockedReason = vault2.getLockedReason();
2827
+ return {
2828
+ state: unlocked ? "active" : "locked",
2829
+ expiresAt: unlocked ? vault2.getSessionExpiresAt() : null,
2830
+ ...lockedReason ? { lockedReason } : {}
2831
+ };
2832
+ }
2833
+
2568
2834
  // src/lib/denials-reader.ts
2569
- import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
2570
- import { join as join6 } from "path";
2835
+ import { existsSync as existsSync6, readFileSync as readFileSync7 } from "fs";
2836
+ import { join as join8 } from "path";
2571
2837
  function loadDenials(dataDir, decrypt) {
2572
- const encPath = join6(dataDir, "denials.enc.json");
2573
- const plainPath = join6(dataDir, "denials.json");
2838
+ const encPath = join8(dataDir, "denials.enc.json");
2839
+ const plainPath = join8(dataDir, "denials.json");
2574
2840
  if (existsSync6(encPath)) {
2575
- const { blob } = JSON.parse(readFileSync5(encPath, "utf-8"));
2841
+ const { blob } = JSON.parse(readFileSync7(encPath, "utf-8"));
2576
2842
  return JSON.parse(decrypt(blob)).records ?? [];
2577
2843
  }
2578
2844
  if (existsSync6(plainPath)) {
2579
- return JSON.parse(readFileSync5(plainPath, "utf-8")).records ?? [];
2845
+ return JSON.parse(readFileSync7(plainPath, "utf-8")).records ?? [];
2580
2846
  }
2581
2847
  return [];
2582
2848
  }
2849
+ var DENIAL_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
2583
2850
  function selectDenials(records, opts = {}) {
2584
- let out = [...records].sort((a, b) => b.ts - a.ts);
2851
+ const cutoff = (opts.now ?? Date.now()) - DENIAL_MAX_AGE_MS;
2852
+ let out = records.filter((r) => r.ts >= cutoff).sort((a, b) => b.ts - a.ts);
2585
2853
  if (opts.since !== void 0 && Number.isFinite(opts.since)) out = out.filter((r) => r.ts >= opts.since);
2586
2854
  const count = out.length;
2587
2855
  if (opts.limit !== void 0 && Number.isFinite(opts.limit) && opts.limit > 0) out = out.slice(0, opts.limit);
@@ -2591,11 +2859,11 @@ function selectDenials(records, opts = {}) {
2591
2859
  // src/index.ts
2592
2860
  (function migrateDataDir() {
2593
2861
  if (process.env.SUVEREN_DATA_DIR) return;
2594
- const oldDir = join7(homedir6(), ".hap");
2595
- const newDir = join7(homedir6(), ".suveren");
2862
+ const oldDir = join9(homedir7(), ".hap");
2863
+ const newDir = join9(homedir7(), ".suveren");
2596
2864
  if (!existsSync7(newDir) && existsSync7(oldDir)) {
2597
2865
  try {
2598
- renameSync(oldDir, newDir);
2866
+ renameSync2(oldDir, newDir);
2599
2867
  console.error(`[Control Plane] Migrated data directory: ${oldDir} \u2192 ${newDir}`);
2600
2868
  } catch (err) {
2601
2869
  console.error(`[Control Plane] FATAL: could not migrate data directory ${oldDir} \u2192 ${newDir}:`, err);
@@ -2606,7 +2874,7 @@ function selectDenials(records, opts = {}) {
2606
2874
  var SP_URL2 = process.env.SUVEREN_AS_URL ?? "https://www.suveren.ai";
2607
2875
  var port = parseInt(process.env.SUVEREN_CP_PORT ?? "3402", 10);
2608
2876
  var HAP_MODE = process.env.HAP_MODE ?? "personal";
2609
- var UI_DIST = process.env.HAP_UI_DIST ?? join7(import.meta.dirname ?? __dirname, "../../ui/dist");
2877
+ var UI_DIST = process.env.HAP_UI_DIST ?? join9(import.meta.dirname ?? __dirname, "../../ui/dist");
2610
2878
  var vault = new Vault();
2611
2879
  var internalSecret2 = process.env.SUVEREN_INTERNAL_SECRET ?? randomBytes2(32).toString("hex");
2612
2880
  setInternalSecret(internalSecret2);
@@ -2630,7 +2898,17 @@ function loginRateLimit(req, res, next) {
2630
2898
  }
2631
2899
  next();
2632
2900
  }
2633
- app.use("/auth", jsonParser, createAuthRouter(vault, requireAuth(vault), loginRateLimit));
2901
+ var lockExpiredSession = createSessionLock({ vault, port });
2902
+ var sessionExpiryScheduler = new SessionExpiryScheduler(
2903
+ () => vault.getSessionExpiresAt(),
2904
+ lockExpiredSession
2905
+ );
2906
+ app.use("/auth", jsonParser, createAuthRouter(
2907
+ vault,
2908
+ requireAuth(vault),
2909
+ loginRateLimit,
2910
+ () => sessionExpiryScheduler.reschedule()
2911
+ ));
2634
2912
  function resolveOrigin(req) {
2635
2913
  const fwdHost = req.get("x-forwarded-host");
2636
2914
  if (fwdHost) return `${req.protocol}://${fwdHost}`;
@@ -2836,13 +3114,14 @@ app.get("/auth/oauth/:integrationId/health", authGuard, async (req, res) => {
2836
3114
  }
2837
3115
  });
2838
3116
  app.get("/events", requireAllowedHost, requireAuthQueryOrHeader(vault), createEventsHandler());
2839
- app.use("/internal", jsonParser, createInternalEventsRouter(() => internalSecret2));
3117
+ app.use("/internal", jsonParser, createInternalEventsRouter(() => internalSecret2, lockExpiredSession));
2840
3118
  app.use("/vault", jsonParser, authGuard, createVaultRouter(vault));
2841
3119
  app.use("/ai", jsonParser, authGuard, createAIRouter(vault));
2842
3120
  app.use("/ai-prompts", jsonParser, authGuard, createAIPromptsRouter());
2843
3121
  app.use("/mcp", jsonParser, authGuard, createMCPRouter());
2844
3122
  app.use("/autostart", jsonParser, authGuard, createAutostartRouter());
2845
3123
  app.use("/api/gateway-settings", jsonParser, authGuard, createGatewaySettingsRouter());
3124
+ app.use("/api/archived-mandates", jsonParser, authGuard, createArchivedMandatesRouter());
2846
3125
  app.use("/api/encrypt-intent", jsonParser, authGuard, createEncryptIntentRouter());
2847
3126
  app.use("/api/evidence-export", authGuard, createEvidenceExportRouter(SP_URL2, vault));
2848
3127
  app.use("/api/evidence", authGuard, createEvidenceRouter());
@@ -2976,7 +3255,7 @@ app.get("/active-authorizations", authGuard, async (_req, res) => {
2976
3255
  });
2977
3256
  app.get("/denials", authGuard, (req, res) => {
2978
3257
  try {
2979
- const dataDir = process.env.SUVEREN_DATA_DIR ?? join7(homedir6(), ".suveren");
3258
+ const dataDir = process.env.SUVEREN_DATA_DIR ?? join9(homedir7(), ".suveren");
2980
3259
  const all = loadDenials(dataDir, (blob) => vault.decrypt(blob));
2981
3260
  const since = Number(req.query.since);
2982
3261
  const limit = Number(req.query.limit);
@@ -3027,12 +3306,12 @@ app.put("/agent-brief/context", jsonParser, authGuard, async (req, res) => {
3027
3306
  res.status(413).json({ error: `Context exceeds ${AGENT_CONTEXT_MAX_BYTES}-byte cap` });
3028
3307
  return;
3029
3308
  }
3030
- const { mkdirSync: mkdirSync5, writeFileSync: writeFileSync5, renameSync: renameSync2 } = await import("fs");
3309
+ const { mkdirSync: mkdirSync6, writeFileSync: writeFileSync6, renameSync: renameSync3 } = await import("fs");
3031
3310
  const filePath = agentBriefPath();
3032
- mkdirSync5(dirname5(filePath), { recursive: true });
3311
+ mkdirSync6(dirname6(filePath), { recursive: true });
3033
3312
  const tmpPath = `${filePath}.tmp`;
3034
- writeFileSync5(tmpPath, content, "utf-8");
3035
- renameSync2(tmpPath, filePath);
3313
+ writeFileSync6(tmpPath, content, "utf-8");
3314
+ renameSync3(tmpPath, filePath);
3036
3315
  res.json({ ok: true });
3037
3316
  } catch (err) {
3038
3317
  console.error("[Control Plane] agent-brief/context PUT failed:", err);
@@ -3054,6 +3333,9 @@ app.use(
3054
3333
  },
3055
3334
  proxyRes: (proxyRes, req) => {
3056
3335
  const status = proxyRes.statusCode ?? 0;
3336
+ if (status === 401 && vault.isUnlocked()) {
3337
+ lockExpiredSession();
3338
+ }
3057
3339
  if (status < 200 || status >= 300) return;
3058
3340
  const method = req.method?.toUpperCase();
3059
3341
  const url = req.url ?? "";
@@ -3086,8 +3368,8 @@ function detectInstallMethod() {
3086
3368
  if (dir.includes("/node_modules/@suveren/gateway/")) return "npm";
3087
3369
  let cursor = dir;
3088
3370
  for (let i = 0; i < 8; i++) {
3089
- if (existsSync7(join7(cursor, ".git"))) return "dev";
3090
- const parent = dirname5(cursor);
3371
+ if (existsSync7(join9(cursor, ".git"))) return "dev";
3372
+ const parent = dirname6(cursor);
3091
3373
  if (parent === cursor) break;
3092
3374
  cursor = parent;
3093
3375
  }
@@ -3099,10 +3381,10 @@ function detectRunningVersion() {
3099
3381
  return process.env.HAP_BUILD_SHA ?? "dev";
3100
3382
  }
3101
3383
  const dir = import.meta.dirname ?? __dirname;
3102
- const bundlePkg = join7(dir, "..", "..", "package.json");
3384
+ const bundlePkg = join9(dir, "..", "..", "package.json");
3103
3385
  if (existsSync7(bundlePkg)) {
3104
3386
  try {
3105
- const pkg = JSON.parse(readFileSync6(bundlePkg, "utf8"));
3387
+ const pkg = JSON.parse(readFileSync8(bundlePkg, "utf8"));
3106
3388
  if (pkg.name === "@suveren/gateway" && typeof pkg.version === "string") {
3107
3389
  return pkg.version;
3108
3390
  }
@@ -3125,6 +3407,7 @@ app.get("/health", async (req, res) => {
3125
3407
  updateAvailable: update.updateAvailable,
3126
3408
  installMethod: INSTALL_METHOD,
3127
3409
  spUrl: SP_URL2,
3410
+ session: buildSessionHealth(vault),
3128
3411
  security: {
3129
3412
  note: "Gateway secures tool execution. Agent host isolation is the user's responsibility."
3130
3413
  }
@@ -3140,7 +3423,7 @@ if (existsSync7(UI_DIST)) {
3140
3423
  }));
3141
3424
  app.get("*", (_req, res) => {
3142
3425
  res.setHeader("Cache-Control", "no-store, must-revalidate");
3143
- res.sendFile(join7(UI_DIST, "index.html"));
3426
+ res.sendFile(join9(UI_DIST, "index.html"));
3144
3427
  });
3145
3428
  } else {
3146
3429
  app.get("/", (_req, res) => {
@@ -3157,6 +3440,7 @@ app.listen(port, "0.0.0.0", () => {
3157
3440
  notify(title, message, process.platform, url);
3158
3441
  }
3159
3442
  startNotificationDispatcher({ url: `http://localhost:${port}` });
3443
+ sessionExpiryScheduler.start();
3160
3444
  console.error(`[Control Plane] SP proxy: ${SP_URL2}`);
3161
3445
  console.error(`[Control Plane] UI dist: ${UI_DIST}`);
3162
3446
  console.error(`[Control Plane] Internal secret: configured`);