@lifeaitools/clauth 2.1.1 → 2.10.0

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 (98) hide show
  1. package/.clauth-skill/references/keys-guide.md +270 -270
  2. package/cli/api.js +238 -238
  3. package/cli/commands/install.js +413 -396
  4. package/cli/commands/login.test.js +49 -1
  5. package/cli/commands/ops-install.js +11 -10
  6. package/cli/commands/ops.test.js +270 -0
  7. package/cli/commands/scrub.test.js +70 -0
  8. package/cli/commands/serve.js +2613 -11219
  9. package/cli/commands/uninstall.js +164 -164
  10. package/cli/dashboard/dashboard.js +592 -0
  11. package/cli/dashboard/global.css +65 -0
  12. package/cli/dashboard/panel-element.js +79 -0
  13. package/cli/dashboard/panels/build-status-panel.css +12 -0
  14. package/cli/dashboard/panels/build-status-panel.js +98 -0
  15. package/cli/dashboard/panels/panel-registry.js +58 -0
  16. package/cli/dashboard/panels/services-panel.css +95 -0
  17. package/cli/dashboard/panels/services-panel.js +872 -0
  18. package/cli/dashboard/panels/surfaces-panel.css +59 -0
  19. package/cli/dashboard/panels/surfaces-panel.js +474 -0
  20. package/cli/dashboard/panels/toolbar-panel.css +42 -0
  21. package/cli/dashboard/panels/toolbar-panel.js +374 -0
  22. package/cli/dashboard/panels/tunnel-panel.css +61 -0
  23. package/cli/dashboard/panels/tunnel-panel.js +688 -0
  24. package/cli/dashboard/panels/webdav-panel.css +38 -0
  25. package/cli/dashboard/panels/webdav-panel.js +166 -0
  26. package/cli/fingerprint.test.js +95 -0
  27. package/cli/http/components/callagent-terminal-component.js +310 -0
  28. package/cli/http/components/dashboard-component.js +123 -0
  29. package/cli/http/components/inbox-component.js +41 -0
  30. package/cli/http/components/mcp-transport-component.js +275 -0
  31. package/cli/http/components/oauth-component.js +207 -0
  32. package/cli/http/components/service-crud-component.js +288 -0
  33. package/cli/http/components/supervisor-component.js +104 -0
  34. package/cli/http/components/system-component.js +184 -0
  35. package/cli/http/components/tunnel-component.js +508 -0
  36. package/cli/http/components/vault-auth-component.js +264 -0
  37. package/cli/http/components/watchdog-component.js +28 -0
  38. package/cli/http/request-utils.js +21 -0
  39. package/cli/http/servers/ops-server.js +335 -0
  40. package/cli/index.js +5 -91
  41. package/cli/lib/fs-git.test.js +467 -0
  42. package/cli/mcp/providers/call-agent-provider.js +30 -0
  43. package/cli/mcp/providers/clauth-core-provider.js +461 -0
  44. package/cli/mcp/providers/ops-provider.js +42 -0
  45. package/cli/mcp/providers/terminal-provider.js +81 -0
  46. package/cli/mcp/tool-registry.js +48 -0
  47. package/cli/mcp/tool-schemas.js +822 -0
  48. package/cli/mcp/tool-schemas.test.js +49 -0
  49. package/cli/ops/pm2-preflight.js +54 -0
  50. package/cli/recovery.js +6 -0
  51. package/cli/recovery.test.js +179 -0
  52. package/cli/services/agent-pool-instance.js +38 -0
  53. package/cli/services/claude-binary.js +29 -0
  54. package/cli/services/delegation-lane-instance.js +35 -0
  55. package/cli/services/delegation-lane-instance.test.js +37 -0
  56. package/cli/services/file-io.js +38 -0
  57. package/cli/services/file-io.test.js +10 -0
  58. package/cli/services/fs-mount.js +66 -0
  59. package/cli/services/fs-mount.test.js +14 -0
  60. package/cli/services/fs-upload-sessions.js +20 -0
  61. package/cli/services/fs-upload-sessions.test.js +14 -0
  62. package/cli/services/git-exec.js +37 -0
  63. package/cli/services/git-exec.test.js +41 -0
  64. package/cli/services/git-import-guard.js +28 -0
  65. package/cli/services/git-import-guard.test.js +18 -0
  66. package/cli/services/service-status.js +191 -0
  67. package/cli/services/service-status.test.js +39 -0
  68. package/cli/services/terminal-dispatch.js +135 -0
  69. package/cli/services/terminal-dispatch.test.js +9 -0
  70. package/cli/services/terminal-registry.js +39 -0
  71. package/cli/services/terminal-registry.test.js +31 -0
  72. package/cli/services/terminal-sessions.js +100 -0
  73. package/cli/services/terminal-sessions.test.js +26 -0
  74. package/cli/services/terminal-window.js +106 -0
  75. package/cli/services/vault-read.js +14 -0
  76. package/cli/services/vault-read.test.js +35 -0
  77. package/cli/supervisor-registry.js +116 -0
  78. package/cli/supervisor-registry.test.js +165 -0
  79. package/cli/supervisor-ui.test.js +230 -30
  80. package/cli/webdav-config.js +84 -0
  81. package/cli/webdav-config.test.js +79 -0
  82. package/cli/webdav-obscure.js +23 -0
  83. package/cli/webdav-obscure.test.js +41 -0
  84. package/cli/webdav-rclone-discovery.js +44 -0
  85. package/cli/webdav-service.js +30 -118
  86. package/cli/webdav-service.test.js +107 -0
  87. package/install.ps1 +102 -102
  88. package/install.sh +49 -49
  89. package/package.json +2 -2
  90. package/scripts/bootstrap.cjs +121 -121
  91. package/supabase/functions/auth-vault/index.ts +350 -350
  92. package/supabase/migrations/001_clauth_schema.sql +94 -94
  93. package/supabase/migrations/002_vault_helpers.sql +90 -90
  94. package/supabase/migrations/20260317_lockout.sql +26 -26
  95. package/cli/assets/codevelop/launcher-active.cmd.template +0 -20
  96. package/cli/assets/codevelop/launcher-static.cmd.template +0 -7
  97. package/cli/assets/codevelop/windows-terminal.profiles.json +0 -48
  98. package/cli/commands/codevelop.js +0 -1190
