@sanctuary-framework/mcp-server 0.5.5 → 0.5.7

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
@@ -9,6 +9,7 @@ var crypto = require('crypto');
9
9
  var aes_js = require('@noble/ciphers/aes.js');
10
10
  var sha256 = require('@noble/hashes/sha256');
11
11
  var hmac = require('@noble/hashes/hmac');
12
+ var ed25519 = require('@noble/curves/ed25519');
12
13
  var hashWasm = require('hash-wasm');
13
14
  var hkdf = require('@noble/hashes/hkdf');
14
15
  var http = require('http');
@@ -16,7 +17,6 @@ var https = require('https');
16
17
  var fs = require('fs');
17
18
  var child_process = require('child_process');
18
19
  var stdio_js = require('@modelcontextprotocol/sdk/server/stdio.js');
19
- var ed25519 = require('@noble/curves/ed25519');
20
20
  var index_js = require('@modelcontextprotocol/sdk/server/index.js');
21
21
  var types_js = require('@modelcontextprotocol/sdk/types.js');
22
22
 
@@ -560,6 +560,111 @@ var init_hashing = __esm({
560
560
  init_encoding();
561
561
  }
562
562
  });
563
+ function generateKeypair() {
564
+ const privateKey = randomBytes(32);
565
+ const publicKey = ed25519.ed25519.getPublicKey(privateKey);
566
+ return { publicKey, privateKey };
567
+ }
568
+ function publicKeyToDid(publicKey) {
569
+ const multicodec = new Uint8Array([237, 1, ...publicKey]);
570
+ return `did:key:z${toBase64url(multicodec)}`;
571
+ }
572
+ function generateIdentityId(publicKey) {
573
+ const keyHash = hash(publicKey);
574
+ return Array.from(keyHash.slice(0, 16)).map((b) => b.toString(16).padStart(2, "0")).join("");
575
+ }
576
+ function createIdentity(label, encryptionKey, keyProtection) {
577
+ const { publicKey, privateKey } = generateKeypair();
578
+ const identityId = generateIdentityId(publicKey);
579
+ const did = publicKeyToDid(publicKey);
580
+ const now = (/* @__PURE__ */ new Date()).toISOString();
581
+ const encryptedPrivateKey = encrypt(privateKey, encryptionKey);
582
+ privateKey.fill(0);
583
+ const publicIdentity = {
584
+ identity_id: identityId,
585
+ label,
586
+ public_key: toBase64url(publicKey),
587
+ did,
588
+ created_at: now,
589
+ key_type: "ed25519",
590
+ key_protection: keyProtection
591
+ };
592
+ const storedIdentity = {
593
+ ...publicIdentity,
594
+ encrypted_private_key: encryptedPrivateKey,
595
+ rotation_history: []
596
+ };
597
+ return { publicIdentity, storedIdentity };
598
+ }
599
+ function sign(payload, encryptedPrivateKey, encryptionKey) {
600
+ const privateKey = decrypt(encryptedPrivateKey, encryptionKey);
601
+ try {
602
+ return ed25519.ed25519.sign(payload, privateKey);
603
+ } finally {
604
+ privateKey.fill(0);
605
+ }
606
+ }
607
+ function verify(payload, signature, publicKey) {
608
+ try {
609
+ return ed25519.ed25519.verify(signature, payload, publicKey);
610
+ } catch {
611
+ return false;
612
+ }
613
+ }
614
+ function rotateKeys(storedIdentity, encryptionKey, reason) {
615
+ const { publicKey: newPublicKey, privateKey: newPrivateKey } = generateKeypair();
616
+ const newIdentityDid = publicKeyToDid(newPublicKey);
617
+ const now = (/* @__PURE__ */ new Date()).toISOString();
618
+ const eventData = JSON.stringify({
619
+ old_public_key: storedIdentity.public_key,
620
+ new_public_key: toBase64url(newPublicKey),
621
+ identity_id: storedIdentity.identity_id,
622
+ reason,
623
+ rotated_at: now
624
+ });
625
+ const eventBytes = new TextEncoder().encode(eventData);
626
+ const signature = sign(
627
+ eventBytes,
628
+ storedIdentity.encrypted_private_key,
629
+ encryptionKey
630
+ );
631
+ const rotationEvent = {
632
+ old_public_key: storedIdentity.public_key,
633
+ new_public_key: toBase64url(newPublicKey),
634
+ identity_id: storedIdentity.identity_id,
635
+ reason,
636
+ rotated_at: now,
637
+ signature: toBase64url(signature)
638
+ };
639
+ const encryptedNewPrivateKey = encrypt(newPrivateKey, encryptionKey);
640
+ newPrivateKey.fill(0);
641
+ const updatedIdentity = {
642
+ ...storedIdentity,
643
+ public_key: toBase64url(newPublicKey),
644
+ did: newIdentityDid,
645
+ encrypted_private_key: encryptedNewPrivateKey,
646
+ rotation_history: [
647
+ ...storedIdentity.rotation_history,
648
+ {
649
+ old_public_key: storedIdentity.public_key,
650
+ new_public_key: toBase64url(newPublicKey),
651
+ rotation_event: toBase64url(
652
+ new TextEncoder().encode(JSON.stringify(rotationEvent))
653
+ ),
654
+ rotated_at: now
655
+ }
656
+ ]
657
+ };
658
+ return { updatedIdentity, rotationEvent };
659
+ }
660
+ var init_identity = __esm({
661
+ "src/core/identity.ts"() {
662
+ init_encoding();
663
+ init_encryption();
664
+ init_hashing();
665
+ init_random();
666
+ }
667
+ });
563
668
  async function deriveMasterKey(passphrase, existingParams) {
564
669
  const salt = existingParams ? fromBase64url(existingParams.salt) : generateSalt();
565
670
  const params = existingParams ?? {
@@ -870,6 +975,8 @@ tier3_always_allow:
870
975
  - handshake_respond
871
976
  - handshake_complete
872
977
  - handshake_status
978
+ - handshake_exchange
979
+ - handshake_verify_attestation
873
980
  - reputation_query_weighted
874
981
  - federation_peers
875
982
  - federation_trust_evaluate
@@ -971,6 +1078,8 @@ var init_loader = __esm({
971
1078
  "handshake_respond",
972
1079
  "handshake_complete",
973
1080
  "handshake_status",
1081
+ "handshake_exchange",
1082
+ "handshake_verify_attestation",
974
1083
  "reputation_query_weighted",
975
1084
  "federation_peers",
976
1085
  "federation_trust_evaluate",
@@ -1160,1592 +1269,2023 @@ var init_baseline = __esm({
1160
1269
  }
1161
1270
  });
1162
1271
 
1163
- // src/principal-policy/dashboard-html.ts
1164
- function generateLoginHTML(options) {
1165
- return `<!DOCTYPE html>
1166
- <html lang="en">
1167
- <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>
1282
- </head>
1283
- <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.
1297
- </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 }
1313
- });
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';
1272
+ // src/shr/types.ts
1273
+ function deepSortKeys(obj) {
1274
+ if (obj === null || typeof obj !== "object") return obj;
1275
+ if (Array.isArray(obj)) return obj.map(deepSortKeys);
1276
+ const sorted = {};
1277
+ for (const key of Object.keys(obj).sort()) {
1278
+ sorted[key] = deepSortKeys(obj[key]);
1332
1279
  }
1333
- return false;
1280
+ return sorted;
1334
1281
  }
1335
- </script>
1336
- </body>
1337
- </html>`;
1282
+ function canonicalizeForSigning(body) {
1283
+ return JSON.stringify(deepSortKeys(body));
1338
1284
  }
1339
- function generateDashboardHTML(options) {
1340
- return `<!DOCTYPE html>
1341
- <html lang="en">
1342
- <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
- }
1368
-
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 */
1384
-
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;
1285
+ var init_types = __esm({
1286
+ "src/shr/types.ts"() {
1398
1287
  }
1288
+ });
1399
1289
 
1400
- .status-bar-left {
1401
- display: flex;
1402
- align-items: center;
1403
- gap: 12px;
1404
- flex: 0 0 auto;
1290
+ // src/shr/generator.ts
1291
+ function generateSHR(identityId, opts) {
1292
+ const { config, identityManager, masterKey, validityMs } = opts;
1293
+ const identity = identityId ? identityManager.get(identityId) : identityManager.getDefault();
1294
+ if (!identity) {
1295
+ return "No identity available for signing. Create an identity first.";
1405
1296
  }
1406
-
1407
- .sanctuary-logo {
1408
- font-weight: 700;
1409
- font-size: 16px;
1410
- letter-spacing: -0.5px;
1411
- color: var(--text-primary);
1297
+ const now = /* @__PURE__ */ new Date();
1298
+ const expiresAt = new Date(now.getTime() + (validityMs ?? DEFAULT_VALIDITY_MS));
1299
+ const degradations = [];
1300
+ if (config.execution.environment === "local-process") {
1301
+ degradations.push({
1302
+ layer: "l2",
1303
+ code: "PROCESS_ISOLATION_ONLY",
1304
+ severity: "warning",
1305
+ description: "Process-level isolation only (no TEE)",
1306
+ mitigation: "TEE support planned for a future release"
1307
+ });
1308
+ degradations.push({
1309
+ layer: "l2",
1310
+ code: "SELF_REPORTED_ATTESTATION",
1311
+ severity: "warning",
1312
+ description: "Attestation is self-reported (no hardware root of trust)",
1313
+ mitigation: "TEE attestation planned for a future release"
1314
+ });
1412
1315
  }
1413
-
1414
- .sanctuary-logo span {
1415
- color: var(--blue);
1316
+ const body = {
1317
+ shr_version: "1.0",
1318
+ implementation: {
1319
+ sanctuary_version: config.version,
1320
+ node_version: process.versions.node,
1321
+ generated_by: "sanctuary-mcp-server"
1322
+ },
1323
+ instance_id: identity.identity_id,
1324
+ generated_at: now.toISOString(),
1325
+ expires_at: expiresAt.toISOString(),
1326
+ layers: {
1327
+ l1: {
1328
+ status: "active",
1329
+ encryption: config.state.encryption,
1330
+ key_custody: "self",
1331
+ integrity: config.state.integrity,
1332
+ identity_type: config.state.identity_provider,
1333
+ state_portable: true
1334
+ },
1335
+ l2: {
1336
+ status: config.execution.environment === "local-process" ? "degraded" : "active",
1337
+ isolation_type: config.execution.environment,
1338
+ attestation_available: config.execution.attestation
1339
+ },
1340
+ l3: {
1341
+ status: "active",
1342
+ proof_system: config.disclosure.proof_system,
1343
+ selective_disclosure: true
1344
+ },
1345
+ l4: {
1346
+ status: "active",
1347
+ reputation_mode: config.reputation.mode,
1348
+ attestation_format: config.reputation.attestation_format,
1349
+ reputation_portable: true
1350
+ }
1351
+ },
1352
+ capabilities: {
1353
+ handshake: true,
1354
+ shr_exchange: true,
1355
+ reputation_verify: true,
1356
+ encrypted_channel: false
1357
+ // Not yet implemented
1358
+ },
1359
+ degradations
1360
+ };
1361
+ const canonical = canonicalizeForSigning(body);
1362
+ const payload = stringToBytes(canonical);
1363
+ const encryptionKey = derivePurposeKey(masterKey, "identity-encryption");
1364
+ const signatureBytes = sign(
1365
+ payload,
1366
+ identity.encrypted_private_key,
1367
+ encryptionKey
1368
+ );
1369
+ return {
1370
+ body,
1371
+ signed_by: identity.public_key,
1372
+ signature: toBase64url(signatureBytes)
1373
+ };
1374
+ }
1375
+ var DEFAULT_VALIDITY_MS;
1376
+ var init_generator = __esm({
1377
+ "src/shr/generator.ts"() {
1378
+ init_types();
1379
+ init_identity();
1380
+ init_encoding();
1381
+ init_key_derivation();
1382
+ DEFAULT_VALIDITY_MS = 60 * 60 * 1e3;
1416
1383
  }
1384
+ });
1417
1385
 
1418
- .version {
1419
- font-size: 11px;
1420
- color: var(--text-secondary);
1421
- font-family: var(--mono);
1422
- }
1386
+ // src/principal-policy/dashboard-html.ts
1387
+ function generateLoginHTML(options) {
1388
+ return `<!DOCTYPE html>
1389
+ <html lang="en">
1390
+ <head>
1391
+ <meta charset="UTF-8">
1392
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1393
+ <title>Sanctuary \u2014 Principal Dashboard</title>
1394
+ <style>
1395
+ :root {
1396
+ --bg: #0d1117;
1397
+ --surface: #161b22;
1398
+ --border: #30363d;
1399
+ --text-primary: #e6edf3;
1400
+ --text-secondary: #8b949e;
1401
+ --green: #3fb950;
1402
+ --amber: #d29922;
1403
+ --red: #f85149;
1404
+ --blue: #58a6ff;
1405
+ }
1406
+
1407
+ * {
1408
+ margin: 0;
1409
+ padding: 0;
1410
+ box-sizing: border-box;
1411
+ }
1412
+
1413
+ body {
1414
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
1415
+ background-color: var(--bg);
1416
+ color: var(--text-primary);
1417
+ min-height: 100vh;
1418
+ display: flex;
1419
+ align-items: center;
1420
+ justify-content: center;
1421
+ }
1422
+
1423
+ .login-container {
1424
+ width: 100%;
1425
+ max-width: 400px;
1426
+ padding: 20px;
1427
+ }
1423
1428
 
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
- }
1429
+ .login-card {
1430
+ background-color: var(--surface);
1431
+ border: 1px solid var(--border);
1432
+ border-radius: 8px;
1433
+ padding: 40px 32px;
1434
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
1435
+ }
1442
1436
 
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
- }
1437
+ .login-header {
1438
+ display: flex;
1439
+ align-items: center;
1440
+ gap: 12px;
1441
+ margin-bottom: 32px;
1442
+ }
1456
1443
 
1457
- .sovereignty-score.high {
1458
- background: var(--green);
1459
- }
1444
+ .logo {
1445
+ font-size: 24px;
1446
+ font-weight: 700;
1447
+ color: var(--blue);
1448
+ }
1460
1449
 
1461
- .sovereignty-score.medium {
1462
- background: var(--amber);
1463
- }
1450
+ .logo-text {
1451
+ display: flex;
1452
+ flex-direction: column;
1453
+ }
1464
1454
 
1465
- .sovereignty-score.low {
1466
- background: var(--red);
1467
- }
1455
+ .logo-text .title {
1456
+ font-size: 18px;
1457
+ font-weight: 600;
1458
+ letter-spacing: -0.5px;
1459
+ }
1468
1460
 
1469
- .status-bar-right {
1470
- display: flex;
1471
- align-items: center;
1472
- gap: 16px;
1473
- flex: 0 0 auto;
1474
- }
1461
+ .logo-text .version {
1462
+ font-size: 12px;
1463
+ color: var(--text-secondary);
1464
+ margin-top: 2px;
1465
+ }
1475
1466
 
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
- }
1467
+ .form-group {
1468
+ margin-bottom: 24px;
1469
+ }
1484
1470
 
1485
- .protections-indicator .count {
1486
- color: var(--text-primary);
1487
- font-weight: 600;
1488
- }
1471
+ label {
1472
+ display: block;
1473
+ font-size: 14px;
1474
+ font-weight: 500;
1475
+ margin-bottom: 8px;
1476
+ color: var(--text-primary);
1477
+ }
1489
1478
 
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
- }
1479
+ input[type="text"],
1480
+ input[type="password"] {
1481
+ width: 100%;
1482
+ padding: 10px 12px;
1483
+ background-color: var(--bg);
1484
+ border: 1px solid var(--border);
1485
+ border-radius: 6px;
1486
+ color: var(--text-primary);
1487
+ font-size: 14px;
1488
+ font-family: 'JetBrains Mono', monospace;
1489
+ transition: border-color 0.2s;
1490
+ }
1498
1491
 
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
- }
1492
+ input[type="text"]:focus,
1493
+ input[type="password"]:focus {
1494
+ outline: none;
1495
+ border-color: var(--blue);
1496
+ box-shadow: 0 0 0 2px rgba(88, 166, 255, 0.1);
1497
+ }
1506
1498
 
