@sanctuary-framework/mcp-server 0.5.5 → 0.5.6

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.js CHANGED
@@ -867,6 +867,8 @@ tier3_always_allow:
867
867
  - handshake_respond
868
868
  - handshake_complete
869
869
  - handshake_status
870
+ - handshake_exchange
871
+ - handshake_verify_attestation
870
872
  - reputation_query_weighted
871
873
  - federation_peers
872
874
  - federation_trust_evaluate
@@ -968,6 +970,8 @@ var init_loader = __esm({
968
970
  "handshake_respond",
969
971
  "handshake_complete",
970
972
  "handshake_status",
973
+ "handshake_exchange",
974
+ "handshake_verify_attestation",
971
975
  "reputation_query_weighted",
972
976
  "federation_peers",
973
977
  "federation_trust_evaluate",
@@ -1162,174 +1166,255 @@ function generateLoginHTML(options) {
1162
1166
  return `<!DOCTYPE html>
1163
1167
  <html lang="en">
1164
1168
  <head>
1165
- <meta charset="utf-8">
1166
- <meta name="viewport" content="width=device-width, initial-scale=1">
1167
- <title>Sanctuary \u2014 Login</title>
1168
- <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
1169
- <style>
1170
- :root {
1171
- --bg: #0d1117;
1172
- --surface: #161b22;
1173
- --border: #30363d;
1174
- --text-primary: #e6edf3;
1175
- --text-secondary: #8b949e;
1176
- --green: #3fb950;
1177
- --red: #f85149;
1178
- --blue: #58a6ff;
1179
- --mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
1180
- --sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
1181
- --radius: 6px;
1182
- }
1183
- * { box-sizing: border-box; margin: 0; padding: 0; }
1184
- html, body { width: 100%; height: 100%; }
1185
- body {
1186
- font-family: var(--sans);
1187
- background: var(--bg);
1188
- color: var(--text-primary);
1189
- display: flex;
1190
- align-items: center;
1191
- justify-content: center;
1192
- }
1193
- .login-container {
1194
- width: 100%;
1195
- max-width: 400px;
1196
- padding: 40px 32px;
1197
- background: var(--surface);
1198
- border: 1px solid var(--border);
1199
- border-radius: 12px;
1200
- }
1201
- .login-logo {
1202
- text-align: center;
1203
- font-size: 20px;
1204
- font-weight: 700;
1205
- letter-spacing: -0.5px;
1206
- margin-bottom: 8px;
1207
- }
1208
- .login-logo span { color: var(--blue); }
1209
- .login-version {
1210
- text-align: center;
1211
- font-size: 11px;
1212
- color: var(--text-secondary);
1213
- font-family: var(--mono);
1214
- margin-bottom: 32px;
1215
- }
1216
- .login-label {
1217
- display: block;
1218
- font-size: 13px;
1219
- font-weight: 600;
1220
- color: var(--text-secondary);
1221
- margin-bottom: 8px;
1222
- }
1223
- .login-input {
1224
- width: 100%;
1225
- padding: 10px 14px;
1226
- background: var(--bg);
1227
- border: 1px solid var(--border);
1228
- border-radius: var(--radius);
1229
- color: var(--text-primary);
1230
- font-family: var(--mono);
1231
- font-size: 14px;
1232
- outline: none;
1233
- transition: border-color 0.15s;
1234
- }
1235
- .login-input:focus { border-color: var(--blue); }
1236
- .login-input::placeholder { color: var(--text-secondary); opacity: 0.5; }
1237
- .login-btn {
1238
- width: 100%;
1239
- margin-top: 20px;
1240
- padding: 10px;
1241
- background: var(--blue);
1242
- color: var(--bg);
1243
- border: none;
1244
- border-radius: var(--radius);
1245
- font-size: 14px;
1246
- font-weight: 600;
1247
- cursor: pointer;
1248
- transition: opacity 0.15s;
1249
- font-family: var(--sans);
1250
- }
1251
- .login-btn:hover { opacity: 0.9; }
1252
- .login-btn:disabled { opacity: 0.5; cursor: not-allowed; }
1253
- .login-error {
1254
- margin-top: 16px;
1255
- padding: 10px 14px;
1256
- background: rgba(248, 81, 73, 0.1);
1257
- border: 1px solid var(--red);
1258
- border-radius: var(--radius);
1259
- font-size: 12px;
1260
- color: var(--red);
1261
- display: none;
1262
- }
1263
- .login-hint {
1264
- margin-top: 24px;
1265
- padding-top: 16px;
1266
- border-top: 1px solid var(--border);
1267
- font-size: 11px;
1268
- color: var(--text-secondary);
1269
- line-height: 1.5;
1270
- }
1271
- .login-hint code {
1272
- font-family: var(--mono);
1273
- background: var(--bg);
1274
- padding: 1px 4px;
1275
- border-radius: 3px;
1276
- font-size: 10px;
1277
- }
1278
- </style>
1169
+ <meta charset="UTF-8">
1170
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1171
+ <title>Sanctuary Dashboard</title>
1172
+ <style>
1173
+ :root {
1174
+ --bg: #0d1117;
1175
+ --surface: #161b22;
1176
+ --border: #30363d;
1177
+ --text-primary: #e6edf3;
1178
+ --text-secondary: #8b949e;
1179
+ --green: #3fb950;
1180
+ --amber: #d29922;
1181
+ --red: #f85149;
1182
+ --blue: #58a6ff;
1183
+ }
1184
+
1185
+ * {
1186
+ margin: 0;
1187
+ padding: 0;
1188
+ box-sizing: border-box;
1189
+ }
1190
+
1191
+ body {
1192
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
1193
+ background-color: var(--bg);
1194
+ color: var(--text-primary);
1195
+ min-height: 100vh;
1196
+ display: flex;
1197
+ align-items: center;
1198
+ justify-content: center;
1199
+ }
1200
+
1201
+ .login-container {
1202
+ width: 100%;
1203
+ max-width: 400px;
1204
+ padding: 20px;
1205
+ }
1206
+
1207
+ .login-card {
1208
+ background-color: var(--surface);
1209
+ border: 1px solid var(--border);
1210
+ border-radius: 8px;
1211
+ padding: 40px 32px;
1212
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
1213
+ }
1214
+
1215
+ .login-header {
1216
+ display: flex;
1217
+ align-items: center;
1218
+ gap: 12px;
1219
+ margin-bottom: 32px;
1220
+ }
1221
+
1222
+ .logo {
1223
+ font-size: 24px;
1224
+ font-weight: 700;
1225
+ color: var(--blue);
1226
+ }
1227
+
1228
+ .logo-text {
1229
+ display: flex;
1230
+ flex-direction: column;
1231
+ }
1232
+
1233
+ .logo-text .title {
1234
+ font-size: 18px;
1235
+ font-weight: 600;
1236
+ letter-spacing: -0.5px;
1237
+ }
1238
+
1239
+ .logo-text .version {
1240
+ font-size: 12px;
1241
+ color: var(--text-secondary);
1242
+ margin-top: 2px;
1243
+ }
1244
+
1245
+ .form-group {
1246
+ margin-bottom: 24px;
1247
+ }
1248
+
1249
+ label {
1250
+ display: block;
1251
+ font-size: 14px;
1252
+ font-weight: 500;
1253
+ margin-bottom: 8px;
1254
+ color: var(--text-primary);
1255
+ }
1256
+
1257
+ input[type="text"],
1258
+ input[type="password"] {
1259
+ width: 100%;
1260
+ padding: 10px 12px;
1261
+ background-color: var(--bg);
1262
+ border: 1px solid var(--border);
1263
+ border-radius: 6px;
1264
+ color: var(--text-primary);
1265
+ font-size: 14px;
1266
+ font-family: 'JetBrains Mono', monospace;
1267
+ transition: border-color 0.2s;
1268
+ }
1269
+
1270
+ input[type="text"]:focus,
1271
+ input[type="password"]:focus {
1272
+ outline: none;
1273
+ border-color: var(--blue);
1274
+ box-shadow: 0 0 0 2px rgba(88, 166, 255, 0.1);
1275
+ }
1276
+
1277
+ .error-message {
1278
+ display: none;
1279
+ background-color: rgba(248, 81, 73, 0.1);
1280
+ border: 1px solid var(--red);
1281
+ color: #ff9999;
1282
+ padding: 12px;
1283
+ border-radius: 6px;
1284
+ font-size: 13px;
1285
+ margin-bottom: 20px;
1286
+ }
1287
+
1288
+ .error-message.show {
1289
+ display: block;
1290
+ }
1291
+
1292
+ button {
1293
+ width: 100%;
1294
+ padding: 10px 16px;
1295
+ background-color: var(--blue);
1296
+ color: var(--bg);
1297
+ border: none;
1298
+ border-radius: 6px;
1299
+ font-size: 14px;
1300
+ font-weight: 600;
1301
+ cursor: pointer;
1302
+ transition: background-color 0.2s;
1303
+ }
1304
+
1305
+ button:hover {
1306
+ background-color: #79c0ff;
1307
+ }
1308
+
1309
+ button:active {
1310
+ background-color: #4184e4;
1311
+ }
1312
+
1313
+ button:disabled {
1314
+ background-color: var(--text-secondary);
1315
+ cursor: not-allowed;
1316
+ opacity: 0.5;
1317
+ }
1318
+
1319
+ .info-text {
1320
+ font-size: 12px;
1321
+ color: var(--text-secondary);
1322
+ margin-top: 16px;
1323
+ text-align: center;
1324
+ }
1325
+ </style>
1279
1326
  </head>
1280
1327
  <body>
1281
- <div class="login-container">
1282
- <div class="login-logo"><span>&#9670;</span> SANCTUARY</div>
1283
- <div class="login-version">Principal Dashboard v${options.serverVersion}</div>
1284
- <form id="loginForm" onsubmit="return handleLogin(event)">
1285
- <label class="login-label" for="tokenInput">Dashboard Auth Token</label>
1286
- <input class="login-input" type="password" id="tokenInput"
1287
- placeholder="Enter your auth token" autocomplete="off" autofocus required>
1288
- <button class="login-btn" type="submit" id="loginBtn">Open Dashboard</button>
1289
- </form>
1290
- <div class="login-error" id="loginError"></div>
1291
- <div class="login-hint">
1292
- Your token is set via <code>SANCTUARY_DASHBOARD_AUTH_TOKEN</code> environment variable,
1293
- or check your server's startup output.
1328
+ <div class="login-container">
1329
+ <div class="login-card">
1330
+ <div class="login-header">
1331
+ <div class="logo">\u25C6</div>
1332
+ <div class="logo-text">
1333
+ <div class="title">SANCTUARY</div>
1334
+ <div class="version">v${options.serverVersion}</div>
1335
+ </div>
1336
+ </div>
1337
+
1338
+ <div id="error-message" class="error-message"></div>
1339
+
1340
+ <form id="login-form">
1341
+ <div class="form-group">
1342
+ <label for="auth-token">Bearer Token</label>
1343
+ <input
1344
+ type="text"
1345
+ id="auth-token"
1346
+ name="token"
1347
+ placeholder="Paste your session token..."
1348
+ autocomplete="off"
1349
+ spellcheck="false"
1350
+ required
1351
+ />
1352
+ </div>
1353
+
1354
+ <button type="submit" id="login-button">Open Dashboard</button>
1355
+ </form>
1356
+
1357
+ <div class="info-text">
1358
+ Session tokens expire after 1 hour of inactivity
1359
+ </div>
1360
+ </div>
1294
1361
  </div>
1295
- </div>
1296
- <script>
1297
- async function handleLogin(e) {
1298
- e.preventDefault();
1299
- var btn = document.getElementById('loginBtn');
1300
- var errEl = document.getElementById('loginError');
1301
- var token = document.getElementById('tokenInput').value.trim();
1302
- if (!token) return false;
1303
- btn.disabled = true;
1304
- btn.textContent = 'Authenticating...';
1305
- errEl.style.display = 'none';
1306
- try {
1307
- var resp = await fetch('/auth/session', {
1308
- method: 'POST',
1309
- headers: { 'Authorization': 'Bearer ' + token }
1362
+
1363
+ <script>
1364
+ const loginForm = document.getElementById('login-form');
1365
+ const authTokenInput = document.getElementById('auth-token');
1366
+ const errorMessage = document.getElementById('error-message');
1367
+ const loginButton = document.getElementById('login-button');
1368
+
1369
+ loginForm.addEventListener('submit', async (e) => {
1370
+ e.preventDefault();
1371
+ const token = authTokenInput.value.trim();
1372
+
1373
+ if (!token) {
1374
+ showError('Token is required');
1375
+ return;
1376
+ }
1377
+
1378
+ loginButton.disabled = true;
1379
+ loginButton.textContent = 'Verifying...';
1380
+ errorMessage.classList.remove('show');
1381
+
1382
+ try {
1383
+ const response = await fetch('/auth/session', {
1384
+ method: 'POST',
1385
+ headers: {
1386
+ 'Content-Type': 'application/json',
1387
+ 'Authorization': 'Bearer ' + token,
1388
+ },
1389
+ body: JSON.stringify({ token }),
1390
+ });
1391
+
1392
+ if (response.ok) {
1393
+ const data = await response.json();
1394
+ sessionStorage.setItem('authToken', token);
1395
+ window.location.href = '/dashboard';
1396
+ } else if (response.status === 401) {
1397
+ showError('Invalid token. Please check and try again.');
1398
+ } else {
1399
+ showError('Authentication failed. Please try again.');
1400
+ }
1401
+ } catch (err) {
1402
+ showError('Connection error. Please check your network.');
1403
+ } finally {
1404
+ loginButton.disabled = false;
1405
+ loginButton.textContent = 'Open Dashboard';
1406
+ }
1310
1407
  });
1311
- if (!resp.ok) {
1312
- var data = await resp.json().catch(function() { return {}; });
1313
- throw new Error(data.error || 'Authentication failed');
1314
- }
1315
- var result = await resp.json();
1316
- // Store token in sessionStorage for auto-renewal inside the dashboard
1317
- try { sessionStorage.setItem('sanctuary_token', token); } catch(_) {}
1318
- // Set session cookie
1319
- var maxAge = result.expires_in_seconds || 300;
1320
- document.cookie = 'sanctuary_session=' + result.session_id +
1321
- '; path=/; SameSite=Strict; max-age=' + maxAge;
1322
- // Reload to enter the dashboard
1323
- window.location.reload();
1324
- } catch (err) {
1325
- errEl.textContent = err.message || 'Authentication failed. Check your token.';
1326
- errEl.style.display = 'block';
1327
- btn.disabled = false;
1328
- btn.textContent = 'Open Dashboard';
1329
- }
1330
- return false;
1331
- }
1332
- </script>
1408
+
1409
+ function showError(message) {
1410
+ errorMessage.textContent = message;
1411
+ errorMessage.classList.add('show');
1412
+ }
1413
+
1414
+ authTokenInput.addEventListener('input', () => {
1415
+ errorMessage.classList.remove('show');
1416
+ });
1417
+ </script>
1333
1418
  </body>
1334
1419
  </html>`;
1335
1420
  }
@@ -1337,1412 +1422,1648 @@ function generateDashboardHTML(options) {
1337
1422
  return `<!DOCTYPE html>
1338
1423
  <html lang="en">
1339
1424
  <head>
1340
- <meta charset="utf-8">
1341
- <meta name="viewport" content="width=device-width, initial-scale=1">
1342
- <title>Sanctuary \u2014 Principal Dashboard</title>
1343
- <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
1344
- <style>
1345
- :root {
1346
- --bg: #0d1117;
1347
- --surface: #161b22;
1348
- --border: #30363d;
1349
- --text-primary: #e6edf3;
1350
- --text-secondary: #8b949e;
1351
- --green: #3fb950;
1352
- --amber: #d29922;
1353
- --red: #f85149;
1354
- --blue: #58a6ff;
1355
- --mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
1356
- --sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
1357
- --radius: 6px;
1358
- }
1359
-
1360
- * {
1361
- box-sizing: border-box;
1362
- margin: 0;
1363
- padding: 0;
1364
- }
1425
+ <meta charset="UTF-8">
1426
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1427
+ <title>Sanctuary Dashboard</title>
1428
+ <style>
1429
+ :root {
1430
+ --bg: #0d1117;
1431
+ --surface: #161b22;
1432
+ --border: #30363d;
1433
+ --text-primary: #e6edf3;
1434
+ --text-secondary: #8b949e;
1435
+ --green: #3fb950;
1436
+ --amber: #d29922;
1437
+ --red: #f85149;
1438
+ --blue: #58a6ff;
1439
+ --success: #3fb950;
1440
+ --warning: #d29922;
1441
+ --error: #f85149;
1442
+ --muted: #21262d;
1443
+ }
1444
+
1445
+ * {
1446
+ margin: 0;
1447
+ padding: 0;
1448
+ box-sizing: border-box;
1449
+ }
1450
+
1451
+ html, body {
1452
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
1453
+ background-color: var(--bg);
1454
+ color: var(--text-primary);
1455
+ height: 100%;
1456
+ overflow: hidden;
1457
+ }
1458
+
1459
+ body {
1460
+ display: flex;
1461
+ flex-direction: column;
1462
+ }
1463
+
1464
+ /* Status Bar */
1465
+ .status-bar {
1466
+ position: fixed;
1467
+ top: 0;
1468
+ left: 0;
1469
+ right: 0;
1470
+ height: 56px;
1471
+ background-color: var(--surface);
1472
+ border-bottom: 1px solid var(--border);
1473
+ display: flex;
1474
+ align-items: center;
1475
+ padding: 0 24px;
1476
+ gap: 24px;
1477
+ z-index: 100;
1478
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
1479
+ }
1480
+
1481
+ .status-bar-left {
1482
+ display: flex;
1483
+ align-items: center;
1484
+ gap: 12px;
1485
+ flex: 0 0 auto;
1486
+ }
1365
1487
 
1366
- html, body {
1367
- width: 100%;
1368
- height: 100%;
1369
- overflow: hidden;
1370
- }
1371
-
1372
- body {
1373
- font-family: var(--sans);
1374
- background: var(--bg);
1375
- color: var(--text-primary);
1376
- display: flex;
1377
- flex-direction: column;
1378
- }
1379
-
1380
- /* \u2500\u2500 Top Status Bar (fixed) \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 */
1488
+ .logo-icon {
1489
+ font-size: 20px;
1490
+ color: var(--blue);
1491
+ font-weight: 700;
1492
+ }
1381
1493
 
1382
- .status-bar {
1383
- position: fixed;
1384
- top: 0;
1385
- left: 0;
1386
- right: 0;
1387
- height: 56px;
1388
- background: var(--surface);
1389
- border-bottom: 1px solid var(--border);
1390
- display: flex;
1391
- align-items: center;
1392
- padding: 0 20px;
1393
- gap: 24px;
1394
- z-index: 1000;
1395
- }
1494
+ .logo-info {
1495
+ display: flex;
1496
+ flex-direction: column;
1497
+ }
1396
1498
 
1397
- .status-bar-left {
1398
- display: flex;
1399
- align-items: center;
1400
- gap: 12px;
1401
- flex: 0 0 auto;
1402
- }
1499
+ .logo-title {
1500
+ font-size: 13px;
1501
+ font-weight: 600;
1502
+ line-height: 1;
1503
+ color: var(--text-primary);
1504
+ }
1403
1505
 
1404
- .sanctuary-logo {
1405
- font-weight: 700;
1406
- font-size: 16px;
1407
- letter-spacing: -0.5px;
1408
- color: var(--text-primary);
1409
- }
1506
+ .logo-version {
1507
+ font-size: 11px;
1508
+ color: var(--text-secondary);
1509
+ margin-top: 2px;
1510
+ }
1410
1511
 
1411
- .sanctuary-logo span {
1412
- color: var(--blue);
1413
- }
1512
+ .status-bar-center {
1513
+ flex: 1;
1514
+ display: flex;
1515
+ justify-content: center;
1516
+ }
1414
1517
 
1415
- .version {
1416
- font-size: 11px;
1417
- color: var(--text-secondary);
1418
- font-family: var(--mono);
1419
- }
1518
+ .sovereignty-badge {
1519
+ display: flex;
1520
+ align-items: center;
1521
+ gap: 8px;
1522
+ padding: 8px 16px;
1523
+ background-color: rgba(63, 185, 80, 0.1);
1524
+ border: 1px solid rgba(63, 185, 80, 0.3);
1525
+ border-radius: 6px;
1526
+ font-size: 13px;
1527
+ font-weight: 500;
1528
+ }
1420
1529
 
1421
- .status-bar-center {
1422
- flex: 1;
1423
- display: flex;
1424
- align-items: center;
1425
- justify-content: center;
1426
- }
1427
-
1428
- .sovereignty-badge {
1429
- display: flex;
1430
- align-items: center;
1431
- gap: 8px;
1432
- padding: 6px 12px;
1433
- background: rgba(88, 166, 255, 0.1);
1434
- border: 1px solid var(--blue);
1435
- border-radius: 20px;
1436
- font-size: 13px;
1437
- font-weight: 600;
1438
- }
1530
+ .sovereignty-badge.degraded {
1531
+ background-color: rgba(210, 153, 34, 0.1);
1532
+ border-color: rgba(210, 153, 34, 0.3);
1533
+ }
1439
1534
 
1440
- .sovereignty-score {
1441
- display: flex;
1442
- align-items: center;
1443
- justify-content: center;
1444
- width: 28px;
1445
- height: 28px;
1446
- border-radius: 50%;
1447
- font-family: var(--mono);
1448
- font-weight: 700;
1449
- font-size: 12px;
1450
- background: var(--blue);
1451
- color: var(--bg);
1452
- }
1535
+ .sovereignty-badge.inactive {
1536
+ background-color: rgba(248, 81, 73, 0.1);
1537
+ border-color: rgba(248, 81, 73, 0.3);
1538
+ }
1453
1539
 
1454
- .sovereignty-score.high {
1455
- background: var(--green);
1456
- }
1540
+ .sovereignty-score {
1541
+ font-weight: 700;
1542
+ color: var(--green);
1543
+ }
1457
1544
 
1458
- .sovereignty-score.medium {
1459
- background: var(--amber);
1460
- }
1545
+ .sovereignty-badge.degraded .sovereignty-score {
1546
+ color: var(--amber);
1547
+ }
1461
1548
 
1462
- .sovereignty-score.low {
1463
- background: var(--red);
1464
- }
1549
+ .sovereignty-badge.inactive .sovereignty-score {
1550
+ color: var(--red);
1551
+ }
1465
1552
 
1466
- .status-bar-right {
1467
- display: flex;
1468
- align-items: center;
1469
- gap: 16px;
1470
- flex: 0 0 auto;
1471
- }
1553
+ .status-bar-right {
1554
+ display: flex;
1555
+ align-items: center;
1556
+ gap: 16px;
1557
+ flex: 0 0 auto;
1558
+ }
1472
1559
 
1473
- .protections-indicator {
1474
- display: flex;
1475
- align-items: center;
1476
- gap: 6px;
1477
- font-size: 12px;
1478
- color: var(--text-secondary);
1479
- font-family: var(--mono);
1480
- }
1560
+ .status-item {
1561
+ display: flex;
1562
+ align-items: center;
1563
+ gap: 6px;
1564
+ font-size: 12px;
1565
+ color: var(--text-secondary);
1566
+ }
1481
1567
 
1482
- .protections-indicator .count {
1483
- color: var(--text-primary);
1484
- font-weight: 600;
1485
- }
1568
+ .status-item strong {
1569
+ color: var(--text-primary);
1570
+ font-weight: 500;
1571
+ }
1486
1572
 
1487
- .uptime {
1488
- display: flex;
1489
- align-items: center;
1490
- gap: 6px;
1491
- font-size: 12px;
1492
- color: var(--text-secondary);
1493
- font-family: var(--mono);
1494
- }
1573
+ .status-dot {
1574
+ width: 8px;
1575
+ height: 8px;
1576
+ border-radius: 50%;
1577
+ background-color: var(--green);
1578
+ }
1495
1579
 
1496
- .status-dot {
1497
- width: 8px;
1498
- height: 8px;
1499
- border-radius: 50%;
1500
- background: var(--green);
1501
- animation: pulse 2s ease-in-out infinite;
1502
- }
1580
+ .status-dot.disconnected {
1581
+ background-color: var(--red);
1582
+ }
1503
1583
 
1504
- .status-dot.disconnected {
1505
- background: var(--red);
1506
- animation: none;
1507
- }
1584
+ .pending-badge {
1585
+ display: flex;
1586
+ align-items: center;
1587
+ gap: 6px;
1588
+ padding: 4px 8px;
1589
+ background-color: var(--blue);
1590
+ color: var(--bg);
1591
+ border-radius: 4px;
1592
+ font-size: 11px;
1593
+ font-weight: 600;
1594
+ cursor: pointer;
1595
+ }
1508
1596
 
1509
- @keyframes pulse {
1510
- 0%, 100% { opacity: 1; }
1511
- 50% { opacity: 0.5; }
1512
- }
1597
+ /* Main Content */
1598
+ .main-content {
1599
+ flex: 1;
1600
+ margin-top: 56px;
1601
+ overflow-y: auto;
1602
+ padding: 24px;
1603
+ }
1513
1604
 
1514
- .pending-badge {
1515
- display: inline-flex;
1516
- align-items: center;
1517
- justify-content: center;
1518
- min-width: 24px;
1519
- height: 24px;
1520
- padding: 0 6px;
1521
- background: var(--red);
1522
- color: white;
1523
- border-radius: 12px;
1524
- font-size: 11px;
1525
- font-weight: 700;
1526
- animation: pulse 1s ease-in-out infinite;
1527
- }
1605
+ .grid {
1606
+ display: grid;
1607
+ gap: 20px;
1608
+ }
1528
1609
 
1529
- .pending-badge.hidden {
1530
- display: none;
1531
- }
1610
+ /* Row 1: Sovereignty Layers */
1611
+ .sovereignty-layers {
1612
+ display: grid;
1613
+ grid-template-columns: repeat(4, 1fr);
1614
+ gap: 16px;
1615
+ }
1532
1616
 
1533
- /* \u2500\u2500 Main Layout \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\u2500 */
1617
+ .layer-card {
1618
+ background-color: var(--surface);
1619
+ border: 1px solid var(--border);
1620
+ border-radius: 8px;
1621
+ padding: 20px;
1622
+ display: flex;
1623
+ flex-direction: column;
1624
+ gap: 12px;
1625
+ }
1534
1626
 
1535
- .main-container {
1536
- flex: 1;
1537
- display: flex;
1538
- margin-top: 56px;
1539
- overflow: hidden;
1540
- }
1627
+ .layer-card.degraded {
1628
+ border-color: var(--amber);
1629
+ background-color: rgba(210, 153, 34, 0.05);
1630
+ }
1541
1631
 
1542
- .activity-feed {
1543
- flex: 3;
1544
- display: flex;
1545
- flex-direction: column;
1546
- border-right: 1px solid var(--border);
1547
- overflow: hidden;
1548
- }
1632
+ .layer-card.inactive {
1633
+ border-color: var(--red);
1634
+ background-color: rgba(248, 81, 73, 0.05);
1635
+ }
1549
1636
 
1550
- .feed-header {
1551
- padding: 16px 20px;
1552
- border-bottom: 1px solid var(--border);
1553
- display: flex;
1554
- align-items: center;
1555
- gap: 8px;
1556
- font-size: 12px;
1557
- font-weight: 600;
1558
- text-transform: uppercase;
1559
- letter-spacing: 0.5px;
1560
- color: var(--text-secondary);
1561
- }
1637
+ .layer-name {
1638
+ font-size: 12px;
1639
+ font-weight: 600;
1640
+ color: var(--text-secondary);
1641
+ text-transform: uppercase;
1642
+ letter-spacing: 0.5px;
1643
+ }
1562
1644
 
1563
- .feed-header-dot {
1564
- width: 6px;
1565
- height: 6px;
1566
- border-radius: 50%;
1567
- background: var(--green);
1568
- }
1645
+ .layer-title {
1646
+ font-size: 14px;
1647
+ font-weight: 600;
1648
+ color: var(--text-primary);
1649
+ }
1569
1650
 
1570
- .activity-list {
1571
- flex: 1;
1572
- overflow-y: auto;
1573
- overflow-x: hidden;
1574
- }
1651
+ .layer-status {
1652
+ display: inline-flex;
1653
+ align-items: center;
1654
+ gap: 6px;
1655
+ padding: 4px 8px;
1656
+ background-color: rgba(63, 185, 80, 0.15);
1657
+ color: var(--green);
1658
+ border-radius: 4px;
1659
+ font-size: 11px;
1660
+ font-weight: 600;
1661
+ width: fit-content;
1662
+ }
1575
1663
 
1576
- .activity-item {
1577
- padding: 12px 20px;
1578
- border-bottom: 1px solid rgba(48, 54, 61, 0.5);
1579
- font-size: 13px;
1580
- font-family: var(--mono);
1581
- cursor: pointer;
1582
- transition: background 0.15s;
1583
- display: flex;
1584
- align-items: flex-start;
1585
- gap: 10px;
1586
- }
1664
+ .layer-card.degraded .layer-status {
1665
+ background-color: rgba(210, 153, 34, 0.15);
1666
+ color: var(--amber);
1667
+ }
1587
1668
 
1588
- .activity-item:hover {
1589
- background: rgba(88, 166, 255, 0.05);
1590
- }
1669
+ .layer-card.inactive .layer-status {
1670
+ background-color: rgba(248, 81, 73, 0.15);
1671
+ color: var(--red);
1672
+ }
1591
1673
 
1592
- .activity-item-icon {
1593
- flex: 0 0 auto;
1594
- width: 16px;
1595
- text-align: center;
1596
- font-size: 12px;
1597
- color: var(--text-secondary);
1598
- margin-top: 1px;
1599
- }
1674
+ .layer-detail {
1675
+ font-size: 12px;
1676
+ color: var(--text-secondary);
1677
+ font-family: 'JetBrains Mono', monospace;
1678
+ padding: 8px;
1679
+ background-color: var(--bg);
1680
+ border-radius: 4px;
1681
+ border-left: 2px solid var(--blue);
1682
+ }
1600
1683
 
1601
- .activity-item-content {
1602
- flex: 1;
1603
- min-width: 0;
1604
- }
1684
+ /* Row 2: Info Cards */
1685
+ .info-cards {
1686
+ display: grid;
1687
+ grid-template-columns: repeat(3, 1fr);
1688
+ gap: 16px;
1689
+ }
1605
1690
 
1606
- .activity-time {
1607
- color: var(--text-secondary);
1608
- font-size: 11px;
1609
- margin-bottom: 2px;
1610
- }
1691
+ .info-card {
1692
+ background-color: var(--surface);
1693
+ border: 1px solid var(--border);
1694
+ border-radius: 8px;
1695
+ padding: 20px;
1696
+ }
1611
1697
 
1612
- .activity-main {
1613
- display: flex;
1614
- gap: 8px;
1615
- align-items: baseline;
1616
- margin-bottom: 4px;
1617
- }
1698
+ .card-header {
1699
+ font-size: 12px;
1700
+ font-weight: 600;
1701
+ color: var(--text-secondary);
1702
+ text-transform: uppercase;
1703
+ letter-spacing: 0.5px;
1704
+ margin-bottom: 16px;
1705
+ }
1618
1706
 
1619
- .activity-tier {
1620
- display: inline-flex;
1621
- align-items: center;
1622
- justify-content: center;
1623
- width: 24px;
1624
- height: 16px;
1625
- font-size: 10px;
1626
- font-weight: 700;
1627
- border-radius: 3px;
1628
- text-transform: uppercase;
1629
- flex: 0 0 auto;
1630
- }
1707
+ .card-row {
1708
+ display: flex;
1709
+ justify-content: space-between;
1710
+ align-items: center;
1711
+ margin-bottom: 12px;
1712
+ font-size: 13px;
1713
+ }
1631
1714
 
1632
- .activity-tier.t1 {
1633
- background: rgba(248, 81, 73, 0.2);
1634
- color: var(--red);
1635
- }
1715
+ .card-row:last-child {
1716
+ margin-bottom: 0;
1717
+ }
1636
1718
 
1637
- .activity-tier.t2 {
1638
- background: rgba(210, 153, 34, 0.2);
1639
- color: var(--amber);
1640
- }
1719
+ .card-label {
1720
+ color: var(--text-secondary);
1721
+ }
1641
1722
 
1642
- .activity-tier.t3 {
1643
- background: rgba(63, 185, 80, 0.2);
1644
- color: var(--green);
1645
- }
1723
+ .card-value {
1724
+ color: var(--text-primary);
1725
+ font-family: 'JetBrains Mono', monospace;
1726
+ font-weight: 500;
1727
+ }
1646
1728
 
1647
- .activity-tool {
1648
- color: var(--text-primary);
1649
- font-weight: 600;
1650
- }
1729
+ .identity-badge {
1730
+ display: inline-flex;
1731
+ align-items: center;
1732
+ gap: 4px;
1733
+ padding: 2px 6px;
1734
+ background-color: rgba(88, 166, 255, 0.15);
1735
+ color: var(--blue);
1736
+ border-radius: 3px;
1737
+ font-size: 10px;
1738
+ font-weight: 600;
1739
+ text-transform: uppercase;
1740
+ }
1651
1741
 
1652
- .activity-outcome {
1653
- color: var(--green);
1654
- }
1742
+ .trust-tier-badge {
1743
+ display: inline-flex;
1744
+ align-items: center;
1745
+ gap: 4px;
1746
+ padding: 2px 6px;
1747
+ background-color: rgba(63, 185, 80, 0.15);
1748
+ color: var(--green);
1749
+ border-radius: 3px;
1750
+ font-size: 10px;
1751
+ font-weight: 600;
1752
+ }
1655
1753
 
1656
- .activity-outcome.denied {
1657
- color: var(--red);
1658
- }
1754
+ .truncated {
1755
+ max-width: 200px;
1756
+ overflow: hidden;
1757
+ text-overflow: ellipsis;
1758
+ white-space: nowrap;
1759
+ }
1659
1760
 
1660
- .activity-detail {
1661
- font-size: 12px;
1662
- color: var(--text-secondary);
1663
- margin-left: 0;
1664
- }
1761
+ /* Row 3: SHR & Activity */
1762
+ .main-panels {
1763
+ display: grid;
1764
+ grid-template-columns: 1fr 1fr;
1765
+ gap: 16px;
1766
+ min-height: 400px;
1767
+ }
1665
1768
 
1666
- .activity-item.expanded .activity-detail {
1667
- display: block;
1668
- margin-top: 8px;
1669
- padding: 10px;
1670
- background: rgba(88, 166, 255, 0.08);
1671
- border-left: 2px solid var(--blue);
1672
- border-radius: 4px;
1673
- }
1769
+ .panel {
1770
+ background-color: var(--surface);
1771
+ border: 1px solid var(--border);
1772
+ border-radius: 8px;
1773
+ display: flex;
1774
+ flex-direction: column;
1775
+ overflow: hidden;
1776
+ }
1674
1777
 
1675
- .activity-empty {
1676
- display: flex;
1677
- flex-direction: column;
1678
- align-items: center;
1679
- justify-content: center;
1680
- height: 100%;
1681
- color: var(--text-secondary);
1682
- }
1778
+ .panel-header {
1779
+ padding: 16px 20px;
1780
+ border-bottom: 1px solid var(--border);
1781
+ display: flex;
1782
+ justify-content: space-between;
1783
+ align-items: center;
1784
+ }
1683
1785
 
1684
- .activity-empty-icon {
1685
- font-size: 32px;
1686
- margin-bottom: 12px;
1687
- }
1786
+ .panel-title {
1787
+ font-size: 14px;
1788
+ font-weight: 600;
1789
+ color: var(--text-primary);
1790
+ }
1688
1791
 
1689
- .activity-empty-text {
1690
- font-size: 14px;
1691
- }
1792
+ .panel-action {
1793
+ background: none;
1794
+ border: none;
1795
+ color: var(--blue);
1796
+ cursor: pointer;
1797
+ font-size: 12px;
1798
+ padding: 0;
1799
+ font-weight: 500;
1800
+ transition: color 0.2s;
1801
+ }
1692
1802
 
1693
- /* \u2500\u2500 Protection Status Sidebar (40%) \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 */
1803
+ .panel-action:hover {
1804
+ color: #79c0ff;
1805
+ }
1694
1806
 
1695
- .protection-sidebar {
1696
- flex: 2;
1697
- display: flex;
1698
- flex-direction: column;
1699
- background: rgba(22, 27, 34, 0.5);
1700
- overflow: hidden;
1701
- }
1807
+ .panel-content {
1808
+ flex: 1;
1809
+ overflow-y: auto;
1810
+ padding: 20px;
1811
+ }
1702
1812
 
1703
- .sidebar-header {
1704
- padding: 16px 20px;
1705
- border-bottom: 1px solid var(--border);
1706
- font-size: 12px;
1707
- font-weight: 600;
1708
- text-transform: uppercase;
1709
- letter-spacing: 0.5px;
1710
- color: var(--text-secondary);
1711
- display: flex;
1712
- align-items: center;
1713
- gap: 8px;
1714
- }
1813
+ /* SHR Viewer */
1814
+ .shr-json {
1815
+ font-family: 'JetBrains Mono', monospace;
1816
+ font-size: 12px;
1817
+ line-height: 1.6;
1818
+ color: var(--text-secondary);
1819
+ }
1715
1820
 
1716
- .sidebar-content {
1717
- flex: 1;
1718
- overflow-y: auto;
1719
- padding: 16px 16px;
1720
- display: grid;
1721
- grid-template-columns: 1fr 1fr;
1722
- gap: 12px;
1723
- }
1821
+ .shr-section {
1822
+ margin-bottom: 12px;
1823
+ }
1724
1824
 
1725
- .protection-card {
1726
- background: var(--surface);
1727
- border: 1px solid var(--border);
1728
- border-radius: var(--radius);
1729
- padding: 14px;
1730
- display: flex;
1731
- flex-direction: column;
1732
- gap: 8px;
1733
- }
1825
+ .shr-section-header {
1826
+ display: flex;
1827
+ align-items: center;
1828
+ gap: 8px;
1829
+ cursor: pointer;
1830
+ font-weight: 600;
1831
+ color: var(--text-primary);
1832
+ padding: 8px;
1833
+ background-color: var(--bg);
1834
+ border-radius: 4px;
1835
+ user-select: none;
1836
+ }
1734
1837
 
1735
- .protection-card-icon {
1736
- font-size: 14px;
1737
- }
1838
+ .shr-section-header:hover {
1839
+ background-color: var(--muted);
1840
+ }
1738
1841
 
1739
- .protection-card-label {
1740
- font-size: 11px;
1741
- font-weight: 600;
1742
- text-transform: uppercase;
1743
- letter-spacing: 0.5px;
1744
- color: var(--text-secondary);
1745
- }
1842
+ .shr-toggle {
1843
+ width: 16px;
1844
+ height: 16px;
1845
+ display: flex;
1846
+ align-items: center;
1847
+ justify-content: center;
1848
+ font-size: 10px;
1849
+ transition: transform 0.2s;
1850
+ }
1746
1851
 
1747
- .protection-card-status {
1748
- display: flex;
1749
- align-items: center;
1750
- gap: 6px;
1751
- font-size: 12px;
1752
- font-weight: 600;
1753
- }
1852
+ .shr-section.collapsed .shr-toggle {
1853
+ transform: rotate(-90deg);
1854
+ }
1754
1855
 
1755
- .protection-card-status.active {
1756
- color: var(--green);
1757
- }
1856
+ .shr-section-content {
1857
+ padding: 8px 16px;
1858
+ background-color: rgba(0, 0, 0, 0.2);
1859
+ border-radius: 4px;
1860
+ margin-top: 4px;
1861
+ }
1758
1862
 
1759
- .protection-card-status.inactive {
1760
- color: var(--text-secondary);
1761
- }
1863
+ .shr-section.collapsed .shr-section-content {
1864
+ display: none;
1865
+ }
1762
1866
 
1763
- .protection-card-stat {
1764
- font-size: 11px;
1765
- color: var(--text-secondary);
1766
- font-family: var(--mono);
1767
- margin-top: 4px;
1768
- }
1867
+ .shr-item {
1868
+ display: flex;
1869
+ margin-bottom: 4px;
1870
+ }
1769
1871
 
1770
- /* \u2500\u2500 Pending Approvals Overlay \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 */
1872
+ .shr-key {
1873
+ color: var(--blue);
1874
+ margin-right: 8px;
1875
+ min-width: 120px;
1876
+ }
1771
1877
 
1772
- .pending-overlay {
1773
- position: fixed;
1774
- top: 56px;
1775
- right: 0;
1776
- bottom: 0;
1777
- width: 0;
1778
- background: var(--surface);
1779
- border-left: 1px solid var(--border);
1780
- z-index: 999;
1781
- overflow-y: auto;
1782
- transition: width 0.3s ease-out;
1783
- display: flex;
1784
- flex-direction: column;
1785
- }
1878
+ .shr-value {
1879
+ color: var(--green);
1880
+ word-break: break-all;
1881
+ }
1786
1882
 
1787
- .pending-overlay.active {
1788
- width: 380px;
1789
- }
1883
+ /* Activity Feed */
1884
+ .activity-feed {
1885
+ display: flex;
1886
+ flex-direction: column;
1887
+ gap: 12px;
1888
+ }
1790
1889
 
1791
- @media (max-width: 1400px) {
1792
- .pending-overlay.active {
1793
- width: 100%;
1794
- right: auto;
1795
- left: 0;
1890
+ .activity-item {
1891
+ padding: 12px;
1892
+ background-color: var(--bg);
1893
+ border-left: 2px solid var(--border);
1894
+ border-radius: 4px;
1895
+ font-size: 12px;
1796
1896
  }
1797
- }
1798
1897
 
1799
- .pending-overlay-header {
1800
- padding: 16px 20px;
1801
- border-bottom: 1px solid var(--border);
1802
- display: flex;
1803
- align-items: center;
1804
- justify-content: space-between;
1805
- flex: 0 0 auto;
1806
- }
1898
+ .activity-item.tool-call {
1899
+ border-left-color: var(--blue);
1900
+ }
1807
1901
 
1808
- .pending-overlay-title {
1809
- font-size: 13px;
1810
- font-weight: 600;
1811
- text-transform: uppercase;
1812
- letter-spacing: 0.5px;
1813
- color: var(--text-primary);
1814
- }
1902
+ .activity-item.context-gate {
1903
+ border-left-color: var(--amber);
1904
+ }
1815
1905
 
1816
- .pending-overlay-close {
1817
- background: none;
1818
- border: none;
1819
- color: var(--text-secondary);
1820
- cursor: pointer;
1821
- font-size: 18px;
1822
- padding: 0;
1823
- display: flex;
1824
- align-items: center;
1825
- justify-content: center;
1826
- }
1906
+ .activity-item.injection {
1907
+ border-left-color: var(--red);
1908
+ }
1827
1909
 
1828
- .pending-overlay-close:hover {
1829
- color: var(--text-primary);
1830
- }
1910
+ .activity-item.protection {
1911
+ border-left-color: var(--green);
1912
+ }
1831
1913
 
1832
- .pending-list {
1833
- flex: 1;
1834
- overflow-y: auto;
1835
- }
1914
+ .activity-type {
1915
+ font-weight: 600;
1916
+ color: var(--text-primary);
1917
+ margin-bottom: 4px;
1918
+ text-transform: uppercase;
1919
+ font-size: 11px;
1920
+ letter-spacing: 0.5px;
1921
+ }
1836
1922
 
1837
- .pending-item {
1838
- padding: 16px 20px;
1839
- border-bottom: 1px solid rgba(48, 54, 61, 0.5);
1840
- display: flex;
1841
- flex-direction: column;
1842
- gap: 10px;
1843
- }
1923
+ .activity-content {
1924
+ color: var(--text-secondary);
1925
+ font-family: 'JetBrains Mono', monospace;
1926
+ margin-bottom: 4px;
1927
+ word-break: break-all;
1928
+ }
1844
1929
 
1845
- .pending-item-header {
1846
- display: flex;
1847
- align-items: center;
1848
- gap: 8px;
1849
- }
1930
+ .activity-time {
1931
+ font-size: 11px;
1932
+ color: var(--text-secondary);
1933
+ }
1850
1934
 
1851
- .pending-item-op {
1852
- font-family: var(--mono);
1853
- font-size: 12px;
1854
- font-weight: 600;
1855
- color: var(--text-primary);
1856
- flex: 1;
1857
- }
1935
+ .empty-state {
1936
+ display: flex;
1937
+ align-items: center;
1938
+ justify-content: center;
1939
+ height: 100%;
1940
+ color: var(--text-secondary);
1941
+ font-size: 13px;
1942
+ }
1858
1943
 
1859
- .pending-item-tier {
1860
- display: inline-flex;
1861
- align-items: center;
1862
- justify-content: center;
1863
- width: 28px;
1864
- height: 20px;
1865
- font-size: 9px;
1866
- font-weight: 700;
1867
- border-radius: 3px;
1868
- text-transform: uppercase;
1869
- color: white;
1870
- }
1944
+ /* Row 4: Handshake History */
1945
+ .handshake-table {
1946
+ background-color: var(--surface);
1947
+ border: 1px solid var(--border);
1948
+ border-radius: 8px;
1949
+ overflow: hidden;
1950
+ }
1871
1951
 
1872
- .pending-item-tier.tier1 {
1873
- background: var(--red);
1874
- }
1952
+ .table-header {
1953
+ display: grid;
1954
+ grid-template-columns: 2fr 1fr 1fr 1fr 1.5fr 1.5fr;
1955
+ gap: 16px;
1956
+ padding: 16px 20px;
1957
+ border-bottom: 1px solid var(--border);
1958
+ background-color: var(--bg);
1959
+ font-size: 12px;
1960
+ font-weight: 600;
1961
+ color: var(--text-secondary);
1962
+ text-transform: uppercase;
1963
+ letter-spacing: 0.5px;
1964
+ }
1875
1965
 
1876
- .pending-item-tier.tier2 {
1877
- background: var(--amber);
1878
- }
1966
+ .table-rows {
1967
+ max-height: 300px;
1968
+ overflow-y: auto;
1969
+ }
1879
1970
 
1880
- .pending-item-reason {
1881
- font-size: 12px;
1882
- color: var(--text-secondary);
1883
- }
1971
+ .table-row {
1972
+ display: grid;
1973
+ grid-template-columns: 2fr 1fr 1fr 1fr 1.5fr 1.5fr;
1974
+ gap: 16px;
1975
+ padding: 14px 20px;
1976
+ border-bottom: 1px solid var(--border);
1977
+ align-items: center;
1978
+ font-size: 12px;
1979
+ cursor: pointer;
1980
+ transition: background-color 0.2s;
1981
+ }
1884
1982
 
1885
- .pending-item-timer {
1886
- display: flex;
1887
- align-items: center;
1888
- gap: 6px;
1889
- font-size: 11px;
1890
- font-family: var(--mono);
1891
- color: var(--text-secondary);
1892
- }
1983
+ .table-row:hover {
1984
+ background-color: var(--bg);
1985
+ }
1893
1986
 
1894
- .pending-item-timer-bar {
1895
- flex: 1;
1896
- height: 4px;
1897
- background: rgba(48, 54, 61, 0.8);
1898
- border-radius: 2px;
1899
- overflow: hidden;
1900
- }
1987
+ .table-row:last-child {
1988
+ border-bottom: none;
1989
+ }
1901
1990
 
1902
- .pending-item-timer-fill {
1903
- height: 100%;
1904
- background: var(--blue);
1905
- transition: width 0.1s linear;
1906
- }
1991
+ .table-cell {
1992
+ color: var(--text-secondary);
1993
+ font-family: 'JetBrains Mono', monospace;
1994
+ }
1907
1995
 
1908
- .pending-item-timer.urgent .pending-item-timer-fill {
1909
- background: var(--red);
1910
- }
1996
+ .table-cell.strong {
1997
+ color: var(--text-primary);
1998
+ font-weight: 500;
1999
+ }
1911
2000
 
1912
- .pending-item-actions {
1913
- display: flex;
1914
- gap: 8px;
1915
- }
2001
+ .table-empty {
2002
+ padding: 40px 20px;
2003
+ text-align: center;
2004
+ color: var(--text-secondary);
2005
+ font-size: 13px;
2006
+ }
1916
2007
 
1917
- .btn {
1918
- flex: 1;
1919
- padding: 8px 12px;
1920
- border: none;
1921
- border-radius: var(--radius);
1922
- font-size: 12px;
1923
- font-weight: 600;
1924
- cursor: pointer;
1925
- transition: all 0.15s;
1926
- font-family: var(--sans);
1927
- }
2008
+ /* Pending Overlay */
2009
+ .pending-overlay {
2010
+ position: fixed;
2011
+ top: 0;
2012
+ right: -400px;
2013
+ width: 400px;
2014
+ height: 100vh;
2015
+ background-color: var(--surface);
2016
+ border-left: 1px solid var(--border);
2017
+ box-shadow: -2px 0 8px rgba(0, 0, 0, 0.3);
2018
+ z-index: 200;
2019
+ transition: right 0.3s ease;
2020
+ display: flex;
2021
+ flex-direction: column;
2022
+ overflow-y: auto;
2023
+ }
1928
2024
 
1929
- .btn-approve {
1930
- background: var(--green);
1931
- color: var(--bg);
1932
- }
2025
+ .pending-overlay.show {
2026
+ right: 0;
2027
+ }
1933
2028
 
1934
- .btn-approve:hover {
1935
- background: #4ecf5e;
1936
- }
2029
+ .pending-header {
2030
+ padding: 16px 20px;
2031
+ border-bottom: 1px solid var(--border);
2032
+ font-weight: 600;
2033
+ color: var(--text-primary);
2034
+ }
1937
2035
 
1938
- .btn-deny {
1939
- background: var(--red);
1940
- color: white;
1941
- }
2036
+ .pending-items {
2037
+ flex: 1;
2038
+ overflow-y: auto;
2039
+ padding: 16px;
2040
+ }
1942
2041
 
1943
- .btn-deny:hover {
1944
- background: #f9605e;
1945
- }
2042
+ .pending-item {
2043
+ background-color: var(--bg);
2044
+ border: 1px solid var(--border);
2045
+ border-radius: 6px;
2046
+ padding: 16px;
2047
+ margin-bottom: 12px;
2048
+ }
1946
2049
 
1947
- /* \u2500\u2500 Threat Panel (collapsible footer) \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 */
2050
+ .pending-title {
2051
+ font-weight: 600;
2052
+ color: var(--text-primary);
2053
+ margin-bottom: 8px;
2054
+ word-break: break-word;
2055
+ }
1948
2056
 
1949
- .threat-panel {
1950
- position: fixed;
1951
- bottom: 0;
1952
- left: 0;
1953
- right: 0;
1954
- background: var(--surface);
1955
- border-top: 1px solid var(--border);
1956
- max-height: 240px;
1957
- z-index: 500;
1958
- display: flex;
1959
- flex-direction: column;
1960
- transition: max-height 0.3s ease-out;
1961
- }
2057
+ .pending-countdown {
2058
+ font-size: 12px;
2059
+ color: var(--amber);
2060
+ margin-bottom: 12px;
2061
+ font-weight: 500;
2062
+ }
1962
2063
 
1963
- .threat-panel.collapsed {
1964
- max-height: 40px;
1965
- }
2064
+ .pending-actions {
2065
+ display: flex;
2066
+ gap: 8px;
2067
+ }
1966
2068
 
1967
- .threat-header {
1968
- padding: 12px 20px;
1969
- cursor: pointer;
1970
- display: flex;
1971
- align-items: center;
1972
- gap: 8px;
1973
- font-size: 12px;
1974
- font-weight: 600;
1975
- text-transform: uppercase;
1976
- letter-spacing: 0.5px;
1977
- color: var(--text-secondary);
1978
- flex: 0 0 auto;
1979
- }
2069
+ .pending-btn {
2070
+ flex: 1;
2071
+ padding: 8px 12px;
2072
+ border: none;
2073
+ border-radius: 4px;
2074
+ font-size: 12px;
2075
+ font-weight: 600;
2076
+ cursor: pointer;
2077
+ transition: background-color 0.2s;
2078
+ }
1980
2079
 
1981
- .threat-header:hover {
1982
- background: rgba(88, 166, 255, 0.05);
1983
- }
2080
+ .pending-approve {
2081
+ background-color: var(--green);
2082
+ color: var(--bg);
2083
+ }
1984
2084
 
1985
- .threat-icon {
1986
- font-size: 14px;
1987
- }
2085
+ .pending-approve:hover {
2086
+ background-color: #3fa040;
2087
+ }
1988
2088
 
1989
- .threat-content {
1990
- flex: 1;
1991
- overflow-y: auto;
1992
- padding: 0 20px 12px;
1993
- display: flex;
1994
- flex-direction: column;
1995
- gap: 10px;
1996
- }
2089
+ .pending-deny {
2090
+ background-color: var(--red);
2091
+ color: var(--bg);
2092
+ }
1997
2093
 
1998
- .threat-item {
1999
- padding: 8px 10px;
2000
- background: rgba(248, 81, 73, 0.1);
2001
- border-left: 2px solid var(--red);
2002
- border-radius: 4px;
2003
- font-size: 11px;
2004
- color: var(--text-secondary);
2005
- }
2094
+ .pending-deny:hover {
2095
+ background-color: #e03c3c;
2096
+ }
2006
2097
 
2007
- .threat-item-type {
2008
- font-weight: 600;
2009
- color: var(--red);
2010
- font-family: var(--mono);
2011
- }
2098
+ /* Threat Panel */
2099
+ .threat-panel {
2100
+ background-color: var(--surface);
2101
+ border: 1px solid var(--border);
2102
+ border-radius: 8px;
2103
+ margin-top: 20px;
2104
+ overflow: hidden;
2105
+ }
2012
2106
 
2013
- .threat-empty {
2014
- text-align: center;
2015
- padding: 20px 10px;
2016
- color: var(--text-secondary);
2017
- font-size: 12px;
2018
- }
2107
+ .threat-header {
2108
+ padding: 16px 20px;
2109
+ border-bottom: 1px solid var(--border);
2110
+ display: flex;
2111
+ justify-content: space-between;
2112
+ align-items: center;
2113
+ cursor: pointer;
2114
+ user-select: none;
2115
+ }
2019
2116
 
2020
- /* \u2500\u2500 Scrollbars \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\u2500 */
2117
+ .threat-title {
2118
+ font-weight: 600;
2119
+ color: var(--text-primary);
2120
+ }
2021
2121
 
2022
- ::-webkit-scrollbar {
2023
- width: 6px;
2024
- }
2122
+ .threat-toggle {
2123
+ font-size: 10px;
2124
+ color: var(--text-secondary);
2125
+ transition: transform 0.2s;
2126
+ }
2025
2127
 
2026
- ::-webkit-scrollbar-track {
2027
- background: transparent;
2028
- }
2128
+ .threat-panel.collapsed .threat-toggle {
2129
+ transform: rotate(-90deg);
2130
+ }
2029
2131
 
2030
- ::-webkit-scrollbar-thumb {
2031
- background: var(--border);
2032
- border-radius: 3px;
2033
- }
2132
+ .threat-content {
2133
+ padding: 16px 20px;
2134
+ max-height: 300px;
2135
+ overflow-y: auto;
2136
+ }
2034
2137
 
2035
- ::-webkit-scrollbar-thumb:hover {
2036
- background: rgba(88, 166, 255, 0.3);
2037
- }
2138
+ .threat-panel.collapsed .threat-content {
2139
+ display: none;
2140
+ }
2038
2141
 
2039
- /* \u2500\u2500 Responsive \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\u2500 */
2142
+ .threat-alert {
2143
+ background-color: rgba(248, 81, 73, 0.1);
2144
+ border: 1px solid var(--red);
2145
+ border-radius: 4px;
2146
+ padding: 12px;
2147
+ margin-bottom: 8px;
2148
+ font-size: 12px;
2149
+ }
2040
2150
 
2041
- @media (max-width: 1200px) {
2042
- .protection-sidebar {
2043
- display: none;
2151
+ .threat-alert:last-child {
2152
+ margin-bottom: 0;
2044
2153
  }
2045
2154
 
2046
- .activity-feed {
2047
- border-right: none;
2155
+ .threat-type {
2156
+ font-weight: 600;
2157
+ color: var(--red);
2158
+ margin-bottom: 4px;
2159
+ text-transform: uppercase;
2160
+ font-size: 10px;
2161
+ letter-spacing: 0.5px;
2048
2162
  }
2049
- }
2050
2163
 
2051
- @media (max-width: 768px) {
2052
- .status-bar {
2053
- padding: 0 12px;
2054
- gap: 12px;
2055
- height: 48px;
2164
+ .threat-message {
2165
+ color: var(--text-secondary);
2056
2166
  }
2057
2167
 
2058
- .sanctuary-logo {
2059
- font-size: 14px;
2168
+ /* Scrollbar */
2169
+ ::-webkit-scrollbar {
2170
+ width: 8px;
2060
2171
  }
2061
2172
 
2062
- .status-bar-center {
2063
- display: none;
2173
+ ::-webkit-scrollbar-track {
2174
+ background-color: transparent;
2064
2175
  }
2065
2176
 
2066
- .main-container {
2067
- margin-top: 48px;
2177
+ ::-webkit-scrollbar-thumb {
2178
+ background-color: var(--border);
2179
+ border-radius: 4px;
2068
2180
  }
2069
2181
 
2070
- .activity-item {
2071
- padding: 10px 12px;
2182
+ ::-webkit-scrollbar-thumb:hover {
2183
+ background-color: var(--text-secondary);
2072
2184
  }
2073
2185
 
2074
- .pending-overlay.active {
2075
- width: 100%;
2186
+ /* Responsive */
2187
+ @media (max-width: 1400px) {
2188
+ .sovereignty-layers {
2189
+ grid-template-columns: repeat(2, 1fr);
2190
+ }
2191
+
2192
+ .main-panels {
2193
+ grid-template-columns: 1fr;
2194
+ }
2195
+
2196
+ .pending-overlay {
2197
+ width: 100%;
2198
+ right: -100%;
2199
+ }
2076
2200
  }
2077
2201
 
2078
- .threat-panel {
2079
- max-height: 200px;
2202
+ @media (max-width: 768px) {
2203
+ .status-bar {
2204
+ flex-wrap: wrap;
2205
+ height: auto;
2206
+ padding: 12px;
2207
+ gap: 12px;
2208
+ }
2209
+
2210
+ .status-bar-center {
2211
+ order: 3;
2212
+ flex-basis: 100%;
2213
+ }
2214
+
2215
+ .main-content {
2216
+ margin-top: auto;
2217
+ }
2218
+
2219
+ .info-cards {
2220
+ grid-template-columns: 1fr;
2221
+ }
2222
+
2223
+ .table-header,
2224
+ .table-row {
2225
+ grid-template-columns: 1fr;
2226
+ }
2080
2227
  }
2081
- }
2082
- </style>
2228
+ </style>
2083
2229
  </head>
2084
2230
  <body>
2085
-
2086
- <!-- Status Bar (fixed, top) -->
2087
- <div class="status-bar">
2088
- <div class="status-bar-left">
2089
- <div class="sanctuary-logo"><span>\u25C6</span> SANCTUARY</div>
2090
- <div class="version">v${options.serverVersion}</div>
2091
- </div>
2092
- <div class="status-bar-center">
2093
- <div class="sovereignty-badge">
2094
- <div class="sovereignty-score" id="sovereigntyScore">85</div>
2095
- <span>Sovereignty Health</span>
2096
- </div>
2097
- </div>
2098
- <div class="status-bar-right">
2099
- <div class="protections-indicator">
2100
- <span class="count" id="activeProtections">6</span>/6 protections
2101
- </div>
2102
- <div class="uptime">
2103
- <span id="uptimeText">\u2014</span>
2231
+ <!-- Status Bar -->
2232
+ <div class="status-bar">
2233
+ <div class="status-bar-left">
2234
+ <div class="logo-icon">\u25C6</div>
2235
+ <div class="logo-info">
2236
+ <div class="logo-title">SANCTUARY</div>
2237
+ <div class="logo-version">v${options.serverVersion}</div>
2238
+ </div>
2104
2239
  </div>
2105
- <div class="status-dot" id="statusDot"></div>
2106
- <div class="pending-badge hidden" id="pendingBadge">0</div>
2107
- </div>
2108
- </div>
2109
-
2110
- <!-- Main Layout -->
2111
- <div class="main-container">
2112
- <!-- Activity Feed -->
2113
- <div class="activity-feed">
2114
- <div class="feed-header">
2115
- <div class="feed-header-dot"></div>
2116
- Live Activity
2240
+
2241
+ <div class="status-bar-center">
2242
+ <div id="sovereignty-badge" class="sovereignty-badge">
2243
+ <span>Sovereignty Health:</span>
2244
+ <span class="sovereignty-score" id="sovereignty-score">\u2014</span>
2245
+ <span>/ 100</span>
2246
+ </div>
2117
2247
  </div>
2118
- <div class="activity-list" id="activityList">
2119
- <div class="activity-empty">
2120
- <div class="activity-empty-icon">\u2192</div>
2121
- <div class="activity-empty-text">Waiting for activity...</div>
2248
+
2249
+ <div class="status-bar-right">
2250
+ <div class="status-item">
2251
+ <strong id="protections-count">\u2014</strong>
2252
+ <span>Protections</span>
2253
+ </div>
2254
+ <div class="status-item">
2255
+ <strong id="uptime-value">\u2014</strong>
2256
+ <span>Uptime</span>
2257
+ </div>
2258
+ <div class="status-dot" id="connection-status"></div>
2259
+ <div id="pending-item-badge" class="pending-badge" style="display: none;">
2260
+ <span>\u23F3</span>
2261
+ <span id="pending-count">0</span>
2122
2262
  </div>
2123
2263
  </div>
2124
2264
  </div>
2125
2265
 
2126
- <!-- Protection Status Sidebar -->
2127
- <div class="protection-sidebar" id="protectionSidebar">
2128
- <div class="sidebar-header">
2129
- <span>\u25C6</span> Protection Status
2130
- </div>
2131
- <div class="sidebar-content">
2132
- <div class="protection-card">
2133
- <div class="protection-card-icon">\u{1F510}</div>
2134
- <div class="protection-card-label">Encryption</div>
2135
- <div class="protection-card-status active" id="encryptionStatus">\u2713 Active</div>
2136
- <div class="protection-card-stat" id="encryptionStat">Ed25519</div>
2137
- </div>
2138
-
2139
- <div class="protection-card">
2140
- <div class="protection-card-icon">\u2713</div>
2141
- <div class="protection-card-label">Approval Gate</div>
2142
- <div class="protection-card-status active" id="approvalStatus">\u2713 Active</div>
2143
- <div class="protection-card-stat" id="approvalStat">T1: 2 | T2: 3</div>
2266
+ <!-- Main Content -->
2267
+ <div class="main-content">
2268
+ <div class="grid">
2269
+ <!-- Row 1: Sovereignty Layers -->
2270
+ <div class="sovereignty-layers" id="sovereignty-layers">
2271
+ <div class="layer-card" data-layer="l1">
2272
+ <div class="layer-name">Layer 1</div>
2273
+ <div class="layer-title">Cognitive Sovereignty</div>
2274
+ <div class="layer-status"><span>\u25CF</span> <span id="l1-status">\u2014</span></div>
2275
+ <div class="layer-detail" id="l1-detail">Loading...</div>
2276
+ </div>
2277
+ <div class="layer-card" data-layer="l2">
2278
+ <div class="layer-name">Layer 2</div>
2279
+ <div class="layer-title">Operational Isolation</div>
2280
+ <div class="layer-status"><span>\u25CF</span> <span id="l2-status">\u2014</span></div>
2281
+ <div class="layer-detail" id="l2-detail">Loading...</div>
2282
+ </div>
2283
+ <div class="layer-card" data-layer="l3">
2284
+ <div class="layer-name">Layer 3</div>
2285
+ <div class="layer-title">Selective Disclosure</div>
2286
+ <div class="layer-status"><span>\u25CF</span> <span id="l3-status">\u2014</span></div>
2287
+ <div class="layer-detail" id="l3-detail">Loading...</div>
2288
+ </div>
2289
+ <div class="layer-card" data-layer="l4">
2290
+ <div class="layer-name">Layer 4</div>
2291
+ <div class="layer-title">Verifiable Reputation</div>
2292
+ <div class="layer-status"><span>\u25CF</span> <span id="l4-status">\u2014</span></div>
2293
+ <div class="layer-detail" id="l4-detail">Loading...</div>
2294
+ </div>
2144
2295
  </div>
2145
2296
 
2146
- <div class="protection-card">
2147
- <div class="protection-card-icon">\u{1F3AF}</div>
2148
- <div class="protection-card-label">Context Gating</div>
2149
- <div class="protection-card-status active" id="contextStatus">\u2713 Active</div>
2150
- <div class="protection-card-stat" id="contextStat">12 filtered</div>
2297
+ <!-- Row 2: Info Cards -->
2298
+ <div class="info-cards">
2299
+ <div class="info-card">
2300
+ <div class="card-header">Identity</div>
2301
+ <div class="card-row">
2302
+ <span class="card-label">Primary</span>
2303
+ <span class="card-value" id="identity-label">\u2014</span>
2304
+ </div>
2305
+ <div class="card-row">
2306
+ <span class="card-label">DID</span>
2307
+ <span class="card-value truncated" id="identity-did" title="">\u2014</span>
2308
+ </div>
2309
+ <div class="card-row">
2310
+ <span class="card-label">Public Key</span>
2311
+ <span class="card-value truncated" id="identity-pubkey" title="">\u2014</span>
2312
+ </div>
2313
+ <div class="card-row">
2314
+ <span class="card-label">Type</span>
2315
+ <span class="identity-badge">Ed25519</span>
2316
+ </div>
2317
+ <div class="card-row">
2318
+ <span class="card-label">Created</span>
2319
+ <span class="card-value" id="identity-created">\u2014</span>
2320
+ </div>
2321
+ <div class="card-row">
2322
+ <span class="card-label">Identities</span>
2323
+ <span class="card-value" id="identity-count">\u2014</span>
2324
+ </div>
2325
+ </div>
2326
+
2327
+ <div class="info-card">
2328
+ <div class="card-header">Handshakes</div>
2329
+ <div class="card-row">
2330
+ <span class="card-label">Total</span>
2331
+ <span class="card-value" id="handshake-count">\u2014</span>
2332
+ </div>
2333
+ <div class="card-row">
2334
+ <span class="card-label">Latest Peer</span>
2335
+ <span class="card-value truncated" id="handshake-latest">\u2014</span>
2336
+ </div>
2337
+ <div class="card-row">
2338
+ <span class="card-label">Trust Tier</span>
2339
+ <span class="trust-tier-badge" id="handshake-tier">Unverified</span>
2340
+ </div>
2341
+ <div class="card-row">
2342
+ <span class="card-label">Timestamp</span>
2343
+ <span class="card-value" id="handshake-time">\u2014</span>
2344
+ </div>
2345
+ </div>
2346
+
2347
+ <div class="info-card">
2348
+ <div class="card-header">Reputation</div>
2349
+ <div class="card-row">
2350
+ <span class="card-label">Weighted Score</span>
2351
+ <span class="card-value" id="reputation-score">\u2014</span>
2352
+ </div>
2353
+ <div class="card-row">
2354
+ <span class="card-label">Attestations</span>
2355
+ <span class="card-value" id="reputation-attestations">\u2014</span>
2356
+ </div>
2357
+ <div class="card-row">
2358
+ <span class="card-label">Verified Sovereign</span>
2359
+ <span class="card-value" id="reputation-verified">\u2014</span>
2360
+ </div>
2361
+ <div class="card-row">
2362
+ <span class="card-label">Verified Degraded</span>
2363
+ <span class="card-value" id="reputation-degraded">\u2014</span>
2364
+ </div>
2365
+ <div class="card-row">
2366
+ <span class="card-label">Unverified</span>
2367
+ <span class="card-value" id="reputation-unverified">\u2014</span>
2368
+ </div>
2369
+ </div>
2151
2370
  </div>
2152
2371
 
2153
- <div class="protection-card">
2154
- <div class="protection-card-icon">\u26A0</div>
2155
- <div class="protection-card-label">Injection Detection</div>
2156
- <div class="protection-card-status active" id="injectionStatus">\u2713 Active</div>
2157
- <div class="protection-card-stat" id="injectionStat">3 flags today</div>
2372
+ <!-- Row 3: SHR & Activity -->
2373
+ <div class="main-panels">
2374
+ <div class="panel">
2375
+ <div class="panel-header">
2376
+ <div class="panel-title">Sovereignty Health Report</div>
2377
+ <button class="panel-action" id="copy-shr-btn">Copy JSON</button>
2378
+ </div>
2379
+ <div class="panel-content">
2380
+ <div class="shr-json" id="shr-viewer">
2381
+ <div class="empty-state">Loading SHR...</div>
2382
+ </div>
2383
+ </div>
2384
+ </div>
2385
+
2386
+ <div class="panel">
2387
+ <div class="panel-header">
2388
+ <div class="panel-title">Activity Feed</div>
2389
+ </div>
2390
+ <div class="panel-content">
2391
+ <div id="activity-feed" class="activity-feed">
2392
+ <div class="empty-state">Waiting for activity...</div>
2393
+ </div>
2394
+ </div>
2395
+ </div>
2158
2396
  </div>
2159
2397
 
2160
- <div class="protection-card">
2161
- <div class="protection-card-icon">\u{1F4CA}</div>
2162
- <div class="protection-card-label">Behavioral Baseline</div>
2163
- <div class="protection-card-status active" id="baselineStatus">\u2713 Active</div>
2164
- <div class="protection-card-stat" id="baselineStat">0 anomalies</div>
2398
+ <!-- Row 4: Handshake History -->
2399
+ <div class="handshake-table">
2400
+ <div class="table-header">
2401
+ <div>Counterparty</div>
2402
+ <div>Trust Tier</div>
2403
+ <div>Sovereignty</div>
2404
+ <div>Verified</div>
2405
+ <div>Completed</div>
2406
+ <div>Expires</div>
2407
+ </div>
2408
+ <div class="table-rows" id="handshake-table">
2409
+ <div class="table-empty">No handshakes completed yet</div>
2410
+ </div>
2165
2411
  </div>
2166
2412
 
2167
- <div class="protection-card">
2168
- <div class="protection-card-icon">\u{1F4CB}</div>
2169
- <div class="protection-card-label">Audit Trail</div>
2170
- <div class="protection-card-status active" id="auditStatus">\u2713 Active</div>
2171
- <div class="protection-card-stat" id="auditStat">284 entries</div>
2413
+ <!-- Threat Panel -->
2414
+ <div class="threat-panel collapsed">
2415
+ <div class="threat-header">
2416
+ <div class="threat-title">Security Threats</div>
2417
+ <div class="threat-toggle">\u25B6</div>
2418
+ </div>
2419
+ <div class="threat-content" id="threat-alerts">
2420
+ <div class="empty-state">No threats detected</div>
2421
+ </div>
2172
2422
  </div>
2173
2423
  </div>
2174
2424
  </div>
2175
- </div>
2176
2425
 
2177
- <!-- Pending Approvals Overlay -->
2178
- <div class="pending-overlay" id="pendingOverlay">
2179
- <div class="pending-overlay-header">
2180
- <div class="pending-overlay-title">Pending Approvals</div>
2181
- <button class="pending-overlay-close" onclick="closePendingOverlay()">\xD7</button>
2182
- </div>
2183
- <div class="pending-list" id="pendingList"></div>
2184
- </div>
2185
-
2186
- <!-- Threat Panel (collapsible footer) -->
2187
- <div class="threat-panel collapsed" id="threatPanel">
2188
- <div class="threat-header" onclick="toggleThreatPanel()">
2189
- <span class="threat-icon">\u26A0</span>
2190
- Recent Threats
2191
- <span id="threatCount" style="margin-left: auto; color: var(--red); font-weight: 700;">0</span>
2426
+ <!-- Pending Overlay -->
2427
+ <div class="pending-overlay" id="pending-overlay">
2428
+ <div class="pending-header">Pending Approvals</div>
2429
+ <div class="pending-items" id="pending-items"></div>
2192
2430
  </div>
2193
- <div class="threat-content" id="threatContent">
2194
- <div class="threat-empty">No threats detected</div>
2195
- </div>
2196
- </div>
2197
2431
 
2198
- <script>
2199
- (function() {
2200
- 'use strict';
2432
+ <script>
2433
+ // Constants
2434
+ const AUTH_TOKEN = '${options.authToken || ""}' || sessionStorage.getItem('authToken') || '';
2435
+ const TIMEOUT_SECONDS = ${options.timeoutSeconds};
2436
+ const API_BASE = '';
2437
+
2438
+ // State
2439
+ let apiState = {
2440
+ sovereignty: null,
2441
+ identity: null,
2442
+ handshakes: [],
2443
+ shr: null,
2444
+ status: null,
2445
+ };
2201
2446
 
2202
- // \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
2447
+ let pendingRequests = new Map();
2448
+ let activityLog = [];
2449
+ const maxActivityItems = 50;
2203
2450
 
2204
- const TIMEOUT_SECONDS = ${options.timeoutSeconds};
2205
- // AUTH_TOKEN: embedded token (for direct session access) or from sessionStorage (login page flow)
2206
- const EMBEDDED_TOKEN = ${options.authToken ? JSON.stringify(options.authToken) : "null"};
2207
- const AUTH_TOKEN = EMBEDDED_TOKEN || (function() { try { return sessionStorage.getItem('sanctuary_token'); } catch(_) { return null; } })();
2208
- const MAX_ACTIVITY_ITEMS = 100;
2209
- const MAX_THREAT_ITEMS = 20;
2451
+ // Helpers
2452
+ function esc(text) {
2453
+ if (!text) return '';
2454
+ const div = document.createElement('div');
2455
+ div.textContent = text;
2456
+ return div.innerHTML;
2457
+ }
2458
+
2459
+ function formatTime(isoString) {
2460
+ if (!isoString) return '\u2014';
2461
+ const date = new Date(isoString);
2462
+ return date.toLocaleString('en-US', {
2463
+ month: 'short',
2464
+ day: 'numeric',
2465
+ hour: '2-digit',
2466
+ minute: '2-digit',
2467
+ });
2468
+ }
2210
2469
 
2211
- // \u2500\u2500 State \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2470
+ function truncate(str, len = 16) {
2471
+ if (!str) return '\u2014';
2472
+ if (str.length <= len) return str;
2473
+ return str.slice(0, len) + '...';
2474
+ }
2212
2475
 
2213
- let SESSION_ID = null;
2214
- let evtSource = null;
2215
- let startTime = Date.now();
2216
- let activityCount = 0;
2217
- let threatCount = 0;
2218
- const pendingRequests = new Map();
2219
- const activityItems = [];
2220
- const threatItems = [];
2221
- let sovereigntyScore = 85;
2222
- let sessionRenewalTimer = null;
2476
+ function calculateSovereigntyScore(shr) {
2477
+ if (!shr || !shr.layers) return 0;
2478
+ const layers = shr.layers;
2479
+ let score = 100;
2223
2480
 
2224
- // \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
2481
+ if (layers.l1?.status === 'degraded') score -= 20;
2482
+ if (layers.l1?.status === 'inactive') score -= 35;
2483
+ if (layers.l2?.status === 'degraded') score -= 15;
2484
+ if (layers.l2?.status === 'inactive') score -= 25;
2485
+ if (layers.l3?.status === 'degraded') score -= 15;
2486
+ if (layers.l3?.status === 'inactive') score -= 25;
2487
+ if (layers.l4?.status === 'degraded') score -= 10;
2488
+ if (layers.l4?.status === 'inactive') score -= 20;
2225
2489
 
2226
- function authHeaders() {
2227
- const h = { 'Content-Type': 'application/json' };
2228
- if (AUTH_TOKEN) h['Authorization'] = 'Bearer ' + AUTH_TOKEN;
2229
- return h;
2230
- }
2490
+ return Math.max(0, Math.min(100, score));
2491
+ }
2231
2492
 
2232
- function sessionQuery(url) {
2233
- if (!SESSION_ID) return url;
2234
- const sep = url.includes('?') ? '&' : '?';
2235
- return url + sep + 'session=' + SESSION_ID;
2236
- }
2493
+ async function fetchAPI(endpoint) {
2494
+ try {
2495
+ const response = await fetch(API_BASE + endpoint, {
2496
+ headers: {
2497
+ 'Authorization': 'Bearer ' + AUTH_TOKEN,
2498
+ },
2499
+ });
2237
2500
 
2238
- function setCookie(sessionId, maxAge) {
2239
- document.cookie = 'sanctuary_session=' + sessionId +
2240
- '; path=/; SameSite=Strict; max-age=' + maxAge;
2241
- }
2501
+ if (response.status === 401) {
2502
+ redirectToLogin();
2503
+ return null;
2504
+ }
2242
2505
 
2243
- async function exchangeSession() {
2244
- if (!AUTH_TOKEN) return;
2245
- try {
2246
- const resp = await fetch('/auth/session', { method: 'POST', headers: authHeaders() });
2247
- if (resp.ok) {
2248
- const data = await resp.json();
2249
- SESSION_ID = data.session_id;
2250
- var ttl = data.expires_in_seconds || 300;
2251
- // Update cookie with new session
2252
- setCookie(SESSION_ID, ttl);
2253
- // Schedule renewal at 80% of TTL
2254
- if (sessionRenewalTimer) clearTimeout(sessionRenewalTimer);
2255
- sessionRenewalTimer = setTimeout(function() {
2256
- exchangeSession().then(function() { reconnectSSE(); });
2257
- }, ttl * 800);
2258
- } else if (resp.status === 401) {
2259
- // Token invalid or expired \u2014 show non-destructive re-login overlay
2260
- showSessionExpired();
2261
- }
2262
- } catch (e) {
2263
- // Network error \u2014 retry in 30s
2264
- if (sessionRenewalTimer) clearTimeout(sessionRenewalTimer);
2265
- sessionRenewalTimer = setTimeout(function() {
2266
- exchangeSession().then(function() { reconnectSSE(); });
2267
- }, 30000);
2268
- }
2269
- }
2270
-
2271
- function showSessionExpired() {
2272
- // Clear stored token
2273
- try { sessionStorage.removeItem('sanctuary_token'); } catch(_) {}
2274
- // Redirect to login page
2275
- document.cookie = 'sanctuary_session=; path=/; max-age=0';
2276
- window.location.reload();
2277
- }
2278
-
2279
- // \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
2280
-
2281
- function esc(s) {
2282
- const d = document.createElement('div');
2283
- d.textContent = String(s || '');
2284
- return d.innerHTML;
2285
- }
2286
-
2287
- function closePendingOverlay() {
2288
- document.getElementById('pendingOverlay').classList.remove('active');
2289
- }
2290
-
2291
- function toggleThreatPanel() {
2292
- document.getElementById('threatPanel').classList.toggle('collapsed');
2293
- }
2294
-
2295
- function updateUptime() {
2296
- const elapsed = Math.floor((Date.now() - startTime) / 1000);
2297
- const hours = Math.floor(elapsed / 3600);
2298
- const mins = Math.floor((elapsed % 3600) / 60);
2299
- const secs = elapsed % 60;
2300
- let uptimeStr = '';
2301
- if (hours > 0) uptimeStr += hours + 'h ';
2302
- if (mins > 0) uptimeStr += mins + 'm ';
2303
- uptimeStr += secs + 's';
2304
- document.getElementById('uptimeText').textContent = uptimeStr;
2305
- }
2306
-
2307
- // \u2500\u2500 Sovereignty Score \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
2308
-
2309
- function updateSovereigntyScore(score) {
2310
- sovereigntyScore = Math.min(100, Math.max(0, score || 85));
2311
- const badge = document.getElementById('sovereigntyScore');
2312
- badge.textContent = sovereigntyScore;
2313
- badge.className = 'sovereignty-score';
2314
- if (sovereigntyScore >= 80) {
2315
- badge.classList.add('high');
2316
- } else if (sovereigntyScore >= 50) {
2317
- badge.classList.add('medium');
2318
- } else {
2319
- badge.classList.add('low');
2506
+ if (!response.ok) {
2507
+ console.error('API Error:', response.status);
2508
+ return null;
2509
+ }
2510
+
2511
+ return await response.json();
2512
+ } catch (err) {
2513
+ console.error('Fetch error:', err);
2514
+ return null;
2515
+ }
2320
2516
  }
2321
- }
2322
2517
 
2323
- // \u2500\u2500 Activity Feed \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
2518
+ function redirectToLogin() {
2519
+ sessionStorage.removeItem('authToken');
2520
+ window.location.href = '/';
2521
+ }
2324
2522
 
2325
- function addActivityItem(data) {
2326
- const {
2327
- timestamp,
2328
- tier,
2329
- tool,
2330
- outcome,
2331
- detail,
2332
- hasInjection,
2333
- isContextGated
2334
- } = data;
2335
-
2336
- const item = {
2337
- id: 'activity-' + activityCount++,
2338
- timestamp: timestamp || new Date().toISOString(),
2339
- tier: tier || 1,
2340
- tool: tool || 'unknown_tool',
2341
- outcome: outcome || 'executed',
2342
- detail: detail || '',
2343
- hasInjection: !!hasInjection,
2344
- isContextGated: !!isContextGated
2345
- };
2523
+ // API Updates
2524
+ async function updateSovereignty() {
2525
+ const data = await fetchAPI('/api/sovereignty');
2526
+ if (!data) return;
2346
2527
 
2347
- activityItems.unshift(item);
2348
- if (activityItems.length > MAX_ACTIVITY_ITEMS) {
2349
- activityItems.pop();
2350
- }
2528
+ apiState.sovereignty = data;
2351
2529
 
2352
- renderActivityFeed();
2353
- }
2530
+ const score = calculateSovereigntyScore(data.shr);
2531
+ const badge = document.getElementById('sovereignty-badge');
2532
+ const scoreEl = document.getElementById('sovereignty-score');
2354
2533
 
2355
- function renderActivityFeed() {
2356
- const list = document.getElementById('activityList');
2534
+ scoreEl.textContent = score;
2357
2535
 
2358
- if (activityItems.length === 0) {
2359
- list.innerHTML = '<div class="activity-empty"><div class="activity-empty-icon">\u2192</div><div class="activity-empty-text">Waiting for activity...</div></div>';
2360
- return;
2536
+ badge.classList.remove('degraded', 'inactive');
2537
+ if (score < 70) badge.classList.add('degraded');
2538
+ if (score < 40) badge.classList.add('inactive');
2539
+
2540
+ updateLayerCards(data.shr);
2361
2541
  }
2362
2542
 
2363
- list.innerHTML = '';
2364
- for (const item of activityItems) {
2365
- const tr = document.createElement('div');
2366
- tr.className = 'activity-item';
2367
- tr.id = item.id;
2368
-
2369
- const time = new Date(item.timestamp);
2370
- const timeStr = time.toLocaleTimeString();
2371
-
2372
- const tierClass = 't' + item.tier;
2373
- const outcomeClass = item.outcome === 'denied' ? 'outcome denied' : 'outcome';
2374
-
2375
- let icon = '\u25CF';
2376
- if (item.isContextGated) icon = '\u{1F3AF}';
2377
- else if (item.hasInjection) icon = '\u26A0';
2378
- else if (item.outcome === 'denied') icon = '\u2717';
2379
- else icon = '\u2713';
2380
-
2381
- tr.innerHTML =
2382
- '<div class="activity-item-icon">' + esc(icon) + '</div>' +
2383
- '<div class="activity-item-content">' +
2384
- '<div class="activity-time">' + esc(timeStr) + '</div>' +
2385
- '<div class="activity-main">' +
2386
- '<span class="activity-tier ' + tierClass + '">T' + item.tier + '</span>' +
2387
- '<span class="activity-tool">' + esc(item.tool) + '</span>' +
2388
- '<span class="activity-outcome ' + (outcomeClass === 'outcome denied' ? 'denied' : '') + '">' + (item.outcome === 'denied' ? '\u2717 denied' : '\u2713 allowed') + '</span>' +
2389
- '</div>' +
2390
- '<div class="activity-detail">' + esc(item.detail) + '</div>' +
2391
- '</div>' +
2392
- '';
2393
-
2394
- tr.addEventListener('click', () => {
2395
- tr.classList.toggle('expanded');
2396
- });
2543
+ function updateLayerCards(shr) {
2544
+ if (!shr || !shr.layers) return;
2397
2545
 
2398
- list.appendChild(tr);
2399
- }
2400
- }
2546
+ const layers = shr.layers;
2401
2547
 
2402
- // \u2500\u2500 Pending Approvals \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
2548
+ updateLayerCard('l1', layers.l1, layers.l1?.encryption || 'AES-256-GCM');
2549
+ updateLayerCard('l2', layers.l2, layers.l2?.isolation_type || 'Process-level');
2550
+ updateLayerCard('l3', layers.l3, layers.l3?.proof_system || 'Schnorr-Pedersen');
2551
+ updateLayerCard('l4', layers.l4, layers.l4?.reputation_mode || 'Weighted');
2552
+ }
2403
2553
 
2404
- function addPendingRequest(data) {
2405
- const {
2406
- request_id,
2407
- operation,
2408
- tier,
2409
- reason,
2410
- context,
2411
- timestamp
2412
- } = data;
2413
-
2414
- const pending = {
2415
- id: request_id,
2416
- operation: operation || 'unknown',
2417
- tier: tier || 1,
2418
- reason: reason || '',
2419
- context: context || {},
2420
- timestamp: timestamp || new Date().toISOString(),
2421
- remaining: TIMEOUT_SECONDS
2422
- };
2554
+ function updateLayerCard(layer, layerData, detail) {
2555
+ if (!layerData) return;
2423
2556
 
2424
- pendingRequests.set(request_id, pending);
2425
- updatePendingUI();
2426
- }
2557
+ const card = document.querySelector(\`[data-layer="\${layer}"]\`);
2558
+ if (!card) return;
2427
2559
 
2428
- function removePendingRequest(id) {
2429
- pendingRequests.delete(id);
2430
- updatePendingUI();
2431
- }
2560
+ const status = layerData.status || 'inactive';
2561
+ card.classList.remove('degraded', 'inactive');
2432
2562
 
2433
- function updatePendingUI() {
2434
- const count = pendingRequests.size;
2435
- const badge = document.getElementById('pendingBadge');
2563
+ if (status === 'degraded') {
2564
+ card.classList.add('degraded');
2565
+ } else if (status === 'inactive') {
2566
+ card.classList.add('inactive');
2567
+ }
2436
2568
 
2437
- if (count > 0) {
2438
- badge.classList.remove('hidden');
2439
- badge.textContent = count;
2440
- document.getElementById('pendingOverlay').classList.add('active');
2441
- } else {
2442
- badge.classList.add('hidden');
2443
- document.getElementById('pendingOverlay').classList.remove('active');
2569
+ document.getElementById(\`\${layer}-status\`).textContent = status.toUpperCase();
2570
+ document.getElementById(\`\${layer}-detail\`).textContent = detail;
2444
2571
  }
2445
2572
 
2446
- renderPendingList();
2447
- }
2573
+ async function updateIdentity() {
2574
+ const data = await fetchAPI('/api/identity');
2575
+ if (!data) return;
2448
2576
 
2449
- function renderPendingList() {
2450
- const list = document.getElementById('pendingList');
2451
- list.innerHTML = '';
2577
+ apiState.identity = data;
2452
2578
 
2453
- for (const [id, req] of pendingRequests) {
2454
- const item = document.createElement('div');
2455
- item.className = 'pending-item';
2579
+ const primary = data.primary || {};
2580
+ document.getElementById('identity-label').textContent = primary.label || '\u2014';
2581
+ document.getElementById('identity-did').textContent = truncate(primary.did, 24);
2582
+ document.getElementById('identity-did').title = primary.did || '';
2583
+ document.getElementById('identity-pubkey').textContent = truncate(primary.publicKey, 24);
2584
+ document.getElementById('identity-pubkey').title = primary.publicKey || '';
2585
+ document.getElementById('identity-created').textContent = formatTime(primary.createdAt);
2586
+ document.getElementById('identity-count').textContent = data.identities?.length || '\u2014';
2587
+ }
2456
2588
 
2457
- const tier = req.tier || 1;
2458
- const tierClass = 'tier' + tier;
2459
- const pct = Math.max(0, Math.min(100, (req.remaining / TIMEOUT_SECONDS) * 100));
2460
- const isUrgent = req.remaining <= 30;
2589
+ async function updateHandshakes() {
2590
+ const data = await fetchAPI('/api/handshakes');
2591
+ if (!data) return;
2461
2592
 
2462
- item.innerHTML =
2463
- '<div class="pending-item-header">' +
2464
- '<div class="pending-item-op">' + esc(req.operation) + '</div>' +
2465
- '<div class="pending-item-tier ' + tierClass + '">T' + tier + '</div>' +
2466
- '</div>' +
2467
- '<div class="pending-item-reason">' + esc(req.reason) + '</div>' +
2468
- '<div class="pending-item-timer ' + (isUrgent ? 'urgent' : '') + '">' +
2469
- '<div class="pending-item-timer-bar">' +
2470
- '<div class="pending-item-timer-fill" style="width: ' + pct + '%"></div>' +
2471
- '</div>' +
2472
- '<span id="timer-' + id + '">' + req.remaining + 's</span>' +
2473
- '</div>' +
2474
- '<div class="pending-item-actions">' +
2475
- '<button class="btn btn-approve" onclick="handleApprove('' + id + '')">Approve</button>' +
2476
- '<button class="btn btn-deny" onclick="handleDeny('' + id + '')">Deny</button>' +
2477
- '</div>' +
2478
- '';
2593
+ apiState.handshakes = data.handshakes || [];
2479
2594
 
2480
- list.appendChild(item);
2481
- }
2482
- }
2595
+ document.getElementById('handshake-count').textContent = data.handshakes?.length || '0';
2483
2596
 
2484
- window.handleApprove = function(id) {
2485
- fetch('/api/approve/' + id, { method: 'POST', headers: authHeaders() }).then(() => {
2486
- removePendingRequest(id);
2487
- }).catch(() => {});
2488
- };
2597
+ if (data.handshakes && data.handshakes.length > 0) {
2598
+ const latest = data.handshakes[0];
2599
+ document.getElementById('handshake-latest').textContent = truncate(latest.counterpartyId, 20);
2600
+ document.getElementById('handshake-latest').title = latest.counterpartyId || '';
2601
+ document.getElementById('handshake-tier').textContent = (latest.trustTier || 'Unverified').toUpperCase();
2602
+ document.getElementById('handshake-time').textContent = formatTime(latest.completedAt);
2603
+ } else {
2604
+ document.getElementById('handshake-latest').textContent = '\u2014';
2605
+ document.getElementById('handshake-tier').textContent = 'Unverified';
2606
+ document.getElementById('handshake-time').textContent = '\u2014';
2607
+ }
2489
2608
 
2490
- window.handleDeny = function(id) {
2491
- fetch('/api/deny/' + id, { method: 'POST', headers: authHeaders() }).then(() => {
2492
- removePendingRequest(id);
2493
- }).catch(() => {});
2494
- };
2609
+ updateHandshakeTable(data.handshakes || []);
2610
+ }
2495
2611
 
2496
- // \u2500\u2500 Threats \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\u2500\u2500\u2500\u2500\u2500
2497
-
2498
- function addThreat(data) {
2499
- const {
2500
- timestamp,
2501
- severity,
2502
- type,
2503
- details
2504
- } = data;
2505
-
2506
- const threat = {
2507
- id: 'threat-' + threatCount++,
2508
- timestamp: timestamp || new Date().toISOString(),
2509
- severity: severity || 'medium',
2510
- type: type || 'unknown',
2511
- details: details || ''
2512
- };
2612
+ function updateHandshakeTable(handshakes) {
2613
+ const table = document.getElementById('handshake-table');
2614
+
2615
+ if (!handshakes || handshakes.length === 0) {
2616
+ table.innerHTML = '<div class="table-empty">No handshakes completed yet</div>';
2617
+ return;
2618
+ }
2513
2619
 
2514
- threatItems.unshift(threat);
2515
- if (threatItems.length > MAX_THREAT_ITEMS) {
2516
- threatItems.pop();
2620
+ table.innerHTML = handshakes
2621
+ .map(
2622
+ (hs) => \`
2623
+ <div class="table-row">
2624
+ <div class="table-cell strong">\${esc(truncate(hs.counterpartyId, 24))}</div>
2625
+ <div class="table-cell">\${esc(hs.trustTier || 'Unverified')}</div>
2626
+ <div class="table-cell">\${esc(hs.sovereigntyLevel || '\u2014')}</div>
2627
+ <div class="table-cell">\${hs.verified ? 'Yes' : 'No'}</div>
2628
+ <div class="table-cell">\${formatTime(hs.completedAt)}</div>
2629
+ <div class="table-cell">\${formatTime(hs.expiresAt)}</div>
2630
+ </div>
2631
+ \`
2632
+ )
2633
+ .join('');
2517
2634
  }
2518
2635
 
2519
- if (threatCount > 0) {
2520
- document.getElementById('threatPanel').classList.remove('collapsed');
2636
+ async function updateSHR() {
2637
+ const data = await fetchAPI('/api/shr');
2638
+ if (!data) return;
2639
+
2640
+ apiState.shr = data;
2641
+ renderSHRViewer(data);
2521
2642
  }
2522
2643
 
2523
- renderThreats();
2524
- }
2644
+ function renderSHRViewer(shr) {
2645
+ const viewer = document.getElementById('shr-viewer');
2646
+
2647
+ if (!shr) {
2648
+ viewer.innerHTML = '<div class="empty-state">No SHR available</div>';
2649
+ return;
2650
+ }
2525
2651
 
2526
- function renderThreats() {
2527
- const content = document.getElementById('threatContent');
2528
- const badge = document.getElementById('threatCount');
2652
+ let html = '';
2653
+
2654
+ // Implementation
2655
+ html += \`
2656
+ <div class="shr-section">
2657
+ <div class="shr-section-header">
2658
+ <div class="shr-toggle">\u25BC</div>
2659
+ <div>Implementation</div>
2660
+ </div>
2661
+ <div class="shr-section-content">
2662
+ <div class="shr-item">
2663
+ <div class="shr-key">sanctuary_version:</div>
2664
+ <div class="shr-value">\${esc(shr.implementation?.sanctuary_version || '\u2014')}</div>
2665
+ </div>
2666
+ <div class="shr-item">
2667
+ <div class="shr-key">node_version:</div>
2668
+ <div class="shr-value">\${esc(shr.implementation?.node_version || '\u2014')}</div>
2669
+ </div>
2670
+ <div class="shr-item">
2671
+ <div class="shr-key">generated_by:</div>
2672
+ <div class="shr-value">\${esc(shr.implementation?.generated_by || '\u2014')}</div>
2673
+ </div>
2674
+ </div>
2675
+ </div>
2676
+ \`;
2677
+
2678
+ // Metadata
2679
+ html += \`
2680
+ <div class="shr-section">
2681
+ <div class="shr-section-header">
2682
+ <div class="shr-toggle">\u25BC</div>
2683
+ <div>Metadata</div>
2684
+ </div>
2685
+ <div class="shr-section-content">
2686
+ <div class="shr-item">
2687
+ <div class="shr-key">instance_id:</div>
2688
+ <div class="shr-value">\${esc(truncate(shr.instance_id, 20))}</div>
2689
+ </div>
2690
+ <div class="shr-item">
2691
+ <div class="shr-key">generated_at:</div>
2692
+ <div class="shr-value">\${formatTime(shr.generated_at)}</div>
2693
+ </div>
2694
+ <div class="shr-item">
2695
+ <div class="shr-key">expires_at:</div>
2696
+ <div class="shr-value">\${formatTime(shr.expires_at)}</div>
2697
+ </div>
2698
+ </div>
2699
+ </div>
2700
+ \`;
2701
+
2702
+ // Layers
2703
+ if (shr.layers) {
2704
+ html += \`<div class="shr-section">
2705
+ <div class="shr-section-header">
2706
+ <div class="shr-toggle">\u25BC</div>
2707
+ <div>Layers</div>
2708
+ </div>
2709
+ <div class="shr-section-content">
2710
+ \`;
2711
+
2712
+ for (const [key, layer] of Object.entries(shr.layers)) {
2713
+ html += \`
2714
+ <div style="margin-bottom: 12px;">
2715
+ <div style="color: var(--blue); font-weight: 600; margin-bottom: 4px;">\${esc(key)}</div>
2716
+ <div style="padding-left: 12px;">
2717
+ \`;
2718
+
2719
+ for (const [lkey, lvalue] of Object.entries(layer || {})) {
2720
+ const displayValue =
2721
+ typeof lvalue === 'boolean'
2722
+ ? lvalue
2723
+ ? 'true'
2724
+ : 'false'
2725
+ : esc(String(lvalue));
2726
+ html += \`
2727
+ <div class="shr-item">
2728
+ <div class="shr-key">\${esc(lkey)}:</div>
2729
+ <div class="shr-value">\${displayValue}</div>
2730
+ </div>
2731
+ \`;
2732
+ }
2529
2733
 
2530
- if (threatItems.length === 0) {
2531
- content.innerHTML = '<div class="threat-empty">No threats detected</div>';
2532
- badge.textContent = '0';
2533
- return;
2734
+ html += \`
2735
+ </div>
2736
+ </div>
2737
+ \`;
2738
+ }
2739
+
2740
+ html += \`
2741
+ </div>
2742
+ </div>
2743
+ \`;
2744
+ }
2745
+
2746
+ // Capabilities
2747
+ if (shr.capabilities) {
2748
+ html += \`
2749
+ <div class="shr-section">
2750
+ <div class="shr-section-header">
2751
+ <div class="shr-toggle">\u25BC</div>
2752
+ <div>Capabilities</div>
2753
+ </div>
2754
+ <div class="shr-section-content">
2755
+ \`;
2756
+
2757
+ for (const [key, value] of Object.entries(shr.capabilities)) {
2758
+ const displayValue = value ? 'true' : 'false';
2759
+ html += \`
2760
+ <div class="shr-item">
2761
+ <div class="shr-key">\${esc(key)}:</div>
2762
+ <div class="shr-value">\${displayValue}</div>
2763
+ </div>
2764
+ \`;
2765
+ }
2766
+
2767
+ html += \`
2768
+ </div>
2769
+ </div>
2770
+ \`;
2771
+ }
2772
+
2773
+ // Signature
2774
+ html += \`
2775
+ <div class="shr-section">
2776
+ <div class="shr-section-header">
2777
+ <div class="shr-toggle">\u25BC</div>
2778
+ <div>Signature</div>
2779
+ </div>
2780
+ <div class="shr-section-content">
2781
+ <div class="shr-item">
2782
+ <div class="shr-key">signed_by:</div>
2783
+ <div class="shr-value">\${esc(truncate(shr.signed_by, 20))}</div>
2784
+ </div>
2785
+ <div class="shr-item">
2786
+ <div class="shr-key">signature:</div>
2787
+ <div class="shr-value">\${esc(truncate(shr.signature, 32))}</div>
2788
+ </div>
2789
+ </div>
2790
+ </div>
2791
+ \`;
2792
+
2793
+ viewer.innerHTML = html;
2794
+
2795
+ // Add collapse functionality
2796
+ document.querySelectorAll('.shr-section-header').forEach((header) => {
2797
+ header.addEventListener('click', () => {
2798
+ header.closest('.shr-section').classList.toggle('collapsed');
2799
+ });
2800
+ });
2534
2801
  }
2535
2802
 
2536
- badge.textContent = threatItems.length;
2537
- content.innerHTML = '';
2803
+ async function updateStatus() {
2804
+ const data = await fetchAPI('/api/status');
2805
+ if (!data) return;
2538
2806
 
2539
- for (const threat of threatItems) {
2540
- const div = document.createElement('div');
2541
- div.className = 'threat-item';
2542
- const time = new Date(threat.timestamp).toLocaleTimeString();
2543
- div.innerHTML =
2544
- '<div style="margin-bottom: 3px;">' +
2545
- '<span class="threat-item-type">' + esc(threat.type) + '</span>' +
2546
- '<span style="font-size: 10px; color: var(--text-secondary); margin-left: 6px;">' + esc(time) + '</span>' +
2547
- '</div>' +
2548
- '<div>' + esc(threat.details) + '</div>' +
2549
- '';
2550
- content.appendChild(div);
2551
- }
2552
- }
2807
+ apiState.status = data;
2553
2808
 
2554
- // \u2500\u2500 SSE Connection \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
2809
+ document.getElementById('protections-count').textContent = data.protectionsCount || '0';
2810
+ document.getElementById('uptime-value').textContent = formatUptime(data.uptime);
2555
2811
 
2556
- function reconnectSSE() {
2557
- if (evtSource) evtSource.close();
2558
- connect();
2559
- }
2812
+ const connectionStatus = document.getElementById('connection-status');
2813
+ connectionStatus.classList.toggle('disconnected', !data.connected);
2814
+ }
2560
2815
 
2561
- function connect() {
2562
- evtSource = new EventSource(sessionQuery('/events'));
2816
+ function formatUptime(seconds) {
2817
+ if (!seconds) return '\u2014';
2818
+ const hours = Math.floor(seconds / 3600);
2819
+ const minutes = Math.floor((seconds % 3600) / 60);
2820
+ if (hours > 0) return \`\${hours}h \${minutes}m\`;
2821
+ return \`\${minutes}m\`;
2822
+ }
2563
2823
 
2564
- evtSource.onopen = () => {
2565
- document.getElementById('statusDot').classList.remove('disconnected');
2566
- };
2824
+ // SSE Setup
2825
+ function setupSSE() {
2826
+ const eventSource = new EventSource(API_BASE + '/api/events', {
2827
+ headers: {
2828
+ 'Authorization': 'Bearer ' + AUTH_TOKEN,
2829
+ },
2830
+ });
2567
2831
 
2568
- evtSource.onerror = () => {
2569
- document.getElementById('statusDot').classList.add('disconnected');
2570
- };
2832
+ eventSource.addEventListener('init', (e) => {
2833
+ console.log('Connected to SSE');
2834
+ });
2571
2835
 
2572
- evtSource.addEventListener('init', (e) => {
2573
- const data = JSON.parse(e.data);
2574
- if (data.baseline) {
2575
- updateBaseline(data.baseline);
2576
- }
2577
- if (data.policy) {
2578
- updatePolicy(data.policy);
2579
- }
2580
- if (data.pending) {
2581
- data.pending.forEach(addPendingRequest);
2582
- }
2583
- });
2836
+ eventSource.addEventListener('sovereignty-update', () => {
2837
+ updateSovereignty();
2838
+ });
2584
2839
 
2585
- evtSource.addEventListener('pending-request', (e) => {
2586
- const data = JSON.parse(e.data);
2587
- addPendingRequest(data);
2588
- });
2840
+ eventSource.addEventListener('handshake-update', () => {
2841
+ updateHandshakes();
2842
+ });
2589
2843
 
2590
- evtSource.addEventListener('request-resolved', (e) => {
2591
- const data = JSON.parse(e.data);
2592
- removePendingRequest(data.request_id);
2593
- });
2844
+ eventSource.addEventListener('tool-call', (e) => {
2845
+ const data = JSON.parse(e.data);
2846
+ addActivityItem({
2847
+ type: 'tool-call',
2848
+ title: 'Tool Call',
2849
+ content: data.toolName,
2850
+ timestamp: new Date().toISOString(),
2851
+ });
2852
+ });
2594
2853
 
2595
- evtSource.addEventListener('tool-call', (e) => {
2596
- const data = JSON.parse(e.data);
2597
- addActivityItem({
2598
- timestamp: data.timestamp,
2599
- tier: data.tier || 1,
2600
- tool: data.tool || 'unknown',
2601
- outcome: data.outcome || 'executed',
2602
- detail: data.detail || ''
2854
+ eventSource.addEventListener('context-gate-decision', (e) => {
2855
+ const data = JSON.parse(e.data);
2856
+ addActivityItem({
2857
+ type: 'context-gate',
2858
+ title: 'Context Gate',
2859
+ content: data.decision,
2860
+ timestamp: new Date().toISOString(),
2861
+ });
2603
2862
  });
2604
- });
2605
2863
 
2606
- evtSource.addEventListener('context-gate-decision', (e) => {
2607
- const data = JSON.parse(e.data);
2608
- addActivityItem({
2609
- timestamp: data.timestamp,
2610
- tier: data.tier || 1,
2611
- tool: data.tool || 'unknown',
2612
- outcome: data.outcome || 'gated',
2613
- detail: data.fields_filtered ? 'Filtered ' + data.fields_filtered + ' fields' : data.reason || '',
2614
- isContextGated: true
2864
+ eventSource.addEventListener('injection-alert', (e) => {
2865
+ const data = JSON.parse(e.data);
2866
+ addActivityItem({
2867
+ type: 'injection',
2868
+ title: 'Injection Alert',
2869
+ content: data.pattern,
2870
+ timestamp: new Date().toISOString(),
2871
+ });
2872
+ addThreatAlert(data);
2615
2873
  });
2616
- });
2617
2874
 
2618
- evtSource.addEventListener('injection-alert', (e) => {
2619
- const data = JSON.parse(e.data);
2620
- addActivityItem({
2621
- timestamp: data.timestamp,
2622
- tier: data.tier || 2,
2623
- tool: data.tool || 'unknown',
2624
- outcome: data.allowed ? 'allowed' : 'denied',
2625
- detail: data.signal || 'Injection detected',
2626
- hasInjection: true
2875
+ eventSource.addEventListener('pending-request', (e) => {
2876
+ const data = JSON.parse(e.data);
2877
+ addPendingRequest(data);
2627
2878
  });
2628
- addThreat({
2629
- timestamp: data.timestamp,
2630
- severity: data.severity || 'medium',
2631
- type: 'Injection Alert',
2632
- details: data.signal || 'Suspicious pattern detected'
2879
+
2880
+ eventSource.addEventListener('request-resolved', (e) => {
2881
+ const data = JSON.parse(e.data);
2882
+ removePendingRequest(data.requestId);
2633
2883
  });
2634
- });
2635
2884
 
2636
- evtSource.addEventListener('protection-status', (e) => {
2637
- const data = JSON.parse(e.data);
2638
- updateProtectionStatus(data);
2639
- });
2885
+ eventSource.onerror = () => {
2886
+ console.error('SSE error');
2887
+ setTimeout(setupSSE, 5000);
2888
+ };
2889
+ }
2640
2890
 
2641
- evtSource.addEventListener('audit-entry', (e) => {
2642
- const data = JSON.parse(e.data);
2643
- // Audit entries don't show in activity by default, but we could add them
2644
- });
2891
+ // Activity Feed
2892
+ function addActivityItem(item) {
2893
+ activityLog.unshift(item);
2894
+ if (activityLog.length > maxActivityItems) {
2895
+ activityLog.pop();
2896
+ }
2645
2897
 
2646
- evtSource.addEventListener('baseline-update', (e) => {
2647
- const data = JSON.parse(e.data);
2648
- updateBaseline(data);
2649
- });
2650
- }
2898
+ const feed = document.getElementById('activity-feed');
2899
+ const html = \`
2900
+ <div class="activity-item \${item.type}">
2901
+ <div class="activity-type">\${esc(item.title)}</div>
2902
+ <div class="activity-content">\${esc(item.content)}</div>
2903
+ <div class="activity-time">\${formatTime(item.timestamp)}</div>
2904
+ </div>
2905
+ \`;
2651
2906
 
2652
- function updateBaseline(baseline) {
2653
- if (!baseline) return;
2654
- // Update baseline-derived stats if needed
2655
- }
2907
+ if (feed.querySelector('.empty-state')) {
2908
+ feed.innerHTML = '';
2909
+ }
2656
2910
 
2657
- function updatePolicy(policy) {
2658
- if (!policy) return;
2659
- // Update policy-derived stats
2660
- if (policy.approval_channel) {
2661
- // Policy info updated
2662
- }
2663
- }
2911
+ feed.insertAdjacentHTML('afterbegin', html);
2664
2912
 
2665
- function updateProtectionStatus(status) {
2666
- if (status.sovereignty_score !== undefined) {
2667
- updateSovereigntyScore(status.sovereignty_score);
2668
- }
2669
- if (status.active_protections !== undefined) {
2670
- document.getElementById('activeProtections').textContent = status.active_protections;
2671
- }
2672
- // Update individual protection cards
2673
- if (status.encryption !== undefined) {
2674
- const el = document.getElementById('encryptionStatus');
2675
- el.className = 'protection-card-status ' + (status.encryption ? 'active' : 'inactive');
2676
- el.textContent = status.encryption ? '\u2713 Active' : '\u2717 Inactive';
2677
- }
2678
- if (status.approval_gate !== undefined) {
2679
- const el = document.getElementById('approvalStatus');
2680
- el.className = 'protection-card-status ' + (status.approval_gate ? 'active' : 'inactive');
2681
- el.textContent = status.approval_gate ? '\u2713 Active' : '\u2717 Inactive';
2682
- }
2683
- if (status.context_gating !== undefined) {
2684
- const el = document.getElementById('contextStatus');
2685
- el.className = 'protection-card-status ' + (status.context_gating ? 'active' : 'inactive');
2686
- el.textContent = status.context_gating ? '\u2713 Active' : '\u2717 Inactive';
2687
- }
2688
- if (status.injection_detection !== undefined) {
2689
- const el = document.getElementById('injectionStatus');
2690
- el.className = 'protection-card-status ' + (status.injection_detection ? 'active' : 'inactive');
2691
- el.textContent = status.injection_detection ? '\u2713 Active' : '\u2717 Inactive';
2913
+ if (feed.children.length > maxActivityItems) {
2914
+ feed.lastChild.remove();
2915
+ }
2692
2916
  }
2693
- if (status.baseline !== undefined) {
2694
- const el = document.getElementById('baselineStatus');
2695
- el.className = 'protection-card-status ' + (status.baseline ? 'active' : 'inactive');
2696
- el.textContent = status.baseline ? '\u2713 Active' : '\u2717 Inactive';
2917
+
2918
+ // Pending Requests
2919
+ function addPendingRequest(request) {
2920
+ pendingRequests.set(request.requestId, {
2921
+ id: request.requestId,
2922
+ title: request.title,
2923
+ details: request.details,
2924
+ expiresAt: new Date(Date.now() + TIMEOUT_SECONDS * 1000),
2925
+ });
2926
+
2927
+ updatePendingDisplay();
2697
2928
  }
2698
- if (status.audit_trail !== undefined) {
2699
- const el = document.getElementById('auditStatus');
2700
- el.className = 'protection-card-status ' + (status.audit_trail ? 'active' : 'inactive');
2701
- el.textContent = status.audit_trail ? '\u2713 Active' : '\u2717 Inactive';
2929
+
2930
+ function removePendingRequest(requestId) {
2931
+ pendingRequests.delete(requestId);
2932
+ updatePendingDisplay();
2702
2933
  }
2703
- }
2704
2934
 
2705
- // \u2500\u2500 Initialization \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
2935
+ function updatePendingDisplay() {
2936
+ const badge = document.getElementById('pending-item-badge');
2937
+ const count = pendingRequests.size;
2938
+
2939
+ if (count > 0) {
2940
+ document.getElementById('pending-count').textContent = count;
2941
+ badge.style.display = 'flex';
2942
+ } else {
2943
+ badge.style.display = 'none';
2944
+ }
2945
+
2946
+ const overlay = document.getElementById('pending-overlay');
2947
+ const items = document.getElementById('pending-items');
2948
+
2949
+ if (count === 0) {
2950
+ items.innerHTML = '';
2951
+ overlay.classList.remove('show');
2952
+ return;
2953
+ }
2954
+
2955
+ let html = '';
2956
+ for (const req of pendingRequests.values()) {
2957
+ const remaining = Math.max(0, Math.floor((req.expiresAt - Date.now()) / 1000));
2958
+ html += \`
2959
+ <div class="pending-item">
2960
+ <div class="pending-title">\${esc(req.title)}</div>
2961
+ <div class="pending-countdown">Expires in \${remaining}s</div>
2962
+ <div class="pending-actions">
2963
+ <button class="pending-btn pending-approve" data-id="\${req.id}">Approve</button>
2964
+ <button class="pending-btn pending-deny" data-id="\${req.id}">Deny</button>
2965
+ </div>
2966
+ </div>
2967
+ \`;
2968
+ }
2969
+
2970
+ items.innerHTML = html;
2706
2971
 
2707
- (async function init() {
2708
- await exchangeSession();
2709
- // Clean legacy ?token= from URL
2710
- if (window.location.search.includes('token=')) {
2711
- window.history.replaceState({}, '', window.location.pathname);
2972
+ document.querySelectorAll('.pending-approve').forEach((btn) => {
2973
+ btn.addEventListener('click', async () => {
2974
+ const id = btn.getAttribute('data-id');
2975
+ await fetchAPI(\`/api/approve/\${id}\`);
2976
+ });
2977
+ });
2978
+
2979
+ document.querySelectorAll('.pending-deny').forEach((btn) => {
2980
+ btn.addEventListener('click', async () => {
2981
+ const id = btn.getAttribute('data-id');
2982
+ await fetchAPI(\`/api/deny/\${id}\`);
2983
+ });
2984
+ });
2712
2985
  }
2713
- connect();
2714
2986
 
2715
- // Start uptime ticker
2716
- setInterval(updateUptime, 1000);
2717
- updateUptime();
2987
+ // Threat Panel
2988
+ function addThreatAlert(alert) {
2989
+ const panel = document.querySelector('.threat-panel');
2990
+ const content = document.getElementById('threat-alerts');
2718
2991
 
2719
- // Pending request countdown timer
2720
- setInterval(() => {
2721
- for (const [id, req] of pendingRequests) {
2722
- req.remaining = Math.max(0, req.remaining - 1);
2723
- const el = document.getElementById('timer-' + id);
2724
- if (el) {
2725
- el.textContent = req.remaining + 's';
2726
- }
2992
+ if (content.querySelector('.empty-state')) {
2993
+ content.innerHTML = '';
2727
2994
  }
2728
- }, 1000);
2729
2995
 
2730
- // Load initial status
2731
- try {
2732
- const resp = await fetch('/api/status', { headers: authHeaders() });
2733
- if (resp.ok) {
2734
- const status = await resp.json();
2735
- if (status.baseline) updateBaseline(status.baseline);
2736
- if (status.policy) updatePolicy(status.policy);
2996
+ panel.classList.remove('collapsed');
2997
+
2998
+ const html = \`
2999
+ <div class="threat-alert">
3000
+ <div class="threat-type">\${esc(alert.type || 'Injection Alert')}</div>
3001
+ <div class="threat-message">\${esc(alert.message || alert.pattern || '\u2014')}</div>
3002
+ </div>
3003
+ \`;
3004
+
3005
+ content.insertAdjacentHTML('afterbegin', html);
3006
+
3007
+ const alerts = content.querySelectorAll('.threat-alert');
3008
+ if (alerts.length > 10) {
3009
+ alerts[alerts.length - 1].remove();
2737
3010
  }
2738
- } catch (e) {
2739
- // Ignore
2740
3011
  }
2741
- })();
2742
3012
 
2743
- })();
2744
- </script>
3013
+ // Threat Panel Toggle
3014
+ document.querySelector('.threat-header').addEventListener('click', () => {
3015
+ document.querySelector('.threat-panel').classList.toggle('collapsed');
3016
+ });
3017
+
3018
+ // SHR Copy Button
3019
+ document.getElementById('copy-shr-btn').addEventListener('click', async () => {
3020
+ if (!apiState.shr) return;
3021
+
3022
+ const json = JSON.stringify(apiState.shr, null, 2);
3023
+ try {
3024
+ await navigator.clipboard.writeText(json);
3025
+ const btn = document.getElementById('copy-shr-btn');
3026
+ const original = btn.textContent;
3027
+ btn.textContent = 'Copied!';
3028
+ setTimeout(() => {
3029
+ btn.textContent = original;
3030
+ }, 2000);
3031
+ } catch (err) {
3032
+ console.error('Copy failed:', err);
3033
+ }
3034
+ });
3035
+
3036
+ // Pending Overlay Toggle
3037
+ document.getElementById('pending-item-badge').addEventListener('click', () => {
3038
+ document.getElementById('pending-overlay').classList.toggle('show');
3039
+ });
3040
+
3041
+ // Initialize
3042
+ async function initialize() {
3043
+ if (!AUTH_TOKEN) {
3044
+ redirectToLogin();
3045
+ return;
3046
+ }
3047
+
3048
+ // Initial data fetch
3049
+ await Promise.all([
3050
+ updateSovereignty(),
3051
+ updateIdentity(),
3052
+ updateHandshakes(),
3053
+ updateSHR(),
3054
+ updateStatus(),
3055
+ ]);
3056
+
3057
+ // Setup SSE for real-time updates
3058
+ setupSSE();
3059
+
3060
+ // Refresh status periodically
3061
+ setInterval(updateStatus, 30000);
3062
+ }
2745
3063
 
3064
+ // Start
3065
+ initialize();
3066
+ </script>
2746
3067
  </body>
2747
3068
  </html>`;
2748
3069
  }
@@ -8284,6 +8605,155 @@ function deriveTrustTier(level) {
8284
8605
  }
8285
8606
  }
8286
8607
 
8608
+ // src/handshake/attestation.ts
8609
+ init_encoding();
8610
+ init_key_derivation();
8611
+ init_encoding();
8612
+ var ATTESTATION_VERSION = "1.0";
8613
+ function deriveTrustTier2(level) {
8614
+ switch (level) {
8615
+ case "full":
8616
+ return "verified-sovereign";
8617
+ case "degraded":
8618
+ return "verified-degraded";
8619
+ default:
8620
+ return "unverified";
8621
+ }
8622
+ }
8623
+ function generateAttestation(opts) {
8624
+ const {
8625
+ attesterSHR,
8626
+ subjectSHR,
8627
+ verificationResult,
8628
+ mutual = false,
8629
+ identityManager,
8630
+ masterKey,
8631
+ identityId
8632
+ } = opts;
8633
+ const identity = identityId ? identityManager.get(identityId) : identityManager.getDefault();
8634
+ if (!identity) {
8635
+ return { error: "No identity available for signing attestation" };
8636
+ }
8637
+ const now = /* @__PURE__ */ new Date();
8638
+ const attesterExpiry = new Date(attesterSHR.body.expires_at);
8639
+ const subjectExpiry = new Date(subjectSHR.body.expires_at);
8640
+ const earliestExpiry = attesterExpiry < subjectExpiry ? attesterExpiry : subjectExpiry;
8641
+ const sovereigntyLevel = verificationResult.valid ? verificationResult.sovereignty_level : "unverified";
8642
+ const body = {
8643
+ attestation_version: ATTESTATION_VERSION,
8644
+ attester_id: attesterSHR.body.instance_id,
8645
+ subject_id: subjectSHR.body.instance_id,
8646
+ attester_shr: attesterSHR,
8647
+ subject_shr: subjectSHR,
8648
+ verification: {
8649
+ subject_shr_valid: verificationResult.valid,
8650
+ subject_sovereignty_level: sovereigntyLevel,
8651
+ subject_trust_tier: deriveTrustTier2(sovereigntyLevel),
8652
+ mutual,
8653
+ errors: verificationResult.errors,
8654
+ warnings: verificationResult.warnings
8655
+ },
8656
+ attested_at: now.toISOString(),
8657
+ expires_at: earliestExpiry.toISOString()
8658
+ };
8659
+ const canonical = JSON.stringify(deepSortKeys(body));
8660
+ const payload = stringToBytes(canonical);
8661
+ const encryptionKey = derivePurposeKey(masterKey, "identity-encryption");
8662
+ const signatureBytes = sign(
8663
+ payload,
8664
+ identity.encrypted_private_key,
8665
+ encryptionKey
8666
+ );
8667
+ const summary = generateSummary(body);
8668
+ return {
8669
+ body,
8670
+ signed_by: identity.public_key,
8671
+ signature: toBase64url(signatureBytes),
8672
+ summary
8673
+ };
8674
+ }
8675
+ function layerLine(label, status) {
8676
+ const icon = status === "active" ? "\u2713" : status === "degraded" ? "~" : "x";
8677
+ return ` ${icon} ${label}: ${status}`;
8678
+ }
8679
+ function generateSummary(body) {
8680
+ const v = body.verification;
8681
+ const sLayers = body.subject_shr.body.layers;
8682
+ const aLayers = body.attester_shr.body.layers;
8683
+ const tierLabel = v.subject_trust_tier === "verified-sovereign" ? "Verified Sovereign" : v.subject_trust_tier === "verified-degraded" ? "Verified (Degraded)" : "Unverified";
8684
+ const lines = [
8685
+ `--- Sovereignty Attestation ---`,
8686
+ ``,
8687
+ `Attester: ${body.attester_id.slice(0, 16)}...`,
8688
+ `Subject: ${body.subject_id.slice(0, 16)}...`,
8689
+ `Result: ${tierLabel}`,
8690
+ ``,
8691
+ `Subject Sovereignty Posture:`,
8692
+ layerLine("L1 Cognitive Sovereignty", sLayers.l1.status),
8693
+ layerLine("L2 Operational Isolation", sLayers.l2.status),
8694
+ layerLine("L3 Selective Disclosure", sLayers.l3.status),
8695
+ layerLine("L4 Verifiable Reputation", sLayers.l4.status),
8696
+ ``,
8697
+ `Attester Sovereignty Posture:`,
8698
+ layerLine("L1 Cognitive Sovereignty", aLayers.l1.status),
8699
+ layerLine("L2 Operational Isolation", aLayers.l2.status),
8700
+ layerLine("L3 Selective Disclosure", aLayers.l3.status),
8701
+ layerLine("L4 Verifiable Reputation", aLayers.l4.status),
8702
+ ``,
8703
+ `Mutual: ${v.mutual ? "Yes" : "One-sided"}`,
8704
+ `Attested: ${body.attested_at}`,
8705
+ `Expires: ${body.expires_at}`,
8706
+ `Signature: ${body.attestation_version} / Ed25519`
8707
+ ];
8708
+ if (v.warnings.length > 0) {
8709
+ lines.push(``, `Warnings: ${v.warnings.join("; ")}`);
8710
+ }
8711
+ if (v.errors.length > 0) {
8712
+ lines.push(``, `Errors: ${v.errors.join("; ")}`);
8713
+ }
8714
+ lines.push(``, `--- Verify: compare signed_by against attester's known public key ---`);
8715
+ return lines.join("\n");
8716
+ }
8717
+ function verifyAttestation(attestation, now) {
8718
+ const errors = [];
8719
+ const currentTime = /* @__PURE__ */ new Date();
8720
+ if (attestation.body.attestation_version !== ATTESTATION_VERSION) {
8721
+ errors.push(
8722
+ `Unsupported attestation version: ${attestation.body.attestation_version}`
8723
+ );
8724
+ }
8725
+ if (!attestation.body.attester_id || !attestation.body.subject_id) {
8726
+ errors.push("Missing attester_id or subject_id");
8727
+ }
8728
+ if (!attestation.body.attester_shr || !attestation.body.subject_shr) {
8729
+ errors.push("Missing attester or subject SHR");
8730
+ }
8731
+ const expired = new Date(attestation.body.expires_at) <= currentTime;
8732
+ if (expired) {
8733
+ errors.push("Attestation has expired");
8734
+ }
8735
+ try {
8736
+ const publicKey = fromBase64url(attestation.signed_by);
8737
+ const canonical = JSON.stringify(deepSortKeys(attestation.body));
8738
+ const payload = stringToBytes(canonical);
8739
+ const signatureBytes = fromBase64url(attestation.signature);
8740
+ const signatureValid = verify(payload, signatureBytes, publicKey);
8741
+ if (!signatureValid) {
8742
+ errors.push("Attestation signature is invalid");
8743
+ }
8744
+ } catch (e) {
8745
+ errors.push(`Signature verification error: ${e.message}`);
8746
+ }
8747
+ return {
8748
+ valid: errors.length === 0,
8749
+ errors,
8750
+ attester_id: attestation.body.attester_id ?? "unknown",
8751
+ subject_id: attestation.body.subject_id ?? "unknown",
8752
+ trust_tier: errors.length === 0 ? attestation.body.verification.subject_trust_tier : "unverified",
8753
+ expired
8754
+ };
8755
+ }
8756
+
8287
8757
  // src/handshake/tools.ts
8288
8758
  function createHandshakeTools(config, identityManager, masterKey, auditLog) {
8289
8759
  const sessions = /* @__PURE__ */ new Map();
@@ -8469,6 +8939,103 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
8469
8939
  result: session.result ?? null
8470
8940
  });
