feat(tunnel,ui): purple tunnel button + opt-in unauthenticated tunnel with warning (v1.1.9)

- Daylight Blue: Cloudflare Tunnel welcome button is now purple (was orange),
  keeping Claude blue / Tunnel purple / OpenCode green distinct.
- Allow enabling the Cloudflare tunnel with no CODEMAN_PASSWORD via the UI: the
  toggle now pops a security confirm dialog and, on confirm, sends an explicit
  per-request acknowledgeUnauthTunnel:true (new action field, never persisted).
  Server logs a loud warning whenever a passwordless public tunnel starts.
  curl/API/CLI stay refused unless password/env/flag — no accidental exposure.

Tests: extend test/routes/system-routes-tunnel-guard.test.ts (ack allows + not
persisted; ack:false still refuses). Verified e2e on an isolated instance
(purple button, confirm dialog, retry carries the flag, no real tunnel opened).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-06-16 18:44:03 +02:00
parent f7814ad364
commit a0ac10a07c
9 changed files with 133 additions and 35 deletions
+42
View File
@@ -1176,6 +1176,48 @@ Object.assign(CodemanApp.prototype, {
} catch {
/* non-JSON body — use the default message */
}
// 403 = the no-password safety refusal (COD-55). Warn loudly and let the
// operator acknowledge the risk; on confirm, retry with explicit acknowledgment.
if (res.status === 403) {
const confirmed = confirm(
'⚠️ SECURITY WARNING — no password set\n\n' +
'Enabling the Cloudflare tunnel will publish THIS machine to a public URL with ' +
'NO login. Anyone who gets the URL has full terminal control — effectively remote ' +
'code execution on your computer.\n\n' +
'Strongly recommended: set CODEMAN_PASSWORD instead.\n\n' +
'Enable the unauthenticated public tunnel anyway?'
);
if (!confirmed) {
this._dismissTunnelConnecting?.();
this.showToast('Tunnel not enabled', 'info');
return true;
}
try {
const retry = await fetch('/api/settings', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tunnelEnabled: true, acknowledgeUnauthTunnel: true }),
});
if (retry.ok) {
this.showToast('Public tunnel enabling — no password set ⚠️', 'warning');
return false; // proceed with the caller's success/connecting path
}
let m = 'Failed to enable tunnel.';
try {
const b = await retry.json();
if (b && b.error) m = b.error;
} catch {
/* non-JSON */
}
this._dismissTunnelConnecting?.();
this.showToast(m, 'error');
return true;
} catch {
this._dismissTunnelConnecting?.();
this.showToast('Failed to enable tunnel', 'error');
return true;
}
}
this._dismissTunnelConnecting?.();
this.showToast(message, 'error');
return true;
+15 -15
View File
@@ -10201,10 +10201,10 @@ html:not([data-skin="og"]) {
}
/* ---- Daylight Blue: distinct identity per welcome action button ----
Run Claude Code keeps the blue accent; Cloudflare Tunnel takes Cloudflare's
brand orange; Run OpenCode takes an emerald green — so the three are clearly
different colors instead of all reading blue. Scoped to daylight-blue only;
placed after the shared daylight block to win on equal specificity. ---- */
Run Claude Code keeps the blue accent; Cloudflare Tunnel is purple; Run
OpenCode is emerald green — so the three are clearly different colors instead
of all reading blue. Scoped to daylight-blue only; placed after the shared
daylight block to win on equal specificity. ---- */
html[data-skin="daylight-blue"] .welcome-btn-opencode {
background: linear-gradient(135deg, #0d9f6e, #2fbf85);
border-color: rgba(16, 185, 129, 0.5);
@@ -10216,21 +10216,21 @@ html[data-skin="daylight-blue"] .welcome-btn-opencode:hover {
box-shadow: 0 0 28px -4px rgba(16, 185, 129, 0.4);
}
html[data-skin="daylight-blue"] .welcome-btn-tunnel {
background: linear-gradient(135deg, #ef7611, #fbad41);
border-color: rgba(246, 130, 31, 0.5);
color: #2e1503;
background: linear-gradient(135deg, #7c3aed, #a855f7);
border-color: rgba(168, 85, 247, 0.5);
color: #f5f3ff;
}
html[data-skin="daylight-blue"] .welcome-btn-tunnel:hover {
background: linear-gradient(135deg, #fb8b2a, #ffc05a);
border-color: rgba(251, 173, 65, 0.6);
box-shadow: 0 0 28px -4px rgba(246, 130, 31, 0.45);
background: linear-gradient(135deg, #8b5cf6, #c084fc);
border-color: rgba(192, 132, 252, 0.6);
box-shadow: 0 0 28px -4px rgba(124, 58, 237, 0.45);
}
html[data-skin="daylight-blue"] .welcome-btn-tunnel.active {
background: linear-gradient(135deg, #d9650b, #f6821f);
border-color: rgba(246, 130, 31, 0.6);
color: #fff7ed;
background: linear-gradient(135deg, #6d28d9, #7c3aed);
border-color: rgba(124, 58, 237, 0.6);
color: #f5f3ff;
}
html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
background: linear-gradient(135deg, #ef7611, #fb8b2a);
box-shadow: 0 0 28px -4px rgba(246, 130, 31, 0.5);
background: linear-gradient(135deg, #7c3aed, #8b5cf6);
box-shadow: 0 0 28px -4px rgba(124, 58, 237, 0.5);
}