@lifeaitools/clauth 0.2.1 → 0.3.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 +109 -66
- package/cli/commands/scrub.js +231 -0
- package/cli/commands/serve.js +378 -0
- package/cli/commands/uninstall.js +164 -0
- package/cli/index.js +94 -6
- package/package.json +1 -1
- package/supabase/functions/auth-vault/index.ts +132 -235
- package/supabase/migrations/20260317_lockout.sql +26 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
// cli/commands/serve.js
|
|
2
|
+
// Localhost-only credential daemon with daemon lifecycle management
|
|
3
|
+
// Binds 127.0.0.1 ONLY — unreachable from outside the machine
|
|
4
|
+
// 3 failed requests of any kind → process exits, requires manual restart
|
|
5
|
+
// Supports: start (background daemon), stop, restart, ping, foreground
|
|
6
|
+
|
|
7
|
+
import http from "http";
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import os from "os";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import { getMachineHash, deriveToken } from "../fingerprint.js";
|
|
12
|
+
import * as api from "../api.js";
|
|
13
|
+
import chalk from "chalk";
|
|
14
|
+
|
|
15
|
+
const PID_FILE = path.join(os.tmpdir(), "clauth-serve.pid");
|
|
16
|
+
const LOG_FILE = path.join(os.tmpdir(), "clauth-serve.log");
|
|
17
|
+
|
|
18
|
+
// ── PID helpers ──────────────────────────────────────────────
|
|
19
|
+
function readPid() {
|
|
20
|
+
try {
|
|
21
|
+
const raw = fs.readFileSync(PID_FILE, "utf8").trim();
|
|
22
|
+
const [pid, port] = raw.split(":");
|
|
23
|
+
return { pid: parseInt(pid, 10), port: parseInt(port, 10) };
|
|
24
|
+
} catch { return null; }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function writePid(pid, port) {
|
|
28
|
+
fs.writeFileSync(PID_FILE, `${pid}:${port}`, "utf8");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function removePid() {
|
|
32
|
+
try { fs.unlinkSync(PID_FILE); } catch {}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isProcessAlive(pid) {
|
|
36
|
+
try { process.kill(pid, 0); return true; } catch { return false; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Server logic (shared by foreground + daemon) ─────────────
|
|
40
|
+
function createServer(password, whitelist, port) {
|
|
41
|
+
const MAX_FAILS = 3;
|
|
42
|
+
let failCount = 0;
|
|
43
|
+
const machineHash = getMachineHash();
|
|
44
|
+
|
|
45
|
+
function strike(res, code, message) {
|
|
46
|
+
failCount++;
|
|
47
|
+
const remaining = MAX_FAILS - failCount;
|
|
48
|
+
const logLine = `[${new Date().toISOString()}] [FAIL ${failCount}/${MAX_FAILS}] ${message}\n`;
|
|
49
|
+
try { fs.appendFileSync(LOG_FILE, logLine); } catch {}
|
|
50
|
+
|
|
51
|
+
const body = JSON.stringify({
|
|
52
|
+
error: message,
|
|
53
|
+
failures: failCount,
|
|
54
|
+
failures_remaining: remaining,
|
|
55
|
+
...(failCount >= MAX_FAILS ? { shutdown: true } : {})
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
res.writeHead(code, { "Content-Type": "application/json" });
|
|
59
|
+
res.end(body);
|
|
60
|
+
|
|
61
|
+
if (failCount >= MAX_FAILS) {
|
|
62
|
+
const msg = `[${new Date().toISOString()}] Failure limit reached — shutting down\n`;
|
|
63
|
+
try { fs.appendFileSync(LOG_FILE, msg); } catch {}
|
|
64
|
+
removePid();
|
|
65
|
+
setTimeout(() => process.exit(1), 100);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function ok(res, data) {
|
|
70
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
71
|
+
res.end(JSON.stringify(data));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const server = http.createServer(async (req, res) => {
|
|
75
|
+
// Hard reject anything not from loopback
|
|
76
|
+
const remote = req.socket.remoteAddress;
|
|
77
|
+
const isLocal = remote === "127.0.0.1" || remote === "::1" || remote === "::ffff:127.0.0.1";
|
|
78
|
+
if (!isLocal) {
|
|
79
|
+
return strike(res, 403, `Rejected non-local address: ${remote}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const url = new URL(req.url, `http://127.0.0.1:${port}`);
|
|
83
|
+
const reqPath = url.pathname;
|
|
84
|
+
const method = req.method;
|
|
85
|
+
|
|
86
|
+
// GET /ping
|
|
87
|
+
if (method === "GET" && reqPath === "/ping") {
|
|
88
|
+
return ok(res, {
|
|
89
|
+
status: "ok",
|
|
90
|
+
pid: process.pid,
|
|
91
|
+
failures: failCount,
|
|
92
|
+
failures_remaining: MAX_FAILS - failCount,
|
|
93
|
+
services: whitelist || "all",
|
|
94
|
+
port
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// GET /shutdown (for daemon stop)
|
|
99
|
+
if (method === "GET" && reqPath === "/shutdown") {
|
|
100
|
+
ok(res, { ok: true, message: "shutting down" });
|
|
101
|
+
removePid();
|
|
102
|
+
setTimeout(() => process.exit(0), 100);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// GET /status
|
|
107
|
+
if (method === "GET" && reqPath === "/status") {
|
|
108
|
+
try {
|
|
109
|
+
const { token, timestamp } = deriveToken(password, machineHash);
|
|
110
|
+
const result = await api.status(password, machineHash, token, timestamp);
|
|
111
|
+
if (result.error) return strike(res, 502, result.error);
|
|
112
|
+
if (whitelist) {
|
|
113
|
+
result.services = (result.services || []).filter(
|
|
114
|
+
s => whitelist.includes(s.name.toLowerCase())
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
return ok(res, result);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
return strike(res, 502, err.message);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// GET /get/:service
|
|
124
|
+
const getMatch = reqPath.match(/^\/get\/([a-zA-Z0-9_-]+)$/);
|
|
125
|
+
if (method === "GET" && getMatch) {
|
|
126
|
+
const service = getMatch[1].toLowerCase();
|
|
127
|
+
|
|
128
|
+
if (whitelist && !whitelist.includes(service)) {
|
|
129
|
+
return strike(res, 403, `Service '${service}' not in whitelist`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const { token, timestamp } = deriveToken(password, machineHash);
|
|
134
|
+
const result = await api.retrieve(password, machineHash, token, timestamp, service);
|
|
135
|
+
if (result.error) return strike(res, 502, result.error);
|
|
136
|
+
return ok(res, { service, value: result.value, key_type: result.key_type });
|
|
137
|
+
} catch (err) {
|
|
138
|
+
return strike(res, 502, err.message);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Unknown route
|
|
143
|
+
return strike(res, 404, `Unknown endpoint: ${reqPath}`);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
return server;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── Actions ──────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
async function verifyAuth(password) {
|
|
152
|
+
const machineHash = getMachineHash();
|
|
153
|
+
const { token, timestamp } = deriveToken(password, machineHash);
|
|
154
|
+
const result = await api.test(password, machineHash, token, timestamp);
|
|
155
|
+
if (result.error) throw new Error(result.error);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function actionStart(opts) {
|
|
159
|
+
const port = parseInt(opts.port || "52437", 10);
|
|
160
|
+
const password = opts.pw;
|
|
161
|
+
const whitelist = opts.services
|
|
162
|
+
? opts.services.split(",").map(s => s.trim().toLowerCase())
|
|
163
|
+
: null;
|
|
164
|
+
|
|
165
|
+
// Check for existing instance
|
|
166
|
+
const existing = readPid();
|
|
167
|
+
if (existing && isProcessAlive(existing.pid)) {
|
|
168
|
+
console.log(chalk.yellow(`\n clauth serve already running (PID ${existing.pid}, port ${existing.port})`));
|
|
169
|
+
console.log(chalk.gray(` Stop it first: clauth serve stop\n`));
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// If we're the daemon child, run the server directly
|
|
174
|
+
if (process.env.__CLAUTH_DAEMON === "1") {
|
|
175
|
+
try {
|
|
176
|
+
await verifyAuth(password);
|
|
177
|
+
} catch (err) {
|
|
178
|
+
const msg = `[${new Date().toISOString()}] Auth failed: ${err.message}\n`;
|
|
179
|
+
fs.appendFileSync(LOG_FILE, msg);
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const server = createServer(password, whitelist, port);
|
|
184
|
+
server.listen(port, "127.0.0.1", () => {
|
|
185
|
+
writePid(process.pid, port);
|
|
186
|
+
const msg = `[${new Date().toISOString()}] clauth serve started — PID ${process.pid}, port ${port}, services: ${whitelist ? whitelist.join(",") : "all"}\n`;
|
|
187
|
+
fs.appendFileSync(LOG_FILE, msg);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
server.on("error", err => {
|
|
191
|
+
const msg = `[${new Date().toISOString()}] Server error: ${err.message}\n`;
|
|
192
|
+
fs.appendFileSync(LOG_FILE, msg);
|
|
193
|
+
process.exit(1);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const shutdown = () => { removePid(); process.exit(0); };
|
|
197
|
+
process.on("SIGTERM", shutdown);
|
|
198
|
+
process.on("SIGINT", shutdown);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Parent: verify auth first (show errors to user)
|
|
203
|
+
console.log(chalk.gray("\n Verifying vault credentials..."));
|
|
204
|
+
try {
|
|
205
|
+
await verifyAuth(password);
|
|
206
|
+
} catch (err) {
|
|
207
|
+
console.log(chalk.red(`\n Auth failed: ${err.message}\n`));
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
console.log(chalk.green(" ✓ Vault auth verified"));
|
|
211
|
+
|
|
212
|
+
// Spawn detached daemon child
|
|
213
|
+
const { spawn } = await import("child_process");
|
|
214
|
+
const { fileURLToPath } = await import("url");
|
|
215
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
216
|
+
|
|
217
|
+
// Build args: node serve.js --action start --port N --pw PW [--services S]
|
|
218
|
+
const childArgs = [__filename, "--action", "start", "--port", String(port), "--pw", password];
|
|
219
|
+
if (opts.services) childArgs.push("--services", opts.services);
|
|
220
|
+
|
|
221
|
+
const out = fs.openSync(LOG_FILE, "a");
|
|
222
|
+
const child = spawn(process.execPath, childArgs, {
|
|
223
|
+
detached: true,
|
|
224
|
+
stdio: ["ignore", out, out],
|
|
225
|
+
env: { ...process.env, __CLAUTH_DAEMON: "1" },
|
|
226
|
+
});
|
|
227
|
+
child.unref();
|
|
228
|
+
|
|
229
|
+
// Give it a moment then verify
|
|
230
|
+
await new Promise(r => setTimeout(r, 800));
|
|
231
|
+
|
|
232
|
+
const info = readPid();
|
|
233
|
+
if (info && isProcessAlive(info.pid)) {
|
|
234
|
+
console.log(chalk.green(`\n 🔐 clauth serve started`));
|
|
235
|
+
console.log(chalk.gray(` PID: ${info.pid}`));
|
|
236
|
+
console.log(chalk.gray(` Port: 127.0.0.1:${info.port}`));
|
|
237
|
+
console.log(chalk.gray(` Services: ${whitelist ? whitelist.join(", ") : "all"}`));
|
|
238
|
+
console.log(chalk.gray(` Log: ${LOG_FILE}`));
|
|
239
|
+
console.log(chalk.gray(` Stop: clauth serve stop\n`));
|
|
240
|
+
} else {
|
|
241
|
+
console.log(chalk.red(`\n ❌ Failed to start daemon — check ${LOG_FILE}\n`));
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function actionStop() {
|
|
247
|
+
const info = readPid();
|
|
248
|
+
if (!info) {
|
|
249
|
+
console.log(chalk.yellow("\n No clauth serve PID file found — not running.\n"));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (!isProcessAlive(info.pid)) {
|
|
254
|
+
console.log(chalk.yellow(`\n PID ${info.pid} is not running (stale PID file). Cleaning up.\n`));
|
|
255
|
+
removePid();
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Try HTTP shutdown first (clean)
|
|
260
|
+
try {
|
|
261
|
+
const resp = await fetch(`http://127.0.0.1:${info.port}/shutdown`);
|
|
262
|
+
if (resp.ok) {
|
|
263
|
+
await new Promise(r => setTimeout(r, 300));
|
|
264
|
+
console.log(chalk.green(`\n 🛑 clauth serve stopped (was PID ${info.pid}, port ${info.port})\n`));
|
|
265
|
+
removePid();
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
} catch {}
|
|
269
|
+
|
|
270
|
+
// Fallback: kill the process
|
|
271
|
+
try {
|
|
272
|
+
process.kill(info.pid, "SIGTERM");
|
|
273
|
+
await new Promise(r => setTimeout(r, 300));
|
|
274
|
+
console.log(chalk.green(`\n 🛑 clauth serve stopped via SIGTERM (PID ${info.pid})\n`));
|
|
275
|
+
} catch (err) {
|
|
276
|
+
console.log(chalk.yellow(`\n Could not kill PID ${info.pid}: ${err.message}\n`));
|
|
277
|
+
}
|
|
278
|
+
removePid();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function actionPing() {
|
|
282
|
+
const info = readPid();
|
|
283
|
+
if (!info) {
|
|
284
|
+
console.log(chalk.red("\n clauth serve is not running (no PID file)\n"));
|
|
285
|
+
process.exit(1);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (!isProcessAlive(info.pid)) {
|
|
289
|
+
console.log(chalk.red(`\n PID ${info.pid} is not alive (stale PID file)\n`));
|
|
290
|
+
removePid();
|
|
291
|
+
process.exit(1);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
const resp = await fetch(`http://127.0.0.1:${info.port}/ping`);
|
|
296
|
+
const data = await resp.json();
|
|
297
|
+
if (data.status === "ok") {
|
|
298
|
+
console.log(chalk.green(`\n ✅ clauth serve running`));
|
|
299
|
+
console.log(chalk.gray(` PID: ${info.pid}`));
|
|
300
|
+
console.log(chalk.gray(` Port: ${info.port}`));
|
|
301
|
+
console.log(chalk.gray(` Fails: ${data.failures}/${data.failures + data.failures_remaining}`));
|
|
302
|
+
console.log(chalk.gray(` Services: ${Array.isArray(data.services) ? data.services.join(", ") : data.services}\n`));
|
|
303
|
+
} else {
|
|
304
|
+
console.log(chalk.yellow(`\n PID alive but /ping returned unexpected response\n`));
|
|
305
|
+
}
|
|
306
|
+
} catch (err) {
|
|
307
|
+
console.log(chalk.yellow(`\n PID ${info.pid} alive but HTTP failed: ${err.message}\n`));
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function actionRestart(opts) {
|
|
312
|
+
const info = readPid();
|
|
313
|
+
if (info && isProcessAlive(info.pid)) {
|
|
314
|
+
await actionStop();
|
|
315
|
+
await new Promise(r => setTimeout(r, 500));
|
|
316
|
+
}
|
|
317
|
+
await actionStart(opts);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function actionForeground(opts) {
|
|
321
|
+
const port = parseInt(opts.port || "52437", 10);
|
|
322
|
+
const password = opts.pw;
|
|
323
|
+
const whitelist = opts.services
|
|
324
|
+
? opts.services.split(",").map(s => s.trim().toLowerCase())
|
|
325
|
+
: null;
|
|
326
|
+
|
|
327
|
+
console.log(chalk.gray("\n Verifying vault credentials..."));
|
|
328
|
+
try {
|
|
329
|
+
await verifyAuth(password);
|
|
330
|
+
} catch (err) {
|
|
331
|
+
console.log(chalk.red(`\n Auth failed: ${err.message}\n`));
|
|
332
|
+
process.exit(1);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
console.log(chalk.green(" ✓ Vault auth verified"));
|
|
336
|
+
console.log(chalk.gray(` Port: 127.0.0.1:${port}`));
|
|
337
|
+
console.log(chalk.gray(` Services: ${whitelist ? whitelist.join(", ") : "all"}`));
|
|
338
|
+
console.log(chalk.gray(` Lockout: 3 failures → exit\n`));
|
|
339
|
+
|
|
340
|
+
const server = createServer(password, whitelist, port);
|
|
341
|
+
server.listen(port, "127.0.0.1", () => {
|
|
342
|
+
writePid(process.pid, port);
|
|
343
|
+
console.log(chalk.green(` clauth serve → http://127.0.0.1:${port}`));
|
|
344
|
+
console.log(chalk.gray(" Ctrl+C to stop\n"));
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
server.on("error", err => {
|
|
348
|
+
if (err.code === "EADDRINUSE") {
|
|
349
|
+
console.log(chalk.red(`\n Port ${port} already in use. Use --port to choose another.\n`));
|
|
350
|
+
} else {
|
|
351
|
+
console.log(chalk.red(`\n Server error: ${err.message}\n`));
|
|
352
|
+
}
|
|
353
|
+
process.exit(1);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
process.on("SIGINT", () => {
|
|
357
|
+
console.log(chalk.yellow("\n Stopping clauth serve...\n"));
|
|
358
|
+
removePid();
|
|
359
|
+
server.close(() => process.exit(0));
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ── Export ────────────────────────────────────────────────────
|
|
364
|
+
export async function runServe(opts) {
|
|
365
|
+
const action = opts.action || "foreground";
|
|
366
|
+
|
|
367
|
+
switch (action) {
|
|
368
|
+
case "start": return actionStart(opts);
|
|
369
|
+
case "stop": return actionStop();
|
|
370
|
+
case "restart": return actionRestart(opts);
|
|
371
|
+
case "ping": return actionPing();
|
|
372
|
+
case "foreground": return actionForeground(opts);
|
|
373
|
+
default:
|
|
374
|
+
console.log(chalk.red(`\n Unknown serve action: ${action}`));
|
|
375
|
+
console.log(chalk.gray(" Actions: start | stop | restart | ping | foreground\n"));
|
|
376
|
+
process.exit(1);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// cli/commands/uninstall.js
|
|
2
|
+
// clauth uninstall — full teardown: DB objects, Edge Function, secrets, skill, local config
|
|
3
|
+
//
|
|
4
|
+
// Reverses everything `clauth install` does:
|
|
5
|
+
// 1. Drops clauth tables, policies, triggers, functions from Supabase
|
|
6
|
+
// 2. Deletes auth-vault Edge Function
|
|
7
|
+
// 3. Removes CLAUTH_* secrets
|
|
8
|
+
// 4. Removes Claude skill directory
|
|
9
|
+
// 5. Clears local config (Conf store)
|
|
10
|
+
|
|
11
|
+
import { existsSync, rmSync } from 'fs';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
import Conf from 'conf';
|
|
14
|
+
import chalk from 'chalk';
|
|
15
|
+
import ora from 'ora';
|
|
16
|
+
|
|
17
|
+
const MGMT = 'https://api.supabase.com/v1';
|
|
18
|
+
const SKILLS_DIR = process.env.CLAUTH_SKILLS_DIR ||
|
|
19
|
+
(process.platform === 'win32'
|
|
20
|
+
? join(process.env.USERPROFILE || '', '.claude', 'skills')
|
|
21
|
+
: join(process.env.HOME || '', '.claude', 'skills'));
|
|
22
|
+
|
|
23
|
+
// ─────────────────────────────────────────────
|
|
24
|
+
// Supabase Management API helper
|
|
25
|
+
// ─────────────────────────────────────────────
|
|
26
|
+
async function mgmt(pat, method, path, body) {
|
|
27
|
+
const res = await fetch(`${MGMT}${path}`, {
|
|
28
|
+
method,
|
|
29
|
+
headers: { 'Authorization': `Bearer ${pat}`, 'Content-Type': 'application/json' },
|
|
30
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
31
|
+
});
|
|
32
|
+
if (!res.ok) {
|
|
33
|
+
const text = await res.text().catch(() => res.statusText);
|
|
34
|
+
throw new Error(`${method} ${path} → HTTP ${res.status}: ${text}`);
|
|
35
|
+
}
|
|
36
|
+
if (res.status === 204) return {};
|
|
37
|
+
const text = await res.text();
|
|
38
|
+
if (!text) return {};
|
|
39
|
+
return JSON.parse(text);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─────────────────────────────────────────────
|
|
43
|
+
// Main uninstall command
|
|
44
|
+
// ─────────────────────────────────────────────
|
|
45
|
+
export async function runUninstall(opts = {}) {
|
|
46
|
+
console.log(chalk.red('\n🗑️ clauth uninstall\n'));
|
|
47
|
+
|
|
48
|
+
const config = new Conf({ projectName: 'clauth' });
|
|
49
|
+
|
|
50
|
+
// ── Collect credentials ────────────────────
|
|
51
|
+
const ref = opts.ref || config.get('supabase_url')?.match(/https:\/\/(.+)\.supabase\.co/)?.[1];
|
|
52
|
+
const pat = opts.pat;
|
|
53
|
+
|
|
54
|
+
if (!ref) {
|
|
55
|
+
console.log(chalk.red(' Cannot determine Supabase project ref.'));
|
|
56
|
+
console.log(chalk.gray(' Use: clauth uninstall --ref <project-ref> --pat <personal-access-token>'));
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
if (!pat) {
|
|
60
|
+
console.log(chalk.red(' Supabase PAT required for teardown.'));
|
|
61
|
+
console.log(chalk.gray(' Use: clauth uninstall --ref <project-ref> --pat <personal-access-token>'));
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log(chalk.gray(` Project: ${ref}\n`));
|
|
66
|
+
|
|
67
|
+
// ── Step 1: Drop database objects ──────────
|
|
68
|
+
const s1 = ora('Dropping clauth database objects...').start();
|
|
69
|
+
const teardownSQL = `
|
|
70
|
+
-- Drop triggers
|
|
71
|
+
DROP TRIGGER IF EXISTS clauth_services_updated ON public.clauth_services;
|
|
72
|
+
|
|
73
|
+
-- Drop tables (CASCADE drops policies automatically)
|
|
74
|
+
DROP TABLE IF EXISTS public.clauth_audit CASCADE;
|
|
75
|
+
DROP TABLE IF EXISTS public.clauth_machines CASCADE;
|
|
76
|
+
DROP TABLE IF EXISTS public.clauth_services CASCADE;
|
|
77
|
+
|
|
78
|
+
-- Drop functions
|
|
79
|
+
DROP FUNCTION IF EXISTS public.clauth_touch_updated() CASCADE;
|
|
80
|
+
DROP FUNCTION IF EXISTS public.clauth_upsert_vault_secret(text, text) CASCADE;
|
|
81
|
+
DROP FUNCTION IF EXISTS public.clauth_get_vault_secret(text) CASCADE;
|
|
82
|
+
DROP FUNCTION IF EXISTS public.clauth_delete_vault_secret(text) CASCADE;
|
|
83
|
+
`;
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
await mgmt(pat, 'POST', `/projects/${ref}/database/query`, { query: teardownSQL });
|
|
87
|
+
s1.succeed('Database objects dropped (tables, triggers, functions, policies)');
|
|
88
|
+
} catch (e) {
|
|
89
|
+
s1.fail(`Database teardown failed: ${e.message}`);
|
|
90
|
+
console.log(chalk.yellow(' You may need to drop objects manually via SQL editor.'));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Step 2: Delete Edge Function ───────────
|
|
94
|
+
const s2 = ora('Deleting auth-vault Edge Function...').start();
|
|
95
|
+
try {
|
|
96
|
+
const res = await fetch(`${MGMT}/projects/${ref}/functions/auth-vault`, {
|
|
97
|
+
method: 'DELETE',
|
|
98
|
+
headers: { 'Authorization': `Bearer ${pat}` },
|
|
99
|
+
});
|
|
100
|
+
if (res.ok || res.status === 404) {
|
|
101
|
+
s2.succeed(res.status === 404
|
|
102
|
+
? 'Edge Function not found (already deleted)'
|
|
103
|
+
: 'Edge Function deleted');
|
|
104
|
+
} else {
|
|
105
|
+
const text = await res.text().catch(() => res.statusText);
|
|
106
|
+
throw new Error(`HTTP ${res.status}: ${text}`);
|
|
107
|
+
}
|
|
108
|
+
} catch (e) {
|
|
109
|
+
s2.fail(`Edge Function delete failed: ${e.message}`);
|
|
110
|
+
console.log(chalk.yellow(' Delete manually: Supabase Dashboard → Edge Functions → auth-vault → Delete'));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── Step 3: Remove secrets ─────────────────
|
|
114
|
+
const s3 = ora('Removing clauth secrets...').start();
|
|
115
|
+
try {
|
|
116
|
+
// Supabase Management API: DELETE /projects/{ref}/secrets with body listing secret names
|
|
117
|
+
const res = await fetch(`${MGMT}/projects/${ref}/secrets`, {
|
|
118
|
+
method: 'DELETE',
|
|
119
|
+
headers: { 'Authorization': `Bearer ${pat}`, 'Content-Type': 'application/json' },
|
|
120
|
+
body: JSON.stringify(['CLAUTH_HMAC_SALT', 'CLAUTH_ADMIN_BOOTSTRAP_TOKEN']),
|
|
121
|
+
});
|
|
122
|
+
if (res.ok) {
|
|
123
|
+
s3.succeed('Secrets removed (CLAUTH_HMAC_SALT, CLAUTH_ADMIN_BOOTSTRAP_TOKEN)');
|
|
124
|
+
} else {
|
|
125
|
+
const text = await res.text().catch(() => res.statusText);
|
|
126
|
+
throw new Error(`HTTP ${res.status}: ${text}`);
|
|
127
|
+
}
|
|
128
|
+
} catch (e) {
|
|
129
|
+
s3.warn(`Secret removal failed: ${e.message}`);
|
|
130
|
+
console.log(chalk.yellow(' Remove manually: Supabase → Settings → Edge Functions → Secrets'));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── Step 4: Remove Claude skill ────────────
|
|
134
|
+
const s4 = ora('Removing Claude skill...').start();
|
|
135
|
+
const skillDir = join(SKILLS_DIR, 'clauth');
|
|
136
|
+
if (existsSync(skillDir)) {
|
|
137
|
+
try {
|
|
138
|
+
rmSync(skillDir, { recursive: true, force: true });
|
|
139
|
+
s4.succeed(`Skill removed: ${skillDir}`);
|
|
140
|
+
} catch (e) {
|
|
141
|
+
s4.warn(`Could not remove skill: ${e.message}`);
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
s4.succeed('Skill directory not found (already removed)');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── Step 5: Clear local config ─────────────
|
|
148
|
+
const s5 = ora('Clearing local config...').start();
|
|
149
|
+
try {
|
|
150
|
+
config.clear();
|
|
151
|
+
s5.succeed('Local config cleared');
|
|
152
|
+
} catch (e) {
|
|
153
|
+
s5.warn(`Could not clear config: ${e.message}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── Done ───────────────────────────────────
|
|
157
|
+
console.log('');
|
|
158
|
+
console.log(chalk.red('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
159
|
+
console.log(chalk.yellow(' ✓ clauth fully uninstalled'));
|
|
160
|
+
console.log(chalk.red('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
161
|
+
console.log('');
|
|
162
|
+
console.log(chalk.gray(' To reinstall: npx @lifeaitools/clauth install'));
|
|
163
|
+
console.log('');
|
|
164
|
+
}
|
package/cli/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import * as api from "./api.js";
|
|
|
11
11
|
import os from "os";
|
|
12
12
|
|
|
13
13
|
const config = new Conf({ projectName: "clauth" });
|
|
14
|
-
const VERSION = "0.
|
|
14
|
+
const VERSION = "0.3.0";
|
|
15
15
|
|
|
16
16
|
// ============================================================
|
|
17
17
|
// Password prompt helper
|
|
@@ -51,6 +51,9 @@ program
|
|
|
51
51
|
// clauth install (Supabase provisioning + skill install + test)
|
|
52
52
|
// ──────────────────────────────────────────────
|
|
53
53
|
import { runInstall } from './commands/install.js';
|
|
54
|
+
import { runUninstall } from './commands/uninstall.js';
|
|
55
|
+
import { runScrub } from './commands/scrub.js';
|
|
56
|
+
import { runServe } from './commands/serve.js';
|
|
54
57
|
|
|
55
58
|
program
|
|
56
59
|
.command('install')
|
|
@@ -61,6 +64,28 @@ program
|
|
|
61
64
|
await runInstall(opts);
|
|
62
65
|
});
|
|
63
66
|
|
|
67
|
+
program
|
|
68
|
+
.command('uninstall')
|
|
69
|
+
.description('Full teardown — drop DB objects, Edge Function, secrets, skill, config')
|
|
70
|
+
.option('--ref <ref>', 'Supabase project ref')
|
|
71
|
+
.option('--pat <pat>', 'Supabase Personal Access Token (required)')
|
|
72
|
+
.option('--yes', 'Skip confirmation prompt')
|
|
73
|
+
.action(async (opts) => {
|
|
74
|
+
if (!opts.yes) {
|
|
75
|
+
const inquirerMod = await import('inquirer');
|
|
76
|
+
const { confirm } = await inquirerMod.default.prompt([{
|
|
77
|
+
type: 'input',
|
|
78
|
+
name: 'confirm',
|
|
79
|
+
message: chalk.red('Type "CONFIRM UNINSTALL" to proceed:'),
|
|
80
|
+
}]);
|
|
81
|
+
if (confirm !== 'CONFIRM UNINSTALL') {
|
|
82
|
+
console.log(chalk.yellow('\n Uninstall cancelled.\n'));
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
await runUninstall(opts);
|
|
87
|
+
});
|
|
88
|
+
|
|
64
89
|
// ──────────────────────────────────────────────
|
|
65
90
|
// clauth setup
|
|
66
91
|
// ──────────────────────────────────────────────
|
|
@@ -273,11 +298,17 @@ addCmd
|
|
|
273
298
|
.option("-p, --pw <password>")
|
|
274
299
|
.action(async (name, opts) => {
|
|
275
300
|
const auth = await getAuth(opts.pw);
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
{
|
|
280
|
-
|
|
301
|
+
let answers;
|
|
302
|
+
if (opts.type && opts.label) {
|
|
303
|
+
// Non-interactive — all flags provided
|
|
304
|
+
answers = { label: opts.label, key_type: opts.type, desc: opts.description || "" };
|
|
305
|
+
} else {
|
|
306
|
+
answers = await inquirer.prompt([
|
|
307
|
+
{ type: "input", name: "label", message: "Label:", default: opts.label || name },
|
|
308
|
+
{ type: "list", name: "key_type", message: "Key type:", choices: ["token","keypair","connstring","oauth"], default: opts.type || "token" },
|
|
309
|
+
{ type: "input", name: "desc", message: "Description (optional):", default: opts.description || "" }
|
|
310
|
+
]);
|
|
311
|
+
}
|
|
281
312
|
const spinner = ora(`Adding service: ${name}...`).start();
|
|
282
313
|
try {
|
|
283
314
|
const result = await api.addService(
|
|
@@ -396,6 +427,24 @@ program
|
|
|
396
427
|
} catch (err) { spinner.fail(chalk.red(err.message)); }
|
|
397
428
|
});
|
|
398
429
|
|
|
430
|
+
// ──────────────────────────────────────────────
|
|
431
|
+
// clauth scrub [target]
|
|
432
|
+
// ──────────────────────────────────────────────
|
|
433
|
+
program
|
|
434
|
+
.command("scrub [target]")
|
|
435
|
+
.description("Scrub credentials from Claude Code transcript logs (no auth required)")
|
|
436
|
+
.option("--force", "Rescrub files even if already marked clean")
|
|
437
|
+
.addHelpText("after", `
|
|
438
|
+
Examples:
|
|
439
|
+
clauth scrub Scrub the most recent (active) transcript
|
|
440
|
+
clauth scrub <file> Scrub a specific .jsonl file
|
|
441
|
+
clauth scrub all Scrub every transcript in ~/.claude/projects/
|
|
442
|
+
clauth scrub all --force Rescrub all files (ignore markers)
|
|
443
|
+
`)
|
|
444
|
+
.action(async (target, opts) => {
|
|
445
|
+
await runScrub(target, opts);
|
|
446
|
+
});
|
|
447
|
+
|
|
399
448
|
// ──────────────────────────────────────────────
|
|
400
449
|
// clauth --help override banner
|
|
401
450
|
// ──────────────────────────────────────────────
|
|
@@ -409,4 +458,43 @@ program.addHelpText("beforeAll", chalk.cyan(`
|
|
|
409
458
|
v${VERSION} — LIFEAI Credential Vault
|
|
410
459
|
`));
|
|
411
460
|
|
|
461
|
+
// ──────────────────────────────────────────────
|
|
462
|
+
// clauth serve [action]
|
|
463
|
+
// ──────────────────────────────────────────────
|
|
464
|
+
program
|
|
465
|
+
.command("serve [action]")
|
|
466
|
+
.description("Manage localhost HTTP vault daemon (start|stop|restart|ping)")
|
|
467
|
+
.option("--port <n>", "Port (default: 52437)")
|
|
468
|
+
.option("-p, --pw <password>", "clauth password (required for start/restart)")
|
|
469
|
+
.option("--services <list>", "Comma-separated service whitelist (default: all)")
|
|
470
|
+
.option("--action <action>", "Internal: action override for daemon child")
|
|
471
|
+
.addHelpText("after", `
|
|
472
|
+
Actions:
|
|
473
|
+
start Start the server as a background daemon
|
|
474
|
+
stop Stop the running daemon
|
|
475
|
+
restart Stop + start
|
|
476
|
+
ping Check if the daemon is running
|
|
477
|
+
foreground Run in foreground (Ctrl+C to stop) — default if no action given
|
|
478
|
+
|
|
479
|
+
Examples:
|
|
480
|
+
clauth serve -p mypass Run in foreground (original behavior)
|
|
481
|
+
clauth serve start -p mypass Start as background daemon
|
|
482
|
+
clauth serve stop Stop the daemon
|
|
483
|
+
clauth serve ping Check status
|
|
484
|
+
clauth serve restart -p mypass Restart the daemon
|
|
485
|
+
clauth serve start --services github,vercel -p mypass
|
|
486
|
+
`)
|
|
487
|
+
.action(async (action, opts) => {
|
|
488
|
+
const resolvedAction = opts.action || action || "foreground";
|
|
489
|
+
|
|
490
|
+
// stop and ping don't need a password
|
|
491
|
+
if (!["stop", "ping"].includes(resolvedAction) && !opts.pw) {
|
|
492
|
+
console.log(chalk.red("\n --pw is required for serve mode\n"));
|
|
493
|
+
console.log(chalk.gray(" Example: clauth serve start --pw yourpassword\n"));
|
|
494
|
+
process.exit(1);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
await runServe({ ...opts, action: resolvedAction });
|
|
498
|
+
});
|
|
499
|
+
|
|
412
500
|
program.parse(process.argv);
|