@sanctuary-framework/mcp-server 0.5.5 → 0.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -3638,6 +3638,8 @@ var DEFAULT_POLICY = {
3638
3638
  "handshake_respond",
3639
3639
  "handshake_complete",
3640
3640
  "handshake_status",
3641
+ "handshake_exchange",
3642
+ "handshake_verify_attestation",
3641
3643
  "reputation_query_weighted",
3642
3644
  "federation_peers",
3643
3645
  "federation_trust_evaluate",
@@ -3807,6 +3809,8 @@ tier3_always_allow:
3807
3809
  - handshake_respond
3808
3810
  - handshake_complete
3809
3811
  - handshake_status
3812
+ - handshake_exchange
3813
+ - handshake_verify_attestation
3810
3814
  - reputation_query_weighted
3811
3815
  - federation_peers
3812
3816
  - federation_trust_evaluate
@@ -4068,174 +4072,255 @@ function generateLoginHTML(options) {
4068
4072
  return `<!DOCTYPE html>
4069
4073
  <html lang="en">
4070
4074
  <head>
4071
- <meta charset="utf-8">
4072
- <meta name="viewport" content="width=device-width, initial-scale=1">
4073
- <title>Sanctuary \u2014 Login</title>
4074
- <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
4075
- <style>
4076
- :root {
4077
- --bg: #0d1117;
4078
- --surface: #161b22;
4079
- --border: #30363d;
4080
- --text-primary: #e6edf3;
4081
- --text-secondary: #8b949e;
4082
- --green: #3fb950;
4083
- --red: #f85149;
4084
- --blue: #58a6ff;
4085
- --mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
4086
- --sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
4087
- --radius: 6px;
4088
- }
4089
- * { box-sizing: border-box; margin: 0; padding: 0; }
4090
- html, body { width: 100%; height: 100%; }
4091
- body {
4092
- font-family: var(--sans);
4093
- background: var(--bg);
4094
- color: var(--text-primary);
4095
- display: flex;
4096
- align-items: center;
4097
- justify-content: center;
4098
- }
4099
- .login-container {
4100
- width: 100%;
4101
- max-width: 400px;
4102
- padding: 40px 32px;
4103
- background: var(--surface);
4104
- border: 1px solid var(--border);
4105
- border-radius: 12px;
4106
- }
4107
- .login-logo {
4108
- text-align: center;
4109
- font-size: 20px;
4110
- font-weight: 700;
4111
- letter-spacing: -0.5px;
4112
- margin-bottom: 8px;
4113
- }
4114
- .login-logo span { color: var(--blue); }
4115
- .login-version {
4116
- text-align: center;
4117
- font-size: 11px;
4118
- color: var(--text-secondary);
4119
- font-family: var(--mono);
4120
- margin-bottom: 32px;
4121
- }
4122
- .login-label {
4123
- display: block;
4124
- font-size: 13px;
4125
- font-weight: 600;
4126
- color: var(--text-secondary);
4127
- margin-bottom: 8px;
4128
- }
4129
- .login-input {
4130
- width: 100%;
4131
- padding: 10px 14px;
4132
- background: var(--bg);
4133
- border: 1px solid var(--border);
4134
- border-radius: var(--radius);
4135
- color: var(--text-primary);
4136
- font-family: var(--mono);
4137
- font-size: 14px;
4138
- outline: none;
4139
- transition: border-color 0.15s;
4140
- }
4141
- .login-input:focus { border-color: var(--blue); }
4142
- .login-input::placeholder { color: var(--text-secondary); opacity: 0.5; }
4143
- .login-btn {
4144
- width: 100%;
4145
- margin-top: 20px;
4146
- padding: 10px;
4147
- background: var(--blue);
4148
- color: var(--bg);
4149
- border: none;
4150
- border-radius: var(--radius);
4151
- font-size: 14px;
4152
- font-weight: 600;
4153
- cursor: pointer;
4154
- transition: opacity 0.15s;
4155
- font-family: var(--sans);
4156
- }
4157
- .login-btn:hover { opacity: 0.9; }
4158
- .login-btn:disabled { opacity: 0.5; cursor: not-allowed; }
4159
- .login-error {
4160
- margin-top: 16px;
4161
- padding: 10px 14px;
4162
- background: rgba(248, 81, 73, 0.1);
4163
- border: 1px solid var(--red);
4164
- border-radius: var(--radius);
4165
- font-size: 12px;
4166
- color: var(--red);
4167
- display: none;
4168
- }
4169
- .login-hint {
4170
- margin-top: 24px;
4171
- padding-top: 16px;
4172
- border-top: 1px solid var(--border);
4173
- font-size: 11px;
4174
- color: var(--text-secondary);
4175
- line-height: 1.5;
4176
- }
4177
- .login-hint code {
4178
- font-family: var(--mono);
4179
- background: var(--bg);
4180
- padding: 1px 4px;
4181
- border-radius: 3px;
4182
- font-size: 10px;
4183
- }
4184
- </style>
4075
+ <meta charset="UTF-8">
4076
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
4077
+ <title>Sanctuary Dashboard</title>
4078
+ <style>
4079
+ :root {
4080
+ --bg: #0d1117;
4081
+ --surface: #161b22;
4082
+ --border: #30363d;
4083
+ --text-primary: #e6edf3;
4084
+ --text-secondary: #8b949e;
4085
+ --green: #3fb950;
4086
+ --amber: #d29922;
4087
+ --red: #f85149;
4088
+ --blue: #58a6ff;
4089
+ }
4090
+
4091
+ * {
4092
+ margin: 0;
4093
+ padding: 0;
4094
+ box-sizing: border-box;
4095
+ }
4096
+
4097
+ body {
4098
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
4099
+ background-color: var(--bg);
4100
+ color: var(--text-primary);
4101
+ min-height: 100vh;
4102
+ display: flex;
4103
+ align-items: center;
4104
+ justify-content: center;
4105
+ }
4106
+
4107
+ .login-container {
4108
+ width: 100%;
4109
+ max-width: 400px;
4110
+ padding: 20px;
4111
+ }
4112
+
4113
+ .login-card {
4114
+ background-color: var(--surface);
4115
+ border: 1px solid var(--border);
4116
+ border-radius: 8px;
4117
+ padding: 40px 32px;
4118
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
4119
+ }
4120
+
4121
+ .login-header {
4122
+ display: flex;
4123
+ align-items: center;
4124
+ gap: 12px;
4125
+ margin-bottom: 32px;
4126
+ }
4127
+
4128
+ .logo {
4129
+ font-size: 24px;
4130
+ font-weight: 700;
4131
+ color: var(--blue);
4132
+ }
4133
+
4134
+ .logo-text {
4135
+ display: flex;
4136
+ flex-direction: column;
4137
+ }
4138
+
4139
+ .logo-text .title {
4140
+ font-size: 18px;
4141
+ font-weight: 600;
4142
+ letter-spacing: -0.5px;
4143
+ }
4144
+
4145
+ .logo-text .version {
4146
+ font-size: 12px;
4147
+ color: var(--text-secondary);
4148
+ margin-top: 2px;
4149
+ }
4150
+
4151
+ .form-group {
4152
+ margin-bottom: 24px;
4153
+ }
4154
+
4155
+ label {
4156
+ display: block;
4157
+ font-size: 14px;
4158
+ font-weight: 500;
4159
+ margin-bottom: 8px;
4160
+ color: var(--text-primary);
4161
+ }
4162
+
4163
+ input[type="text"],
4164
+ input[type="password"] {
4165
+ width: 100%;
4166
+ padding: 10px 12px;
4167
+ background-color: var(--bg);
4168
+ border: 1px solid var(--border);
4169
+ border-radius: 6px;
4170
+ color: var(--text-primary);
4171
+ font-size: 14px;
4172
+ font-family: 'JetBrains Mono', monospace;
4173
+ transition: border-color 0.2s;
4174
+ }
4175
+
4176
+ input[type="text"]:focus,
4177
+ input[type="password"]:focus {
4178
+ outline: none;
4179
+ border-color: var(--blue);
4180
+ box-shadow: 0 0 0 2px rgba(88, 166, 255, 0.1);
4181
+ }
4182
+
4183
+ .error-message {
4184
+ display: none;
4185
+ background-color: rgba(248, 81, 73, 0.1);
4186
+ border: 1px solid var(--red);
4187
+ color: #ff9999;
4188
+ padding: 12px;
4189
+ border-radius: 6px;
4190
+ font-size: 13px;
4191
+ margin-bottom: 20px;
4192
+ }
4193
+
4194
+ .error-message.show {
4195
+ display: block;
4196
+ }
4197
+
4198
+ button {
4199
+ width: 100%;
4200
+ padding: 10px 16px;
4201
+ background-color: var(--blue);
4202
+ color: var(--bg);
4203
+ border: none;
4204
+ border-radius: 6px;
4205
+ font-size: 14px;
4206
+ font-weight: 600;
4207
+ cursor: pointer;
4208
+ transition: background-color 0.2s;
4209
+ }
4210
+
4211
+ button:hover {
4212
+ background-color: #79c0ff;
4213
+ }
4214
+
4215
+ button:active {
4216
+ background-color: #4184e4;
4217
+ }
4218
+
4219
+ button:disabled {
4220
+ background-color: var(--text-secondary);
4221
+ cursor: not-allowed;
4222
+ opacity: 0.5;
4223
+ }
4224
+
4225
+ .info-text {
4226
+ font-size: 12px;
4227
+ color: var(--text-secondary);
4228
+ margin-top: 16px;
4229
+ text-align: center;
4230
+ }
4231
+ </style>
4185
4232
  </head>
4186
4233
  <body>
4187
- <div class="login-container">
4188
- <div class="login-logo"><span>&#9670;</span> SANCTUARY</div>
4189
- <div class="login-version">Principal Dashboard v${options.serverVersion}</div>
4190
- <form id="loginForm" onsubmit="return handleLogin(event)">
4191
- <label class="login-label" for="tokenInput">Dashboard Auth Token</label>
4192
- <input class="login-input" type="password" id="tokenInput"
4193
- placeholder="Enter your auth token" autocomplete="off" autofocus required>
4194
- <button class="login-btn" type="submit" id="loginBtn">Open Dashboard</button>
4195
- </form>
4196
- <div class="login-error" id="loginError"></div>
4197
- <div class="login-hint">
4198
- Your token is set via <code>SANCTUARY_DASHBOARD_AUTH_TOKEN</code> environment variable,
4199
- or check your server's startup output.
4234
+ <div class="login-container">
4235
+ <div class="login-card">
4236
+ <div class="login-header">
4237
+ <div class="logo">\u25C6</div>
4238
+ <div class="logo-text">
4239
+ <div class="title">SANCTUARY</div>
4240
+ <div class="version">v${options.serverVersion}</div>
4241
+ </div>
4242
+ </div>
4243
+
4244
+ <div id="error-message" class="error-message"></div>
4245
+
4246
+ <form id="login-form">
4247
+ <div class="form-group">
4248
+ <label for="auth-token">Bearer Token</label>
4249
+ <input
4250
+ type="text"
4251
+ id="auth-token"
4252
+ name="token"
4253
+ placeholder="Paste your session token..."
4254
+ autocomplete="off"
4255
+ spellcheck="false"
4256
+ required
4257
+ />
4258
+ </div>
4259
+
4260
+ <button type="submit" id="login-button">Open Dashboard</button>
4261
+ </form>
4262
+
4263
+ <div class="info-text">
4264
+ Session tokens expire after 1 hour of inactivity
4265
+ </div>
4266
+ </div>
4200
4267
  </div>
4201
- </div>
4202
- <script>
4203
- async function handleLogin(e) {
4204
- e.preventDefault();
4205
- var btn = document.getElementById('loginBtn');
4206
- var errEl = document.getElementById('loginError');
4207
- var token = document.getElementById('tokenInput').value.trim();
4208
- if (!token) return false;
4209
- btn.disabled = true;
4210
- btn.textContent = 'Authenticating...';
4211
- errEl.style.display = 'none';
4212
- try {
4213
- var resp = await fetch('/auth/session', {
4214
- method: 'POST',
4215
- headers: { 'Authorization': 'Bearer ' + token }
4268
+
4269
+ <script>
4270
+ const loginForm = document.getElementById('login-form');
4271
+ const authTokenInput = document.getElementById('auth-token');
4272
+ const errorMessage = document.getElementById('error-message');
4273
+ const loginButton = document.getElementById('login-button');
4274
+
4275
+ loginForm.addEventListener('submit', async (e) => {
4276
+ e.preventDefault();
4277
+ const token = authTokenInput.value.trim();
4278
+
4279
+ if (!token) {
4280
+ showError('Token is required');
4281
+ return;
4282
+ }
4283
+
4284
+ loginButton.disabled = true;
4285
+ loginButton.textContent = 'Verifying...';
4286
+ errorMessage.classList.remove('show');
4287
+
4288
+ try {
4289
+ const response = await fetch('/auth/session', {
4290
+ method: 'POST',
4291
+ headers: {
4292
+ 'Content-Type': 'application/json',
4293
+ 'Authorization': 'Bearer ' + token,
4294
+ },
4295
+ body: JSON.stringify({ token }),
4296
+ });
4297
+
4298
+ if (response.ok) {
4299
+ const data = await response.json();
4300
+ sessionStorage.setItem('authToken', token);
4301
+ window.location.href = '/dashboard';
4302
+ } else if (response.status === 401) {
4303
+ showError('Invalid token. Please check and try again.');
4304
+ } else {
4305
+ showError('Authentication failed. Please try again.');
4306
+ }
4307
+ } catch (err) {
4308
+ showError('Connection error. Please check your network.');
4309
+ } finally {
4310
+ loginButton.disabled = false;
4311
+ loginButton.textContent = 'Open Dashboard';
4312
+ }
4216
4313
  });
4217
- if (!resp.ok) {
4218
- var data = await resp.json().catch(function() { return {}; });
4219
- throw new Error(data.error || 'Authentication failed');
4220
- }
4221
- var result = await resp.json();
4222
- // Store token in sessionStorage for auto-renewal inside the dashboard
4223
- try { sessionStorage.setItem('sanctuary_token', token); } catch(_) {}
4224
- // Set session cookie
4225
- var maxAge = result.expires_in_seconds || 300;
4226
- document.cookie = 'sanctuary_session=' + result.session_id +
4227
- '; path=/; SameSite=Strict; max-age=' + maxAge;
4228
- // Reload to enter the dashboard
4229
- window.location.reload();
4230
- } catch (err) {
4231
- errEl.textContent = err.message || 'Authentication failed. Check your token.';
4232
- errEl.style.display = 'block';
4233
- btn.disabled = false;
4234
- btn.textContent = 'Open Dashboard';
4235
- }
4236
- return false;
4237
- }
4238
- </script>
4314
+
4315
+ function showError(message) {
4316
+ errorMessage.textContent = message;
4317
+ errorMessage.classList.add('show');
4318
+ }
4319
+
4320
+ authTokenInput.addEventListener('input', () => {
4321
+ errorMessage.classList.remove('show');
4322
+ });
4323
+ </script>
4239
4324
  </body>
4240
4325
  </html>`;
4241
4326
  }
@@ -4243,1412 +4328,1648 @@ function generateDashboardHTML(options) {
4243
4328
  return `<!DOCTYPE html>
4244
4329
  <html lang="en">
4245
4330
  <head>
4246
- <meta charset="utf-8">
4247
- <meta name="viewport" content="width=device-width, initial-scale=1">
4248
- <title>Sanctuary \u2014 Principal Dashboard</title>
4249
- <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
4250
- <style>
4251
- :root {
4252
- --bg: #0d1117;
4253
- --surface: #161b22;
4254
- --border: #30363d;
4255
- --text-primary: #e6edf3;
4256
- --text-secondary: #8b949e;
4257
- --green: #3fb950;
4258
- --amber: #d29922;
4259
- --red: #f85149;
4260
- --blue: #58a6ff;
4261
- --mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
4262
- --sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
4263
- --radius: 6px;
4264
- }
4265
-
4266
- * {
4267
- box-sizing: border-box;
4268
- margin: 0;
4269
- padding: 0;
4270
- }
4331
+ <meta charset="UTF-8">
4332
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
4333
+ <title>Sanctuary Dashboard</title>
4334
+ <style>
4335
+ :root {
4336
+ --bg: #0d1117;
4337
+ --surface: #161b22;
4338
+ --border: #30363d;
4339
+ --text-primary: #e6edf3;
4340
+ --text-secondary: #8b949e;
4341
+ --green: #3fb950;
4342
+ --amber: #d29922;
4343
+ --red: #f85149;
4344
+ --blue: #58a6ff;
4345
+ --success: #3fb950;
4346
+ --warning: #d29922;
4347
+ --error: #f85149;
4348
+ --muted: #21262d;
4349
+ }
4350
+
4351
+ * {
4352
+ margin: 0;
4353
+ padding: 0;
4354
+ box-sizing: border-box;
4355
+ }
4356
+
4357
+ html, body {
4358
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
4359
+ background-color: var(--bg);
4360
+ color: var(--text-primary);
4361
+ height: 100%;
4362
+ overflow: hidden;
4363
+ }
4364
+
4365
+ body {
4366
+ display: flex;
4367
+ flex-direction: column;
4368
+ }
4369
+
4370
+ /* Status Bar */
4371
+ .status-bar {
4372
+ position: fixed;
4373
+ top: 0;
4374
+ left: 0;
4375
+ right: 0;
4376
+ height: 56px;
4377
+ background-color: var(--surface);
4378
+ border-bottom: 1px solid var(--border);
4379
+ display: flex;
4380
+ align-items: center;
4381
+ padding: 0 24px;
4382
+ gap: 24px;
4383
+ z-index: 100;
4384
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
4385
+ }
4386
+
4387
+ .status-bar-left {
4388
+ display: flex;
4389
+ align-items: center;
4390
+ gap: 12px;
4391
+ flex: 0 0 auto;
4392
+ }
4271
4393
 
4272
- html, body {
4273
- width: 100%;
4274
- height: 100%;
4275
- overflow: hidden;
4276
- }
4277
-
4278
- body {
4279
- font-family: var(--sans);
4280
- background: var(--bg);
4281
- color: var(--text-primary);
4282
- display: flex;
4283
- flex-direction: column;
4284
- }
4285
-
4286
- /* \u2500\u2500 Top Status Bar (fixed) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
4394
+ .logo-icon {
4395
+ font-size: 20px;
4396
+ color: var(--blue);
4397
+ font-weight: 700;
4398
+ }
4287
4399
 
4288
- .status-bar {
4289
- position: fixed;
4290
- top: 0;
4291
- left: 0;
4292
- right: 0;
4293
- height: 56px;
4294
- background: var(--surface);
4295
- border-bottom: 1px solid var(--border);
4296
- display: flex;
4297
- align-items: center;
4298
- padding: 0 20px;
4299
- gap: 24px;
4300
- z-index: 1000;
4301
- }
4400
+ .logo-info {
4401
+ display: flex;
4402
+ flex-direction: column;
4403
+ }
4302
4404
 
4303
- .status-bar-left {
4304
- display: flex;
4305
- align-items: center;
4306
- gap: 12px;
4307
- flex: 0 0 auto;
4308
- }
4405
+ .logo-title {
4406
+ font-size: 13px;
4407
+ font-weight: 600;
4408
+ line-height: 1;
4409
+ color: var(--text-primary);
4410
+ }
4309
4411
 
4310
- .sanctuary-logo {
4311
- font-weight: 700;
4312
- font-size: 16px;
4313
- letter-spacing: -0.5px;
4314
- color: var(--text-primary);
4315
- }
4412
+ .logo-version {
4413
+ font-size: 11px;
4414
+ color: var(--text-secondary);
4415
+ margin-top: 2px;
4416
+ }
4316
4417
 
4317
- .sanctuary-logo span {
4318
- color: var(--blue);
4319
- }
4418
+ .status-bar-center {
4419
+ flex: 1;
4420
+ display: flex;
4421
+ justify-content: center;
4422
+ }
4320
4423
 
4321
- .version {
4322
- font-size: 11px;
4323
- color: var(--text-secondary);
4324
- font-family: var(--mono);
4325
- }
4424
+ .sovereignty-badge {
4425
+ display: flex;
4426
+ align-items: center;
4427
+ gap: 8px;
4428
+ padding: 8px 16px;
4429
+ background-color: rgba(63, 185, 80, 0.1);
4430
+ border: 1px solid rgba(63, 185, 80, 0.3);
4431
+ border-radius: 6px;
4432
+ font-size: 13px;
4433
+ font-weight: 500;
4434
+ }
4326
4435
 
4327
- .status-bar-center {
4328
- flex: 1;
4329
- display: flex;
4330
- align-items: center;
4331
- justify-content: center;
4332
- }
4333
-
4334
- .sovereignty-badge {
4335
- display: flex;
4336
- align-items: center;
4337
- gap: 8px;
4338
- padding: 6px 12px;
4339
- background: rgba(88, 166, 255, 0.1);
4340
- border: 1px solid var(--blue);
4341
- border-radius: 20px;
4342
- font-size: 13px;
4343
- font-weight: 600;
4344
- }
4436
+ .sovereignty-badge.degraded {
4437
+ background-color: rgba(210, 153, 34, 0.1);
4438
+ border-color: rgba(210, 153, 34, 0.3);
4439
+ }
4345
4440
 
4346
- .sovereignty-score {
4347
- display: flex;
4348
- align-items: center;
4349
- justify-content: center;
4350
- width: 28px;
4351
- height: 28px;
4352
- border-radius: 50%;
4353
- font-family: var(--mono);
4354
- font-weight: 700;
4355
- font-size: 12px;
4356
- background: var(--blue);
4357
- color: var(--bg);
4358
- }
4441
+ .sovereignty-badge.inactive {
4442
+ background-color: rgba(248, 81, 73, 0.1);
4443
+ border-color: rgba(248, 81, 73, 0.3);
4444
+ }
4359
4445
 
4360
- .sovereignty-score.high {
4361
- background: var(--green);
4362
- }
4446
+ .sovereignty-score {
4447
+ font-weight: 700;
4448
+ color: var(--green);
4449
+ }
4363
4450
 
4364
- .sovereignty-score.medium {
4365
- background: var(--amber);
4366
- }
4451
+ .sovereignty-badge.degraded .sovereignty-score {
4452
+ color: var(--amber);
4453
+ }
4367
4454
 
4368
- .sovereignty-score.low {
4369
- background: var(--red);
4370
- }
4455
+ .sovereignty-badge.inactive .sovereignty-score {
4456
+ color: var(--red);
4457
+ }
4371
4458
 
4372
- .status-bar-right {
4373
- display: flex;
4374
- align-items: center;
4375
- gap: 16px;
4376
- flex: 0 0 auto;
4377
- }
4459
+ .status-bar-right {
4460
+ display: flex;
4461
+ align-items: center;
4462
+ gap: 16px;
4463
+ flex: 0 0 auto;
4464
+ }
4378
4465
 
4379
- .protections-indicator {
4380
- display: flex;
4381
- align-items: center;
4382
- gap: 6px;
4383
- font-size: 12px;
4384
- color: var(--text-secondary);
4385
- font-family: var(--mono);
4386
- }
4466
+ .status-item {
4467
+ display: flex;
4468
+ align-items: center;
4469
+ gap: 6px;
4470
+ font-size: 12px;
4471
+ color: var(--text-secondary);
4472
+ }
4387
4473
 
4388
- .protections-indicator .count {
4389
- color: var(--text-primary);
4390
- font-weight: 600;
4391
- }
4474
+ .status-item strong {
4475
+ color: var(--text-primary);
4476
+ font-weight: 500;
4477
+ }
4392
4478
 
4393
- .uptime {
4394
- display: flex;
4395
- align-items: center;
4396
- gap: 6px;
4397
- font-size: 12px;
4398
- color: var(--text-secondary);
4399
- font-family: var(--mono);
4400
- }
4479
+ .status-dot {
4480
+ width: 8px;
4481
+ height: 8px;
4482
+ border-radius: 50%;
4483
+ background-color: var(--green);
4484
+ }
4401
4485
 
4402
- .status-dot {
4403
- width: 8px;
4404
- height: 8px;
4405
- border-radius: 50%;
4406
- background: var(--green);
4407
- animation: pulse 2s ease-in-out infinite;
4408
- }
4486
+ .status-dot.disconnected {
4487
+ background-color: var(--red);
4488
+ }
4409
4489
 
4410
- .status-dot.disconnected {
4411
- background: var(--red);
4412
- animation: none;
4413
- }
4490
+ .pending-badge {
4491
+ display: flex;
4492
+ align-items: center;
4493
+ gap: 6px;
4494
+ padding: 4px 8px;
4495
+ background-color: var(--blue);
4496
+ color: var(--bg);
4497
+ border-radius: 4px;
4498
+ font-size: 11px;
4499
+ font-weight: 600;
4500
+ cursor: pointer;
4501
+ }
4414
4502
 
4415
- @keyframes pulse {
4416
- 0%, 100% { opacity: 1; }
4417
- 50% { opacity: 0.5; }
4418
- }
4503
+ /* Main Content */
4504
+ .main-content {
4505
+ flex: 1;
4506
+ margin-top: 56px;
4507
+ overflow-y: auto;
4508
+ padding: 24px;
4509
+ }
4419
4510
 
4420
- .pending-badge {
4421
- display: inline-flex;
4422
- align-items: center;
4423
- justify-content: center;
4424
- min-width: 24px;
4425
- height: 24px;
4426
- padding: 0 6px;
4427
- background: var(--red);
4428
- color: white;
4429
- border-radius: 12px;
4430
- font-size: 11px;
4431
- font-weight: 700;
4432
- animation: pulse 1s ease-in-out infinite;
4433
- }
4511
+ .grid {
4512
+ display: grid;
4513
+ gap: 20px;
4514
+ }
4434
4515
 
4435
- .pending-badge.hidden {
4436
- display: none;
4437
- }
4516
+ /* Row 1: Sovereignty Layers */
4517
+ .sovereignty-layers {
4518
+ display: grid;
4519
+ grid-template-columns: repeat(4, 1fr);
4520
+ gap: 16px;
4521
+ }
4438
4522
 
4439
- /* \u2500\u2500 Main Layout \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
4523
+ .layer-card {
4524
+ background-color: var(--surface);
4525
+ border: 1px solid var(--border);
4526
+ border-radius: 8px;
4527
+ padding: 20px;
4528
+ display: flex;
4529
+ flex-direction: column;
4530
+ gap: 12px;
4531
+ }
4440
4532
 
4441
- .main-container {
4442
- flex: 1;
4443
- display: flex;
4444
- margin-top: 56px;
4445
- overflow: hidden;
4446
- }
4533
+ .layer-card.degraded {
4534
+ border-color: var(--amber);
4535
+ background-color: rgba(210, 153, 34, 0.05);
4536
+ }
4447
4537
 
4448
- .activity-feed {
4449
- flex: 3;
4450
- display: flex;
4451
- flex-direction: column;
4452
- border-right: 1px solid var(--border);
4453
- overflow: hidden;
4454
- }
4538
+ .layer-card.inactive {
4539
+ border-color: var(--red);
4540
+ background-color: rgba(248, 81, 73, 0.05);
4541
+ }
4455
4542
 
4456
- .feed-header {
4457
- padding: 16px 20px;
4458
- border-bottom: 1px solid var(--border);
4459
- display: flex;
4460
- align-items: center;
4461
- gap: 8px;
4462
- font-size: 12px;
4463
- font-weight: 600;
4464
- text-transform: uppercase;
4465
- letter-spacing: 0.5px;
4466
- color: var(--text-secondary);
4467
- }
4543
+ .layer-name {
4544
+ font-size: 12px;
4545
+ font-weight: 600;
4546
+ color: var(--text-secondary);
4547
+ text-transform: uppercase;
4548
+ letter-spacing: 0.5px;
4549
+ }
4468
4550
 
4469
- .feed-header-dot {
4470
- width: 6px;
4471
- height: 6px;
4472
- border-radius: 50%;
4473
- background: var(--green);
4474
- }
4551
+ .layer-title {
4552
+ font-size: 14px;
4553
+ font-weight: 600;
4554
+ color: var(--text-primary);
4555
+ }
4475
4556
 
4476
- .activity-list {
4477
- flex: 1;
4478
- overflow-y: auto;
4479
- overflow-x: hidden;
4480
- }
4557
+ .layer-status {
4558
+ display: inline-flex;
4559
+ align-items: center;
4560
+ gap: 6px;
4561
+ padding: 4px 8px;
4562
+ background-color: rgba(63, 185, 80, 0.15);
4563
+ color: var(--green);
4564
+ border-radius: 4px;
4565
+ font-size: 11px;
4566
+ font-weight: 600;
4567
+ width: fit-content;
4568
+ }
4481
4569
 
4482
- .activity-item {
4483
- padding: 12px 20px;
4484
- border-bottom: 1px solid rgba(48, 54, 61, 0.5);
4485
- font-size: 13px;
4486
- font-family: var(--mono);
4487
- cursor: pointer;
4488
- transition: background 0.15s;
4489
- display: flex;
4490
- align-items: flex-start;
4491
- gap: 10px;
4492
- }
4570
+ .layer-card.degraded .layer-status {
4571
+ background-color: rgba(210, 153, 34, 0.15);
4572
+ color: var(--amber);
4573
+ }
4493
4574
 
4494
- .activity-item:hover {
4495
- background: rgba(88, 166, 255, 0.05);
4496
- }
4575
+ .layer-card.inactive .layer-status {
4576
+ background-color: rgba(248, 81, 73, 0.15);
4577
+ color: var(--red);
4578
+ }
4497
4579
 
4498
- .activity-item-icon {
4499
- flex: 0 0 auto;
4500
- width: 16px;
4501
- text-align: center;
4502
- font-size: 12px;
4503
- color: var(--text-secondary);
4504
- margin-top: 1px;
4505
- }
4580
+ .layer-detail {
4581
+ font-size: 12px;
4582
+ color: var(--text-secondary);
4583
+ font-family: 'JetBrains Mono', monospace;
4584
+ padding: 8px;
4585
+ background-color: var(--bg);
4586
+ border-radius: 4px;
4587
+ border-left: 2px solid var(--blue);
4588
+ }
4506
4589
 
4507
- .activity-item-content {
4508
- flex: 1;
4509
- min-width: 0;
4510
- }
4590
+ /* Row 2: Info Cards */
4591
+ .info-cards {
4592
+ display: grid;
4593
+ grid-template-columns: repeat(3, 1fr);
4594
+ gap: 16px;
4595
+ }
4511
4596
 
4512
- .activity-time {
4513
- color: var(--text-secondary);
4514
- font-size: 11px;
4515
- margin-bottom: 2px;
4516
- }
4597
+ .info-card {
4598
+ background-color: var(--surface);
4599
+ border: 1px solid var(--border);
4600
+ border-radius: 8px;
4601
+ padding: 20px;
4602
+ }
4517
4603
 
4518
- .activity-main {
4519
- display: flex;
4520
- gap: 8px;
4521
- align-items: baseline;
4522
- margin-bottom: 4px;
4523
- }
4604
+ .card-header {
4605
+ font-size: 12px;
4606
+ font-weight: 600;
4607
+ color: var(--text-secondary);
4608
+ text-transform: uppercase;
4609
+ letter-spacing: 0.5px;
4610
+ margin-bottom: 16px;
4611
+ }
4524
4612
 
4525
- .activity-tier {
4526
- display: inline-flex;
4527
- align-items: center;
4528
- justify-content: center;
4529
- width: 24px;
4530
- height: 16px;
4531
- font-size: 10px;
4532
- font-weight: 700;
4533
- border-radius: 3px;
4534
- text-transform: uppercase;
4535
- flex: 0 0 auto;
4536
- }
4613
+ .card-row {
4614
+ display: flex;
4615
+ justify-content: space-between;
4616
+ align-items: center;
4617
+ margin-bottom: 12px;
4618
+ font-size: 13px;
4619
+ }
4537
4620
 
4538
- .activity-tier.t1 {
4539
- background: rgba(248, 81, 73, 0.2);
4540
- color: var(--red);
4541
- }
4621
+ .card-row:last-child {
4622
+ margin-bottom: 0;
4623
+ }
4542
4624
 
4543
- .activity-tier.t2 {
4544
- background: rgba(210, 153, 34, 0.2);
4545
- color: var(--amber);
4546
- }
4625
+ .card-label {
4626
+ color: var(--text-secondary);
4627
+ }
4547
4628
 
4548
- .activity-tier.t3 {
4549
- background: rgba(63, 185, 80, 0.2);
4550
- color: var(--green);
4551
- }
4629
+ .card-value {
4630
+ color: var(--text-primary);
4631
+ font-family: 'JetBrains Mono', monospace;
4632
+ font-weight: 500;
4633
+ }
4552
4634
 
4553
- .activity-tool {
4554
- color: var(--text-primary);
4555
- font-weight: 600;
4556
- }
4635
+ .identity-badge {
4636
+ display: inline-flex;
4637
+ align-items: center;
4638
+ gap: 4px;
4639
+ padding: 2px 6px;
4640
+ background-color: rgba(88, 166, 255, 0.15);
4641
+ color: var(--blue);
4642
+ border-radius: 3px;
4643
+ font-size: 10px;
4644
+ font-weight: 600;
4645
+ text-transform: uppercase;
4646
+ }
4557
4647
 
4558
- .activity-outcome {
4559
- color: var(--green);
4560
- }
4648
+ .trust-tier-badge {
4649
+ display: inline-flex;
4650
+ align-items: center;
4651
+ gap: 4px;
4652
+ padding: 2px 6px;
4653
+ background-color: rgba(63, 185, 80, 0.15);
4654
+ color: var(--green);
4655
+ border-radius: 3px;
4656
+ font-size: 10px;
4657
+ font-weight: 600;
4658
+ }
4561
4659
 
4562
- .activity-outcome.denied {
4563
- color: var(--red);
4564
- }
4660
+ .truncated {
4661
+ max-width: 200px;
4662
+ overflow: hidden;
4663
+ text-overflow: ellipsis;
4664
+ white-space: nowrap;
4665
+ }
4565
4666
 
4566
- .activity-detail {
4567
- font-size: 12px;
4568
- color: var(--text-secondary);
4569
- margin-left: 0;
4570
- }
4667
+ /* Row 3: SHR & Activity */
4668
+ .main-panels {
4669
+ display: grid;
4670
+ grid-template-columns: 1fr 1fr;
4671
+ gap: 16px;
4672
+ min-height: 400px;
4673
+ }
4571
4674
 
4572
- .activity-item.expanded .activity-detail {
4573
- display: block;
4574
- margin-top: 8px;
4575
- padding: 10px;
4576
- background: rgba(88, 166, 255, 0.08);
4577
- border-left: 2px solid var(--blue);
4578
- border-radius: 4px;
4579
- }
4675
+ .panel {
4676
+ background-color: var(--surface);
4677
+ border: 1px solid var(--border);
4678
+ border-radius: 8px;
4679
+ display: flex;
4680
+ flex-direction: column;
4681
+ overflow: hidden;
4682
+ }
4580
4683
 
4581
- .activity-empty {
4582
- display: flex;
4583
- flex-direction: column;
4584
- align-items: center;
4585
- justify-content: center;
4586
- height: 100%;
4587
- color: var(--text-secondary);
4588
- }
4684
+ .panel-header {
4685
+ padding: 16px 20px;
4686
+ border-bottom: 1px solid var(--border);
4687
+ display: flex;
4688
+ justify-content: space-between;
4689
+ align-items: center;
4690
+ }
4589
4691
 
4590
- .activity-empty-icon {
4591
- font-size: 32px;
4592
- margin-bottom: 12px;
4593
- }
4692
+ .panel-title {
4693
+ font-size: 14px;
4694
+ font-weight: 600;
4695
+ color: var(--text-primary);
4696
+ }
4594
4697
 
4595
- .activity-empty-text {
4596
- font-size: 14px;
4597
- }
4698
+ .panel-action {
4699
+ background: none;
4700
+ border: none;
4701
+ color: var(--blue);
4702
+ cursor: pointer;
4703
+ font-size: 12px;
4704
+ padding: 0;
4705
+ font-weight: 500;
4706
+ transition: color 0.2s;
4707
+ }
4598
4708
 
4599
- /* \u2500\u2500 Protection Status Sidebar (40%) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
4709
+ .panel-action:hover {
4710
+ color: #79c0ff;
4711
+ }
4600
4712
 
4601
- .protection-sidebar {
4602
- flex: 2;
4603
- display: flex;
4604
- flex-direction: column;
4605
- background: rgba(22, 27, 34, 0.5);
4606
- overflow: hidden;
4607
- }
4713
+ .panel-content {
4714
+ flex: 1;
4715
+ overflow-y: auto;
4716
+ padding: 20px;
4717
+ }
4608
4718
 
4609
- .sidebar-header {
4610
- padding: 16px 20px;
4611
- border-bottom: 1px solid var(--border);
4612
- font-size: 12px;
4613
- font-weight: 600;
4614
- text-transform: uppercase;
4615
- letter-spacing: 0.5px;
4616
- color: var(--text-secondary);
4617
- display: flex;
4618
- align-items: center;
4619
- gap: 8px;
4620
- }
4719
+ /* SHR Viewer */
4720
+ .shr-json {
4721
+ font-family: 'JetBrains Mono', monospace;
4722
+ font-size: 12px;
4723
+ line-height: 1.6;
4724
+ color: var(--text-secondary);
4725
+ }
4621
4726
 
4622
- .sidebar-content {
4623
- flex: 1;
4624
- overflow-y: auto;
4625
- padding: 16px 16px;
4626
- display: grid;
4627
- grid-template-columns: 1fr 1fr;
4628
- gap: 12px;
4629
- }
4727
+ .shr-section {
4728
+ margin-bottom: 12px;
4729
+ }
4630
4730
 
4631
- .protection-card {
4632
- background: var(--surface);
4633
- border: 1px solid var(--border);
4634
- border-radius: var(--radius);
4635
- padding: 14px;
4636
- display: flex;
4637
- flex-direction: column;
4638
- gap: 8px;
4639
- }
4731
+ .shr-section-header {
4732
+ display: flex;
4733
+ align-items: center;
4734
+ gap: 8px;
4735
+ cursor: pointer;
4736
+ font-weight: 600;
4737
+ color: var(--text-primary);
4738
+ padding: 8px;
4739
+ background-color: var(--bg);
4740
+ border-radius: 4px;
4741
+ user-select: none;
4742
+ }
4640
4743
 
4641
- .protection-card-icon {
4642
- font-size: 14px;
4643
- }
4744
+ .shr-section-header:hover {
4745
+ background-color: var(--muted);
4746
+ }
4644
4747
 
4645
- .protection-card-label {
4646
- font-size: 11px;
4647
- font-weight: 600;
4648
- text-transform: uppercase;
4649
- letter-spacing: 0.5px;
4650
- color: var(--text-secondary);
4651
- }
4748
+ .shr-toggle {
4749
+ width: 16px;
4750
+ height: 16px;
4751
+ display: flex;
4752
+ align-items: center;
4753
+ justify-content: center;
4754
+ font-size: 10px;
4755
+ transition: transform 0.2s;
4756
+ }
4652
4757
 
4653
- .protection-card-status {
4654
- display: flex;
4655
- align-items: center;
4656
- gap: 6px;
4657
- font-size: 12px;
4658
- font-weight: 600;
4659
- }
4758
+ .shr-section.collapsed .shr-toggle {
4759
+ transform: rotate(-90deg);
4760
+ }
4660
4761
 
4661
- .protection-card-status.active {
4662
- color: var(--green);
4663
- }
4762
+ .shr-section-content {
4763
+ padding: 8px 16px;
4764
+ background-color: rgba(0, 0, 0, 0.2);
4765
+ border-radius: 4px;
4766
+ margin-top: 4px;
4767
+ }
4664
4768
 
4665
- .protection-card-status.inactive {
4666
- color: var(--text-secondary);
4667
- }
4769
+ .shr-section.collapsed .shr-section-content {
4770
+ display: none;
4771
+ }
4668
4772
 
4669
- .protection-card-stat {
4670
- font-size: 11px;
4671
- color: var(--text-secondary);
4672
- font-family: var(--mono);
4673
- margin-top: 4px;
4674
- }
4773
+ .shr-item {
4774
+ display: flex;
4775
+ margin-bottom: 4px;
4776
+ }
4675
4777
 
4676
- /* \u2500\u2500 Pending Approvals Overlay \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
4778
+ .shr-key {
4779
+ color: var(--blue);
4780
+ margin-right: 8px;
4781
+ min-width: 120px;
4782
+ }
4677
4783
 
4678
- .pending-overlay {
4679
- position: fixed;
4680
- top: 56px;
4681
- right: 0;
4682
- bottom: 0;
4683
- width: 0;
4684
- background: var(--surface);
4685
- border-left: 1px solid var(--border);
4686
- z-index: 999;
4687
- overflow-y: auto;
4688
- transition: width 0.3s ease-out;
4689
- display: flex;
4690
- flex-direction: column;
4691
- }
4784
+ .shr-value {
4785
+ color: var(--green);
4786
+ word-break: break-all;
4787
+ }
4692
4788
 
4693
- .pending-overlay.active {
4694
- width: 380px;
4695
- }
4789
+ /* Activity Feed */
4790
+ .activity-feed {
4791
+ display: flex;
4792
+ flex-direction: column;
4793
+ gap: 12px;
4794
+ }
4696
4795
 
4697
- @media (max-width: 1400px) {
4698
- .pending-overlay.active {
4699
- width: 100%;
4700
- right: auto;
4701
- left: 0;
4796
+ .activity-item {
4797
+ padding: 12px;
4798
+ background-color: var(--bg);
4799
+ border-left: 2px solid var(--border);
4800
+ border-radius: 4px;
4801
+ font-size: 12px;
4702
4802
  }
4703
- }
4704
4803
 
4705
- .pending-overlay-header {
4706
- padding: 16px 20px;
4707
- border-bottom: 1px solid var(--border);
4708
- display: flex;
4709
- align-items: center;
4710
- justify-content: space-between;
4711
- flex: 0 0 auto;
4712
- }
4804
+ .activity-item.tool-call {
4805
+ border-left-color: var(--blue);
4806
+ }
4713
4807
 
4714
- .pending-overlay-title {
4715
- font-size: 13px;
4716
- font-weight: 600;
4717
- text-transform: uppercase;
4718
- letter-spacing: 0.5px;
4719
- color: var(--text-primary);
4720
- }
4808
+ .activity-item.context-gate {
4809
+ border-left-color: var(--amber);
4810
+ }
4721
4811
 
4722
- .pending-overlay-close {
4723
- background: none;
4724
- border: none;
4725
- color: var(--text-secondary);
4726
- cursor: pointer;
4727
- font-size: 18px;
4728
- padding: 0;
4729
- display: flex;
4730
- align-items: center;
4731
- justify-content: center;
4732
- }
4812
+ .activity-item.injection {
4813
+ border-left-color: var(--red);
4814
+ }
4733
4815
 
4734
- .pending-overlay-close:hover {
4735
- color: var(--text-primary);
4736
- }
4816
+ .activity-item.protection {
4817
+ border-left-color: var(--green);
4818
+ }
4737
4819
 
4738
- .pending-list {
4739
- flex: 1;
4740
- overflow-y: auto;
4741
- }
4820
+ .activity-type {
4821
+ font-weight: 600;
4822
+ color: var(--text-primary);
4823
+ margin-bottom: 4px;
4824
+ text-transform: uppercase;
4825
+ font-size: 11px;
4826
+ letter-spacing: 0.5px;
4827
+ }
4742
4828
 
4743
- .pending-item {
4744
- padding: 16px 20px;
4745
- border-bottom: 1px solid rgba(48, 54, 61, 0.5);
4746
- display: flex;
4747
- flex-direction: column;
4748
- gap: 10px;
4749
- }
4829
+ .activity-content {
4830
+ color: var(--text-secondary);
4831
+ font-family: 'JetBrains Mono', monospace;
4832
+ margin-bottom: 4px;
4833
+ word-break: break-all;
4834
+ }
4750
4835
 
4751
- .pending-item-header {
4752
- display: flex;
4753
- align-items: center;
4754
- gap: 8px;
4755
- }
4836
+ .activity-time {
4837
+ font-size: 11px;
4838
+ color: var(--text-secondary);
4839
+ }
4756
4840
 
4757
- .pending-item-op {
4758
- font-family: var(--mono);
4759
- font-size: 12px;
4760
- font-weight: 600;
4761
- color: var(--text-primary);
4762
- flex: 1;
4763
- }
4841
+ .empty-state {
4842
+ display: flex;
4843
+ align-items: center;
4844
+ justify-content: center;
4845
+ height: 100%;
4846
+ color: var(--text-secondary);
4847
+ font-size: 13px;
4848
+ }
4764
4849
 
4765
- .pending-item-tier {
4766
- display: inline-flex;
4767
- align-items: center;
4768
- justify-content: center;
4769
- width: 28px;
4770
- height: 20px;
4771
- font-size: 9px;
4772
- font-weight: 700;
4773
- border-radius: 3px;
4774
- text-transform: uppercase;
4775
- color: white;
4776
- }
4850
+ /* Row 4: Handshake History */
4851
+ .handshake-table {
4852
+ background-color: var(--surface);
4853
+ border: 1px solid var(--border);
4854
+ border-radius: 8px;
4855
+ overflow: hidden;
4856
+ }
4777
4857
 
4778
- .pending-item-tier.tier1 {
4779
- background: var(--red);
4780
- }
4858
+ .table-header {
4859
+ display: grid;
4860
+ grid-template-columns: 2fr 1fr 1fr 1fr 1.5fr 1.5fr;
4861
+ gap: 16px;
4862
+ padding: 16px 20px;
4863
+ border-bottom: 1px solid var(--border);
4864
+ background-color: var(--bg);
4865
+ font-size: 12px;
4866
+ font-weight: 600;
4867
+ color: var(--text-secondary);
4868
+ text-transform: uppercase;
4869
+ letter-spacing: 0.5px;
4870
+ }
4781
4871
 
4782
- .pending-item-tier.tier2 {
4783
- background: var(--amber);
4784
- }
4872
+ .table-rows {
4873
+ max-height: 300px;
4874
+ overflow-y: auto;
4875
+ }
4785
4876
 
4786
- .pending-item-reason {
4787
- font-size: 12px;
4788
- color: var(--text-secondary);
4789
- }
4877
+ .table-row {
4878
+ display: grid;
4879
+ grid-template-columns: 2fr 1fr 1fr 1fr 1.5fr 1.5fr;
4880
+ gap: 16px;
4881
+ padding: 14px 20px;
4882
+ border-bottom: 1px solid var(--border);
4883
+ align-items: center;
4884
+ font-size: 12px;
4885
+ cursor: pointer;
4886
+ transition: background-color 0.2s;
4887
+ }
4790
4888
 
4791
- .pending-item-timer {
4792
- display: flex;
4793
- align-items: center;
4794
- gap: 6px;
4795
- font-size: 11px;
4796
- font-family: var(--mono);
4797
- color: var(--text-secondary);
4798
- }
4889
+ .table-row:hover {
4890
+ background-color: var(--bg);
4891
+ }
4799
4892
 
4800
- .pending-item-timer-bar {
4801
- flex: 1;
4802
- height: 4px;
4803
- background: rgba(48, 54, 61, 0.8);
4804
- border-radius: 2px;
4805
- overflow: hidden;
4806
- }
4893
+ .table-row:last-child {
4894
+ border-bottom: none;
4895
+ }
4807
4896
 
4808
- .pending-item-timer-fill {
4809
- height: 100%;
4810
- background: var(--blue);
4811
- transition: width 0.1s linear;
4812
- }
4897
+ .table-cell {
4898
+ color: var(--text-secondary);
4899
+ font-family: 'JetBrains Mono', monospace;
4900
+ }
4813
4901
 
4814
- .pending-item-timer.urgent .pending-item-timer-fill {
4815
- background: var(--red);
4816
- }
4902
+ .table-cell.strong {
4903
+ color: var(--text-primary);
4904
+ font-weight: 500;
4905
+ }
4817
4906
 
4818
- .pending-item-actions {
4819
- display: flex;
4820
- gap: 8px;
4821
- }
4907
+ .table-empty {
4908
+ padding: 40px 20px;
4909
+ text-align: center;
4910
+ color: var(--text-secondary);
4911
+ font-size: 13px;
4912
+ }
4822
4913
 
4823
- .btn {
4824
- flex: 1;
4825
- padding: 8px 12px;
4826
- border: none;
4827
- border-radius: var(--radius);
4828
- font-size: 12px;
4829
- font-weight: 600;
4830
- cursor: pointer;
4831
- transition: all 0.15s;
4832
- font-family: var(--sans);
4833
- }
4914
+ /* Pending Overlay */
4915
+ .pending-overlay {
4916
+ position: fixed;
4917
+ top: 0;
4918
+ right: -400px;
4919
+ width: 400px;
4920
+ height: 100vh;
4921
+ background-color: var(--surface);
4922
+ border-left: 1px solid var(--border);
4923
+ box-shadow: -2px 0 8px rgba(0, 0, 0, 0.3);
4924
+ z-index: 200;
4925
+ transition: right 0.3s ease;
4926
+ display: flex;
4927
+ flex-direction: column;
4928
+ overflow-y: auto;
4929
+ }
4834
4930
 
4835
- .btn-approve {
4836
- background: var(--green);
4837
- color: var(--bg);
4838
- }
4931
+ .pending-overlay.show {
4932
+ right: 0;
4933
+ }
4839
4934
 
4840
- .btn-approve:hover {
4841
- background: #4ecf5e;
4842
- }
4935
+ .pending-header {
4936
+ padding: 16px 20px;
4937
+ border-bottom: 1px solid var(--border);
4938
+ font-weight: 600;
4939
+ color: var(--text-primary);
4940
+ }
4843
4941
 
4844
- .btn-deny {
4845
- background: var(--red);
4846
- color: white;
4847
- }
4942
+ .pending-items {
4943
+ flex: 1;
4944
+ overflow-y: auto;
4945
+ padding: 16px;
4946
+ }
4848
4947
 
4849
- .btn-deny:hover {
4850
- background: #f9605e;
4851
- }
4948
+ .pending-item {
4949
+ background-color: var(--bg);
4950
+ border: 1px solid var(--border);
4951
+ border-radius: 6px;
4952
+ padding: 16px;
4953
+ margin-bottom: 12px;
4954
+ }
4852
4955
 
4853
- /* \u2500\u2500 Threat Panel (collapsible footer) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
4956
+ .pending-title {
4957
+ font-weight: 600;
4958
+ color: var(--text-primary);
4959
+ margin-bottom: 8px;
4960
+ word-break: break-word;
4961
+ }
4854
4962
 
4855
- .threat-panel {
4856
- position: fixed;
4857
- bottom: 0;
4858
- left: 0;
4859
- right: 0;
4860
- background: var(--surface);
4861
- border-top: 1px solid var(--border);
4862
- max-height: 240px;
4863
- z-index: 500;
4864
- display: flex;
4865
- flex-direction: column;
4866
- transition: max-height 0.3s ease-out;
4867
- }
4963
+ .pending-countdown {
4964
+ font-size: 12px;
4965
+ color: var(--amber);
4966
+ margin-bottom: 12px;
4967
+ font-weight: 500;
4968
+ }
4868
4969
 
4869
- .threat-panel.collapsed {
4870
- max-height: 40px;
4871
- }
4970
+ .pending-actions {
4971
+ display: flex;
4972
+ gap: 8px;
4973
+ }
4872
4974
 
4873
- .threat-header {
4874
- padding: 12px 20px;
4875
- cursor: pointer;
4876
- display: flex;
4877
- align-items: center;
4878
- gap: 8px;
4879
- font-size: 12px;
4880
- font-weight: 600;
4881
- text-transform: uppercase;
4882
- letter-spacing: 0.5px;
4883
- color: var(--text-secondary);
4884
- flex: 0 0 auto;
4885
- }
4975
+ .pending-btn {
4976
+ flex: 1;
4977
+ padding: 8px 12px;
4978
+ border: none;
4979
+ border-radius: 4px;
4980
+ font-size: 12px;
4981
+ font-weight: 600;
4982
+ cursor: pointer;
4983
+ transition: background-color 0.2s;
4984
+ }
4886
4985
 
4887
- .threat-header:hover {
4888
- background: rgba(88, 166, 255, 0.05);
4889
- }
4986
+ .pending-approve {
4987
+ background-color: var(--green);
4988
+ color: var(--bg);
4989
+ }
4890
4990
 
4891
- .threat-icon {
4892
- font-size: 14px;
4893
- }
4991
+ .pending-approve:hover {
4992
+ background-color: #3fa040;
4993
+ }
4894
4994
 
4895
- .threat-content {
4896
- flex: 1;
4897
- overflow-y: auto;
4898
- padding: 0 20px 12px;
4899
- display: flex;
4900
- flex-direction: column;
4901
- gap: 10px;
4902
- }
4995
+ .pending-deny {
4996
+ background-color: var(--red);
4997
+ color: var(--bg);
4998
+ }
4903
4999
 
4904
- .threat-item {
4905
- padding: 8px 10px;
4906
- background: rgba(248, 81, 73, 0.1);
4907
- border-left: 2px solid var(--red);
4908
- border-radius: 4px;
4909
- font-size: 11px;
4910
- color: var(--text-secondary);
4911
- }
5000
+ .pending-deny:hover {
5001
+ background-color: #e03c3c;
5002
+ }
4912
5003
 
4913
- .threat-item-type {
4914
- font-weight: 600;
4915
- color: var(--red);
4916
- font-family: var(--mono);
4917
- }
5004
+ /* Threat Panel */
5005
+ .threat-panel {
5006
+ background-color: var(--surface);
5007
+ border: 1px solid var(--border);
5008
+ border-radius: 8px;
5009
+ margin-top: 20px;
5010
+ overflow: hidden;
5011
+ }
4918
5012
 
4919
- .threat-empty {
4920
- text-align: center;
4921
- padding: 20px 10px;
4922
- color: var(--text-secondary);
4923
- font-size: 12px;
4924
- }
5013
+ .threat-header {
5014
+ padding: 16px 20px;
5015
+ border-bottom: 1px solid var(--border);
5016
+ display: flex;
5017
+ justify-content: space-between;
5018
+ align-items: center;
5019
+ cursor: pointer;
5020
+ user-select: none;
5021
+ }
4925
5022
 
4926
- /* \u2500\u2500 Scrollbars \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
5023
+ .threat-title {
5024
+ font-weight: 600;
5025
+ color: var(--text-primary);
5026
+ }
4927
5027
 
4928
- ::-webkit-scrollbar {
4929
- width: 6px;
4930
- }
5028
+ .threat-toggle {
5029
+ font-size: 10px;
5030
+ color: var(--text-secondary);
5031
+ transition: transform 0.2s;
5032
+ }
4931
5033
 
4932
- ::-webkit-scrollbar-track {
4933
- background: transparent;
4934
- }
5034
+ .threat-panel.collapsed .threat-toggle {
5035
+ transform: rotate(-90deg);
5036
+ }
4935
5037
 
4936
- ::-webkit-scrollbar-thumb {
4937
- background: var(--border);
4938
- border-radius: 3px;
4939
- }
5038
+ .threat-content {
5039
+ padding: 16px 20px;
5040
+ max-height: 300px;
5041
+ overflow-y: auto;
5042
+ }
4940
5043
 
4941
- ::-webkit-scrollbar-thumb:hover {
4942
- background: rgba(88, 166, 255, 0.3);
4943
- }
5044
+ .threat-panel.collapsed .threat-content {
5045
+ display: none;
5046
+ }
4944
5047
 
4945
- /* \u2500\u2500 Responsive \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
5048
+ .threat-alert {
5049
+ background-color: rgba(248, 81, 73, 0.1);
5050
+ border: 1px solid var(--red);
5051
+ border-radius: 4px;
5052
+ padding: 12px;
5053
+ margin-bottom: 8px;
5054
+ font-size: 12px;
5055
+ }
4946
5056
 
4947
- @media (max-width: 1200px) {
4948
- .protection-sidebar {
4949
- display: none;
5057
+ .threat-alert:last-child {
5058
+ margin-bottom: 0;
4950
5059
  }
4951
5060
 
4952
- .activity-feed {
4953
- border-right: none;
5061
+ .threat-type {
5062
+ font-weight: 600;
5063
+ color: var(--red);
5064
+ margin-bottom: 4px;
5065
+ text-transform: uppercase;
5066
+ font-size: 10px;
5067
+ letter-spacing: 0.5px;
4954
5068
  }
4955
- }
4956
5069
 
4957
- @media (max-width: 768px) {
4958
- .status-bar {
4959
- padding: 0 12px;
4960
- gap: 12px;
4961
- height: 48px;
5070
+ .threat-message {
5071
+ color: var(--text-secondary);
4962
5072
  }
4963
5073
 
4964
- .sanctuary-logo {
4965
- font-size: 14px;
5074
+ /* Scrollbar */
5075
+ ::-webkit-scrollbar {
5076
+ width: 8px;
4966
5077
  }
4967
5078
 
4968
- .status-bar-center {
4969
- display: none;
5079
+ ::-webkit-scrollbar-track {
5080
+ background-color: transparent;
4970
5081
  }
4971
5082
 
4972
- .main-container {
4973
- margin-top: 48px;
5083
+ ::-webkit-scrollbar-thumb {
5084
+ background-color: var(--border);
5085
+ border-radius: 4px;
4974
5086
  }
4975
5087
 
4976
- .activity-item {
4977
- padding: 10px 12px;
5088
+ ::-webkit-scrollbar-thumb:hover {
5089
+ background-color: var(--text-secondary);
4978
5090
  }
4979
5091
 
4980
- .pending-overlay.active {
4981
- width: 100%;
5092
+ /* Responsive */
5093
+ @media (max-width: 1400px) {
5094
+ .sovereignty-layers {
5095
+ grid-template-columns: repeat(2, 1fr);
5096
+ }
5097
+
5098
+ .main-panels {
5099
+ grid-template-columns: 1fr;
5100
+ }
5101
+
5102
+ .pending-overlay {
5103
+ width: 100%;
5104
+ right: -100%;
5105
+ }
4982
5106
  }
4983
5107
 
4984
- .threat-panel {
4985
- max-height: 200px;
5108
+ @media (max-width: 768px) {
5109
+ .status-bar {
5110
+ flex-wrap: wrap;
5111
+ height: auto;
5112
+ padding: 12px;
5113
+ gap: 12px;
5114
+ }
5115
+
5116
+ .status-bar-center {
5117
+ order: 3;
5118
+ flex-basis: 100%;
5119
+ }
5120
+
5121
+ .main-content {
5122
+ margin-top: auto;
5123
+ }
5124
+
5125
+ .info-cards {
5126
+ grid-template-columns: 1fr;
5127
+ }
5128
+
5129
+ .table-header,
5130
+ .table-row {
5131
+ grid-template-columns: 1fr;
5132
+ }
4986
5133
  }
4987
- }
4988
- </style>
5134
+ </style>
4989
5135
  </head>
4990
5136
  <body>
4991
-
4992
- <!-- Status Bar (fixed, top) -->
4993
- <div class="status-bar">
4994
- <div class="status-bar-left">
4995
- <div class="sanctuary-logo"><span>\u25C6</span> SANCTUARY</div>
4996
- <div class="version">v${options.serverVersion}</div>
4997
- </div>
4998
- <div class="status-bar-center">
4999
- <div class="sovereignty-badge">
5000
- <div class="sovereignty-score" id="sovereigntyScore">85</div>
5001
- <span>Sovereignty Health</span>
5002
- </div>
5003
- </div>
5004
- <div class="status-bar-right">
5005
- <div class="protections-indicator">
5006
- <span class="count" id="activeProtections">6</span>/6 protections
5007
- </div>
5008
- <div class="uptime">
5009
- <span id="uptimeText">\u2014</span>
5010
- </div>
5011
- <div class="status-dot" id="statusDot"></div>
5012
- <div class="pending-badge hidden" id="pendingBadge">0</div>
5013
- </div>
5014
- </div>
5015
-
5016
- <!-- Main Layout -->
5017
- <div class="main-container">
5018
- <!-- Activity Feed -->
5019
- <div class="activity-feed">
5020
- <div class="feed-header">
5021
- <div class="feed-header-dot"></div>
5022
- Live Activity
5023
- </div>
5024
- <div class="activity-list" id="activityList">
5025
- <div class="activity-empty">
5026
- <div class="activity-empty-icon">\u2192</div>
5027
- <div class="activity-empty-text">Waiting for activity...</div>
5137
+ <!-- Status Bar -->
5138
+ <div class="status-bar">
5139
+ <div class="status-bar-left">
5140
+ <div class="logo-icon">\u25C6</div>
5141
+ <div class="logo-info">
5142
+ <div class="logo-title">SANCTUARY</div>
5143
+ <div class="logo-version">v${options.serverVersion}</div>
5028
5144
  </div>
5029
5145
  </div>
5030
- </div>
5031
5146
 
5032
- <!-- Protection Status Sidebar -->
5033
- <div class="protection-sidebar" id="protectionSidebar">
5034
- <div class="sidebar-header">
5035
- <span>\u25C6</span> Protection Status
5147
+ <div class="status-bar-center">
5148
+ <div id="sovereignty-badge" class="sovereignty-badge">
5149
+ <span>Sovereignty Health:</span>
5150
+ <span class="sovereignty-score" id="sovereignty-score">\u2014</span>
5151
+ <span>/ 100</span>
5152
+ </div>
5036
5153
  </div>
5037
- <div class="sidebar-content">
5038
- <div class="protection-card">
5039
- <div class="protection-card-icon">\u{1F510}</div>
5040
- <div class="protection-card-label">Encryption</div>
5041
- <div class="protection-card-status active" id="encryptionStatus">\u2713 Active</div>
5042
- <div class="protection-card-stat" id="encryptionStat">Ed25519</div>
5154
+
5155
+ <div class="status-bar-right">
5156
+ <div class="status-item">
5157
+ <strong id="protections-count">\u2014</strong>
5158
+ <span>Protections</span>
5043
5159
  </div>
5160
+ <div class="status-item">
5161
+ <strong id="uptime-value">\u2014</strong>
5162
+ <span>Uptime</span>
5163
+ </div>
5164
+ <div class="status-dot" id="connection-status"></div>
5165
+ <div id="pending-item-badge" class="pending-badge" style="display: none;">
5166
+ <span>\u23F3</span>
5167
+ <span id="pending-count">0</span>
5168
+ </div>
5169
+ </div>
5170
+ </div>
5044
5171
 
5045
- <div class="protection-card">
5046
- <div class="protection-card-icon">\u2713</div>
5047
- <div class="protection-card-label">Approval Gate</div>
5048
- <div class="protection-card-status active" id="approvalStatus">\u2713 Active</div>
5049
- <div class="protection-card-stat" id="approvalStat">T1: 2 | T2: 3</div>
5172
+ <!-- Main Content -->
5173
+ <div class="main-content">
5174
+ <div class="grid">
5175
+ <!-- Row 1: Sovereignty Layers -->
5176
+ <div class="sovereignty-layers" id="sovereignty-layers">
5177
+ <div class="layer-card" data-layer="l1">
5178
+ <div class="layer-name">Layer 1</div>
5179
+ <div class="layer-title">Cognitive Sovereignty</div>
5180
+ <div class="layer-status"><span>\u25CF</span> <span id="l1-status">\u2014</span></div>
5181
+ <div class="layer-detail" id="l1-detail">Loading...</div>
5182
+ </div>
5183
+ <div class="layer-card" data-layer="l2">
5184
+ <div class="layer-name">Layer 2</div>
5185
+ <div class="layer-title">Operational Isolation</div>
5186
+ <div class="layer-status"><span>\u25CF</span> <span id="l2-status">\u2014</span></div>
5187
+ <div class="layer-detail" id="l2-detail">Loading...</div>
5188
+ </div>
5189
+ <div class="layer-card" data-layer="l3">
5190
+ <div class="layer-name">Layer 3</div>
5191
+ <div class="layer-title">Selective Disclosure</div>
5192
+ <div class="layer-status"><span>\u25CF</span> <span id="l3-status">\u2014</span></div>
5193
+ <div class="layer-detail" id="l3-detail">Loading...</div>
5194
+ </div>
5195
+ <div class="layer-card" data-layer="l4">
5196
+ <div class="layer-name">Layer 4</div>
5197
+ <div class="layer-title">Verifiable Reputation</div>
5198
+ <div class="layer-status"><span>\u25CF</span> <span id="l4-status">\u2014</span></div>
5199
+ <div class="layer-detail" id="l4-detail">Loading...</div>
5200
+ </div>
5050
5201
  </div>
5051
5202
 
5052
- <div class="protection-card">
5053
- <div class="protection-card-icon">\u{1F3AF}</div>
5054
- <div class="protection-card-label">Context Gating</div>
5055
- <div class="protection-card-status active" id="contextStatus">\u2713 Active</div>
5056
- <div class="protection-card-stat" id="contextStat">12 filtered</div>
5203
+ <!-- Row 2: Info Cards -->
5204
+ <div class="info-cards">
5205
+ <div class="info-card">
5206
+ <div class="card-header">Identity</div>
5207
+ <div class="card-row">
5208
+ <span class="card-label">Primary</span>
5209
+ <span class="card-value" id="identity-label">\u2014</span>
5210
+ </div>
5211
+ <div class="card-row">
5212
+ <span class="card-label">DID</span>
5213
+ <span class="card-value truncated" id="identity-did" title="">\u2014</span>
5214
+ </div>
5215
+ <div class="card-row">
5216
+ <span class="card-label">Public Key</span>
5217
+ <span class="card-value truncated" id="identity-pubkey" title="">\u2014</span>
5218
+ </div>
5219
+ <div class="card-row">
5220
+ <span class="card-label">Type</span>
5221
+ <span class="identity-badge">Ed25519</span>
5222
+ </div>
5223
+ <div class="card-row">
5224
+ <span class="card-label">Created</span>
5225
+ <span class="card-value" id="identity-created">\u2014</span>
5226
+ </div>
5227
+ <div class="card-row">
5228
+ <span class="card-label">Identities</span>
5229
+ <span class="card-value" id="identity-count">\u2014</span>
5230
+ </div>
5231
+ </div>
5232
+
5233
+ <div class="info-card">
5234
+ <div class="card-header">Handshakes</div>
5235
+ <div class="card-row">
5236
+ <span class="card-label">Total</span>
5237
+ <span class="card-value" id="handshake-count">\u2014</span>
5238
+ </div>
5239
+ <div class="card-row">
5240
+ <span class="card-label">Latest Peer</span>
5241
+ <span class="card-value truncated" id="handshake-latest">\u2014</span>
5242
+ </div>
5243
+ <div class="card-row">
5244
+ <span class="card-label">Trust Tier</span>
5245
+ <span class="trust-tier-badge" id="handshake-tier">Unverified</span>
5246
+ </div>
5247
+ <div class="card-row">
5248
+ <span class="card-label">Timestamp</span>
5249
+ <span class="card-value" id="handshake-time">\u2014</span>
5250
+ </div>
5251
+ </div>
5252
+
5253
+ <div class="info-card">
5254
+ <div class="card-header">Reputation</div>
5255
+ <div class="card-row">
5256
+ <span class="card-label">Weighted Score</span>
5257
+ <span class="card-value" id="reputation-score">\u2014</span>
5258
+ </div>
5259
+ <div class="card-row">
5260
+ <span class="card-label">Attestations</span>
5261
+ <span class="card-value" id="reputation-attestations">\u2014</span>
5262
+ </div>
5263
+ <div class="card-row">
5264
+ <span class="card-label">Verified Sovereign</span>
5265
+ <span class="card-value" id="reputation-verified">\u2014</span>
5266
+ </div>
5267
+ <div class="card-row">
5268
+ <span class="card-label">Verified Degraded</span>
5269
+ <span class="card-value" id="reputation-degraded">\u2014</span>
5270
+ </div>
5271
+ <div class="card-row">
5272
+ <span class="card-label">Unverified</span>
5273
+ <span class="card-value" id="reputation-unverified">\u2014</span>
5274
+ </div>
5275
+ </div>
5057
5276
  </div>
5058
5277
 
5059
- <div class="protection-card">
5060
- <div class="protection-card-icon">\u26A0</div>
5061
- <div class="protection-card-label">Injection Detection</div>
5062
- <div class="protection-card-status active" id="injectionStatus">\u2713 Active</div>
5063
- <div class="protection-card-stat" id="injectionStat">3 flags today</div>
5278
+ <!-- Row 3: SHR & Activity -->
5279
+ <div class="main-panels">
5280
+ <div class="panel">
5281
+ <div class="panel-header">
5282
+ <div class="panel-title">Sovereignty Health Report</div>
5283
+ <button class="panel-action" id="copy-shr-btn">Copy JSON</button>
5284
+ </div>
5285
+ <div class="panel-content">
5286
+ <div class="shr-json" id="shr-viewer">
5287
+ <div class="empty-state">Loading SHR...</div>
5288
+ </div>
5289
+ </div>
5290
+ </div>
5291
+
5292
+ <div class="panel">
5293
+ <div class="panel-header">
5294
+ <div class="panel-title">Activity Feed</div>
5295
+ </div>
5296
+ <div class="panel-content">
5297
+ <div id="activity-feed" class="activity-feed">
5298
+ <div class="empty-state">Waiting for activity...</div>
5299
+ </div>
5300
+ </div>
5301
+ </div>
5064
5302
  </div>
5065
5303
 
5066
- <div class="protection-card">
5067
- <div class="protection-card-icon">\u{1F4CA}</div>
5068
- <div class="protection-card-label">Behavioral Baseline</div>
5069
- <div class="protection-card-status active" id="baselineStatus">\u2713 Active</div>
5070
- <div class="protection-card-stat" id="baselineStat">0 anomalies</div>
5304
+ <!-- Row 4: Handshake History -->
5305
+ <div class="handshake-table">
5306
+ <div class="table-header">
5307
+ <div>Counterparty</div>
5308
+ <div>Trust Tier</div>
5309
+ <div>Sovereignty</div>
5310
+ <div>Verified</div>
5311
+ <div>Completed</div>
5312
+ <div>Expires</div>
5313
+ </div>
5314
+ <div class="table-rows" id="handshake-table">
5315
+ <div class="table-empty">No handshakes completed yet</div>
5316
+ </div>
5071
5317
  </div>
5072
5318
 
5073
- <div class="protection-card">
5074
- <div class="protection-card-icon">\u{1F4CB}</div>
5075
- <div class="protection-card-label">Audit Trail</div>
5076
- <div class="protection-card-status active" id="auditStatus">\u2713 Active</div>
5077
- <div class="protection-card-stat" id="auditStat">284 entries</div>
5319
+ <!-- Threat Panel -->
5320
+ <div class="threat-panel collapsed">
5321
+ <div class="threat-header">
5322
+ <div class="threat-title">Security Threats</div>
5323
+ <div class="threat-toggle">\u25B6</div>
5324
+ </div>
5325
+ <div class="threat-content" id="threat-alerts">
5326
+ <div class="empty-state">No threats detected</div>
5327
+ </div>
5078
5328
  </div>
5079
5329
  </div>
5080
5330
  </div>
5081
- </div>
5082
5331
 
5083
- <!-- Pending Approvals Overlay -->
5084
- <div class="pending-overlay" id="pendingOverlay">
5085
- <div class="pending-overlay-header">
5086
- <div class="pending-overlay-title">Pending Approvals</div>
5087
- <button class="pending-overlay-close" onclick="closePendingOverlay()">\xD7</button>
5332
+ <!-- Pending Overlay -->
5333
+ <div class="pending-overlay" id="pending-overlay">
5334
+ <div class="pending-header">Pending Approvals</div>
5335
+ <div class="pending-items" id="pending-items"></div>
5088
5336
  </div>
5089
- <div class="pending-list" id="pendingList"></div>
5090
- </div>
5091
-
5092
- <!-- Threat Panel (collapsible footer) -->
5093
- <div class="threat-panel collapsed" id="threatPanel">
5094
- <div class="threat-header" onclick="toggleThreatPanel()">
5095
- <span class="threat-icon">\u26A0</span>
5096
- Recent Threats
5097
- <span id="threatCount" style="margin-left: auto; color: var(--red); font-weight: 700;">0</span>
5098
- </div>
5099
- <div class="threat-content" id="threatContent">
5100
- <div class="threat-empty">No threats detected</div>
5101
- </div>
5102
- </div>
5103
5337
 
5104
- <script>
5105
- (function() {
5106
- 'use strict';
5338
+ <script>
5339
+ // Constants
5340
+ const AUTH_TOKEN = '${options.authToken || ""}' || sessionStorage.getItem('authToken') || '';
5341
+ const TIMEOUT_SECONDS = ${options.timeoutSeconds};
5342
+ const API_BASE = '';
5343
+
5344
+ // State
5345
+ let apiState = {
5346
+ sovereignty: null,
5347
+ identity: null,
5348
+ handshakes: [],
5349
+ shr: null,
5350
+ status: null,
5351
+ };
5107
5352
 
5108
- // \u2500\u2500 Configuration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
5353
+ let pendingRequests = new Map();
5354
+ let activityLog = [];
5355
+ const maxActivityItems = 50;
5109
5356
 
5110
- const TIMEOUT_SECONDS = ${options.timeoutSeconds};
5111
- // AUTH_TOKEN: embedded token (for direct session access) or from sessionStorage (login page flow)
5112
- const EMBEDDED_TOKEN = ${options.authToken ? JSON.stringify(options.authToken) : "null"};
5113
- const AUTH_TOKEN = EMBEDDED_TOKEN || (function() { try { return sessionStorage.getItem('sanctuary_token'); } catch(_) { return null; } })();
5114
- const MAX_ACTIVITY_ITEMS = 100;
5115
- const MAX_THREAT_ITEMS = 20;
5357
+ // Helpers
5358
+ function esc(text) {
5359
+ if (!text) return '';
5360
+ const div = document.createElement('div');
5361
+ div.textContent = text;
5362
+ return div.innerHTML;
5363
+ }
5364
+
5365
+ function formatTime(isoString) {
5366
+ if (!isoString) return '\u2014';
5367
+ const date = new Date(isoString);
5368
+ return date.toLocaleString('en-US', {
5369
+ month: 'short',
5370
+ day: 'numeric',
5371
+ hour: '2-digit',
5372
+ minute: '2-digit',
5373
+ });
5374
+ }
5116
5375
 
5117
- // \u2500\u2500 State \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
5376
+ function truncate(str, len = 16) {
5377
+ if (!str) return '\u2014';
5378
+ if (str.length <= len) return str;
5379
+ return str.slice(0, len) + '...';
5380
+ }
5118
5381
 
5119
- let SESSION_ID = null;
5120
- let evtSource = null;
5121
- let startTime = Date.now();
5122
- let activityCount = 0;
5123
- let threatCount = 0;
5124
- const pendingRequests = new Map();
5125
- const activityItems = [];
5126
- const threatItems = [];
5127
- let sovereigntyScore = 85;
5128
- let sessionRenewalTimer = null;
5382
+ function calculateSovereigntyScore(shr) {
5383
+ if (!shr || !shr.layers) return 0;
5384
+ const layers = shr.layers;
5385
+ let score = 100;
5129
5386
 
5130
- // \u2500\u2500 Auth Helpers (SEC-012) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
5387
+ if (layers.l1?.status === 'degraded') score -= 20;
5388
+ if (layers.l1?.status === 'inactive') score -= 35;
5389
+ if (layers.l2?.status === 'degraded') score -= 15;
5390
+ if (layers.l2?.status === 'inactive') score -= 25;
5391
+ if (layers.l3?.status === 'degraded') score -= 15;
5392
+ if (layers.l3?.status === 'inactive') score -= 25;
5393
+ if (layers.l4?.status === 'degraded') score -= 10;
5394
+ if (layers.l4?.status === 'inactive') score -= 20;
5131
5395
 
5132
- function authHeaders() {
5133
- const h = { 'Content-Type': 'application/json' };
5134
- if (AUTH_TOKEN) h['Authorization'] = 'Bearer ' + AUTH_TOKEN;
5135
- return h;
5136
- }
5396
+ return Math.max(0, Math.min(100, score));
5397
+ }
5137
5398
 
5138
- function sessionQuery(url) {
5139
- if (!SESSION_ID) return url;
5140
- const sep = url.includes('?') ? '&' : '?';
5141
- return url + sep + 'session=' + SESSION_ID;
5142
- }
5399
+ async function fetchAPI(endpoint) {
5400
+ try {
5401
+ const response = await fetch(API_BASE + endpoint, {
5402
+ headers: {
5403
+ 'Authorization': 'Bearer ' + AUTH_TOKEN,
5404
+ },
5405
+ });
5143
5406
 
5144
- function setCookie(sessionId, maxAge) {
5145
- document.cookie = 'sanctuary_session=' + sessionId +
5146
- '; path=/; SameSite=Strict; max-age=' + maxAge;
5147
- }
5407
+ if (response.status === 401) {
5408
+ redirectToLogin();
5409
+ return null;
5410
+ }
5148
5411
 
5149
- async function exchangeSession() {
5150
- if (!AUTH_TOKEN) return;
5151
- try {
5152
- const resp = await fetch('/auth/session', { method: 'POST', headers: authHeaders() });
5153
- if (resp.ok) {
5154
- const data = await resp.json();
5155
- SESSION_ID = data.session_id;
5156
- var ttl = data.expires_in_seconds || 300;
5157
- // Update cookie with new session
5158
- setCookie(SESSION_ID, ttl);
5159
- // Schedule renewal at 80% of TTL
5160
- if (sessionRenewalTimer) clearTimeout(sessionRenewalTimer);
5161
- sessionRenewalTimer = setTimeout(function() {
5162
- exchangeSession().then(function() { reconnectSSE(); });
5163
- }, ttl * 800);
5164
- } else if (resp.status === 401) {
5165
- // Token invalid or expired \u2014 show non-destructive re-login overlay
5166
- showSessionExpired();
5167
- }
5168
- } catch (e) {
5169
- // Network error \u2014 retry in 30s
5170
- if (sessionRenewalTimer) clearTimeout(sessionRenewalTimer);
5171
- sessionRenewalTimer = setTimeout(function() {
5172
- exchangeSession().then(function() { reconnectSSE(); });
5173
- }, 30000);
5174
- }
5175
- }
5176
-
5177
- function showSessionExpired() {
5178
- // Clear stored token
5179
- try { sessionStorage.removeItem('sanctuary_token'); } catch(_) {}
5180
- // Redirect to login page
5181
- document.cookie = 'sanctuary_session=; path=/; max-age=0';
5182
- window.location.reload();
5183
- }
5184
-
5185
- // \u2500\u2500 UI Utilities \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
5186
-
5187
- function esc(s) {
5188
- const d = document.createElement('div');
5189
- d.textContent = String(s || '');
5190
- return d.innerHTML;
5191
- }
5192
-
5193
- function closePendingOverlay() {
5194
- document.getElementById('pendingOverlay').classList.remove('active');
5195
- }
5196
-
5197
- function toggleThreatPanel() {
5198
- document.getElementById('threatPanel').classList.toggle('collapsed');
5199
- }
5200
-
5201
- function updateUptime() {
5202
- const elapsed = Math.floor((Date.now() - startTime) / 1000);
5203
- const hours = Math.floor(elapsed / 3600);
5204
- const mins = Math.floor((elapsed % 3600) / 60);
5205
- const secs = elapsed % 60;
5206
- let uptimeStr = '';
5207
- if (hours > 0) uptimeStr += hours + 'h ';
5208
- if (mins > 0) uptimeStr += mins + 'm ';
5209
- uptimeStr += secs + 's';
5210
- document.getElementById('uptimeText').textContent = uptimeStr;
5211
- }
5212
-
5213
- // \u2500\u2500 Sovereignty Score \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
5214
-
5215
- function updateSovereigntyScore(score) {
5216
- sovereigntyScore = Math.min(100, Math.max(0, score || 85));
5217
- const badge = document.getElementById('sovereigntyScore');
5218
- badge.textContent = sovereigntyScore;
5219
- badge.className = 'sovereignty-score';
5220
- if (sovereigntyScore >= 80) {
5221
- badge.classList.add('high');
5222
- } else if (sovereigntyScore >= 50) {
5223
- badge.classList.add('medium');
5224
- } else {
5225
- badge.classList.add('low');
5412
+ if (!response.ok) {
5413
+ console.error('API Error:', response.status);
5414
+ return null;
5415
+ }
5416
+
5417
+ return await response.json();
5418
+ } catch (err) {
5419
+ console.error('Fetch error:', err);
5420
+ return null;
5421
+ }
5226
5422
  }
5227
- }
5228
5423
 
5229
- // \u2500\u2500 Activity Feed \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
5424
+ function redirectToLogin() {
5425
+ sessionStorage.removeItem('authToken');
5426
+ window.location.href = '/';
5427
+ }
5230
5428
 
5231
- function addActivityItem(data) {
5232
- const {
5233
- timestamp,
5234
- tier,
5235
- tool,
5236
- outcome,
5237
- detail,
5238
- hasInjection,
5239
- isContextGated
5240
- } = data;
5241
-
5242
- const item = {
5243
- id: 'activity-' + activityCount++,
5244
- timestamp: timestamp || new Date().toISOString(),
5245
- tier: tier || 1,
5246
- tool: tool || 'unknown_tool',
5247
- outcome: outcome || 'executed',
5248
- detail: detail || '',
5249
- hasInjection: !!hasInjection,
5250
- isContextGated: !!isContextGated
5251
- };
5429
+ // API Updates
5430
+ async function updateSovereignty() {
5431
+ const data = await fetchAPI('/api/sovereignty');
5432
+ if (!data) return;
5252
5433
 
5253
- activityItems.unshift(item);
5254
- if (activityItems.length > MAX_ACTIVITY_ITEMS) {
5255
- activityItems.pop();
5256
- }
5434
+ apiState.sovereignty = data;
5257
5435
 
5258
- renderActivityFeed();
5259
- }
5436
+ const score = calculateSovereigntyScore(data.shr);
5437
+ const badge = document.getElementById('sovereignty-badge');
5438
+ const scoreEl = document.getElementById('sovereignty-score');
5260
5439
 
5261
- function renderActivityFeed() {
5262
- const list = document.getElementById('activityList');
5440
+ scoreEl.textContent = score;
5263
5441
 
5264
- if (activityItems.length === 0) {
5265
- list.innerHTML = '<div class="activity-empty"><div class="activity-empty-icon">\u2192</div><div class="activity-empty-text">Waiting for activity...</div></div>';
5266
- return;
5442
+ badge.classList.remove('degraded', 'inactive');
5443
+ if (score < 70) badge.classList.add('degraded');
5444
+ if (score < 40) badge.classList.add('inactive');
5445
+
5446
+ updateLayerCards(data.shr);
5267
5447
  }
5268
5448
 
5269
- list.innerHTML = '';
5270
- for (const item of activityItems) {
5271
- const tr = document.createElement('div');
5272
- tr.className = 'activity-item';
5273
- tr.id = item.id;
5274
-
5275
- const time = new Date(item.timestamp);
5276
- const timeStr = time.toLocaleTimeString();
5277
-
5278
- const tierClass = 't' + item.tier;
5279
- const outcomeClass = item.outcome === 'denied' ? 'outcome denied' : 'outcome';
5280
-
5281
- let icon = '\u25CF';
5282
- if (item.isContextGated) icon = '\u{1F3AF}';
5283
- else if (item.hasInjection) icon = '\u26A0';
5284
- else if (item.outcome === 'denied') icon = '\u2717';
5285
- else icon = '\u2713';
5286
-
5287
- tr.innerHTML =
5288
- '<div class="activity-item-icon">' + esc(icon) + '</div>' +
5289
- '<div class="activity-item-content">' +
5290
- '<div class="activity-time">' + esc(timeStr) + '</div>' +
5291
- '<div class="activity-main">' +
5292
- '<span class="activity-tier ' + tierClass + '">T' + item.tier + '</span>' +
5293
- '<span class="activity-tool">' + esc(item.tool) + '</span>' +
5294
- '<span class="activity-outcome ' + (outcomeClass === 'outcome denied' ? 'denied' : '') + '">' + (item.outcome === 'denied' ? '\u2717 denied' : '\u2713 allowed') + '</span>' +
5295
- '</div>' +
5296
- '<div class="activity-detail">' + esc(item.detail) + '</div>' +
5297
- '</div>' +
5298
- '';
5299
-
5300
- tr.addEventListener('click', () => {
5301
- tr.classList.toggle('expanded');
5302
- });
5449
+ function updateLayerCards(shr) {
5450
+ if (!shr || !shr.layers) return;
5303
5451
 
5304
- list.appendChild(tr);
5305
- }
5306
- }
5452
+ const layers = shr.layers;
5307
5453
 
5308
- // \u2500\u2500 Pending Approvals \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
5454
+ updateLayerCard('l1', layers.l1, layers.l1?.encryption || 'AES-256-GCM');
5455
+ updateLayerCard('l2', layers.l2, layers.l2?.isolation_type || 'Process-level');
5456
+ updateLayerCard('l3', layers.l3, layers.l3?.proof_system || 'Schnorr-Pedersen');
5457
+ updateLayerCard('l4', layers.l4, layers.l4?.reputation_mode || 'Weighted');
5458
+ }
5309
5459
 
5310
- function addPendingRequest(data) {
5311
- const {
5312
- request_id,
5313
- operation,
5314
- tier,
5315
- reason,
5316
- context,
5317
- timestamp
5318
- } = data;
5319
-
5320
- const pending = {
5321
- id: request_id,
5322
- operation: operation || 'unknown',
5323
- tier: tier || 1,
5324
- reason: reason || '',
5325
- context: context || {},
5326
- timestamp: timestamp || new Date().toISOString(),
5327
- remaining: TIMEOUT_SECONDS
5328
- };
5460
+ function updateLayerCard(layer, layerData, detail) {
5461
+ if (!layerData) return;
5329
5462
 
5330
- pendingRequests.set(request_id, pending);
5331
- updatePendingUI();
5332
- }
5463
+ const card = document.querySelector(\`[data-layer="\${layer}"]\`);
5464
+ if (!card) return;
5333
5465
 
5334
- function removePendingRequest(id) {
5335
- pendingRequests.delete(id);
5336
- updatePendingUI();
5337
- }
5466
+ const status = layerData.status || 'inactive';
5467
+ card.classList.remove('degraded', 'inactive');
5338
5468
 
5339
- function updatePendingUI() {
5340
- const count = pendingRequests.size;
5341
- const badge = document.getElementById('pendingBadge');
5469
+ if (status === 'degraded') {
5470
+ card.classList.add('degraded');
5471
+ } else if (status === 'inactive') {
5472
+ card.classList.add('inactive');
5473
+ }
5342
5474
 
5343
- if (count > 0) {
5344
- badge.classList.remove('hidden');
5345
- badge.textContent = count;
5346
- document.getElementById('pendingOverlay').classList.add('active');
5347
- } else {
5348
- badge.classList.add('hidden');
5349
- document.getElementById('pendingOverlay').classList.remove('active');
5475
+ document.getElementById(\`\${layer}-status\`).textContent = status.toUpperCase();
5476
+ document.getElementById(\`\${layer}-detail\`).textContent = detail;
5350
5477
  }
5351
5478
 
5352
- renderPendingList();
5353
- }
5479
+ async function updateIdentity() {
5480
+ const data = await fetchAPI('/api/identity');
5481
+ if (!data) return;
5354
5482
 
5355
- function renderPendingList() {
5356
- const list = document.getElementById('pendingList');
5357
- list.innerHTML = '';
5483
+ apiState.identity = data;
5358
5484
 
5359
- for (const [id, req] of pendingRequests) {
5360
- const item = document.createElement('div');
5361
- item.className = 'pending-item';
5485
+ const primary = data.primary || {};
5486
+ document.getElementById('identity-label').textContent = primary.label || '\u2014';
5487
+ document.getElementById('identity-did').textContent = truncate(primary.did, 24);
5488
+ document.getElementById('identity-did').title = primary.did || '';
5489
+ document.getElementById('identity-pubkey').textContent = truncate(primary.publicKey, 24);
5490
+ document.getElementById('identity-pubkey').title = primary.publicKey || '';
5491
+ document.getElementById('identity-created').textContent = formatTime(primary.createdAt);
5492
+ document.getElementById('identity-count').textContent = data.identities?.length || '\u2014';
5493
+ }
5362
5494
 
5363
- const tier = req.tier || 1;
5364
- const tierClass = 'tier' + tier;
5365
- const pct = Math.max(0, Math.min(100, (req.remaining / TIMEOUT_SECONDS) * 100));
5366
- const isUrgent = req.remaining <= 30;
5495
+ async function updateHandshakes() {
5496
+ const data = await fetchAPI('/api/handshakes');
5497
+ if (!data) return;
5367
5498
 
5368
- item.innerHTML =
5369
- '<div class="pending-item-header">' +
5370
- '<div class="pending-item-op">' + esc(req.operation) + '</div>' +
5371
- '<div class="pending-item-tier ' + tierClass + '">T' + tier + '</div>' +
5372
- '</div>' +
5373
- '<div class="pending-item-reason">' + esc(req.reason) + '</div>' +
5374
- '<div class="pending-item-timer ' + (isUrgent ? 'urgent' : '') + '">' +
5375
- '<div class="pending-item-timer-bar">' +
5376
- '<div class="pending-item-timer-fill" style="width: ' + pct + '%"></div>' +
5377
- '</div>' +
5378
- '<span id="timer-' + id + '">' + req.remaining + 's</span>' +
5379
- '</div>' +
5380
- '<div class="pending-item-actions">' +
5381
- '<button class="btn btn-approve" onclick="handleApprove('' + id + '')">Approve</button>' +
5382
- '<button class="btn btn-deny" onclick="handleDeny('' + id + '')">Deny</button>' +
5383
- '</div>' +
5384
- '';
5499
+ apiState.handshakes = data.handshakes || [];
5385
5500
 
5386
- list.appendChild(item);
5387
- }
5388
- }
5501
+ document.getElementById('handshake-count').textContent = data.handshakes?.length || '0';
5389
5502
 
5390
- window.handleApprove = function(id) {
5391
- fetch('/api/approve/' + id, { method: 'POST', headers: authHeaders() }).then(() => {
5392
- removePendingRequest(id);
5393
- }).catch(() => {});
5394
- };
5503
+ if (data.handshakes && data.handshakes.length > 0) {
5504
+ const latest = data.handshakes[0];
5505
+ document.getElementById('handshake-latest').textContent = truncate(latest.counterpartyId, 20);
5506
+ document.getElementById('handshake-latest').title = latest.counterpartyId || '';
5507
+ document.getElementById('handshake-tier').textContent = (latest.trustTier || 'Unverified').toUpperCase();
5508
+ document.getElementById('handshake-time').textContent = formatTime(latest.completedAt);
5509
+ } else {
5510
+ document.getElementById('handshake-latest').textContent = '\u2014';
5511
+ document.getElementById('handshake-tier').textContent = 'Unverified';
5512
+ document.getElementById('handshake-time').textContent = '\u2014';
5513
+ }
5395
5514
 
5396
- window.handleDeny = function(id) {
5397
- fetch('/api/deny/' + id, { method: 'POST', headers: authHeaders() }).then(() => {
5398
- removePendingRequest(id);
5399
- }).catch(() => {});
5400
- };
5515
+ updateHandshakeTable(data.handshakes || []);
5516
+ }
5401
5517
 
5402
- // \u2500\u2500 Threats \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
5518
+ function updateHandshakeTable(handshakes) {
5519
+ const table = document.getElementById('handshake-table');
5403
5520
 
5404
- function addThreat(data) {
5405
- const {
5406
- timestamp,
5407
- severity,
5408
- type,
5409
- details
5410
- } = data;
5411
-
5412
- const threat = {
5413
- id: 'threat-' + threatCount++,
5414
- timestamp: timestamp || new Date().toISOString(),
5415
- severity: severity || 'medium',
5416
- type: type || 'unknown',
5417
- details: details || ''
5418
- };
5521
+ if (!handshakes || handshakes.length === 0) {
5522
+ table.innerHTML = '<div class="table-empty">No handshakes completed yet</div>';
5523
+ return;
5524
+ }
5419
5525
 
5420
- threatItems.unshift(threat);
5421
- if (threatItems.length > MAX_THREAT_ITEMS) {
5422
- threatItems.pop();
5526
+ table.innerHTML = handshakes
5527
+ .map(
5528
+ (hs) => \`
5529
+ <div class="table-row">
5530
+ <div class="table-cell strong">\${esc(truncate(hs.counterpartyId, 24))}</div>
5531
+ <div class="table-cell">\${esc(hs.trustTier || 'Unverified')}</div>
5532
+ <div class="table-cell">\${esc(hs.sovereigntyLevel || '\u2014')}</div>
5533
+ <div class="table-cell">\${hs.verified ? 'Yes' : 'No'}</div>
5534
+ <div class="table-cell">\${formatTime(hs.completedAt)}</div>
5535
+ <div class="table-cell">\${formatTime(hs.expiresAt)}</div>
5536
+ </div>
5537
+ \`
5538
+ )
5539
+ .join('');
5423
5540
  }
5424
5541
 
5425
- if (threatCount > 0) {
5426
- document.getElementById('threatPanel').classList.remove('collapsed');
5542
+ async function updateSHR() {
5543
+ const data = await fetchAPI('/api/shr');
5544
+ if (!data) return;
5545
+
5546
+ apiState.shr = data;
5547
+ renderSHRViewer(data);
5427
5548
  }
5428
5549
 
5429
- renderThreats();
5430
- }
5550
+ function renderSHRViewer(shr) {
5551
+ const viewer = document.getElementById('shr-viewer');
5431
5552
 
5432
- function renderThreats() {
5433
- const content = document.getElementById('threatContent');
5434
- const badge = document.getElementById('threatCount');
5553
+ if (!shr) {
5554
+ viewer.innerHTML = '<div class="empty-state">No SHR available</div>';
5555
+ return;
5556
+ }
5435
5557
 
5436
- if (threatItems.length === 0) {
5437
- content.innerHTML = '<div class="threat-empty">No threats detected</div>';
5438
- badge.textContent = '0';
5439
- return;
5440
- }
5558
+ let html = '';
5559
+
5560
+ // Implementation
5561
+ html += \`
5562
+ <div class="shr-section">
5563
+ <div class="shr-section-header">
5564
+ <div class="shr-toggle">\u25BC</div>
5565
+ <div>Implementation</div>
5566
+ </div>
5567
+ <div class="shr-section-content">
5568
+ <div class="shr-item">
5569
+ <div class="shr-key">sanctuary_version:</div>
5570
+ <div class="shr-value">\${esc(shr.implementation?.sanctuary_version || '\u2014')}</div>
5571
+ </div>
5572
+ <div class="shr-item">
5573
+ <div class="shr-key">node_version:</div>
5574
+ <div class="shr-value">\${esc(shr.implementation?.node_version || '\u2014')}</div>
5575
+ </div>
5576
+ <div class="shr-item">
5577
+ <div class="shr-key">generated_by:</div>
5578
+ <div class="shr-value">\${esc(shr.implementation?.generated_by || '\u2014')}</div>
5579
+ </div>
5580
+ </div>
5581
+ </div>
5582
+ \`;
5583
+
5584
+ // Metadata
5585
+ html += \`
5586
+ <div class="shr-section">
5587
+ <div class="shr-section-header">
5588
+ <div class="shr-toggle">\u25BC</div>
5589
+ <div>Metadata</div>
5590
+ </div>
5591
+ <div class="shr-section-content">
5592
+ <div class="shr-item">
5593
+ <div class="shr-key">instance_id:</div>
5594
+ <div class="shr-value">\${esc(truncate(shr.instance_id, 20))}</div>
5595
+ </div>
5596
+ <div class="shr-item">
5597
+ <div class="shr-key">generated_at:</div>
5598
+ <div class="shr-value">\${formatTime(shr.generated_at)}</div>
5599
+ </div>
5600
+ <div class="shr-item">
5601
+ <div class="shr-key">expires_at:</div>
5602
+ <div class="shr-value">\${formatTime(shr.expires_at)}</div>
5603
+ </div>
5604
+ </div>
5605
+ </div>
5606
+ \`;
5607
+
5608
+ // Layers
5609
+ if (shr.layers) {
5610
+ html += \`<div class="shr-section">
5611
+ <div class="shr-section-header">
5612
+ <div class="shr-toggle">\u25BC</div>
5613
+ <div>Layers</div>
5614
+ </div>
5615
+ <div class="shr-section-content">
5616
+ \`;
5617
+
5618
+ for (const [key, layer] of Object.entries(shr.layers)) {
5619
+ html += \`
5620
+ <div style="margin-bottom: 12px;">
5621
+ <div style="color: var(--blue); font-weight: 600; margin-bottom: 4px;">\${esc(key)}</div>
5622
+ <div style="padding-left: 12px;">
5623
+ \`;
5624
+
5625
+ for (const [lkey, lvalue] of Object.entries(layer || {})) {
5626
+ const displayValue =
5627
+ typeof lvalue === 'boolean'
5628
+ ? lvalue
5629
+ ? 'true'
5630
+ : 'false'
5631
+ : esc(String(lvalue));
5632
+ html += \`
5633
+ <div class="shr-item">
5634
+ <div class="shr-key">\${esc(lkey)}:</div>
5635
+ <div class="shr-value">\${displayValue}</div>
5636
+ </div>
5637
+ \`;
5638
+ }
5441
5639
 
5442
- badge.textContent = threatItems.length;
5443
- content.innerHTML = '';
5640
+ html += \`
5641
+ </div>
5642
+ </div>
5643
+ \`;
5644
+ }
5444
5645
 
5445
- for (const threat of threatItems) {
5446
- const div = document.createElement('div');
5447
- div.className = 'threat-item';
5448
- const time = new Date(threat.timestamp).toLocaleTimeString();
5449
- div.innerHTML =
5450
- '<div style="margin-bottom: 3px;">' +
5451
- '<span class="threat-item-type">' + esc(threat.type) + '</span>' +
5452
- '<span style="font-size: 10px; color: var(--text-secondary); margin-left: 6px;">' + esc(time) + '</span>' +
5453
- '</div>' +
5454
- '<div>' + esc(threat.details) + '</div>' +
5455
- '';
5456
- content.appendChild(div);
5646
+ html += \`
5647
+ </div>
5648
+ </div>
5649
+ \`;
5650
+ }
5651
+
5652
+ // Capabilities
5653
+ if (shr.capabilities) {
5654
+ html += \`
5655
+ <div class="shr-section">
5656
+ <div class="shr-section-header">
5657
+ <div class="shr-toggle">\u25BC</div>
5658
+ <div>Capabilities</div>
5659
+ </div>
5660
+ <div class="shr-section-content">
5661
+ \`;
5662
+
5663
+ for (const [key, value] of Object.entries(shr.capabilities)) {
5664
+ const displayValue = value ? 'true' : 'false';
5665
+ html += \`
5666
+ <div class="shr-item">
5667
+ <div class="shr-key">\${esc(key)}:</div>
5668
+ <div class="shr-value">\${displayValue}</div>
5669
+ </div>
5670
+ \`;
5671
+ }
5672
+
5673
+ html += \`
5674
+ </div>
5675
+ </div>
5676
+ \`;
5677
+ }
5678
+
5679
+ // Signature
5680
+ html += \`
5681
+ <div class="shr-section">
5682
+ <div class="shr-section-header">
5683
+ <div class="shr-toggle">\u25BC</div>
5684
+ <div>Signature</div>
5685
+ </div>
5686
+ <div class="shr-section-content">
5687
+ <div class="shr-item">
5688
+ <div class="shr-key">signed_by:</div>
5689
+ <div class="shr-value">\${esc(truncate(shr.signed_by, 20))}</div>
5690
+ </div>
5691
+ <div class="shr-item">
5692
+ <div class="shr-key">signature:</div>
5693
+ <div class="shr-value">\${esc(truncate(shr.signature, 32))}</div>
5694
+ </div>
5695
+ </div>
5696
+ </div>
5697
+ \`;
5698
+
5699
+ viewer.innerHTML = html;
5700
+
5701
+ // Add collapse functionality
5702
+ document.querySelectorAll('.shr-section-header').forEach((header) => {
5703
+ header.addEventListener('click', () => {
5704
+ header.closest('.shr-section').classList.toggle('collapsed');
5705
+ });
5706
+ });
5457
5707
  }
5458
- }
5459
5708
 
5460
- // \u2500\u2500 SSE Connection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
5709
+ async function updateStatus() {
5710
+ const data = await fetchAPI('/api/status');
5711
+ if (!data) return;
5461
5712
 
5462
- function reconnectSSE() {
5463
- if (evtSource) evtSource.close();
5464
- connect();
5465
- }
5713
+ apiState.status = data;
5466
5714
 
5467
- function connect() {
5468
- evtSource = new EventSource(sessionQuery('/events'));
5715
+ document.getElementById('protections-count').textContent = data.protectionsCount || '0';
5716
+ document.getElementById('uptime-value').textContent = formatUptime(data.uptime);
5469
5717
 
5470
- evtSource.onopen = () => {
5471
- document.getElementById('statusDot').classList.remove('disconnected');
5472
- };
5718
+ const connectionStatus = document.getElementById('connection-status');
5719
+ connectionStatus.classList.toggle('disconnected', !data.connected);
5720
+ }
5473
5721
 
5474
- evtSource.onerror = () => {
5475
- document.getElementById('statusDot').classList.add('disconnected');
5476
- };
5722
+ function formatUptime(seconds) {
5723
+ if (!seconds) return '\u2014';
5724
+ const hours = Math.floor(seconds / 3600);
5725
+ const minutes = Math.floor((seconds % 3600) / 60);
5726
+ if (hours > 0) return \`\${hours}h \${minutes}m\`;
5727
+ return \`\${minutes}m\`;
5728
+ }
5477
5729
 
5478
- evtSource.addEventListener('init', (e) => {
5479
- const data = JSON.parse(e.data);
5480
- if (data.baseline) {
5481
- updateBaseline(data.baseline);
5482
- }
5483
- if (data.policy) {
5484
- updatePolicy(data.policy);
5485
- }
5486
- if (data.pending) {
5487
- data.pending.forEach(addPendingRequest);
5488
- }
5489
- });
5730
+ // SSE Setup
5731
+ function setupSSE() {
5732
+ const eventSource = new EventSource(API_BASE + '/api/events', {
5733
+ headers: {
5734
+ 'Authorization': 'Bearer ' + AUTH_TOKEN,
5735
+ },
5736
+ });
5490
5737
 
5491
- evtSource.addEventListener('pending-request', (e) => {
5492
- const data = JSON.parse(e.data);
5493
- addPendingRequest(data);
5494
- });
5738
+ eventSource.addEventListener('init', (e) => {
5739
+ console.log('Connected to SSE');
5740
+ });
5495
5741
 
5496
- evtSource.addEventListener('request-resolved', (e) => {
5497
- const data = JSON.parse(e.data);
5498
- removePendingRequest(data.request_id);
5499
- });
5742
+ eventSource.addEventListener('sovereignty-update', () => {
5743
+ updateSovereignty();
5744
+ });
5500
5745
 
5501
- evtSource.addEventListener('tool-call', (e) => {
5502
- const data = JSON.parse(e.data);
5503
- addActivityItem({
5504
- timestamp: data.timestamp,
5505
- tier: data.tier || 1,
5506
- tool: data.tool || 'unknown',
5507
- outcome: data.outcome || 'executed',
5508
- detail: data.detail || ''
5746
+ eventSource.addEventListener('handshake-update', () => {
5747
+ updateHandshakes();
5509
5748
  });
5510
- });
5511
5749
 
5512
- evtSource.addEventListener('context-gate-decision', (e) => {
5513
- const data = JSON.parse(e.data);
5514
- addActivityItem({
5515
- timestamp: data.timestamp,
5516
- tier: data.tier || 1,
5517
- tool: data.tool || 'unknown',
5518
- outcome: data.outcome || 'gated',
5519
- detail: data.fields_filtered ? 'Filtered ' + data.fields_filtered + ' fields' : data.reason || '',
5520
- isContextGated: true
5750
+ eventSource.addEventListener('tool-call', (e) => {
5751
+ const data = JSON.parse(e.data);
5752
+ addActivityItem({
5753
+ type: 'tool-call',
5754
+ title: 'Tool Call',
5755
+ content: data.toolName,
5756
+ timestamp: new Date().toISOString(),
5757
+ });
5521
5758
  });
5522
- });
5523
5759
 
5524
- evtSource.addEventListener('injection-alert', (e) => {
5525
- const data = JSON.parse(e.data);
5526
- addActivityItem({
5527
- timestamp: data.timestamp,
5528
- tier: data.tier || 2,
5529
- tool: data.tool || 'unknown',
5530
- outcome: data.allowed ? 'allowed' : 'denied',
5531
- detail: data.signal || 'Injection detected',
5532
- hasInjection: true
5760
+ eventSource.addEventListener('context-gate-decision', (e) => {
5761
+ const data = JSON.parse(e.data);
5762
+ addActivityItem({
5763
+ type: 'context-gate',
5764
+ title: 'Context Gate',
5765
+ content: data.decision,
5766
+ timestamp: new Date().toISOString(),
5767
+ });
5533
5768
  });
5534
- addThreat({
5535
- timestamp: data.timestamp,
5536
- severity: data.severity || 'medium',
5537
- type: 'Injection Alert',
5538
- details: data.signal || 'Suspicious pattern detected'
5769
+
5770
+ eventSource.addEventListener('injection-alert', (e) => {
5771
+ const data = JSON.parse(e.data);
5772
+ addActivityItem({
5773
+ type: 'injection',
5774
+ title: 'Injection Alert',
5775
+ content: data.pattern,
5776
+ timestamp: new Date().toISOString(),
5777
+ });
5778
+ addThreatAlert(data);
5539
5779
  });
5540
- });
5541
5780
 
5542
- evtSource.addEventListener('protection-status', (e) => {
5543
- const data = JSON.parse(e.data);
5544
- updateProtectionStatus(data);
5545
- });
5781
+ eventSource.addEventListener('pending-request', (e) => {
5782
+ const data = JSON.parse(e.data);
5783
+ addPendingRequest(data);
5784
+ });
5546
5785
 
5547
- evtSource.addEventListener('audit-entry', (e) => {
5548
- const data = JSON.parse(e.data);
5549
- // Audit entries don't show in activity by default, but we could add them
5550
- });
5786
+ eventSource.addEventListener('request-resolved', (e) => {
5787
+ const data = JSON.parse(e.data);
5788
+ removePendingRequest(data.requestId);
5789
+ });
5551
5790
 
5552
- evtSource.addEventListener('baseline-update', (e) => {
5553
- const data = JSON.parse(e.data);
5554
- updateBaseline(data);
5555
- });
5556
- }
5791
+ eventSource.onerror = () => {
5792
+ console.error('SSE error');
5793
+ setTimeout(setupSSE, 5000);
5794
+ };
5795
+ }
5557
5796
 
5558
- function updateBaseline(baseline) {
5559
- if (!baseline) return;
5560
- // Update baseline-derived stats if needed
5561
- }
5797
+ // Activity Feed
5798
+ function addActivityItem(item) {
5799
+ activityLog.unshift(item);
5800
+ if (activityLog.length > maxActivityItems) {
5801
+ activityLog.pop();
5802
+ }
5562
5803
 
5563
- function updatePolicy(policy) {
5564
- if (!policy) return;
5565
- // Update policy-derived stats
5566
- if (policy.approval_channel) {
5567
- // Policy info updated
5568
- }
5569
- }
5804
+ const feed = document.getElementById('activity-feed');
5805
+ const html = \`
5806
+ <div class="activity-item \${item.type}">
5807
+ <div class="activity-type">\${esc(item.title)}</div>
5808
+ <div class="activity-content">\${esc(item.content)}</div>
5809
+ <div class="activity-time">\${formatTime(item.timestamp)}</div>
5810
+ </div>
5811
+ \`;
5570
5812
 
5571
- function updateProtectionStatus(status) {
5572
- if (status.sovereignty_score !== undefined) {
5573
- updateSovereigntyScore(status.sovereignty_score);
5574
- }
5575
- if (status.active_protections !== undefined) {
5576
- document.getElementById('activeProtections').textContent = status.active_protections;
5577
- }
5578
- // Update individual protection cards
5579
- if (status.encryption !== undefined) {
5580
- const el = document.getElementById('encryptionStatus');
5581
- el.className = 'protection-card-status ' + (status.encryption ? 'active' : 'inactive');
5582
- el.textContent = status.encryption ? '\u2713 Active' : '\u2717 Inactive';
5583
- }
5584
- if (status.approval_gate !== undefined) {
5585
- const el = document.getElementById('approvalStatus');
5586
- el.className = 'protection-card-status ' + (status.approval_gate ? 'active' : 'inactive');
5587
- el.textContent = status.approval_gate ? '\u2713 Active' : '\u2717 Inactive';
5588
- }
5589
- if (status.context_gating !== undefined) {
5590
- const el = document.getElementById('contextStatus');
5591
- el.className = 'protection-card-status ' + (status.context_gating ? 'active' : 'inactive');
5592
- el.textContent = status.context_gating ? '\u2713 Active' : '\u2717 Inactive';
5593
- }
5594
- if (status.injection_detection !== undefined) {
5595
- const el = document.getElementById('injectionStatus');
5596
- el.className = 'protection-card-status ' + (status.injection_detection ? 'active' : 'inactive');
5597
- el.textContent = status.injection_detection ? '\u2713 Active' : '\u2717 Inactive';
5813
+ if (feed.querySelector('.empty-state')) {
5814
+ feed.innerHTML = '';
5815
+ }
5816
+
5817
+ feed.insertAdjacentHTML('afterbegin', html);
5818
+
5819
+ if (feed.children.length > maxActivityItems) {
5820
+ feed.lastChild.remove();
5821
+ }
5598
5822
  }
5599
- if (status.baseline !== undefined) {
5600
- const el = document.getElementById('baselineStatus');
5601
- el.className = 'protection-card-status ' + (status.baseline ? 'active' : 'inactive');
5602
- el.textContent = status.baseline ? '\u2713 Active' : '\u2717 Inactive';
5823
+
5824
+ // Pending Requests
5825
+ function addPendingRequest(request) {
5826
+ pendingRequests.set(request.requestId, {
5827
+ id: request.requestId,
5828
+ title: request.title,
5829
+ details: request.details,
5830
+ expiresAt: new Date(Date.now() + TIMEOUT_SECONDS * 1000),
5831
+ });
5832
+
5833
+ updatePendingDisplay();
5603
5834
  }
5604
- if (status.audit_trail !== undefined) {
5605
- const el = document.getElementById('auditStatus');
5606
- el.className = 'protection-card-status ' + (status.audit_trail ? 'active' : 'inactive');
5607
- el.textContent = status.audit_trail ? '\u2713 Active' : '\u2717 Inactive';
5835
+
5836
+ function removePendingRequest(requestId) {
5837
+ pendingRequests.delete(requestId);
5838
+ updatePendingDisplay();
5608
5839
  }
5609
- }
5610
5840
 
5611
- // \u2500\u2500 Initialization \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
5841
+ function updatePendingDisplay() {
5842
+ const badge = document.getElementById('pending-item-badge');
5843
+ const count = pendingRequests.size;
5844
+
5845
+ if (count > 0) {
5846
+ document.getElementById('pending-count').textContent = count;
5847
+ badge.style.display = 'flex';
5848
+ } else {
5849
+ badge.style.display = 'none';
5850
+ }
5851
+
5852
+ const overlay = document.getElementById('pending-overlay');
5853
+ const items = document.getElementById('pending-items');
5854
+
5855
+ if (count === 0) {
5856
+ items.innerHTML = '';
5857
+ overlay.classList.remove('show');
5858
+ return;
5859
+ }
5860
+
5861
+ let html = '';
5862
+ for (const req of pendingRequests.values()) {
5863
+ const remaining = Math.max(0, Math.floor((req.expiresAt - Date.now()) / 1000));
5864
+ html += \`
5865
+ <div class="pending-item">
5866
+ <div class="pending-title">\${esc(req.title)}</div>
5867
+ <div class="pending-countdown">Expires in \${remaining}s</div>
5868
+ <div class="pending-actions">
5869
+ <button class="pending-btn pending-approve" data-id="\${req.id}">Approve</button>
5870
+ <button class="pending-btn pending-deny" data-id="\${req.id}">Deny</button>
5871
+ </div>
5872
+ </div>
5873
+ \`;
5874
+ }
5875
+
5876
+ items.innerHTML = html;
5877
+
5878
+ document.querySelectorAll('.pending-approve').forEach((btn) => {
5879
+ btn.addEventListener('click', async () => {
5880
+ const id = btn.getAttribute('data-id');
5881
+ await fetchAPI(\`/api/approve/\${id}\`);
5882
+ });
5883
+ });
5612
5884
 
5613
- (async function init() {
5614
- await exchangeSession();
5615
- // Clean legacy ?token= from URL
5616
- if (window.location.search.includes('token=')) {
5617
- window.history.replaceState({}, '', window.location.pathname);
5885
+ document.querySelectorAll('.pending-deny').forEach((btn) => {
5886
+ btn.addEventListener('click', async () => {
5887
+ const id = btn.getAttribute('data-id');
5888
+ await fetchAPI(\`/api/deny/\${id}\`);
5889
+ });
5890
+ });
5618
5891
  }
5619
- connect();
5620
5892
 
5621
- // Start uptime ticker
5622
- setInterval(updateUptime, 1000);
5623
- updateUptime();
5893
+ // Threat Panel
5894
+ function addThreatAlert(alert) {
5895
+ const panel = document.querySelector('.threat-panel');
5896
+ const content = document.getElementById('threat-alerts');
5624
5897
 
5625
- // Pending request countdown timer
5626
- setInterval(() => {
5627
- for (const [id, req] of pendingRequests) {
5628
- req.remaining = Math.max(0, req.remaining - 1);
5629
- const el = document.getElementById('timer-' + id);
5630
- if (el) {
5631
- el.textContent = req.remaining + 's';
5632
- }
5898
+ if (content.querySelector('.empty-state')) {
5899
+ content.innerHTML = '';
5633
5900
  }
5634
- }, 1000);
5635
5901
 
5636
- // Load initial status
5637
- try {
5638
- const resp = await fetch('/api/status', { headers: authHeaders() });
5639
- if (resp.ok) {
5640
- const status = await resp.json();
5641
- if (status.baseline) updateBaseline(status.baseline);
5642
- if (status.policy) updatePolicy(status.policy);
5902
+ panel.classList.remove('collapsed');
5903
+
5904
+ const html = \`
5905
+ <div class="threat-alert">
5906
+ <div class="threat-type">\${esc(alert.type || 'Injection Alert')}</div>
5907
+ <div class="threat-message">\${esc(alert.message || alert.pattern || '\u2014')}</div>
5908
+ </div>
5909
+ \`;
5910
+
5911
+ content.insertAdjacentHTML('afterbegin', html);
5912
+
5913
+ const alerts = content.querySelectorAll('.threat-alert');
5914
+ if (alerts.length > 10) {
5915
+ alerts[alerts.length - 1].remove();
5643
5916
  }
5644
- } catch (e) {
5645
- // Ignore
5646
5917
  }
5647
- })();
5648
5918
 
5649
- })();
5650
- </script>
5919
+ // Threat Panel Toggle
5920
+ document.querySelector('.threat-header').addEventListener('click', () => {
5921
+ document.querySelector('.threat-panel').classList.toggle('collapsed');
5922
+ });
5923
+
5924
+ // SHR Copy Button
5925
+ document.getElementById('copy-shr-btn').addEventListener('click', async () => {
5926
+ if (!apiState.shr) return;
5927
+
5928
+ const json = JSON.stringify(apiState.shr, null, 2);
5929
+ try {
5930
+ await navigator.clipboard.writeText(json);
5931
+ const btn = document.getElementById('copy-shr-btn');
5932
+ const original = btn.textContent;
5933
+ btn.textContent = 'Copied!';
5934
+ setTimeout(() => {
5935
+ btn.textContent = original;
5936
+ }, 2000);
5937
+ } catch (err) {
5938
+ console.error('Copy failed:', err);
5939
+ }
5940
+ });
5941
+
5942
+ // Pending Overlay Toggle
5943
+ document.getElementById('pending-item-badge').addEventListener('click', () => {
5944
+ document.getElementById('pending-overlay').classList.toggle('show');
5945
+ });
5946
+
5947
+ // Initialize
5948
+ async function initialize() {
5949
+ if (!AUTH_TOKEN) {
5950
+ redirectToLogin();
5951
+ return;
5952
+ }
5953
+
5954
+ // Initial data fetch
5955
+ await Promise.all([
5956
+ updateSovereignty(),
5957
+ updateIdentity(),
5958
+ updateHandshakes(),
5959
+ updateSHR(),
5960
+ updateStatus(),
5961
+ ]);
5962
+
5963
+ // Setup SSE for real-time updates
5964
+ setupSSE();
5965
+
5966
+ // Refresh status periodically
5967
+ setInterval(updateStatus, 30000);
5968
+ }
5651
5969
 
5970
+ // Start
5971
+ initialize();
5972
+ </script>
5652
5973
  </body>
5653
5974
  </html>`;
5654
5975
  }
@@ -8080,6 +8401,154 @@ function deriveTrustTier(level) {
8080
8401
  }
8081
8402
  }
