@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.d.cts
CHANGED
|
@@ -58,6 +58,9 @@ interface SanctuaryConfig {
|
|
|
58
58
|
}
|
|
59
59
|
/**
|
|
60
60
|
* Load configuration from file, falling back to defaults.
|
|
61
|
+
*
|
|
62
|
+
* Precedence (highest wins): CLI flags > env vars > config file > defaults
|
|
63
|
+
* This matches the standard config precedence pattern used by most tools.
|
|
61
64
|
*/
|
|
62
65
|
declare function loadConfig(configPath?: string): Promise<SanctuaryConfig>;
|
|
63
66
|
|
|
@@ -1993,8 +1996,11 @@ declare class DashboardApprovalChannel implements ApprovalChannel {
|
|
|
1993
1996
|
private baseline;
|
|
1994
1997
|
private auditLog;
|
|
1995
1998
|
private dashboardHTML;
|
|
1999
|
+
private loginHTML;
|
|
1996
2000
|
private authToken;
|
|
1997
2001
|
private useTLS;
|
|
2002
|
+
/** Session TTL: longer for localhost, shorter for remote */
|
|
2003
|
+
private sessionTTLMs;
|
|
1998
2004
|
/** SEC-012: Short-lived session store. Sessions replace URL query tokens. */
|
|
1999
2005
|
private sessions;
|
|
2000
2006
|
private sessionCleanupTimer;
|
|
@@ -2034,6 +2040,15 @@ declare class DashboardApprovalChannel implements ApprovalChannel {
|
|
|
2034
2040
|
* Returns true if auth passes, false if blocked (response already sent).
|
|
2035
2041
|
*/
|
|
2036
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;
|
|
2037
2052
|
/**
|
|
2038
2053
|
* Create a short-lived session by exchanging the long-lived auth token
|
|
2039
2054
|
* (provided in the Authorization header) for a session ID.
|
|
@@ -2071,6 +2086,7 @@ declare class DashboardApprovalChannel implements ApprovalChannel {
|
|
|
2071
2086
|
* normal checkAuth flow.
|
|
2072
2087
|
*/
|
|
2073
2088
|
private handleSessionExchange;
|
|
2089
|
+
private serveLoginPage;
|
|
2074
2090
|
private serveDashboard;
|
|
2075
2091
|
private handleSSE;
|
|
2076
2092
|
private handleStatus;
|
|
@@ -2117,6 +2133,15 @@ declare class DashboardApprovalChannel implements ApprovalChannel {
|
|
|
2117
2133
|
* Broadcast current protection status to connected dashboards.
|
|
2118
2134
|
*/
|
|
2119
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;
|
|
2120
2145
|
/** Get the number of pending requests */
|
|
2121
2146
|
get pendingCount(): number;
|
|
2122
2147
|
/** Get the number of connected SSE clients */
|
package/dist/index.d.ts
CHANGED
|
@@ -58,6 +58,9 @@ interface SanctuaryConfig {
|
|
|
58
58
|
}
|
|
59
59
|
/**
|
|
60
60
|
* Load configuration from file, falling back to defaults.
|
|
61
|
+
*
|
|
62
|
+
* Precedence (highest wins): CLI flags > env vars > config file > defaults
|
|
63
|
+
* This matches the standard config precedence pattern used by most tools.
|
|
61
64
|
*/
|
|
62
65
|
declare function loadConfig(configPath?: string): Promise<SanctuaryConfig>;
|
|
63
66
|
|
|
@@ -1993,8 +1996,11 @@ declare class DashboardApprovalChannel implements ApprovalChannel {
|
|
|
1993
1996
|
private baseline;
|
|
1994
1997
|
private auditLog;
|
|
1995
1998
|
private dashboardHTML;
|
|
1999
|
+
private loginHTML;
|
|
1996
2000
|
private authToken;
|
|
1997
2001
|
private useTLS;
|
|
2002
|
+
/** Session TTL: longer for localhost, shorter for remote */
|
|
2003
|
+
private sessionTTLMs;
|
|
1998
2004
|
/** SEC-012: Short-lived session store. Sessions replace URL query tokens. */
|
|
1999
2005
|
private sessions;
|
|
2000
2006
|
private sessionCleanupTimer;
|
|
@@ -2034,6 +2040,15 @@ declare class DashboardApprovalChannel implements ApprovalChannel {
|
|
|
2034
2040
|
* Returns true if auth passes, false if blocked (response already sent).
|
|
2035
2041
|
*/
|
|
2036
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;
|
|
2037
2052
|
/**
|
|
2038
2053
|
* Create a short-lived session by exchanging the long-lived auth token
|
|
2039
2054
|
* (provided in the Authorization header) for a session ID.
|
|
@@ -2071,6 +2086,7 @@ declare class DashboardApprovalChannel implements ApprovalChannel {
|
|
|
2071
2086
|
* normal checkAuth flow.
|
|
2072
2087
|
*/
|
|
2073
2088
|
private handleSessionExchange;
|
|
2089
|
+
private serveLoginPage;
|
|
2074
2090
|
private serveDashboard;
|
|
2075
2091
|
private handleSSE;
|
|
2076
2092
|
private handleStatus;
|
|
@@ -2117,6 +2133,15 @@ declare class DashboardApprovalChannel implements ApprovalChannel {
|
|
|
2117
2133
|
* Broadcast current protection status to connected dashboards.
|
|
2118
2134
|
*/
|
|
2119
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;
|
|
2120
2145
|
/** Get the number of pending requests */
|
|
2121
2146
|
get pendingCount(): number;
|
|
2122
2147
|
/** Get the number of connected SSE clients */
|
package/dist/index.js
CHANGED
|
@@ -254,7 +254,18 @@ function defaultConfig() {
|
|
|
254
254
|
};
|
|
255
255
|
}
|
|
256
256
|
async function loadConfig(configPath) {
|
|
257
|
-
|
|
257
|
+
let config = defaultConfig();
|
|
258
|
+
const storagePath = process.env.SANCTUARY_STORAGE_PATH ?? config.storage_path;
|
|
259
|
+
const path = configPath ?? join(storagePath, "sanctuary.json");
|
|
260
|
+
try {
|
|
261
|
+
const raw = await readFile(path, "utf-8");
|
|
262
|
+
const fileConfig = JSON.parse(raw);
|
|
263
|
+
config = deepMerge(config, fileConfig);
|
|
264
|
+
} catch (err) {
|
|
265
|
+
if (err instanceof Error && err.message.includes("unimplemented features")) {
|
|
266
|
+
throw err;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
258
269
|
if (process.env.SANCTUARY_STORAGE_PATH) {
|
|
259
270
|
config.storage_path = process.env.SANCTUARY_STORAGE_PATH;
|
|
260
271
|
}
|
|
@@ -267,6 +278,9 @@ async function loadConfig(configPath) {
|
|
|
267
278
|
if (process.env.SANCTUARY_DASHBOARD_ENABLED === "true") {
|
|
268
279
|
config.dashboard.enabled = true;
|
|
269
280
|
}
|
|
281
|
+
if (process.env.SANCTUARY_DASHBOARD_ENABLED === "false") {
|
|
282
|
+
config.dashboard.enabled = false;
|
|
283
|
+
}
|
|
270
284
|
if (process.env.SANCTUARY_DASHBOARD_PORT) {
|
|
271
285
|
config.dashboard.port = parseInt(process.env.SANCTUARY_DASHBOARD_PORT, 10);
|
|
272
286
|
}
|
|
@@ -285,6 +299,9 @@ async function loadConfig(configPath) {
|
|
|
285
299
|
if (process.env.SANCTUARY_WEBHOOK_ENABLED === "true") {
|
|
286
300
|
config.webhook.enabled = true;
|
|
287
301
|
}
|
|
302
|
+
if (process.env.SANCTUARY_WEBHOOK_ENABLED === "false") {
|
|
303
|
+
config.webhook.enabled = false;
|
|
304
|
+
}
|
|
288
305
|
if (process.env.SANCTUARY_WEBHOOK_URL) {
|
|
289
306
|
config.webhook.url = process.env.SANCTUARY_WEBHOOK_URL;
|
|
290
307
|
}
|
|
@@ -297,19 +314,9 @@ async function loadConfig(configPath) {
|
|
|
297
314
|
if (process.env.SANCTUARY_WEBHOOK_CALLBACK_HOST) {
|
|
298
315
|
config.webhook.callback_host = process.env.SANCTUARY_WEBHOOK_CALLBACK_HOST;
|
|
299
316
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const fileConfig = JSON.parse(raw);
|
|
304
|
-
const merged = deepMerge(config, fileConfig);
|
|
305
|
-
validateConfig(merged);
|
|
306
|
-
return merged;
|
|
307
|
-
} catch (err) {
|
|
308
|
-
if (err instanceof Error && err.message.includes("unimplemented features")) {
|
|
309
|
-
throw err;
|
|
310
|
-
}
|
|
311
|
-
return config;
|
|
312
|
-
}
|
|
317
|
+
config.version = PKG_VERSION;
|
|
318
|
+
validateConfig(config);
|
|
319
|
+
return config;
|
|
313
320
|
}
|
|
314
321
|
async function saveConfig(config, configPath) {
|
|
315
322
|
const path = join(config.storage_path, "sanctuary.json");
|
|
@@ -4048,6 +4055,181 @@ var AutoApproveChannel = class {
|
|
|
4048
4055
|
};
|
|
4049
4056
|
|
|
4050
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
|
+
}
|
|
4051
4233
|
function generateDashboardHTML(options) {
|
|
4052
4234
|
return `<!DOCTYPE html>
|
|
4053
4235
|
<html lang="en">
|
|
@@ -4917,7 +5099,9 @@ function generateDashboardHTML(options) {
|
|
|
4917
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
|
|
4918
5100
|
|
|
4919
5101
|
const TIMEOUT_SECONDS = ${options.timeoutSeconds};
|
|
4920
|
-
|
|
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; } })();
|
|
4921
5105
|
const MAX_ACTIVITY_ITEMS = 100;
|
|
4922
5106
|
const MAX_THREAT_ITEMS = 20;
|
|
4923
5107
|
|
|
@@ -4932,6 +5116,7 @@ function generateDashboardHTML(options) {
|
|
|
4932
5116
|
const activityItems = [];
|
|
4933
5117
|
const threatItems = [];
|
|
4934
5118
|
let sovereigntyScore = 85;
|
|
5119
|
+
let sessionRenewalTimer = null;
|
|
4935
5120
|
|
|
4936
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
|
|
4937
5122
|
|
|
@@ -4947,6 +5132,11 @@ function generateDashboardHTML(options) {
|
|
|
4947
5132
|
return url + sep + 'session=' + SESSION_ID;
|
|
4948
5133
|
}
|
|
4949
5134
|
|
|
5135
|
+
function setCookie(sessionId, maxAge) {
|
|
5136
|
+
document.cookie = 'sanctuary_session=' + sessionId +
|
|
5137
|
+
'; path=/; SameSite=Strict; max-age=' + maxAge;
|
|
5138
|
+
}
|
|
5139
|
+
|
|
4950
5140
|
async function exchangeSession() {
|
|
4951
5141
|
if (!AUTH_TOKEN) return;
|
|
4952
5142
|
try {
|
|
@@ -4954,14 +5144,35 @@ function generateDashboardHTML(options) {
|
|
|
4954
5144
|
if (resp.ok) {
|
|
4955
5145
|
const data = await resp.json();
|
|
4956
5146
|
SESSION_ID = data.session_id;
|
|
4957
|
-
|
|
4958
|
-
|
|
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();
|
|
4959
5158
|
}
|
|
4960
5159
|
} catch (e) {
|
|
4961
|
-
//
|
|
5160
|
+
// Network error \u2014 retry in 30s
|
|
5161
|
+
if (sessionRenewalTimer) clearTimeout(sessionRenewalTimer);
|
|
5162
|
+
sessionRenewalTimer = setTimeout(function() {
|
|
5163
|
+
exchangeSession().then(function() { reconnectSSE(); });
|
|
5164
|
+
}, 30000);
|
|
4962
5165
|
}
|
|
4963
5166
|
}
|
|
4964
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
|
+
|
|
4965
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
|
|
4966
5177
|
|
|
4967
5178
|
function esc(s) {
|
|
@@ -5434,7 +5645,8 @@ function generateDashboardHTML(options) {
|
|
|
5434
5645
|
}
|
|
5435
5646
|
|
|
5436
5647
|
// src/principal-policy/dashboard.ts
|
|
5437
|
-
var
|
|
5648
|
+
var SESSION_TTL_REMOTE_MS = 5 * 60 * 1e3;
|
|
5649
|
+
var SESSION_TTL_LOCAL_MS = 24 * 60 * 60 * 1e3;
|
|
5438
5650
|
var MAX_SESSIONS = 1e3;
|
|
5439
5651
|
var RATE_LIMIT_WINDOW_MS = 6e4;
|
|
5440
5652
|
var RATE_LIMIT_GENERAL = 120;
|
|
@@ -5449,8 +5661,11 @@ var DashboardApprovalChannel = class {
|
|
|
5449
5661
|
baseline = null;
|
|
5450
5662
|
auditLog = null;
|
|
5451
5663
|
dashboardHTML;
|
|
5664
|
+
loginHTML;
|
|
5452
5665
|
authToken;
|
|
5453
5666
|
useTLS;
|
|
5667
|
+
/** Session TTL: longer for localhost, shorter for remote */
|
|
5668
|
+
sessionTTLMs;
|
|
5454
5669
|
/** SEC-012: Short-lived session store. Sessions replace URL query tokens. */
|
|
5455
5670
|
sessions = /* @__PURE__ */ new Map();
|
|
5456
5671
|
sessionCleanupTimer = null;
|
|
@@ -5460,11 +5675,14 @@ var DashboardApprovalChannel = class {
|
|
|
5460
5675
|
this.config = config;
|
|
5461
5676
|
this.authToken = config.auth_token;
|
|
5462
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;
|
|
5463
5680
|
this.dashboardHTML = generateDashboardHTML({
|
|
5464
5681
|
timeoutSeconds: config.timeout_seconds,
|
|
5465
5682
|
serverVersion: SANCTUARY_VERSION,
|
|
5466
5683
|
authToken: this.authToken
|
|
5467
5684
|
});
|
|
5685
|
+
this.loginHTML = generateLoginHTML({ serverVersion: SANCTUARY_VERSION });
|
|
5468
5686
|
this.sessionCleanupTimer = setInterval(() => this.cleanupSessions(), 6e4);
|
|
5469
5687
|
}
|
|
5470
5688
|
/**
|
|
@@ -5494,25 +5712,26 @@ var DashboardApprovalChannel = class {
|
|
|
5494
5712
|
const protocol = this.useTLS ? "https" : "http";
|
|
5495
5713
|
const baseUrl = `${protocol}://${this.config.host}:${this.config.port}`;
|
|
5496
5714
|
this.httpServer.listen(this.config.port, this.config.host, () => {
|
|
5497
|
-
|
|
5498
|
-
|
|
5499
|
-
process.stderr.write(
|
|
5500
|
-
`
|
|
5715
|
+
process.stderr.write(
|
|
5716
|
+
`
|
|
5501
5717
|
Sanctuary Principal Dashboard: ${baseUrl}
|
|
5502
5718
|
`
|
|
5503
|
-
|
|
5719
|
+
);
|
|
5720
|
+
if (this.authToken) {
|
|
5721
|
+
const sessionUrl = this.createSessionUrl();
|
|
5504
5722
|
process.stderr.write(
|
|
5505
|
-
`
|
|
5506
|
-
|
|
5723
|
+
` Quick open: ${sessionUrl}
|
|
5507
5724
|
`
|
|
5508
5725
|
);
|
|
5509
|
-
|
|
5726
|
+
const hint = this.authToken.slice(0, 4) + "..." + this.authToken.slice(-4);
|
|
5510
5727
|
process.stderr.write(
|
|
5511
|
-
`
|
|
5512
|
-
Sanctuary Principal Dashboard: ${baseUrl}
|
|
5728
|
+
` Auth token: ${hint}
|
|
5513
5729
|
|
|
5514
5730
|
`
|
|
5515
5731
|
);
|
|
5732
|
+
} else {
|
|
5733
|
+
process.stderr.write(`
|
|
5734
|
+
`);
|
|
5516
5735
|
}
|
|
5517
5736
|
resolve();
|
|
5518
5737
|
});
|
|
@@ -5616,10 +5835,47 @@ var DashboardApprovalChannel = class {
|
|
|
5616
5835
|
if (sessionId && this.validateSession(sessionId)) {
|
|
5617
5836
|
return true;
|
|
5618
5837
|
}
|
|
5838
|
+
const cookieSession = this.parseCookie(req, "sanctuary_session");
|
|
5839
|
+
if (cookieSession && this.validateSession(cookieSession)) {
|
|
5840
|
+
return true;
|
|
5841
|
+
}
|
|
5619
5842
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
5620
5843
|
res.end(JSON.stringify({ error: "Unauthorized \u2014 use Authorization: Bearer header or a valid session" }));
|
|
5621
5844
|
return false;
|
|
5622
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
|
+
}
|
|
5623
5879
|
// ── Session Management (SEC-012) ──────────────────────────────────
|
|
5624
5880
|
/**
|
|
5625
5881
|
* Create a short-lived session by exchanging the long-lived auth token
|
|
@@ -5640,7 +5896,7 @@ var DashboardApprovalChannel = class {
|
|
|
5640
5896
|
this.sessions.set(id, {
|
|
5641
5897
|
id,
|
|
5642
5898
|
created_at: now,
|
|
5643
|
-
expires_at: now +
|
|
5899
|
+
expires_at: now + this.sessionTTLMs
|
|
5644
5900
|
});
|
|
5645
5901
|
return id;
|
|
5646
5902
|
}
|
|
@@ -5739,13 +5995,26 @@ var DashboardApprovalChannel = class {
|
|
|
5739
5995
|
res.end();
|
|
5740
5996
|
return;
|
|
5741
5997
|
}
|
|
5742
|
-
if (
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
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 {
|
|
5746
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);
|
|
5747
6012
|
return;
|
|
5748
6013
|
}
|
|
6014
|
+
}
|
|
6015
|
+
if (!this.checkAuth(req, url, res)) return;
|
|
6016
|
+
if (!this.checkRateLimit(req, res, "general")) return;
|
|
6017
|
+
try {
|
|
5749
6018
|
if (method === "GET" && url.pathname === "/") {
|
|
5750
6019
|
this.serveDashboard(res);
|
|
5751
6020
|
} else if (method === "GET" && url.pathname === "/events") {
|
|
@@ -5802,12 +6071,23 @@ var DashboardApprovalChannel = class {
|
|
|
5802
6071
|
return;
|
|
5803
6072
|
}
|
|
5804
6073
|
const sessionId = this.createSession();
|
|
5805
|
-
|
|
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
|
+
});
|
|
5806
6079
|
res.end(JSON.stringify({
|
|
5807
6080
|
session_id: sessionId,
|
|
5808
|
-
expires_in_seconds:
|
|
6081
|
+
expires_in_seconds: ttlSeconds
|
|
5809
6082
|
}));
|
|
5810
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
|
+
}
|
|
5811
6091
|
serveDashboard(res) {
|
|
5812
6092
|
res.writeHead(200, {
|
|
5813
6093
|
"Content-Type": "text/html; charset=utf-8",
|
|
@@ -5983,6 +6263,22 @@ data: ${JSON.stringify(data)}
|
|
|
5983
6263
|
broadcastProtectionStatus(data) {
|
|
5984
6264
|
this.broadcastSSE("protection-status", data);
|
|
5985
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
|
+
}
|
|
5986
6282
|
/** Get the number of pending requests */
|
|
5987
6283
|
get pendingCount() {
|
|
5988
6284
|
return this.pending.size;
|
|
@@ -11852,6 +12148,32 @@ async function createSanctuaryServer(options) {
|
|
|
11852
12148
|
} : void 0;
|
|
11853
12149
|
const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog, injectionDetector, onInjectionAlert);
|
|
11854
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
|
+
}
|
|
11855
12177
|
let allTools = [
|
|
11856
12178
|
...l1Tools,
|
|
11857
12179
|
...l2Tools,
|
|
@@ -11865,6 +12187,7 @@ async function createSanctuaryServer(options) {
|
|
|
11865
12187
|
...auditTools,
|
|
11866
12188
|
...contextGateTools,
|
|
11867
12189
|
...hardeningTools,
|
|
12190
|
+
...dashboardTools,
|
|
11868
12191
|
manifestTool
|
|
11869
12192
|
];
|
|
11870
12193
|
allTools = allTools.map((tool) => ({
|