@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/cli.cjs +338 -22
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +338 -22
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +338 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +338 -22
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -4058,6 +4058,181 @@ var AutoApproveChannel = class {
|
|
|
4058
4058
|
};
|
|
4059
4059
|
|
|
4060
4060
|
// src/principal-policy/dashboard-html.ts
|
|
4061
|
+
function generateLoginHTML(options) {
|
|
4062
|
+
return `<!DOCTYPE html>
|
|
4063
|
+
<html lang="en">
|
|
4064
|
+
<head>
|
|
4065
|
+
<meta charset="utf-8">
|
|
4066
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
4067
|
+
<title>Sanctuary \u2014 Login</title>
|
|
4068
|
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
|
|
4069
|
+
<style>
|
|
4070
|
+
:root {
|
|
4071
|
+
--bg: #0d1117;
|
|
4072
|
+
--surface: #161b22;
|
|
4073
|
+
--border: #30363d;
|
|
4074
|
+
--text-primary: #e6edf3;
|
|
4075
|
+
--text-secondary: #8b949e;
|
|
4076
|
+
--green: #3fb950;
|
|
4077
|
+
--red: #f85149;
|
|
4078
|
+
--blue: #58a6ff;
|
|
4079
|
+
--mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
|
4080
|
+
--sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
4081
|
+
--radius: 6px;
|
|
4082
|
+
}
|
|
4083
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
4084
|
+
html, body { width: 100%; height: 100%; }
|
|
4085
|
+
body {
|
|
4086
|
+
font-family: var(--sans);
|
|
4087
|
+
background: var(--bg);
|
|
4088
|
+
color: var(--text-primary);
|
|
4089
|
+
display: flex;
|
|
4090
|
+
align-items: center;
|
|
4091
|
+
justify-content: center;
|
|
4092
|
+
}
|
|
4093
|
+
.login-container {
|
|
4094
|
+
width: 100%;
|
|
4095
|
+
max-width: 400px;
|
|
4096
|
+
padding: 40px 32px;
|
|
4097
|
+
background: var(--surface);
|
|
4098
|
+
border: 1px solid var(--border);
|
|
4099
|
+
border-radius: 12px;
|
|
4100
|
+
}
|
|
4101
|
+
.login-logo {
|
|
4102
|
+
text-align: center;
|
|
4103
|
+
font-size: 20px;
|
|
4104
|
+
font-weight: 700;
|
|
4105
|
+
letter-spacing: -0.5px;
|
|
4106
|
+
margin-bottom: 8px;
|
|
4107
|
+
}
|
|
4108
|
+
.login-logo span { color: var(--blue); }
|
|
4109
|
+
.login-version {
|
|
4110
|
+
text-align: center;
|
|
4111
|
+
font-size: 11px;
|
|
4112
|
+
color: var(--text-secondary);
|
|
4113
|
+
font-family: var(--mono);
|
|
4114
|
+
margin-bottom: 32px;
|
|
4115
|
+
}
|
|
4116
|
+
.login-label {
|
|
4117
|
+
display: block;
|
|
4118
|
+
font-size: 13px;
|
|
4119
|
+
font-weight: 600;
|
|
4120
|
+
color: var(--text-secondary);
|
|
4121
|
+
margin-bottom: 8px;
|
|
4122
|
+
}
|
|
4123
|
+
.login-input {
|
|
4124
|
+
width: 100%;
|
|
4125
|
+
padding: 10px 14px;
|
|
4126
|
+
background: var(--bg);
|
|
4127
|
+
border: 1px solid var(--border);
|
|
4128
|
+
border-radius: var(--radius);
|
|
4129
|
+
color: var(--text-primary);
|
|
4130
|
+
font-family: var(--mono);
|
|
4131
|
+
font-size: 14px;
|
|
4132
|
+
outline: none;
|
|
4133
|
+
transition: border-color 0.15s;
|
|
4134
|
+
}
|
|
4135
|
+
.login-input:focus { border-color: var(--blue); }
|
|
4136
|
+
.login-input::placeholder { color: var(--text-secondary); opacity: 0.5; }
|
|
4137
|
+
.login-btn {
|
|
4138
|
+
width: 100%;
|
|
4139
|
+
margin-top: 20px;
|
|
4140
|
+
padding: 10px;
|
|
4141
|
+
background: var(--blue);
|
|
4142
|
+
color: var(--bg);
|
|
4143
|
+
border: none;
|
|
4144
|
+
border-radius: var(--radius);
|
|
4145
|
+
font-size: 14px;
|
|
4146
|
+
font-weight: 600;
|
|
4147
|
+
cursor: pointer;
|
|
4148
|
+
transition: opacity 0.15s;
|
|
4149
|
+
font-family: var(--sans);
|
|
4150
|
+
}
|
|
4151
|
+
.login-btn:hover { opacity: 0.9; }
|
|
4152
|
+
.login-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
4153
|
+
.login-error {
|
|
4154
|
+
margin-top: 16px;
|
|
4155
|
+
padding: 10px 14px;
|
|
4156
|
+
background: rgba(248, 81, 73, 0.1);
|
|
4157
|
+
border: 1px solid var(--red);
|
|
4158
|
+
border-radius: var(--radius);
|
|
4159
|
+
font-size: 12px;
|
|
4160
|
+
color: var(--red);
|
|
4161
|
+
display: none;
|
|
4162
|
+
}
|
|
4163
|
+
.login-hint {
|
|
4164
|
+
margin-top: 24px;
|
|
4165
|
+
padding-top: 16px;
|
|
4166
|
+
border-top: 1px solid var(--border);
|
|
4167
|
+
font-size: 11px;
|
|
4168
|
+
color: var(--text-secondary);
|
|
4169
|
+
line-height: 1.5;
|
|
4170
|
+
}
|
|
4171
|
+
.login-hint code {
|
|
4172
|
+
font-family: var(--mono);
|
|
4173
|
+
background: var(--bg);
|
|
4174
|
+
padding: 1px 4px;
|
|
4175
|
+
border-radius: 3px;
|
|
4176
|
+
font-size: 10px;
|
|
4177
|
+
}
|
|
4178
|
+
</style>
|
|
4179
|
+
</head>
|
|
4180
|
+
<body>
|
|
4181
|
+
<div class="login-container">
|
|
4182
|
+
<div class="login-logo"><span>◆</span> SANCTUARY</div>
|
|
4183
|
+
<div class="login-version">Principal Dashboard v${options.serverVersion}</div>
|
|
4184
|
+
<form id="loginForm" onsubmit="return handleLogin(event)">
|
|
4185
|
+
<label class="login-label" for="tokenInput">Dashboard Auth Token</label>
|
|
4186
|
+
<input class="login-input" type="password" id="tokenInput"
|
|
4187
|
+
placeholder="Enter your auth token" autocomplete="off" autofocus required>
|
|
4188
|
+
<button class="login-btn" type="submit" id="loginBtn">Open Dashboard</button>
|
|
4189
|
+
</form>
|
|
4190
|
+
<div class="login-error" id="loginError"></div>
|
|
4191
|
+
<div class="login-hint">
|
|
4192
|
+
Your token is set via <code>SANCTUARY_DASHBOARD_AUTH_TOKEN</code> environment variable,
|
|
4193
|
+
or check your server's startup output.
|
|
4194
|
+
</div>
|
|
4195
|
+
</div>
|
|
4196
|
+
<script>
|
|
4197
|
+
async function handleLogin(e) {
|
|
4198
|
+
e.preventDefault();
|
|
4199
|
+
var btn = document.getElementById('loginBtn');
|
|
4200
|
+
var errEl = document.getElementById('loginError');
|
|
4201
|
+
var token = document.getElementById('tokenInput').value.trim();
|
|
4202
|
+
if (!token) return false;
|
|
4203
|
+
btn.disabled = true;
|
|
4204
|
+
btn.textContent = 'Authenticating...';
|
|
4205
|
+
errEl.style.display = 'none';
|
|
4206
|
+
try {
|
|
4207
|
+
var resp = await fetch('/auth/session', {
|
|
4208
|
+
method: 'POST',
|
|
4209
|
+
headers: { 'Authorization': 'Bearer ' + token }
|
|
4210
|
+
});
|
|
4211
|
+
if (!resp.ok) {
|
|
4212
|
+
var data = await resp.json().catch(function() { return {}; });
|
|
4213
|
+
throw new Error(data.error || 'Authentication failed');
|
|
4214
|
+
}
|
|
4215
|
+
var result = await resp.json();
|
|
4216
|
+
// Store token in sessionStorage for auto-renewal inside the dashboard
|
|
4217
|
+
try { sessionStorage.setItem('sanctuary_token', token); } catch(_) {}
|
|
4218
|
+
// Set session cookie
|
|
4219
|
+
var maxAge = result.expires_in_seconds || 300;
|
|
4220
|
+
document.cookie = 'sanctuary_session=' + result.session_id +
|
|
4221
|
+
'; path=/; SameSite=Strict; max-age=' + maxAge;
|
|
4222
|
+
// Reload to enter the dashboard
|
|
4223
|
+
window.location.reload();
|
|
4224
|
+
} catch (err) {
|
|
4225
|
+
errEl.textContent = err.message || 'Authentication failed. Check your token.';
|
|
4226
|
+
errEl.style.display = 'block';
|
|
4227
|
+
btn.disabled = false;
|
|
4228
|
+
btn.textContent = 'Open Dashboard';
|
|
4229
|
+
}
|
|
4230
|
+
return false;
|
|
4231
|
+
}
|
|
4232
|
+
</script>
|
|
4233
|
+
</body>
|
|
4234
|
+
</html>`;
|
|
4235
|
+
}
|
|
4061
4236
|
function generateDashboardHTML(options) {
|
|
4062
4237
|
return `<!DOCTYPE html>
|
|
4063
4238
|
<html lang="en">
|
|
@@ -4927,7 +5102,9 @@ function generateDashboardHTML(options) {
|
|
|
4927
5102
|
// \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
|
|
4928
5103
|
|
|
4929
5104
|
const TIMEOUT_SECONDS = ${options.timeoutSeconds};
|
|
4930
|
-
|
|
5105
|
+
// AUTH_TOKEN: embedded token (for direct session access) or from sessionStorage (login page flow)
|
|
5106
|
+
const EMBEDDED_TOKEN = ${options.authToken ? JSON.stringify(options.authToken) : "null"};
|
|
5107
|
+
const AUTH_TOKEN = EMBEDDED_TOKEN || (function() { try { return sessionStorage.getItem('sanctuary_token'); } catch(_) { return null; } })();
|
|
4931
5108
|
const MAX_ACTIVITY_ITEMS = 100;
|
|
4932
5109
|
const MAX_THREAT_ITEMS = 20;
|
|
4933
5110
|
|
|
@@ -4942,6 +5119,7 @@ function generateDashboardHTML(options) {
|
|
|
4942
5119
|
const activityItems = [];
|
|
4943
5120
|
const threatItems = [];
|
|
4944
5121
|
let sovereigntyScore = 85;
|
|
5122
|
+
let sessionRenewalTimer = null;
|
|
4945
5123
|
|
|
4946
5124
|
// \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
|
|
4947
5125
|
|
|
@@ -4957,6 +5135,11 @@ function generateDashboardHTML(options) {
|
|
|
4957
5135
|
return url + sep + 'session=' + SESSION_ID;
|
|
4958
5136
|
}
|
|
4959
5137
|
|
|
5138
|
+
function setCookie(sessionId, maxAge) {
|
|
5139
|
+
document.cookie = 'sanctuary_session=' + sessionId +
|
|
5140
|
+
'; path=/; SameSite=Strict; max-age=' + maxAge;
|
|
5141
|
+
}
|
|
5142
|
+
|
|
4960
5143
|
async function exchangeSession() {
|
|
4961
5144
|
if (!AUTH_TOKEN) return;
|
|
4962
5145
|
try {
|
|
@@ -4964,14 +5147,35 @@ function generateDashboardHTML(options) {
|
|
|
4964
5147
|
if (resp.ok) {
|
|
4965
5148
|
const data = await resp.json();
|
|
4966
5149
|
SESSION_ID = data.session_id;
|
|
4967
|
-
|
|
4968
|
-
|
|
5150
|
+
var ttl = data.expires_in_seconds || 300;
|
|
5151
|
+
// Update cookie with new session
|
|
5152
|
+
setCookie(SESSION_ID, ttl);
|
|
5153
|
+
// Schedule renewal at 80% of TTL
|
|
5154
|
+
if (sessionRenewalTimer) clearTimeout(sessionRenewalTimer);
|
|
5155
|
+
sessionRenewalTimer = setTimeout(function() {
|
|
5156
|
+
exchangeSession().then(function() { reconnectSSE(); });
|
|
5157
|
+
}, ttl * 800);
|
|
5158
|
+
} else if (resp.status === 401) {
|
|
5159
|
+
// Token invalid or expired \u2014 show non-destructive re-login overlay
|
|
5160
|
+
showSessionExpired();
|
|
4969
5161
|
}
|
|
4970
5162
|
} catch (e) {
|
|
4971
|
-
//
|
|
5163
|
+
// Network error \u2014 retry in 30s
|
|
5164
|
+
if (sessionRenewalTimer) clearTimeout(sessionRenewalTimer);
|
|
5165
|
+
sessionRenewalTimer = setTimeout(function() {
|
|
5166
|
+
exchangeSession().then(function() { reconnectSSE(); });
|
|
5167
|
+
}, 30000);
|
|
4972
5168
|
}
|
|
4973
5169
|
}
|
|
4974
5170
|
|
|
5171
|
+
function showSessionExpired() {
|
|
5172
|
+
// Clear stored token
|
|
5173
|
+
try { sessionStorage.removeItem('sanctuary_token'); } catch(_) {}
|
|
5174
|
+
// Redirect to login page
|
|
5175
|
+
document.cookie = 'sanctuary_session=; path=/; max-age=0';
|
|
5176
|
+
window.location.reload();
|
|
5177
|
+
}
|
|
5178
|
+
|
|
4975
5179
|
// \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
|
|
4976
5180
|
|
|
4977
5181
|
function esc(s) {
|
|
@@ -5444,7 +5648,8 @@ function generateDashboardHTML(options) {
|
|
|
5444
5648
|
}
|
|
5445
5649
|
|
|
5446
5650
|
// src/principal-policy/dashboard.ts
|
|
5447
|
-
var
|
|
5651
|
+
var SESSION_TTL_REMOTE_MS = 5 * 60 * 1e3;
|
|
5652
|
+
var SESSION_TTL_LOCAL_MS = 24 * 60 * 60 * 1e3;
|
|
5448
5653
|
var MAX_SESSIONS = 1e3;
|
|
5449
5654
|
var RATE_LIMIT_WINDOW_MS = 6e4;
|
|
5450
5655
|
var RATE_LIMIT_GENERAL = 120;
|
|
@@ -5459,8 +5664,11 @@ var DashboardApprovalChannel = class {
|
|
|
5459
5664
|
baseline = null;
|
|
5460
5665
|
auditLog = null;
|
|
5461
5666
|
dashboardHTML;
|
|
5667
|
+
loginHTML;
|
|
5462
5668
|
authToken;
|
|
5463
5669
|
useTLS;
|
|
5670
|
+
/** Session TTL: longer for localhost, shorter for remote */
|
|
5671
|
+
sessionTTLMs;
|
|
5464
5672
|
/** SEC-012: Short-lived session store. Sessions replace URL query tokens. */
|
|
5465
5673
|
sessions = /* @__PURE__ */ new Map();
|
|
5466
5674
|
sessionCleanupTimer = null;
|
|
@@ -5470,11 +5678,14 @@ var DashboardApprovalChannel = class {
|
|
|
5470
5678
|
this.config = config;
|
|
5471
5679
|
this.authToken = config.auth_token;
|
|
5472
5680
|
this.useTLS = !!(config.tls?.cert_path && config.tls?.key_path);
|
|
5681
|
+
const isLocalhost = config.host === "127.0.0.1" || config.host === "localhost" || config.host === "::1";
|
|
5682
|
+
this.sessionTTLMs = isLocalhost ? SESSION_TTL_LOCAL_MS : SESSION_TTL_REMOTE_MS;
|
|
5473
5683
|
this.dashboardHTML = generateDashboardHTML({
|
|
5474
5684
|
timeoutSeconds: config.timeout_seconds,
|
|
5475
5685
|
serverVersion: SANCTUARY_VERSION,
|
|
5476
5686
|
authToken: this.authToken
|
|
5477
5687
|
});
|
|
5688
|
+
this.loginHTML = generateLoginHTML({ serverVersion: SANCTUARY_VERSION });
|
|
5478
5689
|
this.sessionCleanupTimer = setInterval(() => this.cleanupSessions(), 6e4);
|
|
5479
5690
|
}
|
|
5480
5691
|
/**
|
|
@@ -5504,25 +5715,26 @@ var DashboardApprovalChannel = class {
|
|
|
5504
5715
|
const protocol = this.useTLS ? "https" : "http";
|
|
5505
5716
|
const baseUrl = `${protocol}://${this.config.host}:${this.config.port}`;
|
|
5506
5717
|
this.httpServer.listen(this.config.port, this.config.host, () => {
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
process.stderr.write(
|
|
5510
|
-
`
|
|
5718
|
+
process.stderr.write(
|
|
5719
|
+
`
|
|
5511
5720
|
Sanctuary Principal Dashboard: ${baseUrl}
|
|
5512
5721
|
`
|
|
5513
|
-
|
|
5722
|
+
);
|
|
5723
|
+
if (this.authToken) {
|
|
5724
|
+
const sessionUrl = this.createSessionUrl();
|
|
5514
5725
|
process.stderr.write(
|
|
5515
|
-
`
|
|
5516
|
-
|
|
5726
|
+
` Quick open: ${sessionUrl}
|
|
5517
5727
|
`
|
|
5518
5728
|
);
|
|
5519
|
-
|
|
5729
|
+
const hint = this.authToken.slice(0, 4) + "..." + this.authToken.slice(-4);
|
|
5520
5730
|
process.stderr.write(
|
|
5521
|
-
`
|
|
5522
|
-
Sanctuary Principal Dashboard: ${baseUrl}
|
|
5731
|
+
` Auth token: ${hint}
|
|
5523
5732
|
|
|
5524
5733
|
`
|
|
5525
5734
|
);
|
|
5735
|
+
} else {
|
|
5736
|
+
process.stderr.write(`
|
|
5737
|
+
`);
|
|
5526
5738
|
}
|
|
5527
5739
|
resolve();
|
|
5528
5740
|
});
|
|
@@ -5626,10 +5838,47 @@ var DashboardApprovalChannel = class {
|
|
|
5626
5838
|
if (sessionId && this.validateSession(sessionId)) {
|
|
5627
5839
|
return true;
|
|
5628
5840
|
}
|
|
5841
|
+
const cookieSession = this.parseCookie(req, "sanctuary_session");
|
|
5842
|
+
if (cookieSession && this.validateSession(cookieSession)) {
|
|
5843
|
+
return true;
|
|
5844
|
+
}
|
|
5629
5845
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
5630
5846
|
res.end(JSON.stringify({ error: "Unauthorized \u2014 use Authorization: Bearer header or a valid session" }));
|
|
5631
5847
|
return false;
|
|
5632
5848
|
}
|
|
5849
|
+
/**
|
|
5850
|
+
* Check if a request is authenticated WITHOUT sending a response.
|
|
5851
|
+
* Used to decide between login page vs dashboard for GET /.
|
|
5852
|
+
*/
|
|
5853
|
+
isAuthenticated(req, url) {
|
|
5854
|
+
if (!this.authToken) return true;
|
|
5855
|
+
const authHeader = req.headers.authorization;
|
|
5856
|
+
if (authHeader) {
|
|
5857
|
+
const parts = authHeader.split(" ");
|
|
5858
|
+
if (parts.length === 2 && parts[0] === "Bearer" && parts[1] === this.authToken) {
|
|
5859
|
+
return true;
|
|
5860
|
+
}
|
|
5861
|
+
}
|
|
5862
|
+
const sessionId = url.searchParams.get("session");
|
|
5863
|
+
if (sessionId && this.validateSession(sessionId)) return true;
|
|
5864
|
+
const cookieSession = this.parseCookie(req, "sanctuary_session");
|
|
5865
|
+
if (cookieSession && this.validateSession(cookieSession)) return true;
|
|
5866
|
+
return false;
|
|
5867
|
+
}
|
|
5868
|
+
/**
|
|
5869
|
+
* Parse a specific cookie value from the request.
|
|
5870
|
+
*/
|
|
5871
|
+
parseCookie(req, name) {
|
|
5872
|
+
const header = req.headers.cookie;
|
|
5873
|
+
if (!header) return null;
|
|
5874
|
+
for (const part of header.split(";")) {
|
|
5875
|
+
const [key, ...rest] = part.split("=");
|
|
5876
|
+
if (key?.trim() === name) {
|
|
5877
|
+
return rest.join("=").trim();
|
|
5878
|
+
}
|
|
5879
|
+
}
|
|
5880
|
+
return null;
|
|
5881
|
+
}
|
|
5633
5882
|
// ── Session Management (SEC-012) ──────────────────────────────────
|
|
5634
5883
|
/**
|
|
5635
5884
|
* Create a short-lived session by exchanging the long-lived auth token
|
|
@@ -5650,7 +5899,7 @@ var DashboardApprovalChannel = class {
|
|
|
5650
5899
|
this.sessions.set(id, {
|
|
5651
5900
|
id,
|
|
5652
5901
|
created_at: now,
|
|
5653
|
-
expires_at: now +
|
|
5902
|
+
expires_at: now + this.sessionTTLMs
|
|
5654
5903
|
});
|
|
5655
5904
|
return id;
|
|
5656
5905
|
}
|
|
@@ -5749,13 +5998,26 @@ var DashboardApprovalChannel = class {
|
|
|
5749
5998
|
res.end();
|
|
5750
5999
|
return;
|
|
5751
6000
|
}
|
|
5752
|
-
if (
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
if (method === "POST" && url.pathname === "/auth/session") {
|
|
6001
|
+
if (method === "POST" && url.pathname === "/auth/session") {
|
|
6002
|
+
if (!this.checkRateLimit(req, res, "general")) return;
|
|
6003
|
+
try {
|
|
5756
6004
|
this.handleSessionExchange(req, res);
|
|
6005
|
+
} catch {
|
|
6006
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
6007
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
6008
|
+
}
|
|
6009
|
+
return;
|
|
6010
|
+
}
|
|
6011
|
+
if (method === "GET" && url.pathname === "/" && this.authToken) {
|
|
6012
|
+
if (!this.isAuthenticated(req, url)) {
|
|
6013
|
+
if (!this.checkRateLimit(req, res, "general")) return;
|
|
6014
|
+
this.serveLoginPage(res);
|
|
5757
6015
|
return;
|
|
5758
6016
|
}
|
|
6017
|
+
}
|
|
6018
|
+
if (!this.checkAuth(req, url, res)) return;
|
|
6019
|
+
if (!this.checkRateLimit(req, res, "general")) return;
|
|
6020
|
+
try {
|
|
5759
6021
|
if (method === "GET" && url.pathname === "/") {
|
|
5760
6022
|
this.serveDashboard(res);
|
|
5761
6023
|
} else if (method === "GET" && url.pathname === "/events") {
|
|
@@ -5812,12 +6074,23 @@ var DashboardApprovalChannel = class {
|
|
|
5812
6074
|
return;
|
|
5813
6075
|
}
|
|
5814
6076
|
const sessionId = this.createSession();
|
|
5815
|
-
|
|
6077
|
+
const ttlSeconds = Math.floor(this.sessionTTLMs / 1e3);
|
|
6078
|
+
res.writeHead(200, {
|
|
6079
|
+
"Content-Type": "application/json",
|
|
6080
|
+
"Set-Cookie": `sanctuary_session=${sessionId}; Path=/; SameSite=Strict; Max-Age=${ttlSeconds}`
|
|
6081
|
+
});
|
|
5816
6082
|
res.end(JSON.stringify({
|
|
5817
6083
|
session_id: sessionId,
|
|
5818
|
-
expires_in_seconds:
|
|
6084
|
+
expires_in_seconds: ttlSeconds
|
|
5819
6085
|
}));
|
|
5820
6086
|
}
|
|
6087
|
+
serveLoginPage(res) {
|
|
6088
|
+
res.writeHead(200, {
|
|
6089
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
6090
|
+
"Cache-Control": "no-cache, no-store"
|
|
6091
|
+
});
|
|
6092
|
+
res.end(this.loginHTML);
|
|
6093
|
+
}
|
|
5821
6094
|
serveDashboard(res) {
|
|
5822
6095
|
res.writeHead(200, {
|
|
5823
6096
|
"Content-Type": "text/html; charset=utf-8",
|
|
@@ -5993,6 +6266,22 @@ data: ${JSON.stringify(data)}
|
|
|
5993
6266
|
broadcastProtectionStatus(data) {
|
|
5994
6267
|
this.broadcastSSE("protection-status", data);
|
|
5995
6268
|
}
|
|
6269
|
+
/**
|
|
6270
|
+
* Create a pre-authenticated URL for the dashboard.
|
|
6271
|
+
* Used by the sanctuary_dashboard_open tool and at startup.
|
|
6272
|
+
*/
|
|
6273
|
+
createSessionUrl() {
|
|
6274
|
+
const sessionId = this.createSession();
|
|
6275
|
+
const protocol = this.useTLS ? "https" : "http";
|
|
6276
|
+
return `${protocol}://${this.config.host}:${this.config.port}/?session=${sessionId}`;
|
|
6277
|
+
}
|
|
6278
|
+
/**
|
|
6279
|
+
* Get the base URL for the dashboard.
|
|
6280
|
+
*/
|
|
6281
|
+
getBaseUrl() {
|
|
6282
|
+
const protocol = this.useTLS ? "https" : "http";
|
|
6283
|
+
return `${protocol}://${this.config.host}:${this.config.port}`;
|
|
6284
|
+
}
|
|
5996
6285
|
/** Get the number of pending requests */
|
|
5997
6286
|
get pendingCount() {
|
|
5998
6287
|
return this.pending.size;
|
|
@@ -11862,6 +12151,32 @@ async function createSanctuaryServer(options) {
|
|
|
11862
12151
|
} : void 0;
|
|
11863
12152
|
const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog, injectionDetector, onInjectionAlert);
|
|
11864
12153
|
const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
|
|
12154
|
+
const dashboardTools = [];
|
|
12155
|
+
if (dashboard) {
|
|
12156
|
+
dashboardTools.push({
|
|
12157
|
+
name: "sanctuary/dashboard_open",
|
|
12158
|
+
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.",
|
|
12159
|
+
inputSchema: {
|
|
12160
|
+
type: "object",
|
|
12161
|
+
properties: {}
|
|
12162
|
+
},
|
|
12163
|
+
handler: async () => {
|
|
12164
|
+
const url = dashboard.createSessionUrl();
|
|
12165
|
+
return {
|
|
12166
|
+
content: [
|
|
12167
|
+
{
|
|
12168
|
+
type: "text",
|
|
12169
|
+
text: JSON.stringify({
|
|
12170
|
+
dashboard_url: url,
|
|
12171
|
+
base_url: dashboard.getBaseUrl(),
|
|
12172
|
+
note: "Click the dashboard_url to open the Principal Dashboard. The session is pre-authenticated."
|
|
12173
|
+
}, null, 2)
|
|
12174
|
+
}
|
|
12175
|
+
]
|
|
12176
|
+
};
|
|
12177
|
+
}
|
|
12178
|
+
});
|
|
12179
|
+
}
|
|
11865
12180
|
let allTools = [
|
|
11866
12181
|
...l1Tools,
|
|
11867
12182
|
...l2Tools,
|
|
@@ -11875,6 +12190,7 @@ async function createSanctuaryServer(options) {
|
|
|
11875
12190
|
...auditTools,
|
|
11876
12191
|
...contextGateTools,
|
|
11877
12192
|
...hardeningTools,
|
|
12193
|
+
...dashboardTools,
|
|
11878
12194
|
manifestTool
|
|
11879
12195
|
];
|
|
11880
12196
|
allTools = allTools.map((tool) => ({
|