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