1507
- .status-dot.disconnected {
1508
- background: var(--red);
1509
- animation: none;
1510
- }
1499
+ .error-message {
1500
+ display: none;
1501
+ background-color: rgba(248, 81, 73, 0.1);
1502
+ border: 1px solid var(--red);
1503
+ color: #ff9999;
1504
+ padding: 12px;
1505
+ border-radius: 6px;
1506
+ font-size: 13px;
1507
+ margin-bottom: 20px;
1508
+ }
1511
1509
 
1512
- @keyframes pulse {
1513
- 0%, 100% { opacity: 1; }
1514
- 50% { opacity: 0.5; }
1515
- }
1510
+ .error-message.show {
1511
+ display: block;
1512
+ }
1516
1513
 
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
- }
1514
+ button {
1515
+ width: 100%;
1516
+ padding: 10px 16px;
1517
+ background-color: var(--blue);
1518
+ color: var(--bg);
1519
+ border: none;
1520
+ border-radius: 6px;
1521
+ font-size: 14px;
1522
+ font-weight: 600;
1523
+ cursor: pointer;
1524
+ transition: background-color 0.2s;
1525
+ }
1531
1526
 
1532
- .pending-badge.hidden {
1533
- display: none;
1534
- }
1527
+ button:hover {
1528
+ background-color: #79c0ff;
1529
+ }
1535
1530
 
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 */
1531
+ button:active {
1532
+ background-color: #4184e4;
1533
+ }
1537
1534
 
1538
- .main-container {
1539
- flex: 1;
1540
- display: flex;
1541
- margin-top: 56px;
1542
- overflow: hidden;
1543
- }
1535
+ button:disabled {
1536
+ background-color: var(--text-secondary);
1537
+ cursor: not-allowed;
1538
+ opacity: 0.5;
1539
+ }
1544
1540
 
1545
- .activity-feed {
1546
- flex: 3;
1547
- display: flex;
1548
- flex-direction: column;
1549
- border-right: 1px solid var(--border);
1550
- overflow: hidden;
1551
- }
1541
+ .info-text {
1542
+ font-size: 12px;
1543
+ color: var(--text-secondary);
1544
+ margin-top: 16px;
1545
+ text-align: center;
1546
+ }
1547
+ </style>
1548
+ </head>
1549
+ <body>
1550
+ <div class="login-container">
1551
+ <div class="login-card">
1552
+ <div class="login-header">
1553
+ <div class="logo">\u25C6</div>
1554
+ <div class="logo-text">
1555
+ <div class="title">SANCTUARY</div>
1556
+ <div class="version">v${options.serverVersion}</div>
1557
+ </div>
1558
+ </div>
1552
1559
 
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
- }
1560
+ <div id="error-message" class="error-message"></div>
1561
+
1562
+ <form id="login-form">
1563
+ <div class="form-group">
1564
+ <label for="auth-token">Auth Token</label>
1565
+ <input
1566
+ type="text"
1567
+ id="auth-token"
1568
+ name="token"
1569
+ placeholder="Paste your session token..."
1570
+ autocomplete="off"
1571
+ spellcheck="false"
1572
+ required
1573
+ />
1574
+ </div>
1575
+
1576
+ <button type="submit" id="login-button">Open Dashboard</button>
1577
+ </form>
1578
+
1579
+ <div class="info-text">
1580
+ Session tokens expire after 1 hour of inactivity
1581
+ </div>
1582
+ </div>
1583
+ </div>
1565
1584
 
1566
- .feed-header-dot {
1567
- width: 6px;
1568
- height: 6px;
1569
- border-radius: 50%;
1570
- background: var(--green);
1571
- }
1585
+ <script>
1586
+ const loginForm = document.getElementById('login-form');
1587
+ const authTokenInput = document.getElementById('auth-token');
1588
+ const errorMessage = document.getElementById('error-message');
1589
+ const loginButton = document.getElementById('login-button');
1572
1590
 
1573
- .activity-list {
1574
- flex: 1;
1575
- overflow-y: auto;
1576
- overflow-x: hidden;
1577
- }
1591
+ loginForm.addEventListener('submit', async (e) => {
1592
+ e.preventDefault();
1593
+ const token = authTokenInput.value.trim();
1578
1594
 
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
- }
1595
+ if (!token) {
1596
+ showError('Token is required');
1597
+ return;
1598
+ }
1590
1599
 
1591
- .activity-item:hover {
1592
- background: rgba(88, 166, 255, 0.05);
1593
- }
1600
+ loginButton.disabled = true;
1601
+ loginButton.textContent = 'Verifying...';
1602
+ errorMessage.classList.remove('show');
1594
1603
 
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
- }
1604
+ try {
1605
+ const response = await fetch('/auth/session', {
1606
+ method: 'POST',
1607
+ headers: {
1608
+ 'Content-Type': 'application/json',
1609
+ 'Authorization': 'Bearer ' + token,
1610
+ },
1611
+ body: JSON.stringify({ token }),
1612
+ });
1603
1613
 
1604
- .activity-item-content {
1605
- flex: 1;
1606
- min-width: 0;
1607
- }
1614
+ if (response.ok) {
1615
+ const data = await response.json();
1616
+ sessionStorage.setItem('authToken', token);
1617
+ window.location.href = '/dashboard';
1618
+ } else if (response.status === 401) {
1619
+ showError('Invalid token. Please check and try again.');
1620
+ } else {
1621
+ showError('Authentication failed. Please try again.');
1622
+ }
1623
+ } catch (err) {
1624
+ showError('Connection error. Please check your network.');
1625
+ } finally {
1626
+ loginButton.disabled = false;
1627
+ loginButton.textContent = 'Open Dashboard';
1628
+ }
1629
+ });
1608
1630
 
1609
- .activity-time {
1610
- color: var(--text-secondary);
1611
- font-size: 11px;
1612
- margin-bottom: 2px;
1613
- }
1631
+ function showError(message) {
1632
+ errorMessage.textContent = message;
1633
+ errorMessage.classList.add('show');
1634
+ }
1614
1635
 
1615
- .activity-main {
1616
- display: flex;
1617
- gap: 8px;
1618
- align-items: baseline;
1619
- margin-bottom: 4px;
1620
- }
1636
+ authTokenInput.addEventListener('input', () => {
1637
+ errorMessage.classList.remove('show');
1638
+ });
1639
+ </script>
1640
+ </body>
1641
+ </html>`;
1642
+ }
1643
+ function generateDashboardHTML(options) {
1644
+ return `<!DOCTYPE html>
1645
+ <html lang="en">
1646
+ <head>
1647
+ <meta charset="UTF-8">
1648
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1649
+ <title>Sanctuary \u2014 Principal Dashboard</title>
1650
+ <style>
1651
+ :root {
1652
+ --bg: #0d1117;
1653
+ --surface: #161b22;
1654
+ --border: #30363d;
1655
+ --text-primary: #e6edf3;
1656
+ --text-secondary: #8b949e;
1657
+ --green: #3fb950;
1658
+ --amber: #d29922;
1659
+ --red: #f85149;
1660
+ --blue: #58a6ff;
1661
+ --success: #3fb950;
1662
+ --warning: #d29922;
1663
+ --error: #f85149;
1664
+ --muted: #21262d;
1665
+ }
1666
+
1667
+ * {
1668
+ margin: 0;
1669
+ padding: 0;
1670
+ box-sizing: border-box;
1671
+ }
1672
+
1673
+ html, body {
1674
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
1675
+ background-color: var(--bg);
1676
+ color: var(--text-primary);
1677
+ height: 100%;
1678
+ overflow: hidden;
1679
+ }
1680
+
1681
+ body {
1682
+ display: flex;
1683
+ flex-direction: column;
1684
+ }
1685
+
1686
+ /* Status Bar */
1687
+ .status-bar {
1688
+ position: fixed;
1689
+ top: 0;
1690
+ left: 0;
1691
+ right: 0;
1692
+ height: 56px;
1693
+ background-color: var(--surface);
1694
+ border-bottom: 1px solid var(--border);
1695
+ display: flex;
1696
+ align-items: center;
1697
+ padding: 0 24px;
1698
+ gap: 24px;
1699
+ z-index: 100;
1700
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
1701
+ }
1702
+
1703
+ .status-bar-left {
1704
+ display: flex;
1705
+ align-items: center;
1706
+ gap: 12px;
1707
+ flex: 0 0 auto;
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
+ .logo-icon {
1711
+ font-size: 20px;
1712
+ color: var(--blue);
1713
+ font-weight: 700;
1714
+ }
1634
1715
 
1635
- .activity-tier.t1 {
1636
- background: rgba(248, 81, 73, 0.2);
1637
- color: var(--red);
1638
- }
1716
+ .logo-info {
1717
+ display: flex;
1718
+ flex-direction: column;
1719
+ }
1639
1720
 
1640
- .activity-tier.t2 {
1641
- background: rgba(210, 153, 34, 0.2);
1642
- color: var(--amber);
1643
- }
1721
+ .logo-title {
1722
+ font-size: 13px;
1723
+ font-weight: 600;
1724
+ line-height: 1;
1725
+ color: var(--text-primary);
1726
+ }
1644
1727
 
1645
- .activity-tier.t3 {
1646
- background: rgba(63, 185, 80, 0.2);
1647
- color: var(--green);
1648
- }
1728
+ .logo-version {
1729
+ font-size: 11px;
1730
+ color: var(--text-secondary);
1731
+ margin-top: 2px;
1732
+ }
1649
1733
 
1650
- .activity-tool {
1651
- color: var(--text-primary);
1652
- font-weight: 600;
1653
- }
1734
+ .status-bar-center {
1735
+ flex: 1;
1736
+ display: flex;
1737
+ justify-content: center;
1738
+ }
1654
1739
 
1655
- .activity-outcome {
1656
- color: var(--green);
1657
- }
1740
+ .sovereignty-badge {
1741
+ display: flex;
1742
+ align-items: center;
1743
+ gap: 8px;
1744
+ padding: 8px 16px;
1745
+ background-color: rgba(63, 185, 80, 0.1);
1746
+ border: 1px solid rgba(63, 185, 80, 0.3);
1747
+ border-radius: 6px;
1748
+ font-size: 13px;
1749
+ font-weight: 500;
1750
+ }
1658
1751
 
1659
- .activity-outcome.denied {
1660
- color: var(--red);
1661
- }
1752
+ .sovereignty-badge.degraded {
1753
+ background-color: rgba(210, 153, 34, 0.1);
1754
+ border-color: rgba(210, 153, 34, 0.3);
1755
+ }
1662
1756
 
1663
- .activity-detail {
1664
- font-size: 12px;
1665
- color: var(--text-secondary);
1666
- margin-left: 0;
1667
- }
1757
+ .sovereignty-badge.inactive {
1758
+ background-color: rgba(248, 81, 73, 0.1);
1759
+ border-color: rgba(248, 81, 73, 0.3);
1760
+ }
1668
1761
 
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
- }
1762
+ .sovereignty-score {
1763
+ font-weight: 700;
1764
+ color: var(--green);
1765
+ }
1677
1766
 
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
- }
1767
+ .sovereignty-badge.degraded .sovereignty-score {
1768
+ color: var(--amber);
1769
+ }
1686
1770
 
1687
- .activity-empty-icon {
1688
- font-size: 32px;
1689
- margin-bottom: 12px;
1690
- }
1771
+ .sovereignty-badge.inactive .sovereignty-score {
1772
+ color: var(--red);
1773
+ }
1691
1774
 
1692
- .activity-empty-text {
1693
- font-size: 14px;
1694
- }
1775
+ .status-bar-right {
1776
+ display: flex;
1777
+ align-items: center;
1778
+ gap: 16px;
1779
+ flex: 0 0 auto;
1780
+ }
1695
1781
 
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 */
1782
+ .status-item {
1783
+ display: flex;
1784
+ align-items: center;
1785
+ gap: 6px;
1786
+ font-size: 12px;
1787
+ color: var(--text-secondary);
1788
+ }
1697
1789
 
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
- }
1790
+ .status-item strong {
1791
+ color: var(--text-primary);
1792
+ font-weight: 500;
1793
+ }
1705
1794
 
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
- }
1795
+ .status-dot {
1796
+ width: 8px;
1797
+ height: 8px;
1798
+ border-radius: 50%;
1799
+ background-color: var(--green);
1800
+ }
1718
1801
 
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
- }
1802
+ .status-dot.disconnected {
1803
+ background-color: var(--red);
1804
+ }
1727
1805
 
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
- }
1806
+ .pending-badge {
1807
+ display: flex;
1808
+ align-items: center;
1809
+ gap: 6px;
1810
+ padding: 4px 8px;
1811
+ background-color: var(--blue);
1812
+ color: var(--bg);
1813
+ border-radius: 4px;
1814
+ font-size: 11px;
1815
+ font-weight: 600;
1816
+ cursor: pointer;
1817
+ }
1737
1818
 
1738
- .protection-card-icon {
1739
- font-size: 14px;
1740
- }
1819
+ /* Main Content */
1820
+ .main-content {
1821
+ flex: 1;
1822
+ margin-top: 56px;
1823
+ overflow-y: auto;
1824
+ padding: 24px;
1825
+ }
1741
1826
 
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
- }
1827
+ .grid {
1828
+ display: grid;
1829
+ gap: 20px;
1830
+ }
1749
1831
 
1750
- .protection-card-status {
1751
- display: flex;
1752
- align-items: center;
1753
- gap: 6px;
1754
- font-size: 12px;
1755
- font-weight: 600;
1756
- }
1832
+ /* Row 1: Sovereignty Layers */
1833
+ .sovereignty-layers {
1834
+ display: grid;
1835
+ grid-template-columns: repeat(4, 1fr);
1836
+ gap: 16px;
1837
+ }
1757
1838
 
1758
- .protection-card-status.active {
1759
- color: var(--green);
1760
- }
1839
+ .layer-card {
1840
+ background-color: var(--surface);
1841
+ border: 1px solid var(--border);
1842
+ border-radius: 8px;
1843
+ padding: 20px;
1844
+ display: flex;
1845
+ flex-direction: column;
1846
+ gap: 12px;
1847
+ }
1761
1848
 
1762
- .protection-card-status.inactive {
1763
- color: var(--text-secondary);
1764
- }
1849
+ .layer-card.degraded {
1850
+ border-color: var(--amber);
1851
+ background-color: rgba(210, 153, 34, 0.05);
1852
+ }
1765
1853
 
1766
- .protection-card-stat {
1767
- font-size: 11px;
1768
- color: var(--text-secondary);
1769
- font-family: var(--mono);
1770
- margin-top: 4px;
1771
- }
1854
+ .layer-card.inactive {
1855
+ border-color: var(--red);
1856
+ background-color: rgba(248, 81, 73, 0.05);
1857
+ }
1772
1858
 
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 */
1859
+ .layer-name {
1860
+ font-size: 12px;
1861
+ font-weight: 600;
1862
+ color: var(--text-secondary);
1863
+ text-transform: uppercase;
1864
+ letter-spacing: 0.5px;
1865
+ }
1774
1866
 
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
- }
1867
+ .layer-title {
1868
+ font-size: 14px;
1869
+ font-weight: 600;
1870
+ color: var(--text-primary);
1871
+ }
1789
1872
 
1790
- .pending-overlay.active {
1791
- width: 380px;
1792
- }
1873
+ .layer-status {
1874
+ display: inline-flex;
1875
+ align-items: center;
1876
+ gap: 6px;
1877
+ padding: 4px 8px;
1878
+ background-color: rgba(63, 185, 80, 0.15);
1879
+ color: var(--green);
1880
+ border-radius: 4px;
1881
+ font-size: 11px;
1882
+ font-weight: 600;
1883
+ width: fit-content;
1884
+ }
1793
1885
 
