@sanctuary-framework/mcp-server 0.5.1 → 0.5.3
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 +375 -27
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +377 -29
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +375 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +32 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +377 -29
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -295,6 +295,12 @@ async function loadConfig(configPath) {
|
|
|
295
295
|
if (process.env.SANCTUARY_DASHBOARD_AUTH_TOKEN) {
|
|
296
296
|
config.dashboard.auth_token = process.env.SANCTUARY_DASHBOARD_AUTH_TOKEN;
|
|
297
297
|
}
|
|
298
|
+
if (process.env.SANCTUARY_DASHBOARD_AUTO_OPEN === "true") {
|
|
299
|
+
config.dashboard.auto_open = true;
|
|
300
|
+
}
|
|
301
|
+
if (process.env.SANCTUARY_DASHBOARD_AUTO_OPEN === "false") {
|
|
302
|
+
config.dashboard.auto_open = false;
|
|
303
|
+
}
|
|
298
304
|
if (process.env.SANCTUARY_DASHBOARD_TLS_CERT && process.env.SANCTUARY_DASHBOARD_TLS_KEY) {
|
|
299
305
|
config.dashboard.tls = {
|
|
300
306
|
cert_path: process.env.SANCTUARY_DASHBOARD_TLS_CERT,
|
|
@@ -4042,6 +4048,181 @@ var StderrApprovalChannel = class {
|
|
|
4042
4048
|
};
|
|
4043
4049
|
|
|
4044
4050
|
// src/principal-policy/dashboard-html.ts
|
|
4051
|
+
function generateLoginHTML(options) {
|
|
4052
|
+
return `<!DOCTYPE html>
|
|
4053
|
+
<html lang="en">
|
|
4054
|
+
<head>
|
|
4055
|
+
<meta charset="utf-8">
|
|
4056
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
4057
|
+
<title>Sanctuary \u2014 Login</title>
|
|
4058
|
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
|
|
4059
|
+
<style>
|
|
4060
|
+
:root {
|
|
4061
|
+
--bg: #0d1117;
|
|
4062
|
+
--surface: #161b22;
|
|
4063
|
+
--border: #30363d;
|
|
4064
|
+
--text-primary: #e6edf3;
|
|
4065
|
+
--text-secondary: #8b949e;
|
|
4066
|
+
--green: #3fb950;
|
|
4067
|
+
--red: #f85149;
|
|
4068
|
+
--blue: #58a6ff;
|
|
4069
|
+
--mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
|
4070
|
+
--sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
4071
|
+
--radius: 6px;
|
|
4072
|
+
}
|
|
4073
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
4074
|
+
html, body { width: 100%; height: 100%; }
|
|
4075
|
+
body {
|
|
4076
|
+
font-family: var(--sans);
|
|
4077
|
+
background: var(--bg);
|
|
4078
|
+
color: var(--text-primary);
|
|
4079
|
+
display: flex;
|
|
4080
|
+
align-items: center;
|
|
4081
|
+
justify-content: center;
|
|
4082
|
+
}
|
|
4083
|
+
.login-container {
|
|
4084
|
+
width: 100%;
|
|
4085
|
+
max-width: 400px;
|
|
4086
|
+
padding: 40px 32px;
|
|
4087
|
+
background: var(--surface);
|
|
4088
|
+
border: 1px solid var(--border);
|
|
4089
|
+
border-radius: 12px;
|
|
4090
|
+
}
|
|
4091
|
+
.login-logo {
|
|
4092
|
+
text-align: center;
|
|
4093
|
+
font-size: 20px;
|
|
4094
|
+
font-weight: 700;
|
|
4095
|
+
letter-spacing: -0.5px;
|
|
4096
|
+
margin-bottom: 8px;
|
|
4097
|
+
}
|
|
4098
|
+
.login-logo span { color: var(--blue); }
|
|
4099
|
+
.login-version {
|
|
4100
|
+
text-align: center;
|
|
4101
|
+
font-size: 11px;
|
|
4102
|
+
color: var(--text-secondary);
|
|
4103
|
+
font-family: var(--mono);
|
|
4104
|
+
margin-bottom: 32px;
|
|
4105
|
+
}
|
|
4106
|
+
.login-label {
|
|
4107
|
+
display: block;
|
|
4108
|
+
font-size: 13px;
|
|
4109
|
+
font-weight: 600;
|
|
4110
|
+
color: var(--text-secondary);
|
|
4111
|
+
margin-bottom: 8px;
|
|
4112
|
+
}
|
|
4113
|
+
.login-input {
|
|
4114
|
+
width: 100%;
|
|
4115
|
+
padding: 10px 14px;
|
|
4116
|
+
background: var(--bg);
|
|
4117
|
+
border: 1px solid var(--border);
|
|
4118
|
+
border-radius: var(--radius);
|
|
4119
|
+
color: var(--text-primary);
|
|
4120
|
+
font-family: var(--mono);
|
|
4121
|
+
font-size: 14px;
|
|
4122
|
+
outline: none;
|
|
4123
|
+
transition: border-color 0.15s;
|
|
4124
|
+
}
|
|
4125
|
+
.login-input:focus { border-color: var(--blue); }
|
|
4126
|
+
.login-input::placeholder { color: var(--text-secondary); opacity: 0.5; }
|
|
4127
|
+
.login-btn {
|
|
4128
|
+
width: 100%;
|
|
4129
|
+
margin-top: 20px;
|
|
4130
|
+
padding: 10px;
|
|
4131
|
+
background: var(--blue);
|
|
4132
|
+
color: var(--bg);
|
|
4133
|
+
border: none;
|
|
4134
|
+
border-radius: var(--radius);
|
|
4135
|
+
font-size: 14px;
|
|
4136
|
+
font-weight: 600;
|
|
4137
|
+
cursor: pointer;
|
|
4138
|
+
transition: opacity 0.15s;
|
|
4139
|
+
font-family: var(--sans);
|
|
4140
|
+
}
|
|
4141
|
+
.login-btn:hover { opacity: 0.9; }
|
|
4142
|
+
.login-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
4143
|
+
.login-error {
|
|
4144
|
+
margin-top: 16px;
|
|
4145
|
+
padding: 10px 14px;
|
|
4146
|
+
background: rgba(248, 81, 73, 0.1);
|
|
4147
|
+
border: 1px solid var(--red);
|
|
4148
|
+
border-radius: var(--radius);
|
|
4149
|
+
font-size: 12px;
|
|
4150
|
+
color: var(--red);
|
|
4151
|
+
display: none;
|
|
4152
|
+
}
|
|
4153
|
+
.login-hint {
|
|
4154
|
+
margin-top: 24px;
|
|
4155
|
+
padding-top: 16px;
|
|
4156
|
+
border-top: 1px solid var(--border);
|
|
4157
|
+
font-size: 11px;
|
|
4158
|
+
color: var(--text-secondary);
|
|
4159
|
+
line-height: 1.5;
|
|
4160
|
+
}
|
|
4161
|
+
.login-hint code {
|
|
4162
|
+
font-family: var(--mono);
|
|
4163
|
+
background: var(--bg);
|
|
4164
|
+
padding: 1px 4px;
|
|
4165
|
+
border-radius: 3px;
|
|
4166
|
+
font-size: 10px;
|
|
4167
|
+
}
|
|
4168
|
+
</style>
|
|
4169
|
+
</head>
|
|
4170
|
+
<body>
|
|
4171
|
+
<div class="login-container">
|
|
4172
|
+
<div class="login-logo"><span>◆</span> SANCTUARY</div>
|
|
4173
|
+
<div class="login-version">Principal Dashboard v${options.serverVersion}</div>
|
|
4174
|
+
<form id="loginForm" onsubmit="return handleLogin(event)">
|
|
4175
|
+
<label class="login-label" for="tokenInput">Dashboard Auth Token</label>
|
|
4176
|
+
<input class="login-input" type="password" id="tokenInput"
|
|
4177
|
+
placeholder="Enter your auth token" autocomplete="off" autofocus required>
|
|
4178
|
+
<button class="login-btn" type="submit" id="loginBtn">Open Dashboard</button>
|
|
4179
|
+
</form>
|
|
4180
|
+
<div class="login-error" id="loginError"></div>
|
|
4181
|
+
<div class="login-hint">
|
|
4182
|
+
Your token is set via <code>SANCTUARY_DASHBOARD_AUTH_TOKEN</code> environment variable,
|
|
4183
|
+
or check your server's startup output.
|
|
4184
|
+
</div>
|
|
4185
|
+
</div>
|
|
4186
|
+
<script>
|
|
4187
|
+
async function handleLogin(e) {
|
|
4188
|
+
e.preventDefault();
|
|
4189
|
+
var btn = document.getElementById('loginBtn');
|
|
4190
|
+
var errEl = document.getElementById('loginError');
|
|
4191
|
+
var token = document.getElementById('tokenInput').value.trim();
|
|
4192
|
+
if (!token) return false;
|
|
4193
|
+
btn.disabled = true;
|
|
4194
|
+
btn.textContent = 'Authenticating...';
|
|
4195
|
+
errEl.style.display = 'none';
|
|
4196
|
+
try {
|
|
4197
|
+
var resp = await fetch('/auth/session', {
|
|
4198
|
+
method: 'POST',
|
|
4199
|
+
headers: { 'Authorization': 'Bearer ' + token }
|
|
4200
|
+
});
|
|
4201
|
+
if (!resp.ok) {
|
|
4202
|
+
var data = await resp.json().catch(function() { return {}; });
|
|
4203
|
+
throw new Error(data.error || 'Authentication failed');
|
|
4204
|
+
}
|
|
4205
|
+
var result = await resp.json();
|
|
4206
|
+
// Store token in sessionStorage for auto-renewal inside the dashboard
|
|
4207
|
+
try { sessionStorage.setItem('sanctuary_token', token); } catch(_) {}
|
|
4208
|
+
// Set session cookie
|
|
4209
|
+
var maxAge = result.expires_in_seconds || 300;
|
|
4210
|
+
document.cookie = 'sanctuary_session=' + result.session_id +
|
|
4211
|
+
'; path=/; SameSite=Strict; max-age=' + maxAge;
|
|
4212
|
+
// Reload to enter the dashboard
|
|
4213
|
+
window.location.reload();
|
|
4214
|
+
} catch (err) {
|
|
4215
|
+
errEl.textContent = err.message || 'Authentication failed. Check your token.';
|
|
4216
|
+
errEl.style.display = 'block';
|
|
4217
|
+
btn.disabled = false;
|
|
4218
|
+
btn.textContent = 'Open Dashboard';
|
|
4219
|
+
}
|
|
4220
|
+
return false;
|
|
4221
|
+
}
|
|
4222
|
+
</script>
|
|
4223
|
+
</body>
|
|
4224
|
+
</html>`;
|
|
4225
|
+
}
|
|
4045
4226
|
function generateDashboardHTML(options) {
|
|
4046
4227
|
return `<!DOCTYPE html>
|
|
4047
4228
|
<html lang="en">
|
|
@@ -4911,7 +5092,9 @@ function generateDashboardHTML(options) {
|
|
|
4911
5092
|
// \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
5093
|
|
|
4913
5094
|
const TIMEOUT_SECONDS = ${options.timeoutSeconds};
|
|
4914
|
-
|
|
5095
|
+
// AUTH_TOKEN: embedded token (for direct session access) or from sessionStorage (login page flow)
|
|
5096
|
+
const EMBEDDED_TOKEN = ${options.authToken ? JSON.stringify(options.authToken) : "null"};
|
|
5097
|
+
const AUTH_TOKEN = EMBEDDED_TOKEN || (function() { try { return sessionStorage.getItem('sanctuary_token'); } catch(_) { return null; } })();
|
|
4915
5098
|
const MAX_ACTIVITY_ITEMS = 100;
|
|
4916
5099
|
const MAX_THREAT_ITEMS = 20;
|
|
4917
5100
|
|
|
@@ -4926,6 +5109,7 @@ function generateDashboardHTML(options) {
|
|
|
4926
5109
|
const activityItems = [];
|
|
4927
5110
|
const threatItems = [];
|
|
4928
5111
|
let sovereigntyScore = 85;
|
|
5112
|
+
let sessionRenewalTimer = null;
|
|
4929
5113
|
|
|
4930
5114
|
// \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
5115
|
|
|
@@ -4941,6 +5125,11 @@ function generateDashboardHTML(options) {
|
|
|
4941
5125
|
return url + sep + 'session=' + SESSION_ID;
|
|
4942
5126
|
}
|
|
4943
5127
|
|
|
5128
|
+
function setCookie(sessionId, maxAge) {
|
|
5129
|
+
document.cookie = 'sanctuary_session=' + sessionId +
|
|
5130
|
+
'; path=/; SameSite=Strict; max-age=' + maxAge;
|
|
5131
|
+
}
|
|
5132
|
+
|
|
4944
5133
|
async function exchangeSession() {
|
|
4945
5134
|
if (!AUTH_TOKEN) return;
|
|
4946
5135
|
try {
|
|
@@ -4948,14 +5137,35 @@ function generateDashboardHTML(options) {
|
|
|
4948
5137
|
if (resp.ok) {
|
|
4949
5138
|
const data = await resp.json();
|
|
4950
5139
|
SESSION_ID = data.session_id;
|
|
4951
|
-
|
|
4952
|
-
|
|
5140
|
+
var ttl = data.expires_in_seconds || 300;
|
|
5141
|
+
// Update cookie with new session
|
|
5142
|
+
setCookie(SESSION_ID, ttl);
|
|
5143
|
+
// Schedule renewal at 80% of TTL
|
|
5144
|
+
if (sessionRenewalTimer) clearTimeout(sessionRenewalTimer);
|
|
5145
|
+
sessionRenewalTimer = setTimeout(function() {
|
|
5146
|
+
exchangeSession().then(function() { reconnectSSE(); });
|
|
5147
|
+
}, ttl * 800);
|
|
5148
|
+
} else if (resp.status === 401) {
|
|
5149
|
+
// Token invalid or expired \u2014 show non-destructive re-login overlay
|
|
5150
|
+
showSessionExpired();
|
|
4953
5151
|
}
|
|
4954
5152
|
} catch (e) {
|
|
4955
|
-
//
|
|
5153
|
+
// Network error \u2014 retry in 30s
|
|
5154
|
+
if (sessionRenewalTimer) clearTimeout(sessionRenewalTimer);
|
|
5155
|
+
sessionRenewalTimer = setTimeout(function() {
|
|
5156
|
+
exchangeSession().then(function() { reconnectSSE(); });
|
|
5157
|
+
}, 30000);
|
|
4956
5158
|
}
|
|
4957
5159
|
}
|
|
4958
5160
|
|
|
5161
|
+
function showSessionExpired() {
|
|
5162
|
+
// Clear stored token
|
|
5163
|
+
try { sessionStorage.removeItem('sanctuary_token'); } catch(_) {}
|
|
5164
|
+
// Redirect to login page
|
|
5165
|
+
document.cookie = 'sanctuary_session=; path=/; max-age=0';
|
|
5166
|
+
window.location.reload();
|
|
5167
|
+
}
|
|
5168
|
+
|
|
4959
5169
|
// \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
5170
|
|
|
4961
5171
|
function esc(s) {
|
|
@@ -5428,7 +5638,8 @@ function generateDashboardHTML(options) {
|
|
|
5428
5638
|
}
|
|
5429
5639
|
|
|
5430
5640
|
// src/principal-policy/dashboard.ts
|
|
5431
|
-
var
|
|
5641
|
+
var SESSION_TTL_REMOTE_MS = 5 * 60 * 1e3;
|
|
5642
|
+
var SESSION_TTL_LOCAL_MS = 24 * 60 * 60 * 1e3;
|
|
5432
5643
|
var MAX_SESSIONS = 1e3;
|
|
5433
5644
|
var RATE_LIMIT_WINDOW_MS = 6e4;
|
|
5434
5645
|
var RATE_LIMIT_GENERAL = 120;
|
|
@@ -5443,8 +5654,11 @@ var DashboardApprovalChannel = class {
|
|
|
5443
5654
|
baseline = null;
|
|
5444
5655
|
auditLog = null;
|
|
5445
5656
|
dashboardHTML;
|
|
5657
|
+
loginHTML;
|
|
5446
5658
|
authToken;
|
|
5447
5659
|
useTLS;
|
|
5660
|
+
/** Session TTL: longer for localhost, shorter for remote */
|
|
5661
|
+
sessionTTLMs;
|
|
5448
5662
|
/** SEC-012: Short-lived session store. Sessions replace URL query tokens. */
|
|
5449
5663
|
sessions = /* @__PURE__ */ new Map();
|
|
5450
5664
|
sessionCleanupTimer = null;
|
|
@@ -5454,11 +5668,14 @@ var DashboardApprovalChannel = class {
|
|
|
5454
5668
|
this.config = config;
|
|
5455
5669
|
this.authToken = config.auth_token;
|
|
5456
5670
|
this.useTLS = !!(config.tls?.cert_path && config.tls?.key_path);
|
|
5671
|
+
const isLocalhost = config.host === "127.0.0.1" || config.host === "localhost" || config.host === "::1";
|
|
5672
|
+
this.sessionTTLMs = isLocalhost ? SESSION_TTL_LOCAL_MS : SESSION_TTL_REMOTE_MS;
|
|
5457
5673
|
this.dashboardHTML = generateDashboardHTML({
|
|
5458
5674
|
timeoutSeconds: config.timeout_seconds,
|
|
5459
5675
|
serverVersion: SANCTUARY_VERSION,
|
|
5460
5676
|
authToken: this.authToken
|
|
5461
5677
|
});
|
|
5678
|
+
this.loginHTML = generateLoginHTML({ serverVersion: SANCTUARY_VERSION });
|
|
5462
5679
|
this.sessionCleanupTimer = setInterval(() => this.cleanupSessions(), 6e4);
|
|
5463
5680
|
}
|
|
5464
5681
|
/**
|
|
@@ -5488,26 +5705,27 @@ var DashboardApprovalChannel = class {
|
|
|
5488
5705
|
const protocol = this.useTLS ? "https" : "http";
|
|
5489
5706
|
const baseUrl = `${protocol}://${this.config.host}:${this.config.port}`;
|
|
5490
5707
|
this.httpServer.listen(this.config.port, this.config.host, () => {
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
`
|
|
5708
|
+
const sessionUrl = this.authToken ? this.createSessionUrl() : baseUrl;
|
|
5709
|
+
process.stderr.write(
|
|
5710
|
+
`
|
|
5495
5711
|
Sanctuary Principal Dashboard: ${baseUrl}
|
|
5496
5712
|
`
|
|
5497
|
-
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
`
|
|
5502
|
-
);
|
|
5503
|
-
} else {
|
|
5713
|
+
);
|
|
5714
|
+
if (this.authToken) {
|
|
5715
|
+
const hint = this.authToken.slice(0, 4) + "..." + this.authToken.slice(-4);
|
|
5504
5716
|
process.stderr.write(
|
|
5505
|
-
`
|
|
5506
|
-
Sanctuary Principal Dashboard: ${baseUrl}
|
|
5507
|
-
|
|
5717
|
+
` Auth token: ${hint}
|
|
5508
5718
|
`
|
|
5509
5719
|
);
|
|
5510
5720
|
}
|
|
5721
|
+
process.stderr.write(`
|
|
5722
|
+
`);
|
|
5723
|
+
const isTest = !!(process.env.VITEST || process.env.NODE_ENV === "test" || process.env.CI);
|
|
5724
|
+
const isLocalhost = this.config.host === "127.0.0.1" || this.config.host === "localhost" || this.config.host === "::1";
|
|
5725
|
+
const shouldAutoOpen = !isTest && (this.config.auto_open ?? isLocalhost);
|
|
5726
|
+
if (shouldAutoOpen) {
|
|
5727
|
+
this.openInBrowser(sessionUrl);
|
|
5728
|
+
}
|
|
5511
5729
|
resolve();
|
|
5512
5730
|
});
|
|
5513
5731
|
this.httpServer.on("error", reject);
|
|
@@ -5610,10 +5828,47 @@ var DashboardApprovalChannel = class {
|
|
|
5610
5828
|
if (sessionId && this.validateSession(sessionId)) {
|
|
5611
5829
|
return true;
|
|
5612
5830
|
}
|
|
5831
|
+
const cookieSession = this.parseCookie(req, "sanctuary_session");
|
|
5832
|
+
if (cookieSession && this.validateSession(cookieSession)) {
|
|
5833
|
+
return true;
|
|
5834
|
+
}
|
|
5613
5835
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
5614
5836
|
res.end(JSON.stringify({ error: "Unauthorized \u2014 use Authorization: Bearer header or a valid session" }));
|
|
5615
5837
|
return false;
|
|
5616
5838
|
}
|
|
5839
|
+
/**
|
|
5840
|
+
* Check if a request is authenticated WITHOUT sending a response.
|
|
5841
|
+
* Used to decide between login page vs dashboard for GET /.
|
|
5842
|
+
*/
|
|
5843
|
+
isAuthenticated(req, url) {
|
|
5844
|
+
if (!this.authToken) return true;
|
|
5845
|
+
const authHeader = req.headers.authorization;
|
|
5846
|
+
if (authHeader) {
|
|
5847
|
+
const parts = authHeader.split(" ");
|
|
5848
|
+
if (parts.length === 2 && parts[0] === "Bearer" && parts[1] === this.authToken) {
|
|
5849
|
+
return true;
|
|
5850
|
+
}
|
|
5851
|
+
}
|
|
5852
|
+
const sessionId = url.searchParams.get("session");
|
|
5853
|
+
if (sessionId && this.validateSession(sessionId)) return true;
|
|
5854
|
+
const cookieSession = this.parseCookie(req, "sanctuary_session");
|
|
5855
|
+
if (cookieSession && this.validateSession(cookieSession)) return true;
|
|
5856
|
+
return false;
|
|
5857
|
+
}
|
|
5858
|
+
/**
|
|
5859
|
+
* Parse a specific cookie value from the request.
|
|
5860
|
+
*/
|
|
5861
|
+
parseCookie(req, name) {
|
|
5862
|
+
const header = req.headers.cookie;
|
|
5863
|
+
if (!header) return null;
|
|
5864
|
+
for (const part of header.split(";")) {
|
|
5865
|
+
const [key, ...rest] = part.split("=");
|
|
5866
|
+
if (key?.trim() === name) {
|
|
5867
|
+
return rest.join("=").trim();
|
|
5868
|
+
}
|
|
5869
|
+
}
|
|
5870
|
+
return null;
|
|
5871
|
+
}
|
|
5617
5872
|
// ── Session Management (SEC-012) ──────────────────────────────────
|
|
5618
5873
|
/**
|
|
5619
5874
|
* Create a short-lived session by exchanging the long-lived auth token
|
|
@@ -5634,7 +5889,7 @@ var DashboardApprovalChannel = class {
|
|
|
5634
5889
|
this.sessions.set(id, {
|
|
5635
5890
|
id,
|
|
5636
5891
|
created_at: now,
|
|
5637
|
-
expires_at: now +
|
|
5892
|
+
expires_at: now + this.sessionTTLMs
|
|
5638
5893
|
});
|
|
5639
5894
|
return id;
|
|
5640
5895
|
}
|
|
@@ -5733,13 +5988,26 @@ var DashboardApprovalChannel = class {
|
|
|
5733
5988
|
res.end();
|
|
5734
5989
|
return;
|
|
5735
5990
|
}
|
|
5736
|
-
if (
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
if (method === "POST" && url.pathname === "/auth/session") {
|
|
5991
|
+
if (method === "POST" && url.pathname === "/auth/session") {
|
|
5992
|
+
if (!this.checkRateLimit(req, res, "general")) return;
|
|
5993
|
+
try {
|
|
5740
5994
|
this.handleSessionExchange(req, res);
|
|
5995
|
+
} catch {
|
|
5996
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
5997
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
5998
|
+
}
|
|
5999
|
+
return;
|
|
6000
|
+
}
|
|
6001
|
+
if (method === "GET" && url.pathname === "/" && this.authToken) {
|
|
6002
|
+
if (!this.isAuthenticated(req, url)) {
|
|
6003
|
+
if (!this.checkRateLimit(req, res, "general")) return;
|
|
6004
|
+
this.serveLoginPage(res);
|
|
5741
6005
|
return;
|
|
5742
6006
|
}
|
|
6007
|
+
}
|
|
6008
|
+
if (!this.checkAuth(req, url, res)) return;
|
|
6009
|
+
if (!this.checkRateLimit(req, res, "general")) return;
|
|
6010
|
+
try {
|
|
5743
6011
|
if (method === "GET" && url.pathname === "/") {
|
|
5744
6012
|
this.serveDashboard(res);
|
|
5745
6013
|
} else if (method === "GET" && url.pathname === "/events") {
|
|
@@ -5796,12 +6064,23 @@ var DashboardApprovalChannel = class {
|
|
|
5796
6064
|
return;
|
|
5797
6065
|
}
|
|
5798
6066
|
const sessionId = this.createSession();
|
|
5799
|
-
|
|
6067
|
+
const ttlSeconds = Math.floor(this.sessionTTLMs / 1e3);
|
|
6068
|
+
res.writeHead(200, {
|
|
6069
|
+
"Content-Type": "application/json",
|
|
6070
|
+
"Set-Cookie": `sanctuary_session=${sessionId}; Path=/; SameSite=Strict; Max-Age=${ttlSeconds}`
|
|
6071
|
+
});
|
|
5800
6072
|
res.end(JSON.stringify({
|
|
5801
6073
|
session_id: sessionId,
|
|
5802
|
-
expires_in_seconds:
|
|
6074
|
+
expires_in_seconds: ttlSeconds
|
|
5803
6075
|
}));
|
|
5804
6076
|
}
|
|
6077
|
+
serveLoginPage(res) {
|
|
6078
|
+
res.writeHead(200, {
|
|
6079
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
6080
|
+
"Cache-Control": "no-cache, no-store"
|
|
6081
|
+
});
|
|
6082
|
+
res.end(this.loginHTML);
|
|
6083
|
+
}
|
|
5805
6084
|
serveDashboard(res) {
|
|
5806
6085
|
res.writeHead(200, {
|
|
5807
6086
|
"Content-Type": "text/html; charset=utf-8",
|
|
@@ -5977,6 +6256,47 @@ data: ${JSON.stringify(data)}
|
|
|
5977
6256
|
broadcastProtectionStatus(data) {
|
|
5978
6257
|
this.broadcastSSE("protection-status", data);
|
|
5979
6258
|
}
|
|
6259
|
+
/**
|
|
6260
|
+
* Open a URL in the system's default browser.
|
|
6261
|
+
* Cross-platform: macOS (open), Linux (xdg-open), Windows (start).
|
|
6262
|
+
* Fails silently — dashboard still works via terminal URL.
|
|
6263
|
+
*/
|
|
6264
|
+
openInBrowser(url) {
|
|
6265
|
+
const os$1 = os.platform();
|
|
6266
|
+
let cmd;
|
|
6267
|
+
if (os$1 === "darwin") {
|
|
6268
|
+
cmd = `open "${url}"`;
|
|
6269
|
+
} else if (os$1 === "win32") {
|
|
6270
|
+
cmd = `start "" "${url}"`;
|
|
6271
|
+
} else {
|
|
6272
|
+
cmd = `xdg-open "${url}"`;
|
|
6273
|
+
}
|
|
6274
|
+
child_process.exec(cmd, (err) => {
|
|
6275
|
+
if (err) {
|
|
6276
|
+
process.stderr.write(
|
|
6277
|
+
` (Could not auto-open browser. Open the URL above manually.)
|
|
6278
|
+
|
|
6279
|
+
`
|
|
6280
|
+
);
|
|
6281
|
+
}
|
|
6282
|
+
});
|
|
6283
|
+
}
|
|
6284
|
+
/**
|
|
6285
|
+
* Create a pre-authenticated URL for the dashboard.
|
|
6286
|
+
* Used by the sanctuary_dashboard_open tool and at startup.
|
|
6287
|
+
*/
|
|
6288
|
+
createSessionUrl() {
|
|
6289
|
+
const sessionId = this.createSession();
|
|
6290
|
+
const protocol = this.useTLS ? "https" : "http";
|
|
6291
|
+
return `${protocol}://${this.config.host}:${this.config.port}/?session=${sessionId}`;
|
|
6292
|
+
}
|
|
6293
|
+
/**
|
|
6294
|
+
* Get the base URL for the dashboard.
|
|
6295
|
+
*/
|
|
6296
|
+
getBaseUrl() {
|
|
6297
|
+
const protocol = this.useTLS ? "https" : "http";
|
|
6298
|
+
return `${protocol}://${this.config.host}:${this.config.port}`;
|
|
6299
|
+
}
|
|
5980
6300
|
/** Get the number of pending requests */
|
|
5981
6301
|
get pendingCount() {
|
|
5982
6302
|
return this.pending.size;
|
|
@@ -11752,7 +12072,8 @@ async function createSanctuaryServer(options) {
|
|
|
11752
12072
|
timeout_seconds: policy.approval_channel.timeout_seconds,
|
|
11753
12073
|
// SEC-002: auto_deny removed — timeout always denies
|
|
11754
12074
|
auth_token: authToken,
|
|
11755
|
-
tls: config.dashboard.tls
|
|
12075
|
+
tls: config.dashboard.tls,
|
|
12076
|
+
auto_open: config.dashboard.auto_open
|
|
11756
12077
|
});
|
|
11757
12078
|
dashboard.setDependencies({ policy, baseline, auditLog });
|
|
11758
12079
|
await dashboard.start();
|
|
@@ -11791,6 +12112,32 @@ async function createSanctuaryServer(options) {
|
|
|
11791
12112
|
} : void 0;
|
|
11792
12113
|
const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog, injectionDetector, onInjectionAlert);
|
|
11793
12114
|
const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
|
|
12115
|
+
const dashboardTools = [];
|
|
12116
|
+
if (dashboard) {
|
|
12117
|
+
dashboardTools.push({
|
|
12118
|
+
name: "sanctuary/dashboard_open",
|
|
12119
|
+
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.",
|
|
12120
|
+
inputSchema: {
|
|
12121
|
+
type: "object",
|
|
12122
|
+
properties: {}
|
|
12123
|
+
},
|
|
12124
|
+
handler: async () => {
|
|
12125
|
+
const url = dashboard.createSessionUrl();
|
|
12126
|
+
return {
|
|
12127
|
+
content: [
|
|
12128
|
+
{
|
|
12129
|
+
type: "text",
|
|
12130
|
+
text: JSON.stringify({
|
|
12131
|
+
dashboard_url: url,
|
|
12132
|
+
base_url: dashboard.getBaseUrl(),
|
|
12133
|
+
note: "Click the dashboard_url to open the Principal Dashboard. The session is pre-authenticated."
|
|
12134
|
+
}, null, 2)
|
|
12135
|
+
}
|
|
12136
|
+
]
|
|
12137
|
+
};
|
|
12138
|
+
}
|
|
12139
|
+
});
|
|
12140
|
+
}
|
|
11794
12141
|
let allTools = [
|
|
11795
12142
|
...l1Tools,
|
|
11796
12143
|
...l2Tools,
|
|
@@ -11804,6 +12151,7 @@ async function createSanctuaryServer(options) {
|
|
|
11804
12151
|
...auditTools,
|
|
11805
12152
|
...contextGateTools,
|
|
11806
12153
|
...hardeningTools,
|
|
12154
|
+
...dashboardTools,
|
|
11807
12155
|
manifestTool
|
|
11808
12156
|
];
|
|
11809
12157
|
allTools = allTools.map((tool) => ({
|