@lifeaitools/clauth 1.30.18 → 1.30.20
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/cli/commands/serve.js +101 -41
- package/cli/supervisor-registry.js +589 -589
- package/cli/supervisor-registry.test.js +397 -397
- package/cli/supervisor-ui.test.js +83 -5
- package/package.json +2 -2
- package/scripts/bin/bootstrap-linux +0 -0
- package/scripts/bin/bootstrap-macos +0 -0
- package/scripts/bin/bootstrap-win.exe +0 -0
package/cli/commands/serve.js
CHANGED
|
@@ -41,9 +41,9 @@ import {
|
|
|
41
41
|
listTunnels,
|
|
42
42
|
readSupervisorEvents,
|
|
43
43
|
addTunnelRoute,
|
|
44
|
-
removeTunnelRoute,
|
|
45
|
-
reconcileSurfaceHealth,
|
|
46
|
-
runPluginAction,
|
|
44
|
+
removeTunnelRoute,
|
|
45
|
+
reconcileSurfaceHealth,
|
|
46
|
+
runPluginAction,
|
|
47
47
|
runSurfaceAction,
|
|
48
48
|
setPluginEnabled,
|
|
49
49
|
supervisorHealth,
|
|
@@ -486,7 +486,7 @@ const LOG_FILE = path.join(os.tmpdir(), "clauth-serve.log");
|
|
|
486
486
|
const LIVE_PORT = 52437;
|
|
487
487
|
const STAGED_PORT = 52438;
|
|
488
488
|
const WRITE_TOKEN_BYTES = 32;
|
|
489
|
-
const WRITE_TOKEN_TTL_MS =
|
|
489
|
+
const WRITE_TOKEN_TTL_MS = 30 * 60 * 1000;
|
|
490
490
|
|
|
491
491
|
function makeWriteToken() {
|
|
492
492
|
return {
|
|
@@ -677,6 +677,9 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
|
|
|
677
677
|
.btn-unlock:hover{background:#2563eb}
|
|
678
678
|
.btn-unlock:disabled{background:#1e3a5f;color:#4a6fa5;cursor:not-allowed}
|
|
679
679
|
.lock-err{color:#f87171;font-size:.82rem;margin-top:.75rem;min-height:1.2em}
|
|
680
|
+
#write-unlock-overlay{display:none;position:fixed;inset:0;background:rgba(2,6,23,.72);z-index:9999;align-items:center;justify-content:center;padding:2rem}
|
|
681
|
+
.write-unlock-cancel{width:100%;background:transparent;color:#94a3b8;border:1px solid #334155;border-radius:8px;padding:8px;font-size:.85rem;cursor:pointer;margin-top:.6rem;transition:border-color .15s}
|
|
682
|
+
.write-unlock-cancel:hover{border-color:#64748b;color:#cbd5e1}
|
|
680
683
|
/* ── Main view ── */
|
|
681
684
|
#main-view{display:none;padding:2rem}
|
|
682
685
|
.header{display:flex;align-items:center;gap:10px;margin-bottom:1.5rem;flex-wrap:wrap}
|
|
@@ -951,6 +954,24 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
|
|
|
951
954
|
</div>
|
|
952
955
|
</div>
|
|
953
956
|
|
|
957
|
+
<!-- Write-unlock modal (replaces window.prompt(), which silently no-ops in
|
|
958
|
+
embedded/webview browser contexts and after a browser suppresses repeated
|
|
959
|
+
native dialogs) -->
|
|
960
|
+
<div id="write-unlock-overlay">
|
|
961
|
+
<div class="lock-card">
|
|
962
|
+
<div class="lock-icon">🔒</div>
|
|
963
|
+
<div class="lock-title">Enable writes</div>
|
|
964
|
+
<div class="lock-sub">Enter your vault password to enable saving changes (30-minute write session)</div>
|
|
965
|
+
<form onsubmit="submitWriteUnlock();return false;" autocomplete="on">
|
|
966
|
+
<input type="text" name="username" value="clauth" autocomplete="username" style="display:none">
|
|
967
|
+
<input class="lock-input" id="write-unlock-input" type="password" placeholder="••••••••••••" autocomplete="current-password">
|
|
968
|
+
<button class="btn-unlock" id="write-unlock-btn" type="submit">Unlock Writes</button>
|
|
969
|
+
</form>
|
|
970
|
+
<div class="lock-err" id="write-unlock-err"></div>
|
|
971
|
+
<button type="button" class="write-unlock-cancel" onclick="closeWriteUnlockModal()">Cancel</button>
|
|
972
|
+
</div>
|
|
973
|
+
</div>
|
|
974
|
+
|
|
954
975
|
<!-- ── Main view (shown after unlock) ──────── -->
|
|
955
976
|
<div id="main-view">
|
|
956
977
|
<div id="upgrade-banner" style="display:none" class="upgrade-banner">
|
|
@@ -1535,7 +1556,7 @@ function renderSupervisorSurface(surface) {
|
|
|
1535
1556
|
return '<div class="supervisor-row"><div class="supervisor-row-top"><span class="supervisor-name">' + htmlEscape(surface.plugin_id + ":" + surface.id) + '</span>' + supervisorBadge(surface.lifecycle_owner || "unknown", ownerKind) + supervisorBadge(surface.state || surface.status || "unknown", stateKind) + '</div>' +
|
|
1536
1557
|
'<div class="supervisor-meta">' + htmlEscape(surface.destination || "—") + ' · port ' + htmlEscape(surface.port || "—") + '</div>' +
|
|
1537
1558
|
'<div class="supervisor-meta">' + htmlEscape(surface.health || surface.health_url || "no health") + '</div>' +
|
|
1538
|
-
'<div class="supervisor-row-actions">' + ["start","stop","restart","reconcile","test","promote","rollback"].map(a => '<button class="supervisor-action" data-supervisor-action="' + a + '" onclick="runSupervisorSurface(' + jsArg(surface.id) + ',' + jsArg(a) + ')">' + a + '</button>').join("") + '</div></div>';
|
|
1559
|
+
'<div class="supervisor-row-actions">' + ["start","stop","restart","reconcile","test","promote","rollback"].map(a => '<button class="supervisor-action" data-supervisor-action="' + a + '" onclick="runSupervisorSurface(' + jsArg(surface.plugin_id + ":" + surface.id) + ',' + jsArg(a) + ')">' + a + '</button>').join("") + '</div></div>';
|
|
1539
1560
|
}
|
|
1540
1561
|
|
|
1541
1562
|
async function rescanSupervisorPlugins() {
|
|
@@ -1641,24 +1662,63 @@ async function lockVault() {
|
|
|
1641
1662
|
showLockScreen(r.hard_locked || false);
|
|
1642
1663
|
}
|
|
1643
1664
|
|
|
1644
|
-
//
|
|
1665
|
+
// -- Unlock writes (re-establish write scope without locking) --
|
|
1645
1666
|
// Needed when the daemon auto-unlocks via --pw/boot.key: the page never sees the
|
|
1646
|
-
// unlock screen, so it holds no write token. POST /auth mints one (
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1667
|
+
// unlock screen, so it holds no write token. POST /auth mints one (30-min TTL).
|
|
1668
|
+
//
|
|
1669
|
+
// Uses an in-page modal rather than window.prompt()/confirm()/alert(): those
|
|
1670
|
+
// native dialogs silently no-op in embedded/webview browser contexts (VS Code
|
|
1671
|
+
// Simple Browser, WebView2, Electron wrappers) and after a real browser
|
|
1672
|
+
// suppresses repeated dialogs for a tab ("prevent this page from creating
|
|
1673
|
+
// additional dialogs") -- both cases fail with zero visible feedback.
|
|
1674
|
+
function unlockWrites() {
|
|
1675
|
+
if (writeToken) {
|
|
1676
|
+
if (!confirm("Writes are already unlocked this session. Re-unlock?")) return;
|
|
1677
|
+
}
|
|
1678
|
+
openWriteUnlockModal();
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
function openWriteUnlockModal() {
|
|
1682
|
+
const overlay = document.getElementById("write-unlock-overlay");
|
|
1683
|
+
const input = document.getElementById("write-unlock-input");
|
|
1684
|
+
const err = document.getElementById("write-unlock-err");
|
|
1685
|
+
if (err) err.textContent = "";
|
|
1686
|
+
if (input) { input.value = ""; input.className = "lock-input"; }
|
|
1687
|
+
if (overlay) overlay.style.display = "flex";
|
|
1688
|
+
if (input) setTimeout(() => input.focus(), 50);
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
function closeWriteUnlockModal() {
|
|
1692
|
+
const overlay = document.getElementById("write-unlock-overlay");
|
|
1693
|
+
if (overlay) overlay.style.display = "none";
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
async function submitWriteUnlock() {
|
|
1697
|
+
const input = document.getElementById("write-unlock-input");
|
|
1698
|
+
const btn = document.getElementById("write-unlock-btn");
|
|
1699
|
+
const err = document.getElementById("write-unlock-err");
|
|
1700
|
+
const pw = input ? input.value : "";
|
|
1701
|
+
if (!pw) { if (err) err.textContent = "Password is required."; return; }
|
|
1702
|
+
if (btn) { btn.disabled = true; btn.textContent = "Verifying..."; }
|
|
1651
1703
|
try {
|
|
1652
1704
|
const r = await fetch(BASE + "/auth", {
|
|
1653
1705
|
method: "POST",
|
|
1654
1706
|
headers: { "Content-Type": "application/json" },
|
|
1655
1707
|
body: JSON.stringify({ password: pw }),
|
|
1656
1708
|
}).then(r => r.json());
|
|
1657
|
-
if (r.error) {
|
|
1709
|
+
if (r.error) {
|
|
1710
|
+
if (input) { input.className = "lock-input error"; setTimeout(() => input.className = "lock-input", 600); }
|
|
1711
|
+
if (err) err.textContent = "Invalid: " + (r.error || "Invalid password");
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1658
1714
|
writeToken = r.write_token || null;
|
|
1659
1715
|
refreshWriteLockUi();
|
|
1660
|
-
|
|
1661
|
-
} catch (e) {
|
|
1716
|
+
closeWriteUnlockModal();
|
|
1717
|
+
} catch (e) {
|
|
1718
|
+
if (err) err.textContent = "Unlock error: " + (e.message || e);
|
|
1719
|
+
} finally {
|
|
1720
|
+
if (btn) { btn.disabled = false; btn.textContent = "Unlock Writes"; }
|
|
1721
|
+
}
|
|
1662
1722
|
}
|
|
1663
1723
|
|
|
1664
1724
|
// Reflect write-lock state on the button so it is obvious when a save will fail.
|
|
@@ -7820,7 +7880,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
7820
7880
|
|
|
7821
7881
|
// If vault is already unlocked at startup (auto-unlock from boot.key skips POST /unlock),
|
|
7822
7882
|
// mirror the tunnel-start logic that the unlock handler would have run.
|
|
7823
|
-
if (initPassword) {
|
|
7883
|
+
if (initPassword) {
|
|
7824
7884
|
setImmediate(() => {
|
|
7825
7885
|
if (tunnelHostname) {
|
|
7826
7886
|
tunnelStatus = "starting";
|
|
@@ -7836,32 +7896,32 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
7836
7896
|
}
|
|
7837
7897
|
}).catch(() => { tunnelStatus = "error"; });
|
|
7838
7898
|
}
|
|
7839
|
-
});
|
|
7840
|
-
}
|
|
7841
|
-
|
|
7842
|
-
// The localhost supervisor is the only process allowed to repair clauth-owned
|
|
7843
|
-
// local surfaces. Keep this loop out of the vault/staged instances and make
|
|
7844
|
-
// the cadence configurable for deterministic tests.
|
|
7845
|
-
if (port === getSupervisorPort() && process.env.CLAUTH_SUPERVISOR_HEALTH_RECONCILE !== "0") {
|
|
7846
|
-
const configuredInterval = Number(process.env.CLAUTH_SUPERVISOR_HEALTH_INTERVAL_MS || 10000);
|
|
7847
|
-
const intervalMs = Number.isFinite(configuredInterval) ? Math.max(1000, Math.min(configuredInterval, 300000)) : 10000;
|
|
7848
|
-
let healthReconcileInFlight = false;
|
|
7849
|
-
const runHealthReconcile = () => {
|
|
7850
|
-
if (healthReconcileInFlight) return;
|
|
7851
|
-
healthReconcileInFlight = true;
|
|
7852
|
-
reconcileSurfaceHealth().catch((err) => {
|
|
7853
|
-
try { fs.appendFileSync(LOG_FILE, `[${new Date().toISOString()}] supervisor health reconcile failed: ${err.message}\n`); } catch {}
|
|
7854
|
-
}).finally(() => { healthReconcileInFlight = false; });
|
|
7855
|
-
};
|
|
7856
|
-
const healthTimer = setInterval(runHealthReconcile, intervalMs);
|
|
7857
|
-
healthTimer.unref?.();
|
|
7858
|
-
server.__supervisorHealthTimer = healthTimer;
|
|
7859
|
-
server.on("close", () => clearInterval(healthTimer));
|
|
7860
|
-
setImmediate(runHealthReconcile);
|
|
7861
|
-
}
|
|
7862
|
-
|
|
7863
|
-
return server;
|
|
7864
|
-
}
|
|
7899
|
+
});
|
|
7900
|
+
}
|
|
7901
|
+
|
|
7902
|
+
// The localhost supervisor is the only process allowed to repair clauth-owned
|
|
7903
|
+
// local surfaces. Keep this loop out of the vault/staged instances and make
|
|
7904
|
+
// the cadence configurable for deterministic tests.
|
|
7905
|
+
if (port === getSupervisorPort() && process.env.CLAUTH_SUPERVISOR_HEALTH_RECONCILE !== "0") {
|
|
7906
|
+
const configuredInterval = Number(process.env.CLAUTH_SUPERVISOR_HEALTH_INTERVAL_MS || 10000);
|
|
7907
|
+
const intervalMs = Number.isFinite(configuredInterval) ? Math.max(1000, Math.min(configuredInterval, 300000)) : 10000;
|
|
7908
|
+
let healthReconcileInFlight = false;
|
|
7909
|
+
const runHealthReconcile = () => {
|
|
7910
|
+
if (healthReconcileInFlight) return;
|
|
7911
|
+
healthReconcileInFlight = true;
|
|
7912
|
+
reconcileSurfaceHealth().catch((err) => {
|
|
7913
|
+
try { fs.appendFileSync(LOG_FILE, `[${new Date().toISOString()}] supervisor health reconcile failed: ${err.message}\n`); } catch {}
|
|
7914
|
+
}).finally(() => { healthReconcileInFlight = false; });
|
|
7915
|
+
};
|
|
7916
|
+
const healthTimer = setInterval(runHealthReconcile, intervalMs);
|
|
7917
|
+
healthTimer.unref?.();
|
|
7918
|
+
server.__supervisorHealthTimer = healthTimer;
|
|
7919
|
+
server.on("close", () => clearInterval(healthTimer));
|
|
7920
|
+
setImmediate(runHealthReconcile);
|
|
7921
|
+
}
|
|
7922
|
+
|
|
7923
|
+
return server;
|
|
7924
|
+
}
|
|
7865
7925
|
|
|
7866
7926
|
// ── Actions ──────────────────────────────────────────────────
|
|
7867
7927
|
|