1794
- @media (max-width: 1400px) {
1795
- .pending-overlay.active {
1796
- width: 100%;
1797
- right: auto;
1798
- left: 0;
1886
+ .layer-card.degraded .layer-status {
1887
+ background-color: rgba(210, 153, 34, 0.15);
1888
+ color: var(--amber);
1799
1889
  }
1800
- }
1801
1890
 
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
- }
1891
+ .layer-card.inactive .layer-status {
1892
+ background-color: rgba(248, 81, 73, 0.15);
1893
+ color: var(--red);
1894
+ }
1810
1895
 
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
- }
1896
+ .layer-detail {
1897
+ font-size: 12px;
1898
+ color: var(--text-secondary);
1899
+ font-family: 'JetBrains Mono', monospace;
1900
+ padding: 8px;
1901
+ background-color: var(--bg);
1902
+ border-radius: 4px;
1903
+ border-left: 2px solid var(--blue);
1904
+ }
1818
1905
 
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
- }
1906
+ /* Row 2: Info Cards */
1907
+ .info-cards {
1908
+ display: grid;
1909
+ grid-template-columns: repeat(3, 1fr);
1910
+ gap: 16px;
1911
+ }
1830
1912
 
1831
- .pending-overlay-close:hover {
1832
- color: var(--text-primary);
1833
- }
1913
+ .info-card {
1914
+ background-color: var(--surface);
1915
+ border: 1px solid var(--border);
1916
+ border-radius: 8px;
1917
+ padding: 20px;
1918
+ }
1834
1919
 
1835
- .pending-list {
1836
- flex: 1;
1837
- overflow-y: auto;
1838
- }
1920
+ .card-header {
1921
+ font-size: 12px;
1922
+ font-weight: 600;
1923
+ color: var(--text-secondary);
1924
+ text-transform: uppercase;
1925
+ letter-spacing: 0.5px;
1926
+ margin-bottom: 16px;
1927
+ }
1839
1928
 
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
- }
1929
+ .card-row {
1930
+ display: flex;
1931
+ justify-content: space-between;
1932
+ align-items: center;
1933
+ margin-bottom: 12px;
1934
+ font-size: 13px;
1935
+ }
1847
1936
 
1848
- .pending-item-header {
1849
- display: flex;
1850
- align-items: center;
1851
- gap: 8px;
1852
- }
1937
+ .card-row:last-child {
1938
+ margin-bottom: 0;
1939
+ }
1853
1940
 
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
- }
1941
+ .card-label {
1942
+ color: var(--text-secondary);
1943
+ }
1861
1944
 
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
- }
1945
+ .card-value {
1946
+ color: var(--text-primary);
1947
+ font-family: 'JetBrains Mono', monospace;
1948
+ font-weight: 500;
1949
+ }
1874
1950
 
1875
- .pending-item-tier.tier1 {
1876
- background: var(--red);
1877
- }
1951
+ .identity-badge {
1952
+ display: inline-flex;
1953
+ align-items: center;
1954
+ gap: 4px;
1955
+ padding: 2px 6px;
1956
+ background-color: rgba(88, 166, 255, 0.15);
1957
+ color: var(--blue);
1958
+ border-radius: 3px;
1959
+ font-size: 10px;
1960
+ font-weight: 600;
1961
+ text-transform: uppercase;
1962
+ }
1878
1963
 
1879
- .pending-item-tier.tier2 {
1880
- background: var(--amber);
1881
- }
1964
+ .trust-tier-badge {
1965
+ display: inline-flex;
1966
+ align-items: center;
1967
+ gap: 4px;
1968
+ padding: 2px 6px;
1969
+ background-color: rgba(63, 185, 80, 0.15);
1970
+ color: var(--green);
1971
+ border-radius: 3px;
1972
+ font-size: 10px;
1973
+ font-weight: 600;
1974
+ }
1882
1975
 
1883
- .pending-item-reason {
1884
- font-size: 12px;
1885
- color: var(--text-secondary);
1886
- }
1976
+ .truncated {
1977
+ max-width: 200px;
1978
+ overflow: hidden;
1979
+ text-overflow: ellipsis;
1980
+ white-space: nowrap;
1981
+ }
1887
1982
 
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
- }
1983
+ /* Row 3: SHR & Activity */
1984
+ .main-panels {
1985
+ display: grid;
1986
+ grid-template-columns: 1fr 1fr;
1987
+ gap: 16px;
1988
+ min-height: 400px;
1989
+ }
1896
1990
 
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
- }
1991
+ .panel {
1992
+ background-color: var(--surface);
1993
+ border: 1px solid var(--border);
1994
+ border-radius: 8px;
1995
+ display: flex;
1996
+ flex-direction: column;
1997
+ overflow: hidden;
1998
+ }
1904
1999
 
1905
- .pending-item-timer-fill {
1906
- height: 100%;
1907
- background: var(--blue);
1908
- transition: width 0.1s linear;
1909
- }
2000
+ .panel-header {
2001
+ padding: 16px 20px;
2002
+ border-bottom: 1px solid var(--border);
2003
+ display: flex;
2004
+ justify-content: space-between;
2005
+ align-items: center;
2006
+ }
1910
2007
 
1911
- .pending-item-timer.urgent .pending-item-timer-fill {
1912
- background: var(--red);
1913
- }
2008
+ .panel-title {
2009
+ font-size: 14px;
2010
+ font-weight: 600;
2011
+ color: var(--text-primary);
2012
+ }
1914
2013
 
1915
- .pending-item-actions {
1916
- display: flex;
1917
- gap: 8px;
1918
- }
2014
+ .panel-action {
2015
+ background: none;
2016
+ border: none;
2017
+ color: var(--blue);
2018
+ cursor: pointer;
2019
+ font-size: 12px;
2020
+ padding: 0;
2021
+ font-weight: 500;
2022
+ transition: color 0.2s;
2023
+ }
1919
2024
 
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
- }
2025
+ .panel-action:hover {
2026
+ color: #79c0ff;
2027
+ }
1931
2028
 
1932
- .btn-approve {
1933
- background: var(--green);
1934
- color: var(--bg);
1935
- }
2029
+ .panel-content {
2030
+ flex: 1;
2031
+ overflow-y: auto;
2032
+ padding: 20px;
2033
+ }
1936
2034
 
1937
- .btn-approve:hover {
1938
- background: #4ecf5e;
1939
- }
2035
+ /* SHR Viewer */
2036
+ .shr-json {
2037
+ font-family: 'JetBrains Mono', monospace;
2038
+ font-size: 12px;
2039
+ line-height: 1.6;
2040
+ color: var(--text-secondary);
2041
+ }
1940
2042
 
1941
- .btn-deny {
1942
- background: var(--red);
1943
- color: white;
1944
- }
2043
+ .shr-section {
2044
+ margin-bottom: 12px;
2045
+ }
1945
2046
 
1946
- .btn-deny:hover {
1947
- background: #f9605e;
1948
- }
2047
+ .shr-section-header {
2048
+ display: flex;
2049
+ align-items: center;
2050
+ gap: 8px;
2051
+ cursor: pointer;
2052
+ font-weight: 600;
2053
+ color: var(--text-primary);
2054
+ padding: 8px;
2055
+ background-color: var(--bg);
2056
+ border-radius: 4px;
2057
+ user-select: none;
2058
+ }
1949
2059
 
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 */
2060
+ .shr-section-header:hover {
2061
+ background-color: var(--muted);
2062
+ }
1951
2063
 
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
- }
2064
+ .shr-toggle {
2065
+ width: 16px;
2066
+ height: 16px;
2067
+ display: flex;
2068
+ align-items: center;
2069
+ justify-content: center;
2070
+ font-size: 10px;
2071
+ transition: transform 0.2s;
2072
+ }
1965
2073
 
1966
- .threat-panel.collapsed {
1967
- max-height: 40px;
1968
- }
2074
+ .shr-section.collapsed .shr-toggle {
2075
+ transform: rotate(-90deg);
2076
+ }
1969
2077
 
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
- }
2078
+ .shr-section-content {
2079
+ padding: 8px 16px;
2080
+ background-color: rgba(0, 0, 0, 0.2);
2081
+ border-radius: 4px;
2082
+ margin-top: 4px;
2083
+ }
1983
2084
 
1984
- .threat-header:hover {
1985
- background: rgba(88, 166, 255, 0.05);
1986
- }
2085
+ .shr-section.collapsed .shr-section-content {
2086
+ display: none;
2087
+ }
1987
2088
 
1988
- .threat-icon {
1989
- font-size: 14px;
1990
- }
2089
+ .shr-item {
2090
+ display: flex;
2091
+ margin-bottom: 4px;
2092
+ }
1991
2093
 
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
- }
2094
+ .shr-key {
2095
+ color: var(--blue);
2096
+ margin-right: 8px;
2097
+ min-width: 120px;
2098
+ }
2000
2099
 
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
- }
2100
+ .shr-value {
2101
+ color: var(--green);
2102
+ word-break: break-all;
2103
+ }
2009
2104
 
2010
- .threat-item-type {
2011
- font-weight: 600;
2012
- color: var(--red);
2013
- font-family: var(--mono);
2014
- }
2105
+ /* Activity Feed */
2106
+ .activity-feed {
2107
+ display: flex;
2108
+ flex-direction: column;
2109
+ gap: 12px;
2110
+ }
2015
2111
 
