@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/index.cjs CHANGED
@@ -3638,6 +3638,8 @@ var DEFAULT_POLICY = {
3638
3638
  "handshake_respond",
3639
3639
  "handshake_complete",
3640
3640
  "handshake_status",
3641
+ "handshake_exchange",
3642
+ "handshake_verify_attestation",
3641
3643
  "reputation_query_weighted",
3642
3644
  "federation_peers",
3643
3645
  "federation_trust_evaluate",
@@ -3807,6 +3809,8 @@ tier3_always_allow:
3807
3809
  - handshake_respond
3808
3810
  - handshake_complete
3809
3811
  - handshake_status
3812
+ - handshake_exchange
3813
+ - handshake_verify_attestation
3810
3814
  - reputation_query_weighted
3811
3815
  - federation_peers
3812
3816
  - federation_trust_evaluate
@@ -4063,179 +4067,362 @@ var AutoApproveChannel = class {
4063
4067
  }
4064
4068
  };
4065
4069
 
4070
+ // src/shr/types.ts
4071
+ function deepSortKeys(obj) {
4072
+ if (obj === null || typeof obj !== "object") return obj;
4073
+ if (Array.isArray(obj)) return obj.map(deepSortKeys);
4074
+ const sorted = {};
4075
+ for (const key of Object.keys(obj).sort()) {
4076
+ sorted[key] = deepSortKeys(obj[key]);
4077
+ }
4078
+ return sorted;
4079
+ }
4080
+ function canonicalizeForSigning(body) {
4081
+ return JSON.stringify(deepSortKeys(body));
4082
+ }
4083
+
4084
+ // src/shr/generator.ts
4085
+ init_encoding();
4086
+ var DEFAULT_VALIDITY_MS = 60 * 60 * 1e3;
4087
+ function generateSHR(identityId, opts) {
4088
+ const { config, identityManager, masterKey, validityMs } = opts;
4089
+ const identity = identityId ? identityManager.get(identityId) : identityManager.getDefault();
4090
+ if (!identity) {
4091
+ return "No identity available for signing. Create an identity first.";
4092
+ }
4093
+ const now = /* @__PURE__ */ new Date();
4094
+ const expiresAt = new Date(now.getTime() + (validityMs ?? DEFAULT_VALIDITY_MS));
4095
+ const degradations = [];
4096
+ if (config.execution.environment === "local-process") {
4097
+ degradations.push({
4098
+ layer: "l2",
4099
+ code: "PROCESS_ISOLATION_ONLY",
4100
+ severity: "warning",
4101
+ description: "Process-level isolation only (no TEE)",
4102
+ mitigation: "TEE support planned for a future release"
4103
+ });
4104
+ degradations.push({
4105
+ layer: "l2",
4106
+ code: "SELF_REPORTED_ATTESTATION",
4107
+ severity: "warning",
4108
+ description: "Attestation is self-reported (no hardware root of trust)",
4109
+ mitigation: "TEE attestation planned for a future release"
4110
+ });
4111
+ }
4112
+ const body = {
4113
+ shr_version: "1.0",
4114
+ implementation: {
4115
+ sanctuary_version: config.version,
4116
+ node_version: process.versions.node,
4117
+ generated_by: "sanctuary-mcp-server"
4118
+ },
4119
+ instance_id: identity.identity_id,
4120
+ generated_at: now.toISOString(),
4121
+ expires_at: expiresAt.toISOString(),
4122
+ layers: {
4123
+ l1: {
4124
+ status: "active",
4125
+ encryption: config.state.encryption,
4126
+ key_custody: "self",
4127
+ integrity: config.state.integrity,
4128
+ identity_type: config.state.identity_provider,
4129
+ state_portable: true
4130
+ },
4131
+ l2: {
4132
+ status: config.execution.environment === "local-process" ? "degraded" : "active",
4133
+ isolation_type: config.execution.environment,
4134
+ attestation_available: config.execution.attestation
4135
+ },
4136
+ l3: {
4137
+ status: "active",
4138
+ proof_system: config.disclosure.proof_system,
4139
+ selective_disclosure: true
4140
+ },
4141
+ l4: {
4142
+ status: "active",
4143
+ reputation_mode: config.reputation.mode,
4144
+ attestation_format: config.reputation.attestation_format,
4145
+ reputation_portable: true
4146
+ }
4147
+ },
4148
+ capabilities: {
4149
+ handshake: true,
4150
+ shr_exchange: true,
4151
+ reputation_verify: true,
4152
+ encrypted_channel: false
4153
+ // Not yet implemented
4154
+ },
4155
+ degradations
4156
+ };
4157
+ const canonical = canonicalizeForSigning(body);
4158
+ const payload = stringToBytes(canonical);
4159
+ const encryptionKey = derivePurposeKey(masterKey, "identity-encryption");
4160
+ const signatureBytes = sign(
4161
+ payload,
4162
+ identity.encrypted_private_key,
4163
+ encryptionKey
4164
+ );
4165
+ return {
4166
+ body,
4167
+ signed_by: identity.public_key,
4168
+ signature: toBase64url(signatureBytes)
4169
+ };
4170
+ }
4171
+
4066
4172
  // src/principal-policy/dashboard-html.ts
4067
4173
  function generateLoginHTML(options) {
4068
4174
  return `<!DOCTYPE html>
4069
4175
  <html lang="en">
4070
4176
  <head>
4071
- <meta charset="utf-8">
4072
- <meta name="viewport" content="width=device-width, initial-scale=1">
4073
- <title>Sanctuary \u2014 Login</title>
4074
- <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
4075
- <style>
4076
- :root {
4077
- --bg: #0d1117;
4078
- --surface: #161b22;
4079
- --border: #30363d;
4080
- --text-primary: #e6edf3;
4081
- --text-secondary: #8b949e;
4082
- --green: #3fb950;
4083
- --red: #f85149;
4084
- --blue: #58a6ff;
4085
- --mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
4086
- --sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
4087
- --radius: 6px;
4088
- }
4089
- * { box-sizing: border-box; margin: 0; padding: 0; }
4090
- html, body { width: 100%; height: 100%; }
4091
- body {
4092
- font-family: var(--sans);
4093
- background: var(--bg);
4094
- color: var(--text-primary);
4095
- display: flex;
4096
- align-items: center;
4097
- justify-content: center;
4098
- }
4099
- .login-container {
4100
- width: 100%;
4101
- max-width: 400px;
4102
- padding: 40px 32px;
4103
- background: var(--surface);
4104
- border: 1px solid var(--border);
4105
- border-radius: 12px;
4106
- }
4107
- .login-logo {
4108
- text-align: center;
4109
- font-size: 20px;
4110
- font-weight: 700;
4111
- letter-spacing: -0.5px;
4112
- margin-bottom: 8px;
4113
- }
4114
- .login-logo span { color: var(--blue); }
4115
- .login-version {
4116
- text-align: center;
4117
- font-size: 11px;
4118
- color: var(--text-secondary);
4119
- font-family: var(--mono);
4120
- margin-bottom: 32px;
4121
- }
4122
- .login-label {
4123
- display: block;
4124
- font-size: 13px;
4125
- font-weight: 600;
4126
- color: var(--text-secondary);
4127
- margin-bottom: 8px;
4128
- }
4129
- .login-input {
4130
- width: 100%;
4131
- padding: 10px 14px;
4132
- background: var(--bg);
4133
- border: 1px solid var(--border);
4134
- border-radius: var(--radius);
4135
- color: var(--text-primary);
4136
- font-family: var(--mono);
4137
- font-size: 14px;
4138
- outline: none;
4139
- transition: border-color 0.15s;
4140
- }
4141
- .login-input:focus { border-color: var(--blue); }
4142
- .login-input::placeholder { color: var(--text-secondary); opacity: 0.5; }
4143
- .login-btn {
4144
- width: 100%;
4145
- margin-top: 20px;
4146
- padding: 10px;
4147
- background: var(--blue);
4148
- color: var(--bg);
4149
- border: none;
4150
- border-radius: var(--radius);
4151
- font-size: 14px;
4152
- font-weight: 600;
4153
- cursor: pointer;
4154
- transition: opacity 0.15s;
4155
- font-family: var(--sans);
4156
- }
4157
- .login-btn:hover { opacity: 0.9; }
4158
- .login-btn:disabled { opacity: 0.5; cursor: not-allowed; }
4159
- .login-error {
4160
- margin-top: 16px;
4161
- padding: 10px 14px;
4162
- background: rgba(248, 81, 73, 0.1);
4163
- border: 1px solid var(--red);
4164
- border-radius: var(--radius);
4165
- font-size: 12px;
4166
- color: var(--red);
4167
- display: none;
4168
- }
4169
- .login-hint {
4170
- margin-top: 24px;
4171
- padding-top: 16px;
4172
- border-top: 1px solid var(--border);
4173
- font-size: 11px;
4174
- color: var(--text-secondary);
4175
- line-height: 1.5;
4176
- }
4177
- .login-hint code {
4178
- font-family: var(--mono);
4179
- background: var(--bg);
4180
- padding: 1px 4px;
4181
- border-radius: 3px;
4182
- font-size: 10px;
4183
- }
4184
- </style>
4177
+ <meta charset="UTF-8">
4178
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
4179
+ <title>Sanctuary \u2014 Principal Dashboard</title>
4180
+ <style>
4181
+ :root {
4182
+ --bg: #0d1117;
4183
+ --surface: #161b22;
4184
+ --border: #30363d;
4185
+ --text-primary: #e6edf3;
4186
+ --text-secondary: #8b949e;
4187
+ --green: #3fb950;
4188
+ --amber: #d29922;
4189
+ --red: #f85149;
4190
+ --blue: #58a6ff;
4191
+ }
4192
+
4193
+ * {
4194
+ margin: 0;
4195
+ padding: 0;
4196
+ box-sizing: border-box;
4197
+ }
4198
+
4199
+ body {
4200
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
4201
+ background-color: var(--bg);
4202
+ color: var(--text-primary);
4203
+ min-height: 100vh;
4204
+ display: flex;
4205
+ align-items: center;
4206
+ justify-content: center;
4207
+ }
4208
+
4209
+ .login-container {
4210
+ width: 100%;
4211
+ max-width: 400px;
4212
+ padding: 20px;
4213
+ }
4214
+
4215
+ .login-card {
4216
+ background-color: var(--surface);
4217
+ border: 1px solid var(--border);
4218
+ border-radius: 8px;
4219
+ padding: 40px 32px;
4220
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
4221
+ }
4222
+
4223
+ .login-header {
4224
+ display: flex;
4225
+ align-items: center;
4226
+ gap: 12px;
4227
+ margin-bottom: 32px;
4228
+ }
4229
+
4230
+ .logo {
4231
+ font-size: 24px;
4232
+ font-weight: 700;
4233
+ color: var(--blue);
4234
+ }
4235
+
4236
+ .logo-text {
4237
+ display: flex;
4238
+ flex-direction: column;
4239
+ }
4240
+
4241
+ .logo-text .title {
4242
+ font-size: 18px;
4243
+ font-weight: 600;
4244
+ letter-spacing: -0.5px;
4245
+ }
4246
+
4247
+ .logo-text .version {
4248
+ font-size: 12px;
4249
+ color: var(--text-secondary);
4250
+ margin-top: 2px;
4251
+ }
4252
+
4253
+ .form-group {
4254
+ margin-bottom: 24px;
4255
+ }
4256
+
4257
+ label {
4258
+ display: block;
4259
+ font-size: 14px;
4260
+ font-weight: 500;
4261
+ margin-bottom: 8px;
4262
+ color: var(--text-primary);
4263
+ }
4264
+
4265
+ input[type="text"],
4266
+ input[type="password"] {
4267
+ width: 100%;
4268
+ padding: 10px 12px;
4269
+ background-color: var(--bg);
4270
+ border: 1px solid var(--border);
4271
+ border-radius: 6px;
4272
+ color: var(--text-primary);
4273
+ font-size: 14px;
4274
+ font-family: 'JetBrains Mono', monospace;
4275
+ transition: border-color 0.2s;
4276
+ }
4277
+
4278
+ input[type="text"]:focus,
4279
+ input[type="password"]:focus {
4280
+ outline: none;
4281
+ border-color: var(--blue);
4282
+ box-shadow: 0 0 0 2px rgba(88, 166, 255, 0.1);
4283
+ }
4284
+
4285
+ .error-message {
4286
+ display: none;
4287
+ background-color: rgba(248, 81, 73, 0.1);
4288
+ border: 1px solid var(--red);
4289
+ color: #ff9999;
4290
+ padding: 12px;
4291
+ border-radius: 6px;
4292
+ font-size: 13px;
4293
+ margin-bottom: 20px;
4294
+ }
4295
+
4296
+ .error-message.show {
4297
+ display: block;
4298
+ }
4299
+
4300
+ button {
4301
+ width: 100%;
4302
+ padding: 10px 16px;
4303
+ background-color: var(--blue);
4304
+ color: var(--bg);
4305
+ border: none;
4306
+ border-radius: 6px;
4307
+ font-size: 14px;
4308
+ font-weight: 600;
4309
+ cursor: pointer;
4310
+ transition: background-color 0.2s;
4311
+ }
4312
+
4313
+ button:hover {
4314
+ background-color: #79c0ff;
4315
+ }
4316
+
4317
+ button:active {
4318
+ background-color: #4184e4;
4319
+ }
4320
+
4321
+ button:disabled {
4322
+ background-color: var(--text-secondary);
4323
+ cursor: not-allowed;
4324
+ opacity: 0.5;
4325
+ }
4326
+
4327
+ .info-text {
4328
+ font-size: 12px;
4329
+ color: var(--text-secondary);
4330
+ margin-top: 16px;
4331
+ text-align: center;
4332
+ }
4333
+ </style>
4185
4334
  </head>
4186
4335
  <body>
4187
- <div class="login-container">
4188
- <div class="login-logo"><span>&#9670;</span> SANCTUARY</div>
4189
- <div class="login-version">Principal Dashboard v${options.serverVersion}</div>
4190
- <form id="loginForm" onsubmit="return handleLogin(event)">
4191
- <label class="login-label" for="tokenInput">Dashboard Auth Token</label>
4192
- <input class="login-input" type="password" id="tokenInput"
4193
- placeholder="Enter your auth token" autocomplete="off" autofocus required>
4194
- <button class="login-btn" type="submit" id="loginBtn">Open Dashboard</button>
4195
- </form>
4196
- <div class="login-error" id="loginError"></div>
4197
- <div class="login-hint">
4198
- Your token is set via <code>SANCTUARY_DASHBOARD_AUTH_TOKEN</code> environment variable,
4199
- or check your server's startup output.
4336
+ <div class="login-container">
4337
+ <div class="login-card">
4338
+ <div class="login-header">
4339
+ <div class="logo">\u25C6</div>
4340
+ <div class="logo-text">
4341
+ <div class="title">SANCTUARY</div>
4342
+ <div class="version">v${options.serverVersion}</div>
4343
+ </div>
4344
+ </div>
4345
+
4346
+ <div id="error-message" class="error-message"></div>
4347
+
4348
+ <form id="login-form">
4349
+ <div class="form-group">
4350
+ <label for="auth-token">Auth Token</label>
4351
+ <input
4352
+ type="text"
4353
+ id="auth-token"
4354
+ name="token"
4355
+ placeholder="Paste your session token..."
4356
+ autocomplete="off"
4357
+ spellcheck="false"
4358
+ required
4359
+ />
4360
+ </div>
4361
+
4362
+ <button type="submit" id="login-button">Open Dashboard</button>
4363
+ </form>
4364
+
4365
+ <div class="info-text">
4366
+ Session tokens expire after 1 hour of inactivity
4367
+ </div>
4368
+ </div>
4200
4369
  </div>
4201
- </div>
4202
- <script>
4203
- async function handleLogin(e) {
4204
- e.preventDefault();
4205
- var btn = document.getElementById('loginBtn');
4206
- var errEl = document.getElementById('loginError');
4207
- var token = document.getElementById('tokenInput').value.trim();
4208
- if (!token) return false;
4209
- btn.disabled = true;
4210
- btn.textContent = 'Authenticating...';
4211
- errEl.style.display = 'none';
4212
- try {
4213
- var resp = await fetch('/auth/session', {
4214
- method: 'POST',
4215
- headers: { 'Authorization': 'Bearer ' + token }
4370
+
4371
+ <script>
4372
+ const loginForm = document.getElementById('login-form');
4373
+ const authTokenInput = document.getElementById('auth-token');
4374
+ const errorMessage = document.getElementById('error-message');
4375
+ const loginButton = document.getElementById('login-button');
4376
+
4377
+ loginForm.addEventListener('submit', async (e) => {
4378
+ e.preventDefault();
4379
+ const token = authTokenInput.value.trim();
4380
+
4381
+ if (!token) {
4382
+ showError('Token is required');
4383
+ return;
4384
+ }
4385
+
4386
+ loginButton.disabled = true;
4387
+ loginButton.textContent = 'Verifying...';
4388
+ errorMessage.classList.remove('show');
4389
+
4390
+ try {
4391
+ const response = await fetch('/auth/session', {
4392
+ method: 'POST',
4393
+ headers: {
4394
+ 'Content-Type': 'application/json',
4395
+ 'Authorization': 'Bearer ' + token,
4396
+ },
4397
+ body: JSON.stringify({ token }),
4398
+ });
4399
+
4400
+ if (response.ok) {
4401
+ const data = await response.json();
4402
+ sessionStorage.setItem('authToken', token);
4403
+ window.location.href = '/dashboard';
4404
+ } else if (response.status === 401) {
4405
+ showError('Invalid token. Please check and try again.');
4406
+ } else {
4407
+ showError('Authentication failed. Please try again.');
4408
+ }
4409
+ } catch (err) {
4410
+ showError('Connection error. Please check your network.');
4411
+ } finally {
4412
+ loginButton.disabled = false;
4413
+ loginButton.textContent = 'Open Dashboard';
4414
+ }
4216
4415
  });
4217
- if (!resp.ok) {
4218
- var data = await resp.json().catch(function() { return {}; });
4219
- throw new Error(data.error || 'Authentication failed');
4220
- }
4221
- var result = await resp.json();
4222
- // Store token in sessionStorage for auto-renewal inside the dashboard
4223
- try { sessionStorage.setItem('sanctuary_token', token); } catch(_) {}
4224
- // Set session cookie
4225
- var maxAge = result.expires_in_seconds || 300;
4226
- document.cookie = 'sanctuary_session=' + result.session_id +
4227
- '; path=/; SameSite=Strict; max-age=' + maxAge;
4228
- // Reload to enter the dashboard
4229
- window.location.reload();
4230
- } catch (err) {
4231
- errEl.textContent = err.message || 'Authentication failed. Check your token.';
4232
- errEl.style.display = 'block';
4233
- btn.disabled = false;
4234
- btn.textContent = 'Open Dashboard';
4235
- }
4236
- return false;
4237
- }
4238
- </script>
4416
+
4417
+ function showError(message) {
4418
+ errorMessage.textContent = message;
4419
+ errorMessage.classList.add('show');
4420
+ }
4421
+
4422
+ authTokenInput.addEventListener('input', () => {
4423
+ errorMessage.classList.remove('show');
4424
+ });
4425
+ </script>
4239
4426
  </body>
4240
4427
  </html>`;
4241
4428
  }
