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