8471
8941
  }
8942
+ },
8943
+ // ─── Streamlined Exchange ─────────────────────────────────────────
8944
+ {
8945
+ name: "sanctuary/handshake_exchange",
8946
+ description: "One-shot sovereignty exchange. Accepts a counterparty's signed SHR, verifies it, generates our SHR, and produces a signed attestation artifact \u2014 all in a single call. Returns a shareable attestation with human-readable summary. Use this instead of the 4-step handshake protocol when you want a quick, portable sovereignty verification (e.g., for social posting or async exchanges).",
8947
+ inputSchema: {
8948
+ type: "object",
8949
+ properties: {
8950
+ counterparty_shr: {
8951
+ type: "object",
8952
+ description: "The counterparty's signed SHR (SignedSHR object with body, signed_by, signature)."
8953
+ },
8954
+ identity_id: {
8955
+ type: "string",
8956
+ description: "Identity to use for the exchange. Defaults to primary identity."
8957
+ }
8958
+ },
8959
+ required: ["counterparty_shr"]
8960
+ },
8961
+ handler: async (args) => {
8962
+ const counterpartySHR = args.counterparty_shr;
8963
+ const ourSHR = generateSHR(args.identity_id, shrOpts);
8964
+ if (typeof ourSHR === "string") {
8965
+ return toolResult({ error: ourSHR });
8966
+ }
8967
+ const verificationResult = verifySHR(counterpartySHR);
8968
+ const attestation = generateAttestation({
8969
+ attesterSHR: ourSHR,
8970
+ subjectSHR: counterpartySHR,
8971
+ verificationResult,
8972
+ mutual: false,
8973
+ identityManager,
8974
+ masterKey,
8975
+ identityId: args.identity_id
8976
+ });
8977
+ if ("error" in attestation) {
8978
+ auditLog.append("l4", "handshake_exchange", ourSHR.body.instance_id, void 0, "failure");
8979
+ return toolResult({ error: attestation.error });
8980
+ }
8981
+ if (verificationResult.valid) {
8982
+ const sovereigntyLevel = verificationResult.sovereignty_level;
8983
+ const trustTier = sovereigntyLevel === "full" ? "verified-sovereign" : sovereigntyLevel === "degraded" ? "verified-degraded" : "unverified";
8984
+ handshakeResults.set(verificationResult.counterparty_id, {
8985
+ counterparty_id: verificationResult.counterparty_id,
8986
+ counterparty_shr: counterpartySHR,
8987
+ verified: true,
8988
+ sovereignty_level: sovereigntyLevel,
8989
+ trust_tier: trustTier,
8990
+ completed_at: (/* @__PURE__ */ new Date()).toISOString(),
8991
+ expires_at: verificationResult.expires_at,
8992
+ errors: []
8993
+ });
8994
+ }
8995
+ auditLog.append("l4", "handshake_exchange", ourSHR.body.instance_id);
8996
+ return toolResult({
8997
+ attestation,
8998
+ our_shr: ourSHR,
8999
+ verification: {
9000
+ counterparty_valid: verificationResult.valid,
9001
+ counterparty_sovereignty: verificationResult.sovereignty_level,
9002
+ counterparty_id: verificationResult.counterparty_id,
9003
+ errors: verificationResult.errors,
9004
+ warnings: verificationResult.warnings
9005
+ },
9006
+ instructions: "The 'attestation' object is a signed, portable sovereignty verification artifact. Share it with the counterparty or post attestation.summary publicly. The counterparty can verify the attestation signature using your public key. Our SHR is included so the counterparty can perform their own verification of us.",
9007
+ _content_trust: "external"
9008
+ });
9009
+ }
9010
+ },
9011
+ {
9012
+ name: "sanctuary/handshake_verify_attestation",
9013
+ description: "Verify a signed attestation artifact from another agent. Checks the Ed25519 signature, temporal validity, and structural integrity.",
9014
+ inputSchema: {
9015
+ type: "object",
9016
+ properties: {
9017
+ attestation: {
9018
+ type: "object",
9019
+ description: "The SignedAttestation object to verify (body, signed_by, signature, summary)."
9020
+ }
9021
+ },
9022
+ required: ["attestation"]
9023
+ },
9024
+ handler: async (args) => {
9025
+ const attestation = args.attestation;
9026
+ const result = verifyAttestation(attestation);
9027
+ auditLog.append(
9028
+ "l4",
9029
+ "handshake_verify_attestation",
9030
+ result.attester_id,
9031
+ void 0,
9032
+ result.valid ? "success" : "failure"
9033
+ );
9034
+ return toolResult({
9035
+ ...result,
9036
+ _content_trust: "external"
9037
+ });
9038
+ }
8472
9039
  }
8473
9040
  ];
8474
9041
  return { tools, handshakeResults };