@lifeaitools/clauth 0.3.11 → 0.4.0
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/.clauth-skill/SKILL.md +184 -184
- package/.clauth-skill/references/keys-guide.md +270 -270
- package/.clauth-skill/references/operator-guide.md +148 -148
- package/README.md +125 -125
- package/cli/api.js +113 -112
- package/cli/commands/install.js +265 -264
- package/cli/commands/scrub.js +231 -231
- package/cli/commands/serve.js +514 -74
- package/cli/commands/uninstall.js +164 -164
- package/cli/conf-path.js +21 -0
- package/cli/fingerprint.js +91 -91
- package/cli/index.js +6 -2
- package/install.ps1 +44 -44
- package/install.sh +38 -38
- package/package.json +54 -54
- package/scripts/bin/bootstrap-linux +0 -0
- package/scripts/bin/bootstrap-macos +0 -0
- package/scripts/bootstrap.cjs +43 -43
- package/scripts/build.sh +45 -45
- package/supabase/functions/auth-vault/index.ts +235 -235
- package/supabase/migrations/001_clauth_schema.sql +103 -103
- package/supabase/migrations/002_vault_helpers.sql +90 -90
- package/supabase/migrations/20260317_lockout.sql +26 -26
package/cli/commands/serve.js
CHANGED
|
@@ -221,32 +221,36 @@ const KEY_URLS = {
|
|
|
221
221
|
};
|
|
222
222
|
|
|
223
223
|
// ── OAuth import config ─────────────────────
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
{ key: "refresh_token", label: "Refresh Token", hint: "From Google OAuth Playground or your app's auth callback"
|
|
231
|
-
|
|
232
|
-
]
|
|
224
|
+
// Services where Google downloads a JSON file. jsonFields = keys to extract
|
|
225
|
+
// from the downloaded JSON (top-level or under an "installed"/"web" wrapper).
|
|
226
|
+
// extra = additional fields the user must provide separately.
|
|
227
|
+
const OAUTH_IMPORT = {
|
|
228
|
+
"gmail": {
|
|
229
|
+
jsonFields: ["client_id", "client_secret"],
|
|
230
|
+
extra: [{ key: "refresh_token", label: "Refresh Token", hint: "From Google OAuth Playground or your app's auth callback" }]
|
|
231
|
+
}
|
|
233
232
|
};
|
|
234
233
|
|
|
235
234
|
function renderSetPanel(name) {
|
|
236
|
-
const
|
|
237
|
-
if (
|
|
238
|
-
const
|
|
235
|
+
const imp = OAUTH_IMPORT[name];
|
|
236
|
+
if (imp) {
|
|
237
|
+
const extraHtml = imp.extra.map(f => \`
|
|
239
238
|
<div class="oauth-field">
|
|
240
|
-
<label class="oauth-label">\${f.label}
|
|
239
|
+
<label class="oauth-label">\${f.label}</label>
|
|
241
240
|
\${f.hint ? \`<div class="oauth-hint">\${f.hint}</div>\` : ""}
|
|
242
241
|
<input type="text" class="oauth-input" id="ofield-\${name}-\${f.key}" placeholder="Paste \${f.label}…" spellcheck="false" autocomplete="off">
|
|
243
242
|
</div>
|
|
244
243
|
\`).join("");
|
|
245
244
|
return \`
|
|
246
245
|
<div class="set-panel" id="set-panel-\${name}">
|
|
247
|
-
<label>Set <strong>\${name}</strong> credentials — paste directly from
|
|
246
|
+
<label>Set <strong>\${name}</strong> credentials — paste directly from Google, never in chat</label>
|
|
248
247
|
<div class="oauth-fields">
|
|
249
|
-
|
|
248
|
+
<div class="oauth-field">
|
|
249
|
+
<label class="oauth-label">OAuth JSON from Google Cloud Console</label>
|
|
250
|
+
<div class="oauth-hint">Download from APIs & Services → Credentials → your OAuth client → ↓ Download JSON</div>
|
|
251
|
+
<textarea class="set-input" id="ofield-\${name}-json" placeholder='{"installed":{"client_id":"…","client_secret":"…",...}}' spellcheck="false" rows="3"></textarea>
|
|
252
|
+
</div>
|
|
253
|
+
\${extraHtml}
|
|
250
254
|
</div>
|
|
251
255
|
<div class="set-foot">
|
|
252
256
|
<button class="btn btn-save" onclick="saveKey('\${name}')">Save</button>
|
|
@@ -449,68 +453,63 @@ function toggleSet(name) {
|
|
|
449
453
|
|
|
450
454
|
async function saveKey(name) {
|
|
451
455
|
const msg = document.getElementById("set-msg-" + name);
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
456
|
+
const imp = OAUTH_IMPORT[name];
|
|
457
|
+
let value;
|
|
458
|
+
|
|
459
|
+
if (imp) {
|
|
460
|
+
const jsonEl = document.getElementById("ofield-" + name + "-json");
|
|
461
|
+
const raw = jsonEl ? jsonEl.value.trim() : "";
|
|
462
|
+
if (!raw) { msg.className = "set-msg fail"; msg.textContent = "Paste the OAuth JSON first."; return; }
|
|
463
|
+
let parsed;
|
|
464
|
+
try { parsed = JSON.parse(raw); } catch { msg.className = "set-msg fail"; msg.textContent = "Invalid JSON — copy the full file content."; return; }
|
|
465
|
+
// Google wraps fields under "installed" or "web"
|
|
466
|
+
const src = parsed.installed || parsed.web || parsed;
|
|
467
|
+
const obj = {};
|
|
468
|
+
for (const k of imp.jsonFields) {
|
|
469
|
+
if (!src[k]) { msg.className = "set-msg fail"; msg.textContent = k + " not found in JSON."; return; }
|
|
470
|
+
obj[k] = src[k];
|
|
462
471
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
const v = el ? el.value.trim() : "";
|
|
469
|
-
if (!v) continue; // skip optional empty fields
|
|
470
|
-
const r = await fetch(BASE + "/set/" + name + "." + f.key, {
|
|
471
|
-
method: "POST",
|
|
472
|
-
headers: { "Content-Type": "application/json" },
|
|
473
|
-
body: JSON.stringify({ value: v })
|
|
474
|
-
}).then(r => r.json());
|
|
475
|
-
if (r.locked) { showLockScreen(); return; }
|
|
476
|
-
if (r.error) throw new Error(r.error);
|
|
477
|
-
}
|
|
478
|
-
msg.className = "set-msg ok"; msg.textContent = "✓ Saved";
|
|
479
|
-
fields.forEach(f => { const el = document.getElementById("ofield-" + name + "-" + f.key); if (el) el.value = ""; });
|
|
480
|
-
} catch (e) {
|
|
481
|
-
msg.className = "set-msg fail"; msg.textContent = e.message || "Save failed";
|
|
482
|
-
return;
|
|
472
|
+
for (const f of imp.extra) {
|
|
473
|
+
const el = document.getElementById("ofield-" + name + "-" + f.key);
|
|
474
|
+
const v = el ? el.value.trim() : "";
|
|
475
|
+
if (!v) { msg.className = "set-msg fail"; msg.textContent = f.label + " is required."; return; }
|
|
476
|
+
obj[f.key] = v;
|
|
483
477
|
}
|
|
478
|
+
value = JSON.stringify(obj);
|
|
484
479
|
} else {
|
|
485
480
|
const input = document.getElementById("set-input-" + name);
|
|
486
|
-
|
|
481
|
+
value = input ? input.value.trim() : "";
|
|
487
482
|
if (!value) { msg.className = "set-msg fail"; msg.textContent = "Value is empty."; return; }
|
|
483
|
+
}
|
|
488
484
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
485
|
+
msg.className = "set-msg"; msg.textContent = "Saving…";
|
|
486
|
+
try {
|
|
487
|
+
const r = await fetch(BASE + "/set/" + name, {
|
|
488
|
+
method: "POST",
|
|
489
|
+
headers: { "Content-Type": "application/json" },
|
|
490
|
+
body: JSON.stringify({ value })
|
|
491
|
+
}).then(r => r.json());
|
|
492
|
+
|
|
493
|
+
if (r.locked) { showLockScreen(); return; }
|
|
494
|
+
if (r.error) throw new Error(r.error);
|
|
495
|
+
msg.className = "set-msg ok"; msg.textContent = "✓ Saved";
|
|
496
|
+
if (imp) {
|
|
497
|
+
const jsonEl = document.getElementById("ofield-" + name + "-json");
|
|
498
|
+
if (jsonEl) jsonEl.value = "";
|
|
499
|
+
imp.extra.forEach(f => { const el = document.getElementById("ofield-" + name + "-" + f.key); if (el) el.value = ""; });
|
|
500
|
+
} else {
|
|
500
501
|
const inp = document.getElementById("set-input-" + name);
|
|
501
502
|
if (inp) inp.value = "";
|
|
502
|
-
} catch (e) {
|
|
503
|
-
msg.className = "set-msg fail"; msg.textContent = e.message || "Save failed";
|
|
504
|
-
return;
|
|
505
503
|
}
|
|
504
|
+
const dot = document.getElementById("sdot-" + name);
|
|
505
|
+
if (dot) { dot.className = "status-dot"; dot.title = ""; }
|
|
506
|
+
setTimeout(() => {
|
|
507
|
+
document.getElementById("set-panel-" + name).style.display = "none";
|
|
508
|
+
msg.textContent = "";
|
|
509
|
+
}, 1800);
|
|
510
|
+
} catch (e) {
|
|
511
|
+
msg.className = "set-msg fail"; msg.textContent = "✗ " + e.message;
|
|
506
512
|
}
|
|
507
|
-
|
|
508
|
-
const dot = document.getElementById("sdot-" + name);
|
|
509
|
-
if (dot) { dot.className = "status-dot"; dot.title = ""; }
|
|
510
|
-
setTimeout(() => {
|
|
511
|
-
document.getElementById("set-panel-" + name).style.display = "none";
|
|
512
|
-
msg.textContent = "";
|
|
513
|
-
}, 1800);
|
|
514
513
|
}
|
|
515
514
|
|
|
516
515
|
// ── Enable / Disable service ────────────────
|
|
@@ -772,7 +771,7 @@ function createServer(initPassword, whitelist, port) {
|
|
|
772
771
|
}
|
|
773
772
|
|
|
774
773
|
// GET /get/:service
|
|
775
|
-
const getMatch = reqPath.match(/^\/get\/([a-zA-Z0-9_
|
|
774
|
+
const getMatch = reqPath.match(/^\/get\/([a-zA-Z0-9_-]+)$/);
|
|
776
775
|
if (method === "GET" && getMatch) {
|
|
777
776
|
if (lockedGuard(res)) return;
|
|
778
777
|
const service = getMatch[1].toLowerCase();
|
|
@@ -918,7 +917,7 @@ function createServer(initPassword, whitelist, port) {
|
|
|
918
917
|
}
|
|
919
918
|
|
|
920
919
|
// POST /set/:service — write a new key value into vault
|
|
921
|
-
const setMatch = reqPath.match(/^\/set\/([a-zA-Z0-9_
|
|
920
|
+
const setMatch = reqPath.match(/^\/set\/([a-zA-Z0-9_-]+)$/);
|
|
922
921
|
if (method === "POST" && setMatch) {
|
|
923
922
|
if (lockedGuard(res)) return;
|
|
924
923
|
const service = setMatch[1].toLowerCase();
|
|
@@ -1195,19 +1194,460 @@ async function actionForeground(opts) {
|
|
|
1195
1194
|
});
|
|
1196
1195
|
}
|
|
1197
1196
|
|
|
1197
|
+
// ── MCP stdio server ──────────────────────────────────────────
|
|
1198
|
+
// JSON-RPC 2.0 over stdin/stdout for Claude Code integration.
|
|
1199
|
+
// Reuses the same auth model as the HTTP daemon.
|
|
1200
|
+
// Secrets are delivered via temp files — never in the MCP response.
|
|
1201
|
+
|
|
1202
|
+
import { createInterface } from "readline";
|
|
1203
|
+
import { execSync } from "child_process";
|
|
1204
|
+
|
|
1205
|
+
const ENV_MAP = {
|
|
1206
|
+
"github": "GITHUB_TOKEN",
|
|
1207
|
+
"supabase-anon": "NEXT_PUBLIC_SUPABASE_ANON_KEY",
|
|
1208
|
+
"supabase-service": "SUPABASE_SERVICE_ROLE_KEY",
|
|
1209
|
+
"supabase-db": "SUPABASE_DB_URL",
|
|
1210
|
+
"vercel": "VERCEL_TOKEN",
|
|
1211
|
+
"anthropic": "ANTHROPIC_API_KEY",
|
|
1212
|
+
"cloudflare": "CLOUDFLARE_API_TOKEN",
|
|
1213
|
+
"r2": "R2_ACCESS_KEY_ID",
|
|
1214
|
+
"r2-bucket": "R2_BUCKET_NAME",
|
|
1215
|
+
"neo4j": "NEO4J_URI",
|
|
1216
|
+
"rocketreach": "ROCKETREACH_API_KEY",
|
|
1217
|
+
"npm": "NPM_TOKEN",
|
|
1218
|
+
"namecheap": "NAMECHEAP_API_KEY",
|
|
1219
|
+
"gmail": "GMAIL_CREDENTIALS",
|
|
1220
|
+
};
|
|
1221
|
+
|
|
1222
|
+
const MCP_TOOLS = [
|
|
1223
|
+
{
|
|
1224
|
+
name: "clauth_ping",
|
|
1225
|
+
description: "Check if the vault is locked or unlocked, show failure count",
|
|
1226
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false }
|
|
1227
|
+
},
|
|
1228
|
+
{
|
|
1229
|
+
name: "clauth_unlock",
|
|
1230
|
+
description: "Unlock the vault with the master password (password stays in MCP server memory only)",
|
|
1231
|
+
inputSchema: { type: "object", properties: { password: { type: "string", description: "clauth master password" } }, required: ["password"], additionalProperties: false }
|
|
1232
|
+
},
|
|
1233
|
+
{
|
|
1234
|
+
name: "clauth_lock",
|
|
1235
|
+
description: "Lock the vault — clears password from memory",
|
|
1236
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false }
|
|
1237
|
+
},
|
|
1238
|
+
{
|
|
1239
|
+
name: "clauth_status",
|
|
1240
|
+
description: "List all services with type, enabled state, key presence, and last retrieval time",
|
|
1241
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false }
|
|
1242
|
+
},
|
|
1243
|
+
{
|
|
1244
|
+
name: "clauth_list",
|
|
1245
|
+
description: "List registered service names",
|
|
1246
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false }
|
|
1247
|
+
},
|
|
1248
|
+
{
|
|
1249
|
+
name: "clauth_get",
|
|
1250
|
+
description: "Retrieve a secret and deliver to a temp file (default), clipboard, or stdout. Temp files auto-delete after 30 seconds.",
|
|
1251
|
+
inputSchema: {
|
|
1252
|
+
type: "object",
|
|
1253
|
+
properties: {
|
|
1254
|
+
service: { type: "string", description: "Service name (e.g. github, anthropic, vercel)" },
|
|
1255
|
+
target: { type: "string", enum: ["file", "clipboard", "stdout"], default: "file", description: "Where to deliver the secret. 'file' writes a temp file, 'clipboard' copies to clipboard, 'stdout' returns the value in the response (DANGEROUS — enters transcript)" }
|
|
1256
|
+
},
|
|
1257
|
+
required: ["service"],
|
|
1258
|
+
additionalProperties: false
|
|
1259
|
+
}
|
|
1260
|
+
},
|
|
1261
|
+
{
|
|
1262
|
+
name: "clauth_inject",
|
|
1263
|
+
description: "Bulk retrieve multiple services into a single sourceable env file. Usage: source the returned file path before running commands.",
|
|
1264
|
+
inputSchema: {
|
|
1265
|
+
type: "object",
|
|
1266
|
+
properties: {
|
|
1267
|
+
services: { type: "array", items: { type: "string" }, description: "Service names to inject (e.g. ['github', 'vercel', 'anthropic'])" }
|
|
1268
|
+
},
|
|
1269
|
+
required: ["services"],
|
|
1270
|
+
additionalProperties: false
|
|
1271
|
+
}
|
|
1272
|
+
},
|
|
1273
|
+
{
|
|
1274
|
+
name: "clauth_enable",
|
|
1275
|
+
description: "Enable a service in the vault",
|
|
1276
|
+
inputSchema: { type: "object", properties: { service: { type: "string" } }, required: ["service"], additionalProperties: false }
|
|
1277
|
+
},
|
|
1278
|
+
{
|
|
1279
|
+
name: "clauth_disable",
|
|
1280
|
+
description: "Disable a service in the vault",
|
|
1281
|
+
inputSchema: { type: "object", properties: { service: { type: "string" } }, required: ["service"], additionalProperties: false }
|
|
1282
|
+
},
|
|
1283
|
+
{
|
|
1284
|
+
name: "clauth_test",
|
|
1285
|
+
description: "Test the HMAC handshake without retrieving any keys",
|
|
1286
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false }
|
|
1287
|
+
},
|
|
1288
|
+
{
|
|
1289
|
+
name: "clauth_scrub",
|
|
1290
|
+
description: "Scrub secrets from Claude Code transcript .jsonl files",
|
|
1291
|
+
inputSchema: {
|
|
1292
|
+
type: "object",
|
|
1293
|
+
properties: {
|
|
1294
|
+
target: { type: "string", enum: ["latest", "all"], default: "latest", description: "Scrub the most recent transcript or all transcripts" }
|
|
1295
|
+
},
|
|
1296
|
+
additionalProperties: false
|
|
1297
|
+
}
|
|
1298
|
+
},
|
|
1299
|
+
];
|
|
1300
|
+
|
|
1301
|
+
function writeTempSecret(service, value) {
|
|
1302
|
+
const filePath = path.join(os.tmpdir(), `.clauth-${service}`);
|
|
1303
|
+
fs.writeFileSync(filePath, value, { mode: 0o600 });
|
|
1304
|
+
setTimeout(() => { try { fs.unlinkSync(filePath); } catch {} }, 30_000);
|
|
1305
|
+
return filePath;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
function copyToClipboard(value) {
|
|
1309
|
+
const platform = os.platform();
|
|
1310
|
+
if (platform === "win32") {
|
|
1311
|
+
execSync("clip", { input: value, stdio: ["pipe", "pipe", "pipe"] });
|
|
1312
|
+
} else if (platform === "darwin") {
|
|
1313
|
+
execSync("pbcopy", { input: value, stdio: ["pipe", "pipe", "pipe"] });
|
|
1314
|
+
} else {
|
|
1315
|
+
execSync("xclip -selection clipboard", { input: value, stdio: ["pipe", "pipe", "pipe"] });
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
function mcpResult(text) {
|
|
1320
|
+
return { content: [{ type: "text", text }] };
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
function mcpError(text) {
|
|
1324
|
+
return { content: [{ type: "text", text }], isError: true };
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
async function handleMcpTool(vault, name, args) {
|
|
1328
|
+
switch (name) {
|
|
1329
|
+
case "clauth_ping": {
|
|
1330
|
+
return mcpResult(
|
|
1331
|
+
vault.password
|
|
1332
|
+
? `unlocked | pid: ${process.pid} | failures: ${vault.failCount}/${vault.MAX_FAILS}`
|
|
1333
|
+
: `locked | pid: ${process.pid} | failures: ${vault.failCount}/${vault.MAX_FAILS}`
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
case "clauth_unlock": {
|
|
1338
|
+
const pw = args.password;
|
|
1339
|
+
if (!pw) return mcpError("password is required");
|
|
1340
|
+
try {
|
|
1341
|
+
const { token, timestamp } = deriveToken(pw, vault.machineHash);
|
|
1342
|
+
const result = await api.test(pw, vault.machineHash, token, timestamp);
|
|
1343
|
+
if (result.error) return mcpError(`Unlock failed: ${result.error}`);
|
|
1344
|
+
vault.password = pw;
|
|
1345
|
+
return mcpResult("Vault unlocked");
|
|
1346
|
+
} catch (err) {
|
|
1347
|
+
return mcpError(`Unlock failed: ${err.message}`);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
case "clauth_lock": {
|
|
1352
|
+
vault.password = null;
|
|
1353
|
+
return mcpResult("Vault locked — password cleared from memory");
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
case "clauth_status": {
|
|
1357
|
+
if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
|
|
1358
|
+
try {
|
|
1359
|
+
const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
|
|
1360
|
+
const result = await api.status(vault.password, vault.machineHash, token, timestamp);
|
|
1361
|
+
if (result.error) return mcpError(result.error);
|
|
1362
|
+
let services = result.services || [];
|
|
1363
|
+
if (vault.whitelist) {
|
|
1364
|
+
services = services.filter(s => vault.whitelist.includes(s.name.toLowerCase()));
|
|
1365
|
+
}
|
|
1366
|
+
const lines = ["SERVICE TYPE STATUS KEY LAST RETRIEVED",
|
|
1367
|
+
"--- ---- ------ --- --------------"];
|
|
1368
|
+
for (const s of services) {
|
|
1369
|
+
const status = s.enabled ? "ACTIVE" : (s.vault_key ? "SUSPENDED" : "NO KEY");
|
|
1370
|
+
const hasKey = s.vault_key ? "yes" : "—";
|
|
1371
|
+
const lastGet = s.last_retrieved ? new Date(s.last_retrieved).toLocaleDateString() : "never";
|
|
1372
|
+
lines.push(`${s.name.padEnd(20)} ${(s.key_type || "").padEnd(12)} ${status.padEnd(12)} ${hasKey.padEnd(6)} ${lastGet}`);
|
|
1373
|
+
}
|
|
1374
|
+
return mcpResult(lines.join("\n"));
|
|
1375
|
+
} catch (err) {
|
|
1376
|
+
return mcpError(err.message);
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
case "clauth_list": {
|
|
1381
|
+
if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
|
|
1382
|
+
try {
|
|
1383
|
+
const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
|
|
1384
|
+
const result = await api.status(vault.password, vault.machineHash, token, timestamp);
|
|
1385
|
+
if (result.error) return mcpError(result.error);
|
|
1386
|
+
let services = result.services || [];
|
|
1387
|
+
if (vault.whitelist) {
|
|
1388
|
+
services = services.filter(s => vault.whitelist.includes(s.name.toLowerCase()));
|
|
1389
|
+
}
|
|
1390
|
+
return mcpResult(services.map(s => s.name).join(", "));
|
|
1391
|
+
} catch (err) {
|
|
1392
|
+
return mcpError(err.message);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
case "clauth_get": {
|
|
1397
|
+
if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
|
|
1398
|
+
const service = (args.service || "").toLowerCase();
|
|
1399
|
+
const target = args.target || "file";
|
|
1400
|
+
if (!service) return mcpError("service is required");
|
|
1401
|
+
if (vault.whitelist && !vault.whitelist.includes(service)) {
|
|
1402
|
+
return mcpError(`Service '${service}' not in whitelist`);
|
|
1403
|
+
}
|
|
1404
|
+
try {
|
|
1405
|
+
const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
|
|
1406
|
+
const result = await api.retrieve(vault.password, vault.machineHash, token, timestamp, service);
|
|
1407
|
+
if (result.error) return mcpError(result.error);
|
|
1408
|
+
const value = typeof result.value === "string" ? result.value : JSON.stringify(result.value);
|
|
1409
|
+
|
|
1410
|
+
if (target === "clipboard") {
|
|
1411
|
+
try {
|
|
1412
|
+
copyToClipboard(value);
|
|
1413
|
+
return mcpResult(`${service} copied to clipboard`);
|
|
1414
|
+
} catch (err) {
|
|
1415
|
+
return mcpError(`Clipboard failed: ${err.message}. Use target 'file' instead.`);
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
if (target === "stdout") {
|
|
1420
|
+
return {
|
|
1421
|
+
content: [{ type: "text", text: `WARNING: secret in response — will appear in transcript. Use 'file' or 'clipboard' mode instead.\n\n${value}` }],
|
|
1422
|
+
isError: true
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
// Default: file
|
|
1427
|
+
const envVar = ENV_MAP[service] || service.toUpperCase().replace(/-/g, "_");
|
|
1428
|
+
const filePath = writeTempSecret(service, value);
|
|
1429
|
+
return mcpResult(`${service} → ${filePath} (auto-deletes in 30s)\nEnv var: ${envVar}\nUsage: export ${envVar}=$(cat ${filePath.replace(/\\/g, "/")})`);
|
|
1430
|
+
} catch (err) {
|
|
1431
|
+
return mcpError(err.message);
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
case "clauth_inject": {
|
|
1436
|
+
if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
|
|
1437
|
+
const services = args.services || [];
|
|
1438
|
+
if (!services.length) return mcpError("services array is required");
|
|
1439
|
+
|
|
1440
|
+
const lines = [];
|
|
1441
|
+
const errors = [];
|
|
1442
|
+
for (const svc of services) {
|
|
1443
|
+
const service = svc.toLowerCase();
|
|
1444
|
+
if (vault.whitelist && !vault.whitelist.includes(service)) {
|
|
1445
|
+
errors.push(`${service}: not in whitelist`);
|
|
1446
|
+
continue;
|
|
1447
|
+
}
|
|
1448
|
+
try {
|
|
1449
|
+
const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
|
|
1450
|
+
const result = await api.retrieve(vault.password, vault.machineHash, token, timestamp, service);
|
|
1451
|
+
if (result.error) { errors.push(`${service}: ${result.error}`); continue; }
|
|
1452
|
+
const value = typeof result.value === "string" ? result.value : JSON.stringify(result.value);
|
|
1453
|
+
const envVar = ENV_MAP[service] || service.toUpperCase().replace(/-/g, "_");
|
|
1454
|
+
lines.push(`export ${envVar}="${value.replace(/"/g, '\\"')}"`);
|
|
1455
|
+
} catch (err) {
|
|
1456
|
+
errors.push(`${service}: ${err.message}`);
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
if (!lines.length) return mcpError(`No services retrieved:\n${errors.join("\n")}`);
|
|
1461
|
+
|
|
1462
|
+
const envFilePath = path.join(os.tmpdir(), ".clauth-env");
|
|
1463
|
+
fs.writeFileSync(envFilePath, lines.join("\n") + "\n", { mode: 0o600 });
|
|
1464
|
+
setTimeout(() => { try { fs.unlinkSync(envFilePath); } catch {} }, 30_000);
|
|
1465
|
+
|
|
1466
|
+
let msg = `${lines.length} service(s) → ${envFilePath.replace(/\\/g, "/")} (auto-deletes in 30s)\nUsage: source ${envFilePath.replace(/\\/g, "/")}`;
|
|
1467
|
+
if (errors.length) msg += `\n\nErrors:\n${errors.join("\n")}`;
|
|
1468
|
+
return mcpResult(msg);
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
case "clauth_enable": {
|
|
1472
|
+
if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
|
|
1473
|
+
const service = (args.service || "").toLowerCase();
|
|
1474
|
+
if (!service) return mcpError("service is required");
|
|
1475
|
+
try {
|
|
1476
|
+
const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
|
|
1477
|
+
const result = await api.enable(vault.password, vault.machineHash, token, timestamp, service, true);
|
|
1478
|
+
if (result.error) return mcpError(result.error);
|
|
1479
|
+
return mcpResult(`${service} enabled`);
|
|
1480
|
+
} catch (err) {
|
|
1481
|
+
return mcpError(err.message);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
case "clauth_disable": {
|
|
1486
|
+
if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
|
|
1487
|
+
const service = (args.service || "").toLowerCase();
|
|
1488
|
+
if (!service) return mcpError("service is required");
|
|
1489
|
+
try {
|
|
1490
|
+
const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
|
|
1491
|
+
const result = await api.enable(vault.password, vault.machineHash, token, timestamp, service, false);
|
|
1492
|
+
if (result.error) return mcpError(result.error);
|
|
1493
|
+
return mcpResult(`${service} disabled`);
|
|
1494
|
+
} catch (err) {
|
|
1495
|
+
return mcpError(err.message);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
case "clauth_test": {
|
|
1500
|
+
if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
|
|
1501
|
+
try {
|
|
1502
|
+
const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
|
|
1503
|
+
const result = await api.test(vault.password, vault.machineHash, token, timestamp);
|
|
1504
|
+
if (result.error) return mcpError(`FAIL: ${result.error}`);
|
|
1505
|
+
return mcpResult(`PASS — machine: ${vault.machineHash.slice(0, 16)}... | window: ${new Date(result.timestamp).toISOString()}`);
|
|
1506
|
+
} catch (err) {
|
|
1507
|
+
return mcpError(`FAIL: ${err.message}`);
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
case "clauth_scrub": {
|
|
1512
|
+
const target = args.target || "latest";
|
|
1513
|
+
try {
|
|
1514
|
+
const { runScrub: doScrub } = await import("./scrub.js");
|
|
1515
|
+
// runScrub writes to stdout via chalk — capture isn't clean in MCP mode.
|
|
1516
|
+
// Call it and trust it works; report success.
|
|
1517
|
+
await doScrub(target === "all" ? "all" : undefined, {});
|
|
1518
|
+
return mcpResult(`Scrub complete (target: ${target})`);
|
|
1519
|
+
} catch (err) {
|
|
1520
|
+
return mcpError(`Scrub failed: ${err.message}`);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
default:
|
|
1525
|
+
return mcpError(`Unknown tool: ${name}`);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
function createMcpServer(initPassword, whitelist) {
|
|
1530
|
+
// Ensure wmic is reachable — bash shells on Windows may not have
|
|
1531
|
+
// C:\Windows\System32\Wbem on PATH, which getMachineHash() needs.
|
|
1532
|
+
if (os.platform() === "win32" && !process.env.PATH?.includes("Wbem")) {
|
|
1533
|
+
process.env.PATH = (process.env.PATH || "") + ";C:\\Windows\\System32\\Wbem";
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
// Lazy-init machineHash — defer to first tool call that needs auth.
|
|
1537
|
+
let _machineHash = null;
|
|
1538
|
+
function ensureMachineHash() {
|
|
1539
|
+
if (!_machineHash) _machineHash = getMachineHash();
|
|
1540
|
+
return _machineHash;
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
const vault = {
|
|
1544
|
+
password: initPassword || null,
|
|
1545
|
+
get machineHash() { return ensureMachineHash(); },
|
|
1546
|
+
whitelist,
|
|
1547
|
+
failCount: 0,
|
|
1548
|
+
MAX_FAILS: 3,
|
|
1549
|
+
};
|
|
1550
|
+
|
|
1551
|
+
const rl = createInterface({ input: process.stdin, terminal: false });
|
|
1552
|
+
|
|
1553
|
+
function send(msg) {
|
|
1554
|
+
process.stdout.write(JSON.stringify(msg) + "\n");
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
rl.on("line", async (line) => {
|
|
1558
|
+
let msg;
|
|
1559
|
+
try {
|
|
1560
|
+
msg = JSON.parse(line);
|
|
1561
|
+
} catch {
|
|
1562
|
+
send({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } });
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
const id = msg.id;
|
|
1567
|
+
|
|
1568
|
+
// notifications (no id) — handle initialized, etc.
|
|
1569
|
+
if (msg.method === "notifications/initialized" || msg.method === "initialized") {
|
|
1570
|
+
return; // no response needed for notifications
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
if (msg.method === "initialize") {
|
|
1574
|
+
return send({
|
|
1575
|
+
jsonrpc: "2.0", id,
|
|
1576
|
+
result: {
|
|
1577
|
+
protocolVersion: "2024-11-05",
|
|
1578
|
+
serverInfo: { name: "clauth", version: VERSION },
|
|
1579
|
+
capabilities: { tools: {} }
|
|
1580
|
+
}
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
if (msg.method === "tools/list") {
|
|
1585
|
+
return send({
|
|
1586
|
+
jsonrpc: "2.0", id,
|
|
1587
|
+
result: { tools: MCP_TOOLS }
|
|
1588
|
+
});
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
if (msg.method === "tools/call") {
|
|
1592
|
+
const { name, arguments: args } = msg.params || {};
|
|
1593
|
+
try {
|
|
1594
|
+
const result = await handleMcpTool(vault, name, args || {});
|
|
1595
|
+
return send({ jsonrpc: "2.0", id, result });
|
|
1596
|
+
} catch (err) {
|
|
1597
|
+
return send({ jsonrpc: "2.0", id, result: mcpError(`Internal error: ${err.message}`) });
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
// Unknown method
|
|
1602
|
+
send({ jsonrpc: "2.0", id, error: { code: -32601, message: `Unknown method: ${msg.method}` } });
|
|
1603
|
+
});
|
|
1604
|
+
|
|
1605
|
+
rl.on("close", () => {
|
|
1606
|
+
process.exit(0);
|
|
1607
|
+
});
|
|
1608
|
+
|
|
1609
|
+
// Prevent unhandled errors from crashing the MCP server
|
|
1610
|
+
process.on("uncaughtException", (err) => {
|
|
1611
|
+
const msg = `[${new Date().toISOString()}] MCP uncaught: ${err.message}\n`;
|
|
1612
|
+
try { fs.appendFileSync(LOG_FILE, msg); } catch {}
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
async function actionMcp(opts) {
|
|
1617
|
+
const password = opts.pw || null;
|
|
1618
|
+
const whitelist = opts.services
|
|
1619
|
+
? opts.services.split(",").map(s => s.trim().toLowerCase())
|
|
1620
|
+
: null;
|
|
1621
|
+
|
|
1622
|
+
if (password) {
|
|
1623
|
+
try {
|
|
1624
|
+
await verifyAuth(password);
|
|
1625
|
+
} catch (err) {
|
|
1626
|
+
// Can't use stderr for errors in MCP mode without careful framing
|
|
1627
|
+
// Write to log file instead
|
|
1628
|
+
const msg = `[${new Date().toISOString()}] MCP auth failed: ${err.message}\n`;
|
|
1629
|
+
try { fs.appendFileSync(LOG_FILE, msg); } catch {}
|
|
1630
|
+
process.exit(1);
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
createMcpServer(password, whitelist);
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1198
1637
|
// ── Export ────────────────────────────────────────────────────
|
|
1199
1638
|
export async function runServe(opts) {
|
|
1200
1639
|
const action = opts.action || "foreground";
|
|
1201
1640
|
|
|
1202
1641
|
switch (action) {
|
|
1203
|
-
case "start":
|
|
1204
|
-
case "stop":
|
|
1205
|
-
case "restart":
|
|
1206
|
-
case "ping":
|
|
1642
|
+
case "start": return actionStart(opts);
|
|
1643
|
+
case "stop": return actionStop();
|
|
1644
|
+
case "restart": return actionRestart(opts);
|
|
1645
|
+
case "ping": return actionPing();
|
|
1207
1646
|
case "foreground": return actionForeground(opts);
|
|
1647
|
+
case "mcp": return actionMcp(opts);
|
|
1208
1648
|
default:
|
|
1209
1649
|
console.log(chalk.red(`\n Unknown serve action: ${action}`));
|
|
1210
|
-
console.log(chalk.gray(" Actions: start | stop | restart | ping | foreground\n"));
|
|
1650
|
+
console.log(chalk.gray(" Actions: start | stop | restart | ping | foreground | mcp\n"));
|
|
1211
1651
|
process.exit(1);
|
|
1212
1652
|
}
|
|
1213
1653
|
}
|