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