@@ -4243,1412 +4430,1648 @@ function generateDashboardHTML(options) {
4243
4430
  return `<!DOCTYPE html>
4244
4431
  <html lang="en">
4245
4432
  <head>
4246
- <meta charset="utf-8">
4247
- <meta name="viewport" content="width=device-width, initial-scale=1">
4248
- <title>Sanctuary \u2014 Principal Dashboard</title>
4249
- <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
4250
- <style>
4251
- :root {
4252
- --bg: #0d1117;
4253
- --surface: #161b22;
4254
- --border: #30363d;
4255
- --text-primary: #e6edf3;
4256
- --text-secondary: #8b949e;
4257
- --green: #3fb950;
4258
- --amber: #d29922;
4259
- --red: #f85149;
4260
- --blue: #58a6ff;
4261
- --mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
4262
- --sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
4263
- --radius: 6px;
4264
- }
4265
-
4266
- * {
4267
- box-sizing: border-box;
4268
- margin: 0;
4269
- padding: 0;
4270
- }
4271
-
4272
- html, body {
4273
- width: 100%;
4274
- height: 100%;
4275
- overflow: hidden;
4276
- }
4277
-
4278
- body {
4279
- font-family: var(--sans);
4280
- background: var(--bg);
4281
- color: var(--text-primary);
4282
- display: flex;
4283
- flex-direction: column;
4284
- }
4285
-
4286
- /* \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 */
4287
-
4288
- .status-bar {
4289
- position: fixed;
4290
- top: 0;
4291
- left: 0;
4292
- right: 0;
4293
- height: 56px;
4294
- background: var(--surface);
4295
- border-bottom: 1px solid var(--border);
4296
- display: flex;
4297
- align-items: center;
4298
- padding: 0 20px;
4299
- gap: 24px;
4300
- z-index: 1000;
4301
- }
4433
+ <meta charset="UTF-8">
4434
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
4435
+ <title>Sanctuary \u2014 Principal Dashboard</title>
4436
+ <style>
4437
+ :root {
4438
+ --bg: #0d1117;
4439
+ --surface: #161b22;
4440
+ --border: #30363d;
4441
+ --text-primary: #e6edf3;
4442
+ --text-secondary: #8b949e;
4443
+ --green: #3fb950;
4444
+ --amber: #d29922;
4445
+ --red: #f85149;
4446
+ --blue: #58a6ff;
4447
+ --success: #3fb950;
4448
+ --warning: #d29922;
4449
+ --error: #f85149;
4450
+ --muted: #21262d;
4451
+ }
4452
+
4453
+ * {
4454
+ margin: 0;
4455
+ padding: 0;
4456
+ box-sizing: border-box;
4457
+ }
4458
+
4459
+ html, body {
4460
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
4461
+ background-color: var(--bg);
4462
+ color: var(--text-primary);
4463
+ height: 100%;
4464
+ overflow: hidden;
4465
+ }
4466
+
4467
+ body {
4468
+ display: flex;
4469
+ flex-direction: column;
4470
+ }
4471
+
4472
+ /* Status Bar */
4473
+ .status-bar {
4474
+ position: fixed;
4475
+ top: 0;
4476
+ left: 0;
4477
+ right: 0;
4478
+ height: 56px;
4479
+ background-color: var(--surface);
4480
+ border-bottom: 1px solid var(--border);
4481
+ display: flex;
4482
+ align-items: center;
4483
+ padding: 0 24px;
4484
+ gap: 24px;
4485
+ z-index: 100;
4486
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
4487
+ }
4488
+
4489
+ .status-bar-left {
4490
+ display: flex;
4491
+ align-items: center;
4492
+ gap: 12px;
4493
+ flex: 0 0 auto;
4494
+ }
4302
4495
 
4303
- .status-bar-left {
4304
- display: flex;
4305
- align-items: center;
4306
- gap: 12px;
4307
- flex: 0 0 auto;
4308
- }
4496
+ .logo-icon {
4497
+ font-size: 20px;
4498
+ color: var(--blue);
4499
+ font-weight: 700;
4500
+ }
4309
4501
 
4310
- .sanctuary-logo {
4311
- font-weight: 700;
4312
- font-size: 16px;
4313
- letter-spacing: -0.5px;
4314
- color: var(--text-primary);
4315
- }
4502
+ .logo-info {
4503
+ display: flex;
4504
+ flex-direction: column;
4505
+ }
4316
4506
 
4317
- .sanctuary-logo span {
4318
- color: var(--blue);
4319
- }
4507
+ .logo-title {
4508
+ font-size: 13px;
4509
+ font-weight: 600;
4510
+ line-height: 1;
4511
+ color: var(--text-primary);
4512
+ }
4320
4513
 
4321
- .version {
4322
- font-size: 11px;
4323
- color: var(--text-secondary);
4324
- font-family: var(--mono);
4325
- }
4514
+ .logo-version {
4515
+ font-size: 11px;
4516
+ color: var(--text-secondary);
4517
+ margin-top: 2px;
4518
+ }
4326
4519
 
4327
- .status-bar-center {
4328
- flex: 1;
4329
- display: flex;
4330
- align-items: center;
4331
- justify-content: center;
4332
- }
4333
-
4334
- .sovereignty-badge {
4335
- display: flex;
4336
- align-items: center;
4337
- gap: 8px;
4338
- padding: 6px 12px;
4339
- background: rgba(88, 166, 255, 0.1);
4340
- border: 1px solid var(--blue);
4341
- border-radius: 20px;
4342
- font-size: 13px;
4343
- font-weight: 600;
4344
- }
4520
+ .status-bar-center {
4521
+ flex: 1;
4522
+ display: flex;
4523
+ justify-content: center;
4524
+ }
4345
4525
 
4346
- .sovereignty-score {
4347
- display: flex;
4348
- align-items: center;
4349
- justify-content: center;
4350
- width: 28px;
4351
- height: 28px;
4352
- border-radius: 50%;
4353
- font-family: var(--mono);
4354
- font-weight: 700;
4355
- font-size: 12px;
4356
- background: var(--blue);
4357
- color: var(--bg);
4358
- }
4526
+ .sovereignty-badge {
4527
+ display: flex;
4528
+ align-items: center;
4529
+ gap: 8px;
4530
+ padding: 8px 16px;
4531
+ background-color: rgba(63, 185, 80, 0.1);
4532
+ border: 1px solid rgba(63, 185, 80, 0.3);
4533
+ border-radius: 6px;
4534
+ font-size: 13px;
4535
+ font-weight: 500;
4536
+ }
4359
4537
 
4360
- .sovereignty-score.high {
4361
- background: var(--green);
4362
- }
4538
+ .sovereignty-badge.degraded {
4539
+ background-color: rgba(210, 153, 34, 0.1);
4540
+ border-color: rgba(210, 153, 34, 0.3);
4541
+ }
4363
4542
 
4364
- .sovereignty-score.medium {
4365
- background: var(--amber);
4366
- }
4543
+ .sovereignty-badge.inactive {
4544
+ background-color: rgba(248, 81, 73, 0.1);
4545
+ border-color: rgba(248, 81, 73, 0.3);
4546
+ }
4367
4547
 
4368
- .sovereignty-score.low {
4369
- background: var(--red);
4370
- }
4548
+ .sovereignty-score {
4549
+ font-weight: 700;
4550
+ color: var(--green);
4551
+ }
4371
4552
 
4372
- .status-bar-right {
4373
- display: flex;
4374
- align-items: center;
4375
- gap: 16px;
4376
- flex: 0 0 auto;
4377
- }
4553
+ .sovereignty-badge.degraded .sovereignty-score {
4554
+ color: var(--amber);
4555
+ }
4378
4556
 
4379
- .protections-indicator {
4380
- display: flex;
4381
- align-items: center;
4382
- gap: 6px;
4383
- font-size: 12px;
4384
- color: var(--text-secondary);
4385
- font-family: var(--mono);
4386
- }
4557
+ .sovereignty-badge.inactive .sovereignty-score {
4558
+ color: var(--red);
4559
+ }
4387
4560
 
4388
- .protections-indicator .count {
4389
- color: var(--text-primary);
4390
- font-weight: 600;
4391
- }
4561
+ .status-bar-right {
4562
+ display: flex;
4563
+ align-items: center;
4564
+ gap: 16px;
4565
+ flex: 0 0 auto;
4566
+ }
4392
4567
 
4393
- .uptime {
4394
- display: flex;
4395
- align-items: center;
4396
- gap: 6px;
4397
- font-size: 12px;
4398
- color: var(--text-secondary);
4399
- font-family: var(--mono);
4400
- }
4568
+ .status-item {
4569
+ display: flex;
4570
+ align-items: center;
4571
+ gap: 6px;
4572
+ font-size: 12px;
4573
+ color: var(--text-secondary);
4574
+ }
4401
4575
 
4402
- .status-dot {
4403
- width: 8px;
4404
- height: 8px;
4405
- border-radius: 50%;
4406
- background: var(--green);
4407
- animation: pulse 2s ease-in-out infinite;
4408
- }
4576
+ .status-item strong {
4577
+ color: var(--text-primary);
4578
+ font-weight: 500;
4579
+ }
4409
4580
 
4410
- .status-dot.disconnected {
4411
- background: var(--red);
4412
- animation: none;
4413
- }
4581
+ .status-dot {
4582
+ width: 8px;
4583
+ height: 8px;
4584
+ border-radius: 50%;
4585
+ background-color: var(--green);
4586
+ }
4414
4587
 
4415
- @keyframes pulse {
4416
- 0%, 100% { opacity: 1; }
4417
- 50% { opacity: 0.5; }
4418
- }
4588
+ .status-dot.disconnected {
4589
+ background-color: var(--red);
4590
+ }
4419
4591
 
4420
- .pending-badge {
4421
- display: inline-flex;
4422
- align-items: center;
4423
- justify-content: center;
4424
- min-width: 24px;
4425
- height: 24px;
4426
- padding: 0 6px;
4427
- background: var(--red);
4428
- color: white;
4429
- border-radius: 12px;
4430
- font-size: 11px;
4431
- font-weight: 700;
4432
- animation: pulse 1s ease-in-out infinite;
4433
- }
4592
+ .pending-badge {
4593
+ display: flex;
4594
+ align-items: center;
4595
+ gap: 6px;
4596
+ padding: 4px 8px;
4597
+ background-color: var(--blue);
4598
+ color: var(--bg);
4599
+ border-radius: 4px;
4600
+ font-size: 11px;
4601
+ font-weight: 600;
4602
+ cursor: pointer;
4603
+ }
4434
4604
 
4435
- .pending-badge.hidden {
4436
- display: none;
4437
- }
4605
+ /* Main Content */
4606
+ .main-content {
4607
+ flex: 1;
4608
+ margin-top: 56px;
4609
+ overflow-y: auto;
4610
+ padding: 24px;
4611
+ }
4438
4612
 
4439
- /* \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 */
4613
+ .grid {
4614
+ display: grid;
4615
+ gap: 20px;
4616
+ }
4440
4617
 
4441
- .main-container {
4442
- flex: 1;
4443
- display: flex;
4444
- margin-top: 56px;
4445
- overflow: hidden;
4446
- }
4618
+ /* Row 1: Sovereignty Layers */
4619
+ .sovereignty-layers {
4620
+ display: grid;
4621
+ grid-template-columns: repeat(4, 1fr);
4622
+ gap: 16px;
4623
+ }
4447
4624
 
4448
- .activity-feed {
4449
- flex: 3;
4450
- display: flex;
4451
- flex-direction: column;
4452
- border-right: 1px solid var(--border);
4453
- overflow: hidden;
4454
- }
4625
+ .layer-card {
4626
+ background-color: var(--surface);
4627
+ border: 1px solid var(--border);
4628
+ border-radius: 8px;
4629
+ padding: 20px;
4630
+ display: flex;
4631
+ flex-direction: column;
4632
+ gap: 12px;
4633
+ }
4455
4634
 
4456
- .feed-header {
4457
- padding: 16px 20px;
4458
- border-bottom: 1px solid var(--border);
4459
- display: flex;
4460
- align-items: center;
4461
- gap: 8px;
4462
- font-size: 12px;
4463
- font-weight: 600;
4464
- text-transform: uppercase;
4465
- letter-spacing: 0.5px;
4466
- color: var(--text-secondary);
4467
- }
4635
+ .layer-card.degraded {
4636
+ border-color: var(--amber);
4637
+ background-color: rgba(210, 153, 34, 0.05);
4638
+ }
4468
4639
 
4469
- .feed-header-dot {
4470
- width: 6px;
4471
- height: 6px;
4472
- border-radius: 50%;
4473
- background: var(--green);
4474
- }
4640
+ .layer-card.inactive {
4641
+ border-color: var(--red);
4642
+ background-color: rgba(248, 81, 73, 0.05);
4643
+ }
4475
4644
 
4476
- .activity-list {
4477
- flex: 1;
4478
- overflow-y: auto;
4479
- overflow-x: hidden;
4480
- }
4645
+ .layer-name {
4646
+ font-size: 12px;
4647
+ font-weight: 600;
4648
+ color: var(--text-secondary);
4649
+ text-transform: uppercase;
4650
+ letter-spacing: 0.5px;
4651
+ }
4481
4652
 
4482
- .activity-item {
4483
- padding: 12px 20px;
4484
- border-bottom: 1px solid rgba(48, 54, 61, 0.5);
4485
- font-size: 13px;
4486
- font-family: var(--mono);
4487
- cursor: pointer;
4488
- transition: background 0.15s;
4489
- display: flex;
4490
- align-items: flex-start;
4491
- gap: 10px;
4492
- }
4653
+ .layer-title {
4654
+ font-size: 14px;
4655
+ font-weight: 600;
4656
+ color: var(--text-primary);
4657
+ }
4493
4658
 
4494
- .activity-item:hover {
4495
- background: rgba(88, 166, 255, 0.05);
4496
- }
4659
+ .layer-status {
4660
+ display: inline-flex;
4661
+ align-items: center;
4662
+ gap: 6px;
4663
+ padding: 4px 8px;
4664
+ background-color: rgba(63, 185, 80, 0.15);
4665
+ color: var(--green);
4666
+ border-radius: 4px;
4667
+ font-size: 11px;
4668
+ font-weight: 600;
4669
+ width: fit-content;
4670
+ }
4497
4671
 
4498
- .activity-item-icon {
4499
- flex: 0 0 auto;
4500
- width: 16px;
4501
- text-align: center;
4502
- font-size: 12px;
4503
- color: var(--text-secondary);
4504
- margin-top: 1px;
4505
- }
4672
+ .layer-card.degraded .layer-status {
4673
+ background-color: rgba(210, 153, 34, 0.15);
4674
+ color: var(--amber);
4675
+ }
4506
4676
 
4507
- .activity-item-content {
4508
- flex: 1;
4509
- min-width: 0;
4510
- }
4677
+ .layer-card.inactive .layer-status {
4678
+ background-color: rgba(248, 81, 73, 0.15);
4679
+ color: var(--red);
4680
+ }
4511
4681
 
4512
- .activity-time {
4513
- color: var(--text-secondary);
4514
- font-size: 11px;
4515
- margin-bottom: 2px;
4516
- }
4682
+ .layer-detail {
4683
+ font-size: 12px;
4684
+ color: var(--text-secondary);
4685
+ font-family: 'JetBrains Mono', monospace;
4686
+ padding: 8px;
4687
+ background-color: var(--bg);
4688
+ border-radius: 4px;
4689
+ border-left: 2px solid var(--blue);
4690
+ }
4517
4691
 
4518
- .activity-main {
4519
- display: flex;
4520
- gap: 8px;
4521
- align-items: baseline;
4522
- margin-bottom: 4px;
4523
- }
4692
+ /* Row 2: Info Cards */
4693
+ .info-cards {
4694
+ display: grid;
4695
+ grid-template-columns: repeat(3, 1fr);
4696
+ gap: 16px;
4697
+ }
4524
4698
 
4525
- .activity-tier {
4526
- display: inline-flex;
4527
- align-items: center;
4528
- justify-content: center;
4529
- width: 24px;
4530
- height: 16px;
4531
- font-size: 10px;
4532
- font-weight: 700;
4533
- border-radius: 3px;
4534
- text-transform: uppercase;
4535
- flex: 0 0 auto;
4536
- }
4699
+ .info-card {
4700
+ background-color: var(--surface);
4701
+ border: 1px solid var(--border);
4702
+ border-radius: 8px;
4703
+ padding: 20px;
4704
+ }
4537
4705
 
4538
- .activity-tier.t1 {
4539
- background: rgba(248, 81, 73, 0.2);
4540
- color: var(--red);
4541
- }
4706
+ .card-header {
4707
+ font-size: 12px;
4708
+ font-weight: 600;
4709
+ color: var(--text-secondary);
4710
+ text-transform: uppercase;
4711
+ letter-spacing: 0.5px;
4712
+ margin-bottom: 16px;
4713
+ }
4542
4714
 
4543
- .activity-tier.t2 {
4544
- background: rgba(210, 153, 34, 0.2);
4545
- color: var(--amber);
4546
- }
4715
+ .card-row {
4716
+ display: flex;
4717
+ justify-content: space-between;
4718
+ align-items: center;
4719
+ margin-bottom: 12px;
4720
+ font-size: 13px;
4721
+ }
4547
4722
 
4548
- .activity-tier.t3 {
4549
- background: rgba(63, 185, 80, 0.2);
4550
- color: var(--green);
4551
- }
4723
+ .card-row:last-child {
4724
+ margin-bottom: 0;
4725
+ }
4552
4726
 
4553
- .activity-tool {
4554
- color: var(--text-primary);
4555
- font-weight: 600;
4556
- }
4727
+ .card-label {
4728
+ color: var(--text-secondary);
4729
+ }
4557
4730
 
4558
- .activity-outcome {
4559
- color: var(--green);
4560
- }
4731
+ .card-value {
4732
+ color: var(--text-primary);
4733
+ font-family: 'JetBrains Mono', monospace;
4734
+ font-weight: 500;
4735
+ }
4561
4736
 
4562
- .activity-outcome.denied {
4563
- color: var(--red);
4564
- }
4737
+ .identity-badge {
4738
+ display: inline-flex;
4739
+ align-items: center;
4740
+ gap: 4px;
4741
+ padding: 2px 6px;
4742
+ background-color: rgba(88, 166, 255, 0.15);
4743
+ color: var(--blue);
4744
+ border-radius: 3px;
4745
+ font-size: 10px;
4746
+ font-weight: 600;
4747
+ text-transform: uppercase;
4748
+ }
4565
4749
 
4566
- .activity-detail {
4567
- font-size: 12px;
4568
- color: var(--text-secondary);
4569
- margin-left: 0;
4570
- }
4750
+ .trust-tier-badge {
4751
+ display: inline-flex;
4752
+ align-items: center;
4753
+ gap: 4px;
4754
+ padding: 2px 6px;
4755
+ background-color: rgba(63, 185, 80, 0.15);
4756
+ color: var(--green);
4757
+ border-radius: 3px;
4758
+ font-size: 10px;
4759
+ font-weight: 600;
4760
+ }
4571
4761
 
4572
- .activity-item.expanded .activity-detail {
4573
- display: block;
4574
- margin-top: 8px;
4575
- padding: 10px;
4576
- background: rgba(88, 166, 255, 0.08);
4577
- border-left: 2px solid var(--blue);
4578
- border-radius: 4px;
4579
- }
4762
+ .truncated {
4763
+ max-width: 200px;
4764
+ overflow: hidden;
4765
+ text-overflow: ellipsis;
4766
+ white-space: nowrap;
4767
+ }
4580
4768
 
4581
- .activity-empty {
4582
- display: flex;
4583
- flex-direction: column;
4584
- align-items: center;
4585
- justify-content: center;
4586
- height: 100%;
4587
- color: var(--text-secondary);
4588
- }
4769
+ /* Row 3: SHR & Activity */
4770
+ .main-panels {
4771
+ display: grid;
4772
+ grid-template-columns: 1fr 1fr;
4773
+ gap: 16px;
4774
+ min-height: 400px;
4775
+ }
4589
4776
 
4590
- .activity-empty-icon {
4591
- font-size: 32px;
4592
- margin-bottom: 12px;
4593
- }
4777
+ .panel {
4778
+ background-color: var(--surface);
4779
+ border: 1px solid var(--border);
4780
+ border-radius: 8px;
4781
+ display: flex;
4782
+ flex-direction: column;
4783
+ overflow: hidden;
4784
+ }
4594
4785
 
4595
- .activity-empty-text {
4596
- font-size: 14px;
4597
- }
4786
+ .panel-header {
4787
+ padding: 16px 20px;
4788
+ border-bottom: 1px solid var(--border);
4789
+ display: flex;
4790
+ justify-content: space-between;
4791
+ align-items: center;
4792
+ }
4598
4793
 
4599
- /* \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 */
4794
+ .panel-title {
4795
+ font-size: 14px;
4796
+ font-weight: 600;
4797
+ color: var(--text-primary);
4798
+ }
4600
4799
 
4601
- .protection-sidebar {
4602
- flex: 2;
4603
- display: flex;
4604
- flex-direction: column;
4605
- background: rgba(22, 27, 34, 0.5);
4606
- overflow: hidden;
4607
- }
4800
+ .panel-action {
4801
+ background: none;
4802
+ border: none;
4803
+ color: var(--blue);
4804
+ cursor: pointer;
4805
+ font-size: 12px;
4806
+ padding: 0;
4807
+ font-weight: 500;
4808
+ transition: color 0.2s;
4809
+ }
4608
4810
 
4609
- .sidebar-header {
4610
- padding: 16px 20px;
4611
- border-bottom: 1px solid var(--border);
4612
- font-size: 12px;
4613
- font-weight: 600;
4614
- text-transform: uppercase;
4615
- letter-spacing: 0.5px;
4616
- color: var(--text-secondary);
4617
- display: flex;
4618
- align-items: center;
4619
- gap: 8px;
4620
- }
4811
+ .panel-action:hover {
4812
+ color: #79c0ff;
4813
+ }
4621
4814
 
4622
- .sidebar-content {
4623
- flex: 1;
4624
- overflow-y: auto;
4625
- padding: 16px 16px;
4626
- display: grid;
4627
- grid-template-columns: 1fr 1fr;
4628
- gap: 12px;
4629
- }
4815
+ .panel-content {
4816
+ flex: 1;
4817
+ overflow-y: auto;
4818
+ padding: 20px;
4819
+ }
4630
4820
 
4631
- .protection-card {
4632
- background: var(--surface);
4633
- border: 1px solid var(--border);
4634
- border-radius: var(--radius);
4635
- padding: 14px;
4636
- display: flex;
4637
- flex-direction: column;
4638
- gap: 8px;
4639
- }
4821
+ /* SHR Viewer */
4822
+ .shr-json {
4823
+ font-family: 'JetBrains Mono', monospace;
4824
+ font-size: 12px;
4825
+ line-height: 1.6;
4826
+ color: var(--text-secondary);
4827
+ }
4640
4828
 
4641
- .protection-card-icon {
4642
- font-size: 14px;
4643
- }
4829
+ .shr-section {
4830
+ margin-bottom: 12px;
4831
+ }
4644
4832
 
4645
- .protection-card-label {
4646
- font-size: 11px;
4647
- font-weight: 600;
4648
- text-transform: uppercase;
4649
- letter-spacing: 0.5px;
4650
- color: var(--text-secondary);
4651
- }
4833
+ .shr-section-header {
4834
+ display: flex;
4835
+ align-items: center;
4836
+ gap: 8px;
4837
+ cursor: pointer;
4838
+ font-weight: 600;
4839
+ color: var(--text-primary);
4840
+ padding: 8px;
4841
+ background-color: var(--bg);
4842
+ border-radius: 4px;
4843
+ user-select: none;
4844
+ }
4652
4845
 
4653
- .protection-card-status {
4654
- display: flex;
4655
- align-items: center;
4656
- gap: 6px;
4657
- font-size: 12px;
4658
- font-weight: 600;
4659
- }
4846
+ .shr-section-header:hover {
4847
+ background-color: var(--muted);
4848
+ }
4660
4849
 
4661
- .protection-card-status.active {
4662
- color: var(--green);
4663
- }
4850
+ .shr-toggle {
4851
+ width: 16px;
4852
+ height: 16px;
4853
+ display: flex;
4854
+ align-items: center;
4855
+ justify-content: center;
4856
+ font-size: 10px;
4857
+ transition: transform 0.2s;
4858
+ }
4664
4859
 
4665
- .protection-card-status.inactive {
4666
- color: var(--text-secondary);
4667
- }
4860
+ .shr-section.collapsed .shr-toggle {
4861
+ transform: rotate(-90deg);
4862
+ }
4668
4863
 
4669
- .protection-card-stat {
4670
- font-size: 11px;
4671
- color: var(--text-secondary);
4672
- font-family: var(--mono);
4673
- margin-top: 4px;
4674
- }
4864
+ .shr-section-content {
4865
+ padding: 8px 16px;
4866
+ background-color: rgba(0, 0, 0, 0.2);
4867
+ border-radius: 4px;
4868
+ margin-top: 4px;
4869
+ }
4675
4870
 
4676
- /* \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 */
4871
+ .shr-section.collapsed .shr-section-content {
4872
+ display: none;
4873
+ }
4677
4874
 
4678
- .pending-overlay {
4679
- position: fixed;
4680
- top: 56px;
4681
- right: 0;
4682
- bottom: 0;
4683
- width: 0;
4684
- background: var(--surface);
4685
- border-left: 1px solid var(--border);
4686
- z-index: 999;
4687
- overflow-y: auto;
4688
- transition: width 0.3s ease-out;
4689
- display: flex;
4690
- flex-direction: column;
4691
- }
4875
+ .shr-item {
4876
+ display: flex;
4877
+ margin-bottom: 4px;
4878
+ }
4692
4879
 
4693
- .pending-overlay.active {
4694
- width: 380px;
4695
- }
4880
+ .shr-key {
4881
+ color: var(--blue);
4882
+ margin-right: 8px;
4883
+ min-width: 120px;
4884
+ }
4696
4885
 
4697
- @media (max-width: 1400px) {
4698
- .pending-overlay.active {
4699
- width: 100%;
4700
- right: auto;
4701
- left: 0;
4886
+ .shr-value {
4887
+ color: var(--green);
4888
+ word-break: break-all;
4702
4889
  }
4703
- }
4704
4890
 
4705
- .pending-overlay-header {
4706
- padding: 16px 20px;
4707
- border-bottom: 1px solid var(--border);
4708
- display: flex;
4709
- align-items: center;
4710
- justify-content: space-between;
4711
- flex: 0 0 auto;
4712
- }
4891
+ /* Activity Feed */
4892
+ .activity-feed {
4893
+ display: flex;
4894
+ flex-direction: column;
4895
+ gap: 12px;
4896
+ }
4713
4897
 
4714
- .pending-overlay-title {
4715
- font-size: 13px;
4716
- font-weight: 600;
4717
- text-transform: uppercase;
4718
- letter-spacing: 0.5px;
4719
- color: var(--text-primary);
4720
- }
4898
+ .activity-item {
4899
+ padding: 12px;
4900
+ background-color: var(--bg);
4901
+ border-left: 2px solid var(--border);
4902
+ border-radius: 4px;
4903
+ font-size: 12px;
4904
+ }
4721
4905
 
4722
- .pending-overlay-close {
4723
- background: none;
4724
- border: none;
4725
- color: var(--text-secondary);
4726
- cursor: pointer;
4727
- font-size: 18px;
4728
- padding: 0;
4729
- display: flex;
4730
- align-items: center;
4731
- justify-content: center;
4732
- }
4906
+ .activity-item.tool-call {
4907
+ border-left-color: var(--blue);
4908
+ }
4733
4909
 
4734
- .pending-overlay-close:hover {
4735
- color: var(--text-primary);
4736
- }
4910
+ .activity-item.context-gate {
4911
+ border-left-color: var(--amber);
4912
+ }
4737
4913
 
4738
- .pending-list {
4739
- flex: 1;
4740
- overflow-y: auto;
4741
- }
4914
+ .activity-item.injection {
4915
+ border-left-color: var(--red);
4916
+ }
4742
4917
 
4743
- .pending-item {
4744
- padding: 16px 20px;
4745
- border-bottom: 1px solid rgba(48, 54, 61, 0.5);
4746
- display: flex;
4747
- flex-direction: column;
4748
- gap: 10px;
4749
- }
4918
+ .activity-item.protection {
4919
+ border-left-color: var(--green);
4920
+ }
4750
4921
 
4751
- .pending-item-header {
4752
- display: flex;
4753
- align-items: center;
4754
- gap: 8px;
4755
- }
4922
+ .activity-type {
4923
+ font-weight: 600;
4924
+ color: var(--text-primary);
4925
+ margin-bottom: 4px;
4926
+ text-transform: uppercase;
4927
+ font-size: 11px;
4928
+ letter-spacing: 0.5px;
4929
+ }
4756
4930
 
4757
- .pending-item-op {
4758
- font-family: var(--mono);
4759
- font-size: 12px;
4760
- font-weight: 600;
4761
- color: var(--text-primary);
4762
- flex: 1;
4763
- }
4931
+ .activity-content {
4932
+ color: var(--text-secondary);
4933
+ font-family: 'JetBrains Mono', monospace;
4934
+ margin-bottom: 4px;
4935
+ word-break: break-all;
4936
+ }
4764
4937
 
4765
- .pending-item-tier {
4766
- display: inline-flex;
4767
- align-items: center;
4768
- justify-content: center;
4769
- width: 28px;
4770
- height: 20px;
4771
- font-size: 9px;
4772
- font-weight: 700;
4773
- border-radius: 3px;
4774
- text-transform: uppercase;
4775
- color: white;
4776
- }
4938
+ .activity-time {
4939
+ font-size: 11px;
4940
+ color: var(--text-secondary);
4941
+ }
4777
4942
 
4778
- .pending-item-tier.tier1 {
4779
- background: var(--red);
4780
- }
4943
+ .empty-state {
4944
+ display: flex;
4945
+ align-items: center;
4946
+ justify-content: center;
4947
+ height: 100%;
4948
+ color: var(--text-secondary);
4949
+ font-size: 13px;
4950
+ }
4781
4951
 
4782
- .pending-item-tier.tier2 {
4783
- background: var(--amber);
4784
- }
4952
+ /* Row 4: Handshake History */
4953
+ .handshake-table {
4954
+ background-color: var(--surface);
4955
+ border: 1px solid var(--border);
4956
+ border-radius: 8px;
4957
+ overflow: hidden;
4958
+ }
4785
4959
 
4786
- .pending-item-reason {
4787
- font-size: 12px;
4788
- color: var(--text-secondary);
4789
- }
4960
+ .table-header {
4961
+ display: grid;
4962
+ grid-template-columns: 2fr 1fr 1fr 1fr 1.5fr 1.5fr;
4963
+ gap: 16px;
4964
+ padding: 16px 20px;
4965
+ border-bottom: 1px solid var(--border);
4966
+ background-color: var(--bg);
4967
+ font-size: 12px;
4968
+ font-weight: 600;
4969
+ color: var(--text-secondary);
4970
+ text-transform: uppercase;
4971
+ letter-spacing: 0.5px;
4972
+ }
4790
4973
 
4791
- .pending-item-timer {
4792
- display: flex;
4793
- align-items: center;
4794
- gap: 6px;
4795
- font-size: 11px;
4796
- font-family: var(--mono);
4797
- color: var(--text-secondary);
4798
- }
4974
+ .table-rows {
4975
+ max-height: 300px;
4976
+ overflow-y: auto;
4977
+ }
4799
4978
 
4800
- .pending-item-timer-bar {
4801
- flex: 1;
4802
- height: 4px;
4803
- background: rgba(48, 54, 61, 0.8);
4804
- border-radius: 2px;
4805
- overflow: hidden;
4806
- }
4979
+ .table-row {
4980
+ display: grid;
4981
+ grid-template-columns: 2fr 1fr 1fr 1fr 1.5fr 1.5fr;
4982
+ gap: 16px;
4983
+ padding: 14px 20px;
4984
+ border-bottom: 1px solid var(--border);
4985
+ align-items: center;
4986
+ font-size: 12px;
4987
+ cursor: pointer;
4988
+ transition: background-color 0.2s;
4989
+ }
4807
4990
 
4808
- .pending-item-timer-fill {
4809
- height: 100%;
4810
- background: var(--blue);
4811
- transition: width 0.1s linear;
4812
- }
4991
+ .table-row:hover {
4992
+ background-color: var(--bg);
4993
+ }
4813
4994
 
4814
- .pending-item-timer.urgent .pending-item-timer-fill {
4815
- background: var(--red);
4816
- }
4995
+ .table-row:last-child {
4996
+ border-bottom: none;
4997
+ }
4817
4998
 
4818
- .pending-item-actions {
4819
- display: flex;
4820
- gap: 8px;
4821
- }
4999
+ .table-cell {
5000
+ color: var(--text-secondary);
5001
+ font-family: 'JetBrains Mono', monospace;
5002
+ }
4822
5003
 
4823
- .btn {
4824
- flex: 1;
4825
- padding: 8px 12px;
4826
- border: none;
4827
- border-radius: var(--radius);
4828
- font-size: 12px;
4829
- font-weight: 600;
4830
- cursor: pointer;
4831
- transition: all 0.15s;
4832
- font-family: var(--sans);
4833
- }
5004
+ .table-cell.strong {
5005
+ color: var(--text-primary);
5006
+ font-weight: 500;
5007
+ }
4834
5008
 
4835
- .btn-approve {
4836
- background: var(--green);
4837
- color: var(--bg);
4838
- }
5009
+ .table-empty {
5010
+ padding: 40px 20px;
5011
+ text-align: center;
5012
+ color: var(--text-secondary);
5013
+ font-size: 13px;
5014
+ }
4839
5015
 
4840
- .btn-approve:hover {
4841
- background: #4ecf5e;
4842
- }
5016
+ /* Pending Overlay */
5017
+ .pending-overlay {
5018
+ position: fixed;
5019
+ top: 0;
5020
+ right: -400px;
5021
+ width: 400px;
5022
+ height: 100vh;
5023
+ background-color: var(--surface);
5024
+ border-left: 1px solid var(--border);
5025
+ box-shadow: -2px 0 8px rgba(0, 0, 0, 0.3);
5026
+ z-index: 200;
5027
+ transition: right 0.3s ease;
5028
+ display: flex;
5029
+ flex-direction: column;
5030
+ overflow-y: auto;
5031
+ }
4843
5032
 
4844
- .btn-deny {
4845
- background: var(--red);
4846
- color: white;
4847
- }
5033
+ .pending-overlay.show {
5034
+ right: 0;
5035
+ }
4848
5036
 
4849
- .btn-deny:hover {
4850
- background: #f9605e;
4851
- }
5037
+ .pending-header {
5038
+ padding: 16px 20px;
5039
+ border-bottom: 1px solid var(--border);
5040
+ font-weight: 600;
5041
+ color: var(--text-primary);
5042
+ }
4852
5043
 
4853
- /* \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 */
5044
+ .pending-items {
5045
+ flex: 1;
5046
+ overflow-y: auto;
5047
+ padding: 16px;
5048
+ }
4854
5049
 
4855
- .threat-panel {
4856
- position: fixed;
4857
- bottom: 0;
4858
- left: 0;
4859
- right: 0;
4860
- background: var(--surface);
4861
- border-top: 1px solid var(--border);
4862
- max-height: 240px;
4863
- z-index: 500;
4864
- display: flex;
4865
- flex-direction: column;
4866
- transition: max-height 0.3s ease-out;
4867
- }
5050
+ .pending-item {
5051
+ background-color: var(--bg);
5052
+ border: 1px solid var(--border);
5053
+ border-radius: 6px;
5054
+ padding: 16px;
5055
+ margin-bottom: 12px;
5056
+ }
4868
5057
 
4869
- .threat-panel.collapsed {
4870
- max-height: 40px;
4871
- }
5058
+ .pending-title {
5059
+ font-weight: 600;
5060
+ color: var(--text-primary);
5061
+ margin-bottom: 8px;
5062
+ word-break: break-word;
5063
+ }
4872
5064
 
4873
- .threat-header {
4874
- padding: 12px 20px;
4875
- cursor: pointer;
4876
- display: flex;
4877
- align-items: center;
4878
- gap: 8px;
4879
- font-size: 12px;
4880
- font-weight: 600;
4881
- text-transform: uppercase;
4882
- letter-spacing: 0.5px;
4883
- color: var(--text-secondary);
4884
- flex: 0 0 auto;
4885
- }
5065
+ .pending-countdown {
5066
+ font-size: 12px;
5067
+ color: var(--amber);
5068
+ margin-bottom: 12px;
5069
+ font-weight: 500;
5070
+ }
4886
5071
 
4887
- .threat-header:hover {
4888
- background: rgba(88, 166, 255, 0.05);
4889
- }
5072
+ .pending-actions {
5073
+ display: flex;
5074
+ gap: 8px;
5075
+ }
4890
5076
 
4891
- .threat-icon {
4892
- font-size: 14px;
4893
- }
5077
+ .pending-btn {
5078
+ flex: 1;
5079
+ padding: 8px 12px;
5080
+ border: none;
5081
+ border-radius: 4px;
5082
+ font-size: 12px;
5083
+ font-weight: 600;
5084
+ cursor: pointer;
5085
+ transition: background-color 0.2s;
5086
+ }
4894
5087
 
4895
- .threat-content {
4896
- flex: 1;
4897
- overflow-y: auto;
4898
- padding: 0 20px 12px;
4899
- display: flex;
4900
- flex-direction: column;
4901
- gap: 10px;
4902
- }
5088
+ .pending-approve {
5089
+ background-color: var(--green);
5090
+ color: var(--bg);
5091
+ }
4903
5092
 
4904
- .threat-item {
4905
- padding: 8px 10px;
4906
- background: rgba(248, 81, 73, 0.1);
4907
- border-left: 2px solid var(--red);
4908
- border-radius: 4px;
4909
- font-size: 11px;
4910
- color: var(--text-secondary);
4911
- }
5093
+ .pending-approve:hover {
5094
+ background-color: #3fa040;
5095
+ }
4912
5096
 
4913
- .threat-item-type {
4914
- font-weight: 600;
4915
- color: var(--red);
4916
- font-family: var(--mono);
4917
- }
5097
+ .pending-deny {
5098
+ background-color: var(--red);
5099
+ color: var(--bg);
5100
+ }
4918
5101
 
4919
- .threat-empty {
4920
- text-align: center;
4921
- padding: 20px 10px;
4922
- color: var(--text-secondary);
4923
- font-size: 12px;
4924
- }
5102
+ .pending-deny:hover {
5103
+ background-color: #e03c3c;
5104
+ }
4925
5105
 
4926
- /* \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 */
5106
+ /* Threat Panel */
5107
+ .threat-panel {
5108
+ background-color: var(--surface);
5109
+ border: 1px solid var(--border);
5110
+ border-radius: 8px;
5111
+ margin-top: 20px;
5112
+ overflow: hidden;
5113
+ }
4927
5114
 
4928
- ::-webkit-scrollbar {
4929
- width: 6px;
4930
- }
5115
+ .threat-header {
5116
+ padding: 16px 20px;
5117
+ border-bottom: 1px solid var(--border);
5118
+ display: flex;
5119
+ justify-content: space-between;
5120
+ align-items: center;
5121
+ cursor: pointer;
5122
+ user-select: none;
5123
+ }
4931
5124
 
4932
- ::-webkit-scrollbar-track {
4933
- background: transparent;
4934
- }
5125
+ .threat-title {
5126
+ font-weight: 600;
5127
+ color: var(--text-primary);
5128
+ }
4935
5129
 
4936
- ::-webkit-scrollbar-thumb {
4937
- background: var(--border);
4938
- border-radius: 3px;
4939
- }
5130
+ .threat-toggle {
5131
+ font-size: 10px;
5132
+ color: var(--text-secondary);
5133
+ transition: transform 0.2s;
5134
+ }
4940
5135
 
4941
- ::-webkit-scrollbar-thumb:hover {
4942
- background: rgba(88, 166, 255, 0.3);
4943
- }
5136
+ .threat-panel.collapsed .threat-toggle {
5137
+ transform: rotate(-90deg);
5138
+ }
4944
5139
 
4945
- /* \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 */
5140
+ .threat-content {
5141
+ padding: 16px 20px;
5142
+ max-height: 300px;
5143
+ overflow-y: auto;
5144
+ }
4946
5145
 
4947
- @media (max-width: 1200px) {
4948
- .protection-sidebar {
5146
+ .threat-panel.collapsed .threat-content {
4949
5147
  display: none;
4950
5148
  }
4951
5149
 
4952
- .activity-feed {
4953
- border-right: none;
5150
+ .threat-alert {
5151
+ background-color: rgba(248, 81, 73, 0.1);
5152
+ border: 1px solid var(--red);
5153
+ border-radius: 4px;
5154
+ padding: 12px;
5155
+ margin-bottom: 8px;
5156
+ font-size: 12px;
4954
5157
  }
4955
- }
4956
5158
 
4957
- @media (max-width: 768px) {
4958
- .status-bar {
4959
- padding: 0 12px;
4960
- gap: 12px;
4961
- height: 48px;
5159
+ .threat-alert:last-child {
5160
+ margin-bottom: 0;
4962
5161
  }
4963
5162
 
4964
- .sanctuary-logo {
4965
- font-size: 14px;
5163
+ .threat-type {
5164
+ font-weight: 600;
5165
+ color: var(--red);
5166
+ margin-bottom: 4px;
5167
+ text-transform: uppercase;
5168
+ font-size: 10px;
5169
+ letter-spacing: 0.5px;
4966
5170
  }
4967
5171
 
4968
- .status-bar-center {
4969
- display: none;
5172
+ .threat-message {
5173
+ color: var(--text-secondary);
4970
5174
  }
4971
5175
 
4972
- .main-container {
4973
- margin-top: 48px;
5176
+ /* Scrollbar */
5177
+ ::-webkit-scrollbar {
5178
+ width: 8px;
4974
5179
  }
4975
5180
 
4976
- .activity-item {
4977
- padding: 10px 12px;
5181
+ ::-webkit-scrollbar-track {
5182
+ background-color: transparent;
4978
5183
  }
4979
5184
 
4980
- .pending-overlay.active {
4981
- width: 100%;
5185
+ ::-webkit-scrollbar-thumb {
5186
+ background-color: var(--border);
5187
+ border-radius: 4px;
4982
5188
  }
4983
5189
 
4984
- .threat-panel {
4985
- max-height: 200px;
5190
+ ::-webkit-scrollbar-thumb:hover {
5191
+ background-color: var(--text-secondary);
4986
5192
  }
4987
- }
4988
- </style>
5193
+
5194
+ /* Responsive */
5195
+ @media (max-width: 1400px) {
5196
+ .sovereignty-layers {
5197
+ grid-template-columns: repeat(2, 1fr);
5198
+ }
5199
+
5200
+ .main-panels {
5201
+ grid-template-columns: 1fr;
5202
+ }
5203
+
5204
+ .pending-overlay {
5205
+ width: 100%;
5206
+ right: -100%;
5207
+ }
5208
+ }
5209
+
5210
+ @media (max-width: 768px) {
5211
+ .status-bar {
5212
+ flex-wrap: wrap;
5213
+ height: auto;
5214
+ padding: 12px;
5215
+ gap: 12px;
5216
+ }
5217
+
5218
+ .status-bar-center {
5219
+ order: 3;
5220
+ flex-basis: 100%;
5221
+ }
5222
+
5223
+ .main-content {
5224
+ margin-top: auto;
5225
+ }
5226
+
5227
+ .info-cards {
5228
+ grid-template-columns: 1fr;
5229
+ }
5230
+
5231
+ .table-header,
5232
+ .table-row {
5233
+ grid-template-columns: 1fr;
5234
+ }
5235
+ }
5236
+ </style>
4989
5237
  </head>
4990
5238
  <body>
4991
-
4992
- <!-- Status Bar (fixed, top) -->
4993
- <div class="status-bar">
4994
- <div class="status-bar-left">
4995
- <div class="sanctuary-logo"><span>\u25C6</span> SANCTUARY</div>
4996
- <div class="version">v${options.serverVersion}</div>
4997
- </div>
4998
- <div class="status-bar-center">
4999
- <div class="sovereignty-badge">
5000
- <div class="sovereignty-score" id="sovereigntyScore">85</div>
5001
- <span>Sovereignty Health</span>
5002
- </div>
5003
- </div>
5004
- <div class="status-bar-right">
5005
- <div class="protections-indicator">
5006
- <span class="count" id="activeProtections">6</span>/6 protections
5007
- </div>
5008
- <div class="uptime">
5009
- <span id="uptimeText">\u2014</span>
5010
- </div>
5011
- <div class="status-dot" id="statusDot"></div>
5012
- <div class="pending-badge hidden" id="pendingBadge">0</div>
5013
- </div>
5014
- </div>
5015
-
5016
- <!-- Main Layout -->
5017
- <div class="main-container">
5018
- <!-- Activity Feed -->
5019
- <div class="activity-feed">
5020
- <div class="feed-header">
5021
- <div class="feed-header-dot"></div>
5022
- Live Activity
5023
- </div>
5024
- <div class="activity-list" id="activityList">
5025
- <div class="activity-empty">
5026
- <div class="activity-empty-icon">\u2192</div>
5027
- <div class="activity-empty-text">Waiting for activity...</div>
5239
+ <!-- Status Bar -->
5240
+ <div class="status-bar">
5241
+ <div class="status-bar-left">
5242
+ <div class="logo-icon">\u25C6</div>
5243
+ <div class="logo-info">
5244
+ <div class="logo-title">SANCTUARY</div>
5245
+ <div class="logo-version">v${options.serverVersion}</div>
5028
5246
  </div>
5029
5247
  </div>
5030
- </div>
5031
5248
 
5032
- <!-- Protection Status Sidebar -->
5033
- <div class="protection-sidebar" id="protectionSidebar">
5034
- <div class="sidebar-header">
5035
- <span>\u25C6</span> Protection Status
5249
+ <div class="status-bar-center">
5250
+ <div id="sovereignty-badge" class="sovereignty-badge">
5251
+ <span>Sovereignty Health:</span>
5252
+ <span class="sovereignty-score" id="sovereignty-score">\u2014</span>
5253
+ <span>/ 100</span>
5254
+ </div>
5036
5255
  </div>
5037
- <div class="sidebar-content">
5038
- <div class="protection-card">
5039
- <div class="protection-card-icon">\u{1F510}</div>
5040
- <div class="protection-card-label">Encryption</div>
5041
- <div class="protection-card-status active" id="encryptionStatus">\u2713 Active</div>
5042
- <div class="protection-card-stat" id="encryptionStat">Ed25519</div>
5256
+
5257
+ <div class="status-bar-right">
5258
+ <div class="status-item">
5259
+ <strong id="protections-count">\u2014</strong>
5260
+ <span>Protections</span>
5261
+ </div>
5262
+ <div class="status-item">
5263
+ <strong id="uptime-value">\u2014</strong>
5264
+ <span>Uptime</span>
5043
5265
  </div>
5266
+ <div class="status-dot" id="connection-status"></div>
5267
+ <div id="pending-item-badge" class="pending-badge" style="display: none;">
5268
+ <span>\u23F3</span>
5269
+ <span id="pending-count">0</span>
5270
+ </div>
5271
+ </div>
5272
+ </div>
5044
5273
 
5045
- <div class="protection-card">
5046
- <div class="protection-card-icon">\u2713</div>
5047
- <div class="protection-card-label">Approval Gate</div>
5048
- <div class="protection-card-status active" id="approvalStatus">\u2713 Active</div>
5049
- <div class="protection-card-stat" id="approvalStat">T1: 2 | T2: 3</div>
5274
+ <!-- Main Content -->
5275
+ <div class="main-content">
5276
+ <div class="grid">
5277
+ <!-- Row 1: Sovereignty Layers -->
5278
+ <div class="sovereignty-layers" id="sovereignty-layers">
5279
+ <div class="layer-card" data-layer="l1">
5280
+ <div class="layer-name">Layer 1</div>
5281
+ <div class="layer-title">Cognitive Sovereignty</div>
5282
+ <div class="layer-status"><span>\u25CF</span> <span id="l1-status">\u2014</span></div>
5283
+ <div class="layer-detail" id="l1-detail">Loading...</div>
5284
+ </div>
5285
+ <div class="layer-card" data-layer="l2">
5286
+ <div class="layer-name">Layer 2</div>
5287
+ <div class="layer-title">Operational Isolation</div>
5288
+ <div class="layer-status"><span>\u25CF</span> <span id="l2-status">\u2014</span></div>
5289
+ <div class="layer-detail" id="l2-detail">Loading...</div>
5290
+ </div>
5291
+ <div class="layer-card" data-layer="l3">
5292
+ <div class="layer-name">Layer 3</div>
5293
+ <div class="layer-title">Selective Disclosure</div>
5294
+ <div class="layer-status"><span>\u25CF</span> <span id="l3-status">\u2014</span></div>
5295
+ <div class="layer-detail" id="l3-detail">Loading...</div>
5296
+ </div>
5297
+ <div class="layer-card" data-layer="l4">
5298
+ <div class="layer-name">Layer 4</div>
5299
+ <div class="layer-title">Verifiable Reputation</div>
5300
+ <div class="layer-status"><span>\u25CF</span> <span id="l4-status">\u2014</span></div>
5301
+ <div class="layer-detail" id="l4-detail">Loading...</div>
5302
+ </div>
5050
5303
  </div>
5051
5304
 
5052
- <div class="protection-card">
5053
- <div class="protection-card-icon">\u{1F3AF}</div>
5054
- <div class="protection-card-label">Context Gating</div>
5055
- <div class="protection-card-status active" id="contextStatus">\u2713 Active</div>
5056
- <div class="protection-card-stat" id="contextStat">12 filtered</div>
5305
+ <!-- Row 2: Info Cards -->
5306
+ <div class="info-cards">
5307
+ <div class="info-card">
5308
+ <div class="card-header">Identity</div>
5309
+ <div class="card-row">
5310
+ <span class="card-label">Primary</span>
5311
+ <span class="card-value" id="identity-label">\u2014</span>
5312
+ </div>
5313
+ <div class="card-row">
5314
+ <span class="card-label">DID</span>
5315
+ <span class="card-value truncated" id="identity-did" title="">\u2014</span>
5316
+ </div>
5317
+ <div class="card-row">
5318
+ <span class="card-label">Public Key</span>
5319
+ <span class="card-value truncated" id="identity-pubkey" title="">\u2014</span>
5320
+ </div>
5321
+ <div class="card-row">
5322
+ <span class="card-label">Type</span>
5323
+ <span class="identity-badge">Ed25519</span>
5324
+ </div>
5325
+ <div class="card-row">
5326
+ <span class="card-label">Created</span>
5327
+ <span class="card-value" id="identity-created">\u2014</span>
5328
+ </div>
5329
+ <div class="card-row">
5330
+ <span class="card-label">Identities</span>
5331
+ <span class="card-value" id="identity-count">\u2014</span>
5332
+ </div>
5333
+ </div>
5334
+
5335
+ <div class="info-card">
5336
+ <div class="card-header">Handshakes</div>
5337
+ <div class="card-row">
5338
+ <span class="card-label">Total</span>
5339
+ <span class="card-value" id="handshake-count">\u2014</span>
5340
+ </div>
5341
+ <div class="card-row">
5342
+ <span class="card-label">Latest Peer</span>
5343
+ <span class="card-value truncated" id="handshake-latest">\u2014</span>
5344
+ </div>
5345
+ <div class="card-row">
5346
+ <span class="card-label">Trust Tier</span>
5347
+ <span class="trust-tier-badge" id="handshake-tier">Unverified</span>
5348
+ </div>
5349
+ <div class="card-row">
5350
+ <span class="card-label">Timestamp</span>
5351
+ <span class="card-value" id="handshake-time">\u2014</span>
5352
+ </div>
5353
+ </div>
5354
+
5355
+ <div class="info-card">
5356
+ <div class="card-header">Reputation</div>
5357
+ <div class="card-row">
5358
+ <span class="card-label">Weighted Score</span>
5359
+ <span class="card-value" id="reputation-score">\u2014</span>
5360
+ </div>
5361
+ <div class="card-row">
5362
+ <span class="card-label">Attestations</span>
5363
+ <span class="card-value" id="reputation-attestations">\u2014</span>
5364
+ </div>
5365
+ <div class="card-row">
5366
+ <span class="card-label">Verified Sovereign</span>
5367
+ <span class="card-value" id="reputation-verified">\u2014</span>
5368
+ </div>
5369
+ <div class="card-row">
5370
+ <span class="card-label">Verified Degraded</span>
5371
+ <span class="card-value" id="reputation-degraded">\u2014</span>
5372
+ </div>
5373
+ <div class="card-row">
5374
+ <span class="card-label">Unverified</span>
5375
+ <span class="card-value" id="reputation-unverified">\u2014</span>
5376
+ </div>
5377
+ </div>
5057
5378
  </div>
5058
5379
 
5059
- <div class="protection-card">
5060
- <div class="protection-card-icon">\u26A0</div>
5061
- <div class="protection-card-label">Injection Detection</div>
5062
- <div class="protection-card-status active" id="injectionStatus">\u2713 Active</div>
5063
- <div class="protection-card-stat" id="injectionStat">3 flags today</div>
5380
+ <!-- Row 3: SHR & Activity -->
5381
+ <div class="main-panels">
5382
+ <div class="panel">
5383
+ <div class="panel-header">
5384
+ <div class="panel-title">Sovereignty Health Report</div>
5385
+ <button class="panel-action" id="copy-shr-btn">Copy JSON</button>
5386
+ </div>
5387
+ <div class="panel-content">
5388
+ <div class="shr-json" id="shr-viewer">
5389
+ <div class="empty-state">Loading SHR...</div>
5390
+ </div>
5391
+ </div>
5392
+ </div>
5393
+
5394
+ <div class="panel">
5395
+ <div class="panel-header">
5396
+ <div class="panel-title">Activity Feed</div>
5397
+ </div>
5398
+ <div class="panel-content">
5399
+ <div id="activity-feed" class="activity-feed">
5400
+ <div class="empty-state">Waiting for activity...</div>
5401
+ </div>
5402
+ </div>
5403
+ </div>
5064
5404
  </div>
5065
5405
 
5066
- <div class="protection-card">
5067
- <div class="protection-card-icon">\u{1F4CA}</div>
5068
- <div class="protection-card-label">Behavioral Baseline</div>
5069
- <div class="protection-card-status active" id="baselineStatus">\u2713 Active</div>
5070
- <div class="protection-card-stat" id="baselineStat">0 anomalies</div>
5406
+ <!-- Row 4: Handshake History -->
5407
+ <div class="handshake-table">
5408
+ <div class="table-header">
5409
+ <div>Counterparty</div>
5410
+ <div>Trust Tier</div>
5411
+ <div>Sovereignty</div>
5412
+ <div>Verified</div>
5413
+ <div>Completed</div>
5414
+ <div>Expires</div>
5415
+ </div>
5416
+ <div class="table-rows" id="handshake-table">
5417
+ <div class="table-empty">No handshakes completed yet</div>
5418
+ </div>
5071
5419
  </div>
5072
5420
 
5073
- <div class="protection-card">
5074
- <div class="protection-card-icon">\u{1F4CB}</div>
5075
- <div class="protection-card-label">Audit Trail</div>
5076
- <div class="protection-card-status active" id="auditStatus">\u2713 Active</div>
5077
- <div class="protection-card-stat" id="auditStat">284 entries</div>
5421
+ <!-- Threat Panel -->
5422
+ <div class="threat-panel collapsed">
5423
+ <div class="threat-header">
5424
+ <div class="threat-title">Security Threats</div>
5425
+ <div class="threat-toggle">\u25B6</div>
5426
+ </div>
5427
+ <div class="threat-content" id="threat-alerts">
5428
+ <div class="empty-state">No threats detected</div>
5429
+ </div>
5078
5430
  </div>
5079
5431
  </div>
5080
5432
  </div>
5081
- </div>
5082
5433
 
5083
- <!-- Pending Approvals Overlay -->
5084
- <div class="pending-overlay" id="pendingOverlay">
5085
- <div class="pending-overlay-header">
5086
- <div class="pending-overlay-title">Pending Approvals</div>
5087
- <button class="pending-overlay-close" onclick="closePendingOverlay()">\xD7</button>
5088
- </div>
5089
- <div class="pending-list" id="pendingList"></div>
5090
- </div>
5091
-
5092
- <!-- Threat Panel (collapsible footer) -->
5093
- <div class="threat-panel collapsed" id="threatPanel">
5094
- <div class="threat-header" onclick="toggleThreatPanel()">
5095
- <span class="threat-icon">\u26A0</span>
5096
- Recent Threats
5097
- <span id="threatCount" style="margin-left: auto; color: var(--red); font-weight: 700;">0</span>
5434
+ <!-- Pending Overlay -->
5435
+ <div class="pending-overlay" id="pending-overlay">
5436
+ <div class="pending-header">Pending Approvals</div>
5437
+ <div class="pending-items" id="pending-items"></div>
5098
5438
  </div>
5099
- <div class="threat-content" id="threatContent">
5100
- <div class="threat-empty">No threats detected</div>
5101
- </div>
5102
- </div>
5103
5439
 
5104
- <script>
5105
- (function() {
5106
- 'use strict';
5440
+ <script>
5441
+ // Constants
5442
+ const AUTH_TOKEN = '${options.authToken || ""}' || sessionStorage.getItem('authToken') || '';
5443
+ const TIMEOUT_SECONDS = ${options.timeoutSeconds};
5444
+ const API_BASE = '';
5445
+
5446
+ // State
5447
+ let apiState = {
5448
+ sovereignty: null,
5449
+ identity: null,
5450
+ handshakes: [],
5451
+ shr: null,
5452
+ status: null,
5453
+ };
5107
5454
 
5108
- // \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
5455
+ let pendingRequests = new Map();
5456
+ let activityLog = [];
5457
+ const maxActivityItems = 50;
5109
5458
 
5110
- const TIMEOUT_SECONDS = ${options.timeoutSeconds};
5111
- // AUTH_TOKEN: embedded token (for direct session access) or from sessionStorage (login page flow)
5112
- const EMBEDDED_TOKEN = ${options.authToken ? JSON.stringify(options.authToken) : "null"};
5113
- const AUTH_TOKEN = EMBEDDED_TOKEN || (function() { try { return sessionStorage.getItem('sanctuary_token'); } catch(_) { return null; } })();
5114
- const MAX_ACTIVITY_ITEMS = 100;
5115
- const MAX_THREAT_ITEMS = 20;
5459
+ // Helpers
5460
+ function esc(text) {
5461
+ if (!text) return '';
5462
+ const div = document.createElement('div');
5463
+ div.textContent = text;
5464
+ return div.innerHTML;
5465
+ }
5466
+
5467
+ function formatTime(isoString) {
5468
+ if (!isoString) return '\u2014';
5469
+ const date = new Date(isoString);
5470
+ return date.toLocaleString('en-US', {
5471
+ month: 'short',
5472
+ day: 'numeric',
5473
+ hour: '2-digit',
5474
+ minute: '2-digit',
5475
+ });
5476
+ }
5116
5477
 
5117
- // \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
5478
+ function truncate(str, len = 16) {
5479
+ if (!str) return '\u2014';
5480
+ if (str.length <= len) return str;
5481
+ return str.slice(0, len) + '...';
5482
+ }
5118
5483
 
5119
- let SESSION_ID = null;
5120
- let evtSource = null;
5121
- let startTime = Date.now();
5122
- let activityCount = 0;
5123
- let threatCount = 0;
5124
- const pendingRequests = new Map();
5125
- const activityItems = [];
5126
- const threatItems = [];
5127
- let sovereigntyScore = 85;
5128
- let sessionRenewalTimer = null;
5484
+ function calculateSovereigntyScore(shr) {
5485
+ if (!shr || !shr.layers) return 0;
5486
+ const layers = shr.layers;
5487
+ let score = 100;
5129
5488
 
5130
- // \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
5489
+ if (layers.l1?.status === 'degraded') score -= 20;
5490
+ if (layers.l1?.status === 'inactive') score -= 35;
5491
+ if (layers.l2?.status === 'degraded') score -= 15;
5492
+ if (layers.l2?.status === 'inactive') score -= 25;
5493
+ if (layers.l3?.status === 'degraded') score -= 15;
5494
+ if (layers.l3?.status === 'inactive') score -= 25;
5495
+ if (layers.l4?.status === 'degraded') score -= 10;
5496
+ if (layers.l4?.status === 'inactive') score -= 20;
5131
5497
 
5132
- function authHeaders() {
5133
- const h = { 'Content-Type': 'application/json' };
5134
- if (AUTH_TOKEN) h['Authorization'] = 'Bearer ' + AUTH_TOKEN;
5135
- return h;
5136
- }
5498
+ return Math.max(0, Math.min(100, score));
5499
+ }
5137
5500
 
5138
- function sessionQuery(url) {
5139
- if (!SESSION_ID) return url;
5140
- const sep = url.includes('?') ? '&' : '?';
5141
- return url + sep + 'session=' + SESSION_ID;
5142
- }
5501
+ async function fetchAPI(endpoint) {
5502
+ try {
5503
+ const response = await fetch(API_BASE + endpoint, {
5504
+ headers: {
5505
+ 'Authorization': 'Bearer ' + AUTH_TOKEN,
5506
+ },
5507
+ });
5143
5508
 
5144
- function setCookie(sessionId, maxAge) {
5145
- document.cookie = 'sanctuary_session=' + sessionId +
5146
- '; path=/; SameSite=Strict; max-age=' + maxAge;
5147
- }
5509
+ if (response.status === 401) {
5510
+ redirectToLogin();
5511
+ return null;
5512
+ }
5148
5513
 
5149
- async function exchangeSession() {
5150
- if (!AUTH_TOKEN) return;
5151
- try {
5152
- const resp = await fetch('/auth/session', { method: 'POST', headers: authHeaders() });
5153
- if (resp.ok) {
5154
- const data = await resp.json();
5155
- SESSION_ID = data.session_id;
5156
- var ttl = data.expires_in_seconds || 300;
5157
- // Update cookie with new session
5158
- setCookie(SESSION_ID, ttl);
5159
- // Schedule renewal at 80% of TTL
5160
- if (sessionRenewalTimer) clearTimeout(sessionRenewalTimer);
5161
- sessionRenewalTimer = setTimeout(function() {
5162
- exchangeSession().then(function() { reconnectSSE(); });
5163
- }, ttl * 800);
5164
- } else if (resp.status === 401) {
5165
- // Token invalid or expired \u2014 show non-destructive re-login overlay
5166
- showSessionExpired();
5167
- }
5168
- } catch (e) {
5169
- // Network error \u2014 retry in 30s
5170
- if (sessionRenewalTimer) clearTimeout(sessionRenewalTimer);
5171
- sessionRenewalTimer = setTimeout(function() {
5172
- exchangeSession().then(function() { reconnectSSE(); });
5173
- }, 30000);
5174
- }
5175
- }
5176
-
5177
- function showSessionExpired() {
5178
- // Clear stored token
5179
- try { sessionStorage.removeItem('sanctuary_token'); } catch(_) {}
5180
- // Redirect to login page
5181
- document.cookie = 'sanctuary_session=; path=/; max-age=0';
5182
- window.location.reload();
5183
- }
5184
-
5185
- // \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
5186
-
5187
- function esc(s) {
5188
- const d = document.createElement('div');
5189
- d.textContent = String(s || '');
5190
- return d.innerHTML;
5191
- }
5192
-
5193
- function closePendingOverlay() {
5194
- document.getElementById('pendingOverlay').classList.remove('active');
5195
- }
5196
-
5197
- function toggleThreatPanel() {
5198
- document.getElementById('threatPanel').classList.toggle('collapsed');
5199
- }
5200
-
5201
- function updateUptime() {
5202
- const elapsed = Math.floor((Date.now() - startTime) / 1000);
5203
- const hours = Math.floor(elapsed / 3600);
5204
- const mins = Math.floor((elapsed % 3600) / 60);
5205
- const secs = elapsed % 60;
5206
- let uptimeStr = '';
5207
- if (hours > 0) uptimeStr += hours + 'h ';
5208
- if (mins > 0) uptimeStr += mins + 'm ';
5209
- uptimeStr += secs + 's';
5210
- document.getElementById('uptimeText').textContent = uptimeStr;
5211
- }
5212
-
5213
- // \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
5214
-
5215
- function updateSovereigntyScore(score) {
5216
- sovereigntyScore = Math.min(100, Math.max(0, score || 85));
5217
- const badge = document.getElementById('sovereigntyScore');
5218
- badge.textContent = sovereigntyScore;
5219
- badge.className = 'sovereignty-score';
5220
- if (sovereigntyScore >= 80) {
5221
- badge.classList.add('high');
5222
- } else if (sovereigntyScore >= 50) {
5223
- badge.classList.add('medium');
5224
- } else {
5225
- badge.classList.add('low');
5514
+ if (!response.ok) {
5515
+ console.error('API Error:', response.status);
5516
+ return null;
5517
+ }
5518
+
5519
+ return await response.json();
5520
+ } catch (err) {
5521
+ console.error('Fetch error:', err);
5522
+ return null;
5523
+ }
5226
5524
  }
5227
- }
5228
5525
 
5229
- // \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
5526
+ function redirectToLogin() {
5527
+ sessionStorage.removeItem('authToken');
5528
+ window.location.href = '/';
5529
+ }
5230
5530
 
5231
- function addActivityItem(data) {
5232
- const {
5233
- timestamp,
5234
- tier,
5235
- tool,
5236
- outcome,
5237
- detail,
5238
- hasInjection,
5239
- isContextGated
5240
- } = data;
5241
-
5242
- const item = {
5243
- id: 'activity-' + activityCount++,
5244
- timestamp: timestamp || new Date().toISOString(),
5245
- tier: tier || 1,
5246
- tool: tool || 'unknown_tool',
5247
- outcome: outcome || 'executed',
5248
- detail: detail || '',
5249
- hasInjection: !!hasInjection,
5250
- isContextGated: !!isContextGated
5251
- };
5531
+ // API Updates
5532
+ async function updateSovereignty() {
5533
+ const data = await fetchAPI('/api/sovereignty');
5534
+ if (!data) return;
5252
5535
 
5253
- activityItems.unshift(item);
5254
- if (activityItems.length > MAX_ACTIVITY_ITEMS) {
5255
- activityItems.pop();
5256
- }
5536
+ apiState.sovereignty = data;
5257
5537
 
5258
- renderActivityFeed();
5259
- }
5538
+ const score = calculateSovereigntyScore(data.shr);
5539
+ const badge = document.getElementById('sovereignty-badge');
5540
+ const scoreEl = document.getElementById('sovereignty-score');
5260
5541
 
5261
- function renderActivityFeed() {
5262
- const list = document.getElementById('activityList');
5542
+ scoreEl.textContent = score;
5263
5543
 
5264
- if (activityItems.length === 0) {
5265
- list.innerHTML = '<div class="activity-empty"><div class="activity-empty-icon">\u2192</div><div class="activity-empty-text">Waiting for activity...</div></div>';
5266
- return;
5544
+ badge.classList.remove('degraded', 'inactive');
5545
+ if (score < 70) badge.classList.add('degraded');
5546
+ if (score < 40) badge.classList.add('inactive');
5547
+
5548
+ updateLayerCards(data.shr);
5267
5549
  }
5268
5550
 
5269
- list.innerHTML = '';
5270
- for (const item of activityItems) {
5271
- const tr = document.createElement('div');
5272
- tr.className = 'activity-item';
5273
- tr.id = item.id;
5274
-
5275
- const time = new Date(item.timestamp);
5276
- const timeStr = time.toLocaleTimeString();
5277
-
5278
- const tierClass = 't' + item.tier;
5279
- const outcomeClass = item.outcome === 'denied' ? 'outcome denied' : 'outcome';
5280
-
5281
- let icon = '\u25CF';
5282
- if (item.isContextGated) icon = '\u{1F3AF}';
5283
- else if (item.hasInjection) icon = '\u26A0';
5284
- else if (item.outcome === 'denied') icon = '\u2717';
5285
- else icon = '\u2713';
5286
-
5287
- tr.innerHTML =
5288
- '<div class="activity-item-icon">' + esc(icon) + '</div>' +
5289
- '<div class="activity-item-content">' +
5290
- '<div class="activity-time">' + esc(timeStr) + '</div>' +
5291
- '<div class="activity-main">' +
5292
- '<span class="activity-tier ' + tierClass + '">T' + item.tier + '</span>' +
5293
- '<span class="activity-tool">' + esc(item.tool) + '</span>' +
5294
- '<span class="activity-outcome ' + (outcomeClass === 'outcome denied' ? 'denied' : '') + '">' + (item.outcome === 'denied' ? '\u2717 denied' : '\u2713 allowed') + '</span>' +
5295
- '</div>' +
5296
- '<div class="activity-detail">' + esc(item.detail) + '</div>' +
5297
- '</div>' +
5298
- '';
5299
-
5300
- tr.addEventListener('click', () => {
5301
- tr.classList.toggle('expanded');
5302
- });
5551
+ function updateLayerCards(shr) {
5552
+ if (!shr || !shr.layers) return;
5303
5553
 
5304
- list.appendChild(tr);
5305
- }
5306
- }
5554
+ const layers = shr.layers;
5307
5555
 
5308
- // \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
5556
+ updateLayerCard('l1', layers.l1, layers.l1?.encryption || 'AES-256-GCM');
5557
+ updateLayerCard('l2', layers.l2, layers.l2?.isolation_type || 'Process-level');
5558
+ updateLayerCard('l3', layers.l3, layers.l3?.proof_system || 'Schnorr-Pedersen');
5559
+ updateLayerCard('l4', layers.l4, layers.l4?.reputation_mode || 'Weighted');
5560
+ }
5309
5561
 
5310
- function addPendingRequest(data) {
5311
- const {
5312
- request_id,
5313
- operation,
5314
- tier,
5315
- reason,
5316
- context,
5317
- timestamp
5318
- } = data;
5319
-
5320
- const pending = {
5321
- id: request_id,
5322
- operation: operation || 'unknown',
5323
- tier: tier || 1,
5324
- reason: reason || '',
5325
- context: context || {},
5326
- timestamp: timestamp || new Date().toISOString(),
5327
- remaining: TIMEOUT_SECONDS
5328
- };
5562
+ function updateLayerCard(layer, layerData, detail) {
5563
+ if (!layerData) return;
5329
5564
 
5330
- pendingRequests.set(request_id, pending);
5331
- updatePendingUI();
5332
- }
5565
+ const card = document.querySelector(\`[data-layer="\${layer}"]\`);
5566
+ if (!card) return;
5333
5567
 
5334
- function removePendingRequest(id) {
5335
- pendingRequests.delete(id);
5336
- updatePendingUI();
5337
- }
5568
+ const status = layerData.status || 'inactive';
5569
+ card.classList.remove('degraded', 'inactive');
5338
5570
 
5339
- function updatePendingUI() {
5340
- const count = pendingRequests.size;
5341
- const badge = document.getElementById('pendingBadge');
5571
+ if (status === 'degraded') {
5572
+ card.classList.add('degraded');
5573
+ } else if (status === 'inactive') {
5574
+ card.classList.add('inactive');
5575
+ }
5342
5576
 
5343
- if (count > 0) {
5344
- badge.classList.remove('hidden');
5345
- badge.textContent = count;
5346
- document.getElementById('pendingOverlay').classList.add('active');
5347
- } else {
5348
- badge.classList.add('hidden');
5349
- document.getElementById('pendingOverlay').classList.remove('active');
5577
+ document.getElementById(\`\${layer}-status\`).textContent = status.toUpperCase();
5578
+ document.getElementById(\`\${layer}-detail\`).textContent = detail;
5350
5579
  }
5351
5580
 
5352
- renderPendingList();
5353
- }
5581
+ async function updateIdentity() {
5582
+ const data = await fetchAPI('/api/identity');
5583
+ if (!data) return;
5354
5584
 
5355
- function renderPendingList() {
5356
- const list = document.getElementById('pendingList');
5357
- list.innerHTML = '';
5585
+ apiState.identity = data;
5358
5586
 
5359
- for (const [id, req] of pendingRequests) {
5360
- const item = document.createElement('div');
5361
- item.className = 'pending-item';
5587
+ const primary = data.primary || {};
5588
+ document.getElementById('identity-label').textContent = primary.label || '\u2014';
5589
+ document.getElementById('identity-did').textContent = truncate(primary.did, 24);
5590
+ document.getElementById('identity-did').title = primary.did || '';
5591
+ document.getElementById('identity-pubkey').textContent = truncate(primary.publicKey, 24);
5592
+ document.getElementById('identity-pubkey').title = primary.publicKey || '';
5593
+ document.getElementById('identity-created').textContent = formatTime(primary.createdAt);
5594
+ document.getElementById('identity-count').textContent = data.identities?.length || '\u2014';
5595
+ }
5362
5596
 
5363
- const tier = req.tier || 1;
5364
- const tierClass = 'tier' + tier;
5365
- const pct = Math.max(0, Math.min(100, (req.remaining / TIMEOUT_SECONDS) * 100));
5366
- const isUrgent = req.remaining <= 30;
5597
+ async function updateHandshakes() {
5598
+ const data = await fetchAPI('/api/handshakes');
5599
+ if (!data) return;
5367
5600
 
5368
- item.innerHTML =
5369
- '<div class="pending-item-header">' +
5370
- '<div class="pending-item-op">' + esc(req.operation) + '</div>' +
5371
- '<div class="pending-item-tier ' + tierClass + '">T' + tier + '</div>' +
5372
- '</div>' +
5373
- '<div class="pending-item-reason">' + esc(req.reason) + '</div>' +
5374
- '<div class="pending-item-timer ' + (isUrgent ? 'urgent' : '') + '">' +
5375
- '<div class="pending-item-timer-bar">' +
5376
- '<div class="pending-item-timer-fill" style="width: ' + pct + '%"></div>' +
5377
- '</div>' +
5378
- '<span id="timer-' + id + '">' + req.remaining + 's</span>' +
5379
- '</div>' +
5380
- '<div class="pending-item-actions">' +
5381
- '<button class="btn btn-approve" onclick="handleApprove('' + id + '')">Approve</button>' +
5382
- '<button class="btn btn-deny" onclick="handleDeny('' + id + '')">Deny</button>' +
5383
- '</div>' +
5384
- '';
5601
+ apiState.handshakes = data.handshakes || [];
5385
5602
 
5386
- list.appendChild(item);
5387
- }
5388
- }
5603
+ document.getElementById('handshake-count').textContent = data.handshakes?.length || '0';
5389
5604
 
5390
- window.handleApprove = function(id) {
5391
- fetch('/api/approve/' + id, { method: 'POST', headers: authHeaders() }).then(() => {
5392
- removePendingRequest(id);
5393
- }).catch(() => {});
5394
- };
5605
+ if (data.handshakes && data.handshakes.length > 0) {
5606
+ const latest = data.handshakes[0];
5607
+ document.getElementById('handshake-latest').textContent = truncate(latest.counterpartyId, 20);
5608
+ document.getElementById('handshake-latest').title = latest.counterpartyId || '';
5609
+ document.getElementById('handshake-tier').textContent = (latest.trustTier || 'Unverified').toUpperCase();
5610
+ document.getElementById('handshake-time').textContent = formatTime(latest.completedAt);
5611
+ } else {
5612
+ document.getElementById('handshake-latest').textContent = '\u2014';
5613
+ document.getElementById('handshake-tier').textContent = 'Unverified';
5614
+ document.getElementById('handshake-time').textContent = '\u2014';
5615
+ }
5395
5616
 
5396
- window.handleDeny = function(id) {
5397
- fetch('/api/deny/' + id, { method: 'POST', headers: authHeaders() }).then(() => {
5398
- removePendingRequest(id);
5399
- }).catch(() => {});
5400
- };
5617
+ updateHandshakeTable(data.handshakes || []);
5618
+ }
5401
5619
 
5402
- // \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
5620
+ function updateHandshakeTable(handshakes) {
5621
+ const table = document.getElementById('handshake-table');
5403
5622
 
5404
- function addThreat(data) {
5405
- const {
5406
- timestamp,
5407
- severity,
5408
- type,
5409
- details
5410
- } = data;
5411
-
5412
- const threat = {
5413
- id: 'threat-' + threatCount++,
5414
- timestamp: timestamp || new Date().toISOString(),
5415
- severity: severity || 'medium',
5416
- type: type || 'unknown',
5417
- details: details || ''
5418
- };
5623
+ if (!handshakes || handshakes.length === 0) {
5624
+ table.innerHTML = '<div class="table-empty">No handshakes completed yet</div>';
5625
+ return;
5626
+ }
5419
5627
 
5420
- threatItems.unshift(threat);
5421
- if (threatItems.length > MAX_THREAT_ITEMS) {
5422
- threatItems.pop();
5628
+ table.innerHTML = handshakes
5629
+ .map(
5630
+ (hs) => \`
5631
+ <div class="table-row">
5632
+ <div class="table-cell strong">\${esc(truncate(hs.counterpartyId, 24))}</div>
5633
+ <div class="table-cell">\${esc(hs.trustTier || 'Unverified')}</div>
5634
+ <div class="table-cell">\${esc(hs.sovereigntyLevel || '\u2014')}</div>
5635
+ <div class="table-cell">\${hs.verified ? 'Yes' : 'No'}</div>
5636
+ <div class="table-cell">\${formatTime(hs.completedAt)}</div>
5637
+ <div class="table-cell">\${formatTime(hs.expiresAt)}</div>
5638
+ </div>
5639
+ \`
5640
+ )
5641
+ .join('');
5423
5642
  }
5424
5643
 
5425
- if (threatCount > 0) {
5426
- document.getElementById('threatPanel').classList.remove('collapsed');
5644
+ async function updateSHR() {
5645
+ const data = await fetchAPI('/api/shr');
5646
+ if (!data) return;
5647
+
5648
+ apiState.shr = data;
5649
+ renderSHRViewer(data);
5427
5650
  }
5428
5651
 
5429
- renderThreats();
5430
- }
5652
+ function renderSHRViewer(shr) {
5653
+ const viewer = document.getElementById('shr-viewer');
5431
5654
 
5432
- function renderThreats() {
5433
- const content = document.getElementById('threatContent');
5434
- const badge = document.getElementById('threatCount');
5655
+ if (!shr) {
5656
+ viewer.innerHTML = '<div class="empty-state">No SHR available</div>';
5657
+ return;
5658
+ }
5435
5659
 
5436
- if (threatItems.length === 0) {
5437
- content.innerHTML = '<div class="threat-empty">No threats detected</div>';
5438
- badge.textContent = '0';
5439
- return;
5440
- }
5660
+ let html = '';
5661
+
5662
+ // Implementation
5663
+ html += \`
5664
+ <div class="shr-section">
5665
+ <div class="shr-section-header">
5666
+ <div class="shr-toggle">\u25BC</div>
5667
+ <div>Implementation</div>
5668
+ </div>
5669
+ <div class="shr-section-content">
5670
+ <div class="shr-item">
5671
+ <div class="shr-key">sanctuary_version:</div>
5672
+ <div class="shr-value">\${esc(shr.implementation?.sanctuary_version || '\u2014')}</div>
5673
+ </div>
5674
+ <div class="shr-item">
5675
+ <div class="shr-key">node_version:</div>
5676
+ <div class="shr-value">\${esc(shr.implementation?.node_version || '\u2014')}</div>
5677
+ </div>
5678
+ <div class="shr-item">
5679
+ <div class="shr-key">generated_by:</div>
5680
+ <div class="shr-value">\${esc(shr.implementation?.generated_by || '\u2014')}</div>
5681
+ </div>
5682
+ </div>
5683
+ </div>
5684
+ \`;
5685
+
5686
+ // Metadata
5687
+ html += \`
5688
+ <div class="shr-section">
5689
+ <div class="shr-section-header">
5690
+ <div class="shr-toggle">\u25BC</div>
5691
+ <div>Metadata</div>
5692
+ </div>
5693
+ <div class="shr-section-content">
5694
+ <div class="shr-item">
5695
+ <div class="shr-key">instance_id:</div>
5696
+ <div class="shr-value">\${esc(truncate(shr.instance_id, 20))}</div>
5697
+ </div>
5698
+ <div class="shr-item">
5699
+ <div class="shr-key">generated_at:</div>
5700
+ <div class="shr-value">\${formatTime(shr.generated_at)}</div>
5701
+ </div>
5702
+ <div class="shr-item">
5703
+ <div class="shr-key">expires_at:</div>
5704
+ <div class="shr-value">\${formatTime(shr.expires_at)}</div>
5705
+ </div>
5706
+ </div>
5707
+ </div>
5708
+ \`;
5709
+
5710
+ // Layers
5711
+ if (shr.layers) {
5712
+ html += \`<div class="shr-section">
5713
+ <div class="shr-section-header">
5714
+ <div class="shr-toggle">\u25BC</div>
5715
+ <div>Layers</div>
5716
+ </div>
5717
+ <div class="shr-section-content">
5718
+ \`;
5719
+
5720
+ for (const [key, layer] of Object.entries(shr.layers)) {
5721
+ html += \`
5722
+ <div style="margin-bottom: 12px;">
5723
+ <div style="color: var(--blue); font-weight: 600; margin-bottom: 4px;">\${esc(key)}</div>
5724
+ <div style="padding-left: 12px;">
5725
+ \`;
5726
+
5727
+ for (const [lkey, lvalue] of Object.entries(layer || {})) {
5728
+ const displayValue =
5729
+ typeof lvalue === 'boolean'
5730
+ ? lvalue
5731
+ ? 'true'
5732
+ : 'false'
5733
+ : esc(String(lvalue));
5734
+ html += \`
5735
+ <div class="shr-item">
5736
+ <div class="shr-key">\${esc(lkey)}:</div>
5737
+ <div class="shr-value">\${displayValue}</div>
5738
+ </div>
5739
+ \`;
5740
+ }
5441
5741
 
5442
- badge.textContent = threatItems.length;
5443
- content.innerHTML = '';
5742
+ html += \`
5743
+ </div>
5744
+ </div>
5745
+ \`;
5746
+ }
5444
5747
 
5445
- for (const threat of threatItems) {
5446
- const div = document.createElement('div');
5447
- div.className = 'threat-item';
5448
- const time = new Date(threat.timestamp).toLocaleTimeString();
5449
- div.innerHTML =
5450
- '<div style="margin-bottom: 3px;">' +
5451
- '<span class="threat-item-type">' + esc(threat.type) + '</span>' +
5452
- '<span style="font-size: 10px; color: var(--text-secondary); margin-left: 6px;">' + esc(time) + '</span>' +
5453
- '</div>' +
5454
- '<div>' + esc(threat.details) + '</div>' +
5455
- '';
5456
- content.appendChild(div);
5748
+ html += \`
5749
+ </div>
5750
+ </div>
5751
+ \`;
5752
+ }
5753
+
5754
+ // Capabilities
5755
+ if (shr.capabilities) {
5756
+ html += \`
5757
+ <div class="shr-section">
5758
+ <div class="shr-section-header">
5759
+ <div class="shr-toggle">\u25BC</div>
5760
+ <div>Capabilities</div>
5761
+ </div>
5762
+ <div class="shr-section-content">
5763
+ \`;
5764
+
5765
+ for (const [key, value] of Object.entries(shr.capabilities)) {
5766
+ const displayValue = value ? 'true' : 'false';
5767
+ html += \`
5768
+ <div class="shr-item">
5769
+ <div class="shr-key">\${esc(key)}:</div>
5770
+ <div class="shr-value">\${displayValue}</div>
5771
+ </div>
5772
+ \`;
5773
+ }
5774
+
5775
+ html += \`
5776
+ </div>
5777
+ </div>
5778
+ \`;
5779
+ }
5780
+
5781
+ // Signature
5782
+ html += \`
5783
+ <div class="shr-section">
5784
+ <div class="shr-section-header">
5785
+ <div class="shr-toggle">\u25BC</div>
5786
+ <div>Signature</div>
5787
+ </div>
5788
+ <div class="shr-section-content">
5789
+ <div class="shr-item">
5790
+ <div class="shr-key">signed_by:</div>
5791
+ <div class="shr-value">\${esc(truncate(shr.signed_by, 20))}</div>
5792
+ </div>
5793
+ <div class="shr-item">
5794
+ <div class="shr-key">signature:</div>
5795
+ <div class="shr-value">\${esc(truncate(shr.signature, 32))}</div>
5796
+ </div>
5797
+ </div>
5798
+ </div>
5799
+ \`;
5800
+
5801
+ viewer.innerHTML = html;
5802
+
5803
+ // Add collapse functionality
5804
+ document.querySelectorAll('.shr-section-header').forEach((header) => {
5805
+ header.addEventListener('click', () => {
5806
+ header.closest('.shr-section').classList.toggle('collapsed');
5807
+ });
5808
+ });
5457
5809
  }
5458
- }
5459
5810
 
5460
- // \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
5811
+ async function updateStatus() {
5812
+ const data = await fetchAPI('/api/status');
5813
+ if (!data) return;
5461
5814
 
5462
- function reconnectSSE() {
5463
- if (evtSource) evtSource.close();
5464
- connect();
5465
- }
5815
+ apiState.status = data;
5466
5816
 
5467
- function connect() {
5468
- evtSource = new EventSource(sessionQuery('/events'));
5817
+ document.getElementById('protections-count').textContent = data.protectionsCount || '0';
5818
+ document.getElementById('uptime-value').textContent = formatUptime(data.uptime);
5469
5819
 
5470
- evtSource.onopen = () => {
5471
- document.getElementById('statusDot').classList.remove('disconnected');
5472
- };
5820
+ const connectionStatus = document.getElementById('connection-status');
5821
+ connectionStatus.classList.toggle('disconnected', !data.connected);
5822
+ }
5473
5823
 
5474
- evtSource.onerror = () => {
5475
- document.getElementById('statusDot').classList.add('disconnected');
5476
- };
5824
+ function formatUptime(seconds) {
5825
+ if (!seconds) return '\u2014';
5826
+ const hours = Math.floor(seconds / 3600);
5827
+ const minutes = Math.floor((seconds % 3600) / 60);
5828
+ if (hours > 0) return \`\${hours}h \${minutes}m\`;
5829
+ return \`\${minutes}m\`;
5830
+ }
5477
5831
 
5478
- evtSource.addEventListener('init', (e) => {
5479
- const data = JSON.parse(e.data);
5480
- if (data.baseline) {
5481
- updateBaseline(data.baseline);
5482
- }
5483
- if (data.policy) {
5484
- updatePolicy(data.policy);
5485
- }
5486
- if (data.pending) {
5487
- data.pending.forEach(addPendingRequest);
5488
- }
5489
- });
5832
+ // SSE Setup
5833
+ function setupSSE() {
5834
+ const eventSource = new EventSource(API_BASE + '/api/events', {
5835
+ headers: {
5836
+ 'Authorization': 'Bearer ' + AUTH_TOKEN,
5837
+ },
5838
+ });
5490
5839
 
5491
- evtSource.addEventListener('pending-request', (e) => {
5492
- const data = JSON.parse(e.data);
5493
- addPendingRequest(data);
5494
- });
5840
+ eventSource.addEventListener('init', (e) => {
5841
+ console.log('Connected to SSE');
5842
+ });
5495
5843
 
5496
- evtSource.addEventListener('request-resolved', (e) => {
5497
- const data = JSON.parse(e.data);
5498
- removePendingRequest(data.request_id);
5499
- });
5844
+ eventSource.addEventListener('sovereignty-update', () => {
5845
+ updateSovereignty();
5846
+ });
5500
5847
 
5501
- evtSource.addEventListener('tool-call', (e) => {
5502
- const data = JSON.parse(e.data);
5503
- addActivityItem({
5504
- timestamp: data.timestamp,
5505
- tier: data.tier || 1,
5506
- tool: data.tool || 'unknown',
5507
- outcome: data.outcome || 'executed',
5508
- detail: data.detail || ''
5848
+ eventSource.addEventListener('handshake-update', () => {
5849
+ updateHandshakes();
5509
5850
  });
5510
- });
5511
5851
 
5512
- evtSource.addEventListener('context-gate-decision', (e) => {
5513
- const data = JSON.parse(e.data);
5514
- addActivityItem({
5515
- timestamp: data.timestamp,
5516
- tier: data.tier || 1,
5517
- tool: data.tool || 'unknown',
5518
- outcome: data.outcome || 'gated',
5519
- detail: data.fields_filtered ? 'Filtered ' + data.fields_filtered + ' fields' : data.reason || '',
5520
- isContextGated: true
5852
+ eventSource.addEventListener('tool-call', (e) => {
5853
+ const data = JSON.parse(e.data);
5854
+ addActivityItem({
5855
+ type: 'tool-call',
5856
+ title: 'Tool Call',
5857
+ content: data.toolName,
5858
+ timestamp: new Date().toISOString(),
5859
+ });
5521
5860
  });
5522
- });
5523
5861
 
5524
- evtSource.addEventListener('injection-alert', (e) => {
5525
- const data = JSON.parse(e.data);
5526
- addActivityItem({
5527
- timestamp: data.timestamp,
5528
- tier: data.tier || 2,
5529
- tool: data.tool || 'unknown',
5530
- outcome: data.allowed ? 'allowed' : 'denied',
5531
- detail: data.signal || 'Injection detected',
5532
- hasInjection: true
5862
+ eventSource.addEventListener('context-gate-decision', (e) => {
5863
+ const data = JSON.parse(e.data);
5864
+ addActivityItem({
5865
+ type: 'context-gate',
5866
+ title: 'Context Gate',
5867
+ content: data.decision,
5868
+ timestamp: new Date().toISOString(),
5869
+ });
5533
5870
  });
5534
- addThreat({
5535
- timestamp: data.timestamp,
5536
- severity: data.severity || 'medium',
5537
- type: 'Injection Alert',
5538
- details: data.signal || 'Suspicious pattern detected'
5871
+
5872
+ eventSource.addEventListener('injection-alert', (e) => {
5873
+ const data = JSON.parse(e.data);
5874
+ addActivityItem({
5875
+ type: 'injection',
5876
+ title: 'Injection Alert',
5877
+ content: data.pattern,
5878
+ timestamp: new Date().toISOString(),
5879
+ });
5880
+ addThreatAlert(data);
5539
5881
  });
5540
- });
5541
5882
 
5542
- evtSource.addEventListener('protection-status', (e) => {
5543
- const data = JSON.parse(e.data);
5544
- updateProtectionStatus(data);
5545
- });
5883
+ eventSource.addEventListener('pending-request', (e) => {
5884
+ const data = JSON.parse(e.data);
5885
+ addPendingRequest(data);
5886
+ });
5546
5887
 
5547
- evtSource.addEventListener('audit-entry', (e) => {
5548
- const data = JSON.parse(e.data);
5549
- // Audit entries don't show in activity by default, but we could add them
5550
- });
5888
+ eventSource.addEventListener('request-resolved', (e) => {
5889
+ const data = JSON.parse(e.data);
5890
+ removePendingRequest(data.requestId);
5891
+ });
5551
5892
 
5552
- evtSource.addEventListener('baseline-update', (e) => {
5553
- const data = JSON.parse(e.data);
5554
- updateBaseline(data);
5555
- });
5556
- }
5893
+ eventSource.onerror = () => {
5894
+ console.error('SSE error');
5895
+ setTimeout(setupSSE, 5000);
5896
+ };
5897
+ }
5557
5898
 
5558
- function updateBaseline(baseline) {
5559
- if (!baseline) return;
5560
- // Update baseline-derived stats if needed
5561
- }
5899
+ // Activity Feed
5900
+ function addActivityItem(item) {
5901
+ activityLog.unshift(item);
5902
+ if (activityLog.length > maxActivityItems) {
5903
+ activityLog.pop();
5904
+ }
5562
5905
 
5563
- function updatePolicy(policy) {
5564
- if (!policy) return;
5565
- // Update policy-derived stats
5566
- if (policy.approval_channel) {
5567
- // Policy info updated
5568
- }
5569
- }
5906
+ const feed = document.getElementById('activity-feed');
5907
+ const html = \`
5908
+ <div class="activity-item \${item.type}">
5909
+ <div class="activity-type">\${esc(item.title)}</div>
5910
+ <div class="activity-content">\${esc(item.content)}</div>
5911
+ <div class="activity-time">\${formatTime(item.timestamp)}</div>
5912
+ </div>
5913
+ \`;
5570
5914
 
5571
- function updateProtectionStatus(status) {
5572
- if (status.sovereignty_score !== undefined) {
5573
- updateSovereigntyScore(status.sovereignty_score);
5574
- }
5575
- if (status.active_protections !== undefined) {
5576
- document.getElementById('activeProtections').textContent = status.active_protections;
5577
- }
5578
- // Update individual protection cards
5579
- if (status.encryption !== undefined) {
5580
- const el = document.getElementById('encryptionStatus');
5581
- el.className = 'protection-card-status ' + (status.encryption ? 'active' : 'inactive');
5582
- el.textContent = status.encryption ? '\u2713 Active' : '\u2717 Inactive';
5583
- }
5584
- if (status.approval_gate !== undefined) {
5585
- const el = document.getElementById('approvalStatus');
5586
- el.className = 'protection-card-status ' + (status.approval_gate ? 'active' : 'inactive');
5587
- el.textContent = status.approval_gate ? '\u2713 Active' : '\u2717 Inactive';
5588
- }
5589
- if (status.context_gating !== undefined) {
5590
- const el = document.getElementById('contextStatus');
5591
- el.className = 'protection-card-status ' + (status.context_gating ? 'active' : 'inactive');
5592
- el.textContent = status.context_gating ? '\u2713 Active' : '\u2717 Inactive';
5915
+ if (feed.querySelector('.empty-state')) {
5916
+ feed.innerHTML = '';
5917
+ }
5918
+
5919
+ feed.insertAdjacentHTML('afterbegin', html);
5920
+
5921
+ if (feed.children.length > maxActivityItems) {
5922
+ feed.lastChild.remove();
5923
+ }
5593
5924
  }
5594
- if (status.injection_detection !== undefined) {
5595
- const el = document.getElementById('injectionStatus');
5596
- el.className = 'protection-card-status ' + (status.injection_detection ? 'active' : 'inactive');
5597
- el.textContent = status.injection_detection ? '\u2713 Active' : '\u2717 Inactive';
5925
+
5926
+ // Pending Requests
5927
+ function addPendingRequest(request) {
5928
+ pendingRequests.set(request.requestId, {
5929
+ id: request.requestId,
5930
+ title: request.title,
5931
+ details: request.details,
5932
+ expiresAt: new Date(Date.now() + TIMEOUT_SECONDS * 1000),
5933
+ });
5934
+
5935
+ updatePendingDisplay();
5598
5936
  }
5599
- if (status.baseline !== undefined) {
5600
- const el = document.getElementById('baselineStatus');
5601
- el.className = 'protection-card-status ' + (status.baseline ? 'active' : 'inactive');
5602
- el.textContent = status.baseline ? '\u2713 Active' : '\u2717 Inactive';
5937
+
5938
+ function removePendingRequest(requestId) {
5939
+ pendingRequests.delete(requestId);
5940
+ updatePendingDisplay();
5603
5941
  }
5604
- if (status.audit_trail !== undefined) {
5605
- const el = document.getElementById('auditStatus');
5606
- el.className = 'protection-card-status ' + (status.audit_trail ? 'active' : 'inactive');
5607
- el.textContent = status.audit_trail ? '\u2713 Active' : '\u2717 Inactive';
5942
+
5943
+ function updatePendingDisplay() {
5944
+ const badge = document.getElementById('pending-item-badge');
5945
+ const count = pendingRequests.size;
5946
+
5947
+ if (count > 0) {
5948
+ document.getElementById('pending-count').textContent = count;
5949
+ badge.style.display = 'flex';
5950
+ } else {
5951
+ badge.style.display = 'none';
5952
+ }
5953
+
5954
+ const overlay = document.getElementById('pending-overlay');
5955
+ const items = document.getElementById('pending-items');
5956
+
5957
+ if (count === 0) {
5958
+ items.innerHTML = '';
5959
+ overlay.classList.remove('show');
5960
+ return;
5961
+ }
5962
+
5963
+ let html = '';
5964
+ for (const req of pendingRequests.values()) {
5965
+ const remaining = Math.max(0, Math.floor((req.expiresAt - Date.now()) / 1000));
5966
+ html += \`
5967
+ <div class="pending-item">
5968
+ <div class="pending-title">\${esc(req.title)}</div>
5969
+ <div class="pending-countdown">Expires in \${remaining}s</div>
5970
+ <div class="pending-actions">
5971
+ <button class="pending-btn pending-approve" data-id="\${req.id}">Approve</button>
5972
+ <button class="pending-btn pending-deny" data-id="\${req.id}">Deny</button>
5973
+ </div>
5974
+ </div>
5975
+ \`;
5976
+ }
5977
+
5978
+ items.innerHTML = html;
5979
+
5980
+ document.querySelectorAll('.pending-approve').forEach((btn) => {
5981
+ btn.addEventListener('click', async () => {
5982
+ const id = btn.getAttribute('data-id');
5983
+ await fetchAPI(\`/api/approve/\${id}\`);
5984
+ });
5985
+ });
5986
+
5987
+ document.querySelectorAll('.pending-deny').forEach((btn) => {
5988
+ btn.addEventListener('click', async () => {
5989
+ const id = btn.getAttribute('data-id');
5990
+ await fetchAPI(\`/api/deny/\${id}\`);
5991
+ });
5992
+ });
5608
5993
  }
5609
- }
5610
5994
 
5611
- // \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
5995
+ // Threat Panel
5996
+ function addThreatAlert(alert) {
5997
+ const panel = document.querySelector('.threat-panel');
5998
+ const content = document.getElementById('threat-alerts');
5612
5999
 
5613
- (async function init() {
5614
- await exchangeSession();
5615
- // Clean legacy ?token= from URL
5616
- if (window.location.search.includes('token=')) {
5617
- window.history.replaceState({}, '', window.location.pathname);
6000
+ if (content.querySelector('.empty-state')) {
6001
+ content.innerHTML = '';
6002
+ }
6003
+
6004
+ panel.classList.remove('collapsed');
6005
+
6006
+ const html = \`
6007
+ <div class="threat-alert">
6008
+ <div class="threat-type">\${esc(alert.type || 'Injection Alert')}</div>
6009
+ <div class="threat-message">\${esc(alert.message || alert.pattern || '\u2014')}</div>
6010
+ </div>
6011
+ \`;
6012
+
6013
+ content.insertAdjacentHTML('afterbegin', html);
6014
+
6015
+ const alerts = content.querySelectorAll('.threat-alert');
6016
+ if (alerts.length > 10) {
6017
+ alerts[alerts.length - 1].remove();
6018
+ }
5618
6019
  }
5619
- connect();
5620
6020
 
5621
- // Start uptime ticker
5622
- setInterval(updateUptime, 1000);
5623
- updateUptime();
6021
+ // Threat Panel Toggle
6022
+ document.querySelector('.threat-header').addEventListener('click', () => {
6023
+ document.querySelector('.threat-panel').classList.toggle('collapsed');
6024
+ });
6025
+
6026
+ // SHR Copy Button
6027
+ document.getElementById('copy-shr-btn').addEventListener('click', async () => {
6028
+ if (!apiState.shr) return;
5624
6029
 
5625
- // Pending request countdown timer
5626
- setInterval(() => {
5627
- for (const [id, req] of pendingRequests) {
5628
- req.remaining = Math.max(0, req.remaining - 1);
5629
- const el = document.getElementById('timer-' + id);
5630
- if (el) {
5631
- el.textContent = req.remaining + 's';
5632
- }
6030
+ const json = JSON.stringify(apiState.shr, null, 2);
6031
+ try {
6032
+ await navigator.clipboard.writeText(json);
6033
+ const btn = document.getElementById('copy-shr-btn');
6034
+ const original = btn.textContent;
6035
+ btn.textContent = 'Copied!';
6036
+ setTimeout(() => {
6037
+ btn.textContent = original;
6038
+ }, 2000);
6039
+ } catch (err) {
6040
+ console.error('Copy failed:', err);
5633
6041
  }
5634
- }, 1000);
6042
+ });
5635
6043
 
5636
- // Load initial status
5637
- try {
5638
- const resp = await fetch('/api/status', { headers: authHeaders() });
5639
- if (resp.ok) {
5640
- const status = await resp.json();
5641
- if (status.baseline) updateBaseline(status.baseline);
5642
- if (status.policy) updatePolicy(status.policy);
6044
+ // Pending Overlay Toggle
6045
+ document.getElementById('pending-item-badge').addEventListener('click', () => {
6046
+ document.getElementById('pending-overlay').classList.toggle('show');
6047
+ });
6048
+
6049
+ // Initialize
6050
+ async function initialize() {
6051
+ if (!AUTH_TOKEN) {
6052
+ redirectToLogin();
6053
+ return;
5643
6054
  }
5644
- } catch (e) {
5645
- // Ignore
5646
- }
5647
- })();
5648
6055
 
5649
- })();
5650
- </script>
6056
+ // Initial data fetch
6057
+ await Promise.all([
6058
+ updateSovereignty(),
6059
+ updateIdentity(),
6060
+ updateHandshakes(),
6061
+ updateSHR(),
6062
+ updateStatus(),
6063
+ ]);
5651
6064
 
6065
+ // Setup SSE for real-time updates
6066
+ setupSSE();
6067
+
6068
+ // Refresh status periodically
6069
+ setInterval(updateStatus, 30000);
6070
+ }
6071
+
6072
+ // Start
6073
+ initialize();
6074
+ </script>
5652
6075
  </body>
5653
6076
  </html>`;
5654
6077
  }
