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