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