@@ -5669,6 +6092,10 @@ var DashboardApprovalChannel = class {
5669
6092
  policy = null;
5670
6093
  baseline = null;
5671
6094
  auditLog = null;
6095
+ identityManager = null;
6096
+ handshakeResults = null;
6097
+ shrOpts = null;
6098
+ _sanctuaryConfig = null;
5672
6099
  dashboardHTML;
5673
6100
  loginHTML;
5674
6101
  authToken;
@@ -5702,6 +6129,10 @@ var DashboardApprovalChannel = class {
5702
6129
  this.policy = deps.policy;
5703
6130
  this.baseline = deps.baseline;
5704
6131
  this.auditLog = deps.auditLog;
6132
+ if (deps.identityManager) this.identityManager = deps.identityManager;
6133
+ if (deps.handshakeResults) this.handshakeResults = deps.handshakeResults;
6134
+ if (deps.shrOpts) this.shrOpts = deps.shrOpts;
6135
+ if (deps.sanctuaryConfig) this._sanctuaryConfig = deps.sanctuaryConfig;
5705
6136
  }
5706
6137
  /**
5707
6138
  * Start the HTTP(S) server for the dashboard.
@@ -6034,6 +6465,14 @@ var DashboardApprovalChannel = class {
6034
6465
  this.handlePendingList(res);
6035
6466
  } else if (method === "GET" && url.pathname === "/api/audit-log") {
6036
6467
  this.handleAuditLog(url, res);
6468
+ } else if (method === "GET" && url.pathname === "/api/sovereignty") {
6469
+ this.handleSovereignty(res);
6470
+ } else if (method === "GET" && url.pathname === "/api/identity") {
6471
+ this.handleIdentity(res);
6472
+ } else if (method === "GET" && url.pathname === "/api/handshakes") {
6473
+ this.handleHandshakes(res);
6474
+ } else if (method === "GET" && url.pathname === "/api/shr") {
6475
+ this.handleSHR(res);
6037
6476
  } else if (method === "POST" && url.pathname.startsWith("/api/approve/")) {
6038
6477
  if (!this.checkRateLimit(req, res, "decisions")) return;
6039
6478
  const id = url.pathname.slice("/api/approve/".length);
@@ -6223,6 +6662,107 @@ data: ${JSON.stringify(initData)}
6223
6662
  res.writeHead(200, { "Content-Type": "application/json" });
6224
6663
  res.end(JSON.stringify({ success: true, decision }));
6225
6664
  }
6665
+ // ── Sovereignty Data Routes ─────────────────────────────────────────
6666
+ handleSovereignty(res) {
6667
+ if (!this.shrOpts) {
6668
+ res.writeHead(200, { "Content-Type": "application/json" });
6669
+ res.end(JSON.stringify({ error: "SHR generator not available" }));
6670
+ return;
6671
+ }
6672
+ const shr = generateSHR(void 0, this.shrOpts);
6673
+ if (typeof shr === "string") {
6674
+ res.writeHead(200, { "Content-Type": "application/json" });
6675
+ res.end(JSON.stringify({ error: shr }));
6676
+ return;
6677
+ }
6678
+ const layers = shr.body.layers;
6679
+ let score = 0;
6680
+ for (const layer of [layers.l1, layers.l2, layers.l3, layers.l4]) {
6681
+ if (layer.status === "active") score += 25;
6682
+ else if (layer.status === "degraded") score += 15;
6683
+ }
6684
+ const overallLevel = score === 100 ? "full" : score >= 65 ? "degraded" : score >= 25 ? "minimal" : "unverified";
6685
+ res.writeHead(200, { "Content-Type": "application/json" });
6686
+ res.end(JSON.stringify({
6687
+ score,
6688
+ overall_level: overallLevel,
6689
+ layers: {
6690
+ l1: { status: layers.l1.status, detail: layers.l1.encryption, key_custody: layers.l1.key_custody },
6691
+ l2: { status: layers.l2.status, detail: layers.l2.isolation_type, attestation: layers.l2.attestation_available },
6692
+ l3: { status: layers.l3.status, detail: layers.l3.proof_system, selective_disclosure: layers.l3.selective_disclosure },
6693
+ l4: { status: layers.l4.status, detail: layers.l4.attestation_format, reputation_portable: layers.l4.reputation_portable }
6694
+ },
6695
+ degradations: shr.body.degradations,
6696
+ capabilities: shr.body.capabilities,
6697
+ config_loaded: this._sanctuaryConfig != null
6698
+ }));
6699
+ }
6700
+ handleIdentity(res) {
6701
+ if (!this.identityManager) {
6702
+ res.writeHead(200, { "Content-Type": "application/json" });
6703
+ res.end(JSON.stringify({ identities: [], count: 0 }));
6704
+ return;
6705
+ }
6706
+ const identities = this.identityManager.list().map((id) => ({
6707
+ identity_id: id.identity_id,
6708
+ label: id.label,
6709
+ public_key: id.public_key,
6710
+ did: id.did,
6711
+ created_at: id.created_at,
6712
+ key_type: id.key_type,
6713
+ key_protection: id.key_protection,
6714
+ rotation_count: id.rotation_history?.length ?? 0
6715
+ }));
6716
+ const primary = this.identityManager.getDefault();
6717
+ res.writeHead(200, { "Content-Type": "application/json" });
6718
+ res.end(JSON.stringify({
6719
+ identities,
6720
+ count: identities.length,
6721
+ primary_id: primary?.identity_id ?? null
6722
+ }));
6723
+ }
6724
+ handleHandshakes(res) {
6725
+ if (!this.handshakeResults) {
6726
+ res.writeHead(200, { "Content-Type": "application/json" });
6727
+ res.end(JSON.stringify({ handshakes: [], count: 0 }));
6728
+ return;
6729
+ }
6730
+ const handshakes = Array.from(this.handshakeResults.values()).map((h) => ({
6731
+ counterparty_id: h.counterparty_id,
6732
+ verified: h.verified,
6733
+ sovereignty_level: h.sovereignty_level,
6734
+ trust_tier: h.trust_tier,
6735
+ completed_at: h.completed_at,
6736
+ expires_at: h.expires_at,
6737
+ errors: h.errors
6738
+ }));
6739
+ handshakes.sort((a, b) => new Date(b.completed_at).getTime() - new Date(a.completed_at).getTime());
6740
+ res.writeHead(200, { "Content-Type": "application/json" });
6741
+ res.end(JSON.stringify({
6742
+ handshakes,
6743
+ count: handshakes.length,
6744
+ tier_distribution: {
6745
+ verified_sovereign: handshakes.filter((h) => h.trust_tier === "verified-sovereign").length,
6746
+ verified_degraded: handshakes.filter((h) => h.trust_tier === "verified-degraded").length,
6747
+ unverified: handshakes.filter((h) => h.trust_tier === "unverified").length
6748
+ }
6749
+ }));
6750
+ }
6751
+ handleSHR(res) {
6752
+ if (!this.shrOpts) {
6753
+ res.writeHead(200, { "Content-Type": "application/json" });
6754
+ res.end(JSON.stringify({ error: "SHR generator not available" }));
6755
+ return;
6756
+ }
6757
+ const shr = generateSHR(void 0, this.shrOpts);
6758
+ if (typeof shr === "string") {
6759
+ res.writeHead(200, { "Content-Type": "application/json" });
6760
+ res.end(JSON.stringify({ error: shr }));
6761
+ return;
6762
+ }
6763
+ res.writeHead(200, { "Content-Type": "application/json" });
6764
+ res.end(JSON.stringify(shr));
6765
+ }
6226
6766
  // ── SSE Broadcasting ────────────────────────────────────────────────
6227
6767
  broadcastSSE(event, data) {
6228
6768
  const message = `event: ${event}
@@ -7389,108 +7929,6 @@ function createPrincipalPolicyTools(policy, baseline, auditLog) {
7389
7929
  ];
7390
7930
  }
7391
7931
 
7392
- // src/shr/types.ts
7393
- function deepSortKeys(obj) {
7394
- if (obj === null || typeof obj !== "object") return obj;
7395
- if (Array.isArray(obj)) return obj.map(deepSortKeys);
7396
- const sorted = {};
7397
- for (const key of Object.keys(obj).sort()) {
7398
- sorted[key] = deepSortKeys(obj[key]);
7399
- }
7400
- return sorted;
7401
- }
7402
- function canonicalizeForSigning(body) {
7403
- return JSON.stringify(deepSortKeys(body));
7404
- }
7405
-
7406
- // src/shr/generator.ts
7407
- init_encoding();
7408
- var DEFAULT_VALIDITY_MS = 60 * 60 * 1e3;
7409
- function generateSHR(identityId, opts) {
7410
- const { config, identityManager, masterKey, validityMs } = opts;
7411
- const identity = identityId ? identityManager.get(identityId) : identityManager.getDefault();
7412
- if (!identity) {
7413
- return "No identity available for signing. Create an identity first.";
7414
- }
7415
- const now = /* @__PURE__ */ new Date();
7416
- const expiresAt = new Date(now.getTime() + (validityMs ?? DEFAULT_VALIDITY_MS));
7417
- const degradations = [];
7418
- if (config.execution.environment === "local-process") {
7419
- degradations.push({
7420
- layer: "l2",
7421
- code: "PROCESS_ISOLATION_ONLY",
7422
- severity: "warning",
7423
- description: "Process-level isolation only (no TEE)",
7424
- mitigation: "TEE support planned for a future release"
7425
- });
7426
- degradations.push({
7427
- layer: "l2",
7428
- code: "SELF_REPORTED_ATTESTATION",
7429
- severity: "warning",
7430
- description: "Attestation is self-reported (no hardware root of trust)",
7431
- mitigation: "TEE attestation planned for a future release"
7432
- });
7433
- }
7434
- const body = {
7435
- shr_version: "1.0",
7436
- implementation: {
7437
- sanctuary_version: config.version,
7438
- node_version: process.versions.node,
7439
- generated_by: "sanctuary-mcp-server"
7440
- },
7441
- instance_id: identity.identity_id,
7442
- generated_at: now.toISOString(),
7443
- expires_at: expiresAt.toISOString(),
7444
- layers: {
7445
- l1: {
7446
- status: "active",
7447
- encryption: config.state.encryption,
7448
- key_custody: "self",
7449
- integrity: config.state.integrity,
7450
- identity_type: config.state.identity_provider,
7451
- state_portable: true
7452
- },
7453
- l2: {
7454
- status: config.execution.environment === "local-process" ? "degraded" : "active",
7455
- isolation_type: config.execution.environment,
7456
- attestation_available: config.execution.attestation
7457
- },
7458
- l3: {
7459
- status: "active",
7460
- proof_system: config.disclosure.proof_system,
7461
- selective_disclosure: true
7462
- },
7463
- l4: {
7464
- status: "active",
7465
- reputation_mode: config.reputation.mode,
7466
- attestation_format: config.reputation.attestation_format,
7467
- reputation_portable: true
7468
- }
7469
- },
7470
- capabilities: {
7471
- handshake: true,
7472
- shr_exchange: true,
7473
- reputation_verify: true,
7474
- encrypted_channel: false
7475
- // Not yet implemented
7476
- },
7477
- degradations
7478
- };
7479
- const canonical = canonicalizeForSigning(body);
7480
- const payload = stringToBytes(canonical);
7481
- const encryptionKey = derivePurposeKey(masterKey, "identity-encryption");
7482
- const signatureBytes = sign(
7483
- payload,
7484
- identity.encrypted_private_key,
7485
- encryptionKey
7486
- );
7487
- return {
7488
- body,
7489
- signed_by: identity.public_key,
7490
- signature: toBase64url(signatureBytes)
7491
- };
7492
- }
7493
-
7494
7932
  // src/shr/verifier.ts
