@dollhousemcp/mcp-server 2.0.11 → 2.0.12-rc.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/CHANGELOG.md +33 -0
- package/dist/config/ConfigManager.d.ts +34 -0
- package/dist/config/ConfigManager.d.ts.map +1 -1
- package/dist/config/ConfigManager.js +46 -1
- package/dist/config/env.d.ts +18 -0
- package/dist/config/env.d.ts.map +1 -1
- package/dist/config/env.js +89 -2
- package/dist/di/Container.d.ts +1 -0
- package/dist/di/Container.d.ts.map +1 -1
- package/dist/di/Container.js +38 -2
- package/dist/elements/agents/AgentManager.js +2 -2
- package/dist/generated/version.d.ts +2 -2
- package/dist/generated/version.d.ts.map +1 -1
- package/dist/generated/version.js +3 -3
- package/dist/handlers/mcp-aql/GatekeeperSession.d.ts.map +1 -1
- package/dist/handlers/mcp-aql/GatekeeperSession.js +3 -2
- package/dist/handlers/mcp-aql/MCPAQLHandler.d.ts.map +1 -1
- package/dist/handlers/mcp-aql/MCPAQLHandler.js +11 -3
- package/dist/handlers/mcp-aql/OperationSchema.d.ts.map +1 -1
- package/dist/handlers/mcp-aql/OperationSchema.js +11 -6
- package/dist/handlers/mcp-aql/policies/ToolClassification.d.ts.map +1 -1
- package/dist/handlers/mcp-aql/policies/ToolClassification.js +8 -1
- package/dist/index.js +53 -3
- package/dist/security/InputNormalizer.d.ts +4 -6
- package/dist/security/InputNormalizer.d.ts.map +1 -1
- package/dist/security/InputNormalizer.js +10 -16
- package/dist/security/audit/config/suppressions.d.ts.map +1 -1
- package/dist/security/audit/config/suppressions.js +26 -1
- package/dist/security/constants.d.ts +7 -0
- package/dist/security/constants.d.ts.map +1 -1
- package/dist/security/constants.js +10 -1
- package/dist/security/contentValidator.d.ts.map +1 -1
- package/dist/security/contentValidator.js +14 -6
- package/dist/security/encryption/PatternEncryptor.d.ts.map +1 -1
- package/dist/security/encryption/PatternEncryptor.js +18 -10
- package/dist/security/securityMonitor.d.ts +1 -1
- package/dist/security/securityMonitor.d.ts.map +1 -1
- package/dist/security/securityMonitor.js +1 -1
- package/dist/security/tokenManager.d.ts +18 -2
- package/dist/security/tokenManager.d.ts.map +1 -1
- package/dist/security/tokenManager.js +47 -15
- package/dist/security/validators/unicodeValidator.d.ts +4 -5
- package/dist/security/validators/unicodeValidator.d.ts.map +1 -1
- package/dist/security/validators/unicodeValidator.js +19 -20
- package/dist/services/VerificationNotifier.d.ts.map +1 -1
- package/dist/services/VerificationNotifier.js +15 -13
- package/dist/services/validation/GenericElementValidator.js +5 -5
- package/dist/web/console/IngestRoutes.d.ts +14 -2
- package/dist/web/console/IngestRoutes.d.ts.map +1 -1
- package/dist/web/console/IngestRoutes.js +69 -5
- package/dist/web/console/LeaderElection.d.ts +37 -3
- package/dist/web/console/LeaderElection.d.ts.map +1 -1
- package/dist/web/console/LeaderElection.js +67 -7
- package/dist/web/console/LeaderForwardingSink.d.ts +19 -3
- package/dist/web/console/LeaderForwardingSink.d.ts.map +1 -1
- package/dist/web/console/LeaderForwardingSink.js +44 -9
- package/dist/web/console/PromotionManager.d.ts +29 -0
- package/dist/web/console/PromotionManager.d.ts.map +1 -0
- package/dist/web/console/PromotionManager.js +94 -0
- package/dist/web/console/SessionNames.d.ts +7 -0
- package/dist/web/console/SessionNames.d.ts.map +1 -1
- package/dist/web/console/SessionNames.js +10 -1
- package/dist/web/console/UnifiedConsole.d.ts +26 -1
- package/dist/web/console/UnifiedConsole.d.ts.map +1 -1
- package/dist/web/console/UnifiedConsole.js +143 -20
- package/dist/web/portDiscovery.d.ts +4 -3
- package/dist/web/portDiscovery.d.ts.map +1 -1
- package/dist/web/portDiscovery.js +7 -5
- package/dist/web/public/app.js +35 -18
- package/dist/web/public/consoleAuth.js +152 -0
- package/dist/web/public/index.html +243 -9
- package/dist/web/public/logs.js +1 -1
- package/dist/web/public/metrics.js +3 -3
- package/dist/web/public/permissions.js +1 -1
- package/dist/web/public/security.css +396 -0
- package/dist/web/public/security.js +393 -0
- package/dist/web/public/sessions.css +54 -4
- package/dist/web/public/sessions.js +46 -6
- package/dist/web/public/setup.css +439 -0
- package/dist/web/public/setup.js +490 -18
- package/dist/web/public/styles.css +10 -0
- package/dist/web/routes/permissionRoutes.d.ts.map +1 -1
- package/dist/web/routes/permissionRoutes.js +8 -3
- package/dist/web/routes/setupRoutes.d.ts +4 -0
- package/dist/web/routes/setupRoutes.d.ts.map +1 -1
- package/dist/web/routes/setupRoutes.js +418 -7
- package/dist/web/routes.d.ts.map +1 -1
- package/dist/web/routes.js +9 -5
- package/dist/web/server.d.ts +49 -3
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +282 -82
- package/package.json +11 -2
- package/server.json +2 -2
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication tab for the DollhouseMCP management console (#1791).
|
|
3
|
+
*
|
|
4
|
+
* Panels:
|
|
5
|
+
* - Token: current token display (masked), metadata, rotate button
|
|
6
|
+
* - Authenticator: TOTP enrollment status, enroll/disable flows
|
|
7
|
+
*
|
|
8
|
+
* All API calls use DollhouseAuth.apiFetch() for automatic token injection.
|
|
9
|
+
*
|
|
10
|
+
* @security-audit-suppress DMCP-SEC-004 Client-side JS — all data is
|
|
11
|
+
* pre-normalized server-side. Browser-side NFC normalization applied as
|
|
12
|
+
* defense-in-depth via the esc() helper.
|
|
13
|
+
*
|
|
14
|
+
* @since v2.1.0 — Issue #1791
|
|
15
|
+
*/
|
|
16
|
+
(function () {
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
/** How often to refresh token/TOTP data from the server. */
|
|
20
|
+
var POLL_INTERVAL_MS = 5000;
|
|
21
|
+
/** Handle for the polling interval so we can clear on destroy. */
|
|
22
|
+
var pollTimer = null;
|
|
23
|
+
/** Whether initSecurity() has been called (prevents double-init). */
|
|
24
|
+
var initialized = false;
|
|
25
|
+
/** Last fetched data — stashed during enrollment so we can re-render after. */
|
|
26
|
+
var lastData = null;
|
|
27
|
+
/** When true, poll() skips rendering to avoid overwriting an in-progress
|
|
28
|
+
* enrollment flow (QR code, confirm input, backup codes display). */
|
|
29
|
+
var enrollmentInProgress = false;
|
|
30
|
+
/** Debounce flag — prevents rapid double-clicks on action buttons. */
|
|
31
|
+
var actionInProgress = false;
|
|
32
|
+
|
|
33
|
+
/** NFC-normalize and HTML-escape a string for safe display. */
|
|
34
|
+
function esc(str) {
|
|
35
|
+
var normalized = String(str).normalize('NFC');
|
|
36
|
+
var div = document.createElement('div');
|
|
37
|
+
div.textContent = normalized;
|
|
38
|
+
return div.innerHTML;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Format an ISO timestamp for display. */
|
|
42
|
+
function formatTime(iso) {
|
|
43
|
+
if (!iso) return '—';
|
|
44
|
+
try {
|
|
45
|
+
var d = new Date(iso);
|
|
46
|
+
return d.toLocaleDateString() + ' ' + d.toLocaleTimeString();
|
|
47
|
+
} catch { return iso; }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── HTML template ─────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
function buildDashboardHTML() {
|
|
53
|
+
return ''
|
|
54
|
+
+ '<div class="sec-dashboard">'
|
|
55
|
+
|
|
56
|
+
// Token panel
|
|
57
|
+
+ '<div class="sec-card">'
|
|
58
|
+
+ '<div class="sec-card-header">'
|
|
59
|
+
+ '<h3 class="sec-card-title">Console Token</h3>'
|
|
60
|
+
+ '</div>'
|
|
61
|
+
+ '<div class="sec-card-body">'
|
|
62
|
+
+ '<div id="sec-token-content">Loading...</div>'
|
|
63
|
+
+ '</div>'
|
|
64
|
+
+ '</div>'
|
|
65
|
+
|
|
66
|
+
// Authenticator panel
|
|
67
|
+
+ '<div class="sec-card">'
|
|
68
|
+
+ '<div class="sec-card-header">'
|
|
69
|
+
+ '<h3 class="sec-card-title">Authenticator (TOTP — Time-based One-Time Password)</h3>'
|
|
70
|
+
+ '</div>'
|
|
71
|
+
+ '<div class="sec-card-body">'
|
|
72
|
+
+ '<div id="sec-totp-content">Loading...</div>'
|
|
73
|
+
+ '</div>'
|
|
74
|
+
+ '</div>'
|
|
75
|
+
|
|
76
|
+
+ '</div>';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Rendering ─────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
function renderTokenPanel(data) {
|
|
82
|
+
var el = document.getElementById('sec-token-content');
|
|
83
|
+
if (!el || !data.tokens || !data.tokens.length) {
|
|
84
|
+
if (el) el.textContent = 'No token data available.';
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
var t = data.tokens[0];
|
|
88
|
+
var tokenDisplay = '<code class="sec-token-value">' + esc(t.tokenPreview) + '</code>';
|
|
89
|
+
|
|
90
|
+
el.innerHTML = ''
|
|
91
|
+
+ '<div class="sec-token-row">'
|
|
92
|
+
+ '<span class="sec-label">Token</span>'
|
|
93
|
+
+ tokenDisplay
|
|
94
|
+
+ '<button class="sec-btn sec-btn--sm" id="sec-copy-token" title="Copy token to clipboard">Copy</button>'
|
|
95
|
+
+ '<button class="sec-btn sec-btn--sm" id="sec-copy-curl" title="Copy as curl command">Copy curl</button>'
|
|
96
|
+
+ '</div>'
|
|
97
|
+
+ '<div class="sec-meta-grid">'
|
|
98
|
+
+ '<div class="sec-meta"><span class="sec-label">Name</span> ' + esc(t.name) + '</div>'
|
|
99
|
+
+ '<div class="sec-meta"><span class="sec-label">ID</span> <code>' + esc(t.id) + '</code></div>'
|
|
100
|
+
+ '<div class="sec-meta"><span class="sec-label">Kind</span> ' + esc(t.kind) + '</div>'
|
|
101
|
+
+ '<div class="sec-meta"><span class="sec-label">Created</span> ' + esc(formatTime(t.createdAt)) + '</div>'
|
|
102
|
+
+ '<div class="sec-meta"><span class="sec-label">Last used</span> ' + esc(formatTime(t.lastUsedAt)) + '</div>'
|
|
103
|
+
+ '<div class="sec-meta"><span class="sec-label">Created via</span> ' + esc(t.createdVia) + '</div>'
|
|
104
|
+
+ '<div class="sec-meta"><span class="sec-label">File</span> <code>' + esc(data.filePath) + '</code></div>'
|
|
105
|
+
+ '</div>'
|
|
106
|
+
+ '<div class="sec-actions">'
|
|
107
|
+
+ '<button class="sec-btn sec-btn--danger" id="sec-rotate-btn"'
|
|
108
|
+
+ (data.totp.enrolled ? '' : ' disabled title="Enroll TOTP first"')
|
|
109
|
+
+ '>Rotate Token</button>'
|
|
110
|
+
+ '</div>';
|
|
111
|
+
|
|
112
|
+
// Copy token — uses the live token from DollhouseAuth (already in browser memory)
|
|
113
|
+
var copyBtn = document.getElementById('sec-copy-token');
|
|
114
|
+
if (copyBtn) {
|
|
115
|
+
copyBtn.addEventListener('click', function () {
|
|
116
|
+
var liveToken = DollhouseAuth.token;
|
|
117
|
+
if (!liveToken) {
|
|
118
|
+
alert('No token available — authentication may be disabled.');
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
navigator.clipboard.writeText(liveToken)
|
|
122
|
+
.then(function () { copyBtn.textContent = 'Copied!'; setTimeout(function () { copyBtn.textContent = 'Copy'; }, 1500); })
|
|
123
|
+
.catch(function () { alert('Failed to copy — check clipboard permissions'); });
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Copy as curl — includes live token for a runnable command
|
|
128
|
+
var curlBtn = document.getElementById('sec-copy-curl');
|
|
129
|
+
if (curlBtn) {
|
|
130
|
+
curlBtn.addEventListener('click', function () {
|
|
131
|
+
var liveToken = DollhouseAuth.token || '<TOKEN>';
|
|
132
|
+
var port = location.port || '41715';
|
|
133
|
+
var curl = 'curl -H "Authorization: Bearer ' + liveToken + '" http://localhost:' + port + '/api/elements';
|
|
134
|
+
navigator.clipboard.writeText(curl)
|
|
135
|
+
.then(function () { curlBtn.textContent = 'Copied!'; setTimeout(function () { curlBtn.textContent = 'Copy curl'; }, 1500); })
|
|
136
|
+
.catch(function () { alert('Failed to copy — check clipboard permissions'); });
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Rotate button
|
|
141
|
+
var rotateBtn = document.getElementById('sec-rotate-btn');
|
|
142
|
+
if (rotateBtn && !rotateBtn.disabled) {
|
|
143
|
+
rotateBtn.addEventListener('click', handleRotate);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function renderTotpPanel(data) {
|
|
148
|
+
var el = document.getElementById('sec-totp-content');
|
|
149
|
+
if (!el) return;
|
|
150
|
+
var totp = data.totp;
|
|
151
|
+
|
|
152
|
+
if (totp.enrolled) {
|
|
153
|
+
el.innerHTML = ''
|
|
154
|
+
+ '<div class="sec-totp-status sec-totp-status--enrolled">'
|
|
155
|
+
+ '<span class="sec-status-dot sec-status-dot--green"></span>'
|
|
156
|
+
+ '<strong>Enrolled</strong>'
|
|
157
|
+
+ '<span class="sec-meta-inline">since ' + esc(formatTime(totp.enrolledAt)) + '</span>'
|
|
158
|
+
+ '</div>'
|
|
159
|
+
+ '<div class="sec-meta">'
|
|
160
|
+
+ '<span class="sec-label">Backup codes remaining</span> '
|
|
161
|
+
+ '<strong>' + esc(String(totp.backupCodesRemaining)) + '</strong> of 10'
|
|
162
|
+
+ '</div>'
|
|
163
|
+
+ '<div class="sec-actions">'
|
|
164
|
+
+ '<button class="sec-btn sec-btn--danger" id="sec-totp-disable">Disable TOTP</button>'
|
|
165
|
+
+ '</div>';
|
|
166
|
+
|
|
167
|
+
var disableBtn = document.getElementById('sec-totp-disable');
|
|
168
|
+
if (disableBtn) disableBtn.addEventListener('click', handleDisableTotp);
|
|
169
|
+
} else {
|
|
170
|
+
el.innerHTML = ''
|
|
171
|
+
+ '<div class="sec-totp-status sec-totp-status--not-enrolled">'
|
|
172
|
+
+ '<span class="sec-status-dot sec-status-dot--amber"></span>'
|
|
173
|
+
+ '<strong>Not enrolled</strong>'
|
|
174
|
+
+ '</div>'
|
|
175
|
+
+ '<p class="sec-hint">Enroll an authenticator app to enable token rotation and other privileged operations.</p>'
|
|
176
|
+
+ '<div class="sec-actions">'
|
|
177
|
+
+ '<button class="sec-btn sec-btn--primary" id="sec-totp-enroll">Enroll Authenticator</button>'
|
|
178
|
+
+ '</div>';
|
|
179
|
+
|
|
180
|
+
var enrollBtn = document.getElementById('sec-totp-enroll');
|
|
181
|
+
if (enrollBtn) enrollBtn.addEventListener('click', handleEnrollTotp);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function render(data) {
|
|
186
|
+
lastData = data;
|
|
187
|
+
renderTokenPanel(data);
|
|
188
|
+
renderTotpPanel(data);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ── Actions ───────────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
/** Prompt for TOTP code and rotate the primary token. */
|
|
194
|
+
function handleRotate() {
|
|
195
|
+
if (actionInProgress) return;
|
|
196
|
+
var code = prompt('Enter your TOTP code to rotate the token:');
|
|
197
|
+
if (!code) return;
|
|
198
|
+
actionInProgress = true;
|
|
199
|
+
DollhouseAuth.apiFetch('/api/console/token/rotate', {
|
|
200
|
+
method: 'POST',
|
|
201
|
+
headers: { 'Content-Type': 'application/json' },
|
|
202
|
+
body: JSON.stringify({ confirmationCode: code.trim() }),
|
|
203
|
+
})
|
|
204
|
+
.then(function (r) { return r.json().then(function (body) { return { ok: r.ok, body: body }; }); })
|
|
205
|
+
.then(function (result) {
|
|
206
|
+
actionInProgress = false;
|
|
207
|
+
if (result.ok) {
|
|
208
|
+
DollhouseAuth.refresh(result.body.token);
|
|
209
|
+
alert('Token rotated successfully. The new token is now active.');
|
|
210
|
+
poll();
|
|
211
|
+
} else {
|
|
212
|
+
alert('Rotation failed: ' + (result.body.error || 'Unknown error'));
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
.catch(function (err) { actionInProgress = false; alert('Rotation failed: ' + (err.message || 'network error')); });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Start the TOTP enrollment flow — show QR code, confirm, display backup codes. */
|
|
219
|
+
function handleEnrollTotp() {
|
|
220
|
+
if (actionInProgress) return;
|
|
221
|
+
actionInProgress = true;
|
|
222
|
+
DollhouseAuth.apiFetch('/api/console/totp/enroll/begin', {
|
|
223
|
+
method: 'POST',
|
|
224
|
+
headers: { 'Content-Type': 'application/json' },
|
|
225
|
+
body: JSON.stringify({}),
|
|
226
|
+
})
|
|
227
|
+
.then(function (r) { return r.json().then(function (body) { return { ok: r.ok, body: body }; }); })
|
|
228
|
+
.then(function (result) {
|
|
229
|
+
actionInProgress = false;
|
|
230
|
+
if (!result.ok) {
|
|
231
|
+
alert('Enrollment failed: ' + (result.body.error || 'Unknown error'));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
enrollmentInProgress = true;
|
|
235
|
+
var begin = result.body;
|
|
236
|
+
// Show QR code and secret in a panel
|
|
237
|
+
var totpEl = document.getElementById('sec-totp-content');
|
|
238
|
+
if (!totpEl) return;
|
|
239
|
+
totpEl.innerHTML = ''
|
|
240
|
+
+ '<div class="sec-enroll-flow">'
|
|
241
|
+
+ '<h4>Scan this QR code with your authenticator app</h4>'
|
|
242
|
+
+ '<div class="sec-qr-container">'
|
|
243
|
+
+ '<img src="' + begin.qrSvgDataUrl + '" alt="TOTP QR code" class="sec-qr-img" />'
|
|
244
|
+
+ '</div>'
|
|
245
|
+
+ '<p class="sec-hint">Or enter this secret manually: <code>' + esc(begin.secret) + '</code></p>'
|
|
246
|
+
+ '<div class="sec-confirm-form">'
|
|
247
|
+
+ '<label for="sec-confirm-code">Enter the 6-digit code from your app:</label>'
|
|
248
|
+
+ '<input type="text" id="sec-confirm-code" maxlength="6" pattern="[0-9]{6}" '
|
|
249
|
+
+ 'placeholder="000000" autocomplete="one-time-code" class="sec-input" />'
|
|
250
|
+
+ '<button class="sec-btn sec-btn--primary" id="sec-confirm-enroll">Confirm</button>'
|
|
251
|
+
+ '<button class="sec-btn" id="sec-cancel-enroll">Cancel</button>'
|
|
252
|
+
+ '</div>'
|
|
253
|
+
+ '</div>';
|
|
254
|
+
|
|
255
|
+
document.getElementById('sec-cancel-enroll').addEventListener('click', function () { enrollmentInProgress = false; poll(); });
|
|
256
|
+
document.getElementById('sec-confirm-enroll').addEventListener('click', function () {
|
|
257
|
+
var code = document.getElementById('sec-confirm-code').value.trim();
|
|
258
|
+
if (!code) return;
|
|
259
|
+
DollhouseAuth.apiFetch('/api/console/totp/enroll/confirm', {
|
|
260
|
+
method: 'POST',
|
|
261
|
+
headers: { 'Content-Type': 'application/json' },
|
|
262
|
+
body: JSON.stringify({ pendingId: begin.pendingId, code: code }),
|
|
263
|
+
})
|
|
264
|
+
.then(function (r) { return r.json().then(function (body) { return { ok: r.ok, body: body }; }); })
|
|
265
|
+
.then(function (confirmResult) {
|
|
266
|
+
if (confirmResult.ok) {
|
|
267
|
+
// Show backup codes
|
|
268
|
+
totpEl.innerHTML = ''
|
|
269
|
+
+ '<div class="sec-backup-codes">'
|
|
270
|
+
+ '<h4>Backup Codes</h4>'
|
|
271
|
+
+ '<p class="sec-hint sec-hint--warn">Save these codes — you will never see them again.</p>'
|
|
272
|
+
+ '<div class="sec-codes-grid">'
|
|
273
|
+
+ confirmResult.body.backupCodes.map(function (c) {
|
|
274
|
+
return '<code class="sec-code">' + esc(c) + '</code>';
|
|
275
|
+
}).join('')
|
|
276
|
+
+ '</div>'
|
|
277
|
+
+ '<button class="sec-btn sec-btn--primary" id="sec-codes-done">I\'ve saved my codes</button>'
|
|
278
|
+
+ '</div>';
|
|
279
|
+
document.getElementById('sec-codes-done').addEventListener('click', function () { enrollmentInProgress = false; poll(); });
|
|
280
|
+
} else {
|
|
281
|
+
alert('Confirmation failed: ' + (confirmResult.body.error || 'Invalid code'));
|
|
282
|
+
// Keep enrollmentInProgress true — user can retry the code
|
|
283
|
+
}
|
|
284
|
+
})
|
|
285
|
+
.catch(function (err) { alert('Confirmation failed: ' + (err.message || 'network error')); });
|
|
286
|
+
});
|
|
287
|
+
})
|
|
288
|
+
.catch(function (err) { actionInProgress = false; alert('Enrollment failed: ' + (err.message || 'network error')); });
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Disable TOTP enrollment after code confirmation. */
|
|
292
|
+
function handleDisableTotp() {
|
|
293
|
+
if (actionInProgress) return;
|
|
294
|
+
var code = prompt('Enter your TOTP code (or backup code) to disable TOTP:');
|
|
295
|
+
if (!code) return;
|
|
296
|
+
actionInProgress = true;
|
|
297
|
+
DollhouseAuth.apiFetch('/api/console/totp/disable', {
|
|
298
|
+
method: 'POST',
|
|
299
|
+
headers: { 'Content-Type': 'application/json' },
|
|
300
|
+
body: JSON.stringify({ code: code.trim() }),
|
|
301
|
+
})
|
|
302
|
+
.then(function (r) { return r.json().then(function (body) { return { ok: r.ok, body: body }; }); })
|
|
303
|
+
.then(function (result) {
|
|
304
|
+
actionInProgress = false;
|
|
305
|
+
if (result.ok) {
|
|
306
|
+
alert('TOTP disabled. Token rotation now requires re-enrollment.');
|
|
307
|
+
poll();
|
|
308
|
+
} else {
|
|
309
|
+
alert('Disable failed: ' + (result.body.error || 'Unknown error'));
|
|
310
|
+
}
|
|
311
|
+
})
|
|
312
|
+
.catch(function (err) { actionInProgress = false; alert('Disable failed: ' + (err.message || 'network error')); });
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ── Polling & lifecycle ───────────────────────────────────────────────
|
|
316
|
+
|
|
317
|
+
/** Fetch token info and re-render panels (skipped during enrollment flow). */
|
|
318
|
+
function poll() {
|
|
319
|
+
DollhouseAuth.apiFetch('/api/console/token/info')
|
|
320
|
+
.then(function (r) {
|
|
321
|
+
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
322
|
+
return r.json();
|
|
323
|
+
})
|
|
324
|
+
.then(function (data) {
|
|
325
|
+
if (!enrollmentInProgress) render(data);
|
|
326
|
+
else lastData = data; // stash for when enrollment completes
|
|
327
|
+
})
|
|
328
|
+
.catch(function (err) {
|
|
329
|
+
// Clear stale panel content so the user doesn't see outdated data
|
|
330
|
+
// alongside the error message.
|
|
331
|
+
var tokenEl = document.getElementById('sec-token-content');
|
|
332
|
+
var totpEl = document.getElementById('sec-totp-content');
|
|
333
|
+
if (tokenEl) tokenEl.innerHTML = '';
|
|
334
|
+
if (totpEl) totpEl.innerHTML = '';
|
|
335
|
+
var root = document.getElementById('security-dashboard-root');
|
|
336
|
+
if (root && !document.querySelector('.sec-card')) {
|
|
337
|
+
// Dashboard not yet built — show error in root
|
|
338
|
+
root.innerHTML = '<p class="sec-error">Failed to load authentication data: ' + esc(err.message) + '</p>';
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Attach collapse/expand to the intro card only (not token/TOTP panels). */
|
|
344
|
+
function attachIntroToggle() {
|
|
345
|
+
var intro = document.querySelector('.sec-card--intro .sec-card-header');
|
|
346
|
+
if (!intro) return;
|
|
347
|
+
intro.addEventListener('click', function () {
|
|
348
|
+
var card = intro.parentElement;
|
|
349
|
+
var collapsed = card.dataset.collapsed === 'true';
|
|
350
|
+
card.dataset.collapsed = collapsed ? 'false' : 'true';
|
|
351
|
+
intro.setAttribute('aria-expanded', collapsed ? 'true' : 'false');
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function initSecurity() {
|
|
356
|
+
if (initialized) return;
|
|
357
|
+
initialized = true;
|
|
358
|
+
var root = document.getElementById('security-dashboard-root');
|
|
359
|
+
if (!root) return;
|
|
360
|
+
// Clone the intro card from the <template> in index.html and inject
|
|
361
|
+
// it as the first card inside the dashboard grid.
|
|
362
|
+
root.innerHTML = buildDashboardHTML();
|
|
363
|
+
var tmpl = document.getElementById('sec-intro-template');
|
|
364
|
+
if (tmpl) {
|
|
365
|
+
var dashboard = root.querySelector('.sec-dashboard');
|
|
366
|
+
var frag = tmpl ? tmpl.content.cloneNode(true) : null;
|
|
367
|
+
if (dashboard && frag) {
|
|
368
|
+
if (dashboard.firstChild) {
|
|
369
|
+
dashboard.insertBefore(frag, dashboard.firstChild);
|
|
370
|
+
} else {
|
|
371
|
+
dashboard.appendChild(frag);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
attachIntroToggle();
|
|
376
|
+
poll();
|
|
377
|
+
pollTimer = setInterval(poll, POLL_INTERVAL_MS);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function destroySecurity() {
|
|
381
|
+
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
|
|
382
|
+
initialized = false;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ── Public API ────────────────────────────────────────────────────────
|
|
386
|
+
|
|
387
|
+
window.DollhouseConsole = window.DollhouseConsole || {};
|
|
388
|
+
window.DollhouseConsole.security = {
|
|
389
|
+
init: initSecurity,
|
|
390
|
+
destroy: destroySecurity,
|
|
391
|
+
refresh: function () { poll(); },
|
|
392
|
+
};
|
|
393
|
+
})();
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
position: absolute;
|
|
67
67
|
top: calc(100% + 0.4rem);
|
|
68
68
|
right: 0;
|
|
69
|
-
min-width:
|
|
69
|
+
min-width: 480px;
|
|
70
70
|
background: var(--paper-strong, #fff);
|
|
71
71
|
border: 1px solid var(--line, #c8d5e9);
|
|
72
72
|
border-radius: var(--radius-md, 0.85rem);
|
|
@@ -93,9 +93,10 @@
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
.session-dropdown-item {
|
|
96
|
-
display:
|
|
96
|
+
display: grid;
|
|
97
|
+
grid-template-columns: 1rem 8px 1fr 62px 72px 3.5rem 1.2rem;
|
|
97
98
|
align-items: center;
|
|
98
|
-
gap: 0.
|
|
99
|
+
gap: 0.4rem;
|
|
99
100
|
padding: 0.45rem 0.75rem;
|
|
100
101
|
font-size: var(--step--1, 0.82rem);
|
|
101
102
|
cursor: pointer;
|
|
@@ -103,6 +104,10 @@
|
|
|
103
104
|
user-select: none;
|
|
104
105
|
}
|
|
105
106
|
|
|
107
|
+
.session-dropdown-item--all {
|
|
108
|
+
grid-template-columns: 1rem 1fr auto;
|
|
109
|
+
}
|
|
110
|
+
|
|
106
111
|
.session-dropdown-item:hover {
|
|
107
112
|
background: var(--surface-1, #eaf1ff);
|
|
108
113
|
}
|
|
@@ -135,7 +140,9 @@
|
|
|
135
140
|
.session-dropdown-name {
|
|
136
141
|
font-weight: 600;
|
|
137
142
|
color: var(--ink-900, #18243a);
|
|
138
|
-
|
|
143
|
+
overflow: hidden;
|
|
144
|
+
text-overflow: ellipsis;
|
|
145
|
+
white-space: nowrap;
|
|
139
146
|
}
|
|
140
147
|
|
|
141
148
|
.session-dropdown-uptime {
|
|
@@ -143,6 +150,7 @@
|
|
|
143
150
|
font-family: var(--font-mono, monospace);
|
|
144
151
|
color: var(--ink-500, #677893);
|
|
145
152
|
white-space: nowrap;
|
|
153
|
+
text-align: right;
|
|
146
154
|
}
|
|
147
155
|
|
|
148
156
|
.session-dropdown-role {
|
|
@@ -229,6 +237,48 @@
|
|
|
229
237
|
letter-spacing: 0.02em;
|
|
230
238
|
}
|
|
231
239
|
|
|
240
|
+
/* Session status badges — auth + client indicators (#1805)
|
|
241
|
+
* Two independent dimensions, each with:
|
|
242
|
+
* - Shape (filled/empty circle for auth, checkmark/X for client)
|
|
243
|
+
* - Text label (always present for accessibility)
|
|
244
|
+
* - Color (blue=positive, orange=negative — colorblind-safe pair)
|
|
245
|
+
*/
|
|
246
|
+
.session-status-badge {
|
|
247
|
+
display: inline-block;
|
|
248
|
+
padding: 1px 5px;
|
|
249
|
+
border-radius: 3px;
|
|
250
|
+
font-size: 9px;
|
|
251
|
+
font-weight: 600;
|
|
252
|
+
letter-spacing: 0.04em;
|
|
253
|
+
white-space: nowrap;
|
|
254
|
+
text-align: center;
|
|
255
|
+
min-width: 52px;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.session-status-badge[data-status="positive"] {
|
|
259
|
+
background: #eff6ff;
|
|
260
|
+
color: #1e40af;
|
|
261
|
+
border: 1px solid #bfdbfe;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.session-status-badge[data-status="negative"] {
|
|
265
|
+
background: #fff7ed;
|
|
266
|
+
color: #9a3412;
|
|
267
|
+
border: 1px solid #fed7aa;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
[data-theme="dark"] .session-status-badge[data-status="positive"] {
|
|
271
|
+
background: #172554;
|
|
272
|
+
color: #93c5fd;
|
|
273
|
+
border-color: #1e40af;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
[data-theme="dark"] .session-status-badge[data-status="negative"] {
|
|
277
|
+
background: #431407;
|
|
278
|
+
color: #fdba74;
|
|
279
|
+
border-color: #9a3412;
|
|
280
|
+
}
|
|
281
|
+
|
|
232
282
|
/* Session filter in the log viewer */
|
|
233
283
|
#log-session-filter {
|
|
234
284
|
min-width: 120px;
|
|
@@ -142,7 +142,6 @@
|
|
|
142
142
|
dropdownBuilt = false;
|
|
143
143
|
|
|
144
144
|
var count = active.length;
|
|
145
|
-
if (count === 0) return;
|
|
146
145
|
|
|
147
146
|
// Box button
|
|
148
147
|
var box = document.createElement('button');
|
|
@@ -226,6 +225,37 @@
|
|
|
226
225
|
if (s.color) nameEl.style.color = s.color;
|
|
227
226
|
item.appendChild(nameEl);
|
|
228
227
|
|
|
228
|
+
// Session status badges (#1805) — two independent dimensions:
|
|
229
|
+
// 1. Auth status (filled/empty circle + text)
|
|
230
|
+
// 2. Client attachment (checkmark/X + text)
|
|
231
|
+
// Shape + text + colorblind-safe color (blue/orange) = three
|
|
232
|
+
// independent channels so no single channel carries meaning alone.
|
|
233
|
+
var authBadge = document.createElement('span');
|
|
234
|
+
authBadge.className = 'session-status-badge';
|
|
235
|
+
if (s.authenticated) {
|
|
236
|
+
authBadge.textContent = '\u25CF Auth';
|
|
237
|
+
authBadge.dataset.status = 'positive';
|
|
238
|
+
authBadge.title = 'Authenticated session';
|
|
239
|
+
} else {
|
|
240
|
+
authBadge.textContent = '\u25CB No auth';
|
|
241
|
+
authBadge.dataset.status = 'negative';
|
|
242
|
+
authBadge.title = 'Unauthenticated session';
|
|
243
|
+
}
|
|
244
|
+
item.appendChild(authBadge);
|
|
245
|
+
|
|
246
|
+
var clientBadge = document.createElement('span');
|
|
247
|
+
clientBadge.className = 'session-status-badge';
|
|
248
|
+
if (s.kind === 'mcp') {
|
|
249
|
+
clientBadge.textContent = '\u2713 Client';
|
|
250
|
+
clientBadge.dataset.status = 'positive';
|
|
251
|
+
clientBadge.title = 'MCP client attached';
|
|
252
|
+
} else {
|
|
253
|
+
clientBadge.textContent = '\u2717 No client';
|
|
254
|
+
clientBadge.dataset.status = 'negative';
|
|
255
|
+
clientBadge.title = 'No MCP client attached';
|
|
256
|
+
}
|
|
257
|
+
item.appendChild(clientBadge);
|
|
258
|
+
|
|
229
259
|
var uptimeEl = document.createElement('span');
|
|
230
260
|
uptimeEl.className = 'session-dropdown-uptime';
|
|
231
261
|
uptimeEl.dataset.startedAt = s.startedAt;
|
|
@@ -240,9 +270,16 @@
|
|
|
240
270
|
killBtn.addEventListener('click', function(e) {
|
|
241
271
|
e.stopPropagation();
|
|
242
272
|
if (!confirm('Stop session ' + displayName(s) + '?')) return;
|
|
243
|
-
|
|
244
|
-
.then(function() {
|
|
245
|
-
|
|
273
|
+
DollhouseAuth.apiFetch('/api/sessions/' + encodeURIComponent(s.sessionId) + '/kill', { method: 'POST' })
|
|
274
|
+
.then(function(res) {
|
|
275
|
+
if (!res.ok) {
|
|
276
|
+
alert('Failed to stop session ' + displayName(s) + ': server returned ' + res.status);
|
|
277
|
+
}
|
|
278
|
+
fetchSessions();
|
|
279
|
+
})
|
|
280
|
+
.catch(function(err) {
|
|
281
|
+
alert('Failed to stop session ' + displayName(s) + ': ' + (err.message || 'network error'));
|
|
282
|
+
});
|
|
246
283
|
});
|
|
247
284
|
item.appendChild(killBtn);
|
|
248
285
|
|
|
@@ -326,9 +363,12 @@
|
|
|
326
363
|
}
|
|
327
364
|
}
|
|
328
365
|
|
|
329
|
-
|
|
366
|
+
/**
|
|
367
|
+
* Fetch sessions from the API. The server handles federation with the
|
|
368
|
+
* legacy port (3939) server-side to avoid CORS issues (#1805).
|
|
369
|
+
*/
|
|
330
370
|
function fetchSessions() {
|
|
331
|
-
|
|
371
|
+
DollhouseAuth.apiFetch('/api/sessions').then(function(res) {
|
|
332
372
|
if (!res.ok) return;
|
|
333
373
|
return res.json();
|
|
334
374
|
}).then(function(data) {
|