@sanctuary-framework/mcp-server 0.5.1 → 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 +338 -22
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +338 -22
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +338 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +338 -22
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -1996,8 +1996,11 @@ declare class DashboardApprovalChannel implements ApprovalChannel {
|
|
|
1996
1996
|
private baseline;
|
|
1997
1997
|
private auditLog;
|
|
1998
1998
|
private dashboardHTML;
|
|
1999
|
+
private loginHTML;
|
|
1999
2000
|
private authToken;
|
|
2000
2001
|
private useTLS;
|
|
2002
|
+
/** Session TTL: longer for localhost, shorter for remote */
|
|
2003
|
+
private sessionTTLMs;
|
|
2001
2004
|
/** SEC-012: Short-lived session store. Sessions replace URL query tokens. */
|
|
2002
2005
|
private sessions;
|
|
2003
2006
|
private sessionCleanupTimer;
|
|
@@ -2037,6 +2040,15 @@ declare class DashboardApprovalChannel implements ApprovalChannel {
|
|
|
2037
2040
|
* Returns true if auth passes, false if blocked (response already sent).
|
|
2038
2041
|
*/
|
|
2039
2042
|
private checkAuth;
|
|
2043
|
+
/**
|
|
2044
|
+
* Check if a request is authenticated WITHOUT sending a response.
|
|
2045
|
+
* Used to decide between login page vs dashboard for GET /.
|
|
2046
|
+
*/
|
|
2047
|
+
private isAuthenticated;
|
|
2048
|
+
/**
|
|
2049
|
+
* Parse a specific cookie value from the request.
|
|
2050
|
+
*/
|
|
2051
|
+
private parseCookie;
|
|
2040
2052
|
/**
|
|
2041
2053
|
* Create a short-lived session by exchanging the long-lived auth token
|
|
2042
2054
|
* (provided in the Authorization header) for a session ID.
|
|
@@ -2074,6 +2086,7 @@ declare class DashboardApprovalChannel implements ApprovalChannel {
|
|
|
2074
2086
|
* normal checkAuth flow.
|
|
2075
2087
|
*/
|
|
2076
2088
|
private handleSessionExchange;
|
|
2089
|
+
private serveLoginPage;
|
|
2077
2090
|
private serveDashboard;
|
|
2078
2091
|
private handleSSE;
|
|
2079
2092
|
private handleStatus;
|
|
@@ -2120,6 +2133,15 @@ declare class DashboardApprovalChannel implements ApprovalChannel {
|
|
|
2120
2133
|
* Broadcast current protection status to connected dashboards.
|
|
2121
2134
|
*/
|
|
2122
2135
|
broadcastProtectionStatus(data: Record<string, unknown>): void;
|
|
2136
|
+
/**
|
|
2137
|
+
* Create a pre-authenticated URL for the dashboard.
|
|
2138
|
+
* Used by the sanctuary_dashboard_open tool and at startup.
|
|
2139
|
+
*/
|
|
2140
|
+
createSessionUrl(): string;
|
|
2141
|
+
/**
|
|
2142
|
+
* Get the base URL for the dashboard.
|
|
2143
|
+
*/
|
|
2144
|
+
getBaseUrl(): string;
|
|
2123
2145
|
/** Get the number of pending requests */
|
|
2124
2146
|
get pendingCount(): number;
|
|
2125
2147
|
/** Get the number of connected SSE clients */
|
package/dist/index.d.ts
CHANGED
|
@@ -1996,8 +1996,11 @@ declare class DashboardApprovalChannel implements ApprovalChannel {
|
|
|
1996
1996
|
private baseline;
|
|
1997
1997
|
private auditLog;
|
|
1998
1998
|
private dashboardHTML;
|
|
1999
|
+
private loginHTML;
|
|
1999
2000
|
private authToken;
|
|
2000
2001
|
private useTLS;
|
|
2002
|
+
/** Session TTL: longer for localhost, shorter for remote */
|
|
2003
|
+
private sessionTTLMs;
|
|
2001
2004
|
/** SEC-012: Short-lived session store. Sessions replace URL query tokens. */
|
|
2002
2005
|
private sessions;
|
|
2003
2006
|
private sessionCleanupTimer;
|
|
@@ -2037,6 +2040,15 @@ declare class DashboardApprovalChannel implements ApprovalChannel {
|
|
|
2037
2040
|
* Returns true if auth passes, false if blocked (response already sent).
|
|
2038
2041
|
*/
|
|
2039
2042
|
private checkAuth;
|
|
2043
|
+
/**
|
|
2044
|
+
* Check if a request is authenticated WITHOUT sending a response.
|
|
2045
|
+
* Used to decide between login page vs dashboard for GET /.
|
|
2046
|
+
*/
|
|
2047
|
+
private isAuthenticated;
|
|
2048
|
+
/**
|
|
2049
|
+
* Parse a specific cookie value from the request.
|
|
2050
|
+
*/
|
|
2051
|
+
private parseCookie;
|
|
2040
2052
|
/**
|
|
2041
2053
|
* Create a short-lived session by exchanging the long-lived auth token
|
|
2042
2054
|
* (provided in the Authorization header) for a session ID.
|
|
@@ -2074,6 +2086,7 @@ declare class DashboardApprovalChannel implements ApprovalChannel {
|
|
|
2074
2086
|
* normal checkAuth flow.
|
|
2075
2087
|
*/
|
|
2076
2088
|
private handleSessionExchange;
|
|
2089
|
+
private serveLoginPage;
|
|
2077
2090
|
private serveDashboard;
|
|
2078
2091
|
private handleSSE;
|
|
2079
2092
|
private handleStatus;
|
|
@@ -2120,6 +2133,15 @@ declare class DashboardApprovalChannel implements ApprovalChannel {
|
|
|
2120
2133
|
* Broadcast current protection status to connected dashboards.
|
|
2121
2134
|
*/
|
|
2122
2135
|
broadcastProtectionStatus(data: Record<string, unknown>): void;
|
|
2136
|
+
/**
|
|
2137
|
+
* Create a pre-authenticated URL for the dashboard.
|
|
2138
|
+
* Used by the sanctuary_dashboard_open tool and at startup.
|
|
2139
|
+
*/
|
|
2140
|
+
createSessionUrl(): string;
|
|
2141
|
+
/**
|
|
2142
|
+
* Get the base URL for the dashboard.
|
|
2143
|
+
*/
|
|
2144
|
+
getBaseUrl(): string;
|
|
2123
2145
|
/** Get the number of pending requests */
|
|
2124
2146
|
get pendingCount(): number;
|
|
2125
2147
|
/** Get the number of connected SSE clients */
|
package/dist/index.js
CHANGED
|
@@ -4055,6 +4055,181 @@ var AutoApproveChannel = class {
|
|
|
4055
4055
|
};
|
|
4056
4056
|
|
|
4057
4057
|
// src/principal-policy/dashboard-html.ts
|
|
4058
|
+
function generateLoginHTML(options) {
|
|
4059
|
+
return `<!DOCTYPE html>
|
|
4060
|
+
<html lang="en">
|
|
4061
|
+
<head>
|
|
4062
|
+
<meta charset="utf-8">
|
|
4063
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
4064
|
+
<title>Sanctuary \u2014 Login</title>
|
|
4065
|
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
|
|
4066
|
+
<style>
|
|
4067
|
+
:root {
|
|
4068
|
+
--bg: #0d1117;
|
|
4069
|
+
--surface: #161b22;
|
|
4070
|
+
--border: #30363d;
|
|
4071
|
+
--text-primary: #e6edf3;
|
|
4072
|
+
--text-secondary: #8b949e;
|
|
4073
|
+
--green: #3fb950;
|
|
4074
|
+
--red: #f85149;
|
|
4075
|
+
--blue: #58a6ff;
|
|
4076
|
+
--mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
|
4077
|
+
--sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
4078
|
+
--radius: 6px;
|
|
4079
|
+
}
|
|
4080
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
4081
|
+
html, body { width: 100%; height: 100%; }
|
|
4082
|
+
body {
|
|
4083
|
+
font-family: var(--sans);
|
|
4084
|
+
background: var(--bg);
|
|
4085
|
+
color: var(--text-primary);
|
|
4086
|
+
display: flex;
|
|
4087
|
+
align-items: center;
|
|
4088
|
+
justify-content: center;
|
|
4089
|
+
}
|
|
4090
|
+
.login-container {
|
|
4091
|
+
width: 100%;
|
|
4092
|
+
max-width: 400px;
|
|
4093
|
+
padding: 40px 32px;
|
|
4094
|
+
background: var(--surface);
|
|
4095
|
+
border: 1px solid var(--border);
|
|
4096
|
+
border-radius: 12px;
|
|
4097
|
+
}
|
|
4098
|
+
.login-logo {
|
|
4099
|
+
text-align: center;
|
|
4100
|
+
font-size: 20px;
|
|
4101
|
+
font-weight: 700;
|
|
4102
|
+
letter-spacing: -0.5px;
|
|
4103
|
+
margin-bottom: 8px;
|
|
4104
|
+
}
|
|
4105
|
+
.login-logo span { color: var(--blue); }
|
|
4106
|
+
.login-version {
|
|
4107
|
+
text-align: center;
|
|
4108
|
+
font-size: 11px;
|
|
4109
|
+
color: var(--text-secondary);
|
|
4110
|
+
font-family: var(--mono);
|
|
4111
|
+
margin-bottom: 32px;
|
|
4112
|
+
}
|
|
4113
|
+
.login-label {
|
|
4114
|
+
display: block;
|
|
4115
|
+
font-size: 13px;
|
|
4116
|
+
font-weight: 600;
|
|
4117
|
+
color: var(--text-secondary);
|
|
4118
|
+
margin-bottom: 8px;
|
|
4119
|
+
}
|
|
4120
|
+
.login-input {
|
|
4121
|
+
width: 100%;
|
|
4122
|
+
padding: 10px 14px;
|
|
4123
|
+
background: var(--bg);
|
|
4124
|
+
border: 1px solid var(--border);
|
|
4125
|
+
border-radius: var(--radius);
|
|
4126
|
+
color: var(--text-primary);
|
|
4127
|
+
font-family: var(--mono);
|
|
4128
|
+
font-size: 14px;
|
|
4129
|
+
outline: none;
|
|
4130
|
+
transition: border-color 0.15s;
|
|
4131
|
+
}
|
|
4132
|
+
.login-input:focus { border-color: var(--blue); }
|
|
4133
|
+
.login-input::placeholder { color: var(--text-secondary); opacity: 0.5; }
|
|
4134
|
+
.login-btn {
|
|
4135
|
+
width: 100%;
|
|
4136
|
+
margin-top: 20px;
|
|
4137
|
+
padding: 10px;
|
|
4138
|
+
background: var(--blue);
|
|
4139
|
+
color: var(--bg);
|
|
4140
|
+
border: none;
|
|
4141
|
+
border-radius: var(--radius);
|
|
4142
|
+
font-size: 14px;
|
|
4143
|
+
font-weight: 600;
|
|
4144
|
+
cursor: pointer;
|
|
4145
|
+
transition: opacity 0.15s;
|
|
4146
|
+
font-family: var(--sans);
|
|
4147
|
+
}
|
|
4148
|
+
.login-btn:hover { opacity: 0.9; }
|
|
4149
|
+
.login-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
4150
|
+
.login-error {
|
|
4151
|
+
margin-top: 16px;
|
|
4152
|
+
padding: 10px 14px;
|
|
4153
|
+
background: rgba(248, 81, 73, 0.1);
|
|
4154
|
+
border: 1px solid var(--red);
|
|
4155
|
+
border-radius: var(--radius);
|
|
4156
|
+
font-size: 12px;
|
|
4157
|
+
color: var(--red);
|
|
4158
|
+
display: none;
|
|
4159
|
+
}
|
|
4160
|
+
.login-hint {
|
|
4161
|
+
margin-top: 24px;
|
|
4162
|
+
padding-top: 16px;
|
|
4163
|
+
border-top: 1px solid var(--border);
|
|
4164
|
+
font-size: 11px;
|
|
4165
|
+
color: var(--text-secondary);
|
|
4166
|
+
line-height: 1.5;
|
|
4167
|
+
}
|
|
4168
|
+
.login-hint code {
|
|
4169
|
+
font-family: var(--mono);
|
|
4170
|
+
background: var(--bg);
|
|
4171
|
+
padding: 1px 4px;
|
|
4172
|
+
border-radius: 3px;
|
|
4173
|
+
font-size: 10px;
|
|
4174
|
+
}
|
|
4175
|
+
</style>
|
|
4176
|
+
</head>
|
|
4177
|
+
<body>
|
|
4178
|
+
<div class="login-container">
|
|
4179
|
+
<div class="login-logo"><span>◆</span> SANCTUARY</div>
|
|
4180
|
+
<div class="login-version">Principal Dashboard v${options.serverVersion}</div>
|
|
4181
|
+
<form id="loginForm" onsubmit="return handleLogin(event)">
|
|
4182
|
+
<label class="login-label" for="tokenInput">Dashboard Auth Token</label>
|
|
4183
|
+
<input class="login-input" type="password" id="tokenInput"
|
|
4184
|
+
placeholder="Enter your auth token" autocomplete="off" autofocus required>
|
|
4185
|
+
<button class="login-btn" type="submit" id="loginBtn">Open Dashboard</button>
|
|
4186
|
+
</form>
|
|
4187
|
+
<div class="login-error" id="loginError"></div>
|
|
4188
|
+
<div class="login-hint">
|
|
4189
|
+
Your token is set via <code>SANCTUARY_DASHBOARD_AUTH_TOKEN</code> environment variable,
|
|
4190
|
+
or check your server's startup output.
|
|
4191
|
+
</div>
|
|
4192
|
+
</div>
|
|
4193
|
+
<script>
|
|
4194
|
+
async function handleLogin(e) {
|
|
4195
|
+
e.preventDefault();
|
|
4196
|
+
var btn = document.getElementById('loginBtn');
|
|
4197
|
+
var errEl = document.getElementById('loginError');
|
|
4198
|
+
var token = document.getElementById('tokenInput').value.trim();
|
|
4199
|
+
if (!token) return false;
|
|
4200
|
+
btn.disabled = true;
|
|
4201
|
+
btn.textContent = 'Authenticating...';
|
|
4202
|
+
errEl.style.display = 'none';
|
|
4203
|
+
try {
|
|
4204
|
+
var resp = await fetch('/auth/session', {
|
|
4205
|
+
method: 'POST',
|
|
4206
|
+
headers: { 'Authorization': 'Bearer ' + token }
|
|
4207
|
+
});
|
|
4208
|
+
if (!resp.ok) {
|
|
4209
|
+
var data = await resp.json().catch(function() { return {}; });
|
|
4210
|
+
throw new Error(data.error || 'Authentication failed');
|
|
4211
|
+
}
|
|
4212
|
+
var result = await resp.json();
|
|
4213
|
+
// Store token in sessionStorage for auto-renewal inside the dashboard
|
|
4214
|
+
try { sessionStorage.setItem('sanctuary_token', token); } catch(_) {}
|
|
4215
|
+
// Set session cookie
|
|
4216
|
+
var maxAge = result.expires_in_seconds || 300;
|
|
4217
|
+
document.cookie = 'sanctuary_session=' + result.session_id +
|
|
4218
|
+
'; path=/; SameSite=Strict; max-age=' + maxAge;
|
|
4219
|
+
// Reload to enter the dashboard
|
|
4220
|
+
window.location.reload();
|
|
4221
|
+
} catch (err) {
|
|
4222
|
+
errEl.textContent = err.message || 'Authentication failed. Check your token.';
|
|
4223
|
+
errEl.style.display = 'block';
|
|
4224
|
+
btn.disabled = false;
|
|
4225
|
+
btn.textContent = 'Open Dashboard';
|
|
4226
|
+
}
|
|
4227
|
+
return false;
|
|
4228
|
+
}
|
|
4229
|
+
</script>
|
|
4230
|
+
</body>
|
|
4231
|
+
</html>`;
|
|
4232
|
+
}
|
|
4058
4233
|
function generateDashboardHTML(options) {
|
|
4059
4234
|
return `<!DOCTYPE html>
|
|
4060
4235
|
<html lang="en">
|
|
@@ -4924,7 +5099,9 @@ function generateDashboardHTML(options) {
|
|
|
4924
5099
|
// \u2500\u2500 Configuration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
4925
5100
|
|
|
4926
5101
|
const TIMEOUT_SECONDS = ${options.timeoutSeconds};
|
|
4927
|
-
|
|
5102
|
+
// AUTH_TOKEN: embedded token (for direct session access) or from sessionStorage (login page flow)
|
|
5103
|
+
const EMBEDDED_TOKEN = ${options.authToken ? JSON.stringify(options.authToken) : "null"};
|
|
5104
|
+
const AUTH_TOKEN = EMBEDDED_TOKEN || (function() { try { return sessionStorage.getItem('sanctuary_token'); } catch(_) { return null; } })();
|
|
4928
5105
|
const MAX_ACTIVITY_ITEMS = 100;
|
|
4929
5106
|
const MAX_THREAT_ITEMS = 20;
|
|
4930
5107
|
|
|
@@ -4939,6 +5116,7 @@ function generateDashboardHTML(options) {
|
|
|
4939
5116
|
const activityItems = [];
|
|
4940
5117
|
const threatItems = [];
|
|
4941
5118
|
let sovereigntyScore = 85;
|
|
5119
|
+
let sessionRenewalTimer = null;
|
|
4942
5120
|
|
|
4943
5121
|
// \u2500\u2500 Auth Helpers (SEC-012) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
4944
5122
|
|
|
@@ -4954,6 +5132,11 @@ function generateDashboardHTML(options) {
|
|
|
4954
5132
|
return url + sep + 'session=' + SESSION_ID;
|
|
4955
5133
|
}
|
|
4956
5134
|
|
|
5135
|
+
function setCookie(sessionId, maxAge) {
|
|
5136
|
+
document.cookie = 'sanctuary_session=' + sessionId +
|
|
5137
|
+
'; path=/; SameSite=Strict; max-age=' + maxAge;
|
|
5138
|
+
}
|
|
5139
|
+
|
|
4957
5140
|
async function exchangeSession() {
|
|
4958
5141
|
if (!AUTH_TOKEN) return;
|
|
4959
5142
|
try {
|
|
@@ -4961,14 +5144,35 @@ function generateDashboardHTML(options) {
|
|
|
4961
5144
|
if (resp.ok) {
|
|
4962
5145
|
const data = await resp.json();
|
|
4963
5146
|
SESSION_ID = data.session_id;
|
|
4964
|
-
|
|
4965
|
-
|
|
5147
|
+
var ttl = data.expires_in_seconds || 300;
|
|
5148
|
+
// Update cookie with new session
|
|
5149
|
+
setCookie(SESSION_ID, ttl);
|
|
5150
|
+
// Schedule renewal at 80% of TTL
|
|
5151
|
+
if (sessionRenewalTimer) clearTimeout(sessionRenewalTimer);
|
|
5152
|
+
sessionRenewalTimer = setTimeout(function() {
|
|
5153
|
+
exchangeSession().then(function() { reconnectSSE(); });
|
|
5154
|
+
}, ttl * 800);
|
|
5155
|
+
} else if (resp.status === 401) {
|
|
5156
|
+
// Token invalid or expired \u2014 show non-destructive re-login overlay
|
|
5157
|
+
showSessionExpired();
|
|
4966
5158
|
}
|
|
4967
5159
|
} catch (e) {
|
|
4968
|
-
//
|
|
5160
|
+
// Network error \u2014 retry in 30s
|
|
5161
|
+
if (sessionRenewalTimer) clearTimeout(sessionRenewalTimer);
|
|
5162
|
+
sessionRenewalTimer = setTimeout(function() {
|
|
5163
|
+
exchangeSession().then(function() { reconnectSSE(); });
|
|
5164
|
+
}, 30000);
|
|
4969
5165
|
}
|
|
4970
5166
|
}
|
|
4971
5167
|
|
|
5168
|
+
function showSessionExpired() {
|
|
5169
|
+
// Clear stored token
|
|
5170
|
+
try { sessionStorage.removeItem('sanctuary_token'); } catch(_) {}
|
|
5171
|
+
// Redirect to login page
|
|
5172
|
+
document.cookie = 'sanctuary_session=; path=/; max-age=0';
|
|
5173
|
+
window.location.reload();
|
|
5174
|
+
}
|
|
5175
|
+
|
|
4972
5176
|
// \u2500\u2500 UI Utilities \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
4973
5177
|
|
|
4974
5178
|
function esc(s) {
|
|
@@ -5441,7 +5645,8 @@ function generateDashboardHTML(options) {
|
|
|
5441
5645
|
}
|
|
5442
5646
|
|
|
5443
5647
|
// src/principal-policy/dashboard.ts
|
|
5444
|
-
var
|
|
5648
|
+
var SESSION_TTL_REMOTE_MS = 5 * 60 * 1e3;
|
|
5649
|
+
var SESSION_TTL_LOCAL_MS = 24 * 60 * 60 * 1e3;
|
|
5445
5650
|
var MAX_SESSIONS = 1e3;
|
|
5446
5651
|
var RATE_LIMIT_WINDOW_MS = 6e4;
|
|
5447
5652
|
var RATE_LIMIT_GENERAL = 120;
|
|
@@ -5456,8 +5661,11 @@ var DashboardApprovalChannel = class {
|
|
|
5456
5661
|
baseline = null;
|
|
5457
5662
|
auditLog = null;
|
|
5458
5663
|
dashboardHTML;
|
|
5664
|
+
loginHTML;
|
|
5459
5665
|
authToken;
|
|
5460
5666
|
useTLS;
|
|
5667
|
+
/** Session TTL: longer for localhost, shorter for remote */
|
|
5668
|
+
sessionTTLMs;
|
|
5461
5669
|
/** SEC-012: Short-lived session store. Sessions replace URL query tokens. */
|
|
5462
5670
|
sessions = /* @__PURE__ */ new Map();
|
|
5463
5671
|
sessionCleanupTimer = null;
|
|
@@ -5467,11 +5675,14 @@ var DashboardApprovalChannel = class {
|
|
|
5467
5675
|
this.config = config;
|
|
5468
5676
|
this.authToken = config.auth_token;
|
|
5469
5677
|
this.useTLS = !!(config.tls?.cert_path && config.tls?.key_path);
|
|
5678
|
+
const isLocalhost = config.host === "127.0.0.1" || config.host === "localhost" || config.host === "::1";
|
|
5679
|
+
this.sessionTTLMs = isLocalhost ? SESSION_TTL_LOCAL_MS : SESSION_TTL_REMOTE_MS;
|
|
5470
5680
|
this.dashboardHTML = generateDashboardHTML({
|
|
5471
5681
|
timeoutSeconds: config.timeout_seconds,
|
|
5472
5682
|
serverVersion: SANCTUARY_VERSION,
|
|
5473
5683
|
authToken: this.authToken
|
|
5474
5684
|
});
|
|
5685
|
+
this.loginHTML = generateLoginHTML({ serverVersion: SANCTUARY_VERSION });
|
|
5475
5686
|
this.sessionCleanupTimer = setInterval(() => this.cleanupSessions(), 6e4);
|
|
5476
5687
|
}
|
|
5477
5688
|
/**
|
|
@@ -5501,25 +5712,26 @@ var DashboardApprovalChannel = class {
|
|
|
5501
5712
|
const protocol = this.useTLS ? "https" : "http";
|
|
5502
5713
|
const baseUrl = `${protocol}://${this.config.host}:${this.config.port}`;
|
|
5503
5714
|
this.httpServer.listen(this.config.port, this.config.host, () => {
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
process.stderr.write(
|
|
5507
|
-
`
|
|
5715
|
+
process.stderr.write(
|
|
5716
|
+
`
|
|
5508
5717
|
Sanctuary Principal Dashboard: ${baseUrl}
|
|
5509
5718
|
`
|
|
5510
|
-
|
|
5719
|
+
);
|
|
5720
|
+
if (this.authToken) {
|
|
5721
|
+
const sessionUrl = this.createSessionUrl();
|
|
5511
5722
|
process.stderr.write(
|
|
5512
|
-
`
|
|
5513
|
-
|
|
5723
|
+
` Quick open: ${sessionUrl}
|
|
5514
5724
|
`
|
|
5515
5725
|
);
|
|
5516
|
-
|
|
5726
|
+
const hint = this.authToken.slice(0, 4) + "..." + this.authToken.slice(-4);
|
|
5517
5727
|
process.stderr.write(
|
|
5518
|
-
`
|
|
5519
|
-
Sanctuary Principal Dashboard: ${baseUrl}
|
|
5728
|
+
` Auth token: ${hint}
|
|
5520
5729
|
|
|
5521
5730
|
`
|
|
5522
5731
|
);
|
|
5732
|
+
} else {
|
|
5733
|
+
process.stderr.write(`
|
|
5734
|
+
`);
|
|
5523
5735
|
}
|
|
5524
5736
|
resolve();
|
|
5525
5737
|
});
|
|
@@ -5623,10 +5835,47 @@ var DashboardApprovalChannel = class {
|
|
|
5623
5835
|
if (sessionId && this.validateSession(sessionId)) {
|
|
5624
5836
|
return true;
|
|
5625
5837
|
}
|
|
5838
|
+
const cookieSession = this.parseCookie(req, "sanctuary_session");
|
|
5839
|
+
if (cookieSession && this.validateSession(cookieSession)) {
|
|
5840
|
+
return true;
|
|
5841
|
+
}
|
|
5626
5842
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
5627
5843
|
res.end(JSON.stringify({ error: "Unauthorized \u2014 use Authorization: Bearer header or a valid session" }));
|
|
5628
5844
|
return false;
|
|
5629
5845
|
}
|
|
5846
|
+
/**
|
|
5847
|
+
* Check if a request is authenticated WITHOUT sending a response.
|
|
5848
|
+
* Used to decide between login page vs dashboard for GET /.
|
|
5849
|
+
*/
|
|
5850
|
+
isAuthenticated(req, url) {
|
|
5851
|
+
if (!this.authToken) return true;
|
|
5852
|
+
const authHeader = req.headers.authorization;
|
|
5853
|
+
if (authHeader) {
|
|
5854
|
+
const parts = authHeader.split(" ");
|
|
5855
|
+
if (parts.length === 2 && parts[0] === "Bearer" && parts[1] === this.authToken) {
|
|
5856
|
+
return true;
|
|
5857
|
+
}
|
|
5858
|
+
}
|
|
5859
|
+
const sessionId = url.searchParams.get("session");
|
|
5860
|
+
if (sessionId && this.validateSession(sessionId)) return true;
|
|
5861
|
+
const cookieSession = this.parseCookie(req, "sanctuary_session");
|
|
5862
|
+
if (cookieSession && this.validateSession(cookieSession)) return true;
|
|
5863
|
+
return false;
|
|
5864
|
+
}
|
|
5865
|
+
/**
|
|
5866
|
+
* Parse a specific cookie value from the request.
|
|
5867
|
+
*/
|
|
5868
|
+
parseCookie(req, name) {
|
|
5869
|
+
const header = req.headers.cookie;
|
|
5870
|
+
if (!header) return null;
|
|
5871
|
+
for (const part of header.split(";")) {
|
|
5872
|
+
const [key, ...rest] = part.split("=");
|
|
5873
|
+
if (key?.trim() === name) {
|
|
5874
|
+
return rest.join("=").trim();
|
|
5875
|
+
}
|
|
5876
|
+
}
|
|
5877
|
+
return null;
|
|
5878
|
+
}
|
|
5630
5879
|
// ── Session Management (SEC-012) ──────────────────────────────────
|
|
5631
5880
|
/**
|
|
5632
5881
|
* Create a short-lived session by exchanging the long-lived auth token
|
|
@@ -5647,7 +5896,7 @@ var DashboardApprovalChannel = class {
|
|
|
5647
5896
|
this.sessions.set(id, {
|
|
5648
5897
|
id,
|
|
5649
5898
|
created_at: now,
|
|
5650
|
-
expires_at: now +
|
|
5899
|
+
expires_at: now + this.sessionTTLMs
|
|
5651
5900
|
});
|
|
5652
5901
|
return id;
|
|
5653
5902
|
}
|
|
@@ -5746,13 +5995,26 @@ var DashboardApprovalChannel = class {
|
|
|
5746
5995
|
res.end();
|
|
5747
5996
|
return;
|
|
5748
5997
|
}
|
|
5749
|
-
if (
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
if (method === "POST" && url.pathname === "/auth/session") {
|
|
5998
|
+
if (method === "POST" && url.pathname === "/auth/session") {
|
|
5999
|
+
if (!this.checkRateLimit(req, res, "general")) return;
|
|
6000
|
+
try {
|
|
5753
6001
|
this.handleSessionExchange(req, res);
|
|
6002
|
+
} catch {
|
|
6003
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
6004
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
6005
|
+
}
|
|
6006
|
+
return;
|
|
6007
|
+
}
|
|
6008
|
+
if (method === "GET" && url.pathname === "/" && this.authToken) {
|
|
6009
|
+
if (!this.isAuthenticated(req, url)) {
|
|
6010
|
+
if (!this.checkRateLimit(req, res, "general")) return;
|
|
6011
|
+
this.serveLoginPage(res);
|
|
5754
6012
|
return;
|
|
5755
6013
|
}
|
|
6014
|
+
}
|
|
6015
|
+
if (!this.checkAuth(req, url, res)) return;
|
|
6016
|
+
if (!this.checkRateLimit(req, res, "general")) return;
|
|
6017
|
+
try {
|
|
5756
6018
|
if (method === "GET" && url.pathname === "/") {
|
|
5757
6019
|
this.serveDashboard(res);
|
|
5758
6020
|
} else if (method === "GET" && url.pathname === "/events") {
|
|
@@ -5809,12 +6071,23 @@ var DashboardApprovalChannel = class {
|
|
|
5809
6071
|
return;
|
|
5810
6072
|
}
|
|
5811
6073
|
const sessionId = this.createSession();
|
|
5812
|
-
|
|
6074
|
+
const ttlSeconds = Math.floor(this.sessionTTLMs / 1e3);
|
|
6075
|
+
res.writeHead(200, {
|
|
6076
|
+
"Content-Type": "application/json",
|
|
6077
|
+
"Set-Cookie": `sanctuary_session=${sessionId}; Path=/; SameSite=Strict; Max-Age=${ttlSeconds}`
|
|
6078
|
+
});
|
|
5813
6079
|
res.end(JSON.stringify({
|
|
5814
6080
|
session_id: sessionId,
|
|
5815
|
-
expires_in_seconds:
|
|
6081
|
+
expires_in_seconds: ttlSeconds
|
|
5816
6082
|
}));
|
|
5817
6083
|
}
|
|
6084
|
+
serveLoginPage(res) {
|
|
6085
|
+
res.writeHead(200, {
|
|
6086
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
6087
|
+
"Cache-Control": "no-cache, no-store"
|
|
6088
|
+
});
|
|
6089
|
+
res.end(this.loginHTML);
|
|
6090
|
+
}
|
|
5818
6091
|
serveDashboard(res) {
|
|
5819
6092
|
res.writeHead(200, {
|
|
5820
6093
|
"Content-Type": "text/html; charset=utf-8",
|
|
@@ -5990,6 +6263,22 @@ data: ${JSON.stringify(data)}
|
|
|
5990
6263
|
broadcastProtectionStatus(data) {
|
|
5991
6264
|
this.broadcastSSE("protection-status", data);
|
|
5992
6265
|
}
|
|
6266
|
+
/**
|
|
6267
|
+
* Create a pre-authenticated URL for the dashboard.
|
|
6268
|
+
* Used by the sanctuary_dashboard_open tool and at startup.
|
|
6269
|
+
*/
|
|
6270
|
+
createSessionUrl() {
|
|
6271
|
+
const sessionId = this.createSession();
|
|
6272
|
+
const protocol = this.useTLS ? "https" : "http";
|
|
6273
|
+
return `${protocol}://${this.config.host}:${this.config.port}/?session=${sessionId}`;
|
|
6274
|
+
}
|
|
6275
|
+
/**
|
|
6276
|
+
* Get the base URL for the dashboard.
|
|
6277
|
+
*/
|
|
6278
|
+
getBaseUrl() {
|
|
6279
|
+
const protocol = this.useTLS ? "https" : "http";
|
|
6280
|
+
return `${protocol}://${this.config.host}:${this.config.port}`;
|
|
6281
|
+
}
|
|
5993
6282
|
/** Get the number of pending requests */
|
|
5994
6283
|
get pendingCount() {
|
|
5995
6284
|
return this.pending.size;
|
|
@@ -11859,6 +12148,32 @@ async function createSanctuaryServer(options) {
|
|
|
11859
12148
|
} : void 0;
|
|
11860
12149
|
const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog, injectionDetector, onInjectionAlert);
|
|
11861
12150
|
const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
|
|
12151
|
+
const dashboardTools = [];
|
|
12152
|
+
if (dashboard) {
|
|
12153
|
+
dashboardTools.push({
|
|
12154
|
+
name: "sanctuary/dashboard_open",
|
|
12155
|
+
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.",
|
|
12156
|
+
inputSchema: {
|
|
12157
|
+
type: "object",
|
|
12158
|
+
properties: {}
|
|
12159
|
+
},
|
|
12160
|
+
handler: async () => {
|
|
12161
|
+
const url = dashboard.createSessionUrl();
|
|
12162
|
+
return {
|
|
12163
|
+
content: [
|
|
12164
|
+
{
|
|
12165
|
+
type: "text",
|
|
12166
|
+
text: JSON.stringify({
|
|
12167
|
+
dashboard_url: url,
|
|
12168
|
+
base_url: dashboard.getBaseUrl(),
|
|
12169
|
+
note: "Click the dashboard_url to open the Principal Dashboard. The session is pre-authenticated."
|
|
12170
|
+
}, null, 2)
|
|
12171
|
+
}
|
|
12172
|
+
]
|
|
12173
|
+
};
|
|
12174
|
+
}
|
|
12175
|
+
});
|
|
12176
|
+
}
|
|
11862
12177
|
let allTools = [
|
|
11863
12178
|
...l1Tools,
|
|
11864
12179
|
...l2Tools,
|
|
@@ -11872,6 +12187,7 @@ async function createSanctuaryServer(options) {
|
|
|
11872
12187
|
...auditTools,
|
|
11873
12188
|
...contextGateTools,
|
|
11874
12189
|
...hardeningTools,
|
|
12190
|
+
...dashboardTools,
|
|
11875
12191
|
manifestTool
|
|
11876
12192
|
];
|
|
11877
12193
|
allTools = allTools.map((tool) => ({
|