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