Spaces:
Running
Running
Env Builder: add Quick Guide and 'Required' selector; auth cookie/caching and password gating fixes
Browse files- README.md +1 -1
- env-builder.html +23 -0
- env-builder.js +7 -1
- health-server.js +8 -8
- start.sh +1 -1
README.md
CHANGED
|
@@ -241,7 +241,7 @@ Configure password access and network restrictions:
|
|
| 241 |
|
| 242 |
| Variable | Default | Description |
|
| 243 |
| :--- | :--- | :--- |
|
| 244 |
-
| `OPENCLAW_PASSWORD` | — | Enable simple password auth instead of token |
|
| 245 |
| `TRUSTED_PROXIES` | — | Comma-separated IPs of HF proxies |
|
| 246 |
| `ALLOWED_ORIGINS` | — | Comma-separated allowed origins for Control UI |
|
| 247 |
| `CLOUDFLARE_KEEPALIVE_ENABLED` | `true` | Set to `false` to disable the automatic Cloudflare KeepAlive worker |
|
|
|
|
| 241 |
|
| 242 |
| Variable | Default | Description |
|
| 243 |
| :--- | :--- | :--- |
|
| 244 |
+
| `OPENCLAW_PASSWORD` | — | Enable simple password auth instead of token (applies only when `GATEWAY_TOKEN` is empty) |
|
| 245 |
| `TRUSTED_PROXIES` | — | Comma-separated IPs of HF proxies |
|
| 246 |
| `ALLOWED_ORIGINS` | — | Comma-separated allowed origins for Control UI |
|
| 247 |
| `CLOUDFLARE_KEEPALIVE_ENABLED` | `true` | Set to `false` to disable the automatic Cloudflare KeepAlive worker |
|
env-builder.html
CHANGED
|
@@ -734,6 +734,10 @@ body {
|
|
| 734 |
::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 4px; }
|
| 735 |
|
| 736 |
/* ── Responsive ── */
|
|
|
|
|
|
|
|
|
|
|
|
|
| 737 |
@media (max-width: 900px) {
|
| 738 |
:root { --panel-w: 280px; --sidebar-w: 180px; }
|
| 739 |
}
|
|
@@ -775,6 +779,9 @@ body {
|
|
| 775 |
flex-shrink: 0;
|
| 776 |
}
|
| 777 |
.tag-legend[open] .legend-hint { opacity: 0; }
|
|
|
|
|
|
|
|
|
|
| 778 |
.legend-body {
|
| 779 |
padding: 8px 12px 10px;
|
| 780 |
border-top: 1px solid var(--border);
|
|
@@ -834,6 +841,7 @@ body {
|
|
| 834 |
|
| 835 |
<!-- toolbar -->
|
| 836 |
<div class="toolbar">
|
|
|
|
| 837 |
<div class="search-wrap">
|
| 838 |
<span class="search-icon">⌕</span>
|
| 839 |
<input id="search" type="text" placeholder="Search variables…" autocomplete="off" spellcheck="false">
|
|
@@ -841,6 +849,7 @@ body {
|
|
| 841 |
|
| 842 |
<div class="tb-sep"></div>
|
| 843 |
|
|
|
|
| 844 |
<button id="selectCommon" class="btn">★ Common</button>
|
| 845 |
<button id="selectVisible" class="btn">☑ Visible</button>
|
| 846 |
<button id="clearAll" class="btn btn-ghost">✕ Clear</button>
|
|
@@ -893,6 +902,20 @@ body {
|
|
| 893 |
<div class="panel-scroll">
|
| 894 |
|
| 895 |
<!-- Summary -->
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 896 |
<div class="pblock">
|
| 897 |
<div class="pblock-head">
|
| 898 |
<span class="pblock-title">📊 Summary</span>
|
|
|
|
| 734 |
::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 4px; }
|
| 735 |
|
| 736 |
/* ── Responsive ── */
|
| 737 |
+
|
| 738 |
+
@media (max-width: 1100px) {
|
| 739 |
+
.toolbar-hint { display: none; }
|
| 740 |
+
}
|
| 741 |
@media (max-width: 900px) {
|
| 742 |
:root { --panel-w: 280px; --sidebar-w: 180px; }
|
| 743 |
}
|
|
|
|
| 779 |
flex-shrink: 0;
|
| 780 |
}
|
| 781 |
.tag-legend[open] .legend-hint { opacity: 0; }
|
| 782 |
+
.toolbar-hint { color: var(--muted); font-size: 12px; margin-right: 10px; white-space: nowrap; }
|
| 783 |
+
.quick-guide { margin: 0; padding-left: 18px; color: var(--muted); display: grid; gap: 6px; font-size: 12.5px; }
|
| 784 |
+
.quick-guide strong { color: var(--text); }
|
| 785 |
.legend-body {
|
| 786 |
padding: 8px 12px 10px;
|
| 787 |
border-top: 1px solid var(--border);
|
|
|
|
| 841 |
|
| 842 |
<!-- toolbar -->
|
| 843 |
<div class="toolbar">
|
| 844 |
+
<div class="toolbar-hint">Tip: Start with <strong>⚡ Required</strong>, then fill keys and click <strong># Generate Bundle</strong>.</div>
|
| 845 |
<div class="search-wrap">
|
| 846 |
<span class="search-icon">⌕</span>
|
| 847 |
<input id="search" type="text" placeholder="Search variables…" autocomplete="off" spellcheck="false">
|
|
|
|
| 849 |
|
| 850 |
<div class="tb-sep"></div>
|
| 851 |
|
| 852 |
+
<button id="selectRequired" class="btn" title="Select all critical variables first">⚡ Required</button>
|
| 853 |
<button id="selectCommon" class="btn">★ Common</button>
|
| 854 |
<button id="selectVisible" class="btn">☑ Visible</button>
|
| 855 |
<button id="clearAll" class="btn btn-ghost">✕ Clear</button>
|
|
|
|
| 902 |
<div class="panel-scroll">
|
| 903 |
|
| 904 |
<!-- Summary -->
|
| 905 |
+
<div class="pblock">
|
| 906 |
+
<div class="pblock-head">
|
| 907 |
+
<span class="pblock-title">🧭 Quick Guide</span>
|
| 908 |
+
</div>
|
| 909 |
+
<div class="pblock-body">
|
| 910 |
+
<ol class="quick-guide">
|
| 911 |
+
<li>Click <strong>⚡ Required</strong> to select must-have vars.</li>
|
| 912 |
+
<li>Fill values in selected cards (search works by key/tag/group).</li>
|
| 913 |
+
<li>Click <strong># Generate Bundle</strong> and copy Bundle/Env line.</li>
|
| 914 |
+
<li>Paste into HF Space variables or use <strong>↓ Import & Apply</strong>.</li>
|
| 915 |
+
</ol>
|
| 916 |
+
</div>
|
| 917 |
+
</div>
|
| 918 |
+
|
| 919 |
<div class="pblock">
|
| 920 |
<div class="pblock-head">
|
| 921 |
<span class="pblock-title">📊 Summary</span>
|
env-builder.js
CHANGED
|
@@ -2115,7 +2115,7 @@ function cardHTML(f, origIdx = 0) {
|
|
| 2115 |
const tm = TAG_META[f.tag] || TAG_META.optional;
|
| 2116 |
const badge = `<span class="badge ${tm.cls}">${tm.lbl}</span>`;
|
| 2117 |
|
| 2118 |
-
return `<div class="env-card" data-row data-orig-idx="${origIdx}" data-group="${esc(f.g)}" data-search="${esc((f.g + ' ' + f.k + ' ' + (f.lbl || '') + ' ' + (f.tag || '')).toLowerCase())}">
|
| 2119 |
<div class="card-top">
|
| 2120 |
<input type="checkbox" class="card-check" data-check="${esc(f.k)}" ${f.common ? 'data-common="1"' : ''}>
|
| 2121 |
<div class="card-info">
|
|
@@ -2465,6 +2465,12 @@ refresh();
|
|
| 2465 |
|
| 2466 |
// ── Events ──
|
| 2467 |
$('search').oninput = filter;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2468 |
$('selectCommon').onclick = () => {
|
| 2469 |
document.querySelectorAll('[data-common="1"]').forEach(c => c.checked = true);
|
| 2470 |
sortAllSections();
|
|
|
|
| 2115 |
const tm = TAG_META[f.tag] || TAG_META.optional;
|
| 2116 |
const badge = `<span class="badge ${tm.cls}">${tm.lbl}</span>`;
|
| 2117 |
|
| 2118 |
+
return `<div class="env-card" data-row data-orig-idx="${origIdx}" data-group="${esc(f.g)}" data-search="${esc((f.g + ' ' + f.k + ' ' + (f.lbl || '') + ' ' + (f.tag || '')).toLowerCase())}" data-tag="${esc(f.tag || 'optional')}">
|
| 2119 |
<div class="card-top">
|
| 2120 |
<input type="checkbox" class="card-check" data-check="${esc(f.k)}" ${f.common ? 'data-common="1"' : ''}>
|
| 2121 |
<div class="card-info">
|
|
|
|
| 2465 |
|
| 2466 |
// ── Events ──
|
| 2467 |
$('search').oninput = filter;
|
| 2468 |
+
$('selectRequired').onclick = () => {
|
| 2469 |
+
document.querySelectorAll('[data-row][data-tag="critical"] [data-check]').forEach(c => c.checked = true);
|
| 2470 |
+
sortAllSections();
|
| 2471 |
+
markSelected();
|
| 2472 |
+
refresh();
|
| 2473 |
+
};
|
| 2474 |
$('selectCommon').onclick = () => {
|
| 2475 |
document.querySelectorAll('[data-common="1"]').forEach(c => c.checked = true);
|
| 2476 |
sortAllSections();
|
health-server.js
CHANGED
|
@@ -664,11 +664,11 @@ const server = http.createServer(async (req, res) => {
|
|
| 664 |
const body = await readBody(req);
|
| 665 |
const token = decodeURIComponent((body.match(/(?:^|&)token=([^&]*)/) || [])[1] || "").replace(/\+/g, " ");
|
| 666 |
if (safeEqual(token, GATEWAY_TOKEN)) {
|
| 667 |
-
const cookie = `hc_env_auth=${encodeURIComponent(GATEWAY_TOKEN)}; Path=/
|
| 668 |
res.writeHead(302, { Location: "/env-builder", "Set-Cookie": cookie, "Cache-Control": "no-store" });
|
| 669 |
return res.end();
|
| 670 |
}
|
| 671 |
-
res.writeHead(200, { "Content-Type": "text/html" });
|
| 672 |
return res.end(renderEnvBuilderLogin(true));
|
| 673 |
}
|
| 674 |
res.writeHead(302, { Location: "/env-builder", "Cache-Control": "no-store" });
|
|
@@ -676,31 +676,31 @@ const server = http.createServer(async (req, res) => {
|
|
| 676 |
}
|
| 677 |
|
| 678 |
if (pathname === "/env-builder/logout") {
|
| 679 |
-
res.writeHead(302, { Location: "/env-builder", "Set-Cookie": "hc_env_auth=; Path=/
|
| 680 |
return res.end();
|
| 681 |
}
|
| 682 |
|
| 683 |
if (pathname === "/env-builder" || pathname === "/env-builder/") {
|
| 684 |
if (isDirectHfSpaceRequest) {
|
| 685 |
-
res.writeHead(200, { "Content-Type": "text/html" });
|
| 686 |
return res.end(renderPrivateRedirect(HF_SPACE_URL));
|
| 687 |
}
|
| 688 |
if (!isEnvBuilderAuthed(req)) {
|
| 689 |
-
res.writeHead(200, { "Content-Type": "text/html" });
|
| 690 |
return res.end(renderEnvBuilderLogin(false));
|
| 691 |
}
|
| 692 |
-
res.writeHead(200, { "Content-Type": "text/html" });
|
| 693 |
return res.end(renderEnvBuilder());
|
| 694 |
}
|
| 695 |
|
| 696 |
if (pathname === "/env-builder.js") {
|
| 697 |
if (!isEnvBuilderAuthed(req)) {
|
| 698 |
-
res.writeHead(401, { "Content-Type": "text/plain" });
|
| 699 |
return res.end("Unauthorized");
|
| 700 |
}
|
| 701 |
try {
|
| 702 |
const js = fs.readFileSync(require("path").join(__dirname, "env-builder.js"), "utf8");
|
| 703 |
-
res.writeHead(200, { "Content-Type": "application/javascript" });
|
| 704 |
return res.end(js);
|
| 705 |
} catch (exc) {
|
| 706 |
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
|
|
| 664 |
const body = await readBody(req);
|
| 665 |
const token = decodeURIComponent((body.match(/(?:^|&)token=([^&]*)/) || [])[1] || "").replace(/\+/g, " ");
|
| 666 |
if (safeEqual(token, GATEWAY_TOKEN)) {
|
| 667 |
+
const cookie = `hc_env_auth=${encodeURIComponent(GATEWAY_TOKEN)}; Path=/; HttpOnly; SameSite=Strict; Max-Age=86400`;
|
| 668 |
res.writeHead(302, { Location: "/env-builder", "Set-Cookie": cookie, "Cache-Control": "no-store" });
|
| 669 |
return res.end();
|
| 670 |
}
|
| 671 |
+
res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" });
|
| 672 |
return res.end(renderEnvBuilderLogin(true));
|
| 673 |
}
|
| 674 |
res.writeHead(302, { Location: "/env-builder", "Cache-Control": "no-store" });
|
|
|
|
| 676 |
}
|
| 677 |
|
| 678 |
if (pathname === "/env-builder/logout") {
|
| 679 |
+
res.writeHead(302, { Location: "/env-builder", "Set-Cookie": "hc_env_auth=; Path=/; HttpOnly; Max-Age=0", "Cache-Control": "no-store" });
|
| 680 |
return res.end();
|
| 681 |
}
|
| 682 |
|
| 683 |
if (pathname === "/env-builder" || pathname === "/env-builder/") {
|
| 684 |
if (isDirectHfSpaceRequest) {
|
| 685 |
+
res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" });
|
| 686 |
return res.end(renderPrivateRedirect(HF_SPACE_URL));
|
| 687 |
}
|
| 688 |
if (!isEnvBuilderAuthed(req)) {
|
| 689 |
+
res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" });
|
| 690 |
return res.end(renderEnvBuilderLogin(false));
|
| 691 |
}
|
| 692 |
+
res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" });
|
| 693 |
return res.end(renderEnvBuilder());
|
| 694 |
}
|
| 695 |
|
| 696 |
if (pathname === "/env-builder.js") {
|
| 697 |
if (!isEnvBuilderAuthed(req)) {
|
| 698 |
+
res.writeHead(401, { "Content-Type": "text/plain", "Cache-Control": "no-store", "Vary": "Cookie" });
|
| 699 |
return res.end("Unauthorized");
|
| 700 |
}
|
| 701 |
try {
|
| 702 |
const js = fs.readFileSync(require("path").join(__dirname, "env-builder.js"), "utf8");
|
| 703 |
+
res.writeHead(200, { "Content-Type": "application/javascript", "Cache-Control": "no-store", "Vary": "Cookie" });
|
| 704 |
return res.end(js);
|
| 705 |
} catch (exc) {
|
| 706 |
res.writeHead(404, { "Content-Type": "text/plain" });
|
start.sh
CHANGED
|
@@ -676,7 +676,7 @@ CONFIG_JSON=$(jq \
|
|
| 676 |
| (if $spaceHost != "" then
|
| 677 |
.gateway.controlUi.allowedOrigins = ["https://" + $spaceHost]
|
| 678 |
else . end)
|
| 679 |
-
| (if $password != "" then
|
| 680 |
.gateway.auth.mode = "password" | .gateway.auth.password = $password
|
| 681 |
else . end)' <<<"$CONFIG_JSON")
|
| 682 |
|
|
|
|
| 676 |
| (if $spaceHost != "" then
|
| 677 |
.gateway.controlUi.allowedOrigins = ["https://" + $spaceHost]
|
| 678 |
else . end)
|
| 679 |
+
| (if ($password != "" and (.gateway.auth.token // "") == "") then
|
| 680 |
.gateway.auth.mode = "password" | .gateway.auth.password = $password
|
| 681 |
else . end)' <<<"$CONFIG_JSON")
|
| 682 |
|