@@ -0,0 +1,374 @@
1
+ // cli/dashboard/panels/toolbar-panel.js
2
+ // The main toolbar (menu bar), as a real Web Component: <clauth-toolbar-panel>.
3
+ // Owns its own Shadow DOM: the menu bar itself, plus the two modals that are
4
+ // genuinely this toolbar's own concern -- Change Password (chpw-*) and the
5
+ // Enroll Machine panel/result box. Both used to be separate top-level
6
+ // overlays in the served HTML; they now live in this panel's own shadow
7
+ // tree, same move tunnel-panel.js made for its setup wizard.
8
+ //
9
+ // #add-panel (the "Add Service" form) is explicitly OUT of scope for this
10
+ // conversion pass even though its trigger button lives in this toolbar --
11
+ // toggleAddService() stays here as the button's owner (its data-action lives
12
+ // in this template), but the body still reaches into the plain light-DOM
13
+ // #add-panel via document.getElementById, unchanged, because that panel's
14
+ // markup belongs to a different (not yet componentized) surface.
15
+ //
16
+ // Every onclick="globalFn()" became data-action="methodName", dispatched by
17
+ // the shared listener in ../panel-element.js. The one <form> (Change
18
+ // Password) needs a "submit" handler the shared listener doesn't cover
19
+ // (click/keydown only), so mount() wires that one manually. Per-item
20
+ // dynamic args (Windows vs Linux enrollment script) move from inline call
21
+ // arguments to a data-target attribute read off the clicked element --
22
+ // see wizSelectTunnel/wizCopy in tunnel-panel.js for the same pattern.
23
+ //
24
+ // Three of these methods -- restartDaemon, changePassword, enrollMachine --
25
+ // are WRITE_ACTIONS (dashboard.js's installWriteAccessGuards() wraps the
26
+ // GLOBAL name with the write-token acquire-and-retry gate; it can only do
27
+ // that for a real global function, not a class instance method). Each keeps
28
+ // a thin window.<name> bridge below, and the actions map dispatches to
29
+ // window.<name>() for exactly those three, never this.<name>() directly --
30
+ // same reasoning as tunnel-panel.js's wizSubmitCfToken bridge.
31
+ class ClauthToolbarPanel extends window.ClauthPanelElement {
32
+ static tagName = "clauth-toolbar-panel";
33
+ static cssHref = "/dashboard/panels/toolbar-panel.css";
34
+
35
+ template() {
36
+ return `
37
+ <section class="toolbar">
38
+ <button class="btn-refresh" data-action="loadServices">↻ Refresh</button>
39
+ <button class="btn-add" data-action="toggleAddService">+ Add Service</button>
40
+ <button class="btn-check" id="check-btn" data-action="checkAll">⬤ Check All</button>
41
+ <button class="btn-lock" id="unlock-writes-btn" data-action="unlockWrites" title="Enter password to enable saving changes this browser session (needed after a daemon restart when auto-unlocked via --pw)">🔓 Unlock Writes</button>
42
+ <button class="btn-lock" data-action="lockVault">🔒 Lock</button>
43
+ <button class="btn-stop restart" data-action="restartDaemon" title="Restart daemon — keeps vault unlocked">↺ Restart</button>
44
+ <button class="btn-stop stop" data-action="stopDaemon" title="Stop daemon — password required on next start">⏹ Stop</button>
45
+ <button class="btn-cancel" style="margin-left:auto" data-action="toggleChangePw">Change Password</button>
46
+ <button class="btn-add" data-action="toggleEnrollPanel">+ Enroll Machine</button>
47
+ </section>
48
+
49
+ <div class="add-panel" id="enroll-panel" style="display:none">
50
+ <h3>Enroll New Machine</h3>
51
+ <p style="color:#94a3b8;font-size:.82rem;margin:0 0 12px">Generate a one-time script to install clauth on another computer.</p>
52
+ <div class="add-row">
53
+ <div class="add-field">
54
+ <label>Machine label <span style="color:#475569;font-weight:400">(e.g. laptop, server2)</span></label>
55
+ <input class="add-input" id="enroll-label" type="text" placeholder="new-computer" autocomplete="off" spellcheck="false">
56
+ </div>
57
+ <div class="add-field">
58
+ <label>Expires in (hours)</label>
59
+ <input class="add-input" id="enroll-ttl" type="number" min="1" max="168" value="1" style="width:80px">
60
+ </div>
61
+ </div>
62
+ <div style="display:flex;gap:8px;margin-top:8px">
63
+ <button class="btn-chpw-save" data-action="enrollMachine" data-target="windows">Windows Script</button>
64
+ <button class="btn-chpw-save" data-action="enrollMachine" data-target="linux">Linux Script</button>
65
+ <button class="btn-cancel" data-action="toggleEnrollPanel">Cancel</button>
66
+ </div>
67
+ <div id="enroll-msg" class="add-msg"></div>
68
+ <pre id="enroll-result" style="display:none;background:#0a0f1a;border:1px solid #1e3a5f;border-radius:6px;padding:12px;font-size:.78rem;color:#86efac;margin-top:8px;white-space:pre-wrap;word-break:break-all"></pre>
69
+ </div>
70
+
71
+ <div class="chpw-panel" id="chpw-panel">
72
+ <h3>Change Master Password</h3>
73
+ <form id="chpw-form" autocomplete="on">
74
+ <input type="text" name="username" value="clauth" autocomplete="username" style="display:none">
75
+ <div class="chpw-row">
76
+ <div class="chpw-field">
77
+ <label>New password</label>
78
+ <input class="chpw-input" id="chpw-new" type="password" placeholder="min 8 chars" autocomplete="new-password">
79
+ </div>
80
+ <div class="chpw-field">
81
+ <label>Confirm</label>
82
+ <input class="chpw-input" id="chpw-confirm" type="password" placeholder="repeat" autocomplete="new-password">
83
+ </div>
84
+ </div>
85
+ <div class="chpw-foot">
86
+ <button class="btn-chpw-save" type="submit">Update Password</button>
87
+ <button class="btn-cancel" type="button" data-action="toggleChangePw">Cancel</button>
88
+ <span class="chpw-msg" id="chpw-msg"></span>
89
+ </div>
90
+ </form>
91
+ </div>
92
+ `;
93
+ }
94
+
95
+ mount() {
96
+ // The shared delegated listener in ../panel-element.js only covers
97
+ // click/keydown -- a <form> submit needs its own listener. Dispatches
98
+ // through window.changePassword() (the write-guard bridge below), not
99
+ // this.changePassword() directly, for the same reason the actions map
100
+ // does the same for the button-driven write actions.
101
+ this.$("#chpw-form")?.addEventListener("submit", (e) => {
102
+ e.preventDefault();
103
+ window.changePassword();
104
+ });
105
+ }
106
+
107
+ // ── Vault lock / unlock ─────────────────────
108
+ async lockVault() {
109
+ const r = await fetch(BASE + "/lock", { method: "POST" }).then(r => r.json()).catch(() => ({}));
110
+ writeToken = null;
111
+ showLockScreen(r.hard_locked || false);
112
+ }
113
+
114
+ unlockWrites() {
115
+ // Manual unlock from the toolbar — nothing is parked, so make sure a stale
116
+ // park from an earlier dismissed attempt cannot ride along on this unlock.
117
+ clearPendingWriteAction();
118
+ openWriteUnlockModal();
119
+ }
120
+
121
+ // ── Daemon lifecycle ────────────────────────
122
+ async restartDaemon() {
123
+ if (!confirm("Restart the daemon?\n\nThe vault will stay unlocked (boot.key kept).")) return;
124
+ const pull = confirm("Pull latest from git and reinstall dependencies first?\n\nOK = git pull + npm install, then restart\nCancel = just restart with the code already on disk");
125
+ const btn = this.$('[data-action="restartDaemon"]');
126
+ if (btn) { btn.disabled = true; btn.textContent = pull ? "↺ Pulling latest…" : "↺ Restarting…"; }
127
+ try {
128
+ const r = await fetch(BASE + "/restart", {
129
+ method: "POST",
130
+ headers: writeHeaders({ "Content-Type": "application/json" }),
131
+ body: JSON.stringify({ pull })
132
+ });
133
+ if (!r.ok) {
134
+ const body = await r.json().catch(() => ({}));
135
+ alert("Restart aborted — " + (body.error || "pull/install failed") + (body.detail ? "\n\n" + body.detail : ""));
136
+ if (btn) { btn.disabled = false; btn.textContent = "↺ Restart"; }
137
+ return;
138
+ }
139
+ } catch {}
140
+ document.getElementById("main-view").innerHTML = '<div style="text-align:center;padding:80px 20px"><div style="font-size:3rem;margin-bottom:16px">↺</div><div style="font-size:1.1rem;color:#86efac">' + (pull ? "Pulled latest — restarting…" : "Restarting…") + '</div><div style="font-size:.85rem;color:#64748b;margin-top:8px">Page will reload automatically.</div></div>';
141
+ }
142
+
143
+ async stopDaemon() {
144
+ if (!confirm("Stop the daemon?\n\nCredentials will need to be re-entered on next start.")) return;
145
+ try {
146
+ await fetch(BASE + "/shutdown-ui", { method: "POST" });
147
+ } catch {}
148
+ document.getElementById("main-view").innerHTML = '<div style="text-align:center;padding:80px 20px"><div style="font-size:3rem;margin-bottom:16px">⏹</div><div style="font-size:1.1rem;color:#94a3b8">Daemon stopped</div><div style="font-size:.85rem;color:#64748b;margin-top:8px">Restart with: clauth serve start</div></div>';
149
+ }
150
+
151
+ // ── Health check-all ────────────────────────
152
+ async checkAll() {
153
+ const btn = this.$("#check-btn");
154
+ btn.disabled = true; btn.textContent = "Checking…";
155
+
156
+ // Cross-panel calls through the services panel's own public methods --
157
+ // not a DOM-id/class reach into its internals. clauth-services-panel now
158
+ // owns every credential dot inside its own shadow root, same boundary
159
+ // fix as the tunnel-panel delegation right below.
160
+ const servicesPanel = document.querySelector("clauth-services-panel");
161
+ servicesPanel?.setAllDotsChecking();
162
+
163
+ try {
164
+ const r = await fetch(BASE + "/check-all").then(r => r.json());
165
+ if (r.locked) { showLockScreen(); return; }
166
+ if (r.error) throw new Error(r.error);
167
+
168
+ const results = r.results || {};
169
+ for (const [name, result] of Object.entries(results)) {
170
+ if (name === "__tunnel__") {
171
+ // Cross-panel call through the tunnel panel's own public method --
172
+ // not a DOM-id reach into its internals (it's a real component
173
+ // with its own shadow root; this file can't see inside it any
174
+ // other way, which is the point).
175
+ const tunnelPanel = document.querySelector("clauth-tunnel-panel");
176
+ if (tunnelPanel?.updateHealthCheckResult) tunnelPanel.updateHealthCheckResult(result);
177
+ continue;
178
+ }
179
+ servicesPanel?.updateCheckResult(name, result);
180
+ }
181
+
182
+ // Fade green dots after 3 s — red dots stay until next check
183
+ setTimeout(() => servicesPanel?.fadeOkDots(), 3000);
184
+
185
+ } catch (e) {
186
+ servicesPanel?.resetAllDots();
187
+ const errBar = document.getElementById("error-bar");
188
+ errBar.textContent = "⚠ Check failed: " + e.message;
189
+ errBar.style.display = "block";
190
+ } finally {
191
+ btn.disabled = false; btn.textContent = "⬤ Check All";
192
+ }
193
+ }
194
+
195
+ // ── Change Password ─────────────────────────
196
+ toggleChangePw() {
197
+ const panel = this.$("#chpw-panel");
198
+ const open = panel.style.display === "block";
199
+ panel.style.display = open ? "none" : "block";
200
+ if (!open) {
201
+ this.$("#chpw-new").value = "";
202
+ this.$("#chpw-confirm").value = "";
203
+ this.$("#chpw-msg").textContent = "";
204
+ this.$("#chpw-new").focus();
205
+ }
206
+ }
207
+
208
+ async changePassword() {
209
+ const newPw = this.$("#chpw-new").value;
210
+ const confPw = this.$("#chpw-confirm").value;
211
+ const msg = this.$("#chpw-msg");
212
+
213
+ if (!newPw) { msg.className = "chpw-msg fail"; msg.textContent = "Enter a new password."; return; }
214
+ if (newPw.length < 8) { msg.className = "chpw-msg fail"; msg.textContent = "Minimum 8 characters."; return; }
215
+ if (newPw !== confPw) { msg.className = "chpw-msg fail"; msg.textContent = "Passwords don't match."; return; }
216
+
217
+ msg.className = "chpw-msg"; msg.textContent = "Updating…";
218
+ try {
219
+ const r = await fetch(BASE + "/change-pw", {
220
+ method: "POST",
221
+ headers: writeHeaders({ "Content-Type": "application/json" }),
222
+ body: JSON.stringify({ newPassword: newPw })
223
+ }).then(r => r.json());
224
+
225
+ if (r.locked) { showLockScreen(); return; }
226
+ if (r.error) throw new Error(r.error);
227
+ writeToken = r.write_token || null;
228
+
229
+ msg.className = "chpw-msg ok"; msg.textContent = "✓ Password updated";
230
+ this.$("#chpw-new").value = "";
231
+ this.$("#chpw-confirm").value = "";
232
+ setTimeout(() => {
233
+ this.$("#chpw-panel").style.display = "none";
234
+ msg.textContent = "";
235
+ }, 2000);
236
+ } catch (e) {
237
+ msg.className = "chpw-msg fail"; msg.textContent = "✗ " + e.message;
238
+ }
239
+ }
240
+
241
+ // ── Add Service trigger (panel owned elsewhere) ─────
242
+ // #add-panel is NOT part of this component's shadow DOM -- it is a
243
+ // separate, still-light-DOM concern (out of scope for this conversion
244
+ // pass, per the integration brief). This method reaches into it via plain
245
+ // document.getElementById, exactly as it did before conversion.
246
+ async toggleAddService() {
247
+ const panel = document.getElementById("add-panel");
248
+ const open = panel.style.display === "block";
249
+ panel.style.display = open ? "none" : "block";
250
+ if (!open) {
251
+ document.getElementById("add-name").value = "";
252
+ document.getElementById("add-label").value = "";
253
+ document.getElementById("add-description").value = "";
254
+ document.getElementById("add-project").value = "";
255
+ document.getElementById("add-msg").textContent = "";
256
+ // Fetch available key types from server
257
+ const sel = document.getElementById("add-type");
258
+ sel.innerHTML = '<option value="">Loading…</option>';
259
+ try {
260
+ const r = await fetch(BASE + "/meta").then(r => r.json());
261
+ const types = r.key_types || ["token", "secret", "keypair", "connstring", "oauth", "fileserver"];
262
+ sel.innerHTML = types.map(t =>
263
+ `<option value="${t}">${TYPE_LABELS[t] || t}</option>`
264
+ ).join("");
265
+ } catch {
266
+ sel.innerHTML = ["token","secret","keypair","connstring","oauth","fileserver"].map(t =>
267
+ `<option value="${t}">${TYPE_LABELS[t] || t}</option>`
268
+ ).join("");
269
+ }
270
+ document.getElementById("add-name").focus();
271
+ }
272
+ }
273
+
274
+ // ── Enroll Machine ───────────────────────────
275
+ toggleEnrollPanel() {
276
+ const panel = this.$("#enroll-panel");
277
+ panel.style.display = panel.style.display === "none" ? "block" : "none";
278
+ this.$("#enroll-msg").textContent = "";
279
+ this.$("#enroll-result").style.display = "none";
280
+ }
281
+
282
+ async enrollMachine(target) {
283
+ const label = this.$("#enroll-label").value.trim() || "new-computer";
284
+ const ttl = (Number(this.$("#enroll-ttl").value) || 1) * 60;
285
+ const msg = this.$("#enroll-msg");
286
+ const result = this.$("#enroll-result");
287
+ msg.className = "add-msg"; msg.textContent = "Generating enrollment code…";
288
+ result.style.display = "none";
289
+ try {
290
+ const r = await fetch(BASE + "/enroll-machine", {
291
+ method: "POST",
292
+ headers: writeHeaders({ "Content-Type": "application/json" }),
293
+ body: JSON.stringify({ label, ttl_minutes: Number(ttl), target })
294
+ }).then(r => r.json());
295
+ if (r.locked) { showLockScreen(); return; }
296
+ if (r.error) throw new Error(r.error);
297
+ msg.className = "add-msg ok";
298
+ msg.textContent = "✓ Enrollment code generated — script saved to:";
299
+ result.style.display = "block";
300
+ result.textContent = r.script_path + "\n\nRun this script on the new machine.\nIt installs clauth, enrolls with the one-time code, sets up auto-start, then deletes itself.\n\nExpires: " + (r.expires_at || ttl + " minutes");
301
+ } catch (e) {
302
+ msg.className = "add-msg fail";
303
+ msg.textContent = "✗ " + e.message;
304
+ }
305
+ }
306
+
307
+ // Public method for dashboard.js's refreshWriteLockUi() -- #unlock-writes-btn
308
+ // now lives inside this panel's own shadow root, so raw
309
+ // document.getElementById("unlock-writes-btn") from dashboard.js can no
310
+ // longer see it (getElementById never crosses a shadow boundary). Same fix
311
+ // as tunnel-panel.js's updateHealthCheckResult(): a public method the other
312
+ // file calls through document.querySelector("clauth-toolbar-panel"),
313
+ // instead of a raw DOM-id reach into this panel's internals.
314
+ refreshWriteLockButton(hasToken) {
315
+ const b = this.$("#unlock-writes-btn");
316
+ if (!b) return;
317
+ if (hasToken) { b.textContent = "🔓 Writes On"; b.style.opacity = "0.6"; b.style.borderColor = ""; }
318
+ else { b.textContent = "🔓 Unlock Writes"; b.style.opacity = "1"; b.style.borderColor = "#f59e0b"; }
319
+ }
320
+
321
+ get actions() {
322
+ return {
323
+ loadServices: () => window.loadServices(),
324
+ toggleAddService: () => this.toggleAddService(),
325
+ checkAll: () => this.checkAll(),
326
+ unlockWrites: () => this.unlockWrites(),
327
+ lockVault: () => this.lockVault(),
328
+ // Write-guarded -- dispatch through the window bridge, not the
329
+ // instance method directly, so installWriteAccessGuards()'s wrap is
330
+ // never bypassed. See the bridges below and tunnel-panel.js's
331
+ // wizSubmitCfToken for the same reasoning.
332
+ restartDaemon: () => window.restartDaemon(),
333
+ stopDaemon: () => this.stopDaemon(),
334
+ toggleChangePw: () => this.toggleChangePw(),
335
+ toggleEnrollPanel: () => this.toggleEnrollPanel(),
336
+ enrollMachine: (e, el) => window.enrollMachine(el.dataset.target),
337
+ };
338
+ }
339
+ }
340
+ customElements.define(ClauthToolbarPanel.tagName, ClauthToolbarPanel);
341
+
342
+ // Write-guard bridges: installWriteAccessGuards() (dashboard.js) wraps each of
343
+ // these names by reassigning window[name] to a guarded version -- it can only
344
+ // do that for a real global function, not a class instance method. These thin
345
+ // globals delegate to the real implementation on the live panel instance; the
346
+ // actions getter above (and the manual #chpw-form submit listener in
347
+ // mount()) call window.<name>(), never this.<name>() directly, so the guard
348
+ // is never bypassed. Same pattern as tunnel-panel.js's wizSubmitCfToken bridge.
349
+ window.restartDaemon = () => {
350
+ const el = document.querySelector(ClauthToolbarPanel.tagName);
351
+ return el?.restartDaemon();
352
+ };
353
+ window.changePassword = () => {
354
+ const el = document.querySelector(ClauthToolbarPanel.tagName);
355
+ return el?.changePassword();
356
+ };
357
+ window.enrollMachine = (target) => {
358
+ const el = document.querySelector(ClauthToolbarPanel.tagName);
359
+ return el?.enrollMachine(target);
360
+ };
361
+
362
+ // Panel interface registration — see ./panel-registry.js. connectedCallback()
363
+ // only builds the shadow DOM (safe -- no external dependency); it
364
+ // deliberately does NOT call mount() itself, since it fires during HTML
365
+ // parsing, before dashboard.js (and its globals) has loaded. The registry's
366
+ // mountAll() runs after every script has loaded, so THIS is the first safe
367
+ // point to call the real mount().
368
+ window.ClauthPanelRegistry.register({
369
+ id: "toolbar",
370
+ mount: () => {
371
+ const el = document.querySelector(ClauthToolbarPanel.tagName);
372
+ if (el) el.mount();
373
+ },
374
+ });
@@ -0,0 +1,61 @@
1
+ /* cli/dashboard/panels/tunnel-panel.css
2
+ The tunnel bar (data-panel="tunnel-bar") plus its Cloudflare tunnel-setup
3
+ wizard modal -- both owned entirely by tunnel-panel.js. Depends on the
4
+ tokens in ../global.css, loaded before this file. */
5
+ .tunnel-panel{background:#0f1d2d;border:1px solid var(--border-dark);border-radius:8px;padding:1rem 1.25rem;margin-bottom:1.25rem;display:flex;align-items:center;gap:12px;flex-wrap:wrap}
6
+ .tunnel-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}
7
+ .tunnel-dot.off{background:var(--grey-dim)}
8
+ .tunnel-dot.starting{background:var(--orange);animation:pulse 1s infinite}
9
+ .tunnel-dot.on{background:var(--green-bright);animation:pulse 2s infinite}
10
+ .tunnel-dot.err{background:var(--red-bright)}
11
+ .tunnel-label{font-size:.85rem;color:var(--grey);flex:1}
12
+ .tunnel-label strong{color:var(--text-bright)}
13
+ .tunnel-url{font-family:'Courier New',monospace;font-size:.78rem;color:var(--blue-light);word-break:break-all}
14
+ .tunnel-url a{color:var(--blue-light);text-decoration:none}
15
+ .tunnel-url a:hover{text-decoration:underline}
16
+ .btn-claude{background:linear-gradient(135deg,var(--copper),var(--orange));color:var(--bg-darker);padding:8px 18px;font-size:.85rem;border-radius:7px;border:none;cursor:pointer;font-weight:700;letter-spacing:.3px;transition:all .15s;white-space:nowrap}
17
+ .btn-claude:hover{filter:brightness(1.1);transform:translateY(-1px)}
18
+ .btn-claude:disabled{opacity:.4;cursor:not-allowed;transform:none;filter:none}
19
+ .btn-tunnel-stop{background:var(--bg-card);color:var(--red);border:1px solid var(--border);padding:6px 12px;font-size:.8rem;border-radius:6px;cursor:pointer;font-weight:500}
20
+ .btn-tunnel-stop:hover{background:#2d1f1f;border-color:var(--red)}
21
+ .tunnel-err{font-size:.78rem;color:var(--red);width:100%;margin-top:4px}
22
+ .mcp-row{display:flex;align-items:center;gap:8px;margin-bottom:8px}
23
+ .mcp-label{font-size:.72rem;color:var(--grey-dim);min-width:80px;text-transform:uppercase;letter-spacing:.5px;font-weight:600}
24
+ .mcp-val{flex:1;font-family:'Courier New',monospace;font-size:.82rem;color:var(--blue-light);background:var(--bg-darker);border:1px solid var(--border-dark);border-radius:4px;padding:6px 10px;word-break:break-all;user-select:all}
25
+ .mcp-copy{background:none;border:1px solid var(--border);color:var(--grey);border-radius:4px;padding:4px 8px;cursor:pointer;font-size:.75rem;font-family:'Courier New',monospace;transition:all .15s;flex-shrink:0}
26
+ .mcp-copy:hover{border-color:var(--blue-light);color:var(--blue-light)}
27
+ .mcp-copy.ok{border-color:var(--green);color:var(--green)}
28
+ /* Tunnel setup wizard */
29
+ .wizard-panel{background:#0a1628;border:1px solid var(--border-dark);border-radius:10px;padding:1.5rem;margin-bottom:1.25rem;display:none}
30
+ .wizard-panel.open{display:block}
31
+ .wizard-header{display:flex;align-items:center;gap:10px;margin-bottom:1.25rem}
32
+ .wizard-title{font-size:1rem;font-weight:600;color:var(--text-white);flex:1}
33
+ .wizard-steps{display:flex;gap:6px;margin-bottom:1.5rem;flex-wrap:wrap}
34
+ .wstep{padding:4px 10px;border-radius:20px;font-size:.72rem;font-weight:600;border:1px solid var(--border-dark);color:var(--grey-darker);background:var(--bg-dark)}
35
+ .wstep.active{border-color:var(--blue);color:var(--blue-light);background:rgba(59,130,246,.1)}
36
+ .wstep.done{border-color:var(--green-dark);color:var(--green);background:rgba(74,222,128,.08)}
37
+ .wizard-body{min-height:80px}
38
+ .wizard-foot{display:flex;gap:8px;margin-top:1.25rem;align-items:center}
39
+ .btn-wiz-primary{background:var(--blue);color:var(--white);padding:8px 20px;font-size:.875rem;border-radius:7px;border:none;cursor:pointer;font-weight:600}
40
+ .btn-wiz-primary:hover{background:var(--blue-dark)}
41
+ .btn-wiz-primary:disabled{opacity:.4;cursor:not-allowed}
42
+ .btn-wiz-secondary{background:var(--bg-card);color:var(--grey);padding:8px 16px;font-size:.875rem;border-radius:7px;border:1px solid var(--border);cursor:pointer}
43
+ .btn-wiz-secondary:hover{color:var(--text-bright)}
44
+ .wiz-input{width:100%;background:var(--bg-dark);border:1px solid var(--border);border-radius:6px;color:var(--text-bright);font-family:'Courier New',monospace;font-size:.9rem;padding:9px 12px;outline:none;margin-top:8px;transition:border-color .2s}
45
+ .wiz-input:focus{border-color:var(--blue)}
46
+ .wiz-label{font-size:.8rem;color:var(--amber);margin-bottom:4px}
47
+ .wiz-msg{font-size:.82rem;margin-left:auto}
48
+ .wiz-msg.ok{color:var(--green)}.wiz-msg.fail{color:var(--red)}
49
+ .wiz-log{background:#030712;border:1px solid var(--bg-card);border-radius:6px;padding:10px;font-family:'Courier New',monospace;font-size:.75rem;color:#6ee7b7;max-height:160px;overflow-y:auto;margin-top:10px;white-space:pre-wrap}
50
+ .wiz-tunnel-list{display:flex;flex-direction:column;gap:8px;margin-top:10px}
51
+ .wiz-tunnel-item{display:flex;align-items:center;gap:10px;padding:10px 12px;background:var(--bg-dark);border:1px solid var(--border-dark);border-radius:6px;cursor:pointer;transition:border-color .15s}
52
+ .wiz-tunnel-item:hover,.wiz-tunnel-item.selected{border-color:var(--blue);background:rgba(59,130,246,.08)}
53
+ .wiz-tunnel-name{font-weight:600;color:var(--text-white);font-size:.9rem;flex:1}
54
+ .wiz-tunnel-id{font-family:'Courier New',monospace;font-size:.72rem;color:var(--grey-darker)}
55
+ .wiz-tunnel-status{font-size:.72rem;padding:2px 7px;border-radius:4px;background:rgba(74,222,128,.1);color:var(--green);border:1px solid rgba(74,222,128,.2)}
56
+ .wiz-desc{font-size:.85rem;color:var(--grey);line-height:1.6;margin-bottom:.75rem}
57
+ .wiz-link{color:var(--blue-light);text-decoration:none;font-size:.82rem}
58
+ .wiz-link:hover{text-decoration:underline}
59
+ .wiz-test-result{padding:12px 14px;border-radius:8px;font-size:.85rem;margin-top:10px;display:none}
60
+ .wiz-test-result.ok{background:rgba(74,222,128,.08);border:1px solid rgba(74,222,128,.2);color:var(--green)}
61
+ .wiz-test-result.fail{background:rgba(248,113,113,.08);border:1px solid rgba(248,113,113,.2);color:var(--red)}