total-recall-brain 3.41.2 → 3.41.4

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.
Files changed (128) hide show
  1. package/bin/total-recall.mjs +19 -4
  2. package/frontend/dist/assets/{index-BG9NkVUy.css → index-BIQmqDKP.css} +1 -1
  3. package/frontend/dist/assets/{index-BtuiZqwP.js → index-CRzjQLu7.js} +7 -7
  4. package/frontend/dist/index.html +2 -2
  5. package/frontend/dist/sw.js +8 -5
  6. package/frontend/src/api/auth.ts +0 -3
  7. package/frontend/src/components/plugins/PluginPanel.tsx +7 -3
  8. package/frontend/src/components/plugins/card/CardEngine.tsx +1 -1
  9. package/frontend/src/pages/LoginPage.tsx +1 -41
  10. package/frontend/src/pages/PluginsPage.tsx +14 -7
  11. package/package.json +7 -5
  12. package/scaffold/.agent/skills/plugin/scripts/record.mjs +6 -3
  13. package/scaffold/.agent/skills/total-recall/plugins/auth/cli.mjs +197 -31
  14. package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +6 -6
  15. package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +1 -1
  16. package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +8 -1
  17. package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +48 -0
  18. package/scaffold/.agent/skills/total-recall/plugins/auth/src/login-projections.mjs +42 -0
  19. package/scaffold/.agent/skills/total-recall/plugins/auth/src/mesh-secrets.mjs +18 -0
  20. package/scaffold/.agent/skills/total-recall/plugins/auth/src/routes/secrets.mjs +5 -2
  21. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-carriers.mjs +76 -0
  22. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-prune.mjs +37 -0
  23. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-sync.mjs +53 -17
  24. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/DESIGN.md +56 -8
  25. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/card.js +1068 -204
  26. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/panel.js +4 -2
  27. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/DESIGN.md +32 -0
  28. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/card.js +436 -274
  29. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +12 -12
  30. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +1 -1
  31. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/DESIGN.md +16 -2
  32. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/card.js +461 -242
  33. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +12 -12
  34. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +1 -1
  35. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +1 -7
  36. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/DESIGN.md +17 -2
  37. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/card.js +426 -300
  38. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +23 -8
  39. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +6 -2
  40. package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/DESIGN.md +32 -0
  41. package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/card.js +350 -283
  42. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +23 -8
  43. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package.json +5 -1
  44. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/ui/card.js +31 -19
  45. package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +23 -10
  46. package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +5 -3
  47. package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +1 -1
  48. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/cli/grants.mjs +7 -3
  49. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/cli/mesh.mjs +61 -38
  50. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/leader-election.mjs +57 -2
  51. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/mesh.mjs +220 -36
  52. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/server/routes/mesh.mjs +7 -2
  53. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/README.md +65 -0
  54. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/BRICK.md +59 -0
  55. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/index.mjs +534 -0
  56. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +139 -0
  57. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +2849 -0
  58. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +26 -0
  59. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +376 -0
  60. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/skills/meta-harness/SKILL.md +37 -0
  61. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/adapters/dsh.mjs +446 -0
  62. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/alerts.mjs +23 -0
  63. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +398 -0
  64. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/documentation.mjs +152 -0
  65. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/executor.mjs +82 -0
  66. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/learner.mjs +90 -0
  67. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +415 -0
  68. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/runtime.mjs +36 -0
  69. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/setup.mjs +35 -0
  70. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/tracker.mjs +133 -0
  71. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +233 -0
  72. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/DESIGN.md +32 -0
  73. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/card.js +845 -0
  74. package/scaffold/.agent/skills/total-recall/plugins/openwiki/capability/BRICK.md +64 -0
  75. package/scaffold/.agent/skills/total-recall/plugins/openwiki/capability/index.mjs +250 -0
  76. package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +344 -14
  77. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +2917 -140
  78. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +9 -3
  79. package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +223 -5
  80. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/config.mjs +129 -0
  81. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +355 -0
  82. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/upstream-watcher.mjs +375 -0
  83. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/DESIGN.md +37 -10
  84. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/card.js +986 -280
  85. package/scaffold/.agent/skills/total-recall/plugins/security/cli.mjs +7 -1
  86. package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +25 -10
  87. package/scaffold/.agent/skills/total-recall/plugins/security/package.json +6 -2
  88. package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +1 -1
  89. package/scaffold/.agent/skills/total-recall/plugins/security/src/scanner.mjs +17 -3
  90. package/scaffold/.agent/skills/total-recall/plugins/security/ui/DESIGN.md +38 -15
  91. package/scaffold/.agent/skills/total-recall/plugins/security/ui/card.js +60 -34
  92. package/src/cli/command.mjs +23 -18
  93. package/src/cli/config.mjs +3 -0
  94. package/src/cli/connect.mjs +12 -116
  95. package/src/cli/daemon.mjs +23 -1
  96. package/src/cli/harness.mjs +0 -1
  97. package/src/cli/plugin/copies.mjs +8 -5
  98. package/src/cli/plugin/create.mjs +56 -3
  99. package/src/cli/plugin/index.mjs +40 -5
  100. package/src/cli/plugin/monetization.mjs +232 -12
  101. package/src/cli/preflight.mjs +161 -0
  102. package/src/cli/rebuild.mjs +22 -0
  103. package/src/cli/reset-password.mjs +3 -2
  104. package/src/cli/skill.mjs +8 -2
  105. package/src/cli/status.mjs +25 -7
  106. package/src/core/automatic-brain-updates.mjs +9 -3
  107. package/src/core/brain-updates.mjs +63 -5
  108. package/src/core/cli-commands.mjs +2 -0
  109. package/src/core/context-compiler.mjs +8 -1
  110. package/src/core/default-plugins.mjs +1 -1
  111. package/src/core/global-brain-updates.mjs +21 -1
  112. package/src/core/mesh-brain-updates.mjs +5 -2
  113. package/src/core/meta-harness.mjs +32 -79
  114. package/src/core/plugin-dependencies.mjs +6 -1
  115. package/src/core/plugin-runner.mjs +6 -1
  116. package/src/core/plugin-store.mjs +67 -1
  117. package/src/core/plugin-updates.mjs +1 -1
  118. package/src/core/portable-skills-maintenance.mjs +19 -0
  119. package/src/core/preflight-gate.mjs +77 -0
  120. package/src/core/preflight-receipt.mjs +123 -0
  121. package/src/core/required-plugins.mjs +7 -0
  122. package/src/core/runtime.mjs +0 -3
  123. package/src/core/skills-registry.mjs +33 -3
  124. package/src/core/surface.mjs +38 -1
  125. package/src/server/auth.mjs +41 -1
  126. package/src/server/memory-app.mjs +2 -1
  127. package/src/server/routes/auth.mjs +25 -1
  128. package/src/server/routes/plugins.mjs +17 -4
@@ -1,94 +1,217 @@
1
1
  /**
2
- * Auth & Secrets — Hardware WebAuthn Biometric FIDO2 Passkeys & Enclave Security
3
- * Purpose-built domain interface: queries live auth status on mount, renders real
4
- * passkey and OAuth telemetry, and exposes authentic administrative controls.
2
+ * Auth & Secrets — Hardware WebAuthn FIDO2, OAuth Identity & Secrets Store.
5
3
  *
6
- * Zero emojis, authentic commercial brand identity, touch-first ergonomics (>=44px).
4
+ * This face is purpose-built for the operator: it provides an instant
5
+ * verdict on the secrets store, hardware biometric passkeys, Google OAuth
6
+ * account tokens, and system security audit baseline findings.
7
+ *
8
+ * Every value traces directly to a host command read. A value the command did not
9
+ * supply renders an em dash with its reason; nothing is seeded, and nothing is
10
+ * defaulted to a reassuring word.
11
+ *
12
+ * Design rationale: ui/DESIGN.md.
7
13
  */
