@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.
- package/.clauth-skill/references/keys-guide.md +270 -270
- package/cli/api.js +238 -238
- package/cli/commands/install.js +413 -396
- package/cli/commands/login.test.js +49 -1
- package/cli/commands/ops-install.js +11 -10
- package/cli/commands/ops.test.js +270 -0
- package/cli/commands/scrub.test.js +70 -0
- package/cli/commands/serve.js +2613 -11219
- package/cli/commands/uninstall.js +164 -164
- package/cli/dashboard/dashboard.js +592 -0
- package/cli/dashboard/global.css +65 -0
- package/cli/dashboard/panel-element.js +79 -0
- package/cli/dashboard/panels/build-status-panel.css +12 -0
- package/cli/dashboard/panels/build-status-panel.js +98 -0
- package/cli/dashboard/panels/panel-registry.js +58 -0
- package/cli/dashboard/panels/services-panel.css +95 -0
- package/cli/dashboard/panels/services-panel.js +872 -0
- package/cli/dashboard/panels/surfaces-panel.css +59 -0
- package/cli/dashboard/panels/surfaces-panel.js +474 -0
- package/cli/dashboard/panels/toolbar-panel.css +42 -0
- package/cli/dashboard/panels/toolbar-panel.js +374 -0
- package/cli/dashboard/panels/tunnel-panel.css +61 -0
- package/cli/dashboard/panels/tunnel-panel.js +688 -0
- package/cli/dashboard/panels/webdav-panel.css +38 -0
- package/cli/dashboard/panels/webdav-panel.js +166 -0
- package/cli/fingerprint.test.js +95 -0
- package/cli/http/components/callagent-terminal-component.js +310 -0
- package/cli/http/components/dashboard-component.js +123 -0
- package/cli/http/components/inbox-component.js +41 -0
- package/cli/http/components/mcp-transport-component.js +275 -0
- package/cli/http/components/oauth-component.js +207 -0
- package/cli/http/components/service-crud-component.js +288 -0
- package/cli/http/components/supervisor-component.js +104 -0
- package/cli/http/components/system-component.js +184 -0
- package/cli/http/components/tunnel-component.js +508 -0
- package/cli/http/components/vault-auth-component.js +264 -0
- package/cli/http/components/watchdog-component.js +28 -0
- package/cli/http/request-utils.js +21 -0
- package/cli/http/servers/ops-server.js +335 -0
- package/cli/index.js +5 -91
- package/cli/lib/fs-git.test.js +467 -0
- package/cli/mcp/providers/call-agent-provider.js +30 -0
- package/cli/mcp/providers/clauth-core-provider.js +461 -0
- package/cli/mcp/providers/ops-provider.js +42 -0
- package/cli/mcp/providers/terminal-provider.js +81 -0
- package/cli/mcp/tool-registry.js +48 -0
- package/cli/mcp/tool-schemas.js +822 -0
- package/cli/mcp/tool-schemas.test.js +49 -0
- package/cli/ops/pm2-preflight.js +54 -0
- package/cli/recovery.js +6 -0
- package/cli/recovery.test.js +179 -0
- package/cli/services/agent-pool-instance.js +38 -0
- package/cli/services/claude-binary.js +29 -0
- package/cli/services/delegation-lane-instance.js +35 -0
- package/cli/services/delegation-lane-instance.test.js +37 -0
- package/cli/services/file-io.js +38 -0
- package/cli/services/file-io.test.js +10 -0
- package/cli/services/fs-mount.js +66 -0
- package/cli/services/fs-mount.test.js +14 -0
- package/cli/services/fs-upload-sessions.js +20 -0
- package/cli/services/fs-upload-sessions.test.js +14 -0
- package/cli/services/git-exec.js +37 -0
- package/cli/services/git-exec.test.js +41 -0
- package/cli/services/git-import-guard.js +28 -0
- package/cli/services/git-import-guard.test.js +18 -0
- package/cli/services/service-status.js +191 -0
- package/cli/services/service-status.test.js +39 -0
- package/cli/services/terminal-dispatch.js +135 -0
- package/cli/services/terminal-dispatch.test.js +9 -0
- package/cli/services/terminal-registry.js +39 -0
- package/cli/services/terminal-registry.test.js +31 -0
- package/cli/services/terminal-sessions.js +100 -0
- package/cli/services/terminal-sessions.test.js +26 -0
- package/cli/services/terminal-window.js +106 -0
- package/cli/services/vault-read.js +14 -0
- package/cli/services/vault-read.test.js +35 -0
- package/cli/supervisor-registry.js +116 -0
- package/cli/supervisor-registry.test.js +165 -0
- package/cli/supervisor-ui.test.js +230 -30
- package/cli/webdav-config.js +84 -0
- package/cli/webdav-config.test.js +79 -0
- package/cli/webdav-obscure.js +23 -0
- package/cli/webdav-obscure.test.js +41 -0
- package/cli/webdav-rclone-discovery.js +44 -0
- package/cli/webdav-service.js +30 -118
- package/cli/webdav-service.test.js +107 -0
- package/install.ps1 +102 -102
- package/install.sh +49 -49
- package/package.json +2 -2
- package/scripts/bootstrap.cjs +121 -121
- package/supabase/functions/auth-vault/index.ts +350 -350
- package/supabase/migrations/001_clauth_schema.sql +94 -94
- package/supabase/migrations/002_vault_helpers.sql +90 -90
- package/supabase/migrations/20260317_lockout.sql +26 -26
- package/cli/assets/codevelop/launcher-active.cmd.template +0 -20
- package/cli/assets/codevelop/launcher-static.cmd.template +0 -7
- package/cli/assets/codevelop/windows-terminal.profiles.json +0 -48
- 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)}
|