@suveren/gateway 0.8.6 → 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 (59) hide show
  1. package/dist/control-plane/index.mjs +262 -46
  2. package/dist/mcp-server/http.mjs +73 -9
  3. package/dist/ui/assets/index-tzStUGuj.js +110 -0
  4. package/dist/ui/icons/apple-touch-icon.png +0 -0
  5. package/dist/ui/icons/icon-192.png +0 -0
  6. package/dist/ui/icons/icon-512.png +0 -0
  7. package/dist/ui/icons/icon-maskable-512.png +0 -0
  8. package/dist/ui/index.html +7 -1
  9. package/dist/ui/manifest.webmanifest +15 -0
  10. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/auth.d.ts +15 -4
  11. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/auth.d.ts.map +1 -1
  12. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/auth.js +22 -3
  13. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/auth.js.map +1 -1
  14. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/express.d.ts.map +1 -1
  15. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/express.js +3 -1
  16. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/express.js.map +1 -1
  17. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/requestBody.d.ts +25 -0
  18. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/requestBody.d.ts.map +1 -0
  19. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/requestBody.js +64 -0
  20. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/requestBody.js.map +1 -0
  21. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/webStandardStreamableHttp.d.ts +10 -1
  22. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/webStandardStreamableHttp.d.ts.map +1 -1
  23. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/webStandardStreamableHttp.js +13 -1
  24. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/webStandardStreamableHttp.js.map +1 -1
  25. package/node_modules/@modelcontextprotocol/sdk/dist/esm/client/auth.d.ts +15 -4
  26. package/node_modules/@modelcontextprotocol/sdk/dist/esm/client/auth.d.ts.map +1 -1
  27. package/node_modules/@modelcontextprotocol/sdk/dist/esm/client/auth.js +22 -3
  28. package/node_modules/@modelcontextprotocol/sdk/dist/esm/client/auth.js.map +1 -1
  29. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/express.d.ts.map +1 -1
  30. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/express.js +3 -1
  31. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/express.js.map +1 -1
  32. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/requestBody.d.ts +25 -0
  33. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/requestBody.d.ts.map +1 -0
  34. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/requestBody.js +58 -0
  35. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/requestBody.js.map +1 -0
  36. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/webStandardStreamableHttp.d.ts +10 -1
  37. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/webStandardStreamableHttp.d.ts.map +1 -1
  38. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/webStandardStreamableHttp.js +13 -1
  39. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/webStandardStreamableHttp.js.map +1 -1
  40. package/node_modules/@modelcontextprotocol/sdk/package.json +1 -1
  41. package/node_modules/hono/dist/adapter/aws-lambda/handler.js +6 -3
  42. package/node_modules/hono/dist/adapter/lambda-edge/handler.js +26 -12
  43. package/node_modules/hono/dist/cjs/adapter/aws-lambda/handler.js +6 -3
  44. package/node_modules/hono/dist/cjs/adapter/lambda-edge/handler.js +26 -12
  45. package/node_modules/hono/dist/cjs/helper/accepts/accepts.js +7 -5
  46. package/node_modules/hono/dist/cjs/jsx/components.js +1 -1
  47. package/node_modules/hono/dist/cjs/jsx/streaming.js +1 -1
  48. package/node_modules/hono/dist/cjs/middleware/pretty-json/index.js +7 -1
  49. package/node_modules/hono/dist/cjs/router/linear-router/router.js +3 -3
  50. package/node_modules/hono/dist/cjs/utils/jwt/jwt.js +7 -6
  51. package/node_modules/hono/dist/helper/accepts/accepts.js +7 -5
  52. package/node_modules/hono/dist/jsx/components.js +1 -1
  53. package/node_modules/hono/dist/jsx/streaming.js +1 -1
  54. package/node_modules/hono/dist/middleware/pretty-json/index.js +7 -1
  55. package/node_modules/hono/dist/router/linear-router/router.js +3 -3
  56. package/node_modules/hono/dist/utils/jwt/jwt.js +7 -6
  57. package/node_modules/hono/package.json +1 -1
  58. package/package.json +1 -1
  59. package/dist/ui/assets/index-DWpjjPIf.js +0 -110
