@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.
- package/cli/commands/serve.js +318 -4
- package/cli/fingerprint.js +9 -7
- package/cli/index.js +5 -1
- package/package.json +1 -1
package/cli/commands/serve.js
CHANGED
|
@@ -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
|
-
//
|
|
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 —
|
|
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
|
-
//
|
|
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
|
}
|
package/cli/fingerprint.js
CHANGED
|
@@ -15,16 +15,18 @@ function getMachineId() {
|
|
|
15
15
|
|
|
16
16
|
try {
|
|
17
17
|
if (platform === "win32") {
|
|
18
|
-
// Primary: BIOS UUID via
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
"
|
|
27
|
+
`${psPath} -NoProfile -Command "(Get-ItemProperty 'HKLM:\\SOFTWARE\\Microsoft\\Cryptography').MachineGuid"`,
|
|
26
28
|
{ encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] }
|
|
27
|
-
).
|
|
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";
|