@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.
Files changed (93) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/config/ConfigManager.d.ts +34 -0
  3. package/dist/config/ConfigManager.d.ts.map +1 -1
  4. package/dist/config/ConfigManager.js +46 -1
  5. package/dist/config/env.d.ts +18 -0
  6. package/dist/config/env.d.ts.map +1 -1
  7. package/dist/config/env.js +89 -2
  8. package/dist/di/Container.d.ts +1 -0
  9. package/dist/di/Container.d.ts.map +1 -1
  10. package/dist/di/Container.js +38 -2
  11. package/dist/elements/agents/AgentManager.js +2 -2
  12. package/dist/generated/version.d.ts +2 -2
  13. package/dist/generated/version.d.ts.map +1 -1
  14. package/dist/generated/version.js +3 -3
  15. package/dist/handlers/mcp-aql/GatekeeperSession.d.ts.map +1 -1
  16. package/dist/handlers/mcp-aql/GatekeeperSession.js +3 -2
  17. package/dist/handlers/mcp-aql/MCPAQLHandler.d.ts.map +1 -1
  18. package/dist/handlers/mcp-aql/MCPAQLHandler.js +11 -3
  19. package/dist/handlers/mcp-aql/OperationSchema.d.ts.map +1 -1
  20. package/dist/handlers/mcp-aql/OperationSchema.js +11 -6
  21. package/dist/handlers/mcp-aql/policies/ToolClassification.d.ts.map +1 -1
  22. package/dist/handlers/mcp-aql/policies/ToolClassification.js +8 -1
  23. package/dist/index.js +53 -3
  24. package/dist/security/InputNormalizer.d.ts +4 -6
  25. package/dist/security/InputNormalizer.d.ts.map +1 -1
  26. package/dist/security/InputNormalizer.js +10 -16
  27. package/dist/security/audit/config/suppressions.d.ts.map +1 -1
  28. package/dist/security/audit/config/suppressions.js +26 -1
  29. package/dist/security/constants.d.ts +7 -0
  30. package/dist/security/constants.d.ts.map +1 -1
  31. package/dist/security/constants.js +10 -1
  32. package/dist/security/contentValidator.d.ts.map +1 -1
  33. package/dist/security/contentValidator.js +14 -6
  34. package/dist/security/encryption/PatternEncryptor.d.ts.map +1 -1
  35. package/dist/security/encryption/PatternEncryptor.js +18 -10
  36. package/dist/security/securityMonitor.d.ts +1 -1
  37. package/dist/security/securityMonitor.d.ts.map +1 -1
  38. package/dist/security/securityMonitor.js +1 -1
  39. package/dist/security/tokenManager.d.ts +18 -2
  40. package/dist/security/tokenManager.d.ts.map +1 -1
  41. package/dist/security/tokenManager.js +47 -15
  42. package/dist/security/validators/unicodeValidator.d.ts +4 -5
  43. package/dist/security/validators/unicodeValidator.d.ts.map +1 -1
  44. package/dist/security/validators/unicodeValidator.js +19 -20
  45. package/dist/services/VerificationNotifier.d.ts.map +1 -1
  46. package/dist/services/VerificationNotifier.js +15 -13
  47. package/dist/services/validation/GenericElementValidator.js +5 -5
  48. package/dist/web/console/IngestRoutes.d.ts +14 -2
  49. package/dist/web/console/IngestRoutes.d.ts.map +1 -1
  50. package/dist/web/console/IngestRoutes.js +69 -5
  51. package/dist/web/console/LeaderElection.d.ts +37 -3
  52. package/dist/web/console/LeaderElection.d.ts.map +1 -1
  53. package/dist/web/console/LeaderElection.js +67 -7
  54. package/dist/web/console/LeaderForwardingSink.d.ts +19 -3
  55. package/dist/web/console/LeaderForwardingSink.d.ts.map +1 -1
  56. package/dist/web/console/LeaderForwardingSink.js +44 -9
  57. package/dist/web/console/PromotionManager.d.ts +29 -0
  58. package/dist/web/console/PromotionManager.d.ts.map +1 -0
  59. package/dist/web/console/PromotionManager.js +94 -0
  60. package/dist/web/console/SessionNames.d.ts +7 -0
  61. package/dist/web/console/SessionNames.d.ts.map +1 -1
  62. package/dist/web/console/SessionNames.js +10 -1
  63. package/dist/web/console/UnifiedConsole.d.ts +26 -1
  64. package/dist/web/console/UnifiedConsole.d.ts.map +1 -1
  65. package/dist/web/console/UnifiedConsole.js +143 -20
  66. package/dist/web/portDiscovery.d.ts +4 -3
  67. package/dist/web/portDiscovery.d.ts.map +1 -1
  68. package/dist/web/portDiscovery.js +7 -5
  69. package/dist/web/public/app.js +35 -18
  70. package/dist/web/public/consoleAuth.js +152 -0
  71. package/dist/web/public/index.html +243 -9
  72. package/dist/web/public/logs.js +1 -1
  73. package/dist/web/public/metrics.js +3 -3
  74. package/dist/web/public/permissions.js +1 -1
  75. package/dist/web/public/security.css +396 -0
  76. package/dist/web/public/security.js +393 -0
  77. package/dist/web/public/sessions.css +54 -4
  78. package/dist/web/public/sessions.js +46 -6
  79. package/dist/web/public/setup.css +439 -0
  80. package/dist/web/public/setup.js +490 -18
  81. package/dist/web/public/styles.css +10 -0
  82. package/dist/web/routes/permissionRoutes.d.ts.map +1 -1
  83. package/dist/web/routes/permissionRoutes.js +8 -3
  84. package/dist/web/routes/setupRoutes.d.ts +4 -0
  85. package/dist/web/routes/setupRoutes.d.ts.map +1 -1
  86. package/dist/web/routes/setupRoutes.js +418 -7
  87. package/dist/web/routes.d.ts.map +1 -1
  88. package/dist/web/routes.js +9 -5
  89. package/dist/web/server.d.ts +49 -3
  90. package/dist/web/server.d.ts.map +1 -1
  91. package/dist/web/server.js +282 -82
  92. package/package.json +11 -2
  93. 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 &mdash; 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: 200px;
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: flex;
96
+ display: grid;
97
+ grid-template-columns: 1rem 8px 1fr 62px 72px 3.5rem 1.2rem;
97
98
  align-items: center;
98
- gap: 0.5rem;
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
- flex: 1;
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
- fetch('/api/sessions/' + encodeURIComponent(s.sessionId) + '/kill', { method: 'POST' })
244
- .then(function() { fetchSessions(); })
245
- .catch(function() {});
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
- // Fetch sessions from the API
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
- fetch('/api/sessions').then(function(res) {
371
+ DollhouseAuth.apiFetch('/api/sessions').then(function(res) {
332
372
  if (!res.ok) return;
333
373
  return res.json();
334
374
  }).then(function(data) {