@lifeaitools/clauth 0.7.0 → 0.7.4

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.
@@ -166,6 +166,20 @@ function dashboardHtml(port, whitelist) {
166
166
  .btn-mcp-setup{background:#1e293b;color:#94a3b8;border:1px solid #334155;padding:6px 12px;font-size:.8rem;border-radius:6px;cursor:pointer;font-weight:500;transition:all .15s}
167
167
  .btn-mcp-setup:hover{border-color:#60a5fa;color:#60a5fa}
168
168
  .btn-mcp-setup:disabled{opacity:.4;cursor:not-allowed}
169
+ .btn-add{background:#1a2e1a;color:#4ade80;border:1px solid #166534;padding:7px 16px;font-size:.85rem;border-radius:7px;cursor:pointer;font-weight:500;transition:all .15s}
170
+ .btn-add:hover{background:#1a3d22;border-color:#4ade80}
171
+ .add-panel{display:none;background:#1a1f2e;border:1px solid #334155;border-radius:8px;padding:1.25rem;margin-bottom:1.5rem}
172
+ .add-panel h3{font-size:.9rem;font-weight:600;color:#f8fafc;margin-bottom:1rem}
173
+ .add-row{display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end;margin-bottom:.75rem}
174
+ .add-field{display:flex;flex-direction:column;gap:4px}
175
+ .add-field label{font-size:.75rem;color:#64748b}
176
+ .add-input{background:#0f172a;border:1px solid #334155;border-radius:6px;color:#e2e8f0;font-family:'Courier New',monospace;font-size:.88rem;padding:7px 12px;outline:none;width:200px;transition:border-color .2s}
177
+ .add-input:focus{border-color:#3b82f6}
178
+ .add-select{background:#0f172a;border:1px solid #334155;border-radius:6px;color:#e2e8f0;font-size:.88rem;padding:7px 12px;outline:none;width:200px;transition:border-color .2s;cursor:pointer}
179
+ .add-select:focus{border-color:#3b82f6}
180
+ .add-foot{display:flex;gap:8px;align-items:center}
181
+ .add-msg{font-size:.82rem}
182
+ .add-msg.ok{color:#4ade80} .add-msg.fail{color:#f87171}
169
183
  .footer{margin-top:2rem;font-size:.75rem;color:#475569;text-align:center}
170
184
  .oauth-fields{display:flex;flex-direction:column;gap:8px;margin-bottom:8px}
171
185
  .oauth-field{display:flex;flex-direction:column;gap:3px}
@@ -204,11 +218,33 @@ function dashboardHtml(port, whitelist) {
204
218
  </div>
205
219
  <div class="toolbar">
206
220
  <button class="btn-refresh" onclick="loadServices()">↻ Refresh</button>
221
+ <button class="btn-add" onclick="toggleAddService()">+ Add Service</button>
207
222
  <button class="btn-check" id="check-btn" onclick="checkAll()">⬤ Check All</button>
208
223
  <button class="btn-lock" onclick="lockVault()">🔒 Lock</button>
209
224
  <button class="btn-cancel" style="margin-left:auto" onclick="toggleChangePw()">Change Password</button>
210
225
  </div>
211
226
 
227
+ <div class="add-panel" id="add-panel">
228
+ <h3>Add New Service</h3>
229
+ <div class="add-row">
230
+ <div class="add-field">
231
+ <label>Service name</label>
232
+ <input class="add-input" id="add-name" type="text" placeholder="e.g. coolify-admin" autocomplete="off" spellcheck="false">
233
+ </div>
234
+ <div class="add-field">
235
+ <label>Key type</label>
236
+ <select class="add-select" id="add-type">
237
+ <option value="">Loading…</option>
238
+ </select>
239
+ </div>
240
+ </div>
241
+ <div class="add-foot">
242
+ <button class="btn-chpw-save" onclick="addService()">Create Service</button>
243
+ <button class="btn-cancel" onclick="toggleAddService()">Cancel</button>
244
+ <span class="add-msg" id="add-msg"></span>
245
+ </div>
246
+ </div>
247
+
212
248
  <div class="chpw-panel" id="chpw-panel">
213
249
  <h3>Change Master Password</h3>
214
250
  <div class="chpw-row">
@@ -722,11 +758,78 @@ async function changePassword() {
722
758
  }
723
759
  }
724
760
 
725
- // Enter key on lock screen
761
+ // ── Add Service ──────────────────────────────
762
+ const TYPE_LABELS = {
763
+ token: "token (API key)",
764
+ secret: "secret (password, secret)",
765
+ keypair: "keypair (user:key pair)",
766
+ connstring: "connstring (connection string)",
767
+ oauth: "oauth (OAuth credentials)",
768
+ };
769
+
770
+ async function toggleAddService() {
771
+ const panel = document.getElementById("add-panel");
772
+ const open = panel.style.display === "block";
773
+ panel.style.display = open ? "none" : "block";
774
+ if (!open) {
775
+ document.getElementById("add-name").value = "";
776
+ document.getElementById("add-msg").textContent = "";
777
+ // Fetch available key types from server
778
+ const sel = document.getElementById("add-type");
779
+ sel.innerHTML = '<option value="">Loading…</option>';
780
+ try {
781
+ const r = await fetch(BASE + "/meta").then(r => r.json());
782
+ const types = r.key_types || ["token", "secret", "keypair", "connstring", "oauth"];
783
+ sel.innerHTML = types.map(t =>
784
+ \`<option value="\${t}">\${TYPE_LABELS[t] || t}</option>\`
785
+ ).join("");
786
+ } catch {
787
+ sel.innerHTML = ["token","secret","keypair","connstring","oauth"].map(t =>
788
+ \`<option value="\${t}">\${TYPE_LABELS[t] || t}</option>\`
789
+ ).join("");
790
+ }
791
+ document.getElementById("add-name").focus();
792
+ }
793
+ }
794
+
795
+ async function addService() {
796
+ const name = document.getElementById("add-name").value.trim().toLowerCase();
797
+ const type = document.getElementById("add-type").value;
798
+ const msg = document.getElementById("add-msg");
799
+
800
+ if (!name) { msg.className = "add-msg fail"; msg.textContent = "Service name is required."; return; }
801
+ if (!/^[a-z0-9][a-z0-9_-]*$/.test(name)) { msg.className = "add-msg fail"; msg.textContent = "Lowercase letters, numbers, hyphens, underscores only."; return; }
802
+
803
+ msg.className = "add-msg"; msg.textContent = "Creating…";
804
+ try {
805
+ const r = await fetch(BASE + "/add-service", {
806
+ method: "POST",
807
+ headers: { "Content-Type": "application/json" },
808
+ body: JSON.stringify({ name, key_type: type, label: name })
809
+ }).then(r => r.json());
810
+
811
+ if (r.locked) { showLockScreen(); return; }
812
+ if (r.error) throw new Error(r.error);
813
+
814
+ msg.className = "add-msg ok"; msg.textContent = "✓ " + name + " created";
815
+ setTimeout(() => {
816
+ document.getElementById("add-panel").style.display = "none";
817
+ msg.textContent = "";
818
+ loadServices();
819
+ }, 1200);
820
+ } catch (e) {
821
+ msg.className = "add-msg fail"; msg.textContent = "✗ " + e.message;
822
+ }
823
+ }
824
+
825
+ // Enter key on lock screen and add-service panel
726
826
  document.addEventListener("DOMContentLoaded", () => {
727
827
  document.getElementById("lock-input").addEventListener("keydown", e => {
728
828
  if (e.key === "Enter") unlock();
729
829
  });
830
+ document.getElementById("add-name").addEventListener("keydown", e => {
831
+ if (e.key === "Enter") addService();
832
+ });
730
833
  });
731
834
 
732
835
  // ── Tunnel management ───────────────────────
@@ -1120,6 +1223,43 @@ function createServer(initPassword, whitelist, port, tunnelHostname = null) {
1120
1223
  res.end(JSON.stringify(data));
1121
1224
  }
1122
1225
 
1226
+ // ── Build status (populated via Supabase Realtime) ─────────
1227
+ let buildStatus = { active: false, status: "idle", apps: [], updated_at: null };
1228
+
1229
+ (async () => {
1230
+ try {
1231
+ const sbUrl = (api.getBaseUrl() || "").replace("/functions/v1/auth-vault", "");
1232
+ const sbKey = api.getAnonKey();
1233
+ if (!sbUrl || !sbKey) return;
1234
+
1235
+ const { createClient: createSB } = await import("@supabase/supabase-js");
1236
+ const sb = createSB(sbUrl, sbKey);
1237
+
1238
+ // Initial fetch
1239
+ const { data } = await sb.from("prt_storage").select("value").eq("key", "build_status").single();
1240
+ if (data?.value) buildStatus = typeof data.value === "string" ? JSON.parse(data.value) : data.value;
1241
+
1242
+ // Realtime subscription
1243
+ sb.channel("build-status")
1244
+ .on("postgres_changes", { event: "*", schema: "public", table: "prt_storage", filter: "key=eq.build_status" },
1245
+ (payload) => {
1246
+ const val = payload.new?.value;
1247
+ if (val) {
1248
+ buildStatus = typeof val === "string" ? JSON.parse(val) : val;
1249
+ const logMsg = `[${new Date().toISOString()}] Build status update: ${buildStatus.status} (${buildStatus.commit?.slice(0,8) || "?"})\n`;
1250
+ try { fs.appendFileSync(LOG_FILE, logMsg); } catch {}
1251
+ }
1252
+ })
1253
+ .subscribe();
1254
+
1255
+ const logMsg = `[${new Date().toISOString()}] Build status: Realtime subscription active\n`;
1256
+ try { fs.appendFileSync(LOG_FILE, logMsg); } catch {}
1257
+ } catch (err) {
1258
+ const logMsg = `[${new Date().toISOString()}] Build status subscription failed: ${err.message}\n`;
1259
+ try { fs.appendFileSync(LOG_FILE, logMsg); } catch {}
1260
+ }
1261
+ })();
1262
+
1123
1263
  const server = http.createServer(async (req, res) => {
1124
1264
  const remote = req.socket.remoteAddress;
1125
1265
  const isLocal = remote === "127.0.0.1" || remote === "::1" || remote === "::ffff:127.0.0.1";
@@ -1292,7 +1432,9 @@ function createServer(initPassword, whitelist, port, tunnelHostname = null) {
1292
1432
  res.writeHead(401, { "Content-Type": "application/json", ...CORS });
1293
1433
  return res.end(JSON.stringify({ error: "invalid_token" }));
1294
1434
  }
1295
- // Token valid — fall through to MCP handling below
1435
+ // Token valid — mark as remote caller (claude.ai via tunnel)
1436
+ req._clauthRemote = true;
1437
+ // fall through to MCP handling below
1296
1438
  }
1297
1439
 
1298
1440
  // ── MCP Streamable HTTP transport (2025-03-26 spec) ──
@@ -1333,6 +1475,7 @@ function createServer(initPassword, whitelist, port, tunnelHostname = null) {
1333
1475
  if (rpcMethod === "tools/call") {
1334
1476
  const { name, arguments: args } = body.params || {};
1335
1477
  const vault = sseVault();
1478
+ vault.remote = !!req._clauthRemote;
1336
1479
  try {
1337
1480
  const result = await handleMcpTool(vault, name, args || {});
1338
1481
  password = vault.password;
@@ -1490,6 +1633,11 @@ function createServer(initPassword, whitelist, port, tunnelHostname = null) {
1490
1633
  });
1491
1634
  }
1492
1635
 
1636
+ // GET /builds — CI build status (no auth required, same as /ping)
1637
+ if (method === "GET" && reqPath === "/builds") {
1638
+ return ok(res, buildStatus);
1639
+ }
1640
+
1493
1641
  // GET /mcp-setup — OAuth credentials for claude.ai MCP setup (localhost only)
1494
1642
  if (method === "GET" && reqPath === "/mcp-setup") {
1495
1643
  return ok(res, {
@@ -1535,6 +1683,31 @@ function createServer(initPassword, whitelist, port, tunnelHostname = null) {
1535
1683
  return false;
1536
1684
  }
1537
1685
 
1686
+ // GET /meta — return valid key_types from DB check constraint
1687
+ if (method === "GET" && reqPath === "/meta") {
1688
+ if (lockedGuard(res)) return;
1689
+ try {
1690
+ const baseUrl = api.getBaseUrl().replace("/functions/v1/auth-vault", "");
1691
+ const anonKey = api.getAnonKey();
1692
+ const sql = `SELECT check_clause FROM information_schema.check_constraints WHERE constraint_name = 'clauth_services_key_type_check'`;
1693
+ const r = await fetch(`${baseUrl}/rest/v1/rpc/`, {
1694
+ method: "POST",
1695
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${anonKey}`, "apikey": anonKey }
1696
+ }).catch(() => null);
1697
+ // Fallback: parse from a direct SQL query via postgrest isn't possible without an RPC,
1698
+ // so we query the status endpoint which returns services with their key_types
1699
+ const { token, timestamp } = deriveToken(password, machineHash);
1700
+ const statusResult = await api.status(password, machineHash, token, timestamp);
1701
+ const existingTypes = [...new Set((statusResult.services || []).map(s => s.key_type).filter(Boolean))];
1702
+ // Merge with known types (in case no service of that type exists yet)
1703
+ const knownTypes = ["token", "secret", "keypair", "connstring", "oauth"];
1704
+ const allTypes = [...new Set([...knownTypes, ...existingTypes])];
1705
+ return ok(res, { key_types: allTypes });
1706
+ } catch (err) {
1707
+ return ok(res, { key_types: ["token", "secret", "keypair", "connstring", "oauth"] });
1708
+ }
1709
+ }
1710
+
1538
1711
  // GET /status
1539
1712
  if (method === "GET" && reqPath === "/status") {
1540
1713
  if (lockedGuard(res)) return;
@@ -1783,6 +1956,38 @@ function createServer(initPassword, whitelist, port, tunnelHostname = null) {
1783
1956
  }
1784
1957
  }
1785
1958
 
1959
+ // POST /add-service — register a new service in the vault
1960
+ if (method === "POST" && reqPath === "/add-service") {
1961
+ if (lockedGuard(res)) return;
1962
+
1963
+ let body;
1964
+ try { body = await readBody(req); } catch {
1965
+ res.writeHead(400, { "Content-Type": "application/json", ...CORS });
1966
+ return res.end(JSON.stringify({ error: "Invalid JSON body" }));
1967
+ }
1968
+
1969
+ const { name, label, key_type, description } = body;
1970
+ if (!name || typeof name !== "string" || !name.trim()) {
1971
+ res.writeHead(400, { "Content-Type": "application/json", ...CORS });
1972
+ return res.end(JSON.stringify({ error: "name is required" }));
1973
+ }
1974
+ const validTypes = ["token", "keypair", "connstring", "oauth", "secret"];
1975
+ const type = (key_type || "token").toLowerCase();
1976
+ if (!validTypes.includes(type)) {
1977
+ res.writeHead(400, { "Content-Type": "application/json", ...CORS });
1978
+ return res.end(JSON.stringify({ error: `key_type must be one of: ${validTypes.join(", ")}` }));
1979
+ }
1980
+
1981
+ try {
1982
+ const { token, timestamp } = deriveToken(password, machineHash);
1983
+ const result = await api.addService(password, machineHash, token, timestamp, name.trim().toLowerCase(), label || name.trim(), type, description || "");
1984
+ if (result.error) return strike(res, 502, result.error);
1985
+ return ok(res, { ok: true, service: name.trim().toLowerCase() });
1986
+ } catch (err) {
1987
+ return strike(res, 502, err.message);
1988
+ }
1989
+ }
1990
+
1786
1991
  // Unknown route — don't count browser/MCP noise as auth failures
1787
1992
  // Don't count browser noise, MCP discovery probes, or OAuth probes as auth failures
1788
1993
  const isBenign = reqPath.startsWith("/.well-known/") || [
@@ -2286,7 +2491,14 @@ async function handleMcpTool(vault, name, args) {
2286
2491
  };
2287
2492
  }
2288
2493
 
2289
- // Default: file
2494
+ // Remote caller (claude.ai via tunnel) — can't read local temp files
2495
+ // Return value inline in MCP response (safe: stays in AI context, not a shell transcript)
2496
+ if (vault.remote) {
2497
+ const envVar = ENV_MAP[service] || service.toUpperCase().replace(/-/g, "_");
2498
+ return mcpResult(`${envVar}=${value}`);
2499
+ }
2500
+
2501
+ // Default: file (local callers)
2290
2502
  const envVar = ENV_MAP[service] || service.toUpperCase().replace(/-/g, "_");
2291
2503
  const filePath = writeTempSecret(service, value);
2292
2504
  return mcpResult(`${service} → ${filePath} (auto-deletes in 30s)\nEnv var: ${envVar}\nUsage: export ${envVar}=$(cat ${filePath.replace(/\\/g, "/")})`);
@@ -2322,6 +2534,14 @@ async function handleMcpTool(vault, name, args) {
2322
2534
 
2323
2535
  if (!lines.length) return mcpError(`No services retrieved:\n${errors.join("\n")}`);
2324
2536
 
2537
+ // Remote caller — return env vars inline
2538
+ if (vault.remote) {
2539
+ let msg = lines.join("\n");
2540
+ if (errors.length) msg += `\n\nErrors:\n${errors.join("\n")}`;
2541
+ return mcpResult(msg);
2542
+ }
2543
+
2544
+ // Local caller — write temp env file
2325
2545
  const envFilePath = path.join(os.tmpdir(), ".clauth-env");
2326
2546
  fs.writeFileSync(envFilePath, lines.join("\n") + "\n", { mode: 0o600 });
2327
2547
  setTimeout(() => { try { fs.unlinkSync(envFilePath); } catch {} }, 30_000);
@@ -2497,6 +2717,98 @@ async function actionMcp(opts) {
2497
2717
  createMcpServer(password, whitelist);
2498
2718
  }
2499
2719
 
2720
+ // ── DPAPI auto-start install / uninstall (Windows only) ──────
2721
+ const AUTOSTART_DIR = path.join(os.homedir(), "AppData", "Roaming", "clauth");
2722
+ const BOOT_KEY_PATH = path.join(AUTOSTART_DIR, "boot.key");
2723
+ const PS_SCRIPT_PATH = path.join(AUTOSTART_DIR, "autostart.ps1");
2724
+ const TASK_NAME = "ClauthAutostart";
2725
+
2726
+ async function actionInstall(opts) {
2727
+ if (os.platform() !== "win32") {
2728
+ console.log(chalk.red("\n serve install is only supported on Windows\n"));
2729
+ process.exit(1);
2730
+ }
2731
+
2732
+ const { default: inquirer } = await import("inquirer");
2733
+ const { pw } = await inquirer.prompt([{
2734
+ type: "password", name: "pw",
2735
+ message: "Enter clauth password to store for auto-start:", mask: "*"
2736
+ }]);
2737
+
2738
+ fs.mkdirSync(AUTOSTART_DIR, { recursive: true });
2739
+
2740
+ // Encrypt password with Windows DPAPI (CurrentUser scope — machine+user bound)
2741
+ const spinner = ora("Encrypting password with Windows DPAPI...").start();
2742
+ let encrypted;
2743
+ try {
2744
+ const { execSync } = await import("child_process");
2745
+ const pwEscaped = pw.replace(/'/g, "''");
2746
+ const psExpr = `[Convert]::ToBase64String([Security.Cryptography.ProtectedData]::Protect([Text.Encoding]::UTF8.GetBytes('${pwEscaped}'),$null,'CurrentUser'))`;
2747
+ encrypted = execSync(`powershell -NoProfile -Command "${psExpr}"`, { encoding: "utf8" }).trim();
2748
+ fs.writeFileSync(BOOT_KEY_PATH, encrypted, "utf8");
2749
+ spinner.succeed(chalk.green("Password encrypted → boot.key"));
2750
+ } catch (err) {
2751
+ spinner.fail(chalk.red(`DPAPI encryption failed: ${err.message}`));
2752
+ process.exit(1);
2753
+ }
2754
+
2755
+ // Write PowerShell autostart script — decrypts boot.key and pipes to clauth serve start
2756
+ const cliEntry = path.resolve(__dirname, "../index.js").replace(/\\/g, "\\\\");
2757
+ const nodeExe = process.execPath.replace(/\\/g, "\\\\");
2758
+ const bootKey = BOOT_KEY_PATH.replace(/\\/g, "\\\\");
2759
+ const psScript = [
2760
+ "# clauth autostart — generated by clauth serve install",
2761
+ `$enc = (Get-Content '${bootKey}' -Raw).Trim()`,
2762
+ `$pw = [Text.Encoding]::UTF8.GetString([Security.Cryptography.ProtectedData]::Unprotect([Convert]::FromBase64String($enc),$null,'CurrentUser'))`,
2763
+ `Start-Process '${nodeExe}' -ArgumentList "'${cliEntry}' serve start -p $pw" -WindowStyle Hidden`,
2764
+ ].join("\n");
2765
+ fs.writeFileSync(PS_SCRIPT_PATH, psScript, "utf8");
2766
+
2767
+ // Register Windows Scheduled Task — triggers on user logon
2768
+ const spinner2 = ora("Registering Windows Scheduled Task...").start();
2769
+ try {
2770
+ const { execSync } = await import("child_process");
2771
+ const psScriptEsc = PS_SCRIPT_PATH.replace(/\\/g, "\\\\");
2772
+ const args = `-NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File "${psScriptEsc}"`;
2773
+ execSync(
2774
+ `schtasks /create /f /tn "${TASK_NAME}" /sc onlogon /tr "powershell.exe ${args}"`,
2775
+ { encoding: "utf8", stdio: "pipe" }
2776
+ );
2777
+ spinner2.succeed(chalk.green(`Scheduled Task "${TASK_NAME}" registered`));
2778
+ } catch (err) {
2779
+ spinner2.fail(chalk.yellow(`Scheduled task failed (non-fatal): ${err.message}`));
2780
+ console.log(chalk.gray(" You can still start manually: clauth serve start"));
2781
+ }
2782
+
2783
+ console.log(chalk.cyan("\n Auto-start installed:\n"));
2784
+ console.log(chalk.gray(` boot.key: ${BOOT_KEY_PATH}`));
2785
+ console.log(chalk.gray(` script: ${PS_SCRIPT_PATH}`));
2786
+ console.log(chalk.gray(` task: ${TASK_NAME}\n`));
2787
+ console.log(chalk.green(" Daemon will auto-start on next Windows login.\n"));
2788
+ }
2789
+
2790
+ async function actionUninstall() {
2791
+ if (os.platform() !== "win32") {
2792
+ console.log(chalk.red("\n serve uninstall is only supported on Windows\n"));
2793
+ process.exit(1);
2794
+ }
2795
+ const { execSync } = await import("child_process");
2796
+
2797
+ // Remove scheduled task
2798
+ try {
2799
+ execSync(`schtasks /delete /f /tn "${TASK_NAME}"`, { encoding: "utf8", stdio: "pipe" });
2800
+ console.log(chalk.green(` Removed Scheduled Task: ${TASK_NAME}`));
2801
+ } catch { console.log(chalk.gray(` Task not found (already removed): ${TASK_NAME}`)); }
2802
+
2803
+ // Remove boot.key and autostart script
2804
+ for (const f of [BOOT_KEY_PATH, PS_SCRIPT_PATH]) {
2805
+ try { fs.unlinkSync(f); console.log(chalk.green(` Deleted: ${f}`)); }
2806
+ catch { console.log(chalk.gray(` Not found (skipped): ${f}`)); }
2807
+ }
2808
+
2809
+ console.log(chalk.cyan("\n Auto-start uninstalled.\n"));
2810
+ }
2811
+
2500
2812
  // ── Export ────────────────────────────────────────────────────
2501
2813
  export async function runServe(opts) {
2502
2814
  const action = opts.action || "foreground";
@@ -2508,9 +2820,11 @@ export async function runServe(opts) {
2508
2820
  case "ping": return actionPing();
2509
2821
  case "foreground": return actionForeground(opts);
2510
2822
  case "mcp": return actionMcp(opts);
2823
+ case "install": return actionInstall(opts);
2824
+ case "uninstall": return actionUninstall();
2511
2825
  default:
2512
2826
  console.log(chalk.red(`\n Unknown serve action: ${action}`));
2513
- console.log(chalk.gray(" Actions: start | stop | restart | ping | foreground | mcp\n"));
2827
+ console.log(chalk.gray(" Actions: start | stop | restart | ping | foreground | mcp | install | uninstall\n"));
2514
2828
  process.exit(1);
2515
2829
  }
2516
2830
  }
@@ -15,16 +15,18 @@ function getMachineId() {
15
15
 
16
16
  try {
17
17
  if (platform === "win32") {
18
- // Primary: BIOS UUID via WMIC
19
- const uuid = execSync("wmic csproduct get uuid /format:value", {
20
- encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"]
21
- }).match(/UUID=([A-F0-9-]+)/i)?.[1]?.trim();
18
+ // Primary: BIOS UUID via PowerShell (wmic removed in Win11 26xxx+)
19
+ const psPath = "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe";
20
+ const uuid = execSync(
21
+ `${psPath} -NoProfile -Command "(Get-CimInstance Win32_ComputerSystemProduct).UUID"`,
22
+ { encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] }
23
+ ).trim();
22
24
 
23
- // Secondary: Windows MachineGuid from registry
25
+ // Secondary: Windows MachineGuid from registry (via PowerShell for Win11 compat)
24
26
  const machineGuid = execSync(
25
- "reg query HKLM\\SOFTWARE\\Microsoft\\Cryptography /v MachineGuid",
27
+ `${psPath} -NoProfile -Command "(Get-ItemProperty 'HKLM:\\SOFTWARE\\Microsoft\\Cryptography').MachineGuid"`,
26
28
  { encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] }
27
- ).match(/MachineGuid\s+REG_SZ\s+([a-f0-9-]+)/i)?.[1]?.trim();
29
+ ).trim();
28
30
 
29
31
  if (!uuid || !machineGuid) throw new Error("Could not read Windows machine IDs");
30
32
  return { primary: uuid, secondary: machineGuid, platform: "win32" };
package/cli/index.js CHANGED
@@ -464,7 +464,7 @@ program.addHelpText("beforeAll", chalk.cyan(`
464
464
  // ──────────────────────────────────────────────
465
465
  program
466
466
  .command("serve [action]")
467
- .description("Manage localhost HTTP vault daemon (start|stop|restart|ping)")
467
+ .description("Manage localhost HTTP vault daemon (start|stop|restart|ping|install|uninstall)")
468
468
  .option("--port <n>", "Port (default: 52437)")
469
469
  .option("-p, --pw <password>", "clauth password (optional — omit to start locked, unlock in browser)")
470
470
  .option("--services <list>", "Comma-separated service whitelist (default: all)")
@@ -478,6 +478,8 @@ Actions:
478
478
  ping Check if the daemon is running
479
479
  foreground Run in foreground (Ctrl+C to stop) — default if no action given
480
480
  mcp Run as MCP stdio server for Claude Code (JSON-RPC over stdin/stdout)
481
+ install (Windows) Encrypt password with DPAPI + create Scheduled Task for auto-start on login
482
+ uninstall (Windows) Remove Scheduled Task + delete boot.key
481
483
 
482
484
  MCP SSE (built into start/foreground):
483
485
  The HTTP daemon also serves MCP SSE transport at GET /sse + POST /message.
@@ -492,6 +494,8 @@ Examples:
492
494
  clauth serve start --services github,vercel
493
495
  clauth serve mcp Start MCP server for Claude Code
494
496
  clauth serve mcp -p mypass Start MCP server pre-unlocked
497
+ clauth serve install (Windows) Set up auto-start on login via DPAPI
498
+ clauth serve uninstall (Windows) Remove auto-start
495
499
  `)
496
500
  .action(async (action, opts) => {
497
501
  const resolvedAction = opts.action || action || "foreground";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifeaitools/clauth",
3
- "version": "0.7.0",
3
+ "version": "0.7.4",
4
4
  "description": "Hardware-bound credential vault for the LIFEAI infrastructure stack",
5
5
  "type": "module",
6
6
  "bin": {