@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/index.cjs
CHANGED
|
@@ -257,7 +257,18 @@ function defaultConfig() {
|
|
|
257
257
|
};
|
|
258
258
|
}
|
|
259
259
|
async function loadConfig(configPath) {
|
|
260
|
-
|
|
260
|
+
let config = defaultConfig();
|
|
261
|
+
const storagePath = process.env.SANCTUARY_STORAGE_PATH ?? config.storage_path;
|
|
262
|
+
const path$1 = configPath ?? path.join(storagePath, "sanctuary.json");
|
|
263
|
+
try {
|
|
264
|
+
const raw = await promises.readFile(path$1, "utf-8");
|
|
265
|
+
const fileConfig = JSON.parse(raw);
|
|
266
|
+
config = deepMerge(config, fileConfig);
|
|
267
|
+
} catch (err) {
|
|
268
|
+
if (err instanceof Error && err.message.includes("unimplemented features")) {
|
|
269
|
+
throw err;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
261
272
|
if (process.env.SANCTUARY_STORAGE_PATH) {
|
|
262
273
|
config.storage_path = process.env.SANCTUARY_STORAGE_PATH;
|
|
263
274
|
}
|
|
@@ -270,6 +281,9 @@ async function loadConfig(configPath) {
|
|
|
270
281
|
if (process.env.SANCTUARY_DASHBOARD_ENABLED === "true") {
|
|
271
282
|
config.dashboard.enabled = true;
|
|
272
283
|
}
|
|
284
|
+
if (process.env.SANCTUARY_DASHBOARD_ENABLED === "false") {
|
|
285
|
+
config.dashboard.enabled = false;
|
|
286
|
+
}
|
|
273
287
|
if (process.env.SANCTUARY_DASHBOARD_PORT) {
|
|
274
288
|
config.dashboard.port = parseInt(process.env.SANCTUARY_DASHBOARD_PORT, 10);
|
|
275
289
|
}
|
|
@@ -288,6 +302,9 @@ async function loadConfig(configPath) {
|
|
|
288
302
|
if (process.env.SANCTUARY_WEBHOOK_ENABLED === "true") {
|
|
289
303
|
config.webhook.enabled = true;
|
|
290
304
|
}
|
|
305
|
+
if (process.env.SANCTUARY_WEBHOOK_ENABLED === "false") {
|
|
306
|
+
config.webhook.enabled = false;
|
|
307
|
+
}
|
|
291
308
|
if (process.env.SANCTUARY_WEBHOOK_URL) {
|
|
292
309
|
config.webhook.url = process.env.SANCTUARY_WEBHOOK_URL;
|
|
293
310
|
}
|
|
@@ -300,19 +317,9 @@ async function loadConfig(configPath) {
|
|
|
300
317
|
if (process.env.SANCTUARY_WEBHOOK_CALLBACK_HOST) {
|
|
301
318
|
config.webhook.callback_host = process.env.SANCTUARY_WEBHOOK_CALLBACK_HOST;
|
|
302
319
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const fileConfig = JSON.parse(raw);
|
|
307
|
-
const merged = deepMerge(config, fileConfig);
|
|
308
|
-
validateConfig(merged);
|
|
309
|
-
return merged;
|
|
310
|
-
} catch (err) {
|
|
311
|
-
if (err instanceof Error && err.message.includes("unimplemented features")) {
|
|
312
|
-
throw err;
|
|
313
|
-
}
|
|
314
|
-
return config;
|
|
315
|
-
}
|
|
320
|
+
config.version = PKG_VERSION;
|
|
321
|
+
validateConfig(config);
|
|
322
|
+
return config;
|
|
316
323
|
}
|
|
317
324
|
async function saveConfig(config, configPath) {
|
|
318
325
|
const path$1 = path.join(config.storage_path, "sanctuary.json");
|
|
@@ -4051,6 +4058,181 @@ var AutoApproveChannel = class {
|
|
|
4051
4058
|
};
|
|
4052
4059
|
|
|
4053
4060
|
// src/principal-policy/dashboard-html.ts
|
|
4061
|
+
function generateLoginHTML(options) {
|
|
4062
|
+
return `<!DOCTYPE html>
|
|
4063
|
+
<html lang="en">
|
|
4064
|
+
<head>
|
|
4065
|
+
<meta charset="utf-8">
|
|
4066
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
4067
|
+
<title>Sanctuary \u2014 Login</title>
|
|
4068
|
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
|
|
4069
|
+
<style>
|
|
4070
|
+
:root {
|
|
4071
|
+
--bg: #0d1117;
|
|
4072
|
+
--surface: #161b22;
|
|
4073
|
+
--border: #30363d;
|
|
4074
|
+
--text-primary: #e6edf3;
|
|
4075
|
+
--text-secondary: #8b949e;
|
|
4076
|
+
--green: #3fb950;
|
|
4077
|
+
--red: #f85149;
|
|
4078
|
+
--blue: #58a6ff;
|
|
4079
|
+
--mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
|
4080
|
+
--sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
4081
|
+
--radius: 6px;
|
|
4082
|
+
}
|
|
4083
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
4084
|
+
html, body { width: 100%; height: 100%; }
|
|
4085
|
+
body {
|
|
4086
|
+
font-family: var(--sans);
|
|
4087
|
+
background: var(--bg);
|
|
4088
|
+
color: var(--text-primary);
|
|
4089
|
+
display: flex;
|
|
4090
|
+
align-items: center;
|
|
4091
|
+
justify-content: center;
|
|
4092
|
+
}
|
|
4093
|
+
.login-container {
|
|
4094
|
+
width: 100%;
|
|
4095
|
+
max-width: 400px;
|
|
4096
|
+
padding: 40px 32px;
|
|
4097
|
+
background: var(--surface);
|
|
4098
|
+
border: 1px solid var(--border);
|
|
4099
|
+
border-radius: 12px;
|
|
4100
|
+
}
|
|
4101
|
+
.login-logo {
|
|
4102
|
+
text-align: center;
|
|
4103
|
+
font-size: 20px;
|
|
4104
|
+
font-weight: 700;
|
|
4105
|
+
letter-spacing: -0.5px;
|
|
4106
|
+
margin-bottom: 8px;
|
|
4107
|
+
}
|
|
4108
|
+
.login-logo span { color: var(--blue); }
|
|
4109
|
+
.login-version {
|
|
4110
|
+
text-align: center;
|
|
4111
|
+
font-size: 11px;
|
|
4112
|
+
color: var(--text-secondary);
|
|
4113
|
+
font-family: var(--mono);
|
|
4114
|
+
margin-bottom: 32px;
|
|
4115
|
+
}
|
|
4116
|
+
.login-label {
|
|
4117
|
+
display: block;
|
|
4118
|
+
font-size: 13px;
|
|
4119
|
+
font-weight: 600;
|
|
4120
|
+
color: var(--text-secondary);
|
|
4121
|
+
margin-bottom: 8px;
|
|
4122
|
+
}
|
|
4123
|
+
.login-input {
|
|
4124
|
+
width: 100%;
|
|
4125
|
+
padding: 10px 14px;
|
|
4126
|
+
background: var(--bg);
|
|
4127
|
+
border: 1px solid var(--border);
|
|
4128
|
+
border-radius: var(--radius);
|
|
4129
|
+
color: var(--text-primary);
|
|
4130
|
+
font-family: var(--mono);
|
|
4131
|
+
font-size: 14px;
|
|
4132
|
+
outline: none;
|
|
4133
|
+
transition: border-color 0.15s;
|
|
4134
|
+
}
|
|
4135
|
+
.login-input:focus { border-color: var(--blue); }
|
|
4136
|
+
.login-input::placeholder { color: var(--text-secondary); opacity: 0.5; }
|
|
4137
|
+
.login-btn {
|
|
4138
|
+
width: 100%;
|
|
4139
|
+
margin-top: 20px;
|
|
4140
|
+
padding: 10px;
|
|
4141
|
+
background: var(--blue);
|
|
4142
|
+
color: var(--bg);
|
|
4143
|
+
border: none;
|
|
4144
|
+
border-radius: var(--radius);
|
|
4145
|
+
font-size: 14px;
|
|
4146
|
+
font-weight: 600;
|
|
4147
|
+
cursor: pointer;
|
|
4148
|
+
transition: opacity 0.15s;
|
|
4149
|
+
font-family: var(--sans);
|
|
4150
|
+
}
|
|
4151
|
+
.login-btn:hover { opacity: 0.9; }
|
|
4152
|
+
.login-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
4153
|
+
.login-error {
|
|
4154
|
+
margin-top: 16px;
|
|
4155
|
+
padding: 10px 14px;
|
|
4156
|
+
background: rgba(248, 81, 73, 0.1);
|
|
4157
|
+
border: 1px solid var(--red);
|
|
4158
|
+
border-radius: var(--radius);
|
|
4159
|
+
font-size: 12px;
|
|
4160
|
+
color: var(--red);
|
|
4161
|
+
display: none;
|
|
4162
|
+
}
|
|
4163
|
+
.login-hint {
|
|
4164
|
+
margin-top: 24px;
|
|
4165
|
+
padding-top: 16px;
|
|
4166
|
+
border-top: 1px solid var(--border);
|
|
4167
|
+
font-size: 11px;
|
|
4168
|
+
color: var(--text-secondary);
|
|
4169
|
+
line-height: 1.5;
|
|
4170
|
+
}
|
|
4171
|
+
.login-hint code {
|
|
4172
|
+
font-family: var(--mono);
|
|
4173
|
+
background: var(--bg);
|
|
4174
|
+
padding: 1px 4px;
|
|
4175
|
+
border-radius: 3px;
|
|
4176
|
+
font-size: 10px;
|
|
4177
|
+
}
|
|
4178
|
+
</style>
|
|
4179
|
+
</head>
|
|
4180
|
+
<body>
|
|
4181
|
+
<div class="login-container">
|
|
4182
|
+
<div class="login-logo"><span>◆</span> SANCTUARY</div>
|
|
4183
|
+
<div class="login-version">Principal Dashboard v${options.serverVersion}</div>
|
|
4184
|
+
<form id="loginForm" onsubmit="return handleLogin(event)">
|
|
4185
|
+
<label class="login-label" for="tokenInput">Dashboard Auth Token</label>
|
|
4186
|
+
<input class="login-input" type="password" id="tokenInput"
|
|
4187
|
+
placeholder="Enter your auth token" autocomplete="off" autofocus required>
|
|
4188
|
+
<button class="login-btn" type="submit" id="loginBtn">Open Dashboard</button>
|
|
4189
|
+
</form>
|
|
4190
|
+
<div class="login-error" id="loginError"></div>
|
|
4191
|
+
<div class="login-hint">
|
|
4192
|
+
Your token is set via <code>SANCTUARY_DASHBOARD_AUTH_TOKEN</code> environment variable,
|
|
4193
|
+
or check your server's startup output.
|
|
4194
|
+
</div>
|
|
4195
|
+
</div>
|
|
4196
|
+
<script>
|
|
4197
|
+
async function handleLogin(e) {
|
|
4198
|
+
e.preventDefault();
|
|
4199
|
+
var btn = document.getElementById('loginBtn');
|
|
4200
|
+
var errEl = document.getElementById('loginError');
|
|
4201
|
+
var token = document.getElementById('tokenInput').value.trim();
|
|
4202
|
+
if (!token) return false;
|
|
4203
|
+
btn.disabled = true;
|
|
4204
|
+
btn.textContent = 'Authenticating...';
|
|
4205
|
+
errEl.style.display = 'none';
|
|
4206
|
+
try {
|
|
4207
|
+
var resp = await fetch('/auth/session', {
|
|
4208
|
+
method: 'POST',
|
|
4209
|
+
headers: { 'Authorization': 'Bearer ' + token }
|
|
4210
|
+
});
|
|
4211
|
+
if (!resp.ok) {
|
|
4212
|
+
var data = await resp.json().catch(function() { return {}; });
|
|
4213
|
+
throw new Error(data.error || 'Authentication failed');
|
|
4214
|
+
}
|
|
4215
|
+
var result = await resp.json();
|
|
4216
|
+
// Store token in sessionStorage for auto-renewal inside the dashboard
|
|
4217
|
+
try { sessionStorage.setItem('sanctuary_token', token); } catch(_) {}
|
|
4218
|
+
// Set session cookie
|
|
4219
|
+
var maxAge = result.expires_in_seconds || 300;
|
|
4220
|
+
document.cookie = 'sanctuary_session=' + result.session_id +
|
|
4221
|
+
'; path=/; SameSite=Strict; max-age=' + maxAge;
|
|
4222
|
+
// Reload to enter the dashboard
|
|
4223
|
+
window.location.reload();
|
|
4224
|
+
} catch (err) {
|
|
4225
|
+
errEl.textContent = err.message || 'Authentication failed. Check your token.';
|
|
4226
|
+
errEl.style.display = 'block';
|
|
4227
|
+
btn.disabled = false;
|
|
4228
|
+
btn.textContent = 'Open Dashboard';
|
|
4229
|
+
}
|
|
4230
|
+
return false;
|
|
4231
|
+
}
|
|
4232
|
+
</script>
|
|
4233
|
+
</body>
|
|
4234
|
+
</html>`;
|
|
4235
|
+
}
|
|
4054
4236
|
function generateDashboardHTML(options) {
|
|
4055
4237
|
return `<!DOCTYPE html>
|
|
4056
4238
|
<html lang="en">
|
|
@@ -4920,7 +5102,9 @@ function generateDashboardHTML(options) {
|
|
|
4920
5102
|
// \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
|
|
4921
5103
|
|
|
4922
5104
|
const TIMEOUT_SECONDS = ${options.timeoutSeconds};
|
|
4923
|
-
|
|
5105
|
+
// AUTH_TOKEN: embedded token (for direct session access) or from sessionStorage (login page flow)
|
|
5106
|
+
const EMBEDDED_TOKEN = ${options.authToken ? JSON.stringify(options.authToken) : "null"};
|
|
5107
|
+
const AUTH_TOKEN = EMBEDDED_TOKEN || (function() { try { return sessionStorage.getItem('sanctuary_token'); } catch(_) { return null; } })();
|
|
4924
5108
|
const MAX_ACTIVITY_ITEMS = 100;
|
|
4925
5109
|
const MAX_THREAT_ITEMS = 20;
|
|
4926
5110
|
|
|
@@ -4935,6 +5119,7 @@ function generateDashboardHTML(options) {
|
|
|
4935
5119
|
const activityItems = [];
|
|
4936
5120
|
const threatItems = [];
|
|
4937
5121
|
let sovereigntyScore = 85;
|
|
5122
|
+
let sessionRenewalTimer = null;
|
|
4938
5123
|
|
|
4939
5124
|
// \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
|
|
4940
5125
|
|
|
@@ -4950,6 +5135,11 @@ function generateDashboardHTML(options) {
|
|
|
4950
5135
|
return url + sep + 'session=' + SESSION_ID;
|
|
4951
5136
|
}
|
|
4952
5137
|
|
|
5138
|
+
function setCookie(sessionId, maxAge) {
|
|
5139
|
+
document.cookie = 'sanctuary_session=' + sessionId +
|
|
5140
|
+
'; path=/; SameSite=Strict; max-age=' + maxAge;
|
|
5141
|
+
}
|
|
5142
|
+
|
|
4953
5143
|
async function exchangeSession() {
|
|
4954
5144
|
if (!AUTH_TOKEN) return;
|
|
4955
5145
|
try {
|
|
@@ -4957,14 +5147,35 @@ function generateDashboardHTML(options) {
|
|
|
4957
5147
|
if (resp.ok) {
|
|
4958
5148
|
const data = await resp.json();
|
|
4959
5149
|
SESSION_ID = data.session_id;
|
|
4960
|
-
|
|
4961
|
-
|
|
5150
|
+
var ttl = data.expires_in_seconds || 300;
|
|
5151
|
+
// Update cookie with new session
|
|
5152
|
+
setCookie(SESSION_ID, ttl);
|
|
5153
|
+
// Schedule renewal at 80% of TTL
|
|
5154
|
+
if (sessionRenewalTimer) clearTimeout(sessionRenewalTimer);
|
|
5155
|
+
sessionRenewalTimer = setTimeout(function() {
|
|
5156
|
+
exchangeSession().then(function() { reconnectSSE(); });
|
|
5157
|
+
}, ttl * 800);
|
|
5158
|
+
} else if (resp.status === 401) {
|
|
5159
|
+
// Token invalid or expired \u2014 show non-destructive re-login overlay
|
|
5160
|
+
showSessionExpired();
|
|
4962
5161
|
}
|
|
4963
5162
|
} catch (e) {
|
|
4964
|
-
//
|
|
5163
|
+
// Network error \u2014 retry in 30s
|
|
5164
|
+
if (sessionRenewalTimer) clearTimeout(sessionRenewalTimer);
|
|
5165
|
+
sessionRenewalTimer = setTimeout(function() {
|
|
5166
|
+
exchangeSession().then(function() { reconnectSSE(); });
|
|
5167
|
+
}, 30000);
|
|
4965
5168
|
}
|
|
4966
5169
|
}
|
|
4967
5170
|
|
|
5171
|
+
function showSessionExpired() {
|
|
5172
|
+
// Clear stored token
|
|
5173
|
+
try { sessionStorage.removeItem('sanctuary_token'); } catch(_) {}
|
|
5174
|
+
// Redirect to login page
|
|
5175
|
+
document.cookie = 'sanctuary_session=; path=/; max-age=0';
|
|
5176
|
+
window.location.reload();
|
|
5177
|
+
}
|
|
5178
|
+
|
|
4968
5179
|
// \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
|
|
4969
5180
|
|
|
4970
5181
|
function esc(s) {
|
|
@@ -5437,7 +5648,8 @@ function generateDashboardHTML(options) {
|
|
|
5437
5648
|
}
|
|
5438
5649
|
|
|
5439
5650
|
// src/principal-policy/dashboard.ts
|
|
5440
|
-
var
|
|
5651
|
+
var SESSION_TTL_REMOTE_MS = 5 * 60 * 1e3;
|
|
5652
|
+
var SESSION_TTL_LOCAL_MS = 24 * 60 * 60 * 1e3;
|
|
5441
5653
|
var MAX_SESSIONS = 1e3;
|
|
5442
5654
|
var RATE_LIMIT_WINDOW_MS = 6e4;
|
|
5443
5655
|
var RATE_LIMIT_GENERAL = 120;
|
|
@@ -5452,8 +5664,11 @@ var DashboardApprovalChannel = class {
|
|
|
5452
5664
|
baseline = null;
|
|
5453
5665
|
auditLog = null;
|
|
5454
5666
|
dashboardHTML;
|
|
5667
|
+
loginHTML;
|
|
5455
5668
|
authToken;
|
|
5456
5669
|
useTLS;
|
|
5670
|
+
/** Session TTL: longer for localhost, shorter for remote */
|
|
5671
|
+
sessionTTLMs;
|
|
5457
5672
|
/** SEC-012: Short-lived session store. Sessions replace URL query tokens. */
|
|
5458
5673
|
sessions = /* @__PURE__ */ new Map();
|
|
5459
5674
|
sessionCleanupTimer = null;
|
|
@@ -5463,11 +5678,14 @@ var DashboardApprovalChannel = class {
|
|
|
5463
5678
|
this.config = config;
|
|
5464
5679
|
this.authToken = config.auth_token;
|
|
5465
5680
|
this.useTLS = !!(config.tls?.cert_path && config.tls?.key_path);
|
|
5681
|
+
const isLocalhost = config.host === "127.0.0.1" || config.host === "localhost" || config.host === "::1";
|
|
5682
|
+
this.sessionTTLMs = isLocalhost ? SESSION_TTL_LOCAL_MS : SESSION_TTL_REMOTE_MS;
|
|
5466
5683
|
this.dashboardHTML = generateDashboardHTML({
|
|
5467
5684
|
timeoutSeconds: config.timeout_seconds,
|
|
5468
5685
|
serverVersion: SANCTUARY_VERSION,
|
|
5469
5686
|
authToken: this.authToken
|
|
5470
5687
|
});
|
|
5688
|
+
this.loginHTML = generateLoginHTML({ serverVersion: SANCTUARY_VERSION });
|
|
5471
5689
|
this.sessionCleanupTimer = setInterval(() => this.cleanupSessions(), 6e4);
|
|
5472
5690
|
}
|
|
5473
5691
|
/**
|
|
@@ -5497,25 +5715,26 @@ var DashboardApprovalChannel = class {
|
|
|
5497
5715
|
const protocol = this.useTLS ? "https" : "http";
|
|
5498
5716
|
const baseUrl = `${protocol}://${this.config.host}:${this.config.port}`;
|
|
5499
5717
|
this.httpServer.listen(this.config.port, this.config.host, () => {
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
process.stderr.write(
|
|
5503
|
-
`
|
|
5718
|
+
process.stderr.write(
|
|
5719
|
+
`
|
|
5504
5720
|
Sanctuary Principal Dashboard: ${baseUrl}
|
|
5505
5721
|
`
|
|
5506
|
-
|
|
5722
|
+
);
|
|
5723
|
+
if (this.authToken) {
|
|
5724
|
+
const sessionUrl = this.createSessionUrl();
|
|
5507
5725
|
process.stderr.write(
|
|
5508
|
-
`
|
|
5509
|
-
|
|
5726
|
+
` Quick open: ${sessionUrl}
|
|
5510
5727
|
`
|
|
5511
5728
|
);
|
|
5512
|
-
|
|
5729
|
+
const hint = this.authToken.slice(0, 4) + "..." + this.authToken.slice(-4);
|
|
5513
5730
|
process.stderr.write(
|
|
5514
|
-
`
|
|
5515
|
-
Sanctuary Principal Dashboard: ${baseUrl}
|
|
5731
|
+
` Auth token: ${hint}
|
|
5516
5732
|
|
|
5517
5733
|
`
|
|
5518
5734
|
);
|
|
5735
|
+
} else {
|
|
5736
|
+
process.stderr.write(`
|
|
5737
|
+
`);
|
|
5519
5738
|
}
|
|
5520
5739
|
resolve();
|
|
5521
5740
|
});
|
|
@@ -5619,10 +5838,47 @@ var DashboardApprovalChannel = class {
|
|
|
5619
5838
|
if (sessionId && this.validateSession(sessionId)) {
|
|
5620
5839
|
return true;
|
|
5621
5840
|
}
|
|
5841
|
+
const cookieSession = this.parseCookie(req, "sanctuary_session");
|
|
5842
|
+
if (cookieSession && this.validateSession(cookieSession)) {
|
|
5843
|
+
return true;
|
|
5844
|
+
}
|
|
5622
5845
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
5623
5846
|
res.end(JSON.stringify({ error: "Unauthorized \u2014 use Authorization: Bearer header or a valid session" }));
|
|
5624
5847
|
return false;
|
|
5625
5848
|
}
|
|
5849
|
+
/**
|
|
5850
|
+
* Check if a request is authenticated WITHOUT sending a response.
|
|
5851
|
+
* Used to decide between login page vs dashboard for GET /.
|
|
5852
|
+
*/
|
|
5853
|
+
isAuthenticated(req, url) {
|
|
5854
|
+
if (!this.authToken) return true;
|
|
5855
|
+
const authHeader = req.headers.authorization;
|
|
5856
|
+
if (authHeader) {
|
|
5857
|
+
const parts = authHeader.split(" ");
|
|
5858
|
+
if (parts.length === 2 && parts[0] === "Bearer" && parts[1] === this.authToken) {
|
|
5859
|
+
return true;
|
|
5860
|
+
}
|
|
5861
|
+
}
|
|
5862
|
+
const sessionId = url.searchParams.get("session");
|
|
5863
|
+
if (sessionId && this.validateSession(sessionId)) return true;
|
|
5864
|
+
const cookieSession = this.parseCookie(req, "sanctuary_session");
|
|
5865
|
+
if (cookieSession && this.validateSession(cookieSession)) return true;
|
|
5866
|
+
return false;
|
|
5867
|
+
}
|
|
5868
|
+
/**
|
|
5869
|
+
* Parse a specific cookie value from the request.
|
|
5870
|
+
*/
|
|
5871
|
+
parseCookie(req, name) {
|
|
5872
|
+
const header = req.headers.cookie;
|
|
5873
|
+
if (!header) return null;
|
|
5874
|
+
for (const part of header.split(";")) {
|
|
5875
|
+
const [key, ...rest] = part.split("=");
|
|
5876
|
+
if (key?.trim() === name) {
|
|
5877
|
+
return rest.join("=").trim();
|
|
5878
|
+
}
|
|
5879
|
+
}
|
|
5880
|
+
return null;
|
|
5881
|
+
}
|
|
5626
5882
|
// ── Session Management (SEC-012) ──────────────────────────────────
|
|
5627
5883
|
/**
|
|
5628
5884
|
* Create a short-lived session by exchanging the long-lived auth token
|
|
@@ -5643,7 +5899,7 @@ var DashboardApprovalChannel = class {
|
|
|
5643
5899
|
this.sessions.set(id, {
|
|
5644
5900
|
id,
|
|
5645
5901
|
created_at: now,
|
|
5646
|
-
expires_at: now +
|
|
5902
|
+
expires_at: now + this.sessionTTLMs
|
|
5647
5903
|
});
|
|
5648
5904
|
return id;
|
|
5649
5905
|
}
|
|
@@ -5742,13 +5998,26 @@ var DashboardApprovalChannel = class {
|
|
|
5742
5998
|
res.end();
|
|
5743
5999
|
return;
|
|
5744
6000
|
}
|
|
5745
|
-
if (
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
-
if (method === "POST" && url.pathname === "/auth/session") {
|
|
6001
|
+
if (method === "POST" && url.pathname === "/auth/session") {
|
|
6002
|
+
if (!this.checkRateLimit(req, res, "general")) return;
|
|
6003
|
+
try {
|
|
5749
6004
|
this.handleSessionExchange(req, res);
|
|
6005
|
+
} catch {
|
|
6006
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
6007
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
6008
|
+
}
|
|
6009
|
+
return;
|
|
6010
|
+
}
|
|
6011
|
+
if (method === "GET" && url.pathname === "/" && this.authToken) {
|
|
6012
|
+
if (!this.isAuthenticated(req, url)) {
|
|
6013
|
+
if (!this.checkRateLimit(req, res, "general")) return;
|
|
6014
|
+
this.serveLoginPage(res);
|
|
5750
6015
|
return;
|
|
5751
6016
|
}
|
|
6017
|
+
}
|
|
6018
|
+
if (!this.checkAuth(req, url, res)) return;
|
|
6019
|
+
if (!this.checkRateLimit(req, res, "general")) return;
|
|
6020
|
+
try {
|
|
5752
6021
|
if (method === "GET" && url.pathname === "/") {
|
|
5753
6022
|
this.serveDashboard(res);
|
|
5754
6023
|
} else if (method === "GET" && url.pathname === "/events") {
|
|
@@ -5805,12 +6074,23 @@ var DashboardApprovalChannel = class {
|
|
|
5805
6074
|
return;
|
|
5806
6075
|
}
|
|
5807
6076
|
const sessionId = this.createSession();
|
|
5808
|
-
|
|
6077
|
+
const ttlSeconds = Math.floor(this.sessionTTLMs / 1e3);
|
|
6078
|
+
res.writeHead(200, {
|
|
6079
|
+
"Content-Type": "application/json",
|
|
6080
|
+
"Set-Cookie": `sanctuary_session=${sessionId}; Path=/; SameSite=Strict; Max-Age=${ttlSeconds}`
|
|
6081
|
+
});
|
|
5809
6082
|
res.end(JSON.stringify({
|
|
5810
6083
|
session_id: sessionId,
|
|
5811
|
-
expires_in_seconds:
|
|
6084
|
+
expires_in_seconds: ttlSeconds
|
|
5812
6085
|
}));
|
|
5813
6086
|
}
|
|
6087
|
+
serveLoginPage(res) {
|
|
6088
|
+
res.writeHead(200, {
|
|
6089
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
6090
|
+
"Cache-Control": "no-cache, no-store"
|
|
6091
|
+
});
|
|
6092
|
+
res.end(this.loginHTML);
|
|
6093
|
+
}
|
|
5814
6094
|
serveDashboard(res) {
|
|
5815
6095
|
res.writeHead(200, {
|
|
5816
6096
|
"Content-Type": "text/html; charset=utf-8",
|
|
@@ -5986,6 +6266,22 @@ data: ${JSON.stringify(data)}
|
|
|
5986
6266
|
broadcastProtectionStatus(data) {
|
|
5987
6267
|
this.broadcastSSE("protection-status", data);
|
|
5988
6268
|
}
|
|
6269
|
+
/**
|
|
6270
|
+
* Create a pre-authenticated URL for the dashboard.
|
|
6271
|
+
* Used by the sanctuary_dashboard_open tool and at startup.
|
|
6272
|
+
*/
|
|
6273
|
+
createSessionUrl() {
|
|
6274
|
+
const sessionId = this.createSession();
|
|
6275
|
+
const protocol = this.useTLS ? "https" : "http";
|
|
6276
|
+
return `${protocol}://${this.config.host}:${this.config.port}/?session=${sessionId}`;
|
|
6277
|
+
}
|
|
6278
|
+
/**
|
|
6279
|
+
* Get the base URL for the dashboard.
|
|
6280
|
+
*/
|
|
6281
|
+
getBaseUrl() {
|
|
6282
|
+
const protocol = this.useTLS ? "https" : "http";
|
|
6283
|
+
return `${protocol}://${this.config.host}:${this.config.port}`;
|
|
6284
|
+
}
|
|
5989
6285
|
/** Get the number of pending requests */
|
|
5990
6286
|
get pendingCount() {
|
|
5991
6287
|
return this.pending.size;
|
|
@@ -11855,6 +12151,32 @@ async function createSanctuaryServer(options) {
|
|
|
11855
12151
|
} : void 0;
|
|
11856
12152
|
const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog, injectionDetector, onInjectionAlert);
|
|
11857
12153
|
const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
|
|
12154
|
+
const dashboardTools = [];
|
|
12155
|
+
if (dashboard) {
|
|
12156
|
+
dashboardTools.push({
|
|
12157
|
+
name: "sanctuary/dashboard_open",
|
|
12158
|
+
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.",
|
|
12159
|
+
inputSchema: {
|
|
12160
|
+
type: "object",
|
|
12161
|
+
properties: {}
|
|
12162
|
+
},
|
|
12163
|
+
handler: async () => {
|
|
12164
|
+
const url = dashboard.createSessionUrl();
|
|
12165
|
+
return {
|
|
12166
|
+
content: [
|
|
12167
|
+
{
|
|
12168
|
+
type: "text",
|
|
12169
|
+
text: JSON.stringify({
|
|
12170
|
+
dashboard_url: url,
|
|
12171
|
+
base_url: dashboard.getBaseUrl(),
|
|
12172
|
+
note: "Click the dashboard_url to open the Principal Dashboard. The session is pre-authenticated."
|
|
12173
|
+
}, null, 2)
|
|
12174
|
+
}
|
|
12175
|
+
]
|
|
12176
|
+
};
|
|
12177
|
+
}
|
|
12178
|
+
});
|
|
12179
|
+
}
|
|
11858
12180
|
let allTools = [
|
|
11859
12181
|
...l1Tools,
|
|
11860
12182
|
...l2Tools,
|
|
@@ -11868,6 +12190,7 @@ async function createSanctuaryServer(options) {
|
|
|
11868
12190
|
...auditTools,
|
|
11869
12191
|
...contextGateTools,
|
|
11870
12192
|
...hardeningTools,
|
|
12193
|
+
...dashboardTools,
|
|
11871
12194
|
manifestTool
|
|
11872
12195
|
];
|
|
11873
12196
|
allTools = allTools.map((tool) => ({
|