@@ -5,8 +5,8 @@ import {
5
5
 
6
6
  // src/index.ts
7
7
  import { randomBytes as randomBytes2 } from "crypto";
8
- import { existsSync as existsSync7, readFileSync as readFileSync7, renameSync as renameSync2 } from "fs";
9
- import { dirname as dirname6, join as join8 } from "path";
8
+ import { existsSync as existsSync7, readFileSync as readFileSync8, renameSync as renameSync2 } from "fs";
9
+ import { dirname as dirname6, join as join9 } from "path";
10
10
  import { homedir as homedir7 } from "os";
11
11
  import express from "express";
12
12
  import { createProxyMiddleware } from "http-proxy-middleware";
@@ -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
@@ -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 = "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";
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
  };
@@ -2453,20 +2561,20 @@ function createGatewaySettingsRouter() {
2453
2561
  import { Router as Router13 } from "express";
2454
2562
 
2455
2563
  // src/lib/archived-mandates-store.ts
2456
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, renameSync } from "fs";
2564
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, renameSync } from "fs";
2457
2565
  import { homedir as homedir6 } from "os";
2458
- import { join as join6, dirname as dirname5 } from "path";
2566
+ import { join as join7, dirname as dirname5 } from "path";
2459
2567
  var ID_PATTERN = /^[A-Za-z0-9_.:-]{1,200}$/;
2460
2568
  function isValidMandateId(id) {
2461
2569
  return typeof id === "string" && ID_PATTERN.test(id);
2462
2570
  }
2463
2571
  function storePath(dataDir) {
2464
- const dir = dataDir ?? process.env.SUVEREN_DATA_DIR ?? join6(homedir6(), ".suveren");
2465
- return join6(dir, "archived-mandates.json");
2572
+ const dir = dataDir ?? process.env.SUVEREN_DATA_DIR ?? join7(homedir6(), ".suveren");
2573
+ return join7(dir, "archived-mandates.json");
2466
2574
  }