7495
7933
  init_encoding();
7496
7934
  function verifySHR(shr, now) {
@@ -8080,6 +8518,154 @@ function deriveTrustTier(level) {
8080
8518
  }
8081
8519
  }
8082
8520
 
8521
+ // src/handshake/attestation.ts
8522
+ init_encoding();
8523
+ init_encoding();
8524
+ var ATTESTATION_VERSION = "1.0";
8525
+ function deriveTrustTier2(level) {
8526
+ switch (level) {
8527
+ case "full":
8528
+ return "verified-sovereign";
8529
+ case "degraded":
8530
+ return "verified-degraded";
8531
+ default:
8532
+ return "unverified";
8533
+ }
8534
+ }
8535
+ function generateAttestation(opts) {
8536
+ const {
8537
+ attesterSHR,
8538
+ subjectSHR,
8539
+ verificationResult,
8540
+ mutual = false,
8541
+ identityManager,
8542
+ masterKey,
8543
+ identityId
8544
+ } = opts;
8545
+ const identity = identityId ? identityManager.get(identityId) : identityManager.getDefault();
8546
+ if (!identity) {
8547
+ return { error: "No identity available for signing attestation" };
8548
+ }
8549
+ const now = /* @__PURE__ */ new Date();
8550
+ const attesterExpiry = new Date(attesterSHR.body.expires_at);
8551
+ const subjectExpiry = new Date(subjectSHR.body.expires_at);
8552
+ const earliestExpiry = attesterExpiry < subjectExpiry ? attesterExpiry : subjectExpiry;
8553
+ const sovereigntyLevel = verificationResult.valid ? verificationResult.sovereignty_level : "unverified";
8554
+ const body = {
8555
+ attestation_version: ATTESTATION_VERSION,
8556
+ attester_id: attesterSHR.body.instance_id,
8557
+ subject_id: subjectSHR.body.instance_id,
8558
+ attester_shr: attesterSHR,
8559
+ subject_shr: subjectSHR,
8560
+ verification: {
8561
+ subject_shr_valid: verificationResult.valid,
8562
+ subject_sovereignty_level: sovereigntyLevel,
8563
+ subject_trust_tier: deriveTrustTier2(sovereigntyLevel),
8564
+ mutual,
8565
+ errors: verificationResult.errors,
8566
+ warnings: verificationResult.warnings
8567
+ },
8568
+ attested_at: now.toISOString(),
8569
+ expires_at: earliestExpiry.toISOString()
8570
+ };
8571
+ const canonical = JSON.stringify(deepSortKeys(body));
8572
+ const payload = stringToBytes(canonical);
8573
+ const encryptionKey = derivePurposeKey(masterKey, "identity-encryption");
8574
+ const signatureBytes = sign(
8575
+ payload,
8576
+ identity.encrypted_private_key,
8577
+ encryptionKey
8578
+ );
8579
+ const summary = generateSummary(body);
8580
+ return {
8581
+ body,
8582
+ signed_by: identity.public_key,
8583
+ signature: toBase64url(signatureBytes),
8584
+ summary
8585
+ };
8586
+ }
8587
+ function layerLine(label, status) {
8588
+ const icon = status === "active" ? "\u2713" : status === "degraded" ? "~" : "x";
8589
+ return ` ${icon} ${label}: ${status}`;
8590
+ }
8591
+ function generateSummary(body) {
8592
+ const v = body.verification;
8593
+ const sLayers = body.subject_shr.body.layers;
8594
+ const aLayers = body.attester_shr.body.layers;
8595
+ const tierLabel = v.subject_trust_tier === "verified-sovereign" ? "Verified Sovereign" : v.subject_trust_tier === "verified-degraded" ? "Verified (Degraded)" : "Unverified";
8596
+ const lines = [
8597
+ `--- Sovereignty Attestation ---`,
8598
+ ``,
8599
+ `Attester: ${body.attester_id.slice(0, 16)}...`,
8600
+ `Subject: ${body.subject_id.slice(0, 16)}...`,
8601
+ `Result: ${tierLabel}`,
8602
+ ``,
8603
+ `Subject Sovereignty Posture:`,
8604
+ layerLine("L1 Cognitive Sovereignty", sLayers.l1.status),
8605
+ layerLine("L2 Operational Isolation", sLayers.l2.status),
8606
+ layerLine("L3 Selective Disclosure", sLayers.l3.status),
8607
+ layerLine("L4 Verifiable Reputation", sLayers.l4.status),
8608
+ ``,
8609
+ `Attester Sovereignty Posture:`,
8610
+ layerLine("L1 Cognitive Sovereignty", aLayers.l1.status),
8611
+ layerLine("L2 Operational Isolation", aLayers.l2.status),
8612
+ layerLine("L3 Selective Disclosure", aLayers.l3.status),
8613
+ layerLine("L4 Verifiable Reputation", aLayers.l4.status),
8614
+ ``,
8615
+ `Mutual: ${v.mutual ? "Yes" : "One-sided"}`,
8616
+ `Attested: ${body.attested_at}`,
8617
+ `Expires: ${body.expires_at}`,
8618
+ `Signature: ${body.attestation_version} / Ed25519`
8619
+ ];
8620
+ if (v.warnings.length > 0) {
8621
+ lines.push(``, `Warnings: ${v.warnings.join("; ")}`);
8622
+ }
8623
+ if (v.errors.length > 0) {
8624
+ lines.push(``, `Errors: ${v.errors.join("; ")}`);
8625
+ }
8626
+ lines.push(``, `--- Verify: compare signed_by against attester's known public key ---`);
8627
+ return lines.join("\n");
8628
+ }
8629
+ function verifyAttestation(attestation, now) {
8630
+ const errors = [];
8631
+ const currentTime = now ?? /* @__PURE__ */ new Date();
8632
+ if (attestation.body.attestation_version !== ATTESTATION_VERSION) {
8633
+ errors.push(
8634
+ `Unsupported attestation version: ${attestation.body.attestation_version}`
8635
+ );
8636
+ }
8637
+ if (!attestation.body.attester_id || !attestation.body.subject_id) {
8638
+ errors.push("Missing attester_id or subject_id");
8639
+ }
8640
+ if (!attestation.body.attester_shr || !attestation.body.subject_shr) {
8641
+ errors.push("Missing attester or subject SHR");
8642
+ }
8643
+ const expired = new Date(attestation.body.expires_at) <= currentTime;
8644
+ if (expired) {
8645
+ errors.push("Attestation has expired");
8646
+ }
8647
+ try {
8648
+ const publicKey = fromBase64url(attestation.signed_by);
8649
+ const canonical = JSON.stringify(deepSortKeys(attestation.body));
8650
+ const payload = stringToBytes(canonical);
8651
+ const signatureBytes = fromBase64url(attestation.signature);
8652
+ const signatureValid = verify(payload, signatureBytes, publicKey);
8653
+ if (!signatureValid) {
8654
+ errors.push("Attestation signature is invalid");
8655
+ }
8656
+ } catch (e) {
8657
+ errors.push(`Signature verification error: ${e.message}`);
8658
+ }
8659
+ return {
8660
+ valid: errors.length === 0,
8661
+ errors,
8662
+ attester_id: attestation.body.attester_id ?? "unknown",
8663
+ subject_id: attestation.body.subject_id ?? "unknown",
8664
+ trust_tier: errors.length === 0 ? attestation.body.verification.subject_trust_tier : "unverified",
8665
+ expired
8666
+ };
8667
+ }
8668
+
8083
8669
  // src/handshake/tools.ts
