@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/index.cjs
CHANGED
|
@@ -293,6 +293,12 @@ async function loadConfig(configPath) {
|
|
|
293
293
|
if (process.env.SANCTUARY_DASHBOARD_AUTH_TOKEN) {
|
|
294
294
|
config.dashboard.auth_token = process.env.SANCTUARY_DASHBOARD_AUTH_TOKEN;
|
|
295
295
|
}
|
|
296
|
+
if (process.env.SANCTUARY_DASHBOARD_AUTO_OPEN === "true") {
|
|
297
|
+
config.dashboard.auto_open = true;
|
|
298
|
+
}
|
|
299
|
+
if (process.env.SANCTUARY_DASHBOARD_AUTO_OPEN === "false") {
|
|
300
|
+
config.dashboard.auto_open = false;
|
|
301
|
+
}
|
|
296
302
|
if (process.env.SANCTUARY_DASHBOARD_TLS_CERT && process.env.SANCTUARY_DASHBOARD_TLS_KEY) {
|
|
297
303
|
config.dashboard.tls = {
|
|
298
304
|
cert_path: process.env.SANCTUARY_DASHBOARD_TLS_CERT,
|
|
@@ -4058,6 +4064,181 @@ var AutoApproveChannel = class {
|
|
|
4058
4064
|
};
|
|
4059
4065
|
|
|
4060
4066
|
// src/principal-policy/dashboard-html.ts
|
|
4067
|
+
function generateLoginHTML(options) {
|
|
4068
|
+
return `<!DOCTYPE html>
|
|
4069
|
+
<html lang="en">
|
|
4070
|
+
<head>
|
|
4071
|
+
<meta charset="utf-8">
|
|
4072
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
4073
|
+
<title>Sanctuary \u2014 Login</title>
|
|
4074
|
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
|
|
4075
|
+
<style>
|
|
4076
|
+
:root {
|
|
4077
|
+
--bg: #0d1117;
|
|
4078
|
+
--surface: #161b22;
|
|
4079
|
+
--border: #30363d;
|
|
4080
|
+
--text-primary: #e6edf3;
|
|
4081
|
+
--text-secondary: #8b949e;
|
|
4082
|
+
--green: #3fb950;
|
|
4083
|
+
--red: #f85149;
|
|
4084
|
+
--blue: #58a6ff;
|
|
4085
|
+
--mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
|
4086
|
+
--sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
4087
|
+
--radius: 6px;
|
|
4088
|
+
}
|
|
4089
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
4090
|
+
html, body { width: 100%; height: 100%; }
|
|
4091
|
+
body {
|
|
4092
|
+
font-family: var(--sans);
|
|
4093
|
+
background: var(--bg);
|
|
4094
|
+
color: var(--text-primary);
|
|
4095
|
+
display: flex;
|
|
4096
|
+
align-items: center;
|
|
4097
|
+
justify-content: center;
|
|
4098
|
+
}
|
|
4099
|
+
.login-container {
|
|
4100
|
+
width: 100%;
|
|
4101
|
+
max-width: 400px;
|
|
4102
|
+
padding: 40px 32px;
|
|
4103
|
+
background: var(--surface);
|
|
4104
|
+
border: 1px solid var(--border);
|
|
4105
|
+
border-radius: 12px;
|
|
4106
|
+
}
|
|
4107
|
+
.login-logo {
|
|
4108
|
+
text-align: center;
|
|
4109
|
+
font-size: 20px;
|
|
4110
|
+
font-weight: 700;
|
|
4111
|
+
letter-spacing: -0.5px;
|
|
4112
|
+
margin-bottom: 8px;
|
|
4113
|
+
}
|
|
4114
|
+
.login-logo span { color: var(--blue); }
|
|
4115
|
+
.login-version {
|
|
4116
|
+
text-align: center;
|
|
4117
|
+
font-size: 11px;
|
|
4118
|
+
color: var(--text-secondary);
|
|
4119
|
+
font-family: var(--mono);
|
|
4120
|
+
margin-bottom: 32px;
|
|
4121
|
+
}
|
|
4122
|
+
.login-label {
|
|
4123
|
+
display: block;
|
|
4124
|
+
font-size: 13px;
|
|
4125
|
+
font-weight: 600;
|
|
4126
|
+
color: var(--text-secondary);
|
|
4127
|
+
margin-bottom: 8px;
|
|
4128
|
+
}
|
|
4129
|
+
.login-input {
|
|
4130
|
+
width: 100%;
|
|
4131
|
+
padding: 10px 14px;
|
|
4132
|
+
background: var(--bg);
|
|
4133
|
+
border: 1px solid var(--border);
|
|
4134
|
+
border-radius: var(--radius);
|
|
4135
|
+
color: var(--text-primary);
|
|
4136
|
+
font-family: var(--mono);
|
|
4137
|
+
font-size: 14px;
|
|
4138
|
+
outline: none;
|
|
4139
|
+
transition: border-color 0.15s;
|
|
4140
|
+
}
|
|
4141
|
+
.login-input:focus { border-color: var(--blue); }
|
|
4142
|
+
.login-input::placeholder { color: var(--text-secondary); opacity: 0.5; }
|
|
4143
|
+
.login-btn {
|
|
4144
|
+
width: 100%;
|
|
4145
|
+
margin-top: 20px;
|
|
4146
|
+
padding: 10px;
|
|
4147
|
+
background: var(--blue);
|
|
4148
|
+
color: var(--bg);
|
|
4149
|
+
border: none;
|
|
4150
|
+
border-radius: var(--radius);
|
|
4151
|
+
font-size: 14px;
|
|
4152
|
+
font-weight: 600;
|
|
4153
|
+
cursor: pointer;
|
|
4154
|
+
transition: opacity 0.15s;
|
|
4155
|
+
font-family: var(--sans);
|
|
4156
|
+
}
|
|
4157
|
+
.login-btn:hover { opacity: 0.9; }
|
|
4158
|
+
.login-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
4159
|
+
.login-error {
|
|
4160
|
+
margin-top: 16px;
|
|
4161
|
+
padding: 10px 14px;
|
|
4162
|
+
background: rgba(248, 81, 73, 0.1);
|
|
4163
|
+
border: 1px solid var(--red);
|
|
4164
|
+
border-radius: var(--radius);
|
|
4165
|
+
font-size: 12px;
|
|
4166
|
+
color: var(--red);
|
|
4167
|
+
display: none;
|
|
4168
|
+
}
|
|
4169
|
+
.login-hint {
|
|
4170
|
+
margin-top: 24px;
|
|
4171
|
+
padding-top: 16px;
|
|
4172
|
+
border-top: 1px solid var(--border);
|
|
4173
|
+
font-size: 11px;
|
|
4174
|
+
color: var(--text-secondary);
|
|
4175
|
+
line-height: 1.5;
|
|
4176
|
+
}
|
|
4177
|
+
.login-hint code {
|
|
4178
|
+
font-family: var(--mono);
|
|
4179
|
+
background: var(--bg);
|
|
4180
|
+
padding: 1px 4px;
|
|
4181
|
+
border-radius: 3px;
|
|
4182
|
+
font-size: 10px;
|
|
4183
|
+
}
|
|
4184
|
+
</style>
|
|
4185
|
+
</head>
|
|
4186
|
+
<body>
|
|
4187
|
+
<div class="login-container">
|
|
4188
|
+
<div class="login-logo"><span>◆</span> SANCTUARY</div>
|
|
4189
|
+
<div class="login-version">Principal Dashboard v${options.serverVersion}</div>
|
|
4190
|
+
<form id="loginForm" onsubmit="return handleLogin(event)">
|
|
4191
|
+
<label class="login-label" for="tokenInput">Dashboard Auth Token</label>
|
|
4192
|
+
<input class="login-input" type="password" id="tokenInput"
|
|
4193
|
+
placeholder="Enter your auth token" autocomplete="off" autofocus required>
|
|
4194
|
+
<button class="login-btn" type="submit" id="loginBtn">Open Dashboard</button>
|
|
4195
|
+
</form>
|
|
4196
|
+
<div class="login-error" id="loginError"></div>
|
|
4197
|
+
<div class="login-hint">
|
|
4198
|
+
Your token is set via <code>SANCTUARY_DASHBOARD_AUTH_TOKEN</code> environment variable,
|
|
4199
|
+
or check your server's startup output.
|
|
4200
|
+
</div>
|
|
4201
|
+
</div>
|
|
4202
|
+
<script>
|
|
4203
|
+
async function handleLogin(e) {
|
|
4204
|
+
e.preventDefault();
|
|
4205
|
+
var btn = document.getElementById('loginBtn');
|
|
4206
|
+
var errEl = document.getElementById('loginError');
|
|
4207
|
+
var token = document.getElementById('tokenInput').value.trim();
|
|
4208
|
+
if (!token) return false;
|
|
4209
|
+
btn.disabled = true;
|
|
4210
|
+
btn.textContent = 'Authenticating...';
|
|
4211
|
+
errEl.style.display = 'none';
|
|
4212
|
+
try {
|
|
4213
|
+
var resp = await fetch('/auth/session', {
|
|
4214
|
+
method: 'POST',
|
|
4215
|
+
headers: { 'Authorization': 'Bearer ' + token }
|
|
4216
|
+
});
|
|
4217
|
+
if (!resp.ok) {
|
|
4218
|
+
var data = await resp.json().catch(function() { return {}; });
|
|
4219
|
+
throw new Error(data.error || 'Authentication failed');
|
|
4220
|
+
}
|
|
4221
|
+
var result = await resp.json();
|
|
4222
|
+
// Store token in sessionStorage for auto-renewal inside the dashboard
|
|
4223
|
+
try { sessionStorage.setItem('sanctuary_token', token); } catch(_) {}
|
|
4224
|
+
// Set session cookie
|
|
4225
|
+
var maxAge = result.expires_in_seconds || 300;
|
|
4226
|
+
document.cookie = 'sanctuary_session=' + result.session_id +
|
|
4227
|
+
'; path=/; SameSite=Strict; max-age=' + maxAge;
|
|
4228
|
+
// Reload to enter the dashboard
|
|
4229
|
+
window.location.reload();
|
|
4230
|
+
} catch (err) {
|
|
4231
|
+
errEl.textContent = err.message || 'Authentication failed. Check your token.';
|
|
4232
|
+
errEl.style.display = 'block';
|
|
4233
|
+
btn.disabled = false;
|
|
4234
|
+
btn.textContent = 'Open Dashboard';
|
|
4235
|
+
}
|
|
4236
|
+
return false;
|
|
4237
|
+
}
|
|
4238
|
+
</script>
|
|
4239
|
+
</body>
|
|
4240
|
+
</html>`;
|
|
4241
|
+
}
|
|
4061
4242
|
function generateDashboardHTML(options) {
|
|
4062
4243
|
return `<!DOCTYPE html>
|
|
4063
4244
|
<html lang="en">
|
|
@@ -4927,7 +5108,9 @@ function generateDashboardHTML(options) {
|
|
|
4927
5108
|
// \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
5109
|
|
|
4929
5110
|
const TIMEOUT_SECONDS = ${options.timeoutSeconds};
|
|
4930
|
-
|
|
5111
|
+
// AUTH_TOKEN: embedded token (for direct session access) or from sessionStorage (login page flow)
|
|
5112
|
+
const EMBEDDED_TOKEN = ${options.authToken ? JSON.stringify(options.authToken) : "null"};
|
|
5113
|
+
const AUTH_TOKEN = EMBEDDED_TOKEN || (function() { try { return sessionStorage.getItem('sanctuary_token'); } catch(_) { return null; } })();
|
|
4931
5114
|
const MAX_ACTIVITY_ITEMS = 100;
|
|
4932
5115
|
const MAX_THREAT_ITEMS = 20;
|
|
4933
5116
|
|
|
@@ -4942,6 +5125,7 @@ function generateDashboardHTML(options) {
|
|
|
4942
5125
|
const activityItems = [];
|
|
4943
5126
|
const threatItems = [];
|
|
4944
5127
|
let sovereigntyScore = 85;
|
|
5128
|
+
let sessionRenewalTimer = null;
|
|
4945
5129
|
|
|
4946
5130
|
// \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
5131
|
|
|
@@ -4957,6 +5141,11 @@ function generateDashboardHTML(options) {
|
|
|
4957
5141
|
return url + sep + 'session=' + SESSION_ID;
|
|
4958
5142
|
}
|
|
4959
5143
|
|
|
5144
|
+
function setCookie(sessionId, maxAge) {
|
|
5145
|
+
document.cookie = 'sanctuary_session=' + sessionId +
|
|
5146
|
+
'; path=/; SameSite=Strict; max-age=' + maxAge;
|
|
5147
|
+
}
|
|
5148
|
+
|
|
4960
5149
|
async function exchangeSession() {
|
|
4961
5150
|
if (!AUTH_TOKEN) return;
|
|
4962
5151
|
try {
|
|
@@ -4964,14 +5153,35 @@ function generateDashboardHTML(options) {
|
|
|
4964
5153
|
if (resp.ok) {
|
|
4965
5154
|
const data = await resp.json();
|
|
4966
5155
|
SESSION_ID = data.session_id;
|
|
4967
|
-
|
|
4968
|
-
|
|
5156
|
+
var ttl = data.expires_in_seconds || 300;
|
|
5157
|
+
// Update cookie with new session
|
|
5158
|
+
setCookie(SESSION_ID, ttl);
|
|
5159
|
+
// Schedule renewal at 80% of TTL
|
|
5160
|
+
if (sessionRenewalTimer) clearTimeout(sessionRenewalTimer);
|
|
5161
|
+
sessionRenewalTimer = setTimeout(function() {
|
|
5162
|
+
exchangeSession().then(function() { reconnectSSE(); });
|
|
5163
|
+
}, ttl * 800);
|
|
5164
|
+
} else if (resp.status === 401) {
|
|
5165
|
+
// Token invalid or expired \u2014 show non-destructive re-login overlay
|
|
5166
|
+
showSessionExpired();
|
|
4969
5167
|
}
|
|
4970
5168
|
} catch (e) {
|
|
4971
|
-
//
|
|
5169
|
+
// Network error \u2014 retry in 30s
|
|
5170
|
+
if (sessionRenewalTimer) clearTimeout(sessionRenewalTimer);
|
|
5171
|
+
sessionRenewalTimer = setTimeout(function() {
|
|
5172
|
+
exchangeSession().then(function() { reconnectSSE(); });
|
|
5173
|
+
}, 30000);
|
|
4972
5174
|
}
|
|
4973
5175
|
}
|
|
4974
5176
|
|
|
5177
|
+
function showSessionExpired() {
|
|
5178
|
+
// Clear stored token
|
|
5179
|
+
try { sessionStorage.removeItem('sanctuary_token'); } catch(_) {}
|
|
5180
|
+
// Redirect to login page
|
|
5181
|
+
document.cookie = 'sanctuary_session=; path=/; max-age=0';
|
|
5182
|
+
window.location.reload();
|
|
5183
|
+
}
|
|
5184
|
+
|
|
4975
5185
|
// \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
5186
|
|
|
4977
5187
|
function esc(s) {
|
|
@@ -5444,7 +5654,8 @@ function generateDashboardHTML(options) {
|
|
|
5444
5654
|
}
|
|
5445
5655
|
|
|
5446
5656
|
// src/principal-policy/dashboard.ts
|
|
5447
|
-
var
|
|
5657
|
+
var SESSION_TTL_REMOTE_MS = 5 * 60 * 1e3;
|
|
5658
|
+
var SESSION_TTL_LOCAL_MS = 24 * 60 * 60 * 1e3;
|
|
5448
5659
|
var MAX_SESSIONS = 1e3;
|
|
5449
5660
|
var RATE_LIMIT_WINDOW_MS = 6e4;
|
|
5450
5661
|
var RATE_LIMIT_GENERAL = 120;
|
|
@@ -5459,8 +5670,11 @@ var DashboardApprovalChannel = class {
|
|
|
5459
5670
|
baseline = null;
|
|
5460
5671
|
auditLog = null;
|
|
5461
5672
|
dashboardHTML;
|
|
5673
|
+
loginHTML;
|
|
5462
5674
|
authToken;
|
|
5463
5675
|
useTLS;
|
|
5676
|
+
/** Session TTL: longer for localhost, shorter for remote */
|
|
5677
|
+
sessionTTLMs;
|
|
5464
5678
|
/** SEC-012: Short-lived session store. Sessions replace URL query tokens. */
|
|
5465
5679
|
sessions = /* @__PURE__ */ new Map();
|
|
5466
5680
|
sessionCleanupTimer = null;
|
|
@@ -5470,11 +5684,14 @@ var DashboardApprovalChannel = class {
|
|
|
5470
5684
|
this.config = config;
|
|
5471
5685
|
this.authToken = config.auth_token;
|
|
5472
5686
|
this.useTLS = !!(config.tls?.cert_path && config.tls?.key_path);
|
|
5687
|
+
const isLocalhost = config.host === "127.0.0.1" || config.host === "localhost" || config.host === "::1";
|
|
5688
|
+
this.sessionTTLMs = isLocalhost ? SESSION_TTL_LOCAL_MS : SESSION_TTL_REMOTE_MS;
|
|
5473
5689
|
this.dashboardHTML = generateDashboardHTML({
|
|
5474
5690
|
timeoutSeconds: config.timeout_seconds,
|
|
5475
5691
|
serverVersion: SANCTUARY_VERSION,
|
|
5476
5692
|
authToken: this.authToken
|
|
5477
5693
|
});
|
|
5694
|
+
this.loginHTML = generateLoginHTML({ serverVersion: SANCTUARY_VERSION });
|
|
5478
5695
|
this.sessionCleanupTimer = setInterval(() => this.cleanupSessions(), 6e4);
|
|
5479
5696
|
}
|
|
5480
5697
|
/**
|
|
@@ -5504,26 +5721,27 @@ var DashboardApprovalChannel = class {
|
|
|
5504
5721
|
const protocol = this.useTLS ? "https" : "http";
|
|
5505
5722
|
const baseUrl = `${protocol}://${this.config.host}:${this.config.port}`;
|
|
5506
5723
|
this.httpServer.listen(this.config.port, this.config.host, () => {
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
`
|
|
5724
|
+
const sessionUrl = this.authToken ? this.createSessionUrl() : baseUrl;
|
|
5725
|
+
process.stderr.write(
|
|
5726
|
+
`
|
|
5511
5727
|
Sanctuary Principal Dashboard: ${baseUrl}
|
|
5512
5728
|
`
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
`
|
|
5518
|
-
);
|
|
5519
|
-
} else {
|
|
5729
|
+
);
|
|
5730
|
+
if (this.authToken) {
|
|
5731
|
+
const hint = this.authToken.slice(0, 4) + "..." + this.authToken.slice(-4);
|
|
5520
5732
|
process.stderr.write(
|
|
5521
|
-
`
|
|
5522
|
-
Sanctuary Principal Dashboard: ${baseUrl}
|
|
5523
|
-
|
|
5733
|
+
` Auth token: ${hint}
|
|
5524
5734
|
`
|
|
5525
5735
|
);
|
|
5526
5736
|
}
|
|
5737
|
+
process.stderr.write(`
|
|
5738
|
+
`);
|
|
5739
|
+
const isTest = !!(process.env.VITEST || process.env.NODE_ENV === "test" || process.env.CI);
|
|
5740
|
+
const isLocalhost = this.config.host === "127.0.0.1" || this.config.host === "localhost" || this.config.host === "::1";
|
|
5741
|
+
const shouldAutoOpen = !isTest && (this.config.auto_open ?? isLocalhost);
|
|
5742
|
+
if (shouldAutoOpen) {
|
|
5743
|
+
this.openInBrowser(sessionUrl);
|
|
5744
|
+
}
|
|
5527
5745
|
resolve();
|
|
5528
5746
|
});
|
|
5529
5747
|
this.httpServer.on("error", reject);
|
|
@@ -5626,10 +5844,47 @@ var DashboardApprovalChannel = class {
|
|
|
5626
5844
|
if (sessionId && this.validateSession(sessionId)) {
|
|
5627
5845
|
return true;
|
|
5628
5846
|
}
|
|
5847
|
+
const cookieSession = this.parseCookie(req, "sanctuary_session");
|
|
5848
|
+
if (cookieSession && this.validateSession(cookieSession)) {
|
|
5849
|
+
return true;
|
|
5850
|
+
}
|
|
5629
5851
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
5630
5852
|
res.end(JSON.stringify({ error: "Unauthorized \u2014 use Authorization: Bearer header or a valid session" }));
|
|
5631
5853
|
return false;
|
|
5632
5854
|
}
|
|
5855
|
+
/**
|
|
5856
|
+
* Check if a request is authenticated WITHOUT sending a response.
|
|
5857
|
+
* Used to decide between login page vs dashboard for GET /.
|
|
5858
|
+
*/
|
|
5859
|
+
isAuthenticated(req, url) {
|
|
5860
|
+
if (!this.authToken) return true;
|
|
5861
|
+
const authHeader = req.headers.authorization;
|
|
5862
|
+
if (authHeader) {
|
|
5863
|
+
const parts = authHeader.split(" ");
|
|
5864
|
+
if (parts.length === 2 && parts[0] === "Bearer" && parts[1] === this.authToken) {
|
|
5865
|
+
return true;
|
|
5866
|
+
}
|
|
5867
|
+
}
|
|
5868
|
+
const sessionId = url.searchParams.get("session");
|
|
5869
|
+
if (sessionId && this.validateSession(sessionId)) return true;
|
|
5870
|
+
const cookieSession = this.parseCookie(req, "sanctuary_session");
|
|
5871
|
+
if (cookieSession && this.validateSession(cookieSession)) return true;
|
|
5872
|
+
return false;
|
|
5873
|
+
}
|
|
5874
|
+
/**
|
|
5875
|
+
* Parse a specific cookie value from the request.
|
|
5876
|
+
*/
|
|
5877
|
+
parseCookie(req, name) {
|
|
5878
|
+
const header = req.headers.cookie;
|
|
5879
|
+
if (!header) return null;
|
|
5880
|
+
for (const part of header.split(";")) {
|
|
5881
|
+
const [key, ...rest] = part.split("=");
|
|
5882
|
+
if (key?.trim() === name) {
|
|
5883
|
+
return rest.join("=").trim();
|
|
5884
|
+
}
|
|
5885
|
+
}
|
|
5886
|
+
return null;
|
|
5887
|
+
}
|
|
5633
5888
|
// ── Session Management (SEC-012) ──────────────────────────────────
|
|
5634
5889
|
/**
|
|
5635
5890
|
* Create a short-lived session by exchanging the long-lived auth token
|
|
@@ -5650,7 +5905,7 @@ var DashboardApprovalChannel = class {
|
|
|
5650
5905
|
this.sessions.set(id, {
|
|
5651
5906
|
id,
|
|
5652
5907
|
created_at: now,
|
|
5653
|
-
expires_at: now +
|
|
5908
|
+
expires_at: now + this.sessionTTLMs
|
|
5654
5909
|
});
|
|
5655
5910
|
return id;
|
|
5656
5911
|
}
|
|
@@ -5749,13 +6004,26 @@ var DashboardApprovalChannel = class {
|
|
|
5749
6004
|
res.end();
|
|
5750
6005
|
return;
|
|
5751
6006
|
}
|
|
5752
|
-
if (
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
if (method === "POST" && url.pathname === "/auth/session") {
|
|
6007
|
+
if (method === "POST" && url.pathname === "/auth/session") {
|
|
6008
|
+
if (!this.checkRateLimit(req, res, "general")) return;
|
|
6009
|
+
try {
|
|
5756
6010
|
this.handleSessionExchange(req, res);
|
|
6011
|
+
} catch {
|
|
6012
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
6013
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
6014
|
+
}
|
|
6015
|
+
return;
|
|
6016
|
+
}
|
|
6017
|
+
if (method === "GET" && url.pathname === "/" && this.authToken) {
|
|
6018
|
+
if (!this.isAuthenticated(req, url)) {
|
|
6019
|
+
if (!this.checkRateLimit(req, res, "general")) return;
|
|
6020
|
+
this.serveLoginPage(res);
|
|
5757
6021
|
return;
|
|
5758
6022
|
}
|
|
6023
|
+
}
|
|
6024
|
+
if (!this.checkAuth(req, url, res)) return;
|
|
6025
|
+
if (!this.checkRateLimit(req, res, "general")) return;
|
|
6026
|
+
try {
|
|
5759
6027
|
if (method === "GET" && url.pathname === "/") {
|
|
5760
6028
|
this.serveDashboard(res);
|
|
5761
6029
|
} else if (method === "GET" && url.pathname === "/events") {
|
|
@@ -5812,12 +6080,23 @@ var DashboardApprovalChannel = class {
|
|
|
5812
6080
|
return;
|
|
5813
6081
|
}
|
|
5814
6082
|
const sessionId = this.createSession();
|
|
5815
|
-
|
|
6083
|
+
const ttlSeconds = Math.floor(this.sessionTTLMs / 1e3);
|
|
6084
|
+
res.writeHead(200, {
|
|
6085
|
+
"Content-Type": "application/json",
|
|
6086
|
+
"Set-Cookie": `sanctuary_session=${sessionId}; Path=/; SameSite=Strict; Max-Age=${ttlSeconds}`
|
|
6087
|
+
});
|
|
5816
6088
|
res.end(JSON.stringify({
|
|
5817
6089
|
session_id: sessionId,
|
|
5818
|
-
expires_in_seconds:
|
|
6090
|
+
expires_in_seconds: ttlSeconds
|
|
5819
6091
|
}));
|
|
5820
6092
|
}
|
|
6093
|
+
serveLoginPage(res) {
|
|
6094
|
+
res.writeHead(200, {
|
|
6095
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
6096
|
+
"Cache-Control": "no-cache, no-store"
|
|
6097
|
+
});
|
|
6098
|
+
res.end(this.loginHTML);
|
|
6099
|
+
}
|
|
5821
6100
|
serveDashboard(res) {
|
|
5822
6101
|
res.writeHead(200, {
|
|
5823
6102
|
"Content-Type": "text/html; charset=utf-8",
|
|
@@ -5993,6 +6272,47 @@ data: ${JSON.stringify(data)}
|
|
|
5993
6272
|
broadcastProtectionStatus(data) {
|
|
5994
6273
|
this.broadcastSSE("protection-status", data);
|
|
5995
6274
|
}
|
|
6275
|
+
/**
|
|
6276
|
+
* Open a URL in the system's default browser.
|
|
6277
|
+
* Cross-platform: macOS (open), Linux (xdg-open), Windows (start).
|
|
6278
|
+
* Fails silently — dashboard still works via terminal URL.
|
|
6279
|
+
*/
|
|
6280
|
+
openInBrowser(url) {
|
|
6281
|
+
const os$1 = os.platform();
|
|
6282
|
+
let cmd;
|
|
6283
|
+
if (os$1 === "darwin") {
|
|
6284
|
+
cmd = `open "${url}"`;
|
|
6285
|
+
} else if (os$1 === "win32") {
|
|
6286
|
+
cmd = `start "" "${url}"`;
|
|
6287
|
+
} else {
|
|
6288
|
+
cmd = `xdg-open "${url}"`;
|
|
6289
|
+
}
|
|
6290
|
+
child_process.exec(cmd, (err) => {
|
|
6291
|
+
if (err) {
|
|
6292
|
+
process.stderr.write(
|
|
6293
|
+
` (Could not auto-open browser. Open the URL above manually.)
|
|
6294
|
+
|
|
6295
|
+
`
|
|
6296
|
+
);
|
|
6297
|
+
}
|
|
6298
|
+
});
|
|
6299
|
+
}
|
|
6300
|
+
/**
|
|
6301
|
+
* Create a pre-authenticated URL for the dashboard.
|
|
6302
|
+
* Used by the sanctuary_dashboard_open tool and at startup.
|
|
6303
|
+
*/
|
|
6304
|
+
createSessionUrl() {
|
|
6305
|
+
const sessionId = this.createSession();
|
|
6306
|
+
const protocol = this.useTLS ? "https" : "http";
|
|
6307
|
+
return `${protocol}://${this.config.host}:${this.config.port}/?session=${sessionId}`;
|
|
6308
|
+
}
|
|
6309
|
+
/**
|
|
6310
|
+
* Get the base URL for the dashboard.
|
|
6311
|
+
*/
|
|
6312
|
+
getBaseUrl() {
|
|
6313
|
+
const protocol = this.useTLS ? "https" : "http";
|
|
6314
|
+
return `${protocol}://${this.config.host}:${this.config.port}`;
|
|
6315
|
+
}
|
|
5996
6316
|
/** Get the number of pending requests */
|
|
5997
6317
|
get pendingCount() {
|
|
5998
6318
|
return this.pending.size;
|
|
@@ -11823,7 +12143,8 @@ async function createSanctuaryServer(options) {
|
|
|
11823
12143
|
timeout_seconds: policy.approval_channel.timeout_seconds,
|
|
11824
12144
|
// SEC-002: auto_deny removed — timeout always denies
|
|
11825
12145
|
auth_token: authToken,
|
|
11826
|
-
tls: config.dashboard.tls
|
|
12146
|
+
tls: config.dashboard.tls,
|
|
12147
|
+
auto_open: config.dashboard.auto_open
|
|
11827
12148
|
});
|
|
11828
12149
|
dashboard.setDependencies({ policy, baseline, auditLog });
|
|
11829
12150
|
await dashboard.start();
|
|
@@ -11862,6 +12183,32 @@ async function createSanctuaryServer(options) {
|
|
|
11862
12183
|
} : void 0;
|
|
11863
12184
|
const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog, injectionDetector, onInjectionAlert);
|
|
11864
12185
|
const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
|
|
12186
|
+
const dashboardTools = [];
|
|
12187
|
+
if (dashboard) {
|
|
12188
|
+
dashboardTools.push({
|
|
12189
|
+
name: "sanctuary/dashboard_open",
|
|
12190
|
+
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.",
|
|
12191
|
+
inputSchema: {
|
|
12192
|
+
type: "object",
|
|
12193
|
+
properties: {}
|
|
12194
|
+
},
|
|
12195
|
+
handler: async () => {
|
|
12196
|
+
const url = dashboard.createSessionUrl();
|
|
12197
|
+
return {
|
|
12198
|
+
content: [
|
|
12199
|
+
{
|
|
12200
|
+
type: "text",
|
|
12201
|
+
text: JSON.stringify({
|
|
12202
|
+
dashboard_url: url,
|
|
12203
|
+
base_url: dashboard.getBaseUrl(),
|
|
12204
|
+
note: "Click the dashboard_url to open the Principal Dashboard. The session is pre-authenticated."
|
|
12205
|
+
}, null, 2)
|
|
12206
|
+
}
|
|
12207
|
+
]
|
|
12208
|
+
};
|
|
12209
|
+
}
|
|
12210
|
+
});
|
|
12211
|
+
}
|
|
11865
12212
|
let allTools = [
|
|
11866
12213
|
...l1Tools,
|
|
11867
12214
|
...l2Tools,
|
|
@@ -11875,6 +12222,7 @@ async function createSanctuaryServer(options) {
|
|
|
11875
12222
|
...auditTools,
|
|
11876
12223
|
...contextGateTools,
|
|
11877
12224
|
...hardeningTools,
|
|
12225
|
+
...dashboardTools,
|
|
11878
12226
|
manifestTool
|
|
11879
12227
|
];
|
|
11880
12228
|
allTools = allTools.map((tool) => ({
|