@lifeaitools/clauth 2.1.1 → 2.10.1

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 +2618 -11238
  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 +86 -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,688 @@
1
+ // cli/dashboard/panels/tunnel-panel.js
2
+ // The Tunnel panel, as a real Web Component: <clauth-tunnel-panel>. Owns its
3
+ // own Shadow DOM (tunnel bar + the full Cloudflare tunnel-setup wizard, both
4
+ // rendered inside this element -- the wizard used to be a separate top-level
5
+ // overlay in the served HTML; it's genuinely part of this panel's concern,
6
+ // so it now lives in this panel's own shadow tree). No other panel can see
7
+ // or be affected by anything in here, and this panel can't see anything
8
+ // outside itself except through the shared apiFetch/writeHeaders/BASE
9
+ // globals dashboard.js provides (a cross-cutting API client, not panel
10
+ // state -- sharing that is normal for any component library).
11
+ //
12
+ // State that used to be module-level `let` (tunnelPollTimer, wizStep,
13
+ // wizData, window._wizSelectedTunnel) is now instance state on `this` --
14
+ // the correct home for one panel instance's own data.
15
+ //
16
+ // Every onclick="globalFn()" became data-action="methodName" (or
17
+ // data-keydown for Enter-key submits), dispatched by the shared listener in
18
+ // ../panel-element.js. Per-item dynamic args (which tunnel was clicked,
19
+ // which copy target) move from inline call arguments to data-* attributes
20
+ // read off the clicked element -- the standard delegated-event pattern.
21
+ class ClauthTunnelPanel extends window.ClauthPanelElement {
22
+ static tagName = "clauth-tunnel-panel";
23
+ static cssHref = "/dashboard/panels/tunnel-panel.css";
24
+
25
+ template() {
26
+ return `
27
+ <section class="tunnel-panel">
28
+ <!-- not_configured -->
29
+ <div class="tunnel-state not-configured" style="display:none;align-items:center;gap:10px;width:100%;flex-wrap:wrap">
30
+ <div class="tunnel-dot off"></div>
31
+ <div class="tunnel-label"><strong>claude.ai MCP</strong> — No tunnel configured</div>
32
+ <button class="btn-tunnel-stop" data-action="runTunnelSetup">Setup Tunnel</button>
33
+ </div>
34
+ <!-- missing_cloudflared -->
35
+ <div class="tunnel-state missing-cloudflared" style="display:none;align-items:center;gap:10px;width:100%;flex-wrap:wrap">
36
+ <div class="tunnel-dot err"></div>
37
+ <div class="tunnel-label"><strong>claude.ai MCP</strong> — cloudflared not installed</div>
38
+ <a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/" target="_blank" style="color:#60a5fa;font-size:.8rem">Download cloudflared ↗</a>
39
+ <span class="tunnel-err" style="display:inline;font-size:.78rem;color:#94a3b8">Then run: clauth tunnel setup</span>
40
+ </div>
41
+ <!-- starting -->
42
+ <div class="tunnel-state starting" style="display:none;align-items:center;gap:10px;width:100%;flex-wrap:wrap">
43
+ <div class="tunnel-dot starting"></div>
44
+ <div class="tunnel-label"><strong>claude.ai MCP</strong> — Tunnel starting…</div>
45
+ </div>
46
+ <!-- live -->
47
+ <div class="tunnel-state live" style="display:none;align-items:center;gap:10px;width:100%;flex-wrap:wrap">
48
+ <div class="tunnel-dot on"></div>
49
+ <div class="tunnel-label"><strong>claude.ai MCP</strong> — <span class="tunnel-url"><a href="" target="_blank" id="tunnel-live-url"></a></span></div>
50
+ <button class="btn-check" style="padding:6px 12px;font-size:.8rem" data-action="testTunnel">Test</button>
51
+ <button class="btn-claude" data-action="openClaude">Connect claude.ai</button>
52
+ <button class="btn-tunnel-stop" data-action="stopTunnel">Stop</button>
53
+ </div>
54
+ <!-- error -->
55
+ <div class="tunnel-state error" style="display:none;align-items:center;gap:10px;width:100%;flex-wrap:wrap">
56
+ <div class="tunnel-dot err"></div>
57
+ <div class="tunnel-label"><strong>claude.ai MCP</strong> — Tunnel error — check cloudflared config</div>
58
+ <button class="btn-tunnel-stop" data-action="startTunnel">Retry</button>
59
+ </div>
60
+ <!-- not_started -->
61
+ <div class="tunnel-state not-started" style="display:flex;align-items:center;gap:10px;width:100%;flex-wrap:wrap">
62
+ <div class="tunnel-dot off"></div>
63
+ <div class="tunnel-label"><strong>claude.ai MCP</strong> — checking tunnel…</div>
64
+ <button class="btn-tunnel-stop" data-action="startTunnel">Start Tunnel</button>
65
+ </div>
66
+ </section>
67
+
68
+ <div class="wizard-panel" id="wizard-panel">
69
+ <div class="wizard-header">
70
+ <div class="tunnel-dot off" id="wiz-dot" style="width:10px;height:10px;border-radius:50%"></div>
71
+ <div class="wizard-title">claude.ai Tunnel Setup</div>
72
+ <button class="btn-cancel" data-action="closeSetupWizard">✕ Dismiss</button>
73
+ </div>
74
+ <div class="wizard-steps" id="wizard-steps"></div>
75
+ <div class="wizard-body" id="wizard-body"><p class="loading">Checking setup state…</p></div>
76
+ <div class="wizard-foot" id="wizard-foot"></div>
77
+ </div>
78
+ `;
79
+ }
80
+
81
+ mount() {
82
+ this.tunnelPollTimer = null;
83
+ this.wizStep = null;
84
+ this.wizData = {};
85
+ this.wizSelectedTunnel = null;
86
+ this.pollTunnel();
87
+ }
88
+
89
+ // ── Tunnel management ───────────────────────
90
+ async pollTunnel() {
91
+ if (this.tunnelPollTimer) clearInterval(this.tunnelPollTimer);
92
+ const r = await apiFetch("/tunnel");
93
+ if (r) this.renderTunnelPanel(r.status, r.url, r.error);
94
+ // Poll every 3s; slow to 10s once live
95
+ this.tunnelPollTimer = setInterval(async () => {
96
+ const r2 = await apiFetch("/tunnel");
97
+ if (!r2) return;
98
+ this.renderTunnelPanel(r2.status, r2.url, r2.error);
99
+ if (r2.status === "live") {
100
+ clearInterval(this.tunnelPollTimer);
101
+ this.tunnelPollTimer = setInterval(async () => {
102
+ const r3 = await apiFetch("/tunnel");
103
+ if (r3) this.renderTunnelPanel(r3.status, r3.url, r3.error);
104
+ }, 10000);
105
+ }
106
+ }, 3000);
107
+ }
108
+
109
+ renderTunnelPanel(status, url, error) {
110
+ const panel = this.$(".tunnel-panel");
111
+ if (!panel) return;
112
+ panel.querySelectorAll(".tunnel-state").forEach(el => el.style.display = "none");
113
+ const cls = (status || "not-started").replace(/_/g, "-");
114
+ const active = panel.querySelector(".tunnel-state." + cls);
115
+ if (active) active.style.display = "flex";
116
+ if (status === "live" && url) {
117
+ const sseUrl = url.startsWith("http") ? url + "/sse" : url;
118
+ const link = panel.querySelector(".tunnel-state.live a");
119
+ if (link) { link.href = sseUrl; link.textContent = sseUrl; }
120
+ const liveUrlEl = this.$("#tunnel-live-url");
121
+ if (liveUrlEl) { liveUrlEl.href = sseUrl; liveUrlEl.textContent = sseUrl; }
122
+ }
123
+ }
124
+
125
+ async toggleTunnel(action) {
126
+ try {
127
+ await fetch(BASE + "/tunnel", {
128
+ method: "POST",
129
+ headers: { "Content-Type": "application/json" },
130
+ body: JSON.stringify({ action })
131
+ });
132
+ setTimeout(() => this.pollTunnel(), action === "start" ? 2000 : 500);
133
+ } catch {}
134
+ }
135
+
136
+ async testTunnel() {
137
+ const liveState = this.$(".tunnel-panel .tunnel-state.live");
138
+ const btn = liveState ? liveState.querySelector(".btn-check") : null;
139
+ const labelEl = liveState ? liveState.querySelector(".tunnel-label") : null;
140
+ if (btn) { btn.disabled = true; btn.textContent = "Testing…"; }
141
+
142
+ try {
143
+ const r = await fetch(BASE + "/tunnel/test").then(r => r.json());
144
+ if (labelEl) {
145
+ if (r.ok) {
146
+ labelEl.innerHTML = '<strong>claude.ai MCP</strong> — <span style="color:#4ade80">PASS</span> ' +
147
+ r.latencyMs + 'ms · SSE ' + (r.sseReachable ? 'reachable' : 'unreachable') +
148
+ ' · <span class="tunnel-url"><a href="' + r.sseUrl + '" target="_blank">' + r.sseUrl + '</a></span>';
149
+ } else {
150
+ labelEl.innerHTML = '<strong>claude.ai MCP</strong> — <span style="color:#f87171">FAIL</span> ' + (r.reason || "unknown error");
151
+ }
152
+ }
153
+ } catch (e) {
154
+ if (labelEl) labelEl.innerHTML = '<strong>claude.ai MCP</strong> — <span style="color:#f87171">FAIL</span> ' + e.message;
155
+ } finally {
156
+ if (btn) { btn.disabled = false; btn.textContent = "Test"; }
157
+ }
158
+ }
159
+
160
+ // Public method other panels call for cross-panel communication -- the
161
+ // correct boundary now that this panel is a real component: toolbar-
162
+ // panel.js's checkAll() calls this instead of reaching into DOM by id
163
+ // (the id it used to reach for, "tunnel-dot"/"tunnel-label", never
164
+ // existed even before this panel had its own shadow root -- only the
165
+ // CLASS names tunnel-dot/tunnel-label did, so this call site was already
166
+ // dead/broken, just never exercised in a way that surfaced the
167
+ // TypeError). Fixed here as the correct component-boundary call, not a
168
+ // DOM-id lookup into another panel's internals.
169
+ updateHealthCheckResult(result) {
170
+ const liveState = this.$(".tunnel-panel .tunnel-state.live");
171
+ const tDot = liveState ? liveState.querySelector(".tunnel-dot") : null;
172
+ const tLabel = liveState ? liveState.querySelector(".tunnel-label") : null;
173
+ if (!tDot || !tLabel) return;
174
+ if (result.ok) {
175
+ tDot.className = "tunnel-dot on";
176
+ tLabel.innerHTML = '<strong>claude.ai MCP</strong> — tunnel healthy';
177
+ } else if (result.running) {
178
+ tDot.className = "tunnel-dot starting";
179
+ tLabel.innerHTML = '<strong>claude.ai MCP</strong> — ' + (result.reason || "starting…");
180
+ } else {
181
+ tDot.className = "tunnel-dot err";
182
+ tLabel.innerHTML = '<strong>claude.ai MCP</strong> — ' + (result.reason || "not running");
183
+ }
184
+ }
185
+
186
+ openClaude() {
187
+ fetch(BASE + "/tunnel").then(r => r.json()).then(t => {
188
+ const sseUrl = t.sseUrl || (t.url && t.url.startsWith("http") ? t.url + "/sse" : null);
189
+ if (sseUrl) {
190
+ navigator.clipboard.writeText(sseUrl).then(() => {
191
+ const btn = this.$(".tunnel-panel .tunnel-state.live .btn-claude");
192
+ if (btn) {
193
+ btn.textContent = "SSE URL copied!";
194
+ setTimeout(() => { btn.textContent = "Connect claude.ai"; }, 2000);
195
+ }
196
+ }).catch(() => {});
197
+ window.open("https://claude.ai/settings/integrations", "_blank");
198
+ }
199
+ });
200
+ }
201
+
202
+ // ── Tunnel Setup Wizard ─────────────────────
203
+ async openSetupWizard() {
204
+ const panel = this.$("#wizard-panel");
205
+ panel.classList.add("open");
206
+ panel.scrollIntoView({ behavior: "smooth", block: "start" });
207
+ this.wizStep = "loading";
208
+ this.renderWizBody('<p class="loading">Checking setup state…</p>', [], []);
209
+
210
+ const state = await apiFetch("/tunnel/setup-state");
211
+ if (!state) {
212
+ this.renderWizBody('<p class="wiz-desc" style="color:#f87171">Could not reach daemon.</p>', [], []);
213
+ return;
214
+ }
215
+
216
+ if (state.step === "ready") {
217
+ await apiFetch("/tunnel/start", { method: "POST" });
218
+ this.closeSetupWizard();
219
+ return;
220
+ }
221
+
222
+ this.wizData = state;
223
+
224
+ if (state.step === "need_cf_token") this.wizShowCfToken();
225
+ else if (state.step === "pick_tunnel") this.wizShowPickTunnel(state.tunnels);
226
+ else if (state.step === "no_tunnels") this.wizShowCreateViaCfApi(state.accountId);
227
+ else this.wizShowInstallCheck(); // no CF token — needs cloudflared login
228
+ }
229
+
230
+ closeSetupWizard() {
231
+ this.$("#wizard-panel").classList.remove("open");
232
+ this.wizStep = null; this.wizData = {};
233
+ }
234
+
235
+ renderWizBody(html, steps, footHtml) {
236
+ this.$("#wizard-body").innerHTML = html;
237
+
238
+ const stepLabels = ["CF Token", "Tunnel", "cloudflared", "MCP Setup", "Test"];
239
+ const stepsEl = this.$("#wizard-steps");
240
+ stepsEl.innerHTML = stepLabels.map((label, i) => {
241
+ const stepKeys = ["need_cf_token", "pick_tunnel", "check_cf", "mcp_setup", "test"];
242
+ const currentIdx = stepKeys.indexOf(this.wizStep);
243
+ let cls = "wstep";
244
+ if (i < currentIdx) cls += " done";
245
+ else if (i === currentIdx) cls += " active";
246
+ return `<div class="${cls}">${i < currentIdx ? "✓ " : ""}${label}</div>`;
247
+ }).join("");
248
+
249
+ this.$("#wizard-foot").innerHTML = footHtml.join("");
250
+ }
251
+
252
+ // Step: Enter CF API token
253
+ wizShowCfToken() {
254
+ this.wizStep = "need_cf_token";
255
+ this.renderWizBody(`
256
+ <div class="wiz-desc">A Cloudflare API token is needed to check for existing tunnels and configure new ones.</div>
257
+ <div class="wiz-label">Cloudflare API Token</div>
258
+ <input class="wiz-input" id="wiz-cf-token-input" type="password" placeholder="Paste your CF API token…" autocomplete="off" spellcheck="false" data-keydown="onCfTokenKeydown">
259
+ <div style="margin-top:8px">
260
+ <a class="wiz-link" href="https://dash.cloudflare.com/profile/api-tokens" target="_blank">↗ Create token at Cloudflare Dashboard</a>
261
+ <span style="color:#475569;font-size:.78rem;margin-left:8px">(needs Cloudflare Tunnel: Edit permission)</span>
262
+ </div>
263
+ <div class="wiz-msg fail" id="wiz-cf-err" style="display:none;margin-top:8px"></div>
264
+ `, [], [
265
+ `<button class="btn-wiz-primary" id="wiz-cf-btn" data-action="wizSubmitCfToken">Verify &amp; Save Token</button>`,
266
+ `<button class="btn-wiz-secondary" data-action="closeSetupWizard">Postpone</button>`,
267
+ `<span class="wiz-msg" id="wiz-cf-msg"></span>`
268
+ ]);
269
+ }
270
+
271
+ onCfTokenKeydown(e) {
272
+ if (e.key === "Enter") this.wizSubmitCfToken();
273
+ }
274
+
275
+ async wizSubmitCfToken() {
276
+ const input = this.$("#wiz-cf-token-input");
277
+ const btn = this.$("#wiz-cf-btn");
278
+ const errEl = this.$("#wiz-cf-err");
279
+ const token = input.value.trim();
280
+ if (!token) return;
281
+
282
+ btn.disabled = true; btn.textContent = "Verifying…";
283
+ if (errEl) errEl.style.display = "none";
284
+
285
+ const r = await fetch(BASE + "/tunnel/setup/cf-token", {
286
+ method: "POST",
287
+ headers: writeHeaders({ "Content-Type": "application/json" }),
288
+ body: JSON.stringify({ token }),
289
+ }).then(r => r.json()).catch(() => null);
290
+
291
+ btn.disabled = false; btn.textContent = "Verify & Save Token";
292
+
293
+ if (!r || r.error) {
294
+ if (errEl) { errEl.textContent = r?.error || "Request failed"; errEl.style.display = "block"; }
295
+ return;
296
+ }
297
+
298
+ this.wizData.accountId = r.accountId;
299
+ const tunnelState = await apiFetch("/tunnel/setup-state");
300
+ if (tunnelState?.step === "pick_tunnel") this.wizShowPickTunnel(tunnelState.tunnels);
301
+ else this.wizShowInstallCheck();
302
+ }
303
+
304
+ // Step: Pick existing tunnel
305
+ wizShowPickTunnel(tunnels) {
306
+ this.wizStep = "pick_tunnel";
307
+
308
+ const listHtml = tunnels.map(t => `
309
+ <div class="wiz-tunnel-item" id="wti-${t.id}" data-action="wizSelectTunnel" data-tunnel-id="${t.id}" data-tunnel-name="${t.name}">
310
+ <div style="flex:1">
311
+ <div class="wiz-tunnel-name">${t.name}</div>
312
+ <div class="wiz-tunnel-id">${t.id}</div>
313
+ </div>
314
+ <div class="wiz-tunnel-status">${t.status || "active"}</div>
315
+ </div>
316
+ `).join("");
317
+
318
+ this.renderWizBody(`
319
+ <div class="wiz-desc">Found ${tunnels.length} existing Cloudflare tunnel${tunnels.length !== 1 ? "s" : ""}. Select one to use, or create a new one.</div>
320
+ <div class="wiz-tunnel-list">${listHtml}</div>
321
+ <div style="margin-top:12px">
322
+ <div class="wiz-label">Public hostname for selected tunnel (e.g. clauth.yourdomain.com)</div>
323
+ <input class="wiz-input" id="wiz-hostname-input" type="text" placeholder="clauth.yourdomain.com" spellcheck="false" autocomplete="off">
324
+ </div>
325
+ <div class="wiz-msg fail" id="wiz-pick-err" style="display:none;margin-top:8px"></div>
326
+ `, [], [
327
+ `<button class="btn-wiz-primary" id="wiz-pick-btn" data-action="wizSaveTunnel">Use Selected Tunnel</button>`,
328
+ `<button class="btn-wiz-secondary" data-action="wizShowInstallCheck">Create New Tunnel Instead</button>`,
329
+ `<button class="btn-wiz-secondary" data-action="closeSetupWizard">Postpone</button>`
330
+ ]);
331
+
332
+ this.wizSelectedTunnel = null;
333
+ }
334
+
335
+ wizSelectTunnel(e, el) {
336
+ this.$$(".wiz-tunnel-item").forEach(item => item.classList.remove("selected"));
337
+ el.classList.add("selected");
338
+ this.wizSelectedTunnel = { id: el.dataset.tunnelId, name: el.dataset.tunnelName };
339
+ }
340
+
341
+ async wizSaveTunnel() {
342
+ const sel = this.wizSelectedTunnel;
343
+ const hostname = this.$("#wiz-hostname-input")?.value.trim();
344
+ const errEl = this.$("#wiz-pick-err");
345
+ const btn = this.$("#wiz-pick-btn");
346
+
347
+ if (!sel) { if (errEl) { errEl.textContent = "Select a tunnel first"; errEl.style.display = "block"; } return; }
348
+ if (!hostname) { if (errEl) { errEl.textContent = "Enter a public hostname"; errEl.style.display = "block"; } return; }
349
+
350
+ btn.disabled = true; btn.textContent = "Saving…";
351
+
352
+ const r = await fetch(BASE + "/tunnel/setup/cf-save", {
353
+ method: "POST",
354
+ headers: { "Content-Type": "application/json" },
355
+ body: JSON.stringify({ tunnelId: sel.id, tunnelName: sel.name, hostname }),
356
+ }).then(r => r.json()).catch(() => null);
357
+
358
+ btn.disabled = false; btn.textContent = "Use Selected Tunnel";
359
+
360
+ if (!r?.ok) {
361
+ if (errEl) { errEl.textContent = r?.error || "Save failed"; errEl.style.display = "block"; }
362
+ return;
363
+ }
364
+
365
+ await apiFetch("/tunnel/start", { method: "POST" });
366
+ this.wizShowMcpSetup(hostname);
367
+ }
368
+
369
+ // Step: Create tunnel via CF API (CF token already in vault — no cloudflared login needed)
370
+ wizShowCreateViaCfApi(accountId) {
371
+ this.wizStep = "pick_tunnel";
372
+ this.wizData.accountId = accountId;
373
+ this.renderWizBody(`
374
+ <div class="wiz-desc">No existing tunnels found. Create a new one using your Cloudflare API token.</div>
375
+ <div class="wiz-label">Tunnel name</div>
376
+ <input class="wiz-input" id="wiz-api-tname" type="text" value="clauth" spellcheck="false">
377
+ <div class="wiz-label" style="margin-top:10px">Public hostname (e.g. clauth.yourdomain.com)</div>
378
+ <input class="wiz-input" id="wiz-api-hostname" type="text" placeholder="clauth.yourdomain.com" spellcheck="false">
379
+ <div class="wiz-msg fail" id="wiz-api-err" style="display:none;margin-top:8px"></div>
380
+ `, [], [
381
+ `<button class="btn-wiz-primary" id="wiz-api-btn" data-action="wizRunCreateViaCfApi">Create Tunnel</button>`,
382
+ `<button class="btn-wiz-secondary" data-action="closeSetupWizard">Postpone</button>`,
383
+ `<span class="wiz-msg" id="wiz-api-msg"></span>`
384
+ ]);
385
+ }
386
+
387
+ async wizRunCreateViaCfApi() {
388
+ const name = this.$("#wiz-api-tname")?.value.trim();
389
+ const hostname = this.$("#wiz-api-hostname")?.value.trim();
390
+ const btn = this.$("#wiz-api-btn");
391
+ const err = this.$("#wiz-api-err");
392
+ const msg = this.$("#wiz-api-msg");
393
+ if (!name || !hostname) { if (err) { err.textContent = "Name and hostname required"; err.style.display = "block"; } return; }
394
+ btn.disabled = true; btn.textContent = "Creating…"; if (err) err.style.display = "none";
395
+ if (msg) msg.textContent = "Calling Cloudflare API…";
396
+ const r = await fetch(BASE + "/tunnel/setup/cf-create-api", {
397
+ method: "POST",
398
+ headers: { "Content-Type": "application/json" },
399
+ body: JSON.stringify({ name, hostname, accountId: this.wizData.accountId }),
400
+ }).then(r => r.json()).catch(() => null);
401
+ btn.disabled = false; btn.textContent = "Create Tunnel";
402
+ if (!r?.ok) {
403
+ if (err) { err.textContent = r?.error || "Creation failed"; err.style.display = "block"; }
404
+ if (msg) msg.textContent = "";
405
+ return;
406
+ }
407
+ await apiFetch("/tunnel/start", { method: "POST" });
408
+ this.wizShowMcpSetup(hostname);
409
+ }
410
+
411
+ // Step: cloudflared install check (for new tunnel creation flow)
412
+ async wizShowInstallCheck() {
413
+ this.wizStep = "check_cf";
414
+ this.renderWizBody('<p class="loading">Checking cloudflared…</p>', [], []);
415
+
416
+ const r = await apiFetch("/tunnel/setup/check-cloudflared");
417
+
418
+ if (r?.installed) {
419
+ this.wizShowCfLogin();
420
+ } else {
421
+ this.renderWizBody(`
422
+ <div class="wiz-desc">cloudflared is required to create and run Cloudflare tunnels. It is not currently installed.</div>
423
+ <div style="display:flex;gap:12px;margin-top:12px;flex-wrap:wrap">
424
+ <a class="btn-wiz-secondary" href="https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/" target="_blank" style="text-decoration:none">↗ Download cloudflared</a>
425
+ <span style="color:#475569;font-size:.8rem;align-self:center">or: <code style="color:#60a5fa">winget install Cloudflare.cloudflared</code></span>
426
+ </div>
427
+ <div class="wiz-desc" style="margin-top:12px;font-size:.78rem;color:#475569">After installing, click "Check Again".</div>
428
+ `, [], [
429
+ `<button class="btn-wiz-primary" data-action="wizShowInstallCheck">Check Again</button>`,
430
+ `<button class="btn-wiz-secondary" data-action="closeSetupWizard">Postpone</button>`
431
+ ]);
432
+ }
433
+ }
434
+
435
+ // Step: cloudflared login (Cloudflare auth via browser)
436
+ wizShowCfLogin() {
437
+ this.wizStep = "check_cf";
438
+ this.renderWizBody(`
439
+ <div class="wiz-desc">Authenticate cloudflared with your Cloudflare account. This opens a browser window.</div>
440
+ <div class="wiz-log" id="wiz-login-log" style="display:none"></div>
441
+ `, [], [
442
+ `<button class="btn-wiz-primary" id="wiz-login-btn" data-action="wizRunCfLogin">Open Browser to Authenticate</button>`,
443
+ `<button class="btn-wiz-secondary" data-action="closeSetupWizard">Postpone</button>`
444
+ ]);
445
+ }
446
+
447
+ async wizRunCfLogin() {
448
+ const btn = this.$("#wiz-login-btn");
449
+ const log = this.$("#wiz-login-log");
450
+ btn.disabled = true; btn.textContent = "Authenticating…";
451
+ log.style.display = "block";
452
+
453
+ try {
454
+ const resp = await fetch(BASE + "/tunnel/setup/cf-login", { method: "POST" });
455
+ const reader = resp.body.getReader();
456
+ const dec = new TextDecoder();
457
+ let buf = "";
458
+ while (true) {
459
+ const { done, value } = await reader.read();
460
+ if (done) break;
461
+ buf += dec.decode(value, { stream: true });
462
+ const lines = buf.split("\n");
463
+ buf = lines.pop();
464
+ for (const line of lines) {
465
+ if (!line.startsWith("data:")) continue;
466
+ try {
467
+ const d = JSON.parse(line.slice(5).trim());
468
+ if (d.line !== undefined) { log.textContent += d.line + "\n"; log.scrollTop = log.scrollHeight; }
469
+ if (d.done) {
470
+ if (d.code === 0) this.wizShowCreateTunnel();
471
+ else { btn.disabled = false; btn.textContent = "Retry"; }
472
+ return;
473
+ }
474
+ } catch {}
475
+ }
476
+ }
477
+ } catch (e) {
478
+ log.textContent += "Error: " + e.message;
479
+ btn.disabled = false; btn.textContent = "Retry";
480
+ }
481
+ }
482
+
483
+ // Step: Create new tunnel
484
+ wizShowCreateTunnel() {
485
+ this.wizStep = "check_cf";
486
+ this.renderWizBody(`
487
+ <div class="wiz-desc">Create a new named Cloudflare tunnel. Give it a name and a public hostname.</div>
488
+ <div class="wiz-label">Tunnel name (e.g. clauth)</div>
489
+ <input class="wiz-input" id="wiz-tname" type="text" value="clauth" spellcheck="false">
490
+ <div class="wiz-label" style="margin-top:10px">Public hostname (e.g. clauth.yourdomain.com)</div>
491
+ <input class="wiz-input" id="wiz-thostname" type="text" placeholder="clauth.yourdomain.com" spellcheck="false">
492
+ <div class="wiz-log" id="wiz-create-log" style="display:none;margin-top:10px"></div>
493
+ <div class="wiz-msg fail" id="wiz-create-err" style="display:none;margin-top:8px"></div>
494
+ `, [], [
495
+ `<button class="btn-wiz-primary" id="wiz-create-btn" data-action="wizRunCreateTunnel">Create Tunnel</button>`,
496
+ `<button class="btn-wiz-secondary" data-action="closeSetupWizard">Postpone</button>`
497
+ ]);
498
+ }
499
+
500
+ async wizRunCreateTunnel() {
501
+ const name = this.$("#wiz-tname")?.value.trim();
502
+ const hostname = this.$("#wiz-thostname")?.value.trim();
503
+ const btn = this.$("#wiz-create-btn");
504
+ const log = this.$("#wiz-create-log");
505
+ const err = this.$("#wiz-create-err");
506
+
507
+ if (!name || !hostname) { if (err) { err.textContent = "Name and hostname required"; err.style.display = "block"; } return; }
508
+
509
+ btn.disabled = true; btn.textContent = "Creating…";
510
+ log.style.display = "block"; err.style.display = "none";
511
+
512
+ try {
513
+ const resp = await fetch(BASE + "/tunnel/setup/cf-create", {
514
+ method: "POST",
515
+ headers: { "Content-Type": "application/json" },
516
+ body: JSON.stringify({ name, hostname }),
517
+ });
518
+ const reader = resp.body.getReader();
519
+ const dec = new TextDecoder();
520
+ let buf = "";
521
+ while (true) {
522
+ const { done, value } = await reader.read();
523
+ if (done) break;
524
+ buf += dec.decode(value, { stream: true });
525
+ const lines = buf.split("\n");
526
+ buf = lines.pop();
527
+ for (const line of lines) {
528
+ if (!line.startsWith("data:")) continue;
529
+ try {
530
+ const d = JSON.parse(line.slice(5).trim());
531
+ if (d.line !== undefined) { log.textContent += d.line + "\n"; log.scrollTop = log.scrollHeight; }
532
+ if (d.done && d.hostname) {
533
+ await apiFetch("/tunnel/start", { method: "POST" });
534
+ this.wizShowMcpSetup(d.hostname);
535
+ return;
536
+ }
537
+ if (d.error) { err.textContent = d.error; err.style.display = "block"; btn.disabled = false; btn.textContent = "Retry"; return; }
538
+ } catch {}
539
+ }
540
+ }
541
+ } catch (e) {
542
+ if (err) { err.textContent = e.message; err.style.display = "block"; }
543
+ btn.disabled = false; btn.textContent = "Retry";
544
+ }
545
+ }
546
+
547
+ // Step: MCP setup in claude.ai
548
+ wizShowMcpSetup(hostname) {
549
+ this.wizStep = "mcp_setup";
550
+ const base = `https://${hostname}`;
551
+
552
+ this.renderWizBody(`
553
+ <div class="wiz-desc">Tunnel is live. Add connectors in claude.ai → Integrations.</div>
554
+ <div style="display:flex;flex-direction:column;gap:8px;margin-top:10px">
555
+ <div class="mcp-row">
556
+ <span class="mcp-label">Clauth</span>
557
+ <span class="mcp-val" id="wiz-mcp-url">${base}/clauth</span>
558
+ <button class="mcp-copy" data-action="wizCopy" data-copy-target="wiz-mcp-url">copy</button>
559
+ </div>
560
+ <div class="mcp-row">
561
+ <span class="mcp-label">GWS</span>
562
+ <span class="mcp-val" id="wiz-mcp-gws">${base}/gws</span>
563
+ <button class="mcp-copy" data-action="wizCopy" data-copy-target="wiz-mcp-gws">copy</button>
564
+ </div>
565
+ <div class="mcp-row">
566
+ <span class="mcp-label">FS</span>
567
+ <span class="mcp-val" id="wiz-mcp-fs">${base}/fs</span>
568
+ <button class="mcp-copy" data-action="wizCopy" data-copy-target="wiz-mcp-fs">copy</button>
569
+ </div>
570
+ </div>
571
+ <div style="margin-top:10px;font-size:.78rem;color:#64748b">
572
+ MCP endpoints for direct servers (Regen Media, Web Research) are stored as <code>mcp-*</code> services in the vault.
573
+ </div>
574
+ `, [], [
575
+ `<button class="btn-wiz-primary" data-action="wizOpenClaudeAndTest">I've Added It — Test Now</button>`,
576
+ `<button class="btn-wiz-secondary" data-action="wizShowTest">Skip Test</button>`,
577
+ `<button class="btn-wiz-secondary" data-action="closeSetupWizard">Done</button>`
578
+ ]);
579
+ }
580
+
581
+ wizOpenClaudeAndTest() {
582
+ window.open("https://claude.ai/settings/integrations", "_blank");
583
+ this.wizShowTest();
584
+ }
585
+
586
+ async wizRollCreds() {
587
+ if (!confirm("Invalidate all OAuth tokens? Claude.ai connectors will need to re-authenticate.")) return;
588
+ try {
589
+ const resp = await apiFetch("/roll-mcp-creds", { method: "POST" });
590
+ if (resp?.tokens_invalidated) {
591
+ alert("All OAuth clients and tokens invalidated. Claude.ai will re-register automatically on next connection.");
592
+ }
593
+ } catch (e) { alert("Failed to roll credentials: " + e.message); }
594
+ }
595
+
596
+ wizCopy(e, btn) {
597
+ const elId = btn.dataset.copyTarget;
598
+ const val = this.$("#" + elId)?.textContent?.trim();
599
+ if (!val) return;
600
+ navigator.clipboard.writeText(val).then(() => {
601
+ const orig = btn.textContent;
602
+ btn.textContent = "✓"; btn.classList.add("ok");
603
+ setTimeout(() => { btn.textContent = orig; btn.classList.remove("ok"); }, 1500);
604
+ }).catch(() => {});
605
+ }
606
+
607
+ // Step: Test — verify MCP is working
608
+ wizShowTest() {
609
+ this.wizStep = "test";
610
+ this.renderWizBody(`
611
+ <div class="wiz-desc">In a new claude.ai chat, ask Claude to run this MCP tool call to verify the connection:</div>
612
+ <div style="background:#030712;border:1px solid #1e293b;border-radius:6px;padding:12px;margin:10px 0;font-family:'Courier New',monospace;font-size:.82rem;color:#6ee7b7;user-select:all">
613
+ Use the clauth MCP tool: GET /ping — what is the response?
614
+ </div>
615
+ <div class="wiz-desc">Claude should report back: <code style="color:#4ade80">{"status":"ok","version":"..."}</code></div>
616
+ <div class="wiz-test-result" id="wiz-test-result"></div>
617
+ `, [], [
618
+ `<button class="btn-wiz-primary" data-action="wizMarkTestDone">✓ It Worked — Done</button>`,
619
+ `<button class="btn-wiz-secondary" data-action="closeSetupWizard">Close</button>`
620
+ ]);
621
+ }
622
+
623
+ wizMarkTestDone() {
624
+ const r = this.$("#wiz-test-result");
625
+ r.className = "wiz-test-result ok"; r.style.display = "block";
626
+ r.textContent = "✓ Tunnel and MCP are working. claude.ai can now access your vault.";
627
+ this.$("#wizard-foot").innerHTML = `<button class="btn-wiz-primary" data-action="closeSetupWizard">Close Setup</button>`;
628
+ }
629
+
630
+ get actions() {
631
+ return {
632
+ runTunnelSetup: () => this.openSetupWizard(),
633
+ startTunnel: () => this.toggleTunnel("start"),
634
+ stopTunnel: () => this.toggleTunnel("stop"),
635
+ testTunnel: () => this.testTunnel(),
636
+ openClaude: () => this.openClaude(),
637
+ closeSetupWizard: () => this.closeSetupWizard(),
638
+ onCfTokenKeydown: (e) => this.onCfTokenKeydown(e),
639
+ // Dispatches through window.wizSubmitCfToken (the bridge below), NOT
640
+ // this.wizSubmitCfToken() directly -- installWriteAccessGuards()
641
+ // wraps the GLOBAL name with the write-token acquire-and-retry gate.
642
+ // Calling the instance method directly would silently bypass that
643
+ // gate, restoring the exact write-lock defect this mechanism exists
644
+ // to prevent.
645
+ wizSubmitCfToken: () => window.wizSubmitCfToken(),
646
+ wizSelectTunnel: (e, el) => this.wizSelectTunnel(e, el),
647
+ wizSaveTunnel: () => this.wizSaveTunnel(),
648
+ wizShowInstallCheck: () => this.wizShowInstallCheck(),
649
+ wizRunCreateViaCfApi: () => this.wizRunCreateViaCfApi(),
650
+ wizRunCfLogin: () => this.wizRunCfLogin(),
651
+ wizRunCreateTunnel: () => this.wizRunCreateTunnel(),
652
+ wizOpenClaudeAndTest: () => this.wizOpenClaudeAndTest(),
653
+ wizRollCreds: () => this.wizRollCreds(),
654
+ wizCopy: (e, btn) => this.wizCopy(e, btn),
655
+ wizShowTest: () => this.wizShowTest(),
656
+ wizMarkTestDone: () => this.wizMarkTestDone(),
657
+ };
658
+ }
659
+ }
660
+ customElements.define(ClauthTunnelPanel.tagName, ClauthTunnelPanel);
661
+
662
+ // Write-guard bridge: installWriteAccessGuards() (dashboard.js) wraps every
663
+ // name in WRITE_ACTIONS by reassigning window[name] to a guarded version --
664
+ // it can only do that for a real global function, not a class instance
665
+ // method. This thin global delegates to the real implementation on the live
666
+ // panel instance; installWriteAccessGuards wraps THIS name, and the
667
+ // component's own action dispatcher calls window.wizSubmitCfToken() (see
668
+ // the `actions` getter above), never this.wizSubmitCfToken() directly, so
669
+ // the guard is never bypassed.
670
+ window.wizSubmitCfToken = () => {
671
+ const el = document.querySelector(ClauthTunnelPanel.tagName);
672
+ return el?.wizSubmitCfToken();
673
+ };
674
+
675
+ // Panel interface registration — see ./panel-registry.js. mount() runs
676
+ // connectedCallback() only builds the shadow DOM (safe -- no external
677
+ // dependency); it deliberately does NOT call mount() itself, since it fires
678
+ // during HTML parsing, before dashboard.js (and apiFetch) has loaded. The
679
+ // registry's mountAll() runs after every script has loaded, so THIS is the
680
+ // first safe point to call the real mount() -- which initializes instance
681
+ // state and starts polling.
682
+ window.ClauthPanelRegistry.register({
683
+ id: "tunnel",
684
+ mount: () => {
685
+ const el = document.querySelector(ClauthTunnelPanel.tagName);
686
+ if (el) el.mount();
687
+ },
688
+ });