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