@sanctuary-framework/mcp-server 0.5.1 → 0.5.2

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.
package/dist/index.d.cts CHANGED
@@ -1996,8 +1996,11 @@ declare class DashboardApprovalChannel implements ApprovalChannel {
1996
1996
  private baseline;
1997
1997
  private auditLog;
1998
1998
  private dashboardHTML;
1999
+ private loginHTML;
1999
2000
  private authToken;
2000
2001
  private useTLS;
2002
+ /** Session TTL: longer for localhost, shorter for remote */
2003
+ private sessionTTLMs;
2001
2004
  /** SEC-012: Short-lived session store. Sessions replace URL query tokens. */
2002
2005
  private sessions;
2003
2006
  private sessionCleanupTimer;
@@ -2037,6 +2040,15 @@ declare class DashboardApprovalChannel implements ApprovalChannel {
2037
2040
  * Returns true if auth passes, false if blocked (response already sent).
2038
2041
  */
2039
2042
  private checkAuth;
2043
+ /**
2044
+ * Check if a request is authenticated WITHOUT sending a response.
2045
+ * Used to decide between login page vs dashboard for GET /.
2046
+ */
2047
+ private isAuthenticated;
2048
+ /**
2049
+ * Parse a specific cookie value from the request.
2050
+ */
2051
+ private parseCookie;
2040
2052
  /**
2041
2053
  * Create a short-lived session by exchanging the long-lived auth token
2042
2054
  * (provided in the Authorization header) for a session ID.
@@ -2074,6 +2086,7 @@ declare class DashboardApprovalChannel implements ApprovalChannel {
2074
2086
  * normal checkAuth flow.
2075
2087
  */
2076
2088
  private handleSessionExchange;
2089
+ private serveLoginPage;
2077
2090
  private serveDashboard;
2078
2091
  private handleSSE;
2079
2092
  private handleStatus;
@@ -2120,6 +2133,15 @@ declare class DashboardApprovalChannel implements ApprovalChannel {
2120
2133
  * Broadcast current protection status to connected dashboards.
2121
2134
  */
2122
2135
  broadcastProtectionStatus(data: Record<string, unknown>): void;
2136
+ /**
2137
+ * Create a pre-authenticated URL for the dashboard.
2138
+ * Used by the sanctuary_dashboard_open tool and at startup.
2139
+ */
2140
+ createSessionUrl(): string;
2141
+ /**
2142
+ * Get the base URL for the dashboard.
2143
+ */
2144
+ getBaseUrl(): string;
2123
2145
  /** Get the number of pending requests */
2124
2146
  get pendingCount(): number;
2125
2147
  /** Get the number of connected SSE clients */
package/dist/index.d.ts CHANGED
@@ -1996,8 +1996,11 @@ declare class DashboardApprovalChannel implements ApprovalChannel {
1996
1996
  private baseline;
1997
1997
  private auditLog;
1998
1998
  private dashboardHTML;
1999
+ private loginHTML;
1999
2000
  private authToken;
2000
2001
  private useTLS;
2002
+ /** Session TTL: longer for localhost, shorter for remote */
2003
+ private sessionTTLMs;
2001
2004
  /** SEC-012: Short-lived session store. Sessions replace URL query tokens. */
2002
2005
  private sessions;
2003
2006
  private sessionCleanupTimer;
@@ -2037,6 +2040,15 @@ declare class DashboardApprovalChannel implements ApprovalChannel {
2037
2040
  * Returns true if auth passes, false if blocked (response already sent).
2038
2041
  */
2039
2042
  private checkAuth;
2043
+ /**
2044
+ * Check if a request is authenticated WITHOUT sending a response.
2045
+ * Used to decide between login page vs dashboard for GET /.
2046
+ */
2047
+ private isAuthenticated;
2048
+ /**
2049
+ * Parse a specific cookie value from the request.
2050
+ */
2051
+ private parseCookie;
2040
2052
  /**
2041
2053
  * Create a short-lived session by exchanging the long-lived auth token
2042
2054
  * (provided in the Authorization header) for a session ID.
@@ -2074,6 +2086,7 @@ declare class DashboardApprovalChannel implements ApprovalChannel {
2074
2086
  * normal checkAuth flow.
2075
2087
  */
2076
2088
  private handleSessionExchange;
2089
+ private serveLoginPage;
2077
2090
  private serveDashboard;
2078
2091
  private handleSSE;
2079
2092
  private handleStatus;
@@ -2120,6 +2133,15 @@ declare class DashboardApprovalChannel implements ApprovalChannel {
2120
2133
  * Broadcast current protection status to connected dashboards.
2121
2134
  */
2122
2135
  broadcastProtectionStatus(data: Record<string, unknown>): void;
2136
+ /**
2137
+ * Create a pre-authenticated URL for the dashboard.
2138
+ * Used by the sanctuary_dashboard_open tool and at startup.
2139
+ */
2140
+ createSessionUrl(): string;
2141
+ /**
2142
+ * Get the base URL for the dashboard.
2143
+ */
2144
+ getBaseUrl(): string;
2123
2145
  /** Get the number of pending requests */
2124
2146
  get pendingCount(): number;
2125
2147
  /** Get the number of connected SSE clients */
package/dist/index.js CHANGED
@@ -4055,6 +4055,181 @@ var AutoApproveChannel = class {
4055
4055
  };
4056
4056
 
4057
4057
  // src/principal-policy/dashboard-html.ts
4058
+ function generateLoginHTML(options) {
4059
+ return `<!DOCTYPE html>
4060
+ <html lang="en">
4061
+ <head>
4062
+ <meta charset="utf-8">
4063
+ <meta name="viewport" content="width=device-width, initial-scale=1">
4064
+ <title>Sanctuary \u2014 Login</title>
4065
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
4066
+ <style>
4067
+ :root {
4068
+ --bg: #0d1117;
4069
+ --surface: #161b22;
4070
+ --border: #30363d;
4071
+ --text-primary: #e6edf3;
4072
+ --text-secondary: #8b949e;
4073
+ --green: #3fb950;
4074
+ --red: #f85149;
4075
+ --blue: #58a6ff;
4076
+ --mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
4077
+ --sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
4078
+ --radius: 6px;
4079
+ }
4080
+ * { box-sizing: border-box; margin: 0; padding: 0; }
4081
+ html, body { width: 100%; height: 100%; }
4082
+ body {
4083
+ font-family: var(--sans);
4084
+ background: var(--bg);
4085
+ color: var(--text-primary);
4086
+ display: flex;
4087
+ align-items: center;
4088
+ justify-content: center;
4089
+ }
4090
+ .login-container {
4091
+ width: 100%;
4092
+ max-width: 400px;
4093
+ padding: 40px 32px;
4094
+ background: var(--surface);
4095
+ border: 1px solid var(--border);
4096
+ border-radius: 12px;
4097
+ }
4098
+ .login-logo {
4099
+ text-align: center;
4100
+ font-size: 20px;
4101
+ font-weight: 700;
4102
+ letter-spacing: -0.5px;
4103
+ margin-bottom: 8px;
4104
+ }
4105
+ .login-logo span { color: var(--blue); }
4106
+ .login-version {
4107
+ text-align: center;
4108
+ font-size: 11px;
4109
+ color: var(--text-secondary);
4110
+ font-family: var(--mono);
4111
+ margin-bottom: 32px;
4112
+ }
4113
+ .login-label {
4114
+ display: block;
4115
+ font-size: 13px;
4116
+ font-weight: 600;
4117
+ color: var(--text-secondary);
4118
+ margin-bottom: 8px;
4119
+ }
4120
+ .login-input {
4121
+ width: 100%;
4122
+ padding: 10px 14px;
4123
+ background: var(--bg);
4124
+ border: 1px solid var(--border);
4125
+ border-radius: var(--radius);
4126
+ color: var(--text-primary);
4127
+ font-family: var(--mono);
4128
+ font-size: 14px;
4129
+ outline: none;
4130
+ transition: border-color 0.15s;
4131
+ }
4132
+ .login-input:focus { border-color: var(--blue); }
4133
+ .login-input::placeholder { color: var(--text-secondary); opacity: 0.5; }
4134
+ .login-btn {
4135
+ width: 100%;
4136
+ margin-top: 20px;
4137
+ padding: 10px;
4138
+ background: var(--blue);
4139
+ color: var(--bg);
4140
+ border: none;
4141
+ border-radius: var(--radius);
4142
+ font-size: 14px;
4143
+ font-weight: 600;
4144
+ cursor: pointer;
4145
+ transition: opacity 0.15s;
4146
+ font-family: var(--sans);
4147
+ }
4148
+ .login-btn:hover { opacity: 0.9; }
4149
+ .login-btn:disabled { opacity: 0.5; cursor: not-allowed; }
4150
+ .login-error {
4151
+ margin-top: 16px;
4152
+ padding: 10px 14px;
4153
+ background: rgba(248, 81, 73, 0.1);
4154
+ border: 1px solid var(--red);
4155
+ border-radius: var(--radius);
4156
+ font-size: 12px;
4157
+ color: var(--red);
4158
+ display: none;
4159
+ }
4160
+ .login-hint {
4161
+ margin-top: 24px;
4162
+ padding-top: 16px;
4163
+ border-top: 1px solid var(--border);
4164
+ font-size: 11px;
4165
+ color: var(--text-secondary);
4166
+ line-height: 1.5;
4167
+ }
4168
+ .login-hint code {
4169
+ font-family: var(--mono);
4170
+ background: var(--bg);
4171
+ padding: 1px 4px;
4172
+ border-radius: 3px;
4173
+ font-size: 10px;
4174
+ }
4175
+ </style>
4176
+ </head>
4177
+ <body>
4178
+ <div class="login-container">
4179
+ <div class="login-logo"><span>&#9670;</span> SANCTUARY</div>
4180
+ <div class="login-version">Principal Dashboard v${options.serverVersion}</div>
4181
+ <form id="loginForm" onsubmit="return handleLogin(event)">
4182
+ <label class="login-label" for="tokenInput">Dashboard Auth Token</label>
4183
+ <input class="login-input" type="password" id="tokenInput"
4184
+ placeholder="Enter your auth token" autocomplete="off" autofocus required>
4185
+ <button class="login-btn" type="submit" id="loginBtn">Open Dashboard</button>
4186
+ </form>
4187
+ <div class="login-error" id="loginError"></div>
4188
+ <div class="login-hint">
4189
+ Your token is set via <code>SANCTUARY_DASHBOARD_AUTH_TOKEN</code> environment variable,
4190
+ or check your server's startup output.
4191
+ </div>
4192
+ </div>
4193
+ <script>
4194
+ async function handleLogin(e) {
4195
+ e.preventDefault();
4196
+ var btn = document.getElementById('loginBtn');
4197
+ var errEl = document.getElementById('loginError');
4198
+ var token = document.getElementById('tokenInput').value.trim();
4199
+ if (!token) return false;
4200
+ btn.disabled = true;
4201
+ btn.textContent = 'Authenticating...';
4202
+ errEl.style.display = 'none';
4203
+ try {
4204
+ var resp = await fetch('/auth/session', {
4205
+ method: 'POST',
4206
+ headers: { 'Authorization': 'Bearer ' + token }
4207
+ });
4208
+ if (!resp.ok) {
4209
+ var data = await resp.json().catch(function() { return {}; });
4210
+ throw new Error(data.error || 'Authentication failed');
4211
+ }
4212
+ var result = await resp.json();
4213
+ // Store token in sessionStorage for auto-renewal inside the dashboard
4214
+ try { sessionStorage.setItem('sanctuary_token', token); } catch(_) {}
4215
+ // Set session cookie
4216
+ var maxAge = result.expires_in_seconds || 300;
4217
+ document.cookie = 'sanctuary_session=' + result.session_id +
4218
+ '; path=/; SameSite=Strict; max-age=' + maxAge;
4219
+ // Reload to enter the dashboard
4220
+ window.location.reload();
4221
+ } catch (err) {
4222
+ errEl.textContent = err.message || 'Authentication failed. Check your token.';
4223
+ errEl.style.display = 'block';
4224
+ btn.disabled = false;
4225
+ btn.textContent = 'Open Dashboard';
4226
+ }
4227
+ return false;
4228
+ }
4229
+ </script>
4230
+ </body>
4231
+ </html>`;
4232
+ }
4058
4233
  function generateDashboardHTML(options) {
4059
4234
  return `<!DOCTYPE html>
4060
4235
  <html lang="en">
@@ -4924,7 +5099,9 @@ function generateDashboardHTML(options) {
4924
5099
  // \u2500\u2500 Configuration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4925
5100
 
4926
5101
  const TIMEOUT_SECONDS = ${options.timeoutSeconds};
4927
- const AUTH_TOKEN = ${options.authToken ? JSON.stringify(options.authToken) : "null"};
5102
+ // AUTH_TOKEN: embedded token (for direct session access) or from sessionStorage (login page flow)
5103
+ const EMBEDDED_TOKEN = ${options.authToken ? JSON.stringify(options.authToken) : "null"};
5104
+ const AUTH_TOKEN = EMBEDDED_TOKEN || (function() { try { return sessionStorage.getItem('sanctuary_token'); } catch(_) { return null; } })();
4928
5105
  const MAX_ACTIVITY_ITEMS = 100;
4929
5106
  const MAX_THREAT_ITEMS = 20;
4930
5107
 
@@ -4939,6 +5116,7 @@ function generateDashboardHTML(options) {
4939
5116
  const activityItems = [];
4940
5117
  const threatItems = [];
4941
5118
  let sovereigntyScore = 85;
5119
+ let sessionRenewalTimer = null;
4942
5120
 
4943
5121
  // \u2500\u2500 Auth Helpers (SEC-012) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4944
5122
 
@@ -4954,6 +5132,11 @@ function generateDashboardHTML(options) {
4954
5132
  return url + sep + 'session=' + SESSION_ID;
4955
5133
  }
4956
5134
 
5135
+ function setCookie(sessionId, maxAge) {
5136
+ document.cookie = 'sanctuary_session=' + sessionId +
5137
+ '; path=/; SameSite=Strict; max-age=' + maxAge;
5138
+ }
5139
+
4957
5140
  async function exchangeSession() {
4958
5141
  if (!AUTH_TOKEN) return;
4959
5142
  try {
@@ -4961,14 +5144,35 @@ function generateDashboardHTML(options) {
4961
5144
  if (resp.ok) {
4962
5145
  const data = await resp.json();
4963
5146
  SESSION_ID = data.session_id;
4964
- const refreshMs = (data.expires_in_seconds || 300) * 800;
4965
- setTimeout(() => { exchangeSession(); reconnectSSE(); }, refreshMs);
5147
+ var ttl = data.expires_in_seconds || 300;
5148
+ // Update cookie with new session
5149
+ setCookie(SESSION_ID, ttl);
5150
+ // Schedule renewal at 80% of TTL
5151
+ if (sessionRenewalTimer) clearTimeout(sessionRenewalTimer);
5152
+ sessionRenewalTimer = setTimeout(function() {
5153
+ exchangeSession().then(function() { reconnectSSE(); });
5154
+ }, ttl * 800);
5155
+ } else if (resp.status === 401) {
5156
+ // Token invalid or expired \u2014 show non-destructive re-login overlay
5157
+ showSessionExpired();
4966
5158
  }
4967
5159
  } catch (e) {
4968
- // Retry on next connect
5160
+ // Network error \u2014 retry in 30s
5161
+ if (sessionRenewalTimer) clearTimeout(sessionRenewalTimer);
5162
+ sessionRenewalTimer = setTimeout(function() {
5163
+ exchangeSession().then(function() { reconnectSSE(); });
5164
+ }, 30000);
4969
5165
  }
4970
5166
  }
4971
5167
 
5168
+ function showSessionExpired() {
5169
+ // Clear stored token
5170
+ try { sessionStorage.removeItem('sanctuary_token'); } catch(_) {}
5171
+ // Redirect to login page
5172
+ document.cookie = 'sanctuary_session=; path=/; max-age=0';
5173
+ window.location.reload();
5174
+ }
5175
+
4972
5176
  // \u2500\u2500 UI Utilities \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4973
5177
 
4974
5178
  function esc(s) {
@@ -5441,7 +5645,8 @@ function generateDashboardHTML(options) {
5441
5645
  }
5442
5646
 
5443
5647
  // src/principal-policy/dashboard.ts
5444
- var SESSION_TTL_MS = 5 * 60 * 1e3;
5648
+ var SESSION_TTL_REMOTE_MS = 5 * 60 * 1e3;
5649
+ var SESSION_TTL_LOCAL_MS = 24 * 60 * 60 * 1e3;
5445
5650
  var MAX_SESSIONS = 1e3;
5446
5651
  var RATE_LIMIT_WINDOW_MS = 6e4;
5447
5652
  var RATE_LIMIT_GENERAL = 120;
@@ -5456,8 +5661,11 @@ var DashboardApprovalChannel = class {
5456
5661
  baseline = null;
5457
5662
  auditLog = null;
5458
5663
  dashboardHTML;
5664
+ loginHTML;
5459
5665
  authToken;
5460
5666
  useTLS;
5667
+ /** Session TTL: longer for localhost, shorter for remote */
5668
+ sessionTTLMs;
5461
5669
  /** SEC-012: Short-lived session store. Sessions replace URL query tokens. */
5462
5670
  sessions = /* @__PURE__ */ new Map();
5463
5671
  sessionCleanupTimer = null;
@@ -5467,11 +5675,14 @@ var DashboardApprovalChannel = class {
5467
5675
  this.config = config;
5468
5676
  this.authToken = config.auth_token;
5469
5677
  this.useTLS = !!(config.tls?.cert_path && config.tls?.key_path);
5678
+ const isLocalhost = config.host === "127.0.0.1" || config.host === "localhost" || config.host === "::1";
5679
+ this.sessionTTLMs = isLocalhost ? SESSION_TTL_LOCAL_MS : SESSION_TTL_REMOTE_MS;
5470
5680
  this.dashboardHTML = generateDashboardHTML({
5471
5681
  timeoutSeconds: config.timeout_seconds,
5472
5682
  serverVersion: SANCTUARY_VERSION,
5473
5683
  authToken: this.authToken
5474
5684
  });
5685
+ this.loginHTML = generateLoginHTML({ serverVersion: SANCTUARY_VERSION });
5475
5686
  this.sessionCleanupTimer = setInterval(() => this.cleanupSessions(), 6e4);
5476
5687
  }
5477
5688
  /**
@@ -5501,25 +5712,26 @@ var DashboardApprovalChannel = class {
5501
5712
  const protocol = this.useTLS ? "https" : "http";
5502
5713
  const baseUrl = `${protocol}://${this.config.host}:${this.config.port}`;
5503
5714
  this.httpServer.listen(this.config.port, this.config.host, () => {
5504
- if (this.authToken) {
5505
- const hint = this.authToken.slice(0, 4) + "..." + this.authToken.slice(-4);
5506
- process.stderr.write(
5507
- `
5715
+ process.stderr.write(
5716
+ `
5508
5717
  Sanctuary Principal Dashboard: ${baseUrl}
5509
5718
  `
5510
- );
5719
+ );
5720
+ if (this.authToken) {
5721
+ const sessionUrl = this.createSessionUrl();
5511
5722
  process.stderr.write(
5512
- ` Auth required (token: ${hint}). Use Authorization: Bearer <TOKEN> header.
5513
-
5723
+ ` Quick open: ${sessionUrl}
5514
5724
  `
5515
5725
  );
5516
- } else {
5726
+ const hint = this.authToken.slice(0, 4) + "..." + this.authToken.slice(-4);
5517
5727
  process.stderr.write(
5518
- `
5519
- Sanctuary Principal Dashboard: ${baseUrl}
5728
+ ` Auth token: ${hint}
5520
5729
 
5521
5730
  `
5522
5731
  );
5732
+ } else {
5733
+ process.stderr.write(`
5734
+ `);
5523
5735
  }
5524
5736
  resolve();
5525
5737
  });
@@ -5623,10 +5835,47 @@ var DashboardApprovalChannel = class {
5623
5835
  if (sessionId && this.validateSession(sessionId)) {
5624
5836
  return true;
5625
5837
  }
5838
+ const cookieSession = this.parseCookie(req, "sanctuary_session");
5839
+ if (cookieSession && this.validateSession(cookieSession)) {
5840
+ return true;
5841
+ }
5626
5842
  res.writeHead(401, { "Content-Type": "application/json" });
5627
5843
  res.end(JSON.stringify({ error: "Unauthorized \u2014 use Authorization: Bearer header or a valid session" }));
5628
5844
  return false;
5629
5845
  }
5846
+ /**
5847
+ * Check if a request is authenticated WITHOUT sending a response.
5848
+ * Used to decide between login page vs dashboard for GET /.
5849
+ */
5850
+ isAuthenticated(req, url) {
5851
+ if (!this.authToken) return true;
5852
+ const authHeader = req.headers.authorization;
5853
+ if (authHeader) {
5854
+ const parts = authHeader.split(" ");
5855
+ if (parts.length === 2 && parts[0] === "Bearer" && parts[1] === this.authToken) {
5856
+ return true;
5857
+ }
5858
+ }
5859
+ const sessionId = url.searchParams.get("session");
5860
+ if (sessionId && this.validateSession(sessionId)) return true;
5861
+ const cookieSession = this.parseCookie(req, "sanctuary_session");
5862
+ if (cookieSession && this.validateSession(cookieSession)) return true;
5863
+ return false;
5864
+ }
5865
+ /**
5866
+ * Parse a specific cookie value from the request.
5867
+ */
5868
+ parseCookie(req, name) {
5869
+ const header = req.headers.cookie;
5870
+ if (!header) return null;
5871
+ for (const part of header.split(";")) {
5872
+ const [key, ...rest] = part.split("=");
5873
+ if (key?.trim() === name) {
5874
+ return rest.join("=").trim();
5875
+ }
5876
+ }
5877
+ return null;
5878
+ }
5630
5879
  // ── Session Management (SEC-012) ──────────────────────────────────
5631
5880
  /**
5632
5881
  * Create a short-lived session by exchanging the long-lived auth token
@@ -5647,7 +5896,7 @@ var DashboardApprovalChannel = class {
5647
5896
  this.sessions.set(id, {
5648
5897
  id,
5649
5898
  created_at: now,
5650
- expires_at: now + SESSION_TTL_MS
5899
+ expires_at: now + this.sessionTTLMs
5651
5900
  });
5652
5901
  return id;
5653
5902
  }
@@ -5746,13 +5995,26 @@ var DashboardApprovalChannel = class {
5746
5995
  res.end();
5747
5996
  return;
5748
5997
  }
5749
- if (!this.checkAuth(req, url, res)) return;
5750
- if (!this.checkRateLimit(req, res, "general")) return;
5751
- try {
5752
- if (method === "POST" && url.pathname === "/auth/session") {
5998
+ if (method === "POST" && url.pathname === "/auth/session") {
5999
+ if (!this.checkRateLimit(req, res, "general")) return;
6000
+ try {
5753
6001
  this.handleSessionExchange(req, res);
6002
+ } catch {
6003
+ res.writeHead(500, { "Content-Type": "application/json" });
6004
+ res.end(JSON.stringify({ error: "Internal server error" }));
6005
+ }
6006
+ return;
6007
+ }
6008
+ if (method === "GET" && url.pathname === "/" && this.authToken) {
6009
+ if (!this.isAuthenticated(req, url)) {
6010
+ if (!this.checkRateLimit(req, res, "general")) return;
6011
+ this.serveLoginPage(res);
5754
6012
  return;
5755
6013
  }
6014
+ }
6015
+ if (!this.checkAuth(req, url, res)) return;
6016
+ if (!this.checkRateLimit(req, res, "general")) return;
6017
+ try {
5756
6018
  if (method === "GET" && url.pathname === "/") {
5757
6019
  this.serveDashboard(res);
5758
6020
  } else if (method === "GET" && url.pathname === "/events") {
@@ -5809,12 +6071,23 @@ var DashboardApprovalChannel = class {
5809
6071
  return;
5810
6072
  }
5811
6073
  const sessionId = this.createSession();
5812
- res.writeHead(200, { "Content-Type": "application/json" });
6074
+ const ttlSeconds = Math.floor(this.sessionTTLMs / 1e3);
6075
+ res.writeHead(200, {
6076
+ "Content-Type": "application/json",
6077
+ "Set-Cookie": `sanctuary_session=${sessionId}; Path=/; SameSite=Strict; Max-Age=${ttlSeconds}`
6078
+ });
5813
6079
  res.end(JSON.stringify({
5814
6080
  session_id: sessionId,
5815
- expires_in_seconds: SESSION_TTL_MS / 1e3
6081
+ expires_in_seconds: ttlSeconds
5816
6082
  }));
5817
6083
  }
6084
+ serveLoginPage(res) {
6085
+ res.writeHead(200, {
6086
+ "Content-Type": "text/html; charset=utf-8",
6087
+ "Cache-Control": "no-cache, no-store"
6088
+ });
6089
+ res.end(this.loginHTML);
6090
+ }
5818
6091
  serveDashboard(res) {
5819
6092
  res.writeHead(200, {
5820
6093
  "Content-Type": "text/html; charset=utf-8",
@@ -5990,6 +6263,22 @@ data: ${JSON.stringify(data)}
5990
6263
  broadcastProtectionStatus(data) {
5991
6264
  this.broadcastSSE("protection-status", data);
5992
6265
  }
6266
+ /**
6267
+ * Create a pre-authenticated URL for the dashboard.
6268
+ * Used by the sanctuary_dashboard_open tool and at startup.
6269
+ */
6270
+ createSessionUrl() {
6271
+ const sessionId = this.createSession();
6272
+ const protocol = this.useTLS ? "https" : "http";
6273
+ return `${protocol}://${this.config.host}:${this.config.port}/?session=${sessionId}`;
6274
+ }
6275
+ /**
6276
+ * Get the base URL for the dashboard.
6277
+ */
6278
+ getBaseUrl() {
6279
+ const protocol = this.useTLS ? "https" : "http";
6280
+ return `${protocol}://${this.config.host}:${this.config.port}`;
6281
+ }
5993
6282
  /** Get the number of pending requests */
5994
6283
  get pendingCount() {
5995
6284
  return this.pending.size;
@@ -11859,6 +12148,32 @@ async function createSanctuaryServer(options) {
11859
12148
  } : void 0;
11860
12149
  const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog, injectionDetector, onInjectionAlert);
11861
12150
  const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
12151
+ const dashboardTools = [];
12152
+ if (dashboard) {
12153
+ dashboardTools.push({
12154
+ name: "sanctuary/dashboard_open",
12155
+ description: "Generate a one-click URL to open the Principal Dashboard in a browser. Returns a pre-authenticated link \u2014 no manual token entry needed.",
12156
+ inputSchema: {
12157
+ type: "object",
12158
+ properties: {}
12159
+ },
12160
+ handler: async () => {
12161
+ const url = dashboard.createSessionUrl();
12162
+ return {
12163
+ content: [
12164
+ {
12165
+ type: "text",
12166
+ text: JSON.stringify({
12167
+ dashboard_url: url,
12168
+ base_url: dashboard.getBaseUrl(),
12169
+ note: "Click the dashboard_url to open the Principal Dashboard. The session is pre-authenticated."
12170
+ }, null, 2)
12171
+ }
12172
+ ]
12173
+ };
12174
+ }
12175
+ });
12176
+ }
11862
12177
  let allTools = [
11863
12178
  ...l1Tools,
11864
12179
  ...l2Tools,
@@ -11872,6 +12187,7 @@ async function createSanctuaryServer(options) {
11872
12187
  ...auditTools,
11873
12188
  ...contextGateTools,
11874
12189
  ...hardeningTools,
12190
+ ...dashboardTools,
11875
12191
  manifestTool
11876
12192
  ];
11877
12193
  allTools = allTools.map((tool) => ({