8082
8403
 
8404
+ // src/handshake/attestation.ts
8405
+ init_encoding();
8406
+ init_encoding();
8407
+ var ATTESTATION_VERSION = "1.0";
8408
+ function deriveTrustTier2(level) {
8409
+ switch (level) {
8410
+ case "full":
8411
+ return "verified-sovereign";
8412
+ case "degraded":
8413
+ return "verified-degraded";
8414
+ default:
8415
+ return "unverified";
8416
+ }
8417
+ }
8418
+ function generateAttestation(opts) {
8419
+ const {
8420
+ attesterSHR,
8421
+ subjectSHR,
8422
+ verificationResult,
8423
+ mutual = false,
8424
+ identityManager,
8425
+ masterKey,
8426
+ identityId
8427
+ } = opts;
8428
+ const identity = identityId ? identityManager.get(identityId) : identityManager.getDefault();
8429
+ if (!identity) {
8430
+ return { error: "No identity available for signing attestation" };
8431
+ }
8432
+ const now = /* @__PURE__ */ new Date();
8433
+ const attesterExpiry = new Date(attesterSHR.body.expires_at);
8434
+ const subjectExpiry = new Date(subjectSHR.body.expires_at);
8435
+ const earliestExpiry = attesterExpiry < subjectExpiry ? attesterExpiry : subjectExpiry;
8436
+ const sovereigntyLevel = verificationResult.valid ? verificationResult.sovereignty_level : "unverified";
8437
+ const body = {
8438
+ attestation_version: ATTESTATION_VERSION,
8439
+ attester_id: attesterSHR.body.instance_id,
8440
+ subject_id: subjectSHR.body.instance_id,
8441
+ attester_shr: attesterSHR,
8442
+ subject_shr: subjectSHR,
8443
+ verification: {
8444
+ subject_shr_valid: verificationResult.valid,
8445
+ subject_sovereignty_level: sovereigntyLevel,
8446
+ subject_trust_tier: deriveTrustTier2(sovereigntyLevel),
8447
+ mutual,
8448
+ errors: verificationResult.errors,
8449
+ warnings: verificationResult.warnings
8450
+ },
8451
+ attested_at: now.toISOString(),
8452
+ expires_at: earliestExpiry.toISOString()
8453
+ };
8454
+ const canonical = JSON.stringify(deepSortKeys(body));
8455
+ const payload = stringToBytes(canonical);
8456
+ const encryptionKey = derivePurposeKey(masterKey, "identity-encryption");
8457
+ const signatureBytes = sign(
8458
+ payload,
8459
+ identity.encrypted_private_key,
8460
+ encryptionKey
8461
+ );
8462
+ const summary = generateSummary(body);
8463
+ return {
8464
+ body,
8465
+ signed_by: identity.public_key,
8466
+ signature: toBase64url(signatureBytes),
8467
+ summary
8468
+ };
8469
+ }
8470
+ function layerLine(label, status) {
8471
+ const icon = status === "active" ? "\u2713" : status === "degraded" ? "~" : "x";
8472
+ return ` ${icon} ${label}: ${status}`;
8473
+ }
8474
+ function generateSummary(body) {
8475
+ const v = body.verification;
8476
+ const sLayers = body.subject_shr.body.layers;
8477
+ const aLayers = body.attester_shr.body.layers;
8478
+ const tierLabel = v.subject_trust_tier === "verified-sovereign" ? "Verified Sovereign" : v.subject_trust_tier === "verified-degraded" ? "Verified (Degraded)" : "Unverified";
8479
+ const lines = [
8480
+ `--- Sovereignty Attestation ---`,
8481
+ ``,
8482
+ `Attester: ${body.attester_id.slice(0, 16)}...`,
8483
+ `Subject: ${body.subject_id.slice(0, 16)}...`,
8484
+ `Result: ${tierLabel}`,
8485
+ ``,
8486
+ `Subject Sovereignty Posture:`,
8487
+ layerLine("L1 Cognitive Sovereignty", sLayers.l1.status),
8488
+ layerLine("L2 Operational Isolation", sLayers.l2.status),
8489
+ layerLine("L3 Selective Disclosure", sLayers.l3.status),
8490
+ layerLine("L4 Verifiable Reputation", sLayers.l4.status),
8491
+ ``,
8492
+ `Attester Sovereignty Posture:`,
8493
+ layerLine("L1 Cognitive Sovereignty", aLayers.l1.status),
8494
+ layerLine("L2 Operational Isolation", aLayers.l2.status),
8495
+ layerLine("L3 Selective Disclosure", aLayers.l3.status),
8496
+ layerLine("L4 Verifiable Reputation", aLayers.l4.status),
8497
+ ``,
8498
+ `Mutual: ${v.mutual ? "Yes" : "One-sided"}`,
8499
+ `Attested: ${body.attested_at}`,
8500
+ `Expires: ${body.expires_at}`,
8501
+ `Signature: ${body.attestation_version} / Ed25519`
8502
+ ];
8503
+ if (v.warnings.length > 0) {
8504
+ lines.push(``, `Warnings: ${v.warnings.join("; ")}`);
8505
+ }
8506
+ if (v.errors.length > 0) {
8507
+ lines.push(``, `Errors: ${v.errors.join("; ")}`);
8508
+ }
8509
+ lines.push(``, `--- Verify: compare signed_by against attester's known public key ---`);
8510
+ return lines.join("\n");
8511
+ }
8512
+ function verifyAttestation(attestation, now) {
8513
+ const errors = [];
8514
+ const currentTime = now ?? /* @__PURE__ */ new Date();
8515
+ if (attestation.body.attestation_version !== ATTESTATION_VERSION) {
8516
+ errors.push(
8517
+ `Unsupported attestation version: ${attestation.body.attestation_version}`
8518
+ );
8519
+ }
8520
+ if (!attestation.body.attester_id || !attestation.body.subject_id) {
8521
+ errors.push("Missing attester_id or subject_id");
8522
+ }
8523
+ if (!attestation.body.attester_shr || !attestation.body.subject_shr) {
8524
+ errors.push("Missing attester or subject SHR");
8525
+ }
8526
+ const expired = new Date(attestation.body.expires_at) <= currentTime;
8527
+ if (expired) {
8528
+ errors.push("Attestation has expired");
8529
+ }
8530
+ try {
8531
+ const publicKey = fromBase64url(attestation.signed_by);
8532
+ const canonical = JSON.stringify(deepSortKeys(attestation.body));
8533
+ const payload = stringToBytes(canonical);
8534
+ const signatureBytes = fromBase64url(attestation.signature);
8535
+ const signatureValid = verify(payload, signatureBytes, publicKey);
8536
+ if (!signatureValid) {
8537
+ errors.push("Attestation signature is invalid");
8538
+ }
8539
+ } catch (e) {
8540
+ errors.push(`Signature verification error: ${e.message}`);
8541
+ }
8542
+ return {
8543
+ valid: errors.length === 0,
8544
+ errors,
8545
+ attester_id: attestation.body.attester_id ?? "unknown",
8546
+ subject_id: attestation.body.subject_id ?? "unknown",
8547
+ trust_tier: errors.length === 0 ? attestation.body.verification.subject_trust_tier : "unverified",
8548
+ expired
8549
+ };
8550
+ }
8551
+
8083
8552
  // src/handshake/tools.ts