2467
2575
  function readArchived(dataDir) {
2468
2576
  try {
2469
- const raw = JSON.parse(readFileSync5(storePath(dataDir), "utf8"));
2577
+ const raw = JSON.parse(readFileSync6(storePath(dataDir), "utf8"));
2470
2578
  if (!Array.isArray(raw.archived)) return [];
2471
2579
  return [...new Set(raw.archived.filter(isValidMandateId))];
2472
2580
  } catch {
@@ -2518,6 +2626,7 @@ function createArchivedMandatesRouter(dataDir) {
2518
2626
  import { Router as Router14 } from "express";
2519
2627
  import { timingSafeEqual as timingSafeEqual2 } from "crypto";
2520
2628
  var ALLOWED = ["proposal-added", "action-approval-needed"];
2629
+ var SESSION_EXPIRED = "session-expired";
2521
2630
  function secretMatches(provided, expected) {
2522
2631
  if (!expected) return false;
2523
2632
  if (!provided) return false;
@@ -2526,7 +2635,7 @@ function secretMatches(provided, expected) {
2526
2635
  if (a.length !== b.length) return false;
2527
2636
  return timingSafeEqual2(a, b);
2528
2637
  }
2529
- function createInternalEventsRouter(getSecret) {
2638
+ function createInternalEventsRouter(getSecret, onSessionExpired) {
2530
2639
  const router = Router14();
2531
2640
  router.post("/event", (req, res) => {
2532
2641
  if (!secretMatches(req.headers["x-internal-secret"], getSecret())) {
@@ -2534,6 +2643,11 @@ function createInternalEventsRouter(getSecret) {
2534
2643
  return;
2535
2644
  }
2536
2645
  const type = (req.body ?? {}).type;
2646
+ if (type === SESSION_EXPIRED) {
2647
+ onSessionExpired?.();
2648
+ res.json({ ok: true });
2649
+ return;
2650
+ }
2537
2651
  if (typeof type !== "string" || !ALLOWED.includes(type)) {
2538
2652
  res.status(400).json({ error: "Unsupported event type" });
2539
2653
  return;
@@ -2630,18 +2744,105 @@ function startNotificationDispatcher(opts = {}) {
2630
2744
  return new NotificationDispatcher(opts).start();
2631
2745
  }
2632
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
+
2633
2834
  // src/lib/denials-reader.ts
2634
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
2635
- import { join as join7 } from "path";
2835
+ import { existsSync as existsSync6, readFileSync as readFileSync7 } from "fs";
2836
+ import { join as join8 } from "path";
2636
2837
  function loadDenials(dataDir, decrypt) {
2637
- const encPath = join7(dataDir, "denials.enc.json");
2638
- const plainPath = join7(dataDir, "denials.json");
2838
+ const encPath = join8(dataDir, "denials.enc.json");
2839
+ const plainPath = join8(dataDir, "denials.json");
2639
2840
  if (existsSync6(encPath)) {
2640
- const { blob } = JSON.parse(readFileSync6(encPath, "utf-8"));
2841
+ const { blob } = JSON.parse(readFileSync7(encPath, "utf-8"));
2641
2842
  return JSON.parse(decrypt(blob)).records ?? [];
2642
2843
  }
2643
2844
  if (existsSync6(plainPath)) {
2644
- return JSON.parse(readFileSync6(plainPath, "utf-8")).records ?? [];
2845
+ return JSON.parse(readFileSync7(plainPath, "utf-8")).records ?? [];
2645
2846
  }
2646
2847
  return [];
2647
2848
  }
@@ -2658,8 +2859,8 @@ function selectDenials(records, opts = {}) {
2658
2859
  // src/index.ts
2659
2860
  (function migrateDataDir() {
2660
2861
  if (process.env.SUVEREN_DATA_DIR) return;
2661
- const oldDir = join8(homedir7(), ".hap");
2662
- const newDir = join8(homedir7(), ".suveren");
2862
+ const oldDir = join9(homedir7(), ".hap");
2863
+ const newDir = join9(homedir7(), ".suveren");
2663
2864
  if (!existsSync7(newDir) && existsSync7(oldDir)) {
2664
2865
  try {
2665
2866
  renameSync2(oldDir, newDir);
@@ -2673,7 +2874,7 @@ function selectDenials(records, opts = {}) {
2673
2874
  var SP_URL2 = process.env.SUVEREN_AS_URL ?? "https://www.suveren.ai";
2674
2875
  var port = parseInt(process.env.SUVEREN_CP_PORT ?? "3402", 10);
2675
2876
  var HAP_MODE = process.env.HAP_MODE ?? "personal";
2676
- var UI_DIST = process.env.HAP_UI_DIST ?? join8(import.meta.dirname ?? __dirname, "../../ui/dist");
2877
+ var UI_DIST = process.env.HAP_UI_DIST ?? join9(import.meta.dirname ?? __dirname, "../../ui/dist");
2677
2878
  var vault = new Vault();
2678
2879
  var internalSecret2 = process.env.SUVEREN_INTERNAL_SECRET ?? randomBytes2(32).toString("hex");
2679
2880
  setInternalSecret(internalSecret2);
@@ -2697,7 +2898,17 @@ function loginRateLimit(req, res, next) {
2697
2898
  }
2698
2899
  next();
2699
2900
  }
2700
- 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
+ ));
2701
2912
  function resolveOrigin(req) {
2702
2913
  const fwdHost = req.get("x-forwarded-host");
2703
2914
  if (fwdHost) return `${req.protocol}://${fwdHost}`;
@@ -2903,7 +3114,7 @@ app.get("/auth/oauth/:integrationId/health", authGuard, async (req, res) => {
2903
3114
  }
2904
3115
  });
2905
3116
  app.get("/events", requireAllowedHost, requireAuthQueryOrHeader(vault), createEventsHandler());
2906
- app.use("/internal", jsonParser, createInternalEventsRouter(() => internalSecret2));
3117
+ app.use("/internal", jsonParser, createInternalEventsRouter(() => internalSecret2, lockExpiredSession));
2907
3118
  app.use("/vault", jsonParser, authGuard, createVaultRouter(vault));
2908
3119
  app.use("/ai", jsonParser, authGuard, createAIRouter(vault));
2909
3120
  app.use("/ai-prompts", jsonParser, authGuard, createAIPromptsRouter());
@@ -3044,7 +3255,7 @@ app.get("/active-authorizations", authGuard, async (_req, res) => {
3044
3255
  });
3045
3256
  app.get("/denials", authGuard, (req, res) => {
3046
3257
  try {
3047
- const dataDir = process.env.SUVEREN_DATA_DIR ?? join8(homedir7(), ".suveren");
3258
+ const dataDir = process.env.SUVEREN_DATA_DIR ?? join9(homedir7(), ".suveren");
3048
3259
  const all = loadDenials(dataDir, (blob) => vault.decrypt(blob));
3049
3260
  const since = Number(req.query.since);
3050
3261
  const limit = Number(req.query.limit);
@@ -3122,6 +3333,9 @@ app.use(
3122
3333
  },
3123
3334
  proxyRes: (proxyRes, req) => {
3124
3335
  const status = proxyRes.statusCode ?? 0;
3336
+ if (status === 401 && vault.isUnlocked()) {
3337
+ lockExpiredSession();
3338
+ }
3125
3339
  if (status < 200 || status >= 300) return;
3126
3340
  const method = req.method?.toUpperCase();
3127
3341
  const url = req.url ?? "";
@@ -3154,7 +3368,7 @@ function detectInstallMethod() {
3154
3368
  if (dir.includes("/node_modules/@suveren/gateway/")) return "npm";
3155
3369
  let cursor = dir;
3156
3370
  for (let i = 0; i < 8; i++) {
3157
- if (existsSync7(join8(cursor, ".git"))) return "dev";
3371
+ if (existsSync7(join9(cursor, ".git"))) return "dev";
3158
3372
  const parent = dirname6(cursor);
3159
3373
  if (parent === cursor) break;
3160
3374
  cursor = parent;
@@ -3167,10 +3381,10 @@ function detectRunningVersion() {
3167
3381
  return process.env.HAP_BUILD_SHA ?? "dev";
3168
3382
  }
3169
3383
  const dir = import.meta.dirname ?? __dirname;
3170
- const bundlePkg = join8(dir, "..", "..", "package.json");
3384
+ const bundlePkg = join9(dir, "..", "..", "package.json");
3171
3385
  if (existsSync7(bundlePkg)) {
3172
3386
  try {
3173
- const pkg = JSON.parse(readFileSync7(bundlePkg, "utf8"));
3387
+ const pkg = JSON.parse(readFileSync8(bundlePkg, "utf8"));
3174
3388
  if (pkg.name === "@suveren/gateway" && typeof pkg.version === "string") {
3175
3389
  return pkg.version;
3176
3390
  }
@@ -3193,6 +3407,7 @@ app.get("/health", async (req, res) => {
3193
3407
  updateAvailable: update.updateAvailable,
3194
3408
  installMethod: INSTALL_METHOD,
3195
3409
  spUrl: SP_URL2,
3410
+ session: buildSessionHealth(vault),
3196
3411
  security: {
3197
3412
  note: "Gateway secures tool execution. Agent host isolation is the user's responsibility."
3198
3413
  }
@@ -3208,7 +3423,7 @@ if (existsSync7(UI_DIST)) {
3208
3423
  }));
3209
3424
  app.get("*", (_req, res) => {
3210
3425
  res.setHeader("Cache-Control", "no-store, must-revalidate");
3211
- res.sendFile(join8(UI_DIST, "index.html"));
3426
+ res.sendFile(join9(UI_DIST, "index.html"));
3212
3427
  });
3213
3428
  } else {
3214
3429
  app.get("/", (_req, res) => {
@@ -3225,6 +3440,7 @@ app.listen(port, "0.0.0.0", () => {
3225
3440
  notify(title, message, process.platform, url);
3226
3441
  }
3227
3442
  startNotificationDispatcher({ url: `http://localhost:${port}` });
3443
+ sessionExpiryScheduler.start();
3228
3444
  console.error(`[Control Plane] SP proxy: ${SP_URL2}`);
3229
3445
  console.error(`[Control Plane] UI dist: ${UI_DIST}`);
3230
3446
  console.error(`[Control Plane] Internal secret: configured`);