8
14
 
15
+ const DASH = '\u2014';
16
+
17
+ function escapeHtml(value) {
18
+ if (value === null || value === undefined) return '';
19
+ return String(value).replace(/[&<>"']/g, (c) => ({
20
+ '&': '&amp;',
21
+ '<': '&lt;',
22
+ '>': '&gt;',
23
+ '"': '&quot;',
24
+ "'": '&#39;',
25
+ }[c]));
26
+ }
27
+
28
+ function asObject(result) {
29
+ if (result === null || result === undefined) return null;
30
+ if (typeof result === 'object') {
31
+ if (typeof result.output === 'string') {
32
+ try {
33
+ const parsed = JSON.parse(result.output);
34
+ return typeof parsed === 'object' ? parsed : null;
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+ return result;
40
+ }
41
+ if (typeof result === 'string') {
42
+ try {
43
+ const parsed = JSON.parse(result);
44
+ return typeof parsed === 'object' ? parsed : null;
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+ return null;
50
+ }
51
+
9
52
  export class AuthCard extends HTMLElement {
10
53
  constructor() {
11
54
  super();
12
- this.executing = false;
13
- this.telemetry = {
14
- passkeys: '—',
15
- oauth: '—',
16
- status: 'Checking...'
17
- };
55
+ this.screen = 'secrets';
56
+ this.busy = false;
57
+ this.statusData = null;
58
+ this.passkeysData = null;
59
+ this.googleData = null;
60
+ this.secretsData = null;
61
+ this.auditData = null;
62
+ this.lastError = null;
63
+ this.lastAttempt = null;
64
+ this.activeFilter = 'all';
65
+ this.confirmAction = null;
66
+ }
67
+
68
+ get screens() {
69
+ return [
70
+ { id: 'secrets', label: 'Secrets Store' },
71
+ { id: 'google', label: 'Google OAuth' },
72
+ { id: 'passkeys', label: 'Passkeys' },
73
+ { id: 'audit', label: 'Audit & Sessions' },
74
+ ];
18
75
  }
19
76
 
20
77
  connectedCallback() {
21
- this.attachShadow({ mode: 'open' });
78
+ if (!this.shadowRoot) this.attachShadow({ mode: 'open' });
22
79
  this.render();
23
- this.refreshStatus();
80
+ this.onScreen = (event) => {
81
+ const detail = event.detail || {};
82
+ if (detail.id && this.screens.some((s) => s.id === detail.id)) {
83
+ this.screen = detail.id;
84
+ this.paint();
85
+ }
86
+ };
87
+ window.addEventListener('tr:screen-change', this.onScreen);
88
+ this.readAll();
89
+ }
90
+
91
+ disconnectedCallback() {
92
+ window.removeEventListener('tr:screen-change', this.onScreen);
24
93
  }
25
94
 
26
- async refreshStatus() {
95
+ async readAll() {
96
+ if (this.busy) return;
97
+ this.busy = true;
98
+ this.lastAttempt = new Date().toISOString();
99
+ this.lastError = null;
100
+ this.paint();
101
+
27
102
  try {
28
- const res = await this.host?.run('status', ['--json']);
29
- let data = res;
30
- if (typeof res === 'string') {
31
- try { data = JSON.parse(res); } catch {}
32
- } else if (res && typeof res.output === 'string') {
33
- try { data = JSON.parse(res.output); } catch {}
34
- }
103
+ if (this.host && typeof this.host.run === 'function') {
104
+ const [statusRes, passkeysRes, googleRes, secretsRes, auditRes] = await Promise.allSettled([
105
+ this.host.run('status', ['--json']),
106
+ this.host.run('passkey', ['list', '--json']),
107
+ this.host.run('google', ['accounts', '--json']),
108
+ this.host.run('secret', ['list', '--json']),
109
+ this.host.run('audit', ['--json']),
110
+ ]);
111
+
112
+ if (statusRes.status === 'fulfilled') {
113
+ this.statusData = asObject(statusRes.value);
114
+ } else {
115
+ this.statusData = null;
116
+ }
117
+
118
+ if (passkeysRes.status === 'fulfilled') {
119
+ this.passkeysData = asObject(passkeysRes.value);
120
+ } else {
121
+ this.passkeysData = null;
122
+ }
123
+
124
+ if (googleRes.status === 'fulfilled') {
125
+ this.googleData = asObject(googleRes.value);
126
+ } else {
127
+ this.googleData = null;
128
+ }
129
+
130
+ if (secretsRes.status === 'fulfilled') {
131
+ this.secretsData = asObject(secretsRes.value);
132
+ } else {
133
+ this.secretsData = null;
134
+ }
35
135
 
36
- if (data && typeof data === 'object') {
37
- const passkeyCount = data.passkey?.registered_count ?? (Array.isArray(data.passkey?.credentials) ? data.passkey.credentials.length : 0);
38
- const oauthConfigured = !!data.google_oauth2?.configured;
39
- const systemStatus = data.system?.status || 'Active';
40
-
41
- this.telemetry = {
42
- passkeys: `${passkeyCount} Keys`,
43
- oauth: oauthConfigured ? 'Connected' : 'Unconfigured',
44
- status: systemStatus === 'local-configuration-read' ? 'Ready' : systemStatus
45
- };
46
- this.updateTelemetry();
136
+ if (auditRes.status === 'fulfilled') {
137
+ this.auditData = asObject(auditRes.value);
138
+ } else {
139
+ this.auditData = null;
140
+ }
47
141
  }
48
- } catch {
49
- this.telemetry = {
50
- passkeys: 'Local',
51
- oauth: 'OAuth Ready',
52
- status: 'Active'
53
- };
54
- this.updateTelemetry();
142
+ } catch (err) {
143
+ this.lastError = err && err.message ? err.message : String(err);
144
+ this.statusData = null;
145
+ this.passkeysData = null;
146
+ this.googleData = null;
147
+ this.secretsData = null;
148
+ this.auditData = null;
149
+ } finally {
150
+ this.busy = false;
151
+ this.paint();
55
152
  }
56
153
  }
57
154
 
58
- updateTelemetry() {
59
- const root = this.shadowRoot;
60
- if (!root) return;
61
- const passkeyEl = root.querySelector('#stat-passkeys');
62
- const oauthEl = root.querySelector('#stat-oauth');
63
- const statusEl = root.querySelector('#stat-enclave');
64
- if (passkeyEl) passkeyEl.textContent = this.telemetry.passkeys;
65
- if (oauthEl) oauthEl.textContent = this.telemetry.oauth;
66
- if (statusEl) statusEl.textContent = this.telemetry.status;
155
+ async refreshGoogleAccount(accountId) {
156
+ if (!this.host || typeof this.host.run !== 'function' || this.busy) return;
157
+ this.busy = true;
158
+ this.paint();
159
+ try {
160
+ await this.host.run('google', ['refresh', accountId, '--json']);
161
+ await this.readAll();
162
+ } catch (err) {
163
+ this.lastError = err && err.message ? err.message : String(err);
164
+ this.busy = false;
165
+ this.paint();
166
+ }
67
167
  }
68
168
 
69
- async execute(action, args = []) {
70
- const out = this.shadowRoot?.querySelector('#card-out');
71
- const actionLabel = action.toUpperCase();
72
- if (out) out.textContent = `[ENCLAVE AUTH] Running ${actionLabel}...`;
73
- this.executing = true;
74
-
169
+ async revokePasskey(credentialId) {
170
+ if (!this.host || typeof this.host.run !== 'function' || this.busy) return;
171
+ this.busy = true;
172
+ this.paint();
75
173
  try {
76
- const res = await this.host?.run(action, args);
77
- let text = '';
78
- if (typeof res === 'object') {
79
- text = res.output || JSON.stringify(res, null, 2);
80
- } else {
81
- text = String(res || 'Operation completed.');
82
- }
83
- if (out) out.textContent = text.trim();
84
- if (['rotate', 'passkey', 'google', 'key'].includes(action)) {
85
- this.refreshStatus();
86
- }
174
+ await this.host.run('passkey', ['delete', credentialId, '--json']);
175
+ this.confirmAction = null;
176
+ await this.readAll();
87
177
  } catch (err) {
88
- if (out) out.textContent = `[ENCLAVE ERROR] ${err.message}`;
89
- } finally {
90
- this.executing = false;
178
+ this.lastError = err && err.message ? err.message : String(err);
179
+ this.busy = false;
180
+ this.paint();
181
+ }
182
+ }
183
+
184
+ computeVerdict() {
185
+ if (this.lastError) {
186
+ return { tone: 'danger', label: 'Read Error', detail: this.lastError };
187
+ }
188
+ if (!this.statusData && !this.secretsData) {
189
+ return { tone: 'muted', label: 'Unchecked', detail: 'Reading telemetry' };
190
+ }
191
+
192
+ const overdueCount = (this.secretsData?.keys || []).filter((k) => k.rotation_overdue).length;
193
+ const auditPassed = this.auditData?.passed ?? true;
194
+ const findingsCount = this.auditData?.findings_count ?? 0;
195
+
196
+ if (!auditPassed || findingsCount > 0) {
197
+ return {
198
+ tone: 'danger',
199
+ label: 'Policy Flagged',
200
+ detail: `${findingsCount} security baseline findings detected`,
201
+ };
202
+ }
203
+ if (overdueCount > 0) {
204
+ return {
205
+ tone: 'warn',
206
+ label: 'Rotation Due',
207
+ detail: `${overdueCount} secret keys overdue for 90-day rotation`,
208
+ };
91
209
  }
210
+ return {
211
+ tone: 'ok',
212
+ label: 'Secure',
213
+ detail: 'Hardware passkeys active, zero findings, secrets sealed',
214
+ };
92
215
  }
93
216
 
94
217
  render() {
@@ -99,217 +222,958 @@ export class AuthCard extends HTMLElement {
99
222
  <style>
100
223
  :host {
101
224
  display: block;
102
- font-family: 'Space Grotesk', -apple-system, sans-serif;
103
- background: #021a12;
104
- color: #ecfdf5;
105
- border-radius: 12px;
106
- border: 1px solid #064e3b;
107
- padding: 14px;
225
+ font-family: var(--font-body, 'Outfit', system-ui, sans-serif);
226
+ color: var(--text-primary, var(--face-text-default, #f1f5f9));
227
+ background: var(--bg-primary, var(--face-bg-default, #090d16));
228
+ border: 1px solid var(--border, var(--face-border-default, #1e293b));
229
+ border-radius: var(--border-radius, 8px);
108
230
  box-sizing: border-box;
109
- user-select: none;
231
+ padding: 16px;
232
+ min-width: 320px;
233
+ max-width: 100%;
234
+ line-height: 1.4;
235
+
236
+ /* Palette Tokens */
237
+ --face-text-default: #f1f5f9;
238
+ --face-bg-default: #090d16;
239
+ --face-border-default: #1e293b;
240
+ --face-primary: #10b981;
241
+ --face-primary-bg: #064e3b;
242
+ --face-primary-text: #041f17;
243
+ --face-accent: #34d399;
244
+ --face-ok: #059669;
245
+ --face-ok-bg: rgba(5, 150, 105, 0.15);
246
+ --face-warn: #d97706;
247
+ --face-warn-bg: rgba(217, 119, 6, 0.15);
248
+ --face-danger: #dc2626;
249
+ --face-danger-bg: rgba(220, 38, 38, 0.15);
250
+ --face-muted: var(--text-secondary, #94a3b8);
251
+ --face-surface: var(--bg-secondary, #0f172a);
252
+ --face-surface-subtle: rgba(255, 255, 255, 0.03);
253
+ --face-border: var(--border, #1e293b);
254
+ --face-border-active: #334155;
255
+ --face-font-mono: var(--font-mono, 'JetBrains Mono', ui-monospace, monospace);
110
256
  }
111
257
 
112
- * { box-sizing: border-box; }
258
+ @media (prefers-reduced-motion: reduce) {
259
+ * {
260
+ animation-duration: 0.01ms !important;
261
+ transition-duration: 0.01ms !important;
262
+ }
263
+ }
113
264
 
114
- /* Telemetry Metric Cards */
115
- .telemetry-row {
116
- display: grid;
117
- grid-template-columns: repeat(3, 1fr);
118
- gap: 8px;
119
- margin-bottom: 12px;
265
+ .header {
266
+ display: flex;
267
+ align-items: center;
268
+ justify-content: space-between;
269
+ flex-wrap: wrap;
270
+ gap: 12px;
271
+ padding-bottom: 14px;
272
+ border-bottom: 1px solid var(--face-border);
120
273
  }
121
274
 
122
- .metric-card {
123
- background: rgba(4, 47, 34, 0.7);
124
- border: 1px solid rgba(16, 185, 129, 0.25);
125
- border-radius: 8px;
126
- padding: 8px 10px;
275
+ .brand-group {
127
276
  display: flex;
128
- flex-direction: column;
129
- gap: 2px;
277
+ align-items: center;
278
+ gap: 10px;
130
279
  }
131
280
 
132
- .metric-label {
133
- font-family: 'JetBrains Mono', monospace;
134
- font-size: 9px;
281
+ .wordmark {
282
+ font-size: 15px;
135
283
  font-weight: 700;
136
- color: #6ee7b7;
137
284
  letter-spacing: 0.05em;
138
285
  text-transform: uppercase;
286
+ color: var(--text-primary, var(--face-text-default));
139
287
  }
140
288
 
141
- .metric-value {
289
+ .badge {
290
+ font-family: var(--face-font-mono);
291
+ font-size: 11px;
292
+ font-weight: 600;
293
+ padding: 2px 8px;
294
+ border-radius: 4px;
295
+ background: var(--face-primary-bg);
296
+ color: var(--face-accent);
297
+ border: 1px solid rgba(52, 211, 153, 0.3);
298
+ text-transform: uppercase;
299
+ }
300
+
301
+ .verdict-line {
302
+ display: flex;
303
+ align-items: center;
304
+ gap: 8px;
305
+ padding: 4px 10px;
306
+ border-radius: 6px;
142
307
  font-size: 13px;
143
- font-weight: 700;
144
- color: #ffffff;
145
- font-family: 'JetBrains Mono', monospace;
146
- overflow: hidden;
147
- text-overflow: ellipsis;
148
- white-space: nowrap;
308
+ font-weight: 600;
149
309
  }
150
310
 
151
- /* Action Buttons Grid (>=44px touch targets) */
152
- .actions-grid {
153
- display: grid;
154
- grid-template-columns: repeat(3, 1fr);
311
+ .verdict-line[data-tone="ok"] {
312
+ background: var(--face-ok-bg);
313
+ color: var(--face-accent);
314
+ border: 1px solid rgba(5, 150, 105, 0.3);
315
+ }
316
+
317
+ .verdict-line[data-tone="warn"] {
318
+ background: var(--face-warn-bg);
319
+ color: var(--face-warn);
320
+ border: 1px solid rgba(217, 119, 6, 0.3);
321
+ }
322
+
323
+ .verdict-line[data-tone="danger"] {
324
+ background: var(--face-danger-bg);
325
+ color: var(--face-danger);
326
+ border: 1px solid rgba(220, 38, 38, 0.3);
327
+ }
328
+
329
+ .verdict-line[data-tone="muted"] {
330
+ background: var(--face-surface);
331
+ color: var(--face-muted);
332
+ border: 1px solid var(--face-border);
333
+ }
334
+
335
+ .verdict-dot {
336
+ width: 8px;
337
+ height: 8px;
338
+ border-radius: 50%;
339
+ background: currentColor;
340
+ }
341
+
342
+ .tabs-nav {
343
+ display: flex;
344
+ align-items: center;
155
345
  gap: 6px;
156
- margin-bottom: 10px;
346
+ margin-top: 14px;
347
+ margin-bottom: 16px;
348
+ border-bottom: 1px solid var(--face-border);
349
+ padding-bottom: 10px;
350
+ overflow-x: auto;
157
351
  }
158
352
 
159
- .btn-action {
353
+ .tab-btn {
160
354
  min-height: 44px;
161
- min-width: 44px;
162
- background: rgba(255, 255, 255, 0.04);
163
- border: 1px solid rgba(16, 185, 129, 0.25);
164
- border-radius: 8px;
165
- color: #ecfdf5;
166
- font-family: 'Space Grotesk', sans-serif;
167
- font-size: 11px;
168
- font-weight: 700;
169
- letter-spacing: 0.02em;
355
+ padding: 8px 14px;
356
+ border-radius: 6px;
357
+ border: 1px solid transparent;
358
+ background: transparent;
359
+ color: var(--face-muted);
360
+ font-family: inherit;
361
+ font-size: 13px;
362
+ font-weight: 600;
170
363
  cursor: pointer;
171
- touch-action: manipulation;
172
- display: flex;
364
+ white-space: nowrap;
365
+ display: inline-flex;
173
366
  align-items: center;
174
- justify-content: center;
175
- text-align: center;
176
- padding: 6px;
177
- transition: all 0.15s ease;
367
+ gap: 6px;
368
+ }
369
+
370
+ .tab-btn:hover {
371
+ color: var(--text-primary, var(--face-text-default));
372
+ background: var(--face-surface-subtle);
373
+ }
374
+
375
+ .tab-btn.active {
376
+ color: var(--face-accent);
377
+ background: var(--face-surface);
378
+ border-color: var(--face-border-active);
379
+ }
380
+
381
+ .refresh-btn {
382
+ min-height: 44px;
383
+ padding: 8px 14px;
384
+ border-radius: 6px;
385
+ border: 1px solid var(--face-border);
386
+ background: var(--face-surface);
387
+ color: var(--text-primary, var(--face-text-default));
388
+ font-family: inherit;
389
+ font-size: 13px;
390
+ font-weight: 500;
391
+ cursor: pointer;
392
+ display: inline-flex;
393
+ align-items: center;
394
+ gap: 6px;
395
+ margin-left: auto;
396
+ }
397
+
398
+ .refresh-btn:hover {
399
+ background: var(--face-border-active);
400
+ }
401
+
402
+ .screen-container {
403
+ min-height: 220px;
404
+ }
405
+
406
+ .metrics-strip {
407
+ display: grid;
408
+ grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
409
+ gap: 10px;
410
+ margin-bottom: 16px;
178
411
  }
179
412
 
180
- .btn-action:hover {
181
- background: #047857;
182
- border-color: #10b981;
183
- color: #ffffff;
413
+ .metric-card {
414
+ background: var(--face-surface);
415
+ border: 1px solid var(--face-border);
416
+ border-radius: 6px;
417
+ padding: 10px 12px;
184
418
  }
185
419
 
186
- .btn-action:active {
187
- transform: scale(0.98);
420
+ .metric-label {
421
+ font-size: 11px;
422
+ color: var(--face-muted);
423
+ text-transform: uppercase;
424
+ letter-spacing: 0.04em;
425
+ margin-bottom: 4px;
188
426
  }
189
427
 
190
- .btn-action.btn-primary {
191
- background: rgba(16, 185, 129, 0.18);
192
- border-color: #10b981;
193
- color: #34d399;
428
+ .metric-value {
429
+ font-family: var(--face-font-mono);
430
+ font-size: 18px;
431
+ font-weight: 700;
432
+ color: var(--text-primary, var(--face-text-default));
194
433
  }
195
434
 
196
- .btn-action.btn-primary:hover {
197
- background: #10b981;
198
- color: #021a12;
435
+ .metric-sub {
436
+ font-size: 11px;
437
+ color: var(--face-muted);
438
+ margin-top: 2px;
199
439
  }
200
440
 
201
- /* Terminal Deck */
202
- .terminal-container {
203
- position: relative;
204
- background: #01110b;
205
- border: 1px solid rgba(16, 185, 129, 0.2);
206
- border-radius: 8px;
207
- padding: 8px 10px;
441
+ .item-list {
442
+ display: flex;
443
+ flex-direction: column;
444
+ gap: 8px;
208
445
  }
209
446
 
210
- .terminal-header {
447
+ .item-row {
448
+ background: var(--face-surface);
449
+ border: 1px solid var(--face-border);
450
+ border-radius: 6px;
451
+ padding: 12px;
211
452
  display: flex;
453
+ align-items: center;
212
454
  justify-content: space-between;
455
+ flex-wrap: wrap;
456
+ gap: 10px;
457
+ }
458
+
459
+ .item-main {
460
+ display: flex;
461
+ flex-direction: column;
462
+ gap: 3px;
463
+ }
464
+
465
+ .item-title {
466
+ font-size: 14px;
467
+ font-weight: 600;
468
+ color: var(--text-primary, var(--face-text-default));
469
+ display: flex;
213
470
  align-items: center;
214
- margin-bottom: 4px;
215
- padding-bottom: 4px;
216
- border-bottom: 1px solid rgba(16, 185, 129, 0.1);
471
+ gap: 8px;
472
+ }
473
+
474
+ .item-meta {
475
+ font-family: var(--face-font-mono);
476
+ font-size: 12px;
477
+ color: var(--face-muted);
478
+ }
479
+
480
+ .action-btn {
481
+ min-height: 44px;
482
+ padding: 8px 14px;
483
+ border-radius: 6px;
484
+ font-family: inherit;
485
+ font-size: 13px;
486
+ font-weight: 600;
487
+ cursor: pointer;
488
+ display: inline-flex;
489
+ align-items: center;
490
+ justify-content: center;
491
+ gap: 6px;
492
+ border: 1px solid transparent;
493
+ }
494
+
495
+ .action-btn.primary {
496
+ background: var(--face-primary);
497
+ color: var(--face-primary-text);
498
+ }
499
+
500
+ .action-btn.primary:hover {
501
+ background: var(--face-accent);
502
+ }
503
+
504
+ .action-btn.secondary {
505
+ background: var(--face-surface-subtle);
506
+ border-color: var(--face-border);
507
+ color: var(--text-primary, var(--face-text-default));
508
+ }
509
+
510
+ .action-btn.secondary:hover {
511
+ background: var(--face-border-active);
512
+ }
513
+
514
+ .action-btn.danger {
515
+ background: var(--face-danger-bg);
516
+ border-color: rgba(220, 38, 38, 0.4);
517
+ color: var(--face-danger);
518
+ }
519
+
520
+ .action-btn.danger:hover {
521
+ background: rgba(220, 38, 38, 0.25);
522
+ }
523
+
524
+ .empty-state {
525
+ text-align: center;
526
+ padding: 32px 16px;
527
+ border: 1px dashed var(--face-border);
528
+ border-radius: 6px;
529
+ color: var(--face-muted);
530
+ }
531
+
532
+ .empty-title {
533
+ font-size: 14px;
534
+ font-weight: 600;
535
+ color: var(--text-primary, var(--face-text-default));
536
+ margin-bottom: 6px;
537
+ }
538
+
539
+ .empty-text {
540
+ font-size: 13px;
541
+ margin-bottom: 14px;
542
+ max-width: 440px;
543
+ margin-left: auto;
544
+ margin-right: auto;
217
545
  }
218
546
 
219
- .terminal-label {
220
- font-family: 'JetBrains Mono', monospace;
221
- font-size: 9px;
222
- color: #6ee7b7;
547
+ .table-responsive {
548
+ width: 100%;
549
+ overflow-x: auto;
550
+ border: 1px solid var(--face-border);
551
+ border-radius: 6px;
552
+ }
553
+
554
+ table {
555
+ width: 100%;
556
+ border-collapse: collapse;
557
+ font-size: 13px;
558
+ text-align: left;
559
+ }
560
+
561
+ th {
562
+ background: var(--face-surface);
563
+ color: var(--face-muted);
564
+ font-weight: 600;
565
+ padding: 10px 12px;
566
+ border-bottom: 1px solid var(--face-border);
223
567
  text-transform: uppercase;
568
+ font-size: 11px;
224
569
  letter-spacing: 0.05em;
225
570
  }
226
571
 
227
- .terminal-copy {
228
- background: none;
229
- border: none;
230
- color: #34d399;
231
- font-family: 'JetBrains Mono', monospace;
232
- font-size: 10px;
233
- cursor: pointer;
234
- padding: 2px 4px;
235
- border-radius: 4px;
572
+ td {
573
+ padding: 10px 12px;
574
+ border-bottom: 1px solid var(--face-border);
575
+ color: var(--text-primary, var(--face-text-default));
576
+ }
577
+
578
+ tr:last-child td {
579
+ border-bottom: none;
236
580
  }
237
581
 
238
- .terminal-copy:hover {
239
- background: rgba(16, 185, 129, 0.2);
582
+ .code-cell {
583
+ font-family: var(--face-font-mono);
584
+ font-size: 12px;
240
585
  }
241
586
 
242
- .terminal-readout {
243
- font-family: 'JetBrains Mono', monospace;
587
+ .status-pill {
588
+ display: inline-flex;
589
+ align-items: center;
590
+ gap: 6px;
591
+ padding: 2px 8px;
592
+ border-radius: 4px;
244
593
  font-size: 11px;
245
- line-height: 1.4;
246
- color: #34d399;
247
- min-height: 48px;
248
- max-height: 120px;
249
- overflow-y: auto;
250
- white-space: pre-wrap;
251
- word-break: break-all;
594
+ font-weight: 600;
595
+ font-family: var(--face-font-mono);
596
+ }
597
+
598
+ .status-pill.ok {
599
+ background: var(--face-ok-bg);
600
+ color: var(--face-accent);
601
+ }
602
+
603
+ .status-pill.warn {
604
+ background: var(--face-warn-bg);
605
+ color: var(--face-warn);
606
+ }
607
+
608
+ .status-pill.danger {
609
+ background: var(--face-danger-bg);
610
+ color: var(--face-danger);
611
+ }
612
+
613
+ .status-pill.muted {
614
+ background: var(--face-surface-subtle);
615
+ color: var(--face-muted);
616
+ }
617
+
618
+ .filter-strip {
619
+ display: flex;
620
+ align-items: center;
621
+ gap: 8px;
622
+ margin-bottom: 12px;
623
+ flex-wrap: wrap;
624
+ }
625
+
626
+ .filter-btn {
627
+ min-height: 32px;
628
+ padding: 4px 10px;
629
+ border-radius: 4px;
630
+ border: 1px solid var(--face-border);
631
+ background: var(--face-surface);
632
+ color: var(--face-muted);
633
+ font-size: 12px;
634
+ font-weight: 500;
635
+ cursor: pointer;
636
+ }
637
+
638
+ .filter-btn.active {
639
+ background: var(--face-primary-bg);
640
+ color: var(--face-accent);
641
+ border-color: rgba(52, 211, 153, 0.4);
642
+ }
643
+
644
+ .confirmation-box {
645
+ background: var(--face-danger-bg);
646
+ border: 1px solid var(--face-danger);
647
+ border-radius: 6px;
648
+ padding: 14px;
649
+ margin-bottom: 16px;
650
+ display: flex;
651
+ align-items: center;
652
+ justify-content: space-between;
653
+ flex-wrap: wrap;
654
+ gap: 12px;
655
+ }
656
+
657
+ .footer-note {
658
+ margin-top: 16px;
659
+ padding-top: 12px;
660
+ border-top: 1px solid var(--face-border);
661
+ display: flex;
662
+ align-items: center;
663
+ justify-content: space-between;
664
+ flex-wrap: wrap;
665
+ gap: 8px;
666
+ font-size: 12px;
667
+ color: var(--face-muted);
252
668
  }
253
669
  </style>
254
670
 
255
- <!-- Live Telemetry Deck -->
256
- <div class="telemetry-row">
671
+ <div class="header">
672
+ <div class="brand-group">
673
+ <div class="wordmark">AUTH & SECRETS</div>
674
+ </div>
675
+ <div id="verdict" class="verdict-line" data-tone="muted">
676
+ <div class="verdict-dot"></div>
677
+ <span id="verdict-label">Checking...</span>
678
+ </div>
679
+ </div>
680
+
681
+ <div class="tabs-nav">
682
+ <button class="tab-btn" data-screen="secrets">Secrets</button>
683
+ <button class="tab-btn" data-screen="passkeys">Passkeys</button>
684
+ <button class="tab-btn" data-screen="google">Google OAuth</button>
685
+ <button class="tab-btn" data-screen="audit">Audit & Sessions</button>
686
+ <button class="refresh-btn" id="refresh-action">Refresh</button>
687
+ </div>
688
+
689
+ <div id="confirmation-zone"></div>
690
+ <div id="screen-zone" class="screen-container"></div>
691
+
692
+ <div class="footer-note">
693
+ <span id="last-attempt-note">Attempted: ${DASH}</span>
694
+ <span>Isolated Auth Secrets Store Follows TR_SECRETS_BRAIN</span>
695
+ </div>
696
+ `;
697
+
698
+ this.bindEvents();
699
+ this.paint();
700
+ }
701
+
702
+ bindEvents() {
703
+ const root = this.shadowRoot;
704
+ if (!root) return;
705
+
706
+ root.querySelectorAll('.tab-btn').forEach((btn) => {
707
+ btn.addEventListener('click', (e) => {
708
+ const target = e.currentTarget;
709
+ const targetScreen = target?.getAttribute('data-screen');
710
+ if (targetScreen) {
711
+ this.screen = targetScreen;
712
+ root.querySelectorAll('.tab-btn').forEach((b) => b.classList.remove('active'));
713
+ target.classList.add('active');
714
+ this.paint();
715
+ }
716
+ });
717
+ });
718
+
719
+ const refreshBtn = root.getElementById('refresh-action');
720
+ if (refreshBtn) {
721
+ refreshBtn.addEventListener('click', () => this.readAll());
722
+ }
723
+ }
724
+
725
+ paint() {
726
+ const root = this.shadowRoot;
727
+ if (!root) return;
728
+
729
+ // Update active tab
730
+ root.querySelectorAll('.tab-btn').forEach((btn) => {
731
+ btn.classList.toggle('active', btn.getAttribute('data-screen') === this.screen);
732
+ });
733
+
734
+ // Update verdict
735
+ const verdict = this.computeVerdict();
736
+ const verdictEl = root.getElementById('verdict');
737
+ const verdictLabel = root.getElementById('verdict-label');
738
+ if (verdictEl && verdictLabel) {
739
+ verdictEl.setAttribute('data-tone', verdict.tone);
740
+ verdictLabel.textContent = `${verdict.label} — ${verdict.detail}`;
741
+ }
742
+
743
+ // Update last attempt
744
+ const attemptEl = root.getElementById('last-attempt-note');
745
+ if (attemptEl) {
746
+ attemptEl.textContent = this.lastAttempt
747
+ ? `Last checked: ${new Date(this.lastAttempt).toLocaleTimeString()}`
748
+ : `Attempted: ${DASH}`;
749
+ }
750
+
751
+ // Confirmation zone
752
+ const confirmZone = root.getElementById('confirmation-zone');
753
+ if (confirmZone) {
754
+ if (this.confirmAction) {
755
+ confirmZone.innerHTML = `
756
+ <div class="confirmation-box">
757
+ <div>
758
+ <strong>Confirm Revocation:</strong> Are you sure you want to revoke passkey <code>${escapeHtml(
759
+ this.confirmAction.name || this.confirmAction.id
760
+ )}</code>? This hardware authenticator will no longer grant access.
761
+ </div>
762
+ <div style="display: flex; gap: 8px;">
763
+ <button class="action-btn danger" id="btn-confirm-delete">Revoke Passkey</button>
764
+ <button class="action-btn secondary" id="btn-cancel-delete">Cancel</button>
765
+ </div>
766
+ </div>
767
+ `;
768
+ confirmZone.querySelector('#btn-confirm-delete')?.addEventListener('click', () => {
769
+ this.revokePasskey(this.confirmAction.id);
770
+ });
771
+ confirmZone.querySelector('#btn-cancel-delete')?.addEventListener('click', () => {
772
+ this.confirmAction = null;
773
+ this.paint();
774
+ });
775
+ } else {
776
+ confirmZone.innerHTML = '';
777
+ }
778
+ }
779
+
780
+ // Render active screen
781
+ const screenZone = root.getElementById('screen-zone');
782
+ if (!screenZone) return;
783
+
784
+ switch (this.screen) {
785
+ case 'passkeys':
786
+ this.paintPasskeys(screenZone);
787
+ break;
788
+ case 'google':
789
+ this.paintGoogle(screenZone);
790
+ break;
791
+ case 'secrets':
792
+ this.paintSecrets(screenZone);
793
+ break;
794
+ case 'audit':
795
+ this.paintAudit(screenZone);
796
+ break;
797
+ default:
798
+ this.paintPasskeys(screenZone);
799
+ }
800
+ }
801
+
802
+ paintPasskeys(container) {
803
+ const passkeys = this.passkeysData?.passkeys || [];
804
+ const count = this.passkeysData?.count ?? passkeys.length;
805
+ const isEnabled = this.statusData?.passkey?.enabled ?? true;
806
+
807
+ container.innerHTML = `
808
+ <div class="metrics-strip">
257
809
  <div class="metric-card">
258
- <span class="metric-label">Passkeys</span>
259
- <span class="metric-value" id="stat-passkeys">${this.telemetry.passkeys}</span>
810
+ <div class="metric-label">Registered Passkeys</div>
811
+ <div class="metric-value">${count}</div>
812
+ <div class="metric-sub">${count === 1 ? '1 hardware key' : `${count} keys active`}</div>
260
813
  </div>
261
814
  <div class="metric-card">
262
- <span class="metric-label">OAuth</span>
263
- <span class="metric-value" id="stat-oauth">${this.telemetry.oauth}</span>
815
+ <div class="metric-label">Biometric FIDO2</div>
816
+ <div class="metric-value" style="color: ${isEnabled ? 'var(--face-accent)' : 'var(--face-muted)'};">
817
+ ${isEnabled ? 'Active' : 'Disabled'}
818
+ </div>
819
+ <div class="metric-sub">WebAuthn Relying Party</div>
264
820
  </div>
265
821
  <div class="metric-card">
266
- <span class="metric-label">Enclave</span>
267
- <span class="metric-value" id="stat-enclave">${this.telemetry.status}</span>
822
+ <div class="metric-label">Local Authenticator</div>
823
+ <div class="metric-value">Platform</div>
824
+ <div class="metric-sub">Touch ID / Platform Key</div>
268
825
  </div>
269
826
  </div>
270
827
 
271
- <!-- Touch-First Actions (>=44px touch targets) -->
272
- <div class="actions-grid">
273
- <button class="btn-action btn-primary" data-cmd="audit">AUDIT</button>
274
- <button class="btn-action" data-cmd="passkey">PASSKEYS</button>
275
- <button class="btn-action" data-cmd="session">SESSIONS</button>
276
- <button class="btn-action" data-cmd="google">GOOGLE</button>
277
- <button class="btn-action" data-cmd="key">API KEYS</button>
278
- <button class="btn-action" data-cmd="rotate">ROTATE</button>
828
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
829
+ <h4 style="margin: 0; font-size: 14px;">Registered Hardware Authenticators</h4>
830
+ <div style="font-size: 12px; color: var(--face-muted);">
831
+ Protected by Biometric Security Policy
832
+ </div>
833
+ </div>
834
+
835
+ ${
836
+ passkeys.length === 0
837
+ ? `
838
+ <div class="empty-state">
839
+ <div class="empty-title">Zero Passkeys Registered</div>
840
+ <div class="empty-text">
841
+ No hardware biometric passkeys are registered yet. Register your device Touch ID, Face ID, or YubiKey hardware key for cryptographic sign-in.
842
+ </div>
843
+ <div style="font-size: 12px; font-family: var(--face-font-mono); color: var(--face-muted); background: var(--face-surface); padding: 8px 12px; border-radius: 4px; display: inline-block;">
844
+ total-recall auth passkey register
845
+ </div>
846
+ </div>
847
+ `
848
+ : `
849
+ <div class="item-list">
850
+ ${passkeys
851
+ .map(
852
+ (pk) => `
853
+ <div class="item-row">
854
+ <div class="item-main">
855
+ <div class="item-title">
856
+ <span>${escapeHtml(pk.name || 'Hardware Authenticator')}</span>
857
+ <span class="status-pill ok">Active</span>
858
+ </div>
859
+ <div class="item-meta">
860
+ ID: ${escapeHtml(pk.id || DASH)} · Added: ${
861
+ pk.created_at ? new Date(pk.created_at).toLocaleDateString() : DASH
862
+ } · Last Used: ${pk.last_used_at ? new Date(pk.last_used_at).toLocaleDateString() : 'Never'}
863
+ </div>
864
+ </div>
865
+ <div>
866
+ <button class="action-btn danger btn-revoke-passkey" data-id="${escapeHtml(
867
+ pk.id
868
+ )}" data-name="${escapeHtml(pk.name || pk.id)}">
869
+ Revoke
870
+ </button>
871
+ </div>
872
+ </div>
873
+ `
874
+ )
875
+ .join('')}
876
+ </div>
877
+ `
878
+ }
879
+ `;
880
+
881
+ container.querySelectorAll('.btn-revoke-passkey').forEach((btn) => {
882
+ btn.addEventListener('click', (e) => {
883
+ const id = e.currentTarget?.getAttribute('data-id');
884
+ const name = e.currentTarget?.getAttribute('data-name');
885
+ if (id) {
886
+ this.confirmAction = { type: 'passkey', id, name };
887
+ this.paint();
888
+ }
889
+ });
890
+ });
891
+ }
892
+
893
+ paintGoogle(container) {
894
+ const accounts = this.googleData?.accounts || [];
895
+ const isConfigured = this.statusData?.google_oauth2?.configured ?? (accounts.length > 0);
896
+ const clientId = this.statusData?.google_oauth2?.client_id || DASH;
897
+
898
+ container.innerHTML = `
899
+ <div class="metrics-strip">
900
+ <div class="metric-card">
901
+ <div class="metric-label">Google Accounts</div>
902
+ <div class="metric-value">${accounts.length}</div>
903
+ <div class="metric-sub">${accounts.length === 1 ? '1 active connection' : `${accounts.length} connections`}</div>
904
+ </div>
905
+ <div class="metric-card">
906
+ <div class="metric-label">OAuth2 Service</div>
907
+ <div class="metric-value" style="color: ${isConfigured ? 'var(--face-accent)' : 'var(--face-warn)'};">
908
+ ${isConfigured ? 'Configured' : 'Missing Credentials'}
909
+ </div>
910
+ <div class="metric-sub">Client ID: ${escapeHtml(clientId.slice(0, 16))}…</div>
911
+ </div>
912
+ <div class="metric-card">
913
+ <div class="metric-label">Session Scopes</div>
914
+ <div class="metric-value">OpenID + API</div>
915
+ <div class="metric-sub">Verified OAuth Callback</div>
916
+ </div>
279
917
  </div>
280
918
 
281
- <!-- Live Output Console -->
282
- <div class="terminal-container">
283
- <div class="terminal-header">
284
- <span class="terminal-label">Enclave Console</span>
285
- <button class="terminal-copy" id="btn-copy" title="Copy console output">Copy</button>
919
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
920
+ <h4 style="margin: 0; font-size: 14px;">Connected Google Cloud Accounts</h4>
921
+ <div style="font-size: 12px; color: var(--face-muted);">
922
+ Encrypted Token Rotation
286
923
  </div>
287
- <div class="terminal-readout" id="card-out">Authenticating enclave... Ready for operations.</div>
288
924
  </div>
925
+
926
+ ${
927
+ accounts.length === 0
928
+ ? `
929
+ <div class="empty-state">
930
+ <div class="empty-title">Zero Google Accounts Connected</div>
931
+ <div class="empty-text">
932
+ No Google OAuth accounts are connected. Connect your Google account to grant API access and authenticate sessions.
933
+ </div>
934
+ <div style="font-size: 12px; font-family: var(--face-font-mono); color: var(--face-muted); background: var(--face-surface); padding: 8px 12px; border-radius: 4px; display: inline-block;">
935
+ total-recall auth google connect
936
+ </div>
937
+ </div>
938
+ `
939
+ : `
940
+ <div class="item-list">
941
+ ${accounts
942
+ .map((acc) => {
943
+ const isExpired = acc.expiry ? new Date(acc.expiry).getTime() < Date.now() : false;
944
+ const scopesCount = Array.isArray(acc.scopes) ? acc.scopes.length : 0;
945
+ return `
946
+ <div class="item-row">
947
+ <div class="item-main">
948
+ <div class="item-title">
949
+ <span>${escapeHtml(acc.email || acc.account_id)}</span>
950
+ <span class="status-pill ${isExpired ? 'warn' : 'ok'}">
951
+ ${isExpired ? 'Token Expired' : 'Connected'}
952
+ </span>
953
+ </div>
954
+ <div class="item-meta">
955
+ Scopes: ${scopesCount} granted · Expiry: ${
956
+ acc.expiry ? new Date(acc.expiry).toLocaleString() : DASH
957
+ } · Last Refresh: ${acc.updated_at ? new Date(acc.updated_at).toLocaleString() : DASH}
958
+ </div>
959
+ </div>
960
+ <div style="display: flex; gap: 8px;">
961
+ <button class="action-btn secondary btn-refresh-google" data-id="${escapeHtml(acc.account_id)}">
962
+ Refresh Token
963
+ </button>
964
+ </div>
965
+ </div>
966
+ `;
967
+ })
968
+ .join('')}
969
+ </div>
970
+ `
971
+ }
289
972
  `;
290
973
 
291
- const buttons = root.querySelectorAll('.btn-action');
292
- buttons.forEach(btn => {
293
- btn.addEventListener('click', () => {
294
- const cmd = btn.getAttribute('data-cmd');
295
- if (cmd) this.execute(cmd, []);
974
+ container.querySelectorAll('.btn-refresh-google').forEach((btn) => {
975
+ btn.addEventListener('click', (e) => {
976
+ const id = e.currentTarget?.getAttribute('data-id');
977
+ if (id) this.refreshGoogleAccount(id);
296
978
  });
297
979
  });
980
+ }
981
+
982
+ paintSecrets(container) {
983
+ const keys = this.secretsData?.keys || [];
984
+ const storePath = this.secretsData?.store || DASH;
985
+ const overdueCount = keys.filter((k) => k.rotation_overdue).length;
986
+ const sharedCount = keys.filter((k) => k.shared_value).length;
987
+
988
+ let filteredKeys = keys;
989
+ if (this.activeFilter === 'overdue') {
990
+ filteredKeys = keys.filter((k) => k.rotation_overdue);
991
+ } else if (this.activeFilter === 'ai') {
992
+ filteredKeys = keys.filter((k) =>
993
+ ['google', 'openai', 'anthropic', 'deepseek', 'openrouter'].includes(k.provider)
994
+ );
995
+ } else if (this.activeFilter === 'infra') {
996
+ filteredKeys = keys.filter((k) =>
997
+ ['vercel', 'digitalocean', 'stripe', 'mailcow', 'smtp2go', 'telnyx'].includes(k.provider)
998
+ );
999
+ }
1000
+
1001
+ container.innerHTML = `
1002
+ <div class="metrics-strip">
1003
+ <div class="metric-card">
1004
+ <div class="metric-label">Active Secrets</div>
1005
+ <div class="metric-value">${keys.length}</div>
1006
+ <div class="metric-sub">Single Secrets Store</div>
1007
+ </div>
1008
+ <div class="metric-card">
1009
+ <div class="metric-label">Overdue Rotations</div>
1010
+ <div class="metric-value" style="color: ${overdueCount > 0 ? 'var(--face-warn)' : 'var(--face-accent)'};">
1011
+ ${overdueCount}
1012
+ </div>
1013
+ <div class="metric-sub">${overdueCount === 0 ? 'All keys fresh' : 'Exceeds 90-day cycle'}</div>
1014
+ </div>
1015
+ <div class="metric-card">
1016
+ <div class="metric-label">Shared Bindings</div>
1017
+ <div class="metric-value">${sharedCount}</div>
1018
+ <div class="metric-sub">Multi-repo consumers</div>
1019
+ </div>
1020
+ </div>
1021
+
1022
+ <div class="filter-strip">
1023
+ <button class="filter-btn ${this.activeFilter === 'all' ? 'active' : ''}" data-filter="all">
1024
+ All Keys (${keys.length})
1025
+ </button>
1026
+ <button class="filter-btn ${this.activeFilter === 'overdue' ? 'active' : ''}" data-filter="overdue">
1027
+ Overdue (${overdueCount})
1028
+ </button>
1029
+ <button class="filter-btn ${this.activeFilter === 'ai' ? 'active' : ''}" data-filter="ai">
1030
+ AI & LLM
1031
+ </button>
1032
+ <button class="filter-btn ${this.activeFilter === 'infra' ? 'active' : ''}" data-filter="infra">
1033
+ Infrastructure
1034
+ </button>
1035
+ </div>
1036
+
1037
+ <div class="table-responsive">
1038
+ <table>
1039
+ <thead>
1040
+ <tr>
1041
+ <th>Key Identifier</th>
1042
+ <th>Provider</th>
1043
+ <th>Bound Repositories</th>
1044
+ <th>Fingerprint</th>
1045
+ <th>Rotation Status</th>
1046
+ </tr>
1047
+ </thead>
1048
+ <tbody>
1049
+ ${
1050
+ filteredKeys.length === 0
1051
+ ? `
1052
+ <tr>
1053
+ <td colspan="5" style="text-align: center; color: var(--face-muted); padding: 24px;">
1054
+ No secret keys match the active filter.
1055
+ </td>
1056
+ </tr>
1057
+ `
1058
+ : filteredKeys
1059
+ .slice(0, 50)
1060
+ .map(
1061
+ (k) => `
1062
+ <tr>
1063
+ <td class="code-cell">
1064
+ <strong>${escapeHtml(k.key)}</strong>
1065
+ </td>
1066
+ <td>${escapeHtml(k.provider_name || k.provider || DASH)}</td>
1067
+ <td class="code-cell" style="font-size: 11px;">
1068
+ ${escapeHtml(Array.isArray(k.repos) ? k.repos.join(', ') : k.repo || 'global')}
1069
+ </td>
1070
+ <td class="code-cell">${escapeHtml(k.fingerprint || DASH)}</td>
1071
+ <td>
1072
+ <span class="status-pill ${k.rotation_overdue ? 'warn' : 'ok'}">
1073
+ ${k.rotation_overdue ? 'Overdue' : 'Fresh'}
1074
+ </span>
1075
+ </td>
1076
+ </tr>
1077
+ `
1078
+ )
1079
+ .join('')
1080
+ }
1081
+ </tbody>
1082
+ </table>
1083
+ </div>
298
1084
 
299
- const copyBtn = root.querySelector('#btn-copy');
300
- if (copyBtn) {
301
- copyBtn.addEventListener('click', () => {
302
- const out = root.querySelector('#card-out');
303
- if (out && out.textContent) {
304
- navigator.clipboard?.writeText(out.textContent);
305
- copyBtn.textContent = 'Copied!';
306
- setTimeout(() => { copyBtn.textContent = 'Copy'; }, 1500);
1085
+ <div style="font-size: 11px; font-family: var(--face-font-mono); color: var(--face-muted); margin-top: 10px;">
1086
+ Store: ${escapeHtml(storePath)}
1087
+ </div>
1088
+ `;
1089
+
1090
+ container.querySelectorAll('.filter-btn').forEach((btn) => {
1091
+ btn.addEventListener('click', (e) => {
1092
+ const filter = e.currentTarget?.getAttribute('data-filter');
1093
+ if (filter) {
1094
+ this.activeFilter = filter;
1095
+ this.paintSecrets(container);
307
1096
  }
308
1097
  });
309
- }
1098
+ });
1099
+ }
1100
+
1101
+ paintAudit(container) {
1102
+ const findings = this.auditData?.findings || [];
1103
+ const passed = this.auditData?.passed ?? true;
1104
+ const findingsCount = this.auditData?.findings_count ?? findings.length;
1105
+ const auditTitle = this.auditData?.audit || 'Total Recall Auth Security Baseline';
1106
+
1107
+ container.innerHTML = `
1108
+ <div class="metrics-strip">
1109
+ <div class="metric-card">
1110
+ <div class="metric-label">Security Baseline</div>
1111
+ <div class="metric-value" style="color: ${passed ? 'var(--face-accent)' : 'var(--face-danger)'};">
1112
+ ${passed ? 'PASS' : 'FAIL'}
1113
+ </div>
1114
+ <div class="metric-sub">Deterministic Audit</div>
1115
+ </div>
1116
+ <div class="metric-card">
1117
+ <div class="metric-label">Findings Count</div>
1118
+ <div class="metric-value" style="color: ${findingsCount > 0 ? 'var(--face-danger)' : 'var(--face-accent)'};">
1119
+ ${findingsCount}
1120
+ </div>
1121
+ <div class="metric-sub">${findingsCount === 0 ? 'Zero vulnerabilities' : 'Issues detected'}</div>
1122
+ </div>
1123
+ <div class="metric-card">
1124
+ <div class="metric-label">Audit Engine</div>
1125
+ <div class="metric-value">Active</div>
1126
+ <div class="metric-sub">Hardware Token Verification</div>
1127
+ </div>
1128
+ </div>
1129
+
1130
+ <div style="margin-bottom: 12px;">
1131
+ <h4 style="margin: 0 0 4px 0; font-size: 14px;">${escapeHtml(auditTitle)}</h4>
1132
+ <div style="font-size: 12px; color: var(--face-muted);">
1133
+ Deterministic security posture verification across credentials, session tokens, and permissions.
1134
+ </div>
1135
+ </div>
1136
+
1137
+ ${
1138
+ findings.length === 0
1139
+ ? `
1140
+ <div class="empty-state">
1141
+ <div class="empty-title" style="color: var(--face-accent);">Zero Findings Detected</div>
1142
+ <div class="empty-text">
1143
+ All security baselines satisfied. Permissions, session signing keys, and credential stores are compliant with Total Recall security policies.
1144
+ </div>
1145
+ </div>
1146
+ `
1147
+ : `
1148
+ <div class="item-list">
1149
+ ${findings
1150
+ .map(
1151
+ (f) => `
1152
+ <div class="item-row">
1153
+ <div class="item-main">
1154
+ <div class="item-title">
1155
+ <span class="status-pill ${f.severity === 'critical' ? 'danger' : 'warn'}">
1156
+ ${escapeHtml(f.severity || 'Warning')}
1157
+ </span>
1158
+ <span>${escapeHtml(f.title || f.check || 'Policy Violation')}</span>
1159
+ </div>
1160
+ <div class="item-meta">
1161
+ ${escapeHtml(f.detail || f.message || DASH)}
1162
+ </div>
1163
+ </div>
1164
+ </div>
1165
+ `
1166
+ )
1167
+ .join('')}
1168
+ </div>
1169
+ `
1170
+ }
1171
+ `;
310
1172
  }
311
1173
  }
312
1174
 
313
1175
  if (!customElements.get('tr-auth-card')) {
314
1176
  customElements.define('tr-auth-card', AuthCard);
315
1177
  }
1178
+
1179
+ export { asObject, escapeHtml };