@sanctuary-framework/mcp-server 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +359 -36
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +359 -36
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +359 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +359 -36
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -256,7 +256,18 @@ function defaultConfig() {
|
|
|
256
256
|
};
|
|
257
257
|
}
|
|
258
258
|
async function loadConfig(configPath) {
|
|
259
|
-
|
|
259
|
+
let config = defaultConfig();
|
|
260
|
+
const storagePath = process.env.SANCTUARY_STORAGE_PATH ?? config.storage_path;
|
|
261
|
+
const path = configPath ?? join(storagePath, "sanctuary.json");
|
|
262
|
+
try {
|
|
263
|
+
const raw = await readFile(path, "utf-8");
|
|
264
|
+
const fileConfig = JSON.parse(raw);
|
|
265
|
+
config = deepMerge(config, fileConfig);
|
|
266
|
+
} catch (err) {
|
|
267
|
+
if (err instanceof Error && err.message.includes("unimplemented features")) {
|
|
268
|
+
throw err;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
260
271
|
if (process.env.SANCTUARY_STORAGE_PATH) {
|
|
261
272
|
config.storage_path = process.env.SANCTUARY_STORAGE_PATH;
|
|
262
273
|
}
|
|
@@ -269,6 +280,9 @@ async function loadConfig(configPath) {
|
|
|
269
280
|
if (process.env.SANCTUARY_DASHBOARD_ENABLED === "true") {
|
|
270
281
|
config.dashboard.enabled = true;
|
|
271
282
|
}
|
|
283
|
+
if (process.env.SANCTUARY_DASHBOARD_ENABLED === "false") {
|
|
284
|
+
config.dashboard.enabled = false;
|
|
285
|
+
}
|
|
272
286
|
if (process.env.SANCTUARY_DASHBOARD_PORT) {
|
|
273
287
|
config.dashboard.port = parseInt(process.env.SANCTUARY_DASHBOARD_PORT, 10);
|
|
274
288
|
}
|
|
@@ -287,6 +301,9 @@ async function loadConfig(configPath) {
|
|
|
287
301
|
if (process.env.SANCTUARY_WEBHOOK_ENABLED === "true") {
|
|
288
302
|
config.webhook.enabled = true;
|
|
289
303
|
}
|
|
304
|
+
if (process.env.SANCTUARY_WEBHOOK_ENABLED === "false") {
|
|
305
|
+
config.webhook.enabled = false;
|
|
306
|
+
}
|
|
290
307
|
if (process.env.SANCTUARY_WEBHOOK_URL) {
|
|
291
308
|
config.webhook.url = process.env.SANCTUARY_WEBHOOK_URL;
|
|
292
309
|
}
|
|
@@ -299,19 +316,9 @@ async function loadConfig(configPath) {
|
|
|
299
316
|
if (process.env.SANCTUARY_WEBHOOK_CALLBACK_HOST) {
|
|
300
317
|
config.webhook.callback_host = process.env.SANCTUARY_WEBHOOK_CALLBACK_HOST;
|
|
301
318
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const fileConfig = JSON.parse(raw);
|
|
306
|
-
const merged = deepMerge(config, fileConfig);
|
|
307
|
-
validateConfig(merged);
|
|
308
|
-
return merged;
|
|
309
|
-
} catch (err) {
|
|
310
|
-
if (err instanceof Error && err.message.includes("unimplemented features")) {
|
|
311
|
-
throw err;
|
|
312
|
-
}
|
|
313
|
-
return config;
|
|
314
|
-
}
|
|
319
|
+
config.version = PKG_VERSION;
|
|
320
|
+
validateConfig(config);
|
|
321
|
+
return config;
|
|
315
322
|
}
|
|
316
323
|
async function saveConfig(config, configPath) {
|
|
317
324
|
const path = join(config.storage_path, "sanctuary.json");
|
|
@@ -4032,6 +4039,181 @@ var StderrApprovalChannel = class {
|
|
|
4032
4039
|
};
|
|
4033
4040
|
|
|
4034
4041
|
// src/principal-policy/dashboard-html.ts
|
|
4042
|
+
function generateLoginHTML(options) {
|
|
4043
|
+
return `<!DOCTYPE html>
|
|
4044
|
+
<html lang="en">
|
|
4045
|
+
<head>
|
|
4046
|
+
<meta charset="utf-8">
|
|
4047
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
4048
|
+
<title>Sanctuary \u2014 Login</title>
|
|
4049
|
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
|
|
4050
|
+
<style>
|
|
4051
|
+
:root {
|
|
4052
|
+
--bg: #0d1117;
|
|
4053
|
+
--surface: #161b22;
|
|
4054
|
+
--border: #30363d;
|
|
4055
|
+
--text-primary: #e6edf3;
|
|
4056
|
+
--text-secondary: #8b949e;
|
|
4057
|
+
--green: #3fb950;
|
|
4058
|
+
--red: #f85149;
|
|
4059
|
+
--blue: #58a6ff;
|
|
4060
|
+
--mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
|
4061
|
+
--sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
4062
|
+
--radius: 6px;
|
|
4063
|
+
}
|
|
4064
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
4065
|
+
html, body { width: 100%; height: 100%; }
|
|
4066
|
+
body {
|
|
4067
|
+
font-family: var(--sans);
|
|
4068
|
+
background: var(--bg);
|
|
4069
|
+
color: var(--text-primary);
|
|
4070
|
+
display: flex;
|
|
4071
|
+
align-items: center;
|
|
4072
|
+
justify-content: center;
|
|
4073
|
+
}
|
|
4074
|
+
.login-container {
|
|
4075
|
+
width: 100%;
|
|
4076
|
+
max-width: 400px;
|
|
4077
|
+
padding: 40px 32px;
|
|
4078
|
+
background: var(--surface);
|
|
4079
|
+
border: 1px solid var(--border);
|
|
4080
|
+
border-radius: 12px;
|
|
4081
|
+
}
|
|
4082
|
+
.login-logo {
|
|
4083
|
+
text-align: center;
|
|
4084
|
+
font-size: 20px;
|
|
4085
|
+
font-weight: 700;
|
|
4086
|
+
letter-spacing: -0.5px;
|
|
4087
|
+
margin-bottom: 8px;
|
|
4088
|
+
}
|
|
4089
|
+
.login-logo span { color: var(--blue); }
|
|
4090
|
+
.login-version {
|
|
4091
|
+
text-align: center;
|
|
4092
|
+
font-size: 11px;
|
|
4093
|
+
color: var(--text-secondary);
|
|
4094
|
+
font-family: var(--mono);
|
|
4095
|
+
margin-bottom: 32px;
|
|
4096
|
+
}
|
|
4097
|
+
.login-label {
|
|
4098
|
+
display: block;
|
|
4099
|
+
font-size: 13px;
|
|
4100
|
+
font-weight: 600;
|
|
4101
|
+
color: var(--text-secondary);
|
|
4102
|
+
margin-bottom: 8px;
|
|
4103
|
+
}
|
|
4104
|
+
.login-input {
|
|
4105
|
+
width: 100%;
|
|
4106
|
+
padding: 10px 14px;
|
|
4107
|
+
background: var(--bg);
|
|
4108
|
+
border: 1px solid var(--border);
|
|
4109
|
+
border-radius: var(--radius);
|
|
4110
|
+
color: var(--text-primary);
|
|
4111
|
+
font-family: var(--mono);
|
|
4112
|
+
font-size: 14px;
|
|
4113
|
+
outline: none;
|
|
4114
|
+
transition: border-color 0.15s;
|
|
4115
|
+
}
|
|
4116
|
+
.login-input:focus { border-color: var(--blue); }
|
|
4117
|
+
.login-input::placeholder { color: var(--text-secondary); opacity: 0.5; }
|
|
4118
|
+
.login-btn {
|
|
4119
|
+
width: 100%;
|
|
4120
|
+
margin-top: 20px;
|
|
4121
|
+
padding: 10px;
|
|
4122
|
+
background: var(--blue);
|
|
4123
|
+
color: var(--bg);
|
|
4124
|
+
border: none;
|
|
4125
|
+
border-radius: var(--radius);
|
|
4126
|
+
font-size: 14px;
|
|
4127
|
+
font-weight: 600;
|
|
4128
|
+
cursor: pointer;
|
|
4129
|
+
transition: opacity 0.15s;
|
|
4130
|
+
font-family: var(--sans);
|
|
4131
|
+
}
|
|
4132
|
+
.login-btn:hover { opacity: 0.9; }
|
|
4133
|
+
.login-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
4134
|
+
.login-error {
|
|
4135
|
+
margin-top: 16px;
|
|
4136
|
+
padding: 10px 14px;
|
|
4137
|
+
background: rgba(248, 81, 73, 0.1);
|
|
4138
|
+
border: 1px solid var(--red);
|
|
4139
|
+
border-radius: var(--radius);
|
|
4140
|
+
font-size: 12px;
|
|
4141
|
+
color: var(--red);
|
|
4142
|
+
display: none;
|
|
4143
|
+
}
|
|
4144
|
+
.login-hint {
|
|
4145
|
+
margin-top: 24px;
|
|
4146
|
+
padding-top: 16px;
|
|
4147
|
+
border-top: 1px solid var(--border);
|
|
4148
|
+
font-size: 11px;
|
|
4149
|
+
color: var(--text-secondary);
|
|
4150
|
+
line-height: 1.5;
|
|
4151
|
+
}
|
|
4152
|
+
.login-hint code {
|
|
4153
|
+
font-family: var(--mono);
|
|
4154
|
+
background: var(--bg);
|
|
4155
|
+
padding: 1px 4px;
|
|
4156
|
+
border-radius: 3px;
|
|
4157
|
+
font-size: 10px;
|
|
4158
|
+
}
|
|
4159
|
+
</style>
|
|
4160
|
+
</head>
|
|
4161
|
+
<body>
|
|
4162
|
+
<div class="login-container">
|
|
4163
|
+
<div class="login-logo"><span>◆</span> SANCTUARY</div>
|
|
4164
|
+
<div class="login-version">Principal Dashboard v${options.serverVersion}</div>
|
|
4165
|
+
<form id="loginForm" onsubmit="return handleLogin(event)">
|
|
4166
|
+
<label class="login-label" for="tokenInput">Dashboard Auth Token</label>
|
|
4167
|
+
<input class="login-input" type="password" id="tokenInput"
|
|
4168
|
+
placeholder="Enter your auth token" autocomplete="off" autofocus required>
|
|
4169
|
+
<button class="login-btn" type="submit" id="loginBtn">Open Dashboard</button>
|
|
4170
|
+
</form>
|
|
4171
|
+
<div class="login-error" id="loginError"></div>
|
|
4172
|
+
<div class="login-hint">
|
|
4173
|
+
Your token is set via <code>SANCTUARY_DASHBOARD_AUTH_TOKEN</code> environment variable,
|
|
4174
|
+
or check your server's startup output.
|
|
4175
|
+
</div>
|
|
4176
|
+
</div>
|
|
4177
|
+
<script>
|
|
4178
|
+
async function handleLogin(e) {
|
|
4179
|
+
e.preventDefault();
|
|
4180
|
+
var btn = document.getElementById('loginBtn');
|
|
4181
|
+
var errEl = document.getElementById('loginError');
|
|
4182
|
+
var token = document.getElementById('tokenInput').value.trim();
|
|
4183
|
+
if (!token) return false;
|
|
4184
|
+
btn.disabled = true;
|
|
4185
|
+
btn.textContent = 'Authenticating...';
|
|
4186
|
+
errEl.style.display = 'none';
|
|
4187
|
+
try {
|
|
4188
|
+
var resp = await fetch('/auth/session', {
|
|
4189
|
+
method: 'POST',
|
|
4190
|
+
headers: { 'Authorization': 'Bearer ' + token }
|
|
4191
|
+
});
|
|
4192
|
+
if (!resp.ok) {
|
|
4193
|
+
var data = await resp.json().catch(function() { return {}; });
|
|
4194
|
+
throw new Error(data.error || 'Authentication failed');
|
|
4195
|
+
}
|
|
4196
|
+
var result = await resp.json();
|
|
4197
|
+
// Store token in sessionStorage for auto-renewal inside the dashboard
|
|
4198
|
+
try { sessionStorage.setItem('sanctuary_token', token); } catch(_) {}
|
|
4199
|
+
// Set session cookie
|
|
4200
|
+
var maxAge = result.expires_in_seconds || 300;
|
|
4201
|
+
document.cookie = 'sanctuary_session=' + result.session_id +
|
|
4202
|
+
'; path=/; SameSite=Strict; max-age=' + maxAge;
|
|
4203
|
+
// Reload to enter the dashboard
|
|
4204
|
+
window.location.reload();
|
|
4205
|
+
} catch (err) {
|
|
4206
|
+
errEl.textContent = err.message || 'Authentication failed. Check your token.';
|
|
4207
|
+
errEl.style.display = 'block';
|
|
4208
|
+
btn.disabled = false;
|
|
4209
|
+
btn.textContent = 'Open Dashboard';
|
|
4210
|
+
}
|
|
4211
|
+
return false;
|
|
4212
|
+
}
|
|
4213
|
+
</script>
|
|
4214
|
+
</body>
|
|
4215
|
+
</html>`;
|
|
4216
|
+
}
|
|
4035
4217
|
function generateDashboardHTML(options) {
|
|
4036
4218
|
return `<!DOCTYPE html>
|
|
4037
4219
|
<html lang="en">
|
|
@@ -4901,7 +5083,9 @@ function generateDashboardHTML(options) {
|
|
|
4901
5083
|
// \u2500\u2500 Configuration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
4902
5084
|
|
|
4903
5085
|
const TIMEOUT_SECONDS = ${options.timeoutSeconds};
|
|
4904
|
-
|
|
5086
|
+
// AUTH_TOKEN: embedded token (for direct session access) or from sessionStorage (login page flow)
|
|
5087
|
+
const EMBEDDED_TOKEN = ${options.authToken ? JSON.stringify(options.authToken) : "null"};
|
|
5088
|
+
const AUTH_TOKEN = EMBEDDED_TOKEN || (function() { try { return sessionStorage.getItem('sanctuary_token'); } catch(_) { return null; } })();
|
|
4905
5089
|
const MAX_ACTIVITY_ITEMS = 100;
|
|
4906
5090
|
const MAX_THREAT_ITEMS = 20;
|
|
4907
5091
|
|
|
@@ -4916,6 +5100,7 @@ function generateDashboardHTML(options) {
|
|
|
4916
5100
|
const activityItems = [];
|
|
4917
5101
|
const threatItems = [];
|
|
4918
5102
|
let sovereigntyScore = 85;
|
|
5103
|
+
let sessionRenewalTimer = null;
|
|
4919
5104
|
|
|
4920
5105
|
// \u2500\u2500 Auth Helpers (SEC-012) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
4921
5106
|
|
|
@@ -4931,6 +5116,11 @@ function generateDashboardHTML(options) {
|
|
|
4931
5116
|
return url + sep + 'session=' + SESSION_ID;
|
|
4932
5117
|
}
|
|
4933
5118
|
|
|
5119
|
+
function setCookie(sessionId, maxAge) {
|
|
5120
|
+
document.cookie = 'sanctuary_session=' + sessionId +
|
|
5121
|
+
'; path=/; SameSite=Strict; max-age=' + maxAge;
|
|
5122
|
+
}
|
|
5123
|
+
|
|
4934
5124
|
async function exchangeSession() {
|
|
4935
5125
|
if (!AUTH_TOKEN) return;
|
|
4936
5126
|
try {
|
|
@@ -4938,14 +5128,35 @@ function generateDashboardHTML(options) {
|
|
|
4938
5128
|
if (resp.ok) {
|
|
4939
5129
|
const data = await resp.json();
|
|
4940
5130
|
SESSION_ID = data.session_id;
|
|
4941
|
-
|
|
4942
|
-
|
|
5131
|
+
var ttl = data.expires_in_seconds || 300;
|
|
5132
|
+
// Update cookie with new session
|
|
5133
|
+
setCookie(SESSION_ID, ttl);
|
|
5134
|
+
// Schedule renewal at 80% of TTL
|
|
5135
|
+
if (sessionRenewalTimer) clearTimeout(sessionRenewalTimer);
|
|
5136
|
+
sessionRenewalTimer = setTimeout(function() {
|
|
5137
|
+
exchangeSession().then(function() { reconnectSSE(); });
|
|
5138
|
+
}, ttl * 800);
|
|
5139
|
+
} else if (resp.status === 401) {
|
|
5140
|
+
// Token invalid or expired \u2014 show non-destructive re-login overlay
|
|
5141
|
+
showSessionExpired();
|
|
4943
5142
|
}
|
|
4944
5143
|
} catch (e) {
|
|
4945
|
-
//
|
|
5144
|
+
// Network error \u2014 retry in 30s
|
|
5145
|
+
if (sessionRenewalTimer) clearTimeout(sessionRenewalTimer);
|
|
5146
|
+
sessionRenewalTimer = setTimeout(function() {
|
|
5147
|
+
exchangeSession().then(function() { reconnectSSE(); });
|
|
5148
|
+
}, 30000);
|
|
4946
5149
|
}
|
|
4947
5150
|
}
|
|
4948
5151
|
|
|
5152
|
+
function showSessionExpired() {
|
|
5153
|
+
// Clear stored token
|
|
5154
|
+
try { sessionStorage.removeItem('sanctuary_token'); } catch(_) {}
|
|
5155
|
+
// Redirect to login page
|
|
5156
|
+
document.cookie = 'sanctuary_session=; path=/; max-age=0';
|
|
5157
|
+
window.location.reload();
|
|
5158
|
+
}
|
|
5159
|
+
|
|
4949
5160
|
// \u2500\u2500 UI Utilities \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
4950
5161
|
|
|
4951
5162
|
function esc(s) {
|
|
@@ -5418,7 +5629,8 @@ function generateDashboardHTML(options) {
|
|
|
5418
5629
|
}
|
|
5419
5630
|
|
|
5420
5631
|
// src/principal-policy/dashboard.ts
|
|
5421
|
-
var
|
|
5632
|
+
var SESSION_TTL_REMOTE_MS = 5 * 60 * 1e3;
|
|
5633
|
+
var SESSION_TTL_LOCAL_MS = 24 * 60 * 60 * 1e3;
|
|
5422
5634
|
var MAX_SESSIONS = 1e3;
|
|
5423
5635
|
var RATE_LIMIT_WINDOW_MS = 6e4;
|
|
5424
5636
|
var RATE_LIMIT_GENERAL = 120;
|
|
@@ -5433,8 +5645,11 @@ var DashboardApprovalChannel = class {
|
|
|
5433
5645
|
baseline = null;
|
|
5434
5646
|
auditLog = null;
|
|
5435
5647
|
dashboardHTML;
|
|
5648
|
+
loginHTML;
|
|
5436
5649
|
authToken;
|
|
5437
5650
|
useTLS;
|
|
5651
|
+
/** Session TTL: longer for localhost, shorter for remote */
|
|
5652
|
+
sessionTTLMs;
|
|
5438
5653
|
/** SEC-012: Short-lived session store. Sessions replace URL query tokens. */
|
|
5439
5654
|
sessions = /* @__PURE__ */ new Map();
|
|
5440
5655
|
sessionCleanupTimer = null;
|
|
@@ -5444,11 +5659,14 @@ var DashboardApprovalChannel = class {
|
|
|
5444
5659
|
this.config = config;
|
|
5445
5660
|
this.authToken = config.auth_token;
|
|
5446
5661
|
this.useTLS = !!(config.tls?.cert_path && config.tls?.key_path);
|
|
5662
|
+
const isLocalhost = config.host === "127.0.0.1" || config.host === "localhost" || config.host === "::1";
|
|
5663
|
+
this.sessionTTLMs = isLocalhost ? SESSION_TTL_LOCAL_MS : SESSION_TTL_REMOTE_MS;
|
|
5447
5664
|
this.dashboardHTML = generateDashboardHTML({
|
|
5448
5665
|
timeoutSeconds: config.timeout_seconds,
|
|
5449
5666
|
serverVersion: SANCTUARY_VERSION,
|
|
5450
5667
|
authToken: this.authToken
|
|
5451
5668
|
});
|
|
5669
|
+
this.loginHTML = generateLoginHTML({ serverVersion: SANCTUARY_VERSION });
|
|
5452
5670
|
this.sessionCleanupTimer = setInterval(() => this.cleanupSessions(), 6e4);
|
|
5453
5671
|
}
|
|
5454
5672
|
/**
|
|
@@ -5478,25 +5696,26 @@ var DashboardApprovalChannel = class {
|
|
|
5478
5696
|
const protocol = this.useTLS ? "https" : "http";
|
|
5479
5697
|
const baseUrl = `${protocol}://${this.config.host}:${this.config.port}`;
|
|
5480
5698
|
this.httpServer.listen(this.config.port, this.config.host, () => {
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
process.stderr.write(
|
|
5484
|
-
`
|
|
5699
|
+
process.stderr.write(
|
|
5700
|
+
`
|
|
5485
5701
|
Sanctuary Principal Dashboard: ${baseUrl}
|
|
5486
5702
|
`
|
|
5487
|
-
|
|
5703
|
+
);
|
|
5704
|
+
if (this.authToken) {
|
|
5705
|
+
const sessionUrl = this.createSessionUrl();
|
|
5488
5706
|
process.stderr.write(
|
|
5489
|
-
`
|
|
5490
|
-
|
|
5707
|
+
` Quick open: ${sessionUrl}
|
|
5491
5708
|
`
|
|
5492
5709
|
);
|
|
5493
|
-
|
|
5710
|
+
const hint = this.authToken.slice(0, 4) + "..." + this.authToken.slice(-4);
|
|
5494
5711
|
process.stderr.write(
|
|
5495
|
-
`
|
|
5496
|
-
Sanctuary Principal Dashboard: ${baseUrl}
|
|
5712
|
+
` Auth token: ${hint}
|
|
5497
5713
|
|
|
5498
5714
|
`
|
|
5499
5715
|
);
|
|
5716
|
+
} else {
|
|
5717
|
+
process.stderr.write(`
|
|
5718
|
+
`);
|
|
5500
5719
|
}
|
|
5501
5720
|
resolve();
|
|
5502
5721
|
});
|
|
@@ -5600,10 +5819,47 @@ var DashboardApprovalChannel = class {
|
|
|
5600
5819
|
if (sessionId && this.validateSession(sessionId)) {
|
|
5601
5820
|
return true;
|
|
5602
5821
|
}
|
|
5822
|
+
const cookieSession = this.parseCookie(req, "sanctuary_session");
|
|
5823
|
+
if (cookieSession && this.validateSession(cookieSession)) {
|
|
5824
|
+
return true;
|
|
5825
|
+
}
|
|
5603
5826
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
5604
5827
|
res.end(JSON.stringify({ error: "Unauthorized \u2014 use Authorization: Bearer header or a valid session" }));
|
|
5605
5828
|
return false;
|
|
5606
5829
|
}
|
|
5830
|
+
/**
|
|
5831
|
+
* Check if a request is authenticated WITHOUT sending a response.
|
|
5832
|
+
* Used to decide between login page vs dashboard for GET /.
|
|
5833
|
+
*/
|
|
5834
|
+
isAuthenticated(req, url) {
|
|
5835
|
+
if (!this.authToken) return true;
|
|
5836
|
+
const authHeader = req.headers.authorization;
|
|
5837
|
+
if (authHeader) {
|
|
5838
|
+
const parts = authHeader.split(" ");
|
|
5839
|
+
if (parts.length === 2 && parts[0] === "Bearer" && parts[1] === this.authToken) {
|
|
5840
|
+
return true;
|
|
5841
|
+
}
|
|
5842
|
+
}
|
|
5843
|
+
const sessionId = url.searchParams.get("session");
|
|
5844
|
+
if (sessionId && this.validateSession(sessionId)) return true;
|
|
5845
|
+
const cookieSession = this.parseCookie(req, "sanctuary_session");
|
|
5846
|
+
if (cookieSession && this.validateSession(cookieSession)) return true;
|
|
5847
|
+
return false;
|
|
5848
|
+
}
|
|
5849
|
+
/**
|
|
5850
|
+
* Parse a specific cookie value from the request.
|
|
5851
|
+
*/
|
|
5852
|
+
parseCookie(req, name) {
|
|
5853
|
+
const header = req.headers.cookie;
|
|
5854
|
+
if (!header) return null;
|
|
5855
|
+
for (const part of header.split(";")) {
|
|
5856
|
+
const [key, ...rest] = part.split("=");
|
|
5857
|
+
if (key?.trim() === name) {
|
|
5858
|
+
return rest.join("=").trim();
|
|
5859
|
+
}
|
|
5860
|
+
}
|
|
5861
|
+
return null;
|
|
5862
|
+
}
|
|
5607
5863
|
// ── Session Management (SEC-012) ──────────────────────────────────
|
|
5608
5864
|
/**
|
|
5609
5865
|
* Create a short-lived session by exchanging the long-lived auth token
|
|
@@ -5624,7 +5880,7 @@ var DashboardApprovalChannel = class {
|
|
|
5624
5880
|
this.sessions.set(id, {
|
|
5625
5881
|
id,
|
|
5626
5882
|
created_at: now,
|
|
5627
|
-
expires_at: now +
|
|
5883
|
+
expires_at: now + this.sessionTTLMs
|
|
5628
5884
|
});
|
|
5629
5885
|
return id;
|
|
5630
5886
|
}
|
|
@@ -5723,13 +5979,26 @@ var DashboardApprovalChannel = class {
|
|
|
5723
5979
|
res.end();
|
|
5724
5980
|
return;
|
|
5725
5981
|
}
|
|
5726
|
-
if (
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
if (method === "POST" && url.pathname === "/auth/session") {
|
|
5982
|
+
if (method === "POST" && url.pathname === "/auth/session") {
|
|
5983
|
+
if (!this.checkRateLimit(req, res, "general")) return;
|
|
5984
|
+
try {
|
|
5730
5985
|
this.handleSessionExchange(req, res);
|
|
5986
|
+
} catch {
|
|
5987
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
5988
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
5989
|
+
}
|
|
5990
|
+
return;
|
|
5991
|
+
}
|
|
5992
|
+
if (method === "GET" && url.pathname === "/" && this.authToken) {
|
|
5993
|
+
if (!this.isAuthenticated(req, url)) {
|
|
5994
|
+
if (!this.checkRateLimit(req, res, "general")) return;
|
|
5995
|
+
this.serveLoginPage(res);
|
|
5731
5996
|
return;
|
|
5732
5997
|
}
|
|
5998
|
+
}
|
|
5999
|
+
if (!this.checkAuth(req, url, res)) return;
|
|
6000
|
+
if (!this.checkRateLimit(req, res, "general")) return;
|
|
6001
|
+
try {
|
|
5733
6002
|
if (method === "GET" && url.pathname === "/") {
|
|
5734
6003
|
this.serveDashboard(res);
|
|
5735
6004
|
} else if (method === "GET" && url.pathname === "/events") {
|
|
@@ -5786,12 +6055,23 @@ var DashboardApprovalChannel = class {
|
|
|
5786
6055
|
return;
|
|
5787
6056
|
}
|
|
5788
6057
|
const sessionId = this.createSession();
|
|
5789
|
-
|
|
6058
|
+
const ttlSeconds = Math.floor(this.sessionTTLMs / 1e3);
|
|
6059
|
+
res.writeHead(200, {
|
|
6060
|
+
"Content-Type": "application/json",
|
|
6061
|
+
"Set-Cookie": `sanctuary_session=${sessionId}; Path=/; SameSite=Strict; Max-Age=${ttlSeconds}`
|
|
6062
|
+
});
|
|
5790
6063
|
res.end(JSON.stringify({
|
|
5791
6064
|
session_id: sessionId,
|
|
5792
|
-
expires_in_seconds:
|
|
6065
|
+
expires_in_seconds: ttlSeconds
|
|
5793
6066
|
}));
|
|
5794
6067
|
}
|
|
6068
|
+
serveLoginPage(res) {
|
|
6069
|
+
res.writeHead(200, {
|
|
6070
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
6071
|
+
"Cache-Control": "no-cache, no-store"
|
|
6072
|
+
});
|
|
6073
|
+
res.end(this.loginHTML);
|
|
6074
|
+
}
|
|
5795
6075
|
serveDashboard(res) {
|
|
5796
6076
|
res.writeHead(200, {
|
|
5797
6077
|
"Content-Type": "text/html; charset=utf-8",
|
|
@@ -5967,6 +6247,22 @@ data: ${JSON.stringify(data)}
|
|
|
5967
6247
|
broadcastProtectionStatus(data) {
|
|
5968
6248
|
this.broadcastSSE("protection-status", data);
|
|
5969
6249
|
}
|
|
6250
|
+
/**
|
|
6251
|
+
* Create a pre-authenticated URL for the dashboard.
|
|
6252
|
+
* Used by the sanctuary_dashboard_open tool and at startup.
|
|
6253
|
+
*/
|
|
6254
|
+
createSessionUrl() {
|
|
6255
|
+
const sessionId = this.createSession();
|
|
6256
|
+
const protocol = this.useTLS ? "https" : "http";
|
|
6257
|
+
return `${protocol}://${this.config.host}:${this.config.port}/?session=${sessionId}`;
|
|
6258
|
+
}
|
|
6259
|
+
/**
|
|
6260
|
+
* Get the base URL for the dashboard.
|
|
6261
|
+
*/
|
|
6262
|
+
getBaseUrl() {
|
|
6263
|
+
const protocol = this.useTLS ? "https" : "http";
|
|
6264
|
+
return `${protocol}://${this.config.host}:${this.config.port}`;
|
|
6265
|
+
}
|
|
5970
6266
|
/** Get the number of pending requests */
|
|
5971
6267
|
get pendingCount() {
|
|
5972
6268
|
return this.pending.size;
|
|
@@ -11781,6 +12077,32 @@ async function createSanctuaryServer(options) {
|
|
|
11781
12077
|
} : void 0;
|
|
11782
12078
|
const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog, injectionDetector, onInjectionAlert);
|
|
11783
12079
|
const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
|
|
12080
|
+
const dashboardTools = [];
|
|
12081
|
+
if (dashboard) {
|
|
12082
|
+
dashboardTools.push({
|
|
12083
|
+
name: "sanctuary/dashboard_open",
|
|
12084
|
+
description: "Generate a one-click URL to open the Principal Dashboard in a browser. Returns a pre-authenticated link \u2014 no manual token entry needed.",
|
|
12085
|
+
inputSchema: {
|
|
12086
|
+
type: "object",
|
|
12087
|
+
properties: {}
|
|
12088
|
+
},
|
|
12089
|
+
handler: async () => {
|
|
12090
|
+
const url = dashboard.createSessionUrl();
|
|
12091
|
+
return {
|
|
12092
|
+
content: [
|
|
12093
|
+
{
|
|
12094
|
+
type: "text",
|
|
12095
|
+
text: JSON.stringify({
|
|
12096
|
+
dashboard_url: url,
|
|
12097
|
+
base_url: dashboard.getBaseUrl(),
|
|
12098
|
+
note: "Click the dashboard_url to open the Principal Dashboard. The session is pre-authenticated."
|
|
12099
|
+
}, null, 2)
|
|
12100
|
+
}
|
|
12101
|
+
]
|
|
12102
|
+
};
|
|
12103
|
+
}
|
|
12104
|
+
});
|
|
12105
|
+
}
|
|
11784
12106
|
let allTools = [
|
|
11785
12107
|
...l1Tools,
|
|
11786
12108
|
...l2Tools,
|
|
@@ -11794,6 +12116,7 @@ async function createSanctuaryServer(options) {
|
|
|
11794
12116
|
...auditTools,
|
|
11795
12117
|
...contextGateTools,
|
|
11796
12118
|
...hardeningTools,
|
|
12119
|
+
...dashboardTools,
|
|
11797
12120
|
manifestTool
|
|
11798
12121
|
];
|
|
11799
12122
|
allTools = allTools.map((tool) => ({
|