2016
- .threat-empty {
2017
- text-align: center;
2018
- padding: 20px 10px;
2019
- color: var(--text-secondary);
2020
- font-size: 12px;
2021
- }
2112
+ .activity-item {
2113
+ padding: 12px;
2114
+ background-color: var(--bg);
2115
+ border-left: 2px solid var(--border);
2116
+ border-radius: 4px;
2117
+ font-size: 12px;
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
+ .activity-item.tool-call {
2121
+ border-left-color: var(--blue);
2122
+ }
2024
2123
 
2025
- ::-webkit-scrollbar {
2026
- width: 6px;
2027
- }
2124
+ .activity-item.context-gate {
2125
+ border-left-color: var(--amber);
2126
+ }
2028
2127
 
2029
- ::-webkit-scrollbar-track {
2030
- background: transparent;
2031
- }
2128
+ .activity-item.injection {
2129
+ border-left-color: var(--red);
2130
+ }
2032
2131
 
2033
- ::-webkit-scrollbar-thumb {
2034
- background: var(--border);
2035
- border-radius: 3px;
2036
- }
2132
+ .activity-item.protection {
2133
+ border-left-color: var(--green);
2134
+ }
2037
2135
 
2038
- ::-webkit-scrollbar-thumb:hover {
2039
- background: rgba(88, 166, 255, 0.3);
2040
- }
2136
+ .activity-type {
2137
+ font-weight: 600;
2138
+ color: var(--text-primary);
2139
+ margin-bottom: 4px;
2140
+ text-transform: uppercase;
2141
+ font-size: 11px;
2142
+ letter-spacing: 0.5px;
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
+ .activity-content {
2146
+ color: var(--text-secondary);
2147
+ font-family: 'JetBrains Mono', monospace;
2148
+ margin-bottom: 4px;
2149
+ word-break: break-all;
2150
+ }
2043
2151
 
2044
- @media (max-width: 1200px) {
2045
- .protection-sidebar {
2046
- display: none;
2152
+ .activity-time {
2153
+ font-size: 11px;
2154
+ color: var(--text-secondary);
2047
2155
  }
2048
2156
 
2049
- .activity-feed {
2050
- border-right: none;
2157
+ .empty-state {
2158
+ display: flex;
2159
+ align-items: center;
2160
+ justify-content: center;
2161
+ height: 100%;
2162
+ color: var(--text-secondary);
2163
+ font-size: 13px;
2051
2164
  }
2052
- }
2053
2165
 
2054
- @media (max-width: 768px) {
2055
- .status-bar {
2056
- padding: 0 12px;
2057
- gap: 12px;
2058
- height: 48px;
2166
+ /* Row 4: Handshake History */
2167
+ .handshake-table {
2168
+ background-color: var(--surface);
2169
+ border: 1px solid var(--border);
2170
+ border-radius: 8px;
2171
+ overflow: hidden;
2059
2172
  }
2060
2173
 
2061
- .sanctuary-logo {
2062
- font-size: 14px;
2174
+ .table-header {
2175
+ display: grid;
2176
+ grid-template-columns: 2fr 1fr 1fr 1fr 1.5fr 1.5fr;
2177
+ gap: 16px;
2178
+ padding: 16px 20px;
2179
+ border-bottom: 1px solid var(--border);
2180
+ background-color: var(--bg);
2181
+ font-size: 12px;
2182
+ font-weight: 600;
2183
+ color: var(--text-secondary);
2184
+ text-transform: uppercase;
2185
+ letter-spacing: 0.5px;
2063
2186
  }
2064
2187
 
2065
- .status-bar-center {
2066
- display: none;
2188
+ .table-rows {
2189
+ max-height: 300px;
2190
+ overflow-y: auto;
2067
2191
  }
2068
2192
 
2069
- .main-container {
2070
- margin-top: 48px;
2193
+ .table-row {
2194
+ display: grid;
2195
+ grid-template-columns: 2fr 1fr 1fr 1fr 1.5fr 1.5fr;
2196
+ gap: 16px;
2197
+ padding: 14px 20px;
2198
+ border-bottom: 1px solid var(--border);
2199
+ align-items: center;
2200
+ font-size: 12px;
2201
+ cursor: pointer;
2202
+ transition: background-color 0.2s;
2071
2203
  }
2072
2204
 
2073
- .activity-item {
2074
- padding: 10px 12px;
2205
+ .table-row:hover {
2206
+ background-color: var(--bg);
2075
2207
  }
2076
2208
 
2077
- .pending-overlay.active {
2078
- width: 100%;
2209
+ .table-row:last-child {
2210
+ border-bottom: none;
2211
+ }
2212
+
2213
+ .table-cell {
2214
+ color: var(--text-secondary);
2215
+ font-family: 'JetBrains Mono', monospace;
2216
+ }
2217
+
2218
+ .table-cell.strong {
2219
+ color: var(--text-primary);
2220
+ font-weight: 500;
2221
+ }
2222
+
2223
+ .table-empty {
2224
+ padding: 40px 20px;
2225
+ text-align: center;
2226
+ color: var(--text-secondary);
2227
+ font-size: 13px;
2228
+ }
2229
+
2230
+ /* Pending Overlay */
2231
+ .pending-overlay {
2232
+ position: fixed;
2233
+ top: 0;
2234
+ right: -400px;
2235
+ width: 400px;
2236
+ height: 100vh;
2237
+ background-color: var(--surface);
2238
+ border-left: 1px solid var(--border);
2239
+ box-shadow: -2px 0 8px rgba(0, 0, 0, 0.3);
2240
+ z-index: 200;
2241
+ transition: right 0.3s ease;
2242
+ display: flex;
2243
+ flex-direction: column;
2244
+ overflow-y: auto;
2245
+ }
2246
+
2247
+ .pending-overlay.show {
2248
+ right: 0;
2249
+ }
2250
+
2251
+ .pending-header {
2252
+ padding: 16px 20px;
2253
+ border-bottom: 1px solid var(--border);
2254
+ font-weight: 600;
2255
+ color: var(--text-primary);
2256
+ }
2257
+
2258
+ .pending-items {
2259
+ flex: 1;
2260
+ overflow-y: auto;
2261
+ padding: 16px;
2262
+ }
2263
+
2264
+ .pending-item {
2265
+ background-color: var(--bg);
2266
+ border: 1px solid var(--border);
2267
+ border-radius: 6px;
2268
+ padding: 16px;
2269
+ margin-bottom: 12px;
2270
+ }
2271
+
2272
+ .pending-title {
2273
+ font-weight: 600;
2274
+ color: var(--text-primary);
2275
+ margin-bottom: 8px;
2276
+ word-break: break-word;
2079
2277
  }
2080
2278
 
2279
+ .pending-countdown {
2280
+ font-size: 12px;
2281
+ color: var(--amber);
2282
+ margin-bottom: 12px;
2283
+ font-weight: 500;
2284
+ }
2285
+
2286
+ .pending-actions {
2287
+ display: flex;
2288
+ gap: 8px;
2289
+ }
2290
+
2291
+ .pending-btn {
2292
+ flex: 1;
2293
+ padding: 8px 12px;
2294
+ border: none;
2295
+ border-radius: 4px;
2296
+ font-size: 12px;
2297
+ font-weight: 600;
2298
+ cursor: pointer;
2299
+ transition: background-color 0.2s;
2300
+ }
2301
+
2302
+ .pending-approve {
2303
+ background-color: var(--green);
2304
+ color: var(--bg);
2305
+ }
2306
+
2307
+ .pending-approve:hover {
2308
+ background-color: #3fa040;
2309
+ }
2310
+
2311
+ .pending-deny {
2312
+ background-color: var(--red);
2313
+ color: var(--bg);
2314
+ }
2315
+
2316
+ .pending-deny:hover {
2317
+ background-color: #e03c3c;
2318
+ }
2319
+
2320
+ /* Threat Panel */
2081
2321
  .threat-panel {
2082
- max-height: 200px;
2322
+ background-color: var(--surface);
2323
+ border: 1px solid var(--border);
2324
+ border-radius: 8px;
2325
+ margin-top: 20px;
2326
+ overflow: hidden;
2083
2327
  }
2084
- }
2085
- </style>
2328
+
2329
+ .threat-header {
2330
+ padding: 16px 20px;
2331
+ border-bottom: 1px solid var(--border);
2332
+ display: flex;
2333
+ justify-content: space-between;
2334
+ align-items: center;
2335
+ cursor: pointer;
2336
+ user-select: none;
2337
+ }
2338
+
2339
+ .threat-title {
2340
+ font-weight: 600;
2341
+ color: var(--text-primary);
2342
+ }
2343
+
2344
+ .threat-toggle {
2345
+ font-size: 10px;
2346
+ color: var(--text-secondary);
2347
+ transition: transform 0.2s;
2348
+ }
2349
+
2350
+ .threat-panel.collapsed .threat-toggle {
2351
+ transform: rotate(-90deg);
2352
+ }
2353
+
2354
+ .threat-content {
2355
+ padding: 16px 20px;
2356
+ max-height: 300px;
2357
+ overflow-y: auto;
2358
+ }
2359
+
2360
+ .threat-panel.collapsed .threat-content {
2361
+ display: none;
2362
+ }
2363
+
2364
+ .threat-alert {
2365
+ background-color: rgba(248, 81, 73, 0.1);
2366
+ border: 1px solid var(--red);
2367
+ border-radius: 4px;
2368
+ padding: 12px;
2369
+ margin-bottom: 8px;
2370
+ font-size: 12px;
2371
+ }
2372
+
2373
+ .threat-alert:last-child {
2374
+ margin-bottom: 0;
2375
+ }
2376
+
2377
+ .threat-type {
2378
+ font-weight: 600;
2379
+ color: var(--red);
2380
+ margin-bottom: 4px;
2381
+ text-transform: uppercase;
2382
+ font-size: 10px;
2383
+ letter-spacing: 0.5px;
2384
+ }
2385
+
2386
+ .threat-message {
2387
+ color: var(--text-secondary);
2388
+ }
2389
+
2390
+ /* Scrollbar */
2391
+ ::-webkit-scrollbar {
2392
+ width: 8px;
2393
+ }
2394
+
2395
+ ::-webkit-scrollbar-track {
2396
+ background-color: transparent;
2397
+ }
2398
+
2399
+ ::-webkit-scrollbar-thumb {
2400
+ background-color: var(--border);
2401
+ border-radius: 4px;
2402
+ }
2403
+
2404
+ ::-webkit-scrollbar-thumb:hover {
2405
+ background-color: var(--text-secondary);
2406
+ }
2407
+
2408
+ /* Responsive */
2409
+ @media (max-width: 1400px) {
2410
+ .sovereignty-layers {
2411
+ grid-template-columns: repeat(2, 1fr);
2412
+ }
2413
+
2414
+ .main-panels {
2415
+ grid-template-columns: 1fr;
2416
+ }
2417
+
2418
+ .pending-overlay {
2419
+ width: 100%;
2420
+ right: -100%;
2421
+ }
2422
+ }
2423
+
2424
+ @media (max-width: 768px) {
2425
+ .status-bar {
2426
+ flex-wrap: wrap;
2427
+ height: auto;
2428
+ padding: 12px;
2429
+ gap: 12px;
2430
+ }
2431
+
2432
+ .status-bar-center {
2433
+ order: 3;
2434
+ flex-basis: 100%;
2435
+ }
2436
+
2437
+ .main-content {
2438
+ margin-top: auto;
2439
+ }
2440
+
2441
+ .info-cards {
2442
+ grid-template-columns: 1fr;
2443
+ }
2444
+
2445
+ .table-header,
2446
+ .table-row {
2447
+ grid-template-columns: 1fr;
2448
+ }
2449
+ }
2450
+ </style>
2086
2451
  </head>
2087
2452
  <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>
2107
- </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
2120
- </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>
2453
+ <!-- Status Bar -->
2454
+ <div class="status-bar">
2455
+ <div class="status-bar-left">
2456
+ <div class="logo-icon">\u25C6</div>
2457
+ <div class="logo-info">
2458
+ <div class="logo-title">SANCTUARY</div>
2459
+ <div class="logo-version">v${options.serverVersion}</div>
2125
2460
  </div>
2126
2461
  </div>
2127
- </div>
2128
2462
 
2129
- <!-- Protection Status Sidebar -->
2130
- <div class="protection-sidebar" id="protectionSidebar">
2131
- <div class="sidebar-header">
2132
- <span>\u25C6</span> Protection Status
2463
+ <div class="status-bar-center">
2464
+ <div id="sovereignty-badge" class="sovereignty-badge">
2465
+ <span>Sovereignty Health:</span>
2466
+ <span class="sovereignty-score" id="sovereignty-score">\u2014</span>
2467
+ <span>/ 100</span>
2468
+ </div>
2133
2469
  </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>
2470
+
2471
+ <div class="status-bar-right">
2472
+ <div class="status-item">
2473
+ <strong id="protections-count">\u2014</strong>
2474
+ <span>Protections</span>
2475
+ </div>
2476
+ <div class="status-item">
2477
+ <strong id="uptime-value">\u2014</strong>
2478
+ <span>Uptime</span>
2479
+ </div>
2480
+ <div class="status-dot" id="connection-status"></div>
2481
+ <div id="pending-item-badge" class="pending-badge" style="display: none;">
2482
+ <span>\u23F3</span>
2483
+ <span id="pending-count">0</span>
2140
2484
  </div>
2485
+ </div>
2486
+ </div>
2141
2487
 
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>
2488
+ <!-- Main Content -->
2489
+ <div class="main-content">
2490
+ <div class="grid">
2491
+ <!-- Row 1: Sovereignty Layers -->
2492
+ <div class="sovereignty-layers" id="sovereignty-layers">
2493
+ <div class="layer-card" data-layer="l1">
2494
+ <div class="layer-name">Layer 1</div>
2495
+ <div class="layer-title">Cognitive Sovereignty</div>
2496
+ <div class="layer-status"><span>\u25CF</span> <span id="l1-status">\u2014</span></div>
2497
+ <div class="layer-detail" id="l1-detail">Loading...</div>
2498
+ </div>
2499
+ <div class="layer-card" data-layer="l2">
2500
+ <div class="layer-name">Layer 2</div>
2501
+ <div class="layer-title">Operational Isolation</div>
2502
+ <div class="layer-status"><span>\u25CF</span> <span id="l2-status">\u2014</span></div>
2503
+ <div class="layer-detail" id="l2-detail">Loading...</div>
2504
+ </div>
2505
+ <div class="layer-card" data-layer="l3">
2506
+ <div class="layer-name">Layer 3</div>
2507
+ <div class="layer-title">Selective Disclosure</div>
2508
+ <div class="layer-status"><span>\u25CF</span> <span id="l3-status">\u2014</span></div>
2509
+ <div class="layer-detail" id="l3-detail">Loading...</div>
2510
+ </div>
2511
+ <div class="layer-card" data-layer="l4">
2512
+ <div class="layer-name">Layer 4</div>
2513
+ <div class="layer-title">Verifiable Reputation</div>
2514
+ <div class="layer-status"><span>\u25CF</span> <span id="l4-status">\u2014</span></div>
2515
+ <div class="layer-detail" id="l4-detail">Loading...</div>
2516
+ </div>
2147
2517
  </div>
2148
2518
 
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>
2519
+ <!-- Row 2: Info Cards -->
2520
+ <div class="info-cards">
2521
+ <div class="info-card">
2522
+ <div class="card-header">Identity</div>
2523
+ <div class="card-row">
2524
+ <span class="card-label">Primary</span>
2525
+ <span class="card-value" id="identity-label">\u2014</span>
2526
+ </div>
2527
+ <div class="card-row">
2528
+ <span class="card-label">DID</span>
2529
+ <span class="card-value truncated" id="identity-did" title="">\u2014</span>
2530
+ </div>
2531
+ <div class="card-row">
2532
+ <span class="card-label">Public Key</span>
2533
+ <span class="card-value truncated" id="identity-pubkey" title="">\u2014</span>
2534
+ </div>
2535
+ <div class="card-row">
2536
+ <span class="card-label">Type</span>
2537
+ <span class="identity-badge">Ed25519</span>
2538
+ </div>
2539
+ <div class="card-row">
2540
+ <span class="card-label">Created</span>
2541
+ <span class="card-value" id="identity-created">\u2014</span>
2542
+ </div>
2543
+ <div class="card-row">
2544
+ <span class="card-label">Identities</span>
2545
+ <span class="card-value" id="identity-count">\u2014</span>
2546
+ </div>
2547
+ </div>
2548
+
2549
+ <div class="info-card">
2550
+ <div class="card-header">Handshakes</div>
2551
+ <div class="card-row">
2552
+ <span class="card-label">Total</span>
2553
+ <span class="card-value" id="handshake-count">\u2014</span>
2554
+ </div>
2555
+ <div class="card-row">
2556
+ <span class="card-label">Latest Peer</span>
2557
+ <span class="card-value truncated" id="handshake-latest">\u2014</span>
2558
+ </div>
2559
+ <div class="card-row">
2560
+ <span class="card-label">Trust Tier</span>
2561
+ <span class="trust-tier-badge" id="handshake-tier">Unverified</span>
2562
+ </div>
2563
+ <div class="card-row">
2564
+ <span class="card-label">Timestamp</span>
2565
+ <span class="card-value" id="handshake-time">\u2014</span>
2566
+ </div>
2567
+ </div>
2568
+
2569
+ <div class="info-card">
2570
+ <div class="card-header">Reputation</div>
2571
+ <div class="card-row">
2572
+ <span class="card-label">Weighted Score</span>
2573
+ <span class="card-value" id="reputation-score">\u2014</span>
2574
+ </div>
2575
+ <div class="card-row">
2576
+ <span class="card-label">Attestations</span>
2577
+ <span class="card-value" id="reputation-attestations">\u2014</span>
2578
+ </div>
2579
+ <div class="card-row">
2580
+ <span class="card-label">Verified Sovereign</span>
2581
+ <span class="card-value" id="reputation-verified">\u2014</span>
2582
+ </div>
2583
+ <div class="card-row">
2584
+ <span class="card-label">Verified Degraded</span>
2585
+ <span class="card-value" id="reputation-degraded">\u2014</span>
2586
+ </div>
2587
+ <div class="card-row">
2588
+ <span class="card-label">Unverified</span>
2589
+ <span class="card-value" id="reputation-unverified">\u2014</span>
2590
+ </div>
2591
+ </div>
2154
2592
  </div>
2155
2593
 
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>
2594
+ <!-- Row 3: SHR & Activity -->
2595
+ <div class="main-panels">
2596
+ <div class="panel">
2597
+ <div class="panel-header">
2598
+ <div class="panel-title">Sovereignty Health Report</div>
2599
+ <button class="panel-action" id="copy-shr-btn">Copy JSON</button>
2600
+ </div>
2601
+ <div class="panel-content">
2602
+ <div class="shr-json" id="shr-viewer">
2603
+ <div class="empty-state">Loading SHR...</div>
2604
+ </div>
2605
+ </div>
2606
+ </div>
2607
+
2608
+ <div class="panel">
2609
+ <div class="panel-header">
2610
+ <div class="panel-title">Activity Feed</div>
2611
+ </div>
2612
+ <div class="panel-content">
2613
+ <div id="activity-feed" class="activity-feed">
2614
+ <div class="empty-state">Waiting for activity...</div>
2615
+ </div>
2616
+ </div>
2617
+ </div>
2161
2618
  </div>
2162
2619
 
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>
2620
+ <!-- Row 4: Handshake History -->
2621
+ <div class="handshake-table">
2622
+ <div class="table-header">
2623
+ <div>Counterparty</div>
2624
+ <div>Trust Tier</div>
2625
+ <div>Sovereignty</div>
2626
+ <div>Verified</div>
2627
+ <div>Completed</div>
2628
+ <div>Expires</div>
2629
+ </div>
2630
+ <div class="table-rows" id="handshake-table">
2631
+ <div class="table-empty">No handshakes completed yet</div>
2632
+ </div>
2168
2633
  </div>
2169
2634
 
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>
2635
+ <!-- Threat Panel -->
2636
+ <div class="threat-panel collapsed">
2637
+ <div class="threat-header">
2638
+ <div class="threat-title">Security Threats</div>
2639
+ <div class="threat-toggle">\u25B6</div>
2640
+ </div>
2641
+ <div class="threat-content" id="threat-alerts">
2642
+ <div class="empty-state">No threats detected</div>
2643
+ </div>
2175
2644
  </div>
2176
2645
  </div>
2177
2646
  </div>
2178
- </div>
2179
2647
 
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>
2648
+ <!-- Pending Overlay -->
2649
+ <div class="pending-overlay" id="pending-overlay">
2650
+ <div class="pending-header">Pending Approvals</div>
2651
+ <div class="pending-items" id="pending-items"></div>
2195
2652
  </div>
2196
- <div class="threat-content" id="threatContent">
2197
- <div class="threat-empty">No threats detected</div>
2198
- </div>
2199
- </div>
2200
2653
 
2201
- <script>
2202
- (function() {
2203
- 'use strict';
2654
+ <script>
2655
+ // Constants
2656
+ const AUTH_TOKEN = '${options.authToken || ""}' || sessionStorage.getItem('authToken') || '';
2657
+ const TIMEOUT_SECONDS = ${options.timeoutSeconds};
2658
+ const API_BASE = '';
2659
+
2660
+ // State
2661
+ let apiState = {
2662
+ sovereignty: null,
2663
+ identity: null,
2664
+ handshakes: [],
2665
+ shr: null,
2666
+ status: null,
2667
+ };
2668
+
2669
+ let pendingRequests = new Map();
2670
+ let activityLog = [];
2671
+ const maxActivityItems = 50;
2204
2672
 
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
2673
+ // Helpers
2674
+ function esc(text) {
2675
+ if (!text) return '';
2676
+ const div = document.createElement('div');
2677
+ div.textContent = text;
2678
+ return div.innerHTML;
2679
+ }
2680
+
2681
+ function formatTime(isoString) {
2682
+ if (!isoString) return '\u2014';
2683
+ const date = new Date(isoString);
2684
+ return date.toLocaleString('en-US', {
2685
+ month: 'short',
2686
+ day: 'numeric',
2687
+ hour: '2-digit',
2688
+ minute: '2-digit',
2689
+ });
2690
+ }
2206
2691
 
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;
2692
+ function truncate(str, len = 16) {
2693
+ if (!str) return '\u2014';
2694
+ if (str.length <= len) return str;
2695
+ return str.slice(0, len) + '...';
2696
+ }
2213
2697
 
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
2698
+ function calculateSovereigntyScore(shr) {
2699
+ if (!shr || !shr.layers) return 0;
2700
+ const layers = shr.layers;
2701
+ let score = 100;
2215
2702
 
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;
2703
+ if (layers.l1?.status === 'degraded') score -= 20;
2704
+ if (layers.l1?.status === 'inactive') score -= 35;
2705
+ if (layers.l2?.status === 'degraded') score -= 15;
2706
+ if (layers.l2?.status === 'inactive') score -= 25;
2707
+ if (layers.l3?.status === 'degraded') score -= 15;
2708
+ if (layers.l3?.status === 'inactive') score -= 25;
2709
+ if (layers.l4?.status === 'degraded') score -= 10;
2710
+ if (layers.l4?.status === 'inactive') score -= 20;
2226
2711
 
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
2712
+ return Math.max(0, Math.min(100, score));
2713
+ }
2228
2714
 
2229
- function authHeaders() {
2230
- const h = { 'Content-Type': 'application/json' };
2231
- if (AUTH_TOKEN) h['Authorization'] = 'Bearer ' + AUTH_TOKEN;
2232
- return h;
2233
- }
2715
+ async function fetchAPI(endpoint) {
2716
+ try {
2717
+ const response = await fetch(API_BASE + endpoint, {
2718
+ headers: {
2719
+ 'Authorization': 'Bearer ' + AUTH_TOKEN,
2720
+ },
2721
+ });
2234
2722
 
2235
- function sessionQuery(url) {
2236
- if (!SESSION_ID) return url;
2237
- const sep = url.includes('?') ? '&' : '?';
2238
- return url + sep + 'session=' + SESSION_ID;
2239
- }
2723
+ if (response.status === 401) {
2724
+ redirectToLogin();
2725
+ return null;
2726
+ }
2240
2727
 
2241
- function setCookie(sessionId, maxAge) {
2242
- document.cookie = 'sanctuary_session=' + sessionId +
2243
- '; path=/; SameSite=Strict; max-age=' + maxAge;
2244
- }
2728
+ if (!response.ok) {
2729
+ console.error('API Error:', response.status);
2730
+ return null;
2731
+ }
2245
2732
 
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');
2733
+ return await response.json();
2734
+ } catch (err) {
2735
+ console.error('Fetch error:', err);
2736
+ return null;
2737
+ }
2323
2738
  }
2324
- }
2325
2739
 
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
2740
+ function redirectToLogin() {
2741
+ sessionStorage.removeItem('authToken');
2742
+ window.location.href = '/';
2743
+ }
2327
2744
 
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
- };
2745
+ // API Updates
2746
+ async function updateSovereignty() {
2747
+ const data = await fetchAPI('/api/sovereignty');
2748
+ if (!data) return;
2349
2749
 
2350
- activityItems.unshift(item);
2351
- if (activityItems.length > MAX_ACTIVITY_ITEMS) {
2352
- activityItems.pop();
2750
+ apiState.sovereignty = data;
2751
+
2752
+ const score = calculateSovereigntyScore(data.shr);
2753
+ const badge = document.getElementById('sovereignty-badge');
2754
+ const scoreEl = document.getElementById('sovereignty-score');
2755
+
2756
+ scoreEl.textContent = score;
2757
+
2758
+ badge.classList.remove('degraded', 'inactive');
2759
+ if (score < 70) badge.classList.add('degraded');
2760
+ if (score < 40) badge.classList.add('inactive');
2761
+
2762
+ updateLayerCards(data.shr);
2353
2763
  }
2354
2764
 
2355
- renderActivityFeed();
2356
- }
2765
+ function updateLayerCards(shr) {
2766
+ if (!shr || !shr.layers) return;
2357
2767
 
2358
- function renderActivityFeed() {
2359
- const list = document.getElementById('activityList');
2768
+ const layers = shr.layers;
2360
2769
 
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;
2770
+ updateLayerCard('l1', layers.l1, layers.l1?.encryption || 'AES-256-GCM');
2771
+ updateLayerCard('l2', layers.l2, layers.l2?.isolation_type || 'Process-level');
2772
+ updateLayerCard('l3', layers.l3, layers.l3?.proof_system || 'Schnorr-Pedersen');
2773
+ updateLayerCard('l4', layers.l4, layers.l4?.reputation_mode || 'Weighted');
2364
2774
  }
2365
2775
 
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
- });
2776
+ function updateLayerCard(layer, layerData, detail) {
2777
+ if (!layerData) return;
2778
+
2779
+ const card = document.querySelector(\`[data-layer="\${layer}"]\`);
2780
+ if (!card) return;
2781
+
2782
+ const status = layerData.status || 'inactive';
2783
+ card.classList.remove('degraded', 'inactive');
2400
2784
 
2401
- list.appendChild(tr);
2785
+ if (status === 'degraded') {
2786
+ card.classList.add('degraded');
2787
+ } else if (status === 'inactive') {
2788
+ card.classList.add('inactive');
2789
+ }
2790
+
2791
+ document.getElementById(\`\${layer}-status\`).textContent = status.toUpperCase();
2792
+ document.getElementById(\`\${layer}-detail\`).textContent = detail;
2402
2793
  }
2403
- }
2404
2794
 
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
2795
+ async function updateIdentity() {
2796
+ const data = await fetchAPI('/api/identity');
2797
+ if (!data) return;
2406
2798
 
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
- };
2799
+ apiState.identity = data;
2426
2800
 
2427
- pendingRequests.set(request_id, pending);
2428
- updatePendingUI();
2429
- }
2801
+ const primary = data.primary || {};
2802
+ document.getElementById('identity-label').textContent = primary.label || '\u2014';
2803
+ document.getElementById('identity-did').textContent = truncate(primary.did, 24);
2804
+ document.getElementById('identity-did').title = primary.did || '';
2805
+ document.getElementById('identity-pubkey').textContent = truncate(primary.publicKey, 24);
2806
+ document.getElementById('identity-pubkey').title = primary.publicKey || '';
2807
+ document.getElementById('identity-created').textContent = formatTime(primary.createdAt);
2808
+ document.getElementById('identity-count').textContent = data.identities?.length || '\u2014';
2809
+ }
2430
2810
 
2431
- function removePendingRequest(id) {
2432
- pendingRequests.delete(id);
2433
- updatePendingUI();
2434
- }
2811
+ async function updateHandshakes() {
2812
+ const data = await fetchAPI('/api/handshakes');
2813
+ if (!data) return;
2435
2814
 
2436
- function updatePendingUI() {
2437
- const count = pendingRequests.size;
2438
- const badge = document.getElementById('pendingBadge');
2815
+ apiState.handshakes = data.handshakes || [];
2439
2816
 
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');
2817
+ document.getElementById('handshake-count').textContent = data.handshakes?.length || '0';
2818
+
2819
+ if (data.handshakes && data.handshakes.length > 0) {
2820
+ const latest = data.handshakes[0];
2821
+ document.getElementById('handshake-latest').textContent = truncate(latest.counterpartyId, 20);
2822
+ document.getElementById('handshake-latest').title = latest.counterpartyId || '';
2823
+ document.getElementById('handshake-tier').textContent = (latest.trustTier || 'Unverified').toUpperCase();
2824
+ document.getElementById('handshake-time').textContent = formatTime(latest.completedAt);
2825
+ } else {
2826
+ document.getElementById('handshake-latest').textContent = '\u2014';
2827
+ document.getElementById('handshake-tier').textContent = 'Unverified';
2828
+ document.getElementById('handshake-time').textContent = '\u2014';
2829
+ }
2830
+
2831
+ updateHandshakeTable(data.handshakes || []);
2447
2832
  }
2448
2833
 
2449
- renderPendingList();
2450
- }
2834
+ function updateHandshakeTable(handshakes) {
2835
+ const table = document.getElementById('handshake-table');
2836
+
2837
+ if (!handshakes || handshakes.length === 0) {
2838
+ table.innerHTML = '<div class="table-empty">No handshakes completed yet</div>';
2839
+ return;
2840
+ }
2841
+
2842
+ table.innerHTML = handshakes
2843
+ .map(
2844
+ (hs) => \`
2845
+ <div class="table-row">
2846
+ <div class="table-cell strong">\${esc(truncate(hs.counterpartyId, 24))}</div>
2847
+ <div class="table-cell">\${esc(hs.trustTier || 'Unverified')}</div>
2848
+ <div class="table-cell">\${esc(hs.sovereigntyLevel || '\u2014')}</div>
2849
+ <div class="table-cell">\${hs.verified ? 'Yes' : 'No'}</div>
2850
+ <div class="table-cell">\${formatTime(hs.completedAt)}</div>
2851
+ <div class="table-cell">\${formatTime(hs.expiresAt)}</div>
2852
+ </div>
2853
+ \`
2854
+ )
2855
+ .join('');
2856
+ }
2857
+
2858
+ async function updateSHR() {
2859
+ const data = await fetchAPI('/api/shr');
2860
+ if (!data) return;
2451
2861
 
2452
- function renderPendingList() {
2453
- const list = document.getElementById('pendingList');
2454
- list.innerHTML = '';
2862
+ apiState.shr = data;
2863
+ renderSHRViewer(data);
2864
+ }
2455
2865
 
2456
- for (const [id, req] of pendingRequests) {
2457
- const item = document.createElement('div');
2458
- item.className = 'pending-item';
2866
+ function renderSHRViewer(shr) {
2867
+ const viewer = document.getElementById('shr-viewer');
2459
2868
 
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;
2869
+ if (!shr) {
2870
+ viewer.innerHTML = '<div class="empty-state">No SHR available</div>';
2871
+ return;
2872
+ }
2464
2873
 
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
- '';
2874
+ let html = '';
2875
+
2876
+ // Implementation
2877
+ html += \`
2878
+ <div class="shr-section">
2879
+ <div class="shr-section-header">
2880
+ <div class="shr-toggle">\u25BC</div>
2881
+ <div>Implementation</div>
2882
+ </div>
2883
+ <div class="shr-section-content">
2884
+ <div class="shr-item">
2885
+ <div class="shr-key">sanctuary_version:</div>
2886
+ <div class="shr-value">\${esc(shr.implementation?.sanctuary_version || '\u2014')}</div>
2887
+ </div>
2888
+ <div class="shr-item">
2889
+ <div class="shr-key">node_version:</div>
2890
+ <div class="shr-value">\${esc(shr.implementation?.node_version || '\u2014')}</div>
2891
+ </div>
2892
+ <div class="shr-item">
2893
+ <div class="shr-key">generated_by:</div>
2894
+ <div class="shr-value">\${esc(shr.implementation?.generated_by || '\u2014')}</div>
2895
+ </div>
2896
+ </div>
2897
+ </div>
2898
+ \`;
2899
+
2900
+ // Metadata
2901
+ html += \`
2902
+ <div class="shr-section">
2903
+ <div class="shr-section-header">
2904
+ <div class="shr-toggle">\u25BC</div>
2905
+ <div>Metadata</div>
2906
+ </div>
2907
+ <div class="shr-section-content">
2908
+ <div class="shr-item">
2909
+ <div class="shr-key">instance_id:</div>
2910
+ <div class="shr-value">\${esc(truncate(shr.instance_id, 20))}</div>
2911
+ </div>
2912
+ <div class="shr-item">
2913
+ <div class="shr-key">generated_at:</div>
2914
+ <div class="shr-value">\${formatTime(shr.generated_at)}</div>
2915
+ </div>
2916
+ <div class="shr-item">
2917
+ <div class="shr-key">expires_at:</div>
2918
+ <div class="shr-value">\${formatTime(shr.expires_at)}</div>
2919
+ </div>
2920
+ </div>
2921
+ </div>
2922
+ \`;
2923
+
2924
+ // Layers
2925
+ if (shr.layers) {
2926
+ html += \`<div class="shr-section">
2927
+ <div class="shr-section-header">
2928
+ <div class="shr-toggle">\u25BC</div>
2929
+ <div>Layers</div>
2930
+ </div>
2931
+ <div class="shr-section-content">
2932
+ \`;
2933
+
2934
+ for (const [key, layer] of Object.entries(shr.layers)) {
2935
+ html += \`
2936
+ <div style="margin-bottom: 12px;">
2937
+ <div style="color: var(--blue); font-weight: 600; margin-bottom: 4px;">\${esc(key)}</div>
2938
+ <div style="padding-left: 12px;">
2939
+ \`;
2940
+
2941
+ for (const [lkey, lvalue] of Object.entries(layer || {})) {
2942
+ const displayValue =
2943
+ typeof lvalue === 'boolean'
2944
+ ? lvalue
2945
+ ? 'true'
2946
+ : 'false'
2947
+ : esc(String(lvalue));
2948
+ html += \`
2949
+ <div class="shr-item">
2950
+ <div class="shr-key">\${esc(lkey)}:</div>
2951
+ <div class="shr-value">\${displayValue}</div>
2952
+ </div>
2953
+ \`;
2954
+ }
2482
2955
 
2483
- list.appendChild(item);
2956
+ html += \`
2957
+ </div>
2958
+ </div>
2959
+ \`;
2960
+ }
2961
+
2962
+ html += \`
2963
+ </div>
2964
+ </div>
2965
+ \`;
2966
+ }
2967
+
2968
+ // Capabilities
2969
+ if (shr.capabilities) {
2970
+ html += \`
2971
+ <div class="shr-section">
2972
+ <div class="shr-section-header">
2973
+ <div class="shr-toggle">\u25BC</div>
2974
+ <div>Capabilities</div>
2975
+ </div>
2976
+ <div class="shr-section-content">
2977
+ \`;
2978
+
2979
+ for (const [key, value] of Object.entries(shr.capabilities)) {
2980
+ const displayValue = value ? 'true' : 'false';
2981
+ html += \`
2982
+ <div class="shr-item">
2983
+ <div class="shr-key">\${esc(key)}:</div>
2984
+ <div class="shr-value">\${displayValue}</div>
2985
+ </div>
2986
+ \`;
2987
+ }
2988
+
2989
+ html += \`
2990
+ </div>
2991
+ </div>
2992
+ \`;
2993
+ }
2994
+
2995
+ // Signature
2996
+ html += \`
2997
+ <div class="shr-section">
2998
+ <div class="shr-section-header">
2999
+ <div class="shr-toggle">\u25BC</div>
3000
+ <div>Signature</div>
3001
+ </div>
3002
+ <div class="shr-section-content">
3003
+ <div class="shr-item">
3004
+ <div class="shr-key">signed_by:</div>
3005
+ <div class="shr-value">\${esc(truncate(shr.signed_by, 20))}</div>
3006
+ </div>
3007
+ <div class="shr-item">
3008
+ <div class="shr-key">signature:</div>
3009
+ <div class="shr-value">\${esc(truncate(shr.signature, 32))}</div>
3010
+ </div>
3011
+ </div>
3012
+ </div>
3013
+ \`;
3014
+
3015
+ viewer.innerHTML = html;
3016
+
3017
+ // Add collapse functionality
3018
+ document.querySelectorAll('.shr-section-header').forEach((header) => {
3019
+ header.addEventListener('click', () => {
3020
+ header.closest('.shr-section').classList.toggle('collapsed');
3021
+ });
3022
+ });
2484
3023
  }
2485
- }
2486
3024
 
2487
- window.handleApprove = function(id) {
2488
- fetch('/api/approve/' + id, { method: 'POST', headers: authHeaders() }).then(() => {
2489
- removePendingRequest(id);
2490
- }).catch(() => {});
2491
- };
3025
+ async function updateStatus() {
3026
+ const data = await fetchAPI('/api/status');
3027
+ if (!data) return;
2492
3028
 
2493
- window.handleDeny = function(id) {
2494
- fetch('/api/deny/' + id, { method: 'POST', headers: authHeaders() }).then(() => {
2495
- removePendingRequest(id);
2496
- }).catch(() => {});
2497
- };
3029
+ apiState.status = data;
3030
+
3031
+ document.getElementById('protections-count').textContent = data.protectionsCount || '0';
3032
+ document.getElementById('uptime-value').textContent = formatUptime(data.uptime);
3033
+
3034
+ const connectionStatus = document.getElementById('connection-status');
3035
+ connectionStatus.classList.toggle('disconnected', !data.connected);
3036
+ }
3037
+
3038
+ function formatUptime(seconds) {
3039
+ if (!seconds) return '\u2014';
3040
+ const hours = Math.floor(seconds / 3600);
3041
+ const minutes = Math.floor((seconds % 3600) / 60);
3042
+ if (hours > 0) return \`\${hours}h \${minutes}m\`;
3043
+ return \`\${minutes}m\`;
3044
+ }
3045
+
3046
+ // SSE Setup
3047
+ function setupSSE() {
3048
+ const eventSource = new EventSource(API_BASE + '/api/events', {
3049
+ headers: {
3050
+ 'Authorization': 'Bearer ' + AUTH_TOKEN,
3051
+ },
3052
+ });
3053
+
3054
+ eventSource.addEventListener('init', (e) => {
3055
+ console.log('Connected to SSE');
3056
+ });
3057
+
3058
+ eventSource.addEventListener('sovereignty-update', () => {
3059
+ updateSovereignty();
3060
+ });
3061
+
3062
+ eventSource.addEventListener('handshake-update', () => {
3063
+ updateHandshakes();
3064
+ });
3065
+
3066
+ eventSource.addEventListener('tool-call', (e) => {
3067
+ const data = JSON.parse(e.data);
3068
+ addActivityItem({
3069
+ type: 'tool-call',
3070
+ title: 'Tool Call',
3071
+ content: data.toolName,
3072
+ timestamp: new Date().toISOString(),
3073
+ });
3074
+ });
3075
+
3076
+ eventSource.addEventListener('context-gate-decision', (e) => {
3077
+ const data = JSON.parse(e.data);
3078
+ addActivityItem({
3079
+ type: 'context-gate',
3080
+ title: 'Context Gate',
3081
+ content: data.decision,
3082
+ timestamp: new Date().toISOString(),
3083
+ });
3084
+ });
3085
+
3086
+ eventSource.addEventListener('injection-alert', (e) => {
3087
+ const data = JSON.parse(e.data);
3088
+ addActivityItem({
3089
+ type: 'injection',
3090
+ title: 'Injection Alert',
3091
+ content: data.pattern,
3092
+ timestamp: new Date().toISOString(),
3093
+ });
3094
+ addThreatAlert(data);
3095
+ });
3096
+
3097
+ eventSource.addEventListener('pending-request', (e) => {
3098
+ const data = JSON.parse(e.data);
3099
+ addPendingRequest(data);
3100
+ });
2498
3101
 
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
- };
3102
+ eventSource.addEventListener('request-resolved', (e) => {
3103
+ const data = JSON.parse(e.data);
3104
+ removePendingRequest(data.requestId);
3105
+ });
2516
3106
 
2517
- threatItems.unshift(threat);
2518
- if (threatItems.length > MAX_THREAT_ITEMS) {
2519
- threatItems.pop();
3107
+ eventSource.onerror = () => {
3108
+ console.error('SSE error');
3109
+ setTimeout(setupSSE, 5000);
3110
+ };
2520
3111
  }
2521
3112
 
2522
- if (threatCount > 0) {
2523
- document.getElementById('threatPanel').classList.remove('collapsed');
2524
- }
3113
+ // Activity Feed
3114
+ function addActivityItem(item) {
3115
+ activityLog.unshift(item);
3116
+ if (activityLog.length > maxActivityItems) {
3117
+ activityLog.pop();
3118
+ }
2525
3119
 
2526
- renderThreats();
2527
- }
3120
+ const feed = document.getElementById('activity-feed');
3121
+ const html = \`
3122
+ <div class="activity-item \${item.type}">
3123
+ <div class="activity-type">\${esc(item.title)}</div>
3124
+ <div class="activity-content">\${esc(item.content)}</div>
3125
+ <div class="activity-time">\${formatTime(item.timestamp)}</div>
3126
+ </div>
3127
+ \`;
3128
+
3129
+ if (feed.querySelector('.empty-state')) {
3130
+ feed.innerHTML = '';
3131
+ }
2528
3132
 
2529
- function renderThreats() {
2530
- const content = document.getElementById('threatContent');
2531
- const badge = document.getElementById('threatCount');
3133
+ feed.insertAdjacentHTML('afterbegin', html);
2532
3134
 
2533
- if (threatItems.length === 0) {
2534
- content.innerHTML = '<div class="threat-empty">No threats detected</div>';
2535
- badge.textContent = '0';
2536
- return;
3135
+ if (feed.children.length > maxActivityItems) {
3136
+ feed.lastChild.remove();
3137
+ }
2537
3138
  }
2538
3139
 
2539
- badge.textContent = threatItems.length;
2540
- content.innerHTML = '';
3140
+ // Pending Requests
3141
+ function addPendingRequest(request) {
3142
+ pendingRequests.set(request.requestId, {
3143
+ id: request.requestId,
3144
+ title: request.title,
3145
+ details: request.details,
3146
+ expiresAt: new Date(Date.now() + TIMEOUT_SECONDS * 1000),
3147
+ });
2541
3148
 
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);
3149
+ updatePendingDisplay();
2554
3150
  }
2555
- }
2556
3151
 
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
2558
-
2559
- function reconnectSSE() {
2560
- if (evtSource) evtSource.close();
2561
- connect();
2562
- }
3152
+ function removePendingRequest(requestId) {
3153
+ pendingRequests.delete(requestId);
3154
+ updatePendingDisplay();
3155
+ }
2563
3156
 
2564
- function connect() {
2565
- evtSource = new EventSource(sessionQuery('/events'));
3157
+ function updatePendingDisplay() {
3158
+ const badge = document.getElementById('pending-item-badge');
3159
+ const count = pendingRequests.size;
2566
3160
 
2567
- evtSource.onopen = () => {
2568
- document.getElementById('statusDot').classList.remove('disconnected');
2569
- };
3161
+ if (count > 0) {
3162
+ document.getElementById('pending-count').textContent = count;
3163
+ badge.style.display = 'flex';
3164
+ } else {
3165
+ badge.style.display = 'none';
3166
+ }
2570
3167
 
2571
- evtSource.onerror = () => {
2572
- document.getElementById('statusDot').classList.add('disconnected');
2573
- };
3168
+ const overlay = document.getElementById('pending-overlay');
3169
+ const items = document.getElementById('pending-items');
2574
3170
 
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);
3171
+ if (count === 0) {
3172
+ items.innerHTML = '';
3173
+ overlay.classList.remove('show');
3174
+ return;
2585
3175
  }
2586
- });
2587
3176
 
2588
- evtSource.addEventListener('pending-request', (e) => {
2589
- const data = JSON.parse(e.data);
2590
- addPendingRequest(data);
2591
- });
3177
+ let html = '';
3178
+ for (const req of pendingRequests.values()) {
3179
+ const remaining = Math.max(0, Math.floor((req.expiresAt - Date.now()) / 1000));
3180
+ html += \`
3181
+ <div class="pending-item">
3182
+ <div class="pending-title">\${esc(req.title)}</div>
3183
+ <div class="pending-countdown">Expires in \${remaining}s</div>
3184
+ <div class="pending-actions">
3185
+ <button class="pending-btn pending-approve" data-id="\${req.id}">Approve</button>
3186
+ <button class="pending-btn pending-deny" data-id="\${req.id}">Deny</button>
3187
+ </div>
3188
+ </div>
3189
+ \`;
3190
+ }
2592
3191
 
2593
- evtSource.addEventListener('request-resolved', (e) => {
2594
- const data = JSON.parse(e.data);
2595
- removePendingRequest(data.request_id);
2596
- });
3192
+ items.innerHTML = html;
2597
3193
 
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 || ''
3194
+ document.querySelectorAll('.pending-approve').forEach((btn) => {
3195
+ btn.addEventListener('click', async () => {
3196
+ const id = btn.getAttribute('data-id');
3197
+ await fetchAPI(\`/api/approve/\${id}\`);
3198
+ });
2606
3199
  });
2607
- });
2608
3200
 
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
3201
+ document.querySelectorAll('.pending-deny').forEach((btn) => {
3202
+ btn.addEventListener('click', async () => {
3203
+ const id = btn.getAttribute('data-id');
3204
+ await fetchAPI(\`/api/deny/\${id}\`);
3205
+ });
2618
3206
  });
2619
- });
3207
+ }
2620
3208
 
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
2630
- });
2631
- addThreat({
2632
- timestamp: data.timestamp,
2633
- severity: data.severity || 'medium',
2634
- type: 'Injection Alert',
2635
- details: data.signal || 'Suspicious pattern detected'
2636
- });
2637
- });
3209
+ // Threat Panel
3210
+ function addThreatAlert(alert) {
3211
+ const panel = document.querySelector('.threat-panel');
3212
+ const content = document.getElementById('threat-alerts');
2638
3213
 
2639
- evtSource.addEventListener('protection-status', (e) => {
2640
- const data = JSON.parse(e.data);
2641
- updateProtectionStatus(data);
2642
- });
3214
+ if (content.querySelector('.empty-state')) {
3215
+ content.innerHTML = '';
3216
+ }
2643
3217
 
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
- });
3218
+ panel.classList.remove('collapsed');
2648
3219
 
2649
- evtSource.addEventListener('baseline-update', (e) => {
2650
- const data = JSON.parse(e.data);
2651
- updateBaseline(data);
2652
- });
2653
- }
3220
+ const html = \`
3221
+ <div class="threat-alert">
3222
+ <div class="threat-type">\${esc(alert.type || 'Injection Alert')}</div>
3223
+ <div class="threat-message">\${esc(alert.message || alert.pattern || '\u2014')}</div>
3224
+ </div>
3225
+ \`;
2654
3226
 
2655
- function updateBaseline(baseline) {
2656
- if (!baseline) return;
2657
- // Update baseline-derived stats if needed
2658
- }
3227
+ content.insertAdjacentHTML('afterbegin', html);
2659
3228
 
2660
- function updatePolicy(policy) {
2661
- if (!policy) return;
2662
- // Update policy-derived stats
2663
- if (policy.approval_channel) {
2664
- // Policy info updated
3229
+ const alerts = content.querySelectorAll('.threat-alert');
3230
+ if (alerts.length > 10) {
3231
+ alerts[alerts.length - 1].remove();
3232
+ }
2665
3233
  }
2666
- }
2667
3234
 
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';
2695
- }
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';
2700
- }
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';
2705
- }
2706
- }
3235
+ // Threat Panel Toggle
3236
+ document.querySelector('.threat-header').addEventListener('click', () => {
3237
+ document.querySelector('.threat-panel').classList.toggle('collapsed');
3238
+ });
2707
3239
 
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
3240
+ // SHR Copy Button
3241
+ document.getElementById('copy-shr-btn').addEventListener('click', async () => {
3242
+ if (!apiState.shr) return;
2709
3243
 
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);
2715
- }
2716
- connect();
3244
+ const json = JSON.stringify(apiState.shr, null, 2);
3245
+ try {
3246
+ await navigator.clipboard.writeText(json);
3247
+ const btn = document.getElementById('copy-shr-btn');
3248
+ const original = btn.textContent;
3249
+ btn.textContent = 'Copied!';
3250
+ setTimeout(() => {
3251
+ btn.textContent = original;
3252
+ }, 2000);
3253
+ } catch (err) {
3254
+ console.error('Copy failed:', err);
3255
+ }
3256
+ });
2717
3257
 
2718
- // Start uptime ticker
2719
- setInterval(updateUptime, 1000);
2720
- updateUptime();
3258
+ // Pending Overlay Toggle
3259
+ document.getElementById('pending-item-badge').addEventListener('click', () => {
3260
+ document.getElementById('pending-overlay').classList.toggle('show');
3261
+ });
2721
3262
 
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
- }
3263
+ // Initialize
3264
+ async function initialize() {
3265
+ if (!AUTH_TOKEN) {
3266
+ redirectToLogin();
3267
+ return;
2730
3268
  }
2731
- }, 1000);
2732
3269
 
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);
2740
- }
2741
- } catch (e) {
2742
- // Ignore
2743
- }
2744
- })();
3270
+ // Initial data fetch
3271
+ await Promise.all([
3272
+ updateSovereignty(),
3273
+ updateIdentity(),
3274
+ updateHandshakes(),
3275
+ updateSHR(),
3276
+ updateStatus(),
3277
+ ]);
2745
3278
 
2746
- })();
2747
- </script>
3279
+ // Setup SSE for real-time updates
3280
+ setupSSE();
2748
3281
 
3282
+ // Refresh status periodically
3283
+ setInterval(updateStatus, 30000);
3284
+ }
3285
+
3286
+ // Start
3287
+ initialize();
3288
+ </script>
2749
3289
  </body>
2750
3290
  </html>`;
2751
3291
  }
@@ -2757,6 +3297,7 @@ var SESSION_TTL_REMOTE_MS, SESSION_TTL_LOCAL_MS, MAX_SESSIONS, RATE_LIMIT_WINDOW
2757
3297
  var init_dashboard = __esm({
2758
3298
  "src/principal-policy/dashboard.ts"() {
2759
3299
  init_config();
3300
+ init_generator();
2760
3301
  init_dashboard_html();
2761
3302
  SESSION_TTL_REMOTE_MS = 5 * 60 * 1e3;
2762
3303
  SESSION_TTL_LOCAL_MS = 24 * 60 * 60 * 1e3;
@@ -2773,6 +3314,10 @@ var init_dashboard = __esm({
2773
3314
  policy = null;
2774
3315
  baseline = null;
2775
3316
  auditLog = null;
3317
+ identityManager = null;
3318
+ handshakeResults = null;
3319
+ shrOpts = null;
3320
+ _sanctuaryConfig = null;
2776
3321
  dashboardHTML;
2777
3322
  loginHTML;
2778
3323
  authToken;
@@ -2806,6 +3351,10 @@ var init_dashboard = __esm({
2806
3351
  this.policy = deps.policy;
2807
3352
  this.baseline = deps.baseline;
2808
3353
  this.auditLog = deps.auditLog;
3354
+ if (deps.identityManager) this.identityManager = deps.identityManager;
3355
+ if (deps.handshakeResults) this.handshakeResults = deps.handshakeResults;
3356
+ if (deps.shrOpts) this.shrOpts = deps.shrOpts;
3357
+ if (deps.sanctuaryConfig) this._sanctuaryConfig = deps.sanctuaryConfig;
2809
3358
  }
2810
3359
  /**
2811
3360
  * Start the HTTP(S) server for the dashboard.
@@ -3138,6 +3687,14 @@ var init_dashboard = __esm({
3138
3687
  this.handlePendingList(res);
3139
3688
  } else if (method === "GET" && url.pathname === "/api/audit-log") {
3140
3689
  this.handleAuditLog(url, res);
3690
+ } else if (method === "GET" && url.pathname === "/api/sovereignty") {
3691
+ this.handleSovereignty(res);
3692
+ } else if (method === "GET" && url.pathname === "/api/identity") {
3693
+ this.handleIdentity(res);
3694
+ } else if (method === "GET" && url.pathname === "/api/handshakes") {
3695
+ this.handleHandshakes(res);
3696
+ } else if (method === "GET" && url.pathname === "/api/shr") {
3697
+ this.handleSHR(res);
3141
3698
  } else if (method === "POST" && url.pathname.startsWith("/api/approve/")) {
3142
3699
  if (!this.checkRateLimit(req, res, "decisions")) return;
3143
3700
  const id = url.pathname.slice("/api/approve/".length);
@@ -3327,6 +3884,107 @@ data: ${JSON.stringify(initData)}
3327
3884
  res.writeHead(200, { "Content-Type": "application/json" });
3328
3885
  res.end(JSON.stringify({ success: true, decision }));
3329
3886
  }
3887
+ // ── Sovereignty Data Routes ─────────────────────────────────────────
3888
+ handleSovereignty(res) {
3889
+ if (!this.shrOpts) {
3890
+ res.writeHead(200, { "Content-Type": "application/json" });
3891
+ res.end(JSON.stringify({ error: "SHR generator not available" }));
3892
+ return;
3893
+ }
3894
+ const shr = generateSHR(void 0, this.shrOpts);
3895
+ if (typeof shr === "string") {
3896
+ res.writeHead(200, { "Content-Type": "application/json" });
3897
+ res.end(JSON.stringify({ error: shr }));
3898
+ return;
3899
+ }
3900
+ const layers = shr.body.layers;
3901
+ let score = 0;
3902
+ for (const layer of [layers.l1, layers.l2, layers.l3, layers.l4]) {
3903
+ if (layer.status === "active") score += 25;
3904
+ else if (layer.status === "degraded") score += 15;
3905
+ }
3906
+ const overallLevel = score === 100 ? "full" : score >= 65 ? "degraded" : score >= 25 ? "minimal" : "unverified";
3907
+ res.writeHead(200, { "Content-Type": "application/json" });
3908
+ res.end(JSON.stringify({
3909
+ score,
3910
+ overall_level: overallLevel,
3911
+ layers: {
3912
+ l1: { status: layers.l1.status, detail: layers.l1.encryption, key_custody: layers.l1.key_custody },
3913
+ l2: { status: layers.l2.status, detail: layers.l2.isolation_type, attestation: layers.l2.attestation_available },
3914
+ l3: { status: layers.l3.status, detail: layers.l3.proof_system, selective_disclosure: layers.l3.selective_disclosure },
3915
+ l4: { status: layers.l4.status, detail: layers.l4.attestation_format, reputation_portable: layers.l4.reputation_portable }
3916
+ },
3917
+ degradations: shr.body.degradations,
3918
+ capabilities: shr.body.capabilities,
3919
+ config_loaded: this._sanctuaryConfig != null
3920
+ }));
3921
+ }
3922
+ handleIdentity(res) {
3923
+ if (!this.identityManager) {
3924
+ res.writeHead(200, { "Content-Type": "application/json" });
3925
+ res.end(JSON.stringify({ identities: [], count: 0 }));
3926
+ return;
3927
+ }
3928
+ const identities = this.identityManager.list().map((id) => ({
3929
+ identity_id: id.identity_id,
3930
+ label: id.label,
3931
+ public_key: id.public_key,
3932
+ did: id.did,
3933
+ created_at: id.created_at,
3934
+ key_type: id.key_type,
3935
+ key_protection: id.key_protection,
3936
+ rotation_count: id.rotation_history?.length ?? 0
3937
+ }));
3938
+ const primary = this.identityManager.getDefault();
3939
+ res.writeHead(200, { "Content-Type": "application/json" });
3940
+ res.end(JSON.stringify({
3941
+ identities,
3942
+ count: identities.length,
3943
+ primary_id: primary?.identity_id ?? null
3944
+ }));
3945
+ }
3946
+ handleHandshakes(res) {
3947
+ if (!this.handshakeResults) {
3948
+ res.writeHead(200, { "Content-Type": "application/json" });
3949
+ res.end(JSON.stringify({ handshakes: [], count: 0 }));
3950
+ return;
3951
+ }
3952
+ const handshakes = Array.from(this.handshakeResults.values()).map((h) => ({
3953
+ counterparty_id: h.counterparty_id,
3954
+ verified: h.verified,
3955
+ sovereignty_level: h.sovereignty_level,
3956
+ trust_tier: h.trust_tier,
3957
+ completed_at: h.completed_at,
3958
+ expires_at: h.expires_at,
3959
+ errors: h.errors
3960
+ }));
3961
+ handshakes.sort((a, b) => new Date(b.completed_at).getTime() - new Date(a.completed_at).getTime());
3962
+ res.writeHead(200, { "Content-Type": "application/json" });
3963
+ res.end(JSON.stringify({
3964
+ handshakes,
3965
+ count: handshakes.length,
3966
+ tier_distribution: {
3967
+ verified_sovereign: handshakes.filter((h) => h.trust_tier === "verified-sovereign").length,
3968
+ verified_degraded: handshakes.filter((h) => h.trust_tier === "verified-degraded").length,
3969
+ unverified: handshakes.filter((h) => h.trust_tier === "unverified").length
3970
+ }
3971
+ }));
3972
+ }
3973
+ handleSHR(res) {
3974
+ if (!this.shrOpts) {
3975
+ res.writeHead(200, { "Content-Type": "application/json" });
3976
+ res.end(JSON.stringify({ error: "SHR generator not available" }));
3977
+ return;
3978
+ }
3979
+ const shr = generateSHR(void 0, this.shrOpts);
3980
+ if (typeof shr === "string") {
3981
+ res.writeHead(200, { "Content-Type": "application/json" });
3982
+ res.end(JSON.stringify({ error: shr }));
3983
+ return;
3984
+ }
3985
+ res.writeHead(200, { "Content-Type": "application/json" });
3986
+ res.end(JSON.stringify(shr));
3987
+ }
3330
3988
  // ── SSE Broadcasting ────────────────────────────────────────────────
3331
3989
  broadcastSSE(event, data) {
3332
3990
  const message = `event: ${event}
@@ -3517,162 +4175,58 @@ async function startStandaloneDashboard(options = {}) {
3517
4175
  const auditLog = new AuditLog(storage, masterKey);
3518
4176
  const policy = await loadPrincipalPolicy(config.storage_path);
3519
4177
  const baseline = new BaselineTracker(storage, masterKey);
3520
- await baseline.load();
3521
- const dashboardPort = options.port ?? config.dashboard.port;
3522
- const dashboardHost = options.host ?? config.dashboard.host;
3523
- let authToken = config.dashboard.auth_token;
3524
- if (authToken === "auto") {
3525
- const { randomBytes: randomBytes4 } = await import('crypto');
3526
- authToken = randomBytes4(32).toString("hex");
3527
- }
3528
- const dashboard = new DashboardApprovalChannel({
3529
- port: dashboardPort,
3530
- host: dashboardHost,
3531
- timeout_seconds: policy.approval_channel.timeout_seconds,
3532
- auth_token: authToken,
3533
- tls: config.dashboard.tls,
3534
- auto_open: config.dashboard.auto_open ?? true
3535
- // Default to auto-open in standalone mode
3536
- });
3537
- dashboard.setDependencies({ policy, baseline, auditLog });
3538
- await dashboard.start();
3539
- console.error(`Sanctuary Dashboard v${SANCTUARY_VERSION} (standalone mode)`);
3540
- console.error(`Storage: ${config.storage_path}`);
3541
- console.error(`Listening: http://${dashboardHost}:${dashboardPort}`);
3542
- const saveBaseline = () => {
3543
- baseline.save().catch(() => {
3544
- });
3545
- };
3546
- process.on("SIGINT", saveBaseline);
3547
- process.on("SIGTERM", saveBaseline);
3548
- return dashboard;
3549
- }
3550
- var init_dashboard_standalone = __esm({
3551
- "src/dashboard-standalone.ts"() {
3552
- init_config();
3553
- init_filesystem();
3554
- init_audit_log();
3555
- init_loader();
3556
- init_baseline();
3557
- init_dashboard();
3558
- init_key_derivation();
3559
- init_random();
3560
- init_encoding();
3561
- }
3562
- });
3563
-
3564
- // src/index.ts
3565
- init_config();
3566
- init_filesystem();
3567
-
3568
- // src/l1-cognitive/state-store.ts
3569
- init_encryption();
3570
- init_hashing();
3571
-
3572
- // src/core/identity.ts
3573
- init_encoding();
3574
- init_encryption();
3575
- init_hashing();
3576
- init_random();
3577
- function generateKeypair() {
3578
- const privateKey = randomBytes(32);
3579
- const publicKey = ed25519.ed25519.getPublicKey(privateKey);
3580
- return { publicKey, privateKey };
3581
- }
3582
- function publicKeyToDid(publicKey) {
3583
- const multicodec = new Uint8Array([237, 1, ...publicKey]);
3584
- return `did:key:z${toBase64url(multicodec)}`;
3585
- }
3586
- function generateIdentityId(publicKey) {
3587
- const keyHash = hash(publicKey);
3588
- return Array.from(keyHash.slice(0, 16)).map((b) => b.toString(16).padStart(2, "0")).join("");
3589
- }
3590
- function createIdentity(label, encryptionKey, keyProtection) {
3591
- const { publicKey, privateKey } = generateKeypair();
3592
- const identityId = generateIdentityId(publicKey);
3593
- const did = publicKeyToDid(publicKey);
3594
- const now = (/* @__PURE__ */ new Date()).toISOString();
3595
- const encryptedPrivateKey = encrypt(privateKey, encryptionKey);
3596
- privateKey.fill(0);
3597
- const publicIdentity = {
3598
- identity_id: identityId,
3599
- label,
3600
- public_key: toBase64url(publicKey),
3601
- did,
3602
- created_at: now,
3603
- key_type: "ed25519",
3604
- key_protection: keyProtection
3605
- };
3606
- const storedIdentity = {
3607
- ...publicIdentity,
3608
- encrypted_private_key: encryptedPrivateKey,
3609
- rotation_history: []
3610
- };
3611
- return { publicIdentity, storedIdentity };
3612
- }
3613
- function sign(payload, encryptedPrivateKey, encryptionKey) {
3614
- const privateKey = decrypt(encryptedPrivateKey, encryptionKey);
3615
- try {
3616
- return ed25519.ed25519.sign(payload, privateKey);
3617
- } finally {
3618
- privateKey.fill(0);
3619
- }
3620
- }
3621
- function verify(payload, signature, publicKey) {
3622
- try {
3623
- return ed25519.ed25519.verify(signature, payload, publicKey);
3624
- } catch {
3625
- return false;
3626
- }
3627
- }
3628
- function rotateKeys(storedIdentity, encryptionKey, reason) {
3629
- const { publicKey: newPublicKey, privateKey: newPrivateKey } = generateKeypair();
3630
- const newIdentityDid = publicKeyToDid(newPublicKey);
3631
- const now = (/* @__PURE__ */ new Date()).toISOString();
3632
- const eventData = JSON.stringify({
3633
- old_public_key: storedIdentity.public_key,
3634
- new_public_key: toBase64url(newPublicKey),
3635
- identity_id: storedIdentity.identity_id,
3636
- reason,
3637
- rotated_at: now
3638
- });
3639
- const eventBytes = new TextEncoder().encode(eventData);
3640
- const signature = sign(
3641
- eventBytes,
3642
- storedIdentity.encrypted_private_key,
3643
- encryptionKey
3644
- );
3645
- const rotationEvent = {
3646
- old_public_key: storedIdentity.public_key,
3647
- new_public_key: toBase64url(newPublicKey),
3648
- identity_id: storedIdentity.identity_id,
3649
- reason,
3650
- rotated_at: now,
3651
- signature: toBase64url(signature)
3652
- };
3653
- const encryptedNewPrivateKey = encrypt(newPrivateKey, encryptionKey);
3654
- newPrivateKey.fill(0);
3655
- const updatedIdentity = {
3656
- ...storedIdentity,
3657
- public_key: toBase64url(newPublicKey),
3658
- did: newIdentityDid,
3659
- encrypted_private_key: encryptedNewPrivateKey,
3660
- rotation_history: [
3661
- ...storedIdentity.rotation_history,
3662
- {
3663
- old_public_key: storedIdentity.public_key,
3664
- new_public_key: toBase64url(newPublicKey),
3665
- rotation_event: toBase64url(
3666
- new TextEncoder().encode(JSON.stringify(rotationEvent))
3667
- ),
3668
- rotated_at: now
3669
- }
3670
- ]
4178
+ await baseline.load();
4179
+ const dashboardPort = options.port ?? config.dashboard.port;
4180
+ const dashboardHost = options.host ?? config.dashboard.host;
4181
+ let authToken = config.dashboard.auth_token;
4182
+ if (authToken === "auto") {
4183
+ const { randomBytes: randomBytes4 } = await import('crypto');
4184
+ authToken = randomBytes4(32).toString("hex");
4185
+ }
4186
+ const dashboard = new DashboardApprovalChannel({
4187
+ port: dashboardPort,
4188
+ host: dashboardHost,
4189
+ timeout_seconds: policy.approval_channel.timeout_seconds,
4190
+ auth_token: authToken,
4191
+ tls: config.dashboard.tls,
4192
+ auto_open: config.dashboard.auto_open ?? true
4193
+ // Default to auto-open in standalone mode
4194
+ });
4195
+ dashboard.setDependencies({ policy, baseline, auditLog });
4196
+ await dashboard.start();
4197
+ console.error(`Sanctuary Dashboard v${SANCTUARY_VERSION} (standalone mode)`);
4198
+ console.error(`Storage: ${config.storage_path}`);
4199
+ console.error(`Listening: http://${dashboardHost}:${dashboardPort}`);
4200
+ const saveBaseline = () => {
4201
+ baseline.save().catch(() => {
4202
+ });
3671
4203
  };
3672
- return { updatedIdentity, rotationEvent };
4204
+ process.on("SIGINT", saveBaseline);
4205
+ process.on("SIGTERM", saveBaseline);
4206
+ return dashboard;
3673
4207
  }
4208
+ var init_dashboard_standalone = __esm({
4209
+ "src/dashboard-standalone.ts"() {
4210
+ init_config();
4211
+ init_filesystem();
4212
+ init_audit_log();
4213
+ init_loader();
4214
+ init_baseline();
4215
+ init_dashboard();
4216
+ init_key_derivation();
4217
+ init_random();
4218
+ init_encoding();
4219
+ }
4220
+ });
4221
+
4222
+ // src/index.ts
4223
+ init_config();
4224
+ init_filesystem();
3674
4225
 
3675
4226
  // src/l1-cognitive/state-store.ts
4227
+ init_encryption();
4228
+ init_hashing();
4229
+ init_identity();
3676
4230
  init_key_derivation();
3677
4231
  init_encoding();
3678
4232
  var RESERVED_NAMESPACE_PREFIXES = [
@@ -4212,6 +4766,7 @@ function toolResult(data) {
4212
4766
  }
4213
4767
 
4214
4768
  // src/l1-cognitive/tools.ts
4769
+ init_identity();
4215
4770
  init_key_derivation();
4216
4771
  init_encoding();
4217
4772
  init_encryption();
@@ -5621,6 +6176,7 @@ init_encryption();
5621
6176
  init_key_derivation();
5622
6177
  init_encoding();
5623
6178
  init_random();
6179
+ init_identity();
5624
6180
  function computeMedian(values) {
5625
6181
  if (values.length === 0) return 0;
5626
6182
  const sorted = [...values].sort((a, b) => a - b);
@@ -7593,110 +8149,12 @@ function createPrincipalPolicyTools(policy, baseline, auditLog) {
7593
8149
  ];
7594
8150
  }
7595
8151
 
7596
- // src/shr/types.ts
7597
- function deepSortKeys(obj) {
7598
- if (obj === null || typeof obj !== "object") return obj;
7599
- if (Array.isArray(obj)) return obj.map(deepSortKeys);
7600
- const sorted = {};
7601
- for (const key of Object.keys(obj).sort()) {
7602
- sorted[key] = deepSortKeys(obj[key]);
7603
- }
7604
- return sorted;
7605
- }
7606
- function canonicalizeForSigning(body) {
7607
- return JSON.stringify(deepSortKeys(body));
7608
- }
7609
-
7610
- // src/shr/generator.ts
7611
- init_encoding();
7612
- init_key_derivation();
7613
- var DEFAULT_VALIDITY_MS = 60 * 60 * 1e3;
7614
- function generateSHR(identityId, opts) {
7615
- const { config, identityManager, masterKey, validityMs } = opts;
7616
- const identity = identityId ? identityManager.get(identityId) : identityManager.getDefault();
7617
- if (!identity) {
7618
- return "No identity available for signing. Create an identity first.";
7619
- }
7620
- const now = /* @__PURE__ */ new Date();
7621
- const expiresAt = new Date(now.getTime() + (validityMs ?? DEFAULT_VALIDITY_MS));
7622
- const degradations = [];
7623
- if (config.execution.environment === "local-process") {
7624
- degradations.push({
7625
- layer: "l2",
7626
- code: "PROCESS_ISOLATION_ONLY",
7627
- severity: "warning",
7628
- description: "Process-level isolation only (no TEE)",
7629
- mitigation: "TEE support planned for a future release"
7630
- });
7631
- degradations.push({
7632
- layer: "l2",
7633
- code: "SELF_REPORTED_ATTESTATION",
7634
- severity: "warning",
7635
- description: "Attestation is self-reported (no hardware root of trust)",
7636
- mitigation: "TEE attestation planned for a future release"
7637
- });
7638
- }
7639
- const body = {
7640
- shr_version: "1.0",
7641
- implementation: {
7642
- sanctuary_version: config.version,
7643
- node_version: process.versions.node,
7644
- generated_by: "sanctuary-mcp-server"
7645
- },
7646
- instance_id: identity.identity_id,
7647
- generated_at: now.toISOString(),
7648
- expires_at: expiresAt.toISOString(),
7649
- layers: {
7650
- l1: {
7651
- status: "active",
7652
- encryption: config.state.encryption,
7653
- key_custody: "self",
7654
- integrity: config.state.integrity,
7655
- identity_type: config.state.identity_provider,
7656
- state_portable: true
7657
- },
7658
- l2: {
7659
- status: config.execution.environment === "local-process" ? "degraded" : "active",
7660
- isolation_type: config.execution.environment,
7661
- attestation_available: config.execution.attestation
7662
- },
7663
- l3: {
7664
- status: "active",
7665
- proof_system: config.disclosure.proof_system,
7666
- selective_disclosure: true
7667
- },
7668
- l4: {
7669
- status: "active",
7670
- reputation_mode: config.reputation.mode,
7671
- attestation_format: config.reputation.attestation_format,
7672
- reputation_portable: true
7673
- }
7674
- },
7675
- capabilities: {
7676
- handshake: true,
7677
- shr_exchange: true,
7678
- reputation_verify: true,
7679
- encrypted_channel: false
7680
- // Not yet implemented
7681
- },
7682
- degradations
7683
- };
7684
- const canonical = canonicalizeForSigning(body);
7685
- const payload = stringToBytes(canonical);
7686
- const encryptionKey = derivePurposeKey(masterKey, "identity-encryption");
7687
- const signatureBytes = sign(
7688
- payload,
7689
- identity.encrypted_private_key,
7690
- encryptionKey
7691
- );
7692
- return {
7693
- body,
7694
- signed_by: identity.public_key,
7695
- signature: toBase64url(signatureBytes)
7696
- };
7697
- }
8152
+ // src/shr/tools.ts
8153
+ init_generator();
7698
8154
 
7699
8155
  // src/shr/verifier.ts
8156
+ init_types();
8157
+ init_identity();
7700
8158
  init_encoding();
7701
8159
  function verifySHR(shr, now) {
7702
8160
  const errors = [];
@@ -8118,7 +8576,11 @@ function createSHRTools(config, identityManager, masterKey, auditLog) {
8118
8576
  return { tools };
8119
8577
  }
8120
8578
 
8579
+ // src/handshake/tools.ts
8580
+ init_generator();
8581
+
8121
8582
  // src/handshake/protocol.ts
8583
+ init_identity();
8122
8584
  init_encoding();
8123
8585
  init_random();
8124
8586
  init_key_derivation();
@@ -8287,6 +8749,158 @@ function deriveTrustTier(level) {
8287
8749
  }
8288
8750
  }
8289
8751
 
8752
+ // src/handshake/attestation.ts
8753
+ init_types();
8754
+ init_identity();
8755
+ init_encoding();
8756
+ init_key_derivation();
8757
+ init_identity();
8758
+ init_encoding();
8759
+ var ATTESTATION_VERSION = "1.0";
8760
+ function deriveTrustTier2(level) {
8761
+ switch (level) {
8762
+ case "full":
8763
+ return "verified-sovereign";
8764
+ case "degraded":
8765
+ return "verified-degraded";
8766
+ default:
8767
+ return "unverified";
8768
+ }
8769
+ }
8770
+ function generateAttestation(opts) {
8771
+ const {
8772
+ attesterSHR,
8773
+ subjectSHR,
8774
+ verificationResult,
8775
+ mutual = false,
8776
+ identityManager,
8777
+ masterKey,
8778
+ identityId
8779
+ } = opts;
8780
+ const identity = identityId ? identityManager.get(identityId) : identityManager.getDefault();
8781
+ if (!identity) {
8782
+ return { error: "No identity available for signing attestation" };
8783
+ }
8784
+ const now = /* @__PURE__ */ new Date();
8785
+ const attesterExpiry = new Date(attesterSHR.body.expires_at);
8786
+ const subjectExpiry = new Date(subjectSHR.body.expires_at);
8787
+ const earliestExpiry = attesterExpiry < subjectExpiry ? attesterExpiry : subjectExpiry;
8788
+ const sovereigntyLevel = verificationResult.valid ? verificationResult.sovereignty_level : "unverified";
8789
+ const body = {
8790
+ attestation_version: ATTESTATION_VERSION,
8791
+ attester_id: attesterSHR.body.instance_id,
8792
+ subject_id: subjectSHR.body.instance_id,
8793
+ attester_shr: attesterSHR,
8794
+ subject_shr: subjectSHR,
8795
+ verification: {
8796
+ subject_shr_valid: verificationResult.valid,
8797
+ subject_sovereignty_level: sovereigntyLevel,
8798
+ subject_trust_tier: deriveTrustTier2(sovereigntyLevel),
8799
+ mutual,
8800
+ errors: verificationResult.errors,
8801
+ warnings: verificationResult.warnings
8802
+ },
8803
+ attested_at: now.toISOString(),
8804
+ expires_at: earliestExpiry.toISOString()
8805
+ };
8806
+ const canonical = JSON.stringify(deepSortKeys(body));
8807
+ const payload = stringToBytes(canonical);
8808
+ const encryptionKey = derivePurposeKey(masterKey, "identity-encryption");
8809
+ const signatureBytes = sign(
8810
+ payload,
8811
+ identity.encrypted_private_key,
8812
+ encryptionKey
8813
+ );
8814
+ const summary = generateSummary(body);
8815
+ return {
8816
+ body,
8817
+ signed_by: identity.public_key,
8818
+ signature: toBase64url(signatureBytes),
8819
+ summary
8820
+ };
8821
+ }
8822
+ function layerLine(label, status) {
8823
+ const icon = status === "active" ? "\u2713" : status === "degraded" ? "~" : "x";
8824
+ return ` ${icon} ${label}: ${status}`;
8825
+ }
8826
+ function generateSummary(body) {
8827
+ const v = body.verification;
8828
+ const sLayers = body.subject_shr.body.layers;
8829
+ const aLayers = body.attester_shr.body.layers;
8830
+ const tierLabel = v.subject_trust_tier === "verified-sovereign" ? "Verified Sovereign" : v.subject_trust_tier === "verified-degraded" ? "Verified (Degraded)" : "Unverified";
8831
+ const lines = [
8832
+ `--- Sovereignty Attestation ---`,
8833
+ ``,
8834
+ `Attester: ${body.attester_id.slice(0, 16)}...`,
8835
+ `Subject: ${body.subject_id.slice(0, 16)}...`,
8836
+ `Result: ${tierLabel}`,
8837
+ ``,
8838
+ `Subject Sovereignty Posture:`,
8839
+ layerLine("L1 Cognitive Sovereignty", sLayers.l1.status),
8840
+ layerLine("L2 Operational Isolation", sLayers.l2.status),
8841
+ layerLine("L3 Selective Disclosure", sLayers.l3.status),
8842
+ layerLine("L4 Verifiable Reputation", sLayers.l4.status),
8843
+ ``,
8844
+ `Attester Sovereignty Posture:`,
8845
+ layerLine("L1 Cognitive Sovereignty", aLayers.l1.status),
8846
+ layerLine("L2 Operational Isolation", aLayers.l2.status),
8847
+ layerLine("L3 Selective Disclosure", aLayers.l3.status),
8848
+ layerLine("L4 Verifiable Reputation", aLayers.l4.status),
8849
+ ``,
8850
+ `Mutual: ${v.mutual ? "Yes" : "One-sided"}`,
8851
+ `Attested: ${body.attested_at}`,
8852
+ `Expires: ${body.expires_at}`,
8853
+ `Signature: ${body.attestation_version} / Ed25519`
8854
+ ];
8855
+ if (v.warnings.length > 0) {
8856
+ lines.push(``, `Warnings: ${v.warnings.join("; ")}`);
8857
+ }
8858
+ if (v.errors.length > 0) {
8859
+ lines.push(``, `Errors: ${v.errors.join("; ")}`);
8860
+ }
8861
+ lines.push(``, `--- Verify: compare signed_by against attester's known public key ---`);
8862
+ return lines.join("\n");
8863
+ }
8864
+ function verifyAttestation(attestation, now) {
8865
+ const errors = [];
8866
+ const currentTime = /* @__PURE__ */ new Date();
8867
+ if (attestation.body.attestation_version !== ATTESTATION_VERSION) {
8868
+ errors.push(
8869
+ `Unsupported attestation version: ${attestation.body.attestation_version}`
8870
+ );
8871
+ }
8872
+ if (!attestation.body.attester_id || !attestation.body.subject_id) {
8873
+ errors.push("Missing attester_id or subject_id");
8874
+ }
8875
+ if (!attestation.body.attester_shr || !attestation.body.subject_shr) {
8876
+ errors.push("Missing attester or subject SHR");
8877
+ }
8878
+ const expired = new Date(attestation.body.expires_at) <= currentTime;
8879
+ if (expired) {
8880
+ errors.push("Attestation has expired");
8881
+ }
8882
+ try {
8883
+ const publicKey = fromBase64url(attestation.signed_by);
8884
+ const canonical = JSON.stringify(deepSortKeys(attestation.body));
8885
+ const payload = stringToBytes(canonical);
8886
+ const signatureBytes = fromBase64url(attestation.signature);
8887
+ const signatureValid = verify(payload, signatureBytes, publicKey);
8888
+ if (!signatureValid) {
8889
+ errors.push("Attestation signature is invalid");
8890
+ }
8891
+ } catch (e) {
8892
+ errors.push(`Signature verification error: ${e.message}`);
8893
+ }
8894
+ return {
8895
+ valid: errors.length === 0,
8896
+ errors,
8897
+ attester_id: attestation.body.attester_id ?? "unknown",
8898
+ subject_id: attestation.body.subject_id ?? "unknown",
8899
+ trust_tier: errors.length === 0 ? attestation.body.verification.subject_trust_tier : "unverified",
8900
+ expired
8901
+ };
8902
+ }
8903
+
8290
8904
  // src/handshake/tools.ts
8291
8905
  function createHandshakeTools(config, identityManager, masterKey, auditLog) {
8292
8906
  const sessions = /* @__PURE__ */ new Map();
@@ -8472,6 +9086,103 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
8472
9086
  result: session.result ?? null
8473
9087
  });
8474
9088
  }
9089
+ },
9090
+ // ─── Streamlined Exchange ─────────────────────────────────────────
9091
+ {
9092
+ name: "sanctuary/handshake_exchange",
9093
+ 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).",
9094
+ inputSchema: {
9095
+ type: "object",
9096
+ properties: {
9097
+ counterparty_shr: {
9098
+ type: "object",
9099
+ description: "The counterparty's signed SHR (SignedSHR object with body, signed_by, signature)."
9100
+ },
9101
+ identity_id: {
9102
+ type: "string",
9103
+ description: "Identity to use for the exchange. Defaults to primary identity."
9104
+ }
9105
+ },
9106
+ required: ["counterparty_shr"]
9107
+ },
9108
+ handler: async (args) => {
9109
+ const counterpartySHR = args.counterparty_shr;
9110
+ const ourSHR = generateSHR(args.identity_id, shrOpts);
9111
+ if (typeof ourSHR === "string") {
9112
+ return toolResult({ error: ourSHR });
9113
+ }
9114
+ const verificationResult = verifySHR(counterpartySHR);
9115
+ const attestation = generateAttestation({
9116
+ attesterSHR: ourSHR,
9117
+ subjectSHR: counterpartySHR,
9118
+ verificationResult,
9119
+ mutual: false,
9120
+ identityManager,
9121
+ masterKey,
9122
+ identityId: args.identity_id
9123
+ });
9124
+ if ("error" in attestation) {
9125
+ auditLog.append("l4", "handshake_exchange", ourSHR.body.instance_id, void 0, "failure");
9126
+ return toolResult({ error: attestation.error });
9127
+ }
9128
+ if (verificationResult.valid) {
9129
+ const sovereigntyLevel = verificationResult.sovereignty_level;
9130
+ const trustTier = sovereigntyLevel === "full" ? "verified-sovereign" : sovereigntyLevel === "degraded" ? "verified-degraded" : "unverified";
9131
+ handshakeResults.set(verificationResult.counterparty_id, {
9132
+ counterparty_id: verificationResult.counterparty_id,
9133
+ counterparty_shr: counterpartySHR,
9134
+ verified: true,
9135
+ sovereignty_level: sovereigntyLevel,
9136
+ trust_tier: trustTier,
9137
+ completed_at: (/* @__PURE__ */ new Date()).toISOString(),
9138
+ expires_at: verificationResult.expires_at,
9139
+ errors: []
9140
+ });
9141
+ }
9142
+ auditLog.append("l4", "handshake_exchange", ourSHR.body.instance_id);
9143
+ return toolResult({
9144
+ attestation,
9145
+ our_shr: ourSHR,
9146
+ verification: {
9147
+ counterparty_valid: verificationResult.valid,
9148
+ counterparty_sovereignty: verificationResult.sovereignty_level,
9149
+ counterparty_id: verificationResult.counterparty_id,
9150
+ errors: verificationResult.errors,
9151
+ warnings: verificationResult.warnings
9152
+ },
9153
+ 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.",
9154
+ _content_trust: "external"
9155
+ });
9156
+ }
9157
+ },
9158
+ {
9159
+ name: "sanctuary/handshake_verify_attestation",
9160
+ description: "Verify a signed attestation artifact from another agent. Checks the Ed25519 signature, temporal validity, and structural integrity.",
9161
+ inputSchema: {
9162
+ type: "object",
9163
+ properties: {
9164
+ attestation: {
9165
+ type: "object",
9166
+ description: "The SignedAttestation object to verify (body, signed_by, signature, summary)."
9167
+ }
9168
+ },
9169
+ required: ["attestation"]
9170
+ },
9171
+ handler: async (args) => {
9172
+ const attestation = args.attestation;
9173
+ const result = verifyAttestation(attestation);
9174
+ auditLog.append(
9175
+ "l4",
9176
+ "handshake_verify_attestation",
9177
+ result.attester_id,
9178
+ void 0,
9179
+ result.valid ? "success" : "failure"
9180
+ );
9181
+ return toolResult({
9182
+ ...result,
9183
+ _content_trust: "external"
9184
+ });
9185
+ }
8475
9186
  }
8476
9187
  ];
8477
9188
  return { tools, handshakeResults };
@@ -8841,6 +9552,7 @@ init_encryption();
8841
9552
  init_encoding();
8842
9553
 
8843
9554
  // src/bridge/bridge.ts
9555
+ init_identity();
8844
9556
  init_encoding();
8845
9557
  init_random();
8846
9558
  init_hashing();
@@ -11929,6 +12641,7 @@ init_filesystem();
11929
12641
  init_baseline();
11930
12642
  init_loader();
11931
12643
  init_dashboard();
12644
+ init_generator();
11932
12645
  async function createSanctuaryServer(options) {
11933
12646
  const config = await loadConfig(options?.configPath);
11934
12647
  await promises.mkdir(config.storage_path, { recursive: true, mode: 448 });
@@ -12282,7 +12995,15 @@ async function createSanctuaryServer(options) {
12282
12995
  tls: config.dashboard.tls,
12283
12996
  auto_open: config.dashboard.auto_open
12284
12997
  });
12285
- dashboard.setDependencies({ policy, baseline, auditLog });
12998
+ dashboard.setDependencies({
12999
+ policy,
13000
+ baseline,
13001
+ auditLog,
13002
+ identityManager,
13003
+ handshakeResults,
13004
+ shrOpts: { config, identityManager, masterKey },
13005
+ sanctuaryConfig: config
13006
+ });
12286
13007
  await dashboard.start();
12287
13008
  approvalChannel = dashboard;
12288
13009
  } else if (config.webhook.enabled && config.webhook.url && config.webhook.secret) {