8084
8553
  function createHandshakeTools(config, identityManager, masterKey, auditLog) {
8085
8554
  const sessions = /* @__PURE__ */ new Map();
@@ -8265,6 +8734,103 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
8265
8734
  result: session.result ?? null
8266
8735
  });
8267
8736
  }
8737
+ },
8738
+ // ─── Streamlined Exchange ─────────────────────────────────────────
8739
+ {
8740
+ name: "sanctuary/handshake_exchange",
8741
+ 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).",
8742
+ inputSchema: {
8743
+ type: "object",
8744
+ properties: {
8745
+ counterparty_shr: {
8746
+ type: "object",
8747
+ description: "The counterparty's signed SHR (SignedSHR object with body, signed_by, signature)."
8748
+ },
8749
+ identity_id: {
8750
+ type: "string",
8751
+ description: "Identity to use for the exchange. Defaults to primary identity."
8752
+ }
8753
+ },
8754
+ required: ["counterparty_shr"]
8755
+ },
8756
+ handler: async (args) => {
8757
+ const counterpartySHR = args.counterparty_shr;
8758
+ const ourSHR = generateSHR(args.identity_id, shrOpts);
8759
+ if (typeof ourSHR === "string") {
8760
+ return toolResult({ error: ourSHR });
8761
+ }
8762
+ const verificationResult = verifySHR(counterpartySHR);
8763
+ const attestation = generateAttestation({
8764
+ attesterSHR: ourSHR,
8765
+ subjectSHR: counterpartySHR,
8766
+ verificationResult,
8767
+ mutual: false,
8768
+ identityManager,
8769
+ masterKey,
8770
+ identityId: args.identity_id
8771
+ });
8772
+ if ("error" in attestation) {
8773
+ auditLog.append("l4", "handshake_exchange", ourSHR.body.instance_id, void 0, "failure");
8774
+ return toolResult({ error: attestation.error });
8775
+ }
8776
+ if (verificationResult.valid) {
8777
+ const sovereigntyLevel = verificationResult.sovereignty_level;
8778
+ const trustTier = sovereigntyLevel === "full" ? "verified-sovereign" : sovereigntyLevel === "degraded" ? "verified-degraded" : "unverified";
8779
+ handshakeResults.set(verificationResult.counterparty_id, {
8780
+ counterparty_id: verificationResult.counterparty_id,
8781
+ counterparty_shr: counterpartySHR,
8782
+ verified: true,
8783
+ sovereignty_level: sovereigntyLevel,
8784
+ trust_tier: trustTier,
8785
+ completed_at: (/* @__PURE__ */ new Date()).toISOString(),
8786
+ expires_at: verificationResult.expires_at,
8787
+ errors: []
8788
+ });
8789
+ }
8790
+ auditLog.append("l4", "handshake_exchange", ourSHR.body.instance_id);
8791
+ return toolResult({
8792
+ attestation,
8793
+ our_shr: ourSHR,
8794
+ verification: {
8795
+ counterparty_valid: verificationResult.valid,
8796
+ counterparty_sovereignty: verificationResult.sovereignty_level,
8797
+ counterparty_id: verificationResult.counterparty_id,
8798
+ errors: verificationResult.errors,
8799
+ warnings: verificationResult.warnings
8800
+ },
8801
+ 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.",
8802
+ _content_trust: "external"
8803
+ });
8804
+ }
8805
+ },
8806
+ {
8807
+ name: "sanctuary/handshake_verify_attestation",
8808
+ description: "Verify a signed attestation artifact from another agent. Checks the Ed25519 signature, temporal validity, and structural integrity.",
8809
+ inputSchema: {
8810
+ type: "object",
8811
+ properties: {
8812
+ attestation: {
8813
+ type: "object",
8814
+ description: "The SignedAttestation object to verify (body, signed_by, signature, summary)."
8815
+ }
8816
+ },
8817
+ required: ["attestation"]
8818
+ },
8819
+ handler: async (args) => {
8820
+ const attestation = args.attestation;
8821
+ const result = verifyAttestation(attestation);
8822
+ auditLog.append(
8823
+ "l4",
8824
+ "handshake_verify_attestation",
8825
+ result.attester_id,
8826
+ void 0,
8827
+ result.valid ? "success" : "failure"
8828
+ );
8829
+ return toolResult({
8830
+ ...result,
8831
+ _content_trust: "external"
8832
+ });
8833
+ }
8268
8834
  }
