@lifeaitools/clauth 1.5.82 → 1.5.84
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 +6 -5
- package/README.md +9 -5
- package/cli/commands/npm.js +123 -0
- package/cli/commands/serve.js +259 -49
- package/cli/index.js +134 -0
- package/package.json +1 -1
package/.clauth-skill/SKILL.md
CHANGED
|
@@ -141,11 +141,12 @@ See `references/keys-guide.md` for where to find every credential.
|
|
|
141
141
|
```
|
|
142
142
|
clauth install [--ref R] [--pat P] First-time: provision Supabase + install skill
|
|
143
143
|
clauth setup [--admin-token T] [-p P] Register this machine
|
|
144
|
-
clauth status [-p P] All services + state
|
|
145
|
-
clauth test [-p P] Verify HMAC connection
|
|
146
|
-
clauth list [-p P] Service names
|
|
147
|
-
|
|
148
|
-
|
|
144
|
+
clauth status [-p P] All services + state
|
|
145
|
+
clauth test [-p P] Verify HMAC connection
|
|
146
|
+
clauth list [-p P] Service names
|
|
147
|
+
clauth search <query> [-p P] Search names, metadata, and redacted server addresses
|
|
148
|
+
|
|
149
|
+
clauth write key <service> [-p P] Store a credential
|
|
149
150
|
clauth write pw [-p P] Change password
|
|
150
151
|
clauth enable <svc|all> [-p P] Activate service
|
|
151
152
|
clauth disable <svc|all> [-p P] Suspend service
|
package/README.md
CHANGED
|
@@ -72,16 +72,20 @@ clauth get github
|
|
|
72
72
|
```
|
|
73
73
|
clauth install Provision Supabase + install Claude skill
|
|
74
74
|
clauth setup Register this machine with the vault
|
|
75
|
-
clauth status All services + state
|
|
76
|
-
clauth
|
|
75
|
+
clauth status All services + state
|
|
76
|
+
clauth search <query> Find services by name, project, description, or redacted address
|
|
77
|
+
clauth test Verify connection
|
|
77
78
|
|
|
78
79
|
clauth write key <service> Store a credential
|
|
79
80
|
clauth write pw Change password
|
|
80
81
|
clauth enable <svc|all> Activate service
|
|
81
82
|
clauth disable <svc|all> Suspend service
|
|
82
|
-
clauth get <service> Retrieve a key
|
|
83
|
-
|
|
84
|
-
clauth
|
|
83
|
+
clauth get <service> Retrieve a key
|
|
84
|
+
clauth npm whoami Verify npm token without PowerShell secret plumbing
|
|
85
|
+
clauth npm sync-github-secret LIFEAI/rdc-skills
|
|
86
|
+
Update GitHub NPM_TOKEN from clauth
|
|
87
|
+
|
|
88
|
+
clauth add service <n> Register new service
|
|
85
89
|
clauth remove service <n> Remove service
|
|
86
90
|
clauth revoke <svc|all> Delete key (destructive)
|
|
87
91
|
```
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { spawnSync } from "child_process";
|
|
5
|
+
|
|
6
|
+
const CLAUTH_NPM_URL = "http://127.0.0.1:52437/v/npm";
|
|
7
|
+
|
|
8
|
+
function run(cmd, args, opts = {}) {
|
|
9
|
+
const useCmd = process.platform === "win32" && ["npm", "gh"].includes(cmd);
|
|
10
|
+
const executable = useCmd ? "cmd.exe" : cmd;
|
|
11
|
+
const finalArgs = useCmd ? ["/d", "/s", "/c", cmd, ...args] : args;
|
|
12
|
+
const result = spawnSync(executable, finalArgs, {
|
|
13
|
+
encoding: "utf8",
|
|
14
|
+
...opts,
|
|
15
|
+
env: { ...process.env, ...(opts.env || {}) }
|
|
16
|
+
});
|
|
17
|
+
if (result.error) throw result.error;
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function redactNpmTokenList(text) {
|
|
22
|
+
return String(text || "").replace(/npm_[A-Za-z0-9]+/g, token => `${token.slice(0, 9)}...${token.slice(-4)}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function fetchNpmToken() {
|
|
26
|
+
const response = await fetch(CLAUTH_NPM_URL);
|
|
27
|
+
if (!response.ok) throw new Error(`failed to fetch npm token from clauth daemon: HTTP ${response.status}`);
|
|
28
|
+
const token = (await response.text()).trim();
|
|
29
|
+
if (!token) throw new Error("clauth npm token is empty");
|
|
30
|
+
if (!token.startsWith("npm_")) throw new Error("clauth npm token does not look like an npm token");
|
|
31
|
+
return token;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function withNpmAuth(token, fn) {
|
|
35
|
+
const npmrc = path.join(os.tmpdir(), `clauth-npm-${process.pid}-${Date.now()}.npmrc`);
|
|
36
|
+
try {
|
|
37
|
+
fs.writeFileSync(npmrc, `//registry.npmjs.org/:_authToken=${token}`, { encoding: "utf8", mode: 0o600 });
|
|
38
|
+
return fn(npmrc);
|
|
39
|
+
} finally {
|
|
40
|
+
try { fs.rmSync(npmrc, { force: true }); } catch {}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function runNpmWithToken(token, args) {
|
|
45
|
+
return withNpmAuth(token, npmrc => run("npm", ["--userconfig", npmrc, ...args]));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function printResult(result, { redact = true } = {}) {
|
|
49
|
+
const stdout = redact ? redactNpmTokenList(result.stdout) : result.stdout;
|
|
50
|
+
const stderr = redact ? redactNpmTokenList(result.stderr) : result.stderr;
|
|
51
|
+
if (stdout) process.stdout.write(stdout);
|
|
52
|
+
if (stderr) process.stderr.write(stderr);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function usage() {
|
|
56
|
+
console.log(`clauth npm <action>
|
|
57
|
+
|
|
58
|
+
Actions:
|
|
59
|
+
whoami Verify the clauth npm token identity
|
|
60
|
+
tokens List npm token metadata with token strings redacted
|
|
61
|
+
set-local Write the clauth npm token to the user npm config
|
|
62
|
+
sync-github-secret <repo> Set repo secret NPM_TOKEN from clauth, e.g. LIFEAI/rdc-skills
|
|
63
|
+
rerun <run-id> --repo <repo> Rerun a failed GitHub Actions workflow
|
|
64
|
+
`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function runNpm(action = "help", opts = {}) {
|
|
68
|
+
if (action === "help") {
|
|
69
|
+
usage();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const token = await fetchNpmToken();
|
|
74
|
+
|
|
75
|
+
if (action === "whoami") {
|
|
76
|
+
const result = runNpmWithToken(token, ["whoami", "--registry=https://registry.npmjs.org/"]);
|
|
77
|
+
printResult(result);
|
|
78
|
+
if (result.status !== 0) process.exitCode = result.status;
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (action === "tokens") {
|
|
83
|
+
const result = runNpmWithToken(token, ["token", "list", "--json", "--registry=https://registry.npmjs.org/"]);
|
|
84
|
+
printResult(result);
|
|
85
|
+
if (result.status !== 0) process.exitCode = result.status;
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (action === "set-local") {
|
|
90
|
+
const result = run("npm", ["config", "set", "//registry.npmjs.org/:_authToken", token, "--location=user"]);
|
|
91
|
+
if (result.status !== 0) {
|
|
92
|
+
printResult(result);
|
|
93
|
+
process.exitCode = result.status;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
console.log("local npm auth updated from clauth service 'npm'");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (action === "sync-github-secret") {
|
|
101
|
+
const repo = opts.repo || opts.args?.[0];
|
|
102
|
+
if (!repo) throw new Error("repo is required, e.g. clauth npm sync-github-secret LIFEAI/rdc-skills");
|
|
103
|
+
const result = run("gh", ["secret", "set", "NPM_TOKEN", "--repo", repo], { input: token });
|
|
104
|
+
printResult(result);
|
|
105
|
+
if (result.status !== 0) process.exitCode = result.status;
|
|
106
|
+
else console.log(`GitHub secret NPM_TOKEN updated for ${repo}`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (action === "rerun") {
|
|
111
|
+
const runId = opts.args?.[0];
|
|
112
|
+
const repo = opts.repo;
|
|
113
|
+
if (!runId || !repo) throw new Error("usage: clauth npm rerun <run-id> --repo LIFEAI/rdc-skills");
|
|
114
|
+
const result = run("gh", ["run", "rerun", runId, "--repo", repo, "--failed"]);
|
|
115
|
+
printResult(result);
|
|
116
|
+
if (result.status !== 0) process.exitCode = result.status;
|
|
117
|
+
else console.log(`rerun requested for ${repo} run ${runId}`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
usage();
|
|
122
|
+
throw new Error(`unknown clauth npm action: ${action}`);
|
|
123
|
+
}
|
package/cli/commands/serve.js
CHANGED
|
@@ -607,9 +607,14 @@ function dashboardHtml(port, whitelist, isStaged = false) {
|
|
|
607
607
|
.project-tab{background:none;border:none;border-bottom:2px solid transparent;color:#64748b;padding:8px 16px;font-size:.82rem;font-weight:500;cursor:pointer;white-space:nowrap;transition:all .15s}
|
|
608
608
|
.project-tab:hover{color:#94a3b8;background:rgba(59,130,246,.05)}
|
|
609
609
|
.project-tab.active{color:#60a5fa;border-bottom-color:#3b82f6;background:rgba(59,130,246,.08)}
|
|
610
|
-
.project-tab .tab-count{font-size:.7rem;color:#475569;margin-left:4px;font-weight:400}
|
|
611
|
-
.project-tab.active .tab-count{color:#3b82f6}
|
|
612
|
-
.
|
|
610
|
+
.project-tab .tab-count{font-size:.7rem;color:#475569;margin-left:4px;font-weight:400}
|
|
611
|
+
.project-tab.active .tab-count{color:#3b82f6}
|
|
612
|
+
.service-search{display:flex;align-items:center;gap:10px;margin:-.35rem 0 1rem;background:#0f172a;border:1px solid #1e293b;border-radius:8px;padding:9px 12px}
|
|
613
|
+
.service-search-label{font-size:.76rem;color:#64748b;font-weight:600;letter-spacing:.02em;text-transform:uppercase;white-space:nowrap}
|
|
614
|
+
.service-search-input{flex:1;min-width:180px;background:#0a0f1a;border:1px solid #334155;border-radius:6px;color:#e2e8f0;font-family:'Courier New',monospace;font-size:.88rem;padding:8px 11px;outline:none;transition:border-color .15s}
|
|
615
|
+
.service-search-input:focus{border-color:#3b82f6}
|
|
616
|
+
.service-search-count{font-size:.78rem;color:#64748b;white-space:nowrap}
|
|
617
|
+
.project-edit{display:none;margin-top:8px;padding:8px 10px;background:#0f172a;border:1px solid #334155;border-radius:6px}
|
|
613
618
|
.project-edit.open{display:flex;gap:6px;align-items:center}
|
|
614
619
|
.project-edit input{background:#1e293b;border:1px solid #334155;border-radius:4px;color:#e2e8f0;font-size:.78rem;padding:4px 8px;outline:none;flex:1;font-family:'Courier New',monospace;transition:border-color .15s}
|
|
615
620
|
.project-edit input:focus{border-color:#3b82f6}
|
|
@@ -863,8 +868,13 @@ function dashboardHtml(port, whitelist, isStaged = false) {
|
|
|
863
868
|
<div class="wizard-foot" id="wizard-foot"></div>
|
|
864
869
|
</div>
|
|
865
870
|
|
|
866
|
-
<div id="project-tabs" class="project-tabs" style="display:none"></div>
|
|
867
|
-
<div id="
|
|
871
|
+
<div id="project-tabs" class="project-tabs" style="display:none"></div>
|
|
872
|
+
<div id="service-search" class="service-search">
|
|
873
|
+
<span class="service-search-label">Search</span>
|
|
874
|
+
<input id="service-search-input" class="service-search-input" type="search" placeholder="service name or display name" autocomplete="off" spellcheck="false" oninput="setServiceSearch(this.value)">
|
|
875
|
+
<span id="service-search-count" class="service-search-count"></span>
|
|
876
|
+
</div>
|
|
877
|
+
<div id="grid" class="grid"><p class="loading">Loading services…</p></div>
|
|
868
878
|
<div class="footer">localhost:${port} · 127.0.0.1 only · 10-strike lockout</div>
|
|
869
879
|
</div>
|
|
870
880
|
|
|
@@ -1158,8 +1168,25 @@ async function stopDaemon() {
|
|
|
1158
1168
|
}
|
|
1159
1169
|
|
|
1160
1170
|
// ── Load services ───────────────────────────
|
|
1161
|
-
let allServices = [];
|
|
1162
|
-
let activeProjectTab = "all";
|
|
1171
|
+
let allServices = [];
|
|
1172
|
+
let activeProjectTab = "all";
|
|
1173
|
+
let serviceSearchQuery = "";
|
|
1174
|
+
|
|
1175
|
+
function serviceSort(a, b) {
|
|
1176
|
+
return String(a.name || "").localeCompare(String(b.name || ""), undefined, { sensitivity: "base", numeric: true });
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
function matchesServiceSearch(s, query) {
|
|
1180
|
+
if (!query) return true;
|
|
1181
|
+
const q = query.toLowerCase();
|
|
1182
|
+
return String(s.name || "").toLowerCase().includes(q) ||
|
|
1183
|
+
String(s.label || "").toLowerCase().includes(q);
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
function setServiceSearch(value) {
|
|
1187
|
+
serviceSearchQuery = value || "";
|
|
1188
|
+
renderServiceGrid(allServices);
|
|
1189
|
+
}
|
|
1163
1190
|
|
|
1164
1191
|
function renderProjectTabs(services) {
|
|
1165
1192
|
const tabsEl = document.getElementById("project-tabs");
|
|
@@ -1185,21 +1212,35 @@ function renderProjectTabs(services) {
|
|
|
1185
1212
|
).join("");
|
|
1186
1213
|
}
|
|
1187
1214
|
|
|
1188
|
-
function switchProjectTab(key) {
|
|
1189
|
-
activeProjectTab = key;
|
|
1190
|
-
renderProjectTabs(allServices);
|
|
1191
|
-
renderServiceGrid(allServices);
|
|
1192
|
-
}
|
|
1215
|
+
function switchProjectTab(key) {
|
|
1216
|
+
activeProjectTab = key;
|
|
1217
|
+
renderProjectTabs(allServices);
|
|
1218
|
+
renderServiceGrid(allServices);
|
|
1219
|
+
}
|
|
1193
1220
|
|
|
1194
1221
|
function renderServiceGrid(services) {
|
|
1195
1222
|
const grid = document.getElementById("grid");
|
|
1196
1223
|
let filtered = services;
|
|
1197
1224
|
if (activeProjectTab === "unassigned") {
|
|
1198
1225
|
filtered = services.filter(s => !s.project);
|
|
1199
|
-
} else if (activeProjectTab !== "all") {
|
|
1200
|
-
filtered = services.filter(s => s.project === activeProjectTab);
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1226
|
+
} else if (activeProjectTab !== "all") {
|
|
1227
|
+
filtered = services.filter(s => s.project === activeProjectTab);
|
|
1228
|
+
}
|
|
1229
|
+
filtered = filtered
|
|
1230
|
+
.filter(s => matchesServiceSearch(s, serviceSearchQuery))
|
|
1231
|
+
.slice()
|
|
1232
|
+
.sort(serviceSort);
|
|
1233
|
+
const searchCount = document.getElementById("service-search-count");
|
|
1234
|
+
if (searchCount) {
|
|
1235
|
+
const trimmed = serviceSearchQuery.trim();
|
|
1236
|
+
searchCount.textContent = trimmed ? filtered.length + " match" + (filtered.length === 1 ? "" : "es") : filtered.length + " shown";
|
|
1237
|
+
}
|
|
1238
|
+
if (!filtered.length) {
|
|
1239
|
+
grid.innerHTML = serviceSearchQuery.trim()
|
|
1240
|
+
? '<p class="loading">No services match that search.</p>'
|
|
1241
|
+
: '<p class="loading">No services in this group.</p>';
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1203
1244
|
grid.innerHTML = filtered.map(s => \`
|
|
1204
1245
|
<div class="card">
|
|
1205
1246
|
<div style="display:flex;align-items:flex-start;justify-content:space-between">
|
|
@@ -5076,8 +5117,8 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
5076
5117
|
}
|
|
5077
5118
|
|
|
5078
5119
|
// POST /update-service — update service metadata (project, label, description)
|
|
5079
|
-
if (method === "POST" && reqPath === "/update-service") {
|
|
5080
|
-
if (lockedGuard(res)) return;
|
|
5120
|
+
if (method === "POST" && reqPath === "/update-service") {
|
|
5121
|
+
if (lockedGuard(res)) return;
|
|
5081
5122
|
|
|
5082
5123
|
let body;
|
|
5083
5124
|
try { body = await readBody(req); } catch {
|
|
@@ -5108,10 +5149,39 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
5108
5149
|
return ok(res, { ok: true, service: service.toLowerCase(), ...updates });
|
|
5109
5150
|
} catch (err) {
|
|
5110
5151
|
return strike(res, 502, err.message);
|
|
5111
|
-
}
|
|
5112
|
-
}
|
|
5113
|
-
|
|
5114
|
-
|
|
5152
|
+
}
|
|
5153
|
+
}
|
|
5154
|
+
|
|
5155
|
+
if (method === "GET" && reqPath === "/search") {
|
|
5156
|
+
if (lockedGuard(res)) return;
|
|
5157
|
+
const query = (url.searchParams.get("q") || url.searchParams.get("query") || "").trim();
|
|
5158
|
+
const project = (url.searchParams.get("project") || "").trim() || undefined;
|
|
5159
|
+
const includeAddresses = url.searchParams.get("addresses") !== "false";
|
|
5160
|
+
if (!query) {
|
|
5161
|
+
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
5162
|
+
return res.end(JSON.stringify({ error: "q query parameter is required" }));
|
|
5163
|
+
}
|
|
5164
|
+
|
|
5165
|
+
try {
|
|
5166
|
+
const { token, timestamp } = deriveToken(password, machineHash);
|
|
5167
|
+
const result = await searchServices({
|
|
5168
|
+
password,
|
|
5169
|
+
machineHash,
|
|
5170
|
+
token,
|
|
5171
|
+
timestamp,
|
|
5172
|
+
project,
|
|
5173
|
+
query,
|
|
5174
|
+
includeAddresses,
|
|
5175
|
+
whitelist
|
|
5176
|
+
});
|
|
5177
|
+
if (result.error) return strike(res, 502, result.error);
|
|
5178
|
+
return ok(res, result);
|
|
5179
|
+
} catch (err) {
|
|
5180
|
+
return strike(res, 502, err.message);
|
|
5181
|
+
}
|
|
5182
|
+
}
|
|
5183
|
+
|
|
5184
|
+
// Unknown route — a wrong URL is not an auth failure. Log it, return 404,
|
|
5115
5185
|
// but do NOT increment failCount (which locks the vault at MAX_FAILS).
|
|
5116
5186
|
// Auth failures (wrong password, wrong token) still strike via /auth and /get/:service.
|
|
5117
5187
|
try {
|
|
@@ -6086,9 +6156,9 @@ function stopCodevelopSession(session_id) {
|
|
|
6086
6156
|
return { stopped: true, session_id };
|
|
6087
6157
|
}
|
|
6088
6158
|
|
|
6089
|
-
const ENV_MAP = {
|
|
6090
|
-
"github": "GITHUB_TOKEN",
|
|
6091
|
-
"supabase-anon": "NEXT_PUBLIC_SUPABASE_ANON_KEY",
|
|
6159
|
+
const ENV_MAP = {
|
|
6160
|
+
"github": "GITHUB_TOKEN",
|
|
6161
|
+
"supabase-anon": "NEXT_PUBLIC_SUPABASE_ANON_KEY",
|
|
6092
6162
|
"supabase-service": "SUPABASE_SERVICE_ROLE_KEY",
|
|
6093
6163
|
"supabase-db": "SUPABASE_DB_URL",
|
|
6094
6164
|
"vercel": "VERCEL_TOKEN",
|
|
@@ -6100,11 +6170,116 @@ const ENV_MAP = {
|
|
|
6100
6170
|
"rocketreach": "ROCKETREACH_API_KEY",
|
|
6101
6171
|
"npm": "NPM_TOKEN",
|
|
6102
6172
|
"namecheap": "NAMECHEAP_API_KEY",
|
|
6103
|
-
"gmail": "GMAIL_CREDENTIALS",
|
|
6104
|
-
};
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6173
|
+
"gmail": "GMAIL_CREDENTIALS",
|
|
6174
|
+
};
|
|
6175
|
+
|
|
6176
|
+
const ADDRESS_KEY_TYPES = new Set(["connstring", "fileserver", "oauth"]);
|
|
6177
|
+
const ADDRESS_FIELDS = new Set(["url", "uri", "host", "hostname", "server", "address", "base_url", "endpoint", "path", "root"]);
|
|
6178
|
+
|
|
6179
|
+
function normalizeSearchText(value) {
|
|
6180
|
+
return String(value || "").toLowerCase();
|
|
6181
|
+
}
|
|
6182
|
+
|
|
6183
|
+
function redactUrlish(value) {
|
|
6184
|
+
const text = String(value || "").trim();
|
|
6185
|
+
if (!text) return "";
|
|
6186
|
+
try {
|
|
6187
|
+
const url = new URL(text);
|
|
6188
|
+
if (url.username) url.username = "***";
|
|
6189
|
+
if (url.password) url.password = "***";
|
|
6190
|
+
return url.toString();
|
|
6191
|
+
} catch {
|
|
6192
|
+
return text.replace(/:\/\/([^:@/\s]+):([^@/\s]+)@/g, "://***:***@");
|
|
6193
|
+
}
|
|
6194
|
+
}
|
|
6195
|
+
|
|
6196
|
+
function collectAddressHints(value, keyType) {
|
|
6197
|
+
if (!ADDRESS_KEY_TYPES.has(String(keyType || "").toLowerCase())) return [];
|
|
6198
|
+
const hints = new Set();
|
|
6199
|
+
|
|
6200
|
+
function add(candidate) {
|
|
6201
|
+
if (candidate === undefined || candidate === null) return;
|
|
6202
|
+
const text = redactUrlish(candidate);
|
|
6203
|
+
if (text) hints.add(text);
|
|
6204
|
+
}
|
|
6205
|
+
|
|
6206
|
+
function walk(node, fieldName = "") {
|
|
6207
|
+
if (node === undefined || node === null) return;
|
|
6208
|
+
if (typeof node === "string") {
|
|
6209
|
+
if (fieldName && ADDRESS_FIELDS.has(fieldName.toLowerCase())) add(node);
|
|
6210
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(node) || /^[A-Za-z]:[\\/]/.test(node) || node.startsWith("\\\\")) add(node);
|
|
6211
|
+
return;
|
|
6212
|
+
}
|
|
6213
|
+
if (Array.isArray(node)) {
|
|
6214
|
+
for (const item of node) walk(item, fieldName);
|
|
6215
|
+
return;
|
|
6216
|
+
}
|
|
6217
|
+
if (typeof node === "object") {
|
|
6218
|
+
for (const [key, child] of Object.entries(node)) walk(child, key);
|
|
6219
|
+
}
|
|
6220
|
+
}
|
|
6221
|
+
|
|
6222
|
+
try {
|
|
6223
|
+
walk(JSON.parse(value));
|
|
6224
|
+
} catch {
|
|
6225
|
+
walk(value);
|
|
6226
|
+
}
|
|
6227
|
+
|
|
6228
|
+
return [...hints];
|
|
6229
|
+
}
|
|
6230
|
+
|
|
6231
|
+
async function searchServices({ password, machineHash, token, timestamp, query, project, includeAddresses = true, whitelist }) {
|
|
6232
|
+
const q = normalizeSearchText(query);
|
|
6233
|
+
if (!q) return { error: "query is required" };
|
|
6234
|
+
|
|
6235
|
+
const result = await api.status(password, machineHash, token, timestamp, project);
|
|
6236
|
+
if (result.error) return { error: result.error };
|
|
6237
|
+
|
|
6238
|
+
let services = result.services || [];
|
|
6239
|
+
if (whitelist) services = services.filter(s => whitelist.includes(String(s.name || "").toLowerCase()));
|
|
6240
|
+
|
|
6241
|
+
const matches = [];
|
|
6242
|
+
for (const s of services) {
|
|
6243
|
+
const fields = {
|
|
6244
|
+
name: s.name,
|
|
6245
|
+
label: s.label,
|
|
6246
|
+
project: s.project,
|
|
6247
|
+
type: s.key_type,
|
|
6248
|
+
description: s.description
|
|
6249
|
+
};
|
|
6250
|
+
const matched = Object.entries(fields)
|
|
6251
|
+
.filter(([, value]) => normalizeSearchText(value).includes(q))
|
|
6252
|
+
.map(([field]) => field);
|
|
6253
|
+
|
|
6254
|
+
let addressHints = [];
|
|
6255
|
+
if (includeAddresses && ADDRESS_KEY_TYPES.has(String(s.key_type || "").toLowerCase()) && s.vault_key) {
|
|
6256
|
+
const secret = await api.retrieve(password, machineHash, token, timestamp, s.name);
|
|
6257
|
+
if (!secret.error) {
|
|
6258
|
+
addressHints = collectAddressHints(secret.value, s.key_type);
|
|
6259
|
+
if (addressHints.some(h => normalizeSearchText(h).includes(q))) matched.push("address");
|
|
6260
|
+
}
|
|
6261
|
+
}
|
|
6262
|
+
|
|
6263
|
+
if (matched.length) {
|
|
6264
|
+
matches.push({
|
|
6265
|
+
name: s.name,
|
|
6266
|
+
label: s.label || null,
|
|
6267
|
+
project: s.project || null,
|
|
6268
|
+
key_type: s.key_type,
|
|
6269
|
+
enabled: !!s.enabled,
|
|
6270
|
+
has_key: !!s.vault_key,
|
|
6271
|
+
description: s.description || null,
|
|
6272
|
+
matched: [...new Set(matched)],
|
|
6273
|
+
address_hints: addressHints
|
|
6274
|
+
});
|
|
6275
|
+
}
|
|
6276
|
+
}
|
|
6277
|
+
|
|
6278
|
+
return { query, count: matches.length, matches };
|
|
6279
|
+
}
|
|
6280
|
+
|
|
6281
|
+
// ── Filesystem service config — loaded from clauth vault ──
|
|
6282
|
+
let _fsMountsCache = null;
|
|
6108
6283
|
let _fsMountsCacheTime = 0;
|
|
6109
6284
|
const FS_CACHE_TTL = 60000; // 1 minute
|
|
6110
6285
|
|
|
@@ -6269,14 +6444,28 @@ const MCP_TOOLS = [
|
|
|
6269
6444
|
description: "List all services with type, enabled state, key presence, and last retrieval time",
|
|
6270
6445
|
inputSchema: { type: "object", properties: {}, additionalProperties: false }
|
|
6271
6446
|
},
|
|
6272
|
-
{
|
|
6273
|
-
name: "clauth_list",
|
|
6274
|
-
description: "List registered service names",
|
|
6275
|
-
inputSchema: { type: "object", properties: {}, additionalProperties: false }
|
|
6276
|
-
},
|
|
6277
|
-
{
|
|
6278
|
-
name: "
|
|
6279
|
-
description: "
|
|
6447
|
+
{
|
|
6448
|
+
name: "clauth_list",
|
|
6449
|
+
description: "List registered service names",
|
|
6450
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false }
|
|
6451
|
+
},
|
|
6452
|
+
{
|
|
6453
|
+
name: "clauth_search",
|
|
6454
|
+
description: "Search registered services by name, label, project, description, type, or redacted address hints from address-bearing secrets",
|
|
6455
|
+
inputSchema: {
|
|
6456
|
+
type: "object",
|
|
6457
|
+
properties: {
|
|
6458
|
+
query: { type: "string", description: "Search text, such as part of a service name, project, host, URL, or filesystem path" },
|
|
6459
|
+
project: { type: "string", description: "Optional project scope" },
|
|
6460
|
+
addresses: { type: "boolean", default: true, description: "Include redacted address hints from connstring/fileserver/oauth secrets" }
|
|
6461
|
+
},
|
|
6462
|
+
required: ["query"],
|
|
6463
|
+
additionalProperties: false
|
|
6464
|
+
}
|
|
6465
|
+
},
|
|
6466
|
+
{
|
|
6467
|
+
name: "clauth_get",
|
|
6468
|
+
description: "Retrieve a secret and deliver to a temp file (default), clipboard, or stdout. Temp files auto-delete after 30 seconds.",
|
|
6280
6469
|
inputSchema: {
|
|
6281
6470
|
type: "object",
|
|
6282
6471
|
properties: {
|
|
@@ -6993,10 +7182,10 @@ async function handleMcpTool(vault, name, args) {
|
|
|
6993
7182
|
}
|
|
6994
7183
|
}
|
|
6995
7184
|
|
|
6996
|
-
case "clauth_list": {
|
|
6997
|
-
if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
|
|
6998
|
-
try {
|
|
6999
|
-
const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
|
|
7185
|
+
case "clauth_list": {
|
|
7186
|
+
if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
|
|
7187
|
+
try {
|
|
7188
|
+
const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
|
|
7000
7189
|
const result = await api.status(vault.password, vault.machineHash, token, timestamp);
|
|
7001
7190
|
if (result.error) return mcpError(result.error);
|
|
7002
7191
|
let services = result.services || [];
|
|
@@ -7005,13 +7194,34 @@ async function handleMcpTool(vault, name, args) {
|
|
|
7005
7194
|
}
|
|
7006
7195
|
return mcpResult(services.map(s => s.name).join(", "));
|
|
7007
7196
|
} catch (err) {
|
|
7008
|
-
return mcpError(err.message);
|
|
7009
|
-
}
|
|
7010
|
-
}
|
|
7011
|
-
|
|
7012
|
-
case "
|
|
7013
|
-
if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
|
|
7014
|
-
|
|
7197
|
+
return mcpError(err.message);
|
|
7198
|
+
}
|
|
7199
|
+
}
|
|
7200
|
+
|
|
7201
|
+
case "clauth_search": {
|
|
7202
|
+
if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
|
|
7203
|
+
try {
|
|
7204
|
+
const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
|
|
7205
|
+
const result = await searchServices({
|
|
7206
|
+
password: vault.password,
|
|
7207
|
+
machineHash: vault.machineHash,
|
|
7208
|
+
token,
|
|
7209
|
+
timestamp,
|
|
7210
|
+
query: args.query,
|
|
7211
|
+
project: args.project,
|
|
7212
|
+
includeAddresses: args.addresses !== false,
|
|
7213
|
+
whitelist: vault.whitelist
|
|
7214
|
+
});
|
|
7215
|
+
if (result.error) return mcpError(result.error);
|
|
7216
|
+
return mcpResult(JSON.stringify(result, null, 2));
|
|
7217
|
+
} catch (err) {
|
|
7218
|
+
return mcpError(err.message);
|
|
7219
|
+
}
|
|
7220
|
+
}
|
|
7221
|
+
|
|
7222
|
+
case "clauth_get": {
|
|
7223
|
+
if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
|
|
7224
|
+
const service = (args.service || "").toLowerCase();
|
|
7015
7225
|
const target = args.target || "file";
|
|
7016
7226
|
if (!service) return mcpError("service is required");
|
|
7017
7227
|
if (vault.whitelist && !vault.whitelist.includes(service)) {
|
package/cli/index.js
CHANGED
|
@@ -40,6 +40,97 @@ async function getAuth(pw) {
|
|
|
40
40
|
return { password, machineHash, token, timestamp };
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
const ADDRESS_KEY_TYPES = new Set(["connstring", "fileserver", "oauth"]);
|
|
44
|
+
const ADDRESS_FIELDS = new Set(["url", "uri", "host", "hostname", "server", "address", "base_url", "endpoint", "path", "root"]);
|
|
45
|
+
|
|
46
|
+
function normalizeSearchText(value) {
|
|
47
|
+
return String(value || "").toLowerCase();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function redactUrlish(value) {
|
|
51
|
+
const text = String(value || "").trim();
|
|
52
|
+
if (!text) return "";
|
|
53
|
+
try {
|
|
54
|
+
const url = new URL(text);
|
|
55
|
+
if (url.username) url.username = "***";
|
|
56
|
+
if (url.password) url.password = "***";
|
|
57
|
+
return url.toString();
|
|
58
|
+
} catch {
|
|
59
|
+
return text.replace(/:\/\/([^:@/\s]+):([^@/\s]+)@/g, "://***:***@");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function collectAddressHints(value, keyType) {
|
|
64
|
+
if (!ADDRESS_KEY_TYPES.has(String(keyType || "").toLowerCase())) return [];
|
|
65
|
+
const hints = new Set();
|
|
66
|
+
|
|
67
|
+
function add(candidate) {
|
|
68
|
+
if (candidate === undefined || candidate === null) return;
|
|
69
|
+
const text = redactUrlish(candidate);
|
|
70
|
+
if (text) hints.add(text);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function walk(node, fieldName = "") {
|
|
74
|
+
if (node === undefined || node === null) return;
|
|
75
|
+
if (typeof node === "string") {
|
|
76
|
+
if (fieldName && ADDRESS_FIELDS.has(fieldName.toLowerCase())) add(node);
|
|
77
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(node) || /^[A-Za-z]:[\\/]/.test(node) || node.startsWith("\\\\")) add(node);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (Array.isArray(node)) {
|
|
81
|
+
for (const item of node) walk(item, fieldName);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (typeof node === "object") {
|
|
85
|
+
for (const [key, child] of Object.entries(node)) walk(child, key);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
walk(JSON.parse(value));
|
|
91
|
+
} catch {
|
|
92
|
+
walk(value);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return [...hints];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function searchServices(auth, query, opts = {}) {
|
|
99
|
+
const q = normalizeSearchText(query);
|
|
100
|
+
if (!q) throw new Error("Search query is required");
|
|
101
|
+
const result = await api.status(auth.password, auth.machineHash, auth.token, auth.timestamp, opts.project);
|
|
102
|
+
if (result.error) throw new Error(result.error);
|
|
103
|
+
|
|
104
|
+
const services = result.services || [];
|
|
105
|
+
const rows = [];
|
|
106
|
+
|
|
107
|
+
for (const s of services) {
|
|
108
|
+
const fields = {
|
|
109
|
+
name: s.name,
|
|
110
|
+
label: s.label,
|
|
111
|
+
project: s.project,
|
|
112
|
+
type: s.key_type,
|
|
113
|
+
description: s.description
|
|
114
|
+
};
|
|
115
|
+
const matched = Object.entries(fields)
|
|
116
|
+
.filter(([, value]) => normalizeSearchText(value).includes(q))
|
|
117
|
+
.map(([field]) => field);
|
|
118
|
+
|
|
119
|
+
let addressHints = [];
|
|
120
|
+
if (opts.addresses !== false && ADDRESS_KEY_TYPES.has(String(s.key_type || "").toLowerCase()) && s.vault_key) {
|
|
121
|
+
const secret = await api.retrieve(auth.password, auth.machineHash, auth.token, auth.timestamp, s.name);
|
|
122
|
+
if (!secret.error) {
|
|
123
|
+
addressHints = collectAddressHints(secret.value, s.key_type);
|
|
124
|
+
if (addressHints.some(h => normalizeSearchText(h).includes(q))) matched.push("address");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (matched.length) rows.push({ ...s, matched: [...new Set(matched)], addressHints });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return rows;
|
|
132
|
+
}
|
|
133
|
+
|
|
43
134
|
// ============================================================
|
|
44
135
|
// Program
|
|
45
136
|
// ============================================================
|
|
@@ -58,6 +149,7 @@ import { runUninstall } from './commands/uninstall.js';
|
|
|
58
149
|
import { runScrub } from './commands/scrub.js';
|
|
59
150
|
import { runServe } from './commands/serve.js';
|
|
60
151
|
import { runCodevelop } from './commands/codevelop.js';
|
|
152
|
+
import { runNpm } from './commands/npm.js';
|
|
61
153
|
|
|
62
154
|
program
|
|
63
155
|
.command('install')
|
|
@@ -126,6 +218,16 @@ program
|
|
|
126
218
|
await runCodevelop({ ...opts, action });
|
|
127
219
|
});
|
|
128
220
|
|
|
221
|
+
program
|
|
222
|
+
.command("npm")
|
|
223
|
+
.description("Operate npm auth safely through the clauth npm service")
|
|
224
|
+
.argument("[action]", "whoami | tokens | set-local | sync-github-secret | rerun | help", "help")
|
|
225
|
+
.argument("[args...]", "Action arguments")
|
|
226
|
+
.option("--repo <repo>", "GitHub repo, e.g. LIFEAI/rdc-skills")
|
|
227
|
+
.action(async (action, args, opts) => {
|
|
228
|
+
await runNpm(action, { ...opts, args });
|
|
229
|
+
});
|
|
230
|
+
|
|
129
231
|
// ──────────────────────────────────────────────
|
|
130
232
|
// clauth setup
|
|
131
233
|
// ──────────────────────────────────────────────
|
|
@@ -410,6 +512,38 @@ program
|
|
|
410
512
|
console.log();
|
|
411
513
|
});
|
|
412
514
|
|
|
515
|
+
program
|
|
516
|
+
.command("search <query>")
|
|
517
|
+
.description("Search services by name, label, project, description, type, or redacted address hints")
|
|
518
|
+
.option("-p, --pw <password>")
|
|
519
|
+
.option("--project <name>", "Filter by project scope")
|
|
520
|
+
.option("--no-addresses", "Skip address-bearing secret metadata scans")
|
|
521
|
+
.action(async (query, opts) => {
|
|
522
|
+
const auth = await getAuth(opts.pw);
|
|
523
|
+
const spinner = ora("Searching services...").start();
|
|
524
|
+
try {
|
|
525
|
+
const rows = await searchServices(auth, query, { project: opts.project, addresses: opts.addresses });
|
|
526
|
+
spinner.stop();
|
|
527
|
+
console.log(chalk.cyan(`\n Search results for "${query}":\n`));
|
|
528
|
+
if (!rows.length) {
|
|
529
|
+
console.log(chalk.gray(" No matching services found.\n"));
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
console.log(chalk.bold(" " + "SERVICE".padEnd(24) + "TYPE".padEnd(12) + "PROJECT".padEnd(20) + "MATCHED"));
|
|
533
|
+
console.log(" " + "─".repeat(78));
|
|
534
|
+
for (const s of rows) {
|
|
535
|
+
const project = s.project || "global";
|
|
536
|
+
console.log(` ${chalk.bold(s.name.padEnd(24))}${String(s.key_type || "").padEnd(12)}${project.padEnd(20)}${s.matched.join(", ")}`);
|
|
537
|
+
if (s.label) console.log(chalk.gray(` label: ${s.label}`));
|
|
538
|
+
if (s.description) console.log(chalk.gray(` description: ${s.description}`));
|
|
539
|
+
for (const hint of s.addressHints || []) console.log(chalk.gray(` address: ${hint}`));
|
|
540
|
+
}
|
|
541
|
+
console.log();
|
|
542
|
+
} catch (err) {
|
|
543
|
+
spinner.fail(chalk.red(err.message));
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
|
|
413
547
|
// ──────────────────────────────────────────────
|
|
414
548
|
// clauth test <service|all>
|
|
415
549
|
// ──────────────────────────────────────────────
|