@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.
@@ -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 = 10 * 60 * 1000;
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
- // ── Unlock writes (re-establish write scope without locking) ──
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 (10-min TTL).
1647
- async function unlockWrites() {
1648
- if (writeToken && !confirm("Writes are already unlocked this session. Re-unlock?")) return;
1649
- const pw = prompt("Enter your vault password to enable saving changes (10-minute write session):");
1650
- if (!pw) return;
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) { alert("Unlock failed: " + r.error); return; }
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
- alert(writeToken ? "Writes unlocked for 10 minutes." : "Unlock did not return a write token.");
1661
- } catch (e) { alert("Unlock error: " + (e.message || 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