8269
8835
  ];
8270
8836
  return { tools, handshakeResults };
@@ -12222,6 +12788,7 @@ async function createSanctuaryServer(options) {
12222
12788
  return { server, config };
12223
12789
  }
12224
12790
 
12791
+ exports.ATTESTATION_VERSION = ATTESTATION_VERSION;
12225
12792
  exports.ApprovalGate = ApprovalGate;
12226
12793
  exports.AuditLog = AuditLog;
12227
12794
  exports.AutoApproveChannel = AutoApproveChannel;
@@ -12253,6 +12820,7 @@ exports.createRangeProof = createRangeProof;
12253
12820
  exports.createSanctuaryServer = createSanctuaryServer;
12254
12821
  exports.evaluateField = evaluateField;
12255
12822
  exports.filterContext = filterContext;
12823
+ exports.generateAttestation = generateAttestation;
12256
12824
  exports.generateSHR = generateSHR;
12257
12825
  exports.getTemplate = getTemplate;
12258
12826
  exports.initiateHandshake = initiateHandshake;
@@ -12264,6 +12832,7 @@ exports.resolveTier = resolveTier;
12264
12832
  exports.respondToHandshake = respondToHandshake;
12265
12833
  exports.signPayload = signPayload;
12266
12834
  exports.tierDistribution = tierDistribution;
12835
+ exports.verifyAttestation = verifyAttestation;
12267
12836
  exports.verifyBridgeCommitment = verifyBridgeCommitment;
12268
12837
  exports.verifyCompletion = verifyCompletion;
12269
12838
  exports.verifyPedersenCommitment = verifyPedersenCommitment;