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