8084
8670
  function createHandshakeTools(config, identityManager, masterKey, auditLog) {
8085
8671
  const sessions = /* @__PURE__ */ new Map();
@@ -8265,6 +8851,103 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
8265
8851
  result: session.result ?? null
8266
8852
  });
8267
8853
  }
8854
+ },
8855
+ // ─── Streamlined Exchange ─────────────────────────────────────────
8856
+ {
8857
+ name: "sanctuary/handshake_exchange",
8858
+ 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).",
8859
+ inputSchema: {
8860
+ type: "object",
8861
+ properties: {
8862
+ counterparty_shr: {
8863
+ type: "object",
8864
+ description: "The counterparty's signed SHR (SignedSHR object with body, signed_by, signature)."
8865
+ },
8866
+ identity_id: {
8867
+ type: "string",
8868
+ description: "Identity to use for the exchange. Defaults to primary identity."
8869
+ }
8870
+ },
8871
+ required: ["counterparty_shr"]
8872
+ },
8873
+ handler: async (args) => {
8874
+ const counterpartySHR = args.counterparty_shr;
8875
+ const ourSHR = generateSHR(args.identity_id, shrOpts);
8876
+ if (typeof ourSHR === "string") {
8877
+ return toolResult({ error: ourSHR });
8878
+ }
8879
+ const verificationResult = verifySHR(counterpartySHR);
8880
+ const attestation = generateAttestation({
8881
+ attesterSHR: ourSHR,
8882
+ subjectSHR: counterpartySHR,
8883
+ verificationResult,
8884
+ mutual: false,
8885
+ identityManager,
8886
+ masterKey,
8887
+ identityId: args.identity_id
8888
+ });
8889
+ if ("error" in attestation) {
8890
+ auditLog.append("l4", "handshake_exchange", ourSHR.body.instance_id, void 0, "failure");
8891
+ return toolResult({ error: attestation.error });
8892
+ }
8893
+ if (verificationResult.valid) {
8894
+ const sovereigntyLevel = verificationResult.sovereignty_level;
8895
+ const trustTier = sovereigntyLevel === "full" ? "verified-sovereign" : sovereigntyLevel === "degraded" ? "verified-degraded" : "unverified";
8896
+ handshakeResults.set(verificationResult.counterparty_id, {
8897
+ counterparty_id: verificationResult.counterparty_id,
8898
+ counterparty_shr: counterpartySHR,
8899
+ verified: true,
8900
+ sovereignty_level: sovereigntyLevel,
8901
+ trust_tier: trustTier,
8902
+ completed_at: (/* @__PURE__ */ new Date()).toISOString(),
8903
+ expires_at: verificationResult.expires_at,
8904
+ errors: []
8905
+ });
8906
+ }
8907
+ auditLog.append("l4", "handshake_exchange", ourSHR.body.instance_id);
8908
+ return toolResult({
8909
+ attestation,
8910
+ our_shr: ourSHR,
8911
+ verification: {
8912
+ counterparty_valid: verificationResult.valid,
8913
+ counterparty_sovereignty: verificationResult.sovereignty_level,
8914
+ counterparty_id: verificationResult.counterparty_id,
8915
+ errors: verificationResult.errors,
8916
+ warnings: verificationResult.warnings
8917
+ },
8918
+ 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.",
8919
+ _content_trust: "external"
8920
+ });
8921
+ }
8922
+ },
8923
+ {
8924
+ name: "sanctuary/handshake_verify_attestation",
8925
+ description: "Verify a signed attestation artifact from another agent. Checks the Ed25519 signature, temporal validity, and structural integrity.",
8926
+ inputSchema: {
8927
+ type: "object",
8928
+ properties: {
8929
+ attestation: {
8930
+ type: "object",
8931
+ description: "The SignedAttestation object to verify (body, signed_by, signature, summary)."
8932
+ }
8933
+ },
8934
+ required: ["attestation"]
8935
+ },
8936
+ handler: async (args) => {
8937
+ const attestation = args.attestation;
8938
+ const result = verifyAttestation(attestation);
8939
+ auditLog.append(
8940
+ "l4",
8941
+ "handshake_verify_attestation",
8942
+ result.attester_id,
8943
+ void 0,
8944
+ result.valid ? "success" : "failure"
8945
+ );
8946
+ return toolResult({
8947
+ ...result,
8948
+ _content_trust: "external"
8949
+ });
8950
+ }
8268
8951
  }
