@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/cli.cjs
CHANGED
|
@@ -4042,6 +4042,181 @@ var StderrApprovalChannel = class {
|
|
|
4042
4042
|
};
|
|
4043
4043
|
|
|
4044
4044
|
// src/principal-policy/dashboard-html.ts
|
|
4045
|
+
function generateLoginHTML(options) {
|
|
4046
|
+
return `<!DOCTYPE html>
|
|
4047
|
+
<html lang="en">
|
|
4048
|
+
<head>
|
|
4049
|
+
<meta charset="utf-8">
|
|
4050
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
4051
|
+
<title>Sanctuary \u2014 Login</title>
|
|
4052
|
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
|
|
4053
|
+
<style>
|
|
4054
|
+
:root {
|
|
4055
|
+
--bg: #0d1117;
|
|
4056
|
+
--surface: #161b22;
|
|
4057
|
+
--border: #30363d;
|
|
4058
|
+
--text-primary: #e6edf3;
|
|
4059
|
+
--text-secondary: #8b949e;
|
|
4060
|
+
--green: #3fb950;
|
|
4061
|
+
--red: #f85149;
|
|
4062
|
+
--blue: #58a6ff;
|
|
4063
|
+
--mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
|
4064
|
+
--sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
4065
|
+
--radius: 6px;
|
|
4066
|
+
}
|
|
4067
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
4068
|
+
html, body { width: 100%; height: 100%; }
|
|
4069
|
+
body {
|
|
4070
|
+
font-family: var(--sans);
|
|
4071
|
+
background: var(--bg);
|
|
4072
|
+
color: var(--text-primary);
|
|
4073
|
+
display: flex;
|
|
4074
|
+
align-items: center;
|
|
4075
|
+
justify-content: center;
|
|
4076
|
+
}
|
|
4077
|
+
.login-container {
|
|
4078
|
+
width: 100%;
|
|
4079
|
+
max-width: 400px;
|
|
4080
|
+
padding: 40px 32px;
|
|
4081
|
+
background: var(--surface);
|
|
4082
|
+
border: 1px solid var(--border);
|
|
4083
|
+
border-radius: 12px;
|
|
4084
|
+
}
|
|
4085
|
+
.login-logo {
|
|
4086
|
+
text-align: center;
|
|
4087
|
+
font-size: 20px;
|
|
4088
|
+
font-weight: 700;
|
|
4089
|
+
letter-spacing: -0.5px;
|
|
4090
|
+
margin-bottom: 8px;
|
|
4091
|
+
}
|
|
4092
|
+
.login-logo span { color: var(--blue); }
|
|
4093
|
+
.login-version {
|
|
4094
|
+
text-align: center;
|
|
4095
|
+
font-size: 11px;
|
|
4096
|
+
color: var(--text-secondary);
|
|
4097
|
+
font-family: var(--mono);
|
|
4098
|
+
margin-bottom: 32px;
|
|
4099
|
+
}
|
|
4100
|
+
.login-label {
|
|
4101
|
+
display: block;
|
|
4102
|
+
font-size: 13px;
|
|
4103
|
+
font-weight: 600;
|
|
4104
|
+
color: var(--text-secondary);
|
|
4105
|
+
margin-bottom: 8px;
|
|
4106
|
+
}
|
|
4107
|
+
.login-input {
|
|
4108
|
+
width: 100%;
|
|
4109
|
+
padding: 10px 14px;
|
|
4110
|
+
background: var(--bg);
|
|
4111
|
+
border: 1px solid var(--border);
|
|
4112
|
+
border-radius: var(--radius);
|
|
4113
|
+
color: var(--text-primary);
|
|
4114
|
+
font-family: var(--mono);
|
|
4115
|
+
font-size: 14px;
|
|
4116
|
+
outline: none;
|
|
4117
|
+
transition: border-color 0.15s;
|
|
4118
|
+
}
|
|
4119
|
+
.login-input:focus { border-color: var(--blue); }
|
|
4120
|
+
.login-input::placeholder { color: var(--text-secondary); opacity: 0.5; }
|
|
4121
|
+
.login-btn {
|
|
4122
|
+
width: 100%;
|
|
4123
|
+
margin-top: 20px;
|
|
4124
|
+
padding: 10px;
|
|
4125
|
+
background: var(--blue);
|
|
4126
|
+
color: var(--bg);
|
|
4127
|
+
border: none;
|
|
4128
|
+
border-radius: var(--radius);
|
|
4129
|
+
font-size: 14px;
|
|
4130
|
+
font-weight: 600;
|
|
4131
|
+
cursor: pointer;
|
|
4132
|
+
transition: opacity 0.15s;
|
|
4133
|
+
font-family: var(--sans);
|
|
4134
|
+
}
|
|
4135
|
+
.login-btn:hover { opacity: 0.9; }
|
|
4136
|
+
.login-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
4137
|
+
.login-error {
|
|
4138
|
+
margin-top: 16px;
|
|
4139
|
+
padding: 10px 14px;
|
|
4140
|
+
background: rgba(248, 81, 73, 0.1);
|
|
4141
|
+
border: 1px solid var(--red);
|
|
4142
|
+
border-radius: var(--radius);
|
|
4143
|
+
font-size: 12px;
|
|
4144
|
+
color: var(--red);
|
|
4145
|
+
display: none;
|
|
4146
|
+
}
|
|
4147
|
+
.login-hint {
|
|
4148
|
+
margin-top: 24px;
|
|
4149
|
+
padding-top: 16px;
|
|
4150
|
+
border-top: 1px solid var(--border);
|
|
4151
|
+
font-size: 11px;
|
|
4152
|
+
color: var(--text-secondary);
|
|
4153
|
+
line-height: 1.5;
|
|
4154
|
+
}
|
|
4155
|
+
.login-hint code {
|
|
4156
|
+
font-family: var(--mono);
|
|
4157
|
+
background: var(--bg);
|
|
4158
|
+
padding: 1px 4px;
|
|
4159
|
+
border-radius: 3px;
|
|
4160
|
+
font-size: 10px;
|
|
4161
|
+
}
|
|
4162
|
+
</style>
|
|
4163
|
+
</head>
|
|
4164
|
+
<body>
|
|
4165
|
+
<div class="login-container">
|
|
4166
|
+
<div class="login-logo"><span>◆</span> SANCTUARY</div>
|
|
4167
|
+
<div class="login-version">Principal Dashboard v${options.serverVersion}</div>
|
|
4168
|
+
<form id="loginForm" onsubmit="return handleLogin(event)">
|
|
4169
|
+
<label class="login-label" for="tokenInput">Dashboard Auth Token</label>
|
|
4170
|
+
<input class="login-input" type="password" id="tokenInput"
|
|
4171
|
+
placeholder="Enter your auth token" autocomplete="off" autofocus required>
|
|
4172
|
+
<button class="login-btn" type="submit" id="loginBtn">Open Dashboard</button>
|
|
4173
|
+
</form>
|
|
4174
|
+
<div class="login-error" id="loginError"></div>
|
|
4175
|
+
<div class="login-hint">
|
|
4176
|
+
Your token is set via <code>SANCTUARY_DASHBOARD_AUTH_TOKEN</code> environment variable,
|
|
4177
|
+
or check your server's startup output.
|
|
4178
|
+
</div>
|
|
4179
|
+
</div>
|
|
4180
|
+
<script>
|
|
4181
|
+
async function handleLogin(e) {
|
|
4182
|
+
e.preventDefault();
|
|
4183
|
+
var btn = document.getElementById('loginBtn');
|
|
4184
|
+
var errEl = document.getElementById('loginError');
|
|
4185
|
+
var token = document.getElementById('tokenInput').value.trim();
|
|
4186
|
+
if (!token) return false;
|
|
4187
|
+
btn.disabled = true;
|
|
4188
|
+
btn.textContent = 'Authenticating...';
|
|
4189
|
+
errEl.style.display = 'none';
|
|
4190
|
+
try {
|
|
4191
|
+
var resp = await fetch('/auth/session', {
|
|
4192
|
+
method: 'POST',
|
|
4193
|
+
headers: { 'Authorization': 'Bearer ' + token }
|
|
4194
|
+
});
|
|
4195
|
+
if (!resp.ok) {
|
|
4196
|
+
var data = await resp.json().catch(function() { return {}; });
|
|
4197
|
+
throw new Error(data.error || 'Authentication failed');
|
|
4198
|
+
}
|
|
4199
|
+
var result = await resp.json();
|
|
4200
|
+
// Store token in sessionStorage for auto-renewal inside the dashboard
|
|
4201
|
+
try { sessionStorage.setItem('sanctuary_token', token); } catch(_) {}
|
|
4202
|
+
// Set session cookie
|
|
4203
|
+
var maxAge = result.expires_in_seconds || 300;
|
|
4204
|
+
document.cookie = 'sanctuary_session=' + result.session_id +
|
|
4205
|
+
'; path=/; SameSite=Strict; max-age=' + maxAge;
|
|
4206
|
+
// Reload to enter the dashboard
|
|
4207
|
+
window.location.reload();
|
|
4208
|
+
} catch (err) {
|
|
4209
|
+
errEl.textContent = err.message || 'Authentication failed. Check your token.';
|
|
4210
|
+
errEl.style.display = 'block';
|
|
4211
|
+
btn.disabled = false;
|
|
4212
|
+
btn.textContent = 'Open Dashboard';
|
|
4213
|
+
}
|
|
4214
|
+
return false;
|
|
4215
|
+
}
|
|
4216
|
+
</script>
|
|
4217
|
+
</body>
|
|
4218
|
+
</html>`;
|
|
4219
|
+
}
|
|
4045
4220
|
function generateDashboardHTML(options) {
|
|
4046
4221
|
return `<!DOCTYPE html>
|
|
4047
4222
|
<html lang="en">
|
|
@@ -4911,7 +5086,9 @@ function generateDashboardHTML(options) {
|
|
|
4911
5086
|
// \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
|
|
4912
5087
|
|
|
4913
5088
|
const TIMEOUT_SECONDS = ${options.timeoutSeconds};
|
|
4914
|
-
|
|
5089
|
+
// AUTH_TOKEN: embedded token (for direct session access) or from sessionStorage (login page flow)
|
|
5090
|
+
const EMBEDDED_TOKEN = ${options.authToken ? JSON.stringify(options.authToken) : "null"};
|
|
5091
|
+
const AUTH_TOKEN = EMBEDDED_TOKEN || (function() { try { return sessionStorage.getItem('sanctuary_token'); } catch(_) { return null; } })();
|
|
4915
5092
|
const MAX_ACTIVITY_ITEMS = 100;
|
|
4916
5093
|
const MAX_THREAT_ITEMS = 20;
|
|
4917
5094
|
|
|
@@ -4926,6 +5103,7 @@ function generateDashboardHTML(options) {
|
|
|
4926
5103
|
const activityItems = [];
|
|
4927
5104
|
const threatItems = [];
|
|
4928
5105
|
let sovereigntyScore = 85;
|
|
5106
|
+
let sessionRenewalTimer = null;
|
|
4929
5107
|
|
|
4930
5108
|
// \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
|
|
4931
5109
|
|
|
@@ -4941,6 +5119,11 @@ function generateDashboardHTML(options) {
|
|
|
4941
5119
|
return url + sep + 'session=' + SESSION_ID;
|
|
4942
5120
|
}
|
|
4943
5121
|
|
|
5122
|
+
function setCookie(sessionId, maxAge) {
|
|
5123
|
+
document.cookie = 'sanctuary_session=' + sessionId +
|
|
5124
|
+
'; path=/; SameSite=Strict; max-age=' + maxAge;
|
|
5125
|
+
}
|
|
5126
|
+
|
|
4944
5127
|
async function exchangeSession() {
|
|
4945
5128
|
if (!AUTH_TOKEN) return;
|
|
4946
5129
|
try {
|
|
@@ -4948,14 +5131,35 @@ function generateDashboardHTML(options) {
|
|
|
4948
5131
|
if (resp.ok) {
|
|
4949
5132
|
const data = await resp.json();
|
|
4950
5133
|
SESSION_ID = data.session_id;
|
|
4951
|
-
|
|
4952
|
-
|
|
5134
|
+
var ttl = data.expires_in_seconds || 300;
|
|
5135
|
+
// Update cookie with new session
|
|
5136
|
+
setCookie(SESSION_ID, ttl);
|
|
5137
|
+
// Schedule renewal at 80% of TTL
|
|
5138
|
+
if (sessionRenewalTimer) clearTimeout(sessionRenewalTimer);
|
|
5139
|
+
sessionRenewalTimer = setTimeout(function() {
|
|
5140
|
+
exchangeSession().then(function() { reconnectSSE(); });
|
|
5141
|
+
}, ttl * 800);
|
|
5142
|
+
} else if (resp.status === 401) {
|
|
5143
|
+
// Token invalid or expired \u2014 show non-destructive re-login overlay
|
|
5144
|
+
showSessionExpired();
|
|
4953
5145
|
}
|
|
4954
5146
|
} catch (e) {
|
|
4955
|
-
//
|
|
5147
|
+
// Network error \u2014 retry in 30s
|
|
5148
|
+
if (sessionRenewalTimer) clearTimeout(sessionRenewalTimer);
|
|
5149
|
+
sessionRenewalTimer = setTimeout(function() {
|
|
5150
|
+
exchangeSession().then(function() { reconnectSSE(); });
|
|
5151
|
+
}, 30000);
|
|
4956
5152
|
}
|
|
4957
5153
|
}
|
|
4958
5154
|
|
|
5155
|
+
function showSessionExpired() {
|
|
5156
|
+
// Clear stored token
|
|
5157
|
+
try { sessionStorage.removeItem('sanctuary_token'); } catch(_) {}
|
|
5158
|
+
// Redirect to login page
|
|
5159
|
+
document.cookie = 'sanctuary_session=; path=/; max-age=0';
|
|
5160
|
+
window.location.reload();
|
|
5161
|
+
}
|
|
5162
|
+
|
|
4959
5163
|
// \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
|
|
4960
5164
|
|
|
4961
5165
|
function esc(s) {
|
|
@@ -5428,7 +5632,8 @@ function generateDashboardHTML(options) {
|
|
|
5428
5632
|
}
|
|
5429
5633
|
|
|
5430
5634
|
// src/principal-policy/dashboard.ts
|
|
5431
|
-
var
|
|
5635
|
+
var SESSION_TTL_REMOTE_MS = 5 * 60 * 1e3;
|
|
5636
|
+
var SESSION_TTL_LOCAL_MS = 24 * 60 * 60 * 1e3;
|
|
5432
5637
|
var MAX_SESSIONS = 1e3;
|
|
5433
5638
|
var RATE_LIMIT_WINDOW_MS = 6e4;
|
|
5434
5639
|
var RATE_LIMIT_GENERAL = 120;
|
|
@@ -5443,8 +5648,11 @@ var DashboardApprovalChannel = class {
|
|
|
5443
5648
|
baseline = null;
|
|
5444
5649
|
auditLog = null;
|
|
5445
5650
|
dashboardHTML;
|
|
5651
|
+
loginHTML;
|
|
5446
5652
|
authToken;
|
|
5447
5653
|
useTLS;
|
|
5654
|
+
/** Session TTL: longer for localhost, shorter for remote */
|
|
5655
|
+
sessionTTLMs;
|
|
5448
5656
|
/** SEC-012: Short-lived session store. Sessions replace URL query tokens. */
|
|
5449
5657
|
sessions = /* @__PURE__ */ new Map();
|
|
5450
5658
|
sessionCleanupTimer = null;
|
|
@@ -5454,11 +5662,14 @@ var DashboardApprovalChannel = class {
|
|
|
5454
5662
|
this.config = config;
|
|
5455
5663
|
this.authToken = config.auth_token;
|
|
5456
5664
|
this.useTLS = !!(config.tls?.cert_path && config.tls?.key_path);
|
|
5665
|
+
const isLocalhost = config.host === "127.0.0.1" || config.host === "localhost" || config.host === "::1";
|
|
5666
|
+
this.sessionTTLMs = isLocalhost ? SESSION_TTL_LOCAL_MS : SESSION_TTL_REMOTE_MS;
|
|
5457
5667
|
this.dashboardHTML = generateDashboardHTML({
|
|
5458
5668
|
timeoutSeconds: config.timeout_seconds,
|
|
5459
5669
|
serverVersion: SANCTUARY_VERSION,
|
|
5460
5670
|
authToken: this.authToken
|
|
5461
5671
|
});
|
|
5672
|
+
this.loginHTML = generateLoginHTML({ serverVersion: SANCTUARY_VERSION });
|
|
5462
5673
|
this.sessionCleanupTimer = setInterval(() => this.cleanupSessions(), 6e4);
|
|
5463
5674
|
}
|
|
5464
5675
|
/**
|
|
@@ -5488,25 +5699,26 @@ var DashboardApprovalChannel = class {
|
|
|
5488
5699
|
const protocol = this.useTLS ? "https" : "http";
|
|
5489
5700
|
const baseUrl = `${protocol}://${this.config.host}:${this.config.port}`;
|
|
5490
5701
|
this.httpServer.listen(this.config.port, this.config.host, () => {
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
process.stderr.write(
|
|
5494
|
-
`
|
|
5702
|
+
process.stderr.write(
|
|
5703
|
+
`
|
|
5495
5704
|
Sanctuary Principal Dashboard: ${baseUrl}
|
|
5496
5705
|
`
|
|
5497
|
-
|
|
5706
|
+
);
|
|
5707
|
+
if (this.authToken) {
|
|
5708
|
+
const sessionUrl = this.createSessionUrl();
|
|
5498
5709
|
process.stderr.write(
|
|
5499
|
-
`
|
|
5500
|
-
|
|
5710
|
+
` Quick open: ${sessionUrl}
|
|
5501
5711
|
`
|
|
5502
5712
|
);
|
|
5503
|
-
|
|
5713
|
+
const hint = this.authToken.slice(0, 4) + "..." + this.authToken.slice(-4);
|
|
5504
5714
|
process.stderr.write(
|
|
5505
|
-
`
|
|
5506
|
-
Sanctuary Principal Dashboard: ${baseUrl}
|
|
5715
|
+
` Auth token: ${hint}
|
|
5507
5716
|
|
|
5508
5717
|
`
|
|
5509
5718
|
);
|
|
5719
|
+
} else {
|
|
5720
|
+
process.stderr.write(`
|
|
5721
|
+
`);
|
|
5510
5722
|
}
|
|
5511
5723
|
resolve();
|
|
5512
5724
|
});
|
|
@@ -5610,10 +5822,47 @@ var DashboardApprovalChannel = class {
|
|
|
5610
5822
|
if (sessionId && this.validateSession(sessionId)) {
|
|
5611
5823
|
return true;
|
|
5612
5824
|
}
|
|
5825
|
+
const cookieSession = this.parseCookie(req, "sanctuary_session");
|
|
5826
|
+
if (cookieSession && this.validateSession(cookieSession)) {
|
|
5827
|
+
return true;
|
|
5828
|
+
}
|
|
5613
5829
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
5614
5830
|
res.end(JSON.stringify({ error: "Unauthorized \u2014 use Authorization: Bearer header or a valid session" }));
|
|
5615
5831
|
return false;
|
|
5616
5832
|
}
|
|
5833
|
+
/**
|
|
5834
|
+
* Check if a request is authenticated WITHOUT sending a response.
|
|
5835
|
+
* Used to decide between login page vs dashboard for GET /.
|
|
5836
|
+
*/
|
|
5837
|
+
isAuthenticated(req, url) {
|
|
5838
|
+
if (!this.authToken) return true;
|
|
5839
|
+
const authHeader = req.headers.authorization;
|
|
5840
|
+
if (authHeader) {
|
|
5841
|
+
const parts = authHeader.split(" ");
|
|
5842
|
+
if (parts.length === 2 && parts[0] === "Bearer" && parts[1] === this.authToken) {
|
|
5843
|
+
return true;
|
|
5844
|
+
}
|
|
5845
|
+
}
|
|
5846
|
+
const sessionId = url.searchParams.get("session");
|
|
5847
|
+
if (sessionId && this.validateSession(sessionId)) return true;
|
|
5848
|
+
const cookieSession = this.parseCookie(req, "sanctuary_session");
|
|
5849
|
+
if (cookieSession && this.validateSession(cookieSession)) return true;
|
|
5850
|
+
return false;
|
|
5851
|
+
}
|
|
5852
|
+
/**
|
|
5853
|
+
* Parse a specific cookie value from the request.
|
|
5854
|
+
*/
|
|
5855
|
+
parseCookie(req, name) {
|
|
5856
|
+
const header = req.headers.cookie;
|
|
5857
|
+
if (!header) return null;
|
|
5858
|
+
for (const part of header.split(";")) {
|
|
5859
|
+
const [key, ...rest] = part.split("=");
|
|
5860
|
+
if (key?.trim() === name) {
|
|
5861
|
+
return rest.join("=").trim();
|
|
5862
|
+
}
|
|
5863
|
+
}
|
|
5864
|
+
return null;
|
|
5865
|
+
}
|
|
5617
5866
|
// ── Session Management (SEC-012) ──────────────────────────────────
|
|
5618
5867
|
/**
|
|
5619
5868
|
* Create a short-lived session by exchanging the long-lived auth token
|
|
@@ -5634,7 +5883,7 @@ var DashboardApprovalChannel = class {
|
|
|
5634
5883
|
this.sessions.set(id, {
|
|
5635
5884
|
id,
|
|
5636
5885
|
created_at: now,
|
|
5637
|
-
expires_at: now +
|
|
5886
|
+
expires_at: now + this.sessionTTLMs
|
|
5638
5887
|
});
|
|
5639
5888
|
return id;
|
|
5640
5889
|
}
|
|
@@ -5733,13 +5982,26 @@ var DashboardApprovalChannel = class {
|
|
|
5733
5982
|
res.end();
|
|
5734
5983
|
return;
|
|
5735
5984
|
}
|
|
5736
|
-
if (
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
if (method === "POST" && url.pathname === "/auth/session") {
|
|
5985
|
+
if (method === "POST" && url.pathname === "/auth/session") {
|
|
5986
|
+
if (!this.checkRateLimit(req, res, "general")) return;
|
|
5987
|
+
try {
|
|
5740
5988
|
this.handleSessionExchange(req, res);
|
|
5989
|
+
} catch {
|
|
5990
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
5991
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
5992
|
+
}
|
|
5993
|
+
return;
|
|
5994
|
+
}
|
|
5995
|
+
if (method === "GET" && url.pathname === "/" && this.authToken) {
|
|
5996
|
+
if (!this.isAuthenticated(req, url)) {
|
|
5997
|
+
if (!this.checkRateLimit(req, res, "general")) return;
|
|
5998
|
+
this.serveLoginPage(res);
|
|
5741
5999
|
return;
|
|
5742
6000
|
}
|
|
6001
|
+
}
|
|
6002
|
+
if (!this.checkAuth(req, url, res)) return;
|
|
6003
|
+
if (!this.checkRateLimit(req, res, "general")) return;
|
|
6004
|
+
try {
|
|
5743
6005
|
if (method === "GET" && url.pathname === "/") {
|
|
5744
6006
|
this.serveDashboard(res);
|
|
5745
6007
|
} else if (method === "GET" && url.pathname === "/events") {
|
|
@@ -5796,12 +6058,23 @@ var DashboardApprovalChannel = class {
|
|
|
5796
6058
|
return;
|
|
5797
6059
|
}
|
|
5798
6060
|
const sessionId = this.createSession();
|
|
5799
|
-
|
|
6061
|
+
const ttlSeconds = Math.floor(this.sessionTTLMs / 1e3);
|
|
6062
|
+
res.writeHead(200, {
|
|
6063
|
+
"Content-Type": "application/json",
|
|
6064
|
+
"Set-Cookie": `sanctuary_session=${sessionId}; Path=/; SameSite=Strict; Max-Age=${ttlSeconds}`
|
|
6065
|
+
});
|
|
5800
6066
|
res.end(JSON.stringify({
|
|
5801
6067
|
session_id: sessionId,
|
|
5802
|
-
expires_in_seconds:
|
|
6068
|
+
expires_in_seconds: ttlSeconds
|
|
5803
6069
|
}));
|
|
5804
6070
|
}
|
|
6071
|
+
serveLoginPage(res) {
|
|
6072
|
+
res.writeHead(200, {
|
|
6073
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
6074
|
+
"Cache-Control": "no-cache, no-store"
|
|
6075
|
+
});
|
|
6076
|
+
res.end(this.loginHTML);
|
|
6077
|
+
}
|
|
5805
6078
|
serveDashboard(res) {
|
|
5806
6079
|
res.writeHead(200, {
|
|
5807
6080
|
"Content-Type": "text/html; charset=utf-8",
|
|
@@ -5977,6 +6250,22 @@ data: ${JSON.stringify(data)}
|
|
|
5977
6250
|
broadcastProtectionStatus(data) {
|
|
5978
6251
|
this.broadcastSSE("protection-status", data);
|
|
5979
6252
|
}
|
|
6253
|
+
/**
|
|
6254
|
+
* Create a pre-authenticated URL for the dashboard.
|
|
6255
|
+
* Used by the sanctuary_dashboard_open tool and at startup.
|
|
6256
|
+
*/
|
|
6257
|
+
createSessionUrl() {
|
|
6258
|
+
const sessionId = this.createSession();
|
|
6259
|
+
const protocol = this.useTLS ? "https" : "http";
|
|
6260
|
+
return `${protocol}://${this.config.host}:${this.config.port}/?session=${sessionId}`;
|
|
6261
|
+
}
|
|
6262
|
+
/**
|
|
6263
|
+
* Get the base URL for the dashboard.
|
|
6264
|
+
*/
|
|
6265
|
+
getBaseUrl() {
|
|
6266
|
+
const protocol = this.useTLS ? "https" : "http";
|
|
6267
|
+
return `${protocol}://${this.config.host}:${this.config.port}`;
|
|
6268
|
+
}
|
|
5980
6269
|
/** Get the number of pending requests */
|
|
5981
6270
|
get pendingCount() {
|
|
5982
6271
|
return this.pending.size;
|
|
@@ -11791,6 +12080,32 @@ async function createSanctuaryServer(options) {
|
|
|
11791
12080
|
} : void 0;
|
|
11792
12081
|
const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog, injectionDetector, onInjectionAlert);
|
|
11793
12082
|
const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
|
|
12083
|
+
const dashboardTools = [];
|
|
12084
|
+
if (dashboard) {
|
|
12085
|
+
dashboardTools.push({
|
|
12086
|
+
name: "sanctuary/dashboard_open",
|
|
12087
|
+
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.",
|
|
12088
|
+
inputSchema: {
|
|
12089
|
+
type: "object",
|
|
12090
|
+
properties: {}
|
|
12091
|
+
},
|
|
12092
|
+
handler: async () => {
|
|
12093
|
+
const url = dashboard.createSessionUrl();
|
|
12094
|
+
return {
|
|
12095
|
+
content: [
|
|
12096
|
+
{
|
|
12097
|
+
type: "text",
|
|
12098
|
+
text: JSON.stringify({
|
|
12099
|
+
dashboard_url: url,
|
|
12100
|
+
base_url: dashboard.getBaseUrl(),
|
|
12101
|
+
note: "Click the dashboard_url to open the Principal Dashboard. The session is pre-authenticated."
|
|
12102
|
+
}, null, 2)
|
|
12103
|
+
}
|
|
12104
|
+
]
|
|
12105
|
+
};
|
|
12106
|
+
}
|
|
12107
|
+
});
|
|
12108
|
+
}
|
|
11794
12109
|
let allTools = [
|
|
11795
12110
|
...l1Tools,
|
|
11796
12111
|
...l2Tools,
|
|
@@ -11804,6 +12119,7 @@ async function createSanctuaryServer(options) {
|
|
|
11804
12119
|
...auditTools,
|
|
11805
12120
|
...contextGateTools,
|
|
11806
12121
|
...hardeningTools,
|
|
12122
|
+
...dashboardTools,
|
|
11807
12123
|
manifestTool
|
|
11808
12124
|
];
|
|
11809
12125
|
allTools = allTools.map((tool) => ({
|