@lifeaitools/clauth 0.7.5 → 1.1.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/cli/api.js +13 -5
- package/cli/commands/doctor.js +302 -0
- package/cli/commands/install.js +113 -0
- package/cli/commands/invite.js +175 -0
- package/cli/commands/join.js +179 -0
- package/cli/commands/serve.js +1153 -230
- package/cli/commands/tunnel.js +210 -0
- package/cli/index.js +140 -16
- package/install.ps1 +7 -0
- package/install.sh +11 -0
- package/package.json +7 -2
- package/scripts/bootstrap.cjs +78 -0
- package/scripts/postinstall.js +52 -0
- package/supabase/functions/auth-vault/index.ts +27 -7
- package/supabase/migrations/003_clauth_config.sql +13 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// cli/commands/tunnel.js
|
|
2
|
+
// clauth tunnel setup — configures Cloudflare named tunnel for claude.ai web integration
|
|
3
|
+
|
|
4
|
+
import { execSync, spawnSync } from "child_process";
|
|
5
|
+
import os from "os";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
import readline from "readline";
|
|
9
|
+
|
|
10
|
+
function ask(question) {
|
|
11
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
12
|
+
return new Promise(resolve => rl.question(question, ans => { rl.close(); resolve(ans.trim()); }));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function actionTunnelSetup() {
|
|
16
|
+
console.log("\n clauth tunnel setup\n");
|
|
17
|
+
console.log(" This wizard configures a Cloudflare named tunnel so claude.ai web");
|
|
18
|
+
console.log(" can connect to your local clauth daemon without exposing any ports.\n");
|
|
19
|
+
|
|
20
|
+
// Step 1: Check cloudflared is installed
|
|
21
|
+
try {
|
|
22
|
+
execSync("cloudflared --version", { stdio: "ignore" });
|
|
23
|
+
console.log(" ✓ cloudflared detected\n");
|
|
24
|
+
} catch {
|
|
25
|
+
console.log(" ✗ cloudflared not found.\n");
|
|
26
|
+
console.log(" Install it first:");
|
|
27
|
+
console.log(" Windows: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/");
|
|
28
|
+
console.log(" Or via winget: winget install Cloudflare.cloudflared\n");
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Step 2: Authenticate with Cloudflare
|
|
33
|
+
console.log(" Step 1/4 — Authenticate with Cloudflare");
|
|
34
|
+
console.log(" This will open your browser to log in.\n");
|
|
35
|
+
const doAuth = await ask(" Proceed? (y/n): ");
|
|
36
|
+
if (doAuth.toLowerCase() !== "y") { console.log(" Aborted."); process.exit(0); }
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
spawnSync("cloudflared", ["tunnel", "login"], { stdio: "inherit" });
|
|
40
|
+
console.log(" ✓ Authenticated\n");
|
|
41
|
+
} catch (e) {
|
|
42
|
+
console.error(" ✗ Login failed:", e.message);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Step 3: Create tunnel
|
|
47
|
+
console.log(" Step 2/4 — Create named tunnel");
|
|
48
|
+
const tunnelName = await ask(" Tunnel name (default: clauth): ") || "clauth";
|
|
49
|
+
|
|
50
|
+
let tunnelId;
|
|
51
|
+
try {
|
|
52
|
+
const result = execSync(`cloudflared tunnel create ${tunnelName}`, { encoding: "utf8" });
|
|
53
|
+
const match = result.match(/Created tunnel (.+) with id ([a-f0-9-]+)/i);
|
|
54
|
+
if (match) tunnelId = match[2];
|
|
55
|
+
console.log(` ✓ Tunnel created: ${tunnelName} (${tunnelId || "see above"})\n`);
|
|
56
|
+
} catch (e) {
|
|
57
|
+
// Tunnel may already exist
|
|
58
|
+
console.log(" (Tunnel may already exist — continuing)\n");
|
|
59
|
+
try {
|
|
60
|
+
const listResult = execSync(`cloudflared tunnel list`, { encoding: "utf8" });
|
|
61
|
+
const lines = listResult.split("\n");
|
|
62
|
+
for (const line of lines) {
|
|
63
|
+
if (line.toLowerCase().includes(tunnelName.toLowerCase())) {
|
|
64
|
+
const parts = line.trim().split(/\s+/);
|
|
65
|
+
if (parts[0] && parts[0].includes("-")) tunnelId = parts[0];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
// ignore list errors
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Step 4: Configure hostname
|
|
74
|
+
console.log(" Step 3/4 — Set public hostname");
|
|
75
|
+
const hostname = await ask(" Public hostname (e.g. clauth.yourdomain.com): ");
|
|
76
|
+
if (!hostname) { console.log(" Hostname required."); process.exit(1); }
|
|
77
|
+
|
|
78
|
+
// Write config.yml
|
|
79
|
+
const cfDir = path.join(os.homedir(), ".cloudflared");
|
|
80
|
+
if (!fs.existsSync(cfDir)) fs.mkdirSync(cfDir, { recursive: true });
|
|
81
|
+
|
|
82
|
+
const configPath = path.join(cfDir, "config.yml");
|
|
83
|
+
const credFile = tunnelId ? path.join(cfDir, `${tunnelId}.json`) : `<tunnel-id>.json`;
|
|
84
|
+
const config = `tunnel: ${tunnelId || tunnelName}
|
|
85
|
+
credentials-file: ${credFile}
|
|
86
|
+
ingress:
|
|
87
|
+
- hostname: ${hostname}
|
|
88
|
+
service: http://127.0.0.1:52437
|
|
89
|
+
- service: http_status:404
|
|
90
|
+
`;
|
|
91
|
+
fs.writeFileSync(configPath, config, "utf8");
|
|
92
|
+
console.log(` ✓ Config written: ${configPath}\n`);
|
|
93
|
+
|
|
94
|
+
// Route DNS
|
|
95
|
+
console.log(" Step 4/4 — Configure DNS");
|
|
96
|
+
try {
|
|
97
|
+
execSync(`cloudflared tunnel route dns ${tunnelName} ${hostname}`, { stdio: "inherit" });
|
|
98
|
+
console.log(` ✓ DNS configured: ${hostname}\n`);
|
|
99
|
+
} catch {
|
|
100
|
+
console.log(` ⚠ DNS route failed — add manually in Cloudflare dashboard:`);
|
|
101
|
+
console.log(` CNAME ${hostname} → ${tunnelId}.cfargotunnel.com\n`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Save hostname to clauth config via api module
|
|
105
|
+
try {
|
|
106
|
+
const apiMod = await import("../api.js").catch(() => null);
|
|
107
|
+
if (apiMod) {
|
|
108
|
+
const sbUrl = (apiMod.getBaseUrl?.() || "").replace("/functions/v1/auth-vault", "");
|
|
109
|
+
const sbKey = apiMod.getAnonKey?.();
|
|
110
|
+
if (sbUrl && sbKey) {
|
|
111
|
+
await fetch(
|
|
112
|
+
`${sbUrl}/rest/v1/clauth_config`,
|
|
113
|
+
{
|
|
114
|
+
method: "POST",
|
|
115
|
+
headers: {
|
|
116
|
+
apikey: sbKey,
|
|
117
|
+
Authorization: `Bearer ${sbKey}`,
|
|
118
|
+
"Content-Type": "application/json",
|
|
119
|
+
Prefer: "resolution=merge-duplicates",
|
|
120
|
+
},
|
|
121
|
+
body: JSON.stringify({ key: "tunnel_hostname", value: hostname }),
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
console.log(` ✓ Hostname saved to clauth config: ${hostname}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
console.log(` ⚠ Could not save to DB. Hostname is set in config.yml — 'clauth serve' will pick it up.`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.log("\n Setup complete!");
|
|
132
|
+
console.log(` Restart the daemon: clauth serve restart`);
|
|
133
|
+
console.log(` Or use: npm run worker:restart (from clauth directory)\n`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// clauth tunnel start — tells daemon to start the tunnel
|
|
137
|
+
async function actionTunnelStart() {
|
|
138
|
+
try {
|
|
139
|
+
const r = await fetch("http://127.0.0.1:52437/tunnel/start", {
|
|
140
|
+
method: "POST",
|
|
141
|
+
headers: { "Content-Type": "application/json" },
|
|
142
|
+
signal: AbortSignal.timeout(5000),
|
|
143
|
+
});
|
|
144
|
+
const data = await r.json().catch(() => ({}));
|
|
145
|
+
if (!r.ok) {
|
|
146
|
+
console.error(` ✗ ${data.error || r.statusText}`);
|
|
147
|
+
if (r.status === 401) console.error(" Unlock the daemon first: http://127.0.0.1:52437");
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
console.log(` ✓ ${data.message || "Tunnel starting — check status with: clauth tunnel status"}`);
|
|
151
|
+
} catch (e) {
|
|
152
|
+
console.error(" ✗ Daemon not running. Start it with: clauth serve");
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// clauth tunnel stop — tells daemon to stop the tunnel
|
|
158
|
+
async function actionTunnelStop() {
|
|
159
|
+
try {
|
|
160
|
+
const r = await fetch("http://127.0.0.1:52437/tunnel/stop", {
|
|
161
|
+
method: "POST",
|
|
162
|
+
headers: { "Content-Type": "application/json" },
|
|
163
|
+
signal: AbortSignal.timeout(5000),
|
|
164
|
+
});
|
|
165
|
+
const data = await r.json().catch(() => ({}));
|
|
166
|
+
if (!r.ok) {
|
|
167
|
+
console.error(` ✗ ${data.error || r.statusText}`);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
console.log(" ✓ Tunnel stopped.");
|
|
171
|
+
} catch (e) {
|
|
172
|
+
console.error(" ✗ Daemon not running.");
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// clauth tunnel status — shows current tunnel status from daemon
|
|
178
|
+
async function actionTunnelStatus() {
|
|
179
|
+
try {
|
|
180
|
+
const r = await fetch("http://127.0.0.1:52437/tunnel", {
|
|
181
|
+
signal: AbortSignal.timeout(5000),
|
|
182
|
+
});
|
|
183
|
+
const data = await r.json().catch(() => ({}));
|
|
184
|
+
|
|
185
|
+
const icons = {
|
|
186
|
+
live: "✓",
|
|
187
|
+
starting: "◌",
|
|
188
|
+
not_configured: "⚠",
|
|
189
|
+
not_started: "○",
|
|
190
|
+
error: "✗",
|
|
191
|
+
missing_cloudflared: "✗",
|
|
192
|
+
};
|
|
193
|
+
const labels = {
|
|
194
|
+
live: `Live — ${data.url || ""}`,
|
|
195
|
+
starting: "Starting...",
|
|
196
|
+
not_configured: "Not configured — run: clauth tunnel setup",
|
|
197
|
+
not_started: "Not started — run: clauth tunnel start",
|
|
198
|
+
error: `Error${data.error ? ": " + data.error : ""} — check cloudflared config`,
|
|
199
|
+
missing_cloudflared: "cloudflared not installed — https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/",
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const status = data.status || "unknown";
|
|
203
|
+
console.log(`\n ${icons[status] || "?"} Tunnel: ${labels[status] || status}\n`);
|
|
204
|
+
} catch (e) {
|
|
205
|
+
console.error(" ✗ Daemon not running. Start it with: clauth serve");
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export { actionTunnelSetup, actionTunnelStart, actionTunnelStop, actionTunnelStatus };
|
package/cli/index.js
CHANGED
|
@@ -10,9 +10,10 @@ import { getConfOptions } from "./conf-path.js";
|
|
|
10
10
|
import { getMachineHash, deriveToken, deriveSeedHash } from "./fingerprint.js";
|
|
11
11
|
import * as api from "./api.js";
|
|
12
12
|
import os from "os";
|
|
13
|
+
import fs from "fs";
|
|
13
14
|
|
|
14
15
|
const config = new Conf(getConfOptions());
|
|
15
|
-
const VERSION =
|
|
16
|
+
const VERSION = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
16
17
|
|
|
17
18
|
// ============================================================
|
|
18
19
|
// Password prompt helper
|
|
@@ -128,6 +129,7 @@ program
|
|
|
128
129
|
const result = await api.registerMachine(machineHash, seedHash, answers.label, answers.adminTk);
|
|
129
130
|
if (result.error) throw new Error(result.error);
|
|
130
131
|
spinner.succeed(chalk.green(`Machine registered: ${machineHash.slice(0,12)}...`));
|
|
132
|
+
|
|
131
133
|
console.log(chalk.green("\n✓ clauth is ready.\n"));
|
|
132
134
|
console.log(chalk.cyan(" clauth test — verify connection"));
|
|
133
135
|
console.log(chalk.cyan(" clauth status — see all services\n"));
|
|
@@ -144,22 +146,24 @@ program
|
|
|
144
146
|
.command("status")
|
|
145
147
|
.description("Show all services and their state")
|
|
146
148
|
.option("-p, --pw <password>", "Password (or will prompt)")
|
|
149
|
+
.option("--project <name>", "Filter by project scope")
|
|
147
150
|
.action(async (opts) => {
|
|
148
151
|
const auth = await getAuth(opts.pw);
|
|
149
152
|
const spinner = ora("Fetching service status...").start();
|
|
150
153
|
try {
|
|
151
|
-
const result = await api.status(auth.password, auth.machineHash, auth.token, auth.timestamp);
|
|
154
|
+
const result = await api.status(auth.password, auth.machineHash, auth.token, auth.timestamp, opts.project);
|
|
152
155
|
spinner.stop();
|
|
153
156
|
if (result.error) { console.log(chalk.red(`Error: ${result.error}`)); return; }
|
|
154
157
|
|
|
155
|
-
|
|
158
|
+
const heading = opts.project ? `clauth service status (project: ${opts.project})` : "clauth service status";
|
|
159
|
+
console.log(chalk.cyan(`\n🔐 ${heading}\n`));
|
|
156
160
|
console.log(
|
|
157
161
|
chalk.bold(
|
|
158
|
-
" " + "SERVICE".padEnd(
|
|
162
|
+
" " + "SERVICE".padEnd(24) + "TYPE".padEnd(12) + "PROJECT".padEnd(22) + "STATUS".padEnd(12) +
|
|
159
163
|
"KEY STORED".padEnd(12) + "LAST RETRIEVED"
|
|
160
164
|
)
|
|
161
165
|
);
|
|
162
|
-
console.log(" " + "─".repeat(
|
|
166
|
+
console.log(" " + "─".repeat(90));
|
|
163
167
|
|
|
164
168
|
for (const s of result.services || []) {
|
|
165
169
|
const status = s.enabled
|
|
@@ -171,8 +175,9 @@ program
|
|
|
171
175
|
const lastGet = s.last_retrieved
|
|
172
176
|
? new Date(s.last_retrieved).toLocaleDateString()
|
|
173
177
|
: chalk.gray("never");
|
|
178
|
+
const proj = s.project ? chalk.blue(s.project.padEnd(22)) : chalk.gray("—".padEnd(22));
|
|
174
179
|
|
|
175
|
-
console.log(` ${s.name.padEnd(
|
|
180
|
+
console.log(` ${s.name.padEnd(24)}${s.key_type.padEnd(12)}${proj}${status}${hasKey}${lastGet}`);
|
|
176
181
|
}
|
|
177
182
|
console.log();
|
|
178
183
|
} catch (err) {
|
|
@@ -296,6 +301,7 @@ addCmd
|
|
|
296
301
|
.option("--type <type>", "Key type: token | keypair | connstring | oauth")
|
|
297
302
|
.option("--label <label>", "Human-readable label")
|
|
298
303
|
.option("--description <desc>", "Description")
|
|
304
|
+
.option("--project <project>", "Project scope (groups related services)")
|
|
299
305
|
.option("-p, --pw <password>")
|
|
300
306
|
.action(async (name, opts) => {
|
|
301
307
|
const auth = await getAuth(opts.pw);
|
|
@@ -310,14 +316,14 @@ addCmd
|
|
|
310
316
|
{ type: "input", name: "desc", message: "Description (optional):", default: opts.description || "" }
|
|
311
317
|
]);
|
|
312
318
|
}
|
|
313
|
-
const spinner = ora(`Adding service: ${name}...`).start();
|
|
319
|
+
const spinner = ora(`Adding service: ${name}${opts.project ? ` (project: ${opts.project})` : ""}...`).start();
|
|
314
320
|
try {
|
|
315
321
|
const result = await api.addService(
|
|
316
322
|
auth.password, auth.machineHash, auth.token, auth.timestamp,
|
|
317
|
-
name, answers.label, answers.key_type, answers.desc
|
|
323
|
+
name, answers.label, answers.key_type, answers.desc, opts.project
|
|
318
324
|
);
|
|
319
325
|
if (result.error) throw new Error(result.error);
|
|
320
|
-
spinner.succeed(chalk.green(`Service added: ${name} (${answers.key_type})`));
|
|
326
|
+
spinner.succeed(chalk.green(`Service added: ${name} (${answers.key_type})${opts.project ? chalk.blue(` [${opts.project}]`) : ""}`));
|
|
321
327
|
console.log(chalk.gray(` Next: clauth write key ${name}`));
|
|
322
328
|
} catch (err) { spinner.fail(chalk.red(err.message)); }
|
|
323
329
|
});
|
|
@@ -346,13 +352,22 @@ program
|
|
|
346
352
|
.command("list")
|
|
347
353
|
.description("List all registered services")
|
|
348
354
|
.option("-p, --pw <password>")
|
|
355
|
+
.option("--project <name>", "Filter by project scope")
|
|
349
356
|
.action(async (opts) => {
|
|
350
357
|
const auth = await getAuth(opts.pw);
|
|
351
|
-
const result = await api.status(auth.password, auth.machineHash, auth.token, auth.timestamp);
|
|
358
|
+
const result = await api.status(auth.password, auth.machineHash, auth.token, auth.timestamp, opts.project);
|
|
352
359
|
if (result.error) { console.log(chalk.red(result.error)); return; }
|
|
353
|
-
|
|
360
|
+
const heading = opts.project ? `Registered services (project: ${opts.project})` : "Registered services";
|
|
361
|
+
console.log(chalk.cyan(`\n ${heading}:\n`));
|
|
362
|
+
let lastProject = undefined;
|
|
354
363
|
for (const s of result.services || []) {
|
|
355
|
-
|
|
364
|
+
const proj = s.project || null;
|
|
365
|
+
if (proj !== lastProject) {
|
|
366
|
+
if (lastProject !== undefined) console.log();
|
|
367
|
+
console.log(chalk.gray(` [${proj || "global"}]`));
|
|
368
|
+
lastProject = proj;
|
|
369
|
+
}
|
|
370
|
+
console.log(` ${chalk.bold(s.name.padEnd(24))} ${chalk.gray(s.key_type.padEnd(12))} ${chalk.gray(s.label || "")}`);
|
|
356
371
|
}
|
|
357
372
|
console.log();
|
|
358
373
|
});
|
|
@@ -446,6 +461,113 @@ Examples:
|
|
|
446
461
|
await runScrub(target, opts);
|
|
447
462
|
});
|
|
448
463
|
|
|
464
|
+
// ──────────────────────────────────────────────
|
|
465
|
+
// clauth doctor
|
|
466
|
+
// ──────────────────────────────────────────────
|
|
467
|
+
program
|
|
468
|
+
.command("doctor")
|
|
469
|
+
.description("Check all prerequisites and diagnose issues")
|
|
470
|
+
.action(async () => {
|
|
471
|
+
const { runDoctor } = await import("./commands/doctor.js");
|
|
472
|
+
await runDoctor();
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// ──────────────────────────────────────────────
|
|
476
|
+
// clauth invite generate|list|revoke
|
|
477
|
+
// ──────────────────────────────────────────────
|
|
478
|
+
const invite = program.command("invite").description("Manage vault invites");
|
|
479
|
+
|
|
480
|
+
invite
|
|
481
|
+
.command("generate")
|
|
482
|
+
.description("Generate an invite code for a friend")
|
|
483
|
+
.option("--uses <n>", "Max redemptions", "1")
|
|
484
|
+
.option("--expires <hours>", "Expiry in hours", "168")
|
|
485
|
+
.action(async (opts) => {
|
|
486
|
+
const { runInvite } = await import("./commands/invite.js");
|
|
487
|
+
await runInvite("generate", opts);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
invite
|
|
491
|
+
.command("list")
|
|
492
|
+
.description("List active invites")
|
|
493
|
+
.action(async () => {
|
|
494
|
+
const { runInvite } = await import("./commands/invite.js");
|
|
495
|
+
await runInvite("list", {});
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
invite
|
|
499
|
+
.command("revoke <code>")
|
|
500
|
+
.description("Revoke an invite code")
|
|
501
|
+
.action(async (code) => {
|
|
502
|
+
const { runInvite } = await import("./commands/invite.js");
|
|
503
|
+
await runInvite("revoke", { code });
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
// ──────────────────────────────────────────────
|
|
507
|
+
// clauth join <invite-code>
|
|
508
|
+
// ──────────────────────────────────────────────
|
|
509
|
+
program
|
|
510
|
+
.command("join <invite-code>")
|
|
511
|
+
.description("Join a vault using an invite code from a friend")
|
|
512
|
+
.action(async (code) => {
|
|
513
|
+
const { runJoin } = await import("./commands/join.js");
|
|
514
|
+
await runJoin(code);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
// ──────────────────────────────────────────────
|
|
518
|
+
// clauth update
|
|
519
|
+
// ──────────────────────────────────────────────
|
|
520
|
+
program
|
|
521
|
+
.command("update")
|
|
522
|
+
.description("Update clauth to the latest version")
|
|
523
|
+
.action(async () => {
|
|
524
|
+
const { execSync } = await import("child_process");
|
|
525
|
+
console.log(chalk.cyan("\n Updating clauth...\n"));
|
|
526
|
+
try {
|
|
527
|
+
execSync("npm install -g @lifeaitools/clauth@latest", { stdio: "inherit" });
|
|
528
|
+
console.log(chalk.green("\n Updated successfully.\n"));
|
|
529
|
+
} catch (err) {
|
|
530
|
+
console.log(chalk.red(`\n Update failed: ${err.message}\n`));
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
// ──────────────────────────────────────────────
|
|
535
|
+
// clauth tunnel setup
|
|
536
|
+
// ──────────────────────────────────────────────
|
|
537
|
+
const tunnelCmd = program.command("tunnel").description("Manage Cloudflare tunnel for claude.ai web integration");
|
|
538
|
+
|
|
539
|
+
tunnelCmd
|
|
540
|
+
.command("setup")
|
|
541
|
+
.description("Interactive wizard — configure cloudflared named tunnel")
|
|
542
|
+
.action(async () => {
|
|
543
|
+
const { actionTunnelSetup } = await import("./commands/tunnel.js");
|
|
544
|
+
await actionTunnelSetup();
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
tunnelCmd
|
|
548
|
+
.command("start")
|
|
549
|
+
.description("Tell daemon to start the tunnel")
|
|
550
|
+
.action(async () => {
|
|
551
|
+
const { actionTunnelStart } = await import("./commands/tunnel.js");
|
|
552
|
+
await actionTunnelStart();
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
tunnelCmd
|
|
556
|
+
.command("stop")
|
|
557
|
+
.description("Tell daemon to stop the tunnel")
|
|
558
|
+
.action(async () => {
|
|
559
|
+
const { actionTunnelStop } = await import("./commands/tunnel.js");
|
|
560
|
+
await actionTunnelStop();
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
tunnelCmd
|
|
564
|
+
.command("status")
|
|
565
|
+
.description("Show current tunnel status")
|
|
566
|
+
.action(async () => {
|
|
567
|
+
const { actionTunnelStatus } = await import("./commands/tunnel.js");
|
|
568
|
+
await actionTunnelStatus();
|
|
569
|
+
});
|
|
570
|
+
|
|
449
571
|
// ──────────────────────────────────────────────
|
|
450
572
|
// clauth --help override banner
|
|
451
573
|
// ──────────────────────────────────────────────
|
|
@@ -478,8 +600,9 @@ Actions:
|
|
|
478
600
|
ping Check if the daemon is running
|
|
479
601
|
foreground Run in foreground (Ctrl+C to stop) — default if no action given
|
|
480
602
|
mcp Run as MCP stdio server for Claude Code (JSON-RPC over stdin/stdout)
|
|
481
|
-
install
|
|
482
|
-
|
|
603
|
+
install Store password securely + register auto-start service (cross-platform)
|
|
604
|
+
Windows: DPAPI + Scheduled Task | macOS: Keychain + LaunchAgent | Linux: libsecret/openssl + systemd
|
|
605
|
+
uninstall Remove auto-start service + delete stored password
|
|
483
606
|
|
|
484
607
|
MCP SSE (built into start/foreground):
|
|
485
608
|
The HTTP daemon also serves MCP SSE transport at GET /sse + POST /message.
|
|
@@ -494,8 +617,9 @@ Examples:
|
|
|
494
617
|
clauth serve start --services github,vercel
|
|
495
618
|
clauth serve mcp Start MCP server for Claude Code
|
|
496
619
|
clauth serve mcp -p mypass Start MCP server pre-unlocked
|
|
497
|
-
clauth serve install
|
|
498
|
-
clauth serve
|
|
620
|
+
clauth serve install Set up auto-start on login (DPAPI/Keychain/libsecret)
|
|
621
|
+
clauth serve install --tunnel host Auto-start with Cloudflare Tunnel
|
|
622
|
+
clauth serve uninstall Remove auto-start
|
|
499
623
|
`)
|
|
500
624
|
.action(async (action, opts) => {
|
|
501
625
|
const resolvedAction = opts.action || action || "foreground";
|
package/install.ps1
CHANGED
|
@@ -23,6 +23,13 @@ try { node --version | Out-Null } catch {
|
|
|
23
23
|
exit 1
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
# Check cloudflared (soft warning — not required for install)
|
|
27
|
+
if (-not (Get-Command cloudflared -ErrorAction SilentlyContinue)) {
|
|
28
|
+
Write-Host " Note: cloudflared not found. Install after setup for claude.ai web integration." -ForegroundColor Yellow
|
|
29
|
+
Write-Host " winget install Cloudflare.cloudflared" -ForegroundColor Gray
|
|
30
|
+
Write-Host ""
|
|
31
|
+
}
|
|
32
|
+
|
|
26
33
|
# Clone or update
|
|
27
34
|
if (Test-Path "$DIR\.git") {
|
|
28
35
|
Write-Host " Updating clauth..."
|
package/install.sh
CHANGED
|
@@ -19,6 +19,17 @@ if ! command -v node &>/dev/null; then
|
|
|
19
19
|
exit 1
|
|
20
20
|
fi
|
|
21
21
|
|
|
22
|
+
# Check cloudflared (soft warning)
|
|
23
|
+
if ! command -v cloudflared &>/dev/null; then
|
|
24
|
+
echo " Note: cloudflared not found. Install after setup for claude.ai web integration."
|
|
25
|
+
if [[ "$OSTYPE" == "darwin"* ]]; then
|
|
26
|
+
echo " brew install cloudflare/cloudflare/cloudflared"
|
|
27
|
+
else
|
|
28
|
+
echo " https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/"
|
|
29
|
+
fi
|
|
30
|
+
echo ""
|
|
31
|
+
fi
|
|
32
|
+
|
|
22
33
|
# Clone or update
|
|
23
34
|
if [ -d "$DIR/.git" ]; then
|
|
24
35
|
echo " Updating clauth..."; cd "$DIR" && git pull --quiet
|
package/package.json
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lifeaitools/clauth",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Hardware-bound credential vault for the LIFEAI infrastructure stack",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"clauth": "./cli/index.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"build": "bash scripts/build.sh"
|
|
10
|
+
"build": "bash scripts/build.sh",
|
|
11
|
+
"postinstall": "node scripts/postinstall.js",
|
|
12
|
+
"worker:start": "node cli/index.js serve",
|
|
13
|
+
"worker:stop": "curl -s http://127.0.0.1:52437/shutdown 2>nul || taskkill /F /IM cloudflared.exe 2>nul & exit 0",
|
|
14
|
+
"worker:restart": "npm run worker:stop && timeout /t 3 /nobreak >nul && npm run worker:start"
|
|
11
15
|
},
|
|
12
16
|
"dependencies": {
|
|
13
17
|
"chalk": "^5.3.0",
|
|
@@ -41,6 +45,7 @@
|
|
|
41
45
|
"scripts/bin/",
|
|
42
46
|
"scripts/bootstrap.cjs",
|
|
43
47
|
"scripts/build.sh",
|
|
48
|
+
"scripts/postinstall.js",
|
|
44
49
|
"supabase/",
|
|
45
50
|
".clauth-skill/",
|
|
46
51
|
"install.sh",
|
package/scripts/bootstrap.cjs
CHANGED
|
@@ -38,6 +38,84 @@ if (lnk.status !== 0) {
|
|
|
38
38
|
}
|
|
39
39
|
console.log(' + clauth linked');
|
|
40
40
|
|
|
41
|
+
// Check if cloudflared is installed
|
|
42
|
+
var cfFound = false;
|
|
43
|
+
try {
|
|
44
|
+
execSync('cloudflared --version', { stdio: 'ignore' });
|
|
45
|
+
cfFound = true;
|
|
46
|
+
} catch {}
|
|
47
|
+
|
|
48
|
+
if (!cfFound) {
|
|
49
|
+
console.log('\n cloudflared not found — needed for claude.ai web integration (tunnel).');
|
|
50
|
+
|
|
51
|
+
var platform = process.platform;
|
|
52
|
+
var installCmd = null;
|
|
53
|
+
var installLabel = null;
|
|
54
|
+
|
|
55
|
+
if (platform === 'win32') {
|
|
56
|
+
try {
|
|
57
|
+
execSync('winget --version', { stdio: 'ignore' });
|
|
58
|
+
installCmd = 'winget install Cloudflare.cloudflared --silent --accept-package-agreements --accept-source-agreements';
|
|
59
|
+
installLabel = 'winget';
|
|
60
|
+
} catch {}
|
|
61
|
+
} else if (platform === 'darwin') {
|
|
62
|
+
try {
|
|
63
|
+
execSync('brew --version', { stdio: 'ignore' });
|
|
64
|
+
installCmd = 'brew install cloudflare/cloudflare/cloudflared';
|
|
65
|
+
installLabel = 'Homebrew';
|
|
66
|
+
} catch {}
|
|
67
|
+
}
|
|
68
|
+
// Linux: no auto-install — falls through to manual instructions
|
|
69
|
+
|
|
70
|
+
if (installCmd) {
|
|
71
|
+
process.stdout.write(' Install now via ' + installLabel + '? (y/n): ');
|
|
72
|
+
var answer = (function () {
|
|
73
|
+
try {
|
|
74
|
+
var buf = Buffer.alloc(3);
|
|
75
|
+
var fd = require('fs').openSync('/dev/tty', 'r');
|
|
76
|
+
require('fs').readSync(fd, buf, 0, 3);
|
|
77
|
+
require('fs').closeSync(fd);
|
|
78
|
+
return buf.toString().trim().toLowerCase();
|
|
79
|
+
} catch {
|
|
80
|
+
try {
|
|
81
|
+
var result = require('child_process').spawnSync(
|
|
82
|
+
'powershell',
|
|
83
|
+
['-Command', "$k = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown'); $k.Character"],
|
|
84
|
+
{ encoding: 'utf8' }
|
|
85
|
+
);
|
|
86
|
+
return (result.stdout || '').trim().toLowerCase();
|
|
87
|
+
} catch { return 'n'; }
|
|
88
|
+
}
|
|
89
|
+
})();
|
|
90
|
+
|
|
91
|
+
if (answer === 'y') {
|
|
92
|
+
console.log(' Installing cloudflared via ' + installLabel + '...');
|
|
93
|
+
try {
|
|
94
|
+
execSync(installCmd, { stdio: 'inherit' });
|
|
95
|
+
console.log(' + cloudflared installed.');
|
|
96
|
+
} catch (e) {
|
|
97
|
+
console.log(' x Auto-install failed. Install manually:');
|
|
98
|
+
console.log(' https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/');
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
console.log(' Skipped. Install later and run: clauth tunnel setup');
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
console.log(' Install manually:');
|
|
105
|
+
if (platform === 'win32') {
|
|
106
|
+
console.log(' winget install Cloudflare.cloudflared');
|
|
107
|
+
console.log(' or: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/');
|
|
108
|
+
} else if (platform === 'darwin') {
|
|
109
|
+
console.log(' brew install cloudflare/cloudflare/cloudflared');
|
|
110
|
+
console.log(' or: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/');
|
|
111
|
+
} else {
|
|
112
|
+
console.log(' https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/');
|
|
113
|
+
}
|
|
114
|
+
console.log(' Then run: clauth tunnel setup');
|
|
115
|
+
}
|
|
116
|
+
console.log('');
|
|
117
|
+
}
|
|
118
|
+
|
|
41
119
|
console.log('\n -> Launching clauth install...\n');
|
|
42
120
|
var r = run('clauth install');
|
|
43
121
|
process.exit(r.status || 0);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/postinstall.js
|
|
3
|
+
// Runs after npm install -g @lifeaitools/clauth
|
|
4
|
+
// Detects fresh install vs upgrade and acts accordingly
|
|
5
|
+
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import os from "os";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
|
|
13
|
+
const CONFIG_DIR = os.platform() === "win32"
|
|
14
|
+
? path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), "clauth-nodejs", "Config")
|
|
15
|
+
: path.join(os.homedir(), ".config", "clauth-nodejs");
|
|
16
|
+
|
|
17
|
+
let version = "1.0.0";
|
|
18
|
+
try {
|
|
19
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8"));
|
|
20
|
+
version = pkg.version;
|
|
21
|
+
} catch {}
|
|
22
|
+
|
|
23
|
+
async function main() {
|
|
24
|
+
const configPath = path.join(CONFIG_DIR, "config.json");
|
|
25
|
+
const isUpgrade = fs.existsSync(configPath);
|
|
26
|
+
|
|
27
|
+
if (isUpgrade) {
|
|
28
|
+
console.log(`\n \u2713 clauth upgraded to v${version}`);
|
|
29
|
+
console.log(` \u2713 Config preserved at ${CONFIG_DIR}`);
|
|
30
|
+
|
|
31
|
+
// Try to detect running daemon
|
|
32
|
+
try {
|
|
33
|
+
const controller = new AbortController();
|
|
34
|
+
const timeout = setTimeout(() => controller.abort(), 2000);
|
|
35
|
+
const res = await fetch("http://127.0.0.1:52437/ping", { signal: controller.signal });
|
|
36
|
+
clearTimeout(timeout);
|
|
37
|
+
if (res.ok) {
|
|
38
|
+
console.log(" \u21BB Daemon is running \u2014 restart it with: clauth serve restart");
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
console.log(" \u25CB Daemon not running");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
console.log(" Run 'clauth doctor' to verify installation\n");
|
|
45
|
+
} else {
|
|
46
|
+
console.log(`\n Welcome to clauth v${version}!`);
|
|
47
|
+
console.log(" Run 'clauth install' to set up your vault");
|
|
48
|
+
console.log(" Run 'clauth doctor' to check prerequisites\n");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
main().catch(() => {});
|