8269
8952
  ];
8270
8953
  return { tools, handshakeResults };
@@ -12116,7 +12799,15 @@ async function createSanctuaryServer(options) {
12116
12799
  tls: config.dashboard.tls,
12117
12800
  auto_open: config.dashboard.auto_open
12118
12801
  });
12119
- dashboard.setDependencies({ policy, baseline, auditLog });
12802
+ dashboard.setDependencies({
12803
+ policy,
12804
+ baseline,
12805
+ auditLog,
12806
+ identityManager,
12807
+ handshakeResults,
12808
+ shrOpts: { config, identityManager, masterKey },
12809
+ sanctuaryConfig: config
12810
+ });
12120
12811
  await dashboard.start();
12121
12812
  approvalChannel = dashboard;
12122
12813
  } else if (config.webhook.enabled && config.webhook.url && config.webhook.secret) {
@@ -12222,6 +12913,7 @@ async function createSanctuaryServer(options) {
12222
12913
  return { server, config };
12223
12914
  }
12224
12915
 
12916
+ exports.ATTESTATION_VERSION = ATTESTATION_VERSION;
12225
12917
  exports.ApprovalGate = ApprovalGate;
12226
12918
  exports.AuditLog = AuditLog;
12227
12919
  exports.AutoApproveChannel = AutoApproveChannel;
@@ -12253,6 +12945,7 @@ exports.createRangeProof = createRangeProof;
12253
12945
  exports.createSanctuaryServer = createSanctuaryServer;
12254
12946
  exports.evaluateField = evaluateField;
12255
12947
  exports.filterContext = filterContext;
12948
+ exports.generateAttestation = generateAttestation;
12256
12949
  exports.generateSHR = generateSHR;
12257
12950
  exports.getTemplate = getTemplate;
12258
12951
  exports.initiateHandshake = initiateHandshake;
@@ -12264,6 +12957,7 @@ exports.resolveTier = resolveTier;
12264
12957
  exports.respondToHandshake = respondToHandshake;
12265
12958
  exports.signPayload = signPayload;
12266
12959
  exports.tierDistribution = tierDistribution;
12960
+ exports.verifyAttestation = verifyAttestation;
12267
12961
  exports.verifyBridgeCommitment = verifyBridgeCommitment;
12268
12962
  exports.verifyCompletion = verifyCompletion;
12269
12963
  exports.verifyPedersenCommitment = verifyPedersenCommitment;